diff --git a/.github/actions/flutter_build/action.yml b/.github/actions/flutter_build/action.yml index bfcb501327..66bfce44a5 100644 --- a/.github/actions/flutter_build/action.yml +++ b/.github/actions/flutter_build/action.yml @@ -58,24 +58,19 @@ runs: - name: Install prerequisites working-directory: frontend - shell: bash run: | - case $RUNNER_OS in - Linux) - sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub - sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list - sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev - ;; - Windows) - vcpkg integrate install - vcpkg update - ;; - macOS) - # No additional prerequisites needed for macOS - ;; - esac + if [ "$RUNNER_OS" == "Linux" ]; then + sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub + sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list + sudo apt-get update + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv + elif [ "$RUNNER_OS" == "Windows" ]; then + vcpkg integrate install + elif [ "$RUNNER_OS" == "macOS" ]; then + echo 'do nothing' + fi cargo make appflowy-flutter-deps-tools + shell: bash - name: Build AppFlowy working-directory: frontend @@ -99,4 +94,4 @@ runs: - uses: actions/upload-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} - path: appflowy_flutter.tar.gz + path: appflowy_flutter.tar.gz \ No newline at end of file diff --git a/.github/actions/flutter_integration_test/action.yml b/.github/actions/flutter_integration_test/action.yml index e0fa508ade..63066e0f38 100644 --- a/.github/actions/flutter_integration_test/action.yml +++ b/.github/actions/flutter_integration_test/action.yml @@ -52,7 +52,7 @@ runs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev network-manager libmpv-dev mpv shell: bash - name: Enable Flutter Desktop @@ -75,4 +75,4 @@ runs: sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & sudo apt-get install network-manager flutter test ${{ inputs.test_path }} -d Linux --coverage - shell: bash + shell: bash \ No newline at end of file diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak index 81e132cbf8..0cb110bae4 100644 --- a/.github/workflows/android_ci.yaml.bak +++ b/.github/workflows/android_ci.yaml.bak @@ -1,196 +1,126 @@ -name: Android CI +# name: Android CI -on: - push: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" +# on: +# push: +# branches: +# - "main" +# paths: +# - ".github/workflows/mobile_ci.yaml" +# - "frontend/**" +# - "!frontend/appflowy_tauri/**" - pull_request: - branches: - - "main" - paths: - - ".github/workflows/mobile_ci.yaml" - - "frontend/**" - - "!frontend/appflowy_tauri/**" +# pull_request: +# branches: +# - "main" +# paths: +# - ".github/workflows/mobile_ci.yaml" +# - "frontend/**" +# - "!frontend/appflowy_tauri/**" -env: - CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" - CARGO_MAKE_VERSION: "0.37.18" - CLOUD_VERSION: 0.6.54-amd64 +# env: +# CARGO_TERM_COLOR: always +# FLUTTER_VERSION: "3.22.0" +# RUST_TOOLCHAIN: "1.77.2" +# CARGO_MAKE_VERSION: "0.36.6" -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +# 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: [ubuntu-latest] - runs-on: ${{ matrix.os }} +# jobs: +# build: +# if: github.event.pull_request.draft != true +# strategy: +# fail-fast: true +# matrix: +# os: [macos-14] +# runs-on: ${{ matrix.os }} - steps: - - name: Check storage space - run: - df -h +# steps: +# - name: Check storage space +# run: df -h - # the following step is required to avoid running out of space - - name: Maximize build space - if: matrix.os == 'ubuntu-latest' - run: | - sudo rm -rf /usr/share/dotnet - sudo rm -rf /opt/ghc - sudo rm -rf "/usr/local/share/boost" - sudo rm -rf "$AGENT_TOOLSDIRECTORY" - sudo docker image prune --all --force - sudo rm -rf /opt/hostedtoolcache/codeQL - sudo rm -rf ${GITHUB_WORKSPACE}/.git +# # the following step is required to avoid running out of space +# - name: Maximize build space +# if: matrix.os == 'ubuntu-latest' +# run: | +# sudo rm -rf /usr/share/dotnet +# sudo rm -rf /opt/ghc +# sudo rm -rf "/usr/local/share/boost" +# sudo rm -rf "$AGENT_TOOLSDIRECTORY" +# sudo docker image prune --all --force +# sudo rm -rf /opt/hostedtoolcache/codeQL +# sudo rm -rf ${GITHUB_WORKSPACE}/.git +# sudo rm -rf $ANDROID_HOME/ndk - - name: Check storage space - run: df -h +# - name: Check storage space +# run: df -h - - name: Checkout appflowy cloud code - uses: actions/checkout@v4 - with: - repository: AppFlowy-IO/AppFlowy-Cloud - path: AppFlowy-Cloud +# - name: Checkout source code +# uses: actions/checkout@v4 - - 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/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env - sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env - sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env +# - uses: actions/setup-java@v4 +# with: +# distribution: temurin +# java-version: 11 - - name: Run Docker-Compose - working-directory: AppFlowy-Cloud - env: - APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }} - 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. Restarting with the correct version..." - # Remove all containers if any exist - if [ "$(docker ps -aq)" ]; then - docker rm -f $(docker ps -aq) - else - echo "No containers to remove." - fi +# - name: Install Rust toolchain +# id: rust_toolchain +# uses: actions-rs/toolchain@v1 +# with: +# toolchain: ${{ env.RUST_TOOLCHAIN }} +# override: true +# profile: minimal - # Remove all volumes if any exist - if [ "$(docker volume ls -q)" ]; then - docker volume rm $(docker volume ls -q) - else - echo "No volumes to remove." - fi - docker compose pull - docker compose up -d - echo "Waiting for the container to be ready..." - sleep 10 - docker ps -a - docker compose logs - else - echo "AppFlowy-Cloud is running with the correct version." - fi - fi +# - name: Install flutter +# id: flutter +# uses: subosito/flutter-action@v2 +# with: +# channel: "stable" +# flutter-version: ${{ env.FLUTTER_VERSION }} - - name: Checkout source code - uses: actions/checkout@v4 +# - uses: gradle/gradle-build-action@v3 +# with: +# gradle-version: 7.4.2 - - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 11 +# - uses: davidB/rust-cargo-make@v1 +# with: +# version: "0.36.6" - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - override: true - profile: minimal +# - name: Install prerequisites +# working-directory: frontend +# run: | +# rustup target install aarch64-linux-android +# rustup target install x86_64-linux-android +# cargo install --force duckscript_cli +# cargo install cargo-ndk +# if [ "$RUNNER_OS" == "Linux" ]; then +# sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub +# sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list +# sudo apt-get update +# sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev +# sudo apt-get install keybinder-3.0 libnotify-dev +# sudo apt-get install gcc-multilib +# elif [ "$RUNNER_OS" == "Windows" ]; then +# vcpkg integrate install +# elif [ "$RUNNER_OS" == "macOS" ]; then +# echo 'do nothing' +# fi +# cargo make appflowy-flutter-deps-tools +# shell: bash - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: ${{ env.FLUTTER_VERSION }} +# - name: Build AppFlowy +# working-directory: frontend +# run: | +# cargo make --profile development-android appflowy-android-dev-ci - - uses: gradle/gradle-build-action@v3 - with: - gradle-version: 8.10 - - uses: davidB/rust-cargo-make@v1 - with: - version: ${{ env.CARGO_MAKE_VERSION }} - - - name: Install prerequisites - working-directory: frontend - run: | - rustup target install aarch64-linux-android - rustup target install x86_64-linux-android - rustup target add armv7-linux-androideabi - cargo install --force --locked duckscript_cli - cargo install cargo-ndk - if [ "$RUNNER_OS" == "Linux" ]; then - sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub - sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list - sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev - sudo apt-get install keybinder-3.0 libnotify-dev - sudo apt-get install gcc-multilib - elif [ "$RUNNER_OS" == "Windows" ]; then - vcpkg integrate install - elif [ "$RUNNER_OS" == "macOS" ]; then - echo 'do nothing' - fi - cargo make appflowy-flutter-deps-tools - shell: bash - - - name: Build AppFlowy - working-directory: frontend - run: | - cargo make --profile development-android appflowy-core-dev-android - cargo make --profile development-android code_generation - cd rust-lib - cargo clean - - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Run integration tests - # https://github.com/ReactiveCircus/android-emulator-runner - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 33 - arch: x86_64 - disk-size: 2048M - working-directory: frontend/appflowy_flutter - disable-animations: true - force-avd-creation: false - target: google_apis - script: flutter test integration_test/mobile/cloud/cloud_runner.dart +# - name: Run integration tests +# # https://github.com/ReactiveCircus/android-emulator-runner +# uses: reactivecircus/android-emulator-runner@v2 +# with: +# api-level: 32 +# arch: arm64-v8a +# disk-size: 2048M +# working-directory: frontend/appflowy_flutter +# script: flutter test integration_test/runner.dart \ No newline at end of file diff --git a/.github/workflows/deploy_test_web.yaml b/.github/workflows/deploy_test_web.yaml new file mode 100644 index 0000000000..f7d3339d77 --- /dev/null +++ b/.github/workflows/deploy_test_web.yaml @@ -0,0 +1,72 @@ +name: Deploy Web (Test) + +on: + push: + branches: + - build/test +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + env: + SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }} + REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }} + REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }} + SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }} + SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }} + ENV_FILE: test.env + steps: + - uses: actions/checkout@v4 + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + - name: Node_modules cache + uses: actions/cache@v2 + with: + path: frontend/appflowy_web_app/node_modules + key: node-modules-${{ runner.os }} + - name: install frontend dependencies + working-directory: frontend/appflowy_web_app + run: | + pnpm install + - name: copy env file + working-directory: frontend/appflowy_web_app + run: | + cp ${{ env.ENV_FILE }} .env + - name: test and lint + working-directory: frontend/appflowy_web_app + run: | + pnpm run lint + - name: build + working-directory: frontend/appflowy_web_app + run: | + pnpm run build + - name: generate SSL certificate + run: | + echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt + echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key + - name: Deploy to EC2 + uses: easingthemes/ssh-deploy@main + with: + SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }} + ARGS: "-rlgoDzvc -i" + SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key" + REMOTE_HOST: ${{ env.REMOTE_HOST }} + REMOTE_USER: ${{ env.REMOTE_USER }} + EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/" + SCRIPT_AFTER: | + docker build -t appflowy-web-app . + docker rm -f appflowy-web-app || true + docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index 51e8a2ac28..2c2143c1fe 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -2,10 +2,18 @@ name: Docker-CI on: push: - branches: [ "main", "release/*" ] + branches: + - main + - release/* + paths: + - frontend/** pull_request: - branches: [ "main", "release/*" ] - workflow_dispatch: + branches: + - main + - release/* + paths: + - frontend/** + types: [opened, synchronize, reopened, unlocked, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -19,29 +27,25 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - # cache the docker layers - # don't cache anything temporarly, because it always triggers "no space left on device" error - # - name: Cache Docker layers - # uses: actions/cache@v3 - # with: - # path: /tmp/.buildx-cache - # key: ${{ runner.os }}-buildx-${{ github.sha }} - # restore-keys: | - # ${{ runner.os }}-buildx- + - name: Set up Docker Compose + run: | + docker-compose --version || { + echo "Docker Compose not found, installing..." + sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose --version + } - name: Build the app - uses: docker/build-push-action@v5 - with: - context: . - file: ./frontend/scripts/docker-buildfiles/Dockerfile - push: false - # cache-from: type=local,src=/tmp/.buildx-cache - # cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - # - name: Move cache - # run: | - # rm -rf /tmp/.buildx-cache - # mv /tmp/.buildx-cache-new /tmp/.buildx-cache + shell: bash + run: | + set -eu -o pipefail + cd frontend/scripts/docker-buildfiles + docker-compose build --no-cache --progress=plain \ + | while read line; do \ + if [[ "$line" =~ ^Step[[:space:]] ]]; then \ + echo "$(date -u '+%H:%M:%S') | $line"; \ + else \ + echo "$line"; \ + fi; \ + done diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 1fc1b0e052..c1cdd7bc17 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -25,10 +25,9 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" - CARGO_MAKE_VERSION: "0.37.18" - CLOUD_VERSION: 0.6.54-amd64 + FLUTTER_VERSION: "3.22.0" + RUST_TOOLCHAIN: "1.77.2" + CARGO_MAKE_VERSION: "0.36.6" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -40,7 +39,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -74,7 +73,7 @@ jobs: strategy: fail-fast: true matrix: - os: [windows-latest] + os: [ windows-latest ] include: - os: windows-latest flutter_profile: development-windows-x86 @@ -101,7 +100,7 @@ jobs: strategy: fail-fast: true matrix: - os: [macos-latest] + os: [ macos-latest ] include: - os: macos-latest flutter_profile: development-mac-x86_64 @@ -123,12 +122,12 @@ jobs: flutter_profile: ${{ matrix.flutter_profile }} unit_test: - needs: [prepare-linux] + needs: [ prepare-linux ] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -174,7 +173,7 @@ jobs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv fi shell: bash @@ -217,11 +216,11 @@ jobs: shell: bash cloud_integration_test: - needs: [prepare-linux] + needs: [ prepare-linux ] strategy: fail-fast: false matrix: - os: [ubuntu-latest] + os: [ ubuntu-latest ] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -242,50 +241,17 @@ jobs: cp deploy.env .env sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env sed -i 's/GOTRUE_EXTERNAL_GOOGLE_ENABLED=.*/GOTRUE_EXTERNAL_GOOGLE_ENABLED=true/' .env - sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env - name: Run Docker-Compose working-directory: AppFlowy-Cloud env: - APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }} + BACKEND_VERSION: 0.3.24-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. Restarting with the correct version..." - # Remove all containers if any exist - if [ "$(docker ps -aq)" ]; then - docker rm -f $(docker ps -aq) - else - echo "No containers to remove." - fi - - # Remove all volumes if any exist - if [ "$(docker volume ls -q)" ]; then - docker volume rm $(docker volume ls -q) - else - echo "No volumes to remove." - fi - docker compose pull - docker compose up -d - echo "Waiting for the container to be ready..." - sleep 10 - docker ps -a - docker compose logs - else - echo "AppFlowy-Cloud is running with the correct version." - fi - fi + docker compose down -v --remove-orphans + docker compose pull + docker compose up -d + sleep 10 - name: Checkout source code uses: actions/checkout@v4 @@ -308,7 +274,7 @@ jobs: sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev + sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev keybinder-3.0 libnotify-dev libmpv-dev mpv shell: bash - name: Enable Flutter Desktop @@ -336,30 +302,96 @@ jobs: sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & sudo apt-get install network-manager docker ps -a - flutter test integration_test/desktop/cloud/cloud_runner.dart -d Linux --coverage + flutter test integration_test/cloud/cloud_runner.dart -d Linux --coverage shell: bash - integration_test: - needs: [prepare-linux] + # split the integration tests into different machines to minimize the time + integration_test_1: + needs: [ prepare-linux ] if: github.event.pull_request.draft != true strategy: fail-fast: false matrix: - os: [ubuntu-latest] - test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9] + os: [ ubuntu-latest ] include: - os: ubuntu-latest - target: "x86_64-unknown-linux-gnu" + target: 'x86_64-unknown-linux-gnu' runs-on: ${{ matrix.os }} steps: - name: Checkout source code uses: actions/checkout@v4 - - name: Flutter Integration Test ${{ matrix.test_number }} + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + + - name: Flutter Integration Test 1 uses: ./.github/actions/flutter_integration_test with: - test_path: integration_test/desktop_runner_${{ matrix.test_number }}.dart + test_path: integration_test/desktop_runner_1.dart flutter_version: ${{ env.FLUTTER_VERSION }} rust_toolchain: ${{ env.RUST_TOOLCHAIN }} cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} rust_target: ${{ matrix.target }} + + integration_test_2: + needs: [ prepare-linux ] + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + include: + - os: ubuntu-latest + target: 'x86_64-unknown-linux-gnu' + runs-on: ${{ matrix.os }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + + - name: Flutter Integration Test 2 + uses: ./.github/actions/flutter_integration_test + with: + test_path: integration_test/desktop_runner_2.dart + flutter_version: ${{ env.FLUTTER_VERSION }} + rust_toolchain: ${{ env.RUST_TOOLCHAIN }} + cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} + rust_target: ${{ matrix.target }} + + integration_test_3: + needs: [ prepare-linux ] + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest ] + include: + - os: ubuntu-latest + target: 'x86_64-unknown-linux-gnu' + runs-on: ${{ matrix.os }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + + - name: Install video dependency + run: | + sudo apt-get update + sudo apt-get -y install libmpv-dev mpv + shell: bash + + - name: Flutter Integration Test 3 + uses: ./.github/actions/flutter_integration_test + with: + test_path: integration_test/desktop_runner_3.dart + flutter_version: ${{ env.FLUTTER_VERSION }} + rust_toolchain: ${{ env.RUST_TOOLCHAIN }} + cargo_make_version: ${{ env.CARGO_MAKE_VERSION }} + rust_target: ${{ matrix.target }} \ No newline at end of file diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index e13863f4a7..d24eaed1f3 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -7,6 +7,7 @@ on: paths: - ".github/workflows/mobile_ci.yaml" - "frontend/**" + - "!frontend/appflowy_tauri/**" - "!frontend/appflowy_web_app/**" pull_request: @@ -15,46 +16,32 @@ on: paths: - ".github/workflows/mobile_ci.yaml" - "frontend/**" + - "!frontend/appflowy_tauri/**" - "!frontend/appflowy_web_app/**" env: - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" + FLUTTER_VERSION: "3.22.0" + RUST_TOOLCHAIN: "1.77.2" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - build-self-hosted: - if: github.event.pull_request.head.repo.full_name == github.repository - runs-on: self-hosted + build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: true + matrix: + os: [ macos-14 ] + runs-on: ${{ matrix.os }} steps: - name: Checkout source code uses: actions/checkout@v2 - - name: 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 - - integration-tests: - if: github.event.pull_request.head.repo.full_name != github.repository - runs-on: macos-latest - - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - name: Install Rust toolchain + id: rust_toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_TOOLCHAIN }} @@ -62,7 +49,8 @@ jobs: override: true profile: minimal - - name: Install Flutter + - name: Install flutter + id: flutter uses: subosito/flutter-action@v2 with: channel: "stable" @@ -71,19 +59,19 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - prefix-key: macos-latest + prefix-key: ${{ matrix.os }} workspaces: | frontend/rust-lib - uses: davidB/rust-cargo-make@v1 with: - version: "0.37.15" + version: "0.36.6" - name: Install prerequisites working-directory: frontend run: | rustup target install aarch64-apple-ios-sim - cargo install --force --locked duckscript_cli + cargo install --force duckscript_cli cargo install cargo-lipo cargo make appflowy-flutter-deps-tools shell: bash @@ -97,23 +85,19 @@ 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 - working-directory: frontend/appflowy_flutter - run: | - flutter run -d ${{ steps.simulator-action.outputs.udid }} & - pid=$! - sleep 500 - kill $pid - continue-on-error: true + # - name: Run AppFlowy on simulator + # working-directory: frontend/appflowy_flutter + # run: | + # flutter run -d ${{ steps.simulator-action.outputs.udid }} & + # pid=$! + # sleep 500 + # kill $pid + # continue-on-error: true - # Integration tests - - name: Run integration tests - working-directory: frontend/appflowy_flutter - # The integration tests are flaky and sometimes fail with "Connection timed out": - # Don't block the CI. If the tests fail, the CI will still pass. - # Instead, we're using Code Magic to re-run the tests to check if they pass. - continue-on-error: true - run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} + # enable it again if the 12 mins timeout is fixed + # - name: Run integration tests + # working-directory: frontend/appflowy_flutter + # run: flutter test integration_test/runner.dart -d ${{ steps.simulator-action.outputs.udid }} diff --git a/.github/workflows/mobile_ci.yml b/.github/workflows/mobile_ci.yml deleted file mode 100644 index 4606a67799..0000000000 --- a/.github/workflows/mobile_ci.yml +++ /dev/null @@ -1,83 +0,0 @@ -name: Mobile-CI - -on: - workflow_dispatch: - inputs: - branch: - description: "Branch to build" - required: true - default: "main" - workflow_id: - description: "Codemagic workflow ID" - required: true - default: "ios-workflow" - type: choice - options: - - ios-workflow - - android-workflow - -env: - CODEMAGIC_API_TOKEN: ${{ secrets.CODEMAGIC_API_TOKEN }} - APP_ID: "6731d2f427e7c816080c3674" - -jobs: - trigger-mobile-build: - runs-on: ubuntu-latest - steps: - - name: Trigger Codemagic Build - id: trigger_build - run: | - RESPONSE=$(curl -X POST \ - --header "Content-Type: application/json" \ - --header "x-auth-token: $CODEMAGIC_API_TOKEN" \ - --data '{ - "appId": "${{ env.APP_ID }}", - "workflowId": "${{ github.event.inputs.workflow_id }}", - "branch": "${{ github.event.inputs.branch }}" - }' \ - https://api.codemagic.io/builds) - - BUILD_ID=$(echo $RESPONSE | jq -r '.buildId') - echo "build_id=$BUILD_ID" >> $GITHUB_OUTPUT - echo "build_id=$BUILD_ID" - - - name: Wait for build and check status - id: check_status - run: | - while true; do - curl -X GET \ - --header "Content-Type: application/json" \ - --header "x-auth-token: $CODEMAGIC_API_TOKEN" \ - https://api.codemagic.io/builds/${{ steps.trigger_build.outputs.build_id }} > /tmp/response.json - - RESPONSE_WITHOUT_COMMAND=$(cat /tmp/response.json | jq 'walk(if type == "object" and has("subactions") then .subactions |= map(del(.command)) else . end)') - STATUS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.build.status') - - if [ "$STATUS" = "finished" ]; then - SUCCESS=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.success') - BUILD_URL=$(echo $RESPONSE_WITHOUT_COMMAND | jq -r '.buildUrl') - echo "status=$STATUS" >> $GITHUB_OUTPUT - echo "success=$SUCCESS" >> $GITHUB_OUTPUT - echo "build_url=$BUILD_URL" >> $GITHUB_OUTPUT - break - elif [ "$STATUS" = "failed" ]; then - echo "status=failed" >> $GITHUB_OUTPUT - break - fi - - sleep 60 - done - - - name: Slack Notification - uses: 8398a7/action-slack@v3 - if: always() - with: - status: ${{ steps.check_status.outputs.success == 'true' && 'success' || 'failure' }} - fields: repo,message,commit,author,action,eventName,ref,workflow,job,took - text: | - Mobile CI Build Result - Branch: ${{ github.event.inputs.branch }} - Workflow: ${{ github.event.inputs.workflow_id }} - Build URL: ${{ steps.check_status.outputs.build_url }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4582ffa74..3c589b2611 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,8 +6,8 @@ on: - "*" env: - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" + FLUTTER_VERSION: "3.22.0" + RUST_TOOLCHAIN: "1.77.2" jobs: create-release: @@ -73,8 +73,8 @@ jobs: working-directory: frontend run: | vcpkg integrate install - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli + cargo install --force cargo-make + cargo install --force duckscript_cli - name: Build Windows app working-directory: frontend @@ -135,7 +135,7 @@ jobs: fail-fast: false matrix: job: - - { target: x86_64-apple-darwin, os: macos-13, extra-build-args: "" } + - { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" } steps: - name: Checkout source code uses: actions/checkout@v4 @@ -158,8 +158,8 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli + cargo install --force cargo-make + cargo install --force duckscript_cli - name: Build AppFlowy working-directory: frontend @@ -232,10 +232,10 @@ jobs: matrix: job: - { - targets: "aarch64-apple-darwin,x86_64-apple-darwin", - os: macos-latest, - extra-build-args: "", - } + targets: "aarch64-apple-darwin,x86_64-apple-darwin", + os: macos-11, + extra-build-args: "", + } steps: - name: Checkout source code uses: actions/checkout@v4 @@ -256,8 +256,8 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli + cargo install --force cargo-make + cargo install --force duckscript_cli - name: Build AppFlowy working-directory: frontend @@ -336,12 +336,12 @@ jobs: matrix: job: - { - arch: x86_64, - target: x86_64-unknown-linux-gnu, - os: ubuntu-22.04, - extra-build-args: "", - flutter_profile: production-linux-x86_64, - } + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-args: "", + flutter_profile: production-linux-x86_64, + } steps: - name: Checkout source code uses: actions/checkout@v4 @@ -368,10 +368,10 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev sudo apt-get install keybinder-3.0 - sudo apt-get install -y alien libnotify-dev + sudo apt-get install -y alien libnotify-dev libmpv-dev mpv source $HOME/.cargo/env - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli + cargo install --force cargo-make + cargo install --force duckscript_cli rustup target add ${{ matrix.job.target }} - name: Install gcc-aarch64-linux-gnu @@ -479,24 +479,6 @@ jobs: cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max - notify-failure: - runs-on: ubuntu-latest - needs: - - build-for-macOS-x86_64 - - build-for-windows - - build-for-linux - if: failure() - steps: - - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - text: | - 🔴🔴🔴Workflow ${{ github.workflow }} in repository ${{ github.repository }} was failed 🔴🔴🔴. - fields: repo,message,author,eventName,ref,workflow - env: - SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }} - if: always() - notify-discord: runs-on: ubuntu-latest needs: diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 36c2e82064..f61925f9a6 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -8,28 +8,31 @@ on: - "release/*" paths: - "frontend/rust-lib/**" - - ".github/workflows/rust_ci.yaml" pull_request: branches: - "main" - "develop" - "release/*" + paths: + - "frontend/rust-lib/**" env: CARGO_TERM_COLOR: always - CLOUD_VERSION: 0.8.3-amd64 - RUST_TOOLCHAIN: "1.81.0" + RUST_TOOLCHAIN: "1.77.2" jobs: - ubuntu-job: + test-on-ubuntu: runs-on: ubuntu-latest steps: - - name: Set timezone for action - uses: szenius/set-timezone@v2.0 - with: - timezoneLinux: "US/Pacific" + # - 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 @@ -42,16 +45,23 @@ jobs: uses: actions/checkout@v4 - name: Install Rust toolchain + id: rust_toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ env.RUST_TOOLCHAIN }} override: true components: rustfmt, clippy profile: minimal + + - name: Install prerequisites + working-directory: frontend + run: | + cargo install --force cargo-make + cargo install --force duckscript_cli + - uses: Swatinem/rust-cache@v2 with: - prefix-key: ${{ runner.os }} - cache-on-failure: true + prefix-key: "ubuntu-latest" workspaces: | frontend/rust-lib @@ -64,38 +74,18 @@ jobs: - name: Prepare appflowy cloud env working-directory: AppFlowy-Cloud run: | + # log level cp deploy.env .env sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env - sed -i 's|GOTRUE_MAILER_AUTOCONFIRM=.*|GOTRUE_MAILER_AUTOCONFIRM=true|' .env sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env - - name: Ensure AppFlowy-Cloud is Running with Correct Version + - name: Run Docker-Compose working-directory: AppFlowy-Cloud env: - APPFLOWY_CLOUD_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_HISTORY_VERSION: ${{ env.CLOUD_VERSION }} - APPFLOWY_WORKER_VERSION: ${{ env.CLOUD_VERSION }} + BACKEND_VERSION: 0.3.24-amd64 run: | - # Remove all containers if any exist - if [ "$(docker ps -aq)" ]; then - docker rm -f $(docker ps -aq) - else - echo "No containers to remove." - fi - - # Remove all volumes if any exist - if [ "$(docker volume ls -q)" ]; then - docker volume rm $(docker volume ls -q) - else - echo "No volumes to remove." - fi - - docker compose pull + docker pull appflowyinc/appflowy_cloud:latest docker compose up -d - echo "Waiting for the container to be ready..." - sleep 10 - docker ps -a - docker compose logs - name: Run rust-lib tests working-directory: frontend/rust-lib @@ -116,12 +106,6 @@ jobs: run: cargo clippy --all-targets -- -D warnings working-directory: frontend/rust-lib - - name: "Debug: show Appflowy-Cloud container logs" - if: failure() - working-directory: AppFlowy-Cloud - run: | - docker compose logs appflowy_cloud - - name: Clean up Docker images run: | docker image prune -af diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 53a5f66748..12e728698f 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -10,8 +10,8 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.27.4" - RUST_TOOLCHAIN: "1.81.0" + FLUTTER_VERSION: "3.22.0" + RUST_TOOLCHAIN: "1.77.2" jobs: tests: @@ -40,8 +40,8 @@ jobs: - name: Install prerequisites working-directory: frontend run: | - cargo install --force --locked cargo-make - cargo install --force --locked duckscript_cli + cargo install --force cargo-make + cargo install --force duckscript_cli - uses: Swatinem/rust-cache@v2 with: diff --git a/.github/workflows/tauri2_ci.yaml b/.github/workflows/tauri2_ci.yaml new file mode 100644 index 0000000000..28414f0997 --- /dev/null +++ b/.github/workflows/tauri2_ci.yaml @@ -0,0 +1,113 @@ +name: Tauri-CI +on: + pull_request: + paths: + - ".github/workflows/tauri2_ci.yaml" + - "frontend/rust-lib/**" + - "frontend/appflowy_web_app/**" + - "frontend/resources/**" + +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.77.2" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + tauri-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-20.04 ] + + runs-on: ${{ matrix.platform }} + + env: + CI: true + steps: + - uses: actions/checkout@v4 + + - name: Maximize build space (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./frontend/appflowy_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' + working-directory: frontend + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: install cargo-make + working-directory: frontend + run: | + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + + - 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" \ No newline at end of file diff --git a/.github/workflows/tauri_ci.yaml b/.github/workflows/tauri_ci.yaml new file mode 100644 index 0000000000..70ad621451 --- /dev/null +++ b/.github/workflows/tauri_ci.yaml @@ -0,0 +1,111 @@ +name: Tauri-CI +on: + push: + branches: + - build/tauri + +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.77.2" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + tauri-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-20.04 ] + + runs-on: ${{ matrix.platform }} + + env: + CI: true + steps: + - uses: actions/checkout@v4 + + - name: Maximize build space (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./frontend/appflowy_tauri/src-tauri -> target" + + - name: Node_modules cache + uses: actions/cache@v2 + with: + path: frontend/appflowy_tauri/node_modules + key: node-modules-${{ runner.os }} + + - name: install dependencies (windows only) + if: matrix.platform == 'windows-latest' + working-directory: frontend + run: | + cargo install --force duckscript_cli + vcpkg integrate install + + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + working-directory: frontend + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: install cargo-make + working-directory: frontend + run: | + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + + - name: install frontend dependencies + working-directory: frontend/appflowy_tauri + run: | + mkdir dist + pnpm install + cargo make --cwd .. tauri_build + + - name: frontend tests and linting + working-directory: frontend/appflowy_tauri + run: | + pnpm test + pnpm test:errors + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tauriScript: pnpm tauri + projectPath: frontend/appflowy_tauri + args: "--debug" \ No newline at end of file diff --git a/.github/workflows/tauri_release.yml b/.github/workflows/tauri_release.yml new file mode 100644 index 0000000000..7de80b017e --- /dev/null +++ b/.github/workflows/tauri_release.yml @@ -0,0 +1,153 @@ +name: Publish Tauri Release + +on: + workflow_dispatch: + inputs: + branch: + description: 'The branch to release' + required: true + default: 'main' + version: + description: 'The version to release' + required: true + default: '0.0.0' +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.77.2" + +jobs: + + publish-tauri: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + settings: + - platform: windows-latest + args: "--verbose" + target: "windows-x86_64" + - platform: macos-latest + args: "--target x86_64-apple-darwin" + target: "macos-x86_64" + - platform: ubuntu-20.04 + args: "--target x86_64-unknown-linux-gnu" + target: "linux-x86_64" + + runs-on: ${{ matrix.settings.platform }} + + env: + CI: true + PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.branch }} + + - name: Maximize build space (ubuntu only) + if: matrix.settings.platform == 'ubuntu-20.04' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./frontend/appflowy_tauri/src-tauri -> target" + + - name: install dependencies (windows only) + if: matrix.settings.platform == 'windows-latest' + working-directory: frontend + run: | + cargo install --force duckscript_cli + vcpkg integrate install + + - name: install dependencies (ubuntu only) + if: matrix.settings.platform == 'ubuntu-20.04' + working-directory: frontend + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: install cargo-make + working-directory: frontend + run: | + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + + - name: install frontend dependencies + working-directory: frontend/appflowy_tauri + run: | + mkdir dist + pnpm install + pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }} + cargo make --cwd .. tauri_build + + - uses: tauri-apps/tauri-action@dev + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }} + APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }} + APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }} + APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }} + CI: true + with: + args: ${{ matrix.settings.args }} + appVersion: ${{ github.event.inputs.version }} + tauriScript: pnpm tauri + projectPath: frontend/appflowy_tauri + + - name: Upload EXE package(windows only) + uses: actions/upload-artifact@v4 + if: matrix.settings.platform == 'windows-latest' + with: + name: ${{ env.PACKAGE_PREFIX }}.exe + path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe + + - name: Upload DMG package(macos only) + uses: actions/upload-artifact@v4 + if: matrix.settings.platform == 'macos-latest' + with: + name: ${{ env.PACKAGE_PREFIX }}.dmg + path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg + + - name: Upload Deb package(ubuntu only) + uses: actions/upload-artifact@v4 + if: matrix.settings.platform == 'ubuntu-20.04' + with: + name: ${{ env.PACKAGE_PREFIX }}.deb + path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb + + - name: Upload AppImage package(ubuntu only) + uses: actions/upload-artifact@v4 + if: matrix.settings.platform == 'ubuntu-20.04' + with: + name: ${{ env.PACKAGE_PREFIX }}.AppImage + path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage diff --git a/.github/workflows/web2_ci.yaml b/.github/workflows/web2_ci.yaml new file mode 100644 index 0000000000..c52f71dd84 --- /dev/null +++ b/.github/workflows/web2_ci.yaml @@ -0,0 +1,75 @@ +name: Web-CI +on: + pull_request: + paths: + - ".github/workflows/web2_ci.yaml" + - "frontend/appflowy_web_app/**" + - "frontend/resources/**" +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + web-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-20.04 ] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + - name: Maximize build space (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + - name: Node_modules cache + uses: actions/cache@v2 + with: + path: frontend/appflowy_web_app/node_modules + key: node-modules-${{ runner.os }} + + - name: install frontend dependencies + working-directory: frontend/appflowy_web_app + run: | + pnpm install + - name: Run lint check + working-directory: frontend/appflowy_web_app + run: | + pnpm run lint + + - name: build and analyze + working-directory: frontend/appflowy_web_app + run: | + pnpm run analyze >> analyze-size.txt + - name: Upload analyze-size.txt + uses: actions/upload-artifact@v4 + with: + name: analyze-size.txt + path: frontend/appflowy_web_app/analyze-size.txt + retention-days: 30 + - name: Upload stats.html + uses: actions/upload-artifact@v4 + with: + name: stats.html + path: frontend/appflowy_web_app/dist/stats.html + retention-days: 30 diff --git a/.github/workflows/web_ci.yaml b/.github/workflows/web_ci.yaml new file mode 100644 index 0000000000..9c568e1916 --- /dev/null +++ b/.github/workflows/web_ci.yaml @@ -0,0 +1,83 @@ +name: WEB-CI + +on: + workflow_dispatch: + inputs: + build: + description: 'Build the web app' + required: true + default: 'true' + +env: + CARGO_TERM_COLOR: always + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.77.2" + CARGO_MAKE_VERSION: "0.36.6" + +jobs: + web-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-latest ] + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Cache Rust Dependencies + uses: Swatinem/rust-cache@v2 + with: + key: rust-dependencies-${{ runner.os }} + workspaces: | + frontend/rust-lib + frontend/appflowy_web/appflowy_wasm + + # TODO: Can combine caching deps and node_modules in one + # See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob + - name: Cache Node.js dependencies + uses: actions/cache@v4 + with: + path: ~/.npm + key: npm-${{ runner.os }} + + - name: Cache node_modules + uses: actions/cache@v4 + with: + path: frontend/appflowy_web/node_modules + key: node-modules-${{ runner.os }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - name: Install wasm-pack + run: cargo install wasm-pack + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-make@${{ env.CARGO_MAKE_VERSION }} + + - name: install dependencies + if: matrix.platform == 'ubuntu-latest' + working-directory: frontend + run: | + sudo apt-get update + npm install -g pnpm@${{ env.PNPM_VERSION }} + cargo make install_web_protobuf + + - name: Build + working-directory: frontend/appflowy_web + run: | + pnpm install + pnpm run build_release_wasm diff --git a/.github/workflows/web_coverage.yaml b/.github/workflows/web_coverage.yaml new file mode 100644 index 0000000000..7803f719c9 --- /dev/null +++ b/.github/workflows/web_coverage.yaml @@ -0,0 +1,65 @@ +name: Web Code Coverage + +on: + pull_request: + paths: + - ".github/workflows/web2_ci.yaml" + - "frontend/appflowy_web_app/**" + - "frontend/resources/**" + +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + test: + if: github.event.pull_request.draft != true + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Maximize build space (ubuntu only) + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + # Install pnpm dependencies, cache them correctly + # and run all Cypress tests + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + working-directory: frontend/appflowy_web_app + component: true + build: pnpm run build + start: pnpm run start + browser: chrome + + - name: Jest run + working-directory: frontend/appflowy_web_app + run: | + pnpm run test:unit + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + token: cf9245e0-e136-4e21-b0ee-35755fa0c493 + files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info + flags: appflowy_web_app + name: frontend/appflowy_web_app + fail_ci_if_error: true + verbose: true + diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e7e268a5..277a2827ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,362 +1,4 @@ # Release Notes -## Version 0.8.9 - 16/04/2025 -### Desktop -#### New Features -- Supported pasting a link as a mention, providing a more condensed visualization of linked content -- Supported converting between link formats (e.g. transforming a mention into a bookmark) -- Improved the link editing experience with enhanced UX -- Added OTP (One-Time Password) support for sign-in authentication -- Added latest AI models: GPT-4.1, GPT-4.1-mini, and Claude 3.7 Sonnet -#### Bug Fixes -- Fixed an issue where properties were not displaying in the row detail page -- Fixed a bug where Undo didn't work in the row detail page -- Fixed an issue where blocks didn't grow when the grid got bigger -- Fixed several bugs related to AI writers -### Mobile -#### New Features -- Added sign-in with OTP (One-Time Password) -#### Bug Fixes -- Fixed an issue where the slash menu sometimes failed to display -- Updated the mention page block to handle page selection with more context. - -## Version 0.8.8 - 01/04/2025 -### New Features -- Added support for selecting AI models in AI writer -- Revamped link menu in toolbar -- Added support for using ":" to add emojis in documents -- Passed the history of past AI prompts and responses to AI writer -### Bug Fixes -- Improved AI writer scrolling user experience -- Fixed issue where checklist items would disappear during reordering -- Fixed numbered lists generated by AI to maintain the same index as the input - -## Version 0.8.7 - 18/03/2025 -### New Features -- Made local AI free and integrated with Ollama -- Supported nested lists within callout and quote blocks -- Revamped the document's floating toolbar and added Turn Into -- Enabled custom icons in callout blocks -### Bug Fixes -- Fixed occasional incorrect positioning of the slash menu -- Improved AI Chat and AI Writers with various bug fixes -- Adjusted the columns block to match the width of the editor -- Fixed a potential segfault caused by infinite recursion in the trash view -- Resolved an issue where the first added cover might be invisible -- Fixed adding cover images via Unsplash - -## Version 0.8.6 - 06/03/2025 -### Bug Fixes -- Fix the incorrect title positioning when adjusting the document width setting -- Enhance the user experience of the icon color picker for smoother interactions -- Add missing icons to the database to ensure completeness and consistency -- Resolve the issue with links not functioning correctly on Linux systems -- Improve the outline feature to work seamlessly within columns -- Center the bulleted list icon within columns for better visual alignment -- Enable dragging blocks under tables in the second column to enhance flexibility -- Disable the AI writer feature within tables to prevent conflicts and improve usability -- Automatically enable the header row when converting content from Markdown to ensure proper formatting -- Use the "Undo" function to revert the auto-formatting - -## Version 0.8.5 - 04/03/2025 -### New Features -- Columns in Documents: Arrange content side by side using drag-and-drop or the slash menu -- AI Writers: New AI assistants in documents with response formatting options (list, table, text with images, image-only), follow-up questions, contextual memory, and more -- Compact Mode for Databases: Enable compact mode for grid and kanban views (full-page and inline) to increase information density, displaying more data per screen -### Bug Fixes -- Fixed an issue where callout blocks couldn’t be deleted when appearing as the first line in a document -- Fixed a bug preventing the relation field in databases from opening -- Fixed an issue where links in documents were unclickable on Linux - -## Version 0.8.4 - 18/02/2025 -### New Features -- Switch AI mode on mobile -- Support locking page -- Support uploading svg file as icon -- Support the slash, at, and plus menus on mobile -### Bug Fixes -- Gallery not rendering in row page -- Save image should not copy the image (mobile) -- Support exporting more content to markdown - -## Version 0.8.2 - 23/01/2025 -### New Features -- Customized database view icons -- Support for uploading images as custom icons -- Enabled selecting multiple AI messages to save into a document -- Added the ability to scale the app's display size on mobile -- Support for pasting image links without file extensions -### Bug Fixes -- Fixed an issue where pasting tables from other apps wasn't working -- Fixed homepage URL issues in Settings -- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page - -## Version 0.8.1 - 14/01/2025 -### New Features -- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only -- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat -- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language -- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more -- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar -- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile -### Bug Fixes -- Resolved an icon rendering issue in callout blocks, tab bars, and search results -- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails - -## Version 0.8.0 - 06/01/2025 -### Bug Fixes -- Fixed error displaying in the page style menu -- Fixed filter logic in the icon picker -- Fixed error displaying in the Favorite/Recent page -- Fixed the color picker displaying when tapping down -- Fixed icons not being supported in subpage blocks -- Fixed recent icon functionality in the space icon menu -- Fixed "Insert Below" not auto-scrolling the table -- Fixed a to-do item with an emoji automatically creating a soft break -- Fixed header row/column tap areas being too small -- Fixed simple table alignment not working for items that wrap -- Fixed web content reverting after removing the inline code format on desktop -- Fixed inability to make changes to a row or column in the table when opening a new tab -- Fixed changing the language to CKB-KU causing a gray screen on mobile - -## Version 0.7.9 - 30/12/2024 -### New Features -- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser. - - Create beautiful documents with 22 content types and markdown support - - Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos - - Invite members to your workspace for seamless collaboration - - Create multiple public/private spaces to better organize your content -- Simple Table is now available on Mobile, designed specifically for mobile devices. - - Create and manage Simple Table blocks on Mobile with easy-to-use action menus. - - Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile - - Use '/' to insert a content block into a table cell on Desktop -- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources -- Add messages to an editable document while chatting with AI side by side -- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons -- Drag a page from the sidebar into a document to easily mention the page without typing its title -- Paste as plain text, a new option in the right-click paste menu -### Bug Fixes -- Fixed misalignment in numbered lists -- Resolved several bugs in the emoji menu -- Fixed a bug with checklist items - -## Version 0.7.8 - 18/12/2024 -### New Features -image - -- Meet Simple Table 2.0: - - Insert a list into a table cell - - Insert images, quotes, callouts, and code blocks into a table cell - - Drag to move rows or columns - - Toggle header rows or columns on/off - - Distribute columns evenly - - Adjust to page width -- Enjoy a new UI/UX for a seamless experience -- Revamped mention page interactions in AI Chat -- Improved AppFlowy AI service - -### Bug Fixes -- Fixed an error when opening files in the database in local mode -- Fixed arrow up/down navigation not working for selecting a language in Code Block -- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work - -## Version 0.7.7 - 09/12/2024 -### Bug Fixes -- Fixed sidebar menu resize regression -- Fixed AI chat loading issues -- Fixed inability to open local files in database -- Fixed mentions remaining in notifications after removal from document -- Fixed event card closing when clicking on empty space -- Fixed keyboard shortcut issues - -## Version 0.7.6 - 03/12/2024 -### New Features -- Revamped the simple table UI -- Added support for capturing images from camera on mobile -### Bug Fixes -- Improved markdown rendering capabilities in AI writer -- Fixed an issue where pressing Enter on a collapsed toggle list would add an unnecessary new line -- Fixed an issue where creating a document from slash menu could insert content at incorrect position - -## Version 0.7.5 - 25/11/2024 -### Bug Fixes -- Improved chat response parsing -- Fixed toggle list icon direction for RTL mode -- Fixed cross blocks formatting not reflecting in float toolbar -- Fixed unable to click inside the toggle list to create a new paragraph -- Fixed open file error 50 on macOS -- Fixed upload file exceed limit error - -## Version 0.7.4 - 19/11/2024 -### New Features -- Support uploading WebP and BMP images -- Support managing workspaces on mobile -- Support adding toggle headings on mobile -- Improve the AI chat page UI -### Bug Fixes -- Optimized the workspace menu loading performance -- Optimized tab switching performance -- Fixed searching issues in Document page - -## Version 0.7.3 - 07/11/2024 -### New Features -- Enable custom URLs for published pages -- Support toggling headings -- Create a subpage by typing in the document -- Turn selected blocks into a subpage -- Add a manual date picker for the Date property - -### Bug Fixes -- Fixed an issue where the workspace owner was unable to delete spaces created by others -- Fixed cursor height inconsistencies with text height -- Fixed editing issues in Kanban cards -- Fixed an issue preventing images or files from being dropped into empty paragraphs - -## Version 0.7.2 - 22/10/2024 -### New Features -- Copy link to block -- Support turn into in document -- Enable sharing links and publishing pages on mobile -- Enable drag and drop in row documents -- Right-click on page in sidebar to open more actions -- Create new subpage in document using `+` character -- Allow reordering checklist item - -### Bug Fixes -- Fixed issue with inability to cancel inline code format in French IME -- Fixed delete with Shift or Ctrl shortcuts not working in documents -- Fixed the issues with incorrect time zone being used in filters. - -## Version 0.7.1 - 07/10/2024 -### New Features -- Copy link to share and open it in a browser -- Enable the ability to edit the page title within the body of the document -- Filter by last modified, created at, or a date range -- Allow customization of database property icons -- Support CTRL/CMD+X to delete the current line when the selection is collapsed in the document -- Support window tiling on macOS -- Add filters to grid views on mobile -- Create and manage workspaces on mobile -- Automatically convert property types for imported CSV files - -### Bug Fixes -- Fixed calculations with filters applied -- Fixed issues with importing data folders into a cloud account -- Fixed French IME backtick issues -- Fixed selection gesture bugs on mobile - -## Version 0.7.0 - 19/09/2024 -### New Features -- Support reordering blocks in document with drag and drop -- Support for adding a cover to a row/card in databases -- Added support for accessing settings on the sign-in page -- Added "Move to" option to the document menu in top right corner -- Support for adjusting the document width from settings -- Show full name of a group on hover -- Colored group names in kanban boards -- Support "Ask AI" on multiple lines of text -- Support for keyboard gestures to move cursor on Mobile -- Added markdown support for quickly inserting a code block using three backticks - -### Bug Fixes -- Fixed a critical bug where the backtick character would crash the application -- Fixed an issue with signing-in from the settings dialog where the dialog would persist -- Fixed a visual bug with icon alignment in primary cell of database rows -- Fixed a bug with filters applied where new rows were inserted in wrong position -- Fixed a bug where "Untitled" would override the name of the row -- Fixed page title not updating after renaming from "More"-menu -- Fixed File block breaking row detail document -- Fixed issues with reordering rows with sorting rules applied -- Improvements to the File & Media type in Database -- Performance improvement in Grid view -- Fixed filters sometimes not applying properly in databases - -## Version 0.6.9 - 09/09/2024 -### New Features -- Added a new property type, 'Files & media' -- Supported Apple Sign-in -- Displayed the page icon next to the row name when the row page contains nested notes -- Enabled Delete Account in Settings -- Included a collapsible navigation menu in your published site - -### Bug Fixes -- Fixed the space name color issue in the community themes -- Fixed database filters and sorting issues -- Fixed the issue of not being able to fully display the title on Kanban cards -- Fixed the inability to see the entire text of a checklist item when it's more than one line long -- Fixed hide/unhide buttons in the No Status group -- Fixed the inability to edit group names on Kanban boards -- Made error codes more user-friendly -- Added leading zeros to day and month in date format - -## Version 0.6.8 - 22/08/2024 -### New Features -- Enabled viewing data inside a database record on mobile. -- Added the ability to invite members to a workspace on mobile. -- Introduced Ask AI in the Home tab on mobile. -- Import CSV files with up to 1,000 rows. -- Convert properties from one type to another while preserving the data. -- Optimized the speed of opening documents and databases. -- Improved syncing performance across devices. -- Added support for a monochrome app icon on Android. - -### Bug Fixes -- Removed the Wayland header from the AppImage build. -- Fixed the issue where pasting a web image on mobile failed. -- Corrected the Local AI state when switching between different workspaces. -- Fixed high CPU usage when opening large databases. - -## 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. -- Added the ability to choose cursor color and selection color from a palette in settings page. -### Bug Fixes -- Optimized the performance for loading recent pages. -- Fixed an issue where the cursor would jump randomly when typing in the document title on mobile. - -## Version 0.6.3 - 08/07/2024 -### New Features -- Publish a Document to the Web - -## Version 0.6.2 - 01/07/2024 -### New Features -- Added support for duplicating spaces. -- Added support for moving pages across spaces. -- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`. -- Improved shortcuts settings UI. -### Bug Fixes -- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards. -- Fixed unable to paste nested lists in existing lists. - -## Version 0.6.1 - 22/06/2024 -### New Features -- Introduced the "Space" feature to help you organize your pages more efficiently. -### Bug Fixes -- Resolved shortcut conflicts on the board page. -- Resolved an issue where underscores could cause the editor to freeze. - ## Version 0.6.0 - 19/06/2024 ### New Features - Introduced the "Space" feature to help you organize your pages more efficiently. @@ -1117,4 +759,4 @@ Bug fixes and improvements - Increased height of action - CPU performance issue - Fix potential data parser error -- More foundation work for online collaboration \ No newline at end of file +- More foundation work for online collaboration diff --git a/README.md b/README.md index 565908e756..580fa98a48 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@

- AppFlowy
+ AppFlowy.IO
⭐️ The Open Source Alternative To Notion ⭐️

-AppFlowy is the AI workspace where you achieve more without losing control of your data +You are in charge of your data and customizations.

@@ -18,37 +18,21 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo

- Website • - Forum • + WebsiteDiscord • - RedditTwitter

-

AppFlowy Kanban Board for To-dos

-

AppFlowy Databases for Tasks and Projects

-

AppFlowy Sites for Beautiful documentation

-

AppFlowy AI

-

AppFlowy Templates

- -

-

- Work across devices

-

- Work across devices

-

- Work across devices

+

AppFlowy Docs & Notes & Wikis

+

AppFlowy Databases for Tasks and Projects

+

AppFlowy Kanban Board for To-Dos

+

AppFlowy Calendars for Plan and Manage Content

+

AppFlowy OpenAI GPT Writers

## User Installation -- [Download AppFlowy Desktop (macOS, Windows, and Linux)](https://github.com/AppFlowy-IO/AppFlowy/releases) -- Other - channels: [FlatHub](https://flathub.org/apps/io.appflowy.AppFlowy), [Snapcraft](https://snapcraft.io/appflowy), [Sourceforge](https://sourceforge.net/projects/appflowy/) -- Available on - - [App Store](https://apps.apple.com/app/appflowy/id6457261352): iPhone - - [Play Store](https://play.google.com/store/apps/details?id=io.appflowy.appflowy): Android 10 or above; ARMv7 is - not supported -- [Self-hosting AppFlowy](https://appflowy.com/docs/self-host-appflowy-overview) +- [Windows/Mac/Linux](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/mac-windows-linux-packages) +- [Docker](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/installing-with-docker) - [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source) ## Built With @@ -63,41 +47,32 @@ AppFlowy is the AI workspace where you achieve more without losing control of yo ## Getting Started with development -Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific -development instructions +Please view the [documentation](https://docs.appflowy.io/docs/documentation/appflowy/from-source) for OS specific development instructions ## Roadmap - [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap) - [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) -If you'd like to propose a feature, submit a feature -request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+)
-If you'd like to report a bug, submit a bug -report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+) +If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+)
+If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+) ## **Releases** -Please see the [changelog](https://appflowy.com/what-is-new) for more details about a given release. +Please see the [changelog](https://www.appflowy.io/whatsnew) for more details about a given release. ## Contributing -Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make -are **greatly appreciated**. Please look -at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) -for details. +Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details. -If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly -easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains -the community, **Congratulations!** You are now an official contributor to AppFlowy. +If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! +Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. ## Translations 🌎🗺 [![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge) -To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use -the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or -run `npx inlang machine translate` to add missing translations. +To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations. ## Join the community to build AppFlowy together @@ -107,30 +82,16 @@ run `npx inlang machine translate` to add missing translations. ## Why Are We Building This? -Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and -functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. -These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative -workplace management tools also have their constraints. +Notion has been our favourite project and knowledge management tool in recent years because of its aesthetic appeal and functionality. Our team uses it daily, and we are on its paid plan. However, as we all know, Notion has its limitations. These include weak data security and poor compatibility with mobile devices. Likewise, alternative collaborative workplace management tools also have their constraints. -The limitations we encountered using these tools and our past work experience with collaborative productivity tools have -led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates -from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a -proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to -come up with a one-size fits all solution in such a fragmented market. +The limitations we encountered using these tools and our past work experience with collaborative productivity tools have led to our firm belief that there is a glass ceiling on what's possible for these tools in the future. This emanates from the fact that these tools will probably struggle to scale horizontally at some point and be forced to prioritize a proportion of customers whose needs differ from the rest. While decision-makers want a workplace OS, it is impossible to come up with a one-size fits all solution in such a fragmented market. -When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, -in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is -a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention -the speed and native experience. The same may apply to individual users as well. +When a customer's evolving core needs are not satisfied, they either switch to another or build one from the ground up, in-house. Consequently, they either go under another ceiling or buy an expensive ticket to learn a hard lesson. This is a requirement for many resources and expertise, building a reliable and easy-to-use collaborative tool, not to mention the speed and native experience. The same may apply to individual users as well. -All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs -well. +All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well. - To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience. -- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to - enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy - your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term - maintainability. +- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability. We decided to achieve this mission by upholding the three most fundamental values: @@ -138,20 +99,16 @@ We decided to achieve this mission by upholding the three most fundamental value - Reliable native experience - Community-driven extensibility -We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority -doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the -knowledge and wheels of making complex workplace management tools while enabling people and businesses to create -beautiful things on their own by equipping them with a versatile toolbox of building blocks. +We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks. ## License -Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for -more information. +Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppFlowy-IO/AppFlowy/blob/main/LICENSE) for more information. -## Acknowledgments +## Acknowledgements -Special thanks to these amazing projects which help power AppFlowy: +Special thanks to these amazing projects which help power AppFlowy.IO: +- [flutter-quill](https://github.com/singerdmx/flutter-quill) - [cargo-make](https://github.com/sagiegurari/cargo-make) - [contrib.rocks](https://contrib.rocks) -- [flutter_chat_ui](https://pub.dev/packages/flutter_chat_ui) diff --git a/codemagic.yaml b/codemagic.yaml deleted file mode 100644 index 9ba2a1a562..0000000000 --- a/codemagic.yaml +++ /dev/null @@ -1,47 +0,0 @@ -workflows: - ios-workflow: - name: iOS Workflow - instance_type: mac_mini_m2 - max_build_duration: 30 - environment: - flutter: 3.27.4 - xcode: latest - cocoapods: default - - scripts: - - name: Build Flutter - script: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source "$HOME/.cargo/env" - rustc --version - cargo --version - - cd frontend - - rustup target install aarch64-apple-ios-sim - cargo install --force cargo-make - cargo install --force --locked duckscript_cli - cargo install --force cargo-lipo - - cargo make appflowy-flutter-deps-tools - cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios - cargo make --profile development-ios-arm64-sim code_generation - - - name: iOS integration tests - script: | - cd frontend/appflowy_flutter - flutter emulators --launch apple_ios_simulator - flutter -d iPhone test integration_test/runner.dart - - artifacts: - - build/ios/ipa/*.ipa - - /tmp/xcodebuild_logs/*.log - - flutter_drive.log - - publishing: - email: - recipients: - - lucas.xu@appflowy.io - notify: - success: true - failure: true diff --git a/doc/readme/desktop_guide_1.jpg b/doc/readme/desktop_guide_1.jpg deleted file mode 100644 index d264c81695..0000000000 Binary files a/doc/readme/desktop_guide_1.jpg and /dev/null differ diff --git a/doc/readme/desktop_guide_2.jpg b/doc/readme/desktop_guide_2.jpg deleted file mode 100644 index d9cdbe5fc1..0000000000 Binary files a/doc/readme/desktop_guide_2.jpg and /dev/null differ diff --git a/doc/readme/getting_started_1.png b/doc/readme/getting_started_1.png deleted file mode 100644 index 8c3c7658ff..0000000000 Binary files a/doc/readme/getting_started_1.png and /dev/null differ diff --git a/doc/readme/mobile_guide_1.png b/doc/readme/mobile_guide_1.png deleted file mode 100644 index 744fdf29dc..0000000000 Binary files a/doc/readme/mobile_guide_1.png and /dev/null differ diff --git a/doc/readme/mobile_guide_2.png b/doc/readme/mobile_guide_2.png deleted file mode 100644 index d92c0295c6..0000000000 Binary files a/doc/readme/mobile_guide_2.png and /dev/null differ diff --git a/doc/readme/mobile_guide_3.png b/doc/readme/mobile_guide_3.png deleted file mode 100644 index 9e3cc52d92..0000000000 Binary files a/doc/readme/mobile_guide_3.png and /dev/null differ diff --git a/doc/readme/mobile_guide_4.png b/doc/readme/mobile_guide_4.png deleted file mode 100644 index b39e03c251..0000000000 Binary files a/doc/readme/mobile_guide_4.png and /dev/null differ diff --git a/doc/readme/mobile_guide_5.png b/doc/readme/mobile_guide_5.png deleted file mode 100644 index 9083b80bed..0000000000 Binary files a/doc/readme/mobile_guide_5.png and /dev/null differ diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index d4ff85a2dd..72d398e0fa 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -1,125 +1,141 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - // This task only builds the Dart code of AppFlowy. - // It supports both the desktop and mobile version. - "name": "AF: Build Dart Only", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "env": { - "RUST_LOG": "debug", - }, - // uncomment the following line to testing performance. - // "flutterMode": "profile", - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - // This task builds the Rust and Dart code of AppFlowy. - "name": "AF-desktop: Build All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Build Appflowy Core", - "env": { - "RUST_LOG": "trace", - "RUST_BACKTRACE": "1" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - // This task builds will: - // - call the clean task, - // - rebuild all the generated Files (including freeze and language files) - // - rebuild the the Rust and Dart code of AppFlowy. - "name": "AF-desktop: Clean + Rebuild All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Clean + Rebuild All", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-iOS: Build All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Build Appflowy Core For iOS", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-iOS: Clean + Rebuild All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Clean + Rebuild All (iOS)", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-iOS-Simulator: Build All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Build Appflowy Core For iOS Simulator", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-iOS-Simulator: Clean + Rebuild All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-Android: Build All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Build Appflowy Core For Android", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-Android: Clean + Rebuild All", - "request": "launch", - "program": "./lib/main.dart", - "type": "dart", - "preLaunchTask": "AF: Clean + Rebuild All (Android)", - "env": { - "RUST_LOG": "trace" - }, - "cwd": "${workspaceRoot}/appflowy_flutter" - }, - { - "name": "AF-desktop: Debug Rust", - "type": "lldb", - "request": "attach", - "pid": "${command:pickMyProcess}" - // To launch the application directly, use the following configuration: - // "request": "launch", - // "program": "[YOUR_APPLICATION_PATH]", - }, - ] -} + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // This task only builds the Dart code of AppFlowy. + // It supports both the desktop and mobile version. + "name": "AF: Build Dart Only", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "env": { + "RUST_LOG": "debug", + "RUST_BACKTRACE": "1" + }, + // uncomment the following line to testing performance. + // "flutterMode": "profile", + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + // This task builds the Rust and Dart code of AppFlowy. + "name": "AF-desktop: Build All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Build Appflowy Core", + "env": { + "RUST_LOG": "trace", + "RUST_BACKTRACE": "1" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + // This task builds will: + // - call the clean task, + // - rebuild all the generated Files (including freeze and language files) + // - rebuild the the Rust and Dart code of AppFlowy. + "name": "AF-desktop: Clean + Rebuild All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-iOS: Build All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Build Appflowy Core For iOS", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-iOS: Clean + Rebuild All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All (iOS)", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-iOS-Simulator: Build All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Build Appflowy Core For iOS Simulator", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-iOS-Simulator: Clean + Rebuild All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-Android: Build All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Build Appflowy Core For Android", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-Android: Clean + Rebuild All", + "request": "launch", + "program": "./lib/main.dart", + "type": "dart", + "preLaunchTask": "AF: Clean + Rebuild All (Android)", + "env": { + "RUST_LOG": "trace" + }, + "cwd": "${workspaceRoot}/appflowy_flutter" + }, + { + "name": "AF-desktop: Debug Rust", + "type": "lldb", + "request": "attach", + "pid": "${command:pickMyProcess}" + // To launch the application directly, use the following configuration: + // "request": "launch", + // "program": "[YOUR_APPLICATION_PATH]", + }, + { + // https://tauri.app/v1/guides/debugging/vs-code + "type": "lldb", + "request": "launch", + "name": "AF-tauri: Debug backend", + "cargo": { + "args": [ + "build", + "--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml", + "--no-default-features" + ] + }, + "preLaunchTask": "AF: Tauri UI Dev", + "cwd": "${workspaceRoot}/appflowy_tauri/" + }, + ] +} \ No newline at end of file diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 0be167fb12..d940eef0a8 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -245,6 +245,51 @@ "problemMatcher": [], "detail": "appflowy_flutter" }, + { + "label": "AF: Tauri UI Build", + "type": "shell", + "command": "pnpm run build", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri UI Dev", + "type": "shell", + "isBackground": true, + "command": "pnpm sync:i18n && pnpm run dev", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, + { + "label": "AF: Tauri Clean", + "type": "shell", + "command": "cargo make tauri_clean", + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Tauri Clean + Dev", + "type": "shell", + "dependsOrder": "sequence", + "dependsOn": [ + "AF: Tauri Clean", + "AF: Tauri UI Dev" + ], + "options": { + "cwd": "${workspaceFolder}" + } + }, + { + "label": "AF: Tauri ESLint", + "type": "shell", + "command": "npx eslint --fix src", + "options": { + "cwd": "${workspaceFolder}/appflowy_tauri" + } + }, { "label": "AF: Generate Env File", "type": "shell", diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 41fdffb1af..6e433b7513 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.8.9" +APPFLOWY_VERSION = "0.6.1" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" @@ -50,6 +50,7 @@ APP_ENVIRONMENT = "local" FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend" TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend" WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend" +WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm" TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend" # Test default config TEST_CRATE_TYPE = "cdylib" diff --git a/frontend/appflowy_flutter/analysis_options.yaml b/frontend/appflowy_flutter/analysis_options.yaml index 4579b2d8c5..c8066c92a5 100644 --- a/frontend/appflowy_flutter/analysis_options.yaml +++ b/frontend/appflowy_flutter/analysis_options.yaml @@ -1,12 +1,32 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. + include: package:flutter_lints/flutter.yaml analyzer: exclude: - "**/*.g.dart" - "**/*.freezed.dart" - - "packages/**/*.dart" linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. rules: - require_trailing_commas @@ -31,5 +51,8 @@ linter: - sort_constructors_first - unawaited_futures +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + errors: invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index 0b96e32472..a9df7ef279 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -52,8 +52,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.appflowy.appflowy" - minSdkVersion 29 - targetSdkVersion 35 + minSdkVersion 24 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true @@ -87,13 +87,6 @@ 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/AndroidManifest.xml b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml index f746eeb610..279b17320c 100644 --- a/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml +++ b/frontend/appflowy_flutter/android/app/src/main/AndroidManifest.xml @@ -36,6 +36,7 @@ + - - + @@ -67,5 +65,4 @@ --> - \ No newline at end of file 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 deleted file mode 100644 index c691e14bdc..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_background.xml b/frontend/appflowy_flutter/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_background.xml rename to frontend/appflowy_flutter/android/app/src/main/res/drawable/launch_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 deleted file mode 100644 index c7ec6fdd6f..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ 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 deleted file mode 100644 index ba42ab6878..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ 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 deleted file mode 100644 index 036d09bc5f..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ 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 911ee844c7..b00c03fd17 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 deleted file mode 100644 index 1b466c0eb2..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null 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 deleted file mode 100644 index 56ea852799..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png and /dev/null 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 deleted file mode 100644 index f4d14c0d60..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null 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 fe7a94797a..e76d95c5be 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 deleted file mode 100644 index 15fb3c4ddf..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null 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 deleted file mode 100644 index 63fa775f58..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png and /dev/null 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 deleted file mode 100644 index fda3c7fa3e..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null 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 61e49810e8..c5188d2de4 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 deleted file mode 100644 index 132a0e9ff0..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null 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 deleted file mode 100644 index f9e393537d..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png and /dev/null 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 deleted file mode 100644 index 8efe0ff281..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null 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 be4cf46069..3cc1a254c9 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 deleted file mode 100644 index 95a312fbc5..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null 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 deleted file mode 100644 index a63acece70..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png and /dev/null 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 deleted file mode 100644 index 727cb0c58a..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null 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 c9e8059fe3..c8f21cf1b3 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 deleted file mode 100644 index d5ce932756..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null 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 deleted file mode 100644 index ad1543e064..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png and /dev/null 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 deleted file mode 100644 index 010733d23d..0000000000 Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null 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 deleted file mode 100644 index c5d5899fdf..0000000000 --- a/frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFF - \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/fonts/.gitkeep b/frontend/appflowy_flutter/assets/fonts/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf new file mode 100644 index 0000000000..8f03a5c8f9 Binary files /dev/null and b/frontend/appflowy_flutter/assets/fonts/FlowyIconData.ttf differ diff --git a/frontend/appflowy_flutter/assets/icons/icons.json b/frontend/appflowy_flutter/assets/icons/icons.json deleted file mode 100644 index 4ad858c414..0000000000 --- a/frontend/appflowy_flutter/assets/icons/icons.json +++ /dev/null @@ -1 +0,0 @@ -{ "artificial_intelligence": [ { "name": "ai-chip-spark", "keywords": [ "chip", "processor", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-cloud-spark", "keywords": [ "cloud", "internet", "server", "network", "artificial", "intelligence", "ai" ], "content": "\n \n \n \n \n \n \n \n \n\n" }, { "name": "ai-edit-spark", "keywords": [ "change", "edit", "modify", "pencil", "write", "writing", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-email-generator-spark", "keywords": [ "mail", "envelope", "inbox", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-gaming-spark", "keywords": [ "remote", "control", "controller", "technology", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-generate-landscape-image-spark", "keywords": [ "picture", "photography", "photo", "image", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-generate-music-spark", "keywords": [ "music", "audio", "note", "entertainment", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-generate-portrait-image-spark", "keywords": [ "picture", "photography", "photo", "image", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-generate-variation-spark", "keywords": [ "module", "application", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-navigation-spark", "keywords": [ "map", "location", "direction", "travel", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-network-spark", "keywords": [ "globe", "internet", "world", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-prompt-spark", "keywords": [ "app", "code", "apps", "window", "website", "web", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-redo-spark", "keywords": [ "arrow", "refresh", "sync", "synchronize", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-science-spark", "keywords": [ "atom", "scientific", "experiment", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-settings-spark", "keywords": [ "cog", "gear", "settings", "machine", "artificial", "intelligence" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-technology-spark", "keywords": [ "lightbulb", "idea", "bright", "lighting", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-upscale-spark", "keywords": [ "magnifier", "zoom", "view", "find", "search", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ai-vehicle-spark-1", "keywords": [ "car", "automated", "transportation", "artificial", "intelligence", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "artificial-intelligence-spark", "keywords": [ "brain", "thought", "ai", "automated", "ai" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "computer_devices": [ { "name": "adobe", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "alt", "keywords": [ "windows", "key", "alt", "pc", "keyboard" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "amazon", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "android", "keywords": [ "android", "code", "apps", "bugdroid", "programming" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "app-store", "keywords": [], "content": "\n\n\n" }, { "name": "apple", "keywords": [ "os", "system", "apple" ], "content": "\n\n\n" }, { "name": "asterisk-1", "keywords": [ "asterisk", "star", "keyboard" ], "content": "\n\n\n" }, { "name": "battery-alert-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "alert", "warning" ], "content": "\n\n\n" }, { "name": "battery-charging", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "charging" ], "content": "\n\n\n" }, { "name": "battery-empty-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "empty", "power", "battery" ], "content": "\n\n\n" }, { "name": "battery-empty-2", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "empty", "power", "battery" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "battery-full-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "full" ], "content": "\n\n\n" }, { "name": "battery-low-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "low" ], "content": "\n\n\n" }, { "name": "battery-medium-1", "keywords": [ "phone", "mobile", "charge", "medium", "device", "electricity", "power", "battery" ], "content": "\n\n\n" }, { "name": "bluetooth", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "connection" ], "content": "\n\n\n" }, { "name": "bluetooth-disabled", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "disabled", "off", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bluetooth-searching", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "searching", "connecting", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-wifi", "keywords": [ "wireless", "wifi", "internet", "server", "network", "browser", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chrome", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "command", "keywords": [ "mac", "command", "apple", "keyboard" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "computer-chip-1", "keywords": [ "computer", "device", "chip", "electronics", "cpu", "microprocessor" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "computer-chip-2", "keywords": [ "core", "microprocessor", "device", "electronics", "chip", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "computer-pc-desktop", "keywords": [ "screen", "desktop", "monitor", "device", "electronics", "display", "pc", "computer" ], "content": "\n\n\n" }, { "name": "controller", "keywords": [ "remote", "quadcopter", "drones", "flying", "drone", "control", "controller", "technology", "fly" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "controller-1", "keywords": [ "remote", "quadcopter", "drones", "flying", "drone", "control", "controller", "technology", "fly" ], "content": "\n\n\n" }, { "name": "controller-wireless", "keywords": [ "remote", "gaming", "drones", "drone", "control", "controller", "technology", "console" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cursor-click", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cyborg", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android" ], "content": "\n\n\n" }, { "name": "cyborg-2", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android" ], "content": "\n\n\n" }, { "name": "database", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-check", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "check", "approve" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-lock", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "password", "security", "protection", "lock", "secure" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-refresh", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "refresh" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-remove", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "remove", "delete", "cross" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-server-1", "keywords": [ "server", "network", "internet" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-server-2", "keywords": [ "server", "network", "internet" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-setting", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "setting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "database-subtract-2-raid-storage-code-disk-programming-database-array-hard-disc-minus", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "delete-keyboard", "keywords": [], "content": "\n\n\n" }, { "name": "desktop-chat", "keywords": [ "bubble", "chat", "customer", "service", "conversation", "display", "device" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-check", "keywords": [ "success", "approve", "device", "display", "desktop", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-code", "keywords": [ "desktop", "device", "display", "computer", "code", "terminal", "html", "css", "programming", "system" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-delete", "keywords": [ "device", "remove", "display", "computer", "deny", "desktop", "fail", "failure", "cross" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-dollar", "keywords": [ "cash", "desktop", "display", "device", "notification", "computer", "money", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-emoji", "keywords": [ "device", "display", "desktop", "padlock", "smiley" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-favorite-star", "keywords": [ "desktop", "device", "display", "like", "favorite", "star" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-game", "keywords": [ "controller", "display", "device", "computer", "games", "leisure" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "desktop-help", "keywords": [ "device", "help", "information", "display", "desktop", "question", "info" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "device-database-encryption-1", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "discord", "keywords": [], "content": "\n\n\n" }, { "name": "drone", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android", "flying" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dropbox", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "eject", "keywords": [ "eject", "unmount", "dismount", "remove", "keyboard" ], "content": "\n\n\n" }, { "name": "electric-cord-1", "keywords": [ "electricity", "electronic", "appliances", "device", "cord", "cable", "plug", "connection" ], "content": "\n\n\n" }, { "name": "electric-cord-3", "keywords": [ "electricity", "electronic", "appliances", "device", "cord", "cable", "plug", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "facebook-1", "keywords": [ "media", "facebook", "social" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "figma", "keywords": [], "content": "\n\n\n" }, { "name": "floppy-disk", "keywords": [ "disk", "floppy", "electronics", "device", "disc", "computer", "storage" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "gmail", "keywords": [], "content": "\n\n\n" }, { "name": "google", "keywords": [ "media", "google", "social" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "google-drive", "keywords": [], "content": "\n\n\n" }, { "name": "hand-held", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hand-held-tablet-drawing", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "digital", "drawing", "canvas" ], "content": "\n\n\n" }, { "name": "hand-held-tablet-writing", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "writing", "digital", "paper", "notepad" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hard-disk", "keywords": [ "device", "disc", "drive", "disk", "electronics", "platter", "turntable", "raid", "storage" ], "content": "\n\n\n" }, { "name": "hard-drive-1", "keywords": [ "disk", "device", "electronics", "disc", "drive", "raid", "storage" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "instagram", "keywords": [], "content": "\n\n\n" }, { "name": "keyboard", "keywords": [ "keyboard", "device", "electronics", "dvorak", "qwerty" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "keyboard-virtual", "keywords": [ "remote", "device", "electronics", "qwerty", "keyboard", "virtual", "interface" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "keyboard-wireless-2", "keywords": [ "remote", "device", "wireless", "electronics", "qwerty", "keyboard", "bluetooth" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "laptop-charging", "keywords": [ "device", "laptop", "electronics", "computer", "notebook", "charging" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "linkedin", "keywords": [ "network", "linkedin", "professional" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "local-storage-folder", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "meta", "keywords": [], "content": "\n\n\n" }, { "name": "mouse", "keywords": [ "device", "electronics", "mouse" ], "content": "\n\n\n" }, { "name": "mouse-wireless", "keywords": [ "remote", "wireless", "device", "electronics", "mouse", "computer" ], "content": "\n\n\n" }, { "name": "mouse-wireless-1", "keywords": [ "remote", "wireless", "device", "electronics", "mouse", "computer" ], "content": "\n\n\n" }, { "name": "netflix", "keywords": [], "content": "\n\n\n" }, { "name": "network", "keywords": [ "network", "server", "internet", "ethernet", "connection" ], "content": "\n\n\n" }, { "name": "next", "keywords": [ "next", "arrow", "right", "keyboard" ], "content": "\n\n\n" }, { "name": "paypal", "keywords": [ "payment", "paypal" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-store", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "printer", "keywords": [ "scan", "device", "electronics", "printer", "print", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "return-2", "keywords": [ "arrow", "return", "enter", "keyboard" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "screen-1", "keywords": [ "screen", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "screen-2", "keywords": [ "screen", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "screen-curve", "keywords": [ "screen", "curved", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "screensaver-monitor-wallpaper", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shift", "keywords": [ "key", "shift", "up", "arrow", "keyboard" ], "content": "\n\n\n" }, { "name": "shredder", "keywords": [ "device", "electronics", "shred", "paper", "cut", "destroy", "remove", "delete" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "signal-loading", "keywords": [ "bracket", "loading", "internet", "angle", "signal", "server", "network", "connecting", "connection" ], "content": "\n\n\n" }, { "name": "slack", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "spotify", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "telegram", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tiktok", "keywords": [], "content": "\n\n\n" }, { "name": "tinder", "keywords": [], "content": "\n\n\n" }, { "name": "twitter", "keywords": [ "media", "twitter", "social" ], "content": "\n\n\n" }, { "name": "usb-drive", "keywords": [ "usb", "drive", "stick", "memory", "storage", "data", "connection" ], "content": "\n\n\n" }, { "name": "virtual-reality", "keywords": [ "gaming", "virtual", "gear", "controller", "reality", "games", "headset", "technology", "vr", "eyewear" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "voice-mail", "keywords": [ "mic", "audio", "mike", "music", "microphone" ], "content": "\n\n\n" }, { "name": "voice-mail-off", "keywords": [ "mic", "audio", "mike", "music", "microphone", "mute", "off" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "VPN-connection", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "watch-1", "keywords": [ "device", "timepiece", "cirle", "electronics", "face", "blank", "watch", "smart" ], "content": "\n\n\n" }, { "name": "watch-2", "keywords": [ "device", "square", "timepiece", "electronics", "face", "blank", "watch", "smart" ], "content": "\n\n\n" }, { "name": "watch-circle-charging", "keywords": [ "device", "timepiece", "circle", "watch", "round", "charge", "charging", "power" ], "content": "\n\n\n" }, { "name": "watch-circle-heartbeat-monitor-1", "keywords": [ "device", "timepiece", "circle", "watch", "round", "heart", "beat", "monitor", "healthcare" ], "content": "\n\n\n" }, { "name": "watch-circle-heartbeat-monitor-2", "keywords": [ "device", "timepiece", "circle", "watch", "round", "heart", "beat", "monitor", "healthcare" ], "content": "\n\n\n" }, { "name": "watch-circle-menu", "keywords": [ "device", "timepiece", "circle", "watch", "round", "menu", "list", "option", "app" ], "content": "\n\n\n" }, { "name": "watch-circle-time", "keywords": [ "device", "timepiece", "circle", "watch", "round", "time", "clock", "analog" ], "content": "\n\n\n" }, { "name": "webcam", "keywords": [ "webcam", "camera", "future", "tech", "chat", "skype", "technology", "video" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "webcam-video", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office" ], "content": "\n\n\n" }, { "name": "webcam-video-circle", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "webcam-video-off", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office", "off" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "whatsapp", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wifi", "keywords": [ "wireless", "wifi", "internet", "server", "network", "connection" ], "content": "\n\n\n" }, { "name": "wifi-antenna", "keywords": [ "wireless", "wifi", "internet", "server", "network", "antenna", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wifi-disabled", "keywords": [ "wireless", "wifi", "internet", "server", "network", "disabled", "off", "offline", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wifi-horizontal", "keywords": [ "wireless", "wifi", "internet", "server", "network", "horizontal", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wifi-router", "keywords": [ "wireless", "wifi", "internet", "server", "network", "connection" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "windows", "keywords": [ "os", "system", "microsoft" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "culture": [ { "name": "christian-cross-1", "keywords": [ "religion", "christian", "cross", "culture", "bold" ], "content": "\n\n\n" }, { "name": "christian-cross-2", "keywords": [ "religion", "christian", "cross", "culture", "bold" ], "content": "\n\n\n" }, { "name": "christianity", "keywords": [ "religion", "jesus", "christianity", "christ", "fish", "culture" ], "content": "\n\n\n" }, { "name": "dhammajak", "keywords": [ "religion", "dhammajak", "culture", "bhuddhism", "buddish" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hexagram", "keywords": [ "star", "jew", "jewish", "judaism", "hexagram", "culture", "religion", "david" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hinduism", "keywords": [ "religion", "hinduism", "culture", "hindu" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "islam", "keywords": [ "religion", "islam", "moon", "crescent", "muslim", "culture", "star" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "news-paper", "keywords": [ "newspaper", "periodical", "fold", "content", "entertainment" ], "content": "\n\n\n" }, { "name": "peace-symbol", "keywords": [ "religion", "peace", "war", "culture", "symbol" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "politics-compaign", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "politics-speech", "keywords": [], "content": "\n\n\n" }, { "name": "politics-vote-2", "keywords": [], "content": "\n\n\n" }, { "name": "ticket-1", "keywords": [ "hobby", "ticket", "event", "entertainment", "stub", "theater", "entertainment", "culture" ], "content": "\n\n\n" }, { "name": "tickets", "keywords": [ "hobby", "ticket", "event", "entertainment", "stub", "theater", "entertainment", "culture" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "yin-yang-symbol", "keywords": [ "religion", "tao", "yin", "yang", "taoism", "culture", "symbol" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-1", "keywords": [ "sign", "astrology", "stars", "space", "scorpio" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-10", "keywords": [ "sign", "astrology", "stars", "space", "pisces" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-11", "keywords": [ "sign", "astrology", "stars", "space", "sagittarius" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-12", "keywords": [ "sign", "astrology", "stars", "space", "cancer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-2", "keywords": [ "sign", "astrology", "stars", "space", "virgo" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-3", "keywords": [ "sign", "astrology", "stars", "space", "leo" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-4", "keywords": [ "sign", "astrology", "stars", "space", "aquarius" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-5", "keywords": [ "sign", "astrology", "stars", "space", "taurus" ], "content": "\n\n\n" }, { "name": "zodiac-6", "keywords": [ "sign", "astrology", "stars", "space", "capricorn" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-7", "keywords": [ "sign", "astrology", "stars", "space", "ares" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "zodiac-8", "keywords": [ "sign", "astrology", "stars", "space", "libra" ], "content": "\n\n\n" }, { "name": "zodiac-9", "keywords": [ "sign", "astrology", "stars", "space", "gemini" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "entertainment": [ { "name": "balloon", "keywords": [ "hobby", "entertainment", "party", "balloon" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bow", "keywords": [ "entertainment", "gaming", "bow", "weapon" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-fast-forward-1", "keywords": [ "button", "controls", "fast", "forward", "movies", "television", "video", "tv" ], "content": "\n\n\n" }, { "name": "button-fast-forward-2", "keywords": [ "button", "controls", "fast", "forward", "movies", "television", "video", "tv" ], "content": "\n\n\n" }, { "name": "button-next", "keywords": [ "button", "television", "buttons", "movies", "skip", "next", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-pause-2", "keywords": [ "button", "television", "buttons", "movies", "tv", "pause", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-play", "keywords": [ "button", "television", "buttons", "movies", "play", "tv", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-power-1", "keywords": [ "power", "button", "on", "off" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-previous", "keywords": [ "button", "television", "buttons", "movies", "skip", "previous", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-record-3", "keywords": [ "button", "television", "buttons", "movies", "record", "tv", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "button-rewind-1", "keywords": [ "rewind", "television", "button", "movies", "buttons", "tv", "video", "controls" ], "content": "\n\n\n" }, { "name": "button-rewind-2", "keywords": [ "rewind", "television", "button", "movies", "buttons", "tv", "video", "controls" ], "content": "\n\n\n" }, { "name": "button-stop", "keywords": [ "button", "television", "buttons", "movies", "stop", "tv", "video", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "camera-video", "keywords": [ "film", "television", "tv", "camera", "movies", "video", "recorder" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cards", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chess-bishop", "keywords": [], "content": "\n\n\n" }, { "name": "chess-king", "keywords": [], "content": "\n\n\n" }, { "name": "chess-knight", "keywords": [], "content": "\n\n\n" }, { "name": "chess-pawn", "keywords": [], "content": "\n\n\n" }, { "name": "cloud-gaming-1", "keywords": [ "entertainment", "cloud", "gaming" ], "content": "\n\n\n" }, { "name": "clubs-symbol", "keywords": [ "entertainment", "gaming", "card", "clubs", "symbol" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "diamonds-symbol", "keywords": [ "entertainment", "gaming", "card", "diamonds", "symbol" ], "content": "\n\n\n" }, { "name": "dice-1", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dice-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dice-3", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dice-4", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dice-5", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dice-6", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dices-entertainment-gaming-dices", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "earpods", "keywords": [ "airpods", "audio", "earpods", "music", "earbuds", "true", "wireless", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "epic-games-1", "keywords": [ "epic", "games", "entertainment", "gaming" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "esports", "keywords": [ "entertainment", "gaming", "esports" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fireworks-rocket", "keywords": [ "hobby", "entertainment", "party", "fireworks", "rocket" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "gameboy", "keywords": [ "entertainment", "gaming", "device", "gameboy" ], "content": "\n\n\n" }, { "name": "gramophone", "keywords": [ "music", "audio", "note", "gramophone", "player", "vintage", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hearts-symbol", "keywords": [ "entertainment", "gaming", "card", "hearts", "symbol" ], "content": "\n\n\n" }, { "name": "music-equalizer", "keywords": [ "music", "audio", "note", "wave", "sound", "equalizer", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "music-note-1", "keywords": [ "music", "audio", "note", "entertainment" ], "content": "\n\n\n" }, { "name": "music-note-2", "keywords": [ "music", "audio", "note", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "music-note-off-1", "keywords": [ "music", "audio", "note", "off", "mute", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "music-note-off-2", "keywords": [ "music", "audio", "note", "off", "mute", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "nintendo-switch", "keywords": [ "nintendo", "switch", "entertainment", "gaming" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "one-vesus-one", "keywords": [ "entertainment", "gaming", "one", "vesus", "one" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pacman", "keywords": [ "entertainment", "gaming", "pacman", "video" ], "content": "\n\n\n" }, { "name": "party-popper", "keywords": [ "hobby", "entertainment", "party", "popper", "confetti", "event" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-list-4", "keywords": [ "screen", "television", "display", "player", "movies", "players", "tv", "media", "video", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-list-5", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-list-8", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-list-9", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-list-folder", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "play-station", "keywords": [ "play", "station", "entertainment", "gaming" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "radio", "keywords": [ "antenna", "audio", "music", "radio", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "recording-tape-bubble-circle", "keywords": [ "phone", "device", "mail", "mobile", "voice", "machine", "answering", "chat", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "recording-tape-bubble-square", "keywords": [ "phone", "device", "mail", "mobile", "voice", "machine", "answering", "chat", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "song-recommendation", "keywords": [ "song", "recommendation", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "spades-symbol", "keywords": [ "entertainment", "gaming", "card", "spades", "symbol" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "speaker-1", "keywords": [ "speaker", "music", "audio", "subwoofer", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "speaker-2", "keywords": [ "speakers", "music", "audio", "entertainment" ], "content": "\n\n\n" }, { "name": "stream", "keywords": [ "stream", "entertainment", "gaming" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tape-cassette-record", "keywords": [ "music", "entertainment", "tape", "cassette", "record" ], "content": "\n\n\n" }, { "name": "volume-down", "keywords": [ "speaker", "down", "volume", "control", "audio", "music", "decrease", "entertainment" ], "content": "\n\n\n" }, { "name": "volume-level-high", "keywords": [ "speaker", "high", "volume", "control", "audio", "music", "entertainment" ], "content": "\n\n\n" }, { "name": "volume-level-low", "keywords": [ "volume", "speaker", "lower", "down", "control", "music", "low", "audio", "entertainment" ], "content": "\n\n\n" }, { "name": "volume-level-off", "keywords": [ "volume", "speaker", "control", "music", "audio", "entertainment" ], "content": "\n\n\n" }, { "name": "volume-mute", "keywords": [ "speaker", "remove", "volume", "control", "audio", "music", "mute", "off", "cross", "entertainment" ], "content": "\n\n\n" }, { "name": "volume-off", "keywords": [ "speaker", "music", "mute", "volume", "control", "audio", "off", "mute", "entertainment" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "vr-headset-1", "keywords": [ "entertainment", "gaming", "vr", "headset" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "vr-headset-2", "keywords": [ "entertainment", "gaming", "vr", "headset" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "xbox", "keywords": [ "xbox", "entertainment", "gaming" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "food_drink": [ { "name": "beer-mug", "keywords": [ "beer", "cook", "brewery", "drink", "mug", "cooking", "nutrition", "brew", "brewing", "food" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "beer-pitch", "keywords": [ "drink", "glass", "beer", "pitch" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "burger", "keywords": [ "burger", "fast", "cook", "cooking", "nutrition", "food" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "burrito-fastfood", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cake-slice", "keywords": [ "cherry", "cake", "birthday", "event", "special", "sweet", "bake" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "candy-cane", "keywords": [ "candy", "sweet", "cane", "christmas" ], "content": "\n\n\n" }, { "name": "champagne-party-alcohol", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cheese", "keywords": [ "cook", "cheese", "animal", "products", "cooking", "nutrition", "dairy", "food" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cherries", "keywords": [ "cook", "plant", "cherry", "plants", "cooking", "nutrition", "vegetarian", "fruit", "food", "cherries" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chicken-grilled-stream", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cocktail", "keywords": [ "cook", "alcohol", "food", "cocktail", "drink", "cooking", "nutrition", "alcoholic", "beverage", "glass" ], "content": "\n\n\n" }, { "name": "coffee-bean", "keywords": [ "cook", "cooking", "nutrition", "coffee", "bean" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "coffee-mug", "keywords": [ "coffee", "cook", "cup", "drink", "mug", "cooking", "nutrition", "cafe", "caffeine", "food" ], "content": "\n\n\n" }, { "name": "coffee-takeaway-cup", "keywords": [ "cup", "coffee", "hot", "takeaway", "drink", "caffeine" ], "content": "\n\n\n" }, { "name": "donut", "keywords": [ "dessert", "donut" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fork-knife", "keywords": [ "fork", "spoon", "knife", "food", "dine", "cook", "utensils", "eat", "restaurant", "dining", "kitchenware" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fork-spoon", "keywords": [ "fork", "spoon", "food", "dine", "cook", "utensils", "eat", "restaurant", "dining", "kitchenware" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ice-cream-2", "keywords": [ "cook", "frozen", "popsicle", "freezer", "nutrition", "cream", "stick", "cold", "ice", "cooking" ], "content": "\n\n\n" }, { "name": "ice-cream-3", "keywords": [ "cook", "frozen", "cone", "cream", "ice", "cooking", "nutrition", "freezer", "cold", "food" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lemon-fruit-seasoning", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "microwave", "keywords": [ "cook", "food", "appliances", "cooking", "nutrition", "appliance", "microwave", "kitchenware" ], "content": "\n\n\n" }, { "name": "milkshake", "keywords": [ "milkshake", "drink", "takeaway", "cup", "cold", "beverage" ], "content": "\n\n\n" }, { "name": "popcorn", "keywords": [ "cook", "corn", "movie", "snack", "cooking", "nutrition", "bake", "popcorn" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pork-meat", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "refrigerator", "keywords": [ "fridge", "cook", "appliances", "cooking", "nutrition", "freezer", "appliance", "food", "kitchenware" ], "content": "\n\n\n" }, { "name": "serving-dome", "keywords": [ "cook", "tool", "dome", "kitchen", "serving", "paltter", "dish", "tools", "food", "kitchenware" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shrimp", "keywords": [ "sea", "food", "shrimp" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "strawberry", "keywords": [ "fruit", "sweet", "berries", "plant", "strawberry" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tea-cup", "keywords": [ "herbal", "cook", "tea", "tisane", "cup", "drink", "cooking", "nutrition", "mug", "food" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "toast", "keywords": [ "bread", "toast", "breakfast" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "water-glass", "keywords": [ "glass", "water", "juice", "drink", "liquid" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wine", "keywords": [ "drink", "cook", "glass", "cooking", "wine", "nutrition", "food" ], "content": "\n\n\n" } ], "health": [ { "name": "ambulance", "keywords": [ "car", "emergency", "health", "medical", "ambulance" ], "content": "\n\n\n" }, { "name": "bacteria-virus-cells-biology", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bandage", "keywords": [ "health", "medical", "hospital", "medicine", "capsule", "bandage", "vaccine" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "blood-bag-donation", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "blood-donate-drop", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "blood-drop-donation", "keywords": [], "content": "\n\n\n" }, { "name": "brain", "keywords": [ "medical", "health", "brain" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "brain-cognitive", "keywords": [ "health", "medical", "brain", "cognitive", "specialities" ], "content": "\n\n\n" }, { "name": "call-center-support-service", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "checkup-medical-report-clipboard", "keywords": [], "content": "\n\n\n" }, { "name": "ear-hearing", "keywords": [ "health", "medical", "hearing", "ear" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "eye-optic", "keywords": [ "health", "medical", "eye", "optic" ], "content": "\n\n\n" }, { "name": "flu-mask", "keywords": [ "health", "medical", "hospital", "mask", "flu", "vaccine", "protection" ], "content": "\n\n\n" }, { "name": "health-care-2", "keywords": [ "health", "medical", "hospital", "heart", "care", "symbol" ], "content": "\n\n\n" }, { "name": "heart-rate-pulse-graph", "keywords": [], "content": "\n\n\n" }, { "name": "heart-rate-search", "keywords": [ "health", "medical", "monitor", "heart", "rate", "search" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hospital-sign-circle", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "circle", "emergency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hospital-sign-square", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "square", "emergency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "insurance-hand", "keywords": [ "health", "medical", "insurance", "hand", "cross" ], "content": "\n\n\n" }, { "name": "medical-bag", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "bag", "medicine", "medkit" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "medical-cross-sign-healthcare", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "medical-cross-symbol", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "emergency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "medical-files-report-history", "keywords": [], "content": "\n\n\n" }, { "name": "medical-ribbon-1", "keywords": [ "ribbon", "medical", "cancer", "health", "beauty", "symbol" ], "content": "\n\n\n" }, { "name": "medical-search-diagnosis", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "microscope-observation-sciene", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "nurse-assistant-emergency", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "nurse-hat", "keywords": [ "health", "medical", "hospital", "nurse", "doctor", "cap" ], "content": "\n\n\n" }, { "name": "online-medical-call-service", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "online-medical-service-monitor", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "online-medical-web-service", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "petri-dish-lab-equipment", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pharmacy", "keywords": [ "health", "medical", "pharmacy", "sign", "medicine", "mortar", "pestle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "prescription-pills-drugs-healthcare", "keywords": [], "content": "\n\n\n" }, { "name": "sign-cross-square", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "cross", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sos-help-emergency-sign", "keywords": [], "content": "\n\n\n" }, { "name": "stethoscope", "keywords": [ "instrument", "health", "medical", "stethoscope" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "syringe", "keywords": [ "instrument", "medical", "syringe", "health", "beauty", "needle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tablet-capsule", "keywords": [ "health", "medical", "hospital", "medicine", "capsule", "tablet" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tooth", "keywords": [ "health", "medical", "tooth" ], "content": "\n\n\n" }, { "name": "virus-antivirus", "keywords": [ "health", "medical", "covid19", "flu", "influenza", "virus", "antivirus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "waiting-appointments-calendar", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wheelchair", "keywords": [ "health", "medical", "hospital", "wheelchair", "disable", "help", "sign" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "images_photography": [ { "name": "auto-flash", "keywords": [], "content": "\n\n\n" }, { "name": "camera-1", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures" ], "content": "\n\n\n" }, { "name": "camera-disabled", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "disabled", "off" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "camera-loading", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "loading", "option", "setting" ], "content": "\n\n\n" }, { "name": "camera-square", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "frame", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "composition-oval", "keywords": [ "camera", "frame", "composition", "photography", "pictures", "landscape", "photo", "oval" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "composition-vertical", "keywords": [ "camera", "portrait", "frame", "vertical", "composition", "photography", "photo" ], "content": "\n\n\n" }, { "name": "compsition-horizontal", "keywords": [ "camera", "horizontal", "panorama", "composition", "photography", "photo", "pictures" ], "content": "\n\n\n" }, { "name": "edit-image-photo", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "film-roll-1", "keywords": [ "photos", "camera", "shutter", "picture", "photography", "pictures", "photo", "film", "roll" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "film-slate", "keywords": [ "pictures", "photo", "film", "slate" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flash-1", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flash-2", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flash-3", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n\n\n" }, { "name": "flash-off", "keywords": [ "flash", "power", "connect", "charge", "off", "electricity", "lightning" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flower", "keywords": [ "photos", "photo", "picture", "camera", "photography", "pictures", "flower", "image" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "focus-points", "keywords": [ "camera", "frame", "photography", "pictures", "photo", "focus", "position" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "landscape-2", "keywords": [ "photos", "photo", "landscape", "picture", "photography", "camera", "pictures", "image" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "landscape-setting", "keywords": [ "design", "composition", "horizontal", "lanscape" ], "content": "\n\n\n" }, { "name": "laptop-camera", "keywords": [ "photos", "photo", "picture", "photography", "camera", "pictures", "laptop", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "mobile-phone-camera", "keywords": [ "photos", "photo", "picture", "photography", "camera", "pictures", "phone" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "orientation-landscape", "keywords": [ "photos", "photo", "orientation", "landscape", "picture", "photography", "camera", "pictures", "image" ], "content": "\n\n\n" }, { "name": "orientation-portrait", "keywords": [ "photos", "photo", "orientation", "portrait", "picture", "photography", "camera", "pictures", "image" ], "content": "\n\n\n" }, { "name": "polaroid-four", "keywords": [ "photos", "camera", "polaroid", "picture", "photography", "pictures", "four", "photo", "image" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "interface_essential": [ { "name": "add-1", "keywords": [ "expand", "cross", "buttons", "button", "more", "remove", "plus", "add", "+", "mathematics", "math" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "add-bell-notification", "keywords": [ "notification", "alarm", "alert", "bell", "add" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "add-circle", "keywords": [ "button", "remove", "cross", "add", "buttons", "plus", "circle", "+", "mathematics", "math" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "add-layer-2", "keywords": [ "layer", "add", "design", "plus", "layers", "square", "box" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "add-square", "keywords": [ "square", "remove", "cross", "buttons", "add", "plus", "button", "+", "mathematics", "math" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "alarm-clock", "keywords": [ "time", "tock", "stopwatch", "measure", "clock", "tick" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "align-back-1", "keywords": [ "back", "design", "layer", "layers", "pile", "stack", "arrange", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "align-center", "keywords": [ "text", "alignment", "align", "paragraph", "centered", "formatting", "center" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "align-front-1", "keywords": [ "design", "front", "layer", "layers", "pile", "stack", "arrange", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "align-left", "keywords": [ "paragraph", "text", "alignment", "align", "left", "formatting", "right" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "align-right", "keywords": [ "rag", "paragraph", "text", "alignment", "align", "right", "formatting", "left" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ampersand", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "archive-box", "keywords": [ "box", "content", "banker", "archive", "file" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-bend-left-down-2", "keywords": [ "arrow", "bend", "curve", "change", "direction", "left", "to", "down" ], "content": "\n\n\n" }, { "name": "arrow-bend-right-down-2", "keywords": [ "arrow", "bend", "curve", "change", "direction", "right", "to", "down" ], "content": "\n\n\n" }, { "name": "arrow-crossover-down", "keywords": [ "cross", "move", "over", "arrow", "arrows", "down" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-crossover-left", "keywords": [ "cross", "move", "over", "arrow", "arrows", "left" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-crossover-right", "keywords": [ "cross", "move", "over", "arrow", "arrows", "ight" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-crossover-up", "keywords": [ "cross", "move", "over", "arrow", "arrows", "right" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-cursor-1", "keywords": [ "mouse", "select", "cursor" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-cursor-2", "keywords": [ "mouse", "select", "cursor" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-curvy-up-down-1", "keywords": [ "both", "direction", "arrow", "curvy", "diagram", "zigzag", "vertical" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-curvy-up-down-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-down-2", "keywords": [ "down", "move", "arrow", "arrows" ], "content": "\n\n\n" }, { "name": "arrow-down-dashed-square", "keywords": [ "arrow", "keyboard", "button", "down", "square", "dashes" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-expand", "keywords": [ "expand", "small", "bigger", "retract", "smaller", "big" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-infinite-loop", "keywords": [ "arrow", "diagram", "loop", "infinity", "repeat" ], "content": "\n\n\n" }, { "name": "arrow-move", "keywords": [ "move", "button", "arrows", "direction" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-reload-horizontal-1", "keywords": [ "arrows", "load", "arrow", "sync", "square", "loading", "reload", "synchronize" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-reload-horizontal-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-reload-vertical-1", "keywords": [ "arrows", "load", "arrow", "sync", "square", "loading", "reload", "synchronize" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-reload-vertical-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-roadmap", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-round-left", "keywords": [ "diagram", "round", "arrow", "left" ], "content": "\n\n\n" }, { "name": "arrow-round-right", "keywords": [ "diagram", "round", "arrow", "right" ], "content": "\n\n\n" }, { "name": "arrow-shrink", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-shrink-diagonal-1", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-shrink-diagonal-2", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-transfer-diagonal-1", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-transfer-diagonal-2", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-transfer-diagonal-3", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-up-1", "keywords": [ "arrow", "up", "keyboard" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "arrow-up-dashed-square", "keywords": [ "arrow", "keyboard", "button", "up", "square", "dashes" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ascending-number-order", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "attribution", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "blank-calendar", "keywords": [ "blank", "calendar", "date", "day", "month", "empty" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "blank-notepad", "keywords": [ "content", "notes", "book", "notepad", "notebook" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "block-bell-notification", "keywords": [ "notification", "alarm", "alert", "bell", "block" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bomb", "keywords": [ "delete", "bomb", "remove" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bookmark", "keywords": [ "bookmarks", "tags", "favorite" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "braces-circle", "keywords": [ "interface", "math", "braces", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "brightness-1", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "brightness-2", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls", "half" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "brightness-3", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls", "dot", "small" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "broken-link-2", "keywords": [ "break", "broken", "hyperlink", "link", "remove", "unlink", "chain" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bullet-list", "keywords": [ "points", "bullet", "unordered", "list", "lists", "bullets" ], "content": "\n\n\n" }, { "name": "calendar-add", "keywords": [ "add", "calendar", "date", "day", "month" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "calendar-edit", "keywords": [ "calendar", "date", "day", "compose", "edit", "note" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "calendar-jump-to-date", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "calendar-star", "keywords": [ "calendar", "date", "day", "favorite", "like", "month", "star" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "celsius", "keywords": [ "degrees", "temperature", "centigrade", "celsius", "degree", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "check", "keywords": [ "check", "form", "validation", "checkmark", "success", "add", "addition", "tick" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "check-square", "keywords": [ "check", "form", "validation", "checkmark", "success", "add", "addition", "box", "square", "tick" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "circle", "keywords": [ "geometric", "circle", "round", "design", "shape", "shapes", "shape" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "circle-clock", "keywords": [ "clock", "loading", "measure", "time", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "clipboard-add", "keywords": [ "edit", "task", "edition", "add", "clipboard", "form" ], "content": "\n\n\n" }, { "name": "clipboard-check", "keywords": [ "checkmark", "edit", "task", "edition", "checklist", "check", "success", "clipboard", "form" ], "content": "\n\n\n" }, { "name": "clipboard-remove", "keywords": [ "edit", "task", "edition", "remove", "delete", "clipboard", "form" ], "content": "\n\n\n" }, { "name": "cloud", "keywords": [ "cloud", "meteorology", "cloudy", "overcast", "cover", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cog", "keywords": [ "work", "loading", "cog", "gear", "settings", "machine" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "color-palette", "keywords": [ "color", "palette", "company", "office", "supplies", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "color-picker", "keywords": [ "color", "colors", "design", "dropper", "eye", "eyedrop", "eyedropper", "painting", "picker" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "color-swatches", "keywords": [ "color", "colors", "design", "painting", "palette", "sample", "swatch" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cone-shape", "keywords": [], "content": "\n\n\n" }, { "name": "convert-PDF-2", "keywords": [ "essential", "files", "folder", "convert", "to", "PDF" ], "content": "\n\n\n" }, { "name": "copy-paste", "keywords": [ "clipboard", "copy", "cut", "paste" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "creative-commons", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "crop-selection", "keywords": [ "artboard", "crop", "design", "image", "picture" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "crown", "keywords": [ "reward", "social", "rating", "media", "queen", "vip", "king", "crown" ], "content": "\n\n\n" }, { "name": "customer-support-1", "keywords": [ "customer", "headset", "help", "microphone", "phone", "support" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cut", "keywords": [ "coupon", "cut", "discount", "price", "prices", "scissors" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dark-dislay-mode", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dashboard-3", "keywords": [ "app", "application", "dashboard", "home", "layout", "vertical" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dashboard-circle", "keywords": [ "app", "application", "dashboard", "home", "layout", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "delete-1", "keywords": [ "remove", "add", "button", "buttons", "delete", "cross", "x", "mathematics", "multiply", "math" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "descending-number-order", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "disable-bell-notification", "keywords": [ "disable", "silent", "notification", "off", "silence", "alarm", "bell", "alert" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "disable-heart", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "division-circle", "keywords": [ "interface", "math", "divided", "by", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "download-box-1", "keywords": [ "arrow", "box", "down", "download", "internet", "network", "server", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "download-circle", "keywords": [ "arrow", "circle", "down", "download", "internet", "network", "server", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "download-computer", "keywords": [ "action", "actions", "computer", "desktop", "device", "display", "download", "monitor", "screen" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "download-file", "keywords": [], "content": "\n\n\n" }, { "name": "empty-clipboard", "keywords": [ "work", "plain", "clipboard", "task", "list", "company", "office" ], "content": "\n\n\n" }, { "name": "equal-sign", "keywords": [ "interface", "math", "equal", "sign", "mathematics" ], "content": "\n\n\n" }, { "name": "expand", "keywords": [ "big", "bigger", "design", "expand", "larger", "resize", "size", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "expand-horizontal-1", "keywords": [ "expand", "resize", "bigger", "horizontal", "smaller", "size", "arrow", "arrows", "big" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "expand-window-2", "keywords": [ "expand", "small", "bigger", "retract", "smaller", "big" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "face-scan-1", "keywords": [ "identification", "angle", "secure", "human", "id", "person", "face", "security", "brackets" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "factorial", "keywords": [ "interface", "math", "number", "factorial", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fahrenheit", "keywords": [ "degrees", "temperature", "fahrenheit", "degree", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fastforward-clock", "keywords": [ "time", "clock", "reset", "stopwatch", "circle", "measure", "loading" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "file-add-alternate", "keywords": [ "file", "common", "add" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "file-delete-alternate", "keywords": [ "file", "common", "delete", "cross" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "file-remove-alternate", "keywords": [ "file", "common", "remove", "minus", "subtract" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "filter-2", "keywords": [ "funnel", "filter", "angle", "oil" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fingerprint-1", "keywords": [ "identification", "password", "touch", "id", "secure", "fingerprint", "finger", "security" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fingerprint-2", "keywords": [ "identification", "password", "touch", "id", "secure", "fingerprint", "finger", "security" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fist", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fit-to-height-square", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flip-vertical-arrow-2", "keywords": [ "arrow", "design", "flip", "reflect", "up", "down" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flip-vertical-circle-1", "keywords": [ "flip", "bottom", "object", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flip-vertical-square-2", "keywords": [ "design", "up", "flip", "reflect", "vertical" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "folder-add", "keywords": [ "add", "folder", "plus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "folder-check", "keywords": [ "remove", "check", "folder" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "folder-delete", "keywords": [ "remove", "minus", "folder", "subtract", "delete" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "front-camera", "keywords": [], "content": "\n\n\n" }, { "name": "gif-format", "keywords": [], "content": "\n\n\n" }, { "name": "give-gift", "keywords": [ "reward", "social", "rating", "media", "queen", "vip", "gift" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "glasses", "keywords": [ "vision", "sunglasses", "protection", "spectacles", "correction", "sun", "eye", "glasses" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "half-star-1", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars", "half" ], "content": "\n\n\n" }, { "name": "hand-cursor", "keywords": [ "hand", "select", "cursor", "finger" ], "content": "\n\n\n" }, { "name": "hand-grab", "keywords": [ "hand", "select", "cursor", "finger", "grab" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "heading-1-paragraph-styles-heading", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "heading-2-paragraph-styles-heading", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "heading-3-paragraph-styles-heading", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "heart", "keywords": [ "reward", "social", "rating", "media", "heart", "it", "like", "favorite", "love" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "help-chat-2", "keywords": [ "bubble", "help", "mark", "message", "query", "question", "speech", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "help-question-1", "keywords": [ "circle", "faq", "frame", "help", "info", "mark", "more", "query", "question" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hierarchy-10", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n" }, { "name": "hierarchy-13", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hierarchy-14", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hierarchy-2", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hierarchy-4", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n" }, { "name": "hierarchy-7", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "home-3", "keywords": [ "home", "house", "roof", "shelter" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "home-4", "keywords": [ "home", "house", "roof", "shelter" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "horizontal-menu-circle", "keywords": [ "navigation", "dots", "three", "circle", "button", "horizontal", "menu" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "humidity-none", "keywords": [ "humidity", "drop", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "image-blur", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "image-saturation", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "information-circle", "keywords": [ "information", "frame", "info", "more", "help", "point", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "input-box", "keywords": [ "cursor", "text", "formatting", "type", "format" ], "content": "\n\n\n" }, { "name": "insert-side", "keywords": [ "points", "bullet", "align", "paragraph", "formatting", "bullets", "text" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "insert-top-left", "keywords": [ "alignment", "wrap", "formatting", "paragraph", "image", "left", "text" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "insert-top-right", "keywords": [ "paragraph", "image", "text", "alignment", "wrap", "right", "formatting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "invisible-1", "keywords": [ "disable", "eye", "eyeball", "hide", "off", "view" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "invisible-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "jump-object", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "key", "keywords": [ "entry", "key", "lock", "login", "pass", "unlock", "access" ], "content": "\n\n\n" }, { "name": "keyhole-lock-circle", "keywords": [ "circle", "frame", "key", "keyhole", "lock", "locked", "secure", "security" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lasso-tool", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "layers-1", "keywords": [ "design", "layer", "layers", "pile", "stack", "align" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "layers-2", "keywords": [ "design", "layer", "layers", "pile", "stack", "align" ], "content": "\n\n\n" }, { "name": "layout-window-1", "keywords": [ "column", "layout", "layouts", "left", "sidebar" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "layout-window-11", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "layout-window-2", "keywords": [ "column", "header", "layout", "layouts", "masthead", "sidebar" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "layout-window-8", "keywords": [ "grid", "header", "layout", "layouts", "masthead" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lightbulb", "keywords": [ "lighting", "light", "incandescent", "bulb", "lights" ], "content": "\n\n\n" }, { "name": "like-1", "keywords": [ "reward", "social", "up", "rating", "media", "like", "thumb", "hand" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "link-chain", "keywords": [ "create", "hyperlink", "link", "make", "unlink", "connection", "chain" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "live-video", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lock-rotation", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "login-1", "keywords": [ "arrow", "enter", "frame", "left", "login", "point", "rectangle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "logout-1", "keywords": [ "arrow", "exit", "frame", "leave", "logout", "rectangle", "right" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "loop-1", "keywords": [ "multimedia", "multi", "button", "repeat", "media", "loop", "infinity", "controls" ], "content": "\n\n\n" }, { "name": "magic-wand-2", "keywords": [ "design", "magic", "star", "supplies", "tool", "wand" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "magnifying-glass", "keywords": [ "glass", "search", "magnifying" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "magnifying-glass-circle", "keywords": [ "circle", "glass", "search", "magnifying" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "manual-book", "keywords": [], "content": "\n\n\n" }, { "name": "megaphone-2", "keywords": [ "bullhorn", "loud", "megaphone", "share", "speaker", "transmit" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "minimize-window-2", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "moon-cloud", "keywords": [ "cloud", "meteorology", "cloudy", "partly", "sunny", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "move-left", "keywords": [ "move", "left", "arrows" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "move-right", "keywords": [ "move", "right", "arrows" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "multiple-file-2", "keywords": [ "double", "common", "file" ], "content": "\n\n\n" }, { "name": "music-folder-song", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "new-file", "keywords": [ "empty", "common", "file", "content" ], "content": "\n\n\n" }, { "name": "new-folder", "keywords": [ "empty", "folder" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "new-sticky-note", "keywords": [ "empty", "common", "file" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "not-equal-sign", "keywords": [ "interface", "math", "not", "equal", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ok-hand", "keywords": [], "content": "\n\n\n" }, { "name": "one-finger-drag-horizontal", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "one-finger-drag-vertical", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "one-finger-hold", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "one-finger-tap", "keywords": [], "content": "\n\n\n" }, { "name": "open-book", "keywords": [ "content", "books", "book", "open" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "open-umbrella", "keywords": [ "storm", "rain", "umbrella", "open", "weather" ], "content": "\n\n\n" }, { "name": "padlock-square-1", "keywords": [ "combination", "combo", "lock", "locked", "padlock", "secure", "security", "shield", "keyhole" ], "content": "\n\n\n" }, { "name": "page-setting", "keywords": [ "page", "setting", "square", "triangle", "circle", "line", "combination", "variation" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paint-bucket", "keywords": [ "bucket", "color", "colors", "design", "paint", "painting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paint-palette", "keywords": [ "color", "colors", "design", "paint", "painting", "palette" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paintbrush-1", "keywords": [ "brush", "color", "colors", "design", "paint", "painting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paintbrush-2", "keywords": [ "brush", "color", "colors", "design", "paint", "painting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paperclip-1", "keywords": [ "attachment", "link", "paperclip", "unlink" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "paragraph", "keywords": [ "alignment", "paragraph", "formatting", "text" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-divide", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-exclude", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-intersect", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-merge", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-minus-front-1", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-trim", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pathfinder-union", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "peace-hand", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pen-3", "keywords": [ "content", "creation", "edit", "pen", "pens", "write" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pen-draw", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pen-tool", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pencil", "keywords": [ "change", "edit", "modify", "pencil", "write", "writing" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pentagon", "keywords": [ "pentagon", "design", "geometric", "shape", "shapes", "shape" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pi-symbol-circle", "keywords": [ "interface", "math", "pi", "sign", "mathematics", "22", "7" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pictures-folder-memories", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "podium", "keywords": [ "work", "desk", "notes", "company", "presentation", "office", "podium", "microphone" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "polygon", "keywords": [ "polygon", "octangle", "design", "geometric", "shape", "shapes", "shape" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "praying-hand", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "projector-board", "keywords": [ "projector", "screen", "work", "meeting", "presentation" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "pyramid-shape", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "quotation-2", "keywords": [ "quote", "quotation", "format", "formatting", "open", "close", "marks", "text" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "radioactive-2", "keywords": [ "warning", "radioactive", "radiation", "emergency", "danger", "safety" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rain-cloud", "keywords": [ "cloud", "rain", "rainy", "meteorology", "precipitation", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "recycle-bin-2", "keywords": [ "remove", "delete", "empty", "bin", "trash", "garbage" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ringing-bell-notification", "keywords": [ "notification", "vibrate", "ring", "sound", "alarm", "alert", "bell", "noise" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rock-and-roll-hand", "keywords": [], "content": "\n\n\n" }, { "name": "rotate-angle-45", "keywords": [ "rotate", "angle", "company", "office", "supplies", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "round-cap", "keywords": [], "content": "\n\n\n" }, { "name": "satellite-dish", "keywords": [ "broadcast", "satellite", "share", "transmit", "satellite" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "scanner", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "search-visual", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "select-circle-area-1", "keywords": [ "select", "area", "object", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "share-link", "keywords": [ "share", "transmit" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shield-1", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shield-2", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shield-check", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover", "check" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shield-cross", "keywords": [ "shield", "secure", "security", "cross", "add", "plus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shrink-horizontal-1", "keywords": [ "resize", "shrink", "bigger", "horizontal", "smaller", "size", "arrow", "arrows", "big" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shuffle", "keywords": [ "multimedia", "shuffle", "multi", "button", "controls", "media" ], "content": "\n\n\n" }, { "name": "sigma", "keywords": [ "formula", "text", "format", "sigma", "formatting", "sum" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "skull-1", "keywords": [ "crash", "death", "delete", "die", "error", "garbage", "remove", "skull", "trash" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sleep", "keywords": [], "content": "\n\n\n" }, { "name": "snow-flake", "keywords": [ "winter", "freeze", "snow", "freezing", "ice", "cold", "weather", "snowflake" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sort-descending", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "spiral-shape", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "split-vertical", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "spray-paint", "keywords": [ "can", "color", "colors", "design", "paint", "painting", "spray" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "square-brackets-circle", "keywords": [ "interface", "math", "brackets", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "square-cap", "keywords": [], "content": "\n\n\n" }, { "name": "square-clock", "keywords": [ "clock", "loading", "frame", "measure", "time", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "square-root-x-circle", "keywords": [ "interface", "math", "square", "root", "sign", "mathematics" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "star-1", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "star-2", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars", "spark" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "star-badge", "keywords": [ "ribbon", "reward", "like", "social", "rating", "media" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "straight-cap", "keywords": [], "content": "\n\n\n" }, { "name": "subtract-1", "keywords": [ "button", "delete", "buttons", "subtract", "horizontal", "remove", "line", "add", "mathematics", "math", "minus" ], "content": "\n\n\n" }, { "name": "subtract-circle", "keywords": [ "delete", "add", "circle", "subtract", "button", "buttons", "remove", "mathematics", "math", "minus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "subtract-square", "keywords": [ "subtract", "buttons", "remove", "add", "button", "square", "delete", "mathematics", "math", "minus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sun-cloud", "keywords": [ "cloud", "meteorology", "cloudy", "partly", "sunny", "weather" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "synchronize-disable", "keywords": [ "arrows", "loading", "load", "sync", "synchronize", "arrow", "reload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "synchronize-warning", "keywords": [ "arrow", "fail", "notification", "sync", "warning", "failure", "synchronize", "error" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "table-lamp-1", "keywords": [ "lighting", "light", "incandescent", "bulb", "lights", "table", "lamp" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tag", "keywords": [ "tags", "bookmark", "favorite" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "text-flow-rows", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "text-square", "keywords": [ "text", "options", "formatting", "format", "square", "color", "border", "fill" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "text-style", "keywords": [ "text", "style", "formatting", "format" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "thermometer", "keywords": [ "temperature", "thermometer", "weather", "level", "meter", "mercury", "measure" ], "content": "\n\n\n" }, { "name": "trending-content", "keywords": [ "lit", "flame", "torch", "trending" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "trophy", "keywords": [ "reward", "rating", "trophy", "social", "award", "media" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "two-finger-drag-hotizontal", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "two-finger-tap", "keywords": [], "content": "\n\n\n" }, { "name": "underline-text-1", "keywords": [ "text", "underline", "formatting", "format" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "upload-box-1", "keywords": [ "arrow", "box", "download", "internet", "network", "server", "up", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "upload-circle", "keywords": [ "arrow", "circle", "download", "internet", "network", "server", "up", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "upload-computer", "keywords": [ "action", "actions", "computer", "desktop", "device", "display", "monitor", "screen", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "upload-file", "keywords": [], "content": "\n\n\n" }, { "name": "user-add-plus", "keywords": [ "actions", "add", "close", "geometric", "human", "person", "plus", "single", "up", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-check-validate", "keywords": [ "actions", "close", "checkmark", "check", "geometric", "human", "person", "single", "success", "up", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-circle-single", "keywords": [ "circle", "geometric", "human", "person", "single", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-identifier-card", "keywords": [], "content": "\n\n\n" }, { "name": "user-multiple-circle", "keywords": [ "close", "geometric", "human", "multiple", "person", "up", "user", "circle" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-multiple-group", "keywords": [ "close", "geometric", "human", "multiple", "person", "up", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-profile-focus", "keywords": [ "close", "geometric", "human", "person", "profile", "focus", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-protection-2", "keywords": [ "shield", "secure", "security", "profile", "person" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-remove-subtract", "keywords": [ "actions", "remove", "close", "geometric", "human", "person", "minus", "single", "up", "user" ], "content": "\n\n\n" }, { "name": "user-single-neutral-male", "keywords": [ "close", "geometric", "human", "person", "single", "up", "user", "male" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "user-sync-online-in-person", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "vertical-slider-square", "keywords": [ "adjustment", "adjust", "controls", "fader", "vertical", "settings", "slider", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "video-swap-camera", "keywords": [], "content": "\n\n\n" }, { "name": "visible", "keywords": [ "eye", "eyeball", "open", "view" ], "content": "\n\n\n" }, { "name": "voice-scan-2", "keywords": [ "identification", "secure", "id", "soundwave", "sound", "voice", "brackets", "security" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "waning-cresent-moon", "keywords": [ "night", "new", "moon", "crescent", "weather", "time", "waning" ], "content": "\n\n\n" }, { "name": "warning-octagon", "keywords": [ "frame", "alert", "warning", "octagon", "exclamation", "caution" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "warning-triangle", "keywords": [ "frame", "alert", "warning", "triangle", "exclamation", "caution" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "mail": [ { "name": "chat-bubble-oval", "keywords": [ "messages", "message", "bubble", "chat", "oval" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-oval-notification", "keywords": [ "messages", "message", "bubble", "chat", "oval", "notify", "ping" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-oval-smiley-1", "keywords": [ "messages", "message", "bubble", "chat", "oval", "smiley", "smile" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-oval-smiley-2", "keywords": [ "messages", "message", "bubble", "chat", "oval", "smiley", "smile" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-square-block", "keywords": [ "messages", "message", "bubble", "chat", "square", "block" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-square-question", "keywords": [ "bubble", "square", "messages", "notification", "chat", "message", "question", "help" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-square-warning", "keywords": [ "bubble", "square", "messages", "notification", "chat", "message", "warning", "alert" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-square-write", "keywords": [ "messages", "message", "bubble", "chat", "square", "write", "review", "pen", "pencil", "compose" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-text-square", "keywords": [ "messages", "message", "bubble", "text", "square", "chat" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-bubble-typing-oval", "keywords": [ "messages", "message", "bubble", "typing", "chat" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "chat-two-bubbles-oval", "keywords": [ "messages", "message", "bubble", "chat", "oval", "conversation" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "discussion-converstion-reply", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "happy-face", "keywords": [ "smiley", "chat", "message", "smile", "emoji", "face", "satisfied" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-block", "keywords": [ "mail", "envelope", "email", "message", "block", "spam", "remove" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-favorite", "keywords": [ "mail", "envelope", "email", "message", "star", "favorite", "important", "bookmark" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-favorite-heart", "keywords": [ "mail", "envelope", "email", "message", "heart", "favorite", "like", "love", "important", "bookmark" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-lock", "keywords": [ "mail", "envelope", "email", "message", "secure", "password", "lock", "encryption" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-tray-1", "keywords": [ "mail", "email", "outbox", "drawer", "empty", "open", "inbox", "arrow", "down" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "inbox-tray-2", "keywords": [ "mail", "email", "outbox", "drawer", "empty", "open", "inbox", "arrow", "up" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "mail-incoming", "keywords": [ "inbox", "envelope", "email", "message", "down", "arrow", "inbox" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "mail-search", "keywords": [ "inbox", "envelope", "email", "message", "search" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "mail-send-email-message", "keywords": [ "send", "email", "paper", "airplane", "deliver" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "mail-send-envelope", "keywords": [ "envelope", "email", "message", "unopened", "sealed", "close" ], "content": "\n\n\n" }, { "name": "mail-send-reply-all", "keywords": [ "email", "message", "reply", "all", "actions", "action", "arrow" ], "content": "\n\n\n" }, { "name": "sad-face", "keywords": [ "smiley", "chat", "message", "emoji", "sad", "face", "unsatisfied" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "send-email", "keywords": [ "mail", "send", "email", "paper", "airplane" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sign-at", "keywords": [ "mail", "email", "at", "sign", "read", "address" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sign-hashtag", "keywords": [ "mail", "sharp", "sign", "hashtag", "tag" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-angry", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-cool", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-crying-1", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-cute", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-drool", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-emoji-kiss-nervous", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-emoji-terrified", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-grumpy", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-happy", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-in-love", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-kiss", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "smiley-laughing-3", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "map_travel": [ { "name": "airplane", "keywords": [ "travel", "plane", "adventure", "airplane", "transportation" ], "content": "\n\n\n" }, { "name": "airport-plane-transit", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "airport-plane", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "airport-security", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "anchor", "keywords": [ "anchor", "marina", "harbor", "port", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "baggage", "keywords": [ "check", "baggage", "travel", "adventure", "luggage", "bag", "checked", "airport" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "beach", "keywords": [ "island", "waves", "outdoor", "recreation", "tree", "beach", "palm", "wave", "water", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bicycle-bike", "keywords": [], "content": "\n\n\n" }, { "name": "braille-blind", "keywords": [ "disability", "braille", "blind" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bus", "keywords": [ "transportation", "travel", "bus", "transit", "transport", "motorcoach", "public" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "camping-tent", "keywords": [ "outdoor", "recreation", "camping", "tent", "teepee", "tipi", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cane", "keywords": [ "disability", "cane" ], "content": "\n\n\n" }, { "name": "capitol", "keywords": [ "capitol", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "car-battery-charging", "keywords": [], "content": "\n\n\n" }, { "name": "car-taxi-1", "keywords": [ "transportation", "travel", "taxi", "transport", "cab", "car" ], "content": "\n\n\n\n\n" }, { "name": "city-hall", "keywords": [ "city", "hall", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "compass-navigator", "keywords": [], "content": "\n\n\n" }, { "name": "crutch", "keywords": [ "disability", "crutch" ], "content": "\n\n\n" }, { "name": "dangerous-zone-sign", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "earth-1", "keywords": [ "planet", "earth", "globe", "world" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "earth-airplane", "keywords": [ "travel", "plane", "trip", "airplane", "international", "adventure", "globe", "world", "airport" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "emergency-exit", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fire-alarm-2", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "fire-extinguisher-sign", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "gas-station-fuel-petroleum", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hearing-deaf-1", "keywords": [ "disability", "hearing", "deaf" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hearing-deaf-2", "keywords": [ "disability", "hearing", "deaf" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "high-speed-train-front", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hot-spring", "keywords": [ "relax", "location", "outdoor", "recreation", "spa", "travel", "places" ], "content": "\n\n\n" }, { "name": "hotel-air-conditioner", "keywords": [ "heating", "ac", "air", "hvac", "cool", "cooling", "cold", "hot", "conditioning", "hotel" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hotel-bed-2", "keywords": [ "bed", "double", "bedroom", "bedrooms", "queen", "king", "full", "hotel", "hotel" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hotel-laundry", "keywords": [ "laundry", "machine", "hotel" ], "content": "\n\n\n" }, { "name": "hotel-one-star", "keywords": [ "one", "star", "reviews", "review", "rating", "hotel", "star" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "hotel-shower-head", "keywords": [ "bathe", "bath", "bathroom", "shower", "water", "head", "hotel" ], "content": "\n\n\n" }, { "name": "hotel-two-star", "keywords": [ "two", "stars", "reviews", "review", "rating", "hotel", "star" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "information-desk-customer", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "information-desk", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "iron", "keywords": [ "laundry", "iron", "heat", "hotel" ], "content": "\n\n\n" }, { "name": "ladder", "keywords": [ "business", "product", "metaphor", "ladder" ], "content": "\n\n\n" }, { "name": "lift", "keywords": [ "arrow", "up", "human", "down", "person", "user", "lift", "elevator" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lift-disability", "keywords": [ "arrow", "up", "human", "down", "person", "user", "lift", "elevator", "disability", "wheelchair", "accessible" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "location-compass-1", "keywords": [ "arrow", "compass", "location", "gps", "map", "maps", "point" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "location-pin-3", "keywords": [ "navigation", "map", "maps", "pin", "gps", "location" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "location-pin-disabled", "keywords": [ "navigation", "map", "maps", "pin", "gps", "location", "disabled", "off" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "location-target-1", "keywords": [ "navigation", "location", "map", "services", "maps", "gps", "target" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lost-and-found", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "man-symbol", "keywords": [ "geometric", "gender", "boy", "person", "male", "human", "user" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "map-fold", "keywords": [ "navigation", "map", "maps", "gps", "travel", "fold" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "navigation-arrow-off", "keywords": [ "compass", "arrow", "map", "bearing", "navigation", "maps", "heading", "gps", "off", "disable" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "navigation-arrow-on", "keywords": [ "compass", "arrow", "map", "bearing", "navigation", "maps", "heading", "gps", "off", "disable" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "parking-sign", "keywords": [ "discount", "coupon", "parking", "price", "prices", "hotel" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "parliament", "keywords": [ "travel", "places", "parliament" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "passport", "keywords": [ "travel", "book", "id", "adventure", "visa", "airport" ], "content": "\n\n\n" }, { "name": "pet-paw", "keywords": [ "paw", "foot", "animals", "pets", "footprint", "track", "hotel" ], "content": "\n\n\n" }, { "name": "pets-allowed", "keywords": [ "travel", "wayfinder", "pets", "allowed" ], "content": "\n\n\n" }, { "name": "pool-ladder", "keywords": [ "pool", "stairs", "swim", "swimming", "water", "ladder", "hotel" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rock-slide", "keywords": [ "hill", "cliff", "sign", "danger", "stone" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sail-ship", "keywords": [ "travel", "boat", "transportation", "transport", "ocean", "ship", "sea", "water" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "school-bus-side", "keywords": [], "content": "\n\n\n" }, { "name": "smoke-detector", "keywords": [ "smoke", "alert", "fire", "signal" ], "content": "\n\n\n" }, { "name": "smoking-area", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "snorkle", "keywords": [ "diving", "scuba", "outdoor", "recreation", "ocean", "mask", "water", "sea", "snorkle", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "steering-wheel", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "street-road", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "street-sign", "keywords": [ "crossroad", "street", "sign", "metaphor", "directions", "travel", "places" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "take-off", "keywords": [ "travel", "plane", "adventure", "airplane", "take", "off", "airport" ], "content": "\n\n\n" }, { "name": "toilet-man", "keywords": [ "travel", "wayfinder", "toilet", "man" ], "content": "\n\n\n" }, { "name": "toilet-sign-man-woman-2", "keywords": [ "toilet", "sign", "restroom", "bathroom", "user", "human", "person" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "toilet-women", "keywords": [ "travel", "wayfinder", "toilet", "women" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "traffic-cone", "keywords": [ "street", "sign", "traffic", "cone", "road" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "triangle-flag", "keywords": [ "navigation", "map", "maps", "flag", "gps", "location", "destination", "goal" ], "content": "\n\n\n" }, { "name": "wheelchair-1", "keywords": [ "person", "access", "wheelchair", "accomodation", "human", "disability", "disabled", "user" ], "content": "\n\n\n" }, { "name": "woman-symbol", "keywords": [ "geometric", "gender", "female", "person", "human", "user" ], "content": "\n\n\n" } ], "money_shopping": [ { "name": "annoncement-megaphone", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "backpack", "keywords": [ "bag", "backpack", "school", "baggage", "cloth", "clothing", "accessories" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-dollar", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-pound", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-rupee", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-suitcase-1", "keywords": [ "product", "business", "briefcase" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-suitcase-2", "keywords": [ "product", "business", "briefcase" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bag-yen", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ball", "keywords": [ "sports", "ball", "sport", "basketball", "shopping", "catergories" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bank", "keywords": [ "institution", "saving", "bank", "payment", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "beanie", "keywords": [ "beanie", "winter", "hat", "warm", "cloth", "clothing", "wearable", "accessories" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bill-1", "keywords": [ "billing", "bills", "payment", "finance", "cash", "currency", "money", "accounting" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bill-2", "keywords": [ "currency", "billing", "payment", "finance", "cash", "bill", "money", "accounting" ], "content": "\n\n\n" }, { "name": "bill-4", "keywords": [ "accounting", "billing", "payment", "finance", "cash", "currency", "money", "bill", "dollar", "stack" ], "content": "\n\n\n" }, { "name": "bill-cashless", "keywords": [ "currency", "billing", "payment", "finance", "no", "cash", "bill", "money", "accounting", "cashless" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "binance-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "binance", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bitcoin", "keywords": [ "crypto", "circle", "payment", "blokchain", "finance", "bitcoin", "money", "currency" ], "content": "\n\n\n" }, { "name": "bow-tie", "keywords": [ "bow", "tie", "dress", "gentleman", "cloth", "clothing", "accessories" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "briefcase-dollar", "keywords": [ "briefcase", "payment", "cash", "money", "finance", "baggage", "bag" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "building-2", "keywords": [ "real", "home", "tower", "building", "house", "estate" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "business-card", "keywords": [ "name", "card", "business", "information", "money", "payment" ], "content": "\n\n\n" }, { "name": "business-handshake", "keywords": [ "deal", "contract", "business", "money", "payment", "agreement" ], "content": "\n\n\n" }, { "name": "business-idea-money", "keywords": [], "content": "\n\n\n" }, { "name": "business-profession-home-office", "keywords": [ "workspace", "home", "office", "work", "business", "remote", "working" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "business-progress-bar-2", "keywords": [ "business", "production", "arrow", "workflow", "money", "flag", "timeline" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "business-user-curriculum", "keywords": [], "content": "\n\n\n" }, { "name": "calculator-1", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "calculate", "math" ], "content": "\n\n\n" }, { "name": "calculator-2", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "calculate", "math", "sign" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cane", "keywords": [ "walking", "stick", "cane", "accessories", "gentleman", "accessories" ], "content": "\n\n\n" }, { "name": "chair", "keywords": [ "chair", "business", "product", "comfort", "decoration", "sit", "furniture" ], "content": "\n\n\n" }, { "name": "closet", "keywords": [ "closet", "dressing", "dresser", "product", "decoration", "cloth", "clothing", "cabinet", "furniture" ], "content": "\n\n\n" }, { "name": "coin-share", "keywords": [ "payment", "cash", "money", "finance", "receive", "give", "coin", "hand" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "coins-stack", "keywords": [ "accounting", "billing", "payment", "stack", "cash", "coins", "currency", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "credit-card-1", "keywords": [ "credit", "pay", "payment", "debit", "card", "finance", "plastic", "money", "atm" ], "content": "\n\n\n" }, { "name": "credit-card-2", "keywords": [ "deposit", "payment", "finance", "atm", "withdraw", "atm" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "diamond-2", "keywords": [ "diamond", "money", "payment", "finance", "wealth", "jewelry" ], "content": "\n\n\n" }, { "name": "discount-percent-badge", "keywords": [ "shop", "shops", "stores", "discount", "coupon" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "discount-percent-circle", "keywords": [ "store", "shop", "shops", "stores", "discount", "coupon" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "discount-percent-coupon", "keywords": [ "shop", "shops", "stores", "discount", "coupon", "voucher" ], "content": "\n\n\n" }, { "name": "discount-percent-cutout", "keywords": [ "store", "shop", "shops", "stores", "discount", "coupon" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "discount-percent-fire", "keywords": [ "shop", "shops", "stores", "discount", "coupon", "hot", "trending" ], "content": "\n\n\n" }, { "name": "dollar-coin", "keywords": [ "accounting", "billing", "payment", "cash", "coin", "currency", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dollar-coin-1", "keywords": [ "accounting", "billing", "payment", "cash", "coin", "currency", "money", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dressing-table", "keywords": [ "makeup", "dressing", "table", "mirror", "cabinet", "product", "decoration", "furniture" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "ethereum", "keywords": [ "crypto", "circle", "payment", "blokchain", "finance", "ethereum", "eth", "currency" ], "content": "\n\n\n" }, { "name": "ethereum-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "eth", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "euro", "keywords": [ "exchange", "payment", "euro", "forex", "finance", "foreign", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "gift", "keywords": [ "reward", "box", "social", "present", "gift", "media", "rating", "bow" ], "content": "\n\n\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "gift-2", "keywords": [ "reward", "box", "social", "present", "gift", "media", "rating", "bow" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "gold", "keywords": [ "gold", "money", "payment", "bars", "finance", "wealth", "bullion", "jewelry" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "graph", "keywords": [ "analytics", "business", "product", "graph", "data", "chart", "analysis" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "graph-arrow-decrease", "keywords": [ "down", "stats", "graph", "descend", "right", "arrow" ], "content": "\n\n\n" }, { "name": "graph-arrow-increase", "keywords": [ "ascend", "growth", "up", "arrow", "stats", "graph", "right", "grow" ], "content": "\n\n\n" }, { "name": "graph-bar-decrease", "keywords": [ "arrow", "product", "performance", "down", "decrease", "graph", "business", "chart" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "graph-bar-increase", "keywords": [ "up", "product", "performance", "increase", "arrow", "graph", "business", "chart" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "graph-dot", "keywords": [ "product", "data", "bars", "analysis", "analytics", "graph", "business", "chart", "dot" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "investment-selection", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "justice-hammer", "keywords": [ "hammer", "work", "mallet", "office", "company", "gavel", "justice", "judge", "arbitration", "court" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "justice-scale-1", "keywords": [ "office", "work", "scale", "justice", "company", "arbitration", "balance", "court" ], "content": "\n\n\n" }, { "name": "justice-scale-2", "keywords": [ "office", "work", "scale", "justice", "unequal", "company", "arbitration", "unbalance", "court" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "lipstick", "keywords": [ "fashion", "beauty", "lip", "lipstick", "makeup", "shopping" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "make-up-brush", "keywords": [ "fashion", "beauty", "make", "up", "brush" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "moustache", "keywords": [ "fashion", "beauty", "moustache", "grooming" ], "content": "\n\n\n" }, { "name": "mouth-lip", "keywords": [ "fashion", "beauty", "mouth", "lip" ], "content": "\n\n\n" }, { "name": "necklace", "keywords": [ "diamond", "money", "payment", "finance", "wealth", "accessory", "necklace", "jewelry" ], "content": "\n\n\n" }, { "name": "necktie", "keywords": [ "necktie", "businessman", "business", "cloth", "clothing", "gentleman", "accessories" ], "content": "\n\n\n" }, { "name": "payment-10", "keywords": [ "deposit", "payment", "finance", "atm", "transfer", "dollar" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "payment-cash-out-3", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "pie-chart", "keywords": [ "product", "data", "analysis", "analytics", "pie", "business", "chart" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "piggy-bank", "keywords": [ "institution", "saving", "bank", "payment", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "polka-dot-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "polka", "dot", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "production-belt", "keywords": [ "production", "produce", "box", "belt", "factory", "product", "package", "business" ], "content": "\n\n\n" }, { "name": "qr-code", "keywords": [ "codes", "tags", "code", "qr" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "receipt", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "receipt-add", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "add", "plus", "new" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "receipt-check", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "check", "confirm" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "receipt-subtract", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "subtract", "minus", "remove" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "safe-vault", "keywords": [ "saving", "combo", "payment", "safe", "combination", "finance" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "scanner-3", "keywords": [ "payment", "electronic", "cash", "dollar", "codes", "tags", "upc", "barcode", "qr" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "scanner-bar-code", "keywords": [ "codes", "tags", "upc", "barcode" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shelf", "keywords": [ "shelf", "drawer", "cabinet", "prodcut", "decoration", "furniture" ], "content": "\n\n\n" }, { "name": "shopping-bag-hand-bag-2", "keywords": [ "shopping", "bag", "purse", "goods", "item", "products" ], "content": "\n\n\n" }, { "name": "shopping-basket-1", "keywords": [ "shopping", "basket" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-basket-2", "keywords": [ "shopping", "basket" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-1", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-2", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-3", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-add", "keywords": [ "shopping", "cart", "checkout", "add", "plus", "new" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-check", "keywords": [ "shopping", "cart", "checkout", "check", "confirm" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shopping-cart-subtract", "keywords": [ "shopping", "cart", "checkout", "subtract", "minus", "remove" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "signage-3", "keywords": [ "street", "sandwich", "shops", "shop", "stores", "board", "sign", "store" ], "content": "\n\n\n" }, { "name": "signage-4", "keywords": [ "street", "billboard", "shops", "shop", "stores", "board", "sign", "ads", "banner" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "startup", "keywords": [ "shop", "rocket", "launch", "startup" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "stock", "keywords": [ "price", "stock", "wallstreet", "dollar", "money", "currency", "fluctuate", "candlestick", "business" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "store-1", "keywords": [ "store", "shop", "shops", "stores" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "store-2", "keywords": [ "store", "shop", "shops", "stores" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "store-computer", "keywords": [ "store", "shop", "shops", "stores", "online", "computer", "website", "desktop", "app" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "subscription-cashflow", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tag", "keywords": [ "codes", "tags", "tag", "product", "label" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tall-hat", "keywords": [ "tall", "hat", "cloth", "clothing", "wearable", "magician", "gentleman", "accessories" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "target", "keywords": [ "shop", "bullseye", "arrow", "target" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "target-3", "keywords": [ "shop", "bullseye", "shooting", "target" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wallet", "keywords": [ "money", "payment", "finance", "wallet" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "wallet-purse", "keywords": [ "money", "payment", "finance", "wallet", "purse" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "xrp-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "xrp", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "yuan", "keywords": [ "exchange", "payment", "forex", "finance", "yuan", "foreign", "currency" ], "content": "\n\n\n" }, { "name": "yuan-circle", "keywords": [ "exchange", "payment", "forex", "finance", "yuan", "foreign", "currency" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "nature_ecology": [ { "name": "affordable-and-clean-energy", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "alien", "keywords": [ "science", "extraterristerial", "life", "form", "space", "universe", "head", "astronomy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bone", "keywords": [ "nature", "pet", "dog", "bone", "food", "snack" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cat-1", "keywords": [ "nature", "head", "cat", "pet", "animals", "felyne" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "circle-flask", "keywords": [ "science", "experiment", "lab", "flask", "chemistry", "solution" ], "content": "\n\n\n" }, { "name": "clean-water-and-sanitation", "keywords": [], "content": "\n\n\n" }, { "name": "comet", "keywords": [ "nature", "meteor", "fall", "space", "object", "danger" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "decent-work-and-economic-growth", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dna", "keywords": [ "science", "biology", "experiment", "lab", "science" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "erlenmeyer-flask", "keywords": [ "science", "experiment", "lab", "flask", "chemistry", "solution" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "flower", "keywords": [ "nature", "plant", "tree", "flower", "petals", "bloom" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "galaxy-1", "keywords": [ "science", "space", "universe", "astronomy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "galaxy-2", "keywords": [ "science", "space", "universe", "astronomy" ], "content": "\n\n\n" }, { "name": "gender-equality", "keywords": [], "content": "\n\n\n" }, { "name": "good-health-and-well-being", "keywords": [], "content": "\n\n\n" }, { "name": "industry-innovation-and-infrastructure", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "leaf", "keywords": [ "nature", "environment", "leaf", "ecology", "plant", "plants", "eco" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "log", "keywords": [ "nature", "tree", "plant", "circle", "round", "log" ], "content": "\n\n\n" }, { "name": "no-poverty", "keywords": [], "content": "\n\n\n" }, { "name": "octopus", "keywords": [ "nature", "sealife", "animals" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "planet", "keywords": [ "science", "solar", "system", "ring", "planet", "saturn", "space", "astronomy", "astronomy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "potted-flower-tulip", "keywords": [ "nature", "flower", "plant", "tree", "pot" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "quality-education", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rainbow", "keywords": [ "nature", "arch", "rain", "colorful", "rainbow", "curve", "half", "circle" ], "content": "\n\n\n" }, { "name": "recycle-1", "keywords": [ "nature", "sign", "environment", "protect", "save", "arrows" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "reduced-inequalities", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rose", "keywords": [ "nature", "flower", "rose", "plant", "tree" ], "content": "\n\n\n" }, { "name": "shell", "keywords": [ "nature", "sealife", "animals" ], "content": "\n\n\n" }, { "name": "shovel-rake", "keywords": [ "nature", "crops", "plants" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "sprout", "keywords": [], "content": "\n\n\n" }, { "name": "telescope", "keywords": [ "science", "experiment", "star", "gazing", "sky", "night", "space", "universe", "astronomy", "astronomy" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "test-tube", "keywords": [ "science", "experiment", "lab", "chemistry", "test", "tube", "solution" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tidal-wave", "keywords": [ "nature", "ocean", "wave" ], "content": "\n\n\n\n\n\n\n\n\n\n\n" }, { "name": "tree-2", "keywords": [ "nature", "tree", "plant", "circle", "round", "park" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "tree-3", "keywords": [ "nature", "tree", "plant", "cloud", "shape", "park" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "volcano", "keywords": [ "nature", "eruption", "erupt", "mountain", "volcano", "lava", "magma", "explosion" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "windmill", "keywords": [], "content": "\n\n\n" }, { "name": "zero-hunger", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "phone": [ { "name": "airplane-disabled", "keywords": [ "server", "plane", "airplane", "disabled", "off", "wireless", "mode", "internet", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "airplane-enabled", "keywords": [ "server", "plane", "airplane", "enabled", "on", "wireless", "mode", "internet", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "back-camera-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "camera", "lenses" ], "content": "\n\n\n" }, { "name": "call-hang-up", "keywords": [ "phone", "telephone", "mobile", "device", "smartphone", "call", "hang", "up" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cellular-network-4g", "keywords": [], "content": "\n\n\n" }, { "name": "cellular-network-5g", "keywords": [], "content": "\n\n\n" }, { "name": "cellular-network-lte", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "contact-phonebook-2", "keywords": [], "content": "\n\n\n" }, { "name": "hang-up-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "telephone", "hang", "up" ], "content": "\n\n\n" }, { "name": "hang-up-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "telephone", "hang", "up" ], "content": "\n\n\n" }, { "name": "incoming-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "incoming", "call" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "missed-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "missed", "call" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "notification-alarm-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "bell", "alarm" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "notification-application-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "box" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "notification-application-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "box" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "notification-message-alert", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "message", "text" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "outgoing-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "outgoing", "call" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "phone", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "phone-mobile-phone", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone" ], "content": "\n\n\n" }, { "name": "phone-qr", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "qr", "code", "scan" ], "content": "\n\n\n" }, { "name": "phone-ringing-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "incoming", "call" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "phone-ringing-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing" ], "content": "\n\n\n" }, { "name": "signal-full", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "bars", "full", "android" ], "content": "\n\n\n" }, { "name": "signal-low", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "low", "bars", "android" ], "content": "\n\n\n" }, { "name": "signal-medium", "keywords": [ "smartphone", "phone", "mobile", "device", "iphone", "signal", "medium", "wireless", "bar", "bars", "android" ], "content": "\n\n\n" }, { "name": "signal-none", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "bars", "no", "zero", "android" ], "content": "\n\n\n" } ], "programing": [ { "name": "application-add", "keywords": [ "application", "new", "add", "square" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bracket", "keywords": [ "code", "angle", "programming", "file", "bracket" ], "content": "\n\n\n" }, { "name": "browser-add", "keywords": [ "app", "code", "apps", "add", "window", "plus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-block", "keywords": [ "block", "access", "denied", "window", "browser", "privacy", "remove" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-build", "keywords": [ "build", "website", "development", "window", "code", "web", "backend", "browser", "dev" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-check", "keywords": [ "checkmark", "pass", "window", "app", "code", "success", "check", "apps" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-delete", "keywords": [ "app", "code", "apps", "fail", "delete", "window", "remove", "cross" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-hash", "keywords": [ "window", "hash", "code", "internet", "language", "browser", "web", "tag" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-lock", "keywords": [ "secure", "password", "window", "browser", "lock", "security", "login", "encryption" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-multiple-window", "keywords": [ "app", "code", "apps", "two", "window", "cascade" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-remove", "keywords": [ "app", "code", "apps", "subtract", "window", "minus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "browser-website-1", "keywords": [ "app", "code", "apps", "window", "website", "web" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug", "keywords": [ "code", "bug", "security", "programming", "secure", "computer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug-antivirus-debugging", "keywords": [ "code", "bug", "security", "programming", "secure", "computer", "antivirus", "block", "protection", "malware", "debugging" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug-antivirus-shield", "keywords": [ "code", "bug", "security", "programming", "secure", "computer", "antivirus", "shield", "protection", "malware" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug-virus-browser", "keywords": [ "bug", "browser", "file", "virus", "threat", "danger", "internet" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug-virus-document", "keywords": [ "bug", "document", "file", "virus", "threat", "danger" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "bug-virus-folder", "keywords": [ "bug", "document", "folder", "virus", "threat", "danger" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-add", "keywords": [ "cloud", "network", "internet", "add", "server", "plus" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-block", "keywords": [ "cloud", "network", "internet", "block", "server", "deny" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-check", "keywords": [ "cloud", "network", "internet", "check", "server", "approve" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-data-transfer", "keywords": [ "cloud", "data", "transfer", "internet", "server", "network" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-refresh", "keywords": [ "cloud", "network", "internet", "server", "refresh" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-share", "keywords": [ "cloud", "network", "internet", "server", "share" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-warning", "keywords": [ "cloud", "network", "internet", "server", "warning", "alert" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "cloud-wifi", "keywords": [ "cloud", "wifi", "internet", "server", "network" ], "content": "\n\n\n" }, { "name": "code-analysis", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "code-monitor-1", "keywords": [ "code", "tags", "angle", "bracket", "monitor" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "code-monitor-2", "keywords": [ "code", "tags", "angle", "image", "ui", "ux", "design" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "css-three", "keywords": [ "language", "three", "code", "programming", "html", "css" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "curly-brackets", "keywords": [], "content": "\n\n\n" }, { "name": "file-code-1", "keywords": [ "code", "files", "angle", "programming", "file", "bracket" ], "content": "\n\n\n" }, { "name": "incognito-mode", "keywords": [ "internet", "safe", "mode", "browser" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "insert-cloud-video", "keywords": [], "content": "\n\n\n" }, { "name": "markdown-circle-programming", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "markdown-document-programming", "keywords": [], "content": "\n\n\n" }, { "name": "module-puzzle-1", "keywords": [ "code", "puzzle", "module", "programming", "plugin", "piece" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "module-puzzle-3", "keywords": [ "code", "puzzle", "module", "programming", "plugin", "piece" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "module-three", "keywords": [ "code", "three", "module", "programming", "plugin" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "rss-square", "keywords": [ "wireless", "rss", "feed", "square", "transmit", "broadcast" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "shipping": [ { "name": "box-sign", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping", "this", "way", "up", "arrow", "sign", "sticker" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "container", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping", "container" ], "content": "\n\n\n" }, { "name": "fragile", "keywords": [ "fragile", "shipping", "glass", "delivery", "wine", "crack", "shipment", "sign", "sticker" ], "content": "\n\n\n" }, { "name": "parachute-drop", "keywords": [ "package", "box", "fulfillment", "cart", "warehouse", "shipping", "delivery", "drop", "parachute" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipment-add", "keywords": [ "shipping", "parcel", "shipment", "add" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipment-check", "keywords": [ "shipping", "parcel", "shipment", "check", "approved" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipment-download", "keywords": [ "shipping", "parcel", "shipment", "download" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipment-remove", "keywords": [ "shipping", "parcel", "shipment", "remove", "subtract" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipment-upload", "keywords": [ "shipping", "parcel", "shipment", "upload" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipping-box-1", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "shipping-truck", "keywords": [ "truck", "shipping", "delivery", "transfer" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "transfer-motorcycle", "keywords": [ "motorcycle", "shipping", "delivery", "courier", "transfer" ], "content": "\n\n\n" }, { "name": "transfer-van", "keywords": [ "van", "shipping", "delivery", "transfer" ], "content": "\n\n\n" }, { "name": "warehouse-1", "keywords": [ "delivery", "warehouse", "shipping", "fulfillment" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ], "work_education": [ { "name": "book-reading", "keywords": [ "book", "reading", "learning" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "class-lesson", "keywords": [ "class", "lesson", "education", "teacher" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "collaborations-idea", "keywords": [ "collaborations", "idea", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "definition-search-book", "keywords": [], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "dictionary-language-book", "keywords": [], "content": "\n\n\n" }, { "name": "global-learning", "keywords": [ "global", "learning", "education" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "graduation-cap", "keywords": [ "graduation", "cap", "education" ], "content": "\n\n\n" }, { "name": "group-meeting-call", "keywords": [ "group", "meeting", "call", "work" ], "content": "\n\n\n" }, { "name": "office-building-1", "keywords": [ "office", "building", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "office-worker", "keywords": [ "office", "worker", "human", "resources" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "search-dollar", "keywords": [ "search", "pay", "product", "currency", "query", "magnifying", "cash", "business", "money", "glass" ], "content": "\n\n\n\n\n\n\n\n\n\n" }, { "name": "strategy-tasks", "keywords": [ "strategy", "tasks", "work" ], "content": "\n\n\n" }, { "name": "task-list", "keywords": [ "task", "list", "work" ], "content": "\n\n\n" }, { "name": "workspace-desk", "keywords": [ "workspace", "desk", "work" ], "content": "\n\n\n\n\n\n\n\n\n\n" } ] } \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/test/images/sample.svg b/frontend/appflowy_flutter/assets/test/images/sample.svg deleted file mode 100644 index 7dcd6907d8..0000000000 --- a/frontend/appflowy_flutter/assets/test/images/sample.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_flutter/assets/test/workspaces/database/v069.afdb b/frontend/appflowy_flutter/assets/test/workspaces/database/v069.afdb deleted file mode 100644 index 9c497cff5d..0000000000 --- a/frontend/appflowy_flutter/assets/test/workspaces/database/v069.afdb +++ /dev/null @@ -1,14 +0,0 @@ -"{""id"":""RGmzka"",""name"":""Name"",""field_type"":0,""type_options"":{""0"":{""data"":""""}},""is_primary"":true}","{""id"":""oYoH-q"",""name"":""Time Slot"",""field_type"":2,""type_options"":{""0"":{""date_format"":3,""data"":"""",""time_format"":1,""timezone_id"":""""},""2"":{""date_format"":3,""time_format"":1,""timezone_id"":""""}},""is_primary"":false}","{""id"":""zVrp17"",""name"":""Amount"",""field_type"":1,""type_options"":{""1"":{""scale"":0,""format"":4,""name"":""Number"",""symbol"":""RUB""},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""_p4EGt"",""name"":""Delta"",""field_type"":1,""type_options"":{""1"":{""name"":""Number"",""format"":36,""symbol"":""RUB"",""scale"":0},""0"":{""data"":"""",""symbol"":""RUB"",""name"":""Number"",""format"":0,""scale"":0}},""is_primary"":false}","{""id"":""Z909lc"",""name"":""Email"",""field_type"":6,""type_options"":{""6"":{""url"":"""",""content"":""""},""0"":{""data"":"""",""content"":"""",""url"":""""}},""is_primary"":false}","{""id"":""dBrSc7"",""name"":""Registration Complete"",""field_type"":5,""type_options"":{""5"":{}},""is_primary"":false}","{""id"":""VoigvK"",""name"":""Progress"",""field_type"":7,""type_options"":{""0"":{""data"":""""},""7"":{}},""is_primary"":false}","{""id"":""gbbQwh"",""name"":""Attachments"",""field_type"":14,""type_options"":{""0"":{""data"":"""",""content"":""{\""files\"":[]}""},""14"":{""content"":""{\""files\"":[]}""}},""is_primary"":false}","{""id"":""id3L0G"",""name"":""Priority"",""field_type"":3,""type_options"":{""3"":{""content"":""{\""options\"":[{\""id\"":\""cplL\"",\""name\"":\""VIP\"",\""color\"":\""Purple\""},{\""id\"":\""GSf_\"",\""name\"":\""High\"",\""color\"":\""Blue\""},{\""id\"":\""qnja\"",\""name\"":\""Medium\"",\""color\"":\""Green\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""541SFC"",""name"":""Tags"",""field_type"":4,""type_options"":{""0"":{""data"":"""",""content"":""{\""options\"":[],\""disable_color\"":false}""},""4"":{""content"":""{\""options\"":[{\""id\"":\""1i4f\"",\""name\"":\""Education\"",\""color\"":\""Yellow\""},{\""id\"":\""yORP\"",\""name\"":\""Health\"",\""color\"":\""Orange\""},{\""id\"":\""SEUo\"",\""name\"":\""Hobby\"",\""color\"":\""LightPink\""},{\""id\"":\""uRAO\"",\""name\"":\""Family\"",\""color\"":\""Pink\""},{\""id\"":\""R9I7\"",\""name\"":\""Work\"",\""color\"":\""Purple\""}],\""disable_color\"":false}""}},""is_primary"":false}","{""id"":""lg0B7O"",""name"":""Last modified"",""field_type"":8,""type_options"":{""0"":{""time_format"":1,""field_type"":8,""date_format"":3,""data"":"""",""include_time"":true},""8"":{""date_format"":3,""field_type"":8,""time_format"":1,""include_time"":true}},""is_primary"":false}","{""id"":""5riGR7"",""name"":""Created at"",""field_type"":9,""type_options"":{""0"":{""field_type"":9,""include_time"":true,""date_format"":3,""time_format"":1,""data"":""""},""9"":{""include_time"":true,""field_type"":9,""date_format"":3,""time_format"":1}},""is_primary"":false}" -"{""data"":""Olaf"",""created_at"":1726063289,""last_modified"":1726063289,""field_type"":0}","{""last_modified"":1726122374,""created_at"":1726110045,""reminder_id"":"""",""is_range"":true,""include_time"":true,""end_timestamp"":""1725415200"",""field_type"":2,""data"":""1725256800""}","{""field_type"":1,""data"":""55200"",""last_modified"":1726063592,""created_at"":1726063592}","{""last_modified"":1726062441,""created_at"":1726062441,""data"":""0.5"",""field_type"":1}","{""created_at"":1726063719,""last_modified"":1726063732,""data"":""doyouwannabuildasnowman@arendelle.gov"",""field_type"":6}",,"{""field_type"":7,""last_modified"":1726064207,""data"":""{\""options\"":[{\""id\"":\""oqXQ\"",\""name\"":\""find elsa\"",\""color\"":\""Purple\""},{\""id\"":\""eQwp\"",\""name\"":\""find anna\"",\""color\"":\""Purple\""},{\""id\"":\""5-B3\"",\""name\"":\""play in the summertime\"",\""color\"":\""Purple\""},{\""id\"":\""UBFn\"",\""name\"":\""get a personal flurry\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""oqXQ\"",\""eQwp\"",\""UBFn\""]}"",""created_at"":1726064129}",,"{""created_at"":1726065208,""data"":""cplL"",""last_modified"":1726065282,""field_type"":3}","{""field_type"":4,""data"":""1i4f"",""last_modified"":1726105102,""created_at"":1726105102}","{""field_type"":8,""data"":""1726122374""}","{""data"":""1726060476"",""field_type"":9}" -"{""field_type"":0,""last_modified"":1726063323,""data"":""Beatrice"",""created_at"":1726063323}",,"{""last_modified"":1726063638,""data"":""828600"",""created_at"":1726063607,""field_type"":1}","{""field_type"":1,""created_at"":1726062488,""data"":""-2.25"",""last_modified"":1726062488}","{""last_modified"":1726063790,""data"":""btreee17@gmail.com"",""field_type"":6,""created_at"":1726063790}","{""created_at"":1726062718,""data"":""Yes"",""field_type"":5,""last_modified"":1726062724}","{""created_at"":1726064277,""data"":""{\""options\"":[{\""id\"":\""BDuH\"",\""name\"":\""get the leaf node\"",\""color\"":\""Purple\""},{\""id\"":\""GXAr\"",\""name\"":\""upgrade to b+\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""field_type"":7,""last_modified"":1726064293}",,"{""data"":""GSf_"",""created_at"":1726065288,""last_modified"":1726065288,""field_type"":3}","{""created_at"":1726105110,""data"":""yORP,uRAO"",""last_modified"":1726105111,""field_type"":4}","{""data"":""1726105111"",""field_type"":8}","{""field_type"":9,""data"":""1726060476""}" -"{""last_modified"":1726063355,""created_at"":1726063355,""field_type"":0,""data"":""Lancelot""}","{""data"":""1726468159"",""is_range"":true,""end_timestamp"":""1726727359"",""reminder_id"":"""",""include_time"":false,""field_type"":2,""created_at"":1726122403,""last_modified"":1726122559}","{""created_at"":1726063617,""last_modified"":1726063617,""data"":""22500"",""field_type"":1}","{""data"":""11.6"",""last_modified"":1726062504,""field_type"":1,""created_at"":1726062504}","{""field_type"":6,""data"":""sir.lancelot@gmail.com"",""last_modified"":1726063812,""created_at"":1726063812}","{""data"":""No"",""field_type"":5,""last_modified"":1726062724,""created_at"":1726062375}",,,"{""data"":""cplL"",""created_at"":1726065286,""last_modified"":1726065286,""field_type"":3}","{""last_modified"":1726105237,""data"":""SEUo"",""created_at"":1726105237,""field_type"":4}","{""field_type"":8,""data"":""1726122559""}","{""field_type"":9,""data"":""1726060476""}" -"{""data"":""Scotty"",""last_modified"":1726063399,""created_at"":1726063399,""field_type"":0}","{""reminder_id"":"""",""last_modified"":1726122418,""include_time"":true,""data"":""1725868800"",""end_timestamp"":""1726646400"",""created_at"":1726122381,""field_type"":2,""is_range"":true}","{""created_at"":1726063650,""last_modified"":1726063650,""data"":""10900"",""field_type"":1}","{""data"":""0"",""created_at"":1726062581,""last_modified"":1726062581,""field_type"":1}","{""last_modified"":1726063835,""created_at"":1726063835,""field_type"":6,""data"":""scottylikestosing@outlook.com""}","{""data"":""Yes"",""field_type"":5,""created_at"":1726062718,""last_modified"":1726062718}","{""created_at"":1726064309,""data"":""{\""options\"":[{\""id\"":\""Cw0K\"",\""name\"":\""vocal warmup\"",\""color\"":\""Purple\""},{\""id\"":\""nYMo\"",\""name\"":\""mixed voice training\"",\""color\"":\""Purple\""},{\""id\"":\""i-OX\"",\""name\"":\""belting training\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""Cw0K\"",\""nYMo\"",\""i-OX\""]}"",""field_type"":7,""last_modified"":1726064325}","{""last_modified"":1726122911,""created_at"":1726122835,""data"":[""{\""id\"":\""746a741d-98f8-4cc6-b807-a82d2e78c221\"",\""name\"":\""googlelogo_color_272x92dp.png\"",\""url\"":\""https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}"",""{\""id\"":\""cbbab3ee-32ab-4438-a909-3f69f935a8bd\"",\""name\"":\""tL_v571NdZ0.svg\"",\""url\"":\""https://static.xx.fbcdn.net/rsrc.php/y9/r/tL_v571NdZ0.svg\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Link\""}""],""field_type"":14}",,"{""data"":""SEUo,yORP"",""field_type"":4,""last_modified"":1726105123,""created_at"":1726105115}","{""data"":""1726122911"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}" -"{""field_type"":0,""created_at"":1726063405,""last_modified"":1726063421,""data"":""""}",,,"{""last_modified"":1726062625,""field_type"":1,""data"":"""",""created_at"":1726062607}",,"{""data"":""No"",""last_modified"":1726062702,""created_at"":1726062393,""field_type"":5}",,,,,"{""data"":""1726063421"",""field_type"":8}","{""data"":""1726060539"",""field_type"":9}" -"{""field_type"":0,""data"":""Thomas"",""last_modified"":1726063421,""created_at"":1726063421}","{""reminder_id"":"""",""field_type"":2,""data"":""1725627600"",""is_range"":false,""created_at"":1726122583,""last_modified"":1726122593,""end_timestamp"":"""",""include_time"":true}","{""last_modified"":1726063666,""field_type"":1,""data"":""465800"",""created_at"":1726063666}","{""last_modified"":1726062516,""field_type"":1,""created_at"":1726062516,""data"":""-0.03""}","{""field_type"":6,""last_modified"":1726063848,""created_at"":1726063848,""data"":""tfp3827@gmail.com""}","{""field_type"":5,""last_modified"":1726062725,""data"":""Yes"",""created_at"":1726062376}","{""created_at"":1726064344,""data"":""{\""options\"":[{\""id\"":\""D6X8\"",\""name\"":\""brainstorm\"",\""color\"":\""Purple\""},{\""id\"":\""XVN9\"",\""name\"":\""schedule\"",\""color\"":\""Purple\""},{\""id\"":\""nJx8\"",\""name\"":\""shoot\"",\""color\"":\""Purple\""},{\""id\"":\""7Mrm\"",\""name\"":\""edit\"",\""color\"":\""Purple\""},{\""id\"":\""o6vg\"",\""name\"":\""publish\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""D6X8\""]}"",""last_modified"":1726064379,""field_type"":7}",,"{""last_modified"":1726065298,""created_at"":1726065298,""field_type"":3,""data"":""GSf_""}","{""data"":""yORP,SEUo"",""field_type"":4,""last_modified"":1726105229,""created_at"":1726105229}","{""data"":""1726122593"",""field_type"":8}","{""field_type"":9,""data"":""1726060540""}" -"{""data"":""Juan"",""last_modified"":1726063423,""created_at"":1726063423,""field_type"":0}","{""created_at"":1726122510,""reminder_id"":"""",""include_time"":false,""is_range"":true,""last_modified"":1726122515,""data"":""1725604115"",""end_timestamp"":""1725776915"",""field_type"":2}","{""field_type"":1,""created_at"":1726063677,""last_modified"":1726063677,""data"":""93100""}","{""field_type"":1,""data"":""4.86"",""created_at"":1726062597,""last_modified"":1726062597}",,"{""last_modified"":1726062377,""field_type"":5,""data"":""Yes"",""created_at"":1726062377}","{""last_modified"":1726064412,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""tTDq\"",\""name\"":\""complete onboarding\"",\""color\"":\""Purple\""},{\""id\"":\""E8Ds\"",\""name\"":\""contact support\"",\""color\"":\""Purple\""},{\""id\"":\""RoGN\"",\""name\"":\""get started\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""tTDq\"",\""E8Ds\""]}"",""created_at"":1726064396}",,"{""created_at"":1726065278,""field_type"":3,""data"":""qnja"",""last_modified"":1726065278}","{""data"":""R9I7,yORP,1i4f"",""field_type"":4,""created_at"":1726105126,""last_modified"":1726105127}","{""data"":""1726122515"",""field_type"":8}","{""data"":""1726060541"",""field_type"":9}" -"{""data"":""Alex"",""created_at"":1726063432,""last_modified"":1726063432,""field_type"":0}","{""reminder_id"":"""",""data"":""1725292800"",""include_time"":true,""last_modified"":1726122448,""created_at"":1726122422,""is_range"":true,""end_timestamp"":""1725551940"",""field_type"":2}","{""field_type"":1,""last_modified"":1726063683,""created_at"":1726063683,""data"":""3560""}","{""created_at"":1726062561,""data"":""1.96"",""last_modified"":1726062561,""field_type"":1}","{""last_modified"":1726063952,""created_at"":1726063931,""data"":""al3x1343@protonmail.com"",""field_type"":6}","{""last_modified"":1726062375,""field_type"":5,""created_at"":1726062375,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""qNyr\"",\""name\"":\""finish reading book\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064616,""last_modified"":1726064616,""field_type"":7}",,"{""data"":""qnja"",""created_at"":1726065272,""last_modified"":1726065272,""field_type"":3}","{""created_at"":1726105180,""last_modified"":1726105180,""field_type"":4,""data"":""R9I7,1i4f""}","{""field_type"":8,""data"":""1726122448""}","{""field_type"":9,""data"":""1726060541""}" -"{""last_modified"":1726063478,""created_at"":1726063436,""field_type"":0,""data"":""Alexander""}",,"{""field_type"":1,""last_modified"":1726063691,""created_at"":1726063691,""data"":""2073""}","{""field_type"":1,""data"":""0.5"",""last_modified"":1726062577,""created_at"":1726062577}","{""last_modified"":1726063991,""field_type"":6,""created_at"":1726063991,""data"":""alexandernotthedra@gmail.com""}","{""field_type"":5,""last_modified"":1726062378,""created_at"":1726062377,""data"":""No""}",,,"{""created_at"":1726065291,""data"":""GSf_"",""last_modified"":1726065291,""field_type"":3}","{""last_modified"":1726105142,""created_at"":1726105133,""data"":""SEUo"",""field_type"":4}","{""field_type"":8,""data"":""1726105142""}","{""field_type"":9,""data"":""1726060542""}" -"{""field_type"":0,""created_at"":1726063454,""last_modified"":1726063454,""data"":""George""}","{""created_at"":1726122467,""end_timestamp"":""1726468070"",""include_time"":false,""is_range"":true,""reminder_id"":"""",""field_type"":2,""data"":""1726295270"",""last_modified"":1726122470}",,,"{""field_type"":6,""data"":""george.aq@appflowy.io"",""last_modified"":1726064104,""created_at"":1726064016}","{""last_modified"":1726062376,""created_at"":1726062376,""field_type"":5,""data"":""Yes""}","{""data"":""{\""options\"":[{\""id\"":\""s_dQ\"",\""name\"":\""bug triage\"",\""color\"":\""Purple\""},{\""id\"":\""-Zfo\"",\""name\"":\""fix bugs\"",\""color\"":\""Purple\""},{\""id\"":\""wsDN\"",\""name\"":\""attend meetings\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""s_dQ\"",\""-Zfo\""]}"",""last_modified"":1726064468,""created_at"":1726064424,""field_type"":7}","{""data"":[""{\""id\"":\""8a77f84d-64e9-4e67-b902-fa23980459ec\"",\""name\"":\""BQdTmxpRI6f.png\"",\""url\"":\""https://static.cdninstagram.com/rsrc.php/v3/ym/r/BQdTmxpRI6f.png\"",\""upload_type\"":\""NetworkMedia\"",\""file_type\"":\""Image\""}""],""field_type"":14,""created_at"":1726122956,""last_modified"":1726122956}","{""field_type"":3,""data"":""qnja"",""created_at"":1726065313,""last_modified"":1726065313}","{""data"":""R9I7,yORP"",""field_type"":4,""last_modified"":1726105198,""created_at"":1726105187}","{""data"":""1726122956"",""field_type"":8}","{""data"":""1726060543"",""field_type"":9}" -"{""field_type"":0,""last_modified"":1726063467,""data"":""Joanna"",""created_at"":1726063467}","{""include_time"":false,""end_timestamp"":""1727072893"",""is_range"":true,""last_modified"":1726122493,""created_at"":1726122483,""data"":""1726554493"",""field_type"":2,""reminder_id"":""""}","{""last_modified"":1726065463,""data"":""16470"",""field_type"":1,""created_at"":1726065463}","{""created_at"":1726062626,""field_type"":1,""last_modified"":1726062626,""data"":""-5.36""}","{""last_modified"":1726064069,""data"":""joannastrawberry29+hello@gmail.com"",""created_at"":1726064069,""field_type"":6}",,"{""field_type"":7,""created_at"":1726064444,""last_modified"":1726064460,""data"":""{\""options\"":[{\""id\"":\""ZxJz\"",\""name\"":\""post on Twitter\"",\""color\"":\""Purple\""},{\""id\"":\""upwi\"",\""name\"":\""watch Youtube videos\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""upwi\""]}""}",,"{""created_at"":1726065317,""last_modified"":1726065317,""field_type"":3,""data"":""qnja""}","{""field_type"":4,""last_modified"":1726105173,""data"":""uRAO,yORP"",""created_at"":1726105170}","{""data"":""1726122493"",""field_type"":8}","{""data"":""1726060545"",""field_type"":9}" -"{""last_modified"":1726063457,""created_at"":1726063457,""data"":""George"",""field_type"":0}","{""include_time"":true,""reminder_id"":"""",""field_type"":2,""is_range"":true,""created_at"":1726122521,""end_timestamp"":""1725829200"",""data"":""1725822900"",""last_modified"":1726122535}","{""last_modified"":1726065493,""field_type"":1,""data"":""9500"",""created_at"":1726065493}","{""last_modified"":1726062680,""created_at"":1726062680,""field_type"":1,""data"":""1.7""}","{""data"":""plgeorgebball@gmail.com"",""field_type"":6,""last_modified"":1726064087,""created_at"":1726064036}",,"{""last_modified"":1726064513,""data"":""{\""options\"":[{\""id\"":\""zy0x\"",\""name\"":\""game vs celtics\"",\""color\"":\""Purple\""},{\""id\"":\""WJsv\"",\""name\"":\""training\"",\""color\"":\""Purple\""},{\""id\"":\""w-f8\"",\""name\"":\""game vs spurs\"",\""color\"":\""Purple\""},{\""id\"":\""p1VQ\"",\""name\"":\""game vs knicks\"",\""color\"":\""Purple\""},{\""id\"":\""VjUA\"",\""name\"":\""recovery\"",\""color\"":\""Purple\""},{\""id\"":\""sQ8X\"",\""name\"":\""don't get injured\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[]}"",""created_at"":1726064486,""field_type"":7}",,"{""field_type"":3,""last_modified"":1726065310,""data"":""qnja"",""created_at"":1726065310}","{""created_at"":1726105205,""field_type"":4,""last_modified"":1726105249,""data"":""R9I7,1i4f,yORP,SEUo""}","{""data"":""1726122535"",""field_type"":8}","{""field_type"":9,""data"":""1726060546""}" -"{""data"":""Judy"",""created_at"":1726063475,""field_type"":0,""last_modified"":1726063487}","{""end_timestamp"":"""",""reminder_id"":"""",""data"":""1726640950"",""field_type"":2,""include_time"":false,""created_at"":1726122550,""last_modified"":1726122550,""is_range"":false}",,,"{""created_at"":1726063882,""field_type"":6,""last_modified"":1726064000,""data"":""judysmithjr@outlook.com""}","{""last_modified"":1726062712,""field_type"":5,""data"":""Yes"",""created_at"":1726062712}","{""created_at"":1726064549,""field_type"":7,""data"":""{\""options\"":[{\""id\"":\""j8cC\"",\""name\"":\""finish training\"",\""color\"":\""Purple\""},{\""id\"":\""SmSk\"",\""name\"":\""brainwash\"",\""color\"":\""Purple\""},{\""id\"":\""mnf5\"",\""name\"":\""welcome to ba sing se\"",\""color\"":\""Purple\""},{\""id\"":\""hcrj\"",\""name\"":\""don't mess up\"",\""color\"":\""Purple\""}],\""selected_option_ids\"":[\""j8cC\"",\""SmSk\"",\""mnf5\"",\""hcrj\""]}"",""last_modified"":1726064591}",,,"{""field_type"":4,""last_modified"":1726105152,""created_at"":1726105152,""data"":""R9I7""}","{""field_type"":8,""data"":""1726122550""}","{""field_type"":9,""data"":""1726060549""}" diff --git a/frontend/appflowy_flutter/assets/translations/mr-IN.json b/frontend/appflowy_flutter/assets/translations/mr-IN.json deleted file mode 100644 index f86a1e0081..0000000000 --- a/frontend/appflowy_flutter/assets/translations/mr-IN.json +++ /dev/null @@ -1,3210 +0,0 @@ -{ - "appName": "AppFlowy", - "defaultUsername": "मी", - "welcomeText": "@:appName मध्ये आ पले स्वागत आहे.", - "welcomeTo": "मध्ये आ पले स्वागत आ हे", - "githubStarText": "GitHub वर स्टार करा", - "subscribeNewsletterText": "वृत्तपत्राची सदस्यता घ्या", - "letsGoButtonText": "क्विक स्टार्ट", - "title": "Title", - "youCanAlso": "तुम्ही देखील", - "and": "आ णि", - "failedToOpenUrl": "URL उघडण्यात अयशस्वी: {}", - "blockActions": { - "addBelowTooltip": "खाली जोडण्यासाठी क्लिक करा", - "addAboveCmd": "Alt+click", - "addAboveMacCmd": "Option+click", - "addAboveTooltip": "वर जोडण्यासाठी", - "dragTooltip": "Drag to move", - "openMenuTooltip": "मेनू उघडण्यासाठी क्लिक करा" - }, - "signUp": { - "buttonText": "साइन अप", - "title": "साइन अप to @:appName", - "getStartedText": "सुरुवात करा", - "emptyPasswordError": "पासवर्ड रिकामा असू शकत नाही", - "repeatPasswordEmptyError": "Repeat पासवर्ड रिकामा असू शकत नाही", - "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", - "alreadyHaveAnAccount": "आधीच खाते आहे?", - "emailHint": "Email", - "passwordHint": "Password", - "repeatPasswordHint": "पासवर्ड पुन्हा लिहा", - "signUpWith": "यामध्ये साइन अप करा:" - }, - "signIn": { - "loginTitle": "@:appName मध्ये लॉगिन करा", - "loginButtonText": "लॉगिन", - "loginStartWithAnonymous": "अनामिक सत्रासह पुढे जा", - "continueAnonymousUser": "अनामिक सत्रासह पुढे जा", - "anonymous": "अनामिक", - "buttonText": "साइन इन", - "signingInText": "साइन इन होत आहे...", - "forgotPassword": "पासवर्ड विसरलात?", - "emailHint": "ईमेल", - "passwordHint": "पासवर्ड", - "dontHaveAnAccount": "तुमचं खाते नाही?", - "createAccount": "खाते तयार करा", - "repeatPasswordEmptyError": "पुन्हा पासवर्ड रिकामा असू शकत नाही", - "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", - "syncPromptMessage": "डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका", - "or": "किंवा", - "signInWithGoogle": "Google सह पुढे जा", - "signInWithGithub": "GitHub सह पुढे जा", - "signInWithDiscord": "Discord सह पुढे जा", - "signInWithApple": "Apple सह पुढे जा", - "continueAnotherWay": "इतर पर्यायांनी पुढे जा", - "signUpWithGoogle": "Google सह साइन अप करा", - "signUpWithGithub": "GitHub सह साइन अप करा", - "signUpWithDiscord": "Discord सह साइन अप करा", - "signInWith": "यासह पुढे जा:", - "signInWithEmail": "ईमेलसह पुढे जा", - "signInWithMagicLink": "पुढे जा", - "signUpWithMagicLink": "Magic Link सह साइन अप करा", - "pleaseInputYourEmail": "कृपया तुमचा ईमेल पत्ता टाका", - "settings": "सेटिंग्ज", - "magicLinkSent": "Magic Link पाठवण्यात आली आहे!", - "invalidEmail": "कृपया वैध ईमेल पत्ता टाका", - "alreadyHaveAnAccount": "आधीच खाते आहे?", - "logIn": "लॉगिन", - "generalError": "काहीतरी चुकलं. कृपया नंतर प्रयत्न करा", - "limitRateError": "सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता", - "magicLinkSentDescription": "तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल." - }, - "workspace": { - "chooseWorkspace": "तुमचे workspace निवडा", - "defaultName": "माझे Workspace", - "create": "नवीन workspace तयार करा", - "new": "नवीन workspace", - "importFromNotion": "Notion मधून आयात करा", - "learnMore": "अधिक जाणून घ्या", - "reset": "workspace रीसेट करा", - "renameWorkspace": "workspace चे नाव बदला", - "workspaceNameCannotBeEmpty": "workspace चे नाव रिकामे असू शकत नाही", - "resetWorkspacePrompt": "workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.", - "hint": "workspace", - "notFoundError": "workspace सापडले नाही", - "failedToLoad": "काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.", - "errorActions": { - "reportIssue": "समस्या नोंदवा", - "reportIssueOnGithub": "Github वर समस्या नोंदवा", - "exportLogFiles": "लॉग फाइल्स निर्यात करा", - "reachOut": "Discord वर संपर्क करा" - }, - "menuTitle": "कार्यक्षेत्रे", - "deleteWorkspaceHintText": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.", - "createSuccess": "कार्यक्षेत्र यशस्वीरित्या तयार झाले", - "createFailed": "कार्यक्षेत्र तयार करण्यात अयशस्वी", - "createLimitExceeded": "तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.", - "deleteSuccess": "कार्यक्षेत्र यशस्वीरित्या हटवले गेले", - "deleteFailed": "कार्यक्षेत्र हटवण्यात अयशस्वी", - "openSuccess": "कार्यक्षेत्र यशस्वीरित्या उघडले", - "openFailed": "कार्यक्षेत्र उघडण्यात अयशस्वी", - "renameSuccess": "कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले", - "renameFailed": "कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी", - "updateIconSuccess": "कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले", - "updateIconFailed": "कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी", - "cannotDeleteTheOnlyWorkspace": "फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही", - "fetchWorkspacesFailed": "कार्यक्षेत्रे मिळवण्यात अयशस्वी", - "leaveCurrentWorkspace": "कार्यक्षेत्र सोडा", - "leaveCurrentWorkspacePrompt": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?" - }, - "shareAction": { - "buttonText": "शेअर करा", - "workInProgress": "लवकरच येत आहे", - "markdown": "Markdown", - "html": "HTML", - "clipboard": "क्लिपबोर्डवर कॉपी करा", - "csv": "CSV", - "copyLink": "लिंक कॉपी करा", - "publishToTheWeb": "वेबवर प्रकाशित करा", - "publishToTheWebHint": "AppFlowy सह वेबसाइट तयार करा", - "publish": "प्रकाशित करा", - "unPublish": "अप्रकाशित करा", - "visitSite": "साइटला भेट द्या", - "exportAsTab": "या स्वरूपात निर्यात करा", - "publishTab": "प्रकाशित करा", - "shareTab": "शेअर करा", - "publishOnAppFlowy": "AppFlowy वर प्रकाशित करा", - "shareTabTitle": "सहकार्य करण्यासाठी आमंत्रित करा", - "shareTabDescription": "कोणासही सहज सहकार्य करण्यासाठी", - "copyLinkSuccess": "लिंक क्लिपबोर्डवर कॉपी केली", - "copyShareLink": "शेअर लिंक कॉपी करा", - "copyLinkFailed": "लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", - "copyLinkToBlockSuccess": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली", - "copyLinkToBlockFailed": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", - "manageAllSites": "सर्व साइट्स व्यवस्थापित करा", - "updatePathName": "पथाचे नाव अपडेट करा" - }, - "moreAction": { - "small": "लहान", - "medium": "मध्यम", - "large": "मोठा", - "fontSize": "फॉन्ट आकार", - "import": "Import", - "moreOptions": "अधिक पर्याय", - "wordCount": "शब्द संख्या: {}", - "charCount": "अक्षर संख्या: {}", - "createdAt": "निर्मिती: {}", - "deleteView": "हटवा", - "duplicateView": "प्रत बनवा", - "wordCountLabel": "शब्द संख्या: ", - "charCountLabel": "अक्षर संख्या: ", - "createdAtLabel": "निर्मिती: ", - "syncedAtLabel": "सिंक केले: ", - "saveAsNewPage": "संदेश पृष्ठात जोडा", - "saveAsNewPageDisabled": "उपलब्ध संदेश नाहीत" - }, - "importPanel": { - "textAndMarkdown": "मजकूर आणि Markdown", - "documentFromV010": "v0.1.0 पासून दस्तऐवज", - "databaseFromV010": "v0.1.0 पासून डेटाबेस", - "notionZip": "Notion निर्यात केलेली Zip फाईल", - "csv": "CSV", - "database": "डेटाबेस" - }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा ", - "placeholderUpload": "अपलोड", - "placeholderRight": ", किंवा इमेज लिंक पेस्ट करा.", - "dropToUpload": "अपलोडसाठी फाईल ड्रॉप करा", - "change": "बदला" - } - }, - "disclosureAction": { - "rename": "नाव बदला", - "delete": "हटवा", - "duplicate": "प्रत बनवा", - "unfavorite": "आवडतीतून काढा", - "favorite": "आवडतीत जोडा", - "openNewTab": "नवीन टॅबमध्ये उघडा", - "moveTo": "या ठिकाणी हलवा", - "addToFavorites": "आवडतीत जोडा", - "copyLink": "लिंक कॉपी करा", - "changeIcon": "आयकॉन बदला", - "collapseAllPages": "सर्व उपपृष्ठे संकुचित करा", - "movePageTo": "पृष्ठ हलवा", - "move": "हलवा", - "lockPage": "पृष्ठ लॉक करा" - }, - "blankPageTitle": "रिक्त पृष्ठ", - "newPageText": "नवीन पृष्ठ", - "newDocumentText": "नवीन दस्तऐवज", - "newGridText": "नवीन ग्रिड", - "newCalendarText": "नवीन कॅलेंडर", - "newBoardText": "नवीन बोर्ड", - "chat": { - "newChat": "AI गप्पा", - "inputMessageHint": "@:appName AI ला विचार करा", - "inputLocalAIMessageHint": "@:appName लोकल AI ला विचार करा", - "unsupportedCloudPrompt": "ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे", - "relatedQuestion": "सूचवलेले", - "serverUnavailable": "कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा", - "aiServerUnavailable": "AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.", - "retry": "पुन्हा प्रयत्न करा", - "clickToRetry": "पुन्हा प्रयत्न करण्यासाठी क्लिक करा", - "regenerateAnswer": "उत्तर पुन्हा तयार करा", - "question1": "Kanban वापरून कामे कशी व्यवस्थापित करायची", - "question2": "GTD पद्धत समजावून सांगा", - "question3": "Rust का वापरावा", - "question4": "माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी", - "question5": "माझ्या पृष्ठासाठी एक चित्र तयार करा", - "question6": "या आठवड्याची माझी कामांची यादी तयार करा", - "aiMistakePrompt": "AI चुकू शकतो. महत्त्वाची माहिती तपासा.", - "chatWithFilePrompt": "तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?", - "indexFileSuccess": "फाईल यशस्वीरित्या अनुक्रमित केली गेली", - "inputActionNoPages": "काहीही पृष्ठे सापडली नाहीत", - "referenceSource": { - "zero": "0 स्रोत सापडले", - "one": "{count} स्रोत सापडला", - "other": "{count} स्रोत सापडले" - } - }, - "clickToMention": "पृष्ठाचा उल्लेख करा", - "uploadFile": "PDFs, मजकूर किंवा markdown फाइल्स जोडा", - "questionDetail": "नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?", - "indexingFile": "{} अनुक्रमित करत आहे", - "generatingResponse": "उत्तर तयार होत आहे", - "selectSources": "स्रोत निवडा", - "currentPage": "सध्याचे पृष्ठ", - "sourcesLimitReached": "तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता", - "sourceUnsupported": "सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही", - "regenerate": "पुन्हा प्रयत्न करा", - "addToPageButton": "संदेश पृष्ठावर जोडा", - "addToPageTitle": "या पृष्ठात संदेश जोडा...", - "addToNewPage": "नवीन पृष्ठ तयार करा", - "addToNewPageName": "\"{}\" मधून काढलेले संदेश", - "addToNewPageSuccessToast": "संदेश जोडण्यात आला", - "openPagePreviewFailedToast": "पृष्ठ उघडण्यात अयशस्वी", - "changeFormat": { - "actionButton": "फॉरमॅट बदला", - "confirmButton": "या फॉरमॅटसह पुन्हा तयार करा", - "textOnly": "मजकूर", - "imageOnly": "फक्त प्रतिमा", - "textAndImage": "मजकूर आणि प्रतिमा", - "text": "परिच्छेद", - "bullet": "बुलेट यादी", - "number": "क्रमांकित यादी", - "table": "सारणी", - "blankDescription": "उत्तराचे फॉरमॅट ठरवा", - "defaultDescription": "स्वयंचलित उत्तर फॉरमॅट", - "textWithImageDescription": "@:chat.changeFormat.text प्रतिमेसह", - "numberWithImageDescription": "@:chat.changeFormat.number प्रतिमेसह", - "bulletWithImageDescription": "@:chat.changeFormat.bullet प्रतिमेसह", - " tableWithImageDescription": "@:chat.changeFormat.table प्रतिमेसह" - }, - "switchModel": { - "label": "मॉडेल बदला", - "localModel": "स्थानिक मॉडेल", - "cloudModel": "क्लाऊड मॉडेल", - "autoModel": "स्वयंचलित" - }, - "selectBanner": { - "saveButton": "… मध्ये जोडा", - "selectMessages": "संदेश निवडा", - "nSelected": "{} निवडले गेले", - "allSelected": "सर्व निवडले गेले" - }, - "stopTooltip": "उत्पन्न करणे थांबवा", - "trash": { - "text": "कचरा", - "restoreAll": "सर्व पुनर्संचयित करा", - "restore": "पुनर्संचयित करा", - "deleteAll": "सर्व हटवा", - "pageHeader": { - "fileName": "फाईलचे नाव", - "lastModified": "शेवटचा बदल", - "created": "निर्मिती" - } - }, - "confirmDeleteAll": { - "title": "कचरापेटीतील सर्व पृष्ठे", - "caption": "तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "confirmRestoreAll": { - "title": "कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा", - "caption": "ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "restorePage": { - "title": "पुनर्संचयित करा: {}", - "caption": "तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?" - }, - "mobile": { - "actions": "कचरा क्रिया", - "empty": "कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत", - "emptyDescription": "जे आवश्यक नाही ते कचरापेटीत हलवा.", - "isDeleted": "हटवले गेले आहे", - "isRestored": "पुनर्संचयित केले गेले आहे" - }, - "confirmDeleteTitle": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?", - "deletePagePrompt": { - "text": "हे पृष्ठ कचरापेटीत आहे", - "restore": "पृष्ठ पुनर्संचयित करा", - "deletePermanent": "कायमचे हटवा", - "deletePermanentDescription": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "dialogCreatePageNameHint": "पृष्ठाचे नाव", - "questionBubble": { - "shortcuts": "शॉर्टकट्स", - "whatsNew": "नवीन काय आहे?", - "help": "मदत आणि समर्थन", - "markdown": "Markdown", - "debug": { - "name": "डीबग माहिती", - "success": "डीबग माहिती क्लिपबोर्डवर कॉपी केली!", - "fail": "डीबग माहिती कॉपी करता आली नाही" - }, - "feedback": "अभिप्राय" - }, - "menuAppHeader": { - "moreButtonToolTip": "हटवा, नाव बदला आणि अधिक...", - "addPageTooltip": "तत्काळ एक पृष्ठ जोडा", - "defaultNewPageName": "शीर्षक नसलेले", - "renameDialog": "नाव बदला", - "pageNameSuffix": "प्रत" - }, - "noPagesInside": "अंदर कोणतीही पृष्ठे नाहीत", - "toolbar": { - "undo": "पूर्ववत करा", - "redo": "पुन्हा करा", - "bold": "ठळक", - "italic": "तिरकस", - "underline": "अधोरेखित", - "strike": "मागे ओढलेले", - "numList": "क्रमांकित यादी", - "bulletList": "बुलेट यादी", - "checkList": "चेक यादी", - "inlineCode": "इनलाइन कोड", - "quote": "उद्धरण ब्लॉक", - "header": "शीर्षक", - "highlight": "हायलाइट", - "color": "रंग", - "addLink": "लिंक जोडा" - }, - "tooltip": { - "lightMode": "लाइट मोडमध्ये स्विच करा", - "darkMode": "डार्क मोडमध्ये स्विच करा", - "openAsPage": "पृष्ठ म्हणून उघडा", - "addNewRow": "नवीन पंक्ती जोडा", - "openMenu": "मेनू उघडण्यासाठी क्लिक करा", - "dragRow": "पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा", - "viewDataBase": "डेटाबेस पहा", - "referencePage": "हे {name} संदर्भित आहे", - "addBlockBelow": "खाली एक ब्लॉक जोडा", - "aiGenerate": "निर्मिती करा" - }, - "sideBar": { - "closeSidebar": "साइडबार बंद करा", - "openSidebar": "साइडबार उघडा", - "expandSidebar": "पूर्ण पृष्ठावर विस्तारित करा", - "personal": "वैयक्तिक", - "private": "खाजगी", - "workspace": "कार्यक्षेत्र", - "favorites": "आवडती", - "clickToHidePrivate": "खाजगी जागा लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील", - "clickToHideWorkspace": "कार्यक्षेत्र लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील", - "clickToHidePersonal": "वैयक्तिक जागा लपवण्यासाठी क्लिक करा", - "clickToHideFavorites": "आवडती जागा लपवण्यासाठी क्लिक करा", - "addAPage": "नवीन पृष्ठ जोडा", - "addAPageToPrivate": "खाजगी जागेत पृष्ठ जोडा", - "addAPageToWorkspace": "कार्यक्षेत्रात पृष्ठ जोडा", - "recent": "अलीकडील", - "today": "आज", - "thisWeek": "या आठवड्यात", - "others": "पूर्वीच्या आवडती", - "earlier": "पूर्वीचे", - "justNow": "आत्ताच", - "minutesAgo": "{count} मिनिटांपूर्वी", - "lastViewed": "शेवटी पाहिलेले", - "favoriteAt": "आवडते म्हणून चिन्हांकित", - "emptyRecent": "अलीकडील पृष्ठे नाहीत", - "emptyRecentDescription": "तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.", - "emptyFavorite": "आवडती पृष्ठे नाहीत", - "emptyFavoriteDescription": "पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!", - "removePageFromRecent": "हे पृष्ठ अलीकडील यादीतून काढायचे?", - "removeSuccess": "यशस्वीरित्या काढले गेले", - "favoriteSpace": "आवडती", - "RecentSpace": "अलीकडील", - "Spaces": "जागा", - "upgradeToPro": "Pro मध्ये अपग्रेड करा", - "upgradeToAIMax": "अमर्यादित AI अनलॉक करा", - "storageLimitDialogTitle": "तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा", - "storageLimitDialogTitleIOS": "तुमचा मोफत स्टोरेज संपला आहे.", - "aiResponseLimitTitle": "तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा", - "aiResponseLimitDialogTitle": "AI प्रतिसाद मर्यादा गाठली आहे", - "aiResponseLimit": "तुमचे मोफत AI प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा", - "askOwnerToUpgradeToPro": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा", - "askOwnerToUpgradeToProIOS": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.", - "askOwnerToUpgradeToAIMax": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा", - "askOwnerToUpgradeToAIMaxIOS": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.", - "purchaseAIMax": "तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा", - "aiImageResponseLimit": "तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा", - "purchaseStorageSpace": "स्टोरेज स्पेस खरेदी करा", - "singleFileProPlanLimitationDescription": "तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा", - "purchaseAIResponse": "AI प्रतिसाद खरेदी करा", - "askOwnerToUpgradeToLocalAI": "कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा", - "upgradeToAILocal": "अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा", - "upgradeToAILocalDesc": "PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा" -}, - "notifications": { - "export": { - "markdown": "टीप Markdown मध्ये निर्यात केली", - "path": "Documents/flowy" - } - }, - "contactsPage": { - "title": "संपर्क", - "whatsHappening": "या आठवड्यात काय घडत आहे?", - "addContact": "संपर्क जोडा", - "editContact": "संपर्क संपादित करा" - }, - "button": { - "ok": "ठीक आहे", - "confirm": "खात्री करा", - "done": "पूर्ण", - "cancel": "रद्द करा", - "signIn": "साइन इन", - "signOut": "साइन आउट", - "complete": "पूर्ण करा", - "save": "जतन करा", - "generate": "निर्माण करा", - "esc": "ESC", - "keep": "ठेवा", - "tryAgain": "पुन्हा प्रयत्न करा", - "discard": "टाका", - "replace": "बदला", - "insertBelow": "खाली घाला", - "insertAbove": "वर घाला", - "upload": "अपलोड करा", - "edit": "संपादित करा", - "delete": "हटवा", - "copy": "कॉपी करा", - "duplicate": "प्रत बनवा", - "putback": "परत ठेवा", - "update": "अद्यतनित करा", - "share": "शेअर करा", - "removeFromFavorites": "आवडतीतून काढा", - "removeFromRecent": "अलीकडील यादीतून काढा", - "addToFavorites": "आवडतीत जोडा", - "favoriteSuccessfully": "आवडतीत यशस्वीरित्या जोडले", - "unfavoriteSuccessfully": "आवडतीतून यशस्वीरित्या काढले", - "duplicateSuccessfully": "प्रत यशस्वीरित्या तयार झाली", - "rename": "नाव बदला", - "helpCenter": "मदत केंद्र", - "add": "जोड़ा", - "yes": "होय", - "no": "नाही", - "clear": "साफ करा", - "remove": "काढा", - "dontRemove": "काढू नका", - "copyLink": "लिंक कॉपी करा", - "align": "जुळवा", - "login": "लॉगिन", - "logout": "लॉगआउट", - "deleteAccount": "खाते हटवा", - "back": "मागे", - "signInGoogle": "Google सह पुढे जा", - "signInGithub": "GitHub सह पुढे जा", - "signInDiscord": "Discord सह पुढे जा", - "more": "अधिक", - "create": "तयार करा", - "close": "बंद करा", - "next": "पुढे", - "previous": "मागील", - "submit": "सबमिट करा", - "download": "डाउनलोड करा", - "backToHome": "मुख्यपृष्ठावर परत जा", - "viewing": "पाहत आहात", - "editing": "संपादन करत आहात", - "gotIt": "समजले", - "retry": "पुन्हा प्रयत्न करा", - "uploadFailed": "अपलोड अयशस्वी.", - "copyLinkOriginal": "मूळ दुव्याची कॉपी करा" - }, - "label": { - "welcome": "स्वागत आहे!", - "firstName": "पहिले नाव", - "middleName": "मधले नाव", - "lastName": "आडनाव", - "stepX": "पायरी {X}" - }, - "oAuth": { - "err": { - "failedTitle": "तुमच्या खात्याशी कनेक्ट होता आले नाही.", - "failedMsg": "कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे." - }, - "google": { - "title": "GOOGLE साइन-इन", - "instruction1": "तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.", - "instruction2": "ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:", - "instruction3": "तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:", - "instruction4": "साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:" - } - }, - "settings": { - "title": "सेटिंग्ज", - "popupMenuItem": { - "settings": "सेटिंग्ज", - "members": "सदस्य", - "trash": "कचरा", - "helpAndSupport": "मदत आणि समर्थन" - }, - "sites": { - "title": "साइट्स", - "namespaceTitle": "नेमस्पेस", - "namespaceDescription": "तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा", - "namespaceHeader": "नेमस्पेस", - "homepageHeader": "मुख्यपृष्ठ", - "updateNamespace": "नेमस्पेस अद्यतनित करा", - "removeHomepage": "मुख्यपृष्ठ हटवा", - "selectHomePage": "एक पृष्ठ निवडा", - "clearHomePage": "या नेमस्पेससाठी मुख्यपृष्ठ साफ करा", - "customUrl": "स्वतःची URL", - "namespace": { - "description": "हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत", - "tooltip": "कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो", - "updateExistingNamespace": "विद्यमान नेमस्पेस अद्यतनित करा", - "upgradeToPro": "मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा", - "redirectToPayment": "पेमेंट पृष्ठावर वळवत आहे...", - "onlyWorkspaceOwnerCanSetHomePage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो", - "pleaseAskOwnerToSetHomePage": "कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा" - }, - "publishedPage": { - "title": "सर्व प्रकाशित पृष्ठे", - "description": "तुमची प्रकाशित पृष्ठे व्यवस्थापित करा", - "page": "पृष्ठ", - "pathName": "पथाचे नाव", - "date": "प्रकाशन तारीख", - "emptyHinText": "या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत", - "noPublishedPages": "प्रकाशित पृष्ठे नाहीत", - "settings": "प्रकाशन सेटिंग्ज", - "clickToOpenPageInApp": "पृष्ठ अ‍ॅपमध्ये उघडा", - "clickToOpenPageInBrowser": "पृष्ठ ब्राउझरमध्ये उघडा" - } - } - }, - "error": { - "failedToGeneratePaymentLink": "Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी", - "failedToUpdateNamespace": "नेमस्पेस अद्यतनित करण्यात अयशस्वी", - "proPlanLimitation": "नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे", - "namespaceAlreadyInUse": "नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा", - "invalidNamespace": "अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा", - "namespaceLengthAtLeast2Characters": "नेमस्पेस किमान २ अक्षरे लांब असावे", - "onlyWorkspaceOwnerCanUpdateNamespace": "फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो", - "onlyWorkspaceOwnerCanRemoveHomepage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो", - "setHomepageFailed": "मुख्यपृष्ठ सेट करण्यात अयशस्वी", - "namespaceTooLong": "नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा", - "namespaceTooShort": "नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा", - "namespaceIsReserved": "हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा", - "updatePathNameFailed": "पथाचे नाव अद्यतनित करण्यात अयशस्वी", - "removeHomePageFailed": "मुख्यपृष्ठ हटवण्यात अयशस्वी", - "publishNameContainsInvalidCharacters": "पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", - "publishNameTooShort": "पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा", - "publishNameTooLong": "पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा", - "publishNameAlreadyInUse": "हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा", - "namespaceContainsInvalidCharacters": "नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", - "publishPermissionDenied": "फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो", - "publishNameCannotBeEmpty": "पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा" - }, - "success": { - "namespaceUpdated": "नेमस्पेस यशस्वीरित्या अद्यतनित केला", - "setHomepageSuccess": "मुख्यपृष्ठ यशस्वीरित्या सेट केले", - "updatePathNameSuccess": "पथाचे नाव यशस्वीरित्या अद्यतनित केले", - "removeHomePageSuccess": "मुख्यपृष्ठ यशस्वीरित्या हटवले" - }, - "accountPage": { - "menuLabel": "खाते आणि अ‍ॅप", - "title": "माझे खाते", - "general": { - "title": "खात्याचे नाव आणि प्रोफाइल प्रतिमा", - "changeProfilePicture": "प्रोफाइल प्रतिमा बदला" - }, - "email": { - "title": "ईमेल", - "actions": { - "change": "ईमेल बदला" - } - }, - "login": { - "title": "खाते लॉगिन", - "loginLabel": "लॉगिन", - "logoutLabel": "लॉगआउट" - }, - "isUpToDate": "@:appName अद्ययावत आहे!", - "officialVersion": "आवृत्ती {version} (अधिकृत बिल्ड)" -}, - "workspacePage": { - "menuLabel": "कार्यक्षेत्र", - "title": "कार्यक्षेत्र", - "description": "तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.", - "workspaceName": { - "title": "कार्यक्षेत्राचे नाव" - }, - "workspaceIcon": { - "title": "कार्यक्षेत्राचे चिन्ह", - "description": "तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल." - }, - "appearance": { - "title": "दृश्यरूप", - "description": "कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.", - "options": { - "system": "स्वयंचलित", - "light": "लाइट", - "dark": "डार्क" - } - } - }, - "resetCursorColor": { - "title": "दस्तऐवज कर्सरचा रंग रीसेट करा", - "description": "तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?" - }, - "resetSelectionColor": { - "title": "दस्तऐवज निवडीचा रंग रीसेट करा", - "description": "तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?" - }, - "resetWidth": { - "resetSuccess": "दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली" - }, - "theme": { - "title": "थीम", - "description": "पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.", - "uploadCustomThemeTooltip": "स्वतःची थीम अपलोड करा" - }, - "workspaceFont": { - "title": "कार्यक्षेत्र फॉन्ट", - "noFontHint": "कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा." - }, - "textDirection": { - "title": "मजकूर दिशा", - "leftToRight": "डावीकडून उजवीकडे", - "rightToLeft": "उजवीकडून डावीकडे", - "auto": "स्वयंचलित", - "enableRTLItems": "RTL टूलबार घटक सक्षम करा" - }, - "layoutDirection": { - "title": "लेआउट दिशा", - "leftToRight": "डावीकडून उजवीकडे", - "rightToLeft": "उजवीकडून डावीकडे" - }, - "dateTime": { - "title": "दिनांक आणि वेळ", - "example": "{} वाजता {} ({})", - "24HourTime": "२४-तास वेळ", - "dateFormat": { - "label": "दिनांक फॉरमॅट", - "local": "स्थानिक", - "us": "US", - "iso": "ISO", - "friendly": "सुलभ", - "dmy": "D/M/Y" - } - }, - "language": { - "title": "भाषा" - }, - "deleteWorkspacePrompt": { - "title": "कार्यक्षेत्र हटवा", - "content": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील." - }, - "leaveWorkspacePrompt": { - "title": "कार्यक्षेत्र सोडा", - "content": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.", - "success": "तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.", - "fail": "कार्यक्षेत्र सोडण्यात अयशस्वी." - }, - "manageWorkspace": { - "title": "कार्यक्षेत्र व्यवस्थापित करा", - "leaveWorkspace": "कार्यक्षेत्र सोडा", - "deleteWorkspace": "कार्यक्षेत्र हटवा" - }, - "manageDataPage": { - "menuLabel": "डेटा व्यवस्थापित करा", - "title": "डेटा व्यवस्थापन", - "description": "@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.", - "dataStorage": { - "title": "फाइल संचयन स्थान", - "tooltip": "जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान", - "actions": { - "change": "मार्ग बदला", - "open": "फोल्डर उघडा", - "openTooltip": "सध्याच्या डेटा फोल्डरचे स्थान उघडा", - "copy": "मार्ग कॉपी करा", - "copiedHint": "मार्ग कॉपी केला!", - "resetTooltip": "मूलभूत स्थानावर रीसेट करा" - }, - "resetDialog": { - "title": "तुम्हाला खात्री आहे का?", - "description": "पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा." - } - }, - "importData": { - "title": "डेटा आयात करा", - "tooltip": "@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा", - "description": "बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा", - "action": "फाइल निवडा" - }, - "encryption": { - "title": "एनक्रिप्शन", - "tooltip": "तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा", - "descriptionNoEncryption": "एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.", - "descriptionEncrypted": "तुमचा डेटा एनक्रिप्टेड आहे.", - "action": "डेटा एनक्रिप्ट करा", - "dialog": { - "title": "संपूर्ण डेटा एनक्रिप्ट करायचा?", - "description": "तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?" - } - }, - "cache": { - "title": "कॅशे साफ करा", - "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", - "dialog": { - "title": "कॅशे साफ करा", - "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", - "successHint": "कॅशे साफ झाली!" - } - }, - "data": { - "fixYourData": "तुमचा डेटा सुधारा", - "fixButton": "सुधारा", - "fixYourDataDescription": "तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता." - } - }, - "shortcutsPage": { - "menuLabel": "शॉर्टकट्स", - "title": "शॉर्टकट्स", - "editBindingHint": "नवीन बाइंडिंग टाका", - "searchHint": "शोधा", - "actions": { - "resetDefault": "मूलभूत रीसेट करा" - }, - "errorPage": { - "message": "शॉर्टकट्स लोड करण्यात अयशस्वी: {}", - "howToFix": "कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा." - }, - "resetDialog": { - "title": "शॉर्टकट्स रीसेट करा", - "description": "हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?", - "buttonLabel": "रीसेट करा" - }, - "conflictDialog": { - "title": "{} आधीच वापरले जात आहे", - "descriptionPrefix": "हे कीबाइंडिंग सध्या ", - "descriptionSuffix": " यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.", - "confirmLabel": "पुढे जा" - }, - "editTooltip": "कीबाइंडिंग संपादित करण्यासाठी क्लिक करा", - "keybindings": { - "toggleToDoList": "टू-डू सूची चालू/बंद करा", - "insertNewParagraphInCodeblock": "नवीन परिच्छेद टाका", - "pasteInCodeblock": "कोडब्लॉकमध्ये पेस्ट करा", - "selectAllCodeblock": "सर्व निवडा", - "indentLineCodeblock": "ओळीच्या सुरुवातीला दोन स्पेस टाका", - "outdentLineCodeblock": "ओळीच्या सुरुवातीची दोन स्पेस काढा", - "twoSpacesCursorCodeblock": "कर्सरवर दोन स्पेस टाका", - "copy": "निवड कॉपी करा", - "paste": "मजकुरात पेस्ट करा", - "cut": "निवड कट करा", - "alignLeft": "मजकूर डावीकडे संरेखित करा", - "alignCenter": "मजकूर मधोमध संरेखित करा", - "alignRight": "मजकूर उजवीकडे संरेखित करा", - "insertInlineMathEquation": "इनलाइन गणितीय सूत्र टाका", - "undo": "पूर्ववत करा", - "redo": "पुन्हा करा", - "convertToParagraph": "ब्लॉक परिच्छेदात रूपांतरित करा", - "backspace": "हटवा", - "deleteLeftWord": "डावीकडील शब्द हटवा", - "deleteLeftSentence": "डावीकडील वाक्य हटवा", - "delete": "उजवीकडील अक्षर हटवा", - "deleteMacOS": "डावीकडील अक्षर हटवा", - "deleteRightWord": "उजवीकडील शब्द हटवा", - "moveCursorLeft": "कर्सर डावीकडे हलवा", - "moveCursorBeginning": "कर्सर सुरुवातीला हलवा", - "moveCursorLeftWord": "कर्सर एक शब्द डावीकडे हलवा", - "moveCursorLeftSelect": "निवडा आणि कर्सर डावीकडे हलवा", - "moveCursorBeginSelect": "निवडा आणि कर्सर सुरुवातीला हलवा", - "moveCursorLeftWordSelect": "निवडा आणि कर्सर एक शब्द डावीकडे हलवा", - "moveCursorRight": "कर्सर उजवीकडे हलवा", - "moveCursorEnd": "कर्सर शेवटी हलवा", - "moveCursorRightWord": "कर्सर एक शब्द उजवीकडे हलवा", - "moveCursorRightSelect": "निवडा आणि कर्सर उजवीकडे हलवा", - "moveCursorEndSelect": "निवडा आणि कर्सर शेवटी हलवा", - "moveCursorRightWordSelect": "निवडा आणि कर्सर एक शब्द उजवीकडे हलवा", - "moveCursorUp": "कर्सर वर हलवा", - "moveCursorTopSelect": "निवडा आणि कर्सर वर हलवा", - "moveCursorTop": "कर्सर वर हलवा", - "moveCursorUpSelect": "निवडा आणि कर्सर वर हलवा", - "moveCursorBottomSelect": "निवडा आणि कर्सर खाली हलवा", - "moveCursorBottom": "कर्सर खाली हलवा", - "moveCursorDown": "कर्सर खाली हलवा", - "moveCursorDownSelect": "निवडा आणि कर्सर खाली हलवा", - "home": "वर स्क्रोल करा", - "end": "खाली स्क्रोल करा", - "toggleBold": "बोल्ड चालू/बंद करा", - "toggleItalic": "इटालिक चालू/बंद करा", - "toggleUnderline": "अधोरेखित चालू/बंद करा", - "toggleStrikethrough": "स्ट्राईकथ्रू चालू/बंद करा", - "toggleCode": "इनलाइन कोड चालू/बंद करा", - "toggleHighlight": "हायलाईट चालू/बंद करा", - "showLinkMenu": "लिंक मेनू दाखवा", - "openInlineLink": "इनलाइन लिंक उघडा", - "openLinks": "सर्व निवडलेले लिंक उघडा", - "indent": "इंडेंट", - "outdent": "आउटडेंट", - "exit": "संपादनातून बाहेर पडा", - "pageUp": "एक पृष्ठ वर स्क्रोल करा", - "pageDown": "एक पृष्ठ खाली स्क्रोल करा", - "selectAll": "सर्व निवडा", - "pasteWithoutFormatting": "फॉरमॅटिंगशिवाय पेस्ट करा", - "showEmojiPicker": "इमोजी निवडकर्ता दाखवा", - "enterInTableCell": "टेबलमध्ये नवीन ओळ जोडा", - "leftInTableCell": "टेबलमध्ये डावीकडील सेलमध्ये जा", - "rightInTableCell": "टेबलमध्ये उजवीकडील सेलमध्ये जा", - "upInTableCell": "टेबलमध्ये वरील सेलमध्ये जा", - "downInTableCell": "टेबलमध्ये खालील सेलमध्ये जा", - "tabInTableCell": "टेबलमध्ये पुढील सेलमध्ये जा", - "shiftTabInTableCell": "टेबलमध्ये मागील सेलमध्ये जा", - "backSpaceInTableCell": "सेलच्या सुरुवातीला थांबा" - }, - "commands": { - "codeBlockNewParagraph": "कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका", - "codeBlockIndentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका", - "codeBlockOutdentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा", - "codeBlockAddTwoSpaces": "कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका", - "codeBlockSelectAll": "कोडब्लॉकमधील सर्व मजकूर निवडा", - "codeBlockPasteText": "कोडब्लॉकमध्ये मजकूर पेस्ट करा", - "textAlignLeft": "मजकूर डावीकडे संरेखित करा", - "textAlignCenter": "मजकूर मधोमध संरेखित करा", - "textAlignRight": "मजकूर उजवीकडे संरेखित करा" - }, - "couldNotLoadErrorMsg": "शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा", - "couldNotSaveErrorMsg": "शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा" -}, - "aiPage": { - "title": "AI सेटिंग्ज", - "menuLabel": "AI सेटिंग्ज", - "keys": { - "enableAISearchTitle": "AI शोध", - "aiSettingsDescription": "AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.", - "loginToEnableAIFeature": "AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.", - "llmModel": "भाषा मॉडेल", - "llmModelType": "भाषा मॉडेल प्रकार", - "downloadLLMPrompt": "{} डाउनलोड करा", - "downloadAppFlowyOfflineAI": "AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?", - "downloadLLMPromptDetail": "{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?", - "downloadBigFilePrompt": "डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात", - "downloadAIModelButton": "डाउनलोड करा", - "downloadingModel": "डाउनलोड करत आहे", - "localAILoaded": "स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे", - "localAIStart": "स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा", - "localAILoading": "स्थानिक AI Chat मॉडेल लोड होत आहे...", - "localAIStopped": "स्थानिक AI थांबले आहे", - "localAIRunning": "स्थानिक AI चालू आहे", - "localAINotReadyRetryLater": "स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा", - "localAIDisabled": "तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा", - "localAIInitializing": "स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात", - "localAINotReadyTextFieldPrompt": "स्थानिक AI लोड होत असताना संपादन करता येणार नाही", - "failToLoadLocalAI": "स्थानिक AI सुरू करण्यात अयशस्वी.", - "restartLocalAI": "पुन्हा सुरू करा", - "disableLocalAITitle": "स्थानिक AI अकार्यक्षम करा", - "disableLocalAIDescription": "तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?", - "localAIToggleTitle": "AppFlowy स्थानिक AI (LAI)", - "localAIToggleSubTitle": "AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा", - "offlineAIInstruction1": "हे अनुसरा", - "offlineAIInstruction2": "सूचना", - "offlineAIInstruction3": "ऑफलाइन AI सक्षम करण्यासाठी.", - "offlineAIDownload1": "जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया", - "offlineAIDownload2": "डाउनलोड", - "offlineAIDownload3": "करा", - "activeOfflineAI": "सक्रिय", - "downloadOfflineAI": "डाउनलोड करा", - "openModelDirectory": "फोल्डर उघडा", - "laiNotReady": "स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.", - "ollamaNotReady": "Ollama सर्व्हर तयार नाही.", - "pleaseFollowThese": "कृपया हे अनुसरा", - "instructions": "सूचना", - "installOllamaLai": "Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.", - "modelsMissing": "आवश्यक मॉडेल्स सापडत नाहीत.", - "downloadModel": "त्यांना डाउनलोड करण्यासाठी." - } -}, - "planPage": { - "menuLabel": "योजना", - "title": "दर योजना", - "planUsage": { - "title": "योजनेचा वापर सारांश", - "storageLabel": "स्टोरेज", - "storageUsage": "{} पैकी {} GB", - "unlimitedStorageLabel": "अमर्यादित स्टोरेज", - "collaboratorsLabel": "सदस्य", - "collaboratorsUsage": "{} पैकी {}", - "aiResponseLabel": "AI प्रतिसाद", - "aiResponseUsage": "{} पैकी {}", - "unlimitedAILabel": "अमर्यादित AI प्रतिसाद", - "proBadge": "प्रो", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "मॅकसाठी ऑन-डिव्हाइस AI", - "memberProToggle": "अधिक सदस्य आणि अमर्यादित AI", - "aiMaxToggle": "अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश", - "aiOnDeviceToggle": "जास्त गोपनीयतेसाठी स्थानिक AI", - "aiCredit": { - "title": "@:appName AI क्रेडिट जोडा", - "price": "{}", - "priceDescription": "1,000 क्रेडिट्ससाठी", - "purchase": "AI खरेदी करा", - "info": "प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:", - "infoItemOne": "प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद", - "infoItemTwo": "प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद" - }, - "currentPlan": { - "bannerLabel": "सद्य योजना", - "freeTitle": "फ्री", - "proTitle": "प्रो", - "teamTitle": "टीम", - "freeInfo": "2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम", - "proInfo": "10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य", - "teamInfo": "सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य", - "upgrade": "योजना बदला", - "canceledInfo": "तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल." - }, - "addons": { - "title": "ऍड-ऑन्स", - "addLabel": "जोडा", - "activeLabel": "जोडले गेले", - "aiMax": { - "title": "AI Max", - "description": "प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा", - "price": "{}", - "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)" - }, - "aiOnDevice": { - "title": "मॅकसाठी ऑन-डिव्हाइस AI", - "description": "तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा", - "price": "{}", - "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)", - "recommend": "M1 किंवा नवीनतम शिफारस केली जाते" - } - }, - "deal": { - "bannerLabel": "नववर्षाचे विशेष ऑफर!", - "title": "तुमची टीम वाढवा!", - "info": "Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.", - "viewPlans": "योजना पहा" - } - } -}, - "billingPage": { - "menuLabel": "बिलिंग", - "title": "बिलिंग", - "plan": { - "title": "योजना", - "freeLabel": "फ्री", - "proLabel": "प्रो", - "planButtonLabel": "योजना बदला", - "billingPeriod": "बिलिंग कालावधी", - "periodButtonLabel": "कालावधी संपादित करा" - }, - "paymentDetails": { - "title": "पेमेंट तपशील", - "methodLabel": "पेमेंट पद्धत", - "methodButtonLabel": "पद्धत संपादित करा" - }, - "addons": { - "title": "ऍड-ऑन्स", - "addLabel": "जोडा", - "removeLabel": "काढा", - "renewLabel": "नवीन करा", - "aiMax": { - "label": "AI Max", - "description": "अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा", - "activeDescription": "पुढील बिलिंग तारीख {} आहे", - "canceledDescription": "AI Max {} पर्यंत उपलब्ध असेल" - }, - "aiOnDevice": { - "label": "मॅकसाठी ऑन-डिव्हाइस AI", - "description": "तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा", - "activeDescription": "पुढील बिलिंग तारीख {} आहे", - "canceledDescription": "मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल" - }, - "removeDialog": { - "title": "{} काढा", - "description": "तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल." - } - }, - "currentPeriodBadge": "सद्य कालावधी", - "changePeriod": "कालावधी बदला", - "planPeriod": "{} कालावधी", - "monthlyInterval": "मासिक", - "monthlyPriceInfo": "प्रति सदस्य, मासिक बिलिंग", - "annualInterval": "वार्षिक", - "annualPriceInfo": "प्रति सदस्य, वार्षिक बिलिंग" -}, - "comparePlanDialog": { - "title": "योजना तुलना आणि निवड", - "planFeatures": "योजनेची\nवैशिष्ट्ये", - "current": "सध्याची", - "actions": { - "upgrade": "अपग्रेड करा", - "downgrade": "डाऊनग्रेड करा", - "current": "सध्याची" - }, - "freePlan": { - "title": "फ्री", - "description": "२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी", - "price": "{}", - "priceInfo": "सदैव फ्री" - }, - "proPlan": { - "title": "प्रो", - "description": "लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी", - "price": "{}", - "priceInfo": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग\n\n{} मासिक बिलिंगसाठी" - }, - "planLabels": { - "itemOne": "वर्कस्पेसेस", - "itemTwo": "सदस्य", - "itemThree": "स्टोरेज", - "itemFour": "रिअल-टाइम सहकार्य", - "itemFive": "मोबाईल अ‍ॅप", - "itemSix": "AI प्रतिसाद", - "itemSeven": "AI प्रतिमा", - "itemFileUpload": "फाइल अपलोड", - "customNamespace": "सानुकूल नेमस्पेस", - "tooltipSix": "‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही", - "intelligentSearch": "स्मार्ट शोध", - "tooltipSeven": "तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते", - "customNamespaceTooltip": "सानुकूल प्रकाशित साइट URL" - }, - "freeLabels": { - "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", - "itemTwo": "२ पर्यंत", - "itemThree": "५ GB", - "itemFour": "होय", - "itemFive": "होय", - "itemSix": "१० कायमस्वरूपी", - "itemSeven": "२ कायमस्वरूपी", - "itemFileUpload": "७ MB पर्यंत", - "intelligentSearch": "स्मार्ट शोध" - }, - "proLabels": { - "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", - "itemTwo": "१० पर्यंत", - "itemThree": "अमर्यादित", - "itemFour": "होय", - "itemFive": "होय", - "itemSix": "अमर्यादित", - "itemSeven": "दर महिन्याला १० प्रतिमा", - "itemFileUpload": "अमर्यादित", - "intelligentSearch": "स्मार्ट शोध" - }, - "paymentSuccess": { - "title": "तुम्ही आता {} योजनेवर आहात!", - "description": "तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता." - }, - "downgradeDialog": { - "title": "तुम्हाला योजना डाऊनग्रेड करायची आहे का?", - "description": "तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.", - "downgradeLabel": "योजना डाऊनग्रेड करा" - } -}, - "cancelSurveyDialog": { - "title": "तुम्ही जात आहात याचे दुःख आहे", - "description": "तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.", - "commonOther": "इतर", - "otherHint": "तुमचे उत्तर येथे लिहा", - "questionOne": { - "question": "तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?", - "answerOne": "खर्च खूप जास्त आहे", - "answerTwo": "वैशिष्ट्ये अपेक्षांनुसार नव्हती", - "answerThree": "यापेक्षा चांगला पर्याय सापडला", - "answerFour": "वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही", - "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" - }, - "questionTwo": { - "question": "भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?", - "answerOne": "खूप शक्यता आहे", - "answerTwo": "काहीशी शक्यता आहे", - "answerThree": "निश्चित नाही", - "answerFour": "अल्प शक्यता", - "answerFive": "एकदम कमी शक्यता" - }, - "questionThree": { - "question": "तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?", - "answerOne": "अनेक वापरकर्त्यांशी सहकार्य", - "answerTwo": "लांब कालावधीची आवृत्ती इतिहास", - "answerThree": "अमर्यादित AI प्रतिसाद", - "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" - }, - "questionFour": { - "question": "@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?", - "answerOne": "खूप छान", - "answerTwo": "चांगला", - "answerThree": "सरासरी", - "answerFour": "सरासरीपेक्षा कमी", - "answerFive": "असंतोषजनक" - } -}, - "common": { - "uploadingFile": "फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका", - "uploadNotionSuccess": "तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल", - "reset": "रीसेट करा" -}, - "menu": { - "appearance": "दृश्यरूप", - "language": "भाषा", - "user": "वापरकर्ता", - "files": "फाईल्स", - "notifications": "सूचना", - "open": "सेटिंग्ज उघडा", - "logout": "लॉगआउट", - "logoutPrompt": "तुम्हाला नक्की लॉगआउट करायचे आहे का?", - "selfEncryptionLogoutPrompt": "तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे", - "syncSetting": "सिंक्रोनायझेशन सेटिंग", - "cloudSettings": "क्लाऊड सेटिंग्ज", - "enableSync": "सिंक्रोनायझेशन सक्षम करा", - "enableSyncLog": "सिंक लॉगिंग सक्षम करा", - "enableSyncLogWarning": "सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा", - "enableEncrypt": "डेटा एन्क्रिप्ट करा", - "cloudURL": "बेस URL", - "webURL": "वेब URL", - "invalidCloudURLScheme": "अवैध स्कीम", - "cloudServerType": "क्लाऊड सर्व्हर", - "cloudServerTypeTip": "कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते", - "cloudLocal": "स्थानिक", - "cloudAppFlowy": "@:appName Cloud", - "cloudAppFlowySelfHost": "@:appName क्लाऊड सेल्फ-होस्टेड", - "appFlowyCloudUrlCanNotBeEmpty": "क्लाऊड URL रिकामा असू शकत नाही", - "clickToCopy": "क्लिपबोर्डवर कॉपी करा", - "selfHostStart": "जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा", - "selfHostContent": "दस्तऐवज", - "selfHostEnd": "तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी", - "pleaseInputValidURL": "कृपया वैध URL टाका", - "changeUrl": "सेल्फ-होस्टेड URL {} मध्ये बदला", - "cloudURLHint": "तुमच्या सर्व्हरचा बेस URL टाका", - "webURLHint": "तुमच्या वेब सर्व्हरचा बेस URL टाका", - "cloudWSURL": "वेबसॉकेट URL", - "cloudWSURLHint": "तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका", - "restartApp": "अ‍ॅप रीस्टार्ट करा", - "restartAppTip": "बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.", - "changeServerTip": "सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे", - "enableEncryptPrompt": "तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा", - "inputEncryptPrompt": "कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:", - "clickToCopySecret": "गुप्तकी कॉपी करण्यासाठी क्लिक करा", - "configServerSetting": "तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा", - "configServerGuide": "`Quick Start` निवडल्यानंतर, `Settings` → \"Cloud Settings\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.", - "inputTextFieldHint": "तुमची गुप्तकी", - "historicalUserList": "वापरकर्ता लॉगिन इतिहास", - "historicalUserListTooltip": "ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात", - "openHistoricalUser": "अनामिक खाते उघडण्यासाठी क्लिक करा", - "customPathPrompt": "@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.", - "importAppFlowyData": "बाह्य @:appName फोल्डरमधून डेटा आयात करा", - "importingAppFlowyDataTip": "डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका", - "importAppFlowyDataDescription": "बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा", - "importSuccess": "@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला", - "importFailed": "@:appName डेटा फोल्डर आयात करण्यात अयशस्वी", - "importGuide": "अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा" -}, - "notifications": { - "enableNotifications": { - "label": "सूचना सक्षम करा", - "hint": "स्थानिक सूचना दिसू नयेत यासाठी बंद करा." - }, - "showNotificationsIcon": { - "label": "सूचना चिन्ह दाखवा", - "hint": "साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा." - }, - "archiveNotifications": { - "allSuccess": "सर्व सूचना यशस्वीरित्या संग्रहित केल्या", - "success": "सूचना यशस्वीरित्या संग्रहित केली" - }, - "markAsReadNotifications": { - "allSuccess": "सर्व वाचलेल्या म्हणून चिन्हांकित केल्या", - "success": "वाचलेले म्हणून चिन्हांकित केले" - }, - "action": { - "markAsRead": "वाचलेले म्हणून चिन्हांकित करा", - "multipleChoice": "अधिक निवडा", - "archive": "संग्रहित करा" - }, - "settings": { - "settings": "सेटिंग्ज", - "markAllAsRead": "सर्व वाचलेले म्हणून चिन्हांकित करा", - "archiveAll": "सर्व संग्रहित करा" - }, - "emptyInbox": { - "title": "इनबॉक्स झिरो!", - "description": "इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा." - }, - "emptyUnread": { - "title": "कोणतीही न वाचलेली सूचना नाही", - "description": "तुम्ही सर्व वाचले आहे!" - }, - "emptyArchived": { - "title": "कोणतीही संग्रहित सूचना नाही", - "description": "संग्रहित सूचना इथे दिसतील." - }, - "tabs": { - "inbox": "इनबॉक्स", - "unread": "न वाचलेले", - "archived": "संग्रहित" - }, - "refreshSuccess": "सूचना यशस्वीरित्या रीफ्रेश केल्या", - "titles": { - "notifications": "सूचना", - "reminder": "रिमाइंडर" - } -}, - "appearance": { - "resetSetting": "रीसेट", - "fontFamily": { - "label": "फॉन्ट फॅमिली", - "search": "शोध", - "defaultFont": "सिस्टम" - }, - "themeMode": { - "label": "थीम मोड", - "light": "लाइट मोड", - "dark": "डार्क मोड", - "system": "सिस्टमशी जुळवा" - }, - "fontScaleFactor": "फॉन्ट स्केल घटक", - "displaySize": "डिस्प्ले आकार", - "documentSettings": { - "cursorColor": "डॉक्युमेंट कर्सरचा रंग", - "selectionColor": "डॉक्युमेंट निवडीचा रंग", - "width": "डॉक्युमेंटची रुंदी", - "changeWidth": "बदला", - "pickColor": "रंग निवडा", - "colorShade": "रंगाची छटा", - "opacity": "अपारदर्शकता", - "hexEmptyError": "Hex रंग रिकामा असू शकत नाही", - "hexLengthError": "Hex व्हॅल्यू 6 अंकांची असावी", - "hexInvalidError": "अवैध Hex व्हॅल्यू", - "opacityEmptyError": "अपारदर्शकता रिकामी असू शकत नाही", - "opacityRangeError": "अपारदर्शकता 1 ते 100 दरम्यान असावी", - "app": "अ‍ॅप", - "flowy": "Flowy", - "apply": "लागू करा" - }, - "layoutDirection": { - "label": "लेआउट दिशा", - "hint": "तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.", - "ltr": "LTR", - "rtl": "RTL" - }, - "textDirection": { - "label": "मूलभूत मजकूर दिशा", - "hint": "मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.", - "ltr": "LTR", - "rtl": "RTL", - "auto": "स्वयं", - "fallback": "लेआउट दिशेशी जुळवा" - }, - "themeUpload": { - "button": "अपलोड", - "uploadTheme": "थीम अपलोड करा", - "description": "खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.", - "loading": "कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...", - "uploadSuccess": "तुमची थीम यशस्वीरित्या अपलोड झाली आहे", - "deletionFailure": "थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.", - "filePickerDialogTitle": ".flowy_plugin फाईल निवडा", - "urlUploadFailure": "URL उघडण्यात अयशस्वी: {}" - }, - "theme": "थीम", - "builtInsLabel": "अंतर्गत थीम्स", - "pluginsLabel": "प्लगइन्स", - "dateFormat": { - "label": "दिनांक फॉरमॅट", - "local": "स्थानिक", - "us": "US", - "iso": "ISO", - "friendly": "अनौपचारिक", - "dmy": "D/M/Y" - }, - "timeFormat": { - "label": "वेळ फॉरमॅट", - "twelveHour": "१२ तास", - "twentyFourHour": "२४ तास" - }, - "showNamingDialogWhenCreatingPage": "पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा", - "enableRTLToolbarItems": "RTL टूलबार आयटम्स सक्षम करा", - "members": { - "title": "सदस्य सेटिंग्ज", - "inviteMembers": "सदस्यांना आमंत्रण द्या", - "inviteHint": "ईमेलद्वारे आमंत्रण द्या", - "sendInvite": "आमंत्रण पाठवा", - "copyInviteLink": "आमंत्रण दुवा कॉपी करा", - "label": "सदस्य", - "user": "वापरकर्ता", - "role": "भूमिका", - "removeFromWorkspace": "वर्कस्पेसमधून काढा", - "removeFromWorkspaceSuccess": "वर्कस्पेसमधून यशस्वीरित्या काढले", - "removeFromWorkspaceFailed": "वर्कस्पेसमधून काढण्यात अयशस्वी", - "owner": "मालक", - "guest": "अतिथी", - "member": "सदस्य", - "memberHintText": "सदस्य पृष्ठे वाचू व संपादित करू शकतो", - "guestHintText": "अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.", - "emailInvalidError": "अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा", - "emailSent": "ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा", - "members": "सदस्य", - "membersCount": { - "zero": "{} सदस्य", - "one": "{} सदस्य", - "other": "{} सदस्य" - }, - "inviteFailedDialogTitle": "आमंत्रण पाठवण्यात अयशस्वी", - "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.", - "inviteFailedMemberLimitMobile": "तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.", - "memberLimitExceeded": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया ", - "memberLimitExceededUpgrade": "अपग्रेड करा", - "memberLimitExceededPro": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "सदस्य जोडण्यात अयशस्वी", - "addMemberSuccess": "सदस्य यशस्वीरित्या जोडला गेला", - "removeMember": "सदस्य काढा", - "areYouSureToRemoveMember": "तुम्हाला हा सदस्य काढायचा आहे का?", - "inviteMemberSuccess": "आमंत्रण यशस्वीरित्या पाठवले गेले", - "failedToInviteMember": "सदस्य आमंत्रित करण्यात अयशस्वी", - "workspaceMembersError": "अरे! काहीतरी चूक झाली आहे", - "workspaceMembersErrorDescription": "आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा" - } -}, - "files": { - "copy": "कॉपी करा", - "defaultLocation": "फाईल्स आणि डेटाचा संचय स्थान", - "exportData": "तुमचा डेटा निर्यात करा", - "doubleTapToCopy": "पथ कॉपी करण्यासाठी दोनदा टॅप करा", - "restoreLocation": "@:appName चे मूळ स्थान पुनर्संचयित करा", - "customizeLocation": "इतर फोल्डर उघडा", - "restartApp": "बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.", - "exportDatabase": "डेटाबेस निर्यात करा", - "selectFiles": "निर्यात करण्यासाठी फाईल्स निवडा", - "selectAll": "सर्व निवडा", - "deselectAll": "सर्व निवड रद्द करा", - "createNewFolder": "नवीन फोल्डर तयार करा", - "createNewFolderDesc": "तुमचा डेटा कुठे साठवायचा हे सांगा", - "defineWhereYourDataIsStored": "तुमचा डेटा कुठे साठवला जातो हे ठरवा", - "open": "उघडा", - "openFolder": "आधीक फोल्डर उघडा", - "openFolderDesc": "तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा", - "folderHintText": "फोल्डरचे नाव", - "location": "नवीन फोल्डर तयार करत आहे", - "locationDesc": "तुमच्या @:appName डेटासाठी नाव निवडा", - "browser": "ब्राउझ करा", - "create": "तयार करा", - "set": "सेट करा", - "folderPath": "फोल्डर साठवण्याचा मार्ग", - "locationCannotBeEmpty": "मार्ग रिकामा असू शकत नाही", - "pathCopiedSnackbar": "फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!", - "changeLocationTooltips": "डेटा डिरेक्टरी बदला", - "change": "बदला", - "openLocationTooltips": "इतर डेटा डिरेक्टरी उघडा", - "openCurrentDataFolder": "सध्याची डेटा डिरेक्टरी उघडा", - "recoverLocationTooltips": "@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा", - "exportFileSuccess": "फाईल यशस्वीरित्या निर्यात झाली!", - "exportFileFail": "फाईल निर्यात करण्यात अयशस्वी!", - "export": "निर्यात करा", - "clearCache": "कॅशे साफ करा", - "clearCacheDesc": "प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.", - "areYouSureToClearCache": "तुम्हाला नक्की कॅशे साफ करायची आहे का?", - "clearCacheSuccess": "कॅशे यशस्वीरित्या साफ झाली!" -}, - "user": { - "name": "नाव", - "email": "ईमेल", - "tooltipSelectIcon": "चिन्ह निवडा", - "selectAnIcon": "चिन्ह निवडा", - "pleaseInputYourOpenAIKey": "कृपया तुमची AI की टाका", - "clickToLogout": "सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा" -}, - "mobile": { - "personalInfo": "वैयक्तिक माहिती", - "username": "वापरकर्तानाव", - "usernameEmptyError": "वापरकर्तानाव रिकामे असू शकत नाही", - "about": "विषयी", - "pushNotifications": "पुश सूचना", - "support": "सपोर्ट", - "joinDiscord": "Discord मध्ये सहभागी व्हा", - "privacyPolicy": "गोपनीयता धोरण", - "userAgreement": "वापरकर्ता करार", - "termsAndConditions": "अटी व शर्ती", - "userprofileError": "वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी", - "userprofileErrorDescription": "कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.", - "selectLayout": "लेआउट निवडा", - "selectStartingDay": "सप्ताहाचा प्रारंभ दिवस निवडा", - "version": "आवृत्ती" -}, - "grid": { - "deleteView": "तुम्हाला हे दृश्य हटवायचे आहे का?", - "createView": "नवीन", - "title": { - "placeholder": "नाव नाही" - }, - "settings": { - "filter": "फिल्टर", - "sort": "क्रमवारी", - "sortBy": "यावरून क्रमवारी लावा", - "properties": "गुणधर्म", - "reorderPropertiesTooltip": "गुणधर्मांचे स्थान बदला", - "group": "समूह", - "addFilter": "फिल्टर जोडा", - "deleteFilter": "फिल्टर हटवा", - "filterBy": "यावरून फिल्टर करा", - "typeAValue": "मूल्य लिहा...", - "layout": "लेआउट", - "compactMode": "कॉम्पॅक्ट मोड", - "databaseLayout": "लेआउट", - "viewList": { - "zero": "० दृश्ये", - "one": "{count} दृश्य", - "other": "{count} दृश्ये" - }, - "editView": "दृश्य संपादित करा", - "boardSettings": "बोर्ड सेटिंग", - "calendarSettings": "कॅलेंडर सेटिंग", - "createView": "नवीन दृश्य", - "duplicateView": "दृश्याची प्रत बनवा", - "deleteView": "दृश्य हटवा", - "numberOfVisibleFields": "{} दर्शविले" - }, - "filter": { - "empty": "कोणतेही सक्रिय फिल्टर नाहीत", - "addFilter": "फिल्टर जोडा", - "cannotFindCreatableField": "फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही", - "conditon": "अट", - "where": "जिथे" - }, - "textFilter": { - "contains": "अंतर्भूत आहे", - "doesNotContain": "अंतर्भूत नाही", - "endsWith": "याने समाप्त होते", - "startWith": "याने सुरू होते", - "is": "आहे", - "isNot": "नाही", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही", - "choicechipPrefix": { - "isNot": "नाही", - "startWith": "याने सुरू होते", - "endWith": "याने समाप्त होते", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" - } - }, - "checkboxFilter": { - "isChecked": "निवडलेले आहे", - "isUnchecked": "निवडलेले नाही", - "choicechipPrefix": { - "is": "आहे" - } - }, - "checklistFilter": { - "isComplete": "पूर्ण झाले आहे", - "isIncomplted": "अपूर्ण आहे" - }, - "selectOptionFilter": { - "is": "आहे", - "isNot": "नाही", - "contains": "अंतर्भूत आहे", - "doesNotContain": "अंतर्भूत नाही", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" -}, -"dateFilter": { - "is": "या दिवशी आहे", - "before": "पूर्वी आहे", - "after": "नंतर आहे", - "onOrBefore": "या दिवशी किंवा त्याआधी आहे", - "onOrAfter": "या दिवशी किंवा त्यानंतर आहे", - "between": "दरम्यान आहे", - "empty": "रिकामे आहे", - "notEmpty": "रिकामे नाही", - "startDate": "सुरुवातीची तारीख", - "endDate": "शेवटची तारीख", - "choicechipPrefix": { - "before": "पूर्वी", - "after": "नंतर", - "between": "दरम्यान", - "onOrBefore": "या दिवशी किंवा त्याआधी", - "onOrAfter": "या दिवशी किंवा त्यानंतर", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" - } -}, -"numberFilter": { - "equal": "बरोबर आहे", - "notEqual": "बरोबर नाही", - "lessThan": "पेक्षा कमी आहे", - "greaterThan": "पेक्षा जास्त आहे", - "lessThanOrEqualTo": "किंवा कमी आहे", - "greaterThanOrEqualTo": "किंवा जास्त आहे", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" -}, -"field": { - "label": "गुणधर्म", - "hide": "गुणधर्म लपवा", - "show": "गुणधर्म दर्शवा", - "insertLeft": "डावीकडे जोडा", - "insertRight": "उजवीकडे जोडा", - "duplicate": "प्रत बनवा", - "delete": "हटवा", - "wrapCellContent": "पाठ लपेटा", - "clear": "सेल्स रिकामे करा", - "switchPrimaryFieldTooltip": "प्राथमिक फील्डचा प्रकार बदलू शकत नाही", - "textFieldName": "मजकूर", - "checkboxFieldName": "चेकबॉक्स", - "dateFieldName": "तारीख", - "updatedAtFieldName": "शेवटचे अपडेट", - "createdAtFieldName": "तयार झाले", - "numberFieldName": "संख्या", - "singleSelectFieldName": "सिंगल सिलेक्ट", - "multiSelectFieldName": "मल्टीसिलेक्ट", - "urlFieldName": "URL", - "checklistFieldName": "चेकलिस्ट", - "relationFieldName": "संबंध", - "summaryFieldName": "AI सारांश", - "timeFieldName": "वेळ", - "mediaFieldName": "फाईल्स आणि मीडिया", - "translateFieldName": "AI भाषांतर", - "translateTo": "मध्ये भाषांतर करा", - "numberFormat": "संख्या स्वरूप", - "dateFormat": "तारीख स्वरूप", - "includeTime": "वेळ जोडा", - "isRange": "शेवटची तारीख", - "dateFormatFriendly": "महिना दिवस, वर्ष", - "dateFormatISO": "वर्ष-महिना-दिनांक", - "dateFormatLocal": "महिना/दिवस/वर्ष", - "dateFormatUS": "वर्ष/महिना/दिवस", - "dateFormatDayMonthYear": "दिवस/महिना/वर्ष", - "timeFormat": "वेळ स्वरूप", - "invalidTimeFormat": "अवैध स्वरूप", - "timeFormatTwelveHour": "१२ तास", - "timeFormatTwentyFourHour": "२४ तास", - "clearDate": "तारीख हटवा", - "dateTime": "तारीख व वेळ", - "startDateTime": "सुरुवातीची तारीख व वेळ", - "endDateTime": "शेवटची तारीख व वेळ", - "failedToLoadDate": "तारीख मूल्य लोड करण्यात अयशस्वी", - "selectTime": "वेळ निवडा", - "selectDate": "तारीख निवडा", - "visibility": "दृश्यता", - "propertyType": "गुणधर्माचा प्रकार", - "addSelectOption": "पर्याय जोडा", - "typeANewOption": "नवीन पर्याय लिहा", - "optionTitle": "पर्याय", - "addOption": "पर्याय जोडा", - "editProperty": "गुणधर्म संपादित करा", - "newProperty": "नवीन गुणधर्म", - "openRowDocument": "पृष्ठ म्हणून उघडा", - "deleteFieldPromptMessage": "तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल", - "clearFieldPromptMessage": "तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील", - "newColumn": "नवीन कॉलम", - "format": "स्वरूप", - "reminderOnDateTooltip": "या सेलमध्ये अनुस्मारक शेड्यूल केले आहे", - "optionAlreadyExist": "पर्याय आधीच अस्तित्वात आहे" -}, - "rowPage": { - "newField": "नवीन फील्ड जोडा", - "fieldDragElementTooltip": "मेनू उघडण्यासाठी क्लिक करा", - "showHiddenFields": { - "one": "{count} लपलेले फील्ड दाखवा", - "many": "{count} लपलेली फील्ड दाखवा", - "other": "{count} लपलेली फील्ड दाखवा" - }, - "hideHiddenFields": { - "one": "{count} लपलेले फील्ड लपवा", - "many": "{count} लपलेली फील्ड लपवा", - "other": "{count} लपलेली फील्ड लपवा" - }, - "openAsFullPage": "पूर्ण पृष्ठ म्हणून उघडा", - "moreRowActions": "अधिक पंक्ती क्रिया" -}, -"sort": { - "ascending": "चढत्या क्रमाने", - "descending": "उतरत्या क्रमाने", - "by": "द्वारे", - "empty": "सक्रिय सॉर्ट्स नाहीत", - "cannotFindCreatableField": "सॉर्टसाठी योग्य फील्ड सापडले नाही", - "deleteAllSorts": "सर्व सॉर्ट्स हटवा", - "addSort": "सॉर्ट जोडा", - "sortsActive": "सॉर्टिंग करत असताना {intention} करू शकत नाही", - "removeSorting": "या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?", - "fieldInUse": "या फील्डवर आधीच सॉर्टिंग चालू आहे" -}, -"row": { - "label": "पंक्ती", - "duplicate": "प्रत बनवा", - "delete": "हटवा", - "titlePlaceholder": "शीर्षक नाही", - "textPlaceholder": "रिक्त", - "copyProperty": "गुणधर्म क्लिपबोर्डवर कॉपी केला", - "count": "संख्या", - "newRow": "नवीन पंक्ती", - "loadMore": "अधिक लोड करा", - "action": "क्रिया", - "add": "खाली जोडा वर क्लिक करा", - "drag": "हलवण्यासाठी ड्रॅग करा", - "deleteRowPrompt": "तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "deleteCardPrompt": "तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "dragAndClick": "हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा", - "insertRecordAbove": "वर रेकॉर्ड जोडा", - "insertRecordBelow": "खाली रेकॉर्ड जोडा", - "noContent": "माहिती नाही", - "reorderRowDescription": "पंक्तीचे पुन्हा क्रमांकन", - "createRowAboveDescription": "वर पंक्ती तयार करा", - "createRowBelowDescription": "खाली पंक्ती जोडा" -}, -"selectOption": { - "create": "तयार करा", - "purpleColor": "जांभळा", - "pinkColor": "गुलाबी", - "lightPinkColor": "फिकट गुलाबी", - "orangeColor": "नारंगी", - "yellowColor": "पिवळा", - "limeColor": "लिंबू", - "greenColor": "हिरवा", - "aquaColor": "आक्वा", - "blueColor": "निळा", - "deleteTag": "टॅग हटवा", - "colorPanelTitle": "रंग", - "panelTitle": "पर्याय निवडा किंवा नवीन तयार करा", - "searchOption": "पर्याय शोधा", - "searchOrCreateOption": "पर्याय शोधा किंवा तयार करा", - "createNew": "नवीन तयार करा", - "orSelectOne": "किंवा पर्याय निवडा", - "typeANewOption": "नवीन पर्याय टाइप करा", - "tagName": "टॅग नाव" -}, -"checklist": { - "taskHint": "कार्याचे वर्णन", - "addNew": "नवीन कार्य जोडा", - "submitNewTask": "तयार करा", - "hideComplete": "पूर्ण कार्ये लपवा", - "showComplete": "सर्व कार्ये दाखवा" -}, -"url": { - "launch": "बाह्य ब्राउझरमध्ये लिंक उघडा", - "copy": "लिंक क्लिपबोर्डवर कॉपी करा", - "textFieldHint": "URL टाका", - "copiedNotification": "क्लिपबोर्डवर कॉपी केले!" -}, -"relation": { - "relatedDatabasePlaceLabel": "संबंधित डेटाबेस", - "relatedDatabasePlaceholder": "काही नाही", - "inRelatedDatabase": "या मध्ये", - "rowSearchTextFieldPlaceholder": "शोध", - "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:", - "emptySearchResult": "कोणतीही नोंद सापडली नाही", - "linkedRowListLabel": "{count} लिंक केलेल्या पंक्ती", - "unlinkedRowListLabel": "आणखी एक पंक्ती लिंक करा" -}, -"menuName": "ग्रिड", -"referencedGridPrefix": "दृश्य", -"calculate": "गणना करा", -"calculationTypeLabel": { - "none": "काही नाही", - "average": "सरासरी", - "max": "कमाल", - "median": "मध्यम", - "min": "किमान", - "sum": "बेरीज", - "count": "मोजणी", - "countEmpty": "रिकाम्यांची मोजणी", - "countEmptyShort": "रिक्त", - "countNonEmpty": "रिक्त नसलेल्यांची मोजणी", - "countNonEmptyShort": "भरलेले" -}, -"media": { - "rename": "पुन्हा नाव द्या", - "download": "डाउनलोड करा", - "expand": "मोठे करा", - "delete": "हटवा", - "moreFilesHint": "+{}", - "addFileOrImage": "फाईल किंवा लिंक जोडा", - "attachmentsHint": "{}", - "addFileMobile": "फाईल जोडा", - "extraCount": "+{}", - "deleteFileDescription": "तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "showFileNames": "फाईलचे नाव दाखवा", - "downloadSuccess": "फाईल डाउनलोड झाली", - "downloadFailedToken": "फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध", - "setAsCover": "कव्हर म्हणून सेट करा", - "openInBrowser": "ब्राउझरमध्ये उघडा", - "embedLink": "फाईल लिंक एम्बेड करा" - } -}, - "document": { - "menuName": "दस्तऐवज", - "date": { - "timeHintTextInTwelveHour": "01:00 PM", - "timeHintTextInTwentyFourHour": "13:00" - }, - "creating": "तयार करत आहे...", - "slashMenu": { - "board": { - "selectABoardToLinkTo": "लिंक करण्यासाठी बोर्ड निवडा", - "createANewBoard": "नवीन बोर्ड तयार करा" - }, - "grid": { - "selectAGridToLinkTo": "लिंक करण्यासाठी ग्रिड निवडा", - "createANewGrid": "नवीन ग्रिड तयार करा" - }, - "calendar": { - "selectACalendarToLinkTo": "लिंक करण्यासाठी दिनदर्शिका निवडा", - "createANewCalendar": "नवीन दिनदर्शिका तयार करा" - }, - "document": { - "selectADocumentToLinkTo": "लिंक करण्यासाठी दस्तऐवज निवडा" - }, - "name": { - "textStyle": "मजकुराची शैली", - "list": "यादी", - "toggle": "टॉगल", - "fileAndMedia": "फाईल व मीडिया", - "simpleTable": "सोपे टेबल", - "visuals": "दृश्य घटक", - "document": "दस्तऐवज", - "advanced": "प्रगत", - "text": "मजकूर", - "heading1": "शीर्षक 1", - "heading2": "शीर्षक 2", - "heading3": "शीर्षक 3", - "image": "प्रतिमा", - "bulletedList": "बुलेट यादी", - "numberedList": "क्रमांकित यादी", - "todoList": "करण्याची यादी", - "doc": "दस्तऐवज", - "linkedDoc": "पृष्ठाशी लिंक करा", - "grid": "ग्रिड", - "linkedGrid": "लिंक केलेला ग्रिड", - "kanban": "कानबन", - "linkedKanban": "लिंक केलेला कानबन", - "calendar": "दिनदर्शिका", - "linkedCalendar": "लिंक केलेली दिनदर्शिका", - "quote": "उद्धरण", - "divider": "विभाजक", - "table": "टेबल", - "callout": "महत्त्वाचा मजकूर", - "outline": "रूपरेषा", - "mathEquation": "गणिती समीकरण", - "code": "कोड", - "toggleList": "टॉगल यादी", - "toggleHeading1": "टॉगल शीर्षक 1", - "toggleHeading2": "टॉगल शीर्षक 2", - "toggleHeading3": "टॉगल शीर्षक 3", - "emoji": "इमोजी", - "aiWriter": "AI ला काहीही विचारा", - "dateOrReminder": "दिनांक किंवा स्मरणपत्र", - "photoGallery": "फोटो गॅलरी", - "file": "फाईल", - "twoColumns": "२ स्तंभ", - "threeColumns": "३ स्तंभ", - "fourColumns": "४ स्तंभ" - }, - "subPage": { - "name": "दस्तऐवज", - "keyword1": "उपपृष्ठ", - "keyword2": "पृष्ठ", - "keyword3": "चाइल्ड पृष्ठ", - "keyword4": "पृष्ठ जोडा", - "keyword5": "एम्बेड पृष्ठ", - "keyword6": "नवीन पृष्ठ", - "keyword7": "पृष्ठ तयार करा", - "keyword8": "दस्तऐवज" - } - }, - "selectionMenu": { - "outline": "रूपरेषा", - "codeBlock": "कोड ब्लॉक" - }, - "plugins": { - "referencedBoard": "संदर्भित बोर्ड", - "referencedGrid": "संदर्भित ग्रिड", - "referencedCalendar": "संदर्भित दिनदर्शिका", - "referencedDocument": "संदर्भित दस्तऐवज", - "aiWriter": { - "userQuestion": "AI ला काहीही विचारा", - "continueWriting": "लेखन सुरू ठेवा", - "fixSpelling": "स्पेलिंग व व्याकरण सुधारणा", - "improveWriting": "लेखन सुधारित करा", - "summarize": "सारांश द्या", - "explain": "स्पष्टीकरण द्या", - "makeShorter": "लहान करा", - "makeLonger": "मोठे करा" - }, - "autoGeneratorMenuItemName": "AI लेखक", -"autoGeneratorTitleName": "AI: काहीही लिहिण्यासाठी AI ला विचारा...", -"autoGeneratorLearnMore": "अधिक जाणून घ्या", -"autoGeneratorGenerate": "उत्पन्न करा", -"autoGeneratorHintText": "AI ला विचारा...", -"autoGeneratorCantGetOpenAIKey": "AI की मिळवू शकलो नाही", -"autoGeneratorRewrite": "पुन्हा लिहा", -"smartEdit": "AI ला विचारा", -"aI": "AI", -"smartEditFixSpelling": "स्पेलिंग आणि व्याकरण सुधारा", -"warning": "⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.", -"smartEditSummarize": "सारांश द्या", -"smartEditImproveWriting": "लेखन सुधारित करा", -"smartEditMakeLonger": "लांब करा", -"smartEditCouldNotFetchResult": "AI कडून उत्तर मिळवता आले नाही", -"smartEditCouldNotFetchKey": "AI की मिळवता आली नाही", -"smartEditDisabled": "सेटिंग्जमध्ये AI कनेक्ट करा", -"appflowyAIEditDisabled": "AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा", -"discardResponse": "AI उत्तर फेकून द्यायचं आहे का?", -"createInlineMathEquation": "समीकरण तयार करा", -"fonts": "फॉन्ट्स", -"insertDate": "तारीख जोडा", -"emoji": "इमोजी", -"toggleList": "टॉगल यादी", -"emptyToggleHeading": "रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.", -"emptyToggleList": "रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.", -"emptyToggleHeadingWeb": "रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा", -"quoteList": "उद्धरण यादी", -"numberedList": "क्रमांकित यादी", -"bulletedList": "बुलेट यादी", -"todoList": "करण्याची यादी", -"callout": "ठळक मजकूर", -"simpleTable": { - "moreActions": { - "color": "रंग", - "align": "पंक्तिबद्ध करा", - "delete": "हटा", - "duplicate": "डुप्लिकेट करा", - "insertLeft": "डावीकडे घाला", - "insertRight": "उजवीकडे घाला", - "insertAbove": "वर घाला", - "insertBelow": "खाली घाला", - "headerColumn": "हेडर स्तंभ", - "headerRow": "हेडर ओळ", - "clearContents": "सामग्री साफ करा", - "setToPageWidth": "पृष्ठाच्या रुंदीप्रमाणे सेट करा", - "distributeColumnsWidth": "स्तंभ समान करा", - "duplicateRow": "ओळ डुप्लिकेट करा", - "duplicateColumn": "स्तंभ डुप्लिकेट करा", - "textColor": "मजकूराचा रंग", - "cellBackgroundColor": "सेलचा पार्श्वभूमी रंग", - "duplicateTable": "टेबल डुप्लिकेट करा" - }, - "clickToAddNewRow": "नवीन ओळ जोडण्यासाठी क्लिक करा", - "clickToAddNewColumn": "नवीन स्तंभ जोडण्यासाठी क्लिक करा", - "clickToAddNewRowAndColumn": "नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा", - "headerName": { - "table": "टेबल", - "alignText": "मजकूर पंक्तिबद्ध करा" - } -}, -"cover": { - "changeCover": "कव्हर बदला", - "colors": "रंग", - "images": "प्रतिमा", - "clearAll": "सर्व साफ करा", - "abstract": "ऍबस्ट्रॅक्ट", - "addCover": "कव्हर जोडा", - "addLocalImage": "स्थानिक प्रतिमा जोडा", - "invalidImageUrl": "अवैध प्रतिमा URL", - "failedToAddImageToGallery": "प्रतिमा गॅलरीत जोडता आली नाही", - "enterImageUrl": "प्रतिमा URL लिहा", - "add": "जोडा", - "back": "मागे", - "saveToGallery": "गॅलरीत जतन करा", - "removeIcon": "आयकॉन काढा", - "removeCover": "कव्हर काढा", - "pasteImageUrl": "प्रतिमा URL पेस्ट करा", - "or": "किंवा", - "pickFromFiles": "फाईल्समधून निवडा", - "couldNotFetchImage": "प्रतिमा मिळवता आली नाही", - "imageSavingFailed": "प्रतिमा जतन करणे अयशस्वी", - "addIcon": "आयकॉन जोडा", - "changeIcon": "आयकॉन बदला", - "coverRemoveAlert": "हे हटवल्यानंतर कव्हरमधून काढले जाईल.", - "alertDialogConfirmation": "तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?" -}, -"mathEquation": { - "name": "गणिती समीकरण", - "addMathEquation": "TeX समीकरण जोडा", - "editMathEquation": "गणिती समीकरण संपादित करा" -}, -"optionAction": { - "click": "क्लिक", - "toOpenMenu": "मेनू उघडण्यासाठी", - "drag": "ओढा", - "toMove": "हलवण्यासाठी", - "delete": "हटा", - "duplicate": "डुप्लिकेट करा", - "turnInto": "मध्ये बदला", - "moveUp": "वर हलवा", - "moveDown": "खाली हलवा", - "color": "रंग", - "align": "पंक्तिबद्ध करा", - "left": "डावीकडे", - "center": "मध्यभागी", - "right": "उजवीकडे", - "defaultColor": "डिफॉल्ट", - "depth": "खोली", - "copyLinkToBlock": "ब्लॉकसाठी लिंक कॉपी करा" -}, - "image": { - "addAnImage": "प्रतिमा जोडा", - "copiedToPasteBoard": "प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "addAnImageDesktop": "प्रतिमा जोडा", - "addAnImageMobile": "एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा", - "dropImageToInsert": "प्रतिमा ड्रॉप करून जोडा", - "imageUploadFailed": "प्रतिमा अपलोड करण्यात अयशस्वी", - "imageDownloadFailed": "प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "imageDownloadFailedToken": "युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "errorCode": "त्रुटी कोड" -}, -"photoGallery": { - "name": "फोटो गॅलरी", - "imageKeyword": "प्रतिमा", - "imageGalleryKeyword": "प्रतिमा गॅलरी", - "photoKeyword": "फोटो", - "photoBrowserKeyword": "फोटो ब्राउझर", - "galleryKeyword": "गॅलरी", - "addImageTooltip": "प्रतिमा जोडा", - "changeLayoutTooltip": "लेआउट बदला", - "browserLayout": "ब्राउझर", - "gridLayout": "ग्रिड", - "deleteBlockTooltip": "संपूर्ण गॅलरी हटवा" -}, -"math": { - "copiedToPasteBoard": "समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे" -}, -"urlPreview": { - "copiedToPasteBoard": "लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "convertToLink": "एंबेड लिंकमध्ये रूपांतर करा" -}, -"outline": { - "addHeadingToCreateOutline": "सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.", - "noMatchHeadings": "जुळणारी शीर्षके आढळली नाहीत." -}, -"table": { - "addAfter": "नंतर जोडा", - "addBefore": "आधी जोडा", - "delete": "हटा", - "clear": "सामग्री साफ करा", - "duplicate": "डुप्लिकेट करा", - "bgColor": "पार्श्वभूमीचा रंग" -}, -"contextMenu": { - "copy": "कॉपी करा", - "cut": "कापा", - "paste": "पेस्ट करा", - "pasteAsPlainText": "साध्या मजकूराच्या स्वरूपात पेस्ट करा" -}, -"action": "कृती", -"database": { - "selectDataSource": "डेटा स्रोत निवडा", - "noDataSource": "डेटा स्रोत नाही", - "selectADataSource": "डेटा स्रोत निवडा", - "toContinue": "पुढे जाण्यासाठी", - "newDatabase": "नवीन डेटाबेस", - "linkToDatabase": "डेटाबेसशी लिंक करा" -}, -"date": "तारीख", -"video": { - "label": "व्हिडिओ", - "emptyLabel": "व्हिडिओ जोडा", - "placeholder": "व्हिडिओ लिंक पेस्ट करा", - "copiedToPasteBoard": "व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "insertVideo": "व्हिडिओ जोडा", - "invalidVideoUrl": "ही URL सध्या समर्थित नाही.", - "invalidVideoUrlYouTube": "YouTube सध्या समर्थित नाही.", - "supportedFormats": "समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" -}, -"file": { - "name": "फाईल", - "uploadTab": "अपलोड", - "uploadMobile": "फाईल निवडा", - "uploadMobileGallery": "फोटो गॅलरीमधून", - "networkTab": "लिंक एम्बेड करा", - "placeholderText": "फाईल अपलोड किंवा एम्बेड करा", - "placeholderDragging": "फाईल ड्रॉप करून अपलोड करा", - "dropFileToUpload": "फाईल ड्रॉप करून अपलोड करा", - "fileUploadHint": "फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा ", - "fileUploadHintSuffix": "ब्राउझ करा", - "networkHint": "फाईल लिंक पेस्ट करा", - "networkUrlInvalid": "अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.", - "networkAction": "एम्बेड", - "fileTooBigError": "फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा", - "renameFile": { - "title": "फाईलचे नाव बदला", - "description": "या फाईलसाठी नवीन नाव लिहा", - "nameEmptyError": "फाईलचे नाव रिकामे असू शकत नाही." - }, - "uploadedAt": "{} रोजी अपलोड केले", - "linkedAt": "{} रोजी लिंक जोडली", - "failedToOpenMsg": "उघडण्यात अयशस्वी, फाईल सापडली नाही" -}, -"subPage": { - "handlingPasteHint": " - (पेस्ट प्रक्रिया करत आहे)", - "errors": { - "failedDeletePage": "पृष्ठ हटवण्यात अयशस्वी", - "failedCreatePage": "पृष्ठ तयार करण्यात अयशस्वी", - "failedMovePage": "हे पृष्ठ हलवण्यात अयशस्वी", - "failedDuplicatePage": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी", - "failedDuplicateFindView": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही" - } -}, - "cannotMoveToItsChildren": "मुलांमध्ये हलवू शकत नाही" -}, -"outlineBlock": { - "placeholder": "सामग्री सूची" -}, -"textBlock": { - "placeholder": "कमांडसाठी '/' टाइप करा" -}, -"title": { - "placeholder": "शीर्षक नाही" -}, -"imageBlock": { - "placeholder": "प्रतिमा जोडण्यासाठी क्लिक करा", - "upload": { - "label": "अपलोड", - "placeholder": "प्रतिमा अपलोड करण्यासाठी क्लिक करा" - }, - "url": { - "label": "प्रतिमेची URL", - "placeholder": "प्रतिमेची URL टाका" - }, - "ai": { - "label": "AI द्वारे प्रतिमा तयार करा", - "placeholder": "AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" - }, - "stability_ai": { - "label": "Stability AI द्वारे प्रतिमा तयार करा", - "placeholder": "Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" - }, - "support": "प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG", - "error": { - "invalidImage": "अवैध प्रतिमा", - "invalidImageSize": "प्रतिमेचा आकार 5MB पेक्षा कमी असावा", - "invalidImageFormat": "प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "अवैध प्रतिमेची URL", - "noImage": "अशी फाईल किंवा निर्देशिका नाही", - "multipleImagesFailed": "एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा" - }, - "embedLink": { - "label": "लिंक एम्बेड करा", - "placeholder": "प्रतिमेची लिंक पेस्ट करा किंवा टाका" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "प्रतिमा शोधा", - "pleaseInputYourOpenAIKey": "कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा", - "saveImageToGallery": "प्रतिमा जतन करा", - "failedToAddImageToGallery": "प्रतिमा जतन करण्यात अयशस्वी", - "successToAddImageToGallery": "प्रतिमा 'Photos' मध्ये जतन केली", - "unableToLoadImage": "प्रतिमा लोड करण्यात अयशस्वी", - "maximumImageSize": "कमाल प्रतिमा अपलोड आकार 10MB आहे", - "uploadImageErrorImageSizeTooBig": "प्रतिमेचा आकार 10MB पेक्षा कमी असावा", - "imageIsUploading": "प्रतिमा अपलोड होत आहे", - "openFullScreen": "पूर्ण स्क्रीनमध्ये उघडा", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "मागील प्रतिमा", - "nextImageTooltip": "पुढील प्रतिमा", - "zoomOutTooltip": "लहान करा", - "zoomInTooltip": "मोठी करा", - "changeZoomLevelTooltip": "झूम पातळी बदला", - "openLocalImage": "प्रतिमा उघडा", - "downloadImage": "प्रतिमा डाउनलोड करा", - "closeViewer": "इंटरअॅक्टिव्ह व्ह्युअर बंद करा", - "scalePercentage": "{}%", - "deleteImageTooltip": "प्रतिमा हटवा" - } - } -}, - "codeBlock": { - "language": { - "label": "भाषा", - "placeholder": "भाषा निवडा", - "auto": "स्वयंचलित" - }, - "copyTooltip": "कॉपी करा", - "searchLanguageHint": "भाषा शोधा", - "codeCopiedSnackbar": "कोड क्लिपबोर्डवर कॉपी झाला!" -}, -"inlineLink": { - "placeholder": "लिंक पेस्ट करा किंवा टाका", - "openInNewTab": "नवीन टॅबमध्ये उघडा", - "copyLink": "लिंक कॉपी करा", - "removeLink": "लिंक काढा", - "url": { - "label": "लिंक URL", - "placeholder": "लिंक URL टाका" - }, - "title": { - "label": "लिंक शीर्षक", - "placeholder": "लिंक शीर्षक टाका" - } -}, -"mention": { - "placeholder": "कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...", - "page": { - "label": "पृष्ठाला लिंक करा", - "tooltip": "पृष्ठ उघडण्यासाठी क्लिक करा" - }, - "deleted": "हटवले गेले", - "deletedContent": "ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे", - "noAccess": "प्रवेश नाही", - "deletedPage": "हटवलेले पृष्ठ", - "trashHint": " - ट्रॅशमध्ये", - "morePages": "अजून पृष्ठे" -}, -"toolbar": { - "resetToDefaultFont": "डीफॉल्ट फॉन्टवर परत जा", - "textSize": "मजकूराचा आकार", - "textColor": "मजकूराचा रंग", - "h1": "मथळा 1", - "h2": "मथळा 2", - "h3": "मथळा 3", - "alignLeft": "डावीकडे संरेखित करा", - "alignRight": "उजवीकडे संरेखित करा", - "alignCenter": "मध्यभागी संरेखित करा", - "link": "लिंक", - "textAlign": "मजकूर संरेखन", - "moreOptions": "अधिक पर्याय", - "font": "फॉन्ट", - "inlineCode": "इनलाइन कोड", - "suggestions": "सूचना", - "turnInto": "मध्ये रूपांतरित करा", - "equation": "समीकरण", - "insert": "घाला", - "linkInputHint": "लिंक पेस्ट करा किंवा पृष्ठे शोधा", - "pageOrURL": "पृष्ठ किंवा URL", - "linkName": "लिंकचे नाव", - "linkNameHint": "लिंकचे नाव प्रविष्ट करा" -}, -"errorBlock": { - "theBlockIsNotSupported": "ब्लॉक सामग्री पार्स करण्यात अक्षम", - "clickToCopyTheBlockContent": "ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा", - "blockContentHasBeenCopied": "ब्लॉक सामग्री कॉपी केली आहे.", - "parseError": "{} ब्लॉक पार्स करताना त्रुटी आली.", - "copyBlockContent": "ब्लॉक सामग्री कॉपी करा" -}, -"mobilePageSelector": { - "title": "पृष्ठ निवडा", - "failedToLoad": "पृष्ठ यादी लोड करण्यात अयशस्वी", - "noPagesFound": "कोणतीही पृष्ठे सापडली नाहीत" -}, -"attachmentMenu": { - "choosePhoto": "फोटो निवडा", - "takePicture": "फोटो काढा", - "chooseFile": "फाईल निवडा" - } - }, - "board": { - "column": { - "label": "स्तंभ", - "createNewCard": "नवीन", - "renameGroupTooltip": "गटाचे नाव बदलण्यासाठी क्लिक करा", - "createNewColumn": "नवीन गट जोडा", - "addToColumnTopTooltip": "वर नवीन कार्ड जोडा", - "addToColumnBottomTooltip": "खाली नवीन कार्ड जोडा", - "renameColumn": "स्तंभाचे नाव बदला", - "hideColumn": "लपवा", - "newGroup": "नवीन गट", - "deleteColumn": "हटवा", - "deleteColumnConfirmation": "हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?" - }, - "hiddenGroupSection": { - "sectionTitle": "लपवलेले गट", - "collapseTooltip": "लपवलेले गट लपवा", - "expandTooltip": "लपवलेले गट पाहा" - }, - "cardDetail": "कार्ड तपशील", - "cardActions": "कार्ड क्रिया", - "cardDuplicated": "कार्डची प्रत तयार झाली", - "cardDeleted": "कार्ड हटवले गेले", - "showOnCard": "कार्ड तपशिलावर दाखवा", - "setting": "सेटिंग", - "propertyName": "गुणधर्माचे नाव", - "menuName": "बोर्ड", - "showUngrouped": "गटात नसलेली कार्ड्स दाखवा", - "ungroupedButtonText": "गट नसलेली", - "ungroupedButtonTooltip": "ज्या कार्ड्स कोणत्याही गटात नाहीत", - "ungroupedItemsTitle": "बोर्डमध्ये जोडण्यासाठी क्लिक करा", - "groupBy": "या आधारावर गट करा", - "groupCondition": "गट स्थिती", - "referencedBoardPrefix": "याचे दृश्य", - "notesTooltip": "नोट्स आहेत", - "mobile": { - "editURL": "URL संपादित करा", - "showGroup": "गट दाखवा", - "showGroupContent": "हा गट बोर्डवर दाखवायचा आहे का?", - "failedToLoad": "बोर्ड दृश्य लोड होण्यात अयशस्वी" - }, - "dateCondition": { - "weekOf": "{} - {} ची आठवडा", - "today": "आज", - "yesterday": "काल", - "tomorrow": "उद्या", - "lastSevenDays": "शेवटचे ७ दिवस", - "nextSevenDays": "पुढील ७ दिवस", - "lastThirtyDays": "शेवटचे ३० दिवस", - "nextThirtyDays": "पुढील ३० दिवस" - }, - "noGroup": "गटासाठी कोणताही गुणधर्म निवडलेला नाही", - "noGroupDesc": "बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे", - "media": { - "cardText": "{} {}", - "fallbackName": "फायली" - } -}, - "calendar": { - "menuName": "कॅलेंडर", - "defaultNewCalendarTitle": "नाव नाही", - "newEventButtonTooltip": "नवीन इव्हेंट जोडा", - "navigation": { - "today": "आज", - "jumpToday": "आजवर जा", - "previousMonth": "मागील महिना", - "nextMonth": "पुढील महिना", - "views": { - "day": "दिवस", - "week": "आठवडा", - "month": "महिना", - "year": "वर्ष" - } - }, - "mobileEventScreen": { - "emptyTitle": "सध्या कोणतेही इव्हेंट नाहीत", - "emptyBody": "या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा." - }, - "settings": { - "showWeekNumbers": "आठवड्याचे क्रमांक दाखवा", - "showWeekends": "सप्ताहांत दाखवा", - "firstDayOfWeek": "आठवड्याची सुरुवात", - "layoutDateField": "कॅलेंडर मांडणी दिनांकानुसार", - "changeLayoutDateField": "मांडणी फील्ड बदला", - "noDateTitle": "तारीख नाही", - "noDateHint": { - "zero": "नियोजित नसलेली इव्हेंट्स येथे दिसतील", - "one": "{count} नियोजित नसलेली इव्हेंट", - "other": "{count} नियोजित नसलेल्या इव्हेंट्स" - }, - "unscheduledEventsTitle": "नियोजित नसलेल्या इव्हेंट्स", - "clickToAdd": "कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा", - "name": "कॅलेंडर सेटिंग्ज", - "clickToOpen": "रेकॉर्ड उघडण्यासाठी क्लिक करा" - }, - "referencedCalendarPrefix": "याचे दृश्य", - "quickJumpYear": "या वर्षावर जा", - "duplicateEvent": "इव्हेंट डुप्लिकेट करा" -}, - "errorDialog": { - "title": "@:appName त्रुटी", - "howToFixFallback": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.", - "howToFixFallbackHint1": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया ", - "howToFixFallbackHint2": " पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.", - "github": "GitHub वर पहा" -}, -"search": { - "label": "शोध", - "sidebarSearchIcon": "पृष्ठ शोधा आणि पटकन जा", - "placeholder": { - "actions": "कृती शोधा..." - } -}, -"message": { - "copy": { - "success": "कॉपी झाले!", - "fail": "कॉपी करू शकत नाही" - } -}, -"unSupportBlock": "सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.", -"views": { - "deleteContentTitle": "तुम्हाला हे {pageType} हटवायचे आहे का?", - "deleteContentCaption": "हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता." -}, - "colors": { - "custom": "सानुकूल", - "default": "डीफॉल्ट", - "red": "लाल", - "orange": "संत्रा", - "yellow": "पिवळा", - "green": "हिरवा", - "blue": "निळा", - "purple": "जांभळा", - "pink": "गुलाबी", - "brown": "तपकिरी", - "gray": "करड्या रंगाचा" -}, - "emoji": { - "emojiTab": "इमोजी", - "search": "इमोजी शोधा", - "noRecent": "अलीकडील कोणतेही इमोजी नाहीत", - "noEmojiFound": "कोणतेही इमोजी सापडले नाहीत", - "filter": "फिल्टर", - "random": "योगायोगाने", - "selectSkinTone": "त्वचेचा टोन निवडा", - "remove": "इमोजी काढा", - "categories": { - "smileys": "स्मायली आणि भावना", - "people": "लोक", - "animals": "प्राणी आणि निसर्ग", - "food": "अन्न", - "activities": "क्रिया", - "places": "स्थळे", - "objects": "वस्तू", - "symbols": "चिन्हे", - "flags": "ध्वज", - "nature": "निसर्ग", - "frequentlyUsed": "नेहमी वापरलेले" - }, - "skinTone": { - "default": "डीफॉल्ट", - "light": "हलका", - "mediumLight": "मध्यम-हलका", - "medium": "मध्यम", - "mediumDark": "मध्यम-गडद", - "dark": "गडद" - }, - "openSourceIconsFrom": "खुल्या स्रोताचे आयकॉन्स" -}, - "inlineActions": { - "noResults": "निकाल नाही", - "recentPages": "अलीकडील पृष्ठे", - "pageReference": "पृष्ठ संदर्भ", - "docReference": "दस्तऐवज संदर्भ", - "boardReference": "बोर्ड संदर्भ", - "calReference": "कॅलेंडर संदर्भ", - "gridReference": "ग्रिड संदर्भ", - "date": "तारीख", - "reminder": { - "groupTitle": "स्मरणपत्र", - "shortKeyword": "remind" - }, - "createPage": "\"{}\" उप-पृष्ठ तयार करा" -}, - "datePicker": { - "dateTimeFormatTooltip": "सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला", - "dateFormat": "तारीख फॉरमॅट", - "includeTime": "वेळ समाविष्ट करा", - "isRange": "शेवटची तारीख", - "timeFormat": "वेळ फॉरमॅट", - "clearDate": "तारीख साफ करा", - "reminderLabel": "स्मरणपत्र", - "selectReminder": "स्मरणपत्र निवडा", - "reminderOptions": { - "none": "काहीही नाही", - "atTimeOfEvent": "इव्हेंटच्या वेळी", - "fiveMinsBefore": "५ मिनिटे आधी", - "tenMinsBefore": "१० मिनिटे आधी", - "fifteenMinsBefore": "१५ मिनिटे आधी", - "thirtyMinsBefore": "३० मिनिटे आधी", - "oneHourBefore": "१ तास आधी", - "twoHoursBefore": "२ तास आधी", - "onDayOfEvent": "इव्हेंटच्या दिवशी", - "oneDayBefore": "१ दिवस आधी", - "twoDaysBefore": "२ दिवस आधी", - "oneWeekBefore": "१ आठवडा आधी", - "custom": "सानुकूल" - } -}, - "relativeDates": { - "yesterday": "काल", - "today": "आज", - "tomorrow": "उद्या", - "oneWeek": "१ आठवडा" -}, - "notificationHub": { - "title": "सूचना", - "mobile": { - "title": "अपडेट्स" - }, - "emptyTitle": "सर्व पूर्ण झाले!", - "emptyBody": "कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.", - "tabs": { - "inbox": "इनबॉक्स", - "upcoming": "आगामी" - }, - "actions": { - "markAllRead": "सर्व वाचलेल्या म्हणून चिन्हित करा", - "showAll": "सर्व", - "showUnreads": "न वाचलेल्या" - }, - "filters": { - "ascending": "आरोही", - "descending": "अवरोही", - "groupByDate": "तारीखेनुसार गटबद्ध करा", - "showUnreadsOnly": "फक्त न वाचलेल्या दाखवा", - "resetToDefault": "डीफॉल्टवर रीसेट करा" - } -}, - "reminderNotification": { - "title": "स्मरणपत्र", - "message": "तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!", - "tooltipDelete": "हटवा", - "tooltipMarkRead": "वाचले म्हणून चिन्हित करा", - "tooltipMarkUnread": "न वाचले म्हणून चिन्हित करा" -}, - "findAndReplace": { - "find": "शोधा", - "previousMatch": "मागील जुळणारे", - "nextMatch": "पुढील जुळणारे", - "close": "बंद करा", - "replace": "बदला", - "replaceAll": "सर्व बदला", - "noResult": "कोणतेही निकाल नाहीत", - "caseSensitive": "केस सेंसिटिव्ह", - "searchMore": "अधिक निकालांसाठी शोधा" -}, - "error": { - "weAreSorry": "आम्ही क्षमस्व आहोत", - "loadingViewError": "हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.", - "syncError": "इतर डिव्हाइसमधून डेटा सिंक झाला नाही", - "syncErrorHint": "कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.", - "clickToCopy": "एरर कोड कॉपी करण्यासाठी क्लिक करा" -}, - "editor": { - "bold": "जाड", - "bulletedList": "बुलेट यादी", - "bulletedListShortForm": "बुलेट", - "checkbox": "चेकबॉक्स", - "embedCode": "कोड एम्बेड करा", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "हायलाइट", - "color": "रंग", - "image": "प्रतिमा", - "date": "तारीख", - "page": "पृष्ठ", - "italic": "तिरका", - "link": "लिंक", - "numberedList": "क्रमांकित यादी", - "numberedListShortForm": "क्रमांकित", - "toggleHeading1ShortForm": "Toggle H1", - "toggleHeading2ShortForm": "Toggle H2", - "toggleHeading3ShortForm": "Toggle H3", - "quote": "कोट", - "strikethrough": "ओढून टाका", - "text": "मजकूर", - "underline": "अधोरेखित", - "fontColorDefault": "डीफॉल्ट", - "fontColorGray": "धूसर", - "fontColorBrown": "तपकिरी", - "fontColorOrange": "केशरी", - "fontColorYellow": "पिवळा", - "fontColorGreen": "हिरवा", - "fontColorBlue": "निळा", - "fontColorPurple": "जांभळा", - "fontColorPink": "पिंग", - "fontColorRed": "लाल", - "backgroundColorDefault": "डीफॉल्ट पार्श्वभूमी", - "backgroundColorGray": "धूसर पार्श्वभूमी", - "backgroundColorBrown": "तपकिरी पार्श्वभूमी", - "backgroundColorOrange": "केशरी पार्श्वभूमी", - "backgroundColorYellow": "पिवळी पार्श्वभूमी", - "backgroundColorGreen": "हिरवी पार्श्वभूमी", - "backgroundColorBlue": "निळी पार्श्वभूमी", - "backgroundColorPurple": "जांभळी पार्श्वभूमी", - "backgroundColorPink": "पिंग पार्श्वभूमी", - "backgroundColorRed": "लाल पार्श्वभूमी", - "backgroundColorLime": "लिंबू पार्श्वभूमी", - "backgroundColorAqua": "पाण्याचा पार्श्वभूमी", - "done": "पूर्ण", - "cancel": "रद्द करा", - "tint1": "टिंट 1", - "tint2": "टिंट 2", - "tint3": "टिंट 3", - "tint4": "टिंट 4", - "tint5": "टिंट 5", - "tint6": "टिंट 6", - "tint7": "टिंट 7", - "tint8": "टिंट 8", - "tint9": "टिंट 9", - "lightLightTint1": "जांभळा", - "lightLightTint2": "पिंग", - "lightLightTint3": "फिकट पिंग", - "lightLightTint4": "केशरी", - "lightLightTint5": "पिवळा", - "lightLightTint6": "लिंबू", - "lightLightTint7": "हिरवा", - "lightLightTint8": "पाणी", - "lightLightTint9": "निळा", - "urlHint": "URL", - "mobileHeading1": "Heading 1", - "mobileHeading2": "Heading 2", - "mobileHeading3": "Heading 3", - "mobileHeading4": "Heading 4", - "mobileHeading5": "Heading 5", - "mobileHeading6": "Heading 6", - "textColor": "मजकूराचा रंग", - "backgroundColor": "पार्श्वभूमीचा रंग", - "addYourLink": "तुमची लिंक जोडा", - "openLink": "लिंक उघडा", - "copyLink": "लिंक कॉपी करा", - "removeLink": "लिंक काढा", - "editLink": "लिंक संपादित करा", - "linkText": "मजकूर", - "linkTextHint": "कृपया मजकूर प्रविष्ट करा", - "linkAddressHint": "कृपया URL प्रविष्ट करा", - "highlightColor": "हायलाइट रंग", - "clearHighlightColor": "हायलाइट काढा", - "customColor": "स्वतःचा रंग", - "hexValue": "Hex मूल्य", - "opacity": "अपारदर्शकता", - "resetToDefaultColor": "डीफॉल्ट रंगावर रीसेट करा", - "ltr": "LTR", - "rtl": "RTL", - "auto": "स्वयंचलित", - "cut": "कट", - "copy": "कॉपी", - "paste": "पेस्ट", - "find": "शोधा", - "select": "निवडा", - "selectAll": "सर्व निवडा", - "previousMatch": "मागील जुळणारे", - "nextMatch": "पुढील जुळणारे", - "closeFind": "बंद करा", - "replace": "बदला", - "replaceAll": "सर्व बदला", - "regex": "Regex", - "caseSensitive": "केस सेंसिटिव्ह", - "uploadImage": "प्रतिमा अपलोड करा", - "urlImage": "URL प्रतिमा", - "incorrectLink": "चुकीची लिंक", - "upload": "अपलोड", - "chooseImage": "प्रतिमा निवडा", - "loading": "लोड करत आहे", - "imageLoadFailed": "प्रतिमा लोड करण्यात अयशस्वी", - "divider": "विभाजक", - "table": "तक्त्याचे स्वरूप", - "colAddBefore": "यापूर्वी स्तंभ जोडा", - "rowAddBefore": "यापूर्वी पंक्ती जोडा", - "colAddAfter": "यानंतर स्तंभ जोडा", - "rowAddAfter": "यानंतर पंक्ती जोडा", - "colRemove": "स्तंभ काढा", - "rowRemove": "पंक्ती काढा", - "colDuplicate": "स्तंभ डुप्लिकेट", - "rowDuplicate": "पंक्ती डुप्लिकेट", - "colClear": "सामग्री साफ करा", - "rowClear": "सामग्री साफ करा", - "slashPlaceHolder": "'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा", - "typeSomething": "काहीतरी लिहा...", - "toggleListShortForm": "टॉगल", - "quoteListShortForm": "कोट", - "mathEquationShortForm": "सूत्र", - "codeBlockShortForm": "कोड" -}, - "favorite": { - "noFavorite": "कोणतेही आवडते पृष्ठ नाही", - "noFavoriteHintText": "पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा", - "removeFromSidebar": "साइडबारमधून काढा", - "addToSidebar": "साइडबारमध्ये पिन करा" -}, -"cardDetails": { - "notesPlaceholder": "/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा" -}, -"blockPlaceholders": { - "todoList": "करण्याची यादी", - "bulletList": "यादी", - "numberList": "क्रमांकित यादी", - "quote": "कोट", - "heading": "मथळा {}" -}, -"titleBar": { - "pageIcon": "पृष्ठ चिन्ह", - "language": "भाषा", - "font": "फॉन्ट", - "actions": "क्रिया", - "date": "तारीख", - "addField": "फील्ड जोडा", - "userIcon": "वापरकर्त्याचे चिन्ह" -}, -"noLogFiles": "कोणतीही लॉग फाइल्स नाहीत", -"newSettings": { - "myAccount": { - "title": "माझे खाते", - "subtitle": "तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.", - "profileLabel": "खाते नाव आणि प्रोफाइल चित्र", - "profileNamePlaceholder": "तुमचे नाव प्रविष्ट करा", - "accountSecurity": "खाते सुरक्षा", - "2FA": "2-स्टेप प्रमाणीकरण", - "aiKeys": "AI कीज", - "accountLogin": "खाते लॉगिन", - "updateNameError": "नाव अपडेट करण्यात अयशस्वी", - "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", - "aboutAppFlowy": "@:appName विषयी", - "deleteAccount": { - "title": "खाते हटवा", - "subtitle": "तुमचे खाते आणि सर्व डेटा कायमचे हटवा.", - "description": "तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.", - "deleteMyAccount": "माझे खाते हटवा", - "dialogTitle": "खाते हटवा", - "dialogContent1": "तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?", - "dialogContent2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.", - "confirmHint1": "कृपया पुष्टीसाठी \"@:newSettings.myAccount.deleteAccount.confirmHint3\" टाइप करा.", - "confirmHint2": "मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.", - "confirmHint3": "DELETE MY ACCOUNT", - "checkToConfirmError": "हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे", - "failedToGetCurrentUser": "वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी", - "confirmTextValidationFailed": "तुमचा पुष्टी मजकूर \"@:newSettings.myAccount.deleteAccount.confirmHint3\" शी जुळत नाही", - "deleteAccountSuccess": "खाते यशस्वीरित्या हटवले गेले" - } - }, - "workplace": { - "name": "वर्कस्पेस", - "title": "वर्कस्पेस सेटिंग्स", - "subtitle": "तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.", - "workplaceName": "वर्कस्पेसचे नाव", - "workplaceNamePlaceholder": "वर्कस्पेसचे नाव टाका", - "workplaceIcon": "वर्कस्पेस चिन्ह", - "workplaceIconSubtitle": "एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.", - "renameError": "वर्कस्पेसचे नाव बदलण्यात अयशस्वी", - "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", - "chooseAnIcon": "चिन्ह निवडा", - "appearance": { - "name": "दृश्यरूप", - "themeMode": { - "auto": "स्वयंचलित", - "light": "प्रकाश मोड", - "dark": "गडद मोड" - }, - "language": "भाषा" - } - }, - "syncState": { - "syncing": "सिंक्रोनायझ करत आहे", - "synced": "सिंक्रोनायझ झाले", - "noNetworkConnected": "नेटवर्क कनेक्ट केलेले नाही" - } -}, - "pageStyle": { - "title": "पृष्ठ शैली", - "layout": "लेआउट", - "coverImage": "मुखपृष्ठ प्रतिमा", - "pageIcon": "पृष्ठ चिन्ह", - "colors": "रंग", - "gradient": "ग्रेडियंट", - "backgroundImage": "पार्श्वभूमी प्रतिमा", - "presets": "पूर्वनियोजित", - "photo": "फोटो", - "unsplash": "Unsplash", - "pageCover": "पृष्ठ कव्हर", - "none": "काही नाही", - "openSettings": "सेटिंग्स उघडा", - "photoPermissionTitle": "@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे", - "photoPermissionDescription": "तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे", - "cameraPermissionTitle": "@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे", - "cameraPermissionDescription": "कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे", - "doNotAllow": "परवानगी देऊ नका", - "image": "प्रतिमा" -}, -"commandPalette": { - "placeholder": "शोधा किंवा प्रश्न विचारा...", - "bestMatches": "सर्वोत्तम जुळवणी", - "recentHistory": "अलीकडील इतिहास", - "navigateHint": "नेव्हिगेट करण्यासाठी", - "loadingTooltip": "आम्ही निकाल शोधत आहोत...", - "betaLabel": "बेटा", - "betaTooltip": "सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो", - "fromTrashHint": "कचरापेटीतून", - "noResultsHint": "आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.", - "clearSearchTooltip": "शोध फील्ड साफ करा" -}, -"space": { - "delete": "हटवा", - "deleteConfirmation": "हटवा: ", - "deleteConfirmationDescription": "या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.", - "rename": "स्पेसचे नाव बदला", - "changeIcon": "चिन्ह बदला", - "manage": "स्पेस व्यवस्थापित करा", - "addNewSpace": "स्पेस तयार करा", - "collapseAllSubPages": "सर्व उपपृष्ठे संकुचित करा", - "createNewSpace": "नवीन स्पेस तयार करा", - "createSpaceDescription": "तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.", - "spaceName": "स्पेसचे नाव", - "spaceNamePlaceholder": "उदा. मार्केटिंग, अभियांत्रिकी, HR", - "permission": "स्पेस परवानगी", - "publicPermission": "सार्वजनिक", - "publicPermissionDescription": "पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य", - "privatePermission": "खाजगी", - "privatePermissionDescription": "फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे", - "spaceIconBackground": "पार्श्वभूमीचा रंग", - "spaceIcon": "चिन्ह", - "dangerZone": "धोकादायक क्षेत्र", - "unableToDeleteLastSpace": "शेवटची स्पेस हटवता येणार नाही", - "unableToDeleteSpaceNotCreatedByYou": "तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही", - "enableSpacesForYourWorkspace": "तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा", - "title": "स्पेसेस", - "defaultSpaceName": "सामान्य", - "upgradeSpaceTitle": "स्पेस सक्षम करा", - "upgradeSpaceDescription": "तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.", - "upgrade": "अपग्रेड", - "upgradeYourSpace": "अनेक स्पेस तयार करा", - "quicklySwitch": "पुढील स्पेसवर पटकन स्विच करा", - "duplicate": "स्पेस डुप्लिकेट करा", - "movePageToSpace": "पृष्ठ स्पेसमध्ये हलवा", - "cannotMovePageToDatabase": "पृष्ठ डेटाबेसमध्ये हलवता येणार नाही", - "switchSpace": "स्पेस स्विच करा", - "spaceNameCannotBeEmpty": "स्पेसचे नाव रिकामे असू शकत नाही", - "success": { - "deleteSpace": "स्पेस यशस्वीरित्या हटवली", - "renameSpace": "स्पेसचे नाव यशस्वीरित्या बदलले", - "duplicateSpace": "स्पेस यशस्वीरित्या डुप्लिकेट केली", - "updateSpace": "स्पेस यशस्वीरित्या अपडेट केली" - }, - "error": { - "deleteSpace": "स्पेस हटवण्यात अयशस्वी", - "renameSpace": "स्पेसचे नाव बदलण्यात अयशस्वी", - "duplicateSpace": "स्पेस डुप्लिकेट करण्यात अयशस्वी", - "updateSpace": "स्पेस अपडेट करण्यात अयशस्वी" - }, - "createSpace": "स्पेस तयार करा", - "manageSpace": "स्पेस व्यवस्थापित करा", - "renameSpace": "स्पेसचे नाव बदला", - "mSpaceIconColor": "स्पेस चिन्हाचा रंग", - "mSpaceIcon": "स्पेस चिन्ह" -}, - "publish": { - "hasNotBeenPublished": "हे पृष्ठ अजून प्रकाशित केलेले नाही", - "spaceHasNotBeenPublished": "स्पेस प्रकाशित करण्यासाठी समर्थन नाही", - "reportPage": "पृष्ठाची तक्रार करा", - "databaseHasNotBeenPublished": "डेटाबेस प्रकाशित करण्यास समर्थन नाही.", - "createdWith": "यांनी तयार केले", - "downloadApp": "AppFlowy डाउनलोड करा", - "copy": { - "codeBlock": "कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे", - "imageBlock": "प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "mathBlock": "गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे", - "fileBlock": "फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे" - }, - "containsPublishedPage": "या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?", - "publishSuccessfully": "यशस्वीरित्या प्रकाशित झाले", - "unpublishSuccessfully": "यशस्वीरित्या अनपब्लिश झाले", - "publishFailed": "प्रकाशित करण्यात अयशस्वी", - "unpublishFailed": "अनपब्लिश करण्यात अयशस्वी", - "noAccessToVisit": "या पृष्ठावर प्रवेश नाही...", - "createWithAppFlowy": "AppFlowy ने वेबसाइट तयार करा", - "fastWithAI": "AI सह जलद आणि सोपे.", - "tryItNow": "आत्ताच वापरून पहा", - "onlyGridViewCanBePublished": "फक्त Grid view प्रकाशित केला जाऊ शकतो", - "database": { - "zero": "{} निवडलेले दृश्य प्रकाशित करा", - "one": "{} निवडलेली दृश्ये प्रकाशित करा", - "many": "{} निवडलेली दृश्ये प्रकाशित करा", - "other": "{} निवडलेली दृश्ये प्रकाशित करा" - }, - "mustSelectPrimaryDatabase": "प्राथमिक दृश्य निवडणे आवश्यक आहे", - "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.", - "unableToDeselectPrimaryDatabase": "प्राथमिक डेटाबेस अननिवड करता येणार नाही", - "saveThisPage": "या टेम्पलेटपासून सुरू करा", - "duplicateTitle": "तुम्हाला हे कुठे जोडायचे आहे", - "selectWorkspace": "वर्कस्पेस निवडा", - "addTo": "मध्ये जोडा", - "duplicateSuccessfully": "तुमच्या वर्कस्पेसमध्ये जोडले गेले", - "duplicateSuccessfullyDescription": "AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.", - "downloadIt": "डाउनलोड करा", - "openApp": "अ‍ॅपमध्ये उघडा", - "duplicateFailed": "डुप्लिकेट करण्यात अयशस्वी", - "membersCount": { - "zero": "सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "useThisTemplate": "हा टेम्पलेट वापरा" -}, -"web": { - "continue": "पुढे जा", - "or": "किंवा", - "continueWithGoogle": "Google सह पुढे जा", - "continueWithGithub": "GitHub सह पुढे जा", - "continueWithDiscord": "Discord सह पुढे जा", - "continueWithApple": "Apple सह पुढे जा", - "moreOptions": "अधिक पर्याय", - "collapse": "आकुंचन", - "signInAgreement": "\"पुढे जा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", - "signInLocalAgreement": "\"सुरुवात करा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", - "and": "आणि", - "termOfUse": "वापर अटी", - "privacyPolicy": "गोपनीयता धोरण", - "signInError": "साइन इन त्रुटी", - "login": "साइन अप किंवा लॉग इन करा", - "fileBlock": { - "uploadedAt": "{time} रोजी अपलोड केले", - "linkedAt": "{time} रोजी लिंक जोडली", - "empty": "फाईल अपलोड करा किंवा एम्बेड करा", - "uploadFailed": "अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "retry": "पुन्हा प्रयत्न करा" - }, - "importNotion": "Notion वरून आयात करा", - "import": "आयात करा", - "importSuccess": "यशस्वीरित्या अपलोड केले", - "importSuccessMessage": "आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.", - "importFailed": "आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा", - "dropNotionFile": "तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा", - "error": { - "pageNameIsEmpty": "पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा" - } -}, - "globalComment": { - "comments": "टिप्पण्या", - "addComment": "टिप्पणी जोडा", - "reactedBy": "यांनी प्रतिक्रिया दिली", - "addReaction": "प्रतिक्रिया जोडा", - "reactedByMore": "आणि {count} इतर", - "showSeconds": { - "one": "1 सेकंदापूर्वी", - "other": "{count} सेकंदांपूर्वी", - "zero": "आत्ताच", - "many": "{count} सेकंदांपूर्वी" - }, - "showMinutes": { - "one": "1 मिनिटापूर्वी", - "other": "{count} मिनिटांपूर्वी", - "many": "{count} मिनिटांपूर्वी" - }, - "showHours": { - "one": "1 तासापूर्वी", - "other": "{count} तासांपूर्वी", - "many": "{count} तासांपूर्वी" - }, - "showDays": { - "one": "1 दिवसापूर्वी", - "other": "{count} दिवसांपूर्वी", - "many": "{count} दिवसांपूर्वी" - }, - "showMonths": { - "one": "1 महिन्यापूर्वी", - "other": "{count} महिन्यांपूर्वी", - "many": "{count} महिन्यांपूर्वी" - }, - "showYears": { - "one": "1 वर्षापूर्वी", - "other": "{count} वर्षांपूर्वी", - "many": "{count} वर्षांपूर्वी" - }, - "reply": "उत्तर द्या", - "deleteComment": "टिप्पणी हटवा", - "youAreNotOwner": "तुम्ही या टिप्पणीचे मालक नाही", - "confirmDeleteDescription": "तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?", - "hasBeenDeleted": "हटवले गेले", - "replyingTo": "याला उत्तर देत आहे", - "noAccessDeleteComment": "तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही", - "collapse": "संकुचित करा", - "readMore": "अधिक वाचा", - "failedToAddComment": "टिप्पणी जोडण्यात अयशस्वी", - "commentAddedSuccessfully": "टिप्पणी यशस्वीरित्या जोडली गेली.", - "commentAddedSuccessTip": "तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?" -}, - "template": { - "asTemplate": "टेम्पलेट म्हणून जतन करा", - "name": "टेम्पलेट नाव", - "description": "टेम्पलेट वर्णन", - "about": "टेम्पलेट माहिती", - "deleteFromTemplate": "टेम्पलेटमधून हटवा", - "preview": "टेम्पलेट पूर्वदृश्य", - "categories": "टेम्पलेट श्रेणी", - "isNewTemplate": "नवीन टेम्पलेटमध्ये पिन करा", - "featured": "वैशिष्ट्यीकृतमध्ये पिन करा", - "relatedTemplates": "संबंधित टेम्पलेट्स", - "requiredField": "{field} आवश्यक आहे", - "addCategory": "\"{category}\" जोडा", - "addNewCategory": "नवीन श्रेणी जोडा", - "addNewCreator": "नवीन निर्माता जोडा", - "deleteCategory": "श्रेणी हटवा", - "editCategory": "श्रेणी संपादित करा", - "editCreator": "निर्माता संपादित करा", - "category": { - "name": "श्रेणीचे नाव", - "icon": "श्रेणी चिन्ह", - "bgColor": "श्रेणी पार्श्वभूमीचा रंग", - "priority": "श्रेणी प्राधान्य", - "desc": "श्रेणीचे वर्णन", - "type": "श्रेणी प्रकार", - "icons": "श्रेणी चिन्हे", - "colors": "श्रेणी रंग", - "byUseCase": "वापराच्या आधारे", - "byFeature": "वैशिष्ट्यांनुसार", - "deleteCategory": "श्रेणी हटवा", - "deleteCategoryDescription": "तुम्हाला ही श्रेणी हटवायची आहे का?", - "typeToSearch": "श्रेणी शोधण्यासाठी टाइप करा..." - }, - "creator": { - "label": "टेम्पलेट निर्माता", - "name": "निर्मात्याचे नाव", - "avatar": "निर्मात्याचा अवतार", - "accountLinks": "निर्मात्याचे खाते दुवे", - "uploadAvatar": "अवतार अपलोड करण्यासाठी क्लिक करा", - "deleteCreator": "निर्माता हटवा", - "deleteCreatorDescription": "तुम्हाला हा निर्माता हटवायचा आहे का?", - "typeToSearch": "निर्माते शोधण्यासाठी टाइप करा..." - }, - "uploadSuccess": "टेम्पलेट यशस्वीरित्या अपलोड झाले", - "uploadSuccessDescription": "तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.", - "viewTemplate": "टेम्पलेट पहा", - "deleteTemplate": "टेम्पलेट हटवा", - "deleteSuccess": "टेम्पलेट यशस्वीरित्या हटवले गेले", - "deleteTemplateDescription": "याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?", - "addRelatedTemplate": "संबंधित टेम्पलेट जोडा", - "removeRelatedTemplate": "संबंधित टेम्पलेट हटवा", - "uploadAvatar": "अवतार अपलोड करा", - "searchInCategory": "{category} मध्ये शोधा", - "label": "टेम्पलेट्स" -}, - "fileDropzone": { - "dropFile": "फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा", - "uploading": "अपलोड करत आहे...", - "uploadFailed": "अपलोड अयशस्वी", - "uploadSuccess": "अपलोड यशस्वी", - "uploadSuccessDescription": "फाइल यशस्वीरित्या अपलोड झाली आहे", - "uploadFailedDescription": "फाइल अपलोड अयशस्वी झाली आहे", - "uploadingDescription": "फाइल अपलोड होत आहे" -}, - "gallery": { - "preview": "पूर्ण स्क्रीनमध्ये उघडा", - "copy": "कॉपी करा", - "download": "डाउनलोड", - "prev": "मागील", - "next": "पुढील", - "resetZoom": "झूम रिसेट करा", - "zoomIn": "झूम इन", - "zoomOut": "झूम आउट" -}, - "invitation": { - "join": "सामील व्हा", - "on": "वर", - "invitedBy": "यांनी आमंत्रित केले", - "membersCount": { - "zero": "{count} सदस्य", - "one": "{count} सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "tip": "तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.", - "joinWorkspace": "वर्कस्पेसमध्ये सामील व्हा", - "success": "तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात", - "successMessage": "आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.", - "openWorkspace": "AppFlowy उघडा", - "alreadyAccepted": "तुम्ही आधीच आमंत्रण स्वीकारले आहे", - "errorModal": { - "title": "काहीतरी चुकले आहे", - "description": "तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.", - "contactOwner": "मालकाशी संपर्क करा", - "close": "मुख्यपृष्ठावर परत जा", - "changeAccount": "खाते बदला" - } -}, - "requestAccess": { - "title": "या पृष्ठासाठी प्रवेश नाही", - "subtitle": "तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.", - "requestAccess": "प्रवेशाची विनंती करा", - "backToHome": "मुख्यपृष्ठावर परत जा", - "tip": "तुम्ही सध्या म्हणून लॉग इन आहात.", - "mightBe": "कदाचित तुम्हाला दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.", - "successful": "विनंती यशस्वीपणे पाठवली गेली", - "successfulMessage": "मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.", - "requestError": "प्रवेशाची विनंती अयशस्वी", - "repeatRequestError": "तुम्ही यासाठी आधीच विनंती केली आहे" -}, - "approveAccess": { - "title": "वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा", - "requestSummary": " यांनी मध्ये सामील होण्यासाठी आणि पाहण्यासाठी विनंती केली आहे", - "upgrade": "अपग्रेड", - "downloadApp": "AppFlowy डाउनलोड करा", - "approveButton": "मंजूर करा", - "approveSuccess": "मंजूर यशस्वी", - "approveError": "मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा", - "getRequestInfoError": "विनंतीची माहिती मिळवण्यात अयशस्वी", - "memberCount": { - "zero": "कोणतेही सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "alreadyProTitle": "वर्कस्पेस योजना मर्यादा गाठली आहे", - "alreadyProMessage": "अधिक सदस्यांसाठी शी संपर्क साधा", - "repeatApproveError": "तुम्ही ही विनंती आधीच मंजूर केली आहे", - "ensurePlanLimit": "कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना करा किंवा करा.", - "requestToJoin": "मध्ये सामील होण्यासाठी विनंती केली", - "asMember": "सदस्य म्हणून" -}, - "upgradePlanModal": { - "title": "Pro प्लॅनवर अपग्रेड करा", - "message": "{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.", - "upgradeSteps": "AppFlowy वर तुमची योजना कशी अपग्रेड करावी:", - "step1": "1. सेटिंग्जमध्ये जा", - "step2": "2. 'योजना' वर क्लिक करा", - "step3": "3. 'योजना बदला' निवडा", - "appNote": "नोंद:", - "actionButton": "अपग्रेड करा", - "downloadLink": "अ‍ॅप डाउनलोड करा", - "laterButton": "नंतर", - "refreshNote": "यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी वर क्लिक करा.", - "refresh": "येथे" -}, - "breadcrumbs": { - "label": "ब्रेडक्रम्स" -}, - "time": { - "justNow": "आत्ताच", - "seconds": { - "one": "1 सेकंद", - "other": "{count} सेकंद" - }, - "minutes": { - "one": "1 मिनिट", - "other": "{count} मिनिटे" - }, - "hours": { - "one": "1 तास", - "other": "{count} तास" - }, - "days": { - "one": "1 दिवस", - "other": "{count} दिवस" - }, - "weeks": { - "one": "1 आठवडा", - "other": "{count} आठवडे" - }, - "months": { - "one": "1 महिना", - "other": "{count} महिने" - }, - "years": { - "one": "1 वर्ष", - "other": "{count} वर्षे" - }, - "ago": "पूर्वी", - "yesterday": "काल", - "today": "आज" -}, - "members": { - "zero": "सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" -}, - "tabMenu": { - "close": "बंद करा", - "closeDisabledHint": "पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा", - "closeOthers": "इतर टॅब बंद करा", - "closeOthersHint": "हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता", - "closeOthersDisabledHint": "सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत", - "favorite": "आवडते", - "unfavorite": "आवडते काढा", - "favoriteDisabledHint": "हे दृश्य आवडते म्हणून जतन करता येत नाही", - "pinTab": "पिन करा", - "unpinTab": "अनपिन करा" -}, - "openFileMessage": { - "success": "फाइल यशस्वीरित्या उघडली", - "fileNotFound": "फाइल सापडली नाही", - "noAppToOpenFile": "ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही", - "permissionDenied": "ही फाइल उघडण्यासाठी परवानगी नाही", - "unknownError": "फाइल उघडण्यात अयशस्वी" -}, - "inviteMember": { - "requestInviteMembers": "तुमच्या वर्कस्पेसमध्ये आमंत्रित करा", - "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, कृपया ", - "upgrade": "अपग्रेड करा", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "आमंत्रण पाठवा", - "inviteAlready": "तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}", - "inviteSuccess": "आमंत्रण यशस्वीपणे पाठवले", - "description": "खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.", - "emails": "ईमेल" -}, - "quickNote": { - "label": "झटपट नोंद", - "quickNotes": "झटपट नोंदी", - "search": "झटपट नोंदी शोधा", - "collapseFullView": "पूर्ण दृश्य लपवा", - "expandFullView": "पूर्ण दृश्य उघडा", - "createFailed": "झटपट नोंद तयार करण्यात अयशस्वी", - "quickNotesEmpty": "कोणत्याही झटपट नोंदी नाहीत", - "emptyNote": "रिकामी नोंद", - "deleteNotePrompt": "निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?", - "addNote": "नवीन नोंद", - "noAdditionalText": "अधिक माहिती नाही" -}, - "subscribe": { - "upgradePlanTitle": "योजना तुलना करा आणि निवडा", - "yearly": "वार्षिक", - "save": "{discount}% बचत", - "monthly": "मासिक", - "priceIn": "किंमत येथे: ", - "free": "फ्री", - "pro": "प्रो", - "freeDescription": "2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी", - "proDescription": "प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी", - "proDuration": { - "monthly": "प्रति सदस्य प्रति महिना\nमासिक बिलिंग", - "yearly": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग" - }, - "cancel": "खालच्या योजनेवर जा", - "changePlan": "प्रो योजनेवर अपग्रेड करा", - "everythingInFree": "फ्री योजनेतील सर्व काही +", - "currentPlan": "सध्याची योजना", - "freeDuration": "कायम", - "freePoints": { - "first": "1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)", - "second": "अमर्यादित पृष्ठे आणि ब्लॉक्स", - "three": "5 GB संचयन", - "four": "बुद्धिमान शोध", - "five": "20 AI प्रतिसाद", - "six": "मोबाईल अ‍ॅप", - "seven": "रिअल-टाइम सहकार्य" - }, - "proPoints": { - "first": "अमर्यादित संचयन", - "second": "10 वर्कस्पेस सदस्यांपर्यंत", - "three": "अमर्यादित AI प्रतिसाद", - "four": "अमर्यादित फाइल अपलोड्स", - "five": "कस्टम नेमस्पेस" - }, - "cancelPlan": { - "title": "आपल्याला जाताना पाहून वाईट वाटते", - "success": "आपली सदस्यता यशस्वीरित्या रद्द झाली आहे", - "description": "आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.", - "commonOther": "इतर", - "otherHint": "आपले उत्तर येथे लिहा", - "questionOne": { - "question": "तुम्ही AppFlowy Pro सदस्यता का रद्द केली?", - "answerOne": "खर्च खूप जास्त आहे", - "answerTwo": "वैशिष्ट्ये अपेक्षेनुसार नव्हती", - "answerThree": "चांगला पर्याय सापडला", - "answerFour": "खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता", - "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" - }, - "questionTwo": { - "question": "तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?", - "answerOne": "खूप शक्यता आहे", - "answerTwo": "काहीशी शक्यता आहे", - "answerThree": "निश्चित नाही", - "answerFour": "अल्प शक्यता आहे", - "answerFive": "शक्यता नाही" - }, - "questionThree": { - "question": "सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?", - "answerOne": "मल्टी-यूजर सहकार्य", - "answerTwo": "लांब कालावधीसाठी आवृत्ती इतिहास", - "answerThree": "अमर्यादित AI प्रतिसाद", - "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" - }, - "questionFour": { - "question": "AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?", - "answerOne": "छान", - "answerTwo": "चांगला", - "answerThree": "सामान्य", - "answerFour": "थोडासा वाईट", - "answerFive": "असंतोषजनक" - } - } -}, - "ai": { - "contentPolicyViolation": "संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.", - "textLimitReachedDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.", - "imageLimitReachedDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.", - "limitReachedAction": { - "textDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया", - "imageDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया", - "upgrade": "अपग्रेड करा", - "toThe": "या योजनेवर", - "proPlan": "प्रो योजना", - "orPurchaseAn": "किंवा खरेदी करा", - "aiAddon": "AI अ‍ॅड-ऑन" - }, - "editing": "संपादन करत आहे", - "analyzing": "विश्लेषण करत आहे", - "continueWritingEmptyDocumentTitle": "लेखन सुरू ठेवता आले नाही", - "continueWritingEmptyDocumentDescription": "तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!", - "more": "अधिक" -}, - "autoUpdate": { - "criticalUpdateTitle": "अद्यतन आवश्यक आहे", - "criticalUpdateDescription": "तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.", - "criticalUpdateButton": "अद्यतन करा", - "bannerUpdateTitle": "नवीन आवृत्ती उपलब्ध!", - "bannerUpdateDescription": "नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.", - "bannerUpdateButton": "अद्यतन करा", - "settingsUpdateTitle": "नवीन आवृत्ती ({newVersion}) उपलब्ध!", - "settingsUpdateDescription": "सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}", - "settingsUpdateButton": "अद्यतन करा", - "settingsUpdateWhatsNew": "काय नवीन आहे" -}, - "lockPage": { - "lockPage": "लॉक केलेले", - "reLockPage": "पुन्हा लॉक करा", - "lockTooltip": "अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.", - "pageLockedToast": "पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.", - "lockedOperationTooltip": "पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे." -}, - "suggestion": { - "accept": "स्वीकारा", - "keep": "जसे आहे तसे ठेवा", - "discard": "रद्द करा", - "close": "बंद करा", - "tryAgain": "पुन्हा प्रयत्न करा", - "rewrite": "पुन्हा लिहा", - "insertBelow": "खाली टाका" -} -} diff --git a/frontend/appflowy_flutter/build.yaml b/frontend/appflowy_flutter/build.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/appflowy_flutter/dart_dependency_validator.yaml b/frontend/appflowy_flutter/dart_dependency_validator.yaml deleted file mode 100644 index cb1df68bb6..0000000000 --- a/frontend/appflowy_flutter/dart_dependency_validator.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# dart_dependency_validator.yaml - -allow_pins: true - -include: - - "lib/**" - -exclude: - - "packages/**" - -ignore: - - analyzer diff --git a/frontend/appflowy_flutter/distribute_options.yaml b/frontend/appflowy_flutter/distribute_options.yaml deleted file mode 100644 index 60f603a938..0000000000 --- a/frontend/appflowy_flutter/distribute_options.yaml +++ /dev/null @@ -1,12 +0,0 @@ -output: dist/ -releases: - - name: dev - jobs: - - name: release-dev-linux-deb - package: - platform: linux - target: deb - - name: release-dev-linux-rpm - package: - platform: linux - target: rpm diff --git a/frontend/appflowy_flutter/dsa_pub.pem b/frontend/appflowy_flutter/dsa_pub.pem deleted file mode 100644 index 6a9d213b8a..0000000000 --- a/frontend/appflowy_flutter/dsa_pub.pem +++ /dev/null @@ -1,36 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIGQzCCBDUGByqGSM44BAEwggQoAoICAQDlkozRmUnVH1MJFqOamAmUYu0YruaT -rrt6rCIZ0LFrfNnmHA4LOQEcXwBTTyn5sBmkPq+lb/rjmERKhmvl1rfo6q7tJ8mG -4TWqSu0tOJQ6QxexnNW4yhzK/r9MS5MQus4Al+y2hQLaAMOUIOnaWIrC9OHy7xyw -+sVipECVKyQqipS4shGUSqbcN+ocQuTB+I0MtIjBii0DGSEY3pxQrfNWjHFL7iTV -KiTn3YOWPJQvv3FvEDrN+5xU5JZpD97ZhXaJpLUyOQaNvcPaOELPWcOSJwqHOpf5 -b5N/VZ8SGbHNdxy9d5sSChBgtuAOihEhSp6SjFQ9eVHOf4NyJwSEMmi0gpdpqm4Z -QRJUnM2zIi0p9twR9FRYXzrxOs6yGCQEY+xFG93ShTLTj3zMrIyFqBsqEwFyJiJW -YWe/zp0V7UlLP+jXO9u9eghNmly7QVqD2P0qs/1V0jZFRuLWpsv4inau/qMZ5EhG -G4xCJZXfN1pkehy6e05/h+vs5anK3Wa/H8AtY6cK4CpzAanELvn3AH7VLbAhLswu -6d5CV+DoFgxCWMzGBSdmCYU+2wRLaL8Q9TZHDR+pvQlunEFdfFoGES9WjBPhAsVA -6Mq22U8XSje9yHI3X9Eqe/7a+ajSgcGmB7oQ11+4xf5h2PtubRW/JL0KMjxCxMTp -q1md6Ndx/ptBUwIdAIOyiKb2YcTLWAOt+LAlRXMsY1+W4pTXJfV6RcMCggIAPxbd -0HNj2O/aQhJxNZDMBIcx6+cZ+LKch7qLcaEpVqWHvDSnR2eOJJzWn0RoKK+Vuix/ -4T8texSQkWxAeFFdo6kyrR9XNL7hqEFFq8o9VpmvRzvG6h/bBgh3AHAQE3p/8Wrb -K13IhnlWqd0MjFufSphm63o0gaWl95j+6KeUoKQnioetu9HiMtFKx0d/KYqTQJg7 -hvR6VNCU2oShfXR3ce7RnUYwD37+djrUjUkoAZkZq2KoxBiKyeoSIeqAme19tKcO -s6b17mhALELuJ+NtDwlDunyiCDUYX9lTPijHwKeIFtBs38+OtRk3aIqmWTQdbsCz -Axp+kUMA5ESBME/RBNCSPHuDvtA3wfWvNbA5DXfZLwCgNSxhekq8XntIsRzfJ4v4 -uPzKFcVM3+sUUfSF04HHC9ol+PpLqXUyMnskiizqxFPq7H+6tyFZ7X2HiG6TjcfV -Wthmv+JyfcABjVnk2qFH7GagENbdtYmfUox13LhE59Sh5chaJnCFtCDp8NClWgZn -ixCOFQ9EgTLaH6MovTvWpEgG2MfBCu5SMUHi2qSflorqpRFH+rA7NZSnyz3wm7NB -+fJSOP0IjEkOh7MafU6Z61oK9WY/Fc+F1zIENVv8PUc3p75y/4RAp4xzyKcTilaN -C9U/3MRr3QmWwY7ejtZx6xdOxsvWBRDRSNbDdIkDggIGAAKCAgEAt1DHYZoeXY0r -vYXmxdNO6zfnbz1GGZHXpakzm9h4BrxPDP5J8DQ9ZeVVKg5+cU9AyMO3cZHp7wkx -k6IB+ZDUpqO1D3lWriRl2fI8cS4edI0fzpnW1nyhhFD4MbKmP+v27aH+DhZ4Up3y -GMmJTLmKiYx1EgZp7Sx77PBYDVMsKKd3h9+Hjp2YtUTfD2lleAmC+wcQGZiNtGw/ -eKpsmUVnWrepOdntWTtCQi1OvfcHaF2QmgktCq+68hbDNYWaXmzVIiQqrdv/zzOG -hCFIrRGWemrxL0iFG4Pzc4UfOINsISQLcUxRuF6pQWPxF8O/mWKfzAeqWxmIujUM -EoSEuI3yQ8VjlYpW/8FSK7UhgnHBHOpCJWPWs/vQXAnaUR2PYyzuIzhVEhFs8YA8 -iBIKnixIC2hu0YbEk3TBr/TRcbd7mDw9Mq7NT88xzdU13+Wh+4zhdX3rtBHYzBtI -7GaONGUNyY4h0duoyLpH6dxevaeKN6/bEdzYESjoE58QA88CpnAZGhJVphAba4cb -w6GTDhK3RlPWh6hRqJwLDILGtnJS3UKeBDRmKMqNuqmHqPjyAAvt9JBO8lzjoLgf -1cDsXHNWBVwA2jsX2CukNJPlY1Fa3MWhdaUXmy6QGMSisr1sptvBt1Phry8T2u+P -Y29SB4jvwqls268rP0cWqy4WXwlVwuc= ------END PUBLIC KEY----- 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 new file mode 100644 index 0000000000..0c8b96fa20 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -0,0 +1,92 @@ +// ignore_for_file: unused_import + +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'; +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/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'; +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_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:intl/intl.dart'; +import 'package:path/path.dart' as p; + +import '../desktop/board/board_hide_groups_test.dart'; +import '../shared/dir.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('appflowy cloud', () { + testWidgets('anon user and then sign in', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + + tester.expectToSeeText(LocaleKeys.signIn_loginStartWithAnonymous.tr()); + await tester.tapAnonymousSignInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + // reanme the name of the anon user + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + await tester.pumpAndSettle(); + + await tester.enterUserName('local_user'); + + // Scroll to sign-in + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + + await tester.tapButton(find.byType(SignInOutButton)); + + // sign up with Google + await tester.tapGoogleLoginInButton(); + + // sign out + await tester.expectToSeeHomePage(); + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + + // Scroll to sign-out + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + + await tester.logout(); + await tester.pumpAndSettle(); + + // tap the continue as anonymous button + await tester + .tapButton(find.text(LocaleKeys.signIn_loginStartWithAnonymous.tr())); + await tester.expectToSeeHomePage(); + + // New anon user name + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + final userNameInput = + tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; + 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 new file mode 100644 index 0000000000..5aa3a02d83 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart @@ -0,0 +1,101 @@ +// ignore_for_file: unused_import + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.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/workspace/application/settings/prelude.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'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('appflowy cloud auth', () { + testWidgets('sign in', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + }); + + testWidgets('sign out', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + await tester.tapGoogleLoginInButton(); + + // Open the setting page and sign out + 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.expectToSeeText(LocaleKeys.button_confirm.tr()); + await tester.tapButtonWithName(LocaleKeys.button_confirm.tr()); + + // Go to the sign in page again + await tester.pumpAndSettle(const Duration(seconds: 1)); + tester.expectToSeeGoogleLoginButton(); + }); + + testWidgets('sign in as anonymous', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + 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-in + await tester.scrollUntilVisible( + find.byType(SignInOutButton), + 100, + scrollable: find.findSettingsScrollable(), + ); + await tester.tapButton(find.byType(SignInOutButton)); + + tester.expectToSeeGoogleLoginButton(); + }); + + testWidgets('enable sync', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + + 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.assertAppFlowyCloudEnableSyncSwitchValue(true); + await tester.toggleEnableSync(AppFlowyCloudEnableSync); + + // the switch should be off + tester.assertAppFlowyCloudEnableSyncSwitchValue(false); + + // the switch should be on after toggling + await tester.toggleEnableSync(AppFlowyCloudEnableSync); + tester.assertAppFlowyCloudEnableSyncSwitchValue(true); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart new file mode 100644 index 0000000000..3a72f012d6 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart @@ -0,0 +1,19 @@ +import 'anon_user_continue_test.dart' as anon_user_continue_test; +import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; +import 'empty_test.dart' as preset_af_cloud_env_test; +import 'user_setting_sync_test.dart' as user_sync_test; +import 'workspace/change_name_and_icon_test.dart' + as change_workspace_name_and_icon_test; +import 'workspace/collaborative_workspace_test.dart' + as collaboration_workspace_test; + +Future main() async { + preset_af_cloud_env_test.main(); + appflowy_cloud_auth_test.main(); + user_sync_test.main(); + anon_user_continue_test.main(); + + // workspace + collaboration_workspace_test.main(); + change_workspace_name_and_icon_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart new file mode 100644 index 0000000000..c5f2c0d1aa --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/document_sync_test.dart @@ -0,0 +1,71 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.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/workspace/application/settings/prelude.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_backend/protobuf/flowy-folder/view.pb.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'; +import 'package:path/path.dart' as p; + +import '../shared/dir.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final email = '${uuid()}@appflowy.io'; + const inputContent = 'Hello world, this is a test document'; + +// The test will create a new document called Sample, and sync it to the server. +// Then the test will logout the user, and login with the same user. The data will +// be synced from the server. + group('appflowy cloud document', () { + testWidgets('sync local docuemnt to server', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + // create a new document called Sample + await tester.createNewPage(); + + // focus on the editor + await tester.editor.tapLineOfEditorAt(0); + await tester.ime.insertText(inputContent); + expect(find.text(inputContent, findRichText: true), findsOneWidget); + + // TODO(nathan): remove the await + // 6 seconds for data sync + await tester.waitForSeconds(6); + + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + await tester.logout(); + }); + + testWidgets('sync doc from server', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePage(); + + // the latest document will be opened, so the content must be the inputContent + await tester.pumpAndSettle(); + expect(find.text(inputContent, findRichText: true), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart b/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart new file mode 100644 index 0000000000..9f7d3ce9ed --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/empty_test.dart @@ -0,0 +1,18 @@ +import 'package:appflowy/env/cloud_env.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../shared/util.dart'; + +// This test is meaningless, just for preventing the CI from failing. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Empty', () { + testWidgets('set appflowy cloud', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + ); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart new file mode 100644 index 0000000000..15c9c3c347 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart @@ -0,0 +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); + }); + }); +} 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 new file mode 100644 index 0000000000..5791803a0e --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart @@ -0,0 +1,75 @@ +// ignore_for_file: unused_import + +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'; +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/settings_account_view.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +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_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../desktop/board/board_hide_groups_test.dart'; +import '../shared/database_test_op.dart'; +import '../shared/dir.dart'; +import '../shared/emoji.dart'; +import '../shared/mock/mock_file_picker.dart'; +import '../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final email = '${uuid()}@appflowy.io'; + const name = 'nathan'; + + group('appflowy cloud setting', () { + testWidgets('sync user name and icon to server', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + + await tester.enterUserName(name); + await tester.tapEscButton(); + + // 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, + email: email, + ); + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + await tester.pumpAndSettle(); + + await tester.openSettings(); + await tester.openSettingsPage(SettingsPage.account); + + // Verify name + final profileSetting = + tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting; + + expect(profileSetting.name, name); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart similarity index 80% rename from frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart rename to frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart index f205b35354..75e420baac 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/change_name_and_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart @@ -1,14 +1,23 @@ +// ignore_for_file: unused_import + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.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/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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'; +import 'package:path/path.dart' as p; -import '../../../shared/util.dart'; -import '../../../shared/workspace.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; +import '../../shared/workspace.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -40,10 +49,6 @@ void main() { await tester.changeWorkspaceIcon(icon); await tester.changeWorkspaceName(name); - await tester.pumpUntilNotFound( - find.text(LocaleKeys.workspace_renameSuccess.tr()), - ); - workspaceIcon = tester.widget( find.byType(WorkspaceIcon), ); diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart new file mode 100644 index 0000000000..059b609fc7 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -0,0 +1,100 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; +import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/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/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +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:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/database_test_op.dart'; +import '../../shared/dir.dart'; +import '../../shared/emoji.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // final email = '${uuid()}@appflowy.io'; + + group('collaborative workspace', () { + // combine the create and delete workspace test to reduce the time + testWidgets('create a new workspace, open it and then delete it', + (tester) async { + // only run the test when the feature flag is on + // if (!FeatureFlag.collaborativeWorkspace.isOn) { + // return; + // } + + // await tester.initializeAppFlowy( + // cloudType: AuthenticatorType.appflowyCloudSelfHost, + // email: email, + // ); + // await tester.tapGoogleLoginInButton(); + // await tester.expectToSeeHomePageWithGetStartedPage(); + + // const name = 'AppFlowy.IO'; + // // the workspace will be opened after created + // await tester.createCollaborativeWorkspace(name); + + // final loading = find.byType(Loading); + // await tester.pumpUntilNotFound(loading); + + // Finder success; + + // final Finder items = find.byType(WorkspaceMenuItem); + + // // delete the newly created workspace + // await tester.openCollaborativeWorkspaceMenu(); + // await tester.pumpUntilFound(items); + + // expect(items, findsNWidgets(2)); + // expect( + // tester.widget(items.last).workspace.name, + // name, + // ); + + // final secondWorkspace = find.byType(WorkspaceMenuItem).last; + // await tester.hoverOnWidget( + // secondWorkspace, + // onHover: () async { + // // click the more button + // final moreButton = find.byType(WorkspaceMoreActionList); + // expect(moreButton, findsOneWidget); + // await tester.tapButton(moreButton); + // // click the delete button + // final deleteButton = find.text(LocaleKeys.button_delete.tr()); + // expect(deleteButton, findsOneWidget); + // await tester.tapButton(deleteButton); + // // see the delete confirm dialog + // final confirm = + // find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr()); + // expect(confirm, findsOneWidget); + // await tester.tapButton(find.text(LocaleKeys.button_ok.tr())); + // // delete success + // success = find.text(LocaleKeys.workspace_createSuccess.tr()); + // await tester.pumpUntilFound(success); + // expect(success, findsOneWidget); + // await tester.pumpUntilNotFound(success); + // }, + // ); + }); + }); +} 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 bdd0ecdb2c..3fe48b5f6f 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,10 +22,9 @@ void main() { const fieldName = "test change field"; await tester.createField( FieldType.RichText, - name: fieldName, + fieldName, layout: ViewLayoutPB.Board, ); - await tester.dismissRowDetailPage(); await tester.tapButton(card1); await tester.changeFieldTypeOfFieldWithName( fieldName, diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart index 3eedbdb3bf..68848503c4 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_group_test.dart @@ -1,21 +1,17 @@ -import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('board group test:', () { + group('board group test', () { testWidgets('move row to another group', (tester) async { const card1Name = 'Card 1'; await tester.initializeAppFlowy(); @@ -50,105 +46,5 @@ void main() { final card1StatusText = tester.widget(card1StatusFinder).data; expect(card1StatusText, 'Doing'); }); - - testWidgets('rename group', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - final headers = find.byType(BoardColumnHeader); - expect(headers, findsNWidgets(4)); - - // try to tap no status - final noStatus = headers.first; - expect( - find.descendant(of: noStatus, matching: find.text("No Status")), - findsOneWidget, - ); - await tester.tapButton(noStatus); - expect( - find.descendant(of: noStatus, matching: find.byType(TextField)), - findsNothing, - ); - - // tap on To Do and edit it - final todo = headers.at(1); - expect( - find.descendant(of: todo, matching: find.text("To Do")), - findsOneWidget, - ); - await tester.tapButton(todo); - await tester.enterText( - find.descendant(of: todo, matching: find.byType(TextField)), - "tada", - ); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - final newHeaders = find.byType(BoardColumnHeader); - expect(newHeaders, findsNWidgets(4)); - final tada = find.byType(BoardColumnHeader).at(1); - expect( - find.descendant(of: tada, matching: find.byType(TextField)), - findsNothing, - ); - expect( - find.descendant( - of: tada, - matching: find.text("tada"), - ), - findsOneWidget, - ); - }); - - testWidgets('edit select option from row detail', (tester) async { - const card1Name = 'Card 1'; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - - await tester.tapButton( - find.descendant( - of: find.byType(RowCard), - matching: find.text(card1Name), - ), - ); - - await tester.tapGridFieldWithNameInRowDetailPage("Status"); - await tester.tapButton( - find.byWidgetPredicate( - (widget) => - widget is SelectOptionTagCell && widget.option.name == "To Do", - ), - ); - final editor = find.byType(SelectOptionEditor); - await tester.enterText( - find.descendant(of: editor, matching: find.byType(TextField)), - "tada", - ); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester.dismissFieldEditor(); - await tester.dismissRowDetailPage(); - - final newHeaders = find.byType(BoardColumnHeader); - expect(newHeaders, findsNWidgets(4)); - final tada = find.byType(BoardColumnHeader).at(1); - expect( - find.descendant(of: tada, matching: find.byType(TextField)), - findsNothing, - ); - expect( - find.descendant( - of: tada, - matching: find.text("tada"), - ), - findsOneWidget, - ); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart index 6a012ac763..b1f3d0fb45 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_hide_groups_test.dart @@ -1,13 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_hidden_groups.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/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { @@ -23,24 +23,24 @@ void main() { final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s); // Is expanded by default - expect(collapseFinder, findsNothing); - expect(expandFinder, findsOneWidget); - - // Collapse hidden groups - await tester.tap(expandFinder); - await tester.pumpAndSettle(); - - // Is collapsed expect(collapseFinder, findsOneWidget); expect(expandFinder, findsNothing); - // Expand hidden groups + // Collapse hidden groups await tester.tap(collapseFinder); await tester.pumpAndSettle(); - // Is expanded + // Is collapsed expect(collapseFinder, findsNothing); expect(expandFinder, findsOneWidget); + + // Expand hidden groups + await tester.tap(expandFinder); + await tester.pumpAndSettle(); + + // Is expanded + expect(collapseFinder, findsOneWidget); + expect(expandFinder, findsNothing); }); testWidgets('hide first group, and show it again', (tester) async { @@ -48,9 +48,6 @@ void main() { await tester.tapAnonymousSignInButton(); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - final expandFinder = find.byFlowySvg(FlowySvgs.hamburger_s_s); - await tester.tapButton(expandFinder); - // Tap the options of the first group final optionsFinder = find .descendant( @@ -80,46 +77,65 @@ void main() { shownGroups = tester.widgetList(find.byType(BoardColumnHeader)).length; expect(shownGroups, 4); }); + }); - testWidgets('delete a group', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); + testWidgets('delete a group', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4); + expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4); - // tap group option button for the first group. Delete shouldn't show up - await tester.tapButton( - find - .descendant( - of: find.byType(BoardColumnHeader), - matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), - ) - .first, - ); - expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing); + // tap group option button for the first group. Delete shouldn't show up + await tester.tapButton( + find + .descendant( + of: find.byType(BoardColumnHeader), + matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), + ) + .first, + ); + expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing); - // dismiss the popup - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); + // dismiss the popup + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); - // tap group option button for the first group. Delete should show up - await tester.tapButton( - find - .descendant( - of: find.byType(BoardColumnHeader), - matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), - ) - .at(1), - ); - expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget); + // tap group option button for the first group. Delete should show up + await tester.tapButton( + find + .descendant( + of: find.byType(BoardColumnHeader), + matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), + ) + .at(1), + ); + expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget); - // Tap the delete button and confirm - await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s)); - await tester.tapButtonWithName(LocaleKeys.space_delete.tr()); + // Tap the delete button and confirm + await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s)); + await tester.tapDialogOkButton(); - // Expect number of groups to decrease by one - expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3); - }); + // Expect number of groups to decrease by one + expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3); }); } + +extension FlowySvgFinder on CommonFinders { + Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg); +} + +class _FlowySvgFinder extends MatchFinder { + _FlowySvgFinder(this.svg); + + final FlowySvgData svg; + + @override + String get description => 'flowy_svg "$svg"'; + + @override + bool matches(Element candidate) { + final Widget widget = candidate.widget; + return widget is FlowySvg && widget.svg == svg; + } +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart index 868c27d302..393cadbaf5 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_row_test.dart @@ -6,7 +6,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:time/time.dart'; import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; @@ -15,31 +14,6 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('board row test', () { - testWidgets('edit item in ToDo card', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - const name = 'Card 1'; - final card1 = find.ancestor( - matching: find.byType(RowCard), - of: find.text(name), - ); - await tester.hoverOnWidget( - card1, - onHover: () async { - final editCard = find.byType(EditCardAccessory); - await tester.tapButton(editCard); - }, - ); - await tester.showKeyboard(card1); - tester.testTextInput.enterText(""); - await tester.pump(300.milliseconds); - tester.testTextInput.enterText("a"); - await tester.pump(300.milliseconds); - expect(find.text('a'), findsOneWidget); - }); - testWidgets('delete item in ToDo card', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -55,7 +29,7 @@ void main() { }, ); await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); + await tester.tapOKButton(); expect(find.text(name), findsNothing); }); @@ -77,37 +51,6 @@ void main() { expect(find.textContaining(name, findRichText: true), findsNWidgets(2)); }); - testWidgets('duplicate item in ToDo card then delete', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - const name = 'Card 1'; - final card1 = find.text(name); - await tester.hoverOnWidget( - card1, - onHover: () async { - final moreOption = find.byType(MoreCardOptionsAccessory); - await tester.tapButton(moreOption); - }, - ); - await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr()); - expect(find.textContaining(name, findRichText: true), findsNWidgets(2)); - - // get the last widget that contains the name - final duplicatedCard = find.textContaining(name, findRichText: true).last; - await tester.hoverOnWidget( - duplicatedCard, - onHover: () async { - final moreOption = find.byType(MoreCardOptionsAccessory); - await tester.tapButton(moreOption); - }, - ); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - expect(find.textContaining(name, findRichText: true), findsNWidgets(1)); - }); - testWidgets('add new group', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart index 75323a1c80..a786367fff 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart @@ -3,8 +3,6 @@ import 'package:integration_test/integration_test.dart'; import 'board_add_row_test.dart' as board_add_row_test; import 'board_group_test.dart' as board_group_test; import 'board_row_test.dart' as board_row_test; -import 'board_field_test.dart' as board_field_test; -import 'board_hide_groups_test.dart' as board_hide_groups_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -13,6 +11,4 @@ void main() { board_row_test.main(); board_add_row_test.main(); board_group_test.main(); - board_field_test.main(); - board_hide_groups_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart deleted file mode 100644 index a8c05d5f80..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/cloud_runner.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'data_migration/data_migration_test_runner.dart' - as data_migration_test_runner; -import 'database/database_test_runner.dart' as database_test_runner; -import 'document/document_test_runner.dart' as document_test_runner; -import 'set_env.dart' as preset_af_cloud_env_test; -import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test; -import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test; -import 'sidebar/sidebar_rename_untitled_test.dart' - as sidebar_rename_untitled_test; -import 'uncategorized/uncategorized_test_runner.dart' - as uncategorized_test_runner; -import 'workspace/workspace_test_runner.dart' as workspace_test_runner; - -Future main() async { - preset_af_cloud_env_test.main(); - - data_migration_test_runner.main(); - - // uncategorized - uncategorized_test_runner.main(); - - // workspace - workspace_test_runner.main(); - - // document - document_test_runner.main(); - - // sidebar - sidebar_move_page_test.main(); - sidebar_rename_untitled_test.main(); - sidebar_icon_test.main(); - - // database - database_test_runner.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart deleted file mode 100644 index e34ac02aab..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/anon_user_data_migration_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('appflowy cloud', () { - testWidgets('anon user -> sign in -> open imported space', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - - await tester.tapAnonymousSignInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Test Document'; - await tester.createNewPageWithNameUnderParent(name: pageName); - tester.expectToSeePageName(pageName); - - // rename the name of the anon user - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - await tester.pumpAndSettle(); - - await tester.enterUserName('local_user'); - - // Scroll to sign-in - await tester.tapButton(find.byType(AccountSignInOutButton)); - - // sign up with Google - await tester.tapGoogleLoginInButton(); - // await tester.pumpAndSettle(const Duration(seconds: 16)); - - // open the imported space - await tester.expectToSeeHomePage(); - await tester.clickSpaceHeader(); - - // After import the anon user data, we will create a new space for it - await tester.openSpace("Getting started"); - await tester.openPage(pageName); - - await tester.pumpAndSettle(); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/data_migration_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/data_migration_test_runner.dart deleted file mode 100644 index a69c0480ce..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/data_migration/data_migration_test_runner.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'anon_user_data_migration_test.dart' as anon_user_test; - -void main() async { - anon_user_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart deleted file mode 100644 index 5561d40033..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_image_test.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide UploadImageMenu, ResizableImage; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/database_test_op.dart'; -import '../../../shared/mock/mock_file_picker.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // copy link to block - group('database image:', () { - testWidgets('insert image', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open the first row detail page and upload an image - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Grid, - pageName: 'database image', - ); - await tester.openFirstRowDetailPage(); - - // insert an image block - { - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); - } - - // upload an image - { - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final imagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final file = File(imagePath) - ..writeAsBytesSync(image.buffer.asUint8List()); - - mockPickFilePaths( - paths: [imagePath], - ); - - await getIt().set(KVKeys.kCloudType, '0'); - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ); - await tester.pumpAndSettle(); - expect(find.byType(ResizableImage), findsOneWidget); - final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotEmpty); - - // remove the temp file - file.deleteSync(); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart deleted file mode 100644 index 4d1a623f07..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/database/database_test_runner.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'database_image_test.dart' as database_image_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - database_image_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart deleted file mode 100644 index f163608ccb..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_ai_writer_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('AI Writer:', () { - testWidgets('the ai writer transaction should only apply in memory', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_aiWriter.tr(), - ); - expect(find.byType(AiWriterBlockComponent), findsOneWidget); - - // switch to another page - await tester.openPage(Constants.gettingStartedPageName); - // switch back to the page - await tester.openPage(pageName); - - // expect the ai writer block is not in the document - expect(find.byType(AiWriterBlockComponent), findsNothing); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_copy_link_to_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_copy_link_to_block_test.dart deleted file mode 100644 index 24106cf99a..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_copy_link_to_block_test.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/document_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/shared/patterns/common_patterns.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:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // copy link to block - group('copy link to block:', () { - testWidgets('copy link to check if the clipboard has the correct content', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - await tester.editor.copyLinkToBlock([0]); - await tester.pumpAndSettle(Durations.short1); - - // check the clipboard - final content = await Clipboard.getData(Clipboard.kTextPlain); - expect( - content?.text, - matches(appflowySharePageLinkPattern), - ); - }); - - testWidgets('copy link to block(another page) and paste it in doc', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - await tester.editor.copyLinkToBlock([0]); - - // create a new page and paste it - const pageName = 'copy link to block'; - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // paste the link to the new page - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.paste(); - await tester.pumpAndSettle(); - - // check the content of the block - final node = tester.editor.getNodeAtPath([0]); - final delta = node.delta!; - final insert = (delta.first as TextInsert).text; - final attributes = delta.first.attributes; - expect(insert, MentionBlockKeys.mentionChar); - final mention = - attributes?[MentionBlockKeys.mention] as Map; - expect(mention[MentionBlockKeys.type], MentionType.page.name); - expect(mention[MentionBlockKeys.blockId], isNotNull); - expect(mention[MentionBlockKeys.pageId], isNotNull); - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.textContaining( - Constants.gettingStartedPageName, - findRichText: true, - ), - ), - findsOneWidget, - ); - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.textContaining( - // the pasted block content is 'Welcome to AppFlowy' - 'Welcome to AppFlowy', - findRichText: true, - ), - ), - findsOneWidget, - ); - - // tap the mention block to jump to the page - await tester.tapButton(find.byType(MentionPageBlock)); - await tester.pumpAndSettle(); - - // expect to go to the getting started page - final documentPage = find.byType(DocumentPage); - expect(documentPage, findsOneWidget); - expect( - tester.widget(documentPage).view.name, - Constants.gettingStartedPageName, - ); - // and the block is selected - expect( - tester.widget(documentPage).initialBlockId, - mention[MentionBlockKeys.blockId], - ); - expect( - tester.editor.getCurrentEditorState().selection, - Selection.collapsed( - Position( - path: [0], - ), - ), - ); - }); - - testWidgets('copy link to block(same page) and paste it in doc', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // create a new page and paste it - const pageName = 'copy link to block'; - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // copy the link to block from the first line - const inputText = 'Hello World'; - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(inputText); - await tester.ime.insertCharacter('\n'); - await tester.pumpAndSettle(); - await tester.editor.copyLinkToBlock([0]); - - // paste the link to the second line - await tester.editor.tapLineOfEditorAt(1); - await tester.editor.paste(); - await tester.pumpAndSettle(); - - // check the content of the block - final node = tester.editor.getNodeAtPath([1]); - final delta = node.delta!; - final insert = (delta.first as TextInsert).text; - final attributes = delta.first.attributes; - expect(insert, MentionBlockKeys.mentionChar); - final mention = - attributes?[MentionBlockKeys.mention] as Map; - expect(mention[MentionBlockKeys.type], MentionType.page.name); - expect(mention[MentionBlockKeys.blockId], isNotNull); - expect(mention[MentionBlockKeys.pageId], isNotNull); - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.textContaining( - inputText, - findRichText: true, - ), - ), - findsNWidgets(2), - ); - - // edit the pasted block - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('!'); - await tester.pumpAndSettle(); - - // check the content of the block - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.textContaining( - '$inputText!', - findRichText: true, - ), - ), - findsNWidgets(2), - ); - - // tap the mention block - await tester.tapButton(find.byType(MentionPageBlock)); - expect( - tester.editor.getCurrentEditorState().selection, - Selection.collapsed( - Position( - path: [0], - ), - ), - ); - }); - - testWidgets('''1. copy link to block from another page - 2. paste the link to the new page - 3. delete the original page - 4. check the content of the block, it should be no access to the page - ''', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - await tester.editor.copyLinkToBlock([0]); - - // create a new page and paste it - const pageName = 'copy link to block'; - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // paste the link to the new page - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.paste(); - await tester.pumpAndSettle(); - - // tap the mention block to jump to the page - await tester.tapButton(find.byType(MentionPageBlock)); - await tester.pumpAndSettle(); - - // expect to go to the getting started page - final documentPage = find.byType(DocumentPage); - expect(documentPage, findsOneWidget); - expect( - tester.widget(documentPage).view.name, - Constants.gettingStartedPageName, - ); - // delete the getting started page - await tester.hoverOnPageName( - Constants.gettingStartedPageName, - onHover: () async => tester.tapDeletePageButton(), - ); - tester.expectToSeeDocumentBanner(); - tester.expectNotToSeePageName(gettingStarted); - - // delete the page permanently - await tester.tapDeletePermanentlyButton(); - - // go back the page - await tester.openPage(pageName); - await tester.pumpAndSettle(); - - // check the content of the block - // it should be no access to the page - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.findTextInFlowyText( - LocaleKeys.document_mention_noAccess.tr(), - ), - ), - findsOneWidget, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart deleted file mode 100644 index 1bc9bd8f92..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_option_actions_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document option actions:', () { - testWidgets('drag block to the top', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - - // before move - final beforeMoveBlock = tester.editor.getNodeAtPath([1]); - - // move the desktop guide to the top, above the getting started - await tester.editor.dragBlock( - [1], - const Offset(20, -80), - ); - - // wait for the move animation to complete - await tester.pumpAndSettle(Durations.short1); - - // check if the block is moved to the top - final afterMoveBlock = tester.editor.getNodeAtPath([0]); - expect(afterMoveBlock.delta, beforeMoveBlock.delta); - }); - - testWidgets('drag block to other block\'s child', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - - // before move - final beforeMoveBlock = tester.editor.getNodeAtPath([10]); - - // move the checkbox to the child of the block at path [9] - await tester.editor.dragBlock( - [10], - const Offset(120, -20), - ); - - // wait for the move animation to complete - await tester.pumpAndSettle(Durations.short1); - - // check if the block is moved to the child of the block at path [9] - final afterMoveBlock = tester.editor.getNodeAtPath([9, 0]); - expect(afterMoveBlock.delta, beforeMoveBlock.delta); - }); - - testWidgets('hover on the block and delete it', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open getting started page - await tester.openPage(Constants.gettingStartedPageName); - - // before delete - final path = [1]; - final beforeDeletedBlock = tester.editor.getNodeAtPath(path); - - // hover on the block and delete it - final optionButton = find.byWidgetPredicate( - (widget) => - widget is DraggableOptionButton && - widget.blockComponentContext.node.path.equals(path), - ); - - await tester.hoverOnWidget( - optionButton, - onHover: () async { - // click the delete button - await tester.tapButton(optionButton); - }, - ); - await tester.pumpAndSettle(Durations.short1); - - // click the delete button - final deleteButton = - find.findTextInFlowyText(LocaleKeys.button_delete.tr()); - await tester.tapButton(deleteButton); - - // wait for the deletion - await tester.pumpAndSettle(Durations.short1); - - // check if the block is deleted - final afterDeletedBlock = tester.editor.getNodeAtPath([1]); - expect(afterDeletedBlock.id, isNot(equals(beforeDeletedBlock.id))); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_publish_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_publish_test.dart deleted file mode 100644 index 7877143116..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_publish_test.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/publish_tab.dart'; -import 'package:appflowy/plugins/shared/share/share_menu.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/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Publish:', () { - testWidgets('publish document', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // open the publish menu - await tester.openPublishMenu(); - - // publish the document - final publishButton = find.byType(PublishButton); - final unpublishButton = find.byType(UnPublishButton); - await tester.tapButton(publishButton); - - // expect to see unpublish, visit site and manage all sites button - expect(unpublishButton, findsOneWidget); - expect(find.text(LocaleKeys.shareAction_visitSite.tr()), findsOneWidget); - - // unpublish the document - await tester.tapButton(unpublishButton); - - // expect to see publish button - expect(publishButton, findsOneWidget); - }); - - testWidgets('rename path name', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // open the publish menu - await tester.openPublishMenu(); - - // publish the document - final publishButton = find.byType(PublishButton); - await tester.tapButton(publishButton); - - // rename the path name - final inputField = find.descendant( - of: find.byType(ShareMenu), - matching: find.byType(TextField), - ); - - // rename with invalid name - await tester.tap(inputField); - await tester.enterText(inputField, '&&&&????'); - await tester.tapButton(find.text(LocaleKeys.button_save.tr())); - await tester.pumpAndSettle(); - - // expect to see the toast with error message - final errorToast1 = find.text( - LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters - .tr(), - ); - await tester.pumpUntilFound(errorToast1); - await tester.pumpUntilNotFound(errorToast1); - - // rename with long name - await tester.tap(inputField); - await tester.enterText(inputField, 'long-path-name' * 200); - await tester.tapButton(find.text(LocaleKeys.button_save.tr())); - await tester.pumpAndSettle(); - - // expect to see the toast with error message - final errorToast2 = find.text( - LocaleKeys.settings_sites_error_publishNameTooLong.tr(), - ); - await tester.pumpUntilFound(errorToast2); - await tester.pumpUntilNotFound(errorToast2); - - // rename with empty name - await tester.tap(inputField); - await tester.enterText(inputField, ''); - await tester.tapButton(find.text(LocaleKeys.button_save.tr())); - await tester.pumpAndSettle(); - - // expect to see the toast with error message - final errorToast3 = find.text( - LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(), - ); - await tester.pumpUntilFound(errorToast3); - await tester.pumpUntilNotFound(errorToast3); - - // input the new path name - await tester.tap(inputField); - await tester.enterText(inputField, 'new-path-name'); - // click save button - await tester.tapButton(find.text(LocaleKeys.button_save.tr())); - await tester.pumpAndSettle(); - - // expect to see the toast with success message - final successToast = find.text( - LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - await tester.pumpUntilFound(successToast); - await tester.pumpUntilNotFound(successToast); - - // click the copy link button - await tester.tapButton( - find.byWidgetPredicate( - (widget) => - widget is FlowySvg && - widget.svg.path == FlowySvgs.m_toolbar_link_m.path, - ), - ); - await tester.pumpAndSettle(); - // check the clipboard has the link - final content = await Clipboard.getData(Clipboard.kTextPlain); - expect( - content?.text?.contains('new-path-name'), - isTrue, - ); - }); - - testWidgets('re-publish the document', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // open the publish menu - await tester.openPublishMenu(); - - // publish the document - final publishButton = find.byType(PublishButton); - await tester.tapButton(publishButton); - - // rename the path name - final inputField = find.descendant( - of: find.byType(ShareMenu), - matching: find.byType(TextField), - ); - - // input the new path name - const newName = 'new-path-name'; - await tester.enterText(inputField, newName); - // click save button - await tester.tapButton(find.text(LocaleKeys.button_save.tr())); - await tester.pumpAndSettle(); - - // expect to see the toast with success message - final successToast = find.text( - LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - await tester.pumpUntilNotFound(successToast); - - // unpublish the document - final unpublishButton = find.byType(UnPublishButton); - await tester.tapButton(unpublishButton); - - final unpublishSuccessToast = find.text( - LocaleKeys.publish_unpublishSuccessfully.tr(), - ); - await tester.pumpUntilNotFound(unpublishSuccessToast); - - // re-publish the document - await tester.tapButton(publishButton); - - // expect to see the toast with success message - final rePublishSuccessToast = find.text( - LocaleKeys.publish_publishSuccessfully.tr(), - ); - await tester.pumpUntilNotFound(rePublishSuccessToast); - - // check the clipboard has the link - final content = await Clipboard.getData(Clipboard.kTextPlain); - expect( - content?.text?.contains(newName), - isTrue, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart deleted file mode 100644 index 58a9d7398b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/document/document_test_runner.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'document_ai_writer_test.dart' as document_ai_writer_test; -import 'document_copy_link_to_block_test.dart' - as document_copy_link_to_block_test; -import 'document_option_actions_test.dart' as document_option_actions_test; -import 'document_publish_test.dart' as document_publish_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - document_option_actions_test.main(); - document_copy_link_to_block_test.main(); - document_publish_test.main(); - document_ai_writer_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart deleted file mode 100644 index b24c0faf27..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/set_env.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -// This test is meaningless, just for preventing the CI from failing. -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Empty', () { - testWidgets('set appflowy cloud', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart deleted file mode 100644 index 5bcca50153..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_icon_test.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/emoji.dart'; -import '../../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - testWidgets('Change slide bar space icon', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - final emojiIconData = await tester.loadIcon(); - final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji)); - - await tester.hoverOnWidget( - find.byType(SidebarSpaceHeader), - onHover: () async { - final moreOption = find.byType(SpaceMorePopup); - await tester.tapButton(moreOption); - expect(find.byType(FlowyIconEmojiPicker), findsNothing); - await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg); - expect(find.byType(FlowyIconEmojiPicker), findsOneWidget); - }, - ); - - final icons = find.byWidgetPredicate( - (w) => w is FlowySvg && w.svgString == firstIcon.svgString, - ); - expect(icons, findsOneWidget); - await tester.tapIcon(EmojiIconData.icon(firstIcon)); - - final spaceHeader = find.byType(SidebarSpaceHeader); - final spaceIcon = find.descendant( - of: spaceHeader, - matching: find.byWidgetPredicate( - (w) => w is FlowySvg && w.svgString == firstIcon.svgString, - ), - ); - expect(spaceIcon, findsOneWidget); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_move_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_move_page_test.dart deleted file mode 100644 index 37abd19ebc..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_move_page_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('sidebar move page: ', () { - testWidgets('create a new document and move it to Getting started', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // click the ... button and move to Getting started - await tester.hoverOnPageName( - pageName, - onHover: () async { - await tester.tapPageOptionButton(); - await tester.tapButtonWithName( - LocaleKeys.disclosureAction_moveTo.tr(), - ); - }, - ); - - // expect to see two pages - // one is in the sidebar, the other is in the move to page list - // 1. Getting started - // 2. To-dos - final gettingStarted = find.findTextInFlowyText( - Constants.gettingStartedPageName, - ); - final toDos = find.findTextInFlowyText(Constants.toDosPageName); - await tester.pumpUntilFound(gettingStarted); - await tester.pumpUntilFound(toDos); - expect(gettingStarted, findsNWidgets(2)); - - // skip the length check on Linux temporarily, - // because it failed in expect check but the previous pumpUntilFound is successful - if (!UniversalPlatform.isLinux) { - expect(toDos, findsNWidgets(2)); - - // hover on the todos page, and will see a forbidden icon - await tester.hoverOnWidget( - toDos.last, - onHover: () async { - final tooltips = find.byTooltip( - LocaleKeys.space_cannotMovePageToDatabase.tr(), - ); - expect(tooltips, findsOneWidget); - }, - ); - await tester.pumpAndSettle(); - } - - // Attempt right-click on the page name and expect not to see - await tester.tap(gettingStarted.last, buttons: kSecondaryButton); - await tester.pumpAndSettle(); - expect( - find.text(LocaleKeys.disclosureAction_moveTo.tr()), - findsOneWidget, - ); - - // move the current page to Getting started - await tester.tapButton( - gettingStarted.last, - ); - - await tester.pumpAndSettle(); - - // after moving, expect to not see the page name in the sidebar - final page = tester.findPageName(pageName); - expect(page, findsNothing); - - // click to expand the getting started page - await tester.expandOrCollapsePage( - pageName: Constants.gettingStartedPageName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - // expect to see the page name in the getting started page - final pageInGettingStarted = tester.findPageName( - pageName, - parentName: Constants.gettingStartedPageName, - ); - expect(pageInGettingStarted, findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_rename_untitled_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_rename_untitled_test.dart deleted file mode 100644 index 8226b68b26..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/sidebar/sidebar_rename_untitled_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text_input.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Rename empty name view (untitled)', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - ); - - // click the ... button and open rename dialog - await tester.hoverOnPageName( - ViewLayoutPB.Document.defaultName, - onHover: () async { - await tester.tapPageOptionButton(); - await tester.tapButtonWithName( - LocaleKeys.disclosureAction_rename.tr(), - ); - }, - ); - await tester.pumpAndSettle(); - - expect(find.byType(NavigatorTextFieldDialog), findsOneWidget); - - final textField = tester.widget( - find.descendant( - of: find.byType(NavigatorTextFieldDialog), - matching: find.byType(FlowyFormTextInput), - ), - ); - - expect( - textField.controller!.text, - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - ); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart deleted file mode 100644 index fd65c29927..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/appflowy_cloud_auth_test.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.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/widgets/setting_appflowy_cloud.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('appflowy cloud auth', () { - testWidgets('sign in', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - }); - - testWidgets('sign out', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - - // Open the setting page and sign out - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - - // Scroll to sign-out - await tester.scrollUntilVisible( - find.byType(AccountSignInOutButton), - 100, - scrollable: find.findSettingsScrollable(), - ); - await tester.tapButton(find.byType(AccountSignInOutButton)); - - 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: 5)); - tester.expectToSeeGoogleLoginButton(); - }); - - testWidgets('sign in as anonymous', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapSignInAsGuest(); - - // should not see the sync setting page when sign in as anonymous - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - - await tester.tapButton(find.byType(AccountSignInOutButton)); - - tester.expectToSeeGoogleLoginButton(); - }); - - testWidgets('enable sync', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - - await tester.tapGoogleLoginInButton(); - // Open the setting page and sign out - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.cloud); - await tester.pumpAndSettle(); - - // the switch should be on by default - tester.assertAppFlowyCloudEnableSyncSwitchValue(true); - await tester.toggleEnableSync(AppFlowyCloudEnableSync); - // wait for the switch animation - await tester.wait(250); - - // the switch should be off - tester.assertAppFlowyCloudEnableSyncSwitchValue(false); - - // the switch should be on after toggling - await tester.toggleEnableSync(AppFlowyCloudEnableSync); - tester.assertAppFlowyCloudEnableSyncSwitchValue(true); - await tester.wait(250); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/document_sync_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/document_sync_test.dart deleted file mode 100644 index b6b4ecf025..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/document_sync_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final email = '${uuid()}@appflowy.io'; - const inputContent = 'Hello world, this is a test document'; - -// The test will create a new document called Sample, and sync it to the server. -// Then the test will logout the user, and login with the same user. The data will -// be synced from the server. - group('appflowy cloud document', () { - testWidgets('sync local docuemnt to server', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - email: email, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // create a new document called Sample - await tester.createNewPage(); - - // focus on the editor - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(inputContent); - expect(find.text(inputContent, findRichText: true), findsOneWidget); - - // 6 seconds for data sync - await tester.waitForSeconds(6); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - await tester.logout(); - }); - - testWidgets('sync doc from server', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - email: email, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePage(); - - // the latest document will be opened, so the content must be the inputContent - await tester.pumpAndSettle(); - expect(find.text(inputContent, findRichText: true), findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/uncategorized_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/uncategorized_test_runner.dart deleted file mode 100644 index 278d880965..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/uncategorized_test_runner.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; -import 'user_setting_sync_test.dart' as user_sync_test; - -void main() async { - appflowy_cloud_auth_test.main(); - user_sync_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/user_setting_sync_test.dart deleted file mode 100644 index e666289bf5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/uncategorized/user_setting_sync_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - final email = '${uuid()}@appflowy.io'; - const name = 'nathan'; - - group('appflowy cloud setting', () { - testWidgets('sync user name and icon to server', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - email: email, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - - await tester.enterUserName(name); - await tester.pumpAndSettle(const Duration(seconds: 6)); - await tester.logout(); - - await tester.pumpAndSettle(const Duration(seconds: 2)); - }); - }); - testWidgets('get user icon and name from server', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - email: email, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - await tester.pumpAndSettle(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.account); - - // Verify name - final profileSetting = - tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile; - - expect(profileSetting.name, name); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart deleted file mode 100644 index 4d2e027646..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/collaborative_workspace_test.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('collaborative workspace:', () { - // combine the create and delete workspace test to reduce the time - testWidgets('create a new workspace, open it and then delete it', - (tester) async { - // only run the test when the feature flag is on - if (!FeatureFlag.collaborativeWorkspace.isOn) { - return; - } - - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const name = 'AppFlowy.IO'; - // the workspace will be opened after created - await tester.createCollaborativeWorkspace(name); - - final loading = find.byType(Loading); - await tester.pumpUntilNotFound(loading); - - Finder success; - - final Finder items = find.byType(WorkspaceMenuItem); - - // delete the newly created workspace - await tester.openCollaborativeWorkspaceMenu(); - await tester.pumpUntilFound(items); - - expect(items, findsNWidgets(2)); - expect( - tester.widget(items.last).workspace.name, - name, - ); - - final secondWorkspace = find.byType(WorkspaceMenuItem).last; - await tester.hoverOnWidget( - secondWorkspace, - onHover: () async { - // click the more button - final moreButton = find.byType(WorkspaceMoreActionList); - expect(moreButton, findsOneWidget); - await tester.tapButton(moreButton); - // click the delete button - final deleteButton = find.text(LocaleKeys.button_delete.tr()); - expect(deleteButton, findsOneWidget); - await tester.tapButton(deleteButton); - // see the delete confirm dialog - final confirm = - find.text(LocaleKeys.workspace_deleteWorkspaceHintText.tr()); - expect(confirm, findsOneWidget); - await tester.tapButton(find.text(LocaleKeys.button_ok.tr())); - // delete success - success = find.text(LocaleKeys.workspace_createSuccess.tr()); - await tester.pumpUntilFound(success); - expect(success, findsOneWidget); - await tester.pumpUntilNotFound(success); - }, - ); - }); - - testWidgets('check the member count immediately after creating a workspace', - (tester) async { - // only run the test when the feature flag is on - if (!FeatureFlag.collaborativeWorkspace.isOn) { - return; - } - - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const name = 'AppFlowy.IO'; - // the workspace will be opened after created - await tester.createCollaborativeWorkspace(name); - - final loading = find.byType(Loading); - await tester.pumpUntilNotFound(loading); - - await tester.openCollaborativeWorkspaceMenu(); - - // expect to see the member count - final memberCount = find.text('1 member'); - expect(memberCount, findsNWidgets(2)); - }); - - testWidgets('workspace menu popover behavior test', (tester) async { - // only run the test when the feature flag is on - if (!FeatureFlag.collaborativeWorkspace.isOn) { - return; - } - - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const name = 'AppFlowy.IO'; - // the workspace will be opened after created - await tester.createCollaborativeWorkspace(name); - - final loading = find.byType(Loading); - await tester.pumpUntilNotFound(loading); - - await tester.openCollaborativeWorkspaceMenu(); - - // hover on the workspace and click the more button - final workspaceItem = find.byWidgetPredicate( - (w) => w is WorkspaceMenuItem && w.workspace.name == name, - ); - - // the workspace menu shouldn't conflict with logout - await tester.hoverOnWidget( - workspaceItem, - onHover: () async { - final moreButton = find.byWidgetPredicate( - (w) => w is WorkspaceMoreActionList && w.workspace.name == name, - ); - expect(moreButton, findsOneWidget); - await tester.tapButton(moreButton); - expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); - - final logoutButton = find.byType(WorkspaceMoreButton); - await tester.tapButton(logoutButton); - expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget); - expect(moreButton, findsNothing); - - await tester.tapButton(moreButton); - expect(find.text(LocaleKeys.button_logout.tr()), findsNothing); - expect(moreButton, findsOneWidget); - }, - ); - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // clicking on the more action button for the same workspace shouldn't do - // anything - await tester.openCollaborativeWorkspaceMenu(); - await tester.hoverOnWidget( - workspaceItem, - onHover: () async { - final moreButton = find.byWidgetPredicate( - (w) => w is WorkspaceMoreActionList && w.workspace.name == name, - ); - expect(moreButton, findsOneWidget); - await tester.tapButton(moreButton); - expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); - - // click it again - await tester.tapButton(moreButton); - - // nothing should happen - expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); - }, - ); - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // clicking on the more button of another workspace should close the menu - // for this one - await tester.openCollaborativeWorkspaceMenu(); - final moreButton = find.byWidgetPredicate( - (w) => w is WorkspaceMoreActionList && w.workspace.name == name, - ); - await tester.hoverOnWidget( - workspaceItem, - onHover: () async { - expect(moreButton, findsOneWidget); - await tester.tapButton(moreButton); - expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); - }, - ); - - final otherWorspaceItem = find.byWidgetPredicate( - (w) => w is WorkspaceMenuItem && w.workspace.name != name, - ); - final otherMoreButton = find.byWidgetPredicate( - (w) => w is WorkspaceMoreActionList && w.workspace.name != name, - ); - await tester.hoverOnWidget( - otherWorspaceItem, - onHover: () async { - expect(otherMoreButton, findsOneWidget); - await tester.tapButton(otherMoreButton); - expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget); - - expect(moreButton, findsNothing); - }, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/share_menu_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/share_menu_test.dart deleted file mode 100644 index 70bb46279e..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/share_menu_test.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/plugins/shared/share/share_menu.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Share menu:', () { - testWidgets('share tab', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // click the share button - await tester.tapShareButton(); - - // expect the share menu is shown - final shareMenu = find.byType(ShareMenu); - expect(shareMenu, findsOneWidget); - - // click the copy link button - final copyLinkButton = find.textContaining( - LocaleKeys.button_copyLink.tr(), - ); - await tester.tapButton(copyLinkButton); - - // read the clipboard content - final clipboardContent = await getIt().getData(); - final plainText = clipboardContent.plainText; - expect( - plainText, - matches(appflowySharePageLinkPattern), - ); - - final shareValues = plainText! - .replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '') - .split('/'); - final workspaceId = shareValues[0]; - expect(workspaceId, isNotEmpty); - final pageId = shareValues[1]; - expect(pageId, isNotEmpty); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/tabs_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/tabs_test.dart deleted file mode 100644 index 5c07d99afa..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/tabs_test.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; -import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart'; -import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Tabs', () { - testWidgets('close other tabs before opening a new workspace', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const name = 'AppFlowy.IO'; - // the workspace will be opened after created - await tester.createCollaborativeWorkspace(name); - - final loading = find.byType(Loading); - await tester.pumpUntilNotFound(loading); - - // create new tabs in the workspace - expect(find.byType(FlowyTab), findsNothing); - - const documentOneName = 'document one'; - const documentTwoName = 'document two'; - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: documentOneName, - ); - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: documentTwoName, - ); - - /// Open second menu item in a new tab - await tester.openAppInNewTab(documentOneName, ViewLayoutPB.Document); - - /// Open third menu item in a new tab - await tester.openAppInNewTab(documentTwoName, ViewLayoutPB.Document); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(FlowyTab), - ), - findsNWidgets(2), - ); - - // switch to the another workspace - final Finder items = find.byType(WorkspaceMenuItem); - await tester.openCollaborativeWorkspaceMenu(); - await tester.pumpUntilFound(items); - expect(items, findsNWidgets(2)); - - // open the first workspace - await tester.tap(items.first); - await tester.pumpUntilNotFound(loading); - - expect(find.byType(FlowyTab), findsNothing); - }); - - testWidgets('the space view should not be opened', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - expect(find.byType(AppFlowyEditorPage), findsNothing); - expect(find.text('Blank page'), findsOne); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_icon_test.dart deleted file mode 100644 index e9ad06caee..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_icon_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.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'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; -import '../../../shared/workspace.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('workspace icon:', () { - testWidgets('remove icon from workspace', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openWorkspaceMenu(); - - // click the workspace icon - await tester.tapButton( - find.descendant( - of: find.byType(WorkspaceMenuItem), - matching: find.byType(WorkspaceIcon), - ), - ); - // click the remove icon button - await tester.tapButton( - find.text(LocaleKeys.button_remove.tr()), - ); - - // nothing should happen - expect( - find.text(LocaleKeys.workspace_updateIconSuccess.tr()), - findsNothing, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_settings_test.dart deleted file mode 100644 index a58fea25b8..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_settings_test.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/plugins/shared/share/publish_tab.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.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_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('workspace settings: ', () { - testWidgets( - 'change document width', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.workspace); - - final documentWidthSettings = find.findTextInFlowyText( - LocaleKeys.settings_appearance_documentSettings_width.tr(), - ); - - final scrollable = find.ancestor( - of: find.byType(SettingsWorkspaceView), - matching: find.descendant( - of: find.byType(SingleChildScrollView), - matching: find.byType(Scrollable), - ), - ); - - await tester.scrollUntilVisible( - documentWidthSettings, - 0, - scrollable: scrollable, - ); - await tester.pumpAndSettle(); - - // change the document width - final slider = find.byType(Slider); - final oldValue = tester.widget(slider).value; - await tester.drag(slider, const Offset(-100, 0)); - await tester.pumpAndSettle(); - - // check the document width is changed - expect(tester.widget(slider).value, lessThan(oldValue)); - - // click the reset button - final resetButton = find.descendant( - of: find.byType(DocumentPaddingSetting), - matching: find.byType(SettingsResetButton), - ); - await tester.tap(resetButton); - await tester.pumpAndSettle(); - - // check the document width is reset - expect( - tester.widget(slider).value, - EditorStyleCustomizer.maxDocumentWidth, - ); - }, - ); - }); - - group('sites settings:', () { - testWidgets( - 'manage published page, set it as homepage, remove the homepage', - (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // open the publish menu - await tester.openPublishMenu(); - - // publish the document - await tester.tapButton(find.byType(PublishButton)); - - // click empty area to close the publish menu - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - // check if the page is published in sites page - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.sites); - // wait the backend return the sites data - await tester.wait(1000); - - // check if the page is published in sites page - final pageItem = find.byWidgetPredicate( - (widget) => - widget is PublishedViewItem && - widget.publishInfoView.view.name == pageName, - ); - if (pageItem.evaluate().isEmpty) { - return; - } - - expect(pageItem, findsOneWidget); - - // comment it out because it's not allowed to update the namespace in free plan - // // set it to homepage - // await tester.tapButton( - // find.textContaining( - // LocaleKeys.settings_sites_selectHomePage.tr(), - // ), - // ); - // await tester.tapButton( - // find.descendant( - // of: find.byType(SelectHomePageMenu), - // matching: find.text(pageName), - // ), - // ); - // await tester.pumpAndSettle(); - - // // check if the page is set to homepage - // final homePageItem = find.descendant( - // of: find.byType(DomainItem), - // matching: find.text(pageName), - // ); - // expect(homePageItem, findsOneWidget); - - // // remove the homepage - // await tester.tapButton(find.byType(DomainMoreAction)); - // await tester.tapButton( - // find.text(LocaleKeys.settings_sites_removeHomepage.tr()), - // ); - // await tester.pumpAndSettle(); - - // // check if the page is removed from homepage - // expect(homePageItem, findsNothing); - }); - - testWidgets('update namespace', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // check if the page is published in sites page - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.sites); - // wait the backend return the sites data - await tester.wait(1000); - - // update the domain - final domainMoreAction = find.byType(DomainMoreAction); - await tester.tapButton(domainMoreAction); - final updateNamespaceButton = find.text( - LocaleKeys.settings_sites_updateNamespace.tr(), - ); - await tester.pumpUntilFound(updateNamespaceButton); - - // click the update namespace button - - await tester.tapButton(updateNamespaceButton); - - // comment it out because it's not allowed to update the namespace in free plan - // expect to see the dialog - // await tester.updateNamespace('&&&???'); - - // // need to upgrade to pro plan to update the namespace - // final errorToast = find.text( - // LocaleKeys.settings_sites_error_proPlanLimitation.tr(), - // ); - // await tester.pumpUntilFound(errorToast); - // expect(errorToast, findsOneWidget); - // await tester.pumpUntilNotFound(errorToast); - - // comment it out because it's not allowed to update the namespace in free plan - // // short namespace - // await tester.updateNamespace('a'); - - // // expect to see the toast with error message - // final errorToast2 = find.text( - // LocaleKeys.settings_sites_error_namespaceTooShort.tr(), - // ); - // await tester.pumpUntilFound(errorToast2); - // expect(errorToast2, findsOneWidget); - // await tester.pumpUntilNotFound(errorToast2); - // // valid namespace - // await tester.updateNamespace('AppFlowy'); - - // // expect to see the toast with success message - // final successToast = find.text( - // LocaleKeys.settings_sites_success_namespaceUpdated.tr(), - // ); - // await tester.pumpUntilFound(successToast); - // expect(successToast, findsOneWidget); - }); - - testWidgets(''' -More actions for published page: -1. visit site -2. copy link -3. settings -4. unpublish -5. custom url -''', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - const pageName = 'Document'; - - await tester.createNewPageInSpace( - spaceName: Constants.generalSpaceName, - layout: ViewLayoutPB.Document, - pageName: pageName, - ); - - // open the publish menu - await tester.openPublishMenu(); - - // publish the document - await tester.tapButton(find.byType(PublishButton)); - - // click empty area to close the publish menu - await tester.tapAt(Offset.zero); - await tester.pumpAndSettle(); - // check if the page is published in sites page - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.sites); - // wait the backend return the sites data - await tester.wait(2000); - - // check if the page is published in sites page - final pageItem = find.byWidgetPredicate( - (widget) => - widget is PublishedViewItem && - widget.publishInfoView.view.name == pageName, - ); - if (pageItem.evaluate().isEmpty) { - return; - } - - expect(pageItem, findsOneWidget); - - final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr()); - final customUrlItem = find.text(LocaleKeys.settings_sites_customUrl.tr()); - final unpublishItem = find.text(LocaleKeys.shareAction_unPublish.tr()); - - // custom url - final publishMoreAction = find.byType(PublishedViewMoreAction); - - // click the copy link button - { - await tester.tapButton(publishMoreAction); - await tester.pumpAndSettle(); - await tester.pumpUntilFound(copyLinkItem); - await tester.tapButton(copyLinkItem); - await tester.pumpAndSettle(); - await tester.pumpUntilNotFound(copyLinkItem); - - final clipboardContent = await getIt().getData(); - final plainText = clipboardContent.plainText; - expect( - plainText, - contains(pageName), - ); - } - - // custom url - { - await tester.tapButton(publishMoreAction); - await tester.pumpAndSettle(); - await tester.pumpUntilFound(customUrlItem); - await tester.tapButton(customUrlItem); - await tester.pumpAndSettle(); - await tester.pumpUntilNotFound(customUrlItem); - - // see the custom url dialog - final customUrlDialog = find.byType(PublishedViewSettingsDialog); - expect(customUrlDialog, findsOneWidget); - - // rename the custom url - final textField = find.descendant( - of: customUrlDialog, - matching: find.byType(TextField), - ); - await tester.enterText(textField, 'hello-world'); - await tester.pumpAndSettle(); - - // click the save button - final saveButton = find.descendant( - of: customUrlDialog, - matching: find.text(LocaleKeys.button_save.tr()), - ); - await tester.tapButton(saveButton); - await tester.pumpAndSettle(); - - // expect to see the toast with success message - final successToast = find.text( - LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - await tester.pumpUntilFound(successToast); - expect(successToast, findsOneWidget); - } - - // unpublish - { - await tester.tapButton(publishMoreAction); - await tester.pumpAndSettle(); - await tester.pumpUntilFound(unpublishItem); - await tester.tapButton(unpublishItem); - await tester.pumpAndSettle(); - await tester.pumpUntilNotFound(unpublishItem); - - // expect to see the toast with success message - final successToast = find.text( - LocaleKeys.publish_unpublishSuccessfully.tr(), - ); - await tester.pumpUntilFound(successToast); - expect(successToast, findsOneWidget); - await tester.pumpUntilNotFound(successToast); - - // check if the page is unpublished in sites page - expect(pageItem, findsNothing); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_test_runner.dart deleted file mode 100644 index 4d2862038e..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/cloud/workspace/workspace_test_runner.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'change_name_and_icon_test.dart' as change_name_and_icon_test; -import 'collaborative_workspace_test.dart' as collaborative_workspace_test; -import 'share_menu_test.dart' as share_menu_test; -import 'tabs_test.dart' as tabs_test; -import 'workspace_icon_test.dart' as workspace_icon_test; -import 'workspace_settings_test.dart' as workspace_settings_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - workspace_settings_test.main(); - share_menu_test.main(); - collaborative_workspace_test.main(); - change_name_and_icon_test.main(); - workspace_icon_test.main(); - tabs_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart index 0b77a0167b..80c907b9e9 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/folder_search_test.dart @@ -1,15 +1,6 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_cell.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -17,9 +8,7 @@ import 'package:integration_test/integration_test.dart'; import '../../shared/util.dart'; void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - }); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Folder Search', () { testWidgets('Search for views', (tester) async { @@ -44,106 +33,21 @@ void main() { await tester.pumpAndSettle(const Duration(milliseconds: 200)); // Expect two search results "ViewOna" and "ViewOne" (Distance 1 to ViewOna) - expect(find.byType(SearchResultCell), findsNWidgets(2)); + expect(find.byType(SearchResultTile), findsNWidgets(2)); // The score should be higher for "ViewOna" thus it should be shown first final secondDocumentWidget = tester - .widget(find.byType(SearchResultCell).first) as SearchResultCell; - expect(secondDocumentWidget.item.displayName, secondDocument); + .widget(find.byType(SearchResultTile).first) as SearchResultTile; + expect(secondDocumentWidget.result.data, secondDocument); // Change search to "ViewOne" await tester.enterText(searchFieldFinder, firstDocument); await tester.pumpAndSettle(const Duration(seconds: 1)); // The score should be higher for "ViewOne" thus it should be shown first - final firstDocumentWidget = tester.widget( - find.byType(SearchResultCell).first, - ) as SearchResultCell; - expect(firstDocumentWidget.item.displayName, firstDocument); - }); - - testWidgets('Displaying icons in search results', (tester) async { - final randomValue = Random().nextInt(10000) + 10000; - final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue']; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final emojiIconData = await tester.loadIcon(); - - /// create two pages - for (final pageName in pageNames) { - await tester.createNewPageWithNameUnderParent(name: pageName); - await tester.updatePageIconInTitleBarByName( - name: pageName, - layout: ViewLayoutPB.Document, - icon: emojiIconData, - ); - } - - await tester.toggleCommandPalette(); - - /// search for `Page` - final searchFieldFinder = find.descendant( - of: find.byType(SearchField), - matching: find.byType(FlowyTextField), - ); - await tester.enterText(searchFieldFinder, 'Page-$randomValue'); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - expect(find.byType(SearchResultCell), findsNWidgets(2)); - - /// check results - final svgs = find.descendant( - of: find.byType(SearchResultCell), - matching: find.byType(FlowySvg), - ); - expect(svgs, findsNWidgets(2)); - - final firstSvg = svgs.first.evaluate().first.widget as FlowySvg, - lastSvg = svgs.last.evaluate().first.widget as FlowySvg; - final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji)); - - /// icon displayed correctly - expect(firstSvg.svgString, iconData.svgString); - expect(lastSvg.svgString, iconData.svgString); - - testWidgets('select the content in document and search', (tester) async { - const firstDocument = ''; // empty document - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(name: firstDocument); - await tester.editor.updateSelection( - Selection( - start: Position( - path: [0], - ), - end: Position( - path: [0], - offset: 10, - ), - ), - ); - await tester.pumpAndSettle(); - - expect( - find.byType(FloatingToolbar), - findsOneWidget, - ); - - await tester.toggleCommandPalette(); - expect(find.byType(CommandPaletteModal), findsOneWidget); - - expect( - find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()), - findsOneWidget, - ); - - expect( - find.text(firstDocument), - findsOneWidget, - ); - }); + final firstDocumentWidget = tester + .widget(find.byType(SearchResultTile).first) as SearchResultTile; + expect(firstDocumentWidget.result.data, firstDocument); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart index b9495ae0e7..277ae8f21e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/command_palette/recent_history_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -27,12 +27,11 @@ void main() { expect(find.byType(RecentViewsList), findsOneWidget); // Expect three recent history items - expect(find.byType(SearchRecentViewCell), findsNWidgets(3)); + expect(find.byType(RecentViewTile), findsNWidgets(3)); // Expect the first item to be the last viewed document final firstDocumentWidget = - tester.widget(find.byType(SearchRecentViewCell).first) - as SearchRecentViewCell; + tester.widget(find.byType(RecentViewTile).first) as RecentViewTile; expect(firstDocumentWidget.view.name, secondDocument); }); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart index 3a565cbee9..a9912e3ef3 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_calendar_test.dart @@ -1,6 +1,5 @@ import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,14 +9,7 @@ import '../../shared/database_test_op.dart'; import '../../shared/util.dart'; void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('calendar', () { testWidgets('update calendar layout', (tester) async { @@ -285,74 +277,5 @@ void main() { tester.assertRowDetailPageOpened(); }); - - testWidgets('filter calendar events', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // Create the calendar view - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Calendar, - ); - - // Create a new event on the first of this month - final today = DateTime.now(); - final firstOfThisMonth = DateTime(today.year, today.month); - await tester.doubleClickCalendarCell(firstOfThisMonth); - await tester.dismissEventEditor(); - - tester.assertNumberOfEventsInCalendar(1); - - await tester.openCalendarEvent(index: 0, date: firstOfThisMonth); - await tester.tapButton(finderForFieldType(FieldType.MultiSelect)); - await tester.createOption(name: "asdf"); - await tester.createOption(name: "qwer"); - await tester.selectOption(name: "asdf"); - await tester.dismissCellEditor(); - await tester.dismissCellEditor(); - - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType(FieldType.MultiSelect, "Tags"); - - await tester.tapFilterButtonInGrid('Tags'); - await tester.tapOptionFilterWithName('asdf'); - await tester.dismissCellEditor(); - - tester.assertNumberOfEventsInCalendar(0); - - await tester.tapFilterButtonInGrid('Tags'); - await tester.tapOptionFilterWithName('asdf'); - await tester.dismissCellEditor(); - - tester.assertNumberOfEventsInCalendar(1); - - await tester.tapFilterButtonInGrid('Tags'); - await tester.tapOptionFilterWithName('asdf'); - await tester.dismissCellEditor(); - - tester.assertNumberOfEventsInCalendar(0); - - final secondOfThisMonth = DateTime(today.year, today.month, 2); - await tester.doubleClickCalendarCell(secondOfThisMonth); - await tester.dismissEventEditor(); - tester.assertNumberOfEventsInCalendar(1); - - await tester.openCalendarEvent(index: 0, date: secondOfThisMonth); - await tester.tapButton(finderForFieldType(FieldType.MultiSelect)); - await tester.selectOption(name: "asdf"); - await tester.dismissCellEditor(); - await tester.dismissCellEditor(); - - tester.assertNumberOfEventsInCalendar(0); - - await tester.tapFilterButtonInGrid('Tags'); - await tester.changeSelectFilterCondition( - SelectOptionFilterConditionPB.OptionIsEmpty, - ); - await tester.dismissCellEditor(); - - tester.assertNumberOfEventsInCalendar(1); - tester.assertNumberOfEventsOnSpecificDay(1, secondOfThisMonth); - }); }); } 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 ca565474ec..cb8338fbb6 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,4 +1,3 @@ -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'; @@ -42,7 +41,7 @@ void main() { name: 'my grid', layout: ViewLayoutPB.Grid, ); - await tester.createField(FieldType.RichText, name: 'description'); + await tester.createField(FieldType.RichText, 'description'); await tester.editCell( rowIndex: 0, @@ -82,7 +81,7 @@ void main() { const fieldType = FieldType.Number; // Create a number field - await tester.createField(fieldType); + await tester.createField(fieldType, fieldType.name); await tester.editCell( rowIndex: 0, @@ -158,7 +157,7 @@ void main() { const fieldType = FieldType.CreatedTime; // Create a create time field // The create time field is not editable - await tester.createField(fieldType); + await tester.createField(fieldType, fieldType.name); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -176,7 +175,7 @@ void main() { const fieldType = FieldType.LastEditedTime; // Create a last time field // The last time field is not editable - await tester.createField(fieldType); + await tester.createField(fieldType, fieldType.name); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -192,7 +191,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.DateTime; - await tester.createField(fieldType); + await tester.createField(fieldType, fieldType.name); // Tap the cell to invoke the field editor await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -211,21 +210,21 @@ void main() { await tester.toggleIncludeTime(); // Select a date - DateTime now = DateTime.now(); - await tester.selectDay(content: now.day); + final today = DateTime.now(); + await tester.selectDay(content: today.day); await tester.dismissCellEditor(); tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y').format(now), + content: DateFormat('MMM dd, y').format(today), ); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); // Toggle include time - now = DateTime.now(); + final now = DateTime.now(); await tester.toggleIncludeTime(); await tester.dismissCellEditor(); @@ -299,7 +298,7 @@ void main() { await tester.dismissCellEditor(); // Make sure the option is created and displayed in the cell - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: 'tag 1', ); @@ -311,12 +310,12 @@ void main() { await tester.createOption(name: 'tag 2'); await tester.dismissCellEditor(); - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: 'tag 2', ); - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsOneWidget, ); @@ -328,12 +327,12 @@ void main() { await tester.selectOption(name: 'tag 1'); await tester.dismissCellEditor(); - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: 'tag 1', ); - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsOneWidget, ); @@ -345,7 +344,7 @@ void main() { await tester.selectOption(name: 'tag 1'); await tester.dismissCellEditor(); - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsNothing, ); @@ -367,7 +366,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.MultiSelect; - await tester.createField(fieldType, name: fieldType.i18n); + await tester.createField(fieldType, fieldType.name); // Tap the cell to invoke the selection option editor await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -378,7 +377,7 @@ void main() { await tester.dismissCellEditor(); // Make sure the option is created and displayed in the cell - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: tags.first, ); @@ -393,13 +392,13 @@ void main() { await tester.dismissCellEditor(); for (final tag in tags) { - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: tag, ); } - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsNWidgets(4), ); @@ -413,7 +412,7 @@ void main() { } await tester.dismissCellEditor(); - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsNothing, ); @@ -426,16 +425,16 @@ void main() { await tester.selectOption(name: tags[3]); await tester.dismissCellEditor(); - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: tags[1], ); - tester.findSelectOptionWithNameInGrid( + await tester.findSelectOptionWithNameInGrid( rowIndex: 0, name: tags[3], ); - tester.assertNumberOfSelectedOptionsInGrid( + await tester.assertNumberOfSelectedOptionsInGrid( rowIndex: 0, matcher: findsNWidgets(2), ); @@ -450,7 +449,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); const fieldType = FieldType.Checklist; - await tester.createField(fieldType); + await tester.createField(fieldType, fieldType.name); // assert that there is no progress bar in the grid tester.assertChecklistCellInGrid(rowIndex: 0, percent: null); @@ -462,22 +461,22 @@ void main() { tester.assertChecklistEditorVisible(visible: true); // create a new task with enter - await tester.createNewChecklistTask(name: "task 1", enter: true); + await tester.createNewChecklistTask(name: "task 0", enter: true); // assert that the task is displayed tester.assertChecklistTaskInEditor( index: 0, - name: "task 1", + name: "task 0", isChecked: false, ); // update the task's name - await tester.renameChecklistTask(index: 0, name: "task 11"); + await tester.renameChecklistTask(index: 0, name: "task 1"); // assert that the task's name is updated tester.assertChecklistTaskInEditor( index: 0, - name: "task 11", + name: "task 1", isChecked: false, ); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart index a71110f1e0..865ea15479 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_settings_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -10,12 +10,11 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('grid field settings test:', () { + group('database field settings', () { testWidgets('field visibility', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - // create a database and add a linked database view await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); @@ -30,11 +29,6 @@ void main() { await tester.tapHidePropertyButton(); tester.noFieldWithName('New field 1'); - // create another field, New field 1 to be hidden still - await tester.tapNewPropertyButton(); - await tester.dismissFieldEditor(); - tester.noFieldWithName('New field 1'); - // go back to inline database view, expect field to be shown await tester.tapTabBarLinkedViewByViewName('Untitled'); tester.findFieldWithName('New field 1'); @@ -56,50 +50,6 @@ void main() { await tester.tapHidePropertyButtonInFieldEditor(); await tester.dismissRowDetailPage(); tester.noFieldWithName('New field 1'); - - // the field should still be sort and filter-able - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.RichText, - "New field 1", - ); - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, "New field 1"); - }); - - testWidgets('field cell width', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a database and add a linked database view - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); - - // create a field - await tester.scrollToRight(find.byType(GridPage)); - await tester.tapNewPropertyButton(); - await tester.renameField('New field 1'); - await tester.dismissFieldEditor(); - - // check the width of the field - expect(tester.getFieldWidth('New field 1'), 150); - - // change the width of the field - await tester.changeFieldWidth('New field 1', 200); - expect(tester.getFieldWidth('New field 1'), 205); - - // create another field, New field 1 to be same width - await tester.tapNewPropertyButton(); - await tester.dismissFieldEditor(); - expect(tester.getFieldWidth('New field 1'), 205); - - // go back to inline database view, expect New field 1 to be 150px - await tester.tapTabBarLinkedViewByViewName('Untitled'); - expect(tester.getFieldWidth('New field 1'), 150); - - // go back to linked database view, expect New field 1 to be 205px - await tester.tapTabBarLinkedViewByViewName('Grid'); - expect(tester.getFieldWidth('New field 1'), 205); }); }); } 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 6ce248a8a1..cc1187da21 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,29 +1,20 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-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'; void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('grid edit field test:', () { + group('grid field editor:', () { testWidgets('rename existing field', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -32,6 +23,7 @@ void main() { // Invoke the field editor await tester.tapGridFieldWithName('Name'); + await tester.tapEditFieldButton(); await tester.renameField('hello world'); await tester.dismissFieldEditor(); @@ -40,32 +32,6 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('edit field icon', (tester) async { - const icon = 'artificial_intelligence/ai-upscale-spark'; - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - tester.assertFieldSvg('Name', FieldType.RichText); - - // choose specific icon - await tester.tapGridFieldWithName('Name'); - await tester.changeFieldIcon(icon); - await tester.dismissFieldEditor(); - - tester.assertFieldCustomSvg('Name', icon); - - // remove icon - await tester.tapGridFieldWithName('Name'); - await tester.changeFieldIcon(''); - await tester.dismissFieldEditor(); - - tester.assertFieldSvg('Name', FieldType.RichText); - - await tester.pumpAndSettle(); - }); - testWidgets('update field type of existing field', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -90,22 +56,11 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.createField(FieldType.Checklist); - tester.findFieldWithName(FieldType.Checklist.i18n); + await tester.createField(FieldType.Checklist, 'checklist'); - // 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); + // check the field is created successfully + tester.findFieldWithName('checklist'); + await tester.pumpAndSettle(); }); testWidgets('delete field', (tester) async { @@ -115,14 +70,14 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.createField(FieldType.Checkbox, name: 'New field 1'); + await tester.createField(FieldType.Checkbox, 'New field 1'); // Delete the field await tester.tapGridFieldWithName('New field 1'); await tester.tapDeletePropertyButton(); // confirm delete - await tester.tapButtonWithName(LocaleKeys.space_delete.tr()); + await tester.tapDialogOkButton(); tester.noFieldWithName('New field 1'); await tester.pumpAndSettle(); @@ -135,7 +90,10 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // create a field - await tester.createField(FieldType.RichText, name: 'New field 1'); + await tester.scrollToRight(find.byType(GridPage)); + await tester.tapNewPropertyButton(); + await tester.renameField('New field 1'); + await tester.dismissFieldEditor(); // duplicate the field await tester.tapGridFieldWithName('New field 1'); @@ -159,7 +117,7 @@ void main() { await tester.dismissFieldEditor(); tester.findFieldWithName('Right'); - // insert new field to the left + // insert new field to the right await tester.tapGridFieldWithName('Type'); await tester.tapInsertFieldButton(left: true, name: "Left"); await tester.dismissFieldEditor(); @@ -168,6 +126,26 @@ 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(); @@ -184,10 +162,18 @@ void main() { FieldType.CreatedTime, FieldType.Checkbox, ]) { - await tester.createField(fieldType); + 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(); // After update the field type, the cells should be updated - tester.findCellByFieldType(fieldType); + await tester.findCellByFieldType(fieldType); await tester.pumpAndSettle(); } }); @@ -204,7 +190,15 @@ void main() { FieldType.Checklist, FieldType.URL, ]) { - await tester.createField(fieldType); + // 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(); // open the field editor await tester.tapGridFieldWithName(fieldType.i18n); @@ -224,7 +218,11 @@ void main() { await tester.scrollToRight(find.byType(GridPage)); // create a number field - await tester.createField(FieldType.Number); + await tester.tapNewPropertyButton(); + await tester.renameField("Number"); + await tester.tapSwitchFieldTypeButton(); + await tester.selectFieldType(FieldType.Number); + await tester.dismissFieldEditor(); // enter some data into the first number cell await tester.editCell( @@ -245,7 +243,7 @@ void main() { ); // open editor and change number format - await tester.tapGridFieldWithName(FieldType.Number.i18n); + await tester.tapGridFieldWithName('Number'); await tester.tapEditFieldButton(); await tester.changeNumberFieldFormat(); await tester.dismissFieldEditor(); @@ -278,6 +276,7 @@ void main() { matching: find.byType(TextField), ); await tester.enterText(inputField, text); + await tester.pumpAndSettle(); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(const Duration(milliseconds: 500)); @@ -293,7 +292,11 @@ void main() { await tester.scrollToRight(find.byType(GridPage)); // create a date field - await tester.createField(FieldType.DateTime); + await tester.tapNewPropertyButton(); + await tester.renameField(FieldType.DateTime.i18n); + await tester.tapSwitchFieldTypeButton(); + await tester.selectFieldType(FieldType.DateTime); + await tester.dismissFieldEditor(); // edit the first date cell await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); @@ -324,30 +327,6 @@ void main() { ); }); - testWidgets('text in viewport while typing', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.changeCalculateAtIndex(0, CalculationType.Count); - - // add very large text with 200 lines - final largeText = List.generate( - 200, - (index) => 'Line ${index + 1}', - ).join('\n'); - - await tester.editCell( - rowIndex: 2, - fieldType: FieldType.RichText, - input: largeText, - ); - - // checks if last line is in view port - tester.expectToSeeText('Line 200'); - }); - // Disable this test because it fails on CI randomly // testWidgets('last modified and created at field type options', // (tester) async { @@ -413,188 +392,5 @@ void main() { // content: DateFormat('dd/MM/y hh:mm a').format(modified), // ); // }); - - testWidgets('select option transform', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Grid, - ); - - // invoke the field editor of existing Single-Select field Type - await tester.tapGridFieldWithName('Type'); - await tester.tapEditFieldButton(); - - // add some select options - await tester.tapAddSelectOptionButton(); - for (final optionName in ['A', 'B', 'C']) { - final inputField = find.descendant( - of: find.byType(CreateOptionTextField), - matching: find.byType(TextField), - ); - await tester.enterText(inputField, optionName); - await tester.testTextInput.receiveAction(TextInputAction.done); - } - await tester.dismissFieldEditor(); - - // select A in first row's cell under the Type field - await tester.tapCellInGrid( - rowIndex: 0, - fieldType: FieldType.SingleSelect, - ); - await tester.selectOption(name: 'A'); - await tester.dismissCellEditor(); - tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0); - - await tester.changeFieldTypeOfFieldWithName('Type', FieldType.RichText); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.RichText, - content: "A", - cellIndex: 1, - ); - - // add some random text in the second row - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.RichText, - input: "random", - cellIndex: 1, - ); - tester.assertCellContent( - rowIndex: 1, - fieldType: FieldType.RichText, - content: "random", - cellIndex: 1, - ); - - await tester.changeFieldTypeOfFieldWithName( - 'Type', - FieldType.SingleSelect, - ); - tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0); - tester.assertNumberOfSelectedOptionsInGrid( - rowIndex: 1, - matcher: findsNothing, - ); - - // create a new field for testing - await tester.createField(FieldType.RichText, name: 'Test'); - - // edit the first 2 rows - await tester.editCell( - rowIndex: 0, - fieldType: FieldType.RichText, - input: "E,F", - cellIndex: 1, - ); - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.RichText, - input: "G", - cellIndex: 1, - ); - - await tester.changeFieldTypeOfFieldWithName( - 'Test', - FieldType.MultiSelect, - ); - tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0); - tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1); - - await tester.tapCellInGrid( - rowIndex: 2, - fieldType: FieldType.MultiSelect, - ); - await tester.selectOption(name: 'G'); - await tester.createOption(name: 'H'); - await tester.dismissCellEditor(); - tester.findSelectOptionWithNameInGrid(name: 'A', rowIndex: 0); - tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2); - - await tester.changeFieldTypeOfFieldWithName( - 'Test', - FieldType.RichText, - ); - tester.assertCellContent( - rowIndex: 2, - fieldType: FieldType.RichText, - content: "G,H", - cellIndex: 1, - ); - await tester.changeFieldTypeOfFieldWithName( - 'Test', - FieldType.MultiSelect, - ); - - tester.assertMultiSelectOption(contents: ['E', 'F'], rowIndex: 0); - tester.assertMultiSelectOption(contents: ['G'], rowIndex: 1); - tester.assertMultiSelectOption(contents: ['G', 'H'], rowIndex: 2); - }); - - testWidgets('date time transform', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.scrollToRight(find.byType(GridPage)); - - // create a date field - await tester.createField(FieldType.DateTime); - - // edit the first date cell - await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); - final now = DateTime.now(); - await tester.toggleIncludeTime(); - await tester.selectDay(content: now.day); - - await tester.dismissCellEditor(); - - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y HH:mm').format(now), - ); - - await tester.changeFieldTypeOfFieldWithName( - 'Date', - FieldType.RichText, - ); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.RichText, - content: DateFormat('MMM dd, y HH:mm').format(now), - cellIndex: 1, - ); - - await tester.editCell( - rowIndex: 1, - fieldType: FieldType.RichText, - input: "Oct 5, 2024", - cellIndex: 1, - ); - tester.assertCellContent( - rowIndex: 1, - fieldType: FieldType.RichText, - content: "Oct 5, 2024", - cellIndex: 1, - ); - - await tester.changeFieldTypeOfFieldWithName( - 'Date', - FieldType.DateTime, - ); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y').format(now), - ); - tester.assertCellContent( - rowIndex: 1, - fieldType: FieldType.DateTime, - content: "Oct 05, 2024", - ); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart index 8e79445503..b6db3e1a62 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart @@ -1,21 +1,17 @@ -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('grid filter:', () { testWidgets('add text filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a filter await tester.tapDatabaseFilterButton(); @@ -23,68 +19,68 @@ void main() { await tester.tapFilterButtonInGrid('Name'); // enter 'A' in the filter text field - tester.assertNumberOfRowsInGridPage(10); + await tester.assertNumberOfRowsInGridPage(10); await tester.enterTextInTextFilter('A'); - tester.assertNumberOfRowsInGridPage(1); + await tester.assertNumberOfRowsInGridPage(1); // after remove the filter, the grid should show all rows await tester.enterTextInTextFilter(''); - tester.assertNumberOfRowsInGridPage(10); + await tester.assertNumberOfRowsInGridPage(10); await tester.enterTextInTextFilter('B'); - tester.assertNumberOfRowsInGridPage(1); + await tester.assertNumberOfRowsInGridPage(1); // open the menu to delete the filter await tester.tapDisclosureButtonInFinder(find.byType(TextFilterEditor)); await tester.tapDeleteFilterButtonInGrid(); - tester.assertNumberOfRowsInGridPage(10); + await tester.assertNumberOfRowsInGridPage(10); await tester.pumpAndSettle(); }); testWidgets('add checkbox filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a filter await tester.tapDatabaseFilterButton(); await tester.tapCreateFilterByFieldType(FieldType.Checkbox, 'Done'); - tester.assertNumberOfRowsInGridPage(5); + await tester.assertNumberOfRowsInGridPage(5); await tester.tapFilterButtonInGrid('Done'); await tester.tapCheckboxFilterButtonInGrid(); await tester.tapUnCheckedButtonOnCheckboxFilter(); - tester.assertNumberOfRowsInGridPage(5); + await tester.assertNumberOfRowsInGridPage(5); await tester .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); await tester.tapDeleteFilterButtonInGrid(); - tester.assertNumberOfRowsInGridPage(10); + await tester.assertNumberOfRowsInGridPage(10); await tester.pumpAndSettle(); }); testWidgets('add checklist filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a filter await tester.tapDatabaseFilterButton(); await tester.tapCreateFilterByFieldType(FieldType.Checklist, 'checklist'); // By default, the condition of checklist filter is 'uncompleted' - tester.assertNumberOfRowsInGridPage(9); + await tester.assertNumberOfRowsInGridPage(9); await tester.tapFilterButtonInGrid('checklist'); await tester.tapChecklistFilterButtonInGrid(); await tester.tapCompletedButtonOnChecklistFilter(); - tester.assertNumberOfRowsInGridPage(1); + await tester.assertNumberOfRowsInGridPage(1); await tester.pumpAndSettle(); }); testWidgets('add single select filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a filter await tester.tapDatabaseFilterButton(); @@ -94,27 +90,27 @@ void main() { // select the option 's6' await tester.tapOptionFilterWithName('s6'); - tester.assertNumberOfRowsInGridPage(0); + await tester.assertNumberOfRowsInGridPage(0); // unselect the option 's6' await tester.tapOptionFilterWithName('s6'); - tester.assertNumberOfRowsInGridPage(10); + await tester.assertNumberOfRowsInGridPage(10); // select the option 's5' await tester.tapOptionFilterWithName('s5'); - tester.assertNumberOfRowsInGridPage(1); + await tester.assertNumberOfRowsInGridPage(1); // select the option 's4' await tester.tapOptionFilterWithName('s4'); // The row with 's4' should be shown. - tester.assertNumberOfRowsInGridPage(2); + await tester.assertNumberOfRowsInGridPage(1); await tester.pumpAndSettle(); }); testWidgets('add multi select filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a filter await tester.tapDatabaseFilterButton(); @@ -128,95 +124,17 @@ void main() { // select the option 'm1'. Any option with 'm1' should be shown. await tester.tapOptionFilterWithName('m1'); - tester.assertNumberOfRowsInGridPage(5); + await tester.assertNumberOfRowsInGridPage(5); await tester.tapOptionFilterWithName('m1'); // select the option 'm2'. Any option with 'm2' should be shown. await tester.tapOptionFilterWithName('m2'); - tester.assertNumberOfRowsInGridPage(4); + await tester.assertNumberOfRowsInGridPage(4); await tester.tapOptionFilterWithName('m2'); // select the option 'm4'. Any option with 'm4' should be shown. await tester.tapOptionFilterWithName('m4'); - tester.assertNumberOfRowsInGridPage(1); - - await tester.pumpAndSettle(); - }); - - testWidgets('add date filter', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType(FieldType.DateTime, 'date'); - - // By default, the condition of date filter is current day and time - tester.assertNumberOfRowsInGridPage(0); - - await tester.tapFilterButtonInGrid('date'); - await tester.changeDateFilterCondition(DateTimeFilterCondition.before); - tester.assertNumberOfRowsInGridPage(7); - - await tester.changeDateFilterCondition(DateTimeFilterCondition.isEmpty); - tester.assertNumberOfRowsInGridPage(3); - - await tester.pumpAndSettle(); - }); - - testWidgets('add timestamp filter', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - await tester.createField( - FieldType.CreatedTime, - name: 'Created at', - ); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.CreatedTime, - 'Created at', - ); - await tester.pumpAndSettle(); - - tester.assertNumberOfRowsInGridPage(3); - - await tester.tapFilterButtonInGrid('Created at'); - await tester.changeDateFilterCondition(DateTimeFilterCondition.before); - tester.assertNumberOfRowsInGridPage(0); - - await tester.pumpAndSettle(); - }); - - testWidgets('create new row when filters don\'t autofill', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.RichText, - 'Name', - ); - tester.assertNumberOfRowsInGridPage(3); - - await tester.tapCreateRowButtonInGrid(); - tester.assertNumberOfRowsInGridPage(4); - - await tester.tapFilterButtonInGrid('Name'); - await tester - .changeTextFilterCondition(TextFilterConditionPB.TextIsNotEmpty); - await tester.dismissCellEditor(); - tester.assertNumberOfRowsInGridPage(0); - - await tester.tapCreateRowButtonInGrid(); - tester.assertNumberOfRowsInGridPage(0); - expect(find.byType(RowDetailPage), findsOneWidget); + await tester.assertNumberOfRowsInGridPage(1); await tester.pumpAndSettle(); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_icon_test.dart deleted file mode 100644 index e6a629ded5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_icon_test.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; -import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; -import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - testWidgets('change icon', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final iconData = await tester.loadIcon(); - - const pageName = 'Database'; - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Grid, - name: pageName, - ); - - /// create board - final addButton = find.byType(AddDatabaseViewButton); - await tester.tapButton(addButton); - await tester.tapButton( - find.text( - '${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}', - findRichText: true, - ), - ); - - /// create calendar - await tester.tapButton(addButton); - await tester.tapButton( - find.text( - '${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}', - findRichText: true, - ), - ); - - final databaseTabBarItem = find.byType(DatabaseTabBarItem); - expect(databaseTabBarItem, findsNWidgets(3)); - final gridItem = databaseTabBarItem.first, - boardItem = databaseTabBarItem.at(1), - calendarItem = databaseTabBarItem.last; - - /// change the icon of grid - /// the first tapping is to select specific item - /// the second tapping is to show the menu - await tester.tapButton(gridItem); - await tester.tapButton(gridItem); - - /// change icon - await tester - .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr())); - await tester.tapIcon(iconData, enableColor: false); - final gridIcon = find.descendant( - of: gridItem, - matching: find.byType(RawEmojiIconWidget), - ); - final gridIconWidget = - gridIcon.evaluate().first.widget as RawEmojiIconWidget; - final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji)); - final gridIconsData = - IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji)); - expect(gridIconsData.iconName, iconsData.iconName); - - /// change the icon of board - await tester.tapButton(boardItem); - await tester.tapButton(boardItem); - await tester - .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr())); - await tester.tapIcon(iconData, enableColor: false); - final boardIcon = find.descendant( - of: boardItem, - matching: find.byType(RawEmojiIconWidget), - ); - final boardIconWidget = - boardIcon.evaluate().first.widget as RawEmojiIconWidget; - final boardIconsData = - IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji)); - expect(boardIconsData.iconName, iconsData.iconName); - - /// change the icon of calendar - await tester.tapButton(calendarItem); - await tester.tapButton(calendarItem); - await tester - .tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr())); - await tester.tapIcon(iconData, enableColor: false); - final calendarIcon = find.descendant( - of: calendarItem, - matching: find.byType(RawEmojiIconWidget), - ); - final calendarIconWidget = - calendarIcon.evaluate().first.widget as RawEmojiIconWidget; - final calendarIconsData = - IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji)); - expect(calendarIconsData.iconName, iconsData.iconName); - }); - - testWidgets('change database icon from sidebar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final iconData = await tester.loadIcon(); - final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄'; - - const pageName = 'Database'; - await tester.createNewPageWithNameUnderParent( - layout: ViewLayoutPB.Grid, - name: pageName, - ); - final viewItem = find.descendant( - of: find.byType(SidebarFolder), - matching: find.byWidgetPredicate( - (w) => w is ViewItem && w.view.name == pageName, - ), - ); - - /// change icon to emoji - await tester.tapButton( - find.descendant( - of: viewItem, - matching: find.byType(FlowySvg), - ), - ); - await tester.tapEmoji(emoji); - final iconWidget = find.descendant( - of: viewItem, - matching: find.byType(RawEmojiIconWidget), - ); - expect( - (iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji, - emoji, - ); - - /// the icon will not be displayed in database item - Finder databaseIcon = find.descendant( - of: find.byType(DatabaseTabBarItem), - matching: find.byType(FlowySvg), - ); - expect( - (databaseIcon.evaluate().first.widget as FlowySvg).svg, - FlowySvgs.icon_grid_s, - ); - - /// change emoji to icon - await tester.tapButton(iconWidget); - await tester.tapIcon(iconData); - expect( - (iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji, - iconData.emoji, - ); - - databaseIcon = find.descendant( - of: find.byType(DatabaseTabBarItem), - matching: find.byType(RawEmojiIconWidget), - ); - final databaseIconWidget = - databaseIcon.evaluate().first.widget as RawEmojiIconWidget; - final databaseIconsData = - IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji)); - expect(icon.svgString, databaseIconsData.svgString); - expect(icon.color, isNotEmpty); - expect(icon.color, databaseIconsData.color); - - /// the icon in database item should not show the color - expect(databaseIconWidget.enableColor, false); - }); -} 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 deleted file mode 100644 index cb24a949bb..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_media_test.dart +++ /dev/null @@ -1,297 +0,0 @@ -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/flowy_svgs.g.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/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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 two times', (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.tapFileUploadHint(); - - // 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.tapFileUploadHint(); - await tester.pumpAndSettle(); - - // Expect two files - expect(find.byType(RenderMedia), findsNWidgets(2)); - - // Remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); - }); - - testWidgets('add two files at once', (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, secondImagePath]); - 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.tapFileUploadHint(); - - // Expect two files - expect(find.byType(RenderMedia), findsNWidgets(2)); - - // Remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); - }); - - testWidgets('delete 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, secondImagePath]); - 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.tapFileUploadHint(); - - // Expect two files - expect(find.byType(RenderMedia), findsNWidgets(2)); - - // Tap on the three dots menu for the first RenderMedia - final mediaMenuFinder = find.descendant( - of: find.byType(RenderMedia), - matching: find.byFlowySvg(FlowySvgs.three_dots_s), - ); - - await tester.tap(mediaMenuFinder.first); - await tester.pumpAndSettle(); - - // Tap on the delete button - await tester.tap(find.text(LocaleKeys.grid_media_delete.tr())); - await tester.pumpAndSettle(); - - // Tap on Delete button in the confirmation dialog - await tester.tap( - find.descendant( - of: find.byType(SpaceCancelOrConfirmButton), - matching: find.text(LocaleKeys.grid_media_delete.tr()), - ), - ); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - // Expect one file - expect(find.byType(RenderMedia), findsOneWidget); - - // Remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); - }); - - testWidgets('show file names', (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, secondImagePath]); - 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.tapFileUploadHint(); - - // Expect two files - expect(find.byType(RenderMedia), findsNWidgets(2)); - - await tester.dismissCellEditor(); - await tester.pumpAndSettle(); - - // Open first row in row detail view then toggle show file names - await tester.openFirstRowDetailPage(); - await tester.pumpAndSettle(); - - // Expect file names to not be shown (hidden) - expect(find.text('sample.jpeg'), findsNothing); - expect(find.text('sample.gif'), findsNothing); - - await tester.tapGridFieldWithNameInRowDetailPage('Type'); - await tester.pumpAndSettle(); - - // Toggle show file names - await tester.tap(find.byType(Toggle)); - await tester.pumpAndSettle(); - - // Expect file names to be shown - expect(find.text('sample.jpeg'), findsOneWidget); - expect(find.text('sample.gif'), findsOneWidget); - - await tester.dismissRowDetailPage(); - await tester.pumpAndSettle(); - - // Remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_cover_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_cover_test.dart deleted file mode 100644 index 8741dcd75f..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_cover_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -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/database/widgets/card/card.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package: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('database row cover', () { - testWidgets('add and remove cover from Row Detail Card', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - // Open first row in row detail view - await tester.openFirstRowDetailPage(); - await tester.pumpAndSettle(); - - // Expect no cover - expect(find.byType(RowCover), findsNothing); - - // Hover on RowBanner to show Add Cover button - await tester.hoverRowBanner(); - - // Click on Add Cover button - await tester.tapAddCoverButton(); - - // Expect a cover to be shown - the default asset cover - expect(find.byType(RowCover), findsOneWidget); - - // Tap on the delete cover button - await tester.tapButton(find.byType(DeleteCoverButton)); - await tester.pumpAndSettle(); - - // Expect no cover to be shown - expect(find.byType(AFImage), findsNothing); - }); - - testWidgets('add and change cover and check in Board', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); - await tester.pumpAndSettle(); - - // Open "Card 1" - await tester.tap(find.text('Card 1'), warnIfMissed: false); - await tester.pumpAndSettle(); - - // Expect no cover - expect(find.byType(RowCover), findsNothing); - - // Hover on RowBanner to show Add Cover button - await tester.hoverRowBanner(); - - // Click on Add Cover button - await tester.tapAddCoverButton(); - - // Expect default cover to be shown - expect(find.byType(RowCover), findsOneWidget); - - // Prepare image for upload from local - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final imagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final file = File(imagePath) - ..writeAsBytesSync(image.buffer.asUint8List()); - - mockPickFilePaths(paths: [imagePath]); - await getIt().set(KVKeys.kCloudType, '0'); - - // Hover on RowBanner to show Change Cover button - await tester.hoverRowBanner(); - - // Tap on the change cover button - await tester.tapButtonWithName( - LocaleKeys.document_plugins_cover_changeCover.tr(), - ); - await tester.pumpAndSettle(); - - // Change tab to Upload tab - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_upload_label.tr(), - ); - - // Tab on the upload button - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ); - - // Expect one cover - expect(find.byType(RowCover), findsOneWidget); - - // Expect the cover to be shown both in RowCover and in CardCover - expect(find.byType(AFImage), findsNWidgets(2)); - - // Dismiss Row Detail Page - await tester.dismissRowDetailPage(); - - // Expect a cover to be shown in CardCover - expect( - find.descendant( - of: find.byType(CardCover), - matching: find.byType(AFImage), - ), - findsOneWidget, - ); - - // Remove the temp file - await Future.wait([file.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 22f059d199..0934e7721b 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,19 +1,10 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; -import 'package:appflowy/plugins/document/document_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/util/field_type_extension.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.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/services.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -22,14 +13,7 @@ import '../../shared/emoji.dart'; import '../../shared/util.dart'; void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('grid row detail page:', () { testWidgets('opens', (tester) async { @@ -49,7 +33,26 @@ void main() { await tester.assertDocumentExistInRowDetailPage(); }); - testWidgets('add and update emoji', (tester) async { + testWidgets('add emoji', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + // Create a new grid + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + + // Hover first row and then open the row page + await tester.openFirstRowDetailPage(); + + await tester.hoverRowBanner(); + + await tester.openEmojiPicker(); + await tester.tapEmoji('😀'); + + // After select the emoji, the EmojiButton will show up + await tester.tapButton(find.byType(EmojiButton)); + }); + + testWidgets('update emoji', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -62,18 +65,8 @@ void main() { await tester.openEmojiPicker(); await tester.tapEmoji('😀'); - // expect to find the emoji selected - final firstEmojiFinder = find.byWidgetPredicate( - (w) => w is FlowyText && w.text == '😀', - ); - - // There are 2 eomjis - one in the row banner and another in the primary cell - expect(firstEmojiFinder, findsNWidgets(2)); - - // Update existing selected emoji - tap on it to update - await tester.tapButton(find.byType(EmojiIconWidget)); - await tester.pumpAndSettle(); - + // Update existing selected emoji + await tester.tapButton(find.byType(EmojiButton)); await tester.tapEmoji('😅'); // The emoji already displayed in the row banner @@ -84,24 +77,6 @@ void main() { // The number of emoji should be two. One in the row displayed in the grid // one in the row detail page. expect(emojiText, findsNWidgets(2)); - - // insert a sub page in database - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_subPage_name.tr(), - offset: 100, - ); - await tester.pumpAndSettle(); - - // the row detail page should be closed - final rowDetailPage = find.byType(RowDetailPage); - await tester.pumpUntilNotFound(rowDetailPage); - - // expect to see a document page - final documentPage = find.byType(DocumentPage); - expect(documentPage, findsOneWidget); }); testWidgets('remove emoji', (tester) async { @@ -118,9 +93,7 @@ void main() { await tester.tapEmoji('😀'); // Remove the emoji - await tester.tapButton(find.byType(EmojiIconWidget)); - await tester.tapButton(find.text(LocaleKeys.button_remove.tr())); - + await tester.tapButton(find.byType(RemoveEmojiButton)); final emojiText = find.byWidgetPredicate( (widget) => widget is FlowyText && widget.text == '😀', ); @@ -148,24 +121,15 @@ 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 - tester.findCellByFieldType(fieldType); + await tester.findCellByFieldType(fieldType); await tester.scrollRowDetailByOffset(const Offset(0, -50)); } }); @@ -345,7 +309,7 @@ void main() { await tester.tapRowDetailPageDeleteRowButton(); await tester.tapEscButton(); - tester.assertNumberOfRowsInGridPage(2); + await tester.assertNumberOfRowsInGridPage(2); }); testWidgets('duplicate row', (tester) async { @@ -362,147 +326,7 @@ void main() { await tester.tapRowDetailPageDuplicateRowButton(); await tester.tapEscButton(); - tester.assertNumberOfRowsInGridPage(4); - }); - - testWidgets('edit checklist cell', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - const fieldType = FieldType.Checklist; - await tester.createField(fieldType); - - await tester.openFirstRowDetailPage(); - await tester.hoverOnWidget( - find.byType(ChecklistRowDetailCell), - onHover: () async { - await tester.tapButton(find.byType(ChecklistItemControl)); - }, - ); - - tester.assertPhantomChecklistItemAtIndex(index: 0); - await tester.enterText(find.byType(PhantomChecklistItem), 'task 1'); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - tester.assertChecklistTaskInEditor( - index: 0, - name: "task 1", - isChecked: false, - ); - tester.assertPhantomChecklistItemAtIndex(index: 1); - tester.assertPhantomChecklistItemContent(""); - - await tester.enterText(find.byType(PhantomChecklistItem), 'task 2'); - await tester.pumpAndSettle(); - await tester.hoverOnWidget( - find.byType(ChecklistRowDetailCell), - onHover: () async { - await tester.tapButton(find.byType(ChecklistItemControl)); - }, - ); - - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 2", - isChecked: false, - ); - tester.assertPhantomChecklistItemAtIndex(index: 2); - tester.assertPhantomChecklistItemContent(""); - - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - expect(find.byType(PhantomChecklistItem), findsNothing); - - await tester.renameChecklistTask(index: 0, name: "task -1", enter: false); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - - tester.assertChecklistTaskInEditor( - index: 0, - name: "task -1", - isChecked: false, - ); - tester.assertPhantomChecklistItemAtIndex(index: 1); - - await tester.enterText(find.byType(PhantomChecklistItem), 'task 0'); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - tester.assertPhantomChecklistItemAtIndex(index: 2); - - await tester.checkChecklistTask(index: 1); - expect(find.byType(PhantomChecklistItem), findsNothing); - expect(find.byType(ChecklistItem), findsNWidgets(3)); - - tester.assertChecklistTaskInEditor( - index: 0, - name: "task -1", - isChecked: false, - ); - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 0", - isChecked: true, - ); - tester.assertChecklistTaskInEditor( - index: 2, - name: "task 2", - isChecked: false, - ); - - await tester.tapButton( - find.descendant( - of: find.byType(ProgressAndHideCompleteButton), - matching: find.byType(FlowyIconButton), - ), - ); - expect(find.byType(ChecklistItem), findsNWidgets(2)); - - tester.assertChecklistTaskInEditor( - index: 0, - name: "task -1", - isChecked: false, - ); - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 2", - isChecked: false, - ); - - await tester.renameChecklistTask(index: 1, name: "task 3", enter: false); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - - await tester.renameChecklistTask(index: 0, name: "task 1", enter: false); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - - await tester.enterText(find.byType(PhantomChecklistItem), 'task 2'); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); - - tester.assertChecklistTaskInEditor( - index: 0, - name: "task 1", - isChecked: false, - ); - tester.assertChecklistTaskInEditor( - index: 1, - name: "task 2", - isChecked: false, - ); - tester.assertChecklistTaskInEditor( - index: 2, - name: "task 3", - isChecked: false, - ); - tester.assertPhantomChecklistItemAtIndex(index: 2); - - await tester.checkChecklistTask(index: 1); - expect(find.byType(ChecklistItem), findsNWidgets(2)); + await tester.assertNumberOfRowsInGridPage(4); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart new file mode 100644 index 0000000000..9865a47a6e --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_test.dart @@ -0,0 +1,66 @@ +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('grid', () { + testWidgets('create row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + await tester.tapCreateRowButtonInGrid(); + + // 3 initial rows + 1 created + await tester.assertNumberOfRowsInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('create row from row menu of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + + await tester.hoverOnFirstRowOfGrid(); + + await tester.tapCreateRowButtonInRowMenuOfGrid(); + + // 3 initial rows + 1 created + await tester.assertNumberOfRowsInGridPage(4); + await tester.pumpAndSettle(); + }); + + testWidgets('delete row of the grid', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + await tester.hoverOnFirstRowOfGrid(() async { + // Open the row menu and then click the delete + await tester.tapRowMenuButtonInGrid(); + await tester.pumpAndSettle(); + await tester.tapDeleteOnRowMenu(); + await tester.pumpAndSettle(); + + // 3 initial rows - 1 deleted + await tester.assertNumberOfRowsInGridPage(2); + }); + }); + + testWidgets('check number of row indicator in the initial grid', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + + await tester.pumpAndSettle(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart index 2beb74a5f2..bd3adff7cc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_share_test.dart @@ -9,7 +9,7 @@ void main() { group('database', () { testWidgets('import v0.2.0 database data', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // wait the database data is loaded await tester.pumpAndSettle(const Duration(microseconds: 500)); @@ -115,7 +115,7 @@ void main() { [], ]; for (final (index, contents) in multiSelectCells.indexed) { - tester.assertMultiSelectOption( + await tester.assertMultiSelectOption( rowIndex: index, contents: contents, ); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart index e09d8718be..e28072cebc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_sort_test.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -8,9 +7,9 @@ import '../../shared/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('grid sort:', () { - testWidgets('text sort', (tester) async { - await tester.openTestDatabase(v020GridFileName); + group('grid', () { + testWidgets('add text sort', (tester) async { + await tester.openV020database(); // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); @@ -38,7 +37,7 @@ void main() { // open the sort menu and select order by descending await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Name'); + await tester.tapSortButtonByName('Name'); await tester.tapSortByDescending(); for (final (index, content) in [ 'E', @@ -61,7 +60,7 @@ void main() { // delete all sorts await tester.tapSortMenuInSettingBar(); - await tester.tapDeleteAllSortsButton(); + await tester.tapAllSortButton(); // check the text cell order for (final (index, content) in [ @@ -85,8 +84,8 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('checkbox', (tester) async { - await tester.openTestDatabase(v020GridFileName); + testWidgets('add checkbox sort', (tester) async { + await tester.openV020database(); // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); @@ -112,7 +111,7 @@ void main() { // open the sort menu and select order by descending await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Done'); + await tester.tapSortButtonByName('Done'); await tester.tapSortByDescending(); for (final (index, content) in [ true, @@ -135,8 +134,8 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('number', (tester) async { - await tester.openTestDatabase(v020GridFileName); + testWidgets('add number sort', (tester) async { + await tester.openV020database(); // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Number, 'number'); @@ -163,7 +162,7 @@ void main() { // open the sort menu and select order by descending await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('number'); + await tester.tapSortButtonByName('number'); await tester.tapSortByDescending(); for (final (index, content) in [ '12', @@ -187,15 +186,15 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('checkbox and number', (tester) async { - await tester.openTestDatabase(v020GridFileName); + testWidgets('add checkbox and number sort', (tester) async { + await tester.openV020database(); // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); // open the sort menu and sort checkbox by descending await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Done'); + await tester.tapSortButtonByName('Done'); await tester.tapSortByDescending(); for (final (index, content) in [ true, @@ -221,7 +220,7 @@ void main() { FieldType.Number, 'number', ); - await tester.tapEditSortConditionButtonByFieldName('number'); + await tester.tapSortButtonByName('number'); await tester.tapSortByDescending(); // check checkbox cell order @@ -267,14 +266,14 @@ void main() { }); testWidgets('reorder sort', (tester) async { - await tester.openTestDatabase(v020GridFileName); + await tester.openV020database(); // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); // open the sort menu and sort checkbox by descending await tester.tapSortMenuInSettingBar(); - await tester.tapEditSortConditionButtonByFieldName('Done'); + await tester.tapSortButtonByName('Done'); await tester.tapSortByDescending(); // add another sort, this time by number descending @@ -283,7 +282,7 @@ void main() { FieldType.Number, 'number', ); - await tester.tapEditSortConditionButtonByFieldName('number'); + await tester.tapSortButtonByName('number'); await tester.tapSortByDescending(); // check checkbox cell order @@ -371,101 +370,5 @@ void main() { ); } }); - - testWidgets('edit field', (tester) async { - await tester.openTestDatabase(v020GridFileName); - - // create a number sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.Number, 'number'); - - // check the number cell order - for (final (index, content) in [ - '-2', - '-1', - '0.1', - '0.2', - '1', - '2', - '10', - '11', - '12', - '', - ].indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.Number, - content: content, - ); - } - - final textCells = [ - 'B', - 'A', - 'C', - 'D', - 'E', - '', - '', - '', - '', - '', - ]; - for (final (index, content) in textCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - - // edit the name of the number field - await tester.tapGridFieldWithName('number'); - - await tester.renameField('hello world'); - await tester.dismissFieldEditor(); - - await tester.tapGridFieldWithName('hello world'); - await tester.dismissFieldEditor(); - - // expect name to be changed as well - await tester.tapSortMenuInSettingBar(); - final sortItem = find.ancestor( - of: find.text('hello world'), - matching: find.byType(DatabaseSortItem), - ); - expect(sortItem, findsOneWidget); - - // change the field type of the field to checkbox - await tester.tapGridFieldWithName('hello world'); - await tester.changeFieldTypeOfFieldWithName( - 'hello world', - FieldType.Checkbox, - ); - - // expect name to be changed as well - await tester.tapSortMenuInSettingBar(); - expect(sortItem, findsOneWidget); - - final newTextCells = [ - 'A', - 'B', - 'C', - 'D', - 'E', - '', - '', - '', - '', - '', - ]; - for (final (index, content) in newTextCells.indexed) { - tester.assertCellContent( - rowIndex: index, - fieldType: FieldType.RichText, - content: content, - ); - } - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_1.dart deleted file mode 100644 index 3a5854bc1b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_1.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'database_cell_test.dart' as database_cell_test; -import 'database_field_settings_test.dart' as database_field_settings_test; -import 'database_field_test.dart' as database_field_test; -import 'database_row_page_test.dart' as database_row_page_test; -import 'database_setting_test.dart' as database_setting_test; -import 'database_share_test.dart' as database_share_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - database_cell_test.main(); - database_field_test.main(); - database_field_settings_test.main(); - database_share_test.main(); - database_row_page_test.main(); - database_setting_test.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_2.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_2.dart deleted file mode 100644 index 26b64af495..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_test_runner_2.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'database_calendar_test.dart' as database_calendar_test; -import 'database_filter_test.dart' as database_filter_test; -import 'database_media_test.dart' as database_media_test; -import 'database_row_cover_test.dart' as database_row_cover_test; -import 'database_share_test.dart' as database_share_test; -import 'database_sort_test.dart' as database_sort_test; -import 'database_view_test.dart' as database_view_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - database_filter_test.main(); - database_sort_test.main(); - database_view_test.main(); - database_calendar_test.main(); - database_media_test.main(); - database_row_cover_test.main(); - database_share_test.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart index 71656c1ea6..e35c9cc9d8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_view_test.dart @@ -1,10 +1,5 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -78,37 +73,5 @@ void main() { await tester.pumpAndSettle(); }); - - testWidgets('insert grid in column', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// create page and show slash menu - await tester.createNewPageWithNameUnderParent(name: 'test page'); - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - - /// create a column - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_twoColumns.tr(), - ); - final actionList = find.byType(BlockActionList); - expect(actionList, findsNWidgets(2)); - final position = tester.getCenter(actionList.last); - - /// tap the second child of column - await tester.tapAt(position.copyWith(dx: position.dx + 50)); - - /// create a grid - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_grid.tr(), - ); - - final grid = find.byType(GridPageContent); - expect(grid, findsOneWidget); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index 1a8a3fcda8..d95d907881 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -27,9 +27,8 @@ void main() { await tester.pumpAndSettle(); // click the align center - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_center_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); // expect to see the align center final editorState = tester.editor.getCurrentEditorState(); @@ -37,15 +36,13 @@ void main() { expect(first.attributes[blockComponentAlign], 'center'); // click the align right - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_right_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_center_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); expect(first.attributes[blockComponentAlign], 'right'); // click the align left - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_alignment_m); - await tester - .tapButtonWithFlowySvgData(FlowySvgs.toolbar_text_align_left_m); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_right_s); + await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_align_left_s); expect(first.attributes[blockComponentAlign], 'left'); }); @@ -78,7 +75,7 @@ void main() { [ LogicalKeyboardKey.control, LogicalKeyboardKey.shift, - LogicalKeyboardKey.keyC, + LogicalKeyboardKey.keyE, ], tester: tester, withKeyUp: true, diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_app_lifecycle_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_app_lifecycle_test.dart deleted file mode 100644 index fdde8bbeb8..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_app_lifecycle_test.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:ui'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Editor AppLifeCycle tests', () { - testWidgets( - 'Selection is added back after pausing AppFlowy', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final selection = Selection.single(path: [4], startOffset: 0); - await tester.editor.updateSelection(selection); - - binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); - expect(tester.editor.getCurrentEditorState().selection, null); - - binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); - await tester.pumpAndSettle(); - - expect(tester.editor.getCurrentEditorState().selection, selection); - }, - ); - - testWidgets( - 'Null selection is retained after pausing AppFlowy', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final selection = Selection.single(path: [4], startOffset: 0); - await tester.editor.updateSelection(selection); - await tester.editor.updateSelection(null); - - binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); - expect(tester.editor.getCurrentEditorState().selection, null); - - binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); - await tester.pumpAndSettle(); - - expect(tester.editor.getCurrentEditorState().selection, null); - }, - ); - - testWidgets( - 'Non-collapsed selection is retained after pausing AppFlowy', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final selection = Selection( - start: Position(path: [3]), - end: Position(path: [3], offset: 8), - ); - await tester.editor.updateSelection(selection); - - binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); - binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); - await tester.pumpAndSettle(); - - expect(tester.editor.getCurrentEditorState().selection, selection); - }, - ); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_block_option_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_block_option_test.dart deleted file mode 100644 index 76e5dfcb6c..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_block_option_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Block option interaction tests', () { - testWidgets('has correct block selection on tap option button', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // We edit the document by entering some characters, to ensure the document has focus - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [2])), - ); - - // Insert character 'a' three times - easy to identify - await tester.ime.insertText('aaa'); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([2]); - expect(node?.delta?.toPlainText(), startsWith('aaa')); - - final multiSelection = Selection( - start: Position(path: [2], offset: 3), - end: Position(path: [4], offset: 40), - ); - - // Select multiple items - await tester.editor.updateSelection(multiSelection); - await tester.pumpAndSettle(); - - // Press the block option menu - await tester.editor.hoverAndClickOptionMenuButton([2]); - await tester.pumpAndSettle(); - - // Expect the selection to be Block type and not have changed - expect(editorState.selectionType, SelectionType.block); - expect(editorState.selection, multiSelection); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart deleted file mode 100644 index b5449ec622..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_callout_test.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/icon/icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - testWidgets('callout with emoji icon picker', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final emojiIconData = await tester.loadIcon(); - - /// create a new document - await tester.createNewPageWithNameUnderParent(); - - /// tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - - /// create callout - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_callout.tr(), - ); - - /// select an icon - final emojiPickerButton = find.descendant( - of: find.byType(CalloutBlockComponentWidget), - matching: find.byType(EmojiPickerButton), - ); - await tester.tapButton(emojiPickerButton); - await tester.tapIcon(emojiIconData); - - /// verification results - final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji)); - final iconWidget = find - .descendant( - of: emojiPickerButton, - matching: find.byType(IconWidget), - ) - .evaluate() - .first - .widget as IconWidget; - final iconWidgetData = iconWidget.iconsData; - expect(iconWidgetData.svgString, iconData.svgString); - expect(iconWidgetData.iconName, iconData.iconName); - expect(iconWidgetData.groupName, iconData.groupName); - }); -} 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 a498086952..f06a273fac 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'; @@ -13,21 +13,18 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('paste in codeblock:', () { + group('paste in codeblock', () { testWidgets('paste multiple lines in codeblock', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); // create a new document - await tester.createNewPageWithNameUnderParent(name: 'Test Document'); - // focus on the editor - await tester.tapButton(find.byType(AppFlowyEditor)); + await tester.createNewPageWithNameUnderParent(); // mock the clipboard 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); @@ -54,9 +51,7 @@ Future insertCodeBlockInDocument(WidgetTester tester) async { // open the actions menu and insert the codeBlock await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_code.tr(), - offset: 150, + LocaleKeys.document_selectionMenu_codeBlock.tr(), ); - // 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 d1e34edcb5..0ea1391790 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,40 +1,29 @@ -import 'dart:async'; import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.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:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('copy and paste in document:', () { + group('copy and paste in document', () { testWidgets('paste multiple lines at the first line', (tester) async { // mock the clipboard const lines = 3; await tester.pasteContent( plainText: List.generate(lines, (index) => 'line $index').join('\n'), (editorState) { - expect(editorState.document.root.children.length, 1); - final text = - editorState.document.root.children.first.delta!.toPlainText(); - final textLines = text.split('\n'); + expect(editorState.document.root.children.length, 3); for (var i = 0; i < lines; i++) { expect( - textLines[i], + editorState.getNodeAtPath([i])!.delta!.toPlainText(), 'line $i', ); } @@ -127,410 +116,146 @@ void main() { ]); }); }); + }); - testWidgets('paste bulleted list in numbered list', (tester) async { - const inAppJson = - '{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}'; + testWidgets('paste image(png) from memory', (tester) async { + final image = await rootBundle.load('assets/test/images/sample.png'); + final bytes = image.buffer.asUint8List(); + await tester.pasteContent(image: ('png', bytes), (editorState) { + expect(editorState.document.root.children.length, 2); + final node = editorState.getNodeAtPath([0])!; + expect(node.type, ImageBlockKeys.type); + expect(node.attributes[ImageBlockKeys.url], isNotNull); + }); + }); + testWidgets('paste image(jpeg) from memory', (tester) async { + final image = await rootBundle.load('assets/test/images/sample.jpeg'); + final bytes = image.buffer.asUint8List(); + await tester.pasteContent(image: ('jpeg', bytes), (editorState) { + expect(editorState.document.root.children.length, 2); + final node = editorState.getNodeAtPath([0])!; + expect(node.type, ImageBlockKeys.type); + expect(node.attributes[ImageBlockKeys.url], isNotNull); + }); + }); + + testWidgets('paste image(gif) from memory', (tester) async { + // It's not supported yet. + // final image = await rootBundle.load('assets/test/images/sample.gif'); + // final bytes = image.buffer.asUint8List(); + // await tester.pasteContent(image: ('gif', bytes), (editorState) { + // expect(editorState.document.root.children.length, 2); + // final node = editorState.getNodeAtPath([0])!; + // expect(node.type, ImageBlockKeys.type); + // expect(node.attributes[ImageBlockKeys.url], isNotNull); + // }); + }); + + testWidgets( + 'format the selected text to href when pasting url if available', + (tester) async { + const text = 'appflowy'; + const url = 'https://appflowy.io'; await tester.pasteContent( - inAppJson: inAppJson, + plainText: url, beforeTest: (editorState) async { - final transaction = editorState.transaction; - // Insert two numbered list nodes - // 1. Parent One - // 2. - transaction.insertNodes( - [0], - [ - Node( - type: NumberedListBlockKeys.type, - attributes: { - 'delta': [ - {"insert": "One"}, - ], - }, - ), - Node( - type: NumberedListBlockKeys.type, - attributes: {'delta': []}, - ), - ], + await tester.ime.insertText(text); + await tester.editor.updateSelection( + Selection.single( + path: [0], + startOffset: 0, + endOffset: text.length, + ), ); - - // Set the selection to the second numbered list node (which has empty delta) - transaction.afterSelection = Selection.collapsed(Position(path: [1])); - - await editorState.apply(transaction); - await tester.pumpAndSettle(); }, (editorState) { - final secondNode = editorState.getNodeAtPath([1]); - expect(secondNode?.delta?.toPlainText(), 'Hello'); - expect(secondNode?.children.length, 1); - - final childNode = secondNode?.children.first; - expect(childNode?.delta?.toPlainText(), 'World'); - expect(childNode?.type, BulletedListBlockKeys.type); + final node = editorState.getNodeAtPath([0])!; + expect(node.type, ParagraphBlockKeys.type); + expect(node.delta!.toJson(), [ + { + 'insert': text, + 'attributes': {'href': url}, + } + ]); }, ); - }); + }, + ); - 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 { + // https://github.com/AppFlowy-IO/AppFlowy/issues/3263 + testWidgets( + 'paste the image from clipboard when html and image are both available', + (tester) async { + const html = + '''image'''; final image = await rootBundle.load('assets/test/images/sample.png'); final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('png', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); + await tester.pasteContent( + html: html, + image: ('png', bytes), + (editorState) { + 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', + ); + }, + ); + }, + ); - testWidgets('paste image(jpeg) from memory', (tester) async { - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('jpeg', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); - - testWidgets('paste image(gif) from memory', (tester) async { - final image = await rootBundle.load('assets/test/images/sample.gif'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(image: ('gif', bytes), (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotNull); - }); - }); - - testWidgets( - 'format the selected text to href when pasting url if available', - (tester) async { - const text = 'appflowy'; - const url = 'https://appflowy.io'; - await tester.pasteContent( - plainText: url, - beforeTest: (editorState) async { - await tester.ime.insertText(text); - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text.length, - ), - ); - }, - (editorState) { - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - { - 'insert': text, - 'attributes': {'href': url}, - } - ]); - }, - ); - }, - ); - - // https://github.com/AppFlowy-IO/AppFlowy/issues/3263 - testWidgets( - 'paste the image from clipboard when html and image are both available', - (tester) async { - const html = - '''image'''; - final image = await rootBundle.load('assets/test/images/sample.png'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent( - html: html, - image: ('png', bytes), - (editorState) { - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - }, - ); - }, - ); - - testWidgets('paste the html content contains section', (tester) async { - const html = - '''
AppFlowy
Hello World
'''; - await tester.pasteContent(html: html, (editorState) { + testWidgets('paste the html content contains section', (tester) async { + const html = + '''
AppFlowy
Hello World
'''; + await tester.pasteContent( + html: html, + (editorState) { expect(editorState.document.root.children.length, 2); final node1 = editorState.getNodeAtPath([0])!; final node2 = editorState.getNodeAtPath([1])!; expect(node1.type, ParagraphBlockKeys.type); expect(node2.type, ParagraphBlockKeys.type); - }); - }); - - testWidgets('paste the html from google translation', (tester) async { - const html = - '''
new force
Assessment focus: potential motivations, empathy

➢Personality characteristics and potential motivations:
-Reflection of self-worth
-Need to be respected
-Have a unique definition of success
-Be true to your own lifestyle
'''; - await tester.pasteContent(html: html, (editorState) { - expect(editorState.document.root.children.length, 8); - }); - }); - - testWidgets( - 'auto convert url to link preview block', - (tester) async { - const url = 'https://appflowy.io'; - await tester.pasteContent(plainText: url, (editorState) async { - final pasteAsMenu = find.byType(PasteAsMenu); - expect(pasteAsMenu, findsOneWidget); - final bookmarkButton = find.text( - LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(), - ); - await tester.tapButton(bookmarkButton); - // the second one is the paragraph node - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, LinkPreviewBlockKeys.type); - expect(node.attributes[LinkPreviewBlockKeys.url], url); - }); - - // hover on the link preview block - // click the more button - // and select convert to link - await tester.hoverOnWidget( - find.byType(CustomLinkPreviewWidget), - onHover: () async { - /// show menu - final menu = find.byType(CustomLinkPreviewMenu); - expect(menu, findsOneWidget); - await tester.tapButton(menu); - - final convertToLinkButton = find.text( - LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(), - ); - expect(convertToLinkButton, findsOneWidget); - await tester.tapButton(convertToLinkButton); - }, - ); - - final editorState = tester.editor.getCurrentEditorState(); - final textNode = editorState.getNodeAtPath([0])!; - expect(textNode.type, ParagraphBlockKeys.type); - expect(textNode.delta!.toJson(), [ - { - 'insert': url, - 'attributes': {'href': url}, - } - ]); }, ); - - testWidgets( - 'ctrl/cmd+z to undo the auto convert url to link preview block', - (tester) async { - const url = 'https://appflowy.io'; - await tester.pasteContent(plainText: url, (editorState) async { - final pasteAsMenu = find.byType(PasteAsMenu); - expect(pasteAsMenu, findsOneWidget); - final bookmarkButton = find.text( - LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark.tr(), - ); - await tester.tapButton(bookmarkButton); - // the second one is the paragraph node - expect(editorState.document.root.children.length, 1); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, LinkPreviewBlockKeys.type); - expect(node.attributes[LinkPreviewBlockKeys.url], url); - }); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - { - 'insert': url, - 'attributes': {'href': url}, - } - ]); - }, - ); - - testWidgets( - 'paste the nodes start with non-delta node', - (tester) async { - await tester.pasteContent((_) {}); - const text = 'Hello World'; - final editorState = tester.editor.getCurrentEditorState(); - final transaction = editorState.transaction; - // [image_block] - // [paragraph_block] - transaction.insertNodes([ - 0, - ], [ - customImageNode(url: ''), - paragraphNode(text: text), - ]); - await editorState.apply(transaction); - await tester.pumpAndSettle(); - - await tester.editor.tapLineOfEditorAt(0); - // select all and copy - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - - // put the cursor to the end of the paragraph block - await tester.editor.tapLineOfEditorAt(0); - - // paste the content - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - // expect the image and the paragraph block are inserted below the cursor - expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type); - expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type); - expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type); - expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type); - }, - ); - - testWidgets('paste the url without protocol', (tester) async { - // paste the image that from local file - const plainText = '1.jpg'; - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes), - (editorState) { - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotEmpty); - }); - }); - - testWidgets('paste the image url', (tester) async { - const plainText = 'http://example.com/1.jpg'; - final image = await rootBundle.load('assets/test/images/sample.jpeg'); - final bytes = image.buffer.asUint8List(); - await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes), - (editorState) { - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ImageBlockKeys.type); - expect(node.attributes[ImageBlockKeys.url], isNotEmpty); - }); - }); - - const testMarkdownText = ''' -# I'm h1 -## I'm h2 -### I'm h3 -#### I'm h4 -##### I'm h5 -###### I'm h6'''; - - testWidgets('paste markdowns', (tester) async { - await tester.pasteContent( - plainText: testMarkdownText, - (editorState) { - final children = editorState.document.root.children; - expect(children.length, 6); - for (int i = 1; i <= children.length; i++) { - final text = children[i - 1].delta!.toPlainText(); - expect(text, 'I\'m h$i'); - } - }, - ); - }); - - testWidgets('paste markdowns as plain', (tester) async { - await tester.pasteContent( - plainText: testMarkdownText, - pasteAsPlain: true, - (editorState) { - final children = editorState.document.root.children; - expect(children.length, 6); - for (int i = 1; i <= children.length; i++) { - final text = children[i - 1].delta!.toPlainText(); - final expectText = '${'#' * i} I\'m h$i'; - expect(text, expectText); - } - }, - ); - }); }); + + testWidgets('paste the html from google translation', (tester) async { + const html = + '''
new force
Assessment focus: potential motivations, empathy

➢Personality characteristics and potential motivations:
-Reflection of self-worth
-Need to be respected
-Have a unique definition of success
-Be true to your own lifestyle
'''; + await tester.pasteContent( + html: html, + (editorState) { + expect(editorState.document.root.children.length, 8); + }, + ); + }); + + testWidgets( + 'auto convert url to link preview block', + (widgetTester) async { + const url = 'https://appflowy.io'; + await widgetTester.pasteContent( + plainText: url, + (editorState) { + expect(editorState.document.root.children.length, 2); + final node = editorState.getNodeAtPath([0])!; + expect(node.type, LinkPreviewBlockKeys.type); + expect(node.attributes[LinkPreviewBlockKeys.url], url); + }, + ); + }, + ); } extension on WidgetTester { Future pasteContent( - FutureOr Function(EditorState editorState) test, { + void Function(EditorState editorState) test, { Future Function(EditorState editorState)? beforeTest, String? plainText, String? html, - String? inAppJson, - bool pasteAsPlain = false, (String, Uint8List?)? image, }) async { await initializeAppFlowy(); @@ -538,8 +263,6 @@ extension on WidgetTester { // create a new document await createNewPageWithNameUnderParent(); - // tap the editor - await tapButton(find.byType(AppFlowyEditor)); await beforeTest?.call(editor.getCurrentEditorState()); @@ -548,7 +271,6 @@ extension on WidgetTester { ClipboardServiceData( plainText: plainText, html: html, - inAppJson: inAppJson, image: image, ), ); @@ -557,11 +279,10 @@ extension on WidgetTester { await simulateKeyEvent( LogicalKeyboardKey.keyV, isControlPressed: Platform.isLinux || Platform.isWindows, - isShiftPressed: pasteAsPlain, isMetaPressed: Platform.isMacOS, ); - await pumpAndSettle(const Duration(milliseconds: 1000)); + await pumpAndSettle(); - await test(editor.getCurrentEditorState()); + test(editor.getCurrentEditorState()); } } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart index c2e00a4b48..cf45afc828 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_create_and_delete_test.dart @@ -1,4 +1,6 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -13,15 +15,14 @@ void main() { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - final finder = find.text(gettingStarted, findRichText: true); - await tester.pumpUntilFound(finder, timeout: const Duration(seconds: 2)); // create a new document - const pageName = 'Test Document'; - await tester.createNewPageWithNameUnderParent(name: pageName); + await tester.createNewPageWithNameUnderParent(); // expect to see a new document - tester.expectToSeePageName(pageName); + tester.expectToSeePageName( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ); // and with one paragraph block expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart deleted file mode 100644 index 5cbb133f9d..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_customer_test.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('customer:', () { - testWidgets('backtick issue - inline code', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const pageName = 'backtick issue'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.tap(find.byType(AppFlowyEditor)); - // input backtick - const text = '`Hello` AppFlowy'; - - for (var i = 0; i < text.length; i++) { - await tester.ime.insertCharacter(text[i]); - } - - final node = tester.editor.getNodeAtPath([0]); - expect( - node.delta?.toJson(), - equals([ - { - "insert": "Hello", - "attributes": {"code": true}, - }, - {"insert": " AppFlowy"}, - ]), - ); - }); - - testWidgets('backtick issue - inline code', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const pageName = 'backtick issue'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.tap(find.byType(AppFlowyEditor)); - // input backtick - const text = '```'; - - for (var i = 0; i < text.length; i++) { - await tester.ime.insertCharacter(text[i]); - } - - final node = tester.editor.getNodeAtPath([0]); - expect(node.type, equals(CodeBlockKeys.type)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_deletion_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_deletion_test.dart deleted file mode 100644 index f38138ce8a..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_deletion_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; -import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -import 'document_inline_page_reference_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Document deletion', () { - testWidgets('Trash breadcrumb', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // This test shares behavior with the inline page reference test, thus - // we utilize the same helper functions there. - final name = await createDocumentToReference(tester); - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - await triggerReferenceDocumentBySlashMenu(tester); - - // Search for prefix of document - await enterDocumentText(tester); - - // Select result - final optionFinder = find.descendant( - of: find.byType(InlineActionsHandler), - matching: find.text(name), - ); - - await tester.tap(optionFinder); - await tester.pumpAndSettle(); - - final mentionBlock = find.byType(MentionPageBlock); - expect(mentionBlock, findsOneWidget); - - // Delete the page - await tester.hoverOnPageName( - name, - onHover: () async => tester.tapDeletePageButton(), - ); - await tester.pumpAndSettle(); - - // Navigate to the deleted page from the inline mention - await tester.tap(mentionBlock); - await tester.pumpUntilFound(find.byType(TrashBreadcrumb)); - - expect(find.byType(TrashBreadcrumb), findsOneWidget); - - // Navigate using the trash breadcrumb - await tester.tap( - find.descendant( - of: find.byType(TrashBreadcrumb), - matching: find.text( - LocaleKeys.trash_text.tr(), - ), - ), - ); - await tester.pumpUntilFound(find.text(LocaleKeys.trash_restoreAll.tr())); - - // Restore all - await tester.tap(find.text(LocaleKeys.trash_restoreAll.tr())); - await tester.pumpAndSettle(); - await tester.tap(find.text(LocaleKeys.trash_restore.tr())); - await tester.pumpAndSettle(); - - // Navigate back to the document - await tester.openPage('Getting started'); - await tester.pumpAndSettle(); - - await tester.tap(mentionBlock); - await tester.pumpAndSettle(); - - expect(find.byType(TrashBreadcrumb), findsNothing); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_find_menu_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_find_menu_test.dart deleted file mode 100644 index 6212e7d9cf..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_find_menu_test.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.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:universal_platform/universal_platform.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - String generateRandomString(int len) { - final r = Random(); - return String.fromCharCodes( - List.generate(len, (index) => r.nextInt(33) + 89), - ); - } - - testWidgets( - 'document find menu test', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - // tap editor to get focus - await tester.tapButton(find.byType(AppFlowyEditor)); - - // set clipboard data - final data = [ - "123456\n\n", - ...List.generate(100, (_) => "${generateRandomString(50)}\n\n"), - "1234567\n\n", - ...List.generate(100, (_) => "${generateRandomString(50)}\n\n"), - "12345678\n\n", - ...List.generate(100, (_) => "${generateRandomString(50)}\n\n"), - ].join(); - await getIt().setData( - ClipboardServiceData( - plainText: data, - ), - ); - - // paste - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - // go back to beginning of document - // FIXME: Cannot run Ctrl+F unless selection is on screen - await tester.editor - .updateSelection(Selection.collapsed(Position(path: [0]))); - await tester.pumpAndSettle(); - - expect(find.byType(FindAndReplaceMenuWidget), findsNothing); - - // press cmd/ctrl+F to display the find menu - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyF, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget); - - final textField = find.descendant( - of: find.byType(FindAndReplaceMenuWidget), - matching: find.byType(TextField), - ); - - await tester.enterText( - textField, - "123456", - ); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.text("123456", findRichText: true), - ), - findsOneWidget, - ); - - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.text("1234567", findRichText: true), - ), - findsOneWidget, - ); - - await tester.showKeyboard(textField); - await tester.idle(); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.text("12345678", findRichText: true), - ), - findsOneWidget, - ); - - // tap next button, go back to beginning of document - await tester.tapButton( - find.descendant( - of: find.byType(FindMenu), - matching: find.byFlowySvg(FlowySvgs.arrow_down_s), - ), - ); - - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.text("123456", findRichText: true), - ), - findsOneWidget, - ); - - /// press cmd/ctrl+F to display the find menu - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyF, - isControlPressed: - UniversalPlatform.isLinux || UniversalPlatform.isWindows, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(FindAndReplaceMenuWidget), findsOneWidget); - - /// press esc to dismiss the find menu - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - expect(find.byType(FindAndReplaceMenuWidget), findsNothing); - }, - ); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_sub_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_sub_page_test.dart deleted file mode 100644 index 30e115774a..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_sub_page_test.dart +++ /dev/null @@ -1,382 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/keyboard.dart'; -import '../../shared/util.dart'; - -const _firstDocName = "Inline Sub Page Mention"; -const _createdPageName = "hi world"; - -// Test cases that are covered in this file: -// - [x] Insert sub page mention from action menu (+) -// - [x] Delete sub page mention from editor -// - [x] Delete page from sidebar -// - [x] Delete page from sidebar and then trash -// - [x] Undo delete sub page mention -// - [x] Cut+paste in same document -// - [x] Cut+paste in different document -// - [x] Cut+paste in same document and then paste again in same document -// - [x] Turn paragraph with sub page mention into a heading -// - [x] Turn heading with sub page mention into a paragraph -// - [x] Duplicate a Block containing two sub page mentions - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document inline sub-page mention tests:', () { - testWidgets('Insert (& delete) a sub page mention from action menu', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.insertInlineSubPageFromPlusMenu(); - - await tester.expandOrCollapsePage( - pageName: _firstDocName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Delete from editor - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNothing); - expect(find.byType(MentionSubPageBlock), findsNothing); - - // Undo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Move to trash (delete from sidebar) - await tester.rightClickOnPageName(_createdPageName); - await tester.tapButtonWithName(ViewMoreActionType.delete.name); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsOneWidget); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - expect( - find.text(LocaleKeys.document_mention_trashHint.tr()), - findsOneWidget, - ); - - // Delete from trash - await tester.tapTrashButton(); - await tester.pumpAndSettle(); - - await tester.tap(find.text(LocaleKeys.trash_deleteAll.tr())); - await tester.pumpAndSettle(); - - await tester.tap(find.text(LocaleKeys.button_delete.tr())); - await tester.pumpAndSettle(); - - await tester.openPage(_firstDocName); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNothing); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - expect( - find.text(LocaleKeys.document_mention_deletedPage.tr()), - findsOneWidget, - ); - }); - - testWidgets( - 'Cut+paste in same document and cut+paste in different document', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.insertInlineSubPageFromPlusMenu(); - - await tester.expandOrCollapsePage( - pageName: _firstDocName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Cut from editor - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNothing); - expect(find.byType(MentionSubPageBlock), findsNothing); - - // Paste in same document - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Cut again - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - // Create another document - const anotherDocName = "Another Document"; - await tester.createOpenRenameDocumentUnderParent( - name: anotherDocName, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNothing); - expect(find.byType(MentionSubPageBlock), findsNothing); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - // Paste in document - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpUntilFound(find.byType(MentionSubPageBlock)); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsOneWidget); - - await tester.expandOrCollapsePage( - pageName: anotherDocName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - }); - testWidgets( - 'Cut+paste in same docuemnt and then paste again in same document', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.insertInlineSubPageFromPlusMenu(); - - await tester.expandOrCollapsePage( - pageName: _firstDocName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Cut from editor - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNothing); - expect(find.byType(MentionSubPageBlock), findsNothing); - - // Paste in same document - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Paste again - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsNWidgets(2)); - expect(find.text('$_createdPageName (copy)'), findsNWidgets(2)); - }); - - testWidgets('Turn into w/ sub page mentions', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.insertInlineSubPageFromPlusMenu(); - - await tester.expandOrCollapsePage( - pageName: _firstDocName, - layout: ViewLayoutPB.Document, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - final headingText = LocaleKeys.document_slashMenu_name_heading1.tr(); - final paragraphText = LocaleKeys.document_slashMenu_name_text.tr(); - - // Turn into heading - await tester.editor.openTurnIntoMenu([0]); - await tester.tapButton(find.findTextInFlowyText(headingText)); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - - // Turn into paragraph - await tester.editor.openTurnIntoMenu([0]); - await tester.tapButton(find.findTextInFlowyText(paragraphText)); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsNWidgets(2)); - expect(find.byType(MentionSubPageBlock), findsOneWidget); - }); - - testWidgets('Duplicate a block containing two sub page mentions', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.insertInlineSubPageFromPlusMenu(); - - // Copy paste it - await tester.editor.updateSelection( - Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: 1), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsOneWidget); - expect(find.text("$_createdPageName (copy)"), findsOneWidget); - expect(find.byType(MentionSubPageBlock), findsNWidgets(2)); - - // Duplicate node from block action menu - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr()); - await tester.pumpAndSettle(); - - expect(find.text(_createdPageName), findsOneWidget); - expect(find.text("$_createdPageName (copy)"), findsNWidgets(2)); - expect(find.text("$_createdPageName (copy) (copy)"), findsOneWidget); - }); - - testWidgets('Cancel inline page reference menu by space', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createOpenRenameDocumentUnderParent(name: _firstDocName); - - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showPlusMenu(); - - // Cancel by space - await tester.simulateKeyEvent( - LogicalKeyboardKey.space, - ); - await tester.pumpAndSettle(); - - expect(find.byType(InlineActionsMenu), findsNothing); - }); - }); -} - -extension _InlineSubPageTestHelper on WidgetTester { - Future insertInlineSubPageFromPlusMenu() async { - await editor.tapLineOfEditorAt(0); - - await editor.showPlusMenu(); - - // Workaround to allow typing a document name - await FlowyTestKeyboard.simulateKeyDownEvent( - tester: this, - withKeyUp: true, - [ - LogicalKeyboardKey.keyH, - LogicalKeyboardKey.keyI, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyW, - LogicalKeyboardKey.keyO, - LogicalKeyboardKey.keyR, - LogicalKeyboardKey.keyL, - LogicalKeyboardKey.keyD, - ], - ); - - await FlowyTestKeyboard.simulateKeyDownEvent( - tester: this, - withKeyUp: true, - [LogicalKeyboardKey.enter], - ); - await pumpUntilFound(find.byType(MentionSubPageBlock)); - } -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart deleted file mode 100644 index 39f8bfd4f6..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_link_preview_test.dart +++ /dev/null @@ -1,453 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_link_preview.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:easy_localization/easy_localization.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 '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - const avaliableLink = 'https://appflowy.io/', - unavailableLink = 'www.thereIsNoting.com'; - - Future preparePage(WidgetTester tester, {String? pageName}) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: pageName); - await tester.editor.tapLineOfEditorAt(0); - } - - Future pasteLink(WidgetTester tester, String link) async { - await getIt() - .setData(ClipboardServiceData(plainText: link)); - - /// paste the link - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(Duration(seconds: 1)); - } - - Future pasteAs( - WidgetTester tester, - String link, - PasteMenuType type, { - Duration waitTime = const Duration(milliseconds: 500), - }) async { - await pasteLink(tester, link); - final convertToMentionButton = find.text(type.title); - await tester.tapButton(convertToMentionButton); - await tester.pumpAndSettle(waitTime); - } - - void checkUrl(Node node, String link) { - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - { - 'insert': link, - 'attributes': {'href': link}, - } - ]); - } - - void checkMention(Node node, String link) { - final delta = node.delta!; - final insert = (delta.first as TextInsert).text; - final attributes = delta.first.attributes; - expect(insert, MentionBlockKeys.mentionChar); - final mention = - attributes?[MentionBlockKeys.mention] as Map; - expect(mention[MentionBlockKeys.type], MentionType.externalLink.name); - expect(mention[MentionBlockKeys.url], avaliableLink); - } - - void checkBookmark(Node node, String link) { - expect(node.type, LinkPreviewBlockKeys.type); - expect(node.attributes[LinkPreviewBlockKeys.url], link); - } - - void checkEmbed(Node node, String link) { - expect(node.type, LinkPreviewBlockKeys.type); - expect(node.attributes[LinkEmbedKeys.previewType], LinkEmbedKeys.embed); - expect(node.attributes[LinkPreviewBlockKeys.url], link); - } - - group('Paste as URL', () { - Future pasteAndTurnInto( - WidgetTester tester, - String link, - String title, - ) async { - await pasteLink(tester, link); - final convertToLinkButton = find - .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr()); - await tester.tapButton(convertToLinkButton); - - /// hover link and turn into mention - await tester.hoverOnWidget( - find.byType(LinkHoverTrigger), - onHover: () async { - final turnintoButton = find.byFlowySvg(FlowySvgs.turninto_m); - await tester.tapButton(turnintoButton); - final convertToButton = find.text(title); - await tester.tapButton(convertToButton); - await tester.pumpAndSettle(Duration(seconds: 1)); - }, - ); - } - - testWidgets('paste a link', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteLink(tester, link); - final convertToLinkButton = find - .text(LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr()); - await tester.tapButton(convertToLinkButton); - final node = tester.editor.getNodeAtPath([0]); - checkUrl(node, link); - }); - - testWidgets('paste a link and turn into mention', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAndTurnInto( - tester, - link, - LinkConvertMenuCommand.toMention.title, - ); - - /// check metion values - final node = tester.editor.getNodeAtPath([0]); - checkMention(node, link); - }); - - testWidgets('paste a link and turn into bookmark', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAndTurnInto( - tester, - link, - LinkConvertMenuCommand.toBookmark.title, - ); - - /// check metion values - final node = tester.editor.getNodeAtPath([0]); - checkBookmark(node, link); - }); - - testWidgets('paste a link and turn into embed', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAndTurnInto( - tester, - link, - LinkConvertMenuCommand.toEmbed.title, - ); - - /// check metion values - final node = tester.editor.getNodeAtPath([0]); - checkEmbed(node, link); - }); - }); - - group('Paste as Mention', () { - Future pasteAsMention(WidgetTester tester, String link) => - pasteAs(tester, link, PasteMenuType.mention); - - String getMentionLink(Node node) { - final insert = node.delta?.first as TextInsert?; - final mention = insert?.attributes?[MentionBlockKeys.mention] - as Map?; - return mention?[MentionBlockKeys.url] ?? ''; - } - - Future hoverMentionAndClick( - WidgetTester tester, - String command, - ) async { - final mentionLink = find.byType(MentionLinkBlock); - expect(mentionLink, findsOneWidget); - await tester.hoverOnWidget( - mentionLink, - onHover: () async { - final errorPreview = find.byType(MentionLinkErrorPreview); - expect(errorPreview, findsOneWidget); - final convertButton = find.byFlowySvg(FlowySvgs.turninto_m); - await tester.tapButton(convertButton); - final menuButton = find.text(command); - await tester.tapButton(menuButton); - }, - ); - } - - testWidgets('paste a link as mention', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - final node = tester.editor.getNodeAtPath([0]); - checkMention(node, link); - }); - - testWidgets('paste as mention and copy link', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - final mentionLink = find.byType(MentionLinkBlock); - expect(mentionLink, findsOneWidget); - await tester.hoverOnWidget( - mentionLink, - onHover: () async { - final preview = find.byType(MentionLinkPreview); - if (!preview.hasFound) { - final copyButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); - await tester.tapButton(copyButton); - } else { - final moreOptionButton = find.byFlowySvg(FlowySvgs.toolbar_more_m); - await tester.tapButton(moreOptionButton); - final copyButton = - find.text(MentionLinktMenuCommand.copyLink.title); - await tester.tapButton(copyButton); - } - }, - ); - final clipboardContent = await getIt().getData(); - expect(clipboardContent.plainText, link); - }); - - testWidgets('paste as error mention and turninto url', (tester) async { - String link = unavailableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - Node node = tester.editor.getNodeAtPath([0]); - link = getMentionLink(node); - await hoverMentionAndClick( - tester, - MentionLinktErrorMenuCommand.toURL.title, - ); - node = tester.editor.getNodeAtPath([0]); - checkUrl(node, link); - }); - - testWidgets('paste as error mention and turninto embed', (tester) async { - String link = unavailableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - Node node = tester.editor.getNodeAtPath([0]); - link = getMentionLink(node); - await hoverMentionAndClick( - tester, - MentionLinktErrorMenuCommand.toEmbed.title, - ); - node = tester.editor.getNodeAtPath([0]); - checkEmbed(node, link); - }); - - testWidgets('paste as error mention and turninto bookmark', (tester) async { - String link = unavailableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - Node node = tester.editor.getNodeAtPath([0]); - link = getMentionLink(node); - await hoverMentionAndClick( - tester, - MentionLinktErrorMenuCommand.toBookmark.title, - ); - node = tester.editor.getNodeAtPath([0]); - checkBookmark(node, link); - }); - - testWidgets('paste as error mention and remove link', (tester) async { - String link = unavailableLink; - await preparePage(tester); - await pasteAsMention(tester, link); - Node node = tester.editor.getNodeAtPath([0]); - link = getMentionLink(node); - await hoverMentionAndClick( - tester, - MentionLinktErrorMenuCommand.removeLink.title, - ); - node = tester.editor.getNodeAtPath([0]); - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - {'insert': link}, - ]); - }); - }); - - group('Paste as Bookmark', () { - Future pasteAsBookmark(WidgetTester tester, String link) => - pasteAs(tester, link, PasteMenuType.bookmark); - - Future hoverAndClick( - WidgetTester tester, - LinkPreviewMenuCommand command, - ) async { - final bookmark = find.byType(CustomLinkPreviewBlockComponent); - expect(bookmark, findsOneWidget); - await tester.hoverOnWidget( - bookmark, - onHover: () async { - final menuButton = find.byFlowySvg(FlowySvgs.toolbar_more_m); - await tester.tapButton(menuButton); - final commandButton = find.text(command.title); - await tester.tapButton(commandButton); - }, - ); - } - - testWidgets('paste a link as bookmark', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - final node = tester.editor.getNodeAtPath([0]); - checkBookmark(node, link); - }); - - testWidgets('paste a link as bookmark and convert to mention', - (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.convertToMention); - final node = tester.editor.getNodeAtPath([0]); - checkMention(node, link); - }); - - testWidgets('paste a link as bookmark and convert to url', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.convertToUrl); - final node = tester.editor.getNodeAtPath([0]); - checkUrl(node, link); - }); - - testWidgets('paste a link as bookmark and convert to embed', - (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.convertToEmbed); - final node = tester.editor.getNodeAtPath([0]); - checkEmbed(node, link); - }); - - testWidgets('paste a link as bookmark and copy link', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.copyLink); - final clipboardContent = await getIt().getData(); - expect(clipboardContent.plainText, link); - }); - - testWidgets('paste a link as bookmark and replace link', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.replace); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.simulateKeyEvent(LogicalKeyboardKey.delete); - await tester.enterText(find.byType(TextFormField), unavailableLink); - await tester.tapButton(find.text(LocaleKeys.button_replace.tr())); - final node = tester.editor.getNodeAtPath([0]); - checkBookmark(node, unavailableLink); - }); - - testWidgets('paste a link as bookmark and remove link', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsBookmark(tester, link); - await hoverAndClick(tester, LinkPreviewMenuCommand.removeLink); - final node = tester.editor.getNodeAtPath([0]); - expect(node.type, ParagraphBlockKeys.type); - expect(node.delta!.toJson(), [ - {'insert': link}, - ]); - }); - }); - group('Paste as Embed', () { - Future pasteAsEmbed(WidgetTester tester, String link) => - pasteAs(tester, link, PasteMenuType.embed); - - Future hoverAndConvert( - WidgetTester tester, - LinkEmbedConvertCommand command, - ) async { - final embed = find.byType(LinkEmbedBlockComponent); - expect(embed, findsOneWidget); - await tester.hoverOnWidget( - embed, - onHover: () async { - final menuButton = find.byFlowySvg(FlowySvgs.turninto_m); - await tester.tapButton(menuButton); - final commandButton = find.text(command.title); - await tester.tapButton(commandButton); - }, - ); - } - - testWidgets('paste a link as embed', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsEmbed(tester, link); - final node = tester.editor.getNodeAtPath([0]); - checkEmbed(node, link); - }); - - testWidgets('paste a link as bookmark and convert to mention', - (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, LinkEmbedConvertCommand.toMention); - final node = tester.editor.getNodeAtPath([0]); - checkMention(node, link); - }); - - testWidgets('paste a link as bookmark and convert to url', (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, LinkEmbedConvertCommand.toURL); - final node = tester.editor.getNodeAtPath([0]); - checkUrl(node, link); - }); - - testWidgets('paste a link as bookmark and convert to bookmark', - (tester) async { - final link = avaliableLink; - await preparePage(tester); - await pasteAsEmbed(tester, link); - await hoverAndConvert(tester, LinkEmbedConvertCommand.toBookmark); - final node = tester.editor.getNodeAtPath([0]); - checkBookmark(node, link); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart index eeb2ea3925..d4cc11d7f0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -1,10 +1,4 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -37,104 +31,4 @@ void main() { expect(pageFinder, findsNWidgets(1)); }); }); - - testWidgets('count title towards word count', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(); - - Finder title = tester.editor.findDocumentTitle(''); - - await tester.openMoreViewActions(); - final viewMetaInfo = find.byType(ViewMetaInfo); - expect(viewMetaInfo, findsOneWidget); - - ViewMetaInfo viewMetaInfoWidget = - viewMetaInfo.evaluate().first.widget as ViewMetaInfo; - Counters titleCounter = viewMetaInfoWidget.titleCounters!; - - expect(titleCounter.charCount, 0); - expect(titleCounter.wordCount, 0); - - /// input [str1] within title - const str1 = 'Hello', - str2 = '$str1 AppFlowy', - str3 = '$str2!', - str4 = 'Hello world'; - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.tapButton(title); - await tester.enterText(title, str1); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.openMoreViewActions(); - viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo; - titleCounter = viewMetaInfoWidget.titleCounters!; - expect(titleCounter.charCount, str1.length); - expect(titleCounter.wordCount, 1); - - /// input [str2] within title - title = tester.editor.findDocumentTitle(str1); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.tapButton(title); - await tester.enterText(title, str2); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.openMoreViewActions(); - viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo; - titleCounter = viewMetaInfoWidget.titleCounters!; - expect(titleCounter.charCount, str2.length); - expect(titleCounter.wordCount, 2); - - /// input [str3] within title - title = tester.editor.findDocumentTitle(str2); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.tapButton(title); - await tester.enterText(title, str3); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.openMoreViewActions(); - viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo; - titleCounter = viewMetaInfoWidget.titleCounters!; - expect(titleCounter.charCount, str3.length); - expect(titleCounter.wordCount, 2); - - /// input [str4] within document - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.editor - .updateSelection(Selection.collapsed(Position(path: [0]))); - await tester.pumpAndSettle(); - await tester.editor - .getCurrentEditorState() - .insertTextAtCurrentSelection(str4); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.openMoreViewActions(); - final texts = - find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText)); - expect(texts, findsNWidgets(3)); - viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo; - titleCounter = viewMetaInfoWidget.titleCounters!; - final Counters documentCounters = viewMetaInfoWidget.documentCounters!; - final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText, - charCounter = texts.evaluate().elementAt(1).widget as FlowyText; - final numberFormat = NumberFormat(); - expect( - wordCounter.text, - LocaleKeys.moreAction_wordCount.tr( - args: [ - numberFormat - .format(titleCounter.wordCount + documentCounters.wordCount) - .toString(), - ], - ), - ); - expect( - charCounter.text, - LocaleKeys.moreAction_charCount.tr( - args: [ - numberFormat - .format( - titleCounter.charCount + documentCounters.charCount, - ) - .toString(), - ], - ), - ); - }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart index 6ec12287a8..cfea4381e0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_option_action_test.dart @@ -1,8 +1,3 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.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'; @@ -12,23 +7,9 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // +, ... button beside the block component. - group('block option action:', () { - Future turnIntoBlock( - WidgetTester tester, - Path path, { - required String menuText, - required String afterType, - }) async { - await tester.editor.openTurnIntoMenu(path); - await tester.tapButton( - find.findTextInFlowyText(menuText), - ); - final node = tester.editor.getCurrentEditorState().getNodeAtPath(path); - expect(node?.type, afterType); - } - - testWidgets('''click + to add a block after current selection, - and click + and option key to add a block before current selection''', + group('document with option action button', () { + testWidgets( + 'click + to add a block after current selection, and click + and option key to add a block before current selection', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -59,120 +40,5 @@ void main() { expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), isEmpty); expect(editorState.getNodeAtPath([2])?.delta?.toPlainText(), isEmpty); }); - - testWidgets('turn into - single line', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const name = 'Test Document'; - await tester.createNewPageWithNameUnderParent(name: name); - await tester.openPage(name); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('turn into'); - - // click the block option button to convert it to another blocks - final values = { - LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.editor_bulletedListShortForm.tr(): - BulletedListBlockKeys.type, - LocaleKeys.editor_numberedListShortForm.tr(): - NumberedListBlockKeys.type, - LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, - LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, - LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, - }; - - for (final value in values.entries) { - final menuText = value.key; - final afterType = value.value; - await turnIntoBlock( - tester, - [0], - menuText: menuText, - afterType: afterType, - ); - } - }); - - testWidgets('turn into - multi lines', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const name = 'Test Document'; - await tester.createNewPageWithNameUnderParent(name: name); - await tester.openPage(name); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('turn into 1'); - await tester.ime.insertCharacter('\n'); - await tester.ime.insertText('turn into 2'); - - // click the block option button to convert it to another blocks - final values = { - LocaleKeys.document_slashMenu_name_heading1.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_heading2.tr(): HeadingBlockKeys.type, - LocaleKeys.document_slashMenu_name_heading3.tr(): HeadingBlockKeys.type, - LocaleKeys.editor_bulletedListShortForm.tr(): - BulletedListBlockKeys.type, - LocaleKeys.editor_numberedListShortForm.tr(): - NumberedListBlockKeys.type, - LocaleKeys.document_slashMenu_name_quote.tr(): QuoteBlockKeys.type, - LocaleKeys.editor_checkbox.tr(): TodoListBlockKeys.type, - LocaleKeys.document_slashMenu_name_callout.tr(): CalloutBlockKeys.type, - LocaleKeys.document_slashMenu_name_text.tr(): ParagraphBlockKeys.type, - }; - - for (final value in values.entries) { - final editorState = tester.editor.getCurrentEditorState(); - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [1], offset: 2), - ); - final menuText = value.key; - final afterType = value.value; - await turnIntoBlock( - tester, - [0], - menuText: menuText, - afterType: afterType, - ); - } - }); - - testWidgets( - 'selecting the parent should deselect all the child nodes as well', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const name = 'Test Document'; - await tester.createNewPageWithNameUnderParent(name: name); - await tester.openPage(name); - - // create a nested list - // Item 1 - // Nested Item 1 - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('Item 1'); - await tester.ime.insertCharacter('\n'); - await tester.simulateKeyEvent(LogicalKeyboardKey.tab); - await tester.ime.insertText('Nested Item 1'); - - // select the 'Nested Item 1' and then tap the option button of the 'Item 1' - final editorState = tester.editor.getCurrentEditorState(); - final selection = Selection.collapsed( - Position(path: [0, 0], offset: 1), - ); - editorState.selection = selection; - await tester.pumpAndSettle(); - expect(editorState.selection, selection); - await tester.editor.hoverAndClickOptionMenuButton([0]); - expect(editorState.selection, Selection.collapsed(Position(path: [0]))); - }, - ); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart deleted file mode 100644 index de1cb880a5..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_selection_test.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.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 '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document selection:', () { - testWidgets('select text from start to end by pan gesture ', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - final editor = tester.editor; - final editorState = editor.getCurrentEditorState(); - // insert a paragraph - final transaction = editorState.transaction; - transaction.insertNode( - [0], - paragraphNode( - text: - '''Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.''', - ), - ); - await editorState.apply(transaction); - await tester.pumpAndSettle(Durations.short1); - - final textBlocks = find.byType(AppFlowyRichText); - final topLeft = tester.getTopLeft(textBlocks.at(0)); - - final gesture = await tester.startGesture( - topLeft, - pointer: 7, - ); - await tester.pumpAndSettle(); - - for (var i = 0; i < 10; i++) { - await gesture.moveBy(const Offset(10, 0)); - await tester.pump(Durations.short1); - } - - expect(editorState.selection!.start.offset, 0); - }); - - testWidgets('select and delete text', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// create a new document - await tester.createNewPageWithNameUnderParent(); - - /// input text - final editor = tester.editor; - final editorState = editor.getCurrentEditorState(); - - const inputText = 'Test for text selection and deletion'; - final texts = inputText.split(' '); - await editor.tapLineOfEditorAt(0); - await tester.ime.insertText(inputText); - - /// selecte and delete - int index = 0; - while (texts.isNotEmpty) { - final text = texts.removeAt(0); - await tester.editor.updateSelection( - Selection( - start: Position(path: [0], offset: index), - end: Position(path: [0], offset: index + text.length), - ), - ); - await tester.simulateKeyEvent(LogicalKeyboardKey.delete); - index++; - } - - /// excpete the text value is correct - final node = editorState.getNodeAtPath([0])!; - final nodeText = node.delta?.toPlainText() ?? ''; - expect(nodeText, ' ' * (index - 1)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_shortcuts_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_shortcuts_test.dart deleted file mode 100644 index cf33a66947..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_shortcuts_test.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document shortcuts:', () { - testWidgets('custom cut command', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const pageName = 'Test Document Shortcuts'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.tap(find.byType(AppFlowyEditor)); - - // mock the data - final editorState = tester.editor.getCurrentEditorState(); - final transaction = editorState.transaction; - const text1 = '1. First line'; - const text2 = '2. Second line'; - transaction.insertNodes([ - 0, - ], [ - paragraphNode(text: text1), - paragraphNode(text: text2), - ]); - await editorState.apply(transaction); - await tester.pumpAndSettle(); - - // focus on the end of the first line - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [0], offset: text1.length), - ), - ); - // press the keybinding - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - // check the clipboard - final clipboard = await Clipboard.getData(Clipboard.kTextPlain); - expect( - clipboard?.text, - equals(text1), - ); - - final node = tester.editor.getNodeAtPath([0]); - expect( - node.delta?.toPlainText(), - equals(text2), - ); - - // select the whole line - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text2.length, - ), - ); - - // press the keybinding - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - // all the text should be deleted - expect( - node.delta?.toPlainText(), - equals(''), - ); - - final clipboard2 = await Clipboard.getData(Clipboard.kTextPlain); - expect( - clipboard2?.text, - equals(text2), - ); - }); - - testWidgets( - 'custom copy command - copy whole line when selection is collapsed', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - const pageName = 'Test Document Shortcuts'; - await tester.createNewPageWithNameUnderParent(name: pageName); - - // focus on the editor - await tester.tap(find.byType(AppFlowyEditor)); - - // mock the data - final editorState = tester.editor.getCurrentEditorState(); - final transaction = editorState.transaction; - const text1 = '1. First line'; - transaction.insertNodes([ - 0, - ], [ - paragraphNode(text: text1), - ]); - await editorState.apply(transaction); - await tester.pumpAndSettle(); - - // focus on the end of the first line - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [0], offset: text1.length), - ), - ); - // press the keybinding - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - // check the clipboard - final clipboard = await Clipboard.getData(Clipboard.kTextPlain); - expect( - clipboard?.text, - equals(text1), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart deleted file mode 100644 index 50f0f903bc..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_sub_page_test.dart +++ /dev/null @@ -1,528 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.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 '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -// Test cases for the Document SubPageBlock that needs to be covered: -// - [x] Insert a new SubPageBlock from Slash menu items (Expect it will create a child view under current view) -// - [x] Delete a SubPageBlock from Block Action Menu (Expect the view is moved to trash / deleted) -// - [x] Delete a SubPageBlock with backspace when selected (Expect the view is moved to trash / deleted) -// - [x] Copy+paste a SubPageBlock in same Document (Expect a new view is created under current view with same content and name) -// - [x] Copy+paste a SubPageBlock in different Document (Expect a new view is created under current view with same content and name) -// - [x] Cut+paste a SubPageBlock in same Document (Expect the view to be deleted on Cut, and brought back on Paste) -// - [x] Cut+paste a SubPageBlock in different Document (Expect the view to be deleted on Cut, and brought back on Paste) -// - [x] Undo adding a SubPageBlock (Expect the view to be deleted) -// - [x] Undo delete of a SubPageBlock (Expect the view to be brought back to original position) -// - [x] Redo adding a SubPageBlock (Expect the view to be restored) -// - [x] Redo delete of a SubPageBlock (Expect the view to be moved to trash again) -// - [x] Renaming a child view (Expect the view name to be updated in the document) -// - [x] Deleting a view (to trash) linked to a SubPageBlock deleted the SubPageBlock (Expect the SubPageBlock to be deleted) -// - [x] Duplicating a SubPageBlock node from Action Menu (Expect a new view is created under current view with same content and name + (copy)) -// - [x] Dragging a SubPageBlock node to a new position in the document (Expect everything to be normal) - -/// The defaut page name is empty, if we're looking for a "text" we can look for -/// [LocaleKeys.menuAppHeader_defaultNewPageName] but it won't work for eg. hoverOnPageName -/// as it looks at the text provided instead of the actual displayed text. -/// -const _defaultPageName = ""; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('Document SubPageBlock tests', () { - testWidgets('Insert a new SubPageBlock from Slash menu items', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - expect( - find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()), - findsNWidgets(3), - ); - }); - - testWidgets('Rename and then Delete a SubPageBlock from Block Action Menu', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - await tester.pumpAndSettle(); - - expect(find.text('Child page'), findsNothing); - }); - - testWidgets('Copy+paste a SubPageBlock in same Document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor.hoverAndClickOptionAddButton([0], false); - await tester.editor.tapLineOfEditorAt(1); - - // This is a workaround to allow CTRL+A and CTRL+C to work to copy - // the SubPageBlock as well. - await tester.ime.insertText('ABC'); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.editor.hoverAndClickOptionAddButton([1], false); - await tester.editor.tapLineOfEditorAt(2); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(const Duration(seconds: 5)); - - expect(find.byType(SubPageBlockComponent), findsNWidgets(2)); - expect(find.text('Child page'), findsNWidgets(2)); - expect(find.text('Child page (copy)'), findsNWidgets(2)); - }); - - testWidgets('Copy+paste a SubPageBlock in different Document', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor.hoverAndClickOptionAddButton([0], false); - await tester.editor.tapLineOfEditorAt(1); - - // This is a workaround to allow CTRL+A and CTRL+C to work to copy - // the SubPageBlock as well. - await tester.ime.insertText('ABC'); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2'); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.expandOrCollapsePage( - pageName: 'SubPageBlock-2', - layout: ViewLayoutPB.Document, - ); - - expect(find.byType(SubPageBlockComponent), findsOneWidget); - expect(find.text('Child page'), findsOneWidget); - expect(find.text('Child page (copy)'), findsNWidgets(2)); - }); - - testWidgets('Cut+paste a SubPageBlock in same Document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor - .updateSelection(Selection.single(path: [0], startOffset: 0)); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsNothing); - expect(find.text('Child page'), findsNothing); - - await tester.editor.tapLineOfEditorAt(0); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsOneWidget); - expect(find.text('Child page'), findsNWidgets(2)); - }); - - testWidgets('Cut+paste a SubPageBlock in different Document', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor - .updateSelection(Selection.single(path: [0], startOffset: 0)); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsNothing); - expect(find.text('Child page'), findsNothing); - - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock-2'); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - await tester.expandOrCollapsePage( - pageName: 'SubPageBlock-2', - layout: ViewLayoutPB.Document, - ); - - expect(find.byType(SubPageBlockComponent), findsOneWidget); - expect(find.text('Child page'), findsNWidgets(2)); - expect(find.text('Child page (copy)'), findsNothing); - }); - - testWidgets('Undo delete of a SubPageBlock', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - await tester.pumpAndSettle(); - - expect(find.text('Child page'), findsNothing); - expect(find.byType(SubPageBlockComponent), findsNothing); - - // Since there is no selection active in editor before deleting Node, - // we need to give focus back to the editor - await tester.editor - .updateSelection(Selection.collapsed(Position(path: [0]))); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.text('Child page'), findsNWidgets(2)); - expect(find.byType(SubPageBlockComponent), findsOneWidget); - }); - - // Redo: undoing deleting a subpage block, then redoing to delete it again - // -> Add a subpage block - // -> Delete - // -> Undo - // -> Redo - testWidgets('Redo delete of a SubPageBlock', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(true); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - // Delete - await tester.editor.hoverAndClickOptionMenuButton([1]); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - await tester.pumpAndSettle(); - - expect(find.text('Child page'), findsNothing); - expect(find.byType(SubPageBlockComponent), findsNothing); - - await tester.editor.tapLineOfEditorAt(0); - - // Undo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - expect(find.byType(SubPageBlockComponent), findsOneWidget); - expect(find.text('Child page'), findsNWidgets(2)); - - // Redo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isShiftPressed: true, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsNothing); - expect(find.text('Child page'), findsNothing); - }); - - testWidgets('Delete a view from sidebar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - expect(find.byType(SubPageBlockComponent), findsOneWidget); - - await tester.hoverOnPageName( - 'Child page', - onHover: () async { - await tester.tapDeletePageButton(); - }, - ); - await tester.pumpAndSettle(const Duration(seconds: 1)); - - expect(find.text('Child page'), findsNothing); - expect(find.byType(SubPageBlockComponent), findsNothing); - }); - - testWidgets('Duplicate SubPageBlock from Block Menu', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(); - await tester.renamePageWithSecondary(_defaultPageName, 'Child page'); - expect(find.text('Child page'), findsNWidgets(2)); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - - await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr()); - await tester.pumpAndSettle(); - - expect(find.text('Child page'), findsNWidgets(2)); - expect(find.text('Child page (copy)'), findsNWidgets(2)); - expect(find.byType(SubPageBlockComponent), findsNWidgets(2)); - }); - - testWidgets('Drag SubPageBlock to top of Document', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - await tester.insertSubPageFromSlashMenu(true); - - expect(find.byType(SubPageBlockComponent), findsOneWidget); - - final beforeNode = tester.editor.getNodeAtPath([1]); - - await tester.editor.dragBlock([1], const Offset(20, -45)); - await tester.pumpAndSettle(Durations.long1); - - final afterNode = tester.editor.getNodeAtPath([0]); - - expect(afterNode.type, SubPageBlockKeys.type); - expect(afterNode.type, beforeNode.type); - expect(find.byType(SubPageBlockComponent), findsOneWidget); - }); - - testWidgets('turn into page', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock'); - - final editorState = tester.editor.getCurrentEditorState(); - - // Insert nested list - final transaction = editorState.transaction; - transaction.insertNode( - [0], - bulletedListNode( - text: 'Parent', - children: [ - bulletedListNode(text: 'Child 1'), - bulletedListNode(text: 'Child 2'), - ], - ), - ); - await editorState.apply(transaction); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsNothing); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButtonWithName( - LocaleKeys.document_plugins_optionAction_turnInto.tr(), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.text(LocaleKeys.editor_page.tr())); - await tester.pumpAndSettle(); - - expect(find.byType(SubPageBlockComponent), findsOneWidget); - - await tester.expandOrCollapsePage( - pageName: 'SubPageBlock', - layout: ViewLayoutPB.Document, - ); - - expect(find.text('Parent'), findsNWidgets(2)); - }); - - testWidgets('Displaying icon of subpage', (tester) async { - const firstPage = 'FirstPage'; - - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(name: firstPage); - final icon = await tester.loadIcon(); - - /// create subpage - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_subPage_name.tr(), - offset: 100, - ); - - /// add icon - await tester.editor.hoverOnCoverToolbar(); - await tester.editor.tapAddIconButton(); - await tester.tapIcon(icon); - await tester.pumpAndSettle(); - await tester.openPage(firstPage); - - await tester.expandOrCollapsePage( - pageName: firstPage, - layout: ViewLayoutPB.Document, - ); - - /// check if there is a icon in document - final iconWidget = find.byWidgetPredicate((w) { - if (w is! RawEmojiIconWidget) return false; - final iconData = w.emoji.emoji; - return iconData == icon.emoji; - }); - expect(iconWidget, findsOneWidget); - }); - }); -} - -extension _SubPageTestHelper on WidgetTester { - Future insertSubPageFromSlashMenu([bool withTextNode = false]) async { - await editor.tapLineOfEditorAt(0); - - if (withTextNode) { - await ime.insertText('ABC'); - await editor.getCurrentEditorState().insertNewLine(); - await pumpAndSettle(); - } - - await editor.showSlashMenu(); - await editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_subPage_name.tr(), - offset: 100, - ); - - // Navigate to the previous page to see the SubPageBlock - await openPage('SubPageBlock'); - await pumpAndSettle(); - - await pumpUntilFound(find.byType(SubPageBlockComponent)); - } - - Future renamePageWithSecondary( - String currentName, - String newName, - ) async { - await hoverOnPageName(currentName, onHover: () async => pumpAndSettle()); - await rightClickOnPageName(currentName); - await tapButtonWithName(ViewMoreActionType.rename.name); - await enterText(find.byType(TextFormField), newName); - await tapOKButton(); - await pumpAndSettle(); - } -} 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 new file mode 100644 index 0000000000..239e7e09a8 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart @@ -0,0 +1,43 @@ +import 'package:integration_test/integration_test.dart'; + +import 'document_alignment_test.dart' as document_alignment_test; +import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test; +import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; +import 'document_create_and_delete_test.dart' + as document_create_and_delete_test; +import 'document_option_action_test.dart' as document_option_action_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; +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_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_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; + +void startTesting() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Document integration tests + document_create_and_delete_test.main(); + document_edit_test.main(); + document_with_database_test.main(); + document_with_inline_page_test.main(); + document_with_inline_math_equation_test.main(); + document_with_cover_image_test.main(); + document_with_outline_block.main(); + document_with_toggle_list_test.main(); + document_copy_and_paste_test.main(); + document_codeblock_paste_test.main(); + document_alignment_test.main(); + document_text_direction_test.main(); + document_option_action_test.main(); + document_with_image_block_test.main(); + document_inline_page_reference_test.main(); + document_more_actions_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_1.dart deleted file mode 100644 index 6a4ad5cb62..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_1.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'document_create_and_delete_test.dart' - as document_create_and_delete_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_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 'edit_document_test.dart' as document_edit_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Document integration tests - document_create_and_delete_test.main(); - document_edit_test.main(); - document_with_database_test.main(); - document_with_inline_page_test.main(); - document_with_inline_math_equation_test.main(); - document_with_cover_image_test.main(); - // Don't add new tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_2.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_2.dart deleted file mode 100644 index f32db64aa7..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_2.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'document_app_lifecycle_test.dart' as document_app_lifecycle_test; -import 'document_deletion_test.dart' as document_deletion_test; -import 'document_inline_sub_page_test.dart' as document_inline_sub_page_test; -import 'document_option_action_test.dart' as document_option_action_test; -import 'document_title_test.dart' as document_title_test; -import 'document_with_date_reminder_test.dart' - as document_with_date_reminder_test; -import 'document_with_toggle_heading_block_test.dart' - as document_with_toggle_heading_block_test; -import 'document_sub_page_test.dart' as document_sub_page_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Document integration tests - document_title_test.main(); - document_app_lifecycle_test.main(); - document_with_date_reminder_test.main(); - document_deletion_test.main(); - document_option_action_test.main(); - document_inline_sub_page_test.main(); - document_with_toggle_heading_block_test.main(); - document_sub_page_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_3.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_3.dart deleted file mode 100644 index cecdaca580..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_3.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'document_alignment_test.dart' as document_alignment_test; -import 'document_codeblock_paste_test.dart' as document_codeblock_paste_test; -import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; -import 'document_text_direction_test.dart' as document_text_direction_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; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Document integration tests - document_with_outline_block.main(); - document_with_toggle_list_test.main(); - document_copy_and_paste_test.main(); - document_codeblock_paste_test.main(); - document_alignment_test.main(); - document_text_direction_test.main(); - - // Don't add new tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart deleted file mode 100644 index bc0671834b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner_4.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'document_block_option_test.dart' as document_block_option_test; -import 'document_find_menu_test.dart' as document_find_menu_test; -import 'document_inline_page_reference_test.dart' - as document_inline_page_reference_test; -import 'document_more_actions_test.dart' as document_more_actions_test; -import 'document_shortcuts_test.dart' as document_shortcuts_test; -import 'document_toolbar_test.dart' as document_toolbar_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_multi_image_block_test.dart' - as document_with_multi_image_block_test; -import 'document_with_simple_table_test.dart' - as document_with_simple_table_test; -import 'document_link_preview_test.dart' as document_link_preview_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Document integration tests - 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(); - document_shortcuts_test.main(); - document_block_option_test.main(); - document_find_menu_test.main(); - document_toolbar_test.main(); - document_with_simple_table_test.main(); - document_link_preview_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_title_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_title_test.dart deleted file mode 100644 index c694ba8d6b..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_title_test.dart +++ /dev/null @@ -1,373 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.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:universal_platform/universal_platform.dart'; - -import '../../shared/constants.dart'; -import '../../shared/util.dart'; - -const _testDocumentName = 'Test Document'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document title:', () { - testWidgets('create a new document and edit title', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - expect(title, findsOneWidget); - - // input name - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - final newTitle = tester.editor.findDocumentTitle(_testDocumentName); - expect(newTitle, findsOneWidget); - - // press enter to create a new line - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - - const firstLine = 'First line of text'; - await tester.ime.insertText(firstLine); - await tester.pumpAndSettle(); - - final firstLineText = find.text(firstLine, findRichText: true); - expect(firstLineText, findsOneWidget); - - // press cmd/ctrl+left to move the cursor to the start of the line - if (UniversalPlatform.isMacOS) { - await tester.simulateKeyEvent( - LogicalKeyboardKey.arrowLeft, - isMetaPressed: true, - ); - } else { - await tester.simulateKeyEvent(LogicalKeyboardKey.home); - } - await tester.pumpAndSettle(); - - // press arrow left to delete the first line - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowLeft); - await tester.pumpAndSettle(); - - // check if the title is on focus - final titleOnFocus = tester.editor.findDocumentTitle(_testDocumentName); - final titleWidget = tester.widget(titleOnFocus); - expect(titleWidget.focusNode?.hasFocus, isTrue); - - // press the right arrow key to move the cursor to the first line - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight); - - // check if the title is not on focus - expect(titleWidget.focusNode?.hasFocus, isFalse); - - final editorState = tester.editor.getCurrentEditorState(); - expect(editorState.selection, Selection.collapsed(Position(path: [0]))); - - // press the backspace key to go to the title - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - - expect(editorState.selection, null); - expect(titleWidget.focusNode?.hasFocus, isTrue); - }); - - testWidgets('check if the title is saved', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - expect(title, findsOneWidget); - - // input name - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - if (UniversalPlatform.isLinux) { - // wait for the name to be saved - await tester.wait(250); - } - - // go to the get started page - await tester.tapButton( - tester.findPageName(Constants.gettingStartedPageName), - ); - - // go back to the page - await tester.tapButton(tester.findPageName(_testDocumentName)); - - // check if the title is saved - final testDocumentTitle = tester.editor.findDocumentTitle( - _testDocumentName, - ); - expect(testDocumentTitle, findsOneWidget); - }); - - testWidgets('arrow up from first line moves focus to title', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.ime.insertText('First line of text'); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.home); - - // press the arrow upload - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp); - - final titleWidget = tester.widget( - tester.editor.findDocumentTitle(_testDocumentName), - ); - expect(titleWidget.focusNode?.hasFocus, isTrue); - - final editorState = tester.editor.getCurrentEditorState(); - expect(editorState.selection, null); - }); - - testWidgets( - 'backspace at start of first line moves focus to title and deletes empty paragraph', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - - final editorState = tester.editor.getCurrentEditorState(); - expect(editorState.document.root.children.length, equals(2)); - - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - - final titleWidget = tester.widget( - tester.editor.findDocumentTitle(_testDocumentName), - ); - expect(titleWidget.focusNode?.hasFocus, isTrue); - - // at least one empty paragraph node is created - expect(editorState.document.root.children.length, equals(1)); - }); - - testWidgets('arrow right from end of title moves focus to first line', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.ime.insertText('First line of text'); - - await tester.tapButton( - tester.editor.findDocumentTitle(_testDocumentName), - ); - await tester.simulateKeyEvent(LogicalKeyboardKey.end); - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowRight); - - final editorState = tester.editor.getCurrentEditorState(); - expect( - editorState.selection, - Selection.collapsed( - Position(path: [0]), - ), - ); - }); - - testWidgets('change the title via sidebar, check the title is updated', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - expect(title, findsOneWidget); - - await tester.hoverOnPageName( - '', - onHover: () async { - await tester.renamePage(_testDocumentName); - await tester.pumpAndSettle(); - }, - ); - await tester.pumpAndSettle(); - - final newTitle = tester.editor.findDocumentTitle(_testDocumentName); - expect(newTitle, findsOneWidget); - }); - - testWidgets('execute undo and redo in title', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - // press a random key to make the undo stack not empty - await tester.simulateKeyEvent(LogicalKeyboardKey.keyA); - await tester.pumpAndSettle(); - - // undo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - // wait for the undo to be applied - await tester.pumpAndSettle(Durations.long1); - - // expect the title is empty - expect( - tester - .widget( - tester.editor.findDocumentTitle(''), - ) - .controller - ?.text, - '', - ); - - // redo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - isShiftPressed: true, - ); - - await tester.pumpAndSettle(Durations.short1); - - if (UniversalPlatform.isMacOS) { - expect( - tester - .widget( - tester.editor.findDocumentTitle(_testDocumentName), - ) - .controller - ?.text, - _testDocumentName, - ); - } - }); - - testWidgets('escape key should exit the editing mode', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - expect( - tester - .widget( - tester.editor.findDocumentTitle(_testDocumentName), - ) - .focusNode - ?.hasFocus, - isFalse, - ); - }); - - testWidgets('press arrow down key in title, check if the cursor flashes', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.enterText(title, _testDocumentName); - await tester.pumpAndSettle(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - const inputText = 'Hello World'; - await tester.ime.insertText(inputText); - - await tester.tapButton( - tester.editor.findDocumentTitle(_testDocumentName), - ); - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown); - final editorState = tester.editor.getCurrentEditorState(); - expect( - editorState.selection, - Selection.collapsed( - Position(path: [0], offset: inputText.length), - ), - ); - }); - - testWidgets( - 'hover on the cover title, check if the add icon & add cover button are shown', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - final title = tester.editor.findDocumentTitle(''); - await tester.hoverOnWidget( - title, - onHover: () async { - expect(find.byType(DocumentCoverWidget), findsOneWidget); - }, - ); - - await tester.pumpAndSettle(); - }); - - testWidgets('paste text in title, check if the text is updated', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - await Clipboard.setData(const ClipboardData(text: _testDocumentName)); - - final title = tester.editor.findDocumentTitle(''); - await tester.tapButton(title); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isMetaPressed: UniversalPlatform.isMacOS, - isControlPressed: !UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - final newTitle = tester.editor.findDocumentTitle(_testDocumentName); - expect(newTitle, findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart deleted file mode 100644 index f455cd479d..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart +++ /dev/null @@ -1,370 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.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 '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - Future selectText(WidgetTester tester, String text) async { - await tester.editor.updateSelection( - Selection.single( - path: [0], - startOffset: 0, - endOffset: text.length, - ), - ); - } - - Future prepareForToolbar(WidgetTester tester, String text) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(text); - await selectText(tester, text); - } - - group('document toolbar:', () { - testWidgets('font family', (tester) async { - await prepareForToolbar(tester, 'font family'); - - // tap more options button - await tester.tapButtonWithFlowySvgData(FlowySvgs.toolbar_more_m); - // tap the font family button - final fontFamilyButton = find.byKey(kFontFamilyToolbarItemKey); - await tester.tapButton(fontFamilyButton); - - // expect to see the font family dropdown immediately - expect(find.byType(FontFamilyDropDown), findsOneWidget); - - // click the font family 'Abel' - const abel = 'Abel'; - await tester.tapButton(find.text(abel)); - - // check the text is updated to 'Abel' - final editorState = tester.editor.getCurrentEditorState(); - expect( - editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.fontFamily, - ), - abel, - ); - }); - - testWidgets('heading 1~3', (tester) async { - const text = 'heading'; - await prepareForToolbar(tester, text); - - Future testChangeHeading( - FlowySvgData svg, - String title, - int level, - ) async { - /// tap suggestions item - final suggestionsButton = find.byKey(kSuggestionsItemKey); - await tester.tapButton(suggestionsButton); - - /// tap item - await tester.ensureVisible(find.byFlowySvg(svg)); - await tester.tapButton(find.byFlowySvg(svg)); - - /// check the type of node is [HeadingBlockKeys.type] - await selectText(tester, text); - final editorState = tester.editor.getCurrentEditorState(); - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!, - nodeLevel = node.attributes[HeadingBlockKeys.level]!; - expect(node.type, HeadingBlockKeys.type); - expect(nodeLevel, level); - - /// show toolbar again - await selectText(tester, text); - - /// the text of suggestions item should be changed - expect( - find.descendant(of: suggestionsButton, matching: find.text(title)), - findsOneWidget, - ); - } - - await testChangeHeading( - FlowySvgs.type_h1_m, - LocaleKeys.document_toolbar_h1.tr(), - 1, - ); - - await testChangeHeading( - FlowySvgs.type_h2_m, - LocaleKeys.document_toolbar_h2.tr(), - 2, - ); - await testChangeHeading( - FlowySvgs.type_h3_m, - LocaleKeys.document_toolbar_h3.tr(), - 3, - ); - }); - - testWidgets('toggle 1~3', (tester) async { - const text = 'toggle'; - await prepareForToolbar(tester, text); - - Future testChangeToggle( - FlowySvgData svg, - String title, - int? level, - ) async { - /// tap suggestions item - final suggestionsButton = find.byKey(kSuggestionsItemKey); - await tester.tapButton(suggestionsButton); - - /// tap item - await tester.ensureVisible(find.byFlowySvg(svg)); - await tester.tapButton(find.byFlowySvg(svg)); - - /// check the type of node is [HeadingBlockKeys.type] - await selectText(tester, text); - final editorState = tester.editor.getCurrentEditorState(); - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!, - nodeLevel = node.attributes[ToggleListBlockKeys.level]; - expect(node.type, ToggleListBlockKeys.type); - expect(nodeLevel, level); - - /// show toolbar again - await selectText(tester, text); - - /// the text of suggestions item should be changed - expect( - find.descendant(of: suggestionsButton, matching: find.text(title)), - findsOneWidget, - ); - } - - await testChangeToggle( - FlowySvgs.type_toggle_list_m, - LocaleKeys.editor_toggleListShortForm.tr(), - null, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h1_m, - LocaleKeys.editor_toggleHeading1ShortForm.tr(), - 1, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h2_m, - LocaleKeys.editor_toggleHeading2ShortForm.tr(), - 2, - ); - - await testChangeToggle( - FlowySvgs.type_toggle_h3_m, - LocaleKeys.editor_toggleHeading3ShortForm.tr(), - 3, - ); - }); - - testWidgets('toolbar will not rebuild after click item', (tester) async { - const text = 'Test rebuilding'; - await prepareForToolbar(tester, text); - Finder toolbar = find.byType(DesktopFloatingToolbar); - Element toolbarElement = toolbar.evaluate().first; - final elementHashcode = toolbarElement.hashCode; - final boldButton = find.byFlowySvg(FlowySvgs.toolbar_bold_m), - underlineButton = find.byFlowySvg(FlowySvgs.toolbar_underline_m), - italicButton = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m); - - /// tap format buttons - await tester.tapButton(boldButton); - await tester.tapButton(underlineButton); - await tester.tapButton(italicButton); - toolbar = find.byType(DesktopFloatingToolbar); - toolbarElement = toolbar.evaluate().first; - - /// check if the toolbar is not rebuilt - expect(elementHashcode, toolbarElement.hashCode); - final editorState = tester.editor.getCurrentEditorState(); - - /// check text formats - expect( - editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.bold), - true, - ); - expect( - editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.italic), - true, - ); - expect( - editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.underline), - true, - ); - }); - }); - - group('document toolbar: link', () { - String? getLinkFromNode(Node node) { - for (final insert in node.delta!) { - final link = insert.attributes?.href; - if (link != null) return link; - } - return null; - } - - bool isPageLink(Node node) { - for (final insert in node.delta!) { - final isPage = insert.attributes?.isPage; - if (isPage == true) return true; - } - return false; - } - - String getNodeText(Node node) { - for (final insert in node.delta!) { - if (insert is TextInsert) return insert.text; - } - return ''; - } - - testWidgets('insert link and remove link', (tester) async { - const text = 'insert link', link = 'https://test.appflowy.cloud'; - await prepareForToolbar(tester, text); - - final toolbar = find.byType(DesktopFloatingToolbar); - expect(toolbar, findsOneWidget); - - /// tap link button to show CreateLinkMenu - final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); - await tester.tapButton(linkButton); - final createLinkMenu = find.byType(LinkCreateMenu); - expect(createLinkMenu, findsOneWidget); - - /// test esc to close - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - expect(toolbar, findsNothing); - - /// show toolbar again - await tester.editor.tapLineOfEditorAt(0); - await selectText(tester, text); - await tester.tapButton(linkButton); - - /// insert link - final textField = find.descendant( - of: createLinkMenu, - matching: find.byType(TextFormField), - ); - await tester.enterText(textField, link); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - Node node = tester.editor.getNodeAtPath([0]); - expect(getLinkFromNode(node), link); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - - /// hover link - await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); - final hoverMenu = find.byType(LinkHoverMenu); - expect(hoverMenu, findsOneWidget); - - /// copy link - final copyButton = find.descendant( - of: hoverMenu, - matching: find.byFlowySvg(FlowySvgs.toolbar_link_m), - ); - await tester.tapButton(copyButton); - final clipboardContent = await getIt().getData(); - final plainText = clipboardContent.plainText; - expect(plainText, link); - - /// remove link - await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); - await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m)); - node = tester.editor.getNodeAtPath([0]); - expect(getLinkFromNode(node), null); - }); - - testWidgets('insert link and edit link', (tester) async { - const text = 'edit link', - link = 'https://test.appflowy.cloud', - afterText = '$text after'; - await prepareForToolbar(tester, text); - - /// tap link button to show CreateLinkMenu - final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); - await tester.tapButton(linkButton); - - /// search for page and select it - final textField = find.descendant( - of: find.byType(LinkCreateMenu), - matching: find.byType(TextFormField), - ); - await tester.enterText(textField, gettingStarted); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - - Node node = tester.editor.getNodeAtPath([0]); - expect(isPageLink(node), true); - expect(getLinkFromNode(node) == link, false); - - /// hover link - await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); - - /// click edit button to show LinkEditMenu - final editButton = find.byFlowySvg(FlowySvgs.toolbar_link_edit_m); - await tester.tapButton(editButton); - final linkEditMenu = find.byType(LinkEditMenu); - expect(linkEditMenu, findsOneWidget); - - /// change the link text - final titleField = find.descendant( - of: linkEditMenu, - matching: find.byType(TextFormField), - ); - await tester.enterText(titleField, afterText); - await tester.pumpAndSettle(); - await tester.tapButton( - find.descendant(of: linkEditMenu, matching: find.text(gettingStarted)), - ); - final linkField = find.ancestor( - of: find.text(LocaleKeys.document_toolbar_linkInputHint.tr()), - matching: find.byType(TextFormField), - ); - await tester.enterText(linkField, link); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - - /// apply the change - final applyButton = - find.text(LocaleKeys.settings_appearance_documentSettings_apply.tr()); - await tester.tapButton(applyButton); - - node = tester.editor.getNodeAtPath([0]); - expect(isPageLink(node), false); - expect(getLinkFromNode(node), link); - expect(getNodeText(node), afterText); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart index 84b6790403..f7a88d7bec 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart @@ -1,36 +1,19 @@ -import 'dart:io'; - import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; import '../../shared/emoji.dart'; -import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('cover image:', () { + group('cover image', () { testWidgets('document cover tests', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -68,59 +51,6 @@ void main() { tester.expectToSeeNoDocumentCover(); }); - testWidgets('document cover local image tests', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - tester.expectToSeeNoDocumentCover(); - - // Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons - await tester.editor.hoverOnCoverToolbar(); - - // Insert a document cover - await tester.editor.tapOnAddCover(); - tester.expectToSeeDocumentCover(CoverType.asset); - - // Hover over the cover to show the 'Change Cover' and delete buttons - await tester.editor.hoverOnCover(); - tester.expectChangeCoverAndDeleteButton(); - - // Change cover to a local image image - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - - await tester.editor.hoverOnCover(); - await tester.editor.tapOnChangeCover(); - - final uploadButton = find.findTextInFlowyText( - LocaleKeys.document_imageBlock_upload_label.tr(), - ); - await tester.tapButton(uploadButton); - - mockPickFilePaths(paths: [localImagePath]); - await tester.tapButtonWithName( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ); - - await tester.pumpAndSettle(); - tester.expectToSeeDocumentCover(CoverType.file); - - // Remove the cover - await tester.editor.hoverOnCover(); - await tester.editor.tapOnRemoveCover(); - tester.expectToSeeNoDocumentCover(); - - // Test if deleteImageFromLocalStorage(localImagePath) function is called once - await tester.pump(kDoubleTapTimeout); - expect(deleteImageTestCounter, 1); - - // delete temp files - await imageFile.delete(); - }); - testWidgets('document icon tests', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -217,7 +147,7 @@ void main() { tester.expectViewHasIcon( gettingStarted, ViewLayoutPB.Document, - EmojiIconData.emoji(punch), + punch, ); }); }); 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 158eb501e3..f401cb1e0b 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,15 +1,14 @@ +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'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; 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/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -23,7 +22,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - await insertLinkedDatabase(tester, ViewLayoutPB.Grid); + await insertReferenceDatabase(tester, ViewLayoutPB.Grid); // validate the referenced grid is inserted expect( @@ -51,7 +50,7 @@ void main() { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - await insertLinkedDatabase(tester, ViewLayoutPB.Board); + await insertReferenceDatabase(tester, ViewLayoutPB.Board); // validate the referenced board is inserted expect( @@ -63,62 +62,11 @@ void main() { ); }); - testWidgets('insert multiple referenced boards', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new grid - final id = uuid(); - final name = '${ViewLayoutPB.Board.name}_$id'; - await tester.createNewPageWithNameUnderParent( - name: name, - layout: ViewLayoutPB.Board, - openAfterCreated: false, - ); - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'insert_a_reference_${ViewLayoutPB.Board.name}', - ); - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a referenced view - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - ViewLayoutPB.Board.slashMenuLinkedName, - ); - final referencedDatabase1 = find.descendant( - of: find.byType(InlineActionsHandler), - matching: find.findTextInFlowyText(name), - ); - expect(referencedDatabase1, findsOneWidget); - await tester.tapButton(referencedDatabase1); - - await tester.editor.tapLineOfEditorAt(1); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - ViewLayoutPB.Board.slashMenuLinkedName, - ); - final referencedDatabase2 = find.descendant( - of: find.byType(InlineActionsHandler), - matching: find.findTextInFlowyText(name), - ); - expect(referencedDatabase2, findsOneWidget); - await tester.tapButton(referencedDatabase2); - - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(DesktopBoardPage), - ), - findsNWidgets(2), - ); - }); - testWidgets('insert a referenced calendar', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - await insertLinkedDatabase(tester, ViewLayoutPB.Calendar); + await insertReferenceDatabase(tester, ViewLayoutPB.Calendar); // validate the referenced grid is inserted expect( @@ -177,112 +125,11 @@ void main() { findsOneWidget, ); }); - - testWidgets('insert a referenced grid with many rows (load more option)', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertLinkedDatabase(tester, ViewLayoutPB.Grid); - - // validate the referenced grid is inserted - expect( - find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.byType(GridPage), - ), - findsOneWidget, - ); - - // https://github.com/AppFlowy-IO/AppFlowy/issues/3533 - // test: the selection of editor should be clear when editing the grid - await tester.editor.updateSelection( - Selection.collapsed( - Position(path: [1]), - ), - ); - final gridTextCell = find.byType(EditableTextCell).first; - await tester.tapButton(gridTextCell); - - expect(tester.editor.getCurrentEditorState().selection, isNull); - - final editorScrollable = find - .descendant( - of: find.byType(AppFlowyEditor), - matching: find.byWidgetPredicate( - (w) => w is Scrollable && w.axis == Axis.vertical, - ), - ) - .first; - - // Add 100 Rows to the linked database - final addRowFinder = find.byType(GridAddRowButton); - for (var i = 0; i < 100; i++) { - await tester.scrollUntilVisible( - addRowFinder, - 100, - scrollable: editorScrollable, - ); - await tester.tapButton(addRowFinder); - await tester.pumpAndSettle(); - } - - // Since all rows visible are those we added, we should see all of them - expect(find.byType(GridRow), findsNWidgets(103)); - - // Navigate to getting started - await tester.openPage(gettingStarted); - - // Navigate back to the document - await tester.openPage('insert_a_reference_${ViewLayoutPB.Grid.name}'); - - // We see only 25 Grid Rows - expect(find.byType(GridRow), findsNWidgets(25)); - - // We see Add row and load more button - expect(find.byType(GridAddRowButton), findsOneWidget); - expect(find.byType(GridRowLoadMoreButton), findsOneWidget); - - // Load more rows, expect 50 visible - await _loadMoreRows(tester, editorScrollable, 50); - - // Load more rows, expect 75 visible - await _loadMoreRows(tester, editorScrollable, 75); - - // Load more rows, expect 100 visible - await _loadMoreRows(tester, editorScrollable, 100); - - // Load more rows, expect 103 visible - await _loadMoreRows(tester, editorScrollable, 103); - - // We no longer see load more option - expect(find.byType(GridRowLoadMoreButton), findsNothing); - }); }); } -Future _loadMoreRows( - WidgetTester tester, - Finder scrollable, [ - int? expectedRows, -]) async { - await tester.scrollUntilVisible( - find.byType(GridRowLoadMoreButton), - 100, - scrollable: scrollable, - ); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(GridRowLoadMoreButton)); - await tester.pumpAndSettle(); - - if (expectedRows != null) { - expect(find.byType(GridRow), findsNWidgets(expectedRows)); - } -} - /// Insert a referenced database of [layout] into the document -Future insertLinkedDatabase( +Future insertReferenceDatabase( WidgetTester tester, ViewLayoutPB layout, ) async { @@ -303,7 +150,7 @@ Future insertLinkedDatabase( // insert a referenced view await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( - layout.slashMenuLinkedName, + layout.referencedMenuName, ); final linkToPageMenu = find.byType(InlineActionsHandler); @@ -329,9 +176,16 @@ 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( - layout.slashMenuName, - offset: 100, + name, ); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart deleted file mode 100644 index ccfdbae76e..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart +++ /dev/null @@ -1,466 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.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:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:table_calendar/table_calendar.dart'; - -import '../../shared/util.dart'; - -void main() { - setUp(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - group('date or reminder block in document:', () { - testWidgets("insert date with time block", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'Date with time 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_dateOrReminder.tr(), - ); - - final dateTimeSettings = DateTimeSettingsPB( - dateFormat: UserDateFormatPB.Friendly, - timeFormat: UserTimeFormatPB.TwentyFourHour, - ); - final DateTime currentDateTime = DateTime.now(); - final String formattedDate = - dateTimeSettings.dateFormat.formatDate(currentDateTime, false); - - // get current date in editor - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - - // tap on date field - await tester.tap(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - - // tap the toggle of include time - await tester.tap(find.byType(Toggle)); - await tester.pumpAndSettle(); - - // add time 11:12 - final textField = find - .descendant( - of: find.byType(DesktopAppFlowyDatePicker), - matching: find.byType(TextField), - ) - .last; - await tester.pumpUntilFound(textField); - await tester.enterText(textField, "11:12"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - // we will get field with current date and 11:12 as time - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate 11:12'), findsOneWidget); - }); - - testWidgets("insert date with reminder block", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'Date with reminder 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_dateOrReminder.tr(), - ); - - final dateTimeSettings = DateTimeSettingsPB( - dateFormat: UserDateFormatPB.Friendly, - timeFormat: UserTimeFormatPB.TwentyFourHour, - ); - final DateTime currentDateTime = DateTime.now(); - final String formattedDate = - dateTimeSettings.dateFormat.formatDate(currentDateTime, false); - - // get current date in editor - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - - // tap on date field - await tester.tap(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - - // tap reminder and set reminder to 1 day before - await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr())); - await tester.pumpAndSettle(); - await tester.tap( - find.textContaining( - LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), - ), - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - - // we will get field with current date reminder_clock.svg icon - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget); - }); - - testWidgets("copy, cut and paste a date mention", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'copy, cut and paste a date mention', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_dateOrReminder.tr(), - ); - - final dateTimeSettings = DateTimeSettingsPB( - dateFormat: UserDateFormatPB.Friendly, - timeFormat: UserTimeFormatPB.TwentyFourHour, - ); - final DateTime currentDateTime = DateTime.now(); - final String formattedDate = - dateTimeSettings.dateFormat.formatDate(currentDateTime, false); - - // get current date in editor - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - - // update selection and copy - await tester.editor.updateSelection( - Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: 1), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - // update selection and paste - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsNWidgets(2)); - expect(find.text('@$formattedDate'), findsNWidgets(2)); - - // update selection and cut - await tester.editor.updateSelection( - Selection( - start: Position(path: [0], offset: 1), - end: Position(path: [0], offset: 2), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - - // update selection and paste - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsNWidgets(2)); - expect(find.text('@$formattedDate'), findsNWidgets(2)); - }); - - testWidgets("copy, cut and paste a reminder mention", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'copy, cut and paste a reminder mention', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_dateOrReminder.tr(), - ); - - // trigger popup - await tester.tapButton(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - - // set date to be fifteenth of the next month - await tester.tap( - find.descendant( - of: find.byType(DesktopAppFlowyDatePicker), - matching: find.byFlowySvg(FlowySvgs.arrow_right_s), - ), - ); - await tester.pumpAndSettle(); - await tester.tap( - find.descendant( - of: find.byType(TableCalendar), - matching: find.text(15.toString()), - ), - ); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // add a reminder - await tester.tap(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr())); - await tester.pumpAndSettle(); - await tester.tap( - find.textContaining( - LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // verify - final dateTimeSettings = DateTimeSettingsPB( - dateFormat: UserDateFormatPB.Friendly, - timeFormat: UserTimeFormatPB.TwentyFourHour, - ); - final now = DateTime.now(); - final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15); - final formattedDate = - dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false); - - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget); - expect(getIt().state.reminders.map((e) => e.id).length, 1); - - // update selection and copy - await tester.editor.updateSelection( - Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: 1), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyC, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - // update selection and paste - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsNWidgets(2)); - expect(find.text('@$formattedDate'), findsNWidgets(2)); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2)); - expect( - getIt().state.reminders.map((e) => e.id).toSet().length, - 2, - ); - - // update selection and cut - await tester.editor.updateSelection( - Selection( - start: Position(path: [0], offset: 1), - end: Position(path: [0], offset: 2), - ), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyX, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget); - expect(getIt().state.reminders.map((e) => e.id).length, 1); - - // update selection and paste - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsNWidgets(2)); - expect(find.text('@$formattedDate'), findsNWidgets(2)); - expect(find.byType(MentionDateBlock), findsNWidgets(2)); - expect(find.text('@$formattedDate'), findsNWidgets(2)); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNWidgets(2)); - expect( - getIt().state.reminders.map((e) => e.id).toSet().length, - 2, - ); - }); - - testWidgets("delete, undo and redo a reminder mention", (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'delete, undo and redo a reminder mention', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_dateOrReminder.tr(), - ); - - // trigger popup - await tester.tapButton(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - - // set date to be fifteenth of the next month - await tester.tap( - find.descendant( - of: find.byType(DesktopAppFlowyDatePicker), - matching: find.byFlowySvg(FlowySvgs.arrow_right_s), - ), - ); - await tester.pumpAndSettle(); - await tester.tap( - find.descendant( - of: find.byType(TableCalendar), - matching: find.text(15.toString()), - ), - ); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // add a reminder - await tester.tap(find.byType(MentionDateBlock)); - await tester.pumpAndSettle(); - await tester.tap(find.text(LocaleKeys.datePicker_reminderLabel.tr())); - await tester.pumpAndSettle(); - await tester.tap( - find.textContaining( - LocaleKeys.datePicker_reminderOptions_oneDayBefore.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - - // verify - final dateTimeSettings = DateTimeSettingsPB( - dateFormat: UserDateFormatPB.Friendly, - timeFormat: UserTimeFormatPB.TwentyFourHour, - ); - final now = DateTime.now(); - final fifteenthOfNextMonth = DateTime(now.year, now.month + 1, 15); - final formattedDate = - dateTimeSettings.dateFormat.formatDate(fifteenthOfNextMonth, false); - - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget); - expect(getIt().state.reminders.map((e) => e.id).length, 1); - - // update selection and backspace to delete the mention - await tester.editor.updateSelection( - Selection.collapsed(Position(path: [0], offset: 1)), - ); - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - await tester.pumpAndSettle(); - - expect(find.byType(MentionDateBlock), findsNothing); - expect(find.text('@$formattedDate'), findsNothing); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing); - expect(getIt().state.reminders.isEmpty, isTrue); - - // undo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isWindows || Platform.isLinux, - isMetaPressed: Platform.isMacOS, - ); - - expect(find.byType(MentionDateBlock), findsOneWidget); - expect(find.text('@$formattedDate'), findsOneWidget); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsOneWidget); - expect(getIt().state.reminders.map((e) => e.id).length, 1); - - // redo - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyZ, - isControlPressed: Platform.isWindows || Platform.isLinux, - isMetaPressed: Platform.isMacOS, - isShiftPressed: true, - ); - - expect(find.byType(MentionDateBlock), findsNothing); - expect(find.text('@$formattedDate'), findsNothing); - expect(find.byFlowySvg(FlowySvgs.reminder_clock_s), findsNothing); - expect(getIt().state.reminders.isEmpty, isTrue); - }); - }); -} 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 deleted file mode 100644 index 9d7a97e6a8..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_file_test.dart +++ /dev/null @@ -1,161 +0,0 @@ -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/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_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); - 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.tapFileUploadHint(); - 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); - 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 3dcd6be8ae..976d812da1 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,20 +3,24 @@ 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/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.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/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu, ResizableImage; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:run_with_network_images/run_with_network_images.dart'; import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; @@ -32,15 +36,13 @@ void main() { // create a new document await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), + name: LocaleKeys.document_plugins_image_addAnImage.tr(), ); // tap the first line of the document await tester.editor.tapLineOfEditorAt(0); await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); + await tester.editor.tapSlashMenuItemWithName('Image'); expect(find.byType(CustomImageBlockComponent), findsOneWidget); expect(find.byType(ImagePlaceholder), findsOneWidget); expect( @@ -76,21 +78,19 @@ void main() { file.deleteSync(); }); - testWidgets('insert two images from local file at once', (tester) async { + testWidgets('insert an image from network', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); // create a new document await tester.createNewPageWithNameUnderParent( - name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(), + name: LocaleKeys.document_plugins_image_addAnImage.tr(), ); // tap the first line of the document await tester.editor.tapLineOfEditorAt(0); await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_image.tr(), - ); + await tester.editor.tapSlashMenuItemWithName('Image'); expect(find.byType(CustomImageBlockComponent), findsOneWidget); expect(find.byType(ImagePlaceholder), findsOneWidget); expect( @@ -102,42 +102,64 @@ void main() { ); 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(), + 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.tapButton( + find.descendant( + of: find.byType(EmbedImageUrlWidget), + matching: find.text( + LocaleKeys.document_imageBlock_embedLink_label.tr(), + findRichText: true, + ), + ), ); await tester.pumpAndSettle(); + expect(find.byType(ResizableImage), findsOneWidget); + final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!; + expect(node.type, ImageBlockKeys.type); + expect(node.attributes[ImageBlockKeys.url], url); + }); - expect(find.byType(ResizableImage), findsNWidgets(2)); + testWidgets('insert an image from unsplash', (tester) async { + await runWithNetworkImages(() async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(firstNode.type, ImageBlockKeys.type); - expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty); + // create a new document + await tester.createNewPageWithNameUnderParent( + name: LocaleKeys.document_plugins_image_addAnImage.tr(), + ); - final secondNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - expect(secondNode.type, ImageBlockKeys.type); - expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty); + // tap the first line of the document + await tester.editor.tapLineOfEditorAt(0); + await tester.editor.showSlashMenu(); + await tester.editor.tapSlashMenuItemWithName('Image'); + expect(find.byType(CustomImageBlockComponent), findsOneWidget); + expect(find.byType(ImagePlaceholder), findsOneWidget); + expect( + find.descendant( + of: find.byType(ImagePlaceholder), + matching: find.byType(AppFlowyPopover), + ), + findsOneWidget, + ); + expect(find.byType(UploadImageMenu), findsOneWidget); - // remove the temp files - await Future.wait([firstFile.delete(), secondFile.delete()]); + await tester.tapButtonWithName( + 'Unsplash', + ); + expect(find.byType(UnsplashImageWidget), findsOneWidget); + }); }); }); } 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 67e0149cd1..27f02f17cd 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 @@ -1,11 +1,9 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.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'; @@ -34,15 +32,9 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - // tap the inline math equation button - final inlineMathEquationButton = find.text( - LocaleKeys.document_toolbar_equation.tr(), + final inlineMathEquationButton = find.byTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), ); await tester.tapButton(inlineMathEquationButton); @@ -85,15 +77,10 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: formula.length), ); - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - // tap the inline math equation button - final inlineMathEquationButton = - find.byFlowySvg(FlowySvgs.type_formula_m); + var inlineMathEquationButton = find.byTooltip( + LocaleKeys.document_plugins_createInlineMathEquation.tr(), + ); await tester.tapButton(inlineMathEquationButton); // expect to see the math equation block @@ -105,7 +92,17 @@ void main() { Selection.single(path: [0], startOffset: 0, endOffset: 1), ); - await tester.tapButton(moreOptionButton); + // 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(), + ); + expect( + tester.widget(inlineMathEquationButton).isHighlight, + isTrue, + ); // cancel the format await tester.tapButton(inlineMathEquationButton); @@ -116,110 +113,5 @@ void main() { tester.expectToSeeText(formula); }); - - testWidgets('insert a inline math equation and type something after it', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'math equation', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a inline page - const formula = 'E = MC ^ 2'; - await tester.ime.insertText(formula); - await tester.editor.updateSelection( - Selection.single(path: [0], startOffset: 0, endOffset: formula.length), - ); - - // tap the more options button - final moreOptionButton = find.findFlowyTooltip( - LocaleKeys.document_toolbar_moreOptions.tr(), - ); - await tester.tapButton(moreOptionButton); - - // tap the inline math equation button - final inlineMathEquationButton = - find.byFlowySvg(FlowySvgs.type_formula_m); - await tester.tapButton(inlineMathEquationButton); - - // expect to see the math equation block - final inlineMathEquation = find.byType(InlineMathEquation); - expect(inlineMathEquation, findsOneWidget); - - await tester.editor.tapLineOfEditorAt(0); - const text = 'Hello World'; - await tester.ime.insertText(text); - - final inlineText = find.textContaining(text, findRichText: true); - expect(inlineText, findsOneWidget); - - // the text should be in the same line with the math equation - final inlineMathEquationPosition = tester.getRect(inlineMathEquation); - final textPosition = tester.getRect(inlineText); - // allow 5px difference - expect( - (textPosition.top - inlineMathEquationPosition.top).abs(), - lessThan(5), - ); - expect( - (textPosition.bottom - inlineMathEquationPosition.bottom).abs(), - lessThan(5), - ); - }); - - testWidgets('insert inline math equation by shortcut', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create a new document - await tester.createNewPageWithNameUnderParent( - name: 'insert inline math equation by shortcut', - ); - - // tap the first line of the document - await tester.editor.tapLineOfEditorAt(0); - // insert a inline page - const formula = 'E = MC ^ 2'; - await tester.ime.insertText(formula); - await tester.editor.updateSelection( - Selection.single(path: [0], startOffset: 0, endOffset: formula.length), - ); - - // mock key event - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyE, - isShiftPressed: true, - isControlPressed: true, - ); - - // expect to see the math equation block - final inlineMathEquation = find.byType(InlineMathEquation); - expect(inlineMathEquation, findsOneWidget); - - await tester.editor.tapLineOfEditorAt(0); - const text = 'Hello World'; - await tester.ime.insertText(text); - - final inlineText = find.textContaining(text, findRichText: true); - expect(inlineText, findsOneWidget); - - // the text should be in the same line with the math equation - final inlineMathEquationPosition = tester.getRect(inlineMathEquation); - final textPosition = tester.getRect(inlineText); - // allow 5px difference - expect( - (textPosition.top - inlineMathEquationPosition.top).abs(), - lessThan(5), - ); - expect( - (textPosition.bottom - inlineMathEquationPosition.bottom).abs(), - lessThan(5), - ); - }); }); } 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 12047bd37f..335f9a377f 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/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.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,21 +92,7 @@ void main() { ); expect(finder, findsOneWidget); await tester.tapButton(finder); - expect(find.byType(GridPage), findsOneWidget); - }); - - testWidgets('insert a inline page and type something after the page', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await insertInlinePage(tester, ViewLayoutPB.Grid); - - await tester.editor.tapLineOfEditorAt(0); - const text = 'Hello World'; - await tester.ime.insertText(text); - - expect(find.textContaining(text, findRichText: true), findsOneWidget); + expect(find.byType(FlowyErrorPage), 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 deleted file mode 100644 index d8b0784a39..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart +++ /dev/null @@ -1,291 +0,0 @@ -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_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'; - -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 2f3f8c80b9..bfd8198295 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 @@ -1,7 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -44,9 +46,6 @@ void main() { * # Heading 1 * ## Heading 2 * ### Heading 3 - * > # Heading 1 - * > ## Heading 2 - * > ### Heading 3 */ await tester.editor.tapLineOfEditorAt(3); @@ -57,7 +56,7 @@ void main() { of: find.byType(OutlineBlockWidget), matching: find.text(heading1), ), - findsNWidgets(2), + findsOneWidget, ); // Heading 2 is prefixed with a bullet @@ -66,7 +65,7 @@ void main() { of: find.byType(OutlineBlockWidget), matching: find.text(heading2), ), - findsNWidgets(2), + findsOneWidget, ); // Heading 3 is prefixed with a dash @@ -75,7 +74,7 @@ void main() { of: find.byType(OutlineBlockWidget), matching: find.text(heading3), ), - findsNWidgets(2), + findsOneWidget, ); // update the Heading 1 to Heading 1Hello world @@ -103,16 +102,13 @@ void main() { * # Heading 1 * ## Heading 2 * ### Heading 3 - * > # Heading 1 - * > ## Heading 2 - * > ### Heading 3 */ - await tester.editor.tapLineOfEditorAt(7); + await tester.editor.tapLineOfEditorAt(3); await insertOutlineInDocument(tester); // expect to find only the `heading1` widget under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 1); + await hoverAndClickDepthOptionAction(tester, [3], 1); expect( find.descendant( of: find.byType(OutlineBlockWidget), @@ -130,7 +126,7 @@ void main() { ////// /// expect to find only the 'heading1' and 'heading2' under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 2); + await hoverAndClickDepthOptionAction(tester, [3], 2); expect( find.descendant( of: find.byType(OutlineBlockWidget), @@ -141,13 +137,13 @@ void main() { ////// // expect to find all the headings under the [OutlineBlockWidget] - await hoverAndClickDepthOptionAction(tester, [6], 3); + await hoverAndClickDepthOptionAction(tester, [3], 3); expect( find.descendant( of: find.byType(OutlineBlockWidget), matching: find.text(heading1), ), - findsNWidgets(2), + findsOneWidget, ); expect( @@ -155,7 +151,7 @@ void main() { of: find.byType(OutlineBlockWidget), matching: find.text(heading2), ), - findsNWidgets(2), + findsOneWidget, ); expect( @@ -163,7 +159,7 @@ void main() { of: find.byType(OutlineBlockWidget), matching: find.text(heading3), ), - findsNWidgets(2), + findsOneWidget, ); ////// }); @@ -175,7 +171,7 @@ Future insertOutlineInDocument(WidgetTester tester) async { // open the actions menu and insert the outline block await tester.editor.showSlashMenu(); await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_outline.tr(), + LocaleKeys.document_selectionMenu_outline.tr(), ); await tester.pumpAndSettle(); } @@ -185,25 +181,19 @@ Future hoverAndClickDepthOptionAction( List path, int level, ) async { - await tester.editor.openDepthMenu(path); - final type = OptionDepthType.fromLevel(level); - await tester.tapButton(find.findTextInFlowyText(type.description)); + await tester.editor.hoverAndClickOptionMenuButton([3]); + await tester.tap(find.byType(AppFlowyPopover).hitTestable().last); + await tester.pumpAndSettle(); + + // Find a total of 4 HoverButtons under the [BlockOptionButton], + // in addition to 3 HoverButtons under the [DepthOptionAction] - (child of BlockOptionButton) + await tester.tap(find.byType(HoverButton).hitTestable().at(3 + level)); await tester.pumpAndSettle(); } Future insertHeadingComponent(WidgetTester tester) async { await tester.editor.tapLineOfEditorAt(0); - - // # heading 1-3 await tester.ime.insertText('# $heading1\n'); await tester.ime.insertText('## $heading2\n'); await tester.ime.insertText('### $heading3\n'); - - // > # toggle heading 1-3 - await tester.ime.insertText('> # $heading1\n'); - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - await tester.ime.insertText('> ## $heading2\n'); - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - await tester.ime.insertText('> ### $heading3\n'); - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart deleted file mode 100644 index bcf3fde24f..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart +++ /dev/null @@ -1,783 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.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'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../shared/util.dart'; - -const String heading1 = "Heading 1"; -const String heading2 = "Heading 2"; -const String heading3 = "Heading 3"; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('simple table block test:', () { - testWidgets('insert a simple table block', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // validate the table is inserted - expect(find.byType(SimpleTableBlockWidget), findsOneWidget); - - final editorState = tester.editor.getCurrentEditorState(); - expect( - editorState.selection, - // table -> row -> cell -> paragraph - Selection.collapsed(Position(path: [0, 0, 0, 0])), - ); - - final firstCell = find.byType(SimpleTableCellBlockWidget).first; - expect( - tester - .state(firstCell) - .isEditingCellNotifier - .value, - isTrue, - ); - }); - - testWidgets('select all in table cell', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - const cell1Content = 'Cell 1'; - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('New Table'); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.pumpAndSettle(); - await tester.editor.tapLineOfEditorAt(1); - await tester.insertTableInDocument(); - await tester.ime.insertText(cell1Content); - await tester.pumpAndSettle(); - // Select all in the cell - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - - expect( - tester.editor.getCurrentEditorState().selection, - Selection( - start: Position(path: [1, 0, 0, 0]), - end: Position(path: [1, 0, 0, 0], offset: cell1Content.length), - ), - ); - - // Press select all again, the selection should be the entire document - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - - expect( - tester.editor.getCurrentEditorState().selection, - Selection( - start: Position(path: [0]), - end: Position(path: [1, 1, 1, 0]), - ), - ); - }); - - testWidgets(''' -1. hover on the table - 1.1 click the add row button - 1.2 click the add column button - 1.3 click the add row and column button -2. validate the table is updated -3. delete the last column -4. delete the last row -5. validate the table is updated -''', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // add a new row - final row = find.byWidgetPredicate((w) { - return w is SimpleTableRowBlockWidget && w.node.rowIndex == 1; - }); - await tester.hoverOnWidget( - row, - onHover: () async { - final addRowButton = find.byType(SimpleTableAddRowButton).first; - await tester.tap(addRowButton); - }, - ); - await tester.pumpAndSettle(); - - // add a new column - final column = find.byWidgetPredicate((w) { - return w is SimpleTableCellBlockWidget && w.node.columnIndex == 1; - }).first; - await tester.hoverOnWidget( - column, - onHover: () async { - final addColumnButton = find.byType(SimpleTableAddColumnButton).first; - await tester.tap(addColumnButton); - }, - ); - await tester.pumpAndSettle(); - - // add a new row and a new column - final row2 = find.byWidgetPredicate((w) { - return w is SimpleTableCellBlockWidget && - w.node.rowIndex == 2 && - w.node.columnIndex == 2; - }).first; - await tester.hoverOnWidget( - row2, - onHover: () async { - // click the add row and column button - final addRowAndColumnButton = - find.byType(SimpleTableAddColumnAndRowButton).first; - await tester.tap(addRowAndColumnButton); - }, - ); - await tester.pumpAndSettle(); - - final tableNode = - tester.editor.getCurrentEditorState().document.nodeAtPath([0])!; - expect(tableNode.columnLength, 4); - expect(tableNode.rowLength, 4); - - // delete the last row - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: tableNode.rowLength - 1, - action: SimpleTableMoreAction.delete, - ); - await tester.pumpAndSettle(); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 4); - - // delete the last column - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: tableNode.columnLength - 1, - action: SimpleTableMoreAction.delete, - ); - await tester.pumpAndSettle(); - - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 3); - }); - - testWidgets('enable header column and header row', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // enable the header row - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.enableHeaderRow, - ); - await tester.pumpAndSettle(); - // enable the header column - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.enableHeaderColumn, - ); - await tester.pumpAndSettle(); - - final tableNode = - tester.editor.getCurrentEditorState().document.nodeAtPath([0])!; - - expect(tableNode.isHeaderColumnEnabled, isTrue); - expect(tableNode.isHeaderRowEnabled, isTrue); - - // disable the header row - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.enableHeaderRow, - ); - await tester.pumpAndSettle(); - expect(tableNode.isHeaderColumnEnabled, isTrue); - expect(tableNode.isHeaderRowEnabled, isFalse); - - // disable the header column - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.enableHeaderColumn, - ); - await tester.pumpAndSettle(); - expect(tableNode.isHeaderColumnEnabled, isFalse); - expect(tableNode.isHeaderRowEnabled, isFalse); - }); - - testWidgets('duplicate a column / row', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // duplicate the row - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.duplicate, - ); - await tester.pumpAndSettle(); - - // duplicate the column - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.duplicate, - ); - await tester.pumpAndSettle(); - - final tableNode = - tester.editor.getCurrentEditorState().document.nodeAtPath([0])!; - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 3); - }); - - testWidgets('insert left / insert right', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // insert left - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.insertLeft, - ); - await tester.pumpAndSettle(); - - // insert right - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.insertRight, - ); - await tester.pumpAndSettle(); - - final tableNode = - tester.editor.getCurrentEditorState().document.nodeAtPath([0])!; - expect(tableNode.columnLength, 4); - expect(tableNode.rowLength, 2); - }); - - testWidgets('insert above / insert below', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - // insert above - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.insertAbove, - ); - await tester.pumpAndSettle(); - - // insert below - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.insertBelow, - ); - await tester.pumpAndSettle(); - - final tableNode = - tester.editor.getCurrentEditorState().document.nodeAtPath([0])!; - expect(tableNode.rowLength, 4); - expect(tableNode.columnLength, 2); - }); - }); - - testWidgets('set column width to page width (1)', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - final tableNode = tester.editor.getNodeAtPath([0]); - final beforeWidth = tableNode.width; - - // set the column width to page width - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.setToPageWidth, - ); - await tester.pumpAndSettle(); - - final afterWidth = tableNode.width; - expect(afterWidth, greaterThan(beforeWidth)); - }); - - testWidgets('set column width to page width (2)', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - final tableNode = tester.editor.getNodeAtPath([0]); - final beforeWidth = tableNode.width; - - // set the column width to page width - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.setToPageWidth, - ); - await tester.pumpAndSettle(); - - final afterWidth = tableNode.width; - expect(afterWidth, greaterThan(beforeWidth)); - }); - - testWidgets('distribute columns evenly (1)', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - final tableNode = tester.editor.getNodeAtPath([0]); - final beforeWidth = tableNode.width; - - // set the column width to page width - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.row, - index: 0, - action: SimpleTableMoreAction.distributeColumnsEvenly, - ); - await tester.pumpAndSettle(); - - final afterWidth = tableNode.width; - expect(afterWidth, equals(beforeWidth)); - - final distributeColumnWidthsEvenly = - tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly]; - expect(distributeColumnWidthsEvenly, isTrue); - }); - - testWidgets('distribute columns evenly (2)', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - final tableNode = tester.editor.getNodeAtPath([0]); - final beforeWidth = tableNode.width; - - // set the column width to page width - await tester.clickMoreActionItemInTableMenu( - type: SimpleTableMoreActionType.column, - index: 0, - action: SimpleTableMoreAction.distributeColumnsEvenly, - ); - await tester.pumpAndSettle(); - - final afterWidth = tableNode.width; - expect(afterWidth, equals(beforeWidth)); - - final distributeColumnWidthsEvenly = - tableNode.attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly]; - expect(distributeColumnWidthsEvenly, isTrue); - }); - - testWidgets('using option menu to set column width', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - await tester.editor.hoverAndClickOptionMenuButton([0]); - - final editorState = tester.editor.getCurrentEditorState(); - final beforeWidth = editorState.document.nodeAtPath([0])!.width; - - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterWidth = editorState.document.nodeAtPath([0])!.width; - expect(afterWidth, greaterThan(beforeWidth)); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButton( - find.text( - LocaleKeys - .document_plugins_simpleTable_moreActions_distributeColumnsWidth - .tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterWidth2 = editorState.document.nodeAtPath([0])!.width; - expect(afterWidth2, equals(afterWidth)); - }); - - testWidgets('insert a table and use select all the delete it', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - await tester.editor.tapLineOfEditorAt(1); - await tester.ime.insertText('Hello World'); - - // select all - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyA, - isMetaPressed: UniversalPlatform.isMacOS, - isControlPressed: !UniversalPlatform.isMacOS, - ); - - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - // only one paragraph left - expect(editorState.document.root.children.length, 1); - final paragraphNode = editorState.document.nodeAtPath([0])!; - expect(paragraphNode.delta, isNull); - }); - - testWidgets('use tab or shift+tab to navigate in table', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - await tester.simulateKeyEvent(LogicalKeyboardKey.tab); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final selection = editorState.selection; - expect(selection, isNotNull); - expect(selection!.start.path, [0, 0, 1, 0]); - expect(selection.end.path, [0, 0, 1, 0]); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.tab, - isShiftPressed: true, - ); - await tester.pumpAndSettle(); - - final selection2 = editorState.selection; - expect(selection2, isNotNull); - expect(selection2!.start.path, [0, 0, 0, 0]); - expect(selection2.end.path, [0, 0, 0, 0]); - }); - - testWidgets('shift+enter to insert a new line in table', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - await tester.simulateKeyEvent( - LogicalKeyboardKey.enter, - isShiftPressed: true, - ); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.document.nodeAtPath([0, 0, 0])!; - expect(node.children.length, 1); - }); - - testWidgets('using option menu to set table align', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - await tester.editor.hoverAndClickOptionMenuButton([0]); - - final editorState = tester.editor.getCurrentEditorState(); - final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign; - expect(beforeAlign, TableAlign.left); - - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_center.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign, TableAlign.center); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_right.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign2, TableAlign.right); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_left.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign3, TableAlign.left); - }); - - testWidgets('using option menu to set table align', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - await tester.editor.hoverAndClickOptionMenuButton([0]); - - final editorState = tester.editor.getCurrentEditorState(); - final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign; - expect(beforeAlign, TableAlign.left); - - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_center.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign, TableAlign.center); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_right.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign2, TableAlign.right); - - await tester.editor.hoverAndClickOptionMenuButton([0]); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - ), - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.text( - LocaleKeys.document_plugins_optionAction_left.tr(), - ), - ); - await tester.pumpAndSettle(); - - final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign; - expect(afterAlign3, TableAlign.left); - }); - - testWidgets('support slash menu in table', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent( - name: 'simple_table_test', - ); - - final editorState = tester.editor.getCurrentEditorState(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.insertTableInDocument(); - - final path = [0, 0, 0, 0]; - final selection = Selection.collapsed(Position(path: path)); - editorState.selection = selection; - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - - final paragraphItem = find.byWidgetPredicate((w) { - return w is SelectionMenuItemWidget && - w.item.name == LocaleKeys.document_slashMenu_name_text.tr(); - }); - expect(paragraphItem, findsOneWidget); - - await tester.tap(paragraphItem); - await tester.pumpAndSettle(); - - final paragraphNode = editorState.document.nodeAtPath(path)!; - expect(paragraphNode.type, equals(ParagraphBlockKeys.type)); - }); -} - -extension on WidgetTester { - /// Insert a table in the document - Future insertTableInDocument() async { - // open the actions menu and insert the outline block - await editor.showSlashMenu(); - await editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_table.tr(), - ); - await pumpAndSettle(); - } - - Future clickMoreActionItemInTableMenu({ - required SimpleTableMoreActionType type, - required int index, - required SimpleTableMoreAction action, - }) async { - if (type == SimpleTableMoreActionType.row) { - final row = find.byWidgetPredicate((w) { - return w is SimpleTableRowBlockWidget && w.node.rowIndex == index; - }); - await hoverOnWidget( - row, - onHover: () async { - final moreActionButton = find.byWidgetPredicate((w) { - return w is SimpleTableMoreActionMenu && - w.type == SimpleTableMoreActionType.row && - w.index == index; - }); - await tapButton(moreActionButton); - await tapButton(find.text(action.name)); - }, - ); - await pumpAndSettle(); - } else if (type == SimpleTableMoreActionType.column) { - final column = find.byWidgetPredicate((w) { - return w is SimpleTableCellBlockWidget && w.node.columnIndex == index; - }).first; - await hoverOnWidget( - column, - onHover: () async { - final moreActionButton = find.byWidgetPredicate((w) { - return w is SimpleTableMoreActionMenu && - w.type == SimpleTableMoreActionType.column && - w.index == index; - }); - await tapButton(moreActionButton); - await tapButton(find.text(action.name)); - }, - ); - await pumpAndSettle(); - } - - await tapAt(Offset.zero); - } -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart deleted file mode 100644 index c4aa289855..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_heading_block_test.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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'; - -import '../../shared/util.dart'; - -const String _heading1 = 'Heading 1'; -const String _heading2 = 'Heading 2'; -const String _heading3 = 'Heading 3'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('toggle heading block test:', () { - testWidgets('insert toggle heading 1 - 3 block', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'toggle heading block test', - ); - - for (var i = 1; i <= 3; i++) { - await tester.editor.tapLineOfEditorAt(0); - await _insertToggleHeadingBlockInDocument(tester, i); - await tester.pumpAndSettle(); - expect( - find.byWidgetPredicate( - (widget) => - widget is ToggleListBlockComponentWidget && - widget.node.attributes[ToggleListBlockKeys.level] == i, - ), - findsOneWidget, - ); - } - }); - - testWidgets('insert toggle heading 1 - 3 block by shortcuts', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'toggle heading block test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('# > $_heading1\n'); - await tester.ime.insertText('## > $_heading2\n'); - await tester.ime.insertText('### > $_heading3\n'); - await tester.ime.insertText('> # $_heading1\n'); - await tester.ime.insertText('> ## $_heading2\n'); - await tester.ime.insertText('> ### $_heading3\n'); - await tester.pumpAndSettle(); - - expect( - find.byType(ToggleListBlockComponentWidget), - findsNWidgets(6), - ); - }); - - testWidgets('insert toggle heading and convert it to heading', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent( - name: 'toggle heading block test', - ); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText('# > $_heading1\n'); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - await tester.ime.insertText('item 1'); - await tester.pumpAndSettle(); - - await tester.editor.updateSelection( - Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: _heading1.length), - ), - ); - - await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_text_format_m)); - - // tap the H1 button - await tester.tapButton(find.byFlowySvg(FlowySvgs.type_h1_m).at(0)); - await tester.pumpAndSettle(); - - final editorState = tester.editor.getCurrentEditorState(); - final node1 = editorState.document.nodeAtPath([0])!; - expect(node1.type, HeadingBlockKeys.type); - expect(node1.attributes[HeadingBlockKeys.level], 1); - - final node2 = editorState.document.nodeAtPath([1])!; - expect(node2.type, ParagraphBlockKeys.type); - expect(node2.delta!.toPlainText(), 'item 1'); - }); - }); -} - -Future _insertToggleHeadingBlockInDocument( - WidgetTester tester, - int level, -) async { - final name = switch (level) { - 1 => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(), - 2 => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(), - 3 => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(), - _ => throw Exception('Invalid level: $level'), - }; - await tester.editor.showSlashMenu(); - await tester.editor.tapSlashMenuItemWithName( - name, - offset: 150, - ); - await tester.pumpAndSettle(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart index a4d011dccb..0f3bab2f8e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_toggle_list_test.dart @@ -1,9 +1,7 @@ import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -216,73 +214,5 @@ void main() { expectToggleListOpened(); }); - - Future prepareToggleHeadingBlock( - WidgetTester tester, - String text, - ) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.ime.insertText(text); - } - - testWidgets('> + # to toggle heading 1 block', (tester) async { - await prepareToggleHeadingBlock(tester, '> # Hello'); - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 1); - expect(node.delta!.toPlainText(), 'Hello'); - }); - - testWidgets('> + ### to toggle heading 3 block', (tester) async { - await prepareToggleHeadingBlock(tester, '> ### Hello'); - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 3); - expect(node.delta!.toPlainText(), 'Hello'); - }); - - testWidgets('# + > to toggle heading 1 block', (tester) async { - await prepareToggleHeadingBlock(tester, '# > Hello'); - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 1); - expect(node.delta!.toPlainText(), 'Hello'); - }); - - testWidgets('### + > to toggle heading 3 block', (tester) async { - await prepareToggleHeadingBlock(tester, '### > Hello'); - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0])!; - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 3); - expect(node.delta!.toPlainText(), 'Hello'); - }); - - testWidgets('click the toggle list to create a new paragraph', - (tester) async { - await prepareToggleHeadingBlock(tester, '> # Hello'); - final emptyHintText = find.text( - LocaleKeys.document_plugins_emptyToggleHeading.tr( - args: ['1'], - ), - ); - expect(emptyHintText, findsOneWidget); - - await tester.tapButton(emptyHintText); - await tester.pumpAndSettle(); - - // check the new paragraph is created - final editorState = tester.editor.getCurrentEditorState(); - final node = editorState.getNodeAtPath([0, 0])!; - expect(node.type, ParagraphBlockKeys.type); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/first_test/first_test.dart b/frontend/appflowy_flutter/integration_test/desktop/first_test/first_test.dart deleted file mode 100644 index 36c0e391fb..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/first_test/first_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -// This test is meaningless, just for preventing the CI from failing. -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Empty', () { - testWidgets('empty test', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.wait(500); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_edit_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_edit_row_test.dart deleted file mode 100644 index 64b7a40ad1..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_edit_row_test.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:time/time.dart'; - -import '../../shared/database_test_op.dart'; -import 'grid_test_extensions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid edit row test:', () { - testWidgets('with sort configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final unsorted = tester.getGridRows(); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - final sorted = [ - unsorted[7], - unsorted[8], - unsorted[1], - unsorted[9], - unsorted[11], - unsorted[10], - unsorted[6], - unsorted[12], - unsorted[2], - unsorted[0], - unsorted[3], - unsorted[5], - unsorted[4], - ]; - - List actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - await tester.editCell( - rowIndex: 4, - fieldType: FieldType.RichText, - input: "x", - ); - await tester.pumpAndSettle(200.milliseconds); - - final reSorted = [ - unsorted[7], - unsorted[8], - unsorted[1], - unsorted[9], - unsorted[10], - unsorted[6], - unsorted[12], - unsorted[2], - unsorted[0], - unsorted[3], - unsorted[5], - unsorted[11], - unsorted[4], - ]; - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(reSorted)); - - // delete the sort - await tester.tapSortMenuInSettingBar(); - await tester.tapDeleteAllSortsButton(); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(unsorted)); - }); - - testWidgets('with filter configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - final filtered = [ - original[1], - original[3], - original[5], - original[6], - original[7], - original[9], - original[12], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - expect(actual.length, equals(7)); - tester.assertNumberOfRowsInGridPage(7); - - await tester.tapCheckboxCellInGrid(rowIndex: 0); - await tester.pumpAndSettle(200.milliseconds); - - // verify grid data - actual = tester.getGridRows(); - expect(actual.length, equals(6)); - tester.assertNumberOfRowsInGridPage(6); - final edited = [ - original[3], - original[5], - original[6], - original[7], - original[9], - original[12], - ]; - expect(actual, orderedEquals(edited)); - - // delete the filter - await tester.tapFilterButtonInGrid('Registration Complete'); - await tester - .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - - // verify grid data - actual = tester.getGridRows(); - expect(actual.length, equals(13)); - tester.assertNumberOfRowsInGridPage(13); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_filter_and_sort_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_filter_and_sort_test.dart deleted file mode 100644 index c9fed5b02e..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_filter_and_sort_test.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import 'grid_test_extensions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid simultaneous sort and filter test:', () { - // testWidgets('delete filter with active sort', (tester) async { - // await tester.openTestDatabase(v069GridFileName); - - // // get grid data - // final original = tester.getGridRows(); - - // // add a filter - // await tester.tapDatabaseFilterButton(); - // await tester.tapCreateFilterByFieldType( - // FieldType.Checkbox, - // 'Registration Complete', - // ); - - // // add a sort - // await tester.tapDatabaseSortButton(); - // await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - // final filteredAndSorted = [ - // original[7], - // original[1], - // original[9], - // original[6], - // original[12], - // original[3], - // original[5], - // ]; - - // // verify grid data - // List actual = tester.getGridRows(); - // expect(actual, orderedEquals(filteredAndSorted)); - - // // delete the filter - // await tester.tapFilterButtonInGrid('Registration Complete'); - // await tester - // .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - // await tester.tapDeleteFilterButtonInGrid(); - - // final sorted = [ - // original[7], - // original[8], - // original[1], - // original[9], - // original[11], - // original[10], - // original[6], - // original[12], - // original[2], - // original[0], - // original[3], - // original[5], - // original[4], - // ]; - - // // verify grid data - // actual = tester.getGridRows(); - // expect(actual, orderedEquals(sorted)); - // }); - - testWidgets('delete sort with active fiilter', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // add a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - final filteredAndSorted = [ - original[7], - original[1], - original[9], - original[6], - original[12], - original[3], - original[5], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filteredAndSorted)); - - // delete the sort - await tester.tapSortMenuInSettingBar(); - await tester.tapDeleteAllSortsButton(); - - final filtered = [ - original[1], - original[3], - original[5], - original[6], - original[7], - original[9], - original[12], - ]; - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reopen_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reopen_test.dart deleted file mode 100644 index a4363f7a83..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reopen_test.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; -import 'grid_test_extensions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid reopen test:', () { - testWidgets('base case', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - final expected = tester.getGridRows(); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - final actual = tester.getGridRows(); - - expect(actual, orderedEquals(expected)); - }); - - testWidgets('with sort configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final unsorted = tester.getGridRows(); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - final sorted = [ - unsorted[7], - unsorted[8], - unsorted[1], - unsorted[9], - unsorted[11], - unsorted[10], - unsorted[6], - unsorted[12], - unsorted[2], - unsorted[0], - unsorted[3], - unsorted[5], - unsorted[4], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - // delete sorts - // TODO(RS): Shouldn't the sort/filter list show automatically!? - await tester.tapDatabaseSortButton(); - await tester.tapSortMenuInSettingBar(); - await tester.tapDeleteAllSortsButton(); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(unsorted)); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(unsorted)); - }); - - testWidgets('with filter configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final unfiltered = tester.getGridRows(); - - // add a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - final filtered = [ - unfiltered[1], - unfiltered[3], - unfiltered[5], - unfiltered[6], - unfiltered[7], - unfiltered[9], - unfiltered[12], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - - // delete the filter - // TODO(RS): Shouldn't the sort/filter list show automatically!? - await tester.tapDatabaseFilterButton(); - await tester.tapFilterButtonInGrid('Registration Complete'); - await tester - .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(unfiltered)); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(unfiltered)); - }); - - testWidgets('with both filter and sort configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // add a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - final filteredAndSorted = [ - original[7], - original[1], - original[9], - original[6], - original[12], - original[3], - original[5], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filteredAndSorted)); - - // go to another page and come back - await tester.openPage('Getting started'); - await tester.openPage('v069', layout: ViewLayoutPB.Grid); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(filteredAndSorted)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reorder_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reorder_row_test.dart deleted file mode 100644 index 40f7252a91..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_reorder_row_test.dart +++ /dev/null @@ -1,214 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; -import 'grid_test_extensions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid reorder row test:', () { - testWidgets('base case', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // reorder row - await tester.reorderRow(original[4], original[1]); - - // verify grid data - List reordered = [ - original[0], - original[4], - original[1], - original[2], - original[3], - original[5], - original[6], - original[7], - original[8], - original[9], - original[10], - original[11], - original[12], - ]; - List actual = tester.getGridRows(); - expect(actual, orderedEquals(reordered)); - - // reorder row - await tester.reorderRow(reordered[1], reordered[3]); - - // verify grid data - reordered = [ - original[0], - original[1], - original[2], - original[4], - original[3], - original[5], - original[6], - original[7], - original[8], - original[9], - original[10], - original[11], - original[12], - ]; - actual = tester.getGridRows(); - expect(actual, orderedEquals(reordered)); - - // reorder row - await tester.reorderRow(reordered[2], reordered[0]); - - // verify grid data - reordered = [ - original[2], - original[0], - original[1], - original[4], - original[3], - original[5], - original[6], - original[7], - original[8], - original[9], - original[10], - original[11], - original[12], - ]; - actual = tester.getGridRows(); - expect(actual, orderedEquals(reordered)); - }); - - testWidgets('with active sort', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - // verify grid data - final sorted = [ - original[7], - original[8], - original[1], - original[9], - original[11], - original[10], - original[6], - original[12], - original[2], - original[0], - original[3], - original[5], - original[4], - ]; - List actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - // reorder row - await tester.reorderRow(original[4], original[1]); - expect(find.byType(ConfirmPopup), findsOneWidget); - await tester.tapButtonWithName(LocaleKeys.button_cancel.tr()); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - }); - - testWidgets('with active filter', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // add a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - final filtered = [ - original[1], - original[3], - original[5], - original[6], - original[7], - original[9], - original[12], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - - // reorder row - await tester.reorderRow(filtered[3], filtered[1]); - - // verify grid data - List reordered = [ - original[1], - original[6], - original[3], - original[5], - original[7], - original[9], - original[12], - ]; - actual = tester.getGridRows(); - expect(actual, orderedEquals(reordered)); - - // reorder row - await tester.reorderRow(reordered[3], reordered[5]); - - // verify grid data - reordered = [ - original[1], - original[6], - original[3], - original[7], - original[9], - original[5], - original[12], - ]; - actual = tester.getGridRows(); - expect(actual, orderedEquals(reordered)); - - // delete the filter - await tester.tapFilterButtonInGrid('Registration Complete'); - await tester - .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - - // verify grid data - final expected = [ - original[0], - original[1], - original[2], - original[6], - original[3], - original[4], - original[7], - original[8], - original[9], - original[5], - original[10], - original[11], - original[12], - ]; - actual = tester.getGridRows(); - expect(actual, orderedEquals(expected)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_row_test.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_row_test.dart deleted file mode 100644 index d7efb797f0..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_row_test.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/database_test_op.dart'; -import '../../shared/util.dart'; - -import 'grid_test_extensions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('grid row test:', () { - testWidgets('create from the bottom', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - final expected = tester.getGridRows(); - - // create row - await tester.tapCreateRowButtonInGrid(); - - final actual = tester.getGridRows(); - expect(actual.slice(0, 3), orderedEquals(expected)); - expect(actual.length, equals(4)); - tester.assertNumberOfRowsInGridPage(4); - }); - - testWidgets('create from a row\'s menu', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - - final expected = tester.getGridRows(); - - // create row - await tester.hoverOnFirstRowOfGrid(); - await tester.tapCreateRowButtonAfterHoveringOnGridRow(); - - final actual = tester.getGridRows(); - expect([actual[0], actual[2], actual[3]], orderedEquals(expected)); - expect(actual.length, equals(4)); - tester.assertNumberOfRowsInGridPage(4); - }); - - testWidgets('create with sort configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final unsorted = tester.getGridRows(); - - // add a sort - await tester.tapDatabaseSortButton(); - await tester.tapCreateSortByFieldType(FieldType.RichText, 'Name'); - - final sorted = [ - unsorted[7], - unsorted[8], - unsorted[1], - unsorted[9], - unsorted[11], - unsorted[10], - unsorted[6], - unsorted[12], - unsorted[2], - unsorted[0], - unsorted[3], - unsorted[5], - unsorted[4], - ]; - - List actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - // create row - await tester.hoverOnFirstRowOfGrid(); - await tester.tapCreateRowButtonAfterHoveringOnGridRow(); - - // cancel - expect(find.byType(ConfirmPopup), findsOneWidget); - await tester.tapButtonWithName(LocaleKeys.button_cancel.tr()); - - // verify grid data - actual = tester.getGridRows(); - expect(actual, orderedEquals(sorted)); - - // try again, but confirm this time - await tester.hoverOnFirstRowOfGrid(); - await tester.tapCreateRowButtonAfterHoveringOnGridRow(); - expect(find.byType(ConfirmPopup), findsOneWidget); - await tester.tapButtonWithName(LocaleKeys.button_remove.tr()); - - // verify grid data - actual = tester.getGridRows(); - expect(actual.length, equals(14)); - tester.assertNumberOfRowsInGridPage(14); - }); - - testWidgets('create with filter configured', (tester) async { - await tester.openTestDatabase(v069GridFileName); - - // get grid data - final original = tester.getGridRows(); - - // create a filter - await tester.tapDatabaseFilterButton(); - await tester.tapCreateFilterByFieldType( - FieldType.Checkbox, - 'Registration Complete', - ); - - final filtered = [ - original[1], - original[3], - original[5], - original[6], - original[7], - original[9], - original[12], - ]; - - // verify grid data - List actual = tester.getGridRows(); - expect(actual, orderedEquals(filtered)); - - // create row (one before and after the first row, and one at the bottom) - await tester.tapCreateRowButtonInGrid(); - await tester.hoverOnFirstRowOfGrid(); - await tester.tapCreateRowButtonAfterHoveringOnGridRow(); - await tester.hoverOnFirstRowOfGrid(() async { - await tester.tapRowMenuButtonInGrid(); - await tester.tapCreateRowAboveButtonInRowMenu(); - }); - - actual = tester.getGridRows(); - expect(actual.length, equals(10)); - tester.assertNumberOfRowsInGridPage(10); - actual = [ - actual[1], - actual[3], - actual[4], - actual[5], - actual[6], - actual[7], - actual[8], - ]; - expect(actual, orderedEquals(filtered)); - - // delete the filter - await tester.tapFilterButtonInGrid('Registration Complete'); - await tester - .tapDisclosureButtonInFinder(find.byType(CheckboxFilterEditor)); - await tester.tapDeleteFilterButtonInGrid(); - - // verify grid data - actual = tester.getGridRows(); - expect(actual.length, equals(16)); - tester.assertNumberOfRowsInGridPage(16); - actual = [ - actual[0], - actual[2], - actual[4], - actual[5], - actual[6], - actual[7], - actual[8], - actual[9], - actual[10], - actual[11], - actual[12], - actual[13], - actual[14], - ]; - expect(actual, orderedEquals(original)); - }); - - testWidgets('delete row of the grid', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.hoverOnFirstRowOfGrid(() async { - // Open the row menu and click the delete button - await tester.tapRowMenuButtonInGrid(); - await tester.tapDeleteOnRowMenu(); - }); - expect(find.byType(ConfirmPopup), findsOneWidget); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - - tester.assertNumberOfRowsInGridPage(2); - }); - - testWidgets('delete row in two views', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - await tester.renameLinkedView( - tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid), - 'grid 1', - ); - tester.assertNumberOfRowsInGridPage(3); - - await tester.tapCreateLinkedDatabaseViewButton(DatabaseLayoutPB.Grid); - await tester.renameLinkedView( - tester.findTabBarLinkViewByViewLayout(ViewLayoutPB.Grid).at(1), - 'grid 2', - ); - tester.assertNumberOfRowsInGridPage(3); - - await tester.hoverOnFirstRowOfGrid(() async { - // Open the row menu and click the delete button - await tester.tapRowMenuButtonInGrid(); - await tester.tapDeleteOnRowMenu(); - }); - expect(find.byType(ConfirmPopup), findsOneWidget); - await tester.tapButtonWithName(LocaleKeys.button_delete.tr()); - // 3 initial rows - 1 deleted - tester.assertNumberOfRowsInGridPage(2); - - await tester.tapTabBarLinkedViewByViewName('grid 1'); - tester.assertNumberOfRowsInGridPage(2); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_extensions.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_extensions.dart deleted file mode 100644 index c5a0b404e7..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_extensions.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:flutter_test/flutter_test.dart'; - -extension GridTestExtensions on WidgetTester { - List getGridRows() { - final databaseController = - widget(find.byType(GridPage)).databaseController; - return [ - ...databaseController.rowCache.rowInfos.map((e) => e.rowId), - ]; - } -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_runner_1.dart deleted file mode 100644 index ff42bb6cc2..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/grid/grid_test_runner_1.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'grid_edit_row_test.dart' as grid_edit_row_test_runner; -import 'grid_filter_and_sort_test.dart' as grid_filter_and_sort_test_runner; -import 'grid_reopen_test.dart' as grid_reopen_test_runner; -import 'grid_reorder_row_test.dart' as grid_reorder_row_test_runner; -import 'grid_row_test.dart' as grid_row_test_runner; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - grid_reopen_test_runner.main(); - grid_row_test_runner.main(); - grid_reorder_row_test_runner.main(); - grid_filter_and_sort_test_runner.main(); - grid_edit_row_test_runner.main(); - // grid_calculations_test_runner.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart index bdfe2dae9f..2a1f0fe3e4 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/reminder/document_reminder_test.dart @@ -15,7 +15,7 @@ import 'package:integration_test/integration_test.dart'; import '../../shared/base.dart'; import '../../shared/common_operations.dart'; -import '../../shared/document_test_operations.dart'; +import '../../shared/editor_test_operations.dart'; import '../../shared/expectation.dart'; import '../../shared/keyboard.dart'; diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart index 570c482fb5..958910dd80 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/notifications_settings_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -17,25 +17,25 @@ void main() { await tester.openSettingsPage(SettingsPage.notifications); await tester.pumpAndSettle(); - final toggleFinder = find.byType(Toggle).first; + final switchFinder = find.byType(Switch).first; // Defaults to enabled - Toggle toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, true); + Switch switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, true); // Disable - await tester.tap(toggleFinder); + await tester.tap(switchFinder); await tester.pumpAndSettle(); - toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, false); + switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, false); // Enable again - await tester.tap(toggleFinder); + await tester.tap(switchFinder); await tester.pumpAndSettle(); - toggleWidget = tester.widget(toggleFinder); - expect(toggleWidget.value, true); + switchWidget = tester.widget(switchFinder); + expect(switchWidget.value, true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart index 617d495265..34fe511e9e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/settings_runner.dart @@ -3,7 +3,6 @@ import 'package:integration_test/integration_test.dart'; import 'notifications_settings_test.dart' as notifications_settings_test; import 'settings_billing_test.dart' as settings_billing_test; import 'shortcuts_settings_test.dart' as shortcuts_settings_test; -import 'sign_in_page_settings_test.dart' as sign_in_page_settings_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -11,5 +10,4 @@ void main() { notifications_settings_test.main(); settings_billing_test.main(); shortcuts_settings_test.main(); - sign_in_page_settings_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart index fe91becba6..fb3383a218 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/settings/shortcuts_settings_test.dart @@ -1,21 +1,23 @@ +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/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart'; import 'package:easy_localization/easy_localization.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 '../../shared/keyboard.dart'; import '../../shared/util.dart'; +import '../board/board_hide_groups_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('shortcuts:', () { - testWidgets('change and overwrite shortcut', (tester) async { + group('shortcuts test', () { + testWidgets('can change and overwrite shortcut', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -27,20 +29,14 @@ void main() { LocaleKeys.settings_shortcutsPage_keybindings_backspace.tr(); // Input "Delete" into the search field - final inputField = find.descendant( - of: find.byType(SettingsShortcutsView), - matching: find.byType(TextField), - ); - await tester.enterText(inputField, backspaceCmd); + await tester.enterText(find.byType(TextField), backspaceCmd); await tester.pumpAndSettle(); await tester.hoverOnWidget( - find - .descendant( - of: find.byType(ShortcutSettingTile), - matching: find.text(backspaceCmd), - ) - .first, + find.descendant( + of: find.byType(ShortcutSettingTile), + matching: find.text(backspaceCmd), + ), onHover: () async { await tester.tap(find.byFlowySvg(FlowySvgs.edit_s)); await tester.pumpAndSettle(); diff --git a/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart b/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart deleted file mode 100644 index 047e02da36..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/settings/sign_in_page_settings_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; -import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - Finder findServerType(AuthenticatorType type) { - return find - .descendant( - of: find.byType(SettingsServerDropdownMenu), - matching: find.findTextInFlowyText( - type.label, - ), - ) - .last; - } - - group('sign-in page settings:', () { - testWidgets('change server type', (tester) async { - await tester.initializeAppFlowy(); - - // reset the app to the default state - await useAppFlowyBetaCloudWithURL( - kAppflowyCloudUrl, - AuthenticatorType.appflowyCloud, - ); - - // open the settings page - final settingsButton = find.byType(DesktopSignInSettingsButton); - await tester.tapButton(settingsButton); - - expect(find.byType(SimpleSettingsDialog), findsOneWidget); - - // the default type should be appflowy cloud - final appflowyCloudType = findServerType(AuthenticatorType.appflowyCloud); - expect(appflowyCloudType, findsOneWidget); - - // change the server type to self-host - await tester.tapButton(appflowyCloudType); - final selfHostedButton = findServerType( - AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapButton(selfHostedButton); - - // update server url - const serverUrl = 'https://self-hosted.appflowy.cloud'; - await tester.enterText( - find.byKey(kSelfHostedTextInputFieldKey), - serverUrl, - ); - await tester.pumpAndSettle(); - // update the web url - const webUrl = 'https://self-hosted.appflowy.com'; - await tester.enterText( - find.byKey(kSelfHostedWebTextInputFieldKey), - webUrl, - ); - await tester.pumpAndSettle(); - await tester.tapButton( - find.findTextInFlowyText(LocaleKeys.button_save.tr()), - ); - - // wait the app to restart, and the tooltip to disappear - await tester.pumpUntilNotFound(find.byType(DesktopToast)); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); - - // open settings page to check the result - await tester.tapButton(settingsButton); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); - - // check the server type - expect( - findServerType(AuthenticatorType.appflowyCloudSelfHost), - findsOneWidget, - ); - // check the server url - expect( - find.text(serverUrl), - findsOneWidget, - ); - // check the web url - expect( - find.text(webUrl), - findsOneWidget, - ); - - // reset to appflowy cloud - await tester.tapButton( - findServerType(AuthenticatorType.appflowyCloudSelfHost), - ); - // change the server type to appflowy cloud - await tester.tapButton( - findServerType(AuthenticatorType.appflowyCloud), - ); - - // wait the app to restart, and the tooltip to disappear - await tester.pumpUntilNotFound(find.byType(DesktopToast)); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); - - // check the server type - await tester.tapButton(settingsButton); - expect( - findServerType(AuthenticatorType.appflowyCloud), - findsOneWidget, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index ad18cf3de6..f2a1fae8ae 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,12 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -48,82 +44,5 @@ void main() { ); expect(isExpanded(type: FolderSpaceType.private), true); }); - - testWidgets('Expanding with subpage', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - const page1 = 'SubPageBloc', page2 = '$page1 2'; - await tester.createNewPageWithNameUnderParent(name: page1); - await tester.createNewPageWithNameUnderParent( - name: page2, - parentName: page1, - ); - - await tester.expandOrCollapsePage( - pageName: gettingStarted, - layout: ViewLayoutPB.Document, - ); - - await tester.tapNewPageButton(); - - await tester.editor.tapLineOfEditorAt(0); - await tester.pumpAndSettle(); - await tester.editor.showSlashMenu(); - await tester.pumpAndSettle(); - - final slashMenu = find - .ancestor( - of: find.byType(SelectionMenuItemWidget), - matching: find.byWidgetPredicate( - (widget) => widget is Scrollable, - ), - ) - .first; - final slashMenuItem = find.text( - LocaleKeys.document_slashMenu_name_linkedDoc.tr(), - ); - await tester.scrollUntilVisible( - slashMenuItem, - 100, - scrollable: slashMenu, - duration: const Duration(milliseconds: 250), - ); - - final menuItemFinder = find.byWidgetPredicate( - (w) => - w is SelectionMenuItemWidget && - w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(), - ); - - final menuItem = - menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget; - - /// tapSlashMenuItemWithName is not working, so invoke this function directly - menuItem.item.handler( - menuItem.editorState, - menuItem.menuService, - menuItemFinder.evaluate().first, - ); - - await tester.pumpAndSettle(); - final actionHandler = find.byType(InlineActionsHandler); - final subPage = find.descendant( - of: actionHandler, - matching: find.text(page2, findRichText: true), - ); - await tester.tapButton(subPage); - - final subpageBlock = find.descendant( - of: find.byType(AppFlowyEditor), - matching: find.text(page2, findRichText: true), - ); - - expect(find.text(page2, findRichText: true), findsOneWidget); - await tester.tapButton(subpageBlock); - - /// one is in SectionFolder, another one is in CoverTitle - /// the last one is in FlowyNavigation - expect(find.text(page2, findRichText: true), findsNWidgets(3)); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index 3345ed30ab..729ee62a3e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -196,58 +196,5 @@ void main() { await tester.pumpAndSettle(); }, ); - - testWidgets( - 'reorder favorites', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// there are no favorite views - final favorites = find.descendant( - of: find.byType(FavoriteFolder), - matching: find.byType(ViewItem), - ); - expect(favorites, findsNothing); - - /// create views and then favorite them - const pageNames = ['001', '002', '003']; - for (final name in pageNames) { - await tester.createNewPageWithNameUnderParent(name: name); - } - for (final name in pageNames) { - await tester.favoriteViewByName(name); - } - expect(favorites, findsNWidgets(pageNames.length)); - - final oldNames = favorites - .evaluate() - .map((e) => (e.widget as ViewItem).view.name) - .toList(); - expect(oldNames, pageNames); - - /// drag first to last - await tester.reorderFavorite( - fromName: '001', - toName: '003', - ); - List newNames = favorites - .evaluate() - .map((e) => (e.widget as ViewItem).view.name) - .toList(); - expect(newNames, ['002', '003', '001']); - - /// drag first to second - await tester.reorderFavorite( - fromName: '002', - toName: '003', - ); - newNames = favorites - .evaluate() - .map((e) => (e.widget as ViewItem).view.name) - .toList(); - expect(newNames, ['003', '002', '001']); - }, - ); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart index 2236f03960..4659c98b55 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_icon_test.dart @@ -1,346 +1,82 @@ -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import '../../shared/base.dart'; import '../../shared/common_operations.dart'; -import '../../shared/emoji.dart'; import '../../shared/expectation.dart'; void main() { - final emoji = EmojiIconData.emoji('😁'); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); + const emoji = '😁'; - tearDownAll(() { - RecentIcons.enable = true; - }); + group('Icon', () { + testWidgets('Update page icon in sidebar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); - testWidgets('Update page emoji in sidebar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; + // update its icon + await tester.updatePageIconInSidebarByName( + name: value.name, + parentName: gettingStarted, + layout: value, + icon: emoji, + ); + + tester.expectViewHasIcon( + value.name, + value, + emoji, + ); } - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); + }); - // update its emoji - await tester.updatePageIconInSidebarByName( - name: value.name, - parentName: gettingStarted, - layout: value, - icon: emoji, - ); + testWidgets('Update page icon in title bar', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); - tester.expectViewHasIcon( - value.name, - value, - emoji, - ); - } - }); + // create document, board, grid and calendar views + for (final value in ViewLayoutPB.values) { + if (value == ViewLayoutPB.Chat) { + continue; + } + await tester.createNewPageWithNameUnderParent( + name: value.name, + parentName: gettingStarted, + layout: value, + ); - testWidgets('Update page emoji in title bar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); + // update its icon + await tester.updatePageIconInTitleBarByName( + name: value.name, + layout: value, + icon: emoji, + ); - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; + tester.expectViewHasIcon( + value.name, + value, + emoji, + ); + + tester.expectViewTitleHasIcon( + value.name, + value, + emoji, + ); } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - // update its emoji - await tester.updatePageIconInTitleBarByName( - name: value.name, - layout: value, - icon: emoji, - ); - - tester.expectViewHasIcon( - value.name, - value, - emoji, - ); - - tester.expectViewTitleHasIcon( - value.name, - value, - emoji, - ); - } - }); - - testWidgets('Emoji Search Bar Get Focus', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - await tester.openPage( - value.name, - layout: value, - ); - final title = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.text(value.name), - ); - await tester.tapButton(title); - await tester.tapButton(find.byType(EmojiPickerButton)); - - final emojiPicker = find.byType(FlowyEmojiPicker); - expect(emojiPicker, findsOneWidget); - final textField = find.descendant( - of: emojiPicker, - matching: find.byType(FlowyTextField), - ); - expect(textField, findsOneWidget); - final textFieldWidget = - textField.evaluate().first.widget as FlowyTextField; - assert(textFieldWidget.focusNode!.hasFocus); - await tester.tapEmoji(emoji.emoji); - await tester.pumpAndSettle(); - tester.expectViewHasIcon( - value.name, - value, - emoji, - ); - } - }); - - testWidgets('Update page icon in sidebar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final iconData = await tester.loadIcon(); - - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - // update its icon - await tester.updatePageIconInSidebarByName( - name: value.name, - parentName: gettingStarted, - layout: value, - icon: iconData, - ); - - tester.expectViewHasIcon( - value.name, - value, - iconData, - ); - } - }); - - testWidgets('Update page icon in title bar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - final iconData = await tester.loadIcon(); - - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - // update its icon - await tester.updatePageIconInTitleBarByName( - name: value.name, - layout: value, - icon: iconData, - ); - - tester.expectViewHasIcon( - value.name, - value, - iconData, - ); - - tester.expectViewTitleHasIcon( - value.name, - value, - iconData, - ); - } - }); - - testWidgets('Update page custom image icon in title bar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// prepare local image - final iconData = await tester.prepareImageIcon(); - - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - // update its icon - await tester.updatePageIconInTitleBarByName( - name: value.name, - layout: value, - icon: iconData, - ); - - tester.expectViewHasIcon( - value.name, - value, - iconData, - ); - - tester.expectViewTitleHasIcon( - value.name, - value, - iconData, - ); - } - }); - - testWidgets('Update page custom svg icon in title bar', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// prepare local image - final iconData = await tester.prepareSvgIcon(); - - // create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - // update its icon - await tester.updatePageIconInTitleBarByName( - name: value.name, - layout: value, - icon: iconData, - ); - - tester.expectViewHasIcon( - value.name, - value, - iconData, - ); - - tester.expectViewTitleHasIcon( - value.name, - value, - iconData, - ); - } - }); - - testWidgets('Update page custom svg icon in title bar by pasting a link', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// prepare local image - const testIconLink = - 'https://beta.appflowy.cloud/api/file_storage/008e6f23-516b-4d8d-b1fe-2b75c51eee26/v1/blob/6bdf8dff%2D0e54%2D4d35%2D9981%2Dcde68bef1141/BGpLnRtb3AGBNgSJsceu70j83zevYKrMLzqsTIJcBeI=.svg'; - - /// create document, board, grid and calendar views - for (final value in ViewLayoutPB.values) { - if (value == ViewLayoutPB.Chat) { - continue; - } - - await tester.createNewPageWithNameUnderParent( - name: value.name, - parentName: gettingStarted, - layout: value, - ); - - /// update its icon - await tester.updatePageIconInTitleBarByPasteALink( - name: value.name, - layout: value, - iconLink: testIconLink, - ); - - /// check if there is a svg in page - final pageName = tester.findPageName( - value.name, - layout: value, - ); - final imageInPage = find.descendant( - of: pageName, - matching: find.byType(SvgPicture), - ); - expect(imageInPage, findsOneWidget); - - /// check if there is a svg in title - final imageInTitle = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.byWidgetPredicate((w) { - if (w is! SvgPicture) return false; - final loader = w.bytesLoader; - if (loader is! SvgFileLoader) return false; - return loader.file.path.endsWith('.svg'); - }), - ); - expect(imageInTitle, findsOneWidget); - } + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_recent_icon_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_recent_icon_test.dart deleted file mode 100644 index 2b724ffac1..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_recent_icon_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../shared/base.dart'; -import '../../shared/common_operations.dart'; -import '../../shared/expectation.dart'; - -void main() { - testWidgets('Skip the empty group name icon in recent icons', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// clear local data - RecentIcons.clear(); - await loadIconGroups(); - final groups = kIconGroups!; - final List localIcons = []; - for (final e in groups) { - localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList()); - } - await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, '')); - await tester.openPage(gettingStarted); - final title = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.text(gettingStarted), - ); - await tester.tapButton(title); - - /// tap emoji picker button - await tester.tapButton(find.byType(EmojiPickerButton)); - expect(find.byType(FlowyIconEmojiPicker), findsOneWidget); - - /// tap icon tab - final pickTab = find.byType(PickerTab); - final iconTab = find.descendant( - of: pickTab, - matching: find.text(PickerTabType.icon.tr), - ); - await tester.tapButton(iconTab); - - expect(find.byType(FlowyIconPicker), findsOneWidget); - - /// no recent icons - final recentText = find.descendant( - of: find.byType(FlowyIconPicker), - matching: find.text('Recent'), - ); - expect(recentText, findsNothing); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart index 304e8e2e35..006d7ff0b6 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test.dart @@ -1,3 +1,4 @@ +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,6 +8,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.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:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -15,7 +17,7 @@ import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('sidebar:', () { + group('sidebar test', () { testWidgets('create a new page', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); @@ -24,7 +26,9 @@ void main() { await tester.tapNewPageButton(); // expect to see a new document - tester.expectToSeePageName(''); + tester.expectToSeePageName( + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ); // and with one paragraph block expect(find.byType(ParagraphBlockComponentWidget), findsOneWidget); }); @@ -198,7 +202,7 @@ void main() { layout: ViewLayoutPB.Grid, onHover: () async { expect(find.byType(ViewAddButton), findsNothing); - expect(find.byType(ViewMoreActionPopover), findsOneWidget); + expect(find.byType(ViewMoreActionButton), findsOneWidget); }, ); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index ef7d3dbc8b..3bc41d78c0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -2,9 +2,7 @@ import 'package:integration_test/integration_test.dart'; import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; -import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test; import 'sidebar_test.dart' as sidebar_test; -import 'sidebar_view_item_test.dart' as sidebar_view_item_test; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -14,6 +12,4 @@ void main() { // sidebar_expanded_test.main(); sidebar_favorite_test.main(); sidebar_icon_test.main(); - sidebar_view_item_test.main(); - sidebar_recent_icon_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart deleted file mode 100644 index f2b721e686..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_view_item_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('Sidebar view item tests', () { - testWidgets('Access view item context menu by right click', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - // Right click on the view item and change icon - await tester.hoverOnWidget( - find.byType(ViewItem), - onHover: () async { - await tester.tap(find.byType(ViewItem), buttons: kSecondaryButton); - await tester.pumpAndSettle(); - }, - ); - - // Change icon - final changeIconButton = - find.text(LocaleKeys.document_plugins_cover_changeIcon.tr()); - - await tester.tapButton(changeIconButton); - await tester.pumpUntilFound(find.byType(FlowyEmojiPicker)); - - const emoji = '😁'; - await tester.tapEmoji(emoji); - await tester.pumpAndSettle(); - - tester.expectViewHasIcon( - gettingStarted, - ViewLayoutPB.Document, - EmojiIconData.emoji(emoji), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/code_block_language_selector_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/code_block_language_selector_test.dart deleted file mode 100644 index e522e2fc73..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/code_block_language_selector_test.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/base.dart'; -import '../../shared/common_operations.dart'; -import '../../shared/document_test_operations.dart'; -import '../document/document_codeblock_paste_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Code Block Language Selector Test', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - /// create a new document - await tester.createNewPageWithNameUnderParent(); - - /// tap editor to get focus - await tester.tapButton(find.byType(AppFlowyEditor)); - - expect(find.byType(CodeBlockLanguageSelector), findsNothing); - await insertCodeBlockInDocument(tester); - - ///tap button - await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget)); - await tester - .tapButtonWithName(LocaleKeys.document_codeBlock_language_auto.tr()); - expect(find.byType(CodeBlockLanguageSelector), findsOneWidget); - - for (var i = 0; i < 3; ++i) { - await onKey(tester, LogicalKeyboardKey.arrowDown); - } - for (var i = 0; i < 2; ++i) { - await onKey(tester, LogicalKeyboardKey.arrowUp); - } - - await onKey(tester, LogicalKeyboardKey.enter); - - final editorState = tester.editor.getCurrentEditorState(); - String language = editorState - .getNodeAtPath([0])! - .attributes[CodeBlockKeys.language] - .toString(); - expect( - language.toLowerCase(), - defaultCodeBlockSupportedLanguages.first.toLowerCase(), - ); - - await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget)); - await tester.tapButtonWithName(language); - - await onKey(tester, LogicalKeyboardKey.arrowUp); - await onKey(tester, LogicalKeyboardKey.enter); - - language = editorState - .getNodeAtPath([0])! - .attributes[CodeBlockKeys.language] - .toString(); - expect( - language.toLowerCase(), - defaultCodeBlockSupportedLanguages.last.toLowerCase(), - ); - - await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget)); - await tester.tapButtonWithName(language); - tester.testTextInput.enterText("rust"); - await onKey(tester, LogicalKeyboardKey.delete); - await onKey(tester, LogicalKeyboardKey.delete); - await onKey(tester, LogicalKeyboardKey.arrowDown); - tester.testTextInput.enterText("st"); - await onKey(tester, LogicalKeyboardKey.arrowDown); - await onKey(tester, LogicalKeyboardKey.enter); - language = editorState - .getNodeAtPath([0])! - .attributes[CodeBlockKeys.language] - .toString(); - expect(language.toLowerCase(), 'rust'); - }); -} - -Future onKey(WidgetTester tester, LogicalKeyboardKey key) async { - await tester.simulateKeyEvent(key); - await tester.pumpAndSettle(); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart index d3226a3ad0..554a6eecbf 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/emoji_shortcut_test.dart @@ -1,166 +1,42 @@ import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/emoji/emoji_handler.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; + +import '../../shared/keyboard.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - Future prepare(WidgetTester tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await tester.createNewPageWithNameUnderParent(); - await tester.editor.tapLineOfEditorAt(0); - } - // May be better to move this to an existing test but unsure what it fits with group('Keyboard shortcuts related to emojis', () { testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker', (tester) async { - await prepare(tester); + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); - expect(find.byType(EmojiHandler), findsNothing); + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyE, - isAltPressed: true, - isMetaPressed: Platform.isMacOS, - isControlPressed: !Platform.isMacOS, + expect(find.byType(EmojiSelectionMenu), findsNothing); + + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + Platform.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.alt, + LogicalKeyboardKey.keyE, + ], + tester: tester, ); - await tester.pumpAndSettle(Duration(seconds: 1)); - expect(find.byType(EmojiHandler), findsOneWidget); - /// press backspace to hide the emoji picker - await tester.simulateKeyEvent(LogicalKeyboardKey.backspace); - expect(find.byType(EmojiHandler), findsNothing); - }); - - testWidgets('insert emoji by slash menu', (tester) async { - await prepare(tester); - await tester.editor.showSlashMenu(); - - /// show emoji picler - await tester.editor.tapSlashMenuItemWithName( - LocaleKeys.document_slashMenu_name_emoji.tr(), - offset: 100, - ); - await tester.pumpAndSettle(Duration(seconds: 1)); - expect(find.byType(EmojiHandler), findsOneWidget); - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - - /// except the emoji is in document - expect(firstNode.delta!.toPlainText().contains('😀'), true); - }); - }); - - group('insert emoji by colon', () { - Future createNewDocumentAndShowEmojiList( - WidgetTester tester, { - String? search, - }) async { - await prepare(tester); - await tester.ime.insertText(':${search ?? 'a'}'); - await tester.pumpAndSettle(Duration(seconds: 1)); - } - - testWidgets('insert with click', (tester) async { - await createNewDocumentAndShowEmojiList(tester); - - /// emoji list is showing - final emojiHandler = find.byType(EmojiHandler); - expect(emojiHandler, findsOneWidget); - final emojiButtons = - find.descendant(of: emojiHandler, matching: find.byType(FlowyButton)); - final firstTextFinder = find.descendant( - of: emojiButtons.first, - matching: find.byType(FlowyText), - ); - final emojiText = - (firstTextFinder.evaluate().first.widget as FlowyText).text; - - /// click first emoji item - await tester.tapButton(emojiButtons.first); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - - /// except the emoji is in document - expect(emojiText.contains(firstNode.delta!.toPlainText()), true); - }); - - testWidgets('insert with arrow and enter', (tester) async { - await createNewDocumentAndShowEmojiList(tester); - - /// emoji list is showing - final emojiHandler = find.byType(EmojiHandler); - expect(emojiHandler, findsOneWidget); - final emojiButtons = - find.descendant(of: emojiHandler, matching: find.byType(FlowyButton)); - - /// tap arrow down and arrow up - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowUp); - await tester.simulateKeyEvent(LogicalKeyboardKey.arrowDown); - - final firstTextFinder = find.descendant( - of: emojiButtons.first, - matching: find.byType(FlowyText), - ); - final emojiText = - (firstTextFinder.evaluate().first.widget as FlowyText).text; - - /// tap enter - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - - /// except the emoji is in document - expect(emojiText.contains(firstNode.delta!.toPlainText()), true); - }); - - testWidgets('insert with searching', (tester) async { - await createNewDocumentAndShowEmojiList(tester, search: 's'); - - /// search for `smiling eyes`, IME is not working, use keyboard input - final searchText = [ - LogicalKeyboardKey.keyM, - LogicalKeyboardKey.keyI, - LogicalKeyboardKey.keyL, - LogicalKeyboardKey.keyI, - LogicalKeyboardKey.keyN, - LogicalKeyboardKey.keyG, - LogicalKeyboardKey.space, - LogicalKeyboardKey.keyE, - LogicalKeyboardKey.keyY, - LogicalKeyboardKey.keyE, - LogicalKeyboardKey.keyS, - ]; - - for (final key in searchText) { - await tester.simulateKeyEvent(key); - } - - /// tap enter - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0])!; - - /// except the emoji is in document - expect(firstNode.delta!.toPlainText().contains('😄'), true); - }); - - testWidgets('start searching with sapce', (tester) async { - await createNewDocumentAndShowEmojiList(tester, search: ' '); - - /// emoji list is showing - final emojiHandler = find.byType(EmojiHandler); - expect(emojiHandler, findsNothing); + expect(find.byType(EmojiSelectionMenu), findsOneWidget); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart new file mode 100644 index 0000000000..a058ff2281 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/empty_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +// This test is meaningless, just for preventing the CI from failing. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Empty', () { + testWidgets('toggle theme mode', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart index 1d0f13eebc..4a38dde920 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/hotkeys_test.dart @@ -1,12 +1,13 @@ import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:easy_localization/easy_localization.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'; @@ -37,7 +38,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_light.tr(), ), ); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); + await tester.pumpAndSettle(); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.light); @@ -47,7 +48,7 @@ void main() { LocaleKeys.settings_workspacePage_appearance_options_dark.tr(), ), ); - await tester.pumpAndSettle(const Duration(milliseconds: 250)); + await tester.pumpAndSettle(); themeMode = tester.widget(appFinder).themeMode; expect(themeMode, ThemeMode.dark); @@ -65,11 +66,10 @@ void main() { ], tester: tester, ); - await tester.pumpAndSettle(const Duration(milliseconds: 500)); + await tester.pumpAndSettle(); - // disable it temporarily. It works on macOS but not on Linux. - // themeMode = tester.widget(appFinder).themeMode; - // expect(themeMode, ThemeMode.light); + themeMode = tester.widget(appFinder).themeMode; + expect(themeMode, ThemeMode.light); }); testWidgets('show or hide home menu', (tester) async { diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart index 84da89f6b7..ffe65ea7cc 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -83,12 +82,12 @@ void main() { HeadingBlockKeys.type, ); expect( - importedPageEditorState.getNodeAtPath([1])!.type, + importedPageEditorState.getNodeAtPath([2])!.type, HeadingBlockKeys.type, ); expect( - importedPageEditorState.getNodeAtPath([2])!.type, - SimpleTableBlockKeys.type, + importedPageEditorState.getNodeAtPath([4])!.type, + TableBlockKeys.type, ); }); }); 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 new file mode 100644 index 0000000000..f739820d04 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart @@ -0,0 +1,113 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart'; +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/desktop/uncategorized/share_markdown_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart index 8c3c29ab77..f73e61ea82 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/share_markdown_test.dart @@ -1,16 +1,12 @@ -import 'dart:convert'; import 'dart:io'; -import 'package:appflowy/plugins/shared/share/share_button.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:archive/archive.dart'; +import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; -import '../document/document_with_database_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -22,7 +18,7 @@ void main() { // mock the file picker final path = await mockSaveFilePath( - p.join(context.applicationDataDirectory, 'test.zip'), + p.join(context.applicationDataDirectory, 'test.md'), ); // click the share button and select markdown await tester.tapShareButton(); @@ -32,14 +28,10 @@ void main() { tester.expectToExportSuccess(); final file = File(path); - expect(file.existsSync(), true); - final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); - for (final entry in archive) { - if (entry.isFile && entry.name.endsWith('.md')) { - final markdown = utf8.decode(entry.content); - expect(markdown, expectedMarkdown); - } - } + final isExist = file.existsSync(); + expect(isExist, true); + final markdown = file.readAsStringSync(); + expect(markdown, expectedMarkdown); }); testWidgets( @@ -59,13 +51,13 @@ void main() { }, ); - final shareButton = find.byType(ShareButton); - final shareButtonState = tester.widget(shareButton) as ShareButton; - + final shareButton = find.byType(ShareActionList); + final shareButtonState = + tester.state(shareButton) as ShareActionListState; final path = await mockSaveFilePath( p.join( context.applicationDataDirectory, - '${shareButtonState.view.name}.zip', + '${shareButtonState.name}.md', ), ); @@ -77,44 +69,10 @@ void main() { tester.expectToExportSuccess(); final file = File(path); - expect(file.existsSync(), true); - final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); - for (final entry in archive) { - if (entry.isFile && entry.name.endsWith('.md')) { - final markdown = utf8.decode(entry.content); - expect(markdown, expectedMarkdown); - } - } + final isExist = file.existsSync(); + expect(isExist, true); }, ); - - testWidgets('share the markdown with database', (tester) async { - final context = await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - await insertLinkedDatabase(tester, ViewLayoutPB.Grid); - - // mock the file picker - final path = await mockSaveFilePath( - p.join(context.applicationDataDirectory, 'test.zip'), - ); - // click the share button and select markdown - await tester.tapShareButton(); - await tester.tapMarkdownButton(); - - // expect to see the success dialog - tester.expectToExportSuccess(); - - final file = File(path); - expect(file.existsSync(), true); - final archive = ZipDecoder().decodeBytes(file.readAsBytesSync()); - bool hasCsvFile = false; - for (final entry in archive) { - if (entry.isFile && entry.name.endsWith('.csv')) { - hasCsvFile = true; - } - } - expect(hasCsvFile, true); - }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart index 63ec958c54..7deea4aae4 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/tabs_test.dart @@ -1,23 +1,17 @@ -import 'dart:convert'; import 'dart:io'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart'; import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart'; -import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import '../../shared/base.dart'; +import '../../shared/common_operations.dart'; +import '../../shared/expectation.dart'; import '../../shared/keyboard.dart'; -import '../../shared/util.dart'; const _documentName = 'First Doc'; const _documentTwoName = 'Second Doc'; @@ -26,12 +20,17 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Tabs', () { - testWidgets('open/navigate/close tabs', (tester) async { + testWidgets('Open AppFlowy and open/navigate/close tabs', (tester) async { await tester.initializeAppFlowy(); await tester.tapAnonymousSignInButton(); - // No tabs rendered yet - expect(find.byType(FlowyTab), findsNothing); + expect( + find.descendant( + of: find.byType(TabsManager), + matching: find.byType(TabBar), + ), + findsNothing, + ); await tester.createNewPageWithNameUnderParent(name: _documentName); @@ -45,7 +44,7 @@ void main() { expect( find.descendant( - of: find.byType(TabsManager), + of: find.byType(TabBar), matching: find.byType(FlowyTab), ), findsNWidgets(3), @@ -72,300 +71,11 @@ void main() { expect( find.descendant( - of: find.byType(TabsManager), + of: find.byType(TabBar), matching: find.byType(FlowyTab), ), findsNWidgets(2), ); }); - - testWidgets('right click show tab menu, close others', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(TabBar), - ), - findsNothing, - ); - - await tester.createNewPageWithNameUnderParent(name: _documentName); - await tester.createNewPageWithNameUnderParent(name: _documentTwoName); - - /// Open second menu item in a new tab - await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document); - - /// Open third menu item in a new tab - await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(FlowyTab), - ), - findsNWidgets(3), - ); - - /// Right click on second tab - await tester.tap( - buttons: kSecondaryButton, - find.descendant( - of: find.byType(FlowyTab), - matching: find.text(gettingStarted), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byType(TabMenu), findsOneWidget); - - final firstTabFinder = find.descendant( - of: find.byType(FlowyTab), - matching: find.text(_documentTwoName), - ); - final secondTabFinder = find.descendant( - of: find.byType(FlowyTab), - matching: find.text(gettingStarted), - ); - final thirdTabFinder = find.descendant( - of: find.byType(FlowyTab), - matching: find.text(_documentName), - ); - - expect(firstTabFinder, findsOneWidget); - expect(secondTabFinder, findsOneWidget); - expect(thirdTabFinder, findsOneWidget); - - // Close other tabs than the second item - await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr())); - await tester.pumpAndSettle(); - - // We expect to not find any tabs - expect(firstTabFinder, findsNothing); - expect(secondTabFinder, findsNothing); - expect(thirdTabFinder, findsNothing); - - // Expect second tab to be current page (current page has breadcrumb, cover title, - // and in this case view name in sidebar) - expect(find.text(gettingStarted), findsNWidgets(3)); - }); - - testWidgets('cannot close pinned tabs', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(TabBar), - ), - findsNothing, - ); - - await tester.createNewPageWithNameUnderParent(name: _documentName); - await tester.createNewPageWithNameUnderParent(name: _documentTwoName); - - // Open second menu item in a new tab - await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document); - - // Open third menu item in a new tab - await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(FlowyTab), - ), - findsNWidgets(3), - ); - - const firstTab = _documentTwoName; - const secondTab = gettingStarted; - const thirdTab = _documentName; - - expect(tester.isTabAtIndex(firstTab, 0), isTrue); - expect(tester.isTabAtIndex(secondTab, 1), isTrue); - expect(tester.isTabAtIndex(thirdTab, 2), isTrue); - - expect(tester.isTabPinned(gettingStarted), isFalse); - - // Right click on second tab - await tester.openTabMenu(gettingStarted); - expect(find.byType(TabMenu), findsOneWidget); - - // Pin second tab - await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr())); - await tester.pumpAndSettle(); - - expect(tester.isTabPinned(gettingStarted), isTrue); - - /// Right click on first unpinned tab (second tab) - await tester.openTabMenu(_documentTwoName); - - // Close others - await tester.tap(find.text(LocaleKeys.tabMenu_closeOthers.tr())); - await tester.pumpAndSettle(); - - // We expect to find 2 tabs, the first pinned tab and the second tab - expect(find.byType(FlowyTab), findsNWidgets(2)); - expect(tester.isTabAtIndex(gettingStarted, 0), isTrue); - expect(tester.isTabAtIndex(_documentTwoName, 1), isTrue); - }); - - testWidgets('pin/unpin tabs proper order', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(TabBar), - ), - findsNothing, - ); - - await tester.createNewPageWithNameUnderParent(name: _documentName); - await tester.createNewPageWithNameUnderParent(name: _documentTwoName); - - // Open second menu item in a new tab - await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document); - - // Open third menu item in a new tab - await tester.openAppInNewTab(_documentName, ViewLayoutPB.Document); - - expect( - find.descendant( - of: find.byType(TabsManager), - matching: find.byType(FlowyTab), - ), - findsNWidgets(3), - ); - - const firstTabName = _documentTwoName; - const secondTabName = gettingStarted; - const thirdTabName = _documentName; - - // Expect correct order - expect(tester.isTabAtIndex(firstTabName, 0), isTrue); - expect(tester.isTabAtIndex(secondTabName, 1), isTrue); - expect(tester.isTabAtIndex(thirdTabName, 2), isTrue); - - // Pin second tab - await tester.openTabMenu(secondTabName); - await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr())); - await tester.pumpAndSettle(); - - expect(tester.isTabPinned(secondTabName), isTrue); - - // Expect correct order - expect(tester.isTabAtIndex(secondTabName, 0), isTrue); - expect(tester.isTabAtIndex(firstTabName, 1), isTrue); - expect(tester.isTabAtIndex(thirdTabName, 2), isTrue); - - // Pin new second tab (first tab) - await tester.openTabMenu(firstTabName); - await tester.tap(find.text(LocaleKeys.tabMenu_pinTab.tr())); - await tester.pumpAndSettle(); - - expect(tester.isTabPinned(firstTabName), isTrue); - expect(tester.isTabPinned(secondTabName), isTrue); - expect(tester.isTabPinned(thirdTabName), isFalse); - - expect(tester.isTabAtIndex(secondTabName, 0), isTrue); - expect(tester.isTabAtIndex(firstTabName, 1), isTrue); - expect(tester.isTabAtIndex(thirdTabName, 2), isTrue); - - // Unpin second tab - await tester.openTabMenu(secondTabName); - await tester.tap(find.text(LocaleKeys.tabMenu_unpinTab.tr())); - await tester.pumpAndSettle(); - - expect(tester.isTabPinned(firstTabName), isTrue); - expect(tester.isTabPinned(secondTabName), isFalse); - expect(tester.isTabPinned(thirdTabName), isFalse); - - expect(tester.isTabAtIndex(firstTabName, 0), isTrue); - expect(tester.isTabAtIndex(secondTabName, 1), isTrue); - expect(tester.isTabAtIndex(thirdTabName, 2), isTrue); - }); - - testWidgets('displaying icons in tab', (tester) async { - RecentIcons.enable = false; - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final icon = await tester.loadIcon(); - // update emoji - await tester.updatePageIconInSidebarByName( - name: gettingStarted, - parentName: gettingStarted, - layout: ViewLayoutPB.Document, - icon: icon, - ); - - /// create new page - await tester.createNewPageWithNameUnderParent(name: _documentName); - - /// open new tab for [gettingStarted] - await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document); - - final tabs = find.descendant( - of: find.byType(TabsManager), - matching: find.byType(FlowyTab), - ); - expect(tabs, findsNWidgets(2)); - - final svgInTab = - find.descendant(of: tabs.last, matching: find.byType(FlowySvg)); - final svgWidget = svgInTab.evaluate().first.widget as FlowySvg; - final iconsData = IconsData.fromJson(jsonDecode(icon.emoji)); - expect(svgWidget.svgString, iconsData.svgString); - }); }); } - -extension _TabsTester on WidgetTester { - bool isTabPinned(String tabName) { - final tabFinder = find.ancestor( - of: find.byWidgetPredicate( - (w) => w is ViewTabBarItem && w.view.name == tabName, - ), - matching: find.byType(FlowyTab), - ); - - final FlowyTab tabWidget = widget(tabFinder); - return tabWidget.pageManager.isPinned; - } - - bool isTabAtIndex(String tabName, int index) { - final tabFinder = find.ancestor( - of: find.byWidgetPredicate( - (w) => w is ViewTabBarItem && w.view.name == tabName, - ), - matching: find.byType(FlowyTab), - ); - - final pluginId = (widget(tabFinder) as FlowyTab).pageManager.plugin.id; - - final pluginIds = find - .byType(FlowyTab) - .evaluate() - .map((e) => (e.widget as FlowyTab).pageManager.plugin.id); - - return pluginIds.elementAt(index) == pluginId; - } - - Future openTabMenu(String tabName) async { - await tap( - buttons: kSecondaryButton, - find.ancestor( - of: find.byWidgetPredicate( - (w) => w is ViewTabBarItem && w.view.name == tabName, - ), - matching: find.byType(FlowyTab), - ), - ); - await pumpAndSettle(); - } -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart deleted file mode 100644 index 836cfe4ccd..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/uncategorized_test_runner_1.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'emoji_shortcut_test.dart' as emoji_shortcut_test; -import 'hotkeys_test.dart' as hotkeys_test; -import 'import_files_test.dart' as import_files_test; -import 'share_markdown_test.dart' as share_markdown_test; -import 'zoom_in_out_test.dart' as zoom_in_out_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // This test must be run first, otherwise the CI will fail. - hotkeys_test.main(); - emoji_shortcut_test.main(); - hotkeys_test.main(); - share_markdown_test.main(); - import_files_test.main(); - zoom_in_out_test.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/zoom_in_out_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/zoom_in_out_test.dart deleted file mode 100644 index f0cddadf68..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/zoom_in_out_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../shared/base.dart'; -import '../../shared/common_operations.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('Zoom in/out:', () { - Future resetAppFlowyScaleFactor( - WindowSizeManager windowSizeManager, - ) async { - appflowyScaleFactor = 1.0; - await windowSizeManager.setScaleFactor(1.0); - } - - testWidgets('Zoom in', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - double currentScaleFactor = 1.0; - - // this value can't be defined in the setUp method, because the windowSizeManager is not initialized yet. - final windowSizeManager = WindowSizeManager(); - await resetAppFlowyScaleFactor(windowSizeManager); - - // zoom in 2 times - for (final keycode in zoomInKeyCodes) { - if (UniversalPlatform.isLinux && - keycode.logicalKey == LogicalKeyboardKey.add) { - // Key LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add") not found in - // linux keyCode map - continue; - } - - // test each keycode 2 times - for (var i = 0; i < 2; i++) { - await tester.simulateKeyEvent( - keycode.logicalKey, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - // Register the physical key for the "Add" key, otherwise the test will fail and throw an error: - // Physical key for LogicalKeyboardKey#79c9b(keyId: "0x0000002b", keyLabel: "+", debugName: "Add") - // not found in known physical keys - physicalKey: keycode.logicalKey == LogicalKeyboardKey.add - ? PhysicalKeyboardKey.equal - : null, - ); - - await tester.pumpAndSettle(); - - currentScaleFactor += 0.1; - - final scaleFactor = await windowSizeManager.getScaleFactor(); - expect(currentScaleFactor, appflowyScaleFactor); - expect(currentScaleFactor, scaleFactor); - } - } - }); - - testWidgets('Reset zoom', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - final windowSizeManager = WindowSizeManager(); - - for (final keycode in resetZoomKeyCodes) { - await tester.simulateKeyEvent( - keycode.logicalKey, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - final scaleFactor = await windowSizeManager.getScaleFactor(); - expect(1.0, appflowyScaleFactor); - expect(1.0, scaleFactor); - } - }); - - testWidgets('Zoom out', (tester) async { - await tester.initializeAppFlowy(); - await tester.tapAnonymousSignInButton(); - - double currentScaleFactor = 1.0; - - final windowSizeManager = WindowSizeManager(); - await resetAppFlowyScaleFactor(windowSizeManager); - - // zoom out 2 times - for (final keycode in zoomOutKeyCodes) { - if (UniversalPlatform.isLinux && - keycode.logicalKey == LogicalKeyboardKey.numpadSubtract) { - // Key LogicalKeyboardKey#2c39f(keyId: "0x20000022d", keyLabel: "Numpad Subtract", debugName: "Numpad - // Subtract") not found in linux keyCode map - continue; - } - // test each keycode 2 times - for (var i = 0; i < 2; i++) { - await tester.simulateKeyEvent( - keycode.logicalKey, - isControlPressed: !UniversalPlatform.isMacOS, - isMetaPressed: UniversalPlatform.isMacOS, - ); - await tester.pumpAndSettle(); - - currentScaleFactor -= 0.1; - - final scaleFactor = await windowSizeManager.getScaleFactor(); - expect(currentScaleFactor, appflowyScaleFactor); - expect(currentScaleFactor, scaleFactor); - } - } - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_1.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_1.dart index c91ba21edb..e972f49fbb 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_1.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_1.dart @@ -1,6 +1,7 @@ import 'package:integration_test/integration_test.dart'; -import 'desktop/document/document_test_runner_1.dart' as document_test_runner_1; +import 'desktop/document/document_test_runner.dart' as document_test_runner; +import 'desktop/uncategorized/empty_test.dart' as first_test; import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test; Future main() async { @@ -10,7 +11,11 @@ Future main() async { Future runIntegration1OnDesktop() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // This test must be run first, otherwise the CI will fail. + first_test.main(); + switch_folder_test.main(); - document_test_runner_1.main(); - // DON'T add more tests here. + document_test_runner.startTesting(); + + // DON'T add more tests here. This is the first test runner for desktop. } diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart index 99d6f7d58f..9053da8d18 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_2.dart @@ -1,7 +1,18 @@ import 'package:integration_test/integration_test.dart'; -import 'desktop/database/database_test_runner_1.dart' as database_test_runner_1; -import 'desktop/first_test/first_test.dart' as first_test; +import 'desktop/database/database_calendar_test.dart' as database_calendar_test; +import 'desktop/database/database_cell_test.dart' as database_cell_test; +import 'desktop/database/database_field_settings_test.dart' + as database_field_settings_test; +import 'desktop/database/database_field_test.dart' as database_field_test; +import 'desktop/database/database_filter_test.dart' as database_filter_test; +import 'desktop/database/database_row_page_test.dart' as database_row_page_test; +import 'desktop/database/database_row_test.dart' as database_row_test; +import 'desktop/database/database_setting_test.dart' as database_setting_test; +import 'desktop/database/database_share_test.dart' as database_share_test; +import 'desktop/database/database_sort_test.dart' as database_sort_test; +import 'desktop/database/database_view_test.dart' as database_view_test; +import 'desktop/uncategorized/empty_test.dart' as first_test; Future main() async { await runIntegration2OnDesktop(); @@ -10,8 +21,20 @@ Future main() async { Future runIntegration2OnDesktop() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // This test must be run first, otherwise the CI will fail. first_test.main(); - database_test_runner_1.main(); + database_cell_test.main(); + database_field_test.main(); + database_field_settings_test.main(); + database_share_test.main(); + database_row_page_test.main(); + database_row_test.main(); + database_setting_test.main(); + database_filter_test.main(); + database_sort_test.main(); + database_view_test.main(); + database_calendar_test.main(); + // DON'T add more tests here. This is the second test runner for desktop. } diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart index a9d3783f1d..f1025b8f1e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart @@ -1,8 +1,14 @@ import 'package:integration_test/integration_test.dart'; import 'desktop/board/board_test_runner.dart' as board_test_runner; -import 'desktop/first_test/first_test.dart' as first_test; -import 'desktop/grid/grid_test_runner_1.dart' as grid_test_runner_1; +import 'desktop/settings/settings_runner.dart' as settings_test_runner; +import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner; +import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test; +import 'desktop/uncategorized/empty_test.dart' as first_test; +import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test; +import 'desktop/uncategorized/import_files_test.dart' as import_files_test; +import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test; +import 'desktop/uncategorized/tabs_test.dart' as tabs_test; Future main() async { await runIntegration3OnDesktop(); @@ -11,9 +17,17 @@ Future main() async { Future runIntegration3OnDesktop() async { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // This test must be run first, otherwise the CI will fail. first_test.main(); + hotkeys_test.main(); + emoji_shortcut_test.main(); + hotkeys_test.main(); + emoji_shortcut_test.main(); + settings_test_runner.main(); + share_markdown_test.main(); + import_files_test.main(); + sidebar_test_runner.main(); board_test_runner.main(); - grid_test_runner_1.main(); - // DON'T add more tests here. + tabs_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_4.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_4.dart deleted file mode 100644 index e51c711549..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_4.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/document/document_test_runner_2.dart' as document_test_runner_2; -import 'desktop/first_test/first_test.dart' as first_test; - -Future main() async { - await runIntegration4OnDesktop(); -} - -Future runIntegration4OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - document_test_runner_2.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_5.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_5.dart deleted file mode 100644 index be393e90c7..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_5.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/database/database_test_runner_2.dart' as database_test_runner_2; -import 'desktop/first_test/first_test.dart' as first_test; - -Future main() async { - await runIntegration5OnDesktop(); -} - -Future runIntegration5OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - database_test_runner_2.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_6.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_6.dart deleted file mode 100644 index a1c5627b20..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_6.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/first_test/first_test.dart' as first_test; -import 'desktop/settings/settings_runner.dart' as settings_test_runner; -import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner; -import 'desktop/uncategorized/uncategorized_test_runner_1.dart' - as uncategorized_test_runner_1; - -Future main() async { - await runIntegration6OnDesktop(); -} - -Future runIntegration6OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - settings_test_runner.main(); - sidebar_test_runner.main(); - uncategorized_test_runner_1.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_7.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_7.dart deleted file mode 100644 index 0200591c57..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_7.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/document/document_test_runner_3.dart' as document_test_runner_3; -import 'desktop/first_test/first_test.dart' as first_test; - -Future main() async { - await runIntegration7OnDesktop(); -} - -Future runIntegration7OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - document_test_runner_3.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_8.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_8.dart deleted file mode 100644 index 5a706e5dec..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_8.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/document/document_test_runner_4.dart' as document_test_runner_4; -import 'desktop/first_test/first_test.dart' as first_test; - -Future main() async { - await runIntegration8OnDesktop(); -} - -Future runIntegration8OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - - document_test_runner_4.main(); - // DON'T add more tests here. -} diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_9.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_9.dart deleted file mode 100644 index 451e24cdc1..0000000000 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_9.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'desktop/database/database_icon_test.dart' as database_icon_test; -import 'desktop/first_test/first_test.dart' as first_test; -import 'desktop/uncategorized/code_block_language_selector_test.dart' - as code_language_selector; -import 'desktop/uncategorized/tabs_test.dart' as tabs_test; - -Future main() async { - await runIntegration9OnDesktop(); -} - -Future runIntegration9OnDesktop() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - first_test.main(); - tabs_test.main(); - code_language_selector.main(); - database_icon_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/mobile/cloud/cloud_runner.dart deleted file mode 100644 index c2f3d7103a..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/cloud/cloud_runner.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'document/publish_test.dart' as publish_test; -import 'document/share_link_test.dart' as share_link_test; -import 'space/space_test.dart' as space_test; -import 'workspace/workspace_operations_test.dart' as workspace_operations_test; - -Future main() async { - workspace_operations_test.main(); - share_link_test.main(); - publish_test.main(); - space_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/cloud/document/publish_test.dart b/frontend/appflowy_flutter/integration_test/mobile/cloud/document/publish_test.dart deleted file mode 100644 index e6015d0896..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/cloud/document/publish_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('publish:', () { - testWidgets(''' -1. publish document -2. update path name -3. unpublish document -''', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - await tester.openPage(Constants.gettingStartedPageName); - await tester.editor.openMoreActionMenuOnMobile(); - - // click the publish button - await tester.editor.clickMoreActionItemOnMobile( - LocaleKeys.shareAction_publish.tr(), - ); - - // wait the notification dismiss - final publishSuccessText = find.findTextInFlowyText( - LocaleKeys.publish_publishSuccessfully.tr(), - ); - expect(publishSuccessText, findsOneWidget); - await tester.pumpUntilNotFound(publishSuccessText); - - // open the menu again, to check the publish status - await tester.editor.openMoreActionMenuOnMobile(); - // expect to see the unpublish button and the visit site button - expect( - find.text(LocaleKeys.shareAction_unPublish.tr()), - findsOneWidget, - ); - expect( - find.text(LocaleKeys.shareAction_visitSite.tr()), - findsOneWidget, - ); - - // update the path name - await tester.editor.clickMoreActionItemOnMobile( - LocaleKeys.shareAction_updatePathName.tr(), - ); - - const pathName1 = '???????????????'; - const pathName2 = 'AppFlowy'; - - final textField = find.descendant( - of: find.byType(EditWorkspaceNameBottomSheet), - matching: find.byType(TextFormField), - ); - await tester.enterText(textField, pathName1); - await tester.pumpAndSettle(); - - // wait 50ms to ensure the error message is shown - await tester.wait(50); - - // click the confirm button - final confirmButton = find.text(LocaleKeys.button_confirm.tr()); - await tester.tapButton(confirmButton); - - // expect to see the update path name failed toast - final updatePathFailedText = find.text( - LocaleKeys.settings_sites_error_publishNameContainsInvalidCharacters - .tr(), - ); - expect(updatePathFailedText, findsOneWidget); - - // input the valid path name - await tester.enterText(textField, pathName2); - await tester.pumpAndSettle(); - // click the confirm button - await tester.tapButton(confirmButton); - - // wait 50ms to ensure the error message is shown - await tester.wait(50); - - // expect to see the update path name success toast - final updatePathSuccessText = find.findTextInFlowyText( - LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - expect(updatePathSuccessText, findsOneWidget); - await tester.pumpUntilNotFound(updatePathSuccessText); - - // unpublish the document - await tester.editor.clickMoreActionItemOnMobile( - LocaleKeys.shareAction_unPublish.tr(), - ); - final unPublishSuccessText = find.findTextInFlowyText( - LocaleKeys.publish_unpublishSuccessfully.tr(), - ); - expect(unPublishSuccessText, findsOneWidget); - await tester.pumpUntilNotFound(unPublishSuccessText); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/cloud/document/share_link_test.dart b/frontend/appflowy_flutter/integration_test/mobile/cloud/document/share_link_test.dart deleted file mode 100644 index bf0ddc8711..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/cloud/document/share_link_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('share link:', () { - testWidgets('copy share link and paste it on doc', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // open the getting started page and paste the link - await tester.openPage(Constants.gettingStartedPageName); - - // open the more action menu - await tester.editor.openMoreActionMenuOnMobile(); - - // click the share link item - await tester.editor.clickMoreActionItemOnMobile( - LocaleKeys.shareAction_copyLink.tr(), - ); - - // check the clipboard - final content = await Clipboard.getData(Clipboard.kTextPlain); - expect( - content?.text, - matches(appflowySharePageLinkPattern), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/cloud/space/space_test.dart b/frontend/appflowy_flutter/integration_test/mobile/cloud/space/space_test.dart deleted file mode 100644 index e7bf3afcc7..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/cloud/space/space_test.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/home/space/manage_space_widget.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/home/space/space_menu_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/home/space/widgets.dart'; -import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('space operations:', () { - Future openSpaceMenu(WidgetTester tester) async { - final spaceHeader = find.byType(MobileSpaceHeader); - await tester.tapButton(spaceHeader); - await tester.pumpUntilFound(find.byType(MobileSpaceMenu)); - } - - Future openSpaceMenuMoreOptions( - WidgetTester tester, - ViewPB space, - ) async { - final spaceMenuItemTrailing = find.byWidgetPredicate( - (w) => w is SpaceMenuItemTrailing && w.space.id == space.id, - ); - final moreOptions = find.descendant( - of: spaceMenuItemTrailing, - matching: find.byWidgetPredicate( - (w) => - w is FlowySvg && - w.svg.path == FlowySvgs.workspace_three_dots_s.path, - ), - ); - await tester.tapButton(moreOptions); - await tester.pumpUntilFound(find.byType(SpaceMenuMoreOptions)); - } - - // combine the tests together to reduce the CI time - testWidgets(''' -1. create a new space -2. update the space name -3. update the space permission -4. update the space icon -5. delete the space -''', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // 1. create a new space - // click the space menu - await openSpaceMenu(tester); - - // click the create a new space button - final createNewSpaceButton = find.text( - LocaleKeys.space_createNewSpace.tr(), - ); - await tester.pumpUntilFound(createNewSpaceButton); - await tester.tapButton(createNewSpaceButton); - - // input the new space name - final inputField = find.descendant( - of: find.byType(ManageSpaceWidget), - matching: find.byType(TextField), - ); - const newSpaceName = 'AppFlowy'; - await tester.enterText(inputField, newSpaceName); - await tester.pumpAndSettle(); - - // change the space permission to private - final permissionOption = find.byType(ManageSpacePermissionOption); - await tester.tapButton(permissionOption); - await tester.pumpAndSettle(); - - final privateOption = find.text(LocaleKeys.space_privatePermission.tr()); - await tester.tapButton(privateOption); - await tester.pumpAndSettle(); - - // change the space icon color - final color = builtInSpaceColors[1]; - final iconOption = find.descendant( - of: find.byType(ManageSpaceIconOption), - matching: find.byWidgetPredicate( - (w) => w is SpaceColorItem && w.color == color, - ), - ); - await tester.tapButton(iconOption); - await tester.pumpAndSettle(); - - // change the space icon - final icon = kIconGroups![0].icons[1]; - final iconItem = find.descendant( - of: find.byType(ManageSpaceIconOption), - matching: find.byWidgetPredicate( - (w) => w is SpaceIconItem && w.icon == icon, - ), - ); - await tester.tapButton(iconItem); - await tester.pumpAndSettle(); - - // click the done button - final doneButton = find.descendant( - of: find.byWidgetPredicate( - (w) => - w is BottomSheetHeader && - w.title == LocaleKeys.space_createSpace.tr(), - ), - matching: find.text(LocaleKeys.button_done.tr()), - ); - await tester.tapButton(doneButton); - await tester.pumpAndSettle(); - - // wait 100ms for the space to be created - await tester.wait(100); - - // verify the space is created - await openSpaceMenu(tester); - final spaceItems = find.byType(MobileSpaceMenuItem); - // expect to see 3 space items, 2 are built-in, 1 is the new space - expect(spaceItems, findsNWidgets(3)); - // convert the space item to a widget - final spaceWidget = - tester.widgetList(spaceItems).last; - final space = spaceWidget.space; - expect(space.name, newSpaceName); - expect(space.spacePermission, SpacePermission.private); - expect(space.spaceIcon, icon.iconPath); - expect(space.spaceIconColor, color); - - // open the SpaceMenuMoreOptions menu - await openSpaceMenuMoreOptions(tester, space); - - // 2. rename the space name - final renameOption = find.text(LocaleKeys.button_rename.tr()); - await tester.tapButton(renameOption); - await tester.pumpUntilFound(find.byType(EditWorkspaceNameBottomSheet)); - - // input the new space name - final renameInputField = find.descendant( - of: find.byType(EditWorkspaceNameBottomSheet), - matching: find.byType(TextField), - ); - const renameSpaceName = 'HelloWorld'; - await tester.enterText(renameInputField, renameSpaceName); - await tester.pumpAndSettle(); - await tester.tapButton(find.text(LocaleKeys.button_confirm.tr())); - - // click the done button - await tester.pumpAndSettle(); - - final renameSuccess = find.text( - LocaleKeys.space_success_renameSpace.tr(), - ); - await tester.pumpUntilNotFound(renameSuccess); - - // check the space name is updated - await openSpaceMenu(tester); - final renameSpaceItem = find.descendant( - of: find.byType(MobileSpaceMenuItem), - matching: find.text(renameSpaceName), - ); - expect(renameSpaceItem, findsOneWidget); - - // 3. manage the space - await openSpaceMenuMoreOptions(tester, space); - - final manageOption = find.text(LocaleKeys.space_manage.tr()); - await tester.tapButton(manageOption); - await tester.pumpUntilFound(find.byType(ManageSpaceWidget)); - - // 3.1 rename the space - final textField = find.descendant( - of: find.byType(ManageSpaceWidget), - matching: find.byType(TextField), - ); - await tester.enterText(textField, 'AppFlowy'); - await tester.pumpAndSettle(); - - // 3.2 change the permission - final permissionOption2 = find.byType(ManageSpacePermissionOption); - await tester.tapButton(permissionOption2); - await tester.pumpAndSettle(); - - final publicOption = find.text(LocaleKeys.space_publicPermission.tr()); - await tester.tapButton(publicOption); - await tester.pumpAndSettle(); - - // 3.3 change the icon - // change the space icon color - final color2 = builtInSpaceColors[2]; - final iconOption2 = find.descendant( - of: find.byType(ManageSpaceIconOption), - matching: find.byWidgetPredicate( - (w) => w is SpaceColorItem && w.color == color2, - ), - ); - await tester.tapButton(iconOption2); - await tester.pumpAndSettle(); - - // change the space icon - final icon2 = kIconGroups![0].icons[2]; - final iconItem2 = find.descendant( - of: find.byType(ManageSpaceIconOption), - matching: find.byWidgetPredicate( - (w) => w is SpaceIconItem && w.icon == icon2, - ), - ); - await tester.tapButton(iconItem2); - await tester.pumpAndSettle(); - - // click the done button - final doneButton2 = find.descendant( - of: find.byWidgetPredicate( - (w) => - w is BottomSheetHeader && - w.title == LocaleKeys.space_manageSpace.tr(), - ), - matching: find.text(LocaleKeys.button_done.tr()), - ); - await tester.tapButton(doneButton2); - await tester.pumpAndSettle(); - - // check the space is updated - final spaceItems2 = find.byType(MobileSpaceMenuItem); - final spaceWidget2 = - tester.widgetList(spaceItems2).last; - final space2 = spaceWidget2.space; - expect(space2.name, 'AppFlowy'); - expect(space2.spacePermission, SpacePermission.publicToAll); - expect(space2.spaceIcon, icon2.iconPath); - expect(space2.spaceIconColor, color2); - final manageSuccess = find.text( - LocaleKeys.space_success_updateSpace.tr(), - ); - await tester.pumpUntilNotFound(manageSuccess); - - // 4. duplicate the space - await openSpaceMenuMoreOptions(tester, space); - final duplicateOption = find.text(LocaleKeys.space_duplicate.tr()); - await tester.tapButton(duplicateOption); - final duplicateSuccess = find.text( - LocaleKeys.space_success_duplicateSpace.tr(), - ); - await tester.pumpUntilNotFound(duplicateSuccess); - - // check the space is duplicated - await openSpaceMenu(tester); - final spaceItems3 = find.byType(MobileSpaceMenuItem); - expect(spaceItems3, findsNWidgets(4)); - - // 5. delete the space - await openSpaceMenuMoreOptions(tester, space); - final deleteOption = find.text(LocaleKeys.button_delete.tr()); - await tester.tapButton(deleteOption); - final confirmDeleteButton = find.descendant( - of: find.byType(CupertinoDialogAction), - matching: find.text(LocaleKeys.button_delete.tr()), - ); - await tester.tapButton(confirmDeleteButton); - final deleteSuccess = find.text( - LocaleKeys.space_success_deleteSpace.tr(), - ); - await tester.pumpUntilNotFound(deleteSuccess); - - // check the space is deleted - final spaceItems4 = find.byType(MobileSpaceMenuItem); - expect(spaceItems4, findsNWidgets(3)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/cloud/workspace/workspace_operations_test.dart b/frontend/appflowy_flutter/integration_test/mobile/cloud/workspace/workspace_operations_test.dart deleted file mode 100644 index 210d1bcf0e..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/cloud/workspace/workspace_operations_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../../shared/constants.dart'; -import '../../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('workspace operations:', () { - testWidgets('create a new workspace', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.appflowyCloudSelfHost, - ); - await tester.tapGoogleLoginInButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); - - // click the create a new workspace button - await tester.tapButton(find.text(Constants.defaultWorkspaceName)); - await tester.tapButton(find.text(LocaleKeys.workspace_create.tr())); - - // input the new workspace name - final inputField = find.byType(TextFormField); - const newWorkspaceName = 'AppFlowy'; - await tester.enterText(inputField, newWorkspaceName); - await tester.pumpAndSettle(); - - // wait for the workspace to be created - await tester.pumpUntilFound( - find.text(LocaleKeys.workspace_createSuccess.tr()), - ); - - // expect to see the new workspace - expect(find.text(newWorkspaceName), findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart deleted file mode 100644 index 2b348d3a2e..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/at_menu_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart'; -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - const title = 'Test At Menu'; - - group('at menu', () { - testWidgets('show at menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowAtMenu(title); - final menuWidget = find.byType(MobileInlineActionsMenu); - expect(menuWidget, findsOneWidget); - }); - - testWidgets('search by at menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowAtMenu(title); - const searchText = gettingStarted; - await tester.ime.insertText(searchText); - final actionWidgets = find.byType(MobileInlineActionsWidget); - expect(actionWidgets, findsNWidgets(2)); - }); - - testWidgets('tap at menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowAtMenu(title); - const searchText = gettingStarted; - await tester.ime.insertText(searchText); - final actionWidgets = find.byType(MobileInlineActionsWidget); - await tester.tap(actionWidgets.last); - expect(find.byType(MentionPageBlock), findsOneWidget); - }); - - testWidgets('create subpage with at menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile(title); - await tester.editor.tapLineOfEditorAt(0); - const subpageName = 'Subpage'; - await tester.ime.insertText('[[$subpageName'); - await tester.pumpAndSettle(); - final actionWidgets = find.byType(MobileInlineActionsWidget); - await tester.tapButton(actionWidgets.first); - final firstNode = - tester.editor.getCurrentEditorState().getNodeAtPath([0]); - assert(firstNode != null); - expect(firstNode!.delta?.toPlainText().contains('['), false); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/mobile/document/document_test_runner.dart deleted file mode 100644 index 90d5ca6d0d..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/document_test_runner.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -import 'at_menu_test.dart' as at_menu; -import 'at_menu_test.dart' as at_menu_test; -import 'page_style_test.dart' as page_style_test; -import 'plus_menu_test.dart' as plus_menu_test; -import 'simple_table_test.dart' as simple_table_test; -import 'slash_menu_test.dart' as slash_menu; -import 'title_test.dart' as title_test; -import 'toolbar_test.dart' as toolbar_test; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - // Document integration tests - title_test.main(); - page_style_test.main(); - plus_menu_test.main(); - at_menu_test.main(); - simple_table_test.main(); - toolbar_test.main(); - slash_menu.main(); - at_menu.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart deleted file mode 100644 index da7c7e92e7..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/icon_test.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document title:', () { - testWidgets('update page custom image icon in title bar', (tester) async { - await tester.launchInAnonymousMode(); - - /// prepare local image - final iconData = await tester.prepareImageIcon(); - - /// create an empty page - await tester - .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey)); - - /// show Page style page - await tester.tapButton(find.byType(MobileViewPageLayoutButton)); - final pageStyleIcon = find.byType(PageStyleIcon); - final iconInPageStyleIcon = find.descendant( - of: pageStyleIcon, - matching: find.byType(RawEmojiIconWidget), - ); - expect(iconInPageStyleIcon, findsNothing); - - /// show icon picker - await tester.tapButton(pageStyleIcon); - - /// upload custom icon - await tester.pickImage(iconData); - - /// check result - final documentPage = find.byType(MobileDocumentScreen); - final rawEmojiIconFinder = find - .descendant( - of: documentPage, - matching: find.byType(RawEmojiIconWidget), - ) - .last; - final rawEmojiIconWidget = - rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; - final iconDataInWidget = rawEmojiIconWidget.emoji; - expect(iconDataInWidget.type, FlowyIconType.custom); - final imageFinder = - find.descendant(of: rawEmojiIconFinder, matching: find.byType(Image)); - expect(imageFinder, findsOneWidget); - }); - - testWidgets('update page custom svg icon in title bar', (tester) async { - await tester.launchInAnonymousMode(); - - /// prepare local image - final iconData = await tester.prepareSvgIcon(); - - /// create an empty page - await tester - .tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey)); - - /// show Page style page - await tester.tapButton(find.byType(MobileViewPageLayoutButton)); - final pageStyleIcon = find.byType(PageStyleIcon); - final iconInPageStyleIcon = find.descendant( - of: pageStyleIcon, - matching: find.byType(RawEmojiIconWidget), - ); - expect(iconInPageStyleIcon, findsNothing); - - /// show icon picker - await tester.tapButton(pageStyleIcon); - - /// upload custom icon - await tester.pickImage(iconData); - - /// check result - final documentPage = find.byType(MobileDocumentScreen); - final rawEmojiIconFinder = find - .descendant( - of: documentPage, - matching: find.byType(RawEmojiIconWidget), - ) - .last; - final rawEmojiIconWidget = - rawEmojiIconFinder.evaluate().first.widget as RawEmojiIconWidget; - final iconDataInWidget = rawEmojiIconWidget.emoji; - expect(iconDataInWidget.type, FlowyIconType.custom); - final svgFinder = find.descendant( - of: rawEmojiIconFinder, - matching: find.byType(SvgPicture), - ); - expect(svgFinder, findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart deleted file mode 100644 index e3d3bc093f..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/page_style_test.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; -import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/emoji.dart'; -import '../../shared/util.dart'; - -void main() { - setUpAll(() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - RecentIcons.enable = false; - }); - - tearDownAll(() { - RecentIcons.enable = true; - }); - - group('document page style:', () { - double getCurrentEditorFontSize() { - final editorPage = find - .byType(AppFlowyEditorPage) - .evaluate() - .single - .widget as AppFlowyEditorPage; - return editorPage.styleCustomizer - .style() - .textStyleConfiguration - .text - .fontSize!; - } - - double getCurrentEditorLineHeight() { - final editorPage = find - .byType(AppFlowyEditorPage) - .evaluate() - .single - .widget as AppFlowyEditorPage; - return editorPage.styleCustomizer - .style() - .textStyleConfiguration - .lineHeight; - } - - testWidgets('change font size in page style settings', (tester) async { - await tester.launchInAnonymousMode(); - - // click the getting start page - await tester.openPage(gettingStarted); - // click the layout button - await tester.tapButton(find.byType(MobileViewPageLayoutButton)); - expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize); - // change font size from normal to large - await tester.tapSvgButton(FlowySvgs.m_font_size_large_s); - expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize); - // change font size from large to small - await tester.tapSvgButton(FlowySvgs.m_font_size_small_s); - expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize); - }); - - testWidgets('change line height in page style settings', (tester) async { - await tester.launchInAnonymousMode(); - - // click the getting start page - await tester.openPage(gettingStarted); - // click the layout button - await tester.tapButton(find.byType(MobileViewPageLayoutButton)); - var lineHeight = getCurrentEditorLineHeight(); - expect( - lineHeight, - PageStyleLineHeightLayout.normal.lineHeight, - ); - // change line height from normal to large - await tester.tapSvgButton(FlowySvgs.m_layout_large_s); - await tester.pumpAndSettle(); - lineHeight = getCurrentEditorLineHeight(); - expect( - lineHeight, - PageStyleLineHeightLayout.large.lineHeight, - ); - // change line height from large to small - await tester.tapSvgButton(FlowySvgs.m_layout_small_s); - lineHeight = getCurrentEditorLineHeight(); - expect( - lineHeight, - PageStyleLineHeightLayout.small.lineHeight, - ); - }); - - testWidgets('use built-in image as cover', (tester) async { - await tester.launchInAnonymousMode(); - - // click the getting start page - await tester.openPage(gettingStarted); - // click the layout button - await tester.tapButton(find.byType(MobileViewPageLayoutButton)); - // toggle the preset button - await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m); - - // select the first preset - final firstBuiltInImage = find.byWidgetPredicate( - (widget) => - widget is Image && - widget.image is AssetImage && - (widget.image as AssetImage).assetName == - PageStyleCoverImageType.builtInImagePath('1'), - ); - await tester.tap(firstBuiltInImage); - - // click done button to exit the page style settings - await tester.tapButton(find.byType(BottomSheetDoneButton).first); - - // check the cover - final builtInCover = find.descendant( - of: find.byType(DocumentImmersiveCover), - matching: firstBuiltInImage, - ); - expect(builtInCover, findsOneWidget); - }); - - testWidgets('page style icon', (tester) async { - await tester.launchInAnonymousMode(); - - final createPageButton = - find.byKey(BottomNavigationBarItemType.add.valueKey); - await tester.tapButton(createPageButton); - - /// toggle the preset button - await tester.tapSvgButton(FlowySvgs.m_layout_s); - - /// select document plugins emoji - final pageStyleIcon = find.byType(PageStyleIcon); - - /// there should be none of emoji - final noneText = find.text(LocaleKeys.pageStyle_none.tr()); - expect(noneText, findsOneWidget); - await tester.tapButton(pageStyleIcon); - - /// select an emoji - const emoji = '😄'; - await tester.tapEmoji(emoji); - await tester.tapSvgButton(FlowySvgs.m_layout_s); - expect(noneText, findsNothing); - expect( - find.descendant( - of: pageStyleIcon, - matching: find.text(emoji), - ), - findsOneWidget, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/plus_menu_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/plus_menu_test.dart deleted file mode 100644 index b54c543f7e..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/plus_menu_test.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart'; -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document plus menu:', () { - testWidgets('add the toggle heading blocks via plus menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('toggle heading blocks'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // open the plus menu and select the toggle heading block - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_toggleHeading1.tr(), - ); - - // check the block is inserted - final block1 = editorState.getNodeAtPath([0])!; - expect(block1.type, equals(ToggleListBlockKeys.type)); - expect(block1.attributes[ToggleListBlockKeys.level], equals(1)); - - // click the expand button won't cancel the selection - await tester.tapButton(find.byIcon(Icons.arrow_right)); - expect( - editorState.selection, - equals(Selection.collapsed(Position(path: [0]))), - ); - - // focus on the next line - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [1])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // open the plus menu and select the toggle heading block - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_toggleHeading2.tr(), - ); - - // check the block is inserted - final block2 = editorState.getNodeAtPath([1])!; - expect(block2.type, equals(ToggleListBlockKeys.type)); - expect(block2.attributes[ToggleListBlockKeys.level], equals(2)); - - // focus on the next line - await tester.pumpAndSettle(); - - // open the plus menu and select the toggle heading block - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_toggleHeading3.tr(), - ); - - // check the block is inserted - final block3 = editorState.getNodeAtPath([2])!; - expect(block3.type, equals(ToggleListBlockKeys.type)); - expect(block3.attributes[ToggleListBlockKeys.level], equals(3)); - - // wait a few milliseconds to ensure the selection is updated - await Future.delayed(const Duration(milliseconds: 100)); - // check the selection is collapsed - expect( - editorState.selection, - equals(Selection.collapsed(Position(path: [2]))), - ); - }); - - const title = 'Test Plus Menu'; - testWidgets('show plus menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowPlusMenu(title); - final menuWidget = find.byType(MobileInlineActionsMenu); - expect(menuWidget, findsOneWidget); - }); - - testWidgets('search by plus menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowPlusMenu(title); - const searchText = gettingStarted; - await tester.ime.insertText(searchText); - final actionWidgets = find.byType(MobileInlineActionsWidget); - expect(actionWidgets, findsNWidgets(2)); - }); - - testWidgets('tap plus menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowPlusMenu(title); - const searchText = gettingStarted; - await tester.ime.insertText(searchText); - final actionWidgets = find.byType(MobileInlineActionsWidget); - await tester.tap(actionWidgets.last); - expect(find.byType(MentionPageBlock), findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart deleted file mode 100644 index 546baebb31..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/simple_table_test.dart +++ /dev/null @@ -1,554 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('simple table:', () { - testWidgets(''' -1. insert a simple table via + menu -2. insert a row above the table -3. insert a row below the table -4. insert a column left to the table -5. insert a column right to the table -6. delete the first row -7. delete the first column -''', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('simple table'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - final firstParagraphPath = [0, 0, 0, 0]; - - // open the plus menu and select the table block - { - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_table.tr(), - ); - - // check the block is inserted - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.rowLength, equals(2)); - expect(table.columnLength, equals(2)); - - // focus on the first cell - - final selection = editorState.selection!; - expect(selection.isCollapsed, isTrue); - expect(selection.start.path, equals(firstParagraphPath)); - } - - // insert left and insert right - { - // click the column menu button - await tester.clickColumnMenuButton(0); - - // insert left, insert right - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(), - ), - ); - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_insertRight - .tr(), - ), - ); - - await tester.cancelTableActionMenu(); - - // check the table is updated - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.rowLength, equals(2)); - expect(table.columnLength, equals(4)); - } - - // insert above and insert below - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the row menu button - await tester.clickRowMenuButton(0); - - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove - .tr(), - ), - ); - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow - .tr(), - ), - ); - await tester.cancelTableActionMenu(); - - // check the table is updated - final table = editorState.getNodeAtPath([0])!; - expect(table.rowLength, equals(4)); - expect(table.columnLength, equals(4)); - } - - // delete the first row - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // delete the first row - await tester.clickRowMenuButton(0); - await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete); - await tester.cancelTableActionMenu(); - - // check the table is updated - final table = editorState.getNodeAtPath([0])!; - expect(table.rowLength, equals(3)); - expect(table.columnLength, equals(4)); - } - - // delete the first column - { - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - await tester.clickColumnMenuButton(0); - await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete); - await tester.cancelTableActionMenu(); - - // check the table is updated - final table = editorState.getNodeAtPath([0])!; - expect(table.rowLength, equals(3)); - expect(table.columnLength, equals(3)); - } - }); - - testWidgets(''' -1. insert a simple table via + menu -2. enable header column -3. enable header row -4. set to page width -5. distribute columns evenly -''', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('simple table'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - final firstParagraphPath = [0, 0, 0, 0]; - - // open the plus menu and select the table block - { - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_table.tr(), - ); - - // check the block is inserted - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.rowLength, equals(2)); - expect(table.columnLength, equals(2)); - - // focus on the first cell - - final selection = editorState.selection!; - expect(selection.isCollapsed, isTrue); - expect(selection.start.path, equals(firstParagraphPath)); - } - - // enable header column - { - // click the column menu button - await tester.clickColumnMenuButton(0); - - // enable header column - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn - .tr(), - ), - ); - } - - // enable header row - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the row menu button - await tester.clickRowMenuButton(0); - - // enable header column - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(), - ), - ); - } - - // check the table is updated - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.isHeaderColumnEnabled, isTrue); - expect(table.isHeaderRowEnabled, isTrue); - - // disable header column - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the row menu button - await tester.clickColumnMenuButton(0); - - final toggleButton = find.descendant( - of: find.byType(SimpleTableHeaderActionButton), - matching: find.byType(CupertinoSwitch), - ); - await tester.tapButton(toggleButton); - } - - // enable header row - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the row menu button - await tester.clickRowMenuButton(0); - - // enable header column - final toggleButton = find.descendant( - of: find.byType(SimpleTableHeaderActionButton), - matching: find.byType(CupertinoSwitch), - ); - await tester.tapButton(toggleButton); - } - - // check the table is updated - expect(table.isHeaderColumnEnabled, isFalse); - expect(table.isHeaderRowEnabled, isFalse); - - // set to page width - { - final table = editorState.getNodeAtPath([0])!; - final beforeWidth = table.width; - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the row menu button - await tester.clickRowMenuButton(0); - - // enable header column - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth - .tr(), - ), - ); - - // check the table is updated - expect(table.width, greaterThan(beforeWidth)); - } - - // distribute columns evenly - { - final table = editorState.getNodeAtPath([0])!; - final beforeWidth = table.width; - - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the column menu button - await tester.clickColumnMenuButton(0); - - // distribute columns evenly - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys - .document_plugins_simpleTable_moreActions_distributeColumnsWidth - .tr(), - ), - ); - - // check the table is updated - expect(table.width, equals(beforeWidth)); - } - }); - - testWidgets(''' -1. insert a simple table via + menu -2. bold -3. clear content -''', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('simple table'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - final firstParagraphPath = [0, 0, 0, 0]; - - // open the plus menu and select the table block - { - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_table.tr(), - ); - - // check the block is inserted - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.rowLength, equals(2)); - expect(table.columnLength, equals(2)); - - // focus on the first cell - - final selection = editorState.selection!; - expect(selection.isCollapsed, isTrue); - expect(selection.start.path, equals(firstParagraphPath)); - } - - await tester.ime.insertText('Hello'); - - // enable bold - { - // click the column menu button - await tester.clickColumnMenuButton(0); - - // enable bold - await tester.clickSimpleTableBoldContentAction(); - await tester.cancelTableActionMenu(); - - // check the first cell is bold - final paragraph = editorState.getNodeAtPath(firstParagraphPath)!; - expect(paragraph.isInBoldColumn, isTrue); - } - - // clear content - { - // focus on the first cell - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: firstParagraphPath)), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - // click the column menu button - await tester.clickColumnMenuButton(0); - - final clearContents = find.findTextInFlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_clearContents - .tr(), - ); - - // clear content - final scrollable = find.descendant( - of: find.byType(SimpleTableBottomSheet), - matching: find.byType(Scrollable), - ); - await tester.scrollUntilVisible( - clearContents, - 100, - scrollable: scrollable, - ); - await tester.tapButton(clearContents); - await tester.cancelTableActionMenu(); - - // check the first cell is empty - final paragraph = editorState.getNodeAtPath(firstParagraphPath)!; - expect(paragraph.delta!, isEmpty); - } - }); - - testWidgets(''' -1. insert a simple table via + menu -2. insert a heading block in table cell -''', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('simple table'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - final firstParagraphPath = [0, 0, 0, 0]; - - // open the plus menu and select the table block - { - await tester.openPlusMenuAndClickButton( - LocaleKeys.document_slashMenu_name_table.tr(), - ); - - // check the block is inserted - final table = editorState.getNodeAtPath([0])!; - expect(table.type, equals(SimpleTableBlockKeys.type)); - expect(table.rowLength, equals(2)); - expect(table.columnLength, equals(2)); - - // focus on the first cell - - final selection = editorState.selection!; - expect(selection.isCollapsed, isTrue); - expect(selection.start.path, equals(firstParagraphPath)); - } - - // open the plus menu and select the heading block - { - await tester.openPlusMenuAndClickButton( - LocaleKeys.editor_heading1.tr(), - ); - - // check the heading block is inserted - final heading = editorState.getNodeAtPath([0, 0, 0, 0])!; - expect(heading.type, equals(HeadingBlockKeys.type)); - expect(heading.level, equals(1)); - } - }); - - testWidgets(''' -1. insert a simple table via + menu -2. resize column -''', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile('simple table'); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(); - - final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth; - - // find the first cell - { - final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first; - final offset = tester.getCenter(resizeHandle); - final gesture = await tester.startGesture(offset, pointer: 7); - await tester.pumpAndSettle(); - - await gesture.moveBy(const Offset(100, 0)); - await tester.pumpAndSettle(); - - await gesture.up(); - await tester.pumpAndSettle(); - } - - // check the table is updated - final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth; - expect(afterWidth1, greaterThan(beforeWidth)); - - // resize back to the original width - { - final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first; - final offset = tester.getCenter(resizeHandle); - final gesture = await tester.startGesture(offset, pointer: 7); - await tester.pumpAndSettle(); - - await gesture.moveBy(const Offset(-100, 0)); - await tester.pumpAndSettle(); - - await gesture.up(); - await tester.pumpAndSettle(); - } - - // check the table is updated - final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth; - expect(afterWidth2, equals(beforeWidth)); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/slash_menu_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/slash_menu_test.dart deleted file mode 100644 index 11031d2b71..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/slash_menu_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart'; -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart'; -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - const title = 'Test Slash Menu'; - - group('slash menu', () { - testWidgets('show slash menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowSlashMenu(title); - final menuWidget = find.byType(MobileSelectionMenuWidget); - expect(menuWidget, findsOneWidget); - final items = - (menuWidget.evaluate().first.widget as MobileSelectionMenuWidget) - .items; - int i = 0; - for (final item in items) { - final localItem = mobileItems[i]; - expect(item.name, localItem.name); - i++; - } - }); - - testWidgets('search by slash menu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createPageAndShowSlashMenu(title); - const searchText = 'Heading'; - await tester.ime.insertText(searchText); - final itemWidgets = find.byType(MobileSelectionMenuItemWidget); - int number = 0; - for (final item in mobileItems) { - if (item is MobileSelectionMenuItem) { - for (final childItem in item.children) { - if (childItem.name - .toLowerCase() - .contains(searchText.toLowerCase())) { - number++; - } - } - } else { - if (item.name.toLowerCase().contains(searchText.toLowerCase())) { - number++; - } - } - } - expect(itemWidgets, findsNWidgets(number)); - }); - - testWidgets('tap to show submenu', (tester) async { - await tester.launchInAnonymousMode(); - await tester.createNewDocumentOnMobile(title); - await tester.editor.tapLineOfEditorAt(0); - final listview = find.descendant( - of: find.byType(MobileSelectionMenuWidget), - matching: find.byType(ListView), - ); - for (final item in mobileItems) { - if (item is! MobileSelectionMenuItem) continue; - await tester.editor.showSlashMenu(); - await tester.scrollUntilVisible( - find.text(item.name), - 50, - scrollable: listview, - duration: const Duration(milliseconds: 250), - ); - await tester.tap(find.text(item.name)); - final childrenLength = ((listview.evaluate().first.widget as ListView) - .childrenDelegate as SliverChildListDelegate) - .children - .length; - expect(childrenLength, item.children.length); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/title_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/title_test.dart deleted file mode 100644 index 01b1d574ce..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/title_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/mobile/presentation/presentation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('document title:', () { - testWidgets('create a new page, the title should be empty', (tester) async { - await tester.launchInAnonymousMode(); - - final createPageButton = find.byKey( - BottomNavigationBarItemType.add.valueKey, - ); - await tester.tapButton(createPageButton); - expect(find.byType(MobileDocumentScreen), findsOneWidget); - - final title = tester.editor.findDocumentTitle(''); - expect(title, findsOneWidget); - final textField = tester.widget(title); - expect(textField.focusNode!.hasFocus, isTrue); - - // input new name and press done button - const name = 'test document'; - await tester.enterText(title, name); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - final newTitle = tester.editor.findDocumentTitle(name); - expect(newTitle, findsOneWidget); - expect(textField.controller!.text, name); - - // the document should get focus - final editor = tester.widget( - find.byType(AppFlowyEditorPage), - ); - expect( - editor.editorState.selection, - Selection.collapsed(Position(path: [0])), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/document/toolbar_test.dart b/frontend/appflowy_flutter/integration_test/mobile/document/toolbar_test.dart deleted file mode 100644 index 72da283cd6..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/document/toolbar_test.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart'; -import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('toolbar menu:', () { - testWidgets('insert links', (tester) async { - await tester.launchInAnonymousMode(); - - final createPageButton = find.byKey( - BottomNavigationBarItemType.add.valueKey, - ); - await tester.tapButton(createPageButton); - expect(find.byType(MobileDocumentScreen), findsOneWidget); - - final editor = find.byType(AppFlowyEditor); - expect(editor, findsOneWidget); - final editorState = tester.editor.getCurrentEditorState(); - - /// move cursor to content - final root = editorState.document.root; - final lastNode = root.children.lastOrNull; - await editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: lastNode!.path)), - ); - await tester.pumpAndSettle(); - - /// insert two lines of text - const strFirst = 'FirstLine', - strSecond = 'SecondLine', - link = 'google.com'; - await editorState.insertTextAtCurrentSelection(strFirst); - await tester.pumpAndSettle(); - await editorState.insertNewLine(); - await tester.pumpAndSettle(); - await editorState.insertTextAtCurrentSelection(strSecond); - await tester.pumpAndSettle(); - final firstLine = find.text(strFirst, findRichText: true), - secondLine = find.text(strSecond, findRichText: true); - expect(firstLine, findsOneWidget); - expect(secondLine, findsOneWidget); - - /// select the first line - await tester.longPress(firstLine); - await tester.pumpAndSettle(); - - /// find aa item and tap it - final aaItem = find.byWidgetPredicate( - (widget) => - widget is AppFlowyMobileToolbarIconItem && - widget.icon == FlowySvgs.m_toolbar_aa_m, - ); - expect(aaItem, findsOneWidget); - await tester.tapButton(aaItem); - - /// find link button and tap it - final linkButton = find.byWidgetPredicate( - (widget) => - widget is MobileToolbarMenuItemWrapper && - widget.icon == FlowySvgs.m_toolbar_link_m, - ); - expect(linkButton, findsOneWidget); - await tester.tapButton(linkButton); - - /// input the link - final linkField = find.byWidgetPredicate( - (w) => - w is FlowyTextField && - w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(), - ); - await tester.enterText(linkField, link); - await tester.pumpAndSettle(); - - /// complete inputting - await tester.tapButton(find.text(LocaleKeys.button_done.tr())); - - /// do it again - /// select the second line - await tester.longPress(secondLine); - await tester.pumpAndSettle(); - await tester.tapButton(aaItem); - await tester.tapButton(linkButton); - await tester.enterText(linkField, link); - await tester.pumpAndSettle(); - await tester.tapButton(find.text(LocaleKeys.button_done.tr())); - - final firstNode = editorState.getNodeAtPath([0]); - final secondNode = editorState.getNodeAtPath([1]); - - Map commonDeltaJson(String insert) => { - "insert": insert, - "attributes": {"href": link}, - }; - - expect( - firstNode?.delta?.toJson(), - commonDeltaJson(strFirst), - ); - expect( - secondNode?.delta?.toJson(), - commonDeltaJson(strSecond), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart index d64ab094de..8e3724a583 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart @@ -1,25 +1,47 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; +import 'package:appflowy/plugins/document/document_page.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/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +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:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('create new page in home page:', () { + group('create new page', () { testWidgets('create document', (tester) async { - await tester.launchInAnonymousMode(); + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.local, + ); + + // click the anonymousSignInButton + final anonymousSignInButton = find.byType(SignInAnonymousButtonV2); + expect(anonymousSignInButton, findsOneWidget); + await tester.tapButton(anonymousSignInButton); // tap the create page button - final createPageButton = find.byWidgetPredicate( - (widget) => - widget is FlowySvg && - widget.svg.path == FlowySvgs.m_home_add_m.path, - ); + final createPageButton = find.byKey(mobileCreateNewPageButtonKey); await tester.tapButton(createPageButton); - await tester.pumpAndSettle(); expect(find.byType(MobileDocumentScreen), findsOneWidget); }); }); diff --git a/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart b/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart new file mode 100644 index 0000000000..c915ebadfd --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile/page_style/document_page_style_test.dart @@ -0,0 +1,139 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.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/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +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:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('document page style', () { + double getCurrentEditorFontSize() { + final editorPage = find + .byType(AppFlowyEditorPage) + .evaluate() + .single + .widget as AppFlowyEditorPage; + return editorPage.styleCustomizer + .style() + .textStyleConfiguration + .text + .fontSize!; + } + + double getCurrentEditorLineHeight() { + final editorPage = find + .byType(AppFlowyEditorPage) + .evaluate() + .single + .widget as AppFlowyEditorPage; + return editorPage.styleCustomizer + .style() + .textStyleConfiguration + .text + .height!; + } + + testWidgets('change font size in page style settings', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.normal.fontSize); + // change font size from normal to large + await tester.tapSvgButton(FlowySvgs.m_font_size_large_s); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.large.fontSize); + // change font size from large to small + await tester.tapSvgButton(FlowySvgs.m_font_size_small_s); + expect(getCurrentEditorFontSize(), PageStyleFontLayout.small.fontSize); + }); + + testWidgets('change line height in page style settings', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.normal.lineHeight, + ); + // change line height from normal to large + await tester.tapSvgButton(FlowySvgs.m_layout_large_s); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.large.lineHeight, + ); + // change line height from large to small + await tester.tapSvgButton(FlowySvgs.m_layout_small_s); + expect( + getCurrentEditorLineHeight(), + PageStyleLineHeightLayout.small.lineHeight, + ); + }); + + testWidgets('use built-in image as cover', (tester) async { + await tester.launchInAnonymousMode(); + + // click the getting start page + await tester.openPage(gettingStarted); + // click the layout button + await tester.tapButton(find.byType(MobileViewPageLayoutButton)); + // toggle the preset button + await tester.tapSvgButton(FlowySvgs.m_page_style_presets_m); + + // select the first preset + final firstBuiltInImage = find.byWidgetPredicate( + (widget) => + widget is Image && + widget.image is AssetImage && + (widget.image as AssetImage).assetName == + PageStyleCoverImageType.builtInImagePath('1'), + ); + await tester.tap(firstBuiltInImage); + + // click done button to exit the page style settings + await tester.tapButton(find.byType(BottomSheetDoneButton).first); + await tester.tapButton(find.byType(BottomSheetDoneButton).first); + + // check the cover + final builtInCover = find.descendant( + of: find.byType(DocumentImmersiveCover), + matching: firstBuiltInImage, + ); + expect(builtInCover, findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile/settings/default_text_direction_test.dart b/frontend/appflowy_flutter/integration_test/mobile/settings/default_text_direction_test.dart deleted file mode 100644 index 158264cad1..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/settings/default_text_direction_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart'; -import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Change default text direction', (tester) async { - await tester.launchInAnonymousMode(); - - /// tap [Setting] button - await tester.tapButton(find.byType(HomePageSettingsPopupMenu)); - await tester - .tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr())); - - /// tap [Default Text Direction] - await tester.tapButton( - find.text(LocaleKeys.settings_appearance_textDirection_label.tr()), - ); - - /// there are 3 items: LTR-RTL-AUTO - final bottomSheet = find.ancestor( - of: find.byType(FlowyOptionTile), - matching: find.byType(SafeArea), - ); - final items = find.descendant( - of: bottomSheet, - matching: find.byType(FlowyOptionTile), - ); - expect(items, findsNWidgets(3)); - - /// select [Auto] - await tester.tapButton(items.last); - expect( - find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()), - findsOneWidget, - ); - - /// go back home - await tester.tapButton(find.byType(AppBarImmersiveBackButton)); - - /// create new page - final createPageButton = - find.byKey(BottomNavigationBarItemType.add.valueKey); - await tester.tapButton(createPageButton); - - final editorState = tester.editor.getCurrentEditorState(); - // focus on the editor - await tester.editor.tapLineOfEditorAt(0); - - const testEnglish = 'English', testArabic = 'إنجليزي'; - - /// insert [testEnglish] - await editorState.insertTextAtCurrentSelection(testEnglish); - await tester.pumpAndSettle(); - await editorState.insertNewLine(position: editorState.selection!.end); - await tester.pumpAndSettle(); - - /// insert [testArabic] - await editorState.insertTextAtCurrentSelection(testArabic); - await tester.pumpAndSettle(); - final testEnglishFinder = find.text(testEnglish, findRichText: true), - testArabicFinder = find.text(testArabic, findRichText: true); - final testEnglishRenderBox = - testEnglishFinder.evaluate().first.renderObject as RenderBox, - testArabicRenderBox = - testArabicFinder.evaluate().first.renderObject as RenderBox; - final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero), - arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero); - expect(englishPosition.dx > arabicPosition.dx, true); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/settings/scale_factor_test.dart b/frontend/appflowy_flutter/integration_test/mobile/settings/scale_factor_test.dart deleted file mode 100644 index 908caa89d5..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile/settings/scale_factor_test.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import '../../shared/util.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('test for change scale factor', (tester) async { - await tester.launchInAnonymousMode(); - - /// tap [Setting] button - await tester.tapButton(find.byType(HomePageSettingsPopupMenu)); - await tester - .tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr())); - - /// tap [Font Scale Factor] - await tester.tapButton( - find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()), - ); - - /// drag slider - final slider = find.descendant( - of: find.byType(FontSizeStepper), - matching: find.byType(Slider), - ); - await tester.slideToValue(slider, 0.8); - expect(appflowyScaleFactor, 0.8); - - await tester.slideToValue(slider, 0.9); - expect(appflowyScaleFactor, 0.9); - - await tester.slideToValue(slider, 1.0); - expect(appflowyScaleFactor, 1.0); - - await tester.slideToValue(slider, 1.1); - expect(appflowyScaleFactor, 1.1); - - await tester.slideToValue(slider, 1.2); - expect(appflowyScaleFactor, 1.2); - }); -} diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart index ab98ca190a..65f48a87ff 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart @@ -1,15 +1,38 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/home/home.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/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +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:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('anonymous sign in on mobile:', () { + group('anonymous sign in on mobile', () { testWidgets('anon user and then sign in', (tester) async { - await tester.launchInAnonymousMode(); + await tester.initializeAppFlowy(); + + // click the anonymousSignInButton + final anonymousSignInButton = find.byType(SignInAnonymousButtonV2); + expect(anonymousSignInButton, findsOneWidget); + await tester.tapButton(anonymousSignInButton); // expect to see the home page expect(find.byType(MobileHomeScreen), findsOneWidget); diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner.dart b/frontend/appflowy_flutter/integration_test/mobile_runner.dart new file mode 100644 index 0000000000..9ebc2dcd97 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile_runner.dart @@ -0,0 +1,10 @@ +import 'package:integration_test/integration_test.dart'; + +import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test; +import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; + +Future runIntegrationOnMobile() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + anonymous_sign_in_test.main(); + create_new_page_test.main(); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart b/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart deleted file mode 100644 index 4d92db7d25..0000000000 --- a/frontend/appflowy_flutter/integration_test/mobile_runner_1.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy_backend/log.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'mobile/document/document_test_runner.dart' as document_test_runner; -import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test; -import 'mobile/settings/default_text_direction_test.dart' - as default_text_direction_test; -import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; - -Future main() async { - Log.shared.disableLog = true; - - await runIntegration1OnMobile(); -} - -Future runIntegration1OnMobile() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - anonymous_sign_in_test.main(); - create_new_page_test.main(); - document_test_runner.main(); - default_text_direction_test.main(); -} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index 0fc3c5d826..cb7d2d6e33 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -3,13 +3,7 @@ import 'dart:io'; import 'desktop_runner_1.dart'; import 'desktop_runner_2.dart'; import 'desktop_runner_3.dart'; -import 'desktop_runner_4.dart'; -import 'desktop_runner_5.dart'; -import 'desktop_runner_6.dart'; -import 'desktop_runner_7.dart'; -import 'desktop_runner_8.dart'; -import 'desktop_runner_9.dart'; -import 'mobile_runner_1.dart'; +import 'mobile_runner.dart'; /// The main task runner for all integration tests in AppFlowy. /// @@ -23,14 +17,8 @@ Future main() async { await runIntegration1OnDesktop(); await runIntegration2OnDesktop(); await runIntegration3OnDesktop(); - await runIntegration4OnDesktop(); - await runIntegration5OnDesktop(); - await runIntegration6OnDesktop(); - await runIntegration7OnDesktop(); - await runIntegration8OnDesktop(); - await runIntegration9OnDesktop(); } else if (Platform.isIOS || Platform.isAndroid) { - await runIntegration1OnMobile(); + await runIntegrationOnMobile(); } else { throw Exception('Unsupported platform'); } diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart index 88f9634afd..9e205182a4 100644 --- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart @@ -1,35 +1,34 @@ +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/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:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'util.dart'; extension AppFlowyAuthTest on WidgetTester { - Future tapGoogleLoginInButton({bool pumpAndSettle = true}) async { - await tapButton( - find.byKey(signInWithGoogleButtonKey), - pumpAndSettle: pumpAndSettle, - ); + Future tapGoogleLoginInButton() async { + await tapButton(find.byKey(const Key('signInWithGoogleButton'))); } /// Requires being on the SettingsPage.account of the SettingsDialog Future logout() async { final scrollable = find.findSettingsScrollable(); await scrollUntilVisible( - find.byType(AccountSignInOutButton), + find.byType(SignInOutButton), 100, scrollable: scrollable, ); - await tapButton(find.byType(AccountSignInOutButton)); + await tapButton(find.byType(SignInOutButton)); - expectToSeeText(LocaleKeys.button_ok.tr()); - await tapButtonWithName(LocaleKeys.button_ok.tr()); + expectToSeeText(LocaleKeys.button_confirm.tr()); + await tapButtonWithName(LocaleKeys.button_confirm.tr()); } Future tapSignInAsGuest() async { @@ -37,7 +36,7 @@ extension AppFlowyAuthTest on WidgetTester { } void expectToSeeGoogleLoginButton() { - expect(find.byKey(signInWithGoogleButtonKey), findsOneWidget); + expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget); } void assertSwitchValue(Finder finder, bool value) { @@ -52,6 +51,26 @@ 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( @@ -62,6 +81,15 @@ 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 493cb4c1f0..ab72247c24 100644 --- a/frontend/appflowy_flutter/integration_test/shared/base.dart +++ b/frontend/appflowy_flutter/integration_test/shared/base.dart @@ -1,24 +1,24 @@ 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/material.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'; -import 'package:universal_platform/universal_platform.dart'; class FlowyTestContext { FlowyTestContext({required this.applicationDataDirectory}); @@ -56,6 +56,8 @@ 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"; @@ -74,6 +76,13 @@ 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(); @@ -107,7 +116,7 @@ extension AppFlowyTestBase on WidgetTester { } Future waitUntilSignInPageShow() async { - if (isAuthEnabled || UniversalPlatform.isMobile) { + if (isAuthEnabled) { final finder = find.byType(SignInAnonymousButtonV2); await pumpUntilFound(finder, timeout: const Duration(seconds: 30)); expect(finder, findsOneWidget); @@ -160,45 +169,23 @@ extension AppFlowyTestBase on WidgetTester { Future tapButton( Finder finder, { + int? pointer, int buttons = kPrimaryButton, bool warnIfMissed = false, int milliseconds = 500, bool pumpAndSettle = true, }) async { - await tap(finder, buttons: buttons, warnIfMissed: warnIfMissed); - - if (pumpAndSettle) { - await this.pumpAndSettle( - Duration(milliseconds: milliseconds), - EnginePhase.sendSemanticsUpdate, - const Duration(seconds: 15), - ); - } - } - - Future tapDown( - Finder finder, { - int? pointer, - int buttons = kPrimaryButton, - PointerDeviceKind kind = PointerDeviceKind.touch, - bool pumpAndSettle = true, - int milliseconds = 500, - }) async { - final location = getCenter(finder); - final TestGesture gesture = await startGesture( - location, - pointer: pointer, + await tap( + finder, buttons: buttons, - kind: kind, + warnIfMissed: warnIfMissed, ); - await gesture.cancel(); - await gesture.down(location); - await gesture.cancel(); + if (pumpAndSettle) { await this.pumpAndSettle( Duration(milliseconds: milliseconds), EnginePhase.sendSemanticsUpdate, - const Duration(seconds: 15), + const Duration(seconds: 5), ); } } @@ -236,25 +223,6 @@ extension AppFlowyTestBase on WidgetTester { Future wait(int milliseconds) async { await pumpAndSettle(Duration(milliseconds: milliseconds)); } - - Future slideToValue( - Finder slider, - double value, { - double paddingOffset = 24.0, - }) async { - final sliderWidget = slider.evaluate().first.widget as Slider; - final range = sliderWidget.max - sliderWidget.min; - final initialRate = (value - sliderWidget.min) / range; - final totalWidth = getSize(slider).width - (2 * paddingOffset); - final zeroPoint = getTopLeft(slider) + - Offset( - paddingOffset + initialRate * totalWidth, - getSize(slider).height / 2, - ); - final calculatedOffset = value * (totalWidth / 100); - await dragFrom(zeroPoint, Offset(calculatedOffset, 0)); - await pumpAndSettle(); - } } extension AppFlowyFinderTestBase on CommonFinders { @@ -263,16 +231,13 @@ extension AppFlowyFinderTestBase on CommonFinders { (widget) => widget is FlowyText && widget.text == text, ); } +} - Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) { - return byWidgetPredicate( - (widget) => - widget is FlowyTooltip && - widget.richMessage != null && - widget.richMessage!.toPlainText().contains(richMessage), - skipOffstage: skipOffstage, - ); - } +Future useTestSupabaseCloud() async { + await useSupabaseCloud( + url: TestEnv.supabaseUrl, + anonKey: TestEnv.supabaseAnonKey, + ); } 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 d7a505d152..6d7cf5ab2f 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,37 +1,27 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; -import 'package:appflowy/plugins/shared/share/share_button.dart'; +import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; @@ -45,14 +35,7 @@ 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 'package:path/path.dart' as p; -import 'package:path_provider/path_provider.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'emoji.dart'; import 'util.dart'; @@ -67,15 +50,9 @@ extension CommonOperations on WidgetTester { } else { // cloud version final anonymousButton = find.byType(SignInAnonymousButtonV2); - await tapButton(anonymousButton, warnIfMissed: true); + await tapButton(anonymousButton); } - await pumpAndSettle(const Duration(milliseconds: 200)); - } - - Future tapContinousAnotherWay() async { - // local version - await tapButtonWithName(LocaleKeys.signIn_continueAnotherWay.tr()); if (Platform.isWindows) { await pumpAndSettle(const Duration(milliseconds: 200)); } @@ -195,21 +172,6 @@ extension CommonOperations on WidgetTester { } } - /// Right click on the page name. - Future rightClickOnPageName( - String name, { - ViewLayoutPB layout = ViewLayoutPB.Document, - }) async { - final page = findPageName(name, layout: layout); - await hoverOnPageName( - name, - onHover: () async { - await tap(page, buttons: kSecondaryMouseButton); - await pumpAndSettle(); - }, - ); - } - /// open the page with given name. Future openPage( String name, { @@ -224,10 +186,7 @@ extension CommonOperations on WidgetTester { /// /// Must call [hoverOnPageName] first. Future tapPageOptionButton() async { - final optionButton = find.descendant( - of: find.byType(ViewMoreActionPopover), - matching: find.byFlowySvg(FlowySvgs.workspace_three_dots_s), - ); + final optionButton = find.byType(ViewMoreActionButton); await tapButton(optionButton); } @@ -268,10 +227,6 @@ extension CommonOperations on WidgetTester { await tapOKButton(); } - Future tapTrashButton() async { - await tap(find.byType(SidebarTrashButton)); - } - Future tapOKButton() async { final okButton = find.byWidgetPredicate( (widget) => @@ -281,20 +236,6 @@ extension CommonOperations on WidgetTester { await tapButton(okButton); } - /// Expand or collapse the page. - Future expandOrCollapsePage({ - required String pageName, - required ViewLayoutPB layout, - }) async { - final page = findPageName(pageName, layout: layout); - await hoverOnWidget(page); - final expandButton = find.descendant( - of: page, - matching: find.byType(ViewItemDefaultLeftIcon), - ); - await tapButton(expandButton.first); - } - /// Tap the restore button. /// /// the restore button will show after the current page is deleted. @@ -307,33 +248,22 @@ extension CommonOperations on WidgetTester { /// Tap the delete permanently button. /// - /// the delete permanently button will show after the current page is deleted. + /// the restore button will show after the current page is deleted. Future tapDeletePermanentlyButton() async { - final deleteButton = find.textContaining( + final restoreButton = find.textContaining( LocaleKeys.deletePagePrompt_deletePermanent.tr(), ); - await tapButton(deleteButton); - await tap(find.text(LocaleKeys.button_delete.tr())); - await pumpAndSettle(); + await tapButton(restoreButton); } /// Tap the share button above the document page. Future tapShareButton() async { final shareButton = find.byWidgetPredicate( - (widget) => widget is ShareButton, + (widget) => widget is DocumentShareButton, ); await tapButton(shareButton); } - // open the share menu and then click the publish tab - Future openPublishMenu() async { - await tapShareButton(); - final publishButton = find.textContaining( - LocaleKeys.shareAction_publishTab.tr(), - ); - await tapButton(publishButton); - } - /// Tap the export markdown button /// /// Must call [tapShareButton] first. @@ -366,7 +296,7 @@ extension CommonOperations on WidgetTester { // hover on it and change it's name if (name != null) { await hoverOnPageName( - layout.defaultName, + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout: layout, onHover: () async { await renamePage(name); @@ -380,110 +310,13 @@ extension CommonOperations on WidgetTester { if (openAfterCreated) { await openPage( // if the name is null, use the default name - name ?? layout.defaultName, + name ?? LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout: layout, ); await pumpAndSettle(); } } - Future createOpenRenameDocumentUnderParent({ - required String name, - String? parentName, - }) async { - // create a new page - await tapAddViewButton(name: parentName ?? gettingStarted); - await tapButtonWithName(ViewLayoutPB.Document.menuName); - final settingsOrFailure = await getIt().getWithFormat( - KVKeys.showRenameDialogWhenCreatingNewFile, - (value) => bool.parse(value), - ); - final showRenameDialog = settingsOrFailure ?? false; - if (showRenameDialog) { - await tapOKButton(); - } - await pumpAndSettle(); - - // open the page after created - await openPage(ViewLayoutPB.Document.defaultName); - await pumpAndSettle(); - - // Enter new name in the document title - await enterText(find.byType(TextFieldWithMetricLines), name); - await pumpAndSettle(); - } - - /// Create a new page in the space - Future createNewPageInSpace({ - required String spaceName, - required ViewLayoutPB layout, - bool openAfterCreated = true, - String? pageName, - }) async { - final currentSpace = find.byWidgetPredicate( - (widget) => widget is CurrentSpace && widget.space.name == spaceName, - ); - if (currentSpace.evaluate().isEmpty) { - throw Exception('Current space not found'); - } - - await hoverOnWidget( - currentSpace, - onHover: () async { - // click the + button - await clickAddPageButtonInSpaceHeader(); - await tapButtonWithName(layout.menuName); - }, - ); - await pumpAndSettle(); - - if (pageName != null) { - // move the cursor to other place to disable to tooltips - await tapAt(Offset.zero); - - // hover on new created page and change it's name - await hoverOnPageName( - '', - layout: layout, - onHover: () async { - await renamePage(pageName); - await pumpAndSettle(); - }, - ); - await pumpAndSettle(); - } - - // open the page after created - if (openAfterCreated) { - // if the name is null, use empty string - await openPage(pageName ?? '', layout: layout); - await pumpAndSettle(); - } - } - - /// Click the + button in the space header - Future clickAddPageButtonInSpaceHeader() async { - final addPageButton = find.descendant( - of: find.byType(SidebarSpaceHeader), - matching: find.byType(ViewAddButton), - ); - await tapButton(addPageButton); - } - - /// Click the + button in the space header - Future clickSpaceHeader() async { - await tapButton(find.byType(SidebarSpaceHeader)); - } - - Future openSpace(String spaceName) async { - final space = find.descendant( - of: find.byType(SidebarSpaceMenuItem), - matching: find.text(spaceName), - ); - await tapButton(space); - } - - /// Create a new page on the top level Future createNewPage({ ViewLayoutPB layout = ViewLayoutPB.Document, bool openAfterCreated = true, @@ -497,7 +330,6 @@ extension CommonOperations on WidgetTester { bool isShiftPressed = false, bool isAltPressed = false, bool isMetaPressed = false, - PhysicalKeyboardKey? physicalKey, }) async { if (isControlPressed) { await simulateKeyDownEvent(LogicalKeyboardKey.control); @@ -511,14 +343,8 @@ extension CommonOperations on WidgetTester { if (isMetaPressed) { await simulateKeyDownEvent(LogicalKeyboardKey.meta); } - await simulateKeyDownEvent( - key, - physicalKey: physicalKey, - ); - await simulateKeyUpEvent( - key, - physicalKey: physicalKey, - ); + await simulateKeyDownEvent(key); + await simulateKeyUpEvent(key); if (isControlPressed) { await simulateKeyUpEvent(LogicalKeyboardKey.control); } @@ -601,23 +427,6 @@ extension CommonOperations on WidgetTester { await pumpAndSettle(); } - Future reorderFavorite({ - required String fromName, - required String toName, - }) async { - final from = find.descendant( - of: find.byType(FavoriteFolder), - matching: find.text(fromName), - ), - to = find.descendant( - of: find.byType(FavoriteFolder), - matching: find.text(toName), - ); - final distanceY = getCenter(to).dy - getCenter(from).dx; - await drag(from, Offset(0, distanceY)); - await pumpAndSettle(const Duration(seconds: 1)); - } - // tap the button with [FlowySvgData] Future tapButtonWithFlowySvgData(FlowySvgData svg) async { final button = find.byWidgetPredicate( @@ -629,9 +438,9 @@ extension CommonOperations on WidgetTester { // update the page icon in the sidebar Future updatePageIconInSidebarByName({ required String name, - String? parentName, + required String parentName, required ViewLayoutPB layout, - required EmojiIconData icon, + required String icon, }) async { final iconButton = find.descendant( of: findPageName( @@ -643,11 +452,7 @@ extension CommonOperations on WidgetTester { find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()), ); await tapButton(iconButton); - if (icon.type == FlowyIconType.emoji) { - await tapEmoji(icon.emoji); - } else if (icon.type == FlowyIconType.icon) { - await tapIcon(icon); - } + await tapEmoji(icon); await pumpAndSettle(); } @@ -655,7 +460,7 @@ extension CommonOperations on WidgetTester { Future updatePageIconInTitleBarByName({ required String name, required ViewLayoutPB layout, - required EmojiIconData icon, + required String icon, }) async { await openPage( name, @@ -667,32 +472,7 @@ extension CommonOperations on WidgetTester { ); await tapButton(title); await tapButton(find.byType(EmojiPickerButton)); - if (icon.type == FlowyIconType.emoji) { - await tapEmoji(icon.emoji); - } else if (icon.type == FlowyIconType.icon) { - await tapIcon(icon); - } else if (icon.type == FlowyIconType.custom) { - await pickImage(icon); - } - await pumpAndSettle(); - } - - Future updatePageIconInTitleBarByPasteALink({ - required String name, - required ViewLayoutPB layout, - required String iconLink, - }) async { - await openPage( - name, - layout: layout, - ); - final title = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.text(name), - ); - await tapButton(title); - await tapButton(find.byType(EmojiPickerButton)); - await pasteImageLinkAsIcon(iconLink); + await tapEmoji(icon); await pumpAndSettle(); } @@ -760,11 +540,7 @@ extension CommonOperations on WidgetTester { expect(createWorkspaceDialog, findsOneWidget); // input the workspace name - final workspaceNameInput = find.descendant( - of: createWorkspaceDialog, - matching: find.byType(TextField), - ); - await enterText(workspaceNameInput, name); + await enterText(find.byType(TextField), name); await tapButtonWithName(LocaleKeys.button_ok.tr(), pumpAndSettle: false); await pump(const Duration(seconds: 5)); @@ -796,7 +572,8 @@ extension CommonOperations on WidgetTester { Future openMoreViewActions() async { final button = find.byType(MoreViewActions); - await tapButton(button); + await tap(button); + await pumpAndSettle(); } /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup. @@ -804,9 +581,12 @@ extension CommonOperations on WidgetTester { /// [openMoreViewActions] must be called beforehand! /// Future duplicateByMoreViewActions() async { - final button = find.byWidgetPredicate( - (widget) => - widget is ViewAction && widget.type == ViewMoreActionType.duplicate, + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.duplicate, + ), ); await tap(button); await pump(); @@ -821,178 +601,12 @@ extension CommonOperations on WidgetTester { of: find.byType(ListView), matching: find.byWidgetPredicate( (widget) => - widget is ViewAction && widget.type == ViewMoreActionType.delete, + widget is ViewAction && widget.type == ViewActionType.delete, ), ); await tap(button); await pump(); } - - Future tapFileUploadHint() async { - final finder = find.byWidgetPredicate( - (w) => - w is RichText && - w.text.toPlainText().contains( - LocaleKeys.document_plugins_file_fileUploadHint.tr(), - ), - ); - await tap(finder); - await pumpAndSettle(const Duration(seconds: 2)); - } - - /// Create a new document on mobile - Future createNewDocumentOnMobile(String name) async { - final createPageButton = find.byKey( - BottomNavigationBarItemType.add.valueKey, - ); - await tapButton(createPageButton); - expect(find.byType(MobileDocumentScreen), findsOneWidget); - - final title = editor.findDocumentTitle(''); - expect(title, findsOneWidget); - final textField = widget(title); - expect(textField.focusNode!.hasFocus, isTrue); - - // input new name and press done button - await enterText(title, name); - await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(); - final newTitle = editor.findDocumentTitle(name); - expect(newTitle, findsOneWidget); - expect(textField.controller!.text, name); - } - - /// Open the plus menu - Future openPlusMenuAndClickButton(String buttonName) async { - assert( - UniversalPlatform.isMobile, - 'This method is only supported on mobile platforms', - ); - - final plusMenuButton = find.byKey(addBlockToolbarItemKey); - final addMenuItem = find.byType(AddBlockMenu); - await tapButton(plusMenuButton); - await pumpUntilFound(addMenuItem); - - final toggleHeading1 = find.byWidgetPredicate( - (widget) => - widget is TypeOptionMenuItem && widget.value.text == buttonName, - ); - final scrollable = find.ancestor( - of: find.byType(TypeOptionGridView), - matching: find.byType(Scrollable), - ); - await scrollUntilVisible( - toggleHeading1, - 100, - scrollable: scrollable, - ); - await tapButton(toggleHeading1); - await pumpUntilNotFound(addMenuItem); - } - - /// Click the column menu button in the simple table - Future clickColumnMenuButton(int index) async { - final columnMenuButton = find.byWidgetPredicate( - (w) => - w is SimpleTableMobileReorderButton && - w.index == index && - w.type == SimpleTableMoreActionType.column, - ); - await tapButton(columnMenuButton); - await pumpUntilFound(find.byType(SimpleTableCellBottomSheet)); - } - - /// Click the row menu button in the simple table - Future clickRowMenuButton(int index) async { - final rowMenuButton = find.byWidgetPredicate( - (w) => - w is SimpleTableMobileReorderButton && - w.index == index && - w.type == SimpleTableMoreActionType.row, - ); - await tapButton(rowMenuButton); - await pumpUntilFound(find.byType(SimpleTableCellBottomSheet)); - } - - /// Click the SimpleTableQuickAction - Future clickSimpleTableQuickAction(SimpleTableMoreAction action) async { - final button = find.byWidgetPredicate( - (widget) => widget is SimpleTableQuickAction && widget.type == action, - ); - await tapButton(button); - } - - /// Click the SimpleTableContentAction - Future clickSimpleTableBoldContentAction() async { - final button = find.byType(SimpleTableContentBoldAction); - await tapButton(button); - } - - /// Cancel the table action menu - Future cancelTableActionMenu() async { - final finder = find.byType(SimpleTableCellBottomSheet); - if (finder.evaluate().isEmpty) { - return; - } - - await tapAt(Offset.zero); - await pumpUntilNotFound(finder); - } - - /// load icon list and return the first one - Future loadIcon() async { - await loadIconGroups(); - final groups = kIconGroups!; - final firstGroup = groups.first; - final firstIcon = firstGroup.icons.first; - return EmojiIconData.icon( - IconsData( - firstGroup.name, - firstIcon.name, - builtInSpaceColors.first, - ), - ); - } - - Future prepareImageIcon() async { - final imagePath = await rootBundle.load('assets/test/images/sample.jpeg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.jpeg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - return EmojiIconData.custom(imageFile.path); - } - - Future prepareSvgIcon() async { - final imagePath = await rootBundle.load('assets/test/images/sample.svg'); - final tempDirectory = await getTemporaryDirectory(); - final localImagePath = p.join(tempDirectory.path, 'sample.svg'); - final imageFile = File(localImagePath) - ..writeAsBytesSync(imagePath.buffer.asUint8List()); - return EmojiIconData.custom(imageFile.path); - } - - /// create new page and show slash menu - Future createPageAndShowSlashMenu(String title) async { - await createNewDocumentOnMobile(title); - await editor.tapLineOfEditorAt(0); - await editor.showSlashMenu(); - } - - /// create new page and show at menu - Future createPageAndShowAtMenu(String title) async { - await createNewDocumentOnMobile(title); - await editor.tapLineOfEditorAt(0); - await editor.showAtMenu(); - } - - /// create new page and show plus menu - Future createPageAndShowPlusMenu(String title) async { - await createNewDocumentOnMobile(title); - await editor.tapLineOfEditorAt(0); - await editor.showPlusMenu(); - } } extension SettingsFinder on CommonFinders { @@ -1021,25 +635,6 @@ extension SettingsFinder on CommonFinders { .first; } -extension FlowySvgFinder on CommonFinders { - Finder byFlowySvg(FlowySvgData svg) => _FlowySvgFinder(svg); -} - -class _FlowySvgFinder extends MatchFinder { - _FlowySvgFinder(this.svg); - - final FlowySvgData svg; - - @override - String get description => 'flowy_svg "$svg"'; - - @override - bool matches(Element candidate) { - final Widget widget = candidate.widget; - return widget is FlowySvg && widget.svg == svg; - } -} - extension ViewLayoutPBTest on ViewLayoutPB { String get menuName { switch (this) { @@ -1068,34 +663,4 @@ 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/constants.dart b/frontend/appflowy_flutter/integration_test/shared/constants.dart deleted file mode 100644 index bfe3349b10..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/constants.dart +++ /dev/null @@ -1,8 +0,0 @@ -class Constants { - // this page name is default page name in the new workspace - static const gettingStartedPageName = 'Getting started'; - static const toDosPageName = 'To-dos'; - static const generalSpaceName = 'General'; - - static const defaultWorkspaceName = 'My Workspace'; -} diff --git a/frontend/appflowy_flutter/integration_test/shared/data.dart b/frontend/appflowy_flutter/integration_test/shared/data.dart index c1777638d3..6a2ad830bb 100644 --- a/frontend/appflowy_flutter/integration_test/shared/data.dart +++ b/frontend/appflowy_flutter/integration_test/shared/data.dart @@ -1,8 +1,9 @@ import 'dart:io'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv_keys.dart'; import 'package:archive/archive_io.dart'; -import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -59,7 +60,7 @@ class TestWorkspaceService { final inputStream = InputFileStream(await workspace.zip.then((value) => value.path)); final archive = ZipDecoder().decodeBuffer(inputStream); - await extractArchiveToDisk( + extractArchiveToDisk( archive, await TestWorkspace._parent.then((value) => value.path), ); 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 970965f294..754e80342c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1,9 +1,12 @@ import 'dart:io'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; @@ -17,15 +20,13 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/disclosure_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; @@ -37,11 +38,9 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.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'; @@ -49,9 +48,8 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/ti import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.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'; @@ -70,12 +68,11 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/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'; @@ -86,9 +83,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as p; // Non-exported member of the table_calendar library @@ -100,11 +94,8 @@ import 'common_operations.dart'; import 'expectation.dart'; import 'mock/mock_file_picker.dart'; -const v020GridFileName = "v020.afdb"; -const v069GridFileName = "v069.afdb"; - extension AppFlowyDatabaseTest on WidgetTester { - Future openTestDatabase(String fileName) async { + Future openV020database() async { final context = await initializeAppFlowy(); await tapAnonymousSignInButton(); @@ -114,24 +105,29 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapAddViewButton(); await tapImportButton(); - // Don't use the p.join to build the path that used in loadString. It - // is not working on windows. - final str = await rootBundle - .loadString("assets/test/workspaces/database/$fileName"); + final testFileNames = ['v020.afdb']; + final paths = []; + for (final fileName in testFileNames) { + // Don't use the p.join to build the path that used in loadString. It + // is not working on windows. + final str = await rootBundle + .loadString("assets/test/workspaces/database/$fileName"); - // Write the content to the file. - final path = p.join( - context.applicationDataDirectory, - fileName, - ); - final pageName = p.basenameWithoutExtension(path); - File(path).writeAsStringSync(str); + // Write the content to the file. + final path = p.join( + context.applicationDataDirectory, + fileName, + ); + paths.add(path); + File(path).writeAsStringSync(str); + } // mock get files mockPickFilePaths( - paths: [path], + paths: paths, ); await tapDatabaseRawDataButton(); - await openPage(pageName, layout: ViewLayoutPB.Grid); + await pumpAndSettle(); + await openPage('v020', layout: ViewLayoutPB.Grid); } Future hoverOnFirstRowOfGrid([Future Function()? onHover]) async { @@ -152,7 +148,6 @@ extension AppFlowyDatabaseTest on WidgetTester { expect(cell, findsOneWidget); await enterText(cell, input); - await testTextInput.receiveAction(TextInputAction.done); await pumpAndSettle(); } @@ -248,10 +243,10 @@ extension AppFlowyDatabaseTest on WidgetTester { } } - void assertMultiSelectOption({ + Future assertMultiSelectOption({ required int rowIndex, required List contents, - }) { + }) async { final findCell = cellFinder(rowIndex, FieldType.MultiSelect); for (final content in contents) { if (content.isNotEmpty) { @@ -412,20 +407,17 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future selectOption({required String name}) async { - final option = find.descendant( - of: find.byType(SelectOptionCellEditor), - matching: find.byWidgetPredicate( - (widget) => widget is SelectOptionTagCell && widget.option.name == name, - ), + final option = find.byWidgetPredicate( + (widget) => widget is SelectOptionTagCell && widget.option.name == name, ); await tapButton(option); } - void findSelectOptionWithNameInGrid({ + Future findSelectOptionWithNameInGrid({ required int rowIndex, required String name, - }) { + }) async { final findRow = find.byType(GridRow); final option = find.byWidgetPredicate( (widget) => @@ -437,10 +429,10 @@ extension AppFlowyDatabaseTest on WidgetTester { expect(cell, findsOneWidget); } - void assertNumberOfSelectedOptionsInGrid({ + Future assertNumberOfSelectedOptionsInGrid({ required int rowIndex, required Matcher matcher, - }) { + }) async { final findRow = find.byType(GridRow); final options = find.byWidgetPredicate( @@ -482,7 +474,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); if (enter) { await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(const Duration(milliseconds: 500)); + await pumpAndSettle(); } else { await tapButton( find.descendant( @@ -508,7 +500,6 @@ extension AppFlowyDatabaseTest on WidgetTester { Future renameChecklistTask({ required int index, required String name, - bool enter = true, }) async { final textField = find .descendant( @@ -518,9 +509,7 @@ extension AppFlowyDatabaseTest on WidgetTester { .at(index); await enterText(textField, name); - if (enter) { - await testTextInput.receiveAction(TextInputAction.done); - } + await testTextInput.receiveAction(TextInputAction.done); await pumpAndSettle(); } @@ -538,38 +527,14 @@ extension AppFlowyDatabaseTest on WidgetTester { Future deleteChecklistTask({required int index}) async { final task = find.byType(ChecklistItem).at(index); - await hoverOnWidget( - task, - onHover: () async { - final button = find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s, - ); - await tapButton(button); - }, - ); - } + await startGesture(getCenter(task), kind: PointerDeviceKind.mouse); + await pumpAndSettle(); - void assertPhantomChecklistItemAtIndex({required int index}) { - final paddings = find.descendant( - of: find.descendant( - of: find.byType(ChecklistRowDetailCell), - matching: find.byType(ReorderableListView), - ), - matching: find.byWidgetPredicate( - (widget) => - widget is Padding && - (widget.child is ChecklistItem || - widget.child is PhantomChecklistItem), - ), + final button = find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s, ); - final phantom = widget(paddings.at(index)).child!; - expect(phantom is PhantomChecklistItem, true); - } - void assertPhantomChecklistItemContent(String content) { - final phantom = find.byType(PhantomChecklistItem); - final text = find.text(content); - expect(find.descendant(of: phantom, matching: text), findsOneWidget); + await tapButton(button); } Future openFirstRowDetailPage() async { @@ -601,25 +566,12 @@ extension AppFlowyDatabaseTest on WidgetTester { final banner = find.byType(RowBanner); expect(banner, findsOneWidget); - await startGesture( - getCenter(banner) + const Offset(0, -10), - kind: PointerDeviceKind.mouse, - ); + await startGesture(getCenter(banner), kind: PointerDeviceKind.mouse); await pumpAndSettle(); } - /// Used to open the add cover popover, by pressing on "Add cover"-button. - /// - /// Should call [hoverRowBanner] first. - /// - Future tapAddCoverButton() async { - await tapButtonWithName( - LocaleKeys.document_plugins_cover_addCover.tr(), - ); - } - Future openEmojiPicker() async => - tapButton(find.text(LocaleKeys.document_plugins_cover_addIcon.tr())); + tapButton(find.byType(AddEmojiButton)); Future tapDateCellInRowDetailPage() async { final findDateCell = find.byType(EditableDateCell); @@ -675,7 +627,12 @@ extension AppFlowyDatabaseTest on WidgetTester { (w) => w is FieldActionCell && w.action == FieldAction.delete, ); await tapButton(deleteButton); - await tapButtonWithName(LocaleKeys.space_delete.tr()); + + final confirmButton = find.descendant( + of: find.byType(NavigatorAlertDialog), + matching: find.byType(PrimaryTextButton), + ); + await tapButton(confirmButton); } Future scrollRowDetailByOffset(Offset offset) async { @@ -717,53 +674,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await dismissFieldEditor(); } - Future changeFieldIcon(String icon) async { - await tapButton(find.byType(FieldEditIconButton)); - if (icon.isEmpty) { - final button = find.descendant( - of: find.byType(FlowyIconEmojiPicker), - matching: find.text( - LocaleKeys.button_remove.tr(), - ), - ); - await tapButton(button); - } else { - final svgContent = kIconGroups?.findSvgContent(icon); - await tapButton( - find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svgString == svgContent, - ), - ); - } - } - - void assertFieldSvg(String name, FieldType fieldType) { - final svgFinder = find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svg == fieldType.svgData, - ); - final fieldButton = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - expect( - find.descendant(of: fieldButton, matching: svgFinder), - findsOneWidget, - ); - } - - void assertFieldCustomSvg(String name, String svg) { - final svgContent = kIconGroups?.findSvgContent(svg); - final svgFinder = find.byWidgetPredicate( - (widget) => widget is FlowySvg && widget.svgString == svgContent, - ); - final fieldButton = find.byWidgetPredicate( - (widget) => widget is FieldCellButton && widget.field.name == name, - ); - expect( - find.descendant(of: fieldButton, matching: svgFinder), - findsOneWidget, - ); - } - Future changeCalculateAtIndex(int index, CalculationType type) async { await tap(find.byType(CalculateCell).at(index)); await pumpAndSettle(); @@ -876,12 +786,12 @@ 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. - void findCellByFieldType(FieldType fieldType) { + Future findCellByFieldType(FieldType fieldType) async { final finder = finderForFieldType(fieldType); expect(finder, findsWidgets); } - void assertNumberOfRowsInGridPage(int num) { + Future assertNumberOfRowsInGridPage(int num) async { expect( find.byType(GridRow, skipOffstage: false), findsNWidgets(num), @@ -942,41 +852,11 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(const Duration(milliseconds: 200)); } - Future changeFieldWidth(String fieldName, double width) async { - final field = find.byWidgetPredicate( - (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName, - ); - await hoverOnWidget( - field, - onHover: () async { - final dragHandle = find.descendant( - of: field, - matching: find.byType(DragToExpandLine), - ); - await drag(dragHandle, Offset(width - getSize(field).width, 0)); - await pumpAndSettle(const Duration(milliseconds: 200)); - }, - ); - } - - double getFieldWidth(String fieldName) { - final field = find.byWidgetPredicate( - (widget) => widget is GridFieldCell && widget.fieldInfo.name == fieldName, - ); - - return getSize(field).width; - } - Future findDateEditor(dynamic matcher) async { final finder = find.byType(DateCellEditor); 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); @@ -991,65 +871,34 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(find.byType(GridAddRowButton)); } - Future tapCreateRowButtonAfterHoveringOnGridRow() async { + Future tapCreateRowButtonInRowMenuOfGrid() async { await tapButton(find.byType(InsertRowButton)); } Future tapRowMenuButtonInGrid() async { + expect(find.byType(RowMenuButton), findsOneWidget); await tapButton(find.byType(RowMenuButton)); } - /// Should call [tapRowMenuButtonInGrid] first. - Future tapCreateRowAboveButtonInRowMenu() async { - await tapButtonWithName(LocaleKeys.grid_row_insertRecordAbove.tr()); - } - /// Should call [tapRowMenuButtonInGrid] first. Future tapDeleteOnRowMenu() async { + expect(find.text(LocaleKeys.grid_row_delete.tr()), findsOneWidget); await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); } - Future reorderRow( - String from, - String to, - ) async { - final fromRow = find.byWidgetPredicate( - (widget) => widget is GridRow && widget.rowId == from, - ); - final toRow = find.byWidgetPredicate( - (widget) => widget is GridRow && widget.rowId == to, - ); - await hoverOnWidget( - fromRow, - onHover: () async { - final dragElement = find.descendant( - of: fromRow, - matching: find.byType(ReorderableDragStartListener), - ); - await timedDrag( - dragElement, - getCenter(toRow) - getCenter(fromRow), - const Duration(milliseconds: 200), - ); - await pumpAndSettle(); - }, - ); - } - 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(); - if (name != null) { - await renameField(name); - } + await renameField(name); await tapSwitchFieldTypeButton(); await selectFieldType(fieldType); + await dismissFieldEditor(); } Future tapDatabaseSettingButton() async { @@ -1067,7 +916,7 @@ extension AppFlowyDatabaseTest on WidgetTester { Future tapCreateFilterByFieldType(FieldType type, String title) async { final findFilter = find.byWidgetPredicate( (widget) => - widget is FilterableFieldButton && + widget is GridFilterPropertyCell && widget.fieldInfo.fieldType == type && widget.fieldInfo.name == title, ); @@ -1075,9 +924,8 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future tapFilterButtonInGrid(String name) async { - final button = find.byWidgetPredicate( - (widget) => widget is ChoiceChipButton && widget.fieldInfo.name == name, - ); + final findFilter = find.byType(FilterMenuItem); + final button = find.descendant(of: findFilter, matching: find.text(name)); await tapButton(button); } @@ -1115,15 +963,12 @@ extension AppFlowyDatabaseTest on WidgetTester { } /// Must call [tapSortMenuInSettingBar] first. - Future tapEditSortConditionButtonByFieldName(String name) async { - final sortItem = find.descendant( - of: find.ancestor( - of: find.text(name), - matching: find.byType(DatabaseSortItem), - ), - matching: find.byType(SortConditionButton), + Future tapSortButtonByName(String name) async { + final findSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && widget.sortInfo.fieldInfo.name == name, ); - await tapButton(sortItem); + await tapButton(findSortItem); } /// Must call [tapSortMenuInSettingBar] first. @@ -1131,26 +976,18 @@ extension AppFlowyDatabaseTest on WidgetTester { (FieldType, String) from, (FieldType, String) to, ) async { - final fromSortItem = find.ancestor( - of: find.text(from.$2), - matching: find.byType(DatabaseSortItem), + final fromSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && + widget.sortInfo.fieldInfo.fieldType == from.$1 && + widget.sortInfo.fieldInfo.name == from.$2, ); - final toSortItem = find.ancestor( - of: find.text(to.$2), - matching: find.byType(DatabaseSortItem), + final toSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && + widget.sortInfo.fieldInfo.fieldType == to.$1 && + widget.sortInfo.fieldInfo.name == to.$2, ); - // final fromSortItem = find.byWidgetPredicate( - // (widget) => - // widget is DatabaseSortItem && - // widget.sort.fieldInfo.fieldType == from.$1 && - // widget.sort.fieldInfo.name == from.$2, - // ); - // final toSortItem = find.byWidgetPredicate( - // (widget) => - // widget is DatabaseSortItem && - // widget.sort.fieldInfo.fieldType == to.$1 && - // widget.sort.fieldInfo.name == to.$2, - // ); final dragElement = find.descendant( of: fromSortItem, matching: find.byType(ReorderableDragStartListener), @@ -1159,13 +996,16 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(const Duration(milliseconds: 200)); } - /// Must call [tapEditSortConditionButtonByFieldName] first. + /// Must call [tapSortButtonByName] first. Future tapSortByDescending() async { await tapButton( - find.byWidgetPredicate( - (widget) => - widget is OrderPanelItem && - widget.condition == SortConditionPB.Descending, + find.descendant( + of: find.byType(OrderPannelItem), + matching: find.byWidgetPredicate( + (widget) => + widget is FlowyText && + widget.text == LocaleKeys.grid_sort_descending.tr(), + ), ), ); await sendKeyEvent(LogicalKeyboardKey.escape); @@ -1173,7 +1013,7 @@ extension AppFlowyDatabaseTest on WidgetTester { } /// Must call [tapSortMenuInSettingBar] first. - Future tapDeleteAllSortsButton() async { + Future tapAllSortButton() async { await tapButton(find.byType(DeleteAllSortsButton)); } @@ -1248,44 +1088,6 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(button); } - Future changeTextFilterCondition( - TextFilterConditionPB condition, - ) async { - await tapButton(find.byType(TextFilterConditionList)); - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text( - condition.filterName, - ), - ); - - await tapButton(button); - } - - Future changeSelectFilterCondition( - SelectOptionFilterConditionPB condition, - ) async { - await tapButton(find.byType(SelectOptionFilterConditionList)); - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(condition.i18n), - ); - - await tapButton(button); - } - - Future changeDateFilterCondition( - DateTimeFilterCondition condition, - ) async { - await tapButton(find.byType(DateFilterConditionList)); - final button = find.descendant( - of: find.byType(HoverButton), - matching: find.text(condition.filterName), - ); - - await tapButton(button); - } - /// Should call [tapDatabaseSettingButton] first. Future tapViewPropertiesButton() async { final findSettingItem = find.byType(DatabaseSettingsList); @@ -1448,7 +1250,7 @@ extension AppFlowyDatabaseTest on WidgetTester { matching: find.byType(EventCard), ); - await tapButton(cards.at(index), milliseconds: 1000); + await tapButton(cards.at(index)); } void assertEventEditorOpen() => @@ -1464,7 +1266,6 @@ extension AppFlowyDatabaseTest on WidgetTester { ); await enterText(textField, title); - await testTextInput.receiveAction(TextInputAction.done); await pumpAndSettle(const Duration(milliseconds: 300)); } @@ -1488,7 +1289,6 @@ extension AppFlowyDatabaseTest on WidgetTester { ); await tapButton(button); - await tapButtonWithName(LocaleKeys.button_delete.tr()); } Future dragDropRescheduleCalendarEvent() async { @@ -1596,7 +1396,7 @@ extension AppFlowyDatabaseTest on WidgetTester { of: textField, matching: find.byWidgetPredicate( (widget) => - widget is FlowySvg && widget.svg == FlowySvgs.close_filled_s, + widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m, ), ), ); @@ -1780,8 +1580,6 @@ 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/document_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart deleted file mode 100644 index 398a3f9657..0000000000 --- a/frontend/appflowy_flutter/integration_test/shared/document_test_operations.dart +++ /dev/null @@ -1,439 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_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/actions/drag_to_reorder/draggable_option_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_title.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/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/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'util.dart'; - -extension EditorWidgetTester on WidgetTester { - EditorOperations get editor => EditorOperations(this); -} - -class EditorOperations { - const EditorOperations(this.tester); - - final WidgetTester tester; - - EditorState getCurrentEditorState() => - tester.widget(find.byType(AppFlowyEditor)).editorState; - - Node getNodeAtPath(Path path) { - final editorState = getCurrentEditorState(); - return editorState.getNodeAtPath(path)!; - } - - /// Tap the line of editor at [index] - Future tapLineOfEditorAt(int index) async { - final textBlocks = find.byType(AppFlowyRichText); - index = index.clamp(0, textBlocks.evaluate().length - 1); - final center = tester.getCenter(textBlocks.at(index)); - final right = tester.getTopRight(textBlocks.at(index)); - final centerRight = Offset(right.dx, center.dy); - await tester.tapAt(centerRight); - await tester.pumpAndSettle(); - } - - /// Hover on cover plugin button above the document - Future hoverOnCoverToolbar() async { - final coverToolbar = find.byType(DocumentHeaderToolbar); - await tester.startGesture( - tester.getBottomLeft(coverToolbar).translate(5, -5), - kind: PointerDeviceKind.mouse, - ); - await tester.pumpAndSettle(); - } - - /// Taps on the 'Add Icon' button in the cover toolbar - Future tapAddIconButton() async { - await tester.tapButtonWithName( - LocaleKeys.document_plugins_cover_addIcon.tr(), - ); - expect(find.byType(FlowyEmojiPicker), findsOneWidget); - } - - Future paste() async { - if (UniversalPlatform.isMacOS) { - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isMetaPressed: true, - ); - } else { - await tester.simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: true, - ); - } - } - - Future tapGettingStartedIcon() async { - await tester.tapButton( - find.descendant( - of: find.byType(DocumentCoverWidget), - matching: find.findTextInFlowyText('⭐️'), - ), - ); - } - - /// Taps on the 'Skin tone' button - /// - /// Must call [tapAddIconButton] first. - Future changeEmojiSkinTone(EmojiSkinTone skinTone) async { - await tester.tapButton( - find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()), - ); - final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon)); - await tester.tapButton(skinToneButton); - } - - /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover - Future tapRemoveIconButton({bool isInPicker = false}) async { - final Finder button = !isInPicker - ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()) - : find.descendant( - of: find.byType(FlowyIconEmojiPicker), - matching: find.text(LocaleKeys.button_remove.tr()), - ); - await tester.tapButton(button); - } - - /// Requires that the document must already have an icon. This opens the icon - /// picker - Future tapOnIconWidget() async { - final iconWidget = find.byType(EmojiIconWidget); - await tester.tapButton(iconWidget); - } - - Future tapOnAddCover() async { - await tester.tapButtonWithName( - LocaleKeys.document_plugins_cover_addCover.tr(), - ); - } - - Future tapOnChangeCover() async { - await tester.tapButtonWithName( - LocaleKeys.document_plugins_cover_changeCover.tr(), - ); - } - - Future switchSolidColorBackground() async { - final findPurpleButton = find.byWidgetPredicate( - (widget) => widget is ColorItem && widget.option.name == 'Purple', - ); - await tester.tapButton(findPurpleButton); - } - - Future addNetworkImageCover(String imageUrl) async { - final embedLinkButton = find.findTextInFlowyText( - LocaleKeys.document_imageBlock_embedLink_label.tr(), - ); - await tester.tapButton(embedLinkButton); - - final imageUrlTextField = find.descendant( - of: find.byType(EmbedImageUrlWidget), - matching: find.byType(TextField), - ); - await tester.enterText(imageUrlTextField, imageUrl); - await tester.pumpAndSettle(); - await tester.tapButton( - find.descendant( - of: find.byType(EmbedImageUrlWidget), - matching: find.findTextInFlowyText( - LocaleKeys.document_imageBlock_embedLink_label.tr(), - ), - ), - ); - } - - Future tapOnRemoveCover() async => - tester.tapButton(find.byType(DeleteCoverButton)); - - /// A cover must be present in the document to function properly since this - /// catches all cover types collectively - Future hoverOnCover() async { - final cover = find.byType(DocumentCover); - await tester.startGesture( - tester.getCenter(cover), - kind: PointerDeviceKind.mouse, - ); - await tester.pumpAndSettle(); - } - - Future dismissCoverPicker() async { - await tester.sendKeyEvent(LogicalKeyboardKey.escape); - await tester.pumpAndSettle(); - } - - /// trigger the slash command (selection menu) - Future showSlashMenu() async { - await tester.ime.insertCharacter('/'); - } - - /// trigger the mention (@) command - Future showAtMenu() async { - await tester.ime.insertCharacter('@'); - } - - /// trigger the plus action menu (+) command - Future showPlusMenu() async { - await tester.ime.insertCharacter('+'); - } - - /// Tap the slash menu item with [name] - /// - /// Must call [showSlashMenu] first. - 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); - } - - /// Tap the at menu item with [name] - /// - /// Must call [showAtMenu] first. - Future tapAtMenuItemWithName(String name) async { - final atMenuItem = find.descendant( - of: find.byType(InlineActionsHandler), - matching: find.text(name, findRichText: true), - ); - await tester.tapButton(atMenuItem); - } - - /// Update the editor's selection - Future updateSelection(Selection? selection) async { - final editorState = getCurrentEditorState(); - unawaited( - editorState.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, - ), - ); - await tester.pumpAndSettle(const Duration(milliseconds: 200)); - } - - /// hover and click on the + button beside the block component. - Future hoverAndClickOptionAddButton( - Path path, - bool withModifiedKey, // alt on windows or linux, option on macos - ) async { - final optionAddButton = find.byWidgetPredicate( - (widget) => - widget is BlockComponentActionWrapper && - widget.node.path.equals(path), - ); - await tester.hoverOnWidget( - optionAddButton, - onHover: () async { - if (withModifiedKey) { - await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); - } - await tester.tapButton( - find.byWidgetPredicate( - (widget) => - widget is BlockAddButton && - widget.blockComponentContext.node.path.equals(path), - ), - ); - if (withModifiedKey) { - await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); - } - }, - ); - } - - /// hover and click on the option menu button beside the block component. - Future hoverAndClickOptionMenuButton(Path path) async { - final optionMenuButton = find.byWidgetPredicate( - (widget) => - widget is BlockComponentActionWrapper && - widget.node.path.equals(path), - ); - await tester.hoverOnWidget( - optionMenuButton, - onHover: () async { - await tester.tapButton( - find.byWidgetPredicate( - (widget) => - widget is BlockOptionButton && - widget.blockComponentContext.node.path.equals(path), - ), - ); - await tester.pumpUntilFound(find.byType(PopoverActionList)); - }, - ); - } - - /// open the turn into menu - Future openTurnIntoMenu(Path path) async { - await hoverAndClickOptionMenuButton(path); - await tester.tapButton( - find - .findTextInFlowyText( - LocaleKeys.document_plugins_optionAction_turnInto.tr(), - ) - .first, - ); - await tester.pumpUntilFound(find.byType(TurnIntoOptionMenu)); - } - - /// copy link to block - Future copyLinkToBlock(Path path) async { - await hoverAndClickOptionMenuButton(path); - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(), - ), - ); - } - - Future openDepthMenu(Path path) async { - await hoverAndClickOptionMenuButton(path); - await tester.tapButton( - find.findTextInFlowyText( - LocaleKeys.document_plugins_optionAction_depth.tr(), - ), - ); - await tester.pumpUntilFound(find.byType(DepthOptionMenu)); - } - - /// Drag block - /// - /// [offset] is the offset to move the block. - /// - /// [path] is the path of the block to move. - Future dragBlock( - Path path, - Offset offset, - ) async { - final dragToMoveAction = find.byWidgetPredicate( - (widget) => - widget is DraggableOptionButton && - widget.blockComponentContext.node.path.equals(path), - ); - - await tester.hoverOnWidget( - dragToMoveAction, - onHover: () async { - final dragToMoveTooltip = find.findFlowyTooltip( - LocaleKeys.blockActions_dragTooltip.tr(), - ); - await tester.pumpUntilFound(dragToMoveTooltip); - final location = tester.getCenter(dragToMoveAction); - final gesture = await tester.startGesture( - location, - pointer: 7, - ); - await tester.pump(); - - // divide the steps to small move to avoid the drag area not found error - const steps = 5; - final stepOffset = Offset(offset.dx / steps, offset.dy / steps); - - for (var i = 0; i < steps; i++) { - await gesture.moveBy(stepOffset); - await tester.pump(Durations.short1); - } - - // check if the drag to move action is dragging - expect( - isDraggingAppFlowyEditorBlock.value, - isTrue, - ); - - await gesture.up(); - await tester.pump(); - }, - ); - await tester.pumpAndSettle(Durations.short1); - } - - Finder findDocumentTitle(String? title) { - final parent = UniversalPlatform.isDesktop - ? find.byType(CoverTitle) - : find.byType(DocumentImmersiveCover); - - return find.descendant( - of: parent, - matching: find.byWidgetPredicate( - (widget) { - if (widget is! TextField) { - return false; - } - - if (widget.controller?.text == title) { - return true; - } - - if (title == null) { - return true; - } - - if (title.isEmpty) { - return widget.controller?.text.isEmpty ?? false; - } - - return false; - }, - ), - ); - } - - /// open the more action menu on mobile - Future openMoreActionMenuOnMobile() async { - final moreActionButton = find.byType(MobileViewPageMoreButton); - await tester.tapButton(moreActionButton); - await tester.pumpAndSettle(); - } - - /// click the more action item on mobile - /// - /// rename, add collaborator, publish, delete, etc. - Future clickMoreActionItemOnMobile(String name) async { - final moreActionItem = find.descendant( - of: find.byType(MobileQuickActionButton), - matching: find.findTextInFlowyText(name), - ); - await tester.tapButton(moreActionItem); - await tester.pumpAndSettle(); - } -} diff --git a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart new file mode 100644 index 0000000000..4eff62321a --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart @@ -0,0 +1,251 @@ +import 'dart:async'; +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/inline_actions/widgets/inline_actions_handler.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/services.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'util.dart'; + +extension EditorWidgetTester on WidgetTester { + EditorOperations get editor => EditorOperations(this); +} + +class EditorOperations { + const EditorOperations(this.tester); + + final WidgetTester tester; + + EditorState getCurrentEditorState() => + tester.widget(find.byType(AppFlowyEditor)).editorState; + + /// Tap the line of editor at [index] + Future tapLineOfEditorAt(int index) async { + final textBlocks = find.byType(AppFlowyRichText); + index = index.clamp(0, textBlocks.evaluate().length - 1); + await tester.tapAt(tester.getTopRight(textBlocks.at(index))); + await tester.pumpAndSettle(); + } + + /// Hover on cover plugin button above the document + Future hoverOnCoverToolbar() async { + final coverToolbar = find.byType(DocumentHeaderToolbar); + await tester.startGesture( + tester.getBottomLeft(coverToolbar).translate(5, -5), + kind: PointerDeviceKind.mouse, + ); + await tester.pumpAndSettle(); + } + + /// Taps on the 'Add Icon' button in the cover toolbar + Future tapAddIconButton() async { + await tester.tapButtonWithName( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ); + expect(find.byType(FlowyEmojiPicker), findsOneWidget); + } + + Future tapGettingStartedIcon() async { + await tester.tapButton( + find.descendant( + of: find.byType(DocumentCoverWidget), + matching: find.findTextInFlowyText('⭐️'), + ), + ); + } + + /// Taps on the 'Skin tone' button + /// + /// Must call [tapAddIconButton] first. + Future changeEmojiSkinTone(EmojiSkinTone skinTone) async { + await tester.tapButton( + find.byTooltip(LocaleKeys.emoji_selectSkinTone.tr()), + ); + final skinToneButton = find.byKey(emojiSkinToneKey(skinTone.icon)); + await tester.tapButton(skinToneButton); + } + + /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover + Future tapRemoveIconButton({bool isInPicker = false}) async { + final Finder button = !isInPicker + ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()) + : find.descendant( + of: find.byType(FlowyIconPicker), + matching: find.text(LocaleKeys.button_remove.tr()), + ); + await tester.tapButton(button); + } + + /// Requires that the document must already have an icon. This opens the icon + /// picker + Future tapOnIconWidget() async { + final iconWidget = find.byType(EmojiIconWidget); + await tester.tapButton(iconWidget); + } + + Future tapOnAddCover() async { + await tester.tapButtonWithName( + LocaleKeys.document_plugins_cover_addCover.tr(), + ); + } + + Future tapOnChangeCover() async { + await tester.tapButtonWithName( + LocaleKeys.document_plugins_cover_changeCover.tr(), + ); + } + + Future switchSolidColorBackground() async { + final findPurpleButton = find.byWidgetPredicate( + (widget) => widget is ColorItem && widget.option.name == 'Purple', + ); + await tester.tapButton(findPurpleButton); + } + + Future addNetworkImageCover(String imageUrl) async { + final embedLinkButton = find.findTextInFlowyText( + LocaleKeys.document_imageBlock_embedLink_label.tr(), + ); + await tester.tapButton(embedLinkButton); + + final imageUrlTextField = find.descendant( + of: find.byType(EmbedImageUrlWidget), + matching: find.byType(TextField), + ); + await tester.enterText(imageUrlTextField, imageUrl); + await tester.pumpAndSettle(); + await tester.tapButton( + find.descendant( + of: find.byType(EmbedImageUrlWidget), + matching: find.findTextInFlowyText( + LocaleKeys.document_imageBlock_embedLink_label.tr(), + ), + ), + ); + } + + Future tapOnRemoveCover() async => + tester.tapButton(find.byType(DeleteCoverButton)); + + /// A cover must be present in the document to function properly since this + /// catches all cover types collectively + Future hoverOnCover() async { + final cover = find.byType(DocumentCover); + await tester.startGesture( + tester.getCenter(cover), + kind: PointerDeviceKind.mouse, + ); + await tester.pumpAndSettle(); + } + + Future dismissCoverPicker() async { + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + } + + /// trigger the slash command (selection menu) + Future showSlashMenu() async { + await tester.ime.insertCharacter('/'); + } + + /// trigger the mention (@) command + Future showAtMenu() async { + await tester.ime.insertCharacter('@'); + } + + /// Tap the slash menu item with [name] + /// + /// Must call [showSlashMenu] first. + Future tapSlashMenuItemWithName(String name) async { + final slashMenuItem = find.text(name, findRichText: true); + await tester.tapButton(slashMenuItem); + } + + /// Tap the at menu item with [name] + /// + /// Must call [showAtMenu] first. + Future tapAtMenuItemWithName(String name) async { + final atMenuItem = find.descendant( + of: find.byType(InlineActionsHandler), + matching: find.text(name, findRichText: true), + ); + await tester.tapButton(atMenuItem); + } + + /// Update the editor's selection + Future updateSelection(Selection selection) async { + final editorState = getCurrentEditorState(); + unawaited( + editorState.updateSelectionWithReason( + selection, + reason: SelectionUpdateReason.uiEvent, + ), + ); + await tester.pumpAndSettle(const Duration(milliseconds: 200)); + } + + /// hover and click on the + button beside the block component. + Future hoverAndClickOptionAddButton( + Path path, + bool withModifiedKey, // alt on windows or linux, option on macos + ) async { + final optionAddButton = find.byWidgetPredicate( + (widget) => + widget is BlockComponentActionWrapper && + widget.node.path.equals(path), + ); + await tester.hoverOnWidget( + optionAddButton, + onHover: () async { + if (withModifiedKey) { + await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); + } + await tester.tapButton( + find.byWidgetPredicate( + (widget) => + widget is BlockAddButton && + widget.blockComponentContext.node.path.equals(path), + ), + ); + if (withModifiedKey) { + await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); + } + }, + ); + } + + /// hover and click on the option menu button beside the block component. + Future hoverAndClickOptionMenuButton(Path path) async { + final optionMenuButton = find.byWidgetPredicate( + (widget) => + widget is BlockComponentActionWrapper && + widget.node.path.equals(path), + ); + await tester.hoverOnWidget( + optionMenuButton, + onHover: () async { + await tester.tapButton( + find.byWidgetPredicate( + (widget) => + widget is BlockOptionButton && + widget.blockComponentContext.node.path.equals(path), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/integration_test/shared/emoji.dart b/frontend/appflowy_flutter/integration_test/shared/emoji.dart index cccd00a3f6..d439a9b3f7 100644 --- a/frontend/appflowy_flutter/integration_test/shared/emoji.dart +++ b/frontend/appflowy_flutter/integration_test/shared/emoji.dart @@ -1,24 +1,7 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_uploader.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_test/flutter_test.dart'; import 'base.dart'; -import 'common_operations.dart'; extension EmojiTestExtension on WidgetTester { Future tapEmoji(String emoji) async { @@ -28,117 +11,4 @@ extension EmojiTestExtension on WidgetTester { ); await tapButton(emojiWidget); } - - Future tapIcon(EmojiIconData icon, {bool enableColor = true}) async { - final iconsData = IconsData.fromJson(jsonDecode(icon.emoji)); - final pickTab = find.byType(PickerTab); - expect(pickTab, findsOneWidget); - await pumpAndSettle(); - final iconTab = find.descendant( - of: pickTab, - matching: find.text(PickerTabType.icon.tr), - ); - expect(iconTab, findsOneWidget); - await tapButton(iconTab); - final selectedSvg = find.descendant( - of: find.byType(FlowyIconPicker), - matching: find.byWidgetPredicate( - (w) => w is FlowySvg && w.svgString == iconsData.svgString, - ), - ); - - await tapButton(selectedSvg.first); - if (enableColor) { - final colorPicker = find.byType(IconColorPicker); - expect(colorPicker, findsOneWidget); - final selectedColor = find.descendant( - of: colorPicker, - matching: find.byWidgetPredicate((w) { - if (w is Container) { - final d = w.decoration; - if (d is ShapeDecoration) { - if (d.color == - Color( - int.parse(iconsData.color ?? builtInSpaceColors.first), - )) { - return true; - } - } - } - return false; - }), - ); - await tapButton(selectedColor); - } - } - - Future pickImage(EmojiIconData icon) async { - final pickTab = find.byType(PickerTab); - expect(pickTab, findsOneWidget); - await pumpAndSettle(); - - /// switch to custom tab - final iconTab = find.descendant( - of: pickTab, - matching: find.text(PickerTabType.custom.tr), - ); - expect(iconTab, findsOneWidget); - await tapButton(iconTab); - - /// mock for dragging image - final dropTarget = find.descendant( - of: find.byType(IconUploader), - matching: find.byType(DropTarget), - ); - expect(dropTarget, findsOneWidget); - final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget; - dropTargetWidget.onDragDone?.call( - DropDoneDetails( - files: [DropItemFile(icon.emoji)], - localPosition: Offset.zero, - globalPosition: Offset.zero, - ), - ); - await pumpAndSettle(const Duration(seconds: 3)); - - /// confirm to upload - final confirmButton = find.descendant( - of: find.byType(IconUploader), - matching: find.byType(PrimaryRoundedButton), - ); - await tapButton(confirmButton); - } - - Future pasteImageLinkAsIcon(String link) async { - final pickTab = find.byType(PickerTab); - expect(pickTab, findsOneWidget); - await pumpAndSettle(); - - /// switch to custom tab - final iconTab = find.descendant( - of: pickTab, - matching: find.text(PickerTabType.custom.tr), - ); - expect(iconTab, findsOneWidget); - await tapButton(iconTab); - - // mock the clipboard - await getIt() - .setData(ClipboardServiceData(plainText: link)); - - // paste the link - await simulateKeyEvent( - LogicalKeyboardKey.keyV, - isControlPressed: Platform.isLinux || Platform.isWindows, - isMetaPressed: Platform.isMacOS, - ); - await pumpAndSettle(const Duration(seconds: 5)); - - /// confirm to upload - final confirmButton = find.descendant( - of: find.byType(IconUploader), - matching: find.byType(PrimaryRoundedButton), - ); - await tapButton(confirmButton); - } } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index 3b9ef0d75c..c4b54a0fda 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -1,16 +1,9 @@ -import 'dart:convert'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/appflowy_network_svg.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; @@ -18,13 +11,11 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/notificati import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'util.dart'; @@ -34,15 +25,9 @@ const String gettingStarted = 'Getting started'; extension Expectation on WidgetTester { /// Expect to see the home page and with a default read me page. Future expectToSeeHomePageWithGetStartedPage() async { - if (UniversalPlatform.isDesktopOrWeb) { - final finder = find.byType(HomeStack); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); - } else if (UniversalPlatform.isMobile) { - final finder = find.byType(MobileHomePage); - await pumpUntilFound(finder); - expect(finder, findsOneWidget); - } + final finder = find.byType(HomeStack); + await pumpUntilFound(finder); + expect(finder, findsOneWidget); final docFinder = find.textContaining(gettingStarted); await pumpUntilFound(docFinder); @@ -125,7 +110,7 @@ extension Expectation on WidgetTester { return; } final iconWidget = find.byWidgetPredicate( - (widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji, + (widget) => widget is EmojiIconWidget && widget.emoji == emoji, ); expect(iconWidget, findsOneWidget); } @@ -199,7 +184,7 @@ extension Expectation on WidgetTester { String? parentName, ViewLayoutPB parentLayout = ViewLayoutPB.Document, }) { - if (UniversalPlatform.isDesktop) { + if (PlatformExtension.isDesktop) { if (parentName == null) { return find.byWidgetPredicate( (widget) => @@ -231,93 +216,24 @@ extension Expectation on WidgetTester { ); } - void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) { + void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) { final pageName = findPageName( name, layout: layout, ); - final type = data.type; - if (type == FlowyIconType.emoji) { - final icon = find.descendant( - of: pageName, - matching: find.text(data.emoji), - ); - expect(icon, findsOneWidget); - } else if (type == FlowyIconType.icon) { - final iconsData = IconsData.fromJson(jsonDecode(data.emoji)); - final icon = find.descendant( - of: pageName, - matching: find.byWidgetPredicate( - (w) => w is FlowySvg && w.svgString == iconsData.svgString, - ), - ); - expect(icon, findsOneWidget); - } else if (type == FlowyIconType.custom) { - final isSvg = data.emoji.endsWith('.svg'); - if (isURL(data.emoji)) { - final image = find.descendant( - of: pageName, - matching: isSvg - ? find.byType(FlowyNetworkSvg) - : find.byType(FlowyNetworkImage), - ); - expect(image, findsOneWidget); - } else { - final image = find.descendant( - of: pageName, - matching: isSvg ? find.byType(SvgPicture) : find.byType(Image), - ); - expect(image, findsOneWidget); - } - } + final icon = find.descendant( + of: pageName, + matching: find.text(emoji), + ); + expect(icon, findsOneWidget); } - void expectViewTitleHasIcon( - String name, - ViewLayoutPB layout, - EmojiIconData data, - ) { - final type = data.type; - if (type == FlowyIconType.emoji) { - final icon = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.text(data.emoji), - ); - expect(icon, findsOneWidget); - } else if (type == FlowyIconType.icon) { - final iconsData = IconsData.fromJson(jsonDecode(data.emoji)); - final icon = find.descendant( - of: find.byType(ViewTitleBar), - matching: find.byWidgetPredicate( - (w) => w is FlowySvg && w.svgString == iconsData.svgString, - ), - ); - expect(icon, findsOneWidget); - } else if (type == FlowyIconType.custom) { - final isSvg = data.emoji.endsWith('.svg'); - if (isURL(data.emoji)) { - final image = find.descendant( - of: find.byType(ViewTitleBar), - matching: isSvg - ? find.byType(FlowyNetworkSvg) - : find.byType(FlowyNetworkImage), - ); - expect(image, findsOneWidget); - } else { - final image = find.descendant( - of: find.byType(ViewTitleBar), - matching: isSvg - ? find.byWidgetPredicate((w) { - if (w is! SvgPicture) return false; - final loader = w.bytesLoader; - if (loader is! SvgFileLoader) return false; - return loader.file.path.endsWith('.svg'); - }) - : find.byType(Image), - ); - expect(image, findsOneWidget); - } - } + void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) { + final icon = find.descendant( + of: find.byType(ViewTitleBar), + matching: find.text(emoji), + ); + expect(icon, findsOneWidget); } void expectSelectedReminder(ReminderOption option) { diff --git a/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart new file mode 100644 index 0000000000..78445a2f4e --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/shared/mock/mock_openai_repository.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart'; +import 'package:http/http.dart' as http; +import 'package:mocktail/mocktail.dart'; + +class MyMockClient extends Mock implements http.Client { + @override + Future send(http.BaseRequest request) async { + final requestType = request.method; + final requestUri = request.url; + + if (requestType == 'POST' && + requestUri == OpenAIRequestType.textCompletion.uri) { + final responseHeaders = { + 'content-type': 'text/event-stream', + }; + final responseBody = Stream.fromIterable([ + utf8.encode( + '{ "choices": [{"text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula ", "index": 0, "logprobs": null, "finish_reason": null}]}', + ), + utf8.encode('\n'), + utf8.encode('[DONE]'), + ]); + + // Return a mocked response with the expected data + return http.StreamedResponse(responseBody, 200, headers: responseHeaders); + } + + // Return an error response for any other request + return http.StreamedResponse(const Stream.empty(), 404); + } +} + +class MockOpenAIRepository extends HttpOpenAIRepository { + MockOpenAIRepository() : super(apiKey: 'dummyKey', client: MyMockClient()); + + @override + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required Future Function() onEnd, + required void Function(OpenAIError error) onError, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + bool useAction = false, + }) async { + final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); + final response = await client.send(request); + + String previousSyntax = ''; + if (response.statusCode == 200) { + await for (final chunk in response.stream + .transform(const Utf8Decoder()) + .transform(const LineSplitter())) { + await onStart(); + final data = chunk.trim().split('data: '); + if (data[0] != '[DONE]') { + final response = TextCompletionResponse.fromJson( + json.decode(data[0]), + ); + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + if (text == previousSyntax && text == '\n') { + continue; + } + await onProcess(response); + previousSyntax = response.choices.first.text; + } + } else { + await onEnd(); + } + } + } + } +} diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index bfc5efedde..34684aab1a 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -2,34 +2,27 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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/account/account_user_profile.dart'; +import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/material.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; } @@ -78,14 +71,14 @@ extension AppFlowySettings on WidgetTester { Future enterUserName(String name) async { // Enable editing username final editUsernameFinder = find.descendant( - of: find.byType(AccountUserProfile), - matching: find.byFlowySvg(FlowySvgs.toolbar_link_edit_m), + of: find.byType(UserProfileSetting), + matching: find.byFlowySvg(FlowySvgs.edit_s), ); - await tap(editUsernameFinder, warnIfMissed: false); + await tap(editUsernameFinder); await pumpAndSettle(); final userNameFinder = find.descendant( - of: find.byType(AccountUserProfile), + of: find.byType(UserProfileSetting), matching: find.byType(FlowyTextField), ); await enterText(userNameFinder, name); @@ -118,20 +111,4 @@ extension AppFlowySettings on WidgetTester { await tapAt(Offset.zero); await pumpAndSettle(); } - - Future updateNamespace(String namespace) async { - final dialog = find.byType(DomainSettingsDialog); - expect(dialog, findsOneWidget); - - // input the new namespace - await enterText( - find.descendant( - of: dialog, - matching: find.byType(TextField), - ), - namespace, - ); - await tapButton(find.text(LocaleKeys.button_save.tr())); - await pumpAndSettle(); - } } diff --git a/frontend/appflowy_flutter/integration_test/shared/util.dart b/frontend/appflowy_flutter/integration_test/shared/util.dart index 5073425cad..283db5f4c0 100644 --- a/frontend/appflowy_flutter/integration_test/shared/util.dart +++ b/frontend/appflowy_flutter/integration_test/shared/util.dart @@ -1,9 +1,9 @@ -export 'auth_operation.dart'; export 'base.dart'; export 'common_operations.dart'; -export 'data.dart'; -export 'document_test_operations.dart'; -export 'expectation.dart'; -export 'ime.dart'; -export 'mock/mock_url_launcher.dart'; export 'settings.dart'; +export 'data.dart'; +export 'expectation.dart'; +export 'editor_test_operations.dart'; +export 'mock/mock_url_launcher.dart'; +export 'ime.dart'; +export 'auth_operation.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart index 1b2f22b944..4d20d88ce1 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/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/plugins/base/icon/icon_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'; @@ -40,13 +40,9 @@ extension AppFlowyWorkspace on WidgetTester { moreButton, onHover: () async { await tapButton(moreButton); - // wait for the menu to open - final renameButton = find.findTextInFlowyText( - LocaleKeys.button_rename.tr(), + await tapButton( + find.findTextInFlowyText(LocaleKeys.button_rename.tr()), ); - await pumpUntilFound(renameButton); - expect(renameButton, findsOneWidget); - await tapButton(renameButton); final input = find.byType(TextFormField); expect(input, findsOneWidget); await enterText(input, name); @@ -62,7 +58,7 @@ extension AppFlowyWorkspace on WidgetTester { ); expect(iconButton, findsOneWidget); await tapButton(iconButton); - final iconPicker = find.byType(FlowyIconEmojiPicker); + final iconPicker = find.byType(FlowyIconPicker); 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 4b7ed5d639..93a8eb77e1 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -1,5 +1,5 @@ PODS: - - app_links (0.0.2): + - app_links (0.0.1): - Flutter - appflowy_backend (0.0.1): - Flutter @@ -48,6 +48,8 @@ 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): @@ -56,8 +58,6 @@ PODS: - Flutter - keyboard_height_plugin (0.0.1): - Flutter - - open_filex (0.0.2): - - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -66,22 +66,15 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.0.0) - - saver_gallery (0.0.1): - - Flutter - 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): - Flutter - FlutterMacOS - - sqflite_darwin (0.0.4): + - sqflite (0.0.3): - Flutter - FlutterMacOS - super_native_extensions (0.0.1): @@ -90,9 +83,6 @@ PODS: - Toast (4.0.0) - url_launcher_ios (0.0.1): - Flutter - - webview_flutter_wkwebview (0.0.1): - - Flutter - - FlutterMacOS DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) @@ -103,22 +93,19 @@ 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`) - keyboard_height_plugin (from `.symlinks/plugins/keyboard_height_plugin/ios`) - - open_filex (from `.symlinks/plugins/open_filex/ios`) - 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`) - - saver_gallery (from `.symlinks/plugins/saver_gallery/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_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: @@ -126,7 +113,6 @@ SPEC REPOS: - DKPhotoGallery - ReachabilitySwift - SDWebImage - - Sentry - SwiftyGif - Toast @@ -147,6 +133,8 @@ 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: @@ -155,64 +143,52 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/irondash_engine_context/ios" keyboard_height_plugin: :path: ".symlinks/plugins/keyboard_height_plugin/ios" - open_filex: - :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" - saver_gallery: - :path: ".symlinks/plugins/saver_gallery/ios" - sentry_flutter: - :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite_darwin: - :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" super_native_extensions: :path: ".symlinks/plugins/super_native_extensions/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - app_links: 3da4c36b46cac3bf24eb897f1a6ce80bda109874 - appflowy_backend: 78f6a053f756e6bc29bcc5a2106cbe77b756e97a - connectivity_plus: 481668c94744c30c53b8895afb39159d1e619bdf - device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896 + app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875 + appflowy_backend: 144c20d8bfb298c4e10fa3fa6701a9f41bf98b88 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517 - flowy_infra_ui: 931b73a18b54a392ab6152eebe29a63a30751f53 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 + flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - fluttertoast: 76fea30fcf04176325f6864c87306927bd7d2038 - image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a - integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e - irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486 - keyboard_height_plugin: ef70a8181b29f27670e9e2450814ca6b6dc05b05 - open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1 - package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 + image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb + image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 + keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 - Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: b763c02dc3a8fd078389f410bf15149179020cb4 + share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index 804ad052be..aa53cf9b88 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -372,7 +372,6 @@ 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)", @@ -384,8 +383,6 @@ 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; @@ -514,7 +511,6 @@ 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)", @@ -526,8 +522,6 @@ 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; @@ -551,7 +545,6 @@ 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)", @@ -563,8 +556,6 @@ 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/AppDelegate.swift b/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift index b636303481..70693e4a8c 100644 --- a/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@main +@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist index 5d6a52bd2e..5ec528b05e 100644 --- a/frontend/appflowy_flutter/ios/Runner/Info.plist +++ b/frontend/appflowy_flutter/ios/Runner/Info.plist @@ -2,6 +2,10 @@ + NSCameraUsageDescription + AppFlowy requires access to the camera. + NSPhotoLibraryUsageDescription + AppFlowy requires access to the photo library. CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion @@ -16,6 +20,8 @@ en + FLTEnableImpeller + CFBundleName AppFlowy CFBundlePackageType @@ -37,24 +43,10 @@ CFBundleVersion $(FLUTTER_BUILD_NUMBER) - FLTEnableImpeller - LSRequiresIPhoneOS - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - NSPhotoLibraryUsageDescription - AppFlowy needs access to your photos to let you add images to your documents - NSPhotoLibraryAddUsageDescription - AppFlowy needs access to your photos to let you add images to your photo library UIApplicationSupportsIndirectInputEvents - NSCameraUsageDescription - AppFlowy needs access to your camera to let you add images to your documents from - camera UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -62,17 +54,22 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight - UISupportsDocumentBrowser - UIViewControllerBasedStatusBarAppearance + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + \ No newline at end of file diff --git a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements b/frontend/appflowy_flutter/ios/Runner/Runner.entitlements index e3bc137465..903def2af5 100644 --- a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements +++ b/frontend/appflowy_flutter/ios/Runner/Runner.entitlements @@ -4,16 +4,5 @@ aps-environment development - com.apple.developer.applesignin - - Default - - com.apple.developer.associated-domains - - applinks:appflowy.com - applinks:appflowy.io - applinks:test.appflowy.com - applinks:test.appflowy.io - diff --git a/frontend/appflowy_flutter/lib/ai/ai.dart b/frontend/appflowy_flutter/lib/ai/ai.dart deleted file mode 100644 index 9bfeeb4e00..0000000000 --- a/frontend/appflowy_flutter/lib/ai/ai.dart +++ /dev/null @@ -1,19 +0,0 @@ -export 'service/ai_entities.dart'; -export 'service/ai_prompt_input_bloc.dart'; -export 'service/appflowy_ai_service.dart'; -export 'service/error.dart'; -export 'service/ai_model_state_notifier.dart'; -export 'service/select_model_bloc.dart'; -export 'widgets/loading_indicator.dart'; -export 'widgets/prompt_input/action_buttons.dart'; -export 'widgets/prompt_input/desktop_prompt_text_field.dart'; -export 'widgets/prompt_input/file_attachment_list.dart'; -export 'widgets/prompt_input/layout_define.dart'; -export 'widgets/prompt_input/mention_page_bottom_sheet.dart'; -export 'widgets/prompt_input/mention_page_menu.dart'; -export 'widgets/prompt_input/mentioned_page_text_span.dart'; -export 'widgets/prompt_input/predefined_format_buttons.dart'; -export 'widgets/prompt_input/select_sources_bottom_sheet.dart'; -export 'widgets/prompt_input/select_sources_menu.dart'; -export 'widgets/prompt_input/select_model_menu.dart'; -export 'widgets/prompt_input/send_button.dart'; diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart b/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart deleted file mode 100644 index b08fadb7f8..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_entities.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:equatable/equatable.dart'; - -class AIStreamEventPrefix { - static const data = 'data:'; - static const error = 'error:'; - static const metadata = 'metadata:'; - static const start = 'start:'; - static const finish = 'finish:'; - static const comment = 'comment:'; - static const aiResponseLimit = 'AI_RESPONSE_LIMIT'; - static const aiImageResponseLimit = 'AI_IMAGE_RESPONSE_LIMIT'; - static const aiMaxRequired = 'AI_MAX_REQUIRED:'; - static const localAINotReady = 'LOCAL_AI_NOT_READY'; - static const localAIDisabled = 'LOCAL_AI_DISABLED'; -} - -enum AiType { - cloud, - local; - - bool get isCloud => this == cloud; - bool get isLocal => this == local; -} - -class PredefinedFormat extends Equatable { - const PredefinedFormat({ - required this.imageFormat, - required this.textFormat, - }); - - final ImageFormat imageFormat; - final TextFormat? textFormat; - - PredefinedFormatPB toPB() { - return PredefinedFormatPB( - imageFormat: switch (imageFormat) { - ImageFormat.text => ResponseImageFormatPB.TextOnly, - ImageFormat.image => ResponseImageFormatPB.ImageOnly, - ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage, - }, - textFormat: switch (textFormat) { - TextFormat.paragraph => ResponseTextFormatPB.Paragraph, - TextFormat.bulletList => ResponseTextFormatPB.BulletedList, - TextFormat.numberedList => ResponseTextFormatPB.NumberedList, - TextFormat.table => ResponseTextFormatPB.Table, - _ => null, - }, - ); - } - - @override - List get props => [imageFormat, textFormat]; -} - -enum ImageFormat { - text, - image, - textAndImage; - - bool get hasText => this == text || this == textAndImage; - - FlowySvgData get icon { - return switch (this) { - ImageFormat.text => FlowySvgs.ai_text_s, - ImageFormat.image => FlowySvgs.ai_image_s, - ImageFormat.textAndImage => FlowySvgs.ai_text_image_s, - }; - } - - String get i18n { - return switch (this) { - ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(), - ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(), - ImageFormat.textAndImage => - LocaleKeys.chat_changeFormat_textAndImage.tr(), - }; - } -} - -enum TextFormat { - paragraph, - bulletList, - numberedList, - table; - - FlowySvgData get icon { - return switch (this) { - TextFormat.paragraph => FlowySvgs.ai_paragraph_s, - TextFormat.bulletList => FlowySvgs.ai_list_s, - TextFormat.numberedList => FlowySvgs.ai_number_list_s, - TextFormat.table => FlowySvgs.ai_table_s, - }; - } - - String get i18n { - return switch (this) { - TextFormat.paragraph => LocaleKeys.chat_changeFormat_text.tr(), - TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(), - TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(), - TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(), - }; - } -} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart b/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart deleted file mode 100644 index 0bcc41da9b..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_model_state_notifier.dart +++ /dev/null @@ -1,181 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:protobuf/protobuf.dart'; -import 'package:universal_platform/universal_platform.dart'; - -typedef OnModelStateChangedCallback = void Function(AiType, bool, String); -typedef OnAvailableModelsChangedCallback = void Function( - List, - AIModelPB?, -); - -class AIModelStateNotifier { - AIModelStateNotifier({required this.objectId}) - : _localAIListener = - UniversalPlatform.isDesktop ? LocalAIStateListener() : null, - _aiModelSwitchListener = AIModelSwitchListener(objectId: objectId) { - _startListening(); - _init(); - } - - final String objectId; - final LocalAIStateListener? _localAIListener; - final AIModelSwitchListener _aiModelSwitchListener; - LocalAIPB? _localAIState; - AvailableModelsPB? _availableModels; - - // callbacks - final List _stateChangedCallbacks = []; - final List - _availableModelsChangedCallbacks = []; - - void _startListening() { - if (UniversalPlatform.isDesktop) { - _localAIListener?.start( - stateCallback: (state) async { - _localAIState = state; - _notifyStateChanged(); - - if (state.state == RunningStatePB.Running || - state.state == RunningStatePB.Stopped) { - await _loadAvailableModels(); - _notifyAvailableModelsChanged(); - } - }, - ); - } - - _aiModelSwitchListener.start( - onUpdateSelectedModel: (model) async { - final updatedModels = _availableModels?.deepCopy() - ?..selectedModel = model; - _availableModels = updatedModels; - _notifyAvailableModelsChanged(); - - if (model.isLocal && UniversalPlatform.isDesktop) { - await _loadLocalAiState(); - } - _notifyStateChanged(); - }, - ); - } - - void _init() async { - await Future.wait([_loadLocalAiState(), _loadAvailableModels()]); - _notifyStateChanged(); - _notifyAvailableModelsChanged(); - } - - void addListener({ - OnModelStateChangedCallback? onStateChanged, - OnAvailableModelsChangedCallback? onAvailableModelsChanged, - }) { - if (onStateChanged != null) { - _stateChangedCallbacks.add(onStateChanged); - } - if (onAvailableModelsChanged != null) { - _availableModelsChangedCallbacks.add(onAvailableModelsChanged); - } - } - - void removeListener({ - OnModelStateChangedCallback? onStateChanged, - OnAvailableModelsChangedCallback? onAvailableModelsChanged, - }) { - if (onStateChanged != null) { - _stateChangedCallbacks.remove(onStateChanged); - } - if (onAvailableModelsChanged != null) { - _availableModelsChangedCallbacks.remove(onAvailableModelsChanged); - } - } - - Future dispose() async { - _stateChangedCallbacks.clear(); - _availableModelsChangedCallbacks.clear(); - await _localAIListener?.stop(); - await _aiModelSwitchListener.stop(); - } - - (AiType, String, bool) getState() { - if (UniversalPlatform.isMobile) { - return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); - } - - final availableModels = _availableModels; - final localAiState = _localAIState; - - if (availableModels == null) { - return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); - } - if (localAiState == null) { - Log.warn("Cannot get local AI state"); - return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); - } - - if (!availableModels.selectedModel.isLocal) { - return (AiType.cloud, LocaleKeys.chat_inputMessageHint.tr(), true); - } - - final editable = localAiState.state == RunningStatePB.Running; - final hintText = editable - ? LocaleKeys.chat_inputLocalAIMessageHint.tr() - : LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(); - - return (AiType.local, hintText, editable); - } - - (List, AIModelPB?) getAvailableModels() { - final availableModels = _availableModels; - if (availableModels == null) { - return ([], null); - } - return (availableModels.models, availableModels.selectedModel); - } - - void _notifyAvailableModelsChanged() { - final (models, selectedModel) = getAvailableModels(); - for (final callback in _availableModelsChangedCallbacks) { - callback(models, selectedModel); - } - } - - void _notifyStateChanged() { - final (type, hintText, isEditable) = getState(); - for (final callback in _stateChangedCallbacks) { - callback(type, isEditable, hintText); - } - } - - Future _loadAvailableModels() { - final payload = AvailableModelsQueryPB(source: objectId); - return AIEventGetAvailableModels(payload).send().fold( - (models) => _availableModels = models, - (err) => Log.error("Failed to get available models: $err"), - ); - } - - Future _loadLocalAiState() { - return AIEventGetLocalAIState().send().fold( - (localAIState) => _localAIState = localAIState, - (error) => Log.error("Failed to get local AI state: $error"), - ); - } -} - -extension AiModelExtension on AIModelPB { - bool get isDefault { - return name == "Auto"; - } - - String get i18n { - return isDefault ? LocaleKeys.chat_switchModel_autoModel.tr() : name; - } -} diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart deleted file mode 100644 index 95854ab047..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_input_bloc.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/service/ai_model_state_notifier.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'ai_entities.dart'; - -part 'ai_prompt_input_bloc.freezed.dart'; - -class AIPromptInputBloc extends Bloc { - AIPromptInputBloc({ - required String objectId, - required PredefinedFormat? predefinedFormat, - }) : aiModelStateNotifier = AIModelStateNotifier(objectId: objectId), - super(AIPromptInputState.initial(predefinedFormat)) { - _dispatch(); - _startListening(); - _init(); - } - - final AIModelStateNotifier aiModelStateNotifier; - - @override - Future close() async { - await aiModelStateNotifier.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) { - event.when( - updateAIState: (aiType, editable, hintText) { - emit( - state.copyWith( - aiType: aiType, - editable: editable, - hintText: hintText, - ), - ); - }, - toggleShowPredefinedFormat: () { - final showPredefinedFormats = !state.showPredefinedFormats; - final predefinedFormat = - showPredefinedFormats && state.predefinedFormat == null - ? PredefinedFormat( - imageFormat: ImageFormat.text, - textFormat: TextFormat.paragraph, - ) - : null; - emit( - state.copyWith( - showPredefinedFormats: showPredefinedFormats, - predefinedFormat: predefinedFormat, - ), - ); - }, - updatePredefinedFormat: (format) { - if (!state.showPredefinedFormats) { - return; - } - emit(state.copyWith(predefinedFormat: format)); - }, - attachFile: (filePath, fileName) { - final newFile = ChatFile.fromFilePath(filePath); - if (newFile != null) { - emit( - state.copyWith( - attachedFiles: [...state.attachedFiles, newFile], - ), - ); - } - }, - removeFile: (file) { - final files = [...state.attachedFiles]; - files.remove(file); - emit( - state.copyWith( - attachedFiles: files, - ), - ); - }, - updateMentionedViews: (views) { - emit( - state.copyWith( - mentionedPages: views, - ), - ); - }, - clearMetadata: () { - emit( - state.copyWith( - attachedFiles: [], - mentionedPages: [], - ), - ); - }, - ); - }, - ); - } - - void _startListening() { - aiModelStateNotifier.addListener( - onStateChanged: (aiType, editable, hintText) { - add(AIPromptInputEvent.updateAIState(aiType, editable, hintText)); - }, - ); - } - - void _init() { - final (aiType, hintText, isEditable) = aiModelStateNotifier.getState(); - add(AIPromptInputEvent.updateAIState(aiType, isEditable, hintText)); - } - - Map consumeMetadata() { - final metadata = { - for (final file in state.attachedFiles) file.filePath: file, - for (final page in state.mentionedPages) page.id: page, - }; - - if (metadata.isNotEmpty && !isClosed) { - add(const AIPromptInputEvent.clearMetadata()); - } - - return metadata; - } -} - -@freezed -class AIPromptInputEvent with _$AIPromptInputEvent { - const factory AIPromptInputEvent.updateAIState( - AiType aiType, - bool editable, - String hintText, - ) = _UpdateAIState; - - const factory AIPromptInputEvent.toggleShowPredefinedFormat() = - _ToggleShowPredefinedFormat; - const factory AIPromptInputEvent.updatePredefinedFormat( - PredefinedFormat format, - ) = _UpdatePredefinedFormat; - const factory AIPromptInputEvent.attachFile( - String filePath, - String fileName, - ) = _AttachFile; - const factory AIPromptInputEvent.removeFile(ChatFile file) = _RemoveFile; - const factory AIPromptInputEvent.updateMentionedViews(List views) = - _UpdateMentionedViews; - const factory AIPromptInputEvent.clearMetadata() = _ClearMetadata; -} - -@freezed -class AIPromptInputState with _$AIPromptInputState { - const factory AIPromptInputState({ - required AiType aiType, - required bool supportChatWithFile, - required bool showPredefinedFormats, - required PredefinedFormat? predefinedFormat, - required List attachedFiles, - required List mentionedPages, - required bool editable, - required String hintText, - }) = _AIPromptInputState; - - factory AIPromptInputState.initial(PredefinedFormat? format) => - AIPromptInputState( - aiType: AiType.cloud, - supportChatWithFile: false, - showPredefinedFormats: format != null, - predefinedFormat: format, - attachedFiles: [], - mentionedPages: [], - editable: true, - hintText: '', - ); -} diff --git a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart b/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart deleted file mode 100644 index 39487652f8..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/appflowy_ai_service.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; -import 'package:appflowy/shared/list_extension.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:fixnum/fixnum.dart' as fixnum; - -import 'ai_entities.dart'; -import 'error.dart'; - -enum LocalAIStreamingState { - notReady, - disabled, -} - -abstract class AIRepository { - Future streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }); -} - -class AppFlowyAIService implements AIRepository { - @override - Future<(String, CompletionStream)?> streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }) async { - final stream = AppFlowyCompletionStream( - onStart: onStart, - processMessage: processMessage, - processAssistMessage: processAssistMessage, - processError: onError, - onLocalAIStreamingStateChange: onLocalAIStreamingStateChange, - onEnd: onEnd, - ); - - final records = history.map((record) => record.toPB()).toList(); - - final payload = CompleteTextPB( - text: text, - completionType: completionType, - format: format?.toPB(), - streamPort: fixnum.Int64(stream.nativePort), - objectId: objectId ?? '', - ragIds: [ - if (objectId != null) objectId, - ...sourceIds, - ].unique(), - history: records, - ); - - return AIEventCompleteText(payload).send().fold( - (task) => (task.taskId, stream), - (error) { - Log.error(error); - return null; - }, - ); - } -} - -abstract class CompletionStream { - CompletionStream({ - required this.onStart, - required this.processMessage, - required this.processAssistMessage, - required this.processError, - required this.onLocalAIStreamingStateChange, - required this.onEnd, - }); - - final Future Function() onStart; - final Future Function(String text) processMessage; - final Future Function(String text) processAssistMessage; - final void Function(AIError error) processError; - final void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange; - final Future Function() onEnd; -} - -class AppFlowyCompletionStream extends CompletionStream { - AppFlowyCompletionStream({ - required super.onStart, - required super.processMessage, - required super.processAssistMessage, - required super.processError, - required super.onEnd, - required super.onLocalAIStreamingStateChange, - }) { - _startListening(); - } - - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - int get nativePort => _port.sendPort.nativePort; - - void _startListening() { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - (event) async { - await _handleEvent(event); - }, - ); - } - - Future dispose() async { - await _controller.close(); - await _subscription.cancel(); - _port.close(); - } - - Future _handleEvent(String event) async { - // Check simple matches first - if (event == AIStreamEventPrefix.aiResponseLimit) { - processError( - AIError( - message: LocaleKeys.ai_textLimitReachedDescription.tr(), - code: AIErrorCode.aiResponseLimitExceeded, - ), - ); - return; - } - - if (event == AIStreamEventPrefix.aiImageResponseLimit) { - processError( - AIError( - message: LocaleKeys.ai_imageLimitReachedDescription.tr(), - code: AIErrorCode.aiImageResponseLimitExceeded, - ), - ); - return; - } - - // Otherwise, parse out prefix:content - if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) { - processError( - AIError( - message: event.substring(AIStreamEventPrefix.aiMaxRequired.length), - code: AIErrorCode.other, - ), - ); - } else if (event.startsWith(AIStreamEventPrefix.start)) { - await onStart(); - } else if (event.startsWith(AIStreamEventPrefix.data)) { - await processMessage( - event.substring(AIStreamEventPrefix.data.length), - ); - } else if (event.startsWith(AIStreamEventPrefix.comment)) { - await processAssistMessage( - event.substring(AIStreamEventPrefix.comment.length), - ); - } else if (event.startsWith(AIStreamEventPrefix.finish)) { - await onEnd(); - } else if (event.startsWith(AIStreamEventPrefix.localAIDisabled)) { - onLocalAIStreamingStateChange( - LocalAIStreamingState.disabled, - ); - } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) { - onLocalAIStreamingStateChange( - LocalAIStreamingState.notReady, - ); - } else if (event.startsWith(AIStreamEventPrefix.error)) { - processError( - AIError( - message: event.substring(AIStreamEventPrefix.error.length), - code: AIErrorCode.other, - ), - ); - } else { - Log.debug('Unknown AI event: $event'); - } - } -} diff --git a/frontend/appflowy_flutter/lib/ai/service/error.dart b/frontend/appflowy_flutter/lib/ai/service/error.dart deleted file mode 100644 index 0c98e83172..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/error.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'error.freezed.dart'; -part 'error.g.dart'; - -@freezed -class AIError with _$AIError { - const factory AIError({ - required String message, - required AIErrorCode code, - }) = _AIError; - - factory AIError.fromJson(Map json) => - _$AIErrorFromJson(json); -} - -enum AIErrorCode { - @JsonValue('AIResponseLimitExceeded') - aiResponseLimitExceeded, - @JsonValue('AIImageResponseLimitExceeded') - aiImageResponseLimitExceeded, - @JsonValue('Other') - other, -} - -extension AIErrorExtension on AIError { - bool get isLimitExceeded => code == AIErrorCode.aiResponseLimitExceeded; -} diff --git a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart b/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart deleted file mode 100644 index 7ad52b9ec4..0000000000 --- a/frontend/appflowy_flutter/lib/ai/service/select_model_bloc.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/service/ai_model_state_notifier.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbserver.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'select_model_bloc.freezed.dart'; - -class SelectModelBloc extends Bloc { - SelectModelBloc({ - required AIModelStateNotifier aiModelStateNotifier, - }) : _aiModelStateNotifier = aiModelStateNotifier, - super(SelectModelState.initial(aiModelStateNotifier)) { - on( - (event, emit) { - event.when( - selectModel: (model) { - AIEventUpdateSelectedModel( - UpdateSelectedModelPB( - source: _aiModelStateNotifier.objectId, - selectedModel: model, - ), - ).send(); - - emit(state.copyWith(selectedModel: model)); - }, - didLoadModels: (models, selectedModel) { - emit( - SelectModelState( - models: models, - selectedModel: selectedModel, - ), - ); - }, - ); - }, - ); - - _aiModelStateNotifier.addListener( - onAvailableModelsChanged: _onAvailableModelsChanged, - ); - } - - final AIModelStateNotifier _aiModelStateNotifier; - - @override - Future close() async { - _aiModelStateNotifier.removeListener( - onAvailableModelsChanged: _onAvailableModelsChanged, - ); - await super.close(); - } - - void _onAvailableModelsChanged( - List models, - AIModelPB? selectedModel, - ) { - if (!isClosed) { - add(SelectModelEvent.didLoadModels(models, selectedModel)); - } - } -} - -@freezed -class SelectModelEvent with _$SelectModelEvent { - const factory SelectModelEvent.selectModel( - AIModelPB model, - ) = _SelectModel; - - const factory SelectModelEvent.didLoadModels( - List models, - AIModelPB? selectedModel, - ) = _DidLoadModels; -} - -@freezed -class SelectModelState with _$SelectModelState { - const factory SelectModelState({ - required List models, - required AIModelPB? selectedModel, - }) = _SelectModelState; - - factory SelectModelState.initial(AIModelStateNotifier notifier) { - final (models, selectedModel) = notifier.getAvailableModels(); - return SelectModelState( - models: models, - selectedModel: selectedModel, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart b/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart deleted file mode 100644 index 3a9c96b255..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/loading_indicator.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; - -/// An animated generating indicator for an AI response -class AILoadingIndicator extends StatelessWidget { - const AILoadingIndicator({ - super.key, - this.text = "", - this.duration = const Duration(seconds: 1), - }); - - final String text; - final Duration duration; - - @override - Widget build(BuildContext context) { - final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return SelectionContainer.disabled( - child: SizedBox( - height: 20, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - Padding( - padding: const EdgeInsetsDirectional.only(end: 4.0), - child: FlowyText( - text, - color: Theme.of(context).hintColor, - ), - ), - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], - ), - ), - ); - } - - Widget buildDot(Color color) { - return SizedBox.square( - dimension: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/action_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/action_buttons.dart deleted file mode 100644 index 9dd370b39b..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/action_buttons.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; - -import 'layout_define.dart'; - -class PromptInputAttachmentButton extends StatelessWidget { - const PromptInputAttachmentButton({required this.onTap, super.key}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_uploadFile.tr(), - child: SizedBox.square( - dimension: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyIconButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: BorderRadius.circular(8), - icon: FlowySvg( - FlowySvgs.ai_attachment_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), - onPressed: onTap, - ), - ), - ); - } -} - -class PromptInputMentionButton extends StatelessWidget { - const PromptInputMentionButton({ - super.key, - required this.buttonSize, - required this.iconSize, - required this.onTap, - }); - - final double buttonSize; - final double iconSize; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_clickToMention.tr(), - preferBelow: false, - child: FlowyIconButton( - width: buttonSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: BorderRadius.circular(8), - icon: FlowySvg( - FlowySvgs.chat_at_s, - size: Size.square(iconSize), - color: Theme.of(context).iconTheme.color, - ), - onPressed: onTap, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart deleted file mode 100644 index a2676f2c15..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/desktop_prompt_text_field.dart +++ /dev/null @@ -1,702 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class DesktopPromptInput extends StatefulWidget { - const DesktopPromptInput({ - super.key, - required this.isStreaming, - required this.textController, - required this.onStopStreaming, - required this.onSubmitted, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - this.hideDecoration = false, - this.hideFormats = false, - this.extraBottomActionButton, - }); - - final bool isStreaming; - final TextEditingController textController; - final void Function() onStopStreaming; - final void Function(String, PredefinedFormat?, Map) - onSubmitted; - final ValueNotifier> selectedSourcesNotifier; - final void Function(List) onUpdateSelectedSources; - final bool hideDecoration; - final bool hideFormats; - final Widget? extraBottomActionButton; - - @override - State createState() => _DesktopPromptInputState(); -} - -class _DesktopPromptInputState extends State { - final textFieldKey = GlobalKey(); - final layerLink = LayerLink(); - final overlayController = OverlayPortalController(); - final inputControlCubit = ChatInputControlCubit(); - final focusNode = FocusNode(); - - late SendButtonState sendButtonState; - bool isComposing = false; - - @override - void initState() { - super.initState(); - - widget.textController.addListener(handleTextControllerChanged); - focusNode - ..addListener( - () { - if (!widget.hideDecoration) { - setState(() {}); // refresh border color - } - if (!focusNode.hasFocus) { - cancelMentionPage(); // hide menu when lost focus - } - }, - ) - ..onKeyEvent = handleKeyEvent; - - updateSendButtonState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - }); - } - - @override - void didUpdateWidget(covariant oldWidget) { - updateSendButtonState(); - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - focusNode.dispose(); - widget.textController.removeListener(handleTextControllerChanged); - inputControlCubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: inputControlCubit, - child: BlocListener( - listener: (context, state) { - state.maybeWhen( - updateSelectedViews: (selectedViews) { - context - .read() - .add(AIPromptInputEvent.updateMentionedViews(selectedViews)); - }, - orElse: () {}, - ); - }, - child: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return PromptInputMentionPageMenu( - anchor: PromptInputAnchor(textFieldKey, layerLink), - textController: widget.textController, - onPageSelected: handlePageSelected, - ); - }, - child: DecoratedBox( - decoration: decoration(context), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: - DesktopAIPromptSizes.attachedFilesBarPadding.vertical + - DesktopAIPromptSizes.attachedFilesPreviewHeight, - ), - child: TextFieldTapRegion( - child: PromptInputFile( - onDeleted: (file) => context - .read() - .add(AIPromptInputEvent.removeFile(file)), - ), - ), - ), - const VSpace(4.0), - BlocBuilder( - builder: (context, state) { - return Stack( - children: [ - ConstrainedBox( - constraints: getTextFieldConstraints( - state.showPredefinedFormats && !widget.hideFormats, - ), - child: inputTextField(), - ), - if (state.showPredefinedFormats && !widget.hideFormats) - Positioned.fill( - bottom: null, - child: TextFieldTapRegion( - child: Padding( - padding: const EdgeInsetsDirectional.only( - start: 8.0, - ), - child: ChangeFormatBar( - showImageFormats: state.aiType.isCloud, - predefinedFormat: state.predefinedFormat, - spacing: 4.0, - onSelectPredefinedFormat: (format) => - context.read().add( - AIPromptInputEvent - .updatePredefinedFormat(format), - ), - ), - ), - ), - ), - Positioned.fill( - top: null, - child: TextFieldTapRegion( - child: _PromptBottomActions( - showPredefinedFormatBar: - state.showPredefinedFormats, - showPredefinedFormatButton: !widget.hideFormats, - onTogglePredefinedFormatSection: () => - context.read().add( - AIPromptInputEvent - .toggleShowPredefinedFormat(), - ), - onStartMention: startMentionPageFromButton, - sendButtonState: sendButtonState, - onSendPressed: handleSend, - onStopStreaming: widget.onStopStreaming, - selectedSourcesNotifier: - widget.selectedSourcesNotifier, - onUpdateSelectedSources: - widget.onUpdateSelectedSources, - extraBottomActionButton: - widget.extraBottomActionButton, - ), - ), - ), - ], - ); - }, - ), - ], - ), - ), - ), - ), - ); - } - - BoxDecoration decoration(BuildContext context) { - if (widget.hideDecoration) { - return BoxDecoration(); - } - return BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: focusNode.hasFocus - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.outline, - width: focusNode.hasFocus ? 1.5 : 1.0, - ), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - ); - } - - void startMentionPageFromButton() { - if (overlayController.isShowing) { - return; - } - if (!focusNode.hasFocus) { - focusNode.requestFocus(); - } - widget.textController.text += '@'; - WidgetsBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - context - .read() - .startSearching(widget.textController.value); - overlayController.show(); - } - }); - } - - void cancelMentionPage() { - if (overlayController.isShowing) { - inputControlCubit.reset(); - overlayController.hide(); - } - } - - void updateSendButtonState() { - if (widget.isStreaming) { - sendButtonState = SendButtonState.streaming; - } else if (widget.textController.text.trim().isEmpty) { - sendButtonState = SendButtonState.disabled; - } else { - sendButtonState = SendButtonState.enabled; - } - } - - void handleSend() { - if (widget.isStreaming) { - return; - } - final trimmedText = inputControlCubit.formatIntputText( - widget.textController.text.trim(), - ); - widget.textController.clear(); - if (trimmedText.isEmpty) { - return; - } - - // get the attached files and mentioned pages - final metadata = context.read().consumeMetadata(); - - final bloc = context.read(); - final showPredefinedFormats = bloc.state.showPredefinedFormats; - final predefinedFormat = bloc.state.predefinedFormat; - - widget.onSubmitted( - trimmedText, - showPredefinedFormats ? predefinedFormat : null, - metadata, - ); - } - - void handleTextControllerChanged() { - setState(() { - // update whether send button is clickable - updateSendButtonState(); - isComposing = !widget.textController.value.composing.isCollapsed; - }); - - if (isComposing) { - return; - } - - // disable mention - return; - - // handle text and selection changes ONLY when mentioning a page - // ignore: dead_code - if (!overlayController.isShowing || - inputControlCubit.filterStartPosition == -1) { - return; - } - - // handle cases where mention a page is cancelled - final textController = widget.textController; - final textSelection = textController.value.selection; - final isSelectingMultipleCharacters = !textSelection.isCollapsed; - final isCaretBeforeStartOfRange = - textSelection.baseOffset < inputControlCubit.filterStartPosition; - final isCaretAfterEndOfRange = - textSelection.baseOffset > inputControlCubit.filterEndPosition; - final isTextSame = inputControlCubit.inputText == textController.text; - - if (isSelectingMultipleCharacters || - isTextSame && (isCaretBeforeStartOfRange || isCaretAfterEndOfRange)) { - cancelMentionPage(); - return; - } - - final previousLength = inputControlCubit.inputText.characters.length; - final currentLength = textController.text.characters.length; - - // delete "@" - if (previousLength != currentLength && isCaretBeforeStartOfRange) { - cancelMentionPage(); - return; - } - - // handle cases where mention the filter is updated - if (previousLength != currentLength) { - final diff = currentLength - previousLength; - final newEndPosition = inputControlCubit.filterEndPosition + diff; - final newFilter = textController.text.substring( - inputControlCubit.filterStartPosition, - newEndPosition, - ); - inputControlCubit.updateFilter( - textController.text, - newFilter, - newEndPosition: newEndPosition, - ); - } else if (!isTextSame) { - final newFilter = textController.text.substring( - inputControlCubit.filterStartPosition, - inputControlCubit.filterEndPosition, - ); - inputControlCubit.updateFilter(textController.text, newFilter); - } - } - - KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - // if (event.character == '@') { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // inputControlCubit.startSearching(widget.textController.value); - // overlayController.show(); - // }); - // } - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - node.unfocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - void handlePageSelected(ViewPB view) { - final newText = widget.textController.text.replaceRange( - inputControlCubit.filterStartPosition, - inputControlCubit.filterEndPosition, - view.id, - ); - widget.textController.value = TextEditingValue( - text: newText, - selection: TextSelection.collapsed( - offset: inputControlCubit.filterStartPosition + view.id.length, - affinity: TextAffinity.upstream, - ), - ); - - inputControlCubit.selectPage(view); - overlayController.hide(); - } - - Widget inputTextField() { - return Shortcuts( - shortcuts: buildShortcuts(), - child: Actions( - actions: buildActions(), - child: CompositedTransformTarget( - link: layerLink, - child: BlocBuilder( - builder: (context, state) { - Widget textField = PromptInputTextField( - key: textFieldKey, - editable: state.editable, - cubit: inputControlCubit, - textController: widget.textController, - textFieldFocusNode: focusNode, - contentPadding: - calculateContentPadding(state.showPredefinedFormats), - hintText: state.hintText, - ); - - if (!state.editable) { - textField = FlowyTooltip( - message: LocaleKeys - .settings_aiPage_keys_localAINotReadyTextFieldPrompt - .tr(), - child: textField, - ); - } - - return textField; - }, - ), - ), - ), - ); - } - - BoxConstraints getTextFieldConstraints(bool showPredefinedFormats) { - double minHeight = DesktopAIPromptSizes.textFieldMinHeight + - DesktopAIPromptSizes.actionBarSendButtonSize + - DesktopAIChatSizes.inputActionBarMargin.vertical; - double maxHeight = 300; - if (showPredefinedFormats) { - minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; - maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight; - } - return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight); - } - - EdgeInsetsGeometry calculateContentPadding(bool showPredefinedFormats) { - final top = showPredefinedFormats - ? DesktopAIPromptSizes.predefinedFormatButtonHeight - : 0.0; - final bottom = DesktopAIPromptSizes.actionBarSendButtonSize + - DesktopAIChatSizes.inputActionBarMargin.vertical; - - return DesktopAIPromptSizes.textFieldContentPadding - .add(EdgeInsets.only(top: top, bottom: bottom)); - } - - Map buildShortcuts() { - if (isComposing) { - return const {}; - } - - return const { - SingleActivator(LogicalKeyboardKey.arrowUp): _FocusPreviousItemIntent(), - SingleActivator(LogicalKeyboardKey.arrowDown): _FocusNextItemIntent(), - SingleActivator(LogicalKeyboardKey.escape): _CancelMentionPageIntent(), - SingleActivator(LogicalKeyboardKey.enter): _SubmitOrMentionPageIntent(), - }; - } - - Map> buildActions() { - return { - _FocusPreviousItemIntent: CallbackAction<_FocusPreviousItemIntent>( - onInvoke: (intent) { - inputControlCubit.updateSelectionUp(); - return; - }, - ), - _FocusNextItemIntent: CallbackAction<_FocusNextItemIntent>( - onInvoke: (intent) { - inputControlCubit.updateSelectionDown(); - return; - }, - ), - _CancelMentionPageIntent: CallbackAction<_CancelMentionPageIntent>( - onInvoke: (intent) { - cancelMentionPage(); - return; - }, - ), - _SubmitOrMentionPageIntent: CallbackAction<_SubmitOrMentionPageIntent>( - onInvoke: (intent) { - if (overlayController.isShowing) { - inputControlCubit.state.maybeWhen( - ready: (visibleViews, focusedViewIndex) { - if (focusedViewIndex != -1 && - focusedViewIndex < visibleViews.length) { - handlePageSelected(visibleViews[focusedViewIndex]); - } - }, - orElse: () {}, - ); - } else { - handleSend(); - } - return; - }, - ), - }; - } -} - -class _SubmitOrMentionPageIntent extends Intent { - const _SubmitOrMentionPageIntent(); -} - -class _CancelMentionPageIntent extends Intent { - const _CancelMentionPageIntent(); -} - -class _FocusPreviousItemIntent extends Intent { - const _FocusPreviousItemIntent(); -} - -class _FocusNextItemIntent extends Intent { - const _FocusNextItemIntent(); -} - -class PromptInputTextField extends StatelessWidget { - const PromptInputTextField({ - super.key, - required this.editable, - required this.cubit, - required this.textController, - required this.textFieldFocusNode, - required this.contentPadding, - this.hintText = "", - }); - - final ChatInputControlCubit cubit; - final TextEditingController textController; - final FocusNode textFieldFocusNode; - final EdgeInsetsGeometry contentPadding; - final bool editable; - final String hintText; - - @override - Widget build(BuildContext context) { - return ExtendedTextField( - controller: textController, - focusNode: textFieldFocusNode, - readOnly: !editable, - enabled: editable, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: contentPadding, - hintText: hintText, - hintStyle: inputHintTextStyle(context), - isCollapsed: true, - isDense: true, - ), - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - minLines: 1, - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - specialTextSpanBuilder: PromptInputTextSpanBuilder( - inputControlCubit: cubit, - specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ), - ); - } - - TextStyle? inputHintTextStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).isLightMode - ? const Color(0xFFBDC2C8) - : const Color(0xFF3C3E51), - ); - } -} - -class _PromptBottomActions extends StatelessWidget { - const _PromptBottomActions({ - required this.sendButtonState, - required this.showPredefinedFormatBar, - required this.showPredefinedFormatButton, - required this.onTogglePredefinedFormatSection, - required this.onStartMention, - required this.onSendPressed, - required this.onStopStreaming, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - this.extraBottomActionButton, - }); - - final bool showPredefinedFormatBar; - final bool showPredefinedFormatButton; - final void Function() onTogglePredefinedFormatSection; - final void Function() onStartMention; - final SendButtonState sendButtonState; - final void Function() onSendPressed; - final void Function() onStopStreaming; - final ValueNotifier> selectedSourcesNotifier; - final void Function(List) onUpdateSelectedSources; - final Widget? extraBottomActionButton; - - @override - Widget build(BuildContext context) { - return Container( - height: DesktopAIPromptSizes.actionBarSendButtonSize, - margin: DesktopAIChatSizes.inputActionBarMargin, - child: BlocBuilder( - builder: (context, state) { - return Row( - children: [ - if (showPredefinedFormatButton) ...[ - _predefinedFormatButton(), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - SelectModelMenu( - aiModelStateNotifier: - context.read().aiModelStateNotifier, - ), - const Spacer(), - if (state.aiType.isCloud) ...[ - _selectSourcesButton(), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - if (extraBottomActionButton != null) ...[ - extraBottomActionButton!, - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - // _mentionButton(context), - // const HSpace( - // DesktopAIPromptSizes.actionBarButtonSpacing, - // ), - if (state.supportChatWithFile) ...[ - _attachmentButton(context), - const HSpace( - DesktopAIChatSizes.inputActionBarButtonSpacing, - ), - ], - _sendButton(), - ], - ); - }, - ), - ); - } - - Widget _predefinedFormatButton() { - return PromptInputDesktopToggleFormatButton( - showFormatBar: showPredefinedFormatBar, - onTap: onTogglePredefinedFormatSection, - ); - } - - Widget _selectSourcesButton() { - return PromptInputDesktopSelectSourcesButton( - onUpdateSelectedSources: onUpdateSelectedSources, - selectedSourcesNotifier: selectedSourcesNotifier, - ); - } - - // Widget _mentionButton(BuildContext context) { - // return PromptInputMentionButton( - // iconSize: DesktopAIPromptSizes.actionBarIconSize, - // buttonSize: DesktopAIPromptSizes.actionBarButtonSize, - // onTap: onStartMention, - // ); - // } - - Widget _attachmentButton(BuildContext context) { - return PromptInputAttachmentButton( - onTap: () async { - final path = await getIt().pickFiles( - dialogTitle: '', - type: FileType.custom, - allowedExtensions: ["pdf", "txt", "md"], - ); - - if (path == null) { - return; - } - - for (final file in path.files) { - if (file.path != null && context.mounted) { - context - .read() - .add(AIPromptInputEvent.attachFile(file.path!, file.name)); - } - } - }, - ); - } - - Widget _sendButton() { - return PromptInputSendButton( - state: sendButtonState, - onSendPressed: onSendPressed, - onStopStreaming: onStopStreaming, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart deleted file mode 100644 index cd68205506..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/file_attachment_list.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/ai/service/ai_prompt_input_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_file_bloc.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:styled_widget/styled_widget.dart'; - -import 'layout_define.dart'; - -class PromptInputFile extends StatelessWidget { - const PromptInputFile({ - super.key, - required this.onDeleted, - }); - - final void Function(ChatFile) onDeleted; - - @override - Widget build(BuildContext context) { - return BlocSelector>( - selector: (state) => state.attachedFiles, - builder: (context, files) { - if (files.isEmpty) { - return const SizedBox.shrink(); - } - return ListView.separated( - scrollDirection: Axis.horizontal, - padding: DesktopAIPromptSizes.attachedFilesBarPadding - - const EdgeInsets.only(top: 6), - separatorBuilder: (context, index) => const HSpace( - DesktopAIPromptSizes.attachedFilesPreviewSpacing - 6, - ), - itemCount: files.length, - itemBuilder: (context, index) => ChatFilePreview( - file: files[index], - onDeleted: () => onDeleted(files[index]), - ), - ); - }, - ); - } -} - -class ChatFilePreview extends StatefulWidget { - const ChatFilePreview({ - required this.file, - required this.onDeleted, - super.key, - }); - - final ChatFile file; - final VoidCallback onDeleted; - - @override - State createState() => _ChatFilePreviewState(); -} - -class _ChatFilePreviewState extends State { - bool isHover = false; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ChatInputFileBloc(file: widget.file), - child: BlocBuilder( - builder: (context, state) { - return MouseRegion( - onEnter: (_) => setHover(true), - onExit: (_) => setHover(false), - child: Stack( - children: [ - Container( - margin: const EdgeInsetsDirectional.only(top: 6, end: 6), - constraints: const BoxConstraints(maxWidth: 240), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular(8), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - decoration: BoxDecoration( - color: AFThemeExtension.of(context).tint1, - borderRadius: BorderRadius.circular(8), - ), - height: 32, - width: 32, - child: Center( - child: FlowySvg( - FlowySvgs.page_m, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ), - ), - ), - const HSpace(8), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText( - widget.file.fileName, - fontSize: 12.0, - ), - FlowyText( - widget.file.fileType.name, - color: Theme.of(context).hintColor, - fontSize: 12.0, - ), - ], - ), - ), - ], - ), - ), - if (isHover) - _CloseButton( - onTap: widget.onDeleted, - ).positioned(top: 0, right: 0), - ], - ), - ); - }, - ), - ); - } - - void setHover(bool value) { - if (value != isHover) { - setState(() => isHover = value); - } - } -} - -class _CloseButton extends StatelessWidget { - const _CloseButton({required this.onTap}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap, - child: FlowySvg( - FlowySvgs.ai_close_filled_s, - color: AFThemeExtension.of(context).greyHover, - size: const Size.square(16), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/layout_define.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/layout_define.dart deleted file mode 100644 index e5c7e54522..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/layout_define.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class DesktopAIPromptSizes { - const DesktopAIPromptSizes._(); - - static const attachedFilesBarPadding = - EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0); - static const attachedFilesPreviewHeight = 48.0; - static const attachedFilesPreviewSpacing = 12.0; - - static const predefinedFormatButtonHeight = 28.0; - static const predefinedFormatIconHeight = 16.0; - - static const textFieldMinHeight = 36.0; - static const textFieldContentPadding = - EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0); - - static const actionBarButtonSize = 28.0; - static const actionBarIconSize = 16.0; - static const actionBarSendButtonSize = 32.0; - static const actionBarSendButtonIconSize = 24.0; -} - -class MobileAIPromptSizes { - const MobileAIPromptSizes._(); - - static const attachedFilesBarHeight = 68.0; - static const attachedFilesBarPadding = - EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0, bottom: 4.0); - static const attachedFilesPreviewHeight = 56.0; - static const attachedFilesPreviewSpacing = 8.0; - - static const predefinedFormatButtonHeight = 32.0; - static const predefinedFormatIconHeight = 20.0; - - static const textFieldMinHeight = 32.0; - static const textFieldContentPadding = - EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0); - - static const mentionIconSize = 20.0; - static const sendButtonSize = 32.0; -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_bottom_sheet.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_bottom_sheet.dart deleted file mode 100644 index 6e17f311f3..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_bottom_sheet.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/workspace/application/view/view_service.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 'mention_page_menu.dart'; - -Future showPageSelectorSheet( - BuildContext context, { - required bool Function(ViewPB view) filter, -}) async { - return showMobileBottomSheet( - context, - backgroundColor: Theme.of(context).colorScheme.surface, - maxChildSize: 0.98, - enableDraggableScrollable: true, - scrollableWidgetBuilder: (context, scrollController) { - return Expanded( - child: _MobilePageSelectorBody( - filter: filter, - scrollController: scrollController, - ), - ); - }, - builder: (context) => const SizedBox.shrink(), - ); -} - -class _MobilePageSelectorBody extends StatefulWidget { - const _MobilePageSelectorBody({ - this.filter, - this.scrollController, - }); - - final bool Function(ViewPB view)? filter; - final ScrollController? scrollController; - - @override - State<_MobilePageSelectorBody> createState() => - _MobilePageSelectorBodyState(); -} - -class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { - final textController = TextEditingController(); - late final Future> _viewsFuture = _fetchViews(); - - @override - void dispose() { - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomScrollView( - controller: widget.scrollController, - shrinkWrap: true, - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _Header( - child: ColoredBox( - color: Theme.of(context).cardColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - SizedBox( - height: 44.0, - child: Center( - child: FlowyText.medium( - LocaleKeys.document_mobilePageSelector_title.tr(), - fontSize: 16.0, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: SizedBox( - height: 44.0, - child: FlowySearchTextField( - controller: textController, - onChanged: (_) => setState(() {}), - ), - ), - ), - const Divider(height: 0.5, thickness: 0.5), - ], - ), - ), - ), - ), - FutureBuilder( - future: _viewsFuture, - builder: (_, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SliverToBoxAdapter( - child: CircularProgressIndicator.adaptive(), - ); - } - - if (snapshot.hasError || snapshot.data == null) { - return SliverToBoxAdapter( - child: FlowyText( - LocaleKeys.document_mobilePageSelector_failedToLoad.tr(), - ), - ); - } - - final views = snapshot.data! - .where((v) => widget.filter?.call(v) ?? true) - .toList(); - - final filtered = views.where( - (v) => - textController.text.isEmpty || - v.name - .toLowerCase() - .contains(textController.text.toLowerCase()), - ); - - if (filtered.isEmpty) { - return SliverToBoxAdapter( - child: FlowyText( - LocaleKeys.document_mobilePageSelector_noPagesFound.tr(), - ), - ); - } - - return SliverPadding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final view = filtered.elementAt(index); - return InkWell( - onTap: () => Navigator.of(context).pop(view), - borderRadius: BorderRadius.circular(12), - splashColor: Colors.transparent, - child: Container( - height: 44, - padding: const EdgeInsets.all(4.0), - child: Row( - children: [ - MentionViewIcon(view: view), - const HSpace(8), - Expanded( - child: MentionViewTitleAndAncestors(view: view), - ), - ], - ), - ), - ); - }, - childCount: filtered.length, - ), - ), - ); - }, - ), - ], - ); - } - - Future> _fetchViews() async => - (await ViewBackendService.getAllViews()).toNullable()?.items ?? []; -} - -class _Header extends SliverPersistentHeaderDelegate { - const _Header({ - required this.child, - }); - - final Widget child; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return child; - } - - @override - double get maxExtent => 120.5; - - @override - double get minExtent => 120.5; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return false; - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart deleted file mode 100644 index ae2dbe5f26..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mention_page_menu.dart +++ /dev/null @@ -1,435 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart' hide TextDirection; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:scroll_to_index/scroll_to_index.dart'; - -const double _itemHeight = 44.0; -const double _noPageHeight = 20.0; -const double _fixedWidth = 360.0; -const double _maxHeight = 328.0; - -class PromptInputAnchor { - PromptInputAnchor(this.anchorKey, this.layerLink); - - final GlobalKey> anchorKey; - final LayerLink layerLink; -} - -class PromptInputMentionPageMenu extends StatefulWidget { - const PromptInputMentionPageMenu({ - super.key, - required this.anchor, - required this.textController, - required this.onPageSelected, - }); - - final PromptInputAnchor anchor; - final TextEditingController textController; - final void Function(ViewPB view) onPageSelected; - - @override - State createState() => - _PromptInputMentionPageMenuState(); -} - -class _PromptInputMentionPageMenuState - extends State { - @override - void initState() { - super.initState(); - Future.delayed(Duration.zero, () { - if (mounted) { - context.read().refreshViews(); - } - }); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Stack( - children: [ - CompositedTransformFollower( - link: widget.anchor.layerLink, - showWhenUnlinked: false, - offset: Offset(getPopupOffsetX(), 0.0), - followerAnchor: Alignment.bottomLeft, - child: Container( - constraints: const BoxConstraints( - minWidth: _fixedWidth, - maxWidth: _fixedWidth, - maxHeight: _maxHeight, - ), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(6.0), - boxShadow: const [ - BoxShadow( - color: Color(0x0A1F2329), - blurRadius: 24, - offset: Offset(0, 8), - spreadRadius: 8, - ), - BoxShadow( - color: Color(0x0A1F2329), - blurRadius: 12, - offset: Offset(0, 6), - ), - BoxShadow( - color: Color(0x0F1F2329), - blurRadius: 8, - offset: Offset(0, 4), - spreadRadius: -8, - ), - ], - ), - child: TextFieldTapRegion( - child: PromptInputMentionPageList( - onPageSelected: widget.onPageSelected, - ), - ), - ), - ), - ], - ); - }, - ); - } - - double getPopupOffsetX() { - if (widget.anchor.anchorKey.currentContext == null) { - return 0.0; - } - - final cubit = context.read(); - if (cubit.filterStartPosition == -1) { - return 0.0; - } - - final textPosition = TextPosition(offset: cubit.filterEndPosition); - final renderBox = - widget.anchor.anchorKey.currentContext?.findRenderObject() as RenderBox; - - final textPainter = TextPainter( - text: TextSpan(text: cubit.formatIntputText(widget.textController.text)), - textDirection: TextDirection.ltr, - ); - textPainter.layout( - minWidth: renderBox.size.width, - maxWidth: renderBox.size.width, - ); - - final caretOffset = textPainter.getOffsetForCaret(textPosition, Rect.zero); - final boxes = textPainter.getBoxesForSelection( - TextSelection( - baseOffset: textPosition.offset, - extentOffset: textPosition.offset, - ), - ); - - if (boxes.isNotEmpty) { - return boxes.last.right; - } - - return caretOffset.dx; - } -} - -class PromptInputMentionPageList extends StatefulWidget { - const PromptInputMentionPageList({ - super.key, - required this.onPageSelected, - }); - - final void Function(ViewPB view) onPageSelected; - - @override - State createState() => - _PromptInputMentionPageListState(); -} - -class _PromptInputMentionPageListState - extends State { - final autoScrollController = SimpleAutoScrollController( - suggestedRowHeight: _itemHeight, - beginGetter: (rect) => rect.top + 8.0, - endGetter: (rect) => rect.bottom - 8.0, - ); - - @override - void dispose() { - autoScrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listenWhen: (previous, current) { - return previous.maybeWhen( - ready: (_, pFocusedViewIndex) => current.maybeWhen( - ready: (_, cFocusedViewIndex) => - pFocusedViewIndex != cFocusedViewIndex, - orElse: () => false, - ), - orElse: () => false, - ); - }, - listener: (context, state) { - state.maybeWhen( - ready: (views, focusedViewIndex) { - if (focusedViewIndex == -1 || !autoScrollController.hasClients) { - return; - } - if (autoScrollController.isAutoScrolling) { - autoScrollController.position - .jumpTo(autoScrollController.position.pixels); - } - autoScrollController.scrollToIndex( - focusedViewIndex, - duration: const Duration(milliseconds: 200), - preferPosition: AutoScrollPosition.begin, - ); - }, - orElse: () {}, - ); - }, - builder: (context, state) { - return state.maybeWhen( - loading: () { - return const Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - height: _noPageHeight, - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ), - ); - }, - ready: (views, focusedViewIndex) { - if (views.isEmpty) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - height: _noPageHeight, - child: Center( - child: FlowyText( - LocaleKeys.chat_inputActionNoPages.tr(), - ), - ), - ), - ); - } - - return ListView.builder( - shrinkWrap: true, - controller: autoScrollController, - padding: const EdgeInsets.all(8.0), - itemCount: views.length, - itemBuilder: (context, index) { - final view = views[index]; - return AutoScrollTag( - key: ValueKey("chat_mention_page_item_${view.id}"), - index: index, - controller: autoScrollController, - child: _ChatMentionPageItem( - view: view, - onTap: () => widget.onPageSelected(view), - isSelected: focusedViewIndex == index, - ), - ); - }, - ); - }, - orElse: () => const SizedBox.shrink(), - ); - }, - ); - } -} - -class _ChatMentionPageItem extends StatelessWidget { - const _ChatMentionPageItem({ - required this.view, - required this.isSelected, - required this.onTap, - }); - - final ViewPB view; - final bool isSelected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: view.name, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: FlowyHover( - isSelected: () => isSelected, - child: Container( - height: _itemHeight, - padding: const EdgeInsets.all(4.0), - child: Row( - children: [ - MentionViewIcon(view: view), - const HSpace(8.0), - Expanded(child: MentionViewTitleAndAncestors(view: view)), - ], - ), - ), - ), - ), - ), - ); - } -} - -class MentionViewIcon extends StatelessWidget { - const MentionViewIcon({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - final spaceIcon = view.buildSpaceIconSvg(context); - - if (view.icon.value.isNotEmpty) { - return SizedBox( - width: 16.0, - child: RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 14, - ), - ); - } - - if (view.isSpace == true && spaceIcon != null) { - return SpaceIcon( - dimension: 16.0, - svgSize: 9.68, - space: view, - cornerRadius: 4, - ); - } - - return FlowySvg( - view.layout.icon, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ); - } -} - -class MentionViewTitleAndAncestors extends StatelessWidget { - const MentionViewTitleAndAncestors({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ViewTitleBarBloc(view: view), - child: BlocBuilder( - builder: (context, state) { - final nonEmptyName = view.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : view.name; - - final ancestorList = _getViewAncestorList(state.ancestors); - - if (state.ancestors.isEmpty || ancestorList.trim().isEmpty) { - return FlowyText( - nonEmptyName, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - nonEmptyName, - fontSize: 14.0, - figmaLineHeight: 20.0, - overflow: TextOverflow.ellipsis, - ), - FlowyText( - ancestorList, - fontSize: 12.0, - figmaLineHeight: 16.0, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ], - ); - }, - ), - ); - } - - /// see workspace/presentation/widgets/view_title_bar.dart, upon which this - /// function was based. This version doesn't include the current view in the - /// result, and returns a string rather than a list of widgets - String _getViewAncestorList( - List views, - ) { - const lowerBound = 2; - final upperBound = views.length - 2; - bool hasAddedEllipsis = false; - String result = ""; - - if (views.length <= 1) { - return ""; - } - - // ignore the workspace name, use section name instead in the future - // skip the workspace view - for (var i = 1; i < views.length - 1; i++) { - final view = views[i]; - - if (i >= lowerBound && i < upperBound) { - if (!hasAddedEllipsis) { - hasAddedEllipsis = true; - result += "… / "; - } - continue; - } - - final nonEmptyName = view.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : view.name; - - result += nonEmptyName; - - if (i != views.length - 2) { - // if not the last one, add a divider - result += " / "; - } - } - return result; - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mentioned_page_text_span.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mentioned_page_text_span.dart deleted file mode 100644 index 7b519226a3..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/mentioned_page_text_span.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:extended_text_library/extended_text_library.dart'; -import 'package:flutter/material.dart'; - -class PromptInputTextSpanBuilder extends SpecialTextSpanBuilder { - PromptInputTextSpanBuilder({ - required this.inputControlCubit, - this.specialTextStyle, - }); - - final ChatInputControlCubit inputControlCubit; - final TextStyle? specialTextStyle; - - @override - SpecialText? createSpecialText( - String flag, { - TextStyle? textStyle, - SpecialTextGestureTapCallback? onTap, - int? index, - }) { - if (flag == '') { - return null; - } - - if (!isStart(flag, AtText.flag)) { - return null; - } - - // index is at the end of the start flag, so the start index should be index - (flag.length - 1) - return AtText( - inputControlCubit, - specialTextStyle ?? textStyle, - onTap, - // scrubbing over text is kinda funky - start: index! - (AtText.flag.length - 1), - ); - } -} - -class AtText extends SpecialText { - AtText( - this.inputControlCubit, - TextStyle? textStyle, - SpecialTextGestureTapCallback? onTap, { - this.start, - }) : super(flag, '', textStyle, onTap: onTap); - - static const String flag = '@'; - - final int? start; - final ChatInputControlCubit inputControlCubit; - - @override - bool isEnd(String value) => inputControlCubit.selectedViewIds.contains(value); - - @override - InlineSpan finishText() { - final String actualText = toString(); - - final viewName = inputControlCubit.allViews - .firstWhereOrNull((view) => view.id == actualText.substring(1)) - ?.name ?? - ""; - final nonEmptyName = viewName.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : viewName; - - return SpecialTextSpan( - text: "@$nonEmptyName", - actualText: actualText, - start: start!, - style: textStyle, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart deleted file mode 100644 index 403b978905..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/predefined_format_buttons.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../service/ai_entities.dart'; -import 'layout_define.dart'; - -class PromptInputDesktopToggleFormatButton extends StatelessWidget { - const PromptInputDesktopToggleFormatButton({ - super.key, - required this.showFormatBar, - required this.onTap, - }); - - final bool showFormatBar; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - tooltipText: showFormatBar - ? LocaleKeys.chat_changeFormat_defaultDescription.tr() - : LocaleKeys.chat_changeFormat_blankDescription.tr(), - width: 28.0, - onPressed: onTap, - icon: showFormatBar - ? const FlowySvg( - FlowySvgs.m_aa_text_s, - size: Size.square(16.0), - color: Color(0xFF666D76), - ) - : const FlowySvg( - FlowySvgs.ai_text_image_s, - size: Size(21.0, 16.0), - color: Color(0xFF666D76), - ), - ); - } -} - -class ChangeFormatBar extends StatelessWidget { - const ChangeFormatBar({ - super.key, - required this.predefinedFormat, - required this.spacing, - required this.onSelectPredefinedFormat, - this.showImageFormats = true, - }); - - final PredefinedFormat? predefinedFormat; - final double spacing; - final void Function(PredefinedFormat) onSelectPredefinedFormat; - final bool showImageFormats; - - @override - Widget build(BuildContext context) { - final showTextFormats = predefinedFormat?.imageFormat.hasText ?? true; - return SizedBox( - height: DesktopAIPromptSizes.predefinedFormatButtonHeight, - child: SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => HSpace(spacing), - children: [ - if (showImageFormats) ...[ - _buildFormatButton(context, ImageFormat.text), - _buildFormatButton(context, ImageFormat.textAndImage), - _buildFormatButton(context, ImageFormat.image), - ], - if (showImageFormats && showTextFormats) _buildDivider(), - if (showTextFormats) ...[ - _buildTextFormatButton(context, TextFormat.paragraph), - _buildTextFormatButton(context, TextFormat.bulletList), - _buildTextFormatButton(context, TextFormat.numberedList), - _buildTextFormatButton(context, TextFormat.table), - ], - ], - ), - ); - } - - Widget _buildFormatButton(BuildContext context, ImageFormat format) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (predefinedFormat != null && - format == predefinedFormat!.imageFormat) { - return; - } - if (format.hasText) { - final textFormat = - predefinedFormat?.textFormat ?? TextFormat.paragraph; - onSelectPredefinedFormat( - PredefinedFormat(imageFormat: format, textFormat: textFormat), - ); - } else { - onSelectPredefinedFormat( - PredefinedFormat(imageFormat: format, textFormat: null), - ); - } - }, - child: FlowyTooltip( - message: format.i18n, - preferBelow: false, - child: SizedBox.square( - dimension: _buttonSize, - child: FlowyHover( - isSelected: () => format == predefinedFormat?.imageFormat, - child: Center( - child: FlowySvg( - format.icon, - size: format == ImageFormat.textAndImage - ? Size(21.0 / 16.0 * _iconSize, _iconSize) - : Size.square(_iconSize), - ), - ), - ), - ), - ), - ); - } - - Widget _buildDivider() { - return VerticalDivider( - indent: 6.0, - endIndent: 6.0, - width: 1.0 + spacing * 2, - ); - } - - Widget _buildTextFormatButton( - BuildContext context, - TextFormat format, - ) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (predefinedFormat != null && - format == predefinedFormat!.textFormat) { - return; - } - onSelectPredefinedFormat( - PredefinedFormat( - imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text, - textFormat: format, - ), - ); - }, - child: FlowyTooltip( - message: format.i18n, - preferBelow: false, - child: SizedBox.square( - dimension: _buttonSize, - child: FlowyHover( - isSelected: () => format == predefinedFormat?.textFormat, - child: Center( - child: FlowySvg( - format.icon, - size: Size.square(_iconSize), - ), - ), - ), - ), - ), - ); - } - - double get _buttonSize { - return UniversalPlatform.isMobile - ? MobileAIPromptSizes.predefinedFormatButtonHeight - : DesktopAIPromptSizes.predefinedFormatButtonHeight; - } - - double get _iconSize { - return UniversalPlatform.isMobile - ? MobileAIPromptSizes.predefinedFormatIconHeight - : DesktopAIPromptSizes.predefinedFormatIconHeight; - } -} - -class PromptInputMobileToggleFormatButton extends StatelessWidget { - const PromptInputMobileToggleFormatButton({ - super.key, - required this.showFormatBar, - required this.onTap, - }); - - final bool showFormatBar; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox.square( - dimension: 32.0, - child: FlowyButton( - radius: const BorderRadius.all(Radius.circular(8.0)), - margin: EdgeInsets.zero, - expandText: false, - text: showFormatBar - ? const FlowySvg( - FlowySvgs.m_aa_text_s, - size: Size.square(20.0), - ) - : const FlowySvg( - FlowySvgs.ai_text_image_s, - size: Size(26.25, 20.0), - ), - onTap: onTap, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart deleted file mode 100644 index a611d84310..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_model_menu.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SelectModelMenu extends StatefulWidget { - const SelectModelMenu({ - super.key, - required this.aiModelStateNotifier, - }); - - final AIModelStateNotifier aiModelStateNotifier; - - @override - State createState() => _SelectModelMenuState(); -} - -class _SelectModelMenuState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SelectModelBloc( - aiModelStateNotifier: widget.aiModelStateNotifier, - ), - child: BlocBuilder( - builder: (context, state) { - return AppFlowyPopover( - offset: Offset(-12.0, 0.0), - constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), - direction: PopoverDirection.topWithLeftAligned, - margin: EdgeInsets.zero, - controller: popoverController, - popupBuilder: (popoverContext) { - return SelectModelPopoverContent( - models: state.models, - selectedModel: state.selectedModel, - onSelectModel: (model) { - if (model != state.selectedModel) { - context - .read() - .add(SelectModelEvent.selectModel(model)); - } - popoverController.close(); - }, - ); - }, - child: _CurrentModelButton( - model: state.selectedModel, - onTap: () { - if (state.selectedModel != null) { - popoverController.show(); - } - }, - ), - ); - }, - ), - ); - } -} - -class SelectModelPopoverContent extends StatelessWidget { - const SelectModelPopoverContent({ - super.key, - required this.models, - required this.selectedModel, - this.onSelectModel, - }); - - final List models; - final AIModelPB? selectedModel; - final void Function(AIModelPB)? onSelectModel; - - @override - Widget build(BuildContext context) { - if (models.isEmpty) { - return const SizedBox.shrink(); - } - - // separate models into local and cloud models - final localModels = models.where((model) => model.isLocal).toList(); - final cloudModels = models.where((model) => !model.isLocal).toList(); - - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (localModels.isNotEmpty) ...[ - _ModelSectionHeader( - title: LocaleKeys.chat_switchModel_localModel.tr(), - ), - const VSpace(4.0), - ], - ...localModels.map( - (model) => _ModelItem( - model: model, - isSelected: model == selectedModel, - onTap: () => onSelectModel?.call(model), - ), - ), - if (cloudModels.isNotEmpty && localModels.isNotEmpty) ...[ - const VSpace(8.0), - _ModelSectionHeader( - title: LocaleKeys.chat_switchModel_cloudModel.tr(), - ), - const VSpace(4.0), - ], - ...cloudModels.map( - (model) => _ModelItem( - model: model, - isSelected: model == selectedModel, - onTap: () => onSelectModel?.call(model), - ), - ), - ], - ), - ); - } -} - -class _ModelSectionHeader extends StatelessWidget { - const _ModelSectionHeader({ - required this.title, - }); - - final String title; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 2), - child: FlowyText( - title, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w500, - ), - ); - } -} - -class _ModelItem extends StatelessWidget { - const _ModelItem({ - required this.model, - required this.isSelected, - required this.onTap, - }); - - final AIModelPB model; - final bool isSelected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 32), - child: FlowyButton( - onTap: onTap, - margin: EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - model.i18n, - figmaLineHeight: 20, - overflow: TextOverflow.ellipsis, - ), - if (model.desc.isNotEmpty) - FlowyText( - model.desc, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ], - ), - rightIcon: isSelected - ? FlowySvg( - FlowySvgs.check_s, - size: const Size.square(20), - color: Theme.of(context).colorScheme.primary, - ) - : null, - ), - ); - } -} - -class _CurrentModelButton extends StatelessWidget { - const _CurrentModelButton({ - required this.model, - required this.onTap, - }); - - final AIModelPB? model; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_switchModel_label.tr(), - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: AnimatedSize( - duration: const Duration(milliseconds: 50), - curve: Curves.easeInOut, - alignment: AlignmentDirectional.centerStart, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.all(4.0), - child: Row( - children: [ - Padding( - // TODO: remove this after change icon to 20px - padding: EdgeInsets.all(2), - child: FlowySvg( - FlowySvgs.ai_sparks_s, - color: Theme.of(context).hintColor, - size: Size.square(16), - ), - ), - if (model != null && !model!.isDefault) - Padding( - padding: EdgeInsetsDirectional.only(end: 2.0), - child: FlowyText( - model!.i18n, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart deleted file mode 100644 index 1f1b2ddf4c..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_bottom_sheet.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/flowy_search_text_field.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; -import 'package:appflowy/plugins/base/drag_handler.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/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.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 'select_sources_menu.dart'; - -class PromptInputMobileSelectSourcesButton extends StatefulWidget { - const PromptInputMobileSelectSourcesButton({ - super.key, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - }); - - final ValueNotifier> selectedSourcesNotifier; - final void Function(List) onUpdateSelectedSources; - - @override - State createState() => - _PromptInputMobileSelectSourcesButtonState(); -} - -class _PromptInputMobileSelectSourcesButtonState - extends State { - late final cubit = ChatSettingsCubit(); - - @override - void initState() { - super.initState(); - widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged); - WidgetsBinding.instance.addPostFrameCallback((_) { - onSelectedSourcesChanged(); - }); - } - - @override - void dispose() { - widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged); - cubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final userProfile = context.read().userProfile; - final workspaceId = state.currentWorkspace?.workspaceId ?? ''; - return MultiBlocProvider( - providers: [ - BlocProvider( - key: ValueKey(workspaceId), - create: (context) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - ), - BlocProvider.value( - value: cubit, - ), - ], - child: BlocBuilder( - builder: (context, state) { - return FlowyButton( - margin: const EdgeInsetsDirectional.fromSTEB(4, 6, 2, 6), - expandText: false, - text: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.ai_page_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(20.0), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(10), - ), - ], - ), - onTap: () async { - context - .read() - .refreshSources(state.spaces, state.currentSpace); - await showMobileBottomSheet( - context, - backgroundColor: Theme.of(context).colorScheme.surface, - maxChildSize: 0.98, - enableDraggableScrollable: true, - scrollableWidgetBuilder: (_, scrollController) { - return Expanded( - child: BlocProvider.value( - value: cubit, - child: _MobileSelectSourcesSheetBody( - scrollController: scrollController, - ), - ), - ); - }, - builder: (context) => const SizedBox.shrink(), - ); - if (context.mounted) { - widget.onUpdateSelectedSources(cubit.selectedSourceIds); - } - }, - ); - }, - ), - ); - }, - ); - } - - void onSelectedSourcesChanged() { - cubit - ..updateSelectedSources(widget.selectedSourcesNotifier.value) - ..updateSelectedStatus(); - } -} - -class _MobileSelectSourcesSheetBody extends StatefulWidget { - const _MobileSelectSourcesSheetBody({ - required this.scrollController, - }); - - final ScrollController scrollController; - - @override - State<_MobileSelectSourcesSheetBody> createState() => - _MobileSelectSourcesSheetBodyState(); -} - -class _MobileSelectSourcesSheetBodyState - extends State<_MobileSelectSourcesSheetBody> { - final textController = TextEditingController(); - - @override - void dispose() { - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomScrollView( - controller: widget.scrollController, - shrinkWrap: true, - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _Header( - child: ColoredBox( - color: Theme.of(context).cardColor, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - SizedBox( - height: 44.0, - child: Center( - child: FlowyText.medium( - LocaleKeys.chat_selectSources.tr(), - fontSize: 16.0, - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: SizedBox( - height: 44.0, - child: FlowySearchTextField( - controller: textController, - onChanged: (value) => context - .read() - .updateFilter(value), - ), - ), - ), - const Divider(height: 0.5, thickness: 0.5), - ], - ), - ), - ), - ), - BlocBuilder( - builder: (context, state) { - final sources = state.visibleSources - .where((e) => e.ignoreStatus != IgnoreViewType.hide); - return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: sources.length, - (context, index) { - final source = sources.elementAt(index); - return ChatSourceTreeItem( - key: ValueKey( - 'visible_select_sources_tree_item_${source.view.id}', - ), - chatSource: source, - level: 0, - isDescendentOfSpace: source.view.isSpace, - isSelectedSection: false, - onSelected: (chatSource) { - context - .read() - .toggleSelectedStatus(chatSource); - }, - height: 40.0, - ); - }, - ), - ); - }, - ), - ], - ); - } -} - -class _Header extends SliverPersistentHeaderDelegate { - const _Header({ - required this.child, - }); - - final Widget child; - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return child; - } - - @override - double get maxExtent => 120.5; - - @override - double get minExtent => 120.5; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return false; - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart deleted file mode 100644 index 51357e6a0b..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/select_sources_menu.dart +++ /dev/null @@ -1,588 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.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:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'layout_define.dart'; -import 'mention_page_menu.dart'; - -class PromptInputDesktopSelectSourcesButton extends StatefulWidget { - const PromptInputDesktopSelectSourcesButton({ - super.key, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - }); - - final ValueNotifier> selectedSourcesNotifier; - final void Function(List) onUpdateSelectedSources; - - @override - State createState() => - _PromptInputDesktopSelectSourcesButtonState(); -} - -class _PromptInputDesktopSelectSourcesButtonState - extends State { - late final cubit = ChatSettingsCubit(); - final popoverController = PopoverController(); - - @override - void initState() { - super.initState(); - widget.selectedSourcesNotifier.addListener(onSelectedSourcesChanged); - WidgetsBinding.instance.addPostFrameCallback((_) { - onSelectedSourcesChanged(); - }); - } - - @override - void dispose() { - widget.selectedSourcesNotifier.removeListener(onSelectedSourcesChanged); - cubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final userWorkspaceBloc = context.read(); - final userProfile = userWorkspaceBloc.userProfile; - final workspaceId = - userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? ''; - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - ), - BlocProvider.value( - value: cubit, - ), - ], - child: BlocBuilder( - builder: (context, state) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(320, 380)), - offset: const Offset(0.0, -10.0), - direction: PopoverDirection.topWithCenterAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () { - context - .read() - .refreshSources(state.spaces, state.currentSpace); - }, - onClose: () { - widget.onUpdateSelectedSources(cubit.selectedSourceIds); - context - .read() - .refreshSources(state.spaces, state.currentSpace); - }, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: const _PopoverContent(), - ); - }, - child: _IndicatorButton( - selectedSourcesNotifier: widget.selectedSourcesNotifier, - onTap: () => popoverController.show(), - ), - ); - }, - ), - ); - } - - void onSelectedSourcesChanged() { - cubit - ..updateSelectedSources(widget.selectedSourcesNotifier.value) - ..updateSelectedStatus(); - } -} - -class _IndicatorButton extends StatelessWidget { - const _IndicatorButton({ - required this.selectedSourcesNotifier, - required this.onTap, - }); - - final ValueNotifier> selectedSourcesNotifier; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.ai_page_s, - color: Theme.of(context).hintColor, - ), - const HSpace(2.0), - ValueListenableBuilder( - valueListenable: selectedSourcesNotifier, - builder: (context, selectedSourceIds, _) { - final documentId = - context.read()?.documentId; - final label = documentId != null && - selectedSourceIds.length == 1 && - selectedSourceIds[0] == documentId - ? LocaleKeys.chat_currentPage.tr() - : selectedSourceIds.length.toString(); - return FlowyText( - label, - fontSize: 12, - figmaLineHeight: 16, - color: Theme.of(context).hintColor, - ); - }, - ), - const HSpace(2.0), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - ), - ), - ), - ); - } -} - -class _PopoverContent extends StatelessWidget { - const _PopoverContent(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 8), - child: SpaceSearchField( - width: 600, - onSearch: (context, value) => - context.read().updateFilter(value), - ), - ), - _buildDivider(), - Flexible( - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), - children: [ - ..._buildSelectedSources(context, state), - if (state.selectedSources.isNotEmpty && - state.visibleSources.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: _buildDivider(), - ), - ..._buildVisibleSources(context, state), - ], - ), - ), - ], - ); - }, - ); - } - - Widget _buildDivider() { - return const Divider( - height: 1.0, - thickness: 1.0, - indent: 8.0, - endIndent: 8.0, - ); - } - - Iterable _buildSelectedSources( - BuildContext context, - ChatSettingsState state, - ) { - return state.selectedSources - .where((e) => e.ignoreStatus != IgnoreViewType.hide) - .map( - (e) => ChatSourceTreeItem( - key: ValueKey( - 'selected_select_sources_tree_item_${e.view.id}', - ), - chatSource: e, - level: 0, - isDescendentOfSpace: e.view.isSpace, - isSelectedSection: true, - onSelected: (chatSource) { - context - .read() - .toggleSelectedStatus(chatSource); - }, - height: 30.0, - ), - ); - } - - Iterable _buildVisibleSources( - BuildContext context, - ChatSettingsState state, - ) { - return state.visibleSources - .where((e) => e.ignoreStatus != IgnoreViewType.hide) - .map( - (e) => ChatSourceTreeItem( - key: ValueKey( - 'visible_select_sources_tree_item_${e.view.id}', - ), - chatSource: e, - level: 0, - isDescendentOfSpace: e.view.isSpace, - isSelectedSection: false, - onSelected: (chatSource) { - context - .read() - .toggleSelectedStatus(chatSource); - }, - height: 30.0, - ), - ); - } -} - -class ChatSourceTreeItem extends StatefulWidget { - const ChatSourceTreeItem({ - super.key, - required this.chatSource, - required this.level, - required this.isDescendentOfSpace, - required this.isSelectedSection, - required this.onSelected, - this.onAdd, - required this.height, - this.showSaveButton = false, - this.showCheckbox = true, - }); - - final ChatSource chatSource; - - /// nested level of the view item - final int level; - - final bool isDescendentOfSpace; - - final bool isSelectedSection; - - final void Function(ChatSource chatSource) onSelected; - - final void Function(ChatSource chatSource)? onAdd; - - final bool showSaveButton; - - final double height; - - final bool showCheckbox; - - @override - State createState() => _ChatSourceTreeItemState(); -} - -class _ChatSourceTreeItemState extends State { - @override - Widget build(BuildContext context) { - final child = SizedBox( - height: widget.height, - child: ChatSourceTreeItemInner( - chatSource: widget.chatSource, - level: widget.level, - isDescendentOfSpace: widget.isDescendentOfSpace, - isSelectedSection: widget.isSelectedSection, - showCheckbox: widget.showCheckbox, - showSaveButton: widget.showSaveButton, - onSelected: widget.onSelected, - onAdd: widget.onAdd, - ), - ); - - final disabledEnabledChild = - widget.chatSource.ignoreStatus == IgnoreViewType.disable - ? FlowyTooltip( - message: widget.showCheckbox - ? switch (widget.chatSource.view.layout) { - ViewLayoutPB.Document => - LocaleKeys.chat_sourcesLimitReached.tr(), - _ => LocaleKeys.chat_sourceUnsupported.tr(), - } - : "", - child: Opacity( - opacity: 0.5, - child: MouseRegion( - cursor: SystemMouseCursors.forbidden, - child: IgnorePointer(child: child), - ), - ), - ) - : child; - - return ValueListenableBuilder( - valueListenable: widget.chatSource.isExpandedNotifier, - builder: (context, isExpanded, child) { - // filter the child views that should be ignored - final childViews = widget.chatSource.children - .where((e) => e.ignoreStatus != IgnoreViewType.hide) - .toList(); - - if (!isExpanded || childViews.isEmpty) { - return disabledEnabledChild; - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - disabledEnabledChild, - ...childViews.map( - (childSource) => ChatSourceTreeItem( - key: ValueKey( - 'select_sources_tree_item_${childSource.view.id}', - ), - chatSource: childSource, - level: widget.level + 1, - isDescendentOfSpace: widget.isDescendentOfSpace, - isSelectedSection: widget.isSelectedSection, - onSelected: widget.onSelected, - height: widget.height, - showCheckbox: widget.showCheckbox, - showSaveButton: widget.showSaveButton, - onAdd: widget.onAdd, - ), - ), - ], - ); - }, - ); - } -} - -class ChatSourceTreeItemInner extends StatelessWidget { - const ChatSourceTreeItemInner({ - super.key, - required this.chatSource, - required this.level, - required this.isDescendentOfSpace, - required this.isSelectedSection, - required this.showCheckbox, - required this.showSaveButton, - this.onSelected, - this.onAdd, - }); - - final ChatSource chatSource; - final int level; - final bool isDescendentOfSpace; - final bool isSelectedSection; - final bool showCheckbox; - final bool showSaveButton; - final void Function(ChatSource)? onSelected; - final void Function(ChatSource)? onAdd; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - if (!isSelectedSection) { - onSelected?.call(chatSource); - } - }, - child: FlowyHover( - cursor: isSelectedSection ? SystemMouseCursors.basic : null, - style: HoverStyle( - hoverColor: isSelectedSection - ? Colors.transparent - : AFThemeExtension.of(context).lightGreyHover, - ), - builder: (context, onHover) { - final isSaveButtonVisible = - showSaveButton && !chatSource.view.isSpace; - final isAddButtonVisible = onAdd != null; - return Row( - children: [ - const HSpace(4.0), - HSpace(max(20.0 * level - (isDescendentOfSpace ? 2 : 0), 0)), - // builds the >, ^ or · button - ToggleIsExpandedButton( - chatSource: chatSource, - isSelectedSection: isSelectedSection, - ), - const HSpace(2.0), - // checkbox - if (!chatSource.view.isSpace && showCheckbox) ...[ - SourceSelectedStatusCheckbox( - chatSource: chatSource, - ), - const HSpace(4.0), - ], - // icon - MentionViewIcon( - view: chatSource.view, - ), - const HSpace(6.0), - // title - Expanded( - child: FlowyText( - chatSource.view.nameOrDefault, - overflow: TextOverflow.ellipsis, - fontSize: 14.0, - figmaLineHeight: 18.0, - ), - ), - if (onHover && (isSaveButtonVisible || isAddButtonVisible)) ...[ - const HSpace(4.0), - if (isSaveButtonVisible) - FlowyIconButton( - tooltipText: LocaleKeys.chat_addToPageButton.tr(), - width: 24, - icon: FlowySvg( - FlowySvgs.ai_add_to_page_s, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ), - onPressed: () => onSelected?.call(chatSource), - ), - if (isSaveButtonVisible && isAddButtonVisible) - const HSpace(4.0), - if (isAddButtonVisible) - FlowyIconButton( - tooltipText: LocaleKeys.chat_addToNewPage.tr(), - width: 24, - icon: FlowySvg( - FlowySvgs.add_less_padding_s, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ), - onPressed: () => onAdd?.call(chatSource), - ), - const HSpace(4.0), - ], - ], - ); - }, - ), - ); - } -} - -class ToggleIsExpandedButton extends StatelessWidget { - const ToggleIsExpandedButton({ - super.key, - required this.chatSource, - required this.isSelectedSection, - }); - - final ChatSource chatSource; - final bool isSelectedSection; - - @override - Widget build(BuildContext context) { - if (isReferencedDatabaseView(chatSource.view, chatSource.parentView)) { - return const _DotIconWidget(); - } - - if (chatSource.children.isEmpty) { - return const SizedBox.square(dimension: 16.0); - } - - return FlowyHover( - child: GestureDetector( - child: ValueListenableBuilder( - valueListenable: chatSource.isExpandedNotifier, - builder: (context, value, _) => FlowySvg( - value - ? FlowySvgs.view_item_expand_s - : FlowySvgs.view_item_unexpand_s, - size: const Size.square(16.0), - ), - ), - onTap: () => context - .read() - .toggleIsExpanded(chatSource, isSelectedSection), - ), - ); - } -} - -class _DotIconWidget extends StatelessWidget { - const _DotIconWidget(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(6.0), - child: Container( - width: 4, - height: 4, - decoration: BoxDecoration( - color: Theme.of(context).iconTheme.color, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} - -class SourceSelectedStatusCheckbox extends StatelessWidget { - const SourceSelectedStatusCheckbox({ - super.key, - required this.chatSource, - }); - - final ChatSource chatSource; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: chatSource.selectedStatusNotifier, - builder: (context, selectedStatus, _) => FlowySvg( - switch (selectedStatus) { - SourceSelectedStatus.unselected => FlowySvgs.uncheck_s, - SourceSelectedStatus.selected => FlowySvgs.check_filled_s, - SourceSelectedStatus.partiallySelected => FlowySvgs.check_partial_s, - }, - size: const Size.square(18.0), - blendMode: null, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart b/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart deleted file mode 100644 index cca6e65f63..0000000000 --- a/frontend/appflowy_flutter/lib/ai/widgets/prompt_input/send_button.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'layout_define.dart'; - -enum SendButtonState { enabled, streaming, disabled } - -class PromptInputSendButton extends StatelessWidget { - const PromptInputSendButton({ - super.key, - required this.state, - required this.onSendPressed, - required this.onStopStreaming, - }); - - final SendButtonState state; - final VoidCallback onSendPressed; - final VoidCallback onStopStreaming; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - width: _buttonSize, - richTooltipText: switch (state) { - SendButtonState.streaming => TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.chat_stopTooltip.tr()} ', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: 'ESC', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ), - _ => null, - }, - icon: switch (state) { - SendButtonState.enabled => FlowySvg( - FlowySvgs.ai_send_filled_s, - size: Size.square(_iconSize), - color: Theme.of(context).colorScheme.primary, - ), - SendButtonState.disabled => FlowySvg( - FlowySvgs.ai_send_filled_s, - size: Size.square(_iconSize), - color: Theme.of(context).disabledColor, - ), - SendButtonState.streaming => FlowySvg( - FlowySvgs.ai_stop_filled_s, - size: Size.square(_iconSize), - color: Theme.of(context).colorScheme.primary, - ), - }, - onPressed: () { - switch (state) { - case SendButtonState.enabled: - onSendPressed(); - break; - case SendButtonState.streaming: - onStopStreaming(); - break; - case SendButtonState.disabled: - break; - } - }, - hoverColor: Colors.transparent, - ); - } - - double get _buttonSize { - return UniversalPlatform.isMobile - ? MobileAIPromptSizes.sendButtonSize - : DesktopAIPromptSizes.actionBarSendButtonSize; - } - - double get _iconSize { - return UniversalPlatform.isMobile - ? MobileAIPromptSizes.sendButtonSize - : DesktopAIPromptSizes.actionBarSendButtonIconSize; - } -} diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index aefd5e5d36..bd4d68fa5a 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -18,13 +18,6 @@ class KVKeys { /// {'dx': 10.0, 'dy': 10.0} static const String windowPosition = 'windowPosition'; - /// The key for saving the window status - /// - /// The value is a json string with the following format: - /// { 'windowMaximized': true } - /// - static const String windowMaximized = 'windowMaximized'; - static const String kDocumentAppearanceFontSize = 'kDocumentAppearanceFontSize'; static const String kDocumentAppearanceFontFamily = @@ -35,7 +28,6 @@ class KVKeys { 'kDocumentAppearanceCursorColor'; static const String kDocumentAppearanceSelectionColor = 'kDocumentAppearanceSelectionColor'; - static const String kDocumentAppearanceWidth = 'kDocumentAppearanceWidth'; /// The key for saving the expanded views /// @@ -49,7 +41,6 @@ class KVKeys { /// {'SidebarFolderCategoryType.value': true} static const String expandedFolders = 'expandedFolders'; - /// @deprecated in version 0.7.6 /// The key for saving if showing the rename dialog when creating a new file /// /// The value is a boolean string. @@ -58,8 +49,8 @@ class KVKeys { static const String kCloudType = 'kCloudType'; static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL'; - static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain'; - static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace'; + static const String kSupabaseURL = 'kSupabaseURL'; + static const String kSupabaseAnonKey = 'kSupabaseAnonKey'; /// The key for saving the text scale factor. /// @@ -110,14 +101,4 @@ class KVKeys { /// /// The value is a boolean string static const String hasUpgradedSpace = 'hasUpgradedSpace060'; - - /// The key for saving the recent icons - /// - /// The value is a json string of [RecentIcons] - static const String recentIcons = 'kRecentIcons'; - - /// The key for saving compact mode ids for node or databse view - /// - /// The value is a json list of id - static const String compactModeIds = 'compactModeIds'; } diff --git a/frontend/appflowy_flutter/lib/core/frameless_window.dart b/frontend/appflowy_flutter/lib/core/frameless_window.dart index 48f0434833..c322da97aa 100644 --- a/frontend/appflowy_flutter/lib/core/frameless_window.dart +++ b/frontend/appflowy_flutter/lib/core/frameless_window.dart @@ -1,7 +1,16 @@ -import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/window_title_bar.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class CocoaWindowChannel { CocoaWindowChannel._(); @@ -30,9 +39,11 @@ class MoveWindowDetector extends StatefulWidget { const MoveWindowDetector({ super.key, this.child, + this.showTitleBar = false, }); final Widget? child; + final bool showTitleBar; @override MoveWindowDetectorState createState() => MoveWindowDetectorState(); @@ -44,15 +55,26 @@ class MoveWindowDetectorState extends State { @override Widget build(BuildContext context) { - // the frameless window is only supported on macOS - if (!UniversalPlatform.isMacOS) { + if (!Platform.isMacOS && !Platform.isWindows) { return widget.child ?? const SizedBox.shrink(); } - // For the macOS version 15 or higher, we can control the window position by using system APIs - if (ApplicationInfo.macOSMajorVersion != null && - ApplicationInfo.macOSMajorVersion! >= 15) { - 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( @@ -75,4 +97,41 @@ 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: FlowyIconButton( + hoverColor: Colors.transparent, + onPressed: () => context + .read() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.all(4.0), + icon: context.read().state.isMenuCollapsed + ? const FlowySvg(FlowySvgs.show_menu_s) + : const FlowySvg(FlowySvgs.hide_menu_m), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index fd8aa03dfe..94d2074c6b 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -1,25 +1,16 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:open_filex/open_filex.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'package:url_launcher/url_launcher.dart' as launcher; typedef OnFailureCallback = void Function(Uri uri); -/// Launch the uri -/// -/// If the uri is a local file path, it will be opened with the OpenFilex. -/// Otherwise, it will be launched with the url_launcher. -Future afLaunchUri( +Future afLaunchUrl( Uri uri, { BuildContext? context, OnFailureCallback? onFailure, @@ -27,48 +18,21 @@ Future afLaunchUri( String? webOnlyWindowName, bool addingHttpSchemeWhenFailed = false, }) async { - final url = uri.toString(); - final decodedUrl = Uri.decodeComponent(url); - - // check if the uri is the local file path - if (localPathRegex.hasMatch(decodedUrl)) { - return _afLaunchLocalUri( + // try to launch the uri directly + bool result; + try { + result = await launcher.launchUrl( uri, - context: context, - onFailure: onFailure, + mode: mode, + webOnlyWindowName: webOnlyWindowName, ); - } - - // on Linux, add http scheme to the url if it is not present - if (UniversalPlatform.isLinux && !isURL(url, {'require_protocol': true})) { - uri = Uri.parse('https://$url'); - } - - /// opening an incorrect link will cause a system error dialog to pop up on macOS - /// only use [canLaunchUrl] on macOS - /// and there is an known issue with url_launcher on Linux where it fails to launch - /// see https://github.com/flutter/flutter/issues/88463 - bool result = true; - if (UniversalPlatform.isMacOS) { - result = await launcher.canLaunchUrl(uri); - } - - if (result) { - try { - // try to launch the uri directly - result = await launcher.launchUrl( - uri, - mode: mode, - webOnlyWindowName: webOnlyWindowName, - ); - } on PlatformException catch (e) { - Log.error('Failed to open uri: $e'); - return false; - } + } on PlatformException catch (e) { + Log.error('Failed to open uri: $e'); + return false; } // if the uri is not a valid url, try to launch it with http scheme - + final url = uri.toString(); if (addingHttpSchemeWhenFailed && !result && !isURL(url, {'require_protocol': true})) { @@ -90,14 +54,9 @@ Future afLaunchUri( return result; } -/// Launch the url string -/// -/// See [afLaunchUri] for more details. Future afLaunchUrlString( String url, { bool addingHttpSchemeWhenFailed = false, - BuildContext? context, - OnFailureCallback? onFailure, }) async { final Uri uri; try { @@ -108,55 +67,12 @@ Future afLaunchUrlString( } // try to launch the uri directly - return afLaunchUri( + return afLaunchUrl( uri, addingHttpSchemeWhenFailed: addingHttpSchemeWhenFailed, - context: context, - onFailure: onFailure, ); } -/// Launch the local uri -/// -/// See [afLaunchUri] for more details. -Future _afLaunchLocalUri( - Uri uri, { - BuildContext? context, - OnFailureCallback? onFailure, -}) async { - final decodedUrl = Uri.decodeComponent(uri.toString()); - // open the file with the OpenfileX - var result = await OpenFilex.open(decodedUrl); - if (result.type != ResultType.done) { - // For the file cant be opened, fallback to open the folder - final parentFolder = Directory(decodedUrl).parent.path; - result = await OpenFilex.open(parentFolder); - } - // show the toast if the file is not found - final message = switch (result.type) { - ResultType.done => LocaleKeys.openFileMessage_success.tr(), - ResultType.fileNotFound => LocaleKeys.openFileMessage_fileNotFound.tr(), - ResultType.noAppToOpen => LocaleKeys.openFileMessage_noAppToOpenFile.tr(), - ResultType.permissionDenied => - LocaleKeys.openFileMessage_permissionDenied.tr(), - ResultType.error => LocaleKeys.failedToOpenUrl.tr(), - }; - if (context != null && context.mounted) { - showToastNotification( - message: message, - type: result.type == ResultType.done - ? ToastificationType.success - : ToastificationType.error, - ); - } - final openFileSuccess = result.type == ResultType.done; - if (!openFileSuccess && onFailure != null) { - onFailure(uri); - Log.error('Failed to open file: $result.message'); - } - return openFileSuccess; -} - void _errorHandler( Uri uri, { BuildContext? context, diff --git a/frontend/appflowy_flutter/lib/core/network_monitor.dart b/frontend/appflowy_flutter/lib/core/network_monitor.dart index 3d01204921..8959db42b9 100644 --- a/frontend/appflowy_flutter/lib/core/network_monitor.dart +++ b/frontend/appflowy_flutter/lib/core/network_monitor.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:flutter/services.dart'; class NetworkListener { diff --git a/frontend/appflowy_flutter/lib/env/backend_env.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart index eb8a61d037..fa0bf575a3 100644 --- a/frontend/appflowy_flutter/lib/env/backend_env.dart +++ b/frontend/appflowy_flutter/lib/env/backend_env.dart @@ -1,8 +1,6 @@ // ignore_for_file: non_constant_identifier_names -import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:json_annotation/json_annotation.dart'; - part 'backend_env.g.dart'; @JsonSerializable() @@ -15,6 +13,7 @@ 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, }); @@ -29,20 +28,47 @@ 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({ required this.base_url, required this.ws_base_url, required this.gotrue_url, - required this.enable_sync_trace, - required this.base_web_domain, }); factory AppFlowyCloudConfiguration.fromJson(Map json) => @@ -51,14 +77,6 @@ class AppFlowyCloudConfiguration { final String base_url; final String ws_base_url; final String gotrue_url; - final bool enable_sync_trace; - - /// The base domain is used in - /// - /// - Share URL - /// - Publish URL - /// - Copy Link To Block - final String base_web_domain; Map toJson() => _$AppFlowyCloudConfigurationToJson(this); @@ -67,8 +85,6 @@ class AppFlowyCloudConfiguration { base_url: '', ws_base_url: '', gotrue_url: '', - enable_sync_trace: false, - base_web_domain: ShareConstants.defaultBaseWebDomain, ); } diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart index 15f3ada42e..9e8ea0d4f9 100644 --- a/frontend/appflowy_flutter/lib/env/cloud_env.dart +++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart @@ -2,7 +2,6 @@ import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/env/backend_env.dart'; import 'package:appflowy/env/env.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/log.dart'; @@ -15,17 +14,16 @@ import 'package:appflowy_backend/log.dart'; /// [ty] - The type of cloud to be set. It must be one of the values from /// [AuthenticatorType] enum. The corresponding integer value of the enum is stored: /// - `CloudType.local` is stored as "0". +/// - `CloudType.supabase` is stored as "1". /// - `CloudType.appflowyCloud` is stored as "2". -/// -/// The gap between [AuthenticatorType.local] and [AuthenticatorType.appflowyCloud] is -/// due to previously supporting Supabase, this has been deprecated since and removed. -/// To not cause conflicts with older clients, we keep the gap. -/// Future _setAuthenticatorType(AuthenticatorType ty) async { switch (ty) { 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; @@ -65,6 +63,8 @@ Future getAuthenticatorType() async { switch (value ?? "0") { case "0": return AuthenticatorType.local; + case "1": + return AuthenticatorType.supabase; case "2": return AuthenticatorType.appflowyCloud; case "3": @@ -89,10 +89,14 @@ Future getAuthenticatorType() async { /// A boolean value indicating whether authentication is enabled. It returns /// `true` if the application is in release or develop mode, and the cloud type /// is not set to `CloudType.local`. Additionally, it checks if either the -/// AppFlowy Cloud configuration is valid. +/// AppFlowy Cloud or Supabase configuration is valid. /// 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; } @@ -100,8 +104,17 @@ bool get isAuthEnabled { return false; } -bool get isLocalAuthEnabled { - return currentCloudType().isLocal; +/// 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. @@ -111,6 +124,7 @@ bool get isAppFlowyCloudEnabled { enum AuthenticatorType { local, + supabase, appflowyCloud, appflowyCloudSelfHost, // The 'appflowyCloudDevelop' type is used for develop purposes only. @@ -123,10 +137,14 @@ 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: @@ -140,6 +158,8 @@ enum AuthenticatorType { switch (value) { case 0: return AuthenticatorType.local; + case 1: + return AuthenticatorType.supabase; case 2: return AuthenticatorType.appflowyCloud; case 3: @@ -160,13 +180,6 @@ Future _setAppFlowyCloudUrl(String? url) async { await getIt().set(KVKeys.kAppflowyCloudBaseURL, url ?? ''); } -Future useBaseWebDomain(String? url) async { - await getIt().set( - KVKeys.kAppFlowyBaseShareDomain, - url ?? ShareConstants.defaultBaseWebDomain, - ); -} - Future useSelfHostedAppFlowyCloudWithURL(String url) async { await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost); await _setAppFlowyCloudUrl(url); @@ -184,15 +197,25 @@ Future useLocalServer() async { await _setAuthenticatorType(AuthenticatorType.local); } -// Use getIt() to get the shared environment. +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; @@ -206,6 +229,10 @@ 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 @@ -217,6 +244,7 @@ class AppFlowyCloudSharedEnv { return AppFlowyCloudSharedEnv( authenticatorType: authenticatorType, appflowyCloudConfig: appflowyCloudConfig, + supabaseConfig: supabaseCloudConfig, ); } else { // Using the cloud settings from the .env file. @@ -224,13 +252,12 @@ class AppFlowyCloudSharedEnv { base_url: Env.afCloudUrl, ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl), gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl), - enable_sync_trace: false, - base_web_domain: Env.baseWebDomain, ); return AppFlowyCloudSharedEnv( authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType), appflowyCloudConfig: appflowyCloudConfig, + supabaseConfig: SupabaseConfiguration.defaultConfig(), ); } } @@ -238,7 +265,8 @@ class AppFlowyCloudSharedEnv { @override String toString() { return 'authenticator: $_authenticatorType\n' - 'appflowy: ${appflowyCloudConfig.toJson()}\n'; + 'appflowy: ${appflowyCloudConfig.toJson()}\n' + 'supabase: ${supabaseConfig.toJson()})\n'; } } @@ -246,29 +274,21 @@ Future configurationFromUri( Uri baseUri, String baseUrl, AuthenticatorType authenticatorType, - String baseShareDomain, ) async { // In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports. // This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server // in the development environment. - // If you modify following code, please update the corresponding documentation in the appflowy billing. if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) { return AppFlowyCloudConfiguration( base_url: "$baseUrl:8000", ws_base_url: "ws://${baseUri.host}:8000/ws/v1", gotrue_url: "$baseUrl:9999", - enable_sync_trace: true, - base_web_domain: ShareConstants.testBaseWebDomain, ); } else { return AppFlowyCloudConfiguration( base_url: baseUrl, ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl), gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl), - enable_sync_trace: await getSyncLogEnabled(), - base_web_domain: authenticatorType == AuthenticatorType.appflowyCloud - ? ShareConstants.defaultBaseWebDomain - : baseShareDomain, ); } } @@ -277,16 +297,10 @@ Future getAppFlowyCloudConfig( AuthenticatorType authenticatorType, ) async { final baseURL = await getAppFlowyCloudUrl(); - final baseShareDomain = await getAppFlowyShareDomain(); try { final uri = Uri.parse(baseURL); - return await configurationFromUri( - uri, - baseURL, - authenticatorType, - baseShareDomain, - ); + return await configurationFromUri(uri, baseURL, authenticatorType); } catch (e) { Log.error("Failed to parse AppFlowy Cloud URL: $e"); return AppFlowyCloudConfiguration.defaultConfig(); @@ -299,30 +313,6 @@ Future getAppFlowyCloudUrl() async { return result ?? kAppflowyCloudUrl; } -Future getAppFlowyShareDomain() async { - final result = - await getIt().get(KVKeys.kAppFlowyBaseShareDomain); - return result ?? ShareConstants.defaultBaseWebDomain; -} - -Future getSyncLogEnabled() async { - final result = - await getIt().get(KVKeys.kAppFlowyEnableSyncTrace); - - if (result == null) { - return false; - } - - return result.toLowerCase() == "true"; -} - -Future setSyncLogEnabled(bool enable) async { - await getIt().set( - KVKeys.kAppFlowyEnableSyncTrace, - enable.toString().toLowerCase(), - ); -} - Future _getAppFlowyCloudWSUrl(String baseURL) async { try { final uri = Uri.parse(baseURL); @@ -342,3 +332,44 @@ Future _getAppFlowyCloudWSUrl(String baseURL) async { Future _getAppFlowyCloudGotrueUrl(String baseURL) async { return "$baseURL/gotrue"; } + +Future setSupabaseServer( + String? url, + String? anonKey, +) async { + assert( + (url != null && anonKey != null) || (url == null && anonKey == null), + "Either both Supabase URL and anon key must be set, or both should be unset", + ); + + if (url == null) { + await getIt().remove(KVKeys.kSupabaseURL); + } else { + await getIt().set(KVKeys.kSupabaseURL, url); + } + + if (anonKey == null) { + await getIt().remove(KVKeys.kSupabaseAnonKey); + } else { + 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 18434f9aa6..a0786e4d5b 100644 --- a/frontend/appflowy_flutter/lib/env/env.dart +++ b/frontend/appflowy_flutter/lib/env/env.dart @@ -1,6 +1,5 @@ // lib/env/env.dart import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; import 'package:envied/envied.dart'; part 'env.g.dart'; @@ -30,25 +29,4 @@ abstract class Env { defaultValue: '', ) static const String afCloudUrl = _Env.afCloudUrl; - - @EnviedField( - obfuscate: false, - varName: 'INTERNAL_BUILD', - defaultValue: '', - ) - static const String internalBuild = _Env.internalBuild; - - @EnviedField( - obfuscate: false, - varName: 'SENTRY_DSN', - defaultValue: '', - ) - static const String sentryDsn = _Env.sentryDsn; - - @EnviedField( - obfuscate: false, - varName: 'BASE_WEB_DOMAIN', - defaultValue: ShareConstants.defaultBaseWebDomain, - ) - static const String baseWebDomain = _Env.baseWebDomain; } diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index 157be012b1..9d18bb14f7 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -16,8 +16,6 @@ const double _kMinimumWidth = 112.0; const double _kDefaultHorizontalPadding = 12.0; -typedef CompareFunction = bool Function(T? left, T? right); - // Navigation shortcuts to move the selected menu items up or down. final Map _kMenuTraversalShortcuts = { @@ -88,7 +86,6 @@ class AFDropdownMenu extends StatefulWidget { this.requestFocusOnTap, this.expandedInsets, this.searchCallback, - this.selectOptionCompare, required this.dropdownMenuEntries, }); @@ -270,11 +267,6 @@ class AFDropdownMenu extends StatefulWidget { /// which contains the contents of the text input field. final SearchCallback? searchCallback; - /// Defines the compare function for the menu items. - /// - /// Defaults to null. If this is null, the menu items will be sorted by the label. - final CompareFunction? selectOptionCompare; - @override State> createState() => _AFDropdownMenuState(); } @@ -309,16 +301,7 @@ class _AFDropdownMenuState extends State> { filteredEntries.any((DropdownMenuEntry entry) => entry.enabled); final int index = filteredEntries.indexWhere( - (DropdownMenuEntry entry) { - if (widget.selectOptionCompare != null) { - return widget.selectOptionCompare!( - entry.value, - widget.initialSelection, - ); - } else { - return entry.value == widget.initialSelection; - } - }, + (DropdownMenuEntry entry) => entry.value == widget.initialSelection, ); if (index != -1) { _textEditingController.value = TextEditingValue( @@ -408,23 +391,17 @@ class _AFDropdownMenuState extends State> { ); } - // Remove the code here, it will throw a FlutterError - // Unless we upgrade to Flutter 3.24 https://github.com/flutter/flutter/issues/146764 void scrollToHighlight() { - // WidgetsBinding.instance.addPostFrameCallback( - // (_) { - // // try { - // final BuildContext? highlightContext = - // buttonItemKeys[currentHighlight!].currentContext; - // if (highlightContext != null) { - // Scrollable.ensureVisible(highlightContext); - // } - // } catch (_) { - // return; - // } - // }, - // debugLabel: 'DropdownMenu.scrollToHighlight', - // ); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final BuildContext? highlightContext = + buttonItemKeys[currentHighlight!].currentContext; + if (highlightContext != null) { + Scrollable.ensureVisible(highlightContext); + } + }, + debugLabel: 'DropdownMenu.scrollToHighlight', + ); } double? getWidth(GlobalKey key) { @@ -519,11 +496,11 @@ class _AFDropdownMenuState extends State> { // Simulate the focused state because the text field should always be focused // during traversal. If the menu item has a custom foreground color, the "focused" - // color will also change to foregroundColor.withValues(alpha: 0.12). + // color will also change to foregroundColor.withOpacity(0.12). effectiveStyle = entry.enabled && i == focusedIndex ? effectiveStyle.copyWith( backgroundColor: WidgetStatePropertyAll( - focusedBackgroundColor.withValues(alpha: 0.12), + focusedBackgroundColor.withOpacity(0.12), ), ) : effectiveStyle; diff --git a/frontend/appflowy_flutter/lib/main.dart b/frontend/appflowy_flutter/lib/main.dart index 9117acfd1b..9f140489c4 100644 --- a/frontend/appflowy_flutter/lib/main.dart +++ b/frontend/appflowy_flutter/lib/main.dart @@ -3,9 +3,7 @@ import 'package:scaled_app/scaled_app.dart'; import 'startup/startup.dart'; Future main() async { - ScaledWidgetsFlutterBinding.ensureInitialized( - scaleFactor: (_) => 1.0, - ); + ScaledWidgetsFlutterBinding.ensureInitialized(scaleFactor: (_) => 1.0); await runAppFlowy(); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index aa02495a49..8b9f1e70ff 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -2,49 +2,27 @@ 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, - bool addInRecent = true, - bool showMoreButton = true, - String? fixedTitle, - String? blockId, - List? tabs, - }) async { + Future pushView(ViewPB view, [Map? arguments]) 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; - } - if (blockId != null) { - queryParameters[MobileDocumentScreen.viewBlockId] = blockId; - } - } - if (tabs != null) { - queryParameters[MobileDocumentScreen.viewSelectTabs] = tabs.join('-'); - } final uri = Uri( path: view.routeName, - queryParameters: queryParameters, + queryParameters: view.queryParameters(arguments), ).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 deleted file mode 100644 index 25fee58a64..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart +++ /dev/null @@ -1,219 +0,0 @@ -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.nameOrDefault, - view: view, - reminderContent: node.delta?.toPlainText() ?? '', - nodes: [node], - status: NotificationReminderStatus.loaded, - blockId: reminder.meta[ReminderMetaKeys.blockId], - ), - ); - } - } else if (layout.isDatabaseView) { - emit( - NotificationReminderState( - createdAt: createdAt, - pageTitle: view.nameOrDefault, - 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, - String? blockId, - 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 650fbf1d85..52552fce3b 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 @@ -6,6 +6,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -22,16 +23,11 @@ class DocumentPageStyleBloc await event.when( initial: () async { try { - if (view.id.isEmpty) { - return; - } - Map layoutObject = {}; - final data = await ViewBackendService.getView(view.id); - data.onSuccess((s) { - if (s.extra.isNotEmpty) { - layoutObject = jsonDecode(s.extra); - } - }); + final layoutObject = + await ViewBackendService.getView(view.id).fold( + (s) => jsonDecode(s.extra), + (f) => {}, + ); final fontLayout = _getSelectedFontLayout(layoutObject); final lineHeightLayout = _getSelectedLineHeightLayout( layoutObject, @@ -150,7 +146,7 @@ class DocumentPageStyleBloc ) { double padding = switch (fontLayout) { PageStyleFontLayout.small => 1.0, - PageStyleFontLayout.normal => 1.0, + PageStyleFontLayout.normal => 2.0, PageStyleFontLayout.large => 4.0, }; switch (lineHeightLayout) { @@ -166,16 +162,6 @@ 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(); @@ -442,15 +428,4 @@ class PageStyleCover { bool get isCustomImage => type == PageStyleCoverImageType.customImage; bool get isUnsplashImage => type == PageStyleCoverImageType.unsplashImage; bool get isLocalImage => type == PageStyleCoverImageType.localImage; - - @override - bool operator ==(Object other) { - if (other is! PageStyleCover) { - return false; - } - return type == other.type && value == other.value; - } - - @override - int get hashCode => Object.hash(type, value); } 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 2d89b3b388..547c81f00b 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,5 +1,7 @@ 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'; @@ -7,8 +9,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - part 'recent_view_bloc.freezed.dart'; class RecentViewBloc extends Bloc { @@ -41,7 +41,7 @@ class RecentViewBloc extends Bloc { add( RecentViewEvent.updateNameOrIcon( view.name, - view.icon.toEmojiIconData(), + view.icon.value, ), ); @@ -63,7 +63,7 @@ class RecentViewBloc extends Bloc { emit( state.copyWith( name: view.name, - icon: view.icon.toEmojiIconData(), + icon: view.icon.value, ), ); } @@ -74,7 +74,7 @@ class RecentViewBloc extends Bloc { emit( state.copyWith( name: view.name, - icon: view.icon.toEmojiIconData(), + icon: view.icon.value, coverTypeV2: cover.type, coverValue: cover.value, ), @@ -84,7 +84,7 @@ class RecentViewBloc extends Bloc { emit( state.copyWith( name: view.name, - icon: view.icon.toEmojiIconData(), + icon: view.icon.value, coverTypeV1: coverTypeV1, coverValue: coverValue, ), @@ -113,6 +113,7 @@ class RecentViewBloc extends Bloc { ); } + final _service = DocumentService(); final ViewPB view; final DocumentListener _documentListener; final ViewListener _viewListener; @@ -123,6 +124,16 @@ 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); } @@ -137,17 +148,14 @@ class RecentViewBloc extends Bloc { @freezed class RecentViewEvent with _$RecentViewEvent { const factory RecentViewEvent.initial() = Initial; - const factory RecentViewEvent.updateCover( - CoverType coverTypeV1, - // for the version under 0.5.5, including 0.5.5 + CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5 PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5 String? coverValue, ) = UpdateCover; - const factory RecentViewEvent.updateNameOrIcon( String name, - EmojiIconData icon, + String icon, ) = UpdateNameOrIcon; } @@ -155,12 +163,12 @@ class RecentViewEvent with _$RecentViewEvent { class RecentViewState with _$RecentViewState { const factory RecentViewState({ required String name, - required EmojiIconData icon, + required String icon, @Default(CoverType.none) CoverType coverTypeV1, PageStyleCoverImageType? coverTypeV2, @Default(null) String? coverValue, }) = _RecentViewState; factory RecentViewState.initial() => - RecentViewState(name: '', icon: EmojiIconData.none()); + const RecentViewState(name: '', icon: ''); } 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 0527316860..7edec07cc1 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,20 +12,21 @@ class UserProfileBloc extends Bloc { UserProfileBloc() : super(const _Initial()) { on((event, emit) async { await event.when( - started: () async => _initialize(emit), + started: () async => _initalize(emit), ); }); } - Future _initialize(Emitter emit) async { + Future _initalize(Emitter emit) async { emit(const UserProfileState.loading()); - final latestOrFailure = + + final workspaceOrFailure = await FolderEventGetCurrentWorkspaceSetting().send(); final userOrFailure = await getIt().getUser(); - final latest = latestOrFailure.fold( - (latestPB) => latestPB, + final workspaceSetting = workspaceOrFailure.fold( + (workspaceSettingPB) => workspaceSettingPB, (error) => null, ); @@ -34,13 +35,13 @@ class UserProfileBloc extends Bloc { (error) => null, ); - if (latest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return emit(const UserProfileState.workspaceFailure()); } emit( UserProfileState.success( - workspaceSettings: latest, + workspaceSettings: workspaceSetting, userProfile: userProfile, ), ); @@ -58,7 +59,7 @@ class UserProfileState with _$UserProfileState { const factory UserProfileState.loading() = _Loading; const factory UserProfileState.workspaceFailure() = _WorkspaceFailure; const factory UserProfileState.success({ - required WorkspaceLatestPB workspaceSettings, + required WorkspaceSettingPB workspaceSettings, required UserProfilePB userProfile, }) = _Success; } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart deleted file mode 100644 index d0e973ae64..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy/shared/feedback_gesture_detector.dart'; -import 'package:flutter/material.dart'; - -class AnimatedGestureDetector extends StatefulWidget { - const AnimatedGestureDetector({ - super.key, - this.scaleFactor = 0.98, - this.feedback = true, - this.duration = const Duration(milliseconds: 100), - this.alignment = Alignment.center, - this.behavior = HitTestBehavior.opaque, - 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 396ecd6bb8..335f1af489 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 AppBarImmersiveBackButton(onTap: onTap); + return AppBarBackButton(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 72142d446b..b59c1e68cc 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,31 +26,6 @@ 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 318b06394a..9e21a002b1 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 @@ -1,28 +1,18 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; 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/document/application/prelude.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -37,10 +27,6 @@ class MobileViewPage extends StatefulWidget { required this.viewLayout, this.title, this.arguments, - this.fixedTitle, - this.showMoreButton = true, - this.blockId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); /// view id @@ -48,12 +34,6 @@ class MobileViewPage extends StatefulWidget { final ViewLayoutPB viewLayout; final String? title; final Map? arguments; - final bool showMoreButton; - final String? blockId; - final List tabs; - - // only used in row page - final String? fixedTitle; @override State createState() => _MobileViewPageState(); @@ -66,22 +46,10 @@ 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(); - - // there's no need to remove the listener, because the observer will be disposed when the widget is unmounted. - // inside the observer, the listener will be removed automatically. - // _scrollNotificationObserver?.removeListener(_onScrollNotification); _scrollNotificationObserver = null; - super.dispose(); } @@ -96,7 +64,7 @@ class _MobileViewPageState extends State { final body = _buildBody(context, state); if (view == null) { - return SizedBox.shrink(); + return _buildApp(context, null, body); } return MultiBlocProvider( @@ -110,28 +78,14 @@ class _MobileViewPageState extends State { ViewBloc(view: view)..add(const ViewEvent.initial()), ), BlocProvider.value( - value: getIt(), + value: getIt() + ..add(const ReminderEvent.started()), ), - BlocProvider( - create: (_) => - ShareBloc(view: view)..add(const ShareEvent.initial()), - ), - if (state.userProfilePB != null) - BlocProvider( - create: (_) => - UserWorkspaceBloc(userProfile: state.userProfilePB!) - ..add(const UserWorkspaceEvent.initial()), - ), if (view.layout.isDocumentView) BlocProvider( create: (_) => DocumentPageStyleBloc(view: view) ..add(const DocumentPageStyleEvent.initial()), ), - if (view.layout.isDocumentView || view.layout.isDatabaseView) - BlocProvider( - create: (_) => ViewLockStatusBloc(view: view) - ..add(const ViewLockStatusEvent.initial()), - ), ], child: Builder( builder: (context) { @@ -162,7 +116,6 @@ class _MobileViewPageState extends State { title: title, appBarOpacity: _appBarOpacity, actions: actions, - view: view, ) : FlowyAppBar(title: title, actions: actions); final body = isDocument @@ -172,7 +125,7 @@ class _MobileViewPageState extends State { return child; }, ) - : SafeArea(child: child); + : child; return Scaffold( extendBodyBehindAppBar: isDocument, appBar: appBar, @@ -204,11 +157,6 @@ class _MobileViewPageState extends State { return plugin.widgetBuilder.buildWidget( shrinkWrap: false, context: PluginContext(userProfile: state.userProfilePB), - data: { - MobileDocumentScreen.viewFixedTitle: widget.fixedTitle, - MobileDocumentScreen.viewBlockId: widget.blockId, - MobileDocumentScreen.viewSelectTabs: widget.tabs, - }, ); }, (error) { @@ -233,8 +181,6 @@ class _MobileViewPageState extends State { final isImmersiveMode = context.read().state.isImmersiveMode; - final isLocked = - context.read()?.state.isLocked ?? false; final actions = []; if (FeatureFlag.syncDocument.isOn) { @@ -253,167 +199,48 @@ class _MobileViewPageState extends State { } } - if (view.layout.isDocumentView && !isLocked) { + if (view.layout.isDocumentView) { actions.addAll([ MobileViewPageLayoutButton( view: view, isImmersiveMode: isImmersiveMode, appBarOpacity: _appBarOpacity, - tabs: widget.tabs, ), ]); } - if (widget.showMoreButton) { - actions.addAll([ - MobileViewPageMoreButton( - view: view, - isImmersiveMode: isImmersiveMode, - appBarOpacity: _appBarOpacity, - ), - ]); - } else { - actions.addAll([ - const HSpace(18.0), - ]); - } + actions.addAll([ + MobileViewPageMoreButton( + view: view, + isImmersiveMode: isImmersiveMode, + appBarOpacity: _appBarOpacity, + ), + ]); return actions; } Widget _buildTitle(BuildContext context, ViewPB? view) { - final icon = view?.icon; - return ValueListenableBuilder( - valueListenable: _appBarOpacity, - builder: (_, value, child) { - if (value < 0.99) { - return Padding( - padding: const EdgeInsets.only(left: 6.0), - child: _buildLockStatus(context, view), - ); - } - - final name = - widget.fixedTitle ?? view?.nameOrDefault ?? widget.title ?? ''; - - return Opacity( - opacity: value, - child: Row( - children: [ - if (icon != null && icon.value.isNotEmpty) ...[ - RawEmojiIconWidget( - emoji: icon.toEmojiIconData(), - emojiSize: 15, - ), - const HSpace(4), - ], - Flexible( - child: FlowyText.medium( - name, - fontSize: 15.0, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 18.0, - ), - ), - const HSpace(4.0), - _buildLockStatusIcon(context, view), - ], + final icon = view?.icon.value; + 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, + ), ), - ); - }, - ); - } - - Widget _buildLockStatus(BuildContext context, ViewPB? view) { - if (view == null || view.layout == ViewLayoutPB.Chat) { - return const SizedBox.shrink(); - } - - return BlocConsumer( - listenWhen: (previous, current) => - previous.isLoadingLockStatus == current.isLoadingLockStatus && - current.isLoadingLockStatus == false, - listener: (context, state) { - if (state.isLocked) { - showToastNotification( - message: LocaleKeys.lockPage_pageLockedToast.tr(), - ); - - EditorNotification.exitEditing().post(); - } - }, - builder: (context, state) { - if (state.isLocked) { - return LockedPageStatus(); - } else if (!state.isLocked && state.lockCounter > 0) { - return ReLockedPageStatus(); - } - return const SizedBox.shrink(); - }, - ); - } - - Widget _buildLockStatusIcon(BuildContext context, ViewPB? view) { - if (view == null || view.layout == ViewLayoutPB.Chat) { - return const SizedBox.shrink(); - } - - return BlocConsumer( - listenWhen: (previous, current) => - previous.isLoadingLockStatus == current.isLoadingLockStatus && - current.isLoadingLockStatus == false, - listener: (context, state) { - if (state.isLocked) { - showToastNotification( - message: LocaleKeys.lockPage_pageLockedToast.tr(), - ); - } - }, - builder: (context, state) { - if (state.isLocked) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - context.read().add( - const ViewLockStatusEvent.unlock(), - ); - }, - child: Padding( - padding: const EdgeInsets.only( - top: 4.0, - right: 8, - bottom: 4.0, - ), - child: FlowySvg( - FlowySvgs.lock_page_fill_s, - blendMode: null, - ), - ), - ); - } else if (!state.isLocked && state.lockCounter > 0) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - context.read().add( - const ViewLockStatusEvent.lock(), - ); - }, - child: Padding( - padding: const EdgeInsets.only( - top: 4.0, - right: 8, - bottom: 4.0, - ), - child: FlowySvg( - FlowySvgs.unlock_page_s, - color: Color(0xFF8F959E), - blendMode: null, - ), - ), - ); - } - return const SizedBox.shrink(); - }, + Expanded( + child: FlowyText.medium( + view?.name ?? widget.title ?? '', + fontSize: 15.0, + overflow: TextOverflow.ellipsis, + ), + ), + ], ); } 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 e0c3140ea9..497f769354 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 @@ -9,14 +9,12 @@ class TypeOptionMenuItemValue { required this.text, required this.backgroundColor, required this.onTap, - this.iconPadding, }); final T value; final FlowySvgData icon; final String text; final Color backgroundColor; - final EdgeInsets? iconPadding; final void Function(BuildContext context, T value) onTap; } @@ -24,7 +22,7 @@ class TypeOptionMenu extends StatelessWidget { const TypeOptionMenu({ super.key, required this.values, - this.width = 98, + this.width = 94, this.iconWidth = 72, this.scaleFactor = 1.0, this.maxAxisSpacing = 18, @@ -41,18 +39,17 @@ class TypeOptionMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return TypeOptionGridView( + return _GridView( crossAxisCount: crossAxisCount, mainAxisSpacing: maxAxisSpacing * scaleFactor, itemWidth: width * scaleFactor, children: values .map( - (value) => TypeOptionMenuItem( + (value) => _TypeOptionMenuItem( value: value, width: width, iconWidth: iconWidth, scaleFactor: scaleFactor, - iconPadding: value.iconPadding, ), ) .toList(), @@ -60,21 +57,18 @@ class TypeOptionMenu extends StatelessWidget { } } -class TypeOptionMenuItem extends StatelessWidget { - const TypeOptionMenuItem({ - super.key, +class _TypeOptionMenuItem extends StatelessWidget { + const _TypeOptionMenuItem({ required this.value, this.width = 94, this.iconWidth = 72, this.scaleFactor = 1.0, - this.iconPadding, }); final TypeOptionMenuItemValue value; final double iconWidth; final double width; final double scaleFactor; - final EdgeInsets? iconPadding; double get scaledIconWidth => iconWidth * scaleFactor; double get scaledWidth => width * scaleFactor; @@ -94,8 +88,7 @@ class TypeOptionMenuItem extends StatelessWidget { borderRadius: BorderRadius.circular(24 * scaleFactor), ), ), - padding: EdgeInsets.all(21 * scaleFactor) + - (iconPadding ?? EdgeInsets.zero), + padding: EdgeInsets.all(21 * scaleFactor), child: FlowySvg( value.icon, ), @@ -109,7 +102,6 @@ class TypeOptionMenuItem extends StatelessWidget { value.text, fontSize: 14.0, maxLines: 2, - lineHeight: 1.0, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), @@ -120,9 +112,8 @@ class TypeOptionMenuItem extends StatelessWidget { } } -class TypeOptionGridView extends StatelessWidget { - const TypeOptionGridView({ - super.key, +class _GridView extends StatelessWidget { + const _GridView({ required this.children, required this.crossAxisCount, required this.mainAxisSpacing, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index a91fbf577b..1301719c41 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -8,11 +8,8 @@ import 'package:appflowy/mobile/presentation/base/view_page/more_bottom_sheet.da import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -29,13 +26,12 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget required this.appBarOpacity, required this.title, required this.actions, - required this.view, }); final ValueListenable appBarOpacity; final Widget title; final List actions; - final ViewPB? view; + @override final Size preferredSize; @@ -45,9 +41,9 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget valueListenable: appBarOpacity, builder: (_, opacity, __) => FlowyAppBar( backgroundColor: - AppBarTheme.of(context).backgroundColor?.withValues(alpha: opacity), + AppBarTheme.of(context).backgroundColor?.withOpacity(opacity), showDivider: false, - title: _buildTitle(context, opacity: opacity), + title: Opacity(opacity: opacity >= 0.99 ? 1.0 : 0, child: title), leadingWidth: 44, leading: Padding( padding: const EdgeInsets.only(top: 4.0, bottom: 4.0, left: 12.0), @@ -58,13 +54,6 @@ class MobileViewPageImmersiveAppBar extends StatelessWidget ); } - Widget _buildTitle( - BuildContext context, { - required double opacity, - }) { - return title; - } - Widget _buildAppBarBackButton(BuildContext context) { return AppBarButton( padding: EdgeInsets.zero, @@ -109,14 +98,6 @@ class MobileViewPageMoreButton extends StatelessWidget { providers: [ BlocProvider.value(value: context.read()), BlocProvider.value(value: context.read()), - BlocProvider.value(value: context.read()), - BlocProvider.value(value: context.read()), - BlocProvider( - create: (context) => ViewLockStatusBloc(view: view) - ..add( - ViewLockStatusEvent.initial(), - ), - ), ], child: MobileViewPageMoreBottomSheet(view: view), ), @@ -139,11 +120,9 @@ class MobileViewPageLayoutButton extends StatelessWidget { required this.view, required this.isImmersiveMode, required this.appBarOpacity, - required this.tabs, }); final ViewPB view; - final List tabs; final bool isImmersiveMode; final ValueListenable appBarOpacity; @@ -174,7 +153,6 @@ class MobileViewPageLayoutButton extends StatelessWidget { ], child: PageStyleBottomSheet( view: context.read().state.view, - tabs: tabs, ), ), ); @@ -239,7 +217,7 @@ class _ImmersiveAppBarButton extends StatelessWidget { child = DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(dimension / 2.0), - color: Colors.black.withValues(alpha: 0.2), + color: Colors.black.withOpacity(0.2), ), child: child, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index be134e0a92..6394ca9647 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -1,31 +1,11 @@ -import 'dart:async'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/plugins/shared/share/publish_name_generator.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/shared/error_code/error_code_map.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:url_launcher/url_launcher.dart'; class MobileViewPageMoreBottomSheet extends StatelessWidget { const MobileViewPageMoreBottomSheet({super.key, required this.view}); @@ -34,318 +14,55 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) => _showToast(context, state), - child: BlocListener( - listener: (context, state) { - if (state.successOrFailure.isSuccess && state.isDeleted) { - context.go('/home'); - } - }, - child: ViewPageBottomSheet( - view: view, - onAction: (action, {arguments}) async => - _onAction(context, action, arguments), - onRename: (name) { - _onRename(context, name); + return ViewPageBottomSheet( + view: view, + onAction: (action) { + switch (action) { + case MobileViewBottomSheetBodyAction.duplicate: context.pop(); - }, - ), - ), - ); - } + context.read().add(const ViewEvent.duplicate()); + // show toast + break; + case MobileViewBottomSheetBodyAction.share: + // unimplemented + context.pop(); + break; + case MobileViewBottomSheetBodyAction.delete: + // pop to home page + context + ..pop() + ..pop(); + context.read().add(const ViewEvent.delete()); + break; + case MobileViewBottomSheetBodyAction.addToFavorites: + case MobileViewBottomSheetBodyAction.removeFromFavorites: + context.pop(); + context.read().add(FavoriteEvent.toggle(view)); - Future _onAction( - BuildContext context, - MobileViewBottomSheetBodyAction action, - Map? arguments, - ) async { - switch (action) { - case MobileViewBottomSheetBodyAction.duplicate: - _duplicate(context); - break; - case MobileViewBottomSheetBodyAction.delete: - context.read().add(const ViewEvent.delete()); - Navigator.of(context).pop(); - break; - case MobileViewBottomSheetBodyAction.addToFavorites: - _addFavorite(context); - break; - case MobileViewBottomSheetBodyAction.removeFromFavorites: - _removeFavorite(context); - break; - case MobileViewBottomSheetBodyAction.undo: - EditorNotification.undo().post(); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.redo: - EditorNotification.redo().post(); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.helpCenter: - // unimplemented - context.pop(); - break; - case MobileViewBottomSheetBodyAction.publish: - await _publish(context); - if (context.mounted) { - context.pop(); + break; + case MobileViewBottomSheetBodyAction.undo: + EditorNotification.undo().post(); + context.pop(); + break; + case MobileViewBottomSheetBodyAction.redo: + EditorNotification.redo().post(); + context.pop(); + break; + case MobileViewBottomSheetBodyAction.helpCenter: + // unimplemented + context.pop(); + break; + case MobileViewBottomSheetBodyAction.rename: + // no need to implement, rename is handled by the onRename callback. + throw UnimplementedError(); + } + }, + onRename: (name) { + if (name != view.name) { + context.read().add(ViewEvent.rename(name)); } - break; - case MobileViewBottomSheetBodyAction.unpublish: - _unpublish(context); context.pop(); - break; - case MobileViewBottomSheetBodyAction.copyPublishLink: - _copyPublishLink(context); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.visitSite: - _visitPublishedSite(context); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.copyShareLink: - _copyShareLink(context); - context.pop(); - break; - case MobileViewBottomSheetBodyAction.updatePathName: - _updatePathName(context); - case MobileViewBottomSheetBodyAction.lockPage: - final isLocked = - arguments?[MobileViewBottomSheetBodyActionArguments.isLockedKey] ?? - false; - await _lockPage(context, isLocked: isLocked); - // context.pop(); - break; - case MobileViewBottomSheetBodyAction.rename: - // no need to implement, rename is handled by the onRename callback. - throw UnimplementedError(); - } - } - - Future _lockPage( - BuildContext context, { - required bool isLocked, - }) async { - if (isLocked) { - context.read().add(const ViewLockStatusEvent.lock()); - } else { - context - .read() - .add(const ViewLockStatusEvent.unlock()); - } - } - - Future _publish(BuildContext context) async { - final id = context.read().view.id; - final lastPublishName = context.read().state.pathName; - final publishName = lastPublishName.orDefault( - await generatePublishName( - id, - view.name, - ), - ); - if (context.mounted) { - context.read().add( - ShareEvent.publish( - '', - publishName, - [view.id], - ), - ); - } - } - - void _duplicate(BuildContext context) { - context.read().add(const ViewEvent.duplicate()); - context.pop(); - - showToastNotification( - message: LocaleKeys.button_duplicateSuccessfully.tr(), - ); - } - - void _addFavorite(BuildContext context) { - _toggleFavorite(context); - - showToastNotification( - message: LocaleKeys.button_favoriteSuccessfully.tr(), - ); - } - - void _removeFavorite(BuildContext context) { - _toggleFavorite(context); - - showToastNotification( - message: LocaleKeys.button_unfavoriteSuccessfully.tr(), - ); - } - - void _toggleFavorite(BuildContext context) { - context.read().add(FavoriteEvent.toggle(view)); - context.pop(); - } - - void _unpublish(BuildContext context) { - context.read().add(const ShareEvent.unPublish()); - } - - void _copyPublishLink(BuildContext context) { - final url = context.read().state.url; - if (url.isNotEmpty) { - unawaited( - getIt().setData( - ClipboardServiceData(plainText: url), - ), - ); - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } - } - - void _visitPublishedSite(BuildContext context) { - final url = context.read().state.url; - if (url.isNotEmpty) { - unawaited( - afLaunchUri( - Uri.parse(url), - mode: LaunchMode.externalApplication, - ), - ); - } - } - - void _copyShareLink(BuildContext context) { - final workspaceId = context.read().state.workspaceId; - final viewId = context.read().state.viewId; - final url = ShareConstants.buildShareUrl( - workspaceId: workspaceId, - viewId: viewId, - ); - if (url.isNotEmpty) { - unawaited( - getIt().setData( - ClipboardServiceData(plainText: url), - ), - ); - showToastNotification( - message: LocaleKeys.shareAction_copyLinkSuccess.tr(), - ); - } else { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkToBlockFailed.tr(), - type: ToastificationType.error, - ); - } - } - - void _onRename(BuildContext context, String name) { - if (name != view.name) { - context.read().add(ViewEvent.rename(name)); - } - } - - void _updatePathName(BuildContext context) async { - final shareBloc = context.read(); - final pathName = shareBloc.state.pathName; - await showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.shareAction_updatePathName.tr(), - showCloseButton: true, - showDragHandle: true, - showDivider: false, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) { - FlowyResult? previousUpdatePathNameResult; - return EditWorkspaceNameBottomSheet( - type: EditWorkspaceNameType.edit, - workspaceName: pathName, - hintText: '', - validator: (value) => null, - validatorBuilder: (context) { - return BlocProvider.value( - value: shareBloc, - child: BlocBuilder( - builder: (context, state) { - final updatePathNameResult = state.updatePathNameResult; - - if (updatePathNameResult == null && - previousUpdatePathNameResult == null) { - return const SizedBox.shrink(); - } - - if (updatePathNameResult != null) { - previousUpdatePathNameResult = updatePathNameResult; - } - - final widget = previousUpdatePathNameResult?.fold( - (value) => const SizedBox.shrink(), - (error) => FlowyText( - error.code.publishErrorMessage.orDefault( - LocaleKeys.settings_sites_error_updatePathNameFailed - .tr(), - ), - maxLines: 3, - fontSize: 12, - textAlign: TextAlign.left, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.error, - ), - ) ?? - const SizedBox.shrink(); - - return widget; - }, - ), - ); - }, - onSubmitted: (name) { - // rename the path name - Log.info('rename the path name, from: $pathName, to: $name'); - - shareBloc.add(ShareEvent.updatePathName(name)); - }, - ); }, ); - shareBloc.add(const ShareEvent.clearPathNameResult()); - } - - void _showToast(BuildContext context, ShareState state) { - if (state.publishResult != null) { - state.publishResult!.fold( - (value) => showToastNotification( - message: LocaleKeys.publish_publishSuccessfully.tr(), - ), - (error) => showToastNotification( - message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', - type: ToastificationType.error, - ), - ); - } else if (state.unpublishResult != null) { - state.unpublishResult!.fold( - (value) => showToastNotification( - message: LocaleKeys.publish_unpublishSuccessfully.tr(), - ), - (error) => showToastNotification( - message: LocaleKeys.publish_unpublishFailed.tr(), - description: error.msg, - type: ToastificationType.error, - ), - ); - } else if (state.updatePathNameResult != null) { - state.updatePathNameResult!.onSuccess( - (value) { - showToastNotification( - message: - LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - - context.pop(); - }, - ); - } } } 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 3316b7049b..1a8ff64f2b 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,10 +24,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { height: 52.0, leftIcon: const FlowySvg( FlowySvgs.icon_document_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, - showBottomBorder: false, onTap: () => onAction(ViewLayoutPB.Document), ), FlowyOptionTile.text( @@ -35,10 +34,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { height: 52.0, leftIcon: const FlowySvg( FlowySvgs.icon_grid_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, - showBottomBorder: false, onTap: () => onAction(ViewLayoutPB.Grid), ), FlowyOptionTile.text( @@ -46,10 +44,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { height: 52.0, leftIcon: const FlowySvg( FlowySvgs.icon_board_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, - showBottomBorder: false, onTap: () => onAction(ViewLayoutPB.Board), ), FlowyOptionTile.text( @@ -57,10 +54,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { height: 52.0, leftIcon: const FlowySvg( FlowySvgs.icon_calendar_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, - showBottomBorder: false, onTap: () => onAction(ViewLayoutPB.Calendar), ), FlowyOptionTile.text( @@ -68,10 +64,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget { height: 52.0, leftIcon: const FlowySvg( FlowySvgs.chat_ai_page_s, - size: Size.square(20), + size: Size.square(18), ), showTopBorder: false, - showBottomBorder: false, onTap: () => onAction(ViewLayoutPB.Chat), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart index b8d0699969..7f7abdfab8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_block_action_widget.dart @@ -49,13 +49,7 @@ class BlockActionBottomSheet extends StatelessWidget { FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.button_duplicate.tr(), - leftIcon: const Padding( - padding: EdgeInsets.all(2), - child: FlowySvg( - FlowySvgs.copy_s, - size: Size.square(16), - ), - ), + leftIcon: const FlowySvg(FlowySvgs.m_duplicate_s), onTap: () => onAction(BlockActionBottomSheetType.duplicate), ), @@ -65,8 +59,7 @@ class BlockActionBottomSheet extends StatelessWidget { showTopBorder: false, text: LocaleKeys.button_delete.tr(), leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(18), + FlowySvgs.m_delete_s, color: Theme.of(context).colorScheme.error, ), textColor: Theme.of(context).colorScheme.error, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart index 8adc2bebec..4d411b7957 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart @@ -98,7 +98,7 @@ class BottomSheetBackButton extends StatelessWidget { width: 18, height: 18, child: FlowySvg( - FlowySvgs.m_bottom_sheet_back_s, + FlowySvgs.m_app_bar_back_s, ), ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart deleted file mode 100644 index 11292e3194..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart'; -import 'package:appflowy/util/xfile_ext.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:flutter/widgets.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; - -class MobileMediaUploadSheetContent extends StatelessWidget { - const MobileMediaUploadSheetContent({super.key, required this.dialogContext}); - - final BuildContext dialogContext; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.only(top: 12), - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: MobileFileUploadMenu( - onInsertLocalFile: (files) async { - dialogContext.pop(); - - await insertLocalFiles( - context, - files, - userProfile: context.read().state.userProfile, - documentId: context.read().rowId, - onUploadSuccess: (file, path, isLocalMode) { - final mediaCellBloc = context.read(); - if (mediaCellBloc.isClosed) { - return; - } - - mediaCellBloc.add( - MediaCellEvent.addFile( - url: path, - name: file.name, - uploadType: isLocalMode - ? FileUploadTypePB.LocalFile - : FileUploadTypePB.CloudFile, - fileType: file.fileType.toMediaFileTypePB(), - ), - ); - }, - ); - }, - onInsertNetworkFile: (url) async => _onInsertNetworkFile( - url, - dialogContext, - context, - ), - ), - ); - } - - Future _onInsertNetworkFile( - String url, - BuildContext dialogContext, - BuildContext context, - ) async { - dialogContext.pop(); - - if (url.isEmpty) return; - final uri = Uri.tryParse(url); - if (uri == null) { - return; - } - - final fakeFile = XFile(uri.path); - MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB(); - fileType = - fileType == MediaFileTypePB.Other ? MediaFileTypePB.Link : fileType; - - String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ""; - if (name.isEmpty && uri.pathSegments.length > 1) { - name = uri.pathSegments[uri.pathSegments.length - 2]; - } else if (name.isEmpty) { - name = uri.host; - } - - context.read().add( - MediaCellEvent.addFile( - url: url, - name: name, - uploadType: FileUploadTypePB.NetworkFile, - fileType: fileType, - ), - ); - } -} 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 8ac4d9b20e..d4f49cb9a9 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,7 +52,6 @@ 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 86021ea938..75b0151a3a 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,3 +1,4 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; @@ -5,7 +6,6 @@ import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -64,9 +64,6 @@ class _MobileViewItemBottomSheetState extends State { case MobileViewItemBottomSheetBodyAction.duplicate: Navigator.pop(context); context.read().add(const ViewEvent.duplicate()); - showToastNotification( - message: LocaleKeys.button_duplicateSuccessfully.tr(), - ); break; case MobileViewItemBottomSheetBodyAction.share: // unimplemented @@ -82,11 +79,6 @@ class _MobileViewItemBottomSheetState extends State { context .read() .add(FavoriteEvent.toggle(widget.view)); - showToastNotification( - message: !widget.view.isFavorite - ? LocaleKeys.button_favoriteSuccessfully.tr() - : LocaleKeys.button_unfavoriteSuccessfully.tr(), - ); break; case MobileViewItemBottomSheetBodyAction.removeFromRecent: _removeFromRecent(context); @@ -117,6 +109,16 @@ 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, + ); + }, + ); }, ); } @@ -124,29 +126,48 @@ class _MobileViewItemBottomSheetState extends State { Future _showConfirmDialog({required VoidCallback onDelete}) async { await showFlowyCupertinoConfirmDialog( title: LocaleKeys.sideBar_removePageFromRecent.tr(), - leftButton: FlowyText( + leftButton: FlowyText.regular( LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), + color: const Color(0xFF1456F0), ), - rightButton: FlowyText( + rightButton: FlowyText.medium( LocaleKeys.button_delete.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, color: const Color(0xFFFE0220), ), onRightButtonPressed: (context) { onDelete(); - Navigator.pop(context); - - showToastNotification( - message: LocaleKeys.sideBar_removeSuccess.tr(), - ); }, ); } } + +class _RemoveToast extends StatelessWidget { + const _RemoveToast(); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.0), + color: const Color(0xE5171717), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.success_s, + blendMode: null, + ), + const HSpace(8.0), + FlowyText.regular( + LocaleKeys.sideBar_removeSuccess.tr(), + fontSize: 16.0, + color: Colors.white, + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index 0ca60fe40b..a078521aec 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart @@ -1,10 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; enum MobileViewItemBottomSheetBodyAction { rename, @@ -42,8 +40,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { BuildContext context, MobileViewItemBottomSheetBodyAction action, ) { - final isLocked = - context.read()?.state.isLocked ?? false; switch (action) { case MobileViewItemBottomSheetBodyAction.rename: return FlowyOptionTile.text( @@ -53,7 +49,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { FlowySvgs.view_item_rename_s, size: Size.square(18), ), - enable: !isLocked, showTopBorder: false, showBottomBorder: false, onTap: () => onAction( @@ -99,7 +94,6 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { size: const Size.square(18), color: Theme.of(context).colorScheme.error, ), - enable: !isLocked, showTopBorder: false, showBottomBorder: false, onTap: () => onAction( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index 47ab37505e..62d471a093 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -1,54 +1,26 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.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:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; enum MobileViewBottomSheetBodyAction { undo, redo, + share, rename, duplicate, delete, addToFavorites, removeFromFavorites, - helpCenter, - publish, - unpublish, - copyPublishLink, - visitSite, - copyShareLink, - updatePathName, - lockPage; - - static const disableInLockedView = [ - undo, - redo, - rename, - delete, - ]; -} - -class MobileViewBottomSheetBodyActionArguments { - static const isLockedKey = 'is_locked'; + helpCenter; } typedef MobileViewBottomSheetBodyActionCallback = void Function( MobileViewBottomSheetBodyAction action, - // for the [MobileViewBottomSheetBodyAction.lockPage] action, - // it will pass the [isLocked] value to the callback. - { - Map? arguments, -}); +); class ViewPageBottomSheet extends StatefulWidget { const ViewPageBottomSheet({ @@ -75,7 +47,7 @@ class _ViewPageBottomSheetState extends State { case MobileBottomSheetType.view: return MobileViewBottomSheetBody( view: widget.view, - onAction: (action, {arguments}) { + onAction: (action) { switch (action) { case MobileViewBottomSheetBodyAction.rename: setState(() { @@ -83,7 +55,7 @@ class _ViewPageBottomSheetState extends State { }); break; default: - widget.onAction(action, arguments: arguments); + widget.onAction(action); } }, ); @@ -112,16 +84,12 @@ class MobileViewBottomSheetBody extends StatelessWidget { @override Widget build(BuildContext context) { final isFavorite = view.isFavorite; - final isLocked = - context.watch()?.state.isLocked ?? false; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ MobileQuickActionButton( text: LocaleKeys.button_rename.tr(), icon: FlowySvgs.view_item_rename_s, - iconSize: const Size.square(18), - enable: !isLocked, onTap: () => onAction( MobileViewBottomSheetBodyAction.rename, ), @@ -132,7 +100,6 @@ class MobileViewBottomSheetBody extends StatelessWidget { ? LocaleKeys.button_removeFromFavorites.tr() : LocaleKeys.button_addToFavorites.tr(), icon: isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s, - iconSize: const Size.square(18), onTap: () => onAction( isFavorite ? MobileViewBottomSheetBodyAction.removeFromFavorites @@ -140,56 +107,19 @@ class MobileViewBottomSheetBody extends StatelessWidget { ), ), _divider(), - if (view.layout.isDatabaseView || view.layout.isDocumentView) ...[ - MobileQuickActionButton( - text: LocaleKeys.disclosureAction_lockPage.tr(), - icon: FlowySvgs.lock_page_s, - iconSize: const Size.square(18), - rightIconBuilder: (context) => _LockPageRightIconBuilder( - onAction: onAction, - ), - onTap: () { - final isLocked = - context.read()?.state.isLocked ?? false; - onAction( - MobileViewBottomSheetBodyAction.lockPage, - arguments: { - MobileViewBottomSheetBodyActionArguments.isLockedKey: - !isLocked, - }, - ); - }, - ), - _divider(), - ], MobileQuickActionButton( text: LocaleKeys.button_duplicate.tr(), icon: FlowySvgs.duplicate_s, - iconSize: const Size.square(18), onTap: () => onAction( MobileViewBottomSheetBodyAction.duplicate, ), ), - // copy link _divider(), - MobileQuickActionButton( - text: LocaleKeys.shareAction_copyLink.tr(), - icon: FlowySvgs.m_copy_link_s, - iconSize: const Size.square(18), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.copyShareLink, - ), - ), - _divider(), - ..._buildPublishActions(context), - MobileQuickActionButton( text: LocaleKeys.button_delete.tr(), textColor: Theme.of(context).colorScheme.error, icon: FlowySvgs.trash_s, iconColor: Theme.of(context).colorScheme.error, - iconSize: const Size.square(18), - enable: !isLocked, onTap: () => onAction( MobileViewBottomSheetBodyAction.delete, ), @@ -199,91 +129,8 @@ class MobileViewBottomSheetBody extends StatelessWidget { ); } - List _buildPublishActions(BuildContext context) { - final userProfile = context.read().state.userProfilePB; - // the publish feature is only available for AppFlowy Cloud - if (userProfile == null || - userProfile.workspaceAuthType != AuthTypePB.Server) { - return []; - } - - final isPublished = context.watch().state.isPublished; - if (isPublished) { - return [ - MobileQuickActionButton( - text: LocaleKeys.shareAction_updatePathName.tr(), - icon: FlowySvgs.view_item_rename_s, - iconSize: const Size.square(18), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.updatePathName, - ), - ), - _divider(), - MobileQuickActionButton( - text: LocaleKeys.shareAction_visitSite.tr(), - icon: FlowySvgs.m_visit_site_s, - iconSize: const Size.square(18), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.visitSite, - ), - ), - _divider(), - MobileQuickActionButton( - text: LocaleKeys.shareAction_unPublish.tr(), - icon: FlowySvgs.m_unpublish_s, - iconSize: const Size.square(18), - onTap: () => onAction( - MobileViewBottomSheetBodyAction.unpublish, - ), - ), - _divider(), - ]; - } else { - return [ - MobileQuickActionButton( - text: LocaleKeys.shareAction_publish.tr(), - icon: FlowySvgs.m_publish_s, - onTap: () => onAction( - MobileViewBottomSheetBodyAction.publish, - ), - ), - _divider(), - ]; - } - } - - Widget _divider() => const MobileQuickActionDivider(); -} - -class _LockPageRightIconBuilder extends StatelessWidget { - const _LockPageRightIconBuilder({ - required this.onAction, - }); - - final MobileViewBottomSheetBodyActionCallback onAction; - - @override - Widget build(BuildContext context) { - final isLocked = - context.watch()?.state.isLocked ?? false; - return SizedBox( - width: 46, - height: 30, - child: FittedBox( - fit: BoxFit.fill, - child: CupertinoSwitch( - value: isLocked, - activeTrackColor: Theme.of(context).colorScheme.primary, - onChanged: (value) { - onAction( - MobileViewBottomSheetBodyAction.lockPage, - arguments: { - MobileViewBottomSheetBodyActionArguments.isLockedKey: value, - }, - ); - }, - ), - ), - ); - } + Widget _divider() => const Divider( + height: 8.5, + thickness: 0.5, + ); } 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 d4b4292443..1583e9e2e0 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,9 +7,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_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'; @@ -43,30 +40,18 @@ enum MobilePaneActionType { backgroundColor: const Color(0xFFFA217F), svg: FlowySvgs.favorite_section_remove_from_favorite_s, size: 24.0, - onPressed: (context) { - showToastNotification( - message: LocaleKeys.button_unfavoriteSuccessfully.tr(), - ); - - context - .read() - .add(FavoriteEvent.toggle(context.read().view)); - }, + onPressed: (context) => 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) { - showToastNotification( - message: LocaleKeys.button_favoriteSuccessfully.tr(), - ); - - context - .read() - .add(FavoriteEvent.toggle(context.read().view)); - }, + onPressed: (context) => context + .read() + .add(FavoriteEvent.toggle(context.read().view)), ); case MobilePaneActionType.add: return MobileSlideActionButton( @@ -84,7 +69,6 @@ enum MobilePaneActionType { showDragHandle: true, showCloseButton: true, useRootNavigator: true, - showDivider: false, backgroundColor: Theme.of(context).colorScheme.surface, builder: (sheetContext) { return AddNewPageWidgetBottomSheet( @@ -93,7 +77,7 @@ enum MobilePaneActionType { Navigator.of(sheetContext).pop(); viewBloc.add( ViewEvent.createView( - layout.defaultName, + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, section: spaceType!.toViewSectionPB, ), @@ -130,11 +114,6 @@ enum MobilePaneActionType { BlocProvider.value(value: favoriteBloc), if (recentViewsBloc != null) BlocProvider.value(value: recentViewsBloc), - BlocProvider( - create: (_) => - ViewLockStatusBloc(view: viewBloc.state.view) - ..add(const ViewLockStatusEvent.initial()), - ), ], child: BlocBuilder( builder: (context, state) { @@ -166,6 +145,8 @@ enum MobilePaneActionType { ? MobileViewItemBottomSheetBodyAction.removeFromFavorites : MobileViewItemBottomSheetBodyAction.addToFavorites, MobileViewItemBottomSheetBodyAction.divider, + if (view.layout != ViewLayoutPB.Chat) + MobileViewItemBottomSheetBodyAction.duplicate, MobileViewItemBottomSheetBodyAction.divider, MobileViewItemBottomSheetBodyAction.removeFromRecent, ]; @@ -175,6 +156,7 @@ enum MobilePaneActionType { ? MobileViewItemBottomSheetBodyAction.removeFromFavorites : MobileViewItemBottomSheetBodyAction.addToFavorites, MobileViewItemBottomSheetBodyAction.divider, + MobileViewItemBottomSheetBodyAction.duplicate, ]; } } @@ -199,13 +181,12 @@ ActionPane buildEndActionPane( bool needSpace = true, MobilePageCardType? cardType, FolderSpaceType? spaceType, - required double spaceRatio, }) { return ActionPane( motion: const ScrollMotion(), - extentRatio: actions.length / spaceRatio, + extentRatio: actions.length / 5, children: [ - if (needSpace) const HSpace(60), + if (needSpace) const HSpace(20), ...actions.map( (action) => action.actionButton( context, 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 a0fa5dc6aa..be815b6550 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 @@ -47,17 +47,13 @@ Future showMobileBottomSheet( Color? barrierColor, double? elevation, bool showDoneButton = false, - void Function(BuildContext context)? onDone, bool enableDraggableScrollable = false, - bool enableScrollable = false, // this field is only used if showDragHandle is true Widget Function(BuildContext, ScrollController)? scrollableWidgetBuilder, // only used when enableDraggableScrollable is true double minChildSize = 0.5, double maxChildSize = 0.8, double initialChildSize = 0.51, - double bottomSheetPadding = 0, - bool enablePadding = true, }) async { assert( showHeader || @@ -74,7 +70,6 @@ Future showMobileBottomSheet( backgroundColor ??= Theme.of(context).brightness == Brightness.light ? const Color(0xFFF7F8FB) : const Color(0xFF23262B); - barrierColor ??= Colors.black.withValues(alpha: 0.3); return showModalBottomSheet( context: context, @@ -114,7 +109,6 @@ Future showMobileBottomSheet( showRemoveButton: showRemoveButton, title: title, onRemove: onRemove, - onDone: onDone, ), ); @@ -146,7 +140,6 @@ Future showMobileBottomSheet( ) ?? Expanded( child: Scrollbar( - controller: scrollController, child: SingleChildScrollView( controller: scrollController, child: child, @@ -157,34 +150,17 @@ Future showMobileBottomSheet( ); }, ); - } else if (enableScrollable) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...children, - Flexible( - child: SingleChildScrollView( - child: child, - ), - ), - VSpace(bottomSheetPadding), - ], - ); } // ----- content area ----- - if (enablePadding) { - // add content padding and extra bottom padding - children.add( - Padding( - padding: - padding + EdgeInsets.only(bottom: context.bottomSheetPadding()), - child: child, - ), - ); - } else { - children.add(child); - } + // add content padding and extra bottom padding + children.add( + Padding( + padding: + padding + EdgeInsets.only(bottom: context.bottomSheetPadding()), + child: child, + ), + ); // ----- content area ----- if (children.length == 1) { @@ -215,23 +191,14 @@ class BottomSheetHeader extends StatelessWidget { required this.title, required this.showDoneButton, this.onRemove, - this.onDone, - this.onBack, - this.onClose, }); - final String title; - final bool showBackButton; final bool showCloseButton; final bool showRemoveButton; + final String title; final bool showDoneButton; - final VoidCallback? onRemove; - final VoidCallback? onBack; - final VoidCallback? onClose; - - final void Function(BuildContext context)? onDone; @override Widget build(BuildContext context) { @@ -242,18 +209,14 @@ class BottomSheetHeader extends StatelessWidget { child: Stack( children: [ if (showBackButton) - Align( + const Align( alignment: Alignment.centerLeft, - child: BottomSheetBackButton( - onTap: onBack, - ), + child: BottomSheetBackButton(), ), if (showCloseButton) - Align( + const Align( alignment: Alignment.centerLeft, - child: BottomSheetCloseButton( - onTap: onClose, - ), + child: BottomSheetCloseButton(), ), if (showRemoveButton) Align( @@ -263,27 +226,17 @@ class BottomSheetHeader extends StatelessWidget { ), ), Align( - child: Container( - constraints: const BoxConstraints(maxWidth: 250), - child: FlowyText( - title, - fontSize: 17.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), + child: FlowyText( + title, + fontSize: 16.0, + fontWeight: FontWeight.w500, ), ), if (showDoneButton) Align( alignment: Alignment.centerRight, child: BottomSheetDoneButton( - onDone: () { - if (onDone != null) { - onDone?.call(context); - } else { - Navigator.pop(context); - } - }, + onDone: () => Navigator.pop(context), ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart index b29817251a..0ff2a6634a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart @@ -329,7 +329,7 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget { (Theme.of(context).brightness == Brightness.dark ? Colors.grey : Colors.black) - .withValues(alpha: secondaryAnimation.value * 0.1), + .withOpacity(secondaryAnimation.value * 0.1), BlendMode.srcOver, ), child: child, 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 29841dd22a..a1fc2a70a3 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,15 +3,14 @@ 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/util/field_type_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -70,10 +69,10 @@ class _MobileBoardPageState extends State { loading: (_) => const Center( child: CircularProgressIndicator.adaptive(), ), - error: (err) => Center( - child: AppFlowyErrorPage( - error: err.error, - ), + error: (err) => FlowyMobileStateContainer.error( + emoji: '🛸', + title: LocaleKeys.board_mobile_failedToLoad.tr(), + errorMsg: err.toString(), ), ready: (data) => const _BoardContent(), orElse: () => const SizedBox.shrink(), @@ -143,8 +142,6 @@ class _BoardContentState extends State<_BoardContent> { return state.maybeMap( orElse: () => const SizedBox.shrink(), ready: (state) { - final isLocked = - context.watch()?.state.isLocked ?? false; final showCreateGroupButton = context .read() .groupingFieldType @@ -162,20 +159,15 @@ class _BoardContentState extends State<_BoardContent> { padding: config.groupHeaderPadding, ) : const HSpace(16), - trailing: showCreateGroupButton && !isLocked + trailing: showCreateGroupButton ? const MobileBoardTrailing() : const HSpace(16), - headerBuilder: (_, groupData) { - final isLocked = - context.read()?.state.isLocked ?? - false; - return IgnorePointer( - ignoring: isLocked, - child: GroupCardHeader( - groupData: groupData, - ), - ); - }, + headerBuilder: (_, groupData) => BlocProvider.value( + value: context.read(), + child: GroupCardHeader( + groupData: groupData, + ), + ), footerBuilder: _buildFooter, cardBuilder: (_, column, columnItem) => _buildCard( context: context, @@ -191,39 +183,34 @@ class _BoardContentState extends State<_BoardContent> { } Widget _buildFooter(BuildContext context, AppFlowyGroupData columnData) { - final isLocked = - context.read()?.state.isLocked ?? false; final style = Theme.of(context); return SizedBox( height: 42, width: double.infinity, - child: IgnorePointer( - ignoring: isLocked, - child: TextButton.icon( - style: TextButton.styleFrom( - padding: const EdgeInsets.only(left: 8), - alignment: Alignment.centerLeft, - ), - icon: FlowySvg( - FlowySvgs.add_m, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.only(left: 8), + alignment: Alignment.centerLeft, + ), + icon: FlowySvg( + FlowySvgs.add_m, + color: style.colorScheme.onSurface, + ), + label: Text( + LocaleKeys.board_column_createNewCard.tr(), + style: style.textTheme.bodyMedium?.copyWith( color: style.colorScheme.onSurface, ), - label: Text( - LocaleKeys.board_column_createNewCard.tr(), - style: style.textTheme.bodyMedium?.copyWith( - color: style.colorScheme.onSurface, - ), - ), - onPressed: () => context.read().add( - BoardEvent.createRow( - columnData.id, - OrderObjectPositionTypePB.End, - null, - null, - ), - ), ), + onPressed: () => context.read().add( + BoardEvent.createRow( + columnData.id, + OrderObjectPositionTypePB.End, + null, + null, + ), + ), ), ); } @@ -243,43 +230,34 @@ class _BoardContentState extends State<_BoardContent> { CardCellBuilder(databaseController: boardBloc.databaseController); final groupItemId = groupItem.row.id + groupData.group.groupId; - final isLocked = - context.read()?.state.isLocked ?? false; return Container( key: ValueKey(groupItemId), margin: cardMargin, decoration: _makeBoxDecoration(context), - child: BlocProvider.value( - value: boardBloc, - child: IgnorePointer( - ignoring: isLocked, - child: RowCard( - fieldController: boardBloc.fieldController, - rowMeta: rowMeta, - viewId: boardBloc.viewId, - rowCache: boardBloc.rowCache, - groupingFieldId: groupItem.fieldInfo.id, - isEditing: false, - cellBuilder: cellBuilder, - onTap: (context) { - context.push( - MobileRowDetailPage.routeName, - extra: { - MobileRowDetailPage.argRowId: rowMeta.id, - MobileRowDetailPage.argDatabaseController: - context.read().databaseController, - }, - ); + child: RowCard( + fieldController: boardBloc.fieldController, + rowMeta: rowMeta, + viewId: boardBloc.viewId, + rowCache: boardBloc.rowCache, + groupingFieldId: groupItem.fieldInfo.id, + isEditing: false, + cellBuilder: cellBuilder, + onTap: (context) { + context.push( + MobileRowDetailPage.routeName, + extra: { + MobileRowDetailPage.argRowId: rowMeta.id, + MobileRowDetailPage.argDatabaseController: + context.read().databaseController, }, - onStartEditing: () {}, - onEndEditing: () {}, - styleConfiguration: RowCardStyleConfiguration( - cellStyleMap: mobileBoardCardCellStyleMap(context), - showAccessory: false, - ), - userProfile: boardBloc.userProfile, - ), + ); + }, + onStartEditing: () {}, + onEndEditing: () {}, + styleConfiguration: RowCardStyleConfiguration( + cellStyleMap: mobileBoardCardCellStyleMap(context), + showAccessory: false, ), ), ); @@ -293,20 +271,14 @@ class _BoardContentState extends State<_BoardContent> { border: themeMode == ThemeMode.light ? Border.fromBorderSide( BorderSide( - color: Theme.of(context) - .colorScheme - .outline - .withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), ), ) : null, boxShadow: themeMode == ThemeMode.light ? [ BoxShadow( - color: Theme.of(context) - .colorScheme - .outline - .withValues(alpha: 0.5), + color: Theme.of(context).colorScheme.outline.withOpacity(0.5), blurRadius: 4, offset: const Offset(0, 2), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart index 8d1e91b708..35208c91bd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/group_card_header.dart @@ -3,10 +3,11 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -112,8 +113,12 @@ class _GroupCardHeaderState extends State { context, showDragHandle: true, backgroundColor: Theme.of(context).colorScheme.surface, - builder: (_) => Column( + builder: (_) => SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.stretch, + separatorBuilder: () => const Divider( + height: 8.5, + thickness: 0.5, + ), children: [ MobileQuickActionButton( text: LocaleKeys.board_column_renameColumn.tr(), @@ -127,7 +132,6 @@ class _GroupCardHeaderState extends State { context.pop(); }, ), - const MobileQuickActionDivider(), MobileQuickActionButton( text: LocaleKeys.board_column_hideColumn.tr(), icon: FlowySvgs.hide_s, 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 f80525786e..0b0c16f951 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,12 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package: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'; @@ -14,6 +11,7 @@ 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'; @@ -202,7 +200,7 @@ class MobileHiddenGroup extends StatelessWidget { children: [ Expanded( child: Text( - group.generateGroupName(databaseController), + context.read().generateGroupNameFromGroup(group), 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 5896c51b9b..c65f899c34 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 @@ -3,7 +3,6 @@ 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'; @@ -19,10 +18,6 @@ import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dar import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; 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'; @@ -58,9 +53,7 @@ class _MobileRowDetailPageState extends State { late final PageController _pageController; String get viewId => widget.databaseController.viewId; - RowCache get rowCache => widget.databaseController.rowCache; - FieldController get fieldController => widget.databaseController.fieldController; @@ -150,59 +143,7 @@ class _MobileRowDetailPageState extends State { icon: FlowySvgs.duplicate_s, text: LocaleKeys.button_duplicate.tr(), ), - const MobileQuickActionDivider(), - MobileQuickActionButton( - onTap: () => showMobileBottomSheet( - context, - title: LocaleKeys.grid_media_addFileMobile.tr(), - showHeader: true, - showCloseButton: true, - showDragHandle: true, - builder: (dialogContext) => Container( - margin: const EdgeInsets.only(top: 12), - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: FileUploadMenu( - onInsertLocalFile: (files) async { - context - ..pop() - ..pop(); - - if (_bloc.state.currentRowId == null) { - return; - } - - await insertLocalFiles( - context, - files, - userProfile: _bloc.userProfile, - documentId: _bloc.state.currentRowId!, - onUploadSuccess: (file, path, isLocalMode) { - _bloc.add( - MobileRowDetailEvent.addCover( - RowCoverPB( - data: path, - uploadType: isLocalMode - ? FileUploadTypePB.LocalFile - : FileUploadTypePB.CloudFile, - coverType: CoverTypePB.FileCover, - ), - ), - ); - }, - ); - }, - onInsertNetworkFile: (url) async => - _onInsertNetworkFile(url, context), - ), - ), - ), - icon: FlowySvgs.add_cover_s, - text: 'Add cover', - ), - const MobileQuickActionDivider(), + const Divider(height: 8.5, thickness: 0.5), MobileQuickActionButton( onTap: () => _performAction(viewId, _bloc.state.currentRowId, true), text: LocaleKeys.button_delete.tr(), @@ -210,6 +151,7 @@ class _MobileRowDetailPageState extends State { icon: FlowySvgs.trash_s, iconColor: Theme.of(context).colorScheme.error, ), + const Divider(height: 8.5, thickness: 0.5), ], ), ); @@ -234,38 +176,6 @@ class _MobileRowDetailPageState extends State { gravity: ToastGravity.BOTTOM, ); } - - Future _onInsertNetworkFile( - String url, - BuildContext context, - ) async { - context - ..pop() - ..pop(); - - if (url.isEmpty) return; - final uri = Uri.tryParse(url); - if (uri == null) { - return; - } - - String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ""; - if (name.isEmpty && uri.pathSegments.length > 1) { - name = uri.pathSegments[uri.pathSegments.length - 2]; - } else if (name.isEmpty) { - name = uri.host; - } - - _bloc.add( - MobileRowDetailEvent.addCover( - RowCoverPB( - data: url, - uploadType: FileUploadTypePB.NetworkFile, - coverType: CoverTypePB.FileCover, - ), - ), - ); - } } class RowDetailFab extends StatelessWidget { @@ -381,12 +291,9 @@ class MobileRowDetailPageContentState late final EditableCellBuilder cellBuilder; String get viewId => widget.databaseController.viewId; - RowCache get rowCache => widget.databaseController.rowCache; - FieldController get fieldController => widget.databaseController.fieldController; - ValueNotifier primaryFieldId = ValueNotifier(''); @override void initState() { @@ -397,8 +304,6 @@ class MobileRowDetailPageContentState viewId: viewId, rowCache: rowCache, ); - rowController.initialize(); - cellBuilder = EditableCellBuilder( databaseController: widget.databaseController, ); @@ -412,129 +317,68 @@ class MobileRowDetailPageContentState rowController: rowController, ), child: BlocBuilder( - builder: (context, rowDetailState) => Column( - children: [ - if (rowDetailState.rowMeta.cover.data.isNotEmpty) ...[ - GestureDetector( - onTap: () => showMobileBottomSheet( - context, - backgroundColor: AFThemeExtension.of(context).background, - showDragHandle: true, - builder: (_) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - MobileQuickActionButton( - onTap: () { - context - ..pop() - ..read() - .add(const RowDetailEvent.removeCover()); - }, - text: LocaleKeys.button_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - icon: FlowySvgs.trash_s, - iconColor: Theme.of(context).colorScheme.error, + builder: (context, rowDetailState) { + return Column( + children: [ + BlocProvider( + create: (context) => RowBannerBloc( + viewId: viewId, + fieldController: fieldController, + rowMeta: rowController.rowMeta, + )..add(const RowBannerEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + if (state.primaryField == null) { + return const SizedBox.shrink(); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: cellBuilder.buildCustom( + CellContext( + rowId: rowController.rowId, + fieldId: state.primaryField!.id, + ), + skinMap: EditableCellSkinMap( + textSkin: _TitleSkin(), + ), ), - ], - ), + ); + }, ), - child: SizedBox( - height: 200, - width: double.infinity, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, + ), + Expanded( + child: ListView( + padding: const EdgeInsets.only(top: 9, bottom: 100), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: MobileRowPropertyList( + databaseController: widget.databaseController, + cellBuilder: cellBuilder, + ), ), - child: AFImage( - url: rowDetailState.rowMeta.cover.data, - uploadType: widget.rowMeta.cover.uploadType, - userProfile: - context.read().userProfile, + Padding( + padding: const EdgeInsets.fromLTRB(6, 6, 16, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (rowDetailState.numHiddenFields != 0) ...[ + const ToggleHiddenFieldsVisibilityButton(), + ], + MobileRowDetailCreateFieldButton( + viewId: viewId, + fieldController: fieldController, + ), + ], + ), ), - ), + ], ), ), ], - BlocProvider( - create: (context) => RowBannerBloc( - viewId: viewId, - fieldController: fieldController, - rowMeta: rowController.rowMeta, - )..add(const RowBannerEvent.initial()), - 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(); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: cellBuilder.buildCustom( - CellContext( - rowId: rowController.rowId, - fieldId: state.primaryField!.id, - ), - skinMap: EditableCellSkinMap(textSkin: _TitleSkin()), - ), - ); - }, - ), - ), - Expanded( - child: ListView( - padding: const EdgeInsets.only(top: 9, bottom: 100), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: MobileRowPropertyList( - databaseController: widget.databaseController, - cellBuilder: cellBuilder, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(6, 6, 16, 0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - 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, - ), - ], - ), - ), - ], - ), - ), - ], - ), + ); + }, ), ); } @@ -545,7 +389,6 @@ class _TitleSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -558,9 +401,7 @@ class _TitleSkin extends IEditableTextCellSkin { fontSize: 23, fontWeight: FontWeight.w500, ), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.text)); - }, + onChanged: (text) => bloc.add(TextCellEvent.updateText(text)), decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(vertical: 9), border: InputBorder.none, 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 1d3d3efcf5..d683a9b72d 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, - maxHeight: GridSize.headerHeight, + minHeight: 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(horizontal: 6, vertical: 2), + EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), 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 42bb241ab8..0498427547 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,9 +1,10 @@ +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'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; 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'; @@ -75,8 +76,10 @@ class _PropertyCellState extends State<_PropertyCell> { children: [ Row( children: [ - FieldIcon( - fieldInfo: fieldInfo, + FlowySvg( + fieldInfo.fieldType.svgData, + color: Theme.of(context).hintColor, + size: const Size.square(16), ), const HSpace(6), Expanded( @@ -84,7 +87,6 @@ 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/option_text_field.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart index e2614296af..fc869a54c7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/option_text_field.dart @@ -11,17 +11,13 @@ class OptionTextField extends StatelessWidget { const OptionTextField({ super.key, required this.controller, - this.autoFocus = false, - required this.isPrimary, - required this.fieldType, + required this.type, required this.onTextChanged, required this.onFieldTypeChanged, }); final TextEditingController controller; - final bool autoFocus; - final bool isPrimary; - final FieldType fieldType; + final FieldType type; final void Function(String value) onTextChanged; final void Function(FieldType value) onFieldTypeChanged; @@ -29,14 +25,10 @@ class OptionTextField extends StatelessWidget { Widget build(BuildContext context) { return FlowyOptionTile.textField( controller: controller, - autofocus: autoFocus, textFieldPadding: const EdgeInsets.symmetric(horizontal: 12.0), onTextChanged: onTextChanged, leftIcon: GestureDetector( onTap: () async { - if (isPrimary) { - return; - } final fieldType = await showFieldTypeGridBottomSheet( context, title: LocaleKeys.grid_field_editProperty.tr(), @@ -51,12 +43,12 @@ class OptionTextField extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Theme.of(context).brightness == Brightness.light - ? fieldType.mobileIconBackgroundColor - : fieldType.mobileIconBackgroundColorDark, + ? type.mobileIconBackgroundColor + : type.mobileIconBackgroundColorDark, ), child: Center( child: FlowySvg( - fieldType.svgData, + type.svgData, size: const Size.square(22), ), ), 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 deleted file mode 100644 index b0f21188cd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart +++ /dev/null @@ -1,145 +0,0 @@ -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/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -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(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, - tabs: [PickerTabType.emoji.name], - ); - } - } - - // 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 aa9d23308a..48d8b2f097 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,11 +1,9 @@ -import 'package:flutter/material.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:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter/material.dart'; class MobileCardContent extends StatelessWidget { const MobileCardContent({ @@ -14,38 +12,29 @@ class MobileCardContent extends StatelessWidget { required this.cellBuilder, required this.cells, required this.styleConfiguration, - required this.userProfile, }); final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; final List cells; final RowCardStyleConfiguration styleConfiguration; - final UserProfilePB? userProfile; @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (rowMeta.cover.data.isNotEmpty) ...[ - CardCover(cover: rowMeta.cover, userProfile: userProfile), - ], - Padding( - padding: styleConfiguration.cardPadding, - child: Column( - children: [ - ...cells.map( - (cellMeta) => cellBuilder.build( - cellContext: cellMeta.cellContext(), - styleMap: mobileBoardCardCellStyleMap(context), - hasNotes: !rowMeta.isDocumentEmpty, - ), - ), - ], - ), - ), - ], + 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(), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index 8262cf6408..5d5774156b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -5,8 +5,9 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -70,52 +71,71 @@ class _MobileDateCellEditScreenState extends State { ); } - Widget _buildDatePicker() { - return BlocProvider( - create: (_) => DateCellEditorBloc( - reminderBloc: getIt(), - cellController: widget.controller, - ), - child: BlocBuilder( - builder: (context, state) { - final dateCellBloc = context.read(); - return MobileAppFlowyDatePicker( - dateTime: state.dateTime, - endDateTime: state.endDateTime, - isRange: state.isRange, - includeTime: state.includeTime, - dateFormat: state.dateTypeOptionPB.dateFormat, - timeFormat: state.dateTypeOptionPB.timeFormat, - reminderOption: state.reminderOption, - onDaySelected: (selectedDay) { - dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay)); - }, - onRangeSelected: (start, end) { - dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end)); - }, - onIsRangeChanged: (value, dateTime, endDateTime) { - dateCellBloc.add( - DateCellEditorEvent.setIsRange(value, dateTime, endDateTime), - ); - }, - onIncludeTimeChanged: (value, dateTime, endDateTime) { - dateCellBloc.add( - DateCellEditorEvent.setIncludeTime( - value, - dateTime, - endDateTime, - ), - ); - }, - onClearDate: () { - dateCellBloc.add(const DateCellEditorEvent.clearDate()); - }, - onReminderSelected: (option) { - dateCellBloc.add(DateCellEditorEvent.setReminderOption(option)); - }, - ); - }, - ), - ); - } + Widget _buildDatePicker() => MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => DateCellEditorBloc( + reminderBloc: getIt(), + cellController: widget.controller, + ), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return MobileAppFlowyDatePicker( + selectedDay: state.dateTime, + dateStr: state.dateStr, + endDateStr: state.endDateStr, + timeStr: state.timeStr, + endTimeStr: state.endTimeStr, + startDay: state.startDay, + endDay: state.endDay, + enableRanges: true, + isRange: state.isRange, + includeTime: state.includeTime, + use24hFormat: state.dateTypeOptionPB.timeFormat == + TimeFormatPB.TwentyFourHour, + timeFormat: state.dateTypeOptionPB.timeFormat, + selectedReminderOption: state.reminderOption, + onStartTimeChanged: (String? time) { + if (time != null) { + context + .read() + .add(DateCellEditorEvent.setTime(time)); + } + }, + onEndTimeChanged: (String? time) { + if (time != null) { + context + .read() + .add(DateCellEditorEvent.setEndTime(time)); + } + }, + onDaySelected: (selectedDay, focusedDay) => context + .read() + .add(DateCellEditorEvent.selectDay(selectedDay)), + onRangeSelected: (start, end, focused) => context + .read() + .add(DateCellEditorEvent.selectDateRange(start, end)), + onRangeChanged: (value) => context + .read() + .add(DateCellEditorEvent.setIsRange(value)), + onIncludeTimeChanged: (value) => context + .read() + .add(DateCellEditorEvent.setIncludeTime(value)), + onClearDate: () => context + .read() + .add(const DateCellEditorEvent.clearDate()), + onReminderSelected: (option) => + context.read().add( + DateCellEditorEvent.setReminderOption( + option: option, + selectedDay: + state.dateTime == null ? DateTime.now() : null, + ), + ), + ); + }, + ), + ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart index 5abb2dc031..0f7c758664 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_create_field_screen.dart @@ -37,7 +37,6 @@ class _MobileNewPropertyScreenState extends State { final type = widget.fieldType ?? FieldType.RichText; optionValues = FieldOptionValues( type: type, - icon: "", name: type.i18n, ); } 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 75b52de414..d794817339 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,8 +1,8 @@ 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/application/field/field_info.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_service.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -48,8 +48,8 @@ class _MobileEditPropertyScreenState extends State { final fieldId = widget.field.id; return PopScope( - onPopInvokedWithResult: (didPop, _) { - if (!didPop) { + onPopInvoked: (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 11a6b239e9..fb1bda724d 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 @@ -21,15 +21,14 @@ import 'mobile_quick_field_editor.dart'; const mobileSupportedFieldTypes = [ FieldType.RichText, FieldType.Number, + FieldType.URL, FieldType.SingleSelect, FieldType.MultiSelect, FieldType.DateTime, - FieldType.Media, - FieldType.URL, - FieldType.Checkbox, - FieldType.Checklist, FieldType.LastEditedTime, FieldType.CreatedTime, + FieldType.Checkbox, + FieldType.Checklist, // FieldType.Time, ]; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart index 04144241a0..f488608d87 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_picker_list.dart @@ -1,9 +1,10 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.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:go_router/go_router.dart'; @@ -131,9 +132,9 @@ class _FieldButton extends StatelessWidget { return FlowyOptionTile.checkbox( text: field.name, isSelected: isSelected, - leftIcon: FieldIcon( - fieldInfo: field, - dimension: 20, + leftIcon: FlowySvg( + field.fieldType.svgData, + size: const Size.square(20), ), showTopBorder: showTopBorder, onTap: () => onSelect(field.id), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart index f3d71a7f0e..ef8ce6e51d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart @@ -9,10 +9,10 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; @@ -35,7 +35,6 @@ class FieldOptionValues { FieldOptionValues({ required this.type, required this.name, - required this.icon, this.dateFormat, this.timeFormat, this.includeTime, @@ -49,7 +48,6 @@ class FieldOptionValues { return FieldOptionValues( type: fieldType, name: field.name, - icon: field.icon, numberFormat: fieldType == FieldType.Number ? NumberTypeOptionPB.fromBuffer(buffer).format : null, @@ -85,7 +83,6 @@ class FieldOptionValues { FieldType type; String name; - String icon; // FieldType.DateTime // FieldType.LastEditedTime @@ -150,8 +147,6 @@ class FieldOptionValues { timeFormat: timeFormat, includeTime: includeTime, ).writeToBuffer(); - case FieldType.Media: - return MediaTypeOptionPB().writeToBuffer(); default: throw UnimplementedError(); } @@ -224,9 +219,7 @@ class _MobileFieldEditorState extends State { const _Divider(), OptionTextField( controller: controller, - autoFocus: widget.mode == FieldOptionMode.add, - fieldType: values.type, - isPrimary: widget.isPrimary, + type: values.type, onTextChanged: (value) { isFieldNameChanged = true; _updateOptionValues(name: value); @@ -863,8 +856,6 @@ class _SelectOptionListState extends State<_SelectOptionList> { return ListView( shrinkWrap: true, padding: EdgeInsets.zero, - // disable the inner scroll physics, so the outer ListView can scroll - physics: const NeverScrollableScrollPhysics(), children: widget.selectOptions .mapIndexed( (index, option) => _SelectOptionTile( 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 f2b90e9c0d..0b415b04a6 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,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart'; @@ -13,6 +11,7 @@ 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'; @@ -62,8 +61,7 @@ class _QuickEditFieldState extends State { create: (_) => FieldEditorBloc( viewId: widget.viewId, fieldController: widget.fieldController, - fieldInfo: widget.fieldInfo, - isNew: false, + field: widget.fieldInfo.field, ), child: BlocConsumer( listenWhen: (previous, current) => @@ -76,8 +74,7 @@ class _QuickEditFieldState extends State { const VSpace(16), OptionTextField( controller: controller, - isPrimary: state.field.isPrimary, - fieldType: state.field.fieldType, + type: state.field.fieldType, onTextChanged: (text) { context .read() @@ -102,7 +99,7 @@ class _QuickEditFieldState extends State { context.pop(); }, ), - if (!widget.fieldInfo.isPrimary) ...[ + if (!widget.fieldInfo.isPrimary) FlowyOptionTile.text( showTopBorder: false, text: fieldVisibility.isVisibleState() @@ -118,6 +115,7 @@ class _QuickEditFieldState extends State { } }, ), + if (!widget.fieldInfo.isPrimary) FlowyOptionTile.text( showTopBorder: false, text: LocaleKeys.grid_field_insertLeft.tr(), @@ -134,7 +132,6 @@ 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/database_field_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart index 9543a4593b..b5ec0f9d80 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart @@ -8,11 +8,12 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/setting/property_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:collection/collection.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'; @@ -78,12 +79,14 @@ class _MobileDatabaseFieldListBody extends StatelessWidget { viewId: viewId, fieldController: databaseController.fieldController, fieldInfo: field, + index: index, showTopBorder: false, ), ) .toList(); return ReorderableListView.builder( + padding: EdgeInsets.zero, proxyDecorator: (_, index, anim) { final field = fields[index]; return AnimatedBuilder( @@ -100,6 +103,7 @@ class _MobileDatabaseFieldListBody extends StatelessWidget { viewId: viewId, fieldController: databaseController.fieldController, fieldInfo: field, + index: index, showTopBorder: true, ), ), @@ -117,32 +121,43 @@ class _MobileDatabaseFieldListBody extends StatelessWidget { }, header: firstCell, footer: canCreate - ? Padding( - padding: const EdgeInsets.only(top: 20), - child: _NewDatabaseFieldTile(viewId: viewId), + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + _divider(), + _NewDatabaseFieldTile(viewId: viewId), + VSpace( + context.bottomSheetPadding( + ignoreViewPadding: false, + ), + ), + ], ) - : null, + : VSpace( + context.bottomSheetPadding(ignoreViewPadding: false), + ), itemCount: cells.length, itemBuilder: (context, index) => cells[index], - padding: EdgeInsets.only( - bottom: context.bottomSheetPadding(ignoreViewPadding: false), - ), ); }, ), ); } + + Widget _divider() => const VSpace(20); } class DatabaseFieldListTile extends StatelessWidget { const DatabaseFieldListTile({ super.key, + this.index, required this.fieldInfo, required this.viewId, required this.fieldController, required this.showTopBorder, }); + final int? index; final FieldInfo fieldInfo; final String viewId; final FieldController fieldController; @@ -153,20 +168,19 @@ class DatabaseFieldListTile extends StatelessWidget { if (fieldInfo.field.isPrimary) { return FlowyOptionTile.text( text: fieldInfo.name, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - dimension: 20, + leftIcon: FlowySvg( + fieldInfo.fieldType.svgData, + size: const Size.square(20), ), - onTap: () => showEditFieldScreen(context, viewId, fieldInfo), showTopBorder: showTopBorder, ); } else { return FlowyOptionTile.toggle( isSelected: fieldInfo.visibility?.isVisibleState() ?? false, text: fieldInfo.name, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - dimension: 20, + leftIcon: FlowySvg( + fieldInfo.fieldType.svgData, + size: const Size.square(20), ), showTopBorder: showTopBorder, onTap: () => showEditFieldScreen(context, viewId, fieldInfo), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet.dart deleted file mode 100644 index aea68ac27a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet.dart +++ /dev/null @@ -1,1108 +0,0 @@ -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_actions.dart'; -import 'package:appflowy/mobile/presentation/database/view/database_filter_condition_list.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/select_option_loader.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart'; -import 'package:appflowy/util/debounce.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:protobuf/protobuf.dart' hide FieldInfo; -import 'package:time/time.dart'; - -import 'database_filter_bottom_sheet_cubit.dart'; - -class MobileFilterEditor extends StatefulWidget { - const MobileFilterEditor({super.key}); - - @override - State createState() => _MobileFilterEditorState(); -} - -class _MobileFilterEditorState extends State { - final pageController = PageController(); - final scrollController = ScrollController(); - - @override - void dispose() { - pageController.dispose(); - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => MobileFilterEditorCubit( - pageController: pageController, - ), - child: Column( - children: [ - const _Header(), - SizedBox( - height: 400, - child: PageView.builder( - controller: pageController, - itemCount: 2, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return switch (index) { - 0 => _ActiveFilters(scrollController: scrollController), - 1 => const _FilterDetail(), - _ => const SizedBox.shrink(), - }; - }, - ), - ), - ], - ), - ); - } -} - -class _Header extends StatelessWidget { - const _Header(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return SizedBox( - height: 44.0, - child: Stack( - children: [ - if (_isBackButtonShown(state)) - Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - onTap: () => context - .read() - .returnToOverview(), - ), - ), - Align( - child: FlowyText.medium( - LocaleKeys.grid_settings_filter.tr(), - fontSize: 16.0, - ), - ), - if (_isSaveButtonShown(state)) - Align( - alignment: Alignment.centerRight, - child: AppBarSaveButton( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - enable: _isSaveButtonEnabled(state), - onTap: () => _saveOnTapHandler(context, state), - ), - ), - ], - ), - ); - }, - ); - } - - bool _isBackButtonShown(MobileFilterEditorState state) { - return state.maybeWhen( - overview: (_) => false, - orElse: () => true, - ); - } - - bool _isSaveButtonShown(MobileFilterEditorState state) { - return state.maybeWhen( - editCondition: (filterId, newFilter, showSave) => showSave, - editContent: (_, __) => true, - orElse: () => false, - ); - } - - bool _isSaveButtonEnabled(MobileFilterEditorState state) { - return state.maybeWhen( - editCondition: (_, __, enableSave) => enableSave, - editContent: (_, __) => true, - orElse: () => false, - ); - } - - void _saveOnTapHandler(BuildContext context, MobileFilterEditorState state) { - state.maybeWhen( - editCondition: (filterId, newFilter, _) { - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - editContent: (filterId, newFilter) { - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - orElse: () {}, - ); - context.read().returnToOverview(); - } -} - -class _ActiveFilters extends StatelessWidget { - const _ActiveFilters({ - required this.scrollController, - }); - - final ScrollController scrollController; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - child: Column( - children: [ - Expanded( - child: state.filters.isEmpty - ? _emptyBackground(context) - : _filterList(context, state), - ), - const VSpace(12), - const _CreateFilterButton(), - ], - ), - ); - }, - ); - } - - Widget _emptyBackground(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.filter_s, - size: const Size.square(60), - color: Theme.of(context).hintColor, - ), - FlowyText( - LocaleKeys.grid_filter_empty.tr(), - color: Theme.of(context).hintColor, - ), - ], - ), - ); - } - - Widget _filterList(BuildContext context, FilterEditorState state) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().state.maybeWhen( - overview: (scrollToBottom) { - if (scrollToBottom && scrollController.hasClients) { - scrollController - .jumpTo(scrollController.position.maxScrollExtent); - context.read().returnToOverview(); - } - }, - orElse: () {}, - ); - }); - - return ListView.separated( - controller: scrollController, - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - itemCount: state.filters.length, - itemBuilder: (context, index) { - final filter = state.filters[index]; - final field = context - .read() - .state - .fields - .firstWhereOrNull((field) => field.id == filter.fieldId); - return field == null - ? const SizedBox.shrink() - : _FilterItem(filter: filter, field: field); - }, - separatorBuilder: (context, index) => const VSpace(12.0), - ); - } -} - -class _CreateFilterButton extends StatelessWidget { - const _CreateFilterButton(); - - @override - Widget build(BuildContext context) { - return Container( - height: 44, - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 14), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - width: 0.5, - color: Theme.of(context).dividerColor, - ), - ), - borderRadius: Corners.s10Border, - ), - child: InkWell( - onTap: () { - if (context.read().state.fields.isEmpty) { - Fluttertoast.showToast( - msg: LocaleKeys.grid_filter_cannotFindCreatableField.tr(), - gravity: ToastGravity.BOTTOM, - ); - } else { - context.read().startCreatingFilter(); - } - }, - borderRadius: Corners.s10Border, - child: Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.add_s, - size: Size.square(16), - ), - const HSpace(6.0), - FlowyText( - LocaleKeys.grid_filter_addFilter.tr(), - fontSize: 15, - ), - ], - ), - ), - ), - ); - } -} - -class _FilterItem extends StatelessWidget { - const _FilterItem({ - required this.filter, - required this.field, - }); - - final DatabaseFilter filter; - final FieldInfo field; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).hoverColor, - borderRadius: BorderRadius.circular(12), - ), - child: Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 14, - horizontal: 8, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: FlowyText.medium( - LocaleKeys.grid_filter_where.tr(), - fontSize: 15, - ), - ), - const VSpace(10), - Row( - children: [ - Expanded( - child: FilterItemInnerButton( - onTap: () => context - .read() - .startEditingFilterField(filter.filterId), - icon: field.fieldType.svgData, - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const HSpace(6), - Expanded( - child: FilterItemInnerButton( - onTap: () => context - .read() - .startEditingFilterCondition( - filter.filterId, - filter, - filter.fieldType == FieldType.DateTime, - ), - child: FlowyText( - filter.conditionName, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - if (filter.canAttachContent) ...[ - const VSpace(6), - filter.getMobileDescription( - field, - onExpand: () => context - .read() - .startEditingFilterContent(filter.filterId, filter), - onUpdate: (newFilter) => context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)), - ), - ], - ], - ), - ), - Positioned( - right: 8, - top: 6, - child: _deleteButton(context), - ), - ], - ), - ); - } - - Widget _deleteButton(BuildContext context) { - return InkWell( - onTap: () => context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)), - // steal from the container LongClickReorderWidget thing - onLongPress: () {}, - borderRadius: BorderRadius.circular(10), - child: SizedBox.square( - dimension: 34, - child: Center( - child: FlowySvg( - FlowySvgs.trash_m, - size: const Size.square(18), - color: Theme.of(context).hintColor, - ), - ), - ), - ); - } -} - -class FilterItemInnerButton extends StatelessWidget { - const FilterItemInnerButton({ - super.key, - required this.onTap, - required this.child, - this.icon, - }); - - final VoidCallback onTap; - final FlowySvgData? icon; - final Widget child; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Container( - height: 44, - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - width: 0.5, - color: Theme.of(context).dividerColor, - ), - ), - borderRadius: Corners.s10Border, - color: Theme.of(context).colorScheme.surface, - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Center( - child: SeparatedRow( - separatorBuilder: () => const HSpace(6.0), - children: [ - if (icon != null) - FlowySvg( - icon!, - size: const Size.square(16), - ), - Expanded(child: child), - FlowySvg( - FlowySvgs.icon_right_small_ccm_outlined_s, - size: const Size.square(14), - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ), - ); - } -} - -class FilterItemInnerTextField extends StatefulWidget { - const FilterItemInnerTextField({ - super.key, - required this.content, - required this.enabled, - required this.onSubmitted, - }); - - final String content; - final bool enabled; - final void Function(String) onSubmitted; - - @override - State createState() => - _FilterItemInnerTextFieldState(); -} - -class _FilterItemInnerTextFieldState extends State { - late final TextEditingController textController = - TextEditingController(text: widget.content); - final FocusNode focusNode = FocusNode(); - final Debounce debounce = Debounce(duration: 300.milliseconds); - - @override - void dispose() { - focusNode.dispose(); - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44, - child: TextField( - enabled: widget.enabled, - focusNode: focusNode, - controller: textController, - onSubmitted: widget.onSubmitted, - onChanged: (value) => debounce.call(() => widget.onSubmitted(value)), - onTapOutside: (_) => focusNode.unfocus(), - decoration: InputDecoration( - filled: true, - fillColor: widget.enabled - ? Theme.of(context).colorScheme.surface - : Theme.of(context).disabledColor, - enabledBorder: _getBorder(Theme.of(context).dividerColor), - border: _getBorder(Theme.of(context).dividerColor), - focusedBorder: _getBorder(Theme.of(context).colorScheme.primary), - contentPadding: const EdgeInsets.symmetric(horizontal: 12), - ), - ), - ); - } - - InputBorder _getBorder(Color color) { - return OutlineInputBorder( - borderSide: BorderSide( - width: 0.5, - color: color, - ), - borderRadius: Corners.s10Border, - ); - } -} - -class _FilterDetail extends StatelessWidget { - const _FilterDetail(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - create: () { - return _FilterableFieldList( - onSelectField: (field) { - context - .read() - .add(FilterEditorEvent.createFilter(field)); - context.read().returnToOverview( - scrollToBottom: true, - ); - }, - ); - }, - editField: (filterId) { - return _FilterableFieldList( - onSelectField: (field) { - final filter = context - .read() - .state - .filters - .firstWhereOrNull((filter) => filter.filterId == filterId); - if (filter != null && field.id != filter.fieldId) { - context.read().add( - FilterEditorEvent.changeFilteringField(filterId, field), - ); - } - context.read().returnToOverview(); - }, - ); - }, - editCondition: (filterId, newFilter, showSave) { - return _FilterConditionList( - filterId: filterId, - onSelect: (newFilter) { - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - context.read().returnToOverview(); - }, - ); - }, - editContent: (filterId, filter) { - return _FilterContentEditor( - filter: filter, - onUpdateFilter: (newFilter) { - context.read().updateFilter(newFilter); - }, - ); - }, - orElse: () => const SizedBox.shrink(), - ); - }, - ); - } -} - -class _FilterableFieldList extends StatelessWidget { - const _FilterableFieldList({ - required this.onSelectField, - }); - - final void Function(FieldInfo field) onSelectField; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyText( - LocaleKeys.grid_settings_filterBy.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - const VSpace(4.0), - const Divider( - height: 0.5, - thickness: 0.5, - ), - Expanded( - child: BlocBuilder( - builder: (context, blocState) { - return ListView.builder( - itemCount: blocState.fields.length, - itemBuilder: (context, index) { - return FlowyOptionTile.text( - text: blocState.fields[index].name, - leftIcon: FieldIcon( - fieldInfo: blocState.fields[index], - ), - showTopBorder: false, - onTap: () => onSelectField(blocState.fields[index]), - ); - }, - ); - }, - ), - ), - ], - ); - } -} - -class _FilterConditionList extends StatelessWidget { - const _FilterConditionList({ - required this.filterId, - required this.onSelect, - }); - - final String filterId; - final void Function(DatabaseFilter filter) onSelect; - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.filters.firstWhereOrNull( - (filter) => filter.filterId == filterId, - ), - builder: (context, filter) { - if (filter == null) { - return const SizedBox.shrink(); - } - - if (filter is DateTimeFilter?) { - return _DateTimeFilterConditionList( - onSelect: (filter) { - if (filter.fieldType == FieldType.DateTime) { - context.read().updateFilter(filter); - } else { - onSelect(filter); - } - }, - ); - } - - final conditions = - FilterCondition.fromFieldType(filter.fieldType).conditions; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyText( - LocaleKeys.grid_filter_conditon.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - const VSpace(4.0), - const Divider( - height: 0.5, - thickness: 0.5, - ), - Expanded( - child: ListView.builder( - itemCount: conditions.length, - itemBuilder: (context, index) { - return FlowyOptionTile.checkbox( - text: conditions[index].$2, - showTopBorder: false, - isSelected: _isSelected(filter, conditions[index].$1), - onTap: () { - final newFilter = - _updateCondition(filter, conditions[index].$1); - onSelect(newFilter); - }, - ); - }, - ), - ), - ], - ); - }, - ); - } - - bool _isSelected(DatabaseFilter filter, ProtobufEnum condition) { - return switch (filter.fieldType) { - FieldType.RichText || - FieldType.URL => - (filter as TextFilter).condition == condition, - FieldType.Number => (filter as NumberFilter).condition == condition, - FieldType.SingleSelect || - FieldType.MultiSelect => - (filter as SelectOptionFilter).condition == condition, - FieldType.Checkbox => (filter as CheckboxFilter).condition == condition, - FieldType.Checklist => (filter as ChecklistFilter).condition == condition, - _ => false, - }; - } - - DatabaseFilter _updateCondition( - DatabaseFilter filter, - ProtobufEnum condition, - ) { - return switch (filter.fieldType) { - FieldType.RichText || FieldType.URL => (filter as TextFilter) - .copyWith(condition: condition as TextFilterConditionPB), - FieldType.Number => (filter as NumberFilter) - .copyWith(condition: condition as NumberFilterConditionPB), - FieldType.SingleSelect || - FieldType.MultiSelect => - (filter as SelectOptionFilter) - .copyWith(condition: condition as SelectOptionFilterConditionPB), - FieldType.Checkbox => (filter as CheckboxFilter) - .copyWith(condition: condition as CheckboxFilterConditionPB), - FieldType.Checklist => (filter as ChecklistFilter) - .copyWith(condition: condition as ChecklistFilterConditionPB), - _ => filter, - }; - } -} - -class _DateTimeFilterConditionList extends StatelessWidget { - const _DateTimeFilterConditionList({ - required this.onSelect, - }); - - final void Function(DatabaseFilter) onSelect; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - orElse: () => const SizedBox.shrink(), - editCondition: (filterId, newFilter, _) { - final filter = newFilter as DateTimeFilter; - final conditions = - DateTimeFilterCondition.availableConditionsForFieldType( - filter.fieldType, - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4.0), - if (filter.fieldType == FieldType.DateTime) - _DateTimeFilterIsStartSelector( - isStart: filter.condition.isStart, - onSelect: (newValue) { - final newFilter = filter.copyWithCondition( - isStart: newValue, - condition: filter.condition.toCondition(), - ); - onSelect(newFilter); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyText( - LocaleKeys.grid_filter_conditon.tr().toUpperCase(), - fontSize: 13, - color: Theme.of(context).hintColor, - ), - ), - const VSpace(4.0), - const Divider( - height: 0.5, - thickness: 0.5, - ), - Expanded( - child: ListView.builder( - itemCount: conditions.length, - itemBuilder: (context, index) { - return FlowyOptionTile.checkbox( - text: conditions[index].filterName, - showTopBorder: false, - isSelected: - filter.condition.toCondition() == conditions[index], - onTap: () { - final newFilter = filter.copyWithCondition( - isStart: filter.condition.isStart, - condition: conditions[index], - ); - onSelect(newFilter); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } -} - -class _DateTimeFilterIsStartSelector extends StatelessWidget { - const _DateTimeFilterIsStartSelector({ - required this.isStart, - required this.onSelect, - }); - - final bool isStart; - final void Function(bool isStart) onSelect; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 20), - child: DefaultTabController( - length: 2, - initialIndex: isStart ? 0 : 1, - child: Container( - padding: const EdgeInsets.all(3.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).hoverColor, - ), - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - labelPadding: EdgeInsets.zero, - padding: EdgeInsets.zero, - indicatorWeight: 0, - indicator: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Theme.of(context).colorScheme.surface, - ), - splashFactory: NoSplash.splashFactory, - overlayColor: const WidgetStatePropertyAll( - Colors.transparent, - ), - onTap: (index) => onSelect(index == 0), - tabs: [ - _tab(LocaleKeys.grid_dateFilter_startDate.tr()), - _tab(LocaleKeys.grid_dateFilter_endDate.tr()), - ], - ), - ), - ), - ); - } - - Tab _tab(String name) { - return Tab( - height: 34, - child: Center( - child: FlowyText( - name, - fontSize: 14, - ), - ), - ); - } -} - -class _FilterContentEditor extends StatelessWidget { - const _FilterContentEditor({ - required this.filter, - required this.onUpdateFilter, - }); - - final DatabaseFilter filter; - final void Function(DatabaseFilter) onUpdateFilter; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final field = state.fields - .firstWhereOrNull((field) => field.id == filter.fieldId); - if (field == null) return const SizedBox.shrink(); - return switch (field.fieldType) { - FieldType.SingleSelect || - FieldType.MultiSelect => - _SelectOptionFilterContentEditor( - filter: filter as SelectOptionFilter, - field: field, - ), - FieldType.CreatedTime || - FieldType.LastEditedTime || - FieldType.DateTime => - _DateTimeFilterContentEditor(filter: filter as DateTimeFilter), - _ => const SizedBox.shrink(), - }; - }, - ); - } -} - -class _SelectOptionFilterContentEditor extends StatefulWidget { - _SelectOptionFilterContentEditor({ - required this.filter, - required this.field, - }) : delegate = filter.makeDelegate(field); - - final SelectOptionFilter filter; - final FieldInfo field; - final SelectOptionFilterDelegate delegate; - - @override - State<_SelectOptionFilterContentEditor> createState() => - _SelectOptionFilterContentEditorState(); -} - -class _SelectOptionFilterContentEditorState - extends State<_SelectOptionFilterContentEditor> { - final TextEditingController textController = TextEditingController(); - String filterText = ""; - final List options = []; - - @override - void initState() { - super.initState(); - options.addAll(widget.delegate.getOptions(widget.field)); - } - - @override - void dispose() { - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - const Divider( - height: 0.5, - thickness: 0.5, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: FlowyMobileSearchTextField( - controller: textController, - onChanged: (text) { - if (textController.value.composing.isCollapsed) { - setState(() { - filterText = text; - filterOptions(); - }); - } - }, - onSubmitted: (_) {}, - hintText: LocaleKeys.grid_selectOption_searchOption.tr(), - ), - ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - separatorBuilder: (context, index) => const VSpace(20), - itemCount: options.length, - itemBuilder: (context, index) { - return MobileSelectOption( - option: options[index], - isSelected: widget.filter.optionIds.contains(options[index].id), - onTap: (isSelected) { - _onTapHandler( - context, - options, - options[index], - isSelected, - ); - }, - indicator: MobileSelectedOptionIndicator.multi, - showMoreOptionsButton: false, - ); - }, - ), - ), - ], - ); - } - - void filterOptions() { - options - ..clear() - ..addAll(widget.delegate.getOptions(widget.field)); - - if (filterText.isNotEmpty) { - options.retainWhere((option) { - final name = option.name.toLowerCase(); - final lFilter = filterText.toLowerCase(); - return name.contains(lFilter); - }); - } - } - - void _onTapHandler( - BuildContext context, - List options, - SelectOptionPB option, - bool isSelected, - ) { - final selectedOptionIds = Set.from(widget.filter.optionIds); - if (isSelected) { - selectedOptionIds.remove(option.id); - } else { - selectedOptionIds.add(option.id); - } - _updateSelectOptions(context, options, selectedOptionIds); - } - - void _updateSelectOptions( - BuildContext context, - List options, - Set selectedOptionIds, - ) { - final optionIds = - options.map((e) => e.id).where(selectedOptionIds.contains).toList(); - final newFilter = widget.filter.copyWith(optionIds: optionIds); - context.read().updateFilter(newFilter); - } -} - -class _DateTimeFilterContentEditor extends StatefulWidget { - const _DateTimeFilterContentEditor({ - required this.filter, - }); - - final DateTimeFilter filter; - - @override - State<_DateTimeFilterContentEditor> createState() => - _DateTimeFilterContentEditorState(); -} - -class _DateTimeFilterContentEditorState - extends State<_DateTimeFilterContentEditor> { - late DateTime focusedDay; - - bool get isRange => widget.filter.condition.isRange; - - @override - void initState() { - super.initState(); - focusedDay = (isRange ? widget.filter.start : widget.filter.timestamp) ?? - DateTime.now(); - } - - @override - Widget build(BuildContext context) { - return MobileDatePicker( - isRange: isRange, - selectedDay: isRange ? widget.filter.start : widget.filter.timestamp, - startDay: isRange ? widget.filter.start : null, - endDay: isRange ? widget.filter.end : null, - focusedDay: focusedDay, - onDaySelected: (selectedDay) { - final newFilter = isRange - ? widget.filter.copyWithRange(start: selectedDay, end: null) - : widget.filter.copyWithTimestamp(timestamp: selectedDay); - context.read().updateFilter(newFilter); - }, - onRangeSelected: (start, end) { - final newFilter = widget.filter.copyWithRange( - start: start, - end: end, - ); - context.read().updateFilter(newFilter); - }, - onPageChanged: (focusedDay) { - setState(() => this.focusedDay = focusedDay); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet_cubit.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet_cubit.dart deleted file mode 100644 index a62ef846ae..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_bottom_sheet_cubit.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'database_filter_bottom_sheet_cubit.freezed.dart'; - -class MobileFilterEditorCubit extends Cubit { - MobileFilterEditorCubit({ - required this.pageController, - }) : super(MobileFilterEditorState.overview()); - - final PageController pageController; - - void returnToOverview({bool scrollToBottom = false}) { - _animateToPage(0); - emit(MobileFilterEditorState.overview(scrollToBottom: scrollToBottom)); - } - - void startCreatingFilter() { - _animateToPage(1); - emit(MobileFilterEditorState.create()); - } - - void startEditingFilterField(String filterId) { - _animateToPage(1); - emit(MobileFilterEditorState.editField(filterId: filterId)); - } - - void updateFilter(DatabaseFilter filter) { - emit( - state.maybeWhen( - editCondition: (filterId, newFilter, showSave) => - MobileFilterEditorState.editCondition( - filterId: filterId, - newFilter: filter, - showSave: showSave, - ), - editContent: (filterId, _) => MobileFilterEditorState.editContent( - filterId: filterId, - newFilter: filter, - ), - orElse: () => state, - ), - ); - } - - void startEditingFilterCondition( - String filterId, - DatabaseFilter filter, - bool showSave, - ) { - _animateToPage(1); - emit( - MobileFilterEditorState.editCondition( - filterId: filterId, - newFilter: filter, - showSave: showSave, - ), - ); - } - - void startEditingFilterContent(String filterId, DatabaseFilter filter) { - _animateToPage(1); - emit( - MobileFilterEditorState.editContent( - filterId: filterId, - newFilter: filter, - ), - ); - } - - Future _animateToPage(int page) async { - return pageController.animateToPage( - page, - duration: const Duration(milliseconds: 150), - curve: Curves.easeOut, - ); - } -} - -@freezed -class MobileFilterEditorState with _$MobileFilterEditorState { - factory MobileFilterEditorState.overview({ - @Default(false) bool scrollToBottom, - }) = _OverviewState; - - factory MobileFilterEditorState.create() = _CreateState; - - factory MobileFilterEditorState.editField({ - required String filterId, - }) = _EditFieldState; - - factory MobileFilterEditorState.editCondition({ - required String filterId, - required DatabaseFilter newFilter, - required bool showSave, - }) = _EditConditionState; - - factory MobileFilterEditorState.editContent({ - required String filterId, - required DatabaseFilter newFilter, - }) = _EditContentState; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_condition_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_condition_list.dart deleted file mode 100644 index d3d0b9bcdd..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_filter_condition_list.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -abstract class FilterCondition { - static FilterCondition fromFieldType(FieldType fieldType) { - return switch (fieldType) { - FieldType.RichText || FieldType.URL => TextFilterCondition().as(), - FieldType.Number => NumberFilterCondition().as(), - FieldType.Checkbox => CheckboxFilterCondition().as(), - FieldType.Checklist => ChecklistFilterCondition().as(), - FieldType.SingleSelect => SingleSelectOptionFilterCondition().as(), - FieldType.MultiSelect => MultiSelectOptionFilterCondition().as(), - _ => MultiSelectOptionFilterCondition().as(), - }; - } - - List<(C, String)> get conditions; -} - -mixin _GenericCastHelper { - FilterCondition as() => this as FilterCondition; -} - -final class TextFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(TextFilterConditionPB, String)> get conditions { - return [ - TextFilterConditionPB.TextContains, - TextFilterConditionPB.TextDoesNotContain, - TextFilterConditionPB.TextIs, - TextFilterConditionPB.TextIsNot, - TextFilterConditionPB.TextStartsWith, - TextFilterConditionPB.TextEndsWith, - TextFilterConditionPB.TextIsEmpty, - TextFilterConditionPB.TextIsNotEmpty, - ].map((e) => (e, e.filterName)).toList(); - } -} - -final class NumberFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(NumberFilterConditionPB, String)> get conditions { - return [ - NumberFilterConditionPB.Equal, - NumberFilterConditionPB.NotEqual, - NumberFilterConditionPB.LessThan, - NumberFilterConditionPB.LessThanOrEqualTo, - NumberFilterConditionPB.GreaterThan, - NumberFilterConditionPB.GreaterThanOrEqualTo, - NumberFilterConditionPB.NumberIsEmpty, - NumberFilterConditionPB.NumberIsNotEmpty, - ].map((e) => (e, e.filterName)).toList(); - } -} - -final class CheckboxFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(CheckboxFilterConditionPB, String)> get conditions { - return [ - CheckboxFilterConditionPB.IsChecked, - CheckboxFilterConditionPB.IsUnChecked, - ].map((e) => (e, e.filterName)).toList(); - } -} - -final class ChecklistFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(ChecklistFilterConditionPB, String)> get conditions { - return [ - ChecklistFilterConditionPB.IsComplete, - ChecklistFilterConditionPB.IsIncomplete, - ].map((e) => (e, e.filterName)).toList(); - } -} - -final class SingleSelectOptionFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(SelectOptionFilterConditionPB, String)> get conditions { - return [ - SelectOptionFilterConditionPB.OptionIs, - SelectOptionFilterConditionPB.OptionIsNot, - SelectOptionFilterConditionPB.OptionIsEmpty, - SelectOptionFilterConditionPB.OptionIsNotEmpty, - ].map((e) => (e, e.i18n)).toList(); - } -} - -final class MultiSelectOptionFilterCondition - with _GenericCastHelper - implements FilterCondition { - @override - List<(SelectOptionFilterConditionPB, String)> get conditions { - return [ - SelectOptionFilterConditionPB.OptionContains, - SelectOptionFilterConditionPB.OptionDoesNotContain, - SelectOptionFilterConditionPB.OptionIsEmpty, - SelectOptionFilterConditionPB.OptionIsNotEmpty, - ].map((e) => (e, e.i18n)).toList(); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart index 009468a8f1..8dd224390c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart @@ -2,13 +2,9 @@ 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_actions.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -143,7 +139,7 @@ class _Overview extends StatelessWidget { return Column( children: [ Expanded( - child: state.sorts.isEmpty + child: state.sortInfos.isEmpty ? Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -171,10 +167,10 @@ class _Overview extends StatelessWidget { onReorder: (oldIndex, newIndex) => context .read() .add(SortEditorEvent.reorderSort(oldIndex, newIndex)), - itemCount: state.sorts.length, + itemCount: state.sortInfos.length, itemBuilder: (context, index) => _SortItem( key: ValueKey("sort_item_$index"), - sort: state.sorts[index], + sort: state.sortInfos[index], ), ), ), @@ -236,7 +232,7 @@ class _Overview extends StatelessWidget { class _SortItem extends StatelessWidget { const _SortItem({super.key, required this.sort}); - final DatabaseSort sort; + final SortInfo sort; @override Widget build(BuildContext context) { @@ -292,18 +288,9 @@ class _SortItem extends StatelessWidget { child: Row( children: [ Expanded( - child: BlocSelector( - selector: (state) => - state.allFields.firstWhereOrNull( - (field) => field.id == sort.fieldId, - ), - builder: (context, field) { - return FlowyText( - field?.name ?? "", - overflow: TextOverflow.ellipsis, - ); - }, + child: FlowyText( + sort.fieldInfo.name, + overflow: TextOverflow.ellipsis, ), ), const HSpace(6.0), @@ -340,7 +327,7 @@ class _SortItem extends StatelessWidget { children: [ Expanded( child: FlowyText( - sort.condition.name, + sort.sortPB.condition.name, ), ), const HSpace(6.0), @@ -362,11 +349,11 @@ class _SortItem extends StatelessWidget { ), Positioned( right: 8, - top: 6, + top: 9, child: InkWell( onTap: () => context .read() - .add(SortEditorEvent.deleteSort(sort.sortId)), + .add(SortEditorEvent.deleteSort(sort)), // steal from the container LongClickReorderWidget thing onLongPress: () {}, borderRadius: BorderRadius.circular(10), @@ -398,14 +385,14 @@ class _SortDetail extends StatelessWidget { return isCreatingNewSort ? const _SortDetailContent() - : BlocSelector( - selector: (state) => state.sorts.firstWhere( - (sort) => - sort.sortId == + : BlocSelector( + selector: (state) => state.sortInfos.firstWhere( + (sortInfo) => + sortInfo.sortId == context.read().state.editingSortId, ), - builder: (context, sort) { - return _SortDetailContent(sort: sort); + builder: (context, sortInfo) { + return _SortDetailContent(sortInfo: sortInfo); }, ); } @@ -413,12 +400,12 @@ class _SortDetail extends StatelessWidget { class _SortDetailContent extends StatelessWidget { const _SortDetailContent({ - this.sort, + this.sortInfo, }); - final DatabaseSort? sort; + final SortInfo? sortInfo; - bool get isCreatingNewSort => sort == null; + bool get isCreatingNewSort => sortInfo == null; @override Widget build(BuildContext context) { @@ -432,7 +419,7 @@ class _SortDetailContent extends StatelessWidget { length: 2, initialIndex: isCreatingNewSort ? 0 - : sort!.condition == SortConditionPB.Ascending + : sortInfo!.sortPB.condition == SortConditionPB.Ascending ? 0 : 1, child: Container( @@ -502,7 +489,7 @@ class _SortDetailContent extends StatelessWidget { child: BlocBuilder( builder: (context, state) { final fields = state.allFields - .where((field) => field.fieldType.canCreateSort) + .where((field) => field.canCreateSort || field.hasSort) .toList(); return ListView.builder( itemCount: fields.length, @@ -514,19 +501,14 @@ class _SortDetailContent extends StatelessWidget { .state .newSortFieldId == fieldInfo.id - : sort!.fieldId == fieldInfo.id; + : sortInfo!.fieldId == fieldInfo.id; - final canSort = - fieldInfo.fieldType.canCreateSort && !fieldInfo.hasSort; - final beingEdited = - !isCreatingNewSort && sort!.fieldId == fieldInfo.id; - final enabled = canSort || beingEdited; + final enabled = fieldInfo.canCreateSort || + isCreatingNewSort && !fieldInfo.hasSort || + !isCreatingNewSort && sortInfo!.fieldId == fieldInfo.id; return FlowyOptionTile.checkbox( text: fieldInfo.field.name, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - ), isSelected: isSelected, textColor: enabled ? null : Theme.of(context).disabledColor, showTopBorder: false, @@ -559,7 +541,7 @@ class _SortDetailContent extends StatelessWidget { } else { context.read().add( SortEditorEvent.editSort( - sortId: sort!.sortId, + sortId: sortInfo!.sortId, condition: newCondition, ), ); @@ -572,7 +554,7 @@ class _SortDetailContent extends StatelessWidget { } else { context.read().add( SortEditorEvent.editSort( - sortId: sort!.sortId, + sortId: sortInfo!.sortId, fieldId: newFieldId, ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart index f7ad313412..763da36918 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart @@ -5,8 +5,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -165,20 +163,9 @@ class MobileDatabaseViewListButton extends StatelessWidget { } Widget _buildViewIconButton(BuildContext context, ViewPB view) { - final iconData = view.icon.toEmojiIconData(); - Widget icon; - if (iconData.isEmpty || iconData.type != FlowyIconType.icon) { - icon = view.defaultIcon(); - } else { - icon = RawEmojiIconWidget( - emoji: iconData, - emojiSize: 14.0, - enableColor: false, - ); - } return SizedBox.square( dimension: 20.0, - child: icon, + child: view.defaultIcon(), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart index a133739a9d..652c93496e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -1,16 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -53,46 +48,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { context.pop(); } }), - const MobileQuickActionDivider(), - _actionButton( - context, - _Action.changeIcon, - () { - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - showHeader: true, - title: LocaleKeys.titleBar_pageIcon.tr(), - backgroundColor: AFThemeExtension.of(context).background, - enableDraggableScrollable: true, - minChildSize: 0.6, - initialChildSize: 0.61, - scrollableWidgetBuilder: (_, controller) { - return Expanded( - child: FlowyIconEmojiPicker( - tabs: const [PickerTabType.icon], - enableBackgroundColorSelection: false, - onSelectedEmoji: (r) { - ViewBackendService.updateViewIcon( - view: view, - viewIcon: r.data, - ); - Navigator.pop(context); - }, - ), - ); - }, - builder: (_) => const SizedBox.shrink(), - ).then((_) { - if (context.mounted) { - Navigator.pop(context); - } - }); - }, - !isInline, - ), - const MobileQuickActionDivider(), + _divider(), _actionButton( context, _Action.duplicate, @@ -102,7 +58,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { }, !isInline, ), - const MobileQuickActionDivider(), + _divider(), _actionButton( context, _Action.delete, @@ -112,6 +68,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { }, !isInline, ), + _divider(), ], ); } @@ -131,20 +88,20 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { enable: enable, ); } + + Widget _divider() => const Divider(height: 8.5, thickness: 0.5); } enum _Action { edit, - changeIcon, - delete, - duplicate; + duplicate, + delete; String get label { return switch (this) { edit => LocaleKeys.grid_settings_editView.tr(), duplicate => LocaleKeys.button_duplicate.tr(), delete => LocaleKeys.button_delete.tr(), - changeIcon => LocaleKeys.disclosureAction_changeIcon.tr(), }; } @@ -153,7 +110,6 @@ enum _Action { edit => FlowySvgs.view_item_rename_s, duplicate => FlowySvgs.duplicate_s, delete => FlowySvgs.trash_s, - changeIcon => FlowySvgs.change_icon_s, }; } 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 f5812541c8..4d8acbbeba 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.calendar_s, + calendar => FlowySvgs.date_s, duplicate => FlowySvgs.copy_s, delete => FlowySvgs.delete_s, }; @@ -176,7 +176,6 @@ 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 373558a480..14c4e022ae 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 @@ -1,5 +1,4 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; @@ -8,27 +7,15 @@ class MobileDocumentScreen extends StatelessWidget { super.key, required this.id, this.title, - this.showMoreButton = true, - this.fixedTitle, - this.blockId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); /// view id final String id; final String? title; - final bool showMoreButton; - final String? fixedTitle; - final String? blockId; - final List tabs; static const routeName = '/docs'; static const viewId = 'id'; static const viewTitle = 'title'; - static const viewShowMoreButton = 'show_more_button'; - static const viewFixedTitle = 'fixed_title'; - static const viewBlockId = 'block_id'; - static const viewSelectTabs = 'select_tabs'; @override Widget build(BuildContext context) { @@ -36,10 +23,6 @@ class MobileDocumentScreen extends StatelessWidget { id: id, title: title, viewLayout: ViewLayoutPB.Document, - showMoreButton: showMoreButton, - fixedTitle: fixedTitle, - blockId: blockId, - tabs: tabs, ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index 0e7a7cb4c6..e6d2d895b1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -31,9 +31,9 @@ class MobileFavoriteScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final latest = snapshots.data?[0].fold( - (latest) { - return latest as WorkspaceLatestPB?; + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) { + return workspaceSettingPB as WorkspaceSettingPB?; }, (error) => null, ); @@ -46,7 +46,7 @@ class MobileFavoriteScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (latest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return const WorkspaceFailedScreen(); } 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 6282421109..ded486983e 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,34 +96,38 @@ class _FavoriteViews extends StatelessWidget { final borderColor = Theme.of(context).isLightMode ? const Color(0xFFE9E9EC) : const Color(0x1AFFFFFF); - 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, + 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, + ), ), ), - ), - 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 1efee460eb..57ac43f255 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,7 +72,6 @@ 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 5651379522..965c396d42 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,17 +25,21 @@ class _MobileHomeSpaceState extends State final workspaceId = context.read().state.currentWorkspace?.workspaceId ?? ''; - 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, + 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, + ), ), ), ); 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 0013650df9..64ad7e4cd1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,6 +1,10 @@ +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'; @@ -11,6 +15,7 @@ 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 { @@ -27,54 +32,73 @@ class MobileFolders extends StatelessWidget { @override Widget build(BuildContext context) { - 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), - ], + 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(), + ], + ), + ); + }, ), - ); - }, + ), + ), ); } @@ -113,3 +137,28 @@ class _MobileFolderState extends State<_MobileFolder> { ]; } } + +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 345a4591d1..1f3ec66dec 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,31 +1,23 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; +import 'dart:io'; + import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart'; -import 'package:appflowy/shared/loading.dart'; import 'package:appflowy/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'; -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'; class MobileHomeScreen extends StatelessWidget { const MobileHomeScreen({super.key}); @@ -44,9 +36,9 @@ class MobileHomeScreen extends StatelessWidget { return const Center(child: CircularProgressIndicator.adaptive()); } - final workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) { - return workspaceLatestPB as WorkspaceLatestPB?; + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) { + return workspaceSettingPB as WorkspaceSettingPB?; }, (error) => null, ); @@ -59,18 +51,10 @@ class MobileHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceLatest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return const WorkspaceFailedScreen(); } - Sentry.configureScope( - (scope) => scope.setUser( - SentryUser( - id: userProfile.id.toString(), - ), - ), - ); - return Scaffold( body: SafeArea( bottom: false, @@ -78,7 +62,7 @@ class MobileHomeScreen extends StatelessWidget { value: userProfile, child: MobileHomePage( userProfile: userProfile, - workspaceLatest: workspaceLatest, + workspaceSetting: workspaceSetting, ), ), ), @@ -88,38 +72,31 @@ class MobileHomeScreen extends StatelessWidget { } } -final PropertyValueNotifier mCurrentWorkspace = - PropertyValueNotifier(null); - class MobileHomePage extends StatefulWidget { const MobileHomePage({ super.key, required this.userProfile, - required this.workspaceLatest, + required this.workspaceSetting, }); final UserProfilePB userProfile; - final WorkspaceLatestPB workspaceLatest; + final WorkspaceSettingPB workspaceSetting; @override State createState() => _MobileHomePageState(); } 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(); } @@ -135,201 +112,52 @@ class _MobileHomePageState extends State { create: (context) => FavoriteBloc()..add(const FavoriteEvent.initial()), ), - BlocProvider.value( - value: getIt()..add(const ReminderEvent.started()), - ), ], - child: _HomePage(userProfile: widget.userProfile), + 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, + ), + ), + + Expanded( + child: BlocProvider( + create: (context) => + SpaceOrderBloc()..add(const SpaceOrderEvent.initial()), + child: MobileSpaceTab( + userProfile: widget.userProfile, + ), + ), + ), + ], + ); + }, + ), ); } void _onLatestViewChange() async { final id = getIt().latestOpenView?.id; - if (id == null || id.isEmpty) { + 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: 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( - userProfile: widget.userProfile, - workspaceId: workspaceId, - )..add( - const SpaceEvent.initial( - openFirstPage: false, - ), - ), - ), - ], - child: MobileSpaceTab( - userProfile: widget.userProfile, - ), - ), - ), - ], - ); - }, - ); - } - - void _showResultDialog(BuildContext context, UserWorkspaceState state) { - final actionResult = state.actionResult; - if (actionResult == null) { - return; - } - - 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.onFailure((e) { - toastType = ToastificationType.error; - return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}'; - }); - break; - case UserWorkspaceActionType.delete: - message = result.fold( - (s) { - toastType = ToastificationType.success; - return LocaleKeys.workspace_deleteSuccess.tr(); - }, - (e) { - toastType = ToastificationType.error; - return '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}'; - }, - ); - break; - case UserWorkspaceActionType.leave: - message = result.fold( - (s) { - toastType = ToastificationType.success; - return LocaleKeys - .settings_workspacePage_leaveWorkspacePrompt_success - .tr(); - }, - (e) { - toastType = ToastificationType.error; - return '${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_fail.tr()}: ${e.msg}'; - }, - ); - break; - case UserWorkspaceActionType.rename: - message = result.fold( - (s) { - toastType = ToastificationType.success; - return LocaleKeys.workspace_renameSuccess.tr(); - }, - (e) { - toastType = ToastificationType.error; - return '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}'; - }, - ); - break; - default: - message = null; - toastType = ToastificationType.error; - break; - } - - if (message != null) { - showToastNotification(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 113f12e543..28d0915aef 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,11 +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/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/plugins/base/icon/icon_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'; @@ -18,8 +17,6 @@ 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, @@ -47,10 +44,15 @@ class MobileHomePageHeader extends StatelessWidget { ? _MobileWorkspace(userProfile: userProfile) : _MobileUser(userProfile: userProfile), ), - HomePageSettingsPopupMenu( - userProfile: userProfile, + GestureDetector( + onTap: () => context.push( + MobileHomeSettingPage.routeName, + ), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: FlowySvg(FlowySvgs.m_setting_m), + ), ), - const HSpace(8.0), ], ), ); @@ -111,10 +113,8 @@ class _MobileWorkspace extends StatelessWidget { if (currentWorkspace == null) { return const SizedBox.shrink(); } - return AnimatedGestureDetector( - scaleFactor: 0.99, - alignment: Alignment.centerLeft, - onTapUp: () { + return GestureDetector( + onTap: () { context.read().add( const UserWorkspaceEvent.fetchWorkspaces(), ); @@ -122,32 +122,29 @@ class _MobileWorkspace extends StatelessWidget { }, child: Row( children: [ - WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 36, - fontSize: 18.0, - enableEdit: true, - alignment: Alignment.centerLeft, - figmaLineHeight: 26.0, - emojiSize: 24.0, - borderRadius: 12.0, - showBorder: false, - onSelected: (result) => context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - currentWorkspace.workspaceId, - result.emoji, + SizedBox.square( + dimension: currentWorkspace.icon.isNotEmpty ? 34.0 : 26.0, + child: WorkspaceIcon( + workspace: currentWorkspace, + iconSize: 26, + fontSize: 16.0, + enableEdit: false, + alignment: Alignment.centerLeft, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + currentWorkspace.workspaceId, + result.emoji, + ), ), - ), + ), ), currentWorkspace.icon.isNotEmpty ? const HSpace(2) : const HSpace(8), - Flexible( - child: FlowyText.semibold( - currentWorkspace.name, - fontSize: 20.0, - overflow: TextOverflow.ellipsis, - ), + FlowyText.semibold( + currentWorkspace.name, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, ), ], ), @@ -165,12 +162,9 @@ class _MobileWorkspace extends StatelessWidget { showHeader: true, showDragHandle: true, showCloseButton: true, - useRootNavigator: true, - enableScrollable: true, - bottomSheetPadding: context.bottomSheetPadding(), title: LocaleKeys.workspace_menuTitle.tr(), backgroundColor: Theme.of(context).colorScheme.surface, - builder: (sheetContext) { + builder: (_) { return BlocProvider.value( value: context.read(), child: BlocBuilder( @@ -185,7 +179,7 @@ class _MobileWorkspace extends StatelessWidget { currentWorkspace: currentWorkspace, workspaces: workspaces, onWorkspaceSelected: (workspace) { - Navigator.of(sheetContext).pop(); + context.pop(); if (workspace == currentWorkspace) { return; @@ -194,7 +188,6 @@ class _MobileWorkspace extends StatelessWidget { context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.workspaceAuthType, ), ); }, @@ -230,13 +223,12 @@ class _UserIcon extends StatelessWidget { fontSize: 26, ), onTap: () async { - final icon = await context.push( + final icon = await context.push( Uri( path: MobileEmojiPickerScreen.routeName, queryParameters: { MobileEmojiPickerScreen.pageTitle: LocaleKeys.titleBar_userIcon.tr(), - MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name], }, ).toString(), ); 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 a01df20549..964f9e5aa5 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 @@ -3,19 +3,15 @@ import 'package:appflowy/env/env.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/presentation.dart'; -import 'package:appflowy/mobile/presentation/setting/ai/ai_settings_group.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'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.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'; class MobileHomeSettingPage extends StatefulWidget { const MobileHomeSettingPage({ @@ -71,42 +67,31 @@ class _MobileHomeSettingPageState extends State { } Widget _buildSettingsWidget(UserProfilePB userProfile) { - return BlocProvider( - create: (context) => UserWorkspaceBloc(userProfile: userProfile) - ..add(const UserWorkspaceEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final currentWorkspaceId = state.currentWorkspace?.workspaceId ?? ''; - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - PersonalInfoSettingGroup( - userProfile: userProfile, - ), - const WorkspaceSettingGroup(), - const AppearanceSettingGroup(), - const LanguageSettingGroup(), - if (Env.enableCustomCloud) const CloudSettingGroup(), - if (isAuthEnabled) - AiSettingsGroup( - key: ValueKey(currentWorkspaceId), - userProfile: userProfile, - workspaceId: currentWorkspaceId, - ), - const SupportSettingGroup(), - const AboutSettingGroup(), - UserSessionSettingGroup( - userProfile: userProfile, - showThirdPartyLogin: false, - ), - const VSpace(20), - ], - ), + // show the third-party sign in buttons if user logged in with local session and auth is enabled. + + final showThirdPartyLogin = + userProfile.authenticator == AuthenticatorPB.Local && isAuthEnabled; + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + PersonalInfoSettingGroup( + userProfile: userProfile, ), - ); - }, + // TODO: Enable and implement along with Push Notifications + // const NotificationsSettingGroup(), + const AppearanceSettingGroup(), + const LanguageSettingGroup(), + if (Env.enableCustomCloud) const CloudSettingGroup(), + const SupportSettingGroup(), + const AboutSettingGroup(), + UserSessionSettingGroup( + 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 73da7594a7..2ee9e14175 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,7 +64,11 @@ class MobileHomeTrashPage extends StatelessWidget { ], ), body: state.objects.isEmpty - ? const _EmptyTrashBin() + ? FlowyMobileStateContainer.info( + emoji: '🗑️', + title: LocaleKeys.trash_mobile_empty.tr(), + description: LocaleKeys.trash_mobile_emptyDescription.tr(), + ) : _DeletedFilesListView(state), ); }, @@ -78,41 +82,6 @@ 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({ @@ -212,7 +181,7 @@ class _DeletedFilesListView extends StatelessWidget { ?.copyWith(color: theme.colorScheme.onSurface), ), horizontalTitleGap: 0, - tileColor: theme.colorScheme.onSurface.withValues(alpha: 0.1), + tileColor: theme.colorScheme.onSurface.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 661a422e0c..a2b9ae52c7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -38,7 +38,8 @@ class _MobileRecentFolderState extends State { builder: (context, state) { final ids = {}; - List recentViews = state.views.map((e) => e.item).toList(); + List recentViews = + state.views.reversed.map((e) => e.item).toList(); recentViews.retainWhere((element) => ids.add(element.id)); // only keep the first 20 items. diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart index 966b1ac61a..18a0338fbb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; @@ -110,7 +110,10 @@ class MobileRecentView extends StatelessWidget { return Padding( padding: const EdgeInsets.only(left: 8.0), child: state.icon.isNotEmpty - ? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30) + ? EmojiText( + emoji: state.icon, + fontSize: 30.0, + ) : SizedBox.square( dimension: 32.0, child: view.defaultIcon(), @@ -134,8 +137,7 @@ class _RecentCover extends StatelessWidget { Widget build(BuildContext context) { final placeholder = Container( // random color, update it once we have a better placeholder - color: - Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.2), + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.2), ); final value = this.value; if (value == null) { 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 c0baa641d9..b2b2fbca85 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 @@ -49,7 +49,7 @@ class _MobileRecentSpaceState extends State List _filterRecentViews(List recentViews) { final ids = {}; - final filteredRecentViews = recentViews.toList(); + final filteredRecentViews = recentViews.reversed.toList(); filteredRecentViews.retainWhere((e) => ids.add(e.item.id)); return filteredRecentViews; } @@ -68,34 +68,38 @@ class _RecentViews extends StatelessWidget { ? const Color(0xFFE9E9EC) : const Color(0x1AFFFFFF); return SlidableAutoCloseBehavior( - 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: 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: 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 0079ed319a..4cd212f5a9 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,9 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; @@ -44,17 +43,47 @@ class MobileSectionFolder extends StatelessWidget { onPressed: () => context .read() .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => _createNewPage(context), + onAdded: () { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: LocaleKeys.menuAppHeader_defaultNewPageName + .tr(), + index: 0, + viewSection: spaceType.toViewSectionPB, + ), + ); + context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ); + }, ), ), if (state.isExpanded) - Padding( - padding: const EdgeInsets.only( - left: HomeSpaceViewSizes.leftPadding, - ), - child: _Pages( - views: views, + ...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, + ); + }, ), ), ], @@ -63,70 +92,4 @@ 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: (v) => context.pushView( - v, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ), - 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 b1d2bf6909..aa84dc7601 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,7 +32,6 @@ class _MobileSectionFolderHeaderState extends State { Widget build(BuildContext context) { return Row( children: [ - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), Expanded( child: FlowyButton( text: FlowyText.medium( @@ -58,19 +57,15 @@ class _MobileSectionFolderHeaderState extends State { }, ), ), - 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, - ), + FlowyIconButton( + key: mobileCreateNewPageButtonKey, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + height: HomeSpaceViewSizes.mViewButtonDimension, + width: HomeSpaceViewSizes.mViewButtonDimension, + icon: 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 deleted file mode 100644 index 659473a6b1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart +++ /dev/null @@ -1,168 +0,0 @@ -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, - helpAndDocumentation, -} - -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.workspaceAuthType == AuthTypePB.Server) ...[ - 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.helpAndDocumentation, - svg: FlowySvgs.help_and_documentation_s, - text: LocaleKeys.settings_popupMenuItem_helpAndDocumentation.tr(), - ), - const PopupMenuDivider(height: 0.5), - _buildItem( - value: _MobileSettingsPopupMenuItem.help, - svg: FlowySvgs.message_support_s, - text: LocaleKeys.settings_popupMenuItem_getSupport.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; - case _MobileSettingsPopupMenuItem.helpAndDocumentation: - _openHelpAndDocumentationPage(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); - } - - void _openHelpAndDocumentationPage(BuildContext context) { - afLaunchUrlString('https://appflowy.com/guide'); - } -} - -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 2de40600f2..341acb8099 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,10 +6,7 @@ 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; @@ -39,7 +36,6 @@ 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 87ce41d5b6..71be20453d 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,20 +5,16 @@ 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/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; 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'; @@ -28,6 +24,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:string_validator/string_validator.dart'; import 'package:time/time.dart'; @@ -79,21 +76,13 @@ class MobileViewPage extends StatelessWidget { : MobilePaneActionType.addToFavorites, ], cardType: type, - spaceRatio: 4, ), - child: AnimatedGestureDetector( - onTapUp: () => context.pushView( - view, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: (_) => context.pushView(view), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), Expanded(child: _buildDescription(context, state)), const HSpace(20.0), SizedBox( @@ -101,7 +90,6 @@ class MobileViewPage extends StatelessWidget { height: 60, child: _buildCover(context, state), ), - const HSpace(HomeSpaceViewSizes.mHorizontalPadding), ], ), ), @@ -126,7 +114,7 @@ class MobileViewPage extends StatelessWidget { } Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) { - final supportAvatar = isURL(state.icon.emoji); + final supportAvatar = isURL(state.icon); if (!supportAvatar) { return _buildLastViewed(context); } @@ -148,8 +136,7 @@ class MobileViewPage extends StatelessWidget { final iconUrl = userProfile?.iconUrl; if (iconUrl == null || iconUrl.isEmpty || - view.createdBy != userProfile?.id || - !isURL(iconUrl)) { + view.createdBy != userProfile?.id) { return const SizedBox.shrink(); } @@ -182,23 +169,23 @@ class MobileViewPage extends StatelessWidget { Widget _buildTitle(BuildContext context, RecentViewState state) { final name = state.name; final icon = state.icon; + final fontFamily = Platform.isAndroid || Platform.isLinux + ? GoogleFonts.notoColorEmoji().fontFamily + : null; return RichText( maxLines: 3, overflow: TextOverflow.ellipsis, text: TextSpan( children: [ - if (icon.isNotEmpty) ...[ - WidgetSpan( - child: SizedBox( - width: 20, - child: RawEmojiIconWidget( - emoji: icon, - emojiSize: 18.0, + TextSpan( + text: icon, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 17.0, + fontWeight: FontWeight.w600, + fontFamily: fontFamily, ), - ), - ), - const WidgetSpan(child: HSpace(8.0)), - ], + ), + if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)), TextSpan( text: name, style: Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -215,7 +202,7 @@ class MobileViewPage extends StatelessWidget { Widget _buildAuthor(BuildContext context, RecentViewState state) { return FlowyText.regular( // view.createdBy.toString(), - '', + 'Lucas', fontSize: 12.0, color: Theme.of(context).hintColor, overflow: TextOverflow.ellipsis, @@ -224,8 +211,8 @@ class MobileViewPage extends StatelessWidget { Widget _buildLastViewed(BuildContext context) { final textColor = Theme.of(context).isLightMode - ? const Color(0x7F171717) - : Colors.white.withValues(alpha: 0.45); + ? const Color(0xFF171717) + : Colors.white.withOpacity(0.45); if (timestamp == null) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/constants.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/constants.dart deleted file mode 100644 index da7d13a174..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/constants.dart +++ /dev/null @@ -1,3 +0,0 @@ -class SpaceUIConstants { - static const itemHeight = 52.0; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/manage_space_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/manage_space_widget.dart deleted file mode 100644 index f24108c945..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/manage_space_widget.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; - -import 'widgets.dart'; - -enum ManageSpaceType { - create, - edit, -} - -class ManageSpaceWidget extends StatelessWidget { - const ManageSpaceWidget({ - super.key, - required this.controller, - required this.permission, - required this.selectedColor, - required this.selectedIcon, - required this.type, - }); - - final TextEditingController controller; - final ValueNotifier permission; - final ValueNotifier selectedColor; - final ValueNotifier selectedIcon; - final ManageSpaceType type; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ManageSpaceNameOption( - controller: controller, - type: type, - ), - ManageSpacePermissionOption(permission: permission), - ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 560, - ), - child: ManageSpaceIconOption( - selectedColor: selectedColor, - selectedIcon: selectedIcon, - ), - ), - const VSpace(60), - ], - ); - } -} 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 3bb62a92c8..69f5d8951a 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,23 +4,36 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart'; import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/shared/list_extension.dart'; +import 'package:appflowy/mobile/presentation/presentation.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 StatelessWidget { - const MobileSpace({ - super.key, - }); +class MobileSpace extends StatefulWidget { + 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) { @@ -37,17 +50,22 @@ class MobileSpace extends StatelessWidget { MobileSpaceHeader( isExpanded: state.isExpanded, space: currentSpace, - onAdded: () => _showCreatePageMenu(context, currentSpace), + onAdded: () { + context.read().add( + SpaceEvent.createPage( + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + index: 0, + ), + ); + context.read().add( + SpaceEvent.expand(currentSpace, true), + ); + }, onPressed: () => _showSpaceMenu(context), ), - Padding( - padding: const EdgeInsets.only( - left: HomeSpaceViewSizes.mHorizontalPadding, - ), - child: _Pages( - key: ValueKey(currentSpace.id), - space: currentSpace, - ), + _Pages( + key: ValueKey(currentSpace.id), + space: currentSpace, ), ], ); @@ -63,11 +81,8 @@ class MobileSpace extends StatelessWidget { showDragHandle: true, showCloseButton: true, showDoneButton: true, - useRootNavigator: true, title: LocaleKeys.space_title.tr(), backgroundColor: Theme.of(context).colorScheme.surface, - enableScrollable: true, - bottomSheetPadding: context.bottomSheetPadding(), builder: (_) { return BlocProvider.value( value: context.read(), @@ -80,37 +95,12 @@ class MobileSpace extends StatelessWidget { ); } - 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: '', - layout: layout, - index: 0, - openAfterCreate: true, - ), - ); - context.read().add( - SpaceEvent.expand(space, true), - ); - }, + void _createNewPage() { + context.read().add( + SpaceEvent.createPage( + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + ), ); - }, - ); } } @@ -132,15 +122,8 @@ 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: childViews + children: state.view.childViews .map( (view) => MobileViewItem( key: ValueKey( @@ -152,26 +135,18 @@ class _Pages extends StatelessWidget { level: 0, leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, - onSelected: (v) => context.pushView( - v, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ), + onSelected: context.pushView, endActionPane: (context) { final view = context.read().state.view; - final actions = [ - MobilePaneActionType.more, - if (view.layout == ViewLayoutPB.Document) - MobilePaneActionType.add, - ]; return buildEndActionPane( context, - actions, + [ + MobilePaneActionType.more, + if (view.layout == ViewLayoutPB.Document) + MobilePaneActionType.add, + ], spaceType: spaceType, - spaceRatio: actions.length == 1 ? 3 : 4, + needSpace: false, ); }, ), 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 0cd80ff1bb..ffc1691404 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,5 +1,4 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -31,12 +30,9 @@ 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), @@ -53,15 +49,8 @@ class MobileSpaceHeader extends StatelessWidget { GestureDetector( behavior: HitTestBehavior.translucent, onTap: onAdded, - child: Container( - // expand the touch area - margin: const EdgeInsets.symmetric( - horizontal: HomeSpaceViewSizes.mHorizontalPadding, - vertical: 8.0, - ), - child: const FlowySvg( - FlowySvgs.m_space_add_s, - ), + child: const FlowySvg( + FlowySvgs.m_space_add_s, ), ), ], 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 485e07a28c..6788e1396d 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 @@ -1,74 +1,50 @@ 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/space/space_menu_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/home/workspaces/create_workspace_menu.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; -import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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' hide Icon; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'constants.dart'; -import 'manage_space_widget.dart'; - class MobileSpaceMenu extends StatelessWidget { - const MobileSpaceMenu({ - super.key, - }); + const MobileSpaceMenu({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(4.0), - for (final space in state.spaces) - SizedBox( - height: SpaceUIConstants.itemHeight, - child: MobileSpaceMenuItem( - space: space, - isSelected: state.currentSpace?.id == space.id, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider( - height: 0.5, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const VSpace(4.0), + for (final space in state.spaces) + SizedBox( + height: 52, + child: _SidebarSpaceMenuItem( + space: space, + isSelected: state.currentSpace?.id == space.id, ), ), - const SizedBox( - height: SpaceUIConstants.itemHeight, - child: _CreateSpaceButton(), - ), - ], - ), + // const Padding( + // padding: EdgeInsets.symmetric(vertical: 8.0), + // child: Divider( + // height: 0.5, + // ), + // ), + // const SizedBox( + // height: 52, + // child: _CreateSpaceButton(), + // ), + ], ); }, ); } } -class MobileSpaceMenuItem extends StatelessWidget { - const MobileSpaceMenuItem({ - super.key, +class _SidebarSpaceMenuItem extends StatelessWidget { + const _SidebarSpaceMenuItem({ required this.space, required this.isSelected, }); @@ -83,7 +59,6 @@ class MobileSpaceMenuItem extends StatelessWidget { children: [ FlowyText.medium( space.name, - fontSize: 16.0, ), const HSpace(6.0), if (space.spacePermission == SpacePermission.private) @@ -93,21 +68,19 @@ class MobileSpaceMenuItem 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(24), - rightIcon: SpaceMenuItemTrailing( - key: ValueKey('${space.id}_space_menu_item_trailing'), - space: space, - currentSpace: context.read().state.currentSpace, - ), + leftIconSize: const Size.square(20), + rightIcon: isSelected + ? const FlowySvg( + FlowySvgs.workspace_selected_s, + blendMode: null, + ) + : null, onTap: () { context.read().add(SpaceEvent.open(space)); Navigator.of(context).pop(); @@ -116,381 +89,40 @@ class MobileSpaceMenuItem extends StatelessWidget { } } -class _CreateSpaceButton extends StatefulWidget { - const _CreateSpaceButton(); +// class _CreateSpaceButton extends StatelessWidget { +// const _CreateSpaceButton(); - @override - State<_CreateSpaceButton> createState() => _CreateSpaceButtonState(); -} +// @override +// Widget build(BuildContext context) { +// return FlowyButton( +// text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()), +// iconPadding: 10, +// leftIcon: const FlowySvg( +// FlowySvgs.space_add_s, +// ), +// leftIconSize: const Size.square(20), +// onTap: () { +// PopoverContainer.of(context).close(); +// _showCreateSpaceDialog(context); +// }, +// ); +// } -class _CreateSpaceButtonState extends State<_CreateSpaceButton> { - final controller = TextEditingController(); - final permission = ValueNotifier( - SpacePermission.publicToAll, - ); - final selectedColor = ValueNotifier( - builtInSpaceColors.first, - ); - final selectedIcon = ValueNotifier( - kIconGroups?.first.icons.first, - ); - - @override - void dispose() { - controller.dispose(); - permission.dispose(); - selectedColor.dispose(); - selectedIcon.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FlowyButton( - text: FlowyText.regular(LocaleKeys.space_createNewSpace.tr()), - iconPadding: 10, - leftIcon: const Padding( - padding: EdgeInsets.all(2.0), - child: FlowySvg( - FlowySvgs.space_add_s, - ), - ), - margin: const EdgeInsets.symmetric(horizontal: 12.0), - leftIconSize: const Size.square(24), - onTap: () => _showCreateSpaceDialog(context), - ); - } - - Future _showCreateSpaceDialog(BuildContext context) async { - await showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.space_createSpace.tr(), - showCloseButton: true, - showDivider: false, - showDoneButton: true, - enableScrollable: true, - showDragHandle: true, - bottomSheetPadding: context.bottomSheetPadding(), - onDone: (bottomSheetContext) { - final iconPath = selectedIcon.value?.iconPath ?? ''; - context.read().add( - SpaceEvent.create( - name: controller.text.orDefault( - LocaleKeys.space_defaultSpaceName.tr(), - ), - permission: permission.value, - iconColor: selectedColor.value, - icon: iconPath, - createNewPageByDefault: true, - openAfterCreate: false, - ), - ); - Navigator.pop(bottomSheetContext); - Navigator.pop(context); - - Log.info( - 'create space on mobile, name: ${controller.text}, permission: ${permission.value}, color: ${selectedColor.value}, icon: $iconPath', - ); - }, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) => ManageSpaceWidget( - controller: controller, - permission: permission, - selectedColor: selectedColor, - selectedIcon: selectedIcon, - type: ManageSpaceType.create, - ), - ); - - _resetState(); - } - - void _resetState() { - controller.clear(); - permission.value = SpacePermission.publicToAll; - selectedColor.value = builtInSpaceColors.first; - selectedIcon.value = kIconGroups?.first.icons.first; - } -} - -class SpaceMenuItemTrailing extends StatefulWidget { - const SpaceMenuItemTrailing({ - super.key, - required this.space, - this.currentSpace, - }); - - final ViewPB space; - final ViewPB? currentSpace; - - @override - State createState() => _SpaceMenuItemTrailingState(); -} - -class _SpaceMenuItemTrailingState extends State { - final controller = TextEditingController(); - final permission = ValueNotifier( - SpacePermission.publicToAll, - ); - final selectedColor = ValueNotifier( - builtInSpaceColors.first, - ); - final selectedIcon = ValueNotifier( - kIconGroups?.first.icons.first, - ); - - @override - void dispose() { - controller.dispose(); - permission.dispose(); - selectedColor.dispose(); - selectedIcon.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - const iconSize = Size.square(20); - return Row( - children: [ - const HSpace(12.0), - // show the check icon if the space is the current space - if (widget.space.id == widget.currentSpace?.id) - const FlowySvg( - FlowySvgs.m_blue_check_s, - size: iconSize, - blendMode: null, - ), - const HSpace(8.0), - // more options button - AnimatedGestureDetector( - onTapUp: () => _showMoreOptions(context), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: FlowySvg( - FlowySvgs.workspace_three_dots_s, - size: iconSize, - ), - ), - ), - ], - ); - } - - void _showMoreOptions(BuildContext context) { - final actions = [ - SpaceMoreActionType.rename, - SpaceMoreActionType.duplicate, - SpaceMoreActionType.manage, - SpaceMoreActionType.delete, - ]; - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (bottomSheetContext) { - return SpaceMenuMoreOptions( - actions: actions, - onAction: (action) => _onActions( - context, - bottomSheetContext, - action, - ), - ); - }, - ); - } - - void _onActions( - BuildContext context, - BuildContext bottomSheetContext, - SpaceMoreActionType action, - ) { - Log.info('execute action in space menu bottom sheet: $action'); - - switch (action) { - case SpaceMoreActionType.rename: - _showRenameSpaceBottomSheet(context); - break; - case SpaceMoreActionType.duplicate: - _duplicateSpace(context, bottomSheetContext); - break; - case SpaceMoreActionType.manage: - _showManageSpaceBottomSheet(context); - break; - case SpaceMoreActionType.delete: - _deleteSpace(context, bottomSheetContext); - break; - default: - assert(false, 'Unsupported action: $action'); - break; - } - } - - void _duplicateSpace(BuildContext context, BuildContext bottomSheetContext) { - Log.info('duplicate the space: ${widget.space.name}'); - - context.read().add(const SpaceEvent.duplicate()); - - showToastNotification( - message: LocaleKeys.space_success_duplicateSpace.tr(), - ); - - Navigator.of(bottomSheetContext).pop(); - Navigator.of(context).pop(); - } - - void _showRenameSpaceBottomSheet(BuildContext context) { - Navigator.of(context).pop(); - - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.space_renameSpace.tr(), - showCloseButton: true, - showDragHandle: true, - showDivider: false, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) { - return EditWorkspaceNameBottomSheet( - type: EditWorkspaceNameType.edit, - workspaceName: widget.space.name, - hintText: LocaleKeys.space_spaceNamePlaceholder.tr(), - validator: (value) => null, - onSubmitted: (name) { - // rename the workspace - Log.info('rename the space, from: ${widget.space.name}, to: $name'); - bottomSheetContext.popToHome(); - - context - .read() - .add(SpaceEvent.rename(space: widget.space, name: name)); - - showToastNotification( - message: LocaleKeys.space_success_renameSpace.tr(), - ); - }, - ); - }, - ); - } - - Future _showManageSpaceBottomSheet(BuildContext context) async { - controller.text = widget.space.name; - permission.value = widget.space.spacePermission; - selectedColor.value = - widget.space.spaceIconColor ?? builtInSpaceColors.first; - selectedIcon.value = widget.space.spaceIcon?.icon; - - await showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.space_manageSpace.tr(), - showCloseButton: true, - showDivider: false, - showDoneButton: true, - enableScrollable: true, - showDragHandle: true, - bottomSheetPadding: context.bottomSheetPadding(), - onDone: (bottomSheetContext) { - String iconName = ''; - final icon = selectedIcon.value; - final iconGroup = icon?.iconGroup; - final iconId = icon?.name; - if (icon != null && iconGroup != null) { - iconName = '${iconGroup.name}/$iconId'; - } - Log.info( - 'update space on mobile, name: ${controller.text}, permission: ${permission.value}, color: ${selectedColor.value}, icon: $iconName', - ); - context.read().add( - SpaceEvent.update( - space: widget.space, - name: controller.text.orDefault( - LocaleKeys.space_defaultSpaceName.tr(), - ), - permission: permission.value, - iconColor: selectedColor.value, - icon: iconName, - ), - ); - - showToastNotification( - message: LocaleKeys.space_success_updateSpace.tr(), - ); - - Navigator.pop(bottomSheetContext); - Navigator.pop(context); - }, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) => ManageSpaceWidget( - controller: controller, - permission: permission, - selectedColor: selectedColor, - selectedIcon: selectedIcon, - type: ManageSpaceType.edit, - ), - ); - } - - void _deleteSpace( - BuildContext context, - BuildContext bottomSheetContext, - ) { - Navigator.of(bottomSheetContext).pop(); - - _showConfirmDialog( - context, - '${LocaleKeys.space_delete.tr()}: ${widget.space.name}', - LocaleKeys.space_deleteConfirmationDescription.tr(), - LocaleKeys.button_delete.tr(), - (_) async { - context.read().add(SpaceEvent.delete(widget.space)); - - showToastNotification( - message: LocaleKeys.space_success_deleteSpace.tr(), - ); - - Navigator.pop(context); - }, - ); - } - - void _showConfirmDialog( - BuildContext context, - String title, - String content, - String rightButtonText, - void Function(BuildContext context)? onRightButtonPressed, - ) { - showFlowyCupertinoConfirmDialog( - title: title, - content: FlowyText( - content, - fontSize: 14, - maxLines: 10, - ), - leftButton: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), - ), - rightButton: FlowyText( - rightButtonText, - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, - color: const Color(0xFFFE0220), - ), - onRightButtonPressed: onRightButtonPressed, - ); - } -} +// void _showCreateSpaceDialog(BuildContext context) { +// final spaceBloc = context.read(); +// showDialog( +// context: context, +// builder: (_) { +// return Dialog( +// shape: RoundedRectangleBorder( +// borderRadius: BorderRadius.circular(12.0), +// ), +// child: BlocProvider.value( +// value: spaceBloc, +// child: const CreateSpacePopup(), +// ), +// ); +// }, +// ); +// } +// } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_menu_bottom_sheet.dart deleted file mode 100644 index 2eaf609af0..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_menu_bottom_sheet.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'constants.dart'; - -class SpaceMenuMoreOptions extends StatelessWidget { - const SpaceMenuMoreOptions({ - super.key, - required this.onAction, - required this.actions, - }); - - final void Function(SpaceMoreActionType action) onAction; - final List actions; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: actions - .map( - (action) => _buildActionButton(context, action), - ) - .toList(), - ); - } - - Widget _buildActionButton( - BuildContext context, - SpaceMoreActionType action, - ) { - switch (action) { - case SpaceMoreActionType.rename: - return FlowyOptionTile.text( - text: LocaleKeys.button_rename.tr(), - height: SpaceUIConstants.itemHeight, - leftIcon: const FlowySvg( - FlowySvgs.view_item_rename_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - SpaceMoreActionType.rename, - ), - ); - case SpaceMoreActionType.delete: - return FlowyOptionTile.text( - text: LocaleKeys.button_delete.tr(), - height: SpaceUIConstants.itemHeight, - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(18), - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - SpaceMoreActionType.delete, - ), - ); - case SpaceMoreActionType.manage: - return FlowyOptionTile.text( - text: LocaleKeys.space_manage.tr(), - height: SpaceUIConstants.itemHeight, - leftIcon: const FlowySvg( - FlowySvgs.settings_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - SpaceMoreActionType.manage, - ), - ); - case SpaceMoreActionType.duplicate: - return FlowyOptionTile.text( - text: SpaceMoreActionType.duplicate.name, - height: SpaceUIConstants.itemHeight, - leftIcon: const FlowySvg( - FlowySvgs.duplicate_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - SpaceMoreActionType.duplicate, - ), - ); - default: - return const SizedBox.shrink(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_permission_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_permission_bottom_sheet.dart deleted file mode 100644 index 85ad35a549..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/space_permission_bottom_sheet.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class SpacePermissionBottomSheet extends StatelessWidget { - const SpacePermissionBottomSheet({ - super.key, - required this.onAction, - required this.permission, - }); - - final SpacePermission permission; - final void Function(SpacePermission action) onAction; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyOptionTile.text( - text: LocaleKeys.space_publicPermission.tr(), - leftIcon: const FlowySvg( - FlowySvgs.space_permission_public_s, - ), - trailing: permission == SpacePermission.publicToAll - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - blendMode: null, - ) - : null, - onTap: () => onAction(SpacePermission.publicToAll), - ), - FlowyOptionTile.text( - text: LocaleKeys.space_privatePermission.tr(), - showTopBorder: false, - leftIcon: const FlowySvg( - FlowySvgs.space_permission_private_s, - ), - trailing: permission == SpacePermission.private - ? const FlowySvg( - FlowySvgs.m_blue_check_s, - blendMode: null, - ) - : null, - onTap: () => onAction(SpacePermission.private), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/widgets.dart deleted file mode 100644 index ab3295a630..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/widgets.dart +++ /dev/null @@ -1,389 +0,0 @@ -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/widgets/widgets.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/shared/icon_emoji_picker/colors.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; - -import 'constants.dart'; -import 'manage_space_widget.dart'; -import 'space_permission_bottom_sheet.dart'; - -class ManageSpaceNameOption extends StatelessWidget { - const ManageSpaceNameOption({ - super.key, - required this.controller, - required this.type, - }); - - final TextEditingController controller; - final ManageSpaceType type; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 4), - child: FlowyText( - LocaleKeys.space_spaceName.tr(), - fontSize: 14, - figmaLineHeight: 20.0, - fontWeight: FontWeight.w400, - color: Theme.of(context).hintColor, - ), - ), - FlowyOptionTile.textField( - controller: controller, - autofocus: type == ManageSpaceType.create ? true : false, - textFieldHintText: LocaleKeys.space_spaceNamePlaceholder.tr(), - ), - const VSpace(16), - ], - ); - } -} - -class ManageSpacePermissionOption extends StatelessWidget { - const ManageSpacePermissionOption({ - super.key, - required this.permission, - }); - - final ValueNotifier permission; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 4), - child: FlowyText( - LocaleKeys.space_permission.tr(), - fontSize: 14, - figmaLineHeight: 20.0, - fontWeight: FontWeight.w400, - color: Theme.of(context).hintColor, - ), - ), - ValueListenableBuilder( - valueListenable: permission, - builder: (context, value, child) => FlowyOptionTile.text( - height: SpaceUIConstants.itemHeight, - text: value.i18n, - leftIcon: FlowySvg(value.icon), - trailing: const FlowySvg( - FlowySvgs.arrow_right_s, - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.space_permission.tr(), - showCloseButton: true, - showDivider: false, - showDragHandle: true, - builder: (context) => SpacePermissionBottomSheet( - permission: value, - onAction: (value) { - permission.value = value; - Navigator.pop(context); - }, - ), - ); - }, - ), - ), - const VSpace(16), - ], - ); - } -} - -class ManageSpaceIconOption extends StatefulWidget { - const ManageSpaceIconOption({ - super.key, - required this.selectedColor, - required this.selectedIcon, - }); - - final ValueNotifier selectedColor; - final ValueNotifier selectedIcon; - - @override - State createState() => _ManageSpaceIconOptionState(); -} - -class _ManageSpaceIconOptionState extends State { - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ..._buildColorOption(context), - ..._buildSpaceIconOption(context), - ], - ); - } - - List _buildColorOption(BuildContext context) { - return [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 4), - child: FlowyText( - LocaleKeys.space_mSpaceIconColor.tr(), - fontSize: 14, - figmaLineHeight: 20.0, - fontWeight: FontWeight.w400, - color: Theme.of(context).hintColor, - ), - ), - ValueListenableBuilder( - valueListenable: widget.selectedColor, - builder: (context, selectedColor, child) { - return FlowyOptionDecorateBox( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: builtInSpaceColors.map((color) { - return SpaceColorItem( - color: color, - selectedColor: selectedColor, - onSelected: (color) => widget.selectedColor.value = color, - ); - }).toList(), - ), - ), - ), - ); - }, - ), - const VSpace(16), - ]; - } - - List _buildSpaceIconOption(BuildContext context) { - return [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 4), - child: FlowyText( - LocaleKeys.space_mSpaceIcon.tr(), - fontSize: 14, - figmaLineHeight: 20.0, - fontWeight: FontWeight.w400, - color: Theme.of(context).hintColor, - ), - ), - Expanded( - child: SizedBox( - width: double.infinity, - child: ValueListenableBuilder( - valueListenable: widget.selectedColor, - builder: (context, selectedColor, child) { - return ValueListenableBuilder( - valueListenable: widget.selectedIcon, - builder: (context, selectedIcon, child) { - return FlowyOptionDecorateBox( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - ), - child: _buildIconGroups( - context, - selectedColor, - selectedIcon, - ), - ), - ); - }, - ); - }, - ), - ), - ), - const VSpace(16), - ]; - } - - Widget _buildIconGroups( - BuildContext context, - String selectedColor, - Icon? selectedIcon, - ) { - final iconGroups = kIconGroups; - if (iconGroups == null) { - return const SizedBox.shrink(); - } - - return ListView.builder( - itemCount: iconGroups.length, - itemBuilder: (context, index) { - final iconGroup = iconGroups[index]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(12.0), - FlowyText( - iconGroup.displayName.capitalize(), - fontSize: 12, - figmaLineHeight: 18.0, - color: context.pickerTextColor, - ), - const VSpace(4.0), - Center( - child: Wrap( - spacing: 10.0, - runSpacing: 8.0, - children: iconGroup.icons.map((icon) { - return SpaceIconItem( - icon: icon, - isSelected: selectedIcon?.name == icon.name, - selectedColor: selectedColor, - onSelectedIcon: (icon) => widget.selectedIcon.value = icon, - ); - }).toList(), - ), - ), - const VSpace(12.0), - if (index == iconGroups.length - 1) ...[ - const StreamlinePermit(), - ], - ], - ); - }, - ); - } -} - -class SpaceIconItem extends StatelessWidget { - const SpaceIconItem({ - super.key, - required this.icon, - required this.onSelectedIcon, - required this.isSelected, - required this.selectedColor, - }); - - final Icon icon; - final void Function(Icon icon) onSelectedIcon; - final bool isSelected; - final String selectedColor; - - @override - Widget build(BuildContext context) { - return AnimatedGestureDetector( - onTapUp: () => onSelectedIcon(icon), - child: Container( - width: 36, - height: 36, - decoration: isSelected - ? BoxDecoration( - color: Color(int.parse(selectedColor)), - borderRadius: BorderRadius.circular(8.0), - ) - : ShapeDecoration( - color: Colors.transparent, - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 0.5, - color: Color(0x661F2329), - ), - borderRadius: BorderRadius.circular(8), - ), - ), - child: Center( - child: FlowySvg.string( - icon.content, - size: const Size.square(18), - color: isSelected - ? Theme.of(context).colorScheme.surface - : context.pickerIconColor, - opacity: isSelected ? 1.0 : 0.7, - ), - ), - ), - ); - } -} - -class SpaceColorItem extends StatelessWidget { - const SpaceColorItem({ - super.key, - required this.color, - required this.selectedColor, - required this.onSelected, - }); - - final String color; - final String selectedColor; - final void Function(String color) onSelected; - - @override - Widget build(BuildContext context) { - final child = Center( - child: Container( - width: 28, - height: 28, - decoration: BoxDecoration( - color: Color(int.parse(color)), - borderRadius: BorderRadius.circular(14), - ), - ), - ); - - final decoration = color != selectedColor - ? null - : ShapeDecoration( - color: Colors.transparent, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.50, - color: Theme.of(context).colorScheme.primary, - ), - borderRadius: BorderRadius.circular(21), - ), - ); - - return AnimatedGestureDetector( - onTapUp: () => onSelected(color), - child: Container( - width: 36, - height: 36, - decoration: decoration, - child: child, - ), - ); - } -} - -extension on SpacePermission { - String get i18n { - switch (this) { - case SpacePermission.publicToAll: - return LocaleKeys.space_publicPermission.tr(); - case SpacePermission.private: - return LocaleKeys.space_privatePermission.tr(); - } - } - - FlowySvgData get icon { - switch (this) { - case SpacePermission.publicToAll: - return FlowySvgs.space_permission_public_s; - case SpacePermission.private: - return FlowySvgs.space_permission_private_s; - } - } -} 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 f8c9a0d3b1..5602f46f89 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,14 +21,12 @@ class MobileSpaceTabBar extends StatelessWidget { Widget build(BuildContext context) { final baseStyle = Theme.of(context).textTheme.bodyMedium; final labelStyle = baseStyle?.copyWith( - fontWeight: FontWeight.w500, + fontWeight: FontWeight.w600, 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 deleted file mode 100644 index cc4176e0ef..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart +++ /dev/null @@ -1,84 +0,0 @@ -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: Hero( - tag: "ai_chat_prompt", - 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.withValues(alpha: 0.05), - ), - ], - ); - } - - BoxDecoration _buildWrapperDecoration(BuildContext context) { - final outlineColor = Theme.of(context).colorScheme.outline; - final borderColor = Theme.of(context).isLightMode - ? outlineColor.withValues(alpha: 0.7) - : outlineColor.withValues(alpha: 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 56f5f3e6ab..097bd22910 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,33 +1,16 @@ -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/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -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: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; @@ -39,24 +22,10 @@ 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(); } @@ -64,74 +33,36 @@ class _MobileSpaceTabState extends State Widget build(BuildContext context) { return Provider.value( value: widget.userProfile, - 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, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ); - } - }, - ), - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) { - final lastCreatedPage = state.lastCreatedRootView; - if (lastCreatedPage != null) { - context.pushView( - lastCreatedPage, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ); - } - }, - ), - ], - child: BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const SizedBox.shrink(); - } + 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), - ); - }, + 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), ), - const HSpace(12.0), - Expanded( - child: TabBarView( - controller: tabController, - children: _buildTabs(state), - ), - ), - ], - ); - }, - ), + ), + ], + ); + }, ), ); } @@ -152,9 +83,11 @@ class _MobileSpaceTabState extends State if (tabController == null) { return; } - context - .read() - .add(SpaceOrderEvent.open(tabController!.index)); + context.read().add( + SpaceOrderEvent.open( + tabController!.index, + ), + ); } List _buildTabs(SpaceOrderState state) { @@ -163,59 +96,12 @@ class _MobileSpaceTabState extends State case MobileSpaceTabType.recent: return const MobileRecentSpace(); case MobileSpaceTabType.spaces: - return Stack( - children: [ - MobileHomeSpace(userProfile: widget.userProfile), - // only show ai chat button for cloud user - if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) - Positioned( - bottom: MediaQuery.of(context).padding.bottom + 16, - left: 20, - right: 20, - child: const FloatingAIEntry(), - ), - ], - ); + return MobileHomeSpace(userProfile: widget.userProfile); case MobileSpaceTabType.favorites: return MobileFavoriteSpace(userProfile: widget.userProfile); + default: + throw Exception('Unknown tab type: $tab'); } }).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: '', - layout: layout, - openAfterCreate: true, - ), - ); - } else if (layout == ViewLayoutPB.Document) { - // only support create document in section - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: '', - index: 0, - viewSection: FolderSpaceType.public.toViewSectionPB, - ), - ); - } - } - - void _leaveWorkspace() { - final workspaceId = - context.read().state.currentWorkspace?.workspaceId; - if (workspaceId == null) { - return Log.error('Workspace ID is null'); - } - context - .read() - .add(UserWorkspaceEvent.leaveWorkspace(workspaceId)); - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/create_workspace_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/create_workspace_menu.dart deleted file mode 100644 index 741cbd6fe9..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/create_workspace_menu.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -enum EditWorkspaceNameType { - create, - edit; - - String get title { - switch (this) { - case EditWorkspaceNameType.create: - return LocaleKeys.workspace_create.tr(); - case EditWorkspaceNameType.edit: - return LocaleKeys.workspace_renameWorkspace.tr(); - } - } - - String get actionTitle { - switch (this) { - case EditWorkspaceNameType.create: - return LocaleKeys.workspace_create.tr(); - case EditWorkspaceNameType.edit: - return LocaleKeys.button_confirm.tr(); - } - } -} - -class EditWorkspaceNameBottomSheet extends StatefulWidget { - const EditWorkspaceNameBottomSheet({ - super.key, - required this.type, - required this.onSubmitted, - required this.workspaceName, - this.hintText, - this.validator, - this.validatorBuilder, - }); - - final EditWorkspaceNameType type; - final void Function(String) onSubmitted; - - // if the workspace name is not empty, it will be used as the initial value of the text field. - final String? workspaceName; - - final String? hintText; - - final String? Function(String?)? validator; - - final WidgetBuilder? validatorBuilder; - - @override - State createState() => - _EditWorkspaceNameBottomSheetState(); -} - -class _EditWorkspaceNameBottomSheetState - extends State { - late final TextEditingController _textFieldController; - - final _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - _textFieldController = TextEditingController( - text: widget.workspaceName, - ); - } - - @override - void dispose() { - _textFieldController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Form( - key: _formKey, - child: TextFormField( - autofocus: true, - controller: _textFieldController, - keyboardType: TextInputType.text, - decoration: InputDecoration( - hintText: - widget.hintText ?? LocaleKeys.workspace_defaultName.tr(), - ), - validator: widget.validator ?? - (value) { - if (value == null || value.isEmpty) { - return LocaleKeys.workspace_workspaceNameCannotBeEmpty.tr(); - } - return null; - }, - onEditingComplete: _onSubmit, - ), - ), - if (widget.validatorBuilder != null) ...[ - const VSpace(4), - widget.validatorBuilder!(context), - const VSpace(4), - ], - const VSpace(16), - SizedBox( - width: double.infinity, - child: PrimaryRoundedButton( - text: widget.type.actionTitle, - fontSize: 16, - margin: const EdgeInsets.symmetric( - vertical: 16, - ), - onTap: _onSubmit, - ), - ), - ], - ); - } - - void _onSubmit() { - if (_formKey.currentState!.validate()) { - final value = _textFieldController.text; - widget.onSubmitted.call(value); - } - } -} 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 d306f48964..17962bcc3c 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 @@ -1,23 +1,16 @@ 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/widgets/widgets.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy_backend/log.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 'create_workspace_menu.dart'; -import 'workspace_more_options.dart'; - // Only works on mobile. class MobileWorkspaceMenu extends StatelessWidget { const MobileWorkspaceMenu({ @@ -35,13 +28,13 @@ class MobileWorkspaceMenu extends StatelessWidget { @override Widget build(BuildContext context) { - // user profile final List children = [ _WorkspaceUserItem(userProfile: userProfile), - _buildDivider(), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), + child: Divider(height: 0.5), + ), ]; - - // workspace list for (var i = 0; i < workspaces.length; i++) { final workspace = workspaces[i]; children.add( @@ -55,98 +48,10 @@ class MobileWorkspaceMenu extends StatelessWidget { ), ); } - - // create workspace button - children.addAll([ - _buildDivider(), - const _CreateWorkspaceButton(), - ]); - return Column( - mainAxisSize: MainAxisSize.min, children: children, ); } - - Widget _buildDivider() { - return const Padding( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Divider(height: 0.5), - ); - } -} - -class _CreateWorkspaceButton extends StatelessWidget { - const _CreateWorkspaceButton(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: FlowyOptionTile.text( - height: 60, - showTopBorder: false, - showBottomBorder: false, - leftIcon: _buildLeftIcon(context), - onTap: () => _showCreateWorkspaceBottomSheet(context), - content: Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: FlowyText.medium( - LocaleKeys.workspace_create.tr(), - fontSize: 14, - ), - ), - ), - ), - ); - } - - void _showCreateWorkspaceBottomSheet(BuildContext context) { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.workspace_create.tr(), - showCloseButton: true, - showDragHandle: true, - showDivider: false, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) { - return EditWorkspaceNameBottomSheet( - type: EditWorkspaceNameType.create, - workspaceName: LocaleKeys.workspace_defaultName.tr(), - onSubmitted: (name) { - // create a new workspace - Log.info('create a new workspace: $name'); - bottomSheetContext.popToHome(); - - context.read().add( - UserWorkspaceEvent.createWorkspace( - name, - AuthTypePB.Server, - ), - ); - }, - ); - }, - ); - } - - Widget _buildLeftIcon(BuildContext context) { - return Container( - width: 36.0, - height: 36.0, - padding: const EdgeInsets.all(7.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0x01717171).withValues(alpha: 0.12), - width: 0.8, - ), - ), - child: const FlowySvg(FlowySvgs.add_workspace_s), - ); - } } class _WorkspaceUserItem extends StatelessWidget { @@ -202,288 +107,59 @@ class _WorkspaceMenuItem extends StatelessWidget { )..add(const WorkspaceMemberEvent.initial()), child: BlocBuilder( builder: (context, state) { + final members = state.members; return FlowyOptionTile.text( + content: Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText( + workspace.name, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + FlowyText( + state.isLoading + ? '' + : LocaleKeys.settings_appearance_members_membersCount + .plural( + members.length, + ), + fontSize: 10.0, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + ), height: 60, showTopBorder: showTopBorder, showBottomBorder: false, - leftIcon: _WorkspaceMenuItemIcon(workspace: workspace), - trailing: _WorkspaceMenuItemTrailing( + leftIcon: WorkspaceIcon( + enableEdit: false, + iconSize: 26, + fontSize: 16.0, workspace: workspace, - currentWorkspace: currentWorkspace, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + workspace.workspaceId, + result.emoji, + ), + ), ), + trailing: workspace.workspaceId == currentWorkspace.workspaceId + ? const FlowySvg( + FlowySvgs.m_blue_check_s, + blendMode: null, + ) + : null, onTap: () => onWorkspaceSelected(workspace), - content: Expanded( - child: _WorkspaceMenuItemContent( - workspace: workspace, - ), - ), ); }, ), ); } } - -// - Workspace name -// - Workspace member count -class _WorkspaceMenuItemContent extends StatelessWidget { - const _WorkspaceMenuItemContent({ - required this.workspace, - }); - - final UserWorkspacePB workspace; - - @override - Widget build(BuildContext context) { - final memberCount = workspace.memberCount.toInt(); - return Padding( - padding: const EdgeInsets.only(left: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowyText( - workspace.name, - fontSize: 14, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - FlowyText( - memberCount == 0 - ? '' - : LocaleKeys.settings_appearance_members_membersCount.plural( - memberCount, - ), - fontSize: 10.0, - color: Theme.of(context).hintColor, - ), - ], - ), - ); - } -} - -class _WorkspaceMenuItemIcon extends StatelessWidget { - const _WorkspaceMenuItemIcon({ - required this.workspace, - }); - - final UserWorkspacePB workspace; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: WorkspaceIcon( - enableEdit: false, - iconSize: 36, - emojiSize: 24.0, - fontSize: 18.0, - figmaLineHeight: 26.0, - borderRadius: 12.0, - workspace: workspace, - onSelected: (result) => context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - workspace.workspaceId, - result.emoji, - ), - ), - ), - ); - } -} - -class _WorkspaceMenuItemTrailing extends StatelessWidget { - const _WorkspaceMenuItemTrailing({ - required this.workspace, - required this.currentWorkspace, - }); - - final UserWorkspacePB workspace; - final UserWorkspacePB currentWorkspace; - - @override - Widget build(BuildContext context) { - const iconSize = Size.square(20); - return Row( - children: [ - const HSpace(12.0), - // show the check icon if the workspace is the current workspace - if (workspace.workspaceId == currentWorkspace.workspaceId) - const FlowySvg( - FlowySvgs.m_blue_check_s, - size: iconSize, - blendMode: null, - ), - const HSpace(8.0), - // more options button - AnimatedGestureDetector( - onTapUp: () => _showMoreOptions(context), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: FlowySvg( - FlowySvgs.workspace_three_dots_s, - size: iconSize, - ), - ), - ), - ], - ); - } - - void _showMoreOptions(BuildContext context) { - final actions = - context.read().state.myRole == AFRolePB.Owner - ? [ - // only the owner can update workspace properties - WorkspaceMenuMoreOption.rename, - WorkspaceMenuMoreOption.delete, - ] - : [ - WorkspaceMenuMoreOption.leave, - ]; - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (bottomSheetContext) { - return WorkspaceMenuMoreOptions( - actions: actions, - onAction: (action) => _onActions(context, bottomSheetContext, action), - ); - }, - ); - } - - void _onActions( - BuildContext context, - BuildContext bottomSheetContext, - WorkspaceMenuMoreOption action, - ) { - Log.info('execute action in workspace menu bottom sheet: $action'); - - switch (action) { - case WorkspaceMenuMoreOption.rename: - _showRenameWorkspaceBottomSheet(context); - break; - case WorkspaceMenuMoreOption.invite: - _pushToInviteMembersPage(context); - break; - case WorkspaceMenuMoreOption.delete: - _deleteWorkspace(context, bottomSheetContext); - break; - case WorkspaceMenuMoreOption.leave: - _leaveWorkspace(context, bottomSheetContext); - break; - } - } - - void _pushToInviteMembersPage(BuildContext context) { - // empty implementation - // we don't support invite members in workspace menu - } - - void _showRenameWorkspaceBottomSheet(BuildContext context) { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.workspace_renameWorkspace.tr(), - showCloseButton: true, - showDragHandle: true, - showDivider: false, - padding: const EdgeInsets.symmetric(horizontal: 16), - builder: (bottomSheetContext) { - return EditWorkspaceNameBottomSheet( - type: EditWorkspaceNameType.edit, - workspaceName: workspace.name, - onSubmitted: (name) { - // rename the workspace - Log.info('rename the workspace: $name'); - bottomSheetContext.popToHome(); - - context.read().add( - UserWorkspaceEvent.renameWorkspace( - workspace.workspaceId, - name, - ), - ); - }, - ); - }, - ); - } - - void _deleteWorkspace(BuildContext context, BuildContext bottomSheetContext) { - Navigator.of(bottomSheetContext).pop(); - - _showConfirmDialog( - context, - '${LocaleKeys.space_delete.tr()}: ${workspace.name}', - LocaleKeys.workspace_deleteWorkspaceHintText.tr(), - LocaleKeys.button_delete.tr(), - (_) async { - context.read().add( - UserWorkspaceEvent.deleteWorkspace( - workspace.workspaceId, - ), - ); - context.popToHome(); - }, - ); - } - - void _leaveWorkspace(BuildContext context, BuildContext bottomSheetContext) { - Navigator.of(bottomSheetContext).pop(); - - _showConfirmDialog( - context, - '${LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_title.tr()}: ${workspace.name}', - LocaleKeys.settings_workspacePage_leaveWorkspacePrompt_content.tr(), - LocaleKeys.button_confirm.tr(), - (_) async { - context.read().add( - UserWorkspaceEvent.leaveWorkspace( - workspace.workspaceId, - ), - ); - context.popToHome(); - }, - ); - } - - void _showConfirmDialog( - BuildContext context, - String title, - String content, - String rightButtonText, - void Function(BuildContext context)? onRightButtonPressed, - ) { - showFlowyCupertinoConfirmDialog( - title: title, - content: FlowyText( - content, - fontSize: 14, - color: Theme.of(context).hintColor, - maxLines: 10, - ), - leftButton: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), - ), - rightButton: FlowyText( - rightButtonText, - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, - color: const Color(0xFFFE0220), - ), - onRightButtonPressed: onRightButtonPressed, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart deleted file mode 100644 index bb6f6207f6..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_more_options.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -enum WorkspaceMenuMoreOption { - rename, - invite, - delete, - leave, -} - -class WorkspaceMenuMoreOptions extends StatelessWidget { - const WorkspaceMenuMoreOptions({ - super.key, - this.isFavorite = false, - required this.onAction, - required this.actions, - }); - - final bool isFavorite; - final void Function(WorkspaceMenuMoreOption action) onAction; - final List actions; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: actions - .map( - (action) => _buildActionButton(context, action), - ) - .toList(), - ); - } - - Widget _buildActionButton( - BuildContext context, - WorkspaceMenuMoreOption action, - ) { - switch (action) { - case WorkspaceMenuMoreOption.rename: - return FlowyOptionTile.text( - text: LocaleKeys.button_rename.tr(), - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.view_item_rename_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - WorkspaceMenuMoreOption.rename, - ), - ); - case WorkspaceMenuMoreOption.delete: - return FlowyOptionTile.text( - text: LocaleKeys.button_delete.tr(), - height: 52.0, - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(18), - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - WorkspaceMenuMoreOption.delete, - ), - ); - case WorkspaceMenuMoreOption.invite: - return FlowyOptionTile.text( - // i18n - text: 'Invite', - height: 52.0, - leftIcon: const FlowySvg( - FlowySvgs.workspace_add_member_s, - size: Size.square(18), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - WorkspaceMenuMoreOption.invite, - ), - ); - case WorkspaceMenuMoreOption.leave: - return FlowyOptionTile.text( - text: LocaleKeys.workspace_leaveCurrentWorkspace.tr(), - height: 52.0, - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.leave_workspace_s, - size: const Size.square(18), - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => onAction( - WorkspaceMenuMoreOption.leave, - ), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_handler.dart b/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_handler.dart deleted file mode 100644 index 74d5e56bce..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_handler.dart +++ /dev/null @@ -1,253 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -import 'mobile_inline_actions_menu_group.dart'; - -extension _StartWithsSort on List { - void sortByStartsWithKeyword(String search) => sort( - (a, b) { - final aCount = a.startsWithKeywords - ?.where( - (key) => search.toLowerCase().startsWith(key), - ) - .length ?? - 0; - - final bCount = b.startsWithKeywords - ?.where( - (key) => search.toLowerCase().startsWith(key), - ) - .length ?? - 0; - - if (aCount > bCount) { - return -1; - } else if (bCount > aCount) { - return 1; - } - - return 0; - }, - ); -} - -const _invalidSearchesAmount = 10; - -class MobileInlineActionsHandler extends StatefulWidget { - const MobileInlineActionsHandler({ - super.key, - required this.results, - required this.editorState, - required this.menuService, - required this.onDismiss, - required this.style, - required this.service, - this.startCharAmount = 1, - this.startOffset = 0, - this.cancelBySpaceHandler, - }); - - final List results; - final EditorState editorState; - final InlineActionsMenuService menuService; - final VoidCallback onDismiss; - final InlineActionsMenuStyle style; - final int startCharAmount; - final InlineActionsService service; - final bool Function()? cancelBySpaceHandler; - final int startOffset; - - @override - State createState() => - _MobileInlineActionsHandlerState(); -} - -class _MobileInlineActionsHandlerState - extends State { - final _focusNode = - FocusNode(debugLabel: 'mobile_inline_actions_menu_handler'); - - late List results = widget.results; - int invalidCounter = 0; - late int startOffset; - - String _search = ''; - - set search(String search) { - _search = search; - _doSearch(); - } - - Future _doSearch() async { - final List newResults = []; - for (final handler in widget.service.handlers) { - final group = await handler.search(_search); - - if (group.results.isNotEmpty) { - newResults.add(group); - } - } - - invalidCounter = results.every((group) => group.results.isEmpty) - ? invalidCounter + 1 - : 0; - - if (invalidCounter >= _invalidSearchesAmount) { - widget.onDismiss(); - - // Workaround to bring focus back to editor - await editorState.updateSelectionWithReason(editorState.selection); - - return; - } - - _resetSelection(); - - newResults.sortByStartsWithKeyword(_search); - setState(() => results = newResults); - } - - void _resetSelection() { - _selectedGroup = 0; - _selectedIndex = 0; - } - - int _selectedGroup = 0; - int _selectedIndex = 0; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => _focusNode.requestFocus(), - ); - - startOffset = editorState.selection?.endIndex ?? 0; - keepEditorFocusNotifier.increase(); - editorState.selectionNotifier.addListener(onSelectionChanged); - } - - @override - void dispose() { - editorState.selectionNotifier.removeListener(onSelectionChanged); - _focusNode.dispose(); - keepEditorFocusNotifier.decrease(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final width = editorState.renderBox!.size.width - 24 * 2; - return Focus( - focusNode: _focusNode, - child: Container( - constraints: BoxConstraints( - maxHeight: 192, - minWidth: width, - maxWidth: width, - ), - margin: EdgeInsets.symmetric(horizontal: 24.0), - decoration: BoxDecoration( - color: widget.style.backgroundColor, - borderRadius: BorderRadius.circular(6.0), - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - ), - child: noResults - ? SizedBox( - width: 150, - child: FlowyText.regular( - LocaleKeys.inlineActions_noResults.tr(), - ), - ) - : SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: Material( - color: Colors.transparent, - child: Padding( - padding: EdgeInsets.all(6.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: results - .where((g) => g.results.isNotEmpty) - .mapIndexed( - (index, group) => MobileInlineActionsGroup( - result: group, - editorState: editorState, - menuService: menuService, - style: widget.style, - onSelected: widget.onDismiss, - startOffset: startOffset - widget.startCharAmount, - endOffset: - _search.length + widget.startCharAmount, - isLastGroup: index == results.length - 1, - isGroupSelected: _selectedGroup == index, - selectedIndex: _selectedIndex, - onPreSelect: (int value) { - setState(() { - _selectedGroup = index; - _selectedIndex = value; - }); - }, - ), - ) - .toList(), - ), - ), - ), - ), - ), - ); - } - - bool get noResults => - results.isEmpty || results.every((e) => e.results.isEmpty); - - int get groupLength => results.length; - - int lengthOfGroup(int index) => - results.length > index ? results[index].results.length : -1; - - InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) => - results[groupIndex].results[handlerIndex]; - - EditorState get editorState => widget.editorState; - - InlineActionsMenuService get menuService => widget.menuService; - - void onSelectionChanged() { - final selection = editorState.selection; - if (selection == null) { - menuService.dismiss(); - return; - } - if (!selection.isCollapsed) { - menuService.dismiss(); - return; - } - final startOffset = widget.startOffset; - final endOffset = selection.end.offset; - if (endOffset < startOffset) { - menuService.dismiss(); - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - final text = node?.delta?.toPlainText() ?? ''; - final search = text.substring(startOffset, endOffset); - this.search = search; - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart deleted file mode 100644 index 6166671391..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'mobile_inline_actions_handler.dart'; - -class MobileInlineActionsMenu extends InlineActionsMenuService { - MobileInlineActionsMenu({ - required this.context, - required this.editorState, - required this.initialResults, - required this.style, - required this.service, - this.startCharAmount = 1, - this.cancelBySpaceHandler, - }); - - final BuildContext context; - final EditorState editorState; - final List initialResults; - final bool Function()? cancelBySpaceHandler; - final InlineActionsService service; - - @override - final InlineActionsMenuStyle style; - - final int startCharAmount; - - OverlayEntry? _menuEntry; - - @override - void dismiss() { - if (_menuEntry != null) { - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); - } - - _menuEntry?.remove(); - _menuEntry = null; - } - - @override - Future show() { - final completer = Completer(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _show(); - completer.complete(); - }); - return completer.future; - } - - void _show() { - final selectionRects = editorState.selectionRects(); - if (selectionRects.isEmpty) { - return; - } - - const double menuHeight = 192.0; - const Offset menuOffset = Offset(0, 10); - final Offset editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final Size editorSize = editorState.renderBox!.size; - - // Default to opening the overlay below - Alignment alignment = Alignment.topLeft; - - final firstRect = selectionRects.first; - Offset offset = firstRect.bottomRight + menuOffset; - - // Show above - if (offset.dy + menuHeight >= editorOffset.dy + editorSize.height) { - offset = firstRect.topRight - menuOffset; - alignment = Alignment.bottomLeft; - - offset = Offset( - offset.dx, - MediaQuery.of(context).size.height - offset.dy, - ); - } - - final (left, top, right, bottom) = _getPosition(alignment, offset); - - _menuEntry = OverlayEntry( - builder: (context) => SizedBox( - width: editorSize.width, - height: editorSize.height, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - Positioned( - top: top, - bottom: bottom, - left: left, - right: right, - child: MobileInlineActionsHandler( - service: service, - results: initialResults, - editorState: editorState, - menuService: this, - onDismiss: dismiss, - style: style, - startCharAmount: startCharAmount, - cancelBySpaceHandler: cancelBySpaceHandler, - startOffset: editorState.selection?.start.offset ?? 0, - ), - ), - ], - ), - ), - ), - ); - - Overlay.of(context).insert(_menuEntry!); - - editorState.service.keyboardService?.disable(showCursor: true); - editorState.service.scrollService?.disable(); - } - - (double? left, double? top, double? right, double? bottom) _getPosition( - Alignment alignment, - Offset offset, - ) { - double? left, top, right, bottom; - switch (alignment) { - case Alignment.topLeft: - left = 0; - top = offset.dy; - break; - case Alignment.bottomLeft: - left = 0; - bottom = offset.dy; - break; - case Alignment.topRight: - right = offset.dx; - top = offset.dy; - break; - case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; - break; - } - - return (left, top, right, bottom); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart deleted file mode 100644 index f340319254..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/inline_actions/mobile_inline_actions_menu_group.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class MobileInlineActionsGroup extends StatelessWidget { - const MobileInlineActionsGroup({ - super.key, - required this.result, - required this.editorState, - required this.menuService, - required this.style, - required this.onSelected, - required this.startOffset, - required this.endOffset, - required this.onPreSelect, - this.isLastGroup = false, - this.isGroupSelected = false, - this.selectedIndex = 0, - }); - - final InlineActionsResult result; - final EditorState editorState; - final InlineActionsMenuService menuService; - final InlineActionsMenuStyle style; - final VoidCallback onSelected; - final ValueChanged onPreSelect; - final int startOffset; - final int endOffset; - - final bool isLastGroup; - final bool isGroupSelected; - final int selectedIndex; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (result.title != null) ...[ - SizedBox( - height: 36, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.medium( - result.title!, - color: style.groupTextColor, - fontSize: 12, - ), - ), - ), - ), - ], - ...result.results.mapIndexed( - (index, item) => GestureDetector( - onTapDown: (e) { - onPreSelect.call(index); - }, - child: MobileInlineActionsWidget( - item: item, - editorState: editorState, - menuService: menuService, - isSelected: isGroupSelected && index == selectedIndex, - style: style, - onSelected: onSelected, - startOffset: startOffset, - endOffset: endOffset, - ), - ), - ), - ], - ); - } -} - -class MobileInlineActionsWidget extends StatelessWidget { - const MobileInlineActionsWidget({ - super.key, - required this.item, - required this.editorState, - required this.menuService, - required this.isSelected, - required this.style, - required this.onSelected, - required this.startOffset, - required this.endOffset, - }); - - final InlineActionsMenuItem item; - final EditorState editorState; - final InlineActionsMenuService menuService; - final bool isSelected; - final InlineActionsMenuStyle style; - final VoidCallback onSelected; - final int startOffset; - final int endOffset; - - @override - Widget build(BuildContext context) { - final hasIcon = item.iconBuilder != null; - return Container( - height: 36, - decoration: BoxDecoration( - color: isSelected ? style.menuItemSelectedColor : null, - borderRadius: BorderRadius.circular(6.0), - ), - child: FlowyButton( - expand: true, - isSelected: isSelected, - text: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Align( - alignment: Alignment.centerLeft, - child: Row( - children: [ - if (hasIcon) ...[ - item.iconBuilder!.call(isSelected), - SizedBox(width: 12), - ], - Flexible( - child: FlowyText.regular( - item.label, - figmaLineHeight: 18, - overflow: TextOverflow.ellipsis, - fontSize: 16, - color: style.menuItemSelectedTextColor, - ), - ), - ], - ), - ), - ), - onTap: () => _onPressed(context), - ), - ); - } - - void _onPressed(BuildContext context) { - onSelected(); - item.onSelected?.call( - context, - editorState, - menuService, - (startOffset, endOffset), - ); - } -} 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 3c6adb8627..5d33dfa500 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,77 +1,40 @@ -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'; -enum BottomNavigationBarActionType { - home, - notificationMultiSelect, -} - -final PropertyValueNotifier mobileCreateNewPageNotifier = +final PropertyValueNotifier createNewPageNotifier = PropertyValueNotifier(null); -final ValueNotifier bottomNavigationBarType = - ValueNotifier(BottomNavigationBarActionType.home); - -enum BottomNavigationBarItemType { - home, - add, - notification; - - String get label { - return switch (this) { - BottomNavigationBarItemType.home => 'home', - BottomNavigationBarItemType.add => 'add', - BottomNavigationBarItemType.notification => 'notification', - }; - } - - ValueKey get valueKey { - return ValueKey(label); - } -} +const _homeLabel = 'home'; +const _addLabel = 'add'; +const _notificationLabel = 'notification'; final _items = [ - BottomNavigationBarItem( - key: BottomNavigationBarItemType.home.valueKey, - label: BottomNavigationBarItemType.home.label, - icon: const FlowySvg(FlowySvgs.m_home_unselected_m), - activeIcon: const FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null), + const BottomNavigationBarItem( + label: _homeLabel, + icon: FlowySvg(FlowySvgs.m_home_unselected_m), + activeIcon: FlowySvg(FlowySvgs.m_home_selected_m, blendMode: null), ), - BottomNavigationBarItem( - key: BottomNavigationBarItemType.add.valueKey, - label: BottomNavigationBarItemType.add.label, - icon: const FlowySvg(FlowySvgs.m_home_add_m), + const BottomNavigationBarItem( + label: _addLabel, + icon: FlowySvg(FlowySvgs.m_home_add_m), ), - BottomNavigationBarItem( - key: BottomNavigationBarItemType.notification.valueKey, - label: BottomNavigationBarItemType.notification.label, - icon: const _NotificationNavigationBarItemIcon(), - activeIcon: const _NotificationNavigationBarItemIcon( - isActive: true, + const BottomNavigationBarItem( + label: _notificationLabel, + icon: FlowySvg(FlowySvgs.m_home_notification_m), + activeIcon: FlowySvg( + FlowySvgs.m_home_notification_m, ), ), ]; /// 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 StatefulWidget { +class MobileBottomNavigationBar extends StatelessWidget { /// Constructs an [MobileBottomNavigationBar]. const MobileBottomNavigationBar({ required this.navigationShell, @@ -81,140 +44,30 @@ class MobileBottomNavigationBar extends StatefulWidget { /// 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) { - _bottomNavigationBar = switch (bottomNavigationBarType.value) { - BottomNavigationBarActionType.home => - _buildHomePageNavigationBar(context), - BottomNavigationBarActionType.notificationMultiSelect => - _buildNotificationNavigationBar(context), - }; - + final isLightMode = Theme.of(context).isLightMode; + final backgroundColor = isLightMode + ? Colors.white.withOpacity(0.95) + : const Color(0xFF23262B).withOpacity(0.95); return Scaffold( - body: widget.navigationShell, + body: navigationShell, extendBody: true, - 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, - ) - : 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, + bottomNavigationBar: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 3, + sigmaY: 3, ), - child: Theme( - data: _getThemeData(context), + child: DecoratedBox( + decoration: BoxDecoration( + border: isLightMode + ? Border( + top: BorderSide(color: Theme.of(context).dividerColor), + ) + : null, + color: backgroundColor, + ), child: BottomNavigationBar( showSelectedLabels: false, showUnselectedLabels: false, @@ -232,32 +85,13 @@ class _HomePageNavigationBar 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) { - // close the popup menu - closePopupMenu(); - - final label = _items[bottomBarIndex].label; - if (label == BottomNavigationBarItemType.add.label) { + if (_items[bottomBarIndex].label == _addLabel) { // show an add dialog - mobileCreateNewPageNotifier.value = ViewLayoutPB.Document; + createNewPageNotifier.value = ViewLayoutPB.Document; return; - } else if (label == BottomNavigationBarItemType.notification.label) { - 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 @@ -272,110 +106,3 @@ class _HomePageNavigationBar 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( - 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( - 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.withValues(alpha: 0.95) - : const Color(0xFF23262B).withValues(alpha: 0.95); - } - - Color get borderColor { - return Theme.of(this).isLightMode - ? const Color(0x141F2329) - : const Color(0xFF23262B).withValues(alpha: 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 deleted file mode 100644 index cf7ce35e80..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart +++ /dev/null @@ -1,59 +0,0 @@ -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_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index 33c2eb3905..64e3e8824d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -28,8 +28,8 @@ class MobileNotificationsScreen extends StatefulWidget { class _MobileNotificationsScreenState extends State with SingleTickerProviderStateMixin { - final ReminderBloc reminderBloc = getIt(); - late final TabController controller = TabController(length: 2, vsync: this); + final ReminderBloc _reminderBloc = getIt(); + late final TabController _controller = TabController(length: 2, vsync: this); @override Widget build(BuildContext context) { @@ -39,7 +39,7 @@ class _MobileNotificationsScreenState extends State create: (context) => UserProfileBloc()..add(const UserProfileEvent.started()), ), - BlocProvider.value(value: reminderBloc), + BlocProvider.value(value: _reminderBloc), BlocProvider( create: (_) => NotificationFilterBloc(), ), @@ -50,12 +50,12 @@ class _MobileNotificationsScreenState extends State orElse: () => const Center(child: CircularProgressIndicator.adaptive()), workspaceFailure: () => const WorkspaceFailedScreen(), - success: (workspaceLatest, userProfile) => + success: (workspaceSetting, userProfile) => _NotificationScreenContent( - workspaceLatest: workspaceLatest, + workspaceSetting: workspaceSetting, userProfile: userProfile, - controller: controller, - reminderBloc: reminderBloc, + controller: _controller, + reminderBloc: _reminderBloc, ), ); }, @@ -66,13 +66,13 @@ class _MobileNotificationsScreenState extends State class _NotificationScreenContent extends StatelessWidget { const _NotificationScreenContent({ - required this.workspaceLatest, + required this.workspaceSetting, required this.userProfile, required this.controller, required this.reminderBloc, }); - final WorkspaceLatestPB workspaceLatest; + final WorkspaceSettingPB workspaceSetting; final UserProfilePB userProfile; final TabController controller; final ReminderBloc reminderBloc; @@ -84,7 +84,7 @@ class _NotificationScreenContent extends StatelessWidget { ..add( SidebarSectionsEvent.initial( userProfile, - workspaceLatest.workspaceId, + workspaceSetting.workspaceId, ), ), child: BlocBuilder( @@ -124,6 +124,7 @@ class _NotificationScreenContent extends StatelessWidget { reminderBloc: reminderBloc, views: sectionState.section.publicViews, onAction: _onAction, + onDelete: _onDelete, onReadChanged: _onReadChanged, actionBar: InboxActionBar( hasUnreads: state.hasUnreads, @@ -160,6 +161,9 @@ class _NotificationScreenContent extends StatelessWidget { ), ); + void _onDelete(ReminderPB reminder) => + reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id)); + void _onReadChanged(ReminderPB reminder, bool isRead) => reminderBloc.add( ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)), ); 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 deleted file mode 100644 index 54a0b4e782..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart +++ /dev/null @@ -1,120 +0,0 @@ -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 deleted file mode 100644 index e11e91ada5..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart +++ /dev/null @@ -1,11 +0,0 @@ -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).withValues(alpha: 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 deleted file mode 100644 index e5598cc6e5..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart +++ /dev/null @@ -1,58 +0,0 @@ -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 deleted file mode 100644 index 9f1311d1e7..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart +++ /dev/null @@ -1,96 +0,0 @@ -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/mobile_notification_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart index 59bfe61822..471b456bc0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/mobile_notification_tab_bar.dart @@ -21,12 +21,6 @@ class _MobileNotificationTabBarState extends State { widget.controller.addListener(_updateState); } - @override - void dispose() { - widget.controller.removeListener(_updateState); - super.dispose(); - } - void _updateState() => setState(() {}); @override 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 deleted file mode 100644 index ea57d5d391..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart +++ /dev/null @@ -1,115 +0,0 @@ -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 deleted file mode 100644 index e5fe288755..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart +++ /dev/null @@ -1,168 +0,0 @@ -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; - final blockId = state.blockId; - if (view == null) { - return; - } - - await context.pushView( - view, - blockId: blockId, - ); - - 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 deleted file mode 100644 index e694f9932d..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart +++ /dev/null @@ -1,167 +0,0 @@ -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( - message: LocaleKeys - .settings_notifications_markAsReadNotifications_allSuccess - .tr(), - ); - - context.read().add(const ReminderEvent.markAllRead()); - } - - void _onArchiveAll(BuildContext context) { - showToastNotification( - message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess - .tr(), - ); - - context.read().add(const ReminderEvent.archiveAll()); - } - - void _onUnarchiveAll(BuildContext context) { - if (!kDebugMode) { - return; - } - - showToastNotification( - 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 deleted file mode 100644 index 70cc8c5214..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/shared.dart +++ /dev/null @@ -1,291 +0,0 @@ -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'; -import 'package:universal_platform/universal_platform.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 = buildBlockComponentBuilders( - context: context, - editorState: editorState, - styleCustomizer: styleCustomizer, - // the editor is not editable in the chat - editable: false, - customHeadingPadding: UniversalPlatform.isDesktop - ? EdgeInsets.zero - : EdgeInsets.symmetric( - vertical: EditorStyleCustomizer.nodeHorizontalPadding, - ), - ); - - 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 deleted file mode 100644 index d1216eed98..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart +++ /dev/null @@ -1,208 +0,0 @@ -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( - message: LocaleKeys - .settings_notifications_markAsReadNotifications_success - .tr(), - ); - - context.read().add( - ReminderEvent.update( - ReminderUpdate( - id: context.read().reminder.id, - isRead: true, - ), - ), - ); - }, - ); - // this action is only used in the debug mode. - case NotificationPaneActionType.unArchive: - return MobileSlideActionButton( - backgroundColor: const Color(0xFF00C8FF), - svg: FlowySvgs.m_notification_action_mark_as_read_s, - size: 24.0, - onPressed: (context) { - showToastNotification( - message: 'Unarchive notification success', - ); - - context.read().add( - ReminderEvent.update( - ReminderUpdate( - id: context.read().reminder.id, - isArchived: false, - ), - ), - ); - }, - ); - case NotificationPaneActionType.more: - return MobileSlideActionButton( - backgroundColor: const Color(0xE5515563), - svg: FlowySvgs.three_dots_s, - size: 24.0, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10), - ), - onPressed: (context) { - final reminderBloc = context.read(); - final notificationReminderBloc = - context.read(); - - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (_) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: reminderBloc), - BlocProvider.value(value: notificationReminderBloc), - ], - child: _NotificationMoreActions( - onClickMultipleChoice: () { - Future.delayed(const Duration(milliseconds: 250), () { - bottomNavigationBarType.value = - BottomNavigationBarActionType - .notificationMultiSelect; - }); - }, - ), - ); - }, - ); - }, - ); - } - } -} - -class _NotificationMoreActions extends StatelessWidget { - const _NotificationMoreActions({ - required this.onClickMultipleChoice, - }); - - final VoidCallback onClickMultipleChoice; - - @override - Widget build(BuildContext context) { - final reminder = context.read().reminder; - return Column( - children: [ - if (!reminder.isRead) - FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.settings_notifications_action_markAsRead.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_notification_action_mark_as_read_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => _onMarkAsRead(context), - ), - FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.settings_notifications_action_multipleChoice.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_notification_action_multiple_choice_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => _onMultipleChoice(context), - ), - if (!reminder.isArchived) - FlowyOptionTile.text( - height: 52.0, - text: LocaleKeys.settings_notifications_action_archive.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_notification_action_archive_s, - size: Size.square(20), - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () => _onArchive(context), - ), - ], - ); - } - - void _onMarkAsRead(BuildContext context) { - Navigator.of(context).pop(); - - showToastNotification( - message: LocaleKeys.settings_notifications_markAsReadNotifications_success - .tr(), - ); - - context.read().add( - ReminderEvent.update( - ReminderUpdate( - id: context.read().reminder.id, - isRead: true, - ), - ), - ); - } - - void _onMultipleChoice(BuildContext context) { - Navigator.of(context).pop(); - - onClickMultipleChoice(); - } - - void _onArchive(BuildContext context) { - showToastNotification( - message: LocaleKeys.settings_notifications_archiveNotifications_success - .tr() - .tr(), - ); - - context.read().add( - ReminderEvent.update( - ReminderUpdate( - id: context.read().reminder.id, - isRead: true, - isArchived: true, - ), - ), - ); - - Navigator.of(context).pop(); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart deleted file mode 100644 index 45e801e07c..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; -import 'package:appflowy/shared/list_extension.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:appflowy_backend/appflowy_backend.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'; - -class NotificationTab extends StatefulWidget { - const NotificationTab({ - super.key, - required this.tabType, - }); - - final MobileNotificationTabType tabType; - - @override - State createState() => _NotificationTabState(); -} - -class _NotificationTabState extends State - with AutomaticKeepAliveClientMixin { - @override - bool get wantKeepAlive => true; - - @override - Widget build(BuildContext context) { - super.build(context); - - return BlocBuilder( - builder: (context, state) { - final reminders = _filterReminders(state.reminders); - - if (reminders.isEmpty) { - // add refresh indicator to the empty notification. - return EmptyNotification( - type: widget.tabType, - ); - } - - final child = ListView.separated( - itemCount: reminders.length, - separatorBuilder: (context, index) => const VSpace(8.0), - itemBuilder: (context, index) { - final reminder = reminders[index]; - return NotificationItem( - key: ValueKey('${widget.tabType}_${reminder.id}'), - tabType: widget.tabType, - reminder: reminder, - ); - }, - ); - - return RefreshIndicator.adaptive( - onRefresh: () async => _onRefresh(context), - child: child, - ); - }, - ); - } - - Future _onRefresh(BuildContext context) async { - context.read().add(const ReminderEvent.refresh()); - - // at least 0.5 seconds to dismiss the refresh indicator. - // otherwise, it will be dismissed immediately. - await context.read().stream.firstOrNull; - await Future.delayed(const Duration(milliseconds: 500)); - - if (context.mounted) { - showToastNotification( - message: LocaleKeys.settings_notifications_refreshSuccess.tr(), - ); - } - } - - List _filterReminders(List reminders) { - switch (widget.tabType) { - case MobileNotificationTabType.inbox: - return reminders.reversed - .where((reminder) => !reminder.isArchived) - .toList() - .unique((reminder) => reminder.id); - case MobileNotificationTabType.archive: - return reminders.reversed - .where((reminder) => reminder.isArchived) - .toList() - .unique((reminder) => reminder.id); - case MobileNotificationTabType.unread: - return reminders.reversed - .where((reminder) => !reminder.isRead) - .toList() - .unique((reminder) => reminder.id); - } - } -} - -class MultiSelectNotificationTab extends StatelessWidget { - const MultiSelectNotificationTab({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - // find the reminders that are not archived or read. - final reminders = state.reminders.reversed - .where((reminder) => !reminder.isArchived || !reminder.isRead) - .toList(); - - if (reminders.isEmpty) { - // add refresh indicator to the empty notification. - return const SizedBox.shrink(); - } - - return ListView.separated( - itemCount: reminders.length, - separatorBuilder: (context, index) => const VSpace(8.0), - itemBuilder: (context, index) { - final reminder = reminders[index]; - return MultiSelectNotificationItem( - key: ValueKey(reminder.id), - reminder: reminder, - ); - }, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart deleted file mode 100644 index 35fb6ea067..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/tab_bar.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; -import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:reorderable_tabbar/reorderable_tabbar.dart'; - -enum MobileNotificationTabType { - inbox, - unread, - archive; - - String get tr { - switch (this) { - case MobileNotificationTabType.inbox: - return LocaleKeys.settings_notifications_tabs_inbox.tr(); - case MobileNotificationTabType.unread: - return LocaleKeys.settings_notifications_tabs_unread.tr(); - case MobileNotificationTabType.archive: - return LocaleKeys.settings_notifications_tabs_archived.tr(); - } - } - - List get actions { - switch (this) { - case MobileNotificationTabType.inbox: - return [ - NotificationPaneActionType.more, - NotificationPaneActionType.markAsRead, - ]; - case MobileNotificationTabType.unread: - case MobileNotificationTabType.archive: - return []; - } - } -} - -class MobileNotificationTabBar extends StatelessWidget { - const MobileNotificationTabBar({ - super.key, - this.height = 38.0, - required this.tabController, - required this.tabs, - }); - - final double height; - final List tabs; - final TabController tabController; - - @override - Widget build(BuildContext context) { - final baseStyle = Theme.of(context).textTheme.bodyMedium; - final labelStyle = baseStyle?.copyWith( - 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( - height: height, - padding: const EdgeInsets.only(left: 8.0), - child: ReorderableTabBar( - controller: tabController, - tabs: tabs.map((e) => Tab(text: e.tr)).toList(), - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - labelStyle: labelStyle, - labelColor: baseStyle?.color, - labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), - unselectedLabelStyle: unselectedLabelStyle, - overlayColor: WidgetStateProperty.all(Colors.transparent), - indicator: const RoundUnderlineTabIndicator( - width: 28.0, - borderSide: BorderSide( - color: Color(0xFF00C8FF), - width: 3, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart deleted file mode 100644 index 92cd83a74e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/widgets.dart +++ /dev/null @@ -1,9 +0,0 @@ -export 'empty.dart'; -export 'header.dart'; -export 'multi_select_notification_item.dart'; -export 'notification_item.dart'; -export 'settings_popup_menu.dart'; -export 'shared.dart'; -export 'slide_actions.dart'; -export 'tab.dart'; -export 'tab_bar.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 6e2611a684..6ef969bb0b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -1,9 +1,5 @@ -import 'dart:io'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -123,7 +119,6 @@ class InnerMobileViewItem extends StatelessWidget { final bool isDraggable; final bool isExpanded; final bool isFirstChild; - // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; @@ -233,7 +228,6 @@ class SingleMobileInnerViewItem extends StatefulWidget { final ViewPB view; final ViewPB? parentView; final bool isExpanded; - // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; @@ -264,9 +258,8 @@ class _SingleMobileInnerViewItemState extends State { // title Expanded( child: FlowyText.regular( - widget.view.nameOrDefault, + widget.view.name, fontSize: 16.0, - figmaLineHeight: 20.0, overflow: TextOverflow.ellipsis, ), ), @@ -300,20 +293,16 @@ class _SingleMobileInnerViewItemState extends State { } Widget _buildViewIcon() { - final iconData = widget.view.icon.toEmojiIconData(); - final icon = iconData.isNotEmpty - ? EmojiIconWidget( - emoji: widget.view.icon.toEmojiIconData(), - emojiSize: Platform.isAndroid ? 16.0 : 18.0, + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 18.0, ) : Opacity( opacity: 0.7, - child: widget.view.defaultIcon(size: const Size.square(18)), + child: widget.view.defaultIcon(), ); - return SizedBox( - width: 18.0, - child: icon, - ); + return SizedBox(width: 18.0, child: icon); } // > button or · button diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart deleted file mode 100644 index f69360575a..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu.dart +++ /dev/null @@ -1,296 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'mobile_selection_menu_item_widget.dart'; -import 'mobile_selection_menu_widget.dart'; - -class MobileSelectionMenu extends SelectionMenuService { - MobileSelectionMenu({ - required this.context, - required this.editorState, - required this.selectionMenuItems, - this.deleteSlashByDefault = false, - this.deleteKeywordsByDefault = false, - this.style = MobileSelectionMenuStyle.light, - this.itemCountFilter = 0, - this.startOffset = 0, - this.singleColumn = false, - }); - - final BuildContext context; - final EditorState editorState; - final List selectionMenuItems; - final bool deleteSlashByDefault; - final bool deleteKeywordsByDefault; - final bool singleColumn; - - @override - final MobileSelectionMenuStyle style; - - OverlayEntry? _selectionMenuEntry; - Offset _offset = Offset.zero; - Alignment _alignment = Alignment.topLeft; - final int itemCountFilter; - final int startOffset; - ValueNotifier<_Position> _positionNotifier = ValueNotifier(_Position.zero); - - @override - void dismiss() { - if (_selectionMenuEntry != null) { - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); - editorState - .removeScrollViewScrolledListener(_checkPositionAfterScrolling); - _positionNotifier.dispose(); - } - - _selectionMenuEntry?.remove(); - _selectionMenuEntry = null; - } - - @override - Future show() async { - final completer = Completer(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _show(); - editorState.addScrollViewScrolledListener(_checkPositionAfterScrolling); - completer.complete(); - }); - return completer.future; - } - - void _show() { - final position = _getCurrentPosition(); - if (position == null) return; - - final editorHeight = editorState.renderBox!.size.height; - final editorWidth = editorState.renderBox!.size.width; - - _positionNotifier = ValueNotifier(position); - final showAtTop = position.top != null; - _selectionMenuEntry = OverlayEntry( - builder: (context) { - return SizedBox( - width: editorWidth, - height: editorHeight, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - ValueListenableBuilder( - valueListenable: _positionNotifier, - builder: (context, value, _) { - return Positioned( - top: value.top, - bottom: value.bottom, - left: value.left, - right: value.right, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: MobileSelectionMenuWidget( - selectionMenuStyle: style, - singleColumn: singleColumn, - showAtTop: showAtTop, - items: selectionMenuItems - ..forEach((element) { - if (element is MobileSelectionMenuItem) { - element.deleteSlash = false; - element.deleteKeywords = - deleteKeywordsByDefault; - for (final e in element.children) { - e.deleteSlash = deleteSlashByDefault; - e.deleteKeywords = deleteKeywordsByDefault; - e.onSelected = () { - dismiss(); - }; - } - } else { - element.deleteSlash = deleteSlashByDefault; - element.deleteKeywords = - deleteKeywordsByDefault; - element.onSelected = () { - dismiss(); - }; - } - }), - maxItemInRow: 5, - editorState: editorState, - itemCountFilter: itemCountFilter, - startOffset: startOffset, - menuService: this, - onExit: () { - dismiss(); - }, - deleteSlashByDefault: deleteSlashByDefault, - ), - ), - ); - }, - ), - ], - ), - ), - ); - }, - ); - - Overlay.of(context, rootOverlay: true).insert(_selectionMenuEntry!); - - editorState.service.keyboardService?.disable(showCursor: true); - editorState.service.scrollService?.disable(); - } - - /// the workaround for: editor auto scrolling that will cause wrong position - /// of slash menu - void _checkPositionAfterScrolling() { - final position = _getCurrentPosition(); - if (position == null) return; - if (position == _positionNotifier.value) { - Future.delayed(const Duration(milliseconds: 100)).then((_) { - final position = _getCurrentPosition(); - if (position == null) return; - if (position != _positionNotifier.value) { - _positionNotifier.value = position; - } - }); - } else { - _positionNotifier.value = position; - } - } - - _Position? _getCurrentPosition() { - final selectionRects = editorState.selectionRects(); - if (selectionRects.isEmpty) { - return null; - } - final screenSize = MediaQuery.of(context).size; - calculateSelectionMenuOffset(selectionRects.first, screenSize); - final (left, top, right, bottom) = getPosition(); - return _Position(left, top, right, bottom); - } - - @override - Alignment get alignment { - return _alignment; - } - - @override - Offset get offset { - return _offset; - } - - @override - (double? left, double? top, double? right, double? bottom) getPosition() { - double? left, top, right, bottom; - switch (alignment) { - case Alignment.topLeft: - left = offset.dx; - top = offset.dy; - break; - case Alignment.bottomLeft: - left = offset.dx; - bottom = offset.dy; - break; - case Alignment.topRight: - right = offset.dx; - top = offset.dy; - break; - case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; - break; - } - return (left, top, right, bottom); - } - - void calculateSelectionMenuOffset(Rect rect, Size screenSize) { - // Workaround: We can customize the padding through the [EditorStyle], - // but the coordinates of overlay are not properly converted currently. - // Just subtract the padding here as a result. - const menuHeight = 192.0, menuWidth = 240.0; - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = editorState.renderBox!.size.height; - final screenHeight = screenSize.height; - final editorWidth = editorState.renderBox!.size.width; - final rectHeight = rect.height; - - // show below default - _alignment = Alignment.bottomRight; - final bottomRight = rect.topLeft; - final offset = bottomRight; - final limitX = editorWidth + editorOffset.dx - menuWidth, - limitY = screenHeight - - editorHeight + - editorOffset.dy - - menuHeight - - rectHeight; - _offset = Offset( - editorWidth - offset.dx - menuWidth, - screenHeight - offset.dy - menuHeight - rectHeight, - ); - - if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { - /// show above - if (offset.dy > menuHeight) { - _offset = Offset( - _offset.dx, - offset.dy - menuHeight, - ); - _alignment = Alignment.topRight; - } else { - _offset = Offset( - _offset.dx, - limitY, - ); - } - } - - if (offset.dx + menuWidth >= editorOffset.dx + editorWidth) { - /// show left - if (offset.dx > menuWidth) { - _alignment = _alignment == Alignment.bottomRight - ? Alignment.bottomLeft - : Alignment.topLeft; - _offset = Offset( - offset.dx - menuWidth, - _offset.dy, - ); - } else { - _offset = Offset( - limitX, - _offset.dy, - ); - } - } - } -} - -class _Position { - const _Position(this.left, this.top, this.right, this.bottom); - - final double? left; - final double? top; - final double? right; - final double? bottom; - - static const _Position zero = _Position(0, 0, 0, 0); - - @override - bool operator ==(Object other) => - identical(this, other) || - other is _Position && - runtimeType == other.runtimeType && - left == other.left && - top == other.top && - right == other.right && - bottom == other.bottom; - - @override - int get hashCode => - left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item.dart deleted file mode 100644 index 22e202816e..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -class MobileSelectionMenuItem extends SelectionMenuItem { - MobileSelectionMenuItem({ - required super.getName, - required super.icon, - super.keywords = const [], - required super.handler, - this.children = const [], - super.nameBuilder, - super.deleteKeywords, - super.deleteSlash, - }); - - final List children; - - bool get isNotEmpty => children.isNotEmpty; -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart deleted file mode 100644 index bdee8f1857..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'mobile_selection_menu_item.dart'; - -class MobileSelectionMenuItemWidget extends StatelessWidget { - const MobileSelectionMenuItemWidget({ - super.key, - required this.editorState, - required this.menuService, - required this.item, - required this.isSelected, - required this.selectionMenuStyle, - required this.onTap, - }); - - final EditorState editorState; - final SelectionMenuService menuService; - final SelectionMenuItem item; - final bool isSelected; - final MobileSelectionMenuStyle selectionMenuStyle; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final style = selectionMenuStyle; - final showRightArrow = item is MobileSelectionMenuItem && - (item as MobileSelectionMenuItem).isNotEmpty; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: TextButton.icon( - icon: item.icon( - editorState, - false, - selectionMenuStyle, - ), - style: ButtonStyle( - alignment: Alignment.centerLeft, - overlayColor: WidgetStateProperty.all(Colors.transparent), - backgroundColor: isSelected - ? WidgetStateProperty.all( - style.selectionMenuItemSelectedColor, - ) - : WidgetStateProperty.all(Colors.transparent), - ), - label: Row( - children: [ - item.nameBuilder?.call(item.name, style, false) ?? - Text( - item.name, - textAlign: TextAlign.left, - style: TextStyle( - color: style.selectionMenuItemTextColor, - fontSize: 16.0, - ), - ), - if (showRightArrow) ...[ - Spacer(), - Icon( - Icons.keyboard_arrow_right_rounded, - color: style.selectionMenuItemRightIconColor, - ), - ], - ], - ), - onPressed: () { - onTap.call(); - item.handler( - editorState, - menuService, - context, - ); - }, - ), - ); - } -} - -class MobileSelectionMenuStyle extends SelectionMenuStyle { - const MobileSelectionMenuStyle({ - required super.selectionMenuBackgroundColor, - required super.selectionMenuItemTextColor, - required super.selectionMenuItemIconColor, - required super.selectionMenuItemSelectedTextColor, - required super.selectionMenuItemSelectedIconColor, - required super.selectionMenuItemSelectedColor, - required super.selectionMenuUnselectedLabelColor, - required super.selectionMenuDividerColor, - required super.selectionMenuLinkBorderColor, - required super.selectionMenuInvalidLinkColor, - required super.selectionMenuButtonColor, - required super.selectionMenuButtonTextColor, - required super.selectionMenuButtonIconColor, - required super.selectionMenuButtonBorderColor, - required super.selectionMenuTabIndicatorColor, - required this.selectionMenuItemRightIconColor, - }); - - final Color selectionMenuItemRightIconColor; - - static const MobileSelectionMenuStyle light = MobileSelectionMenuStyle( - selectionMenuBackgroundColor: Color(0xFFFFFFFF), - selectionMenuItemTextColor: Color(0xFF1F2225), - selectionMenuItemIconColor: Color(0xFF333333), - selectionMenuItemSelectedColor: Color(0xFFF2F5F7), - selectionMenuItemRightIconColor: Color(0xB31E2022), - selectionMenuItemSelectedTextColor: Color.fromARGB(255, 56, 91, 247), - selectionMenuItemSelectedIconColor: Color.fromARGB(255, 56, 91, 247), - selectionMenuUnselectedLabelColor: Color(0xFF333333), - selectionMenuDividerColor: Color(0xFF00BCF0), - selectionMenuLinkBorderColor: Color(0xFF00BCF0), - selectionMenuInvalidLinkColor: Color(0xFFE53935), - selectionMenuButtonColor: Color(0xFF00BCF0), - selectionMenuButtonTextColor: Color(0xFF333333), - selectionMenuButtonIconColor: Color(0xFF333333), - selectionMenuButtonBorderColor: Color(0xFF00BCF0), - selectionMenuTabIndicatorColor: Color(0xFF00BCF0), - ); - - static const MobileSelectionMenuStyle dark = MobileSelectionMenuStyle( - selectionMenuBackgroundColor: Color(0xFF424242), - selectionMenuItemTextColor: Color(0xFFFFFFFF), - selectionMenuItemIconColor: Color(0xFFFFFFFF), - selectionMenuItemSelectedColor: Color(0xFF666666), - selectionMenuItemRightIconColor: Color(0xB3FFFFFF), - selectionMenuItemSelectedTextColor: Color(0xFF131720), - selectionMenuItemSelectedIconColor: Color(0xFF131720), - selectionMenuUnselectedLabelColor: Color(0xFFBBC3CD), - selectionMenuDividerColor: Color(0xFF3A3F44), - selectionMenuLinkBorderColor: Color(0xFF3A3F44), - selectionMenuInvalidLinkColor: Color(0xFFE53935), - selectionMenuButtonColor: Color(0xFF00BCF0), - selectionMenuButtonTextColor: Color(0xFFFFFFFF), - selectionMenuButtonIconColor: Color(0xFFFFFFFF), - selectionMenuButtonBorderColor: Color(0xFF00BCF0), - selectionMenuTabIndicatorColor: Color(0xFF00BCF0), - ); -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart deleted file mode 100644 index d96dd224e1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/mobile_selection_menu_widget.dart +++ /dev/null @@ -1,392 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'mobile_selection_menu_item.dart'; -import 'mobile_selection_menu_item_widget.dart'; -import 'slash_keyboard_service_interceptor.dart'; - -class MobileSelectionMenuWidget extends StatefulWidget { - const MobileSelectionMenuWidget({ - super.key, - required this.items, - required this.itemCountFilter, - required this.maxItemInRow, - required this.menuService, - required this.editorState, - required this.onExit, - required this.selectionMenuStyle, - required this.deleteSlashByDefault, - required this.singleColumn, - required this.startOffset, - required this.showAtTop, - this.nameBuilder, - }); - - final List items; - final int itemCountFilter; - final int maxItemInRow; - - final SelectionMenuService menuService; - final EditorState editorState; - - final VoidCallback onExit; - - final MobileSelectionMenuStyle selectionMenuStyle; - - final bool deleteSlashByDefault; - final bool singleColumn; - final bool showAtTop; - final int startOffset; - - final SelectionMenuItemNameBuilder? nameBuilder; - - @override - State createState() => - _MobileSelectionMenuWidgetState(); -} - -class _MobileSelectionMenuWidgetState extends State { - final _focusNode = FocusNode(debugLabel: 'popup_list_widget'); - - List _showingItems = []; - - int _searchCounter = 0; - - EditorState get editorState => widget.editorState; - - SelectionMenuService get menuService => widget.menuService; - - String _keyword = ''; - - String get keyword => _keyword; - - int selectedIndex = 0; - - late AppFlowyKeyboardServiceInterceptor keyboardInterceptor; - - List get filterItems { - final List items = []; - for (final item in widget.items) { - if (item is MobileSelectionMenuItem) { - for (final childItem in item.children) { - items.add(childItem); - } - } else { - items.add(item); - } - } - return items; - } - - set keyword(String newKeyword) { - _keyword = newKeyword; - - // Search items according to the keyword, and calculate the length of - // the longest keyword, which is used to dismiss the selection_service. - var maxKeywordLength = 0; - - final items = newKeyword.isEmpty - ? widget.items - : filterItems - .where( - (item) => item.allKeywords.any((keyword) { - final value = keyword.contains(newKeyword.toLowerCase()); - if (value) { - maxKeywordLength = max(maxKeywordLength, keyword.length); - } - return value; - }), - ) - .toList(growable: false); - - AppFlowyEditorLog.ui.debug('$items'); - - if (keyword.length >= maxKeywordLength + 2 && - !(widget.deleteSlashByDefault && _searchCounter < 2)) { - return widget.onExit(); - } - - _showingItems = items; - refreshSelectedIndex(); - - if (_showingItems.isEmpty) { - _searchCounter++; - } else { - _searchCounter = 0; - } - } - - @override - void initState() { - super.initState(); - _showingItems = buildInitialItems(); - - keepEditorFocusNotifier.increase(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); - - keyboardInterceptor = SlashKeyboardServiceInterceptor( - onDelete: () async { - if (!mounted) return false; - final hasItemsChanged = !isInitialItems(); - if (keyword.isEmpty && hasItemsChanged) { - _showingItems = buildInitialItems(); - refreshSelectedIndex(); - return true; - } - return false; - }, - onEnter: () { - if (!mounted) return; - if (_showingItems.isEmpty) return; - final item = _showingItems[selectedIndex]; - if (item is MobileSelectionMenuItem) { - selectedIndex = 0; - item.onSelected?.call(); - } else { - item.handler( - editorState, - menuService, - context, - ); - } - }, - ); - editorState.service.keyboardService - ?.registerInterceptor(keyboardInterceptor); - editorState.selectionNotifier.addListener(onSelectionChanged); - } - - @override - void dispose() { - editorState.service.keyboardService - ?.unregisterInterceptor(keyboardInterceptor); - editorState.selectionNotifier.removeListener(onSelectionChanged); - _focusNode.dispose(); - keepEditorFocusNotifier.decrease(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 192, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.showAtTop) Spacer(), - Focus( - focusNode: _focusNode, - child: DecoratedBox( - decoration: BoxDecoration( - color: widget.selectionMenuStyle.selectionMenuBackgroundColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(6.0), - ), - child: _showingItems.isEmpty - ? _buildNoResultsWidget(context) - : _buildResultsWidget( - context, - _showingItems, - widget.itemCountFilter, - ), - ), - ), - if (!widget.showAtTop) Spacer(), - ], - ), - ); - } - - void onSelectionChanged() { - final selection = editorState.selection; - if (selection == null) { - widget.onExit(); - return; - } - if (!selection.isCollapsed) { - widget.onExit(); - return; - } - final startOffset = widget.startOffset; - final endOffset = selection.end.offset; - if (endOffset < startOffset) { - widget.onExit(); - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - final text = node?.delta?.toPlainText() ?? ''; - final search = text.substring(startOffset, endOffset); - keyword = search; - } - - Widget _buildResultsWidget( - BuildContext buildContext, - List items, - int itemCountFilter, - ) { - if (widget.singleColumn) { - final List itemWidgets = []; - for (var i = 0; i < items.length; i++) { - final item = items[i]; - itemWidgets.add( - GestureDetector( - onTapDown: (e) { - setState(() { - selectedIndex = i; - }); - }, - child: MobileSelectionMenuItemWidget( - item: item, - isSelected: i == selectedIndex, - editorState: editorState, - menuService: menuService, - selectionMenuStyle: widget.selectionMenuStyle, - onTap: () { - if (item is MobileSelectionMenuItem) refreshSelectedIndex(); - }, - ), - ), - ); - } - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 192, - minWidth: 240, - maxWidth: 240, - ), - child: ListView( - shrinkWrap: true, - padding: EdgeInsets.zero, - children: itemWidgets, - ), - ); - } else { - final List columns = []; - List itemWidgets = []; - // apply item count filter - if (itemCountFilter > 0) { - items = items.take(itemCountFilter).toList(); - } - - for (var i = 0; i < items.length; i++) { - final item = items[i]; - if (i != 0 && i % (widget.maxItemInRow) == 0) { - columns.add( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: itemWidgets, - ), - ); - itemWidgets = []; - } - itemWidgets.add( - MobileSelectionMenuItemWidget( - item: item, - isSelected: false, - editorState: editorState, - menuService: menuService, - selectionMenuStyle: widget.selectionMenuStyle, - onTap: () { - if (item is MobileSelectionMenuItem) refreshSelectedIndex(); - }, - ), - ); - } - if (itemWidgets.isNotEmpty) { - columns.add( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: itemWidgets, - ), - ); - itemWidgets = []; - } - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: columns, - ); - } - } - - void refreshSelectedIndex() { - if (!mounted) return; - setState(() { - selectedIndex = 0; - }); - } - - Widget _buildNoResultsWidget(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 240, - height: 48, - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Material( - color: Colors.transparent, - child: Center( - child: Text( - LocaleKeys.inlineActions_noResults.tr(), - style: TextStyle(fontSize: 18.0, color: Color(0x801F2225)), - textAlign: TextAlign.center, - ), - ), - ), - ), - ), - ); - } - - List buildInitialItems() { - final List items = []; - for (final item in widget.items) { - if (item is MobileSelectionMenuItem) { - item.onSelected = () { - if (mounted) { - setState(() { - _showingItems = item.children - .map((e) => e..onSelected = widget.onExit) - .toList(); - }); - } - }; - } - items.add(item); - } - return items; - } - - bool isInitialItems() { - if (_showingItems.length != widget.items.length) return false; - int i = 0; - for (final item in _showingItems) { - final widgetItem = widget.items[i]; - if (widgetItem.name != item.name) return false; - i++; - } - return true; - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/slash_keyboard_service_interceptor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/slash_keyboard_service_interceptor.dart deleted file mode 100644 index b7d0fd6e83..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/selection_menu/slash_keyboard_service_interceptor.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -class SlashKeyboardServiceInterceptor extends EditorKeyboardInterceptor { - SlashKeyboardServiceInterceptor({ - required this.onDelete, - required this.onEnter, - }); - - final AsyncValueGetter onDelete; - final VoidCallback onEnter; - - @override - Future interceptDelete( - TextEditingDeltaDeletion deletion, - EditorState editorState, - ) async { - final intercept = await onDelete.call(); - if (intercept) { - return true; - } else { - return super.interceptDelete(deletion, editorState); - } - } - - @override - Future interceptInsert( - TextEditingDeltaInsertion insertion, - EditorState editorState, - List characterShortcutEvents, - ) async { - final text = insertion.textInserted; - if (text.contains('\n')) { - onEnter.call(); - return true; - } - return super - .interceptInsert(insertion, editorState, characterShortcutEvents); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index 2d5a3176cd..337ce2549d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget { trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.com/privacy'), + onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'), ), MobileSettingItem( name: LocaleKeys.settings_mobile_termsAndConditions.tr(), trailing: const Icon( Icons.chevron_right, ), - onTap: () => afLaunchUrlString('https://appflowy.com/terms'), + onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'), ), if (kDebugMode) MobileSettingItem( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart deleted file mode 100644 index b43ada6e42..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/ai/ai_settings_group.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_group_widget.dart'; -import 'package:appflowy/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class AiSettingsGroup extends StatelessWidget { - const AiSettingsGroup({ - super.key, - required this.userProfile, - required this.workspaceId, - }); - - final UserProfilePB userProfile; - final String workspaceId; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return BlocProvider( - create: (context) => SettingsAIBloc( - userProfile, - workspaceId, - )..add(const SettingsAIEvent.started()), - child: BlocBuilder( - builder: (context, state) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_aiPage_title.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_aiPage_keys_llmModelType.tr(), - trailing: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FlowyText( - state.availableModels?.selectedModel.name ?? "", - color: theme.colorScheme.onSurface, - overflow: TextOverflow.ellipsis, - ), - ), - const Icon(Icons.chevron_right), - ], - ), - ), - onTap: () => _onLLMModelTypeTap(context, state), - ), - // enable AI search if needed - // MobileSettingItem( - // name: LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(), - // trailing: const Icon( - // Icons.chevron_right, - // ), - // onTap: () => context.push(AppFlowyCloudPage.routeName), - // ), - ], - ); - }, - ), - ); - } - - void _onLLMModelTypeTap(BuildContext context, SettingsAIState state) { - final availableModels = state.availableModels; - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDivider: false, - title: LocaleKeys.settings_aiPage_keys_llmModelType.tr(), - builder: (_) { - return Column( - children: (availableModels?.models ?? []) - .asMap() - .entries - .map( - (entry) => FlowyOptionTile.checkbox( - text: entry.value.name, - showTopBorder: entry.key == 0, - isSelected: - availableModels?.selectedModel.name == entry.value.name, - onTap: () { - context - .read() - .add(SettingsAIEvent.selectModel(entry.value)); - context.pop(); - }, - ), - ) - .toList(), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart index 47e356e6c1..abb31817e5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/appearance_setting_group.dart @@ -19,7 +19,7 @@ class AppearanceSettingGroup extends StatelessWidget { settingItemList: const [ ThemeSetting(), FontSetting(), - DisplaySizeSetting(), + TextScaleSetting(), RTLSetting(), ], ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart index 5b8035f004..e61281c5c4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -18,15 +17,15 @@ class RTLSetting extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final textDirection = - context.watch().state.textDirection; + final layoutDirection = + context.watch().state.layoutDirection; return MobileSettingItem( name: LocaleKeys.settings_appearance_textDirection_label.tr(), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ FlowyText( - _textDirectionLabelText(textDirection), + _textDirectionLabelText(layoutDirection), color: theme.colorScheme.onSurface, ), const Icon(Icons.chevron_right), @@ -40,33 +39,30 @@ class RTLSetting extends StatelessWidget { showDivider: false, title: LocaleKeys.settings_appearance_textDirection_label.tr(), builder: (context) { + final layoutDirection = + context.watch().state.layoutDirection; return Column( children: [ FlowyOptionTile.checkbox( text: LocaleKeys.settings_appearance_textDirection_ltr.tr(), - isSelected: textDirection == AppFlowyTextDirection.ltr, - onTap: () => applyTextDirectionAndPop( - context, - AppFlowyTextDirection.ltr, - ), + isSelected: layoutDirection == LayoutDirection.ltrLayout, + onTap: () { + context + .read() + .setLayoutDirection(LayoutDirection.ltrLayout); + Navigator.pop(context); + }, ), FlowyOptionTile.checkbox( showTopBorder: false, text: LocaleKeys.settings_appearance_textDirection_rtl.tr(), - isSelected: textDirection == AppFlowyTextDirection.rtl, - onTap: () => applyTextDirectionAndPop( - context, - AppFlowyTextDirection.rtl, - ), - ), - FlowyOptionTile.checkbox( - showTopBorder: false, - text: LocaleKeys.settings_appearance_textDirection_auto.tr(), - isSelected: textDirection == AppFlowyTextDirection.auto, - onTap: () => applyTextDirectionAndPop( - context, - AppFlowyTextDirection.auto, - ), + isSelected: layoutDirection == LayoutDirection.rtlLayout, + onTap: () { + context + .read() + .setLayoutDirection(LayoutDirection.rtlLayout); + Navigator.pop(context); + }, ), ], ); @@ -76,25 +72,13 @@ class RTLSetting extends StatelessWidget { ); } - String _textDirectionLabelText(AppFlowyTextDirection textDirection) { + String _textDirectionLabelText(LayoutDirection? textDirection) { switch (textDirection) { - case AppFlowyTextDirection.auto: - return LocaleKeys.settings_appearance_textDirection_auto.tr(); - case AppFlowyTextDirection.rtl: + case LayoutDirection.rtlLayout: return LocaleKeys.settings_appearance_textDirection_rtl.tr(); - case AppFlowyTextDirection.ltr: + case LayoutDirection.ltrLayout: + default: return LocaleKeys.settings_appearance_textDirection_ltr.tr(); } } - - void applyTextDirectionAndPop( - BuildContext context, - AppFlowyTextDirection textDirection, - ) { - context.read().setTextDirection(textDirection); - context - .read() - .syncDefaultTextDirection(textDirection.name); - Navigator.pop(context); - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart index 7c89185e79..3bdb836a71 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart @@ -1,55 +1,39 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:scaled_app/scaled_app.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../setting.dart'; const int _divisions = 4; -const double _minMobileScaleFactor = 0.8; -const double _maxMobileScaleFactor = 1.2; -class DisplaySizeSetting extends StatefulWidget { - const DisplaySizeSetting({ +class TextScaleSetting extends StatelessWidget { + const TextScaleSetting({ super.key, }); - @override - State createState() => _DisplaySizeSettingState(); -} - -class _DisplaySizeSettingState extends State { - double scaleFactor = 1.0; - final windowSizeManager = WindowSizeManager(); - - @override - void initState() { - super.initState(); - windowSizeManager.getScaleFactor().then((v) { - if (v != scaleFactor && mounted) { - setState(() { - scaleFactor = v; - }); - } - }); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); + final textScaleFactor = + context.watch().state.textScaleFactor; return MobileSettingItem( - name: LocaleKeys.settings_appearance_displaySize.tr(), + name: LocaleKeys.settings_appearance_fontScaleFactor.tr(), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ FlowyText( - scaleFactor.toStringAsFixed(2), + // map the text scale factor to the 0-1 + // 0.8 - 0.0 + // 0.9 - 0.5 + // 1.0 - 1.0 + ((_divisions + 1) * textScaleFactor - _divisions) + .toStringAsFixed(2), color: theme.colorScheme.onSurface, ), const Icon(Icons.chevron_right), @@ -61,15 +45,17 @@ class _DisplaySizeSettingState extends State { showHeader: true, showDragHandle: true, showDivider: false, - title: LocaleKeys.settings_appearance_displaySize.tr(), + title: LocaleKeys.settings_appearance_fontScaleFactor.tr(), builder: (context) { return FontSizeStepper( - value: scaleFactor, - minimumValue: _minMobileScaleFactor, - maximumValue: _maxMobileScaleFactor, + value: textScaleFactor, + minimumValue: 0.8, + maximumValue: 1.0, divisions: _divisions, - onChanged: (newScaleFactor) async { - await _setScale(newScaleFactor); + onChanged: (newTextScaleFactor) { + context + .read() + .setTextScaleFactor(newTextScaleFactor); }, ); }, @@ -77,22 +63,4 @@ class _DisplaySizeSettingState extends State { }, ); } - - Future _setScale(double value) async { - if (FlowyRunner.currentMode == IntegrationMode.integrationTest) { - // The integration test will fail if we check the scale factor in the test. - // #0 ScaledWidgetsFlutterBinding.Eval () - // #1 ScaledWidgetsFlutterBinding.instance (package:scaled_app/scaled_app.dart:66:62) - // ignore: invalid_use_of_visible_for_testing_member - appflowyScaleFactor = value; - } else { - ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value; - } - if (mounted) { - setState(() { - scaleFactor = value; - }); - } - await windowSizeManager.setScaleFactor(value); - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart index 02d620e559..3ce8e57b36 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/cloud/appflowy_cloud_page.dart @@ -1,7 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_cloud.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -17,11 +16,13 @@ class AppFlowyCloudPage extends StatelessWidget { appBar: FlowyAppBar( titleText: LocaleKeys.settings_menu_cloudSettings.tr(), ), - body: SettingCloud( - restartAppFlowy: () async { - await getIt().signOut(); - await runAppFlowy(); - }, + body: Padding( + padding: const EdgeInsets.all(20.0), + child: SettingCloud( + restartAppFlowy: () async { + await runAppFlowy(); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart index 1076b9dba6..050bf4b594 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart @@ -29,7 +29,6 @@ class FontSetting extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ FlowyText( - lineHeight: 1.0, name, color: theme.colorScheme.onSurface, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart index 6473485514..6f4e65f2b4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart @@ -38,7 +38,6 @@ class _LanguageSettingGroupState extends State { mainAxisSize: MainAxisSize.min, children: [ FlowyText( - lineHeight: 1.0, languageFromLocale(locale), color: theme.colorScheme.onSurface, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart index 28ebdb750e..cfdf3defb0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/personal_info_setting_group.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -5,10 +7,10 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../widgets/widgets.dart'; + import 'personal_info.dart'; class PersonalInfoSettingGroup extends StatelessWidget { @@ -30,7 +32,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { selector: (state) => state.userProfile.name, builder: (context, userName) { return MobileSettingGroup( - groupTitle: LocaleKeys.settings_accountPage_title.tr(), + groupTitle: LocaleKeys.settings_mobile_personalInfo.tr(), settingItemList: [ MobileSettingItem( name: userName, @@ -58,7 +60,7 @@ class PersonalInfoSettingGroup extends StatelessWidget { userName: userName, onSubmitted: (value) => context .read() - .add(SettingsUserEvent.updateUserName(name: value)), + .add(SettingsUserEvent.updateUserName(value)), ); }, ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart index dd19c2489d..ebc58290b9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart @@ -6,20 +6,13 @@ import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -enum SelfHostUrlBottomSheetType { - shareDomain, - cloudURL, -} - class SelfHostUrlBottomSheet extends StatefulWidget { const SelfHostUrlBottomSheet({ super.key, required this.url, - required this.type, }); final String url; - final SelfHostUrlBottomSheetType type; @override State createState() => _SelfHostUrlBottomSheetState(); @@ -53,10 +46,8 @@ class _SelfHostUrlBottomSheetState extends State { controller: _textFieldController, keyboardType: TextInputType.text, validator: (value) { - if (value == null || - value.isEmpty || - validateUrl(value).isFailure) { - return LocaleKeys.settings_menu_invalidCloudURLScheme.tr(); + if (value == null || value.isEmpty) { + return LocaleKeys.settings_mobile_usernameEmptyError.tr(); } return null; }, @@ -83,12 +74,7 @@ class _SelfHostUrlBottomSheetState extends State { if (value.isNotEmpty) { validateUrl(value).fold( (url) async { - switch (widget.type) { - case SelfHostUrlBottomSheetType.shareDomain: - await useBaseWebDomain(url); - case SelfHostUrlBottomSheetType.cloudURL: - await useSelfHostedAppFlowyCloudWithURL(url); - } + await useSelfHostedAppFlowyCloudWithURL(url); await runAppFlowy(); }, (err) => Log.error(err), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart index da2e0c773e..095214d6ef 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart @@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'setting.dart'; @@ -18,106 +17,44 @@ class SelfHostSettingGroup extends StatefulWidget { } class _SelfHostSettingGroupState extends State { - final future = Future.wait([ - getAppFlowyCloudUrl(), - getAppFlowyShareDomain(), - ]); + final future = getAppFlowyCloudUrl(); @override Widget build(BuildContext context) { return FutureBuilder( future: future, builder: (context, snapshot) { - final data = snapshot.data; - if (!snapshot.hasData || data == null || data.length != 2) { + if (!snapshot.hasData) { return const SizedBox.shrink(); } - final url = data[0]; - final shareDomain = data[1]; + final url = snapshot.data ?? ''; return MobileSettingGroup( - groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(), + groupTitle: LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(), settingItemList: [ - _buildSelfHostField(url), - _buildShareDomainField(shareDomain), + MobileSettingItem( + name: url, + onTap: () { + showMobileBottomSheet( + context, + showHeader: true, + title: LocaleKeys.editor_urlHint.tr(), + showCloseButton: true, + showDivider: false, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + builder: (_) { + return SelfHostUrlBottomSheet( + url: url, + ); + }, + ); + }, + ), ], ); }, ); } - - Widget _buildSelfHostField(String url) { - return MobileSettingItem( - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.settings_menu_cloudURL.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ), - subtitle: FlowyText( - url, - ), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.editor_urlHint.tr(), - showCloseButton: true, - showDivider: false, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - builder: (_) { - return SelfHostUrlBottomSheet( - url: url, - type: SelfHostUrlBottomSheetType.cloudURL, - ); - }, - ); - }, - ); - } - - Widget _buildShareDomainField(String shareDomain) { - return MobileSettingItem( - title: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.settings_menu_webURL.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ), - subtitle: FlowyText( - shareDomain, - ), - trailing: const Icon( - Icons.chevron_right, - ), - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - title: LocaleKeys.editor_urlHint.tr(), - showCloseButton: true, - showDivider: false, - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - builder: (_) { - return SelfHostUrlBottomSheet( - url: shareDomain, - type: SelfHostUrlBottomSheetType.shareDomain, - ); - }, - ); - }, - ); - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart index e5e4efef77..5222a05b8f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/support_setting_group.dart @@ -7,8 +7,7 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/util/share_log_files.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -75,13 +74,10 @@ class SupportSettingGroup extends StatelessWidget { actionButtonTitle: LocaleKeys.button_yes.tr(), onActionButtonPressed: () async { await getIt().clearAllCache(); - // check the workspace and space health - await WorkspaceDataManager.checkViewHealth( - dryRun: false, - ); if (context.mounted) { - showToastNotification( - message: LocaleKeys.settings_files_clearCacheSuccess.tr(), + showSnackBarMessage( + context, + LocaleKeys.settings_files_clearCacheSuccess.tr(), ); } }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart index 5ca5525099..8f8fd99ecb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/user_session_setting_group.dart @@ -1,14 +1,10 @@ -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'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/account_deletion.dart'; import 'package:appflowy_backend/log.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'; @@ -17,209 +13,48 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class UserSessionSettingGroup extends StatelessWidget { const UserSessionSettingGroup({ super.key, - required this.userProfile, required this.showThirdPartyLogin, }); - final UserProfilePB userProfile; final bool showThirdPartyLogin; @override Widget build(BuildContext context) { return Column( children: [ - // third party sign in buttons - if (showThirdPartyLogin) _buildThirdPartySignInButtons(context), - const VSpace(8.0), - - // logout button - MobileLogoutButton( - text: LocaleKeys.settings_menu_logout.tr(), - onPressed: () async => _showLogoutDialog(), - ), - - // delete account button - // only show the delete account button in cloud mode - if (userProfile.workspaceAuthType == AuthTypePB.Server) ...[ - const VSpace(16.0), - MobileLogoutButton( - text: LocaleKeys.button_deleteAccount.tr(), - textColor: Theme.of(context).colorScheme.error, - onPressed: () => _showDeleteAccountDialog(context), - ), - ], - ], - ); - } - - Widget _buildThirdPartySignInButtons(BuildContext context) { - return BlocProvider( - create: (context) => getIt(), - child: BlocConsumer( - listener: (context, state) { - state.successOrFail?.fold( - (result) => runAppFlowy(), - (e) => Log.error(e), - ); - }, - builder: (context, state) { - return Column( - children: [ - const ContinueWithEmailAndPassword(), - const VSpace(12.0), - const ThirdPartySignInButtons( - expanded: true, - ), - const VSpace(16.0), - ], - ); - }, - ), - ); - } - - Future _showDeleteAccountDialog(BuildContext context) async { - return showMobileBottomSheet( - context, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (_) => const _DeleteAccountBottomSheet(), - ); - } - - Future _showLogoutDialog() async { - return showFlowyCupertinoConfirmDialog( - title: LocaleKeys.settings_menu_logoutPrompt.tr(), - leftButton: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), - ), - rightButton: FlowyText( - LocaleKeys.button_logout.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, - color: const Color(0xFFFE0220), - ), - onRightButtonPressed: (context) async { - Navigator.of(context).pop(); - await getIt().signOut(); - await runAppFlowy(); - }, - ); - } -} - -class _DeleteAccountBottomSheet extends StatefulWidget { - const _DeleteAccountBottomSheet(); - - @override - State<_DeleteAccountBottomSheet> createState() => - _DeleteAccountBottomSheetState(); -} - -class _DeleteAccountBottomSheetState extends State<_DeleteAccountBottomSheet> { - final controller = TextEditingController(); - final isChecked = ValueNotifier(false); - - @override - void dispose() { - controller.dispose(); - isChecked.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(18.0), - const FlowySvg( - FlowySvgs.icon_warning_xl, - blendMode: null, - ), - const VSpace(12.0), - FlowyText( - LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(), - fontSize: 20.0, - fontWeight: FontWeight.w500, - ), - const VSpace(12.0), - FlowyText( - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(), - fontSize: 14.0, - fontWeight: FontWeight.w400, - maxLines: 10, - ), - const VSpace(18.0), - SizedBox( - height: 36.0, - child: FlowyTextField( - controller: controller, - textStyle: const TextStyle(fontSize: 14.0), - hintStyle: const TextStyle(fontSize: 14.0), - hintText: LocaleKeys - .newSettings_myAccount_deleteAccount_confirmHint3 - .tr(), + if (showThirdPartyLogin) ...[ + BlocProvider( + create: (context) => getIt(), + child: BlocConsumer( + listener: (context, state) { + state.successOrFail?.fold( + (result) => runAppFlowy(), + (e) => Log.error(e), + ); + }, + builder: (context, state) { + return const ThirdPartySignInButtons(); + }, ), ), - const VSpace(18.0), - _buildCheckbox(), - const VSpace(18.0), - MobileLogoutButton( - text: LocaleKeys.button_deleteAccount.tr(), - textColor: Theme.of(context).colorScheme.error, - onPressed: () => deleteMyAccount( + const VSpace(8), + ], + MobileSignInOrLogoutButton( + labelText: LocaleKeys.settings_menu_logout.tr(), + onPressed: () async { + await showFlowyMobileConfirmDialog( context, - controller.text.trim(), - isChecked.value, - ), - ), - const VSpace(12.0), - MobileLogoutButton( - text: LocaleKeys.button_cancel.tr(), - onPressed: () => Navigator.of(context).pop(), - ), - const VSpace(36.0), - ], - ), - ); - } - - Widget _buildCheckbox() { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => isChecked.value = !isChecked.value, - child: ValueListenableBuilder( - valueListenable: isChecked, - builder: (context, isChecked, _) { - return Padding( - padding: const EdgeInsets.all(1.0), - child: FlowySvg( - isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - size: const Size.square(16.0), - blendMode: isChecked ? null : BlendMode.srcIn, - ), - ); - }, - ), - ), - const HSpace(6.0), - Expanded( - child: FlowyText.regular( - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2.tr(), - fontSize: 14.0, - figmaLineHeight: 18.0, - maxLines: 3, - ), + content: FlowyText( + LocaleKeys.settings_menu_logoutPrompt.tr(), + ), + actionButtonTitle: LocaleKeys.button_yes.tr(), + actionButtonColor: Theme.of(context).colorScheme.error, + onActionButtonPressed: () async { + await getIt().signOut(); + await runAppFlowy(); + }, + ); + }, ), ], ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart index 82c86065ae..6dc45c1c40 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart @@ -4,29 +4,41 @@ import 'package:flutter/material.dart'; class MobileSettingItem extends StatelessWidget { const MobileSettingItem({ super.key, - this.name, + required this.name, this.padding = const EdgeInsets.only(bottom: 4), this.trailing, this.leadingIcon, - this.title, this.subtitle, this.onTap, }); - final String? name; + final String name; final EdgeInsets padding; final Widget? trailing; final Widget? leadingIcon; final Widget? subtitle; final VoidCallback? onTap; - final Widget? title; @override Widget build(BuildContext context) { return Padding( padding: padding, child: ListTile( - title: title ?? _buildDefaultTitle(name), + title: Row( + children: [ + if (leadingIcon != null) ...[ + leadingIcon!, + const HSpace(8), + ], + Expanded( + child: FlowyText.medium( + name, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), subtitle: subtitle, trailing: trailing, onTap: onTap, @@ -35,22 +47,4 @@ class MobileSettingItem extends StatelessWidget { ), ); } - - Widget _buildDefaultTitle(String? name) { - return Row( - children: [ - if (leadingIcon != null) ...[ - leadingIcon!, - const HSpace(8), - ], - Expanded( - child: FlowyText.medium( - name ?? '', - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ); - } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart deleted file mode 100644 index 18bce0588b..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/invite_members_screen.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart'; -import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; - -import 'member_list.dart'; - -ValueNotifier mobileLeaveWorkspaceNotifier = ValueNotifier(0); - -class InviteMembersScreen extends StatelessWidget { - const InviteMembersScreen({ - super.key, - }); - - static const routeName = '/invite_member'; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar( - titleText: LocaleKeys.settings_appearance_members_label.tr(), - ), - body: const _InviteMemberPage(), - resizeToAvoidBottomInset: false, - ); - } -} - -class _InviteMemberPage extends StatefulWidget { - const _InviteMemberPage(); - - @override - State<_InviteMemberPage> createState() => _InviteMemberPageState(); -} - -class _InviteMemberPageState extends State<_InviteMemberPage> { - final emailController = TextEditingController(); - late final Future userProfile; - bool exceededLimit = false; - - @override - void initState() { - super.initState(); - userProfile = UserBackendService.getCurrentUserProfile().fold( - (s) => s, - (f) => null, - ); - } - - @override - void dispose() { - emailController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: userProfile, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const SizedBox.shrink(); - } - if (snapshot.hasError || snapshot.data == null) { - return _buildError(context); - } - - final userProfile = snapshot.data!; - - return BlocProvider( - create: (context) => WorkspaceMemberBloc(userProfile: userProfile) - ..add(const WorkspaceMemberEvent.initial()), - child: BlocConsumer( - listener: _onListener, - builder: (context, state) { - return Column( - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.myRole.isOwner) ...[ - Padding( - padding: const EdgeInsets.all(16.0), - child: _buildInviteMemberArea(context), - ), - const VSpace(16), - ], - if (state.members.isNotEmpty) ...[ - const VSpace(8), - MobileMemberList( - members: state.members, - userProfile: userProfile, - myRole: state.myRole, - ), - ], - ], - ), - ), - if (state.myRole.isMember) const _LeaveWorkspaceButton(), - const VSpace(48), - ], - ); - }, - ), - ); - }, - ); - } - - Widget _buildInviteMemberArea(BuildContext context) { - return Column( - children: [ - TextFormField( - autofocus: true, - controller: emailController, - keyboardType: TextInputType.text, - decoration: InputDecoration( - hintText: LocaleKeys.settings_appearance_members_inviteHint.tr(), - ), - ), - const VSpace(16), - if (exceededLimit) ...[ - FlowyText.regular( - LocaleKeys.settings_appearance_members_inviteFailedMemberLimitMobile - .tr(), - fontSize: 14.0, - maxLines: 3, - color: Theme.of(context).colorScheme.error, - ), - const VSpace(16), - ], - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => _inviteMember(context), - child: Text( - LocaleKeys.settings_appearance_members_sendInvite.tr(), - ), - ), - ), - ], - ); - } - - Widget _buildError(BuildContext context) { - return Center( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 48.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.medium( - LocaleKeys.settings_appearance_members_workspaceMembersError.tr(), - fontSize: 18.0, - textAlign: TextAlign.center, - ), - const VSpace(8.0), - FlowyText.regular( - LocaleKeys - .settings_appearance_members_workspaceMembersErrorDescription - .tr(), - fontSize: 17.0, - maxLines: 10, - textAlign: TextAlign.center, - lineHeight: 1.3, - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ); - } - - void _onListener(BuildContext context, WorkspaceMemberState state) { - final actionResult = state.actionResult; - if (actionResult == null) { - return; - } - - final actionType = actionResult.actionType; - final result = actionResult.result; - - // get keyboard height - final keyboardHeight = MediaQuery.of(context).viewInsets.bottom; - - // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.addByEmail) { - result.fold( - (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), - bottomPadding: keyboardHeight, - ); - }, - (f) { - Log.error('add workspace member failed: $f'); - final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded - ? LocaleKeys - .settings_appearance_members_inviteFailedMemberLimitMobile - .tr() - : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); - setState(() { - exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; - }); - showToastNotification( - type: ToastificationType.error, - bottomPadding: keyboardHeight, - message: message, - ); - }, - ); - } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { - result.fold( - (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), - bottomPadding: keyboardHeight, - ); - }, - (f) { - Log.error('invite workspace member failed: $f'); - final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded - ? LocaleKeys - .settings_appearance_members_inviteFailedMemberLimitMobile - .tr() - : LocaleKeys.settings_appearance_members_failedToInviteMember - .tr(); - setState(() { - exceededLimit = f.code == ErrorCode.WorkspaceMemberLimitExceeded; - }); - showToastNotification( - type: ToastificationType.error, - message: message, - bottomPadding: keyboardHeight, - ); - }, - ); - } else if (actionType == WorkspaceMemberActionType.removeByEmail) { - result.fold( - (s) { - showToastNotification( - message: LocaleKeys - .settings_appearance_members_removeFromWorkspaceSuccess - .tr(), - bottomPadding: keyboardHeight, - ); - }, - (f) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys - .settings_appearance_members_removeFromWorkspaceFailed - .tr(), - bottomPadding: keyboardHeight, - ); - }, - ); - } - } - - void _inviteMember(BuildContext context) { - final email = emailController.text; - if (!isEmail(email)) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), - ); - return; - } - context - .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); - // clear the email field after inviting - emailController.clear(); - } -} - -class _LeaveWorkspaceButton extends StatelessWidget { - const _LeaveWorkspaceButton(); - - @override - Widget build(BuildContext context) { - return Container( - width: double.infinity, - margin: const EdgeInsets.symmetric(horizontal: 16), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - foregroundColor: Theme.of(context).colorScheme.error, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: BorderSide( - color: Theme.of(context).colorScheme.error, - width: 0.5, - ), - ), - ), - onPressed: () => _leaveWorkspace(context), - child: FlowyText( - LocaleKeys.workspace_leaveCurrentWorkspace.tr(), - fontSize: 14.0, - color: Theme.of(context).colorScheme.error, - fontWeight: FontWeight.w500, - ), - ), - ); - } - - void _leaveWorkspace(BuildContext context) { - showFlowyCupertinoConfirmDialog( - title: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), - leftButton: FlowyText( - LocaleKeys.button_cancel.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w500, - color: const Color(0xFF007AFF), - ), - rightButton: FlowyText( - LocaleKeys.button_confirm.tr(), - fontSize: 17.0, - figmaLineHeight: 24.0, - fontWeight: FontWeight.w400, - color: const Color(0xFFFE0220), - ), - onRightButtonPressed: (buttonContext) async { - // try to use popUntil with a specific route name but failed - // so use pop twice as a workaround - Navigator.of(buttonContext).pop(); - Navigator.of(context).pop(); - Navigator.of(context).pop(); - - mobileLeaveWorkspaceNotifier.value = - mobileLeaveWorkspaceNotifier.value + 1; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart deleted file mode 100644 index b2805d5857..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/member_list.dart +++ /dev/null @@ -1,191 +0,0 @@ -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/widgets.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.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/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class MobileMemberList extends StatelessWidget { - const MobileMemberList({ - super.key, - required this.members, - required this.myRole, - required this.userProfile, - }); - - final List members; - final AFRolePB myRole; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - return SlidableAutoCloseBehavior( - child: SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const FlowyDivider( - padding: EdgeInsets.symmetric(horizontal: 16.0), - ), - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: FlowyText.semibold( - LocaleKeys.settings_appearance_members_label.tr(), - fontSize: 16.0, - ), - ), - ...members.map( - (member) => _MemberItem( - member: member, - myRole: myRole, - userProfile: userProfile, - ), - ), - ], - ), - ); - } -} - -class _MemberItem extends StatelessWidget { - const _MemberItem({ - required this.member, - required this.myRole, - required this.userProfile, - }); - - final WorkspaceMemberPB member; - final AFRolePB myRole; - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final canDelete = myRole.canDelete && member.email != userProfile.email; - final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; - - Widget child; - - if (UniversalPlatform.isDesktop) { - child = Row( - children: [ - Expanded( - child: FlowyText.medium( - member.name, - color: textColor, - fontSize: 15.0, - ), - ), - Expanded( - child: FlowyText.medium( - member.role.description, - color: textColor, - fontSize: 15.0, - textAlign: TextAlign.end, - ), - ), - ], - ); - } else { - child = Row( - children: [ - Expanded( - child: FlowyText.medium( - member.name, - color: textColor, - fontSize: 15.0, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(36.0), - FlowyText.medium( - member.role.description, - color: textColor, - fontSize: 15.0, - textAlign: TextAlign.end, - ), - ], - ); - } - - child = Container( - height: 48, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: child, - ); - - if (canDelete) { - child = Slidable( - key: ValueKey(member.email), - endActionPane: ActionPane( - extentRatio: 1 / 6.0, - motion: const ScrollMotion(), - children: [ - CustomSlidableAction( - backgroundColor: const Color(0xE5515563), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - bottomLeft: Radius.circular(10), - ), - onPressed: (context) { - HapticFeedback.mediumImpact(); - _showDeleteMenu(context); - }, - padding: EdgeInsets.zero, - child: const FlowySvg( - FlowySvgs.three_dots_s, - size: Size.square(24), - color: Colors.white, - ), - ), - ], - ), - child: child, - ); - } - - return child; - } - - void _showDeleteMenu(BuildContext context) { - final workspaceMemberBloc = context.read(); - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useRootNavigator: true, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (context) { - return FlowyOptionTile.text( - text: LocaleKeys.settings_appearance_members_removeFromWorkspace.tr(), - height: 52.0, - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(18), - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, - showBottomBorder: false, - onTap: () { - workspaceMemberBloc.add( - WorkspaceMemberEvent.removeWorkspaceMemberByEmail( - member.email, - ), - ); - Navigator.of(context).pop(); - }, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart deleted file mode 100644 index 9c2161a4d1..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/workspace/workspace_setting_group.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -import '../widgets/widgets.dart'; -import 'invite_members_screen.dart'; - -class WorkspaceSettingGroup extends StatelessWidget { - const WorkspaceSettingGroup({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return MobileSettingGroup( - groupTitle: LocaleKeys.settings_appearance_members_label.tr(), - settingItemList: [ - MobileSettingItem( - name: LocaleKeys.settings_appearance_members_label.tr(), - trailing: const Icon(Icons.chevron_right), - onTap: () { - context.push(InviteMembersScreen.routeName); - }, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index 191deb1e9f..5be3d3e78c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -10,9 +10,7 @@ class MobileQuickActionButton extends StatelessWidget { required this.text, this.textColor, this.iconColor, - this.iconSize, this.enable = true, - this.rightIconBuilder, }); final VoidCallback onTap; @@ -20,39 +18,36 @@ class MobileQuickActionButton extends StatelessWidget { final String text; final Color? textColor; final Color? iconColor; - final Size? iconSize; final bool enable; - final WidgetBuilder? rightIconBuilder; @override Widget build(BuildContext context) { - final iconSize = this.iconSize ?? const Size.square(18); - return Opacity( - opacity: enable ? 1.0 : 0.5, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), child: InkWell( onTap: enable ? onTap : null, + borderRadius: BorderRadius.circular(12), overlayColor: enable ? null : const WidgetStatePropertyAll(Colors.transparent), splashColor: Colors.transparent, child: Container( height: 52, - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ FlowySvg( icon, - size: iconSize, - color: iconColor, + size: const Size.square(18), + color: enable ? iconColor : Theme.of(context).disabledColor, ), - HSpace(30 - iconSize.width), + const HSpace(12), Expanded( child: FlowyText.regular( text, fontSize: 16, - color: textColor, + color: enable ? textColor : Theme.of(context).disabledColor, ), ), - if (rightIconBuilder != null) rightIconBuilder!(context), ], ), ), @@ -60,12 +55,3 @@ class MobileQuickActionButton extends StatelessWidget { ); } } - -class MobileQuickActionDivider extends StatelessWidget { - const MobileQuickActionDivider({super.key}); - - @override - Widget build(BuildContext context) { - return const Divider(height: 0.5, thickness: 0.5); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index f38c724a22..4f76003e23 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -37,7 +37,6 @@ class FlowyOptionTile extends StatelessWidget { this.backgroundColor, this.fontFamily, this.height, - this.enable = true, }); factory FlowyOptionTile.text({ @@ -50,7 +49,6 @@ class FlowyOptionTile extends StatelessWidget { Widget? trailing, VoidCallback? onTap, double? height, - bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.text, @@ -63,7 +61,6 @@ class FlowyOptionTile extends StatelessWidget { leading: leftIcon, trailing: trailing, height: height, - enable: enable, ); } @@ -80,7 +77,6 @@ class FlowyOptionTile extends StatelessWidget { Widget? trailing, String? textFieldHintText, bool autofocus = false, - bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.textField, @@ -94,7 +90,6 @@ class FlowyOptionTile extends StatelessWidget { onTextChanged: onTextChanged, onTextSubmitted: onTextSubmitted, autofocus: autofocus, - enable: enable, ); } @@ -110,7 +105,6 @@ class FlowyOptionTile extends StatelessWidget { bool showBottomBorder = true, String? fontFamily, Color? backgroundColor, - bool enable = true, }) { return FlowyOptionTile._( key: key, @@ -125,7 +119,6 @@ class FlowyOptionTile extends StatelessWidget { showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, leading: leftIcon, - enable: enable, trailing: isSelected ? const FlowySvg( FlowySvgs.m_blue_check_s, @@ -143,7 +136,6 @@ class FlowyOptionTile extends StatelessWidget { bool showTopBorder = true, bool showBottomBorder = true, Widget? leftIcon, - bool enable = true, }) { return FlowyOptionTile._( type: FlowyOptionTileType.toggle, @@ -154,7 +146,6 @@ class FlowyOptionTile extends StatelessWidget { showBottomBorder: showBottomBorder, leading: leftIcon, trailing: _Toggle(value: isSelected, onChanged: onValueChanged), - enable: enable, ); } @@ -190,13 +181,11 @@ class FlowyOptionTile extends StatelessWidget { final double? height; - final bool enable; - @override Widget build(BuildContext context) { final leadingWidget = _buildLeading(); - Widget child = FlowyOptionDecorateBox( + final child = FlowyOptionDecorateBox( color: backgroundColor, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, @@ -220,21 +209,12 @@ class FlowyOptionTile extends StatelessWidget { if (type == FlowyOptionTileType.checkbox || type == FlowyOptionTileType.toggle || type == FlowyOptionTileType.text) { - child = GestureDetector( + return GestureDetector( onTap: onTap, child: child, ); } - if (!enable) { - child = Opacity( - opacity: 0.5, - child: IgnorePointer( - child: child, - ), - ); - } - return child; } @@ -319,7 +299,7 @@ class _Toggle extends StatelessWidget { fit: BoxFit.fill, child: CupertinoSwitch( value: value, - activeTrackColor: Theme.of(context).colorScheme.primary, + activeColor: Theme.of(context).colorScheme.primary, onChanged: onChanged, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/navigation_bar_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/navigation_bar_button.dart deleted file mode 100644 index 2058e03e16..0000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/navigation_bar_button.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class NavigationBarButton extends StatelessWidget { - const NavigationBarButton({ - super.key, - required this.text, - required this.icon, - required this.onTap, - this.enable = true, - }); - - final String text; - final FlowySvgData icon; - final VoidCallback onTap; - final bool enable; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: enable ? 1.0 : 0.3, - child: Container( - height: 40, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide(color: Color(0x3F1F2329)), - borderRadius: BorderRadius.circular(10), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - expandText: false, - iconPadding: 8, - leftIcon: FlowySvg(icon), - onTap: enable ? onTap : null, - text: FlowyText( - text, - fontSize: 15.0, - figmaLineHeight: 18.0, - fontWeight: FontWeight.w400, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart index 96c18f5d91..321632a36a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart @@ -91,7 +91,6 @@ Future showFlowyMobileConfirmDialog( Future showFlowyCupertinoConfirmDialog({ BuildContext? context, required String title, - Widget? content, required Widget leftButton, required Widget rightButton, void Function(BuildContext context)? onLeftButtonPressed, @@ -99,15 +98,13 @@ Future showFlowyCupertinoConfirmDialog({ }) { return showDialog( context: context ?? AppGlobals.context, - barrierColor: Colors.black.withValues(alpha: 0.25), builder: (context) => CupertinoAlertDialog( title: FlowyText.medium( title, - fontSize: 16, + fontSize: 18, maxLines: 10, - figmaLineHeight: 22.0, + lineHeight: 1.3, ), - content: content, actions: [ CupertinoDialogAction( onPressed: () { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart deleted file mode 100644 index 2cfc349bf8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/ai_model_switch_listener.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef OnUpdateSelectedModel = void Function(AIModelPB model); - -class AIModelSwitchListener { - AIModelSwitchListener({required this.objectId}) { - _parser = ChatNotificationParser(id: objectId, callback: _callback); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - final String objectId; - StreamSubscription? _subscription; - ChatNotificationParser? _parser; - - void start({ - OnUpdateSelectedModel? onUpdateSelectedModel, - }) { - this.onUpdateSelectedModel = onUpdateSelectedModel; - } - - OnUpdateSelectedModel? onUpdateSelectedModel; - - void _callback( - ChatNotification ty, - FlowyResult result, - ) { - result.map((r) { - switch (ty) { - case ChatNotification.DidUpdateSelectedModel: - onUpdateSelectedModel?.call(AIModelPB.fromBuffer(r)); - break; - default: - break; - } - }); - } - - Future stop() async { - await _subscription?.cancel(); - _subscription = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart index 47c1668a2c..a806a5c97d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_ai_message_bloc.dart @@ -1,212 +1,131 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; +import 'dart:async'; + +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'chat_message_service.dart'; - part 'chat_ai_message_bloc.freezed.dart'; class ChatAIMessageBloc extends Bloc { ChatAIMessageBloc({ dynamic message, - String? refSourceJsonString, required this.chatId, required this.questionId, - }) : super( - ChatAIMessageState.initial( - message, - parseMetadata(refSourceJsonString), - ), - ) { - _registerEventHandlers(); - _initializeStreamListener(); - _checkInitialStreamState(); - } + }) : super(ChatAIMessageState.initial(message)) { + if (state.stream != null) { + _subscription = state.stream!.listen((text) { + if (isClosed) { + return; + } - final String chatId; - final Int64? questionId; + if (text.startsWith("data:")) { + add(ChatAIMessageEvent.newText(text.substring(5))); + } else if (text.startsWith("error:")) { + add(ChatAIMessageEvent.receiveError(text.substring(5))); + } + }); - void _registerEventHandlers() { - on<_UpdateText>((event, emit) { - emit( - state.copyWith( - text: event.text, - messageState: const MessageState.ready(), - ), - ); - }); - - on<_ReceiveError>((event, emit) { - emit(state.copyWith(messageState: MessageState.onError(event.error))); - }); - - on<_Retry>((event, emit) async { - if (questionId == null) { - Log.error("Question id is not valid: $questionId"); - return; + if (state.stream!.error != null) { + Future.delayed(const Duration(milliseconds: 300), () { + if (!isClosed) { + add(ChatAIMessageEvent.receiveError(state.stream!.error!)); + } + }); } - emit(state.copyWith(messageState: const MessageState.loading())); - final payload = ChatMessageIdPB( - chatId: chatId, - messageId: questionId, - ); - final result = await AIEventGetAnswerForQuestion(payload).send(); - if (!isClosed) { - result.fold( - (answer) => add(ChatAIMessageEvent.retryResult(answer.content)), - (err) { - Log.error("Failed to get answer: $err"); - add(ChatAIMessageEvent.receiveError(err.toString())); + } + + on( + (event, emit) async { + await event.when( + initial: () async {}, + newText: (newText) { + emit(state.copyWith(text: state.text + newText, error: null)); + }, + receiveError: (error) { + emit(state.copyWith(error: error)); + }, + retry: () { + if (questionId is! Int64) { + Log.error("Question id is not Int64: $questionId"); + return; + } + emit( + state.copyWith( + retryState: const LoadingState.loading(), + error: null, + ), + ); + + final payload = ChatMessageIdPB( + chatId: chatId, + messageId: questionId, + ); + ChatEventGetAnswerForQuestion(payload).send().then((result) { + if (!isClosed) { + result.fold( + (answer) { + add(ChatAIMessageEvent.retryResult(answer.content)); + }, + (err) { + Log.error("Failed to get answer: $err"); + add(ChatAIMessageEvent.receiveError(err.toString())); + }, + ); + } + }); + }, + retryResult: (String text) { + emit( + state.copyWith( + text: text, + error: null, + retryState: const LoadingState.finish(), + ), + ); }, ); - } - }); - - on<_RetryResult>((event, emit) { - emit( - state.copyWith( - text: event.text, - messageState: const MessageState.ready(), - ), - ); - }); - - on<_OnAIResponseLimit>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onAIResponseLimit(), - ), - ); - }); - - on<_OnAIImageResponseLimit>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onAIImageResponseLimit(), - ), - ); - }); - - on<_OnAIMaxRquired>((event, emit) { - emit( - state.copyWith( - messageState: MessageState.onAIMaxRequired(event.message), - ), - ); - }); - - on<_OnLocalAIInitializing>((event, emit) { - emit( - state.copyWith( - messageState: const MessageState.onInitializingLocalAI(), - ), - ); - }); - - on<_ReceiveMetadata>((event, emit) { - Log.debug("AI Steps: ${event.metadata.progress?.step}"); - emit( - state.copyWith( - sources: event.metadata.sources, - progress: event.metadata.progress, - ), - ); - }); + }, + ); } - void _initializeStreamListener() { - if (state.stream != null) { - state.stream!.listen( - onData: (text) => _safeAdd(ChatAIMessageEvent.updateText(text)), - onError: (error) => - _safeAdd(ChatAIMessageEvent.receiveError(error.toString())), - onAIResponseLimit: () => - _safeAdd(const ChatAIMessageEvent.onAIResponseLimit()), - onAIImageResponseLimit: () => - _safeAdd(const ChatAIMessageEvent.onAIImageResponseLimit()), - onMetadata: (metadata) => - _safeAdd(ChatAIMessageEvent.receiveMetadata(metadata)), - onAIMaxRequired: (message) { - Log.info(message); - _safeAdd(ChatAIMessageEvent.onAIMaxRequired(message)); - }, - onLocalAIInitializing: () => - _safeAdd(const ChatAIMessageEvent.onLocalAIInitializing()), - ); - } + @override + Future close() { + _subscription?.cancel(); + return super.close(); } - void _checkInitialStreamState() { - if (state.stream != null) { - if (state.stream!.aiLimitReached) { - add(const ChatAIMessageEvent.onAIResponseLimit()); - } else if (state.stream!.error != null) { - add(ChatAIMessageEvent.receiveError(state.stream!.error!)); - } - } - } - - void _safeAdd(ChatAIMessageEvent event) { - if (!isClosed) { - add(event); - } - } + StreamSubscription? _subscription; + final String chatId; + final Int64? questionId; } @freezed class ChatAIMessageEvent with _$ChatAIMessageEvent { - const factory ChatAIMessageEvent.updateText(String text) = _UpdateText; + const factory ChatAIMessageEvent.initial() = Initial; + const factory ChatAIMessageEvent.newText(String text) = _NewText; const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError; const factory ChatAIMessageEvent.retry() = _Retry; const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult; - const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit; - const factory ChatAIMessageEvent.onAIImageResponseLimit() = - _OnAIImageResponseLimit; - const factory ChatAIMessageEvent.onAIMaxRequired(String message) = - _OnAIMaxRquired; - const factory ChatAIMessageEvent.onLocalAIInitializing() = - _OnLocalAIInitializing; - const factory ChatAIMessageEvent.receiveMetadata( - MetadataCollection metadata, - ) = _ReceiveMetadata; } @freezed class ChatAIMessageState with _$ChatAIMessageState { const factory ChatAIMessageState({ AnswerStream? stream, + String? error, required String text, - required MessageState messageState, - required List sources, - required AIChatProgress? progress, + required LoadingState retryState, }) = _ChatAIMessageState; - factory ChatAIMessageState.initial( - dynamic text, - MetadataCollection metadata, - ) { + factory ChatAIMessageState.initial(dynamic text) { return ChatAIMessageState( text: text is String ? text : "", stream: text is AnswerStream ? text : null, - messageState: const MessageState.ready(), - sources: metadata.sources, - progress: metadata.progress, + retryState: const LoadingState.finish(), ); } } - -@freezed -class MessageState with _$MessageState { - const factory MessageState.onError(String error) = _Error; - const factory MessageState.onAIResponseLimit() = _AIResponseLimit; - const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit; - const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired; - const factory MessageState.onInitializingLocalAI() = _LocalAIInitializing; - const factory MessageState.ready() = _Ready; - const factory MessageState.loading() = _Loading; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 602b46f97a..92bcbddfee 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -1,54 +1,46 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:ffi'; +import 'dart:isolate'; -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/util/int64_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:collection/collection.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:nanoid/nanoid.dart'; -import 'chat_entity.dart'; import 'chat_message_listener.dart'; -import 'chat_message_service.dart'; -import 'chat_message_stream.dart'; part 'chat_bloc.freezed.dart'; +const sendMessageErrorKey = "sendMessageError"; + class ChatBloc extends Bloc { ChatBloc({ - required this.chatId, - required this.userId, - }) : chatController = InMemoryChatController(), - listener = ChatMessageListener(chatId: chatId), - selectedSourcesNotifier = ValueNotifier([]), - super(ChatState.initial()) { + required ViewPB view, + required UserProfilePB userProfile, + }) : listener = ChatMessageListener(chatId: view.id), + chatId = view.id, + super( + ChatState.initial(view, userProfile), + ) { _startListening(); _dispatch(); - _loadMessages(); - _loadSetting(); } - final String chatId; - final String userId; final ChatMessageListener listener; - final ValueNotifier> selectedSourcesNotifier; - final ChatController chatController; + final String chatId; /// The last streaming message id - String answerStreamMessageId = ''; - String questionStreamMessageId = ''; - - ChatMessagePB? lastSentMessage; + String lastStreamMessageId = ''; /// Using a temporary map to associate the real message ID with the last streaming message ID. /// @@ -59,19 +51,12 @@ class ChatBloc extends Bloc { /// is 3 (AI response). final HashMap temporaryMessageIDMap = HashMap(); - bool isLoadingPreviousMessages = false; - bool hasMorePreviousMessages = true; - AnswerStream? answerStream; - bool isFetchingRelatedQuestions = false; - bool shouldFetchRelatedQuestions = false; - @override Future close() async { - await answerStream?.dispose(); + if (state.answerStream != null) { + await state.answerStream?.dispose(); + } await listener.stop(); - final request = ViewIdPB(value: chatId); - unawaited(FolderEventCloseView(request).send()); - selectedSourcesNotifier.dispose(); return super.close(); } @@ -79,196 +64,165 @@ class ChatBloc extends Bloc { on( (event, emit) async { await event.when( - // Loading messages - didLoadLatestMessages: (List messages) async { - for (final message in messages) { - await chatController.insert(message, index: 0); - } - - switch (state.loadingState) { - case LoadChatMessageStatus.loading - when chatController.messages.isEmpty: - emit( - state.copyWith( - loadingState: LoadChatMessageStatus.loadingRemote, - ), - ); - break; - case LoadChatMessageStatus.loading: - case LoadChatMessageStatus.loadingRemote: - emit( - state.copyWith(loadingState: LoadChatMessageStatus.ready), - ); - break; - default: - break; - } + initialLoad: () { + final payload = LoadNextChatMessagePB( + chatId: state.view.id, + limit: Int64(10), + ); + ChatEventLoadNextMessage(payload).send().then( + (result) { + result.fold((list) { + if (!isClosed) { + final messages = + list.messages.map(_createTextMessage).toList(); + add(ChatEvent.didLoadLatestMessages(messages)); + } + }, (err) { + Log.error("Failed to load messages: $err"); + }); + }, + ); }, - loadPreviousMessages: () { - if (isLoadingPreviousMessages) { - return; - } - - final oldestMessage = _getOldestMessage(); - + startLoadingPrevMessage: () async { + Int64? beforeMessageId; + final oldestMessage = _getOlderstMessage(); if (oldestMessage != null) { - final oldestMessageId = Int64.tryParseInt(oldestMessage.id); - if (oldestMessageId == null) { - Log.error("Failed to parse message_id: ${oldestMessage.id}"); - return; - } - isLoadingPreviousMessages = true; - _loadPreviousMessages(oldestMessageId); + beforeMessageId = Int64.parseInt(oldestMessage.id); } + _loadPrevMessage(beforeMessageId); + emit( + state.copyWith( + loadingPreviousStatus: const LoadingState.loading(), + ), + ); }, - didLoadPreviousMessages: (messages, hasMore) { + didLoadPreviousMessages: (List messages, bool hasMore) { Log.debug("did load previous messages: ${messages.length}"); + final onetimeMessages = _getOnetimeMessages(); + final allMessages = _perminentMessages(); + final uniqueMessages = {...allMessages, ...messages}.toList() + ..sort((a, b) => b.id.compareTo(a.id)); - for (final message in messages) { - chatController.insert(message, index: 0); - } - - isLoadingPreviousMessages = false; - hasMorePreviousMessages = hasMore; - }, - didFinishAnswerStream: () { - emit( - state.copyWith(promptResponseState: PromptResponseState.ready), - ); - }, - didReceiveRelatedQuestions: (List questions) { - if (questions.isEmpty) { - return; - } - - final metadata = { - onetimeShotType: OnetimeShotType.relatedQuestion, - 'questions': questions, - }; - - final createdAt = DateTime.now(); - - final message = TextMessage( - id: "related_question_$createdAt", - text: '', - metadata: metadata, - author: const User(id: systemUserId), - createdAt: createdAt, - ); - - chatController.insert(message); - }, - receiveMessage: (Message message) { - final oldMessage = chatController.messages - .firstWhereOrNull((m) => m.id == message.id); - if (oldMessage == null) { - chatController.insert(message); - } else { - chatController.update(oldMessage, message); - } - }, - sendMessage: ( - String message, - PredefinedFormat? format, - Map? metadata, - ) { - _clearErrorMessages(emit); - _clearRelatedQuestions(); - _startStreamingMessage(message, format, metadata); - lastSentMessage = null; - - isFetchingRelatedQuestions = false; - shouldFetchRelatedQuestions = - format == null || format.imageFormat.hasText; + uniqueMessages.insertAll(0, onetimeMessages); emit( state.copyWith( - promptResponseState: PromptResponseState.sendingQuestion, + messages: uniqueMessages, + loadingPreviousStatus: const LoadingState.finish(), + hasMorePrevMessage: hasMore, ), ); }, - finishSending: () { + didLoadLatestMessages: (List messages) { + final onetimeMessages = _getOnetimeMessages(); + final allMessages = _perminentMessages(); + final uniqueMessages = {...allMessages, ...messages}.toList() + ..sort((a, b) => b.id.compareTo(a.id)); + uniqueMessages.insertAll(0, onetimeMessages); + emit( state.copyWith( - promptResponseState: PromptResponseState.streamingAnswer, + messages: uniqueMessages, + initialLoadingStatus: const LoadingState.finish(), ), ); }, + streaming: (Message message) { + final allMessages = _perminentMessages(); + allMessages.insert(0, message); + emit( + state.copyWith( + messages: allMessages, + streamingStatus: const LoadingState.loading(), + ), + ); + }, + didFinishStreaming: () { + emit( + state.copyWith(streamingStatus: const LoadingState.finish()), + ); + }, + receveMessage: (Message message) { + final allMessages = _perminentMessages(); + // remove message with the same id + allMessages.removeWhere((element) => element.id == message.id); + allMessages.insert(0, message); + emit( + state.copyWith( + messages: allMessages, + ), + ); + }, + sendMessage: (String message) { + _startStreamingMessage(message, emit); + final allMessages = _perminentMessages(); + emit( + state.copyWith( + lastSentMessage: null, + messages: allMessages, + relatedQuestions: [], + ), + ); + }, + didReceiveRelatedQuestion: (List questions) { + final allMessages = _perminentMessages(); + final message = CustomMessage( + metadata: OnetimeShotType.relatedQuestion.toMap(), + author: const User(id: "system"), + id: 'system', + ); + allMessages.insert(0, message); + emit( + state.copyWith( + messages: allMessages, + relatedQuestions: questions, + ), + ); + }, + clearReleatedQuestion: () { + emit( + state.copyWith( + relatedQuestions: [], + ), + ); + }, + didSentUserMessage: (ChatMessagePB message) { + emit( + state.copyWith( + lastSentMessage: message, + ), + ); + }, + didUpdateAnswerStream: (AnswerStream stream) { + emit(state.copyWith(answerStream: stream)); + }, stopStream: () async { - if (answerStream == null) { + if (state.answerStream == null) { return; } - // tell backend to stop final payload = StopStreamPB(chatId: chatId); - await AIEventStopStream(payload).send(); + await ChatEventStopStream(payload).send(); + final allMessages = _perminentMessages(); + if (state.streamingStatus != const LoadingState.finish()) { + // If the streaming is not started, remove the message from the list + if (!state.answerStream!.hasStarted) { + allMessages.removeWhere( + (element) => element.id == lastStreamMessageId, + ); + lastStreamMessageId = ""; + } - // allow user input - emit( - state.copyWith( - promptResponseState: PromptResponseState.ready, - ), - ); - - // no need to remove old message if stream has started already - if (answerStream!.hasStarted) { - return; + // when stop stream, we will set the answer stream to null. Which means the streaming + // is finished or canceled. + emit( + state.copyWith( + messages: allMessages, + answerStream: null, + streamingStatus: const LoadingState.finish(), + ), + ); } - - // remove the non-started message from the list - final message = chatController.messages.lastWhereOrNull( - (e) => e.id == answerStreamMessageId, - ); - if (message != null) { - await chatController.remove(message); - } - - // set answer stream to null - await answerStream?.dispose(); - answerStream = null; - answerStreamMessageId = ''; - }, - failedSending: () { - final lastMessage = chatController.messages.lastOrNull; - if (lastMessage != null) { - chatController.remove(lastMessage); - } - emit( - state.copyWith( - promptResponseState: PromptResponseState.ready, - ), - ); - }, - regenerateAnswer: (id, format, model) { - _clearRelatedQuestions(); - _regenerateAnswer(id, format, model); - lastSentMessage = null; - - isFetchingRelatedQuestions = false; - shouldFetchRelatedQuestions = false; - - emit( - state.copyWith( - promptResponseState: PromptResponseState.sendingQuestion, - ), - ); - }, - didReceiveChatSettings: (settings) { - selectedSourcesNotifier.value = settings.ragIds; - }, - updateSelectedSources: (selectedSourcesIds) async { - selectedSourcesNotifier.value = [...selectedSourcesIds]; - - final payload = UpdateChatSettingsPB( - chatId: ChatId(value: chatId), - ragIds: selectedSourcesIds, - ); - await AIEventUpdateChatSettings(payload) - .send() - .onFailure(Log.error); - }, - deleteMessage: (mesesage) async { - await chatController.remove(mesesage); }, ); }, @@ -278,35 +232,22 @@ class ChatBloc extends Bloc { void _startListening() { listener.start( chatMessageCallback: (pb) { - if (isClosed) { - return; - } + if (!isClosed) { + // 3 mean message response from AI + if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) { + temporaryMessageIDMap[pb.messageId.toString()] = + lastStreamMessageId; + lastStreamMessageId = ""; + } - // 3 mean message response from AI - if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) { - temporaryMessageIDMap.putIfAbsent( - pb.messageId.toString(), - () => answerStreamMessageId, - ); - answerStreamMessageId = ''; + final message = _createTextMessage(pb); + add(ChatEvent.receveMessage(message)); } - - // 1 mean message response from User - if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) { - temporaryMessageIDMap.putIfAbsent( - pb.messageId.toString(), - () => questionStreamMessageId, - ); - questionStreamMessageId = ''; - } - - final message = _createTextMessage(pb); - add(ChatEvent.receiveMessage(message)); }, chatErrorMessageCallback: (err) { if (!isClosed) { Log.error("chat error: ${err.errorMessage}"); - add(const ChatEvent.didFinishAnswerStream()); + add(const ChatEvent.didFinishStreaming()); } }, latestMessageCallback: (list) { @@ -321,250 +262,136 @@ class ChatBloc extends Bloc { add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore)); } }, - finishStreamingCallback: () async { - if (isClosed) { - return; - } - - add(const ChatEvent.didFinishAnswerStream()); - - // The answer stream will bet set to null after the streaming has - // finished, got cancelled, or errored. In this case, don't retrieve - // related questions. - if (answerStream == null || - lastSentMessage == null || - !shouldFetchRelatedQuestions) { - return; - } - - final payload = ChatMessageIdPB( - chatId: chatId, - messageId: lastSentMessage!.messageId, - ); - - isFetchingRelatedQuestions = true; - await AIEventGetRelatedQuestion(payload).send().fold( - (list) { - // while fetching related questions, the user might enter a new - // question or regenerate a previous response. In such cases, don't - // display the relatedQuestions - if (!isClosed && isFetchingRelatedQuestions) { - add( - ChatEvent.didReceiveRelatedQuestions( - list.items.map((e) => e.content).toList(), - ), - ); - isFetchingRelatedQuestions = false; - } - }, - (err) => Log.error("Failed to get related questions: $err"), - ); - }, - ); - } - - void _loadSetting() async { - final getChatSettingsPayload = - AIEventGetChatSettings(ChatId(value: chatId)); - await getChatSettingsPayload.send().fold( - (settings) { + finishStreamingCallback: () { if (!isClosed) { - add(ChatEvent.didReceiveChatSettings(settings: settings)); + add(const ChatEvent.didFinishStreaming()); + // The answer strema will bet set to null after the streaming is finished or canceled. + // so if the answer stream is null, we will not get related question. + if (state.lastSentMessage != null && state.answerStream != null) { + final payload = ChatMessageIdPB( + chatId: chatId, + messageId: state.lastSentMessage!.messageId, + ); + // When user message was sent to the server, we start gettting related question + ChatEventGetRelatedQuestion(payload).send().then((result) { + if (!isClosed) { + result.fold( + (list) { + add(ChatEvent.didReceiveRelatedQuestion(list.items)); + }, + (err) { + Log.error("Failed to get related question: $err"); + }, + ); + } + }); + } } }, - Log.error, ); } - void _loadMessages() async { - final loadMessagesPayload = LoadNextChatMessagePB( - chatId: chatId, - limit: Int64(10), - ); - await AIEventLoadNextMessage(loadMessagesPayload).send().fold( - (list) { - if (!isClosed) { - final messages = list.messages.map(_createTextMessage).toList(); - add(ChatEvent.didLoadLatestMessages(messages)); - } - }, - (err) => Log.error("Failed to load messages: $err"), - ); +// Returns the list of messages that are not include one-time messages. + List _perminentMessages() { + final allMessages = state.messages.where((element) { + return !(element.metadata?.containsKey(onetimeShotType) == true); + }).toList(); + + return allMessages; } - bool _isOneTimeMessage(Message message) { - return message.metadata != null && - message.metadata!.containsKey(onetimeShotType); + List _getOnetimeMessages() { + final messages = state.messages.where((element) { + return (element.metadata?.containsKey(onetimeShotType) == true); + }).toList(); + + return messages; } - /// get the last message that is not a one-time message - Message? _getOldestMessage() { - return chatController.messages - .firstWhereOrNull((message) => !_isOneTimeMessage(message)); + Message? _getOlderstMessage() { + // get the last message that is not a one-time message + final message = state.messages.lastWhereOrNull((element) { + return !(element.metadata?.containsKey(onetimeShotType) == true); + }); + return message; } - void _loadPreviousMessages(Int64? beforeMessageId) { + void _loadPrevMessage(Int64? beforeMessageId) { final payload = LoadPrevChatMessagePB( - chatId: chatId, + chatId: state.view.id, limit: Int64(10), beforeMessageId: beforeMessageId, ); - AIEventLoadPrevMessage(payload).send(); + ChatEventLoadPrevMessage(payload).send(); } Future _startStreamingMessage( String message, - PredefinedFormat? format, - Map? metadata, + Emitter emit, ) async { - await answerStream?.dispose(); - - answerStream = AnswerStream(); - final questionStream = QuestionStream(); - - // add a streaming question message - final questionStreamMessage = _createQuestionStreamMessage( - questionStream, - metadata, - ); - add(ChatEvent.receiveMessage(questionStreamMessage)); - - final payload = StreamChatPayloadPB( - chatId: chatId, - message: message, - messageType: ChatMessageTypePB.User, - questionStreamPort: Int64(questionStream.nativePort), - answerStreamPort: Int64(answerStream!.nativePort), - //metadata: await metadataPBFromMetadata(metadata), - ); - if (format != null) { - payload.format = format.toPB(); + if (state.answerStream != null) { + await state.answerStream?.dispose(); } - // stream the question to the server - await AIEventStreamMessage(payload).send().fold( - (question) { - if (!isClosed) { - final streamAnswer = _createAnswerStreamMessage( - stream: answerStream!, - questionMessageId: question.messageId, - fakeQuestionMessageId: questionStreamMessage.id, - ); + final answerStream = AnswerStream(); + add(ChatEvent.didUpdateAnswerStream(answerStream)); - lastSentMessage = question; - add(const ChatEvent.finishSending()); - add(ChatEvent.receiveMessage(streamAnswer)); + final payload = StreamChatPayloadPB( + chatId: state.view.id, + message: message, + messageType: ChatMessageTypePB.User, + textStreamPort: Int64(answerStream.nativePort), + ); + + // Stream message to the server + final result = await ChatEventStreamMessage(payload).send(); + result.fold( + (ChatMessagePB question) { + if (!isClosed) { + add(ChatEvent.didSentUserMessage(question)); + + final questionMessageId = question.messageId; + final message = _createTextMessage(question); + add(ChatEvent.receveMessage(message)); + + final streamAnswer = + _createStreamMessage(answerStream, questionMessageId); + add(ChatEvent.streaming(streamAnswer)); } }, (err) { if (!isClosed) { Log.error("Failed to send message: ${err.msg}"); + final metadata = OnetimeShotType.invalidSendMesssage.toMap(); + if (err.code != ErrorCode.Internal) { + metadata[sendMessageErrorKey] = err.msg; + } - final metadata = { - onetimeShotType: OnetimeShotType.error, - if (err.code != ErrorCode.Internal) errorMessageTextKey: err.msg, - }; - - final error = TextMessage( - text: '', + final error = CustomMessage( metadata: metadata, - author: const User(id: systemUserId), - id: systemUserId, - createdAt: DateTime.now(), + author: const User(id: "system"), + id: 'system', ); - add(const ChatEvent.failedSending()); - add(ChatEvent.receiveMessage(error)); + add(ChatEvent.receveMessage(error)); } }, ); } - void _regenerateAnswer( - String answerMessageIdString, - PredefinedFormat? format, - AIModelPB? model, - ) async { - final id = temporaryMessageIDMap.entries - .firstWhereOrNull((e) => e.value == answerMessageIdString) - ?.key ?? - answerMessageIdString; - final answerMessageId = Int64.tryParseInt(id); - - if (answerMessageId == null) { - return; - } - - await answerStream?.dispose(); - answerStream = AnswerStream(); - - final payload = RegenerateResponsePB( - chatId: chatId, - answerMessageId: answerMessageId, - answerStreamPort: Int64(answerStream!.nativePort), - ); - if (format != null) { - payload.format = format.toPB(); - } - if (model != null) { - payload.model = model; - } - - await AIEventRegenerateResponse(payload).send().fold( - (success) { - if (!isClosed) { - final streamAnswer = _createAnswerStreamMessage( - stream: answerStream!, - questionMessageId: answerMessageId - 1, - ).copyWith(id: answerMessageIdString); - - add(ChatEvent.receiveMessage(streamAnswer)); - add(const ChatEvent.finishSending()); - } - }, - (err) => Log.error("Failed to send message: ${err.msg}"), - ); - } - - Message _createAnswerStreamMessage({ - required AnswerStream stream, - required Int64 questionMessageId, - String? fakeQuestionMessageId, - }) { - answerStreamMessageId = fakeQuestionMessageId == null - ? (questionMessageId + 1).toString() - : "${fakeQuestionMessageId}_ans"; + Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) { + final streamMessageId = nanoid(); + lastStreamMessageId = streamMessageId; return TextMessage( - id: answerStreamMessageId, - text: '', - author: User(id: "streamId:${nanoid()}"), + author: User(id: nanoid()), metadata: { "$AnswerStream": stream, - messageQuestionIdKey: questionMessageId, + "question": questionMessageId, "chatId": chatId, }, - createdAt: DateTime.now(), - ); - } - - Message _createQuestionStreamMessage( - QuestionStream stream, - Map? sentMetadata, - ) { - final now = DateTime.now(); - - questionStreamMessageId = (now.millisecondsSinceEpoch ~/ 1000).toString(); - - return TextMessage( - author: User(id: userId), - metadata: { - "$QuestionStream": stream, - "chatId": chatId, - messageChatFileListKey: chatFilesFromMessageMetadata(sentMetadata), - }, - id: questionStreamMessageId, - createdAt: now, + id: streamMessageId, + createdAt: DateTime.now().millisecondsSinceEpoch, text: '', ); } @@ -581,110 +408,158 @@ class ChatBloc extends Bloc { author: User(id: message.authorId), id: messageId, text: message.content, - createdAt: message.createdAt.toDateTime(), - metadata: { - messageRefSourceJsonStringKey: message.metadata, - }, + createdAt: message.createdAt.toInt() * 1000, ); } - - void _clearErrorMessages(Emitter emit) { - final errorMessages = chatController.messages - .where( - (message) => - onetimeMessageTypeFromMeta(message.metadata) == - OnetimeShotType.error, - ) - .toList(); - - for (final message in errorMessages) { - chatController.remove(message); - } - emit(state.copyWith(clearErrorMessages: !state.clearErrorMessages)); - } - - void _clearRelatedQuestions() { - final relatedQuestionMessages = chatController.messages - .where( - (message) => - onetimeMessageTypeFromMeta(message.metadata) == - OnetimeShotType.relatedQuestion, - ) - .toList(); - - for (final message in relatedQuestionMessages) { - chatController.remove(message); - } - } } @freezed class ChatEvent with _$ChatEvent { - // chat settings - const factory ChatEvent.didReceiveChatSettings({ - required ChatSettingsPB settings, - }) = _DidReceiveChatSettings; - const factory ChatEvent.updateSelectedSources({ - required List selectedSourcesIds, - }) = _UpdateSelectedSources; - - // send message - const factory ChatEvent.sendMessage({ - required String message, - PredefinedFormat? format, - Map? metadata, - }) = _SendMessage; - const factory ChatEvent.finishSending() = _FinishSendMessage; - const factory ChatEvent.failedSending() = _FailSendMessage; - - // regenerate - const factory ChatEvent.regenerateAnswer( - String id, - PredefinedFormat? format, - AIModelPB? model, - ) = _RegenerateAnswer; - - // streaming answer - const factory ChatEvent.stopStream() = _StopStream; - const factory ChatEvent.didFinishAnswerStream() = _DidFinishAnswerStream; - - // receive message - const factory ChatEvent.receiveMessage(Message message) = _ReceiveMessage; - - // loading messages - const factory ChatEvent.didLoadLatestMessages(List messages) = - _DidLoadMessages; - const factory ChatEvent.loadPreviousMessages() = _LoadPreviousMessages; + const factory ChatEvent.initialLoad() = _InitialLoadMessage; + const factory ChatEvent.sendMessage(String message) = _SendMessage; + const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage; const factory ChatEvent.didLoadPreviousMessages( List messages, bool hasMore, ) = _DidLoadPreviousMessages; + const factory ChatEvent.didLoadLatestMessages(List messages) = + _DidLoadMessages; + const factory ChatEvent.streaming(Message message) = _StreamingMessage; + const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage; - // related questions - const factory ChatEvent.didReceiveRelatedQuestions( - List questions, + const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage; + const factory ChatEvent.didReceiveRelatedQuestion( + List questions, ) = _DidReceiveRelatedQueston; - - const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage; + const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion; + const factory ChatEvent.didSentUserMessage(ChatMessagePB message) = + _DidSendUserMessage; + const factory ChatEvent.didUpdateAnswerStream( + AnswerStream stream, + ) = _DidUpdateAnswerStream; + const factory ChatEvent.stopStream() = _StopStream; } @freezed class ChatState with _$ChatState { const factory ChatState({ - required LoadChatMessageStatus loadingState, - required PromptResponseState promptResponseState, - required bool clearErrorMessages, + required ViewPB view, + required List messages, + required UserProfilePB userProfile, + // When opening the chat, the initial loading status will be set as loading. + //After the initial loading is done, the status will be set as finished. + required LoadingState initialLoadingStatus, + // When loading previous messages, the status will be set as loading. + // After the loading is done, the status will be set as finished. + required LoadingState loadingPreviousStatus, + // When sending a user message, the status will be set as loading. + // After the message is sent, the status will be set as finished. + required LoadingState streamingStatus, + // Indicate whether there are more previous messages to load. + required bool hasMorePrevMessage, + // The related questions that are received after the user message is sent. + required List relatedQuestions, + // The last user message that is sent to the server. + ChatMessagePB? lastSentMessage, + AnswerStream? answerStream, }) = _ChatState; - factory ChatState.initial() => const ChatState( - loadingState: LoadChatMessageStatus.loading, - promptResponseState: PromptResponseState.ready, - clearErrorMessages: false, + factory ChatState.initial(ViewPB view, UserProfilePB userProfile) => + ChatState( + view: view, + messages: [], + userProfile: userProfile, + initialLoadingStatus: const LoadingState.finish(), + loadingPreviousStatus: const LoadingState.finish(), + streamingStatus: const LoadingState.finish(), + hasMorePrevMessage: true, + relatedQuestions: [], ); } -bool isOtherUserMessage(Message message) { - return message.author.id != aiResponseUserId && - message.author.id != systemUserId && - !message.author.id.startsWith("streamId:"); +@freezed +class LoadingState with _$LoadingState { + const factory LoadingState.loading() = _Loading; + const factory LoadingState.finish() = _Finish; +} + +enum OnetimeShotType { + unknown, + relatedQuestion, + invalidSendMesssage, +} + +const onetimeShotType = "OnetimeShotType"; + +extension OnetimeMessageTypeExtension on OnetimeShotType { + static OnetimeShotType fromString(String value) { + switch (value) { + case 'OnetimeShotType.relatedQuestion': + return OnetimeShotType.relatedQuestion; + case 'OnetimeShotType.invalidSendMesssage': + return OnetimeShotType.invalidSendMesssage; + default: + Log.error('Unknown OnetimeShotType: $value'); + return OnetimeShotType.unknown; + } + } + + Map toMap() { + return { + onetimeShotType: toString(), + }; + } +} + +OnetimeShotType? onetimeMessageTypeFromMeta(Map? metadata) { + if (metadata == null) { + return null; + } + + for (final entry in metadata.entries) { + if (entry.key == onetimeShotType) { + return OnetimeMessageTypeExtension.fromString(entry.value as String); + } + } + return null; +} + +typedef AnswerStreamElement = String; + +class AnswerStream { + AnswerStream() { + _port.handler = _controller.add; + _subscription = _controller.stream.listen( + (event) { + if (event.startsWith("data:")) { + _hasStarted = true; + } else if (event.startsWith("error:")) { + _error = event.substring(5); + } + }, + ); + } + + final RawReceivePort _port = RawReceivePort(); + final StreamController _controller = + StreamController.broadcast(); + late StreamSubscription _subscription; + bool _hasStarted = false; + String? _error; + + int get nativePort => _port.sendPort.nativePort; + bool get hasStarted => _hasStarted; + String? get error => _error; + + Future dispose() async { + await _controller.close(); + await _subscription.cancel(); + _port.close(); + } + + StreamSubscription listen( + void Function(AnswerStreamElement event)? onData, + ) { + return _controller.stream.listen(onData); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_edit_document_service.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_edit_document_service.dart deleted file mode 100644 index 630feb379b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_edit_document_service.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; - -class ChatEditDocumentService { - const ChatEditDocumentService._(); - - static Future saveMessagesToNewPage( - String chatPageName, - String parentViewId, - List messages, - ) async { - if (messages.isEmpty) { - return null; - } - - // Convert messages to markdown and trim the last empty newline. - final completeMessage = messages.map((m) => m.text).join('\n').trimRight(); - if (completeMessage.isEmpty) { - return null; - } - - final document = customMarkdownToDocument( - completeMessage, - tableWidth: 250.0, - ); - final initialBytes = - DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); - if (initialBytes == null) { - Log.error('Failed to convert messages to document'); - return null; - } - - return ViewBackendService.createView( - name: LocaleKeys.chat_addToNewPageName.tr(args: [chatPageName]), - layoutType: ViewLayoutPB.Document, - parentViewId: parentViewId, - initialDataBytes: initialBytes, - ).toNullable(); - } - - static Future addMessagesToPage( - String documentId, - List messages, - ) async { - // Convert messages to markdown and trim the last empty newline. - final completeMessage = messages.map((m) => m.text).join('\n').trimRight(); - if (completeMessage.isEmpty) { - return; - } - - final bloc = DocumentBloc( - documentId: documentId, - saveToBlocMap: false, - )..add(const DocumentEvent.initial()); - - if (bloc.state.editorState == null) { - await bloc.stream.firstWhere((state) => state.editorState != null); - } - - final editorState = bloc.state.editorState; - if (editorState == null) { - Log.error("Can't get EditorState of document"); - return; - } - - final messageDocument = customMarkdownToDocument( - completeMessage, - tableWidth: 250.0, - ); - if (messageDocument.isEmpty) { - Log.error('Failed to convert message to document'); - return; - } - - final lastNodeOrNull = editorState.document.root.children.lastOrNull; - - final rootIsEmpty = lastNodeOrNull == null; - final isLastLineEmpty = lastNodeOrNull?.children.isNotEmpty == false && - lastNodeOrNull?.delta?.isNotEmpty == false; - - final nodes = [ - if (rootIsEmpty || !isLastLineEmpty) paragraphNode(), - ...messageDocument.root.children, - paragraphNode(), - ]; - final insertPath = rootIsEmpty || - listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty - ? const [0] - : lastNodeOrNull.path.next; - - final transaction = editorState.transaction..insertNodes(insertPath, nodes); - await editorState.apply(transaction); - await bloc.close(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart deleted file mode 100644 index 41e2a6946d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_entity.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:equatable/equatable.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:path/path.dart' as path; - -part 'chat_entity.g.dart'; -part 'chat_entity.freezed.dart'; - -const errorMessageTextKey = "errorMessageText"; -const systemUserId = "system"; -const aiResponseUserId = "0"; - -/// `messageRefSourceJsonStringKey` is the key used for metadata that contains the reference source of a message. -/// Each message may include this information. -/// - When used in a sent message, it indicates that the message includes an attachment. -/// - When used in a received message, it indicates the AI reference sources used to answer a question. -const messageRefSourceJsonStringKey = "ref_source_json_string"; -const messageChatFileListKey = "chat_files"; -const messageQuestionIdKey = "question_id"; - -@JsonSerializable() -class ChatMessageRefSource { - ChatMessageRefSource({ - required this.id, - required this.name, - required this.source, - }); - - factory ChatMessageRefSource.fromJson(Map json) => - _$ChatMessageRefSourceFromJson(json); - - final String id; - final String name; - final String source; - - Map toJson() => _$ChatMessageRefSourceToJson(this); -} - -@JsonSerializable() -class AIChatProgress { - AIChatProgress({ - required this.step, - }); - - factory AIChatProgress.fromJson(Map json) => - _$AIChatProgressFromJson(json); - - final String step; - - Map toJson() => _$AIChatProgressToJson(this); -} - -enum PromptResponseState { - ready, - sendingQuestion, - streamingAnswer, -} - -class ChatFile extends Equatable { - const ChatFile({ - required this.filePath, - required this.fileName, - required this.fileType, - }); - - static ChatFile? fromFilePath(String filePath) { - final file = File(filePath); - if (!file.existsSync()) { - return null; - } - - final fileName = path.basename(filePath); - final extension = path.extension(filePath).toLowerCase(); - - ContextLoaderTypePB fileType; - switch (extension) { - case '.pdf': - fileType = ContextLoaderTypePB.PDF; - break; - case '.txt': - fileType = ContextLoaderTypePB.Txt; - break; - case '.md': - fileType = ContextLoaderTypePB.Markdown; - break; - default: - fileType = ContextLoaderTypePB.UnknownLoaderType; - } - - return ChatFile( - filePath: filePath, - fileName: fileName, - fileType: fileType, - ); - } - - final String filePath; - final String fileName; - final ContextLoaderTypePB fileType; - - @override - List get props => [filePath]; -} - -typedef ChatFileMap = Map; -typedef ChatMentionedPageMap = Map; - -@freezed -class ChatLoadingState with _$ChatLoadingState { - const factory ChatLoadingState.loading() = _Loading; - const factory ChatLoadingState.finish({FlowyError? error}) = _Finish; -} - -extension ChatLoadingStateExtension on ChatLoadingState { - bool get isLoading => this is _Loading; - bool get isFinish => this is _Finish; -} - -enum OnetimeShotType { - sendingMessage, - relatedQuestion, - error, -} - -const onetimeShotType = "OnetimeShotType"; - -OnetimeShotType? onetimeMessageTypeFromMeta(Map? metadata) { - return metadata?[onetimeShotType]; -} - -enum LoadChatMessageStatus { - loading, - loadingRemote, - ready, -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_control_cubit.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_control_cubit.dart deleted file mode 100644 index 63acb8be6d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_control_cubit.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/services.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_input_control_cubit.freezed.dart'; - -class ChatInputControlCubit extends Cubit { - ChatInputControlCubit() : super(const ChatInputControlState.loading()); - - final List allViews = []; - final List selectedViewIds = []; - - /// used when mentioning a page - /// - /// the text position after the @ character - int _filterStartPosition = -1; - - /// used when mentioning a page - /// - /// the text position after the @ character, at the end of the filter - int _filterEndPosition = -1; - - /// used when mentioning a page - /// - /// the entire string input in the prompt - String _inputText = ""; - - /// used when mentioning a page - /// - /// the current filtering text, after the @ characater - String _filter = ""; - - String get inputText => _inputText; - int get filterStartPosition => _filterStartPosition; - int get filterEndPosition => _filterEndPosition; - - void refreshViews() async { - final newViews = await ViewBackendService.getAllViews().fold( - (result) { - return result.items - .where( - (v) => - !v.isSpace && - v.layout.isDocumentView && - v.parentViewId != v.id, - ) - .toList(); - }, - (err) { - Log.error(err); - return []; - }, - ); - allViews - ..clear() - ..addAll(newViews); - - // update visible views - newViews.retainWhere((v) => !selectedViewIds.contains(v.id)); - if (_filter.isNotEmpty) { - newViews.retainWhere( - (v) { - final nonEmptyName = v.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : v.name; - return nonEmptyName.toLowerCase().contains(_filter); - }, - ); - } - final focusedViewIndex = newViews.isEmpty ? -1 : 0; - emit( - ChatInputControlState.ready( - visibleViews: newViews, - focusedViewIndex: focusedViewIndex, - ), - ); - } - - void startSearching(TextEditingValue textEditingValue) { - _filterStartPosition = - _filterEndPosition = textEditingValue.selection.baseOffset; - _filter = ""; - _inputText = textEditingValue.text; - state.maybeMap( - ready: (readyState) { - emit( - readyState.copyWith( - visibleViews: allViews, - focusedViewIndex: allViews.isEmpty ? -1 : 0, - ), - ); - }, - orElse: () {}, - ); - } - - void reset() { - _filterStartPosition = _filterEndPosition = -1; - _filter = _inputText = ""; - state.maybeMap( - ready: (readyState) { - emit( - readyState.copyWith( - visibleViews: allViews, - focusedViewIndex: allViews.isEmpty ? -1 : 0, - ), - ); - }, - orElse: () {}, - ); - } - - void updateFilter( - String newInputText, - String newFilter, { - int? newEndPosition, - }) { - updateInputText(newInputText); - - // filter the views - _filter = newFilter.toLowerCase(); - if (newEndPosition != null) { - _filterEndPosition = newEndPosition; - } - - final newVisibleViews = - allViews.where((v) => !selectedViewIds.contains(v.id)).toList(); - - if (_filter.isNotEmpty) { - newVisibleViews.retainWhere( - (v) { - final nonEmptyName = v.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : v.name; - return nonEmptyName.toLowerCase().contains(_filter); - }, - ); - } - - state.maybeWhen( - ready: (_, oldFocusedIndex) { - final newFocusedViewIndex = oldFocusedIndex < newVisibleViews.length - ? oldFocusedIndex - : (newVisibleViews.isEmpty ? -1 : 0); - emit( - ChatInputControlState.ready( - visibleViews: newVisibleViews, - focusedViewIndex: newFocusedViewIndex, - ), - ); - }, - orElse: () {}, - ); - } - - void updateInputText(String newInputText) { - _inputText = newInputText; - - // input text is changed, see if there are any deletions - selectedViewIds.retainWhere(_inputText.contains); - _notifyUpdateSelectedViews(); - } - - void updateSelectionUp() { - state.maybeMap( - ready: (readyState) { - final newIndex = readyState.visibleViews.isEmpty - ? -1 - : (readyState.focusedViewIndex - 1) % - readyState.visibleViews.length; - emit( - readyState.copyWith(focusedViewIndex: newIndex), - ); - }, - orElse: () {}, - ); - } - - void updateSelectionDown() { - state.maybeMap( - ready: (readyState) { - final newIndex = readyState.visibleViews.isEmpty - ? -1 - : (readyState.focusedViewIndex + 1) % - readyState.visibleViews.length; - emit( - readyState.copyWith(focusedViewIndex: newIndex), - ); - }, - orElse: () {}, - ); - } - - void selectPage(ViewPB view) { - selectedViewIds.add(view.id); - _notifyUpdateSelectedViews(); - reset(); - } - - String formatIntputText(final String input) { - String result = input; - for (final viewId in selectedViewIds) { - if (!result.contains(viewId)) { - continue; - } - final view = allViews.firstWhereOrNull((view) => view.id == viewId); - if (view != null) { - final nonEmptyName = view.name.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : view.name; - result = result.replaceAll(RegExp(viewId), nonEmptyName); - } - } - return result; - } - - void _notifyUpdateSelectedViews() { - final stateCopy = state; - final selectedViews = - allViews.where((view) => selectedViewIds.contains(view.id)).toList(); - emit(ChatInputControlState.updateSelectedViews(selectedViews)); - emit(stateCopy); - } -} - -@freezed -class ChatInputControlState with _$ChatInputControlState { - const factory ChatInputControlState.loading() = _Loading; - - const factory ChatInputControlState.ready({ - required List visibleViews, - required int focusedViewIndex, - }) = _Ready; - - const factory ChatInputControlState.updateSelectedViews( - List selectedViews, - ) = _UpdateOneShot; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart deleted file mode 100644 index 31d58eb000..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_input_file_bloc.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_input_file_bloc.freezed.dart'; - -class ChatInputFileBloc extends Bloc { - ChatInputFileBloc({ - required this.file, - }) : super(const ChatInputFileState()) { - on( - (event, emit) async { - event.when( - updateUploadState: (UploadFileIndicator indicator) { - emit(state.copyWith(uploadFileIndicator: indicator)); - }, - ); - }, - ); - } - - final ChatFile file; -} - -@freezed -class ChatInputFileEvent with _$ChatInputFileEvent { - const factory ChatInputFileEvent.updateUploadState( - UploadFileIndicator indicator, - ) = _UpdateUploadState; -} - -@freezed -class ChatInputFileState with _$ChatInputFileState { - const factory ChatInputFileState({ - UploadFileIndicator? uploadFileIndicator, - }) = _ChatInputFileState; -} - -@freezed -class UploadFileIndicator with _$UploadFileIndicator { - const factory UploadFileIndicator.finish() = _Finish; - const factory UploadFileIndicator.uploading() = _Uploading; - const factory UploadFileIndicator.error(String error) = _Error; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart deleted file mode 100644 index 2547ff668e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_member_bloc.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:equatable/equatable.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_member_bloc.freezed.dart'; - -class ChatMemberBloc extends Bloc { - ChatMemberBloc() : super(const ChatMemberState()) { - on( - (event, emit) async { - await event.when( - receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) { - final members = Map.from(state.members); - members[id] = ChatMember(info: memberInfo); - emit(state.copyWith(members: members)); - }, - getMemberInfo: (String userId) async { - if (state.members.containsKey(userId)) { - // Member info already exists. Debouncing refresh member info from backend would be better. - return; - } - - final payload = WorkspaceMemberIdPB( - uid: Int64.parseInt(userId), - ); - - await UserEventGetMemberInfo(payload).send().then((result) { - result.fold( - (member) { - if (!isClosed) { - add(ChatMemberEvent.receiveMemberInfo(userId, member)); - } - }, - (err) => Log.error("Error getting member info: $err"), - ); - }); - }, - ); - }, - ); - } -} - -@freezed -class ChatMemberEvent with _$ChatMemberEvent { - const factory ChatMemberEvent.getMemberInfo( - String userId, - ) = _GetMemberInfo; - const factory ChatMemberEvent.receiveMemberInfo( - String id, - WorkspaceMemberPB memberInfo, - ) = _ReceiveMemberInfo; -} - -@freezed -class ChatMemberState with _$ChatMemberState { - const factory ChatMemberState({ - @Default({}) Map members, - }) = _ChatMemberState; -} - -class ChatMember extends Equatable { - ChatMember({ - required this.info, - }); - final DateTime _date = DateTime.now(); - final WorkspaceMemberPB info; - - @override - List get props => [_date, info]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart index 4667806286..a26acd916f 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_listener.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart deleted file mode 100644 index 5bd8a35e5b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_service.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:nanoid/nanoid.dart'; - -/// Indicate file source from appflowy document -const appflowySource = "appflowy"; - -List fileListFromMessageMetadata( - Map? map, -) { - final List metadata = []; - if (map != null) { - for (final entry in map.entries) { - if (entry.value is ChatFile) { - metadata.add(entry.value); - } - } - } - - return metadata; -} - -List chatFilesFromMetadataString(String? s) { - if (s == null || s.isEmpty || s == "null") { - return []; - } - - final metadataJson = jsonDecode(s); - if (metadataJson is Map) { - final file = chatFileFromMap(metadataJson); - if (file != null) { - return [file]; - } else { - return []; - } - } else if (metadataJson is List) { - return metadataJson - .map((e) => e as Map) - .map(chatFileFromMap) - .where((file) => file != null) - .cast() - .toList(); - } else { - Log.error("Invalid metadata: $metadataJson"); - return []; - } -} - -ChatFile? chatFileFromMap(Map? map) { - if (map == null) return null; - - final filePath = map['source'] as String?; - final fileName = map['name'] as String?; - - if (filePath == null || fileName == null) { - return null; - } - return ChatFile.fromFilePath(filePath); -} - -class MetadataCollection { - MetadataCollection({ - required this.sources, - this.progress, - }); - final List sources; - final AIChatProgress? progress; -} - -MetadataCollection parseMetadata(String? s) { - if (s == null || s.trim().isEmpty || s.toLowerCase() == "null") { - return MetadataCollection(sources: []); - } - - final List metadata = []; - AIChatProgress? progress; - - try { - final dynamic decodedJson = jsonDecode(s); - if (decodedJson == null) { - return MetadataCollection(sources: []); - } - - void processMap(Map map) { - if (map.containsKey("step") && map["step"] != null) { - progress = AIChatProgress.fromJson(map); - } else if (map.containsKey("id") && map["id"] != null) { - metadata.add(ChatMessageRefSource.fromJson(map)); - } else { - Log.info("Unsupported metadata format: $map"); - } - } - - if (decodedJson is Map) { - processMap(decodedJson); - } else if (decodedJson is List) { - for (final element in decodedJson) { - if (element is Map) { - processMap(element); - } else { - Log.error("Invalid metadata element: $element"); - } - } - } else { - Log.error("Invalid metadata format: $decodedJson"); - } - } catch (e, stacktrace) { - Log.error("Failed to parse metadata: $e, input: $s"); - Log.debug(stacktrace.toString()); - } - - return MetadataCollection(sources: metadata, progress: progress); -} - -Future> metadataPBFromMetadata( - Map? map, -) async { - if (map == null) return []; - - final List metadata = []; - - for (final value in map.values) { - switch (value) { - case ViewPB _ when value.layout.isDocumentView: - final payload = OpenDocumentPayloadPB(documentId: value.id); - await DocumentEventGetDocumentText(payload).send().fold( - (pb) { - metadata.add( - ChatMessageMetaPB( - id: value.id, - name: value.name, - data: pb.text, - loaderType: ContextLoaderTypePB.Txt, - source: appflowySource, - ), - ); - }, - (err) => Log.error('Failed to get document text: $err'), - ); - break; - case ChatFile( - filePath: final filePath, - fileName: final fileName, - fileType: final fileType, - ): - metadata.add( - ChatMessageMetaPB( - id: nanoid(8), - name: fileName, - data: filePath, - loaderType: fileType, - source: filePath, - ), - ); - break; - } - } - - return metadata; -} - -List chatFilesFromMessageMetadata( - Map? map, -) { - final List metadata = []; - if (map != null) { - for (final entry in map.entries) { - if (entry.value is ChatFile) { - metadata.add(entry.value); - } - } - } - - return metadata; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart deleted file mode 100644 index c22559f21b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; - -import 'package:appflowy/ai/service/ai_entities.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart'; - -/// A stream that receives answer events from an isolate or external process. -/// It caches events that might occur before a listener is attached. -class AnswerStream { - AnswerStream() { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - _handleEvent, - onDone: _onDoneCallback, - onError: _handleError, - ); - } - - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - - bool _hasStarted = false; - bool _aiLimitReached = false; - bool _aiImageLimitReached = false; - String? _error; - String _text = ""; - - // Callbacks - void Function(String text)? _onData; - void Function()? _onStart; - void Function()? _onEnd; - void Function(String error)? _onError; - void Function()? _onLocalAIInitializing; - void Function()? _onAIResponseLimit; - void Function()? _onAIImageResponseLimit; - void Function(String message)? _onAIMaxRequired; - void Function(MetadataCollection metadata)? _onMetadata; - - // Caches for events that occur before listen() is called. - final List _pendingAIMaxRequiredEvents = []; - bool _pendingLocalAINotReady = false; - - int get nativePort => _port.sendPort.nativePort; - bool get hasStarted => _hasStarted; - bool get aiLimitReached => _aiLimitReached; - bool get aiImageLimitReached => _aiImageLimitReached; - String? get error => _error; - String get text => _text; - - /// Releases the resources used by the AnswerStream. - Future dispose() async { - await _controller.close(); - await _subscription.cancel(); - _port.close(); - } - - /// Handles incoming events from the underlying stream. - void _handleEvent(String event) { - if (event.startsWith(AIStreamEventPrefix.data)) { - _hasStarted = true; - final newText = event.substring(AIStreamEventPrefix.data.length); - _text += newText; - _onData?.call(_text); - } else if (event.startsWith(AIStreamEventPrefix.error)) { - _error = event.substring(AIStreamEventPrefix.error.length); - _onError?.call(_error!); - } else if (event.startsWith(AIStreamEventPrefix.metadata)) { - final s = event.substring(AIStreamEventPrefix.metadata.length); - _onMetadata?.call(parseMetadata(s)); - } else if (event == AIStreamEventPrefix.aiResponseLimit) { - _aiLimitReached = true; - _onAIResponseLimit?.call(); - } else if (event == AIStreamEventPrefix.aiImageResponseLimit) { - _aiImageLimitReached = true; - _onAIImageResponseLimit?.call(); - } else if (event.startsWith(AIStreamEventPrefix.aiMaxRequired)) { - final msg = event.substring(AIStreamEventPrefix.aiMaxRequired.length); - if (_onAIMaxRequired != null) { - _onAIMaxRequired!(msg); - } else { - _pendingAIMaxRequiredEvents.add(msg); - } - } else if (event.startsWith(AIStreamEventPrefix.localAINotReady)) { - if (_onLocalAIInitializing != null) { - _onLocalAIInitializing!(); - } else { - _pendingLocalAINotReady = true; - } - } - } - - void _onDoneCallback() { - _onEnd?.call(); - } - - void _handleError(dynamic error) { - _error = error.toString(); - _onError?.call(_error!); - } - - /// Registers listeners for various events. - /// - /// If certain events have already occurred (e.g. AI_MAX_REQUIRED or LOCAL_AI_NOT_READY), - /// they will be flushed immediately. - void listen({ - void Function(String text)? onData, - void Function()? onStart, - void Function()? onEnd, - void Function(String error)? onError, - void Function()? onAIResponseLimit, - void Function()? onAIImageResponseLimit, - void Function(String message)? onAIMaxRequired, - void Function(MetadataCollection metadata)? onMetadata, - void Function()? onLocalAIInitializing, - }) { - _onData = onData; - _onStart = onStart; - _onEnd = onEnd; - _onError = onError; - _onAIResponseLimit = onAIResponseLimit; - _onAIImageResponseLimit = onAIImageResponseLimit; - _onAIMaxRequired = onAIMaxRequired; - _onMetadata = onMetadata; - _onLocalAIInitializing = onLocalAIInitializing; - - // Flush pending AI_MAX_REQUIRED events. - if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) { - for (final msg in _pendingAIMaxRequiredEvents) { - _onAIMaxRequired!(msg); - } - _pendingAIMaxRequiredEvents.clear(); - } - - // Flush pending LOCAL_AI_NOT_READY event. - if (_pendingLocalAINotReady && _onLocalAIInitializing != null) { - _onLocalAIInitializing!(); - _pendingLocalAINotReady = false; - } - - _onStart?.call(); - } -} - -class QuestionStream { - QuestionStream() { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - (event) { - if (event.startsWith("data:")) { - _hasStarted = true; - final newText = event.substring(5); - _text += newText; - if (_onData != null) { - _onData!(_text); - } - } else if (event.startsWith("message_id:")) { - final messageId = event.substring(11); - _onMessageId?.call(messageId); - } else if (event.startsWith("start_index_file:")) { - final indexName = event.substring(17); - _onFileIndexStart?.call(indexName); - } else if (event.startsWith("end_index_file:")) { - final indexName = event.substring(10); - _onFileIndexEnd?.call(indexName); - } else if (event.startsWith("index_file_error:")) { - final indexName = event.substring(16); - _onFileIndexError?.call(indexName); - } else if (event.startsWith("index_start:")) { - _onIndexStart?.call(); - } else if (event.startsWith("index_end:")) { - _onIndexEnd?.call(); - } else if (event.startsWith("done:")) { - _onDone?.call(); - } else if (event.startsWith("error:")) { - _error = event.substring(5); - if (_onError != null) { - _onError!(_error!); - } - } - }, - onError: (error) { - if (_onError != null) { - _onError!(error.toString()); - } - }, - ); - } - - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - bool _hasStarted = false; - String? _error; - String _text = ""; - - // Callbacks - void Function(String text)? _onData; - void Function(String error)? _onError; - void Function(String messageId)? _onMessageId; - void Function(String indexName)? _onFileIndexStart; - void Function(String indexName)? _onFileIndexEnd; - void Function(String indexName)? _onFileIndexError; - void Function()? _onIndexStart; - void Function()? _onIndexEnd; - void Function()? _onDone; - - int get nativePort => _port.sendPort.nativePort; - bool get hasStarted => _hasStarted; - String? get error => _error; - String get text => _text; - - Future dispose() async { - await _controller.close(); - await _subscription.cancel(); - _port.close(); - } - - void listen({ - void Function(String text)? onData, - void Function(String error)? onError, - void Function(String messageId)? onMessageId, - void Function(String indexName)? onFileIndexStart, - void Function(String indexName)? onFileIndexEnd, - void Function(String indexName)? onFileIndexFail, - void Function()? onIndexStart, - void Function()? onIndexEnd, - void Function()? onDone, - }) { - _onData = onData; - _onError = onError; - _onMessageId = onMessageId; - - _onFileIndexStart = onFileIndexStart; - _onFileIndexEnd = onFileIndexEnd; - _onFileIndexError = onFileIndexFail; - - _onIndexStart = onIndexStart; - _onIndexEnd = onIndexEnd; - _onDone = onDone; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart index 7dc1b550c3..194748858b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_notification.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:appflowy/core/notification/notification_helper.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/rust_stream.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart deleted file mode 100644 index 9977d1df72..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_message_bloc.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/util.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_select_message_bloc.freezed.dart'; - -class ChatSelectMessageBloc - extends Bloc { - ChatSelectMessageBloc({required this.viewNotifier}) - : super(ChatSelectMessageState.initial()) { - _dispatch(); - } - - final ViewPluginNotifier viewNotifier; - - void _dispatch() { - on( - (event, emit) { - event.when( - enableStartSelectingMessages: () { - emit(state.copyWith(enabled: true)); - }, - toggleSelectingMessages: () { - if (state.isSelectingMessages) { - emit( - state.copyWith( - isSelectingMessages: false, - selectedMessages: [], - ), - ); - } else { - emit(state.copyWith(isSelectingMessages: true)); - } - }, - toggleSelectMessage: (Message message) { - if (state.selectedMessages.contains(message)) { - emit( - state.copyWith( - selectedMessages: state.selectedMessages - .where((m) => m != message) - .toList(), - ), - ); - } else { - emit( - state.copyWith( - selectedMessages: [...state.selectedMessages, message], - ), - ); - } - }, - selectAllMessages: (List messages) { - final filtered = messages.where(isAIMessage).toList(); - emit(state.copyWith(selectedMessages: filtered)); - }, - unselectAllMessages: () { - emit(state.copyWith(selectedMessages: const [])); - }, - reset: () { - emit( - state.copyWith( - isSelectingMessages: false, - selectedMessages: [], - ), - ); - }, - ); - }, - ); - } - - bool isMessageSelected(String messageId) => - state.selectedMessages.any((m) => m.id == messageId); - - bool isAIMessage(Message message) { - return message.author.id == aiResponseUserId || - message.author.id == systemUserId || - message.author.id.startsWith("streamId:"); - } -} - -@freezed -class ChatSelectMessageEvent with _$ChatSelectMessageEvent { - const factory ChatSelectMessageEvent.enableStartSelectingMessages() = - _EnableStartSelectingMessages; - const factory ChatSelectMessageEvent.toggleSelectingMessages() = - _ToggleSelectingMessages; - const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) = - _ToggleSelectMessage; - const factory ChatSelectMessageEvent.selectAllMessages( - List messages, - ) = _SelectAllMessages; - const factory ChatSelectMessageEvent.unselectAllMessages() = - _UnselectAllMessages; - const factory ChatSelectMessageEvent.reset() = _Reset; -} - -@freezed -class ChatSelectMessageState with _$ChatSelectMessageState { - const factory ChatSelectMessageState({ - required bool isSelectingMessages, - required List selectedMessages, - required bool enabled, - }) = _ChatSelectMessageState; - - factory ChatSelectMessageState.initial() => const ChatSelectMessageState( - enabled: false, - isSelectingMessages: false, - selectedMessages: [], - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart deleted file mode 100644 index 76cbd69cdf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_select_sources_cubit.dart +++ /dev/null @@ -1,420 +0,0 @@ -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'chat_select_sources_cubit.freezed.dart'; - -const int _kMaxSelectedParentPageCount = 3; - -enum SourceSelectedStatus { - unselected, - selected, - partiallySelected; - - bool get isUnselected => this == unselected; - bool get isSelected => this == selected; - bool get isPartiallySelected => this == partiallySelected; -} - -class ChatSource { - ChatSource({ - required this.view, - required this.parentView, - required this.children, - required bool isExpanded, - required SourceSelectedStatus selectedStatus, - required IgnoreViewType ignoreStatus, - }) : isExpandedNotifier = ValueNotifier(isExpanded), - selectedStatusNotifier = ValueNotifier(selectedStatus), - ignoreStatusNotifier = ValueNotifier(ignoreStatus); - - final ViewPB view; - final ViewPB? parentView; - final List children; - final ValueNotifier isExpandedNotifier; - final ValueNotifier selectedStatusNotifier; - final ValueNotifier ignoreStatusNotifier; - - bool get isExpanded => isExpandedNotifier.value; - SourceSelectedStatus get selectedStatus => selectedStatusNotifier.value; - IgnoreViewType get ignoreStatus => ignoreStatusNotifier.value; - - void toggleIsExpanded() { - isExpandedNotifier.value = !isExpanded; - } - - ChatSource copy() { - return ChatSource( - view: view, - parentView: parentView, - children: children.map((child) => child.copy()).toList(), - ignoreStatus: ignoreStatus, - isExpanded: isExpanded, - selectedStatus: selectedStatus, - ); - } - - ChatSource? findChildBySourceId(String sourceId) { - if (view.id == sourceId) { - return this; - } - for (final child in children) { - final childResult = child.findChildBySourceId(sourceId); - if (childResult != null) { - return childResult; - } - } - return null; - } - - void resetIgnoreViewTypeRecursive() { - ignoreStatusNotifier.value = view.layout.isDocumentView - ? IgnoreViewType.none - : IgnoreViewType.disable; - - for (final child in children) { - child.resetIgnoreViewTypeRecursive(); - } - } - - void updateIgnoreViewTypeRecursive(IgnoreViewType newIgnoreViewType) { - ignoreStatusNotifier.value = newIgnoreViewType; - for (final child in children) { - child.updateIgnoreViewTypeRecursive(newIgnoreViewType); - } - } - - void dispose() { - for (final child in children) { - child.dispose(); - } - isExpandedNotifier.dispose(); - selectedStatusNotifier.dispose(); - ignoreStatusNotifier.dispose(); - } -} - -class ChatSettingsCubit extends Cubit { - ChatSettingsCubit({ - this.hideDisabled = false, - }) : super(ChatSettingsState.initial()); - - final bool hideDisabled; - - final List selectedSourceIds = []; - final List sources = []; - final List selectedSources = []; - String filter = ''; - - void updateSelectedSources(List newSelectedSourceIds) { - selectedSourceIds.clear(); - selectedSourceIds.addAll(newSelectedSourceIds); - } - - void refreshSources(List spaceViews, ViewPB? currentSpace) async { - filter = ""; - - final newSources = await Future.wait( - spaceViews.map((view) => _recursiveBuild(view, null)), - ); - for (final source in newSources) { - _restrictSelectionIfNecessary(source.children); - } - if (currentSpace != null) { - newSources - .firstWhereOrNull((e) => e.view.id == currentSpace.id) - ?.toggleIsExpanded(); - } - - final selected = newSources - .map((source) => _buildSelectedSources(source)) - .flattened - .toList(); - - emit( - state.copyWith( - selectedSources: selected, - visibleSources: newSources, - ), - ); - - sources - ..forEach((e) => e.dispose()) - ..clear() - ..addAll(newSources.map((e) => e.copy())); - - selectedSources - ..forEach((e) => e.dispose()) - ..clear() - ..addAll(selected.map((e) => e.copy())); - } - - Future _recursiveBuild(ViewPB view, ViewPB? parentView) async { - SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected; - final isThisSourceSelected = selectedSourceIds.contains(view.id); - - final childrenViews = - await ViewBackendService.getChildViews(viewId: view.id).toNullable(); - - int selectedCount = 0; - final children = []; - - if (childrenViews != null) { - for (final childView in childrenViews) { - if (childView.layout == ViewLayoutPB.Chat) { - continue; - } - if (childView.layout != ViewLayoutPB.Document && hideDisabled) { - continue; - } - final childChatSource = await _recursiveBuild(childView, view); - if (childChatSource.selectedStatus.isSelected) { - selectedCount++; - } - children.add(childChatSource); - } - - final areAllChildrenSelectedOrNoChildren = - children.length == selectedCount; - final isAnyChildNotUnselected = - children.any((e) => !e.selectedStatus.isUnselected); - - if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) { - selectedStatus = SourceSelectedStatus.selected; - } else if (isThisSourceSelected || isAnyChildNotUnselected) { - selectedStatus = SourceSelectedStatus.partiallySelected; - } - } else if (isThisSourceSelected) { - selectedStatus = SourceSelectedStatus.selected; - } - - return ChatSource( - view: view, - parentView: parentView, - children: children, - ignoreStatus: view.layout.isDocumentView - ? IgnoreViewType.none - : IgnoreViewType.disable, - isExpanded: false, - selectedStatus: selectedStatus, - ); - } - - void _restrictSelectionIfNecessary(List sources) { - for (final source in sources) { - source.resetIgnoreViewTypeRecursive(); - } - if (sources.where((e) => !e.selectedStatus.isUnselected).length >= - _kMaxSelectedParentPageCount) { - sources - .where((e) => e.selectedStatus == SourceSelectedStatus.unselected) - .forEach( - (e) => e.updateIgnoreViewTypeRecursive(IgnoreViewType.disable), - ); - } - } - - void updateFilter(String filter) { - this.filter = filter; - for (final source in state.visibleSources) { - source.dispose(); - } - if (sources.isEmpty) { - emit(ChatSettingsState.initial()); - } else { - final selected = - selectedSources.map(_buildSearchResults).nonNulls.toList(); - final visible = - sources.map(_buildSearchResults).nonNulls.nonNulls.toList(); - emit( - state.copyWith( - selectedSources: selected, - visibleSources: visible, - ), - ); - } - } - - /// traverse tree to build up search query - ChatSource? _buildSearchResults(ChatSource chatSource) { - final isVisible = chatSource.view.nameOrDefault - .toLowerCase() - .contains(filter.toLowerCase()); - - final childrenResults = []; - for (final childSource in chatSource.children) { - final childResult = _buildSearchResults(childSource); - if (childResult != null) { - childrenResults.add(childResult); - } - } - - return isVisible || childrenResults.isNotEmpty - ? ChatSource( - view: chatSource.view, - parentView: chatSource.parentView, - children: childrenResults, - ignoreStatus: chatSource.ignoreStatus, - isExpanded: chatSource.isExpanded, - selectedStatus: chatSource.selectedStatus, - ) - : null; - } - - /// traverse tree to build up selected sources - Iterable _buildSelectedSources(ChatSource chatSource) { - final children = []; - - for (final childSource in chatSource.children) { - children.addAll(_buildSelectedSources(childSource)); - } - - return selectedSourceIds.contains(chatSource.view.id) - ? [ - ChatSource( - view: chatSource.view, - parentView: chatSource.parentView, - children: children, - ignoreStatus: chatSource.ignoreStatus, - selectedStatus: chatSource.selectedStatus, - isExpanded: true, - ), - ] - : children; - } - - void toggleSelectedStatus(ChatSource chatSource) { - if (chatSource.view.isSpace) { - return; - } - final allIds = _recursiveGetSourceIds(chatSource); - - if (chatSource.selectedStatus.isUnselected || - chatSource.selectedStatus.isPartiallySelected && - !chatSource.view.layout.isDocumentView) { - for (final id in allIds) { - if (!selectedSourceIds.contains(id)) { - selectedSourceIds.add(id); - } - } - } else { - for (final id in allIds) { - if (selectedSourceIds.contains(id)) { - selectedSourceIds.remove(id); - } - } - } - - updateSelectedStatus(); - } - - List _recursiveGetSourceIds(ChatSource chatSource) { - return [ - if (chatSource.view.layout.isDocumentView) chatSource.view.id, - for (final childSource in chatSource.children) - ..._recursiveGetSourceIds(childSource), - ]; - } - - void updateSelectedStatus() { - if (sources.isEmpty) { - return; - } - for (final source in sources) { - _recursiveUpdateSelectedStatus(source); - } - _restrictSelectionIfNecessary(sources); - for (final visibleSource in state.visibleSources) { - visibleSource.dispose(); - } - final visible = sources.map(_buildSearchResults).nonNulls.toList(); - - emit( - state.copyWith( - visibleSources: visible, - ), - ); - } - - SourceSelectedStatus _recursiveUpdateSelectedStatus(ChatSource chatSource) { - SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected; - - int selectedCount = 0; - for (final childSource in chatSource.children) { - final childStatus = _recursiveUpdateSelectedStatus(childSource); - if (childStatus.isSelected) { - selectedCount++; - } - } - - final isThisSourceSelected = selectedSourceIds.contains(chatSource.view.id); - final areAllChildrenSelectedOrNoChildren = - chatSource.children.length == selectedCount; - final isAnyChildNotUnselected = - chatSource.children.any((e) => !e.selectedStatus.isUnselected); - - if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) { - selectedStatus = SourceSelectedStatus.selected; - } else if (isThisSourceSelected || isAnyChildNotUnselected) { - selectedStatus = SourceSelectedStatus.partiallySelected; - } - - chatSource.selectedStatusNotifier.value = selectedStatus; - return selectedStatus; - } - - void toggleIsExpanded(ChatSource chatSource, bool isSelectedSection) { - chatSource.toggleIsExpanded(); - if (isSelectedSection) { - for (final selectedSource in selectedSources) { - selectedSource - .findChildBySourceId(chatSource.view.id) - ?.toggleIsExpanded(); - } - } else { - for (final source in sources) { - final child = source.findChildBySourceId(chatSource.view.id); - if (child != null) { - child.toggleIsExpanded(); - break; - } - } - } - } - - @override - Future close() { - for (final child in sources) { - child.dispose(); - } - for (final child in selectedSources) { - child.dispose(); - } - for (final child in state.selectedSources) { - child.dispose(); - } - for (final child in state.visibleSources) { - child.dispose(); - } - return super.close(); - } -} - -@freezed -class ChatSettingsState with _$ChatSettingsState { - const factory ChatSettingsState({ - required List visibleSources, - required List selectedSources, - }) = _ChatSettingsState; - - factory ChatSettingsState.initial() => const ChatSettingsState( - visibleSources: [], - selectedSources: [], - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart index bcd3713550..cae2b28c30 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart @@ -1,6 +1,9 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'chat_user_message_bloc.freezed.dart'; @@ -8,131 +11,48 @@ part 'chat_user_message_bloc.freezed.dart'; class ChatUserMessageBloc extends Bloc { ChatUserMessageBloc({ - required this.questionStream, - required String text, - }) : super(ChatUserMessageState.initial(text)) { - _dispatch(); - _startListening(); - } - - final QuestionStream? questionStream; - - void _dispatch() { + required Message message, + }) : super(ChatUserMessageState.initial(message)) { on( - (event, emit) { + (event, emit) async { event.when( - updateText: (String text) { - emit(state.copyWith(text: text)); + initial: () { + final payload = + WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id)); + UserEventGetMemberInfo(payload).send().then((result) { + if (!isClosed) { + result.fold((member) { + add(ChatUserMessageEvent.didReceiveMemberInfo(member)); + }, (err) { + Log.error("Error getting member info: $err"); + }); + } + }); }, - updateMessageId: (String messageId) { - emit(state.copyWith(messageId: messageId)); - }, - receiveError: (String error) {}, - updateQuestionState: (QuestionMessageState newState) { - emit(state.copyWith(messageState: newState)); + didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) { + emit(state.copyWith(member: memberInfo)); }, ); }, ); } - - void _startListening() { - questionStream?.listen( - onData: (text) { - if (!isClosed) { - add(ChatUserMessageEvent.updateText(text)); - } - }, - onMessageId: (messageId) { - if (!isClosed) { - add(ChatUserMessageEvent.updateMessageId(messageId)); - } - }, - onError: (error) { - if (!isClosed) { - add(ChatUserMessageEvent.receiveError(error.toString())); - } - }, - onFileIndexStart: (indexName) { - Log.debug("index start: $indexName"); - }, - onFileIndexEnd: (indexName) { - Log.info("index end: $indexName"); - }, - onFileIndexFail: (indexName) { - Log.debug("index fail: $indexName"); - }, - onIndexStart: () { - if (!isClosed) { - add( - const ChatUserMessageEvent.updateQuestionState( - QuestionMessageState.indexStart(), - ), - ); - } - }, - onIndexEnd: () { - if (!isClosed) { - add( - const ChatUserMessageEvent.updateQuestionState( - QuestionMessageState.indexEnd(), - ), - ); - } - }, - onDone: () { - if (!isClosed) { - add( - const ChatUserMessageEvent.updateQuestionState( - QuestionMessageState.finish(), - ), - ); - } - }, - ); - } } @freezed class ChatUserMessageEvent with _$ChatUserMessageEvent { - const factory ChatUserMessageEvent.updateText(String text) = _UpdateText; - const factory ChatUserMessageEvent.updateQuestionState( - QuestionMessageState newState, - ) = _UpdateQuestionState; - const factory ChatUserMessageEvent.updateMessageId(String messageId) = - _UpdateMessageId; - const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError; + const factory ChatUserMessageEvent.initial() = Initial; + const factory ChatUserMessageEvent.didReceiveMemberInfo( + WorkspaceMemberPB memberInfo, + ) = _MemberInfo; } @freezed class ChatUserMessageState with _$ChatUserMessageState { const factory ChatUserMessageState({ - required String text, - required String? messageId, - required QuestionMessageState messageState, + required Message message, + WorkspaceMemberPB? member, }) = _ChatUserMessageState; - factory ChatUserMessageState.initial(String message) => ChatUserMessageState( - text: message, - messageId: null, - messageState: const QuestionMessageState.finish(), - ); -} - -@freezed -class QuestionMessageState with _$QuestionMessageState { - const factory QuestionMessageState.indexFileStart(String fileName) = - _IndexFileStart; - const factory QuestionMessageState.indexFileEnd(String fileName) = - _IndexFileEnd; - const factory QuestionMessageState.indexFileFail(String fileName) = - _IndexFileFail; - - const factory QuestionMessageState.indexStart() = _IndexStart; - const factory QuestionMessageState.indexEnd() = _IndexEnd; - const factory QuestionMessageState.finish() = _Finish; -} - -extension QuestionMessageStateX on QuestionMessageState { - bool get isFinish => this is _Finish; + factory ChatUserMessageState.initial(Message message) => + ChatUserMessageState(message: message); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart index 76aba27dc0..540779b1a1 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart @@ -1,22 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/chat_page.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -54,16 +44,13 @@ class AIChatPagePlugin extends Plugin { }) : notifier = ViewPluginNotifier(view: view); late final ViewInfoBloc _viewInfoBloc; - late final _chatMessageSelectorBloc = - ChatSelectMessageBloc(viewNotifier: notifier); @override final ViewPluginNotifier notifier; @override PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder( - viewInfoBloc: _viewInfoBloc, - chatMessageSelectorBloc: _chatMessageSelectorBloc, + bloc: _viewInfoBloc, notifier: notifier, ); @@ -78,57 +65,40 @@ class AIChatPagePlugin extends Plugin { _viewInfoBloc = ViewInfoBloc(view: notifier.view) ..add(const ViewInfoEvent.started()); } - - @override - void dispose() { - _viewInfoBloc.close(); - _chatMessageSelectorBloc.close(); - notifier.dispose(); - } } class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder with NavigationItem { AIChatPagePluginWidgetBuilder({ - required this.viewInfoBloc, - required this.chatMessageSelectorBloc, + required this.bloc, required this.notifier, }); - final ViewInfoBloc viewInfoBloc; - final ChatSelectMessageBloc chatMessageSelectorBloc; + final ViewInfoBloc bloc; final ViewPluginNotifier notifier; int? deletedViewIndex; - @override - String? get viewName => notifier.view.nameOrDefault; - @override Widget get leftBarItem => ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => - ViewTabBarItem(view: notifier.view, shortForm: shortForm); + Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @override Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) { - notifier.isDeleted.addListener(_onDeleted); + notifier.isDeleted.addListener(() { + final deletedView = notifier.isDeleted.value; + if (deletedView != null && deletedView.hasIndex()) { + deletedViewIndex = deletedView.index; + } + }); - if (context.userProfile == null) { - Log.error("User profile is null when opening AI Chat plugin"); - return const SizedBox(); - } - - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: chatMessageSelectorBloc), - BlocProvider.value(value: viewInfoBloc), - ], + return BlocProvider.value( + value: bloc, child: AIChatPage( userProfile: context.userProfile!, key: ValueKey(notifier.view.id), @@ -139,67 +109,6 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder ); } - void _onDeleted() { - final deletedView = notifier.isDeleted.value; - if (deletedView != null && deletedView.hasIndex()) { - deletedViewIndex = deletedView.index; - } - } - @override List get navigationItems => [this]; - - @override - EdgeInsets get contentPadding => EdgeInsets.zero; - - @override - Widget? get rightBarItem => MultiBlocProvider( - providers: [ - BlocProvider.value(value: viewInfoBloc), - BlocProvider.value(value: chatMessageSelectorBloc), - ], - child: BlocBuilder( - builder: (context, state) { - if (state.isSelectingMessages) { - return const SizedBox.shrink(); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - ViewFavoriteButton( - key: ValueKey('favorite_button_${notifier.view.id}'), - view: notifier.view, - ), - const HSpace(4), - MoreViewActions( - key: ValueKey(notifier.view.id), - view: notifier.view, - customActions: [ - CustomViewAction( - view: notifier.view, - disabled: !state.enabled, - leftIcon: FlowySvgs.ai_add_to_page_s, - label: LocaleKeys.moreAction_saveAsNewPage.tr(), - tooltipMessage: state.enabled - ? null - : LocaleKeys.moreAction_saveAsNewPageDisabled.tr(), - onTap: () { - chatMessageSelectorBloc.add( - const ChatSelectMessageEvent - .toggleSelectingMessages(), - ); - }, - ), - ViewAction( - type: ViewMoreActionType.divider, - view: notifier.view, - ), - ], - ), - ], - ); - }, - ), - ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index 90085354db..1a7393a0a9 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -1,40 +1,54 @@ -import 'dart:io'; - -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/user_message_bubble.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:desktop_drop/desktop_drop.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'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; -import 'package:flutter_chat_ui/flutter_chat_ui.dart' - hide ChatAnimatedListReversed; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat; +import 'package:flutter_chat_types/flutter_chat_types.dart' as types; -import 'application/chat_bloc.dart'; -import 'application/chat_entity.dart'; -import 'application/chat_member_bloc.dart'; -import 'application/chat_select_message_bloc.dart'; -import 'application/chat_message_stream.dart'; -import 'presentation/animated_chat_list.dart'; -import 'presentation/chat_input/mobile_chat_input.dart'; -import 'presentation/chat_related_question.dart'; +import 'presentation/chat_input.dart'; +import 'presentation/chat_popmenu.dart'; +import 'presentation/chat_theme.dart'; +import 'presentation/chat_user_invalid_message.dart'; import 'presentation/chat_welcome_page.dart'; -import 'presentation/layout_define.dart'; import 'presentation/message/ai_text_message.dart'; -import 'presentation/message/error_text_message.dart'; -import 'presentation/message/message_util.dart'; import 'presentation/message/user_text_message.dart'; -import 'presentation/scroll_to_bottom.dart'; -class AIChatPage extends StatelessWidget { +class AIChatUILayout { + static EdgeInsets get chatPadding => + isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 70); + + static EdgeInsets get welcomePagePadding => isMobile + ? const EdgeInsets.symmetric(horizontal: 20) + : const EdgeInsets.symmetric(horizontal: 100); + + static double get messageWidthRatio => 0.85; + + static EdgeInsets safeAreaInsets(BuildContext context) { + final query = MediaQuery.of(context); + return isMobile + ? EdgeInsets.fromLTRB( + query.padding.left, + 0, + query.padding.right, + query.viewInsets.bottom + query.padding.bottom, + ) + : const EdgeInsets.symmetric(horizontal: 70); + } +} + +class AIChatPage extends StatefulWidget { const AIChatPage({ super.key, required this.view, @@ -47,445 +61,311 @@ class AIChatPage extends StatelessWidget { final UserProfilePB userProfile; @override - Widget build(BuildContext context) { - // if (userProfile.authenticator != AuthTypePB.Server) { - // return Center( - // child: FlowyText( - // LocaleKeys.chat_unsupportedCloudPrompt.tr(), - // fontSize: 20, - // ), - // ); - // } - - return MultiBlocProvider( - providers: [ - /// [ChatBloc] is used to handle chat messages including send/receive message - BlocProvider( - create: (_) => ChatBloc( - chatId: view.id, - userId: userProfile.id.toString(), - ), - ), - - /// [AIPromptInputBloc] is used to handle the user prompt - BlocProvider( - create: (_) => AIPromptInputBloc( - objectId: view.id, - predefinedFormat: PredefinedFormat( - imageFormat: ImageFormat.text, - textFormat: TextFormat.bulletList, - ), - ), - ), - BlocProvider(create: (_) => ChatMemberBloc()), - ], - child: Builder( - builder: (context) { - return DropTarget( - onDragDone: (DropDoneDetails detail) async { - if (context.read().state.supportChatWithFile) { - for (final file in detail.files) { - context - .read() - .add(AIPromptInputEvent.attachFile(file.path, file.name)); - } - } - }, - child: FocusScope( - onKeyEvent: (focusNode, event) { - if (event is! KeyUpEvent) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.escape || - event.logicalKey == LogicalKeyboardKey.keyC && - HardwareKeyboard.instance.isControlPressed) { - final chatBloc = context.read(); - if (chatBloc.state.promptResponseState != - PromptResponseState.ready) { - chatBloc.add(ChatEvent.stopStream()); - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; - }, - child: _ChatContentPage( - view: view, - userProfile: userProfile, - ), - ), - ); - }, - ), - ); - } + State createState() => _AIChatPageState(); } -class _ChatContentPage extends StatelessWidget { - const _ChatContentPage({ - required this.view, - required this.userProfile, - }); +class _AIChatPageState extends State { + late types.User _user; - final UserProfilePB userProfile; - final ViewPB view; + @override + void initState() { + super.initState(); + _user = types.User(id: widget.userProfile.id.toString()); + } @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return switch (state.loadingState) { - LoadChatMessageStatus.ready => Column( - children: [ - ChatMessageSelectorBanner( - view: view, - allMessages: context.read().chatController.messages, - ), - Expanded( - child: Align( - alignment: Alignment.topCenter, - child: _wrapConstraints( - SelectionArea( - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context) - .copyWith(scrollbars: false), - child: Chat( - chatController: - context.read().chatController, - user: User(id: userProfile.id.toString()), - darkTheme: - ChatTheme.fromThemeData(Theme.of(context)), - theme: ChatTheme.fromThemeData(Theme.of(context)), - builders: Builders( - inputBuilder: (_) => const SizedBox.shrink(), - textMessageBuilder: _buildTextMessage, - chatMessageBuilder: _buildChatMessage, - scrollToBottomBuilder: _buildScrollToBottom, - chatAnimatedListBuilder: _buildChatAnimatedList, + if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) { + return buildChatWidget(); + } else { + return Center( + child: FlowyText( + LocaleKeys.chat_unsupportedCloudPrompt.tr(), + fontSize: 20, + ), + ); + } + } + + Widget buildChatWidget() { + return SizedBox.expand( + child: Padding( + padding: AIChatUILayout.chatPadding, + child: BlocProvider( + create: (context) => ChatBloc( + view: widget.view, + userProfile: widget.userProfile, + )..add(const ChatEvent.initialLoad()), + child: BlocBuilder( + builder: (blocContext, state) { + return Chat( + messages: state.messages, + onAttachmentPressed: () {}, + onSendPressed: (types.PartialText message) { + // We use custom bottom widget for chat input, so + // do not need to handle this event. + }, + customBottomWidget: buildChatInput(blocContext), + user: _user, + theme: buildTheme(context), + onEndReached: () async { + if (state.hasMorePrevMessage && + state.loadingPreviousStatus != + const LoadingState.loading()) { + blocContext + .read() + .add(const ChatEvent.startLoadingPrevMessage()); + } + }, + emptyState: BlocBuilder( + builder: (context, state) { + return state.initialLoadingStatus == + const LoadingState.finish() + ? Padding( + padding: AIChatUILayout.welcomePagePadding, + child: ChatWelcomePage( + onSelectedQuestion: (question) { + blocContext + .read() + .add(ChatEvent.sendMessage(question)); + }, ), - ), - ), - ), - ), - ), + ) + : const Center( + child: CircularProgressIndicator.adaptive(), + ); + }, ), - _wrapConstraints( - _Input(view: view), - ), - ], - ), - _ => const Center(child: CircularProgressIndicator.adaptive()), - }; - }, + messageWidthRatio: AIChatUILayout.messageWidthRatio, + textMessageBuilder: ( + textMessage, { + required messageWidth, + required showName, + }) { + return _buildAITextMessage(blocContext, textMessage); + }, + bubbleBuilder: ( + child, { + required message, + required nextMessageInGroup, + }) { + if (message.author.id == _user.id) { + return ChatUserMessageBubble( + message: message, + child: child, + ); + } else { + return _buildAIBubble(message, blocContext, state, child); + } + }, + ); + }, + ), + ), + ), ); } - Widget _wrapConstraints(Widget child) { - return Container( - constraints: const BoxConstraints(maxWidth: 784), - margin: UniversalPlatform.isDesktop - ? const EdgeInsets.symmetric(horizontal: 60.0) - : null, - child: child, - ); + Widget _buildAITextMessage(BuildContext context, TextMessage message) { + final isAuthor = message.author.id == _user.id; + if (isAuthor) { + return ChatTextMessageWidget( + user: message.author, + messageUserId: message.id, + text: message.text, + ); + } else { + final stream = message.metadata?["$AnswerStream"]; + final questionId = message.metadata?["question"]; + return ChatAITextMessageWidget( + user: message.author, + messageUserId: message.id, + text: stream is AnswerStream ? stream : message.text, + key: ValueKey(message.id), + questionId: questionId, + chatId: widget.view.id, + ); + } } - Widget _buildTextMessage( - BuildContext context, - TextMessage message, + Widget _buildAIBubble( + Message message, + BuildContext blocContext, + ChatState state, + Widget child, ) { final messageType = onetimeMessageTypeFromMeta( message.metadata, ); - if (messageType == OnetimeShotType.error) { - return ChatErrorMessageWidget( - errorMessage: message.metadata?[errorMessageTextKey] ?? "", + if (messageType == OnetimeShotType.invalidSendMesssage) { + return ChatInvalidUserMessage( + message: message, ); } if (messageType == OnetimeShotType.relatedQuestion) { return RelatedQuestionList( - relatedQuestions: message.metadata!['questions'], onQuestionSelected: (question) { - final bloc = context.read(); - final showPredefinedFormats = bloc.state.showPredefinedFormats; - final predefinedFormat = bloc.state.predefinedFormat; - - context.read().add( - ChatEvent.sendMessage( - message: question, - format: showPredefinedFormats ? predefinedFormat : null, - ), - ); + blocContext.read().add(ChatEvent.sendMessage(question)); + blocContext + .read() + .add(const ChatEvent.clearReleatedQuestion()); }, + chatId: widget.view.id, + relatedQuestions: state.relatedQuestions, ); } - if (message.author.id == userProfile.id.toString() || - isOtherUserMessage(message)) { - return ChatUserMessageWidget( - user: message.author, - message: message, - ); - } - - final stream = message.metadata?["$AnswerStream"]; - final questionId = message.metadata?[messageQuestionIdKey]; - final refSourceJsonString = - message.metadata?[messageRefSourceJsonStringKey] as String?; - - return BlocSelector( - selector: (state) => state.isSelectingMessages, - builder: (context, isSelectingMessages) { - return BlocBuilder( - builder: (context, state) { - final chatController = context.read().chatController; - final messages = chatController.messages - .where((e) => onetimeMessageTypeFromMeta(e.metadata) == null); - final isLastMessage = - messages.isEmpty ? false : messages.last.id == message.id; - return ChatAIMessageWidget( - user: message.author, - messageUserId: message.id, - message: message, - stream: stream is AnswerStream ? stream : null, - questionId: questionId, - chatId: view.id, - refSourceJsonString: refSourceJsonString, - isStreaming: - state.promptResponseState != PromptResponseState.ready, - isLastMessage: isLastMessage, - isSelectingMessages: isSelectingMessages, - onSelectedMetadata: (metadata) => - _onSelectMetadata(context, metadata), - onRegenerate: () => context - .read() - .add(ChatEvent.regenerateAnswer(message.id, null, null)), - onChangeFormat: (format) => context - .read() - .add(ChatEvent.regenerateAnswer(message.id, format, null)), - onChangeModel: (model) => context - .read() - .add(ChatEvent.regenerateAnswer(message.id, null, model)), - onStopStream: () => context.read().add( - const ChatEvent.stopStream(), - ), - ); - }, - ); - }, - ); - } - - Widget _buildChatMessage( - BuildContext context, - Message message, - Animation animation, - Widget child, - ) { - return ChatMessage( + return ChatAIMessageBubble( message: message, - animation: animation, - padding: const EdgeInsets.symmetric(vertical: 12.0), - receivedMessageScaleAnimationAlignment: Alignment.center, + customMessageType: messageType, child: child, ); } - Widget _buildScrollToBottom( - BuildContext context, - Animation animation, - VoidCallback onPressed, - ) { - return CustomScrollToBottom( - animation: animation, - onPressed: onPressed, - ); - } - - Widget _buildChatAnimatedList( - BuildContext context, - ScrollController scrollController, - ChatItem itemBuilder, - ) { - final bloc = context.read(); - - if (bloc.chatController.messages.isEmpty) { - return ChatWelcomePage( - userProfile: userProfile, - onSelectedQuestion: (question) { - final aiPromptInputBloc = context.read(); - final showPredefinedFormats = - aiPromptInputBloc.state.showPredefinedFormats; - final predefinedFormat = aiPromptInputBloc.state.predefinedFormat; - bloc.add( - ChatEvent.sendMessage( - message: question, - format: showPredefinedFormats ? predefinedFormat : null, - ), + Widget buildBubble(Message message, Widget child) { + final isAuthor = message.author.id == _user.id; + const borderRadius = BorderRadius.all(Radius.circular(6)); + final childWithPadding = isAuthor + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + child: child, + ) + : Padding( + padding: const EdgeInsets.all(8), + child: child, ); - }, + + // If the message is from the author, we will decorate it with a different color + final decoratedChild = isAuthor + ? DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: !isAuthor || message.type == types.MessageType.image + ? AFThemeExtension.of(context).tint1 + : Theme.of(context).colorScheme.secondary, + ), + child: childWithPadding, + ) + : childWithPadding; + + // If the message is from the author, no further actions are needed + if (isAuthor) { + return ClipRRect( + borderRadius: borderRadius, + child: decoratedChild, ); - } - - context - .read() - .add(ChatSelectMessageEvent.enableStartSelectingMessages()); - - return BlocSelector( - selector: (state) => state.isSelectingMessages, - builder: (context, isSelectingMessages) { - return ChatAnimatedListReversed( - scrollController: scrollController, - itemBuilder: itemBuilder, - bottomPadding: isSelectingMessages - ? 48.0 + DesktopAIChatSizes.messageActionBarIconSize - : 8.0, - onLoadPreviousMessages: () { - bloc.add(const ChatEvent.loadPreviousMessages()); + } else { + if (isMobile) { + return ChatPopupMenu( + onAction: (action) { + switch (action) { + case ChatMessageAction.copy: + if (message is TextMessage) { + Clipboard.setData(ClipboardData(text: message.text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + } + break; + } }, + builder: (context) => + ClipRRect(borderRadius: borderRadius, child: decoratedChild), ); - }, + } else { + // Show hover effect only on desktop + return ClipRRect( + borderRadius: borderRadius, + child: ChatAIMessageHover( + message: message, + child: decoratedChild, + ), + ); + } + } + } + + Widget buildChatInput(BuildContext context) { + return ClipRect( + child: Padding( + padding: AIChatUILayout.safeAreaInsets(context), + child: Column( + children: [ + BlocSelector( + selector: (state) => state.streamingStatus, + builder: (context, state) { + return ChatInput( + chatId: widget.view.id, + onSendPressed: (message) => + onSendPressed(context, message.text), + isStreaming: state != const LoadingState.finish(), + onStopStreaming: () { + context.read().add(const ChatEvent.stopStream()); + }, + ); + }, + ), + const VSpace(6), + Opacity( + opacity: 0.6, + child: FlowyText( + LocaleKeys.chat_aiMistakePrompt.tr(), + fontSize: 12, + ), + ), + ], + ), + ), ); } - void _onSelectMetadata( - BuildContext context, - ChatMessageRefSource metadata, - ) async { - // When the source of metatdata is appflowy, which means it is a appflowy page - if (metadata.source == "appflowy") { - final sidebarView = - await ViewBackendService.getView(metadata.id).toNullable(); - if (context.mounted) { - openPageFromMessage(context, sidebarView); - } - return; - } - - if (metadata.source == "web") { - if (isURL(metadata.name)) { - late Uri uri; - try { - uri = Uri.parse(metadata.name); - // `Uri` identifies `localhost` as a scheme - if (!uri.hasScheme || uri.scheme == 'localhost') { - uri = Uri.parse("http://${metadata.name}"); - await InternetAddress.lookup(uri.host); - } - await launchUrl(uri); - } catch (err) { - Log.error("failed to open url $err"); - } - } - return; - } - } -} - -class _Input extends StatefulWidget { - const _Input({ - required this.view, - }); - - final ViewPB view; - - @override - State<_Input> createState() => _InputState(); -} - -class _InputState extends State<_Input> { - final textController = TextEditingController(); - - @override - void dispose() { - textController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) => state.isSelectingMessages, - builder: (context, isSelectingMessages) { - return AnimatedSwitcher( - duration: const Duration(milliseconds: 150), - transitionBuilder: (child, animation) { - return SizeTransition( - sizeFactor: animation, - axisAlignment: -1, - child: child, - ); - }, - child: isSelectingMessages - ? const SizedBox.shrink() - : Padding( - padding: AIChatUILayout.safeAreaInsets(context), - child: BlocSelector( - selector: (state) { - return state.promptResponseState == - PromptResponseState.ready; - }, - builder: (context, canSendMessage) { - final chatBloc = context.read(); - - return UniversalPlatform.isDesktop - ? DesktopPromptInput( - isStreaming: !canSendMessage, - textController: textController, - onStopStreaming: () { - chatBloc.add(const ChatEvent.stopStream()); - }, - onSubmitted: (text, format, metadata) { - chatBloc.add( - ChatEvent.sendMessage( - message: text, - format: format, - metadata: metadata, - ), - ); - }, - selectedSourcesNotifier: - chatBloc.selectedSourcesNotifier, - onUpdateSelectedSources: (ids) { - chatBloc.add( - ChatEvent.updateSelectedSources( - selectedSourcesIds: ids, - ), - ); - }, - ) - : MobileChatInput( - isStreaming: !canSendMessage, - onStopStreaming: () { - chatBloc.add(const ChatEvent.stopStream()); - }, - onSubmitted: (text, format, metadata) { - chatBloc.add( - ChatEvent.sendMessage( - message: text, - format: format, - metadata: metadata, - ), - ); - }, - selectedSourcesNotifier: - chatBloc.selectedSourcesNotifier, - onUpdateSelectedSources: (ids) { - chatBloc.add( - ChatEvent.updateSelectedSources( - selectedSourcesIds: ids, - ), - ); - }, - ); - }, - ), - ), - ); - }, + AFDefaultChatTheme buildTheme(BuildContext context) { + return AFDefaultChatTheme( + backgroundColor: AFThemeExtension.of(context).background, + primaryColor: Theme.of(context).colorScheme.primary, + secondaryColor: AFThemeExtension.of(context).tint1, + receivedMessageDocumentIconColor: Theme.of(context).primaryColor, + receivedMessageCaptionTextStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedMessageBodyTextStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedMessageLinkTitleTextStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + receivedMessageBodyLinkTextStyle: const TextStyle( + color: Colors.lightBlue, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + sentMessageBodyTextStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + sentMessageBodyLinkTextStyle: const TextStyle( + color: Colors.blue, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + inputElevation: 2, ); } + + void onSendPressed(BuildContext context, String message) { + context.read().add(ChatEvent.sendMessage(message)); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart new file mode 100644 index 0000000000..8246378bda --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/ai_message_bubble.dart @@ -0,0 +1,186 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:styled_widget/styled_widget.dart'; + +const _leftPadding = 16.0; + +class ChatAIMessageBubble extends StatelessWidget { + const ChatAIMessageBubble({ + super.key, + required this.message, + required this.child, + this.customMessageType, + }); + + final Message message; + final Widget child; + final OnetimeShotType? customMessageType; + + @override + Widget build(BuildContext context) { + const padding = EdgeInsets.symmetric(horizontal: _leftPadding); + final childWithPadding = Padding(padding: padding, child: child); + final widget = isMobile + ? _wrapPopMenu(childWithPadding) + : _wrapHover(childWithPadding); + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ChatBorderedCircleAvatar( + child: FlowySvg( + FlowySvgs.flowy_ai_chat_logo_s, + size: Size.square(24), + ), + ), + Expanded(child: widget), + ], + ); + } + + ChatAIMessageHover _wrapHover(Padding child) { + return ChatAIMessageHover( + message: message, + customMessageType: customMessageType, + child: child, + ); + } + + ChatPopupMenu _wrapPopMenu(Padding childWithPadding) { + return ChatPopupMenu( + onAction: (action) { + if (action == ChatMessageAction.copy && message is TextMessage) { + Clipboard.setData(ClipboardData(text: (message as TextMessage).text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + } + }, + builder: (context) => childWithPadding, + ); + } +} + +class ChatAIMessageHover extends StatefulWidget { + const ChatAIMessageHover({ + super.key, + required this.child, + required this.message, + this.customMessageType, + }); + + final Widget child; + final Message message; + final bool autoShowHover = true; + final OnetimeShotType? customMessageType; + + @override + State createState() => _ChatAIMessageHoverState(); +} + +class _ChatAIMessageHoverState extends State { + bool _isHover = false; + + @override + void initState() { + super.initState(); + _isHover = widget.autoShowHover ? false : true; + } + + @override + Widget build(BuildContext context) { + final List children = [ + DecoratedBox( + decoration: const BoxDecoration( + color: Colors.transparent, + borderRadius: Corners.s6Border, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: widget.child, + ), + ), + ]; + + if (_isHover) { + children.addAll(_buildOnHoverItems()); + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = true; + } + }), + onExit: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = false; + } + }), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: children, + ), + ); + } + + List _buildOnHoverItems() { + final List children = []; + if (widget.customMessageType != null) { + // + } else { + if (widget.message is TextMessage) { + children.add( + CopyButton( + textMessage: widget.message as TextMessage, + ).positioned(left: _leftPadding, bottom: 0), + ); + } + } + + return children; + } +} + +class CopyButton extends StatelessWidget { + const CopyButton({ + super.key, + required this.textMessage, + }); + final TextMessage textMessage; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.settings_menu_clickToCopy.tr(), + child: FlowyIconButton( + width: 24, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + fillColor: Theme.of(context).cardColor, + icon: FlowySvg( + FlowySvgs.ai_copy_s, + size: const Size.square(14), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () { + Clipboard.setData(ClipboardData(text: textMessage.text)); + showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart deleted file mode 100644 index 9b7aadf4a4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/animated_chat_list.dart +++ /dev/null @@ -1,370 +0,0 @@ -// ignore_for_file: implementation_imports - -import 'dart:async'; -import 'dart:math'; - -import 'package:diffutil_dart/diffutil.dart' as diffutil; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; -import 'package:flutter_chat_ui/src/scroll_to_bottom.dart'; -import 'package:flutter_chat_ui/src/utils/message_list_diff.dart'; -import 'package:provider/provider.dart'; - -class ChatAnimatedListReversed extends StatefulWidget { - const ChatAnimatedListReversed({ - super.key, - required this.scrollController, - required this.itemBuilder, - this.insertAnimationDuration = const Duration(milliseconds: 250), - this.removeAnimationDuration = const Duration(milliseconds: 250), - this.scrollToEndAnimationDuration = const Duration(milliseconds: 250), - this.scrollToBottomAppearanceDelay = const Duration(milliseconds: 250), - this.bottomPadding = 8, - this.onLoadPreviousMessages, - }); - - final ScrollController scrollController; - final ChatItem itemBuilder; - final Duration insertAnimationDuration; - final Duration removeAnimationDuration; - final Duration scrollToEndAnimationDuration; - final Duration scrollToBottomAppearanceDelay; - final double? bottomPadding; - final VoidCallback? onLoadPreviousMessages; - - @override - ChatAnimatedListReversedState createState() => - ChatAnimatedListReversedState(); -} - -class ChatAnimatedListReversedState extends State - with SingleTickerProviderStateMixin { - final GlobalKey _listKey = GlobalKey(); - late ChatController _chatController; - late List _oldList; - late StreamSubscription _operationsSubscription; - - late final AnimationController _scrollToBottomController; - late final Animation _scrollToBottomAnimation; - Timer? _scrollToBottomShowTimer; - - bool _userHasScrolled = false; - bool _isScrollingToBottom = false; - String _lastInsertedMessageId = ''; - - @override - void initState() { - super.initState(); - _chatController = Provider.of(context, listen: false); - // TODO: Add assert for messages having same id - _oldList = List.from(_chatController.messages); - _operationsSubscription = _chatController.operationsStream.listen((event) { - switch (event.type) { - case ChatOperationType.insert: - assert( - event.index != null, - 'Index must be provided when inserting a message.', - ); - assert( - event.message != null, - 'Message must be provided when inserting a message.', - ); - _onInserted(event.index!, event.message!); - _oldList = List.from(_chatController.messages); - break; - case ChatOperationType.remove: - assert( - event.index != null, - 'Index must be provided when removing a message.', - ); - assert( - event.message != null, - 'Message must be provided when removing a message.', - ); - _onRemoved(event.index!, event.message!); - _oldList = List.from(_chatController.messages); - break; - case ChatOperationType.set: - final newList = _chatController.messages; - - final updates = diffutil - .calculateDiff( - MessageListDiff(_oldList, newList), - ) - .getUpdatesWithData(); - - for (var i = updates.length - 1; i >= 0; i--) { - _onDiffUpdate(updates.elementAt(i)); - } - - _oldList = List.from(newList); - break; - default: - break; - } - }); - - _scrollToBottomController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 300), - ); - _scrollToBottomAnimation = CurvedAnimation( - parent: _scrollToBottomController, - curve: Curves.easeInOut, - ); - - widget.scrollController.addListener(_handleLoadPreviousMessages); - WidgetsBinding.instance.addPostFrameCallback((_) { - _handleLoadPreviousMessages(); - }); - } - - @override - void dispose() { - super.dispose(); - _scrollToBottomShowTimer?.cancel(); - _scrollToBottomController.dispose(); - _operationsSubscription.cancel(); - widget.scrollController.removeListener(_handleLoadPreviousMessages); - } - - @override - Widget build(BuildContext context) { - final builders = context.watch(); - - return NotificationListener( - onNotification: (notification) { - if (notification is UserScrollNotification) { - // When user scrolls up, save it to `_userHasScrolled` - if (notification.direction == ScrollDirection.reverse) { - _userHasScrolled = true; - } else { - // When user overscolls to the bottom or stays idle at the bottom, set `_userHasScrolled` to false - if (notification.metrics.pixels == - notification.metrics.minScrollExtent) { - _userHasScrolled = false; - } - } - } - - if (notification is ScrollUpdateNotification) { - _handleToggleScrollToBottom(); - } - - // Allow other listeners to get the notification - return false; - }, - child: Stack( - children: [ - CustomScrollView( - reverse: true, - controller: widget.scrollController, - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - top: widget.bottomPadding ?? 0, - ), - ), - SliverAnimatedList( - key: _listKey, - initialItemCount: _chatController.messages.length, - itemBuilder: ( - BuildContext context, - int index, - Animation animation, - ) { - final message = _chatController.messages[ - max(_chatController.messages.length - 1 - index, 0)]; - return widget.itemBuilder( - context, - animation, - message, - ); - }, - ), - ], - ), - builders.scrollToBottomBuilder?.call( - context, - _scrollToBottomAnimation, - _handleScrollToBottom, - ) ?? - ScrollToBottom( - animation: _scrollToBottomAnimation, - onPressed: _handleScrollToBottom, - ), - ], - ), - ); - } - - void _subsequentScrollToEnd(Message data) async { - final user = Provider.of(context, listen: false); - - // We only want to scroll to the bottom if user has not scrolled up - // or if the message is sent by the current user. - if (data.id == _lastInsertedMessageId && - widget.scrollController.offset > - widget.scrollController.position.minScrollExtent && - (user.id == data.author.id && _userHasScrolled)) { - if (widget.scrollToEndAnimationDuration == Duration.zero) { - widget.scrollController - .jumpTo(widget.scrollController.position.minScrollExtent); - } else { - await widget.scrollController.animateTo( - widget.scrollController.position.minScrollExtent, - duration: widget.scrollToEndAnimationDuration, - curve: Curves.linearToEaseOut, - ); - } - - if (!widget.scrollController.hasClients || !mounted) return; - - // Because of the issue I have opened here https://github.com/flutter/flutter/issues/129768 - // we need an additional jump to the end. Sometimes Flutter - // will not scroll to the very end. Sometimes it will not scroll to the - // very end even with this, so this is something that needs to be - // addressed by the Flutter team. - // - // Additionally here we have a check for the message id, because - // if new message arrives in the meantime it will trigger another - // scroll to the end animation, making this logic redundant. - if (data.id == _lastInsertedMessageId && - widget.scrollController.offset > - widget.scrollController.position.minScrollExtent && - (user.id == data.author.id && _userHasScrolled)) { - widget.scrollController - .jumpTo(widget.scrollController.position.minScrollExtent); - } - } - } - - void _scrollToEnd(Message data) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - if (!widget.scrollController.hasClients || !mounted) return; - - _subsequentScrollToEnd(data); - }, - ); - } - - void _handleScrollToBottom() { - _isScrollingToBottom = true; - _scrollToBottomController.reverse(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (!widget.scrollController.hasClients || !mounted) return; - - if (widget.scrollToEndAnimationDuration == Duration.zero) { - widget.scrollController - .jumpTo(widget.scrollController.position.minScrollExtent); - } else { - await widget.scrollController.animateTo( - widget.scrollController.position.minScrollExtent, - duration: widget.scrollToEndAnimationDuration, - curve: Curves.linearToEaseOut, - ); - } - - if (!widget.scrollController.hasClients || !mounted) return; - - if (widget.scrollController.offset < - widget.scrollController.position.minScrollExtent) { - widget.scrollController.jumpTo( - widget.scrollController.position.minScrollExtent, - ); - } - - _isScrollingToBottom = false; - }); - } - - void _handleToggleScrollToBottom() { - if (_isScrollingToBottom) { - return; - } - - _scrollToBottomShowTimer?.cancel(); - if (widget.scrollController.offset > - widget.scrollController.position.minScrollExtent) { - _scrollToBottomShowTimer = - Timer(widget.scrollToBottomAppearanceDelay, () { - if (mounted) { - _scrollToBottomController.forward(); - } - }); - } else { - if (_scrollToBottomController.status != AnimationStatus.completed) { - _scrollToBottomController.stop(); - } - _scrollToBottomController.reverse(); - } - } - - void _onInserted(final int position, final Message data) { - // There is a scroll notification listener the controls the - // `_userHasScrolled` variable. - // - // If for some reason `_userHasScrolled` is true and the user is not at the - // bottom of the list, set `_userHasScrolled` to false so that the scroll - // animation is triggered. - if (position == 0 && - _userHasScrolled && - widget.scrollController.offset > - widget.scrollController.position.minScrollExtent) { - _userHasScrolled = false; - } - - _listKey.currentState!.insertItem( - 0, - duration: widget.insertAnimationDuration, - ); - - // Used later to trigger scroll to end only for the last inserted message. - _lastInsertedMessageId = data.id; - - if (position == _oldList.length) { - _scrollToEnd(data); - } - } - - void _onRemoved(final int position, final Message data) { - final visualPosition = max(_oldList.length - position - 1, 0); - _listKey.currentState!.removeItem( - visualPosition, - (context, animation) => widget.itemBuilder( - context, - animation, - data, - isRemoved: true, - ), - duration: widget.removeAnimationDuration, - ); - } - - void _onChanged(int position, Message oldData, Message newData) { - _onRemoved(position, oldData); - _listKey.currentState!.insertItem( - max(_oldList.length - position - 1, 0), - duration: widget.insertAnimationDuration, - ); - } - - void _onDiffUpdate(diffutil.DataDiffUpdate update) { - update.when( - insert: (pos, data) => _onInserted(max(_oldList.length - pos, 0), data), - remove: (pos, data) => _onRemoved(pos, data), - change: (pos, oldData, newData) => _onChanged(pos, oldData, newData), - move: (_, __, ___) => throw UnimplementedError('unused'), - ); - } - - void _handleLoadPreviousMessages() { - if (widget.scrollController.offset >= - widget.scrollController.position.maxScrollExtent) { - widget.onLoadPreviousMessages?.call(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart index 59b7fbd39b..7f2e01f989 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_avatar.dart @@ -4,32 +4,44 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/util/built_in_svgs.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:string_validator/string_validator.dart'; -import 'layout_define.dart'; +class ChatChatUserAvatar extends StatelessWidget { + const ChatChatUserAvatar({required this.userId, super.key}); -class ChatAIAvatar extends StatelessWidget { - const ChatAIAvatar({super.key}); + final String userId; @override Widget build(BuildContext context) { - return Container( - width: DesktopAIChatSizes.avatarSize, - height: DesktopAIChatSizes.avatarSize, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(shape: BoxShape.circle), - foregroundDecoration: ShapeDecoration( - shape: CircleBorder( - side: BorderSide(color: Theme.of(context).colorScheme.outline), - ), - ), - child: const CircleAvatar( - backgroundColor: Colors.transparent, - child: FlowySvg( - FlowySvgs.app_logo_s, - size: Size.square(16), - blendMode: null, + return const ChatBorderedCircleAvatar(); + } +} + +class ChatBorderedCircleAvatar extends StatelessWidget { + const ChatBorderedCircleAvatar({ + super.key, + this.border = const BorderSide(), + this.backgroundImage, + this.child, + }); + + final BorderSide border; + final ImageProvider? backgroundImage; + final Widget? child; + + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: border.color, + child: ConstrainedBox( + constraints: const BoxConstraints.expand(), + child: CircleAvatar( + backgroundImage: backgroundImage, + backgroundColor: + Theme.of(context).colorScheme.surfaceContainerHighest, + child: child, ), ), ); @@ -41,40 +53,30 @@ class ChatUserAvatar extends StatelessWidget { super.key, required this.iconUrl, required this.name, - this.defaultName, + required this.size, + this.isHovering = false, }); final String iconUrl; final String name; - final String? defaultName; + final double size; + + // If true, a border will be applied on top of the avatar + final bool isHovering; @override Widget build(BuildContext context) { - late final Widget child; if (iconUrl.isEmpty) { - child = _buildEmptyAvatar(context); + return _buildEmptyAvatar(context); } else if (isURL(iconUrl)) { - child = _buildUrlAvatar(context); + return _buildUrlAvatar(context); } else { - child = _buildEmojiAvatar(context); + return _buildEmojiAvatar(context); } - return Container( - width: DesktopAIChatSizes.avatarSize, - height: DesktopAIChatSizes.avatarSize, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(shape: BoxShape.circle), - foregroundDecoration: ShapeDecoration( - shape: CircleBorder( - side: BorderSide(color: Theme.of(context).colorScheme.outline), - ), - ), - child: child, - ); } Widget _buildEmptyAvatar(BuildContext context) { - final String nameOrDefault = _userName(name, defaultName); - + final String nameOrDefault = _userName(name); final Color color = ColorGenerator(name).toColor(); const initialsCount = 2; @@ -86,50 +88,96 @@ class ChatUserAvatar extends StatelessWidget { .map((element) => element[0].toUpperCase()) .join(); - return ColoredBox( - color: color, - child: Center( - child: FlowyText.regular( - nameInitials, - color: Colors.black, - ), + return Container( + width: size, + height: size, + alignment: Alignment.center, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: _darken(color), + width: 4, + ) + : null, + ), + child: FlowyText.regular( + nameInitials, + color: Colors.black, ), ); } Widget _buildUrlAvatar(BuildContext context) { - return CircleAvatar( - backgroundColor: Colors.transparent, - radius: DesktopAIChatSizes.avatarSize / 2, - child: Image.network( - iconUrl, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _buildEmptyAvatar(context), + return SizedBox.square( + dimension: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.secondary, + width: 4, + ) + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), + ), + ), + ), ), ); } Widget _buildEmojiAvatar(BuildContext context) { - return CircleAvatar( - backgroundColor: Colors.transparent, - radius: DesktopAIChatSizes.avatarSize / 2, - child: builtInSVGIcons.contains(iconUrl) - ? FlowySvg( - FlowySvgData('emoji/$iconUrl'), - blendMode: null, - ) - : FlowyText.emoji( - iconUrl, - fontSize: 24, // cannot reduce - optimizeEmojiAlign: true, - ), + return SizedBox.square( + dimension: size, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: isHovering + ? Border.all( + color: Theme.of(context).colorScheme.primary, + width: 4, + ) + : null, + ), + child: ClipRRect( + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: builtInSVGIcons.contains(iconUrl) + ? FlowySvg( + FlowySvgData('emoji/$iconUrl'), + blendMode: null, + ) + : FlowyText.emoji(iconUrl), + ), + ), + ), ); } - /// Return the user name. + /// Return the user name, if the user name is empty, + /// return the default user name. /// - /// If the user name is empty, return the default user name. - String _userName(String name, String? defaultName) => - name.isEmpty ? (defaultName ?? LocaleKeys.defaultUsername.tr()) : name; + String _userName(String name) => + name.isEmpty ? LocaleKeys.defaultUsername.tr() : name; + + /// Used to darken the generated color for the hover border effect. + /// The color is darkened by 15% - Hence the 0.15 value. + /// + Color _darken(Color color) { + final hsl = HSLColor.fromColor(color); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart deleted file mode 100644 index b79e3c52c3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_editor_style.dart +++ /dev/null @@ -1,151 +0,0 @@ -// ref appflowy_flutter/lib/plugins/document/presentation/editor_style.dart - -// diff: -// - text style -// - heading text style and padding builders -// - don't listen to document appearance cubit -// - -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/workspace/application/appearance_defaults.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:collection/collection.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class ChatEditorStyleCustomizer extends EditorStyleCustomizer { - ChatEditorStyleCustomizer({ - required super.context, - required super.padding, - super.width, - }); - - @override - EditorStyle desktop() { - final theme = Theme.of(context); - final afThemeExtension = AFThemeExtension.of(context); - final appearanceFont = context.read().state.font; - final appearance = context.read().state; - const fontSize = 14.0; - String fontFamily = appearance.fontFamily; - if (fontFamily.isEmpty && appearanceFont.isNotEmpty) { - fontFamily = appearanceFont; - } - - return EditorStyle.desktop( - padding: padding, - maxWidth: width, - cursorColor: appearance.cursorColor ?? - DefaultAppearanceSettings.getDefaultCursorColor(context), - selectionColor: appearance.selectionColor ?? - DefaultAppearanceSettings.getDefaultSelectionColor(context), - defaultTextDirection: appearance.defaultTextDirection, - textStyleConfiguration: TextStyleConfiguration( - lineHeight: 20 / 14, - applyHeightToFirstAscent: true, - applyHeightToLastDescent: true, - text: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize, - color: afThemeExtension.onBackground, - ), - bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith( - fontWeight: FontWeight.w600, - ), - italic: baseTextStyle(fontFamily).copyWith(fontStyle: FontStyle.italic), - underline: baseTextStyle(fontFamily).copyWith( - decoration: TextDecoration.underline, - ), - strikethrough: baseTextStyle(fontFamily).copyWith( - decoration: TextDecoration.lineThrough, - ), - href: baseTextStyle(fontFamily).copyWith( - color: theme.colorScheme.primary, - decoration: TextDecoration.underline, - ), - code: GoogleFonts.robotoMono( - textStyle: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize, - fontWeight: FontWeight.normal, - color: Colors.red, - backgroundColor: - theme.colorScheme.inverseSurface.withValues(alpha: 0.8), - ), - ), - ), - textSpanDecorator: customizeAttributeDecorator, - textScaleFactor: - context.watch().state.textScaleFactor, - ); - } - - @override - TextStyle headingStyleBuilder(int level) { - final String? fontFamily; - final List fontSizes; - const fontSize = 14.0; - - fontFamily = context.read().state.fontFamily; - fontSizes = [ - fontSize + 12, - fontSize + 10, - fontSize + 6, - fontSize + 2, - fontSize, - ]; - return baseTextStyle(fontFamily, fontWeight: FontWeight.w600).copyWith( - fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize, - ); - } - - @override - CodeBlockStyle codeBlockStyleBuilder() { - final fontFamily = - context.read().state.codeFontFamily; - - return CodeBlockStyle( - textStyle: baseTextStyle(fontFamily).copyWith( - height: 1.4, - color: AFThemeExtension.of(context).onBackground, - ), - backgroundColor: AFThemeExtension.of(context).calloutBGColor, - foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155), - wrapLines: true, - ); - } - - @override - TextStyle calloutBlockStyleBuilder() { - if (UniversalPlatform.isMobile) { - final afThemeExtension = AFThemeExtension.of(context); - final pageStyle = context.read().state; - final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; - final baseTextStyle = this.baseTextStyle(fontFamily); - return baseTextStyle.copyWith( - color: afThemeExtension.onBackground, - ); - } else { - final fontSize = context.read().state.fontSize; - return baseTextStyle(null).copyWith( - fontSize: fontSize, - height: 1.5, - ); - } - } - - @override - TextStyle outlineBlockPlaceholderStyleBuilder() { - return TextStyle( - fontFamily: defaultFontFamily, - height: 1.5, - color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart new file mode 100644 index 0000000000..93bf80b4a5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input.dart @@ -0,0 +1,303 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart' as types; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; + +class ChatInput extends StatefulWidget { + /// Creates [ChatInput] widget. + const ChatInput({ + super.key, + this.isAttachmentUploading, + this.onAttachmentPressed, + required this.onSendPressed, + required this.chatId, + this.options = const InputOptions(), + required this.isStreaming, + required this.onStopStreaming, + }); + + final bool? isAttachmentUploading; + final VoidCallback? onAttachmentPressed; + final void Function(types.PartialText) onSendPressed; + final void Function() onStopStreaming; + final InputOptions options; + final String chatId; + final bool isStreaming; + + @override + State createState() => _ChatInputState(); +} + +/// [ChatInput] widget state. +class _ChatInputState extends State { + late final _inputFocusNode = FocusNode( + onKeyEvent: (node, event) { + if (event.physicalKey == PhysicalKeyboardKey.enter && + !HardwareKeyboard.instance.physicalKeysPressed.any( + (el) => { + PhysicalKeyboardKey.shiftLeft, + PhysicalKeyboardKey.shiftRight, + }.contains(el), + )) { + if (kIsWeb && _textController.value.isComposingRangeValid) { + return KeyEventResult.ignored; + } + if (event is KeyDownEvent) { + _handleSendPressed(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + bool _sendButtonVisible = false; + late TextEditingController _textController; + + @override + void initState() { + super.initState(); + + _textController = + widget.options.textEditingController ?? InputTextFieldController(); + _handleSendButtonVisibilityModeChange(); + } + + void _handleSendButtonVisibilityModeChange() { + _textController.removeListener(_handleTextControllerChange); + _sendButtonVisible = + _textController.text.trim() != '' || widget.isStreaming; + _textController.addListener(_handleTextControllerChange); + } + + void _handleSendPressed() { + if (widget.isStreaming) { + widget.onStopStreaming(); + } else { + final trimmedText = _textController.text.trim(); + if (trimmedText != '') { + final partialText = types.PartialText(text: trimmedText); + widget.onSendPressed(partialText); + + if (widget.options.inputClearMode == InputClearMode.always) { + _textController.clear(); + } + } + } + } + + void _handleTextControllerChange() { + if (_textController.value.isComposingRangeValid) { + return; + } + setState(() { + _sendButtonVisible = _textController.text.trim() != ''; + }); + } + + Widget _inputBuilder() { + const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6); + const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6); + const inputPadding = EdgeInsets.all(6); + + return Focus( + autofocus: !widget.options.autofocus, + child: Padding( + padding: inputPadding, + child: Material( + borderRadius: BorderRadius.circular(30), + color: isMobile + ? Theme.of(context).colorScheme.surfaceContainer + : Theme.of(context).colorScheme.surfaceContainerHighest, + elevation: 0.6, + child: Row( + children: [ + if (widget.onAttachmentPressed != null) + AttachmentButton( + isLoading: widget.isAttachmentUploading ?? false, + onPressed: widget.onAttachmentPressed, + padding: buttonPadding, + ), + Expanded(child: _inputTextField(textPadding)), + _sendButton(buttonPadding), + ], + ), + ), + ), + ); + } + + Padding _inputTextField(EdgeInsets textPadding) { + return Padding( + padding: textPadding, + child: TextField( + controller: _textController, + readOnly: widget.isStreaming, + focusNode: _inputFocusNode, + decoration: InputDecoration( + border: InputBorder.none, + hintText: LocaleKeys.chat_inputMessageHint.tr(), + hintStyle: TextStyle( + color: AFThemeExtension.of(context).textColor.withOpacity(0.5), + ), + ), + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + ), + enabled: widget.options.enabled, + autocorrect: widget.options.autocorrect, + autofocus: widget.options.autofocus, + enableSuggestions: widget.options.enableSuggestions, + keyboardType: widget.options.keyboardType, + textCapitalization: TextCapitalization.sentences, + maxLines: 10, + minLines: 1, + onChanged: widget.options.onTextChanged, + onTap: widget.options.onTextFieldTap, + ), + ); + } + + ConstrainedBox _sendButton(EdgeInsets buttonPadding) { + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: buttonPadding.bottom + buttonPadding.top + 24, + ), + child: Visibility( + visible: _sendButtonVisible, + child: Padding( + padding: buttonPadding, + child: AccessoryButton( + onSendPressed: () { + _handleSendPressed(); + }, + onStopStreaming: () { + widget.onStopStreaming(); + }, + isStreaming: widget.isStreaming, + ), + ), + ), + ); + } + + @override + void didUpdateWidget(covariant ChatInput oldWidget) { + super.didUpdateWidget(oldWidget); + _handleSendButtonVisibilityModeChange(); + } + + @override + void dispose() { + _inputFocusNode.dispose(); + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => GestureDetector( + onTap: () => _inputFocusNode.requestFocus(), + child: _inputBuilder(), + ); +} + +@immutable +class InputOptions { + const InputOptions({ + this.inputClearMode = InputClearMode.always, + this.keyboardType = TextInputType.multiline, + this.onTextChanged, + this.onTextFieldTap, + this.textEditingController, + this.autocorrect = true, + this.autofocus = false, + this.enableSuggestions = true, + this.enabled = true, + }); + + /// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always]. + final InputClearMode inputClearMode; + + /// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline]. + final TextInputType keyboardType; + + /// Will be called whenever the text inside [TextField] changes. + final void Function(String)? onTextChanged; + + /// Will be called on [TextField] tap. + final VoidCallback? onTextFieldTap; + + /// Custom [TextEditingController]. If not provided, defaults to the + /// [InputTextFieldController], which extends [TextEditingController] and has + /// additional fatures like markdown support. If you want to keep additional + /// features but still need some methods from the default [TextEditingController], + /// you can create your own [InputTextFieldController] (imported from this lib) + /// and pass it here. + final TextEditingController? textEditingController; + + /// Controls the [TextInput] autocorrect behavior. Defaults to [true]. + final bool autocorrect; + + /// Whether [TextInput] should have focus. Defaults to [false]. + final bool autofocus; + + /// Controls the [TextInput] enableSuggestions behavior. Defaults to [true]. + final bool enableSuggestions; + + /// Controls the [TextInput] enabled behavior. Defaults to [true]. + final bool enabled; +} + +final isMobile = defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + +class AccessoryButton extends StatelessWidget { + const AccessoryButton({ + required this.onSendPressed, + required this.onStopStreaming, + required this.isStreaming, + super.key, + }); + + final void Function() onSendPressed; + final void Function() onStopStreaming; + final bool isStreaming; + + @override + Widget build(BuildContext context) { + if (isStreaming) { + return FlowyIconButton( + width: 36, + icon: FlowySvg( + FlowySvgs.ai_stream_stop_s, + size: const Size.square(28), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: onStopStreaming, + radius: BorderRadius.circular(18), + fillColor: AFThemeExtension.of(context).lightGreyHover, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + ); + } else { + return FlowyIconButton( + width: 36, + fillColor: AFThemeExtension.of(context).lightGreyHover, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + radius: BorderRadius.circular(18), + icon: FlowySvg( + FlowySvgs.send_s, + size: const Size.square(24), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: onSendPressed, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart deleted file mode 100644 index 76d1af7134..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input/mobile_chat_input.dart +++ /dev/null @@ -1,369 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:extended_text_field/extended_text_field.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MobileChatInput extends StatefulWidget { - const MobileChatInput({ - super.key, - required this.isStreaming, - required this.onStopStreaming, - required this.onSubmitted, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - }); - - final bool isStreaming; - final void Function() onStopStreaming; - final ValueNotifier> selectedSourcesNotifier; - final void Function(String, PredefinedFormat?, Map) - onSubmitted; - final void Function(List) onUpdateSelectedSources; - - @override - State createState() => _MobileChatInputState(); -} - -class _MobileChatInputState extends State { - final inputControlCubit = ChatInputControlCubit(); - final focusNode = FocusNode(); - final textController = TextEditingController(); - - late SendButtonState sendButtonState; - - @override - void initState() { - super.initState(); - - textController.addListener(handleTextControllerChanged); - // focusNode.onKeyEvent = handleKeyEvent; - - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - }); - - updateSendButtonState(); - } - - @override - void didUpdateWidget(covariant oldWidget) { - updateSendButtonState(); - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - focusNode.dispose(); - textController.dispose(); - inputControlCubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Hero( - tag: "ai_chat_prompt", - child: BlocProvider.value( - value: inputControlCubit, - child: BlocListener( - listener: (context, state) { - state.maybeWhen( - updateSelectedViews: (selectedViews) { - context.read().add( - AIPromptInputEvent.updateMentionedViews(selectedViews), - ); - }, - orElse: () {}, - ); - }, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - top: BorderSide(color: Theme.of(context).colorScheme.outline), - ), - color: Theme.of(context).colorScheme.surface, - boxShadow: const [ - BoxShadow( - blurRadius: 4.0, - offset: Offset(0, -2), - color: Color.fromRGBO(0, 0, 0, 0.05), - ), - ], - borderRadius: - const BorderRadius.vertical(top: Radius.circular(8.0)), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MobileAIPromptSizes - .attachedFilesBarPadding.vertical + - MobileAIPromptSizes.attachedFilesPreviewHeight, - ), - child: PromptInputFile( - onDeleted: (file) => context - .read() - .add(AIPromptInputEvent.removeFile(file)), - ), - ), - if (state.showPredefinedFormats) - TextFieldTapRegion( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ChangeFormatBar( - predefinedFormat: state.predefinedFormat, - spacing: 8.0, - onSelectPredefinedFormat: (format) => - context.read().add( - AIPromptInputEvent.updatePredefinedFormat( - format, - ), - ), - ), - ), - ) - else - const VSpace(8.0), - inputTextField(context), - TextFieldTapRegion( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const HSpace(8.0), - leadingButtons( - context, - state.showPredefinedFormats, - ), - const Spacer(), - sendButton(), - const HSpace(12.0), - ], - ), - ), - ), - ], - ); - }, - ), - ), - ), - ), - ); - } - - void updateSendButtonState() { - if (widget.isStreaming) { - sendButtonState = SendButtonState.streaming; - } else if (textController.text.trim().isEmpty) { - sendButtonState = SendButtonState.disabled; - } else { - sendButtonState = SendButtonState.enabled; - } - } - - void handleSendPressed() { - if (widget.isStreaming) { - return; - } - final trimmedText = inputControlCubit.formatIntputText( - textController.text.trim(), - ); - textController.clear(); - if (trimmedText.isEmpty) { - return; - } - - // get the attached files and mentioned pages - final metadata = context.read().consumeMetadata(); - - final bloc = context.read(); - final showPredefinedFormats = bloc.state.showPredefinedFormats; - final predefinedFormat = bloc.state.predefinedFormat; - - widget.onSubmitted( - trimmedText, - showPredefinedFormats ? predefinedFormat : null, - metadata, - ); - } - - void handleTextControllerChanged() { - if (textController.value.isComposingRangeValid) { - return; - } - // inputControlCubit.updateInputText(textController.text); - setState(() => updateSendButtonState()); - } - - // KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - // if (event.character == '@') { - // WidgetsBinding.instance.addPostFrameCallback((_) { - // mentionPage(context); - // }); - // } - // return KeyEventResult.ignored; - // } - - Future mentionPage(BuildContext context) async { - // if the focus node is on focus, unfocus it for better animation - // otherwise, the page sheet animation will be blocked by the keyboard - inputControlCubit.refreshViews(); - inputControlCubit.startSearching(textController.value); - if (focusNode.hasFocus) { - focusNode.unfocus(); - await Future.delayed(const Duration(milliseconds: 100)); - } - - if (context.mounted) { - final selectedView = await showPageSelectorSheet( - context, - filter: (view) => - !view.isSpace && - view.layout.isDocumentView && - view.parentViewId != view.id && - !inputControlCubit.selectedViewIds.contains(view.id), - ); - if (selectedView != null) { - final newText = textController.text.replaceRange( - inputControlCubit.filterStartPosition, - inputControlCubit.filterStartPosition, - selectedView.id, - ); - textController.value = TextEditingValue( - text: newText, - selection: TextSelection.collapsed( - offset: - textController.selection.baseOffset + selectedView.id.length, - affinity: TextAffinity.upstream, - ), - ); - - inputControlCubit.selectPage(selectedView); - } - focusNode.requestFocus(); - inputControlCubit.reset(); - } - } - - Widget inputTextField(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return ExtendedTextField( - controller: textController, - focusNode: focusNode, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: MobileAIPromptSizes.textFieldContentPadding, - hintText: switch (state.aiType) { - AiType.cloud => LocaleKeys.chat_inputMessageHint.tr(), - AiType.local => LocaleKeys.chat_inputLocalAIMessageHint.tr() - }, - hintStyle: inputHintTextStyle(context), - isCollapsed: true, - isDense: true, - ), - keyboardType: TextInputType.multiline, - textCapitalization: TextCapitalization.sentences, - minLines: 1, - maxLines: null, - style: - Theme.of(context).textTheme.bodyMedium?.copyWith(height: 20 / 14), - specialTextSpanBuilder: PromptInputTextSpanBuilder( - inputControlCubit: inputControlCubit, - specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.w600, - ), - ), - onTapOutside: (_) => focusNode.unfocus(), - ); - }, - ); - } - - TextStyle? inputHintTextStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).isLightMode - ? const Color(0xFFBDC2C8) - : const Color(0xFF3C3E51), - ); - } - - Widget leadingButtons(BuildContext context, bool showPredefinedFormats) { - return _LeadingActions( - // onMention: () { - // textController.text += '@'; - // if (!focusNode.hasFocus) { - // focusNode.requestFocus(); - // } - // WidgetsBinding.instance.addPostFrameCallback((_) { - // mentionPage(context); - // }); - // }, - showPredefinedFormats: showPredefinedFormats, - onTogglePredefinedFormatSection: () { - context - .read() - .add(AIPromptInputEvent.toggleShowPredefinedFormat()); - }, - selectedSourcesNotifier: widget.selectedSourcesNotifier, - onUpdateSelectedSources: widget.onUpdateSelectedSources, - ); - } - - Widget sendButton() { - return PromptInputSendButton( - state: sendButtonState, - onSendPressed: handleSendPressed, - onStopStreaming: widget.onStopStreaming, - ); - } -} - -class _LeadingActions extends StatelessWidget { - const _LeadingActions({ - required this.showPredefinedFormats, - required this.onTogglePredefinedFormatSection, - required this.selectedSourcesNotifier, - required this.onUpdateSelectedSources, - }); - - final bool showPredefinedFormats; - final void Function() onTogglePredefinedFormatSection; - final ValueNotifier> selectedSourcesNotifier; - final void Function(List) onUpdateSelectedSources; - - @override - Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.surface, - child: SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(4.0), - children: [ - PromptInputMobileSelectSourcesButton( - selectedSourcesNotifier: selectedSourcesNotifier, - onUpdateSelectedSources: onUpdateSelectedSources, - ), - PromptInputMobileToggleFormatButton( - showFormatBar: showPredefinedFormats, - onTap: onTogglePredefinedFormatSection, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_loading.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_loading.dart new file mode 100644 index 0000000000..9c4bd64cb6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_loading.dart @@ -0,0 +1,69 @@ +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class ChatAILoading extends StatelessWidget { + const ChatAILoading({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: AFThemeExtension.of(context).lightGreyHover, + highlightColor: + AFThemeExtension.of(context).lightGreyHover.withOpacity(0.5), + period: const Duration(seconds: 3), + child: const ContentPlaceholder(), + ); + } +} + +class ContentPlaceholder extends StatelessWidget { + const ContentPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 30, + height: 16.0, + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4.0), + ), + ), + const HSpace(10), + Container( + width: 100, + height: 16.0, + margin: const EdgeInsets.only(bottom: 8.0), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4.0), + ), + ), + ], + ), + // Container( + // width: 140, + // height: 16.0, + // margin: const EdgeInsets.only(bottom: 8.0), + // decoration: BoxDecoration( + // color: AFThemeExtension.of(context).lightGreyHover, + // borderRadius: BorderRadius.circular(4.0), + // ), + // ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart deleted file mode 100644 index 790a3fac3c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_message_selector_banner.dart +++ /dev/null @@ -1,314 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/prelude.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/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; - -import 'message/ai_message_action_bar.dart'; -import 'message/message_util.dart'; - -class ChatMessageSelectorBanner extends StatelessWidget { - const ChatMessageSelectorBanner({ - super.key, - required this.view, - this.allMessages = const [], - }); - - final ViewPB view; - final List allMessages; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (!state.isSelectingMessages) { - return const SizedBox.shrink(); - } - - final selectedAmount = state.selectedMessages.length; - final totalAmount = allMessages.length; - final allSelected = selectedAmount == totalAmount; - - return Container( - height: 48, - color: const Color(0xFF00BCF0), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - GestureDetector( - onTap: () { - if (selectedAmount > 0) { - _unselectAllMessages(context); - } else { - _selectAllMessages(context); - } - }, - child: FlowySvg( - allSelected - ? FlowySvgs.checkbox_ai_selected_s - : selectedAmount > 0 - ? FlowySvgs.checkbox_ai_minus_s - : FlowySvgs.checkbox_ai_empty_s, - blendMode: BlendMode.dstIn, - size: const Size.square(18), - ), - ), - const HSpace(8), - Expanded( - child: FlowyText.semibold( - allSelected - ? LocaleKeys.chat_selectBanner_allSelected.tr() - : selectedAmount > 0 - ? LocaleKeys.chat_selectBanner_nSelected - .tr(args: [selectedAmount.toString()]) - : LocaleKeys.chat_selectBanner_selectMessages.tr(), - figmaLineHeight: 16, - color: Colors.white, - ), - ), - SaveToPageButton( - view: view, - ), - const HSpace(8), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => context.read().add( - const ChatSelectMessageEvent.toggleSelectingMessages(), - ), - child: const FlowySvg( - FlowySvgs.close_m, - color: Colors.white, - size: Size.square(24), - ), - ), - ), - ], - ), - ); - }, - ); - } - - void _selectAllMessages(BuildContext context) => context - .read() - .add(ChatSelectMessageEvent.selectAllMessages(allMessages)); - - void _unselectAllMessages(BuildContext context) => context - .read() - .add(const ChatSelectMessageEvent.unselectAllMessages()); -} - -class SaveToPageButton extends StatefulWidget { - const SaveToPageButton({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - State createState() => _SaveToPageButtonState(); -} - -class _SaveToPageButtonState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - final userWorkspaceBloc = context.read(); - final userProfile = userWorkspaceBloc.userProfile; - final workspaceId = - userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? ''; - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - ), - BlocProvider( - create: (context) => ChatSettingsCubit(hideDisabled: true), - ), - ], - child: BlocSelector( - selector: (state) => state.currentSpace, - builder: (context, spaceView) { - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - offset: const Offset(0, 18), - direction: PopoverDirection.bottomWithRightAligned, - constraints: const BoxConstraints.tightFor(width: 300, height: 400), - child: buildButton(context, spaceView), - popupBuilder: (_) => buildPopover(context), - ); - }, - ), - ); - } - - Widget buildButton(BuildContext context, ViewPB? spaceView) { - return BlocBuilder( - builder: (context, state) { - final selectedAmount = state.selectedMessages.length; - - return Opacity( - opacity: selectedAmount == 0 ? 0.5 : 1, - child: FlowyTextButton( - LocaleKeys.chat_selectBanner_saveButton.tr(), - onPressed: selectedAmount == 0 - ? null - : () async { - final documentId = getOpenedDocumentId(); - if (documentId != null) { - await onAddToExistingPage(context, documentId); - await forceReload(documentId); - await Future.delayed(const Duration(milliseconds: 500)); - await updateSelection(documentId); - } else { - if (spaceView != null) { - context - .read() - .refreshSources([spaceView], spaceView); - } - popoverController.show(); - } - }, - fontColor: Colors.white, - borderColor: Colors.white, - fillColor: Colors.transparent, - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 6.0, - ), - ), - ); - }, - ); - } - - Widget buildPopover(BuildContext context) { - return BlocProvider.value( - value: context.read(), - child: SaveToPagePopoverContent( - onAddToNewPage: (parentViewId) async { - await addMessageToNewPage(context, parentViewId); - popoverController.close(); - }, - onAddToExistingPage: (documentId) async { - final view = await onAddToExistingPage(context, documentId); - - if (context.mounted) { - openPageFromMessage(context, view); - } - await Future.delayed(const Duration(milliseconds: 500)); - await updateSelection(documentId); - popoverController.close(); - }, - ), - ); - } - - Future onAddToExistingPage( - BuildContext context, - String documentId, - ) async { - final bloc = context.read(); - - final selectedMessages = [ - ...bloc.state.selectedMessages.whereType(), - ]..sort((a, b) => a.createdAt.compareTo(b.createdAt)); - - await ChatEditDocumentService.addMessagesToPage( - documentId, - selectedMessages, - ); - await Future.delayed(const Duration(milliseconds: 500)); - final view = await ViewBackendService.getView(documentId).toNullable(); - if (context.mounted) { - showSaveMessageSuccessToast(context, view); - } - - bloc.add(const ChatSelectMessageEvent.reset()); - - return view; - } - - Future addMessageToNewPage( - BuildContext context, - String parentViewId, - ) async { - final bloc = context.read(); - - final selectedMessages = [ - ...bloc.state.selectedMessages.whereType(), - ]..sort((a, b) => a.createdAt.compareTo(b.createdAt)); - - final newView = await ChatEditDocumentService.saveMessagesToNewPage( - widget.view.nameOrDefault, - parentViewId, - selectedMessages, - ); - - if (context.mounted) { - showSaveMessageSuccessToast(context, newView); - openPageFromMessage(context, newView); - } - bloc.add(const ChatSelectMessageEvent.reset()); - } - - Future forceReload(String documentId) async { - final bloc = DocumentBloc.findOpen(documentId); - if (bloc == null) { - return; - } - await bloc.forceReloadDocumentState(); - } - - Future updateSelection(String documentId) async { - final bloc = DocumentBloc.findOpen(documentId); - if (bloc == null) { - return; - } - await bloc.forceReloadDocumentState(); - final editorState = bloc.state.editorState; - final lastNodePath = editorState?.getLastSelectable()?.$1.path; - if (editorState == null || lastNodePath == null) { - return; - } - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: lastNodePath)), - ), - ); - } - - String? getOpenedDocumentId() { - final pageManager = getIt().state.currentPageManager; - if (!pageManager.showSecondaryPluginNotifier.value) { - return null; - } - return pageManager.secondaryNotifier.plugin.id; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_popmenu.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_popmenu.dart new file mode 100644 index 0000000000..6b0b50dcca --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_popmenu.dart @@ -0,0 +1,70 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; + +import 'package:flutter/material.dart'; + +class ChatPopupMenu extends StatefulWidget { + const ChatPopupMenu({ + super.key, + required this.onAction, + required this.builder, + }); + + final Function(ChatMessageAction) onAction; + final Widget Function(BuildContext context) builder; + + @override + State createState() => _ChatPopupMenuState(); +} + +class _ChatPopupMenuState extends State { + @override + Widget build(BuildContext context) { + return PopoverActionList( + asBarrier: true, + actions: ChatMessageAction.values + .map((action) => ChatMessageActionWrapper(action)) + .toList(), + buildChild: (controller) { + return GestureDetector( + onLongPress: () { + controller.show(); + }, + child: widget.builder(context), + ); + }, + onSelected: (action, controller) async { + widget.onAction(action.inner); + controller.close(); + }, + direction: PopoverDirection.bottomWithCenterAligned, + ); + } +} + +enum ChatMessageAction { + copy, +} + +class ChatMessageActionWrapper extends ActionCell { + ChatMessageActionWrapper(this.inner); + + final ChatMessageAction inner; + + @override + Widget? leftIcon(Color iconColor) => null; + + @override + String get name => inner.name; +} + +extension ChatMessageActionExtension on ChatMessageAction { + String get name { + switch (this) { + case ChatMessageAction.copy: + return LocaleKeys.document_plugins_contextMenu_copy.tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart index 2c09e77050..a37a0824ed 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_related_question.dart @@ -1,91 +1,112 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'layout_define.dart'; class RelatedQuestionList extends StatelessWidget { const RelatedQuestionList({ - super.key, + required this.chatId, required this.onQuestionSelected, required this.relatedQuestions, + super.key, }); - final void Function(String) onQuestionSelected; - final List relatedQuestions; + final String chatId; + final Function(String) onQuestionSelected; + final List relatedQuestions; @override Widget build(BuildContext context) { - return SelectionContainer.disabled( - child: ListView.separated( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: relatedQuestions.length + 1, - padding: - const EdgeInsets.only(bottom: 8.0) + AIChatUILayout.messageMargin, - separatorBuilder: (context, index) => const VSpace(4.0), - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText( - LocaleKeys.chat_relatedQuestion.tr(), - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w600, + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: relatedQuestions.length, + itemBuilder: (context, index) { + final question = relatedQuestions[index]; + if (index == 0) { + return Column( + children: [ + const Divider(height: 36), + Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + FlowySvg( + FlowySvgs.ai_summary_generate_s, + size: const Size.square(24), + color: Theme.of(context).colorScheme.primary, + ), + const HSpace(6), + FlowyText( + LocaleKeys.chat_relatedQuestion.tr(), + fontSize: 18, + ), + ], + ), ), - ); - } else { - return Align( - alignment: AlignmentDirectional.centerStart, - child: RelatedQuestionItem( - question: relatedQuestions[index - 1], + const Divider(height: 6), + RelatedQuestionItem( + question: question, onQuestionSelected: onQuestionSelected, ), - ); - } - }, - ), + ], + ); + } else { + return RelatedQuestionItem( + question: question, + onQuestionSelected: onQuestionSelected, + ); + } + }, ); } } -class RelatedQuestionItem extends StatelessWidget { +class RelatedQuestionItem extends StatefulWidget { const RelatedQuestionItem({ required this.question, required this.onQuestionSelected, super.key, }); - final String question; + final RelatedQuestionPB question; final Function(String) onQuestionSelected; + @override + State createState() => _RelatedQuestionItemState(); +} + +class _RelatedQuestionItemState extends State { + bool _isHovered = false; + @override Widget build(BuildContext context) { - return FlowyButton( - mainAxisAlignment: MainAxisAlignment.start, - text: Flexible( - child: FlowyText( - question, - lineHeight: 1.4, - maxLines: 2, - overflow: TextOverflow.ellipsis, + return MouseRegion( + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + ), + title: Text( + widget.question.content, + style: TextStyle( + color: _isHovered ? Theme.of(context).colorScheme.primary : null, + fontSize: 14, + ), + ), + onTap: () { + widget.onQuestionSelected(widget.question.content); + }, + trailing: FlowySvg( + FlowySvgs.add_m, + color: Theme.of(context).colorScheme.primary, ), ), - expandText: false, - margin: UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0) - : const EdgeInsets.all(8.0), - leftIcon: FlowySvg( - FlowySvgs.ai_chat_outlined_s, - color: Theme.of(context).colorScheme.primary, - size: const Size.square(16.0), - ), - onTap: () => onQuestionSelected(question), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_stream_text_field.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_stream_text_field.dart new file mode 100644 index 0000000000..7589a1b634 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_stream_text_field.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; + +class StreamTextField extends StatelessWidget { + const StreamTextField({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_streaming_error_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_streaming_error_message.dart new file mode 100644 index 0000000000..1f825d3355 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_streaming_error_message.dart @@ -0,0 +1,83 @@ +// import 'package:appflowy/generated/locale_keys.g.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_chat_types/flutter_chat_types.dart'; + +// class ChatStreamingError extends StatelessWidget { +// const ChatStreamingError({ +// required this.message, +// required this.onRetryPressed, +// super.key, +// }); + +// final void Function() onRetryPressed; +// final Message message; +// @override +// Widget build(BuildContext context) { +// final canRetry = message.metadata?[canRetryKey] != null; + +// if (canRetry) { +// return Column( +// children: [ +// const Divider(height: 4, thickness: 1), +// const VSpace(16), +// Center( +// child: Column( +// children: [ +// _aiUnvaliable(), +// const VSpace(10), +// _retryButton(), +// ], +// ), +// ), +// ], +// ); +// } else { +// return Center( +// child: Column( +// children: [ +// const Divider(height: 20, thickness: 1), +// Padding( +// padding: const EdgeInsets.all(8.0), +// child: FlowyText( +// LocaleKeys.chat_serverUnavailable.tr(), +// fontSize: 14, +// ), +// ), +// ], +// ), +// ); +// } +// } + +// FlowyButton _retryButton() { +// return FlowyButton( +// radius: BorderRadius.circular(20), +// useIntrinsicWidth: true, +// text: Padding( +// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), +// child: FlowyText( +// LocaleKeys.chat_regenerateAnswer.tr(), +// fontSize: 14, +// ), +// ), +// onTap: onRetryPressed, +// iconPadding: 0, +// leftIcon: const Icon( +// Icons.refresh, +// size: 20, +// ), +// ); +// } + +// Padding _aiUnvaliable() { +// return Padding( +// padding: const EdgeInsets.all(8.0), +// child: FlowyText( +// LocaleKeys.chat_aiServerUnavailable.tr(), +// fontSize: 14, +// ), +// ); +// } +// } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart new file mode 100644 index 0000000000..456ac0c184 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_theme.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; + +// For internal usage only. Use values from theme itself. + +/// See [ChatTheme.userAvatarNameColors]. +const colors = [ + Color(0xffff6767), + Color(0xff66e0da), + Color(0xfff5a2d9), + Color(0xfff0c722), + Color(0xff6a85e5), + Color(0xfffd9a6f), + Color(0xff92db6e), + Color(0xff73b8e5), + Color(0xfffd7590), + Color(0xffc78ae5), +]; + +/// Dark. +const dark = Color(0xff1f1c38); + +/// Error. +const error = Color(0xffff6767); + +/// N0. +const neutral0 = Color(0xff1d1c21); + +/// N1. +const neutral1 = Color(0xff615e6e); + +/// N2. +const neutral2 = Color(0xff9e9cab); + +/// N7. +const neutral7 = Color(0xffffffff); + +/// N7 with opacity. +const neutral7WithOpacity = Color(0x80ffffff); + +/// Primary. +const primary = Color(0xff6f61e8); + +/// Secondary. +const secondary = Color(0xfff5f5f7); + +/// Secondary dark. +const secondaryDark = Color(0xff2b2250); + +/// Default chat theme which extends [ChatTheme]. +@immutable +class AFDefaultChatTheme extends ChatTheme { + /// Creates a default chat theme. Use this constructor if you want to + /// override only a couple of properties, otherwise create a new class + /// which extends [ChatTheme]. + const AFDefaultChatTheme({ + super.attachmentButtonIcon, + super.attachmentButtonMargin, + super.backgroundColor = neutral7, + super.bubbleMargin, + super.dateDividerMargin = const EdgeInsets.only( + bottom: 32, + top: 16, + ), + super.dateDividerTextStyle = const TextStyle( + color: neutral2, + fontSize: 12, + fontWeight: FontWeight.w800, + height: 1.333, + ), + super.deliveredIcon, + super.documentIcon, + super.emptyChatPlaceholderTextStyle = const TextStyle( + color: neutral2, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + super.errorColor = error, + super.errorIcon, + super.inputBackgroundColor = neutral0, + super.inputSurfaceTintColor = neutral0, + super.inputElevation = 0, + super.inputBorderRadius = const BorderRadius.vertical( + top: Radius.circular(20), + ), + super.inputContainerDecoration, + super.inputMargin = EdgeInsets.zero, + super.inputPadding = const EdgeInsets.fromLTRB(14, 20, 14, 20), + super.inputTextColor = neutral7, + super.inputTextCursorColor, + super.inputTextDecoration = const InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + isCollapsed: true, + ), + super.inputTextStyle = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + super.messageBorderRadius = 20, + super.messageInsetsHorizontal = 0, + super.messageInsetsVertical = 0, + super.messageMaxWidth = 1000, + super.primaryColor = primary, + super.receivedEmojiMessageTextStyle = const TextStyle(fontSize: 40), + super.receivedMessageBodyBoldTextStyle, + super.receivedMessageBodyCodeTextStyle, + super.receivedMessageBodyLinkTextStyle, + super.receivedMessageBodyTextStyle = const TextStyle( + color: neutral0, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + super.receivedMessageCaptionTextStyle = const TextStyle( + color: neutral2, + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.333, + ), + super.receivedMessageDocumentIconColor = primary, + super.receivedMessageLinkDescriptionTextStyle = const TextStyle( + color: neutral0, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.428, + ), + super.receivedMessageLinkTitleTextStyle = const TextStyle( + color: neutral0, + fontSize: 16, + fontWeight: FontWeight.w800, + height: 1.375, + ), + super.secondaryColor = secondary, + super.seenIcon, + super.sendButtonIcon, + super.sendButtonMargin, + super.sendingIcon, + super.sentEmojiMessageTextStyle = const TextStyle(fontSize: 40), + super.sentMessageBodyBoldTextStyle, + super.sentMessageBodyCodeTextStyle, + super.sentMessageBodyLinkTextStyle, + super.sentMessageBodyTextStyle = const TextStyle( + color: neutral7, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + super.sentMessageCaptionTextStyle = const TextStyle( + color: neutral7WithOpacity, + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.333, + ), + super.sentMessageDocumentIconColor = neutral7, + super.sentMessageLinkDescriptionTextStyle = const TextStyle( + color: neutral7, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.428, + ), + super.sentMessageLinkTitleTextStyle = const TextStyle( + color: neutral7, + fontSize: 16, + fontWeight: FontWeight.w800, + height: 1.375, + ), + super.statusIconPadding = const EdgeInsets.symmetric(horizontal: 4), + super.systemMessageTheme = const SystemMessageTheme( + margin: EdgeInsets.only( + bottom: 24, + top: 8, + left: 8, + right: 8, + ), + textStyle: TextStyle( + color: neutral2, + fontSize: 12, + fontWeight: FontWeight.w800, + height: 1.333, + ), + ), + super.typingIndicatorTheme = const TypingIndicatorTheme( + animatedCirclesColor: neutral1, + animatedCircleSize: 5.0, + bubbleBorder: BorderRadius.all(Radius.circular(27.0)), + bubbleColor: neutral7, + countAvatarColor: primary, + countTextColor: secondary, + multipleUserTextStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: neutral2, + ), + ), + super.unreadHeaderTheme = const UnreadHeaderTheme( + color: secondary, + textStyle: TextStyle( + color: neutral2, + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.333, + ), + ), + super.userAvatarImageBackgroundColor = Colors.transparent, + super.userAvatarNameColors = colors, + super.userAvatarTextStyle = const TextStyle( + color: neutral7, + fontSize: 12, + fontWeight: FontWeight.w800, + height: 1.333, + ), + super.userNameTextStyle = const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w800, + height: 1.333, + ), + super.highlightMessageColor, + }); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_invalid_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_invalid_message.dart new file mode 100644 index 0000000000..8ae9b91d32 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_user_invalid_message.dart @@ -0,0 +1,31 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; + +class ChatInvalidUserMessage extends StatelessWidget { + const ChatInvalidUserMessage({ + required this.message, + super.key, + }); + + final Message message; + @override + Widget build(BuildContext context) { + final errorMessage = message.metadata?[sendMessageErrorKey] ?? ""; + return Center( + child: Column( + children: [ + const Divider(height: 20, thickness: 1), + Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyText( + errorMessage, + fontSize: 14, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index 30dc918f70..5aceca2025 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -1,109 +1,58 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; + +import 'chat_input.dart'; class ChatWelcomePage extends StatelessWidget { - const ChatWelcomePage({ - required this.userProfile, - required this.onSelectedQuestion, - super.key, - }); + ChatWelcomePage({required this.onSelectedQuestion, super.key}); final void Function(String) onSelectedQuestion; - final UserProfilePB userProfile; - static final List desktopItems = [ + final List items = [ LocaleKeys.chat_question1.tr(), LocaleKeys.chat_question2.tr(), LocaleKeys.chat_question3.tr(), LocaleKeys.chat_question4.tr(), ]; - - static final List> mobileItems = [ - [ - LocaleKeys.chat_question1.tr(), - LocaleKeys.chat_question2.tr(), - ], - [ - LocaleKeys.chat_question3.tr(), - LocaleKeys.chat_question4.tr(), - ], - [ - LocaleKeys.chat_question5.tr(), - LocaleKeys.chat_question6.tr(), - ], - ]; - @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.app_logo_xl, - size: Size.square(32), - blendMode: null, - ), - const VSpace(16), - FlowyText( - fontSize: 15, - LocaleKeys.chat_questionDetail.tr(args: [userProfile.name]), - ), - UniversalPlatform.isDesktop ? const VSpace(32 - 16) : const VSpace(24), - ...UniversalPlatform.isDesktop - ? buildDesktopSampleQuestions(context) - : buildMobileSampleQuestions(context), - ], - ); - } - - Iterable buildDesktopSampleQuestions(BuildContext context) { - return desktopItems.map( - (question) => Padding( - padding: const EdgeInsets.only(top: 16.0), - child: WelcomeSampleQuestion( - question: question, - onSelected: onSelectedQuestion, - ), + return AnimatedOpacity( + opacity: 1.0, + duration: const Duration(seconds: 3), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const FlowySvg( + FlowySvgs.flowy_ai_chat_logo_s, + size: Size.square(44), + ), + const SizedBox(height: 40), + GridView.builder( + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isMobile ? 2 : 4, + crossAxisSpacing: 6, + mainAxisSpacing: 6, + childAspectRatio: 16.0 / 9.0, + ), + itemCount: items.length, + itemBuilder: (context, index) => WelcomeQuestion( + question: items[index], + onSelected: onSelectedQuestion, + ), + ), + ], ), ); } - - Iterable buildMobileSampleQuestions(BuildContext context) { - return [ - _AutoScrollingSampleQuestions( - key: const ValueKey('inf_scroll_1'), - onSelected: onSelectedQuestion, - questions: mobileItems[0], - offset: 60.0, - ), - const VSpace(8), - _AutoScrollingSampleQuestions( - key: const ValueKey('inf_scroll_2'), - onSelected: onSelectedQuestion, - questions: mobileItems[1], - offset: -50.0, - reverse: true, - ), - const VSpace(8), - _AutoScrollingSampleQuestions( - key: const ValueKey('inf_scroll_3'), - onSelected: onSelectedQuestion, - questions: mobileItems[2], - offset: 120.0, - ), - ]; - } } -class WelcomeSampleQuestion extends StatelessWidget { - const WelcomeSampleQuestion({ +class WelcomeQuestion extends StatelessWidget { + const WelcomeQuestion({ required this.question, required this.onSelected, super.key, @@ -114,168 +63,30 @@ class WelcomeSampleQuestion extends StatelessWidget { @override Widget build(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - offset: const Offset(0, 1), - blurRadius: 2, - spreadRadius: -2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), + return InkWell( + onTap: () => onSelected(question), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: FlowyHover( + // Make the hover effect only available on mobile + isSelected: () => isMobile, + style: HoverStyle( + borderRadius: BorderRadius.circular(6), ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 4, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 8, - spreadRadius: 2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - ], - ), - child: TextButton( - onPressed: () => onSelected(question), - style: ButtonStyle( - padding: WidgetStatePropertyAll( - EdgeInsets.symmetric( - horizontal: 16, - vertical: UniversalPlatform.isDesktop ? 8 : 0, - ), - ), - backgroundColor: WidgetStateProperty.resolveWith((state) { - if (state.contains(WidgetState.hovered)) { - return isLightMode - ? const Color(0xFFF9FAFD) - : AFThemeExtension.of(context).lightGreyHover; - } - return Theme.of(context).colorScheme.surface; - }), - overlayColor: WidgetStateColor.transparent, - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: Theme.of(context).dividerColor), + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText( + question, + maxLines: null, + ), + ], ), ), ), - child: FlowyText( - question, - color: isLightMode - ? Theme.of(context).hintColor - : const Color(0xFF666D76), - ), ), ); } } - -class _AutoScrollingSampleQuestions extends StatefulWidget { - const _AutoScrollingSampleQuestions({ - super.key, - required this.questions, - this.offset = 0.0, - this.reverse = false, - required this.onSelected, - }); - - final List questions; - final void Function(String) onSelected; - final double offset; - final bool reverse; - - @override - State<_AutoScrollingSampleQuestions> createState() => - _AutoScrollingSampleQuestionsState(); -} - -class _AutoScrollingSampleQuestionsState - extends State<_AutoScrollingSampleQuestions> { - late final scrollController = ScrollController( - initialScrollOffset: widget.offset, - ); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 36, - child: InfiniteScrollView( - scrollController: scrollController, - centerKey: UniqueKey(), - itemCount: widget.questions.length, - itemBuilder: (context, index) { - return WelcomeSampleQuestion( - question: widget.questions[index], - onSelected: widget.onSelected, - ); - }, - separatorBuilder: (context, index) => const HSpace(8), - ), - ); - } -} - -class InfiniteScrollView extends StatelessWidget { - const InfiniteScrollView({ - super.key, - required this.itemCount, - required this.centerKey, - required this.itemBuilder, - required this.separatorBuilder, - this.scrollController, - }); - - final int itemCount; - final Widget Function(BuildContext context, int index) itemBuilder; - final Widget Function(BuildContext context, int index) separatorBuilder; - final Key centerKey; - - final ScrollController? scrollController; - - @override - Widget build(BuildContext context) { - return CustomScrollView( - scrollDirection: Axis.horizontal, - controller: scrollController, - center: centerKey, - anchor: 0.5, - slivers: [ - _buildList(isForward: false), - SliverToBoxAdapter( - child: separatorBuilder.call(context, 0), - ), - SliverToBoxAdapter( - key: centerKey, - child: itemBuilder.call(context, 0), - ), - SliverToBoxAdapter( - child: separatorBuilder.call(context, 0), - ), - _buildList(isForward: true), - ], - ); - } - - Widget _buildList({required bool isForward}) { - return SliverList.separated( - itemBuilder: (context, index) { - index = (index + 1) % itemCount; - return itemBuilder(context, index); - }, - separatorBuilder: (context, index) { - index = (index + 1) % itemCount; - return separatorBuilder(context, index); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart deleted file mode 100644 index 611ff5d922..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/layout_define.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class AIChatUILayout { - const AIChatUILayout._(); - - static EdgeInsets safeAreaInsets(BuildContext context) { - final query = MediaQuery.of(context); - return UniversalPlatform.isMobile - ? EdgeInsets.fromLTRB( - query.padding.left, - 0, - query.padding.right, - query.viewInsets.bottom + query.padding.bottom, - ) - : const EdgeInsets.only(bottom: 24); - } - - static EdgeInsets get messageMargin => UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 16) - : EdgeInsets.zero; -} - -class DesktopAIChatSizes { - const DesktopAIChatSizes._(); - - static const avatarSize = 32.0; - static const avatarAndChatBubbleSpacing = 12.0; - - static const messageActionBarIconSize = 28.0; - static const messageHoverActionBarPadding = EdgeInsets.all(2.0); - static const messageHoverActionBarRadius = - BorderRadius.all(Radius.circular(8.0)); - static const messageHoverActionBarIconRadius = - BorderRadius.all(Radius.circular(6.0)); - static const messageActionBarIconRadius = - BorderRadius.all(Radius.circular(8.0)); - - static const inputActionBarMargin = - EdgeInsetsDirectional.fromSTEB(8, 0, 8, 4); - static const inputActionBarButtonSpacing = 4.0; -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart deleted file mode 100644 index 5fa3b8f8a7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_format_bottom_sheet.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:appflowy/ai/ai.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_actions.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -Future showChangeFormatBottomSheet( - BuildContext context, -) { - return showMobileBottomSheet( - context, - showDragHandle: true, - builder: (context) => const _ChangeFormatBottomSheetContent(), - ); -} - -class _ChangeFormatBottomSheetContent extends StatefulWidget { - const _ChangeFormatBottomSheetContent(); - - @override - State<_ChangeFormatBottomSheetContent> createState() => - _ChangeFormatBottomSheetContentState(); -} - -class _ChangeFormatBottomSheetContentState - extends State<_ChangeFormatBottomSheetContent> { - PredefinedFormat? predefinedFormat; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _Header( - onCancel: () => Navigator.of(context).pop(), - onDone: () => Navigator.of(context).pop(predefinedFormat), - ), - const VSpace(4.0), - _Body( - predefinedFormat: predefinedFormat, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); - }, - ), - const VSpace(16.0), - ], - ); - } -} - -class _Header extends StatelessWidget { - const _Header({ - required this.onCancel, - required this.onDone, - }); - - final VoidCallback onCancel; - final VoidCallback onDone; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44.0, - child: Stack( - children: [ - Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - onTap: onCancel, - ), - ), - Align( - child: Container( - constraints: const BoxConstraints(maxWidth: 250), - child: FlowyText( - LocaleKeys.chat_changeFormat_actionButton.tr(), - fontSize: 17.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - ), - ), - Align( - alignment: Alignment.centerRight, - child: AppBarDoneButton( - onTap: onDone, - ), - ), - ], - ), - ); - } -} - -class _Body extends StatelessWidget { - const _Body({ - required this.predefinedFormat, - required this.onSelectPredefinedFormat, - }); - - final PredefinedFormat? predefinedFormat; - final void Function(PredefinedFormat) onSelectPredefinedFormat; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildFormatButton(ImageFormat.text, true), - _buildFormatButton(ImageFormat.textAndImage), - _buildFormatButton(ImageFormat.image), - const VSpace(32.0), - Opacity( - opacity: predefinedFormat?.imageFormat.hasText ?? true ? 1 : 0, - child: Column( - children: [ - _buildTextFormatButton(TextFormat.paragraph, true), - _buildTextFormatButton(TextFormat.bulletList), - _buildTextFormatButton(TextFormat.numberedList), - _buildTextFormatButton(TextFormat.table), - ], - ), - ), - ], - ); - } - - Widget _buildFormatButton( - ImageFormat format, [ - bool isFirst = false, - ]) { - return FlowyOptionTile.checkbox( - text: format.i18n, - isSelected: format == predefinedFormat?.imageFormat, - showTopBorder: isFirst, - leftIcon: FlowySvg( - format.icon, - size: format == ImageFormat.textAndImage - ? const Size(21.0 / 16.0 * 20, 20) - : const Size.square(20), - ), - onTap: () { - if (predefinedFormat != null && - format == predefinedFormat!.imageFormat) { - return; - } - if (format.hasText) { - final textFormat = - predefinedFormat?.textFormat ?? TextFormat.paragraph; - onSelectPredefinedFormat( - PredefinedFormat(imageFormat: format, textFormat: textFormat), - ); - } else { - onSelectPredefinedFormat( - PredefinedFormat(imageFormat: format, textFormat: null), - ); - } - }, - ); - } - - Widget _buildTextFormatButton( - TextFormat format, [ - bool isFirst = false, - ]) { - return FlowyOptionTile.checkbox( - text: format.i18n, - isSelected: format == predefinedFormat?.textFormat, - showTopBorder: isFirst, - leftIcon: FlowySvg( - format.icon, - size: const Size.square(20), - ), - onTap: () { - if (predefinedFormat != null && - format == predefinedFormat!.textFormat) { - return; - } - onSelectPredefinedFormat( - PredefinedFormat( - imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text, - textFormat: format, - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart deleted file mode 100644 index aa0d840574..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_change_model_bottom_sheet.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -Future showChangeModelBottomSheet( - BuildContext context, - List models, -) { - return showMobileBottomSheet( - context, - showDragHandle: true, - builder: (context) => _ChangeModelBottomSheetContent(models: models), - ); -} - -class _ChangeModelBottomSheetContent extends StatefulWidget { - const _ChangeModelBottomSheetContent({ - required this.models, - }); - - final List models; - - @override - State<_ChangeModelBottomSheetContent> createState() => - _ChangeModelBottomSheetContentState(); -} - -class _ChangeModelBottomSheetContentState - extends State<_ChangeModelBottomSheetContent> { - AIModelPB? model; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _Header( - onCancel: () => Navigator.of(context).pop(), - onDone: () => Navigator.of(context).pop(model), - ), - const VSpace(4.0), - _Body( - models: widget.models, - selectedModel: model, - onSelectModel: (format) { - setState(() => model = format); - }, - ), - const VSpace(16.0), - ], - ); - } -} - -class _Header extends StatelessWidget { - const _Header({ - required this.onCancel, - required this.onDone, - }); - - final VoidCallback onCancel; - final VoidCallback onDone; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 44.0, - child: Stack( - children: [ - Align( - alignment: Alignment.centerLeft, - child: AppBarBackButton( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - onTap: onCancel, - ), - ), - Align( - child: Container( - constraints: const BoxConstraints(maxWidth: 250), - child: FlowyText( - LocaleKeys.chat_switchModel_label.tr(), - fontSize: 17.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - ), - ), - Align( - alignment: Alignment.centerRight, - child: AppBarDoneButton( - onTap: onDone, - ), - ), - ], - ), - ); - } -} - -class _Body extends StatelessWidget { - const _Body({ - required this.models, - required this.selectedModel, - required this.onSelectModel, - }); - - final List models; - final AIModelPB? selectedModel; - final void Function(AIModelPB) onSelectModel; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: models - .mapIndexed( - (index, model) => _buildModelButton(model, index == 0), - ) - .toList(), - ); - } - - Widget _buildModelButton( - AIModelPB model, [ - bool isFirst = false, - ]) { - return FlowyOptionTile.checkbox( - text: model.name, - isSelected: model == selectedModel, - showTopBorder: isFirst, - onTap: () { - onSelectModel(model); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart deleted file mode 100644 index 1e7d428263..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_markdown_text.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/markdown_to_document.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:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../chat_editor_style.dart'; - -// Wrap the appflowy_editor as a chat text message widget -class AIMarkdownText extends StatelessWidget { - const AIMarkdownText({ - super.key, - required this.markdown, - }); - - final String markdown; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DocumentPageStyleBloc(view: ViewPB()) - ..add(const DocumentPageStyleEvent.initial()), - child: _AppFlowyEditorMarkdown(markdown: markdown), - ); - } -} - -class _AppFlowyEditorMarkdown extends StatefulWidget { - const _AppFlowyEditorMarkdown({ - required this.markdown, - }); - - // the text should be the markdown format - final String markdown; - - @override - State<_AppFlowyEditorMarkdown> createState() => - _AppFlowyEditorMarkdownState(); -} - -class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> { - late EditorState editorState; - late EditorScrollController scrollController; - - @override - void initState() { - super.initState(); - - editorState = _parseMarkdown(widget.markdown.trim()); - scrollController = EditorScrollController( - editorState: editorState, - shrinkWrap: true, - ); - } - - @override - void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.markdown != widget.markdown) { - final editorState = _parseMarkdown( - widget.markdown.trim(), - previousDocument: this.editorState.document, - ); - this.editorState.dispose(); - this.editorState = editorState; - scrollController.dispose(); - scrollController = EditorScrollController( - editorState: editorState, - shrinkWrap: true, - ); - } - } - - @override - void dispose() { - scrollController.dispose(); - editorState.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // don't lazy load the styleCustomizer and blockBuilders, - // it needs the context to get the theme. - final styleCustomizer = ChatEditorStyleCustomizer( - context: context, - padding: EdgeInsets.zero, - ); - final editorStyle = styleCustomizer.style().copyWith( - // hide the cursor - cursorColor: Colors.transparent, - cursorWidth: 0, - ); - final blockBuilders = buildBlockComponentBuilders( - context: context, - editorState: editorState, - styleCustomizer: styleCustomizer, - // the editor is not editable in the chat - editable: false, - alwaysDistributeSimpleTableColumnWidths: UniversalPlatform.isDesktop, - ); - return IntrinsicHeight( - child: AppFlowyEditor( - shrinkWrap: true, - // the editor is not editable in the chat - editable: false, - disableKeyboardService: UniversalPlatform.isMobile, - disableSelectionService: UniversalPlatform.isMobile, - editorStyle: editorStyle, - editorScrollController: scrollController, - blockComponentBuilders: blockBuilders, - commandShortcutEvents: [customCopyCommand], - disableAutoScroll: true, - editorState: editorState, - contextMenuItems: [ - [ - ContextMenuItem( - getName: LocaleKeys.document_plugins_contextMenu_copy.tr, - onPressed: (editorState) => - customCopyCommand.execute(editorState), - ), - ] - ], - ), - ); - } - - EditorState _parseMarkdown( - String markdown, { - Document? previousDocument, - }) { - // merge the nodes from the previous document with the new document to keep the same node ids - final document = customMarkdownToDocument(markdown); - final documentIterator = NodeIterator( - document: document, - startNode: document.root, - ); - if (previousDocument != null) { - final previousDocumentIterator = NodeIterator( - document: previousDocument, - startNode: previousDocument.root, - ); - while ( - documentIterator.moveNext() && previousDocumentIterator.moveNext()) { - final currentNode = documentIterator.current; - final previousNode = previousDocumentIterator.current; - if (currentNode.path.equals(previousNode.path)) { - currentNode.id = previousNode.id; - } - } - } - final editorState = EditorState(document: document); - return editorState; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart deleted file mode 100644 index 08fd82188d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_action_bar.dart +++ /dev/null @@ -1,779 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/prelude.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package: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:flutter_chat_core/flutter_chat_core.dart'; - -import '../layout_define.dart'; -import 'message_util.dart'; - -class AIMessageActionBar extends StatefulWidget { - const AIMessageActionBar({ - super.key, - required this.message, - required this.showDecoration, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - this.onOverrideVisibility, - }); - - final Message message; - final bool showDecoration; - final void Function()? onRegenerate; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - final void Function(bool)? onOverrideVisibility; - - @override - State createState() => _AIMessageActionBarState(); -} - -class _AIMessageActionBarState extends State { - final popoverMutex = PopoverMutex(); - - @override - Widget build(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - - final child = SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(8.0), - children: _buildChildren(), - ); - - return widget.showDecoration - ? Container( - padding: DesktopAIChatSizes.messageHoverActionBarPadding, - decoration: BoxDecoration( - borderRadius: DesktopAIChatSizes.messageHoverActionBarRadius, - border: Border.all( - color: isLightMode - ? const Color(0x1F1F2329) - : Theme.of(context).dividerColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 1), - blurRadius: 2, - spreadRadius: -2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 4, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 8, - spreadRadius: 2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - ], - ), - child: child, - ) - : child; - } - - List _buildChildren() { - return [ - CopyButton( - isInHoverBar: widget.showDecoration, - textMessage: widget.message as TextMessage, - ), - RegenerateButton( - isInHoverBar: widget.showDecoration, - onTap: () => widget.onRegenerate?.call(), - ), - ChangeFormatButton( - isInHoverBar: widget.showDecoration, - onRegenerate: widget.onChangeFormat, - popoverMutex: popoverMutex, - onOverrideVisibility: widget.onOverrideVisibility, - ), - ChangeModelButton( - isInHoverBar: widget.showDecoration, - onRegenerate: widget.onChangeModel, - popoverMutex: popoverMutex, - onOverrideVisibility: widget.onOverrideVisibility, - ), - SaveToPageButton( - textMessage: widget.message as TextMessage, - isInHoverBar: widget.showDecoration, - popoverMutex: popoverMutex, - onOverrideVisibility: widget.onOverrideVisibility, - ), - ]; - } -} - -class CopyButton extends StatelessWidget { - const CopyButton({ - super.key, - required this.isInHoverBar, - required this.textMessage, - }); - - final bool isInHoverBar; - final TextMessage textMessage; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_clickToCopy.tr(), - child: FlowyIconButton( - width: DesktopAIChatSizes.messageActionBarIconSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: isInHoverBar - ? DesktopAIChatSizes.messageHoverActionBarIconRadius - : DesktopAIChatSizes.messageActionBarIconRadius, - icon: FlowySvg( - FlowySvgs.copy_s, - color: Theme.of(context).hintColor, - size: const Size.square(16), - ), - onPressed: () async { - final messageText = textMessage.text.trim(); - final document = customMarkdownToDocument( - messageText, - tableWidth: 250.0, - ); - await getIt().setData( - ClipboardServiceData( - plainText: _stripMarkdownIfNecessary(messageText), - inAppJson: jsonEncode(document.toJson()), - ), - ); - if (context.mounted) { - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } - }, - ), - ); - } - - String _stripMarkdownIfNecessary(String plainText) { - // match and capture inner url as group - final matches = singleLineMarkdownImageRegex.allMatches(plainText); - - if (matches.length != 1) { - return plainText; - } - - return matches.first[1] ?? plainText; - } -} - -class RegenerateButton extends StatelessWidget { - const RegenerateButton({ - super.key, - required this.isInHoverBar, - required this.onTap, - }); - - final bool isInHoverBar; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_regenerate.tr(), - child: FlowyIconButton( - width: DesktopAIChatSizes.messageActionBarIconSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: isInHoverBar - ? DesktopAIChatSizes.messageHoverActionBarIconRadius - : DesktopAIChatSizes.messageActionBarIconRadius, - icon: FlowySvg( - FlowySvgs.ai_try_again_s, - color: Theme.of(context).hintColor, - size: const Size.square(16), - ), - onPressed: onTap, - ), - ); - } -} - -class ChangeFormatButton extends StatefulWidget { - const ChangeFormatButton({ - super.key, - required this.isInHoverBar, - this.popoverMutex, - this.onRegenerate, - this.onOverrideVisibility, - }); - - final bool isInHoverBar; - final PopoverMutex? popoverMutex; - final void Function(PredefinedFormat)? onRegenerate; - final void Function(bool)? onOverrideVisibility; - - @override - State createState() => _ChangeFormatButtonState(); -} - -class _ChangeFormatButtonState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - mutex: widget.popoverMutex, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - offset: Offset(0, widget.isInHoverBar ? 8 : 4), - direction: PopoverDirection.bottomWithLeftAligned, - constraints: const BoxConstraints(), - onClose: () => widget.onOverrideVisibility?.call(false), - child: buildButton(context), - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: _ChangeFormatPopoverContent( - onRegenerate: widget.onRegenerate, - ), - ), - ); - } - - Widget buildButton(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_changeFormat_actionButton.tr(), - child: FlowyIconButton( - width: 32.0, - height: DesktopAIChatSizes.messageActionBarIconSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: widget.isInHoverBar - ? DesktopAIChatSizes.messageHoverActionBarIconRadius - : DesktopAIChatSizes.messageActionBarIconRadius, - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.ai_retry_font_s, - color: Theme.of(context).hintColor, - size: const Size.square(16), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - onPressed: () { - widget.onOverrideVisibility?.call(true); - popoverController.show(); - }, - ), - ); - } -} - -class _ChangeFormatPopoverContent extends StatefulWidget { - const _ChangeFormatPopoverContent({ - this.onRegenerate, - }); - - final void Function(PredefinedFormat)? onRegenerate; - - @override - State<_ChangeFormatPopoverContent> createState() => - _ChangeFormatPopoverContentState(); -} - -class _ChangeFormatPopoverContentState - extends State<_ChangeFormatPopoverContent> { - PredefinedFormat? predefinedFormat; - - @override - Widget build(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - return Container( - padding: const EdgeInsets.all(2.0), - decoration: BoxDecoration( - borderRadius: DesktopAIChatSizes.messageHoverActionBarRadius, - border: Border.all( - color: isLightMode - ? const Color(0x1F1F2329) - : Theme.of(context).dividerColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 1), - blurRadius: 2, - spreadRadius: -2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 4, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 8, - spreadRadius: 2, - color: isLightMode - ? const Color(0x051F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.02), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - BlocBuilder( - builder: (context, state) { - return ChangeFormatBar( - spacing: 2.0, - showImageFormats: state.aiType.isCloud, - predefinedFormat: predefinedFormat, - onSelectPredefinedFormat: (format) { - setState(() => predefinedFormat = format); - }, - ); - }, - ), - const HSpace(4.0), - FlowyTooltip( - message: LocaleKeys.chat_changeFormat_confirmButton.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (predefinedFormat != null) { - widget.onRegenerate?.call(predefinedFormat!); - } - }, - child: SizedBox.square( - dimension: DesktopAIPromptSizes.predefinedFormatButtonHeight, - child: Center( - child: FlowySvg( - FlowySvgs.ai_retry_filled_s, - color: Theme.of(context).colorScheme.primary, - size: const Size.square(20), - ), - ), - ), - ), - ), - ), - ], - ), - ); - } -} - -class ChangeModelButton extends StatefulWidget { - const ChangeModelButton({ - super.key, - required this.isInHoverBar, - this.popoverMutex, - this.onRegenerate, - this.onOverrideVisibility, - }); - - final bool isInHoverBar; - final PopoverMutex? popoverMutex; - final void Function(AIModelPB)? onRegenerate; - final void Function(bool)? onOverrideVisibility; - - @override - State createState() => _ChangeModelButtonState(); -} - -class _ChangeModelButtonState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - mutex: widget.popoverMutex, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - offset: Offset(8, 0), - direction: PopoverDirection.rightWithBottomAligned, - constraints: BoxConstraints(maxWidth: 250, maxHeight: 600), - onClose: () => widget.onOverrideVisibility?.call(false), - child: buildButton(context), - popupBuilder: (_) { - final bloc = context.read(); - final (models, _) = bloc.aiModelStateNotifier.getAvailableModels(); - return SelectModelPopoverContent( - models: models, - selectedModel: null, - onSelectModel: widget.onRegenerate, - ); - }, - ); - } - - Widget buildButton(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.chat_switchModel_label.tr(), - child: FlowyIconButton( - width: 32.0, - height: DesktopAIChatSizes.messageActionBarIconSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: widget.isInHoverBar - ? DesktopAIChatSizes.messageHoverActionBarIconRadius - : DesktopAIChatSizes.messageActionBarIconRadius, - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.ai_sparks_s, - color: Theme.of(context).hintColor, - size: const Size.square(16), - ), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - onPressed: () { - widget.onOverrideVisibility?.call(true); - popoverController.show(); - }, - ), - ); - } -} - -class SaveToPageButton extends StatefulWidget { - const SaveToPageButton({ - super.key, - required this.textMessage, - required this.isInHoverBar, - this.popoverMutex, - this.onOverrideVisibility, - }); - - final TextMessage textMessage; - final bool isInHoverBar; - final PopoverMutex? popoverMutex; - final void Function(bool)? onOverrideVisibility; - - @override - State createState() => _SaveToPageButtonState(); -} - -class _SaveToPageButtonState extends State { - final popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - final userWorkspaceBloc = context.read(); - final userProfile = userWorkspaceBloc.userProfile; - final workspaceId = - userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? ''; - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - ), - BlocProvider( - create: (context) => ChatSettingsCubit(hideDisabled: true), - ), - ], - child: BlocSelector( - selector: (state) => state.currentSpace, - builder: (context, spaceView) { - return AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - margin: EdgeInsets.zero, - mutex: widget.popoverMutex, - offset: const Offset(8, 0), - direction: PopoverDirection.rightWithBottomAligned, - constraints: const BoxConstraints.tightFor(width: 300, height: 400), - onClose: () { - if (spaceView != null) { - context - .read() - .refreshSources([spaceView], spaceView); - } - widget.onOverrideVisibility?.call(false); - }, - child: buildButton(context, spaceView), - popupBuilder: (_) => buildPopover(context), - ); - }, - ), - ); - } - - Widget buildButton(BuildContext context, ViewPB? spaceView) { - return FlowyTooltip( - message: LocaleKeys.chat_addToPageButton.tr(), - child: FlowyIconButton( - width: DesktopAIChatSizes.messageActionBarIconSize, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - radius: widget.isInHoverBar - ? DesktopAIChatSizes.messageHoverActionBarIconRadius - : DesktopAIChatSizes.messageActionBarIconRadius, - icon: FlowySvg( - FlowySvgs.ai_add_to_page_s, - color: Theme.of(context).hintColor, - size: const Size.square(16), - ), - onPressed: () async { - final documentId = getOpenedDocumentId(); - if (documentId != null) { - await onAddToExistingPage(context, documentId); - await forceReload(documentId); - await Future.delayed(const Duration(milliseconds: 500)); - await updateSelection(documentId); - } else { - widget.onOverrideVisibility?.call(true); - if (spaceView != null) { - context - .read() - .refreshSources([spaceView], spaceView); - } - popoverController.show(); - } - }, - ), - ); - } - - Widget buildPopover(BuildContext context) { - return BlocProvider.value( - value: context.read(), - child: SaveToPagePopoverContent( - onAddToNewPage: (parentViewId) { - addMessageToNewPage(context, parentViewId); - popoverController.close(); - }, - onAddToExistingPage: (documentId) async { - popoverController.close(); - final view = await onAddToExistingPage(context, documentId); - - if (context.mounted) { - openPageFromMessage(context, view); - } - await Future.delayed(const Duration(milliseconds: 500)); - await updateSelection(documentId); - }, - ), - ); - } - - Future onAddToExistingPage( - BuildContext context, - String documentId, - ) async { - await ChatEditDocumentService.addMessagesToPage( - documentId, - [widget.textMessage], - ); - await Future.delayed(const Duration(milliseconds: 500)); - final view = await ViewBackendService.getView(documentId).toNullable(); - if (context.mounted) { - showSaveMessageSuccessToast(context, view); - } - return view; - } - - void addMessageToNewPage(BuildContext context, String parentViewId) async { - final chatView = await ViewBackendService.getView( - context.read().chatId, - ).toNullable(); - if (chatView != null) { - final newView = await ChatEditDocumentService.saveMessagesToNewPage( - chatView.nameOrDefault, - parentViewId, - [widget.textMessage], - ); - - if (context.mounted) { - showSaveMessageSuccessToast(context, newView); - openPageFromMessage(context, newView); - } - } - } - - Future forceReload(String documentId) async { - final bloc = DocumentBloc.findOpen(documentId); - if (bloc == null) { - return; - } - await bloc.forceReloadDocumentState(); - } - - Future updateSelection(String documentId) async { - final bloc = DocumentBloc.findOpen(documentId); - if (bloc == null) { - return; - } - await bloc.forceReloadDocumentState(); - final editorState = bloc.state.editorState; - final lastNodePath = editorState?.getLastSelectable()?.$1.path; - if (editorState == null || lastNodePath == null) { - return; - } - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: lastNodePath)), - ), - ); - } - - String? getOpenedDocumentId() { - final pageManager = getIt().state.currentPageManager; - if (!pageManager.showSecondaryPluginNotifier.value) { - return null; - } - return pageManager.secondaryNotifier.plugin.id; - } -} - -class SaveToPagePopoverContent extends StatelessWidget { - const SaveToPagePopoverContent({ - super.key, - required this.onAddToNewPage, - required this.onAddToExistingPage, - }); - - final void Function(String) onAddToNewPage; - final void Function(String) onAddToExistingPage; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 24, - margin: const EdgeInsets.fromLTRB(12, 8, 12, 4), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: FlowyText( - LocaleKeys.chat_addToPageTitle.tr(), - fontSize: 12.0, - color: Theme.of(context).hintColor, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8), - child: SpaceSearchField( - width: 600, - onSearch: (context, value) => - context.read().updateFilter(value), - ), - ), - _buildDivider(), - Expanded( - child: ListView( - shrinkWrap: true, - padding: const EdgeInsets.fromLTRB(8, 4, 8, 12), - children: _buildVisibleSources(context, state).toList(), - ), - ), - ], - ); - }, - ); - } - - Widget _buildDivider() { - return const Divider( - height: 1.0, - thickness: 1.0, - indent: 12.0, - endIndent: 12.0, - ); - } - - Iterable _buildVisibleSources( - BuildContext context, - ChatSettingsState state, - ) { - return state.visibleSources - .where((e) => e.ignoreStatus != IgnoreViewType.hide) - .map( - (e) => ChatSourceTreeItem( - key: ValueKey( - 'save_to_page_tree_item_${e.view.id}', - ), - chatSource: e, - level: 0, - isDescendentOfSpace: e.view.isSpace, - isSelectedSection: false, - showCheckbox: false, - showSaveButton: true, - onSelected: (source) { - if (source.view.isSpace) { - onAddToNewPage(source.view.id); - } else { - onAddToExistingPage(source.view.id); - } - }, - onAdd: (source) { - onAddToNewPage(source.view.id); - }, - height: 30.0, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart deleted file mode 100644 index 2786799520..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_message_bubble.dart +++ /dev/null @@ -1,550 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.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:flutter_chat_core/flutter_chat_core.dart'; -import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../chat_avatar.dart'; -import '../layout_define.dart'; -import 'ai_change_model_bottom_sheet.dart'; -import 'ai_message_action_bar.dart'; -import 'ai_change_format_bottom_sheet.dart'; -import 'message_util.dart'; - -/// Wraps an AI response message with the avatar and actions. On desktop, -/// the actions will be displayed below the response if the response is the -/// last message in the chat. For the others, the actions will be shown on hover -/// On mobile, the actions will be displayed in a bottom sheet on long press. -class ChatAIMessageBubble extends StatelessWidget { - const ChatAIMessageBubble({ - super.key, - required this.message, - required this.child, - required this.showActions, - this.isLastMessage = false, - this.isSelectingMessages = false, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - }); - - final Message message; - final Widget child; - final bool showActions; - final bool isLastMessage; - final bool isSelectingMessages; - final void Function()? onRegenerate; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - - @override - Widget build(BuildContext context) { - final messageWidget = _WrapIsSelectingMessage( - isSelectingMessages: isSelectingMessages, - message: message, - child: child, - ); - - return !isSelectingMessages && showActions - ? UniversalPlatform.isMobile - ? _wrapPopMenu(messageWidget) - : isLastMessage - ? _wrapBottomActions(messageWidget) - : _wrapHover(messageWidget) - : messageWidget; - } - - Widget _wrapBottomActions(Widget child) { - return ChatAIBottomInlineActions( - message: message, - onRegenerate: onRegenerate, - onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, - child: child, - ); - } - - Widget _wrapHover(Widget child) { - return ChatAIMessageHover( - message: message, - onRegenerate: onRegenerate, - onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, - child: child, - ); - } - - Widget _wrapPopMenu(Widget child) { - return ChatAIMessagePopup( - message: message, - onRegenerate: onRegenerate, - onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, - child: child, - ); - } -} - -class ChatAIBottomInlineActions extends StatelessWidget { - const ChatAIBottomInlineActions({ - super.key, - required this.child, - required this.message, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - }); - - final Widget child; - final Message message; - final void Function()? onRegenerate; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - const VSpace(16.0), - Padding( - padding: const EdgeInsetsDirectional.only( - start: DesktopAIChatSizes.avatarSize + - DesktopAIChatSizes.avatarAndChatBubbleSpacing, - ), - child: AIMessageActionBar( - message: message, - showDecoration: false, - onRegenerate: onRegenerate, - onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, - ), - ), - const VSpace(32.0), - ], - ); - } -} - -class ChatAIMessageHover extends StatefulWidget { - const ChatAIMessageHover({ - super.key, - required this.child, - required this.message, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - }); - - final Widget child; - final Message message; - final void Function()? onRegenerate; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - - @override - State createState() => _ChatAIMessageHoverState(); -} - -class _ChatAIMessageHoverState extends State { - final controller = OverlayPortalController(); - final layerLink = LayerLink(); - - bool hoverBubble = false; - bool hoverActionBar = false; - bool overrideVisibility = false; - - ScrollPosition? scrollPosition; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - addScrollListener(); - controller.show(); - }); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - opaque: false, - onEnter: (_) { - if (!hoverBubble && isBottomOfWidgetVisible(context)) { - setState(() => hoverBubble = true); - } - }, - onHover: (_) { - if (!hoverBubble && isBottomOfWidgetVisible(context)) { - setState(() => hoverBubble = true); - } - }, - onExit: (_) { - if (hoverBubble) { - setState(() => hoverBubble = false); - } - }, - child: OverlayPortal( - controller: controller, - overlayChildBuilder: (_) { - return CompositedTransformFollower( - showWhenUnlinked: false, - link: layerLink, - targetAnchor: Alignment.bottomLeft, - offset: const Offset( - DesktopAIChatSizes.avatarSize + - DesktopAIChatSizes.avatarAndChatBubbleSpacing, - 0, - ), - child: Align( - alignment: Alignment.topLeft, - child: MouseRegion( - opaque: false, - onEnter: (_) { - if (!hoverActionBar && isBottomOfWidgetVisible(context)) { - setState(() => hoverActionBar = true); - } - }, - onExit: (_) { - if (hoverActionBar) { - setState(() => hoverActionBar = false); - } - }, - child: SizedBox( - width: 784, - height: DesktopAIChatSizes.messageActionBarIconSize + - DesktopAIChatSizes.messageHoverActionBarPadding.vertical, - child: hoverBubble || hoverActionBar || overrideVisibility - ? Align( - alignment: AlignmentDirectional.centerStart, - child: AIMessageActionBar( - message: widget.message, - showDecoration: true, - onRegenerate: widget.onRegenerate, - onChangeFormat: widget.onChangeFormat, - onChangeModel: widget.onChangeModel, - onOverrideVisibility: (visibility) { - overrideVisibility = visibility; - }, - ), - ) - : null, - ), - ), - ), - ); - }, - child: CompositedTransformTarget( - link: layerLink, - child: widget.child, - ), - ), - ); - } - - void addScrollListener() { - if (!mounted) { - return; - } - scrollPosition = Scrollable.maybeOf(context)?.position; - scrollPosition?.addListener(handleScroll); - } - - void handleScroll() { - if (!mounted) { - return; - } - if ((hoverActionBar || hoverBubble) && !isBottomOfWidgetVisible(context)) { - setState(() { - hoverBubble = false; - hoverActionBar = false; - }); - } - } - - bool isBottomOfWidgetVisible(BuildContext context) { - if (Scrollable.maybeOf(context) == null) { - return false; - } - final scrollableRenderBox = - Scrollable.of(context).context.findRenderObject() as RenderBox; - final scrollableHeight = scrollableRenderBox.size.height; - final scrollableOffset = scrollableRenderBox.localToGlobal(Offset.zero); - - final messageRenderBox = context.findRenderObject() as RenderBox; - final messageOffset = messageRenderBox.localToGlobal(Offset.zero); - final messageHeight = messageRenderBox.size.height; - - return messageOffset.dy + - messageHeight + - DesktopAIChatSizes.messageActionBarIconSize + - DesktopAIChatSizes.messageHoverActionBarPadding.vertical <= - scrollableOffset.dy + scrollableHeight; - } - - @override - void dispose() { - scrollPosition?.isScrollingNotifier.removeListener(handleScroll); - super.dispose(); - } -} - -class ChatAIMessagePopup extends StatelessWidget { - const ChatAIMessagePopup({ - super.key, - required this.child, - required this.message, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - }); - - final Widget child; - final Message message; - final void Function()? onRegenerate; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPress: () { - showMobileBottomSheet( - context, - showDragHandle: true, - backgroundColor: AFThemeExtension.of(context).background, - builder: (bottomSheetContext) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _copyButton(context, bottomSheetContext), - _divider(), - _regenerateButton(context), - _divider(), - _changeFormatButton(context), - _divider(), - _changeModelButton(context), - _divider(), - _saveToPageButton(context), - ], - ); - }, - ); - }, - child: child, - ); - } - - Widget _divider() => const MobileQuickActionDivider(); - - Widget _copyButton(BuildContext context, BuildContext bottomSheetContext) { - return MobileQuickActionButton( - onTap: () async { - if (message is! TextMessage) { - return; - } - final textMessage = message as TextMessage; - final document = customMarkdownToDocument(textMessage.text); - await getIt().setData( - ClipboardServiceData( - plainText: textMessage.text, - inAppJson: jsonEncode(document.toJson()), - ), - ); - if (bottomSheetContext.mounted) { - Navigator.of(bottomSheetContext).pop(); - } - if (context.mounted) { - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } - }, - icon: FlowySvgs.copy_s, - iconSize: const Size.square(20), - text: LocaleKeys.button_copy.tr(), - ); - } - - Widget _regenerateButton(BuildContext context) { - return MobileQuickActionButton( - onTap: () { - onRegenerate?.call(); - Navigator.of(context).pop(); - }, - icon: FlowySvgs.ai_try_again_s, - iconSize: const Size.square(20), - text: LocaleKeys.chat_regenerate.tr(), - ); - } - - Widget _changeFormatButton(BuildContext context) { - return MobileQuickActionButton( - onTap: () async { - final result = await showChangeFormatBottomSheet(context); - if (result != null) { - onChangeFormat?.call(result); - if (context.mounted) { - Navigator.of(context).pop(); - } - } - }, - icon: FlowySvgs.ai_retry_font_s, - iconSize: const Size.square(20), - text: LocaleKeys.chat_changeFormat_actionButton.tr(), - ); - } - - Widget _changeModelButton(BuildContext context) { - return MobileQuickActionButton( - onTap: () async { - final bloc = context.read(); - final (models, _) = bloc.aiModelStateNotifier.getAvailableModels(); - final result = await showChangeModelBottomSheet(context, models); - if (result != null) { - onChangeModel?.call(result); - if (context.mounted) { - Navigator.of(context).pop(); - } - } - }, - icon: FlowySvgs.ai_sparks_s, - iconSize: const Size.square(20), - text: LocaleKeys.chat_switchModel_label.tr(), - ); - } - - Widget _saveToPageButton(BuildContext context) { - return MobileQuickActionButton( - onTap: () async { - final selectedView = await showPageSelectorSheet( - context, - filter: (view) => - !view.isSpace && - view.layout.isDocumentView && - view.parentViewId != view.id, - ); - if (selectedView == null) { - return; - } - - await ChatEditDocumentService.addMessagesToPage( - selectedView.id, - [message as TextMessage], - ); - - if (context.mounted) { - context.pop(); - openPageFromMessage(context, selectedView); - } - }, - icon: FlowySvgs.ai_add_to_page_s, - iconSize: const Size.square(20), - text: LocaleKeys.chat_addToPageButton.tr(), - ); - } -} - -class _WrapIsSelectingMessage extends StatelessWidget { - const _WrapIsSelectingMessage({ - required this.message, - required this.child, - this.isSelectingMessages = false, - }); - - final Message message; - final Widget child; - final bool isSelectingMessages; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final isSelected = - context.read().isMessageSelected(message.id); - return GestureDetector( - onTap: () { - if (isSelectingMessages) { - context - .read() - .add(ChatSelectMessageEvent.toggleSelectMessage(message)); - } - }, - behavior: isSelectingMessages ? HitTestBehavior.opaque : null, - child: DecoratedBox( - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.tertiaryContainer - : null, - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isSelectingMessages) - ChatSelectMessageIndicator(isSelected: isSelected) - else - SelectionContainer.disabled( - child: const ChatAIAvatar(), - ), - const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), - Expanded( - child: IgnorePointer( - ignoring: isSelectingMessages, - child: child, - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class ChatSelectMessageIndicator extends StatelessWidget { - const ChatSelectMessageIndicator({ - super.key, - required this.isSelected, - }); - - final bool isSelected; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: SizedBox.square( - dimension: 30.0, - child: Center( - child: FlowySvg( - isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: const Size.square(20), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart deleted file mode 100644 index cc97610e8d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:time/time.dart'; - -class AIMessageMetadata extends StatefulWidget { - const AIMessageMetadata({ - required this.sources, - required this.onSelectedMetadata, - super.key, - }); - - final List sources; - final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; - - @override - State createState() => _AIMessageMetadataState(); -} - -class _AIMessageMetadataState extends State { - bool isExpanded = true; - - @override - Widget build(BuildContext context) { - return AnimatedSize( - duration: 150.milliseconds, - alignment: AlignmentDirectional.topStart, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(8.0), - ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 24, - maxWidth: 240, - ), - child: FlowyButton( - margin: const EdgeInsets.all(4.0), - useIntrinsicWidth: true, - hoverColor: Colors.transparent, - radius: BorderRadius.circular(8.0), - text: FlowyText( - LocaleKeys.chat_referenceSource.plural( - widget.sources.length, - namedArgs: {'count': '${widget.sources.length}'}, - ), - fontSize: 12, - color: Theme.of(context).hintColor, - ), - rightIcon: FlowySvg( - isExpanded ? FlowySvgs.arrow_up_s : FlowySvgs.arrow_down_s, - size: const Size.square(10), - ), - onTap: () { - setState(() => isExpanded = !isExpanded); - }, - ), - ), - if (isExpanded) ...[ - const VSpace(4.0), - Wrap( - spacing: 8.0, - runSpacing: 4.0, - children: widget.sources.map( - (m) { - if (isURL(m.id)) { - return _MetadataButton( - name: m.id, - onTap: () => widget.onSelectedMetadata?.call(m), - ); - } else if (isUUID(m.id)) { - return FutureBuilder( - future: ViewBackendService.getView(m.id) - .then((f) => f.toNullable()), - builder: (context, snapshot) { - final data = snapshot.data; - if (!snapshot.hasData || - snapshot.connectionState != ConnectionState.done || - data == null) { - return _MetadataButton( - name: m.name, - onTap: () => widget.onSelectedMetadata?.call(m), - ); - } - return BlocProvider( - create: (_) => ViewBloc(view: data), - child: BlocBuilder( - builder: (context, state) { - return _MetadataButton( - name: state.view.nameOrDefault, - onTap: () => widget.onSelectedMetadata?.call(m), - ); - }, - ), - ); - }, - ); - } else { - return _MetadataButton( - name: m.name, - onTap: () => widget.onSelectedMetadata?.call(m), - ); - } - }, - ).toList(), - ), - ], - ], - ), - ); - } -} - -class _MetadataButton extends StatelessWidget { - const _MetadataButton({ - this.name = "", - this.onTap, - }); - - final String name; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 24, - maxWidth: 240, - ), - child: FlowyButton( - margin: const EdgeInsets.all(4.0), - useIntrinsicWidth: true, - radius: BorderRadius.circular(8.0), - text: FlowyText( - name, - fontSize: 12, - overflow: TextOverflow.ellipsis, - ), - leftIcon: FlowySvg( - FlowySvgs.icon_document_s, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ), - onTap: onTap, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index 380767105f..305eba9cd2 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -1,173 +1,361 @@ -import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:markdown_widget/markdown_widget.dart'; -import '../layout_define.dart'; -import 'ai_markdown_text.dart'; -import 'ai_message_bubble.dart'; -import 'ai_metadata.dart'; -import 'error_text_message.dart'; +import 'selectable_highlight.dart'; -/// [ChatAIMessageWidget] includes both the text of the AI response as well as -/// the avatar, decorations and hover effects that are also rendered. This is -/// different from [ChatUserMessageWidget] which only contains the message and -/// has to be separately wrapped with a bubble since the hover effects need to -/// know the current streaming status of the message. -class ChatAIMessageWidget extends StatelessWidget { - const ChatAIMessageWidget({ +class ChatAITextMessageWidget extends StatelessWidget { + const ChatAITextMessageWidget({ super.key, required this.user, required this.messageUserId, - required this.message, - required this.stream, + required this.text, required this.questionId, required this.chatId, - required this.refSourceJsonString, - required this.onStopStream, - this.onSelectedMetadata, - this.onRegenerate, - this.onChangeFormat, - this.onChangeModel, - this.isLastMessage = false, - this.isStreaming = false, - this.isSelectingMessages = false, }); final User user; final String messageUserId; - - final Message message; - final AnswerStream? stream; + final dynamic text; final Int64? questionId; final String chatId; - final String? refSourceJsonString; - final void Function(ChatMessageRefSource metadata)? onSelectedMetadata; - final void Function()? onRegenerate; - final void Function() onStopStream; - final void Function(PredefinedFormat)? onChangeFormat; - final void Function(AIModelPB)? onChangeModel; - final bool isStreaming; - final bool isLastMessage; - final bool isSelectingMessages; @override Widget build(BuildContext context) { return BlocProvider( create: (context) => ChatAIMessageBloc( - message: stream ?? (message as TextMessage).text, - refSourceJsonString: refSourceJsonString, + message: text, chatId: chatId, questionId: questionId, - ), + )..add(const ChatAIMessageEvent.initial()), child: BlocBuilder( builder: (context, state) { - final loadingText = - state.progress?.step ?? LocaleKeys.chat_generatingResponse.tr(); + if (state.error != null) { + return StreamingError( + onRetryPressed: () { + context.read().add( + const ChatAIMessageEvent.retry(), + ); + }, + ); + } - return BlocListener( - listenWhen: (previous, current) => - previous.clearErrorMessages != current.clearErrorMessages, - listener: (context, chatState) { - if (state.stream?.error?.isEmpty != false) { - return; - } - context.read().add(ChatEvent.deleteMessage(message)); - }, - child: Padding( - padding: AIChatUILayout.messageMargin, - child: state.messageState.when( - loading: () => ChatAIMessageBubble( - message: message, - showActions: false, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: AILoadingIndicator(text: loadingText), - ), - ), - ready: () { - return state.text.isEmpty - ? ChatAIMessageBubble( - message: message, - showActions: false, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: AILoadingIndicator(text: loadingText), - ), - ) - : ChatAIMessageBubble( - message: message, - isLastMessage: isLastMessage, - showActions: stream == null && - state.text.isNotEmpty && - !isStreaming, - isSelectingMessages: isSelectingMessages, - onRegenerate: onRegenerate, - onChangeFormat: onChangeFormat, - onChangeModel: onChangeModel, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AIMarkdownText( - markdown: state.text, - ), - if (state.sources.isNotEmpty) - SelectionContainer.disabled( - child: AIMessageMetadata( - sources: state.sources, - onSelectedMetadata: onSelectedMetadata, - ), - ), - if (state.sources.isNotEmpty && !isLastMessage) - const VSpace(8.0), - ], - ), - ); - }, - onError: (error) { - return ChatErrorMessageWidget( - errorMessage: LocaleKeys.chat_aiServerUnavailable.tr(), - ); - }, - onAIResponseLimit: () { - return ChatErrorMessageWidget( - errorMessage: - LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(), - ); - }, - onAIImageResponseLimit: () { - return ChatErrorMessageWidget( - errorMessage: LocaleKeys.sideBar_purchaseAIMax.tr(), - ); - }, - onAIMaxRequired: (message) { - return ChatErrorMessageWidget( - errorMessage: message, - ); - }, - onInitializingLocalAI: () { - onStopStream(); + if (state.retryState == const LoadingState.loading()) { + return const ChatAILoading(); + } - return ChatErrorMessageWidget( - errorMessage: LocaleKeys - .settings_aiPage_keys_localAIInitializing - .tr(), - ); - }, - ), - ), - ); + if (state.text.isEmpty) { + return const ChatAILoading(); + } else { + return _textWidgetBuilder(user, context, state.text); + } }, ), ); } + + Widget _textWidgetBuilder( + User user, + BuildContext context, + String text, + ) { + return MarkdownWidget( + data: text, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + config: configFromContext(context), + ); + } + + MarkdownConfig configFromContext(BuildContext context) { + return MarkdownConfig( + configs: [ + HrConfig(color: AFThemeExtension.of(context).textColor), + ChatH1Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 24, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + ChatH2Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 20, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + ChatH3Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 18, + fontWeight: FontWeight.bold, + height: 1.5, + ), + dividerColor: AFThemeExtension.of(context).lightGreyHover, + ), + H4Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + H5Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + H6Config( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 12, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + PreConfig( + builder: (code, language) { + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 800, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + child: SelectableHighlightView( + code, + language: language, + theme: getHightlineTheme(context), + padding: const EdgeInsets.all(14), + textStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + height: 1.5, + ), + ), + ), + ); + }, + ), + PConfig( + textStyle: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + CodeConfig( + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + BlockquoteConfig( + sideColor: AFThemeExtension.of(context).lightGreyHover, + textColor: AFThemeExtension.of(context).textColor, + ), + ], + ); + } +} + +Map getHightlineTheme(BuildContext context) { + return { + 'root': TextStyle( + color: const Color(0xffabb2bf), + backgroundColor: + Theme.of(context).isLightMode ? Colors.white : Colors.black38, + ), + 'comment': + const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'quote': + const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic), + 'doctag': const TextStyle(color: Color(0xffc678dd)), + 'keyword': const TextStyle(color: Color(0xffc678dd)), + 'formula': const TextStyle(color: Color(0xffc678dd)), + 'section': const TextStyle(color: Color(0xffe06c75)), + 'name': const TextStyle(color: Color(0xffe06c75)), + 'selector-tag': const TextStyle(color: Color(0xffe06c75)), + 'deletion': const TextStyle(color: Color(0xffe06c75)), + 'subst': const TextStyle(color: Color(0xffe06c75)), + 'literal': const TextStyle(color: Color(0xff56b6c2)), + 'string': const TextStyle(color: Color(0xff98c379)), + 'regexp': const TextStyle(color: Color(0xff98c379)), + 'addition': const TextStyle(color: Color(0xff98c379)), + 'attribute': const TextStyle(color: Color(0xff98c379)), + 'meta-string': const TextStyle(color: Color(0xff98c379)), + 'built_in': const TextStyle(color: Color(0xffe6c07b)), + 'attr': const TextStyle(color: Color(0xffd19a66)), + 'variable': const TextStyle(color: Color(0xffd19a66)), + 'template-variable': const TextStyle(color: Color(0xffd19a66)), + 'type': const TextStyle(color: Color(0xffd19a66)), + 'selector-class': const TextStyle(color: Color(0xffd19a66)), + 'selector-attr': const TextStyle(color: Color(0xffd19a66)), + 'selector-pseudo': const TextStyle(color: Color(0xffd19a66)), + 'number': const TextStyle(color: Color(0xffd19a66)), + 'symbol': const TextStyle(color: Color(0xff61aeee)), + 'bullet': const TextStyle(color: Color(0xff61aeee)), + 'link': const TextStyle(color: Color(0xff61aeee)), + 'meta': const TextStyle(color: Color(0xff61aeee)), + 'selector-id': const TextStyle(color: Color(0xff61aeee)), + 'title': const TextStyle(color: Color(0xff61aeee)), + 'emphasis': const TextStyle(fontStyle: FontStyle.italic), + 'strong': const TextStyle(fontWeight: FontWeight.bold), + }; +} + +class ChatH1Config extends HeadingConfig { + const ChatH1Config({ + this.style = const TextStyle( + fontSize: 32, + height: 40 / 32, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h1.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} + +///config class for h2 +class ChatH2Config extends HeadingConfig { + const ChatH2Config({ + this.style = const TextStyle( + fontSize: 24, + height: 30 / 24, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h2.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} + +class ChatH3Config extends HeadingConfig { + const ChatH3Config({ + this.style = const TextStyle( + fontSize: 24, + height: 30 / 24, + fontWeight: FontWeight.bold, + ), + required this.dividerColor, + }); + + @override + final TextStyle style; + final Color dividerColor; + + @override + String get tag => MarkdownTag.h3.name; + + @override + HeadingDivider? get divider => HeadingDivider( + space: 10, + color: dividerColor, + height: 10, + ); +} + +class StreamingError extends StatelessWidget { + const StreamingError({ + required this.onRetryPressed, + super.key, + }); + + final void Function() onRetryPressed; + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(height: 4, thickness: 1), + const VSpace(16), + Center( + child: Column( + children: [ + _aiUnvaliable(), + const VSpace(10), + _retryButton(), + ], + ), + ), + ], + ); + } + + FlowyButton _retryButton() { + return FlowyButton( + radius: BorderRadius.circular(20), + useIntrinsicWidth: true, + text: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: FlowyText( + LocaleKeys.chat_regenerateAnswer.tr(), + fontSize: 14, + ), + ), + onTap: onRetryPressed, + iconPadding: 0, + leftIcon: const Icon( + Icons.refresh, + size: 20, + ), + ); + } + + Padding _aiUnvaliable() { + return Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyText( + LocaleKeys.chat_aiServerUnavailable.tr(), + fontSize: 14, + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart deleted file mode 100644 index 6056ffffa6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/error_text_message.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.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/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class ChatErrorMessageWidget extends StatefulWidget { - const ChatErrorMessageWidget({ - super.key, - required this.errorMessage, - this.onRetry, - }); - - final String errorMessage; - final VoidCallback? onRetry; - - @override - State createState() => _ChatErrorMessageWidgetState(); -} - -class _ChatErrorMessageWidgetState extends State { - late final TapGestureRecognizer recognizer; - - @override - void initState() { - super.initState(); - recognizer = TapGestureRecognizer()..onTap = widget.onRetry; - } - - @override - void dispose() { - recognizer.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 24.0) + - (UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 16) - : EdgeInsets.zero), - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - constraints: UniversalPlatform.isDesktop - ? const BoxConstraints(maxWidth: 480) - : null, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.toast_error_filled_s, - blendMode: null, - ), - const HSpace(8.0), - Flexible( - child: _buildText(), - ), - ], - ), - ), - ); - } - - Widget _buildText() { - final errorMessage = widget.errorMessage; - - return widget.onRetry != null - ? RichText( - text: TextSpan( - children: [ - TextSpan( - text: errorMessage, - style: Theme.of(context).textTheme.bodyMedium, - ), - TextSpan( - text: ' ', - style: Theme.of(context).textTheme.bodyMedium, - ), - TextSpan( - text: LocaleKeys.chat_retry.tr(), - recognizer: recognizer, - mouseCursor: SystemMouseCursors.click, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - ), - ], - ), - ) - : FlowyText( - errorMessage, - lineHeight: 1.4, - maxLines: null, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart deleted file mode 100644 index 652fe3791b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/message_util.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// Opens a message in the right hand sidebar on desktop, and push the page -/// on mobile -void openPageFromMessage(BuildContext context, ViewPB? view) { - if (view == null) { - showToastNotification( - message: LocaleKeys.chat_openPagePreviewFailedToast.tr(), - type: ToastificationType.error, - ); - return; - } - if (UniversalPlatform.isDesktop) { - getIt().add( - TabsEvent.openSecondaryPlugin( - plugin: view.plugin(), - ), - ); - } else { - context.pushView(view); - } -} - -void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) { - if (view == null) { - return; - } - showToastNotification( - richMessage: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.chat_addToNewPageSuccessToast.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: const Color(0xFFFFFFFF), - ), - ), - const TextSpan( - text: ' ', - ), - TextSpan( - text: view.nameOrDefault, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: const Color(0xFFFFFFFF), - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart new file mode 100644 index 0000000000..1452f5af8c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/selectable_highlight.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; +import 'package:highlight/highlight.dart'; + +/// Highlight Flutter Widget +class SelectableHighlightView extends StatelessWidget { + SelectableHighlightView( + String input, { + super.key, + this.language, + this.theme = const {}, + this.padding, + this.textStyle, + int tabSize = 8, + }) : source = input.replaceAll('\t', ' ' * tabSize); + + /// The original code to be highlighted + final String source; + + /// Highlight language + /// + /// It is recommended to give it a value for performance + /// + /// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) + final String? language; + + /// Highlight theme + /// + /// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes) + final Map theme; + + /// Padding + final EdgeInsetsGeometry? padding; + + /// Text styles + /// + /// Specify text styles such as font family and font size + final TextStyle? textStyle; + + List _convert(List nodes) { + final List spans = []; + var currentSpans = spans; + final List> stack = []; + + // ignore: always_declare_return_types + traverse(Node node) { + if (node.value != null) { + currentSpans.add( + node.className == null + ? TextSpan(text: node.value) + : TextSpan(text: node.value, style: theme[node.className!]), + ); + } else if (node.children != null) { + final List tmp = []; + currentSpans + .add(TextSpan(children: tmp, style: theme[node.className!])); + stack.add(currentSpans); + currentSpans = tmp; + + for (final n in node.children!) { + traverse(n); + if (n == node.children!.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + } + } + } + + for (final node in nodes) { + traverse(node); + } + + return spans; + } + + static const _rootKey = 'root'; + static const _defaultBackgroundColor = Color(0xffffffff); + + @override + Widget build(BuildContext context) { + return Container( + color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, + padding: padding, + child: SelectableText.rich( + TextSpan( + style: textStyle, + children: + _convert(highlight.parse(source, language: language).nodes!), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart deleted file mode 100644 index 8bd115ad0f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart +++ /dev/null @@ -1,156 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; - -import '../chat_avatar.dart'; -import '../layout_define.dart'; - -class ChatUserMessageBubble extends StatelessWidget { - const ChatUserMessageBubble({ - super.key, - required this.message, - required this.child, - this.files = const [], - }); - - final Message message; - final Widget child; - final List files; - - @override - Widget build(BuildContext context) { - context - .read() - .add(ChatMemberEvent.getMemberInfo(message.author.id)); - - return Padding( - padding: AIChatUILayout.messageMargin, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (files.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.only(right: 32), - child: _MessageFileList(files: files), - ), - const VSpace(6), - ], - Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - _buildBubble(context), - const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing), - _buildAvatar(), - ], - ), - ], - ), - ); - } - - Widget _buildAvatar() { - return BlocBuilder( - builder: (context, state) { - final member = state.members[message.author.id]; - return SelectionContainer.disabled( - child: ChatUserAvatar( - iconUrl: member?.info.avatarUrl ?? "", - name: member?.info.name ?? "", - ), - ); - }, - ); - } - - Widget _buildBubble(BuildContext context) { - return Flexible( - flex: 5, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: child, - ), - ); - } -} - -class _MessageFileList extends StatelessWidget { - const _MessageFileList({required this.files}); - - final List files; - - @override - Widget build(BuildContext context) { - final List children = files - .map( - (file) => _MessageFile( - file: file, - ), - ) - .toList(); - - return Wrap( - direction: Axis.vertical, - crossAxisAlignment: WrapCrossAlignment.end, - spacing: 6, - runSpacing: 6, - children: children, - ); - } -} - -class _MessageFile extends StatelessWidget { - const _MessageFile({required this.file}); - - final ChatFile file; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Theme.of(context).colorScheme.secondary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.page_m, - size: const Size.square(16), - color: Theme.of(context).hintColor, - ), - const HSpace(6), - Flexible( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: FlowyText( - file.fileName, - fontSize: 12, - maxLines: 6, - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index c73100b59d..3709cdd413 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -1,66 +1,38 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; -import 'user_message_bubble.dart'; - -class ChatUserMessageWidget extends StatelessWidget { - const ChatUserMessageWidget({ +class ChatTextMessageWidget extends StatelessWidget { + const ChatTextMessageWidget({ super.key, required this.user, - required this.message, + required this.messageUserId, + required this.text, }); final User user; - final TextMessage message; + final String messageUserId; + final String text; @override Widget build(BuildContext context) { - final stream = message.metadata?["$QuestionStream"]; - final messageText = stream is QuestionStream ? stream.text : message.text; - - return BlocProvider( - create: (context) => ChatUserMessageBloc( - text: messageText, - questionStream: stream, - ), - child: ChatUserMessageBubble( - message: message, - files: _getFiles(), - child: BlocBuilder( - builder: (context, state) { - return Opacity( - opacity: state.messageState.isFinish ? 1.0 : 0.8, - child: TextMessageText( - text: state.text, - ), - ); - }, - ), - ), - ); + return _textWidgetBuilder(user, context, text); } - List _getFiles() { - if (message.metadata == null) { - return const []; - } - - final refSourceMetadata = - message.metadata?[messageRefSourceJsonStringKey] as String?; - if (refSourceMetadata != null) { - return chatFilesFromMetadataString(refSourceMetadata); - } - - final chatFileList = - message.metadata![messageChatFileListKey] as List?; - return chatFileList ?? []; + Widget _textWidgetBuilder( + User user, + BuildContext context, + String text, + ) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextMessageText( + text: text, + ), + ], + ); } } @@ -78,8 +50,11 @@ class TextMessageText extends StatelessWidget { Widget build(BuildContext context) { return FlowyText( text, - lineHeight: 1.4, + fontSize: 16, + fontWeight: FontWeight.w500, + lineHeight: 1.5, maxLines: null, + selectable: true, color: AFThemeExtension.of(context).textColor, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart deleted file mode 100644 index d66a6665b3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/scroll_to_bottom.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -const BorderRadius _borderRadius = BorderRadius.all(Radius.circular(16)); - -class CustomScrollToBottom extends StatelessWidget { - const CustomScrollToBottom({ - super.key, - required this.animation, - required this.onPressed, - }); - - final Animation animation; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - - return Positioned( - bottom: 24, - left: 0, - right: 0, - child: Center( - child: ScaleTransition( - scale: animation, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: Theme.of(context).dividerColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: _borderRadius, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 8), - blurRadius: 16, - spreadRadius: 8, - color: isLightMode - ? const Color(0x0F1F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.06), - ), - BoxShadow( - offset: const Offset(0, 4), - blurRadius: 8, - color: isLightMode - ? const Color(0x141F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.08), - ), - BoxShadow( - offset: const Offset(0, 2), - blurRadius: 4, - color: isLightMode - ? const Color(0x1F1F2329) - : Theme.of(context).shadowColor.withValues(alpha: 0.12), - ), - ], - ), - child: Material( - borderRadius: _borderRadius, - color: Colors.transparent, - borderOnForeground: false, - child: InkWell( - overlayColor: WidgetStateProperty.all( - AFThemeExtension.of(context).lightGreyHover, - ), - borderRadius: _borderRadius, - onTap: onPressed, - child: const SizedBox.square( - dimension: 32, - child: Center( - child: FlowySvg( - FlowySvgs.ai_scroll_to_bottom_s, - size: Size.square(20), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart new file mode 100644 index 0000000000..9adf3d593b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/user_message_bubble.dart @@ -0,0 +1,171 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:styled_widget/styled_widget.dart'; + +class ChatUserMessageBubble extends StatelessWidget { + const ChatUserMessageBubble({ + super.key, + required this.message, + required this.child, + }); + + final Message message; + final Widget child; + + @override + Widget build(BuildContext context) { + const borderRadius = BorderRadius.all(Radius.circular(6)); + final backgroundColor = + Theme.of(context).colorScheme.surfaceContainerHighest; + + return BlocProvider( + create: (context) => ChatUserMessageBloc(message: message) + ..add(const ChatUserMessageEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // _wrapHover( + Flexible( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: borderRadius, + color: backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: child, + ), + ), + ), + // ), + BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ChatUserAvatar( + iconUrl: state.member?.avatarUrl ?? "", + name: state.member?.name ?? "", + size: 36, + ), + ); + }, + ), + ], + ); + }, + ), + ); + } +} + +class ChatUserMessageHover extends StatefulWidget { + const ChatUserMessageHover({ + super.key, + required this.child, + required this.message, + }); + + final Widget child; + final Message message; + final bool autoShowHover = true; + + @override + State createState() => _ChatUserMessageHoverState(); +} + +class _ChatUserMessageHoverState extends State { + bool _isHover = false; + + @override + void initState() { + super.initState(); + _isHover = widget.autoShowHover ? false : true; + } + + @override + Widget build(BuildContext context) { + final List children = [ + DecoratedBox( + decoration: const BoxDecoration( + color: Colors.transparent, + borderRadius: Corners.s6Border, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 30), + child: widget.child, + ), + ), + ]; + + if (_isHover) { + if (widget.message is TextMessage) { + children.add( + EditButton( + textMessage: widget.message as TextMessage, + ).positioned(right: 0, bottom: 0), + ); + } + } + + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = true; + } + }), + onExit: (p) => setState(() { + if (widget.autoShowHover) { + _isHover = false; + } + }), + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: children, + ), + ); + } +} + +class EditButton extends StatelessWidget { + const EditButton({ + super.key, + required this.textMessage, + }); + final TextMessage textMessage; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.settings_menu_clickToCopy.tr(), + child: FlowyIconButton( + width: 24, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + fillColor: Theme.of(context).cardColor, + icon: FlowySvg( + FlowySvgs.ai_copy_s, + size: const Size.square(14), + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () {}, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 27b288090a..d7a7b9ceb2 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,9 +1,6 @@ -import 'dart:math'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; -import 'package:appflowy/shared/icon_emoji_picker/emoji_search_bar.dart'; -import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,39 +8,23 @@ import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; // use a global value to store the selected emoji to prevent reloading every time. EmojiData? kCachedEmojiData; -const _kRecentEmojiCategoryId = 'Recent'; - -class EmojiPickerResult { - EmojiPickerResult({ - required this.emojiId, - required this.emoji, - this.isRandom = false, - }); - - final String emojiId; - final String emoji; - final bool isRandom; -} class FlowyEmojiPicker extends StatefulWidget { const FlowyEmojiPicker({ super.key, required this.onEmojiSelected, this.emojiPerLine = 9, - this.ensureFocus = false, }); - final ValueChanged onEmojiSelected; + final EmojiSelectedCallback onEmojiSelected; final int emojiPerLine; - final bool ensureFocus; @override State createState() => _FlowyEmojiPickerState(); } class _FlowyEmojiPickerState extends State { - late EmojiData emojiData; - bool loaded = false; + EmojiData? emojiData; @override void initState() { @@ -51,12 +32,14 @@ class _FlowyEmojiPickerState extends State { // load the emoji data from cache if it's available if (kCachedEmojiData != null) { - loadEmojis(kCachedEmojiData!); + emojiData = kCachedEmojiData; } else { EmojiData.builtIn().then( (value) { kCachedEmojiData = value; - loadEmojis(value); + setState(() { + emojiData = value; + }); }, ); } @@ -64,7 +47,7 @@ class _FlowyEmojiPickerState extends State { @override Widget build(BuildContext context) { - if (!loaded) { + if (emojiData == null) { return const Center( child: SizedBox.square( dimension: 24.0, @@ -76,79 +59,45 @@ class _FlowyEmojiPickerState extends State { } return EmojiPicker( - emojiData: emojiData, + emojiData: emojiData!, configuration: EmojiPickerConfiguration( showTabs: false, defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, + perLine: widget.emojiPerLine, ), - onEmojiSelected: (id, emoji) { - widget.onEmojiSelected.call( - EmojiPickerResult(emojiId: id, emoji: emoji), + onEmojiSelected: widget.onEmojiSelected, + headerBuilder: (context, category) { + return FlowyEmojiHeader( + category: category, ); - RecentIcons.putEmoji(id); }, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - headerBuilder: (_, category) => FlowyEmojiHeader(category: category), itemBuilder: (context, emojiId, emoji, callback) { - final name = emojiData.emojis[emojiId]?.name ?? ''; - return SizedBox.square( - dimension: 36.0, + return SizedBox( + width: 36, + height: 36, child: FlowyButton( margin: EdgeInsets.zero, radius: Corners.s8Border, - text: FlowyTooltip( - message: name, - preferBelow: false, - child: FlowyText.emoji( - emoji, - fontSize: 24.0, - ), + text: FlowyText.emoji( + emoji, + fontSize: 24.0, ), onTap: () => callback(emojiId, emoji), ), ); }, searchBarBuilder: (context, keyword, skinTone) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyEmojiSearchBar( - emojiData: emojiData, - ensureFocus: widget.ensureFocus, - onKeywordChanged: (value) { - keyword.value = value; - }, - onSkinToneChanged: (value) { - skinTone.value = value; - }, - onRandomEmojiSelected: (id, emoji) { - widget.onEmojiSelected.call( - EmojiPickerResult(emojiId: id, emoji: emoji, isRandom: true), - ); - RecentIcons.putEmoji(id); - }, - ), + return FlowyEmojiSearchBar( + emojiData: emojiData!, + onKeywordChanged: (value) { + keyword.value = value; + }, + onSkinToneChanged: (value) { + skinTone.value = value; + }, + onRandomEmojiSelected: widget.onEmojiSelected, ); }, ); } - - void loadEmojis(EmojiData data) { - RecentIcons.getEmojiIds().then((v) { - if (v.isEmpty) { - emojiData = data; - if (mounted) setState(() => loaded = true); - return; - } - final categories = List.of(data.categories); - categories.insert( - 0, - Category( - id: _kRecentEmojiCategoryId, - emojiIds: v.sublist(0, min(widget.emojiPerLine, v.length)), - ), - ); - emojiData = EmojiData(categories: categories, emojis: data.emojis); - if (mounted) setState(() => loaded = true); - }); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart index 9b41dd8bce..9f05c80f09 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -1,8 +1,7 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.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_emoji_mart/flutter_emoji_mart.dart'; -import 'package:universal_platform/universal_platform.dart'; class FlowyEmojiHeader extends StatelessWidget { const FlowyEmojiHeader({ @@ -14,14 +13,14 @@ class FlowyEmojiHeader extends StatelessWidget { @override Widget build(BuildContext context) { - if (UniversalPlatform.isDesktop) { + if (PlatformExtension.isDesktopOrWeb) { return Container( height: 22, color: Theme.of(context).cardColor, child: Padding( padding: const EdgeInsets.only(bottom: 4.0), child: FlowyText.regular( - category.id.capitalize(), + category.id, color: Theme.of(context).hintColor, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart index 47f257d174..85e2197cb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_screen.dart @@ -1,48 +1,23 @@ -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import '../../../generated/locale_keys.g.dart'; -import '../../../mobile/presentation/base/app_bar/app_bar.dart'; -import '../../../shared/icon_emoji_picker/tab.dart'; - class MobileEmojiPickerScreen extends StatelessWidget { - const MobileEmojiPickerScreen({ - super.key, - this.title, - this.selectedType, - this.documentId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); + const MobileEmojiPickerScreen({super.key, this.title}); - final PickerTabType? selectedType; final String? title; - final String? documentId; - final List tabs; static const routeName = '/emoji_picker'; static const pageTitle = 'title'; - static const iconSelectedType = 'iconSelected_type'; - static const selectTabs = 'tabs'; - static const uploadDocumentId = 'document_id'; @override Widget build(BuildContext context) { - return Scaffold( - appBar: FlowyAppBar( - titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(), - ), - body: SafeArea( - child: FlowyIconEmojiPicker( - tabs: tabs, - documentId: documentId, - initialType: selectedType, - onSelectedEmoji: (r) { - context.pop(r.data); - }, - ), - ), + return IconPickerPage( + title: title, + onSelected: (result) { + context.pop(result); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart similarity index 75% rename from frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart rename to frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart index 4520a2b118..c6cec89ecc 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -1,13 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.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:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'colors.dart'; typedef EmojiKeywordChangedCallback = void Function(String keyword); typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); @@ -15,14 +14,12 @@ typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); class FlowyEmojiSearchBar extends StatefulWidget { const FlowyEmojiSearchBar({ super.key, - this.ensureFocus = false, required this.emojiData, required this.onKeywordChanged, required this.onSkinToneChanged, required this.onRandomEmojiSelected, }); - final bool ensureFocus; final EmojiData emojiData; final EmojiKeywordChangedCallback onKeywordChanged; final EmojiSkinToneChanged onSkinToneChanged; @@ -34,7 +31,6 @@ class FlowyEmojiSearchBar extends StatefulWidget { class _FlowyEmojiSearchBarState extends State { final TextEditingController controller = TextEditingController(); - EmojiSkinTone skinTone = lastSelectedEmojiSkinTone ?? EmojiSkinTone.none; @override void dispose() { @@ -47,30 +43,23 @@ class _FlowyEmojiSearchBarState extends State { return Padding( padding: EdgeInsets.symmetric( vertical: 12.0, - horizontal: UniversalPlatform.isDesktopOrWeb ? 0.0 : 8.0, + horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, ), child: Row( children: [ Expanded( child: _SearchTextField( onKeywordChanged: widget.onKeywordChanged, - ensureFocus: widget.ensureFocus, ), ), const HSpace(8.0), _RandomEmojiButton( - skinTone: skinTone, emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), const HSpace(8.0), FlowyEmojiSkinToneSelector( - onEmojiSkinToneChanged: (v) { - setState(() { - skinTone = v; - }); - widget.onSkinToneChanged.call(v); - }, + onEmojiSkinToneChanged: widget.onSkinToneChanged, ), ], ), @@ -80,12 +69,10 @@ class _FlowyEmojiSearchBarState extends State { class _RandomEmojiButton extends StatelessWidget { const _RandomEmojiButton({ - required this.skinTone, required this.emojiData, required this.onRandomEmojiSelected, }); - final EmojiSkinTone skinTone; final EmojiData emojiData; final EmojiSelectedCallback onRandomEmojiSelected; @@ -96,7 +83,7 @@ class _RandomEmojiButton extends StatelessWidget { height: 36, decoration: ShapeDecoration( shape: RoundedRectangleBorder( - side: BorderSide(color: context.pickerButtonBoarderColor), + side: const BorderSide(color: Color(0x1E171717)), borderRadius: BorderRadius.circular(8), ), ), @@ -109,14 +96,9 @@ class _RandomEmojiButton extends StatelessWidget { ), onTap: () { final random = emojiData.random; - final emojiId = random.$1; - final emoji = emojiData.getEmojiById( - emojiId, - skinTone: skinTone, - ); onRandomEmojiSelected( - emojiId, - emoji, + random.$1, + random.$2, ); }, ), @@ -128,11 +110,9 @@ class _RandomEmojiButton extends StatelessWidget { class _SearchTextField extends StatefulWidget { const _SearchTextField({ required this.onKeywordChanged, - this.ensureFocus = false, }); final EmojiKeywordChangedCallback onKeywordChanged; - final bool ensureFocus; @override State<_SearchTextField> createState() => _SearchTextFieldState(); @@ -142,20 +122,6 @@ class _SearchTextFieldState extends State<_SearchTextField> { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(); - @override - void initState() { - super.initState(); - - /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] - /// this is to ensure that focus can be regained within a short period of time - if (widget.ensureFocus) { - Future.delayed(const Duration(milliseconds: 200), () { - if (!mounted || focusNode.hasFocus) return; - focusNode.requestFocus(); - }); - } - } - @override void dispose() { controller.dispose(); @@ -176,7 +142,6 @@ class _SearchTextFieldState extends State<_SearchTextField> { fontWeight: FontWeight.w400, color: Theme.of(context).hintColor, ), - enableBorderColor: context.pickerSearchBarBorderColor, controller: controller, onChanged: widget.onKeywordChanged, prefixIcon: const Padding( diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart similarity index 94% rename from frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart rename to frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart index aa97980182..3add90773d 100644 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -1,11 +1,11 @@ import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'colors.dart'; - // use a temporary global value to store last selected skin tone EmojiSkinTone? lastSelectedEmojiSkinTone; @@ -69,7 +69,7 @@ class _FlowyEmojiSkinToneSelectorState width: 36, height: 36, decoration: BoxDecoration( - border: Border.all(color: context.pickerButtonBoarderColor), + border: Border.all(color: const Color(0x1E171717)), borderRadius: BorderRadius.circular(8), ), child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart index 9df541f4a2..5481c7676a 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart @@ -32,12 +32,11 @@ class EmojiText extends StatelessWidget { strutStyle: const StrutStyle(forceStrutHeight: true), fallbackFontFamily: _cachedFallbackFontFamily, lineHeight: lineHeight, - isEmoji: true, ); } void _loadFallbackFontFamily() { - if (Platform.isLinux) { + if (Platform.isLinux || Platform.isAndroid) { final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily; if (notoColorEmoji != null) { _cachedFallbackFontFamily = [notoColorEmoji]; diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart new file mode 100644 index 0000000000..08e63251e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -0,0 +1,111 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +extension ToProto on FlowyIconType { + ViewIconTypePB toProto() { + switch (this) { + case FlowyIconType.emoji: + return ViewIconTypePB.Emoji; + case FlowyIconType.icon: + return ViewIconTypePB.Icon; + case FlowyIconType.custom: + return ViewIconTypePB.Url; + } + } +} + +enum FlowyIconType { + emoji, + icon, + custom; +} + +class EmojiPickerResult { + factory EmojiPickerResult.none() => + const EmojiPickerResult(FlowyIconType.icon, ''); + + factory EmojiPickerResult.emoji(String emoji) => + EmojiPickerResult(FlowyIconType.emoji, emoji); + + const EmojiPickerResult( + this.type, + this.emoji, + ); + + final FlowyIconType type; + final String emoji; +} + +class FlowyIconPicker extends StatelessWidget { + const FlowyIconPicker({ + super.key, + required this.onSelected, + }); + + final void Function(EmojiPickerResult result) onSelected; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const VSpace(8.0), + Row( + children: [ + FlowyText(LocaleKeys.newSettings_workplace_chooseAnIcon.tr()), + const Spacer(), + _RemoveIconButton( + onTap: () => onSelected(EmojiPickerResult.none()), + ), + ], + ), + const VSpace(12.0), + const Divider(height: 0.5), + Expanded( + child: FlowyEmojiPicker( + emojiPerLine: _getEmojiPerLine(context), + onEmojiSelected: (_, emoji) => + onSelected(EmojiPickerResult.emoji(emoji)), + ), + ), + ], + ), + ); + } + + int _getEmojiPerLine(BuildContext context) { + if (PlatformExtension.isDesktopOrWeb) { + return 9; + } + final width = MediaQuery.of(context).size.width; + return width ~/ 40.0; // the size of the emoji + } +} + +class _RemoveIconButton extends StatelessWidget { + const _RemoveIconButton({required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24, + child: FlowyButton( + onTap: onTap, + useIntrinsicWidth: true, + text: FlowyText.regular( + LocaleKeys.button_remove.tr(), + color: Theme.of(context).hintColor, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart new file mode 100644 index 0000000000..15cc8c59e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker_page.dart @@ -0,0 +1,29 @@ +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/plugins/base/icon/icon_picker.dart'; +import 'package:easy_localization/easy_localization.dart'; + +class IconPickerPage extends StatelessWidget { + const IconPickerPage({ + super.key, + this.title, + required this.onSelected, + }); + + final void Function(EmojiPickerResult) onSelected; + final String? title; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: FlowyAppBar( + titleText: title ?? LocaleKeys.titleBar_pageIcon.tr(), + ), + body: SafeArea( + child: FlowyIconPicker(onSelected: onSelected), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_widget.dart deleted file mode 100644 index 630219e060..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:flutter/material.dart'; - -import '../../../generated/flowy_svgs.g.dart'; - -class IconWidget extends StatelessWidget { - const IconWidget({super.key, required this.size, required this.iconsData}); - - final IconsData iconsData; - final double size; - - @override - Widget build(BuildContext context) { - final colorValue = int.tryParse(iconsData.color ?? ''); - Color? color; - if (colorValue != null) { - color = Color(colorValue); - } - final svgString = iconsData.svgString; - if (svgString == null) { - return EmojiText( - emoji: '❓', - fontSize: size, - textAlign: TextAlign.center, - ); - } - return FlowySvg.string( - svgString, - size: Size.square(size), - color: color, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart index ebda487515..1aec6b7037 100644 --- a/frontend/appflowy_flutter/lib/plugins/blank/blank.dart +++ b/frontend/appflowy_flutter/lib/plugins/blank/blank.dart @@ -36,7 +36,7 @@ class BlankPagePlugin extends Plugin { PluginWidgetBuilder get widgetBuilder => BlankPagePluginWidgetBuilder(); @override - PluginId get id => ""; + PluginId get id => "BlankStack"; @override PluginType get pluginType => PluginType.blank; @@ -44,20 +44,16 @@ class BlankPagePlugin extends Plugin { class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder with NavigationItem { - @override - String? get viewName => LocaleKeys.blankPageTitle.tr(); - @override Widget get leftBarItem => FlowyText.medium(LocaleKeys.blankPageTitle.tr()); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem; + Widget tabBarItem(String pluginId) => leftBarItem; @override Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) => const BlankPage(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart index 34c922aaf9..7aad1108b1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart @@ -33,8 +33,6 @@ class ChecklistCellBloc extends Bloc { final ChecklistCellBackendService _checklistCellService; void Function()? _onCellChangedFn; - int? nextPhantomIndex; - @override Future close() async { if (_onCellChangedFn != null) { @@ -54,23 +52,17 @@ class ChecklistCellBloc extends Bloc { const ChecklistCellState( tasks: [], percent: 0, - showIncompleteOnly: false, - phantomIndex: null, + newTask: false, ), ); return; } - final phantomIndex = state.phantomIndex != null - ? nextPhantomIndex ?? state.phantomIndex - : null; emit( state.copyWith( tasks: _makeChecklistSelectOptions(data), percent: data.percentage, - phantomIndex: phantomIndex, ), ); - nextPhantomIndex = null; }, updateTaskName: (option, name) { _updateOption(option, name); @@ -78,28 +70,16 @@ class ChecklistCellBloc extends Bloc { selectTask: (id) async { await _checklistCellService.select(optionId: id); }, - createNewTask: (name, index) async { - await _createTask(name, index); + createNewTask: (name) async { + final result = await _checklistCellService.create(name: name); + result.fold( + (l) => emit(state.copyWith(newTask: true)), + (err) => Log.error(err), + ); }, deleteTask: (id) async { await _deleteOption([id]); }, - reorderTask: (fromIndex, toIndex) async { - await _reorderTask(fromIndex, toIndex, emit); - }, - toggleShowIncompleteOnly: () { - emit(state.copyWith(showIncompleteOnly: !state.showIncompleteOnly)); - }, - updatePhantomIndex: (index) { - emit( - ChecklistCellState( - tasks: state.tasks, - percent: state.percent, - showIncompleteOnly: state.showIncompleteOnly, - phantomIndex: index, - ), - ); - }, ); }, ); @@ -115,31 +95,6 @@ class ChecklistCellBloc extends Bloc { ); } - Future _createTask(String name, int? index) async { - nextPhantomIndex = index == null ? state.tasks.length + 1 : index + 1; - - int? actualIndex = index; - if (index != null && state.showIncompleteOnly) { - int notSelectedTaskCount = 0; - for (int i = 0; i < state.tasks.length; i++) { - if (!state.tasks[i].isSelected) { - notSelectedTaskCount++; - } - - if (notSelectedTaskCount == index) { - actualIndex = i + 1; - break; - } - } - } - - final result = await _checklistCellService.create( - name: name, - index: actualIndex, - ); - result.fold((l) {}, (err) => Log.error(err)); - } - void _updateOption(SelectOptionPB option, String name) async { final result = await _checklistCellService.updateName(option: option, name: name); @@ -150,32 +105,6 @@ class ChecklistCellBloc extends Bloc { final result = await _checklistCellService.delete(optionIds: options); result.fold((l) => null, (err) => Log.error(err)); } - - Future _reorderTask( - int fromIndex, - int toIndex, - Emitter emit, - ) async { - if (fromIndex < toIndex) { - toIndex--; - } - - final tasks = state.showIncompleteOnly - ? state.tasks.where((task) => !task.isSelected).toList() - : state.tasks; - - final fromId = tasks[fromIndex].data.id; - final toId = tasks[toIndex].data.id; - - final newTasks = [...state.tasks]; - newTasks.insert(toIndex, newTasks.removeAt(fromIndex)); - emit(state.copyWith(tasks: newTasks)); - final result = await _checklistCellService.reorder( - fromTaskId: fromId, - toTaskId: toId, - ); - result.fold((l) => null, (err) => Log.error(err)); - } } @freezed @@ -188,17 +117,9 @@ class ChecklistCellEvent with _$ChecklistCellEvent { String name, ) = _UpdateTaskName; const factory ChecklistCellEvent.selectTask(String taskId) = _SelectTask; - const factory ChecklistCellEvent.createNewTask( - String description, { - int? index, - }) = _CreateNewTask; + const factory ChecklistCellEvent.createNewTask(String description) = + _CreateNewTask; const factory ChecklistCellEvent.deleteTask(String taskId) = _DeleteTask; - const factory ChecklistCellEvent.reorderTask(int fromIndex, int toIndex) = - _ReorderTask; - - const factory ChecklistCellEvent.toggleShowIncompleteOnly() = _IncompleteOnly; - const factory ChecklistCellEvent.updatePhantomIndex(int? index) = - _UpdatePhantomIndex; } @freezed @@ -206,8 +127,7 @@ class ChecklistCellState with _$ChecklistCellState { const factory ChecklistCellState({ required List tasks, required double percent, - required bool showIncompleteOnly, - required int? phantomIndex, + required bool newTask, }) = _ChecklistCellState; factory ChecklistCellState.initial(ChecklistCellController cellController) { @@ -216,8 +136,7 @@ class ChecklistCellState with _$ChecklistCellState { return ChecklistCellState( tasks: _makeChecklistSelectOptions(cellData), percent: cellData?.percentage ?? 0, - showIncompleteOnly: false, - phantomIndex: null, + newTask: false, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart index 6f1d57fb50..c5573a9061 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; @@ -7,8 +6,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart' import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'date_cell_editor_bloc.dart'; - part 'date_cell_bloc.freezed.dart'; class DateCellBloc extends Bloc { @@ -19,7 +16,7 @@ class DateCellBloc extends Bloc { } final DateCellController cellController; - VoidCallback? _onCellChangedFn; + void Function()? _onCellChangedFn; @override Future close() async { @@ -38,19 +35,15 @@ class DateCellBloc extends Bloc { (event, emit) async { event.when( didReceiveCellUpdate: (DateCellDataPB? cellData) { - final dateCellData = DateCellData.fromPB(cellData); emit( state.copyWith( - cellData: dateCellData, + data: cellData, + dateStr: _dateStrFromCellData(cellData), ), ); }, didUpdateField: (fieldInfo) { - emit( - state.copyWith( - fieldInfo: fieldInfo, - ), - ); + emit(state.copyWith(fieldInfo: fieldInfo)); }, ); }, @@ -86,16 +79,41 @@ class DateCellEvent with _$DateCellEvent { @freezed class DateCellState with _$DateCellState { const factory DateCellState({ + required DateCellDataPB? data, + required String dateStr, required FieldInfo fieldInfo, - required DateCellData cellData, }) = _DateCellState; - factory DateCellState.initial(DateCellController cellController) { - final cellData = DateCellData.fromPB(cellController.getCellData()); + factory DateCellState.initial(DateCellController context) { + final cellData = context.getCellData(); return DateCellState( - fieldInfo: cellController.fieldInfo, - cellData: cellData, + fieldInfo: context.fieldInfo, + data: cellData, + dateStr: _dateStrFromCellData(cellData), ); } } + +String _dateStrFromCellData(DateCellDataPB? cellData) { + if (cellData == null || !cellData.hasTimestamp()) { + return ""; + } + + String dateStr = ""; + if (cellData.isRange) { + if (cellData.includeTime) { + dateStr = + "${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}"; + } else { + dateStr = "${cellData.date} → ${cellData.endDate}"; + } + } else { + if (cellData.includeTime) { + dateStr = "${cellData.date} ${cellData.time}"; + } else { + dateStr = cellData.date; + } + } + return dateStr.trim(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart index 8f0c37fb0d..879e759f35 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/domain/date_cell_service.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; @@ -12,17 +11,18 @@ import 'package:appflowy/util/int64_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; import 'package:fixnum/fixnum.dart'; +import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:nanoid/non_secure.dart'; -import 'package:protobuf/protobuf.dart' hide FieldInfo; +import 'package:protobuf/protobuf.dart'; part 'date_cell_editor_bloc.freezed.dart'; @@ -45,7 +45,6 @@ class DateCellEditorBloc final DateCellBackendService _dateCellBackendService; final DateCellController cellController; final ReminderBloc _reminderBloc; - void Function()? _onCellChangedFn; void _dispatch() { @@ -53,216 +52,303 @@ class DateCellEditorBloc (event, emit) async { await event.when( didReceiveCellUpdate: (DateCellDataPB? cellData) { - final dateCellData = DateCellData.fromPB(cellData); + final dateCellData = _dateDataFromCellData(cellData); + final endDay = + dateCellData.isRange == state.isRange && dateCellData.isRange + ? dateCellData.endDateTime + : null; + ReminderOption option = state.reminderOption; - ReminderOption reminderOption = ReminderOption.none; + if (dateCellData.dateTime != null && + (state.reminderId?.isEmpty ?? true) && + (dateCellData.reminderId?.isNotEmpty ?? false) && + state.reminderOption != ReminderOption.none) { + final date = state.reminderOption.withoutTime + ? dateCellData.dateTime!.withoutTime + : dateCellData.dateTime!; - if (dateCellData.reminderId.isNotEmpty && + // Add Reminder + _reminderBloc.add( + ReminderEvent.addById( + reminderId: dateCellData.reminderId!, + objectId: cellController.viewId, + meta: { + ReminderMetaKeys.includeTime: true.toString(), + ReminderMetaKeys.rowId: cellController.rowId, + }, + scheduledAt: Int64( + state.reminderOption + .fromDate(date) + .millisecondsSinceEpoch ~/ + 1000, + ), + ), + ); + } + + if ((dateCellData.reminderId?.isNotEmpty ?? false) && dateCellData.dateTime != null) { - final reminder = _reminderBloc.state.reminders - .firstWhereOrNull((r) => r.id == dateCellData.reminderId); - if (reminder != null) { - reminderOption = ReminderOption.fromDateDifference( - dateCellData.dateTime!, - reminder.scheduledAt.toDateTime(), - ); + if (option.requiresNoTime && dateCellData.includeTime) { + option = ReminderOption.atTimeOfEvent; + } else if (!option.withoutTime && !dateCellData.includeTime) { + option = ReminderOption.onDayOfEvent; } + + final date = option.withoutTime + ? dateCellData.dateTime!.withoutTime + : dateCellData.dateTime!; + + final scheduledAt = option.fromDate(date); + + // Update Reminder + _reminderBloc.add( + ReminderEvent.update( + ReminderUpdate( + id: dateCellData.reminderId!, + scheduledAt: scheduledAt, + includeTime: true, + ), + ), + ); } emit( state.copyWith( dateTime: dateCellData.dateTime, + timeStr: dateCellData.timeStr, endDateTime: dateCellData.endDateTime, + endTimeStr: dateCellData.endTimeStr, includeTime: dateCellData.includeTime, isRange: dateCellData.isRange, + startDay: dateCellData.isRange ? dateCellData.dateTime : null, + endDay: endDay, + dateStr: dateCellData.dateStr, + endDateStr: dateCellData.endDateStr, reminderId: dateCellData.reminderId, - reminderOption: reminderOption, + reminderOption: option, ), ); }, - didUpdateField: (field) { - final typeOption = DateTypeOptionDataParser() - .fromBuffer(field.field.typeOptionData); - emit(state.copyWith(dateTypeOptionPB: typeOption)); + didReceiveTimeFormatError: ( + String? parseTimeError, + String? parseEndTimeError, + ) { + emit( + state.copyWith( + parseTimeError: parseTimeError, + parseEndTimeError: parseEndTimeError, + ), + ); }, - updateDateTime: (date) async { - if (state.isRange) { - return; - } - await _updateDateData(date: date); - }, - updateDateRange: (DateTime start, DateTime end) async { + selectDay: (date) async { if (!state.isRange) { - return; + await _updateDateData(date: date); } - await _updateDateData(date: start, endDate: end); }, - setIncludeTime: (includeTime, dateTime, endDateTime) async { - await _updateIncludeTime(includeTime, dateTime, endDateTime); + setIncludeTime: (includeTime) async => + _updateDateData(includeTime: includeTime), + setIsRange: (isRange) async => _updateDateData(isRange: isRange), + setTime: (timeStr) async { + emit(state.copyWith(timeStr: timeStr)); + await _updateDateData(timeStr: timeStr); }, - setIsRange: (isRange, dateTime, endDateTime) async { - await _updateIsRange(isRange, dateTime, endDateTime); + selectDateRange: (DateTime? start, DateTime? end) async { + if (end == null && state.startDay != null && state.endDay == null) { + final (newStart, newEnd) = state.startDay!.isBefore(start!) + ? (state.startDay!, start) + : (start, state.startDay!); + + emit(state.copyWith(startDay: null, endDay: null)); + + await _updateDateData(date: newStart.date, endDate: newEnd.date); + } else if (end == null) { + emit(state.copyWith(startDay: start, endDay: null)); + } else { + await _updateDateData(date: start!.date, endDate: end.date); + } }, - setDateFormat: (DateFormatPB dateFormat) async { - await _updateTypeOption(emit, dateFormat: dateFormat); + setStartDay: (DateTime startDay) async { + if (state.endDay == null) { + emit(state.copyWith(startDay: startDay)); + } else if (startDay.isAfter(state.endDay!)) { + emit(state.copyWith(startDay: startDay, endDay: null)); + } else { + emit(state.copyWith(startDay: startDay)); + await _updateDateData( + date: startDay.date, + endDate: state.endDay!.date, + ); + } }, - setTimeFormat: (TimeFormatPB timeFormat) async { - await _updateTypeOption(emit, timeFormat: timeFormat); + setEndDay: (DateTime endDay) { + if (state.startDay == null) { + emit(state.copyWith(endDay: endDay)); + } else if (endDay.isBefore(state.startDay!)) { + emit(state.copyWith(startDay: null, endDay: endDay)); + } else { + emit(state.copyWith(endDay: endDay)); + _updateDateData(date: state.startDay!.date, endDate: endDay.date); + } }, + setEndTime: (String? endTime) async { + emit(state.copyWith(endTimeStr: endTime)); + await _updateDateData(endTimeStr: endTime); + }, + setDateFormat: (DateFormatPB dateFormat) async => + await _updateTypeOption(emit, dateFormat: dateFormat), + setTimeFormat: (TimeFormatPB timeFormat) async => + await _updateTypeOption(emit, timeFormat: timeFormat), clearDate: () async { // Remove reminder if neccessary - if (state.reminderId.isNotEmpty) { + if (state.reminderId != null) { _reminderBloc - .add(ReminderEvent.remove(reminderId: state.reminderId)); + .add(ReminderEvent.remove(reminderId: state.reminderId!)); } await _clearDate(); }, - setReminderOption: (ReminderOption option) async { - await _setReminderOption(option); + setReminderOption: ( + ReminderOption option, + DateTime? selectedDay, + ) async { + if (state.reminderId?.isEmpty ?? + true && + (state.dateTime != null || selectedDay != null) && + option != ReminderOption.none) { + // New Reminder + final reminderId = nanoid(); + await _updateDateData(reminderId: reminderId, date: selectedDay); + + emit( + state.copyWith(reminderOption: option, dateTime: selectedDay), + ); + } else if (option == ReminderOption.none && + (state.reminderId?.isNotEmpty ?? false)) { + // Remove reminder + _reminderBloc + .add(ReminderEvent.remove(reminderId: state.reminderId!)); + await _updateDateData(reminderId: ""); + emit(state.copyWith(reminderOption: option)); + } else if (state.dateTime != null && + (state.reminderId?.isNotEmpty ?? false)) { + final scheduledAt = option.fromDate(state.dateTime!); + + // Update reminder + _reminderBloc.add( + ReminderEvent.update( + ReminderUpdate( + id: state.reminderId!, + scheduledAt: scheduledAt, + includeTime: true, + ), + ), + ); + } }, + // Empty String signifies no reminder + removeReminder: () async => _updateDateData(reminderId: ""), ); }, ); } - Future> _updateDateData({ + Future _updateDateData({ DateTime? date, + String? timeStr, DateTime? endDate, - bool updateReminderIfNecessary = true, + String? endTimeStr, + bool? includeTime, + bool? isRange, + String? reminderId, }) async { - final result = await _dateCellBackendService.update( - date: date, - endDate: endDate, + // make sure that not both date and time are updated at the same time + assert( + !(date != null && timeStr != null) || + !(endDate != null && endTimeStr != null), ); - if (updateReminderIfNecessary) { - result.onSuccess((_) => _updateReminderIfNecessary(date)); - } - return result; - } - Future _updateIsRange( - bool isRange, - DateTime? dateTime, - DateTime? endDateTime, - ) { - return _dateCellBackendService - .update( - date: dateTime, - endDate: endDateTime, - isRange: isRange, - ) - .fold((s) => _updateReminderIfNecessary(dateTime), Log.error); - } + // if not updating the time, use the old time in the state + final String? newTime = timeStr ?? state.timeStr; + final DateTime? newDate = timeStr != null && timeStr.isNotEmpty + ? state.dateTime ?? DateTime.now() + : _utcToLocalAndAddCurrentTime(date); - Future _updateIncludeTime( - bool includeTime, - DateTime? dateTime, - DateTime? endDateTime, - ) { - return _dateCellBackendService - .update( - date: dateTime, - endDate: endDateTime, - includeTime: includeTime, - ) - .fold((s) => _updateReminderIfNecessary(dateTime), Log.error); + // if not updating the time, use the old time in the state + final String? newEndTime = endTimeStr ?? state.endTimeStr; + final DateTime? newEndDate = endTimeStr != null && endTimeStr.isNotEmpty + ? state.endDateTime ?? DateTime.now() + : _utcToLocalAndAddCurrentTime(endDate); + + final result = await _dateCellBackendService.update( + date: newDate, + time: newTime, + endDate: newEndDate, + endTime: newEndTime, + includeTime: includeTime ?? state.includeTime, + isRange: isRange ?? state.isRange, + reminderId: reminderId ?? state.reminderId, + ); + + result.fold( + (_) { + if (!isClosed && + (state.parseEndTimeError != null || state.parseTimeError != null)) { + add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null)); + } + }, + (err) { + switch (err.code) { + case ErrorCode.InvalidDateTimeFormat: + if (isClosed) { + return; + } + + // to determine which textfield should show error + final (startError, endError) = newDate != null + ? (timeFormatPrompt(err), null) + : (null, timeFormatPrompt(err)); + + add( + DateCellEditorEvent.didReceiveTimeFormatError( + startError, + endError, + ), + ); + break; + default: + Log.error(err); + } + }, + ); } Future _clearDate() async { final result = await _dateCellBackendService.clear(); - result.onFailure(Log.error); - } - - Future _setReminderOption(ReminderOption option) async { - if (state.reminderId.isEmpty) { - if (option == ReminderOption.none) { - // do nothing - return; - } - // if no date, fill it first - final fillerDateTime = - state.includeTime ? DateTime.now() : DateTime.now().withoutTime; - if (state.dateTime == null) { - final result = await _updateDateData( - date: fillerDateTime, - endDate: state.isRange ? fillerDateTime : null, - updateReminderIfNecessary: false, - ); - // return if filling date is unsuccessful - if (result.isFailure) { - return; + result.fold( + (_) { + if (!isClosed) { + add(const DateCellEditorEvent.didReceiveTimeFormatError(null, null)); } - } - - // create a reminder - final reminderId = nanoid(); - await _updateCellReminderId(reminderId); - final dateTime = state.dateTime ?? fillerDateTime; - _reminderBloc.add( - ReminderEvent.addById( - reminderId: reminderId, - objectId: cellController.viewId, - meta: { - ReminderMetaKeys.includeTime: state.includeTime.toString(), - ReminderMetaKeys.rowId: cellController.rowId, - }, - scheduledAt: Int64( - option.getNotificationDateTime(dateTime).millisecondsSinceEpoch ~/ - 1000, - ), - ), - ); - } else { - if (option == ReminderOption.none) { - // remove reminder from reminder bloc and cell data - _reminderBloc.add(ReminderEvent.remove(reminderId: state.reminderId)); - await _updateCellReminderId(""); - } else { - // Update reminder - final scheduledAt = option.getNotificationDateTime(state.dateTime!); - _reminderBloc.add( - ReminderEvent.update( - ReminderUpdate( - id: state.reminderId, - scheduledAt: scheduledAt, - includeTime: state.includeTime, - ), - ), - ); - } - } - } - - Future _updateCellReminderId( - String reminderId, - ) async { - final result = await _dateCellBackendService.update( - reminderId: reminderId, + }, + (err) => Log.error(err), ); - result.onFailure(Log.error); } - void _updateReminderIfNecessary( - DateTime? dateTime, - ) { - if (state.reminderId.isEmpty || - state.reminderOption == ReminderOption.none || - dateTime == null) { - return; + DateTime? _utcToLocalAndAddCurrentTime(DateTime? date) { + if (date == null) { + return null; } - - final scheduledAt = state.reminderOption.getNotificationDateTime(dateTime); - - // Update Reminder - _reminderBloc.add( - ReminderEvent.update( - ReminderUpdate( - id: state.reminderId, - scheduledAt: scheduledAt, - includeTime: state.includeTime, - ), - ), + final now = DateTime.now(); + // the incoming date is Utc. This trick converts it into Local + // and add the current time. The time may be overwritten by + // explicitly provided time string in the backend though + return DateTime( + date.year, + date.month, + date.day, + now.hour, + now.minute, + now.second, ); } @@ -281,7 +367,6 @@ class DateCellEditorBloc if (_onCellChangedFn != null) { cellController.removeListener( onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, ); } return super.close(); @@ -294,17 +379,10 @@ class DateCellEditorBloc add(DateCellEditorEvent.didReceiveCellUpdate(cell)); } }, - onFieldChanged: _onFieldChangedListener, ); } - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(DateCellEditorEvent.didUpdateField(fieldInfo)); - } - } - - Future _updateTypeOption( + Future? _updateTypeOption( Emitter emit, { DateFormatPB? dateFormat, TimeFormatPB? timeFormat, @@ -326,43 +404,61 @@ class DateCellEditorBloc typeOptionData: newDateTypeOption.writeToBuffer(), ); - result.onFailure(Log.error); + result.fold( + (_) => emit( + state.copyWith( + dateTypeOptionPB: newDateTypeOption, + timeHintText: _timeHintText(newDateTypeOption), + ), + ), + (err) => Log.error(err), + ); } } @freezed class DateCellEditorEvent with _$DateCellEditorEvent { - const factory DateCellEditorEvent.didUpdateField( - FieldInfo fieldInfo, - ) = _DidUpdateField; - // notification that cell is updated in the backend const factory DateCellEditorEvent.didReceiveCellUpdate( DateCellDataPB? data, ) = _DidReceiveCellUpdate; - const factory DateCellEditorEvent.updateDateTime(DateTime day) = - _UpdateDateTime; + const factory DateCellEditorEvent.didReceiveTimeFormatError( + String? parseTimeError, + String? parseEndTimeError, + ) = _DidReceiveTimeFormatError; - const factory DateCellEditorEvent.updateDateRange( - DateTime start, - DateTime end, - ) = _UpdateDateRange; + // date cell data is modified + const factory DateCellEditorEvent.selectDay(DateTime day) = _SelectDay; - const factory DateCellEditorEvent.setIncludeTime( - bool includeTime, - DateTime? dateTime, - DateTime? endDateTime, - ) = _IncludeTime; + const factory DateCellEditorEvent.selectDateRange( + DateTime? start, + DateTime? end, + ) = _SelectDateRange; - const factory DateCellEditorEvent.setIsRange( - bool isRange, - DateTime? dateTime, - DateTime? endDateTime, - ) = _SetIsRange; + const factory DateCellEditorEvent.setStartDay( + DateTime startDay, + ) = _SetStartDay; - const factory DateCellEditorEvent.setReminderOption(ReminderOption option) = - _SetReminderOption; + const factory DateCellEditorEvent.setEndDay( + DateTime endDay, + ) = _SetEndDay; + + const factory DateCellEditorEvent.setTime(String time) = _SetTime; + + const factory DateCellEditorEvent.setEndTime(String endTime) = _SetEndTime; + + const factory DateCellEditorEvent.setIncludeTime(bool includeTime) = + _IncludeTime; + + const factory DateCellEditorEvent.setIsRange(bool isRange) = _SetIsRange; + + const factory DateCellEditorEvent.setReminderOption({ + required ReminderOption option, + @Default(null) DateTime? selectedDay, + }) = _SetReminderOption; + + const factory DateCellEditorEvent.removeReminder() = _RemoveReminder; // date field type options are modified const factory DateCellEditorEvent.setTimeFormat(TimeFormatPB timeFormat) = @@ -380,12 +476,25 @@ class DateCellEditorState with _$DateCellEditorState { // the date field's type option required DateTypeOptionPB dateTypeOptionPB, + // used when selecting a date range + required DateTime? startDay, + required DateTime? endDay, + // cell data from the backend required DateTime? dateTime, required DateTime? endDateTime, + required String? timeStr, + required String? endTimeStr, required bool includeTime, required bool isRange, - required String reminderId, + required String? dateStr, + required String? endDateStr, + required String? reminderId, + + // error and hint text + required String? parseTimeError, + required String? parseEndTimeError, + required String timeHintText, @Default(ReminderOption.none) ReminderOption reminderOption, }) = _DateCellEditorState; @@ -395,11 +504,11 @@ class DateCellEditorState with _$DateCellEditorState { ) { final typeOption = controller.getTypeOption(DateTypeOptionDataParser()); final cellData = controller.getCellData(); - final dateCellData = DateCellData.fromPB(cellData); + final dateCellData = _dateDataFromCellData(cellData); ReminderOption reminderOption = ReminderOption.none; - - if (dateCellData.reminderId.isNotEmpty && dateCellData.dateTime != null) { + if ((dateCellData.reminderId?.isNotEmpty ?? false) && + dateCellData.dateTime != null) { final reminder = reminderBloc.state.reminders .firstWhereOrNull((r) => r.id == dateCellData.reminderId); if (reminder != null) { @@ -415,60 +524,114 @@ class DateCellEditorState with _$DateCellEditorState { return DateCellEditorState( dateTypeOptionPB: typeOption, + startDay: dateCellData.isRange ? dateCellData.dateTime : null, + endDay: dateCellData.isRange ? dateCellData.endDateTime : null, dateTime: dateCellData.dateTime, endDateTime: dateCellData.endDateTime, + timeStr: dateCellData.timeStr, + endTimeStr: dateCellData.endTimeStr, + dateStr: dateCellData.dateStr, + endDateStr: dateCellData.endDateStr, includeTime: dateCellData.includeTime, isRange: dateCellData.isRange, + parseTimeError: null, + parseEndTimeError: null, + timeHintText: _timeHintText(typeOption), reminderId: dateCellData.reminderId, reminderOption: reminderOption, ); } } -/// Helper class to parse ProtoBuf payloads into DateCellEditorState -class DateCellData { - const DateCellData({ - required this.dateTime, - required this.endDateTime, - required this.includeTime, - required this.isRange, - required this.reminderId, - }); +String _timeHintText(DateTypeOptionPB typeOption) { + switch (typeOption.timeFormat) { + case TimeFormatPB.TwelveHour: + return LocaleKeys.document_date_timeHintTextInTwelveHour.tr(); + case TimeFormatPB.TwentyFourHour: + return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr(); + default: + return ""; + } +} - const DateCellData.empty() - : dateTime = null, - endDateTime = null, - includeTime = false, - isRange = false, - reminderId = ""; - - factory DateCellData.fromPB(DateCellDataPB? cellData) { - // a null DateCellDataPB may be returned, indicating that all the fields are - // their default values: empty strings and false booleans - if (cellData == null) { - return const DateCellData.empty(); - } - - final dateTime = - cellData.hasTimestamp() ? cellData.timestamp.toDateTime() : null; - final endDateTime = dateTime == null || !cellData.isRange - ? null - : cellData.hasEndTimestamp() - ? cellData.endTimestamp.toDateTime() - : null; - - return DateCellData( - dateTime: dateTime, - endDateTime: endDateTime, - includeTime: cellData.includeTime, - isRange: cellData.isRange, - reminderId: cellData.reminderId, +_DateCellData _dateDataFromCellData( + DateCellDataPB? cellData, +) { + // a null DateCellDataPB may be returned, indicating that all the fields are + // their default values: empty strings and false booleans + if (cellData == null) { + return _DateCellData( + dateTime: null, + endDateTime: null, + timeStr: null, + endTimeStr: null, + includeTime: false, + isRange: false, + dateStr: null, + endDateStr: null, + reminderId: null, ); } + DateTime? dateTime; + String? timeStr; + DateTime? endDateTime; + String? endTimeStr; + + String? endDateStr; + if (cellData.hasTimestamp()) { + final timestamp = cellData.timestamp * 1000; + dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); + timeStr = cellData.time; + if (cellData.hasEndTimestamp()) { + final endTimestamp = cellData.endTimestamp * 1000; + endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt()); + endTimeStr = cellData.endTime; + } + } + + final bool includeTime = cellData.includeTime; + final bool isRange = cellData.isRange; + + if (cellData.isRange) { + endDateStr = cellData.endDate; + } + + final String dateStr = cellData.date; + + return _DateCellData( + dateTime: dateTime, + endDateTime: endDateTime, + timeStr: timeStr, + endTimeStr: endTimeStr, + includeTime: includeTime, + isRange: isRange, + dateStr: dateStr, + endDateStr: endDateStr, + reminderId: cellData.reminderId, + ); +} + +class _DateCellData { + _DateCellData({ + required this.dateTime, + required this.endDateTime, + required this.timeStr, + required this.endTimeStr, + required this.includeTime, + required this.isRange, + required this.dateStr, + required this.endDateStr, + required this.reminderId, + }); + final DateTime? dateTime; final DateTime? endDateTime; + final String? timeStr; + final String? endTimeStr; final bool includeTime; final bool isRange; - final String reminderId; + final String? dateStr; + final String? endDateStr; + final String? reminderId; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart deleted file mode 100644 index 7a3075e0f2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/media_cell_bloc.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'media_cell_bloc.freezed.dart'; - -class MediaCellBloc extends Bloc { - MediaCellBloc({ - required this.cellController, - }) : super(MediaCellState.initial(cellController)) { - _dispatch(); - _startListening(); - } - - late final RowBackendService _rowService = - RowBackendService(viewId: cellController.viewId); - final MediaCellController cellController; - - void Function()? _onCellChangedFn; - - String get databaseId => cellController.viewId; - String get rowId => cellController.rowId; - bool get wrapContent => cellController.fieldInfo.wrapCellContent ?? false; - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener( - onCellChanged: _onCellChangedFn!, - onFieldChanged: _onFieldChangedListener, - ); - } - await cellController.dispose(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - // Fetch user profile - final userProfileResult = - await UserBackendService.getCurrentUserProfile(); - userProfileResult.fold( - (userProfile) => emit(state.copyWith(userProfile: userProfile)), - (l) => Log.error(l), - ); - }, - didUpdateCell: (files) { - emit(state.copyWith(files: files)); - }, - didUpdateField: (fieldName) { - final typeOption = - cellController.getTypeOption(MediaTypeOptionDataParser()); - - emit( - state.copyWith( - fieldName: fieldName, - hideFileNames: typeOption.hideFileNames, - ), - ); - }, - addFile: (url, name, uploadType, fileType) async { - final newFile = MediaFilePB( - id: uuid(), - url: url, - name: name, - uploadType: uploadType, - fileType: fileType, - ); - - final payload = MediaCellChangesetPB( - viewId: cellController.viewId, - cellId: CellIdPB( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - insertedFiles: [newFile], - removedIds: [], - ); - - final result = await DatabaseEventUpdateMediaCell(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); - }, - removeFile: (id) async { - final payload = MediaCellChangesetPB( - viewId: cellController.viewId, - cellId: CellIdPB( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - insertedFiles: [], - removedIds: [id], - ); - - final result = await DatabaseEventUpdateMediaCell(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); - }, - reorderFiles: (from, to) async { - final files = List.from(state.files); - files.insert(to, files.removeAt(from)); - - // We emit the new state first to update the UI - emit(state.copyWith(files: files)); - - final payload = MediaCellChangesetPB( - viewId: cellController.viewId, - cellId: CellIdPB( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - insertedFiles: files, - // In the backend we remove all files by id before we do inserts. - // So this will effectively reorder the files. - removedIds: files.map((file) => file.id).toList(), - ); - - final result = await DatabaseEventUpdateMediaCell(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); - }, - renameFile: (fileId, name) async { - final payload = RenameMediaChangesetPB( - viewId: cellController.viewId, - cellId: CellIdPB( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - fileId: fileId, - name: name, - ); - - final result = await DatabaseEventRenameMediaFile(payload).send(); - result.fold((l) => null, (err) => Log.error(err)); - }, - toggleShowAllFiles: () { - emit(state.copyWith(showAllFiles: !state.showAllFiles)); - }, - setCover: (cover) => _rowService.updateMeta( - rowId: cellController.rowId, - cover: cover, - ), - ); - }, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (cellData) { - if (!isClosed) { - add(MediaCellEvent.didUpdateCell(cellData?.files ?? const [])); - } - }, - onFieldChanged: _onFieldChangedListener, - ); - } - - void _onFieldChangedListener(FieldInfo fieldInfo) { - if (!isClosed) { - add(MediaCellEvent.didUpdateField(fieldInfo.name)); - } - } - - void renameFile(String fileId, String name) => - add(MediaCellEvent.renameFile(fileId: fileId, name: name)); - - void deleteFile(String fileId) => - add(MediaCellEvent.removeFile(fileId: fileId)); -} - -@freezed -class MediaCellEvent with _$MediaCellEvent { - const factory MediaCellEvent.initial() = _Initial; - - const factory MediaCellEvent.didUpdateCell(List files) = - _DidUpdateCell; - - const factory MediaCellEvent.didUpdateField(String fieldName) = - _DidUpdateField; - - const factory MediaCellEvent.addFile({ - required String url, - required String name, - required FileUploadTypePB uploadType, - required MediaFileTypePB fileType, - }) = _AddFile; - - const factory MediaCellEvent.removeFile({ - required String fileId, - }) = _RemoveFile; - - const factory MediaCellEvent.reorderFiles({ - required int from, - required int to, - }) = _ReorderFiles; - - const factory MediaCellEvent.renameFile({ - required String fileId, - required String name, - }) = _RenameFile; - - const factory MediaCellEvent.toggleShowAllFiles() = _ToggleShowAllFiles; - - const factory MediaCellEvent.setCover(RowCoverPB cover) = _SetCover; -} - -@freezed -class MediaCellState with _$MediaCellState { - const factory MediaCellState({ - UserProfilePB? userProfile, - required String fieldName, - @Default([]) List files, - @Default(false) showAllFiles, - @Default(true) hideFileNames, - }) = _MediaCellState; - - factory MediaCellState.initial(MediaCellController cellController) { - final cellData = cellController.getCellData(); - final typeOption = - cellController.getTypeOption(MediaTypeOptionDataParser()); - - return MediaCellState( - fieldName: cellController.fieldInfo.field.name, - files: cellData?.files ?? const [], - hideFileNames: typeOption.hideFileNames, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart index 73b2d2977b..df159b817b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart @@ -47,6 +47,15 @@ class NumberCellBloc extends Bloc { if (state.content != text) { emit(state.copyWith(content: text)); await cellController.saveCellData(text); + + // If the input content is "abc" that can't parsered as number then the data stored in the backend will be an empty string. + // So for every cell data that will be formatted in the backend. + // It needs to get the formatted data after saving. + add( + NumberCellEvent.didReceiveCellUpdate( + cellController.getCellData(), + ), + ); } }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart index ec789b03a0..70c5e074ab 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart @@ -143,11 +143,12 @@ class RelationCellBloc extends Bloc { (f) => null, ); if (databaseMeta != null) { - final result = await ViewBackendService.getView(databaseMeta.viewId); + final result = + await ViewBackendService.getView(databaseMeta.inlineViewId); return result.fold( (s) => DatabaseMeta( databaseId: databaseId, - viewId: databaseMeta.viewId, + inlineViewId: databaseMeta.inlineViewId, databaseName: s.name, ), (f) => null, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart index c6e4e6484b..c0bfa48a85 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/widgets.dart'; - import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart'; @@ -11,6 +9,7 @@ import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -110,16 +109,16 @@ class SelectOptionCellEditorBloc selectOption: (optionId) async { await _selectOptionService.select(optionIds: [optionId]); }, - unselectOption: (optionId) async { - await _selectOptionService.unselect(optionIds: [optionId]); + unSelectOption: (optionId) async { + await _selectOptionService.unSelect(optionIds: [optionId]); }, - unselectLastOption: () async { + unSelectLastOption: () async { if (state.selectedOptions.isEmpty) { return; } final lastSelectedOptionId = state.selectedOptions.last.id; await _selectOptionService - .unselect(optionIds: [lastSelectedOptionId]); + .unSelect(optionIds: [lastSelectedOptionId]); }, submitTextField: () { _submitTextFieldValue(emit); @@ -241,11 +240,6 @@ class SelectOptionCellEditorBloc } else if (!state.selectedOptions .any((option) => option.id == focusedOptionId)) { _selectOptionService.select(optionIds: [focusedOptionId]); - emit( - state.copyWith( - clearFilter: true, - ), - ); } } @@ -359,10 +353,10 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { const factory SelectOptionCellEditorEvent.createOption() = _CreateOption; const factory SelectOptionCellEditorEvent.selectOption(String optionId) = _SelectOption; - const factory SelectOptionCellEditorEvent.unselectOption(String optionId) = - _UnselectOption; - const factory SelectOptionCellEditorEvent.unselectLastOption() = - _UnselectLastOption; + const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) = + _UnSelectOption; + const factory SelectOptionCellEditorEvent.unSelectLastOption() = + _UnSelectLastOption; const factory SelectOptionCellEditorEvent.updateOption( SelectOptionPB option, ) = _UpdateOption; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart index 7960b34d7c..22baf26599 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -34,7 +33,7 @@ class TextCellBloc extends Bloc { on( (event, emit) { event.when( - didReceiveCellUpdate: (content) { + didReceiveCellUpdate: (String content) { emit(state.copyWith(content: content)); }, didUpdateField: (fieldInfo) { @@ -43,10 +42,11 @@ class TextCellBloc extends Bloc { emit(state.copyWith(wrap: wrap)); } }, + didUpdateEmoji: (String emoji) { + emit(state.copyWith(emoji: emoji)); + }, updateText: (String text) { - // If the content is null, it indicates that either the cell is empty (no data) - // or the cell data is still being fetched from the backend and is not yet available. - if (state.content != null && state.content != text) { + if (state.content != text) { cellController.saveCellData(text, debounce: true); } }, @@ -62,10 +62,17 @@ class TextCellBloc extends Bloc { _onCellChangedFn = cellController.addListener( onCellChanged: (cellContent) { if (!isClosed) { - add(TextCellEvent.didReceiveCellUpdate(cellContent)); + add(TextCellEvent.didReceiveCellUpdate(cellContent ?? "")); } }, onFieldChanged: _onFieldChangedListener, + onRowMetaChanged: cellController.fieldInfo.isPrimary + ? () { + if (!isClosed) { + add(TextCellEvent.didUpdateEmoji(cellController.icon ?? "")); + } + } + : null, ); } @@ -78,39 +85,34 @@ class TextCellBloc extends Bloc { @freezed class TextCellEvent with _$TextCellEvent { - const factory TextCellEvent.didReceiveCellUpdate(String? cellContent) = + const factory TextCellEvent.didReceiveCellUpdate(String cellContent) = _DidReceiveCellUpdate; const factory TextCellEvent.didUpdateField(FieldInfo fieldInfo) = _DidUpdateField; const factory TextCellEvent.updateText(String text) = _UpdateText; const factory TextCellEvent.enableEdit(bool enabled) = _EnableEdit; + const factory TextCellEvent.didUpdateEmoji(String emoji) = _UpdateEmoji; } @freezed class TextCellState with _$TextCellState { const factory TextCellState({ - required String? content, - required ValueNotifier? emoji, - required ValueNotifier? hasDocument, + required String content, + required String emoji, required bool enableEdit, required bool wrap, }) = _TextCellState; factory TextCellState.initial(TextCellController cellController) { - final cellData = cellController.getCellData(); + final cellData = cellController.getCellData() ?? ""; final wrap = cellController.fieldInfo.wrapCellContent ?? true; - ValueNotifier? emoji; - ValueNotifier? hasDocument; - if (cellController.fieldInfo.isPrimary) { - emoji = cellController.icon; - hasDocument = cellController.hasDocument; - } + final emoji = + cellController.fieldInfo.isPrimary ? cellController.icon ?? "" : ""; return TextCellState( content: cellData, emoji: emoji, enableEdit: false, - hasDocument: hasDocument, wrap: wrap, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart index d9009b2ba0..858acadc5a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller.dart @@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/domain/cell_listener.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/domain/row_meta_listener.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -60,8 +61,10 @@ class CellController { final CellDataPersistence _cellDataPersistence; CellListener? _cellListener; + RowMetaListener? _rowMetaListener; CellDataNotifier? _cellDataNotifier; + VoidCallback? _onRowMetaChanged; Timer? _loadDataOperation; Timer? _saveDataOperation; @@ -72,9 +75,8 @@ class CellController { FieldInfo get fieldInfo => _fieldController.getField(_cellContext.fieldId)!; FieldType get fieldType => _fieldController.getField(_cellContext.fieldId)!.fieldType; - ValueNotifier? get icon => _rowCache.getRow(rowId)?.rowIconNotifier; - ValueNotifier? get hasDocument => - _rowCache.getRow(rowId)?.rowDocumentNotifier; + RowMetaPB? get rowMeta => _rowCache.getRow(rowId)?.rowMeta; + String? get icon => rowMeta?.icon; CellMemCache get _cellCache => _rowCache.cellCache; /// casting method for painless type coersion @@ -105,12 +107,23 @@ class CellController { fieldId, onFieldChanged: _onFieldChangedListener, ); + + // 3. If the field is primary listen to row meta changes. + if (fieldInfo.field.isPrimary) { + _rowMetaListener = RowMetaListener(_cellContext.rowId); + _rowMetaListener?.start( + callback: (newRowMeta) { + _onRowMetaChanged?.call(); + }, + ); + } } /// Add a new listener VoidCallback? addListener({ required void Function(T?) onCellChanged, void Function(FieldInfo fieldInfo)? onFieldChanged, + VoidCallback? onRowMetaChanged, }) { /// an adaptor for the onCellChanged listener void onCellChangedFn() => onCellChanged(_cellDataNotifier?.value); @@ -123,6 +136,8 @@ class CellController { ); } + _onRowMetaChanged = onRowMetaChanged; + // Return the function pointer that can be used when calling removeListener. return onCellChangedFn; } @@ -218,6 +233,9 @@ class CellController { } Future dispose() async { + await _rowMetaListener?.stop(); + _rowMetaListener = null; + await _cellListener?.stop(); _cellListener = null; @@ -231,6 +249,7 @@ class CellController { _saveDataOperation?.cancel(); _cellDataNotifier?.dispose(); _cellDataNotifier = null; + _onRowMetaChanged = null; } } @@ -241,11 +260,15 @@ class CellDataNotifier extends ChangeNotifier { bool Function(T? oldValue, T? newValue)? listenWhen; set value(T newValue) { - if (listenWhen != null && !listenWhen!.call(_value, newValue)) { - return; + if (listenWhen?.call(_value, newValue) ?? false) { + _value = newValue; + notifyListeners(); + } else { + if (_value != newValue) { + _value = newValue; + notifyListeners(); + } } - _value = newValue; - notifyListeners(); } T get value => _value; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart index afe05e8b70..50ef7ccb74 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart @@ -18,7 +18,6 @@ typedef RelationCellController = CellController; typedef SummaryCellController = CellController; typedef TimeCellController = CellController; typedef TranslateCellController = CellController; -typedef MediaCellController = CellController; CellController makeCellController( DatabaseController databaseController, @@ -171,18 +170,6 @@ CellController makeCellController( ), cellDataPersistence: TextCellDataPersistence(), ); - case FieldType.Media: - return MediaCellController( - viewId: viewId, - fieldController: fieldController, - cellContext: cellContext, - rowCache: rowCache, - cellDataLoader: CellDataLoader( - parser: MediaCellDataParser(), - reloadOnFieldChange: true, - ), - cellDataPersistence: TextCellDataPersistence(), - ); } throw UnimplementedError; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart index cfab4668ae..1c03239cde 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart @@ -196,19 +196,3 @@ class TimeCellDataParser implements CellDataParser { } } } - -class MediaCellDataParser implements CellDataParser { - @override - MediaCellDataPB? parserData(List data) { - if (data.isEmpty) { - return null; - } - - try { - return MediaCellDataPB.fromBuffer(data); - } catch (e) { - Log.error("Failed to parse media cell data: $e"); - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart index 5317539128..b1bc6c43a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/database_controller.dart @@ -40,9 +40,7 @@ class GroupCallbacks { } class DatabaseLayoutSettingCallbacks { - DatabaseLayoutSettingCallbacks({ - required this.onLayoutSettingsChanged, - }); + DatabaseLayoutSettingCallbacks({required this.onLayoutSettingsChanged}); final void Function(DatabaseLayoutSettingPB) onLayoutSettingsChanged; } @@ -98,11 +96,9 @@ class DatabaseController { final List _databaseCallbacks = []; final List _groupCallbacks = []; final List _layoutCallbacks = []; - final Set> _compactModeCallbacks = {}; // Getters RowCache get rowCache => _viewCache.rowCache; - String get viewId => view.id; // Listener @@ -110,26 +106,17 @@ class DatabaseController { final DatabaseLayoutSettingListener _layoutListener; final ValueNotifier _isLoading = ValueNotifier(true); - final ValueNotifier _compactMode = ValueNotifier(true); - void setIsLoading(bool isLoading) => _isLoading.value = isLoading; - - ValueNotifier get isLoading => _isLoading; - - void setCompactMode(bool compactMode) { - _compactMode.value = compactMode; - for (final callback in Set.of(_compactModeCallbacks)) { - callback.call(compactMode); - } + void setIsLoading(bool isLoading) { + _isLoading.value = isLoading; } - ValueNotifier get compactModeNotifier => _compactMode; + ValueNotifier get isLoading => _isLoading; void addListener({ DatabaseCallbacks? onDatabaseChanged, DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, GroupCallbacks? onGroupChanged, - ValueChanged? onCompactModeChanged, }) { if (onLayoutSettingsChanged != null) { _layoutCallbacks.add(onLayoutSettingsChanged); @@ -142,33 +129,6 @@ class DatabaseController { if (onGroupChanged != null) { _groupCallbacks.add(onGroupChanged); } - - if (onCompactModeChanged != null) { - _compactModeCallbacks.add(onCompactModeChanged); - } - } - - void removeListener({ - DatabaseCallbacks? onDatabaseChanged, - DatabaseLayoutSettingCallbacks? onLayoutSettingsChanged, - GroupCallbacks? onGroupChanged, - ValueChanged? onCompactModeChanged, - }) { - if (onDatabaseChanged != null) { - _databaseCallbacks.remove(onDatabaseChanged); - } - - if (onLayoutSettingsChanged != null) { - _layoutCallbacks.remove(onLayoutSettingsChanged); - } - - if (onGroupChanged != null) { - _groupCallbacks.remove(onGroupChanged); - } - - if (onCompactModeChanged != null) { - _compactModeCallbacks.remove(onCompactModeChanged); - } } Future> open() async { @@ -263,8 +223,6 @@ class DatabaseController { _databaseCallbacks.clear(); _groupCallbacks.clear(); _layoutCallbacks.clear(); - _compactModeCallbacks.clear(); - _isLoading.dispose(); } Future _loadGroups() async { @@ -398,10 +356,4 @@ class DatabaseController { }, ); } - - void initCompactMode(bool enableCompactMode) { - if (_compactMode.value != enableCompactMode) { - _compactMode.value = enableCompactMode; - } - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart index 65deae7e58..48785fc87f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/defines.dart @@ -1,24 +1,25 @@ import 'dart:collection'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +// TODO(RS): remove dependency on presentation code +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'field/field_info.dart'; -import 'field/sort_entities.dart'; import 'row/row_cache.dart'; import 'row/row_service.dart'; part 'defines.freezed.dart'; typedef OnFieldsChanged = void Function(UnmodifiableListView); -typedef OnFiltersChanged = void Function(List); -typedef OnSortsChanged = void Function(List); +typedef OnFiltersChanged = void Function(List); +typedef OnSortsChanged = void Function(List); typedef OnDatabaseChanged = void Function(DatabasePB); -typedef OnRowsCreated = void Function(List rows); +typedef OnRowsCreated = void Function(List rowIds); typedef OnRowsUpdated = void Function( List rowIds, ChangedReason reason, @@ -29,9 +30,6 @@ typedef OnNumOfRowsChanged = void Function( UnmodifiableMapView rowById, ChangedReason reason, ); -typedef OnRowsVisibilityChanged = void Function( - List<(RowId, bool)> rowVisibilityChanges, -); @freezed class LoadingState with _$LoadingState { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index 93fd69bcfc..ad0d1b4653 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -1,5 +1,7 @@ import 'dart:collection'; +import 'package:flutter/foundation.dart'; + import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/setting/setting_listener.dart'; import 'package:appflowy/plugins/database/domain/database_view_service.dart'; @@ -10,17 +12,17 @@ import 'package:appflowy/plugins/database/domain/filter_listener.dart'; import 'package:appflowy/plugins/database/domain/filter_service.dart'; import 'package:appflowy/plugins/database/domain/sort_listener.dart'; import 'package:appflowy/plugins/database/domain/sort_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import '../setting/setting_service.dart'; + import 'field_info.dart'; -import 'filter_entities.dart'; -import 'sort_entities.dart'; class _GridFieldNotifier extends ChangeNotifier { List _fieldInfos = []; @@ -39,9 +41,9 @@ class _GridFieldNotifier extends ChangeNotifier { } class _GridFilterNotifier extends ChangeNotifier { - List _filters = []; + List _filters = []; - set filters(List filters) { + set filters(List filters) { _filters = filters; notifyListeners(); } @@ -50,13 +52,13 @@ class _GridFilterNotifier extends ChangeNotifier { notifyListeners(); } - List get filters => _filters; + List get filters => _filters; } class _GridSortNotifier extends ChangeNotifier { - List _sorts = []; + List _sorts = []; - set sorts(List sorts) { + set sorts(List sorts) { _sorts = sorts; notifyListeners(); } @@ -65,14 +67,15 @@ class _GridSortNotifier extends ChangeNotifier { notifyListeners(); } - List get sorts => _sorts; + List get sorts => _sorts; } typedef OnReceiveUpdateFields = void Function(List); typedef OnReceiveField = void Function(FieldInfo); typedef OnReceiveFields = void Function(List); -typedef OnReceiveFilters = void Function(List); -typedef OnReceiveSorts = void Function(List); +typedef OnReceiveFilters = void Function(List); +typedef OnReceiveSorts = void Function(List); + class FieldController { FieldController({required this.viewId}) @@ -132,8 +135,8 @@ class FieldController { // Getters List get fieldInfos => [..._fieldNotifier.fieldInfos]; - List get filters => [..._filterNotifier?.filters ?? []]; - List get sorts => [..._sortNotifier?.sorts ?? []]; + List get filterInfos => [..._filterNotifier?.filters ?? []]; + List get sortInfos => [..._sortNotifier?.sorts ?? []]; List get groupSettings => _groupConfigurationByFieldId.entries.map((e) => e.value).toList(); @@ -142,22 +145,22 @@ class FieldController { .firstWhereOrNull((element) => element.id == fieldId); } - DatabaseFilter? getFilterByFilterId(String filterId) { + FilterInfo? getFilterByFilterId(String filterId) { return _filterNotifier?.filters .firstWhereOrNull((element) => element.filterId == filterId); } - DatabaseFilter? getFilterByFieldId(String fieldId) { + FilterInfo? getFilterByFieldId(String fieldId) { return _filterNotifier?.filters .firstWhereOrNull((element) => element.fieldId == fieldId); } - DatabaseSort? getSortBySortId(String sortId) { + SortInfo? getSortBySortId(String sortId) { return _sortNotifier?.sorts .firstWhereOrNull((element) => element.sortId == sortId); } - DatabaseSort? getSortByFieldId(String fieldId) { + SortInfo? getSortByFieldId(String fieldId) { return _sortNotifier?.sorts .firstWhereOrNull((element) => element.fieldId == fieldId); } @@ -172,10 +175,22 @@ class FieldController { result.fold( (FilterChangesetNotificationPB changeset) { - _filterNotifier?.filters = - _filterListFromPBs(changeset.filters.items); - _fieldNotifier.fieldInfos = - _updateFieldInfos(_fieldNotifier.fieldInfos); + final List filters = []; + for (final filter in changeset.filters.items) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: filter.data.fieldId, + fieldType: filter.data.fieldType, + ); + + if (fieldInfo != null) { + final filterInfo = FilterInfo(viewId, filter, fieldInfo); + filters.add(filterInfo); + } + } + + _filterNotifier?.filters = filters; + _updateFieldInfos(); }, (err) => Log.error(err), ); @@ -186,55 +201,76 @@ class FieldController { /// Listen for sort changes in the backend. void _listenOnSortChanged() { void deleteSortFromChangeset( - List newDatabaseSorts, + List newSortInfos, SortChangesetNotificationPB changeset, ) { final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList(); if (deleteSortIds.isNotEmpty) { - newDatabaseSorts.retainWhere( + newSortInfos.retainWhere( (element) => !deleteSortIds.contains(element.sortId), ); } } void insertSortFromChangeset( - List newDatabaseSorts, + List newSortInfos, SortChangesetNotificationPB changeset, ) { for (final newSortPB in changeset.insertSorts) { - final sortIndex = newDatabaseSorts + final sortIndex = newSortInfos .indexWhere((element) => element.sortId == newSortPB.sort.id); if (sortIndex == -1) { - newDatabaseSorts.insert( - newSortPB.index, - DatabaseSort.fromPB(newSortPB.sort), + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: newSortPB.sort.fieldId, + fieldType: null, ); + + if (fieldInfo != null) { + newSortInfos.insert( + newSortPB.index, + SortInfo(sortPB: newSortPB.sort, fieldInfo: fieldInfo), + ); + } } } } void updateSortFromChangeset( - List newDatabaseSorts, + List newSortInfos, SortChangesetNotificationPB changeset, ) { for (final updatedSort in changeset.updateSorts) { - final newDatabaseSort = DatabaseSort.fromPB(updatedSort); - - final sortIndex = newDatabaseSorts.indexWhere( + final sortIndex = newSortInfos.indexWhere( (element) => element.sortId == updatedSort.id, ); - + // Remove the old filter if (sortIndex != -1) { - newDatabaseSorts.removeAt(sortIndex); - newDatabaseSorts.insert(sortIndex, newDatabaseSort); - } else { - newDatabaseSorts.add(newDatabaseSort); + newSortInfos.removeAt(sortIndex); + } + + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: updatedSort.fieldId, + fieldType: null, + ); + + if (fieldInfo != null) { + final newSortInfo = SortInfo( + sortPB: updatedSort, + fieldInfo: fieldInfo, + ); + if (sortIndex != -1) { + newSortInfos.insert(sortIndex, newSortInfo); + } else { + newSortInfos.add(newSortInfo); + } } } } void updateFieldInfos( - List newDatabaseSorts, + List newSortInfos, SortChangesetNotificationPB changeset, ) { final changedFieldIds = HashSet.from([ @@ -253,7 +289,7 @@ class FieldController { continue; } newFieldInfos[index] = newFieldInfos[index].copyWith( - hasSort: newDatabaseSorts.any((sort) => sort.fieldId == fieldId), + hasSort: newSortInfos.any((sort) => sort.fieldId == fieldId), ); } @@ -267,13 +303,13 @@ class FieldController { } result.fold( (SortChangesetNotificationPB changeset) { - final List newDatabaseSorts = sorts; - deleteSortFromChangeset(newDatabaseSorts, changeset); - insertSortFromChangeset(newDatabaseSorts, changeset); - updateSortFromChangeset(newDatabaseSorts, changeset); + final List newSortInfos = sortInfos; + deleteSortFromChangeset(newSortInfos, changeset); + insertSortFromChangeset(newSortInfos, changeset); + updateSortFromChangeset(newSortInfos, changeset); - updateFieldInfos(newDatabaseSorts, changeset); - _sortNotifier?.sorts = newDatabaseSorts; + updateFieldInfos(newSortInfos, changeset); + _sortNotifier?.sorts = newSortInfos; }, (err) => Log.error(err), ); @@ -397,7 +433,7 @@ class FieldController { (updatedFields, fieldInfos) = await updateFields(changeset.updatedFields, fieldInfos); - _fieldNotifier.fieldInfos = _updateFieldInfos(fieldInfos); + _fieldNotifier.fieldInfos = fieldInfos; for (final listener in _updatedFieldCallbacks.values) { listener(updatedFields); } @@ -410,29 +446,20 @@ class FieldController { /// Listen for field setting changes in the backend. void _listenOnFieldSettingsChanged() { - FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) { - final newFields = [...fieldInfos]; - - if (newFields.isEmpty) { - return null; - } + FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) { + final List newFields = fieldInfos; + FieldInfo updatedField = newFields[0]; final index = newFields .indexWhere((field) => field.id == updatedFieldSettings.fieldId); - if (index != -1) { newFields[index] = newFields[index].copyWith(fieldSettings: updatedFieldSettings); - _fieldNotifier.fieldInfos = newFields; - _fieldSettings - ..removeWhere( - (field) => field.fieldId == updatedFieldSettings.fieldId, - ) - ..add(updatedFieldSettings); - return newFields[index]; + updatedField = newFields[index]; } - return null; + _fieldNotifier.fieldInfos = newFields; + return updatedField; } _fieldSettingsListener.start( @@ -443,10 +470,6 @@ class FieldController { result.fold( (fieldSettings) { final updatedFieldInfo = updateFieldSettings(fieldSettings); - if (updatedFieldInfo == null) { - return; - } - for (final listener in _updatedFieldCallbacks.values) { listener([updatedFieldInfo]); } @@ -464,29 +487,32 @@ class FieldController { _groupConfigurationByFieldId[configuration.fieldId] = configuration; } - _filterNotifier?.filters = _filterListFromPBs(setting.filters.items); + _filterNotifier?.filters = _filterInfoListFromPBs(setting.filters.items); - _sortNotifier?.sorts = _sortListFromPBs(setting.sorts.items); + _sortNotifier?.sorts = _sortInfoListFromPBs(setting.sorts.items); _fieldSettings.clear(); _fieldSettings.addAll(setting.fieldSettings.items); - _fieldNotifier.fieldInfos = _updateFieldInfos(_fieldNotifier.fieldInfos); + _updateFieldInfos(); } /// Attach sort, filter, group information and field settings to `FieldInfo` - List _updateFieldInfos(List fieldInfos) { - return fieldInfos - .map( - (field) => field.copyWith( - fieldSettings: _fieldSettings - .firstWhereOrNull((setting) => setting.fieldId == field.id), - isGroupField: _groupConfigurationByFieldId[field.id] != null, - hasFilter: getFilterByFieldId(field.id) != null, - hasSort: getSortByFieldId(field.id) != null, - ), - ) - .toList(); + void _updateFieldInfos() { + final List newFieldInfos = []; + for (final field in _fieldNotifier.fieldInfos) { + newFieldInfos.add( + field.copyWith( + fieldSettings: _fieldSettings + .firstWhereOrNull((setting) => setting.fieldId == field.id), + isGroupField: _groupConfigurationByFieldId[field.id] != null, + hasFilter: getFilterByFieldId(field.id) != null, + hasSort: getSortByFieldId(field.id) != null, + ), + ); + } + + _fieldNotifier.fieldInfos = newFieldInfos; } /// Load all of the fields. This is required when opening the database @@ -509,8 +535,7 @@ class FieldController { _loadAllFieldSettings(), _loadSettings(), ]); - _fieldNotifier.fieldInfos = - _updateFieldInfos(_fieldNotifier.fieldInfos); + _updateFieldInfos(); return FlowyResult.success(null); }, @@ -524,7 +549,7 @@ class FieldController { return _filterBackendSvc.getAllFilters().then((result) { return result.fold( (filterPBs) { - _filterNotifier?.filters = _filterListFromPBs(filterPBs); + _filterNotifier?.filters = _filterInfoListFromPBs(filterPBs); return FlowyResult.success(null); }, (err) => FlowyResult.failure(err), @@ -537,7 +562,7 @@ class FieldController { return _sortBackendSvc.getAllSorts().then((result) { return result.fold( (sortPBs) { - _sortNotifier?.sorts = _sortListFromPBs(sortPBs); + _sortNotifier?.sorts = _sortInfoListFromPBs(sortPBs); return FlowyResult.success(null); }, (err) => FlowyResult.failure(err), @@ -576,13 +601,39 @@ class FieldController { } /// Attach corresponding `FieldInfo`s to the `FilterPB`s - List _filterListFromPBs(List filterPBs) { - return filterPBs.map(DatabaseFilter.fromPB).toList(); + List _filterInfoListFromPBs(List filterPBs) { + FilterInfo? getFilterInfo(FilterPB filterPB) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: filterPB.data.fieldId, + fieldType: filterPB.data.fieldType, + ); + return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null; + } + + return filterPBs + .map((filterPB) => getFilterInfo(filterPB)) + .whereType() + .toList(); } /// Attach corresponding `FieldInfo`s to the `SortPB`s - List _sortListFromPBs(List sortPBs) { - return sortPBs.map(DatabaseSort.fromPB).toList(); + List _sortInfoListFromPBs(List sortPBs) { + SortInfo? getSortInfo(SortPB sortPB) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: sortPB.fieldId, + fieldType: null, + ); + return fieldInfo != null + ? SortInfo(sortPB: sortPB, fieldInfo: fieldInfo) + : null; + } + + return sortPBs + .map((sortPB) => getSortInfo(sortPB)) + .whereType() + .toList(); } void addListener({ @@ -620,7 +671,7 @@ class FieldController { if (listenWhen != null && listenWhen() == false) { return; } - onFilters(filters); + onFilters(filterInfos); } _filterCallbacks[onFilters] = callback; @@ -632,7 +683,7 @@ class FieldController { if (listenWhen != null && listenWhen() == false) { return; } - onSorts(sorts); + onSorts(sortInfos); } _sortCallbacks[onSorts] = callback; @@ -771,3 +822,15 @@ class RowCacheDependenciesImpl extends RowFieldsDelegate with RowLifeCycle { } } } + +FieldInfo? _findFieldInfo({ + required List fieldInfos, + required String fieldId, + required FieldType? fieldType, +}) { + return fieldInfos.firstWhereOrNull( + (element) => + element.id == fieldId && + (fieldType == null || element.fieldType == fieldType), + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart index 1c056b1461..b33f9f44ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_editor_bloc.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -18,33 +17,30 @@ part 'field_editor_bloc.freezed.dart'; class FieldEditorBloc extends Bloc { FieldEditorBloc({ required this.viewId, - required this.fieldInfo, required this.fieldController, this.onFieldInserted, - required this.isNew, - }) : _fieldService = FieldBackendService( + required FieldPB field, + }) : fieldId = field.id, + fieldService = FieldBackendService( viewId: viewId, - fieldId: fieldInfo.id, + fieldId: field.id, ), fieldSettingsService = FieldSettingsBackendService(viewId: viewId), - super(FieldEditorState(field: fieldInfo)) { + super(FieldEditorState(field: FieldInfo.initial(field))) { _dispatch(); _startListening(); _init(); } final String viewId; - final FieldInfo fieldInfo; - final bool isNew; + final String fieldId; final FieldController fieldController; - final FieldBackendService _fieldService; + final FieldBackendService fieldService; final FieldSettingsBackendService fieldSettingsService; final void Function(String newFieldId)? onFieldInserted; late final OnReceiveField _listener; - String get fieldId => fieldInfo.id; - @override Future close() { fieldController.removeSingleFieldListener( @@ -62,23 +58,10 @@ class FieldEditorBloc extends Bloc { emit(state.copyWith(field: fieldInfo)); }, switchFieldType: (fieldType) async { - String? fieldName; - if (!state.wasRenameManually && isNew) { - fieldName = fieldType.i18n; - } - - await _fieldService.updateType( - fieldType: fieldType, - fieldName: fieldName, - ); + await fieldService.updateType(fieldType: fieldType); }, renameField: (newName) async { - final result = await _fieldService.updateField(name: newName); - _logIfError(result); - emit(state.copyWith(wasRenameManually: true)); - }, - updateIcon: (icon) async { - final result = await _fieldService.updateField(icon: icon); + final result = await fieldService.updateField(name: newName); _logIfError(result); }, updateTypeOption: (typeOptionData) async { @@ -90,14 +73,14 @@ class FieldEditorBloc extends Bloc { _logIfError(result); }, insertLeft: () async { - final result = await _fieldService.createBefore(); + final result = await fieldService.createBefore(); result.fold( (newField) => onFieldInserted?.call(newField.id), (err) => Log.error("Failed creating field $err"), ); }, insertRight: () async { - final result = await _fieldService.createAfter(); + final result = await fieldService.createAfter(); result.fold( (newField) => onFieldInserted?.call(newField.id), (err) => Log.error("Failed creating field $err"), @@ -111,7 +94,7 @@ class FieldEditorBloc extends Bloc { ? FieldVisibility.AlwaysShown : FieldVisibility.AlwaysHidden; final result = await fieldSettingsService.updateFieldSettings( - fieldId: fieldId, + fieldId: state.field.id, fieldVisibility: newVisibility, ); _logIfError(result); @@ -169,7 +152,6 @@ class FieldEditorEvent with _$FieldEditorEvent { final Uint8List typeOptionData, ) = _UpdateTypeOption; const factory FieldEditorEvent.renameField(final String name) = _RenameField; - const factory FieldEditorEvent.updateIcon(String icon) = _UpdateIcon; const factory FieldEditorEvent.insertLeft() = _InsertLeft; const factory FieldEditorEvent.insertRight() = _InsertRight; const factory FieldEditorEvent.toggleFieldVisibility() = @@ -182,6 +164,5 @@ class FieldEditorEvent with _$FieldEditorEvent { class FieldEditorState with _$FieldEditorState { const factory FieldEditorState({ required final FieldInfo field, - @Default(false) bool wasRenameManually, }) = _FieldEditorState; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart index 46fc8659ca..bc5107f75c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart @@ -1,5 +1,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'field_info.freezed.dart'; @@ -29,8 +30,6 @@ class FieldInfo with _$FieldInfo { String get name => field.name; - String get icon => field.icon; - bool get isPrimary => field.isPrimary; double? get width => fieldSettings?.width.toDouble(); @@ -38,4 +37,68 @@ class FieldInfo with _$FieldInfo { FieldVisibility? get visibility => fieldSettings?.visibility; bool? get wrapCellContent => fieldSettings?.wrapCellContent; + + bool get canBeGroup { + switch (field.fieldType) { + case FieldType.URL: + case FieldType.Checkbox: + case FieldType.MultiSelect: + case FieldType.SingleSelect: + case FieldType.DateTime: + return true; + default: + return false; + } + } + + bool get canCreateFilter { + if (isGroupField) { + return false; + } + + switch (field.fieldType) { + case FieldType.Number: + case FieldType.Checkbox: + case FieldType.MultiSelect: + case FieldType.RichText: + case FieldType.SingleSelect: + case FieldType.Checklist: + case FieldType.URL: + case FieldType.Time: + return true; + default: + return false; + } + } + + bool get canCreateSort { + if (hasSort) { + return false; + } + + switch (field.fieldType) { + case FieldType.RichText: + case FieldType.Checkbox: + case FieldType.Number: + case FieldType.DateTime: + case FieldType.SingleSelect: + case FieldType.MultiSelect: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + case FieldType.Checklist: + case FieldType.Time: + return true; + default: + return false; + } + } + + List get groupConditions { + switch (field.fieldType) { + case FieldType.DateTime: + return DateConditionPB.values; + default: + return []; + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/filter_entities.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/filter_entities.dart deleted file mode 100644 index 46867e1f97..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/filter_entities.dart +++ /dev/null @@ -1,748 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/view/database_filter_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/select_option_loader.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy/util/int64_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:equatable/equatable.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -abstract class DatabaseFilter extends Equatable { - const DatabaseFilter({ - required this.filterId, - required this.fieldId, - required this.fieldType, - }); - - factory DatabaseFilter.fromPB(FilterPB filterPB) { - final FilterDataPB(:fieldId, :fieldType) = filterPB.data; - switch (fieldType) { - case FieldType.RichText: - case FieldType.URL: - final data = TextFilterPB.fromBuffer(filterPB.data.data); - return TextFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - content: data.content, - ); - case FieldType.Number: - final data = NumberFilterPB.fromBuffer(filterPB.data.data); - return NumberFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - content: data.content, - ); - case FieldType.Checkbox: - final data = CheckboxFilterPB.fromBuffer(filterPB.data.data); - return CheckboxFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - ); - case FieldType.Checklist: - final data = ChecklistFilterPB.fromBuffer(filterPB.data.data); - return ChecklistFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - ); - case FieldType.SingleSelect: - case FieldType.MultiSelect: - final data = SelectOptionFilterPB.fromBuffer(filterPB.data.data); - return SelectOptionFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - optionIds: data.optionIds, - ); - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - case FieldType.DateTime: - final data = DateFilterPB.fromBuffer(filterPB.data.data); - return DateTimeFilter( - filterId: filterPB.id, - fieldId: fieldId, - fieldType: fieldType, - condition: data.condition, - timestamp: data.hasTimestamp() ? data.timestamp.toDateTime() : null, - start: data.hasStart() ? data.start.toDateTime() : null, - end: data.hasEnd() ? data.end.toDateTime() : null, - ); - default: - throw ArgumentError(); - } - } - - final String filterId; - final String fieldId; - final FieldType fieldType; - - String get conditionName; - - bool get canAttachContent; - - String getContentDescription(FieldInfo field); - - Widget getMobileDescription( - FieldInfo field, { - required VoidCallback onExpand, - required void Function(DatabaseFilter filter) onUpdate, - }) => - const SizedBox.shrink(); - - Uint8List writeToBuffer(); -} - -final class TextFilter extends DatabaseFilter { - TextFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - required String content, - }) { - this.content = canAttachContent ? content : ""; - } - - final TextFilterConditionPB condition; - late final String content; - - @override - String get conditionName => condition.filterName; - - @override - bool get canAttachContent => - condition != TextFilterConditionPB.TextIsEmpty && - condition != TextFilterConditionPB.TextIsNotEmpty; - - @override - String getContentDescription(FieldInfo field) { - final filterDesc = condition.choicechipPrefix; - - if (condition == TextFilterConditionPB.TextIsEmpty || - condition == TextFilterConditionPB.TextIsNotEmpty) { - return filterDesc; - } - - return content.isEmpty ? filterDesc : "$filterDesc $content"; - } - - @override - Widget getMobileDescription( - FieldInfo field, { - required VoidCallback onExpand, - required void Function(DatabaseFilter filter) onUpdate, - }) { - return FilterItemInnerTextField( - content: content, - enabled: canAttachContent, - onSubmitted: (content) { - final newFilter = copyWith(content: content); - onUpdate(newFilter); - }, - ); - } - - @override - Uint8List writeToBuffer() { - final filterPB = TextFilterPB()..condition = condition; - - if (condition != TextFilterConditionPB.TextIsEmpty && - condition != TextFilterConditionPB.TextIsNotEmpty) { - filterPB.content = content; - } - return filterPB.writeToBuffer(); - } - - TextFilter copyWith({ - TextFilterConditionPB? condition, - String? content, - }) { - return TextFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - content: content ?? this.content, - ); - } - - @override - List get props => [filterId, fieldId, condition, content]; -} - -final class NumberFilter extends DatabaseFilter { - NumberFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - required String content, - }) { - this.content = canAttachContent ? content : ""; - } - - final NumberFilterConditionPB condition; - late final String content; - - @override - String get conditionName => condition.filterName; - - @override - bool get canAttachContent => - condition != NumberFilterConditionPB.NumberIsEmpty && - condition != NumberFilterConditionPB.NumberIsNotEmpty; - - @override - String getContentDescription(FieldInfo field) { - if (condition == NumberFilterConditionPB.NumberIsEmpty || - condition == NumberFilterConditionPB.NumberIsNotEmpty) { - return condition.shortName; - } - - return "${condition.shortName} $content"; - } - - @override - Widget getMobileDescription( - FieldInfo field, { - required VoidCallback onExpand, - required void Function(DatabaseFilter filter) onUpdate, - }) { - return FilterItemInnerTextField( - content: content, - enabled: canAttachContent, - onSubmitted: (content) { - final newFilter = copyWith(content: content); - onUpdate(newFilter); - }, - ); - } - - @override - Uint8List writeToBuffer() { - final filterPB = NumberFilterPB()..condition = condition; - - if (condition != NumberFilterConditionPB.NumberIsEmpty && - condition != NumberFilterConditionPB.NumberIsNotEmpty) { - filterPB.content = content; - } - return filterPB.writeToBuffer(); - } - - NumberFilter copyWith({ - NumberFilterConditionPB? condition, - String? content, - }) { - return NumberFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - content: content ?? this.content, - ); - } - - @override - List get props => [filterId, fieldId, condition, content]; -} - -final class CheckboxFilter extends DatabaseFilter { - const CheckboxFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - }); - - final CheckboxFilterConditionPB condition; - - @override - String get conditionName => condition.filterName; - - @override - bool get canAttachContent => false; - - @override - String getContentDescription(FieldInfo field) => condition.filterName; - - @override - Uint8List writeToBuffer() { - return (CheckboxFilterPB()..condition = condition).writeToBuffer(); - } - - CheckboxFilter copyWith({ - CheckboxFilterConditionPB? condition, - }) { - return CheckboxFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - ); - } - - @override - List get props => [filterId, fieldId, condition]; -} - -final class ChecklistFilter extends DatabaseFilter { - const ChecklistFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - }); - - final ChecklistFilterConditionPB condition; - - @override - String get conditionName => condition.filterName; - - @override - bool get canAttachContent => false; - - @override - String getContentDescription(FieldInfo field) => condition.filterName; - - @override - Uint8List writeToBuffer() { - return (ChecklistFilterPB()..condition = condition).writeToBuffer(); - } - - ChecklistFilter copyWith({ - ChecklistFilterConditionPB? condition, - }) { - return ChecklistFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - ); - } - - @override - List get props => [filterId, fieldId, condition]; -} - -final class SelectOptionFilter extends DatabaseFilter { - SelectOptionFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - required List optionIds, - }) { - if (canAttachContent) { - this.optionIds.addAll(optionIds); - } - } - - final SelectOptionFilterConditionPB condition; - final List optionIds = []; - - @override - String get conditionName => condition.i18n; - - @override - bool get canAttachContent => - condition != SelectOptionFilterConditionPB.OptionIsEmpty && - condition != SelectOptionFilterConditionPB.OptionIsNotEmpty; - - @override - String getContentDescription(FieldInfo field) { - if (!canAttachContent || optionIds.isEmpty) { - return condition.i18n; - } - - final delegate = makeDelegate(field); - final options = delegate.getOptions(field); - - final optionNames = options - .where((option) => optionIds.contains(option.id)) - .map((option) => option.name) - .join(', '); - return "${condition.i18n} $optionNames"; - } - - @override - Widget getMobileDescription( - FieldInfo field, { - required VoidCallback onExpand, - required void Function(DatabaseFilter filter) onUpdate, - }) { - final delegate = makeDelegate(field); - final options = delegate - .getOptions(field) - .where((option) => optionIds.contains(option.id)) - .toList(); - - return FilterItemInnerButton( - onTap: onExpand, - child: ListView.separated( - physics: const NeverScrollableScrollPhysics(), - scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => const HSpace(8), - itemCount: options.length, - itemBuilder: (context, index) => SelectOptionTag( - option: options[index], - fontSize: 14, - borderRadius: BorderRadius.circular(9), - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - ), - padding: const EdgeInsets.symmetric(vertical: 8), - ), - ); - } - - @override - Uint8List writeToBuffer() { - final filterPB = SelectOptionFilterPB()..condition = condition; - - if (canAttachContent) { - filterPB.optionIds.addAll(optionIds); - } - - return filterPB.writeToBuffer(); - } - - SelectOptionFilter copyWith({ - SelectOptionFilterConditionPB? condition, - List? optionIds, - }) { - return SelectOptionFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - optionIds: optionIds ?? this.optionIds, - ); - } - - SelectOptionFilterDelegate makeDelegate(FieldInfo field) => - field.fieldType == FieldType.SingleSelect - ? const SingleSelectOptionFilterDelegateImpl() - : const MultiSelectOptionFilterDelegateImpl(); - - @override - List get props => [filterId, fieldId, condition, optionIds]; -} - -enum DateTimeFilterCondition { - on, - before, - after, - onOrBefore, - onOrAfter, - between, - isEmpty, - isNotEmpty; - - DateFilterConditionPB toPB(bool isStart) { - return isStart - ? switch (this) { - on => DateFilterConditionPB.DateStartsOn, - before => DateFilterConditionPB.DateStartsBefore, - after => DateFilterConditionPB.DateStartsAfter, - onOrBefore => DateFilterConditionPB.DateStartsOnOrBefore, - onOrAfter => DateFilterConditionPB.DateStartsOnOrAfter, - between => DateFilterConditionPB.DateStartsBetween, - isEmpty => DateFilterConditionPB.DateStartIsEmpty, - isNotEmpty => DateFilterConditionPB.DateStartIsNotEmpty, - } - : switch (this) { - on => DateFilterConditionPB.DateEndsOn, - before => DateFilterConditionPB.DateEndsBefore, - after => DateFilterConditionPB.DateEndsAfter, - onOrBefore => DateFilterConditionPB.DateEndsOnOrBefore, - onOrAfter => DateFilterConditionPB.DateEndsOnOrAfter, - between => DateFilterConditionPB.DateEndsBetween, - isEmpty => DateFilterConditionPB.DateEndIsEmpty, - isNotEmpty => DateFilterConditionPB.DateEndIsNotEmpty, - }; - } - - String get choiceChipPrefix { - return switch (this) { - on => "", - before => LocaleKeys.grid_dateFilter_choicechipPrefix_before.tr(), - after => LocaleKeys.grid_dateFilter_choicechipPrefix_after.tr(), - onOrBefore => LocaleKeys.grid_dateFilter_choicechipPrefix_onOrBefore.tr(), - onOrAfter => LocaleKeys.grid_dateFilter_choicechipPrefix_onOrAfter.tr(), - between => LocaleKeys.grid_dateFilter_choicechipPrefix_between.tr(), - isEmpty => LocaleKeys.grid_dateFilter_choicechipPrefix_isEmpty.tr(), - isNotEmpty => LocaleKeys.grid_dateFilter_choicechipPrefix_isNotEmpty.tr(), - }; - } - - String get filterName { - return switch (this) { - on => LocaleKeys.grid_dateFilter_is.tr(), - before => LocaleKeys.grid_dateFilter_before.tr(), - after => LocaleKeys.grid_dateFilter_after.tr(), - onOrBefore => LocaleKeys.grid_dateFilter_onOrBefore.tr(), - onOrAfter => LocaleKeys.grid_dateFilter_onOrAfter.tr(), - between => LocaleKeys.grid_dateFilter_between.tr(), - isEmpty => LocaleKeys.grid_dateFilter_empty.tr(), - isNotEmpty => LocaleKeys.grid_dateFilter_notEmpty.tr(), - }; - } - - static List availableConditionsForFieldType( - FieldType fieldType, - ) { - final result = [...values]; - if (fieldType == FieldType.CreatedTime || - fieldType == FieldType.LastEditedTime) { - result.remove(isEmpty); - result.remove(isNotEmpty); - } - - return result; - } -} - -final class DateTimeFilter extends DatabaseFilter { - DateTimeFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - DateTime? timestamp, - DateTime? start, - DateTime? end, - }) { - if (canAttachContent) { - if (condition == DateFilterConditionPB.DateStartsBetween || - condition == DateFilterConditionPB.DateEndsBetween) { - this.start = start; - this.end = end; - this.timestamp = null; - } else { - this.timestamp = timestamp; - this.start = null; - this.end = null; - } - } else { - this.timestamp = null; - this.start = null; - this.end = null; - } - } - - final DateFilterConditionPB condition; - late final DateTime? timestamp; - late final DateTime? start; - late final DateTime? end; - - @override - String get conditionName => condition.toCondition().filterName; - - @override - bool get canAttachContent => ![ - DateFilterConditionPB.DateStartIsEmpty, - DateFilterConditionPB.DateStartIsNotEmpty, - DateFilterConditionPB.DateEndIsEmpty, - DateFilterConditionPB.DateEndIsNotEmpty, - ].contains(condition); - - @override - String getContentDescription(FieldInfo field) { - return switch (condition) { - DateFilterConditionPB.DateStartIsEmpty || - DateFilterConditionPB.DateStartIsNotEmpty || - DateFilterConditionPB.DateEndIsEmpty || - DateFilterConditionPB.DateEndIsNotEmpty => - condition.toCondition().choiceChipPrefix, - DateFilterConditionPB.DateStartsOn || - DateFilterConditionPB.DateEndsOn => - timestamp?.defaultFormat ?? "", - DateFilterConditionPB.DateStartsBetween || - DateFilterConditionPB.DateEndsBetween => - "${condition.toCondition().choiceChipPrefix} ${start?.defaultFormat ?? ""} - ${end?.defaultFormat ?? ""}", - _ => - "${condition.toCondition().choiceChipPrefix} ${timestamp?.defaultFormat ?? ""}" - }; - } - - @override - Widget getMobileDescription( - FieldInfo field, { - required VoidCallback onExpand, - required void Function(DatabaseFilter filter) onUpdate, - }) { - String? text; - - if (condition.isRange) { - text = "${start?.defaultFormat ?? ""} - ${end?.defaultFormat ?? ""}"; - text = text == " - " ? null : text; - } else { - text = timestamp.defaultFormat; - } - return FilterItemInnerButton( - onTap: onExpand, - child: FlowyText( - text ?? "", - overflow: TextOverflow.ellipsis, - ), - ); - } - - @override - Uint8List writeToBuffer() { - final filterPB = DateFilterPB()..condition = condition; - - Int64 dateTimeToInt(DateTime dateTime) { - return Int64(dateTime.millisecondsSinceEpoch ~/ 1000); - } - - switch (condition) { - case DateFilterConditionPB.DateStartsOn: - case DateFilterConditionPB.DateStartsBefore: - case DateFilterConditionPB.DateStartsOnOrBefore: - case DateFilterConditionPB.DateStartsAfter: - case DateFilterConditionPB.DateStartsOnOrAfter: - case DateFilterConditionPB.DateEndsOn: - case DateFilterConditionPB.DateEndsBefore: - case DateFilterConditionPB.DateEndsOnOrBefore: - case DateFilterConditionPB.DateEndsAfter: - case DateFilterConditionPB.DateEndsOnOrAfter: - if (timestamp != null) { - filterPB.timestamp = dateTimeToInt(timestamp!); - } - break; - case DateFilterConditionPB.DateStartsBetween: - case DateFilterConditionPB.DateEndsBetween: - if (start != null) { - filterPB.start = dateTimeToInt(start!); - } - if (end != null) { - filterPB.end = dateTimeToInt(end!); - } - break; - default: - break; - } - - return filterPB.writeToBuffer(); - } - - DateTimeFilter copyWithCondition({ - required bool isStart, - required DateTimeFilterCondition condition, - }) { - return DateTimeFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition.toPB(isStart), - start: start, - end: end, - timestamp: timestamp, - ); - } - - DateTimeFilter copyWithTimestamp({ - required DateTime timestamp, - }) { - return DateTimeFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition, - start: start, - end: end, - timestamp: timestamp, - ); - } - - DateTimeFilter copyWithRange({ - required DateTime? start, - required DateTime? end, - }) { - return DateTimeFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition, - start: start, - end: end, - timestamp: timestamp, - ); - } - - @override - List get props => - [filterId, fieldId, condition, timestamp, start, end]; -} - -final class TimeFilter extends DatabaseFilter { - const TimeFilter({ - required super.filterId, - required super.fieldId, - required super.fieldType, - required this.condition, - required this.content, - }); - - final NumberFilterConditionPB condition; - final String content; - - @override - String get conditionName => condition.filterName; - - @override - bool get canAttachContent => - condition != NumberFilterConditionPB.NumberIsEmpty && - condition != NumberFilterConditionPB.NumberIsNotEmpty; - - @override - String getContentDescription(FieldInfo field) { - if (condition == NumberFilterConditionPB.NumberIsEmpty || - condition == NumberFilterConditionPB.NumberIsNotEmpty) { - return condition.shortName; - } - - return "${condition.shortName} $content"; - } - - @override - Uint8List writeToBuffer() { - return (NumberFilterPB() - ..condition = condition - ..content = content) - .writeToBuffer(); - } - - TimeFilter copyWith({NumberFilterConditionPB? condition, String? content}) { - return TimeFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: fieldType, - condition: condition ?? this.condition, - content: content ?? this.content, - ); - } - - @override - List get props => [filterId, fieldId, condition, content]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/sort_entities.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/sort_entities.dart deleted file mode 100644 index a5aeaa3e28..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/sort_entities.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:equatable/equatable.dart'; - -final class DatabaseSort extends Equatable { - const DatabaseSort({ - required this.sortId, - required this.fieldId, - required this.condition, - }); - - DatabaseSort.fromPB(SortPB sort) - : sortId = sort.id, - fieldId = sort.fieldId, - condition = sort.condition; - - final String sortId; - final String fieldId; - final SortConditionPB condition; - - @override - List get props => [sortId, fieldId, condition]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart index 4ddde80b79..691b6b7227 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart @@ -17,11 +17,11 @@ class RelationDatabaseListCubit extends Cubit { .send() .fold>((s) => s.items, (f) => []); final futures = metaPBs.map((meta) { - return ViewBackendService.getView(meta.viewId).then( + return ViewBackendService.getView(meta.inlineViewId).then( (result) => result.fold( (s) => DatabaseMeta( databaseId: meta.databaseId, - viewId: meta.viewId, + inlineViewId: meta.inlineViewId, databaseName: s.name, ), (f) => null, @@ -43,10 +43,10 @@ class DatabaseMeta with _$DatabaseMeta { /// id of the database required String databaseId, - /// id of the view - required String viewId, + /// id of the inline view + required String inlineViewId, - /// name of the database + /// name of the database, currently identical to the name of the inline view required String databaseName, }) = _DatabaseMeta; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart index c76e6d095c..f318bad9d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/type_option_data_parser.dart @@ -57,10 +57,3 @@ class TranslateTypeOptionDataParser return TranslateTypeOptionPB.fromBuffer(buffer); } } - -class MediaTypeOptionDataParser extends TypeOptionParser { - @override - MediaTypeOptionPB fromBuffer(List buffer) { - return MediaTypeOptionPB.fromBuffer(buffer); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart index 0f884a1e9a..a5dd0d9ca1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_bloc.dart @@ -31,7 +31,6 @@ class DatabaseLayoutBloc @freezed class DatabaseLayoutEvent with _$DatabaseLayoutEvent { const factory DatabaseLayoutEvent.initial() = _Initial; - const factory DatabaseLayoutEvent.updateLayout(DatabaseLayoutPB layout) = _UpdateLayout; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart index f735618dd8..06e1e2b70f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/related_row_detail_bloc.dart @@ -1,14 +1,11 @@ import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import '../database_controller.dart'; - import 'row_controller.dart'; part 'related_row_detail_bloc.freezed.dart'; @@ -23,15 +20,12 @@ class RelatedRowDetailPageBloc _init(databaseId, initialRowId); } - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - @override Future close() { state.whenOrNull( - ready: (databaseController, rowController) async { - await rowController.dispose(); - await databaseController.dispose(); + ready: (databaseController, rowController) { + rowController.dispose(); + databaseController.dispose(); }, ); return super.close(); @@ -39,19 +33,11 @@ class RelatedRowDetailPageBloc void _dispatch() { on((event, emit) async { - await event.when( - didInitialize: (databaseController, rowController) async { - final response = await UserEventGetUserProfile().send(); - response.fold( - (userProfile) => _userProfile = userProfile, - (err) => Log.error(err), - ); - - await rowController.initialize(); - - await state.maybeWhen( - ready: (_, oldRowController) async { - await oldRowController.dispose(); + event.when( + didInitialize: (databaseController, rowController) { + state.maybeWhen( + ready: (_, oldRowController) { + oldRowController.dispose(); emit( RelatedRowDetailPageState.ready( databaseController: databaseController, @@ -73,24 +59,27 @@ class RelatedRowDetailPageBloc }); } + /// initialize bloc through the `database_id` and `row_id`. The process is as + /// follows: + /// 1. use the `database_id` to get the database meta, which contains the + /// `inline_view_id` + /// 2. use the `inline_view_id` to instantiate a `DatabaseController`. + /// 3. use the `row_id` with the DatabaseController` to create `RowController` void _init(String databaseId, String initialRowId) async { - final viewId = await DatabaseEventGetDefaultDatabaseViewId( - DatabaseIdPB(value: databaseId), - ).send().fold( - (pb) => pb.value, - (error) => null, - ); - - if (viewId == null) { + final databaseMeta = + await DatabaseEventGetDatabaseMeta(DatabaseIdPB(value: databaseId)) + .send() + .fold((s) => s, (f) => null); + if (databaseMeta == null) { return; } - - final databaseView = await ViewBackendService.getView(viewId) - .fold((viewPB) => viewPB, (f) => null); - if (databaseView == null) { + final inlineView = + await ViewBackendService.getView(databaseMeta.inlineViewId) + .fold((viewPB) => viewPB, (f) => null); + if (inlineView == null) { return; } - final databaseController = DatabaseController(view: databaseView); + final databaseController = DatabaseController(view: inlineView); await databaseController.open().fold( (s) => databaseController.setIsLoading(false), (f) => null, @@ -101,10 +90,9 @@ class RelatedRowDetailPageBloc } final rowController = RowController( rowMeta: rowInfo.rowMeta, - viewId: databaseView.id, + viewId: inlineView.id, rowCache: databaseController.rowCache, ); - add( RelatedRowDetailPageEvent.didInitialize( databaseController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart index 7714b7727f..1f05352dc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_banner_bloc.dart @@ -1,13 +1,8 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -31,11 +26,6 @@ class RowBannerBloc extends Bloc { final RowBackendService _rowBackendSvc; final RowMetaListener _metaListener; - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - - bool get hasCover => state.rowMeta.cover.data.isNotEmpty; - @override Future close() async { await _metaListener.stop(); @@ -46,21 +36,15 @@ class RowBannerBloc extends Bloc { on( (event, emit) { event.when( - initial: () async { - await _loadPrimaryField(); + initial: () { + _loadPrimaryField(); _listenRowMetaChanged(); - final result = await UserEventGetUserProfile().send(); - result.fold( - (userProfile) => _userProfile = userProfile, - (error) => Log.error(error), - ); }, didReceiveRowMeta: (RowMetaPB rowMeta) { emit(state.copyWith(rowMeta: rowMeta)); }, - setCover: (RowCoverPB cover) => _updateMeta(cover: cover), + setCover: (String coverURL) => _updateMeta(coverURL: coverURL), setIcon: (String iconURL) => _updateMeta(iconURL: iconURL), - removeCover: () => _removeCover(), didReceiveFieldUpdate: (updatedField) { emit( state.copyWith( @@ -107,19 +91,14 @@ class RowBannerBloc extends Bloc { } /// Update the meta of the row and the view - Future _updateMeta({String? iconURL, RowCoverPB? cover}) async { + Future _updateMeta({String? iconURL, String? coverURL}) async { final result = await _rowBackendSvc.updateMeta( iconURL: iconURL, - cover: cover, + coverURL: coverURL, rowId: state.rowMeta.id, ); result.fold((l) => null, (err) => Log.error(err)); } - - Future _removeCover() async { - final result = await _rowBackendSvc.removeCover(state.rowMeta.id); - result.fold((l) => null, (err) => Log.error(err)); - } } @freezed @@ -130,14 +109,11 @@ class RowBannerEvent with _$RowBannerEvent { const factory RowBannerEvent.didReceiveFieldUpdate(FieldPB field) = _DidReceiveFieldUpdate; const factory RowBannerEvent.setIcon(String iconURL) = _SetIcon; - const factory RowBannerEvent.setCover(RowCoverPB cover) = _SetCover; - const factory RowBannerEvent.removeCover() = _RemoveCover; + const factory RowBannerEvent.setCover(String coverURL) = _SetCover; } @freezed -class RowBannerState extends Equatable with _$RowBannerState { - const RowBannerState._(); - +class RowBannerState with _$RowBannerState { const factory RowBannerState({ required FieldPB? primaryField, required RowMetaPB rowMeta, @@ -149,14 +125,6 @@ class RowBannerState extends Equatable with _$RowBannerState { rowMeta: rowMetaPB, loadingState: const LoadingState.loading(), ); - - @override - List get props => [ - rowMeta.cover.data, - rowMeta.icon, - primaryField, - loadingState, - ]; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart index be5ba29dfc..90f20b2fe7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_cache.dart @@ -1,6 +1,5 @@ import 'dart:collection'; -import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; @@ -45,8 +44,7 @@ class RowCache { for (final fieldInfo in fieldInfos) { _cellMemCache.removeCellWithFieldId(fieldInfo.id); } - - _changedNotifier?.receive(const ChangedReason.fieldDidChange()); + _changedNotifier.receive(const ChangedReason.fieldDidChange()); }); } @@ -55,9 +53,7 @@ class RowCache { final CellMemCache _cellMemCache; final RowLifeCycle _rowLifeCycle; final RowFieldsDelegate _fieldDelegate; - RowChangesetNotifier? _changedNotifier; - bool _isInitialRows = false; - final List _pendingVisibilityChanges = []; + final RowChangesetNotifier _changedNotifier; /// Returns a unmodifiable list of RowInfo UnmodifiableListView get rowInfos { @@ -71,8 +67,7 @@ class RowCache { } CellMemCache get cellCache => _cellMemCache; - ChangedReason get changeReason => - _changedNotifier?.reason ?? const InitialListState(); + ChangedReason get changeReason => _changedNotifier.reason; RowInfo? getRow(RowId rowId) { return _rowList.get(rowId); @@ -83,29 +78,12 @@ class RowCache { final rowInfo = buildGridRow(row); _rowList.add(rowInfo); } - _isInitialRows = true; - _changedNotifier?.receive(const ChangedReason.setInitialRows()); - - for (final changeset in _pendingVisibilityChanges) { - applyRowsVisibility(changeset); - } - _pendingVisibilityChanges.clear(); - } - - void setRowMeta(RowMetaPB rowMeta) { - final rowInfo = _rowList.get(rowMeta.id); - if (rowInfo != null) { - rowInfo.updateRowMeta(rowMeta); - } - - _changedNotifier?.receive(const ChangedReason.didFetchRow()); + _changedNotifier.receive(const ChangedReason.setInitialRows()); } void dispose() { - _rowList.dispose(); _rowLifeCycle.onRowDisposed(); - _changedNotifier?.dispose(); - _changedNotifier = null; + _changedNotifier.dispose(); _cellMemCache.dispose(); } @@ -116,20 +94,13 @@ class RowCache { } void applyRowsVisibility(RowsVisibilityChangePB changeset) { - if (_isInitialRows) { - _hideRows(changeset.invisibleRows); - _showRows(changeset.visibleRows); - _changedNotifier?.receive( - ChangedReason.updateRowsVisibility(changeset), - ); - } else { - _pendingVisibilityChanges.add(changeset); - } + _hideRows(changeset.invisibleRows); + _showRows(changeset.visibleRows); } void reorderAllRows(List rowIds) { _rowList.reorderWithRowIds(rowIds); - _changedNotifier?.receive(const ChangedReason.reorderRows()); + _changedNotifier.receive(const ChangedReason.reorderRows()); } void reorderSingleRow(ReorderSingleRowPB reorderRow) { @@ -140,7 +111,7 @@ class RowCache { reorderRow.oldIndex, reorderRow.newIndex, ); - _changedNotifier?.receive( + _changedNotifier.receive( ChangedReason.reorderSingleRow( reorderRow, rowInfo, @@ -153,25 +124,19 @@ class RowCache { for (final rowId in deletedRowIds) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { - _changedNotifier?.receive(ChangedReason.delete(deletedRow)); + _changedNotifier.receive(ChangedReason.delete(deletedRow)); } } } void _insertRows(List insertRows) { - final InsertedIndexs insertedIndices = []; for (final insertedRow in insertRows) { - if (insertedRow.hasIndex()) { - final index = _rowList.insert( - insertedRow.index, - buildGridRow(insertedRow.rowMeta), - ); - if (index != null) { - insertedIndices.add(index); - } + final insertedIndex = + _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); + if (insertedIndex != null) { + _changedNotifier.receive(ChangedReason.insert(insertedIndex)); } } - _changedNotifier?.receive(ChangedReason.insert(insertedIndices)); } void _updateRows(List updatedRows) { @@ -190,13 +155,11 @@ class RowCache { } } - final updatedIndexs = _rowList.updateRows( - rowMetas: updatedList, - builder: (rowId) => buildGridRow(rowId), - ); + final updatedIndexs = + _rowList.updateRows(updatedList, (rowId) => buildGridRow(rowId)); if (updatedIndexs.isNotEmpty) { - _changedNotifier?.receive(ChangedReason.update(updatedIndexs)); + _changedNotifier.receive(ChangedReason.update(updatedIndexs)); } } @@ -204,7 +167,7 @@ class RowCache { for (final rowId in invisibleRows) { final deletedRow = _rowList.remove(rowId); if (deletedRow != null) { - _changedNotifier?.receive(ChangedReason.delete(deletedRow)); + _changedNotifier.receive(ChangedReason.delete(deletedRow)); } } } @@ -214,16 +177,14 @@ class RowCache { final insertedIndex = _rowList.insert(insertedRow.index, buildGridRow(insertedRow.rowMeta)); if (insertedIndex != null) { - _changedNotifier?.receive(ChangedReason.insert([insertedIndex])); + _changedNotifier.receive(ChangedReason.insert(insertedIndex)); } } } void onRowsChanged(void Function(ChangedReason) onRowChanged) { - _changedNotifier?.addListener(() { - if (_changedNotifier != null) { - onRowChanged(_changedNotifier!.reason); - } + _changedNotifier.addListener(() { + onRowChanged(_changedNotifier.reason); }); } @@ -236,19 +197,17 @@ class RowCache { final rowInfo = _rowList.get(rowId); if (rowInfo != null) { final cellDataMap = _makeCells(rowInfo.rowMeta); - if (_changedNotifier != null) { - onRowChanged(cellDataMap, _changedNotifier!.reason); - } + onRowChanged(cellDataMap, _changedNotifier.reason); } } } - _changedNotifier?.addListener(listenerHandler); + _changedNotifier.addListener(listenerHandler); return listenerHandler; } void removeRowListener(VoidCallback callback) { - _changedNotifier?.removeListener(callback); + _changedNotifier.removeListener(callback); } List loadCells(RowMetaPB rowMeta) { @@ -256,8 +215,7 @@ class RowCache { if (rowInfo == null) { _loadRow(rowMeta.id); } - final cells = _makeCells(rowMeta); - return cells; + return _makeCells(rowMeta); } Future _loadRow(RowId rowId) async { @@ -267,7 +225,9 @@ class RowCache { final rowInfo = _rowList.get(rowMetaPB.id); final rowIndex = _rowList.indexOfRow(rowMetaPB.id); if (rowInfo != null && rowIndex != null) { - rowInfo.rowMetaNotifier.value = rowMetaPB; + final updatedRowInfo = rowInfo.copyWith(rowMeta: rowMetaPB); + _rowList.remove(rowMetaPB.id); + _rowList.insert(rowIndex, updatedRowInfo); final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); updatedIndexs[rowMetaPB.id] = UpdatedIndex( @@ -275,7 +235,7 @@ class RowCache { rowId: rowMetaPB.id, ); - _changedNotifier?.receive(ChangedReason.update(updatedIndexs)); + _changedNotifier.receive(ChangedReason.update(updatedIndexs)); } }, (err) => Log.error(err), @@ -316,48 +276,20 @@ class RowChangesetNotifier extends ChangeNotifier { initial: (_) {}, reorderRows: (_) => notifyListeners(), reorderSingleRow: (_) => notifyListeners(), - updateRowsVisibility: (_) => notifyListeners(), setInitialRows: (_) => notifyListeners(), - didFetchRow: (_) => notifyListeners(), ); } } -class RowInfo extends Equatable { - RowInfo({ - required this.fields, +@unfreezed +class RowInfo with _$RowInfo { + const RowInfo._(); + factory RowInfo({ + required UnmodifiableListView fields, required RowMetaPB rowMeta, - }) : rowMetaNotifier = ValueNotifier(rowMeta), - rowIconNotifier = ValueNotifier(rowMeta.icon), - rowDocumentNotifier = ValueNotifier( - !(rowMeta.hasIsDocumentEmpty() ? rowMeta.isDocumentEmpty : true), - ); + }) = _RowInfo; - final UnmodifiableListView fields; - final ValueNotifier rowMetaNotifier; - final ValueNotifier rowIconNotifier; - final ValueNotifier rowDocumentNotifier; - - String get rowId => rowMetaNotifier.value.id; - - RowMetaPB get rowMeta => rowMetaNotifier.value; - - /// Updates the RowMeta and automatically updates the related notifiers. - void updateRowMeta(RowMetaPB newMeta) { - rowMetaNotifier.value = newMeta; - rowIconNotifier.value = newMeta.icon; - rowDocumentNotifier.value = !newMeta.isDocumentEmpty; - } - - /// Dispose of the notifiers when they are no longer needed. - void dispose() { - rowMetaNotifier.dispose(); - rowIconNotifier.dispose(); - rowDocumentNotifier.dispose(); - } - - @override - List get props => [rowMeta]; + String get rowId => rowMeta.id; } typedef InsertedIndexs = List; @@ -368,20 +300,16 @@ typedef UpdatedIndexMap = LinkedHashMap; @freezed class ChangedReason with _$ChangedReason { - const factory ChangedReason.insert(InsertedIndexs items) = _Insert; + const factory ChangedReason.insert(InsertedIndex item) = _Insert; const factory ChangedReason.delete(DeletedIndex item) = _Delete; const factory ChangedReason.update(UpdatedIndexMap indexs) = _Update; const factory ChangedReason.fieldDidChange() = _FieldDidChange; const factory ChangedReason.initial() = InitialListState; - const factory ChangedReason.didFetchRow() = _DidFetchRow; const factory ChangedReason.reorderRows() = _ReorderRows; const factory ChangedReason.reorderSingleRow( ReorderSingleRowPB reorderRow, RowInfo rowInfo, ) = _ReorderSingleRow; - const factory ChangedReason.updateRowsVisibility( - RowsVisibilityChangePB changeset, - ) = _UpdateRowsVisibility; const factory ChangedReason.setInitialRows() = _SetInitialRows; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart index 0d2bf4985d..b34beba275 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_controller.dart @@ -1,8 +1,4 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/domain/row_listener.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter/material.dart'; import '../cell/cell_cache.dart'; @@ -13,92 +9,35 @@ typedef OnRowChanged = void Function(List, ChangedReason); class RowController { RowController({ - required RowMetaPB rowMeta, + required this.rowMeta, required this.viewId, required RowCache rowCache, this.groupId, - }) : _rowMeta = rowMeta, - _rowCache = rowCache, - _rowBackendSvc = RowBackendService(viewId: viewId), - _rowListener = RowListener(rowMeta.id); + }) : _rowCache = rowCache; - RowMetaPB _rowMeta; + final RowMetaPB rowMeta; final String? groupId; - VoidCallback? _onRowMetaChanged; final String viewId; final List _onRowChangedListeners = []; final RowCache _rowCache; - final RowListener _rowListener; - final RowBackendService _rowBackendSvc; - bool _isDisposed = false; - String get rowId => _rowMeta.id; - RowMetaPB get rowMeta => _rowMeta; CellMemCache get cellCache => _rowCache.cellCache; - List loadCells() => _rowCache.loadCells(rowMeta); + String get rowId => rowMeta.id; - /// This method must be called to initialize the row controller; otherwise, the row will not sync between devices. - /// When creating a row controller, calling [initialize] immediately may not be necessary. - /// Only call [initialize] when the row becomes visible. This approach helps reduce unnecessary sync operations. - Future initialize() async { - await _rowBackendSvc.initRow(rowMeta.id); - unawaited( - _rowBackendSvc.getRowMeta(rowId).then( - (result) { - if (_isDisposed) { - return; - } + List loadData() => _rowCache.loadCells(rowMeta); - result.fold( - (rowMeta) { - _rowMeta = rowMeta; - _rowCache.setRowMeta(rowMeta); - _onRowMetaChanged?.call(); - }, - (error) => debugPrint(error.toString()), - ); - }, - ), - ); - - _rowListener.start( - onRowFetched: (DidFetchRowPB row) { - _rowCache.setRowMeta(row.meta); - }, - onMetaChanged: (newRowMeta) { - if (_isDisposed) { - return; - } - _rowMeta = newRowMeta; - _rowCache.setRowMeta(newRowMeta); - _onRowMetaChanged?.call(); - }, - ); - } - - void addListener({ - OnRowChanged? onRowChanged, - VoidCallback? onMetaChanged, - }) { + void addListener({OnRowChanged? onRowChanged}) { final fn = _rowCache.addListener( rowId: rowMeta.id, - onRowChanged: (context, reasons) { - if (_isDisposed) { - return; - } - onRowChanged?.call(context, reasons); - }, + onRowChanged: onRowChanged, ); // Add the listener to the list so that we can remove it later. _onRowChangedListeners.add(fn); - _onRowMetaChanged = onMetaChanged; } - Future dispose() async { - _isDisposed = true; - await _rowListener.stop(); + void dispose() { for (final fn in _onRowChangedListeners) { _rowCache.removeRowListener(fn); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart index 00b0745448..b31a3022e9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_list.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:math'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; @@ -118,23 +117,20 @@ class RowList { return deletedIndex; } - UpdatedIndexMap updateRows({ - required List rowMetas, - required RowInfo Function(RowMetaPB) builder, - }) { + UpdatedIndexMap updateRows( + List rowMetas, + RowInfo Function(RowMetaPB) builder, + ) { final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); for (final rowMeta in rowMetas) { final index = _rowInfos.indexWhere( (rowInfo) => rowInfo.rowId == rowMeta.id, ); if (index != -1) { - rowInfoByRowId[rowMeta.id]?.updateRowMeta(rowMeta); - } else { - final insertIndex = max(index, _rowInfos.length); final rowInfo = builder(rowMeta); - insert(insertIndex, rowInfo); + insert(index, rowInfo); updatedIndexs[rowMeta.id] = UpdatedIndex( - index: insertIndex, + index: index, rowId: rowMeta.id, ); } @@ -166,11 +162,4 @@ class RowList { bool contains(RowId rowId) { return rowInfoByRowId[rowId] != null; } - - void dispose() { - for (final rowInfo in _rowInfos) { - rowInfo.dispose(); - } - _rowInfos.clear(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart index 151a32d961..1866891336 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart @@ -37,14 +37,6 @@ class RowBackendService { return DatabaseEventCreateRow(payload).send(); } - Future> initRow(RowId rowId) async { - final payload = DatabaseViewRowIdPB() - ..viewId = viewId - ..rowId = rowId; - - return DatabaseEventInitRow(payload).send(); - } - Future> createRowBefore(RowId rowId) { return createRow( viewId: viewId, @@ -65,7 +57,7 @@ class RowBackendService { required String viewId, required String rowId, }) { - final payload = DatabaseViewRowIdPB() + final payload = RowIdPB() ..viewId = viewId ..rowId = rowId; @@ -73,7 +65,7 @@ class RowBackendService { } Future> getRowMeta(RowId rowId) { - final payload = DatabaseViewRowIdPB.create() + final payload = RowIdPB.create() ..viewId = viewId ..rowId = rowId; @@ -83,7 +75,7 @@ class RowBackendService { Future> updateMeta({ required String rowId, String? iconURL, - RowCoverPB? cover, + String? coverURL, bool? isDocumentEmpty, }) { final payload = UpdateRowMetaChangesetPB.create() @@ -93,8 +85,8 @@ class RowBackendService { if (iconURL != null) { payload.iconUrl = iconURL; } - if (cover != null) { - payload.cover = cover; + if (coverURL != null) { + payload.coverUrl = coverURL; } if (isDocumentEmpty != null) { @@ -104,14 +96,6 @@ class RowBackendService { return DatabaseEventUpdateRowMeta(payload).send(); } - Future> removeCover(String rowId) async { - final payload = RemoveCoverPayloadPB.create() - ..viewId = viewId - ..rowId = rowId; - - return DatabaseEventRemoveCover(payload).send(); - } - static Future> deleteRows( String viewId, List rowIds, @@ -127,7 +111,7 @@ class RowBackendService { String viewId, RowId rowId, ) { - final payload = DatabaseViewRowIdPB( + final payload = RowIdPB( viewId: viewId, rowId: rowId, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart index 351dea2cd8..ae0b9173c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -30,9 +30,9 @@ class DatabaseSyncBloc extends Bloc { .then((value) => value.fold((s) => s, (f) => null)); emit( state.copyWith( - shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server && - databaseId != null, + shouldShowIndicator: userProfile?.authenticator == + AuthenticatorPB.AppFlowyCloud && + databaseId != null, ), ); if (databaseId != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart index e55bbb96a4..943f8ec20c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart @@ -1,16 +1,15 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; -import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'database_controller.dart'; @@ -18,17 +17,8 @@ part 'tab_bar_bloc.freezed.dart'; class DatabaseTabBarBloc extends Bloc { - DatabaseTabBarBloc({ - required ViewPB view, - required String compactModeId, - required bool enableCompactMode, - }) : super( - DatabaseTabBarState.initial( - view, - compactModeId, - enableCompactMode, - ), - ) { + DatabaseTabBarBloc({required ViewPB view}) + : super(DatabaseTabBarState.initial(view)) { on( (event, emit) async { await event.when( @@ -62,7 +52,7 @@ class DatabaseTabBarBloc _createLinkedView(layout.layoutType, name ?? layout.layoutName); }, deleteView: (String viewId) async { - final result = await ViewBackendService.deleteView(viewId: viewId); + final result = await ViewBackendService.delete(viewId: viewId); result.fold( (l) {}, (r) => Log.error(r), @@ -164,13 +154,10 @@ class DatabaseTabBarBloc ) { final tabBarControllerByViewId = {...state.tabBarControllerByViewId}; for (final view in newViews) { - final controller = DatabaseTabBarController( - view: view, - compactModeId: state.compactModeId, - enableCompactMode: state.enableCompactMode, - )..onViewUpdated = (newView) { - add(DatabaseTabBarEvent.viewDidUpdate(newView)); - }; + final controller = DatabaseTabBarController(view: view); + controller.onViewUpdated = (newView) { + add(DatabaseTabBarEvent.viewDidUpdate(newView)); + }; tabBarControllerByViewId[view.id] = controller; } @@ -218,27 +205,20 @@ class DatabaseTabBarBloc @freezed class DatabaseTabBarEvent with _$DatabaseTabBarEvent { const factory DatabaseTabBarEvent.initial() = _Initial; - const factory DatabaseTabBarEvent.didLoadChildViews( List childViews, ) = _DidLoadChildViews; - const factory DatabaseTabBarEvent.selectView(String viewId) = _DidSelectView; - const factory DatabaseTabBarEvent.createView( DatabaseLayoutPB layout, String? name, ) = _CreateView; - const factory DatabaseTabBarEvent.renameView(String viewId, String newName) = _RenameView; - const factory DatabaseTabBarEvent.deleteView(String viewId) = _DeleteView; - const factory DatabaseTabBarEvent.didUpdateChildViews( ChildViewUpdatePB updatePB, ) = _DidUpdateChildViews; - const factory DatabaseTabBarEvent.viewDidUpdate(ViewPB view) = _ViewDidUpdate; } @@ -247,29 +227,19 @@ class DatabaseTabBarState with _$DatabaseTabBarState { const factory DatabaseTabBarState({ required ViewPB parentView, required int selectedIndex, - required String compactModeId, - required bool enableCompactMode, required List tabBars, required Map tabBarControllerByViewId, }) = _DatabaseTabBarState; - factory DatabaseTabBarState.initial( - ViewPB view, - String compactModeId, - bool enableCompactMode, - ) { + factory DatabaseTabBarState.initial(ViewPB view) { final tabBar = DatabaseTabBar(view: view); return DatabaseTabBarState( parentView: view, selectedIndex: 0, - compactModeId: compactModeId, - enableCompactMode: enableCompactMode, tabBars: [tabBar], tabBarControllerByViewId: { view.id: DatabaseTabBarController( view: view, - compactModeId: compactModeId, - enableCompactMode: enableCompactMode, ), }, ); @@ -279,7 +249,7 @@ class DatabaseTabBarState with _$DatabaseTabBarState { class DatabaseTabBar extends Equatable { DatabaseTabBar({ required this.view, - }) : _builder = UniversalPlatform.isMobile + }) : _builder = PlatformExtension.isMobile ? view.mobileTabBarItem() : view.tabBarItem(); @@ -287,9 +257,7 @@ class DatabaseTabBar extends Equatable { final DatabaseTabBarItemBuilder _builder; String get viewId => view.id; - DatabaseTabBarItemBuilder get builder => _builder; - ViewLayoutPB get layout => view.layout; @override @@ -306,18 +274,8 @@ typedef OnViewChildViewChanged = void Function( ); class DatabaseTabBarController { - DatabaseTabBarController({ - required this.view, - required String compactModeId, - required bool enableCompactMode, - }) : controller = DatabaseController(view: view) - ..initCompactMode(enableCompactMode) - ..addListener( - onCompactModeChanged: (v) async { - compactModeEventBus - .fire(CompactModeEvent(id: compactModeId, enable: v)); - }, - ), + DatabaseTabBarController({required this.view}) + : controller = DatabaseController(view: view), viewListener = ViewListener(viewId: view.id) { viewListener.start( onViewChildViewsUpdated: (update) => onViewChildViewChanged?.call(update), @@ -335,6 +293,7 @@ class DatabaseTabBarController { OnViewChildViewChanged? onViewChildViewChanged; Future dispose() async { - await Future.wait([viewListener.stop(), controller.dispose()]); + await viewListener.stop(); + await controller.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart index 754b2d1c23..77670fb0bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_cache.dart @@ -69,7 +69,11 @@ class DatabaseViewCache { if (changeset.insertedRows.isNotEmpty) { for (final callback in _callbacks) { - callback.onRowsCreated?.call(changeset.insertedRows); + callback.onRowsCreated?.call( + changeset.insertedRows + .map((insertedRow) => insertedRow.rowMeta.id) + .toList(), + ); } } }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart index 3f97304296..6a41e2f173 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/view/view_listener.dart @@ -7,88 +7,83 @@ import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart' import 'package:appflowy_backend/protobuf/flowy-database2/view_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; -typedef RowsVisibilityCallback = void Function( - FlowyResult, -); -typedef NumberOfRowsCallback = void Function( - FlowyResult, -); -typedef ReorderAllRowsCallback = void Function( - FlowyResult, FlowyError>, -); -typedef SingleRowCallback = void Function( - FlowyResult, -); +typedef RowsVisibilityNotifierValue + = FlowyResult; + +typedef NumberOfRowsNotifierValue = FlowyResult; +typedef ReorderAllRowsNotifierValue = FlowyResult, FlowyError>; +typedef SingleRowNotifierValue = FlowyResult; class DatabaseViewListener { DatabaseViewListener({required this.viewId}); final String viewId; + + PublishNotifier? _rowsNotifier = PublishNotifier(); + PublishNotifier? _reorderAllRows = + PublishNotifier(); + PublishNotifier? _reorderSingleRow = + PublishNotifier(); + PublishNotifier? _rowsVisibility = + PublishNotifier(); + DatabaseNotificationListener? _listener; void start({ - required NumberOfRowsCallback onRowsChanged, - required ReorderAllRowsCallback onReorderAllRows, - required SingleRowCallback onReorderSingleRow, - required RowsVisibilityCallback onRowsVisibilityChanged, + required void Function(NumberOfRowsNotifierValue) onRowsChanged, + required void Function(ReorderAllRowsNotifierValue) onReorderAllRows, + required void Function(SingleRowNotifierValue) onReorderSingleRow, + required void Function(RowsVisibilityNotifierValue) onRowsVisibilityChanged, }) { - // Stop any existing listener - _listener?.stop(); + if (_listener != null) { + _listener?.stop(); + } - // Initialize the notification listener _listener = DatabaseNotificationListener( objectId: viewId, - handler: (ty, result) => _handler( - ty, - result, - onRowsChanged, - onReorderAllRows, - onReorderSingleRow, - onRowsVisibilityChanged, - ), + handler: _handler, ); + + _rowsNotifier?.addPublishListener(onRowsChanged); + _rowsVisibility?.addPublishListener(onRowsVisibilityChanged); + _reorderAllRows?.addPublishListener(onReorderAllRows); + _reorderSingleRow?.addPublishListener(onReorderSingleRow); } void _handler( DatabaseNotification ty, FlowyResult result, - NumberOfRowsCallback onRowsChanged, - ReorderAllRowsCallback onReorderAllRows, - SingleRowCallback onReorderSingleRow, - RowsVisibilityCallback onRowsVisibilityChanged, ) { switch (ty) { case DatabaseNotification.DidUpdateViewRowsVisibility: result.fold( - (payload) => onRowsVisibilityChanged( - FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)), - ), - (error) => onRowsVisibilityChanged(FlowyResult.failure(error)), + (payload) => _rowsVisibility?.value = + FlowyResult.success(RowsVisibilityChangePB.fromBuffer(payload)), + (error) => _rowsVisibility?.value = FlowyResult.failure(error), ); break; case DatabaseNotification.DidUpdateRow: result.fold( - (payload) => onRowsChanged( - FlowyResult.success(RowsChangePB.fromBuffer(payload)), - ), - (error) => onRowsChanged(FlowyResult.failure(error)), + (payload) => _rowsNotifier?.value = + FlowyResult.success(RowsChangePB.fromBuffer(payload)), + (error) => _rowsNotifier?.value = FlowyResult.failure(error), ); break; case DatabaseNotification.DidReorderRows: result.fold( - (payload) => onReorderAllRows( - FlowyResult.success(ReorderAllRowsPB.fromBuffer(payload).rowOrders), + (payload) => _reorderAllRows?.value = FlowyResult.success( + ReorderAllRowsPB.fromBuffer(payload).rowOrders, ), - (error) => onReorderAllRows(FlowyResult.failure(error)), + (error) => _reorderAllRows?.value = FlowyResult.failure(error), ); break; case DatabaseNotification.DidReorderSingleRow: result.fold( - (payload) => onReorderSingleRow( - FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), - ), - (error) => onReorderSingleRow(FlowyResult.failure(error)), + (payload) => _reorderSingleRow?.value = + FlowyResult.success(ReorderSingleRowPB.fromBuffer(payload)), + (error) => _reorderSingleRow?.value = FlowyResult.failure(error), ); break; default: @@ -98,6 +93,16 @@ class DatabaseViewListener { Future stop() async { await _listener?.stop(); - _listener = null; + _rowsVisibility?.dispose(); + _rowsVisibility = null; + + _rowsNotifier?.dispose(); + _rowsNotifier = null; + + _reorderAllRows?.dispose(); + _reorderAllRows = null; + + _reorderSingleRow?.dispose(); + _reorderSingleRow = null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart index 12a1603430..99da7d48f0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_actions_bloc.dart @@ -40,7 +40,6 @@ class BoardActionsCubit extends Cubit { } void startCreateBottomRow(String groupId) { - emit(const BoardActionsState.setFocus(groupedRowIds: [])); emit(BoardActionsState.startCreateBottomRow(groupId: groupId)); emit(const BoardActionsState.initial()); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart index a2c6c89578..0fa07a69c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart @@ -3,15 +3,13 @@ import 'dart:collection'; import 'package:appflowy/plugins/database/application/defines.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/board/group_ext.dart'; import 'package:appflowy/plugins/database/domain/group_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; @@ -19,7 +17,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; -import 'package:universal_platform/universal_platform.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:calendar_view/calendar_view.dart'; import '../../application/database_controller.dart'; import '../../application/field/field_controller.dart'; @@ -49,16 +49,9 @@ class BoardBloc extends Bloc { late final GroupBackendService groupBackendSvc; - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - FieldController get fieldController => databaseController.fieldController; String get viewId => databaseController.viewId; - DatabaseCallbacks? _databaseCallbacks; - DatabaseLayoutSettingCallbacks? _layoutSettingsCallback; - GroupCallbacks? _groupCallbacks; - void _initBoardController(AppFlowyBoardController? controller) { boardController = controller ?? AppFlowyBoardController( @@ -103,12 +96,6 @@ class BoardBloc extends Bloc { emit(BoardState.initial(viewId)); _startListening(); await _openDatabase(emit); - - final result = await UserEventGetUserProfile().send(); - result.fold( - (profile) => _userProfile = profile, - (err) => Log.error('Failed to fetch user profile: ${err.msg}'), - ); }, createRow: (groupId, position, title, targetRowId) async { final primaryField = databaseController.fieldController.fieldInfos @@ -126,7 +113,7 @@ class BoardBloc extends Bloc { ); final startEditing = position != OrderObjectPositionTypePB.End; - final action = UniversalPlatform.isMobile + final action = PlatformExtension.isMobile ? DidCreateRowAction.openAsPage : startEditing ? DidCreateRowAction.startEditing @@ -150,18 +137,11 @@ class BoardBloc extends Bloc { }, createGroup: (name) async { final result = await groupBackendSvc.createGroup(name: name); - result.onFailure(Log.error); + result.fold((_) {}, (err) => Log.error(err)); }, deleteGroup: (groupId) async { final result = await groupBackendSvc.deleteGroup(groupId: groupId); - result.onFailure(Log.error); - }, - renameGroup: (groupId, name) async { - final result = await groupBackendSvc.updateGroup( - groupId: groupId, - name: name, - ); - result.onFailure(Log.error); + result.fold((_) {}, (err) => Log.error(err)); }, didReceiveError: (error) { emit(BoardState.error(error: error)); @@ -222,17 +202,11 @@ class BoardBloc extends Bloc { ); }, endEditingHeader: (String groupId, String? groupName) async { - final group = groupControllers[groupId]?.group; - if (group != null) { - final currentName = group.generateGroupName(databaseController); - if (currentName != groupName) { - await groupBackendSvc.updateGroup( - groupId: groupId, - name: groupName, - ); - } - } - + await groupBackendSvc.updateGroup( + fieldId: groupControllers.values.first.group.fieldId, + groupId: groupId, + name: groupName, + ); state.maybeMap( ready: (state) => emit(state.copyWith(editingHeaderId: null)), orElse: () {}, @@ -277,11 +251,6 @@ class BoardBloc extends Bloc { ); } }, - openRowDetail: (rowMeta) { - final copyState = state; - emit(BoardState.openRowDetail(rowMeta: rowMeta)); - emit(copyState); - }, ); }, ); @@ -306,6 +275,7 @@ class BoardBloc extends Bloc { ); } else { await groupBackendSvc.updateGroup( + fieldId: groupControllers.values.first.group.fieldId, groupId: group.groupId, visible: isVisible, ); @@ -334,17 +304,6 @@ class BoardBloc extends Bloc { for (final controller in groupControllers.values) { await controller.dispose(); } - - databaseController.removeListener( - onDatabaseChanged: _databaseCallbacks, - onLayoutSettingsChanged: _layoutSettingsCallback, - onGroupChanged: _groupCallbacks, - ); - - _databaseCallbacks = null; - _layoutSettingsCallback = null; - _groupCallbacks = null; - boardController.dispose(); return super.close(); } @@ -395,7 +354,7 @@ class BoardBloc extends Bloc { RowCache get rowCache => databaseController.rowCache; void _startListening() { - _layoutSettingsCallback = DatabaseLayoutSettingCallbacks( + final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( onLayoutSettingsChanged: (layoutSettings) { if (isClosed) { return; @@ -418,7 +377,7 @@ class BoardBloc extends Bloc { add(BoardEvent.didUpdateLayoutSettings(layoutSettings.board)); }, ); - _groupCallbacks = GroupCallbacks( + final onGroupChanged = GroupCallbacks( onGroupByField: (groups) { if (isClosed) { return; @@ -470,9 +429,7 @@ class BoardBloc extends Bloc { boardController.getGroupController(group.groupId); if (columnController != null) { // remove the group or update its name - columnController.updateGroupName( - group.generateGroupName(databaseController), - ); + columnController.updateGroupName(generateGroupNameFromGroup(group)); if (!group.isVisible) { boardController.removeGroup(group.groupId); } @@ -480,7 +437,7 @@ class BoardBloc extends Bloc { final newGroup = _initializeGroupData(group); final visibleGroups = [...groupList]..retainWhere( (g) => - (g.isVisible && !g.isDefault) || + g.isVisible || g.isDefault && !hideUngrouped || g.groupId == group.groupId, ); @@ -497,20 +454,10 @@ class BoardBloc extends Bloc { add(BoardEvent.didReceiveGroups(groupList)); }, ); - _databaseCallbacks = DatabaseCallbacks( - onRowsCreated: (rows) { - for (final row in rows) { - if (!isClosed && row.isHiddenInView) { - add(BoardEvent.openRowDetail(row.rowMeta)); - } - } - }, - ); databaseController.addListener( - onDatabaseChanged: _databaseCallbacks, - onLayoutSettingsChanged: _layoutSettingsCallback, - onGroupChanged: _groupCallbacks, + onLayoutSettingsChanged: onLayoutSettingsChanged, + onGroupChanged: onGroupChanged, ); } @@ -534,8 +481,6 @@ class BoardBloc extends Bloc { } GroupController _initializeGroupController(GroupPB group) { - group.freeze(); - final delegate = GroupControllerDelegateImpl( controller: boardController, fieldController: fieldController, @@ -564,7 +509,7 @@ class BoardBloc extends Bloc { AppFlowyGroupData _initializeGroupData(GroupPB group) { return AppFlowyGroupData( id: group.groupId, - name: group.generateGroupName(databaseController), + name: generateGroupNameFromGroup(group), items: _buildGroupItems(group), customData: GroupData( group: group, @@ -572,6 +517,103 @@ class BoardBloc extends Bloc { ), ); } + + String generateGroupNameFromGroup(GroupPB group) { + final field = fieldController.getField(group.fieldId); + if (field == null) { + return ""; + } + + // if the group is the default group, then + if (group.isDefault) { + return "No ${field.name}"; + } + + final groupSettings = databaseController.fieldController.groupSettings + .firstWhereOrNull((gs) => gs.fieldId == field.id); + + switch (field.fieldType) { + case FieldType.SingleSelect: + final options = + SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) + .options; + final option = + options.firstWhereOrNull((option) => option.id == group.groupId); + return option == null ? "" : option.name; + case FieldType.MultiSelect: + final options = + MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) + .options; + final option = + options.firstWhereOrNull((option) => option.id == group.groupId); + return option == null ? "" : option.name; + case FieldType.Checkbox: + return group.groupId; + case FieldType.URL: + return group.groupId; + case FieldType.DateTime: + final config = groupSettings?.content != null + ? DateGroupConfigurationPB.fromBuffer(groupSettings!.content) + : DateGroupConfigurationPB(); + final dateFormat = DateFormat("y/MM/dd"); + try { + final targetDateTime = dateFormat.parseLoose(group.groupId); + switch (config.condition) { + case DateConditionPB.Day: + return DateFormat("MMM dd, y").format(targetDateTime); + case DateConditionPB.Week: + final beginningOfWeek = targetDateTime + .subtract(Duration(days: targetDateTime.weekday - 1)); + final endOfWeek = targetDateTime.add( + Duration(days: DateTime.daysPerWeek - targetDateTime.weekday), + ); + + final beginningOfWeekFormat = + beginningOfWeek.year != endOfWeek.year + ? "MMM dd y" + : "MMM dd"; + final endOfWeekFormat = beginningOfWeek.month != endOfWeek.month + ? "MMM dd y" + : "dd y"; + + return LocaleKeys.board_dateCondition_weekOf.tr( + args: [ + DateFormat(beginningOfWeekFormat).format(beginningOfWeek), + DateFormat(endOfWeekFormat).format(endOfWeek), + ], + ); + case DateConditionPB.Month: + return DateFormat("MMM y").format(targetDateTime); + case DateConditionPB.Year: + return DateFormat("y").format(targetDateTime); + case DateConditionPB.Relative: + final targetDateTimeDay = DateTime( + targetDateTime.year, + targetDateTime.month, + targetDateTime.day, + ); + final nowDay = DateTime.now().withoutTime; + final diff = targetDateTimeDay.difference(nowDay).inDays; + return switch (diff) { + 0 => LocaleKeys.board_dateCondition_today.tr(), + -1 => LocaleKeys.board_dateCondition_yesterday.tr(), + 1 => LocaleKeys.board_dateCondition_tomorrow.tr(), + -7 => LocaleKeys.board_dateCondition_lastSevenDays.tr(), + 2 => LocaleKeys.board_dateCondition_nextSevenDays.tr(), + -30 => LocaleKeys.board_dateCondition_lastThirtyDays.tr(), + 8 => LocaleKeys.board_dateCondition_nextThirtyDays.tr(), + _ => DateFormat("MMM y").format(targetDateTimeDay) + }; + default: + return ""; + } + } on FormatException { + return ""; + } + default: + return ""; + } + } } @freezed @@ -594,8 +636,6 @@ class BoardEvent with _$BoardEvent { ) = _SetGroupVisibility; const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) = _ToggleHiddenSectionVisibility; - const factory BoardEvent.renameGroup(String groupId, String name) = - _RenameGroup; const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup; const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) = _ReorderGroup; @@ -611,7 +651,6 @@ class BoardEvent with _$BoardEvent { GroupedRowId groupedRowId, bool toPrevious, ) = _MoveGroupToAdjacentGroup; - const factory BoardEvent.openRowDetail(RowMetaPB rowMeta) = _OpenRowDetail; } @freezed @@ -638,10 +677,6 @@ class BoardState with _$BoardState { required List groupedRowIds, }) = _BoardSetFocusState; - const factory BoardState.openRowDetail({ - required RowMetaPB rowMeta, - }) = _BoardOpenRowDetailState; - factory BoardState.initial(String viewId) => BoardState.ready( viewId: viewId, groupIds: [], @@ -667,7 +702,10 @@ class GroupItem extends AppFlowyGroupItem { GroupItem({ required this.row, required this.fieldInfo, - }); + bool draggable = true, + }) { + super.draggable.value = draggable; + } final RowMetaPB row; final FieldInfo fieldInfo; @@ -756,7 +794,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { return Log.warn("fieldInfo should not be null"); } - final item = GroupItem(row: row, fieldInfo: fieldInfo); + final item = GroupItem(row: row, fieldInfo: fieldInfo, draggable: false); if (index != null) { controller.insertGroupItem(group.groupId, index, item); @@ -769,7 +807,7 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { } class GroupData { - const GroupData({ + GroupData({ required this.group, required this.fieldInfo, }); diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart index 4e0ad9bada..4fa7a15672 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart @@ -50,10 +50,6 @@ class GroupController { } for (final insertedRow in changeset.insertedRows) { - if (newItems.any((rowPB) => rowPB.id == insertedRow.rowMeta.id)) { - continue; - } - final index = insertedRow.hasIndex() ? insertedRow.index : null; if (insertedRow.hasIndex() && newItems.length > insertedRow.index) { @@ -84,14 +80,11 @@ class GroupController { } } + group.freeze(); group = group.rebuild((group) { group.rows.clear(); group.rows.addAll(newItems); }); - group.freeze(); - Log.debug( - "Build GroupPB:${group.groupId}: items: ${group.rows.length}", - ); onGroupChanged(group); }, (err) => Log.error(err), @@ -100,8 +93,8 @@ class GroupController { ); } - Future dispose() async { - await _listener.stop(); + Future dispose() { + return _listener.stop(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/group_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/board/group_ext.dart deleted file mode 100644 index 5c69a66e62..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/group_ext.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:calendar_view/calendar_view.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension GroupName on GroupPB { - String generateGroupName(DatabaseController databaseController) { - final fieldController = databaseController.fieldController; - final field = fieldController.getField(fieldId); - if (field == null) { - return ""; - } - - // if the group is the default group, then - if (isDefault) { - return "No ${field.name}"; - } - - final groupSettings = databaseController.fieldController.groupSettings - .firstWhereOrNull((gs) => gs.fieldId == field.id); - - switch (field.fieldType) { - case FieldType.SingleSelect: - final options = - SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) - .options; - final option = - options.firstWhereOrNull((option) => option.id == groupId); - return option == null ? "" : option.name; - case FieldType.MultiSelect: - final options = - MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) - .options; - final option = - options.firstWhereOrNull((option) => option.id == groupId); - return option == null ? "" : option.name; - case FieldType.Checkbox: - return groupId; - case FieldType.URL: - return groupId; - case FieldType.DateTime: - final config = groupSettings?.content != null - ? DateGroupConfigurationPB.fromBuffer(groupSettings!.content) - : DateGroupConfigurationPB(); - final dateFormat = DateFormat("y/MM/dd"); - try { - final targetDateTime = dateFormat.parseLoose(groupId); - switch (config.condition) { - case DateConditionPB.Day: - return DateFormat("MMM dd, y").format(targetDateTime); - case DateConditionPB.Week: - final beginningOfWeek = targetDateTime - .subtract(Duration(days: targetDateTime.weekday - 1)); - final endOfWeek = targetDateTime.add( - Duration(days: DateTime.daysPerWeek - targetDateTime.weekday), - ); - - final beginningOfWeekFormat = - beginningOfWeek.year != endOfWeek.year - ? "MMM dd y" - : "MMM dd"; - final endOfWeekFormat = beginningOfWeek.month != endOfWeek.month - ? "MMM dd y" - : "dd y"; - - return LocaleKeys.board_dateCondition_weekOf.tr( - args: [ - DateFormat(beginningOfWeekFormat).format(beginningOfWeek), - DateFormat(endOfWeekFormat).format(endOfWeek), - ], - ); - case DateConditionPB.Month: - return DateFormat("MMM y").format(targetDateTime); - case DateConditionPB.Year: - return DateFormat("y").format(targetDateTime); - case DateConditionPB.Relative: - final targetDateTimeDay = DateTime( - targetDateTime.year, - targetDateTime.month, - targetDateTime.day, - ); - final nowDay = DateTime.now().withoutTime; - final diff = targetDateTimeDay.difference(nowDay).inDays; - return switch (diff) { - 0 => LocaleKeys.board_dateCondition_today.tr(), - -1 => LocaleKeys.board_dateCondition_yesterday.tr(), - 1 => LocaleKeys.board_dateCondition_tomorrow.tr(), - -7 => LocaleKeys.board_dateCondition_lastSevenDays.tr(), - 2 => LocaleKeys.board_dateCondition_nextSevenDays.tr(), - -30 => LocaleKeys.board_dateCondition_lastThirtyDays.tr(), - 8 => LocaleKeys.board_dateCondition_nextThirtyDays.tr(), - _ => DateFormat("MMM y").format(targetDateTimeDay) - }; - default: - return ""; - } - } on FormatException { - return ""; - } - default: - return ""; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 70d00bcd25..c2e8273fd5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -8,27 +8,26 @@ import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/board/application/board_actions_bloc.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/shared/conditional_listenable_builder.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:appflowy_editor/appflowy_editor.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:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../widgets/card/card.dart'; import '../../widgets/cell/card_cell_builder.dart'; @@ -49,12 +48,11 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { bool shrinkWrap, String? initialRowId, ) => - UniversalPlatform.isDesktop + PlatformExtension.isDesktop ? DesktopBoardPage( key: _makeValueKey(controller), view: view, databaseController: controller, - shrinkWrap: shrinkWrap, ) : MobileBoardPage( key: _makeValueKey(controller), @@ -99,7 +97,6 @@ class DesktopBoardPage extends StatefulWidget { required this.view, required this.databaseController, this.onEditStateChanged, - this.shrinkWrap = false, }); final ViewPB view; @@ -109,9 +106,6 @@ class DesktopBoardPage extends StatefulWidget { /// Called when edit state changed final VoidCallback? onEditStateChanged; - /// If true, the board will shrink wrap its content - final bool shrinkWrap; - @override State createState() => _DesktopBoardPageState(); } @@ -184,7 +178,9 @@ class _DesktopBoardPageState extends State { _focusScope.dispose(); _boardBloc.close(); _boardActionsCubit.close(); - _didCreateRow.dispose(); + _didCreateRow + ..removeListener(_handleDidCreateRow) + ..dispose(); super.dispose(); } @@ -192,21 +188,26 @@ class _DesktopBoardPageState extends State { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value(value: _boardBloc), - BlocProvider.value(value: _boardActionsCubit), + BlocProvider.value( + value: _boardBloc, + ), + BlocProvider.value( + value: _boardActionsCubit, + ), ], child: BlocBuilder( builder: (context, state) => state.maybeMap( loading: (_) => const Center( child: CircularProgressIndicator.adaptive(), ), - error: (err) => Center(child: AppFlowyErrorPage(error: err.error)), + error: (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), orElse: () => _BoardContent( - shrinkWrap: widget.shrinkWrap, onEditStateChanged: widget.onEditStateChanged, focusScope: _focusScope, boardController: _boardController, - view: widget.view, ), ), ), @@ -241,16 +242,12 @@ class _BoardContent extends StatefulWidget { const _BoardContent({ required this.boardController, required this.focusScope, - required this.view, this.onEditStateChanged, - this.shrinkWrap = false, }); final AppFlowyBoardController boardController; final BoardFocusScope focusScope; final VoidCallback? onEditStateChanged; - final bool shrinkWrap; - final ViewPB view; @override State<_BoardContent> createState() => _BoardContentState(); @@ -285,9 +282,6 @@ class _BoardContentState extends State<_BoardContent> { @override Widget build(BuildContext context) { - final horizontalPadding = - context.read()?.horizontalPadding ?? - 0.0; return MultiBlocListener( listeners: [ BlocListener( @@ -296,14 +290,6 @@ class _BoardContentState extends State<_BoardContent> { ready: (value) { widget.onEditStateChanged?.call(); }, - openRowDetail: (value) { - _openCard( - context: context, - databaseController: - context.read().databaseController, - rowMeta: value.rowMeta, - ); - }, orElse: () {}, ); }, @@ -340,92 +326,60 @@ class _BoardContentState extends State<_BoardContent> { focusScope: widget.focusScope, child: Padding( padding: const EdgeInsets.only(top: 8.0), - child: ValueListenableBuilder( - valueListenable: databaseController.compactModeNotifier, - builder: (context, compactMode, _) { - return AppFlowyBoard( - boardScrollController: scrollManager, - scrollController: scrollController, - shrinkWrap: widget.shrinkWrap, - controller: context.read().boardController, - groupConstraints: - BoxConstraints.tightFor(width: compactMode ? 196 : 256), - config: config, - leading: HiddenGroupsColumn( - shrinkWrap: widget.shrinkWrap, - margin: config.groupHeaderPadding + - EdgeInsets.only( - left: widget.shrinkWrap ? horizontalPadding : 0.0, - ), - ), - trailing: context - .read() - .groupingFieldType - ?.canCreateNewGroup ?? - false - ? BoardTrailing(scrollController: scrollController) - : const HSpace(40), - headerBuilder: (_, groupData) => BlocProvider.value( + child: AppFlowyBoard( + boardScrollController: scrollManager, + scrollController: scrollController, + controller: context.read().boardController, + groupConstraints: const BoxConstraints.tightFor(width: 256), + config: config, + leading: HiddenGroupsColumn(margin: config.groupHeaderPadding), + trailing: context + .read() + .groupingFieldType + ?.canCreateNewGroup ?? + false + ? BoardTrailing(scrollController: scrollController) + : const HSpace(40), + headerBuilder: (_, groupData) => BlocProvider.value( + value: context.read(), + child: BoardColumnHeader( + groupData: groupData, + margin: config.groupHeaderPadding, + ), + ), + footerBuilder: (_, groupData) => MultiBlocProvider( + providers: [ + BlocProvider.value( value: context.read(), - child: BoardColumnHeader( - databaseController: databaseController, - groupData: groupData, - margin: config.groupHeaderPadding, - ), ), - footerBuilder: (_, groupData) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: context.read()), - BlocProvider.value( - value: context.read(), - ), - ], - child: BoardColumnFooter( - columnData: groupData, - boardConfig: config, - scrollManager: scrollManager, - ), + BlocProvider.value( + value: context.read(), ), - cardBuilder: (cardContext, column, columnItem) => - MultiBlocProvider( - key: ValueKey("board_card_${column.id}_${columnItem.id}"), - providers: [ - BlocProvider.value( - value: cardContext.read(), - ), - BlocProvider.value( - value: cardContext.read(), - ), - BlocProvider( - create: (_) => ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - ], - child: BlocBuilder( - builder: (lockStatusContext, state) { - return IgnorePointer( - ignoring: state.isLocked, - child: _BoardCard( - afGroupData: column, - groupItem: columnItem as GroupItem, - boardConfig: config, - notifier: widget.focusScope, - cellBuilder: cellBuilder, - compactMode: compactMode, - onOpenCard: (rowMeta) => _openCard( - context: context, - databaseController: lockStatusContext - .read() - .databaseController, - rowMeta: rowMeta, - ), - ), - ); - }, - ), + ], + child: BoardColumnFooter( + columnData: groupData, + boardConfig: config, + scrollManager: scrollManager, + ), + ), + cardBuilder: (_, column, columnItem) => MultiBlocProvider( + key: ValueKey("board_card_${column.id}_${columnItem.id}"), + providers: [ + BlocProvider.value( + value: context.read(), ), - ); - }, + BlocProvider.value( + value: context.read(), + ), + ], + child: _BoardCard( + afGroupData: column, + groupItem: columnItem as GroupItem, + boardConfig: config, + notifier: widget.focusScope, + cellBuilder: cellBuilder, + ), + ), ), ), ), @@ -564,14 +518,12 @@ class _BoardColumnFooterState extends State { FlowySvgs.add_s, color: Theme.of(context).hintColor, ), - text: FlowyText( + text: FlowyText.medium( LocaleKeys.board_column_createNewCard.tr(), color: Theme.of(context).hintColor, ), onTap: () { - context - .read() - .startCreateBottomRow(widget.columnData.id); + setState(() => _isCreating = true); }, ), ), @@ -587,8 +539,6 @@ class _BoardCard extends StatefulWidget { required this.boardConfig, required this.cellBuilder, required this.notifier, - required this.compactMode, - required this.onOpenCard, }); final AppFlowyGroupData afGroupData; @@ -596,8 +546,6 @@ class _BoardCard extends StatefulWidget { final AppFlowyBoardConfig boardConfig; final CardCellBuilder cellBuilder; final BoardFocusScope notifier; - final bool compactMode; - final void Function(RowMetaPB) onOpenCard; @override State<_BoardCard> createState() => _BoardCardState(); @@ -609,8 +557,10 @@ class _BoardCardState extends State<_BoardCard> { @override Widget build(BuildContext context) { final boardBloc = context.read(); + final groupData = widget.afGroupData.customData as GroupData; final rowCache = boardBloc.rowCache; + final databaseController = boardBloc.databaseController; final rowMeta = rowCache.getRow(widget.groupItem.id)?.rowMeta ?? widget.groupItem.row; @@ -684,21 +634,15 @@ class _BoardCardState extends State<_BoardCard> { return previousContainsFocus != currentContainsFocus; }, - builder: (context, focusedItems, child) { - final cardMargin = widget.boardConfig.cardMargin; - final margin = widget.compactMode - ? cardMargin - EdgeInsets.symmetric(horizontal: 2) - : cardMargin; - return Container( - margin: margin, - decoration: _makeBoxDecoration( - context, - groupData.group.groupId, - widget.groupItem.id, - ), - child: child, - ); - }, + builder: (context, focusedItems, child) => Container( + margin: widget.boardConfig.cardMargin, + decoration: _makeBoxDecoration( + context, + groupData.group.groupId, + widget.groupItem.id, + ), + child: child, + ), child: RowCard( fieldController: databaseController.fieldController, rowMeta: rowMeta, @@ -707,8 +651,10 @@ class _BoardCardState extends State<_BoardCard> { groupingFieldId: widget.groupItem.fieldInfo.id, isEditing: _isEditing, cellBuilder: widget.cellBuilder, - onTap: (context) => widget.onOpenCard( - context.read().rowController.rowMeta, + onTap: (context) => _openCard( + context: context, + databaseController: databaseController, + rowMeta: context.read().state.rowMeta, ), onShiftTap: (_) { Focus.of(context).requestFocus(); @@ -742,7 +688,6 @@ class _BoardCardState extends State<_BoardCard> { rowId: rowMeta.id, ), ), - userProfile: context.read().userProfile, ), ), ), @@ -763,19 +708,19 @@ class _BoardCardState extends State<_BoardCard> { .isFocused(GroupedRowId(rowId: rowId, groupId: groupId)) ? Theme.of(context).colorScheme.primary : Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withValues(alpha: 0.12) + ? const Color(0xFF1F2329).withOpacity(0.12) : const Color(0xFF59647A), ), ), boxShadow: [ BoxShadow( blurRadius: 4, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), ), BoxShadow( blurRadius: 4, spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), ), ], ); @@ -854,7 +799,7 @@ class _BoardTrailingState extends State { suffixIcon: Padding( padding: const EdgeInsets.only(left: 4, bottom: 8.0), child: FlowyIconButton( - icon: const FlowySvg(FlowySvgs.close_filled_s), + icon: const FlowySvg(FlowySvgs.close_filled_m), hoverColor: Colors.transparent, onPressed: () => _textController.clear(), ), @@ -905,13 +850,9 @@ void _openCard({ FlowyOverlay.show( context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: databaseController, - rowController: rowController, - userProfile: context.read().userProfile, - ), + builder: (_) => RowDetailPage( + databaseController: databaseController, + rowController: rowController, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart index e57364b2d8..aa2883ff73 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart @@ -1,14 +1,11 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; class BoardSettingBar extends StatelessWidget { const BoardSettingBar({ @@ -22,39 +19,35 @@ class BoardSettingBar extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FilterEditorBloc( + return BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: databaseController.viewId, fieldController: databaseController.fieldController, - ), - child: ValueListenableBuilder( - valueListenable: databaseController.isLoading, - builder: (context, value, child) { - if (value) { - return const SizedBox.shrink(); - } - final isReference = - Provider.of(context)?.isReference ?? false; - return SizedBox( - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilterButton( - toggleExtension: toggleExtension, - ), - if (isReference) ...[ + )..add(const DatabaseFilterMenuEvent.initial()), + child: BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + child: ValueListenableBuilder( + valueListenable: databaseController.isLoading, + builder: (context, value, child) { + if (value) { + return const SizedBox.shrink(); + } + return SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const FilterButton(), const HSpace(2), - ViewDatabaseButton(view: databaseController.view), + SettingButton( + databaseController: databaseController, + ), ], - const HSpace(2), - SettingButton( - databaseController: databaseController, - ), - ], - ), - ); - }, + ), + ); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_checkbox_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_checkbox_column_header.dart deleted file mode 100644 index 8ac06bf2df..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_checkbox_column_header.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.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_board/appflowy_board.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'board_column_header.dart'; - -class CheckboxColumnHeader extends StatelessWidget { - const CheckboxColumnHeader({ - super.key, - required this.databaseController, - required this.groupData, - }); - - final DatabaseController databaseController; - final AppFlowyGroupData groupData; - - @override - Widget build(BuildContext context) { - final customData = groupData.customData as GroupData; - final groupName = customData.group.generateGroupName(databaseController); - return Row( - children: [ - FlowySvg( - customData.asCheckboxGroup()!.isCheck - ? FlowySvgs.check_filled_s - : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: const Size.square(18), - ), - const HSpace(6), - Expanded( - child: Align( - alignment: AlignmentDirectional.centerStart, - child: FlowyTooltip( - message: groupName, - child: FlowyText.medium( - groupName, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - const HSpace(6), - GroupOptionsButton( - groupData: groupData, - ), - const HSpace(4), - CreateCardFromTopButton( - groupId: groupData.id, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart index abd28ac022..4504563a97 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart @@ -1,30 +1,26 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/database/board/group_ext.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'board_checkbox_column_header.dart'; -import 'board_editable_column_header.dart'; - class BoardColumnHeader extends StatefulWidget { const BoardColumnHeader({ super.key, - required this.databaseController, required this.groupData, required this.margin, }); - final DatabaseController databaseController; final AppFlowyGroupData groupData; final EdgeInsets margin; @@ -33,61 +29,184 @@ class BoardColumnHeader extends StatefulWidget { } class _BoardColumnHeaderState extends State { - final ValueNotifier isEditing = ValueNotifier(false); + final FocusNode _focusNode = FocusNode(); + final FocusNode _keyboardListenerFocusNode = FocusNode(); - GroupData get customData => widget.groupData.customData; + late final TextEditingController _controller = + TextEditingController.fromValue( + TextEditingValue( + selection: TextSelection.collapsed( + offset: widget.groupData.headerData.groupName.length, + ), + text: widget.groupData.headerData.groupName, + ), + ); + + @override + void initState() { + super.initState(); + _focusNode.addListener(() { + if (!_focusNode.hasFocus) { + _saveEdit(); + } + }); + } @override void dispose() { - isEditing.dispose(); + _focusNode.dispose(); + _keyboardListenerFocusNode.dispose(); + _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final Widget child = switch (customData.fieldType) { - FieldType.MultiSelect || - FieldType.SingleSelect when !customData.group.isDefault => - EditableColumnHeader( - databaseController: widget.databaseController, - groupData: widget.groupData, - isEditing: isEditing, - onSubmitted: (columnName) { - context - .read() - .add(BoardEvent.renameGroup(widget.groupData.id, columnName)); - }, - ), - FieldType.Checkbox => CheckboxColumnHeader( - databaseController: widget.databaseController, - groupData: widget.groupData, - ), - _ => _DefaultColumnHeaderContent( - databaseController: widget.databaseController, - groupData: widget.groupData, - ), - }; + final boardCustomData = widget.groupData.customData as GroupData; - return Container( - padding: widget.margin, - height: 50, - child: child, + return BlocBuilder( + builder: (context, state) { + return state.maybeMap( + orElse: () => const SizedBox.shrink(), + ready: (state) { + if (state.editingHeaderId != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + Widget title = Expanded( + child: FlowyText.medium( + widget.groupData.headerData.groupName, + overflow: TextOverflow.ellipsis, + ), + ); + + if (!boardCustomData.group.isDefault && + boardCustomData.fieldType.canEditHeader) { + title = Flexible( + fit: FlexFit.tight, + child: FlowyTooltip( + message: LocaleKeys.board_column_renameGroupTooltip.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => context.read().add( + BoardEvent.startEditingHeader(widget.groupData.id), + ), + child: FlowyText.medium( + widget.groupData.headerData.groupName, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ), + ); + } + + if (state.editingHeaderId == widget.groupData.id) { + title = _buildTextField(context); + } + + return Padding( + padding: widget.margin, + child: SizedBox( + height: 50, + child: Row( + children: [ + _buildHeaderIcon(boardCustomData), + title, + const HSpace(6), + _groupOptionsButton(context), + const HSpace(4), + FlowyTooltip( + message: + LocaleKeys.board_column_addToColumnTopTooltip.tr(), + preferBelow: false, + child: FlowyIconButton( + width: 20, + icon: const FlowySvg(FlowySvgs.add_s), + iconColorOnHover: + Theme.of(context).colorScheme.onSurface, + onPressed: () => context.read().add( + BoardEvent.createRow( + widget.groupData.id, + OrderObjectPositionTypePB.Start, + null, + null, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ); + }, ); } -} -class GroupOptionsButton extends StatelessWidget { - const GroupOptionsButton({ - super.key, - required this.groupData, - this.isEditing, - }); + Widget _buildTextField(BuildContext context) { + return Expanded( + child: KeyboardListener( + focusNode: _keyboardListenerFocusNode, + onKeyEvent: (event) { + if ([LogicalKeyboardKey.enter, LogicalKeyboardKey.escape] + .contains(event.logicalKey)) { + _saveEdit(); + } + }, + child: TextField( + controller: _controller, + focusNode: _focusNode, + onEditingComplete: _saveEdit, + style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 14), + decoration: InputDecoration( + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + hoverColor: Colors.transparent, + contentPadding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + ), + ), + isDense: true, + ), + ), + ), + ); + } - final AppFlowyGroupData groupData; - final ValueNotifier? isEditing; + void _saveEdit() => context + .read() + .add(BoardEvent.endEditingHeader(widget.groupData.id, _controller.text)); - @override - Widget build(BuildContext context) { + Widget _buildHeaderIcon(GroupData customData) => + switch (customData.fieldType) { + FieldType.Checkbox => FlowySvg( + customData.asCheckboxGroup()!.isCheck + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + ), + _ => const SizedBox.shrink(), + }; + + Widget _groupOptionsButton(BuildContext context) { return AppFlowyPopover( clickHandler: PopoverClickHandler.gestureDetector, margin: const EdgeInsets.all(8), @@ -99,14 +218,14 @@ class GroupOptionsButton extends StatelessWidget { iconColorOnHover: Theme.of(context).colorScheme.onSurface, ), popupBuilder: (popoverContext) { - final customGroupData = groupData.customData as GroupData; + final customGroupData = widget.groupData.customData as GroupData; final isDefault = customGroupData.group.isDefault; - final menuItems = GroupOption.values.toList(); + final menuItems = GroupOptions.values.toList(); if (!customGroupData.fieldType.canEditHeader || isDefault) { - menuItems.remove(GroupOption.rename); + menuItems.remove(GroupOptions.rename); } if (!customGroupData.fieldType.canDeleteGroup || isDefault) { - menuItems.remove(GroupOption.delete); + menuItems.remove(GroupOptions.delete); } return SeparatedColumn( mainAxisSize: MainAxisSize.min, @@ -117,13 +236,12 @@ class GroupOptionsButton extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( leftIcon: FlowySvg(action.icon), - text: FlowyText( + text: FlowyText.medium( action.text, - lineHeight: 1.0, overflow: TextOverflow.ellipsis, ), onTap: () { - run(context, action, customGroupData.group); + action.call(context, customGroupData.group); PopoverContainer.of(popoverContext).close(); }, ), @@ -134,107 +252,37 @@ class GroupOptionsButton extends StatelessWidget { }, ); } +} - void run(BuildContext context, GroupOption option, GroupPB group) { - switch (option) { - case GroupOption.rename: - isEditing?.value = true; +enum GroupOptions { + rename, + hide, + delete; + + void call(BuildContext context, GroupPB group) { + switch (this) { + case rename: + context + .read() + .add(BoardEvent.startEditingHeader(group.groupId)); break; - case GroupOption.hide: + case hide: context .read() .add(BoardEvent.setGroupVisibility(group, false)); break; - case GroupOption.delete: - showConfirmDeletionDialog( - context: context, - name: LocaleKeys.board_column_label.tr(), - description: LocaleKeys.board_column_deleteColumnConfirmation.tr(), - onConfirm: () { + case delete: + NavigatorAlertDialog( + title: LocaleKeys.board_column_deleteColumnConfirmation.tr(), + confirm: () { context .read() .add(BoardEvent.deleteGroup(group.groupId)); }, - ); + ).show(context); break; } } -} - -class CreateCardFromTopButton extends StatelessWidget { - const CreateCardFromTopButton({ - super.key, - required this.groupId, - }); - - final String groupId; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.board_column_addToColumnTopTooltip.tr(), - preferBelow: false, - child: FlowyIconButton( - width: 20, - icon: const FlowySvg(FlowySvgs.add_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - onPressed: () => context.read().add( - BoardEvent.createRow( - groupId, - OrderObjectPositionTypePB.Start, - null, - null, - ), - ), - ), - ); - } -} - -class _DefaultColumnHeaderContent extends StatelessWidget { - const _DefaultColumnHeaderContent({ - required this.databaseController, - required this.groupData, - }); - - final DatabaseController databaseController; - final AppFlowyGroupData groupData; - - @override - Widget build(BuildContext context) { - final customData = groupData.customData as GroupData; - final groupName = customData.group.generateGroupName(databaseController); - return Row( - children: [ - Expanded( - child: Align( - alignment: AlignmentDirectional.centerStart, - child: FlowyTooltip( - message: groupName, - child: FlowyText.medium( - groupName, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - const HSpace(6), - GroupOptionsButton( - groupData: groupData, - ), - const HSpace(4), - CreateCardFromTopButton( - groupId: groupData.id, - ), - ], - ); - } -} - -enum GroupOption { - rename, - hide, - delete; FlowySvgData get icon => switch (this) { rename => FlowySvgs.edit_s, diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_editable_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_editable_column_header.dart deleted file mode 100644 index e6ecca43bc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_editable_column_header.dart +++ /dev/null @@ -1,262 +0,0 @@ -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.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_editor/extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_board/appflowy_board.dart'; -import 'package:collection/collection.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'board_column_header.dart'; - -/// This column header is used for the MultiSelect and SingleSelect field types. -class EditableColumnHeader extends StatefulWidget { - const EditableColumnHeader({ - super.key, - required this.databaseController, - required this.groupData, - required this.isEditing, - required this.onSubmitted, - }); - - final DatabaseController databaseController; - final AppFlowyGroupData groupData; - final ValueNotifier isEditing; - final void Function(String columnName) onSubmitted; - - @override - State createState() => _EditableColumnHeaderState(); -} - -class _EditableColumnHeaderState extends State { - late final FocusNode focusNode; - late final TextEditingController textController = TextEditingController( - text: _generateGroupName(), - ); - - GroupData get customData => widget.groupData.customData; - - @override - void initState() { - super.initState(); - focusNode = FocusNode( - onKeyEvent: (node, event) { - if (event.logicalKey == LogicalKeyboardKey.escape && - event is KeyUpEvent) { - focusNode.unfocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - )..addListener(onFocusChanged); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (oldWidget.groupData.customData != widget.groupData.customData) { - textController.text = _generateGroupName(); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - focusNode - ..removeListener(onFocusChanged) - ..dispose(); - textController.dispose(); - super.dispose(); - } - - void onFocusChanged() { - if (!focusNode.hasFocus) { - widget.isEditing.value = false; - widget.onSubmitted(textController.text); - } else { - textController.selection = TextSelection( - baseOffset: 0, - extentOffset: textController.text.length, - ); - } - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: ValueListenableBuilder( - valueListenable: widget.isEditing, - builder: (context, isEditing, _) { - if (isEditing) { - focusNode.requestFocus(); - } - return isEditing ? _buildTextField() : _buildTitle(); - }, - ), - ), - const HSpace(6), - GroupOptionsButton( - groupData: widget.groupData, - isEditing: widget.isEditing, - ), - const HSpace(4), - CreateCardFromTopButton( - groupId: widget.groupData.id, - ), - ], - ); - } - - Widget _buildTitle() { - final (backgroundColor, dotColor) = _generateGroupColor(); - final groupName = _generateGroupName(); - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - widget.isEditing.value = true; - }, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: FlowyTooltip( - message: groupName, - child: Container( - height: 20, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular(6), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Container( - height: 6, - width: 6, - decoration: BoxDecoration( - color: dotColor, - borderRadius: BorderRadius.circular(3), - ), - ), - ), - const HSpace(4.0), - Flexible( - child: FlowyText.medium( - groupName, - overflow: TextOverflow.ellipsis, - lineHeight: 1.0, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - Widget _buildTextField() { - return TextField( - controller: textController, - focusNode: focusNode, - onEditingComplete: () { - widget.isEditing.value = false; - }, - onSubmitted: widget.onSubmitted, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - filled: true, - fillColor: Theme.of(context).colorScheme.surface, - hoverColor: Colors.transparent, - contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - isDense: true, - ), - ); - } - - String _generateGroupName() { - return customData.group.generateGroupName(widget.databaseController); - } - - (Color? backgroundColor, Color? dotColor) _generateGroupColor() { - Color? backgroundColor; - Color? dotColor; - - final groupId = widget.groupData.id; - final fieldId = customData.fieldInfo.id; - final field = widget.databaseController.fieldController.getField(fieldId); - if (field != null) { - final selectOptions = switch (field.fieldType) { - FieldType.MultiSelect => MultiSelectTypeOptionDataParser() - .fromBuffer(field.field.typeOptionData) - .options, - FieldType.SingleSelect => SingleSelectTypeOptionDataParser() - .fromBuffer(field.field.typeOptionData) - .options, - _ => [], - }; - - final colorPB = - selectOptions.firstWhereOrNull((e) => e.id == groupId)?.color; - - if (colorPB != null) { - backgroundColor = colorPB.toColor(context); - dotColor = getColorOfDot(colorPB); - } - } - - return (backgroundColor, dotColor); - } - - // move to theme file and allow theme customization once palette is finalized - Color getColorOfDot(SelectOptionColorPB color) { - return switch (Theme.of(context).brightness) { - Brightness.light => switch (color) { - SelectOptionColorPB.Purple => const Color(0xFFAB8DFF), - SelectOptionColorPB.Pink => const Color(0xFFFF8EF5), - SelectOptionColorPB.LightPink => const Color(0xFFFF85A9), - SelectOptionColorPB.Orange => const Color(0xFFFFBC7E), - SelectOptionColorPB.Yellow => const Color(0xFFFCD86F), - SelectOptionColorPB.Lime => const Color(0xFFC6EC41), - SelectOptionColorPB.Green => const Color(0xFF74F37D), - SelectOptionColorPB.Aqua => const Color(0xFF40F0D1), - SelectOptionColorPB.Blue => const Color(0xFF00C8FF), - _ => throw ArgumentError, - }, - Brightness.dark => switch (color) { - SelectOptionColorPB.Purple => const Color(0xFF502FD6), - SelectOptionColorPB.Pink => const Color(0xFFBF1CC0), - SelectOptionColorPB.LightPink => const Color(0xFFC42A53), - SelectOptionColorPB.Orange => const Color(0xFFD77922), - SelectOptionColorPB.Yellow => const Color(0xFFC59A1A), - SelectOptionColorPB.Lime => const Color(0xFFA4C824), - SelectOptionColorPB.Green => const Color(0xFF23CA2E), - SelectOptionColorPB.Aqua => const Color(0xFF19CCAC), - SelectOptionColorPB.Blue => const Color(0xFF04A9D7), - _ => throw ArgumentError, - } - }; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart index 1a0d1a3163..acc6db2f0f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart @@ -7,18 +7,17 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/row/row_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/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.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/plugins/database/widgets/row/row_detail.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -26,11 +25,9 @@ class HiddenGroupsColumn extends StatelessWidget { const HiddenGroupsColumn({ super.key, required this.margin, - required this.shrinkWrap, }); final EdgeInsets margin; - final bool shrinkWrap; @override Widget build(BuildContext context) { @@ -45,8 +42,6 @@ class HiddenGroupsColumn extends StatelessWidget { return const SizedBox.shrink(); } final isCollapsed = layoutSettings.collapseHiddenGroups; - final leftPadding = margin.left + - context.read().horizontalPadding; return AnimatedSize( alignment: AlignmentDirectional.topStart, curve: Curves.easeOut, @@ -61,32 +56,39 @@ class HiddenGroupsColumn extends StatelessWidget { ), ), ) - : Container( + : SizedBox( width: 274, - padding: EdgeInsets.only( - left: leftPadding, - right: margin.right + 4, - ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( height: 50, - child: Row( - children: [ - Expanded( - child: FlowyText.medium( - LocaleKeys.board_hiddenGroupSection_sectionTitle - .tr(), - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, + child: Padding( + padding: EdgeInsets.only( + left: 80 + margin.left, + right: margin.right + 4, + ), + child: Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys + .board_hiddenGroupSection_sectionTitle + .tr(), + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, + ), ), - ), - _collapseExpandIcon(context, isCollapsed), - ], + _collapseExpandIcon(context, isCollapsed), + ], + ), + ), + ), + Expanded( + child: HiddenGroupList( + databaseController: databaseController, ), ), - _hiddenGroupList(databaseController), ], ), ), @@ -95,14 +97,6 @@ class HiddenGroupsColumn extends StatelessWidget { ); } - Widget _hiddenGroupList(DatabaseController databaseController) { - final hiddenGroupList = HiddenGroupList( - shrinkWrap: shrinkWrap, - databaseController: databaseController, - ); - return shrinkWrap ? hiddenGroupList : Expanded(child: hiddenGroupList); - } - Widget _collapseExpandIcon(BuildContext context, bool isCollapsed) { return FlowyTooltip( message: isCollapsed @@ -130,11 +124,9 @@ class HiddenGroupList extends StatelessWidget { const HiddenGroupList({ super.key, required this.databaseController, - required this.shrinkWrap, }); final DatabaseController databaseController; - final bool shrinkWrap; @override Widget build(BuildContext context) { @@ -157,7 +149,6 @@ class HiddenGroupList extends StatelessWidget { ], ), ), - shrinkWrap: shrinkWrap, buildDefaultDragHandles: false, itemCount: state.hiddenGroups.length, itemBuilder: (_, index) => Padding( @@ -210,27 +201,31 @@ class _HiddenGroupCardState extends State { final databaseController = widget.bloc.databaseController; final primaryField = databaseController.fieldController.fieldInfos .firstWhereOrNull((element) => element.isPrimary)!; - return AppFlowyPopover( - controller: _popoverController, - direction: PopoverDirection.bottomWithCenterAligned, - triggerActions: PopoverTriggerFlags.none, - constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300), - popupBuilder: (popoverContext) { - return BlocProvider.value( - value: context.read(), - child: HiddenGroupPopupItemList( - viewId: databaseController.viewId, - groupId: widget.group.groupId, - primaryFieldId: primaryField.id, - rowCache: databaseController.rowCache, - ), - ); - }, - child: HiddenGroupButtonContent( - popoverController: _popoverController, - groupId: widget.group.groupId, - index: widget.index, - bloc: widget.bloc, + + return Padding( + padding: const EdgeInsets.only(left: 26), + child: AppFlowyPopover( + controller: _popoverController, + direction: PopoverDirection.bottomWithCenterAligned, + triggerActions: PopoverTriggerFlags.none, + constraints: const BoxConstraints(maxWidth: 234, maxHeight: 300), + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: HiddenGroupPopupItemList( + viewId: databaseController.viewId, + groupId: widget.group.groupId, + primaryFieldId: primaryField.id, + rowCache: databaseController.rowCache, + ), + ); + }, + child: HiddenGroupButtonContent( + popoverController: _popoverController, + groupId: widget.group.groupId, + index: widget.index, + bloc: widget.bloc, + ), ), ); } @@ -287,33 +282,24 @@ class HiddenGroupButtonContent extends StatelessWidget { index: index, ), const HSpace(4), + FlowyText.medium( + bloc.generateGroupNameFromGroup(group), + overflow: TextOverflow.ellipsis, + ), + const HSpace(6), Expanded( - child: Row( - children: [ - Flexible( - child: FlowyText( - group.generateGroupName( - bloc.databaseController, - ), - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6), - FlowyText( - group.rows.length.toString(), - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - ], + child: FlowyText.medium( + group.rows.length.toString(), + overflow: TextOverflow.ellipsis, + color: Theme.of(context).hintColor, ), ), if (isHovering) ...[ - const HSpace(6), FlowyIconButton( width: 20, - icon: const FlowySvg( + icon: FlowySvg( FlowySvgs.show_m, - size: Size.square(16), + color: Theme.of(context).hintColor, ), onPressed: () => context.read().add( @@ -374,11 +360,11 @@ class HiddenGroupCardActions extends StatelessWidget { class HiddenGroupPopupItemList extends StatelessWidget { const HiddenGroupPopupItemList({ - super.key, required this.groupId, required this.viewId, required this.primaryFieldId, required this.rowCache, + super.key, }); final String groupId; @@ -399,12 +385,11 @@ class HiddenGroupPopupItemList extends StatelessWidget { if (group == null) { return const SizedBox.shrink(); } - final bloc = context.read(); final cells = [ Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: FlowyText( - group.generateGroupName(bloc.databaseController), + child: FlowyText.medium( + context.read().generateGroupNameFromGroup(group), fontSize: 10, color: Theme.of(context).hintColor, overflow: TextOverflow.ellipsis, @@ -417,7 +402,6 @@ class HiddenGroupPopupItemList extends StatelessWidget { viewId: viewId, rowCache: rowCache, ); - rowController.initialize(); final databaseController = context.read().databaseController; @@ -435,14 +419,12 @@ class HiddenGroupPopupItemList extends StatelessWidget { onPressed: () { FlowyOverlay.show( context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( + builder: (_) { + return RowDetailPage( databaseController: databaseController, rowController: rowController, - userProfile: context.read().userProfile, - ), - ), + ); + }, ); PopoverContainer.of(context).close(); }, @@ -490,7 +472,7 @@ class HiddenGroupPopupItem extends StatelessWidget { text: cellBuilder.build( cellContext: cellContext, styleMap: {FieldType.RichText: _titleCellStyle(context)}, - hasNotes: false, + hasNotes: !rowMeta.isDocumentEmpty, ), onTap: onPressed, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart index 45157b1a47..8c9e36c2c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_bloc.dart @@ -7,7 +7,6 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:fixnum/fixnum.dart'; @@ -34,34 +33,11 @@ class CalendarBloc extends Bloc { CellMemCache get cellCache => databaseController.rowCache.cellCache; RowCache get rowCache => databaseController.rowCache; - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - - DatabaseCallbacks? _databaseCallbacks; - DatabaseLayoutSettingCallbacks? _layoutSettingCallbacks; - - @override - Future close() async { - databaseController.removeListener( - onDatabaseChanged: _databaseCallbacks, - onLayoutSettingsChanged: _layoutSettingCallbacks, - ); - _databaseCallbacks = null; - _layoutSettingCallbacks = null; - await super.close(); - } - void _dispatch() { on( (event, emit) async { await event.when( initial: () async { - final result = await UserEventGetUserProfile().send(); - result.fold( - (profile) => _userProfile = profile, - (err) => Log.error('Failed to get user profile: $err'), - ); - _startListening(); await _openDatabase(emit); _loadAllEvents(); @@ -136,7 +112,6 @@ class CalendarBloc extends Bloc { deleteEventIds: deletedRowIds, ), ); - emit(state.copyWith(deleteEventIds: const [])); }, didReceiveEvent: (CalendarEventData event) { emit( @@ -145,11 +120,6 @@ class CalendarBloc extends Bloc { newEvent: event, ), ); - emit(state.copyWith(newEvent: null)); - }, - openRowDetail: (row) { - emit(state.copyWith(openRow: row)); - emit(state.copyWith(openRow: null)); }, ); }, @@ -250,14 +220,16 @@ class CalendarBloc extends Bloc { } Future?> _loadEvent(RowId rowId) async { - final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId); - return DatabaseEventGetCalendarEvent(payload).send().fold( - (eventPB) => _calendarEventDataFromEventPB(eventPB), - (r) { - Log.error(r); - return null; - }, - ); + final payload = RowIdPB(viewId: viewId, rowId: rowId); + return DatabaseEventGetCalendarEvent(payload).send().then((result) { + return result.fold( + (eventPB) => _calendarEventDataFromEventPB(eventPB), + (r) { + Log.error(r); + return null; + }, + ); + }); } void _loadAllEvents() async { @@ -314,7 +286,7 @@ class CalendarBloc extends Bloc { } void _startListening() { - _databaseCallbacks = DatabaseCallbacks( + final onDatabaseChanged = DatabaseCallbacks( onDatabaseChanged: (database) { if (isClosed) return; }, @@ -326,18 +298,14 @@ class CalendarBloc extends Bloc { for (final fieldInfo in fieldInfos) fieldInfo.field.id: fieldInfo, }; }, - onRowsCreated: (rows) async { + onRowsCreated: (rowIds) async { if (isClosed) { return; } - for (final row in rows) { - if (row.isHiddenInView) { - add(CalendarEvent.openRowDetail(row.rowMeta)); - } else { - final event = await _loadEvent(row.rowMeta.id); - if (event != null) { - add(CalendarEvent.didReceiveEvent(event)); - } + for (final id in rowIds) { + final event = await _loadEvent(id); + if (event != null && !isClosed) { + add(CalendarEvent.didReceiveEvent(event)); } } }, @@ -363,39 +331,15 @@ class CalendarBloc extends Bloc { } } }, - onNumOfRowsChanged: (rows, rowById, reason) { - reason.maybeWhen( - updateRowsVisibility: (changeset) async { - if (isClosed) { - return; - } - for (final id in changeset.invisibleRows) { - if (_containsEvent(id)) { - add(CalendarEvent.didDeleteEvents([id])); - } - } - for (final row in changeset.visibleRows) { - final id = row.rowMeta.id; - if (!_containsEvent(id)) { - final event = await _loadEvent(id); - if (event != null) { - add(CalendarEvent.didReceiveEvent(event)); - } - } - } - }, - orElse: () {}, - ); - }, ); - _layoutSettingCallbacks = DatabaseLayoutSettingCallbacks( + final onLayoutSettingsChanged = DatabaseLayoutSettingCallbacks( onLayoutSettingsChanged: _didReceiveLayoutSetting, ); databaseController.addListener( - onDatabaseChanged: _databaseCallbacks, - onLayoutSettingsChanged: _layoutSettingCallbacks, + onDatabaseChanged: onDatabaseChanged, + onLayoutSettingsChanged: onLayoutSettingsChanged, ); } @@ -418,10 +362,6 @@ class CalendarBloc extends Bloc { return state.allEvents[index].date.day != event.date.day; } - bool _containsEvent(String rowId) { - return state.allEvents.any((element) => element.event!.eventId == rowId); - } - Int64 _eventTimestamp(CalendarDayEvent event, DateTime date) { final time = event.date.hour * 3600 + event.date.minute * 60 + event.date.second; @@ -487,8 +427,6 @@ class CalendarEvent with _$CalendarEvent { const factory CalendarEvent.deleteEvent(String viewId, String rowId) = _DeleteEvent; - - const factory CalendarEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail; } @freezed @@ -503,7 +441,6 @@ class CalendarState with _$CalendarState { CalendarEventData? updateEvent, required List deleteEventIds, required CalendarLayoutSettingPB? settings, - required RowMetaPB? openRow, required LoadingState loadingState, required FlowyError? noneOrError, }) = _CalendarState; @@ -514,7 +451,6 @@ class CalendarState with _$CalendarState { initialEvents: [], deleteEventIds: [], settings: null, - openRow: null, noneOrError: null, loadingState: LoadingState.loading(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart index 48e159475c..303daff87e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_event_editor_bloc.dart @@ -29,14 +29,12 @@ class CalendarEventEditorBloc (event, emit) async { await event.when( initial: () { - rowController.initialize(); - _startListening(); final primaryFieldId = fieldController.fieldInfos .firstWhere((fieldInfo) => fieldInfo.isPrimary) .id; final cells = rowController - .loadCells() + .loadData() .where( (cellContext) => _filterCellContext(cellContext, primaryFieldId), @@ -90,7 +88,7 @@ class CalendarEventEditorBloc @override Future close() async { - await rowController.dispose(); + rowController.dispose(); return super.close(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart index 1e67ad9e9d..208906f00b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/unschedule_event_bloc.dart @@ -29,15 +29,6 @@ class UnscheduleEventsBloc CellMemCache get cellCache => databaseController.rowCache.cellCache; RowCache get rowCache => databaseController.rowCache; - DatabaseCallbacks? _databaseCallbacks; - - @override - Future close() async { - databaseController.removeListener(onDatabaseChanged: _databaseCallbacks); - _databaseCallbacks = null; - await super.close(); - } - void _dispatch() { on( (event, emit) async { @@ -51,7 +42,7 @@ class UnscheduleEventsBloc state.copyWith( allEvents: events, unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), + events.where((element) => !element.isScheduled).toList(), ), ); }, @@ -64,7 +55,7 @@ class UnscheduleEventsBloc state.copyWith( allEvents: events, unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), + events.where((element) => !element.isScheduled).toList(), ), ); }, @@ -74,7 +65,7 @@ class UnscheduleEventsBloc state.copyWith( allEvents: events, unscheduleEvents: - events.where((element) => !element.hasTimestamp()).toList(), + events.where((element) => !element.isScheduled).toList(), ), ); }, @@ -86,7 +77,7 @@ class UnscheduleEventsBloc Future _loadEvent( RowId rowId, ) async { - final payload = DatabaseViewRowIdPB(viewId: viewId, rowId: rowId); + final payload = RowIdPB(viewId: viewId, rowId: rowId); return DatabaseEventGetCalendarEvent(payload).send().then( (result) => result.fold( (eventPB) => eventPB, @@ -112,13 +103,13 @@ class UnscheduleEventsBloc } void _startListening() { - _databaseCallbacks = DatabaseCallbacks( - onRowsCreated: (rows) async { + final onDatabaseChanged = DatabaseCallbacks( + onRowsCreated: (rowIds) async { if (isClosed) { return; } - for (final row in rows) { - final event = await _loadEvent(row.rowMeta.id); + for (final id in rowIds) { + final event = await _loadEvent(id); if (event != null && !isClosed) { add(UnscheduleEventsEvent.didReceiveEvent(event)); } @@ -144,7 +135,7 @@ class UnscheduleEventsBloc }, ); - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart index 1d2838210d..6dfa3313f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_screen.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; @@ -11,7 +12,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../grid/presentation/layout/sizes.dart'; import '../application/calendar_bloc.dart'; @@ -65,14 +65,14 @@ class CalendarDayCard extends StatelessWidget { const VSpace(6.0), // List of cards or empty space - if (events.isNotEmpty && !UniversalPlatform.isMobile) ...[ + if (events.isNotEmpty && !PlatformExtension.isMobile) ...[ _EventList( events: events, viewId: viewId, rowCache: rowCache, constraints: constraints, ), - ] else if (events.isNotEmpty && UniversalPlatform.isMobile) ...[ + ] else if (events.isNotEmpty && PlatformExtension.isMobile) ...[ const _EventIndicator(), ], ], @@ -82,7 +82,7 @@ class CalendarDayCard extends StatelessWidget { children: [ GestureDetector( onDoubleTap: () => onCreateEvent(date), - onTap: UniversalPlatform.isMobile + onTap: PlatformExtension.isMobile ? () => _mobileOnTap(context) : null, child: Container( @@ -106,7 +106,7 @@ class CalendarDayCard extends StatelessWidget { padding: const EdgeInsets.only(top: 5.0), child: child, ), - if (candidate.isEmpty && !UniversalPlatform.isMobile) + if (candidate.isEmpty && !PlatformExtension.isMobile) NewEventButton( onCreate: () => onCreateEvent(date), ), @@ -242,7 +242,6 @@ class NewEventButton extends StatelessWidget { hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 22, tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(), - radius: Corners.s6Border, decoration: BoxDecoration( border: Border.fromBorderSide( BorderSide( @@ -252,20 +251,20 @@ class NewEventButton extends StatelessWidget { width: 0.5, ), ), - borderRadius: Corners.s6Border, + borderRadius: Corners.s5Border, boxShadow: [ BoxShadow( spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 2, ), BoxShadow( - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 4, ), BoxShadow( spreadRadius: 2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 8, ), ], @@ -304,16 +303,16 @@ class _DayBadge extends StatelessWidget { dayTextColor = Theme.of(context).colorScheme.onPrimary; } - final double size = UniversalPlatform.isMobile ? 20 : 18; + final double size = PlatformExtension.isMobile ? 20 : 18; return SizedBox( height: size, child: Row( - mainAxisAlignment: UniversalPlatform.isMobile + mainAxisAlignment: PlatformExtension.isMobile ? MainAxisAlignment.center : MainAxisAlignment.end, children: [ - if (date.day == 1 && !UniversalPlatform.isMobile) + if (date.day == 1 && !PlatformExtension.isMobile) FlowyText.medium( monthString, fontSize: 11, @@ -327,9 +326,9 @@ class _DayBadge extends StatelessWidget { width: isToday ? size : null, height: isToday ? size : null, child: Center( - child: FlowyText( + child: FlowyText.medium( dayString, - fontSize: UniversalPlatform.isMobile ? 12 : 11, + fontSize: PlatformExtension.isMobile ? 12 : 11, color: dayTextColor, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 5ef2e2c327..e105914908 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -1,13 +1,12 @@ import 'package:appflowy/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/application/row/row_controller.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/calendar_card_cell_style.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -15,7 +14,6 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../application/calendar_bloc.dart'; import 'calendar_event_editor.dart'; @@ -82,9 +80,8 @@ class _EventCardState extends State { rowCache: rowCache, isEditing: false, cellBuilder: cellBuilder, - isCompact: true, onTap: (context) { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { context.push( MobileRowDetailPage.routeName, extra: { @@ -110,7 +107,6 @@ class _EventCardState extends State { ), onStartEditing: () {}, onEndEditing: () {}, - userProfile: context.read().userProfile, ); final decoration = BoxDecoration( @@ -127,16 +123,16 @@ class _EventCardState extends State { boxShadow: [ BoxShadow( spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 2, ), BoxShadow( - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 4, ), BoxShadow( spreadRadius: 2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), blurRadius: 8, ), ], @@ -168,36 +164,15 @@ class _EventCardState extends State { databaseController: widget.databaseController, rowMeta: widget.event.event.rowMeta, layoutSettings: settings, - onExpand: () { - final rowController = RowController( - rowMeta: widget.event.event.rowMeta, - viewId: widget.databaseController.viewId, - rowCache: widget.databaseController.rowCache, - ); - - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: widget.databaseController, - rowController: rowController, - userProfile: context.read().userProfile, - ), - ), - ); - }, ), ); }, - child: Padding( - padding: widget.padding, - child: Material( - color: Colors.transparent, - child: DecoratedBox( - decoration: decoration, - child: card, - ), + child: Material( + color: Colors.transparent, + child: Container( + padding: widget.padding, + decoration: decoration, + child: card, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart index dcbe626dd8..10f078c805 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_editor.dart @@ -1,3 +1,6 @@ +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; @@ -7,17 +10,18 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_event_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalendarEventEditor extends StatelessWidget { @@ -26,7 +30,6 @@ class CalendarEventEditor extends StatelessWidget { required RowMetaPB rowMeta, required this.layoutSettings, required this.databaseController, - required this.onExpand, }) : rowController = RowController( rowMeta: rowMeta, viewId: databaseController.viewId, @@ -39,7 +42,6 @@ class CalendarEventEditor extends StatelessWidget { final DatabaseController databaseController; final RowController rowController; final EditableCellBuilder cellBuilder; - final VoidCallback onExpand; @override Widget build(BuildContext context) { @@ -55,7 +57,6 @@ class CalendarEventEditor extends StatelessWidget { EventEditorControls( rowController: rowController, databaseController: databaseController, - onExpand: onExpand, ), Flexible( child: EventPropertyList( @@ -75,12 +76,10 @@ class EventEditorControls extends StatelessWidget { super.key, required this.rowController, required this.databaseController, - required this.onExpand, }); final RowController rowController; final DatabaseController databaseController; - final VoidCallback onExpand; @override Widget build(BuildContext context) { @@ -93,58 +92,48 @@ class EventEditorControls extends StatelessWidget { message: LocaleKeys.calendar_duplicateEvent.tr(), child: FlowyIconButton( width: 20, - icon: FlowySvg( + icon: const FlowySvg( FlowySvgs.m_duplicate_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, + size: Size.square(17), ), - onPressed: () { - context.read().add( - CalendarEvent.duplicateEvent( - rowController.viewId, - rowController.rowId, - ), - ); - PopoverContainer.of(context).close(); - }, + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + onPressed: () => context.read().add( + CalendarEvent.duplicateEvent( + rowController.viewId, + rowController.rowId, + ), + ), ), ), const HSpace(8.0), FlowyIconButton( width: 20, - icon: FlowySvg( - FlowySvgs.delete_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - showConfirmDeletionDialog( - context: context, - name: LocaleKeys.grid_row_label.tr(), - description: LocaleKeys.grid_row_deleteRowPrompt.tr(), - onConfirm: () { - context.read().add( - CalendarEvent.deleteEvent( - rowController.viewId, - rowController.rowId, - ), - ); - PopoverContainer.of(context).close(); - }, - ); - }, + icon: const FlowySvg(FlowySvgs.delete_s), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + onPressed: () => context.read().add( + CalendarEvent.deleteEvent( + rowController.viewId, + rowController.rowId, + ), + ), ), const HSpace(8.0), FlowyIconButton( width: 20, - icon: FlowySvg( - FlowySvgs.full_view_s, - size: const Size.square(16), - color: Theme.of(context).iconTheme.color, - ), + icon: const FlowySvg(FlowySvgs.full_view_s), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, onPressed: () { PopoverContainer.of(context).close(); - onExpand.call(); + FlowyOverlay.show( + context: context, + builder: (_) => BlocProvider.value( + value: context.read(), + child: RowDetailPage( + databaseController: databaseController, + rowController: rowController, + ), + ), + ); }, ), ], @@ -259,9 +248,10 @@ class _PropertyCellState extends State { padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 6), child: Row( children: [ - FieldIcon( - fieldInfo: fieldInfo, - dimension: 14, + FlowySvg( + fieldInfo.fieldType.svgData, + color: Theme.of(context).hintColor, + size: const Size.square(14), ), const HSpace(4.0), Expanded( @@ -289,7 +279,6 @@ class _TitleTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -299,8 +288,10 @@ class _TitleTextCellSkin extends IEditableTextCellSkin { textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 14), focusNode: focusNode, hintText: LocaleKeys.calendar_defaultNewCalendarTitle.tr(), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.text)); + onChanged: (text) { + if (textEditingController.value.composing.isCollapsed) { + bloc.add(TextCellEvent.updateText(text)); + } }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 1876332d01..ff695eea0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -6,23 +6,22 @@ import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/unschedule_event_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calendar_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../application/row/row_controller.dart'; import '../../widgets/row/row_detail.dart'; @@ -31,8 +30,6 @@ import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { - final _toggleExtension = ToggleExtensionNotifier(); - @override Widget content( BuildContext context, @@ -54,7 +51,6 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { return CalendarSettingBar( key: _makeValueKey(controller), databaseController: controller, - toggleExtension: _toggleExtension, ); } @@ -63,18 +59,7 @@ class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { BuildContext context, DatabaseController controller, ) { - return DatabaseViewSettingExtension( - key: _makeValueKey(controller), - viewId: controller.viewId, - databaseController: controller, - toggleExtension: _toggleExtension, - ); - } - - @override - void dispose() { - _toggleExtension.dispose(); - super.dispose(); + return SizedBox.fromSize(); } ValueKey _makeValueKey(DatabaseController controller) { @@ -105,11 +90,12 @@ class _CalendarPageState extends State { @override void initState() { - super.initState(); _calendarState = GlobalKey(); _calendarBloc = CalendarBloc( databaseController: widget.databaseController, )..add(const CalendarEvent.initial()); + + super.initState(); } @override @@ -122,18 +108,8 @@ class _CalendarPageState extends State { Widget build(BuildContext context) { return CalendarControllerProvider( controller: _eventController, - child: MultiBlocProvider( - providers: [ - BlocProvider.value( - value: _calendarBloc, - ), - BlocProvider( - create: (context) => ViewLockStatusBloc(view: widget.view) - ..add( - ViewLockStatusEvent.initial(), - ), - ), - ], + child: BlocProvider.value( + value: _calendarBloc, child: MultiBlocListener( listeners: [ BlocListener( @@ -176,18 +152,6 @@ class _CalendarPageState extends State { } }, ), - BlocListener( - listenWhen: (p, c) => p.openRow != c.openRow, - listener: (context, state) { - if (state.openRow != null) { - showEventDetails( - context: context, - databaseController: _calendarBloc.databaseController, - rowMeta: state.openRow!, - ); - } - }, - ), ], child: BlocBuilder( builder: (context, state) { @@ -221,17 +185,11 @@ class _CalendarPageState extends State { return LayoutBuilder( // must specify MonthView width for useAvailableVerticalSpace to work properly builder: (context, constraints) { - EdgeInsets padding = UniversalPlatform.isMobile - ? CalendarSize.contentInsetsMobile - : CalendarSize.contentInsets + - const EdgeInsets.symmetric(horizontal: 40); - final double horizontalPadding = - context.read().horizontalPadding; - if (horizontalPadding == 0) { - padding = padding.copyWith(left: 0, right: 0); - } return Padding( - padding: padding, + padding: PlatformExtension.isMobile + ? CalendarSize.contentInsetsMobile + : CalendarSize.contentInsets + + const EdgeInsets.symmetric(horizontal: 40), child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), @@ -239,26 +197,12 @@ class _CalendarPageState extends State { key: _calendarState, controller: _eventController, width: constraints.maxWidth, - cellAspectRatio: UniversalPlatform.isMobile ? 0.9 : 0.6, + cellAspectRatio: PlatformExtension.isMobile ? 0.9 : 0.6, startDay: _weekdayFromInt(firstDayOfWeek), showBorder: false, headerBuilder: _headerNavigatorBuilder, weekDayBuilder: _headerWeekDayBuilder, - cellBuilder: ( - date, - calenderEvents, - isToday, - isInMonth, - position, - ) => - _calendarDayBuilder( - context, - date, - calenderEvents, - isToday, - isInMonth, - position, - ), + cellBuilder: _calendarDayBuilder, useAvailableVerticalSpace: widget.shrinkWrap, ), ), @@ -273,7 +217,7 @@ class _CalendarPageState extends State { child: Row( children: [ GestureDetector( - onTap: UniversalPlatform.isMobile + onTap: PlatformExtension.isMobile ? () => showMobileBottomSheet( context, title: LocaleKeys.calendar_quickJumpYear.tr(), @@ -300,7 +244,7 @@ class _CalendarPageState extends State { DateFormat('MMMM y', context.locale.toLanguageTag()) .format(currentMonth), ), - if (UniversalPlatform.isMobile) ...[ + if (PlatformExtension.isMobile) ...[ const HSpace(6), const FlowySvg(FlowySvgs.arrow_down_s), ], @@ -350,7 +294,7 @@ class _CalendarPageState extends State { final symbols = DateFormat.EEEE(context.locale.toLanguageTag()).dateSymbols; String weekDayString = symbols.WEEKDAYS[(day + 1) % 7]; - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { weekDayString = weekDayString.substring(0, 3); } @@ -367,7 +311,6 @@ class _CalendarPageState extends State { } Widget _calendarDayBuilder( - BuildContext context, DateTime date, List> calenderEvents, isToday, @@ -379,22 +322,17 @@ class _CalendarPageState extends State { // is implemnted in the develop branch(WIP). Will be replaced with that. final events = calenderEvents.map((value) => value.event!).toList() ..sort((a, b) => a.event.timestamp.compareTo(b.event.timestamp)); - final isLocked = - context.watch()?.state.isLocked ?? false; - return IgnorePointer( - ignoring: isLocked, - child: CalendarDayCard( - viewId: widget.view.id, - isToday: isToday, - isInMonth: isInMonth, - events: events, - date: date, - rowCache: _calendarBloc.rowCache, - onCreateEvent: (date) => - _calendarBloc.add(CalendarEvent.createEvent(date)), - position: position, - ), + return CalendarDayCard( + viewId: widget.view.id, + isToday: isToday, + isInMonth: isInMonth, + events: events, + date: date, + rowCache: _calendarBloc.rowCache, + onCreateEvent: (date) => + _calendarBloc.add(CalendarEvent.createEvent(date)), + position: position, ); } @@ -407,10 +345,10 @@ class _CalendarPageState extends State { void showEventDetails({ required BuildContext context, required DatabaseController databaseController, - required RowMetaPB rowMeta, + required CalendarEventPB event, }) { final rowController = RowController( - rowMeta: rowMeta, + rowMeta: event.rowMeta, viewId: databaseController.viewId, rowCache: databaseController.rowCache, ); @@ -419,11 +357,10 @@ void showEventDetails({ context: context, builder: (BuildContext overlayContext) { return BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( rowController: rowController, databaseController: databaseController, - userProfile: context.read().userProfile, ), ); }, @@ -441,7 +378,13 @@ class UnscheduledEventsButton extends StatefulWidget { } class _UnscheduledEventsButtonState extends State { - final PopoverController _popoverController = PopoverController(); + late final PopoverController _popoverController; + + @override + void initState() { + super.initState(); + _popoverController = PopoverController(); + } @override Widget build(BuildContext context) { @@ -465,10 +408,11 @@ class _UnscheduledEventsButtonState extends State { ), side: BorderSide(color: Theme.of(context).dividerColor), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + visualDensity: VisualDensity.compact, ), onPressed: () { if (state.unscheduleEvents.isNotEmpty) { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { _showUnscheduledEventsMobile(state.unscheduleEvents); } else { _popoverController.show(); @@ -486,20 +430,15 @@ class _UnscheduledEventsButtonState extends State { ), ), ), - popupBuilder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: UnscheduleEventsList( + databaseController: widget.databaseController, + unscheduleEvents: state.unscheduleEvents, ), - BlocProvider.value( - value: context.read(), - ), - ], - child: UnscheduleEventsList( - databaseController: widget.databaseController, - unscheduleEvents: state.unscheduleEvents, - ), - ), + ); + }, ); }, ), @@ -512,7 +451,7 @@ class _UnscheduledEventsButtonState extends State { builder: (_) { return Column( children: [ - FlowyText( + FlowyText.medium( LocaleKeys.calendar_settings_unscheduledEventsTitle.tr(), ), UnscheduleEventsList( @@ -538,10 +477,10 @@ class UnscheduleEventsList extends StatelessWidget { @override Widget build(BuildContext context) { final cells = [ - if (!UniversalPlatform.isMobile) + if (!PlatformExtension.isMobile) Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: FlowyText( + child: FlowyText.medium( LocaleKeys.calendar_settings_clickToAdd.tr(), fontSize: 10, color: Theme.of(context).hintColor, @@ -552,7 +491,7 @@ class UnscheduleEventsList extends StatelessWidget { (event) => UnscheduledEventCell( event: event, onPressed: () { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { context.push( MobileRowDetailPage.routeName, extra: { @@ -564,7 +503,7 @@ class UnscheduleEventsList extends StatelessWidget { } else { showEventDetails( context: context, - rowMeta: event.rowMeta, + event: event, databaseController: databaseController, ); PopoverContainer.of(context).close(); @@ -582,7 +521,7 @@ class UnscheduleEventsList extends StatelessWidget { shrinkWrap: true, ); - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return Flexible(child: child); } @@ -602,7 +541,7 @@ class UnscheduledEventCell extends StatelessWidget { @override Widget build(BuildContext context) { - return UniversalPlatform.isMobile + return PlatformExtension.isMobile ? MobileUnscheduledEventTile(event: event, onPressed: onPressed) : DesktopUnscheduledEventTile(event: event, onPressed: onPressed); } @@ -624,7 +563,7 @@ class DesktopUnscheduledEventTile extends StatelessWidget { height: 26, child: FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - text: FlowyText( + text: FlowyText.medium( event.title.isEmpty ? LocaleKeys.calendar_defaultNewCalendarTitle.tr() : event.title, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart index 8a66191950..3ec5e7c34f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; @@ -7,9 +5,12 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Widget that displays a list of settings that alters the appearance of the @@ -29,12 +30,6 @@ class CalendarLayoutSetting extends StatefulWidget { class _CalendarLayoutSettingState extends State { final PopoverMutex popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider( @@ -172,15 +167,12 @@ class LayoutDateField extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - fieldInfo.name, - lineHeight: 1.0, - ), + text: FlowyText.medium(fieldInfo.name), onTap: () { onUpdated(fieldInfo.id); popoverMutex.close(); }, - leftIcon: const FlowySvg(FlowySvgs.date_s), + leftIcon: const FlowySvg(FlowySvgs.grid_s), rightIcon: fieldInfo.id == fieldId ? const FlowySvg(FlowySvgs.check_s) : null, @@ -207,8 +199,7 @@ class LayoutDateField extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( LocaleKeys.calendar_settings_layoutDateField.tr(), ), ), @@ -309,8 +300,7 @@ class FirstDayOfWeek extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( LocaleKeys.calendar_settings_firstDayOfWeek.tr(), ), ), @@ -330,11 +320,12 @@ Widget _toggleItem({ padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), child: Row( children: [ - FlowyText(text), + FlowyText.medium(text), const Spacer(), Toggle( value: value, - onChanged: (value) => onToggle(value), + onChanged: (value) => onToggle(!value), + style: ToggleStyle.big, padding: EdgeInsets.zero, ), ], @@ -371,10 +362,7 @@ class StartFromButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - title, - lineHeight: 1.0, - ), + text: FlowyText.medium(title), onTap: () => onTap(dayIndex), rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart index 6bfe7b99a8..cc496873ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_setting_bar.dart @@ -1,60 +1,26 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; class CalendarSettingBar extends StatelessWidget { const CalendarSettingBar({ super.key, required this.databaseController, - required this.toggleExtension, }); final DatabaseController databaseController; - final ToggleExtensionNotifier toggleExtension; @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FilterEditorBloc( - viewId: databaseController.viewId, - fieldController: databaseController.fieldController, - ), - child: ValueListenableBuilder( - valueListenable: databaseController.isLoading, - builder: (context, value, child) { - if (value) { - return const SizedBox.shrink(); - } - final isReference = - Provider.of(context)?.isReference ?? false; - return SizedBox( - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilterButton( - toggleExtension: toggleExtension, - ), - if (isReference) ...[ - const HSpace(2), - ViewDatabaseButton(view: databaseController.view), - ], - const HSpace(2), - SettingButton( - databaseController: databaseController, - ), - ], - ), - ); - }, + return SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SettingButton( + databaseController: databaseController, + ), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart index 3dcba2ca37..5b89d05643 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/checklist_cell_service.dart @@ -1,5 +1,6 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:protobuf/protobuf.dart'; @@ -17,16 +18,12 @@ class ChecklistCellBackendService { Future> create({ required String name, - int? index, }) { - final insert = ChecklistCellInsertPB()..name = name; - if (index != null) { - insert.index = index; - } - - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..insertTask.add(insert); + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..insertOptions.add(name); return DatabaseEventUpdateChecklistCell(payload).send(); } @@ -34,9 +31,11 @@ class ChecklistCellBackendService { Future> delete({ required List optionIds, }) { - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..deleteTasks.addAll(optionIds); + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..deleteOptionIds.addAll(optionIds); return DatabaseEventUpdateChecklistCell(payload).send(); } @@ -44,9 +43,11 @@ class ChecklistCellBackendService { Future> select({ required String optionId, }) { - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..completedTasks.add(optionId); + final payload = ChecklistCellDataChangesetPB.create() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..selectedOptionIds.add(optionId); return DatabaseEventUpdateChecklistCell(payload).send(); } @@ -59,28 +60,12 @@ class ChecklistCellBackendService { final newOption = option.rebuild((option) { option.name = name; }); - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..updateTasks.add(newOption); - - return DatabaseEventUpdateChecklistCell(payload).send(); - } - - Future> reorder({ - required fromTaskId, - required toTaskId, - }) { - final payload = ChecklistCellDataChangesetPB() - ..cellId = _makdeCellId() - ..reorder = "$fromTaskId $toTaskId"; - - return DatabaseEventUpdateChecklistCell(payload).send(); - } - - CellIdPB _makdeCellId() { - return CellIdPB() + final payload = ChecklistCellDataChangesetPB.create() ..viewId = viewId ..fieldId = fieldId - ..rowId = rowId; + ..rowId = rowId + ..updateOptions.add(newOption); + + return DatabaseEventUpdateChecklistCell(payload).send(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart index 4afd41ad9c..9a9f75e75f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/date_cell_service.dart @@ -10,7 +10,7 @@ final class DateCellBackendService { required String viewId, required String fieldId, required String rowId, - }) : cellId = CellIdPB() + }) : cellId = CellIdPB.create() ..viewId = viewId ..fieldId = fieldId ..rowId = rowId; @@ -18,27 +18,32 @@ final class DateCellBackendService { final CellIdPB cellId; Future> update({ - bool? includeTime, - bool? isRange, + required bool includeTime, + required bool isRange, DateTime? date, + String? time, DateTime? endDate, + String? endTime, String? reminderId, }) { - final payload = DateCellChangesetPB()..cellId = cellId; + final payload = DateCellChangesetPB.create() + ..cellId = cellId + ..includeTime = includeTime + ..isRange = isRange; - if (includeTime != null) { - payload.includeTime = includeTime; - } - if (isRange != null) { - payload.isRange = isRange; - } if (date != null) { final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000; - payload.timestamp = Int64(dateTimestamp); + payload.date = Int64(dateTimestamp); + } + if (time != null) { + payload.time = time; } if (endDate != null) { final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000; - payload.endTimestamp = Int64(dateTimestamp); + payload.endDate = Int64(dateTimestamp); + } + if (endTime != null) { + payload.endTime = endTime; } if (reminderId != null) { payload.reminderId = reminderId; @@ -48,7 +53,7 @@ final class DateCellBackendService { } Future> clear() { - final payload = DateCellChangesetPB() + final payload = DateCellChangesetPB.create() ..cellId = cellId ..clearFlag = true; diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart index 66c941891d..9bc873f7f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart @@ -20,7 +20,6 @@ class FieldBackendService { required String viewId, FieldType fieldType = FieldType.RichText, String? fieldName, - String? icon, Uint8List? typeOptionData, OrderObjectPositionPB? position, }) { @@ -89,7 +88,6 @@ class FieldBackendService { /// Update a field's properties Future> updateField({ String? name, - String? icon, bool? frozen, }) { final payload = FieldChangesetPB.create() @@ -100,10 +98,6 @@ class FieldBackendService { payload.name = name; } - if (icon != null) { - payload.icon = icon; - } - if (frozen != null) { payload.frozen = frozen; } @@ -116,18 +110,12 @@ class FieldBackendService { required String viewId, required String fieldId, required FieldType fieldType, - String? fieldName, }) { final payload = UpdateFieldTypePayloadPB() ..viewId = viewId ..fieldId = fieldId ..fieldType = fieldType; - // Only set if fieldName is not null - if (fieldName != null) { - payload.fieldName = fieldName; - } - return DatabaseEventUpdateFieldType(payload).send(); } @@ -189,13 +177,11 @@ class FieldBackendService { Future> updateType({ required FieldType fieldType, - String? fieldName, }) => updateFieldType( viewId: viewId, fieldId: fieldId, fieldType: fieldType, - fieldName: fieldName, ); Future> delete() => diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart index 4e191bf019..e618da5de9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart @@ -92,14 +92,20 @@ class FilterBackendService { Future> insertDateFilter({ required String fieldId, - required FieldType fieldType, String? filterId, required DateFilterConditionPB condition, + required FieldType fieldType, int? start, int? end, int? timestamp, }) { - final filter = DateFilterPB()..condition = condition; + assert( + fieldType == FieldType.DateTime || + fieldType == FieldType.LastEditedTime || + fieldType == FieldType.CreatedTime, + ); + + final filter = DateFilterPB(); if (timestamp != null) { filter.timestamp = $fixnum.Int64(timestamp); @@ -114,13 +120,13 @@ class FilterBackendService { return filterId == null ? insertFilter( fieldId: fieldId, - fieldType: fieldType, + fieldType: FieldType.DateTime, data: filter.writeToBuffer(), ) : updateFilter( filterId: filterId, fieldId: fieldId, - fieldType: fieldType, + fieldType: FieldType.DateTime, data: filter.writeToBuffer(), ); } @@ -275,34 +281,13 @@ class FilterBackendService { ); } - Future> insertMediaFilter({ - required String fieldId, - String? filterId, - required MediaFilterConditionPB condition, - String content = "", - }) { - final filter = MediaFilterPB() - ..condition = condition - ..content = content; - - return filterId == null - ? insertFilter( - fieldId: fieldId, - fieldType: FieldType.Media, - data: filter.writeToBuffer(), - ) - : updateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: FieldType.Media, - data: filter.writeToBuffer(), - ); - } - Future> deleteFilter({ + required String fieldId, required String filterId, }) async { - final deleteFilterPayload = DeleteFilterPB()..filterId = filterId; + final deleteFilterPayload = DeleteFilterPB() + ..fieldId = fieldId + ..filterId = filterId; final payload = DatabaseSettingChangesetPB() ..viewId = viewId diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart index f7a9be4a9c..934bbba8d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart @@ -22,10 +22,12 @@ class GroupBackendService { Future> updateGroup({ required String groupId, + required String fieldId, String? name, bool? visible, }) { final payload = UpdateGroupPB.create() + ..fieldId = fieldId ..viewId = viewId ..groupId = groupId; diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart index 2e0c24718e..93db3c703d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart @@ -70,7 +70,7 @@ class SelectOptionCellBackendService { return DatabaseEventUpdateSelectOptionCell(payload).send(); } - Future> unselect({ + Future> unSelect({ required Iterable optionIds, }) { final payload = SelectOptionCellChangesetPB() diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart index bdd8ea9716..12255afb7f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/sort_service.dart @@ -85,6 +85,7 @@ class SortBackendService { } Future> deleteSort({ + required String fieldId, required String sortId, }) { final deleteSortPayload = DeleteSortPayloadPB.create() diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart index a2b80a29df..e41fa61b2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart @@ -39,13 +39,11 @@ class CalculationsBloc extends Bloc { _startListening(); await _getAllCalculations(); - if (!isClosed) { - add( - CalculationsEvent.didReceiveFieldUpdate( - _fieldController.fieldInfos, - ), - ); - } + add( + CalculationsEvent.didReceiveFieldUpdate( + _fieldController.fieldInfos, + ), + ); }, didReceiveFieldUpdate: (fields) async { emit( @@ -133,10 +131,6 @@ class CalculationsBloc extends Bloc { Future _getAllCalculations() async { final calculationsOrFailure = await _calculationsService.getCalculations(); - if (isClosed) { - return; - } - final RepeatedCalculationsPB? calculations = calculationsOrFailure.fold((s) => s, (e) => null); if (calculations != null) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart new file mode 100644 index 0000000000..17449bda44 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart @@ -0,0 +1,105 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'checkbox_filter_editor_bloc.freezed.dart'; + +class CheckboxFilterEditorBloc + extends Bloc { + CheckboxFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(CheckboxFilterEditorState.initial(filterInfo)) { + _dispatch(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + updateCondition: (CheckboxFilterConditionPB condition) { + _filterBackendSvc.insertCheckboxFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + didReceiveFilter: (FilterPB filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + final checkboxFilter = filterInfo.checkboxFilter()!; + emit( + state.copyWith( + filterInfo: filterInfo, + filter: checkboxFilter, + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(CheckboxFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class CheckboxFilterEditorEvent with _$CheckboxFilterEditorEvent { + const factory CheckboxFilterEditorEvent.initial() = _Initial; + const factory CheckboxFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory CheckboxFilterEditorEvent.updateCondition( + CheckboxFilterConditionPB condition, + ) = _UpdateCondition; + const factory CheckboxFilterEditorEvent.delete() = _Delete; +} + +@freezed +class CheckboxFilterEditorState with _$CheckboxFilterEditorState { + const factory CheckboxFilterEditorState({ + required FilterInfo filterInfo, + required CheckboxFilterPB filter, + }) = _GridFilterState; + + factory CheckboxFilterEditorState.initial(FilterInfo filterInfo) { + return CheckboxFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.checkboxFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart new file mode 100644 index 0000000000..1decdd8215 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart @@ -0,0 +1,107 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'checklist_filter_bloc.freezed.dart'; + +class ChecklistFilterEditorBloc + extends Bloc { + ChecklistFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(ChecklistFilterEditorState.initial(filterInfo)) { + _dispatch(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + }, + updateCondition: (ChecklistFilterConditionPB condition) { + _filterBackendSvc.insertChecklistFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + didReceiveFilter: (FilterPB filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + final checklistFilter = filterInfo.checklistFilter()!; + emit( + state.copyWith( + filterInfo: filterInfo, + filter: checklistFilter, + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(ChecklistFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class ChecklistFilterEditorEvent with _$ChecklistFilterEditorEvent { + const factory ChecklistFilterEditorEvent.initial() = _Initial; + const factory ChecklistFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory ChecklistFilterEditorEvent.updateCondition( + ChecklistFilterConditionPB condition, + ) = _UpdateCondition; + const factory ChecklistFilterEditorEvent.delete() = _Delete; +} + +@freezed +class ChecklistFilterEditorState with _$ChecklistFilterEditorState { + const factory ChecklistFilterEditorState({ + required FilterInfo filterInfo, + required ChecklistFilterPB filter, + required String filterDesc, + }) = _GridFilterState; + + factory ChecklistFilterEditorState.initial(FilterInfo filterInfo) { + return ChecklistFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.checklistFilter()!, + filterDesc: '', + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart new file mode 100644 index 0000000000..a27b0bf000 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart @@ -0,0 +1,203 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'filter_create_bloc.freezed.dart'; + +class GridCreateFilterBloc + extends Bloc { + GridCreateFilterBloc({required this.viewId, required this.fieldController}) + : _filterBackendSvc = FilterBackendService(viewId: viewId), + super(GridCreateFilterState.initial(fieldController.fieldInfos)) { + _dispatch(); + } + + final String viewId; + final FilterBackendService _filterBackendSvc; + final FieldController fieldController; + void Function(List)? _onFieldFn; + + void _dispatch() { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveFields: (List fields) { + emit( + state.copyWith( + allFields: fields, + creatableFields: _filterFields(fields, state.filterText), + ), + ); + }, + didReceiveFilterText: (String text) { + emit( + state.copyWith( + filterText: text, + creatableFields: _filterFields(state.allFields, text), + ), + ); + }, + createDefaultFilter: (FieldInfo field) { + emit(state.copyWith(didCreateFilter: true)); + _createDefaultFilter(field); + }, + ); + }, + ); + } + + List _filterFields( + List fields, + String filterText, + ) { + final List allFields = List.from(fields); + final keyword = filterText.toLowerCase(); + allFields.retainWhere((field) { + if (!field.canCreateFilter) { + return false; + } + + if (filterText.isNotEmpty) { + return field.name.toLowerCase().contains(keyword); + } + + return true; + }); + + return allFields; + } + + void _startListening() { + _onFieldFn = (fields) { + fields.retainWhere((field) => field.canCreateFilter); + add(GridCreateFilterEvent.didReceiveFields(fields)); + }; + fieldController.addListener(onReceiveFields: _onFieldFn); + } + + Future> _createDefaultFilter( + FieldInfo field, + ) async { + final fieldId = field.id; + switch (field.fieldType) { + case FieldType.Checkbox: + return _filterBackendSvc.insertCheckboxFilter( + fieldId: fieldId, + condition: CheckboxFilterConditionPB.IsChecked, + ); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + return _filterBackendSvc.insertDateFilter( + fieldId: fieldId, + condition: DateFilterConditionPB.DateIs, + timestamp: timestamp, + fieldType: field.fieldType, + ); + case FieldType.MultiSelect: + return _filterBackendSvc.insertSelectOptionFilter( + fieldId: fieldId, + condition: SelectOptionFilterConditionPB.OptionContains, + fieldType: FieldType.MultiSelect, + ); + case FieldType.Checklist: + return _filterBackendSvc.insertChecklistFilter( + fieldId: fieldId, + condition: ChecklistFilterConditionPB.IsIncomplete, + ); + case FieldType.Number: + return _filterBackendSvc.insertNumberFilter( + fieldId: fieldId, + condition: NumberFilterConditionPB.Equal, + ); + case FieldType.Time: + return _filterBackendSvc.insertTimeFilter( + fieldId: fieldId, + condition: NumberFilterConditionPB.Equal, + ); + case FieldType.RichText: + return _filterBackendSvc.insertTextFilter( + fieldId: fieldId, + condition: TextFilterConditionPB.TextContains, + content: '', + ); + case FieldType.SingleSelect: + return _filterBackendSvc.insertSelectOptionFilter( + fieldId: fieldId, + condition: SelectOptionFilterConditionPB.OptionIs, + fieldType: FieldType.SingleSelect, + ); + case FieldType.URL: + return _filterBackendSvc.insertURLFilter( + fieldId: fieldId, + condition: TextFilterConditionPB.TextContains, + ); + default: + throw UnimplementedError(); + } + } + + @override + Future close() async { + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class GridCreateFilterEvent with _$GridCreateFilterEvent { + const factory GridCreateFilterEvent.initial() = _Initial; + const factory GridCreateFilterEvent.didReceiveFields(List fields) = + _DidReceiveFields; + + const factory GridCreateFilterEvent.createDefaultFilter(FieldInfo field) = + _CreateDefaultFilter; + + const factory GridCreateFilterEvent.didReceiveFilterText(String text) = + _DidReceiveFilterText; +} + +@freezed +class GridCreateFilterState with _$GridCreateFilterState { + const factory GridCreateFilterState({ + required String filterText, + required List creatableFields, + required List allFields, + required bool didCreateFilter, + }) = _GridFilterState; + + factory GridCreateFilterState.initial(List fields) { + return GridCreateFilterState( + filterText: "", + creatableFields: getCreatableFilter(fields), + allFields: fields, + didCreateFilter: false, + ); + } +} + +List getCreatableFilter(List fieldInfos) { + final List creatableFields = List.from(fieldInfos); + creatableFields.retainWhere((element) => element.canCreateFilter); + return creatableFields; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_editor_bloc.dart deleted file mode 100644 index 6a386ff130..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_editor_bloc.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/domain/filter_service.dart'; -import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'filter_editor_bloc.freezed.dart'; - -class FilterEditorBloc extends Bloc { - FilterEditorBloc({required this.viewId, required this.fieldController}) - : _filterBackendSvc = FilterBackendService(viewId: viewId), - super( - FilterEditorState.initial( - viewId, - fieldController.filters, - _getCreatableFilter(fieldController.fieldInfos), - ), - ) { - _dispatch(); - _startListening(); - } - - final String viewId; - final FieldController fieldController; - final FilterBackendService _filterBackendSvc; - - void Function(List)? _onFilterFn; - void Function(List)? _onFieldFn; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - didReceiveFilters: (filters) { - emit(state.copyWith(filters: filters)); - }, - didReceiveFields: (List fields) { - emit( - state.copyWith( - fields: _getCreatableFilter(fields), - ), - ); - }, - createFilter: (field) { - return _createDefaultFilter(null, field); - }, - changeFilteringField: (filterId, field) { - return _createDefaultFilter(filterId, field); - }, - updateFilter: (filter) { - return _filterBackendSvc.updateFilter( - filterId: filter.filterId, - fieldId: filter.fieldId, - fieldType: filter.fieldType, - data: filter.writeToBuffer(), - ); - }, - deleteFilter: (filterId) async { - return _filterBackendSvc.deleteFilter(filterId: filterId); - }, - ); - }, - ); - } - - void _startListening() { - _onFilterFn = (filters) { - add(FilterEditorEvent.didReceiveFilters(filters)); - }; - - _onFieldFn = (fields) { - add(FilterEditorEvent.didReceiveFields(fields)); - }; - - fieldController.addListener( - onFilters: _onFilterFn, - onReceiveFields: _onFieldFn, - ); - } - - @override - Future close() async { - if (_onFilterFn != null) { - fieldController.removeListener(onFiltersListener: _onFilterFn!); - _onFilterFn = null; - } - if (_onFieldFn != null) { - fieldController.removeListener(onFieldsListener: _onFieldFn!); - _onFieldFn = null; - } - return super.close(); - } - - Future> _createDefaultFilter( - String? filterId, - FieldInfo field, - ) async { - final fieldId = field.id; - switch (field.fieldType) { - case FieldType.Checkbox: - return _filterBackendSvc.insertCheckboxFilter( - filterId: filterId, - fieldId: fieldId, - condition: CheckboxFilterConditionPB.IsChecked, - ); - case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - final now = DateTime.now(); - final timestamp = - DateTime(now.year, now.month, now.day).millisecondsSinceEpoch ~/ - 1000; - return _filterBackendSvc.insertDateFilter( - filterId: filterId, - fieldId: fieldId, - fieldType: field.fieldType, - condition: DateFilterConditionPB.DateStartsOn, - timestamp: timestamp, - ); - case FieldType.MultiSelect: - return _filterBackendSvc.insertSelectOptionFilter( - filterId: filterId, - fieldId: fieldId, - condition: SelectOptionFilterConditionPB.OptionContains, - fieldType: FieldType.MultiSelect, - ); - case FieldType.Checklist: - return _filterBackendSvc.insertChecklistFilter( - filterId: filterId, - fieldId: fieldId, - condition: ChecklistFilterConditionPB.IsIncomplete, - ); - case FieldType.Number: - return _filterBackendSvc.insertNumberFilter( - filterId: filterId, - fieldId: fieldId, - condition: NumberFilterConditionPB.Equal, - ); - case FieldType.Time: - return _filterBackendSvc.insertTimeFilter( - filterId: filterId, - fieldId: fieldId, - condition: NumberFilterConditionPB.Equal, - ); - case FieldType.RichText: - return _filterBackendSvc.insertTextFilter( - filterId: filterId, - fieldId: fieldId, - condition: TextFilterConditionPB.TextContains, - content: '', - ); - case FieldType.SingleSelect: - return _filterBackendSvc.insertSelectOptionFilter( - filterId: filterId, - fieldId: fieldId, - condition: SelectOptionFilterConditionPB.OptionIs, - fieldType: FieldType.SingleSelect, - ); - case FieldType.URL: - return _filterBackendSvc.insertURLFilter( - filterId: filterId, - fieldId: fieldId, - condition: TextFilterConditionPB.TextContains, - ); - case FieldType.Media: - return _filterBackendSvc.insertMediaFilter( - filterId: filterId, - fieldId: fieldId, - condition: MediaFilterConditionPB.MediaIsNotEmpty, - ); - default: - throw UnimplementedError(); - } - } -} - -@freezed -class FilterEditorEvent with _$FilterEditorEvent { - const factory FilterEditorEvent.didReceiveFilters( - List filters, - ) = _DidReceiveFilters; - const factory FilterEditorEvent.didReceiveFields(List fields) = - _DidReceiveFields; - const factory FilterEditorEvent.createFilter(FieldInfo field) = _CreateFilter; - const factory FilterEditorEvent.updateFilter(DatabaseFilter filter) = - _UpdateFilter; - const factory FilterEditorEvent.changeFilteringField( - String filterId, - FieldInfo field, - ) = _ChangeFilteringField; - const factory FilterEditorEvent.deleteFilter(String filterId) = _DeleteFilter; -} - -@freezed -class FilterEditorState with _$FilterEditorState { - const factory FilterEditorState({ - required String viewId, - required List filters, - required List fields, - }) = _FilterEditorState; - - factory FilterEditorState.initial( - String viewId, - List filterInfos, - List fields, - ) => - FilterEditorState( - viewId: viewId, - filters: filterInfos, - fields: fields, - ); -} - -List _getCreatableFilter(List fieldInfos) { - final List creatableFields = List.from(fieldInfos); - creatableFields.retainWhere( - (field) => field.fieldType.canCreateFilter && !field.isGroupField, - ); - return creatableFields; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart new file mode 100644 index 0000000000..cc26e42b83 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart @@ -0,0 +1,129 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'filter_menu_bloc.freezed.dart'; + +class DatabaseFilterMenuBloc + extends Bloc { + DatabaseFilterMenuBloc({required this.viewId, required this.fieldController}) + : super( + DatabaseFilterMenuState.initial( + viewId, + fieldController.filterInfos, + fieldController.fieldInfos, + ), + ) { + _dispatch(); + } + + final String viewId; + final FieldController fieldController; + void Function(List)? _onFilterFn; + void Function(List)? _onFieldFn; + + void _dispatch() { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveFilters: (filters) { + emit(state.copyWith(filters: filters)); + }, + toggleMenu: () { + final isVisible = !state.isVisible; + emit(state.copyWith(isVisible: isVisible)); + }, + didReceiveFields: (List fields) { + emit( + state.copyWith( + fields: fields, + creatableFields: getCreatableFilter(fields), + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + _onFilterFn = (filters) { + add(DatabaseFilterMenuEvent.didReceiveFilters(filters)); + }; + + _onFieldFn = (fields) { + add(DatabaseFilterMenuEvent.didReceiveFields(fields)); + }; + + fieldController.addListener( + onFilters: (filters) { + _onFilterFn?.call(filters); + }, + onReceiveFields: (fields) { + _onFieldFn?.call(fields); + }, + ); + } + + @override + Future close() async { + if (_onFilterFn != null) { + fieldController.removeListener(onFiltersListener: _onFilterFn!); + _onFilterFn = null; + } + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn!); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent { + const factory DatabaseFilterMenuEvent.initial() = _Initial; + const factory DatabaseFilterMenuEvent.didReceiveFilters( + List filters, + ) = _DidReceiveFilters; + const factory DatabaseFilterMenuEvent.didReceiveFields( + List fields, + ) = _DidReceiveFields; + const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility; +} + +@freezed +class DatabaseFilterMenuState with _$DatabaseFilterMenuState { + const factory DatabaseFilterMenuState({ + required String viewId, + required List filters, + required List fields, + required List creatableFields, + required bool isVisible, + }) = _DatabaseFilterMenuState; + + factory DatabaseFilterMenuState.initial( + String viewId, + List filterInfos, + List fields, + ) => + DatabaseFilterMenuState( + viewId: viewId, + filters: filterInfos, + fields: fields, + creatableFields: getCreatableFilter(fields), + isVisible: false, + ); +} + +List getCreatableFilter(List fieldInfos) { + final List creatableFields = List.from(fieldInfos); + creatableFields.retainWhere((element) => element.canCreateFilter); + return creatableFields; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart new file mode 100644 index 0000000000..d68dd17537 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'number_filter_editor_bloc.freezed.dart'; + +class NumberFilterEditorBloc + extends Bloc { + NumberFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(NumberFilterEditorState.initial(filterInfo)) { + _dispatch(); + _startListening(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + event.when( + didReceiveFilter: (filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + emit( + state.copyWith( + filterInfo: filterInfo, + filter: filterInfo.numberFilter()!, + ), + ); + }, + updateCondition: (NumberFilterConditionPB condition) { + _filterBackendSvc.insertNumberFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (content) { + _filterBackendSvc.insertNumberFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(NumberFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class NumberFilterEditorEvent with _$NumberFilterEditorEvent { + const factory NumberFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory NumberFilterEditorEvent.updateCondition( + NumberFilterConditionPB condition, + ) = _UpdateCondition; + const factory NumberFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory NumberFilterEditorEvent.delete() = _Delete; +} + +@freezed +class NumberFilterEditorState with _$NumberFilterEditorState { + const factory NumberFilterEditorState({ + required FilterInfo filterInfo, + required NumberFilterPB filter, + }) = _NumberFilterEditorState; + + factory NumberFilterEditorState.initial(FilterInfo filterInfo) { + return NumberFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.numberFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart new file mode 100644 index 0000000000..3f44cb6d36 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'select_option_filter_bloc.freezed.dart'; + +class SelectOptionFilterEditorBloc + extends Bloc { + SelectOptionFilterEditorBloc({ + required this.filterInfo, + required this.delegate, + }) : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(SelectOptionFilterEditorState.initial(filterInfo)) { + _dispatch(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + final SelectOptionFilterDelegate delegate; + + void _dispatch() { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + _loadOptions(); + }, + updateCondition: (SelectOptionFilterConditionPB condition) { + _filterBackendSvc.insertSelectOptionFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + optionIds: state.filter.optionIds, + fieldType: state.filterInfo.fieldInfo.fieldType, + ); + }, + updateContent: (List optionIds) { + _filterBackendSvc.insertSelectOptionFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + optionIds: optionIds, + fieldType: state.filterInfo.fieldInfo.fieldType, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + didReceiveFilter: (FilterPB filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + final selectOptionFilter = filterInfo.selectOptionFilter()!; + emit( + state.copyWith( + filterInfo: filterInfo, + filter: selectOptionFilter, + ), + ); + }, + updateFilterDescription: (String desc) { + emit(state.copyWith(filterDesc: desc)); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(SelectOptionFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + void _loadOptions() { + if (!isClosed) { + final options = delegate.loadOptions(); + String filterDesc = ''; + for (final option in options) { + if (state.filter.optionIds.contains(option.id)) { + filterDesc += "${option.name} "; + } + } + add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc)); + } + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent { + const factory SelectOptionFilterEditorEvent.initial() = _Initial; + const factory SelectOptionFilterEditorEvent.didReceiveFilter( + FilterPB filter, + ) = _DidReceiveFilter; + const factory SelectOptionFilterEditorEvent.updateCondition( + SelectOptionFilterConditionPB condition, + ) = _UpdateCondition; + const factory SelectOptionFilterEditorEvent.updateContent( + List optionIds, + ) = _UpdateContent; + const factory SelectOptionFilterEditorEvent.updateFilterDescription( + String desc, + ) = _UpdateDesc; + const factory SelectOptionFilterEditorEvent.delete() = _Delete; +} + +@freezed +class SelectOptionFilterEditorState with _$SelectOptionFilterEditorState { + const factory SelectOptionFilterEditorState({ + required FilterInfo filterInfo, + required SelectOptionFilterPB filter, + required String filterDesc, + }) = _GridFilterState; + + factory SelectOptionFilterEditorState.initial(FilterInfo filterInfo) { + return SelectOptionFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.selectOptionFilter()!, + filterDesc: '', + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart new file mode 100644 index 0000000000..84e1284822 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart @@ -0,0 +1,158 @@ +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'select_option_filter_list_bloc.freezed.dart'; + +class SelectOptionFilterListBloc + extends Bloc { + SelectOptionFilterListBloc({ + required this.delegate, + required List selectedOptionIds, + }) : super(SelectOptionFilterListState.initial(selectedOptionIds)) { + _dispatch(); + } + + final SelectOptionFilterDelegate delegate; + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: () async { + _startListening(); + _loadOptions(); + }, + selectOption: (option, condition) { + final selectedOptionIds = delegate.selectOption( + state.selectedOptionIds, + option.id, + condition, + ); + + _updateSelectOptions( + selectedOptionIds: selectedOptionIds, + emit: emit, + ); + }, + unSelectOption: (option) { + final selectedOptionIds = Set.from(state.selectedOptionIds); + selectedOptionIds.remove(option.id); + + _updateSelectOptions( + selectedOptionIds: selectedOptionIds, + emit: emit, + ); + }, + didReceiveOptions: (newOptions) { + final List options = List.from(newOptions); + options.retainWhere( + (element) => element.name.contains(state.predicate), + ); + + final visibleOptions = options.map((option) { + return VisibleSelectOption( + option, + state.selectedOptionIds.contains(option.id), + ); + }).toList(); + + emit( + state.copyWith( + options: options, + visibleOptions: visibleOptions, + ), + ); + }, + filterOption: (optionName) { + _updateSelectOptions(predicate: optionName, emit: emit); + }, + ); + }, + ); + } + + void _updateSelectOptions({ + String? predicate, + Set? selectedOptionIds, + required Emitter emit, + }) { + final List visibleOptions = _makeVisibleOptions( + predicate ?? state.predicate, + selectedOptionIds ?? state.selectedOptionIds, + ); + + emit( + state.copyWith( + predicate: predicate ?? state.predicate, + visibleOptions: visibleOptions, + selectedOptionIds: selectedOptionIds ?? state.selectedOptionIds, + ), + ); + } + + List _makeVisibleOptions( + String predicate, + Set selectedOptionIds, + ) { + final List options = List.from(state.options); + options.retainWhere((element) => element.name.contains(predicate)); + + return options.map((option) { + return VisibleSelectOption(option, selectedOptionIds.contains(option.id)); + }).toList(); + } + + void _loadOptions() { + if (!isClosed) { + final options = delegate.loadOptions(); + add(SelectOptionFilterListEvent.didReceiveOptions(options)); + } + } + + void _startListening() {} +} + +@freezed +class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent { + const factory SelectOptionFilterListEvent.initial() = _Initial; + const factory SelectOptionFilterListEvent.selectOption( + SelectOptionPB option, + SelectOptionFilterConditionPB condition, + ) = _SelectOption; + const factory SelectOptionFilterListEvent.unSelectOption( + SelectOptionPB option, + ) = _UnSelectOption; + const factory SelectOptionFilterListEvent.didReceiveOptions( + List options, + ) = _DidReceiveOptions; + const factory SelectOptionFilterListEvent.filterOption(String optionName) = + _SelectOptionFilter; +} + +@freezed +class SelectOptionFilterListState with _$SelectOptionFilterListState { + const factory SelectOptionFilterListState({ + required List options, + required List visibleOptions, + required Set selectedOptionIds, + required String predicate, + }) = _SelectOptionFilterListState; + + factory SelectOptionFilterListState.initial(List selectedOptionIds) { + return SelectOptionFilterListState( + options: [], + predicate: '', + visibleOptions: [], + selectedOptionIds: selectedOptionIds.toSet(), + ); + } +} + +class VisibleSelectOption { + VisibleSelectOption(this.optionPB, this.isSelected); + + final SelectOptionPB optionPB; + final bool isSelected; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_loader.dart deleted file mode 100644 index 0e7c59bbb6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_loader.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -abstract class SelectOptionFilterDelegate { - const SelectOptionFilterDelegate(); - - List getOptions(FieldInfo fieldInfo); -} - -class SingleSelectOptionFilterDelegateImpl - implements SelectOptionFilterDelegate { - const SingleSelectOptionFilterDelegateImpl(); - - @override - List getOptions(FieldInfo fieldInfo) { - final parser = SingleSelectTypeOptionDataParser(); - return parser.fromBuffer(fieldInfo.field.typeOptionData).options; - } -} - -class MultiSelectOptionFilterDelegateImpl - implements SelectOptionFilterDelegate { - const MultiSelectOptionFilterDelegateImpl(); - - @override - List getOptions(FieldInfo fieldInfo) { - return MultiSelectTypeOptionDataParser() - .fromBuffer(fieldInfo.field.typeOptionData) - .options; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart new file mode 100644 index 0000000000..e4fa67c4a8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'text_filter_editor_bloc.freezed.dart'; + +class TextFilterEditorBloc + extends Bloc { + TextFilterEditorBloc({required this.filterInfo, required this.fieldType}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(TextFilterEditorState.initial(filterInfo)) { + _dispatch(); + } + + final FilterInfo filterInfo; + final FieldType fieldType; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + updateCondition: (TextFilterConditionPB condition) { + fieldType == FieldType.RichText + ? _filterBackendSvc.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ) + : _filterBackendSvc.insertURLFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (String content) { + fieldType == FieldType.RichText + ? _filterBackendSvc.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ) + : _filterBackendSvc.insertURLFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + didReceiveFilter: (FilterPB filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + final textFilter = filterInfo.textFilter()!; + emit( + state.copyWith( + filterInfo: filterInfo, + filter: textFilter, + ), + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(TextFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class TextFilterEditorEvent with _$TextFilterEditorEvent { + const factory TextFilterEditorEvent.initial() = _Initial; + const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory TextFilterEditorEvent.updateCondition( + TextFilterConditionPB condition, + ) = _UpdateCondition; + const factory TextFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory TextFilterEditorEvent.delete() = _Delete; +} + +@freezed +class TextFilterEditorState with _$TextFilterEditorState { + const factory TextFilterEditorState({ + required FilterInfo filterInfo, + required TextFilterPB filter, + }) = _GridFilterState; + + factory TextFilterEditorState.initial(FilterInfo filterInfo) { + return TextFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.textFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart new file mode 100644 index 0000000000..65625ca7f2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'time_filter_editor_bloc.freezed.dart'; + +class TimeFilterEditorBloc + extends Bloc { + TimeFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(TimeFilterEditorState.initial(filterInfo)) { + _dispatch(); + _startListening(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + event.when( + didReceiveFilter: (filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + emit( + state.copyWith( + filterInfo: filterInfo, + filter: filterInfo.timeFilter()!, + ), + ); + }, + updateCondition: (NumberFilterConditionPB condition) { + _filterBackendSvc.insertTimeFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (content) { + _filterBackendSvc.insertTimeFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(TimeFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class TimeFilterEditorEvent with _$TimeFilterEditorEvent { + const factory TimeFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory TimeFilterEditorEvent.updateCondition( + NumberFilterConditionPB condition, + ) = _UpdateCondition; + const factory TimeFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory TimeFilterEditorEvent.delete() = _Delete; +} + +@freezed +class TimeFilterEditorState with _$TimeFilterEditorState { + const factory TimeFilterEditorState({ + required FilterInfo filterInfo, + required TimeFilterPB filter, + }) = _TimeFilterEditorState; + + factory TimeFilterEditorState.initial(FilterInfo filterInfo) { + return TimeFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.timeFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart index 9b59b997b1..0a18a252e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_bloc.dart @@ -2,15 +2,13 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/defines.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,73 +18,30 @@ import '../../application/database_controller.dart'; part 'grid_bloc.freezed.dart'; class GridBloc extends Bloc { - GridBloc({ - required ViewPB view, - required this.databaseController, - this.shrinkWrapped = false, - }) : super(GridState.initial(view.id)) { + GridBloc({required ViewPB view, required this.databaseController}) + : super(GridState.initial(view.id)) { _dispatch(); } final DatabaseController databaseController; - /// When true will emit the count of visible rows to show - /// - final bool shrinkWrapped; - String get viewId => databaseController.viewId; - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - - DatabaseCallbacks? _databaseCallbacks; - - @override - Future close() async { - databaseController.removeListener(onDatabaseChanged: _databaseCallbacks); - _databaseCallbacks = null; - await super.close(); - } - void _dispatch() { on( (event, emit) async { await event.when( initial: () async { - final response = await UserEventGetUserProfile().send(); - response.fold( - (userProfile) => _userProfile = userProfile, - (err) => Log.error(err), - ); - _startListening(); await _openGrid(emit); }, - openRowDetail: (row) { - emit( - state.copyWith( - createdRow: row, - openRowDetail: true, - ), - ); - }, createRow: (openRowDetail) async { - final lastVisibleRowId = - shrinkWrapped ? state.lastVisibleRow?.rowId : null; - - final result = await RowBackendService.createRow( - viewId: viewId, - position: lastVisibleRowId != null - ? OrderObjectPositionTypePB.After - : null, - targetRowId: lastVisibleRowId, - ); + final result = await RowBackendService.createRow(viewId: viewId); result.fold( (createdRow) => emit( state.copyWith( createdRow: createdRow, openRowDetail: openRowDetail ?? false, - visibleRows: state.visibleRows + 1, ), ), (err) => Log.error(err), @@ -109,8 +64,15 @@ class GridBloc extends Bloc { databaseController.moveRow(fromRowId: fromRow, toRowId: toRow); }, + didReceiveGridUpdate: (grid) { + emit(state.copyWith(grid: grid)); + }, didReceiveFieldUpdate: (fields) { - emit(state.copyWith(fields: fields)); + emit( + state.copyWith( + fields: fields, + ), + ); }, didLoadRows: (newRowInfos, reason) { emit( @@ -121,38 +83,39 @@ class GridBloc extends Bloc { ), ); }, - didReceveFilters: (filters) { - emit(state.copyWith(filters: filters)); + didReceveFilters: (List filters) { + emit( + state.copyWith(filters: filters), + ); }, - didReceveSorts: (sorts) { - emit(state.copyWith(reorderable: sorts.isEmpty, sorts: sorts)); - }, - loadMoreRows: () { - emit(state.copyWith(visibleRows: state.visibleRows + 25)); + didReceveSorts: (List sorts) { + emit( + state.copyWith( + reorderable: sorts.isEmpty, + sorts: sorts, + ), + ); }, ); }, ); } - RowCache get rowCache => databaseController.rowCache; + RowCache getRowCache(RowId rowId) => databaseController.rowCache; void _startListening() { - _databaseCallbacks = DatabaseCallbacks( + final onDatabaseChanged = DatabaseCallbacks( + onDatabaseChanged: (database) { + if (!isClosed) { + add(GridEvent.didReceiveGridUpdate(database)); + } + }, onNumOfRowsChanged: (rowInfos, _, reason) { if (!isClosed) { add(GridEvent.didLoadRows(rowInfos, reason)); } }, - onRowsCreated: (rows) { - for (final row in rows) { - if (!isClosed && row.isHiddenInView) { - add(GridEvent.openRowDetail(row.rowMeta)); - } - } - }, onRowsUpdated: (rows, reason) { - // TODO(nathan): separate different reasons if (!isClosed) { add( GridEvent.didLoadRows(databaseController.rowCache.rowInfos, reason), @@ -175,7 +138,7 @@ class GridBloc extends Bloc { } }, ); - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); } Future _openGrid(Emitter emit) async { @@ -201,7 +164,6 @@ class GridBloc extends Bloc { @freezed class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; - const factory GridEvent.openRowDetail(RowMetaPB row) = _OpenRowDetail; const factory GridEvent.createRow({bool? openRowDetail}) = _CreateRow; const factory GridEvent.resetCreatedRow() = _ResetCreatedRow; const factory GridEvent.deleteRow(RowInfo rowInfo) = _DeleteRow; @@ -213,17 +175,22 @@ class GridEvent with _$GridEvent { const factory GridEvent.didReceiveFieldUpdate( List fields, ) = _DidReceiveFieldUpdate; - const factory GridEvent.didReceveFilters(List filters) = + + const factory GridEvent.didReceiveGridUpdate( + DatabasePB grid, + ) = _DidReceiveGridUpdate; + + const factory GridEvent.didReceveFilters(List filters) = _DidReceiveFilters; - const factory GridEvent.didReceveSorts(List sorts) = + const factory GridEvent.didReceveSorts(List sorts) = _DidReceiveSorts; - const factory GridEvent.loadMoreRows() = _LoadMoreRows; } @freezed class GridState with _$GridState { const factory GridState({ required String viewId, + required DatabasePB? grid, required List fields, required List rowInfos, required int rowCount, @@ -231,10 +198,9 @@ class GridState with _$GridState { required LoadingState loadingState, required bool reorderable, required ChangedReason reason, - required List sorts, - required List filters, + required List sorts, + required List filters, required bool openRowDetail, - @Default(0) int visibleRows, }) = _GridState; factory GridState.initial(String viewId) => GridState( @@ -242,6 +208,7 @@ class GridState with _$GridState { rowInfos: [], rowCount: 0, createdRow: null, + grid: null, viewId: viewId, reorderable: true, loadingState: const LoadingState.loading(), @@ -249,19 +216,5 @@ class GridState with _$GridState { filters: [], sorts: [], openRowDetail: false, - visibleRows: 25, ); } - -extension _LastVisibleRow on GridState { - /// Returns the last visible [RowInfo] in the list of [rowInfos]. - /// Only returns if the visibleRows is less than the rowCount, otherwise returns null. - /// - RowInfo? get lastVisibleRow { - if (visibleRows < rowCount) { - return rowInfos[visibleRows - 1]; - } - - return null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart index a869b636d2..b9fa5b7e92 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/grid_header_bloc.dart @@ -20,12 +20,6 @@ class GridHeaderBloc extends Bloc { final String viewId; final FieldController fieldController; - @override - Future close() async { - fieldController.removeListener(onFieldsListener: _onReceiveFields); - await super.close(); - } - void _dispatch() { on( (event, emit) async { @@ -88,13 +82,11 @@ class GridHeaderBloc extends Bloc { void _startListening() { fieldController.addListener( - onReceiveFields: _onReceiveFields, + onReceiveFields: (fields) => + add(GridHeaderEvent.didReceiveFieldUpdate(fields)), listenWhen: () => !isClosed, ); } - - void _onReceiveFields(List fields) => - add(GridHeaderEvent.didReceiveFieldUpdate(fields)); } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart index ec42cd5cac..60c7ad8e65 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/mobile_row_detail_bloc.dart @@ -1,10 +1,5 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -14,32 +9,17 @@ class MobileRowDetailBloc extends Bloc { MobileRowDetailBloc({required this.databaseController}) : super(MobileRowDetailState.initial()) { - rowBackendService = RowBackendService(viewId: databaseController.viewId); _dispatch(); } final DatabaseController databaseController; - late final RowBackendService rowBackendService; - - UserProfilePB? _userProfile; - UserProfilePB? get userProfile => _userProfile; - - DatabaseCallbacks? _databaseCallbacks; - - @override - Future close() async { - databaseController.removeListener(onDatabaseChanged: _databaseCallbacks); - _databaseCallbacks = null; - await super.close(); - } void _dispatch() { on( (event, emit) { event.when( - initial: (rowId) async { + initial: (rowId) { _startListening(); - emit( state.copyWith( isLoading: false, @@ -47,12 +27,6 @@ class MobileRowDetailBloc rowInfos: databaseController.rowCache.rowInfos, ), ); - - final result = await UserEventGetUserProfile().send(); - result.fold( - (profile) => _userProfile = profile, - (error) => Log.error(error), - ); }, didLoadRows: (rows) { emit(state.copyWith(rowInfos: rows)); @@ -60,23 +34,13 @@ class MobileRowDetailBloc changeRowId: (rowId) { emit(state.copyWith(currentRowId: rowId)); }, - addCover: (rowCover) async { - if (state.currentRowId == null) { - return; - } - - await rowBackendService.updateMeta( - rowId: state.currentRowId!, - cover: rowCover, - ); - }, ); }, ); } void _startListening() { - _databaseCallbacks = DatabaseCallbacks( + final onDatabaseChanged = DatabaseCallbacks( onNumOfRowsChanged: (rowInfos, _, reason) { if (!isClosed) { add(MobileRowDetailEvent.didLoadRows(rowInfos)); @@ -92,7 +56,7 @@ class MobileRowDetailBloc } }, ); - databaseController.addListener(onDatabaseChanged: _databaseCallbacks); + databaseController.addListener(onDatabaseChanged: onDatabaseChanged); } } @@ -102,7 +66,6 @@ class MobileRowDetailEvent with _$MobileRowDetailEvent { const factory MobileRowDetailEvent.didLoadRows(List rows) = _DidLoadRows; const factory MobileRowDetailEvent.changeRowId(String rowId) = _ChangeRowId; - const factory MobileRowDetailEvent.addCover(RowCoverPB cover) = _AddCover; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart index 402ae6e596..a0c0467b95 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_bloc.dart @@ -26,7 +26,6 @@ class RowBloc extends Bloc { _dispatch(); _startListening(); _init(); - rowController.initialize(); } final FieldController fieldController; @@ -37,7 +36,7 @@ class RowBloc extends Bloc { @override Future close() async { - await _rowController.dispose(); + _rowController.dispose(); return super.close(); } @@ -70,19 +69,20 @@ class RowBloc extends Bloc { ); } - void _startListening() => - _rowController.addListener(onRowChanged: _onRowChanged); - - void _onRowChanged(List cells, ChangedReason reason) { - if (!isClosed) { - add(RowEvent.didReceiveCells(cells, reason)); - } + void _startListening() { + _rowController.addListener( + onRowChanged: (cells, reason) { + if (!isClosed) { + add(RowEvent.didReceiveCells(cells, reason)); + } + }, + ); } void _init() { add( RowEvent.didReceiveCells( - _rowController.loadCells(), + _rowController.loadData(), const ChangedReason.setInitialRows(), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart index e7d4658df8..0d655a840b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_detail_bloc.dart @@ -1,17 +1,12 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/row/row_controller.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -import 'package:appflowy/plugins/database/domain/row_meta_listener.dart'; +import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -21,26 +16,20 @@ class RowDetailBloc extends Bloc { RowDetailBloc({ required this.fieldController, required this.rowController, - }) : _metaListener = RowMetaListener(rowController.rowId), - _rowService = RowBackendService(viewId: rowController.viewId), - super(RowDetailState.initial(rowController.rowMeta)) { + }) : super(RowDetailState.initial()) { _dispatch(); _startListening(); _init(); - - rowController.initialize(); } final FieldController fieldController; final RowController rowController; - final RowMetaListener _metaListener; - final RowBackendService _rowService; + final List allCells = []; @override Future close() async { - await rowController.dispose(); - await _metaListener.stop(); + rowController.dispose(); return super.close(); } @@ -91,35 +80,12 @@ class RowDetailBloc extends Bloc { ), ); }, - startEditingField: (fieldId) { - emit(state.copyWith(editingFieldId: fieldId)); - }, - startEditingNewField: (fieldId) { - emit(state.copyWith(editingFieldId: fieldId, newFieldId: fieldId)); - }, - endEditingField: () { - emit(state.copyWith(editingFieldId: "", newFieldId: "")); - }, - removeCover: () => _rowService.removeCover(rowController.rowId), - setCover: (cover) => - _rowService.updateMeta(rowId: rowController.rowId, cover: cover), - didReceiveRowMeta: (rowMeta) { - emit(state.copyWith(rowMeta: rowMeta)); - }, ); }, ); } void _startListening() { - _metaListener.start( - callback: (rowMeta) { - if (!isClosed) { - add(RowDetailEvent.didReceiveRowMeta(rowMeta)); - } - }, - ); - rowController.addListener( onRowChanged: (cellMap, reason) { if (isClosed) { @@ -159,7 +125,7 @@ class RowDetailBloc extends Bloc { } void _init() { - allCells.addAll(rowController.loadCells()); + allCells.addAll(rowController.loadData()); int numHiddenFields = 0; final visibleCells = []; for (final cell in allCells) { @@ -251,23 +217,6 @@ class RowDetailEvent with _$RowDetailEvent { /// Used to hide/show the hidden fields in the row detail page const factory RowDetailEvent.toggleHiddenFieldVisibility() = _ToggleHiddenFieldVisibility; - - /// Begin editing an event; - const factory RowDetailEvent.startEditingField(String fieldId) = - _StartEditingField; - - const factory RowDetailEvent.startEditingNewField(String fieldId) = - _StartEditingNewField; - - /// End editing an event - const factory RowDetailEvent.endEditingField() = _EndEditingField; - - const factory RowDetailEvent.removeCover() = _RemoveCover; - - const factory RowDetailEvent.setCover(RowCoverPB cover) = _SetCover; - - const factory RowDetailEvent.didReceiveRowMeta(RowMetaPB rowMeta) = - _DidReceiveRowMeta; } @freezed @@ -277,18 +226,12 @@ class RowDetailState with _$RowDetailState { required List visibleCells, required bool showHiddenFields, required int numHiddenFields, - required String editingFieldId, - required String newFieldId, - required RowMetaPB rowMeta, }) = _RowDetailState; - factory RowDetailState.initial(RowMetaPB rowMeta) => RowDetailState( + factory RowDetailState.initial() => const RowDetailState( fields: [], visibleCells: [], showHiddenFields: false, numHiddenFields: 0, - editingFieldId: "", - newFieldId: "", - rowMeta: rowMeta, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart index 743d854b23..93b55bfb3d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/row/row_document_bloc.dart @@ -81,7 +81,7 @@ class RowDocumentBloc extends Bloc { // new document for the given document id of the row. final documentView = await _createRowDocumentView(rowMeta.documentId); - if (documentView != null && !isClosed) { + if (documentView != null) { add(RowDocumentEvent.didReceiveRowDocument(documentView)); } } else { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/simple_text_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/simple_text_filter_bloc.dart deleted file mode 100644 index bbf010d279..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/simple_text_filter_bloc.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'simple_text_filter_bloc.freezed.dart'; - -class SimpleTextFilterBloc - extends Bloc, SimpleTextFilterState> { - SimpleTextFilterBloc({ - required this.values, - required this.comparator, - this.filterText = "", - }) : super(SimpleTextFilterState(values: values)) { - _dispatch(); - } - - final String Function(T) comparator; - - final List values; - String filterText; - - void _dispatch() { - on>((event, emit) async { - event.when( - updateFilter: (String filter) { - filterText = filter.toLowerCase(); - _filter(emit); - }, - receiveNewValues: (List newValues) { - values - ..clear() - ..addAll(newValues); - _filter(emit); - }, - ); - }); - } - - void _filter(Emitter> emit) { - final List result = [...values]; - - result.retainWhere((value) { - if (filterText.isNotEmpty) { - return comparator(value).toLowerCase().contains(filterText); - } - return true; - }); - - emit(SimpleTextFilterState(values: result)); - } -} - -@freezed -class SimpleTextFilterEvent with _$SimpleTextFilterEvent { - const factory SimpleTextFilterEvent.updateFilter(String filter) = - _UpdateFilter; - const factory SimpleTextFilterEvent.receiveNewValues(List newValues) = - _ReceiveNewValues; -} - -@freezed -class SimpleTextFilterState with _$SimpleTextFilterState { - const factory SimpleTextFilterState({ - required List values, - }) = _SimpleTextFilterState; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart index 37b0e37747..5f2bd8cf89 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart @@ -2,9 +2,8 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; import 'package:appflowy/plugins/database/domain/sort_service.dart'; -import 'package:appflowy/util/field_type_extension.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_info.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; @@ -20,7 +19,7 @@ class SortEditorBloc extends Bloc { }) : _sortBackendSvc = SortBackendService(viewId: viewId), super( SortEditorState.initial( - fieldController.sorts, + fieldController.sortInfos, fieldController.fieldInfos, ), ) { @@ -33,7 +32,7 @@ class SortEditorBloc extends Bloc { final FieldController fieldController; void Function(List)? _onFieldFn; - void Function(List)? _onSortsFn; + void Function(List)? _onSortsFn; void _dispatch() { on( @@ -43,10 +42,13 @@ class SortEditorBloc extends Bloc { emit( state.copyWith( allFields: fields, - creatableFields: _getCreatableSorts(fields), + creatableFields: getCreatableSorts(fields), ), ); }, + updateCreateSortFilter: (text) { + emit(state.copyWith(filter: text)); + }, createSort: ( String fieldId, SortConditionPB? condition, @@ -62,16 +64,16 @@ class SortEditorBloc extends Bloc { String? fieldId, SortConditionPB? condition, ) async { - final sort = state.sorts + final sortInfo = state.sortInfos .firstWhereOrNull((element) => element.sortId == sortId); - if (sort == null) { + if (sortInfo == null) { return; } final result = await _sortBackendSvc.updateSort( sortId: sortId, - fieldId: fieldId ?? sort.fieldId, - condition: condition ?? sort.condition, + fieldId: fieldId ?? sortInfo.fieldId, + condition: condition ?? sortInfo.sortPB.condition, ); result.fold((l) => {}, (err) => Log.error(err)); }, @@ -79,12 +81,13 @@ class SortEditorBloc extends Bloc { final result = await _sortBackendSvc.deleteAllSorts(); result.fold((l) => {}, (err) => Log.error(err)); }, - didReceiveSorts: (sorts) { - emit(state.copyWith(sorts: sorts)); + didReceiveSorts: (List sortInfos) { + emit(state.copyWith(sortInfos: sortInfos)); }, - deleteSort: (sortId) async { + deleteSort: (SortInfo sortInfo) async { final result = await _sortBackendSvc.deleteSort( - sortId: sortId, + fieldId: sortInfo.fieldInfo.id, + sortId: sortInfo.sortId, ); result.fold((l) => null, (err) => Log.error(err)); }, @@ -93,12 +96,12 @@ class SortEditorBloc extends Bloc { toIndex--; } - final fromId = state.sorts[fromIndex].sortId; - final toId = state.sorts[toIndex].sortId; + final fromId = state.sortInfos[fromIndex].sortId; + final toId = state.sortInfos[toIndex].sortId; - final newSorts = [...state.sorts]; + final newSorts = [...state.sortInfos]; newSorts.insert(toIndex, newSorts.removeAt(fromIndex)); - emit(state.copyWith(sorts: newSorts)); + emit(state.copyWith(sortInfos: newSorts)); final result = await _sortBackendSvc.reorderSort( fromSortId: fromId, toSortId: toId, @@ -141,8 +144,10 @@ class SortEditorBloc extends Bloc { class SortEditorEvent with _$SortEditorEvent { const factory SortEditorEvent.didReceiveFields(List fieldInfos) = _DidReceiveFields; - const factory SortEditorEvent.didReceiveSorts(List sorts) = + const factory SortEditorEvent.didReceiveSorts(List sortInfos) = _DidReceiveSorts; + const factory SortEditorEvent.updateCreateSortFilter(String text) = + _UpdateCreateSortFilter; const factory SortEditorEvent.createSort({ required String fieldId, SortConditionPB? condition, @@ -154,34 +159,34 @@ class SortEditorEvent with _$SortEditorEvent { }) = _EditSort; const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) = _ReorderSort; - const factory SortEditorEvent.deleteSort(String sortId) = _DeleteSort; + const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort; const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; } @freezed class SortEditorState with _$SortEditorState { const factory SortEditorState({ - required List sorts, - required List allFields, + required List sortInfos, required List creatableFields, + required List allFields, + required String filter, }) = _SortEditorState; factory SortEditorState.initial( - List sorts, + List sortInfos, List fields, ) { return SortEditorState( - sorts: sorts, + creatableFields: getCreatableSorts(fields), allFields: fields, - creatableFields: _getCreatableSorts(fields), + sortInfos: sortInfos, + filter: "", ); } } -List _getCreatableSorts(List fieldInfos) { +List getCreatableSorts(List fieldInfos) { final List creatableFields = List.from(fieldInfos); - creatableFields.retainWhere( - (field) => field.fieldType.canCreateSort && !field.hasSort, - ); + creatableFields.retainWhere((element) => element.canCreateSort); return creatableFields; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 4c9fd7bd61..67e238ed51 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -1,35 +1,30 @@ -import 'dart:async'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/domain/sort_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.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: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:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; -import 'package:provider/provider.dart'; import '../../application/database_controller.dart'; import '../../application/row/row_controller.dart'; import '../../tab_bar/tab_bar_view.dart'; import '../../widgets/row/row_detail.dart'; import '../application/grid_bloc.dart'; + import 'grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; @@ -65,7 +60,6 @@ class DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { view: view, databaseController: controller, initialRowId: initialRowId, - shrinkWrap: shrinkWrap, ); } @@ -109,14 +103,12 @@ class GridPage extends StatefulWidget { required this.databaseController, this.onDeleted, this.initialRowId, - this.shrinkWrap = false, }); final ViewPB view; final DatabaseController databaseController; final VoidCallback? onDeleted; final String? initialRowId; - final bool shrinkWrap; @override State createState() => _GridPageState(); @@ -125,30 +117,13 @@ class GridPage extends StatefulWidget { class _GridPageState extends State { bool _didOpenInitialRow = false; - late final GridBloc gridBloc = GridBloc( - view: widget.view, - databaseController: widget.databaseController, - shrinkWrapped: widget.shrinkWrap, - )..add(const GridEvent.initial()); - - @override - void dispose() { - gridBloc.close(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => gridBloc, - ), - BlocProvider( - create: (context) => ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - ], + return BlocProvider( + create: (context) => GridBloc( + view: widget.view, + databaseController: widget.databaseController, + )..add(const GridEvent.initial()), child: BlocListener( listener: (context, state) { final action = state.action; @@ -163,32 +138,46 @@ class _GridPageState extends State { } }, child: BlocConsumer( - listener: listener, + listener: (context, state) => state.loadingState.whenOrNull( + // If initial row id is defined, open row details overlay + finish: (_) { + if (widget.initialRowId != null && !_didOpenInitialRow) { + _didOpenInitialRow = true; + + _openRow(context, widget.initialRowId!); + } + + return; + }, + ), builder: (context, state) => state.loadingState.map( - idle: (_) => const SizedBox.shrink(), - loading: (_) => const Center( - child: CircularProgressIndicator.adaptive(), - ), + loading: (_) => + const Center(child: CircularProgressIndicator.adaptive()), finish: (result) => result.successOrFail.fold( (_) => GridShortcuts( child: GridPageContent( - key: ValueKey(widget.view.id), view: widget.view, - shrinkWrap: widget.shrinkWrap, ), ), - (err) => Center(child: AppFlowyErrorPage(error: err)), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ), ), + idle: (_) => const SizedBox.shrink(), ), ), ), ); } - void _openRow(BuildContext context, String rowId) { + void _openRow( + BuildContext context, + String rowId, + ) { WidgetsBinding.instance.addPostFrameCallback((_) { final gridBloc = context.read(); - final rowCache = gridBloc.rowCache; + final rowCache = gridBloc.getRowCache(rowId); final rowMeta = rowCache.getRow(rowId)?.rowMeta; if (rowMeta == null) { return; @@ -203,69 +192,24 @@ class _GridPageState extends State { FlowyOverlay.show( context: context, builder: (_) => BlocProvider.value( - value: context.read(), + value: context.read(), child: RowDetailPage( databaseController: context.read().databaseController, rowController: rowController, - userProfile: context.read().userProfile, ), ), ); }); } - - void listener(BuildContext context, GridState state) { - state.loadingState.whenOrNull( - // If initial row id is defined, open row details overlay - finish: (_) async { - if (widget.initialRowId != null && !_didOpenInitialRow) { - _didOpenInitialRow = true; - - _openRow(context, widget.initialRowId!); - return; - } - - final bloc = context.read(); - final isCurrentView = - bloc.state.tabBars[bloc.state.selectedIndex].viewId == - widget.view.id; - - if (state.openRowDetail && state.createdRow != null && isCurrentView) { - final rowController = RowController( - viewId: widget.view.id, - rowMeta: state.createdRow!, - rowCache: context.read().rowCache, - ); - unawaited( - FlowyOverlay.show( - context: context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: - context.read().databaseController, - rowController: rowController, - userProfile: context.read().userProfile, - ), - ), - ), - ); - context.read().add(const GridEvent.resetCreatedRow()); - } - }, - ); - } } class GridPageContent extends StatefulWidget { const GridPageContent({ super.key, required this.view, - this.shrinkWrap = false, }); final ViewPB view; - final bool shrinkWrap; @override State createState() => _GridPageContentState(); @@ -293,17 +237,13 @@ class _GridPageContentState extends State { Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ _GridHeader( headerScrollController: headerScrollController, - editable: !context.read().state.isLocked, - shrinkWrap: widget.shrinkWrap, ), _GridRows( viewId: widget.view.id, scrollController: _scrollController, - shrinkWrap: widget.shrinkWrap, ), ], ); @@ -311,33 +251,20 @@ class _GridPageContentState extends State { } class _GridHeader extends StatelessWidget { - const _GridHeader({ - required this.headerScrollController, - required this.editable, - required this.shrinkWrap, - }); + const _GridHeader({required this.headerScrollController}); final ScrollController headerScrollController; - final bool editable; - final bool shrinkWrap; @override Widget build(BuildContext context) { - Widget child = BlocBuilder( - builder: (_, state) => GridHeaderSliverAdaptor( - viewId: state.viewId, - anchorScrollController: headerScrollController, - shrinkWrap: shrinkWrap, - ), + return BlocBuilder( + builder: (context, state) { + return GridHeaderSliverAdaptor( + viewId: state.viewId, + anchorScrollController: headerScrollController, + ); + }, ); - - if (!editable) { - child = IgnorePointer( - child: child, - ); - } - - return child; } } @@ -345,244 +272,133 @@ class _GridRows extends StatefulWidget { const _GridRows({ required this.viewId, required this.scrollController, - this.shrinkWrap = false, }); final String viewId; final GridScrollController scrollController; - /// When [shrinkWrap] is active, the Grid will show items according to - /// GridState.visibleRows and will not have a vertical scroll area. - /// - final bool shrinkWrap; - @override State<_GridRows> createState() => _GridRowsState(); } class _GridRowsState extends State<_GridRows> { bool showFloatingCalculations = false; - bool isAtBottom = false; @override void initState() { super.initState(); - if (!widget.shrinkWrap) { - _evaluateFloatingCalculations(); - widget.scrollController.verticalController.addListener(_onScrollChanged); - } - } - - void _onScrollChanged() { - final controller = widget.scrollController.verticalController; - final isAtBottom = controller.position.atEdge && controller.offset > 0 || - controller.offset >= controller.position.maxScrollExtent - 1; - if (isAtBottom != this.isAtBottom) { - setState(() => this.isAtBottom = isAtBottom); - } - } - - @override - void dispose() { - if (!widget.shrinkWrap) { - widget.scrollController.verticalController - .removeListener(_onScrollChanged); - } - super.dispose(); + _evaluateFloatingCalculations(); } void _evaluateFloatingCalculations() { WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted && !widget.shrinkWrap) { - setState(() { - final verticalController = widget.scrollController.verticalController; - // maxScrollExtent is 0.0 if scrolling is not possible - showFloatingCalculations = - verticalController.position.maxScrollExtent > 0; - - isAtBottom = verticalController.position.atEdge && - verticalController.offset > 0; - }); - } + setState(() { + // maxScrollExtent is 0.0 if scrolling is not possible + showFloatingCalculations = widget + .scrollController.verticalController.position.maxScrollExtent > + 0; + }); }); } @override Widget build(BuildContext context) { - Widget child; - if (widget.shrinkWrap) { - child = Scrollbar( - controller: widget.scrollController.horizontalController, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: widget.scrollController.horizontalController, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: GridLayout.headerWidth( - context - .read() - .horizontalPadding * - 3, - context.read().state.fields, - ), + return BlocBuilder( + buildWhen: (previous, current) => previous.fields != current.fields, + builder: (context, state) { + return Flexible( + child: _WrapScrollView( + scrollController: widget.scrollController, + contentWidth: GridLayout.headerWidth(state.fields), + child: BlocConsumer( + listenWhen: (previous, current) => + previous.rowCount != current.rowCount, + listener: (context, state) => _evaluateFloatingCalculations(), + builder: (context, state) { + return ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + ), + child: _renderList(context, state), + ); + }, ), - child: _shrinkWrapRenderList(context), ), + ); + }, + ); + } + + Widget _renderList( + BuildContext context, + GridState state, + ) { + final children = state.rowInfos.mapIndexed((index, rowInfo) { + return _renderRow( + context, + rowInfo.rowId, + isDraggable: state.reorderable, + index: index, + ); + }).toList() + ..add(const GridRowBottomBar(key: Key('grid_footer'))); + + if (showFloatingCalculations) { + children.add( + const SizedBox( + key: Key('calculations_bottom_padding'), + height: 36, ), ); } else { - child = _WrapScrollView( - scrollController: widget.scrollController, - contentWidth: GridLayout.headerWidth( - context.read().horizontalPadding, - context.read().state.fields, - ), - child: BlocListener( - listenWhen: (previous, current) => - previous.rowCount != current.rowCount, - listener: (context, state) => _evaluateFloatingCalculations(), - child: ScrollConfiguration( - behavior: - ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: _renderList(context), - ), + children.add( + GridCalculationsRow( + key: const Key('grid_calculations'), + viewId: widget.viewId, ), ); } - if (widget.shrinkWrap) { - return child; - } + children.add(const SizedBox(key: Key('footer_padding'), height: 10)); - return Flexible(child: child); - } - - Widget _shrinkWrapRenderList(BuildContext context) { - final state = context.read().state; - final horizontalPadding = - context.read()?.horizontalPadding ?? - 0.0; - return ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + return Stack( children: [ - widget.shrinkWrap - ? _reorderableListView(state) - : Expanded(child: _reorderableListView(state)), - if (showFloatingCalculations && !widget.shrinkWrap) ...[ - _PositionedCalculationsRow( - viewId: widget.viewId, - isAtBottom: isAtBottom, - ), - ], - ], - ); - } - - Widget _renderList(BuildContext context) { - final state = context.read().state; - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - widget.shrinkWrap - ? _reorderableListView(state) - : Expanded(child: _reorderableListView(state)), - if (showFloatingCalculations && !widget.shrinkWrap) ...[ - _PositionedCalculationsRow( - viewId: widget.viewId, - isAtBottom: isAtBottom, - ), - ], - ], - ); - } - - Widget _reorderableListView(GridState state) { - final List footer = [ - const GridRowBottomBar(), - if (widget.shrinkWrap && state.visibleRows < state.rowInfos.length) - const GridRowLoadMoreButton(), - if (!showFloatingCalculations) GridCalculationsRow(viewId: widget.viewId), - ]; - - // If we are using shrinkWrap, we need to show at most - // state.visibleRows + 1 items. The visibleRows can be larger - // than the actual rowInfos length. - final itemCount = widget.shrinkWrap - ? (state.visibleRows + 1).clamp(0, state.rowInfos.length + 1) - : state.rowInfos.length + 1; - - return ReorderableListView.builder( - cacheExtent: 500, - scrollController: widget.scrollController.verticalController, - physics: const ClampingScrollPhysics(), - buildDefaultDragHandles: false, - shrinkWrap: widget.shrinkWrap, - proxyDecorator: (child, _, __) => Provider.value( - value: context.read(), - child: Material( - color: Colors.white.withValues(alpha: .1), - child: Opacity(opacity: .5, child: child), - ), - ), - onReorder: (fromIndex, newIndex) { - final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; - - if (state.sorts.isNotEmpty) { - showCancelAndDeleteDialog( - context: context, - title: LocaleKeys.grid_sort_sortsActive.tr( - namedArgs: { - 'intention': LocaleKeys.grid_row_reorderRowDescription.tr(), - }, + Positioned.fill( + child: ReorderableListView.builder( + /// This is a workaround related to + /// https://github.com/flutter/flutter/issues/25652 + cacheExtent: 5000, + scrollController: widget.scrollController.verticalController, + physics: const ClampingScrollPhysics(), + buildDefaultDragHandles: false, + proxyDecorator: (child, index, animation) => Material( + color: Colors.white.withOpacity(.1), + child: Opacity(opacity: .5, child: child), ), - description: LocaleKeys.grid_sort_removeSorting.tr(), - confirmLabel: LocaleKeys.button_remove.tr(), - closeOnAction: true, - onDelete: () { - SortBackendService(viewId: widget.viewId).deleteAllSorts(); - moveRow(fromIndex, toIndex); + onReorder: (fromIndex, newIndex) { + final toIndex = newIndex > fromIndex ? newIndex - 1 : newIndex; + if (fromIndex != toIndex) { + context + .read() + .add(GridEvent.moveRow(fromIndex, toIndex)); + } }, - ); - } else { - moveRow(fromIndex, toIndex); - } - }, - itemCount: itemCount, - itemBuilder: (context, index) { - if (index == itemCount - 1) { - final child = Column( - key: const Key('grid_footer'), - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: footer, - ); - - if (context.read().state.isLocked) { - return IgnorePointer( - key: const Key('grid_footer'), - child: child, - ); - } - - return child; - } - - return _renderRow( - context, - state.rowInfos[index].rowId, - index: index, - ); - }, + itemCount: children.length, + itemBuilder: (context, index) => children[index], + ), + ), + if (showFloatingCalculations) ...[ + _PositionedCalculationsRow(viewId: widget.viewId), + ], + ], ); } Widget _renderRow( BuildContext context, RowId rowId, { - required int index, + int? index, + required bool isDraggable, Animation? animation, }) { final databaseController = context.read().databaseController; @@ -594,43 +410,33 @@ class _GridRowsState extends State<_GridRows> { Log.warn('RowMeta is null for rowId: $rowId'); return const SizedBox.shrink(); } + final rowController = RowController( + viewId: viewId, + rowMeta: rowMeta, + rowCache: rowCache, + ); final child = GridRow( - key: ValueKey("grid_row_$rowId"), - shrinkWrap: widget.shrinkWrap, + key: ValueKey(rowMeta.id), fieldController: databaseController.fieldController, rowId: rowId, viewId: viewId, index: index, - editable: !context.watch().state.isLocked, - rowController: RowController( - viewId: viewId, - rowMeta: rowMeta, - rowCache: rowCache, - ), + isDraggable: isDraggable, + rowController: rowController, cellBuilder: EditableCellBuilder(databaseController: databaseController), - openDetailPage: (rowDetailContext) => FlowyOverlay.show( - context: rowDetailContext, - builder: (_) { - final rowMeta = rowCache.getRow(rowId)?.rowMeta; - if (rowMeta == null) { - return const SizedBox.shrink(); - } - - return BlocProvider.value( - value: context.read(), + openDetailPage: (rowDetailContext) { + FlowyOverlay.show( + context: rowDetailContext, + builder: (_) => BlocProvider.value( + value: context.read(), child: RowDetailPage( - rowController: RowController( - viewId: viewId, - rowMeta: rowMeta, - rowCache: rowCache, - ), + rowController: rowController, databaseController: databaseController, - userProfile: context.read().userProfile, ), - ); - }, - ), + ), + ); + }, ); if (animation != null) { @@ -639,12 +445,6 @@ class _GridRowsState extends State<_GridRows> { return child; } - - void moveRow(int from, int to) { - if (from != to) { - context.read().add(GridEvent.moveRow(from, to)); - } - } } class _WrapScrollView extends StatelessWidget { @@ -686,16 +486,10 @@ class _WrapScrollView extends StatelessWidget { class _PositionedCalculationsRow extends StatefulWidget { const _PositionedCalculationsRow({ required this.viewId, - this.isAtBottom = false, }); final String viewId; - /// We don't need to show the top border if the scroll offset - /// is at the bottom of the ScrollView. - /// - final bool isAtBottom; - @override State<_PositionedCalculationsRow> createState() => _PositionedCalculationsRowState(); @@ -705,28 +499,27 @@ class _PositionedCalculationsRowState extends State<_PositionedCalculationsRow> { @override Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.only( - left: context.read().horizontalPadding, - ), - padding: const EdgeInsets.only(bottom: 10), - decoration: BoxDecoration( - color: Theme.of(context).canvasColor, - border: widget.isAtBottom - ? null - : Border( - top: BorderSide( - color: AFThemeExtension.of(context).borderColor, - ), - ), - ), - child: SizedBox( - height: 36, - width: double.infinity, - child: GridCalculationsRow( - key: const Key('floating_grid_calculations'), - viewId: widget.viewId, - includeDefaultInsets: false, + return Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding + 40), + padding: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: SizedBox( + height: 36, + width: double.infinity, + child: GridCalculationsRow( + key: const Key('floating_grid_calculations'), + viewId: widget.viewId, + includeDefaultInsets: false, + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart index c7402a17f9..3c5c9a912c 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/layout.dart @@ -1,10 +1,9 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; - import 'sizes.dart'; class GridLayout { - static double headerWidth(double padding, List fields) { + static double headerWidth(List fields) { if (fields.isEmpty) return 0; final fieldsWidth = fields @@ -16,6 +15,9 @@ class GridLayout { .map((fieldInfo) => fieldInfo.width!.toDouble()) .reduce((value, element) => value + element); - return fieldsWidth + padding + GridSize.newPropertyButtonWidth; + return fieldsWidth + + GridSize.horizontalHeaderPadding + + 40 + + GridSize.trailHeaderPadding; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart index 78a8c97dae..c028df7ced 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/layout/sizes.dart @@ -1,50 +1,41 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/widgets.dart'; -import 'package:universal_platform/universal_platform.dart'; class GridSize { static double scale = 1; static double get scrollBarSize => 8 * scale; - - static double get headerHeight => 36 * scale; - - static double get buttonHeight => 38 * scale; - - static double get footerHeight => 36 * scale; - + static double get headerHeight => 40 * scale; + static double get footerHeight => 40 * scale; static double get horizontalHeaderPadding => - UniversalPlatform.isDesktop ? 40 * scale : 16 * scale; - + PlatformExtension.isDesktop ? 40 * scale : 16 * scale; + static double get trailHeaderPadding => 140 * scale; static double get cellHPadding => 10 * scale; - - static double get cellVPadding => 8 * scale; - + static double get cellVPadding => 10 * scale; static double get popoverItemHeight => 26 * scale; - static double get typeOptionSeparatorHeight => 4 * scale; - static double get newPropertyButtonWidth => 140 * scale; - static double get mobileNewPropertyButtonWidth => 200 * scale; - static EdgeInsets get cellContentInsets => EdgeInsets.symmetric( horizontal: GridSize.cellHPadding, vertical: GridSize.cellVPadding, ); - static EdgeInsets get compactCellContentInsets => - cellContentInsets - EdgeInsets.symmetric(vertical: 2); + static EdgeInsets get fieldContentInsets => EdgeInsets.symmetric( + horizontal: GridSize.cellHPadding, + vertical: GridSize.cellVPadding, + ); static EdgeInsets get typeOptionContentInsets => const EdgeInsets.all(4); static EdgeInsets get toolbarSettingButtonInsets => - const EdgeInsets.symmetric(horizontal: 6, vertical: 2); + const EdgeInsets.symmetric(horizontal: 8, vertical: 2); static EdgeInsets get footerContentInsets => EdgeInsets.fromLTRB( GridSize.horizontalHeaderPadding, 0, - UniversalPlatform.isMobile ? GridSize.horizontalHeaderPadding : 0, - UniversalPlatform.isMobile ? 100 : 0, + PlatformExtension.isMobile ? GridSize.horizontalHeaderPadding : 0, + PlatformExtension.isMobile ? 100 : 0, ); static EdgeInsets get contentInsets => EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart index 17e4c0ed1d..80952f30f2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.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/mobile_card_detail_screen.dart'; @@ -5,11 +7,10 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/shortcuts.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -17,7 +18,7 @@ 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:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; @@ -42,7 +43,6 @@ class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { view: view, databaseController: controller, initialRowId: initialRowId, - shrinkWrap: shrinkWrap, ); } @@ -69,14 +69,12 @@ class MobileGridPage extends StatefulWidget { required this.databaseController, this.onDeleted, this.initialRowId, - this.shrinkWrap = false, }); final ViewPB view; final DatabaseController databaseController; final VoidCallback? onDeleted; final String? initialRowId; - final bool shrinkWrap; @override State createState() => _MobileGridPageState(); @@ -107,14 +105,10 @@ class _MobileGridPageState extends State { finish: (result) { _openRow(context, widget.initialRowId, true); return result.successOrFail.fold( - (_) => GridPageContent( - view: widget.view, - shrinkWrap: widget.shrinkWrap, - ), - (err) => Center( - child: AppFlowyErrorPage( - error: err, - ), + (_) => GridShortcuts(child: GridPageContent(view: widget.view)), + (err) => FlowyErrorPage.message( + err.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), ); }, @@ -151,11 +145,9 @@ class GridPageContent extends StatefulWidget { const GridPageContent({ super.key, required this.view, - this.shrinkWrap = false, }); final ViewPB view; - final bool shrinkWrap; @override State createState() => _GridPageContentState(); @@ -183,8 +175,6 @@ class _GridPageContentState extends State { @override Widget build(BuildContext context) { - final isLocked = - context.read()?.state.isLocked ?? false; return BlocListener( listenWhen: (previous, current) => previous.createdRow != current.createdRow, @@ -206,7 +196,6 @@ class _GridPageContentState extends State { children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, children: [ _GridHeader( contentScrollController: contentScrollController, @@ -218,12 +207,11 @@ class _GridPageContentState extends State { ), ], ), - if (!widget.shrinkWrap && !isLocked) - Positioned( - bottom: 16, - right: 16, - child: getGridFabs(context), - ), + Positioned( + bottom: 16, + right: 16, + child: getGridFabs(context), + ), ], ), ); @@ -268,7 +256,7 @@ class _GridRows extends StatelessWidget { buildWhen: (previous, current) => previous.fields != current.fields, builder: (context, state) { final double contentWidth = getMobileGridContentWidth(state.fields); - return Flexible( + return Expanded( child: _WrapScrollView( scrollController: scrollController, contentWidth: contentWidth, @@ -317,7 +305,6 @@ class _GridRows extends StatelessWidget { return ReorderableListView.builder( scrollController: scrollController.verticalController, buildDefaultDragHandles: false, - shrinkWrap: true, proxyDecorator: (child, index, animation) => Material( color: Colors.transparent, child: child, @@ -359,7 +346,7 @@ class _GridRows extends StatelessWidget { final databaseController = context.read().databaseController; - Widget child = MobileGridRow( + final child = MobileGridRow( key: ValueKey(rowMeta.id), rowId: rowId, isDraggable: isDraggable, @@ -376,20 +363,12 @@ class _GridRows extends StatelessWidget { ); if (animation != null) { - child = SizeTransition( + return SizeTransition( sizeFactor: animation, child: child, ); } - final isLocked = - context.read()?.state.isLocked ?? false; - if (isLocked) { - child = IgnorePointer( - child: child, - ); - } - return child; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart index 5ea364bc72..c8199ce135 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; @@ -10,9 +12,10 @@ import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CalculateCell extends StatefulWidget { @@ -138,14 +141,11 @@ class _CalculateCellState extends State { TextSpan( text: widget.calculation!.calculationType.shortLabel .toUpperCase(), - style: context.tooltipTextStyle(), ), const TextSpan(text: ' '), TextSpan( text: calculateValue, - style: context - .tooltipTextStyle() - ?.copyWith(fontWeight: FontWeight.w500), + style: const TextStyle(fontWeight: FontWeight.w500), ), ], ), @@ -166,7 +166,6 @@ class _CalculateCellState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FlowyText( - lineHeight: 1.0, widget.calculation!.calculationType.shortLabel .toUpperCase(), color: Theme.of(context).hintColor, @@ -175,7 +174,6 @@ class _CalculateCellState extends State { if (widget.calculation!.value.isNotEmpty) ...[ const HSpace(8), FlowyText( - lineHeight: 1.0, calculateValue, color: AFThemeExtension.of(context).textColor, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart index b1b696d790..eb1a76fe18 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart @@ -22,11 +22,7 @@ class CalculationTypeItem extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - type.label, - overflow: TextOverflow.ellipsis, - lineHeight: 1.0, - ), + text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis), onTap: () { onTap(); PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart index 5524633a46..7899d5f56d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GridCalculationsRow extends StatelessWidget { @@ -26,12 +27,9 @@ class GridCalculationsRow extends StatelessWidget { )..add(const CalculationsEvent.started()), child: BlocBuilder( builder: (context, state) { - final padding = - context.read().horizontalPadding; return Padding( - padding: includeDefaultInsets - ? EdgeInsets.symmetric(horizontal: padding) - : EdgeInsets.zero, + padding: + includeDefaultInsets ? GridSize.contentInsets : EdgeInsets.zero, child: Row( children: [ ...state.fields.map( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart index 982ee992b6..369d27133e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart @@ -20,7 +20,7 @@ class RemoveCalculationButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( + text: FlowyText.medium( LocaleKeys.grid_calculationTypeLabel_none.tr(), overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart index bda8634cdb..7341f6046d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart @@ -1,60 +1,79 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; + import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../condition_button.dart'; import '../disclosure_button.dart'; - +import '../filter_info.dart'; import 'choicechip.dart'; -class CheckboxFilterChoicechip extends StatelessWidget { - const CheckboxFilterChoicechip({ - super.key, - required this.filterId, - }); +class CheckboxFilterChoicechip extends StatefulWidget { + const CheckboxFilterChoicechip({required this.filterInfo, super.key}); - final String filterId; + final FilterInfo filterInfo; + + @override + State createState() => + _CheckboxFilterChoicechipState(); +} + +class _CheckboxFilterChoicechipState extends State { + late CheckboxFilterEditorBloc bloc; + + @override + void initState() { + bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo) + ..add(const CheckboxFilterEditorEvent.initial()); + super.initState(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(200, 76)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: CheckboxFilterEditor( - filterId: filterId, - ), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.condition.filterName, + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (blocContext, state) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(200, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return CheckboxFilterEditor(bloc: bloc); + }, + child: ChoiceChipButton( + filterInfo: widget.filterInfo, + filterDesc: _makeFilterDesc(state), + ), ); }, ), ); } + + String _makeFilterDesc(CheckboxFilterEditorState state) { + final prefix = LocaleKeys.grid_checkboxFilter_choicechipPrefix_is.tr(); + return "$prefix ${state.filter.condition.filterName}"; + } } class CheckboxFilterEditor extends StatefulWidget { - const CheckboxFilterEditor({ - super.key, - required this.filterId, - }); + const CheckboxFilterEditor({required this.bloc, super.key}); - final String filterId; + final CheckboxFilterEditorBloc bloc; @override State createState() => _CheckboxFilterEditorState(); @@ -64,57 +83,62 @@ class _CheckboxFilterEditorState extends State { final popoverMutex = PopoverMutex(); @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.bloc, + child: BlocBuilder( + builder: (context, state) { + final List children = [ + _buildFilterPanel(context, state), + ]; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: IntrinsicHeight(child: Column(children: children)), + ); + }, + ), + ); } - @override - Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight( - child: Row( - children: [ - Expanded( - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - CheckboxFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - ), - DisclosureButton( - popoverMutex: popoverMutex, - onAction: (action) { - switch (action) { - case FilterDisclosureAction.delete: - context.read().add( - FilterEditorEvent.deleteFilter( - filter.filterId, - ), - ); - break; - } - }, - ), - ], + Widget _buildFilterPanel( + BuildContext context, + CheckboxFilterEditorState state, + ) { + return SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: FlowyText( + state.filterInfo.fieldInfo.field.name, + overflow: TextOverflow.ellipsis, ), ), - ); - }, + const HSpace(4), + CheckboxFilterConditionList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(CheckboxFilterEditorEvent.updateCondition(condition)); + }, + ), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const CheckboxFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), ); } } @@ -122,17 +146,18 @@ class _CheckboxFilterEditorState extends State { class CheckboxFilterConditionList extends StatelessWidget { const CheckboxFilterConditionList({ super.key, - required this.filter, + required this.filterInfo, required this.popoverMutex, required this.onCondition, }); - final CheckboxFilter filter; + final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final void Function(CheckboxFilterConditionPB) onCondition; + final Function(CheckboxFilterConditionPB) onCondition; @override Widget build(BuildContext context) { + final checkboxFilter = filterInfo.checkboxFilter()!; return PopoverActionList( asBarrier: true, mutex: popoverMutex, @@ -141,17 +166,17 @@ class CheckboxFilterConditionList extends StatelessWidget { .map( (action) => ConditionWrapper( action, - filter.condition == action, + checkboxFilter.condition == action, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filter.conditionName, + conditionName: checkboxFilter.condition.filterName, onTap: () => controller.show(), ); }, - onSelected: (action, controller) { + onSelected: (action, controller) async { onCondition(action.inner); controller.close(); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart deleted file mode 100644 index 9dd302ecf7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist.dart +++ /dev/null @@ -1,170 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../condition_button.dart'; -import '../disclosure_button.dart'; -import 'choicechip.dart'; - -class ChecklistFilterChoicechip extends StatelessWidget { - const ChecklistFilterChoicechip({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: PopoverController(), - constraints: BoxConstraints.loose(const Size(200, 160)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: ChecklistFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), - ); - }, - ), - ); - } -} - -class ChecklistFilterEditor extends StatefulWidget { - const ChecklistFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - ChecklistState createState() => ChecklistState(); -} - -class ChecklistState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - ChecklistFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - ), - DisclosureButton( - popoverMutex: popoverMutex, - onAction: (action) { - switch (action) { - case FilterDisclosureAction.delete: - context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)); - break; - } - }, - ), - ], - ), - ); - }, - ); - } -} - -class ChecklistFilterConditionList extends StatelessWidget { - const ChecklistFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final ChecklistFilter filter; - final PopoverMutex popoverMutex; - final void Function(ChecklistFilterConditionPB) onCondition; - - @override - Widget build(BuildContext context) { - return PopoverActionList( - asBarrier: true, - direction: PopoverDirection.bottomWithCenterAligned, - mutex: popoverMutex, - actions: ChecklistFilterConditionPB.values - .map((action) => ConditionWrapper(action)) - .toList(), - buildChild: (controller) { - return ConditionButton( - conditionName: filter.condition.filterName, - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) { - onCondition(action.inner); - controller.close(); - }, - ); - } -} - -class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner); - - final ChecklistFilterConditionPB inner; - - @override - String get name => inner.filterName; -} - -extension ChecklistFilterConditionPBExtension on ChecklistFilterConditionPB { - String get filterName { - switch (this) { - case ChecklistFilterConditionPB.IsComplete: - return LocaleKeys.grid_checklistFilter_isComplete.tr(); - case ChecklistFilterConditionPB.IsIncomplete: - return LocaleKeys.grid_checklistFilter_isIncomplted.tr(); - default: - return ""; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart new file mode 100644 index 0000000000..c16df32306 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart @@ -0,0 +1,179 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/checklist_filter_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../condition_button.dart'; +import '../../disclosure_button.dart'; +import '../../filter_info.dart'; +import '../choicechip.dart'; + +class ChecklistFilterChoicechip extends StatefulWidget { + const ChecklistFilterChoicechip({required this.filterInfo, super.key}); + + final FilterInfo filterInfo; + + @override + State createState() => + _ChecklistFilterChoicechipState(); +} + +class _ChecklistFilterChoicechipState extends State { + late ChecklistFilterEditorBloc bloc; + late PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo); + bloc.add(const ChecklistFilterEditorEvent.initial()); + super.initState(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (blocContext, state) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(200, 160)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return ChecklistFilterEditor( + bloc: bloc, + popoverMutex: popoverMutex, + ); + }, + child: ChoiceChipButton( + filterInfo: widget.filterInfo, + filterDesc: state.filterDesc, + ), + ); + }, + ), + ); + } +} + +class ChecklistFilterEditor extends StatefulWidget { + const ChecklistFilterEditor({ + super.key, + required this.bloc, + required this.popoverMutex, + }); + + final ChecklistFilterEditorBloc bloc; + final PopoverMutex popoverMutex; + + @override + ChecklistState createState() => ChecklistState(); +} + +class ChecklistState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: widget.bloc, + child: BlocBuilder( + builder: (context, state) { + return SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: FlowyText( + state.filterInfo.fieldInfo.field.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + ChecklistFilterConditionList( + filterInfo: state.filterInfo, + ), + DisclosureButton( + popoverMutex: widget.popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const ChecklistFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + }, + ), + ); + } +} + +class ChecklistFilterConditionList extends StatelessWidget { + const ChecklistFilterConditionList({ + super.key, + required this.filterInfo, + }); + + final FilterInfo filterInfo; + + @override + Widget build(BuildContext context) { + final checklistFilter = filterInfo.checklistFilter()!; + return PopoverActionList( + asBarrier: true, + direction: PopoverDirection.bottomWithCenterAligned, + actions: ChecklistFilterConditionPB.values + .map((action) => ConditionWrapper(action)) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: checklistFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + context + .read() + .add(ChecklistFilterEditorEvent.updateCondition(action.inner)); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + ConditionWrapper(this.inner); + + final ChecklistFilterConditionPB inner; + + @override + String get name => inner.filterName; +} + +extension ChecklistFilterConditionPBExtension on ChecklistFilterConditionPB { + String get filterName { + switch (this) { + case ChecklistFilterConditionPB.IsComplete: + return LocaleKeys.grid_checklistFilter_isComplete.tr(); + case ChecklistFilterConditionPB.IsIncomplete: + return LocaleKeys.grid_checklistFilter_isIncomplted.tr(); + default: + return ""; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart index 99804ad69b..2e080e8e68 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart @@ -1,58 +1,54 @@ -import 'dart:math' as math; - import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:collection/collection.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:flowy_infra/theme_extension.dart'; + import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'dart:math' as math; + +import '../filter_info.dart'; class ChoiceChipButton extends StatelessWidget { const ChoiceChipButton({ super.key, - required this.fieldInfo, + required this.filterInfo, this.filterDesc = '', this.onTap, }); - final FieldInfo fieldInfo; + final FilterInfo filterInfo; final String filterDesc; final VoidCallback? onTap; @override Widget build(BuildContext context) { - final buttonText = - filterDesc.isEmpty ? fieldInfo.name : "${fieldInfo.name}: $filterDesc"; + final borderSide = BorderSide( + color: AFThemeExtension.of(context).toggleOffFill, + ); + + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ); return SizedBox( height: 28, child: FlowyButton( - decoration: BoxDecoration( - color: Colors.transparent, - border: Border.fromBorderSide( - BorderSide( - color: AFThemeExtension.of(context).toggleOffFill, - ), - ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), + decoration: decoration, useIntrinsicWidth: true, text: FlowyText( - buttonText, - lineHeight: 1.0, + filterInfo.fieldInfo.field.name, color: AFThemeExtension.of(context).textColor, ), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), radius: const BorderRadius.all(Radius.circular(14)), - leftIcon: FieldIcon( - fieldInfo: fieldInfo, + leftIcon: FlowySvg( + filterInfo.fieldInfo.fieldType.svgData, + color: Theme.of(context).iconTheme.color, ), - rightIcon: const _ChoicechipDownArrow(), + rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc), hoverColor: AFThemeExtension.of(context).lightGreyHover, onTap: onTap, ), @@ -60,54 +56,28 @@ class ChoiceChipButton extends StatelessWidget { } } -class _ChoicechipDownArrow extends StatelessWidget { - const _ChoicechipDownArrow(); +class _ChoicechipFilterDesc extends StatelessWidget { + const _ChoicechipFilterDesc({this.filterDesc = ''}); + + final String filterDesc; @override Widget build(BuildContext context) { - return Transform.rotate( + final arrow = Transform.rotate( angle: -math.pi / 2, child: FlowySvg( FlowySvgs.arrow_left_s, color: AFThemeExtension.of(context).textColor, ), ); - } -} - -class SingleFilterBlocSelector - extends StatelessWidget { - const SingleFilterBlocSelector({ - super.key, - required this.filterId, - required this.builder, - }); - - final String filterId; - final Widget Function(BuildContext, T, FieldInfo) builder; - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) { - final filter = state.filters - .firstWhereOrNull((filter) => filter.filterId == filterId) as T?; - if (filter == null) { - return null; - } - final field = state.fields - .firstWhereOrNull((field) => field.id == filter.fieldId); - if (field == null) { - return null; - } - return (filter, field); - }, - builder: (context, selection) { - if (selection == null) { - return const SizedBox.shrink(); - } - return builder(context, selection.$1, selection.$2); - }, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Row( + children: [ + if (filterDesc.isNotEmpty) FlowyText(': $filterDesc'), + arrow, + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart index a2d61cc5f4..3c97aaddb2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart @@ -1,420 +1,15 @@ -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../condition_button.dart'; -import '../disclosure_button.dart'; +import '../filter_info.dart'; import 'choicechip.dart'; class DateFilterChoicechip extends StatelessWidget { - const DateFilterChoicechip({ - super.key, - required this.filterId, - }); + const DateFilterChoicechip({required this.filterInfo, super.key}); - final String filterId; + final FilterInfo filterInfo; @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(275, 120)), - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: DateFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), - ); - }, - ), - ); - } -} - -class DateFilterEditor extends StatefulWidget { - const DateFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; - - @override - State createState() => _DateFilterEditorState(); -} - -class _DateFilterEditorState extends State { - final popoverMutex = PopoverMutex(); - final popooverController = PopoverController(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { - final List children = [ - _buildFilterPanel(filter, field), - if (![ - DateFilterConditionPB.DateStartIsEmpty, - DateFilterConditionPB.DateStartIsNotEmpty, - DateFilterConditionPB.DateEndIsEmpty, - DateFilterConditionPB.DateStartIsNotEmpty, - ].contains(filter.condition)) ...[ - const VSpace(4), - _buildFilterContentField(filter), - ], - ]; - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight(child: Column(children: children)), - ); - }, - ); - } - - Widget _buildFilterPanel( - DateTimeFilter filter, - FieldInfo field, - ) { - return SizedBox( - height: 20, - child: Row( - children: [ - Expanded( - child: field.fieldType == FieldType.DateTime - ? DateFilterIsStartList( - filter: filter, - popoverMutex: popoverMutex, - onChangeIsStart: (isStart) { - final newFilter = filter.copyWithCondition( - isStart: isStart, - condition: filter.condition.toCondition(), - ); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - ) - : FlowyText( - field.name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(4), - Expanded( - child: DateFilterConditionList( - filter: filter, - popoverMutex: popoverMutex, - onCondition: (condition) { - final newFilter = filter.copyWithCondition( - isStart: filter.condition.isStart, - condition: condition, - ); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - ), - ), - const HSpace(4), - DisclosureButton( - popoverMutex: popoverMutex, - onAction: (action) { - switch (action) { - case FilterDisclosureAction.delete: - context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)); - break; - } - }, - ), - ], - ), - ); - } - - Widget _buildFilterContentField(DateTimeFilter filter) { - final isRange = filter.condition.isRange; - String? text; - - if (isRange) { - text = - "${filter.start?.defaultFormat ?? ""} - ${filter.end?.defaultFormat ?? ""}"; - text = text == " - " ? null : text; - } else { - text = filter.timestamp.defaultFormat; - } - - return AppFlowyPopover( - controller: popooverController, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(260, 620)), - offset: const Offset(0, 4), - margin: EdgeInsets.zero, - mutex: popoverMutex, - child: FlowyButton( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), - ), - borderRadius: Corners.s6Border, - ), - onTap: popooverController.show, - text: FlowyText( - text ?? "", - overflow: TextOverflow.ellipsis, - ), - ), - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { - return DesktopAppFlowyDatePicker( - isRange: isRange, - includeTime: false, - dateFormat: DateFormatPB.Friendly, - timeFormat: TimeFormatPB.TwentyFourHour, - dateTime: isRange ? filter.start : filter.timestamp, - endDateTime: isRange ? filter.end : null, - onDaySelected: (selectedDay) { - final newFilter = isRange - ? filter.copyWithRange(start: selectedDay, end: null) - : filter.copyWithTimestamp(timestamp: selectedDay); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - if (isRange) { - popooverController.close(); - } - }, - onRangeSelected: (start, end) { - final newFilter = filter.copyWithRange( - start: start, - end: end, - ); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - }, - ); - }, - ), - ); - }, - ); - } -} - -class DateFilterIsStartList extends StatelessWidget { - const DateFilterIsStartList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onChangeIsStart, - }); - - final DateTimeFilter filter; - final PopoverMutex popoverMutex; - final Function(bool isStart) onChangeIsStart; - - @override - Widget build(BuildContext context) { - return PopoverActionList<_IsStartWrapper>( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: [ - _IsStartWrapper( - true, - filter.condition.isStart, - ), - _IsStartWrapper( - false, - !filter.condition.isStart, - ), - ], - buildChild: (controller) { - return ConditionButton( - conditionName: filter.condition.isStart - ? LocaleKeys.grid_dateFilter_startDate.tr() - : LocaleKeys.grid_dateFilter_endDate.tr(), - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) { - onChangeIsStart(action.inner); - controller.close(); - }, - ); - } -} - -class _IsStartWrapper extends ActionCell { - _IsStartWrapper(this.inner, this.isSelected); - - final bool inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) => - isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - @override - String get name => inner - ? LocaleKeys.grid_dateFilter_startDate.tr() - : LocaleKeys.grid_dateFilter_endDate.tr(); -} - -class DateFilterConditionList extends StatelessWidget { - const DateFilterConditionList({ - super.key, - required this.filter, - required this.popoverMutex, - required this.onCondition, - }); - - final DateTimeFilter filter; - final PopoverMutex popoverMutex; - final Function(DateTimeFilterCondition) onCondition; - - @override - Widget build(BuildContext context) { - final conditions = DateTimeFilterCondition.availableConditionsForFieldType( - filter.fieldType, - ); - return PopoverActionList( - asBarrier: true, - mutex: popoverMutex, - direction: PopoverDirection.bottomWithCenterAligned, - actions: conditions - .map( - (action) => ConditionWrapper( - action, - filter.condition.toCondition() == action, - ), - ) - .toList(), - buildChild: (controller) { - return ConditionButton( - conditionName: filter.condition.toCondition().filterName, - onTap: () => controller.show(), - ); - }, - onSelected: (action, controller) { - onCondition(action.inner); - controller.close(); - }, - ); - } -} - -class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner, this.isSelected); - - final DateTimeFilterCondition inner; - final bool isSelected; - - @override - Widget? rightIcon(Color iconColor) => - isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - - @override - String get name => inner.filterName; -} - -extension DateFilterConditionPBExtension on DateFilterConditionPB { - bool get isStart { - return switch (this) { - DateFilterConditionPB.DateStartsOn || - DateFilterConditionPB.DateStartsBefore || - DateFilterConditionPB.DateStartsAfter || - DateFilterConditionPB.DateStartsOnOrBefore || - DateFilterConditionPB.DateStartsOnOrAfter || - DateFilterConditionPB.DateStartsBetween || - DateFilterConditionPB.DateStartIsEmpty || - DateFilterConditionPB.DateStartIsNotEmpty => - true, - _ => false - }; - } - - bool get isRange { - return switch (this) { - DateFilterConditionPB.DateStartsBetween || - DateFilterConditionPB.DateEndsBetween => - true, - _ => false, - }; - } - - DateTimeFilterCondition toCondition() { - return switch (this) { - DateFilterConditionPB.DateStartsOn || - DateFilterConditionPB.DateEndsOn => - DateTimeFilterCondition.on, - DateFilterConditionPB.DateStartsBefore || - DateFilterConditionPB.DateEndsBefore => - DateTimeFilterCondition.before, - DateFilterConditionPB.DateStartsAfter || - DateFilterConditionPB.DateEndsAfter => - DateTimeFilterCondition.after, - DateFilterConditionPB.DateStartsOnOrBefore || - DateFilterConditionPB.DateEndsOnOrBefore => - DateTimeFilterCondition.onOrBefore, - DateFilterConditionPB.DateStartsOnOrAfter || - DateFilterConditionPB.DateEndsOnOrAfter => - DateTimeFilterCondition.onOrAfter, - DateFilterConditionPB.DateStartsBetween || - DateFilterConditionPB.DateEndsBetween => - DateTimeFilterCondition.between, - DateFilterConditionPB.DateStartIsEmpty || - DateFilterConditionPB.DateEndIsEmpty => - DateTimeFilterCondition.isEmpty, - DateFilterConditionPB.DateStartIsNotEmpty || - DateFilterConditionPB.DateEndIsNotEmpty => - DateTimeFilterCondition.isNotEmpty, - _ => throw ArgumentError(), - }; - } -} - -extension DateTimeChoicechipExtension on DateTime { - DateTime get considerLocal { - return DateTime(year, month, day); - } -} - -extension DateTimeDefaultFormatExtension on DateTime? { - String? get defaultFormat { - return this != null ? DateFormat('dd/MM/yyyy').format(this!) : null; + return ChoiceChipButton(filterInfo: filterInfo); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart index cc38b4eaba..0947239273 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart @@ -1,10 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/number_filter_editor_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,34 +11,42 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../condition_button.dart'; import '../disclosure_button.dart'; - +import '../filter_info.dart'; import 'choicechip.dart'; -class NumberFilterChoiceChip extends StatelessWidget { +class NumberFilterChoiceChip extends StatefulWidget { const NumberFilterChoiceChip({ super.key, - required this.filterId, + required this.filterInfo, }); - final String filterId; + final FilterInfo filterInfo; + @override + State createState() => _NumberFilterChoiceChipState(); +} + +class _NumberFilterChoiceChipState extends State { @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(200, 100)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: NumberFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), + return BlocProvider( + create: (_) => NumberFilterEditorBloc( + filterInfo: widget.filterInfo, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 100)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: const NumberFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: state.filterInfo, + ), ); }, ), @@ -48,12 +55,7 @@ class NumberFilterChoiceChip extends StatelessWidget { } class NumberFilterEditor extends StatefulWidget { - const NumberFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; + const NumberFilterEditor({super.key}); @override State createState() => _NumberFilterEditorState(); @@ -62,23 +64,17 @@ class NumberFilterEditor extends StatefulWidget { class _NumberFilterEditorState extends State { final popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { + return BlocBuilder( + builder: (context, state) { final List children = [ - _buildFilterPanel(filter, field), - if (filter.condition != NumberFilterConditionPB.NumberIsEmpty && - filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[ + _buildFilterPanel(context, state), + if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty && + state.filter.condition != + NumberFilterConditionPB.NumberIsNotEmpty) ...[ const VSpace(4), - _buildFilterNumberField(filter), + _buildFilterNumberField(context, state), ], ]; @@ -91,8 +87,8 @@ class _NumberFilterEditorState extends State { } Widget _buildFilterPanel( - NumberFilter filter, - FieldInfo field, + BuildContext context, + NumberFilterEditorState state, ) { return SizedBox( height: 20, @@ -100,20 +96,19 @@ class _NumberFilterEditorState extends State { children: [ Expanded( child: FlowyText( - field.name, + state.filterInfo.fieldInfo.name, overflow: TextOverflow.ellipsis, ), ), const HSpace(4), Expanded( - child: NumberFilterConditionList( - filter: filter, + child: NumberFilterConditionPBList( + filterInfo: state.filterInfo, popoverMutex: popoverMutex, onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(NumberFilterEditorEvent.updateCondition(condition)); }, ), ), @@ -123,11 +118,9 @@ class _NumberFilterEditorState extends State { onAction: (action) { switch (action) { case FilterDisclosureAction.delete: - context.read().add( - FilterEditorEvent.deleteFilter( - filter.filterId, - ), - ); + context + .read() + .add(const NumberFilterEditorEvent.delete()); break; } }, @@ -138,37 +131,38 @@ class _NumberFilterEditorState extends State { } Widget _buildFilterNumberField( - NumberFilter filter, + BuildContext context, + NumberFilterEditorState state, ) { return FlowyTextField( - text: filter.content, + text: state.filter.content, hintText: LocaleKeys.grid_settings_typeAValue.tr(), debounceDuration: const Duration(milliseconds: 300), autoFocus: false, onChanged: (text) { - final newFilter = filter.copyWith(content: text); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(NumberFilterEditorEvent.updateContent(text)); }, ); } } -class NumberFilterConditionList extends StatelessWidget { - const NumberFilterConditionList({ +class NumberFilterConditionPBList extends StatelessWidget { + const NumberFilterConditionPBList({ super.key, - required this.filter, + required this.filterInfo, required this.popoverMutex, required this.onCondition, }); - final NumberFilter filter; + final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final void Function(NumberFilterConditionPB) onCondition; + final Function(NumberFilterConditionPB) onCondition; @override Widget build(BuildContext context) { + final numberFilter = filterInfo.numberFilter()!; return PopoverActionList( asBarrier: true, mutex: popoverMutex, @@ -177,13 +171,13 @@ class NumberFilterConditionList extends StatelessWidget { .map( (action) => ConditionWrapper( action, - filter.condition == action, + numberFilter.condition == action, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filter.condition.filterName, + conditionName: numberFilter.condition.filterName, onTap: () => controller.show(), ); }, @@ -210,22 +204,6 @@ class ConditionWrapper extends ActionCell { } extension NumberFilterConditionPBExtension on NumberFilterConditionPB { - String get shortName { - return switch (this) { - NumberFilterConditionPB.Equal => "=", - NumberFilterConditionPB.NotEqual => "≠", - NumberFilterConditionPB.LessThan => "<", - NumberFilterConditionPB.LessThanOrEqualTo => "≤", - NumberFilterConditionPB.GreaterThan => ">", - NumberFilterConditionPB.GreaterThanOrEqualTo => "≥", - NumberFilterConditionPB.NumberIsEmpty => - LocaleKeys.grid_numberFilter_isEmpty.tr(), - NumberFilterConditionPB.NumberIsNotEmpty => - LocaleKeys.grid_numberFilter_isNotEmpty.tr(), - _ => "", - }; - } - String get filterName { return switch (this) { NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart index a8c8f69016..d33dba6293 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart @@ -1,8 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/database/view/database_filter_condition_list.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/condition_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -12,37 +11,33 @@ import 'package:flutter/widgets.dart'; class SelectOptionFilterConditionList extends StatelessWidget { const SelectOptionFilterConditionList({ super.key, - required this.filter, - required this.fieldType, + required this.filterInfo, required this.popoverMutex, required this.onCondition, }); - final SelectOptionFilter filter; - final FieldType fieldType; + final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final void Function(SelectOptionFilterConditionPB) onCondition; + final Function(SelectOptionFilterConditionPB) onCondition; @override Widget build(BuildContext context) { - final conditions = (fieldType == FieldType.SingleSelect - ? SingleSelectOptionFilterCondition().conditions - : MultiSelectOptionFilterCondition().conditions); + final selectOptionFilter = filterInfo.selectOptionFilter()!; return PopoverActionList( asBarrier: true, mutex: popoverMutex, direction: PopoverDirection.bottomWithCenterAligned, - actions: conditions + actions: _conditionsForFieldType(filterInfo.fieldInfo.fieldType) .map( (action) => ConditionWrapper( - action.$1, - filter.condition == action.$1, + action, + selectOptionFilter.condition == action, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filter.condition.i18n, + conditionName: selectOptionFilter.condition.i18n, onTap: () => controller.show(), ); }, @@ -52,6 +47,29 @@ class SelectOptionFilterConditionList extends StatelessWidget { }, ); } + + List _conditionsForFieldType( + FieldType fieldType, + ) { + // SelectOptionFilterConditionPB.values is not in order + return switch (fieldType) { + FieldType.SingleSelect => [ + SelectOptionFilterConditionPB.OptionIs, + SelectOptionFilterConditionPB.OptionIsNot, + SelectOptionFilterConditionPB.OptionIsEmpty, + SelectOptionFilterConditionPB.OptionIsNotEmpty, + ], + FieldType.MultiSelect => [ + SelectOptionFilterConditionPB.OptionContains, + SelectOptionFilterConditionPB.OptionDoesNotContain, + SelectOptionFilterConditionPB.OptionIs, + SelectOptionFilterConditionPB.OptionIsNot, + SelectOptionFilterConditionPB.OptionIsEmpty, + SelectOptionFilterConditionPB.OptionIsNotEmpty, + ], + _ => [], + }; + } } class ConditionWrapper extends ActionCell { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart index 3e9db2df1b..b3c1482453 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart @@ -1,111 +1,115 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'select_option_loader.dart'; + class SelectOptionFilterList extends StatelessWidget { const SelectOptionFilterList({ super.key, - required this.filter, - required this.field, - required this.options, - required this.onTap, + required this.filterInfo, + required this.selectedOptionIds, + required this.onSelectedOptions, }); - final SelectOptionFilter filter; - final FieldInfo field; - final List options; - final VoidCallback onTap; + final FilterInfo filterInfo; + final List selectedOptionIds; + final Function(List) onSelectedOptions; @override Widget build(BuildContext context) { - return ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: options.length, - separatorBuilder: (context, index) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemBuilder: (context, index) { - final option = options[index]; - final isSelected = filter.optionIds.contains(option.id); - return SelectOptionFilterCell( - option: option, - isSelected: isSelected, - onTap: () => _onTapHandler(context, option, isSelected), - ); + return BlocProvider( + create: (context) { + return SelectOptionFilterListBloc( + selectedOptionIds: selectedOptionIds, + delegate: filterInfo.fieldInfo.fieldType == FieldType.SingleSelect + ? SingleSelectOptionFilterDelegateImpl(filterInfo: filterInfo) + : MultiSelectOptionFilterDelegateImpl(filterInfo: filterInfo), + )..add(const SelectOptionFilterListEvent.initial()); }, - ); - } - - void _onTapHandler( - BuildContext context, - SelectOptionPB option, - bool isSelected, - ) { - final selectedOptionIds = Set.from(filter.optionIds); - if (isSelected) { - selectedOptionIds.remove(option.id); - } else { - selectedOptionIds.add(option.id); - } - _updateSelectOptions(context, filter, selectedOptionIds); - onTap(); - } - - void _updateSelectOptions( - BuildContext context, - SelectOptionFilter filter, - Set selectedOptionIds, - ) { - final optionIds = - options.map((e) => e.id).where(selectedOptionIds.contains).toList(); - final newFilter = filter.copyWith(optionIds: optionIds); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); - } -} - -class SelectOptionFilterCell extends StatelessWidget { - const SelectOptionFilterCell({ - super.key, - required this.option, - required this.isSelected, - required this.onTap, - }); - - final SelectOptionPB option; - final bool isSelected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ), - child: SelectOptionTagCell( - option: option, - onSelected: onTap, - children: [ - if (isSelected) - const Padding( - padding: EdgeInsets.only(right: 6), - child: FlowySvg(FlowySvgs.check_s), - ), - ], - ), + child: + BlocConsumer( + listenWhen: (previous, current) => + previous.selectedOptionIds != current.selectedOptionIds, + listener: (context, state) { + onSelectedOptions(state.selectedOptionIds.toList()); + }, + builder: (context, state) { + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: state.visibleOptions.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemBuilder: (BuildContext context, int index) { + final option = state.visibleOptions[index]; + return SelectOptionFilterCell( + option: option.optionPB, + isSelected: option.isSelected, + ); + }, + ); + }, + ), + ); + } +} + +class SelectOptionFilterCell extends StatefulWidget { + const SelectOptionFilterCell({ + super.key, + required this.option, + required this.isSelected, + }); + + final SelectOptionPB option; + final bool isSelected; + + @override + State createState() => _SelectOptionFilterCellState(); +} + +class _SelectOptionFilterCellState extends State { + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: SelectOptionTagCell( + option: widget.option, + onSelected: () { + if (widget.isSelected) { + context + .read() + .add(SelectOptionFilterListEvent.unSelectOption(widget.option)); + } else { + context.read().add( + SelectOptionFilterListEvent.selectOption( + widget.option, + context + .read() + .state + .filter + .condition, + ), + ); + } + }, + children: [ + if (widget.isSelected) + const Padding( + padding: EdgeInsets.only(right: 6), + child: FlowySvg(FlowySvgs.check_s), + ), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart index 50984bbf3d..24d1955a3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart @@ -1,41 +1,75 @@ -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../disclosure_button.dart'; +import '../../filter_info.dart'; import '../choicechip.dart'; - import 'condition_list.dart'; import 'option_list.dart'; +import 'select_option_loader.dart'; -class SelectOptionFilterChoicechip extends StatelessWidget { - const SelectOptionFilterChoicechip({ - super.key, - required this.filterId, - }); +class SelectOptionFilterChoicechip extends StatefulWidget { + const SelectOptionFilterChoicechip({required this.filterInfo, super.key}); - final String filterId; + final FilterInfo filterInfo; + + @override + State createState() => + _SelectOptionFilterChoicechipState(); +} + +class _SelectOptionFilterChoicechipState + extends State { + late SelectOptionFilterEditorBloc bloc; + + @override + void initState() { + if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { + bloc = SelectOptionFilterEditorBloc( + filterInfo: widget.filterInfo, + delegate: + SingleSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo), + ); + } else { + bloc = SelectOptionFilterEditorBloc( + filterInfo: widget.filterInfo, + delegate: + MultiSelectOptionFilterDelegateImpl(filterInfo: widget.filterInfo), + ); + } + bloc.add(const SelectOptionFilterEditorEvent.initial()); + super.initState(); + } + + @override + void dispose() { + bloc.close(); + super.dispose(); + } @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(240, 160)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: SelectOptionFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), + return BlocProvider.value( + value: bloc, + child: BlocBuilder( + builder: (blocContext, state) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(240, 160)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return SelectOptionFilterEditor(bloc: bloc); + }, + child: ChoiceChipButton( + filterInfo: widget.filterInfo, + filterDesc: state.filterDesc, + ), ); }, ), @@ -44,12 +78,9 @@ class SelectOptionFilterChoicechip extends StatelessWidget { } class SelectOptionFilterEditor extends StatefulWidget { - const SelectOptionFilterEditor({ - super.key, - required this.filterId, - }); + const SelectOptionFilterEditor({required this.bloc, super.key}); - final String filterId; + final SelectOptionFilterEditorBloc bloc; @override State createState() => @@ -59,51 +90,55 @@ class SelectOptionFilterEditor extends StatefulWidget { class _SelectOptionFilterEditorState extends State { final popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { - final List slivers = [ - SliverToBoxAdapter(child: _buildFilterPanel(filter, field)), - ]; + return BlocProvider.value( + value: widget.bloc, + child: BlocBuilder( + builder: (context, state) { + final List slivers = [ + SliverToBoxAdapter(child: _buildFilterPanel(context, state)), + ]; - if (filter.canAttachContent) { - slivers - ..add(const SliverToBoxAdapter(child: VSpace(4))) - ..add( + if (state.filter.condition != + SelectOptionFilterConditionPB.OptionIsEmpty && + state.filter.condition != + SelectOptionFilterConditionPB.OptionIsNotEmpty) { + slivers.add(const SliverToBoxAdapter(child: VSpace(4))); + slivers.add( SliverToBoxAdapter( child: SelectOptionFilterList( - filter: filter, - field: field, - options: filter.makeDelegate(field).getOptions(field), - onTap: () => popoverMutex.close(), + filterInfo: state.filterInfo, + selectedOptionIds: state.filter.optionIds, + onSelectedOptions: (optionIds) { + context.read().add( + SelectOptionFilterEditorEvent.updateContent( + optionIds, + ), + ); + }, ), ), ); - } + } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: CustomScrollView( - shrinkWrap: true, - slivers: slivers, - physics: StyledScrollPhysics(), - ), - ); - }, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: CustomScrollView( + shrinkWrap: true, + slivers: slivers, + physics: StyledScrollPhysics(), + ), + ); + }, + ), ); } Widget _buildFilterPanel( - SelectOptionFilter filter, - FieldInfo field, + BuildContext context, + SelectOptionFilterEditorState state, ) { return SizedBox( height: 20, @@ -111,20 +146,18 @@ class _SelectOptionFilterEditorState extends State { children: [ Expanded( child: FlowyText( - field.field.name, + state.filterInfo.fieldInfo.field.name, overflow: TextOverflow.ellipsis, ), ), const HSpace(4), SelectOptionFilterConditionList( - filter: filter, - fieldType: field.fieldType, + filterInfo: state.filterInfo, popoverMutex: popoverMutex, onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); - context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + context.read().add( + SelectOptionFilterEditorEvent.updateCondition(condition), + ); }, ), DisclosureButton( @@ -133,8 +166,8 @@ class _SelectOptionFilterEditorState extends State { switch (action) { case FilterDisclosureAction.delete: context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)); + .read() + .add(const SelectOptionFilterEditorEvent.delete()); break; } }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart new file mode 100644 index 0000000000..7a7aa25cad --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart @@ -0,0 +1,76 @@ +import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; + +abstract class SelectOptionFilterDelegate { + List loadOptions(); + + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ); +} + +class SingleSelectOptionFilterDelegateImpl + implements SelectOptionFilterDelegate { + SingleSelectOptionFilterDelegateImpl({required this.filterInfo}); + + final FilterInfo filterInfo; + + @override + List loadOptions() { + final parser = SingleSelectTypeOptionDataParser(); + return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options; + } + + @override + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ) { + final selectOptionIds = Set.from(currentOptionIds); + + switch (condition) { + case SelectOptionFilterConditionPB.OptionIs: + if (selectOptionIds.isNotEmpty) { + selectOptionIds.clear(); + } + selectOptionIds.add(optionId); + break; + case SelectOptionFilterConditionPB.OptionIsNot: + selectOptionIds.add(optionId); + break; + case SelectOptionFilterConditionPB.OptionIsEmpty || + SelectOptionFilterConditionPB.OptionIsNotEmpty: + selectOptionIds.clear(); + break; + default: + throw UnimplementedError(); + } + + return selectOptionIds; + } +} + +class MultiSelectOptionFilterDelegateImpl + implements SelectOptionFilterDelegate { + MultiSelectOptionFilterDelegateImpl({required this.filterInfo}); + + final FilterInfo filterInfo; + + @override + List loadOptions() { + final parser = MultiSelectTypeOptionDataParser(); + return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options; + } + + @override + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ) => + Set.from(currentOptionIds)..add(optionId); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart index 548f21efbe..66f17e0971 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart @@ -1,10 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,48 +11,59 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../condition_button.dart'; import '../disclosure_button.dart'; - +import '../filter_info.dart'; import 'choicechip.dart'; class TextFilterChoicechip extends StatelessWidget { - const TextFilterChoicechip({ - super.key, - required this.filterId, - }); + const TextFilterChoicechip({required this.filterInfo, super.key}); - final String filterId; + final FilterInfo filterInfo; @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(200, 76)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: TextFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), + return BlocProvider( + create: (_) => TextFilterEditorBloc( + filterInfo: filterInfo, + fieldType: FieldType.RichText, + )..add(const TextFilterEditorEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: const TextFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: filterInfo, + filterDesc: _makeFilterDesc(state), + ), ); }, ), ); } + + String _makeFilterDesc(TextFilterEditorState state) { + String filterDesc = state.filter.condition.choicechipPrefix; + if (state.filter.condition == TextFilterConditionPB.TextIsEmpty || + state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) { + return filterDesc; + } + + if (state.filter.content.isNotEmpty) { + filterDesc += " ${state.filter.content}"; + } + + return filterDesc; + } } class TextFilterEditor extends StatefulWidget { - const TextFilterEditor({ - super.key, - required this.filterId, - }); - - final String filterId; + const TextFilterEditor({super.key}); @override State createState() => _TextFilterEditorState(); @@ -62,25 +72,18 @@ class TextFilterEditor extends StatefulWidget { class _TextFilterEditorState extends State { final popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { + return BlocBuilder( + builder: (context, state) { final List children = [ - _buildFilterPanel(filter, field), + _buildFilterPanel(context, state), ]; - if (filter.condition != TextFilterConditionPB.TextIsEmpty && - filter.condition != TextFilterConditionPB.TextIsNotEmpty) { + if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && + state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) { children.add(const VSpace(4)); - children.add(_buildFilterTextField(filter, field)); + children.add(_buildFilterTextField(context, state)); } return Padding( @@ -91,27 +94,26 @@ class _TextFilterEditorState extends State { ); } - Widget _buildFilterPanel(TextFilter filter, FieldInfo field) { + Widget _buildFilterPanel(BuildContext context, TextFilterEditorState state) { return SizedBox( height: 20, child: Row( children: [ Expanded( child: FlowyText( - field.name, + state.filterInfo.fieldInfo.name, overflow: TextOverflow.ellipsis, ), ), const HSpace(4), Expanded( - child: TextFilterConditionList( - filter: filter, + child: TextFilterConditionPBList( + filterInfo: state.filterInfo, popoverMutex: popoverMutex, onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(TextFilterEditorEvent.updateCondition(condition)); }, ), ), @@ -122,8 +124,8 @@ class _TextFilterEditorState extends State { switch (action) { case FilterDisclosureAction.delete: context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)); + .read() + .add(const TextFilterEditorEvent.delete()); break; } }, @@ -133,36 +135,39 @@ class _TextFilterEditorState extends State { ); } - Widget _buildFilterTextField(TextFilter filter, FieldInfo field) { + Widget _buildFilterTextField( + BuildContext context, + TextFilterEditorState state, + ) { return FlowyTextField( - text: filter.content, + text: state.filter.content, hintText: LocaleKeys.grid_settings_typeAValue.tr(), debounceDuration: const Duration(milliseconds: 300), autoFocus: false, onChanged: (text) { - final newFilter = filter.copyWith(content: text); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(TextFilterEditorEvent.updateContent(text)); }, ); } } -class TextFilterConditionList extends StatelessWidget { - const TextFilterConditionList({ +class TextFilterConditionPBList extends StatelessWidget { + const TextFilterConditionPBList({ super.key, - required this.filter, + required this.filterInfo, required this.popoverMutex, required this.onCondition, }); - final TextFilter filter; + final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final void Function(TextFilterConditionPB) onCondition; + final Function(TextFilterConditionPB) onCondition; @override Widget build(BuildContext context) { + final textFilter = filterInfo.textFilter()!; return PopoverActionList( asBarrier: true, mutex: popoverMutex, @@ -171,13 +176,13 @@ class TextFilterConditionList extends StatelessWidget { .map( (action) => ConditionWrapper( action, - filter.condition == action, + textFilter.condition == action, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filter.condition.filterName, + conditionName: textFilter.condition.filterName, onTap: () => controller.show(), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart index dcd33f66c3..828f124de1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart @@ -1,10 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/time_filter_editor_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,33 +11,42 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../condition_button.dart'; import '../disclosure_button.dart'; - +import '../filter_info.dart'; import 'choicechip.dart'; -class TimeFilterChoiceChip extends StatelessWidget { +class TimeFilterChoiceChip extends StatefulWidget { const TimeFilterChoiceChip({ super.key, - required this.filterId, + required this.filterInfo, }); - final String filterId; + final FilterInfo filterInfo; + @override + State createState() => _TimeFilterChoiceChipState(); +} + +class _TimeFilterChoiceChipState extends State { @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(200, 100)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: TimeFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, + return BlocProvider( + create: (_) => TimeFilterEditorBloc( + filterInfo: widget.filterInfo, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 100)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: const TimeFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: state.filterInfo, + ), ); }, ), @@ -47,12 +55,8 @@ class TimeFilterChoiceChip extends StatelessWidget { } class TimeFilterEditor extends StatefulWidget { - const TimeFilterEditor({ - super.key, - required this.filterId, - }); + const TimeFilterEditor({super.key}); - final String filterId; @override State createState() => _TimeFilterEditorState(); } @@ -60,23 +64,17 @@ class TimeFilterEditor extends StatefulWidget { class _TimeFilterEditorState extends State { final popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return SingleFilterBlocSelector( - filterId: widget.filterId, - builder: (context, filter, field) { + return BlocBuilder( + builder: (context, state) { final List children = [ - _buildFilterPanel(filter, field), - if (filter.condition != NumberFilterConditionPB.NumberIsEmpty && - filter.condition != NumberFilterConditionPB.NumberIsNotEmpty) ...[ + _buildFilterPanel(context, state), + if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty && + state.filter.condition != + NumberFilterConditionPB.NumberIsNotEmpty) ...[ const VSpace(4), - _buildFilterTimeField(filter, field), + _buildFilterTimeField(context, state), ], ]; @@ -89,8 +87,8 @@ class _TimeFilterEditorState extends State { } Widget _buildFilterPanel( - TimeFilter filter, - FieldInfo field, + BuildContext context, + TimeFilterEditorState state, ) { return SizedBox( height: 20, @@ -98,20 +96,19 @@ class _TimeFilterEditorState extends State { children: [ Expanded( child: FlowyText( - field.name, + state.filterInfo.fieldInfo.name, overflow: TextOverflow.ellipsis, ), ), const HSpace(4), Expanded( - child: TimeFilterConditionList( - filter: filter, + child: TimeFilterConditionPBList( + filterInfo: state.filterInfo, popoverMutex: popoverMutex, onCondition: (condition) { - final newFilter = filter.copyWith(condition: condition); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(TimeFilterEditorEvent.updateCondition(condition)); }, ), ), @@ -122,8 +119,8 @@ class _TimeFilterEditorState extends State { switch (action) { case FilterDisclosureAction.delete: context - .read() - .add(FilterEditorEvent.deleteFilter(filter.filterId)); + .read() + .add(const TimeFilterEditorEvent.delete()); break; } }, @@ -134,38 +131,38 @@ class _TimeFilterEditorState extends State { } Widget _buildFilterTimeField( - TimeFilter filter, - FieldInfo field, + BuildContext context, + TimeFilterEditorState state, ) { return FlowyTextField( - text: filter.content, + text: state.filter.content, hintText: LocaleKeys.grid_settings_typeAValue.tr(), debounceDuration: const Duration(milliseconds: 300), autoFocus: false, onChanged: (text) { - final newFilter = filter.copyWith(content: text); context - .read() - .add(FilterEditorEvent.updateFilter(newFilter)); + .read() + .add(TimeFilterEditorEvent.updateContent(text)); }, ); } } -class TimeFilterConditionList extends StatelessWidget { - const TimeFilterConditionList({ +class TimeFilterConditionPBList extends StatelessWidget { + const TimeFilterConditionPBList({ super.key, - required this.filter, + required this.filterInfo, required this.popoverMutex, required this.onCondition, }); - final TimeFilter filter; + final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final void Function(NumberFilterConditionPB) onCondition; + final Function(NumberFilterConditionPB) onCondition; @override Widget build(BuildContext context) { + final timeFilter = filterInfo.timeFilter()!; return PopoverActionList( asBarrier: true, mutex: popoverMutex, @@ -174,13 +171,13 @@ class TimeFilterConditionList extends StatelessWidget { .map( (action) => ConditionWrapper( action, - filter.condition == action, + timeFilter.condition == action, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filter.condition.filterName, + conditionName: timeFilter.condition.filterName, onTap: () => controller.show(), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart index 7e453b9ab7..53d2b0ace8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart @@ -1,40 +1,58 @@ -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../filter_info.dart'; import 'choicechip.dart'; -class URLFilterChoicechip extends StatelessWidget { - const URLFilterChoicechip({ - super.key, - required this.filterId, - }); +class URLFilterChoiceChip extends StatelessWidget { + const URLFilterChoiceChip({required this.filterInfo, super.key}); - final String filterId; + final FilterInfo filterInfo; @override Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(200, 76)), - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: TextFilterEditor(filterId: filterId), - ); - }, - child: SingleFilterBlocSelector( - filterId: filterId, - builder: (context, filter, field) { - return ChoiceChipButton( - fieldInfo: field, - filterDesc: filter.getContentDescription(field), + return BlocProvider( + create: (_) => TextFilterEditorBloc( + filterInfo: filterInfo, + fieldType: FieldType.URL, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: const TextFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: filterInfo, + filterDesc: _makeFilterDesc(state), + ), ); }, ), ); } + + String _makeFilterDesc(TextFilterEditorState state) { + String filterDesc = state.filter.condition.choicechipPrefix; + if (state.filter.condition == TextFilterConditionPB.TextIsEmpty || + state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) { + return filterDesc; + } + + if (state.filter.content.isNotEmpty) { + filterDesc += " ${state.filter.content}"; + } + + return filterDesc; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart index 2be7810546..736fdee63a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart @@ -1,6 +1,5 @@ import 'dart:math' as math; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -32,14 +31,13 @@ class ConditionButton extends StatelessWidget { child: FlowyButton( useIntrinsicWidth: true, text: FlowyText( - lineHeight: 1.0, conditionName, fontSize: 10, color: AFThemeExtension.of(context).textColor, overflow: TextOverflow.ellipsis, ), margin: const EdgeInsets.symmetric(horizontal: 4), - radius: Corners.s6Border, + radius: const BorderRadius.all(Radius.circular(2)), rightIcon: arrow, hoverColor: AFThemeExtension.of(context).lightGreyHover, onTap: onTap, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart index 9cf2cd8322..b91fb44389 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart @@ -1,9 +1,8 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/application/simple_text_filter_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -14,43 +13,57 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class CreateDatabaseViewFilterList extends StatelessWidget { - const CreateDatabaseViewFilterList({ +import '../../../../application/field/field_controller.dart'; +import '../../../application/filter/filter_create_bloc.dart'; + +class GridCreateFilterList extends StatefulWidget { + const GridCreateFilterList({ super.key, - this.onTap, + required this.viewId, + required this.fieldController, + required this.onClosed, + this.onCreateFilter, }); - final VoidCallback? onTap; + final String viewId; + final FieldController fieldController; + final VoidCallback onClosed; + final VoidCallback? onCreateFilter; + + @override + State createState() => _GridCreateFilterListState(); +} + +class _GridCreateFilterListState extends State { + late GridCreateFilterBloc editBloc; + + @override + void initState() { + editBloc = GridCreateFilterBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, + )..add(const GridCreateFilterEvent.initial()); + super.initState(); + } @override Widget build(BuildContext context) { - final filterBloc = context.read(); - return BlocProvider( - create: (_) => SimpleTextFilterBloc( - values: List.from(filterBloc.state.fields), - comparator: (val) => val.name, - ), - child: BlocListener( - listenWhen: (previous, current) => previous.fields != current.fields, + return BlocProvider.value( + value: editBloc, + child: BlocListener( listener: (context, state) { - context - .read>() - .add(SimpleTextFilterEvent.receiveNewValues(state.fields)); + if (state.didCreateFilter) { + widget.onClosed(); + } }, - child: BlocBuilder, - SimpleTextFilterState>( + child: BlocBuilder( builder: (context, state) { - final cells = state.values.map((fieldInfo) { + final cells = state.creatableFields.map((fieldInfo) { return SizedBox( height: GridSize.popoverItemHeight, - child: FilterableFieldButton( + child: GridFilterPropertyCell( fieldInfo: fieldInfo, - onTap: () { - context - .read() - .add(FilterEditorEvent.createFilter(fieldInfo)); - onTap?.call(); - }, + onTap: (fieldInfo) => createFilter(fieldInfo), ), ); }).toList(); @@ -64,9 +77,12 @@ class CreateDatabaseViewFilterList extends StatelessWidget { child: ListView.separated( shrinkWrap: true, itemCount: cells.length, - itemBuilder: (_, int index) => cells[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), + itemBuilder: (BuildContext context, int index) { + return cells[index]; + }, + separatorBuilder: (BuildContext context, int index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, ), ), ]; @@ -80,6 +96,17 @@ class CreateDatabaseViewFilterList extends StatelessWidget { ), ); } + + @override + void dispose() { + editBloc.close(); + super.dispose(); + } + + void createFilter(FieldInfo field) { + editBloc.add(GridCreateFilterEvent.createDefaultFilter(field)); + widget.onCreateFilter?.call(); + } } class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { @@ -101,8 +128,8 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { hintText: LocaleKeys.grid_settings_filterBy.tr(), onChanged: (text) { context - .read>() - .add(SimpleTextFilterEvent.updateFilter(text)); + .read() + .add(GridCreateFilterEvent.didReceiveFilterText(text)); }, ), ); @@ -120,28 +147,28 @@ class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { } } -class FilterableFieldButton extends StatelessWidget { - const FilterableFieldButton({ +class GridFilterPropertyCell extends StatelessWidget { + const GridFilterPropertyCell({ super.key, required this.fieldInfo, required this.onTap, }); final FieldInfo fieldInfo; - final VoidCallback onTap; + final Function(FieldInfo) onTap; @override Widget build(BuildContext context) { return FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( fieldInfo.field.name, color: AFThemeExtension.of(context).textColor, ), - onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, + onTap: () => onTap(fieldInfo), + leftIcon: FlowySvg( + fieldInfo.fieldType.svgData, + color: Theme.of(context).iconTheme.color, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart new file mode 100644 index 0000000000..0f355ebc4c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart @@ -0,0 +1,69 @@ +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; + +class FilterInfo { + FilterInfo(this.viewId, this.filter, this.fieldInfo); + + final String viewId; + final FilterPB filter; + final FieldInfo fieldInfo; + + FilterInfo copyWith({FilterPB? filter, FieldInfo? fieldInfo}) { + return FilterInfo( + viewId, + filter ?? this.filter, + fieldInfo ?? this.fieldInfo, + ); + } + + String get filterId => filter.id; + + String get fieldId => filter.data.fieldId; + + DateFilterPB? dateFilter() { + final fieldType = filter.data.fieldType; + return fieldType == FieldType.DateTime || + fieldType == FieldType.CreatedTime || + fieldType == FieldType.LastEditedTime + ? DateFilterPB.fromBuffer(filter.data.data) + : null; + } + + TextFilterPB? textFilter() { + return filter.data.fieldType == FieldType.RichText || + filter.data.fieldType == FieldType.URL + ? TextFilterPB.fromBuffer(filter.data.data) + : null; + } + + CheckboxFilterPB? checkboxFilter() { + return filter.data.fieldType == FieldType.Checkbox + ? CheckboxFilterPB.fromBuffer(filter.data.data) + : null; + } + + SelectOptionFilterPB? selectOptionFilter() { + return filter.data.fieldType == FieldType.SingleSelect || + filter.data.fieldType == FieldType.MultiSelect + ? SelectOptionFilterPB.fromBuffer(filter.data.data) + : null; + } + + ChecklistFilterPB? checklistFilter() { + return filter.data.fieldType == FieldType.Checklist + ? ChecklistFilterPB.fromBuffer(filter.data.data) + : null; + } + + NumberFilterPB? numberFilter() { + return filter.data.fieldType == FieldType.Number + ? NumberFilterPB.fromBuffer(filter.data.data) + : null; + } + + TimeFilterPB? timeFilter() { + return filter.data.fieldType == FieldType.Time + ? TimeFilterPB.fromBuffer(filter.data.data) + : null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart index c91b47e2b7..80deb98695 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart @@ -1,11 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; + import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,47 +23,43 @@ class FilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FilterEditorBloc( + return BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: fieldController.viewId, fieldController: fieldController, - ), - child: BlocBuilder( - buildWhen: (previous, current) { - final previousIds = previous.filters.map((e) => e.filterId).toList(); - final currentIds = current.filters.map((e) => e.filterId).toList(); - return !listEquals(previousIds, currentIds); - }, + )..add( + const DatabaseFilterMenuEvent.initial(), + ), + child: BlocBuilder( builder: (context, state) { final List children = []; children.addAll( state.filters .map( - (filter) => FilterMenuItem( - key: ValueKey(filter.filterId), - filterId: filter.filterId, - fieldType: state.fields - .firstWhere( - (element) => element.id == filter.fieldId, - ) - .fieldType, + (filterInfo) => FilterMenuItem( + key: ValueKey(filterInfo.filter.id), + filterInfo: filterInfo, ), ) .toList(), ); - if (state.fields.isNotEmpty) { - children.add( - AddFilterButton( - viewId: state.viewId, - ), - ); + if (state.creatableFields.isNotEmpty) { + children.add(AddFilterButton(viewId: state.viewId)); } - return Wrap( - spacing: 6, - runSpacing: 4, - children: children, + return Expanded( + child: Row( + children: [ + Expanded( + child: Wrap( + spacing: 6, + runSpacing: 4, + children: children, + ), + ), + ], + ), ); }, ), @@ -80,16 +77,22 @@ class AddFilterButton extends StatefulWidget { } class _AddFilterButtonState extends State { - final PopoverController popoverController = PopoverController(); + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } @override Widget build(BuildContext context) { return wrapPopover( + context, SizedBox( height: 28, child: FlowyButton( text: FlowyText( - lineHeight: 1.0, LocaleKeys.grid_settings_addFilter.tr(), color: AFThemeExtension.of(context).textColor, ), @@ -105,18 +108,18 @@ class _AddFilterButtonState extends State { ); } - Widget wrapPopover(Widget child) { + Widget wrapPopover(BuildContext buildContext, Widget child) { return AppFlowyPopover( controller: popoverController, constraints: BoxConstraints.loose(const Size(200, 300)), triggerActions: PopoverTriggerFlags.none, child: child, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: CreateDatabaseViewFilterList( - onTap: () => popoverController.close(), - ), + popupBuilder: (BuildContext context) { + final bloc = buildContext.read(); + return GridCreateFilterList( + viewId: widget.viewId, + fieldController: bloc.fieldController, + onClosed: () => popoverController.close(), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart index d7e45840e6..ee9f168130 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart @@ -1,42 +1,37 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; + import 'choicechip/checkbox.dart'; -import 'choicechip/checklist.dart'; +import 'choicechip/checklist/checklist.dart'; import 'choicechip/date.dart'; import 'choicechip/number.dart'; import 'choicechip/select_option/select_option.dart'; import 'choicechip/text.dart'; import 'choicechip/url.dart'; +import 'filter_info.dart'; class FilterMenuItem extends StatelessWidget { - const FilterMenuItem({ - super.key, - required this.fieldType, - required this.filterId, - }); + const FilterMenuItem({required this.filterInfo, super.key}); - final FieldType fieldType; - final String filterId; + final FilterInfo filterInfo; @override Widget build(BuildContext context) { - return switch (fieldType) { - FieldType.RichText => TextFilterChoicechip(filterId: filterId), - FieldType.Number => NumberFilterChoiceChip(filterId: filterId), - FieldType.URL => URLFilterChoicechip(filterId: filterId), - FieldType.Checkbox => CheckboxFilterChoicechip(filterId: filterId), - FieldType.Checklist => ChecklistFilterChoicechip(filterId: filterId), - FieldType.DateTime || - FieldType.LastEditedTime || - FieldType.CreatedTime => - DateFilterChoicechip(filterId: filterId), - FieldType.SingleSelect || + return switch (filterInfo.fieldInfo.fieldType) { + FieldType.Checkbox => CheckboxFilterChoicechip(filterInfo: filterInfo), + FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo), FieldType.MultiSelect => - SelectOptionFilterChoicechip(filterId: filterId), + SelectOptionFilterChoicechip(filterInfo: filterInfo), + FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo), + FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), + FieldType.SingleSelect => + SelectOptionFilterChoicechip(filterInfo: filterInfo), + FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo), + FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), // FieldType.Time => // TimeFilterChoiceChip(filterInfo: filterInfo), - _ => const SizedBox.shrink(), + _ => const SizedBox(), }; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart index 43a0301a10..fa78dd629e 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart @@ -1,13 +1,13 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GridAddRowButton extends StatelessWidget { @@ -15,27 +15,22 @@ class GridAddRowButton extends StatelessWidget { @override Widget build(BuildContext context) { - final color = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF171717).withValues(alpha: 0.4) - : const Color(0xFFFFFFFF).withValues(alpha: 0.4); return FlowyButton( radius: BorderRadius.zero, decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: AFThemeExtension.of(context).borderColor), + bottom: BorderSide(color: Theme.of(context).dividerColor), ), ), text: FlowyText( - lineHeight: 1.0, LocaleKeys.grid_row_newRow.tr(), - color: color, + color: Theme.of(context).hintColor, ), - margin: const EdgeInsets.symmetric(horizontal: 12), hoverColor: AFThemeExtension.of(context).lightGreyHover, onTap: () => context.read().add(const GridEvent.createRow()), leftIcon: FlowySvg( - FlowySvgs.add_less_padding_s, - color: color, + FlowySvgs.add_s, + color: Theme.of(context).hintColor, ), ); } @@ -46,54 +41,10 @@ class GridRowBottomBar extends StatelessWidget { @override Widget build(BuildContext context) { - final padding = - context.read().horizontalPadding; return Container( - padding: GridSize.footerContentInsets.copyWith(left: 0) + - EdgeInsets.only(left: padding), + padding: GridSize.footerContentInsets + const EdgeInsets.only(left: 40), height: GridSize.footerHeight, child: const GridAddRowButton(), ); } } - -class GridRowLoadMoreButton extends StatelessWidget { - const GridRowLoadMoreButton({super.key}); - - @override - Widget build(BuildContext context) { - final padding = - context.read().horizontalPadding; - final color = Theme.of(context).brightness == Brightness.light - ? const Color(0xFF171717).withValues(alpha: 0.4) - : const Color(0xFFFFFFFF).withValues(alpha: 0.4); - - return Container( - padding: GridSize.footerContentInsets.copyWith(left: 0) + - EdgeInsets.only(left: padding), - height: GridSize.footerHeight, - child: FlowyButton( - radius: BorderRadius.zero, - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: AFThemeExtension.of(context).borderColor), - ), - ), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_row_loadMore.tr(), - color: color, - ), - margin: const EdgeInsets.symmetric(horizontal: 12), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () => context.read().add( - const GridEvent.loadMoreRows(), - ), - leftIcon: FlowySvg( - FlowySvgs.load_more_s, - color: color, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index 915bf70a61..fd09b6a591 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -3,10 +3,9 @@ import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/util/field_type_extension.dart'; -import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -42,12 +41,13 @@ class GridFieldCell extends StatefulWidget { } class _GridFieldCellState extends State { - final PopoverController popoverController = PopoverController(); late final FieldCellBloc _bloc; + late PopoverController popoverController; @override void initState() { super.initState(); + popoverController = PopoverController(); _bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo); if (widget.isEditing) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { @@ -86,8 +86,7 @@ class _GridFieldCellState extends State { return FieldEditor( viewId: widget.viewId, fieldController: widget.fieldController, - fieldInfo: widget.fieldInfo, - isNewField: widget.isNew, + field: widget.fieldInfo.field, initialPage: widget.isNew ? FieldEditorPage.details : FieldEditorPage.general, @@ -95,11 +94,10 @@ class _GridFieldCellState extends State { ); }, child: SizedBox( - height: GridSize.headerHeight, + height: 40, child: FieldCellButton( field: widget.fieldInfo.field, onTap: widget.onTap, - margin: const EdgeInsetsDirectional.fromSTEB(12, 9, 10, 9), ), ), ); @@ -108,7 +106,7 @@ class _GridFieldCellState extends State { top: 0, bottom: 0, right: 0, - child: DragToExpandLine(), + child: _DragToExpandLine(), ); return _GridHeaderCellContainer( @@ -141,8 +139,9 @@ class _GridHeaderCellContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final borderSide = - BorderSide(color: AFThemeExtension.of(context).borderColor); + final borderSide = BorderSide( + color: Theme.of(context).dividerColor, + ); final decoration = BoxDecoration( border: Border( right: borderSide, @@ -158,11 +157,8 @@ class _GridHeaderCellContainer extends StatelessWidget { } } -@visibleForTesting -class DragToExpandLine extends StatelessWidget { - const DragToExpandLine({ - super.key, - }); +class _DragToExpandLine extends StatelessWidget { + const _DragToExpandLine(); @override Widget build(BuildContext context) { @@ -213,27 +209,26 @@ class FieldCellButton extends StatelessWidget { final VoidCallback onTap; final int? maxLines; final BorderRadius? radius; - final EdgeInsetsGeometry? margin; + final EdgeInsets? margin; @override Widget build(BuildContext context) { return FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: FieldInfo.initial(field), + leftIcon: FlowySvg( + field.fieldType.svgData, + color: Theme.of(context).iconTheme.color, ), rightIcon: field.fieldType.rightIcon != null ? FlowySvg( field.fieldType.rightIcon!, blendMode: null, - size: const Size.square(18), ) : null, radius: radius, - text: FlowyText( + text: FlowyText.medium( field.name, - lineHeight: 1.0, maxLines: maxLines, overflow: TextOverflow.ellipsis, color: AFThemeExtension.of(context).textColor, @@ -242,39 +237,3 @@ class FieldCellButton extends StatelessWidget { ); } } - -class FieldIcon extends StatelessWidget { - const FieldIcon({ - super.key, - required this.fieldInfo, - this.dimension = 16.0, - }); - - final FieldInfo fieldInfo; - final double dimension; - - @override - Widget build(BuildContext context) { - final svgContent = kIconGroups?.findSvgContent( - fieldInfo.icon, - ); - final color = - Theme.of(context).isLightMode ? const Color(0xFF171717) : Colors.white; - return svgContent == null - ? FlowySvg( - fieldInfo.fieldType.svgData, - color: color.withValues(alpha: 0.6), - size: Size.square(dimension), - ) - : SizedBox.square( - dimension: dimension, - child: Center( - child: FlowySvg.string( - svgContent, - color: color.withValues(alpha: 0.45), - size: Size.square(dimension - 2), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart index c59327113b..3267c74ad7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_extension.dart @@ -1,8 +1,24 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -extension RowDetailAccessoryExtension on FieldType { - bool get showRowDetailAccessory => switch (this) { - FieldType.Media => false, - _ => true, +extension FieldTypeListExtension on FieldType { + bool get canEditHeader => switch (this) { + FieldType.MultiSelect => true, + FieldType.SingleSelect => true, + _ => false, + }; + + bool get canCreateNewGroup => switch (this) { + FieldType.MultiSelect => true, + FieldType.SingleSelect => true, + _ => false, + }; + + bool get canDeleteGroup => switch (this) { + FieldType.URL || + FieldType.SingleSelect || + FieldType.MultiSelect || + FieldType.DateTime => + true, + _ => false, }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index e30c238f96..3b6b4320d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -4,15 +4,15 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.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:reorderables/reorderables.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../layout/sizes.dart'; import 'desktop_field_cell.dart'; @@ -21,13 +21,11 @@ class GridHeaderSliverAdaptor extends StatefulWidget { const GridHeaderSliverAdaptor({ super.key, required this.viewId, - required this.shrinkWrap, required this.anchorScrollController, }); final String viewId; final ScrollController anchorScrollController; - final bool shrinkWrap; @override State createState() => @@ -39,9 +37,6 @@ class _GridHeaderSliverAdaptorState extends State { Widget build(BuildContext context) { final fieldController = context.read().databaseController.fieldController; - final horizontalPadding = - context.read()?.horizontalPadding ?? - 0.0; return BlocProvider( create: (context) { return GridHeaderBloc( @@ -52,14 +47,9 @@ class _GridHeaderSliverAdaptorState extends State { child: SingleChildScrollView( scrollDirection: Axis.horizontal, controller: widget.anchorScrollController, - child: Padding( - padding: widget.shrinkWrap - ? EdgeInsets.symmetric(horizontal: horizontalPadding) - : EdgeInsets.zero, - child: _GridHeader( - viewId: widget.viewId, - fieldController: fieldController, - ), + child: _GridHeader( + viewId: widget.viewId, + fieldController: fieldController, ), ), ); @@ -121,7 +111,7 @@ class _GridHeaderState extends State<_GridHeader> { ), draggingWidgetOpacity: 0, header: _cellLeading(), - needsLongPressDraggable: UniversalPlatform.isMobile, + needsLongPressDraggable: PlatformExtension.isMobile, footer: _CellTrailing(viewId: widget.viewId), onReorder: (int oldIndex, int newIndex) { context @@ -149,9 +139,7 @@ class _GridHeaderState extends State<_GridHeader> { } Widget _cellLeading() { - return SizedBox( - width: context.read().horizontalPadding, - ); + return SizedBox(width: GridSize.horizontalHeaderPadding + 40); } } @@ -163,11 +151,14 @@ class _CellTrailing extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: GridSize.newPropertyButtonWidth, - height: GridSize.headerHeight, + constraints: BoxConstraints( + maxWidth: GridSize.newPropertyButtonWidth, + minHeight: GridSize.headerHeight, + ), + margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m), decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: AFThemeExtension.of(context).borderColor), + bottom: BorderSide(color: Theme.of(context).dividerColor), ), ), child: CreateFieldButton( @@ -180,7 +171,7 @@ class _CellTrailing extends StatelessWidget { } } -class CreateFieldButton extends StatelessWidget { +class CreateFieldButton extends StatefulWidget { const CreateFieldButton({ super.key, required this.viewId, @@ -190,29 +181,33 @@ class CreateFieldButton extends StatelessWidget { final String viewId; final void Function(String fieldId) onFieldCreated; + @override + State createState() => _CreateFieldButtonState(); +} + +class _CreateFieldButtonState extends State { @override Widget build(BuildContext context) { return FlowyButton( margin: GridSize.cellContentInsets, radius: BorderRadius.zero, text: FlowyText( - lineHeight: 1.0, LocaleKeys.grid_field_newProperty.tr(), overflow: TextOverflow.ellipsis, ), hoverColor: AFThemeExtension.of(context).greyHover, onTap: () async { final result = await FieldBackendService.createField( - viewId: viewId, + viewId: widget.viewId, ); result.fold( - (field) => onFieldCreated(field.id), + (field) => widget.onFieldCreated(field.id), (err) => Log.error("Failed to create field type option: $err"), ); }, leftIcon: const FlowySvg( - FlowySvgs.add_less_padding_s, - size: Size.square(16), + FlowySvgs.add_s, + size: Size.square(18), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart index 20cc030ff4..d4c2289136 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_field_button.dart @@ -1,7 +1,8 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/database/field/mobile_field_bottom_sheets.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -42,9 +43,9 @@ class MobileFieldButton extends StatelessWidget { radius: radius, margin: margin, leftIconSize: const Size.square(18), - leftIcon: FieldIcon( - fieldInfo: fieldInfo, - dimension: 18, + leftIcon: FlowySvg( + fieldInfo.fieldType.svgData, + size: const Size.square(18), ), text: FlowyText( fieldInfo.name, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart index 369bdeb523..90bfbbca13 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/mobile_grid_header.dart @@ -5,7 +5,6 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -40,8 +39,6 @@ class _MobileGridHeaderState extends State { Widget build(BuildContext context) { final fieldController = context.read().databaseController.fieldController; - final isLocked = - context.read()?.state.isLocked ?? false; return BlocProvider( create: (context) { return GridHeaderBloc( @@ -79,15 +76,12 @@ class _MobileGridHeaderState extends State { ); }, ), - IgnorePointer( - ignoring: isLocked, - child: SizedBox( - height: _kGridHeaderHeight, - child: _GridHeader( - viewId: widget.viewId, - fieldController: fieldController, - scrollController: widget.reorderableController, - ), + SizedBox( + height: _kGridHeaderHeight, + child: _GridHeader( + viewId: widget.viewId, + fieldController: fieldController, + scrollController: widget.reorderableController, ), ), ], @@ -184,7 +178,7 @@ class _GridHeaderState extends State<_GridHeader> { } } -class CreateFieldButton extends StatelessWidget { +class CreateFieldButton extends StatefulWidget { const CreateFieldButton({ super.key, required this.viewId, @@ -194,11 +188,16 @@ class CreateFieldButton extends StatelessWidget { final String viewId; final void Function(String fieldId) onFieldCreated; + @override + State createState() => _CreateFieldButtonState(); +} + +class _CreateFieldButtonState extends State { @override Widget build(BuildContext context) { return Container( constraints: BoxConstraints( - maxWidth: GridSize.mobileNewPropertyButtonWidth, + maxWidth: GridSize.newPropertyButtonWidth, minHeight: GridSize.headerHeight, ), decoration: _getDecoration(context), @@ -212,7 +211,7 @@ class CreateFieldButton extends StatelessWidget { color: Theme.of(context).hintColor, ), hoverColor: AFThemeExtension.of(context).greyHover, - onTap: () => mobileCreateFieldWorkflow(context, viewId), + onTap: () => mobileCreateFieldWorkflow(context, widget.viewId), leftIconSize: const Size.square(18), leftIcon: FlowySvg( FlowySvgs.add_s, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart index d212c50746..abdeb90e47 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart @@ -1,15 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/domain/sort_service.dart'; -import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class RowActionMenu extends StatelessWidget { const RowActionMenu({ @@ -53,13 +51,19 @@ class RowActionMenu extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - action.text, - overflow: TextOverflow.ellipsis, - lineHeight: 1.0, - ), + text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis), onTap: () { - action.performAction(context, viewId, rowId); + if (action == RowAction.delete) { + NavigatorOkCancelDialog( + message: LocaleKeys.grid_row_deleteRowPrompt.tr(), + onOkPressed: () { + action.performAction(context, viewId, rowId); + }, + ).show(context); + } else { + action.performAction(context, viewId, rowId); + } + PopoverContainer.of(context).close(); }, leftIcon: icon, @@ -78,7 +82,7 @@ enum RowAction { return switch (this) { insertAbove => FlowySvgs.arrow_s, insertBelow => FlowySvgs.add_s, - duplicate => FlowySvgs.duplicate_s, + duplicate => FlowySvgs.copy_s, delete => FlowySvgs.delete_s, }; } @@ -99,45 +103,17 @@ enum RowAction { final position = this == insertAbove ? OrderObjectPositionTypePB.Before : OrderObjectPositionTypePB.After; - final intention = this == insertAbove - ? LocaleKeys.grid_row_createRowAboveDescription.tr() - : LocaleKeys.grid_row_createRowBelowDescription.tr(); - if (context.read().state.sorts.isNotEmpty) { - showCancelAndDeleteDialog( - context: context, - title: LocaleKeys.grid_sort_sortsActive.tr( - namedArgs: {'intention': intention}, - ), - description: LocaleKeys.grid_sort_removeSorting.tr(), - confirmLabel: LocaleKeys.button_remove.tr(), - closeOnAction: true, - onDelete: () { - SortBackendService(viewId: viewId).deleteAllSorts(); - RowBackendService.createRow( - viewId: viewId, - position: position, - targetRowId: rowId, - ); - }, - ); - } else { - RowBackendService.createRow( - viewId: viewId, - position: position, - targetRowId: rowId, - ); - } + RowBackendService.createRow( + viewId: viewId, + position: position, + targetRowId: rowId, + ); break; case duplicate: RowBackendService.duplicateRow(viewId, rowId); break; case delete: - showConfirmDeletionDialog( - context: context, - name: LocaleKeys.grid_row_label.tr(), - description: LocaleKeys.grid_row_deleteRowPrompt.tr(), - onConfirm: () => RowBackendService.deleteRows(viewId, [rowId]), - ); + RowBackendService.deleteRows(viewId, [rowId]); break; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart index 209c439ad1..b6817fc848 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/mobile_row.dart @@ -81,8 +81,8 @@ class _MobileGridRowState extends State { @override Future dispose() async { + _rowController.dispose(); super.dispose(); - await _rowController.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 2306767f46..351062933a 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -1,28 +1,27 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import "package:appflowy/generated/locale_keys.g.dart"; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/domain/sort_service.dart'; -import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import '../../../../widgets/row/accessory/cell_accessory.dart'; import '../../../../widgets/row/cells/cell_container.dart'; import '../../layout/sizes.dart'; + import 'action.dart'; -class GridRow extends StatelessWidget { +class GridRow extends StatefulWidget { const GridRow({ super.key, required this.fieldController, @@ -31,9 +30,8 @@ class GridRow extends StatelessWidget { required this.rowController, required this.cellBuilder, required this.openDetailPage, - required this.index, - this.shrinkWrap = false, - required this.editable, + this.index, + this.isDraggable = false, }); final FieldController fieldController; @@ -42,57 +40,52 @@ class GridRow extends StatelessWidget { final RowController rowController; final EditableCellBuilder cellBuilder; final void Function(BuildContext context) openDetailPage; - final int index; - final bool shrinkWrap; - final bool editable; + final int? index; + final bool isDraggable; + @override + State createState() => _GridRowState(); +} + +class _GridRowState extends State { @override Widget build(BuildContext context) { - Widget rowContent = RowContent( - fieldController: fieldController, - cellBuilder: cellBuilder, - onExpand: () => openDetailPage(context), - ); - - if (!shrinkWrap) { - rowContent = Expanded(child: rowContent); - } - - rowContent = BlocProvider( + return BlocProvider( create: (_) => RowBloc( - fieldController: fieldController, - rowId: rowId, - rowController: rowController, - viewId: viewId, + fieldController: widget.fieldController, + rowId: widget.rowId, + rowController: widget.rowController, + viewId: widget.viewId, ), child: _RowEnterRegion( child: Row( children: [ - _RowLeading(viewId: viewId, index: index), - rowContent, + _RowLeading( + index: widget.index, + isDraggable: widget.isDraggable, + ), + Expanded( + child: RowContent( + fieldController: widget.fieldController, + cellBuilder: widget.cellBuilder, + onExpand: () => widget.openDetailPage(context), + ), + ), ], ), ), ); - - if (!editable) { - rowContent = IgnorePointer( - child: rowContent, - ); - } - - return rowContent; } } class _RowLeading extends StatefulWidget { const _RowLeading({ - required this.viewId, - required this.index, + this.index, + this.isDraggable = false, }); - final String viewId; - final int index; + final int? index; + final bool isDraggable; @override State<_RowLeading> createState() => _RowLeadingState(); @@ -106,25 +99,20 @@ class _RowLeadingState extends State<_RowLeading> { return AppFlowyPopover( controller: popoverController, triggerActions: PopoverTriggerFlags.none, - constraints: BoxConstraints.loose(const Size(200, 200)), + constraints: BoxConstraints.loose(const Size(176, 200)), direction: PopoverDirection.rightWithCenterAligned, margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), popupBuilder: (_) { final bloc = context.read(); - return BlocProvider.value( - value: context.read(), - child: RowActionMenu( - viewId: bloc.viewId, - rowId: bloc.rowId, - ), + return RowActionMenu( + viewId: bloc.viewId, + rowId: bloc.rowId, ); }, child: Consumer( builder: (context, state, _) { return SizedBox( - width: context - .read() - .horizontalPadding, + width: GridSize.horizontalHeaderPadding + 40, child: state.onEnter ? _activeWidget() : null, ); }, @@ -136,25 +124,28 @@ class _RowLeadingState extends State<_RowLeading> { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - InsertRowButton(viewId: widget.viewId), - ReorderableDragStartListener( - index: widget.index, - child: RowMenuButton( + const InsertRowButton(), + if (isDraggable) + ReorderableDragStartListener( + index: widget.index!, + child: RowMenuButton( + isDragEnabled: isDraggable, + openMenu: popoverController.show, + ), + ) + else + RowMenuButton( openMenu: popoverController.show, ), - ), ], ); } + + bool get isDraggable => widget.index != null && widget.isDraggable; } class InsertRowButton extends StatelessWidget { - const InsertRowButton({ - super.key, - required this.viewId, - }); - - final String viewId; + const InsertRowButton({super.key}); @override Widget build(BuildContext context) { @@ -163,28 +154,7 @@ class InsertRowButton extends StatelessWidget { hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 20, height: 30, - onPressed: () { - final rowBloc = context.read(); - if (context.read().state.sorts.isNotEmpty) { - showCancelAndDeleteDialog( - context: context, - title: LocaleKeys.grid_sort_sortsActive.tr( - namedArgs: { - 'intention': LocaleKeys.grid_row_createRowBelowDescription.tr(), - }, - ), - description: LocaleKeys.grid_sort_removeSorting.tr(), - confirmLabel: LocaleKeys.button_remove.tr(), - closeOnAction: true, - onDelete: () { - SortBackendService(viewId: viewId).deleteAllSorts(); - rowBloc.add(const RowEvent.createRow()); - }, - ); - } else { - rowBloc.add(const RowEvent.createRow()); - } - }, + onPressed: () => context.read().add(const RowEvent.createRow()), iconPadding: const EdgeInsets.all(3), icon: FlowySvg( FlowySvgs.add_s, @@ -198,9 +168,11 @@ class RowMenuButton extends StatefulWidget { const RowMenuButton({ super.key, required this.openMenu, + this.isDragEnabled = false, }); final VoidCallback openMenu; + final bool isDragEnabled; @override State createState() => _RowMenuButtonState(); @@ -210,18 +182,16 @@ class _RowMenuButtonState extends State { @override Widget build(BuildContext context) { return FlowyIconButton( - richTooltipText: TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.tooltip_dragRow.tr()}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: LocaleKeys.tooltip_openMenu.tr(), - style: context.tooltipTextStyle(), - ), - ], - ), + tooltipText: + widget.isDragEnabled ? null : LocaleKeys.tooltip_openMenu.tr(), + richTooltipText: widget.isDragEnabled + ? TextSpan( + children: [ + TextSpan(text: '${LocaleKeys.tooltip_dragRow.tr()}\n'), + TextSpan(text: LocaleKeys.tooltip_openMenu.tr()), + ], + ) + : null, hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 20, height: 30, @@ -308,20 +278,14 @@ class RowContent extends StatelessWidget { Widget _finalCellDecoration(BuildContext context) { return MouseRegion( cursor: SystemMouseCursors.basic, - child: ValueListenableBuilder( - valueListenable: cellBuilder.databaseController.compactModeNotifier, - builder: (context, compactMode, _) { - return Container( - width: GridSize.newPropertyButtonWidth, - constraints: BoxConstraints(minHeight: compactMode ? 32 : 36), - decoration: BoxDecoration( - border: Border( - bottom: - BorderSide(color: AFThemeExtension.of(context).borderColor), - ), - ), - ); - }, + child: Container( + width: GridSize.trailHeaderPadding, + constraints: const BoxConstraints(minHeight: 46), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor), + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart index 5218a60ee5..69e46a04ff 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart @@ -1,9 +1,9 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/grid/application/simple_text_filter_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -24,58 +24,44 @@ class CreateDatabaseViewSortList extends StatelessWidget { @override Widget build(BuildContext context) { - final sortBloc = context.read(); - return BlocProvider( - create: (_) => SimpleTextFilterBloc( - values: List.from(sortBloc.state.creatableFields), - comparator: (val) => val.name, - ), - child: BlocListener( - listenWhen: (previous, current) => - previous.creatableFields != current.creatableFields, - listener: (context, state) { - context.read>().add( - SimpleTextFilterEvent.receiveNewValues(state.creatableFields), - ); - }, - child: BlocBuilder, - SimpleTextFilterState>( - builder: (context, state) { - final cells = state.values.map((fieldInfo) { - return GridSortPropertyCell( - fieldInfo: fieldInfo, - onTap: () { - context - .read() - .add(SortEditorEvent.createSort(fieldId: fieldInfo.id)); - onTap.call(); - }, - ); - }).toList(); + return BlocBuilder( + builder: (context, state) { + final filter = state.filter.toLowerCase(); + final cells = state.creatableFields + .where((field) => field.field.name.toLowerCase().contains(filter)) + .map((fieldInfo) { + return GridSortPropertyCell( + fieldInfo: fieldInfo, + onTap: () { + context + .read() + .add(SortEditorEvent.createSort(fieldId: fieldInfo.id)); + onTap.call(); + }, + ); + }).toList(); - final List slivers = [ - SliverPersistentHeader( - pinned: true, - delegate: _SortTextFieldDelegate(), - ), - SliverToBoxAdapter( - child: ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - itemBuilder: (_, index) => cells[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - ), - ), - ]; - return CustomScrollView( + final List slivers = [ + SliverPersistentHeader( + pinned: true, + delegate: _SortTextFieldDelegate(), + ), + SliverToBoxAdapter( + child: ListView.separated( shrinkWrap: true, - slivers: slivers, - physics: StyledScrollPhysics(), - ); - }, - ), - ), + itemCount: cells.length, + itemBuilder: (_, index) => cells[index], + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + ), + ), + ]; + return CustomScrollView( + shrinkWrap: true, + slivers: slivers, + physics: StyledScrollPhysics(), + ); + }, ); } } @@ -99,8 +85,8 @@ class _SortTextFieldDelegate extends SliverPersistentHeaderDelegate { hintText: LocaleKeys.grid_settings_sortBy.tr(), onChanged: (text) { context - .read>() - .add(SimpleTextFilterEvent.updateFilter(text)); + .read() + .add(SortEditorEvent.updateCreateSortFilter(text)); }, ), ); @@ -132,14 +118,14 @@ class GridSortPropertyCell extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( + text: FlowyText.medium( fieldInfo.name, - lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, ), onTap: onTap, - leftIcon: FieldIcon( - fieldInfo: fieldInfo, + leftIcon: FlowySvg( + fieldInfo.fieldType.svgData, + color: Theme.of(context).iconTheme.color, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart index 9bf8f36c85..0d1a0fe1d0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/order_panel.dart @@ -13,7 +13,7 @@ class OrderPanel extends StatelessWidget { @override Widget build(BuildContext context) { final List children = SortConditionPB.values.map((condition) { - return OrderPanelItem( + return OrderPannelItem( condition: condition, onCondition: onCondition, ); @@ -32,8 +32,8 @@ class OrderPanel extends StatelessWidget { } } -class OrderPanelItem extends StatelessWidget { - const OrderPanelItem({ +class OrderPannelItem extends StatelessWidget { + const OrderPannelItem({ super.key, required this.condition, required this.onCondition, @@ -47,7 +47,7 @@ class OrderPanelItem extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText(condition.title), + text: FlowyText.medium(condition.title), onTap: () => onCondition(condition), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart index 4d509b3862..a00bc1002f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart @@ -34,7 +34,6 @@ class SortChoiceButton extends StatelessWidget { useIntrinsicWidth: true, text: FlowyText( text, - lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart index 2f7f68e2f6..671a5c2084 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart @@ -1,23 +1,22 @@ import 'dart:io'; -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; + import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'create_sort_list.dart'; import 'order_panel.dart'; import 'sort_choice_button.dart'; +import 'sort_info.dart'; class SortEditor extends StatefulWidget { const SortEditor({super.key}); @@ -29,25 +28,20 @@ class SortEditor extends StatefulWidget { class _SortEditorState extends State { final popoverMutex = PopoverMutex(); - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + final sortInfos = state.sortInfos; return ReorderableListView.builder( onReorder: (oldIndex, newIndex) => context .read() .add(SortEditorEvent.reorderSort(oldIndex, newIndex)), - itemCount: state.sorts.length, + itemCount: state.sortInfos.length, itemBuilder: (context, index) => DatabaseSortItem( - key: ValueKey(state.sorts[index].sortId), + key: ValueKey(sortInfos[index].sortId), index: index, - sort: state.sorts[index], + sortInfo: sortInfos[index], popoverMutex: popoverMutex, ), proxyDecorator: (child, index, animation) => Material( @@ -96,12 +90,12 @@ class DatabaseSortItem extends StatelessWidget { super.key, required this.index, required this.popoverMutex, - required this.sort, + required this.sortInfo, }); final int index; final PopoverMutex popoverMutex; - final DatabaseSort sort; + final SortInfo sortInfo; @override Widget build(BuildContext context) { @@ -131,16 +125,9 @@ class DatabaseSortItem extends StatelessWidget { fit: FlexFit.tight, child: SizedBox( height: 26, - child: BlocSelector( - selector: (state) => state.allFields.firstWhereOrNull( - (field) => field.id == sort.fieldId, - ), - builder: (context, field) { - return SortChoiceButton( - text: field?.name ?? "", - editable: false, - ); - }, + child: SortChoiceButton( + text: sortInfo.fieldInfo.name, + editable: false, ), ), ), @@ -150,7 +137,7 @@ class DatabaseSortItem extends StatelessWidget { child: SizedBox( height: 26, child: SortConditionButton( - sort: sort, + sortInfo: sortInfo, popoverMutex: popoverMutex, ), ), @@ -161,7 +148,7 @@ class DatabaseSortItem extends StatelessWidget { onPressed: () { context .read() - .add(SortEditorEvent.deleteSort(sort.sortId)); + .add(SortEditorEvent.deleteSort(sortInfo)); PopoverContainer.of(context).close(); }, hoverColor: AFThemeExtension.of(context).lightGreyHover, @@ -222,12 +209,15 @@ class _DatabaseAddSortButtonState extends State { ), ); }, + onClose: () => context + .read() + .add(const SortEditorEvent.updateCreateSortFilter("")), child: SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).greyHover, disable: widget.disable, - text: FlowyText(LocaleKeys.grid_sort_addSort.tr()), + text: FlowyText.medium(LocaleKeys.grid_sort_addSort.tr()), onTap: () => _popoverController.show(), leftIcon: const FlowySvg(FlowySvgs.add_s), ), @@ -248,7 +238,7 @@ class DeleteAllSortsButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText(LocaleKeys.grid_sort_deleteAllSorts.tr()), + text: FlowyText.medium(LocaleKeys.grid_sort_deleteAllSorts.tr()), onTap: () { context .read() @@ -267,11 +257,11 @@ class SortConditionButton extends StatefulWidget { const SortConditionButton({ super.key, required this.popoverMutex, - required this.sort, + required this.sortInfo, }); final PopoverMutex popoverMutex; - final DatabaseSort sort; + final SortInfo sortInfo; @override State createState() => _SortConditionButtonState(); @@ -293,7 +283,7 @@ class _SortConditionButtonState extends State { onCondition: (condition) { context.read().add( SortEditorEvent.editSort( - sortId: widget.sort.sortId, + sortId: widget.sortInfo.sortId, condition: condition, ), ); @@ -302,7 +292,7 @@ class _SortConditionButtonState extends State { ); }, child: SortChoiceButton( - text: widget.sort.condition.title, + text: widget.sortInfo.sortPB.condition.title, rightIcon: FlowySvg( FlowySvgs.arrow_down_s, color: Theme.of(context).iconTheme.color, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_info.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_info.dart new file mode 100644 index 0000000000..eef8d700ba --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_info.dart @@ -0,0 +1,13 @@ +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pb.dart'; + +class SortInfo { + SortInfo({required this.sortPB, required this.fieldInfo}); + + final SortPB sortPB; + final FieldInfo fieldInfo; + + String get sortId => sortPB.id; + + String get fieldId => sortPB.fieldId; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart index fc35e76241..43f583bd07 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart @@ -2,8 +2,8 @@ import 'dart:math' as math; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; -import 'package:appflowy/plugins/database/application/field/sort_entities.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'sort_choice_button.dart'; import 'sort_editor.dart'; +import 'sort_info.dart'; class SortMenu extends StatelessWidget { const SortMenu({ @@ -30,7 +31,7 @@ class SortMenu extends StatelessWidget { ), child: BlocBuilder( builder: (context, state) { - if (state.sorts.isEmpty) { + if (state.sortInfos.isEmpty) { return const SizedBox.shrink(); } @@ -46,7 +47,7 @@ class SortMenu extends StatelessWidget { child: const SortEditor(), ); }, - child: SortChoiceChip(sorts: state.sorts), + child: SortChoiceChip(sortInfos: state.sortInfos), ); }, ), @@ -57,11 +58,11 @@ class SortMenu extends StatelessWidget { class SortChoiceChip extends StatelessWidget { const SortChoiceChip({ super.key, - required this.sorts, + required this.sortInfos, this.onTap, }); - final List sorts; + final List sortInfos; final VoidCallback? onTap; @override diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart index 5c33426281..21c61713e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart @@ -1,22 +1,18 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../layout/sizes.dart'; import '../filter/create_filter_list.dart'; class FilterButton extends StatefulWidget { - const FilterButton({ - super.key, - required this.toggleExtension, - }); - - final ToggleExtensionNotifier toggleExtension; + const FilterButton({super.key}); @override State createState() => _FilterButtonState(); @@ -27,34 +23,38 @@ class _FilterButtonState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { + final textColor = state.filters.isEmpty + ? AFThemeExtension.of(context).textColor + : Theme.of(context).colorScheme.primary; + return _wrapPopover( - MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyIconButton( - tooltipText: LocaleKeys.grid_settings_filter.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - icon: const FlowySvg(FlowySvgs.database_filter_s), - onPressed: () { - final bloc = context.read(); - if (bloc.state.filters.isEmpty) { - _popoverController.show(); - } else { - widget.toggleExtension.toggle(); - } - }, - ), + context, + FlowyTextButton( + LocaleKeys.grid_settings_filter.tr(), + fontColor: textColor, + fontSize: FontSizes.s11, + fontWeight: FontWeight.w400, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.toolbarSettingButtonInsets, + radius: Corners.s4Border, + onPressed: () { + final bloc = context.read(); + if (bloc.state.filters.isEmpty) { + _popoverController.show(); + } else { + bloc.add(const DatabaseFilterMenuEvent.toggleMenu()); + } + }, ), ); }, ); } - Widget _wrapPopover(Widget child) { + Widget _wrapPopover(BuildContext buildContext, Widget child) { return AppFlowyPopover( controller: _popoverController, direction: PopoverDirection.bottomWithLeftAligned, @@ -62,17 +62,17 @@ class _FilterButtonState extends State { offset: const Offset(0, 8), triggerActions: PopoverTriggerFlags.none, child: child, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: CreateDatabaseViewFilterList( - onTap: () { - if (!widget.toggleExtension.isToggled) { - widget.toggleExtension.toggle(); - } - _popoverController.close(); - }, - ), + popupBuilder: (BuildContext context) { + final bloc = buildContext.read(); + return GridCreateFilterList( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + onClosed: () => _popoverController.close(), + onCreateFilter: () { + if (!bloc.state.isVisible) { + bloc.add(const DatabaseFilterMenuEvent.toggleMenu()); + } + }, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart index f325ab206f..312bfd7511 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -1,17 +1,14 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.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 'filter_button.dart'; import 'sort_button.dart'; -import 'view_database_button.dart'; class GridSettingBar extends StatelessWidget { const GridSettingBar({ @@ -27,45 +24,45 @@ class GridSettingBar extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => FilterEditorBloc( + BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: controller.viewId, fieldController: controller.fieldController, - ), + )..add(const DatabaseFilterMenuEvent.initial()), ), - BlocProvider( + BlocProvider( create: (context) => SortEditorBloc( viewId: controller.viewId, fieldController: controller.fieldController, ), ), ], - child: ValueListenableBuilder( - valueListenable: controller.isLoading, - builder: (context, isLoading, child) { - if (isLoading) { - return const SizedBox.shrink(); - } - final isReference = - Provider.of(context)?.isReference ?? false; - return SizedBox( - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - FilterButton(toggleExtension: toggleExtension), - const HSpace(2), - SortButton(toggleExtension: toggleExtension), - if (isReference) ...[ + child: BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + child: ValueListenableBuilder( + valueListenable: controller.isLoading, + builder: (context, value, child) { + if (value) { + return const SizedBox.shrink(); + } + return SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const FilterButton(), const HSpace(2), - ViewDatabaseButton(view: controller.view), + SortButton(toggleExtension: toggleExtension), + const HSpace(2), + SettingButton( + databaseController: controller, + ), ], - const HSpace(2), - SettingButton(databaseController: controller), - ], - ), - ); - }, + ), + ); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart index 6649d53594..be4740cea0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart @@ -1,12 +1,14 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import '../sort/create_sort_list.dart'; @@ -26,31 +28,35 @@ class _SortButtonState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + final textColor = state.sortInfos.isEmpty + ? AFThemeExtension.of(context).textColor + : Theme.of(context).colorScheme.primary; + return wrapPopover( - MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyIconButton( - tooltipText: LocaleKeys.grid_settings_sort.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - icon: const FlowySvg(FlowySvgs.database_sort_s), - onPressed: () { - if (state.sorts.isEmpty) { - _popoverController.show(); - } else { - widget.toggleExtension.toggle(); - } - }, - ), + context, + FlowyTextButton( + LocaleKeys.grid_settings_sort.tr(), + fontColor: textColor, + fontSize: FontSizes.s11, + fontWeight: FontWeight.w400, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.toolbarSettingButtonInsets, + radius: Corners.s4Border, + onPressed: () { + if (state.sortInfos.isEmpty) { + _popoverController.show(); + } else { + widget.toggleExtension.toggle(); + } + }, ), ); }, ); } - Widget wrapPopover(Widget child) { + Widget wrapPopover(BuildContext context, Widget child) { return AppFlowyPopover( controller: _popoverController, direction: PopoverDirection.bottomWithLeftAligned, @@ -70,6 +76,9 @@ class _SortButtonState extends State { ), ); }, + onClose: () => context + .read() + .add(const SortEditorEvent.updateCreateSortFilter("")), child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart deleted file mode 100644 index 93493e599f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/view_database_button.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flutter/material.dart'; - -class ViewDatabaseButton extends StatelessWidget { - const ViewDatabaseButton({super.key, required this.view}); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyIconButton( - tooltipText: LocaleKeys.grid_rowPage_openAsFullPage.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - icon: const FlowySvg(FlowySvgs.database_fullscreen_s), - onPressed: () { - getIt().add( - TabsEvent.openPlugin( - plugin: view.plugin(), - view: view, - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart index 69e5d27d37..5b66c3a149 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/setting_menu.dart @@ -2,6 +2,7 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/grid/application/grid_accessory_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/sort_menu.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -54,24 +55,27 @@ class _DatabaseViewSettingContent extends StatelessWidget { return BlocBuilder( builder: (context, state) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, + return Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.horizontalHeaderPadding, + ), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), ), ), - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - SortMenu(fieldController: fieldController), - const HSpace(6), - Expanded( - child: FilterMenu(fieldController: fieldController), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + SortMenu(fieldController: fieldController), + const HSpace(6), + FilterMenu(fieldController: fieldController), + ], + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart index 71b9fddda5..b928423310 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_add_button.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -31,22 +32,27 @@ class _AddDatabaseViewButtonState extends State { offset: const Offset(0, 8), margin: EdgeInsets.zero, triggerActions: PopoverTriggerFlags.none, - child: Padding( - padding: const EdgeInsetsDirectional.only( - top: 2.0, - bottom: 7.0, - start: 6.0, - ), - child: FlowyIconButton( - width: 26, - hoverColor: AFThemeExtension.of(context).greyHover, - onPressed: () => popoverController.show(), - radius: Corners.s4Border, - icon: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).hintColor, - ), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, + child: SizedBox( + height: 26, + child: Row( + children: [ + VerticalDivider( + width: 1.0, + thickness: 1.0, + indent: 4.0, + endIndent: 4.0, + color: Theme.of(context).dividerColor, + ), + FlowyIconButton( + width: 26, + iconPadding: const EdgeInsets.all(5), + hoverColor: AFThemeExtension.of(context).greyHover, + onPressed: () => popoverController.show(), + radius: Corners.s4Border, + icon: const FlowySvg(FlowySvgs.add_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + ), + ], ), ), popupBuilder: (BuildContext context) { @@ -102,7 +108,7 @@ class TabBarAddButtonActionCell extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( + text: FlowyText.medium( '${LocaleKeys.grid_createView.tr()} ${action.layoutName}', color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index fa5e44a5e6..54a08c1284 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -1,34 +1,31 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; import 'tab_bar_add_button.dart'; class TabBarHeader extends StatelessWidget { - const TabBarHeader({ - super.key, - }); + const TabBarHeader({super.key}); @override Widget build(BuildContext context) { - return SizedBox( - height: 35, + return Container( + height: 30, + padding: EdgeInsets.symmetric( + horizontal: GridSize.horizontalHeaderPadding + 40, + ), child: Stack( children: [ Positioned( @@ -36,7 +33,7 @@ class TabBarHeader extends StatelessWidget { left: 0, right: 0, child: Divider( - color: AFThemeExtension.of(context).borderColor, + color: Theme.of(context).dividerColor, height: 1, thickness: 1, ), @@ -44,18 +41,19 @@ class TabBarHeader extends StatelessWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Expanded( - child: DatabaseTabBar(), - ), - Flexible( - child: BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.only(top: 6.0), - child: pageSettingBarFromState(context, state), - ); - }, - ), + const Flexible(child: DatabaseTabBar()), + BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 200, + child: Column( + children: [ + const VSpace(3), + pageSettingBarFromState(context, state), + ], + ), + ); + }, ), ], ), @@ -98,36 +96,40 @@ class _DatabaseTabBarState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return ListView.separated( - controller: _scrollController, - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemCount: state.tabBars.length + 1, - itemBuilder: (context, index) => index == state.tabBars.length - ? AddDatabaseViewButton( - onTap: (layoutType) { - context - .read() - .add(DatabaseTabBarEvent.createView(layoutType, null)); - }, - ) - : DatabaseTabBarItem( - key: ValueKey(state.tabBars[index].viewId), - view: state.tabBars[index].view, - isSelected: state.selectedIndex == index, - onTap: (selectedView) { - context - .read() - .add(DatabaseTabBarEvent.selectView(selectedView.id)); - }, - ), - separatorBuilder: (context, index) => VerticalDivider( - width: 1.0, - thickness: 1.0, - indent: 8, - endIndent: 13, - color: Theme.of(context).dividerColor, - ), + final children = state.tabBars.indexed.map((indexed) { + final isSelected = state.selectedIndex == indexed.$1; + final tabBar = indexed.$2; + return DatabaseTabBarItem( + key: ValueKey(tabBar.viewId), + view: tabBar.view, + isSelected: isSelected, + onTap: (selectedView) { + context.read().add( + DatabaseTabBarEvent.selectView(selectedView.id), + ); + }, + ); + }).toList(); + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: ListView( + controller: _scrollController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + children: children, + ), + ), + AddDatabaseViewButton( + onTap: (layoutType) async { + context.read().add( + DatabaseTabBarEvent.createView(layoutType, null), + ); + }, + ), + ], ); }, ); @@ -152,15 +154,12 @@ class DatabaseTabBarItem extends StatelessWidget { constraints: const BoxConstraints(maxWidth: 160), child: Stack( children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: SizedBox( - height: 26, - child: TabBarItemButton( - view: view, - isSelected: isSelected, - onTap: () => onTap(view), - ), + SizedBox( + height: 26, + child: TabBarItemButton( + view: view, + isSelected: isSelected, + onTap: () => onTap(view), ), ), if (isSelected) @@ -180,7 +179,7 @@ class DatabaseTabBarItem extends StatelessWidget { } } -class TabBarItemButton extends StatefulWidget { +class TabBarItemButton extends StatelessWidget { const TabBarItemButton({ super.key, required this.view, @@ -192,167 +191,76 @@ class TabBarItemButton extends StatefulWidget { final bool isSelected; final VoidCallback onTap; - @override - State createState() => _TabBarItemButtonState(); -} - -class _TabBarItemButtonState extends State { - final menuController = PopoverController(); - final iconController = PopoverController(); - @override Widget build(BuildContext context) { - Color? color; - if (!widget.isSelected) { - color = Theme.of(context).hintColor; - } - if (Theme.of(context).brightness == Brightness.dark) { - color = null; - } - return AppFlowyPopover( - controller: menuController, - constraints: const BoxConstraints( - minWidth: 120, - maxWidth: 460, - maxHeight: 300, - ), + return PopoverActionList( direction: PopoverDirection.bottomWithCenterAligned, - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (_) { - return IntrinsicHeight( - child: IntrinsicWidth( - child: Column( - children: [ - ActionCellWidget( - action: TabBarViewAction.rename, - itemHeight: ActionListSizes.itemHeight, - onSelected: (action) { - NavigatorTextFieldDialog( - title: LocaleKeys.menuAppHeader_renameDialog.tr(), - value: widget.view.nameOrDefault, - onConfirm: (newValue, _) { - context.read().add( - DatabaseTabBarEvent.renameView( - widget.view.id, - newValue, - ), - ); - }, - ).show(context); - menuController.close(); - }, - ), - AppFlowyPopover( - controller: iconController, - direction: PopoverDirection.rightWithCenterAligned, - constraints: BoxConstraints.loose(const Size(364, 356)), - margin: const EdgeInsets.all(0), - child: ActionCellWidget( - action: TabBarViewAction.changeIcon, - itemHeight: ActionListSizes.itemHeight, - onSelected: (action) { - iconController.show(); - }, - ), - popupBuilder: (context) { - return FlowyIconEmojiPicker( - tabs: const [PickerTabType.icon], - enableBackgroundColorSelection: false, - onSelectedEmoji: (r) { - ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: r.data, - ); - if (!r.keepOpen) { - iconController.close(); - menuController.close(); - } - }, - ); - }, - ), - ActionCellWidget( - action: TabBarViewAction.delete, - itemHeight: ActionListSizes.itemHeight, - onSelected: (action) { - NavigatorAlertDialog( - title: LocaleKeys.grid_deleteView.tr(), - confirm: () { - context.read().add( - DatabaseTabBarEvent.deleteView(widget.view.id), - ); - }, - ).show(context); - menuController.close(); - }, - ), - ], + actions: TabBarViewAction.values, + buildChild: (controller) { + Color? color; + if (!isSelected) { + color = Theme.of(context).hintColor; + } + if (Theme.of(context).brightness == Brightness.dark) { + color = null; + } + return IntrinsicWidth( + child: FlowyButton( + radius: Corners.s6Border, + hoverColor: AFThemeExtension.of(context).greyHover, + onTap: onTap, + onSecondaryTap: () { + controller.show(); + }, + leftIcon: FlowySvg( + view.iconData, + size: const Size(14, 14), + color: color, + ), + text: FlowyText( + view.name, + fontSize: FontSizes.s11, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + color: color, + fontWeight: isSelected ? null : FontWeight.w400, ), ), ); }, - child: IntrinsicWidth( - child: FlowyButton( - radius: Corners.s6Border, - hoverColor: AFThemeExtension.of(context).greyHover, - onTap: () { - if (widget.isSelected) menuController.show(); - widget.onTap.call(); - }, - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - onSecondaryTap: () { - menuController.show(); - }, - leftIcon: _buildViewIcon(), - text: FlowyText( - widget.view.nameOrDefault, - lineHeight: 1.0, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - color: color, - fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400, - ), - ), - ), - ); - } + onSelected: (action, controller) { + switch (action) { + case TabBarViewAction.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.menuAppHeader_renameDialog.tr(), + value: view.name, + onConfirm: (newValue, _) { + context.read().add( + DatabaseTabBarEvent.renameView(view.id, newValue), + ); + }, + ).show(context); + break; + case TabBarViewAction.delete: + NavigatorAlertDialog( + title: LocaleKeys.grid_deleteView.tr(), + confirm: () { + context.read().add( + DatabaseTabBarEvent.deleteView(view.id), + ); + }, + ).show(context); - Widget _buildViewIcon() { - final iconData = widget.view.icon.toEmojiIconData(); - Widget icon; - if (iconData.isEmpty || iconData.type != FlowyIconType.icon) { - icon = widget.view.defaultIcon(); - } else { - icon = RawEmojiIconWidget( - emoji: iconData, - emojiSize: 14.0, - enableColor: false, - ); - } - final isReference = - Provider.of(context)?.isReference ?? false; - final iconWidget = Opacity(opacity: 0.6, child: icon); - return isReference - ? Stack( - children: [ - iconWidget, - const Positioned( - right: 0, - bottom: 0, - child: FlowySvg( - FlowySvgs.referenced_page_s, - blendMode: BlendMode.dstIn, - ), - ), - ], - ) - : iconWidget; + break; + } + controller.close(); + }, + ); } } enum TabBarViewAction implements ActionCell { rename, - changeIcon, delete; @override @@ -360,8 +268,6 @@ enum TabBarViewAction implements ActionCell { switch (this) { case TabBarViewAction.rename: return LocaleKeys.disclosureAction_rename.tr(); - case TabBarViewAction.changeIcon: - return LocaleKeys.disclosureAction_changeIcon.tr(); case TabBarViewAction.delete: return LocaleKeys.disclosureAction_delete.tr(); } @@ -371,8 +277,6 @@ enum TabBarViewAction implements ActionCell { switch (this) { case TabBarViewAction.rename: return const FlowySvg(FlowySvgs.edit_s); - case TabBarViewAction.changeIcon: - return const FlowySvg(FlowySvgs.change_icon_s); case TabBarViewAction.delete: return const FlowySvg(FlowySvgs.delete_s); } @@ -383,9 +287,4 @@ enum TabBarViewAction implements ActionCell { @override Widget? rightIcon(Color iconColor) => null; - - @override - Color? textColor(BuildContext context) { - return null; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index 3a1fcac510..ddddd90fb4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -1,22 +1,29 @@ +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/view/database_view_list.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobileTabBarHeader extends StatelessWidget { +import '../../grid/presentation/grid_page.dart'; + +class MobileTabBarHeader extends StatefulWidget { const MobileTabBarHeader({super.key}); + @override + State createState() => _MobileTabBarHeaderState(); +} + +class _MobileTabBarHeaderState extends State { @override Widget build(BuildContext context) { return Padding( @@ -43,16 +50,7 @@ class MobileTabBarHeader extends StatelessWidget { return MobileDatabaseControls( controller: state .tabBarControllerByViewId[currentView.viewId]!.controller, - features: switch (currentView.layout) { - ViewLayoutPB.Board || ViewLayoutPB.Calendar => [ - MobileDatabaseControlFeatures.filter, - ], - ViewLayoutPB.Grid => [ - MobileDatabaseControlFeatures.sort, - MobileDatabaseControlFeatures.filter, - ], - _ => [], - }, + toggleExtension: ToggleExtensionNotifier(), ); }, ), @@ -105,8 +103,8 @@ class _DatabaseViewSelectorButton extends StatelessWidget { const HSpace(6), Flexible( child: FlowyText.medium( - tabBar.view.nameOrDefault, - fontSize: 14, + tabBar.view.name, + fontSize: 13, overflow: TextOverflow.ellipsis, ), ), @@ -142,16 +140,14 @@ class _DatabaseViewSelectorButton extends StatelessWidget { } Widget _buildViewIconButton(BuildContext context, ViewPB view) { - final iconData = view.icon.toEmojiIconData(); - if (iconData.isEmpty || iconData.type != FlowyIconType.icon) { - return SizedBox.square( - dimension: 16.0, - child: view.defaultIcon(), - ); - } - return RawEmojiIconWidget( - emoji: iconData, - emojiSize: 16, - ); + return view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 16.0, + ) + : SizedBox.square( + dimension: 16.0, + child: view.defaultIcon(), + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 7c2dc40869..690bac3fab 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -1,34 +1,21 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; -import 'package:appflowy/plugins/shared/share/share_button.dart'; +import 'package:appflowy/plugins/database/widgets/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'desktop/tab_bar_header.dart'; import 'mobile/mobile_tab_bar_header.dart'; @@ -70,17 +57,11 @@ class DatabaseTabBarView extends StatefulWidget { super.key, required this.view, required this.shrinkWrap, - required this.showActions, this.initialRowId, - this.actionBuilder, - this.node, }); final ViewPB view; final bool shrinkWrap; - final BlockComponentActionBuilder? actionBuilder; - final bool showActions; - final Node? node; /// Used to open a Row on plugin load /// @@ -91,223 +72,105 @@ class DatabaseTabBarView extends StatefulWidget { } class _DatabaseTabBarViewState extends State { - bool enableCompactMode = false; - bool initialed = false; - StreamSubscription? compactModeSubscription; - - String get compactModeId => widget.node?.id ?? widget.view.id; - - @override - void initState() { - super.initState(); - if (widget.node != null) { - enableCompactMode = - widget.node!.attributes[DatabaseBlockKeys.enableCompactMode] ?? false; - setState(() { - initialed = true; - }); - } else { - fetchLocalCompactMode(compactModeId).then((v) { - if (mounted) { - setState(() { - enableCompactMode = v; - initialed = true; - }); - } - }); - compactModeSubscription = - compactModeEventBus.on().listen((event) { - if (event.id != widget.view.id) return; - updateLocalCompactMode(event.enable); - }); - } - } + final PageController _pageController = PageController(); + late String? _initialRowId = widget.initialRowId; @override void dispose() { + _pageController.dispose(); super.dispose(); - compactModeSubscription?.cancel(); } @override Widget build(BuildContext context) { - if (!initialed) return Center(child: CircularProgressIndicator()); return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => DatabaseTabBarBloc( - view: widget.view, - compactModeId: compactModeId, - enableCompactMode: enableCompactMode, - )..add(const DatabaseTabBarEvent.initial()), + create: (context) => DatabaseTabBarBloc(view: widget.view) + ..add(const DatabaseTabBarEvent.initial()), ), BlocProvider( - create: (_) => ViewBloc(view: widget.view) - ..add( - const ViewEvent.initial(), - ), + create: (context) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), ), ], - child: BlocBuilder( - builder: (innerContext, state) { - final layout = state.tabBars[state.selectedIndex].layout; - final isCalendar = layout == ViewLayoutPB.Calendar; - final horizontalPadding = - context.read().horizontalPadding; - final showActionWrapper = widget.showActions && - widget.actionBuilder != null && - widget.node != null; - final Widget child = Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - if (UniversalPlatform.isMobile) const VSpace(12), - ValueListenableBuilder( - valueListenable: state - .tabBarControllerByViewId[state.parentView.id]! - .controller - .isLoading, - builder: (_, value, ___) { - if (value) { - return const SizedBox.shrink(); - } + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => p.selectedIndex != c.selectedIndex, + listener: (context, state) { + _initialRowId = null; + _pageController.jumpToPage(state.selectedIndex); + }, + ), + ], + child: Column( + children: [ + if (PlatformExtension.isMobile) const VSpace(12), + BlocBuilder( + builder: (context, state) { + return ValueListenableBuilder( + valueListenable: state + .tabBarControllerByViewId[state.parentView.id]! + .controller + .isLoading, + builder: (_, value, ___) { + if (value) { + return const SizedBox.shrink(); + } - Widget child = UniversalPlatform.isDesktop - ? const TabBarHeader() - : const MobileTabBarHeader(); - - if (innerContext.watch().state.view.isLocked) { - child = IgnorePointer( - child: child, - ); - } - - if (showActionWrapper) { - child = BlockComponentActionWrapper( - node: widget.node!, - actionBuilder: widget.actionBuilder!, - child: Padding( - padding: EdgeInsets.only(right: horizontalPadding), - child: child, - ), - ); - } - - if (UniversalPlatform.isDesktop) { - child = Container( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - child: child, - ); - } - - return child; - }, - ), - pageSettingBarExtensionFromState(context, state), - wrapContent( - layout: layout, - child: Padding( - padding: - (isCalendar && widget.shrinkWrap || showActionWrapper) - ? EdgeInsets.only(left: 42 - horizontalPadding) - : EdgeInsets.zero, - child: pageContentFromState(context, state), + return PlatformExtension.isDesktop + ? const TabBarHeader() + : const MobileTabBarHeader(); + }, + ); + }, + ), + BlocBuilder( + builder: (context, state) => + pageSettingBarExtensionFromState(state), + ), + Expanded( + child: BlocBuilder( + builder: (context, state) => PageView( + pageSnapping: false, + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + children: pageContentFromState(state), ), ), - ], - ); - - return child; - }, + ), + ], + ), ), ); } - Future fetchLocalCompactMode(String compactModeId) async { - Set compactModeIds = {}; - try { - final localIds = await getIt().get( - KVKeys.compactModeIds, + List pageContentFromState(DatabaseTabBarState state) { + return state.tabBars.map((tabBar) { + final controller = + state.tabBarControllerByViewId[tabBar.viewId]!.controller; + + return tabBar.builder.content( + context, + tabBar.view, + controller, + widget.shrinkWrap, + _initialRowId, ); - final List decodedList = jsonDecode(localIds ?? ''); - compactModeIds = Set.from(decodedList.map((item) => item as String)); - } catch (e) { - Log.warn('fetch local compact mode from id :$compactModeId failed', e); - } - return compactModeIds.contains(compactModeId); + }).toList(); } - Future updateLocalCompactMode(bool enableCompactMode) async { - Set compactModeIds = {}; - try { - final localIds = await getIt().get( - KVKeys.compactModeIds, - ); - final List decodedList = jsonDecode(localIds ?? ''); - compactModeIds = Set.from(decodedList.map((item) => item as String)); - } catch (e) { - Log.warn('get compact mode ids failed', e); - } - if (enableCompactMode) { - compactModeIds.add(compactModeId); - } else { - compactModeIds.remove(compactModeId); - } - await getIt().set( - KVKeys.compactModeIds, - jsonEncode(compactModeIds.toList()), - ); - } - - Widget wrapContent({required ViewLayoutPB layout, required Widget child}) { - if (widget.shrinkWrap) { - if (layout.shrinkWrappable) { - return child; - } - - return SizedBox( - height: layout.pluginHeight, - child: child, - ); - } - - return Expanded(child: child); - } - - Widget pageContentFromState(BuildContext context, DatabaseTabBarState state) { - final tab = state.tabBars[state.selectedIndex]; - final controller = state.tabBarControllerByViewId[tab.viewId]!.controller; - - return tab.builder.content( - context, - tab.view, - controller, - widget.shrinkWrap, - widget.initialRowId, - ); - } - - Widget pageSettingBarExtensionFromState( - BuildContext context, - DatabaseTabBarState state, - ) { + Widget pageSettingBarExtensionFromState(DatabaseTabBarState state) { if (state.tabBars.length < state.selectedIndex) { return const SizedBox.shrink(); } final tabBar = state.tabBars[state.selectedIndex]; final controller = state.tabBarControllerByViewId[tabBar.viewId]!.controller; - return Padding( - padding: EdgeInsets.symmetric( - horizontal: - context.read().horizontalPadding, - ), - child: tabBar.builder.settingBarExtension( - context, - controller, - ), + return tabBar.builder.settingBarExtension( + context, + controller, ); } } @@ -356,21 +219,6 @@ class DatabaseTabBarViewPlugin extends Plugin { } } -const kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding'; -const kDatabasePluginWidgetBuilderShowActions = 'show_actions'; -const kDatabasePluginWidgetBuilderActionBuilder = 'action_builder'; -const kDatabasePluginWidgetBuilderNode = 'node'; - -class DatabasePluginWidgetBuilderSize { - const DatabasePluginWidgetBuilderSize({ - required this.horizontalPadding, - this.verticalPadding = 16.0, - }); - - final double horizontalPadding; - final double verticalPadding; -} - class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { DatabasePluginWidgetBuilder({ required this.bloc, @@ -385,22 +233,17 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { /// final String? initialRowId; - @override - String? get viewName => notifier.view.nameOrDefault; - @override Widget get leftBarItem => ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => - ViewTabBarItem(view: notifier.view, shortForm: shortForm); + Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @override Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) { notifier.isDeleted.addListener(() { final deletedView = notifier.isDeleted.value; @@ -409,28 +252,11 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { } }); - final horizontalPadding = - data?[kDatabasePluginWidgetBuilderHorizontalPadding] as double? ?? - GridSize.horizontalHeaderPadding + 40; - final BlockComponentActionBuilder? actionBuilder = - data?[kDatabasePluginWidgetBuilderActionBuilder]; - final bool showActions = - data?[kDatabasePluginWidgetBuilderShowActions] ?? false; - final Node? node = data?[kDatabasePluginWidgetBuilderNode]; - - return Provider( - create: (context) => DatabasePluginWidgetBuilderSize( - horizontalPadding: horizontalPadding, - ), - child: DatabaseTabBarView( - key: ValueKey(notifier.view.id), - view: notifier.view, - shrinkWrap: shrinkWrap, - initialRowId: initialRowId, - actionBuilder: actionBuilder, - showActions: showActions, - node: node, - ), + return DatabaseTabBarView( + key: ValueKey(notifier.view.id), + view: notifier.view, + shrinkWrap: shrinkWrap, + initialRowId: initialRowId, ); } @@ -444,11 +270,11 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { value: bloc, child: Row( children: [ - ShareButton(key: ValueKey(view.id), view: view), + DatabaseShareButton(key: ValueKey(view.id), view: view), const HSpace(10), ViewFavoriteButton(view: view), const HSpace(4), - MoreViewActions(view: view), + MoreViewActions(view: view, isDocument: false), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index 68c4b15d5c..d5c73a1179 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -1,26 +1,20 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; -import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/shared/flowy_gradient_colors.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'card_bloc.dart'; import '../cell/card_cell_builder.dart'; import '../cell/card_cell_skeleton/card_cell.dart'; -import 'card_bloc.dart'; import 'container/accessory.dart'; import 'container/card_container.dart'; @@ -41,8 +35,6 @@ class RowCard extends StatefulWidget { this.onShiftTap, this.groupingFieldId, this.groupId, - required this.userProfile, - this.isCompact = false, }); final FieldController fieldController; @@ -70,14 +62,6 @@ class RowCard extends StatefulWidget { final RowCardStyleConfiguration styleConfiguration; - /// Specifically the token is used to handle requests to retrieve images - /// from cloud storage, such as the card cover. - final UserProfilePB? userProfile; - - /// Whether the card is in a narrow space. - /// This is used to determine eg. the Cover height. - final bool isCompact; - @override State createState() => _RowCardState(); } @@ -89,18 +73,13 @@ class _RowCardState extends State { @override void initState() { super.initState(); - final rowController = RowController( - viewId: widget.viewId, - rowMeta: widget.rowMeta, - rowCache: widget.rowCache, - ); - _cardBloc = CardBloc( fieldController: widget.fieldController, viewId: widget.viewId, groupFieldId: widget.groupingFieldId, isEditing: widget.isEditing, - rowController: rowController, + rowMeta: widget.rowMeta, + rowCache: widget.rowCache, )..add(const CardEvent.initial()); } @@ -122,7 +101,7 @@ class _RowCardState extends State { Widget build(BuildContext context) { return BlocProvider.value( value: _cardBloc, - child: BlocListener( + child: BlocConsumer( listenWhen: (previous, current) => previous.isEditing != current.isEditing, listener: (context, state) { @@ -130,30 +109,26 @@ class _RowCardState extends State { widget.onEndEditing(); } }, - child: UniversalPlatform.isMobile ? _mobile() : _desktop(), + builder: (context, state) => + PlatformExtension.isMobile ? _mobile(state) : _desktop(state), ), ); } - Widget _mobile() { - return BlocBuilder( - builder: (context, state) { - return GestureDetector( - onTap: () => widget.onTap(context), - behavior: HitTestBehavior.opaque, - child: MobileCardContent( - userProfile: widget.userProfile, - rowMeta: state.rowMeta, - cellBuilder: widget.cellBuilder, - styleConfiguration: widget.styleConfiguration, - cells: state.cells, - ), - ); - }, + Widget _mobile(CardState state) { + return GestureDetector( + onTap: () => widget.onTap(context), + behavior: HitTestBehavior.opaque, + child: MobileCardContent( + rowMeta: state.rowMeta, + cellBuilder: widget.cellBuilder, + styleConfiguration: widget.styleConfiguration, + cells: state.cells, + ), ); } - Widget _desktop() { + Widget _desktop(CardState state) { final accessories = widget.styleConfiguration.showAccessory ? const [ EditCardAccessory(), @@ -165,34 +140,25 @@ class _RowCardState extends State { triggerActions: PopoverTriggerFlags.none, constraints: BoxConstraints.loose(const Size(140, 200)), direction: PopoverDirection.rightWithCenterAligned, - popupBuilder: (_) => RowActionMenu.board( - viewId: _cardBloc.viewId, - rowId: _cardBloc.rowController.rowId, - groupId: widget.groupId, - ), - child: Builder( - builder: (context) { - return RowCardContainer( - buildAccessoryWhen: () => - !context.watch().state.isEditing, - accessories: accessories ?? [], - openAccessory: _handleOpenAccessory, - onTap: widget.onTap, - onShiftTap: widget.onShiftTap, - child: BlocBuilder( - builder: (context, state) { - return _CardContent( - rowMeta: state.rowMeta, - cellBuilder: widget.cellBuilder, - styleConfiguration: widget.styleConfiguration, - cells: state.cells, - userProfile: widget.userProfile, - isCompact: widget.isCompact, - ); - }, - ), - ); - }, + popupBuilder: (_) { + return RowActionMenu.board( + viewId: _cardBloc.viewId, + rowId: _cardBloc.rowId, + groupId: widget.groupId, + ); + }, + child: RowCardContainer( + buildAccessoryWhen: () => state.isEditing == false, + accessories: accessories ?? [], + openAccessory: _handleOpenAccessory, + onTap: widget.onTap, + onShiftTap: widget.onShiftTap, + child: _CardContent( + rowMeta: state.rowMeta, + cellBuilder: widget.cellBuilder, + styleConfiguration: widget.styleConfiguration, + cells: state.cells, + ), ), ); } @@ -215,35 +181,21 @@ class _CardContent extends StatelessWidget { required this.cellBuilder, required this.cells, required this.styleConfiguration, - this.userProfile, - this.isCompact = false, }); final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; final List cells; final RowCardStyleConfiguration styleConfiguration; - final UserProfilePB? userProfile; - final bool isCompact; @override Widget build(BuildContext context) { - final child = Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - CardCover( - cover: rowMeta.cover, - userProfile: userProfile, - isCompact: isCompact, - ), - Padding( - padding: styleConfiguration.cardPadding, - child: Column( - children: _makeCells(context, rowMeta, cells), - ), - ), - ], + final child = Padding( + padding: styleConfiguration.cardPadding, + child: Column( + mainAxisSize: MainAxisSize.min, + children: _makeCells(context, rowMeta, cells), + ), ); return styleConfiguration.hoverStyle == null ? child @@ -259,164 +211,25 @@ class _CardContent extends StatelessWidget { RowMetaPB rowMeta, List cells, ) { - return cells - .mapIndexed( - (int index, CellMeta cellMeta) => _CardContentCell( - cellBuilder: cellBuilder, - cellMeta: cellMeta, - rowMeta: rowMeta, - isTitle: index == 0, - styleMap: styleConfiguration.cellStyleMap, - ), - ) - .toList(); - } -} + return cells.mapIndexed((int index, CellMeta cellMeta) { + EditableCardNotifier? cellNotifier; -class _CardContentCell extends StatefulWidget { - const _CardContentCell({ - required this.cellBuilder, - required this.cellMeta, - required this.rowMeta, - required this.isTitle, - required this.styleMap, - }); + if (index == 0) { + final bloc = context.read(); + cellNotifier = EditableCardNotifier(isEditing: bloc.state.isEditing); + cellNotifier.isCellEditing.addListener(() { + final isEditing = cellNotifier!.isCellEditing.value; + bloc.add(CardEvent.setIsEditing(isEditing)); + }); + } - final CellMeta cellMeta; - final RowMetaPB rowMeta; - final CardCellBuilder cellBuilder; - final CardCellStyleMap styleMap; - final bool isTitle; - - @override - State<_CardContentCell> createState() => _CardContentCellState(); -} - -class _CardContentCellState extends State<_CardContentCell> { - late final EditableCardNotifier? cellNotifier; - - @override - void initState() { - super.initState(); - cellNotifier = widget.isTitle ? EditableCardNotifier() : null; - cellNotifier?.isCellEditing.addListener(listener); - } - - void listener() { - final isEditing = cellNotifier!.isCellEditing.value; - context.read().add(CardEvent.setIsEditing(isEditing)); - } - - @override - void dispose() { - cellNotifier?.isCellEditing.removeListener(listener); - cellNotifier?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listenWhen: (previous, current) => - previous.isEditing != current.isEditing, - listener: (context, state) { - cellNotifier?.isCellEditing.value = state.isEditing; - }, - child: widget.cellBuilder.build( - cellContext: widget.cellMeta.cellContext(), - styleMap: widget.styleMap, + return cellBuilder.build( + cellContext: cellMeta.cellContext(), cellNotifier: cellNotifier, - hasNotes: !widget.rowMeta.isDocumentEmpty, - ), - ); - } -} - -class CardCover extends StatelessWidget { - const CardCover({ - super.key, - this.cover, - this.userProfile, - this.isCompact = false, - }); - - final RowCoverPB? cover; - final UserProfilePB? userProfile; - final bool isCompact; - - @override - Widget build(BuildContext context) { - if (cover == null || - cover!.data.isEmpty || - cover!.uploadType == FileUploadTypePB.CloudFile && - userProfile == null) { - return const SizedBox.shrink(); - } - - return Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(4), - ), - color: Theme.of(context).cardColor, - ), - child: Row( - children: [ - Expanded(child: _renderCover(context, cover!)), - ], - ), - ); - } - - Widget _renderCover(BuildContext context, RowCoverPB cover) { - final height = isCompact ? 50.0 : 100.0; - - if (cover.coverType == CoverTypePB.FileCover) { - return SizedBox( - height: height, - width: double.infinity, - child: AFImage( - url: cover.data, - uploadType: cover.uploadType, - userProfile: userProfile, - ), + styleMap: styleConfiguration.cellStyleMap, + hasNotes: !rowMeta.isDocumentEmpty, ); - } - - if (cover.coverType == CoverTypePB.AssetCover) { - return SizedBox( - height: height, - width: double.infinity, - child: Image.asset( - PageStyleCoverImageType.builtInImagePath(cover.data), - fit: BoxFit.cover, - ), - ); - } - - if (cover.coverType == CoverTypePB.ColorCover) { - final color = FlowyTint.fromId(cover.data)?.color(context) ?? - cover.data.tryToColor(); - return Container( - height: height, - width: double.infinity, - color: color, - ); - } - - if (cover.coverType == CoverTypePB.GradientCover) { - return Container( - height: height, - width: double.infinity, - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(cover.data).linear, - ), - ); - } - - return const SizedBox.shrink(); + }).toList(); } } @@ -460,7 +273,7 @@ class RowCardStyleConfiguration { const RowCardStyleConfiguration({ required this.cellStyleMap, this.showAccessory = true, - this.cardPadding = const EdgeInsets.all(4), + this.cardPadding = const EdgeInsets.all(8), this.hoverStyle, }); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart index 04f9bb652c..5bd4d6f505 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/foundation.dart'; @@ -8,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; +import 'package:appflowy/plugins/database/domain/row_listener.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -19,36 +19,42 @@ class CardBloc extends Bloc { required this.fieldController, required this.groupFieldId, required this.viewId, + required RowMetaPB rowMeta, + required RowCache rowCache, required bool isEditing, - required this.rowController, - }) : super( + }) : rowId = rowMeta.id, + _rowListener = RowListener(rowMeta.id), + _rowCache = rowCache, + super( CardState.initial( + rowMeta, _makeCells( fieldController, groupFieldId, - rowController, + rowCache.loadCells(rowMeta), ), isEditing, - rowController.rowMeta, ), ) { - rowController.initialize(); _dispatch(); } final FieldController fieldController; + final String rowId; final String? groupFieldId; + final RowCache _rowCache; final String viewId; - final RowController rowController; + final RowListener _rowListener; VoidCallback? _rowCallback; @override Future close() async { if (_rowCallback != null) { + _rowCache.removeRowListener(_rowCallback!); _rowCallback = null; } - await rowController.dispose(); + await _rowListener.stop(); return super.close(); } @@ -68,9 +74,7 @@ class CardBloc extends Bloc { ); }, setIsEditing: (bool isEditing) { - if (isEditing != state.isEditing) { - emit(state.copyWith(isEditing: isEditing)); - } + emit(state.copyWith(isEditing: isEditing)); }, didUpdateRowMeta: (rowMeta) { emit(state.copyWith(rowMeta: rowMeta)); @@ -81,17 +85,20 @@ class CardBloc extends Bloc { } Future _startListening() async { - rowController.addListener( + _rowCallback = _rowCache.addListener( + rowId: rowId, onRowChanged: (cellMap, reason) { if (!isClosed) { - final cells = - _makeCells(fieldController, groupFieldId, rowController); + final cells = _makeCells(fieldController, groupFieldId, cellMap); add(CardEvent.didReceiveCells(cells, reason)); } }, - onMetaChanged: () { + ); + + _rowListener.start( + onMetaChanged: (rowMeta) { if (!isClosed) { - add(CardEvent.didUpdateRowMeta(rowController.rowMeta)); + add(CardEvent.didUpdateRowMeta(rowMeta)); } }, ); @@ -101,18 +108,16 @@ class CardBloc extends Bloc { List _makeCells( FieldController fieldController, String? groupFieldId, - RowController rowController, + List cellContexts, ) { // Only show the non-hidden cells and cells that aren't of the grouping field - final cellContext = rowController.loadCells(); - - cellContext.removeWhere((cellContext) { + cellContexts.removeWhere((cellContext) { final fieldInfo = fieldController.getField(cellContext.fieldId); return fieldInfo == null || !(fieldInfo.visibility?.isVisibleState() ?? false) || (groupFieldId != null && cellContext.fieldId == groupFieldId); }); - return cellContext + return cellContexts .map( (cellCtx) => CellMeta( fieldId: cellCtx.fieldId, @@ -152,19 +157,19 @@ class CellMeta with _$CellMeta { class CardState with _$CardState { const factory CardState({ required List cells, - required bool isEditing, required RowMetaPB rowMeta, + required bool isEditing, ChangedReason? changeReason, }) = _RowCardState; factory CardState.initial( + RowMetaPB rowMeta, List cells, bool isEditing, - RowMetaPB rowMeta, ) => CardState( cells: cells, - isEditing: isEditing, rowMeta: rowMeta, + isEditing: isEditing, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart index e74f947b46..56045d57d6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/accessory.dart @@ -1,6 +1,7 @@ -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; + enum AccessoryType { edit, more, @@ -23,10 +24,6 @@ class CardAccessoryContainer extends StatelessWidget { @override Widget build(BuildContext context) { - if (accessories.isEmpty) { - return const SizedBox.shrink(); - } - final children = accessories.map((accessory) { return GestureDetector( behavior: HitTestBehavior.opaque, @@ -44,7 +41,7 @@ class CardAccessoryContainer extends StatelessWidget { width: 1, thickness: 1, color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withValues(alpha: 0.12) + ? const Color(0xFF1F2329).withOpacity(0.12) : const Color(0xff59647a), ), ); @@ -76,19 +73,19 @@ class CardAccessoryContainer extends StatelessWidget { border: Border.fromBorderSide( BorderSide( color: Theme.of(context).brightness == Brightness.light - ? const Color(0xFF1F2329).withValues(alpha: 0.12) + ? const Color(0xFF1F2329).withOpacity(0.12) : const Color(0xff59647a), ), ), boxShadow: [ BoxShadow( blurRadius: 4, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), ), BoxShadow( blurRadius: 4, spreadRadius: -2, - color: const Color(0xFF1F2329).withValues(alpha: 0.02), + color: const Color(0xFF1F2329).withOpacity(0.02), ), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart index a91ffae42d..3584f2fce0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/container/card_container.dart @@ -28,7 +28,19 @@ class RowCardContainer extends StatelessWidget { create: (_) => _CardContainerNotifier(), child: Consumer<_CardContainerNotifier>( builder: (context, notifier, _) { - final shouldBuildAccessory = buildAccessoryWhen?.call() ?? true; + Widget container = Center(child: child); + bool shouldBuildAccessory = true; + if (buildAccessoryWhen != null) { + shouldBuildAccessory = buildAccessoryWhen!.call(); + } + + if (shouldBuildAccessory && accessories.isNotEmpty) { + container = _CardEnterRegion( + accessories: accessories, + onTapAccessory: openAccessory, + child: container, + ); + } return GestureDetector( behavior: HitTestBehavior.opaque, @@ -40,13 +52,8 @@ class RowCardContainer extends StatelessWidget { } }, child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 36), - child: _CardEnterRegion( - shouldBuildAccessory: shouldBuildAccessory, - accessories: accessories, - onTapAccessory: openAccessory, - child: child, - ), + constraints: const BoxConstraints(minHeight: 30), + child: container, ), ); }, @@ -57,13 +64,11 @@ class RowCardContainer extends StatelessWidget { class _CardEnterRegion extends StatelessWidget { const _CardEnterRegion({ - required this.shouldBuildAccessory, required this.child, required this.accessories, required this.onTapAccessory, }); - final bool shouldBuildAccessory; final Widget child; final List accessories; final void Function(AccessoryType) onTapAccessory; @@ -73,18 +78,19 @@ class _CardEnterRegion extends StatelessWidget { return Selector<_CardContainerNotifier, bool>( selector: (context, notifier) => notifier.onEnter, builder: (context, onEnter, _) { - final List children = [ - child, - if (onEnter && shouldBuildAccessory) + final List children = [child]; + if (onEnter) { + children.add( Positioned( - top: 7.0, - right: 7.0, + top: 10.0, + right: 10.0, child: CardAccessoryContainer( accessories: accessories, onTapAccessory: onTapAccessory, ), ), - ]; + ); + } return MouseRegion( cursor: SystemMouseCursors.click, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart index d17c522de6..aff11f6584 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart @@ -8,7 +8,6 @@ import 'card_cell_skeleton/card_cell.dart'; import 'card_cell_skeleton/checkbox_card_cell.dart'; import 'card_cell_skeleton/checklist_card_cell.dart'; import 'card_cell_skeleton/date_card_cell.dart'; -import 'card_cell_skeleton/media_card_cell.dart'; import 'card_cell_skeleton/number_card_cell.dart'; import 'card_cell_skeleton/relation_card_cell.dart'; import 'card_cell_skeleton/select_option_card_cell.dart'; @@ -114,12 +113,6 @@ class CardCellBuilder { databaseController: databaseController, cellContext: cellContext, ), - FieldType.Media => MediaCardCell( - key: key, - style: isStyleOrNull(style), - databaseController: databaseController, - cellContext: cellContext, - ), _ => throw UnimplementedError, }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart index c459d8cc60..3b47971fdb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/date_card_cell.dart @@ -1,13 +1,10 @@ -import 'package:appflowy/generated/flowy_svgs.g.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/application/cell/bloc/date_cell_bloc.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../editable_cell_skeleton/date.dart'; import 'card_cell.dart'; class DateCardCellStyle extends CardCellStyle { @@ -47,33 +44,18 @@ class _DateCellState extends State { ); }, child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, builder: (context, state) { - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); - - if (dateStr.isEmpty) { + if (state.dateStr.isEmpty) { return const SizedBox.shrink(); } return Container( alignment: Alignment.centerLeft, padding: widget.style.padding, - child: Row( - children: [ - Flexible( - child: Text( - dateStr, - style: widget.style.textStyle, - overflow: TextOverflow.ellipsis, - ), - ), - if (state.cellData.reminderId.isNotEmpty) ...[ - const HSpace(4), - const FlowySvg(FlowySvgs.clock_alarm_s), - ], - ], + child: Text( + state.dateStr, + style: widget.style.textStyle, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart deleted file mode 100644 index 969f80d17b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter/widgets.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/media_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/widgets/cell/card_cell_skeleton/card_cell.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'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MediaCardCellStyle extends CardCellStyle { - const MediaCardCellStyle({ - required super.padding, - required this.textStyle, - }); - - final TextStyle textStyle; -} - -class MediaCardCell extends CardCell { - const MediaCardCell({ - super.key, - required super.style, - required this.databaseController, - required this.cellContext, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - - @override - State createState() => _MediaCellState(); -} - -class _MediaCellState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => MediaCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - )..add(const MediaCellEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - if (state.files.isEmpty) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.only(left: 4), - 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: ['${state.files.length}']), - fontSize: 12, - color: AFThemeExtension.of(context).secondaryTextColor, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart index a3758029d1..b86ff495cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart @@ -1,15 +1,14 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/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/application/cell/bloc/text_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_builder.dart'; @@ -62,8 +61,14 @@ class _TextCellState extends State { @override void initState() { super.initState(); - _textEditingController = - TextEditingController(text: cellBloc.state.content); + + _textEditingController = TextEditingController(text: cellBloc.state.content) + ..addListener(() { + if (_textEditingController.value.composing.isCollapsed) { + cellBloc + .add(TextCellEvent.updateText(_textEditingController.value.text)); + } + }); if (widget.editableNotifier?.isCellEditing.value ?? false) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -75,18 +80,15 @@ class _TextCellState extends State { // If the focusNode lost its focus, the widget's editableNotifier will // set to false, which will cause the [EditableRowNotifier] to receive // end edit event. - focusNode.addListener(_onFocusChanged); + focusNode.addListener(() { + if (!focusNode.hasFocus) { + widget.editableNotifier?.isCellEditing.value = false; + cellBloc.add(const TextCellEvent.enableEdit(false)); + } + }); _bindEditableNotifier(); } - void _onFocusChanged() { - if (!focusNode.hasFocus) { - widget.editableNotifier?.isCellEditing.value = false; - cellBloc.add(const TextCellEvent.enableEdit(false)); - cellBloc.add(TextCellEvent.updateText(_textEditingController.text)); - } - } - void _bindEditableNotifier() { widget.editableNotifier?.isCellEditing.addListener(() { if (!mounted) { @@ -95,8 +97,9 @@ class _TextCellState extends State { final isEditing = widget.editableNotifier?.isCellEditing.value ?? false; if (isEditing) { - WidgetsBinding.instance - .addPostFrameCallback((_) => focusNode.requestFocus()); + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); } cellBloc.add(TextCellEvent.enableEdit(isEditing)); }); @@ -118,7 +121,9 @@ class _TextCellState extends State { child: BlocListener( listenWhen: (previous, current) => previous.content != current.content, listener: (context, state) { - _textEditingController.text = state.content ?? ""; + if (!state.enableEdit) { + _textEditingController.text = state.content; + } }, child: isTitle ? _buildTitle() : _buildText(), ), @@ -136,22 +141,18 @@ class _TextCellState extends State { } Widget? _buildIcon(TextCellState state) { - if (state.emoji?.value.isNotEmpty ?? false) { - return FlowyText.emoji( - optimizeEmojiAlign: true, - state.emoji?.value ?? '', + if (state.emoji.isNotEmpty) { + return Text( + state.emoji, + style: widget.style.titleTextStyle, ); } - if (widget.showNotes) { return FlowyTooltip( message: LocaleKeys.board_notesTooltip.tr(), - child: Padding( - padding: const EdgeInsets.all(1.0), - child: FlowySvg( - FlowySvgs.notes_s, - color: Theme.of(context).hintColor, - ), + child: FlowySvg( + FlowySvgs.notes_s, + color: Theme.of(context).hintColor, ), ); } @@ -161,7 +162,7 @@ class _TextCellState extends State { Widget _buildText() { return BlocBuilder( builder: (context, state) { - final content = state.content ?? ""; + final content = state.content; return content.isEmpty ? const SizedBox.shrink() @@ -183,23 +184,12 @@ class _TextCellState extends State { return BlocBuilder( builder: (context, state) { final icon = _buildIcon(state); - if (icon == null) { - return textField; - } - final resolved = - widget.style.padding.resolve(Directionality.of(context)); - final padding = EdgeInsetsDirectional.only( - start: resolved.left, - top: resolved.top, - bottom: resolved.bottom, - ); return Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - padding: padding, - child: icon, - ), + if (icon != null) ...[ + icon, + const HSpace(4.0), + ], Expanded(child: textField), ], ); @@ -217,22 +207,20 @@ class _TextCellState extends State { bindings: { const SingleActivator(LogicalKeyboardKey.escape): () => focusNode.unfocus(), - const SimpleActivator(LogicalKeyboardKey.enter): () => - focusNode.unfocus(), }, child: TextField( controller: _textEditingController, focusNode: focusNode, onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: null, + maxLines: isEditing ? null : 2, minLines: 1, textInputAction: TextInputAction.done, readOnly: !isEditing, enableInteractiveSelection: isEditing, style: widget.style.titleTextStyle, decoration: InputDecoration( - contentPadding: widget.style.padding, + contentPadding: widget.style.padding + .add(const EdgeInsets.symmetric(vertical: 4.0)), border: InputBorder.none, enabledBorder: InputBorder.none, isDense: true, @@ -250,27 +238,3 @@ class _TextCellState extends State { ); } } - -class SimpleActivator with Diagnosticable implements ShortcutActivator { - const SimpleActivator( - this.trigger, { - this.includeRepeats = true, - }); - - final LogicalKeyboardKey trigger; - final bool includeRepeats; - - @override - bool accepts(KeyEvent event, HardwareKeyboard state) { - return (event is KeyDownEvent || - (includeRepeats && event is KeyRepeatEvent)) && - trigger == event.logicalKey; - } - - @override - String debugDescribeKeys() => - kDebugMode ? trigger.debugName ?? trigger.toStringShort() : ''; - - @override - Iterable? get triggers => [trigger]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart index 23a8a2451f..df7bb72f60 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/calendar_card_cell_style.dart @@ -1,19 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../card_cell_builder.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart'; import '../card_cell_skeleton/checklist_card_cell.dart'; import '../card_cell_skeleton/date_card_cell.dart'; -import '../card_cell_skeleton/media_card_cell.dart'; import '../card_cell_skeleton/number_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart'; -import '../card_cell_skeleton/summary_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart'; -import '../card_cell_skeleton/translate_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart'; CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) { @@ -91,9 +90,5 @@ CardCellStyleMap desktopCalendarCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Media: MediaCardCellStyle( - padding: padding, - textStyle: textStyle, - ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index 6264fea958..7fcb289c1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -1,20 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; +import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../card_cell_builder.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart'; import '../card_cell_skeleton/checklist_card_cell.dart'; import '../card_cell_skeleton/date_card_cell.dart'; -import '../card_cell_skeleton/media_card_cell.dart'; import '../card_cell_skeleton/number_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart'; -import '../card_cell_skeleton/summary_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart'; import '../card_cell_skeleton/time_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart'; -import '../card_cell_skeleton/translate_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart'; CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { @@ -22,6 +21,7 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: 11, overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w400, ); return { @@ -95,9 +95,5 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Media: MediaCardCellStyle( - padding: padding, - textStyle: textStyle, - ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart index 93d98f013e..3911678176 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/media_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -95,9 +94,5 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), - FieldType.Media: MediaCardCellStyle( - padding: padding, - textStyle: textStyle, - ), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart index 74abcecb3a..befe49dd6d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checkbox_cell.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,31 +12,22 @@ class DesktopGridCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Container( - alignment: AlignmentDirectional.centerStart, - padding: padding, - child: FlowyIconButton( - hoverColor: Colors.transparent, - onPressed: () => bloc.add(const CheckboxCellEvent.select()), - icon: FlowySvg( - state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - size: const Size.square(20), - ), - width: 20, - ), - ); - }, + return Container( + alignment: AlignmentDirectional.centerStart, + padding: GridSize.cellContentInsets, + child: FlowyIconButton( + hoverColor: Colors.transparent, + onPressed: () => bloc.add(const CheckboxCellEvent.select()), + icon: FlowySvg( + state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, + blendMode: BlendMode.dst, + size: const Size.square(20), + ), + width: 20, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart index ebc4a6f976..285b0217eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_checklist_cell.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/checklist.dart'; @@ -14,8 +16,8 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, + ChecklistCellState state, PopoverController popoverController, ) { return AppFlowyPopover( @@ -25,7 +27,7 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { direction: PopoverDirection.bottomWithLeftAligned, triggerActions: PopoverTriggerFlags.none, skipTraversal: true, - popupBuilder: (popoverContext) { + popupBuilder: (BuildContext popoverContext) { WidgetsBinding.instance.addPostFrameCallback((_) { cellContainerNotifier.isFocus = true; }); @@ -37,28 +39,15 @@ class DesktopGridChecklistCellSkin extends IEditableChecklistCellSkin { ); }, onClose: () => cellContainerNotifier.isFocus = false, - child: BlocBuilder( - builder: (context, state) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - - return Container( - alignment: AlignmentDirectional.centerStart, - padding: padding, - child: state.tasks.isEmpty - ? const SizedBox.shrink() - : ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - ), - ); - }, - ); - }, + child: Container( + alignment: AlignmentDirectional.centerStart, + padding: GridSize.cellContentInsets, + child: state.tasks.isEmpty + ? const SizedBox.shrink() + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart index de7f7f5a2e..537207c12c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_date_cell.dart @@ -1,11 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/widgets.dart'; import '../editable_cell_skeleton/date.dart'; @@ -15,7 +17,6 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, @@ -29,11 +30,11 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { child: Align( alignment: AlignmentDirectional.centerStart, child: state.fieldInfo.wrapCellContent ?? false - ? _buildCellContent(state, compactModeNotifier) + ? _buildCellContent(state) : SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: _buildCellContent(state, compactModeNotifier), + child: _buildCellContent(state), ), ), popupBuilder: (BuildContext popoverContent) { @@ -48,44 +49,29 @@ class DesktopGridDateCellSkin extends IEditableDateCellSkin { ); } - Widget _buildCellContent( - DateCellState state, - ValueNotifier compactModeNotifier, - ) { + Widget _buildCellContent(DateCellState state) { final wrap = state.fieldInfo.wrapCellContent ?? false; - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Padding( - padding: padding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: FlowyText( - dateStr, - overflow: wrap ? null : TextOverflow.ellipsis, - maxLines: wrap ? null : 1, - ), - ), - if (state.cellData.reminderId.isNotEmpty) ...[ - const HSpace(4), - FlowyTooltip( - message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), - child: const FlowySvg(FlowySvgs.clock_alarm_s), - ), - ], - ], + return Padding( + padding: GridSize.cellContentInsets, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.medium( + state.dateStr, + overflow: wrap ? null : TextOverflow.ellipsis, + maxLines: wrap ? null : 1, + ), ), - ); - }, + if (state.data?.reminderId.isNotEmpty ?? false) ...[ + const HSpace(4), + FlowyTooltip( + message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), + child: const FlowySvg(FlowySvgs.clock_alarm_s), + ), + ], + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart deleted file mode 100644 index b070af7cc7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart +++ /dev/null @@ -1,265 +0,0 @@ -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/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_media_upload.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_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:universal_platform/universal_platform.dart'; - -class GridMediaCellSkin extends IEditableMediaCellSkin { - const GridMediaCellSkin({this.isMobileRowDetail = false}); - - final bool isMobileRowDetail; - - @override - void dispose() {} - - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - PopoverController popoverController, - MediaCellBloc bloc, - ) { - final isMobile = UniversalPlatform.isMobile; - - Widget child = BlocBuilder( - builder: (context, state) { - final wrapContent = context.read().wrapContent; - final List children = state.files - .map( - (file) => GestureDetector( - onTap: () => _openOrExpandFile(context, file, state.files), - child: Padding( - padding: wrapContent - ? const EdgeInsets.only(right: 4) - : EdgeInsets.zero, - child: _FilePreviewRender(file: file), - ), - ), - ) - .toList(); - - if (isMobileRowDetail && state.files.isEmpty) { - children.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Text( - LocaleKeys.grid_row_textPlaceholder.tr(), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 16, - color: Theme.of(context).hintColor, - ), - ), - ), - ); - } - - if (!isMobile && wrapContent) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: SizedBox( - width: double.infinity, - child: Wrap( - runSpacing: 4, - children: children, - ), - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: SizedBox( - width: double.infinity, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: children..add(const SizedBox(width: 16)), - ), - ), - ), - ); - }, - ); - - if (!isMobile) { - child = AppFlowyPopover( - controller: popoverController, - constraints: const BoxConstraints( - minWidth: 250, - maxWidth: 250, - maxHeight: 400, - ), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: const MediaCellEditor(), - ), - onClose: () => cellContainerNotifier.isFocus = false, - child: child, - ); - } else { - child = Align( - alignment: AlignmentDirectional.centerStart, - child: child, - ); - - if (isMobileRowDetail) { - child = Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), - ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - alignment: AlignmentDirectional.centerStart, - child: child, - ); - } - - child = InkWell( - borderRadius: - isMobileRowDetail ? BorderRadius.circular(12) : BorderRadius.zero, - onTap: () => _tapCellMobile(context), - hoverColor: Colors.transparent, - child: child, - ); - } - - return BlocProvider.value( - value: bloc, - child: Builder(builder: (context) => child), - ); - } - - void _openOrExpandFile( - BuildContext context, - MediaFilePB file, - List files, - ) { - if (file.fileType != MediaFileTypePB.Image) { - afLaunchUrlString(file.url, context: context); - return; - } - - final images = - files.where((f) => f.fileType == MediaFileTypePB.Image).toList(); - final index = images.indexOf(file); - - showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfile, - imageProvider: AFBlockImageProvider( - initialIndex: index, - images: images - .map( - (e) => ImageBlockData( - url: e.url, - type: e.uploadType.toCustomImageType(), - ), - ) - .toList(), - onDeleteImage: (index) { - final deleteFile = images[index]; - context.read().deleteFile(deleteFile.id); - }, - ), - ), - ); - } - - void _tapCellMobile(BuildContext context) { - final files = context.read().state.files; - - if (files.isEmpty) { - showMobileBottomSheet( - context, - title: LocaleKeys.grid_media_addFileMobile.tr(), - showHeader: true, - showCloseButton: true, - showDragHandle: true, - builder: (dContext) => BlocProvider.value( - value: context.read(), - child: MobileMediaUploadSheetContent( - dialogContext: dContext, - ), - ), - ); - return; - } - - showMobileBottomSheet( - context, - builder: (_) => BlocProvider.value( - value: context.read(), - child: const MobileMediaCellEditor(), - ), - ); - } -} - -class _FilePreviewRender extends StatelessWidget { - const _FilePreviewRender({required this.file}); - - final MediaFilePB file; - - @override - Widget build(BuildContext context) { - if (file.fileType != MediaFileTypePB.Image) { - return FlowyTooltip( - message: file.name, - child: Container( - height: 28, - width: 28, - clipBehavior: Clip.antiAlias, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).greyHover, - borderRadius: BorderRadius.circular(4), - ), - child: FlowySvg( - file.fileType.icon, - size: const Size.square(12), - color: AFThemeExtension.of(context).textColor, - ), - ), - ); - } - - return FlowyTooltip( - message: file.name, - child: Container( - height: 28, - width: 28, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4)), - child: AFImage( - url: file.url, - uploadType: file.uploadType, - userProfile: context.read().state.userProfile, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart index 7a6f3e63bc..04368bc725 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_number_cell.dart @@ -1,6 +1,6 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,37 +11,27 @@ class DesktopGridNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - - return TextField( - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: context.watch().state.wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: padding, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ); - }, + return TextField( + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: context.watch().state.wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart index dda3183b59..80649f6a02 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart @@ -1,10 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -17,12 +17,10 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, ) { - final userWorkspaceBloc = context.read(); return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, @@ -30,24 +28,16 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { margin: EdgeInsets.zero, onClose: () => cellContainerNotifier.isFocus = false, popupBuilder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: userWorkspaceBloc), - BlocProvider.value(value: bloc), - ], + return BlocProvider.value( + value: bloc, child: const RelationCellEditor(), ); }, child: Align( alignment: AlignmentDirectional.centerStart, - child: ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - return state.wrap - ? _buildWrapRows(context, state.rows, compactMode) - : _buildNoWrapRows(context, state.rows, compactMode); - }, - ), + child: state.wrap + ? _buildWrapRows(context, state.rows) + : _buildNoWrapRows(context, state.rows), ), ); } @@ -55,19 +45,16 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget _buildWrapRows( BuildContext context, List rows, - bool compactMode, ) { return Padding( - padding: compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets, + padding: GridSize.cellContentInsets, child: Wrap( runSpacing: 4, spacing: 4.0, children: rows.map( (row) { final isEmpty = row.name.isEmpty; - return FlowyText( + return FlowyText.medium( isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, color: isEmpty ? Theme.of(context).hintColor : null, decoration: TextDecoration.underline, @@ -82,7 +69,6 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { Widget _buildNoWrapRows( BuildContext context, List rows, - bool compactMode, ) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), @@ -95,7 +81,7 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { children: rows.map( (row) { final isEmpty = row.name.isEmpty; - return FlowyText( + return FlowyText.medium( isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, color: isEmpty ? Theme.of(context).hintColor : null, decoration: TextDecoration.underline, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart index b599acc4f1..45b43efcec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart @@ -1,9 +1,10 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,7 +16,6 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { @@ -36,92 +36,63 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { return Align( alignment: AlignmentDirectional.centerStart, child: state.wrap - ? _buildWrapOptions( - context, - state.selectedOptions, - compactModeNotifier, - ) - : _buildNoWrapOptions( - context, - state.selectedOptions, - compactModeNotifier, - ), + ? _buildWrapOptions(context, state.selectedOptions) + : _buildNoWrapOptions(context, state.selectedOptions), ); }, ), ); } - Widget _buildWrapOptions( - BuildContext context, - List options, - ValueNotifier compactModeNotifier, - ) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Padding( - padding: padding, - child: Wrap( - runSpacing: 4, - children: options.map( - (option) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SelectOptionTag( - option: option, - padding: EdgeInsets.symmetric( - vertical: compactMode ? 2 : 4, - horizontal: 8, - ), - ), - ); - }, - ).toList(), - ), - ); - }, + Widget _buildWrapOptions(BuildContext context, List options) { + return Padding( + padding: GridSize.cellContentInsets, + child: Wrap( + runSpacing: 4, + children: options.map( + (option) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric( + vertical: 1, + horizontal: 8, + ), + ), + ); + }, + ).toList(), + ), ); } Widget _buildNoWrapOptions( BuildContext context, List options, - ValueNotifier compactModeNotifier, ) { return SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Padding( - padding: padding, - child: Row( - mainAxisSize: MainAxisSize.min, - children: options.map( - (option) { - return Padding( - padding: const EdgeInsets.only(right: 4), - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 1, - horizontal: 8, - ), - ), - ); - }, - ).toList(), - ), - ); - }, + child: Padding( + padding: GridSize.cellContentInsets, + child: Row( + mainAxisSize: MainAxisSize.min, + children: options.map( + (option) { + return Padding( + padding: const EdgeInsets.only(right: 4), + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric( + vertical: 1, + horizontal: 8, + ), + ), + ); + }, + ).toList(), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart index 1f3ded0109..b5d915b022 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart @@ -11,7 +11,6 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -28,69 +27,58 @@ class DesktopGridSummaryCellSkin extends IEditableSummaryCellSkin { onExit: (p) => Provider.of(context, listen: false) .onEnter = false, - child: ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - - return ConstrainedBox( - constraints: BoxConstraints( - minHeight: compactMode - ? GridSize.headerHeight - 4 - : GridSize.headerHeight, - ), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: TextField( - controller: textEditingController, - enabled: false, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: padding, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: GridSize.headerHeight, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: TextField( + controller: textEditingController, + enabled: false, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: null, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.cellVPadding, - ), - child: Consumer( - builder: ( - BuildContext context, - SummaryMouseNotifier notifier, - Widget? child, - ) { - if (notifier.onEnter) { - return SummaryCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ).positioned(right: 0, bottom: compactMode ? 4 : 8), - ], + ), ), - ); - }, + Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.cellVPadding, + ), + child: Consumer( + builder: ( + BuildContext context, + SummaryMouseNotifier notifier, + Widget? child, + ) { + if (notifier.onEnter) { + return SummaryCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ).positioned(right: 0, bottom: 8), + ], + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart index 75c973d886..8001590840 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart @@ -1,7 +1,6 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -13,98 +12,53 @@ class DesktopGridTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, data) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Padding( - padding: padding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const _IconOrEmoji(), - Expanded( - child: TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: context.watch().state.wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: context - .read() - .cellController - .fieldInfo - .isPrimary - ? FontWeight.w500 - : null, - ), - decoration: const InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - isCollapsed: true, - ), - ), - ), - ], - ), - ); - }, - ); - } -} - -class _IconOrEmoji extends StatelessWidget { - const _IconOrEmoji(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - // if not a title cell, return empty widget - if (state.emoji == null || state.hasDocument == null) { - return const SizedBox.shrink(); - } - - return ValueListenableBuilder( - valueListenable: state.emoji!, - builder: (context, emoji, _) { - return emoji.isNotEmpty - ? Padding( - padding: const EdgeInsetsDirectional.only(end: 6.0), - child: FlowyText.emoji( - optimizeEmojiAlign: true, - emoji, + return Padding( + padding: GridSize.cellContentInsets, + child: Row( + children: [ + BlocBuilder( + buildWhen: (p, c) => p.emoji != c.emoji, + builder: (context, state) { + if (state.emoji.isEmpty) { + return const SizedBox.shrink(); + } + return Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyText( + state.emoji, + fontSize: 16, ), - ) - : ValueListenableBuilder( - valueListenable: state.hasDocument!, - builder: (context, hasDocument, _) { - return hasDocument - ? Padding( - padding: - const EdgeInsetsDirectional.only(end: 6.0) - .add(const EdgeInsets.all(1)), - child: FlowySvg( - FlowySvgs.notes_s, - color: Theme.of(context).hintColor, - ), - ) - : const SizedBox.shrink(); - }, - ); - }, - ); - }, + const HSpace(6), + ], + ), + ); + }, + ), + Expanded( + child: TextField( + controller: textEditingController, + focusNode: focusNode, + maxLines: context.watch().state.wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium, + decoration: const InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + isCollapsed: true, + ), + ), + ), + ], + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart index 8a1fd92499..d1b6131680 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_timestamp_cell.dart @@ -1,6 +1,6 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -11,41 +11,29 @@ class DesktopGridTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { return Container( alignment: AlignmentDirectional.centerStart, child: state.wrap - ? _buildCellContent(state, compactModeNotifier) + ? _buildCellContent(state) : SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), scrollDirection: Axis.horizontal, - child: _buildCellContent(state, compactModeNotifier), + child: _buildCellContent(state), ), ); } - Widget _buildCellContent( - TimestampCellState state, - ValueNotifier compactModeNotifier, - ) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return Padding( - padding: padding, - child: FlowyText( - state.dateStr, - overflow: state.wrap ? null : TextOverflow.ellipsis, - maxLines: state.wrap ? null : 1, - ), - ); - }, + Widget _buildCellContent(TimestampCellState state) { + return Padding( + padding: GridSize.cellContentInsets, + child: FlowyText.medium( + state.dateStr, + overflow: state.wrap ? null : TextOverflow.ellipsis, + maxLines: state.wrap ? null : 1, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart index 102b491f52..aece28373c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart @@ -11,7 +11,6 @@ class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -19,79 +18,68 @@ class DesktopGridTranslateCellSkin extends IEditableTranslateCellSkin { return ChangeNotifierProvider( create: (_) => TranslateMouseNotifier(), builder: (context, child) { - return ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - - return MouseRegion( - cursor: SystemMouseCursors.click, - opaque: false, - onEnter: (p) => - Provider.of(context, listen: false) - .onEnter = true, - onExit: (p) => - Provider.of(context, listen: false) - .onEnter = false, - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: compactMode - ? GridSize.headerHeight - 4 - : GridSize.headerHeight, - ), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - maxLines: null, - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - contentPadding: padding, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), + return MouseRegion( + cursor: SystemMouseCursors.click, + opaque: false, + onEnter: (p) => + Provider.of(context, listen: false) + .onEnter = true, + onExit: (p) => + Provider.of(context, listen: false) + .onEnter = false, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: GridSize.headerHeight, + ), + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: TextField( + controller: textEditingController, + readOnly: true, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: null, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: GridSize.cellVPadding, - ), - child: Consumer( - builder: ( - BuildContext context, - TranslateMouseNotifier notifier, - Widget? child, - ) { - if (notifier.onEnter) { - return TranslateCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ); - } else { - return const SizedBox.shrink(); - } - }, - ), - ).positioned(right: 0, bottom: compactMode ? 4 : 8), - ], + ), ), - ), - ); - }, + Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.cellVPadding, + ), + child: Consumer( + builder: ( + BuildContext context, + TranslateMouseNotifier notifier, + Widget? child, + ) { + if (notifier.onEnter) { + return TranslateCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ).positioned(right: 0, bottom: 8), + ], + ), + ), ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart index 935716e686..8ccda95391 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart @@ -8,7 +8,6 @@ import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -21,7 +20,6 @@ class DesktopGridURLSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -29,36 +27,28 @@ class DesktopGridURLSkin extends IEditableURLCellSkin { ) { return BlocSelector( selector: (state) => state.wrap, - builder: (context, wrap) => ValueListenableBuilder( - valueListenable: compactModeNotifier, - builder: (context, compactMode, _) { - final padding = compactMode - ? GridSize.compactCellContentInsets - : GridSize.cellContentInsets; - return TextField( - controller: textEditingController, - focusNode: focusNode, - maxLines: wrap ? null : 1, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - decoration: InputDecoration( - contentPadding: padding, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), - isDense: true, + builder: (context, wrap) => TextField( + controller: textEditingController, + focusNode: focusNode, + maxLines: wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, ), - onTapOutside: (_) => focusNode.unfocus(), - ); - }, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).hintColor), + isDense: true, + ), + onTapOutside: (_) => focusNode.unfocus(), ), ); } @@ -212,14 +202,8 @@ class _URLAccessoryIconContainer extends StatelessWidget { ), borderRadius: Corners.s6Border, ), - child: FlowyHover( - style: HoverStyle( - backgroundColor: AFThemeExtension.of(context).background, - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ), - child: Center( - child: child, - ), + child: Center( + child: child, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart index 1e56c5160e..bb15cd5d9f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checkbox_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,7 +11,6 @@ class DesktopRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index ab0533819a..39804c6851 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -2,19 +2,15 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../editable_cell_skeleton/checklist.dart'; @@ -23,423 +19,196 @@ class DesktopRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, + ChecklistCellState state, PopoverController popoverController, ) { - return ChecklistRowDetailCell( + return ChecklistItems( context: context, cellContainerNotifier: cellContainerNotifier, bloc: bloc, + state: state, popoverController: popoverController, ); } } -class ChecklistRowDetailCell extends StatefulWidget { - const ChecklistRowDetailCell({ +class ChecklistItems extends StatefulWidget { + const ChecklistItems({ super.key, required this.context, required this.cellContainerNotifier, required this.bloc, + required this.state, required this.popoverController, }); final BuildContext context; final CellContainerNotifier cellContainerNotifier; final ChecklistCellBloc bloc; + final ChecklistCellState state; final PopoverController popoverController; @override - State createState() => _ChecklistRowDetailCellState(); + State createState() => _ChecklistItemsState(); } -class _ChecklistRowDetailCellState extends State { - final phantomTextController = TextEditingController(); - - @override - void dispose() { - phantomTextController.dispose(); - super.dispose(); - } +class _ChecklistItemsState extends State { + bool showIncompleteOnly = false; @override Widget build(BuildContext context) { + final tasks = [...widget.state.tasks]; + if (showIncompleteOnly) { + tasks.removeWhere((task) => task.isSelected); + } + // final children = tasks + // .mapIndexed( + // (index, task) => Padding( + // padding: const EdgeInsets.symmetric(vertical: 2.0), + // child: ChecklistItem( + // key: ValueKey('${task.data.id}$index'), + // task: task, + // autofocus: widget.state.newTask && index == tasks.length - 1, + // onSubmitted: () { + // if (index == tasks.length - 1) { + // // create a new task under the last task if the users press enter + // widget.bloc.add(const ChecklistCellEvent.createNewTask('')); + // } + // }, + // ), + // ), + // ) + // .toList(); return Align( alignment: AlignmentDirectional.centerStart, child: Column( mainAxisSize: MainAxisSize.min, children: [ - ProgressAndHideCompleteButton( - onToggleHideComplete: () => context - .read() - .add(const ChecklistCellEvent.toggleShowIncompleteOnly()), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ChecklistProgressBar( + tasks: widget.state.tasks, + percent: widget.state.percent, + ), + ), + const HSpace(6.0), + FlowyIconButton( + tooltipText: showIncompleteOnly + ? LocaleKeys.grid_checklist_showComplete.tr() + : LocaleKeys.grid_checklist_hideComplete.tr(), + width: 32, + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + icon: FlowySvg( + showIncompleteOnly ? FlowySvgs.show_m : FlowySvgs.hide_m, + size: const Size.square(16), + ), + onPressed: () { + setState( + () => showIncompleteOnly = !showIncompleteOnly, + ); + }, + ), + ], + ), ), const VSpace(2.0), - _ChecklistItems( - phantomTextController: phantomTextController, - onStartCreatingTaskAfter: (index) { - context - .read() - .add(ChecklistCellEvent.updatePhantomIndex(index + 1)); - }, - ), - ChecklistItemControl( - cellNotifer: widget.cellContainerNotifier, - onTap: () { - final bloc = context.read(); - if (bloc.state.phantomIndex == null) { - bloc.add( - ChecklistCellEvent.updatePhantomIndex( - bloc.state.showIncompleteOnly - ? bloc.state.tasks - .where((task) => !task.isSelected) - .length - : bloc.state.tasks.length, - ), - ); - } else { - bloc.add( - ChecklistCellEvent.createNewTask( - phantomTextController.text, - index: bloc.state.phantomIndex, - ), - ); - } - phantomTextController.clear(); - }, - ), + _ChecklistCellEditors(tasks: tasks), + ChecklistItemControl(cellNotifer: widget.cellContainerNotifier), ], ), ); } } -@visibleForTesting -class ProgressAndHideCompleteButton extends StatelessWidget { - const ProgressAndHideCompleteButton({ - super.key, - required this.onToggleHideComplete, +class _ChecklistCellEditors extends StatelessWidget { + const _ChecklistCellEditors({ + required this.tasks, }); - final VoidCallback onToggleHideComplete; + final List tasks; @override Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.showIncompleteOnly != current.showIncompleteOnly, - builder: (context, state) { - return Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: BlocBuilder( - builder: (context, state) { - return ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - ); - }, - ), - ), - const HSpace(6.0), - FlowyIconButton( - tooltipText: state.showIncompleteOnly - ? LocaleKeys.grid_checklist_showComplete.tr() - : LocaleKeys.grid_checklist_hideComplete.tr(), - width: 32, - iconColorOnHover: Theme.of(context).colorScheme.onSurface, - icon: FlowySvg( - state.showIncompleteOnly - ? FlowySvgs.show_m - : FlowySvgs.hide_m, - size: const Size.square(16), - ), - onPressed: onToggleHideComplete, - ), - ], - ), - ); - }, - ); - } -} - -class _ChecklistItems extends StatelessWidget { - const _ChecklistItems({ - required this.phantomTextController, - required this.onStartCreatingTaskAfter, - }); - - final TextEditingController phantomTextController; - final void Function(int index) onStartCreatingTaskAfter; - - @override - Widget build(BuildContext context) { - return Actions( - actions: { - _CancelCreatingFromPhantomIntent: - CallbackAction<_CancelCreatingFromPhantomIntent>( - onInvoke: (_CancelCreatingFromPhantomIntent intent) { - phantomTextController.clear(); - context - .read() - .add(const ChecklistCellEvent.updatePhantomIndex(null)); - return; - }, - ), - }, - child: BlocBuilder( - builder: (context, state) { - final children = _makeChildren(context, state); - return ReorderableListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: MouseRegion( - cursor: UniversalPlatform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: IgnorePointer( - child: BlocProvider.value( - value: context.read(), - child: child, - ), - ), - ), - ), - buildDefaultDragHandles: false, - itemCount: children.length, - itemBuilder: (_, index) => children[index], - onReorder: (from, to) { - context - .read() - .add(ChecklistCellEvent.reorderTask(from, to)); - }, - ); - }, - ), - ); - } - - List _makeChildren(BuildContext context, ChecklistCellState state) { - final children = []; - - final tasks = [...state.tasks]; - - if (state.showIncompleteOnly) { - tasks.removeWhere((task) => task.isSelected); - } - - children.addAll( - tasks.mapIndexed( - (index, task) => Padding( - key: ValueKey('checklist_row_detail_cell_task_${task.data.id}'), - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: ChecklistItem( - task: task, - index: index, - onSubmitted: () { - onStartCreatingTaskAfter(index); - }, - ), - ), - ), - ); - - if (state.phantomIndex != null) { - children.insert( - state.phantomIndex!, - Padding( - key: const ValueKey('new_checklist_cell_task'), - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: PhantomChecklistItem( - index: state.phantomIndex!, - textController: phantomTextController, - ), - ), - ); - } - - return children; - } -} - -class _CancelCreatingFromPhantomIntent extends Intent { - const _CancelCreatingFromPhantomIntent(); -} - -class _SubmitPhantomTaskIntent extends Intent { - const _SubmitPhantomTaskIntent({ - required this.taskDescription, - required this.index, - }); - - final String taskDescription; - final int index; -} - -@visibleForTesting -class PhantomChecklistItem extends StatefulWidget { - const PhantomChecklistItem({ - super.key, - required this.index, - required this.textController, - }); - - final int index; - final TextEditingController textController; - - @override - State createState() => _PhantomChecklistItemState(); -} - -class _PhantomChecklistItemState extends State { - final focusNode = FocusNode(); - - bool isComposing = false; - - @override - void initState() { - super.initState(); - widget.textController.addListener(_onTextChanged); - focusNode.addListener(_onFocusChanged); - WidgetsBinding.instance - .addPostFrameCallback((_) => focusNode.requestFocus()); - } - - void _onTextChanged() => setState( - () => isComposing = !widget.textController.value.composing.isCollapsed, - ); - - void _onFocusChanged() { - if (!focusNode.hasFocus) { - widget.textController.clear(); - Actions.maybeInvoke( - context, - const _CancelCreatingFromPhantomIntent(), - ); - } - } - - @override - void dispose() { - widget.textController.removeListener(_onTextChanged); - focusNode.removeListener(_onFocusChanged); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Actions( - actions: { - _SubmitPhantomTaskIntent: CallbackAction<_SubmitPhantomTaskIntent>( - onInvoke: (_SubmitPhantomTaskIntent intent) { - context.read().add( - ChecklistCellEvent.createNewTask( - intent.taskDescription, - index: intent.index, - ), - ); - widget.textController.clear(); - return; - }, - ), - }, - child: Shortcuts( - shortcuts: _buildShortcuts(), - child: Container( - constraints: const BoxConstraints(minHeight: 32), - decoration: BoxDecoration( - color: AFThemeExtension.of(context).lightGreyHover, - borderRadius: Corners.s6Border, - ), - child: Center( - child: ChecklistCellTextfield( - textController: widget.textController, - focusNode: focusNode, - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), + final bloc = context.read(); + final state = bloc.state; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...tasks.mapIndexed( + (index, task) => Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: ChecklistItem( + key: ValueKey('${task.data.id}$index'), + task: task, + autofocus: state.newTask && index == tasks.length - 1, + onSubmitted: () { + if (index == tasks.length - 1) { + // create a new task under the last task if the users press enter + bloc.add(const ChecklistCellEvent.createNewTask('')); + } + }, ), ), ), - ), + ], ); } - - Map _buildShortcuts() { - return isComposing - ? const {} - : { - const SingleActivator(LogicalKeyboardKey.enter): - _SubmitPhantomTaskIntent( - taskDescription: widget.textController.text, - index: widget.index, - ), - const SingleActivator(LogicalKeyboardKey.escape): - const _CancelCreatingFromPhantomIntent(), - }; - } } -@visibleForTesting class ChecklistItemControl extends StatelessWidget { - const ChecklistItemControl({ - super.key, - required this.cellNotifer, - required this.onTap, - }); + const ChecklistItemControl({super.key, required this.cellNotifer}); final CellContainerNotifier cellNotifer; - final VoidCallback onTap; @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( value: cellNotifer, child: Consumer( - builder: (buildContext, notifier, _) => TextFieldTapRegion( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Container( - margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0), - height: 12, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 150), - child: notifier.isHover - ? FlowyTooltip( - message: LocaleKeys.grid_checklist_addNew.tr(), - child: Row( - children: [ - const Flexible(child: Center(child: Divider())), - const HSpace(12.0), - FilledButton( - style: FilledButton.styleFrom( - minimumSize: const Size.square(12), - maximumSize: const Size.square(12), - padding: EdgeInsets.zero, - ), - onPressed: onTap, - child: FlowySvg( - FlowySvgs.add_s, - color: Theme.of(context).colorScheme.onPrimary, - ), + builder: (buildContext, notifier, _) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: Container( + margin: const EdgeInsets.fromLTRB(8.0, 2.0, 8.0, 0), + height: 12, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: notifier.isHover + ? FlowyTooltip( + message: LocaleKeys.grid_checklist_addNew.tr(), + child: Row( + children: [ + const Flexible(child: Center(child: Divider())), + const HSpace(12.0), + FilledButton( + style: FilledButton.styleFrom( + minimumSize: const Size.square(12), + maximumSize: const Size.square(12), + padding: EdgeInsets.zero, ), - const HSpace(12.0), - const Flexible(child: Center(child: Divider())), - ], - ), - ) - : const SizedBox.expand(), - ), + onPressed: () => context + .read() + .add( + const ChecklistCellEvent.createNewTask(""), + ), + child: FlowySvg( + FlowySvgs.add_s, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + const HSpace(12.0), + const Flexible(child: Center(child: Divider())), + ], + ), + ) + : const SizedBox.expand(), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart index f1b5f14975..071ed0ab84 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_date_cell.dart @@ -1,11 +1,13 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/date_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { @@ -13,18 +15,14 @@ class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, ) { - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); - final text = - dateStr.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : dateStr; - final color = dateStr.isEmpty ? Theme.of(context).hintColor : null; + final text = state.dateStr.isEmpty + ? LocaleKeys.grid_row_textPlaceholder.tr() + : state.dateStr; + final color = state.dateStr.isEmpty ? Theme.of(context).hintColor : null; return AppFlowyPopover( controller: popoverController, @@ -32,7 +30,6 @@ class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { direction: PopoverDirection.bottomWithLeftAligned, constraints: BoxConstraints.loose(const Size(260, 620)), margin: EdgeInsets.zero, - asBarrier: true, child: Container( alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -40,13 +37,13 @@ class DesktopRowDetailDateCellSkin extends IEditableDateCellSkin { mainAxisSize: MainAxisSize.min, children: [ Flexible( - child: FlowyText( + child: FlowyText.medium( text, color: color, overflow: TextOverflow.ellipsis, ), ), - if (state.cellData.reminderId.isNotEmpty) ...[ + if (state.data?.reminderId.isNotEmpty ?? false) ...[ const HSpace(4), FlowyTooltip( message: LocaleKeys.grid_field_reminderOnDateTooltip.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart deleted file mode 100644 index 6e648eb187..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart +++ /dev/null @@ -1,749 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/util/xfile_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:collection/collection.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:reorderables/reorderables.dart'; - -const _dropFileKey = 'files_media'; -const _itemWidth = 86.4; - -class DekstopRowDetailMediaCellSkin extends IEditableMediaCellSkin { - final mutex = PopoverMutex(); - - @override - void dispose() { - mutex.dispose(); - } - - @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - PopoverController popoverController, - MediaCellBloc bloc, - ) { - return BlocProvider.value( - value: bloc, - child: BlocBuilder( - builder: (context, state) => LayoutBuilder( - builder: (context, constraints) { - if (state.files.isEmpty) { - return _AddFileButton( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - mutex: mutex, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 6, - ), - child: FlowyText( - LocaleKeys.grid_row_textPlaceholder.tr(), - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - int itemsToShow = state.showAllFiles ? state.files.length : 0; - if (!state.showAllFiles) { - // The row width is surrounded by 8px padding on each side - final rowWidth = constraints.maxWidth - 16; - - // Each item needs 94.4 px to render, 86.4px width + 8px runSpacing - final itemsPerRow = rowWidth ~/ (_itemWidth + 8); - - // We show at most 2 rows - itemsToShow = itemsPerRow * 2; - } - - final filesToDisplay = - state.showAllFiles || itemsToShow >= state.files.length - ? state.files - : state.files.take(itemsToShow - 1).toList(); - final extraCount = state.files.length - itemsToShow; - final images = state.files - .where((f) => f.fileType == MediaFileTypePB.Image) - .toList(); - - final size = constraints.maxWidth / 2 - 6; - return _AddFileButton( - controller: popoverController, - mutex: mutex, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: ReorderableWrap( - needsLongPressDraggable: false, - runSpacing: 8, - spacing: 8, - onReorder: (from, to) => context - .read() - .add(MediaCellEvent.reorderFiles(from: from, to: to)), - footer: extraCount > 0 && !state.showAllFiles - ? GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _toggleShowAllFiles(context), - child: _FilePreviewRender( - key: ValueKey(state.files[itemsToShow - 1].id), - file: state.files[itemsToShow - 1], - index: 9, - images: images, - size: size, - mutex: mutex, - hideFileNames: state.hideFileNames, - foregroundText: LocaleKeys.grid_media_extraCount - .tr(args: [extraCount.toString()]), - ), - ) - : null, - buildDraggableFeedback: (_, __, child) => - BlocProvider.value( - value: context.read(), - child: _FilePreviewFeedback(child: child), - ), - children: filesToDisplay - .mapIndexed( - (index, file) => _FilePreviewRender( - key: ValueKey(file.id), - file: file, - index: index, - images: images, - size: size, - mutex: mutex, - hideFileNames: state.hideFileNames, - ), - ) - .toList(), - ), - ), - Padding( - padding: const EdgeInsets.all(4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.add_thin_s, - size: const Size.square(12), - color: Theme.of(context).hintColor, - ), - const HSpace(6), - FlowyText.medium( - LocaleKeys.grid_media_addFileOrImage.tr(), - fontSize: 12, - color: Theme.of(context).hintColor, - figmaLineHeight: 18, - ), - ], - ), - ), - ], - ), - ); - }, - ), - ), - ); - } - - void _toggleShowAllFiles(BuildContext context) { - context - .read() - .add(const MediaCellEvent.toggleShowAllFiles()); - } -} - -class _FilePreviewFeedback extends StatelessWidget { - const _FilePreviewFeedback({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - position: DecorationPosition.foreground, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - border: Border.all( - width: 2, - color: const Color(0xFF00BCF0), - ), - ), - child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: const Color(0xFF1F2329).withValues(alpha: .2), - blurRadius: 6, - offset: const Offset(0, 3), - ), - ], - ), - child: BlocProvider.value( - value: context.read(), - child: Material( - type: MaterialType.transparency, - child: child, - ), - ), - ), - ); - } -} - -const _menuWidth = 350.0; - -class _AddFileButton extends StatefulWidget { - const _AddFileButton({ - this.mutex, - required this.controller, - this.direction = PopoverDirection.bottomWithCenterAligned, - required this.child, - }); - - final PopoverController controller; - final PopoverMutex? mutex; - final PopoverDirection direction; - final Widget child; - - @override - State<_AddFileButton> createState() => _AddFileButtonState(); -} - -class _AddFileButtonState extends State<_AddFileButton> { - Offset? position; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - triggerActions: PopoverTriggerFlags.none, - controller: widget.controller, - mutex: widget.mutex, - offset: const Offset(0, 10), - direction: widget.direction, - constraints: const BoxConstraints(maxWidth: _menuWidth), - margin: EdgeInsets.zero, - asBarrier: true, - onClose: () => - context.read().remove(_dropFileKey), - popupBuilder: (_) { - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().add(_dropFileKey); - }); - - return FileUploadMenu( - allowMultipleFiles: true, - onInsertLocalFile: (files) => insertLocalFiles( - context, - files, - userProfile: context.read().state.userProfile, - documentId: context.read().rowId, - onUploadSuccess: (file, path, isLocalMode) { - final mediaCellBloc = context.read(); - if (mediaCellBloc.isClosed) { - return; - } - - mediaCellBloc.add( - MediaCellEvent.addFile( - url: path, - name: file.name, - uploadType: isLocalMode - ? FileUploadTypePB.LocalFile - : FileUploadTypePB.CloudFile, - fileType: file.fileType.toMediaFileTypePB(), - ), - ); - - widget.controller.close(); - }, - ), - onInsertNetworkFile: (url) { - if (url.isEmpty) return; - final uri = Uri.tryParse(url); - if (uri == null) { - return; - } - - final fakeFile = XFile(uri.path); - MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB(); - fileType = fileType == MediaFileTypePB.Other - ? MediaFileTypePB.Link - : fileType; - - String name = - uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ""; - if (name.isEmpty && uri.pathSegments.length > 1) { - name = uri.pathSegments[uri.pathSegments.length - 2]; - } else if (name.isEmpty) { - name = uri.host; - } - - context.read().add( - MediaCellEvent.addFile( - url: url, - name: name, - uploadType: FileUploadTypePB.NetworkFile, - fileType: fileType, - ), - ); - - widget.controller.close(); - }, - ); - }, - child: MouseRegion( - onEnter: (event) => position = event.position, - onExit: (_) => position = null, - onHover: (event) => position = event.position, - child: GestureDetector( - onTap: () { - if (position != null) { - widget.controller.showAt( - position! - const Offset(_menuWidth / 2, 0), - ); - } - }, - behavior: HitTestBehavior.translucent, - child: FlowyHover(resetHoverOnRebuild: false, child: widget.child), - ), - ), - ); - } -} - -class _FilePreviewRender extends StatefulWidget { - const _FilePreviewRender({ - super.key, - required this.file, - required this.images, - required this.index, - required this.size, - required this.mutex, - this.hideFileNames = false, - this.foregroundText, - }); - - final MediaFilePB file; - final List images; - final int index; - final double size; - final PopoverMutex mutex; - final bool hideFileNames; - final String? foregroundText; - - @override - State<_FilePreviewRender> createState() => _FilePreviewRenderState(); -} - -class _FilePreviewRenderState extends State<_FilePreviewRender> { - final nameController = TextEditingController(); - final controller = PopoverController(); - bool isHovering = false; - bool isSelected = false; - - late int thisIndex; - - MediaFilePB get file => widget.file; - - @override - void initState() { - super.initState(); - thisIndex = widget.images.indexOf(file); - } - - @override - void dispose() { - nameController.dispose(); - controller.close(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant _FilePreviewRender oldWidget) { - thisIndex = widget.images.indexOf(file); - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - Widget child; - if (file.fileType == MediaFileTypePB.Image) { - child = AFImage( - url: file.url, - uploadType: file.uploadType, - userProfile: context.read().state.userProfile, - width: _itemWidth, - borderRadius: BorderRadius.only( - topLeft: Corners.s5Radius, - topRight: Corners.s5Radius, - bottomLeft: widget.hideFileNames ? Corners.s5Radius : Radius.zero, - bottomRight: widget.hideFileNames ? Corners.s5Radius : Radius.zero, - ), - ); - } else { - child = DecoratedBox( - decoration: BoxDecoration(color: file.fileType.color), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: FlowySvg( - file.fileType.icon, - color: const Color(0xFF666D76), - ), - ), - ), - ); - } - - if (widget.foregroundText != null) { - child = Stack( - children: [ - Positioned.fill( - child: DecoratedBox( - position: DecorationPosition.foreground, - decoration: - BoxDecoration(color: Colors.black.withValues(alpha: 0.5)), - child: child, - ), - ), - Positioned.fill( - child: Center( - child: FlowyText.semibold( - widget.foregroundText!, - color: Colors.white, - fontSize: 14, - ), - ), - ), - ], - ); - } - - return MouseRegion( - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: FlowyTooltip( - message: file.name, - child: AppFlowyPopover( - controller: controller, - constraints: const BoxConstraints(maxWidth: 240), - offset: const Offset(0, 5), - triggerActions: PopoverTriggerFlags.none, - onClose: () => setState(() => isSelected = false), - asBarrier: true, - popupBuilder: (popoverContext) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: context.read()), - BlocProvider.value(value: context.read()), - ], - child: _FileMenu( - parentContext: context, - index: thisIndex, - file: file, - images: widget.images, - controller: controller, - nameController: nameController, - ), - ), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: widget.foregroundText != null - ? null - : () { - if (file.uploadType == FileUploadTypePB.LocalFile) { - afLaunchUrlString(file.url); - return; - } - - if (file.fileType != MediaFileTypePB.Image) { - afLaunchUrlString(widget.file.url); - return; - } - - openInteractiveViewerFromFiles( - context, - widget.images, - userProfile: - context.read().state.userProfile, - initialIndex: thisIndex, - onDeleteImage: (index) { - final deleteFile = widget.images[index]; - context.read().deleteFile(deleteFile.id); - }, - ); - }, - child: Container( - width: _itemWidth, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Corners.s6Radius), - border: Border.all(color: Theme.of(context).dividerColor), - color: Theme.of(context).cardColor, - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 68, child: child), - if (!widget.hideFileNames) - Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(4), - child: FlowyText( - file.name, - fontSize: 10, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 16, - ), - ), - ), - ], - ), - ], - ), - if (widget.foregroundText == null && - (isHovering || isSelected)) - Positioned( - top: 3, - right: 3, - child: FlowyIconButton( - onPressed: () { - setState(() => isSelected = true); - controller.show(); - }, - fillColor: Colors.black.withValues(alpha: 0.4), - width: 18, - radius: BorderRadius.circular(4), - icon: const FlowySvg( - FlowySvgs.three_dots_s, - color: Colors.white, - size: Size.square(16), - ), - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class _FileMenu extends StatefulWidget { - const _FileMenu({ - required this.parentContext, - required this.index, - required this.file, - required this.images, - required this.controller, - required this.nameController, - }); - - /// Parent [BuildContext] used to retrieve the [MediaCellBloc] - final BuildContext parentContext; - - /// Index of this file in [widget.images] - final int index; - - /// The current [MediaFilePB] being previewed - final MediaFilePB file; - - /// All images in the field, excluding non-image files- - final List images; - - /// The [PopoverController] to close the popover - final PopoverController controller; - - /// The [TextEditingController] for renaming the file - final TextEditingController nameController; - - @override - State<_FileMenu> createState() => _FileMenuState(); -} - -class _FileMenuState extends State<_FileMenu> { - final errorMessage = ValueNotifier(null); - - @override - void dispose() { - errorMessage.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SeparatedColumn( - separatorBuilder: () => const VSpace(8), - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.file.fileType == MediaFileTypePB.Image) ...[ - MediaMenuItem( - onTap: () { - widget.controller.close(); - _showInteractiveViewer(context); - }, - icon: FlowySvgs.full_view_s, - label: LocaleKeys.grid_media_expand.tr(), - ), - MediaMenuItem( - onTap: () { - widget.controller.close(); - _setCover(context); - }, - icon: FlowySvgs.cover_s, - label: LocaleKeys.grid_media_setAsCover.tr(), - ), - ], - MediaMenuItem( - onTap: () { - widget.controller.close(); - afLaunchUrlString(widget.file.url); - }, - icon: FlowySvgs.open_in_browser_s, - label: LocaleKeys.grid_media_openInBrowser.tr(), - ), - MediaMenuItem( - onTap: () { - widget.controller.close(); - widget.nameController.text = widget.file.name; - widget.nameController.selection = TextSelection( - baseOffset: 0, - extentOffset: widget.nameController.text.length, - ); - - _showRenameConfirmDialog(); - }, - icon: FlowySvgs.rename_s, - label: LocaleKeys.grid_media_rename.tr(), - ), - if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[ - MediaMenuItem( - onTap: () async => downloadMediaFile( - context, - widget.file, - userProfile: context.read().state.userProfile, - ), - icon: FlowySvgs.save_as_s, - label: LocaleKeys.button_download.tr(), - ), - ], - MediaMenuItem( - onTap: () { - widget.controller.close(); - showConfirmDeletionDialog( - context: context, - name: widget.file.name, - description: LocaleKeys.grid_media_deleteFileDescription.tr(), - onConfirm: () => widget.parentContext - .read() - .add(MediaCellEvent.removeFile(fileId: widget.file.id)), - ); - }, - icon: FlowySvgs.trash_s, - label: LocaleKeys.button_delete.tr(), - ), - ], - ); - } - - void _saveName(BuildContext context) { - final newName = widget.nameController.text.trim(); - if (newName.isEmpty) { - return; - } - - context - .read() - .add(MediaCellEvent.renameFile(fileId: widget.file.id, name: newName)); - Navigator.of(context).pop(); - } - - void _showRenameConfirmDialog() { - showCustomConfirmDialog( - context: widget.parentContext, - title: LocaleKeys.document_plugins_file_renameFile_title.tr(), - description: LocaleKeys.document_plugins_file_renameFile_description.tr(), - closeOnConfirm: false, - builder: (builderContext) => FileRenameTextField( - nameController: widget.nameController, - errorMessage: errorMessage, - onSubmitted: () => _saveName(widget.parentContext), - disposeController: false, - ), - style: ConfirmPopupStyle.cancelAndOk, - confirmLabel: LocaleKeys.button_save.tr(), - onConfirm: () => _saveName(widget.parentContext), - onCancel: Navigator.of(widget.parentContext).pop, - ); - } - - void _setCover(BuildContext context) => context.read().add( - RowDetailEvent.setCover( - RowCoverPB( - data: widget.file.url, - uploadType: widget.file.uploadType, - coverType: CoverTypePB.FileCover, - ), - ), - ); - - void _showInteractiveViewer(BuildContext context) => showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: - widget.parentContext.read().state.userProfile, - imageProvider: AFBlockImageProvider( - initialIndex: widget.index, - images: widget.images - .map( - (e) => ImageBlockData( - url: e.url, - type: e.uploadType.toCustomImageType(), - ), - ) - .toList(), - onDeleteImage: (index) { - final deleteFile = widget.images[index]; - widget.parentContext - .read() - .deleteFile(deleteFile.id); - }, - ), - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart index e90fc85549..97f8f80569 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_number_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,7 +11,6 @@ class DesktopRowDetailNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart index d760d3ac29..e481d33cd5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -16,25 +16,19 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, ) { - final userWorkspaceBloc = context.read(); return AppFlowyPopover( controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, constraints: const BoxConstraints(maxWidth: 400, maxHeight: 400), margin: EdgeInsets.zero, - asBarrier: true, onClose: () => cellContainerNotifier.isFocus = false, popupBuilder: (context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: userWorkspaceBloc), - BlocProvider.value(value: bloc), - ], + return BlocProvider.value( + value: bloc, child: const RelationCellEditor(), ); }, @@ -62,7 +56,7 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { children: rows.map( (row) { final isEmpty = row.name.isEmpty; - return FlowyText( + return FlowyText.medium( isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, color: isEmpty ? Theme.of(context).hintColor : null, decoration: TextDecoration.underline, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart index ff84744c27..0e8c6fdffa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart @@ -1,9 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -17,7 +18,6 @@ class DesktopRowDetailSelectOptionCellSkin Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { @@ -25,14 +25,16 @@ class DesktopRowDetailSelectOptionCellSkin controller: popoverController, constraints: const BoxConstraints.tightFor(width: 300), margin: EdgeInsets.zero, - asBarrier: true, - triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (BuildContext popoverContext) { + WidgetsBinding.instance.addPostFrameCallback((_) { + cellContainerNotifier.isFocus = true; + }); + return SelectOptionCellEditor( + cellController: bloc.cellController, + ); + }, onClose: () => cellContainerNotifier.isFocus = false, - onOpen: () => cellContainerNotifier.isFocus = true, - popupBuilder: (_) => SelectOptionCellEditor( - cellController: bloc.cellController, - ), child: BlocBuilder( builder: (context, state) { return Container( @@ -65,7 +67,7 @@ class DesktopRowDetailSelectOptionCellSkin return SelectOptionTag( option: option, padding: const EdgeInsets.symmetric( - vertical: 4, + vertical: 1, horizontal: 8, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart index 30cd54832d..d8a8902a8c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart @@ -10,7 +10,6 @@ class DesktopRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart index 9511c2f871..b1e10e4da3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_text_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,7 +11,6 @@ class DesktopRowDetailTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart index 6fc534f313..ac0d3ea033 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_timestamp_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -10,14 +10,13 @@ class DesktopRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { return Container( alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6.0), - child: FlowyText( + child: FlowyText.medium( state.dateStr, maxLines: null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart index ee9d7e7300..82b1055356 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_url_cell.dart @@ -1,11 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import '../editable_cell_skeleton/url.dart'; @@ -14,15 +13,31 @@ class DesktopRowDetailURLSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, URLCellDataNotifier cellDataNotifier, ) { - return LinkTextField( + return TextField( controller: textEditingController, focusNode: focusNode, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: LocaleKeys.grid_row_textPlaceholder.tr(), + hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + ), + isDense: true, + ), ); } @@ -39,76 +54,3 @@ class DesktopRowDetailURLSkin extends IEditableURLCellSkin { ]; } } - -class LinkTextField extends StatefulWidget { - const LinkTextField({ - super.key, - required this.controller, - required this.focusNode, - }); - - final TextEditingController controller; - final FocusNode focusNode; - - @override - State createState() => _LinkTextFieldState(); -} - -class _LinkTextFieldState extends State { - bool isLinkClickable = false; - - @override - void initState() { - super.initState(); - HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); - } - - @override - void dispose() { - HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); - super.dispose(); - } - - bool _handleGlobalKeyEvent(KeyEvent event) { - final keyboard = HardwareKeyboard.instance; - final canOpenLink = event is KeyDownEvent && - (keyboard.isControlPressed || keyboard.isMetaPressed); - if (canOpenLink != isLinkClickable) { - setState(() => isLinkClickable = canOpenLink); - } - - return false; - } - - @override - Widget build(BuildContext context) { - return TextField( - mouseCursor: - isLinkClickable ? SystemMouseCursors.click : SystemMouseCursors.text, - controller: widget.controller, - focusNode: widget.focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - onTap: () { - if (isLinkClickable) { - openUrlCellLink(widget.controller.text); - } - }, - decoration: InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - hintText: LocaleKeys.grid_row_textPlaceholder.tr(), - hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).hintColor, - ), - isDense: true, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart index a374417b3d..1c7bab9f92 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart @@ -10,7 +10,6 @@ class DesktopRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index e7b5d0d79b..155a6003ce 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -3,7 +3,6 @@ import 'package:flutter/widgets.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -135,13 +134,6 @@ class EditableCellBuilder { skin: IEditableTranslateCellSkin.fromStyle(style), key: key, ), - FieldType.Media => EditableMediaCell( - databaseController: databaseController, - cellContext: cellContext, - skin: IEditableMediaCellSkin.fromStyle(style), - style: style, - key: key, - ), _ => throw UnimplementedError(), }; } @@ -234,13 +226,6 @@ class EditableCellBuilder { skin: skinMap.timeSkin!, key: key, ), - FieldType.Media => EditableMediaCell( - databaseController: databaseController, - cellContext: cellContext, - skin: skinMap.mediaSkin!, - style: EditableCellStyle.desktopGrid, - key: key, - ), _ => throw UnimplementedError(), }; } @@ -289,7 +274,6 @@ abstract class GridCellState extends State { @override void didUpdateWidget(covariant T oldWidget) { if (oldWidget != this) { - oldWidget.requestFocus.removeListener(onRequestFocus); widget.requestFocus.addListener(onRequestFocus); } super.didUpdateWidget(oldWidget); @@ -384,12 +368,6 @@ class SingleListenerFocusNode extends FocusNode { removeListener(_listener!); } } - - @override - void dispose() { - removeAllListener(); - super.dispose(); - } } class EditableCellSkinMap { @@ -404,7 +382,6 @@ class EditableCellSkinMap { this.urlSkin, this.relationSkin, this.timeSkin, - this.mediaSkin, }); final IEditableCheckboxCellSkin? checkboxSkin; @@ -417,7 +394,6 @@ class EditableCellSkinMap { final IEditableURLCellSkin? urlSkin; final IEditableRelationCellSkin? relationSkin; final IEditableTimeCellSkin? timeSkin; - final IEditableMediaCellSkin? mediaSkin; bool has(FieldType fieldType) { return switch (fieldType) { @@ -434,7 +410,6 @@ class EditableCellSkinMap { FieldType.RichText => textSkin != null, FieldType.URL => urlSkin != null, FieldType.Time => timeSkin != null, - FieldType.Media => mediaSkin != null, _ => throw UnimplementedError(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart index ab421b8925..4b7bd2c442 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -27,7 +27,6 @@ abstract class IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ); @@ -72,7 +71,6 @@ class _CheckboxCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, state, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart index fbed429642..dd7bc6c2c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/checklist_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,8 +28,8 @@ abstract class IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, + ChecklistCellState state, PopoverController popoverController, ); } @@ -70,12 +70,16 @@ class GridChecklistCellState extends GridCellState { Widget build(BuildContext context) { return BlocProvider.value( value: cellBloc, - child: widget.skin.build( - context, - widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, - cellBloc, - _popover, + child: BlocBuilder( + builder: (context, state) { + return widget.skin.build( + context, + widget.cellContainerNotifier, + cellBloc, + state, + _popover, + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart index e61c759f48..4b12c780d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart @@ -1,17 +1,12 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_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/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; import '../desktop_grid/desktop_grid_date_cell.dart'; import '../desktop_row_detail/desktop_row_detail_date_cell.dart'; @@ -33,7 +28,6 @@ abstract class IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, @@ -80,7 +74,6 @@ class _DateCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, state, _popover, @@ -97,45 +90,5 @@ class _DateCellState extends GridCellState { } @override - String? onCopy() => getDateCellStrFromCellData( - cellBloc.state.fieldInfo, - cellBloc.state.cellData, - ); -} - -String getDateCellStrFromCellData(FieldInfo field, DateCellData cellData) { - if (cellData.dateTime == null) { - return ""; - } - - final DateTypeOptionPB(:dateFormat, :timeFormat) = - DateTypeOptionDataParser().fromBuffer(field.field.typeOptionData); - - final format = cellData.includeTime - ? DateFormat("${dateFormat.pattern} ${timeFormat.pattern}") - : DateFormat(dateFormat.pattern); - - if (cellData.isRange) { - return "${format.format(cellData.dateTime!)} → ${format.format(cellData.endDateTime!)}"; - } else { - return format.format(cellData.dateTime!); - } -} - -extension GetDateFormatExtension on DateFormatPB { - String get pattern => switch (this) { - DateFormatPB.Local => 'MM/dd/y', - DateFormatPB.US => 'y/MM/dd', - DateFormatPB.ISO => 'y-MM-dd', - DateFormatPB.Friendly => 'MMM dd, y', - DateFormatPB.DayMonthYear => 'dd/MM/y', - _ => 'MMM dd, y', - }; -} - -extension GetTimeFormatExtension on TimeFormatPB { - String get pattern => switch (this) { - TimeFormatPB.TwelveHour => 'hh:mm a', - _ => 'HH:mm', - }; + String? onCopy() => cellBloc.state.dateStr; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/media.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/media.dart deleted file mode 100644 index 55adb85334..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/media.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_media_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_media_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../application/cell/cell_controller_builder.dart'; - -abstract class IEditableMediaCellSkin { - const IEditableMediaCellSkin(); - - factory IEditableMediaCellSkin.fromStyle(EditableCellStyle style) { - return switch (style) { - EditableCellStyle.desktopGrid => const GridMediaCellSkin(), - EditableCellStyle.desktopRowDetail => DekstopRowDetailMediaCellSkin(), - EditableCellStyle.mobileGrid => const GridMediaCellSkin(), - EditableCellStyle.mobileRowDetail => - const GridMediaCellSkin(isMobileRowDetail: true), - }; - } - - bool autoShowPopover(EditableCellStyle style) => switch (style) { - EditableCellStyle.desktopGrid => true, - EditableCellStyle.desktopRowDetail => false, - EditableCellStyle.mobileGrid => false, - EditableCellStyle.mobileRowDetail => false, - }; - - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - PopoverController popoverController, - MediaCellBloc bloc, - ); - - void dispose(); -} - -class EditableMediaCell extends EditableCellWidget { - EditableMediaCell({ - super.key, - required this.databaseController, - required this.cellContext, - required this.skin, - required this.style, - }); - - final DatabaseController databaseController; - final CellContext cellContext; - final IEditableMediaCellSkin skin; - final EditableCellStyle style; - - @override - GridEditableTextCell createState() => - _EditableMediaCellState(); -} - -class _EditableMediaCellState extends GridEditableTextCell { - final PopoverController popoverController = PopoverController(); - - late final cellBloc = MediaCellBloc( - cellController: makeCellController( - widget.databaseController, - widget.cellContext, - ).as(), - ); - - @override - void dispose() { - cellBloc.close(); - widget.skin.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: cellBloc..add(const MediaCellEvent.initial()), - child: Builder( - builder: (context) => widget.skin.build( - context, - widget.cellContainerNotifier, - popoverController, - cellBloc, - ), - ), - ); - } - - @override - SingleListenerFocusNode focusNode = SingleListenerFocusNode(); - - @override - void onRequestFocus() => widget.skin.autoShowPopover(widget.style) - ? popoverController.show() - : null; - - @override - String? onCopy() => null; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart index 4d2bfdf627..b218c78195 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'package:appflowy/plugins/database/application/cell/bloc/number_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,7 +29,6 @@ abstract class IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -90,7 +89,6 @@ class _NumberCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart index 67ca6275a6..4e39900abf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/relation_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,7 +28,6 @@ abstract class IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, @@ -75,7 +74,6 @@ class _RelationCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, state, _popover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart index f7e8b6f435..b45018f4f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; @@ -31,7 +31,6 @@ abstract class IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ); @@ -80,7 +79,6 @@ class _SelectOptionCellState extends GridCellState { child: widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, _popover, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart index 7a086b2a35..4b291719bb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/summary.dart @@ -7,12 +7,11 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/dispatch/error.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -38,7 +37,6 @@ abstract class IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -98,7 +96,6 @@ class _SummaryCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, @@ -152,22 +149,7 @@ class SummaryCellAccessory extends StatelessWidget { rowId: rowId, fieldId: fieldId, ), - child: BlocConsumer( - listenWhen: (previous, current) { - return previous.error != current.error; - }, - listener: (context, state) { - if (state.error != null) { - if (state.error!.isAIResponseLimitExceeded) { - showSnackBarMessage( - context, - LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(), - ); - } else { - showSnackBarMessage(context, state.error!.msg); - } - } - }, + child: BlocBuilder( builder: (context, state) { return const Row( children: [SummaryButton(), HSpace(6), CopyButton()], @@ -187,13 +169,13 @@ class SummaryButton extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return state.loadingState.when( - loading: () { + return state.loadingState.map( + loading: (_) { return const Center( child: CircularProgressIndicator.adaptive(), ); }, - finish: () { + finish: (_) { return FlowyTooltip( message: LocaleKeys.tooltip_aiGenerate.tr(), child: Container( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart index 3ea622374e..2cd318fa64 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart @@ -1,11 +1,11 @@ import 'dart:async'; -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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,7 +29,6 @@ abstract class IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -80,20 +79,16 @@ class _TextCellState extends GridEditableTextCell { return BlocProvider.value( value: cellBloc, child: BlocListener( - listenWhen: (previous, current) => previous.content != current.content, listener: (context, state) { - // It's essential to set the new content to the textEditingController. - // If you don't, the old value in textEditingController will persist and - // overwrite the correct value, leading to inconsistencies between the - // displayed text and the actual data. - _textEditingController.text = state.content ?? ""; + if (!focusNode.hasFocus) { + _textEditingController.text = state.content; + } }, child: Builder( builder: (context) { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart index 2fc9d049cc..6c00e8b4b4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,7 +28,6 @@ abstract class IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ); @@ -75,7 +74,6 @@ class _TimestampCellState extends GridCellState { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, state, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart index b273419aed..2d3fd33751 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart @@ -7,12 +7,11 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/dispatch/error.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -38,7 +37,6 @@ abstract class IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -98,7 +96,6 @@ class _TranslateCellState extends GridEditableTextCell { return widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, @@ -153,22 +150,7 @@ class TranslateCellAccessory extends StatelessWidget { rowId: rowId, fieldId: fieldId, ), - child: BlocConsumer( - listenWhen: (previous, current) { - return previous.error != current.error; - }, - listener: (context, state) { - if (state.error != null) { - if (state.error!.isAIResponseLimitExceeded) { - showSnackBarMessage( - context, - LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(), - ); - } else { - showSnackBarMessage(context, state.error!.msg); - } - } - }, + child: BlocBuilder( builder: (context, state) { return const Row( children: [TranslateButton(), HSpace(6), CopyButton()], diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart index 39616dbcf8..ef18573e1a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart @@ -4,13 +4,13 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/url_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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -40,7 +40,6 @@ abstract class IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, @@ -122,7 +121,6 @@ class _GridURLCellState extends GridEditableTextCell { child: widget.skin.build( context, widget.cellContainerNotifier, - widget.databaseController.compactModeNotifier, cellBloc, focusNode, _textEditingController, @@ -195,7 +193,7 @@ class MobileURLEditor extends StatelessWidget { icon: FlowySvgs.url_s, text: LocaleKeys.grid_url_launch.tr(), ), - const MobileQuickActionDivider(), + const Divider(height: 8.5, thickness: 0.5), MobileQuickActionButton( enable: context.watch().state.content.isNotEmpty, onTap: () { @@ -203,7 +201,7 @@ class MobileURLEditor extends StatelessWidget { ClipboardData(text: textEditingController.text), ); Fluttertoast.showToast( - msg: LocaleKeys.message_copy_success.tr(), + msg: LocaleKeys.grid_url_copiedNotification.tr(), gravity: ToastGravity.BOTTOM, ); context.pop(); @@ -211,6 +209,7 @@ class MobileURLEditor extends StatelessWidget { icon: FlowySvgs.copy_s, text: LocaleKeys.grid_url_copy.tr(), ), + const Divider(height: 8.5, thickness: 0.5), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart index e9ac19c874..8859372c2a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checkbox_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/checkbox.dart'; @@ -10,7 +10,6 @@ class MobileGridCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart index c56d28e1a7..aa55dd9e36 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_checklist_cell.dart @@ -1,9 +1,10 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,40 +16,36 @@ class MobileGridChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, + ChecklistCellState state, PopoverController popoverController, ) { - return BlocBuilder( - builder: (context, state) { - return FlowyButton( - radius: BorderRadius.zero, - hoverColor: Colors.transparent, - text: Container( - alignment: Alignment.centerLeft, - padding: GridSize.cellContentInsets, - child: state.tasks.isEmpty - ? const SizedBox.shrink() - : ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - textStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontSize: 15), - ), - ), - onTap: () => showMobileBottomSheet( - context, - builder: (context) { - return BlocProvider.value( - value: bloc, - child: const MobileChecklistCellEditScreen(), - ); - }, - ), - ); - }, + return FlowyButton( + radius: BorderRadius.zero, + hoverColor: Colors.transparent, + text: Container( + alignment: Alignment.centerLeft, + padding: GridSize.cellContentInsets, + child: state.tasks.isEmpty + ? const SizedBox.shrink() + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + textStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontSize: 15), + ), + ), + onTap: () => showMobileBottomSheet( + context, + builder: (context) { + return BlocProvider.value( + value: bloc, + child: const MobileChecklistCellEditScreen(), + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart index 5686e09295..7984322328 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_date_cell.dart @@ -1,26 +1,23 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class MobileGridDateCellSkin extends IEditableDateCellSkin { @override Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, ) { - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); return FlowyButton( radius: BorderRadius.zero, hoverColor: Colors.transparent, @@ -32,12 +29,12 @@ class MobileGridDateCellSkin extends IEditableDateCellSkin { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ - if (state.cellData.reminderId.isNotEmpty) ...[ + if (state.data?.reminderId.isNotEmpty ?? false) ...[ const FlowySvg(FlowySvgs.clock_alarm_s), const HSpace(6), ], FlowyText( - dateStr, + state.dateStr, fontSize: 15, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart index 310c0b5692..c02cf6aa8d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_number_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/number.dart'; @@ -9,7 +9,6 @@ class MobileGridNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart index 69e9b20104..5c31d41b30 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart @@ -1,6 +1,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -11,7 +12,6 @@ class MobileGridRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart index 010974e49a..9c01536e71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart @@ -1,9 +1,10 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -16,7 +17,6 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart index e48c56d74d..0da8d6bc64 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart @@ -12,7 +12,6 @@ class MobileGridSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart index 43a4fe49d7..bef6202934 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_text_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,35 +11,33 @@ class MobileGridTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { return Row( children: [ - const HSpace(10), BlocBuilder( buildWhen: (p, c) => p.emoji != c.emoji, builder: (context, state) => Center( - child: FlowyText.emoji( - state.emoji?.value ?? "", - fontSize: 15, - optimizeEmojiAlign: true, + child: FlowyText( + state.emoji, + fontSize: 16, ), ), ), + const HSpace(6), Expanded( child: TextField( controller: textEditingController, focusNode: focusNode, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 15, - ), + style: + Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15), decoration: const InputDecoration( enabledBorder: InputBorder.none, focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.symmetric(horizontal: 4), + contentPadding: + EdgeInsets.symmetric(horizontal: 14, vertical: 12), isCollapsed: true, ), onTapOutside: (event) => focusNode.unfocus(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart index 68209e7e05..d9e020eece 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_timestamp_cell.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -10,7 +10,6 @@ class MobileGridTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart index 4288136734..3a7b44cbc5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart @@ -12,7 +12,6 @@ class MobileGridTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index 0dbe5474c7..cddb821943 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -13,7 +13,6 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart index ade82e8c5c..2e9e4b1a24 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart @@ -11,7 +11,6 @@ class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, CheckboxCellBloc bloc, CheckboxCellState state, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart index 75eee9a560..67f9f1c53f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_b import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -17,54 +18,50 @@ class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, ChecklistCellBloc bloc, + ChecklistCellState state, PopoverController popoverController, ) { - return BlocBuilder( - builder: (context, state) { - return InkWell( + return InkWell( + borderRadius: const BorderRadius.all(Radius.circular(14)), + onTap: () => showMobileBottomSheet( + context, + backgroundColor: AFThemeExtension.of(context).background, + builder: (context) { + return BlocProvider.value( + value: bloc, + child: const MobileChecklistCellEditScreen(), + ); + }, + ), + child: Container( + constraints: const BoxConstraints( + minHeight: 48, + minWidth: double.infinity, + ), + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.outline), + ), borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - backgroundColor: AFThemeExtension.of(context).background, - builder: (context) { - return BlocProvider.value( - value: bloc, - child: const MobileChecklistCellEditScreen(), - ); - }, - ), - child: Container( - constraints: const BoxConstraints( - minHeight: 48, - minWidth: double.infinity, - ), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + alignment: AlignmentDirectional.centerStart, + child: state.tasks.isEmpty + ? FlowyText( + LocaleKeys.grid_row_textPlaceholder.tr(), + fontSize: 15, + color: Theme.of(context).hintColor, + ) + : ChecklistProgressBar( + tasks: state.tasks, + percent: state.percent, + textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 15, + color: Theme.of(context).hintColor, + ), ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - alignment: AlignmentDirectional.centerStart, - child: state.tasks.isEmpty - ? FlowyText( - LocaleKeys.grid_row_textPlaceholder.tr(), - fontSize: 15, - color: Theme.of(context).hintColor, - ) - : ChecklistProgressBar( - tasks: state.tasks, - percent: state.percent, - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 15, - color: Theme.of(context).hintColor, - ), - ), - ), - ); - }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart index 0256ee25cf..a5c500cdbc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_date_cell.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/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -14,18 +14,14 @@ class MobileRowDetailDateCellSkin extends IEditableDateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, DateCellBloc bloc, DateCellState state, PopoverController popoverController, ) { - final dateStr = getDateCellStrFromCellData( - state.fieldInfo, - state.cellData, - ); - final text = - dateStr.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : dateStr; - final color = dateStr.isEmpty ? Theme.of(context).hintColor : null; + final text = state.dateStr.isEmpty + ? LocaleKeys.grid_row_textPlaceholder.tr() + : state.dateStr; + final color = state.dateStr.isEmpty ? Theme.of(context).hintColor : null; return InkWell( borderRadius: const BorderRadius.all(Radius.circular(14)), @@ -50,19 +46,11 @@ class MobileRowDetailDateCellSkin extends IEditableDateCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - child: Row( - children: [ - if (state.cellData.reminderId.isNotEmpty) ...[ - const FlowySvg(FlowySvgs.clock_alarm_s), - const HSpace(6), - ], - FlowyText.regular( - text, - fontSize: 16, - color: color, - maxLines: null, - ), - ], + child: FlowyText.regular( + text, + fontSize: 16, + color: color, + maxLines: null, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart index 430044fb5c..6e32fbbbdc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_number_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/number_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,7 +11,6 @@ class MobileRowDetailNumberCellSkin extends IEditableNumberCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, NumberCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart index c3e8b82867..cdbcef64c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart @@ -1,7 +1,8 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/relation.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -10,7 +11,6 @@ class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, RelationCellBloc bloc, RelationCellState state, PopoverController popoverController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart index 7d4eb71f9d..59941394fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart @@ -1,10 +1,11 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -19,7 +20,6 @@ class MobileRowDetailSelectOptionCellSkin Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SelectOptionCellBloc bloc, PopoverController popoverController, ) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart index 9974220b96..a900cf62fb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart @@ -9,59 +9,46 @@ class MobileRowDetailSummaryCellSkin extends IEditableSummaryCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, SummaryCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), + return Column( + children: [ + TextField( + controller: textEditingController, + readOnly: true, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + maxLines: null, + minLines: 1, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - child: Column( - children: [ - TextField( - controller: textEditingController, - readOnly: true, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - maxLines: null, - minLines: 1, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), - Row( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: SummaryCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), + Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: SummaryCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, ), - ], - ), - ], - ), + ), + ], + ), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart index fc8f816103..1cdde84c27 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_text_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -11,7 +11,6 @@ class MobileRowDetailTextCellSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart index f3f800e994..2ca0a40b62 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_timestamp_cell.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,7 +12,6 @@ class MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TimestampCellBloc bloc, TimestampCellState state, ) { @@ -28,7 +27,7 @@ class MobileRowDetailTimestampCellSkin extends IEditableTimestampCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), ), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 13), - child: FlowyText( + child: FlowyText.medium( state.dateStr.isEmpty ? LocaleKeys.grid_row_textPlaceholder.tr() : state.dateStr, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart index c2d84b3d2e..84af6c7062 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart @@ -9,59 +9,46 @@ class MobileRowDetailTranslateCellSkin extends IEditableTranslateCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TranslateCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { - return Container( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), + return Column( + children: [ + TextField( + readOnly: true, + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + maxLines: null, + minLines: 1, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), ), - borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - child: Column( - children: [ - TextField( - readOnly: true, - controller: textEditingController, - focusNode: focusNode, - onEditingComplete: () => focusNode.unfocus(), - onSubmitted: (_) => focusNode.unfocus(), - style: Theme.of(context).textTheme.bodyMedium, - textInputAction: TextInputAction.done, - maxLines: null, - minLines: 1, - decoration: InputDecoration( - contentPadding: GridSize.cellContentInsets, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - isDense: true, - ), - ), - Row( - children: [ - const Spacer(), - Padding( - padding: const EdgeInsets.all(8.0), - child: TranslateCellAccessory( - viewId: bloc.cellController.viewId, - fieldId: bloc.cellController.fieldId, - rowId: bloc.cellController.rowId, - ), + Row( + children: [ + const Spacer(), + Padding( + padding: const EdgeInsets.all(8.0), + child: TranslateCellAccessory( + viewId: bloc.cellController.viewId, + fieldId: bloc.cellController.fieldId, + rowId: bloc.cellController.rowId, ), - ], - ), - ], - ), + ), + ], + ), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index 9bb91255aa..f87b225492 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -4,9 +4,9 @@ import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.da import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; import '../editable_cell_skeleton/url.dart'; @@ -15,7 +15,6 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, URLCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index 9853f9c1bd..ba2dc28702 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; @@ -14,7 +13,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../../application/cell/bloc/checklist_cell_bloc.dart'; import 'checklist_cell_textfield.dart'; @@ -110,43 +108,23 @@ class ChecklistItemList extends StatelessWidget { final itemList = options .mapIndexed( (index, option) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), - key: ValueKey(option.data.id), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ChecklistItem( task: option, - index: index, onSubmitted: index == options.length - 1 ? onUpdateTask : null, + key: ValueKey(option.data.id), ), ), ) .toList(); return Flexible( - child: ReorderableListView.builder( - shrinkWrap: true, - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: MouseRegion( - cursor: UniversalPlatform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: IgnorePointer( - child: BlocProvider.value( - value: context.read(), - child: child, - ), - ), - ), - ), - buildDefaultDragHandles: false, + child: ListView.separated( itemBuilder: (context, index) => itemList[index], + separatorBuilder: (context, index) => const VSpace(4), itemCount: itemList.length, - padding: const EdgeInsets.symmetric(vertical: 6.0), - onReorder: (from, to) { - context - .read() - .add(ChecklistCellEvent.reorderTask(from, to)); - }, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 8.0), ), ); } @@ -160,21 +138,17 @@ class _EndEditingTaskIntent extends Intent { const _EndEditingTaskIntent(); } -class _UpdateTaskDescriptionIntent extends Intent { - const _UpdateTaskDescriptionIntent(); -} - +/// Represents an existing task +@visibleForTesting class ChecklistItem extends StatefulWidget { const ChecklistItem({ super.key, required this.task, - required this.index, this.onSubmitted, this.autofocus = false, }); final ChecklistSelectOption task; - final int index; final VoidCallback? onSubmitted; final bool autofocus; @@ -189,17 +163,25 @@ class _ChecklistItemState extends State { bool isHovered = false; bool isFocused = false; - bool isComposing = false; final _debounceOnChanged = Debounce( duration: const Duration(milliseconds: 300), ); + final selectTaskShortcut = { + SingleActivator( + LogicalKeyboardKey.enter, + meta: Platform.isMacOS, + control: !Platform.isMacOS, + ): const _SelectTaskIntent(), + const SingleActivator(LogicalKeyboardKey.escape): + const _EndEditingTaskIntent(), + }; + @override void initState() { super.initState(); textController.text = widget.task.data.name; - textController.addListener(_onTextChanged); if (widget.autofocus) { WidgetsBinding.instance.addPostFrameCallback((_) { focusNode.requestFocus(); @@ -208,23 +190,10 @@ class _ChecklistItemState extends State { } } - void _onTextChanged() => - setState(() => isComposing = !textController.value.composing.isCollapsed); - - @override - void didUpdateWidget(covariant oldWidget) { - if (!focusNode.hasFocus && - oldWidget.task.data.name != widget.task.data.name) { - textController.text = widget.task.data.name; - } - super.didUpdateWidget(oldWidget); - } - @override void dispose() { _debounceOnChanged.dispose(); - textController.removeListener(_onTextChanged); textController.dispose(); focusNode.dispose(); textFieldFocusNode.dispose(); @@ -233,56 +202,49 @@ class _ChecklistItemState extends State { @override Widget build(BuildContext context) { - final isFocusedOrHovered = isHovered || isFocused; - final color = isFocusedOrHovered || textFieldFocusNode.hasFocus + final isFocusedOrHovered = + isHovered || isFocused || textFieldFocusNode.hasFocus; + final color = isFocusedOrHovered ? AFThemeExtension.of(context).lightGreyHover : Colors.transparent; return FocusableActionDetector( focusNode: focusNode, - onShowHoverHighlight: (value) => setState(() => isHovered = value), - onFocusChange: (value) => setState(() => isFocused = value), + onShowHoverHighlight: (value) => setState(() { + isHovered = value; + }), + onFocusChange: (value) => setState(() { + isFocused = value; + }), actions: _buildActions(), - shortcuts: _buildShortcuts(), + shortcuts: selectTaskShortcut, child: Container( - constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), - decoration: BoxDecoration(color: color, borderRadius: Corners.s6Border), - child: _buildChild(isFocusedOrHovered && !textFieldFocusNode.hasFocus), + constraints: BoxConstraints( + minHeight: GridSize.popoverItemHeight, + ), + decoration: BoxDecoration( + color: color, + borderRadius: Corners.s6Border, + ), + child: _buildChild( + context, + isFocusedOrHovered, + ), ), ); } - Widget _buildChild(bool showTrash) { + Widget _buildChild(BuildContext context, bool isFocusedOrHovered) { return Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ - ReorderableDragStartListener( - index: widget.index, - child: MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: SizedBox( - width: 20, - height: 32, - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: FlowySvg( - FlowySvgs.drag_element_s, - size: const Size.square(14), - color: AFThemeExtension.of(context).onBackground, - ), - ), - ), - ), - ), ChecklistCellCheckIcon(task: widget.task), Expanded( child: ChecklistCellTextfield( textController: textController, focusNode: textFieldFocusNode, + autofocus: widget.autofocus, onChanged: () { _debounceOnChanged.call(() { - if (!isComposing) { + if (textController.selection.isCollapsed) { _submitUpdateTaskDescription(textController.text); } }); @@ -298,32 +260,16 @@ class _ChecklistItemState extends State { }, ), ), - if (showTrash) + if (isFocusedOrHovered) ChecklistCellDeleteButton( - onPressed: () => context - .read() - .add(ChecklistCellEvent.deleteTask(widget.task.data.id)), + onPressed: () => context.read().add( + ChecklistCellEvent.deleteTask(widget.task.data.id), + ), ), ], ); } - Map _buildShortcuts() { - return { - SingleActivator( - LogicalKeyboardKey.enter, - meta: Platform.isMacOS, - control: !Platform.isMacOS, - ): const _SelectTaskIntent(), - if (!isComposing) - const SingleActivator(LogicalKeyboardKey.enter): - const _UpdateTaskDescriptionIntent(), - if (!isComposing) - const SingleActivator(LogicalKeyboardKey.escape): - const _EndEditingTaskIntent(), - }; - } - Map> _buildActions() { return { _SelectTaskIntent: CallbackAction<_SelectTaskIntent>( @@ -334,14 +280,6 @@ class _ChecklistItemState extends State { return; }, ), - _UpdateTaskDescriptionIntent: - CallbackAction<_UpdateTaskDescriptionIntent>( - onInvoke: (_UpdateTaskDescriptionIntent intent) { - textFieldFocusNode.unfocus(); - widget.onSubmitted?.call(); - return; - }, - ), _EndEditingTaskIntent: CallbackAction<_EndEditingTaskIntent>( onInvoke: (_EndEditingTaskIntent intent) { textFieldFocusNode.unfocus(); @@ -351,9 +289,14 @@ class _ChecklistItemState extends State { }; } - void _submitUpdateTaskDescription(String description) => context - .read() - .add(ChecklistCellEvent.updateTaskName(widget.task.data, description)); + void _submitUpdateTaskDescription(String description) { + context.read().add( + ChecklistCellEvent.updateTaskName( + widget.task.data, + description, + ), + ); + } } /// Creates a new task after entering the description and pressing enter. @@ -369,27 +312,19 @@ class NewTaskItem extends StatefulWidget { } class _NewTaskItemState extends State { - final textController = TextEditingController(); - - bool isCreateButtonEnabled = false; - bool isComposing = false; + final _textEditingController = TextEditingController(); @override void initState() { super.initState(); - textController.addListener(_onTextChanged); if (widget.focusNode.canRequestFocus) { widget.focusNode.requestFocus(); } } - void _onTextChanged() => - setState(() => isComposing = !textController.value.composing.isCollapsed); - @override void dispose() { - textController.removeListener(_onTextChanged); - textController.dispose(); + _textEditingController.dispose(); super.dispose(); } @@ -402,68 +337,56 @@ class _NewTaskItemState extends State { children: [ const HSpace(8), Expanded( - child: CallbackShortcuts( - bindings: isComposing - ? const {} - : { - const SingleActivator(LogicalKeyboardKey.enter): () => - _createNewTask(context), - }, - child: TextField( - focusNode: widget.focusNode, - controller: textController, - style: Theme.of(context).textTheme.bodyMedium, - maxLines: null, - decoration: InputDecoration( - border: InputBorder.none, - isCollapsed: true, - contentPadding: const EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 2.0, - ), - hintText: LocaleKeys.grid_checklist_addNew.tr(), - ), - onSubmitted: (_) => _createNewTask(context), - onChanged: (_) => setState( - () => isCreateButtonEnabled = textController.text.isNotEmpty, + child: TextField( + focusNode: widget.focusNode, + controller: _textEditingController, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: const EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 2.0, ), + hintText: LocaleKeys.grid_checklist_addNew.tr(), ), + onSubmitted: (taskDescription) { + if (taskDescription.isNotEmpty) { + context + .read() + .add(ChecklistCellEvent.createNewTask(taskDescription)); + _textEditingController.clear(); + } + widget.focusNode.requestFocus(); + }, + onChanged: (value) => setState(() {}), ), ), FlowyTextButton( LocaleKeys.grid_checklist_submitNewTask.tr(), fontSize: 11, - fillColor: isCreateButtonEnabled - ? Theme.of(context).colorScheme.primary - : Theme.of(context).disabledColor, - hoverColor: isCreateButtonEnabled - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).disabledColor, + fillColor: _textEditingController.text.isEmpty + ? Theme.of(context).disabledColor + : Theme.of(context).colorScheme.primary, + hoverColor: _textEditingController.text.isEmpty + ? Theme.of(context).disabledColor + : Theme.of(context).colorScheme.primaryContainer, fontColor: Theme.of(context).colorScheme.onPrimary, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - onPressed: isCreateButtonEnabled - ? () { + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + onPressed: _textEditingController.text.isEmpty + ? null + : () { context.read().add( - ChecklistCellEvent.createNewTask(textController.text), + ChecklistCellEvent.createNewTask( + _textEditingController.text, + ), ); widget.focusNode.requestFocus(); - textController.clear(); - } - : null, + _textEditingController.clear(); + }, ), ], ), ); } - - void _createNewTask(BuildContext context) { - final taskDescription = textController.text; - if (taskDescription.isNotEmpty) { - context - .read() - .add(ChecklistCellEvent.createNewTask(taskDescription)); - textController.clear(); - } - widget.focusNode.requestFocus(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart index 789a4adf46..abd519f31d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_textfield.dart @@ -40,36 +40,35 @@ class ChecklistCellTextfield extends StatelessWidget { super.key, required this.textController, required this.focusNode, - this.onChanged, - this.contentPadding = const EdgeInsets.symmetric( - vertical: 8, - horizontal: 2, - ), + required this.autofocus, + required this.onChanged, this.onSubmitted, }); final TextEditingController textController; final FocusNode focusNode; - final EdgeInsetsGeometry contentPadding; + final bool autofocus; final VoidCallback? onSubmitted; - final VoidCallback? onChanged; + final VoidCallback onChanged; @override Widget build(BuildContext context) { + const contentPadding = EdgeInsets.symmetric( + vertical: 6.0, + horizontal: 2.0, + ); return TextField( controller: textController, focusNode: focusNode, style: Theme.of(context).textTheme.bodyMedium, - maxLines: null, decoration: InputDecoration( border: InputBorder.none, isCollapsed: true, - isDense: true, contentPadding: contentPadding, hintText: LocaleKeys.grid_checklist_taskHint.tr(), ), textInputAction: onSubmitted == null ? TextInputAction.next : null, - onChanged: (_) => onChanged?.call(), + onChanged: (_) => onChanged(), onSubmitted: (_) => onSubmitted?.call(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart index 7e0b376f77..b51f662930 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_progress_bar.dart @@ -1,7 +1,7 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:percent_indicator/percent_indicator.dart'; - import '../../application/cell/bloc/checklist_cell_bloc.dart'; class ChecklistProgressBar extends StatefulWidget { @@ -32,41 +32,43 @@ class _ChecklistProgressBarState extends State { return Row( children: [ Expanded( - child: widget.tasks.isNotEmpty && - widget.tasks.length <= widget.segmentLimit - ? Row( - children: [ - ...List.generate( - widget.tasks.length, - (index) => Flexible( - child: Container( - decoration: BoxDecoration( - borderRadius: - const BorderRadius.all(Radius.circular(2)), - color: index < numFinishedTasks - ? completedTaskColor - : AFThemeExtension.of(context) - .progressBarBGColor, - ), - margin: const EdgeInsets.symmetric(horizontal: 1), - height: 4.0, - ), + child: Row( + children: [ + if (widget.tasks.isNotEmpty && + widget.tasks.length <= widget.segmentLimit) + ...List.generate( + widget.tasks.length, + (index) => Flexible( + child: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(2)), + color: index < numFinishedTasks + ? completedTaskColor + : AFThemeExtension.of(context).progressBarBGColor, ), + margin: const EdgeInsets.symmetric(horizontal: 1), + height: 4.0, ), - ], + ), ) - : LinearPercentIndicator( - lineHeight: 4.0, - percent: widget.percent, - padding: EdgeInsets.zero, - progressColor: completedTaskColor, - backgroundColor: - AFThemeExtension.of(context).progressBarBGColor, - barRadius: const Radius.circular(2), + else + Expanded( + child: LinearPercentIndicator( + lineHeight: 4.0, + percent: widget.percent, + padding: EdgeInsets.zero, + progressColor: completedTaskColor, + backgroundColor: + AFThemeExtension.of(context).progressBarBGColor, + barRadius: const Radius.circular(2), + ), ), + ], + ), ), SizedBox( - width: 45, + width: PlatformExtension.isDesktop ? 36 : 45, child: Align( alignment: AlignmentDirectional.centerEnd, child: Text( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart deleted file mode 100644 index 3ee7f1ef56..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.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_popover/appflowy_popover.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/cell/bloc/date_cell_editor_bloc.dart'; - -class DateCellEditor extends StatefulWidget { - const DateCellEditor({ - super.key, - required this.onDismissed, - required this.cellController, - }); - - final VoidCallback onDismissed; - final DateCellController cellController; - - @override - State createState() => _DateCellEditor(); -} - -class _DateCellEditor extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => DateCellEditorBloc( - reminderBloc: getIt(), - cellController: widget.cellController, - ), - child: BlocBuilder( - builder: (context, state) { - final dateCellBloc = context.read(); - return DesktopAppFlowyDatePicker( - dateTime: state.dateTime, - endDateTime: state.endDateTime, - dateFormat: state.dateTypeOptionPB.dateFormat, - timeFormat: state.dateTypeOptionPB.timeFormat, - includeTime: state.includeTime, - isRange: state.isRange, - reminderOption: state.reminderOption, - popoverMutex: popoverMutex, - options: [ - OptionGroup( - options: [ - DateTypeOptionButton( - popoverMutex: popoverMutex, - dateFormat: state.dateTypeOptionPB.dateFormat, - timeFormat: state.dateTypeOptionPB.timeFormat, - onDateFormatChanged: (format) { - dateCellBloc - .add(DateCellEditorEvent.setDateFormat(format)); - }, - onTimeFormatChanged: (format) { - dateCellBloc - .add(DateCellEditorEvent.setTimeFormat(format)); - }, - ), - ClearDateButton( - onClearDate: () { - dateCellBloc.add(const DateCellEditorEvent.clearDate()); - }, - ), - ], - ), - ], - onIncludeTimeChanged: (value, dateTime, endDateTime) { - dateCellBloc.add( - DateCellEditorEvent.setIncludeTime( - value, - dateTime, - endDateTime, - ), - ); - }, - onIsRangeChanged: (value, dateTime, endDateTime) { - dateCellBloc.add( - DateCellEditorEvent.setIsRange(value, dateTime, endDateTime), - ); - }, - onDaySelected: (selectedDay) { - dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay)); - }, - onRangeSelected: (start, end) { - dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end)); - }, - onReminderSelected: (option) { - dateCellBloc.add(DateCellEditorEvent.setReminderOption(option)); - }, - ); - }, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart new file mode 100644 index 0000000000..e0d7c3944f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_editor.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.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_popover/appflowy_popover.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../application/cell/bloc/date_cell_editor_bloc.dart'; + +class DateCellEditor extends StatefulWidget { + const DateCellEditor({ + super.key, + required this.onDismissed, + required this.cellController, + }); + + final VoidCallback onDismissed; + final DateCellController cellController; + + @override + State createState() => _DateCellEditor(); +} + +class _DateCellEditor extends State { + final PopoverMutex popoverMutex = PopoverMutex(); + + @override + void dispose() { + popoverMutex.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DateCellEditorBloc( + reminderBloc: getIt(), + cellController: widget.cellController, + ), + ), + ], + child: BlocBuilder( + builder: (context, state) { + final dateCellBloc = context.read(); + return AppFlowyDatePicker( + includeTime: state.includeTime, + rebuildOnDaySelected: false, + onIncludeTimeChanged: (value) => + dateCellBloc.add(DateCellEditorEvent.setIncludeTime(!value)), + isRange: state.isRange, + startDay: state.isRange ? state.startDay : null, + endDay: state.isRange ? state.endDay : null, + onIsRangeChanged: (value) => + dateCellBloc.add(DateCellEditorEvent.setIsRange(!value)), + dateFormat: state.dateTypeOptionPB.dateFormat, + timeFormat: state.dateTypeOptionPB.timeFormat, + selectedDay: state.dateTime, + timeStr: state.timeStr, + endTimeStr: state.endTimeStr, + timeHintText: state.timeHintText, + parseEndTimeError: state.parseEndTimeError, + parseTimeError: state.parseTimeError, + popoverMutex: popoverMutex, + onReminderSelected: (option) => dateCellBloc.add( + DateCellEditorEvent.setReminderOption( + option: option, + selectedDay: state.dateTime == null ? DateTime.now() : null, + ), + ), + selectedReminderOption: state.reminderOption, + options: [ + OptionGroup( + options: [ + DateTypeOptionButton( + popoverMutex: popoverMutex, + dateFormat: state.dateTypeOptionPB.dateFormat, + timeFormat: state.dateTypeOptionPB.timeFormat, + onDateFormatChanged: (format) => dateCellBloc + .add(DateCellEditorEvent.setDateFormat(format)), + onTimeFormatChanged: (format) => dateCellBloc + .add(DateCellEditorEvent.setTimeFormat(format)), + ), + ClearDateButton( + onClearDate: () => + dateCellBloc.add(const DateCellEditorEvent.clearDate()), + ), + ], + ), + ], + onStartTimeSubmitted: (timeStr) => + dateCellBloc.add(DateCellEditorEvent.setTime(timeStr)), + onEndTimeSubmitted: (timeStr) => + dateCellBloc.add(DateCellEditorEvent.setEndTime(timeStr)), + onDaySelected: (selectedDay, _) => + dateCellBloc.add(DateCellEditorEvent.selectDay(selectedDay)), + onRangeSelected: (start, end, _) => dateCellBloc + .add(DateCellEditorEvent.selectDateRange(start, end)), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart index c29c7a23c1..d4d93d4f4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart @@ -1,11 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.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:universal_platform/universal_platform.dart'; extension SelectOptionColorExtension on SelectOptionColorPB { Color toColor(BuildContext context) { @@ -70,7 +70,6 @@ class SelectOptionTag extends StatelessWidget { this.onRemove, this.textAlign, this.isExpanded = false, - this.borderRadius, required this.padding, }) : assert(option != null || name != null && color != null); @@ -81,7 +80,6 @@ class SelectOptionTag extends StatelessWidget { final TextStyle? textStyle; final void Function(String)? onRemove; final EdgeInsets padding; - final BorderRadius? borderRadius; final TextAlign? textAlign; final bool isExpanded; @@ -89,7 +87,7 @@ class SelectOptionTag extends StatelessWidget { Widget build(BuildContext context) { final optionName = option?.name ?? name!; final optionColor = option?.color.toColor(context) ?? color!; - final text = FlowyText( + final text = FlowyText.medium( optionName, fontSize: fontSize, overflow: TextOverflow.ellipsis, @@ -98,11 +96,15 @@ class SelectOptionTag extends StatelessWidget { ); return Container( + height: 20, padding: onRemove == null ? padding : padding.copyWith(right: 2.0), decoration: BoxDecoration( color: optionColor, - borderRadius: borderRadius ?? - BorderRadius.circular(UniversalPlatform.isDesktopOrWeb ? 6 : 11), + borderRadius: BorderRadius.all( + Radius.circular( + PlatformExtension.isDesktopOrWeb ? 6 : 11, + ), + ), ), child: Row( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart deleted file mode 100644 index eba42c1f97..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/media_cell_editor.dart +++ /dev/null @@ -1,622 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/util/xfile_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:cross_file/cross_file.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:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class MediaCellEditor extends StatefulWidget { - const MediaCellEditor({super.key}); - - @override - State createState() => _MediaCellEditorState(); -} - -class _MediaCellEditorState extends State { - final addFilePopoverController = PopoverController(); - final itemMutex = PopoverMutex(); - - @override - void dispose() { - addFilePopoverController.close(); - itemMutex.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final images = state.files - .where((file) => file.fileType == MediaFileTypePB.Image) - .toList(); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (state.files.isNotEmpty) ...[ - Flexible( - child: ReorderableListView.builder( - padding: const EdgeInsets.all(6), - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - buildDefaultDragHandles: false, - itemBuilder: (_, index) => BlocProvider.value( - key: Key(state.files[index].id), - value: context.read(), - child: RenderMedia( - file: state.files[index], - images: images, - index: index, - enableReordering: state.files.length > 1, - mutex: itemMutex, - ), - ), - itemCount: state.files.length, - onReorder: (from, to) { - if (from < to) { - to--; - } - - context - .read() - .add(MediaCellEvent.reorderFiles(from: from, to: to)); - }, - proxyDecorator: (child, index, animation) => Material( - color: Colors.transparent, - child: SizeTransition( - sizeFactor: animation, - child: child, - ), - ), - ), - ), - ], - _AddButton(addFilePopoverController: addFilePopoverController), - ], - ); - }, - ); - } -} - -class _AddButton extends StatelessWidget { - const _AddButton({required this.addFilePopoverController}); - - final PopoverController addFilePopoverController; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: addFilePopoverController, - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - constraints: const BoxConstraints(maxWidth: 350), - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (popoverContext) => FileUploadMenu( - allowMultipleFiles: true, - onInsertLocalFile: (files) async => insertLocalFiles( - context, - files, - userProfile: context.read().state.userProfile, - documentId: context.read().rowId, - onUploadSuccess: (file, path, isLocalMode) { - final mediaCellBloc = context.read(); - if (mediaCellBloc.isClosed) { - return; - } - - mediaCellBloc.add( - MediaCellEvent.addFile( - url: path, - name: file.name, - uploadType: isLocalMode - ? FileUploadTypePB.LocalFile - : FileUploadTypePB.CloudFile, - fileType: file.fileType.toMediaFileTypePB(), - ), - ); - - addFilePopoverController.close(); - }, - ), - onInsertNetworkFile: (url) { - if (url.isEmpty) return; - - final uri = Uri.tryParse(url); - if (uri == null) { - return; - } - - final fakeFile = XFile(uri.path); - MediaFileTypePB fileType = fakeFile.fileType.toMediaFileTypePB(); - fileType = fileType == MediaFileTypePB.Other - ? MediaFileTypePB.Link - : fileType; - - String name = - uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ""; - if (name.isEmpty && uri.pathSegments.length > 1) { - name = uri.pathSegments[uri.pathSegments.length - 2]; - } else if (name.isEmpty) { - name = uri.host; - } - - context.read().add( - MediaCellEvent.addFile( - url: url, - name: name, - uploadType: FileUploadTypePB.NetworkFile, - fileType: fileType, - ), - ); - - addFilePopoverController.close(); - }, - ), - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - top: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: addFilePopoverController.show, - child: FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: const EdgeInsets.all(4.0), - child: Row( - children: [ - FlowySvg( - FlowySvgs.add_thin_s, - size: const Size.square(14), - color: AFThemeExtension.of(context).lightIconColor, - ), - const HSpace(8), - FlowyText.regular( - LocaleKeys.grid_media_addFileOrImage.tr(), - figmaLineHeight: 20, - fontSize: 14, - ), - ], - ), - ), - ), - ), - ), - ), - ); - } -} - -extension ToCustomImageType on FileUploadTypePB { - CustomImageType toCustomImageType() => switch (this) { - FileUploadTypePB.NetworkFile => CustomImageType.external, - FileUploadTypePB.CloudFile => CustomImageType.internal, - _ => CustomImageType.local, - }; -} - -@visibleForTesting -class RenderMedia extends StatefulWidget { - const RenderMedia({ - super.key, - required this.index, - required this.file, - required this.images, - required this.enableReordering, - required this.mutex, - }); - - final int index; - final MediaFilePB file; - final List images; - final bool enableReordering; - final PopoverMutex mutex; - - @override - State createState() => _RenderMediaState(); -} - -class _RenderMediaState extends State { - bool isHovering = false; - int? imageIndex; - - MediaFilePB get file => widget.file; - - late final controller = PopoverController(); - - @override - void initState() { - super.initState(); - imageIndex = widget.images.indexOf(file); - } - - @override - void didUpdateWidget(covariant RenderMedia oldWidget) { - imageIndex = widget.images.indexOf(file); - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 4), - child: MouseRegion( - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - cursor: SystemMouseCursors.click, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: isHovering - ? AFThemeExtension.of(context).greyHover - : Colors.transparent, - ), - child: Row( - crossAxisAlignment: widget.file.fileType == MediaFileTypePB.Image - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - ReorderableDragStartListener( - index: widget.index, - enabled: widget.enableReordering, - child: Padding( - padding: const EdgeInsets.all(4), - child: FlowySvg( - FlowySvgs.drag_element_s, - color: AFThemeExtension.of(context).lightIconColor, - ), - ), - ), - const HSpace(4), - if (widget.file.fileType == MediaFileTypePB.Image) ...[ - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: _openInteractiveViewer( - context, - files: widget.images, - index: imageIndex!, - child: AFImage( - url: widget.file.url, - uploadType: widget.file.uploadType, - userProfile: - context.read().state.userProfile, - ), - ), - ), - ), - ] else ...[ - Expanded( - child: GestureDetector( - onTap: () => afLaunchUrlString(file.url), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: FlowySvg( - file.fileType.icon, - color: AFThemeExtension.of(context).strongText, - size: const Size.square(12), - ), - ), - const HSpace(8), - Flexible( - child: Padding( - padding: const EdgeInsets.only(bottom: 1), - child: FlowyText( - file.name, - overflow: TextOverflow.ellipsis, - fontSize: 14, - ), - ), - ), - ], - ), - ), - ), - ], - const HSpace(4), - AppFlowyPopover( - controller: controller, - mutex: widget.mutex, - asBarrier: true, - offset: const Offset(0, 4), - constraints: const BoxConstraints(maxWidth: 240), - direction: PopoverDirection.bottomWithLeftAligned, - popupBuilder: (popoverContext) => BlocProvider.value( - value: context.read(), - child: MediaItemMenu( - file: file, - images: widget.images, - index: imageIndex ?? -1, - closeContext: popoverContext, - onAction: () => controller.close(), - ), - ), - child: FlowyIconButton( - hoverColor: Colors.transparent, - width: 24, - icon: FlowySvg( - FlowySvgs.three_dots_s, - size: const Size.square(16), - color: AFThemeExtension.of(context).lightIconColor, - ), - ), - ), - ], - ), - ), - ), - ); - } - - Widget _openInteractiveViewer( - BuildContext context, { - required List files, - required int index, - required Widget child, - }) => - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => openInteractiveViewerFromFiles( - context, - files, - onDeleteImage: (index) { - final deleteFile = files[index]; - context.read().deleteFile(deleteFile.id); - }, - userProfile: context.read().state.userProfile, - initialIndex: index, - ), - child: child, - ); -} - -class MediaItemMenu extends StatefulWidget { - const MediaItemMenu({ - super.key, - required this.file, - required this.images, - required this.index, - this.closeContext, - this.onAction, - }); - - /// The [MediaFilePB] this menu concerns - final MediaFilePB file; - - /// The list of [MediaFilePB] which are images - /// This is used to show the [InteractiveImageViewer] - final List images; - - /// The index of the [MediaFilePB] in the [images] list - final int index; - - /// The [BuildContext] used to show the [InteractiveImageViewer] - final BuildContext? closeContext; - - /// Callback to be called when an action is performed - final VoidCallback? onAction; - - @override - State createState() => _MediaItemMenuState(); -} - -class _MediaItemMenuState extends State { - late final nameController = TextEditingController(text: widget.file.name); - final errorMessage = ValueNotifier(null); - - BuildContext? renameContext; - - @override - void dispose() { - nameController.dispose(); - errorMessage.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SeparatedColumn( - separatorBuilder: () => const VSpace(8), - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.file.fileType == MediaFileTypePB.Image) ...[ - MediaMenuItem( - onTap: () { - widget.onAction?.call(); - _showInteractiveViewer(); - }, - icon: FlowySvgs.full_view_s, - label: LocaleKeys.grid_media_expand.tr(), - ), - MediaMenuItem( - onTap: () { - context.read().add( - MediaCellEvent.setCover( - RowCoverPB( - data: widget.file.url, - uploadType: widget.file.uploadType, - coverType: CoverTypePB.FileCover, - ), - ), - ); - widget.onAction?.call(); - }, - icon: FlowySvgs.cover_s, - label: LocaleKeys.grid_media_setAsCover.tr(), - ), - ], - MediaMenuItem( - onTap: () { - widget.onAction?.call(); - afLaunchUrlString(widget.file.url); - }, - icon: FlowySvgs.open_in_browser_s, - label: LocaleKeys.grid_media_openInBrowser.tr(), - ), - MediaMenuItem( - onTap: () async { - await _showRenameDialog(); - widget.onAction?.call(); - }, - icon: FlowySvgs.rename_s, - label: LocaleKeys.grid_media_rename.tr(), - ), - if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[ - MediaMenuItem( - onTap: () async { - await downloadMediaFile( - context, - widget.file, - userProfile: context.read().state.userProfile, - ); - widget.onAction?.call(); - }, - icon: FlowySvgs.save_as_s, - label: LocaleKeys.button_download.tr(), - ), - ], - MediaMenuItem( - onTap: () async { - await showConfirmDeletionDialog( - context: context, - name: widget.file.name, - description: LocaleKeys.grid_media_deleteFileDescription.tr(), - onConfirm: () => context - .read() - .add(MediaCellEvent.removeFile(fileId: widget.file.id)), - ); - widget.onAction?.call(); - }, - icon: FlowySvgs.trash_s, - label: LocaleKeys.button_delete.tr(), - ), - ], - ); - } - - Future _showRenameDialog() async { - nameController.selection = TextSelection( - baseOffset: 0, - extentOffset: nameController.text.length, - ); - - await showCustomConfirmDialog( - context: context, - title: LocaleKeys.document_plugins_file_renameFile_title.tr(), - description: LocaleKeys.document_plugins_file_renameFile_description.tr(), - closeOnConfirm: false, - builder: (dialogContext) { - renameContext = dialogContext; - return FileRenameTextField( - nameController: nameController, - errorMessage: errorMessage, - onSubmitted: () => _saveName(context), - disposeController: false, - ); - }, - confirmLabel: LocaleKeys.button_save.tr(), - onConfirm: () => _saveName(context), - ); - } - - void _saveName(BuildContext context) { - if (nameController.text.isEmpty) { - errorMessage.value = - LocaleKeys.document_plugins_file_renameFile_nameEmptyError.tr(); - return; - } - - context.read().add( - MediaCellEvent.renameFile( - fileId: widget.file.id, - name: nameController.text, - ), - ); - - if (renameContext != null) { - Navigator.of(renameContext!).pop(); - } - } - - void _showInteractiveViewer() { - showDialog( - context: widget.closeContext ?? context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfile, - imageProvider: AFBlockImageProvider( - initialIndex: widget.index, - images: widget.images - .map( - (e) => ImageBlockData( - url: e.url, - type: e.uploadType.toCustomImageType(), - ), - ) - .toList(), - onDeleteImage: (index) { - final deleteFile = widget.images[index]; - context.read().deleteFile(deleteFile.id); - }, - ), - ), - ); - } -} - -class MediaMenuItem extends StatelessWidget { - const MediaMenuItem({ - super.key, - required this.onTap, - required this.icon, - required this.label, - }); - - final VoidCallback onTap; - final FlowySvgData icon; - final String label; - - @override - Widget build(BuildContext context) { - return FlowyButton( - onTap: onTap, - leftIcon: FlowySvg(icon), - text: Padding( - padding: const EdgeInsets.only(left: 4, top: 1, bottom: 1), - child: FlowyText.regular( - label, - figmaLineHeight: 20, - ), - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart index 6e86d88e5b..fb8b11474c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; @@ -10,11 +8,14 @@ import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_b import 'package:collection/collection.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'; class MobileChecklistCellEditScreen extends StatefulWidget { - const MobileChecklistCellEditScreen({super.key}); + const MobileChecklistCellEditScreen({ + super.key, + }); @override State createState() => @@ -27,24 +28,41 @@ class _MobileChecklistCellEditScreenState Widget build(BuildContext context) { return ConstrainedBox( constraints: const BoxConstraints.tightFor(height: 420), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: _buildHeader(context), - ), - const Divider(), - const Expanded(child: _TaskList()), - ], + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const DragHandle(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: _buildHeader(context), + ), + const Divider(), + const Expanded(child: _TaskList()), + ], + ); + }, ), ); } Widget _buildHeader(BuildContext context) { + const iconWidth = 36.0; + const height = 44.0; return Stack( children: [ + Align( + alignment: Alignment.centerLeft, + child: FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(iconWidth), + ), + width: iconWidth, + onPressed: () => context.pop(), + ), + ), SizedBox( height: 44.0, child: Align( @@ -54,7 +72,7 @@ class _MobileChecklistCellEditScreenState ), ), ), - ], + ].map((e) => SizedBox(height: height, child: e)).toList(), ); } } @@ -71,42 +89,19 @@ class _TaskList extends StatelessWidget { state.tasks .mapIndexed( (index, task) => _ChecklistItem( - key: ValueKey('mobile_checklist_task_${task.data.id}'), task: task, - index: index, - autofocus: state.phantomIndex != null && - index == state.tasks.length - 1, - onAutofocus: () { - context - .read() - .add(const ChecklistCellEvent.updatePhantomIndex(null)); - }, + autofocus: state.newTask && index == state.tasks.length - 1, ), ) .toList(), ); - cells.add( - const _NewTaskButton(key: ValueKey('mobile_checklist_new_task')), - ); + cells.add(const _NewTaskButton()); - return ReorderableListView.builder( - shrinkWrap: true, - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: BlocProvider.value( - value: context.read(), - child: child, - ), - ), - buildDefaultDragHandles: false, + return ListView.separated( itemCount: cells.length, - itemBuilder: (_, index) => cells[index], + separatorBuilder: (_, __) => const VSpace(8), + itemBuilder: (_, int index) => cells[index], padding: const EdgeInsets.only(bottom: 12.0), - onReorder: (from, to) { - context - .read() - .add(ChecklistCellEvent.reorderTask(from, to)); - }, ); }, ); @@ -114,37 +109,26 @@ class _TaskList extends StatelessWidget { } class _ChecklistItem extends StatefulWidget { - const _ChecklistItem({ - super.key, - required this.task, - required this.index, - required this.autofocus, - this.onAutofocus, - }); + const _ChecklistItem({required this.task, required this.autofocus}); final ChecklistSelectOption task; - final int index; final bool autofocus; - final VoidCallback? onAutofocus; @override State<_ChecklistItem> createState() => _ChecklistItemState(); } class _ChecklistItemState extends State<_ChecklistItem> { - late final TextEditingController textController; - final FocusNode focusNode = FocusNode(); + late final TextEditingController _textController; + final FocusNode _focusNode = FocusNode(); Timer? _debounceOnChanged; @override void initState() { super.initState(); - textController = TextEditingController(text: widget.task.data.name); + _textController = TextEditingController(text: widget.task.data.name); if (widget.autofocus) { - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - widget.onAutofocus?.call(); - }); + _focusNode.requestFocus(); } } @@ -152,53 +136,48 @@ class _ChecklistItemState extends State<_ChecklistItem> { void didUpdateWidget(covariant oldWidget) { super.didUpdateWidget(oldWidget); if (widget.task.data.name != oldWidget.task.data.name && - !focusNode.hasFocus) { - textController.text = widget.task.data.name; + !_focusNode.hasFocus) { + _textController.text = widget.task.data.name; } } @override void dispose() { - textController.dispose(); - focusNode.dispose(); + _textController.dispose(); + _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4), - constraints: const BoxConstraints(minHeight: 44), + padding: const EdgeInsets.symmetric(horizontal: 5), + height: 44, child: Row( children: [ - ReorderableDelayedDragStartListener( - index: widget.index, - child: InkWell( - borderRadius: BorderRadius.circular(22), - onTap: () => context - .read() - .add(ChecklistCellEvent.selectTask(widget.task.data.id)), - child: SizedBox.square( - dimension: 44, - child: Center( - child: FlowySvg( - widget.task.isSelected - ? FlowySvgs.check_filled_s - : FlowySvgs.uncheck_s, - size: const Size.square(20.0), - blendMode: BlendMode.dst, - ), + InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () => context + .read() + .add(ChecklistCellEvent.selectTask(widget.task.data.id)), + child: SizedBox.square( + dimension: 44, + child: Center( + child: FlowySvg( + widget.task.isSelected + ? FlowySvgs.check_filled_s + : FlowySvgs.uncheck_s, + size: const Size.square(20.0), + blendMode: BlendMode.dst, ), ), ), ), Expanded( child: TextField( - controller: textController, - focusNode: focusNode, + controller: _textController, + focusNode: _focusNode, style: Theme.of(context).textTheme.bodyMedium, - keyboardType: TextInputType.multiline, - maxLines: null, decoration: InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, @@ -294,7 +273,7 @@ class _ChecklistItemState extends State<_ChecklistItem> { } class _NewTaskButton extends StatelessWidget { - const _NewTaskButton({super.key}); + const _NewTaskButton(); @override Widget build(BuildContext context) { @@ -303,9 +282,6 @@ class _NewTaskButton extends StatelessWidget { child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { - context - .read() - .add(const ChecklistCellEvent.updatePhantomIndex(-1)); context .read() .add(const ChecklistCellEvent.createNewTask("")); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart deleted file mode 100644 index 2960a6a34d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_media_cell_editor.dart +++ /dev/null @@ -1,310 +0,0 @@ -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/bottom_sheet/bottom_sheet_media_upload.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/media_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart'; -import 'package:appflowy/plugins/database/widgets/media_file_type_ext.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.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'; -import 'package:go_router/go_router.dart'; - -class MobileMediaCellEditor extends StatelessWidget { - const MobileMediaCellEditor({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - constraints: const BoxConstraints.tightFor(height: 420), - child: BlocProvider.value( - value: context.read(), - child: BlocBuilder( - builder: (context, state) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - const DragHandle(), - SizedBox( - height: 46.0, - child: Stack( - children: [ - Align( - child: FlowyText.medium( - LocaleKeys.grid_field_mediaFieldName.tr(), - fontSize: 18, - ), - ), - Positioned( - top: 8, - right: 18, - child: GestureDetector( - onTap: () => showMobileBottomSheet( - context, - title: LocaleKeys.grid_media_addFileMobile.tr(), - showHeader: true, - showCloseButton: true, - showDragHandle: true, - builder: (dContext) => BlocProvider.value( - value: context.read(), - child: MobileMediaUploadSheetContent( - dialogContext: dContext, - ), - ), - ), - child: const FlowySvg( - FlowySvgs.add_m, - size: Size.square(28), - ), - ), - ), - ], - ), - ), - const Divider(height: 0.5), - Expanded( - child: SingleChildScrollView( - child: Column( - children: [ - if (state.files.isNotEmpty) const Divider(height: .5), - ...state.files.map( - (file) => Padding( - padding: const EdgeInsets.only(bottom: 2), - child: _FileItem(key: Key(file.id), file: file), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class _FileItem extends StatelessWidget { - const _FileItem({super.key, required this.file}); - - final MediaFilePB file; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: ListTile( - contentPadding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - title: Row( - crossAxisAlignment: file.fileType == MediaFileTypePB.Image - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - if (file.fileType != MediaFileTypePB.Image) ...[ - Flexible( - child: GestureDetector( - onTap: () => afLaunchUrlString(file.url), - child: Row( - children: [ - FlowySvg(file.fileType.icon, size: const Size.square(24)), - const HSpace(12), - Expanded( - child: FlowyText( - file.name, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ] else ...[ - Expanded( - child: Container( - alignment: Alignment.centerLeft, - constraints: const BoxConstraints(maxHeight: 96), - child: GestureDetector( - onTap: () => openInteractiveViewer(context), - child: ImageRender( - userProfile: - context.read().state.userProfile, - fit: BoxFit.fitHeight, - borderRadius: BorderRadius.zero, - image: ImageBlockData( - url: file.url, - type: file.uploadType.toCustomImageType(), - ), - ), - ), - ), - ), - ], - FlowyIconButton( - width: 20, - icon: const FlowySvg( - FlowySvgs.three_dots_s, - size: Size.square(20), - ), - onPressed: () => showMobileBottomSheet( - context, - backgroundColor: Theme.of(context).colorScheme.surface, - showDragHandle: true, - builder: (_) => BlocProvider.value( - value: context.read(), - child: _EditFileSheet(file: file), - ), - ), - ), - const HSpace(6), - ], - ), - ), - ); - } - - void openInteractiveViewer(BuildContext context) => - openInteractiveViewerFromFile( - context, - file, - onDeleteImage: (_) => context.read().deleteFile(file.id), - userProfile: context.read().state.userProfile, - ); -} - -class _EditFileSheet extends StatefulWidget { - const _EditFileSheet({required this.file}); - - final MediaFilePB file; - - @override - State<_EditFileSheet> createState() => _EditFileSheetState(); -} - -class _EditFileSheetState extends State<_EditFileSheet> { - late final controller = TextEditingController(text: widget.file.name); - Loading? loader; - - MediaFilePB get file => widget.file; - - @override - void dispose() { - controller.dispose(); - loader?.stop(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - children: [ - const VSpace(16), - if (file.fileType == MediaFileTypePB.Image) ...[ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.grid_media_expand.tr(), - leftIcon: const FlowySvg( - FlowySvgs.full_view_s, - size: Size.square(20), - ), - onTap: () => openInteractiveViewer(context), - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.grid_media_setAsCover.tr(), - leftIcon: const FlowySvg( - FlowySvgs.cover_s, - size: Size.square(20), - ), - onTap: () => context.read().add( - MediaCellEvent.setCover( - RowCoverPB( - data: file.url, - uploadType: file.uploadType, - coverType: CoverTypePB.FileCover, - ), - ), - ), - ), - ], - FlowyOptionTile.text( - showTopBorder: file.fileType == MediaFileTypePB.Image, - text: LocaleKeys.grid_media_openInBrowser.tr(), - leftIcon: const FlowySvg( - FlowySvgs.open_in_browser_s, - size: Size.square(20), - ), - onTap: () => afLaunchUrlString(file.url), - ), - // TODO(Mathias): Rename interaction need design - // FlowyOptionTile.text( - // text: LocaleKeys.grid_media_rename.tr(), - // leftIcon: const FlowySvg( - // FlowySvgs.rename_s, - // size: Size.square(20), - // ), - // onTap: () {}, - // ), - if (widget.file.uploadType == FileUploadTypePB.CloudFile) ...[ - FlowyOptionTile.text( - onTap: () async => downloadMediaFile( - context, - file, - userProfile: context.read().state.userProfile, - onDownloadBegin: () { - loader?.stop(); - loader = Loading(context); - loader?.start(); - }, - onDownloadEnd: () => loader?.stop(), - ), - text: LocaleKeys.button_download.tr(), - leftIcon: const FlowySvg( - FlowySvgs.save_as_s, - size: Size.square(20), - ), - ), - ], - FlowyOptionTile.text( - text: LocaleKeys.grid_media_delete.tr(), - textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.trash_s, - size: const Size.square(20), - color: Theme.of(context).colorScheme.error, - ), - onTap: () { - context.pop(); - context.read().deleteFile(file.id); - }, - ), - ], - ), - ); - } - - void openInteractiveViewer(BuildContext context) => - openInteractiveViewerFromFile( - context, - file, - onDeleteImage: (_) => context.read().deleteFile(file.id), - userProfile: context.read().state.userProfile, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart index bd84c9074d..3750a9294b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; @@ -14,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum. import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.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'; import 'package:go_router/go_router.dart'; import 'package:protobuf/protobuf.dart'; @@ -169,15 +168,15 @@ class _MobileSelectOptionEditorState extends State { .add(const SelectOptionCellEditorEvent.createOption()); searchController.clear(); }, - onCheck: (option, isSelected) { - if (isSelected) { - context - .read() - .add(SelectOptionCellEditorEvent.unselectOption(option.id)); - } else { + onCheck: (option, value) { + if (value) { context .read() .add(SelectOptionCellEditorEvent.selectOption(option.id)); + } else { + context + .read() + .add(SelectOptionCellEditorEvent.unSelectOption(option.id)); } }, onMoreOptions: (option) { @@ -275,13 +274,11 @@ class _OptionList extends StatelessWidget { cells.addAll( state.options.map( - (option) => MobileSelectOption( - indicator: fieldType == FieldType.MultiSelect - ? MobileSelectedOptionIndicator.multi - : MobileSelectedOptionIndicator.single, + (option) => _SelectOption( + fieldType: fieldType, option: option, - isSelected: state.selectedOptions.contains(option), - onTap: (value) => onCheck(option, value), + checked: state.selectedOptions.contains(option), + onCheck: (value) => onCheck(option, value), onMoreOptions: () => onMoreOptions(option), ), ), @@ -300,23 +297,20 @@ class _OptionList extends StatelessWidget { } } -class MobileSelectOption extends StatelessWidget { - const MobileSelectOption({ - super.key, - required this.indicator, +class _SelectOption extends StatelessWidget { + const _SelectOption({ + required this.fieldType, required this.option, - required this.isSelected, - required this.onTap, - this.showMoreOptionsButton = true, - this.onMoreOptions, + required this.checked, + required this.onCheck, + required this.onMoreOptions, }); - final MobileSelectedOptionIndicator indicator; + final FieldType fieldType; final SelectOptionPB option; - final bool isSelected; - final void Function(bool value) onTap; - final bool showMoreOptionsButton; - final VoidCallback? onMoreOptions; + final bool checked; + final void Function(bool value) onCheck; + final VoidCallback onMoreOptions; @override Widget build(BuildContext context) { @@ -325,7 +319,7 @@ class MobileSelectOption extends StatelessWidget { child: GestureDetector( // no need to add click effect, so using gesture detector behavior: HitTestBehavior.translucent, - onTap: () => onTap(isSelected), + onTap: () => onCheck(!checked), child: Row( children: [ // checked or selected icon @@ -333,8 +327,8 @@ class MobileSelectOption extends StatelessWidget { height: 20, width: 20, child: _IsSelectedIndicator( - indicator: indicator, - isSelected: isSelected, + fieldType: fieldType, + isSelected: checked, ), ), // padding @@ -354,16 +348,14 @@ class MobileSelectOption extends StatelessWidget { ), ), ), - if (showMoreOptionsButton) ...[ - const HSpace(24), - // more options - FlowyIconButton( - icon: const FlowySvg( - FlowySvgs.m_field_more_s, - ), - onPressed: onMoreOptions, + const HSpace(24), + // more options + FlowyIconButton( + icon: const FlowySvg( + FlowySvgs.m_field_more_s, ), - ], + onPressed: onMoreOptions, + ), ], ), ), @@ -391,7 +383,7 @@ class _CreateOptionCell extends StatelessWidget { onTap: onTap, child: Row( children: [ - FlowyText( + FlowyText.medium( LocaleKeys.grid_selectOption_create.tr(), color: Theme.of(context).hintColor, ), @@ -503,15 +495,13 @@ class _MoreOptionsState extends State<_MoreOptions> { } } -enum MobileSelectedOptionIndicator { single, multi } - class _IsSelectedIndicator extends StatelessWidget { const _IsSelectedIndicator({ - required this.indicator, + required this.fieldType, required this.isSelected, }); - final MobileSelectedOptionIndicator indicator; + final FieldType fieldType; final bool isSelected; @override @@ -523,7 +513,7 @@ class _IsSelectedIndicator extends StatelessWidget { color: Theme.of(context).colorScheme.primary, ), child: Center( - child: indicator == MobileSelectedOptionIndicator.multi + child: fieldType == FieldType.MultiSelect ? FlowySvg( FlowySvgs.checkmark_tiny_s, color: Theme.of(context).colorScheme.onPrimary, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 7f6960de9d..63ea44008e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -2,16 +2,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/database/widgets/row/relation_row_detail.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; @@ -113,11 +106,8 @@ class _RelationCellEditorContentState @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: bloc), - BlocProvider.value(value: context.read()), - ], + return BlocProvider.value( + value: bloc, child: BlocBuilder( buildWhen: (previous, current) => !listEquals(previous.filteredRows, current.filteredRows), @@ -136,7 +126,7 @@ class _RelationCellEditorContentState shrinkWrap: true, slivers: [ _CellEditorTitle( - databaseMeta: widget.relatedDatabaseMeta, + databaseName: widget.relatedDatabaseMeta.databaseName, ), _SearchField( focusNode: focusNode, @@ -214,10 +204,10 @@ class _RelationCellEditorContentState class _CellEditorTitle extends StatelessWidget { const _CellEditorTitle({ - required this.databaseMeta, + required this.databaseName, }); - final DatabaseMeta databaseMeta; + final String databaseName; @override Widget build(BuildContext context) { @@ -233,20 +223,15 @@ class _CellEditorTitle extends StatelessWidget { fontSize: 11, color: Theme.of(context).hintColor, ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _openRelatedDatbase(context), - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: FlowyText.regular( - databaseMeta.databaseName, - fontSize: 11, - overflow: TextOverflow.ellipsis, - decoration: TextDecoration.underline, - ), - ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 2, + ), + child: FlowyText.regular( + databaseName, + fontSize: 11, + overflow: TextOverflow.ellipsis, ), ), ], @@ -254,28 +239,6 @@ class _CellEditorTitle extends StatelessWidget { ), ); } - - void _openRelatedDatbase(BuildContext context) { - FolderEventGetView(ViewIdPB(value: databaseMeta.viewId)) - .send() - .then((result) { - result.fold( - (view) { - PopoverContainer.of(context).closeAll(); - Navigator.of(context).maybePop(); - getIt().add( - TabsEvent.openPlugin( - plugin: DatabaseTabBarViewPlugin( - view: view, - pluginType: view.pluginType, - ), - ), - ); - }, - (err) => Log.error(err), - ); - }); - } } class _SearchField extends StatelessWidget { @@ -320,16 +283,13 @@ class _SearchField extends StatelessWidget { FlowyOverlay.show( context: context, builder: (BuildContext overlayContext) { - return BlocProvider.value( - value: context.read(), - child: RelatedRowDetailPage( - databaseId: context - .read() - .state - .relatedDatabaseMeta! - .databaseId, - rowId: row.rowId, - ), + return RelatedRowDetailPage( + databaseId: context + .read() + .state + .relatedDatabaseMeta! + .databaseId, + rowId: row.rowId, ); }, ); @@ -398,17 +358,13 @@ class _RowListItem extends StatelessWidget { ), child: GestureDetector( onTap: () { - final userWorkspaceBloc = context.read(); if (isSelected) { FlowyOverlay.show( context: context, builder: (BuildContext overlayContext) { - return BlocProvider.value( - value: userWorkspaceBloc, - child: RelatedRowDetailPage( - databaseId: databaseId, - rowId: row.rowId, - ), + return RelatedRowDetailPage( + databaseId: databaseId, + rowId: row.rowId, ); }, ); @@ -429,7 +385,7 @@ class _RowListItem extends StatelessWidget { child: Row( children: [ Expanded( - child: FlowyText( + child: FlowyText.medium( row.name.trim().isEmpty ? LocaleKeys.grid_title_placeholder.tr() : row.name, @@ -546,7 +502,7 @@ class _RelationCellEditorDatabasePicker extends StatelessWidget { databaseMeta.databaseId, ), ), - text: FlowyText( + text: FlowyText.medium( databaseMeta.databaseName, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart index 6ac2a5b807..d3a41c6c3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart @@ -1,23 +1,23 @@ import 'dart:collection'; import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../grid/presentation/layout/sizes.dart'; import '../../grid/presentation/widgets/common/type_option_separator.dart'; import '../field/type_option_editor/select/select_option_editor.dart'; - import 'extension.dart'; import 'select_option_text_field.dart'; @@ -73,7 +73,7 @@ class _SelectOptionCellEditorState extends State { break; case LogicalKeyboardKey.backspace when event is KeyUpEvent: if (!textEditingController.text.isNotEmpty) { - bloc.add(const SelectOptionCellEditorEvent.unselectLastOption()); + bloc.add(const SelectOptionCellEditorEvent.unSelectLastOption()); return KeyEventResult.handled; } break; @@ -137,7 +137,8 @@ class _OptionList extends StatelessWidget { Widget build(BuildContext context) { return BlocConsumer( - listenWhen: (prev, curr) => prev.clearFilter != curr.clearFilter, + listenWhen: (previous, current) => + previous.clearFilter != current.clearFilter, listener: (context, state) { if (state.clearFilter) { textEditingController.clear(); @@ -150,66 +151,60 @@ class _OptionList extends StatelessWidget { !listEquals(previous.options, current.options) || previous.createSelectOptionSuggestion != current.createSelectOptionSuggestion, - builder: (context, state) => ReorderableListView.builder( - shrinkWrap: true, - proxyDecorator: (child, index, _) => Material( - color: Colors.transparent, - child: Stack( - children: [ - BlocProvider.value( - value: context.read(), - child: child, - ), - MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grabbing, - child: const SizedBox.expand(), - ), - ], - ), - ), - buildDefaultDragHandles: false, - itemCount: state.options.length, - onReorderStart: (_) => popoverMutex.close(), - itemBuilder: (_, int index) { - final option = state.options[index]; - return _SelectOptionCell( - key: ValueKey("select_cell_option_list_${option.id}"), - index: index, - option: option, - popoverMutex: popoverMutex, - ); - }, - onReorder: (oldIndex, newIndex) { - if (oldIndex < newIndex) { - newIndex--; - } - final fromOptionId = state.options[oldIndex].id; - final toOptionId = state.options[newIndex].id; - context.read().add( - SelectOptionCellEditorEvent.reorderOption( - fromOptionId, - toOptionId, + builder: (context, state) { + return ReorderableListView.builder( + shrinkWrap: true, + proxyDecorator: (child, index, _) => Material( + color: Colors.transparent, + child: Stack( + children: [ + BlocProvider.value( + value: context.read(), + child: child, ), - ); - }, - header: Padding( - padding: EdgeInsets.only( - bottom: state.createSelectOptionSuggestion != null || - state.options.isNotEmpty - ? 12 - : 0, + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), + ), + ], + ), ), - child: const _Title(), - ), - footer: state.createSelectOptionSuggestion != null - ? _CreateOptionCell( - suggestion: state.createSelectOptionSuggestion!, - ) - : null, - padding: const EdgeInsets.symmetric(vertical: 8), - ), + buildDefaultDragHandles: false, + itemCount: state.options.length, + onReorderStart: (_) => popoverMutex.close(), + itemBuilder: (_, int index) { + final option = state.options[index]; + return _SelectOptionCell( + key: ValueKey("select_cell_option_list_${option.id}"), + index: index, + option: option, + popoverMutex: popoverMutex, + ); + }, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex--; + } + final fromOptionId = state.options[oldIndex].id; + final toOptionId = state.options[newIndex].id; + context.read().add( + SelectOptionCellEditorEvent.reorderOption( + fromOptionId, + toOptionId, + ), + ); + }, + header: const _Title(), + footer: state.createSelectOptionSuggestion == null + ? null + : _CreateOptionCell( + suggestion: state.createSelectOptionSuggestion!, + ), + padding: const EdgeInsets.symmetric(vertical: 8.0), + ); + }, ); } } @@ -250,9 +245,11 @@ class _TextField extends StatelessWidget { scrollController: scrollController, textSeparators: const [','], onClick: () => popoverMutex.close(), - newText: (text) => context - .read() - .add(SelectOptionCellEditorEvent.filterOption(text)), + newText: (text) { + context + .read() + .add(SelectOptionCellEditorEvent.filterOption(text)); + }, onSubmitted: () { context .read() @@ -267,12 +264,13 @@ class _TextField extends StatelessWidget { ), ); }, - onRemove: (name) => - context.read().add( - SelectOptionCellEditorEvent.unselectOption( - optionMap[name]!.id, - ), + onRemove: (optionName) { + context.read().add( + SelectOptionCellEditorEvent.unSelectOption( + optionMap[optionName]!.id, ), + ); + }, ), ), ); @@ -288,9 +286,12 @@ class _Title extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: FlowyText.regular( - LocaleKeys.grid_selectOption_panelTitle.tr(), - color: Theme.of(context).hintColor, + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyText.regular( + LocaleKeys.grid_selectOption_panelTitle.tr(), + color: Theme.of(context).hintColor, + ), ), ); } @@ -313,7 +314,13 @@ class _SelectOptionCell extends StatefulWidget { } class _SelectOptionCellState extends State<_SelectOptionCell> { - final _popoverController = PopoverController(); + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } @override Widget build(BuildContext context) { @@ -325,27 +332,16 @@ class _SelectOptionCellState extends State<_SelectOptionCell> { constraints: BoxConstraints.loose(const Size(200, 470)), mutex: widget.popoverMutex, clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (popoverContext) => SelectOptionEditor( - key: ValueKey(widget.option.id), - option: widget.option, - onDeleted: () { - context - .read() - .add(SelectOptionCellEditorEvent.deleteOption(widget.option)); - PopoverContainer.of(popoverContext).close(); - }, - onUpdated: (updatedOption) => context - .read() - .add(SelectOptionCellEditorEvent.updateOption(updatedOption)), - ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), child: MouseRegion( - onEnter: (_) => context.read().add( - SelectOptionCellEditorEvent.updateFocusedOption( - widget.option.id, - ), - ), + onEnter: (_) { + context.read().add( + SelectOptionCellEditorEvent.updateFocusedOption( + widget.option.id, + ), + ); + }, child: Container( height: 28, decoration: BoxDecoration( @@ -392,16 +388,42 @@ class _SelectOptionCellState extends State<_SelectOptionCell> { ), ), ), + popupBuilder: (BuildContext popoverContext) { + return SelectOptionEditor( + option: widget.option, + onDeleted: () { + context + .read() + .add(SelectOptionCellEditorEvent.deleteOption(widget.option)); + PopoverContainer.of(popoverContext).close(); + }, + onUpdated: (updatedOption) { + context + .read() + .add(SelectOptionCellEditorEvent.updateOption(updatedOption)); + }, + key: ValueKey( + widget.option.id, + ), // Use ValueKey to refresh the UI, otherwise, it will remain the old value. + ); + }, ); } void _onTap() { widget.popoverMutex.close(); - final bloc = context.read(); - if (bloc.state.selectedOptions.contains(widget.option)) { - bloc.add(SelectOptionCellEditorEvent.unselectOption(widget.option.id)); + if (context + .read() + .state + .selectedOptions + .contains(widget.option)) { + context + .read() + .add(SelectOptionCellEditorEvent.unSelectOption(widget.option.id)); } else { - bloc.add(SelectOptionCellEditorEvent.selectOption(widget.option.id)); + context + .read() + .add(SelectOptionCellEditorEvent.selectOption(widget.option.id)); } } } @@ -453,15 +475,16 @@ class SelectOptionTagCell extends StatelessWidget { onTap: onSelected, child: MouseRegion( cursor: SystemMouseCursors.click, - child: Container( + child: Align( alignment: AlignmentDirectional.centerStart, - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: SelectOptionTag( - fontSize: 14, - option: option, + child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, + horizontal: 6.0, + vertical: 4.0, + ), + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric(horizontal: 8), ), ), ), @@ -475,14 +498,16 @@ class SelectOptionTagCell extends StatelessWidget { } class _CreateOptionCell extends StatelessWidget { - const _CreateOptionCell({required this.suggestion}); + const _CreateOptionCell({ + required this.suggestion, + }); final CreateSelectOptionSuggestion suggestion; @override Widget build(BuildContext context) { return Container( - height: 32, + height: 28, margin: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( @@ -507,7 +532,7 @@ class _CreateOptionCell extends StatelessWidget { }, child: Row( children: [ - FlowyText( + FlowyText.medium( LocaleKeys.grid_selectOption_create.tr(), color: Theme.of(context).hintColor, ), @@ -518,10 +543,10 @@ class _CreateOptionCell extends StatelessWidget { child: SelectOptionTag( name: suggestion.name, color: suggestion.color.toColor(context), - fontSize: 14, + fontSize: 11, padding: const EdgeInsets.symmetric( horizontal: 8, - vertical: 2, + vertical: 1, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart index a03167df9d..f16500f601 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart @@ -60,12 +60,6 @@ class _SelectOptionTextFieldState extends State { _scrollToEnd(); }); } - - if (oldWidget.textController != widget.textController) { - oldWidget.textController.removeListener(_onChanged); - widget.textController.addListener(_onChanged); - } - super.didUpdateWidget(oldWidget); } @@ -138,10 +132,7 @@ class _SelectOptionTextFieldState extends State { (option) => SelectOptionTag( option: option, onRemove: (option) => widget.onRemove(option), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), ), ) .toList(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart index f8118a7e51..4d79b2d075 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_layout_ext.dart @@ -26,7 +26,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB { FlowySvgData get icon { return switch (this) { DatabaseLayoutPB.Board => FlowySvgs.board_s, - DatabaseLayoutPB.Calendar => FlowySvgs.calendar_s, + DatabaseLayoutPB.Calendar => FlowySvgs.date_s, DatabaseLayoutPB.Grid => FlowySvgs.grid_s, _ => throw UnimplementedError(), }; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart index a218e1ed68..ee64eb84af 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/database_view_widget.dart @@ -1,27 +1,19 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class DatabaseViewWidget extends StatefulWidget { const DatabaseViewWidget({ super.key, required this.view, this.shrinkWrap = true, - required this.showActions, - required this.node, - this.actionBuilder, }); final ViewPB view; final bool shrinkWrap; - final BlockComponentActionBuilder? actionBuilder; - final bool showActions; - final Node node; @override State createState() => _DatabaseViewWidgetState(); @@ -58,27 +50,11 @@ class _DatabaseViewWidgetState extends State { @override Widget build(BuildContext context) { - double? horizontalPadding = 0.0; - final databasePluginWidgetBuilderSize = - Provider.of(context); - if (view.layout == ViewLayoutPB.Grid || view.layout == ViewLayoutPB.Board) { - horizontalPadding = 40.0; - } - if (databasePluginWidgetBuilderSize != null) { - horizontalPadding = databasePluginWidgetBuilderSize.horizontalPadding; - } - return ValueListenableBuilder( valueListenable: _layoutTypeChangeNotifier, builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget( shrinkWrap: widget.shrinkWrap, context: PluginContext(), - data: { - kDatabasePluginWidgetBuilderHorizontalPadding: horizontalPadding, - kDatabasePluginWidgetBuilderActionBuilder: widget.actionBuilder, - kDatabasePluginWidgetBuilderShowActions: widget.showActions, - kDatabasePluginWidgetBuilderNode: widget.node, - }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart index 88cd88ee68..445966afe8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:typed_data'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -9,21 +8,17 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../shared/icon_emoji_picker/icon_picker.dart'; import 'field_type_list.dart'; import 'type_option_editor/builder.dart'; @@ -36,39 +31,35 @@ class FieldEditor extends StatefulWidget { const FieldEditor({ super.key, required this.viewId, - required this.fieldInfo, + required this.field, required this.fieldController, - required this.isNewField, this.initialPage = FieldEditorPage.details, this.onFieldInserted, }); final String viewId; - final FieldInfo fieldInfo; + final FieldPB field; final FieldController fieldController; final FieldEditorPage initialPage; final void Function(String fieldId)? onFieldInserted; - final bool isNewField; @override State createState() => _FieldEditorState(); } class _FieldEditorState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); late FieldEditorPage _currentPage; - late final TextEditingController textController = - TextEditingController(text: widget.fieldInfo.name); + late final TextEditingController textController; @override void initState() { super.initState(); _currentPage = widget.initialPage; + textController = TextEditingController(text: widget.field.name); } @override void dispose() { - popoverMutex.dispose(); textController.dispose(); super.dispose(); } @@ -78,14 +69,13 @@ class _FieldEditorState extends State { return BlocProvider( create: (_) => FieldEditorBloc( viewId: widget.viewId, - fieldInfo: widget.fieldInfo, + field: widget.field, fieldController: widget.fieldController, onFieldInserted: widget.onFieldInserted, - isNew: widget.isNewField, ), - child: _currentPage == FieldEditorPage.general - ? _fieldGeneral() - : _fieldDetails(), + child: _currentPage == FieldEditorPage.details + ? _fieldDetails() + : _fieldGeneral(), ); } @@ -96,10 +86,9 @@ class _FieldEditorState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _NameAndIcon( - popoverMutex: popoverMutex, - textController: textController, + FieldNameTextField( padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), + textEditingController: textController, ), VSpace(GridSize.typeOptionSeparatorHeight), _EditFieldButton( @@ -128,21 +117,6 @@ class _FieldEditorState extends State { ); } - Widget _actionCell(FieldAction action) { - return BlocBuilder( - builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FieldActionCell( - viewId: widget.viewId, - fieldInfo: state.field, - action: action, - ), - ); - }, - ); - } - Widget _fieldDetails() { return SizedBox( width: 260, @@ -152,6 +126,19 @@ class _FieldEditorState extends State { ), ); } + + Widget _actionCell(FieldAction action) { + return BlocBuilder( + builder: (context, state) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: FieldActionCell( + viewId: widget.viewId, + fieldInfo: state.field, + action: action, + ), + ), + ); + } } class _EditFieldButton extends StatelessWidget { @@ -170,8 +157,7 @@ class _EditFieldButton extends StatelessWidget { padding: padding, child: FlowyButton( leftIcon: const FlowySvg(FlowySvgs.edit_s), - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( LocaleKeys.grid_field_editProperty.tr(), ), onTap: onTap, @@ -202,30 +188,20 @@ class FieldActionCell extends StatelessWidget { (action == FieldAction.duplicate || action == FieldAction.delete)) { enable = false; } - return FlowyIconTextButton( - resetHoverOnRebuild: false, + + return FlowyButton( disable: !enable, + text: FlowyText.medium( + action.title(fieldInfo), + color: enable ? null : Theme.of(context).disabledColor, + ), onHover: (_) => popoverMutex?.close(), onTap: () => action.run(context, viewId, fieldInfo), - // show the error color when delete is hovered - textBuilder: (onHover) => FlowyText( - action.title(fieldInfo), - lineHeight: 1.0, - color: enable - ? action == FieldAction.delete && onHover - ? Theme.of(context).colorScheme.error - : null - : Theme.of(context).disabledColor, - ), - leftIconBuilder: (onHover) => action.leading( + leftIcon: action.leading( fieldInfo, - enable - ? action == FieldAction.delete && onHover - ? Theme.of(context).colorScheme.error - : null - : Theme.of(context).disabledColor, + enable ? null : Theme.of(context).disabledColor, ), - rightIconBuilder: (_) => action.trailing(context, fieldInfo), + rightIcon: action.trailing(context, fieldInfo), ); } } @@ -282,6 +258,7 @@ enum FieldAction { onChanged: (_) => context .read() .add(const FieldEditorEvent.toggleWrapCellContent()), + style: ToggleStyle.big, padding: EdgeInsets.zero, ); } @@ -341,33 +318,32 @@ enum FieldAction { ); break; case FieldAction.clearData: - PopoverContainer.of(context).closeAll(); - showCancelAndConfirmDialog( - context: context, - title: LocaleKeys.grid_field_label.tr(), - description: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () { + NavigatorAlertDialog( + constraints: const BoxConstraints( + maxWidth: 250, + maxHeight: 260, + ), + title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), + confirm: () { FieldBackendService.clearField( viewId: viewId, fieldId: fieldInfo.id, ); }, - ); + ).show(context); + PopoverContainer.of(context).close(); break; case FieldAction.delete: - PopoverContainer.of(context).closeAll(); - showConfirmDeletionDialog( - context: context, - name: LocaleKeys.grid_field_label.tr(), - description: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), - onConfirm: () { + NavigatorAlertDialog( + title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), + confirm: () { FieldBackendService.deleteField( viewId: viewId, fieldId: fieldInfo.id, ); }, - ); + ).show(context); + PopoverContainer.of(context).close(); break; case FieldAction.wrap: context @@ -395,7 +371,13 @@ class FieldDetailsEditor extends StatefulWidget { } class _FieldDetailsEditorState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); + late PopoverMutex popoverMutex; + + @override + void initState() { + popoverMutex = PopoverMutex(); + super.initState(); + } @override void dispose() { @@ -406,10 +388,10 @@ class _FieldDetailsEditorState extends State { @override Widget build(BuildContext context) { final List children = [ - _NameAndIcon( + FieldNameTextField( popoverMutex: popoverMutex, - textController: widget.textEditingController, padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0), + textEditingController: widget.textEditingController, ), const VSpace(8.0), SwitchFieldButton(popoverMutex: popoverMutex), @@ -528,166 +510,68 @@ class FieldTypeOptionEditor extends StatelessWidget { } } -class _NameAndIcon extends StatelessWidget { - const _NameAndIcon({ - required this.textController, - this.padding = EdgeInsets.zero, - this.popoverMutex, - }); - - final TextEditingController textController; - final PopoverMutex? popoverMutex; - final EdgeInsetsGeometry padding; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding, - child: Row( - children: [ - FieldEditIconButton( - popoverMutex: popoverMutex, - ), - const HSpace(6), - Expanded( - child: FieldNameTextField( - textController: textController, - popoverMutex: popoverMutex, - ), - ), - ], - ), - ); - } -} - -class FieldEditIconButton extends StatefulWidget { - const FieldEditIconButton({ - super.key, - this.popoverMutex, - }); - - final PopoverMutex? popoverMutex; - - @override - State createState() => _FieldEditIconButtonState(); -} - -class _FieldEditIconButtonState extends State { - final PopoverController popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - offset: const Offset(0, 4), - constraints: BoxConstraints.loose(const Size(360, 432)), - margin: EdgeInsets.zero, - direction: PopoverDirection.bottomWithLeftAligned, - controller: popoverController, - mutex: widget.popoverMutex, - child: FlowyIconButton( - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - ), - borderRadius: Corners.s8Border, - ), - icon: BlocBuilder( - builder: (context, state) { - return FieldIcon(fieldInfo: state.field); - }, - ), - width: 32, - onPressed: () => popoverController.show(), - ), - popupBuilder: (popoverContext) { - return FlowyIconEmojiPicker( - enableBackgroundColorSelection: false, - tabs: const [PickerTabType.icon], - onSelectedEmoji: (r) { - if (r.type == FlowyIconType.icon) { - try { - final iconsData = IconsData.fromJson(jsonDecode(r.emoji)); - context.read().add( - FieldEditorEvent.updateIcon( - '${iconsData.groupName}/${iconsData.iconName}', - ), - ); - } on FormatException catch (e) { - Log.warn('FieldEditIconButton onSelectedEmoji error:$e'); - context - .read() - .add(const FieldEditorEvent.updateIcon('')); - } - } - PopoverContainer.of(popoverContext).close(); - }, - ); - }, - ); - } -} - class FieldNameTextField extends StatefulWidget { const FieldNameTextField({ super.key, - required this.textController, + required this.textEditingController, this.popoverMutex, + this.padding = EdgeInsets.zero, }); - final TextEditingController textController; + final TextEditingController textEditingController; final PopoverMutex? popoverMutex; + final EdgeInsets padding; @override State createState() => _FieldNameTextFieldState(); } class _FieldNameTextFieldState extends State { - final focusNode = FocusNode(); + FocusNode focusNode = FocusNode(); @override void initState() { super.initState(); - focusNode.addListener(_onFocusChanged); - widget.popoverMutex?.addPopoverListener(_onPopoverChanged); - } + focusNode.addListener(() { + if (focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + }); - @override - void dispose() { - widget.popoverMutex?.removePopoverListener(_onPopoverChanged); - focusNode.removeListener(_onFocusChanged); - focusNode.dispose(); - - super.dispose(); + widget.popoverMutex?.listenOnPopoverChanged(() { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } + }); } @override Widget build(BuildContext context) { - return FlowyTextField( - focusNode: focusNode, - controller: widget.textController, - onSubmitted: (_) => PopoverContainer.of(context).close(), - onChanged: (newName) { - context - .read() - .add(FieldEditorEvent.renameField(newName)); - }, + return Padding( + padding: widget.padding, + child: FlowyTextField( + focusNode: focusNode, + controller: widget.textEditingController, + onSubmitted: (_) => PopoverContainer.of(context).close(), + onChanged: (newName) { + context + .read() + .add(FieldEditorEvent.renameField(newName)); + }, + ), ); } - void _onFocusChanged() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - } - - void _onPopoverChanged() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } + @override + void dispose() { + focusNode.removeListener(() { + if (focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + }); + focusNode.dispose(); + super.dispose(); } } @@ -710,37 +594,12 @@ class _SwitchFieldButtonState extends State { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (state.field.isPrimary) { - return SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FlowyTooltip( - message: LocaleKeys.grid_field_switchPrimaryFieldTooltip.tr(), - child: FlowyButton( - text: FlowyText( - state.field.fieldType.i18n, - lineHeight: 1.0, - color: Theme.of(context).disabledColor, - ), - leftIcon: FlowySvg( - state.field.fieldType.svgData, - color: Theme.of(context).disabledColor, - ), - rightIcon: FlowySvg( - FlowySvgs.more_s, - color: Theme.of(context).disabledColor, - ), - ), - ), - ), - ); - } + final bool isPrimary = state.field.isPrimary; return SizedBox( height: GridSize.popoverItemHeight, child: AppFlowyPopover( constraints: BoxConstraints.loose(const Size(460, 540)), - triggerActions: PopoverTriggerFlags.hover, + triggerActions: isPrimary ? 0 : PopoverTriggerFlags.hover, mutex: widget.popoverMutex, controller: _popoverController, offset: const Offset(8, 0), @@ -757,16 +616,22 @@ class _SwitchFieldButtonState extends State { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: FlowyButton( - onTap: () => _popoverController.show(), - text: FlowyText( + onTap: () { + if (!isPrimary) { + _popoverController.show(); + } + }, + text: FlowyText.medium( state.field.fieldType.i18n, - lineHeight: 1.0, + color: isPrimary ? Theme.of(context).disabledColor : null, ), leftIcon: FlowySvg( state.field.fieldType.svgData, + color: isPrimary ? Theme.of(context).disabledColor : null, ), - rightIcon: const FlowySvg( + rightIcon: FlowySvg( FlowySvgs.more_s, + color: isPrimary ? Theme.of(context).disabledColor : null, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index 6661d5cd2d..84d4c49177 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; typedef SelectFieldCallback = void Function(FieldType); @@ -14,16 +15,15 @@ const List _supportedFieldTypes = [ FieldType.SingleSelect, FieldType.MultiSelect, FieldType.DateTime, - FieldType.Media, - FieldType.URL, FieldType.Checkbox, FieldType.Checklist, + FieldType.URL, FieldType.LastEditedTime, FieldType.CreatedTime, FieldType.Relation, FieldType.Summary, - FieldType.Translate, // FieldType.Time, + FieldType.Translate, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { @@ -75,7 +75,9 @@ class FieldTypeCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText(fieldType.i18n, lineHeight: 1.0), + text: FlowyText.medium( + fieldType.i18n, + ), onTap: () => onSelectField(fieldType), leftIcon: FlowySvg( fieldType.svgData, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart index 624f9f1fb2..e4bcdd4911 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart @@ -2,7 +2,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/media.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -39,7 +38,6 @@ abstract class TypeOptionEditorFactory { FieldType.Summary => const SummaryTypeOptionEditorFactory(), FieldType.Time => const TimeTypeOptionEditorFactory(), FieldType.Translate => const TranslateTypeOptionEditorFactory(), - FieldType.Media => const MediaTypeOptionEditorFactory(), _ => throw UnimplementedError(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart index ab216c7b98..1ce3b73dad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart @@ -1,5 +1,6 @@ import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:protobuf/protobuf.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart index 862e46fc3b..4c7dc73ae2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart @@ -1,12 +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/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class DateFormatButton extends StatelessWidget { const DateFormatButton({ @@ -23,10 +23,7 @@ class DateFormatButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_field_dateFormat.tr(), - lineHeight: 1.0, - ), + text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()), onTap: onTap, onHover: onHover, rightIcon: const FlowySvg(FlowySvgs.more_s), @@ -50,10 +47,7 @@ class TimeFormatButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - LocaleKeys.grid_field_timeFormat.tr(), - lineHeight: 1.0, - ), + text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()), onTap: onTap, onHover: onHover, rightIcon: const FlowySvg(FlowySvgs.more_s), @@ -74,9 +68,7 @@ class DateFormatList extends StatelessWidget { @override Widget build(BuildContext context) { - final cells = DateFormatPB.values - .where((value) => value != DateFormatPB.FriendlyFull) - .map((format) { + final cells = DateFormatPB.values.map((format) { return DateFormatCell( dateFormat: format, onSelected: onSelected, @@ -122,10 +114,7 @@ class DateFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - dateFormat.title(), - lineHeight: 1.0, - ), + text: FlowyText.medium(dateFormat.title()), rightIcon: checkmark, onTap: () => onSelected(dateFormat), ), @@ -146,8 +135,6 @@ extension DateFormatExtension on DateFormatPB { return LocaleKeys.grid_field_dateFormatUS.tr(); case DateFormatPB.DayMonthYear: return LocaleKeys.grid_field_dateFormatDayMonthYear.tr(); - case DateFormatPB.FriendlyFull: - return LocaleKeys.grid_field_dateFormatFriendly.tr(); default: throw UnimplementedError; } @@ -212,10 +199,7 @@ class TimeFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - timeFormat.title(), - lineHeight: 1.0, - ), + text: FlowyText.medium(timeFormat.title()), rightIcon: checkmark, onTap: () => onSelected(timeFormat), ), @@ -240,11 +224,11 @@ class IncludeTimeButton extends StatelessWidget { const IncludeTimeButton({ super.key, required this.onChanged, - required this.includeTime, + required this.value, }); final Function(bool value) onChanged; - final bool includeTime; + final bool value; @override Widget build(BuildContext context) { @@ -259,11 +243,12 @@ class IncludeTimeButton extends StatelessWidget { color: Theme.of(context).iconTheme.color, ), const HSpace(6), - FlowyText(LocaleKeys.grid_field_includeTime.tr()), + FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), const Spacer(), Toggle( - value: includeTime, + value: value, onChanged: onChanged, + style: ToggleStyle.big, padding: EdgeInsets.zero, ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/media.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/media.dart deleted file mode 100644 index 07dc2bafd0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/media.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:protobuf/protobuf.dart'; - -class MediaTypeOptionEditorFactory implements TypeOptionEditorFactory { - const MediaTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - height: GridSize.popoverItemHeight, - alignment: Alignment.centerLeft, - child: FlowyButton( - resetHoverOnRebuild: false, - text: FlowyText( - LocaleKeys.grid_media_showFileNames.tr(), - lineHeight: 1.0, - ), - onHover: (_) => popoverMutex.close(), - rightIcon: Toggle( - value: !typeOption.hideFileNames, - onChanged: (val) => onTypeOptionUpdated( - _toggleHideFiles(typeOption, !val).writeToBuffer(), - ), - padding: EdgeInsets.zero, - ), - ), - ); - } - - MediaTypeOptionPB _parseTypeOptionData(List data) { - return MediaTypeOptionDataParser().fromBuffer(data); - } - - MediaTypeOptionPB _toggleHideFiles( - MediaTypeOptionPB typeOption, - bool hideFileNames, - ) { - typeOption.freeze(); - return typeOption.rebuild((to) => to.hideFileNames = hideFileNames); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart index b8a40907c6..244f38326c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -30,8 +31,7 @@ class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory { height: GridSize.popoverItemHeight, child: FlowyButton( rightIcon: const FlowySvg(FlowySvgs.more_s), - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( typeOption.format.title(), ), ), @@ -167,10 +167,7 @@ class NumberFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - format.title(), - lineHeight: 1.0, - ), + text: FlowyText.medium(format.title()), onTap: () => onSelected(format), rightIcon: checkmark, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart index 2ee3222b23..9ca2729cb6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart @@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/field/type_option/relation import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -60,7 +61,6 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { (meta) => meta.databaseId == typeOption.databaseId, ); return FlowyText( - lineHeight: 1.0, databaseMeta == null ? LocaleKeys .grid_relation_relatedDatabasePlaceholder @@ -133,8 +133,7 @@ class _DatabaseList extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( onTap: () => onSelectDatabase(meta.databaseId), - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( meta.databaseName, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart index 5201630cc7..4c56121890 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/database/application/field/type_option/select_t import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -179,8 +180,7 @@ class _AddOptionButton extends StatelessWidget { child: SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( LocaleKeys.grid_field_addSelectOption.tr(), ), onTap: () { @@ -205,23 +205,22 @@ class CreateOptionTextField extends StatefulWidget { } class _CreateOptionTextFieldState extends State { - final focusNode = FocusNode(); + late final FocusNode _focusNode; @override void initState() { super.initState(); - - focusNode.addListener(_onFocusChanged); - widget.popoverMutex?.addPopoverListener(_onPopoverChanged); - } - - @override - void dispose() { - widget.popoverMutex?.removePopoverListener(_onPopoverChanged); - focusNode.removeListener(_onFocusChanged); - focusNode.dispose(); - - super.dispose(); + _focusNode = FocusNode() + ..addListener(() { + if (_focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + }); + widget.popoverMutex?.listenOnPopoverChanged(() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + }); } @override @@ -234,7 +233,7 @@ class _CreateOptionTextFieldState extends State { child: FlowyTextField( autoClearWhenDone: true, text: text, - focusNode: focusNode, + focusNode: _focusNode, onCanceled: () { context .read() @@ -252,16 +251,15 @@ class _CreateOptionTextFieldState extends State { ); } - void _onFocusChanged() { - if (focusNode.hasFocus) { - widget.popoverMutex?.close(); - } - } - - void _onPopoverChanged() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } + @override + void dispose() { + _focusNode.removeListener(() { + if (_focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + }); + _focusNode.dispose(); + super.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart index 9946a6ab75..5df44f4b49 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart @@ -106,8 +106,7 @@ class _DeleteTag extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( LocaleKeys.grid_selectOption_deleteTag.tr(), ), leftIcon: const FlowySvg(FlowySvgs.delete_s), @@ -175,7 +174,7 @@ class SelectOptionColorList extends StatelessWidget { padding: GridSize.typeOptionContentInsets, child: SizedBox( height: GridSize.popoverItemHeight, - child: FlowyText( + child: FlowyText.medium( LocaleKeys.grid_selectOption_colorPanelTitle.tr(), textAlign: TextAlign.left, color: Theme.of(context).hintColor, @@ -230,8 +229,7 @@ class _SelectOptionColorCell extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( color.colorName(), color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart index e67929d2dc..e3f50cc857 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart @@ -1,6 +1,7 @@ import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:protobuf/protobuf.dart'; @@ -33,11 +34,11 @@ class TimestampTypeOptionEditorFactory implements TypeOptionEditorFactory { onChanged: (value) { final newTypeOption = _updateTypeOption( typeOption: typeOption, - includeTime: value, + includeTime: !value, ); onTypeOptionUpdated(newTypeOption.writeToBuffer()); }, - includeTime: typeOption.includeTime, + value: typeOption.includeTime, ), ), ], diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart index 70ce6e8049..4cb5b0d9a3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/type_option/translate_type_option_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -96,12 +97,7 @@ class SelectLanguageButton extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( height: 30, - child: FlowyButton( - text: FlowyText( - language, - lineHeight: 1.0, - ), - ), + child: FlowyButton(text: FlowyText(language)), ); } } @@ -163,10 +159,7 @@ class LanguageCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText( - languageTypeToLanguage(languageType), - lineHeight: 1.0, - ), + text: FlowyText.medium(languageTypeToLanguage(languageType)), rightIcon: checkmark, onTap: () => onSelected(languageType), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index f1486094bf..532effbd87 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.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/plugins/database/application/database_controller.dart'; @@ -5,9 +7,9 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -15,7 +17,6 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; @@ -41,7 +42,7 @@ class DatabaseGroupList extends StatelessWidget { child: BlocBuilder( builder: (context, state) { final field = state.fieldInfos.firstWhereOrNull( - (field) => field.fieldType.canBeGroup && field.isGroupField, + (field) => field.canBeGroup && field.isGroupField, ); final showHideUngroupedToggle = field?.fieldType != FieldType.Checkbox; @@ -57,28 +58,26 @@ class DatabaseGroupList extends StatelessWidget { final children = [ if (showHideUngroupedToggle) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: FlowyButton( - resetHoverOnRebuild: false, - text: FlowyText( - LocaleKeys.board_showUngrouped.tr(), - lineHeight: 1.0, - ), - onTap: () { - _updateLayoutSettings( - state.layoutSettings, - !state.layoutSettings.hideUngroupedColumn, - ); - }, - rightIcon: Toggle( - value: !state.layoutSettings.hideUngroupedColumn, - onChanged: (value) => - _updateLayoutSettings(state.layoutSettings, !value), - padding: EdgeInsets.zero, - ), + SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.board_showUngrouped.tr(), + ), + ), + Toggle( + value: !state.layoutSettings.hideUngroupedColumn, + onChanged: (value) => + _updateLayoutSettings(state.layoutSettings, value), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], ), ), ), @@ -89,39 +88,38 @@ class DatabaseGroupList extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: FlowyText( + child: FlowyText.medium( LocaleKeys.board_groupBy.tr(), textAlign: TextAlign.left, color: Theme.of(context).hintColor, ), ), ), - ...state.fieldInfos - .where((fieldInfo) => fieldInfo.fieldType.canBeGroup) - .map( + ...state.fieldInfos.where((fieldInfo) => fieldInfo.canBeGroup).map( (fieldInfo) => _GridGroupCell( fieldInfo: fieldInfo, name: fieldInfo.name, + icon: fieldInfo.fieldType.svgData, checked: fieldInfo.isGroupField, onSelected: onDismissed, key: ValueKey(fieldInfo.id), ), ), - if (field?.fieldType.groupConditions.isNotEmpty ?? false) ...[ + if (field?.groupConditions.isNotEmpty ?? false) ...[ const TypeOptionSeparator(spacing: 0), SizedBox( height: GridSize.popoverItemHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - child: FlowyText( + child: FlowyText.medium( LocaleKeys.board_groupCondition.tr(), textAlign: TextAlign.left, color: Theme.of(context).hintColor, ), ), ), - ...field!.fieldType.groupConditions.map( + ...field!.groupConditions.map( (condition) => _GridGroupCell( fieldInfo: field, name: condition.name, @@ -168,6 +166,7 @@ class _GridGroupCell extends StatelessWidget { required this.checked, required this.name, this.condition = 0, + this.icon, }); final FieldInfo fieldInfo; @@ -175,6 +174,7 @@ class _GridGroupCell extends StatelessWidget { final bool checked; final int condition; final String name; + final FlowySvgData? icon; @override Widget build(BuildContext context) { @@ -192,12 +192,16 @@ class _GridGroupCell extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6.0), child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( + text: FlowyText.medium( name, color: AFThemeExtension.of(context).textColor, - lineHeight: 1.0, ), - leftIcon: FieldIcon(fieldInfo: fieldInfo), + leftIcon: icon != null + ? FlowySvg( + icon!, + color: Theme.of(context).iconTheme.color, + ) + : null, rightIcon: rightIcon, onTap: () { List settingContent = []; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/media_file_type_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/media_file_type_ext.dart deleted file mode 100644 index 9ac6bec394..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/media_file_type_ext.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; - -extension FileTypeDisplay on MediaFileTypePB { - FlowySvgData get icon => switch (this) { - MediaFileTypePB.Image => FlowySvgs.image_s, - MediaFileTypePB.Link => FlowySvgs.ft_link_s, - MediaFileTypePB.Document => FlowySvgs.icon_document_s, - MediaFileTypePB.Archive => FlowySvgs.ft_archive_s, - MediaFileTypePB.Video => FlowySvgs.ft_video_s, - MediaFileTypePB.Audio => FlowySvgs.ft_audio_s, - MediaFileTypePB.Text => FlowySvgs.ft_text_s, - _ => FlowySvgs.icon_document_s, - }; - - Color get color => switch (this) { - MediaFileTypePB.Image => const Color(0xFF5465A1), - MediaFileTypePB.Link => const Color(0xFFEBE4FF), - MediaFileTypePB.Audio => const Color(0xFFE4FFDE), - MediaFileTypePB.Video => const Color(0xFFE0F8FF), - MediaFileTypePB.Archive => const Color(0xFFFFE7EE), - MediaFileTypePB.Text || - MediaFileTypePB.Document || - MediaFileTypePB.Other => - const Color(0xFFF5FFDC), - _ => const Color(0xFF87B3A8), - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart index 6e13cc5ecb..febd6a6749 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/accessory/cell_accessory.dart @@ -1,14 +1,13 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:styled_widget/styled_widget.dart'; import '../../cell/editable_cell_builder.dart'; @@ -125,12 +124,6 @@ class _AccessoryHoverState extends State { @override Widget build(BuildContext context) { - // Some FieldType has built-in handling for more gestures - // and granular control, so we don't need to show the accessory. - if (!widget.fieldType.showRowDetailAccessory) { - return widget.child; - } - final List children = [ DecoratedBox( decoration: BoxDecoration( @@ -188,9 +181,6 @@ class CellAccessoryContainer extends StatelessWidget { ); }).toList(); - return SeparatedRow( - separatorBuilder: () => const HSpace(6), - children: children, - ); + return Wrap(spacing: 6, children: children); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart index 333ff0fe96..22a9ce1381 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart @@ -1,13 +1,12 @@ -import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; import '../../../grid/presentation/layout/sizes.dart'; import '../../../grid/presentation/widgets/row/row.dart'; -import '../../cell/editable_cell_builder.dart'; import '../accessory/cell_accessory.dart'; import '../accessory/cell_shortcuts.dart'; +import '../../cell/editable_cell_builder.dart'; class CellContainer extends StatelessWidget { const CellContainer({ @@ -57,7 +56,7 @@ class CellContainer extends StatelessWidget { } }, child: Container( - constraints: BoxConstraints(maxWidth: width, minHeight: 32), + constraints: BoxConstraints(maxWidth: width, minHeight: 46), decoration: _makeBoxDecoration(context, isFocus), child: container, ), @@ -76,8 +75,7 @@ class CellContainer extends StatelessWidget { return BoxDecoration(border: Border.fromBorderSide(borderSide)); } - final borderSide = - BorderSide(color: AFThemeExtension.of(context).borderColor); + final borderSide = BorderSide(color: Theme.of(context).dividerColor); return BoxDecoration( border: Border(right: borderSide, bottom: borderSide), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart index 8dba996d05..cdc984e4b7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/mobile_cell_container.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import '../accessory/cell_shortcuts.dart'; import '../../cell/editable_cell_builder.dart'; import 'cell_container.dart'; @@ -23,7 +24,7 @@ class MobileCellContainer extends StatelessWidget { child: Selector( selector: (context, notifier) => notifier.isFocus, builder: (providerContext, isFocus, _) { - Widget container = Center(child: child); + Widget container = Center(child: GridCellShortcuts(child: child)); if (isPrimary) { container = IgnorePointer(child: container); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart index 1260641fdf..256de6bc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/relation_row_detail.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database/application/row/related_row_detail_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -23,17 +22,14 @@ class RelatedRowDetailPage extends StatelessWidget { initialRowId: rowId, ), child: BlocBuilder( - builder: (_, state) { + builder: (context, state) { return state.when( loading: () => const SizedBox.shrink(), ready: (databaseController, rowController) { - return BlocProvider.value( - value: context.read(), - child: RowDetailPage( - databaseController: databaseController, - rowController: rowController, - allowOpenAsFullPage: false, - ), + return RowDetailPage( + databaseController: databaseController, + rowController: rowController, + allowOpenAsFullPage: false, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart index c0cf547a06..c9f4a796c0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart @@ -50,10 +50,7 @@ class RowDetailPageDeleteButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.grid_row_delete.tr(), - lineHeight: 1.0, - ), + text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), leftIcon: const FlowySvg(FlowySvgs.trash_m), onTap: () { RowBackendService.deleteRows(viewId, [rowId]); @@ -79,10 +76,7 @@ class RowDetailPageDuplicateButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.grid_row_duplicate.tr(), - lineHeight: 1.0, - ), + text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), leftIcon: const FlowySvg(FlowySvgs.copy_s), onTap: () { RowBackendService.duplicateRow(viewId, rowId); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index debbb467e7..dc1b9435e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -1,50 +1,23 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.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/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_banner_bloc.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart'; -import 'package:appflowy/plugins/shared/cover_type_ext.dart'; -import 'package:appflowy/shared/af_image.dart'; -import 'package:appflowy/shared/flowy_gradient_colors.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; -import '../../../../shared/icon_emoji_picker/tab.dart'; -import '../../../document/presentation/editor_plugins/plugins.dart'; - -/// We have the cover height as public as it is used in the row_detail.dart file -/// Used to determine the position of the row actions depending on if there is a cover or not. -/// -const rowCoverHeight = 250.0; - -const _iconHeight = 60.0; -const _toolbarHeight = 40.0; +const _kBannerActionHeight = 40.0; class RowBanner extends StatefulWidget { const RowBanner({ @@ -53,14 +26,12 @@ class RowBanner extends StatefulWidget { required this.rowController, required this.cellBuilder, this.allowOpenAsFullPage = true, - this.userProfile, }); final DatabaseController databaseController; final RowController rowController; final EditableCellBuilder cellBuilder; final bool allowOpenAsFullPage; - final UserProfilePB? userProfile; @override State createState() => _RowBannerState(); @@ -68,9 +39,7 @@ class RowBanner extends StatefulWidget { class _RowBannerState extends State { final _isHovering = ValueNotifier(false); - late final isLocalMode = - (widget.userProfile?.workspaceAuthType ?? AuthTypePB.Local) == - AuthTypePB.Local; + final popoverController = PopoverController(); @override void dispose() { @@ -86,500 +55,76 @@ class _RowBannerState extends State { fieldController: widget.databaseController.fieldController, rowMeta: widget.rowController.rowMeta, )..add(const RowBannerEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final hasCover = state.rowMeta.cover.data.isNotEmpty; - final hasIcon = state.rowMeta.icon.isNotEmpty; - - return Column( + child: MouseRegion( + onEnter: (event) => _isHovering.value = true, + onExit: (event) => _isHovering.value = false, + child: Padding( + padding: const EdgeInsets.fromLTRB(60, 34, 60, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - LayoutBuilder( - builder: (context, constraints) { - return Stack( - children: [ - SizedBox( - height: _calculateOverallHeight(hasIcon, hasCover), - width: constraints.maxWidth, - child: RowHeaderToolbar( - offset: GridSize.horizontalHeaderPadding + 20, - hasIcon: hasIcon, - hasCover: hasCover, - onIconChanged: (icon) { - if (icon != null) { - context - .read() - .add(RowBannerEvent.setIcon(icon)); - } - }, - onCoverChanged: (cover) { - if (cover != null) { - context - .read() - .add(RowBannerEvent.setCover(cover)); - } - }, - ), - ), - if (hasCover) - RowCover( - rowId: widget.rowController.rowId, - cover: state.rowMeta.cover, - userProfile: widget.userProfile, - onCoverChanged: (type, details, uploadType) { - if (details != null) { - context.read().add( - RowBannerEvent.setCover( - RowCoverPB( - data: details, - uploadType: uploadType, - coverType: type.into(), - ), - ), - ); - } else { - context - .read() - .add(const RowBannerEvent.removeCover()); - } - }, - isLocalMode: isLocalMode, - ), - if (hasIcon) - Positioned( - left: GridSize.horizontalHeaderPadding + 20, - bottom: hasCover - ? _toolbarHeight - _iconHeight / 2 - : _toolbarHeight, - child: RowIcon( - ///TODO: avoid hardcoding for [FlowyIconType] - icon: EmojiIconData( - FlowyIconType.emoji, - state.rowMeta.icon, - ), - onIconChanged: (icon) { - if (icon == null || icon.isEmpty) { - context - .read() - .add(const RowBannerEvent.setIcon("")); - } else { - context - .read() - .add(RowBannerEvent.setIcon(icon)); - } - }, - ), - ), - ], - ); - }, + SizedBox( + height: 30, + child: _BannerAction( + isHovering: _isHovering, + popoverController: popoverController, + ), ), - const VSpace(8), + const VSpace(4), _BannerTitle( cellBuilder: widget.cellBuilder, + popoverController: popoverController, rowController: widget.rowController, ), ], - ); - }, + ), + ), ), ); } - - double _calculateOverallHeight(bool hasIcon, bool hasCover) { - switch ((hasIcon, hasCover)) { - case (true, true): - return rowCoverHeight + _toolbarHeight; - case (true, false): - return 50 + _iconHeight + _toolbarHeight; - case (false, true): - return rowCoverHeight + _toolbarHeight; - case (false, false): - return _toolbarHeight; - } - } } -class RowCover extends StatefulWidget { - const RowCover({ - super.key, - required this.rowId, - required this.cover, - this.userProfile, - required this.onCoverChanged, - this.isLocalMode = true, +class _BannerAction extends StatelessWidget { + const _BannerAction({ + required this.isHovering, + required this.popoverController, }); - final String rowId; - final RowCoverPB cover; - final UserProfilePB? userProfile; - final void Function( - CoverType type, - String? details, - FileUploadTypePB? uploadType, - ) onCoverChanged; - final bool isLocalMode; - - @override - State createState() => _RowCoverState(); -} - -class _RowCoverState extends State { - final popoverController = PopoverController(); - bool isOverlayButtonsHidden = true; - bool isPopoverOpen = false; + final ValueNotifier isHovering; + final PopoverController popoverController; @override Widget build(BuildContext context) { return SizedBox( - height: rowCoverHeight, - child: MouseRegion( - onEnter: (_) => setState(() => isOverlayButtonsHidden = false), - onExit: (_) => setState(() => isOverlayButtonsHidden = true), - child: Stack( - children: [ - SizedBox( - width: double.infinity, - child: DesktopRowCover( - cover: widget.cover, - userProfile: widget.userProfile, - ), - ), - if (!isOverlayButtonsHidden || isPopoverOpen) - _buildCoverOverlayButtons(context), - ], - ), - ), - ); - } + height: _kBannerActionHeight, + child: ValueListenableBuilder( + valueListenable: isHovering, + builder: (BuildContext context, bool isHovering, Widget? child) { + if (!isHovering) { + return const SizedBox.shrink(); + } - Widget _buildCoverOverlayButtons(BuildContext context) { - return Positioned( - bottom: 20, - right: 50, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, - ), - margin: EdgeInsets.zero, - onClose: () => setState(() => isPopoverOpen = false), - child: IntrinsicWidth( - child: RoundedTextButton( - height: 28.0, - onPressed: () => popoverController.show(), - hoverColor: Theme.of(context).colorScheme.surface, - textColor: Theme.of(context).colorScheme.tertiary, - fillColor: Theme.of(context) - .colorScheme - .surface - .withValues(alpha: 0.5), - title: LocaleKeys.document_plugins_cover_changeCover.tr(), - ), - ), - popupBuilder: (BuildContext popoverContext) { - isPopoverOpen = true; - - return UploadImageMenu( - limitMaximumImageSize: !widget.isLocalMode, - supportTypes: const [ - UploadImageType.color, - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, + return BlocBuilder( + builder: (context, state) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (state.rowMeta.icon.isEmpty) + AddEmojiButton( + onTap: () => popoverController.show(), + ) + else + RemoveEmojiButton( + onTap: () => context + .read() + .add(const RowBannerEvent.setIcon('')), + ), ], - onSelectedAIImage: (_) => throw UnimplementedError(), - onSelectedLocalImages: (files) { - popoverController.close(); - if (files.isEmpty) { - return; - } - - final item = files.map((file) => file.path).first; - onCoverChanged( - CoverType.file, - item, - widget.isLocalMode - ? FileUploadTypePB.LocalFile - : FileUploadTypePB.CloudFile, - ); - }, - onSelectedNetworkImage: (url) { - popoverController.close(); - onCoverChanged( - CoverType.file, - url, - FileUploadTypePB.NetworkFile, - ); - }, - onSelectedColor: (color) { - popoverController.close(); - onCoverChanged( - CoverType.color, - color, - FileUploadTypePB.LocalFile, - ); - }, ); }, - ), - const HSpace(10), - DeleteCoverButton( - onTap: () => widget.onCoverChanged(CoverType.none, null, null), - ), - ], - ), - ); - } - - Future onCoverChanged( - CoverType type, - String? details, - FileUploadTypePB? uploadType, - ) async { - if (type == CoverType.file && details != null && !isURL(details)) { - if (widget.isLocalMode) { - details = await saveImageToLocalStorage(details); - } else { - // else we should save the image to cloud storage - (details, _) = await saveImageToCloudStorage(details, widget.rowId); - } - } - widget.onCoverChanged(type, details, uploadType); - } -} - -class DesktopRowCover extends StatefulWidget { - const DesktopRowCover({super.key, required this.cover, this.userProfile}); - - final RowCoverPB cover; - final UserProfilePB? userProfile; - - @override - State createState() => _DesktopRowCoverState(); -} - -class _DesktopRowCoverState extends State { - RowCoverPB get cover => widget.cover; - - @override - Widget build(BuildContext context) { - if (cover.coverType == CoverTypePB.FileCover) { - return SizedBox( - height: rowCoverHeight, - width: double.infinity, - child: AFImage( - url: cover.data, - uploadType: cover.uploadType, - userProfile: widget.userProfile, - ), - ); - } - - if (cover.coverType == CoverTypePB.AssetCover) { - return SizedBox( - height: rowCoverHeight, - width: double.infinity, - child: Image.asset( - PageStyleCoverImageType.builtInImagePath(cover.data), - fit: BoxFit.cover, - ), - ); - } - - if (cover.coverType == CoverTypePB.ColorCover) { - final color = FlowyTint.fromId(cover.data)?.color(context) ?? - cover.data.tryToColor(); - return Container( - height: rowCoverHeight, - width: double.infinity, - color: color, - ); - } - - if (cover.coverType == CoverTypePB.GradientCover) { - return Container( - height: rowCoverHeight, - width: double.infinity, - decoration: BoxDecoration( - gradient: FlowyGradientColor.fromId(cover.data).linear, - ), - ); - } - - return const SizedBox.shrink(); - } -} - -class RowHeaderToolbar extends StatefulWidget { - const RowHeaderToolbar({ - super.key, - required this.offset, - required this.hasIcon, - required this.hasCover, - required this.onIconChanged, - required this.onCoverChanged, - }); - - final double offset; - final bool hasIcon; - final bool hasCover; - - /// Returns null if the icon is removed. - /// - final void Function(String? icon) onIconChanged; - - /// Returns null if the cover is removed. - /// - final void Function(RowCoverPB? cover) onCoverChanged; - - @override - State createState() => _RowHeaderToolbarState(); -} - -class _RowHeaderToolbarState extends State { - final popoverController = PopoverController(); - final bool isDesktop = UniversalPlatform.isDesktopOrWeb; - - bool isHidden = UniversalPlatform.isDesktopOrWeb; - bool isPopoverOpen = false; - - @override - Widget build(BuildContext context) { - if (!isDesktop) { - return const SizedBox.shrink(); - } - - return MouseRegion( - opaque: false, - onEnter: (_) => setState(() => isHidden = false), - onExit: isPopoverOpen ? null : (_) => setState(() => isHidden = true), - child: Container( - alignment: Alignment.bottomLeft, - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: widget.offset), - child: SizedBox( - height: 28, - child: Visibility( - visible: !isHidden || isPopoverOpen, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (!widget.hasCover) - FlowyButton( - resetHoverOnRebuild: false, - useIntrinsicWidth: true, - leftIconSize: const Size.square(18), - leftIcon: const FlowySvg(FlowySvgs.add_cover_s), - text: FlowyText.small( - LocaleKeys.document_plugins_cover_addCover.tr(), - ), - onTap: () => widget.onCoverChanged( - RowCoverPB( - data: isDesktop ? '1' : '0xffe8e0ff', - uploadType: FileUploadTypePB.LocalFile, - coverType: isDesktop - ? CoverTypePB.AssetCover - : CoverTypePB.ColorCover, - ), - ), - ), - if (!widget.hasIcon) - AppFlowyPopover( - controller: popoverController, - onClose: () => setState(() => isPopoverOpen = false), - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithCenterAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (_) { - isPopoverOpen = true; - return FlowyIconEmojiPicker( - tabs: const [PickerTabType.emoji], - onSelectedEmoji: (result) { - widget.onIconChanged(result.emoji); - popoverController.close(); - }, - ); - }, - child: FlowyButton( - useIntrinsicWidth: true, - leftIconSize: const Size.square(18), - leftIcon: const FlowySvg(FlowySvgs.add_icon_s), - text: FlowyText.small( - widget.hasIcon - ? LocaleKeys.document_plugins_cover_removeIcon.tr() - : LocaleKeys.document_plugins_cover_addIcon.tr(), - ), - onTap: () async { - if (!isDesktop) { - final result = await context.push( - MobileEmojiPickerScreen.routeName, - ); - - if (result != null) { - widget.onIconChanged(result.emoji); - } - } else { - popoverController.show(); - } - }, - ), - ), - ], - ), - ), - ), - ), - ); - } -} - -class RowIcon extends StatefulWidget { - const RowIcon({ - super.key, - required this.icon, - required this.onIconChanged, - }); - - final EmojiIconData icon; - final void Function(String?) onIconChanged; - - @override - State createState() => _RowIconState(); -} - -class _RowIconState extends State { - final controller = PopoverController(); - - @override - Widget build(BuildContext context) { - if (widget.icon.isEmpty) { - return const SizedBox.shrink(); - } - - return AppFlowyPopover( - controller: controller, - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithCenterAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), - margin: EdgeInsets.zero, - popupBuilder: (_) => FlowyIconEmojiPicker( - tabs: const [PickerTabType.emoji], - onSelectedEmoji: (result) { - controller.close(); - widget.onIconChanged(result.emoji); + ); }, ), - child: EmojiIconWidget(emoji: widget.icon), ); } } @@ -587,17 +132,25 @@ class _RowIconState extends State { class _BannerTitle extends StatelessWidget { const _BannerTitle({ required this.cellBuilder, + required this.popoverController, required this.rowController, }); final EditableCellBuilder cellBuilder; + final PopoverController popoverController; final RowController rowController; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final children = [ + final children = [ + if (state.rowMeta.icon.isNotEmpty) + EmojiButton( + emoji: state.rowMeta.icon, + showEmojiPicker: () => popoverController.show(), + ), + const HSpace(4), if (state.primaryField != null) Expanded( child: cellBuilder.buildCustom( @@ -610,8 +163,18 @@ class _BannerTitle extends StatelessWidget { ), ]; - return Padding( - padding: const EdgeInsets.only(left: 60), + return AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: const BoxConstraints(maxWidth: 380, maxHeight: 300), + popupBuilder: (popoverContext) => EmojiSelectionMenu( + onSubmitted: (emoji) { + popoverController.close(); + context.read().add(RowBannerEvent.setIcon(emoji)); + }, + onExit: () {}, + ), child: Row(children: children), ); }, @@ -619,43 +182,72 @@ class _BannerTitle extends StatelessWidget { } } -class _TitleSkin extends IEditableTextCellSkin { +class EmojiButton extends StatelessWidget { + const EmojiButton({ + super.key, + required this.emoji, + required this.showEmojiPicker, + }); + + final String emoji; + final VoidCallback showEmojiPicker; + @override - Widget build( - BuildContext context, - CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, - TextCellBloc bloc, - FocusNode focusNode, - TextEditingController textEditingController, - ) { - return CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): () => - focusNode.unfocus(), - const SimpleActivator(LogicalKeyboardKey.enter): () => - focusNode.unfocus(), - }, - child: TextField( - controller: textEditingController, - focusNode: focusNode, - autofocus: true, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28), - maxLines: null, - decoration: InputDecoration( - contentPadding: EdgeInsets.zero, - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - disabledBorder: InputBorder.none, - hintText: LocaleKeys.grid_row_titlePlaceholder.tr(), - isDense: true, - isCollapsed: true, + Widget build(BuildContext context) { + return SizedBox( + width: _kBannerActionHeight, + child: FlowyButton( + margin: EdgeInsets.zero, + text: FlowyText.medium( + emoji, + fontSize: 30, + textAlign: TextAlign.center, ), - onEditingComplete: () { - bloc.add(TextCellEvent.updateText(textEditingController.text)); - }, + onTap: showEmojiPicker, + ), + ); + } +} + +class AddEmojiButton extends StatelessWidget { + const AddEmojiButton({super.key, required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + child: FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_addIcon.tr(), + ), + leftIcon: const FlowySvg(FlowySvgs.emoji_s), + onTap: onTap, + margin: const EdgeInsets.all(4), + ), + ); + } +} + +class RemoveEmojiButton extends StatelessWidget { + const RemoveEmojiButton({super.key, required this.onTap}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 26, + child: FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.medium( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + ), + leftIcon: const FlowySvg(FlowySvgs.emoji_s), + onTap: onTap, + margin: const EdgeInsets.all(4), ), ); } @@ -677,9 +269,43 @@ class RowActionButton extends StatelessWidget { width: 20, height: 20, icon: const FlowySvg(FlowySvgs.details_horizontal_s), - iconColorOnHover: Theme.of(context).colorScheme.onSurface, + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, ), ), ); } } + +class _TitleSkin extends IEditableTextCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TextCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + autofocus: true, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 28), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: LocaleKeys.grid_row_titlePlaceholder.tr(), + isDense: true, + isCollapsed: true, + ), + onChanged: (text) { + if (textEditingController.value.composing.isCollapsed) { + bloc.add(TextCellEvent.updateText(text)); + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart index 8bd181b427..430f817095 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart @@ -6,20 +6,18 @@ import 'package:appflowy/plugins/database/domain/database_view_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/row_document.dart'; import 'package:appflowy/plugins/database_document/database_document_plugin.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; import '../cell/editable_cell_builder.dart'; + import 'row_banner.dart'; import 'row_property.dart'; @@ -29,41 +27,24 @@ class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { required this.rowController, required this.databaseController, this.allowOpenAsFullPage = true, - this.userProfile, }); final RowController rowController; final DatabaseController databaseController; final bool allowOpenAsFullPage; - final UserProfilePB? userProfile; @override State createState() => _RowDetailPageState(); } class _RowDetailPageState extends State { - // To allow blocking drop target in RowDocument from Field dialogs - final dropManagerState = EditorDropManagerState(); - + final scrollController = ScrollController(); late final cellBuilder = EditableCellBuilder( databaseController: widget.databaseController, ); - late final ScrollController scrollController; - - double scrollOffset = 0; - - @override - void initState() { - super.initState(); - scrollController = - ScrollController(onAttach: (_) => attachScrollListener()); - } - - void attachScrollListener() => scrollController.addListener(onScrollChanged); @override void dispose() { - scrollController.removeListener(onScrollChanged); scrollController.dispose(); super.dispose(); } @@ -71,101 +52,62 @@ class _RowDetailPageState extends State { @override Widget build(BuildContext context) { return FlowyDialog( - child: ChangeNotifierProvider.value( - value: dropManagerState, - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => RowDetailBloc( - fieldController: widget.databaseController.fieldController, - rowController: widget.rowController, - ), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => RowDetailBloc( + fieldController: widget.databaseController.fieldController, + rowController: widget.rowController, ), - BlocProvider.value(value: getIt()), - ], - child: BlocBuilder( - builder: (context, state) => Stack( - fit: StackFit.expand, + ), + BlocProvider.value(value: getIt()), + ], + child: Stack( + children: [ + ListView( + controller: scrollController, children: [ - Positioned.fill( - child: NestedScrollView( - controller: scrollController, - headerSliverBuilder: - (BuildContext context, bool innerBoxIsScrolled) { - return [ - SliverToBoxAdapter( - child: Column( - children: [ - RowBanner( - databaseController: widget.databaseController, - rowController: widget.rowController, - cellBuilder: cellBuilder, - allowOpenAsFullPage: widget.allowOpenAsFullPage, - userProfile: widget.userProfile, - ), - const VSpace(16), - Padding( - padding: - const EdgeInsets.only(left: 40, right: 60), - child: RowPropertyList( - cellBuilder: cellBuilder, - viewId: widget.databaseController.viewId, - fieldController: - widget.databaseController.fieldController, - ), - ), - const VSpace(20), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 60), - child: Divider(height: 1.0), - ), - const VSpace(20), - ], - ), - ), - ]; - }, - body: RowDocument( - viewId: widget.rowController.viewId, - rowId: widget.rowController.rowId, - ), + RowBanner( + databaseController: widget.databaseController, + rowController: widget.rowController, + cellBuilder: cellBuilder, + allowOpenAsFullPage: widget.allowOpenAsFullPage, + ), + const VSpace(16), + Padding( + padding: const EdgeInsets.only(left: 40, right: 60), + child: RowPropertyList( + cellBuilder: cellBuilder, + viewId: widget.databaseController.viewId, + fieldController: widget.databaseController.fieldController, ), ), - Positioned( - top: calculateActionsOffset( - state.rowMeta.cover.data.isNotEmpty, - ), - right: 12, - child: Row(children: actions(context)), + const VSpace(20), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 60), + child: Divider(height: 1.0), + ), + const VSpace(20), + RowDocument( + viewId: widget.rowController.viewId, + rowId: widget.rowController.rowId, ), ], ), - ), + Positioned( + top: 12, + right: 12, + child: Row( + children: _actions(context), + ), + ), + ], ), ), ); } - void onScrollChanged() { - if (scrollOffset != scrollController.offset) { - setState(() => scrollOffset = scrollController.offset); - } - } - - double calculateActionsOffset(bool hasCover) { - if (!hasCover) { - return 12; - } - - final offsetByScroll = clampDouble( - rowCoverHeight - scrollOffset, - 0, - rowCoverHeight, - ); - return 12 + offsetByScroll; - } - - List actions(BuildContext context) { + List _actions(BuildContext context) { return [ if (widget.allowOpenAsFullPage) ...[ FlowyTooltip( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart index 436dbd085d..4d58d6fc32 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_document.dart @@ -1,23 +1,16 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_document_bloc.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.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:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; class RowDocument extends StatelessWidget { const RowDocument({ @@ -34,23 +27,18 @@ class RowDocument extends StatelessWidget { return BlocProvider( create: (context) => RowDocumentBloc(viewId: viewId, rowId: rowId) ..add(const RowDocumentEvent.initial()), - child: BlocConsumer( - listener: (_, state) => state.loadingState.maybeWhen( - error: (error) => Log.error('RowDocument error: $error'), - orElse: () => null, - ), + child: BlocBuilder( builder: (context, state) { return state.loadingState.when( loading: () => const Center( child: CircularProgressIndicator.adaptive(), ), - error: (error) => Center( - child: AppFlowyErrorPage( - error: error, - ), + error: (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), - finish: () => _RowEditor( - view: state.viewPB!, + finish: () => RowEditor( + viewPB: state.viewPB!, onIsEmptyChanged: (isEmpty) => context .read() .add(RowDocumentEvent.updateIsEmpty(isEmpty)), @@ -62,101 +50,87 @@ class RowDocument extends StatelessWidget { } } -class _RowEditor extends StatelessWidget { - const _RowEditor({ - required this.view, +class RowEditor extends StatefulWidget { + const RowEditor({ + super.key, + required this.viewPB, this.onIsEmptyChanged, }); - final ViewPB view; + final ViewPB viewPB; final void Function(bool)? onIsEmptyChanged; + @override + State createState() => _RowEditorState(); +} + +class _RowEditorState extends State { + late final DocumentBloc documentBloc; + + @override + void initState() { + super.initState(); + documentBloc = DocumentBloc(documentId: widget.viewPB.id) + ..add(const DocumentEvent.initial()); + } + + @override + void dispose() { + documentBloc.close(); + super.dispose(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => DocumentBloc(documentId: view.id) - ..add(const DocumentEvent.initial()), - ), - BlocProvider( - create: (_) => ViewBloc(view: view)..add(const ViewEvent.initial()), - ), - ], - child: BlocConsumer( + providers: [BlocProvider.value(value: documentBloc)], + child: BlocListener( listenWhen: (previous, current) => previous.isDocumentEmpty != current.isDocumentEmpty, - listener: (_, state) { + listener: (context, state) { if (state.isDocumentEmpty != null) { - onIsEmptyChanged?.call(state.isDocumentEmpty!); - } - if (state.error != null) { - Log.error('RowEditor error: ${state.error}'); - } - if (state.editorState == null) { - Log.error('RowEditor unable to get editorState'); + widget.onIsEmptyChanged?.call(state.isDocumentEmpty!); } }, - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator.adaptive()); - } + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); + } - final editorState = state.editorState; - final error = state.error; - if (error != null || editorState == null) { - return Center( - child: AppFlowyErrorPage(error: error), - ); - } + final editorState = state.editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + } - return BlocProvider( - create: (context) => ViewInfoBloc(view: view), - child: Container( - constraints: const BoxConstraints(minHeight: 300), - child: Provider( - create: (_) { - final context = SharedEditorContext(); - context.isInDatabaseRowPage = true; - return context; - }, - dispose: (_, editorContext) => editorContext.dispose(), - child: AiWriterScrollWrapper( - viewId: view.id, - editorState: editorState, - child: EditorDropHandler( - viewId: view.id, + return IntrinsicHeight( + child: Container( + constraints: const BoxConstraints(minHeight: 300), + child: BlocProvider( + create: (context) => ViewInfoBloc(view: widget.viewPB), + child: AppFlowyEditorPage( + shrinkWrap: true, + autoFocus: false, editorState: editorState, - isLocalMode: context.read().isLocalMode, - dropManagerState: context.read(), - child: EditorTransactionService( - viewId: view.id, - editorState: editorState, - child: Provider( - create: (context) => DatabasePluginWidgetBuilderSize( - horizontalPadding: 0, - ), - child: AppFlowyEditorPage( - shrinkWrap: true, - autoFocus: false, - editorState: editorState, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: const EdgeInsets.only(left: 16, right: 54), - ), - showParagraphPlaceholder: (editorState, _) => - editorState.document.isEmpty, - placeholderText: (_) => - LocaleKeys.cardDetails_notesPlaceholder.tr(), - ), - ), + styleCustomizer: EditorStyleCustomizer( + context: context, + padding: const EdgeInsets.only(left: 16, right: 54), ), + showParagraphPlaceholder: (editorState, node) => + editorState.document.isEmpty, + placeholderText: (node) => + LocaleKeys.cardDetails_notesPlaceholder.tr(), ), ), ), - ), - ); - }, + ); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 240d33f0f2..8762918141 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -11,6 +11,9 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/header/deskt import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -18,10 +21,9 @@ 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:universal_platform/universal_platform.dart'; -import '../cell/editable_cell_builder.dart'; import 'accessory/cell_accessory.dart'; +import '../cell/editable_cell_builder.dart'; /// Display the row properties in a list. Only used in [RowDetailPage]. class RowPropertyList extends StatelessWidget { @@ -126,11 +128,48 @@ class _PropertyCell extends StatefulWidget { class _PropertyCellState extends State<_PropertyCell> { final PopoverController _popoverController = PopoverController(); + final PopoverController _fieldPopoverController = PopoverController(); final ValueNotifier _isFieldHover = ValueNotifier(false); @override Widget build(BuildContext context) { + final dragThumb = MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, + child: SizedBox( + width: 16, + height: 30, + child: AppFlowyPopover( + controller: _fieldPopoverController, + constraints: BoxConstraints.loose(const Size(240, 600)), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (popoverContext) => FieldEditor( + viewId: widget.fieldController.viewId, + field: widget.fieldController + .getField(widget.cellContext.fieldId)! + .field, + fieldController: widget.fieldController, + ), + child: ValueListenableBuilder( + valueListenable: _isFieldHover, + builder: (_, isHovering, child) => + isHovering ? child! : const SizedBox.shrink(), + child: BlockActionButton( + onTap: () => _fieldPopoverController.show(), + svg: FlowySvgs.drag_element_s, + richMessage: TextSpan( + text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), + ), + ), + ), + ), + ), + ); + final cell = widget.cellBuilder.buildStyled( widget.cellContext, EditableCellStyle.desktopRowDetail, @@ -167,12 +206,52 @@ class _PropertyCellState extends State<_PropertyCell> { return ReorderableDragStartListener( index: widget.index, enabled: value, - child: _buildDragHandle(context), + child: dragThumb, ); }, ), const HSpace(4), - _buildFieldButton(context), + BlocSelector( + selector: (state) => state.fields.firstWhereOrNull( + (fieldInfo) => fieldInfo.field.id == widget.cellContext.fieldId, + ), + builder: (context, fieldInfo) { + if (fieldInfo == null) { + return const SizedBox.shrink(); + } + return AppFlowyPopover( + controller: _popoverController, + constraints: BoxConstraints.loose(const Size(240, 600)), + margin: EdgeInsets.zero, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + popupBuilder: (popoverContext) => FieldEditor( + viewId: widget.fieldController.viewId, + field: fieldInfo.field, + fieldController: widget.fieldController, + ), + child: SizedBox( + width: 160, + height: 30, + child: Tooltip( + waitDuration: const Duration(seconds: 1), + preferBelow: false, + verticalOffset: 15, + message: fieldInfo.name, + child: FieldCellButton( + field: fieldInfo.field, + onTap: () => _popoverController.show(), + radius: BorderRadius.circular(6), + margin: const EdgeInsets.symmetric( + horizontal: 4, + vertical: 6, + ), + ), + ), + ), + ); + }, + ), const HSpace(8), Expanded(child: gesture), ], @@ -180,96 +259,6 @@ class _PropertyCellState extends State<_PropertyCell> { ), ); } - - Widget _buildDragHandle(BuildContext context) { - return MouseRegion( - cursor: Platform.isWindows - ? SystemMouseCursors.click - : SystemMouseCursors.grab, - child: SizedBox( - width: 16, - height: 30, - child: BlocListener( - listenWhen: (previous, current) => - previous.editingFieldId != current.editingFieldId, - listener: (context, state) { - if (state.editingFieldId == widget.cellContext.fieldId) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _popoverController.show(); - }); - } - }, - child: ValueListenableBuilder( - valueListenable: _isFieldHover, - builder: (_, isHovering, child) => - isHovering ? child! : const SizedBox.shrink(), - child: BlockActionButton( - onTap: () => context.read().add( - RowDetailEvent.startEditingField( - widget.cellContext.fieldId, - ), - ), - svg: FlowySvgs.drag_element_s, - richMessage: TextSpan( - text: LocaleKeys.grid_rowPage_fieldDragElementTooltip.tr(), - style: context.tooltipTextStyle(), - ), - ), - ), - ), - ), - ); - } - - Widget _buildFieldButton(BuildContext context) { - return BlocSelector( - selector: (state) => state.fields.firstWhereOrNull( - (fieldInfo) => fieldInfo.field.id == widget.cellContext.fieldId, - ), - builder: (context, fieldInfo) { - if (fieldInfo == null) { - return const SizedBox.shrink(); - } - return AppFlowyPopover( - controller: _popoverController, - constraints: BoxConstraints.loose(const Size(240, 600)), - margin: EdgeInsets.zero, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - onClose: () => context - .read() - .add(const RowDetailEvent.endEditingField()), - popupBuilder: (popoverContext) => FieldEditor( - viewId: widget.fieldController.viewId, - fieldInfo: fieldInfo, - fieldController: widget.fieldController, - isNewField: context.watch().state.newFieldId == - widget.cellContext.fieldId, - ), - child: SizedBox( - width: 160, - height: 30, - child: Tooltip( - waitDuration: const Duration(seconds: 1), - preferBelow: false, - verticalOffset: 15, - message: fieldInfo.name, - child: FieldCellButton( - field: fieldInfo.field, - onTap: () => context.read().add( - RowDetailEvent.startEditingField( - widget.cellContext.fieldId, - ), - ), - radius: BorderRadius.circular(6), - margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - ), - ), - ), - ); - }, - ); - } } class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { @@ -292,7 +281,7 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { namedArgs: {'count': '${state.numHiddenFields}'}, ); final quarterTurns = state.showHiddenFields ? 1 : 3; - return UniversalPlatform.isDesktopOrWeb + return PlatformExtension.isDesktopOrWeb ? _desktop(context, text, quarterTurns) : _mobile(context, text, quarterTurns); }, @@ -303,11 +292,7 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { return SizedBox( height: 30, child: FlowyButton( - text: FlowyText( - text, - lineHeight: 1.0, - color: Theme.of(context).hintColor, - ), + text: FlowyText.medium(text, color: Theme.of(context).hintColor), hoverColor: AFThemeExtension.of(context).lightGreyHover, leftIcon: RotatedBox( quarterTurns: quarterTurns, @@ -343,7 +328,7 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), - label: FlowyText( + label: FlowyText.medium( text, fontSize: 15, color: Theme.of(context).hintColor, @@ -363,7 +348,7 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { } } -class CreateRowFieldButton extends StatelessWidget { +class CreateRowFieldButton extends StatefulWidget { const CreateRowFieldButton({ super.key, required this.viewId, @@ -373,35 +358,65 @@ class CreateRowFieldButton extends StatelessWidget { final String viewId; final FieldController fieldController; + @override + State createState() => _CreateRowFieldButtonState(); +} + +class _CreateRowFieldButtonState extends State { + late PopoverController popoverController; + FieldPB? createdField; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + @override Widget build(BuildContext context) { - return SizedBox( - height: 30, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), - text: FlowyText( - lineHeight: 1.0, - LocaleKeys.grid_field_newProperty.tr(), - color: Theme.of(context).hintColor, - ), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () async { - final result = await FieldBackendService.createField( - viewId: viewId, - ); - await Future.delayed(const Duration(milliseconds: 50)); - result.fold( - (field) => context - .read() - .add(RowDetailEvent.startEditingNewField(field.id)), - (err) => Log.error("Failed to create field type option: $err"), - ); - }, - leftIcon: FlowySvg( - FlowySvgs.add_m, - color: Theme.of(context).hintColor, + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(240, 200)), + controller: popoverController, + direction: PopoverDirection.topWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + child: SizedBox( + height: 30, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), + text: FlowyText.medium( + LocaleKeys.grid_field_newProperty.tr(), + color: Theme.of(context).hintColor, + ), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: () async { + final result = await FieldBackendService.createField( + viewId: widget.viewId, + ); + result.fold( + (newField) { + createdField = newField; + popoverController.show(); + }, + (r) => Log.error("Failed to create field type option: $r"), + ); + }, + leftIcon: FlowySvg( + FlowySvgs.add_m, + color: Theme.of(context).hintColor, + ), ), ), + popupBuilder: (BuildContext popoverContext) { + if (createdField == null) { + return const SizedBox.shrink(); + } + return FieldEditor( + viewId: widget.viewId, + field: createdField!, + fieldController: widget.fieldController, + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart index b4ee4134c9..b8d1560141 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart @@ -1,33 +1,35 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/layout/layout_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_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'; -class DatabaseLayoutSelector extends StatelessWidget { +import '../../grid/presentation/layout/sizes.dart'; + +class DatabaseLayoutSelector extends StatefulWidget { const DatabaseLayoutSelector({ super.key, required this.viewId, - required this.databaseController, + required this.currentLayout, }); final String viewId; - final DatabaseController databaseController; + final DatabaseLayoutPB currentLayout; + @override + State createState() => _DatabaseLayoutSelectorState(); +} + +class _DatabaseLayoutSelectorState extends State { @override Widget build(BuildContext context) { return BlocProvider( create: (context) => DatabaseLayoutBloc( - viewId: viewId, - databaseLayout: databaseController.databaseLayout, + viewId: widget.viewId, + databaseLayout: widget.currentLayout, )..add(const DatabaseLayoutEvent.initial()), child: BlocBuilder( builder: (context, state) { @@ -42,57 +44,14 @@ class DatabaseLayoutSelector extends StatelessWidget { ), ) .toList(); - return Padding( + + return ListView.separated( + shrinkWrap: true, + itemCount: cells.length, padding: const EdgeInsets.symmetric(vertical: 6.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - padding: EdgeInsets.zero, - itemBuilder: (_, int index) => cells[index], - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - ), - Container( - height: 1, - margin: EdgeInsets.fromLTRB(8, 4, 8, 0), - color: AFThemeExtension.of(context).borderColor, - ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 4, 8, 2), - child: SizedBox( - height: 30, - child: FlowyButton( - resetHoverOnRebuild: false, - text: FlowyText( - LocaleKeys.grid_settings_compactMode.tr(), - lineHeight: 1.0, - ), - onTap: () { - databaseController.setCompactMode( - !databaseController.compactModeNotifier.value, - ); - }, - rightIcon: ValueListenableBuilder( - valueListenable: databaseController.compactModeNotifier, - builder: (context, compactMode, child) { - return Toggle( - value: compactMode, - duration: Duration.zero, - onChanged: (value) => - databaseController.setCompactMode(value), - padding: EdgeInsets.zero, - ); - }, - ), - ), - ), - ), - ], - ), + itemBuilder: (_, int index) => cells[index], + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), ); }, ), @@ -117,11 +76,10 @@ class DatabaseViewLayoutCell extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 6), child: SizedBox( - height: 30, + height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( databaseLayout.layoutName, color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart index c7bc286371..4d01970406 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart @@ -3,9 +3,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/group/database_group.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_layout_selector.dart'; +import 'package:appflowy/plugins/database/widgets/group/database_group.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -22,13 +23,13 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { FlowySvgData iconData() { switch (this) { case DatabaseSettingAction.showProperties: - return FlowySvgs.multiselect_s; + return FlowySvgs.properties_s; case DatabaseSettingAction.showLayout: - return FlowySvgs.database_layout_s; + return FlowySvgs.database_layout_m; case DatabaseSettingAction.showGroup: return FlowySvgs.group_s; case DatabaseSettingAction.showCalendarLayout: - return FlowySvgs.calendar_layout_s; + return FlowySvgs.calendar_layout_m; } } @@ -53,7 +54,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { final popover = switch (this) { DatabaseSettingAction.showLayout => DatabaseLayoutSelector( viewId: databaseController.viewId, - databaseController: databaseController, + currentLayout: databaseController.databaseLayout, ), DatabaseSettingAction.showGroup => DatabaseGroupList( viewId: databaseController.viewId, @@ -79,16 +80,14 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { height: GridSize.popoverItemHeight, child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( + text: FlowyText.medium( title(), - lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, ), leftIcon: FlowySvg( iconData(), color: Theme.of(context).iconTheme.color, ), - rightIcon: FlowySvg(FlowySvgs.database_settings_arrow_right_s), ), ), popupBuilder: (context) => popover, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart index 79d5e2410e..7422db15ca 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_settings_list.dart @@ -2,11 +2,11 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/widgets.dart'; -import 'package:universal_platform/universal_platform.dart'; class DatabaseSettingsList extends StatefulWidget { const DatabaseSettingsList({ @@ -21,13 +21,7 @@ class DatabaseSettingsList extends StatefulWidget { } class _DatabaseSettingsListState extends State { - final PopoverMutex popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - super.dispose(); - } + late final PopoverMutex popoverMutex = PopoverMutex(); @override Widget build(BuildContext context) { @@ -61,7 +55,7 @@ List actionsForDatabaseLayout(DatabaseLayoutPB? layout) { return [ DatabaseSettingAction.showProperties, DatabaseSettingAction.showLayout, - if (!UniversalPlatform.isMobile) DatabaseSettingAction.showGroup, + if (!PlatformExtension.isMobile) DatabaseSettingAction.showGroup, ]; case DatabaseLayoutPB.Calendar: return [ diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart index 6394a2ac1a..f3a548932d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart @@ -2,11 +2,11 @@ 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/database/view/database_field_list.dart'; -import 'package:appflowy/mobile/presentation/database/view/database_filter_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/database/view/database_sort_bottom_sheet.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -14,27 +14,25 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -enum MobileDatabaseControlFeatures { sort, filter } - class MobileDatabaseControls extends StatelessWidget { const MobileDatabaseControls({ super.key, required this.controller, - required this.features, + required this.toggleExtension, }); final DatabaseController controller; - final List features; + final ToggleExtensionNotifier toggleExtension; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => FilterEditorBloc( + BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: controller.viewId, fieldController: controller.fieldController, - ), + )..add(const DatabaseFilterMenuEvent.initial()), ), BlocProvider( create: (context) => SortEditorBloc( @@ -43,44 +41,38 @@ class MobileDatabaseControls extends StatelessWidget { ), ), ], - child: ValueListenableBuilder( - valueListenable: controller.isLoading, - builder: (context, isLoading, child) { - if (isLoading) { - return const SizedBox.shrink(); - } + child: BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + child: ValueListenableBuilder( + valueListenable: controller.isLoading, + builder: (context, isLoading, child) { + if (isLoading) { + return const SizedBox.shrink(); + } - return SeparatedRow( - separatorBuilder: () => const HSpace(8.0), - children: [ - if (features.contains(MobileDatabaseControlFeatures.sort)) + return SeparatedRow( + separatorBuilder: () => const HSpace(8.0), + children: [ _DatabaseControlButton( icon: FlowySvgs.sort_ascending_s, - count: context.watch().state.sorts.length, + count: context.watch().state.sortInfos.length, onTap: () => _showEditSortPanelFromToolbar( context, controller, ), ), - if (features.contains(MobileDatabaseControlFeatures.filter)) _DatabaseControlButton( - icon: FlowySvgs.filter_s, - count: context.watch().state.filters.length, - onTap: () => _showEditFilterPanelFromToolbar( + icon: FlowySvgs.m_field_hide_s, + onTap: () => _showDatabaseFieldListFromToolbar( context, controller, ), ), - _DatabaseControlButton( - icon: FlowySvgs.m_field_hide_s, - onTap: () => _showDatabaseFieldListFromToolbar( - context, - controller, - ), - ), - ], - ); - }, + ], + ); + }, + ), ), ); } @@ -167,22 +159,3 @@ void _showEditSortPanelFromToolbar( }, ); } - -void _showEditFilterPanelFromToolbar( - BuildContext context, - DatabaseController databaseController, -) { - showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useSafeArea: false, - backgroundColor: AFThemeExtension.of(context).background, - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: const MobileFilterEditor(), - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart index 36a6436b2a..7e52303b33 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_button.dart @@ -1,11 +1,14 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SettingButton extends StatefulWidget { const SettingButton({super.key, required this.databaseController}); @@ -27,17 +30,16 @@ class _SettingButtonState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 8), triggerActions: PopoverTriggerFlags.none, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyIconButton( - tooltipText: LocaleKeys.settings_title.tr(), - width: 24, - height: 24, - iconPadding: const EdgeInsets.all(3), - hoverColor: AFThemeExtension.of(context).lightGreyHover, - icon: const FlowySvg(FlowySvgs.settings_s), - onPressed: _popoverController.show, - ), + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fontColor: AFThemeExtension.of(context).textColor, + fontSize: FontSizes.s11, + fontWeight: FontWeight.w400, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: GridSize.toolbarSettingButtonInsets, + radius: Corners.s4Border, + onPressed: _popoverController.show, ), popupBuilder: (_) => DatabaseSettingsList(databaseController: widget.databaseController), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart index 8c7d35b2e4..ece69f9848 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart @@ -5,10 +5,11 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; +import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -42,12 +43,6 @@ class _DatabasePropertyListState extends State { )..add(const DatabasePropertyEvent.initial()); } - @override - void dispose() { - _popoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider.value( @@ -147,8 +142,7 @@ class _DatabasePropertyCellState extends State { margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, - text: FlowyText( - lineHeight: 1.0, + text: FlowyText.medium( widget.fieldInfo.name, color: AFThemeExtension.of(context).textColor, ), @@ -172,8 +166,10 @@ class _DatabasePropertyCellState extends State { ), ), const HSpace(6.0), - FieldIcon( - fieldInfo: widget.fieldInfo, + FlowySvg( + widget.fieldInfo.fieldType.svgData, + color: Theme.of(context).iconTheme.color, + size: const Size.square(16), ), ], ), @@ -200,9 +196,8 @@ class _DatabasePropertyCellState extends State { popupBuilder: (BuildContext context) { return FieldEditor( viewId: widget.viewId, - fieldInfo: widget.fieldInfo, + field: widget.fieldInfo.field, fieldController: widget.fieldController, - isNewField: false, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart index ee52be8c26..c08b32d9ec 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_page.dart @@ -3,30 +3,22 @@ import 'package:appflowy/plugins/database/application/row/related_row_detail_blo import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; -import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; - -import '../../workspace/application/view/view_bloc.dart'; // This widget is largely copied from `plugins/document/document_page.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. @@ -53,6 +45,18 @@ class DatabaseDocumentPage extends StatefulWidget { class _DatabaseDocumentPageState extends State { EditorState? editorState; + @override + void initState() { + super.initState(); + EditorNotification.addListener(_onEditorNotification); + } + + @override + void dispose() { + EditorNotification.removeListener(_onEditorNotification); + super.dispose(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -67,10 +71,6 @@ class _DatabaseDocumentPageState extends State { documentId: widget.documentId, )..add(const DocumentEvent.initial()), ), - BlocProvider( - create: (_) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), - ), ], child: BlocBuilder( builder: (context, state) { @@ -83,10 +83,9 @@ class _DatabaseDocumentPageState extends State { final error = state.error; if (error != null || editorState == null) { Log.error(error); - return Center( - child: AppFlowyErrorPage( - error: error, - ), + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ); } @@ -97,11 +96,7 @@ class _DatabaseDocumentPageState extends State { return BlocListener( listener: _onNotificationAction, listenWhen: (_, curr) => curr.action != null, - child: AiWriterScrollWrapper( - viewId: widget.view.id, - editorState: editorState, - child: _buildEditorPage(context, state), - ), + child: _buildEditorPage(context, state), ); }, ), @@ -109,44 +104,23 @@ class _DatabaseDocumentPageState extends State { } Widget _buildEditorPage(BuildContext context, DocumentState state) { - final appflowyEditorPage = EditorDropHandler( - viewId: widget.view.id, + final appflowyEditorPage = AppFlowyEditorPage( editorState: state.editorState!, - isLocalMode: context.read().isLocalMode, - child: AppFlowyEditorPage( - editorState: state.editorState!, - styleCustomizer: EditorStyleCustomizer( - context: context, - padding: EditorStyleCustomizer.documentPadding, - editorState: state.editorState!, - ), - header: _buildDatabaseDataContent(context, state.editorState!), - initialSelection: widget.initialSelection, - useViewInfoBloc: false, - placeholderText: (node) => - node.type == ParagraphBlockKeys.type && !node.isInTable - ? LocaleKeys.editor_slashPlaceHolder.tr() - : '', + styleCustomizer: EditorStyleCustomizer( + context: context, + // the 44 is the width of the left action list + padding: EditorStyleCustomizer.documentPadding, ), + header: _buildDatabaseDataContent(context, state.editorState!), + initialSelection: widget.initialSelection, + useViewInfoBloc: false, ); - return Provider( - create: (_) { - final context = SharedEditorContext(); - context.isInDatabaseRowPage = true; - return context; - }, - dispose: (_, editorContext) => editorContext.dispose(), - child: EditorTransactionService( - viewId: widget.view.id, - editorState: state.editorState!, - child: Column( - children: [ - if (state.isDeleted) _buildBanner(context), - Expanded(child: appflowyEditorPage), - ], - ), - ), + return Column( + children: [ + if (state.isDeleted) _buildBanner(context), + Expanded(child: appflowyEditorPage), + ], ); } @@ -164,39 +138,29 @@ class _DatabaseDocumentPageState extends State { return state.when( loading: () => const SizedBox.shrink(), ready: (databaseController, rowController) { - final padding = EditorStyleCustomizer.documentPadding; return BlocProvider( create: (context) => RowDetailBloc( fieldController: databaseController.fieldController, rowController: rowController, ), - child: Column( - children: [ - RowBanner( - databaseController: databaseController, - rowController: rowController, - cellBuilder: EditableCellBuilder( - databaseController: databaseController, - ), - userProfile: - context.read().userProfile, - ), - Padding( - padding: EdgeInsets.only( - top: 24, - left: padding.left, - right: padding.right, - ), - child: RowPropertyList( + child: Padding( + padding: EdgeInsets.only( + top: 24, + left: EditorStyleCustomizer.documentPadding.left + 16 + 6, + right: EditorStyleCustomizer.documentPadding.right, + ), + child: Column( + children: [ + RowPropertyList( viewId: databaseController.viewId, fieldController: databaseController.fieldController, cellBuilder: EditableCellBuilder( databaseController: databaseController, ), ), - ), - const TypeOptionSeparator(spacing: 24.0), - ], + const TypeOptionSeparator(spacing: 24.0), + ], + ), ), ); }, @@ -208,7 +172,6 @@ class _DatabaseDocumentPageState extends State { Widget _buildBanner(BuildContext context) { return DocumentBanner( - viewName: widget.view.name, onRestore: () => context.read().add( const DocumentEvent.restorePage(), ), @@ -218,6 +181,20 @@ class _DatabaseDocumentPageState extends State { ); } + void _onEditorNotification(EditorNotificationType type) { + final editorState = this.editorState; + if (editorState == null) { + return; + } + if (type == EditorNotificationType.undo) { + undoCommand.execute(editorState); + } else if (type == EditorNotificationType.redo) { + redoCommand.execute(editorState); + } else if (type == EditorNotificationType.exitEditing) { + editorState.selection = null; + } + } + void _onNotificationAction( BuildContext context, ActionNavigationState state, diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart index fd238271b7..e415aaa12e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/database_document_plugin.dart @@ -1,10 +1,9 @@ -library; +library document_plugin; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -98,9 +97,6 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder final String documentId; final Selection? initialSelection; - @override - String? get viewName => view.nameOrDefault; - @override EdgeInsets get contentPadding => EdgeInsets.zero; @@ -108,7 +104,6 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) { return BlocBuilder( builder: (_, state) => DatabaseDocumentPage( @@ -127,8 +122,7 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder ViewTitleBarWithRow(view: view, databaseId: databaseId, rowId: rowId); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => - const SizedBox.shrink(); + Widget tabBarItem(String pluginId) => const SizedBox.shrink(); @override Widget? get rightBarItem => const SizedBox.shrink(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart index 7f4493a999..37905ac88b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title.dart @@ -1,22 +1,24 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.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/widgets/cell/editable_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; +import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'database_document_title_bloc.dart'; // This widget is largely copied from `workspace/presentation/widgets/view_title_bar.dart` intentionally instead of opting for an abstraction. We can make an abstraction after the view refactor is done and there's more clarity in that department. @@ -46,16 +48,20 @@ class ViewTitleBarWithRow extends StatelessWidget { if (state.ancestors.isEmpty) { return const SizedBox.shrink(); } - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: SizedBox( - height: 24, - child: Row( - // refresh the view title bar when the ancestors changed - key: ValueKey(state.ancestors.hashCode), - children: _buildViewTitles(state.ancestors), - ), - ), + const maxWidth = WindowSizeManager.minWindowWidth - 200; + return LayoutBuilder( + builder: (context, constraints) { + return Visibility( + visible: maxWidth < constraints.maxWidth, + // if the width is too small, only show one view title bar without the ancestors + replacement: _buildRowName(), + child: Row( + // refresh the view title bar when the ancestors changed + key: ValueKey(state.ancestors.hashCode), + children: _buildViewTitles(state.ancestors), + ), + ); + }, ); }, ), @@ -66,22 +72,16 @@ class ViewTitleBarWithRow extends StatelessWidget { // if the level is too deep, only show the root view, the database view and the row return views.length > 2 ? [ - _buildViewButton(views[1]), - const FlowySvg(FlowySvgs.title_bar_divider_s), - const FlowyText.regular(' ... '), - const FlowySvg(FlowySvgs.title_bar_divider_s), + _buildViewButton(views.first), + const FlowyText.regular('/'), + const FlowyText.regular(' ... /'), _buildViewButton(views.last), - const FlowySvg(FlowySvgs.title_bar_divider_s), + const FlowyText.regular('/'), _buildRowName(), ] : [ ...views - .map( - (e) => [ - _buildViewButton(e), - const FlowySvg(FlowySvgs.title_bar_divider_s), - ], - ) + .map((e) => [_buildViewButton(e), const FlowyText.regular('/')]) .flattened, _buildRowName(), ]; @@ -90,9 +90,9 @@ class ViewTitleBarWithRow extends StatelessWidget { Widget _buildViewButton(ViewPB view) { return FlowyTooltip( message: view.name, - child: ViewTitle( + child: _ViewTitle( view: view, - behavior: ViewTitleBehavior.uneditable, + behavior: _ViewTitleBehavior.uneditable, onUpdated: () {}, ), ); @@ -141,13 +141,12 @@ class _TitleSkin extends IEditableTextCellSkin { Widget build( BuildContext context, CellContainerNotifier cellContainerNotifier, - ValueNotifier compactModeNotifier, TextCellBloc bloc, FocusNode focusNode, TextEditingController textEditingController, ) { return BlocSelector( - selector: (state) => state.content ?? "", + selector: (state) => state.content, builder: (context, content) { final name = content.isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() @@ -167,34 +166,31 @@ class _TitleSkin extends IEditableTextCellSkin { popupBuilder: (_) { return RenameRowPopover( textController: textEditingController, - icon: state.icon ?? EmojiIconData.none(), - onUpdateIcon: (icon) { + icon: state.icon ?? "", + onUpdateIcon: (String icon) { context .read() .add(DatabaseDocumentTitleEvent.updateIcon(icon)); }, onUpdateName: (text) => bloc.add(TextCellEvent.updateText(text)), - tabs: const [PickerTabType.emoji], ); }, child: FlowyButton( useIntrinsicWidth: true, onTap: () {}, - margin: const EdgeInsets.symmetric(horizontal: 6), text: Row( children: [ - if (state.icon != null) ...[ - RawEmojiIconWidget(emoji: state.icon!, emojiSize: 14), - const HSpace(4.0), - ], + EmojiText( + emoji: state.icon ?? "", + fontSize: 18.0, + ), + const HSpace(2.0), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 180), child: FlowyText.regular( name, overflow: TextOverflow.ellipsis, - fontSize: 14.0, - figmaLineHeight: 18.0, ), ), ], @@ -209,6 +205,106 @@ class _TitleSkin extends IEditableTextCellSkin { } } +enum _ViewTitleBehavior { + editable, + uneditable, +} + +class _ViewTitle extends StatefulWidget { + const _ViewTitle({ + required this.view, + this.behavior = _ViewTitleBehavior.editable, + required this.onUpdated, + }) : maxTitleWidth = 180; + + final ViewPB view; + final _ViewTitleBehavior behavior; + final double maxTitleWidth; + final VoidCallback onUpdated; + + @override + State<_ViewTitle> createState() => _ViewTitleState(); +} + +class _ViewTitleState extends State<_ViewTitle> { + late final viewListener = ViewListener(viewId: widget.view.id); + + String name = ''; + String icon = ''; + + @override + void initState() { + super.initState(); + + name = widget.view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : widget.view.name; + icon = widget.view.icon.value; + + viewListener.start( + onViewUpdated: (view) { + if (name != view.name || icon != view.icon.value) { + widget.onUpdated(); + } + setState(() { + name = view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : view.name; + icon = view.icon.value; + }); + }, + ); + } + + @override + void dispose() { + viewListener.stop(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(name), + const HSpace(4.0), + ], + ); + } + + final child = Row( + children: [ + EmojiText( + emoji: icon, + fontSize: 18.0, + ), + const HSpace(2.0), + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: widget.maxTitleWidth, + ), + child: FlowyText.regular( + name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + + return Listener( + onPointerDown: (_) => context.read().openPlugin(widget.view), + child: FlowyButton( + useIntrinsicWidth: true, + onTap: () {}, + text: child, + ), + ); + } +} + class RenameRowPopover extends StatefulWidget { const RenameRowPopover({ super.key, @@ -216,15 +312,13 @@ class RenameRowPopover extends StatefulWidget { required this.onUpdateName, required this.onUpdateIcon, required this.icon, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); final TextEditingController textController; - final EmojiIconData icon; + final String icon; - final ValueChanged onUpdateName; - final ValueChanged onUpdateIcon; - final List tabs; + final void Function(String name) onUpdateName; + final void Function(String icon) onUpdateIcon; @override State createState() => _RenameRowPopoverState(); @@ -250,11 +344,10 @@ class _RenameRowPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), defaultIcon: const FlowySvg(FlowySvgs.document_s), - onSubmitted: (r, _) { - widget.onUpdateIcon(r.data); - if (!r.keepOpen) PopoverContainer.of(context).close(); + onSubmitted: (emoji, _) { + widget.onUpdateIcon(emoji); + PopoverContainer.of(context).close(); }, - tabs: widget.tabs, ), const HSpace(6), SizedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart index 2711274cb2..5f8bf7ca08 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_document/presentation/database_document_title_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; @@ -12,8 +10,6 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - part 'database_document_title_bloc.freezed.dart'; class DatabaseDocumentTitleBloc @@ -59,7 +55,7 @@ class DatabaseDocumentTitleBloc ); }, updateIcon: (icon) { - _updateMeta(icon.emoji); + _updateMeta(icon); }, ); }); @@ -69,11 +65,7 @@ class DatabaseDocumentTitleBloc _metaListener.start( callback: (rowMeta) { if (!isClosed) { - add( - DatabaseDocumentTitleEvent.didUpdateRowIcon( - EmojiIconData.emoji(rowMeta.icon), - ), - ); + add(DatabaseDocumentTitleEvent.didUpdateRowIcon(rowMeta.icon)); } }, ); @@ -95,8 +87,6 @@ class DatabaseDocumentTitleBloc viewId: view.id, rowCache: databaseController.rowCache, ); - unawaited(rowController.initialize()); - final primaryFieldId = await FieldBackendService.getPrimaryField(viewId: view.id).fold( (primaryField) => primaryField.id, @@ -122,11 +112,7 @@ class DatabaseDocumentTitleBloc // initialize icon if (rowInfo.rowMeta.icon.isNotEmpty) { - add( - DatabaseDocumentTitleEvent.didUpdateRowIcon( - EmojiIconData.emoji(rowInfo.rowMeta.icon), - ), - ); + add(DatabaseDocumentTitleEvent.didUpdateRowIcon(rowInfo.rowMeta.icon)); } } @@ -146,19 +132,16 @@ class DatabaseDocumentTitleEvent with _$DatabaseDocumentTitleEvent { const factory DatabaseDocumentTitleEvent.didUpdateAncestors( List ancestors, ) = _DidUpdateAncestors; - const factory DatabaseDocumentTitleEvent.didUpdateRowTitleInfo( DatabaseController databaseController, RowController rowController, String fieldId, ) = _DidUpdateRowTitleInfo; - const factory DatabaseDocumentTitleEvent.didUpdateRowIcon( - EmojiIconData icon, + String icon, ) = _DidUpdateRowIcon; - const factory DatabaseDocumentTitleEvent.updateIcon( - EmojiIconData icon, + String icon, ) = _UpdateIcon; } @@ -169,7 +152,7 @@ class DatabaseDocumentTitleState with _$DatabaseDocumentTitleState { required DatabaseController? databaseController, required RowController? rowController, required String? fieldId, - required EmojiIconData? icon, + required String? icon, }) = _DatabaseDocumentTitleState; factory DatabaseDocumentTitleState.initial() => diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart index c65d818351..1a39c519a3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_appearance_cubit.dart @@ -1,20 +1,17 @@ import 'dart:async'; import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:bloc/bloc.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:universal_platform/universal_platform.dart'; class DocumentAppearance { const DocumentAppearance({ required this.fontSize, required this.fontFamily, required this.codeFontFamily, - required this.width, this.cursorColor, this.selectionColor, this.defaultTextDirection, @@ -26,7 +23,6 @@ class DocumentAppearance { final Color? cursorColor; final Color? selectionColor; final String? defaultTextDirection; - final double width; /// For nullable fields (like `cursorColor`), /// use the corresponding `isNull` flag (like `cursorColorIsNull`) to explicitly set the field to `null`. @@ -43,7 +39,6 @@ class DocumentAppearance { bool cursorColorIsNull = false, bool selectionColorIsNull = false, bool textDirectionIsNull = false, - double? width, }) { return DocumentAppearance( fontSize: fontSize ?? this.fontSize, @@ -55,7 +50,6 @@ class DocumentAppearance { defaultTextDirection: textDirectionIsNull ? null : defaultTextDirection ?? this.defaultTextDirection, - width: width ?? this.width, ); } } @@ -63,13 +57,10 @@ class DocumentAppearance { class DocumentAppearanceCubit extends Cubit { DocumentAppearanceCubit() : super( - DocumentAppearance( + const DocumentAppearance( fontSize: 16.0, fontFamily: defaultFontFamily, codeFontFamily: builtInCodeFontFamily, - width: UniversalPlatform.isMobile - ? double.infinity - : EditorStyleCustomizer.maxDocumentWidth, ), ); @@ -91,7 +82,6 @@ class DocumentAppearanceCubit extends Cubit { final selectionColor = selectionColorString != null ? Color(int.parse(selectionColorString)) : null; - final double? width = prefs.getDouble(KVKeys.kDocumentAppearanceWidth); final textScaleFactor = double.parse(prefs.getString(KVKeys.textScaleFactor) ?? '1.0'); @@ -110,7 +100,6 @@ class DocumentAppearanceCubit extends Cubit { cursorColorIsNull: cursorColor == null, selectionColorIsNull: selectionColor == null, textDirectionIsNull: defaultTextDirection == null, - width: width, ), ); } @@ -197,21 +186,4 @@ class DocumentAppearanceCubit extends Cubit { ); } } - - Future syncWidth(double? width) async { - final prefs = await SharedPreferences.getInstance(); - - width ??= UniversalPlatform.isMobile - ? double.infinity - : EditorStyleCustomizer.maxDocumentWidth; - width = width.clamp( - EditorStyleCustomizer.minDocumentWidth, - EditorStyleCustomizer.maxDocumentWidth, - ); - await prefs.setDouble(KVKeys.kDocumentAppearanceWidth, width); - - if (!isClosed) { - emit(state.copyWith(width: width)); - } - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 264ec4bb11..121582e1f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -6,13 +6,12 @@ import 'package:appflowy/plugins/document/application/document_awareness_metadat import 'package:appflowy/plugins/document/application/document_collab_adapter.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_rules.dart'; import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; @@ -20,14 +19,19 @@ import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/util/throttle.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' - show AppFlowyEditorLogLevel, EditorState, TransactionTime; + show + EditorState, + LogLevel, + TransactionTime, + Selection, + Position, + paragraphNode; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -35,23 +39,12 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'document_bloc.freezed.dart'; -/// Enable this flag to enable the internal log for -/// - document diff -/// - document integrity check -/// - document sync state -/// - document awareness states -bool enableDocumentInternalLog = false; - -final Map _documentBlocMap = {}; - class DocumentBloc extends Bloc { DocumentBloc({ required this.documentId, this.databaseViewId, this.rowId, - bool saveToBlocMap = true, - }) : _saveToBlocMap = saveToBlocMap, - _documentListener = DocumentListener(id: documentId), + }) : _documentListener = DocumentListener(id: documentId), _syncStateListener = DocumentSyncStateListener(id: documentId), super(DocumentState.initial()) { _viewListener = databaseViewId == null && rowId == null @@ -60,17 +53,12 @@ class DocumentBloc extends Bloc { on(_onDocumentEvent); } - static DocumentBloc? findOpen(String documentId) => - _documentBlocMap[documentId]; - /// For a normal document, the document id is the same as the view id final String documentId; final String? databaseViewId; final String? rowId; - final bool _saveToBlocMap; - final DocumentListener _documentListener; final DocumentSyncStateListener _syncStateListener; late final ViewListener? _viewListener; @@ -85,8 +73,6 @@ class DocumentBloc extends Bloc { documentService: _documentService, ); - late final DocumentRules _documentRules; - StreamSubscription? _transactionSubscription; bool isClosing = false; @@ -101,41 +87,26 @@ class DocumentBloc extends Bloc { bool get isLocalMode { final userProfilePB = state.userProfilePB; - final type = userProfilePB?.workspaceAuthType ?? AuthTypePB.Local; - return type == AuthTypePB.Local; + final type = userProfilePB?.authenticator ?? AuthenticatorPB.Local; + return type == AuthenticatorPB.Local; } @override Future close() async { isClosing = true; - if (_saveToBlocMap) { - _documentBlocMap.remove(documentId); - } - await checkDocumentIntegrity(); - await _cancelSubscriptions(); - _clearEditorState(); - return super.close(); - } - - Future _cancelSubscriptions() async { + _updateSelectionDebounce.dispose(); + _syncThrottle.dispose(); await _documentService.syncAwarenessStates(documentId: documentId); await _documentListener.stop(); await _syncStateListener.stop(); await _viewListener?.stop(); await _transactionSubscription?.cancel(); await _documentService.closeDocument(viewId: documentId); - } - - void _clearEditorState() { - _updateSelectionDebounce.dispose(); - _syncThrottle.dispose(); - _syncTimer?.cancel(); _syncTimer = null; - state.editorState?.selectionNotifier - .removeListener(_debounceOnSelectionUpdate); state.editorState?.service.keyboardService?.closeKeyboard(); state.editorState?.dispose(); + return super.close(); } Future _onDocumentEvent( @@ -144,12 +115,14 @@ class DocumentBloc extends Bloc { ) async { await event.when( initial: () async { - if (_saveToBlocMap) { - _documentBlocMap[documentId] = this; - } final result = await _fetchDocumentState(); _onViewChanged(); _onDocumentChanged(); + result.onSuccess((s) { + if (s != null) { + _migrateCover(s); + } + }); final newState = await result.fold( (s) async { final userProfilePB = @@ -187,7 +160,7 @@ class DocumentBloc extends Bloc { }, restorePage: () async { if (databaseViewId == null && rowId == null) { - final result = await TrashService.putback(documentId); + final result = await _trashService.putback(documentId); final isDeleted = result.fold((l) => false, (r) => true); emit(state.copyWith(isDeleted: isDeleted)); } @@ -246,10 +219,6 @@ class DocumentBloc extends Bloc { } Future _initAppFlowyEditorState(DocumentDataPB data) async { - if (enableDocumentInternalLog) { - Log.info('document data: ${data.toProto3Json()}'); - } - final document = data.toDocument(); if (document == null) { assert(false, 'document is null'); @@ -259,42 +228,21 @@ class DocumentBloc extends Bloc { final editorState = EditorState(document: document); _documentCollabAdapter = DocumentCollabAdapter(editorState, documentId); - _documentRules = DocumentRules(editorState: editorState); // subscribe to the document change from the editor _transactionSubscription = editorState.transactionStream.listen( - (value) async { - final time = value.$1; - final transaction = value.$2; - final options = value.$3; + (event) async { + final time = event.$1; + final transaction = event.$2; if (time != TransactionTime.before) { return; } - if (options.inMemoryUpdate) { - if (enableDocumentInternalLog) { - Log.trace('skip transaction for in-memory update'); - } - return; - } - - if (enableDocumentInternalLog) { - Log.trace( - '[TransactionAdapter] 1. transaction before apply: ${transaction.hashCode}', - ); - } - // apply transaction to backend await _transactionAdapter.apply(transaction, editorState); // check if the document is empty. - await _documentRules.applyRules(value: value); - - if (enableDocumentInternalLog) { - Log.trace( - '[TransactionAdapter] 4. transaction after apply: ${transaction.hashCode}', - ); - } + await _applyRules(); if (!isClosed) { // ignore: invalid_use_of_visible_for_testing_member @@ -308,17 +256,53 @@ class DocumentBloc extends Bloc { // output the log from the editor when debug mode if (kDebugMode) { editorState.logConfiguration - ..level = AppFlowyEditorLogLevel.all + ..level = LogLevel.all ..handler = (log) { - if (enableDocumentInternalLog) { - // Log.info(log); - } + // Log.debug(log); }; } return editorState; } + Future _applyRules() async { + await Future.wait([ + _ensureAtLeastOneParagraphExists(), + _ensureLastNodeIsEditable(), + ]); + } + + Future _ensureLastNodeIsEditable() async { + final editorState = state.editorState; + if (editorState == null) { + return; + } + final document = editorState.document; + final lastNode = document.root.children.lastOrNull; + if (lastNode == null || lastNode.delta == null) { + final transaction = editorState.transaction; + transaction.insertNode([document.root.children.length], paragraphNode()); + transaction.afterSelection = transaction.beforeSelection; + await editorState.apply(transaction); + } + } + + Future _ensureAtLeastOneParagraphExists() async { + final editorState = state.editorState; + if (editorState == null) { + return; + } + final document = editorState.document; + if (document.root.children.isEmpty) { + final transaction = editorState.transaction; + transaction.insertNode([0], paragraphNode()); + transaction.afterSelection = Selection.collapsed( + Position(path: [0]), + ); + await editorState.apply(transaction); + } + } + Future _onDocumentStateUpdate(DocEventPB docEvent) async { if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) { return; @@ -348,9 +332,6 @@ class DocumentBloc extends Bloc { } void _throttleSyncDoc(DocEventPB docEvent) { - if (enableDocumentInternalLog) { - Log.info('[DocumentBloc] throttle sync doc: ${docEvent.toProto3Json()}'); - } _syncThrottle.call(() { _onDocumentStateUpdate(docEvent); }); @@ -377,7 +358,7 @@ class DocumentBloc extends Bloc { final basicColor = ColorGenerator(id.toString()).toColor(); final metadata = DocumentAwarenessMetadata( cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), + selectionColor: basicColor.withOpacity(0.6).toHexString(), userName: user.name, userAvatar: user.iconUrl, ); @@ -400,7 +381,7 @@ class DocumentBloc extends Bloc { final basicColor = ColorGenerator(id.toString()).toColor(); final metadata = DocumentAwarenessMetadata( cursorColor: basicColor.toHexString(), - selectionColor: basicColor.withValues(alpha: 0.6).toHexString(), + selectionColor: basicColor.withOpacity(0.6).toHexString(), userName: user.name, userAvatar: user.iconUrl, ); @@ -410,43 +391,12 @@ class DocumentBloc extends Bloc { ); } - Future forceReloadDocumentState() { - return _documentCollabAdapter.syncV3(); - } - - // this is only used for debug mode - Future checkDocumentIntegrity() async { - if (!enableDocumentInternalLog) { - return; - } - - final cloudDocResult = - await _documentService.getDocument(documentId: documentId); - final cloudDoc = cloudDocResult.fold((s) => s, (f) => null)?.toDocument(); - final localDoc = state.editorState?.document; - if (cloudDoc == null || localDoc == null) { - return; - } - final cloudJson = cloudDoc.toJson(); - final localJson = localDoc.toJson(); - final deepEqual = const DeepCollectionEquality().equals( - cloudJson, - localJson, - ); - if (!deepEqual) { - Log.error('document integrity check failed'); - // Enable it to debug the document integrity check failed - Log.error('cloud doc: $cloudJson'); - Log.error('local doc: $localJson'); - - final context = AppGlobals.rootNavKey.currentContext; - if (context != null && context.mounted) { - showToastNotification( - message: 'document integrity check failed', - type: ToastificationType.error, - ); - } - } + // from version 0.5.5, the cover is stored in the view.ext + Future _migrateCover(EditorState editorState) async { + final view = await ViewBackendService.getView(documentId); + view.onSuccess((s) { + return EditorMigration.migrateCoverIfNeeded(s, editorState); + }); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart index f550093b54..8d8841cc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; -import 'package:appflowy/plugins/document/application/document_diff.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; @@ -10,21 +9,17 @@ import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy/util/json_print.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:collection/collection.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class DocumentCollabAdapter { - DocumentCollabAdapter( - this.editorState, - this.docId, - ); + DocumentCollabAdapter(this.editorState, this.docId); final EditorState editorState; final String docId; - final DocumentDiff diff = const DocumentDiff(); final _service = DocumentService(); @@ -80,14 +75,13 @@ class DocumentCollabAdapter { return; } - final ops = diff.diffDocument(editorState.document, document); + final ops = diffNodes(editorState.document.root, document.root); if (ops.isEmpty) { return; } - if (enableDocumentInternalLog) { - prettyPrintJson(ops.map((op) => op.toJson()).toList()); - } + // Use for debugging, DO NOT REMOVE + // prettyPrintJson(ops.map((op) => op.toJson()).toList()); final transaction = editorState.transaction; for (final op in ops) { @@ -95,19 +89,18 @@ class DocumentCollabAdapter { } await editorState.apply(transaction, isRemote: true); - if (enableDocumentInternalLog) { - assert(() { - final local = editorState.document.root.toJson(); - final remote = document.root.toJson(); - if (!const DeepCollectionEquality().equals(local, remote)) { - Log.error('Invalid diff status'); - Log.error('Local: $local'); - Log.error('Remote: $remote'); - return false; - } - return true; - }()); - } + // Use for debugging, DO NOT REMOVE + // assert(() { + // final local = editorState.document.root.toJson(); + // final remote = document.root.toJson(); + // if (!const DeepCollectionEquality().equals(local, remote)) { + // Log.error('Invalid diff status'); + // Log.error('Local: $local'); + // Log.error('Remote: $remote'); + // return false; + // } + // return true; + // }()); } Future forceReload() async { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart index a0678372cf..7e5e4eb528 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collaborators_bloc.dart @@ -32,17 +32,13 @@ class DocumentCollaboratorsBloc emit( state.copyWith( shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, ), ); final deviceId = ApplicationInfo.deviceId; if (userProfile != null) { _listener.start( onDocAwarenessUpdate: (states) { - if (isClosed) { - return; - } - add( DocumentCollaboratorsEvent.update( userProfile, @@ -85,11 +81,7 @@ class DocumentCollaboratorsBloc final ids = {}; final sorted = states.value.values.toList() ..sort((a, b) => b.timestamp.compareTo(a.timestamp)) - // filter the duplicate users - ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId)) - // only keep version 1 and metadata is not empty - ..retainWhere((e) => e.version == 1) - ..retainWhere((e) => e.metadata.isNotEmpty); + ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId)); for (final state in sorted) { if (state.version != 1) { continue; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index 38bf2bcd14..da99886014 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -13,10 +13,12 @@ import 'package:appflowy_editor/appflowy_editor.dart' NodeIterator, NodeExternalValues, HeadingBlockKeys, + QuoteBlockKeys, NumberedListBlockKeys, BulletedListBlockKeys, blockComponentDelta; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; class ExternalValues extends NodeExternalValues { @@ -103,7 +105,7 @@ extension DocumentDataPBFromTo on DocumentDataPB { final children = []; if (childrenIds != null && childrenIds.isNotEmpty) { - children.addAll(childrenIds.map((e) => buildNode(e)).nonNulls); + children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull()); } final node = block?.toNode( @@ -178,8 +180,6 @@ extension NodeToBlock on Node { String? parentId, String? childrenId, Attributes? attributes, - String? externalId, - String? externalType, }) { assert(id.isNotEmpty); final block = BlockPB.create() @@ -192,29 +192,10 @@ extension NodeToBlock on Node { if (parentId != null && parentId.isNotEmpty) { block.parentId = parentId; } - if (externalId != null && externalId.isNotEmpty) { - block.externalId = externalId; - } - if (externalType != null && externalType.isNotEmpty) { - block.externalType = externalType; - } return block; } String _dataAdapter(String type, Attributes attributes) { - try { - return jsonEncode( - attributes, - toEncodable: (value) { - if (value is Map) { - return jsonEncode(value); - } - return value; - }, - ); - } catch (e) { - Log.error('encode attributes error: $e'); - return '{}'; - } + return jsonEncode(attributes); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_diff.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_diff.dart deleted file mode 100644 index e174d6671e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_diff.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; - -/// DocumentDiff compares two document states and generates operations needed -/// to transform one state into another. -class DocumentDiff { - const DocumentDiff({ - this.enableDebugLog = false, - }); - - final bool enableDebugLog; - - // Using DeepCollectionEquality for deep comparison of collections - static const _equality = DeepCollectionEquality(); - - /// Generates operations needed to transform oldDocument into newDocument. - /// Returns a list of operations (Insert, Delete, Update) that can be applied sequentially. - List diffDocument(Document oldDocument, Document newDocument) { - return diffNode(oldDocument.root, newDocument.root); - } - - /// Compares two nodes and their children recursively to generate transformation operations. - /// Returns a list of operations that will transform oldNode into newNode. - List diffNode(Node oldNode, Node newNode) { - final operations = []; - - // Compare and update node attributes if they're different. - // Using DeepCollectionEquality instead of == for deep comparison of collections - if (!_equality.equals(oldNode.attributes, newNode.attributes)) { - operations.add( - UpdateOperation(oldNode.path, newNode.attributes, oldNode.attributes), - ); - } - - final oldChildrenById = Map.fromEntries( - oldNode.children.map((child) => MapEntry(child.id, child)), - ); - final newChildrenById = Map.fromEntries( - newNode.children.map((child) => MapEntry(child.id, child)), - ); - - // Insertion or Update - for (final newChild in newNode.children) { - final oldChild = oldChildrenById[newChild.id]; - if (oldChild == null) { - // If the node doesn't exist in the old document, it's a new node. - operations.add(InsertOperation(newChild.path, [newChild])); - } else { - // If the node exists in the old document, recursively compare its children - operations.addAll(diffNode(oldChild, newChild)); - } - } - - // Deletion - for (final id in oldChildrenById.keys) { - // If the node doesn't exist in the new document, it's a deletion. - if (!newChildrenById.containsKey(id)) { - final oldChild = oldChildrenById[id]!; - operations.add(DeleteOperation(oldChild.path, [oldChild])); - } - } - - // Optimize operations by merging consecutive inserts and deletes - return _optimizeOperations(operations); - } - - /// Optimizes the list of operations by merging consecutive operations where possible. - /// This reduces the total number of operations that need to be applied. - List _optimizeOperations(List operations) { - // Optimize the insert operations first, then the delete operations - final optimizedOps = mergeDeleteOperations( - mergeInsertOperations( - operations, - ), - ); - return optimizedOps; - } - - /// Merges consecutive insert operations to reduce the number of operations. - /// Operations are merged if they target consecutive paths in the document. - List mergeInsertOperations(List operations) { - if (enableDebugLog) { - _logOperations('mergeInsertOperations[before]', operations); - } - - final copy = [...operations]; - final insertOperations = operations - .whereType() - .sorted(_descendingCompareTo) - .toList(); - - _mergeConsecutiveOperations( - insertOperations, - (prev, current) => InsertOperation( - prev.path, - [...prev.nodes, ...current.nodes], - ), - ); - - if (insertOperations.isNotEmpty) { - copy - ..removeWhere((op) => op is InsertOperation) - ..insertAll(0, insertOperations); // Insert ops must be at the start - } - - if (enableDebugLog) { - _logOperations('mergeInsertOperations[after]', copy); - } - - return copy; - } - - /// Merges consecutive delete operations to reduce the number of operations. - /// Operations are merged if they target consecutive paths in the document. - List mergeDeleteOperations(List operations) { - if (enableDebugLog) { - _logOperations('mergeDeleteOperations[before]', operations); - } - - final copy = [...operations]; - final deleteOperations = operations - .whereType() - .sorted(_descendingCompareTo) - .toList(); - - _mergeConsecutiveOperations( - deleteOperations, - (prev, current) => DeleteOperation( - prev.path, - [...prev.nodes, ...current.nodes], - ), - ); - - if (deleteOperations.isNotEmpty) { - copy - ..removeWhere((op) => op is DeleteOperation) - ..addAll(deleteOperations); // Delete ops must be at the end - } - - if (enableDebugLog) { - _logOperations('mergeDeleteOperations[after]', copy); - } - - return copy; - } - - /// Merge consecutive operations of the same type - void _mergeConsecutiveOperations( - List operations, - T Function(T prev, T current) merge, - ) { - for (var i = operations.length - 1; i > 0; i--) { - final op = operations[i]; - final previousOp = operations[i - 1]; - - if (op.path.equals(previousOp.path.next)) { - operations - ..removeAt(i) - ..[i - 1] = merge(previousOp, op); - } - } - } - - void _logOperations(String prefix, List operations) { - debugPrint('$prefix: ${operations.map((op) => op.toJson()).toList()}'); - } - - int _descendingCompareTo(Operation a, Operation b) { - return a.path > b.path ? 1 : -1; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart deleted file mode 100644 index f530b1ef8d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_rules.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// Apply rules to the document -/// -/// 1. ensure there is at least one paragraph in the document, otherwise the user will be blocked from typing -/// 2. remove columns block if its children are empty -class DocumentRules { - DocumentRules({ - required this.editorState, - }); - - final EditorState editorState; - - Future applyRules({ - required EditorTransactionValue value, - }) async { - await Future.wait([ - _ensureAtLeastOneParagraphExists(value: value), - _removeColumnIfItIsEmpty(value: value), - ]); - } - - Future _ensureAtLeastOneParagraphExists({ - required EditorTransactionValue value, - }) async { - final document = editorState.document; - if (document.root.children.isEmpty) { - final transaction = editorState.transaction; - transaction - ..insertNode([0], paragraphNode()) - ..afterSelection = Selection.collapsed( - Position(path: [0]), - ); - await editorState.apply(transaction); - } - } - - Future _removeColumnIfItIsEmpty({ - required EditorTransactionValue value, - }) async { - final transaction = value.$2; - final options = value.$3; - - if (options.inMemoryUpdate) { - return; - } - - for (final operation in transaction.operations) { - final deleteColumnsTransaction = editorState.transaction; - if (operation is DeleteOperation) { - final path = operation.path; - final column = editorState.document.nodeAtPath(path.parent); - if (column != null && column.type == SimpleColumnBlockKeys.type) { - // check if the column is empty - final children = column.children; - if (children.isEmpty) { - // delete the column or the columns - final columns = column.parent; - - if (columns != null && - columns.type == SimpleColumnsBlockKeys.type) { - final nonEmptyColumnCount = columns.children.fold( - 0, - (p, c) => c.children.isEmpty ? p : p + 1, - ); - - // Example: - // columns - // - column 1 - // - paragraph 1-1 - // - paragraph 1-2 - // - column 2 - // - paragraph 2 - // - column 3 - // - paragraph 3 - // - // case 1: delete the paragraph 3 from column 3. - // because there is only one child in column 3, we should delete the column 3 as well. - // the result should be: - // columns - // - column 1 - // - paragraph 1-1 - // - paragraph 1-2 - // - column 2 - // - paragraph 2 - // - // case 2: delete the paragraph 3 from column 3 and delete the paragraph 2 from column 2. - // in this case, there will be only one column left, so we should delete the columns block and flatten the children. - // the result should be: - // paragraph 1-1 - // paragraph 1-2 - - // if there is only one empty column left, delete the columns block and flatten the children - if (nonEmptyColumnCount <= 1) { - // move the children in columns out of the column - final children = columns.children - .map((e) => e.children) - .expand((e) => e) - .map((e) => e.deepCopy()) - .toList(); - deleteColumnsTransaction.insertNodes(columns.path, children); - deleteColumnsTransaction.deleteNode(columns); - } else { - // otherwise, delete the column - deleteColumnsTransaction.deleteNode(column); - - final deletedColumnRatio = - column.attributes[SimpleColumnBlockKeys.ratio]; - if (deletedColumnRatio != null) { - // update the ratio of the columns - final columnsNode = column.columnsParent; - if (columnsNode != null) { - final length = columnsNode.children.length; - for (final columnNode in columnsNode.children) { - final ratio = - columnNode.attributes[SimpleColumnBlockKeys.ratio] ?? - 1.0 / length; - if (ratio != null) { - deleteColumnsTransaction.updateNode(columnNode, { - ...columnNode.attributes, - SimpleColumnBlockKeys.ratio: - ratio + deletedColumnRatio / (length - 1), - }); - } - } - } - } - } - } - } - } - } - - if (deleteColumnsTransaction.operations.isNotEmpty) { - await editorState.apply(deleteColumnsTransaction); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart index f520e20d02..6a0b79c90e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_service.dart @@ -1,6 +1,4 @@ -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -38,41 +36,6 @@ class DocumentService { return result; } - Future> - getDocumentNode({ - required String documentId, - required String blockId, - }) async { - final documentResult = await getDocument(documentId: documentId); - final document = documentResult.fold((l) => l, (f) => null); - if (document == null) { - Log.error('unable to get the document for page $documentId'); - return FlowyResult.failure(FlowyError(msg: 'Document not found')); - } - - final blockResult = await getBlockFromDocument( - document: document, - blockId: blockId, - ); - final block = blockResult.fold((l) => l, (f) => null); - if (block == null) { - Log.error( - 'unable to get the block $blockId from the document $documentId', - ); - return FlowyResult.failure(FlowyError(msg: 'Block not found')); - } - - final node = document.buildNode(blockId); - if (node == null) { - Log.error( - 'unable to get the node for block $blockId in document $documentId', - ); - return FlowyResult.failure(FlowyError(msg: 'Node not found')); - } - - return FlowyResult.success((document, block, node)); - } - Future> getBlockFromDocument({ required DocumentDataPB document, required String blockId, @@ -151,22 +114,20 @@ class DocumentService { /// Upload a file to the cloud storage. Future> uploadFile({ required String localFilePath, - required String documentId, + bool isAsync = true, }) async { final workspace = await FolderEventReadCurrentWorkspace().send(); - return workspace.fold( - (l) async { - final payload = UploadFileParamsPB( - workspaceId: l.id, - localFilePath: localFilePath, - documentId: documentId, - ); - return DocumentEventUploadFile(payload).send(); - }, - (r) async { - return FlowyResult.failure(FlowyError(msg: 'Workspace not found')); - }, - ); + return workspace.fold((l) async { + final payload = UploadFileParamsPB( + workspaceId: l.id, + localFilePath: localFilePath, + isAsync: isAsync, + ); + final result = await DocumentEventUploadFile(payload).send(); + return result; + }, (r) async { + return FlowyResult.failure(FlowyError(msg: 'Workspace not found')); + }); } /// Download a file from the cloud storage. @@ -175,7 +136,7 @@ class DocumentService { }) async { final workspace = await FolderEventReadCurrentWorkspace().send(); return workspace.fold((l) async { - final payload = DownloadFilePB( + final payload = UploadedFilePB( url: url, ); final result = await DocumentEventDownloadFile(payload).send(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart new file mode 100644 index 0000000000..af0d5081f1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_share_bloc.dart @@ -0,0 +1,107 @@ +import 'dart:io'; + +import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'document_share_bloc.freezed.dart'; + +class DocumentShareBloc extends Bloc { + DocumentShareBloc({ + required this.view, + }) : super(const DocumentShareState.initial()) { + on((event, emit) async { + await event.when( + share: (type, path) async { + if (DocumentShareType.unimplemented.contains(type)) { + Log.error('DocumentShareType $type is not implemented'); + return; + } + + emit(const DocumentShareState.loading()); + + final exporter = DocumentExporter(view); + final FlowyResult result = + await exporter.export(type.exportType).then((value) { + return value.fold( + (s) { + if (path != null) { + switch (type) { + case DocumentShareType.markdown: + return FlowyResult.success(_saveMarkdownToPath(s, path)); + case DocumentShareType.html: + return FlowyResult.success(_saveHTMLToPath(s, path)); + default: + break; + } + } + return FlowyResult.failure(FlowyError()); + }, + (f) => FlowyResult.failure(f), + ); + }); + + emit(DocumentShareState.finish(result)); + }, + ); + }); + } + + final ViewPB view; + + ExportDataPB _saveMarkdownToPath(String markdown, String path) { + File(path).writeAsStringSync(markdown); + return ExportDataPB() + ..data = markdown + ..exportType = ExportType.Markdown; + } + + ExportDataPB _saveHTMLToPath(String html, String path) { + File(path).writeAsStringSync(html); + return ExportDataPB() + ..data = html + ..exportType = ExportType.HTML; + } +} + +enum DocumentShareType { + markdown, + html, + text, + link; + + static List get unimplemented => [text, link]; + + DocumentExportType get exportType { + switch (this) { + case DocumentShareType.markdown: + return DocumentExportType.markdown; + case DocumentShareType.html: + return DocumentExportType.html; + case DocumentShareType.text: + return DocumentExportType.text; + case DocumentShareType.link: + throw UnsupportedError('DocumentShareType.link is not supported'); + } + } +} + +@freezed +class DocumentShareEvent with _$DocumentShareEvent { + const factory DocumentShareEvent.share(DocumentShareType type, String? path) = + Share; +} + +@freezed +class DocumentShareState with _$DocumentShareState { + const factory DocumentShareState.initial() = _Initial; + const factory DocumentShareState.loading() = _Loading; + const factory DocumentShareState.finish( + FlowyResult successOrFail, + ) = _Finish; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart index 7254539809..0fae90920d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_sync_bloc.dart @@ -31,7 +31,7 @@ class DocumentSyncBloc extends Bloc { emit( state.copyWith( shouldShowIndicator: - userProfile?.workspaceAuthType == AuthTypePB.Server, + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud, ), ); _syncStateListener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_validator.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_validator.dart deleted file mode 100644 index 8fb2e8008d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_validator.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; - -class DocumentValidator { - const DocumentValidator({ - required this.editorState, - required this.rules, - }); - - final EditorState editorState; - final List rules; - - Future validate(Transaction transaction) async { - // deep copy the document - final root = this.editorState.document.root.deepCopy(); - final dummyDocument = Document(root: root); - if (dummyDocument.isEmpty) { - return true; - } - - final editorState = EditorState(document: dummyDocument); - await editorState.apply(transaction); - - final iterator = NodeIterator( - document: editorState.document, - startNode: editorState.document.root, - ); - - for (final rule in rules) { - while (iterator.moveNext()) { - if (!rule.validate(iterator.current)) { - return false; - } - } - } - - return true; - } - - Future applyTransactionInDummyDocument(Transaction transaction) async { - // deep copy the document - final root = this.editorState.document.root.deepCopy(); - final dummyDocument = Document(root: root); - if (dummyDocument.isEmpty) { - return true; - } - - final editorState = EditorState(document: dummyDocument); - await editorState.apply(transaction); - - final iterator = NodeIterator( - document: editorState.document, - startNode: editorState.document.root, - ); - - for (final rule in rules) { - while (iterator.moveNext()) { - if (!rule.validate(iterator.current)) { - return false; - } - } - } - - return true; - } -} - -class DocumentRule { - const DocumentRule({ - required this.type, - }); - - final String type; - - bool validate(Node node) { - return true; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart index 2094462d6d..666dea3f00 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/editor_transaction_adapter.dart @@ -1,18 +1,26 @@ import 'dart:async'; import 'dart:convert'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/document_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' + show + EditorState, + Transaction, + Operation, + InsertOperation, + UpdateOperation, + DeleteOperation, + PathExtensions, + Node, + Path, + Delta, + composeAttributes; import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; -const kExternalTextType = 'text'; - /// Uses to adjust the data structure between the editor and the backend. /// /// The editor uses a tree structure to represent the document, while the backend uses a flat structure. @@ -26,34 +34,22 @@ class TransactionAdapter { final DocumentService documentService; final String documentId; + final bool _enableDebug = false; + Future apply(Transaction transaction, EditorState editorState) async { - if (enableDocumentInternalLog) { - Log.info( - '[TransactionAdapter] 2. apply transaction begin ${transaction.hashCode} in $hashCode', - ); - } - - await _applyInternal(transaction, editorState); - - if (enableDocumentInternalLog) { - Log.info( - '[TransactionAdapter] 3. apply transaction end ${transaction.hashCode} in $hashCode', - ); - } - } - - Future _applyInternal( - Transaction transaction, - EditorState editorState, - ) async { final stopwatch = Stopwatch()..start(); - if (enableDocumentInternalLog) { - Log.info('transaction => ${transaction.toJson()}'); + if (_enableDebug) { + Log.debug('transaction => ${transaction.toJson()}'); } - - final actions = transactionToBlockActions(transaction, editorState); - final textActions = filterTextDeltaActions(actions); - + final actions = transaction.operations + .map((op) => op.toBlockAction(editorState, documentId)) + .whereNotNull() + .expand((element) => element) + .toList(growable: false); // avoid lazy evaluation + final textActions = actions.where( + (e) => + e.textDeltaType != TextDeltaType.none && e.textDeltaPayloadPB != null, + ); final actionCostTime = stopwatch.elapsedMilliseconds; for (final textAction in textActions) { final payload = textAction.textDeltaPayloadPB!; @@ -64,10 +60,8 @@ class TransactionAdapter { textId: payload.textId, delta: payload.delta, ); - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] create external text: id: ${payload.textId} delta: ${payload.delta}', - ); + if (_enableDebug) { + Log.debug('create external text: ${payload.delta}'); } } else if (type == TextDeltaType.update) { await documentService.updateExternalText( @@ -75,66 +69,25 @@ class TransactionAdapter { textId: payload.textId, delta: payload.delta, ); - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] update external text: id: ${payload.textId} delta: ${payload.delta}', - ); + if (_enableDebug) { + Log.debug('update external text: ${payload.delta}'); } } } - - final blockActions = filterBlockActions(actions); - - for (final action in blockActions) { - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] action => ${action.toProto3Json()}', - ); - } - } - + final blockActions = + actions.map((e) => e.blockActionPB).toList(growable: false); await documentService.applyAction( documentId: documentId, actions: blockActions, ); - final elapsed = stopwatch.elapsedMilliseconds; stopwatch.stop(); - if (enableDocumentInternalLog) { - Log.info( - '[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms', + if (_enableDebug) { + Log.debug( + 'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms', ); } } - - List transactionToBlockActions( - Transaction transaction, - EditorState editorState, - ) { - return transaction.operations - .map((op) => op.toBlockAction(editorState, documentId)) - .nonNulls - .expand((element) => element) - .toList(growable: false); // avoid lazy evaluation - } - - List filterTextDeltaActions( - List actions, - ) { - return actions - .where( - (e) => - e.textDeltaType != TextDeltaType.none && - e.textDeltaPayloadPB != null, - ) - .toList(growable: false); - } - - List filterBlockActions( - List actions, - ) { - return actions.map((e) => e.blockActionPB).toList(growable: false); - } } extension BlockAction on Operation { @@ -163,32 +116,28 @@ extension on InsertOperation { Path currentPath = path; final List actions = []; for (final node in nodes) { - if (node.type == AiWriterBlockKeys.type) { - continue; - } - final parentId = node.parent?.id ?? editorState.getNodeAtPath(currentPath.parent)?.id ?? ''; - assert(parentId.isNotEmpty); - - String prevId = ''; + var prevId = previousNode?.id; // if the node is the first child of the parent, then its prevId should be empty. final isFirstChild = currentPath.previous.equals(currentPath); - if (!isFirstChild) { - prevId = previousNode?.id ?? - editorState.getNodeAtPath(currentPath.previous)?.id ?? - ''; + prevId ??= editorState.getNodeAtPath(currentPath.previous)?.id ?? ''; + } + prevId ??= ''; + assert(parentId.isNotEmpty); + if (isFirstChild) { + prevId = ''; + } else { assert(prevId.isNotEmpty && prevId != node.id); } // create the external text if the node contains the delta in its data. final delta = node.delta; TextDeltaPayloadPB? textDeltaPayloadPB; - String? textId; if (delta != null) { - textId = nanoid(6); + final textId = nanoid(6); textDeltaPayloadPB = TextDeltaPayloadPB( documentId: documentId, @@ -199,18 +148,13 @@ extension on InsertOperation { // sync the text id to the node node.externalValues = ExternalValues( externalId: textId, - externalType: kExternalTextType, + externalType: 'text', ); } // remove the delta from the data when the incremental update is stable. final payload = BlockActionPayloadPB() - ..block = node.toBlock( - childrenId: nanoid(6), - externalId: textId, - externalType: textId != null ? kExternalTextType : null, - attributes: {...node.attributes}..remove(blockComponentDelta), - ) + ..block = node.toBlock(childrenId: nanoid(6)) ..parentId = parentId ..prevId = prevId; @@ -272,17 +216,18 @@ extension on UpdateOperation { assert(parentId.isNotEmpty); // create the external text if the node contains the delta in its data. - final prevDelta = oldAttributes[blockComponentDelta]; - final delta = attributes[blockComponentDelta]; - - final composedAttributes = composeAttributes(oldAttributes, attributes); - final composedDelta = composedAttributes?[blockComponentDelta]; - composedAttributes?.remove(blockComponentDelta); + final prevDelta = oldAttributes['delta']; + final delta = attributes['delta']; + final diff = prevDelta != null && delta != null + ? Delta.fromJson(prevDelta).diff( + Delta.fromJson(delta), + ) + : null; final payload = BlockActionPayloadPB() ..block = node.toBlock( parentId: parentId, - attributes: composedAttributes, + attributes: composeAttributes(oldAttributes, attributes), ) ..parentId = parentId; final blockActionPB = BlockActionPB() @@ -293,32 +238,19 @@ extension on UpdateOperation { if (textId == null || textId.isEmpty) { // to be compatible with the old version, we create a new text id if the text id is empty. final textId = nanoid(6); - final textDelta = composedDelta ?? delta ?? prevDelta; - final correctedTextDelta = - textDelta != null ? _correctAttributes(textDelta) : null; - - final textDeltaPayloadPB = correctedTextDelta == null + final textDeltaPayloadPB = delta == null ? null : TextDeltaPayloadPB( documentId: documentId, textId: textId, - delta: jsonEncode(correctedTextDelta), + delta: jsonEncode(delta), ); node.externalValues = ExternalValues( externalId: textId, - externalType: kExternalTextType, + externalType: 'text', ); - if (enableDocumentInternalLog) { - Log.info('create text delta: $textDeltaPayloadPB'); - } - - // update the external text id and external type to the block - blockActionPB.payload.block - ..externalId = textId - ..externalType = kExternalTextType; - actions.add( BlockActionWrapper( blockActionPB: blockActionPB, @@ -327,31 +259,14 @@ extension on UpdateOperation { ), ); } else { - final diff = prevDelta != null && delta != null - ? Delta.fromJson(prevDelta).diff( - Delta.fromJson(delta), - ) - : null; - - final correctedDiff = diff != null ? _correctDelta(diff) : null; - - final textDeltaPayloadPB = correctedDiff == null + final textDeltaPayloadPB = delta == null ? null : TextDeltaPayloadPB( documentId: documentId, textId: textId, - delta: jsonEncode(correctedDiff), + delta: jsonEncode(diff), ); - if (enableDocumentInternalLog) { - Log.info('update text delta: $textDeltaPayloadPB'); - } - - // update the external text id and external type to the block - blockActionPB.payload.block - ..externalId = textId - ..externalType = kExternalTextType; - actions.add( BlockActionWrapper( blockActionPB: blockActionPB, @@ -363,58 +278,6 @@ extension on UpdateOperation { return actions; } - - // if the value in Delta's attributes is false, we should set the value to null instead. - // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected. - List? _correctDelta(Delta delta) { - // if the value in diff's attributes is false, we should set the value to null instead. - // because on Yjs, canceling format must use the null value. If we use false, the update will be rejected. - final correctedOps = delta.map((op) { - final attributes = op.attributes?.map( - (key, value) => MapEntry( - key, - // if the value is false, we should set the value to null instead. - value == false ? null : value, - ), - ); - - if (attributes != null) { - if (op is TextRetain) { - return TextRetain(op.length, attributes: attributes); - } else if (op is TextInsert) { - return TextInsert(op.text, attributes: attributes); - } - // ignore the other operations that do not contain attributes. - } - - return op; - }); - - return correctedOps.toList(growable: false); - } - - // Refer to [_correctDelta] for more details. - List> _correctAttributes( - List> attributes, - ) { - final correctedAttributes = attributes.map((attribute) { - return attribute.map((key, value) { - if (value is bool) { - return MapEntry(key, value == false ? null : value); - } else if (value is Map) { - return MapEntry( - key, - value.map((key, value) { - return MapEntry(key, value == false ? null : value); - }), - ); - } - return MapEntry(key, value); - }); - }).toList(growable: false); - - return correctedAttributes; - } } extension on DeleteOperation { diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart b/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart index a6497bf6de..78f30608d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/prelude.dart @@ -1,3 +1,3 @@ -export '../../shared/share/share_bloc.dart'; export 'document_bloc.dart'; export 'document_service.dart'; +export 'document_share_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 4ebc6f1b47..ef11ef3403 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,17 +1,14 @@ -library; +library document_plugin; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; -import 'package:appflowy/plugins/shared/share/share_button.dart'; +import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart'; @@ -53,7 +50,6 @@ class DocumentPlugin extends Plugin { required ViewPB view, required PluginType pluginType, this.initialSelection, - this.initialBlockId, }) : notifier = ViewPluginNotifier(view: view) { _pluginType = pluginType; } @@ -64,18 +60,13 @@ class DocumentPlugin extends Plugin { @override final ViewPluginNotifier notifier; - // the initial selection of the document final Selection? initialSelection; - // the initial block id of the document - final String? initialBlockId; - @override PluginWidgetBuilder get widgetBuilder => DocumentPluginWidgetBuilder( bloc: _viewInfoBloc, notifier: notifier, initialSelection: initialSelection, - initialBlockId: initialBlockId, ); @override @@ -103,16 +94,13 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder required this.bloc, required this.notifier, this.initialSelection, - this.initialBlockId, }); final ViewInfoBloc bloc; final ViewPluginNotifier notifier; - ViewPB get view => notifier.view; int? deletedViewIndex; final Selection? initialSelection; - final String? initialBlockId; @override EdgeInsets get contentPadding => EdgeInsets.zero; @@ -121,7 +109,6 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) { notifier.isDeleted.addListener(() { final deletedView = notifier.isDeleted.value; @@ -130,15 +117,6 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } }); - final fixedTitle = data?[MobileDocumentScreen.viewFixedTitle]; - final blockId = initialBlockId ?? data?[MobileDocumentScreen.viewBlockId]; - final tabs = data?[MobileDocumentScreen.viewSelectTabs] ?? - const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ]; - return BlocProvider.value( value: bloc, child: BlocBuilder( @@ -147,23 +125,16 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder view: view, onDeleted: () => context.onDeleted?.call(view, deletedViewIndex), initialSelection: initialSelection, - initialBlockId: blockId, - fixedTitle: fixedTitle, - tabs: tabs, ), ), ); } - @override - String? get viewName => notifier.view.nameOrDefault; - @override Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => - ViewTabBarItem(view: notifier.view, shortForm: shortForm); + Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @override Widget? get rightBarItem { @@ -183,7 +154,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder const HSpace(16), ] : [const HSpace(8)], - ShareButton( + DocumentShareButton( key: ValueKey('share_button_${view.id}'), view: view, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 8716bb7ae2..097a23e5c9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,50 +1,36 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_handler.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, required this.view, required this.onDeleted, - required this.tabs, this.initialSelection, - this.initialBlockId, - this.fixedTitle, }); final ViewPB view; final VoidCallback onDeleted; final Selection? initialSelection; - final String? initialBlockId; - final String? fixedTitle; - final List tabs; @override State createState() => _DocumentPageState(); @@ -53,7 +39,6 @@ class DocumentPage extends StatefulWidget { class _DocumentPageState extends State with WidgetsBindingObserver { EditorState? editorState; - Selection? initialSelection; late final documentBloc = DocumentBloc(documentId: widget.view.id) ..add(const DocumentEvent.initial()); @@ -61,13 +46,14 @@ class _DocumentPageState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + EditorNotification.addListener(_onEditorNotification); } @override void dispose() { + EditorNotification.removeListener(_onEditorNotification); WidgetsBinding.instance.removeObserver(this); documentBloc.close(); - super.dispose(); } @@ -87,178 +73,99 @@ class _DocumentPageState extends State providers: [ BlocProvider.value(value: getIt()), BlocProvider.value(value: documentBloc), - BlocProvider.value( - value: ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - BlocProvider( - create: (context) => - ViewBloc(view: widget.view)..add(const ViewEvent.initial()), - lazy: false, - ), ], - child: BlocConsumer( - listenWhen: (prev, curr) => curr.isLocked != prev.isLocked, - listener: (context, lockStatusState) { - if (lockStatusState.isLoadingLockStatus) { - return; + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); } - editorState?.editable = !lockStatusState.isLocked; - }, - builder: (context, lockStatusState) { - return BlocBuilder( - buildWhen: shouldRebuildDocument, - builder: (context, state) { - if (state.isLoading) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); - } - final editorState = state.editorState; - this.editorState = editorState; - final error = state.error; - if (error != null || editorState == null) { - Log.error(error); - return Center(child: AppFlowyErrorPage(error: error)); - } + final editorState = state.editorState; + this.editorState = editorState; + final error = state.error; + if (error != null || editorState == null) { + Log.error(error); + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), + ); + } - if (state.forceClose) { - widget.onDeleted(); - return const SizedBox.shrink(); - } + if (state.forceClose) { + widget.onDeleted(); + return const SizedBox.shrink(); + } - return MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) => - editorState.editable = !state.isLocked, - ), - BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: onNotificationAction, - ), - ], - child: AiWriterScrollWrapper( - viewId: widget.view.id, - editorState: editorState, - child: buildEditorPage(context, state), - ), - ); - }, + return BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: _onNotificationAction, + child: _buildEditorPage(context, state), ); }, ), ); } - Widget buildEditorPage( - BuildContext context, - DocumentState state, - ) { - final editorState = state.editorState; - if (editorState == null) { - return const SizedBox.shrink(); - } - - final width = context.read().state.width; - - // avoid the initial selection calculation change when the editorState is not changed - initialSelection ??= _calculateInitialSelection(editorState); - + Widget _buildEditorPage(BuildContext context, DocumentState state) { final Widget child; - if (UniversalPlatform.isMobile) { + + if (PlatformExtension.isMobile) { child = BlocBuilder( - builder: (context, styleState) => AppFlowyEditorPage( - editorState: editorState, - // if the view's name is empty, focus on the title - autoFocus: widget.view.name.isEmpty ? false : null, - styleCustomizer: EditorStyleCustomizer( - context: context, - width: width, - padding: EditorStyleCustomizer.documentPadding, - editorState: editorState, - ), - header: buildCoverAndIcon(context, state), - initialSelection: initialSelection, - ), + builder: (context, styleState) { + return AppFlowyEditorPage( + editorState: state.editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + // the 44 is the width of the left action list + padding: EditorStyleCustomizer.documentPadding, + ), + header: _buildCoverAndIcon(context, state), + initialSelection: widget.initialSelection, + ); + }, ); } else { - child = EditorDropHandler( - viewId: widget.view.id, - editorState: editorState, - isLocalMode: context.read().isLocalMode, - child: AppFlowyEditorPage( - editorState: editorState, - // if the view's name is empty, focus on the title - autoFocus: widget.view.name.isEmpty ? false : null, - styleCustomizer: EditorStyleCustomizer( - context: context, - width: width, - padding: EditorStyleCustomizer.documentPadding, - editorState: editorState, - ), - header: buildCoverAndIcon(context, state), - initialSelection: initialSelection, - placeholderText: (node) => - node.type == ParagraphBlockKeys.type && !node.isInTable - ? LocaleKeys.editor_slashPlaceHolder.tr() - : '', + child = AppFlowyEditorPage( + editorState: state.editorState!, + styleCustomizer: EditorStyleCustomizer( + context: context, + // the 44 is the width of the left action list + padding: EditorStyleCustomizer.documentPadding, ), + header: _buildCoverAndIcon(context, state), + initialSelection: widget.initialSelection, ); } - return Provider( - create: (_) { - final context = SharedEditorContext(); - final children = editorState.document.root.children; - final firstDelta = children.firstOrNull?.delta; - final isEmptyDocument = - children.length == 1 && (firstDelta == null || firstDelta.isEmpty); - if (widget.view.name.isEmpty && isEmptyDocument) { - context.requestCoverTitleFocus = true; - } - return context; - }, - dispose: (buildContext, editorContext) => editorContext.dispose(), - child: EditorTransactionService( - viewId: widget.view.id, - editorState: state.editorState!, - child: Column( - children: [ - // the banner only shows on desktop - if (state.isDeleted && UniversalPlatform.isDesktop) - buildBanner(context), - Expanded(child: child), - ], - ), - ), + return Column( + children: [ + if (state.isDeleted) _buildBanner(context), + Expanded(child: child), + ], ); } - Widget buildBanner(BuildContext context) { + Widget _buildBanner(BuildContext context) { return DocumentBanner( - viewName: widget.view.nameOrDefault, - onRestore: () => - context.read().add(const DocumentEvent.restorePage()), + onRestore: () => context.read().add( + const DocumentEvent.restorePage(), + ), onDelete: () => context .read() .add(const DocumentEvent.deletePermanently()), ); } - Widget buildCoverAndIcon(BuildContext context, DocumentState state) { + Widget _buildCoverAndIcon(BuildContext context, DocumentState state) { final editorState = state.editorState; final userProfilePB = state.userProfilePB; if (editorState == null || userProfilePB == null) { return const SizedBox.shrink(); } - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return DocumentImmersiveCover( - fixedTitle: widget.fixedTitle, view: widget.view, - tabs: widget.tabs, userProfilePB: userProfilePB, ); } @@ -266,116 +173,43 @@ class _DocumentPageState extends State final page = editorState.document.root; return DocumentCoverWidget( node: page, - tabs: widget.tabs, editorState: editorState, view: widget.view, onIconChanged: (icon) async => ViewBackendService.updateViewIcon( - view: widget.view, + viewId: widget.view.id, viewIcon: icon, ), ); } - void onNotificationAction( - BuildContext context, - ActionNavigationState state, - ) { - final action = state.action; - if (action == null || - action.type != ActionType.jumpToBlock || - action.objectId != widget.view.id) { - return; - } - - final editorState = context.read().state.editorState; + void _onEditorNotification(EditorNotificationType type) { + final editorState = this.editorState; if (editorState == null) { return; } - - final Path? path = _getPathFromAction(action, editorState); - if (path != null) { - editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); + if (type == EditorNotificationType.undo) { + undoCommand.execute(editorState); + } else if (type == EditorNotificationType.redo) { + redoCommand.execute(editorState); + } else if (type == EditorNotificationType.exitEditing && + editorState.selection != null) { + editorState.selection = null; } } - Path? _getPathFromAction(NavigationAction action, EditorState editorState) { - Path? path = action.arguments?[ActionArgumentKeys.nodePath]; - if (path == null || path.isEmpty) { - final blockId = action.arguments?[ActionArgumentKeys.blockId]; - if (blockId != null) { - path = _findNodePathByBlockId(editorState, blockId); - } - } - return path; - } + void _onNotificationAction( + BuildContext context, + ActionNavigationState state, + ) { + if (state.action != null && state.action!.type == ActionType.jumpToBlock) { + final path = state.action?.arguments?[ActionArgumentKeys.nodePath]; - Path? _findNodePathByBlockId(EditorState editorState, String blockId) { - final document = editorState.document; - final startNode = document.root.children.firstOrNull; - if (startNode == null) { - return null; - } - - final nodeIterator = NodeIterator(document: document, startNode: startNode); - while (nodeIterator.moveNext()) { - final node = nodeIterator.current; - if (node.id == blockId) { - return node.path; - } - } - - return null; - } - - bool shouldRebuildDocument(DocumentState previous, DocumentState current) { - // only rebuild the document page when the below fields are changed - // this is to prevent unnecessary rebuilds - // - // If you confirm the newly added fields should be rebuilt, please update - // this function. - if (previous.editorState != current.editorState) { - return true; - } - - if (previous.forceClose != current.forceClose || - previous.isDeleted != current.isDeleted) { - return true; - } - - if (previous.userProfilePB != current.userProfilePB) { - return true; - } - - if (previous.isLoading != current.isLoading || - previous.error != current.error) { - return true; - } - - return false; - } - - Selection? _calculateInitialSelection(EditorState editorState) { - if (widget.initialSelection != null) { - return widget.initialSelection; - } - - if (widget.initialBlockId != null) { - final path = _findNodePathByBlockId(editorState, widget.initialBlockId!); - if (path != null) { - editorState.selectionType = SelectionType.block; - editorState.selectionExtraInfo = { - selectionExtraInfoDoNotAttachTextService: true, - }; - return Selection.collapsed( - Position( - path: path, - ), + final editorState = context.read().state.editorState; + if (editorState != null && widget.view.id == state.action?.objectId) { + editorState.updateSelectionWithReason( + Selection.collapsed(Position(path: [path])), ); } } - - return null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart index 856763e9b9..e5fd6b6b8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart @@ -1,21 +1,18 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/buttons/base_styled_button.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; class DocumentBanner extends StatelessWidget { const DocumentBanner({ super.key, - required this.viewName, required this.onRestore, required this.onDelete, }); - final String viewName; final void Function() onRestore; final void Function() onDelete; @@ -61,16 +58,7 @@ class DocumentBanner extends StatelessWidget { highlightColor: Theme.of(context).colorScheme.error, outlineColor: colorScheme.tertiaryContainer, borderRadius: Corners.s8Border, - onPressed: () => showConfirmDeletionDialog( - context: context, - name: viewName.trim().isEmpty - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : viewName, - description: LocaleKeys - .deletePagePrompt_deletePermanentDescription - .tr(), - onConfirm: onDelete, - ), + onPressed: onDelete, child: FlowyText.medium( LocaleKeys.deletePagePrompt_deletePermanent.tr(), color: colorScheme.tertiary, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart index 1be5a41d81..8fa15af8b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart @@ -46,7 +46,7 @@ class CollaboratorAvatarStack extends StatelessWidget { width: width, child: WidgetStack( positions: settings, - buildInfoWidget: (value, _) => plusWidgetBuilder(value, border), + buildInfoWidget: (value) => plusWidgetBuilder(value, border), stackedWidgets: avatars .map( (avatar) => CircleAvatar( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart deleted file mode 100644 index eaee989bbc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/compact_mode_event.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:event_bus/event_bus.dart'; - -EventBus compactModeEventBus = EventBus(); - -class CompactModeEvent { - CompactModeEvent({ - required this.id, - required this.enable, - }); - - final String id; - final bool enable; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart index d4a6815e32..1e96e5648b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart @@ -1,13 +1,14 @@ import 'package:appflowy/plugins/document/application/document_awareness_metadata.dart'; import 'package:appflowy/plugins/document/application/document_collaborators_bloc.dart'; import 'package:appflowy/plugins/document/presentation/collaborator_avater_stack.dart'; -import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:avatar_stack/avatar_stack.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'package:string_validator/string_validator.dart'; class DocumentCollaborators extends StatelessWidget { const DocumentCollaborators({ @@ -93,14 +94,39 @@ class _UserAvatar extends StatelessWidget { @override Widget build(BuildContext context) { + final Widget child; + if (isURL(user.userAvatar)) { + child = _buildUrlAvatar(context); + } else { + child = _buildNameAvatar(context); + } return FlowyTooltip( message: user.userName, - child: IgnorePointer( - child: UserAvatar( - iconUrl: user.userAvatar, - name: user.userName, - size: 30.0, - fontSize: fontSize ?? (UniversalPlatform.isMobile ? 14 : 12), + child: child, + ); + } + + Widget _buildNameAvatar(BuildContext context) { + return CircleAvatar( + backgroundColor: user.cursorColor.tryToColor(), + child: FlowyText( + user.userName.characters.firstOrNull ?? ' ', + fontSize: fontSize, + color: Colors.black, + ), + ); + } + + Widget _buildUrlAvatar(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(width), + child: CircleAvatar( + backgroundColor: user.cursorColor.tryToColor(), + child: Image.network( + user.userAvatar, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildNameAvatar(context), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 5e7eefc24e..90c8abebdf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1,434 +1,235 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'editor_plugins/link_preview/custom_link_preview_block_component.dart'; -import 'editor_plugins/page_block/custom_page_block_component.dart'; - -/// A global configuration for the editor. -class EditorGlobalConfiguration { - /// Whether to enable the drag menu in the editor. - /// - /// Case 1, resizing the columns block in the desktop, then the drag menu will be disabled. - static ValueNotifier enableDragMenu = ValueNotifier(true); -} - -/// The node types that support slash menu. -final Set supportSlashMenuNodeTypes = { - ParagraphBlockKeys.type, - HeadingBlockKeys.type, - - // Lists - TodoListBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - QuoteBlockKeys.type, - ToggleListBlockKeys.type, - CalloutBlockKeys.type, - - // Simple table - SimpleTableBlockKeys.type, - SimpleTableRowBlockKeys.type, - SimpleTableCellBlockKeys.type, - - // Columns - SimpleColumnsBlockKeys.type, - SimpleColumnBlockKeys.type, -}; - -/// Build the block component builders. -/// -/// Every block type should have a corresponding builder in the map. -/// Otherwise, the errorBlockComponentBuilder will be rendered. -/// -/// Additional, you can define the block render options in the builder -/// - customize the block option actions. (... button and + button) -/// - customize the block component configuration. (padding, placeholder, etc.) -/// - customize the block icon. (bulleted list, numbered list, todo list) -/// - customize the hover menu. (show the menu at the top-right corner of the block) -Map buildBlockComponentBuilders({ +Map getEditorBuilderMap({ required BuildContext context, required EditorState editorState, required EditorStyleCustomizer styleCustomizer, - SlashMenuItemsBuilder? slashMenuItemsBuilder, + List? slashMenuItems, bool editable = true, ShowPlaceholder? showParagraphPlaceholder, String Function(Node)? placeholderText, - EdgeInsets? customHeadingPadding, - bool alwaysDistributeSimpleTableColumnWidths = false, }) { - final configuration = _buildDefaultConfiguration(context); - final builders = _buildBlockComponentBuilderMap( - context, - configuration: configuration, - editorState: editorState, - styleCustomizer: styleCustomizer, - showParagraphPlaceholder: showParagraphPlaceholder, - placeholderText: placeholderText, - alwaysDistributeSimpleTableColumnWidths: - alwaysDistributeSimpleTableColumnWidths, - ); + final standardActions = [OptionAction.delete, OptionAction.duplicate]; - // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience. - if (editable) { - _customBlockOptionActions( - context, - builders: builders, - editorState: editorState, - styleCustomizer: styleCustomizer, - slashMenuItemsBuilder: slashMenuItemsBuilder, - ); - } - - return builders; -} - -BlockComponentConfiguration _buildDefaultConfiguration(BuildContext context) { + final calloutBGColor = AFThemeExtension.of(context).calloutBGColor; final configuration = BlockComponentConfiguration( - padding: (node) { - if (UniversalPlatform.isMobile) { + // use EdgeInsets.zero to remove the default padding. + padding: (_) { + if (PlatformExtension.isMobile) { final pageStyle = context.read().state; final factor = pageStyle.fontLayout.factor; - final top = pageStyle.lineHeightLayout.padding * factor; - EdgeInsets edgeInsets = EdgeInsets.only(top: top); - // only add padding for the top level node, otherwise the nested node will have extra padding - if (node.path.length == 1) { - if (node.type != SimpleTableBlockKeys.type) { - // do not add padding for the simple table to allow it overflow - edgeInsets = edgeInsets.copyWith( - left: EditorStyleCustomizer.nodeHorizontalPadding, - ); - } - edgeInsets = edgeInsets.copyWith( - right: EditorStyleCustomizer.nodeHorizontalPadding, - ); - } - return edgeInsets; + final padding = pageStyle.lineHeightLayout.padding * factor; + return EdgeInsets.only(top: padding); } return const EdgeInsets.symmetric(vertical: 5.0); }, - indentPadding: (node, textDirection) { - double padding = 26.0; - - // only add indent padding for the top level node to align the children - if (UniversalPlatform.isMobile && node.level == 1) { - padding += EditorStyleCustomizer.nodeHorizontalPadding - 4; - } - - // in the quote block, we reduce the indent padding for the first level block. - // So we have to add more padding for the second level to avoid the drag menu overlay the quote icon. - if (node.parent?.type == QuoteBlockKeys.type && - UniversalPlatform.isDesktop) { - padding += 22; - } - - return textDirection == TextDirection.ltr - ? EdgeInsets.only(left: padding) - : EdgeInsets.only(right: padding); - }, + indentPadding: (node, textDirection) => textDirection == TextDirection.ltr + ? const EdgeInsets.only(left: 26.0) + : const EdgeInsets.only(right: 26.0), ); - return configuration; -} -/// Build the option actions for the block component. -/// -/// Notes: different block type may have different option actions. -/// All the block types have the delete and duplicate options. -List _buildOptionActions(BuildContext context, String type) { - final standardActions = [ - OptionAction.delete, - OptionAction.duplicate, - ]; - - // filter out the copy link to block option if in local mode - if (context.read()?.isLocalMode != true) { - standardActions.add(OptionAction.copyLinkToBlock); - } - - standardActions.add(OptionAction.turnInto); - - if (SimpleTableBlockKeys.type == type) { - standardActions.addAll([ - OptionAction.divider, - OptionAction.setToPageWidth, - OptionAction.distributeColumnsEvenly, - ]); - } - - if (EditorOptionActionType.color.supportTypes.contains(type)) { - standardActions.addAll([OptionAction.divider, OptionAction.color]); - } - - if (EditorOptionActionType.align.supportTypes.contains(type)) { - standardActions.addAll([OptionAction.divider, OptionAction.align]); - } - - if (EditorOptionActionType.depth.supportTypes.contains(type)) { - standardActions.addAll([OptionAction.divider, OptionAction.depth]); - } - - return standardActions; -} - -void _customBlockOptionActions( - BuildContext context, { - required Map builders, - required EditorState editorState, - required EditorStyleCustomizer styleCustomizer, - SlashMenuItemsBuilder? slashMenuItemsBuilder, -}) { - for (final entry in builders.entries) { - if (entry.key == PageBlockKeys.type) { - continue; - } - final builder = entry.value; - final actions = _buildOptionActions(context, entry.key); - - if (UniversalPlatform.isDesktop) { - builder.showActions = (node) { - final parentTableNode = node.parentTableNode; - // disable the option action button in table cell to avoid the misalignment issue - if (node.type != SimpleTableBlockKeys.type && parentTableNode != null) { - return false; - } - return true; - }; - - builder.configuration = builder.configuration.copyWith( - blockSelectionAreaMargin: (_) => const EdgeInsets.symmetric( - vertical: 1, - ), - ); - - builder.actionTrailingBuilder = (context, state) { - if (context.node.parent?.type == QuoteBlockKeys.type) { - return const SizedBox( - width: 24, - height: 24, - ); - } - return const SizedBox.shrink(); - }; - - builder.actionBuilder = (context, state) { - double top = builder.configuration.padding(context.node).top; - final type = context.node.type; - final level = context.node.attributes[HeadingBlockKeys.level] ?? 0; - if ((type == HeadingBlockKeys.type || - type == ToggleListBlockKeys.type) && - level > 0) { - final offset = [13.0, 11.0, 8.0, 6.0, 4.0, 2.0]; - top += offset[level - 1]; - } else if (type == SimpleTableBlockKeys.type) { - top += 8.0; - } else { - top += 2.0; - } - if (overflowTypes.contains(type)) { - top = top / 2; - } - return ValueListenableBuilder( - valueListenable: EditorGlobalConfiguration.enableDragMenu, - builder: (_, enableDragMenu, child) { - return ValueListenableBuilder( - valueListenable: editorState.editableNotifier, - builder: (_, editable, child) { - return IgnorePointer( - ignoring: !editable, - child: Opacity( - opacity: editable && enableDragMenu ? 1.0 : 0.0, - child: Padding( - padding: EdgeInsets.only(top: top), - child: BlockActionList( - blockComponentContext: context, - blockComponentState: state, - editorState: editorState, - blockComponentBuilder: builders, - actions: actions, - showSlashMenu: slashMenuItemsBuilder != null - ? () => customAppFlowySlashCommand( - itemsBuilder: slashMenuItemsBuilder, - shouldInsertSlash: false, - deleteKeywordsByDefault: true, - style: styleCustomizer - .selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: - supportSlashMenuNodeTypes, - ).handler.call(editorState) - : () {}, - ), - ), - ), - ); - }, - ); - }, - ); - }; - } - } -} - -Map _buildBlockComponentBuilderMap( - BuildContext context, { - required BlockComponentConfiguration configuration, - required EditorState editorState, - required EditorStyleCustomizer styleCustomizer, - ShowPlaceholder? showParagraphPlaceholder, - String Function(Node)? placeholderText, - EdgeInsets? customHeadingPadding, - bool alwaysDistributeSimpleTableColumnWidths = false, -}) { final customBlockComponentBuilderMap = { - PageBlockKeys.type: CustomPageBlockComponentBuilder(), - ParagraphBlockKeys.type: _buildParagraphBlockComponentBuilder( - context, - configuration, - showParagraphPlaceholder, - placeholderText, + PageBlockKeys.type: PageBlockComponentBuilder(), + ParagraphBlockKeys.type: ParagraphBlockComponentBuilder( + configuration: configuration.copyWith(placeholderText: placeholderText), + showPlaceholder: showParagraphPlaceholder, ), - TodoListBlockKeys.type: _buildTodoListBlockComponentBuilder( - context, - configuration, + TodoListBlockKeys.type: TodoListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(), + ), + iconBuilder: (_, node, onCheck) => + TodoListIcon(node: node, onCheck: onCheck), + toggleChildrenTriggers: [ + LogicalKeyboardKey.shift, + LogicalKeyboardKey.shiftLeft, + LogicalKeyboardKey.shiftRight, + ], ), - BulletedListBlockKeys.type: _buildBulletedListBlockComponentBuilder( - context, - configuration, + BulletedListBlockKeys.type: BulletedListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(), + ), + iconBuilder: (_, node) => BulletedListIcon(node: node), ), - NumberedListBlockKeys.type: _buildNumberedListBlockComponentBuilder( - context, - configuration, + NumberedListBlockKeys.type: NumberedListBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(), + ), + iconBuilder: (_, node, textDirection) => + NumberedListIcon(node: node, textDirection: textDirection), ), - QuoteBlockKeys.type: _buildQuoteBlockComponentBuilder( - context, - configuration, + QuoteBlockKeys.type: QuoteBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(), + ), ), - HeadingBlockKeys.type: _buildHeadingBlockComponentBuilder( - context, - configuration, - styleCustomizer, - customHeadingPadding, + HeadingBlockKeys.type: HeadingBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (node) { + if (PlatformExtension.isMobile) { + final pageStyle = context.read().state; + final factor = pageStyle.fontLayout.factor; + final headingPaddings = pageStyle.lineHeightLayout.headingPaddings + .map((e) => e * factor); + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return EdgeInsets.only(top: headingPaddings.elementAt(level - 1)); + } + + return const EdgeInsets.only(top: 12.0, bottom: 4.0); + }, + placeholderText: (node) { + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return LocaleKeys.blockPlaceholders_heading.tr( + args: [level.toString()], + ); + }, + ), + textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level), ), - ImageBlockKeys.type: _buildCustomImageBlockComponentBuilder( - context, - configuration, + ImageBlockKeys.type: CustomImageBlockComponentBuilder( + configuration: configuration, + showMenu: true, + menuBuilder: (Node node, CustomImageBlockComponentState state) => + Positioned( + top: 10, + right: 10, + child: ImageMenu(node: node, state: state), + ), ), - MultiImageBlockKeys.type: _buildMultiImageBlockComponentBuilder( - context, - configuration, + TableBlockKeys.type: TableBlockComponentBuilder( + menuBuilder: (node, editorState, position, dir, onBuild, onClose) => + TableMenu( + node: node, + editorState: editorState, + position: position, + dir: dir, + onBuild: onBuild, + onClose: onClose, + ), ), - TableBlockKeys.type: _buildTableBlockComponentBuilder( - context, - configuration, + TableCellBlockKeys.type: TableCellBlockComponentBuilder( + colorBuilder: (context, node) { + final String colorString = + node.attributes[TableCellBlockKeys.colBackgroundColor] ?? + node.attributes[TableCellBlockKeys.rowBackgroundColor] ?? + ''; + if (colorString.isEmpty) { + return null; + } + return buildEditorCustomizedColor(context, node, colorString); + }, + menuBuilder: (node, editorState, position, dir, onBuild, onClose) => + TableMenu( + node: node, + editorState: editorState, + position: position, + dir: dir, + onBuild: onBuild, + onClose: onClose, + ), ), - TableCellBlockKeys.type: _buildTableCellBlockComponentBuilder( - context, - configuration, + DatabaseBlockKeys.gridType: DatabaseViewBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 10), + ), ), - DatabaseBlockKeys.gridType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, + DatabaseBlockKeys.boardType: DatabaseViewBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 10), + ), ), - DatabaseBlockKeys.boardType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, + DatabaseBlockKeys.calendarType: DatabaseViewBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 10), + ), ), - DatabaseBlockKeys.calendarType: _buildDatabaseViewBlockComponentBuilder( - context, - configuration, + CalloutBlockKeys.type: CalloutBlockComponentBuilder( + configuration: configuration.copyWith( + textStyle: (_) => styleCustomizer.calloutBlockStyleBuilder(), + placeholderTextStyle: (_) => styleCustomizer.calloutBlockStyleBuilder(), + ), + defaultColor: calloutBGColor, ), - CalloutBlockKeys.type: _buildCalloutBlockComponentBuilder( - context, - configuration, + DividerBlockKeys.type: DividerBlockComponentBuilder( + configuration: configuration, + height: 28.0, + wrapper: (_, node, child) => MobileBlockActionButtons( + showThreeDots: false, + node: node, + editorState: editorState, + child: child, + ), ), - DividerBlockKeys.type: _buildDividerBlockComponentBuilder( - context, - configuration, - editorState, + MathEquationBlockKeys.type: MathEquationBlockComponentBuilder( + configuration: configuration, ), - MathEquationBlockKeys.type: _buildMathEquationBlockComponentBuilder( - context, - configuration, + CodeBlockKeys.type: CodeBlockComponentBuilder( + configuration: configuration.copyWith( + textStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), + placeholderTextStyle: (_) => styleCustomizer.codeBlockStyleBuilder(), + ), + styleBuilder: () => CodeBlockStyle( + backgroundColor: AFThemeExtension.of(context).calloutBGColor, + foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155), + ), + padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34), + languagePickerBuilder: codeBlockLanguagePickerBuilder, + copyButtonBuilder: codeBlockCopyBuilder, ), - CodeBlockKeys.type: _buildCodeBlockComponentBuilder( - context, - configuration, - styleCustomizer, + AutoCompletionBlockKeys.type: AutoCompletionBlockComponentBuilder(), + SmartEditBlockKeys.type: SmartEditBlockComponentBuilder(), + ToggleListBlockKeys.type: ToggleListBlockComponentBuilder( + configuration: configuration, ), - AiWriterBlockKeys.type: _buildAIWriterBlockComponentBuilder( - context, - configuration, + OutlineBlockKeys.type: OutlineBlockComponentBuilder( + configuration: configuration.copyWith( + placeholderTextStyle: (_) => + styleCustomizer.outlineBlockPlaceholderStyleBuilder(), + padding: (_) => const EdgeInsets.only(top: 12.0, bottom: 4.0), + ), ), - ToggleListBlockKeys.type: _buildToggleListBlockComponentBuilder( - context, - configuration, - styleCustomizer, - customHeadingPadding, - ), - OutlineBlockKeys.type: _buildOutlineBlockComponentBuilder( - context, - configuration, - styleCustomizer, - ), - LinkPreviewBlockKeys.type: _buildLinkPreviewBlockComponentBuilder( - context, - configuration, - ), - // Flutter doesn't support the video widget, so we forward the video block to the link preview block - VideoBlockKeys.type: _buildLinkPreviewBlockComponentBuilder( - context, - configuration, - ), - FileBlockKeys.type: _buildFileBlockComponentBuilder( - context, - configuration, - ), - SubPageBlockKeys.type: _buildSubPageBlockComponentBuilder( - context, - configuration, - styleCustomizer: styleCustomizer, + LinkPreviewBlockKeys.type: LinkPreviewBlockComponentBuilder( + configuration: configuration.copyWith( + padding: (_) => const EdgeInsets.symmetric(vertical: 10), + ), + cache: LinkPreviewDataCache(), + showMenu: true, + menuBuilder: (context, node, state) => Positioned( + top: 10, + right: 0, + child: LinkPreviewMenu(node: node, state: state), + ), + builder: (_, node, url, title, description, imageUrl) => + CustomLinkPreviewWidget( + node: node, + url: url, + title: title, + description: description, + imageUrl: imageUrl, + ), ), errorBlockComponentBuilderKey: ErrorBlockComponentBuilder( configuration: configuration, ), - SimpleTableBlockKeys.type: _buildSimpleTableBlockComponentBuilder( - context, - configuration, - alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths, - ), - SimpleTableRowBlockKeys.type: _buildSimpleTableRowBlockComponentBuilder( - context, - configuration, - alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths, - ), - SimpleTableCellBlockKeys.type: _buildSimpleTableCellBlockComponentBuilder( - context, - configuration, - alwaysDistributeColumnWidths: alwaysDistributeSimpleTableColumnWidths, - ), - SimpleColumnsBlockKeys.type: _buildSimpleColumnsBlockComponentBuilder( - context, - configuration, - ), - SimpleColumnBlockKeys.type: _buildSimpleColumnBlockComponentBuilder( - context, - configuration, - ), }; final builders = { @@ -436,663 +237,69 @@ Map _buildBlockComponentBuilderMap( ...customBlockComponentBuilderMap, }; + if (editable) { + // customize the action builder. actually, we can customize them in their own builder. Put them here just for convenience. + for (final entry in builders.entries) { + if (entry.key == PageBlockKeys.type) { + continue; + } + final builder = entry.value; + + // customize the action builder. + final supportColorBuilderTypes = [ + ParagraphBlockKeys.type, + HeadingBlockKeys.type, + BulletedListBlockKeys.type, + NumberedListBlockKeys.type, + QuoteBlockKeys.type, + TodoListBlockKeys.type, + CalloutBlockKeys.type, + OutlineBlockKeys.type, + ToggleListBlockKeys.type, + ]; + + final supportAlignBuilderType = [ImageBlockKeys.type]; + final supportDepthBuilderType = [OutlineBlockKeys.type]; + final colorAction = [OptionAction.divider, OptionAction.color]; + final alignAction = [OptionAction.divider, OptionAction.align]; + final depthAction = [OptionAction.depth]; + + final List actions = [ + ...standardActions, + if (supportColorBuilderTypes.contains(entry.key)) ...colorAction, + if (supportAlignBuilderType.contains(entry.key)) ...alignAction, + if (supportDepthBuilderType.contains(entry.key)) ...depthAction, + ]; + + if (PlatformExtension.isDesktop) { + builder.showActions = + (node) => node.parent?.type != TableCellBlockKeys.type; + + builder.actionBuilder = (context, state) { + final top = builder.configuration.padding(context.node).top; + final padding = context.node.type == HeadingBlockKeys.type + ? EdgeInsets.only(top: top + 8.0) + : EdgeInsets.only(top: top + 2.0); + return Padding( + padding: padding, + child: BlockActionList( + blockComponentContext: context, + blockComponentState: state, + editorState: editorState, + actions: actions, + showSlashMenu: slashMenuItems != null + ? () => customSlashCommand( + slashMenuItems, + shouldInsertSlash: false, + style: styleCustomizer.selectionMenuStyleBuilder(), + ).handler.call(editorState) + : () {}, + ), + ); + }; + } + } + } + return builders; } - -SimpleTableBlockComponentBuilder _buildSimpleTableBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, { - bool alwaysDistributeColumnWidths = false, -}) { - final copiedConfiguration = configuration.copyWith( - padding: (node) { - final padding = configuration.padding(node); - if (UniversalPlatform.isDesktop) { - return padding; - } else { - return padding; - } - }, - ); - return SimpleTableBlockComponentBuilder( - configuration: copiedConfiguration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - ); -} - -SimpleTableRowBlockComponentBuilder _buildSimpleTableRowBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, { - bool alwaysDistributeColumnWidths = false, -}) { - return SimpleTableRowBlockComponentBuilder( - configuration: configuration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - ); -} - -SimpleTableCellBlockComponentBuilder _buildSimpleTableCellBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, { - bool alwaysDistributeColumnWidths = false, -}) { - return SimpleTableCellBlockComponentBuilder( - configuration: configuration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - ); -} - -ParagraphBlockComponentBuilder _buildParagraphBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - ShowPlaceholder? showParagraphPlaceholder, - String Function(Node)? placeholderText, -) { - return ParagraphBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderText: placeholderText, - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - ), - showPlaceholder: showParagraphPlaceholder, - ); -} - -TodoListBlockComponentBuilder _buildTodoListBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return TodoListBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderText: (_) => LocaleKeys.blockPlaceholders_todoList.tr(), - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - ), - iconBuilder: (_, node, onCheck) => TodoListIcon( - node: node, - onCheck: onCheck, - ), - toggleChildrenTriggers: [ - LogicalKeyboardKey.shift, - LogicalKeyboardKey.shiftLeft, - LogicalKeyboardKey.shiftRight, - ], - ); -} - -BulletedListBlockComponentBuilder _buildBulletedListBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return BulletedListBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderText: (_) => LocaleKeys.blockPlaceholders_bulletList.tr(), - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - ), - iconBuilder: (_, node) => BulletedListIcon(node: node), - ); -} - -NumberedListBlockComponentBuilder _buildNumberedListBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return NumberedListBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderText: (_) => LocaleKeys.blockPlaceholders_numberList.tr(), - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - ), - iconBuilder: (_, node, textDirection) { - TextStyle? textStyle; - if (node.isInHeaderColumn || node.isInHeaderRow) { - textStyle = configuration.textStyle(node).copyWith( - fontWeight: FontWeight.bold, - ); - } - return NumberedListIcon( - node: node, - textDirection: textDirection, - textStyle: textStyle, - ); - }, - ); -} - -QuoteBlockComponentBuilder _buildQuoteBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return QuoteBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderText: (_) => LocaleKeys.blockPlaceholders_quote.tr(), - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - indentPadding: (node, textDirection) { - if (UniversalPlatform.isMobile) { - return configuration.indentPadding(node, textDirection); - } - - if (node.isInTable) { - return textDirection == TextDirection.ltr - ? EdgeInsets.only(left: 24) - : EdgeInsets.only(right: 24); - } - - return EdgeInsets.zero; - }, - ), - ); -} - -HeadingBlockComponentBuilder _buildHeadingBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - EditorStyleCustomizer styleCustomizer, - EdgeInsets? customHeadingPadding, -) { - return HeadingBlockComponentBuilder( - configuration: configuration.copyWith( - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - padding: (node) { - if (customHeadingPadding != null) { - return customHeadingPadding; - } - - if (UniversalPlatform.isMobile) { - final pageStyle = context.read().state; - final factor = pageStyle.fontLayout.factor; - final headingPaddings = - pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor); - final level = - (node.attributes[HeadingBlockKeys.level] ?? 6).clamp(1, 6); - final top = headingPaddings.elementAt(level - 1); - EdgeInsets edgeInsets = EdgeInsets.only(top: top); - if (node.path.length == 1) { - edgeInsets = edgeInsets.copyWith( - left: EditorStyleCustomizer.nodeHorizontalPadding, - right: EditorStyleCustomizer.nodeHorizontalPadding, - ); - } - return edgeInsets; - } - - return const EdgeInsets.only(top: 12.0, bottom: 4.0); - }, - placeholderText: (node) { - int level = node.attributes[HeadingBlockKeys.level] ?? 6; - level = level.clamp(1, 6); - return LocaleKeys.blockPlaceholders_heading.tr( - args: [level.toString()], - ); - }, - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - ), - textStyleBuilder: (level) { - return styleCustomizer.headingStyleBuilder(level); - }, - ); -} - -CustomImageBlockComponentBuilder _buildCustomImageBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return CustomImageBlockComponentBuilder( - configuration: configuration, - showMenu: true, - menuBuilder: (node, state, imageStateNotifier) => Positioned( - top: 10, - right: 10, - child: ImageMenu( - node: node, - state: state, - imageStateNotifier: imageStateNotifier, - ), - ), - ); -} - -MultiImageBlockComponentBuilder _buildMultiImageBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return MultiImageBlockComponentBuilder( - configuration: configuration, - showMenu: true, - menuBuilder: ( - Node node, - MultiImageBlockComponentState state, - ValueNotifier indexNotifier, - VoidCallback onImageDeleted, - ) => - Positioned( - top: 10, - right: 10, - child: MultiImageMenu( - node: node, - state: state, - indexNotifier: indexNotifier, - isLocalMode: context.read().isLocalMode, - onImageDeleted: onImageDeleted, - ), - ), - ); -} - -TableBlockComponentBuilder _buildTableBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return TableBlockComponentBuilder( - menuBuilder: ( - node, - editorState, - position, - dir, - onBuild, - onClose, - ) => - TableMenu( - node: node, - editorState: editorState, - position: position, - dir: dir, - onBuild: onBuild, - onClose: onClose, - ), - ); -} - -TableCellBlockComponentBuilder _buildTableCellBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return TableCellBlockComponentBuilder( - colorBuilder: (context, node) { - final String colorString = - node.attributes[TableCellBlockKeys.colBackgroundColor] ?? - node.attributes[TableCellBlockKeys.rowBackgroundColor] ?? - ''; - if (colorString.isEmpty) { - return null; - } - return buildEditorCustomizedColor(context, node, colorString); - }, - menuBuilder: ( - node, - editorState, - position, - dir, - onBuild, - onClose, - ) => - TableMenu( - node: node, - editorState: editorState, - position: position, - dir: dir, - onBuild: onBuild, - onClose: onClose, - ), - ); -} - -DatabaseViewBlockComponentBuilder _buildDatabaseViewBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return DatabaseViewBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (node) { - if (UniversalPlatform.isMobile) { - return configuration.padding(node); - } - return const EdgeInsets.symmetric(vertical: 10); - }, - ), - ); -} - -CalloutBlockComponentBuilder _buildCalloutBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - final calloutBGColor = AFThemeExtension.of(context).calloutBGColor; - return CalloutBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (node) { - if (UniversalPlatform.isMobile) { - return configuration.padding(node); - } - return const EdgeInsets.symmetric(vertical: 10); - }, - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - textStyle: (node, {TextSpan? textSpan}) => _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ), - indentPadding: (node, _) => EdgeInsets.only(left: 42), - ), - inlinePadding: (node) { - if (node.children.isEmpty) { - return const EdgeInsets.symmetric(vertical: 8.0); - } - return EdgeInsets.only(top: 8.0, bottom: 2.0); - }, - defaultColor: calloutBGColor, - ); -} - -DividerBlockComponentBuilder _buildDividerBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - EditorState editorState, -) { - return DividerBlockComponentBuilder( - configuration: configuration, - height: 28.0, - wrapper: (_, node, child) => MobileBlockActionButtons( - showThreeDots: false, - node: node, - editorState: editorState, - child: child, - ), - ); -} - -MathEquationBlockComponentBuilder _buildMathEquationBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return MathEquationBlockComponentBuilder( - configuration: configuration, - ); -} - -CodeBlockComponentBuilder _buildCodeBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - EditorStyleCustomizer styleCustomizer, -) { - return CodeBlockComponentBuilder( - styleBuilder: styleCustomizer.codeBlockStyleBuilder, - configuration: configuration, - padding: const EdgeInsets.only(left: 20, right: 30, bottom: 34), - languagePickerBuilder: codeBlockLanguagePickerBuilder, - copyButtonBuilder: codeBlockCopyBuilder, - ); -} - -AIWriterBlockComponentBuilder _buildAIWriterBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return AIWriterBlockComponentBuilder(); -} - -ToggleListBlockComponentBuilder _buildToggleListBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - EditorStyleCustomizer styleCustomizer, - EdgeInsets? customHeadingPadding, -) { - return ToggleListBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (node) { - if (customHeadingPadding != null) { - return customHeadingPadding; - } - - if (UniversalPlatform.isMobile) { - final pageStyle = context.read().state; - final factor = pageStyle.fontLayout.factor; - final headingPaddings = - pageStyle.lineHeightLayout.headingPaddings.map((e) => e * factor); - final level = - (node.attributes[HeadingBlockKeys.level] ?? 6).clamp(1, 6); - final top = headingPaddings.elementAt(level - 1); - return configuration.padding(node).copyWith(top: top); - } - - return const EdgeInsets.only(top: 12.0, bottom: 4.0); - }, - textStyle: (node, {TextSpan? textSpan}) { - final textStyle = _buildTextStyleInTableCell( - context, - node: node, - configuration: configuration, - textSpan: textSpan, - ); - final level = node.attributes[ToggleListBlockKeys.level] as int?; - if (level == null) { - return textStyle; - } - return textStyle.merge(styleCustomizer.headingStyleBuilder(level)); - }, - textAlign: (node) => _buildTextAlignInTableCell( - context, - node: node, - configuration: configuration, - ), - placeholderText: (node) { - int? level = node.attributes[ToggleListBlockKeys.level]; - if (level == null) { - return configuration.placeholderText(node); - } - level = level.clamp(1, 6); - return LocaleKeys.blockPlaceholders_heading.tr( - args: [level.toString()], - ); - }, - ), - textStyleBuilder: (level) => styleCustomizer.headingStyleBuilder(level), - ); -} - -OutlineBlockComponentBuilder _buildOutlineBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, - EditorStyleCustomizer styleCustomizer, -) { - return OutlineBlockComponentBuilder( - configuration: configuration.copyWith( - placeholderTextStyle: (node, {TextSpan? textSpan}) => - styleCustomizer.outlineBlockPlaceholderStyleBuilder(), - padding: (node) { - if (UniversalPlatform.isMobile) { - return configuration.padding(node); - } - return const EdgeInsets.only(top: 12.0, bottom: 4.0); - }, - ), - ); -} - -CustomLinkPreviewBlockComponentBuilder _buildLinkPreviewBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return CustomLinkPreviewBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (node) { - if (UniversalPlatform.isMobile) { - return configuration.padding(node); - } - return const EdgeInsets.symmetric(vertical: 10); - }, - ), - ); -} - -FileBlockComponentBuilder _buildFileBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return FileBlockComponentBuilder( - configuration: configuration, - ); -} - -SubPageBlockComponentBuilder _buildSubPageBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, { - required EditorStyleCustomizer styleCustomizer, -}) { - return SubPageBlockComponentBuilder( - configuration: configuration.copyWith( - textStyle: (node, {TextSpan? textSpan}) => - styleCustomizer.subPageBlockTextStyleBuilder(), - padding: (node) { - if (UniversalPlatform.isMobile) { - return const EdgeInsets.symmetric(horizontal: 18); - } - return configuration.padding(node); - }, - ), - ); -} - -SimpleColumnsBlockComponentBuilder _buildSimpleColumnsBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return SimpleColumnsBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (node) { - if (UniversalPlatform.isMobile) { - return configuration.padding(node); - } - - return EdgeInsets.zero; - }, - ), - ); -} - -SimpleColumnBlockComponentBuilder _buildSimpleColumnBlockComponentBuilder( - BuildContext context, - BlockComponentConfiguration configuration, -) { - return SimpleColumnBlockComponentBuilder( - configuration: configuration.copyWith( - padding: (_) => EdgeInsets.zero, - ), - ); -} - -TextStyle _buildTextStyleInTableCell( - BuildContext context, { - required Node node, - required BlockComponentConfiguration configuration, - required TextSpan? textSpan, -}) { - TextStyle textStyle = configuration.textStyle(node, textSpan: textSpan); - - textStyle = textStyle.copyWith( - fontFamily: textSpan?.style?.fontFamily, - fontSize: textSpan?.style?.fontSize, - ); - - if (node.isInHeaderColumn || - node.isInHeaderRow || - node.isInBoldColumn || - node.isInBoldRow) { - textStyle = textStyle.copyWith( - fontWeight: FontWeight.bold, - ); - } - - final cellTextColor = node.textColorInColumn ?? node.textColorInRow; - - // enable it if we need to support the text color of the text span - // final isTextSpanColorNull = textSpan?.style?.color == null; - // final isTextSpanChildrenColorNull = - // textSpan?.children?.every((e) => e.style?.color == null) ?? true; - - if (cellTextColor != null) { - textStyle = textStyle.copyWith( - color: buildEditorCustomizedColor( - context, - node, - cellTextColor, - ), - ); - } - - return textStyle; -} - -TextAlign _buildTextAlignInTableCell( - BuildContext context, { - required Node node, - required BlockComponentConfiguration configuration, -}) { - final isInTable = node.isInTable; - if (!isInTable) { - return configuration.textAlign(node); - } - - return node.tableAlign.textAlign; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart deleted file mode 100644 index 62810545dd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_handler.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; -import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -const _excludeFromDropTarget = [ - ImageBlockKeys.type, - CustomImageBlockKeys.type, - MultiImageBlockKeys.type, - FileBlockKeys.type, - SimpleTableBlockKeys.type, - SimpleTableCellBlockKeys.type, - SimpleTableRowBlockKeys.type, -]; - -class EditorDropHandler extends StatelessWidget { - const EditorDropHandler({ - super.key, - required this.viewId, - required this.editorState, - required this.isLocalMode, - required this.child, - this.dropManagerState, - }); - - final String viewId; - final EditorState editorState; - final bool isLocalMode; - final Widget child; - final EditorDropManagerState? dropManagerState; - - @override - Widget build(BuildContext context) { - final childWidget = Consumer( - builder: (context, dropState, _) => DragTarget( - onLeave: (_) { - editorState.selectionService.removeDropTarget(); - disableAutoScrollWhenDragging = false; - }, - onMove: (details) { - disableAutoScrollWhenDragging = true; - - if (details.data.id == viewId) { - return; - } - - _onDragUpdated(details.offset); - }, - onWillAcceptWithDetails: (details) { - if (!dropState.isDropEnabled) { - return false; - } - - if (details.data.id == viewId) { - return false; - } - - return true; - }, - onAcceptWithDetails: _onDragViewDone, - builder: (context, _, __) => ValueListenableBuilder( - valueListenable: enableDocumentDragNotifier, - builder: (context, value, _) { - final enableDocumentDrag = value; - return DropTarget( - enable: dropState.isDropEnabled && enableDocumentDrag, - onDragExited: (_) => - editorState.selectionService.removeDropTarget(), - onDragUpdated: (details) => - _onDragUpdated(details.globalPosition), - onDragDone: _onDragDone, - child: child, - ); - }, - ), - ), - ); - - // Due to how DropTarget works, there is no way to differentiate if an overlay is - // blocking the target visibly, so when we have an overlay with a drop target, - // we should disable the drop target for the Editor, until it is closed. - // - // See FileBlockComponent for sample use. - // - // Relates to: - // - https://github.com/MixinNetwork/flutter-plugins/issues/2 - // - https://github.com/MixinNetwork/flutter-plugins/issues/331 - if (dropManagerState != null) { - return ChangeNotifierProvider.value( - value: dropManagerState!, - child: childWidget, - ); - } - - return ChangeNotifierProvider( - create: (_) => EditorDropManagerState(), - child: childWidget, - ); - } - - void _onDragUpdated(Offset position) { - final data = editorState.selectionService.getDropTargetRenderData(position); - - if (data != null && - data.dropPath != null && - - // We implement custom Drop logic for image blocks, this is - // how we can exclude them from the Drop Target - !_excludeFromDropTarget.contains(data.cursorNode?.type)) { - // Render the drop target - editorState.selectionService.renderDropTargetForOffset(position); - } else { - editorState.selectionService.removeDropTarget(); - } - } - - Future _onDragDone(DropDoneDetails details) async { - editorState.selectionService.removeDropTarget(); - - final data = editorState.selectionService - .getDropTargetRenderData(details.globalPosition); - - if (data != null) { - final cursorNode = data.cursorNode; - final dropPath = data.dropPath; - - if (cursorNode != null && dropPath != null) { - if (_excludeFromDropTarget.contains(cursorNode.type)) { - return; - } - - for (final file in details.files) { - final fileName = file.name.toLowerCase(); - if (file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(fileName)) { - await editorState.dropImages(dropPath, [file], viewId, isLocalMode); - } else { - await editorState.dropFiles(dropPath, [file], viewId, isLocalMode); - } - } - } - } - } - - void _onDragViewDone(DragTargetDetails details) { - editorState.selectionService.removeDropTarget(); - - final data = - editorState.selectionService.getDropTargetRenderData(details.offset); - if (data != null) { - final cursorNode = data.cursorNode; - final dropPath = data.dropPath; - - if (cursorNode != null && dropPath != null) { - if (_excludeFromDropTarget.contains(cursorNode.type)) { - return; - } - - final view = details.data; - final node = pageMentionNode(view.id); - final t = editorState.transaction..insertNode(dropPath, node); - editorState.apply(t); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_manager.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_manager.dart deleted file mode 100644 index 8b59809f3b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_drop_manager.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class EditorDropManagerState extends ChangeNotifier { - final Set _draggedTypes = {}; - - void add(String type) { - _draggedTypes.add(type); - notifyListeners(); - } - - void remove(String type) { - _draggedTypes.remove(type); - notifyListeners(); - } - - bool get isDropEnabled => _draggedTypes.isEmpty; -} - -final enableDocumentDragNotifier = ValueNotifier(true); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart index fce8b4c16e..109a9e9915 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart @@ -2,16 +2,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -enum EditorNotificationType { - none, - undo, - redo, - exitEditing, - paste, - dragStart, - dragEnd, - turnInto, -} +enum EditorNotificationType { none, undo, redo, exitEditing } class EditorNotification { const EditorNotification({required this.type}); @@ -19,10 +10,6 @@ class EditorNotification { EditorNotification.undo() : type = EditorNotificationType.undo; EditorNotification.redo() : type = EditorNotificationType.redo; EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing; - EditorNotification.paste() : type = EditorNotificationType.paste; - EditorNotification.dragStart() : type = EditorNotificationType.dragStart; - EditorNotification.dragEnd() : type = EditorNotificationType.dragEnd; - EditorNotification.turnInto() : type = EditorNotificationType.turnInto; static final PropertyValueNotifier _notifier = PropertyValueNotifier(EditorNotificationType.none); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index edb19232be..cf90431c8e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,43 +1,81 @@ import 'dart:ui' as ui; -import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/background_color/theme_background_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/i18n/editor_i18n.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart'; import 'package:appflowy/plugins/inline_actions/handlers/date_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; import 'package:appflowy/plugins/inline_actions/handlers/reminder_reference.dart'; +import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide QuoteBlockKeys; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'editor_plugins/toolbar_item/custom_format_toolbar_items.dart'; -import 'editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/text_heading_toolbar_item.dart'; -import 'editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; +final codeBlockLocalization = CodeBlockLocalizations( + codeBlockNewParagraph: + LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(), + codeBlockIndentLines: + LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(), + codeBlockOutdentLines: + LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(), + codeBlockSelectAll: + LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(), + codeBlockPasteText: + LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(), + codeBlockAddTwoSpaces: + LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(), +); + +final localizedCodeBlockCommands = + codeBlockCommands(localizations: codeBlockLocalization); + +final List commandShortcutEvents = [ + toggleToggleListCommand, + ...localizedCodeBlockCommands, + customCopyCommand, + customPasteCommand, + customCutCommand, + ...customTextAlignCommands, + + // remove standard shortcuts for copy, cut, paste, todo + ...standardCommandShortcutEvents + ..removeWhere( + (shortcut) => [ + copyCommand, + cutCommand, + pasteCommand, + toggleTodoListCommand, + ].contains(shortcut), + ), + + emojiShortcutEvent, +]; + +final List defaultCommandShortcutEvents = [ + ...commandShortcutEvents.map((e) => e.copyWith()), +]; /// Wrapper for the appflowy editor. class AppFlowyEditorPage extends StatefulWidget { @@ -73,87 +111,115 @@ class AppFlowyEditorPage extends StatefulWidget { State createState() => _AppFlowyEditorPageState(); } -class _AppFlowyEditorPageState extends State - with WidgetsBindingObserver { +class _AppFlowyEditorPageState extends State { late final ScrollController effectiveScrollController; late final InlineActionsService inlineActionsService = InlineActionsService( context: context, handlers: [ - if (FeatureFlag.inlineSubPageMention.isOn) - InlineChildPageService(currentViewId: documentBloc.documentId), InlinePageReferenceService(currentViewId: documentBloc.documentId), DateReferenceService(context), ReminderReferenceService(context), ], ); - late final List commandShortcuts = [ + late final List cmdShortcutEvents = [ ...commandShortcutEvents, ..._buildFindAndReplaceCommands(), ]; final List toolbarItems = [ - improveWritingItem, - group0PaddingItem, - aiWriterItem, - customTextHeadingItem, - buildPaddingPlaceholderItem( - 1, - isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, - ), - ...customMarkdownFormatItems, - group1PaddingItem, - customTextColorItem, - group1PaddingItem, - customHighlightColorItem, - customInlineCodeItem, - suggestionsItem, - customLinkItem, - group4PaddingItem, - customTextAlignItem, - moreOptionItem, + smartEditItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + paragraphItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + ...headingItems + ..forEach((e) => e.isActive = onlyShowInSingleSelectionAndTextType), + ...markdownFormatItems..forEach((e) => e.isActive = showInAnyTextType), + quoteItem..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + bulletedListItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + numberedListItem + ..isActive = onlyShowInSingleTextTypeSelectionAndExcludeTable, + inlineMathEquationItem, + linkItem, + alignToolbarItem, + buildTextColorItem(), + buildHighlightColorItem(), + customizeFontToolbarItem, ]; - List get characterShortcutEvents { - return buildCharacterShortcutEvents( - context, - documentBloc, - styleCustomizer, - inlineActionsService, - (editorState, node) => _customSlashMenuItems( - editorState: editorState, - node: node, - ), - ); - } + late final List slashMenuItems; + + List get characterShortcutEvents => [ + // code block + ...codeBlockCharacterEvents, + + // callout block + insertNewLineInCalloutBlock, + + // toggle list + formatGreaterToToggleList, + insertChildNodeInsideToggleList, + + // customize the slash menu command + customSlashCommand( + slashMenuItems, + style: styleCustomizer.selectionMenuStyleBuilder(), + ), + + customFormatGreaterEqual, + + ...standardCharacterShortcutEvents + ..removeWhere( + (shortcut) => [ + slashCommand, // Remove default slash command + formatGreaterEqual, // Overridden by customFormatGreaterEqual + ].contains(shortcut), + ), + + /// Inline Actions + /// - Reminder + /// - Inline-page reference + inlineActionsCommand( + inlineActionsService, + style: styleCustomizer.inlineActionsMenuStyleBuilder(), + ), + + /// Inline page menu + /// - Using `[[` + pageReferenceShortcutBrackets( + context, + documentBloc.documentId, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), + + /// - Using `+` + pageReferenceShortcutPlusSign( + context, + documentBloc.documentId, + styleCustomizer.inlineActionsMenuStyleBuilder(), + ), + ]; EditorStyleCustomizer get styleCustomizer => widget.styleCustomizer; - DocumentBloc get documentBloc => context.read(); late final EditorScrollController editorScrollController; late final ViewInfoBloc viewInfoBloc = context.read(); - final editorKeyboardInterceptor = EditorKeyboardInterceptor(); - Future showSlashMenu(editorState) async => customSlashCommand( - _customSlashMenuItems(), + slashMenuItems, shouldInsertSlash: false, style: styleCustomizer.selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: supportSlashMenuNodeTypes, ).handler(editorState); AFFocusManager? focusManager; - AppLifecycleState? lifecycleState = WidgetsBinding.instance.lifecycleState; - List previousSelections = []; + void _loseFocus() => widget.editorState.selection = null; @override void initState() { super.initState(); - WidgetsBinding.instance.addObserver(this); if (widget.useViewInfoBloc) { viewInfoBloc.add( @@ -163,31 +229,10 @@ class _AppFlowyEditorPageState extends State _initEditorL10n(); _initializeShortcuts(); - - AppFlowyRichTextKeys.partialSliced.addAll([ - MentionBlockKeys.mention, - InlineMathEquationKeys.formula, - ]); - - indentableBlockTypes.addAll([ - ToggleListBlockKeys.type, - CalloutBlockKeys.type, - QuoteBlockKeys.type, - ]); - convertibleBlockTypes.addAll([ - ToggleListBlockKeys.type, - CalloutBlockKeys.type, - QuoteBlockKeys.type, - ]); - - editorLaunchUrl = (url) { - if (url != null) { - afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - } - - return Future.value(true); - }; - + appFlowyEditorAutoScrollEdgeOffset = 220; + indentableBlockTypes.add(ToggleListBlockKeys.type); + convertibleBlockTypes.add(ToggleListBlockKeys.type); + slashMenuItems = _customSlashMenuItems(); effectiveScrollController = widget.scrollController ?? ScrollController(); // disable the color parse in the HTML decoder. DocumentHTMLDecoder.enableColorParse = false; @@ -198,21 +243,20 @@ class _AppFlowyEditorPageState extends State scrollController: effectiveScrollController, ); + // keep the previous font style when typing new text. + supportSlashMenuNodeWhiteList.addAll([ + ToggleListBlockKeys.type, + ]); toolbarItemWhiteList.addAll([ ToggleListBlockKeys.type, CalloutBlockKeys.type, TableBlockKeys.type, - SimpleTableBlockKeys.type, - SimpleTableCellBlockKeys.type, - SimpleTableRowBlockKeys.type, ]); AppFlowyRichTextKeys.supportSliced.add(AppFlowyRichTextKeys.fontFamily); // customize the dynamic theme color _customizeBlockComponentBackgroundColorDecorator(); - widget.editorState.selectionNotifier.addListener(onSelectionChanged); - WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; @@ -221,84 +265,12 @@ class _AppFlowyEditorPageState extends State focusManager = AFFocusManager.maybeOf(context); focusManager?.loseFocusNotifier.addListener(_loseFocus); - _scrollToSelectionIfNeeded(); - - widget.editorState.service.keyboardService?.registerInterceptor( - editorKeyboardInterceptor, - ); + if (widget.initialSelection != null) { + widget.editorState.updateSelectionWithReason(widget.initialSelection); + } }); } - void _scrollToSelectionIfNeeded() { - final initialSelection = widget.initialSelection; - final path = initialSelection?.start.path; - if (path == null) { - return; - } - - // on desktop, using jumpTo to scroll to the selection. - // on mobile, using scrollTo to scroll to the selection, because using jumpTo will break the scroll notification metrics. - if (UniversalPlatform.isDesktop) { - editorScrollController.itemScrollController.jumpTo( - index: path.first, - alignment: 0.5, - ); - widget.editorState.updateSelectionWithReason( - initialSelection, - ); - } else { - const delayDuration = Duration(milliseconds: 250); - const animationDuration = Duration(milliseconds: 400); - Future.delayed(delayDuration, () { - editorScrollController.itemScrollController.scrollTo( - index: path.first, - duration: animationDuration, - curve: Curves.easeInOut, - ); - widget.editorState.updateSelectionWithReason( - initialSelection, - extraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableMobileToolbarKey: true, - }, - ); - }).then((_) { - Future.delayed(animationDuration, () { - widget.editorState.selectionType = SelectionType.inline; - widget.editorState.selectionExtraInfo = null; - }); - }); - } - } - - void onSelectionChanged() { - if (widget.editorState.isDisposed) { - return; - } - - previousSelections.add(widget.editorState.selection); - - if (previousSelections.length > 2) { - previousSelections.removeAt(0); - } - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - lifecycleState = state; - - if (widget.editorState.isDisposed) { - return; - } - - if (previousSelections.length == 2 && - state == AppLifecycleState.resumed && - widget.editorState.selection == null) { - widget.editorState.selection = previousSelections.first; - } - } - @override void didChangeDependencies() { final currFocusManager = AFFocusManager.maybeOf(context); @@ -307,16 +279,11 @@ class _AppFlowyEditorPageState extends State focusManager = currFocusManager; focusManager?.loseFocusNotifier.addListener(_loseFocus); } - super.didChangeDependencies(); } @override void dispose() { - widget.editorState.selectionNotifier.removeListener(onSelectionChanged); - widget.editorState.service.keyboardService?.unregisterInterceptor( - editorKeyboardInterceptor, - ); focusManager?.loseFocusNotifier.removeListener(_loseFocus); if (widget.useViewInfoBloc && !viewInfoBloc.isClosed) { @@ -348,17 +315,10 @@ class _AppFlowyEditorPageState extends State context.read().state.enableRtlToolbarItems, ); - final isViewDeleted = context.read().state.isDeleted; - final isLocked = - context.read()?.state.isLocked ?? false; - final editor = Directionality( textDirection: textDirection, child: AppFlowyEditor( editorState: widget.editorState, - editable: !isViewDeleted && !isLocked, - disableSelectionService: UniversalPlatform.isMobile && isLocked, - disableKeyboardService: UniversalPlatform.isMobile && isLocked, editorScrollController: editorScrollController, // setup the auto focus parameters autoFocus: widget.autoFocus ?? autoFocus, @@ -366,11 +326,8 @@ class _AppFlowyEditorPageState extends State // setup the theme editorStyle: styleCustomizer.style(), // customize the block builders - blockComponentBuilders: buildBlockComponentBuilders( - slashMenuItemsBuilder: (editorState, node) => _customSlashMenuItems( - editorState: editorState, - node: node, - ), + blockComponentBuilders: getEditorBuilderMap( + slashMenuItems: slashMenuItems, context: context, editorState: widget.editorState, styleCustomizer: widget.styleCustomizer, @@ -379,39 +336,25 @@ class _AppFlowyEditorPageState extends State ), // customize the shortcuts characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcuts, + commandShortcutEvents: cmdShortcutEvents, // customize the context menu items contextMenuItems: customContextMenuItems, // customize the header and footer. header: widget.header, - autoScrollEdgeOffset: UniversalPlatform.isDesktopOrWeb - ? 250 - : appFlowyEditorAutoScrollEdgeOffset, footer: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () async { // if the last one isn't a empty node, insert a new empty node. await _focusOnLastEmptyParagraph(); }, - child: SizedBox( - width: double.infinity, - height: UniversalPlatform.isDesktopOrWeb ? 600 : 400, - ), - ), - dropTargetStyle: AppFlowyDropTargetStyle( - color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.8), - margin: const EdgeInsets.only(left: 44), + child: VSpace(PlatformExtension.isDesktopOrWeb ? 200 : 400), ), ), ); - if (isViewDeleted) { - return editor; - } - final editorState = widget.editorState; - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return AppFlowyMobileToolbar( toolbarHeight: 42.0, editorState: editorState, @@ -425,69 +368,51 @@ class _AppFlowyEditorPageState extends State anchor: anchor, closeToolbar: closeToolbar, ), - floatingToolbarHeight: 32, child: editor, ), ); } - final appTheme = AppFlowyTheme.of(context); + return Center( - child: BlocProvider.value( - value: context.read(), - child: FloatingToolbar( - floatingToolbarHeight: 40, - padding: EdgeInsets.symmetric(horizontal: 6), - style: FloatingToolbarStyle( - backgroundColor: Theme.of(context).cardColor, - toolbarActiveColor: Color(0xffe0f8fd), - ), - items: toolbarItems, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(appTheme.borderRadius.l), - color: appTheme.surfaceColorScheme.primary, - boxShadow: appTheme.shadow.small, - ), - toolbarBuilder: (_, child, onDismiss, isMetricsChanged) => - BlocProvider.value( - value: context.read(), - child: DesktopFloatingToolbar( - editorState: editorState, - onDismiss: onDismiss, - enableAnimation: !isMetricsChanged, - child: child, - ), - ), - placeHolderBuilder: (_) => customPlaceholderItem, - editorState: editorState, - editorScrollController: editorScrollController, - textDirection: textDirection, - tooltipBuilder: (context, id, message, child) => - widget.styleCustomizer.buildToolbarItemTooltip( - context, - id, - message, - child, - ), - child: editor, - ), + child: FloatingToolbar( + style: styleCustomizer.floatingToolbarStyleBuilder(), + items: toolbarItems, + editorState: editorState, + editorScrollController: editorScrollController, + textDirection: textDirection, + child: editor, ), ); } - List _customSlashMenuItems({ - EditorState? editorState, - Node? node, - }) { - final documentBloc = context.read(); - final isLocalMode = documentBloc.isLocalMode; - final view = context.read().state.view; - return slashMenuItemsBuilder( - editorState: editorState, - node: node, - isLocalMode: isLocalMode, - documentBloc: documentBloc, - view: view, - ); + List _customSlashMenuItems() { + final items = [...standardSelectionMenuItems]; + final imageItem = items + .firstWhereOrNull((e) => e.name == AppFlowyEditorL10n.current.image); + if (imageItem != null) { + final imageItemIndex = items.indexOf(imageItem); + if (imageItemIndex != -1) { + items[imageItemIndex] = customImageMenuItem; + } + } + return [ + ...items, + inlineGridMenuItem(documentBloc), + referencedGridMenuItem, + inlineBoardMenuItem(documentBloc), + referencedBoardMenuItem, + inlineCalendarMenuItem(documentBloc), + referencedCalendarMenuItem, + referencedDocumentMenuItem, + calloutItem, + outlineItem, + mathEquationItem, + codeBlockItem(LocaleKeys.document_selectionMenu_codeBlock.tr()), + toggleListBlockItem, + emojiMenuItem, + autoGeneratorMenuItem, + dateMenuItem, + ]; } (bool, Selection?) _computeAutoFocusParameters() { @@ -503,7 +428,7 @@ class _AppFlowyEditorPageState extends State final customizeShortcuts = await settingsShortcutService.getCustomizeShortcuts(); await settingsShortcutService.updateCommandShortcuts( - commandShortcuts, + cmdShortcutEvents, customizeShortcuts, ); } @@ -537,7 +462,6 @@ class _AppFlowyEditorPageState extends State borderRadius: BorderRadius.circular(4), ), child: FindAndReplaceMenuWidget( - showReplaceMenu: showReplaceMenu, editorState: editorState, onDismiss: onDismiss, ), @@ -548,12 +472,8 @@ class _AppFlowyEditorPageState extends State } void _customizeBlockComponentBackgroundColorDecorator() { - blockComponentBackgroundColorDecorator = (Node node, String colorString) { - if (mounted && context.mounted) { - return buildEditorCustomizedColor(context, node, colorString); - } - return null; - }; + blockComponentBackgroundColorDecorator = (Node node, String colorString) => + buildEditorCustomizedColor(context, node, colorString); } void _initEditorL10n() => AppFlowyEditorL10n.current = EditorI18n(); @@ -575,18 +495,8 @@ class _AppFlowyEditorPageState extends State Position(path: lastNode.path), ); } - - transaction.customSelectionType = SelectionType.inline; - transaction.reason = SelectionUpdateReason.uiEvent; - await editorState.apply(transaction); } - - void _loseFocus() { - if (!widget.editorState.isDisposed) { - widget.editorState.selection = null; - } - } } Color? buildEditorCustomizedColor( @@ -594,10 +504,6 @@ Color? buildEditorCustomizedColor( Node node, String colorString, ) { - if (!context.mounted) { - return null; - } - // the color string is from FlowyTint. final tintColor = FlowyTint.values.firstWhereOrNull( (e) => e.id == colorString, @@ -622,11 +528,7 @@ Color? buildEditorCustomizedColor( return AFThemeExtension.of(context).tableCellBGColor; } - try { - return colorString.tryToColor(); - } catch (e) { - return null; - } + return null; } bool showInAnyTextType(EditorState editorState) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart index 6d01ed5f1b..b58b0a5646 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart @@ -5,7 +5,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.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/services.dart'; @@ -32,19 +31,16 @@ class BlockAddButton extends StatelessWidget { children: [ TextSpan( text: LocaleKeys.blockActions_addBelowTooltip.tr(), - style: context.tooltipTextStyle(), ), const TextSpan(text: '\n'), TextSpan( text: Platform.isMacOS ? LocaleKeys.blockActions_addAboveMacCmd.tr() : LocaleKeys.blockActions_addAboveCmd.tr(), - style: context.tooltipTextStyle(), ), const TextSpan(text: ' '), TextSpan( text: LocaleKeys.blockActions_addAboveTooltip.tr(), - style: context.tooltipTextStyle(), ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart index 9e0b241ab3..e6a88bc4a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_button.dart @@ -1,7 +1,8 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flutter/material.dart'; class BlockActionButton extends StatelessWidget { @@ -10,33 +11,32 @@ class BlockActionButton extends StatelessWidget { required this.svg, required this.richMessage, required this.onTap, - this.showTooltip = true, - this.onPointerDown, }); final FlowySvgData svg; - final bool showTooltip; final InlineSpan richMessage; final VoidCallback onTap; - final VoidCallback? onPointerDown; @override Widget build(BuildContext context) { - return FlowyTooltip( - richMessage: showTooltip ? richMessage : null, - child: FlowyIconButton( - width: 18.0, - hoverColor: Colors.transparent, - iconColorOnHover: Theme.of(context).iconTheme.color, - onPressed: onTap, - icon: MouseRegion( + return Align( + child: FlowyTooltip( + preferBelow: false, + richMessage: richMessage, + child: MouseRegion( cursor: Platform.isWindows ? SystemMouseCursors.click : SystemMouseCursors.grab, - child: FlowySvg( - svg, - size: const Size.square(18.0), - color: Theme.of(context).iconTheme.color, + child: IgnoreParentGestureWidget( + child: GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.deferToChild, + child: FlowySvg( + svg, + size: const Size.square(18.0), + color: Theme.of(context).iconTheme.color, + ), + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart index 6323f675cc..da7b3a0d38 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart @@ -1,9 +1,8 @@ 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/actions/option/option_actions.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; class BlockActionList extends StatelessWidget { const BlockActionList({ @@ -13,7 +12,6 @@ class BlockActionList extends StatelessWidget { required this.editorState, required this.actions, required this.showSlashMenu, - required this.blockComponentBuilder, }); final BlockComponentContext blockComponentContext; @@ -21,7 +19,6 @@ class BlockActionList extends StatelessWidget { final List actions; final VoidCallback showSlashMenu; final EditorState editorState; - final Map blockComponentBuilder; @override Widget build(BuildContext context) { @@ -34,15 +31,14 @@ class BlockActionList extends StatelessWidget { editorState: editorState, showSlashMenu: showSlashMenu, ), - const HSpace(2.0), + const SizedBox(width: 4.0), BlockOptionButton( blockComponentContext: blockComponentContext, blockComponentState: blockComponentState, actions: actions, editorState: editorState, - blockComponentBuilder: blockComponentBuilder, ), - const HSpace(5.0), + const SizedBox(width: 4.0), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart index 4efb1a55b2..a5617a5558 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart @@ -1,139 +1,147 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.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/actions/block_action_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option_action.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'drag_to_reorder/draggable_option_button.dart'; - -class BlockOptionButton extends StatefulWidget { +class BlockOptionButton extends StatelessWidget { const BlockOptionButton({ super.key, required this.blockComponentContext, required this.blockComponentState, required this.actions, required this.editorState, - required this.blockComponentBuilder, }); final BlockComponentContext blockComponentContext; final BlockComponentActionState blockComponentState; final List actions; final EditorState editorState; - final Map blockComponentBuilder; - - @override - State createState() => _BlockOptionButtonState(); -} - -class _BlockOptionButtonState extends State { - // the mutex is used to ensure that only one popover is open at a time - // for example, when the user is selecting the color, the turn into option - // should not be shown. - final mutex = PopoverMutex(); @override Widget build(BuildContext context) { - final direction = - context.read().state.layoutDirection == - LayoutDirection.rtlLayout - ? PopoverDirection.rightWithCenterAligned - : PopoverDirection.leftWithCenterAligned; - return BlocProvider( - create: (context) => BlockActionOptionCubit( - editorState: widget.editorState, - blockComponentBuilder: widget.blockComponentBuilder, - ), - child: BlocBuilder( - builder: (context, _) => PopoverActionList( - actions: _buildPopoverActions(context), - animationDuration: Durations.short3, - slideDistance: 5, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - direction: direction, - onPopupBuilder: _onPopoverBuilder, - onClosed: () => _onPopoverClosed(context), - onSelected: (action, controller) => _onActionSelected( - context, - action, - controller, - ), - buildChild: (controller) => DraggableOptionButton( - controller: controller, - editorState: widget.editorState, - blockComponentContext: widget.blockComponentContext, - blockComponentBuilder: widget.blockComponentBuilder, - ), - ), - ), - ); - } - - @override - void dispose() { - mutex.dispose(); - - super.dispose(); - } - - List _buildPopoverActions(BuildContext context) { - return widget.actions.map((e) { + final popoverActions = actions.map((e) { switch (e) { case OptionAction.divider: return DividerOptionAction(); case OptionAction.color: - return ColorOptionAction( - editorState: widget.editorState, - mutex: mutex, - ); + return ColorOptionAction(editorState: editorState); case OptionAction.align: - return AlignOptionAction(editorState: widget.editorState); + return AlignOptionAction(editorState: editorState); case OptionAction.depth: - return DepthOptionAction(editorState: widget.editorState); - case OptionAction.turnInto: - return TurnIntoOptionAction( - editorState: widget.editorState, - blockComponentBuilder: widget.blockComponentBuilder, - mutex: mutex, - ); + return DepthOptionAction(editorState: editorState); default: return OptionActionWrapper(e); } }).toList(); + + return PopoverActionList( + popoverMutex: PopoverMutex(), + direction: + context.read().state.layoutDirection == + LayoutDirection.rtlLayout + ? PopoverDirection.rightWithCenterAligned + : PopoverDirection.leftWithCenterAligned, + actions: popoverActions, + onPopupBuilder: () { + keepEditorFocusNotifier.increase(); + blockComponentState.alwaysShowActions = true; + }, + onClosed: () { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + editorState.selectionType = null; + editorState.selection = null; + blockComponentState.alwaysShowActions = false; + keepEditorFocusNotifier.decrease(); + }); + }, + onSelected: (action, controller) { + if (action is OptionActionWrapper) { + _onSelectAction(action.inner); + controller.close(); + } + }, + buildChild: (controller) => _buildOptionButton(controller), + ); } - void _onPopoverBuilder() { - keepEditorFocusNotifier.increase(); - widget.blockComponentState.alwaysShowActions = true; + Widget _buildOptionButton(PopoverController controller) { + return BlockActionButton( + svg: FlowySvgs.drag_element_s, + richMessage: TextSpan( + children: [ + TextSpan( + // todo: customize the color to highlight the text. + text: LocaleKeys.document_plugins_optionAction_click.tr(), + ), + TextSpan( + text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), + ), + ], + ), + onTap: () { + controller.show(); + + // update selection + _updateBlockSelection(); + }, + ); } - void _onPopoverClosed(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - widget.editorState.selectionType = null; - widget.editorState.selection = null; - widget.blockComponentState.alwaysShowActions = false; - }); - - PopoverContainer.maybeOf(context)?.closeAll(); - } - - void _onActionSelected( - BuildContext context, - PopoverAction action, - PopoverController controller, - ) { - if (action is! OptionActionWrapper) { - return; + void _updateBlockSelection() { + final startNode = blockComponentContext.node; + var endNode = startNode; + while (endNode.children.isNotEmpty) { + endNode = endNode.children.last; } - context.read().handleAction( - action.inner, - widget.blockComponentContext.node, + final start = Position(path: startNode.path); + final end = endNode.selectable?.end() ?? + Position( + path: endNode.path, + offset: endNode.delta?.length ?? 0, ); - controller.close(); + + editorState.selectionType = SelectionType.block; + editorState.selection = Selection( + start: start, + end: end, + ); + } + + void _onSelectAction(OptionAction action) { + final node = blockComponentContext.node; + final transaction = editorState.transaction; + switch (action) { + case OptionAction.delete: + transaction.deleteNode(node); + break; + case OptionAction.duplicate: + transaction.insertNode( + node.path.next, + node.copyWith(), + ); + break; + case OptionAction.turnInto: + break; + case OptionAction.moveUp: + transaction.moveNode(node.path.previous, node); + break; + case OptionAction.moveDown: + transaction.moveNode(node.path.next.next, node); + break; + case OptionAction.align: + case OptionAction.color: + case OptionAction.divider: + case OptionAction.depth: + throw UnimplementedError(); + } + editorState.apply(transaction); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart deleted file mode 100644 index abed98136d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ /dev/null @@ -1,690 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/plugins/trash/application/prelude.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/view/prelude.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_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class BlockActionOptionState {} - -class BlockActionOptionCubit extends Cubit { - BlockActionOptionCubit({ - required this.editorState, - required this.blockComponentBuilder, - }) : super(BlockActionOptionState()); - - final EditorState editorState; - final Map blockComponentBuilder; - - Future handleAction(OptionAction action, Node node) async { - final transaction = editorState.transaction; - switch (action) { - case OptionAction.delete: - _deleteBlocks(transaction, node); - break; - case OptionAction.duplicate: - await _duplicateBlock(transaction, node); - EditorNotification.paste().post(); - break; - case OptionAction.moveUp: - transaction.moveNode(node.path.previous, node); - break; - case OptionAction.moveDown: - transaction.moveNode(node.path.next.next, node); - break; - case OptionAction.copyLinkToBlock: - await _copyLinkToBlock(node); - break; - case OptionAction.setToPageWidth: - await _setToPageWidth(node); - break; - case OptionAction.distributeColumnsEvenly: - await _distributeColumnsEvenly(node); - break; - case OptionAction.align: - case OptionAction.color: - case OptionAction.divider: - case OptionAction.depth: - case OptionAction.turnInto: - throw UnimplementedError(); - } - - await editorState.apply(transaction); - } - - /// If the selection is a block selection, delete the selected blocks. - /// Otherwise, delete the selected block. - void _deleteBlocks(Transaction transaction, Node selectedNode) { - final selection = editorState.selection; - final selectionType = editorState.selectionType; - if (selectionType == SelectionType.block && selection != null) { - final nodes = editorState.getNodesInSelection(selection.normalized); - transaction.deleteNodes(nodes); - } else { - transaction.deleteNode(selectedNode); - } - } - - Future _duplicateBlock(Transaction transaction, Node node) async { - final selection = editorState.selection; - final selectionType = editorState.selectionType; - if (selectionType == SelectionType.block && selection != null) { - final nodes = editorState.getNodesInSelection(selection.normalized); - for (final node in nodes) { - _validateNode(node); - } - transaction.insertNodes( - selection.normalized.end.path.next, - nodes.map((e) => _copyBlock(e)).toList(), - ); - } else { - _validateNode(node); - transaction.insertNode(node.path.next, _copyBlock(node)); - } - } - - void _validateNode(Node node) { - final type = node.type; - final builder = blockComponentBuilder[type]; - - if (builder == null) { - Log.error('Block type $type is not supported'); - return; - } - - final valid = builder.validate(node); - if (!valid) { - Log.error('Block type $type is not valid'); - } - } - - Node _copyBlock(Node node) { - Node copiedNode = node.deepCopy(); - - final type = node.type; - final builder = blockComponentBuilder[type]; - - if (builder == null) { - Log.error('Block type $type is not supported'); - } else { - final valid = builder.validate(node); - if (!valid) { - Log.error('Block type $type is not valid'); - if (node.type == TableBlockKeys.type) { - copiedNode = _fixTableBlock(node); - copiedNode = _convertTableToSimpleTable(copiedNode); - } - } else { - if (node.type == TableBlockKeys.type) { - copiedNode = _convertTableToSimpleTable(node); - } - } - } - - return copiedNode; - } - - Node _fixTableBlock(Node node) { - if (node.type != TableBlockKeys.type) { - return node; - } - - // the table node should contains colsLen and rowsLen - final colsLen = node.attributes[TableBlockKeys.colsLen]; - final rowsLen = node.attributes[TableBlockKeys.rowsLen]; - if (colsLen == null || rowsLen == null) { - return node; - } - - final newChildren = []; - final children = node.children; - - // based on the colsLen and rowsLen, iterate the children and fix the data - for (var i = 0; i < rowsLen; i++) { - for (var j = 0; j < colsLen; j++) { - final cell = children - .where( - (n) => - n.attributes[TableCellBlockKeys.rowPosition] == i && - n.attributes[TableCellBlockKeys.colPosition] == j, - ) - .firstOrNull; - if (cell != null) { - newChildren.add(cell.deepCopy()); - } else { - newChildren.add( - tableCellNode('', i, j), - ); - } - } - } - - return node.copyWith( - children: newChildren, - attributes: { - ...node.attributes, - TableBlockKeys.colsLen: colsLen, - TableBlockKeys.rowsLen: rowsLen, - }, - ); - } - - Node _convertTableToSimpleTable(Node node) { - if (node.type != TableBlockKeys.type) { - return node; - } - - // the table node should contains colsLen and rowsLen - final colsLen = node.attributes[TableBlockKeys.colsLen]; - final rowsLen = node.attributes[TableBlockKeys.rowsLen]; - if (colsLen == null || rowsLen == null) { - return node; - } - - final rows = >[]; - final children = node.children; - for (var i = 0; i < rowsLen; i++) { - final row = []; - for (var j = 0; j < colsLen; j++) { - final cell = children - .where( - (n) => - n.attributes[TableCellBlockKeys.rowPosition] == i && - n.attributes[TableCellBlockKeys.colPosition] == j, - ) - .firstOrNull; - row.add( - simpleTableCellBlockNode( - children: [cell?.children.first.deepCopy() ?? paragraphNode()], - ), - ); - } - rows.add(row); - } - - return simpleTableBlockNode( - children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(), - ); - } - - Future _copyLinkToBlock(Node node) async { - List nodes = [node]; - - final selection = editorState.selection; - final selectionType = editorState.selectionType; - if (selectionType == SelectionType.block && selection != null) { - nodes = editorState.getNodesInSelection(selection.normalized); - } - - final context = editorState.document.root.context; - final viewId = context?.read().documentId; - if (viewId == null) { - return; - } - - final workspace = await FolderEventReadCurrentWorkspace().send(); - final workspaceId = workspace.fold( - (l) => l.id, - (r) => '', - ); - - if (workspaceId.isEmpty || viewId.isEmpty) { - Log.error('Failed to get workspace id: $workspaceId or view id: $viewId'); - emit(BlockActionOptionState()); // Emit a new state to trigger UI update - return; - } - - final blockIds = nodes.map((e) => e.id); - final links = blockIds.map( - (e) => ShareConstants.buildShareUrl( - workspaceId: workspaceId, - viewId: viewId, - blockId: e, - ), - ); - - await getIt().setData( - ClipboardServiceData(plainText: links.join('\n')), - ); - - emit(BlockActionOptionState()); // Emit a new state to trigger UI update - } - - static Future turnIntoBlock( - String type, - Node node, - EditorState editorState, { - int? level, - String? currentViewId, - bool keepSelection = false, - }) async { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - // Notify the transaction service that the next apply is from turn into action - EditorNotification.turnInto().post(); - - final toType = type; - - // only handle the node in the same depth - final selectedNodes = editorState - .getNodesInSelection(selection.normalized) - .where((e) => e.path.length == node.path.length) - .toList(); - Log.info('turnIntoBlock selectedNodes $selectedNodes'); - - // try to turn into a single toggle heading block - if (await turnIntoSingleToggleHeading( - type: toType, - selectedNodes: selectedNodes, - level: level, - editorState: editorState, - afterSelection: keepSelection ? selection : null, - )) { - return true; - } - - // try to turn into a page block - if (currentViewId != null && - await turnIntoPage( - type: toType, - selectedNodes: selectedNodes, - selection: selection, - currentViewId: currentViewId, - editorState: editorState, - )) { - return true; - } - - final insertedNode = []; - for (final node in selectedNodes) { - Log.info('Turn into block: from ${node.type} to $type'); - - Node afterNode = node.copyWith( - type: type, - attributes: { - if (toType == HeadingBlockKeys.type) HeadingBlockKeys.level: level, - if (toType == ToggleListBlockKeys.type) - ToggleListBlockKeys.level: level, - if (toType == TodoListBlockKeys.type) - TodoListBlockKeys.checked: false, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: (node.delta ?? Delta()).toJson(), - }, - ); - - // heading block should not have children - if ([HeadingBlockKeys.type].contains(toType)) { - afterNode = afterNode.copyWith(children: []); - afterNode = await _handleSubPageNode(afterNode, node); - insertedNode.add(afterNode); - insertedNode.addAll(node.children.map((e) => e.deepCopy())); - } else if (!EditorOptionActionType.turnInto.supportTypes - .contains(node.type)) { - afterNode = node.deepCopy(); - insertedNode.add(afterNode); - } else { - afterNode = await _handleSubPageNode(afterNode, node); - insertedNode.add(afterNode); - } - } - - final transaction = editorState.transaction; - transaction.insertNodes( - node.path, - insertedNode, - ); - transaction.deleteNodes(selectedNodes); - if (keepSelection) transaction.afterSelection = selection; - await editorState.apply(transaction); - - return true; - } - - /// Takes the new [Node] and the Node which is a SubPageBlock. - /// - /// Returns the altered [Node] with the delta as the Views' name. - /// - static Future _handleSubPageNode(Node node, Node subPageNode) async { - if (subPageNode.type != SubPageBlockKeys.type) { - return node; - } - - final delta = await _deltaFromSubPageNode(subPageNode); - return node.copyWith( - attributes: { - ...node.attributes, - blockComponentDelta: (delta ?? Delta()).toJson(), - }, - ); - } - - /// Returns the [Delta] from a SubPage [Node], where the - /// [Delta] is the views' name. - /// - static Future _deltaFromSubPageNode(Node node) async { - if (node.type != SubPageBlockKeys.type) { - return null; - } - - final viewId = node.attributes[SubPageBlockKeys.viewId]; - final viewOrFailure = await ViewBackendService.getView(viewId); - final view = viewOrFailure.toNullable(); - if (view != null) { - return Delta(operations: [TextInsert(view.name)]); - } - - Log.error("Failed to get view by id($viewId)"); - return null; - } - - // turn a single node into toggle heading block - // 1. find the sibling nodes after the selected node until - // meet the first node that contains level and its value is greater or equal to the level - // 2. move the found nodes in the selected node - // - // example: - // Toggle Heading 1 <- selected node - // - bulleted item 1 - // - bulleted item 2 - // - bulleted item 3 - // Heading 1 - // - paragraph 1 - // - paragraph 2 - // when turning "Toggle Heading 1" into toggle heading, the bulleted items will be moved into the toggle heading - static Future turnIntoSingleToggleHeading({ - required String type, - required List selectedNodes, - required EditorState editorState, - int? level, - Delta? delta, - Selection? afterSelection, - }) async { - // only support turn a single node into toggle heading block - if (type != ToggleListBlockKeys.type || - selectedNodes.length != 1 || - level == null) { - return false; - } - - // find the sibling nodes after the selected node until - final insertedNodes = []; - final node = selectedNodes.first; - Path path = node.path.next; - Node? nextNode = editorState.getNodeAtPath(path); - while (nextNode != null) { - if (nextNode.type == HeadingBlockKeys.type && - nextNode.attributes[HeadingBlockKeys.level] != null && - nextNode.attributes[HeadingBlockKeys.level]! <= level) { - break; - } - - if (nextNode.type == ToggleListBlockKeys.type && - nextNode.attributes[ToggleListBlockKeys.level] != null && - nextNode.attributes[ToggleListBlockKeys.level]! <= level) { - break; - } - - insertedNodes.add(nextNode); - - path = path.next; - nextNode = editorState.getNodeAtPath(path); - } - - Log.info('insertedNodes $insertedNodes'); - - Log.info( - 'Turn into block: from ${node.type} to $type', - ); - - Delta newDelta = delta ?? (node.delta ?? Delta()); - if (delta == null && node.type == SubPageBlockKeys.type) { - newDelta = await _deltaFromSubPageNode(node) ?? Delta(); - } - - final afterNode = node.copyWith( - type: type, - attributes: { - ToggleListBlockKeys.level: level, - ToggleListBlockKeys.collapsed: - node.attributes[ToggleListBlockKeys.collapsed] ?? false, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: newDelta.toJson(), - }, - children: [ - ...node.children.map((e) => e.deepCopy()), - ...insertedNodes.map((e) => e.deepCopy()), - ], - ); - - final transaction = editorState.transaction; - transaction.insertNode( - node.path, - afterNode, - ); - transaction.deleteNodes([ - node, - ...insertedNodes, - ]); - if (afterSelection != null) { - transaction.afterSelection = afterSelection; - } else if (insertedNodes.isNotEmpty) { - // select the blocks - transaction.afterSelection = Selection( - start: Position(path: node.path.child(0)), - end: Position(path: node.path.child(insertedNodes.length - 1)), - ); - } else { - transaction.afterSelection = transaction.beforeSelection; - } - await editorState.apply(transaction); - - return true; - } - - static Future turnIntoPage({ - required String type, - required List selectedNodes, - required Selection selection, - required String currentViewId, - required EditorState editorState, - }) async { - if (type != SubPageBlockKeys.type || selectedNodes.isEmpty) { - return false; - } - - if (selectedNodes.length == 1 && - selectedNodes.first.type == SubPageBlockKeys.type) { - return true; - } - - Log.info('Turn into page'); - - final insertedNodes = selectedNodes.map((n) => n.deepCopy()).toList(); - final document = Document.blank()..insert([0], insertedNodes); - final name = await _extractNameFromNodes(selectedNodes); - - final viewResult = await ViewBackendService.createView( - layoutType: ViewLayoutPB.Document, - name: name, - parentViewId: currentViewId, - initialDataBytes: - DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(), - ); - - await viewResult.fold( - (view) async { - final node = subPageNode(viewId: view.id); - final transaction = editorState.transaction; - transaction - ..insertNode(selection.normalized.start.path.next, node) - ..deleteNodes(selectedNodes) - ..afterSelection = Selection.collapsed(selection.normalized.start); - editorState.selectionType = SelectionType.inline; - - await editorState.apply(transaction); - - // We move views after applying transaction to avoid performing side-effects on the views - final viewIdsToMove = _extractChildViewIds(selectedNodes); - for (final viewId in viewIdsToMove) { - // Attempt to put back from trash if necessary - await TrashService.putback(viewId); - - await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: view.id, - prevViewId: null, - ); - } - }, - (err) async => Log.error(err), - ); - - return true; - } - - static Future _extractNameFromNodes(List? nodes) async { - if (nodes == null || nodes.isEmpty) { - return ''; - } - - String name = ''; - for (final node in nodes) { - if (name.length > 30) { - return name.substring(0, name.length > 30 ? 30 : name.length); - } - - if (node.delta != null) { - // "ABC [Hello world]" -> ABC Hello world - final textInserts = node.delta!.whereType(); - for (final ti in textInserts) { - if (ti.attributes?[MentionBlockKeys.mention] != null) { - // fetch the view name - final pageId = ti.attributes![MentionBlockKeys.mention] - [MentionBlockKeys.pageId]; - final viewOrFailure = await ViewBackendService.getView(pageId); - - final view = viewOrFailure.toNullable(); - if (view == null) { - Log.error('Failed to fetch view with id: $pageId'); - continue; - } - - name += view.name; - } else { - name += ti.data!.toString(); - } - } - - if (name.isNotEmpty) { - break; - } - } - - if (node.children.isNotEmpty) { - final n = await _extractNameFromNodes(node.children); - if (n.isNotEmpty) { - name = n; - break; - } - } - } - - return name.substring(0, name.length > 30 ? 30 : name.length); - } - - static List _extractChildViewIds(List nodes) { - final List viewIds = []; - for (final node in nodes) { - if (node.type == SubPageBlockKeys.type) { - final viewId = node.attributes[SubPageBlockKeys.viewId]; - viewIds.add(viewId); - } - - if (node.children.isNotEmpty) { - viewIds.addAll(_extractChildViewIds(node.children)); - } - - if (node.delta == null || node.delta!.isEmpty) { - continue; - } - - final textInserts = node.delta!.whereType(); - for (final ti in textInserts) { - final Map? mention = - ti.attributes?[MentionBlockKeys.mention]; - if (mention != null && - mention[MentionBlockKeys.type] == MentionType.childPage.name) { - final String? viewId = mention[MentionBlockKeys.pageId]; - if (viewId != null) { - viewIds.add(viewId); - } - } - } - } - - return viewIds; - } - - Selection? calculateTurnIntoSelection( - Node selectedNode, - Selection? beforeSelection, - ) { - final path = selectedNode.path; - final selection = Selection.collapsed(Position(path: path)); - - // if the previous selection is null or the start path is not in the same level as the current block path, - // then update the selection with the current block path - // for example,'|' means the selection, - // case 1: collapsed selection - // - bulleted item 1 - // - bulleted |item 2 - // when clicking the bulleted item 1, the bulleted item 1 path should be selected - // case 2: not collapsed selection - // - bulleted item 1 - // - bulleted |item 2 - // - bulleted |item 3 - // when clicking the bulleted item 1, the bulleted item 1 path should be selected - if (beforeSelection == null || - beforeSelection.start.path.length != path.length || - !path.inSelection(beforeSelection)) { - return selection; - } - // if the beforeSelection start with the current block, - // then updating the selection with the beforeSelection that may contains multiple blocks - return beforeSelection; - } - - Future _setToPageWidth(Node node) async { - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - await editorState.setColumnWidthToPageWidth(tableNode: node); - } - - Future _distributeColumnsEvenly(Node node) async { - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - await editorState.distributeColumnWidthToPageWidth(tableNode: node); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart deleted file mode 100644 index 7bc1fba8d9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart +++ /dev/null @@ -1,188 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -import 'draggable_option_button_feedback.dart'; -import 'option_button.dart'; - -// this flag is used to disable the tooltip of the block when it is dragged -ValueNotifier isDraggingAppFlowyEditorBlock = ValueNotifier(false); - -class DraggableOptionButton extends StatefulWidget { - const DraggableOptionButton({ - super.key, - required this.controller, - required this.editorState, - required this.blockComponentContext, - required this.blockComponentBuilder, - }); - - final PopoverController controller; - final EditorState editorState; - final BlockComponentContext blockComponentContext; - final Map blockComponentBuilder; - - @override - State createState() => _DraggableOptionButtonState(); -} - -class _DraggableOptionButtonState extends State { - late Node node; - late BlockComponentContext blockComponentContext; - - Offset? globalPosition; - - @override - void initState() { - super.initState(); - - // copy the node to avoid the node in document being updated - node = widget.blockComponentContext.node.deepCopy(); - } - - @override - void dispose() { - node.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Draggable( - data: node, - onDragStarted: _onDragStart, - onDragUpdate: _onDragUpdate, - onDragEnd: _onDragEnd, - feedback: DraggleOptionButtonFeedback( - controller: widget.controller, - editorState: widget.editorState, - blockComponentContext: widget.blockComponentContext, - blockComponentBuilder: widget.blockComponentBuilder, - ), - child: OptionButton( - isDragging: isDraggingAppFlowyEditorBlock, - controller: widget.controller, - editorState: widget.editorState, - blockComponentContext: widget.blockComponentContext, - ), - ); - } - - void _onDragStart() { - EditorNotification.dragStart().post(); - isDraggingAppFlowyEditorBlock.value = true; - widget.editorState.selectionService.removeDropTarget(); - } - - void _onDragUpdate(DragUpdateDetails details) { - isDraggingAppFlowyEditorBlock.value = true; - - final offset = details.globalPosition; - - widget.editorState.selectionService.renderDropTargetForOffset( - offset, - interceptor: (context, targetNode) { - // if the cursor node is in a columns block or a column block, - // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.columnParent; - if (parentColumnNode != null) { - final position = getDragAreaPosition( - context, - targetNode, - offset, - ); - - if (position != null && position.$2 == HorizontalPosition.right) { - return parentColumnNode; - } - - if (position != null && - position.$2 == HorizontalPosition.left && - position.$1 == VerticalPosition.middle) { - return parentColumnNode; - } - } - - // return simple table block if the target node is in a simple table block - final parentSimpleTableNode = targetNode.parentTableNode; - if (parentSimpleTableNode != null) { - return parentSimpleTableNode; - } - - return targetNode; - }, - builder: (context, data) { - return VisualDragArea( - editorState: widget.editorState, - data: data, - dragNode: widget.blockComponentContext.node, - ); - }, - ); - - globalPosition = details.globalPosition; - - // auto scroll the page when the drag position is at the edge of the screen - widget.editorState.scrollService?.startAutoScroll( - details.localPosition, - ); - } - - void _onDragEnd(DraggableDetails details) { - isDraggingAppFlowyEditorBlock.value = false; - - widget.editorState.selectionService.removeDropTarget(); - - if (globalPosition == null) { - return; - } - - final data = widget.editorState.selectionService.getDropTargetRenderData( - globalPosition!, - interceptor: (context, targetNode) { - // if the cursor node is in a columns block or a column block, - // we will return the node's parent instead to support dragging a node to the inside of a columns block or a column block. - final parentColumnNode = targetNode.columnParent; - if (parentColumnNode != null) { - final position = getDragAreaPosition( - context, - targetNode, - globalPosition!, - ); - - if (position != null && position.$2 == HorizontalPosition.right) { - return parentColumnNode; - } - - if (position != null && - position.$2 == HorizontalPosition.left && - position.$1 == VerticalPosition.middle) { - return parentColumnNode; - } - } - - // return simple table block if the target node is in a simple table block - final parentSimpleTableNode = targetNode.parentTableNode; - if (parentSimpleTableNode != null) { - return parentSimpleTableNode; - } - - return targetNode; - }, - ); - - dragToMoveNode( - context, - node: widget.blockComponentContext.node, - acceptedPath: data?.cursorNode?.path, - dragOffset: globalPosition!, - ).then((_) { - EditorNotification.dragEnd().post(); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button_feedback.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button_feedback.dart deleted file mode 100644 index 0b6c89599a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button_feedback.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class DraggleOptionButtonFeedback extends StatefulWidget { - const DraggleOptionButtonFeedback({ - super.key, - required this.controller, - required this.editorState, - required this.blockComponentContext, - required this.blockComponentBuilder, - }); - - final PopoverController controller; - final EditorState editorState; - final BlockComponentContext blockComponentContext; - final Map blockComponentBuilder; - - @override - State createState() => - _DraggleOptionButtonFeedbackState(); -} - -class _DraggleOptionButtonFeedbackState - extends State { - late Node node; - late BlockComponentContext blockComponentContext; - - @override - void initState() { - super.initState(); - - _setupLockComponentContext(); - widget.blockComponentContext.node.addListener(_updateBlockComponentContext); - } - - @override - void dispose() { - widget.blockComponentContext.node - .removeListener(_updateBlockComponentContext); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final maxWidth = (widget.editorState.renderBox?.size.width ?? - MediaQuery.of(context).size.width) * - 0.8; - - return Opacity( - opacity: 0.7, - child: Material( - color: Colors.transparent, - child: Container( - constraints: BoxConstraints( - maxWidth: maxWidth, - ), - child: IntrinsicHeight( - child: Provider.value( - value: widget.editorState, - child: _buildBlock(), - ), - ), - ), - ), - ); - } - - Widget _buildBlock() { - final node = widget.blockComponentContext.node; - final builder = widget.blockComponentBuilder[node.type]; - if (builder == null) { - return const SizedBox.shrink(); - } - - const unsupportedRenderBlockTypes = [ - TableBlockKeys.type, - CustomImageBlockKeys.type, - MultiImageBlockKeys.type, - FileBlockKeys.type, - DatabaseBlockKeys.boardType, - DatabaseBlockKeys.calendarType, - DatabaseBlockKeys.gridType, - ]; - - if (unsupportedRenderBlockTypes.contains(node.type)) { - // unable to render table block without provider/context - // render a placeholder instead - return Container( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - decoration: BoxDecoration( - color: Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(8), - ), - child: FlowyText(node.type.replaceAll('_', ' ').capitalize()), - ); - } - - return IntrinsicHeight( - child: MultiProvider( - providers: [ - Provider.value(value: widget.editorState), - Provider.value(value: getIt()), - ], - child: builder.build(blockComponentContext), - ), - ); - } - - void _updateBlockComponentContext() { - setState(() => _setupLockComponentContext()); - } - - void _setupLockComponentContext() { - node = widget.blockComponentContext.node.deepCopy(); - blockComponentContext = BlockComponentContext( - widget.blockComponentContext.buildContext, - node, - ); - } -} - -class _OptionButton extends StatefulWidget { - const _OptionButton({ - required this.controller, - required this.editorState, - required this.blockComponentContext, - required this.isDragging, - }); - - final PopoverController controller; - final EditorState editorState; - final BlockComponentContext blockComponentContext; - final ValueNotifier isDragging; - - @override - State<_OptionButton> createState() => _OptionButtonState(); -} - -const _interceptorKey = 'document_option_button_interceptor'; - -class _OptionButtonState extends State<_OptionButton> { - late final gestureInterceptor = SelectionGestureInterceptor( - key: _interceptorKey, - canTap: (details) => !_isTapInBounds(details.globalPosition), - ); - - // the selection will be cleared when tap the option button - // so we need to restore the selection after tap the option button - Selection? beforeSelection; - RenderBox? get renderBox => context.findRenderObject() as RenderBox?; - - @override - void initState() { - super.initState(); - - widget.editorState.service.selectionService.registerGestureInterceptor( - gestureInterceptor, - ); - } - - @override - void dispose() { - widget.editorState.service.selectionService.unregisterGestureInterceptor( - _interceptorKey, - ); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.isDragging, - builder: (context, isDragging, child) { - return BlockActionButton( - svg: FlowySvgs.drag_element_s, - showTooltip: !isDragging, - richMessage: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.document_plugins_optionAction_drag.tr(), - style: context.tooltipTextStyle(), - ), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_toMove.tr(), - style: context.tooltipTextStyle(), - ), - const TextSpan(text: '\n'), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_click.tr(), - style: context.tooltipTextStyle(), - ), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), - style: context.tooltipTextStyle(), - ), - ], - ), - onPointerDown: () { - if (widget.editorState.selection != null) { - beforeSelection = widget.editorState.selection; - } - }, - onTap: () { - if (widget.editorState.selection != null) { - beforeSelection = widget.editorState.selection; - } - - widget.controller.show(); - - // update selection - _updateBlockSelection(); - }, - ); - }, - ); - } - - void _updateBlockSelection() { - if (beforeSelection == null) { - final path = widget.blockComponentContext.node.path; - final selection = Selection.collapsed( - Position(path: path), - ); - widget.editorState.updateSelectionWithReason( - selection, - customSelectionType: SelectionType.block, - ); - } else { - widget.editorState.updateSelectionWithReason( - beforeSelection!, - customSelectionType: SelectionType.block, - ); - } - } - - bool _isTapInBounds(Offset offset) { - if (renderBox == null) { - return false; - } - - final localPosition = renderBox!.globalToLocal(offset); - final result = renderBox!.paintBounds.contains(localPosition); - if (result) { - beforeSelection = widget.editorState.selection; - } else { - beforeSelection = null; - } - - return result; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart deleted file mode 100644 index fa6b771b74..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart +++ /dev/null @@ -1,136 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.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 _interceptorKey = 'document_option_button_interceptor'; - -class OptionButton extends StatefulWidget { - const OptionButton({ - super.key, - required this.controller, - required this.editorState, - required this.blockComponentContext, - required this.isDragging, - }); - - final PopoverController controller; - final EditorState editorState; - final BlockComponentContext blockComponentContext; - final ValueNotifier isDragging; - - @override - State createState() => _OptionButtonState(); -} - -class _OptionButtonState extends State { - late final registerKey = - _interceptorKey + widget.blockComponentContext.node.id; - late final gestureInterceptor = SelectionGestureInterceptor( - key: registerKey, - canTap: (details) => !_isTapInBounds(details.globalPosition), - ); - - // the selection will be cleared when tap the option button - // so we need to restore the selection after tap the option button - Selection? beforeSelection; - RenderBox? get renderBox => context.findRenderObject() as RenderBox?; - - @override - void initState() { - super.initState(); - - widget.editorState.service.selectionService.registerGestureInterceptor( - gestureInterceptor, - ); - } - - @override - void dispose() { - widget.editorState.service.selectionService.unregisterGestureInterceptor( - registerKey, - ); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: widget.isDragging, - builder: (context, isDragging, child) { - return BlockActionButton( - svg: FlowySvgs.drag_element_s, - showTooltip: !isDragging, - richMessage: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.document_plugins_optionAction_drag.tr(), - style: context.tooltipTextStyle(), - ), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_toMove.tr(), - style: context.tooltipTextStyle(), - ), - const TextSpan(text: '\n'), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_click.tr(), - style: context.tooltipTextStyle(), - ), - TextSpan( - text: LocaleKeys.document_plugins_optionAction_toOpenMenu.tr(), - style: context.tooltipTextStyle(), - ), - ], - ), - onTap: () { - final selection = widget.editorState.selection; - if (selection != null) { - beforeSelection = selection.normalized; - } - - widget.controller.show(); - - // update selection - _updateBlockSelection(context); - }, - ); - }, - ); - } - - void _updateBlockSelection(BuildContext context) { - final cubit = context.read(); - final selection = cubit.calculateTurnIntoSelection( - widget.blockComponentContext.node, - beforeSelection, - ); - - widget.editorState.updateSelectionWithReason( - selection, - customSelectionType: SelectionType.block, - ); - } - - bool _isTapInBounds(Offset offset) { - final renderBox = this.renderBox; - if (renderBox == null) { - return false; - } - - final localPosition = renderBox.globalToLocal(offset); - final result = renderBox.paintBounds.contains(localPosition); - if (result) { - beforeSelection = widget.editorState.selection?.normalized; - } else { - beforeSelection = null; - } - - return result; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart deleted file mode 100644 index ca99491b94..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/util.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -enum HorizontalPosition { left, center, right } - -enum VerticalPosition { top, middle, bottom } - -List nodeTypesThatCanContainChildNode = [ - ParagraphBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - QuoteBlockKeys.type, - TodoListBlockKeys.type, - ToggleListBlockKeys.type, -]; - -Future dragToMoveNode( - BuildContext context, { - required Node node, - required Offset dragOffset, - Path? acceptedPath, -}) async { - if (acceptedPath == null) { - Log.info('acceptedPath is null'); - return; - } - - final editorState = context.read(); - final targetNode = editorState.getNodeAtPath(acceptedPath); - if (targetNode == null) { - Log.info('targetNode is null'); - return; - } - - if (shouldIgnoreDragTarget( - editorState: editorState, - dragNode: node, - targetPath: acceptedPath, - )) { - Log.info('Drop ignored: node($node, ${node.path}), path($acceptedPath)'); - return; - } - - final position = getDragAreaPosition(context, targetNode, dragOffset); - if (position == null) { - Log.info('position is null'); - return; - } - - final (verticalPosition, horizontalPosition, _) = position; - Path newPath = targetNode.path; - - // if the horizontal position is right, creating a column block to contain the target node and the drag node - if (horizontalPosition == HorizontalPosition.right) { - // 1. if the targetNode is a column block, it means we should create a column block to contain the node and insert the column node to the target node's parent - // 2. if the targetNode is not a column block, it means we should create a columns block to contain the target node and the drag node - final transaction = editorState.transaction; - final targetNodeParent = targetNode.columnsParent; - - if (targetNodeParent != null) { - final length = targetNodeParent.children.length; - final ratios = targetNodeParent.children - .map( - (e) => - e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length, - ) - .map((e) => e * length / (length + 1)) - .toList(); - - final columnNode = simpleColumnNode( - children: [node.deepCopy()], - ratio: 1.0 / (length + 1), - ); - for (final (index, column) in targetNodeParent.children.indexed) { - transaction.updateNode(column, { - ...column.attributes, - SimpleColumnBlockKeys.ratio: ratios[index], - }); - } - - transaction.insertNode(targetNode.path.next, columnNode); - transaction.deleteNode(node); - } else { - final columnsNode = simpleColumnsNode( - children: [ - simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), - simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), - ], - ); - - transaction.insertNode(newPath, columnsNode); - transaction.deleteNode(targetNode); - transaction.deleteNode(node); - } - - if (transaction.operations.isNotEmpty) { - await editorState.apply(transaction); - } - return; - } else if (horizontalPosition == HorizontalPosition.left && - verticalPosition == VerticalPosition.middle) { - // 1. if the target node is a column block, we should create a column block to contain the node and insert the column node to the target node's parent - // 2. if the target node is not a column block, we should create a columns block to contain the target node and the drag node - final transaction = editorState.transaction; - final targetNodeParent = targetNode.columnsParent; - if (targetNodeParent != null) { - // find the previous sibling node of the target node - final length = targetNodeParent.children.length; - final ratios = targetNodeParent.children - .map( - (e) => - e.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length, - ) - .map((e) => e * length / (length + 1)) - .toList(); - final columnNode = simpleColumnNode( - children: [node.deepCopy()], - ratio: 1.0 / (length + 1), - ); - - for (final (index, column) in targetNodeParent.children.indexed) { - transaction.updateNode(column, { - ...column.attributes, - SimpleColumnBlockKeys.ratio: ratios[index], - }); - } - - transaction.insertNode(targetNode.path.previous, columnNode); - transaction.deleteNode(node); - } else { - final columnsNode = simpleColumnsNode( - children: [ - simpleColumnNode(children: [node.deepCopy()], ratio: 0.5), - simpleColumnNode(children: [targetNode.deepCopy()], ratio: 0.5), - ], - ); - - transaction.insertNode(newPath, columnsNode); - transaction.deleteNode(targetNode); - transaction.deleteNode(node); - } - - if (transaction.operations.isNotEmpty) { - await editorState.apply(transaction); - } - return; - } - // Determine the new path based on drop position - // For VerticalPosition.top, we keep the target node's path - if (verticalPosition == VerticalPosition.bottom) { - if (horizontalPosition == HorizontalPosition.left) { - newPath = newPath.next; - } else if (horizontalPosition == HorizontalPosition.center && - nodeTypesThatCanContainChildNode.contains(targetNode.type)) { - // check if the target node can contain a child node - newPath = newPath.child(0); - } - } - - // Check if the drop should be ignored - if (shouldIgnoreDragTarget( - editorState: editorState, - dragNode: node, - targetPath: newPath, - )) { - Log.info( - 'Drop ignored: node($node, ${node.path}), path($acceptedPath)', - ); - return; - } - - Log.info('Moving node($node, ${node.path}) to path($newPath)'); - - final transaction = editorState.transaction; - transaction.insertNode(newPath, node.deepCopy()); - transaction.deleteNode(node); - await editorState.apply(transaction); -} - -(VerticalPosition, HorizontalPosition, Rect)? getDragAreaPosition( - BuildContext context, - Node dragTargetNode, - Offset dragOffset, -) { - final selectable = dragTargetNode.selectable; - final renderBox = selectable?.context.findRenderObject() as RenderBox?; - if (selectable == null || renderBox == null) { - return null; - } - - // disable the table cell block - if (dragTargetNode.parent?.type == TableCellBlockKeys.type) { - return null; - } - - final globalBlockOffset = renderBox.localToGlobal(Offset.zero); - final globalBlockRect = globalBlockOffset & renderBox.size; - - // Check if the dragOffset is within the globalBlockRect - final isInside = globalBlockRect.contains(dragOffset); - - if (!isInside) { - Log.info( - 'the drag offset is not inside the block, dragOffset($dragOffset), globalBlockRect($globalBlockRect)', - ); - return null; - } - - // Determine the relative position - HorizontalPosition horizontalPosition = HorizontalPosition.left; - VerticalPosition verticalPosition; - - // | ----------------------------- block ----------------------------- | - // | 1. -- 88px --| 2. ---------------------------- | 3. ---- 1/5 ---- | - // 1. drag the node under the block as a sibling node - // 2. drag the node inside the block as a child node - // 3. create a column block to contain the node and the drag node - - // Horizontal position, please refer to the diagram above - // 88px is a hardcoded value, it can be changed based on the project's design - if (dragOffset.dx < globalBlockRect.left + 88) { - horizontalPosition = HorizontalPosition.left; - } else if (dragOffset.dx > globalBlockRect.right * 4.0 / 5.0) { - horizontalPosition = HorizontalPosition.right; - } else if (nodeTypesThatCanContainChildNode.contains(dragTargetNode.type)) { - horizontalPosition = HorizontalPosition.center; - } - - // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is top - // | ----------------------------- block ----------------------------- | <- if the drag position is in this area, the vertical position is middle - // | ----------------------------------------------------------------- | <- if the drag position is in this area, the vertical position is bottom - - // Vertical position - final heightThird = globalBlockRect.height / 3; - if (dragOffset.dy < globalBlockRect.top + heightThird) { - verticalPosition = VerticalPosition.top; - } else if (dragOffset.dy < globalBlockRect.top + heightThird * 2) { - verticalPosition = VerticalPosition.middle; - } else { - verticalPosition = VerticalPosition.bottom; - } - - return (verticalPosition, horizontalPosition, globalBlockRect); -} - -bool shouldIgnoreDragTarget({ - required EditorState editorState, - required Node dragNode, - required Path? targetPath, -}) { - if (targetPath == null) { - return true; - } - - if (dragNode.path.equals(targetPath)) { - return true; - } - - if (dragNode.path.isAncestorOf(targetPath)) { - return true; - } - - final targetNode = editorState.getNodeAtPath(targetPath); - if (targetNode != null && - targetNode.isInTable && - targetNode.type != SimpleTableBlockKeys.type) { - return true; - } - - return false; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart deleted file mode 100644 index 2be8710a8a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/visual_drag_area.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'util.dart'; - -class VisualDragArea extends StatelessWidget { - const VisualDragArea({ - super.key, - required this.data, - required this.dragNode, - required this.editorState, - }); - - final DragAreaBuilderData data; - final Node dragNode; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final targetNode = data.targetNode; - - final ignore = shouldIgnoreDragTarget( - editorState: editorState, - dragNode: dragNode, - targetPath: targetNode.path, - ); - if (ignore) { - return const SizedBox.shrink(); - } - - final selectable = targetNode.selectable; - final renderBox = selectable?.context.findRenderObject() as RenderBox?; - if (selectable == null || renderBox == null) { - return const SizedBox.shrink(); - } - - final position = getDragAreaPosition( - context, - targetNode, - data.dragOffset, - ); - - if (position == null) { - return const SizedBox.shrink(); - } - - final (verticalPosition, horizontalPosition, globalBlockRect) = position; - - // 44 is the width of the drag indicator - const indicatorWidth = 44.0; - final width = globalBlockRect.width - indicatorWidth; - - Widget child = Container( - height: 2, - width: max(width, 0.0), - color: Theme.of(context).colorScheme.primary, - ); - - // if the horizontal position is right, we need to show the indicator on the right side of the target node - // which represent moving the target node and drag node inside the column block. - if (horizontalPosition == HorizontalPosition.left && - verticalPosition == VerticalPosition.middle) { - return Positioned( - top: globalBlockRect.top, - height: globalBlockRect.height, - left: globalBlockRect.left + indicatorWidth, - child: Container( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - ); - } - - if (horizontalPosition == HorizontalPosition.right) { - return Positioned( - top: globalBlockRect.top, - height: globalBlockRect.height, - left: globalBlockRect.right - 2, - child: Container( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - ); - } - - // If the horizontal position is center, we need to show two indicators - //which represent moving the block as the child of the target node. - if (horizontalPosition == HorizontalPosition.center) { - const breakWidth = 22.0; - const padding = 8.0; - child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 2, - width: breakWidth, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: padding), - Container( - height: 2, - width: width - breakWidth - padding, - color: Theme.of(context).colorScheme.primary, - ), - ], - ); - } - - return Positioned( - top: verticalPosition == VerticalPosition.top - ? globalBlockRect.top - : globalBlockRect.bottom, - // 44 is the width of the drag indicator - left: globalBlockRect.left + 44, - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart index a04190f8af..b80bb034a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -7,7 +7,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; /// The ... button shows on the top right corner of a block. /// @@ -36,7 +35,7 @@ class MobileBlockActionButtons extends StatelessWidget { @override Widget build(BuildContext context) { - if (!UniversalPlatform.isMobile) { + if (!PlatformExtension.isMobile) { return child; } @@ -91,7 +90,7 @@ class MobileBlockActionButtons extends StatelessWidget { case BlockActionBottomSheetType.duplicate: transaction.insertNode( node.path.next, - node.deepCopy(), + node.copyWith(), ); break; case BlockActionBottomSheetType.insertAbove: @@ -110,6 +109,7 @@ class MobileBlockActionButtons extends StatelessWidget { ), ); break; + default: } if (transaction.operations.isNotEmpty) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/align_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/align_option_action.dart deleted file mode 100644 index efa1c3e15c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/align_option_action.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -enum OptionAlignType { - left, - center, - right; - - static OptionAlignType fromString(String? value) { - switch (value) { - case 'left': - return OptionAlignType.left; - case 'center': - return OptionAlignType.center; - case 'right': - return OptionAlignType.right; - default: - return OptionAlignType.center; - } - } - - FlowySvgData get svg { - switch (this) { - case OptionAlignType.left: - return FlowySvgs.table_align_left_s; - case OptionAlignType.center: - return FlowySvgs.table_align_center_s; - case OptionAlignType.right: - return FlowySvgs.table_align_right_s; - } - } - - String get description { - switch (this) { - case OptionAlignType.left: - return LocaleKeys.document_plugins_optionAction_left.tr(); - case OptionAlignType.center: - return LocaleKeys.document_plugins_optionAction_center.tr(); - case OptionAlignType.right: - return LocaleKeys.document_plugins_optionAction_right.tr(); - } - } -} - -class AlignOptionAction extends PopoverActionCell { - AlignOptionAction({ - required this.editorState, - }); - - final EditorState editorState; - - @override - Widget? leftIcon(Color iconColor) { - return FlowySvg( - align.svg, - size: const Size.square(18), - ); - } - - @override - String get name { - return LocaleKeys.document_plugins_optionAction_align.tr(); - } - - @override - PopoverActionCellBuilder get builder => - (context, parentController, controller) { - final selection = editorState.selection?.normalized; - if (selection == null) { - return const SizedBox.shrink(); - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return const SizedBox.shrink(); - } - final children = buildAlignOptions(context, (align) async { - await onAlignChanged(align); - controller.close(); - parentController.close(); - }); - return IntrinsicHeight( - child: IntrinsicWidth( - child: Column( - children: children, - ), - ), - ); - }; - - List buildAlignOptions( - BuildContext context, - void Function(OptionAlignType) onTap, - ) { - return OptionAlignType.values.map((e) => OptionAlignWrapper(e)).map((e) { - final leftIcon = e.leftIcon(Theme.of(context).colorScheme.onSurface); - final rightIcon = e.rightIcon(Theme.of(context).colorScheme.onSurface); - return HoverButton( - onTap: () => onTap(e.inner), - itemHeight: ActionListSizes.itemHeight, - leftIcon: SizedBox( - width: 16, - height: 16, - child: leftIcon, - ), - name: e.name, - rightIcon: rightIcon, - ); - }).toList(); - } - - OptionAlignType get align { - final selection = editorState.selection; - if (selection == null) { - return OptionAlignType.center; - } - final node = editorState.getNodeAtPath(selection.start.path); - final align = node?.type == SimpleTableBlockKeys.type - ? node?.tableAlign.key - : node?.attributes[blockComponentAlign]; - return OptionAlignType.fromString(align); - } - - Future onAlignChanged(OptionAlignType align) async { - if (align == this.align) { - return; - } - final selection = editorState.selection; - if (selection == null) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return; - } - // the align attribute for simple table is not same as the align type, - // so we need to convert the align type to the align attribute - if (node.type == SimpleTableBlockKeys.type) { - await editorState.updateTableAlign( - tableNode: node, - align: TableAlign.fromString(align.name), - ); - } else { - final transaction = editorState.transaction; - transaction.updateNode(node, { - blockComponentAlign: align.name, - }); - await editorState.apply(transaction); - } - } -} - -class OptionAlignWrapper extends ActionCell { - OptionAlignWrapper(this.inner); - - final OptionAlignType inner; - - @override - Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg); - - @override - String get name => inner.description; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/color_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/color_option_action.dart deleted file mode 100644 index cb1ec36b56..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/color_option_action.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.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'; - -const optionActionColorDefaultColor = 'appflowy_theme_default_color'; - -class ColorOptionAction extends CustomActionCell { - ColorOptionAction({ - required this.editorState, - required this.mutex, - }); - - final EditorState editorState; - final PopoverController innerController = PopoverController(); - final PopoverMutex mutex; - - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - return ColorOptionButton( - editorState: editorState, - mutex: this.mutex, - controller: controller, - ); - } -} - -class ColorOptionButton extends StatefulWidget { - const ColorOptionButton({ - super.key, - required this.editorState, - required this.mutex, - required this.controller, - }); - - final EditorState editorState; - final PopoverMutex mutex; - final PopoverController controller; - - @override - State createState() => _ColorOptionButtonState(); -} - -class _ColorOptionButtonState extends State { - final PopoverController innerController = PopoverController(); - bool isOpen = false; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - asBarrier: true, - controller: innerController, - mutex: widget.mutex, - popupBuilder: (context) { - isOpen = true; - return _buildColorOptionMenu( - context, - widget.controller, - ); - }, - onClose: () => isOpen = false, - direction: PopoverDirection.rightWithCenterAligned, - animationDuration: Durations.short3, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - child: HoverButton( - itemHeight: ActionListSizes.itemHeight, - leftIcon: const FlowySvg( - FlowySvgs.color_format_m, - size: Size.square(15), - ), - name: LocaleKeys.document_plugins_optionAction_color.tr(), - onTap: () { - if (!isOpen) { - innerController.show(); - } - }, - ), - ); - } - - Widget _buildColorOptionMenu( - BuildContext context, - PopoverController controller, - ) { - final selection = widget.editorState.selection?.normalized; - if (selection == null) { - return const SizedBox.shrink(); - } - - final node = widget.editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return const SizedBox.shrink(); - } - - return _buildColorOptions(context, node, controller); - } - - Widget _buildColorOptions( - BuildContext context, - Node node, - PopoverController controller, - ) { - final selection = widget.editorState.selection?.normalized; - if (selection == null) { - return const SizedBox.shrink(); - } - final node = widget.editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return const SizedBox.shrink(); - } - final bgColor = node.attributes[blockComponentBackgroundColor] as String?; - final selectedColor = bgColor?.tryToColor(); - // get default background color for callout block from themeExtension - final defaultColor = node.type == CalloutBlockKeys.type - ? AFThemeExtension.of(context).calloutBGColor - : Colors.transparent; - final colors = [ - // reset to default background color - FlowyColorOption( - color: defaultColor, - i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(), - id: optionActionColorDefaultColor, - ), - ...FlowyTint.values.map( - (e) => FlowyColorOption( - color: e.color(context), - i18n: e.tintName(AppFlowyEditorL10n.current), - id: e.id, - ), - ), - ]; - - return FlowyColorPicker( - colors: colors, - selected: selectedColor, - border: Border.all( - color: AFThemeExtension.of(context).onBackground, - ), - onTap: (option, index) async { - final editorState = widget.editorState; - final transaction = editorState.transaction; - final selectionType = editorState.selectionType; - final selection = editorState.selection; - - // In multiple selection, we need to update all the nodes in the selection - if (selectionType == SelectionType.block && selection != null) { - final nodes = editorState.getNodesInSelection(selection.normalized); - for (final node in nodes) { - transaction.updateNode(node, { - blockComponentBackgroundColor: option.id, - }); - } - } else { - transaction.updateNode(node, { - blockComponentBackgroundColor: option.id, - }); - } - - await widget.editorState.apply(transaction); - - innerController.close(); - controller.close(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart deleted file mode 100644 index bc083cd617..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/depth_option_action.dart +++ /dev/null @@ -1,142 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -enum OptionDepthType { - h1(1, 'H1'), - h2(2, 'H2'), - h3(3, 'H3'), - h4(4, 'H4'), - h5(5, 'H5'), - h6(6, 'H6'); - - const OptionDepthType(this.level, this.description); - - final String description; - final int level; - - static OptionDepthType fromLevel(int? level) { - switch (level) { - case 1: - return OptionDepthType.h1; - case 2: - return OptionDepthType.h2; - case 3: - default: - return OptionDepthType.h3; - } - } -} - -class DepthOptionAction extends PopoverActionCell { - DepthOptionAction({ - required this.editorState, - }); - - final EditorState editorState; - - @override - Widget? leftIcon(Color iconColor) { - return FlowySvg( - OptionAction.depth.svg, - size: const Size.square(16), - ); - } - - @override - String get name => LocaleKeys.document_plugins_optionAction_depth.tr(); - - @override - PopoverActionCellBuilder get builder => - (context, parentController, controller) { - return DepthOptionMenu( - onTap: (depth) async { - await onDepthChanged(depth); - parentController.close(); - parentController.close(); - }, - ); - }; - - OptionDepthType depth(Node node) { - final level = node.attributes[OutlineBlockKeys.depth]; - return OptionDepthType.fromLevel(level); - } - - Future onDepthChanged(OptionDepthType depth) async { - final selection = editorState.selection; - final node = selection != null - ? editorState.getNodeAtPath(selection.start.path) - : null; - - if (node == null || depth == this.depth(node)) return; - - final transaction = editorState.transaction; - transaction.updateNode( - node, - {OutlineBlockKeys.depth: depth.level}, - ); - await editorState.apply(transaction); - } -} - -class DepthOptionMenu extends StatelessWidget { - const DepthOptionMenu({ - super.key, - required this.onTap, - }); - - final Future Function(OptionDepthType) onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 42, - child: Column( - mainAxisSize: MainAxisSize.min, - children: buildDepthOptions(context, onTap), - ), - ); - } - - List buildDepthOptions( - BuildContext context, - Future Function(OptionDepthType) onTap, - ) { - return OptionDepthType.values - .map((e) => OptionDepthWrapper(e)) - .map( - (e) => HoverButton( - onTap: () => onTap(e.inner), - itemHeight: ActionListSizes.itemHeight, - name: e.name, - ), - ) - .toList(); - } -} - -class OptionDepthWrapper extends ActionCell { - OptionDepthWrapper(this.inner); - - final OptionDepthType inner; - - @override - String get name => inner.description; -} - -class OptionActionWrapper extends ActionCell { - OptionActionWrapper(this.inner); - - final OptionAction inner; - - @override - Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg); - - @override - String get name => inner.description; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/divider_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/divider_option_action.dart deleted file mode 100644 index 75e4339b5e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/divider_option_action.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -class DividerOptionAction extends CustomActionCell { - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 4.0), - child: Divider( - height: 1.0, - thickness: 1.0, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart deleted file mode 100644 index 571cb4baa0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; -import 'package:easy_localization/easy_localization.dart' hide TextDirection; -import 'package:easy_localization/easy_localization.dart'; - -export 'align_option_action.dart'; -export 'color_option_action.dart'; -export 'depth_option_action.dart'; -export 'divider_option_action.dart'; -export 'turn_into_option_action.dart'; - -enum EditorOptionActionType { - turnInto, - color, - align, - depth; - - Set get supportTypes { - switch (this) { - case EditorOptionActionType.turnInto: - return { - ParagraphBlockKeys.type, - HeadingBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - TodoListBlockKeys.type, - ToggleListBlockKeys.type, - SubPageBlockKeys.type, - }; - case EditorOptionActionType.color: - return { - ParagraphBlockKeys.type, - HeadingBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - QuoteBlockKeys.type, - TodoListBlockKeys.type, - CalloutBlockKeys.type, - OutlineBlockKeys.type, - ToggleListBlockKeys.type, - }; - case EditorOptionActionType.align: - return { - ImageBlockKeys.type, - SimpleTableBlockKeys.type, - }; - case EditorOptionActionType.depth: - return { - OutlineBlockKeys.type, - }; - } - } -} - -enum OptionAction { - delete, - duplicate, - turnInto, - moveUp, - moveDown, - copyLinkToBlock, - - /// callout background color - color, - divider, - align, - - // Outline block - depth, - - // Simple table - setToPageWidth, - distributeColumnsEvenly; - - FlowySvgData get svg { - switch (this) { - case OptionAction.delete: - return FlowySvgs.trash_s; - case OptionAction.duplicate: - return FlowySvgs.copy_s; - case OptionAction.turnInto: - return FlowySvgs.turninto_s; - case OptionAction.moveUp: - return const FlowySvgData('editor/move_up'); - case OptionAction.moveDown: - return const FlowySvgData('editor/move_down'); - case OptionAction.color: - return const FlowySvgData('editor/color'); - case OptionAction.divider: - return const FlowySvgData('editor/divider'); - case OptionAction.align: - return FlowySvgs.m_aa_bulleted_list_s; - case OptionAction.depth: - return FlowySvgs.tag_s; - case OptionAction.copyLinkToBlock: - return FlowySvgs.share_tab_copy_s; - case OptionAction.setToPageWidth: - return FlowySvgs.table_set_to_page_width_s; - case OptionAction.distributeColumnsEvenly: - return FlowySvgs.table_distribute_columns_evenly_s; - } - } - - String get description { - switch (this) { - case OptionAction.delete: - return LocaleKeys.document_plugins_optionAction_delete.tr(); - case OptionAction.duplicate: - return LocaleKeys.document_plugins_optionAction_duplicate.tr(); - case OptionAction.turnInto: - return LocaleKeys.document_plugins_optionAction_turnInto.tr(); - case OptionAction.moveUp: - return LocaleKeys.document_plugins_optionAction_moveUp.tr(); - case OptionAction.moveDown: - return LocaleKeys.document_plugins_optionAction_moveDown.tr(); - case OptionAction.color: - return LocaleKeys.document_plugins_optionAction_color.tr(); - case OptionAction.align: - return LocaleKeys.document_plugins_optionAction_align.tr(); - case OptionAction.depth: - return LocaleKeys.document_plugins_optionAction_depth.tr(); - case OptionAction.copyLinkToBlock: - return LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(); - case OptionAction.divider: - throw UnsupportedError('Divider does not have description'); - case OptionAction.setToPageWidth: - return LocaleKeys - .document_plugins_simpleTable_moreActions_setToPageWidth - .tr(); - case OptionAction.distributeColumnsEvenly: - return LocaleKeys - .document_plugins_simpleTable_moreActions_distributeColumnsWidth - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart deleted file mode 100644 index c927fcf85f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option/turn_into_option_action.dart +++ /dev/null @@ -1,281 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; -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 TurnIntoOptionAction extends CustomActionCell { - TurnIntoOptionAction({ - required this.editorState, - required this.blockComponentBuilder, - required this.mutex, - }); - - final EditorState editorState; - final Map blockComponentBuilder; - final PopoverController innerController = PopoverController(); - final PopoverMutex mutex; - - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - return TurnInfoButton( - editorState: editorState, - blockComponentBuilder: blockComponentBuilder, - mutex: this.mutex, - ); - } -} - -class TurnInfoButton extends StatefulWidget { - const TurnInfoButton({ - super.key, - required this.editorState, - required this.blockComponentBuilder, - required this.mutex, - }); - - final EditorState editorState; - final Map blockComponentBuilder; - final PopoverMutex mutex; - - @override - State createState() => _TurnInfoButtonState(); -} - -class _TurnInfoButtonState extends State { - final PopoverController innerController = PopoverController(); - bool isOpen = false; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - asBarrier: true, - controller: innerController, - mutex: widget.mutex, - popupBuilder: (context) { - isOpen = true; - return BlocProvider( - create: (context) => BlockActionOptionCubit( - editorState: widget.editorState, - blockComponentBuilder: widget.blockComponentBuilder, - ), - child: BlocBuilder( - builder: (context, _) => _buildTurnIntoOptionMenu(context), - ), - ); - }, - onClose: () => isOpen = false, - direction: PopoverDirection.rightWithCenterAligned, - animationDuration: Durations.short3, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - child: HoverButton( - itemHeight: ActionListSizes.itemHeight, - // todo(lucas): replace the svg with the correct one - leftIcon: const FlowySvg(FlowySvgs.turninto_s), - name: LocaleKeys.document_plugins_optionAction_turnInto.tr(), - onTap: () { - if (!isOpen) { - innerController.show(); - } - }, - ), - ); - } - - Widget _buildTurnIntoOptionMenu(BuildContext context) { - final selection = widget.editorState.selection?.normalized; - // the selection may not be collapsed, for example, if a block contains some children, - // the selection will be the start from the current block and end at the last child block. - // we should take care of this case: - // converting a block that contains children to a heading block, - // we should move all the children under the heading block. - if (selection == null) { - return const SizedBox.shrink(); - } - - final node = widget.editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return const SizedBox.shrink(); - } - - return TurnIntoOptionMenu( - node: node, - hasNonSupportedTypes: _hasNonSupportedTypes(selection), - ); - } - - bool _hasNonSupportedTypes(Selection selection) { - final nodes = widget.editorState.getNodesInSelection(selection); - if (nodes.isEmpty) { - return false; - } - - for (final node in nodes) { - if (!EditorOptionActionType.turnInto.supportTypes.contains(node.type)) { - return true; - } - } - - return false; - } -} - -class TurnIntoOptionMenu extends StatelessWidget { - const TurnIntoOptionMenu({ - super.key, - required this.node, - required this.hasNonSupportedTypes, - }); - - final Node node; - - /// Signifies whether the selection contains [Node]s that are not supported, - /// these often do not have a [Delta], example could be [FileBlockComponent]. - /// - final bool hasNonSupportedTypes; - - @override - Widget build(BuildContext context) { - if (hasNonSupportedTypes) { - return buildItem( - pateItem, - textSuggestionItem, - context.read().editorState, - ); - } - - return _buildTurnIntoOptions(context, node); - } - - Widget _buildTurnIntoOptions(BuildContext context, Node node) { - final editorState = context.read().editorState; - SuggestionItem currentSuggestionItem = textSuggestionItem; - final List suggestionItems = suggestions.sublist(0, 4); - final List turnIntoItems = - suggestions.sublist(4, suggestions.length); - final textColor = Color(0xff99A1A8); - - void refreshSuggestions() { - final selection = editorState.selection; - if (selection == null || !selection.isSingle) return; - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.delta == null) return; - final nodeType = node.type; - SuggestionType? suggestionType; - if (nodeType == HeadingBlockKeys.type) { - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level == 1) { - suggestionType = SuggestionType.h1; - } else if (level == 2) { - suggestionType = SuggestionType.h2; - } else if (level == 3) { - suggestionType = SuggestionType.h3; - } - } else if (nodeType == ToggleListBlockKeys.type) { - final level = node.attributes[ToggleListBlockKeys.level]; - if (level == null) { - suggestionType = SuggestionType.toggle; - } else if (level == 1) { - suggestionType = SuggestionType.toggleH1; - } else if (level == 2) { - suggestionType = SuggestionType.toggleH2; - } else if (level == 3) { - suggestionType = SuggestionType.toggleH3; - } - } else { - suggestionType = nodeType2SuggestionType[nodeType]; - } - if (suggestionType == null) return; - suggestionItems.clear(); - turnIntoItems.clear(); - for (final item in suggestions) { - if (item.type.group == suggestionType.group && - item.type != suggestionType) { - suggestionItems.add(item); - } else { - turnIntoItems.add(item); - } - } - currentSuggestionItem = - suggestions.where((item) => item.type == suggestionType).first; - } - - refreshSuggestions(); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildSubTitle( - LocaleKeys.document_toolbar_suggestions.tr(), - textColor, - ), - ...List.generate(suggestionItems.length, (index) { - return buildItem( - suggestionItems[index], - currentSuggestionItem, - editorState, - ); - }), - buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor), - ...List.generate(turnIntoItems.length, (index) { - return buildItem( - turnIntoItems[index], - currentSuggestionItem, - editorState, - ); - }), - ], - ); - } - - Widget buildSubTitle(String text, Color color) { - return Container( - height: 32, - margin: EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.semibold( - text, - color: color, - figmaLineHeight: 16, - ), - ), - ); - } - - Widget buildItem( - SuggestionItem item, - SuggestionItem currentSuggestionItem, - EditorState state, - ) { - final isSelected = item.type == currentSuggestionItem.type; - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(item.svg), - iconPadding: 12, - text: FlowyText( - item.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () => item.onTap.call(state, false), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart new file mode 100644 index 0000000000..d5e99e13f8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -0,0 +1,422 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:styled_widget/styled_widget.dart'; + +const optionActionColorDefaultColor = 'appflowy_theme_default_color'; + +enum OptionAction { + delete, + duplicate, + turnInto, + moveUp, + moveDown, + + /// callout background color + color, + divider, + align, + depth; + + FlowySvgData get svg { + switch (this) { + case OptionAction.delete: + return FlowySvgs.delete_s; + case OptionAction.duplicate: + return FlowySvgs.copy_s; + case OptionAction.turnInto: + return const FlowySvgData('editor/turn_into'); + case OptionAction.moveUp: + return const FlowySvgData('editor/move_up'); + case OptionAction.moveDown: + return const FlowySvgData('editor/move_down'); + case OptionAction.color: + return const FlowySvgData('editor/color'); + case OptionAction.divider: + return const FlowySvgData('editor/divider'); + case OptionAction.align: + return FlowySvgs.m_aa_bulleted_list_s; + case OptionAction.depth: + return FlowySvgs.tag_s; + } + } + + String get description { + switch (this) { + case OptionAction.delete: + return LocaleKeys.document_plugins_optionAction_delete.tr(); + case OptionAction.duplicate: + return LocaleKeys.document_plugins_optionAction_duplicate.tr(); + case OptionAction.turnInto: + return LocaleKeys.document_plugins_optionAction_turnInto.tr(); + case OptionAction.moveUp: + return LocaleKeys.document_plugins_optionAction_moveUp.tr(); + case OptionAction.moveDown: + return LocaleKeys.document_plugins_optionAction_moveDown.tr(); + case OptionAction.color: + return LocaleKeys.document_plugins_optionAction_color.tr(); + case OptionAction.align: + return LocaleKeys.document_plugins_optionAction_align.tr(); + case OptionAction.depth: + return LocaleKeys.document_plugins_optionAction_depth.tr(); + case OptionAction.divider: + throw UnsupportedError('Divider does not have description'); + } + } +} + +enum OptionAlignType { + left, + center, + right; + + static OptionAlignType fromString(String? value) { + switch (value) { + case 'left': + return OptionAlignType.left; + case 'center': + return OptionAlignType.center; + case 'right': + return OptionAlignType.right; + default: + return OptionAlignType.center; + } + } + + FlowySvgData get svg { + switch (this) { + case OptionAlignType.left: + return FlowySvgs.align_left_s; + case OptionAlignType.center: + return FlowySvgs.align_center_s; + case OptionAlignType.right: + return FlowySvgs.align_right_s; + } + } + + String get description { + switch (this) { + case OptionAlignType.left: + return LocaleKeys.document_plugins_optionAction_left.tr(); + case OptionAlignType.center: + return LocaleKeys.document_plugins_optionAction_center.tr(); + case OptionAlignType.right: + return LocaleKeys.document_plugins_optionAction_right.tr(); + } + } +} + +enum OptionDepthType { + h1(1, 'H1'), + h2(2, 'H2'), + h3(3, 'H3'), + h4(4, 'H4'), + h5(5, 'H5'), + h6(6, 'H6'); + + const OptionDepthType(this.level, this.description); + + final String description; + final int level; + + static OptionDepthType fromLevel(int? level) { + switch (level) { + case 1: + return OptionDepthType.h1; + case 2: + return OptionDepthType.h2; + case 3: + default: + return OptionDepthType.h3; + } + } +} + +class DividerOptionAction extends CustomActionCell { + @override + Widget buildWithContext(BuildContext context, PopoverController controller) { + return const Divider( + height: 1.0, + thickness: 1.0, + ); + } +} + +class AlignOptionAction extends PopoverActionCell { + AlignOptionAction({ + required this.editorState, + }); + + final EditorState editorState; + + @override + Widget? leftIcon(Color iconColor) { + return FlowySvg( + align.svg, + size: const Size.square(12), + ).padding(all: 2.0); + } + + @override + String get name { + return LocaleKeys.document_plugins_optionAction_align.tr(); + } + + @override + PopoverActionCellBuilder get builder => + (context, parentController, controller) { + final selection = editorState.selection?.normalized; + if (selection == null) { + return const SizedBox.shrink(); + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return const SizedBox.shrink(); + } + final children = buildAlignOptions(context, (align) async { + await onAlignChanged(align); + controller.close(); + parentController.close(); + }); + return IntrinsicHeight( + child: IntrinsicWidth( + child: Column( + children: children, + ), + ), + ); + }; + + List buildAlignOptions( + BuildContext context, + void Function(OptionAlignType) onTap, + ) { + return OptionAlignType.values.map((e) => OptionAlignWrapper(e)).map((e) { + final leftIcon = e.leftIcon(Theme.of(context).colorScheme.onSurface); + final rightIcon = e.rightIcon(Theme.of(context).colorScheme.onSurface); + return HoverButton( + onTap: () => onTap(e.inner), + itemHeight: ActionListSizes.itemHeight, + leftIcon: leftIcon, + name: e.name, + rightIcon: rightIcon, + ); + }).toList(); + } + + OptionAlignType get align { + final selection = editorState.selection; + if (selection == null) { + return OptionAlignType.center; + } + final node = editorState.getNodeAtPath(selection.start.path); + final align = node?.attributes['align']; + return OptionAlignType.fromString(align); + } + + Future onAlignChanged(OptionAlignType align) async { + if (align == this.align) { + return; + } + final selection = editorState.selection; + if (selection == null) { + return; + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return; + } + final transaction = editorState.transaction; + transaction.updateNode(node, { + 'align': align.name, + }); + await editorState.apply(transaction); + } +} + +class ColorOptionAction extends PopoverActionCell { + ColorOptionAction({ + required this.editorState, + }); + + final EditorState editorState; + + @override + Widget? leftIcon(Color iconColor) { + return const FlowySvg( + FlowySvgs.color_format_m, + size: Size.square(12), + ).padding(all: 2.0); + } + + @override + String get name => LocaleKeys.document_plugins_optionAction_color.tr(); + + @override + Widget Function( + BuildContext context, + PopoverController parentController, + PopoverController controller, + ) get builder => (context, parentController, controller) { + final selection = editorState.selection?.normalized; + if (selection == null) { + return const SizedBox.shrink(); + } + final node = editorState.getNodeAtPath(selection.start.path); + if (node == null) { + return const SizedBox.shrink(); + } + final bgColor = + node.attributes[blockComponentBackgroundColor] as String?; + final selectedColor = bgColor?.tryToColor(); + // get default background color for callout block from themeExtension + final defaultColor = node.type == CalloutBlockKeys.type + ? AFThemeExtension.of(context).calloutBGColor + : Colors.transparent; + final colors = [ + // reset to default background color + FlowyColorOption( + color: defaultColor, + i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(), + id: optionActionColorDefaultColor, + ), + ...FlowyTint.values.map( + (e) => FlowyColorOption( + color: e.color(context), + i18n: e.tintName(AppFlowyEditorL10n.current), + id: e.id, + ), + ), + ]; + + return FlowyColorPicker( + colors: colors, + selected: selectedColor, + border: Border.all( + color: AFThemeExtension.of(context).onBackground, + ), + onTap: (option, index) async { + final transaction = editorState.transaction; + transaction.updateNode(node, { + blockComponentBackgroundColor: option.id, + }); + await editorState.apply(transaction); + + controller.close(); + parentController.close(); + }, + ); + }; +} + +class DepthOptionAction extends PopoverActionCell { + DepthOptionAction({required this.editorState}); + + final EditorState editorState; + + @override + Widget? leftIcon(Color iconColor) { + return FlowySvg( + OptionAction.depth.svg, + size: const Size.square(12), + ).padding(all: 2.0); + } + + @override + String get name => LocaleKeys.document_plugins_optionAction_depth.tr(); + + @override + PopoverActionCellBuilder get builder => + (context, parentController, controller) { + final children = buildDepthOptions(context, (depth) async { + await onDepthChanged(depth); + controller.close(); + parentController.close(); + }); + + return SizedBox( + width: 42, + child: Column( + mainAxisSize: MainAxisSize.min, + children: children, + ), + ); + }; + + List buildDepthOptions( + BuildContext context, + Future Function(OptionDepthType) onTap, + ) { + return OptionDepthType.values + .map((e) => OptionDepthWrapper(e)) + .map( + (e) => HoverButton( + onTap: () => onTap(e.inner), + itemHeight: ActionListSizes.itemHeight, + name: e.name, + ), + ) + .toList(); + } + + OptionDepthType depth(Node node) { + final level = node.attributes[OutlineBlockKeys.depth]; + return OptionDepthType.fromLevel(level); + } + + Future onDepthChanged(OptionDepthType depth) async { + final selection = editorState.selection; + final node = selection != null + ? editorState.getNodeAtPath(selection.start.path) + : null; + + if (node == null || depth == this.depth(node)) return; + + final transaction = editorState.transaction; + transaction.updateNode( + node, + {OutlineBlockKeys.depth: depth.level}, + ); + await editorState.apply(transaction); + } +} + +class OptionDepthWrapper extends ActionCell { + OptionDepthWrapper(this.inner); + + final OptionDepthType inner; + + @override + String get name => inner.description; +} + +class OptionActionWrapper extends ActionCell { + OptionActionWrapper(this.inner); + + final OptionAction inner; + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg); + + @override + String get name => inner.description; +} + +class OptionAlignWrapper extends ActionCell { + OptionAlignWrapper(this.inner); + + final OptionAlignType inner; + + @override + Widget? leftIcon(Color iconColor) => FlowySvg(inner.svg); + + @override + String get name => inner.description; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart deleted file mode 100644 index f78f7d35fd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_block_component.dart +++ /dev/null @@ -1,601 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.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:universal_platform/universal_platform.dart'; - -import 'operations/ai_writer_cubit.dart'; -import 'operations/ai_writer_entities.dart'; -import 'operations/ai_writer_node_extension.dart'; -import 'widgets/ai_writer_suggestion_actions.dart'; -import 'widgets/ai_writer_prompt_input_more_button.dart'; - -class AiWriterBlockKeys { - const AiWriterBlockKeys._(); - - static const String type = 'ai_writer'; - - static const String isInitialized = 'is_initialized'; - static const String selection = 'selection'; - static const String command = 'command'; - - /// Sample usage: - /// - /// `attributes: { - /// 'ai_writer_delta_suggestion': 'original' - /// }` - static const String suggestion = 'ai_writer_delta_suggestion'; - static const String suggestionOriginal = 'original'; - static const String suggestionReplacement = 'replacement'; -} - -Node aiWriterNode({ - required Selection? selection, - required AiWriterCommand command, -}) { - return Node( - type: AiWriterBlockKeys.type, - attributes: { - AiWriterBlockKeys.isInitialized: false, - AiWriterBlockKeys.selection: selection?.toJson(), - AiWriterBlockKeys.command: command.index, - }, - ); -} - -class AIWriterBlockComponentBuilder extends BlockComponentBuilder { - AIWriterBlockComponentBuilder(); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return AiWriterBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => - node.children.isEmpty && - node.attributes[AiWriterBlockKeys.isInitialized] is bool && - node.attributes[AiWriterBlockKeys.selection] is Map? && - node.attributes[AiWriterBlockKeys.command] is int; -} - -class AiWriterBlockComponent extends BlockComponentStatefulWidget { - const AiWriterBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => _AIWriterBlockComponentState(); -} - -class _AIWriterBlockComponentState extends State { - final textController = TextEditingController(); - final overlayController = OverlayPortalController(); - final layerLink = LayerLink(); - final focusNode = FocusNode(); - - late final editorState = context.read(); - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((_) { - overlayController.show(); - context.read().register(widget.node); - }); - } - - @override - void dispose() { - textController.dispose(); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isMobile) { - return const SizedBox.shrink(); - } - - final documentId = context.read()?.documentId; - - return BlocProvider( - create: (_) => AIPromptInputBloc( - predefinedFormat: null, - objectId: documentId ?? editorState.document.root.id, - ), - child: LayoutBuilder( - builder: (context, constraints) { - return OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return Center( - child: CompositedTransformFollower( - link: layerLink, - showWhenUnlinked: false, - child: Container( - padding: const EdgeInsets.only( - left: 40.0, - bottom: 16.0, - ), - width: constraints.maxWidth, - child: Focus( - focusNode: focusNode, - child: OverlayContent( - editorState: editorState, - node: widget.node, - textController: textController, - ), - ), - ), - ), - ); - }, - child: CompositedTransformTarget( - link: layerLink, - child: BlocBuilder( - builder: (context, state) { - return SizedBox( - width: double.infinity, - height: 1.0, - ); - }, - ), - ), - ); - }, - ), - ); - } -} - -class OverlayContent extends StatefulWidget { - const OverlayContent({ - super.key, - required this.editorState, - required this.node, - required this.textController, - }); - - final EditorState editorState; - final Node node; - final TextEditingController textController; - - @override - State createState() => _OverlayContentState(); -} - -class _OverlayContentState extends State { - final showCommandsToggle = ValueNotifier(false); - - @override - void dispose() { - showCommandsToggle.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is IdleAiWriterState || - state is DocumentContentEmptyAiWriterState) { - return const SizedBox.shrink(); - } - - final command = (state as RegisteredAiWriter).command; - - final selection = widget.node.aiWriterSelection; - final hasSelection = selection != null && !selection.isCollapsed; - - final markdownText = switch (state) { - final ReadyAiWriterState ready => ready.markdownText, - final GeneratingAiWriterState generating => generating.markdownText, - _ => '', - }; - - final showSuggestedActions = - state is ReadyAiWriterState && !state.isFirstRun; - final isInitialReadyState = - state is ReadyAiWriterState && state.isFirstRun; - final showSuggestedActionsPopup = - showSuggestedActions && markdownText.isEmpty || - (markdownText.isNotEmpty && command != AiWriterCommand.explain); - final showSuggestedActionsWithin = showSuggestedActions && - markdownText.isNotEmpty && - command == AiWriterCommand.explain; - - final borderColor = Theme.of(context).isLightMode - ? Color(0x1F1F2329) - : Color(0xFF505469); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showSuggestedActionsPopup) ...[ - Container( - padding: EdgeInsets.all(4.0), - decoration: _getModalDecoration( - context, - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderColor: borderColor, - ), - child: SuggestionActionBar( - currentCommand: command, - hasSelection: hasSelection, - onTap: (action) { - _onSelectSuggestionAction(context, action); - }, - ), - ), - const VSpace(4.0 + 1.0), - ], - Container( - decoration: _getModalDecoration( - context, - color: null, - borderColor: borderColor, - borderRadius: BorderRadius.all(Radius.circular(12.0)), - ), - constraints: BoxConstraints(maxHeight: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (markdownText.isNotEmpty) ...[ - Flexible( - child: DecoratedBox( - decoration: _secondaryContentDecoration(context), - child: SecondaryContentArea( - markdownText: markdownText, - onSelectSuggestionAction: (action) { - _onSelectSuggestionAction(context, action); - }, - command: command, - showSuggestionActions: showSuggestedActionsWithin, - hasSelection: hasSelection, - ), - ), - ), - Divider(height: 1.0), - ], - DecoratedBox( - decoration: markdownText.isNotEmpty - ? _mainContentDecoration(context) - : _getSingleChildDeocoration(context), - child: MainContentArea( - textController: widget.textController, - isDocumentEmpty: _isDocumentEmpty(), - isInitialReadyState: isInitialReadyState, - showCommandsToggle: showCommandsToggle, - ), - ), - ], - ), - ), - ValueListenableBuilder( - valueListenable: showCommandsToggle, - builder: (context, value, child) { - if (!value || !isInitialReadyState) { - return const SizedBox.shrink(); - } - return Align( - alignment: AlignmentDirectional.centerEnd, - child: MoreAiWriterCommands( - hasSelection: hasSelection, - editorState: widget.editorState, - onSelectCommand: (command) { - final state = context.read().state; - final showPredefinedFormats = state.showPredefinedFormats; - final predefinedFormat = state.predefinedFormat; - final text = widget.textController.text; - - context.read().runCommand( - command, - text, - showPredefinedFormats ? predefinedFormat : null, - ); - }, - ), - ); - }, - ), - ], - ); - }, - ); - } - - BoxDecoration _getModalDecoration( - BuildContext context, { - required Color? color, - required Color borderColor, - required BorderRadius borderRadius, - }) { - return BoxDecoration( - color: color, - border: Border.all( - color: borderColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: borderRadius, - boxShadow: Theme.of(context).isLightMode - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall, - ); - } - - BoxDecoration _getSingleChildDeocoration(BuildContext context) { - return BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.all(Radius.circular(12.0)), - ); - } - - BoxDecoration _secondaryContentDecoration(BuildContext context) { - return BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.vertical(top: Radius.circular(12.0)), - ); - } - - BoxDecoration _mainContentDecoration(BuildContext context) { - return BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.vertical(bottom: Radius.circular(12.0)), - ); - } - - void _onSelectSuggestionAction( - BuildContext context, - SuggestionAction action, - ) { - final predefinedFormat = - context.read().state.predefinedFormat; - context.read().runResponseAction( - action, - predefinedFormat, - ); - } - - bool _isDocumentEmpty() { - if (widget.editorState.isEmptyForContinueWriting()) { - final documentContext = widget.editorState.document.root.context; - if (documentContext == null) { - return true; - } - final view = documentContext.read().state.view; - if (view.name.isEmpty) { - return true; - } - } - return false; - } -} - -class SecondaryContentArea extends StatelessWidget { - const SecondaryContentArea({ - super.key, - required this.command, - required this.markdownText, - required this.showSuggestionActions, - required this.hasSelection, - required this.onSelectSuggestionAction, - }); - - final AiWriterCommand command; - final String markdownText; - final bool showSuggestionActions; - final bool hasSelection; - final void Function(SuggestionAction) onSelectSuggestionAction; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(8.0), - Container( - height: 24.0, - padding: EdgeInsets.symmetric(horizontal: 14.0), - alignment: AlignmentDirectional.centerStart, - child: FlowyText( - command.i18n, - fontSize: 12, - fontWeight: FontWeight.w600, - color: Color(0xFF666D76), - ), - ), - const VSpace(4.0), - Flexible( - child: SingleChildScrollView( - physics: ClampingScrollPhysics(), - padding: EdgeInsets.symmetric(horizontal: 14.0), - child: AIMarkdownText( - markdown: markdownText, - ), - ), - ), - if (showSuggestionActions) ...[ - const VSpace(4.0), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8.0), - child: SuggestionActionBar( - currentCommand: command, - hasSelection: hasSelection, - onTap: onSelectSuggestionAction, - ), - ), - ], - const VSpace(8.0), - ], - ), - ); - } -} - -class MainContentArea extends StatelessWidget { - const MainContentArea({ - super.key, - required this.textController, - required this.isInitialReadyState, - required this.isDocumentEmpty, - required this.showCommandsToggle, - }); - - final TextEditingController textController; - final bool isInitialReadyState; - final bool isDocumentEmpty; - final ValueNotifier showCommandsToggle; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final cubit = context.read(); - - if (state is ReadyAiWriterState) { - return DesktopPromptInput( - isStreaming: false, - hideDecoration: true, - hideFormats: [ - AiWriterCommand.fixSpellingAndGrammar, - AiWriterCommand.improveWriting, - AiWriterCommand.makeLonger, - AiWriterCommand.makeShorter, - ].contains(state.command), - textController: textController, - onSubmitted: (message, format, _) { - cubit.runCommand(state.command, message, format); - }, - onStopStreaming: () => cubit.stopStream(), - selectedSourcesNotifier: cubit.selectedSourcesNotifier, - onUpdateSelectedSources: (sources) { - cubit.selectedSourcesNotifier.value = [ - ...sources, - ]; - }, - extraBottomActionButton: isInitialReadyState - ? ValueListenableBuilder( - valueListenable: showCommandsToggle, - builder: (context, value, _) { - return AiWriterPromptMoreButton( - isEnabled: !isDocumentEmpty, - isSelected: value, - onTap: () => showCommandsToggle.value = !value, - ); - }, - ) - : null, - ); - } - if (state is GeneratingAiWriterState) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const HSpace(6.0), - Expanded( - child: AILoadingIndicator( - text: state.command == AiWriterCommand.explain - ? LocaleKeys.ai_analyzing.tr() - : LocaleKeys.ai_editing.tr(), - ), - ), - const HSpace(8.0), - PromptInputSendButton( - state: SendButtonState.streaming, - onSendPressed: () {}, - onStopStreaming: () => cubit.stopStream(), - ), - ], - ), - ); - } - if (state is ErrorAiWriterState) { - return Padding( - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.toast_error_filled_s, - blendMode: null, - ), - const HSpace(8.0), - Expanded( - child: FlowyText( - state.error.message, - maxLines: null, - ), - ), - const HSpace(8.0), - FlowyIconButton( - width: 32, - hoverColor: Colors.transparent, - icon: FlowySvg( - FlowySvgs.toast_close_s, - size: Size.square(20), - ), - onPressed: () => cubit.exit(), - ), - ], - ), - ); - } - if (state is LocalAIStreamingAiWriterState) { - final text = switch (state.state) { - LocalAIStreamingState.notReady => - LocaleKeys.settings_aiPage_keys_localAINotReadyRetryLater.tr(), - LocalAIStreamingState.disabled => - LocaleKeys.settings_aiPage_keys_localAIDisabled.tr(), - }; - return Padding( - padding: EdgeInsets.all(8.0), - child: Row( - children: [ - const HSpace(8.0), - Opacity( - opacity: 0.5, - child: FlowyText(text), - ), - ], - ), - ); - } - return const SizedBox.shrink(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart deleted file mode 100644 index 70d627d327..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'operations/ai_writer_entities.dart'; - -const _improveWritingToolbarItemId = 'appflowy.editor.ai_improve_writing'; -const _aiWriterToolbarItemId = 'appflowy.editor.ai_writer'; - -final ToolbarItem improveWritingItem = ToolbarItem( - id: _improveWritingToolbarItemId, - group: 0, - isActive: onlyShowInTextTypeAndExcludeTable, - builder: (context, editorState, _, __, tooltipBuilder) => - ImproveWritingButton( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ), -); - -final ToolbarItem aiWriterItem = ToolbarItem( - id: _aiWriterToolbarItemId, - group: 0, - isActive: onlyShowInTextTypeAndExcludeTable, - builder: (context, editorState, _, __, tooltipBuilder) => - AiWriterToolbarActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ), -); - -class AiWriterToolbarActionList extends StatefulWidget { - const AiWriterToolbarActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - - @override - State createState() => - _AiWriterToolbarActionListState(); -} - -class _AiWriterToolbarActionListState extends State { - final popoverController = PopoverController(); - bool isSelected = false; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - Widget buildPopoverContent() { - return SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: [ - actionWrapper(AiWriterCommand.improveWriting), - actionWrapper(AiWriterCommand.userQuestion), - actionWrapper(AiWriterCommand.fixSpellingAndGrammar), - // actionWrapper(AiWriterCommand.summarize), - actionWrapper(AiWriterCommand.explain), - divider(), - actionWrapper(AiWriterCommand.makeLonger), - actionWrapper(AiWriterCommand.makeShorter), - ], - ); - } - - Widget actionWrapper(AiWriterCommand command) { - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.icon), - iconPadding: 12, - text: FlowyText( - command.i18n, - figmaLineHeight: 20, - ), - onTap: () { - popoverController.close(); - _insertAiNode(widget.editorState, command); - }, - ), - ); - } - - Widget divider() { - return const Divider( - thickness: 1.0, - height: 1.0, - ); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; - final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_ai_writer_m, - size: Size.square(20), - color: iconScheme.primary, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconScheme.primary, - ), - ], - ), - onPressed: () { - if (_isAIWriterEnabled(widget.editorState)) { - keepEditorFocusNotifier.increase(); - popoverController.show(); - setState(() { - isSelected = true; - }); - } else { - showToastNotification( - message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - ); - } - }, - ); - - return widget.tooltipBuilder?.call( - context, - _aiWriterToolbarItemId, - _isAIWriterEnabled(widget.editorState) - ? LocaleKeys.document_plugins_aiWriter_userQuestion.tr() - : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - child, - ) ?? - child; - } -} - -class ImproveWritingButton extends StatelessWidget { - const ImproveWritingButton({ - super.key, - required this.editorState, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_ai_improve_writing_m, - size: Size.square(20.0), - color: theme.iconColorScheme.primary, - ), - onPressed: () { - if (_isAIWriterEnabled(editorState)) { - keepEditorFocusNotifier.increase(); - _insertAiNode(editorState, AiWriterCommand.improveWriting); - } else { - showToastNotification( - message: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - ); - } - }, - ); - - return tooltipBuilder?.call( - context, - _aiWriterToolbarItemId, - _isAIWriterEnabled(editorState) - ? LocaleKeys.document_plugins_aiWriter_improveWriting.tr() - : LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(), - child, - ) ?? - child; - } -} - -void _insertAiNode(EditorState editorState, AiWriterCommand command) async { - final selection = editorState.selection?.normalized; - if (selection == null) { - return; - } - - final transaction = editorState.transaction - ..insertNode( - selection.end.path.next, - aiWriterNode( - selection: selection, - command: command, - ), - ) - ..selectionExtraInfo = {selectionExtraInfoDisableToolbar: true}; - - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - inMemoryUpdate: true, - ), - withUpdateSelection: false, - ); -} - -bool _isAIWriterEnabled(EditorState editorState) { - return true; -} - -bool onlyShowInTextTypeAndExcludeTable( - EditorState editorState, -) { - return onlyShowInTextType(editorState) && notShowInTable(editorState); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart deleted file mode 100644 index 1b495a5b23..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_block_operations.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -import '../ai_writer_block_component.dart'; -import 'ai_writer_entities.dart'; -import 'ai_writer_node_extension.dart'; - -Future setAiWriterNodeIsInitialized( - EditorState editorState, - Node node, -) async { - final transaction = editorState.transaction - ..updateNode(node, { - AiWriterBlockKeys.isInitialized: true, - }); - - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - inMemoryUpdate: true, - ), - withUpdateSelection: false, - ); - - final selection = node.aiWriterSelection; - if (selection != null && !selection.isCollapsed) { - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: {selectionExtraInfoDisableToolbar: true}, - ), - ); - } -} - -Future removeAiWriterNode( - EditorState editorState, - Node node, -) async { - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - withUpdateSelection: false, - ); -} - -Future formatSelection( - EditorState editorState, - Selection selection, - ApplySuggestionFormatType formatType, -) async { - final nodes = editorState.getNodesInSelection(selection).toList(); - if (nodes.isEmpty) { - return; - } - final transaction = editorState.transaction; - - if (nodes.length == 1) { - final node = nodes.removeAt(0); - if (node.delta != null) { - final delta = Delta() - ..retain(selection.start.offset) - ..retain( - selection.length, - attributes: formatType.attributes, - ); - transaction.addDeltaToComposeMap(node, delta); - } - } else { - final firstNode = nodes.removeAt(0); - final lastNode = nodes.removeLast(); - - if (firstNode.delta != null) { - final text = firstNode.delta!.toPlainText(); - final remainderLength = text.length - selection.start.offset; - final delta = Delta() - ..retain(selection.start.offset) - ..retain(remainderLength, attributes: formatType.attributes); - transaction.addDeltaToComposeMap(firstNode, delta); - } - - if (lastNode.delta != null) { - final delta = Delta() - ..retain(selection.end.offset, attributes: formatType.attributes); - transaction.addDeltaToComposeMap(lastNode, delta); - } - - for (final node in nodes) { - if (node.delta == null) { - continue; - } - final length = node.delta!.length; - if (length != 0) { - final delta = Delta() - ..retain(length, attributes: formatType.attributes); - transaction.addDeltaToComposeMap(node, delta); - } - } - } - - transaction.compose(); - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - withUpdateSelection: false, - ); -} - -Future ensurePreviousNodeIsEmptyParagraph( - EditorState editorState, - Node aiWriterNode, -) async { - final previous = aiWriterNode.previous; - final needsEmptyParagraphNode = previous == null || - previous.type != ParagraphBlockKeys.type || - (previous.delta?.toPlainText().isNotEmpty ?? false); - - final Position position; - final transaction = editorState.transaction; - - if (needsEmptyParagraphNode) { - position = Position(path: aiWriterNode.path); - transaction.insertNode(aiWriterNode.path, paragraphNode()); - } else { - position = Position(path: previous.path); - } - transaction.afterSelection = Selection.collapsed(position); - - await editorState.apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: true, - recordUndo: false, - ), - ); - - return position; -} - -extension SaveAIResponseExtension on EditorState { - Future insertBelow({ - required Node node, - required String markdownText, - }) async { - final selection = this.selection?.normalized; - if (selection == null) { - return; - } - - final nodes = customMarkdownToDocument( - markdownText, - tableWidth: 250.0, - ).root.children.map((e) => e.deepCopy()).toList(); - if (nodes.isEmpty) { - return; - } - - final insertedPath = selection.end.path.next; - final lastDeltaLength = nodes.lastOrNull?.delta?.length ?? 0; - - final transaction = this.transaction - ..insertNodes(insertedPath, nodes) - ..afterSelection = Selection( - start: Position(path: insertedPath), - end: Position( - path: insertedPath.nextNPath(nodes.length - 1), - offset: lastDeltaLength, - ), - ); - - await apply(transaction); - } - - Future replace({ - required Selection selection, - required String text, - }) async { - final trimmedText = text.trim(); - if (trimmedText.isEmpty) { - return; - } - await switch (kdefaultReplacementType) { - AskAIReplacementType.markdown => - _replaceWithMarkdown(selection, trimmedText), - AskAIReplacementType.plainText => - _replaceWithPlainText(selection, trimmedText), - }; - } - - Future _replaceWithMarkdown( - Selection selection, - String markdownText, - ) async { - final nodes = customMarkdownToDocument(markdownText) - .root - .children - .map((e) => e.deepCopy()) - .toList(); - if (nodes.isEmpty) { - return; - } - - final nodesInSelection = getNodesInSelection(selection); - final newSelection = Selection( - start: selection.start, - end: Position( - path: selection.start.path.nextNPath(nodes.length - 1), - offset: nodes.lastOrNull?.delta?.length ?? 0, - ), - ); - - final transaction = this.transaction - ..insertNodes(selection.start.path, nodes) - ..deleteNodes(nodesInSelection) - ..afterSelection = newSelection; - await apply(transaction); - } - - Future _replaceWithPlainText( - Selection selection, - String plainText, - ) async { - final nodes = getNodesInSelection(selection); - if (nodes.isEmpty || nodes.any((element) => element.delta == null)) { - return; - } - - final replaceTexts = plainText.split('\n') - ..removeWhere((element) => element.isEmpty); - final transaction = this.transaction - ..replaceTexts( - nodes, - selection, - replaceTexts, - ); - await apply(transaction); - - int endOffset = replaceTexts.last.length; - if (replaceTexts.length == 1) { - endOffset += selection.start.offset; - } - final end = Position( - path: [selection.start.path.first + replaceTexts.length - 1], - offset: endOffset, - ); - this.selection = Selection( - start: selection.start, - end: end, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart deleted file mode 100644 index 4bc13321b8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart +++ /dev/null @@ -1,794 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; - -import '../../base/markdown_text_robot.dart'; -import 'ai_writer_block_operations.dart'; -import 'ai_writer_entities.dart'; -import 'ai_writer_node_extension.dart'; - -/// Enable the debug log for the AiWriterCubit. -/// -/// This is useful for debugging the AI writer cubit. -const _aiWriterCubitDebugLog = true; - -class AiWriterCubit extends Cubit { - AiWriterCubit({ - required this.documentId, - required this.editorState, - this.onCreateNode, - this.onRemoveNode, - this.onAppendToDocument, - AppFlowyAIService? aiService, - }) : _aiService = aiService ?? AppFlowyAIService(), - _textRobot = MarkdownTextRobot(editorState: editorState), - selectedSourcesNotifier = ValueNotifier([documentId]), - super(IdleAiWriterState()); - - final String documentId; - final EditorState editorState; - final AppFlowyAIService _aiService; - final MarkdownTextRobot _textRobot; - final void Function()? onCreateNode; - final void Function()? onRemoveNode; - final void Function()? onAppendToDocument; - - Node? aiWriterNode; - - final List records = []; - final ValueNotifier> selectedSourcesNotifier; - - @override - Future close() async { - selectedSourcesNotifier.dispose(); - await super.close(); - } - - Future exit({ - bool withDiscard = true, - bool withUnformat = true, - }) async { - if (aiWriterNode == null) { - return; - } - if (withDiscard) { - await _textRobot.discard( - afterSelection: aiWriterNode!.aiWriterSelection, - ); - } - _textRobot.clear(); - _textRobot.reset(); - onRemoveNode?.call(); - records.clear(); - selectedSourcesNotifier.value = [documentId]; - emit(IdleAiWriterState()); - - if (withUnformat) { - final selection = aiWriterNode!.aiWriterSelection; - if (selection == null) { - return; - } - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, - ); - } - if (aiWriterNode != null) { - await removeAiWriterNode(editorState, aiWriterNode!); - aiWriterNode = null; - } - } - - void register(Node node) async { - if (node.isAiWriterInitialized) { - return; - } - if (aiWriterNode != null && node.id != aiWriterNode!.id) { - await removeAiWriterNode(editorState, node); - return; - } - - aiWriterNode = node; - onCreateNode?.call(); - - await setAiWriterNodeIsInitialized(editorState, node); - - final command = node.aiWriterCommand; - final (run, prompt) = await _addSelectionTextToRecords(command); - - _aiWriterCubitLog( - 'command: $command, run: $run, prompt: $prompt', - ); - - if (!run) { - await exit(); - return; - } - - runCommand(command, prompt, null); - } - - void runCommand( - AiWriterCommand command, - String prompt, - PredefinedFormat? predefinedFormat, - ) async { - if (aiWriterNode == null) { - return; - } - - await _textRobot.discard(); - _textRobot.clear(); - - switch (command) { - case AiWriterCommand.continueWriting: - await _startContinueWriting( - command, - predefinedFormat, - ); - break; - case AiWriterCommand.fixSpellingAndGrammar: - case AiWriterCommand.improveWriting: - case AiWriterCommand.makeLonger: - case AiWriterCommand.makeShorter: - await _startSuggestingEdits(command, prompt, predefinedFormat); - break; - case AiWriterCommand.explain: - await _startInforming(command, prompt, predefinedFormat); - break; - case AiWriterCommand.userQuestion when prompt.isNotEmpty: - _startAskingQuestion(prompt, predefinedFormat); - break; - case AiWriterCommand.userQuestion: - emit( - ReadyAiWriterState(AiWriterCommand.userQuestion, isFirstRun: true), - ); - break; - } - } - - void _retry({ - required PredefinedFormat? predefinedFormat, - }) async { - final lastQuestion = - records.lastWhereOrNull((record) => record.role == AiRole.user); - - if (lastQuestion != null && state is RegisteredAiWriter) { - runCommand( - (state as RegisteredAiWriter).command, - lastQuestion.content, - lastQuestion.format, - ); - } - } - - Future stopStream() async { - if (aiWriterNode == null) { - return; - } - - if (state is GeneratingAiWriterState) { - final generatingState = state as GeneratingAiWriterState; - - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - - if (_textRobot.hasAnyResult) { - records.add(AiWriterRecord.ai(content: _textRobot.markdownText)); - } - - await AIEventStopCompleteText( - CompleteTextTaskPB( - taskId: generatingState.taskId, - ), - ).send(); - - emit( - ReadyAiWriterState( - generatingState.command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - } - } - - void runResponseAction( - SuggestionAction action, [ - PredefinedFormat? predefinedFormat, - ]) async { - if (aiWriterNode == null) { - return; - } - - if (action case SuggestionAction.rewrite || SuggestionAction.tryAgain) { - _retry(predefinedFormat: predefinedFormat); - return; - } - if (action case SuggestionAction.discard || SuggestionAction.close) { - await exit(); - return; - } - - final selection = aiWriterNode?.aiWriterSelection; - if (selection == null) { - return; - } - - // Accept - // - // If the user clicks accept, we need to replace the selection with the AI's response - if (action case SuggestionAction.accept) { - // trim the markdown text to avoid extra new lines - final trimmedMarkdownText = _textRobot.markdownText.trim(); - - _aiWriterCubitLog( - 'trigger accept action, markdown text: $trimmedMarkdownText', - ); - - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, - ); - - await _textRobot.deleteAINodes(); - - await _textRobot.replace( - selection: selection, - markdownText: trimmedMarkdownText, - ); - - await exit(withDiscard: false, withUnformat: false); - - return; - } - - if (action case SuggestionAction.keep) { - await _textRobot.persist(); - await exit(withDiscard: false); - return; - } - - if (action case SuggestionAction.insertBelow) { - if (state is! ReadyAiWriterState) { - return; - } - final command = (state as ReadyAiWriterState).command; - final markdownText = (state as ReadyAiWriterState).markdownText; - if (command == AiWriterCommand.explain && markdownText.isNotEmpty) { - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position); - await _textRobot.persist(markdownText: markdownText); - } else if (_textRobot.hasAnyResult) { - await _textRobot.persist(); - } - - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.clear, - ); - await exit(withDiscard: false); - } - } - - bool hasUnusedResponse() { - return switch (state) { - ReadyAiWriterState( - isFirstRun: final isInitial, - markdownText: final markdownText, - ) => - !isInitial && (markdownText.isNotEmpty || _textRobot.hasAnyResult), - GeneratingAiWriterState() => true, - _ => false, - }; - } - - Future<(bool, String)> _addSelectionTextToRecords( - AiWriterCommand command, - ) async { - final node = aiWriterNode; - - // check the node is registered - if (node == null) { - return (false, ''); - } - - // check the selection is valid - final selection = node.aiWriterSelection?.normalized; - if (selection == null) { - return (false, ''); - } - - // if the command is continue writing, we don't need to get the selection text - if (command == AiWriterCommand.continueWriting) { - return (true, ''); - } - - // if the selection is collapsed, we don't need to get the selection text - if (selection.isCollapsed) { - return (true, ''); - } - - final selectionText = await editorState.getMarkdownInSelection(selection); - - if (command == AiWriterCommand.userQuestion) { - records.add( - AiWriterRecord.user(content: selectionText, format: null), - ); - - return (true, ''); - } else { - return (true, selectionText); - } - } - - Future _getDocumentContentFromTopToPosition(Position position) async { - final beginningToCursorSelection = Selection( - start: Position(path: [0]), - end: position, - ).normalized; - - final documentText = - (await editorState.getMarkdownInSelection(beginningToCursorSelection)) - .trim(); - - final view = await ViewBackendService.getView(documentId).toNullable(); - final viewName = view?.name ?? ''; - - return "$viewName\n$documentText".trim(); - } - - void _startAskingQuestion( - String prompt, - PredefinedFormat? format, - ) async { - if (aiWriterNode == null) { - return; - } - final command = AiWriterCommand.userQuestion; - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: prompt, - format: format, - history: records, - sourceIds: selectedSourcesNotifier.value, - completionType: command.toCompletionType(), - onStart: () async { - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position); - records.add( - AiWriterRecord.user( - content: prompt, - format: format, - ), - ); - }, - processMessage: (text) async { - await _textRobot.appendMarkdownText( - text, - updateSelection: false, - attributes: ApplySuggestionFormatType.replace.attributes, - ); - onAppendToDocument?.call(); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - }, - onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - } - }, - onError: (error) async { - emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, - ); - - if (stream != null) { - emit( - GeneratingAiWriterState( - command, - taskId: stream.$1, - ), - ); - } - } - - Future _startContinueWriting( - AiWriterCommand command, - PredefinedFormat? predefinedFormat, - ) async { - final position = aiWriterNode?.aiWriterSelection?.start; - if (position == null) { - return; - } - final text = await _getDocumentContentFromTopToPosition(position); - - if (text.isEmpty) { - final stateCopy = state; - emit(DocumentContentEmptyAiWriterState(command, onConfirm: exit)); - emit(stateCopy); - return; - } - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: text, - completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, - format: predefinedFormat, - onStart: () async { - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position); - records.add( - AiWriterRecord.user( - content: text, - format: predefinedFormat, - ), - ); - }, - processMessage: (text) async { - await _textRobot.appendMarkdownText( - text, - updateSelection: false, - attributes: ApplySuggestionFormatType.replace.attributes, - ); - onAppendToDocument?.call(); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - }, - onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - } - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onError: (error) async { - emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, - ); - if (stream != null) { - emit( - GeneratingAiWriterState(command, taskId: stream.$1), - ); - } - } - - Future _startSuggestingEdits( - AiWriterCommand command, - String prompt, - PredefinedFormat? predefinedFormat, - ) async { - final selection = aiWriterNode?.aiWriterSelection; - if (selection == null) { - return; - } - if (prompt.isEmpty) { - prompt = records.removeAt(0).content; - } - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: prompt, - format: predefinedFormat, - completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, - onStart: () async { - await formatSelection( - editorState, - selection, - ApplySuggestionFormatType.original, - ); - final position = await ensurePreviousNodeIsEmptyParagraph( - editorState, - aiWriterNode!, - ); - _textRobot.start(position: position, previousSelection: selection); - records.add( - AiWriterRecord.user( - content: prompt, - format: predefinedFormat, - ), - ); - }, - processMessage: (text) async { - await _textRobot.appendMarkdownText( - text, - updateSelection: false, - attributes: ApplySuggestionFormatType.replace.attributes, - ); - onAppendToDocument?.call(); - - _aiWriterCubitLog( - 'received message: $text', - ); - }, - processAssistMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - - _aiWriterCubitLog( - 'received assist message: $text', - ); - }, - onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { - await _textRobot.stop( - attributes: ApplySuggestionFormatType.replace.attributes, - ); - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - - _aiWriterCubitLog( - 'returned response: ${_textRobot.markdownText}', - ); - } - }, - onError: (error) async { - emit(ErrorAiWriterState(command, error: error)); - records.add( - AiWriterRecord.ai(content: _textRobot.markdownText), - ); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, - ); - if (stream != null) { - emit( - GeneratingAiWriterState(command, taskId: stream.$1), - ); - } - } - - Future _startInforming( - AiWriterCommand command, - String prompt, - PredefinedFormat? predefinedFormat, - ) async { - final selection = aiWriterNode?.aiWriterSelection; - if (selection == null) { - return; - } - if (prompt.isEmpty) { - prompt = records.removeAt(0).content; - } - - final stream = await _aiService.streamCompletion( - objectId: documentId, - text: prompt, - completionType: command.toCompletionType(), - history: records, - sourceIds: selectedSourcesNotifier.value, - format: predefinedFormat, - onStart: () async { - records.add( - AiWriterRecord.user( - content: prompt, - format: predefinedFormat, - ), - ); - }, - processMessage: (text) async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - GeneratingAiWriterState( - command, - taskId: generatingState.taskId, - markdownText: generatingState.markdownText + text, - ), - ); - } - }, - processAssistMessage: (_) async {}, - onEnd: () async { - if (state case final GeneratingAiWriterState generatingState) { - emit( - ReadyAiWriterState( - command, - isFirstRun: false, - markdownText: generatingState.markdownText, - ), - ); - records.add( - AiWriterRecord.ai(content: generatingState.markdownText), - ); - } - }, - onError: (error) async { - if (state case final GeneratingAiWriterState generatingState) { - records.add( - AiWriterRecord.ai(content: generatingState.markdownText), - ); - } - emit(ErrorAiWriterState(command, error: error)); - }, - onLocalAIStreamingStateChange: (state) { - emit(LocalAIStreamingAiWriterState(command, state: state)); - }, - ); - if (stream != null) { - emit( - GeneratingAiWriterState(command, taskId: stream.$1), - ); - } - } - - void _aiWriterCubitLog(String message) { - if (_aiWriterCubitDebugLog) { - Log.debug('[AiWriterCubit] $message'); - } - } -} - -mixin RegisteredAiWriter { - AiWriterCommand get command; -} - -sealed class AiWriterState { - const AiWriterState(); -} - -class IdleAiWriterState extends AiWriterState { - const IdleAiWriterState(); -} - -class ReadyAiWriterState extends AiWriterState with RegisteredAiWriter { - const ReadyAiWriterState( - this.command, { - required this.isFirstRun, - this.markdownText = '', - }); - - @override - final AiWriterCommand command; - - final bool isFirstRun; - final String markdownText; -} - -class GeneratingAiWriterState extends AiWriterState with RegisteredAiWriter { - const GeneratingAiWriterState( - this.command, { - required this.taskId, - this.progress = '', - this.markdownText = '', - }); - - @override - final AiWriterCommand command; - - final String taskId; - final String progress; - final String markdownText; -} - -class ErrorAiWriterState extends AiWriterState with RegisteredAiWriter { - const ErrorAiWriterState( - this.command, { - required this.error, - }); - - @override - final AiWriterCommand command; - - final AIError error; -} - -class DocumentContentEmptyAiWriterState extends AiWriterState - with RegisteredAiWriter { - const DocumentContentEmptyAiWriterState( - this.command, { - required this.onConfirm, - }); - - @override - final AiWriterCommand command; - - final void Function() onConfirm; -} - -class LocalAIStreamingAiWriterState extends AiWriterState - with RegisteredAiWriter { - const LocalAIStreamingAiWriterState( - this.command, { - required this.state, - }); - - @override - final AiWriterCommand command; - - final LocalAIStreamingState state; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart deleted file mode 100644 index f15c2e6d7f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart +++ /dev/null @@ -1,159 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; - -import '../ai_writer_block_component.dart'; - -const kdefaultReplacementType = AskAIReplacementType.markdown; - -enum AskAIReplacementType { - markdown, - plainText, -} - -enum SuggestionAction { - accept, - discard, - close, - tryAgain, - rewrite, - keep, - insertBelow; - - String get i18n => switch (this) { - accept => LocaleKeys.suggestion_accept.tr(), - discard => LocaleKeys.suggestion_discard.tr(), - close => LocaleKeys.suggestion_close.tr(), - tryAgain => LocaleKeys.suggestion_tryAgain.tr(), - rewrite => LocaleKeys.suggestion_rewrite.tr(), - keep => LocaleKeys.suggestion_keep.tr(), - insertBelow => LocaleKeys.suggestion_insertBelow.tr(), - }; - - FlowySvg buildIcon(BuildContext context) { - final icon = switch (this) { - accept || keep => FlowySvgs.ai_fix_spelling_grammar_s, - discard || close => FlowySvgs.toast_close_s, - tryAgain || rewrite => FlowySvgs.ai_try_again_s, - insertBelow => FlowySvgs.suggestion_insert_below_s, - }; - - return FlowySvg( - icon, - size: Size.square(16.0), - color: switch (this) { - accept || keep => Color(0xFF278E42), - discard || close => Color(0xFFC40055), - _ => Theme.of(context).iconTheme.color, - }, - ); - } -} - -enum AiWriterCommand { - userQuestion, - explain, - // summarize, - continueWriting, - fixSpellingAndGrammar, - improveWriting, - makeShorter, - makeLonger; - - String defaultPrompt(String input) => switch (this) { - userQuestion => input, - explain => "Explain this phrase in a concise manner:\n\n$input", - // summarize => '$input\n\nTl;dr', - continueWriting => - 'Continue writing based on this existing text:\n\n$input', - fixSpellingAndGrammar => 'Correct this to standard English:\n\n$input', - improveWriting => 'Rewrite this in your own words:\n\n$input', - makeShorter => 'Make this text shorter:\n\n$input', - makeLonger => 'Make this text longer:\n\n$input', - }; - - String get i18n => switch (this) { - userQuestion => LocaleKeys.document_plugins_aiWriter_userQuestion.tr(), - explain => LocaleKeys.document_plugins_aiWriter_explain.tr(), - // summarize => LocaleKeys.document_plugins_aiWriter_summarize.tr(), - continueWriting => - LocaleKeys.document_plugins_aiWriter_continueWriting.tr(), - fixSpellingAndGrammar => - LocaleKeys.document_plugins_aiWriter_fixSpelling.tr(), - improveWriting => - LocaleKeys.document_plugins_smartEditImproveWriting.tr(), - makeShorter => LocaleKeys.document_plugins_aiWriter_makeShorter.tr(), - makeLonger => LocaleKeys.document_plugins_aiWriter_makeLonger.tr(), - }; - - FlowySvgData get icon => switch (this) { - userQuestion => FlowySvgs.ai_sparks_s, - explain => FlowySvgs.ai_explain_m, - // summarize => FlowySvgs.ai_summarize_s, - continueWriting || improveWriting => FlowySvgs.ai_improve_writing_s, - fixSpellingAndGrammar => FlowySvgs.ai_fix_spelling_grammar_s, - makeShorter => FlowySvgs.ai_make_shorter_s, - makeLonger => FlowySvgs.ai_make_longer_s, - }; - - CompletionTypePB toCompletionType() => switch (this) { - userQuestion => CompletionTypePB.UserQuestion, - explain => CompletionTypePB.ExplainSelected, - // summarize => CompletionTypePB.Summarize, - continueWriting => CompletionTypePB.ContinueWriting, - fixSpellingAndGrammar => CompletionTypePB.SpellingAndGrammar, - improveWriting => CompletionTypePB.ImproveWriting, - makeShorter => CompletionTypePB.MakeShorter, - makeLonger => CompletionTypePB.MakeLonger, - }; -} - -enum ApplySuggestionFormatType { - original(AiWriterBlockKeys.suggestionOriginal), - replace(AiWriterBlockKeys.suggestionReplacement), - clear(null); - - const ApplySuggestionFormatType(this.value); - final String? value; - - Map get attributes => {AiWriterBlockKeys.suggestion: value}; -} - -enum AiRole { - user, - system, - ai, -} - -class AiWriterRecord extends Equatable { - const AiWriterRecord.user({ - required this.content, - required this.format, - }) : role = AiRole.user; - - const AiWriterRecord.ai({ - required this.content, - }) : role = AiRole.ai, - format = null; - - final AiRole role; - final String content; - final PredefinedFormat? format; - - @override - List get props => [role, content, format]; - - CompletionRecordPB toPB() { - return CompletionRecordPB( - content: content, - role: switch (role) { - AiRole.user => ChatMessageTypePB.User, - AiRole.system || AiRole.ai => ChatMessageTypePB.System, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart deleted file mode 100644 index 881871b154..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart +++ /dev/null @@ -1,179 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -import '../ai_writer_block_component.dart'; -import 'ai_writer_entities.dart'; - -extension AiWriterExtension on Node { - bool get isAiWriterInitialized { - return attributes[AiWriterBlockKeys.isInitialized]; - } - - Selection? get aiWriterSelection { - final selection = attributes[AiWriterBlockKeys.selection]; - if (selection == null) { - return null; - } - return Selection.fromJson(selection); - } - - AiWriterCommand get aiWriterCommand { - final index = attributes[AiWriterBlockKeys.command]; - return AiWriterCommand.values[index]; - } -} - -extension AiWriterNodeExtension on EditorState { - Future getMarkdownInSelection(Selection? selection) async { - selection ??= this.selection?.normalized; - if (selection == null || selection.isCollapsed) { - return ''; - } - - // if the selected nodes are not entirely selected, slice the nodes - final slicedNodes = []; - final List flattenNodes = getNodesInSelection(selection); - final List nodes = []; - - for (final node in flattenNodes) { - if (nodes.any((element) => element.isParentOf(node))) { - continue; - } - nodes.add(node); - } - - for (final node in nodes) { - final delta = node.delta; - if (delta == null) { - continue; - } - - final slicedDelta = delta.slice( - node == nodes.first ? selection.startIndex : 0, - node == nodes.last ? selection.endIndex : delta.length, - ); - - final copiedNode = node.copyWith( - attributes: { - ...node.attributes, - blockComponentDelta: slicedDelta.toJson(), - }, - ); - - slicedNodes.add(copiedNode); - } - - for (final (i, node) in slicedNodes.indexed) { - final childNodesShouldBeDeleted = []; - for (final child in node.children) { - if (!child.path.inSelection(selection)) { - childNodesShouldBeDeleted.add(child); - } - } - for (final child in childNodesShouldBeDeleted) { - slicedNodes[i] = node.copyWith( - children: node.children.where((e) => e.id != child.id).toList(), - type: selection.startIndex != 0 ? ParagraphBlockKeys.type : node.type, - ); - } - } - - // use \n\n as line break to improve the ai response - // using \n will cause the ai response treat the text as a single line - final markdown = await customDocumentToMarkdown( - Document.blank()..insert([0], slicedNodes), - lineBreak: '\n', - ); - - // trim the last \n if it exists - return markdown.trimRight(); - } - - List getPlainTextInSelection(Selection? selection) { - selection ??= this.selection?.normalized; - if (selection == null || selection.isCollapsed) { - return []; - } - - final res = []; - if (selection.isCollapsed) { - return res; - } - - final nodes = getNodesInSelection(selection); - - for (final node in nodes) { - final delta = node.delta; - if (delta == null) { - continue; - } - final startIndex = node == nodes.first ? selection.startIndex : 0; - final endIndex = node == nodes.last ? selection.endIndex : delta.length; - res.add(delta.slice(startIndex, endIndex).toPlainText()); - } - - return res; - } - - /// Determines whether the document is empty up to the selection - /// - /// If empty and the title is also empty, the continue writing option will be disabled. - bool isEmptyForContinueWriting({ - Selection? selection, - }) { - if (selection != null && !selection.isCollapsed) { - return false; - } - - final effectiveSelection = Selection( - start: Position(path: [0]), - end: selection?.normalized.end ?? - this.selection?.normalized.end ?? - Position(path: getLastSelectable()?.$1.path ?? [0]), - ); - - // if the selected nodes are not entirely selected, slice the nodes - final slicedNodes = []; - final nodes = getNodesInSelection(effectiveSelection); - - for (final node in nodes) { - final delta = node.delta; - if (delta == null) { - continue; - } - - final slicedDelta = delta.slice( - node == nodes.first ? effectiveSelection.startIndex : 0, - node == nodes.last ? effectiveSelection.endIndex : delta.length, - ); - - final copiedNode = node.copyWith( - attributes: { - ...node.attributes, - blockComponentDelta: slicedDelta.toJson(), - }, - ); - - slicedNodes.add(copiedNode); - } - - // using less custom parsers to avoid futures - final markdown = documentToMarkdown( - Document.blank()..insert([0], slicedNodes), - customParsers: [ - const MathEquationNodeParser(), - const CalloutNodeParser(), - const ToggleListNodeParser(), - const CustomParagraphNodeParser(), - const SubPageNodeParser(), - const SimpleTableNodeParser(), - const LinkPreviewNodeParser(), - const FileBlockNodeParser(), - ], - ); - - return markdown.trim().isEmpty; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart deleted file mode 100644 index 8a691acdfc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_gesture_detector.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -class AiWriterGestureDetector extends StatelessWidget { - const AiWriterGestureDetector({ - super.key, - required this.behavior, - required this.onPointerEvent, - this.child, - }); - - final HitTestBehavior behavior; - final void Function() onPointerEvent; - final Widget? child; - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - behavior: behavior, - gestures: { - TapGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), - (instance) => instance - ..onTapDown = ((_) => onPointerEvent()) - ..onSecondaryTapDown = ((_) => onPointerEvent()) - ..onTertiaryTapDown = ((_) => onPointerEvent()), - ), - ImmediateMultiDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - ImmediateMultiDragGestureRecognizer>( - () => ImmediateMultiDragGestureRecognizer(), - (instance) => instance.onStart = (offset) => null, - ), - }, - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart deleted file mode 100644 index 72b8d9560b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_prompt_input_more_button.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import '../operations/ai_writer_entities.dart'; - -class AiWriterPromptMoreButton extends StatelessWidget { - const AiWriterPromptMoreButton({ - super.key, - required this.isEnabled, - required this.isSelected, - required this.onTap, - }); - - final bool isEnabled; - final bool isSelected; - final void Function() onTap; - - @override - Widget build(BuildContext context) { - return IgnorePointer( - ignoring: !isEnabled, - child: GestureDetector( - onTap: onTap, - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: DesktopAIPromptSizes.actionBarButtonSize, - child: FlowyHover( - style: const HoverStyle( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - isSelected: () => isSelected, - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(6, 6, 4, 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - LocaleKeys.ai_more.tr(), - fontSize: 12, - figmaLineHeight: 16, - color: isEnabled - ? Theme.of(context).hintColor - : Theme.of(context).disabledColor, - ), - const HSpace(2.0), - FlowySvg( - FlowySvgs.ai_source_drop_down_s, - color: Theme.of(context).hintColor, - size: const Size.square(8), - ), - ], - ), - ), - ), - ), - ), - ); - } -} - -class MoreAiWriterCommands extends StatelessWidget { - const MoreAiWriterCommands({ - super.key, - required this.hasSelection, - required this.editorState, - required this.onSelectCommand, - }); - - final EditorState editorState; - final bool hasSelection; - final void Function(AiWriterCommand) onSelectCommand; - - @override - Widget build(BuildContext context) { - return Container( - // add one here to take into account the border of the main message box. - // It is configured to be on the outside to hide some graphical - // artifacts. - margin: EdgeInsets.only(top: 4.0 + 1.0), - padding: EdgeInsets.all(8.0), - constraints: BoxConstraints(minWidth: 240.0), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border.all( - color: Theme.of(context).brightness == Brightness.light - ? ColorSchemeConstants.lightBorderColor - : ColorSchemeConstants.darkBorderColor, - strokeAlign: BorderSide.strokeAlignOutside, - ), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - boxShadow: Theme.of(context).isLightMode - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall, - ), - child: IntrinsicWidth( - child: Column( - spacing: 4.0, - crossAxisAlignment: CrossAxisAlignment.start, - children: _getCommands( - hasSelection: hasSelection, - ), - ), - ), - ); - } - - List _getCommands({required bool hasSelection}) { - if (hasSelection) { - return [ - _bottomButton(AiWriterCommand.improveWriting), - _bottomButton(AiWriterCommand.fixSpellingAndGrammar), - _bottomButton(AiWriterCommand.explain), - const Divider(height: 1.0, thickness: 1.0), - _bottomButton(AiWriterCommand.makeLonger), - _bottomButton(AiWriterCommand.makeShorter), - ]; - } else { - return [ - _bottomButton(AiWriterCommand.continueWriting), - ]; - } - } - - Widget _bottomButton(AiWriterCommand command) { - return Builder( - builder: (context) { - return FlowyButton( - leftIcon: FlowySvg( - command.icon, - color: Theme.of(context).iconTheme.color, - ), - leftIconSize: const Size.square(20), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: FlowyText( - command.i18n, - figmaLineHeight: 20, - ), - onTap: () => onSelectCommand(command), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart deleted file mode 100644 index ef8ee81219..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_scroll_wrapper.dart +++ /dev/null @@ -1,242 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/throttle.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../operations/ai_writer_cubit.dart'; -import 'ai_writer_gesture_detector.dart'; - -class AiWriterScrollWrapper extends StatefulWidget { - const AiWriterScrollWrapper({ - super.key, - required this.viewId, - required this.editorState, - required this.child, - }); - - final String viewId; - final EditorState editorState; - final Widget child; - - @override - State createState() => _AiWriterScrollWrapperState(); -} - -class _AiWriterScrollWrapperState extends State { - final overlayController = OverlayPortalController(); - late final throttler = Throttler(); - late final aiWriterCubit = AiWriterCubit( - documentId: widget.viewId, - editorState: widget.editorState, - onCreateNode: () { - aiWriterRegistered = true; - widget.editorState.service.keyboardService?.disableShortcuts(); - }, - onRemoveNode: () { - aiWriterRegistered = false; - widget.editorState.service.keyboardService?.enableShortcuts(); - widget.editorState.service.keyboardService?.enable(); - }, - onAppendToDocument: onAppendToDocument, - ); - - bool userHasScrolled = false; - bool aiWriterRegistered = false; - bool dialogShown = false; - - @override - void initState() { - super.initState(); - overlayController.show(); - } - - @override - void dispose() { - aiWriterCubit.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: aiWriterCubit, - child: NotificationListener( - onNotification: handleScrollNotification, - child: Focus( - autofocus: true, - onKeyEvent: handleKeyEvent, - child: MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) { - if (state is DocumentContentEmptyAiWriterState) { - showConfirmDialog( - context: context, - title: - LocaleKeys.ai_continueWritingEmptyDocumentTitle.tr(), - description: LocaleKeys - .ai_continueWritingEmptyDocumentDescription - .tr(), - onConfirm: state.onConfirm, - ); - } - }, - ), - BlocListener( - listenWhen: (previous, current) => - previous is GeneratingAiWriterState && - current is ReadyAiWriterState, - listener: (context, state) { - widget.editorState.updateSelectionWithReason(null); - }, - ), - ], - child: OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return BlocBuilder( - builder: (context, state) { - return AiWriterGestureDetector( - behavior: state is RegisteredAiWriter - ? HitTestBehavior.translucent - : HitTestBehavior.deferToChild, - onPointerEvent: () => onTapOutside(context), - ); - }, - ); - }, - child: widget.child, - ), - ), - ), - ), - ); - } - - bool handleScrollNotification(ScrollNotification notification) { - if (!aiWriterRegistered) { - return false; - } - - if (notification is UserScrollNotification) { - debounceResetUserHasScrolled(); - userHasScrolled = true; - throttler.cancel(); - } - - return false; - } - - void debounceResetUserHasScrolled() { - Debounce.debounce( - 'user_has_scrolled', - const Duration(seconds: 3), - () => userHasScrolled = false, - ); - } - - void onTapOutside(BuildContext context) { - final aiWriterCubit = context.read(); - - if (aiWriterCubit.hasUnusedResponse()) { - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: stopAndExit, - onCancel: () {}, - ); - } else { - stopAndExit(); - } - } - - KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) { - if (!aiWriterRegistered) { - return KeyEventResult.ignored; - } - if (dialogShown) { - return KeyEventResult.handled; - } - if (event is! KeyDownEvent) { - return KeyEventResult.ignored; - } - - switch (event.logicalKey) { - case LogicalKeyboardKey.escape: - if (aiWriterCubit.state case GeneratingAiWriterState _) { - aiWriterCubit.stopStream(); - } else if (aiWriterCubit.hasUnusedResponse()) { - dialogShown = true; - showConfirmDialog( - context: context, - title: LocaleKeys.button_discard.tr(), - description: LocaleKeys.document_plugins_discardResponse.tr(), - confirmLabel: LocaleKeys.button_discard.tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: stopAndExit, - onCancel: () {}, - ).then((_) => dialogShown = false); - } else { - stopAndExit(); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.keyC - when HardwareKeyboard.instance.isControlPressed: - if (aiWriterCubit.state case GeneratingAiWriterState _) { - aiWriterCubit.stopStream(); - } - return KeyEventResult.handled; - default: - break; - } - - return KeyEventResult.ignored; - } - - void onAppendToDocument() { - if (!aiWriterRegistered || userHasScrolled) { - return; - } - - throttler.call(() { - if (aiWriterCubit.aiWriterNode != null) { - final path = aiWriterCubit.aiWriterNode!.path; - - if (path.isEmpty) { - return; - } - - if (path.previous.isNotEmpty) { - final node = widget.editorState.getNodeAtPath(path.previous); - if (node != null && node.delta != null && node.delta!.isNotEmpty) { - widget.editorState.updateSelectionWithReason( - Selection.collapsed( - Position(path: path, offset: node.delta!.length), - ), - ); - return; - } - } - - widget.editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - ); - } - }); - } - - void stopAndExit() { - Future(() async { - await aiWriterCubit.stopStream(); - await aiWriterCubit.exit(); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart deleted file mode 100644 index d39ede2608..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/ai/widgets/ai_writer_suggestion_actions.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import '../operations/ai_writer_entities.dart'; - -class SuggestionActionBar extends StatelessWidget { - const SuggestionActionBar({ - super.key, - required this.currentCommand, - required this.hasSelection, - required this.onTap, - }); - - final AiWriterCommand currentCommand; - final bool hasSelection; - final void Function(SuggestionAction) onTap; - - @override - Widget build(BuildContext context) { - return SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(4.0), - children: _getSuggestedActions() - .map( - (action) => SuggestionActionButton( - action: action, - onTap: () => onTap(action), - ), - ) - .toList(), - ); - } - - List _getSuggestedActions() { - if (hasSelection) { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - AiWriterCommand.fixSpellingAndGrammar || - AiWriterCommand.improveWriting || - AiWriterCommand.makeShorter || - AiWriterCommand.makeLonger => - [ - SuggestionAction.accept, - SuggestionAction.discard, - SuggestionAction.insertBelow, - SuggestionAction.rewrite, - ], - }; - } else { - return switch (currentCommand) { - AiWriterCommand.userQuestion || AiWriterCommand.continueWriting => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - AiWriterCommand.explain => [ - SuggestionAction.insertBelow, - SuggestionAction.tryAgain, - SuggestionAction.close, - ], - _ => [ - SuggestionAction.keep, - SuggestionAction.discard, - SuggestionAction.rewrite, - ], - }; - } - } -} - -class SuggestionActionButton extends StatelessWidget { - const SuggestionActionButton({ - super.key, - required this.action, - required this.onTap, - }); - - final SuggestionAction action; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: FlowyButton( - text: FlowyText( - action.i18n, - figmaLineHeight: 20, - ), - leftIcon: action.buildIcon(context), - iconPadding: 4.0, - margin: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 4.0, - ), - onTap: onTap, - useIntrinsicWidth: true, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart index cceac56c0d..a20618f961 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart @@ -1,20 +1,21 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; const String leftAlignmentKey = 'left'; const String centerAlignmentKey = 'center'; const String rightAlignmentKey = 'right'; -const String kAlignToolbarItemId = 'editor.align'; final alignToolbarItem = ToolbarItem( - id: kAlignToolbarItemId, + id: 'editor.align', group: 4, isActive: onlyShowInTextType, - builder: (context, editorState, highlightColor, _, tooltipBuilder) { + builder: (context, editorState, highlightColor, _) { final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); @@ -37,37 +38,35 @@ final alignToolbarItem = ToolbarItem( data = FlowySvgs.toolbar_align_right_s; } - Widget child = FlowySvg( + final child = FlowySvg( data, size: const Size.square(16), color: isHighlight ? highlightColor : Colors.white, ); - child = _AlignmentButtons( - child: child, - onAlignChanged: (align) async { - await editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: align, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowyTooltip( + message: LocaleKeys.document_plugins_optionAction_align.tr(), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: _AlignmentButtons( + child: child, + onAlignChanged: (align) async { + await editorState.updateNode( + selection, + (node) => node.copyWith( + attributes: { + ...node.attributes, + blockComponentAlign: align, + }, + ), + ); }, ), - ); - }, + ), + ), ); - - if (tooltipBuilder != null) { - child = tooltipBuilder( - context, - kAlignToolbarItemId, - LocaleKeys.document_plugins_optionAction_align.tr(), - child, - ); - } - - return child; }, ); @@ -85,17 +84,17 @@ class _AlignmentButtons extends StatefulWidget { } class _AlignmentButtonsState extends State<_AlignmentButtons> { - final controller = PopoverController(); - @override Widget build(BuildContext context) { return AppFlowyPopover( windowPadding: const EdgeInsets.all(0), - margin: const EdgeInsets.symmetric(vertical: 2.0), + margin: const EdgeInsets.all(4), direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 10), - decorationColor: Theme.of(context).colorScheme.onTertiary, - borderRadius: BorderRadius.circular(6.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onTertiary, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), popupBuilder: (_) { keepEditorFocusNotifier.increase(); return _AlignButtons(onAlignChanged: widget.onAlignChanged); @@ -103,12 +102,7 @@ class _AlignmentButtonsState extends State<_AlignmentButtons> { onClose: () { keepEditorFocusNotifier.decrease(); }, - child: FlowyButton( - useIntrinsicWidth: true, - text: widget.child, - hoverColor: Colors.grey.withValues(alpha: 0.3), - onTap: () => controller.show(), - ), + child: widget.child, ); } } @@ -123,7 +117,7 @@ class _AlignButtons extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 28, + height: 32, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -165,16 +159,17 @@ class _AlignButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - hoverColor: Colors.grey.withValues(alpha: 0.3), - onTap: onTap, - text: FlowyTooltip( - message: tooltips, - child: FlowySvg( - icon, - size: const Size.square(16), - color: Colors.white, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: FlowyTooltip( + message: tooltips, + child: FlowySvg( + icon, + size: const Size.square(16), + color: Colors.white, + ), ), ), ); @@ -187,7 +182,7 @@ class _Divider extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.all(8), child: Container( width: 1, color: Colors.grey, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart index bc3f5cffa1..9fe78591c3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; final List customTextAlignCommands = [ customTextLeftAlignCommand, @@ -25,8 +26,8 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( handler: (editorState) => _textAlignHandler(editorState, leftAlignmentKey), ); -/// Windows / Linux : ctrl + shift + c -/// macOS : ctrl + shift + c +/// Windows / Linux : ctrl + shift + e +/// macOS : ctrl + shift + e /// Allows the user to align text to the center /// /// - support @@ -35,7 +36,7 @@ final CommandShortcutEvent customTextLeftAlignCommand = CommandShortcutEvent( /// final CommandShortcutEvent customTextCenterAlignCommand = CommandShortcutEvent( key: 'Align text to the center', - command: 'ctrl+shift+c', + command: 'ctrl+shift+e', getDescription: LocaleKeys.settings_shortcutsPage_commands_textAlignCenter.tr, handler: (editorState) => _textAlignHandler(editorState, centerAlignmentKey), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/backtick_character_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/backtick_character_command.dart deleted file mode 100644 index ca26c5a263..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/backtick_character_command.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; - -/// ``` to code block -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent formatBacktickToCodeBlock = CharacterShortcutEvent( - key: '``` to code block', - character: '`', - handler: (editorState) async => _convertBacktickToCodeBlock( - editorState: editorState, - ), -); - -Future _convertBacktickToCodeBlock({ - required EditorState editorState, -}) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return false; - } - - final node = editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null || delta.isEmpty) { - return false; - } - - // only active when the backtick is at the beginning of the line - final plainText = delta.toPlainText(); - if (plainText != '``') { - return false; - } - - final transaction = editorState.transaction; - transaction.insertNode( - selection.end.path, - codeBlockNode(), - ); - transaction.deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position(path: selection.start.path), - ); - await editorState.apply(transaction); - - return true; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart index 090ecdce78..d7e8194867 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart @@ -1,13 +1,22 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.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/plugins.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class BuiltInPageWidget extends StatefulWidget { const BuiltInPageWidget({ @@ -56,15 +65,18 @@ class _BuiltInPageWidgetState extends State { if (snapshot.hasData && page != null) { return _build(context, page); } - if (snapshot.connectionState == ConnectionState.done) { - // Delete the page if not found - WidgetsBinding.instance.addPostFrameCallback((_) => _deletePage()); - - return const Center(child: FlowyText('Cannot load the page')); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + // just delete the page if it is not found + _deletePage(); + }); + return const Center( + child: FlowyText('Cannot load the page'), + ); } - - return const Center(child: CircularProgressIndicator()); + return const Center( + child: CircularProgressIndicator(), + ); }, future: future, ); @@ -74,14 +86,20 @@ class _BuiltInPageWidgetState extends State { return MouseRegion( onEnter: (_) => widget.editorState.service.scrollService?.disable(), onExit: (_) => widget.editorState.service.scrollService?.enable(), - child: _buildPage(context, viewPB), + child: SizedBox( + height: viewPB.pluginType == PluginType.calendar ? 700 : 400, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMenu(context, viewPB), + Expanded(child: _buildPage(context, viewPB)), + ], + ), + ), ); } - Widget _buildPage(BuildContext context, ViewPB view) { - final verticalPadding = - context.read()?.verticalPadding ?? - 0.0; + Widget _buildPage(BuildContext context, ViewPB viewPB) { return Focus( focusNode: focusNode, onFocusChange: (value) { @@ -89,10 +107,66 @@ class _BuiltInPageWidgetState extends State { widget.editorState.service.selectionService.clearSelection(); } }, - child: Padding( - padding: EdgeInsets.symmetric(vertical: verticalPadding), - child: widget.builder(view), - ), + child: widget.builder(viewPB), + ); + } + + Widget _buildMenu(BuildContext context, ViewPB viewPB) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // information + FlowyIconButton( + tooltipText: LocaleKeys.tooltip_referencePage.tr( + namedArgs: {'name': viewPB.layout.name}, + ), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + icon: const FlowySvg( + FlowySvgs.information_s, + ), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + ), + // setting + const Space(7, 0), + PopoverActionList<_ActionWrapper>( + direction: PopoverDirection.bottomWithCenterAligned, + actions: _ActionType.values + .map((action) => _ActionWrapper(action)) + .toList(), + buildChild: (controller) => FlowyIconButton( + tooltipText: LocaleKeys.tooltip_openMenu.tr(), + width: 24, + height: 24, + iconPadding: const EdgeInsets.all(3), + iconColorOnHover: Theme.of(context).colorScheme.onSecondary, + icon: const FlowySvg( + FlowySvgs.settings_s, + ), + onPressed: () => controller.show(), + ), + onSelected: (action, controller) async { + switch (action.inner) { + case _ActionType.viewDatabase: + getIt().add( + TabsEvent.openPlugin( + plugin: viewPB.plugin(), + view: viewPB, + ), + ); + break; + case _ActionType.delete: + final transaction = widget.editorState.transaction; + transaction.deleteNode(widget.node); + await widget.editorState.apply(transaction); + break; + } + controller.close(); + }, + ), + ], ); } @@ -102,3 +176,26 @@ class _BuiltInPageWidgetState extends State { await widget.editorState.apply(transaction); } } + +enum _ActionType { + viewDatabase, + delete, +} + +class _ActionWrapper extends ActionCell { + _ActionWrapper(this.inner); + + final _ActionType inner; + + Widget? icon(Color iconColor) => null; + + @override + String get name { + switch (inner) { + case _ActionType.viewDatabase: + return LocaleKeys.tooltip_viewDataBase.tr(); + case _ActionType.delete: + return LocaleKeys.disclosureAction_delete.tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/cover_title_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/cover_title_command.dart deleted file mode 100644 index 20b4b7901e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/cover_title_command.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -/// Press the backspace at the first position of first line to go to the title -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent backspaceToTitle = CommandShortcutEvent( - key: 'backspace to title', - command: 'backspace', - getDescription: () => 'backspace to title', - handler: (editorState) => _backspaceToTitle( - editorState: editorState, - ), -); - -KeyEventResult _backspaceToTitle({ - required EditorState editorState, -}) { - final coverTitleFocusNode = editorState.document.root.context - ?.read() - ?.coverTitleFocusNode; - if (coverTitleFocusNode == null) { - return KeyEventResult.ignored; - } - - final selection = editorState.selection; - // only active when the backspace is at the first position of first line - if (selection == null || - !selection.isCollapsed || - !selection.start.path.equals([0]) || - selection.start.offset != 0) { - return KeyEventResult.ignored; - } - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null || node.type != ParagraphBlockKeys.type) { - return KeyEventResult.ignored; - } - - // delete the first line - () async { - // only delete the first line if it is empty - if (node.delta == null || node.delta!.isEmpty) { - final transaction = editorState.transaction; - transaction.deleteNode(node); - transaction.afterSelection = null; - await editorState.apply(transaction); - } - - editorState.selection = null; - coverTitleFocusNode.requestFocus(); - }(); - - return KeyEventResult.handled; -} - -/// Press the arrow left at the first position of first line to go to the title -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent arrowLeftToTitle = CommandShortcutEvent( - key: 'arrow left to title', - command: 'arrow left', - getDescription: () => 'arrow left to title', - handler: (editorState) => _arrowKeyToTitle( - editorState: editorState, - checkSelection: (selection) { - if (!selection.isCollapsed || - !selection.start.path.equals([0]) || - selection.start.offset != 0) { - return false; - } - return true; - }, - ), -); - -/// Press the arrow up at the first line to go to the title -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent arrowUpToTitle = CommandShortcutEvent( - key: 'arrow up to title', - command: 'arrow up', - getDescription: () => 'arrow up to title', - handler: (editorState) => _arrowKeyToTitle( - editorState: editorState, - checkSelection: (selection) { - if (!selection.isCollapsed || !selection.start.path.equals([0])) { - return false; - } - return true; - }, - ), -); - -KeyEventResult _arrowKeyToTitle({ - required EditorState editorState, - required bool Function(Selection selection) checkSelection, -}) { - final coverTitleFocusNode = editorState.document.root.context - ?.read() - ?.coverTitleFocusNode; - if (coverTitleFocusNode == null) { - return KeyEventResult.ignored; - } - - final selection = editorState.selection; - // only active when the arrow up is at the first line - if (selection == null || !checkSelection(selection)) { - return KeyEventResult.ignored; - } - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return KeyEventResult.ignored; - } - - editorState.selection = null; - coverTitleFocusNode.requestFocus(); - - return KeyEventResult.handled; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index 93b45cf46a..2a1101794a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -1,11 +1,11 @@ import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; class EmojiPickerButton extends StatelessWidget { EmojiPickerButton({ @@ -19,204 +19,82 @@ class EmojiPickerButton extends StatelessWidget { this.direction, this.title, this.showBorder = true, - this.enable = true, - this.margin, - this.buttonSize, - this.documentId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final EmojiIconData emoji; + final String emoji; final double emojiSize; final Size emojiPickerSize; - final void Function( - SelectedEmojiIconResult result, - PopoverController? controller, - ) onSubmitted; + final void Function(String emoji, PopoverController? controller) onSubmitted; final PopoverController popoverController = PopoverController(); final Widget? defaultIcon; final Offset? offset; final PopoverDirection? direction; final String? title; final bool showBorder; - final bool enable; - final EdgeInsets? margin; - final Size? buttonSize; - final String? documentId; - final List tabs; @override Widget build(BuildContext context) { - if (UniversalPlatform.isDesktopOrWeb) { - return _DesktopEmojiPickerButton( - emoji: emoji, - onSubmitted: onSubmitted, - emojiPickerSize: emojiPickerSize, - emojiSize: emojiSize, - defaultIcon: defaultIcon, + if (PlatformExtension.isDesktopOrWeb) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.expand( + width: emojiPickerSize.width, + height: emojiPickerSize.height, + ), offset: offset, - direction: direction, - title: title, - showBorder: showBorder, - enable: enable, - buttonSize: buttonSize, - tabs: tabs, - documentId: documentId, + direction: direction ?? PopoverDirection.rightWithTopAligned, + popupBuilder: (_) => Container( + width: emojiPickerSize.width, + height: emojiPickerSize.height, + padding: const EdgeInsets.all(4.0), + child: EmojiSelectionMenu( + onSubmitted: (emoji) => onSubmitted(emoji, popoverController), + onExit: () {}, + ), + ), + child: Container( + width: 30.0, + height: 30.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: showBorder + ? Border.all( + color: Theme.of(context).dividerColor, + ) + : null, + ), + child: FlowyButton( + margin: emoji.isEmpty && defaultIcon != null + ? EdgeInsets.zero + : const EdgeInsets.only(left: 2.0), + expandText: false, + text: emoji.isEmpty && defaultIcon != null + ? defaultIcon! + : FlowyText.emoji(emoji, fontSize: emojiSize), + onTap: popoverController.show, + ), + ), ); } - - return _MobileEmojiPickerButton( - emoji: emoji, - onSubmitted: onSubmitted, - emojiSize: emojiSize, - enable: enable, - title: title, - margin: margin, - tabs: tabs, - documentId: documentId, - ); - } -} - -class _DesktopEmojiPickerButton extends StatelessWidget { - _DesktopEmojiPickerButton({ - required this.emoji, - required this.onSubmitted, - this.emojiPickerSize = const Size(360, 380), - this.emojiSize = 18.0, - this.defaultIcon, - this.offset, - this.direction, - this.title, - this.showBorder = true, - this.enable = true, - this.buttonSize, - this.documentId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); - - final EmojiIconData emoji; - final double emojiSize; - final Size emojiPickerSize; - final void Function( - SelectedEmojiIconResult result, - PopoverController? controller, - ) onSubmitted; - final PopoverController popoverController = PopoverController(); - final Widget? defaultIcon; - final Offset? offset; - final PopoverDirection? direction; - final String? title; - final bool showBorder; - final bool enable; - final Size? buttonSize; - final String? documentId; - final List tabs; - - @override - Widget build(BuildContext context) { - final showDefault = emoji.isEmpty && defaultIcon != null; - return AppFlowyPopover( - controller: popoverController, - constraints: BoxConstraints.expand( - width: emojiPickerSize.width, - height: emojiPickerSize.height, - ), - offset: offset, - margin: EdgeInsets.zero, - direction: direction ?? PopoverDirection.rightWithTopAligned, - popupBuilder: (_) => Container( - width: emojiPickerSize.width, - height: emojiPickerSize.height, - padding: const EdgeInsets.all(4.0), - child: FlowyIconEmojiPicker( - initialType: emoji.type.toPickerTabType(), - tabs: tabs, - documentId: documentId, - onSelectedEmoji: (r) { - onSubmitted(r, popoverController); - }, - ), - ), - child: Container( - width: buttonSize?.width ?? 30.0, - height: buttonSize?.height ?? 30.0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: showBorder - ? Border.all( - color: Theme.of(context).dividerColor, - ) - : null, - ), - child: FlowyButton( - margin: emoji.isEmpty && defaultIcon != null - ? EdgeInsets.zero - : const EdgeInsets.only(left: 2.0), - expandText: false, - text: showDefault - ? defaultIcon! - : RawEmojiIconWidget(emoji: emoji, emojiSize: emojiSize), - onTap: enable ? popoverController.show : null, - ), - ), - ); - } -} - -class _MobileEmojiPickerButton extends StatelessWidget { - const _MobileEmojiPickerButton({ - required this.emoji, - required this.onSubmitted, - this.emojiSize = 18.0, - this.enable = true, - this.title, - this.margin, - this.documentId, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], - }); - - final EmojiIconData emoji; - final double emojiSize; - final void Function( - SelectedEmojiIconResult result, - PopoverController? controller, - ) onSubmitted; - final String? title; - final bool enable; - final EdgeInsets? margin; - final String? documentId; - final List tabs; - - @override - Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - margin: - margin ?? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), - text: RawEmojiIconWidget( - emoji: emoji, - emojiSize: emojiSize, - ), - onTap: enable - ? () async { - final result = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.pageTitle: title, - MobileEmojiPickerScreen.iconSelectedType: emoji.type.name, - MobileEmojiPickerScreen.uploadDocumentId: documentId, - MobileEmojiPickerScreen.selectTabs: - tabs.map((e) => e.name).toList().join('-'), - }, - ).toString(), - ); - if (result != null) { - onSubmitted(result.toSelectedResult(), null); - } - } - : null, + return FlowyTextButton( + emoji, + overflow: TextOverflow.visible, + fontSize: emojiSize, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 35.0), + fillColor: Colors.transparent, + mainAxisAlignment: MainAxisAlignment.center, + onPressed: () async { + final result = await context.push( + Uri( + path: MobileEmojiPickerScreen.routeName, + queryParameters: {MobileEmojiPickerScreen.pageTitle: title}, + ).toString(), + ); + if (result != null) { + onSubmitted(result.emoji, null); + } + }, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart index 8548b9354c..6e6e111df1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart @@ -2,13 +2,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; const _greater = '>'; -const _dash = '-'; const _equals = '='; -const _equalGreater = '⇒'; -const _dashGreater = '→'; - -const _hyphen = '-'; -const _emDash = '—'; // This is an em dash — not a single dash - !! +const _arrow = '⇒'; /// format '=' + '>' into an ⇒ /// @@ -23,47 +18,11 @@ final CharacterShortcutEvent customFormatGreaterEqual = CharacterShortcutEvent( handler: (editorState) async => _handleDoubleCharacterReplacement( editorState: editorState, character: _greater, - replacement: _equalGreater, + replacement: _arrow, prefixCharacter: _equals, ), ); -/// format '-' + '>' into ⇒ -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent customFormatDashGreater = CharacterShortcutEvent( - key: 'format - + > into ->', - character: _greater, - handler: (editorState) async => _handleDoubleCharacterReplacement( - editorState: editorState, - character: _greater, - replacement: _dashGreater, - prefixCharacter: _dash, - ), -); - -/// format two hyphens into an em dash -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent customFormatDoubleHyphenEmDash = - CharacterShortcutEvent( - key: 'format double hyphen into an em dash', - character: _hyphen, - handler: (editorState) async => _handleDoubleCharacterReplacement( - editorState: editorState, - character: _hyphen, - replacement: _emDash, - ), -); - /// If [prefixCharacter] is null or empty, [character] is used Future _handleDoubleCharacterReplacement({ required EditorState editorState, @@ -102,29 +61,11 @@ Future _handleDoubleCharacterReplacement({ return false; } - // insert the greater character first and convert it to the replacement character to support undo - final insert = editorState.transaction - ..insertText( - node, - selection.end.offset, - character, - ); - - await editorState.apply( - insert, - skipHistoryDebounce: true, - ); - - final afterSelection = editorState.selection; - if (afterSelection == null) { - return false; - } - final replace = editorState.transaction ..replaceText( node, - afterSelection.end.offset - 2, - 2, + selection.end.offset - 1, + 1, replacement, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart index 11aed036d2..9bcd0e18b8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/insert_page_command.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/domain/database_view_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -70,12 +70,13 @@ extension InsertDatabase on EditorState { node, selection.end.offset, 0, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), + r'$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + }, + }, ); } @@ -97,7 +98,7 @@ extension InsertDatabase on EditorState { final prefix = _referencedDatabasePrefix(view.layout); final ref = await ViewBackendService.createDatabaseLinkedView( parentViewId: view.id, - name: "$prefix ${view.nameOrDefault}", + name: "$prefix ${view.name}", layoutType: view.layout, databaseId: databaseId, ).then((value) => value.toNullable()); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart index 3f1440e100..0dcb8dd3e6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; @@ -6,18 +8,13 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_service.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:flutter/material.dart'; InlineActionsMenuService? _actionsMenuService; - Future showLinkToPageMenu( EditorState editorState, - SelectionMenuService menuService, { - ViewLayoutPB? pageType, - bool? insertPage, -}) async { - keepEditorFocusNotifier.increase(); - + SelectionMenuService menuService, + ViewLayoutPB pageType, +) async { menuService.dismiss(); _actionsMenuService?.dismiss(); @@ -30,10 +27,10 @@ Future showLinkToPageMenu( context: rootContext, handlers: [ InlinePageReferenceService( - currentViewId: '', + currentViewId: "", viewLayout: pageType, customTitle: titleFromPageType(pageType), - insertPage: insertPage ?? pageType != ViewLayoutPB.Document, + insertPage: pageType != ViewLayoutPB.Document, limitResults: 15, ), ], @@ -61,11 +58,11 @@ Future showLinkToPageMenu( startCharAmount: 0, ); - await _actionsMenuService?.show(); + _actionsMenuService?.show(); } } -String titleFromPageType(ViewLayoutPB? layout) => switch (layout) { +String titleFromPageType(ViewLayoutPB layout) => switch (layout) { ViewLayoutPB.Grid => LocaleKeys.inlineActions_gridReference.tr(), ViewLayoutPB.Document => LocaleKeys.inlineActions_docReference.tr(), ViewLayoutPB.Board => LocaleKeys.inlineActions_boardReference.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart deleted file mode 100644 index 259777db94..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart +++ /dev/null @@ -1,566 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:synchronized/synchronized.dart'; - -const _enableDebug = false; - -class MarkdownTextRobot { - MarkdownTextRobot({ - required this.editorState, - }); - - final EditorState editorState; - - final Lock _lock = Lock(); - - /// The text position where new nodes will be inserted - Position? _insertPosition; - - /// The markdown text to be inserted - String _markdownText = ''; - - /// The nodes inserted in the previous refresh. - Iterable _insertedNodes = []; - - /// Only for debug via [_enableDebug]. - final List _debugMarkdownTexts = []; - - /// Selection before the refresh. - Selection? _previousSelection; - - bool get hasAnyResult => _markdownText.isNotEmpty; - - String get markdownText => _markdownText; - - Selection? getInsertedSelection() { - final position = _insertPosition; - if (position == null) { - Log.error("Expected non-null insert markdown text position"); - return null; - } - - if (_insertedNodes.isEmpty) { - return Selection.collapsed(position); - } - return Selection( - start: position, - end: Position( - path: position.path.nextNPath(_insertedNodes.length - 1), - ), - ); - } - - List getInsertedNodes() { - final selection = getInsertedSelection(); - return selection == null ? [] : editorState.getNodesInSelection(selection); - } - - void start({ - Selection? previousSelection, - Position? position, - }) { - _insertPosition = position ?? editorState.selection?.start; - _previousSelection = previousSelection ?? editorState.selection; - - if (_enableDebug) { - Log.info( - 'MarkdownTextRobot start with insert text position: $_insertPosition', - ); - } - } - - /// The text will be inserted into the document but only in memory - Future appendMarkdownText( - String text, { - bool updateSelection = true, - Map? attributes, - }) async { - _markdownText += text; - - await _lock.synchronized(() async { - await _refresh( - inMemoryUpdate: true, - updateSelection: updateSelection, - attributes: attributes, - ); - }); - - if (_enableDebug) { - _debugMarkdownTexts.add(text); - Log.info( - 'MarkdownTextRobot receive markdown: ${jsonEncode(_debugMarkdownTexts)}', - ); - } - } - - Future stop({ - Map? attributes, - }) async { - await _lock.synchronized(() async { - await _refresh( - inMemoryUpdate: true, - attributes: attributes, - ); - }); - } - - /// Persist the text into the document - Future persist({ - String? markdownText, - }) async { - if (markdownText != null) { - _markdownText = markdownText; - } - - await _lock.synchronized(() async { - await _refresh(inMemoryUpdate: false, updateSelection: true); - }); - - if (_enableDebug) { - Log.info('MarkdownTextRobot stop'); - _debugMarkdownTexts.clear(); - } - } - - /// Replace the selected content with the AI's response - Future replace({ - required Selection selection, - required String markdownText, - }) async { - if (selection.isSingle) { - await _replaceInSameLine( - selection: selection, - markdownText: markdownText, - ); - } else { - await _replaceInMultiLines( - selection: selection, - markdownText: markdownText, - ); - } - } - - /// Delete the temporary inserted AI nodes - Future deleteAINodes() async { - final nodes = getInsertedNodes(); - final transaction = editorState.transaction..deleteNodes(nodes); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - ); - } - - /// Discard the inserted content - Future discard({ - Selection? afterSelection, - }) async { - final start = _insertPosition; - if (start == null) { - return; - } - if (_insertedNodes.isEmpty) { - return; - } - - afterSelection ??= Selection.collapsed(start); - - // fallback to the calculated position if the selection is null. - final end = Position( - path: start.path.nextNPath(_insertedNodes.length - 1), - ); - final deletedNodes = editorState.getNodesInSelection( - Selection(start: start, end: end), - ); - final transaction = editorState.transaction - ..deleteNodes(deletedNodes) - ..afterSelection = afterSelection; - - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false, inMemoryUpdate: true), - ); - - if (_enableDebug) { - Log.info('MarkdownTextRobot discard'); - } - } - - void clear() { - _markdownText = ''; - _insertedNodes = []; - } - - void reset() { - _insertPosition = null; - } - - Future _refresh({ - required bool inMemoryUpdate, - bool updateSelection = false, - Map? attributes, - }) async { - final position = _insertPosition; - if (position == null) { - Log.error("Expected non-null insert markdown text position"); - return; - } - - // Convert markdown and deep copy the nodes, prevent ing the linked - // entities from being changed - final documentNodes = customMarkdownToDocument( - _markdownText, - tableWidth: 250.0, - ).root.children; - - // check if the first selected node before the refresh is a numbered list node - final previousSelection = _previousSelection; - final previousSelectedNode = previousSelection == null - ? null - : editorState.getNodeAtPath(previousSelection.start.path); - final firstNodeIsNumberedList = previousSelectedNode != null && - previousSelectedNode.type == NumberedListBlockKeys.type; - - final newNodes = attributes == null - ? documentNodes - : documentNodes.mapIndexed((index, node) { - final n = _styleDelta(node: node, attributes: attributes); - n.externalValues = AINodeExternalValues( - isAINode: true, - ); - if (index == 0 && n.type == NumberedListBlockKeys.type) { - if (firstNodeIsNumberedList) { - final builder = NumberedListIndexBuilder( - editorState: editorState, - node: previousSelectedNode, - ); - final firstIndex = builder.indexInSameLevel; - n.updateAttributes({ - NumberedListBlockKeys.number: firstIndex, - }); - } - - n.externalValues = AINodeExternalValues( - isAINode: true, - isFirstNumberedListNode: true, - ); - } - return n; - }).toList(); - - if (newNodes.isEmpty) { - return; - } - - final deleteTransaction = editorState.transaction - ..deleteNodes(getInsertedNodes()); - - await editorState.apply( - deleteTransaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - recordUndo: false, - ), - ); - - final insertTransaction = editorState.transaction - ..insertNodes(position.path, newNodes); - - final lastDelta = newNodes.lastOrNull?.delta; - if (lastDelta != null) { - insertTransaction.afterSelection = Selection.collapsed( - Position( - path: position.path.nextNPath(newNodes.length - 1), - offset: lastDelta.length, - ), - ); - } - - await editorState.apply( - insertTransaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - recordUndo: !inMemoryUpdate, - ), - withUpdateSelection: updateSelection, - ); - - _insertedNodes = newNodes; - } - - Node _styleDelta({ - required Node node, - required Map attributes, - }) { - if (node.delta != null) { - final delta = node.delta!; - final attributeDelta = Delta() - ..retain(delta.length, attributes: attributes); - final newDelta = delta.compose(attributeDelta); - final newAttributes = node.attributes; - newAttributes['delta'] = newDelta.toJson(); - node.updateAttributes(newAttributes); - } - - List? children; - if (node.children.isNotEmpty) { - children = node.children - .map((child) => _styleDelta(node: child, attributes: attributes)) - .toList(); - } - - return node.copyWith( - children: children, - ); - } - - /// If the selected content is in the same line, - /// keep the selected node and replace the delta. - Future _replaceInSameLine({ - required Selection selection, - required String markdownText, - }) async { - if (markdownText.isEmpty) { - assert(false, 'Expected non-empty markdown text'); - Log.error('Expected non-empty markdown text'); - return; - } - - selection = selection.normalized; - - // If the selection is not a single node, do nothing. - if (!selection.isSingle) { - assert(false, 'Expected single node selection'); - Log.error('Expected single node selection'); - return; - } - - final startIndex = selection.startIndex; - final endIndex = selection.endIndex; - final length = endIndex - startIndex; - - // Get the selected node. - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - assert(false, 'Expected non-null node and delta'); - Log.error('Expected non-null node and delta'); - return; - } - - // Convert the markdown text to delta. - // Question: Why we need to convert the markdown to document first? - // Answer: Because the markdown text may contain the list item, - // if we convert the markdown to delta directly, the list item will be - // treated as a normal text node, and the delta will be incorrect. - // For example, the markdown text is: - // ``` - // 1. item1 - // ``` - // if we convert the markdown to delta directly, the delta will be: - // ``` - // [ - // { - // "insert": "1. item1" - // } - // ] - // ``` - // if we convert the markdown to document first, the document will be: - // ``` - // [ - // { - // "type": "numbered_list", - // "children": [ - // { - // "insert": "item1" - // } - // ] - // } - // ] - final document = customMarkdownToDocument(markdownText); - final nodes = document.root.children; - final decoder = DeltaMarkdownDecoder(); - final markdownDelta = - nodes.firstOrNull?.delta ?? decoder.convert(markdownText); - - if (markdownDelta.isEmpty) { - assert(false, 'Expected non-empty markdown delta'); - Log.error('Expected non-empty markdown delta'); - return; - } - - // Replace the delta of the selected node. - final transaction = editorState.transaction; - - // it means the user selected the entire sentence, we just replace the node - if (startIndex == 0 && length == node.delta?.length) { - if (nodes.isNotEmpty && node.children.isNotEmpty) { - // merge the children of the selected node and the first node of the ai response - nodes[0] = nodes[0].copyWith( - children: [ - ...node.children.map((e) => e.deepCopy()), - ...nodes[0].children, - ], - ); - } - transaction - ..insertNodes(node.path.next, nodes) - ..deleteNode(node); - } else { - // it means the user selected a part of the sentence, we need to delete the - // selected part and insert the new delta. - transaction - ..deleteText(node, startIndex, length) - ..insertTextDelta(node, startIndex, markdownDelta); - - // Add the remaining nodes to the document. - final remainingNodes = nodes.skip(1); - if (remainingNodes.isNotEmpty) { - transaction.insertNodes( - node.path.next, - remainingNodes, - ); - } - } - - await editorState.apply(transaction); - } - - /// If the selected content is in multiple lines - Future _replaceInMultiLines({ - required Selection selection, - required String markdownText, - }) async { - selection = selection.normalized; - - // If the selection is a single node, do nothing. - if (selection.isSingle) { - assert(false, 'Expected multi-line selection'); - Log.error('Expected multi-line selection'); - return; - } - - final markdownNodes = customMarkdownToDocument( - markdownText, - tableWidth: 250.0, - ).root.children; - - // Get the selected nodes. - final flattenNodes = editorState.getNodesInSelection(selection); - final nodes = []; - for (final node in flattenNodes) { - if (nodes.any((element) => element.isParentOf(node))) { - continue; - } - nodes.add(node); - } - - // Note: Don't change its order, otherwise the delta will be incorrect. - // step 1. merge the first selected node and the first node from the ai response - // step 2. merge the last selected node and the last node from the ai response - // step 3. insert the middle nodes from the ai response - // step 4. delete the middle nodes - final transaction = editorState.transaction; - - // step 1 - final firstNode = nodes.firstOrNull; - final delta = firstNode?.delta; - final firstMarkdownNode = markdownNodes.firstOrNull; - final firstMarkdownDelta = firstMarkdownNode?.delta; - if (firstNode != null && - delta != null && - firstMarkdownNode != null && - firstMarkdownDelta != null) { - final startIndex = selection.startIndex; - final length = delta.length - startIndex; - - transaction - ..deleteText(firstNode, startIndex, length) - ..insertTextDelta(firstNode, startIndex, firstMarkdownDelta); - - // if the first markdown node has children, we need to insert the children - // and delete the children of the first node that are in the selection. - if (firstMarkdownNode.children.isNotEmpty) { - transaction.insertNodes( - firstNode.path.child(0), - firstMarkdownNode.children.map((e) => e.deepCopy()), - ); - } - - final nodesToDelete = - firstNode.children.where((e) => e.path.inSelection(selection)); - transaction.deleteNodes(nodesToDelete); - } - - // step 2 - bool handledLastNode = false; - final lastNode = nodes.lastOrNull; - final lastDelta = lastNode?.delta; - final lastMarkdownNode = markdownNodes.lastOrNull; - final lastMarkdownDelta = lastMarkdownNode?.delta; - if (lastNode != null && - lastDelta != null && - lastMarkdownNode != null && - lastMarkdownDelta != null && - firstNode?.id != lastNode.id) { - handledLastNode = true; - - final endIndex = selection.endIndex; - - transaction.deleteText(lastNode, 0, endIndex); - - // if the last node is same as the first node, it means we have replaced the - // selected text in the first node. - if (lastMarkdownNode.id != firstMarkdownNode?.id) { - transaction.insertTextDelta(lastNode, 0, lastMarkdownDelta); - - if (lastMarkdownNode.children.isNotEmpty) { - transaction - ..insertNodes( - lastNode.path.child(0), - lastMarkdownNode.children.map((e) => e.deepCopy()), - ) - ..deleteNodes( - lastNode.children.where((e) => e.path.inSelection(selection)), - ); - } - } - } - - // step 3 - final insertedPath = selection.start.path.nextNPath(1); - final insertLength = handledLastNode ? 2 : 1; - if (markdownNodes.length > insertLength) { - transaction.insertNodes( - insertedPath, - markdownNodes - .skip(1) - .take(markdownNodes.length - insertLength) - .toList(), - ); - } - - // step 4 - final length = nodes.length - 2; - if (length > 0) { - final middleNodes = nodes.skip(1).take(length).toList(); - transaction.deleteNodes(middleNodes); - } - - await editorState.apply(transaction); - } -} - -class AINodeExternalValues extends NodeExternalValues { - const AINodeExternalValues({ - this.isAINode = false, - this.isFirstNumberedListNode = false, - }); - - final bool isAINode; - final bool isFirstNumberedListNode; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart index 007e4ea298..42ee1a63d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -1,13 +1,10 @@ -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/handlers/child_page.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/inline_actions/handlers/inline_page_reference.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; const _bracketChar = '['; const _plusChar = '+'; @@ -48,7 +45,6 @@ CharacterShortcutEvent pageReferenceShortcutPlusSign( ); InlineActionsMenuService? selectionMenuService; - Future inlinePageReferenceCommandHandler( String character, BuildContext context, @@ -58,7 +54,7 @@ Future inlinePageReferenceCommandHandler( String? previousChar, }) async { final selection = editorState.selection; - if (selection == null) { + if (PlatformExtension.isMobile || selection == null) { return false; } @@ -91,8 +87,6 @@ Future inlinePageReferenceCommandHandler( final service = InlineActionsService( context: context, handlers: [ - if (FeatureFlag.inlineSubPageMention.isOn) - InlineChildPageService(currentViewId: currentViewId), InlinePageReferenceService( currentViewId: currentViewId, limitResults: 10, @@ -112,63 +106,17 @@ Future inlinePageReferenceCommandHandler( } if (context.mounted) { - keepEditorFocusNotifier.increase(); - selectionMenuService?.dismiss(); - selectionMenuService = UniversalPlatform.isMobile - ? MobileInlineActionsMenu( - context: service.context!, - editorState: editorState, - service: service, - initialResults: initialResults, - startCharAmount: previousChar != null ? 2 : 1, - style: style, - ) - : InlineActionsMenu( - context: service.context!, - editorState: editorState, - service: service, - initialResults: initialResults, - style: style, - startCharAmount: previousChar != null ? 2 : 1, - cancelBySpaceHandler: () { - if (character == _plusChar) { - final currentSelection = editorState.selection; - if (currentSelection == null) { - return false; - } - // check if the space is after the character - if (currentSelection.isCollapsed && - currentSelection.start.offset == - selection.start.offset + character.length) { - _cancelInlinePageReferenceMenu(editorState); - return true; - } - } - return false; - }, - ); - // disable the keyboard service - editorState.service.keyboardService?.disable(); + selectionMenuService = InlineActionsMenu( + context: service.context!, + editorState: editorState, + service: service, + initialResults: initialResults, + style: style, + startCharAmount: previousChar != null ? 2 : 1, + ); - await selectionMenuService?.show(); - - // enable the keyboard service - editorState.service.keyboardService?.enable(); + selectionMenuService?.show(); } return true; } - -void _cancelInlinePageReferenceMenu(EditorState editorState) { - selectionMenuService?.dismiss(); - selectionMenuService = null; - - // re-focus the selection - final selection = editorState.selection; - if (selection != null) { - editorState.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart index 3c997bbdc4..c56fbd09e9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart @@ -1,10 +1,8 @@ -import 'dart:math'; - -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + class SelectableItemListMenu extends StatelessWidget { const SelectableItemListMenu({ super.key, @@ -12,13 +10,11 @@ class SelectableItemListMenu extends StatelessWidget { required this.selectedIndex, required this.onSelected, this.shrinkWrap = false, - this.controller, }); final List items; final int selectedIndex; final void Function(int) onSelected; - final ItemScrollController? controller; /// shrinkWrapping is useful in cases where you have a list of /// limited amount of items. It will make the list take the minimum @@ -28,12 +24,9 @@ class SelectableItemListMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return ScrollablePositionedList.builder( - physics: const ClampingScrollPhysics(), + return ListView.builder( shrinkWrap: shrinkWrap, itemCount: items.length, - itemScrollController: controller, - initialScrollIndex: max(0, selectedIndex), itemBuilder: (context, index) => SelectableItem( isSelected: index == selectedIndex, item: items[index], @@ -60,11 +53,8 @@ class SelectableItem extends StatelessWidget { return SizedBox( height: 32, child: FlowyButton( - text: FlowyText.medium( - item, - lineHeight: 1.0, - ), - isSelected: isSelected, + text: FlowyText.medium(item), + rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, onTap: onTap, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart index f1b082e0a7..8f3e4b7477 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; + import 'package:flutter/material.dart'; class SelectableSvgWidget extends StatelessWidget { @@ -8,31 +9,21 @@ class SelectableSvgWidget extends StatelessWidget { required this.data, required this.isSelected, required this.style, - this.size, - this.padding, }); final FlowySvgData data; final bool isSelected; final SelectionMenuStyle style; - final Size? size; - final EdgeInsets? padding; @override Widget build(BuildContext context) { - final child = FlowySvg( + return FlowySvg( data, - size: size ?? const Size.square(16.0), + size: const Size.square(18.0), color: isSelected ? style.selectionMenuItemSelectedIconColor : style.selectionMenuItemIconColor, ); - - if (padding != null) { - return Padding(padding: padding!, child: child); - } else { - return child; - } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart index 254c3d53bf..532fc5e434 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/string_extension.dart @@ -1,6 +1,5 @@ extension Capitalize on String { String capitalize() { - if (isEmpty) return this; return "${this[0].toUpperCase()}${substring(1)}"; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart index a20ea9aec5..f50cf7f0e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/text_robot.dart @@ -1,143 +1,59 @@ -import 'dart:async'; - import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:synchronized/synchronized.dart'; enum TextRobotInputType { character, word, - sentence, } class TextRobot { - TextRobot({ + const TextRobot({ required this.editorState, }); final EditorState editorState; - final Lock lock = Lock(); - /// This function is used to insert text in a synchronized way - /// - /// It is suitable for inserting text in a loop. - Future autoInsertTextSync( - String text, { - TextRobotInputType inputType = TextRobotInputType.word, - Duration delay = const Duration(milliseconds: 10), - String separator = '\n', - }) async { - await lock.synchronized(() async { - await autoInsertText( - text, - inputType: inputType, - delay: delay, - separator: separator, - ); - }); - } - - /// This function is used to insert text in an asynchronous way - /// - /// It is suitable for inserting a long paragraph or a long sentence. Future autoInsertText( String text, { TextRobotInputType inputType = TextRobotInputType.word, Duration delay = const Duration(milliseconds: 10), - String separator = '\n', }) async { - if (text == separator) { - await insertNewParagraph(delay); - return; + if (text == '\n') { + return editorState.insertNewLine(); } - final lines = _splitText(text, separator); + final lines = text.split('\n'); for (final line in lines) { if (line.isEmpty) { - await insertNewParagraph(delay); + await editorState.insertNewLine(); continue; } switch (inputType) { case TextRobotInputType.character: - await insertCharacter(line, delay); + final iterator = line.runes.iterator; + while (iterator.moveNext()) { + await editorState.insertTextAtCurrentSelection( + iterator.currentAsString, + ); + await Future.delayed(delay); + } break; case TextRobotInputType.word: - await insertWord(line, delay); - break; - case TextRobotInputType.sentence: - await insertSentence(line, delay); + final words = line.split(' '); + if (words.length == 1 || + (words.length == 2 && + (words.first.isEmpty || words.last.isEmpty))) { + await editorState.insertTextAtCurrentSelection( + line, + ); + } else { + for (final word in words.map((e) => '$e ')) { + await editorState.insertTextAtCurrentSelection( + word, + ); + } + } + await Future.delayed(delay); break; } } } - - Future insertCharacter(String line, Duration delay) async { - final iterator = line.runes.iterator; - while (iterator.moveNext()) { - await insertText(iterator.currentAsString, delay); - } - } - - Future insertWord(String line, Duration delay) async { - final words = line.split(' '); - if (words.length == 1 || - (words.length == 2 && (words.first.isEmpty || words.last.isEmpty))) { - await insertText(line, delay); - } else { - for (final word in words.map((e) => '$e ')) { - await insertText(word, delay); - } - } - await Future.delayed(delay); - } - - Future insertSentence(String line, Duration delay) async { - await insertText(line, delay); - } - - Future insertNewParagraph(Duration delay) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final next = selection.end.path.next; - final transaction = editorState.transaction; - transaction.insertNode( - next, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed( - Position(path: next), - ); - await editorState.apply(transaction); - await Future.delayed(const Duration(milliseconds: 10)); - } - - Future insertText(String text, Duration delay) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final transaction = editorState.transaction; - transaction.insertText(node, selection.endIndex, text); - await editorState.apply(transaction); - await Future.delayed(delay); - } -} - -List _splitText(String text, String separator) { - final parts = text.split(RegExp(separator)); - final result = []; - - for (int i = 0; i < parts.length; i++) { - result.add(parts[i]); - // Only add empty string if it's not the last part and the next part is not empty - if (i < parts.length - 1 && parts[i + 1].isNotEmpty) { - result.add(''); - } - } - - return result; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart index 77245a9f95..ebbfb27db6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart @@ -1,14 +1,5 @@ -import 'dart:ui'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -bool _isTableType(String type) { - return [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(type); -} - bool notShowInTable(EditorState editorState) { final selection = editorState.selection; if (selection == null) { @@ -16,12 +7,12 @@ bool notShowInTable(EditorState editorState) { } final nodes = editorState.getNodesInSelection(selection); return nodes.every((element) { - if (_isTableType(element.type)) { + if (element.type == TableBlockKeys.type) { return false; } var parent = element.parent; while (parent != null) { - if (_isTableType(parent.type)) { + if (parent.type == TableBlockKeys.type) { return false; } parent = parent.parent; @@ -36,31 +27,3 @@ bool onlyShowInSingleTextTypeSelectionAndExcludeTable( return onlyShowInSingleSelectionAndTextType(editorState) && notShowInTable(editorState); } - -bool enableSuggestions(EditorState editorState) { - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return false; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null) { - return false; - } - if (isNarrowWindow(editorState)) return false; - - return (node.delta != null && suggestionsItemTypes.contains(node.type)) && - notShowInTable(editorState); -} - -bool isNarrowWindow(EditorState editorState) { - final editorSize = editorState.renderBox?.size ?? Size.zero; - if (editorSize.width < 650) return true; - return false; -} - -final Set suggestionsItemTypes = { - ...toolbarItemWhiteList, - ToggleListBlockKeys.type, - TodoListBlockKeys.type, - CalloutBlockKeys.type, -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart index 735b6b15df..120343a277 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; class MenuBlockButton extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart deleted file mode 100644 index 80ac61a406..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// A handler for transactions that involve a Block Component. -/// -abstract class BlockTransactionHandler { - const BlockTransactionHandler({required this.blockType}); - - /// The type of the block that this handler is built for. - /// It's used to determine whether to call any of the handlers on certain transactions. - /// - final String blockType; - - Future onTransaction( - BuildContext context, - EditorState editorState, - List added, - List removed, { - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - String? parentViewId, - }); - - void onUndo( - BuildContext context, - EditorState editorState, - List before, - List after, - ); - - void onRedo( - BuildContext context, - EditorState editorState, - List before, - List after, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart index 23b73e75a9..abb166ec12 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/bulleted_list/bulleted_list_icon.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -29,23 +30,22 @@ class BulletedListIcon extends StatelessWidget { return level; } + FlowySvg get icon { + final index = level % bulletedListIcons.length; + return FlowySvg(bulletedListIcons[index]); + } + @override Widget build(BuildContext context) { - final textStyle = - context.read().editorStyle.textStyleConfiguration; - final fontSize = textStyle.text.fontSize ?? 16.0; - final height = textStyle.text.height ?? textStyle.lineHeight; - final size = fontSize * height; - final index = level % bulletedListIcons.length; - final icon = FlowySvg( - bulletedListIcons[index], - size: Size.square(size * 0.8), - ); + final iconPadding = PlatformExtension.isMobile + ? context.read().state.iconPadding + : 0.0; return Container( - width: size, - height: size, - margin: const EdgeInsets.only(right: 8.0), - alignment: Alignment.center, + constraints: const BoxConstraints( + minWidth: 22, + minHeight: 22, + ), + margin: EdgeInsets.only(top: iconPadding, right: 8.0), child: icon, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart index a7fcccd186..e025dd6d27 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart @@ -1,8 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart' show LocaleKeys; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; @@ -11,7 +8,6 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../base/emoji_picker_button.dart'; @@ -35,22 +31,17 @@ class CalloutBlockKeys { /// /// The value is a String. static const String icon = 'icon'; - - /// the type of [FlowyIconType] - static const String iconType = 'icon_type'; } // The one is inserted through selection menu Node calloutNode({ Delta? delta, - EmojiIconData? emoji, + String emoji = '📌', Color? defaultColor, }) { - final defaultEmoji = emoji ?? EmojiIconData.emoji('📌'); final attributes = { CalloutBlockKeys.delta: (delta ?? Delta()).toJson(), - CalloutBlockKeys.icon: defaultEmoji.emoji, - CalloutBlockKeys.iconType: defaultEmoji.type, + CalloutBlockKeys.icon: emoji, CalloutBlockKeys.backgroundColor: defaultColor?.toHex(), }; return Node( @@ -77,11 +68,9 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { CalloutBlockComponentBuilder({ super.configuration, required this.defaultColor, - required this.inlinePadding, }); final Color defaultColor; - final EdgeInsets Function(Node node) inlinePadding; @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { @@ -90,22 +79,21 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder { key: node.key, node: node, defaultColor: defaultColor, - inlinePadding: inlinePadding, configuration: configuration, showActions: showActions(node), actionBuilder: (context, state) => actionBuilder( blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } + // validate the data of the node, if the result is false, the node will be rendered as a placeholder @override - BlockComponentValidate get validate => (node) => node.delta != null; + bool validate(Node node) => + node.delta != null && + node.children.isEmpty && + node.attributes[CalloutBlockKeys.icon] is String; } // the main widget for rendering the callout block @@ -115,14 +103,11 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), required this.defaultColor, - required this.inlinePadding, }); final Color defaultColor; - final EdgeInsets Function(Node node) inlinePadding; @override State createState() => @@ -137,8 +122,7 @@ class _CalloutBlockComponentWidgetState BlockComponentConfigurable, BlockComponentTextDirectionMixin, BlockComponentAlignMixin, - BlockComponentBackgroundColorMixin, - NestedBlockComponentStatefulWidgetMixin { + BlockComponentBackgroundColorMixin { // the key used to forward focus to the richtext child @override final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); @@ -168,120 +152,59 @@ class _CalloutBlockComponentWidgetState } // get the emoji of the note block from the node's attributes or default to '📌' - EmojiIconData get emoji { + String get emoji { final icon = node.attributes[CalloutBlockKeys.icon]; - final type = - node.attributes[CalloutBlockKeys.iconType] ?? FlowyIconType.emoji; - EmojiIconData result = EmojiIconData.emoji('📌'); - try { - result = EmojiIconData(FlowyIconType.values.byName(type), icon); - } catch (_) {} - return result; - } - - @override - Widget build(BuildContext context) { - Widget child = node.children.isEmpty - ? buildComponent(context) - : buildComponentWithChildren(context); - - if (UniversalPlatform.isDesktop) { - child = Padding( - padding: EdgeInsets.symmetric(vertical: 2.0), - child: child, - ); + if (icon == null || icon.isEmpty) { + return '📌'; } - - return child; + return icon; } + // get access to the editor state via provider @override - Widget buildComponentWithChildren(BuildContext context) { - Widget child = Stack( - children: [ - Positioned.fill( - left: UniversalPlatform.isMobile ? 0 : cachedLeft, - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - color: backgroundColor, - ), - ), - ), - NestedListWidget( - indentPadding: indentPadding.copyWith(bottom: 8), - child: buildComponent(context, withBackgroundColor: false), - children: editorState.renderer.buildList( - context, - widget.node.children, - ), - ), - ], - ); - - if (UniversalPlatform.isMobile) { - child = Padding( - padding: padding, - child: child, - ); - } - - return child; - } + late final editorState = Provider.of(context, listen: false); // build the callout block widget @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = true, - }) { + Widget build(BuildContext context) { final textDirection = calculateTextDirection( layoutDirection: Directionality.maybeOf(context), ); - final (emojiSize, emojiButtonSize) = calculateEmojiSize(); - final documentId = context.read()?.documentId; + Widget child = Container( decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6.0)), - color: withBackgroundColor ? backgroundColor : null, + borderRadius: const BorderRadius.all(Radius.circular(8.0)), + color: backgroundColor, ), - padding: widget.inlinePadding(widget.node), width: double.infinity, alignment: alignment, child: Row( - mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, textDirection: textDirection, children: [ - const HSpace(6.0), // the emoji picker button for the note - EmojiPickerButton( - // force to refresh the popover state - key: ValueKey(widget.node.id + emoji.emoji), - enable: editorState.editable, - title: '', - margin: UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0) - : EdgeInsets.zero, - emoji: emoji, - emojiSize: emojiSize, - showBorder: false, - buttonSize: emojiButtonSize, - documentId: documentId, - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - onSubmitted: (r, controller) { - setEmojiIconData(r.data); - if (!r.keepOpen) controller?.close(); - }, + Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: 4.0, + right: 4.0, + ), + child: EmojiPickerButton( + key: ValueKey( + emoji.toString(), + ), // force to refresh the popover state + title: '', + emoji: emoji, + onSubmitted: (emoji, controller) { + setEmoji(emoji); + controller?.close(); + }, + ), ), - if (UniversalPlatform.isDesktopOrWeb) const HSpace(6.0), Flexible( child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: buildCalloutBlockComponent(context, textDirection), ), ), @@ -290,26 +213,17 @@ class _CalloutBlockComponentWidgetState ), ); - if (UniversalPlatform.isMobile && node.children.isEmpty) { - child = Padding( - key: blockComponentKey, - padding: padding, - child: child, - ); - } else { - child = Container( - key: blockComponentKey, - padding: EdgeInsets.zero, - child: child, - ); - } + child = Padding( + key: blockComponentKey, + padding: padding, + child: child, + ); child = BlockSelectionContainer( node: node, delegate: this, listenable: editorState.selectionNotifier, blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, supportTypes: const [ BlockSelectionType.block, ], @@ -320,7 +234,6 @@ class _CalloutBlockComponentWidgetState child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -333,46 +246,36 @@ class _CalloutBlockComponentWidgetState BuildContext context, TextDirection textDirection, ) { - return AppFlowyRichText( - key: forwardKey, - delegate: this, - node: widget.node, - editorState: editorState, - placeholderText: placeholderText, - textAlign: alignment?.toTextAlign ?? textAlign, - textSpanDecorator: (textSpan) => textSpan.updateTextStyle( - textStyleWithTextSpan(textSpan: textSpan), + return Padding( + padding: padding, + child: AppFlowyRichText( + key: forwardKey, + delegate: this, + node: widget.node, + editorState: editorState, + placeholderText: placeholderText, + textSpanDecorator: (textSpan) => textSpan.updateTextStyle( + textStyle, + ), + placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( + placeholderTextStyle, + ), + textDirection: textDirection, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, ), - placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( - placeholderTextStyleWithTextSpan(textSpan: textSpan), - ), - textDirection: textDirection, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, ); } // set the emoji of the note block - Future setEmojiIconData(EmojiIconData data) async { + Future setEmoji(String emoji) async { final transaction = editorState.transaction ..updateNode(node, { - CalloutBlockKeys.icon: data.emoji, - CalloutBlockKeys.iconType: data.type.name, + CalloutBlockKeys.icon: emoji, }) ..afterSelection = Selection.collapsed( Position(path: node.path, offset: node.delta?.length ?? 0), ); await editorState.apply(transaction); } - - (double, Size) calculateEmojiSize() { - const double defaultEmojiSize = 16.0; - const Size defaultEmojiButtonSize = Size(30.0, 30.0); - final double emojiSize = - editorState.editorStyle.textStyleConfiguration.text.fontSize ?? - defaultEmojiSize; - final emojiButtonSize = - defaultEmojiButtonSize * emojiSize / defaultEmojiSize; - return (emojiSize, emojiButtonSize); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart index 842f3f59fd..3c50661071 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart @@ -32,22 +32,9 @@ CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { await editorState.deleteSelection(selection); if (HardwareKeyboard.instance.isShiftPressed) { - // ignore the shift+enter event, fallback to the default behavior - return false; - } else if (node.children.isEmpty) { - // insert a new paragraph within the callout block - final path = node.path.child(0); - final transaction = editorState.transaction; - transaction.insertNode( - path, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - await editorState.apply(transaction); + await editorState.insertNewLine(); + } else { + await editorState.insertTextAtCurrentSelection('\n'); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart index 645de3b2f8..9c6ff01bf9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart @@ -1,17 +1,16 @@ -import 'dart:convert'; +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/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; CodeBlockCopyBuilder codeBlockCopyBuilder = (_, node) => _CopyButton(node: node); @@ -29,25 +28,16 @@ class _CopyButton extends StatelessWidget { message: LocaleKeys.document_codeBlock_copyTooltip.tr(), child: FlowyIconButton( onPressed: () async { - final delta = node.delta; - if (delta == null) { - return; - } - - final document = Document.blank() - ..insert([0], [node.deepCopy()]) - ..toJson(); - await getIt().setData( ClipboardServiceData( - plainText: delta.toPlainText(), - inAppJson: jsonEncode(document.toJson()), + plainText: node.delta?.toPlainText(), ), ); if (context.mounted) { - showToastNotification( - message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), + showSnackBarMessage( + context, + LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(), ); } }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart index c4c2e3e0ba..97ac54e9f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart @@ -3,16 +3,13 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selec import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( editorState, @@ -22,7 +19,7 @@ CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( onMenuClose, onMenuOpen, }) => - CodeBlockLanguageSelector( + _CodeBlockLanguageSelector( editorState: editorState, language: selectedLanguage, supportedLanguages: supportedLanguages, @@ -31,9 +28,8 @@ CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( onMenuOpen: onMenuOpen, ); -class CodeBlockLanguageSelector extends StatefulWidget { - const CodeBlockLanguageSelector({ - super.key, +class _CodeBlockLanguageSelector extends StatefulWidget { + const _CodeBlockLanguageSelector({ required this.editorState, required this.supportedLanguages, this.language, @@ -50,11 +46,12 @@ class CodeBlockLanguageSelector extends StatefulWidget { final VoidCallback? onMenuClose; @override - State createState() => + State<_CodeBlockLanguageSelector> createState() => _CodeBlockLanguageSelectorState(); } -class _CodeBlockLanguageSelectorState extends State { +class _CodeBlockLanguageSelectorState + extends State<_CodeBlockLanguageSelector> { final controller = PopoverController(); @override @@ -78,7 +75,7 @@ class _CodeBlockLanguageSelectorState extends State { fillColor: Colors.transparent, hoverColor: Theme.of(context).colorScheme.secondaryContainer, onPressed: () async { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { final language = await context .push(MobileCodeLanguagePickerScreen.routeName); if (language != null) { @@ -91,7 +88,7 @@ class _CodeBlockLanguageSelectorState extends State { ], ); - if (UniversalPlatform.isDesktopOrWeb) { + if (PlatformExtension.isDesktopOrWeb) { child = AppFlowyPopover( controller: controller, direction: PopoverDirection.bottomWithLeftAligned, @@ -139,8 +136,7 @@ class _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> { late List filteredLanguages = widget.supportedLanguages.map((e) => e.capitalize()).toList(); late int selectedIndex = - widget.supportedLanguages.indexOf(widget.language?.toLowerCase() ?? ''); - final ItemScrollController languageListController = ItemScrollController(); + widget.supportedLanguages.indexOf(widget.language ?? ''); @override void initState() { @@ -164,91 +160,34 @@ class _LanguageSelectionPopoverState extends State<_LanguageSelectionPopover> { @override Widget build(BuildContext context) { - return Shortcuts( - shortcuts: const { - SingleActivator(LogicalKeyboardKey.arrowUp): - _DirectionIntent(AxisDirection.up), - SingleActivator(LogicalKeyboardKey.arrowDown): - _DirectionIntent(AxisDirection.down), - SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(), - }, - child: Actions( - actions: { - _DirectionIntent: CallbackAction<_DirectionIntent>( - onInvoke: (intent) => onArrowKey(intent.direction), - ), - ActivateIntent: CallbackAction( - onInvoke: (intent) { - if (selectedIndex < 0) return; - selectLanguage(selectedIndex); - return null; - }, - ), - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyTextField( - focusNode: focusNode, - autoFocus: false, - controller: searchController, - hintText: LocaleKeys.document_codeBlock_searchLanguageHint.tr(), - onChanged: (_) => setState(() { - filteredLanguages = widget.supportedLanguages - .where( - (e) => e.contains(searchController.text.toLowerCase()), - ) - .map((e) => e.capitalize()) - .toList(); - selectedIndex = - widget.supportedLanguages.indexOf(widget.language ?? ''); - }), - ), - const VSpace(8), - Flexible( - child: SelectableItemListMenu( - controller: languageListController, - shrinkWrap: true, - items: filteredLanguages, - selectedIndex: selectedIndex, - onSelected: selectLanguage, - ), - ), - ], + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTextField( + focusNode: focusNode, + autoFocus: false, + controller: searchController, + hintText: LocaleKeys.document_codeBlock_searchLanguageHint.tr(), + onChanged: (_) => setState(() { + filteredLanguages = widget.supportedLanguages + .where((e) => e.contains(searchController.text.toLowerCase())) + .map((e) => e.capitalize()) + .toList(); + selectedIndex = + widget.supportedLanguages.indexOf(widget.language ?? ''); + }), ), - ), + const VSpace(8), + Flexible( + child: SelectableItemListMenu( + shrinkWrap: true, + items: filteredLanguages, + selectedIndex: selectedIndex, + onSelected: (index) => + widget.onLanguageSelected(filteredLanguages[index]), + ), + ), + ], ); } - - void onArrowKey(AxisDirection direction) { - if (filteredLanguages.isEmpty) return; - final isUp = direction == AxisDirection.up; - if (selectedIndex < 0) { - selectedIndex = isUp ? 0 : -1; - } - final length = filteredLanguages.length; - setState(() { - if (isUp) { - selectedIndex = selectedIndex == 0 ? length - 1 : selectedIndex - 1; - } else { - selectedIndex = selectedIndex == length - 1 ? 0 : selectedIndex + 1; - } - }); - languageListController.scrollTo( - index: selectedIndex, - alignment: 0.5, - duration: const Duration(milliseconds: 300), - ); - } - - void selectLanguage(int index) { - widget.onLanguageSelected(filteredLanguages[index]); - } -} - -/// [ScrollIntent] is not working, so using this custom Intent -class _DirectionIntent extends Intent { - const _DirectionIntent(this.direction); - - final AxisDirection direction; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_menu_item.dart deleted file mode 100644 index c4e395f22f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_menu_item.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; - -final codeBlockSelectionMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_selectionMenu_codeBlock.tr(), - iconBuilder: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.icon_code_block_s, - isSelected: onSelected, - style: style, - ), - keywords: ['code', 'codeblock'], - nodeBuilder: (_, __) => codeBlockNode(), - replace: (_, node) => node.delta?.isEmpty ?? false, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart deleted file mode 100644 index c426ad640f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_component.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -Node simpleColumnNode({ - List? children, - double? ratio, -}) { - return Node( - type: SimpleColumnBlockKeys.type, - children: children ?? [paragraphNode()], - attributes: { - SimpleColumnBlockKeys.ratio: ratio, - }, - ); -} - -extension SimpleColumnBlockAttributes on Node { - // get the next column node of the current column node - // if the current column node is the last column node, return null - Node? get nextColumn { - final index = path.last; - final parent = this.parent; - if (parent == null || index == parent.children.length - 1) { - return null; - } - return parent.children[index + 1]; - } - - // get the previous column node of the current column node - // if the current column node is the first column node, return null - Node? get previousColumn { - final index = path.last; - final parent = this.parent; - if (parent == null || index == 0) { - return null; - } - return parent.children[index - 1]; - } -} - -class SimpleColumnBlockKeys { - const SimpleColumnBlockKeys._(); - - static const String type = 'simple_column'; - - /// @Deprecated Use [SimpleColumnBlockKeys.ratio] instead. - /// - /// This field is no longer used since v0.6.9 - @Deprecated('Use [SimpleColumnBlockKeys.ratio] instead.') - static const String width = 'width'; - - /// The ratio of the column width. - /// - /// The value is a double number between 0 and 1. - static const String ratio = 'ratio'; -} - -class SimpleColumnBlockComponentBuilder extends BlockComponentBuilder { - SimpleColumnBlockComponentBuilder({ - super.configuration, - }); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return SimpleColumnBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isNotEmpty; -} - -class SimpleColumnBlockComponent extends BlockComponentStatefulWidget { - const SimpleColumnBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => - SimpleColumnBlockComponentState(); -} - -class SimpleColumnBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - final columnKey = GlobalKey(); - - late final EditorState editorState = context.read(); - - @override - Widget build(BuildContext context) { - Widget child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: node.children.map( - (e) { - Widget child = Provider( - create: (_) => DatabasePluginWidgetBuilderSize( - verticalPadding: 0, - horizontalPadding: 0, - ), - child: editorState.renderer.build(context, e), - ); - if (SimpleColumnsBlockConstants.enableDebugBorder) { - child = DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: Colors.blue, - ), - ), - child: child, - ); - } - return child; - }, - ).toList(), - ); - - child = Padding( - key: columnKey, - padding: padding, - child: child, - ); - - if (SimpleColumnsBlockConstants.enableDebugBorder) { - child = Container( - color: Colors.green.withValues( - alpha: 0.3, - ), - child: child, - ); - } - - // the column block does not support the block actions and selection - // because the column block is a layout wrapper, it does not have a content - return child; - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - return getRectsInSelection(Selection.invalid()).first; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection( - Selection.collapsed(position), - shiftWithBaseOffset: shiftWithBaseOffset, - ); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final renderBox = columnKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && renderBox is RenderBox) { - return [ - renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & - renderBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => - Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1); - - @override - Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => - _renderBox!.localToGlobal(offset); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart deleted file mode 100644 index 69bec33c61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_block_width_resizer.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/draggable_option_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -class SimpleColumnBlockWidthResizer extends StatefulWidget { - const SimpleColumnBlockWidthResizer({ - super.key, - required this.columnNode, - required this.editorState, - this.height, - }); - - final Node columnNode; - final EditorState editorState; - final double? height; - - @override - State createState() => - _SimpleColumnBlockWidthResizerState(); -} - -class _SimpleColumnBlockWidthResizerState - extends State { - bool isDragging = false; - - ValueNotifier isHovering = ValueNotifier(false); - - @override - void dispose() { - isHovering.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => isHovering.value = true, - onExit: (_) { - // delay the hover state change to avoid flickering - Future.delayed(const Duration(milliseconds: 100), () { - if (!isDragging) { - isHovering.value = false; - } - }); - }, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragStart: _onHorizontalDragStart, - onHorizontalDragUpdate: _onHorizontalDragUpdate, - onHorizontalDragEnd: _onHorizontalDragEnd, - onHorizontalDragCancel: _onHorizontalDragCancel, - child: ValueListenableBuilder( - valueListenable: isHovering, - builder: (context, isHovering, child) { - final hide = isDraggingAppFlowyEditorBlock.value || !isHovering; - return MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - child: Container( - width: 2, - height: widget.height ?? 20, - margin: EdgeInsets.symmetric(horizontal: 2), - color: !hide - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - ); - }, - ), - ), - ); - } - - void _onHorizontalDragStart(DragStartDetails details) { - isDragging = true; - EditorGlobalConfiguration.enableDragMenu.value = false; - } - - void _onHorizontalDragUpdate(DragUpdateDetails details) { - if (!isDragging) { - return; - } - - // update the column width in memory - final columnNode = widget.columnNode; - final columnsNode = columnNode.columnsParent; - if (columnsNode == null) { - return; - } - final editorWidth = columnsNode.rect.width; - final rect = columnNode.rect; - final width = rect.width; - final originalRatio = columnNode.attributes[SimpleColumnBlockKeys.ratio]; - final newWidth = width + details.delta.dx; - - final transaction = widget.editorState.transaction; - final newRatio = newWidth / editorWidth; - transaction.updateNode(columnNode, { - ...columnNode.attributes, - SimpleColumnBlockKeys.ratio: newRatio, - }); - - if (newRatio < 0.1 && newRatio < originalRatio) { - return; - } - - final nextColumn = columnNode.nextColumn; - if (nextColumn != null) { - final nextColumnRect = nextColumn.rect; - final nextColumnWidth = nextColumnRect.width; - final newNextColumnWidth = nextColumnWidth - details.delta.dx; - final newNextColumnRatio = newNextColumnWidth / editorWidth; - if (newNextColumnRatio < 0.1) { - return; - } - transaction.updateNode(nextColumn, { - ...nextColumn.attributes, - SimpleColumnBlockKeys.ratio: newNextColumnRatio, - }); - } - - transaction.updateNode(columnsNode, { - ...columnsNode.attributes, - ColumnsBlockKeys.columnCount: columnsNode.children.length, - }); - - widget.editorState.apply( - transaction, - options: ApplyOptions(inMemoryUpdate: true), - ); - } - - void _onHorizontalDragEnd(DragEndDetails details) { - isHovering.value = false; - EditorGlobalConfiguration.enableDragMenu.value = true; - - if (!isDragging) { - return; - } - - // apply the transaction again to make sure the width is updated - final transaction = widget.editorState.transaction; - final columnsNode = widget.columnNode.columnsParent; - if (columnsNode == null) { - return; - } - for (final columnNode in columnsNode.children) { - transaction.updateNode(columnNode, { - ...columnNode.attributes, - }); - } - widget.editorState.apply(transaction); - - isDragging = false; - } - - void _onHorizontalDragCancel() { - isDragging = false; - isHovering.value = false; - EditorGlobalConfiguration.enableDragMenu.value = true; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart deleted file mode 100644 index 05389fb760..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_column_node_extension.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension SimpleColumnNodeExtension on Node { - /// Returns the parent [Node] of the current node if it is a [SimpleColumnsBlock]. - Node? get columnsParent { - Node? currentNode = parent; - while (currentNode != null) { - if (currentNode.type == SimpleColumnsBlockKeys.type) { - return currentNode; - } - currentNode = currentNode.parent; - } - return null; - } - - /// Returns the parent [Node] of the current node if it is a [SimpleColumnBlock]. - Node? get columnParent { - Node? currentNode = parent; - while (currentNode != null) { - if (currentNode.type == SimpleColumnBlockKeys.type) { - return currentNode; - } - currentNode = currentNode.parent; - } - return null; - } - - /// Returns whether the current node is in a [SimpleColumnsBlock]. - bool get isInColumnsBlock => columnsParent != null; - - /// Returns whether the current node is in a [SimpleColumnBlock]. - bool get isInColumnBlock => columnParent != null; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart deleted file mode 100644 index 58ecde5f2f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_component.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -// if the children is not provided, it will create two columns by default. -// if the columnCount is provided, it will create the specified number of columns. -Node simpleColumnsNode({ - List? children, - int? columnCount, - double? ratio, -}) { - columnCount ??= 2; - children ??= List.generate( - columnCount, - (index) => simpleColumnNode( - ratio: ratio, - children: [paragraphNode()], - ), - ); - - // check the type of children - for (final child in children) { - if (child.type != SimpleColumnBlockKeys.type) { - Log.error('the type of children must be column, but got ${child.type}'); - } - } - - return Node( - type: SimpleColumnsBlockKeys.type, - children: children, - ); -} - -class SimpleColumnsBlockKeys { - const SimpleColumnsBlockKeys._(); - - static const String type = 'simple_columns'; -} - -class SimpleColumnsBlockComponentBuilder extends BlockComponentBuilder { - SimpleColumnsBlockComponentBuilder({super.configuration}); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return ColumnsBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isNotEmpty; -} - -class ColumnsBlockComponent extends BlockComponentStatefulWidget { - const ColumnsBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => ColumnsBlockComponentState(); -} - -class ColumnsBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - final columnsKey = GlobalKey(); - - late final EditorState editorState = context.read(); - - final ScrollController scrollController = ScrollController(); - - final ValueNotifier heightValueNotifier = ValueNotifier(null); - - @override - void initState() { - super.initState(); - _updateColumnsBlock(); - } - - @override - void dispose() { - scrollController.dispose(); - heightValueNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child = Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildChildren(), - ); - - child = Align( - alignment: Alignment.topLeft, - child: child, - ); - - child = Padding( - key: columnsKey, - padding: padding, - child: child, - ); - - if (SimpleColumnsBlockConstants.enableDebugBorder) { - child = DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: Colors.red, - width: 3.0, - ), - ), - child: child, - ); - } - - // the columns block does not support the block actions and selection - // because the columns block is a layout wrapper, it does not have a content - return NotificationListener( - onNotification: (v) => updateHeightValueNotifier(v), - child: SizeChangedLayoutNotifier(child: child), - ); - } - - List _buildChildren() { - final length = node.children.length; - final children = []; - for (var i = 0; i < length; i++) { - final childNode = node.children[i]; - final double ratio = - childNode.attributes[SimpleColumnBlockKeys.ratio]?.toDouble() ?? - 1.0 / length; - - Widget child = editorState.renderer.build(context, childNode); - - child = Expanded( - flex: (max(ratio, 0.1) * 10000).toInt(), - child: child, - ); - - children.add(child); - - if (i != length - 1) { - children.add( - ValueListenableBuilder( - valueListenable: heightValueNotifier, - builder: (context, height, child) { - return SimpleColumnBlockWidthResizer( - columnNode: childNode, - editorState: editorState, - height: height, - ); - }, - ), - ); - } - } - return children; - } - - // Update the existing columns block data - // if the column ratio is not existing, it will be set to 1.0 / columnCount - void _updateColumnsBlock() { - final transaction = editorState.transaction; - final length = node.children.length; - for (int i = 0; i < length; i++) { - final childNode = node.children[i]; - final ratio = childNode.attributes[SimpleColumnBlockKeys.ratio]; - if (ratio == null) { - transaction.updateNode(childNode, { - ...childNode.attributes, - SimpleColumnBlockKeys.ratio: 1.0 / length, - }); - } - } - if (transaction.operations.isNotEmpty) { - editorState.apply(transaction); - } - } - - bool updateHeightValueNotifier(SizeChangedLayoutNotification notification) { - if (!mounted) return true; - final height = _renderBox?.size.height; - if (heightValueNotifier.value == height) return true; - WidgetsBinding.instance.addPostFrameCallback((_) { - heightValueNotifier.value = height; - }); - return true; - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - return getRectsInSelection(Selection.invalid()).first; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection( - Selection.collapsed(position), - shiftWithBaseOffset: shiftWithBaseOffset, - ); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final renderBox = columnsKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && renderBox is RenderBox) { - return [ - renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & - renderBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => - Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1); - - @override - Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => - _renderBox!.localToGlobal(offset); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart deleted file mode 100644 index d8820f8613..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/columns/simple_columns_block_constant.dart +++ /dev/null @@ -1,6 +0,0 @@ -class SimpleColumnsBlockConstants { - const SimpleColumnsBlockConstants._(); - - static const double minimumColumnWidth = 128.0; - static const bool enableDebugBorder = false; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart index cc496ff9d4..6dc8724d26 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/context_menu/custom_context_menu.dart @@ -13,11 +13,6 @@ final List> customContextMenuItems = [ getName: LocaleKeys.document_plugins_contextMenu_paste.tr, onPressed: (editorState) => customPasteCommand.execute(editorState), ), - ContextMenuItem( - getName: LocaleKeys.document_plugins_contextMenu_pasteAsPlainText.tr, - onPressed: (editorState) => - customPastePlainTextCommand.execute(editorState), - ), ContextMenuItem( getName: LocaleKeys.document_plugins_contextMenu_cut.tr, onPressed: (editorState) => customCutCommand.execute(editorState), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart index f108c7e26b..e188e6b29f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart @@ -8,17 +8,21 @@ import 'package:super_clipboard/super_clipboard.dart'; /// Used for in-app copy and paste without losing the format. /// /// It's a Json string representing the copied editor nodes. -const inAppJsonFormat = CustomValueFormat( +final inAppJsonFormat = CustomValueFormat( applicationId: 'io.appflowy.InAppJsonType', - onDecode: _defaultDecode, - onEncode: _defaultEncode, -); - -/// Used for table nodes when coping a row or a column. -const tableJsonFormat = CustomValueFormat( - applicationId: 'io.appflowy.TableJsonType', - onDecode: _defaultDecode, - onEncode: _defaultEncode, + onDecode: (value, platformType) async { + if (value is PlatformDataProvider) { + final data = await value.getData(platformType); + if (data is List) { + return utf8.decode(data, allowMalformed: true); + } + if (data is String) { + return Uri.decodeFull(data); + } + } + return null; + }, + onEncode: (value, platformType) => utf8.encode(value), ); class ClipboardServiceData { @@ -27,53 +31,20 @@ class ClipboardServiceData { this.html, this.image, this.inAppJson, - this.tableJson, }); - /// The [plainText] is the plain text string. - /// - /// It should be used for pasting the plain text from the clipboard. final String? plainText; - - /// The [html] is the html string. - /// - /// It should be used for pasting the html from the clipboard. - /// For example, copy the content in the browser, and paste it in the editor. final String? html; - - /// The [image] is the image data. - /// - /// It should be used for pasting the image from the clipboard. - /// For example, copy the image in the browser or other apps, and paste it in the editor. final (String, Uint8List?)? image; - - /// The [inAppJson] is the json string of the editor nodes. - /// - /// It should be used for pasting the content in-app. - /// For example, pasting the content from document A to document B. final String? inAppJson; - - /// The [tableJson] is the json string of the table nodes. - /// - /// It only works for the table nodes when coping a row or a column. - /// Don't use it for another scenario. - final String? tableJson; } class ClipboardService { - static ClipboardServiceData? _mockData; - - @visibleForTesting - static void mockSetData(ClipboardServiceData? data) { - _mockData = data; - } - Future setData(ClipboardServiceData data) async { final plainText = data.plainText; final html = data.html; final inAppJson = data.inAppJson; final image = data.image; - final tableJson = data.tableJson; final item = DataWriterItem(); if (plainText != null) { @@ -85,9 +56,6 @@ class ClipboardService { if (inAppJson != null) { item.add(inAppJsonFormat(inAppJson)); } - if (tableJson != null) { - item.add(tableJsonFormat(tableJson)); - } if (image != null && image.$2?.isNotEmpty == true) { switch (image.$1) { case 'png': @@ -113,10 +81,6 @@ class ClipboardService { } Future getData() async { - if (_mockData != null) { - return _mockData!; - } - final reader = await SystemClipboard.instance?.read(); if (reader == null) { @@ -125,14 +89,14 @@ class ClipboardService { for (final item in reader.items) { final availableFormats = await item.rawReader!.getAvailableFormats(); - Log.info('availableFormats: $availableFormats'); + Log.debug( + 'availableFormats: $availableFormats', + ); } final plainText = await reader.readValue(Formats.plainText); final html = await reader.readValue(Formats.htmlText); final inAppJson = await reader.readValue(inAppJsonFormat); - final tableJson = await reader.readValue(tableJsonFormat); - final uri = await reader.readValue(Formats.uri); (String, Uint8List?)? image; if (reader.canProvide(Formats.png)) { image = ('png', await reader.readFile(Formats.png)); @@ -140,16 +104,13 @@ class ClipboardService { image = ('jpeg', await reader.readFile(Formats.jpeg)); } else if (reader.canProvide(Formats.gif)) { image = ('gif', await reader.readFile(Formats.gif)); - } else if (reader.canProvide(Formats.webp)) { - image = ('webp', await reader.readFile(Formats.webp)); } return ClipboardServiceData( - plainText: plainText ?? uri?.uri.toString(), + plainText: plainText, html: html, image: image, inAppJson: inAppJson, - tableJson: tableJson, ); } } @@ -177,22 +138,3 @@ extension on DataReader { return c.future; } } - -/// The default decode function for the clipboard service. -Future _defaultDecode(Object value, String platformType) async { - if (value is PlatformDataProvider) { - final data = await value.getData(platformType); - if (data is List) { - return utf8.decode(data, allowMalformed: true); - } - if (data is String) { - return Uri.decodeFull(data); - } - } - return null; -} - -/// The default encode function for the clipboard service. -Future _defaultEncode(String value, String platformType) async { - return utf8.encode(value); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart index e56ccfc941..37f019d750 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -21,54 +20,23 @@ final CommandShortcutEvent customCopyCommand = CommandShortcutEvent( handler: _copyCommandHandler, ); -CommandShortcutEventHandler _copyCommandHandler = - (editorState) => handleCopyCommand(editorState); - -KeyEventResult handleCopyCommand( - EditorState editorState, { - bool isCut = false, -}) { +CommandShortcutEventHandler _copyCommandHandler = (editorState) { final selection = editorState.selection?.normalized; - if (selection == null) { + if (selection == null || selection.isCollapsed) { return KeyEventResult.ignored; } - String? text; - String? html; - String? inAppJson; + // plain text. + final text = editorState.getTextInSelection(selection).join('\n'); - if (selection.isCollapsed) { - // if the selection is collapsed, we will copy the text of the current line. - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return KeyEventResult.ignored; - } + final nodes = editorState.getSelectedNodes(selection: selection); + final document = Document.blank()..insert([0], nodes); - // plain text. - text = node.delta?.toPlainText(); + // in app json + final inAppJson = jsonEncode(document.toJson()); - // in app json - final document = Document.blank() - ..insert([0], [_handleNode(node.deepCopy(), isCut)]); - inAppJson = jsonEncode(document.toJson()); - - // html - html = documentToHTML(document); - } else { - // plain text. - text = editorState.getTextInSelection(selection).join('\n'); - - final document = _buildCopiedDocument( - editorState, - selection, - isCut: isCut, - ); - - inAppJson = jsonEncode(document.toJson()); - - // html - html = documentToHTML(document); - } + // html + final html = documentToHTML(document); () async { await getIt().setData( @@ -81,68 +49,4 @@ KeyEventResult handleCopyCommand( }(); return KeyEventResult.handled; -} - -Document _buildCopiedDocument( - EditorState editorState, - Selection selection, { - bool isCut = false, -}) { - // filter the table nodes - final filteredNodes = []; - final selectedNodes = editorState.getSelectedNodes(selection: selection); - final nodes = _handleSubPageNodes(selectedNodes, isCut); - for (final node in nodes) { - if (node.type == SimpleTableCellBlockKeys.type) { - // if the node is a table cell, we will fetch its children instead. - filteredNodes.addAll(node.children); - } else if (node.type == SimpleTableRowBlockKeys.type) { - // if the node is a table row, we will fetch its children's children instead. - filteredNodes.addAll(node.children.expand((e) => e.children)); - } else if (node.type == SimpleColumnBlockKeys.type) { - // if the node is a column block, we will fetch its children instead. - filteredNodes.addAll(node.children); - } else if (node.type == SimpleColumnsBlockKeys.type) { - // if the node is a columns block, we will fetch its children's children instead. - filteredNodes.addAll(node.children.expand((e) => e.children)); - } else { - filteredNodes.add(node); - } - } - final document = Document.blank() - ..insert( - [0], - filteredNodes.map((e) => e.deepCopy()), - ); - return document; -} - -List _handleSubPageNodes(List nodes, [bool isCut = false]) { - final handled = []; - for (final node in nodes) { - handled.add(_handleNode(node, isCut)); - } - - return handled; -} - -Node _handleNode(Node node, [bool isCut = false]) { - if (!isCut) { - return node.deepCopy(); - } - - final newChildren = node.children.map(_handleNode).toList(); - - if (node.type == SubPageBlockKeys.type) { - return node.copyWith( - attributes: { - ...node.attributes, - SubPageBlockKeys.wasCopied: !isCut, - SubPageBlockKeys.wasCut: isCut, - }, - children: newChildren, - ); - } - - return node.copyWith(children: newChildren); -} +}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart index 9fea34edbf..e7f6fba181 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart @@ -1,8 +1,7 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; /// cut. /// @@ -20,42 +19,7 @@ final CommandShortcutEvent customCutCommand = CommandShortcutEvent( ); CommandShortcutEventHandler _cutCommandHandler = (editorState) { - final selection = editorState.selection; - if (selection == null) { - return KeyEventResult.ignored; - } - - final context = editorState.document.root.context; - if (context == null || !context.mounted) { - return KeyEventResult.ignored; - } - - context.read().didCut(); - - handleCopyCommand(editorState, isCut: true); - - if (!selection.isCollapsed) { - editorState.deleteSelectionIfNeeded(); - } else { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return KeyEventResult.handled; - } - // prevent to cut the node that is selecting the table. - if (node.parentTableNode != null) { - return KeyEventResult.skipRemainingHandlers; - } - - final transaction = editorState.transaction; - transaction.deleteNode(node); - final nextNode = node.next; - if (nextNode != null && nextNode.delta != null) { - transaction.afterSelection = Selection.collapsed( - Position(path: node.path, offset: nextNode.delta?.length ?? 0), - ); - } - editorState.apply(transaction); - } - + customCopyCommand.execute(editorState); + editorState.deleteSelectionIfNeeded(); return KeyEventResult.handled; }; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart index 6399d3b11f..842f00dbe9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart @@ -1,23 +1,17 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/default_extensions.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:http/http.dart' as http; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; +/// Paste. +/// /// - support /// - desktop /// - web @@ -31,147 +25,75 @@ final CommandShortcutEvent customPasteCommand = CommandShortcutEvent( handler: _pasteCommandHandler, ); -final CommandShortcutEvent customPastePlainTextCommand = CommandShortcutEvent( - key: 'paste the plain content', - getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent, - command: 'ctrl+shift+v', - macOSCommand: 'cmd+shift+v', - handler: _pastePlainCommandHandler, -); - CommandShortcutEventHandler _pasteCommandHandler = (editorState) { final selection = editorState.selection; if (selection == null) { return KeyEventResult.ignored; } - doPaste(editorState).then((_) { - final context = editorState.document.root.context; - if (context != null && context.mounted) { - context.read().didPaste(); - } - }); + // because the event handler is not async, so we need to use wrap the async function here + () async { + // dispatch the paste event + final data = await getIt().getData(); + final inAppJson = data.inAppJson; + final html = data.html; + final plainText = data.plainText; + final image = data.image; - return KeyEventResult.handled; -}; - -CommandShortcutEventHandler _pastePlainCommandHandler = (editorState) { - final selection = editorState.selection; - if (selection == null) { - return KeyEventResult.ignored; - } - - doPlainPaste(editorState).then((_) { - final context = editorState.document.root.context; - if (context != null && context.mounted) { - context.read().didPaste(); - } - }); - - return KeyEventResult.handled; -}; - -Future doPaste(EditorState editorState) async { - final selection = editorState.selection; - if (selection == null) { - return; - } - - EditorNotification.paste().post(); - - // dispatch the paste event - final data = await getIt().getData(); - final inAppJson = data.inAppJson; - final html = data.html; - final plainText = data.plainText; - final image = data.image; - - // dump the length of the data here, don't log the data itself for privacy concerns - Log.info('paste command: inAppJson: ${inAppJson?.length}'); - Log.info('paste command: html: ${html?.length}'); - Log.info('paste command: plainText: ${plainText?.length}'); - Log.info('paste command: image: ${image?.$2?.length}'); - - if (await editorState.pasteAppFlowySharePageLink(plainText)) { - return Log.info('Pasted block link'); - } - - // paste as link preview - if (await _pasteAsLinkPreview(editorState, plainText)) { - return Log.info('Pasted as link preview'); - } - - // Order: - // 1. in app json format - // 2. html - // 3. image - // 4. plain text - - // try to paste the content in order, if any of them is failed, then try the next one - if (inAppJson != null && inAppJson.isNotEmpty) { - if (await editorState.pasteInAppJson(inAppJson)) { - return Log.info('Pasted in app json'); - } - } - - // if the image data is not null, we should handle it first - // because the image URL in the HTML may not be reachable due to permission issues - // For example, when pasting an image from Slack, the image URL provided is not public. - if (image != null && image.$2?.isNotEmpty == true) { - final documentBloc = - editorState.document.root.context?.read(); - final documentId = documentBloc?.documentId; - if (documentId == null || documentId.isEmpty) { + // paste as link preview + final result = await _pasteAsLinkPreview(editorState, plainText); + if (result) { return; } - await editorState.deleteSelectionIfNeeded(); - final result = await editorState.pasteImage( - image.$1, - image.$2!, - documentId, - selection: selection, - ); - if (result) { - return Log.info('Pasted image'); - } - } + // Order: + // 1. in app json format + // 2. html + // 3. image + // 4. plain text - if (html != null && html.isNotEmpty) { - await editorState.deleteSelectionIfNeeded(); - if (await editorState.pasteHtml(html)) { - return Log.info('Pasted html'); + // try to paste the content in order, if any of them is failed, then try the next one + if (inAppJson != null && inAppJson.isNotEmpty) { + await editorState.deleteSelectionIfNeeded(); + final result = await editorState.pasteInAppJson(inAppJson); + if (result) { + return; + } } - } - if (plainText != null && plainText.isNotEmpty) { - final currentSelection = editorState.selection; - if (currentSelection == null) { - await editorState.updateSelectionWithReason( - selection, - reason: SelectionUpdateReason.uiEvent, - ); + if (html != null && html.isNotEmpty) { + await editorState.deleteSelectionIfNeeded(); + final result = await editorState.pasteHtml(html); + if (result) { + return; + } } - await editorState.pasteText(plainText); - return Log.info('Pasted plain text'); - } - return Log.info('unable to parse the clipboard content'); -} + if (image != null && image.$2?.isNotEmpty == true) { + await editorState.deleteSelectionIfNeeded(); + final result = await editorState.pasteImage(image.$1, image.$2!); + if (result) { + return; + } + } + + if (plainText != null && plainText.isNotEmpty) { + await editorState.pastePlainText(plainText); + } + }(); + + return KeyEventResult.handled; +}; Future _pasteAsLinkPreview( EditorState editorState, String? text, ) async { - final isMobile = UniversalPlatform.isMobile; - // the url should contain a protocol - if (text == null || !isURL(text, {'require_protocol': true})) { + if (text == null || !isURL(text)) { return false; } final selection = editorState.selection; - // Apply the update only when the selection is collapsed - // and at the start of the current line if (selection == null || !selection.isCollapsed || selection.startIndex != 0) { @@ -179,91 +101,18 @@ Future _pasteAsLinkPreview( } final node = editorState.getNodeAtPath(selection.start.path); - // Apply the update only when the current node is a paragraph - // and the paragraph is empty if (node == null || node.type != ParagraphBlockKeys.type || node.delta?.toPlainText().isNotEmpty == true) { return false; } - if (!isMobile) return false; - final bool isImageUrl; - try { - isImageUrl = await _isImageUrl(text); - } catch (e) { - Log.info('unable to get content header'); - return false; - } - if (!isImageUrl) return false; - - // insert the text with link format - final textTransaction = editorState.transaction - ..insertText( - node, - 0, - text, - attributes: {AppFlowyRichTextKeys.href: text}, - ); - await editorState.apply( - textTransaction, - skipHistoryDebounce: true, + final transaction = editorState.transaction; + transaction.insertNode( + selection.start.path, + linkPreviewNode(url: text), ); - - // convert it to image or link preview node - final replacementInsertedNodes = [ - isImageUrl ? imageNode(url: text) : linkPreviewNode(url: text), - // if the next node is null, insert a empty paragraph node - if (node.next == null) paragraphNode(), - ]; - - final replacementTransaction = editorState.transaction - ..insertNodes( - selection.start.path, - replacementInsertedNodes, - ) - ..deleteNode(node) - ..afterSelection = Selection.collapsed( - Position(path: node.path.next), - ); - - await editorState.apply(replacementTransaction); + await editorState.apply(transaction); return true; } - -Future doPlainPaste(EditorState editorState) async { - final selection = editorState.selection; - if (selection == null) { - return; - } - - EditorNotification.paste().post(); - - // dispatch the paste event - final data = await getIt().getData(); - final plainText = data.plainText; - if (plainText != null && plainText.isNotEmpty) { - await editorState.pastePlainText(plainText); - Log.info('Pasted plain text'); - return; - } - - Log.info('unable to parse the clipboard content'); - return; -} - -Future _isImageUrl(String text) async { - if (isNotImageUrl(text)) return false; - final response = await http.head(Uri.parse(text)); - - if (response.statusCode == 200) { - final contentType = response.headers['content-type']; - if (contentType != null) { - return contentType.startsWith('image/') && - defaultImageExtensions.any(contentType.contains); - } - } - - throw 'bad status code'; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart new file mode 100644 index 0000000000..34c6c8fe06 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart @@ -0,0 +1,169 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +extension PasteNodes on EditorState { + Future pasteSingleLineNode(Node insertedNode) async { + final selection = await deleteSelectionIfNeeded(); + if (selection == null) { + return; + } + final node = getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final transaction = this.transaction; + final insertedDelta = insertedNode.delta; + // if the node is empty and its type is paragprah, replace it with the inserted node. + if (delta.isEmpty && node.type == ParagraphBlockKeys.type) { + transaction.insertNode( + selection.end.path.next, + insertedNode, + ); + transaction.deleteNode(node); + final path = calculatePath(selection.end.path, [insertedNode]); + final offset = calculateLength([insertedNode]); + transaction.afterSelection = Selection.collapsed( + Position( + path: path, + offset: offset, + ), + ); + } else if (insertedDelta != null) { + // if the node is not empty, insert the delta from inserted node after the selection. + transaction.insertTextDelta(node, selection.endIndex, insertedDelta); + } + await apply(transaction); + } + + Future pasteMultiLineNodes(List nodes) async { + assert(nodes.length > 1); + + final selection = await deleteSelectionIfNeeded(); + if (selection == null) { + return; + } + final node = getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + final transaction = this.transaction; + + final lastNodeLength = calculateLength(nodes); + // merge the current selected node delta into the nodes. + if (delta.isNotEmpty) { + nodes.first.insertDelta( + delta.slice(0, selection.startIndex), + insertAfter: false, + ); + + nodes.last.insertDelta( + delta.slice(selection.endIndex), + ); + } + + if (delta.isEmpty && node.type != ParagraphBlockKeys.type) { + nodes[0] = nodes.first.copyWith( + type: node.type, + attributes: { + ...node.attributes, + ...nodes.first.attributes, + }, + ); + } + + for (final child in node.children) { + nodes.last.insert(child); + } + + transaction.insertNodes(selection.end.path, nodes); + + // delete the current node. + transaction.deleteNode(node); + + final path = calculatePath(selection.start.path, nodes); + transaction.afterSelection = Selection.collapsed( + Position( + path: path, + offset: lastNodeLength, + ), + ); + + await apply(transaction); + } + + // delete the selection if it's not collapsed. + Future deleteSelectionIfNeeded() async { + final selection = this.selection; + if (selection == null) { + return null; + } + + // delete the selection first. + if (!selection.isCollapsed) { + await deleteSelection(selection); + } + + // fetch selection again.selection = editorState.selection; + assert(this.selection?.isCollapsed == true); + return this.selection; + } + + Path calculatePath(Path start, List nodes) { + var path = start; + for (var i = 0; i < nodes.length; i++) { + path = path.next; + } + path = path.previous; + if (nodes.last.children.isNotEmpty) { + return [ + ...path, + ...calculatePath([0], nodes.last.children.toList()), + ]; + } + return path; + } + + int calculateLength(List nodes) { + if (nodes.last.children.isNotEmpty) { + return calculateLength(nodes.last.children.toList()); + } + return nodes.last.delta?.length ?? 0; + } +} + +extension on Node { + void insertDelta(Delta delta, {bool insertAfter = true}) { + assert(delta.every((element) => element is TextInsert)); + if (this.delta == null) { + updateAttributes({ + blockComponentDelta: delta.toJson(), + }); + } else if (insertAfter) { + updateAttributes( + { + blockComponentDelta: this + .delta! + .compose( + Delta() + ..retain(this.delta!.length) + ..addAll(delta), + ) + .toJson(), + }, + ); + } else { + updateAttributes( + { + blockComponentDelta: delta + .compose( + Delta() + ..retain(delta.length) + ..addAll(this.delta!), + ) + .toJson(), + }, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart deleted file mode 100644 index c47c0c967d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_block_link.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension PasteFromBlockLink on EditorState { - Future pasteAppFlowySharePageLink(String? sharePageLink) async { - if (sharePageLink == null || sharePageLink.isEmpty) { - return false; - } - - // Check if the link matches the appflowy block link format - final match = appflowySharePageLinkRegex.firstMatch(sharePageLink); - - if (match == null) { - return false; - } - - final workspaceId = match.group(1); - final pageId = match.group(2); - final blockId = match.group(3); - - if (workspaceId == null || pageId == null) { - Log.error( - 'Failed to extract information from block link: $sharePageLink', - ); - return false; - } - - final selection = this.selection; - if (selection == null) { - return false; - } - - final node = getNodesInSelection(selection).firstOrNull; - if (node == null) { - return false; - } - - // todo: if the current link is not from current workspace. - final transaction = this.transaction; - transaction.insertText( - node, - selection.startIndex, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: pageId, - blockId: blockId, - ), - ); - await apply(transaction); - - return true; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart deleted file mode 100644 index dc05e852c9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:cross_file/cross_file.dart'; - -extension PasteFromFile on EditorState { - Future dropFiles( - List dropPath, - List files, - String documentId, - bool isLocalMode, - ) async { - for (final file in files) { - String? path; - FileUrlType? type; - if (isLocalMode) { - path = await saveFileToLocalStorage(file.path); - type = FileUrlType.local; - } else { - (path, _) = await saveFileToCloudStorage(file.path, documentId); - type = FileUrlType.cloud; - } - - if (path == null) { - continue; - } - - final t = transaction - ..insertNode( - dropPath, - fileNode( - url: path, - type: type, - name: file.name, - ), - ); - await apply(t); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart index 3f11759545..e30bcf8a5d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart @@ -1,117 +1,25 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:html2md/html2md.dart' as html2md; extension PasteFromHtml on EditorState { Future pasteHtml(String html) async { - final nodes = convertHtmlToNodes(html); - // if there's no nodes being converted successfully, return false - if (nodes.isEmpty) { - return false; - } - if (nodes.length == 1) { - await pasteSingleLineNode(nodes.first); - checkToShowPasteAsMenu(nodes.first); - } else { - await pasteMultiLineNodes(nodes.toList()); - } - return true; - } - - // Convert the html to document nodes. - // For the google docs table, it will be fallback to the markdown parser. - List convertHtmlToNodes(String html) { - List nodes = htmlToDocument(html).root.children.toList(); - - // 1. remove the front and back empty line + final nodes = htmlToDocument(html).root.children.toList(); + // remove the front and back empty line while (nodes.isNotEmpty && nodes.first.delta?.isEmpty == true) { nodes.removeAt(0); } while (nodes.isNotEmpty && nodes.last.delta?.isEmpty == true) { nodes.removeLast(); } - - // 2. replace the legacy table nodes with the new simple table nodes - for (int i = 0; i < nodes.length; i++) { - final node = nodes[i]; - if (node.type == TableBlockKeys.type) { - nodes[i] = _convertTableToSimpleTable(node); - } + // if there's no nodes being converted successfully, return false + if (nodes.isEmpty) { + return false; } - - // 3. verify the nodes is empty or contains google table flag - // The table from Google Docs will contain the flag 'Google Table' - const googleDocsFlag = 'docs-internal-guid-'; - final isPasteFromGoogleDocs = html.contains(googleDocsFlag); - final isPasteFromAppleNotes = appleNotesRegex.hasMatch(html); - final containsTable = nodes.any( - (node) => - [TableBlockKeys.type, SimpleTableBlockKeys.type].contains(node.type), - ); - if ((nodes.isEmpty || isPasteFromGoogleDocs || containsTable) && - !isPasteFromAppleNotes) { - // fallback to the markdown parser - final markdown = html2md.convert(html); - nodes = customMarkdownToDocument(markdown, tableWidth: 200) - .root - .children - .toList(); + if (nodes.length == 1) { + await pasteSingleLineNode(nodes.first); + } else { + await pasteMultiLineNodes(nodes.toList()); } - - // 4. check if the first node and the last node is bold, because google docs will wrap the table with bold tags - if (isPasteFromGoogleDocs) { - if (nodes.isNotEmpty && nodes.first.delta?.toPlainText() == '**') { - nodes.removeAt(0); - } - if (nodes.isNotEmpty && nodes.last.delta?.toPlainText() == '**') { - nodes.removeLast(); - } - } - - return nodes; - } - - // convert the legacy table node to the new simple table node - // from type 'table' to type 'simple_table' - Node _convertTableToSimpleTable(Node node) { - if (node.type != TableBlockKeys.type) { - return node; - } - - // the table node should contains colsLen and rowsLen - final colsLen = node.attributes[TableBlockKeys.colsLen]; - final rowsLen = node.attributes[TableBlockKeys.rowsLen]; - if (colsLen == null || rowsLen == null) { - return node; - } - - final rows = >[]; - final children = node.children; - for (var i = 0; i < rowsLen; i++) { - final row = []; - for (var j = 0; j < colsLen; j++) { - final cell = children - .where( - (n) => - n.attributes[TableCellBlockKeys.rowPosition] == i && - n.attributes[TableCellBlockKeys.colPosition] == j, - ) - .firstOrNull; - row.add( - simpleTableCellBlockNode( - children: cell?.children.map((e) => e.deepCopy()).toList() ?? - [paragraphNode()], - ), - ); - } - rows.add(row); - } - - return simpleTableBlockNode( - children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(), - ); + return true; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart index d086f36bed..4de0961a85 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart @@ -3,79 +3,32 @@ import 'dart:typed_data'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.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_util.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/default_extensions.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:cross_file/cross_file.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:path/path.dart' as p; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; extension PasteFromImage on EditorState { - Future dropImages( - List dropPath, - List files, - String documentId, - bool isLocalMode, - ) async { - final imageFiles = files.where( - (file) => - file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name.toLowerCase()), - ); + static final supportedImageFormats = [ + 'png', + 'jpeg', + 'gif', + ]; - for (final file in imageFiles) { - String? path; - CustomImageType? type; - if (isLocalMode) { - path = await saveImageToLocalStorage(file.path); - type = CustomImageType.local; - } else { - (path, _) = await saveImageToCloudStorage(file.path, documentId); - type = CustomImageType.internal; - } - - if (path == null) { - continue; - } - - final t = transaction - ..insertNode( - dropPath, - customImageNode(url: path, type: type), - ); - await apply(t); - } - } - - Future pasteImage( - String format, - Uint8List imageBytes, - String documentId, { - Selection? selection, - }) async { - final context = document.root.context; - - if (context == null) { + Future pasteImage(String format, Uint8List imageBytes) async { + if (!supportedImageFormats.contains(format)) { return false; } - if (!defaultImageExtensions.contains(format)) { - Log.info('unsupported format: $format'); - if (UniversalPlatform.isMobile) { - showToastNotification( - message: LocaleKeys.document_imageBlock_error_invalidImageFormat.tr(), - ); - } + final context = document.root.context; + + if (context == null) { return false; } @@ -100,85 +53,47 @@ extension PasteFromImage on EditorState { await File(copyToPath).writeAsBytes(imageBytes); final String? path; - CustomImageType type; + if (context.mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_imageIsUploading.tr(), + ); + } + if (isLocalMode) { path = await saveImageToLocalStorage(copyToPath); - type = CustomImageType.local; } else { - final result = await saveImageToCloudStorage(copyToPath, documentId); + final result = await saveImageToCloudStorage(copyToPath); final errorMessage = result.$2; if (errorMessage != null && context.mounted) { - showToastNotification( - message: errorMessage, + showSnackBarMessage( + context, + errorMessage, ); return false; } path = result.$1; - type = CustomImageType.internal; } if (path != null) { - await insertImageNode(path, selection: selection, type: type); + await insertImageNode(path); } + await File(copyToPath).delete(); return true; } catch (e) { Log.error('cannot copy image file', e); if (context.mounted) { - showToastNotification( - message: LocaleKeys.document_imageBlock_error_invalidImage.tr(), + showSnackBarMessage( + context, + LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); } } return false; } - - Future insertImageNode( - String src, { - Selection? selection, - required CustomImageType type, - }) async { - selection ??= this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final node = getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final transaction = this.transaction; - // if the current node is empty paragraph, replace it with image node - if (node.type == ParagraphBlockKeys.type && - (node.delta?.isEmpty ?? false)) { - transaction - ..insertNode( - node.path, - customImageNode( - url: src, - type: type, - ), - ) - ..deleteNode(node); - } else { - transaction.insertNode( - node.path.next, - customImageNode( - url: src, - type: type, - ), - ); - } - - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path.next, - ), - ); - - return apply(transaction); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart index d4db86d80c..4cc17d599b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart @@ -1,33 +1,19 @@ import 'dart:convert'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; extension PasteFromInAppJson on EditorState { Future pasteInAppJson(String inAppJson) async { try { final nodes = Document.fromJson(jsonDecode(inAppJson)).root.children; - - // skip pasting a table block to another table block - final containsTable = - nodes.any((node) => node.type == SimpleTableBlockKeys.type); - if (containsTable) { - final selectedNodes = getSelectedNodes(withCopy: false); - if (selectedNodes.any((node) => node.parentTableNode != null)) { - return false; - } - } - if (nodes.isEmpty) { - Log.info('pasteInAppJson: nodes is empty'); return false; } if (nodes.length == 1) { - Log.info('pasteInAppJson: single line node'); await pasteSingleLineNode(nodes.first); } else { - Log.info('pasteInAppJson: multi line nodes'); await pasteMultiLineNodes(nodes.toList()); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart index fcb12cefa5..114dde62a3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_plain_text.dart @@ -1,12 +1,15 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:universal_platform/universal_platform.dart'; extension PasteFromPlainText on EditorState { Future pastePlainText(String plainText) async { + if (await pasteHtmlIfAvailable(plainText)) { + return; + } + await deleteSelectionIfNeeded(); + final nodes = plainText .split('\n') .map( @@ -14,7 +17,14 @@ extension PasteFromPlainText on EditorState { ..replaceAll(r'\r', '') ..trimRight(), ) - .map((e) => Delta()..insert(e)) + .map((e) { + // parse the url content + final Attributes attributes = {}; + if (hrefRegex.hasMatch(e)) { + attributes[AppFlowyRichTextKeys.href] = e; + } + return Delta()..insert(e, attributes: attributes); + }) .map((e) => paragraphNode(delta: e)) .toList(); if (nodes.isEmpty) { @@ -27,28 +37,6 @@ extension PasteFromPlainText on EditorState { } } - Future pasteText(String plainText) async { - if (await pasteHtmlIfAvailable(plainText)) { - return; - } - - await deleteSelectionIfNeeded(); - - /// try to parse the plain text as markdown - final nodes = customMarkdownToDocument(plainText).root.children; - if (nodes.isEmpty) { - /// if the markdown parser failed, fallback to the plain text parser - await pastePlainText(plainText); - return; - } - if (nodes.length == 1) { - await pasteSingleLineNode(nodes.first); - checkToShowPasteAsMenu(nodes.first); - } else { - await pasteMultiLineNodes(nodes.toList()); - } - } - Future pasteHtmlIfAvailable(String plainText) async { final selection = this.selection; if (selection == null || @@ -68,29 +56,6 @@ extension PasteFromPlainText on EditorState { AppFlowyRichTextKeys.href: plainText, }); await apply(transaction); - checkToShowPasteAsMenu(node); return true; } - - void checkToShowPasteAsMenu(Node node) { - if (selection == null || !selection!.isCollapsed) return; - if (UniversalPlatform.isMobile) return; - final href = _getLinkFromNode(node); - if (href != null) { - final context = document.root.context; - if (context != null && context.mounted) { - PasteAsMenuService(context: context, editorState: this).show(href); - } - } - } - - String? _getLinkFromNode(Node node) { - final delta = node.delta; - if (delta == null) return null; - final inserts = delta.whereType(); - if (inserts.isEmpty || inserts.length > 1) return null; - final link = inserts.first.attributes?.href; - if (link != null) return inserts.first.text; - return null; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart index 96d39d7500..92f04719a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart @@ -7,15 +7,14 @@ import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -27,8 +26,6 @@ import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - double kDocumentCoverHeight = 98.0; double kDocumentTitlePadding = 20.0; @@ -37,14 +34,10 @@ class DocumentImmersiveCover extends StatefulWidget { super.key, required this.view, required this.userProfilePB, - required this.tabs, - this.fixedTitle, }); final ViewPB view; final UserProfilePB userProfilePB; - final String? fixedTitle; - final List tabs; @override State createState() => _DocumentImmersiveCoverState(); @@ -62,9 +55,6 @@ class _DocumentImmersiveCoverState extends State { void initState() { super.initState(); selectionNotifier?.addListener(_unfocus); - if (widget.view.name.isEmpty) { - focusNode.requestFocus(); - } } @override @@ -85,9 +75,7 @@ class _DocumentImmersiveCoverState extends State { child: BlocConsumer( listener: (context, state) { - if (textEditingController.text != state.name) { - textEditingController.text = state.name; - } + textEditingController.text = state.name; }, builder: (_, state) { final iconAndTitle = _buildIconAndTitle(context, state); @@ -100,22 +88,19 @@ class _DocumentImmersiveCoverState extends State { ); } - return Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Stack( - children: [ - _buildCover(context, state), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 24.0), - child: iconAndTitle, - ), + return Stack( + children: [ + _buildCover(context, state), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: iconAndTitle, ), - ], - ), + ), + ], ); }, ), @@ -153,28 +138,16 @@ class _DocumentImmersiveCoverState extends State { fontFamily = getGoogleFontSafely(documentFontFamily).fontFamily; } - if (widget.fixedTitle != null) { - return FlowyText( - widget.fixedTitle!, - fontSize: 28.0, - fontWeight: FontWeight.w700, - fontFamily: fontFamily, - color: - state.cover.isNone || state.cover.isPresets ? null : Colors.white, - overflow: TextOverflow.ellipsis, - ); - } - return AutoSizeTextField( controller: textEditingController, focusNode: focusNode, minFontSize: 18.0, - decoration: InputDecoration( + decoration: const InputDecoration( border: InputBorder.none, enabledBorder: InputBorder.none, disabledBorder: InputBorder.none, focusedBorder: InputBorder.none, - hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + hintText: '', contentPadding: EdgeInsets.zero, ), scrollController: scrollController, @@ -186,24 +159,12 @@ class _DocumentImmersiveCoverState extends State { state.cover.isNone || state.cover.isPresets ? null : Colors.white, overflow: TextOverflow.ellipsis, ), - onChanged: (name) => Debounce.debounce( - 'rename', - const Duration(milliseconds: 300), - () => _rename(name), - ), - onSubmitted: (name) { - // focus on the document - _createNewLine(); - Debounce.debounce( - 'rename', - const Duration(milliseconds: 300), - () => _rename(name), - ); - }, + onChanged: _rename, + onSubmitted: _rename, ); } - Widget _buildIcon(BuildContext context, EmojiIconData icon) { + Widget _buildIcon(BuildContext context, String icon) { return GestureDetector( child: ConstrainedBox( constraints: const BoxConstraints.tightFor(width: 34.0), @@ -219,26 +180,28 @@ class _DocumentImmersiveCoverState extends State { context, showDragHandle: true, showDivider: false, + showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, + showRemoveButton: true, + onRemove: () { + pageStyleIconBloc.add( + const PageStyleIconEvent.updateIcon('', true), + ); + }, scrollableWidgetBuilder: (_, controller) { return BlocProvider.value( value: pageStyleIconBloc, child: Expanded( - child: FlowyIconEmojiPicker( - initialType: icon.type.toPickerTabType(), - tabs: widget.tabs, - documentId: widget.view.id, - onSelectedEmoji: (r) { - pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r.data, true), - ); - if (!r.keepOpen) Navigator.pop(context); - }, + child: Scrollbar( + controller: controller, + child: IconSelector( + scrollController: controller, + ), ), ), ); @@ -324,35 +287,4 @@ class _DocumentImmersiveCoverState extends State { scrollController.position.jumpTo(0); context.read().add(ViewEvent.rename(name)); } - - Future _createNewLine() async { - focusNode.unfocus(); - - final selection = textEditingController.selection; - final text = textEditingController.text; - // split the text into two lines based on the cursor position - final parts = [ - text.substring(0, selection.baseOffset), - text.substring(selection.baseOffset), - ]; - textEditingController.text = parts[0]; - - final editorState = context.read().state.editorState; - if (editorState == null) { - Log.info('editorState is null when creating new line'); - return; - } - - final transaction = editorState.transaction; - transaction.insertNode([0], paragraphNode(text: parts[1])); - await editorState.apply(transaction); - - // update selection instead of using afterSelection in transaction, - // because it will cause the cursor to jump - await editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - // trigger the keyboard service. - reason: SelectionUpdateReason.uiEvent, - ); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart index 3006fc3104..bd2692e172 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart @@ -1,13 +1,10 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - part 'document_immersive_cover_bloc.freezed.dart'; class DocumentImmersiveCoverBloc @@ -20,14 +17,10 @@ class DocumentImmersiveCoverBloc (event, emit) async { await event.when( initial: () async { - final latestView = await ViewBackendService.getView(view.id); add( DocumentImmersiveCoverEvent.updateCoverAndIcon( - latestView.fold( - (s) => s.cover, - (e) => view.cover, - ), - EmojiIconData.fromViewIconPB(view.icon), + view.cover, + view.icon.value, view.name, ), ); @@ -36,7 +29,7 @@ class DocumentImmersiveCoverBloc add( DocumentImmersiveCoverEvent.updateCoverAndIcon( view.cover, - EmojiIconData.fromViewIconPB(view.icon), + view.icon.value, view.name, ), ); @@ -70,10 +63,9 @@ class DocumentImmersiveCoverBloc @freezed class DocumentImmersiveCoverEvent with _$DocumentImmersiveCoverEvent { const factory DocumentImmersiveCoverEvent.initial() = Initial; - const factory DocumentImmersiveCoverEvent.updateCoverAndIcon( PageStyleCover? cover, - EmojiIconData? icon, + String? icon, String? name, ) = UpdateCoverAndIcon; } @@ -81,7 +73,7 @@ class DocumentImmersiveCoverEvent with _$DocumentImmersiveCoverEvent { @freezed class DocumentImmersiveCoverState with _$DocumentImmersiveCoverState { const factory DocumentImmersiveCoverState({ - @Default(null) EmojiIconData? icon, + @Default(null) String? icon, required PageStyleCover cover, @Default('') String name, }) = _DocumentImmersiveCoverState; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart index 87c2815091..d780a1260e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart @@ -1,9 +1,5 @@ -import 'dart:async'; - import 'package:appflowy/plugins/database/widgets/database_view_widget.dart'; -import 'package:appflowy/plugins/document/presentation/compact_mode_event.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/built_in_page_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -17,14 +13,8 @@ class DatabaseBlockKeys { static const String parentID = 'parent_id'; static const String viewID = 'view_id'; - static const String enableCompactMode = 'enable_compact_mode'; } -const overflowTypes = { - DatabaseBlockKeys.gridType, - DatabaseBlockKeys.boardType, -}; - class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { DatabaseViewBlockComponentBuilder({ super.configuration, @@ -42,15 +32,11 @@ class DatabaseViewBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (node) => + bool validate(Node node) => node.children.isEmpty && node.attributes[DatabaseBlockKeys.parentID] is String && node.attributes[DatabaseBlockKeys.viewID] is String; @@ -62,7 +48,6 @@ class DatabaseBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -80,65 +65,37 @@ class _DatabaseBlockComponentWidgetState @override BlockComponentConfiguration get configuration => widget.configuration; - late StreamSubscription compactModeSubscription; - EditorState? editorState; - - @override - void initState() { - super.initState(); - compactModeSubscription = - compactModeEventBus.on().listen((event) { - if (event.id != node.id) return; - final newAttributes = { - ...node.attributes, - DatabaseBlockKeys.enableCompactMode: event.enable, - }; - final theEditorState = editorState; - if (theEditorState == null) return; - final transaction = theEditorState.transaction; - transaction.updateNode(node, newAttributes); - theEditorState.apply(transaction); - }); - } - - @override - void dispose() { - super.dispose(); - compactModeSubscription.cancel(); - editorState = null; - } - @override Widget build(BuildContext context) { final editorState = Provider.of(context, listen: false); - this.editorState = editorState; Widget child = BuiltInPageWidget( node: widget.node, editorState: editorState, - builder: (view) => Provider.value( - value: ReferenceState(true), - child: DatabaseViewWidget( - key: ValueKey(view.id), - view: view, - actionBuilder: widget.actionBuilder, - showActions: widget.showActions, - node: widget.node, - ), + builder: (viewPB) { + return DatabaseViewWidget( + key: ValueKey(viewPB.id), + view: viewPB, + ); + }, + ); + + child = Padding( + padding: padding, + child: FocusScope( + skipTraversal: true, + onFocusChange: (value) { + if (value) { + context.read().selection = null; + } + }, + child: child, ), ); - child = FocusScope( - skipTraversal: true, - onFocusChange: (value) { - if (value && keepEditorFocusNotifier.value == 0) { - context.read().selection = null; - } - }, - child: child, - ); - - if (!editorState.editable) { - child = IgnorePointer( + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: widget.node, + actionBuilder: widget.actionBuilder!, child: child, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart index 8f6c117833..79af12b668 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/database/referenced_database_menu_item.dart @@ -11,16 +11,13 @@ import 'package:easy_localization/easy_localization.dart'; SelectionMenuItem referencedDocumentMenuItem = SelectionMenuItem( getName: LocaleKeys.document_plugins_referencedDocument.tr, icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.icon_document_s, + data: FlowySvgs.document_s, isSelected: onSelected, style: style, ), keywords: ['page', 'notes', 'referenced page', 'referenced document'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Document, - ), + handler: (editorState, menuService, context) => + showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Document), ); // Database References @@ -33,11 +30,8 @@ SelectionMenuItem referencedGridMenuItem = SelectionMenuItem( style: style, ), keywords: ['referenced', 'grid', 'database'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Grid, - ), + handler: (editorState, menuService, context) => + showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Grid), ); SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem( @@ -48,11 +42,8 @@ SelectionMenuItem referencedBoardMenuItem = SelectionMenuItem( style: style, ), keywords: ['referenced', 'board', 'kanban'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Board, - ), + handler: (editorState, menuService, context) => + showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Board), ); SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem( @@ -63,9 +54,6 @@ SelectionMenuItem referencedCalendarMenuItem = SelectionMenuItem( style: style, ), keywords: ['referenced', 'calendar', 'database'], - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Calendar, - ), + handler: (editorState, menuService, context) => + showLinkToPageMenu(editorState, menuService, ViewLayoutPB.Calendar), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart deleted file mode 100644 index 905c033bda..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/delta/text_delta_extension.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -typedef MentionPageNameGetter = Future Function(String pageId); - -extension TextDeltaExtension on Delta { - /// Convert the delta to a text string. - /// - /// Unlike the [toPlainText], this method will keep the mention text - /// such as mentioned page name, mentioned block content. - /// - /// If the mentioned page or mentioned block not found, it will downgrade to - /// the default plain text. - Future toText({ - required MentionPageNameGetter getMentionPageName, - }) async { - final defaultPlainText = toPlainText(); - - String text = ''; - final ops = iterator; - while (ops.moveNext()) { - final op = ops.current; - final attributes = op.attributes; - if (op is TextInsert) { - // if the text is '\$', it means the block text is empty, - // the real data is in the attributes - if (op.text == MentionBlockKeys.mentionChar) { - final mention = attributes?[MentionBlockKeys.mention]; - final mentionPageId = mention?[MentionBlockKeys.pageId]; - final mentionType = mention?[MentionBlockKeys.type]; - if (mentionPageId != null) { - text += await getMentionPageName(mentionPageId); - continue; - } else if (mentionType == MentionType.externalLink.name) { - final url = mention?[MentionBlockKeys.url] ?? ''; - final info = await LinkInfoCache.get(url); - text += info?.title ?? url; - continue; - } - } - - text += op.text; - } else { - // if the delta contains other types of operations, - // return the default plain text - return defaultPlainText; - } - } - - return text; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart deleted file mode 100644 index 162c7a1c34..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/overlay_util.dart'; -import 'package:flutter/material.dart'; - -class ColorPicker extends StatefulWidget { - const ColorPicker({ - super.key, - required this.title, - required this.selectedColorHex, - required this.onSubmittedColorHex, - required this.colorOptions, - this.resetText, - this.customColorHex, - this.resetIconName, - this.showClearButton = false, - }); - - final String title; - final String? selectedColorHex; - final String? customColorHex; - final void Function(String? color, bool isCustomColor) onSubmittedColorHex; - final String? resetText; - final String? resetIconName; - final bool showClearButton; - - final List colorOptions; - - @override - State createState() => _ColorPickerState(); -} - -class _ColorPickerState extends State { - final TextEditingController _colorHexController = TextEditingController(); - final TextEditingController _colorOpacityController = TextEditingController(); - - @override - void initState() { - super.initState(); - final selectedColorHex = widget.selectedColorHex, - customColorHex = widget.customColorHex; - _colorHexController.text = - _extractColorHex(customColorHex ?? selectedColorHex) ?? 'FFFFFF'; - _colorOpacityController.text = - _convertHexToOpacity(customColorHex ?? selectedColorHex) ?? '100'; - } - - @override - Widget build(BuildContext context) { - return basicOverlay( - context, - width: 300, - height: 250, - children: [ - EditorOverlayTitle(text: widget.title), - const SizedBox(height: 6), - widget.showClearButton && - widget.resetText != null && - widget.resetIconName != null - ? ResetColorButton( - resetText: widget.resetText!, - resetIconName: widget.resetIconName!, - onPressed: (color) => - widget.onSubmittedColorHex.call(color, false), - ) - : const SizedBox.shrink(), - CustomColorItem( - colorController: _colorHexController, - opacityController: _colorOpacityController, - onSubmittedColorHex: (color) => - widget.onSubmittedColorHex.call(color, true), - ), - _buildColorItems( - widget.colorOptions, - widget.selectedColorHex, - ), - ], - ); - } - - Widget _buildColorItems( - List options, - String? selectedColor, - ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: options - .map((e) => _buildColorItem(e, e.colorHex == selectedColor)) - .toList(), - ); - } - - Widget _buildColorItem(ColorOption option, bool isChecked) { - return SizedBox( - height: 36, - child: TextButton.icon( - onPressed: () { - widget.onSubmittedColorHex(option.colorHex, false); - }, - icon: SizedBox.square( - dimension: 12, - child: Container( - decoration: BoxDecoration( - color: option.colorHex.tryToColor(), - shape: BoxShape.circle, - ), - ), - ), - style: buildOverlayButtonStyle(context), - label: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - option.name, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.fade, - style: TextStyle( - color: Theme.of(context).textTheme.labelLarge?.color, - ), - ), - ), - // checkbox - if (isChecked) const FlowySvg(FlowySvgs.toolbar_check_m), - ], - ), - ), - ); - } - - String? _convertHexToOpacity(String? colorHex) { - if (colorHex == null) return null; - final opacityHex = colorHex.substring(2, 4); - final opacity = int.parse(opacityHex, radix: 16) / 2.55; - return opacity.toStringAsFixed(0); - } - - String? _extractColorHex(String? colorHex) { - if (colorHex == null) return null; - return colorHex.substring(4); - } -} - -class ResetColorButton extends StatelessWidget { - const ResetColorButton({ - super.key, - required this.resetText, - required this.resetIconName, - required this.onPressed, - }); - - final Function(String? color) onPressed; - final String resetText; - final String resetIconName; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: double.infinity, - height: 32, - child: TextButton.icon( - onPressed: () => onPressed(null), - icon: EditorSvg( - name: resetIconName, - width: 13, - height: 13, - color: Theme.of(context).iconTheme.color, - ), - label: Text( - resetText, - style: TextStyle( - color: Theme.of(context).hintColor, - ), - textAlign: TextAlign.left, - ), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.hovered)) { - return Theme.of(context).hoverColor; - } - return Colors.transparent; - }, - ), - alignment: Alignment.centerLeft, - ), - ), - ); - } -} - -class CustomColorItem extends StatefulWidget { - const CustomColorItem({ - super.key, - required this.colorController, - required this.opacityController, - required this.onSubmittedColorHex, - }); - - final TextEditingController colorController; - final TextEditingController opacityController; - final void Function(String color) onSubmittedColorHex; - - @override - State createState() => _CustomColorItemState(); -} - -class _CustomColorItemState extends State { - @override - Widget build(BuildContext context) { - return ExpansionTile( - tilePadding: const EdgeInsets.only(left: 8), - shape: Border.all( - color: Colors.transparent, - ), // remove the default border when it is expanded - title: Row( - children: [ - // color sample box - SizedBox.square( - dimension: 12, - child: Container( - decoration: BoxDecoration( - color: Color( - int.tryParse( - _combineColorHexAndOpacity( - widget.colorController.text, - widget.opacityController.text, - ), - ) ?? - 0xFFFFFFFF, - ), - shape: BoxShape.circle, - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - AppFlowyEditorL10n.current.customColor, - style: Theme.of(context).textTheme.labelLarge, - // same style as TextButton.icon - ), - ), - ], - ), - children: [ - const SizedBox(height: 6), - _customColorDetailsTextField( - labelText: AppFlowyEditorL10n.current.hexValue, - controller: widget.colorController, - // update the color sample box when the text changes - onChanged: (_) => setState(() {}), - onSubmitted: _submitCustomColorHex, - ), - const SizedBox(height: 10), - _customColorDetailsTextField( - labelText: AppFlowyEditorL10n.current.opacity, - controller: widget.opacityController, - // update the color sample box when the text changes - onChanged: (_) => setState(() {}), - onSubmitted: _submitCustomColorHex, - ), - const SizedBox(height: 6), - ], - ); - } - - Widget _customColorDetailsTextField({ - required String labelText, - required TextEditingController controller, - Function(String)? onChanged, - Function(String)? onSubmitted, - }) { - return Padding( - padding: const EdgeInsets.only(right: 3), - child: TextField( - controller: controller, - decoration: InputDecoration( - labelText: labelText, - border: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - style: Theme.of(context).textTheme.bodyMedium, - onChanged: onChanged, - onSubmitted: onSubmitted, - ), - ); - } - - String _combineColorHexAndOpacity(String colorHex, String opacity) { - colorHex = _fixColorHex(colorHex); - opacity = _fixOpacity(opacity); - final opacityHex = (int.parse(opacity) * 2.55).round().toRadixString(16); - return '0x$opacityHex$colorHex'; - } - - String _fixColorHex(String colorHex) { - if (colorHex.length > 6) { - colorHex = colorHex.substring(0, 6); - } - if (int.tryParse(colorHex, radix: 16) == null) { - colorHex = 'FFFFFF'; - } - return colorHex; - } - - String _fixOpacity(String opacity) { - // if opacity is 0 - 99, return it - // otherwise return 100 - final RegExp regex = RegExp('^(0|[1-9][0-9]?)'); - if (regex.hasMatch(opacity)) { - return opacity; - } else { - return '100'; - } - } - - void _submitCustomColorHex(String value) { - final String color = _combineColorHexAndOpacity( - widget.colorController.text, - widget.opacityController.text, - ); - widget.onSubmittedColorHex(color); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart deleted file mode 100644 index 03fc12a37c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_animation.dart'; - -class DesktopFloatingToolbar extends StatefulWidget { - const DesktopFloatingToolbar({ - super.key, - required this.editorState, - required this.child, - required this.onDismiss, - this.enableAnimation = true, - }); - - final EditorState editorState; - final Widget child; - final VoidCallback onDismiss; - final bool enableAnimation; - - @override - State createState() => _DesktopFloatingToolbarState(); -} - -class _DesktopFloatingToolbarState extends State { - EditorState get editorState => widget.editorState; - - _Position? position; - final toolbarController = getIt(); - - @override - void initState() { - super.initState(); - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return; - } - final selectionRect = editorState.selectionRects(); - if (selectionRect.isEmpty) return; - position = calculateSelectionMenuOffset(selectionRect.first); - toolbarController._addCallback(dismiss); - } - - @override - void dispose() { - toolbarController._removeCallback(dismiss); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (position == null) return Container(); - return Positioned( - left: position!.left, - top: position!.top, - right: position!.right, - child: widget.enableAnimation - ? ToolbarAnimationWidget(child: widget.child) - : widget.child, - ); - } - - void dismiss() { - widget.onDismiss.call(); - } - - _Position calculateSelectionMenuOffset( - Rect rect, - ) { - const toolbarHeight = 40, topLimit = toolbarHeight + 8; - final bool isLongMenu = onlyShowInSingleSelectionAndTextType(editorState); - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorSize = editorState.renderBox?.size ?? Size.zero; - final menuWidth = - isLongMenu ? (isNarrowWindow(editorState) ? 490.0 : 660.0) : 420.0; - final editorRect = editorOffset & editorSize; - final left = rect.left, leftStart = 50; - final top = - rect.top < topLimit ? rect.bottom + topLimit : rect.top - topLimit; - if (left + menuWidth > editorRect.right) { - return _Position( - editorRect.right - menuWidth, - top, - null, - ); - } else if (rect.left - leftStart > 0) { - return _Position(rect.left - leftStart, top, null); - } else { - return _Position(rect.left, top, null); - } - } -} - -class _Position { - _Position(this.left, this.top, this.right); - - final double? left; - final double? top; - final double? right; -} - -class FloatingToolbarController { - final Set _dismissCallbacks = {}; - final Set _displayListeners = {}; - - void _addCallback(VoidCallback callback) { - _dismissCallbacks.add(callback); - for (final listener in Set.of(_displayListeners)) { - listener.call(); - } - } - - void _removeCallback(VoidCallback callback) => - _dismissCallbacks.remove(callback); - - bool get isToolbarShowing => _dismissCallbacks.isNotEmpty; - - void addDisplayListener(VoidCallback listener) => - _displayListeners.add(listener); - - void removeDisplayListener(VoidCallback listener) => - _displayListeners.remove(listener); - - void hideToolbar() { - if (_dismissCallbacks.isEmpty) return; - for (final callback in _dismissCallbacks) { - callback.call(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart deleted file mode 100644 index 002d569c7b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart +++ /dev/null @@ -1,320 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.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 'link_search_text_field.dart'; - -class LinkCreateMenu extends StatefulWidget { - const LinkCreateMenu({ - super.key, - required this.editorState, - required this.onSubmitted, - required this.onDismiss, - required this.alignment, - required this.currentViewId, - required this.initialText, - }); - - final EditorState editorState; - final void Function(String link, bool isPage) onSubmitted; - final VoidCallback onDismiss; - final String currentViewId; - final String initialText; - final LinkMenuAlignment alignment; - - @override - State createState() => _LinkCreateMenuState(); -} - -class _LinkCreateMenuState extends State { - late LinkSearchTextField searchTextField = LinkSearchTextField( - currentViewId: widget.currentViewId, - initialSearchText: widget.initialText, - onEnter: () { - searchTextField.onSearchResult( - onLink: () => onSubmittedLink(), - onRecentViews: () => - onSubmittedPageLink(searchTextField.currentRecentView), - onSearchViews: () => - onSubmittedPageLink(searchTextField.currentSearchedView), - onEmpty: () {}, - ); - }, - onEscape: widget.onDismiss, - onDataRefresh: () { - if (mounted) setState(() {}); - }, - ); - - bool get isTextfieldEnable => searchTextField.isTextfieldEnable; - - String get searchText => searchTextField.searchText; - - bool get showAtTop => widget.alignment.isTop; - - bool showErrorText = false; - - @override - void initState() { - super.initState(); - searchTextField.requestFocus(); - searchTextField.searchRecentViews(); - final focusNode = searchTextField.focusNode; - bool hasFocus = focusNode.hasFocus; - focusNode.addListener(() { - if (hasFocus != focusNode.hasFocus && mounted) { - setState(() { - hasFocus = focusNode.hasFocus; - }); - } - }); - } - - @override - void dispose() { - searchTextField.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 320, - child: Column( - children: showAtTop - ? [ - searchTextField.buildResultContainer( - margin: EdgeInsets.only(bottom: 2), - context: context, - onLinkSelected: onSubmittedLink, - onPageLinkSelected: onSubmittedPageLink, - ), - buildSearchContainer(), - ] - : [ - buildSearchContainer(), - searchTextField.buildResultContainer( - margin: EdgeInsets.only(top: 2), - context: context, - onLinkSelected: onSubmittedLink, - onPageLinkSelected: onSubmittedPageLink, - ), - ], - ), - ); - } - - Widget buildSearchContainer() { - return Container( - width: 320, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.all(8), - child: ValueListenableBuilder( - valueListenable: searchTextField.textEditingController, - builder: (context, _, __) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: searchTextField.buildTextField(context: context), - ), - HSpace(8), - FlowyTextButton( - LocaleKeys.document_toolbar_insert.tr(), - mainAxisAlignment: MainAxisAlignment.center, - padding: EdgeInsets.zero, - constraints: BoxConstraints(maxWidth: 72, minHeight: 32), - fontSize: 14, - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - lineHeight: 20 / 14, - fontWeight: FontWeight.w600, - onPressed: onSubmittedLink, - ), - ], - ), - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - }, - ), - ); - } - - void onSubmittedLink() { - if (!isTextfieldEnable) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onSubmitted(searchText, false); - } - - void onSubmittedPageLink(ViewPB view) async { - final workspaceId = context - .read() - ?.state - .currentWorkspace - ?.workspaceId ?? - ''; - final link = ShareConstants.buildShareUrl( - workspaceId: workspaceId, - viewId: view.id, - ); - widget.onSubmitted(link, true); - } -} - -void showLinkCreateMenu( - BuildContext context, - EditorState editorState, - Selection selection, - String currentViewId, -) { - if (!context.mounted) return; - final (left, top, right, bottom, alignment) = _getPosition(editorState); - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final selectedText = editorState.getTextInSelection(selection).join(); - - OverlayEntry? overlay; - - void dismissOverlay() { - keepEditorFocusNotifier.decrease(); - overlay?.remove(); - overlay = null; - } - - keepEditorFocusNotifier.increase(); - overlay = FullScreenOverlayEntry( - top: top, - bottom: bottom, - left: left, - right: right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) { - return LinkCreateMenu( - alignment: alignment, - initialText: selectedText, - currentViewId: currentViewId, - editorState: editorState, - onSubmitted: (link, isPage) async { - await editorState.formatDelta(selection, { - BuiltInAttributeKey.href: link, - kIsPageLink: isPage, - }); - await editorState.updateSelectionWithReason( - null, - reason: SelectionUpdateReason.uiEvent, - ); - dismissOverlay(); - }, - onDismiss: dismissOverlay, - ); - }, - ).build(); - - Overlay.of(context, rootOverlay: true).insert(overlay!); -} - -// get a proper position for link menu -( - double? left, - double? top, - double? right, - double? bottom, - LinkMenuAlignment alignment, -) _getPosition( - EditorState editorState, -) { - final rect = editorState.selectionRects().first; - const menuHeight = 222.0, menuWidth = 320.0; - - double? left, right, top, bottom; - LinkMenuAlignment alignment = LinkMenuAlignment.topLeft; - final editorOffset = editorState.renderBox!.localToGlobal(Offset.zero), - editorSize = editorState.renderBox!.size; - final editorBottom = editorSize.height + editorOffset.dy, - editorRight = editorSize.width + editorOffset.dx; - final overflowBottom = rect.bottom + menuHeight > editorBottom, - overflowTop = rect.top - menuHeight < 0, - overflowLeft = rect.left - menuWidth < 0, - overflowRight = rect.right + menuWidth > editorRight; - - if (overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else if (overflowBottom && !overflowTop) { - /// show at top - bottom = editorBottom - rect.top; - } else if (!overflowTop && !overflowBottom) { - /// show at bottom - top = rect.bottom; - } else { - top = 0; - } - - if (overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else if (overflowRight && !overflowLeft) { - /// show at left - right = editorRight - rect.right; - } else if (!overflowLeft && !overflowRight) { - /// show at right - left = rect.left; - } else { - left = 0; - } - - if (left != null && top != null) { - alignment = LinkMenuAlignment.bottomRight; - } else if (left != null && bottom != null) { - alignment = LinkMenuAlignment.topRight; - } else if (right != null && top != null) { - alignment = LinkMenuAlignment.bottomLeft; - } else if (right != null && bottom != null) { - alignment = LinkMenuAlignment.topLeft; - } - - return (left, top, right, bottom, alignment); -} - -ShapeDecoration buildToolbarLinkDecoration( - BuildContext context, { - double radius = 12.0, -}) { - final theme = AppFlowyTheme.of(context); - return ShapeDecoration( - color: theme.surfaceColorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius), - ), - shadows: theme.shadow.small, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart deleted file mode 100644 index e90ee22a80..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_edit_menu.dart +++ /dev/null @@ -1,516 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.dart'; -import 'package:flutter/services.dart'; -import 'link_create_menu.dart'; -import 'link_search_text_field.dart'; -import 'link_styles.dart'; - -class LinkEditMenu extends StatefulWidget { - const LinkEditMenu({ - super.key, - required this.linkInfo, - required this.onDismiss, - required this.onApply, - required this.onRemoveLink, - required this.currentViewId, - }); - - final LinkInfo linkInfo; - final ValueChanged onApply; - final ValueChanged onRemoveLink; - final VoidCallback onDismiss; - final String currentViewId; - - @override - State createState() => _LinkEditMenuState(); -} - -class _LinkEditMenuState extends State { - ValueChanged get onRemoveLink => widget.onRemoveLink; - - VoidCallback get onDismiss => widget.onDismiss; - - late TextEditingController linkNameController = - TextEditingController(text: linkInfo.name); - late FocusNode textFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); - late FocusNode menuFocusNode = FocusNode(onKeyEvent: onFocusKeyEvent); - late LinkInfo linkInfo = widget.linkInfo; - late LinkSearchTextField searchTextField; - bool isShowingSearchResult = false; - ViewPB? currentView; - bool showErrorText = false; - - @override - void initState() { - super.initState(); - final isPageLink = linkInfo.isPage; - if (isPageLink) getPageView(); - searchTextField = LinkSearchTextField( - initialSearchText: isPageLink ? '' : linkInfo.link, - initialViewId: linkInfo.viewId, - currentViewId: widget.currentViewId, - onEnter: onConfirm, - onEscape: () { - if (isShowingSearchResult) { - hideSearchResult(); - } else { - onDismiss(); - } - }, - onDataRefresh: () { - if (mounted) setState(() {}); - }, - )..searchRecentViews(); - makeSureHasFocus(); - } - - @override - void dispose() { - linkNameController.dispose(); - textFocusNode.dispose(); - menuFocusNode.dispose(); - searchTextField.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final showingRecent = - searchTextField.showingRecent && isShowingSearchResult; - final errorHeight = showErrorText ? 20.0 : 0.0; - return GestureDetector( - onTap: onDismiss, - child: Focus( - focusNode: menuFocusNode, - child: Container( - width: 400, - height: 250 + (showingRecent ? 32 : 0), - color: Colors.white.withAlpha(1), - child: Stack( - children: [ - GestureDetector( - onTap: hideSearchResult, - child: Container( - width: 400, - height: 192 + errorHeight, - decoration: buildToolbarLinkDecoration(context), - ), - ), - Positioned( - top: 16, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_pageOrURL.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Positioned( - top: 80 + errorHeight, - left: 20, - child: FlowyText.semibold( - LocaleKeys.document_toolbar_linkName.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Positioned( - top: 144 + errorHeight, - left: 20, - child: buildButtons(), - ), - Positioned( - top: 100 + errorHeight, - left: 20, - child: buildNameTextField(), - ), - Positioned( - top: 36, - left: 20, - child: buildLinkField(), - ), - ], - ), - ), - ), - ); - } - - Widget buildLinkField() { - final showPageView = linkInfo.isPage && !isShowingSearchResult; - Widget child; - if (showPageView) { - child = buildPageView(); - } else if (!isShowingSearchResult) { - child = buildLinkView(); - } else { - return SizedBox( - width: 360, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 360, - height: 32, - child: searchTextField.buildTextField( - autofocus: true, - context: context, - ), - ), - VSpace(6), - searchTextField.buildResultContainer( - context: context, - width: 360, - onPageLinkSelected: onPageSelected, - onLinkSelected: onLinkSelected, - ), - ], - ), - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - child, - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - } - - Widget buildButtons() { - return GestureDetector( - onTap: hideSearchResult, - child: SizedBox( - width: 360, - height: 32, - child: Row( - children: [ - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), - width: 32, - height: 32, - tooltipText: LocaleKeys.editor_removeLink.tr(), - preferBelow: false, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor(context)), - ), - onPressed: () => onRemoveLink.call(linkInfo), - ), - Spacer(), - DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(8)), - border: Border.all(color: LinkStyle.borderColor(context)), - ), - child: FlowyTextButton( - LocaleKeys.button_cancel.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - fontColor: Theme.of(context).isLightMode - ? LinkStyle.textPrimary - : Theme.of(context).iconTheme.color, - fillColor: Colors.transparent, - fontWeight: FontWeight.w400, - onPressed: onDismiss, - ), - ), - HSpace(12), - ValueListenableBuilder( - valueListenable: linkNameController, - builder: (context, _, __) { - return FlowyTextButton( - LocaleKeys.settings_appearance_documentSettings_apply.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - fontWeight: FontWeight.w400, - onPressed: onApply, - ); - }, - ), - ], - ), - ), - ); - } - - Widget buildNameTextField() { - return SizedBox( - width: 360, - height: 32, - child: TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - focusNode: textFocusNode, - autofocus: true, - textAlign: TextAlign.left, - controller: linkNameController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - onChanged: (text) { - linkInfo = LinkInfo( - name: text, - link: linkInfo.link, - isPage: linkInfo.isPage, - ); - }, - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_toolbar_linkNameHint.tr(), - context, - ), - ), - ); - } - - Widget buildPageView() { - late Widget child; - final view = currentView; - if (view == null) { - child = Center( - child: SizedBox.fromSize( - size: Size(10, 10), - child: CircularProgressIndicator(), - ), - ); - } else { - final viewName = view.name; - final displayName = viewName.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : viewName; - child = GestureDetector( - onTap: showSearchResult, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowyTooltip( - preferBelow: false, - message: displayName, - child: Container( - height: 32, - padding: EdgeInsets.fromLTRB(8, 0, 8, 0), - child: Row( - children: [ - searchTextField.buildIcon(view), - HSpace(4), - Flexible( - child: FlowyText.regular( - displayName, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ), - ], - ), - ), - ), - ), - ); - } - return Container( - width: 360, - height: 32, - decoration: buildDecoration(), - child: child, - ); - } - - Widget buildLinkView() { - return Container( - width: 360, - height: 32, - decoration: buildDecoration(), - child: FlowyTooltip( - preferBelow: false, - message: linkInfo.link, - child: GestureDetector( - onTap: showSearchResult, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Padding( - padding: EdgeInsets.fromLTRB(8, 6, 8, 6), - child: Row( - children: [ - FlowySvg(FlowySvgs.toolbar_link_earth_m), - HSpace(8), - Flexible( - child: FlowyText.regular( - linkInfo.link, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } - - KeyEventResult onFocusKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - if (key.logicalKey == LogicalKeyboardKey.enter) { - onApply(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.escape) { - onDismiss(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - Future makeSureHasFocus() async { - final focusNode = textFocusNode; - if (!mounted || focusNode.hasFocus) return; - focusNode.requestFocus(); - WidgetsBinding.instance.addPostFrameCallback((_) { - makeSureHasFocus(); - }); - } - - void onApply() { - if (isShowingSearchResult) { - onConfirm(); - return; - } - if (linkInfo.link.isEmpty) { - widget.onRemoveLink(linkInfo); - return; - } - if (linkInfo.link.isEmpty || !isUri(linkInfo.link)) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onApply.call(linkInfo); - } - - void onConfirm() { - searchTextField.onSearchResult( - onLink: onLinkSelected, - onRecentViews: () => onPageSelected(searchTextField.currentRecentView), - onSearchViews: () => onPageSelected(searchTextField.currentSearchedView), - onEmpty: () { - searchTextField.unfocus(); - }, - ); - menuFocusNode.requestFocus(); - } - - Future getPageView() async { - if (!linkInfo.isPage) return; - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(linkInfo.viewId); - if (mounted) { - setState(() { - currentView = view; - }); - } - } - - void showSearchResult() { - setState(() { - if (linkInfo.isPage) searchTextField.updateText(''); - isShowingSearchResult = true; - searchTextField.requestFocus(); - }); - } - - void hideSearchResult() { - setState(() { - isShowingSearchResult = false; - searchTextField.unfocus(); - textFocusNode.unfocus(); - }); - } - - void onLinkSelected() { - if (mounted) { - linkInfo = LinkInfo( - name: linkInfo.name, - link: searchTextField.searchText, - ); - hideSearchResult(); - } - } - - Future onPageSelected(ViewPB view) async { - currentView = view; - final link = ShareConstants.buildShareUrl( - workspaceId: await UserBackendService.getCurrentWorkspace().fold( - (s) => s.id, - (f) => '', - ), - viewId: view.id, - ); - linkInfo = LinkInfo( - name: linkInfo.name, - link: link, - isPage: true, - ); - searchTextField.updateText(linkInfo.link); - if (mounted) { - setState(() { - isShowingSearchResult = false; - searchTextField.unfocus(); - }); - } - } - - BoxDecoration buildDecoration() => BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: LinkStyle.borderColor(context)), - ); -} - -class LinkInfo { - LinkInfo({this.isPage = false, required this.name, required this.link}); - - final bool isPage; - final String name; - final String link; - - Attributes toAttribute() => - {AppFlowyRichTextKeys.href: link, kIsPageLink: isPage}; - - String get viewId => isPage ? link.split('/').lastOrNull ?? '' : ''; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart deleted file mode 100644 index c992e40c61..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ /dev/null @@ -1,635 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/view/view_service.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:appflowy_editor_plugins/appflowy_editor_plugins.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/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'link_create_menu.dart'; -import 'link_edit_menu.dart'; - -class LinkHoverTrigger extends StatefulWidget { - const LinkHoverTrigger({ - super.key, - required this.editorState, - required this.selection, - required this.node, - required this.attribute, - required this.size, - this.delayToShow = const Duration(milliseconds: 50), - this.delayToHide = const Duration(milliseconds: 300), - }); - - final EditorState editorState; - final Selection selection; - final Node node; - final Attributes attribute; - final Size size; - final Duration delayToShow; - final Duration delayToHide; - - @override - State createState() => _LinkHoverTriggerState(); -} - -class _LinkHoverTriggerState extends State { - final hoverMenuController = PopoverController(); - final editMenuController = PopoverController(); - final toolbarController = getIt(); - bool isHoverMenuShowing = false; - bool isHoverMenuHovering = false; - bool isHoverTriggerHovering = false; - - Size get size => widget.size; - - EditorState get editorState => widget.editorState; - - Selection get selection => widget.selection; - - Attributes get attribute => widget.attribute; - - late HoverTriggerKey triggerKey = HoverTriggerKey(widget.node.id, selection); - - @override - void initState() { - super.initState(); - getIt()._add(triggerKey, showLinkHoverMenu); - toolbarController.addDisplayListener(onToolbarShow); - } - - @override - void dispose() { - hoverMenuController.close(); - editMenuController.close(); - getIt()._remove(triggerKey, showLinkHoverMenu); - toolbarController.removeDisplayListener(onToolbarShow); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (v) { - isHoverTriggerHovering = true; - Future.delayed(widget.delayToShow, () { - if (isHoverTriggerHovering && !isHoverMenuShowing) { - showLinkHoverMenu(); - } - }); - }, - onExit: (v) { - isHoverTriggerHovering = false; - tryToDismissLinkHoverMenu(); - }, - child: buildHoverPopover( - buildEditPopover( - Container( - color: Colors.black.withAlpha(1), - width: size.width, - height: size.height, - ), - ), - ), - ); - } - - Widget buildHoverPopover(Widget child) { - return AppFlowyPopover( - controller: hoverMenuController, - direction: PopoverDirection.topWithLeftAligned, - offset: Offset(0, size.height), - onOpen: () { - keepEditorFocusNotifier.increase(); - isHoverMenuShowing = true; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - isHoverMenuShowing = false; - }, - margin: EdgeInsets.zero, - constraints: BoxConstraints( - maxWidth: max(320, size.width), - maxHeight: 48 + size.height, - ), - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - popupBuilder: (context) => LinkHoverMenu( - attribute: widget.attribute, - triggerSize: size, - onEnter: (_) { - isHoverMenuHovering = true; - }, - onExit: (_) { - isHoverMenuHovering = false; - tryToDismissLinkHoverMenu(); - }, - onConvertTo: (type) => convertLinkTo(editorState, selection, type), - onOpenLink: openLink, - onCopyLink: () => copyLink(context), - onEditLink: showLinkEditMenu, - onRemoveLink: () => removeLink(editorState, selection), - ), - child: child, - ); - } - - Widget buildEditPopover(Widget child) { - final href = attribute.href ?? '', - isPage = attribute.isPage, - title = editorState.getTextInSelection(selection).join(); - final currentViewId = context.read()?.documentId ?? ''; - return AppFlowyPopover( - controller: editMenuController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, 0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - margin: EdgeInsets.zero, - asBarrier: true, - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - constraints: BoxConstraints( - maxWidth: 400, - minHeight: 282, - ), - popupBuilder: (context) => LinkEditMenu( - currentViewId: currentViewId, - linkInfo: LinkInfo(name: title, link: href, isPage: isPage), - onDismiss: () => editMenuController.close(), - onApply: (info) async { - final transaction = editorState.transaction; - transaction.replaceText( - widget.node, - selection.startIndex, - selection.length, - info.name, - attributes: info.toAttribute(), - ); - editMenuController.close(); - await editorState.apply(transaction); - }, - onRemoveLink: (linkinfo) => - onRemoveAndReplaceLink(editorState, selection, linkinfo.name), - ), - child: child, - ); - } - - void onToolbarShow() => hoverMenuController.close(); - - void showLinkHoverMenu() { - if (isHoverMenuShowing || toolbarController.isToolbarShowing || !mounted) { - return; - } - keepEditorFocusNotifier.increase(); - hoverMenuController.show(); - } - - void showLinkEditMenu() { - keepEditorFocusNotifier.increase(); - hoverMenuController.close(); - editMenuController.show(); - } - - void tryToDismissLinkHoverMenu() { - Future.delayed(widget.delayToHide, () { - if (isHoverMenuHovering || isHoverTriggerHovering) { - return; - } - hoverMenuController.close(); - }); - } - - Future openLink() async { - final href = widget.attribute.href ?? '', isPage = widget.attribute.isPage; - - if (isPage) { - final viewId = href.split('/').lastOrNull ?? ''; - if (viewId.isEmpty) { - await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); - } else { - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(viewId); - if (view != null) { - await handleMentionBlockTap(context, widget.editorState, view); - } - } - } else { - await afLaunchUrlString(href, addingHttpSchemeWhenFailed: true); - } - } - - Future copyLink(BuildContext context) async { - final href = widget.attribute.href ?? ''; - await context.copyLink(href); - hoverMenuController.close(); - } - - void removeLink( - EditorState editorState, - Selection selection, - ) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..formatText( - node, - index, - length, - { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); - } - - Future convertLinkTo( - EditorState editorState, - Selection selection, - LinkConvertMenuCommand type, - ) async { - final url = widget.attribute.href ?? ''; - if (type == LinkConvertMenuCommand.toBookmark) { - await convertUrlToLinkPreview(editorState, selection, url); - } else if (type == LinkConvertMenuCommand.toMention) { - await convertUrlToMention(editorState, selection); - } else if (type == LinkConvertMenuCommand.toEmbed) { - await convertUrlToLinkPreview( - editorState, - selection, - url, - previewType: LinkEmbedKeys.embed, - ); - } - } - - void onRemoveAndReplaceLink( - EditorState editorState, - Selection selection, - String text, - ) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..replaceText( - node, - index, - length, - text, - attributes: { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); - } -} - -class LinkHoverMenu extends StatefulWidget { - const LinkHoverMenu({ - super.key, - required this.attribute, - required this.onEnter, - required this.onExit, - required this.triggerSize, - required this.onCopyLink, - required this.onOpenLink, - required this.onEditLink, - required this.onRemoveLink, - required this.onConvertTo, - }); - - final Attributes attribute; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final Size triggerSize; - final VoidCallback onCopyLink; - final VoidCallback onOpenLink; - final VoidCallback onEditLink; - final VoidCallback onRemoveLink; - final ValueChanged onConvertTo; - - @override - State createState() => _LinkHoverMenuState(); -} - -class _LinkHoverMenuState extends State { - ViewPB? currentView; - late bool isPage = widget.attribute.isPage; - late String href = widget.attribute.href ?? ''; - final popoverController = PopoverController(); - bool isConvertButtonSelected = false; - - @override - void initState() { - super.initState(); - if (isPage) getPageView(); - } - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: SizedBox( - width: max(320, widget.triggerSize.width), - height: 48, - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: 320, - height: 48, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.fromLTRB(12, 8, 8, 8), - child: Row( - children: [ - Expanded(child: buildLinkWidget()), - Container( - height: 20, - width: 1, - color: Color(0xffE8ECF3) - .withAlpha(Theme.of(context).isLightMode ? 255 : 40), - margin: EdgeInsets.symmetric(horizontal: 6), - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_m), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onCopyLink, - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_edit_m), - tooltipText: LocaleKeys.editor_editLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onEditLink, - ), - buildConvertButton(), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_unlink_m), - tooltipText: LocaleKeys.editor_removeLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onRemoveLink, - ), - ], - ), - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - onTap: widget.onOpenLink, - child: Container( - width: widget.triggerSize.width, - height: widget.triggerSize.height, - color: Colors.black.withAlpha(1), - ), - ), - ), - ], - ); - } - - Future getPageView() async { - final viewId = href.split('/').lastOrNull ?? ''; - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(viewId); - if (mounted) { - setState(() { - currentView = view; - }); - } - } - - Widget buildLinkWidget() { - final view = currentView; - if (isPage && view == null) { - return SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ); - } - String text = ''; - if (isPage && view != null) { - text = view.name; - if (text.isEmpty) { - text = LocaleKeys.document_title_placeholder.tr(); - } - } else { - text = href; - } - return FlowyTooltip( - message: text, - preferBelow: false, - child: FlowyText.regular( - text, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ); - } - - Widget buildConvertButton() { - return AppFlowyPopover( - offset: Offset(44, 10.0), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg(FlowySvgs.turninto_m), - isSelected: isConvertButtonSelected, - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: () { - setState(() { - isConvertButtonSelected = true; - }); - showConvertMenu(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(LinkConvertMenuCommand.values.length, (index) { - final command = LinkConvertMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () { - widget.onConvertTo(command); - closeConvertMenu(); - }, - ), - ); - }), - ), - ), - ); - } - - void showConvertMenu() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void closeConvertMenu() { - popoverController.close(); - } -} - -class HoverTriggerKey { - HoverTriggerKey(this.nodeId, this.selection); - - final String nodeId; - final Selection selection; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HoverTriggerKey && - runtimeType == other.runtimeType && - nodeId == other.nodeId && - isSelectionSame(other.selection); - - bool isSelectionSame(Selection other) => - (selection.start == other.start && selection.end == other.end) || - (selection.start == other.end && selection.end == other.start); - - @override - int get hashCode => nodeId.hashCode ^ selection.hashCode; -} - -class LinkHoverTriggers { - final Map> _map = {}; - - void _add(HoverTriggerKey key, VoidCallback callback) { - final callbacks = _map[key] ?? {}; - callbacks.add(callback); - _map[key] = callbacks; - } - - void _remove(HoverTriggerKey key, VoidCallback callback) { - final callbacks = _map[key] ?? {}; - callbacks.remove(callback); - _map[key] = callbacks; - } - - void call(HoverTriggerKey key) { - final callbacks = _map[key] ?? {}; - if (callbacks.isEmpty) return; - callbacks.first.call(); - } -} - -enum LinkConvertMenuCommand { - toMention, - toBookmark, - toEmbed; - - String get title { - switch (this) { - case toMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - } - } - - String get type { - switch (this) { - case toMention: - return MentionBlockKeys.type; - case toBookmark: - return LinkPreviewBlockKeys.type; - case toEmbed: - return LinkPreviewBlockKeys.type; - } - } -} - -extension LinkExtension on BuildContext { - Future copyLink(String link) async { - if (link.isEmpty) return; - await getIt() - .setData(ClipboardServiceData(plainText: link)); - if (mounted) { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkSuccess.tr(), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart deleted file mode 100644 index d08442d779..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.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'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.dart'; -import 'package:flutter/services.dart'; - -import 'link_create_menu.dart'; -import 'link_styles.dart'; - -void showReplaceMenu({ - required BuildContext context, - required EditorState editorState, - required Node node, - String? url, - required LTRB ltrb, - required ValueChanged onReplace, -}) { - OverlayEntry? overlay; - - void dismissOverlay() { - keepEditorFocusNotifier.decrease(); - overlay?.remove(); - overlay = null; - } - - keepEditorFocusNotifier.increase(); - overlay = FullScreenOverlayEntry( - top: ltrb.top, - bottom: ltrb.bottom, - left: ltrb.left, - right: ltrb.right, - dismissCallback: () => keepEditorFocusNotifier.decrease(), - builder: (context) { - return LinkReplaceMenu( - link: url ?? '', - onSubmitted: (link) async { - onReplace.call(link); - dismissOverlay(); - }, - onDismiss: dismissOverlay, - ); - }, - ).build(); - - Overlay.of(context, rootOverlay: true).insert(overlay!); -} - -class LinkReplaceMenu extends StatefulWidget { - const LinkReplaceMenu({ - super.key, - required this.onSubmitted, - required this.link, - required this.onDismiss, - }); - - final ValueChanged onSubmitted; - final VoidCallback onDismiss; - final String link; - - @override - State createState() => _LinkReplaceMenuState(); -} - -class _LinkReplaceMenuState extends State { - bool showErrorText = false; - late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); - late TextEditingController textEditingController = - TextEditingController(text: widget.link); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); - }); - } - - @override - void dispose() { - focusNode.dispose(); - textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - width: 330, - padding: EdgeInsets.all(8), - decoration: buildToolbarLinkDecoration(context), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: buildLinkField()), - HSpace(8), - buildReplaceButton(), - ], - ), - ); - } - - Widget buildLinkField() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 32, - child: TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - autofocus: true, - focusNode: focusNode, - textAlign: TextAlign.left, - controller: textEditingController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_pasteHint - .tr(), - context, - showErrorBorder: showErrorText, - ), - ), - ), - if (showErrorText) - Padding( - padding: const EdgeInsets.only(top: 4), - child: FlowyText.regular( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: LinkStyle.textStatusError, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - ], - ); - } - - Widget buildReplaceButton() { - return FlowyTextButton( - LocaleKeys.button_replace.tr(), - padding: EdgeInsets.zero, - mainAxisAlignment: MainAxisAlignment.center, - constraints: BoxConstraints(maxWidth: 78, minHeight: 32), - fontSize: 14, - lineHeight: 20 / 14, - hoverColor: LinkStyle.fillThemeThick.withAlpha(200), - fontColor: Colors.white, - fillColor: LinkStyle.fillThemeThick, - fontWeight: FontWeight.w400, - onPressed: onSubmit, - ); - } - - void onSubmit() { - final link = textEditingController.text.trim(); - if (link.isEmpty || !isUri(link)) { - setState(() { - showErrorText = true; - }); - return; - } - widget.onSubmitted.call(link); - } - - KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - if (key.logicalKey == LogicalKeyboardKey.escape) { - widget.onDismiss.call(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.enter) { - onSubmit(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart deleted file mode 100644 index 97fd6abdad..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_search_text_field.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/list_extension.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/util/link_util.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/services.dart'; - -import 'link_create_menu.dart'; -import 'link_styles.dart'; - -class LinkSearchTextField { - LinkSearchTextField({ - this.onEscape, - this.onEnter, - this.onDataRefresh, - this.initialViewId = '', - required this.currentViewId, - String? initialSearchText, - }) : textEditingController = TextEditingController( - text: isUri(initialSearchText ?? '') ? initialSearchText : '', - ); - - final TextEditingController textEditingController; - final String initialViewId; - final String currentViewId; - final ItemScrollController searchController = ItemScrollController(); - late FocusNode focusNode = FocusNode(onKeyEvent: onKeyEvent); - final List searchedViews = []; - final List recentViews = []; - int selectedIndex = 0; - - final VoidCallback? onEscape; - final VoidCallback? onEnter; - final VoidCallback? onDataRefresh; - - String get searchText => textEditingController.text; - - bool get isTextfieldEnable => searchText.isNotEmpty && isUri(searchText); - - bool get showingRecent => searchText.isEmpty && recentViews.isNotEmpty; - - ViewPB get currentSearchedView => searchedViews[selectedIndex]; - - ViewPB get currentRecentView => recentViews[selectedIndex]; - - void dispose() { - textEditingController.dispose(); - focusNode.dispose(); - searchedViews.clear(); - recentViews.clear(); - } - - Widget buildTextField({ - bool autofocus = false, - bool showError = false, - required BuildContext context, - }) { - return TextFormField( - autovalidateMode: AutovalidateMode.onUserInteraction, - autofocus: autofocus, - focusNode: focusNode, - textAlign: TextAlign.left, - controller: textEditingController, - style: TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - onChanged: (text) { - if (text.isEmpty) { - searchedViews.clear(); - selectedIndex = 0; - onDataRefresh?.call(); - } else { - searchViews(text); - } - }, - decoration: LinkStyle.buildLinkTextFieldInputDecoration( - LocaleKeys.document_toolbar_linkInputHint.tr(), - context, - showErrorBorder: showError, - ), - ); - } - - Widget buildResultContainer({ - EdgeInsetsGeometry? margin, - required BuildContext context, - VoidCallback? onLinkSelected, - ValueChanged? onPageLinkSelected, - double width = 320.0, - }) { - return onSearchResult( - onEmpty: () => SizedBox.shrink(), - onLink: () => Container( - height: 48, - width: width, - padding: EdgeInsets.all(8), - margin: margin, - decoration: buildToolbarLinkDecoration(context), - child: FlowyButton( - leftIcon: FlowySvg(FlowySvgs.toolbar_link_earth_m), - isSelected: true, - text: FlowyText.regular( - searchText, - overflow: TextOverflow.ellipsis, - fontSize: 14, - figmaLineHeight: 20, - ), - onTap: onLinkSelected, - ), - ), - onRecentViews: () => Container( - width: width, - height: recentViews.length.clamp(1, 5) * 32.0 + 48, - margin: margin, - padding: EdgeInsets.all(8), - decoration: buildToolbarLinkDecoration(context), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 32, - padding: EdgeInsets.all(8), - child: FlowyText.semibold( - LocaleKeys.inlineActions_recentPages.tr(), - color: LinkStyle.textTertiary, - fontSize: 12, - figmaLineHeight: 16, - ), - ), - Flexible( - child: ListView.builder( - itemBuilder: (context, index) { - final currentView = recentViews[index]; - return buildPageItem( - currentView, - index == selectedIndex, - onPageLinkSelected, - ); - }, - itemCount: recentViews.length, - ), - ), - ], - ), - ), - onSearchViews: () => Container( - width: width, - height: searchedViews.length.clamp(1, 5) * 32.0 + 16, - margin: margin, - decoration: buildToolbarLinkDecoration(context), - child: ScrollablePositionedList.builder( - padding: EdgeInsets.all(8), - physics: const ClampingScrollPhysics(), - shrinkWrap: true, - itemCount: searchedViews.length, - itemScrollController: searchController, - initialScrollIndex: max(0, selectedIndex), - itemBuilder: (context, index) { - final currentView = searchedViews[index]; - return buildPageItem( - currentView, - index == selectedIndex, - onPageLinkSelected, - ); - }, - ), - ), - ); - } - - Widget buildPageItem( - ViewPB view, - bool isSelected, - ValueChanged? onSubmittedPageLink, - ) { - final viewName = view.name; - final displayName = viewName.isEmpty - ? LocaleKeys.document_title_placeholder.tr() - : viewName; - final isCurrent = initialViewId == view.id; - return SizedBox( - height: 32, - child: FlowyButton( - isSelected: isSelected, - leftIcon: buildIcon(view, padding: EdgeInsets.zero), - text: FlowyText.regular( - displayName, - overflow: TextOverflow.ellipsis, - fontSize: 14, - figmaLineHeight: 20, - ), - rightIcon: isCurrent ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () => onSubmittedPageLink?.call(view), - ), - ); - } - - Widget buildIcon( - ViewPB view, { - EdgeInsetsGeometry padding = const EdgeInsets.only(top: 4), - }) { - if (view.icon.value.isEmpty) return view.defaultIcon(size: Size(20, 20)); - final iconData = view.icon.toEmojiIconData(); - return Padding( - padding: padding, - child: RawEmojiIconWidget( - emoji: iconData, - emojiSize: iconData.type == FlowyIconType.emoji ? 16 : 20, - lineHeight: 1, - ), - ); - } - - void requestFocus() => focusNode.requestFocus(); - - void unfocus() => focusNode.unfocus(); - - void updateText(String text) => textEditingController.text = text; - - T onSearchResult({ - required ValueGetter onLink, - required ValueGetter onRecentViews, - required ValueGetter onSearchViews, - required ValueGetter onEmpty, - }) { - if (searchedViews.isEmpty && recentViews.isEmpty && searchText.isEmpty) { - return onEmpty.call(); - } - if (searchedViews.isEmpty && searchText.isNotEmpty) { - return onLink.call(); - } - if (searchedViews.isEmpty) return onRecentViews.call(); - return onSearchViews.call(); - } - - KeyEventResult onKeyEvent(FocusNode node, KeyEvent key) { - if (key is! KeyDownEvent) return KeyEventResult.ignored; - int index = selectedIndex; - if (key.logicalKey == LogicalKeyboardKey.escape) { - onEscape?.call(); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.arrowUp) { - index = onSearchResult( - onLink: () => 0, - onRecentViews: () { - int result = index - 1; - if (result < 0) result = recentViews.length - 1; - return result; - }, - onSearchViews: () { - int result = index - 1; - if (result < 0) result = searchedViews.length - 1; - searchController.scrollTo( - index: result, - alignment: 0.5, - duration: const Duration(milliseconds: 300), - ); - return result; - }, - onEmpty: () => 0, - ); - refreshIndex(index); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.arrowDown) { - index = onSearchResult( - onLink: () => 0, - onRecentViews: () { - int result = index + 1; - if (result >= recentViews.length) result = 0; - return result; - }, - onSearchViews: () { - int result = index + 1; - if (result >= searchedViews.length) result = 0; - searchController.scrollTo( - index: result, - alignment: 0.5, - duration: const Duration(milliseconds: 300), - ); - return result; - }, - onEmpty: () => 0, - ); - refreshIndex(index); - return KeyEventResult.handled; - } else if (key.logicalKey == LogicalKeyboardKey.enter) { - onEnter?.call(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - Future searchRecentViews() async { - final recentService = getIt(); - final sectionViews = await recentService.recentViews(); - final views = sectionViews - .unique((e) => e.item.id) - .map((e) => e.item) - .where((e) => e.id != currentViewId) - .take(5) - .toList(); - recentViews.clear(); - recentViews.addAll(views); - selectedIndex = 0; - onDataRefresh?.call(); - } - - Future searchViews(String search) async { - final viewResult = await ViewBackendService.getAllViews(); - final allViews = viewResult - .toNullable() - ?.items - .where( - (view) => - (view.id != currentViewId) && - (view.name.toLowerCase().contains(search.toLowerCase()) || - (view.name.isEmpty && search.isEmpty) || - (view.name.isEmpty && - LocaleKeys.menuAppHeader_defaultNewPageName - .tr() - .toLowerCase() - .contains(search.toLowerCase()))), - ) - .take(10) - .toList(); - searchedViews.clear(); - searchedViews.addAll(allViews ?? []); - selectedIndex = 0; - onDataRefresh?.call(); - } - - void refreshIndex(int index) { - selectedIndex = index; - onDataRefresh?.call(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart deleted file mode 100644 index cabc00a312..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -class LinkStyle { - static const textTertiary = Color(0xFF99A1A8); - static const textStatusError = Color(0xffE71D32); - static const fillThemeThick = Color(0xFF00B5FF); - static const shadowMedium = Color(0x1F22251F); - static const textPrimary = Color(0xFF1F2329); - - static Color borderColor(BuildContext context) => - Theme.of(context).isLightMode ? Color(0xFFE8ECF3) : Color(0x64BDBDBD); - - static InputDecoration buildLinkTextFieldInputDecoration( - String hintText, - BuildContext context, { - bool showErrorBorder = false, - }) { - final border = OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)), - borderSide: BorderSide(color: borderColor(context)), - ); - final enableBorder = border.copyWith( - borderSide: BorderSide( - color: showErrorBorder - ? LinkStyle.textStatusError - : LinkStyle.fillThemeThick, - ), - ); - const hintStyle = TextStyle( - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - color: LinkStyle.textTertiary, - ); - return InputDecoration( - hintText: hintText, - hintStyle: hintStyle, - contentPadding: const EdgeInsets.fromLTRB(8, 6, 8, 6), - isDense: true, - border: border, - enabledBorder: border, - focusedBorder: enableBorder, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart deleted file mode 100644 index 7598a2b657..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/toolbar_animation.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:flutter/material.dart'; - -class ToolbarAnimationWidget extends StatefulWidget { - const ToolbarAnimationWidget({ - super.key, - required this.child, - this.duration = const Duration(milliseconds: 150), - this.beginOpacity = 0.0, - this.endOpacity = 1.0, - this.beginScaleFactor = 0.95, - this.endScaleFactor = 1.0, - }); - - final Widget child; - final Duration duration; - final double beginScaleFactor; - final double endScaleFactor; - final double beginOpacity; - final double endOpacity; - - @override - State createState() => _ToolbarAnimationWidgetState(); -} - -class _ToolbarAnimationWidgetState extends State - with SingleTickerProviderStateMixin { - late AnimationController controller; - late Animation fadeAnimation; - late Animation scaleAnimation; - - @override - void initState() { - super.initState(); - controller = AnimationController( - vsync: this, - duration: widget.duration, - ); - fadeAnimation = _buildFadeAnimation(); - scaleAnimation = _buildScaleAnimation(); - controller.forward(); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: controller, - builder: (_, child) => Opacity( - opacity: fadeAnimation.value, - child: Transform.scale( - scale: scaleAnimation.value, - child: child, - ), - ), - child: widget.child, - ); - } - - Animation _buildFadeAnimation() { - return Tween( - begin: widget.beginOpacity, - end: widget.endOpacity, - ).animate( - CurvedAnimation( - parent: controller, - curve: Curves.easeInOut, - ), - ); - } - - Animation _buildScaleAnimation() { - return Tween( - begin: widget.beginScaleFactor, - end: widget.endScaleFactor, - ).animate( - CurvedAnimation( - parent: controller, - curve: Curves.easeInOut, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 6c09ca6a28..ba6f01e908 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -1,17 +1,15 @@ import 'dart:convert'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.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:universal_platform/universal_platform.dart'; class ErrorBlockComponentBuilder extends BlockComponentBuilder { ErrorBlockComponentBuilder({ @@ -30,15 +28,11 @@ class ErrorBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (_) => true; + bool validate(Node node) => true; } class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { @@ -47,7 +41,6 @@ class ErrorBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -66,15 +59,25 @@ class _ErrorBlockComponentWidgetState extends State @override Widget build(BuildContext context) { - Widget child = Container( - width: double.infinity, + Widget child = DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), - child: UniversalPlatform.isDesktopOrWeb - ? _buildDesktopErrorBlock(context) - : _buildMobileErrorBlock(context), + child: FlowyButton( + onTap: () async { + showSnackBarMessage( + context, + LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), + ); + await getIt().setData( + ClipboardServiceData(plainText: jsonEncode(node.toJson())), + ); + }, + text: PlatformExtension.isDesktopOrWeb + ? _buildDesktopErrorBlock(context) + : _buildMobileErrorBlock(context), + ), ); child = Padding( @@ -86,12 +89,11 @@ class _ErrorBlockComponentWidgetState extends State child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { child = MobileBlockActionButtons( node: node, editorState: context.read(), @@ -105,60 +107,40 @@ class _ErrorBlockComponentWidgetState extends State Widget _buildDesktopErrorBlock(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Row( + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, children: [ - const HSpace(12), + const HSpace(4), FlowyText.regular( - LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]), + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), ), - const Spacer(), - OutlinedRoundedButton( - text: LocaleKeys.document_errorBlock_copyBlockContent.tr(), - onTap: _copyBlockContent, + const HSpace(4), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, ), - const HSpace(12), ], ), ); } Widget _buildMobileErrorBlock(BuildContext context) { - return AnimatedGestureDetector( - onTapUp: _copyBlockContent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 4.0, right: 24.0), - child: FlowyText.regular( - LocaleKeys.document_errorBlock_parseError.tr(args: [node.type]), - maxLines: 3, - ), - ), - const VSpace(6), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: FlowyText.regular( - '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', - color: Theme.of(context).hintColor, - fontSize: 12.0, - ), - ), - ], - ), + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + const VSpace(6), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + fontSize: 12.0, + ), + ], ), ); } - - void _copyBlockContent() { - showToastNotification( - message: LocaleKeys.document_errorBlock_blockContentHasBeenCopied.tr(), - ); - - getIt().setData( - ClipboardServiceData(plainText: jsonEncode(node.toJson())), - ); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block.dart deleted file mode 100644 index 31ead6370c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block.dart +++ /dev/null @@ -1,2 +0,0 @@ -export './file_block_component.dart'; -export './file_selection_menu.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart deleted file mode 100644 index fe50224caa..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_component.dart +++ /dev/null @@ -1,641 +0,0 @@ -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/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'file_block_menu.dart'; -import 'file_upload_menu.dart'; - -class FileBlockKeys { - const FileBlockKeys._(); - - static const String type = 'file'; - - /// The src of the file. - /// - /// The value is a String. - /// It can be a url for a network file or a local file path. - /// - static const String url = 'url'; - - /// The name of the file. - /// - /// The value is a String. - /// - static const String name = 'name'; - - /// The type of the url. - /// - /// The value is a FileUrlType enum. - /// - static const String urlType = 'url_type'; - - /// The date of the file upload. - /// - /// The value is a timestamp in ms. - /// - static const String uploadedAt = 'uploaded_at'; - - /// The user who uploaded the file. - /// - /// The value is a String, in form of user id. - /// - static const String uploadedBy = 'uploaded_by'; - - /// The GlobalKey of the FileBlockComponentState. - /// - /// **Note: This value is used in extraInfos of the Node, not in the attributes.** - static const String globalKey = 'global_key'; -} - -enum FileUrlType { - local, - network, - cloud; - - static FileUrlType fromIntValue(int value) { - switch (value) { - case 0: - return FileUrlType.local; - case 1: - return FileUrlType.network; - case 2: - return FileUrlType.cloud; - default: - throw UnimplementedError(); - } - } - - int toIntValue() { - switch (this) { - case FileUrlType.local: - return 0; - case FileUrlType.network: - return 1; - case FileUrlType.cloud: - return 2; - } - } - - FileUploadTypePB toFileUploadTypePB() { - switch (this) { - case FileUrlType.local: - return FileUploadTypePB.LocalFile; - case FileUrlType.network: - return FileUploadTypePB.NetworkFile; - case FileUrlType.cloud: - return FileUploadTypePB.CloudFile; - } - } -} - -Node fileNode({ - required String url, - FileUrlType type = FileUrlType.local, - String? name, -}) { - return Node( - type: FileBlockKeys.type, - attributes: { - FileBlockKeys.url: url, - FileBlockKeys.urlType: type.toIntValue(), - FileBlockKeys.name: name, - FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, - }, - ); -} - -class FileBlockComponentBuilder extends BlockComponentBuilder { - FileBlockComponentBuilder({super.configuration}); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - final extraInfos = node.extraInfos; - final key = extraInfos?[FileBlockKeys.globalKey] as GlobalKey?; - - return FileBlockComponent( - key: key ?? node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; -} - -class FileBlockComponent extends BlockComponentStatefulWidget { - const FileBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => FileBlockComponentState(); -} - -class FileBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - late EditorDropManagerState? dropManagerState = UniversalPlatform.isMobile - ? null - : context.read(); - - final fileKey = GlobalKey(); - final showActionsNotifier = ValueNotifier(false); - final controller = PopoverController(); - final menuController = PopoverController(); - - late final editorState = Provider.of(context, listen: false); - - bool alwaysShowMenu = false; - bool isDragging = false; - bool isHovering = false; - - @override - void didChangeDependencies() { - if (!UniversalPlatform.isMobile) { - dropManagerState = context.read(); - } - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - final url = node.attributes[FileBlockKeys.url]; - final FileUrlType urlType = - FileUrlType.fromIntValue(node.attributes[FileBlockKeys.urlType] ?? 0); - - Widget child = MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) { - setState(() => isHovering = true); - showActionsNotifier.value = true; - }, - onExit: (_) { - setState(() => isHovering = false); - if (!alwaysShowMenu) { - showActionsNotifier.value = false; - } - }, - opaque: false, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: url != null && url.isNotEmpty - ? () async => _openFile(context, urlType, url) - : _openMenu, - child: DecoratedBox( - decoration: BoxDecoration( - color: isHovering - ? Theme.of(context).colorScheme.secondary - : Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - border: isDragging - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ) - : null, - ), - child: SizedBox( - height: 52, - child: Row( - children: [ - const HSpace(10), - FlowySvg( - FlowySvgs.slash_menu_icon_file_s, - color: Theme.of(context).hintColor, - size: const Size.square(24), - ), - const HSpace(10), - ..._buildTrailing(context), - ], - ), - ), - ), - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - if (url == null || url.isEmpty) { - child = DropTarget( - onDragEntered: (_) { - if (dropManagerState?.isDropEnabled == true) { - setState(() => isDragging = true); - } - }, - onDragExited: (_) { - if (dropManagerState?.isDropEnabled == true) { - setState(() => isDragging = false); - } - }, - onDragDone: (details) { - if (dropManagerState?.isDropEnabled == true) { - insertFileFromLocal(details.files); - } - }, - child: AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 480, - maxHeight: 340, - minHeight: 80, - ), - clickHandler: PopoverClickHandler.gestureDetector, - onOpen: () => dropManagerState?.add(FileBlockKeys.type), - onClose: () => dropManagerState?.remove(FileBlockKeys.type), - popupBuilder: (_) => FileUploadMenu( - onInsertLocalFile: insertFileFromLocal, - onInsertNetworkFile: insertNetworkFile, - ), - child: child, - ), - ); - } - - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - blockColor: editorState.editorStyle.selectionColor, - supportTypes: const [BlockSelectionType.block], - child: Padding( - key: fileKey, - padding: padding, - child: child, - ), - ); - } else { - return Padding( - key: fileKey, - padding: padding, - child: MobileBlockActionButtons( - node: widget.node, - editorState: editorState, - child: child, - ), - ); - } - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - if (!UniversalPlatform.isDesktopOrWeb) { - // show a fixed menu on mobile - child = MobileBlockActionButtons( - node: node, - editorState: editorState, - extendActionWidgets: _buildExtendActionWidgets(context), - child: child, - ); - } - - return child; - } - - Future _openFile( - BuildContext context, - FileUrlType urlType, - String url, - ) async { - await afLaunchUrlString(url, context: context); - } - - void _openMenu() { - if (UniversalPlatform.isDesktopOrWeb) { - controller.show(); - dropManagerState?.add(FileBlockKeys.type); - } else { - editorState.updateSelectionWithReason(null, extraInfo: {}); - showUploadFileMobileMenu(); - } - } - - List _buildTrailing(BuildContext context) { - if (node.attributes[FileBlockKeys.url]?.isNotEmpty == true) { - final name = node.attributes[FileBlockKeys.name] as String; - return [ - Expanded( - child: FlowyText( - name, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(8), - if (UniversalPlatform.isDesktopOrWeb) ...[ - ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (_, value, __) { - final url = node.attributes[FileBlockKeys.url]; - if (!value || url == null || url.isEmpty) { - return const SizedBox.shrink(); - } - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: menuController.show, - child: AppFlowyPopover( - controller: menuController, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithRightAligned, - onClose: () { - setState( - () { - alwaysShowMenu = false; - showActionsNotifier.value = false; - }, - ); - }, - popupBuilder: (_) { - alwaysShowMenu = true; - return FileBlockMenu( - controller: menuController, - node: node, - editorState: editorState, - ); - }, - child: const FileMenuTrigger(), - ), - ); - }, - ), - const HSpace(8), - ], - if (UniversalPlatform.isMobile) ...[ - const HSpace(36), - ], - ]; - } else { - return [ - Flexible( - child: FlowyText( - isDragging - ? LocaleKeys.document_plugins_file_placeholderDragging.tr() - : LocaleKeys.document_plugins_file_placeholderText.tr(), - overflow: TextOverflow.ellipsis, - color: Theme.of(context).hintColor, - ), - ), - ]; - } - } - - // only used on mobile platform - List _buildExtendActionWidgets(BuildContext context) { - final String? url = widget.node.attributes[FileBlockKeys.url]; - if (url == null || url.isEmpty) { - return []; - } - - final urlType = FileUrlType.fromIntValue( - widget.node.attributes[FileBlockKeys.urlType] ?? 0, - ); - - if (urlType != FileUrlType.network) { - return []; - } - - return [ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.editor_copyLink.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_field_copy_s, - ), - onTap: () async { - context.pop(); - showSnackBarMessage( - context, - LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), - ); - await getIt().setPlainText(url); - }, - ), - ]; - } - - void showUploadFileMobileMenu() { - showMobileBottomSheet( - context, - title: LocaleKeys.document_plugins_file_name.tr(), - showHeader: true, - showCloseButton: true, - showDragHandle: true, - builder: (context) { - return Container( - margin: const EdgeInsets.only(top: 12.0), - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: FileUploadMenu( - onInsertLocalFile: (file) async { - context.pop(); - await insertFileFromLocal(file); - }, - onInsertNetworkFile: (url) async { - context.pop(); - await insertNetworkFile(url); - }, - ), - ); - }, - ); - } - - Future insertFileFromLocal(List files) async { - if (files.isEmpty) return; - - final file = files.first; - final path = file.path; - final documentBloc = context.read(); - final isLocalMode = documentBloc.isLocalMode; - final urlType = isLocalMode ? FileUrlType.local : FileUrlType.cloud; - - String? url; - String? errorMsg; - if (isLocalMode) { - url = await saveFileToLocalStorage(path); - } else { - final result = - await saveFileToCloudStorage(path, documentBloc.documentId); - url = result.$1; - errorMsg = result.$2; - } - - if (errorMsg != null && mounted) { - return showSnackBarMessage(context, errorMsg); - } - - // Remove the file block from the drop state manager - dropManagerState?.remove(FileBlockKeys.type); - - final transaction = editorState.transaction; - transaction.updateNode(widget.node, { - FileBlockKeys.url: url, - FileBlockKeys.urlType: urlType.toIntValue(), - FileBlockKeys.name: file.name, - FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, - }); - await editorState.apply(transaction); - } - - Future insertNetworkFile(String url) async { - if (url.isEmpty || !isURL(url)) { - // show error - return showSnackBarMessage( - context, - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - ); - } - - // Remove the file block from the drop state manager - dropManagerState?.remove(FileBlockKeys.type); - - final uri = Uri.tryParse(url); - if (uri == null) { - return; - } - - String name = uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ""; - if (name.isEmpty && uri.pathSegments.length > 1) { - name = uri.pathSegments[uri.pathSegments.length - 2]; - } else if (name.isEmpty) { - name = uri.host; - } - - final transaction = editorState.transaction; - transaction.updateNode(widget.node, { - FileBlockKeys.url: url, - FileBlockKeys.urlType: FileUrlType.network.toIntValue(), - FileBlockKeys.name: name, - FileBlockKeys.uploadedAt: DateTime.now().millisecondsSinceEpoch, - }); - await editorState.apply(transaction); - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({bool shiftWithBaseOffset = false}) { - final renderBox = fileKey.currentContext?.findRenderObject(); - if (renderBox is RenderBox) { - return Offset.zero & renderBox.size; - } - return Rect.zero; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection(Selection.collapsed(position)); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final renderBox = fileKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && renderBox is RenderBox) { - return [ - renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & - renderBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: widget.node.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal( - Offset offset, { - bool shiftWithBaseOffset = false, - }) => - _renderBox!.localToGlobal(offset); -} - -@visibleForTesting -class FileMenuTrigger extends StatelessWidget { - const FileMenuTrigger({super.key}); - - @override - Widget build(BuildContext context) { - return const FlowyHover( - resetHoverOnRebuild: false, - child: Padding( - padding: EdgeInsets.all(4), - child: FlowySvg( - FlowySvgs.three_dots_s, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart deleted file mode 100644 index 99529b3b8e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_block_menu.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/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'; - -class FileBlockMenu extends StatefulWidget { - const FileBlockMenu({ - super.key, - required this.controller, - required this.node, - required this.editorState, - }); - - final PopoverController controller; - final Node node; - final EditorState editorState; - - @override - State createState() => _FileBlockMenuState(); -} - -class _FileBlockMenuState extends State { - final nameController = TextEditingController(); - final errorMessage = ValueNotifier(null); - BuildContext? renameContext; - - @override - void initState() { - super.initState(); - nameController.text = widget.node.attributes[FileBlockKeys.name] ?? ''; - nameController.selection = TextSelection( - baseOffset: 0, - extentOffset: nameController.text.length, - ); - } - - @override - void dispose() { - errorMessage.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final uploadedAtInMS = - widget.node.attributes[FileBlockKeys.uploadedAt] as int?; - final uploadedAt = uploadedAtInMS != null - ? DateTime.fromMillisecondsSinceEpoch(uploadedAtInMS) - : null; - final dateFormat = context.read().state.dateFormat; - final urlType = - FileUrlType.fromIntValue(widget.node.attributes[FileBlockKeys.urlType]); - final fileUploadType = urlType.toFileUploadTypePB(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - HoverButton( - itemHeight: 20, - leftIcon: const FlowySvg(FlowySvgs.download_s), - name: LocaleKeys.button_download.tr(), - onTap: () { - final userProfile = widget.editorState.document.root.context - ?.read() - .state - .userProfilePB; - final url = widget.node.attributes[FileBlockKeys.url]; - final name = widget.node.attributes[FileBlockKeys.name]; - if (url != null && name != null) { - final filePB = MediaFilePB( - url: url, - name: name, - uploadType: fileUploadType, - ); - downloadMediaFile( - context, - filePB, - userProfile: userProfile, - ); - } - }, - ), - const VSpace(4), - HoverButton( - itemHeight: 20, - leftIcon: const FlowySvg(FlowySvgs.edit_s), - name: LocaleKeys.document_plugins_file_renameFile_title.tr(), - onTap: () { - widget.controller.close(); - showCustomConfirmDialog( - context: context, - title: LocaleKeys.document_plugins_file_renameFile_title.tr(), - description: - LocaleKeys.document_plugins_file_renameFile_description.tr(), - closeOnConfirm: false, - builder: (context) { - renameContext = context; - return FileRenameTextField( - nameController: nameController, - errorMessage: errorMessage, - onSubmitted: _saveName, - ); - }, - confirmLabel: LocaleKeys.button_save.tr(), - onConfirm: _saveName, - ); - }, - ), - const VSpace(4), - HoverButton( - itemHeight: 20, - leftIcon: const FlowySvg(FlowySvgs.delete_s), - name: LocaleKeys.button_delete.tr(), - onTap: () { - final transaction = widget.editorState.transaction - ..deleteNode(widget.node); - widget.editorState.apply(transaction); - widget.controller.close(); - }, - ), - if (uploadedAt != null) ...[ - const Divider(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: FlowyText.regular( - [FileUrlType.cloud, FileUrlType.local].contains(urlType) - ? LocaleKeys.document_plugins_file_uploadedAt.tr( - args: [dateFormat.formatDate(uploadedAt, false)], - ) - : LocaleKeys.document_plugins_file_linkedAt.tr( - args: [dateFormat.formatDate(uploadedAt, false)], - ), - fontSize: 14, - maxLines: 2, - color: Theme.of(context).hintColor, - ), - ), - const VSpace(2), - ], - ], - ); - } - - void _saveName() { - if (nameController.text.isEmpty) { - errorMessage.value = - LocaleKeys.document_plugins_file_renameFile_nameEmptyError.tr(); - return; - } - - final attributes = widget.node.attributes; - attributes[FileBlockKeys.name] = nameController.text; - - final transaction = widget.editorState.transaction - ..updateNode(widget.node, attributes); - widget.editorState.apply(transaction); - - if (renameContext != null) { - Navigator.of(renameContext!).pop(); - } - } -} - -class FileRenameTextField extends StatefulWidget { - const FileRenameTextField({ - super.key, - required this.nameController, - required this.errorMessage, - required this.onSubmitted, - this.disposeController = true, - }); - - final TextEditingController nameController; - final ValueNotifier errorMessage; - final VoidCallback onSubmitted; - - final bool disposeController; - - @override - State createState() => _FileRenameTextFieldState(); -} - -class _FileRenameTextFieldState extends State { - @override - void initState() { - super.initState(); - widget.errorMessage.addListener(_setState); - } - - @override - void dispose() { - widget.errorMessage.removeListener(_setState); - if (widget.disposeController) { - widget.nameController.dispose(); - } - super.dispose(); - } - - void _setState() { - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyTextField( - controller: widget.nameController, - onSubmitted: (_) => widget.onSubmitted(), - ), - if (widget.errorMessage.value != null) - Padding( - padding: const EdgeInsets.only(top: 8), - child: FlowyText( - widget.errorMessage.value!, - color: Theme.of(context).colorScheme.error, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart deleted file mode 100644 index 132a7b8e7e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_selection_menu.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension InsertFile on EditorState { - Future insertEmptyFileBlock(GlobalKey key) async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final path = selection.end.path; - final node = getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final file = fileNode(url: '')..extraInfos = {'global_key': key}; - - final insertedPath = delta.isEmpty ? path : path.next; - final transaction = this.transaction - ..insertNode(insertedPath, file) - ..insertNode(insertedPath, paragraphNode()) - ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); - - return apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart deleted file mode 100644 index 4ef680d1b9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:dotted_border/dotted_border.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class FileUploadMenu extends StatefulWidget { - const FileUploadMenu({ - super.key, - required this.onInsertLocalFile, - required this.onInsertNetworkFile, - this.allowMultipleFiles = false, - }); - - final void Function(List files) onInsertLocalFile; - final void Function(String url) onInsertNetworkFile; - final bool allowMultipleFiles; - - @override - State createState() => _FileUploadMenuState(); -} - -class _FileUploadMenuState extends State { - int currentTab = 0; - - @override - Widget build(BuildContext context) { - // ClipRRect is used to clip the tab indicator, so the animation doesn't overflow the dialog - return ClipRRect( - child: DefaultTabController( - length: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - TabBar( - onTap: (value) => setState(() => currentTab = value), - isScrollable: true, - indicatorWeight: 3, - tabAlignment: TabAlignment.start, - indicatorSize: TabBarIndicatorSize.label, - labelPadding: EdgeInsets.zero, - padding: EdgeInsets.zero, - overlayColor: WidgetStatePropertyAll( - UniversalPlatform.isDesktop - ? Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - tabs: [ - _Tab( - title: LocaleKeys.document_plugins_file_uploadTab.tr(), - isSelected: currentTab == 0, - ), - _Tab( - title: LocaleKeys.document_plugins_file_networkTab.tr(), - isSelected: currentTab == 1, - ), - ], - ), - const Divider(height: 0), - if (currentTab == 0) ...[ - _FileUploadLocal( - allowMultipleFiles: widget.allowMultipleFiles, - onFilesPicked: (files) { - if (files.isNotEmpty) { - widget.onInsertLocalFile(files); - } - }, - ), - ] else ...[ - _FileUploadNetwork(onSubmit: widget.onInsertNetworkFile), - ], - ], - ), - ), - ); - } -} - -class _Tab extends StatelessWidget { - const _Tab({required this.title, this.isSelected = false}); - - final String title; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: 12.0, - right: 12.0, - bottom: 8.0, - top: UniversalPlatform.isMobile ? 0 : 8.0, - ), - child: FlowyText.semibold( - title, - color: isSelected - ? AFThemeExtension.of(context).strongText - : Theme.of(context).hintColor, - ), - ); - } -} - -class _FileUploadLocal extends StatefulWidget { - const _FileUploadLocal({ - required this.onFilesPicked, - this.allowMultipleFiles = false, - }); - - final void Function(List) onFilesPicked; - final bool allowMultipleFiles; - - @override - State<_FileUploadLocal> createState() => _FileUploadLocalState(); -} - -class _FileUploadLocalState extends State<_FileUploadLocal> { - bool isDragging = false; - - @override - Widget build(BuildContext context) { - final constraints = - UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null; - - if (UniversalPlatform.isMobile) { - return Padding( - padding: const EdgeInsets.all(12), - child: SizedBox( - height: 32, - child: FlowyButton( - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - showDefaultBoxDecorationOnMobile: true, - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.document_plugins_file_uploadMobile.tr(), - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: () => _uploadFile(context), - ), - ), - ); - } - - return Padding( - padding: const EdgeInsets.all(16), - child: DropTarget( - onDragEntered: (_) => setState(() => isDragging = true), - onDragExited: (_) => setState(() => isDragging = false), - onDragDone: (details) => widget.onFilesPicked(details.files), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => _uploadFile(context), - child: FlowyHover( - resetHoverOnRebuild: false, - isSelected: () => isDragging, - style: HoverStyle( - borderRadius: BorderRadius.circular(10), - hoverColor: - isDragging ? AFThemeExtension.of(context).tint9 : null, - ), - child: Container( - height: 172, - constraints: constraints, - child: DottedBorder( - dashPattern: const [3, 3], - radius: const Radius.circular(8), - borderType: BorderType.RRect, - color: isDragging - ? Theme.of(context).colorScheme.primary - : Theme.of(context).hintColor, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (isDragging) ...[ - FlowyText( - LocaleKeys.document_plugins_file_dropFileToUpload - .tr(), - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).hintColor, - ), - ] else ...[ - RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys - .document_plugins_file_fileUploadHint - .tr(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).hintColor, - ), - ), - TextSpan( - text: LocaleKeys - .document_plugins_file_fileUploadHintSuffix - .tr(), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: - Theme.of(context).colorScheme.primary, - ), - ), - ], - ), - ), - ], - ], - ), - ), - ), - ), - ), - ), - ), - ), - ); - } - - Future _uploadFile(BuildContext context) async { - final result = await getIt().pickFiles( - dialogTitle: '', - allowMultiple: widget.allowMultipleFiles, - ); - - final List files = result?.files.isNotEmpty ?? false - ? result!.files.map((f) => f.xFile).toList() - : const []; - - widget.onFilesPicked(files); - } -} - -class _FileUploadNetwork extends StatefulWidget { - const _FileUploadNetwork({required this.onSubmit}); - - final void Function(String url) onSubmit; - - @override - State<_FileUploadNetwork> createState() => _FileUploadNetworkState(); -} - -class _FileUploadNetworkState extends State<_FileUploadNetwork> { - bool isUrlValid = true; - String inputText = ''; - - @override - Widget build(BuildContext context) { - final constraints = - UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null; - - return Container( - padding: const EdgeInsets.all(16), - constraints: constraints, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyTextField( - hintText: LocaleKeys.document_plugins_file_networkHint.tr(), - onChanged: (value) => inputText = value, - onEditingComplete: submit, - ), - if (!isUrlValid) ...[ - const VSpace(4), - FlowyText( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: Theme.of(context).colorScheme.error, - maxLines: 3, - textAlign: TextAlign.start, - ), - ], - const VSpace(16), - SizedBox( - height: 32, - child: FlowyButton( - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - showDefaultBoxDecorationOnMobile: true, - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.document_plugins_file_networkAction.tr(), - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: submit, - ), - ), - ], - ), - ); - } - - void submit() { - if (checkUrlValidity(inputText)) { - return widget.onSubmit(inputText); - } - - setState(() => isUrlValid = false); - } - - bool checkUrlValidity(String url) => hrefRegex.hasMatch(url); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart deleted file mode 100644 index 8e6651ff73..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_util.dart +++ /dev/null @@ -1,261 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_service.dart'; -import 'package:appflowy/shared/custom_image_cache_manager.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/xfile_ext.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/dispatch/error.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_impl.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; -import 'package:universal_platform/universal_platform.dart'; - -Future saveFileToLocalStorage(String localFilePath) async { - final path = await getIt().getPath(); - final filePath = p.join(path, 'files'); - - try { - // create the directory if not exists - final directory = Directory(filePath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - final copyToPath = p.join( - filePath, - '${uuid()}${p.extension(localFilePath)}', - ); - await File(localFilePath).copy( - copyToPath, - ); - return copyToPath; - } catch (e) { - Log.error('cannot save file', e); - return null; - } -} - -Future<(String? path, String? errorMessage)> saveFileToCloudStorage( - String localFilePath, - String documentId, [ - bool isImage = false, -]) async { - final documentService = DocumentService(); - Log.debug("Uploading file from local path: $localFilePath"); - final result = await documentService.uploadFile( - localFilePath: localFilePath, - documentId: documentId, - ); - - return result.fold( - (s) async { - if (isImage) { - await CustomImageCacheManager().putFile( - s.url, - File(localFilePath).readAsBytesSync(), - ); - } - - return (s.url, null); - }, - (err) { - final message = Platform.isIOS - ? LocaleKeys.sideBar_storageLimitDialogTitleIOS.tr() - : LocaleKeys.sideBar_storageLimitDialogTitle.tr(); - if (err.isStorageLimitExceeded) { - return (null, message); - } - return (null, err.msg); - }, - ); -} - -/// Downloads a MediaFilePB -/// -/// On Mobile the file is fetched first using HTTP, and then saved using FilePicker. -/// On Desktop the files location is picked first using FilePicker, and then the file is saved. -/// -Future downloadMediaFile( - BuildContext context, - MediaFilePB file, { - VoidCallback? onDownloadBegin, - VoidCallback? onDownloadEnd, - UserProfilePB? userProfile, -}) async { - if ([ - FileUploadTypePB.NetworkFile, - FileUploadTypePB.LocalFile, - ].contains(file.uploadType)) { - /// When the file is a network file or a local file, we can directly open the file. - await afLaunchUrlString(file.url); - } else { - if (userProfile == null) { - showToastNotification( - message: LocaleKeys.grid_media_downloadFailedToken.tr(), - ); - return; - } - - final uri = Uri.parse(file.url); - final token = jsonDecode(userProfile.token)['access_token']; - - if (UniversalPlatform.isMobile) { - onDownloadBegin?.call(); - - final response = - await http.get(uri, headers: {'Authorization': 'Bearer $token'}); - - if (response.statusCode == 200) { - final tempFile = File(uri.pathSegments.last); - final result = await FilePicker().saveFile( - fileName: p.basename(tempFile.path), - bytes: response.bodyBytes, - ); - - if (result != null && context.mounted) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.grid_media_downloadSuccess.tr(), - ); - } - } else if (context.mounted) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), - ); - } - - onDownloadEnd?.call(); - } else { - final savePath = await FilePicker().saveFile(fileName: file.name); - if (savePath == null) { - return; - } - - onDownloadBegin?.call(); - - final response = - await http.get(uri, headers: {'Authorization': 'Bearer $token'}); - - if (response.statusCode == 200) { - final imgFile = File(savePath); - await imgFile.writeAsBytes(response.bodyBytes); - - if (context.mounted) { - showToastNotification( - message: LocaleKeys.grid_media_downloadSuccess.tr(), - ); - } - } else if (context.mounted) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), - ); - } - - onDownloadEnd?.call(); - } - } -} - -Future insertLocalFile( - BuildContext context, - XFile file, { - required String documentId, - UserProfilePB? userProfile, - void Function(String, bool)? onUploadSuccess, -}) async { - if (file.path.isEmpty) return; - - final fileType = file.fileType.toMediaFileTypePB(); - - // Check upload type - final isLocalMode = - (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; - - String? path; - String? errorMsg; - if (isLocalMode) { - path = await saveFileToLocalStorage(file.path); - } else { - (path, errorMsg) = await saveFileToCloudStorage( - file.path, - documentId, - fileType == MediaFileTypePB.Image, - ); - } - - if (errorMsg != null) { - return showSnackBarMessage(context, errorMsg); - } - - if (path == null) { - return; - } - - onUploadSuccess?.call(path, isLocalMode); -} - -/// [onUploadSuccess] Callback to be called when the upload is successful. -/// -/// The callback is called for each file that is successfully uploaded. -/// In case of an error, the error message will be shown on a per-file basis. -/// -Future insertLocalFiles( - BuildContext context, - List files, { - required String documentId, - UserProfilePB? userProfile, - void Function( - XFile file, - String path, - bool isLocalMode, - )? onUploadSuccess, -}) async { - if (files.every((f) => f.path.isEmpty)) return; - - // Check upload type - final isLocalMode = - (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == AuthTypePB.Local; - - for (final file in files) { - final fileType = file.fileType.toMediaFileTypePB(); - - String? path; - String? errorMsg; - - if (isLocalMode) { - path = await saveFileToLocalStorage(file.path); - } else { - (path, errorMsg) = await saveFileToCloudStorage( - file.path, - documentId, - fileType == MediaFileTypePB.Image, - ); - } - - if (errorMsg != null) { - showSnackBarMessage(context, errorMsg); - continue; - } - - if (path == null) { - continue; - } - onUploadSuccess?.call(file, path, isLocalMode); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart deleted file mode 100644 index f4c7a76c0e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/mobile_file_upload_menu.dart +++ /dev/null @@ -1,269 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class MobileFileUploadMenu extends StatefulWidget { - const MobileFileUploadMenu({ - super.key, - required this.onInsertLocalFile, - required this.onInsertNetworkFile, - this.allowMultipleFiles = false, - }); - - final void Function(List files) onInsertLocalFile; - final void Function(String url) onInsertNetworkFile; - final bool allowMultipleFiles; - - @override - State createState() => _MobileFileUploadMenuState(); -} - -class _MobileFileUploadMenuState extends State { - int currentTab = 0; - - @override - Widget build(BuildContext context) { - // ClipRRect is used to clip the tab indicator, so the animation doesn't overflow the dialog - return ClipRRect( - child: DefaultTabController( - length: 2, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - TabBar( - onTap: (value) => setState(() => currentTab = value), - indicatorWeight: 3, - labelPadding: EdgeInsets.zero, - padding: EdgeInsets.zero, - overlayColor: WidgetStatePropertyAll( - UniversalPlatform.isDesktop - ? Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - tabs: [ - _Tab( - title: LocaleKeys.document_plugins_file_uploadTab.tr(), - isSelected: currentTab == 0, - ), - _Tab( - title: LocaleKeys.document_plugins_file_networkTab.tr(), - isSelected: currentTab == 1, - ), - ], - ), - const Divider(height: 0), - if (currentTab == 0) ...[ - _FileUploadLocal( - allowMultipleFiles: widget.allowMultipleFiles, - onFilesPicked: (files) { - if (files.isNotEmpty) { - widget.onInsertLocalFile(files); - } - }, - ), - ] else ...[ - _FileUploadNetwork(onSubmit: widget.onInsertNetworkFile), - ], - ], - ), - ), - ); - } -} - -class _Tab extends StatelessWidget { - const _Tab({required this.title, this.isSelected = false}); - - final String title; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: 12.0, - right: 12.0, - bottom: 8.0, - top: UniversalPlatform.isMobile ? 0 : 8.0, - ), - child: FlowyText.semibold( - title, - color: isSelected - ? AFThemeExtension.of(context).strongText - : Theme.of(context).hintColor, - ), - ); - } -} - -class _FileUploadLocal extends StatefulWidget { - const _FileUploadLocal({ - required this.onFilesPicked, - this.allowMultipleFiles = false, - }); - - final void Function(List) onFilesPicked; - final bool allowMultipleFiles; - - @override - State<_FileUploadLocal> createState() => _FileUploadLocalState(); -} - -class _FileUploadLocalState extends State<_FileUploadLocal> { - bool isDragging = false; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - SizedBox( - height: 36, - child: FlowyButton( - radius: Corners.s8Border, - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.document_plugins_file_uploadMobileGallery.tr(), - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: () => _uploadFileFromGallery(context), - ), - ), - const VSpace(16), - SizedBox( - height: 36, - child: FlowyButton( - radius: Corners.s8Border, - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.document_plugins_file_uploadMobile.tr(), - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: () => _uploadFile(context), - ), - ), - ], - ), - ); - } - - Future _uploadFileFromGallery(BuildContext context) async { - final photoPermission = - await PermissionChecker.checkPhotoPermission(context); - if (!photoPermission) { - Log.error('Has no permission to access the photo library'); - return; - } - // on mobile, the users can pick a image file from camera or image library - final files = await ImagePicker().pickMultiImage(); - - widget.onFilesPicked(files); - } - - Future _uploadFile(BuildContext context) async { - final result = await getIt().pickFiles( - dialogTitle: '', - allowMultiple: widget.allowMultipleFiles, - ); - - final List files = result?.files.isNotEmpty ?? false - ? result!.files.map((f) => f.xFile).toList() - : const []; - - widget.onFilesPicked(files); - } -} - -class _FileUploadNetwork extends StatefulWidget { - const _FileUploadNetwork({required this.onSubmit}); - - final void Function(String url) onSubmit; - - @override - State<_FileUploadNetwork> createState() => _FileUploadNetworkState(); -} - -class _FileUploadNetworkState extends State<_FileUploadNetwork> { - bool isUrlValid = true; - String inputText = ''; - - @override - Widget build(BuildContext context) { - final constraints = - UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null; - - return Container( - padding: const EdgeInsets.all(16), - constraints: constraints, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyTextField( - hintText: LocaleKeys.document_plugins_file_networkHint.tr(), - onChanged: (value) => inputText = value, - onEditingComplete: submit, - ), - if (!isUrlValid) ...[ - const VSpace(4), - FlowyText( - LocaleKeys.document_plugins_file_networkUrlInvalid.tr(), - color: Theme.of(context).colorScheme.error, - maxLines: 3, - textAlign: TextAlign.start, - ), - ], - const VSpace(16), - SizedBox( - height: 36, - child: FlowyButton( - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - radius: Corners.s8Border, - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.grid_media_embedLink.tr(), - textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: submit, - ), - ), - ], - ), - ); - } - - void submit() { - if (checkUrlValidity(inputText)) { - return widget.onSubmit(inputText); - } - - setState(() => isUrlValid = false); - } - - bool checkUrlValidity(String url) => hrefRegex.hasMatch(url); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart index 2d65c602f5..7c84f2c31b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart @@ -3,101 +3,59 @@ import 'package:appflowy/generated/locale_keys.g.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:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; class FindAndReplaceMenuWidget extends StatefulWidget { const FindAndReplaceMenuWidget({ super.key, required this.onDismiss, required this.editorState, - required this.showReplaceMenu, }); final EditorState editorState; final VoidCallback onDismiss; - /// Whether to show the replace menu initially - final bool showReplaceMenu; - @override State createState() => _FindAndReplaceMenuWidgetState(); } class _FindAndReplaceMenuWidgetState extends State { - late bool showReplaceMenu = widget.showReplaceMenu; - - final findFocusNode = FocusNode(); - final replaceFocusNode = FocusNode(); + bool showReplaceMenu = false; late SearchServiceV3 searchService = SearchServiceV3( editorState: widget.editorState, ); - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.showReplaceMenu) { - replaceFocusNode.requestFocus(); - } else { - findFocusNode.requestFocus(); - } - }); - } - - @override - void dispose() { - findFocusNode.dispose(); - replaceFocusNode.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return Shortcuts( - shortcuts: const { - SingleActivator(LogicalKeyboardKey.escape): DismissIntent(), - }, - child: Actions( - actions: { - DismissIntent: CallbackAction( - onInvoke: (t) => widget.onDismiss.call(), - ), - }, - child: TextFieldTapRegion( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: FindMenu( - onDismiss: widget.onDismiss, - editorState: widget.editorState, - searchService: searchService, - focusNode: findFocusNode, - showReplaceMenu: showReplaceMenu, - onToggleShowReplace: () => setState(() { - showReplaceMenu = !showReplaceMenu; - }), - ), - ), - if (showReplaceMenu) - Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - ), - child: ReplaceMenu( - editorState: widget.editorState, - searchService: searchService, - focusNode: replaceFocusNode, - ), - ), - ], + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: FindMenu( + onDismiss: widget.onDismiss, + editorState: widget.editorState, + searchService: searchService, + onShowReplace: (value) => setState( + () => showReplaceMenu = value, + ), ), ), - ), + showReplaceMenu + ? Padding( + padding: const EdgeInsets.only( + bottom: 8.0, + ), + child: ReplaceMenu( + editorState: widget.editorState, + searchService: searchService, + ), + ) + : const SizedBox.shrink(), + ], ); } } @@ -105,30 +63,29 @@ class _FindAndReplaceMenuWidgetState extends State { class FindMenu extends StatefulWidget { const FindMenu({ super.key, + required this.onDismiss, required this.editorState, required this.searchService, - required this.showReplaceMenu, - required this.focusNode, - required this.onDismiss, - required this.onToggleShowReplace, + required this.onShowReplace, }); final EditorState editorState; - final SearchServiceV3 searchService; - - final bool showReplaceMenu; - final FocusNode focusNode; - final VoidCallback onDismiss; - final void Function() onToggleShowReplace; + final SearchServiceV3 searchService; + final void Function(bool value) onShowReplace; @override State createState() => _FindMenuState(); } class _FindMenuState extends State { - final textController = TextEditingController(); + late final FocusNode findTextFieldFocusNode; + final findTextEditingController = TextEditingController(); + + String queriedPattern = ''; + + bool showReplaceMenu = false; bool caseSensitive = false; @override @@ -138,7 +95,11 @@ class _FindMenuState extends State { widget.searchService.matchWrappers.addListener(_setState); widget.searchService.currentSelectedIndex.addListener(_setState); - textController.addListener(_searchPattern); + findTextEditingController.addListener(_searchPattern); + + WidgetsBinding.instance.addPostFrameCallback((_) { + findTextFieldFocusNode.requestFocus(); + }); } @override @@ -146,7 +107,9 @@ class _FindMenuState extends State { widget.searchService.matchWrappers.removeListener(_setState); widget.searchService.currentSelectedIndex.removeListener(_setState); widget.searchService.dispose(); - textController.dispose(); + findTextEditingController.removeListener(_searchPattern); + findTextEditingController.dispose(); + findTextFieldFocusNode.dispose(); super.dispose(); } @@ -160,36 +123,40 @@ class _FindMenuState extends State { const HSpace(4.0), // expand/collapse button _FindAndReplaceIcon( - icon: widget.showReplaceMenu + icon: showReplaceMenu ? FlowySvgs.drop_menu_show_s : FlowySvgs.drop_menu_hide_s, tooltipText: '', - onPressed: widget.onToggleShowReplace, + onPressed: () { + widget.onShowReplace(!showReplaceMenu); + setState( + () => showReplaceMenu = !showReplaceMenu, + ); + }, ), const HSpace(4.0), // find text input SizedBox( - width: 200, + width: 150, height: 30, - child: TextField( - key: const Key('findTextField'), - focusNode: widget.focusNode, - controller: textController, - style: Theme.of(context).textTheme.bodyMedium, - onSubmitted: (_) { - widget.searchService.navigateToMatch(); - - // after update selection or navigate to match, the editor - // will request focus, here's a workaround to request the - // focus back to the text field - Future.delayed( - const Duration(milliseconds: 50), - () => widget.focusNode.requestFocus(), - ); + child: FlowyFormTextInput( + onFocusCreated: (focusNode) { + findTextFieldFocusNode = focusNode; }, - decoration: _buildInputDecoration( - LocaleKeys.findAndReplace_find.tr(), - ), + onEditingComplete: () { + widget.searchService.navigateToMatch(); + // after update selection or navigate to match, the editor + // will request focus, here's a workaround to request the + // focus back to the findTextField + Future.delayed(const Duration(milliseconds: 50), () { + FocusScope.of(context).requestFocus( + findTextFieldFocusNode, + ); + }); + }, + controller: findTextEditingController, + hintText: LocaleKeys.findAndReplace_find.tr(), + textAlign: TextAlign.left, ), ), // the count of matches @@ -240,8 +207,11 @@ class _FindMenuState extends State { } void _searchPattern() { - widget.searchService.findAndHighlight(textController.text); - _setState(); + if (findTextEditingController.text.isEmpty) { + return; + } + widget.searchService.findAndHighlight(findTextEditingController.text); + setState(() => queriedPattern = findTextEditingController.text); } void _setState() { @@ -254,24 +224,27 @@ class ReplaceMenu extends StatefulWidget { super.key, required this.editorState, required this.searchService, - required this.focusNode, + this.localizations, }); final EditorState editorState; - final SearchServiceV3 searchService; - final FocusNode focusNode; + /// The localizations of the find and replace menu + final FindReplaceLocalizations? localizations; + + final SearchServiceV3 searchService; @override State createState() => _ReplaceMenuState(); } class _ReplaceMenuState extends State { - final textController = TextEditingController(); + late final FocusNode replaceTextFieldFocusNode; + final replaceTextEditingController = TextEditingController(); @override void dispose() { - textController.dispose(); + replaceTextEditingController.dispose(); super.dispose(); } @@ -282,26 +255,29 @@ class _ReplaceMenuState extends State { // placeholder for aligning the replace menu const HSpace(30), SizedBox( - width: 200, + width: 150, height: 30, - child: TextField( - key: const Key('replaceTextField'), - focusNode: widget.focusNode, - controller: textController, - style: Theme.of(context).textTheme.bodyMedium, - onSubmitted: (_) { - _replaceSelectedWord(); - - Future.delayed( - const Duration(milliseconds: 50), - () => widget.focusNode.requestFocus(), - ); + child: FlowyFormTextInput( + onFocusCreated: (focusNode) { + replaceTextFieldFocusNode = focusNode; }, - decoration: _buildInputDecoration( - LocaleKeys.findAndReplace_replace.tr(), - ), + onEditingComplete: () { + widget.searchService.navigateToMatch(); + // after update selection or navigate to match, the editor + // will request focus, here's a workaround to request the + // focus back to the findTextField + Future.delayed(const Duration(milliseconds: 50), () { + FocusScope.of(context).requestFocus( + replaceTextFieldFocusNode, + ); + }); + }, + controller: replaceTextEditingController, + hintText: LocaleKeys.findAndReplace_replace.tr(), + textAlign: TextAlign.left, ), ), + const HSpace(4.0), _FindAndReplaceIcon( onPressed: _replaceSelectedWord, iconBuilder: (_) => const Icon( @@ -318,7 +294,7 @@ class _ReplaceMenuState extends State { ), tooltipText: LocaleKeys.findAndReplace_replaceAll.tr(), onPressed: () => widget.searchService.replaceAllMatches( - textController.text, + replaceTextEditingController.text, ), ), ], @@ -326,7 +302,7 @@ class _ReplaceMenuState extends State { } void _replaceSelectedWord() { - widget.searchService.replaceSelectedWord(textController.text); + widget.searchService.replaceSelectedWord(replaceTextEditingController.text); } } @@ -352,20 +328,10 @@ class _FindAndReplaceIcon extends StatelessWidget { height: 24, onPressed: onPressed, icon: iconBuilder?.call(context) ?? - (icon != null - ? FlowySvg(icon!, color: Theme.of(context).iconTheme.color) - : const Placeholder()), + (icon != null ? FlowySvg(icon!) : const Placeholder()), tooltipText: tooltipText, isSelected: isSelected, iconColorOnHover: Theme.of(context).colorScheme.onSecondary, ); } } - -InputDecoration _buildInputDecoration(String hintText) { - return InputDecoration( - contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), - border: const UnderlineInputBorder(), - hintText: hintText, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart index e0f63e57c7..c610be4dbf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/font/customize_font_toolbar_item.dart @@ -1,3 +1,6 @@ +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +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/document/application/document_appearance_cubit.dart'; @@ -6,19 +9,70 @@ import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/util/levenshtein.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_value_dropdown.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; +final customizeFontToolbarItem = ToolbarItem( + id: 'editor.font', + group: 4, + isActive: onlyShowInTextType, + builder: (context, editorState, highlightColor, _) { + final selection = editorState.selection!; + final popoverController = PopoverController(); + final String? currentFontFamily = editorState + .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); + return MouseRegion( + cursor: SystemMouseCursors.click, + child: FontFamilyDropDown( + currentFontFamily: currentFontFamily ?? '', + offset: const Offset(0, 12), + popoverController: popoverController, + onOpen: () => keepEditorFocusNotifier.increase(), + onClose: () => keepEditorFocusNotifier.decrease(), + showResetButton: true, + onFontFamilyChanged: (fontFamily) async { + popoverController.close(); + try { + await editorState.formatDelta(selection, { + AppFlowyRichTextKeys.fontFamily: fontFamily, + }); + } catch (e) { + Log.error('Failed to set font family: $e'); + } + }, + onResetFont: () async { + popoverController.close(); + await editorState + .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: FlowyTooltip( + message: LocaleKeys.document_plugins_fonts.tr(), + child: const FlowySvg( + FlowySvgs.font_family_s, + size: Size.square(16.0), + color: Colors.white, + ), + ), + ), + ), + ); + }, +); + class ThemeFontFamilySetting extends StatefulWidget { const ThemeFontFamilySetting({ super.key, @@ -27,7 +81,7 @@ class ThemeFontFamilySetting extends StatefulWidget { final String currentFontFamily; static Key textFieldKey = const Key('FontFamilyTextField'); - static Key resetButtonKey = const Key('FontFamilyResetButton'); + static Key resetButtonkey = const Key('FontFamilyResetButton'); static Key popoverKey = const Key('FontFamilyPopover'); @override @@ -39,7 +93,7 @@ class _ThemeFontFamilySettingState extends State { Widget build(BuildContext context) { return SettingListTile( label: LocaleKeys.settings_appearance_fontFamily_label.tr(), - resetButtonKey: ThemeFontFamilySetting.resetButtonKey, + resetButtonKey: ThemeFontFamilySetting.resetButtonkey, onResetRequested: () { context.read().resetFontFamily(); context @@ -63,6 +117,7 @@ class FontFamilyDropDown extends StatefulWidget { this.child, this.popoverController, this.offset, + this.showResetButton = false, this.onResetFont, }); @@ -73,6 +128,7 @@ class FontFamilyDropDown extends StatefulWidget { final Widget? child; final PopoverController? popoverController; final Offset? offset; + final bool showResetButton; final VoidCallback? onResetFont; @override @@ -99,11 +155,6 @@ class _FontFamilyDropDownState extends State { popoverKey: ThemeFontFamilySetting.popoverKey, popoverController: widget.popoverController, currentValue: currentValue, - margin: EdgeInsets.zero, - boxConstraints: const BoxConstraints( - maxWidth: 240, - maxHeight: 420, - ), onClose: () { query.value = ''; widget.onClose?.call(); @@ -112,25 +163,32 @@ class _FontFamilyDropDownState extends State { child: widget.child, popupBuilder: (_) { widget.onOpen?.call(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: FlowyTextField( - key: ThemeFontFamilySetting.textFieldKey, - hintText: LocaleKeys.settings_appearance_fontFamily_search.tr(), - autoFocus: false, - debounceDuration: const Duration(milliseconds: 300), - onChanged: (value) { - setState(() { + return CustomScrollView( + shrinkWrap: true, + slivers: [ + if (widget.showResetButton) + SliverPersistentHeader( + delegate: _ResetFontButton(onPressed: widget.onResetFont), + pinned: true, + ), + SliverPadding( + padding: const EdgeInsets.only(right: 8), + sliver: SliverToBoxAdapter( + child: FlowyTextField( + key: ThemeFontFamilySetting.textFieldKey, + hintText: + LocaleKeys.settings_appearance_fontFamily_search.tr(), + autoFocus: false, + debounceDuration: const Duration(milliseconds: 300), + onChanged: (value) { query.value = value; - }); - }, + }, + ), ), ), - Container(height: 1, color: Theme.of(context).dividerColor), + const SliverToBoxAdapter( + child: SizedBox(height: 4), + ), ValueListenableBuilder( valueListenable: query, builder: (context, value, child) { @@ -145,32 +203,14 @@ class _FontFamilyDropDownState extends State { .sorted((a, b) => levenshtein(a, b)) .toList(); } - return displayed.length >= 10 - ? Flexible( - child: ListView.builder( - padding: const EdgeInsets.all(8.0), - itemBuilder: (context, index) => - _fontFamilyItemButton( - context, - getGoogleFontSafely(displayed[index]), - ), - itemCount: displayed.length, - ), - ) - : Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: List.generate( - displayed.length, - (index) => _fontFamilyItemButton( - context, - getGoogleFontSafely(displayed[index]), - ), - ), - ), - ); + return SliverFixedExtentList.builder( + itemBuilder: (context, index) => _fontFamilyItemButton( + context, + getGoogleFontSafely(displayed[index]), + ), + itemCount: displayed.length, + itemExtent: 32, + ); }, ), ], @@ -190,18 +230,16 @@ class _FontFamilyDropDownState extends State { waitDuration: const Duration(milliseconds: 150), child: SizedBox( key: ValueKey(buttonFontFamily), - height: 36, + height: 32, child: FlowyButton( onHover: (_) => FocusScope.of(context).unfocus(), - text: FlowyText( + text: FlowyText.medium( buttonFontFamily.fontFamilyDisplayName, fontFamily: buttonFontFamily, - figmaLineHeight: 20, - fontWeight: FontWeight.w400, ), rightIcon: buttonFontFamily == widget.currentFontFamily.parseFontFamilyName() - ? const FlowySvg(FlowySvgs.toolbar_check_m) + ? const FlowySvg(FlowySvgs.check_s) : null, onTap: () { if (widget.onFontFamilyChanged != null) { @@ -224,3 +262,37 @@ class _FontFamilyDropDownState extends State { ); } } + +class _ResetFontButton extends SliverPersistentHeaderDelegate { + _ResetFontButton({this.onPressed}); + + final VoidCallback? onPressed; + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Padding( + padding: const EdgeInsets.only(right: 8, bottom: 8.0), + child: FlowyTextButton( + LocaleKeys.document_toolbar_resetToDefaultFont.tr(), + fontColor: AFThemeExtension.of(context).textColor, + fontHoverColor: Theme.of(context).colorScheme.onSurface, + fontSize: 12, + onPressed: onPressed, + ), + ); + } + + @override + double get maxExtent => 35; + + @override + double get minExtent => 35; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => + true; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart index b891aecb6e..85633132d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor_bloc.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -30,7 +30,8 @@ class ChangeCoverPopoverBloc void _dispatch() { on((event, emit) async { await event.map( - fetchPickedImagePaths: (fetchPickedImagePaths) async { + fetchPickedImagePaths: + (FetchPickedImagePaths fetchPickedImagePaths) async { final imageNames = await _getPreviouslyPickedImagePaths(); emit( @@ -40,11 +41,11 @@ class ChangeCoverPopoverBloc ), ); }, - deleteImage: (deleteImage) async { + deleteImage: (DeleteImage deleteImage) async { final currentState = state; final currentlySelectedImage = node.attributes[DocumentHeaderBlockKeys.coverDetails]; - if (currentState is _Loaded) { + if (currentState is Loaded) { await _deleteImageInStorage(deleteImage.path); if (currentlySelectedImage == deleteImage.path) { _removeCoverImageFromNode(); @@ -53,15 +54,15 @@ class ChangeCoverPopoverBloc .where((path) => path != deleteImage.path) .toList(); _updateImagePathsInStorage(updateImageList); - emit(ChangeCoverPopoverState.loaded(updateImageList)); + emit(Loaded(updateImageList)); } }, - clearAllImages: (clearAllImages) async { + clearAllImages: (ClearAllImages clearAllImages) async { final currentState = state; final currentlySelectedImage = node.attributes[DocumentHeaderBlockKeys.coverDetails]; - if (currentState is _Loaded) { + if (currentState is Loaded) { for (final image in currentState.imageNames) { await _deleteImageInStorage(image); if (currentlySelectedImage == image) { @@ -69,7 +70,7 @@ class ChangeCoverPopoverBloc } } _updateImagePathsInStorage([]); - emit(const ChangeCoverPopoverState.loaded([])); + emit(const Loaded([])); } }, ); @@ -112,18 +113,18 @@ class ChangeCoverPopoverBloc class ChangeCoverPopoverEvent with _$ChangeCoverPopoverEvent { const factory ChangeCoverPopoverEvent.fetchPickedImagePaths({ @Default(false) bool selectLatestImage, - }) = _FetchPickedImagePaths; + }) = FetchPickedImagePaths; - const factory ChangeCoverPopoverEvent.deleteImage(String path) = _DeleteImage; - const factory ChangeCoverPopoverEvent.clearAllImages() = _ClearAllImages; + const factory ChangeCoverPopoverEvent.deleteImage(String path) = DeleteImage; + const factory ChangeCoverPopoverEvent.clearAllImages() = ClearAllImages; } @freezed class ChangeCoverPopoverState with _$ChangeCoverPopoverState { - const factory ChangeCoverPopoverState.initial() = _Initial; - const factory ChangeCoverPopoverState.loading() = _Loading; + const factory ChangeCoverPopoverState.initial() = Initial; + const factory ChangeCoverPopoverState.loading() = Loading; const factory ChangeCoverPopoverState.loaded( List imageNames, { @Default(false) selectLatestImage, - }) = _Loaded; + }) = Loaded; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart deleted file mode 100644 index 2c5062d408..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_title.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart'; -import 'package:appflowy/workspace/application/appearance_defaults.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; -import 'package:appflowy_backend/log.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:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class CoverTitle extends StatelessWidget { - const CoverTitle({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ViewBloc(view: view)..add(const ViewEvent.initial()), - child: _InnerCoverTitle( - view: view, - ), - ); - } -} - -class _InnerCoverTitle extends StatefulWidget { - const _InnerCoverTitle({ - required this.view, - }); - - final ViewPB view; - - @override - State<_InnerCoverTitle> createState() => _InnerCoverTitleState(); -} - -class _InnerCoverTitleState extends State<_InnerCoverTitle> { - final titleTextController = TextEditingController(); - - late final editorContext = context.read(); - late final editorState = context.read(); - late final titleFocusNode = editorContext.coverTitleFocusNode; - int lineCount = 1; - - @override - void initState() { - super.initState(); - - titleTextController.text = widget.view.name; - titleTextController.addListener(_onViewNameChanged); - - titleFocusNode - ..onKeyEvent = _onKeyEvent - ..addListener(_onFocusChanged); - - editorState.selectionNotifier.addListener(_onSelectionChanged); - - _requestInitialFocus(); - } - - @override - void dispose() { - titleFocusNode - ..onKeyEvent = null - ..removeListener(_onFocusChanged); - titleTextController.dispose(); - editorState.selectionNotifier.removeListener(_onSelectionChanged); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final fontStyle = Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(fontSize: 40.0, fontWeight: FontWeight.w700); - final width = context.read().state.width; - return BlocConsumer( - listenWhen: (previous, current) => - previous.view.name != current.view.name, - listener: _onListen, - builder: (context, state) { - final appearance = context.read().state; - return Container( - constraints: BoxConstraints(maxWidth: width), - child: Theme( - data: Theme.of(context).copyWith( - textSelectionTheme: TextSelectionThemeData( - cursorColor: appearance.selectionColor, - selectionColor: appearance.selectionColor ?? - DefaultAppearanceSettings.getDefaultSelectionColor(context), - ), - ), - child: TextFieldWithMetricLines( - controller: titleTextController, - enabled: editorState.editable, - focusNode: titleFocusNode, - style: fontStyle, - onLineCountChange: (count) => lineCount = count, - decoration: InputDecoration( - border: InputBorder.none, - hintText: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - hintStyle: fontStyle.copyWith( - color: Theme.of(context).hintColor, - ), - ), - ), - ), - ); - }, - ); - } - - void _requestInitialFocus() { - if (editorContext.requestCoverTitleFocus) { - void requestFocus() { - titleFocusNode.canRequestFocus = true; - titleFocusNode.requestFocus(); - editorContext.requestCoverTitleFocus = false; - } - - // on macOS, if we gain focus immediately, the focus won't work. - // It's a workaround to delay the focus request. - if (UniversalPlatform.isMacOS) { - Future.delayed(Durations.short4, () { - requestFocus(); - }); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - requestFocus(); - }); - } - } - } - - void _onSelectionChanged() { - // if title is focused and the selection is not null, clear the selection - if (editorState.selection != null && titleFocusNode.hasFocus) { - Log.info('title is focused, clear the editor selection'); - editorState.selection = null; - } - } - - void _onListen(BuildContext context, ViewState state) { - _requestFocusIfNeeded(widget.view, state); - - if (state.view.name != titleTextController.text) { - titleTextController.text = state.view.name; - } - } - - bool _shouldFocus(ViewPB view, ViewState? state) { - final name = state?.view.name ?? view.name; - - if (editorState.document.root.children.isNotEmpty) { - return false; - } - - // if the view's name is empty, focus on the title - if (name.isEmpty) { - return true; - } - - return false; - } - - void _requestFocusIfNeeded(ViewPB view, ViewState? state) { - final shouldFocus = _shouldFocus(view, state); - if (shouldFocus) { - titleFocusNode.requestFocus(); - } - } - - void _onFocusChanged() { - if (titleFocusNode.hasFocus) { - // if the document is empty, disable the keyboard service - final children = editorState.document.root.children; - final firstDelta = children.firstOrNull?.delta; - final isEmptyDocument = - children.length == 1 && (firstDelta == null || firstDelta.isEmpty); - if (!isEmptyDocument) { - return; - } - - if (editorState.selection != null) { - Log.info('cover title got focus, clear the editor selection'); - editorState.selection = null; - } - - Log.info('cover title got focus, disable keyboard service'); - editorState.service.keyboardService?.disable(); - } else { - Log.info('cover title lost focus, enable keyboard service'); - editorState.service.keyboardService?.enable(); - } - } - - void _onViewNameChanged() { - Debounce.debounce( - 'update view name', - const Duration(milliseconds: 250), - () { - if (!mounted) { - return; - } - if (context.read().state.view.name != - titleTextController.text) { - context - .read() - .add(ViewEvent.rename(titleTextController.text)); - } - context - .read() - ?.add(ViewInfoEvent.titleChanged(titleTextController.text)); - }, - ); - } - - KeyEventResult _onKeyEvent(FocusNode focusNode, KeyEvent event) { - if (event is KeyUpEvent) { - return KeyEventResult.ignored; - } - - if (event.logicalKey == LogicalKeyboardKey.enter) { - // if enter is pressed, jump the first line of editor. - _createNewLine(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - return _moveCursorToNextLine(event.logicalKey); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - return _moveCursorToNextLine(event.logicalKey); - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - return _exitEditing(); - } else if (event.logicalKey == LogicalKeyboardKey.tab) { - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } - - KeyEventResult _exitEditing() { - titleFocusNode.unfocus(); - return KeyEventResult.handled; - } - - Future _createNewLine() async { - titleFocusNode.unfocus(); - - final selection = titleTextController.selection; - final text = titleTextController.text; - // split the text into two lines based on the cursor position - final parts = [ - text.substring(0, selection.baseOffset), - text.substring(selection.baseOffset), - ]; - titleTextController.text = parts[0]; - - final transaction = editorState.transaction; - transaction.insertNode([0], paragraphNode(text: parts[1])); - await editorState.apply(transaction); - - // update selection instead of using afterSelection in transaction, - // because it will cause the cursor to jump - await editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: [0])), - // trigger the keyboard service. - reason: SelectionUpdateReason.uiEvent, - ); - } - - KeyEventResult _moveCursorToNextLine(LogicalKeyboardKey key) { - final selection = titleTextController.selection; - final text = titleTextController.text; - - // if the cursor is not at the end of the text, ignore the event - if ((key == LogicalKeyboardKey.arrowRight || lineCount != 1) && - (!selection.isCollapsed || text.length != selection.extentOffset)) { - return KeyEventResult.ignored; - } - - final node = editorState.getNodeAtPath([0]); - if (node == null) { - _createNewLine(); - return KeyEventResult.handled; - } - - titleFocusNode.unfocus(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - // delay the update selection to wait for the title to unfocus - int offset = 0; - if (key == LogicalKeyboardKey.arrowDown) { - offset = node.delta?.length ?? 0; - } else if (key == LogicalKeyboardKey.arrowRight) { - offset = 0; - } - editorState.updateSelectionWithReason( - Selection.collapsed( - Position(path: [0], offset: offset), - ), - // trigger the keyboard service. - reason: SelectionUpdateReason.uiEvent, - ); - }); - - return KeyEventResult.handled; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart index f5df4c0904..560661d157 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart @@ -36,49 +36,43 @@ class _CoverImagePickerState extends State { ..add(const CoverImagePickerEvent.initialEvent()), child: BlocListener( listener: (context, state) { - state.maybeWhen( - networkImage: (successOrFail) { - successOrFail.fold( - (s) {}, - (e) => showSnapBar( - context, - LocaleKeys.document_plugins_cover_invalidImageUrl.tr(), - ), - ); - }, - done: (successOrFail) { - successOrFail.fold( - (l) => widget.onFileSubmit(l), - (r) => showSnapBar( - context, - LocaleKeys.document_plugins_cover_failedToAddImageToGallery - .tr(), - ), - ); - }, - orElse: () {}, - ); + if (state is NetworkImagePicked) { + state.successOrFail.fold( + (s) {}, + (e) => showSnapBar( + context, + LocaleKeys.document_plugins_cover_invalidImageUrl.tr(), + ), + ); + } + if (state is Done) { + state.successOrFail.fold( + (l) => widget.onFileSubmit(l), + (r) => showSnapBar( + context, + LocaleKeys.document_plugins_cover_failedToAddImageToGallery + .tr(), + ), + ); + } }, child: BlocBuilder( builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - state.maybeMap( - loading: (_) => const SizedBox( - height: 180, - child: Center( - child: CircularProgressIndicator(), - ), - ), - orElse: () => CoverImagePreviewWidget(state: state), - ), + state is Loading + ? const SizedBox( + height: 180, + child: Center( + child: CircularProgressIndicator(), + ), + ) + : CoverImagePreviewWidget(state: state), const VSpace(10), NetworkImageUrlInput( onAdd: (url) { - context - .read() - .add(CoverImagePickerEvent.urlSubmit(url)); + context.read().add(UrlSubmit(url)); }, ), const VSpace(10), @@ -87,9 +81,9 @@ class _CoverImagePickerState extends State { widget.onBackPressed(); }, onSave: () { - context - .read() - .add(CoverImagePickerEvent.saveToGallery(state)); + context.read().add( + SaveToGallery(state), + ); }, ), ], @@ -117,14 +111,11 @@ class _NetworkImageUrlInputState extends State { @override void initState() { super.initState(); - urlController.addListener(_updateState); + urlController.addListener(() => setState(() {})); } - void _updateState() => setState(() {}); - @override void dispose() { - urlController.removeListener(_updateState); urlController.dispose(); super.dispose(); } @@ -202,7 +193,7 @@ class ImagePickerActionButtons extends StatelessWidget { class CoverImagePreviewWidget extends StatefulWidget { const CoverImagePreviewWidget({super.key, required this.state}); - final CoverImagePickerState state; + final dynamic state; @override State createState() => @@ -248,9 +239,7 @@ class _CoverImagePreviewWidgetState extends State { FlowyButton( hoverColor: Theme.of(context).hoverColor, onTap: () { - ctx - .read() - .add(const CoverImagePickerEvent.pickFileImage()); + ctx.read().add(const PickFileImage()); }, useIntrinsicWidth: true, leftIcon: const FlowySvg( @@ -258,7 +247,6 @@ class _CoverImagePreviewWidgetState extends State { size: Size(20, 20), ), text: FlowyText( - lineHeight: 1.0, LocaleKeys.document_plugins_cover_pickFromFiles.tr(), ), ), @@ -273,9 +261,7 @@ class _CoverImagePreviewWidgetState extends State { top: 10, child: InkWell( onTap: () { - ctx - .read() - .add(const CoverImagePickerEvent.deleteImage()); + ctx.read().add(const DeleteImage()); }, child: DecoratedBox( decoration: BoxDecoration( @@ -301,42 +287,42 @@ class _CoverImagePreviewWidgetState extends State { decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondary, borderRadius: Corners.s6Border, - image: widget.state.whenOrNull( - networkImage: (successOrFail) { - return successOrFail.fold( - (path) => DecorationImage( - image: NetworkImage(path), - fit: BoxFit.cover, - ), - (r) => null, - ); - }, - fileImage: (path) { - return DecorationImage( - image: FileImage(File(path)), - fit: BoxFit.cover, - ); - }, - ), - ), - child: widget.state.whenOrNull( - initial: () => _buildFilePickerWidget(context), - networkImage: (successOrFail) => successOrFail.fold( - (l) => null, - (r) => _buildFilePickerWidget( - context, - ), - ), + image: widget.state is Initial + ? null + : widget.state is NetworkImagePicked + ? widget.state.successOrFail.fold( + (path) => DecorationImage( + image: NetworkImage(path), + fit: BoxFit.cover, + ), + (r) => null, + ) + : widget.state is FileImagePicked + ? DecorationImage( + image: FileImage(File(widget.state.path)), + fit: BoxFit.cover, + ) + : null, ), + child: (widget.state is Initial) + ? _buildFilePickerWidget(context) + : (widget.state is NetworkImagePicked) + ? widget.state.successOrFail.fold( + (l) => null, + (r) => _buildFilePickerWidget( + context, + ), + ) + : null, ), - widget.state.maybeWhen( - fileImage: (_) => _buildImageDeleteButton(context), - networkImage: (successOrFail) => successOrFail.fold( - (l) => _buildImageDeleteButton(context), - (r) => const SizedBox.shrink(), - ), - orElse: () => const SizedBox.shrink(), - ), + (widget.state is FileImagePicked) + ? _buildImageDeleteButton(context) + : (widget.state is NetworkImagePicked) + ? widget.state.successOrFail.fold( + (l) => _buildImageDeleteButton(context), + (r) => const SizedBox.shrink(), + ) + : const SizedBox.shrink(), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart index 64e21eb773..316f2ddd8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker_bloc.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/default_extensions.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -24,14 +23,16 @@ class CoverImagePickerBloc _dispatch(); } + static const allowedExtensions = ['jpg', 'png', 'jpeg']; + void _dispatch() { on( (event, emit) async { await event.map( - initialEvent: (initialEvent) { + initialEvent: (InitialEvent initialEvent) { emit(const CoverImagePickerState.initial()); }, - urlSubmit: (urlSubmit) async { + urlSubmit: (UrlSubmit urlSubmit) async { emit(const CoverImagePickerState.loading()); final validateImage = await _validateURL(urlSubmit.path); if (validateImage) { @@ -53,7 +54,7 @@ class CoverImagePickerBloc ); } }, - pickFileImage: (pickFileImage) async { + pickFileImage: (PickFileImage pickFileImage) async { final imagePickerResults = await _pickImages(); if (imagePickerResults != null) { emit(CoverImagePickerState.fileImage(imagePickerResults)); @@ -61,10 +62,10 @@ class CoverImagePickerBloc emit(const CoverImagePickerState.initial()); } }, - deleteImage: (deleteImage) { + deleteImage: (DeleteImage deleteImage) { emit(const CoverImagePickerState.initial()); }, - saveToGallery: (saveToGallery) async { + saveToGallery: (SaveToGallery saveToGallery) async { emit(const CoverImagePickerState.loading()); final saveImage = await _saveToGallery(saveToGallery.previousState); if (saveImage != null) { @@ -93,7 +94,7 @@ class CoverImagePickerBloc final List imagePaths = prefs.getStringList(kLocalImagesKey) ?? []; final directory = await _coverPath(); - if (state is _FileImagePicked) { + if (state is FileImagePicked) { try { final path = state.path; final newPath = p.join(directory, p.split(path).last); @@ -102,7 +103,7 @@ class CoverImagePickerBloc } catch (e) { return null; } - } else if (state is _NetworkImagePicked) { + } else if (state is NetworkImagePicked) { try { final url = state.successOrFail.fold((path) => path, (r) => null); if (url != null) { @@ -127,7 +128,7 @@ class CoverImagePickerBloc final result = await getIt().pickFiles( dialogTitle: LocaleKeys.document_plugins_cover_addLocalImage.tr(), type: FileType.image, - allowedExtensions: defaultImageExtensions, + allowedExtensions: allowedExtensions, ); if (result != null && result.files.isNotEmpty) { return result.files.first.path; @@ -175,7 +176,7 @@ class CoverImagePickerBloc if (ext != null && ext.isNotEmpty) { ext = ext.substring(1); } - if (defaultImageExtensions.contains(ext)) { + if (allowedExtensions.contains(ext)) { return ext; } return null; @@ -197,25 +198,25 @@ class CoverImagePickerBloc @freezed class CoverImagePickerEvent with _$CoverImagePickerEvent { - const factory CoverImagePickerEvent.urlSubmit(String path) = _UrlSubmit; - const factory CoverImagePickerEvent.pickFileImage() = _PickFileImage; - const factory CoverImagePickerEvent.deleteImage() = _DeleteImage; + const factory CoverImagePickerEvent.urlSubmit(String path) = UrlSubmit; + const factory CoverImagePickerEvent.pickFileImage() = PickFileImage; + const factory CoverImagePickerEvent.deleteImage() = DeleteImage; const factory CoverImagePickerEvent.saveToGallery( CoverImagePickerState previousState, - ) = _SaveToGallery; - const factory CoverImagePickerEvent.initialEvent() = _InitialEvent; + ) = SaveToGallery; + const factory CoverImagePickerEvent.initialEvent() = InitialEvent; } @freezed class CoverImagePickerState with _$CoverImagePickerState { - const factory CoverImagePickerState.initial() = _Initial; - const factory CoverImagePickerState.loading() = _Loading; + const factory CoverImagePickerState.initial() = Initial; + const factory CoverImagePickerState.loading() = Loading; const factory CoverImagePickerState.networkImage( FlowyResult successOrFail, - ) = _NetworkImagePicked; - const factory CoverImagePickerState.fileImage(String path) = _FileImagePicked; + ) = NetworkImagePicked; + const factory CoverImagePickerState.fileImage(String path) = FileImagePicked; const factory CoverImagePickerState.done( FlowyResult, FlowyError> successOrFail, - ) = _Done; + ) = Done; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart index 7265ef6f82..e8d04d7161 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/desktop_cover.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover_bloc.dart'; @@ -11,6 +9,7 @@ import 'package:appflowy/shared/flowy_gradient_colors.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart deleted file mode 100644 index 16605367ca..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart +++ /dev/null @@ -1,935 +0,0 @@ -import 'dart:io'; -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_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_util.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/migration/editor_migration.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'cover_title.dart'; - -const double kCoverHeight = 280.0; -const double kIconHeight = 60.0; -const double kToolbarHeight = 40.0; // with padding to the top - -// Remove this widget if the desktop support immersive cover. -class DocumentHeaderBlockKeys { - const DocumentHeaderBlockKeys._(); - - static const String coverType = 'cover_selection_type'; - static const String coverDetails = 'cover_selection'; - static const String icon = 'selected_icon'; -} - -// for the version under 0.5.5, including 0.5.5 -enum CoverType { - none, - color, - file, - asset; - - static CoverType fromString(String? value) { - if (value == null) { - return CoverType.none; - } - return CoverType.values.firstWhere( - (e) => e.toString() == value, - orElse: () => CoverType.none, - ); - } -} - -// This key is used to intercept the selection event in the document cover widget. -const _interceptorKey = 'document_cover_widget_interceptor'; - -class DocumentCoverWidget extends StatefulWidget { - const DocumentCoverWidget({ - super.key, - required this.node, - required this.editorState, - required this.onIconChanged, - required this.view, - required this.tabs, - }); - - final Node node; - final EditorState editorState; - final ValueChanged onIconChanged; - final ViewPB view; - final List tabs; - - @override - State createState() => _DocumentCoverWidgetState(); -} - -class _DocumentCoverWidgetState extends State { - CoverType get coverType => CoverType.fromString( - widget.node.attributes[DocumentHeaderBlockKeys.coverType], - ); - - String? get coverDetails => - widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - - String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; - - bool get hasIcon => viewIcon.emoji.isNotEmpty; - - bool get hasCover => - coverType != CoverType.none || - (cover != null && cover?.type != PageStyleCoverImageType.none); - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - EmojiIconData viewIcon = EmojiIconData.none(); - - PageStyleCover? cover; - late ViewPB view; - late final ViewListener viewListener; - int retryCount = 0; - - final isCoverTitleHovered = ValueNotifier(false); - - late final gestureInterceptor = SelectionGestureInterceptor( - key: _interceptorKey, - canTap: (details) => !_isTapInBounds(details.globalPosition), - canPanStart: (details) => !_isDragInBounds(details.globalPosition), - ); - - @override - void initState() { - super.initState(); - final icon = widget.view.icon; - viewIcon = EmojiIconData.fromViewIconPB(icon); - cover = widget.view.cover; - view = widget.view; - widget.node.addListener(_reload); - widget.editorState.service.selectionService - .registerGestureInterceptor(gestureInterceptor); - - viewListener = ViewListener(viewId: widget.view.id) - ..start( - onViewUpdated: (view) { - setState(() { - viewIcon = EmojiIconData.fromViewIconPB(view.icon); - cover = view.cover; - view = view; - }); - }, - ); - } - - @override - void dispose() { - viewListener.stop(); - widget.node.removeListener(_reload); - isCoverTitleHovered.dispose(); - widget.editorState.service.selectionService - .unregisterGestureInterceptor(_interceptorKey); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - final offset = _calculateIconLeft(context, constraints); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Stack( - children: [ - SizedBox( - height: _calculateOverallHeight(), - child: DocumentHeaderToolbar( - onIconOrCoverChanged: _saveIconOrCover, - node: widget.node, - editorState: widget.editorState, - hasCover: hasCover, - hasIcon: hasIcon, - offset: offset, - isCoverTitleHovered: isCoverTitleHovered, - documentId: view.id, - tabs: widget.tabs, - ), - ), - if (hasCover) - DocumentCover( - view: view, - editorState: widget.editorState, - node: widget.node, - coverType: coverType, - coverDetails: coverDetails, - onChangeCover: (type, details) => - _saveIconOrCover(cover: (type, details)), - ), - _buildAlignedCoverIcon(context), - ], - ), - _buildAlignedTitle(context), - ], - ); - }, - ); - } - - Widget _buildAlignedTitle(BuildContext context) { - return Center( - child: Container( - constraints: BoxConstraints( - maxWidth: widget.editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: widget.editorState.editorStyle.padding + - const EdgeInsets.symmetric(horizontal: 44), - child: MouseRegion( - onEnter: (event) => isCoverTitleHovered.value = true, - onExit: (event) => isCoverTitleHovered.value = false, - child: CoverTitle( - view: widget.view, - ), - ), - ), - ); - } - - Widget _buildAlignedCoverIcon(BuildContext context) { - if (!hasIcon) { - return const SizedBox.shrink(); - } - - return Positioned( - bottom: hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight, - left: 0, - right: 0, - child: Center( - child: Container( - constraints: BoxConstraints( - maxWidth: - widget.editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: widget.editorState.editorStyle.padding + - const EdgeInsets.symmetric(horizontal: 44), - child: Row( - children: [ - DocumentIcon( - editorState: widget.editorState, - node: widget.node, - icon: viewIcon, - documentId: view.id, - onChangeIcon: (icon) => _saveIconOrCover(icon: icon), - ), - Spacer(), - ], - ), - ), - ), - ); - } - - void _reload() => setState(() {}); - - double _calculateIconLeft(BuildContext context, BoxConstraints constraints) { - final editorState = context.read(); - final appearanceCubit = context.read(); - - final renderBox = editorState.renderBox; - - if (renderBox == null || !renderBox.hasSize) {} - - var renderBoxWidth = 0.0; - if (renderBox != null && renderBox.hasSize) { - renderBoxWidth = renderBox.size.width; - } else if (retryCount <= 3) { - retryCount++; - // this is a workaround for the issue that the renderBox is not initialized - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _reload(); - }); - return 0; - } - - // if the renderBox width equals to 0, it means the editor is not initialized - final editorWidth = renderBoxWidth != 0 - ? min(renderBoxWidth, appearanceCubit.state.width) - : appearanceCubit.state.width; - - // left padding + editor width + right padding = the width of the editor - final leftOffset = (constraints.maxWidth - editorWidth) / 2.0 + - EditorStyleCustomizer.documentPadding.right; - - // ensure the offset is not negative - return max(0, leftOffset); - } - - double _calculateOverallHeight() { - final height = switch ((hasIcon, hasCover)) { - (true, true) => kCoverHeight + kToolbarHeight, - (true, false) => 50 + kIconHeight + kToolbarHeight, - (false, true) => kCoverHeight + kToolbarHeight, - (false, false) => kToolbarHeight, - }; - - return height; - } - - void _saveIconOrCover({ - (CoverType, String?)? cover, - EmojiIconData? icon, - }) async { - final transaction = widget.editorState.transaction; - final coverType = widget.node.attributes[DocumentHeaderBlockKeys.coverType]; - final coverDetails = - widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - final Map attributes = { - DocumentHeaderBlockKeys.coverType: coverType, - DocumentHeaderBlockKeys.coverDetails: coverDetails, - DocumentHeaderBlockKeys.icon: - widget.node.attributes[DocumentHeaderBlockKeys.icon], - CustomImageBlockKeys.imageType: '1', - }; - if (cover != null) { - attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); - attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2; - } - if (icon != null) { - attributes[DocumentHeaderBlockKeys.icon] = icon.emoji; - widget.onIconChanged(icon); - } - - // compatible with version <= 0.5.5. - transaction.updateNode(widget.node, attributes); - await widget.editorState.apply(transaction); - - // compatible with version > 0.5.5. - EditorMigration.migrateCoverIfNeeded( - widget.view, - attributes, - overwrite: true, - ); - } - - bool _isTapInBounds(Offset offset) { - if (_renderBox == null) { - return false; - } - - final localPosition = _renderBox!.globalToLocal(offset); - return _renderBox!.paintBounds.contains(localPosition); - } - - bool _isDragInBounds(Offset offset) { - if (_renderBox == null) { - return false; - } - - final localPosition = _renderBox!.globalToLocal(offset); - return _renderBox!.paintBounds.contains(localPosition); - } -} - -@visibleForTesting -class DocumentHeaderToolbar extends StatefulWidget { - const DocumentHeaderToolbar({ - super.key, - required this.node, - required this.editorState, - required this.hasCover, - required this.hasIcon, - required this.onIconOrCoverChanged, - required this.offset, - this.documentId, - required this.isCoverTitleHovered, - required this.tabs, - }); - - final Node node; - final EditorState editorState; - final bool hasCover; - final bool hasIcon; - final void Function({(CoverType, String?)? cover, EmojiIconData? icon}) - onIconOrCoverChanged; - final double offset; - final String? documentId; - final ValueNotifier isCoverTitleHovered; - final List tabs; - - @override - State createState() => _DocumentHeaderToolbarState(); -} - -class _DocumentHeaderToolbarState extends State { - final _popoverController = PopoverController(); - - bool isHidden = UniversalPlatform.isDesktopOrWeb; - bool isPopoverOpen = false; - - @override - Widget build(BuildContext context) { - Widget child = Container( - alignment: Alignment.bottomLeft, - width: double.infinity, - padding: EdgeInsets.symmetric(horizontal: widget.offset), - child: SizedBox( - height: 28, - child: ValueListenableBuilder( - valueListenable: widget.isCoverTitleHovered, - builder: (context, isHovered, child) { - return Visibility( - visible: !isHidden || isPopoverOpen || isHovered, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: buildRowChildren(), - ), - ); - }, - ), - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - child = MouseRegion( - opaque: false, - onEnter: (event) => setHidden(false), - onExit: isPopoverOpen ? null : (_) => setHidden(true), - child: child, - ); - } - - return child; - } - - List buildRowChildren() { - if (widget.hasCover && widget.hasIcon) { - return []; - } - - final List children = []; - - if (!widget.hasCover) { - children.add( - FlowyButton( - leftIconSize: const Size.square(18), - onTap: () => widget.onIconOrCoverChanged( - cover: UniversalPlatform.isDesktopOrWeb - ? (CoverType.asset, '1') - : (CoverType.color, '0xffe8e0ff'), - ), - useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.add_cover_s), - text: FlowyText.small( - LocaleKeys.document_plugins_cover_addCover.tr(), - color: Theme.of(context).hintColor, - ), - ), - ); - } - - if (widget.hasIcon) { - children.add( - FlowyButton( - onTap: () => widget.onIconOrCoverChanged(icon: EmojiIconData.none()), - useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.add_icon_s), - iconPadding: 4.0, - text: FlowyText.small( - LocaleKeys.document_plugins_cover_removeIcon.tr(), - color: Theme.of(context).hintColor, - ), - ), - ); - } else { - Widget child = FlowyButton( - useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.add_icon_s), - iconPadding: 4.0, - text: FlowyText.small( - LocaleKeys.document_plugins_cover_addIcon.tr(), - color: Theme.of(context).hintColor, - ), - onTap: UniversalPlatform.isDesktop - ? null - : () async { - final result = await context.push( - MobileEmojiPickerScreen.routeName, - ); - if (result != null) { - widget.onIconOrCoverChanged(icon: result); - } - }, - ); - - if (UniversalPlatform.isDesktop) { - child = AppFlowyPopover( - onClose: () => setState(() => isPopoverOpen = false), - controller: _popoverController, - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithCenterAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), - margin: EdgeInsets.zero, - child: child, - popupBuilder: (BuildContext popoverContext) { - isPopoverOpen = true; - return FlowyIconEmojiPicker( - tabs: widget.tabs, - documentId: widget.documentId, - onSelectedEmoji: (r) { - widget.onIconOrCoverChanged(icon: r.data); - if (!r.keepOpen) _popoverController.close(); - }, - ); - }, - ); - } - - children.add(child); - } - - return children; - } - - void setHidden(bool value) { - if (isHidden == value) return; - setState(() { - isHidden = value; - }); - } -} - -@visibleForTesting -class DocumentCover extends StatefulWidget { - const DocumentCover({ - super.key, - required this.view, - required this.node, - required this.editorState, - required this.coverType, - this.coverDetails, - required this.onChangeCover, - }); - - final ViewPB view; - final Node node; - final EditorState editorState; - final CoverType coverType; - final String? coverDetails; - final void Function(CoverType type, String? details) onChangeCover; - - @override - State createState() => DocumentCoverState(); -} - -class DocumentCoverState extends State { - final popoverController = PopoverController(); - - bool isOverlayButtonsHidden = true; - bool isPopoverOpen = false; - - @override - Widget build(BuildContext context) { - return UniversalPlatform.isDesktopOrWeb - ? _buildDesktopCover() - : _buildMobileCover(); - } - - Widget _buildDesktopCover() { - return SizedBox( - height: kCoverHeight, - child: MouseRegion( - onEnter: (event) => setOverlayButtonsHidden(false), - onExit: (event) => - setOverlayButtonsHidden(isPopoverOpen ? false : true), - child: Stack( - children: [ - SizedBox( - height: double.infinity, - width: double.infinity, - child: DesktopCover( - view: widget.view, - editorState: widget.editorState, - node: widget.node, - coverType: widget.coverType, - coverDetails: widget.coverDetails, - ), - ), - if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context), - ], - ), - ), - ); - } - - Widget _buildMobileCover() { - return SizedBox( - height: kCoverHeight, - child: Stack( - children: [ - SizedBox( - height: double.infinity, - width: double.infinity, - child: _buildCoverImage(), - ), - Positioned( - bottom: 8, - right: 12, - child: Row( - children: [ - IntrinsicWidth( - child: RoundedTextButton( - fontSize: 14, - onPressed: () { - showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showCloseButton: true, - title: - LocaleKeys.document_plugins_cover_changeCover.tr(), - builder: (context) { - return Padding( - padding: const EdgeInsets.only(top: 8.0), - child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: UploadImageMenu( - limitMaximumImageSize: !_isLocalMode(), - supportTypes: const [ - UploadImageType.color, - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) async { - context.pop(); - - if (files.isEmpty) { - return; - } - - widget.onChangeCover( - CoverType.file, - files.first.path, - ); - }, - onSelectedAIImage: (_) { - throw UnimplementedError(); - }, - onSelectedNetworkImage: (url) async { - context.pop(); - widget.onChangeCover(CoverType.file, url); - }, - onSelectedColor: (color) { - context.pop(); - widget.onChangeCover(CoverType.color, color); - }, - ), - ), - ); - }, - ); - }, - fillColor: Theme.of(context) - .colorScheme - .onSurfaceVariant - .withValues(alpha: 0.5), - height: 32, - title: LocaleKeys.document_plugins_cover_changeCover.tr(), - ), - ), - const HSpace(8.0), - SizedBox.square( - dimension: 32.0, - child: DeleteCoverButton( - onTap: () => widget.onChangeCover(CoverType.none, null), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildCoverImage() { - final detail = widget.coverDetails; - if (detail == null) { - return const SizedBox.shrink(); - } - switch (widget.coverType) { - case CoverType.file: - if (isURL(detail)) { - final userProfilePB = - context.read().state.userProfilePB; - return FlowyNetworkImage( - url: detail, - userProfilePB: userProfilePB, - errorWidgetBuilder: (context, url, error) => - const SizedBox.shrink(), - ); - } - final imageFile = File(detail); - if (!imageFile.existsSync()) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onChangeCover(CoverType.none, null); - }); - return const SizedBox.shrink(); - } - return Image.file( - imageFile, - fit: BoxFit.cover, - ); - case CoverType.asset: - return Image.asset( - widget.coverDetails!, - fit: BoxFit.cover, - ); - case CoverType.color: - final color = widget.coverDetails?.tryToColor() ?? Colors.white; - return Container(color: color); - case CoverType.none: - return const SizedBox.shrink(); - } - } - - Widget _buildCoverOverlayButtons(BuildContext context) { - return Positioned( - bottom: 20, - right: 50, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - AppFlowyPopover( - controller: popoverController, - triggerActions: PopoverTriggerFlags.none, - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, - ), - margin: EdgeInsets.zero, - onClose: () => isPopoverOpen = false, - child: IntrinsicWidth( - child: RoundedTextButton( - height: 28.0, - onPressed: () => popoverController.show(), - hoverColor: Theme.of(context).colorScheme.surface, - textColor: Theme.of(context).colorScheme.tertiary, - fillColor: Theme.of(context) - .colorScheme - .surface - .withValues(alpha: 0.5), - title: LocaleKeys.document_plugins_cover_changeCover.tr(), - ), - ), - popupBuilder: (BuildContext popoverContext) { - isPopoverOpen = true; - - return UploadImageMenu( - limitMaximumImageSize: !_isLocalMode(), - supportTypes: const [ - UploadImageType.color, - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) { - popoverController.close(); - if (files.isEmpty) { - return; - } - - final item = files.map((file) => file.path).first; - onCoverChanged(CoverType.file, item); - }, - onSelectedAIImage: (_) { - throw UnimplementedError(); - }, - onSelectedNetworkImage: (url) { - popoverController.close(); - onCoverChanged(CoverType.file, url); - }, - onSelectedColor: (color) { - popoverController.close(); - onCoverChanged(CoverType.color, color); - }, - ); - }, - ), - const HSpace(10), - DeleteCoverButton( - onTap: () => onCoverChanged(CoverType.none, null), - ), - ], - ), - ); - } - - Future onCoverChanged(CoverType type, String? details) async { - final previousType = CoverType.fromString( - widget.node.attributes[DocumentHeaderBlockKeys.coverType], - ); - final previousDetails = - widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; - - bool isFileType(CoverType type, String? details) => - type == CoverType.file && details != null && !isURL(details); - - if (isFileType(type, details)) { - if (_isLocalMode()) { - details = await saveImageToLocalStorage(details!); - } else { - // else we should save the image to cloud storage - (details, _) = await saveImageToCloudStorage(details!, widget.view.id); - } - } - widget.onChangeCover(type, details); - - // After cover change,delete from localstorage if previous cover was image type - if (isFileType(previousType, previousDetails) && _isLocalMode()) { - await deleteImageFromLocalStorage(previousDetails); - } - } - - void setOverlayButtonsHidden(bool value) { - if (isOverlayButtonsHidden == value) return; - setState(() { - isOverlayButtonsHidden = value; - }); - } - - bool _isLocalMode() { - return context.read().isLocalMode; - } -} - -@visibleForTesting -class DeleteCoverButton extends StatelessWidget { - const DeleteCoverButton({required this.onTap, super.key}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final fillColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.surface.withValues(alpha: 0.5) - : Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5); - final svgColor = UniversalPlatform.isDesktopOrWeb - ? Theme.of(context).colorScheme.tertiary - : Theme.of(context).colorScheme.onPrimary; - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.surface, - fillColor: fillColor, - iconPadding: const EdgeInsets.all(5), - width: 28, - icon: FlowySvg( - FlowySvgs.delete_s, - color: svgColor, - ), - onPressed: onTap, - ); - } -} - -@visibleForTesting -class DocumentIcon extends StatefulWidget { - const DocumentIcon({ - super.key, - required this.node, - required this.editorState, - required this.icon, - required this.onChangeIcon, - this.documentId, - }); - - final Node node; - final EditorState editorState; - final EmojiIconData icon; - final String? documentId; - final ValueChanged onChangeIcon; - - @override - State createState() => _DocumentIconState(); -} - -class _DocumentIconState extends State { - final PopoverController _popoverController = PopoverController(); - - @override - Widget build(BuildContext context) { - Widget child = EmojiIconWidget(emoji: widget.icon); - - if (UniversalPlatform.isDesktopOrWeb) { - child = AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: _popoverController, - offset: const Offset(0, 8), - constraints: BoxConstraints.loose(const Size(360, 380)), - margin: EdgeInsets.zero, - child: child, - popupBuilder: (BuildContext popoverContext) { - return FlowyIconEmojiPicker( - initialType: widget.icon.type.toPickerTabType(), - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - documentId: widget.documentId, - onSelectedEmoji: (r) { - widget.onChangeIcon(r.data); - if (!r.keepOpen) _popoverController.close(); - }, - ); - }, - ); - } else { - child = GestureDetector( - child: child, - onTap: () async { - final result = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.iconSelectedType: widget.icon.type.name, - }, - ).toString(), - ); - if (result != null) { - widget.onChangeIcon(result); - } - }, - ); - } - - return child; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart new file mode 100644 index 0000000000..3ac9067b89 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -0,0 +1,747 @@ +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/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/plugins/document/application/document_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/desktop_cover.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; +import 'package:appflowy/shared/appflowy_network_image.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:string_validator/string_validator.dart'; + +const double kCoverHeight = 250.0; +const double kIconHeight = 60.0; +const double kToolbarHeight = 40.0; // with padding to the top + +// Remove this widget if the desktop support immersive cover. +class DocumentHeaderBlockKeys { + const DocumentHeaderBlockKeys._(); + + static const String coverType = 'cover_selection_type'; + static const String coverDetails = 'cover_selection'; + static const String icon = 'selected_icon'; +} + +// for the version under 0.5.5, including 0.5.5 +enum CoverType { + none, + color, + file, + asset; + + static CoverType fromString(String? value) { + if (value == null) { + return CoverType.none; + } + return CoverType.values.firstWhere( + (e) => e.toString() == value, + orElse: () => CoverType.none, + ); + } +} + +class DocumentCoverWidget extends StatefulWidget { + const DocumentCoverWidget({ + super.key, + required this.node, + required this.editorState, + required this.onIconChanged, + required this.view, + }); + + final Node node; + final EditorState editorState; + final void Function(String icon) onIconChanged; + final ViewPB view; + + @override + State createState() => _DocumentCoverWidgetState(); +} + +class _DocumentCoverWidgetState extends State { + CoverType get coverType => CoverType.fromString( + widget.node.attributes[DocumentHeaderBlockKeys.coverType], + ); + String? get coverDetails => + widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; + String? get icon => widget.node.attributes[DocumentHeaderBlockKeys.icon]; + bool get hasIcon => viewIcon.isNotEmpty; + bool get hasCover => + coverType != CoverType.none || + (cover != null && cover?.type != PageStyleCoverImageType.none); + + String viewIcon = ''; + PageStyleCover? cover; + late final ViewListener viewListener; + + @override + void initState() { + super.initState(); + final value = widget.view.icon.value; + viewIcon = value.isNotEmpty ? value : icon ?? ''; + cover = widget.view.cover; + widget.node.addListener(_reload); + viewListener = ViewListener( + viewId: widget.view.id, + )..start( + onViewUpdated: (p0) { + setState(() { + viewIcon = p0.icon.value; + cover = p0.cover; + }); + }, + ); + } + + @override + void dispose() { + viewListener.stop(); + widget.node.removeListener(_reload); + super.dispose(); + } + + void _reload() => setState(() {}); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + SizedBox( + height: _calculateOverallHeight(), + child: DocumentHeaderToolbar( + onIconOrCoverChanged: _saveIconOrCover, + node: widget.node, + editorState: widget.editorState, + hasCover: hasCover, + hasIcon: hasIcon, + ), + ), + if (hasCover) + DocumentCover( + view: widget.view, + editorState: widget.editorState, + node: widget.node, + coverType: coverType, + coverDetails: coverDetails, + onChangeCover: (type, details) => + _saveIconOrCover(cover: (type, details)), + ), + if (hasIcon) + Positioned( + left: PlatformExtension.isDesktopOrWeb ? 80 : 20, + // if hasCover, there shouldn't be icons present so the icon can + // be closer to the bottom. + bottom: + hasCover ? kToolbarHeight - kIconHeight / 2 : kToolbarHeight, + child: DocumentIcon( + editorState: widget.editorState, + node: widget.node, + icon: viewIcon, + onChangeIcon: (icon) => _saveIconOrCover(icon: icon), + ), + ), + ], + ); + } + + double _calculateOverallHeight() { + switch ((hasIcon, hasCover)) { + case (true, true): + return kCoverHeight + kToolbarHeight; + case (true, false): + return 50 + kIconHeight + kToolbarHeight; + case (false, true): + return kCoverHeight + kToolbarHeight; + case (false, false): + return kToolbarHeight; + } + } + + void _saveIconOrCover({(CoverType, String?)? cover, String? icon}) async { + final transaction = widget.editorState.transaction; + final coverType = widget.node.attributes[DocumentHeaderBlockKeys.coverType]; + final coverDetails = + widget.node.attributes[DocumentHeaderBlockKeys.coverDetails]; + final Map attributes = { + DocumentHeaderBlockKeys.coverType: coverType, + DocumentHeaderBlockKeys.coverDetails: coverDetails, + DocumentHeaderBlockKeys.icon: + widget.node.attributes[DocumentHeaderBlockKeys.icon], + CustomImageBlockKeys.imageType: '1', + }; + if (cover != null) { + attributes[DocumentHeaderBlockKeys.coverType] = cover.$1.toString(); + attributes[DocumentHeaderBlockKeys.coverDetails] = cover.$2; + } + if (icon != null) { + attributes[DocumentHeaderBlockKeys.icon] = icon; + widget.onIconChanged(icon); + } + + // compatible with version <= 0.5.5. + transaction.updateNode(widget.node, attributes); + await widget.editorState.apply(transaction); + + // compatible with version > 0.5.5. + EditorMigration.migrateCoverIfNeeded( + widget.view, + widget.editorState, + overwrite: true, + ); + } +} + +@visibleForTesting +class DocumentHeaderToolbar extends StatefulWidget { + const DocumentHeaderToolbar({ + super.key, + required this.node, + required this.editorState, + required this.hasCover, + required this.hasIcon, + required this.onIconOrCoverChanged, + }); + + final Node node; + final EditorState editorState; + final bool hasCover; + final bool hasIcon; + final void Function({(CoverType, String?)? cover, String? icon}) + onIconOrCoverChanged; + + @override + State createState() => _DocumentHeaderToolbarState(); +} + +class _DocumentHeaderToolbarState extends State { + bool isHidden = true; + bool isPopoverOpen = false; + + final PopoverController _popoverController = PopoverController(); + + @override + void initState() { + super.initState(); + + isHidden = PlatformExtension.isDesktopOrWeb; + } + + @override + Widget build(BuildContext context) { + Widget child = Container( + alignment: Alignment.bottomLeft, + width: double.infinity, + padding: PlatformExtension.isDesktopOrWeb + ? EdgeInsets.symmetric( + horizontal: EditorStyleCustomizer.documentPadding.right, + ) + : EdgeInsets.symmetric( + horizontal: EditorStyleCustomizer.documentPadding.left, + ), + child: SizedBox( + height: 28, + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: buildRowChildren(), + ), + ), + ); + + if (PlatformExtension.isDesktopOrWeb) { + child = MouseRegion( + onEnter: (event) => setHidden(false), + onExit: (event) { + if (!isPopoverOpen) { + setHidden(true); + } + }, + opaque: false, + child: child, + ); + } + + return child; + } + + List buildRowChildren() { + if (isHidden || widget.hasCover && widget.hasIcon) { + return []; + } + final List children = []; + + if (!widget.hasCover) { + children.add( + FlowyButton( + leftIconSize: const Size.square(18), + onTap: () => widget.onIconOrCoverChanged( + cover: PlatformExtension.isDesktopOrWeb + ? (CoverType.asset, '1') + : (CoverType.color, '0xffe8e0ff'), + ), + useIntrinsicWidth: true, + leftIcon: const FlowySvg(FlowySvgs.add_cover_s), + text: FlowyText.small( + LocaleKeys.document_plugins_cover_addCover.tr(), + color: Theme.of(context).hintColor, + ), + ), + ); + } + + if (widget.hasIcon) { + children.add( + FlowyButton( + onTap: () => widget.onIconOrCoverChanged(icon: ""), + useIntrinsicWidth: true, + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, + text: FlowyText.small( + LocaleKeys.document_plugins_cover_removeIcon.tr(), + color: Theme.of(context).hintColor, + ), + ), + ); + } else { + Widget child = FlowyButton( + useIntrinsicWidth: true, + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, + text: FlowyText.small( + LocaleKeys.document_plugins_cover_addIcon.tr(), + color: Theme.of(context).hintColor, + ), + onTap: PlatformExtension.isDesktop + ? null + : () async { + final result = await context.push( + MobileEmojiPickerScreen.routeName, + ); + if (result != null) { + widget.onIconOrCoverChanged(icon: result.emoji); + } + }, + ); + + if (PlatformExtension.isDesktop) { + child = AppFlowyPopover( + onClose: () => isPopoverOpen = false, + controller: _popoverController, + offset: const Offset(0, 8), + direction: PopoverDirection.bottomWithCenterAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, + popupBuilder: (BuildContext popoverContext) { + isPopoverOpen = true; + return FlowyIconPicker( + onSelected: (result) { + widget.onIconOrCoverChanged(icon: result.emoji); + _popoverController.close(); + }, + ); + }, + ); + } + + children.add(child); + } + + return children; + } + + void setHidden(bool value) { + if (isHidden == value) return; + setState(() { + isHidden = value; + }); + } +} + +@visibleForTesting +class DocumentCover extends StatefulWidget { + const DocumentCover({ + super.key, + required this.view, + required this.node, + required this.editorState, + required this.coverType, + this.coverDetails, + required this.onChangeCover, + }); + + final ViewPB view; + final Node node; + final EditorState editorState; + final CoverType coverType; + final String? coverDetails; + final void Function(CoverType type, String? details) onChangeCover; + + @override + State createState() => DocumentCoverState(); +} + +class DocumentCoverState extends State { + bool isOverlayButtonsHidden = true; + bool isPopoverOpen = false; + final PopoverController popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return PlatformExtension.isDesktopOrWeb + ? _buildDesktopCover() + : _buildMobileCover(); + } + + Widget _buildDesktopCover() { + return SizedBox( + height: kCoverHeight, + child: MouseRegion( + onEnter: (event) => setOverlayButtonsHidden(false), + onExit: (event) => + setOverlayButtonsHidden(isPopoverOpen ? false : true), + child: Stack( + children: [ + SizedBox( + height: double.infinity, + width: double.infinity, + child: DesktopCover( + view: widget.view, + editorState: widget.editorState, + node: widget.node, + coverType: widget.coverType, + coverDetails: widget.coverDetails, + ), + ), + if (!isOverlayButtonsHidden) _buildCoverOverlayButtons(context), + ], + ), + ), + ); + } + + Widget _buildMobileCover() { + return SizedBox( + height: kCoverHeight, + child: Stack( + children: [ + SizedBox( + height: double.infinity, + width: double.infinity, + child: _buildCoverImage(), + ), + Positioned( + bottom: 8, + right: 12, + child: Row( + children: [ + IntrinsicWidth( + child: RoundedTextButton( + fontSize: 14, + onPressed: () { + showMobileBottomSheet( + context, + showHeader: true, + showDragHandle: true, + showCloseButton: true, + title: + LocaleKeys.document_plugins_cover_changeCover.tr(), + builder: (context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 340, + minHeight: 80, + ), + child: UploadImageMenu( + limitMaximumImageSize: !_isLocalMode(), + supportTypes: const [ + UploadImageType.color, + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) async { + context.pop(); + widget.onChangeCover(CoverType.file, path); + }, + onSelectedAIImage: (_) { + throw UnimplementedError(); + }, + onSelectedNetworkImage: (url) async { + context.pop(); + widget.onChangeCover(CoverType.file, url); + }, + onSelectedColor: (color) { + context.pop(); + widget.onChangeCover(CoverType.color, color); + }, + ), + ), + ); + }, + ); + }, + fillColor: Theme.of(context) + .colorScheme + .onSurfaceVariant + .withOpacity(0.5), + height: 32, + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), + ), + const HSpace(8.0), + SizedBox.square( + dimension: 32.0, + child: DeleteCoverButton( + onTap: () => widget.onChangeCover(CoverType.none, null), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildCoverImage() { + final detail = widget.coverDetails; + if (detail == null) { + return const SizedBox.shrink(); + } + switch (widget.coverType) { + case CoverType.file: + if (isURL(detail)) { + final userProfilePB = + context.read().state.userProfilePB; + return FlowyNetworkImage( + url: detail, + userProfilePB: userProfilePB, + errorWidgetBuilder: (context, url, error) => + const SizedBox.shrink(), + ); + } + final imageFile = File(detail); + if (!imageFile.existsSync()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onChangeCover(CoverType.none, null); + }); + return const SizedBox.shrink(); + } + return Image.file( + imageFile, + fit: BoxFit.cover, + ); + case CoverType.asset: + return Image.asset( + widget.coverDetails!, + fit: BoxFit.cover, + ); + case CoverType.color: + final color = widget.coverDetails?.tryToColor() ?? Colors.white; + return Container(color: color); + case CoverType.none: + return const SizedBox.shrink(); + } + } + + Widget _buildCoverOverlayButtons(BuildContext context) { + return Positioned( + bottom: 20, + right: 50, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppFlowyPopover( + controller: popoverController, + triggerActions: PopoverTriggerFlags.none, + offset: const Offset(0, 8), + direction: PopoverDirection.bottomWithCenterAligned, + constraints: const BoxConstraints( + maxWidth: 540, + maxHeight: 360, + minHeight: 80, + ), + margin: EdgeInsets.zero, + onClose: () => isPopoverOpen = false, + child: IntrinsicWidth( + child: RoundedTextButton( + height: 28.0, + onPressed: () => popoverController.show(), + hoverColor: Theme.of(context).colorScheme.surface, + textColor: Theme.of(context).colorScheme.tertiary, + fillColor: + Theme.of(context).colorScheme.surface.withOpacity(0.5), + title: LocaleKeys.document_plugins_cover_changeCover.tr(), + ), + ), + popupBuilder: (BuildContext popoverContext) { + isPopoverOpen = true; + + return UploadImageMenu( + limitMaximumImageSize: !_isLocalMode(), + supportTypes: const [ + UploadImageType.color, + UploadImageType.local, + UploadImageType.url, + UploadImageType.unsplash, + ], + onSelectedLocalImage: (path) { + popoverController.close(); + onCoverChanged(CoverType.file, path); + }, + onSelectedAIImage: (_) { + throw UnimplementedError(); + }, + onSelectedNetworkImage: (url) { + popoverController.close(); + onCoverChanged(CoverType.file, url); + }, + onSelectedColor: (color) { + popoverController.close(); + onCoverChanged(CoverType.color, color); + }, + ); + }, + ), + const HSpace(10), + DeleteCoverButton( + onTap: () => onCoverChanged(CoverType.none, null), + ), + ], + ), + ); + } + + Future onCoverChanged(CoverType type, String? details) async { + if (type == CoverType.file && details != null && !isURL(details)) { + if (_isLocalMode()) { + details = await saveImageToLocalStorage(details); + } else { + // else we should save the image to cloud storage + (details, _) = await saveImageToCloudStorage(details); + } + } + widget.onChangeCover(type, details); + } + + void setOverlayButtonsHidden(bool value) { + if (isOverlayButtonsHidden == value) return; + setState(() { + isOverlayButtonsHidden = value; + }); + } + + bool _isLocalMode() { + return context.read().isLocalMode; + } +} + +@visibleForTesting +class DeleteCoverButton extends StatelessWidget { + const DeleteCoverButton({required this.onTap, super.key}); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final fillColor = PlatformExtension.isDesktopOrWeb + ? Theme.of(context).colorScheme.surface.withOpacity(0.5) + : Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5); + final svgColor = PlatformExtension.isDesktopOrWeb + ? Theme.of(context).colorScheme.tertiary + : Theme.of(context).colorScheme.onPrimary; + return FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.surface, + fillColor: fillColor, + iconPadding: const EdgeInsets.all(5), + width: 28, + icon: FlowySvg( + FlowySvgs.delete_s, + color: svgColor, + ), + onPressed: onTap, + ); + } +} + +@visibleForTesting +class DocumentIcon extends StatefulWidget { + const DocumentIcon({ + super.key, + required this.node, + required this.editorState, + required this.icon, + required this.onChangeIcon, + }); + + final Node node; + final EditorState editorState; + final String icon; + final void Function(String icon) onChangeIcon; + + @override + State createState() => _DocumentIconState(); +} + +class _DocumentIconState extends State { + final PopoverController _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + Widget child = EmojiIconWidget( + emoji: widget.icon, + ); + + if (PlatformExtension.isDesktopOrWeb) { + child = AppFlowyPopover( + direction: PopoverDirection.bottomWithCenterAligned, + controller: _popoverController, + offset: const Offset(0, 8), + constraints: BoxConstraints.loose(const Size(360, 380)), + child: child, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (result) { + widget.onChangeIcon(result.emoji); + _popoverController.close(); + }, + ); + }, + ); + } else { + child = GestureDetector( + child: child, + onTap: () async { + final result = await context.push( + MobileEmojiPickerScreen.routeName, + ); + if (result != null) { + widget.onChangeIcon(result.emoji); + } + }, + ); + } + + return child; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart index cda76233d6..d85ccac645 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart @@ -1,20 +1,5 @@ -import 'dart:convert'; -import 'dart:io'; - import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/appflowy_network_svg.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; -import 'package:string_validator/string_validator.dart'; - -import '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import '../../../../base/icon/icon_widget.dart'; class EmojiIconWidget extends StatefulWidget { const EmojiIconWidget({ @@ -23,7 +8,7 @@ class EmojiIconWidget extends StatefulWidget { this.emojiSize = 60, }); - final EmojiIconData emoji; + final String emoji; final double emojiSize; @override @@ -42,17 +27,15 @@ class _EmojiIconWidgetState extends State { child: Container( decoration: BoxDecoration( color: !hover - ? Theme.of(context) - .colorScheme - .inverseSurface - .withValues(alpha: 0.5) + ? Theme.of(context).colorScheme.inverseSurface.withOpacity(0.5) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), alignment: Alignment.center, - child: RawEmojiIconWidget( + child: EmojiText( emoji: widget.emoji, - emojiSize: widget.emojiSize, + fontSize: widget.emojiSize, + textAlign: TextAlign.center, ), ), ); @@ -65,155 +48,3 @@ class _EmojiIconWidgetState extends State { }); } } - -class RawEmojiIconWidget extends StatefulWidget { - const RawEmojiIconWidget({ - super.key, - required this.emoji, - required this.emojiSize, - this.enableColor = true, - this.lineHeight, - }); - - final EmojiIconData emoji; - final double emojiSize; - final bool enableColor; - final double? lineHeight; - - @override - State createState() => _RawEmojiIconWidgetState(); -} - -class _RawEmojiIconWidgetState extends State { - UserProfilePB? userProfile; - - EmojiIconData get emoji => widget.emoji; - - @override - void initState() { - super.initState(); - loadUserProfile(); - } - - @override - void didUpdateWidget(RawEmojiIconWidget oldWidget) { - super.didUpdateWidget(oldWidget); - loadUserProfile(); - } - - @override - Widget build(BuildContext context) { - final defaultEmoji = SizedBox( - width: widget.emojiSize, - child: EmojiText( - emoji: '❓', - fontSize: widget.emojiSize, - textAlign: TextAlign.center, - ), - ); - try { - switch (widget.emoji.type) { - case FlowyIconType.emoji: - return FlowyText.emoji( - widget.emoji.emoji, - fontSize: widget.emojiSize, - textAlign: TextAlign.justify, - lineHeight: widget.lineHeight, - ); - case FlowyIconType.icon: - IconsData iconData = IconsData.fromJson( - jsonDecode(widget.emoji.emoji), - ); - if (!widget.enableColor) { - iconData = iconData.noColor(); - } - - final iconSize = widget.emojiSize; - return IconWidget( - iconsData: iconData, - size: iconSize, - ); - case FlowyIconType.custom: - final url = widget.emoji.emoji; - final isSvg = url.endsWith('.svg'); - final hasUserProfile = userProfile != null; - if (isURL(url)) { - Widget child = const SizedBox.shrink(); - if (isSvg) { - child = FlowyNetworkSvg( - url, - headers: - hasUserProfile ? _buildRequestHeader(userProfile!) : {}, - width: widget.emojiSize, - height: widget.emojiSize, - ); - } else if (hasUserProfile) { - child = FlowyNetworkImage( - url: url, - width: widget.emojiSize, - height: widget.emojiSize, - userProfilePB: userProfile, - errorWidgetBuilder: (context, url, error) { - return const SizedBox.shrink(); - }, - ); - } - return SizedBox.square( - dimension: widget.emojiSize, - child: child, - ); - } - final imageFile = File(url); - if (!imageFile.existsSync()) { - throw PathNotFoundException(url, const OSError()); - } - return SizedBox.square( - dimension: widget.emojiSize, - child: isSvg - ? SvgPicture.file( - File(url), - width: widget.emojiSize, - height: widget.emojiSize, - ) - : Image.file( - imageFile, - fit: BoxFit.cover, - width: widget.emojiSize, - height: widget.emojiSize, - ), - ); - } - } catch (e) { - Log.error("Display widget error: $e"); - return defaultEmoji; - } - } - - Map _buildRequestHeader(UserProfilePB userProfilePB) { - final header = {}; - final token = userProfilePB.token; - try { - final decodedToken = jsonDecode(token); - header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; - } catch (e) { - Log.error('Unable to decode token: $e'); - } - return header; - } - - Future loadUserProfile() async { - if (userProfile != null) return; - if (emoji.type == FlowyIconType.custom) { - final userProfile = - (await UserBackendService.getCurrentUserProfile()).fold( - (userProfile) => userProfile, - (l) => null, - ); - if (mounted) { - setState(() { - this.userProfile = userProfile; - }); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart deleted file mode 100644 index 3d0c199ea2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/heading/heading_toolbar_item.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -final _headingData = [ - (FlowySvgs.h1_s, LocaleKeys.editor_heading1.tr()), - (FlowySvgs.h2_s, LocaleKeys.editor_heading2.tr()), - (FlowySvgs.h3_s, LocaleKeys.editor_heading3.tr()), -]; - -final headingsToolbarItem = ToolbarItem( - id: 'editor.headings', - group: 1, - isActive: onlyShowInTextType, - builder: (context, editorState, highlightColor, _, __) { - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!; - final delta = (node.delta ?? Delta()).toJson(); - int level = node.attributes[HeadingBlockKeys.level] ?? 1; - final originLevel = level; - final isHighlight = - node.type == HeadingBlockKeys.type && (level >= 1 && level <= 3); - // only supports the level 1 - 3 in the toolbar, ignore the other levels - level = level.clamp(1, 3); - - final svg = _headingData[level - 1].$1; - final message = _headingData[level - 1].$2; - - final child = FlowyTooltip( - message: message, - preferBelow: false, - child: Row( - children: [ - FlowySvg( - svg, - size: const Size.square(18), - color: isHighlight ? highlightColor : Colors.white, - ), - const HSpace(2.0), - const FlowySvg( - FlowySvgs.arrow_down_s, - size: Size.square(12), - color: Colors.grey, - ), - ], - ), - ); - return HeadingPopup( - currentLevel: isHighlight ? level : -1, - highlightColor: highlightColor, - child: child, - onLevelChanged: (newLevel) async { - // same level means cancel the heading - final type = - newLevel == originLevel && node.type == HeadingBlockKeys.type - ? ParagraphBlockKeys.type - : HeadingBlockKeys.type; - - if (type == HeadingBlockKeys.type) { - // from paragraph to heading - final newNode = node.copyWith( - type: type, - attributes: { - HeadingBlockKeys.level: newLevel, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ); - final children = node.children.map((child) => child.deepCopy()); - - final transaction = editorState.transaction; - transaction.insertNodes( - selection.start.path.next, - [newNode, ...children], - ); - transaction.deleteNode(node); - await editorState.apply(transaction); - } else { - // from heading to paragraph - await editorState.formatNode( - selection, - (node) => node.copyWith( - type: type, - attributes: { - HeadingBlockKeys.level: newLevel, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - blockComponentDelta: delta, - }, - ), - ); - } - }, - ); - }, -); - -class HeadingPopup extends StatelessWidget { - const HeadingPopup({ - super.key, - required this.currentLevel, - required this.highlightColor, - required this.onLevelChanged, - required this.child, - }); - - final int currentLevel; - final Color highlightColor; - final Function(int level) onLevelChanged; - final Widget child; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - windowPadding: const EdgeInsets.all(0), - margin: const EdgeInsets.symmetric(vertical: 2.0), - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - decorationColor: Theme.of(context).colorScheme.onTertiary, - borderRadius: BorderRadius.circular(6.0), - popupBuilder: (_) { - keepEditorFocusNotifier.increase(); - return _HeadingButtons( - currentLevel: currentLevel, - highlightColor: highlightColor, - onLevelChanged: onLevelChanged, - ); - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - }, - child: FlowyButton( - useIntrinsicWidth: true, - hoverColor: Colors.grey.withValues(alpha: 0.3), - text: child, - ), - ); - } -} - -class _HeadingButtons extends StatelessWidget { - const _HeadingButtons({ - required this.highlightColor, - required this.currentLevel, - required this.onLevelChanged, - }); - - final int currentLevel; - final Color highlightColor; - final Function(int level) onLevelChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 28, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(4), - ..._headingData.mapIndexed((index, data) { - final svg = data.$1; - final message = data.$2; - return [ - HeadingButton( - icon: svg, - tooltip: message, - onTap: () => onLevelChanged(index + 1), - isHighlight: index + 1 == currentLevel, - highlightColor: highlightColor, - ), - index != _headingData.length - 1 - ? const _Divider() - : const SizedBox.shrink(), - ]; - }).flattened, - const HSpace(4), - ], - ), - ); - } -} - -class HeadingButton extends StatelessWidget { - const HeadingButton({ - super.key, - required this.icon, - required this.tooltip, - required this.onTap, - required this.highlightColor, - required this.isHighlight, - }); - - final Color highlightColor; - final FlowySvgData icon; - final String tooltip; - final VoidCallback onTap; - final bool isHighlight; - - @override - Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - hoverColor: Colors.grey.withValues(alpha: 0.3), - onTap: onTap, - text: FlowyTooltip( - message: tooltip, - preferBelow: true, - child: FlowySvg( - icon, - size: const Size.square(18), - color: isHighlight ? highlightColor : Colors.white, - ), - ), - ); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4), - child: Container( - width: 1, - color: Colors.grey, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart new file mode 100644 index 0000000000..5da4528a3c --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/icon/icon_selector.dart @@ -0,0 +1,159 @@ +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; + +class IconSelector extends StatefulWidget { + const IconSelector({ + super.key, + required this.scrollController, + }); + + final ScrollController scrollController; + + @override + State createState() => _IconSelectorState(); +} + +class _IconSelectorState extends State { + EmojiData? emojiData; + List availableEmojis = []; + + PageStyleIconBloc? pageStyleIconBloc; + + @override + void initState() { + super.initState(); + + // load the emoji data from cache if it's available + if (kCachedEmojiData != null) { + emojiData = kCachedEmojiData; + availableEmojis = _setupAvailableEmojis(emojiData!); + } else { + EmojiData.builtIn().then( + (value) { + kCachedEmojiData = value; + setState(() { + emojiData = value; + availableEmojis = _setupAvailableEmojis(value); + }); + }, + ); + } + + pageStyleIconBloc = context.read(); + } + + @override + void dispose() { + pageStyleIconBloc?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (emojiData == null) { + return const Center(child: CircularProgressIndicator()); + } + + return RepaintBoundary( + child: BlocBuilder( + builder: (_, state) => Column( + children: [ + _buildSearchBar(context), + Expanded( + child: GridView.count( + crossAxisCount: 7, + controller: widget.scrollController, + children: [ + for (final emoji in availableEmojis) + _buildEmoji(context, emoji, state.icon), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildEmoji( + BuildContext context, + String emoji, + String? selectedEmoji, + ) { + Widget child = SizedBox.square( + dimension: 24.0, + child: Center( + child: FlowyText.emoji( + emoji, + fontSize: 24, + ), + ), + ); + + if (emoji == selectedEmoji) { + child = Center( + child: Container( + width: 40, + height: 40, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1.40, + strokeAlign: BorderSide.strokeAlignOutside, + color: Color(0xFF00BCF0), + ), + borderRadius: BorderRadius.circular(10), + ), + ), + child: child, + ), + ); + } + + return GestureDetector( + onTap: () { + context.read().add( + PageStyleIconEvent.updateIcon(emoji, true), + ); + }, + child: child, + ); + } + + List _setupAvailableEmojis(EmojiData emojiData) { + final categories = emojiData.categories; + availableEmojis = categories + .map((e) => e.emojiIds.map((e) => emojiData.getEmojiById(e))) + .expand((e) => e) + .toList(); + return availableEmojis; + } + + Widget _buildSearchBar(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 12.0, + ), + child: FlowyMobileSearchTextField( + onChanged: (keyword) { + if (emojiData == null) { + return; + } + + final filtered = emojiData!.filterByKeyword(keyword); + final availableEmojis = _setupAvailableEmojis(filtered); + + setState(() { + this.availableEmojis = availableEmojis; + }); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/common.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/common.dart deleted file mode 100644 index 24e10f229c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/common.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/widgets.dart'; - -enum CustomImageType { - local, - internal, // the images saved in self-host cloud - external; // the images linked from network, like unsplash, https://xxx/yyy/zzz.jpg - - static CustomImageType fromIntValue(int value) { - switch (value) { - case 0: - return CustomImageType.local; - case 1: - return CustomImageType.internal; - case 2: - return CustomImageType.external; - default: - throw UnimplementedError(); - } - } - - int toIntValue() { - switch (this) { - case CustomImageType.local: - return 0; - case CustomImageType.internal: - return 1; - case CustomImageType.external: - return 2; - } - } -} - -class ImageBlockData { - factory ImageBlockData.fromJson(Map json) { - return ImageBlockData( - url: json['url'] as String? ?? '', - type: CustomImageType.fromIntValue(json['type'] as int), - ); - } - - ImageBlockData({required this.url, required this.type}); - - final String url; - final CustomImageType type; - - bool get isLocal => type == CustomImageType.local; - bool get isNotInternal => type != CustomImageType.internal; - - Map toJson() { - return {'url': url, 'type': type.toIntValue()}; - } - - ImageProvider toImageProvider() { - switch (type) { - case CustomImageType.internal: - case CustomImageType.external: - return NetworkImage(url); - case CustomImageType.local: - return FileImage(File(url)); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart new file mode 100644 index 0000000000..722a79c2f5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart @@ -0,0 +1,415 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.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/unsupport_image_widget.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; +import 'package:easy_localization/easy_localization.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:string_validator/string_validator.dart'; + +const kImagePlaceholderKey = 'imagePlaceholderKey'; + +enum CustomImageType { + local, + internal, // the images saved in self-host cloud + external; // the images linked from network, like unsplash, https://xxx/yyy/zzz.jpg + + static CustomImageType fromIntValue(int value) { + switch (value) { + case 0: + return CustomImageType.local; + case 1: + return CustomImageType.internal; + case 2: + return CustomImageType.external; + default: + throw UnimplementedError(); + } + } + + int toIntValue() { + switch (this) { + case CustomImageType.local: + return 0; + case CustomImageType.internal: + return 1; + case CustomImageType.external: + return 2; + } + } +} + +class CustomImageBlockKeys { + const CustomImageBlockKeys._(); + + static const String type = 'image'; + + /// The align data of a image block. + /// + /// The value is a String. + /// left, center, right + static const String align = 'align'; + + /// The image src of a image block. + /// + /// The value is a String. + /// It can be a url or a base64 string(web). + static const String url = 'url'; + + /// The height of a image block. + /// + /// The value is a double. + static const String width = 'width'; + + /// The width of a image block. + /// + /// The value is a double. + static const String height = 'height'; + + /// The image type of a image block. + /// + /// The value is a CustomImageType enum. + static const String imageType = 'image_type'; +} + +typedef CustomImageBlockComponentMenuBuilder = Widget Function( + Node node, + CustomImageBlockComponentState state, +); + +class CustomImageBlockComponentBuilder extends BlockComponentBuilder { + CustomImageBlockComponentBuilder({ + super.configuration, + this.showMenu = false, + this.menuBuilder, + }); + + /// Whether to show the menu of this block component. + final bool showMenu; + + /// + final CustomImageBlockComponentMenuBuilder? menuBuilder; + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return CustomImageBlockComponent( + key: node.key, + node: node, + showActions: showActions(node), + configuration: configuration, + actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), + showMenu: showMenu, + menuBuilder: menuBuilder, + ); + } + + @override + bool validate(Node node) => node.delta == null && node.children.isEmpty; +} + +class CustomImageBlockComponent extends BlockComponentStatefulWidget { + const CustomImageBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + this.showMenu = false, + this.menuBuilder, + }); + + /// Whether to show the menu of this block component. + final bool showMenu; + + final CustomImageBlockComponentMenuBuilder? menuBuilder; + + @override + State createState() => + CustomImageBlockComponentState(); +} + +class CustomImageBlockComponentState extends State + with SelectableMixin, BlockComponentConfigurable { + @override + BlockComponentConfiguration get configuration => widget.configuration; + + @override + Node get node => widget.node; + + final imageKey = GlobalKey(); + RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; + + late final editorState = Provider.of(context, listen: false); + + final showActionsNotifier = ValueNotifier(false); + + bool alwaysShowMenu = false; + + @override + Widget build(BuildContext context) { + final node = widget.node; + final attributes = node.attributes; + final src = attributes[CustomImageBlockKeys.url]; + + final alignment = AlignmentExtension.fromString( + attributes[CustomImageBlockKeys.align] ?? 'center', + ); + final width = attributes[CustomImageBlockKeys.width]?.toDouble() ?? + MediaQuery.of(context).size.width; + final height = attributes[CustomImageBlockKeys.height]?.toDouble(); + final rawImageType = attributes[CustomImageBlockKeys.imageType] ?? 0; + final imageType = CustomImageType.fromIntValue(rawImageType); + + final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey]; + Widget child; + if (src.isEmpty) { + child = ImagePlaceholder( + key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, + node: node, + ); + } else if (imageType != CustomImageType.internal && + !_checkIfURLIsValid(src)) { + child = const UnSupportImageWidget(); + } else { + child = ResizableImage( + src: src, + width: width, + height: height, + editable: editorState.editable, + alignment: alignment, + type: imageType, + onResize: (width) { + final transaction = editorState.transaction + ..updateNode(node, { + CustomImageBlockKeys.width: width, + }); + editorState.apply(transaction); + }, + ); + } + + if (PlatformExtension.isDesktopOrWeb) { + child = BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + blockColor: editorState.editorStyle.selectionColor, + supportTypes: const [ + BlockSelectionType.block, + ], + child: Padding( + key: imageKey, + padding: padding, + child: child, + ), + ); + } else { + child = Padding( + key: imageKey, + padding: padding, + child: child, + ); + } + + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + // show a hover menu on desktop or web + if (PlatformExtension.isDesktopOrWeb) { + if (widget.showMenu && widget.menuBuilder != null) { + child = MouseRegion( + onEnter: (_) => showActionsNotifier.value = true, + onExit: (_) { + if (!alwaysShowMenu) { + showActionsNotifier.value = false; + } + }, + hitTestBehavior: HitTestBehavior.opaque, + opaque: false, + child: ValueListenableBuilder( + valueListenable: showActionsNotifier, + builder: (context, value, child) { + final url = node.attributes[CustomImageBlockKeys.url]; + return Stack( + children: [ + BlockSelectionContainer( + node: node, + delegate: this, + listenable: editorState.selectionNotifier, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, + child: child!, + ), + if (value && url.isNotEmpty == true) + widget.menuBuilder!( + widget.node, + this, + ), + ], + ); + }, + child: child, + ), + ); + } + } else { + // show a fixed menu on mobile + child = MobileBlockActionButtons( + showThreeDots: false, + node: node, + editorState: editorState, + extendActionWidgets: _buildExtendActionWidgets(context), + child: child, + ); + } + + return child; + } + + @override + Position start() => Position(path: widget.node.path); + + @override + Position end() => Position(path: widget.node.path, offset: 1); + + @override + Position getPositionInOffset(Offset start) => end(); + + @override + bool get shouldCursorBlink => false; + + @override + CursorStyle get cursorStyle => CursorStyle.cover; + + @override + Rect getBlockRect({ + bool shiftWithBaseOffset = false, + }) { + final imageBox = imageKey.currentContext?.findRenderObject(); + if (imageBox is RenderBox) { + return Offset.zero & imageBox.size; + } + return Rect.zero; + } + + @override + Rect? getCursorRectInPosition( + Position position, { + bool shiftWithBaseOffset = false, + }) { + final rects = getRectsInSelection(Selection.collapsed(position)); + return rects.firstOrNull; + } + + @override + List getRectsInSelection( + Selection selection, { + bool shiftWithBaseOffset = false, + }) { + if (_renderBox == null) { + return []; + } + final parentBox = context.findRenderObject(); + final imageBox = imageKey.currentContext?.findRenderObject(); + if (parentBox is RenderBox && imageBox is RenderBox) { + return [ + imageBox.localToGlobal(Offset.zero, ancestor: parentBox) & + imageBox.size, + ]; + } + return [Offset.zero & _renderBox!.size]; + } + + @override + Selection getSelectionInRange(Offset start, Offset end) => Selection.single( + path: widget.node.path, + startOffset: 0, + endOffset: 1, + ); + + @override + Offset localToGlobal( + Offset offset, { + bool shiftWithBaseOffset = false, + }) => + _renderBox!.localToGlobal(offset); + + // only used on mobile platform + List _buildExtendActionWidgets(BuildContext context) { + final String url = widget.node.attributes[CustomImageBlockKeys.url]; + if (!_checkIfURLIsValid(url)) { + return []; + } + + return [ + // disable the copy link button if the image is hosted on appflowy cloud + // because the url needs the verification token to be accessible + if (!url.isAppFlowyCloudUrl) + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.editor_copyLink.tr(), + leftIcon: const FlowySvg( + FlowySvgs.m_field_copy_s, + ), + onTap: () async { + context.pop(); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + ); + await getIt().setPlainText(url); + }, + ), + FlowyOptionTile.text( + showTopBorder: false, + text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(), + leftIcon: const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(20), + ), + onTap: () async { + context.pop(); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + ); + await getIt().setPlainText(url); + }, + ), + ]; + } + + bool _checkIfURLIsValid(dynamic url) { + if (url is! String) { + return false; + } + + if (url.isEmpty) { + return false; + } + + if (!isURL(url) && !File(url).existsSync()) { + return false; + } + + return true; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart deleted file mode 100644 index 7f0105134d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; -import 'package:appflowy/shared/custom_image_cache_manager.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; -import 'package:saver_gallery/saver_gallery.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../common.dart'; - -const kImagePlaceholderKey = 'imagePlaceholderKey'; - -class CustomImageBlockKeys { - const CustomImageBlockKeys._(); - - static const String type = 'image'; - - /// The align data of a image block. - /// - /// The value is a String. - /// left, center, right - static const String align = 'align'; - - /// The image src of a image block. - /// - /// The value is a String. - /// It can be a url or a base64 string(web). - static const String url = 'url'; - - /// The height of a image block. - /// - /// The value is a double. - static const String width = 'width'; - - /// The width of a image block. - /// - /// The value is a double. - static const String height = 'height'; - - /// The image type of a image block. - /// - /// The value is a CustomImageType enum. - static const String imageType = 'image_type'; -} - -Node customImageNode({ - required String url, - String align = 'center', - double? height, - double? width, - CustomImageType type = CustomImageType.local, -}) { - return Node( - type: CustomImageBlockKeys.type, - attributes: { - CustomImageBlockKeys.url: url, - CustomImageBlockKeys.align: align, - CustomImageBlockKeys.height: height, - CustomImageBlockKeys.width: width, - CustomImageBlockKeys.imageType: type.toIntValue(), - }, - ); -} - -typedef CustomImageBlockComponentMenuBuilder = Widget Function( - Node node, - CustomImageBlockComponentState state, - ValueNotifier imageStateNotifier, -); - -class CustomImageBlockComponentBuilder extends BlockComponentBuilder { - CustomImageBlockComponentBuilder({ - super.configuration, - this.showMenu = false, - this.menuBuilder, - }); - - /// Whether to show the menu of this block component. - final bool showMenu; - - /// - final CustomImageBlockComponentMenuBuilder? menuBuilder; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return CustomImageBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - showMenu: showMenu, - menuBuilder: menuBuilder, - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; -} - -class CustomImageBlockComponent extends BlockComponentStatefulWidget { - const CustomImageBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - this.showMenu = false, - this.menuBuilder, - }); - - /// Whether to show the menu of this block component. - final bool showMenu; - - final CustomImageBlockComponentMenuBuilder? menuBuilder; - - @override - State createState() => - CustomImageBlockComponentState(); -} - -class CustomImageBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - final imageKey = GlobalKey(); - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - late final editorState = Provider.of(context, listen: false); - - final showActionsNotifier = ValueNotifier(false); - final imageStateNotifier = - ValueNotifier(ResizableImageState.loading); - - bool alwaysShowMenu = false; - - @override - Widget build(BuildContext context) { - final node = widget.node; - final attributes = node.attributes; - final src = attributes[CustomImageBlockKeys.url]; - - final alignment = AlignmentExtension.fromString( - attributes[CustomImageBlockKeys.align] ?? 'center', - ); - final width = attributes[CustomImageBlockKeys.width]?.toDouble() ?? - MediaQuery.of(context).size.width; - final height = attributes[CustomImageBlockKeys.height]?.toDouble(); - final rawImageType = attributes[CustomImageBlockKeys.imageType] ?? 0; - final imageType = CustomImageType.fromIntValue(rawImageType); - - final imagePlaceholderKey = node.extraInfos?[kImagePlaceholderKey]; - Widget child; - if (src.isEmpty) { - child = ImagePlaceholder( - key: imagePlaceholderKey is GlobalKey ? imagePlaceholderKey : null, - node: node, - ); - } else if (imageType != CustomImageType.internal && - !_checkIfURLIsValid(src)) { - child = const UnsupportedImageWidget(); - } else { - child = ResizableImage( - src: src, - width: width, - height: height, - editable: editorState.editable, - alignment: alignment, - type: imageType, - onStateChange: (state) => imageStateNotifier.value = state, - onDoubleTap: () => showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfilePB, - imageProvider: AFBlockImageProvider( - images: [ImageBlockData(url: src, type: imageType)], - onDeleteImage: (_) async { - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply(transaction); - }, - ), - ), - ), - onResize: (width) { - final transaction = editorState.transaction - ..updateNode(node, {CustomImageBlockKeys.width: width}); - editorState.apply(transaction); - }, - ); - } - - child = Padding( - padding: padding, - child: RepaintBoundary( - key: imageKey, - child: child, - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, - supportTypes: const [BlockSelectionType.block], - child: child, - ); - } - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - // show a hover menu on desktop or web - if (UniversalPlatform.isDesktopOrWeb) { - if (widget.showMenu && widget.menuBuilder != null) { - child = MouseRegion( - onEnter: (_) => showActionsNotifier.value = true, - onExit: (_) { - if (!alwaysShowMenu) { - showActionsNotifier.value = false; - } - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (_, value, child) { - return Stack( - children: [ - editorState.editable - ? BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: - editorState.editorStyle.selectionColor, - child: child!, - ) - : child!, - if (value) - widget.menuBuilder!(widget.node, this, imageStateNotifier), - ], - ); - }, - child: child, - ), - ); - } - } else { - // show a fixed menu on mobile - child = MobileBlockActionButtons( - showThreeDots: false, - node: node, - editorState: editorState, - extendActionWidgets: _buildExtendActionWidgets(context), - child: child, - ); - } - - return child; - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - final imageBox = imageKey.currentContext?.findRenderObject(); - if (imageBox is RenderBox) { - return padding.topLeft & imageBox.size; - } - return Rect.zero; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection(Selection.collapsed(position)); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final imageBox = imageKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && imageBox is RenderBox) { - return [ - imageBox.localToGlobal(Offset.zero, ancestor: parentBox) & - imageBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: widget.node.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal( - Offset offset, { - bool shiftWithBaseOffset = false, - }) => - _renderBox!.localToGlobal(offset); - - // only used on mobile platform - List _buildExtendActionWidgets(BuildContext context) { - final String url = widget.node.attributes[CustomImageBlockKeys.url]; - if (!_checkIfURLIsValid(url)) { - return []; - } - - return [ - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.editor_copy.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_field_copy_s, - ), - onTap: () async { - context.pop(); - showToastNotification( - message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), - ); - await getIt().setPlainText(url); - }, - ), - FlowyOptionTile.text( - showTopBorder: false, - text: LocaleKeys.document_imageBlock_saveImageToGallery.tr(), - leftIcon: const FlowySvg( - FlowySvgs.image_placeholder_s, - size: Size.square(20), - ), - onTap: () async { - context.pop(); - // save the image to the photo library - await _saveImageToGallery(url); - }, - ), - ]; - } - - bool _checkIfURLIsValid(dynamic url) { - if (url is! String) { - return false; - } - - if (url.isEmpty) { - return false; - } - - if (!isURL(url) && !File(url).existsSync()) { - return false; - } - - return true; - } - - Future _saveImageToGallery(String url) async { - final permission = await PermissionChecker.checkPhotoPermission(context); - if (!permission) { - return; - } - - final imageFile = await CustomImageCacheManager().getSingleFile(url); - if (imageFile.existsSync()) { - final result = await SaverGallery.saveImage( - imageFile.readAsBytesSync(), - fileName: imageFile.basename, - skipIfExists: false, - ); - if (mounted) { - showToastNotification( - message: result.isSuccess - ? LocaleKeys.document_imageBlock_successToAddImageToGallery.tr() - : LocaleKeys.document_imageBlock_failedToAddImageToGallery.tr(), - ); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart deleted file mode 100644 index d11d943066..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/image_menu.dart +++ /dev/null @@ -1,323 +0,0 @@ -import 'dart:ui'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.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:flowy_infra_ui/widget/ignore_parent_gesture.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -class ImageMenu extends StatefulWidget { - const ImageMenu({ - super.key, - required this.node, - required this.state, - required this.imageStateNotifier, - }); - - final Node node; - final CustomImageBlockComponentState state; - final ValueNotifier imageStateNotifier; - - @override - State createState() => _ImageMenuState(); -} - -class _ImageMenuState extends State { - late final String? url = widget.node.attributes[CustomImageBlockKeys.url]; - - @override - Widget build(BuildContext context) { - final isPlaceholder = url == null || url!.isEmpty; - final theme = Theme.of(context); - return ValueListenableBuilder( - valueListenable: widget.imageStateNotifier, - builder: (_, state, child) { - if (state == ResizableImageState.loading && !isPlaceholder) { - return const SizedBox.shrink(); - } - - return Container( - height: 32, - decoration: BoxDecoration( - color: theme.cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(4.0), - ), - child: Row( - children: [ - const HSpace(4), - if (!isPlaceholder) ...[ - MenuBlockButton( - tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(), - iconData: FlowySvgs.full_view_s, - onTap: openFullScreen, - ), - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.editor_copy.tr(), - iconData: FlowySvgs.copy_s, - onTap: copyImageLink, - ), - const HSpace(4), - ], - if (widget.state.editorState.editable) ...[ - if (!isPlaceholder) ...[ - _ImageAlignButton(node: widget.node, state: widget.state), - const _Divider(), - ], - MenuBlockButton( - tooltip: LocaleKeys.button_delete.tr(), - iconData: FlowySvgs.trash_s, - onTap: deleteImage, - ), - const HSpace(4), - ], - ], - ), - ); - }, - ); - } - - Future copyImageLink() async { - if (url != null) { - // paste the image url and the image data - final imageData = await captureImage(); - - try { - // /image - await getIt().setData( - ClipboardServiceData( - plainText: url!, - image: ('png', imageData), - ), - ); - - if (mounted) { - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } - } catch (e) { - if (mounted) { - showToastNotification( - message: LocaleKeys.message_copy_fail.tr(), - type: ToastificationType.error, - ); - } - } - } - } - - Future deleteImage() async { - final node = widget.node; - final editorState = context.read(); - final transaction = editorState.transaction; - transaction.deleteNode(node); - transaction.afterSelection = null; - await editorState.apply(transaction); - } - - void openFullScreen() { - showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read()?.userProfile ?? - context.read().state.userProfilePB, - imageProvider: AFBlockImageProvider( - images: [ - ImageBlockData( - url: url!, - type: CustomImageType.fromIntValue( - widget.node.attributes[CustomImageBlockKeys.imageType] ?? 2, - ), - ), - ], - onDeleteImage: widget.state.editorState.editable - ? (_) async { - final transaction = widget.state.editorState.transaction; - transaction.deleteNode(widget.node); - await widget.state.editorState.apply(transaction); - } - : null, - ), - ), - ); - } - - Future captureImage() async { - final boundary = widget.state.imageKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; - final image = await boundary?.toImage(); - final byteData = await image?.toByteData(format: ImageByteFormat.png); - if (byteData == null) { - return Uint8List(0); - } - return byteData.buffer.asUint8List(); - } -} - -class _ImageAlignButton extends StatefulWidget { - const _ImageAlignButton({required this.node, required this.state}); - - final Node node; - final CustomImageBlockComponentState state; - - @override - State<_ImageAlignButton> createState() => _ImageAlignButtonState(); -} - -const _interceptorKey = 'image-align'; - -class _ImageAlignButtonState extends State<_ImageAlignButton> { - final gestureInterceptor = SelectionGestureInterceptor( - key: _interceptorKey, - canTap: (details) => false, - ); - - String get align => - widget.node.attributes[CustomImageBlockKeys.align] ?? centerAlignmentKey; - final popoverController = PopoverController(); - late final EditorState editorState; - - @override - void initState() { - super.initState(); - editorState = context.read(); - } - - @override - void dispose() { - allowMenuClose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return IgnoreParentGestureWidget( - child: AppFlowyPopover( - onClose: allowMenuClose, - controller: popoverController, - windowPadding: const EdgeInsets.all(0), - margin: const EdgeInsets.all(0), - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - child: MenuBlockButton( - tooltip: LocaleKeys.document_plugins_optionAction_align.tr(), - iconData: iconFor(align), - ), - popupBuilder: (_) { - preventMenuClose(); - return _AlignButtons(onAlignChanged: onAlignChanged); - }, - ), - ); - } - - void onAlignChanged(String align) { - popoverController.close(); - - final transaction = editorState.transaction; - transaction.updateNode(widget.node, {CustomImageBlockKeys.align: align}); - editorState.apply(transaction); - - allowMenuClose(); - } - - void preventMenuClose() { - widget.state.alwaysShowMenu = true; - editorState.service.selectionService.registerGestureInterceptor( - gestureInterceptor, - ); - } - - void allowMenuClose() { - widget.state.alwaysShowMenu = false; - editorState.service.selectionService.unregisterGestureInterceptor( - _interceptorKey, - ); - } - - FlowySvgData iconFor(String alignment) { - switch (alignment) { - case rightAlignmentKey: - return FlowySvgs.align_right_s; - case centerAlignmentKey: - return FlowySvgs.align_center_s; - case leftAlignmentKey: - default: - return FlowySvgs.align_left_s; - } - } -} - -class _AlignButtons extends StatelessWidget { - const _AlignButtons({required this.onAlignChanged}); - - final Function(String align) onAlignChanged; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.document_plugins_optionAction_left, - iconData: FlowySvgs.align_left_s, - onTap: () => onAlignChanged(leftAlignmentKey), - ), - const _Divider(), - MenuBlockButton( - tooltip: LocaleKeys.document_plugins_optionAction_center, - iconData: FlowySvgs.align_center_s, - onTap: () => onAlignChanged(centerAlignmentKey), - ), - const _Divider(), - MenuBlockButton( - tooltip: LocaleKeys.document_plugins_optionAction_right, - iconData: FlowySvgs.align_right_s, - onTap: () => onAlignChanged(rightAlignmentKey), - ), - const HSpace(4), - ], - ), - ); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Container(width: 1, color: Colors.grey), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart new file mode 100644 index 0000000000..d34a4fc3a8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/patterns/common_patterns.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class EmbedImageUrlWidget extends StatefulWidget { + const EmbedImageUrlWidget({ + super.key, + required this.onSubmit, + }); + + final void Function(String url) onSubmit; + + @override + State createState() => _EmbedImageUrlWidgetState(); +} + +class _EmbedImageUrlWidgetState extends State { + bool isUrlValid = true; + String inputText = ''; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FlowyTextField( + hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), + onChanged: (value) => inputText = value, + onEditingComplete: submit, + ), + if (!isUrlValid) ...[ + const VSpace(8), + FlowyText( + LocaleKeys.document_plugins_cover_invalidImageUrl.tr(), + color: Theme.of(context).colorScheme.error, + ), + ], + const VSpace(8), + SizedBox( + width: 160, + child: FlowyButton( + showDefaultBoxDecorationOnMobile: true, + margin: const EdgeInsets.all(8.0), + text: FlowyText( + LocaleKeys.document_imageBlock_embedLink_label.tr(), + textAlign: TextAlign.center, + ), + onTap: submit, + ), + ), + ], + ); + } + + void submit() { + if (checkUrlValidity(inputText)) { + return widget.onSubmit(inputText); + } + + setState(() => isUrlValid = false); + } + + bool checkUrlValidity(String url) => imgUrlRegex.hasMatch(url); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart new file mode 100644 index 0000000000..45f5b78507 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart @@ -0,0 +1,40 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; + +class ImagePickerPage extends StatefulWidget { + const ImagePickerPage({ + super.key, + // required this.onSelected, + }); + + // final void Function(EmojiPickerResult) onSelected; + + @override + State createState() => _ImagePickerPageState(); +} + +class _ImagePickerPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + title: FlowyText.semibold( + LocaleKeys.titleBar_pageIcon.tr(), + fontSize: 14.0, + ), + leading: const AppBarBackButton(), + ), + body: SafeArea( + child: UploadImageMenu( + onSubmitted: (_) {}, + onUpload: (_) {}, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart new file mode 100644 index 0000000000..ca765bc0ed --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_menu.dart @@ -0,0 +1,255 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/align_toolbar_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; + +class ImageMenu extends StatefulWidget { + const ImageMenu({ + super.key, + required this.node, + required this.state, + }); + + final Node node; + final CustomImageBlockComponentState state; + + @override + State createState() => _ImageMenuState(); +} + +class _ImageMenuState extends State { + late final String? url = widget.node.attributes[ImageBlockKeys.url]; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + height: 32, + decoration: BoxDecoration( + color: theme.cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + children: [ + const HSpace(4), + // disable the copy link button if the image is hosted on appflowy cloud + // because the url needs the verification token to be accessible + if (!(url?.isAppFlowyCloudUrl ?? false)) ...[ + MenuBlockButton( + tooltip: LocaleKeys.editor_copyLink.tr(), + iconData: FlowySvgs.copy_s, + onTap: copyImageLink, + ), + const HSpace(4), + ], + _ImageAlignButton( + node: widget.node, + state: widget.state, + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.button_delete.tr(), + iconData: FlowySvgs.delete_s, + onTap: deleteImage, + ), + const HSpace(4), + ], + ), + ); + } + + void copyImageLink() { + if (url != null) { + Clipboard.setData(ClipboardData(text: url!)); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), + ); + } + } + + Future deleteImage() async { + final node = widget.node; + final editorState = context.read(); + final transaction = editorState.transaction; + transaction.deleteNode(node); + transaction.afterSelection = null; + await editorState.apply(transaction); + } +} + +class _ImageAlignButton extends StatefulWidget { + const _ImageAlignButton({ + required this.node, + required this.state, + }); + + final Node node; + final CustomImageBlockComponentState state; + + @override + State<_ImageAlignButton> createState() => _ImageAlignButtonState(); +} + +const interceptorKey = 'image-align'; + +class _ImageAlignButtonState extends State<_ImageAlignButton> { + final gestureInterceptor = SelectionGestureInterceptor( + key: interceptorKey, + canTap: (details) => false, + ); + + String get align => + widget.node.attributes[ImageBlockKeys.align] ?? centerAlignmentKey; + final popoverController = PopoverController(); + late final EditorState editorState; + + @override + void initState() { + super.initState(); + + editorState = context.read(); + } + + @override + void dispose() { + allowMenuClose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return IgnoreParentGestureWidget( + child: AppFlowyPopover( + onClose: allowMenuClose, + controller: popoverController, + windowPadding: const EdgeInsets.all(0), + margin: const EdgeInsets.all(0), + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 10), + child: MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_align.tr(), + iconData: iconFor(align), + ), + popupBuilder: (_) { + preventMenuClose(); + return _AlignButtons( + onAlignChanged: onAlignChanged, + ); + }, + ), + ); + } + + void onAlignChanged(String align) { + popoverController.close(); + + final transaction = editorState.transaction; + transaction.updateNode(widget.node, { + ImageBlockKeys.align: align, + }); + editorState.apply(transaction); + + allowMenuClose(); + } + + void preventMenuClose() { + widget.state.alwaysShowMenu = true; + editorState.service.selectionService.registerGestureInterceptor( + gestureInterceptor, + ); + } + + void allowMenuClose() { + widget.state.alwaysShowMenu = false; + editorState.service.selectionService.unregisterGestureInterceptor( + interceptorKey, + ); + } + + FlowySvgData iconFor(String alignment) { + switch (alignment) { + case rightAlignmentKey: + return FlowySvgs.align_right_s; + case centerAlignmentKey: + return FlowySvgs.align_center_s; + case leftAlignmentKey: + default: + return FlowySvgs.align_left_s; + } + } +} + +class _AlignButtons extends StatelessWidget { + const _AlignButtons({ + required this.onAlignChanged, + }); + + final Function(String align) onAlignChanged; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_left, + iconData: FlowySvgs.align_left_s, + onTap: () => onAlignChanged(leftAlignmentKey), + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_center, + iconData: FlowySvgs.align_center_s, + onTap: () => onAlignChanged(centerAlignmentKey), + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_optionAction_right, + iconData: FlowySvgs.align_right_s, + onTap: () => onAlignChanged(rightAlignmentKey), + ), + const HSpace(4), + ], + ), + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 1, + color: Colors.grey, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart index b1c6c94213..5ea7a56c40 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_picker_screen.dart @@ -1,40 +1,13 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/flowy_image_picker.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; - class MobileImagePickerScreen extends StatelessWidget { const MobileImagePickerScreen({super.key}); static const routeName = '/image_picker'; - @override - Widget build(BuildContext context) => const ImagePickerPage(); -} - -class ImagePickerPage extends StatelessWidget { - const ImagePickerPage({super.key}); - @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - titleSpacing: 0, - title: FlowyText.semibold( - LocaleKeys.titleBar_pageIcon.tr(), - fontSize: 14.0, - ), - leading: const AppBarBackButton(), - ), - body: SafeArea( - child: UploadImageMenu( - onSubmitted: (_) {}, - onUpload: (_) {}, - ), - ), - ); + return const ImagePickerPage(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index df975de731..5d51c1b390 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -1,37 +1,35 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; -import 'package:desktop_drop/desktop_drop.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log, UploadImageMenu; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:http/http.dart'; import 'package:path/path.dart' as p; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; class ImagePlaceholder extends StatefulWidget { - const ImagePlaceholder({super.key, required this.node}); + const ImagePlaceholder({ + super.key, + required this.node, + }); final Node node; @@ -47,20 +45,12 @@ class ImagePlaceholderState extends State { bool showLoading = false; String? errorMessage; - bool isDraggingFiles = false; - @override Widget build(BuildContext context) { final Widget child = DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), - border: isDraggingFiles - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ) - : null, ), child: FlowyHover( style: HoverStyle( @@ -71,10 +61,9 @@ class ImagePlaceholderState extends State { child: Row( children: [ const HSpace(10), - FlowySvg( - FlowySvgs.slash_menu_icon_image_s, - size: const Size.square(24), - color: Theme.of(context).hintColor, + const FlowySvg( + FlowySvgs.image_placeholder_s, + size: Size.square(24), ), const HSpace(10), ..._buildTrailing(context), @@ -84,7 +73,7 @@ class ImagePlaceholderState extends State { ), ); - if (UniversalPlatform.isDesktopOrWeb) { + if (PlatformExtension.isDesktopOrWeb) { return AppFlowyPopover( controller: controller, direction: PopoverDirection.bottomWithCenterAligned, @@ -96,24 +85,18 @@ class ImagePlaceholderState extends State { clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (context) { return UploadImageMenu( - allowMultipleImages: true, limitMaximumImageSize: !_isLocalMode(), supportTypes: const [ UploadImageType.local, UploadImageType.url, UploadImageType.unsplash, + UploadImageType.openAI, + UploadImageType.stabilityAI, ], - onSelectedLocalImages: (files) { + onSelectedLocalImage: (path) { controller.close(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - final List items = List.from( - files - .where((file) => file.path.isNotEmpty) - .map((file) => file.path), - ); - if (items.isNotEmpty) { - await insertMultipleLocalImages(items); - } + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + await insertLocalImage(path); }); }, onSelectedAIImage: (url) { @@ -130,27 +113,7 @@ class ImagePlaceholderState extends State { }, ); }, - child: DropTarget( - onDragEntered: (_) => setState(() => isDraggingFiles = true), - onDragExited: (_) => setState(() => isDraggingFiles = false), - onDragDone: (details) { - // Only accept files where the mimetype is an image, - // otherwise we assume it's a file we cannot display. - final imageFiles = details.files - .where( - (file) => - file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), - ) - .toList(); - final paths = imageFiles.map((file) => file.path).toList(); - - WidgetsBinding.instance.addPostFrameCallback( - (_) async => insertMultipleLocalImages(paths), - ); - }, - child: child, - ), + child: child, ); } else { return MobileBlockActionButtons( @@ -170,11 +133,8 @@ class ImagePlaceholderState extends State { List _buildTrailing(BuildContext context) { if (errorMessage != null) { return [ - Flexible( - child: FlowyText( - '${LocaleKeys.document_plugins_image_imageUploadFailed.tr()}: ${errorMessage!}', - maxLines: 3, - ), + FlowyText( + '${LocaleKeys.document_plugins_image_imageUploadFailed.tr()}: ${errorMessage!}', ), ]; } else if (showLoading) { @@ -187,22 +147,15 @@ class ImagePlaceholderState extends State { ]; } else { return [ - Flexible( - child: FlowyText( - UniversalPlatform.isDesktop - ? isDraggingFiles - ? LocaleKeys.document_plugins_image_dropImageToInsert.tr() - : LocaleKeys.document_plugins_image_addAnImageDesktop.tr() - : LocaleKeys.document_plugins_image_addAnImageMobile.tr(), - color: Theme.of(context).hintColor, - ), + FlowyText( + LocaleKeys.document_plugins_image_addAnImage.tr(), ), ]; } } void showUploadImageMenu() { - if (UniversalPlatform.isDesktopOrWeb) { + if (PlatformExtension.isDesktopOrWeb) { controller.show(); } else { final isLocalMode = _isLocalMode(); @@ -226,15 +179,9 @@ class ImagePlaceholderState extends State { UploadImageType.url, UploadImageType.unsplash, ], - onSelectedLocalImages: (files) async { + onSelectedLocalImage: (path) async { context.pop(); - - final items = files - .where((file) => file.path.isNotEmpty) - .map((file) => file.path) - .toList(); - - await insertMultipleLocalImages(items); + await insertLocalImage(path); }, onSelectedAIImage: (url) async { context.pop(); @@ -251,106 +198,73 @@ class ImagePlaceholderState extends State { } } - Future insertMultipleLocalImages(List urls) async { + Future insertLocalImage(String? url) async { controller.close(); - if (urls.isEmpty) { + if (url == null || url.isEmpty) { return; } - setState(() { - showLoading = true; - errorMessage = null; + final transaction = editorState.transaction; + + String? path; + String? errorMessage; + CustomImageType imageType = CustomImageType.local; + + // if the user is using local authenticator, we need to save the image to local storage + if (_isLocalMode()) { + // don't limit the image size for local mode. + path = await saveImageToLocalStorage(url); + } else { + // else we should save the image to cloud storage + setState(() { + showLoading = true; + this.errorMessage = null; + }); + (path, errorMessage) = await saveImageToCloudStorage(url); + setState(() { + showLoading = false; + this.errorMessage = errorMessage; + }); + imageType = CustomImageType.internal; + } + + if (mounted && path == null) { + showSnackBarMessage( + context, + errorMessage == null + ? LocaleKeys.document_imageBlock_error_invalidImage.tr() + : ': $errorMessage', + ); + setState(() { + this.errorMessage = errorMessage; + }); + return; + } + + transaction.updateNode(widget.node, { + CustomImageBlockKeys.url: path, + CustomImageBlockKeys.imageType: imageType.toIntValue(), }); - bool hasError = false; - - if (_isLocalMode()) { - final first = urls.removeAt(0); - final firstPath = await saveImageToLocalStorage(first); - final transaction = editorState.transaction; - transaction.updateNode(widget.node, { - CustomImageBlockKeys.url: firstPath, - CustomImageBlockKeys.imageType: CustomImageType.local.toIntValue(), - }); - - if (urls.isNotEmpty) { - // Create new nodes for the rest of the images: - final paths = await Future.wait(urls.map(saveImageToLocalStorage)); - paths.removeWhere((url) => url == null || url.isEmpty); - - transaction.insertNodes( - widget.node.path.next, - paths.map((url) => customImageNode(url: url!)).toList(), - ); - } - - await editorState.apply(transaction); - } else { - final transaction = editorState.transaction; - - bool isFirst = true; - for (final url in urls) { - // Upload to cloud - final (path, error) = await saveImageToCloudStorage( - url, - context.read().documentId, - ); - - if (error != null) { - hasError = true; - - if (isFirst) { - setState(() => errorMessage = error); - } - - continue; - } - - if (path != null) { - if (isFirst) { - isFirst = false; - transaction.updateNode(widget.node, { - CustomImageBlockKeys.url: path, - CustomImageBlockKeys.imageType: - CustomImageType.internal.toIntValue(), - }); - } else { - transaction.insertNode( - widget.node.path.next, - customImageNode( - url: path, - type: CustomImageType.internal, - ), - ); - } - } - } - - await editorState.apply(transaction); - } - - setState(() => showLoading = false); - - if (hasError && mounted) { - showSnapBar( - context, - LocaleKeys.document_imageBlock_error_multipleImagesFailed.tr(), - ); - } + await editorState.apply(transaction); } Future insertAIImage(String url) async { if (url.isEmpty || !isURL(url)) { // show error - return showSnackBarMessage( + showSnackBarMessage( context, LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); + return; } final path = await getIt().getPath(); - final imagePath = p.join(path, 'images'); + final imagePath = p.join( + path, + 'images', + ); try { // create the directory if not exists final directory = Directory(imagePath); @@ -365,7 +279,7 @@ class ImagePlaceholderState extends State { final response = await get(uri); await File(copyToPath).writeAsBytes(response.bodyBytes); - await insertMultipleLocalImages([copyToPath]); + await insertLocalImage(copyToPath); await File(copyToPath).delete(); } catch (e) { Log.error('cannot save image file', e); @@ -375,16 +289,16 @@ class ImagePlaceholderState extends State { Future insertNetworkImage(String url) async { if (url.isEmpty || !isURL(url)) { // show error - return showSnackBarMessage( + showSnackBarMessage( context, LocaleKeys.document_imageBlock_error_invalidImage.tr(), ); + return; } final transaction = editorState.transaction; transaction.updateNode(widget.node, { - CustomImageBlockKeys.url: url, - CustomImageBlockKeys.imageType: CustomImageType.external.toIntValue(), + ImageBlockKeys.url: url, }); await editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart index 5a30a4dda5..c42e4f8147 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_selection_menu.dart @@ -1,100 +1,64 @@ -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/document/presentation/editor_plugins/base/selectable_svg_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/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/multi_image_block_component/multi_image_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:flutter/material.dart'; final customImageMenuItem = SelectionMenuItem( getName: () => AppFlowyEditorL10n.current.image, - icon: (_, isSelected, style) => SelectionMenuIconWidget( + icon: (editorState, isSelected, style) => SelectionMenuIconWidget( name: 'image', isSelected: isSelected, style: style, ), keywords: ['image', 'picture', 'img', 'photo'], - handler: (editorState, _, __) async { + handler: (editorState, menuService, context) async { // use the key to retrieve the state of the image block to show the popover automatically final imagePlaceholderKey = GlobalKey(); await editorState.insertEmptyImageBlock(imagePlaceholderKey); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { imagePlaceholderKey.currentState?.controller.show(); }); }, ); -final multiImageMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_plugins_photoGallery_name.tr(), - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.image_s, - size: const Size.square(16.0), - isSelected: isSelected, - style: style, - ), - keywords: [ - LocaleKeys.document_plugins_photoGallery_imageKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_imageGalleryKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_photoKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_photoBrowserKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_galleryKeyword.tr(), - ], - handler: (editorState, _, __) async { - final imagePlaceholderKey = GlobalKey(); - await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey); - WidgetsBinding.instance.addPostFrameCallback( - (_) => imagePlaceholderKey.currentState?.controller.show(), - ); - }, -); - extension InsertImage on EditorState { Future insertEmptyImageBlock(GlobalKey key) async { final selection = this.selection; if (selection == null || !selection.isCollapsed) { return; } - final path = selection.end.path; - final node = getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { + final node = getNodeAtPath(selection.end.path); + if (node == null) { return; } final emptyImage = imageNode(url: '') - ..extraInfos = {kImagePlaceholderKey: key}; - - final insertedPath = delta.isEmpty ? path : path.next; - final transaction = this.transaction - ..insertNode(insertedPath, emptyImage) - ..insertNode(insertedPath, paragraphNode()) - ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); - - return apply(transaction); - } - - Future insertEmptyMultiImageBlock(GlobalKey key) async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; + ..extraInfos = { + kImagePlaceholderKey: key, + }; + final transaction = this.transaction; + // if the current node is empty paragraph, replace it with image node + if (node.type == ParagraphBlockKeys.type && + (node.delta?.isEmpty ?? false)) { + transaction + ..insertNode( + node.path, + emptyImage, + ) + ..deleteNode(node); + } else { + transaction.insertNode( + node.path.next, + emptyImage, + ); } - final path = selection.end.path; - final node = getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final emptyBlock = multiImageNode() - ..extraInfos = {kMultiImagePlaceholderKey: key}; - final insertedPath = delta.isEmpty ? path : path.next; - final transaction = this.transaction - ..insertNode(insertedPath, emptyBlock) - ..insertNode(insertedPath, paragraphNode()) - ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); + transaction.afterSelection = Selection.collapsed( + Position( + path: node.path.next, + ), + ); + transaction.selectionExtraInfo = {}; return apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart index 74d1955312..352a6c878e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart @@ -2,17 +2,13 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/file_extension.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/dispatch/error.dart'; import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; Future saveImageToLocalStorage(String localImagePath) async { @@ -43,13 +39,19 @@ Future saveImageToLocalStorage(String localImagePath) async { Future<(String? path, String? errorMessage)> saveImageToCloudStorage( String localImagePath, - String documentId, ) async { + final size = localImagePath.fileSize; + if (size == null || size > 10 * 1024 * 1024) { + // 10MB + return ( + null, + LocaleKeys.document_imageBlock_uploadImageErrorImageSizeTooBig.tr(), + ); + } final documentService = DocumentService(); - Log.debug("Uploading image local path: $localImagePath"); final result = await documentService.uploadFile( localFilePath: localImagePath, - documentId: documentId, + isAsync: false, ); return result.fold( (s) async { @@ -59,74 +61,6 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage( ); return (s.url, null); }, - (err) { - final message = Platform.isIOS - ? LocaleKeys.sideBar_storageLimitDialogTitleIOS.tr() - : LocaleKeys.sideBar_storageLimitDialogTitle.tr(); - if (err.isStorageLimitExceeded) { - return (null, message); - } else { - return (null, err.msg); - } - }, + (e) => (null, e.msg), ); } - -Future> extractAndUploadImages( - BuildContext context, - List urls, - bool isLocalMode, -) async { - final List images = []; - - bool hasError = false; - for (final url in urls) { - if (url == null || url.isEmpty) { - continue; - } - - String? path; - String? errorMsg; - CustomImageType imageType = CustomImageType.local; - - // If the user is using local authenticator, we save the image to local storage - if (isLocalMode) { - path = await saveImageToLocalStorage(url); - } else { - // Else we save the image to cloud storage - (path, errorMsg) = await saveImageToCloudStorage( - url, - context.read().documentId, - ); - imageType = CustomImageType.internal; - } - - if (path != null && errorMsg == null) { - images.add(ImageBlockData(url: path, type: imageType)); - } else { - hasError = true; - } - } - - if (context.mounted && hasError) { - showSnackBarMessage( - context, - LocaleKeys.document_imageBlock_error_multipleImagesFailed.tr(), - ); - } - - return images; -} - -@visibleForTesting -int deleteImageTestCounter = 0; - -Future deleteImageFromLocalStorage(String localImagePath) async { - try { - await File(localImagePath) - .delete() - .whenComplete(() => deleteImageTestCounter++); - } catch (e) { - Log.error('cannot delete image file', e); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart index cb7fd457e0..aa8c6fe496 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/mobile_image_toolbar_item.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; final imageMobileToolbarItem = MobileToolbarItem.action( itemIconBuilder: (_, __, ___) => const FlowySvg(FlowySvgs.m_toolbar_imae_lg), @@ -11,7 +10,7 @@ final imageMobileToolbarItem = MobileToolbarItem.action( final imagePlaceholderKey = GlobalKey(); await editorState.insertEmptyImageBlock(imagePlaceholderKey); - WidgetsBinding.instance.addPostFrameCallback((_) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { imagePlaceholderKey.currentState?.showUploadImageMenu(); }); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart deleted file mode 100644 index 66a14d2c4a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flowy_infra/size.dart'; - -@visibleForTesting -class ImageRender extends StatelessWidget { - const ImageRender({ - super.key, - required this.image, - this.userProfile, - this.fit = BoxFit.cover, - this.borderRadius = Corners.s6Border, - }); - - final ImageBlockData image; - final UserProfilePB? userProfile; - final BoxFit fit; - final BorderRadius? borderRadius; - - @override - Widget build(BuildContext context) { - final child = switch (image.type) { - CustomImageType.internal || CustomImageType.external => FlowyNetworkImage( - url: image.url, - userProfilePB: userProfile, - fit: fit, - ), - CustomImageType.local => Image.file(File(image.url), fit: fit), - }; - - return Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration(borderRadius: borderRadius), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart deleted file mode 100644 index eb8ddba0b8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart +++ /dev/null @@ -1,415 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:collection/collection.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../image_render.dart'; - -const _thumbnailItemSize = 100.0, _imageHeight = 400.0; - -class ImageBrowserLayout extends ImageBlockMultiLayout { - const ImageBrowserLayout({ - super.key, - required super.node, - required super.editorState, - required super.images, - required super.indexNotifier, - required super.isLocalMode, - required this.onIndexChanged, - }); - - final void Function(int) onIndexChanged; - - @override - State createState() => _ImageBrowserLayoutState(); -} - -class _ImageBrowserLayoutState extends State { - UserProfilePB? _userProfile; - bool isDraggingFiles = false; - - @override - void initState() { - super.initState(); - _userProfile = context.read()?.userProfile ?? - context.read().state.userProfilePB; - } - - @override - Widget build(BuildContext context) { - final gallery = Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - height: _imageHeight, - width: MediaQuery.of(context).size.width, - child: GestureDetector( - onDoubleTap: () => _openInteractiveViewer(context), - child: ImageRender( - image: widget.images[widget.indexNotifier.value], - userProfile: _userProfile, - fit: BoxFit.contain, - ), - ), - ), - const VSpace(8), - LayoutBuilder( - builder: (context, constraints) { - final maxItems = - (constraints.maxWidth / (_thumbnailItemSize + 4)).floor(); - final items = widget.images.take(maxItems).toList(); - - return Center( - child: Wrap( - children: items.mapIndexed((index, image) { - final isLast = items.last == image; - final amountLeft = widget.images.length - items.length; - if (isLast && amountLeft > 0) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _openInteractiveViewer( - context, - maxItems - 1, - ), - child: Container( - width: _thumbnailItemSize, - height: _thumbnailItemSize, - padding: const EdgeInsets.all(2), - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: Corners.s8Border, - border: Border.all( - width: 2, - color: Theme.of(context).dividerColor, - ), - ), - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: Corners.s6Border, - image: image.type == CustomImageType.local - ? DecorationImage( - image: FileImage(File(image.url)), - fit: BoxFit.cover, - opacity: 0.5, - ) - : null, - ), - child: Stack( - children: [ - if (image.type != CustomImageType.local) - Positioned.fill( - child: Container( - clipBehavior: Clip.antiAlias, - decoration: const BoxDecoration( - borderRadius: Corners.s6Border, - ), - child: FlowyNetworkImage( - url: image.url, - userProfilePB: _userProfile, - ), - ), - ), - DecoratedBox( - decoration: BoxDecoration( - color: - Colors.white.withValues(alpha: 0.5), - ), - child: Center( - child: FlowyText( - '+$amountLeft', - color: AFThemeExtension.of(context) - .strongText, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - ), - ), - ); - } - - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => widget.onIndexChanged(index), - child: ThumbnailItem( - images: widget.images, - index: index, - selectedIndex: widget.indexNotifier.value, - userProfile: _userProfile, - onDeleted: () async { - final transaction = - widget.editorState.transaction; - - final images = widget.images.toList(); - images.removeAt(index); - - transaction.updateNode( - widget.node, - { - MultiImageBlockKeys.images: - images.map((e) => e.toJson()).toList(), - MultiImageBlockKeys.layout: widget.node - .attributes[MultiImageBlockKeys.layout], - }, - ); - - await widget.editorState.apply(transaction); - - widget.onIndexChanged( - widget.indexNotifier.value > 0 - ? widget.indexNotifier.value - 1 - : 0, - ); - }, - ), - ), - ); - }).toList(), - ), - ); - }, - ), - ], - ), - Positioned.fill( - child: DropTarget( - onDragEntered: (_) => setState(() => isDraggingFiles = true), - onDragExited: (_) => setState(() => isDraggingFiles = false), - onDragDone: (details) { - setState(() => isDraggingFiles = false); - // Only accept files where the mimetype is an image, - // or the file extension is a known image format, - // otherwise we assume it's a file we cannot display. - final imageFiles = details.files - .where( - (file) => - file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), - ) - .toList(); - final paths = imageFiles.map((file) => file.path).toList(); - WidgetsBinding.instance.addPostFrameCallback( - (_) async => insertLocalImages(paths), - ); - }, - child: !isDraggingFiles - ? const SizedBox.shrink() - : SizedBox.expand( - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.5), - ), - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.download_s, - size: Size.square(28), - ), - const HSpace(12), - Flexible( - child: FlowyText( - LocaleKeys - .document_plugins_image_dropImageToInsert - .tr(), - color: AFThemeExtension.of(context).strongText, - fontSize: 22, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - ), - ), - ), - ), - ], - ); - return SizedBox( - height: _imageHeight + _thumbnailItemSize + 20, - child: gallery, - ); - } - - void _openInteractiveViewer(BuildContext context, [int? index]) => showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: _userProfile, - imageProvider: AFBlockImageProvider( - images: widget.images, - initialIndex: index ?? widget.indexNotifier.value, - onDeleteImage: (index) async { - final transaction = widget.editorState.transaction; - final newImages = widget.images.toList(); - newImages.removeAt(index); - - widget.onIndexChanged( - widget.indexNotifier.value > 0 - ? widget.indexNotifier.value - 1 - : 0, - ); - - if (newImages.isNotEmpty) { - transaction.updateNode( - widget.node, - { - MultiImageBlockKeys.images: - newImages.map((e) => e.toJson()).toList(), - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }, - ); - } else { - transaction.deleteNode(widget.node); - } - - await widget.editorState.apply(transaction); - }, - ), - ), - ); - - Future insertLocalImages(List urls) async { - if (urls.isEmpty || urls.every((path) => path?.isEmpty ?? true)) { - return; - } - - final isLocalMode = context.read().isLocalMode; - final transaction = widget.editorState.transaction; - final images = await extractAndUploadImages(context, urls, isLocalMode); - if (images.isEmpty) { - return; - } - - final newImages = [...widget.images, ...images]; - final imagesJson = newImages.map((image) => image.toJson()).toList(); - - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: imagesJson, - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }); - - await widget.editorState.apply(transaction); - } -} - -@visibleForTesting -class ThumbnailItem extends StatefulWidget { - const ThumbnailItem({ - super.key, - required this.images, - required this.index, - required this.selectedIndex, - required this.onDeleted, - this.userProfile, - }); - - final List images; - final int index; - final int selectedIndex; - final VoidCallback onDeleted; - final UserProfilePB? userProfile; - - @override - State createState() => _ThumbnailItemState(); -} - -class _ThumbnailItemState extends State { - bool isHovering = false; - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: Container( - width: _thumbnailItemSize, - height: _thumbnailItemSize, - padding: const EdgeInsets.all(2), - margin: const EdgeInsets.all(2), - decoration: BoxDecoration( - borderRadius: Corners.s8Border, - border: Border.all( - width: 2, - color: widget.index == widget.selectedIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - ), - ), - child: Stack( - children: [ - Positioned.fill( - child: ImageRender( - image: widget.images[widget.index], - userProfile: widget.userProfile, - ), - ), - Positioned( - top: 4, - right: 4, - child: AnimatedOpacity( - opacity: isHovering ? 1 : 0, - duration: const Duration(milliseconds: 100), - child: FlowyTooltip( - message: LocaleKeys.button_delete.tr(), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.onDeleted, - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - backgroundColor: Colors.black.withValues(alpha: 0.6), - hoverColor: Colors.black.withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(4), - child: FlowySvg( - FlowySvgs.delete_s, - color: Colors.white, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart deleted file mode 100644 index 1abe57146e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_grid_layout.dart +++ /dev/null @@ -1,323 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.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/multi_image_layouts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:provider/provider.dart'; - -class ImageGridLayout extends ImageBlockMultiLayout { - const ImageGridLayout({ - super.key, - required super.node, - required super.editorState, - required super.images, - required super.indexNotifier, - required super.isLocalMode, - }); - - @override - State createState() => _ImageGridLayoutState(); -} - -class _ImageGridLayoutState extends State { - @override - Widget build(BuildContext context) { - return StaggeredGridBuilder( - images: widget.images, - onImageDoubleTapped: (index) { - _openInteractiveViewer(context, index); - }, - ); - } - - void _openInteractiveViewer(BuildContext context, int index) => showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfilePB, - imageProvider: AFBlockImageProvider( - images: widget.images, - initialIndex: index, - onDeleteImage: (index) async { - final transaction = widget.editorState.transaction; - final newImages = widget.images.toList(); - newImages.removeAt(index); - - if (newImages.isNotEmpty) { - transaction.updateNode( - widget.node, - { - MultiImageBlockKeys.images: - newImages.map((e) => e.toJson()).toList(), - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }, - ); - } else { - transaction.deleteNode(widget.node); - } - - await widget.editorState.apply(transaction); - }, - ), - ), - ); -} - -/// Draws a staggered grid of images, where the pattern is based -/// on the amount of images to fill the grid at all times. -/// -/// They will be alternating depending on the current index of the images, such that -/// the layout is reversed in odd segments. -/// -/// If there are 4 images in the last segment, this layout will be used: -/// ┌─────┐┌─┐┌─┐ -/// │ │└─┘└─┘ -/// │ │┌────┐ -/// └─────┘└────┘ -/// -/// If there are 3 images in the last segment, this layout will be used: -/// ┌─────┐┌────┐ -/// │ │└────┘ -/// │ │┌────┐ -/// └─────┘└────┘ -/// -/// If there are 2 images in the last segment, this layout will be used: -/// ┌─────┐┌─────┐ -/// │ ││ │ -/// └─────┘└─────┘ -/// -/// If there is 1 image in the last segment, this layout will be used: -/// ┌──────────┐ -/// │ │ -/// └──────────┘ -class StaggeredGridBuilder extends StatefulWidget { - const StaggeredGridBuilder({ - super.key, - required this.images, - required this.onImageDoubleTapped, - }); - - final List images; - final void Function(int) onImageDoubleTapped; - - @override - State createState() => _StaggeredGridBuilderState(); -} - -class _StaggeredGridBuilderState extends State { - late final UserProfilePB? _userProfile; - final List> _splitImages = []; - - @override - void initState() { - super.initState(); - _userProfile = context.read().state.userProfilePB; - - for (int i = 0; i < widget.images.length; i += 4) { - final end = (i + 4 < widget.images.length) ? i + 4 : widget.images.length; - _splitImages.add(widget.images.sublist(i, end)); - } - } - - @override - void didUpdateWidget(covariant StaggeredGridBuilder oldWidget) { - if (widget.images.length != oldWidget.images.length) { - _splitImages.clear(); - for (int i = 0; i < widget.images.length; i += 4) { - final end = - (i + 4 < widget.images.length) ? i + 4 : widget.images.length; - _splitImages.add(widget.images.sublist(i, end)); - } - } - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - return StaggeredGrid.count( - crossAxisCount: 4, - mainAxisSpacing: 6, - crossAxisSpacing: 6, - children: - _splitImages.indexed.map(_buildTilesForImages).flattened.toList(), - ); - } - - List _buildTilesForImages((int, List) data) { - final index = data.$1; - final images = data.$2; - - final isReversed = index.isOdd; - - if (images.length == 4) { - return [ - StaggeredGridTile.count( - crossAxisCellCount: isReversed ? 1 : 2, - mainAxisCellCount: isReversed ? 1 : 2, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[0], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: 1, - mainAxisCellCount: 1, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 1; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[1], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: isReversed ? 2 : 1, - mainAxisCellCount: isReversed ? 2 : 1, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 2; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[2], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: 1, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 3; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[3], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - ]; - } else if (images.length == 3) { - return [ - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: isReversed ? 1 : 2, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[0], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: isReversed ? 2 : 1, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 1; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[1], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: 1, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 2; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[2], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - ]; - } else if (images.length == 2) { - return [ - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: 2, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[0], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - StaggeredGridTile.count( - crossAxisCellCount: 2, - mainAxisCellCount: 2, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4 + 1; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[1], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - ]; - } else { - return [ - StaggeredGridTile.count( - crossAxisCellCount: 4, - mainAxisCellCount: 2, - child: GestureDetector( - onDoubleTap: () { - final imageIndex = index * 4; - widget.onImageDoubleTapped(imageIndex); - }, - child: ImageRender( - image: images[0], - userProfile: _userProfile, - borderRadius: BorderRadius.zero, - ), - ), - ), - ]; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart deleted file mode 100644 index 43d1c7ae36..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.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/layouts/image_grid_layout.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ResizableImage; -import 'package:flutter/material.dart'; - -abstract class ImageBlockMultiLayout extends StatefulWidget { - const ImageBlockMultiLayout({ - super.key, - required this.node, - required this.editorState, - required this.images, - required this.indexNotifier, - required this.isLocalMode, - }); - - final Node node; - final EditorState editorState; - final List images; - final ValueNotifier indexNotifier; - final bool isLocalMode; -} - -class ImageLayoutRender extends StatelessWidget { - const ImageLayoutRender({ - super.key, - required this.node, - required this.editorState, - required this.images, - required this.indexNotifier, - required this.isLocalMode, - required this.onIndexChanged, - }); - - final Node node; - final EditorState editorState; - final List images; - final ValueNotifier indexNotifier; - final bool isLocalMode; - final void Function(int) onIndexChanged; - - @override - Widget build(BuildContext context) { - final layout = _getLayout(); - - return _buildLayout(layout); - } - - MultiImageLayout _getLayout() { - return MultiImageLayout.fromIntValue( - node.attributes[MultiImageBlockKeys.layout] ?? 0, - ); - } - - Widget _buildLayout(MultiImageLayout layout) { - switch (layout) { - case MultiImageLayout.grid: - return ImageGridLayout( - node: node, - editorState: editorState, - images: images, - indexNotifier: indexNotifier, - isLocalMode: isLocalMode, - ); - case MultiImageLayout.browser: - return ImageBrowserLayout( - node: node, - editorState: editorState, - images: images, - indexNotifier: indexNotifier, - isLocalMode: isLocalMode, - onIndexChanged: onIndexChanged, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart deleted file mode 100644 index 51da975938..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart +++ /dev/null @@ -1,374 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/multi_image_layouts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -const kMultiImagePlaceholderKey = 'multiImagePlaceholderKey'; - -Node multiImageNode({List? images}) => Node( - type: MultiImageBlockKeys.type, - attributes: { - MultiImageBlockKeys.images: - MultiImageData(images: images ?? []).toJson(), - MultiImageBlockKeys.layout: MultiImageLayout.browser.toIntValue(), - }, - ); - -class MultiImageBlockKeys { - const MultiImageBlockKeys._(); - - static const String type = 'multi_image'; - - /// The image data for the block, stored as a JSON encoded list of [ImageBlockData]. - /// - static const String images = 'images'; - - /// The layout of the images. - /// - /// The value is a MultiImageLayout enum. - /// - static const String layout = 'layout'; -} - -typedef MultiImageBlockComponentMenuBuilder = Widget Function( - Node node, - MultiImageBlockComponentState state, - ValueNotifier indexNotifier, - VoidCallback onImageDeleted, -); - -class MultiImageBlockComponentBuilder extends BlockComponentBuilder { - MultiImageBlockComponentBuilder({ - super.configuration, - this.showMenu = false, - this.menuBuilder, - }); - - final bool showMenu; - final MultiImageBlockComponentMenuBuilder? menuBuilder; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return MultiImageBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - showMenu: showMenu, - menuBuilder: menuBuilder, - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; -} - -class MultiImageBlockComponent extends BlockComponentStatefulWidget { - const MultiImageBlockComponent({ - super.key, - required super.node, - super.showActions, - this.showMenu = false, - this.menuBuilder, - super.configuration = const BlockComponentConfiguration(), - super.actionBuilder, - super.actionTrailingBuilder, - }); - - final bool showMenu; - - final MultiImageBlockComponentMenuBuilder? menuBuilder; - - @override - State createState() => - MultiImageBlockComponentState(); -} - -class MultiImageBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - final multiImageKey = GlobalKey(); - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - late final editorState = Provider.of(context, listen: false); - - final showActionsNotifier = ValueNotifier(false); - - ValueNotifier indexNotifier = ValueNotifier(0); - - bool alwaysShowMenu = false; - - static const _interceptorKey = 'multi-image-block-interceptor'; - - late final interceptor = SelectionGestureInterceptor( - key: _interceptorKey, - canTap: (details) => _isTapInBounds(details.globalPosition), - canPanStart: (details) => _isTapInBounds(details.globalPosition), - ); - - @override - void initState() { - super.initState(); - editorState.selectionService.registerGestureInterceptor(interceptor); - } - - @override - void dispose() { - editorState.selectionService.unregisterGestureInterceptor(_interceptorKey); - super.dispose(); - } - - bool _isTapInBounds(Offset offset) { - if (_renderBox == null) { - // We shouldn't block any actions if the render box is not available. - // This has the potential to break taps on the editor completely if we - // accidentally return false here. - return true; - } - - final localPosition = _renderBox!.globalToLocal(offset); - return !_renderBox!.paintBounds.contains(localPosition); - } - - @override - Widget build(BuildContext context) { - final data = MultiImageData.fromJson( - node.attributes[MultiImageBlockKeys.images], - ); - - Widget child; - if (data.images.isEmpty) { - final multiImagePlaceholderKey = - node.extraInfos?[kMultiImagePlaceholderKey]; - - child = MultiImagePlaceholder( - key: multiImagePlaceholderKey is GlobalKey - ? multiImagePlaceholderKey - : null, - node: node, - ); - } else { - child = ImageLayoutRender( - node: node, - images: data.images, - editorState: editorState, - indexNotifier: indexNotifier, - isLocalMode: context.read().isLocalMode, - onIndexChanged: (index) => setState(() => indexNotifier.value = index), - ); - } - - if (UniversalPlatform.isDesktopOrWeb) { - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - blockColor: editorState.editorStyle.selectionColor, - supportTypes: const [BlockSelectionType.block], - child: Padding(key: multiImageKey, padding: padding, child: child), - ); - } else { - child = Padding(key: multiImageKey, padding: padding, child: child); - } - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - if (UniversalPlatform.isDesktopOrWeb) { - if (widget.showMenu && widget.menuBuilder != null) { - child = MouseRegion( - onEnter: (_) => showActionsNotifier.value = true, - onExit: (_) { - if (!alwaysShowMenu) { - showActionsNotifier.value = false; - } - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, value, child) { - return Stack( - children: [ - BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - child: child!, - ), - if (value && data.images.isNotEmpty) - widget.menuBuilder!( - widget.node, - this, - indexNotifier, - () => setState( - () => indexNotifier.value = indexNotifier.value > 0 - ? indexNotifier.value - 1 - : 0, - ), - ), - ], - ); - }, - child: child, - ), - ); - } - } else { - // show a fixed menu on mobile - child = MobileBlockActionButtons( - showThreeDots: false, - node: node, - editorState: editorState, - child: child, - ); - } - - return child; - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - final imageBox = multiImageKey.currentContext?.findRenderObject(); - if (imageBox is RenderBox) { - return Offset.zero & imageBox.size; - } - return Rect.zero; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection(Selection.collapsed(position)); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final imageBox = multiImageKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && imageBox is RenderBox) { - return [ - imageBox.localToGlobal(Offset.zero, ancestor: parentBox) & - imageBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: widget.node.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal( - Offset offset, { - bool shiftWithBaseOffset = false, - }) => - _renderBox!.localToGlobal(offset); -} - -/// The data for a multi-image block, primarily used for -/// serializing and deserializing the block's images. -/// -class MultiImageData { - factory MultiImageData.fromJson(List json) { - final images = json - .map((e) => ImageBlockData.fromJson(e as Map)) - .toList(); - return MultiImageData(images: images); - } - - MultiImageData({required this.images}); - - final List images; - - List toJson() => images.map((e) => e.toJson()).toList(); -} - -enum MultiImageLayout { - browser, - grid; - - int toIntValue() { - switch (this) { - case MultiImageLayout.browser: - return 0; - case MultiImageLayout.grid: - return 1; - } - } - - static MultiImageLayout fromIntValue(int value) { - switch (value) { - case 0: - return MultiImageLayout.browser; - case 1: - return MultiImageLayout.grid; - default: - throw UnimplementedError(); - } - } - - String get label => switch (this) { - browser => LocaleKeys.document_plugins_photoGallery_browserLayout.tr(), - grid => LocaleKeys.document_plugins_photoGallery_gridLayout.tr(), - }; - - FlowySvgData get icon => switch (this) { - browser => FlowySvgs.photo_layout_browser_s, - grid => FlowySvgs.photo_layout_grid_s, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart deleted file mode 100644 index dc95054e81..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_menu.dart +++ /dev/null @@ -1,441 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.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/upload_image_menu/upload_image_menu.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; -import 'package:cross_file/cross_file.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart'; -import 'package:path/path.dart' as p; -import 'package:provider/provider.dart'; -import 'package:string_validator/string_validator.dart'; - -const _interceptorKey = 'add-image'; - -class MultiImageMenu extends StatefulWidget { - const MultiImageMenu({ - super.key, - required this.node, - required this.state, - required this.indexNotifier, - this.isLocalMode = true, - required this.onImageDeleted, - }); - - final Node node; - final MultiImageBlockComponentState state; - final ValueNotifier indexNotifier; - final bool isLocalMode; - final VoidCallback onImageDeleted; - - @override - State createState() => _MultiImageMenuState(); -} - -class _MultiImageMenuState extends State { - final gestureInterceptor = SelectionGestureInterceptor( - key: _interceptorKey, - canTap: (details) => false, - ); - - final PopoverController controller = PopoverController(); - final PopoverController layoutController = PopoverController(); - late List images; - late final EditorState editorState; - - @override - void initState() { - super.initState(); - editorState = context.read(); - images = MultiImageData.fromJson( - widget.node.attributes[MultiImageBlockKeys.images] ?? {}, - ).images; - } - - @override - void dispose() { - allowMenuClose(); - controller.close(); - layoutController.close(); - super.dispose(); - } - - @override - void didUpdateWidget(covariant MultiImageMenu oldWidget) { - images = MultiImageData.fromJson( - widget.node.attributes[MultiImageBlockKeys.images] ?? {}, - ).images; - - super.didUpdateWidget(oldWidget); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final layout = MultiImageLayout.fromIntValue( - widget.node.attributes[MultiImageBlockKeys.layout] ?? 0, - ); - return Container( - height: 32, - decoration: BoxDecoration( - color: theme.cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), - ), - ], - borderRadius: BorderRadius.circular(4.0), - ), - child: Row( - children: [ - const HSpace(4), - AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithRightAligned, - onClose: allowMenuClose, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, - ), - offset: const Offset(0, 10), - popupBuilder: (context) { - preventMenuClose(); - return UploadImageMenu( - allowMultipleImages: true, - supportTypes: const [ - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: insertLocalImages, - onSelectedAIImage: insertAIImage, - onSelectedNetworkImage: insertNetworkImage, - ); - }, - child: MenuBlockButton( - tooltip: - LocaleKeys.document_plugins_photoGallery_addImageTooltip.tr(), - iconData: FlowySvgs.add_s, - onTap: () {}, - ), - ), - const HSpace(4), - AppFlowyPopover( - controller: layoutController, - onClose: allowMenuClose, - direction: PopoverDirection.bottomWithRightAligned, - offset: const Offset(0, 10), - constraints: const BoxConstraints( - maxHeight: 300, - maxWidth: 300, - ), - popupBuilder: (context) { - preventMenuClose(); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _LayoutSelector( - selectedLayout: layout, - onSelected: (layout) { - allowMenuClose(); - layoutController.close(); - final transaction = editorState.transaction; - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: - widget.node.attributes[MultiImageBlockKeys.images], - MultiImageBlockKeys.layout: layout.toIntValue(), - }); - editorState.apply(transaction); - }, - ), - ], - ); - }, - child: MenuBlockButton( - tooltip: LocaleKeys - .document_plugins_photoGallery_changeLayoutTooltip - .tr(), - iconData: FlowySvgs.edit_layout_s, - onTap: () {}, - ), - ), - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.document_imageBlock_openFullScreen.tr(), - iconData: FlowySvgs.full_view_s, - onTap: openFullScreen, - ), - - // disable the copy link button if the image is hosted on appflowy cloud - // because the url needs the verification token to be accessible - if (layout == MultiImageLayout.browser && - !images[widget.indexNotifier.value].url.isAppFlowyCloudUrl) ...[ - const HSpace(4), - MenuBlockButton( - tooltip: LocaleKeys.editor_copyLink.tr(), - iconData: FlowySvgs.copy_s, - onTap: copyImageLink, - ), - ], - const _Divider(), - MenuBlockButton( - tooltip: LocaleKeys.document_plugins_photoGallery_deleteBlockTooltip - .tr(), - iconData: FlowySvgs.delete_s, - onTap: deleteImage, - ), - const HSpace(4), - ], - ), - ); - } - - void copyImageLink() { - Clipboard.setData( - ClipboardData(text: images[widget.indexNotifier.value].url), - ); - showToastNotification( - message: LocaleKeys.document_plugins_image_copiedToPasteBoard.tr(), - ); - } - - Future deleteImage() async { - final node = widget.node; - final editorState = context.read(); - final transaction = editorState.transaction; - transaction.deleteNode(node); - transaction.afterSelection = null; - await editorState.apply(transaction); - } - - void openFullScreen() { - showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: context.read().state.userProfilePB, - imageProvider: AFBlockImageProvider( - images: images, - initialIndex: widget.indexNotifier.value, - onDeleteImage: (index) async { - final transaction = editorState.transaction; - final newImages = List.from(images); - newImages.removeAt(index); - - images = newImages; - widget.onImageDeleted(); - - final imagesJson = - newImages.map((image) => image.toJson()).toList(); - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: imagesJson, - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }); - - await editorState.apply(transaction); - }, - ), - ), - ); - } - - void preventMenuClose() { - widget.state.alwaysShowMenu = true; - editorState.service.selectionService.registerGestureInterceptor( - gestureInterceptor, - ); - } - - void allowMenuClose() { - widget.state.alwaysShowMenu = false; - editorState.service.selectionService.unregisterGestureInterceptor( - _interceptorKey, - ); - } - - Future insertLocalImages(List files) async { - controller.close(); - - WidgetsBinding.instance.addPostFrameCallback((_) async { - final urls = files - .map((file) => file.path) - .where((path) => path.isNotEmpty) - .toList(); - - if (urls.isEmpty || urls.every((url) => url.isEmpty)) { - return; - } - - final transaction = editorState.transaction; - final newImages = - await extractAndUploadImages(context, urls, widget.isLocalMode); - if (newImages.isEmpty) { - return; - } - - final imagesJson = - [...images, ...newImages].map((i) => i.toJson()).toList(); - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: imagesJson, - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }); - - await editorState.apply(transaction); - setState(() => images = newImages); - }); - } - - Future insertAIImage(String url) async { - controller.close(); - - if (url.isEmpty || !isURL(url)) { - // show error - return showSnackBarMessage( - context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), - ); - } - - final path = await getIt().getPath(); - final imagePath = p.join(path, 'images'); - try { - // create the directory if not exists - final directory = Directory(imagePath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - final uri = Uri.parse(url); - final copyToPath = p.join( - imagePath, - '${uuid()}${p.extension(uri.path)}', - ); - - final response = await get(uri); - await File(copyToPath).writeAsBytes(response.bodyBytes); - await insertLocalImages([XFile(copyToPath)]); - await File(copyToPath).delete(); - } catch (e) { - Log.error('cannot save image file', e); - } - } - - Future insertNetworkImage(String url) async { - controller.close(); - - if (url.isEmpty || !isURL(url)) { - // show error - return showSnackBarMessage( - context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), - ); - } - - final transaction = editorState.transaction; - - final newImages = [ - ...images, - ImageBlockData(url: url, type: CustomImageType.external), - ]; - - final imagesJson = newImages.map((image) => image.toJson()).toList(); - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: imagesJson, - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout], - }); - - await editorState.apply(transaction); - setState(() => images = newImages); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(8), - child: Container(width: 1, color: Colors.grey), - ); - } -} - -class _LayoutSelector extends StatelessWidget { - const _LayoutSelector({ - required this.selectedLayout, - required this.onSelected, - }); - - final MultiImageLayout selectedLayout; - final Function(MultiImageLayout) onSelected; - - @override - Widget build(BuildContext context) { - return SeparatedRow( - separatorBuilder: () => const HSpace(6), - mainAxisSize: MainAxisSize.min, - children: MultiImageLayout.values - .map( - (layout) => MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => onSelected(layout), - child: Container( - height: 80, - width: 80, - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - border: Border.all( - width: 2, - color: selectedLayout == layout - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - ), - borderRadius: Corners.s8Border, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowySvg( - layout.icon, - color: AFThemeExtension.of(context).strongText, - size: const Size.square(24), - ), - const VSpace(6), - FlowyText(layout.label), - ], - ), - ), - ), - ), - ) - .toList(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart deleted file mode 100644 index 313022bfab..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart +++ /dev/null @@ -1,302 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/application/document_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.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/upload_image_menu/upload_image_menu.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; -import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide UploadImageMenu; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:http/http.dart'; -import 'package:path/path.dart' as p; -import 'package:provider/provider.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class MultiImagePlaceholder extends StatefulWidget { - const MultiImagePlaceholder({super.key, required this.node}); - - final Node node; - - @override - State createState() => MultiImagePlaceholderState(); -} - -class MultiImagePlaceholderState extends State { - final controller = PopoverController(); - final documentService = DocumentService(); - late final editorState = context.read(); - - bool isDraggingFiles = false; - - @override - Widget build(BuildContext context) { - final child = DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - border: isDraggingFiles - ? Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ) - : null, - ), - child: FlowyHover( - style: HoverStyle( - borderRadius: BorderRadius.circular(4), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12), - child: Row( - children: [ - FlowySvg( - FlowySvgs.slash_menu_icon_photo_gallery_s, - color: Theme.of(context).hintColor, - size: const Size.square(24), - ), - const HSpace(10), - FlowyText( - UniversalPlatform.isDesktop - ? isDraggingFiles - ? LocaleKeys.document_plugins_image_dropImageToInsert - .tr() - : LocaleKeys.document_plugins_image_addAnImageDesktop - .tr() - : LocaleKeys.document_plugins_image_addAnImageMobile.tr(), - color: Theme.of(context).hintColor, - ), - ], - ), - ), - ), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - return AppFlowyPopover( - controller: controller, - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 540, - maxHeight: 360, - minHeight: 80, - ), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (_) { - return UploadImageMenu( - allowMultipleImages: true, - limitMaximumImageSize: !_isLocalMode(), - supportTypes: const [ - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - final paths = files.map((file) => file.path).toList(); - await insertLocalImages(paths); - }); - }, - onSelectedAIImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertAIImage(url); - }); - }, - onSelectedNetworkImage: (url) { - controller.close(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - await insertNetworkImage(url); - }); - }, - ); - }, - child: DropTarget( - onDragEntered: (_) => setState(() => isDraggingFiles = true), - onDragExited: (_) => setState(() => isDraggingFiles = false), - onDragDone: (details) { - // Only accept files where the mimetype is an image, - // or the file extension is a known image format, - // otherwise we assume it's a file we cannot display. - final imageFiles = details.files - .where( - (file) => - file.mimeType?.startsWith('image/') ?? - false || imgExtensionRegex.hasMatch(file.name), - ) - .toList(); - final paths = imageFiles.map((file) => file.path).toList(); - WidgetsBinding.instance.addPostFrameCallback( - (_) async => insertLocalImages(paths), - ); - }, - child: child, - ), - ); - } else { - return MobileBlockActionButtons( - node: widget.node, - editorState: editorState, - child: GestureDetector( - onTap: () { - editorState.updateSelectionWithReason(null, extraInfo: {}); - showUploadImageMenu(); - }, - child: child, - ), - ); - } - } - - void showUploadImageMenu() { - if (UniversalPlatform.isDesktopOrWeb) { - controller.show(); - } else { - final isLocalMode = _isLocalMode(); - showMobileBottomSheet( - context, - title: LocaleKeys.editor_image.tr(), - showHeader: true, - showCloseButton: true, - showDragHandle: true, - builder: (context) { - return Container( - margin: const EdgeInsets.only(top: 12.0), - constraints: const BoxConstraints( - maxHeight: 340, - minHeight: 80, - ), - child: UploadImageMenu( - limitMaximumImageSize: !isLocalMode, - allowMultipleImages: true, - supportTypes: const [ - UploadImageType.local, - UploadImageType.url, - UploadImageType.unsplash, - ], - onSelectedLocalImages: (files) async { - context.pop(); - final items = files.map((file) => file.path).toList(); - await insertLocalImages(items); - }, - onSelectedAIImage: (url) async { - context.pop(); - await insertAIImage(url); - }, - onSelectedNetworkImage: (url) async { - context.pop(); - await insertNetworkImage(url); - }, - ), - ); - }, - ); - } - } - - Future insertLocalImages(List urls) async { - controller.close(); - - if (urls.isEmpty || urls.every((path) => path?.isEmpty ?? true)) { - return; - } - - final transaction = editorState.transaction; - final images = await extractAndUploadImages(context, urls, _isLocalMode()); - if (images.isEmpty) { - return; - } - - final imagesJson = images.map((image) => image.toJson()).toList(); - - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: imagesJson, - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout] ?? - MultiImageLayout.browser.toIntValue(), - }); - - await editorState.apply(transaction); - } - - Future insertAIImage(String url) async { - if (url.isEmpty || !isURL(url)) { - // show error - return showSnackBarMessage( - context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), - ); - } - - final path = await getIt().getPath(); - final imagePath = p.join(path, 'images'); - try { - // create the directory if not exists - final directory = Directory(imagePath); - if (!directory.existsSync()) { - await directory.create(recursive: true); - } - final uri = Uri.parse(url); - final copyToPath = p.join( - imagePath, - '${uuid()}${p.extension(uri.path)}', - ); - - final response = await get(uri); - await File(copyToPath).writeAsBytes(response.bodyBytes); - await insertLocalImages([copyToPath]); - await File(copyToPath).delete(); - } catch (e) { - Log.error('cannot save image file', e); - } - } - - Future insertNetworkImage(String url) async { - if (url.isEmpty || !isURL(url)) { - // show error - return showSnackBarMessage( - context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), - ); - } - - final transaction = editorState.transaction; - - final images = [ - ImageBlockData( - url: url, - type: CustomImageType.external, - ), - ]; - - transaction.updateNode(widget.node, { - MultiImageBlockKeys.images: - images.map((image) => image.toJson()).toList(), - MultiImageBlockKeys.layout: - widget.node.attributes[MultiImageBlockKeys.layout] ?? - MultiImageLayout.browser.toIntValue(), - }); - await editorState.apply(transaction); - } - - bool _isLocalMode() { - return context.read().isLocalMode; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart new file mode 100644 index 0000000000..9cd6320518 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class OpenAIImageWidget extends StatefulWidget { + const OpenAIImageWidget({ + super.key, + required this.onSelectNetworkImage, + }); + + final void Function(String url) onSelectNetworkImage; + + @override + State createState() => _OpenAIImageWidgetState(); +} + +class _OpenAIImageWidgetState extends State { + Future, OpenAIError>>? future; + String query = ''; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: FlowyTextField( + hintText: LocaleKeys.document_imageBlock_ai_placeholder.tr(), + onChanged: (value) => query = value, + onEditingComplete: _search, + ), + ), + const HSpace(4.0), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.search_label.tr(), + ), + onTap: _search, + ), + ], + ), + const VSpace(12.0), + if (future != null) + Expanded( + child: FutureBuilder( + future: future, + builder: (context, value) { + final data = value.data; + if (!value.hasData || + value.connectionState != ConnectionState.done || + data == null) { + return const CircularProgressIndicator.adaptive(); + } + return data.fold( + (s) => GridView.count( + crossAxisCount: 3, + mainAxisSpacing: 16.0, + crossAxisSpacing: 10.0, + childAspectRatio: 4 / 3, + children: s + .map( + (e) => GestureDetector( + onTap: () => widget.onSelectNetworkImage(e), + child: Image.network(e), + ), + ) + .toList(), + ), + (e) => Center( + child: FlowyText( + e.message, + maxLines: 3, + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + ), + ], + ); + } + + void _search() async { + final openAI = await getIt.getAsync(); + setState(() { + future = openAI.generateImage( + prompt: query, + n: 6, + ); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index da37945bf5..58d5454b4b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -2,26 +2,17 @@ import 'dart:io'; import 'dart:math'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.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'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:string_validator/string_validator.dart'; -enum ResizableImageState { - loading, - loaded, - failed, -} - class ResizableImage extends StatefulWidget { const ResizableImage({ super.key, @@ -32,8 +23,6 @@ class ResizableImage extends StatefulWidget { required this.width, required this.src, this.height, - this.onDoubleTap, - this.onStateChange, }); final String src; @@ -42,8 +31,6 @@ class ResizableImage extends StatefulWidget { final double? height; final Alignment alignment; final bool editable; - final VoidCallback? onDoubleTap; - final ValueChanged? onStateChange; final void Function(double width) onResize; @@ -54,17 +41,18 @@ class ResizableImage extends StatefulWidget { const _kImageBlockComponentMinWidth = 30.0; class _ResizableImageState extends State { - final documentService = DocumentService(); + late double imageWidth; double initialOffset = 0; double moveDistance = 0; - Widget? _cacheImage; - late double imageWidth; + Widget? _cacheImage; @visibleForTesting bool onFocus = false; + final documentService = DocumentService(); + UserProfilePB? _userProfilePB; @override @@ -73,8 +61,7 @@ class _ResizableImageState extends State { imageWidth = widget.width; - _userProfilePB = context.read()?.userProfile ?? - context.read().state.userProfilePB; + _userProfilePB = context.read().state.userProfilePB; } @override @@ -85,12 +72,13 @@ class _ResizableImageState extends State { width: max(_kImageBlockComponentMinWidth, imageWidth - moveDistance), height: widget.height, child: MouseRegion( - onEnter: (_) => setState(() => onFocus = true), - onExit: (_) => setState(() => onFocus = false), - child: GestureDetector( - onDoubleTap: widget.onDoubleTap, - child: _buildResizableImage(context), - ), + onEnter: (event) => setState(() { + onFocus = true; + }), + onExit: (event) => setState(() { + onFocus = false; + }), + child: _buildResizableImage(context), ), ), ); @@ -100,39 +88,21 @@ class _ResizableImageState extends State { Widget child; final src = widget.src; if (isURL(src)) { + // load network image + if (widget.type == CustomImageType.internal && _userProfilePB == null) { + return _buildLoading(context); + } + _cacheImage = FlowyNetworkImage( url: widget.src, width: imageWidth - moveDistance, userProfilePB: _userProfilePB, - onImageLoaded: (isImageInCache) { - if (isImageInCache) { - widget.onStateChange?.call(ResizableImageState.loaded); - } - }, - progressIndicatorBuilder: (context, _, progress) { - if (progress.totalSize != null) { - if (progress.progress == 1) { - widget.onStateChange?.call(ResizableImageState.loaded); - } else { - widget.onStateChange?.call(ResizableImageState.loading); - } - } - - return _buildLoading(context); - }, - errorWidgetBuilder: (_, __, error) { - widget.onStateChange?.call(ResizableImageState.failed); - return _ImageLoadFailedWidget( - width: imageWidth, - error: error, - onRetry: () { - setState(() { - final retryCounter = FlowyNetworkRetryCounter(); - retryCounter.clear(tag: src, url: src); - }); - }, - ); - }, + errorWidgetBuilder: (context, url, error) => _ImageLoadFailedWidget( + width: imageWidth, + error: error, + ), + progressIndicatorBuilder: (context, url, progress) => + _buildLoading(context), ); child = _cacheImage!; @@ -151,7 +121,11 @@ class _ResizableImageState extends State { left: 5, bottom: 0, width: 5, - onUpdate: (distance) => setState(() => moveDistance = distance), + onUpdate: (distance) { + setState(() { + moveDistance = distance; + }); + }, ), _buildEdgeGesture( context, @@ -159,7 +133,11 @@ class _ResizableImageState extends State { right: 5, bottom: 0, width: 5, - onUpdate: (distance) => setState(() => moveDistance = -distance), + onUpdate: (distance) { + setState(() { + moveDistance = -distance; + }); + }, ), ], ], @@ -176,7 +154,9 @@ class _ResizableImageState extends State { size: const Size(18, 18), child: const CircularProgressIndicator(), ), - SizedBox.fromSize(size: const Size(10, 10)), + SizedBox.fromSize( + size: const Size(10, 10), + ), Text(AppFlowyEditorL10n.current.loading), ], ), @@ -204,7 +184,7 @@ class _ResizableImageState extends State { }, onHorizontalDragUpdate: (details) { if (onUpdate != null) { - double offset = details.globalPosition.dx - initialOffset; + var offset = details.globalPosition.dx - initialOffset; if (widget.alignment == Alignment.center) { offset *= 2.0; } @@ -226,7 +206,7 @@ class _ResizableImageState extends State { child: Container( height: 40, decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.5), + color: Colors.black.withOpacity(0.5), borderRadius: const BorderRadius.all( Radius.circular(5.0), ), @@ -245,50 +225,44 @@ class _ImageLoadFailedWidget extends StatelessWidget { const _ImageLoadFailedWidget({ required this.width, required this.error, - required this.onRetry, }); final double width; final Object error; - final VoidCallback onRetry; @override Widget build(BuildContext context) { final error = _getErrorMessage(); return Container( - height: 160, + height: 140, width: width, alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 8.0), + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), decoration: BoxDecoration( borderRadius: const BorderRadius.all(Radius.circular(4.0)), - border: Border.all(color: Colors.grey.withValues(alpha: 0.6)), + border: Border.all( + color: Colors.grey.withOpacity(0.6), + ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const FlowySvg( FlowySvgs.broken_image_xl, - size: Size.square(36), + size: Size.square(48), ), FlowyText( AppFlowyEditorL10n.current.imageLoadFailed, - fontSize: 14, ), - const VSpace(4), + const VSpace(6), if (error != null) FlowyText( error, textAlign: TextAlign.center, - color: Theme.of(context).hintColor.withValues(alpha: 0.6), + color: Theme.of(context).hintColor.withOpacity(0.6), fontSize: 10, maxLines: 2, ), - const VSpace(12), - OutlinedRoundedButton( - text: LocaleKeys.chat_retry.tr(), - onTap: onRetry, - ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart new file mode 100644 index 0000000000..0d5d986d10 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart @@ -0,0 +1,120 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class StabilityAIImageWidget extends StatefulWidget { + const StabilityAIImageWidget({ + super.key, + required this.onSelectImage, + }); + + final void Function(String url) onSelectImage; + + @override + State createState() => _StabilityAIImageWidgetState(); +} + +class _StabilityAIImageWidgetState extends State { + Future, StabilityAIRequestError>>? future; + String query = ''; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: FlowyTextField( + hintText: LocaleKeys + .document_imageBlock_stability_ai_placeholder + .tr(), + onChanged: (value) => query = value, + onEditingComplete: _search, + ), + ), + const HSpace(4.0), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText( + LocaleKeys.search_label.tr(), + ), + onTap: _search, + ), + ], + ), + const VSpace(12.0), + if (future != null) + Expanded( + child: FutureBuilder( + future: future, + builder: (context, value) { + final data = value.data; + if (!value.hasData || + value.connectionState != ConnectionState.done || + data == null) { + return const CircularProgressIndicator.adaptive(); + } + return data.fold( + (s) => GridView.count( + crossAxisCount: 3, + mainAxisSpacing: 16.0, + crossAxisSpacing: 10.0, + childAspectRatio: 4 / 3, + children: s.map( + (e) { + final base64Image = base64Decode(e); + return GestureDetector( + onTap: () async { + final tempDirectory = await getTemporaryDirectory(); + final path = p.join( + tempDirectory.path, + '${uuid()}.png', + ); + File(path).writeAsBytesSync(base64Image); + widget.onSelectImage(path); + }, + child: Image.memory(base64Image), + ); + }, + ).toList(), + ), + (e) => Center( + child: FlowyText( + e.message, + maxLines: 3, + textAlign: TextAlign.center, + ), + ), + ); + }, + ), + ), + ], + ); + } + + void _search() async { + final stabilityAI = await getIt.getAsync(); + setState(() { + future = stabilityAI.generateImage( + prompt: query, + n: 6, + ); + }); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart index 949e946188..eda320bdb3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart @@ -48,6 +48,7 @@ class _UnsplashImageWidgetState extends State { @override void initState() { super.initState(); + randomPhotos = unsplash.photos .random(count: 18, orientation: PhotoOrientation.landscape) .goAndGet(); @@ -56,6 +57,7 @@ class _UnsplashImageWidgetState extends State { @override void dispose() { unsplash.close(); + super.dispose(); } @@ -130,16 +132,18 @@ class _UnsplashImagesState extends State<_UnsplashImages> { @override Widget build(BuildContext context) { - const mainAxisSpacing = 16.0; final crossAxisCount = switch (widget.type) { UnsplashImageType.halfScreen => 3, UnsplashImageType.fullScreen => 2, }; + final mainAxisSpacing = switch (widget.type) { + UnsplashImageType.halfScreen => 16.0, + UnsplashImageType.fullScreen => 16.0, + }; final crossAxisSpacing = switch (widget.type) { UnsplashImageType.halfScreen => 10.0, UnsplashImageType.fullScreen => 16.0, }; - return GridView.count( crossAxisCount: crossAxisCount, mainAxisSpacing: mainAxisSpacing, @@ -151,11 +155,15 @@ class _UnsplashImagesState extends State<_UnsplashImages> { return _UnsplashImage( type: widget.type, photo: photo, - isSelected: index == _selectedPhotoIndex, onTap: () { - widget.onSelectUnsplashImage(photo.urls.full.toString()); - setState(() => _selectedPhotoIndex = index); + widget.onSelectUnsplashImage( + photo.urls.regular.toString(), + ); + setState(() { + _selectedPhotoIndex = index; + }); }, + isSelected: index == _selectedPhotoIndex, ); }).toList(), ); @@ -211,7 +219,10 @@ class _UnsplashImage extends StatelessWidget { ), ), const HSpace(2.0), - FlowyText('by ${photo.name}', fontSize: 10.0), + FlowyText( + 'by ${photo.name}', + fontSize: 10.0, + ), ], ); } @@ -222,12 +233,14 @@ class _UnsplashImage extends StatelessWidget { child: Stack( children: [ LayoutBuilder( - builder: (_, constraints) => Image.network( - photo.urls.thumb.toString(), - fit: BoxFit.cover, - width: constraints.maxWidth, - height: constraints.maxHeight, - ), + builder: (context, constraints) { + return Image.network( + photo.urls.thumb.toString(), + fit: BoxFit.cover, + width: constraints.maxWidth, + height: constraints.maxHeight, + ); + }, ), Positioned( bottom: 9, @@ -248,9 +261,13 @@ extension on Photo { String get name { if (user.username.isNotEmpty) { return user.username; - } else if (user.name.isNotEmpty) { + } + + if (user.name.isNotEmpty) { return user.name; - } else if (user.email?.isNotEmpty == true) { + } + + if (user.email?.isNotEmpty == true) { return user.email!; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart similarity index 75% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart index f0310a4aa5..017e5a94b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/custom_image_block_component/unsupport_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart @@ -1,13 +1,14 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; -class UnsupportedImageWidget extends StatelessWidget { - const UnsupportedImageWidget({super.key}); +class UnSupportImageWidget extends StatelessWidget { + const UnSupportImageWidget({ + super.key, + }); @override Widget build(BuildContext context) { @@ -17,7 +18,9 @@ class UnsupportedImageWidget extends StatelessWidget { borderRadius: BorderRadius.circular(4), ), child: FlowyHover( - style: HoverStyle(borderRadius: BorderRadius.circular(4)), + style: HoverStyle( + borderRadius: BorderRadius.circular(4), + ), child: SizedBox( height: 52, child: Row( @@ -28,7 +31,9 @@ class UnsupportedImageWidget extends StatelessWidget { size: Size.square(24), ), const HSpace(10), - FlowyText(LocaleKeys.document_imageBlock_unableToLoadImage.tr()), + FlowyText( + LocaleKeys.document_imageBlock_unableToLoadImage.tr(), + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart new file mode 100644 index 0000000000..d4d94be091 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/permission/permission_checker.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; + +class UploadImageFileWidget extends StatelessWidget { + const UploadImageFileWidget({ + super.key, + required this.onPickFile, + this.allowedExtensions = const ['jpg', 'png', 'jpeg'], + }); + + final void Function(String? path) onPickFile; + final List allowedExtensions; + + @override + Widget build(BuildContext context) { + final child = FlowyButton( + showDefaultBoxDecorationOnMobile: true, + text: Container( + margin: const EdgeInsets.all(4.0), + alignment: Alignment.center, + child: FlowyText( + LocaleKeys.document_imageBlock_upload_placeholder.tr(), + ), + ), + onTap: () => _uploadImage(context), + ); + + if (PlatformExtension.isDesktopOrWeb) { + return FlowyHover( + child: child, + ); + } + + return child; + } + + Future _uploadImage(BuildContext context) async { + if (PlatformExtension.isDesktopOrWeb) { + // on desktop, the users can pick a image file from folder + final result = await getIt().pickFiles( + dialogTitle: '', + type: FileType.custom, + allowedExtensions: allowedExtensions, + ); + onPickFile(result?.files.firstOrNull?.path); + } else { + final photoPermission = + await PermissionChecker.checkPhotoPermission(context); + if (!photoPermission) { + Log.error('Has no permission to access the photo library'); + return; + } + // on mobile, the users can pick a image file from camera or image library + final result = await ImagePicker().pickImage(source: ImageSource.gallery); + onPickFile(result?.path); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart new file mode 100644 index 0000000000..0679f87996 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -0,0 +1,250 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.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_file_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; + +enum UploadImageType { + local, + url, + unsplash, + stabilityAI, + openAI, + color; + + String get description { + switch (this) { + case UploadImageType.local: + return LocaleKeys.document_imageBlock_upload_label.tr(); + case UploadImageType.url: + return LocaleKeys.document_imageBlock_embedLink_label.tr(); + case UploadImageType.unsplash: + return LocaleKeys.document_imageBlock_unsplash_label.tr(); + case UploadImageType.openAI: + return LocaleKeys.document_imageBlock_ai_label.tr(); + case UploadImageType.stabilityAI: + return LocaleKeys.document_imageBlock_stability_ai_label.tr(); + case UploadImageType.color: + return LocaleKeys.document_plugins_cover_colors.tr(); + } + } +} + +class UploadImageMenu extends StatefulWidget { + const UploadImageMenu({ + super.key, + required this.onSelectedLocalImage, + required this.onSelectedAIImage, + required this.onSelectedNetworkImage, + this.onSelectedColor, + this.supportTypes = UploadImageType.values, + this.limitMaximumImageSize = false, + }); + + final void Function(String? path) onSelectedLocalImage; + final void Function(String url) onSelectedAIImage; + final void Function(String url) onSelectedNetworkImage; + final void Function(String color)? onSelectedColor; + final List supportTypes; + final bool limitMaximumImageSize; + + @override + State createState() => _UploadImageMenuState(); +} + +class _UploadImageMenuState extends State { + late final List values; + int currentTabIndex = 0; + bool supportOpenAI = false; + bool supportStabilityAI = false; + + @override + void initState() { + super.initState(); + + values = widget.supportTypes; + UserBackendService.getCurrentUserProfile().then( + (value) { + final supportOpenAI = value.fold( + (s) => s.openaiKey.isNotEmpty, + (e) => false, + ); + final supportStabilityAI = value.fold( + (s) => s.stabilityAiKey.isNotEmpty, + (e) => false, + ); + if (supportOpenAI != this.supportOpenAI || + supportStabilityAI != this.supportStabilityAI) { + setState(() { + this.supportOpenAI = supportOpenAI; + this.supportStabilityAI = supportStabilityAI; + }); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: values.length, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TabBar( + onTap: (value) => setState(() { + currentTabIndex = value; + }), + indicatorSize: TabBarIndicatorSize.label, + isScrollable: true, + overlayColor: WidgetStatePropertyAll( + PlatformExtension.isDesktop + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + ), + padding: EdgeInsets.zero, + tabs: values.map( + (e) { + final child = Padding( + padding: EdgeInsets.only( + left: 12.0, + right: 12.0, + bottom: 8.0, + top: PlatformExtension.isMobile ? 0 : 8.0, + ), + child: FlowyText(e.description), + ); + if (PlatformExtension.isDesktop) { + return FlowyHover( + style: const HoverStyle(borderRadius: BorderRadius.zero), + child: child, + ); + } + return child; + }, + ).toList(), + ), + const Divider( + height: 2, + ), + _buildTab(), + ], + ), + ); + } + + Widget _buildTab() { + final constraints = + PlatformExtension.isMobile ? const BoxConstraints(minHeight: 92) : null; + final type = values[currentTabIndex]; + switch (type) { + case UploadImageType.local: + return Container( + padding: const EdgeInsets.all(8.0), + alignment: Alignment.center, + constraints: constraints, + child: Column( + children: [ + UploadImageFileWidget( + onPickFile: widget.onSelectedLocalImage, + ), + if (widget.limitMaximumImageSize) ...[ + const VSpace(6.0), + FlowyText( + LocaleKeys.document_imageBlock_maximumImageSize.tr(), + fontSize: 12.0, + color: Theme.of(context).hintColor, + ), + ], + ], + ), + ); + case UploadImageType.url: + return Container( + padding: const EdgeInsets.all(8.0), + constraints: constraints, + child: EmbedImageUrlWidget( + onSubmit: widget.onSelectedNetworkImage, + ), + ); + case UploadImageType.unsplash: + return Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: UnsplashImageWidget( + onSelectUnsplashImage: widget.onSelectedNetworkImage, + ), + ), + ); + case UploadImageType.openAI: + return supportOpenAI + ? Expanded( + child: Container( + padding: const EdgeInsets.all(8.0), + constraints: constraints, + child: OpenAIImageWidget( + onSelectNetworkImage: widget.onSelectedAIImage, + ), + ), + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyText( + LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(), + ), + ); + case UploadImageType.stabilityAI: + return supportStabilityAI + ? Expanded( + child: Container( + padding: const EdgeInsets.all(8.0), + child: StabilityAIImageWidget( + onSelectImage: widget.onSelectedLocalImage, + ), + ), + ) + : Padding( + padding: const EdgeInsets.all(8.0), + child: FlowyText( + LocaleKeys.document_imageBlock_pleaseInputYourStabilityAIKey + .tr(), + ), + ); + case UploadImageType.color: + final theme = Theme.of(context); + final padding = PlatformExtension.isMobile + ? const EdgeInsets.all(16.0) + : const EdgeInsets.all(8.0); + return Container( + constraints: constraints, + padding: padding, + alignment: Alignment.center, + child: CoverColorPicker( + pickerBackgroundColor: theme.cardColor, + pickerItemHoverColor: theme.hoverColor, + backgroundColorOptions: FlowyTint.values + .map( + (t) => ColorOption( + colorHex: t.color(context).toHex(), + name: t.tintName(AppFlowyEditorL10n.current), + ), + ) + .toList(), + onSubmittedBackgroundColorHex: (color) { + widget.onSelectedColor?.call(color); + }, + ), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart deleted file mode 100644 index 836f087797..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.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/widgets/upload_image_file_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption; -import 'package:cross_file/cross_file.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:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'widgets/embed_image_url_widget.dart'; - -enum UploadImageType { - local, - url, - unsplash, - color; - - String get description => switch (this) { - UploadImageType.local => - LocaleKeys.document_imageBlock_upload_label.tr(), - UploadImageType.url => - LocaleKeys.document_imageBlock_embedLink_label.tr(), - UploadImageType.unsplash => - LocaleKeys.document_imageBlock_unsplash_label.tr(), - UploadImageType.color => LocaleKeys.document_plugins_cover_colors.tr(), - }; -} - -class UploadImageMenu extends StatefulWidget { - const UploadImageMenu({ - super.key, - required this.onSelectedLocalImages, - required this.onSelectedAIImage, - required this.onSelectedNetworkImage, - this.onSelectedColor, - this.supportTypes = UploadImageType.values, - this.limitMaximumImageSize = false, - this.allowMultipleImages = false, - }); - - final void Function(List) onSelectedLocalImages; - final void Function(String url) onSelectedAIImage; - final void Function(String url) onSelectedNetworkImage; - final void Function(String color)? onSelectedColor; - final List supportTypes; - final bool limitMaximumImageSize; - final bool allowMultipleImages; - - @override - State createState() => _UploadImageMenuState(); -} - -class _UploadImageMenuState extends State { - late final List values; - int currentTabIndex = 0; - - @override - void initState() { - super.initState(); - values = widget.supportTypes; - } - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: values.length, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TabBar( - onTap: (value) => setState(() { - currentTabIndex = value; - }), - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - overlayColor: WidgetStatePropertyAll( - UniversalPlatform.isDesktop - ? Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - padding: EdgeInsets.zero, - tabs: values.map( - (e) { - final child = Padding( - padding: EdgeInsets.only( - left: 12.0, - right: 12.0, - bottom: 8.0, - top: UniversalPlatform.isMobile ? 0 : 8.0, - ), - child: FlowyText(e.description), - ); - if (UniversalPlatform.isDesktop) { - return FlowyHover( - style: const HoverStyle(borderRadius: BorderRadius.zero), - child: child, - ); - } - return child; - }, - ).toList(), - ), - const Divider(height: 2), - _buildTab(), - ], - ), - ); - } - - Widget _buildTab() { - final constraints = - UniversalPlatform.isMobile ? const BoxConstraints(minHeight: 92) : null; - final type = values[currentTabIndex]; - switch (type) { - case UploadImageType.local: - Widget child = UploadImageFileWidget( - allowMultipleImages: widget.allowMultipleImages, - onPickFiles: widget.onSelectedLocalImages, - ); - if (UniversalPlatform.isDesktop) { - child = Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - ), - constraints: constraints, - child: child, - ), - ); - } else { - child = Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 12.0, - ), - child: child, - ); - } - return child; - - case UploadImageType.url: - return Container( - padding: const EdgeInsets.all(8.0), - constraints: constraints, - child: EmbedImageUrlWidget( - onSubmit: widget.onSelectedNetworkImage, - ), - ); - case UploadImageType.unsplash: - return Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: UnsplashImageWidget( - onSelectUnsplashImage: widget.onSelectedNetworkImage, - ), - ), - ); - case UploadImageType.color: - final theme = Theme.of(context); - final padding = UniversalPlatform.isMobile - ? const EdgeInsets.all(16.0) - : const EdgeInsets.all(8.0); - return Container( - constraints: constraints, - padding: padding, - alignment: Alignment.center, - child: CoverColorPicker( - pickerBackgroundColor: theme.cardColor, - pickerItemHoverColor: theme.hoverColor, - backgroundColorOptions: FlowyTint.values - .map( - (t) => ColorOption( - colorHex: t.color(context).toHex(), - name: t.tintName(AppFlowyEditorL10n.current), - ), - ) - .toList(), - onSubmittedBackgroundColorHex: (color) { - widget.onSelectedColor?.call(color); - }, - ), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart deleted file mode 100644 index 28dccad72d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class EmbedImageUrlWidget extends StatefulWidget { - const EmbedImageUrlWidget({ - super.key, - required this.onSubmit, - }); - - final void Function(String url) onSubmit; - - @override - State createState() => _EmbedImageUrlWidgetState(); -} - -class _EmbedImageUrlWidgetState extends State { - bool isUrlValid = true; - String inputText = ''; - - @override - Widget build(BuildContext context) { - final textField = FlowyTextField( - hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), - onChanged: (value) => inputText = value, - onEditingComplete: submit, - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14, - ), - hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).hintColor, - fontSize: 14, - ), - ); - return Column( - children: [ - const VSpace(12), - UniversalPlatform.isDesktop - ? textField - : SizedBox( - height: 42, - child: textField, - ), - if (!isUrlValid) ...[ - const VSpace(12), - FlowyText( - LocaleKeys.document_plugins_cover_invalidImageUrl.tr(), - color: Theme.of(context).colorScheme.error, - ), - ], - const VSpace(20), - SizedBox( - height: UniversalPlatform.isMobile ? 36 : 32, - width: 300, - child: FlowyButton( - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - showDefaultBoxDecorationOnMobile: true, - radius: - UniversalPlatform.isMobile ? BorderRadius.circular(8) : null, - margin: const EdgeInsets.all(5), - text: FlowyText( - LocaleKeys.document_imageBlock_embedLink_label.tr(), - lineHeight: 1, - textAlign: TextAlign.center, - color: UniversalPlatform.isMobile - ? null - : Theme.of(context).colorScheme.onPrimary, - fontSize: UniversalPlatform.isMobile ? 14 : null, - ), - onTap: submit, - ), - ), - const VSpace(8), - ], - ); - } - - void submit() { - if (checkUrlValidity(inputText)) { - return widget.onSubmit(inputText); - } - - setState(() => isUrlValid = false); - } - - bool checkUrlValidity(String url) => imgUrlRegex.hasMatch(url); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart deleted file mode 100644 index f08c74b84e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/default_extensions.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class UploadImageFileWidget extends StatelessWidget { - const UploadImageFileWidget({ - super.key, - required this.onPickFiles, - this.allowedExtensions = defaultImageExtensions, - this.allowMultipleImages = false, - }); - - final void Function(List) onPickFiles; - final List allowedExtensions; - final bool allowMultipleImages; - - @override - Widget build(BuildContext context) { - Widget child = FlowyButton( - showDefaultBoxDecorationOnMobile: true, - radius: UniversalPlatform.isMobile ? BorderRadius.circular(8.0) : null, - text: Container( - margin: const EdgeInsets.all(4.0), - alignment: Alignment.center, - child: FlowyText( - LocaleKeys.document_imageBlock_upload_placeholder.tr(), - ), - ), - onTap: () => _uploadImage(context), - ); - - if (UniversalPlatform.isDesktopOrWeb) { - child = FlowyHover(child: child); - } else { - child = Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: child, - ); - } - - return child; - } - - Future _uploadImage(BuildContext context) async { - if (UniversalPlatform.isDesktopOrWeb) { - // on desktop, the users can pick a image file from folder - final result = await getIt().pickFiles( - dialogTitle: '', - type: FileType.custom, - allowedExtensions: allowedExtensions, - allowMultiple: allowMultipleImages, - ); - onPickFiles(result?.files.map((f) => f.xFile).toList() ?? const []); - } else { - final photoPermission = - await PermissionChecker.checkPhotoPermission(context); - if (!photoPermission) { - Log.error('Has no permission to access the photo library'); - return; - } - // on mobile, the users can pick a image file from camera or image library - final result = await ImagePicker().pickMultiImage(); - onPickFiles(result); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart index e41bdc1114..7ce143acba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; @@ -36,6 +37,7 @@ class _InlineMathEquationState extends State { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return _IgnoreParentPointer( child: AppFlowyPopover( controller: popoverController, @@ -59,40 +61,33 @@ class _InlineMathEquationState extends State { }, offset: const Offset(0, 10), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), + padding: const EdgeInsets.symmetric(vertical: 8.0), child: MouseRegion( cursor: SystemMouseCursors.click, - child: _buildMathEquation(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(2), + Math.tex( + widget.formula, + options: MathOptions( + style: MathStyle.text, + mathFontOptions: const FontOptions( + fontShape: FontStyle.italic, + ), + fontSize: 14.0, + color: widget.textStyle?.color ?? + theme.colorScheme.onSurface, + ), + ), + const HSpace(2), + ], + ), ), ), ), ); } - - Widget _buildMathEquation(BuildContext context) { - final theme = Theme.of(context); - final longEq = Math.tex( - widget.formula, - textStyle: widget.textStyle, - mathStyle: MathStyle.text, - options: MathOptions( - style: MathStyle.text, - mathFontOptions: const FontOptions( - fontShape: FontStyle.italic, - ), - fontSize: widget.textStyle?.fontSize ?? 14.0, - color: widget.textStyle?.color ?? theme.colorScheme.onSurface, - ), - onErrorFallback: (errmsg) { - return FlowyText( - errmsg.message, - fontSize: widget.textStyle?.fontSize ?? 14.0, - color: widget.textStyle?.color ?? theme.colorScheme.onSurface, - ); - }, - ); - return longEq; - } } class MathInputTextField extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart index cd3779cb6c..08c23df05b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation_toolbar_item.dart @@ -5,13 +5,11 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -const _kInlineMathEquationToolbarItemId = 'editor.inline_math_equation'; - final ToolbarItem inlineMathEquationItem = ToolbarItem( - id: _kInlineMathEquationToolbarItemId, - group: 4, + id: 'editor.inline_math_equation', + group: 2, isActive: onlyShowInSingleSelectionAndTextType, - builder: (context, editorState, highlightColor, _, tooltipBuilder) { + builder: (context, editorState, highlightColor, _) { final selection = editorState.selection!; final nodes = editorState.getNodesInSelection(selection); final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { @@ -19,7 +17,7 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem( (attributes) => attributes[InlineMathEquationKeys.formula] != null, ); }); - final child = SVGIconItemWidget( + return SVGIconItemWidget( iconBuilder: (_) => FlowySvg( FlowySvgs.math_lg, size: const Size.square(16), @@ -27,6 +25,7 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem( ), isHighlight: isHighlight, highlightColor: highlightColor, + tooltip: LocaleKeys.document_plugins_createInlineMathEquation.tr(), onPressed: () async { final selection = editorState.selection; if (selection == null || selection.isCollapsed) { @@ -63,7 +62,7 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem( node, selection.startIndex, selection.length, - MentionBlockKeys.mentionChar, + '\$', attributes: { InlineMathEquationKeys.formula: text, }, @@ -72,16 +71,5 @@ final ToolbarItem inlineMathEquationItem = ToolbarItem( await editorState.apply(transaction); }, ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - _kInlineMathEquationToolbarItemId, - LocaleKeys.document_plugins_createInlineMathEquation.tr(), - child, - ); - } - - return child; }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart deleted file mode 100644 index 040989243f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class EditorKeyboardInterceptor extends AppFlowyKeyboardServiceInterceptor { - @override - Future interceptInsert( - TextEditingDeltaInsertion insertion, - EditorState editorState, - List characterShortcutEvents, - ) async { - // Only check on the mobile platform: check if the inserted text is a link, if so, try to paste it as a link preview - final text = insertion.textInserted; - if (UniversalPlatform.isMobile && hrefRegex.hasMatch(text)) { - final result = customPasteCommand.execute(editorState); - return result == KeyEventResult.handled; - } - return false; - } - - @override - Future interceptReplace( - TextEditingDeltaReplacement replacement, - EditorState editorState, - List characterShortcutEvents, - ) async { - // Only check on the mobile platform: check if the replaced text is a link, if so, try to paste it as a link preview - final text = replacement.replacementText; - if (UniversalPlatform.isMobile && hrefRegex.hasMatch(text)) { - final result = customPasteCommand.execute(editorState); - return result == KeyEventResult.handled; - } - return false; - } - - @override - Future interceptNonTextUpdate( - TextEditingDeltaNonTextUpdate nonTextUpdate, - EditorState editorState, - List characterShortcutEvents, - ) async { - return _checkIfBacktickPressed( - editorState, - nonTextUpdate, - ); - } - - @override - Future interceptDelete( - TextEditingDeltaDeletion deletion, - EditorState editorState, - ) async { - // check if the current selection is in a code block - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return false; - } - - final onlyContainsOneChild = tableCellNode.children.length == 1; - final isParagraphNode = - tableCellNode.children.first.type == ParagraphBlockKeys.type; - if (onlyContainsOneChild && - selection.isCollapsed && - selection.end.offset == 0 && - isParagraphNode) { - return true; - } - - return false; - } - - /// Check if the backtick pressed event should be handled - Future _checkIfBacktickPressed( - EditorState editorState, - TextEditingDeltaNonTextUpdate nonTextUpdate, - ) async { - // if the composing range is not empty, it means the user is typing a text, - // so we don't need to handle the backtick pressed event - if (!nonTextUpdate.composing.isCollapsed || - !nonTextUpdate.selection.isCollapsed) { - return false; - } - - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - AppFlowyEditorLog.input.debug('selection is null or not collapsed'); - return false; - } - - final node = editorState.getNodesInSelection(selection).firstOrNull; - if (node == null) { - AppFlowyEditorLog.input.debug('node is null'); - return false; - } - - // get last character of the node - final plainText = node.delta?.toPlainText(); - // three backticks to code block - if (plainText != '```') { - return false; - } - - final transaction = editorState.transaction; - transaction.insertNode( - selection.end.path, - codeBlockNode(), - ); - transaction.deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position(path: selection.start.path), - ); - await editorState.apply(transaction); - - return true; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart deleted file mode 100644 index baf9702a36..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart +++ /dev/null @@ -1,310 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:appflowy_ui/appflowy_ui.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 'link_embed_menu.dart'; - -class LinkEmbedKeys { - const LinkEmbedKeys._(); - static const String previewType = 'preview_type'; - static const String embed = 'embed'; - static const String align = 'align'; -} - -Node linkEmbedNode({required String url}) => Node( - type: LinkPreviewBlockKeys.type, - attributes: { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: LinkEmbedKeys.embed, - }, - ); - -class LinkEmbedBlockComponent extends BlockComponentStatefulWidget { - const LinkEmbedBlockComponent({ - super.key, - super.showActions, - super.actionBuilder, - super.configuration = const BlockComponentConfiguration(), - required super.node, - }); - - @override - DefaultSelectableMixinState createState() => - LinkEmbedBlockComponentState(); -} - -class LinkEmbedBlockComponentState - extends DefaultSelectableMixinState - with BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - String get url => widget.node.attributes[LinkPreviewBlockKeys.url] ?? ''; - - LinkLoadingStatus status = LinkLoadingStatus.loading; - final parser = LinkParser(); - late LinkInfo linkInfo = LinkInfo(url: url); - - final showActionsNotifier = ValueNotifier(false); - bool isMenuShowing = false, isHovering = false; - - @override - void initState() { - super.initState(); - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = LinkLoadingStatus.idle; - } else if (!hasOldInfo) { - status = LinkLoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - parser.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget result = MouseRegion( - onEnter: (_) { - isHovering = true; - showActionsNotifier.value = true; - }, - onExit: (_) { - isHovering = false; - Future.delayed(const Duration(milliseconds: 200), () { - if (isMenuShowing || isHovering) return; - if (mounted) showActionsNotifier.value = false; - }); - }, - child: buildChild(context), - ); - final parent = node.parent; - EdgeInsets newPadding = padding; - if (parent?.type == CalloutBlockKeys.type) { - newPadding = padding.copyWith(right: padding.right + 10); - } - - result = Padding(padding: newPadding, child: result); - - if (widget.showActions && widget.actionBuilder != null) { - result = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - child: result, - ); - } - return result; - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - fillSceme = theme.fillColorScheme, - borderScheme = theme.borderColorScheme; - Widget child; - final isIdle = status == LinkLoadingStatus.idle; - if (isIdle) { - child = buildContent(context); - } else { - child = buildErrorLoadingWidget(context); - } - return Container( - height: 450, - key: widgetKey, - decoration: BoxDecoration( - color: isIdle ? Theme.of(context).cardColor : fillSceme.tertiaryHover, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: Border.all(color: borderScheme.greyTertiary), - ), - child: Stack( - children: [ - child, - buildMenu(context), - ], - ), - ); - } - - Widget buildMenu(BuildContext context) { - return Positioned( - top: 12, - right: 12, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, showActions, child) { - if (!showActions) return SizedBox.shrink(); - return LinkEmbedMenu( - editorState: context.read(), - node: node, - onReload: () { - setState(() { - status = LinkLoadingStatus.loading; - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) parser.start(url); - }); - }, - onMenuShowed: () { - isMenuShowing = true; - }, - onMenuHided: () { - isMenuShowing = false; - if (!isHovering && mounted) { - showActionsNotifier.value = false; - } - }, - ); - }, - ), - ); - } - - Widget buildContent(BuildContext context) { - final theme = AppFlowyTheme.of(context), textScheme = theme.textColorScheme; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: ClipRRect( - borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), - child: FlowyNetworkImage( - url: linkInfo.imageUrl ?? '', - width: MediaQuery.of(context).size.width, - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => - afLaunchUrlString(url, addingHttpSchemeWhenFailed: true), - child: Container( - height: 64, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - child: Row( - children: [ - SizedBox.square( - dimension: 40, - child: Center( - child: linkInfo.buildIconWidget(size: Size.square(32)), - ), - ), - HSpace(12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - linkInfo.siteName ?? '', - color: textScheme.primary, - fontSize: 14, - figmaLineHeight: 20, - fontWeight: FontWeight.w600, - overflow: TextOverflow.ellipsis, - ), - VSpace(4), - FlowyText.regular( - url, - color: textScheme.secondary, - fontSize: 12, - figmaLineHeight: 16, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ], - ); - } - - Widget buildErrorLoadingWidget(BuildContext context) { - final theme = AppFlowyTheme.of(context), textSceme = theme.textColorScheme; - final isLoading = status == LinkLoadingStatus.loading; - return isLoading - ? Center( - child: SizedBox.square( - dimension: 64, - child: CircularProgressIndicator.adaptive(), - ), - ) - : Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset( - FlowySvgs.embed_error_xl.path, - ), - VSpace(4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: RichText( - maxLines: 1, - overflow: TextOverflow.ellipsis, - text: TextSpan( - children: [ - TextSpan( - text: '$url ', - style: TextStyle( - color: textSceme.primary, - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w700, - ), - ), - TextSpan( - text: LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_unableToDisplay - .tr(), - style: TextStyle( - color: textSceme.primary, - fontSize: 14, - height: 20 / 14, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - - @override - Node get currentNode => node; - - @override - EdgeInsets get boxPadding => padding; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart deleted file mode 100644 index c3d2aebbcc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_embed/link_embed_menu.dart +++ /dev/null @@ -1,354 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -import 'link_embed_block_component.dart'; - -class LinkEmbedMenu extends StatefulWidget { - const LinkEmbedMenu({ - super.key, - required this.node, - required this.editorState, - required this.onMenuShowed, - required this.onMenuHided, - required this.onReload, - }); - - final Node node; - final EditorState editorState; - final VoidCallback onMenuShowed; - final VoidCallback onMenuHided; - final VoidCallback onReload; - - @override - State createState() => _LinkEmbedMenuState(); -} - -class _LinkEmbedMenuState extends State { - final turnintoController = PopoverController(); - final moreOptionController = PopoverController(); - int turnintoMenuNum = 0, moreOptionNum = 0, alignMenuNum = 0; - final moreOptionButtonKey = GlobalKey(); - bool get isTurnIntoShowing => turnintoMenuNum > 0; - bool get isMoreOptionShowing => moreOptionNum > 0; - bool get isAlignMenuShowing => alignMenuNum > 0; - - Node get node => widget.node; - EditorState get editorState => widget.editorState; - - String get url => node.attributes[LinkPreviewBlockKeys.url] ?? ''; - - @override - void dispose() { - super.dispose(); - turnintoController.close(); - moreOptionController.close(); - widget.onMenuHided.call(); - } - - @override - Widget build(BuildContext context) { - return buildChild(); - } - - Widget buildChild() { - final theme = AppFlowyTheme.of(context), - iconScheme = theme.iconColorScheme, - fillScheme = theme.fillColorScheme; - - return Container( - padding: EdgeInsets.all(4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: fillScheme.primaryAlpha80, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // FlowyIconButton( - // icon: FlowySvg( - // FlowySvgs.embed_fullscreen_m, - // color: iconScheme.tertiary, - // ), - // tooltipText: LocaleKeys.document_imageBlock_openFullScreen.tr(), - // preferBelow: false, - // onPressed: () {}, - // ), - FlowyIconButton( - icon: FlowySvg( - FlowySvgs.toolbar_link_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - onPressed: () => copyLink(context), - ), - buildconvertBotton(), - buildMoreOptionBotton(), - ], - ), - ); - } - - Widget buildconvertBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; - return AppFlowyPopover( - offset: Offset(0, 6), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: turnintoController, - onOpen: () { - keepEditorFocusNotifier.increase(); - turnintoMenuNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - turnintoMenuNum--; - checkToHideMenu(); - }, - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg( - FlowySvgs.turninto_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - onPressed: showTurnIntoMenu, - ), - ); - } - - Widget buildConvertMenu() { - final types = LinkEmbedConvertCommand.values; - return Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(types.length, (index) { - final command = types[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () { - if (command == LinkEmbedConvertCommand.toBookmark) { - final transaction = editorState.transaction; - transaction.updateNode(node, { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: '', - }); - editorState.apply(transaction); - } else if (command == LinkEmbedConvertCommand.toMention) { - convertUrlPreviewNodeToMention(editorState, node); - } else if (command == LinkEmbedConvertCommand.toURL) { - convertUrlPreviewNodeToLink(editorState, node); - } - }, - ), - ); - }), - ), - ); - } - - Widget buildMoreOptionBotton() { - final theme = AppFlowyTheme.of(context), iconScheme = theme.iconColorScheme; - return AppFlowyPopover( - offset: Offset(0, 6), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: moreOptionController, - onOpen: () { - keepEditorFocusNotifier.increase(); - moreOptionNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - moreOptionNum--; - checkToHideMenu(); - }, - popupBuilder: (context) => buildMoreOptionMenu(), - child: FlowyIconButton( - key: moreOptionButtonKey, - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - color: iconScheme.tertiary, - ), - radius: BorderRadius.all(Radius.circular(theme.borderRadius.m)), - tooltipText: LocaleKeys.document_toolbar_moreOptions.tr(), - preferBelow: false, - onPressed: showMoreOptionMenu, - ), - ); - } - - Widget buildMoreOptionMenu() { - final types = LinkEmbedMenuCommand.values; - return Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(types.length, (index) { - final command = types[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onEmbedMenuCommand(command), - ), - ); - }), - ), - ); - } - - void showTurnIntoMenu() { - keepEditorFocusNotifier.increase(); - turnintoController.show(); - checkToShowMenu(); - turnintoMenuNum++; - if (isMoreOptionShowing) closeMoreOptionMenu(); - } - - void closeTurnIntoMenu() { - turnintoController.close(); - checkToHideMenu(); - } - - void showMoreOptionMenu() { - keepEditorFocusNotifier.increase(); - moreOptionController.show(); - checkToShowMenu(); - moreOptionNum++; - if (isTurnIntoShowing) closeTurnIntoMenu(); - } - - void closeMoreOptionMenu() { - moreOptionController.close(); - checkToHideMenu(); - } - - void checkToHideMenu() { - Future.delayed(Duration(milliseconds: 200), () { - if (!mounted) return; - if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { - widget.onMenuHided.call(); - } - }); - } - - void checkToShowMenu() { - if (!isAlignMenuShowing && !isMoreOptionShowing && !isTurnIntoShowing) { - widget.onMenuShowed.call(); - } - } - - Future copyLink(BuildContext context) async { - await context.copyLink(url); - widget.onMenuHided.call(); - } - - void onEmbedMenuCommand(LinkEmbedMenuCommand command) { - switch (command) { - case LinkEmbedMenuCommand.openLink: - afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - break; - case LinkEmbedMenuCommand.replace: - final box = moreOptionButtonKey.currentContext?.findRenderObject() - as RenderBox?; - if (box == null) return; - final p = box.localToGlobal(Offset.zero); - showReplaceMenu( - context: context, - editorState: editorState, - node: node, - url: url, - ltrb: LTRB(left: p.dx - 330, top: p.dy), - onReplace: (url) async { - await convertLinkBlockToOtherLinkBlock( - editorState, - node, - node.type, - url: url, - ); - }, - ); - break; - case LinkEmbedMenuCommand.reload: - widget.onReload.call(); - break; - case LinkEmbedMenuCommand.removeLink: - removeUrlPreviewLink(editorState, node); - break; - } - closeMoreOptionMenu(); - } -} - -enum LinkEmbedMenuCommand { - openLink, - replace, - reload, - removeLink; - - String get title { - switch (this) { - case openLink: - return LocaleKeys.editor_openLink.tr(); - case replace: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace - .tr(); - case reload: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} - -enum LinkEmbedConvertCommand { - toMention, - toURL, - toBookmark; - - String get title { - switch (this) { - case toMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart deleted file mode 100644 index 1907f68d29..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:convert'; -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/appflowy_network_svg.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/material.dart'; -import 'link_parsers/default_parser.dart'; -import 'link_parsers/youtube_parser.dart'; - -class LinkParser { - final Set> _listeners = >{}; - static final Map _hostToParsers = { - 'www.youtube.com': YoutubeParser(), - 'youtube.com': YoutubeParser(), - 'youtu.be': YoutubeParser(), - }; - - Future start(String url, {LinkInfoParser? parser}) async { - final uri = Uri.tryParse(LinkInfoParser.formatUrl(url)) ?? Uri.parse(url); - final data = await LinkInfoCache.get(uri); - if (data != null) { - refreshLinkInfo(data); - } - - final host = uri.host; - final currentParser = parser ?? _hostToParsers[host] ?? DefaultParser(); - await _getLinkInfo(uri, currentParser); - } - - Future _getLinkInfo(Uri uri, LinkInfoParser parser) async { - try { - final linkInfo = await parser.parse(uri) ?? LinkInfo(url: '$uri'); - if (!linkInfo.isEmpty()) await LinkInfoCache.set(uri, linkInfo); - refreshLinkInfo(linkInfo); - return linkInfo; - } catch (e, s) { - Log.error('get link info error: ', e, s); - refreshLinkInfo(LinkInfo(url: '$uri')); - return null; - } - } - - void refreshLinkInfo(LinkInfo info) { - for (final listener in _listeners) { - listener(info); - } - } - - void addLinkInfoListener(ValueChanged listener) { - _listeners.add(listener); - } - - void dispose() { - _listeners.clear(); - } -} - -class LinkInfo { - factory LinkInfo.fromJson(Map json) => LinkInfo( - siteName: json['siteName'], - url: json['url'] ?? '', - title: json['title'], - description: json['description'], - imageUrl: json['imageUrl'], - faviconUrl: json['faviconUrl'], - ); - - LinkInfo({ - required this.url, - this.siteName, - this.title, - this.description, - this.imageUrl, - this.faviconUrl, - }); - - final String url; - final String? siteName; - final String? title; - final String? description; - final String? imageUrl; - final String? faviconUrl; - - Map toJson() => { - 'url': url, - 'siteName': siteName, - 'title': title, - 'description': description, - 'imageUrl': imageUrl, - 'faviconUrl': faviconUrl, - }; - - @override - String toString() { - return 'LinkInfo{url: $url, siteName: $siteName, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl}'; - } - - bool isEmpty() { - return title == null; - } - - Widget buildIconWidget({Size size = const Size.square(20.0)}) { - final iconUrl = faviconUrl; - if (iconUrl == null) { - return FlowySvg(FlowySvgs.toolbar_link_earth_m, size: size); - } - if (iconUrl.endsWith('.svg')) { - return FlowyNetworkSvg( - iconUrl, - height: size.height, - width: size.width, - errorWidget: const FlowySvg(FlowySvgs.toolbar_link_earth_m), - ); - } - return FlowyNetworkImage( - url: iconUrl, - fit: BoxFit.contain, - height: size.height, - width: size.width, - errorWidgetBuilder: (context, error, stackTrace) => - const FlowySvg(FlowySvgs.toolbar_link_earth_m), - ); - } -} - -class LinkInfoCache { - static const _linkInfoPrefix = 'link_info'; - - static Future get(Uri uri) async { - final option = await getIt().getWithFormat( - '$_linkInfoPrefix$uri', - (value) => LinkInfo.fromJson(jsonDecode(value)), - ); - return option; - } - - static Future set(Uri uri, LinkInfo data) async { - await getIt().set( - '$_linkInfoPrefix$uri', - jsonEncode(data.toJson()), - ); - } -} - -enum LinkLoadingStatus { - loading, - idle, - error, -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart index 9be73fcc0b..152e7ed20a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview.dart @@ -1,22 +1,17 @@ +import 'package:flutter/material.dart'; + 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/widgets/widgets.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/util/theme_extension.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:go_router/go_router.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; - -import 'custom_link_parser.dart'; class CustomLinkPreviewWidget extends StatelessWidget { const CustomLinkPreviewWidget({ @@ -26,8 +21,6 @@ class CustomLinkPreviewWidget extends StatelessWidget { this.title, this.description, this.imageUrl, - this.isHovering = false, - this.status = LinkLoadingStatus.loading, }); final Node node; @@ -35,14 +28,9 @@ class CustomLinkPreviewWidget extends StatelessWidget { final String? description; final String? imageUrl; final String url; - final bool isHovering; - final LinkLoadingStatus status; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context), - borderScheme = theme.borderColorScheme, - textScheme = theme.textColorScheme; final documentFontSize = context .read() .editorStyle @@ -50,67 +38,73 @@ class CustomLinkPreviewWidget extends StatelessWidget { .text .fontSize ?? 16.0; - final isInDarkCallout = node.parent?.type == CalloutBlockKeys.type && - !Theme.of(context).isLightMode; - final (fontSize, width) = UniversalPlatform.isDesktopOrWeb - ? (documentFontSize, 160.0) + final (fontSize, width) = PlatformExtension.isDesktopOrWeb + ? (documentFontSize, 180.0) : (documentFontSize - 2, 120.0); final Widget child = Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( border: Border.all( - color: isHovering || isInDarkCallout - ? borderScheme.greyTertiaryHover - : borderScheme.greyTertiary, + color: Theme.of(context).colorScheme.onSurface, + ), + borderRadius: BorderRadius.circular( + 6.0, ), - borderRadius: BorderRadius.circular(16.0), ), - child: SizedBox( - height: 96, + child: IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildImage(context), + if (imageUrl != null) + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6.0), + bottomLeft: Radius.circular(6.0), + ), + child: FlowyNetworkImage( + url: imageUrl!, + width: width, + ), + ), Expanded( child: Padding( - padding: const EdgeInsets.fromLTRB(20, 12, 58, 12), - child: status != LinkLoadingStatus.idle - ? buildLoadingOrErrorWidget() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (title != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyText.medium( - title!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize, - color: textScheme.primary, - figmaLineHeight: 20, - ), - ), - if (description != null) - Padding( - padding: const EdgeInsets.only(bottom: 16.0), - child: FlowyText( - description!, - overflow: TextOverflow.ellipsis, - fontSize: fontSize - 4, - figmaLineHeight: 16, - color: textScheme.primary, - ), - ), - FlowyText( - url.toString(), - overflow: TextOverflow.ellipsis, - color: textScheme.secondary, - fontSize: fontSize - 4, - figmaLineHeight: 16, - ), - ], + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (title != null) + Padding( + padding: const EdgeInsets.only( + bottom: 4.0, + right: 10.0, + ), + child: FlowyText.medium( + title!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + fontSize: fontSize, + ), ), + if (description != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText( + description!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + fontSize: fontSize - 4, + ), + ), + FlowyText( + url.toString(), + overflow: TextOverflow.ellipsis, + maxLines: 2, + color: Theme.of(context).hintColor, + fontSize: fontSize - 4, + ), + ], + ), ), ), ], @@ -118,13 +112,10 @@ class CustomLinkPreviewWidget extends StatelessWidget { ), ); - if (UniversalPlatform.isDesktopOrWeb) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => afLaunchUrlString(url), - child: child, - ), + if (PlatformExtension.isDesktopOrWeb) { + return InkWell( + onTap: () => afLaunchUrlString(url), + child: child, ); } @@ -132,10 +123,7 @@ class CustomLinkPreviewWidget extends StatelessWidget { node: node, editorState: context.read(), extendActionWidgets: _buildExtendActionWidgets(context), - child: GestureDetector( - onTap: () => afLaunchUrlString(url), - child: child, - ), + child: child, ); } @@ -146,8 +134,8 @@ class CustomLinkPreviewWidget extends StatelessWidget { showTopBorder: false, text: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(), leftIcon: const FlowySvg( - FlowySvgs.m_toolbar_link_m, - size: Size.square(18), + FlowySvgs.m_aa_link_s, + size: Size.square(20), ), onTap: () { context.pop(); @@ -159,59 +147,4 @@ class CustomLinkPreviewWidget extends StatelessWidget { ), ]; } - - Widget buildImage(BuildContext context) { - if (imageUrl?.isEmpty ?? true) { - return SizedBox.shrink(); - } - final theme = AppFlowyTheme.of(context), - fillScheme = theme.fillColorScheme, - iconScheme = theme.iconColorScheme; - final width = UniversalPlatform.isDesktopOrWeb ? 160.0 : 120.0; - return ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16.0), - bottomLeft: Radius.circular(16.0), - ), - child: Container( - width: width, - color: fillScheme.quaternary, - child: FlowyNetworkImage( - url: imageUrl!, - width: width, - errorWidgetBuilder: (_, __, ___) => Center( - child: FlowySvg( - FlowySvgs.toolbar_link_earth_m, - color: iconScheme.secondary, - size: Size.square(30), - ), - ), - ), - ), - ); - } - - Widget buildLoadingOrErrorWidget() { - if (status == LinkLoadingStatus.loading) { - return const Center( - child: SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator.adaptive(), - ), - ); - } else if (status == LinkLoadingStatus.error) { - return const Center( - child: SizedBox( - height: 16, - width: 16, - child: Icon( - Icons.error_outline, - color: Colors.red, - ), - ), - ); - } - return SizedBox.shrink(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart deleted file mode 100644 index 3f2128db52..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/custom_link_preview_block_component.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; - -import 'custom_link_preview.dart'; -import 'default_selectable_mixin.dart'; -import 'link_preview_menu.dart'; - -class CustomLinkPreviewBlockComponentBuilder extends BlockComponentBuilder { - CustomLinkPreviewBlockComponentBuilder({ - super.configuration, - }); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - final isEmbed = - node.attributes[LinkEmbedKeys.previewType] == LinkEmbedKeys.embed; - if (isEmbed) { - return LinkEmbedBlockComponent( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (_, state) => - actionBuilder(blockComponentContext, state), - ); - } - return CustomLinkPreviewBlockComponent( - key: node.key, - node: node, - configuration: configuration, - showActions: showActions(node), - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => - (node) => node.attributes[LinkPreviewBlockKeys.url]!.isNotEmpty; -} - -class CustomLinkPreviewBlockComponent extends BlockComponentStatefulWidget { - const CustomLinkPreviewBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - DefaultSelectableMixinState createState() => - CustomLinkPreviewBlockComponentState(); -} - -class CustomLinkPreviewBlockComponentState - extends DefaultSelectableMixinState - with BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - String get url => widget.node.attributes[LinkPreviewBlockKeys.url]!; - - final parser = LinkParser(); - LinkLoadingStatus status = LinkLoadingStatus.loading; - late LinkInfo linkInfo = LinkInfo(url: url); - - final showActionsNotifier = ValueNotifier(false); - bool isMenuShowing = false, isHovering = false; - - @override - void initState() { - super.initState(); - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = LinkLoadingStatus.idle; - } else if (!hasOldInfo) { - status = LinkLoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - parser.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) { - isHovering = true; - showActionsNotifier.value = true; - }, - onExit: (_) { - isHovering = false; - Future.delayed(const Duration(milliseconds: 200), () { - if (isMenuShowing || isHovering) return; - if (mounted) showActionsNotifier.value = false; - }); - }, - hitTestBehavior: HitTestBehavior.opaque, - opaque: false, - child: ValueListenableBuilder( - valueListenable: showActionsNotifier, - builder: (context, showActions, child) { - return buildPreview(showActions); - }, - ), - ); - } - - Widget buildPreview(bool showActions) { - Widget child = CustomLinkPreviewWidget( - key: widgetKey, - node: node, - url: url, - isHovering: showActions, - title: linkInfo.siteName, - description: linkInfo.description, - imageUrl: linkInfo.imageUrl, - status: status, - ); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - child: child, - ); - } - - child = Stack( - children: [ - child, - if (showActions) - Positioned( - top: 12, - right: 12, - child: CustomLinkPreviewMenu( - onMenuShowed: () { - isMenuShowing = true; - }, - onMenuHided: () { - isMenuShowing = false; - if (!isHovering && mounted) { - showActionsNotifier.value = false; - } - }, - onReload: () { - setState(() { - status = LinkLoadingStatus.loading; - }); - Future.delayed(const Duration(milliseconds: 200), () { - if (mounted) parser.start(url); - }); - }, - node: node, - ), - ), - ], - ); - - final parent = node.parent; - EdgeInsets newPadding = padding; - if (parent?.type == CalloutBlockKeys.type) { - newPadding = padding.copyWith(right: padding.right + 10); - } - child = Padding(padding: newPadding, child: child); - - return child; - } - - @override - Node get currentNode => node; - - @override - EdgeInsets get boxPadding => padding; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart deleted file mode 100644 index c894811522..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/default_selectable_mixin.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/widgets.dart'; - -abstract class DefaultSelectableMixinState - extends State with SelectableMixin { - final widgetKey = GlobalKey(); - RenderBox? get _renderBox => - widgetKey.currentContext?.findRenderObject() as RenderBox?; - - Node get currentNode; - - EdgeInsets get boxPadding => EdgeInsets.zero; - - @override - Position start() => Position(path: currentNode.path); - - @override - Position end() => Position(path: currentNode.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - final box = _renderBox; - if (box is RenderBox) { - return boxPadding.topLeft & box.size; - } - return Rect.zero; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection(Selection.collapsed(position)); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final box = widgetKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && box is RenderBox) { - return [ - box.localToGlobal(Offset.zero, ancestor: parentBox) & box.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: currentNode.path, - startOffset: 0, - endOffset: 1, - ); - - @override - Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => - _renderBox!.localToGlobal(offset); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart deleted file mode 100644 index 7b52994654..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_backend/log.dart'; -// ignore: depend_on_referenced_packages -import 'package:html/parser.dart' as html_parser; -import 'package:http/http.dart' as http; -import 'dart:convert'; - -abstract class LinkInfoParser { - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }); - - static String formatUrl(String url) { - Uri? uri = Uri.tryParse(url); - if (uri == null) return url; - if (!uri.hasScheme) uri = Uri.tryParse('http://$url'); - if (uri == null) return url; - final isHome = (uri.hasEmptyPath || uri.path == '/') && !uri.hasQuery; - final homeUrl = '${uri.scheme}://${uri.host}/'; - if (isHome) return homeUrl; - return '$uri'; - } -} - -class DefaultParser implements LinkInfoParser { - @override - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }) async { - try { - final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; - final http.Response response = - await http.get(link, headers: headers).timeout(timeout); - final code = response.statusCode; - if (code != 200 && isHome) { - throw Exception('Http request error: $code'); - } - - final contentType = response.headers['content-type']; - final charset = contentType?.split('charset=').lastOrNull; - String body = ''; - if (charset == null || - charset.toLowerCase() == 'latin-1' || - charset.toLowerCase() == 'iso-8859-1') { - body = latin1.decode(response.bodyBytes); - } else { - body = utf8.decode(response.bodyBytes, allowMalformed: true); - } - - final document = html_parser.parse(body); - - final siteName = document - .querySelector('meta[property="og:site_name"]') - ?.attributes['content']; - - String? title = document - .querySelector('meta[property="og:title"]') - ?.attributes['content']; - title ??= document.querySelector('title')?.text; - - String? description = document - .querySelector('meta[property="og:description"]') - ?.attributes['content']; - description ??= document - .querySelector('meta[name="description"]') - ?.attributes['content']; - - String? imageUrl = document - .querySelector('meta[property="og:image"]') - ?.attributes['content']; - if (imageUrl != null && !imageUrl.startsWith('http')) { - imageUrl = link.resolve(imageUrl).toString(); - } - - final favicon = - 'https://www.faviconextractor.com/favicon/${link.host}?larger=true'; - - return LinkInfo( - url: '$link', - siteName: siteName, - title: title, - description: description, - imageUrl: imageUrl, - faviconUrl: favicon, - ); - } catch (e) { - Log.error('Parse link $link error: $e'); - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart deleted file mode 100644 index 6f1ac6fb22..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_parsers/youtube_parser.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:http/http.dart' as http; -import 'default_parser.dart'; - -class YoutubeParser implements LinkInfoParser { - @override - Future parse( - Uri link, { - Duration timeout = const Duration(seconds: 8), - Map? headers, - }) async { - try { - final isHome = (link.hasEmptyPath || link.path == '/') && !link.hasQuery; - if (isHome) { - return DefaultParser().parse( - link, - timeout: timeout, - headers: headers, - ); - } - - final requestLink = - 'https://www.youtube.com/oembed?url=$link&format=json'; - final http.Response response = await http - .get(Uri.parse(requestLink), headers: headers) - .timeout(timeout); - final code = response.statusCode; - if (code != 200) { - throw Exception('Http request error: $code'); - } - - final youtubeInfo = YoutubeInfo.fromJson(jsonDecode(response.body)); - - final favicon = - 'https://www.google.com/s2/favicons?sz=64&domain=${link.host}'; - return LinkInfo( - url: '$link', - title: youtubeInfo.title, - siteName: youtubeInfo.authorName, - imageUrl: youtubeInfo.thumbnailUrl, - faviconUrl: favicon, - ); - } catch (e) { - Log.error('Parse link $link error: $e'); - return null; - } - } -} - -class YoutubeInfo { - YoutubeInfo({ - this.title, - this.authorName, - this.version, - this.providerName, - this.providerUrl, - this.thumbnailUrl, - }); - - YoutubeInfo.fromJson(Map json) { - title = json['title']; - authorName = json['author_name']; - version = json['version']; - providerName = json['provider_name']; - providerUrl = json['provider_url']; - thumbnailUrl = json['thumbnail_url']; - } - String? title; - String? authorName; - String? version; - String? providerName; - String? providerUrl; - String? thumbnailUrl; - - Map toJson() => { - 'title': title, - 'author_name': authorName, - 'version': version, - 'provider_name': providerName, - 'provider_url': providerUrl, - 'thumbnail_url': thumbnailUrl, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart new file mode 100644 index 0000000000..6688cfe304 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_cache.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; + +class LinkPreviewDataCache implements LinkPreviewDataCacheInterface { + @override + Future get(String url) async { + final option = + await getIt().getWithFormat( + url, + (value) => LinkPreviewData.fromJson(jsonDecode(value)), + ); + return option; + } + + @override + Future set(String url, LinkPreviewData data) async { + await getIt().set( + url, + jsonEncode(data.toJson()), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart index 2fb493dda3..a83fbed589 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/link_preview_menu.dart @@ -1,207 +1,108 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_replace_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -class CustomLinkPreviewMenu extends StatefulWidget { - const CustomLinkPreviewMenu({ +class LinkPreviewMenu extends StatefulWidget { + const LinkPreviewMenu({ super.key, - required this.onMenuShowed, - required this.onMenuHided, - required this.onReload, required this.node, + required this.state, }); - final VoidCallback onMenuShowed; - final VoidCallback onMenuHided; - final VoidCallback onReload; + final Node node; + final LinkPreviewBlockComponentState state; @override - State createState() => _CustomLinkPreviewMenuState(); + State createState() => _LinkPreviewMenuState(); } -class _CustomLinkPreviewMenuState extends State { - final popoverController = PopoverController(); - final buttonKey = GlobalKey(); - bool closed = false; - bool selected = false; - +class _LinkPreviewMenuState extends State { @override - void dispose() { - super.dispose(); - popoverController.close(); - widget.onMenuHided.call(); + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + height: 32, + decoration: BoxDecoration( + color: theme.cardColor, + boxShadow: [ + BoxShadow( + blurRadius: 5, + spreadRadius: 1, + color: Colors.black.withOpacity(0.1), + ), + ], + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + children: [ + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.document_plugins_urlPreview_convertToLink.tr(), + iconData: FlowySvgs.m_aa_link_s, + onTap: () => convertUrlPreviewNodeToLink( + context.read(), + widget.node, + ), + ), + const HSpace(4), + MenuBlockButton( + tooltip: LocaleKeys.editor_copyLink.tr(), + iconData: FlowySvgs.copy_s, + onTap: copyImageLink, + ), + const _Divider(), + MenuBlockButton( + tooltip: LocaleKeys.button_delete.tr(), + iconData: FlowySvgs.delete_s, + onTap: deleteLinkPreviewNode, + ), + const HSpace(4), + ], + ), + ); } + void copyImageLink() { + final url = widget.node.attributes[ImageBlockKeys.url]; + if (url != null) { + Clipboard.setData(ClipboardData(text: url)); + showSnackBarMessage( + context, + LocaleKeys.document_plugins_urlPreview_copiedToPasteBoard.tr(), + ); + } + } + + Future deleteLinkPreviewNode() async { + final node = widget.node; + final editorState = context.read(); + final transaction = editorState.transaction; + transaction.deleteNode(node); + transaction.afterSelection = null; + await editorState.apply(transaction); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + @override Widget build(BuildContext context) { - return AppFlowyPopover( - offset: Offset(0, 0.0), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: popoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - keepEditorFocusNotifier.decrease(); - if (!closed) { - closed = true; - return; - } else { - closed = false; - widget.onMenuHided.call(); - } - setState(() { - selected = false; - }); - }, - popupBuilder: (context) => buildMenu(), - child: FlowyIconButton( - key: buttonKey, - isSelected: selected, - icon: FlowySvg(FlowySvgs.toolbar_more_m), - onPressed: showPopover, + return Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 1, + color: Colors.grey, ), ); } - - Widget buildMenu() { - return MouseRegion( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(LinkPreviewMenuCommand.values.length, (index) { - final command = LinkPreviewMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - Future onTap(LinkPreviewMenuCommand command) async { - final editorState = context.read(); - final node = widget.node; - final url = node.attributes[LinkPreviewBlockKeys.url]; - switch (command) { - case LinkPreviewMenuCommand.convertToMention: - await convertUrlPreviewNodeToMention(editorState, node); - break; - case LinkPreviewMenuCommand.convertToUrl: - await convertUrlPreviewNodeToLink(editorState, node); - break; - case LinkPreviewMenuCommand.convertToEmbed: - final transaction = editorState.transaction; - transaction.updateNode(node, { - LinkPreviewBlockKeys.url: url, - LinkEmbedKeys.previewType: LinkEmbedKeys.embed, - }); - await editorState.apply(transaction); - break; - case LinkPreviewMenuCommand.copyLink: - if (url != null) { - await context.copyLink(url); - } - break; - case LinkPreviewMenuCommand.replace: - final box = buttonKey.currentContext?.findRenderObject() as RenderBox?; - if (box == null) return; - final p = box.localToGlobal(Offset.zero); - showReplaceMenu( - context: context, - editorState: editorState, - node: node, - url: url, - ltrb: LTRB(left: p.dx - 330, top: p.dy), - onReplace: (url) async { - await convertLinkBlockToOtherLinkBlock( - editorState, - node, - node.type, - url: url, - ); - }, - ); - break; - case LinkPreviewMenuCommand.reload: - widget.onReload.call(); - break; - case LinkPreviewMenuCommand.removeLink: - await removeUrlPreviewLink(editorState, node); - break; - } - closePopover(); - } - - void showPopover() { - widget.onMenuShowed.call(); - keepEditorFocusNotifier.increase(); - popoverController.show(); - setState(() { - selected = true; - }); - } - - void closePopover() { - popoverController.close(); - widget.onMenuHided.call(); - } -} - -enum LinkPreviewMenuCommand { - convertToMention, - convertToUrl, - convertToEmbed, - copyLink, - replace, - reload, - removeLink; - - String get title { - switch (this) { - case convertToMention: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toMetion - .tr(); - case LinkPreviewMenuCommand.convertToUrl: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case LinkPreviewMenuCommand.convertToEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case LinkPreviewMenuCommand.copyLink: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink - .tr(); - case LinkPreviewMenuCommand.replace: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_replace - .tr(); - case LinkPreviewMenuCommand.reload: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_reload - .tr(); - case LinkPreviewMenuCommand.removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart deleted file mode 100644 index fb51cdcf47..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart +++ /dev/null @@ -1,259 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/menu/menu_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -const _menuHeighgt = 188.0, _menuWidth = 288.0; - -class PasteAsMenuService { - PasteAsMenuService({ - required this.context, - required this.editorState, - }); - - final BuildContext context; - final EditorState editorState; - OverlayEntry? _menuEntry; - - void show(String href) { - WidgetsBinding.instance.addPostFrameCallback((_) => _show(href)); - } - - void dismiss() { - if (_menuEntry != null) { - keepEditorFocusNotifier.decrease(); - // editorState.service.scrollService?.enable(); - // editorState.service.keyboardService?.enable(); - } - _menuEntry?.remove(); - _menuEntry = null; - } - - void _show(String href) { - final Size editorSize = editorState.renderBox?.size ?? Size.zero; - if (editorSize == Size.zero) return; - final menuPosition = editorState.calculateMenuOffset( - menuWidth: _menuWidth, - menuHeight: _menuHeighgt, - ); - if (menuPosition == null) return; - final ltrb = menuPosition.ltrb; - - _menuEntry = OverlayEntry( - builder: (context) => SizedBox( - height: editorSize.height, - width: editorSize.width, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - ltrb.buildPositioned( - child: PasteAsMenu( - editorState: editorState, - onSelect: (t) { - final selection = editorState.selection; - if (selection == null) return; - final end = selection.end; - final urlSelection = Selection( - start: end.copyWith(offset: end.offset - href.length), - end: end, - ); - if (t == PasteMenuType.bookmark) { - convertUrlToLinkPreview(editorState, urlSelection, href); - } else if (t == PasteMenuType.mention) { - convertUrlToMention(editorState, urlSelection); - } else if (t == PasteMenuType.embed) { - convertUrlToLinkPreview( - editorState, - urlSelection, - href, - previewType: LinkEmbedKeys.embed, - ); - } - dismiss(); - }, - onDismiss: dismiss, - ), - ), - ], - ), - ), - ), - ); - - Overlay.of(context).insert(_menuEntry!); - - keepEditorFocusNotifier.increase(); - // editorState.service.keyboardService?.disable(showCursor: true); - // editorState.service.scrollService?.disable(); - } -} - -class PasteAsMenu extends StatefulWidget { - const PasteAsMenu({ - super.key, - required this.onSelect, - required this.onDismiss, - required this.editorState, - }); - final ValueChanged onSelect; - final VoidCallback onDismiss; - final EditorState editorState; - - @override - State createState() => _PasteAsMenuState(); -} - -class _PasteAsMenuState extends State { - final focusNode = FocusNode(debugLabel: 'paste_as_menu'); - final ValueNotifier selectedIndexNotifier = ValueNotifier(0); - - EditorState get editorState => widget.editorState; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => focusNode.requestFocus(), - ); - editorState.selectionNotifier.addListener(dismiss); - } - - @override - void dispose() { - focusNode.dispose(); - selectedIndexNotifier.dispose(); - editorState.selectionNotifier.removeListener(dismiss); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Focus( - focusNode: focusNode, - onKeyEvent: onKeyEvent, - child: Container( - width: _menuWidth, - height: _menuHeighgt, - padding: EdgeInsets.all(6), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: theme.surfaceColorScheme.primary, - boxShadow: theme.shadow.medium, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: 32, - padding: EdgeInsets.all(8), - child: FlowyText.semibold( - color: theme.textColorScheme.primary, - LocaleKeys.document_plugins_linkPreview_typeSelection_pasteAs - .tr(), - ), - ), - ...List.generate( - PasteMenuType.values.length, - (i) => buildItem(PasteMenuType.values[i], i), - ), - ], - ), - ), - ); - } - - Widget buildItem(PasteMenuType type, int i) { - return ValueListenableBuilder( - valueListenable: selectedIndexNotifier, - builder: (context, value, child) { - final isSelected = i == value; - return SizedBox( - height: 36, - child: FlowyButton( - isSelected: isSelected, - text: FlowyText( - type.title, - ), - onTap: () => onSelect(type), - ), - ); - }, - ); - } - - void changeIndex(int index) => selectedIndexNotifier.value = index; - - KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - int index = selectedIndexNotifier.value, - length = PasteMenuType.values.length; - if (event.logicalKey == LogicalKeyboardKey.enter) { - onSelect(PasteMenuType.values[index]); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - dismiss(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - dismiss(); - } else if ([LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowLeft] - .contains(event.logicalKey)) { - if (index == 0) { - index = length - 1; - } else { - index--; - } - changeIndex(index); - return KeyEventResult.handled; - } else if ([LogicalKeyboardKey.arrowDown, LogicalKeyboardKey.arrowRight] - .contains(event.logicalKey)) { - if (index == length - 1) { - index = 0; - } else { - index++; - } - changeIndex(index); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - void onSelect(PasteMenuType type) => widget.onSelect.call(type); - - void dismiss() => widget.onDismiss.call(); -} - -enum PasteMenuType { - mention, - url, - bookmark, - embed, -} - -extension PasteMenuTypeExtension on PasteMenuType { - String get title { - switch (this) { - case PasteMenuType.mention: - return LocaleKeys.document_plugins_linkPreview_typeSelection_mention - .tr(); - case PasteMenuType.url: - return LocaleKeys.document_plugins_linkPreview_typeSelection_URL.tr(); - case PasteMenuType.bookmark: - return LocaleKeys.document_plugins_linkPreview_typeSelection_bookmark - .tr(); - case PasteMenuType.embed: - return LocaleKeys.document_plugins_linkPreview_typeSelection_embed.tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart index 8b193c70fb..11dae6075d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/link_preview/shared.dart @@ -1,27 +1,12 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -Future convertUrlPreviewNodeToLink( - EditorState editorState, - Node node, -) async { - if (node.type != LinkPreviewBlockKeys.type) { - return; - } - - final url = node.attributes[LinkPreviewBlockKeys.url]; - final delta = Delta() - ..insert( - url, - attributes: { - AppFlowyRichTextKeys.href: url, - }, - ); +void convertUrlPreviewNodeToLink(EditorState editorState, Node node) { + assert(node.type == LinkPreviewBlockKeys.type); + final url = node.attributes[ImageBlockKeys.url]; final transaction = editorState.transaction; transaction - ..insertNode(node.path, paragraphNode(delta: delta)) + ..insertNode(node.path, paragraphNode(text: url)) ..deleteNode(node); transaction.afterSelection = Selection.collapsed( Position( @@ -29,174 +14,5 @@ Future convertUrlPreviewNodeToLink( offset: url.length, ), ); - return editorState.apply(transaction); -} - -Future convertUrlPreviewNodeToMention( - EditorState editorState, - Node node, -) async { - if (node.type != LinkPreviewBlockKeys.type) { - return; - } - - final url = node.attributes[LinkPreviewBlockKeys.url]; - final delta = Delta() - ..insert( - MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.externalLink.name, - MentionBlockKeys.url: url, - }, - }, - ); - final transaction = editorState.transaction; - transaction - ..insertNode(node.path, paragraphNode(delta: delta)) - ..deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path, - offset: url.length, - ), - ); - return editorState.apply(transaction); -} - -Future removeUrlPreviewLink( - EditorState editorState, - Node node, -) async { - if (node.type != LinkPreviewBlockKeys.type) { - return; - } - - final url = node.attributes[LinkPreviewBlockKeys.url]; - final delta = Delta()..insert(url); - final transaction = editorState.transaction; - transaction - ..insertNode(node.path, paragraphNode(delta: delta)) - ..deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path, - offset: url.length, - ), - ); - return editorState.apply(transaction); -} - -Future convertUrlToLinkPreview( - EditorState editorState, - Selection selection, - String url, { - String? previewType, -}) async { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final delta = node.delta; - if (delta == null) return; - final List beforeOperations = [], afterOperations = []; - int index = 0; - for (final insert in delta.whereType()) { - if (index < selection.startIndex) { - beforeOperations.add(insert); - } else if (index >= selection.endIndex) { - afterOperations.add(insert); - } - index += insert.length; - } - final transaction = editorState.transaction; - transaction - ..deleteNode(node) - ..insertNodes(node.path.next, [ - if (beforeOperations.isNotEmpty) - paragraphNode(delta: Delta(operations: beforeOperations)), - if (previewType == LinkEmbedKeys.embed) - linkEmbedNode(url: url) - else - linkPreviewNode(url: url), - if (afterOperations.isNotEmpty) - paragraphNode(delta: Delta(operations: afterOperations)), - ]); - await editorState.apply(transaction); -} - -Future convertUrlToMention( - EditorState editorState, - Selection selection, -) async { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final delta = node.delta; - if (delta == null) return; - String url = ''; - int index = 0; - for (final insert in delta.whereType()) { - if (index >= selection.startIndex && index < selection.endIndex) { - final href = insert.attributes?.href ?? ''; - if (href.isNotEmpty) { - url = href; - break; - } - } - index += insert.length; - } - final transaction = editorState.transaction; - transaction.replaceText( - node, - selection.startIndex, - selection.length, - MentionBlockKeys.mentionChar, - attributes: { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.externalLink.name, - MentionBlockKeys.url: url, - }, - }, - ); - await editorState.apply(transaction); -} - -Future convertLinkBlockToOtherLinkBlock( - EditorState editorState, - Node node, - String toType, { - String? url, -}) async { - final nodeType = node.type; - if (nodeType != LinkPreviewBlockKeys.type || - (nodeType == toType && url == null)) { - return; - } - final insertedNode = []; - - final afterUrl = url ?? node.attributes[LinkPreviewBlockKeys.url] ?? ''; - final previewType = node.attributes[LinkEmbedKeys.previewType]; - Node afterNode = node.copyWith( - type: toType, - attributes: { - LinkPreviewBlockKeys.url: afterUrl, - LinkEmbedKeys.previewType: previewType, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: node.attributes[blockComponentTextDirection], - blockComponentDelta: (node.delta ?? Delta()).toJson(), - }, - ); - afterNode = afterNode.copyWith(children: []); - insertedNode.add(afterNode); - insertedNode.addAll(node.children.map((e) => e.deepCopy())); - final transaction = editorState.transaction; - transaction.insertNodes( - node.path, - insertedNode, - ); - transaction.deleteNodes([node]); - await editorState.apply(transaction); + editorState.apply(transaction); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 2f724061ee..c7d298ff09 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -1,8 +1,5 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -13,7 +10,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_math_fork/flutter_math.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; class MathEquationBlockKeys { const MathEquationBlockKeys._(); @@ -41,11 +37,7 @@ Node mathEquationNode({ // defining the callout block menu item for selection SelectionMenuItem mathEquationItem = SelectionMenuItem.node( getName: LocaleKeys.document_plugins_mathEquation_name.tr, - iconBuilder: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.icon_math_eq_s, - isSelected: onSelected, - style: style, - ), + iconData: Icons.text_fields_rounded, keywords: ['tex, latex, katex', 'math equation', 'formula'], nodeBuilder: (editorState, _) => mathEquationNode(), replace: (_, node) => node.delta?.isEmpty ?? false, @@ -79,15 +71,11 @@ class MathEquationBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (node) => + bool validate(Node node) => node.children.isEmpty && node.attributes[MathEquationBlockKeys.formula] is String; } @@ -98,7 +86,6 @@ class MathEquationBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -116,24 +103,16 @@ class MathEquationBlockComponentWidgetState @override Node get node => widget.node; + bool isHover = false; String get formula => widget.node.attributes[MathEquationBlockKeys.formula] as String; late final editorState = context.read(); - final ValueNotifier isHover = ValueNotifier(false); - - late final controller = TextEditingController(text: formula); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } @override Widget build(BuildContext context) { return InkWell( - onHover: (value) => isHover.value = value, + onHover: (value) => setState(() => isHover = value), onTap: showEditingDialog, child: _build(context), ); @@ -158,42 +137,24 @@ class MathEquationBlockComponentWidgetState ), ); - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - if (UniversalPlatform.isMobile) { - child = MobileBlockActionButtons( - node: node, - editorState: editorState, - child: child, - ); - } - child = Padding( padding: padding, child: child, ); - if (UniversalPlatform.isDesktopOrWeb) { - child = Stack( - children: [ - child, - Positioned( - right: 6, - top: 12, - child: ValueListenableBuilder( - valueListenable: isHover, - builder: (_, value, __) => - value ? _buildDeleteButton(context) : const SizedBox.shrink(), - ), - ), - ], + if (widget.showActions && widget.actionBuilder != null) { + child = BlockComponentActionWrapper( + node: node, + actionBuilder: widget.actionBuilder!, + child: child, + ); + } + + if (PlatformExtension.isMobile) { + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + child: child, ); } @@ -206,15 +167,10 @@ class MathEquationBlockComponentWidgetState child: Row( children: [ const HSpace(10), - FlowySvg( - FlowySvgs.slash_menu_icon_math_equation_s, - color: Theme.of(context).hintColor, - size: const Size.square(24), - ), + const Icon(Icons.text_fields_outlined), const HSpace(10), FlowyText( LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(), - color: Theme.of(context).hintColor, ), ], ), @@ -230,18 +186,8 @@ class MathEquationBlockComponentWidgetState ); } - Widget _buildDeleteButton(BuildContext context) { - return MenuBlockButton( - tooltip: LocaleKeys.button_delete.tr(), - iconData: FlowySvgs.trash_s, - onTap: () { - final transaction = editorState.transaction..deleteNode(widget.node); - editorState.apply(transaction); - }, - ); - } - void showEditingDialog() { + final controller = TextEditingController(text: formula); showDialog( context: context, builder: (context) { @@ -288,7 +234,7 @@ class MathEquationBlockComponentWidgetState actionsAlignment: MainAxisAlignment.spaceAround, ); }, - ); + ).then((_) => controller.dispose()); } void updateMathEquation(String mathEquation, BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart deleted file mode 100644 index 9c9fe7905b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -/// Windows / Linux : ctrl + shift + e -/// macOS : cmd + shift + e -/// Allows the user to insert math equation by shortcut -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent insertInlineMathEquationCommand = - CommandShortcutEvent( - key: 'Insert inline math equation', - command: 'ctrl+shift+e', - macOSCommand: 'cmd+shift+e', - getDescription: LocaleKeys.document_plugins_mathEquation_name.tr, - handler: (editorState) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed || !selection.isSingle) { - return KeyEventResult.ignored; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return KeyEventResult.ignored; - } - if (node.delta == null || !toolbarItemWhiteList.contains(node.type)) { - return KeyEventResult.ignored; - } - final transaction = editorState.transaction; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[InlineMathEquationKeys.formula] != null, - ); - }); - if (isHighlight) { - final formula = delta - .slice(selection.startIndex, selection.endIndex) - .whereType() - .firstOrNull - ?.attributes?[InlineMathEquationKeys.formula]; - assert(formula != null); - if (formula == null) { - return KeyEventResult.ignored; - } - // clear the format - transaction.replaceText( - node, - selection.startIndex, - selection.length, - formula, - attributes: {}, - ); - } else { - final text = editorState.getTextInSelection(selection).join(); - transaction.replaceText( - node, - selection.startIndex, - selection.length, - MentionBlockKeys.mentionChar, - attributes: { - InlineMathEquationKeys.formula: text, - }, - ); - } - editorState.apply(transaction); - return KeyEventResult.handled; - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart deleted file mode 100644 index 77f8c8d0a1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart +++ /dev/null @@ -1,219 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../transaction_handler/mention_transaction_handler.dart'; - -const _pasteIdentifier = 'child_page_transaction'; - -class ChildPageTransactionHandler extends MentionTransactionHandler { - ChildPageTransactionHandler(); - - @override - Future onTransaction( - BuildContext context, - String viewId, - EditorState editorState, - List added, - List removed, { - bool isCut = false, - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - bool isTurnInto = false, - String? parentViewId, - }) async { - if (isDraggingNode || isTurnInto) { - return; - } - - // Remove the mentions that were both added and removed in the same transaction. - // These were just moved around. - final moved = []; - for (final mention in added) { - if (removed.any((r) => r.$2 == mention.$2)) { - moved.add(mention); - } - } - - for (final mention in removed) { - if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) { - return; - } - - if (mention.$2[MentionBlockKeys.type] != MentionType.childPage.name) { - continue; - } - - await _handleDeletion(context, mention); - } - - if (isPaste || isUndoRedo) { - if (context.mounted) { - context.read().startHandlingPaste(_pasteIdentifier); - } - - for (final mention in added) { - if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) { - return; - } - - if (mention.$2[MentionBlockKeys.type] != MentionType.childPage.name) { - continue; - } - - await _handleAddition( - context, - editorState, - mention, - isPaste, - parentViewId, - isCut, - ); - } - - if (context.mounted) { - context.read().endHandlingPaste(_pasteIdentifier); - } - } - } - - Future _handleDeletion( - BuildContext context, - MentionBlockData data, - ) async { - final viewId = data.$2[MentionBlockKeys.pageId]; - - final result = await ViewBackendService.deleteView(viewId: viewId); - result.fold( - (_) {}, - (error) { - Log.error(error); - if (context.mounted) { - showToastNotification( - message: LocaleKeys.document_plugins_subPage_errors_failedDeletePage - .tr(), - ); - } - }, - ); - } - - Future _handleAddition( - BuildContext context, - EditorState editorState, - MentionBlockData data, - bool isPaste, - String? parentViewId, - bool isCut, - ) async { - if (parentViewId == null) { - return; - } - - final viewId = data.$2[MentionBlockKeys.pageId]; - if (isPaste && !isCut) { - _handlePasteFromCopy( - context, - editorState, - data.$1, - data.$3, - viewId, - parentViewId, - ); - } else { - _handlePasteFromCut(viewId, parentViewId); - } - } - - void _handlePasteFromCut(String viewId, String parentViewId) async { - // Attempt to restore from Trash just in case - await TrashService.putback(viewId); - - final view = (await ViewBackendService.getView(viewId)).toNullable(); - if (view == null) { - return Log.error('View not found: $viewId'); - } - - if (view.parentViewId == parentViewId) { - return; - } - - await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: parentViewId, - prevViewId: null, - ); - } - - void _handlePasteFromCopy( - BuildContext context, - EditorState editorState, - Node node, - int index, - String viewId, - String parentViewId, - ) async { - final view = (await ViewBackendService.getView(viewId)).toNullable(); - if (view == null) { - return Log.error('View not found: $viewId'); - } - - final duplicatedViewOrFailure = await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: false, - includeChildren: true, - syncAfterDuplicate: true, - parentViewId: parentViewId, - ); - - await duplicatedViewOrFailure.fold( - (newView) async { - // The index is the index of the delta, to get the index of the mention character - // in all the text, we need to calculate it based on the deltas before the current delta. - int mentionIndex = 0; - for (final (i, delta) in node.delta!.indexed) { - if (i >= index) { - break; - } - - mentionIndex += delta.length; - } - - final transaction = editorState.transaction; - transaction.formatText( - node, - mentionIndex, - MentionBlockKeys.mentionChar.length, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.childPage, - pageId: newView.id, - blockId: null, - ), - ); - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - ); - }, - (error) { - Log.error(error); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedDuplicatePage.tr(), - ); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart deleted file mode 100644 index cb3196e9b7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart +++ /dev/null @@ -1,260 +0,0 @@ -import 'package:appflowy/shared/clipboard_state.dart'; -import 'package:appflowy/startup/startup.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/date_picker/widgets/reminder_selector.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter/material.dart'; -import 'package:nanoid/nanoid.dart'; -import 'package:provider/provider.dart'; - -import '../plugins.dart'; -import '../transaction_handler/mention_transaction_handler.dart'; - -const _pasteIdentifier = 'date_transaction'; - -class DateTransactionHandler extends MentionTransactionHandler { - DateTransactionHandler(); - - @override - Future onTransaction( - BuildContext context, - String viewId, - EditorState editorState, - List added, - List removed, { - bool isCut = false, - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - bool isTurnInto = false, - String? parentViewId, - }) async { - if (isDraggingNode || isTurnInto) { - return; - } - - // Remove the mentions that were both added and removed in the same transaction. - // These were just moved around. - final moved = []; - for (final mention in added) { - if (removed.any((r) => r.$2 == mention.$2)) { - moved.add(mention); - } - } - - for (final mention in removed) { - if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) { - return; - } - - if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) { - continue; - } - - _handleDeletion(context, mention); - } - - if (isPaste || isUndoRedo) { - if (context.mounted) { - context.read().startHandlingPaste(_pasteIdentifier); - } - - for (final mention in added) { - if (!context.mounted || moved.any((m) => m.$2 == mention.$2)) { - return; - } - - if (mention.$2[MentionBlockKeys.type] != MentionType.date.name) { - continue; - } - - _handleAddition( - context, - viewId, - editorState, - mention, - isPaste, - isCut, - ); - } - - if (context.mounted) { - context.read().endHandlingPaste(_pasteIdentifier); - } - } - } - - void _handleDeletion( - BuildContext context, - MentionBlockData data, - ) { - final reminderId = data.$2[MentionBlockKeys.reminderId]; - - if (reminderId case String _ when reminderId.isNotEmpty) { - getIt().add(ReminderEvent.remove(reminderId: reminderId)); - } - } - - void _handleAddition( - BuildContext context, - String viewId, - EditorState editorState, - MentionBlockData data, - bool isPaste, - bool isCut, - ) { - final dateData = _MentionDateBlockData.fromData(data.$2); - if (dateData.dateString.isEmpty) { - Log.error("mention date block doesn't have a valid date string"); - return; - } - - if (isPaste && !isCut) { - _handlePasteFromCopy( - context, - viewId, - editorState, - data.$1, - data.$3, - dateData, - ); - } else { - _handlePasteFromCut(viewId, data.$1, dateData); - } - } - - void _handlePasteFromCut( - String viewId, - Node node, - _MentionDateBlockData data, - ) { - final dateTime = DateTime.tryParse(data.dateString); - - if (data.reminderId == null || dateTime == null) { - return; - } - - getIt().add( - ReminderEvent.addById( - reminderId: data.reminderId!, - objectId: viewId, - scheduledAt: Int64( - data.reminderOption - .getNotificationDateTime(dateTime) - .millisecondsSinceEpoch ~/ - 1000, - ), - meta: { - ReminderMetaKeys.includeTime: data.includeTime.toString(), - ReminderMetaKeys.blockId: node.id, - }, - ), - ); - } - - void _handlePasteFromCopy( - BuildContext context, - String viewId, - EditorState editorState, - Node node, - int index, - _MentionDateBlockData data, - ) async { - final dateTime = DateTime.tryParse(data.dateString); - - if (node.delta == null) { - return; - } - - if (data.reminderId == null || dateTime == null) { - return; - } - - final reminderId = nanoid(); - getIt().add( - ReminderEvent.addById( - reminderId: reminderId, - objectId: viewId, - scheduledAt: Int64( - data.reminderOption - .getNotificationDateTime(dateTime) - .millisecondsSinceEpoch ~/ - 1000, - ), - meta: { - ReminderMetaKeys.includeTime: data.includeTime.toString(), - ReminderMetaKeys.blockId: node.id, - }, - ), - ); - - final newMentionAttributes = MentionBlockKeys.buildMentionDateAttributes( - date: dateTime.toIso8601String(), - reminderId: reminderId, - reminderOption: data.reminderOption.name, - includeTime: data.includeTime, - ); - - // The index is the index of the delta, to get the index of the mention character - // in all the text, we need to calculate it based on the deltas before the current delta. - int mentionIndex = 0; - for (final (i, delta) in node.delta!.indexed) { - if (i >= index) { - break; - } - - mentionIndex += delta.length; - } - - // Required to prevent editing the same spot at the same time - await Future.delayed(const Duration(milliseconds: 100)); - - final transaction = editorState.transaction - ..formatText( - node, - mentionIndex, - MentionBlockKeys.mentionChar.length, - newMentionAttributes, - ); - - await editorState.apply( - transaction, - options: const ApplyOptions(recordUndo: false), - ); - } -} - -/// A helper class to parse and store the mention date block data -class _MentionDateBlockData { - _MentionDateBlockData.fromData(Map data) { - dateString = switch (data[MentionBlockKeys.date]) { - final String string when DateTime.tryParse(string) != null => string, - _ => "", - }; - includeTime = switch (data[MentionBlockKeys.includeTime]) { - final bool flag => flag, - _ => false, - }; - reminderOption = switch (data[MentionBlockKeys.reminderOption]) { - final String name => - ReminderOption.values.firstWhereOrNull((o) => o.name == name) ?? - ReminderOption.none, - _ => ReminderOption.none, - }; - reminderId = switch (data[MentionBlockKeys.reminderId]) { - final String id - when id.isNotEmpty && reminderOption != ReminderOption.none => - id, - _ => null, - }; - } - - late final String dateString; - late final bool includeTime; - late final String? reminderId; - late final ReminderOption reminderOption; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart index 0060d65bb7..29007ada98 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_block.dart @@ -1,24 +1,20 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'mention_link_block.dart'; - enum MentionType { page, - date, - externalLink, - childPage; + reminder, + date; static MentionType fromString(String value) => switch (value) { 'page' => page, 'date' => date, - 'externalLink' => externalLink, - 'childPage' => childPage, // Backwards compatibility 'reminder' => date, _ => throw UnimplementedError(), @@ -30,13 +26,13 @@ Node dateMentionNode() { delta: Delta( operations: [ TextInsert( - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: DateTime.now().toIso8601String(), - reminderId: null, - reminderOption: null, - includeTime: false, - ), + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: DateTime.now().toIso8601String(), + }, + }, ), ], ), @@ -46,52 +42,15 @@ Node dateMentionNode() { class MentionBlockKeys { const MentionBlockKeys._(); + static const reminderId = 'reminder_id'; // ReminderID static const mention = 'mention'; static const type = 'type'; // MentionType, String - static const pageId = 'page_id'; - static const blockId = 'block_id'; - static const url = 'url'; // Related to Reminder and Date blocks static const date = 'date'; // Start Date static const includeTime = 'include_time'; - static const reminderId = 'reminder_id'; // ReminderID static const reminderOption = 'reminder_option'; - - static const mentionChar = '\$'; - - static Map buildMentionPageAttributes({ - required MentionType mentionType, - required String pageId, - required String? blockId, - }) { - return { - MentionBlockKeys.mention: { - MentionBlockKeys.type: mentionType.name, - MentionBlockKeys.pageId: pageId, - if (blockId != null) MentionBlockKeys.blockId: blockId, - }, - }; - } - - static Map buildMentionDateAttributes({ - required String date, - required String? reminderId, - required String? reminderOption, - required bool includeTime, - }) { - return { - MentionBlockKeys.mention: { - MentionBlockKeys.type: MentionType.date.name, - MentionBlockKeys.date: date, - MentionBlockKeys.includeTime: includeTime, - if (reminderId != null) MentionBlockKeys.reminderId: reminderId, - if (reminderOption != null) - MentionBlockKeys.reminderOption: reminderOption, - }, - }; - } } class MentionBlock extends StatelessWidget { @@ -115,36 +74,15 @@ class MentionBlock extends StatelessWidget { switch (type) { case MentionType.page: - final String? pageId = mention[MentionBlockKeys.pageId] as String?; - if (pageId == null) { - return const SizedBox.shrink(); - } - final String? blockId = mention[MentionBlockKeys.blockId] as String?; - + final String pageId = mention[MentionBlockKeys.pageId]; return MentionPageBlock( key: ValueKey(pageId), editorState: editorState, pageId: pageId, - blockId: blockId, node: node, textStyle: textStyle, index: index, ); - case MentionType.childPage: - final String? pageId = mention[MentionBlockKeys.pageId] as String?; - if (pageId == null) { - return const SizedBox.shrink(); - } - - return MentionSubPageBlock( - key: ValueKey(pageId), - editorState: editorState, - pageId: pageId, - node: node, - textStyle: textStyle, - index: index, - ); - case MentionType.date: final String date = mention[MentionBlockKeys.date]; final reminderOption = ReminderOption.values.firstWhereOrNull( @@ -156,23 +94,13 @@ class MentionBlock extends StatelessWidget { editorState: editorState, date: date, node: node, - textStyle: textStyle, index: index, reminderId: mention[MentionBlockKeys.reminderId], - reminderOption: reminderOption ?? ReminderOption.none, + reminderOption: reminderOption, includeTime: mention[MentionBlockKeys.includeTime] ?? false, ); - case MentionType.externalLink: - final String? url = mention[MentionBlockKeys.url] as String?; - if (url == null) { - return const SizedBox.shrink(); - } - return MentionLinkBlock( - url: url, - editorState: editorState, - node: node, - index: index, - ); + default: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 20f60be23d..31fe8f0a4e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -2,30 +2,32 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; +import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; -import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/reminder.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:calendar_view/calendar_view.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:nanoid/non_secure.dart'; -import 'package:universal_platform/universal_platform.dart'; class MentionDateBlock extends StatefulWidget { const MentionDateBlock({ @@ -34,9 +36,8 @@ class MentionDateBlock extends StatefulWidget { required this.date, required this.index, required this.node, - this.textStyle, this.reminderId, - this.reminderOption = ReminderOption.none, + this.reminderOption, this.includeTime = false, }); @@ -49,151 +50,242 @@ class MentionDateBlock extends StatefulWidget { /// null or empty final String? reminderId; - final ReminderOption reminderOption; + final ReminderOption? reminderOption; final bool includeTime; - final TextStyle? textStyle; - @override State createState() => _MentionDateBlockState(); } class _MentionDateBlockState extends State { + final PopoverMutex mutex = PopoverMutex(); + late bool _includeTime = widget.includeTime; late DateTime? parsedDate = DateTime.tryParse(widget.date); - @override - void didUpdateWidget(covariant oldWidget) { - parsedDate = DateTime.tryParse(widget.date); - super.didUpdateWidget(oldWidget); - } - @override Widget build(BuildContext context) { if (parsedDate == null) { return const SizedBox.shrink(); } - final appearance = context.read(); - final reminder = context.read(); + final fontSize = context.read().state.fontSize; - if (appearance == null || reminder == null) { - return const SizedBox.shrink(); - } + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: context.read()), + BlocProvider.value( + value: context.read(), + ), + ], + child: BlocBuilder( + buildWhen: (previous, current) => + previous.dateFormat != current.dateFormat || + previous.timeFormat != current.timeFormat, + builder: (context, appearance) => + BlocBuilder( + builder: (context, state) { + final reminder = state.reminders + .firstWhereOrNull((r) => r.id == widget.reminderId); - return BlocBuilder( - buildWhen: (previous, current) => - previous.dateFormat != current.dateFormat || - previous.timeFormat != current.timeFormat, - builder: (context, appearance) => - BlocBuilder( - builder: (context, state) { - final reminder = state.reminders - .firstWhereOrNull((r) => r.id == widget.reminderId); + final formattedDate = appearance.dateFormat + .formatDate(parsedDate!, _includeTime, appearance.timeFormat); - final formattedDate = appearance.dateFormat - .formatDate(parsedDate!, _includeTime, appearance.timeFormat); + final timeStr = parsedDate != null + ? _timeFromDate(parsedDate!, appearance.timeFormat) + : null; - final options = DatePickerOptions( - focusedDay: parsedDate, - selectedDay: parsedDate, - includeTime: _includeTime, - dateFormat: appearance.dateFormat, - timeFormat: appearance.timeFormat, - selectedReminderOption: widget.reminderOption, - onIncludeTimeChanged: (includeTime, dateTime, _) { - _includeTime = includeTime; + final options = DatePickerOptions( + focusedDay: parsedDate, + popoverMutex: mutex, + selectedDay: parsedDate, + timeStr: timeStr, + includeTime: _includeTime, + dateFormat: appearance.dateFormat, + timeFormat: appearance.timeFormat, + selectedReminderOption: widget.reminderOption, + onIncludeTimeChanged: (includeTime) { + _includeTime = includeTime; - if (widget.reminderOption != ReminderOption.none) { - _updateReminder( - widget.reminderOption, - reminder, - includeTime, - ); - } else if (dateTime != null) { - parsedDate = dateTime; - _updateBlock( - dateTime, - includeTime: includeTime, - ); - } - }, - onDaySelected: (selectedDay) { - parsedDate = selectedDay; + if (![null, ReminderOption.none] + .contains(widget.reminderOption)) { + _updateReminder( + widget.reminderOption!, + reminder, + includeTime, + ); + } else { + _updateBlock( + parsedDate!.withoutTime, + includeTime: includeTime, + ); + } + }, + onStartTimeChanged: (time) { + final parsed = _parseTime(time, appearance.timeFormat); + parsedDate = parsedDate!.withoutTime + .add(Duration(hours: parsed.hour, minutes: parsed.minute)); - if (widget.reminderOption != ReminderOption.none) { - _updateReminder( - widget.reminderOption, - reminder, - _includeTime, - ); - } else { - _updateBlock(selectedDay, includeTime: _includeTime); - } - }, - onReminderSelected: (reminderOption) => - _updateReminder(reminderOption, reminder), - ); + if (![null, ReminderOption.none] + .contains(widget.reminderOption)) { + _updateReminder( + widget.reminderOption!, + reminder, + _includeTime, + ); + } else { + _updateBlock(parsedDate!, includeTime: _includeTime); + } + }, + onDaySelected: (selectedDay, focusedDay) { + parsedDate = selectedDay; - Color? color; - if (reminder != null) { - if (reminder.type == ReminderType.today) { - color = Theme.of(context).isLightMode - ? const Color(0xFFFE0299) - : Theme.of(context).colorScheme.error; - } - } - final textStyle = widget.textStyle?.copyWith( - color: color, - leadingDistribution: TextLeadingDistribution.even, - ); + if (![null, ReminderOption.none] + .contains(widget.reminderOption)) { + _updateReminder( + widget.reminderOption!, + reminder, + _includeTime, + ); + } else { + _updateBlock(selectedDay, includeTime: _includeTime); + } + }, + onReminderSelected: (reminderOption) => + _updateReminder(reminderOption, reminder), + ); - // when font size equals 14, the icon size is 16.0. - // scale the icon size based on the font size. - final iconSize = (widget.textStyle?.fontSize ?? 14.0) / 14.0 * 16.0; - - return GestureDetector( - onTapDown: (details) { - _showDatePicker( - context: context, - offset: details.globalPosition, - reminder: reminder, - options: options, - ); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Text( - '@$formattedDate', - style: textStyle, - strutStyle: textStyle != null - ? StrutStyle.fromTextStyle(textStyle) - : null, + return GestureDetector( + onTapDown: (details) { + if (widget.editorState.editable) { + if (PlatformExtension.isMobile) { + showMobileBottomSheet( + context, + builder: (_) => DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.4, + snapSizes: const [0.4, 0.7, 1.0], + builder: (_, controller) => Material( + color: + Theme.of(context).colorScheme.secondaryContainer, + child: ListView( + controller: controller, + children: [ + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: const Center(child: DragHandle()), + ), + const MobileDateHeader(), + MobileAppFlowyDatePicker( + selectedDay: parsedDate, + timeStr: timeStr, + dateStr: parsedDate != null + ? options.dateFormat + .formatDate(parsedDate!, _includeTime) + : null, + includeTime: options.includeTime, + use24hFormat: options.timeFormat == + UserTimeFormatPB.TwentyFourHour, + rebuildOnDaySelected: true, + rebuildOnTimeChanged: true, + timeFormat: options.timeFormat.simplified, + selectedReminderOption: widget.reminderOption, + onDaySelected: options.onDaySelected, + onStartTimeChanged: (time) => options + .onStartTimeChanged + ?.call(time ?? ""), + onIncludeTimeChanged: + options.onIncludeTimeChanged, + liveDateFormatter: (selected) => + appearance.dateFormat.formatDate( + selected, + false, + appearance.timeFormat, + ), + onReminderSelected: (option) => + _updateReminder(option, reminder), + ), + ], + ), + ), + ), + ); + } else { + DatePickerMenu( + context: context, + editorState: widget.editorState, + ).show(details.globalPosition, options: options); + } + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowySvg( + widget.reminderId != null + ? FlowySvgs.clock_alarm_s + : FlowySvgs.date_s, + size: const Size.square(18.0), + color: reminder?.isAck == true + ? Theme.of(context).colorScheme.error + : null, + ), + const HSpace(2), + FlowyText( + formattedDate, + fontSize: fontSize, + color: reminder?.isAck == true + ? Theme.of(context).colorScheme.error + : null, + ), + ], ), - const HSpace(4), - FlowySvg( - widget.reminderId != null - ? FlowySvgs.reminder_clock_s - : FlowySvgs.date_s, - size: Size.square(iconSize), - color: textStyle?.color, - ), - ], + ), ), - ), - ); - }, + ); + }, + ), ), ); } + DateTime _parseTime(String timeStr, UserTimeFormatPB timeFormat) { + final twelveHourFormat = DateFormat('hh:mm a'); + final twentyFourHourFormat = DateFormat('HH:mm'); + + try { + if (timeFormat == UserTimeFormatPB.TwelveHour) { + return twelveHourFormat.parseStrict(timeStr); + } + + return twentyFourHourFormat.parseStrict(timeStr); + } on FormatException { + Log.error("failed to parse time string ($timeStr)"); + return DateTime.now(); + } + } + + String _timeFromDate(DateTime date, UserTimeFormatPB timeFormat) { + final twelveHourFormat = DateFormat('HH:mm a'); + final twentyFourHourFormat = DateFormat('HH:mm'); + + if (timeFormat == TimeFormatPB.TwelveHour) { + return twelveHourFormat.format(date); + } + + return twentyFourHourFormat.format(date); + } + void _updateBlock( DateTime date, { - required bool includeTime, + bool includeTime = false, String? reminderId, ReminderOption? reminderOption, }) { @@ -201,22 +293,21 @@ class _MentionDateBlockState extends State { (reminderOption == ReminderOption.none ? null : widget.reminderId); final transaction = widget.editorState.transaction - ..formatText( - widget.node, - widget.index, - 1, - MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - reminderId: rId, - includeTime: includeTime, - reminderOption: reminderOption?.name ?? widget.reminderOption.name, - ), - ); + ..formatText(widget.node, widget.index, 1, { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + MentionBlockKeys.reminderId: rId, + MentionBlockKeys.includeTime: includeTime, + MentionBlockKeys.reminderOption: + reminderOption?.name ?? widget.reminderOption?.name, + }, + }); widget.editorState.apply(transaction, withUpdateSelection: false); // Length of rendered block changes, this synchronizes - // the cursor with the new block render + // the cursor with the new block render widget.editorState.updateSelectionWithReason( widget.editorState.selection, ); @@ -251,9 +342,7 @@ class _MentionDateBlockState extends State { ReminderEvent.update( ReminderUpdate( id: widget.reminderId!, - scheduledAt: - reminderOption.getNotificationDateTime(parsedDate!), - date: parsedDate!, + scheduledAt: reminderOption.fromDate(parsedDate!), ), ), ); @@ -279,8 +368,6 @@ class _MentionDateBlockState extends State { meta: { ReminderMetaKeys.includeTime: false.toString(), ReminderMetaKeys.blockId: widget.node.id, - ReminderMetaKeys.createdAt: - DateTime.now().millisecondsSinceEpoch.toString(), }, scheduledAt: Int64(parsedDate!.millisecondsSinceEpoch ~/ 1000), isAck: parsedDate!.isBefore(DateTime.now()), @@ -288,91 +375,4 @@ class _MentionDateBlockState extends State { ), ); } - - void _showDatePicker({ - required BuildContext context, - required DatePickerOptions options, - required Offset offset, - ReminderPB? reminder, - }) { - if (!widget.editorState.editable) { - return; - } - if (UniversalPlatform.isMobile) { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - - showMobileBottomSheet( - context, - builder: (_) => DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.7, - minChildSize: 0.4, - snapSizes: const [0.4, 0.7, 1.0], - builder: (_, controller) => _DatePickerBottomSheet( - controller: controller, - parsedDate: parsedDate, - options: options, - includeTime: _includeTime, - reminderOption: widget.reminderOption, - onReminderSelected: (option) => _updateReminder( - option, - reminder, - ), - ), - ), - ); - } else { - DatePickerMenu( - context: context, - editorState: widget.editorState, - ).show(offset, options: options); - } - } -} - -class _DatePickerBottomSheet extends StatelessWidget { - const _DatePickerBottomSheet({ - required this.controller, - required this.parsedDate, - required this.options, - required this.includeTime, - required this.reminderOption, - required this.onReminderSelected, - }); - - final ScrollController controller; - final DateTime? parsedDate; - final DatePickerOptions options; - final bool includeTime; - final ReminderOption reminderOption; - final void Function(ReminderOption) onReminderSelected; - - @override - Widget build(BuildContext context) { - return Material( - color: Theme.of(context).colorScheme.secondaryContainer, - child: ListView( - controller: controller, - children: [ - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: const Center(child: DragHandle()), - ), - const MobileDateHeader(), - MobileAppFlowyDatePicker( - dateTime: parsedDate, - includeTime: includeTime, - isRange: options.isRange, - dateFormat: options.dateFormat.simplified, - timeFormat: options.timeFormat.simplified, - reminderOption: reminderOption, - onDaySelected: options.onDaySelected, - onIncludeTimeChanged: options.onIncludeTimeChanged, - onReminderSelected: onReminderSelected, - ), - ], - ), - ); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart deleted file mode 100644 index 06ebcb5002..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_block.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_embed/link_embed_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/shared.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'mention_link_error_preview.dart'; -import 'mention_link_preview.dart'; - -class MentionLinkBlock extends StatefulWidget { - const MentionLinkBlock({ - super.key, - required this.url, - required this.editorState, - required this.node, - required this.index, - this.delayToShow = const Duration(milliseconds: 50), - this.delayToHide = const Duration(milliseconds: 300), - }); - - final String url; - final Duration delayToShow; - final Duration delayToHide; - final EditorState editorState; - final Node node; - final int index; - - @override - State createState() => _MentionLinkBlockState(); -} - -class _MentionLinkBlockState extends State { - final parser = LinkParser(); - _LoadingStatus status = _LoadingStatus.loading; - late LinkInfo linkInfo = LinkInfo(url: url); - final previewController = PopoverController(); - bool isHovering = false; - int previewFocusNum = 0; - bool isPreviewHovering = false; - bool showAtBottom = false; - final key = GlobalKey(); - - bool get isPreviewShowing => previewFocusNum > 0; - String get url => widget.url; - - EditorState get editorState => widget.editorState; - - Node get node => widget.node; - - int get index => widget.index; - - bool get readyForPreview => - status == _LoadingStatus.idle && !linkInfo.isEmpty(); - - @override - void initState() { - super.initState(); - - parser.addLinkInfoListener((v) { - final hasNewInfo = !v.isEmpty(), hasOldInfo = !linkInfo.isEmpty(); - if (mounted) { - setState(() { - if (hasNewInfo) { - linkInfo = v; - status = _LoadingStatus.idle; - } else if (!hasOldInfo) { - status = _LoadingStatus.error; - } - }); - } - }); - parser.start(url); - } - - @override - void dispose() { - super.dispose(); - parser.dispose(); - previewController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - key: ValueKey(showAtBottom), - controller: previewController, - direction: showAtBottom - ? PopoverDirection.bottomWithLeftAligned - : PopoverDirection.topWithLeftAligned, - offset: Offset(0, showAtBottom ? -20 : 20), - onOpen: () { - keepEditorFocusNotifier.increase(); - previewFocusNum++; - }, - onClose: () { - keepEditorFocusNotifier.decrease(); - previewFocusNum--; - }, - decorationColor: Colors.transparent, - popoverDecoration: BoxDecoration(), - margin: EdgeInsets.zero, - constraints: getConstraints(), - borderRadius: BorderRadius.circular(16), - popupBuilder: (context) => readyForPreview - ? MentionLinkPreview( - linkInfo: linkInfo, - showAtBottom: showAtBottom, - triggerSize: getSizeFromKey(), - onEnter: (e) { - isPreviewHovering = true; - }, - onExit: (e) { - isPreviewHovering = false; - tryToDismissPreview(); - }, - onCopyLink: () => copyLink(context), - onConvertTo: (s) => convertTo(s), - onRemoveLink: removeLink, - onOpenLink: openLink, - ) - : MentionLinkErrorPreview( - url: url, - triggerSize: getSizeFromKey(), - onEnter: (e) { - isPreviewHovering = true; - }, - onExit: (e) { - isPreviewHovering = false; - tryToDismissPreview(); - }, - onCopyLink: () => copyLink(context), - onConvertTo: (s) => convertTo(s), - onRemoveLink: removeLink, - onOpenLink: openLink, - ), - child: buildIconWithTitle(context), - ); - } - - Widget buildIconWithTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final siteName = linkInfo.siteName, linkTitle = linkInfo.title ?? url; - - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: onEnter, - onExit: onExit, - child: GestureDetector( - onTap: () async { - await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - }, - child: FlowyHoverContainer( - style: - HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary), - applyStyle: isHovering, - child: Row( - mainAxisSize: MainAxisSize.min, - key: key, - children: [ - HSpace(2), - buildIcon(), - HSpace(4), - Flexible( - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - children: [ - if (siteName != null) ...[ - TextSpan( - text: siteName, - style: theme.textStyle.body - .standard(color: theme.textColorScheme.secondary), - ), - WidgetSpan(child: HSpace(2)), - ], - TextSpan( - text: linkTitle, - style: theme.textStyle.body - .standard(color: theme.textColorScheme.primary), - ), - ], - ), - ), - ), - HSpace(2), - ], - ), - ), - ), - ); - } - - Widget buildIcon() { - const defaultWidget = FlowySvg(FlowySvgs.toolbar_link_earth_m); - Widget icon = defaultWidget; - if (status == _LoadingStatus.loading) { - icon = Padding( - padding: const EdgeInsets.all(2.0), - child: const CircularProgressIndicator(strokeWidth: 1), - ); - } else { - icon = linkInfo.buildIconWidget(); - } - return SizedBox( - height: 20, - width: 20, - child: icon, - ); - } - - RenderBox? get box => key.currentContext?.findRenderObject() as RenderBox?; - - Size getSizeFromKey() => box?.size ?? Size.zero; - - Future copyLink(BuildContext context) async { - await context.copyLink(url); - previewController.close(); - } - - Future openLink() async { - await afLaunchUrlString(url, addingHttpSchemeWhenFailed: true); - } - - Future removeLink() async { - final transaction = editorState.transaction - ..replaceText(widget.node, widget.index, 1, url, attributes: {}); - await editorState.apply(transaction); - } - - Future convertTo(PasteMenuType type) async { - if (type == PasteMenuType.url) { - await toUrl(); - } else if (type == PasteMenuType.bookmark) { - await toLinkPreview(); - } else if (type == PasteMenuType.embed) { - await toLinkPreview(previewType: LinkEmbedKeys.embed); - } - } - - Future toUrl() async { - final transaction = editorState.transaction - ..replaceText( - widget.node, - widget.index, - 1, - url, - attributes: { - AppFlowyRichTextKeys.href: url, - }, - ); - await editorState.apply(transaction); - } - - Future toLinkPreview({String? previewType}) async { - final selection = Selection( - start: Position(path: node.path, offset: index), - end: Position(path: node.path, offset: index + 1), - ); - await convertUrlToLinkPreview( - editorState, - selection, - url, - previewType: previewType, - ); - } - - void changeHovering(bool hovering) { - if (isHovering == hovering) return; - if (mounted) { - setState(() { - isHovering = hovering; - }); - } - } - - void changeShowAtBottom(bool bottom) { - if (showAtBottom == bottom) return; - if (mounted) { - setState(() { - showAtBottom = bottom; - }); - } - } - - void tryToDismissPreview() { - Future.delayed(widget.delayToHide, () { - if (isHovering || isPreviewHovering) { - return; - } - previewController.close(); - }); - } - - void onEnter(PointerEnterEvent e) { - changeHovering(true); - final location = box?.localToGlobal(Offset.zero) ?? Offset.zero; - if (readyForPreview) { - if (location.dy < 300) { - changeShowAtBottom(true); - } else { - changeShowAtBottom(false); - } - } - Future.delayed(widget.delayToShow, () { - if (isHovering && !isPreviewShowing && status != _LoadingStatus.loading) { - showPreview(); - } - }); - } - - void onExit(PointerExitEvent e) { - changeHovering(false); - tryToDismissPreview(); - } - - void showPreview() { - if (!mounted) return; - keepEditorFocusNotifier.increase(); - previewController.show(); - previewFocusNum++; - } - - BoxConstraints getConstraints() { - final size = getSizeFromKey(); - if (!readyForPreview) { - return BoxConstraints( - maxWidth: max(320, size.width), - maxHeight: 48 + size.height, - ); - } - final hasImage = linkInfo.imageUrl?.isNotEmpty ?? false; - return BoxConstraints( - maxWidth: max(300, size.width), - maxHeight: hasImage ? 300 : 180, - ); - } -} - -enum _LoadingStatus { - loading, - idle, - error, -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart deleted file mode 100644 index df396108e4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_error_preview.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/util/theme_extension.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/services.dart'; - -class MentionLinkErrorPreview extends StatefulWidget { - const MentionLinkErrorPreview({ - super.key, - required this.url, - required this.onEnter, - required this.onExit, - required this.onCopyLink, - required this.onRemoveLink, - required this.onConvertTo, - required this.onOpenLink, - required this.triggerSize, - }); - - final String url; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final VoidCallback onCopyLink; - final VoidCallback onRemoveLink; - final VoidCallback onOpenLink; - final ValueChanged onConvertTo; - final Size triggerSize; - - @override - State createState() => - _MentionLinkErrorPreviewState(); -} - -class _MentionLinkErrorPreviewState extends State { - final menuController = PopoverController(); - bool isConvertButtonSelected = false; - - @override - void dispose() { - super.dispose(); - menuController.close(); - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: SizedBox( - width: max(320, widget.triggerSize.width), - height: 48, - child: Align( - alignment: Alignment.centerLeft, - child: Container( - width: 320, - height: 48, - decoration: buildToolbarLinkDecoration(context), - padding: EdgeInsets.fromLTRB(12, 8, 8, 8), - child: Row( - children: [ - Expanded(child: buildLinkWidget()), - Container( - height: 20, - width: 1, - color: Color(0xffE8ECF3) - .withAlpha(Theme.of(context).isLightMode ? 255 : 40), - margin: EdgeInsets.symmetric(horizontal: 6), - ), - FlowyIconButton( - icon: FlowySvg(FlowySvgs.toolbar_link_m), - tooltipText: LocaleKeys.editor_copyLink.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: widget.onCopyLink, - ), - buildConvertButton(), - ], - ), - ), - ), - ), - ), - MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - onTap: widget.onOpenLink, - child: Container( - width: widget.triggerSize.width, - height: widget.triggerSize.height, - color: Colors.black.withAlpha(1), - ), - ), - ), - ], - ); - } - - Widget buildLinkWidget() { - final url = widget.url; - return FlowyTooltip( - message: url, - preferBelow: false, - child: FlowyText.regular( - url, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 20, - fontSize: 14, - ), - ); - } - - Widget buildConvertButton() { - return AppFlowyPopover( - offset: Offset(8, 10), - direction: PopoverDirection.bottomWithRightAligned, - margin: EdgeInsets.zero, - controller: menuController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - icon: FlowySvg(FlowySvgs.turninto_m), - isSelected: isConvertButtonSelected, - tooltipText: LocaleKeys.editor_convertTo.tr(), - preferBelow: false, - width: 36, - height: 32, - onPressed: () { - setState(() { - isConvertButtonSelected = true; - }); - showPopover(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: List.generate(MentionLinktErrorMenuCommand.values.length, - (index) { - final command = MentionLinktErrorMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - menuController.show(); - } - - void closePopover() { - menuController.close(); - } - - void onTap(MentionLinktErrorMenuCommand command) { - switch (command) { - case MentionLinktErrorMenuCommand.toURL: - widget.onConvertTo(PasteMenuType.url); - break; - case MentionLinktErrorMenuCommand.toBookmark: - widget.onConvertTo(PasteMenuType.bookmark); - break; - case MentionLinktErrorMenuCommand.toEmbed: - widget.onConvertTo(PasteMenuType.embed); - break; - case MentionLinktErrorMenuCommand.removeLink: - widget.onRemoveLink(); - break; - } - closePopover(); - } -} - -enum MentionLinktErrorMenuCommand { - toURL, - toBookmark, - toEmbed, - removeLink; - - String get title { - switch (this) { - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart deleted file mode 100644 index 00b161379e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_link_preview.dart +++ /dev/null @@ -1,276 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/custom_link_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/paste_as/paste_as_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.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/services.dart'; - -class MentionLinkPreview extends StatefulWidget { - const MentionLinkPreview({ - super.key, - required this.linkInfo, - required this.onEnter, - required this.onExit, - required this.onCopyLink, - required this.onRemoveLink, - required this.onConvertTo, - required this.onOpenLink, - required this.triggerSize, - required this.showAtBottom, - }); - - final LinkInfo linkInfo; - final PointerEnterEventListener onEnter; - final PointerExitEventListener onExit; - final VoidCallback onCopyLink; - final VoidCallback onRemoveLink; - final VoidCallback onOpenLink; - final ValueChanged onConvertTo; - final Size triggerSize; - final bool showAtBottom; - - @override - State createState() => _MentionLinkPreviewState(); -} - -class _MentionLinkPreviewState extends State { - final menuController = PopoverController(); - bool isSelected = false; - - LinkInfo get linkInfo => widget.linkInfo; - - @override - void dispose() { - super.dispose(); - menuController.close(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context), - textColorScheme = theme.textColorScheme; - final imageUrl = linkInfo.imageUrl ?? '', - description = linkInfo.description ?? ''; - final imageHeight = 120.0; - final card = MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Container( - decoration: buildToolbarLinkDecoration(context, radius: 16), - width: 280, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (imageUrl.isNotEmpty) - ClipRRect( - borderRadius: - const BorderRadius.vertical(top: Radius.circular(16)), - child: FlowyNetworkImage( - url: linkInfo.imageUrl ?? '', - width: 280, - height: imageHeight, - ), - ), - VSpace(12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyText.semibold( - linkInfo.title ?? linkInfo.siteName ?? '', - fontSize: 14, - figmaLineHeight: 20, - color: textColorScheme.primary, - overflow: TextOverflow.ellipsis, - ), - ), - VSpace(4), - if (description.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: FlowyText( - description, - fontSize: 12, - figmaLineHeight: 16, - color: textColorScheme.secondary, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - VSpace(36), - ], - Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - height: 28, - child: Row( - children: [ - linkInfo.buildIconWidget(size: Size.square(16)), - HSpace(6), - Expanded( - child: FlowyText( - linkInfo.siteName ?? linkInfo.url, - fontSize: 12, - figmaLineHeight: 16, - color: textColorScheme.primary, - overflow: TextOverflow.ellipsis, - fontWeight: FontWeight.w700, - ), - ), - buildMoreOptionButton(), - ], - ), - ), - VSpace(12), - ], - ), - ), - ); - - final clickPlaceHolder = MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: widget.onEnter, - onExit: widget.onExit, - child: GestureDetector( - child: Container( - height: 20, - width: widget.triggerSize.width, - color: Colors.white.withAlpha(1), - ), - onTap: () { - widget.onOpenLink.call(); - closePopover(); - }, - ), - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: widget.showAtBottom - ? [clickPlaceHolder, card] - : [card, clickPlaceHolder], - ); - } - - Widget buildMoreOptionButton() { - return AppFlowyPopover( - controller: menuController, - direction: PopoverDirection.topWithLeftAligned, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - margin: EdgeInsets.zero, - borderRadius: BorderRadius.circular(12), - popupBuilder: (context) => buildConvertMenu(), - child: FlowyIconButton( - width: 28, - height: 28, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - size: Size.square(20), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ), - ); - } - - Widget buildConvertMenu() { - return MouseRegion( - onEnter: widget.onEnter, - onExit: widget.onExit, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(0.0), - children: - List.generate(MentionLinktMenuCommand.values.length, (index) { - final command = MentionLinktMenuCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - onTap: () => onTap(command), - ), - ); - }), - ), - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - menuController.show(); - } - - void closePopover() { - menuController.close(); - } - - void onTap(MentionLinktMenuCommand command) { - switch (command) { - case MentionLinktMenuCommand.toURL: - widget.onConvertTo(PasteMenuType.url); - break; - case MentionLinktMenuCommand.toBookmark: - widget.onConvertTo(PasteMenuType.bookmark); - break; - case MentionLinktMenuCommand.toEmbed: - widget.onConvertTo(PasteMenuType.embed); - break; - case MentionLinktMenuCommand.copyLink: - widget.onCopyLink(); - break; - case MentionLinktMenuCommand.removeLink: - widget.onRemoveLink(); - break; - } - closePopover(); - } -} - -enum MentionLinktMenuCommand { - toURL, - toBookmark, - toEmbed, - copyLink, - removeLink; - - String get title { - switch (this) { - case toURL: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toUrl - .tr(); - case toBookmark: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_toBookmark - .tr(); - case toEmbed: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_toEmbed - .tr(); - case copyLink: - return LocaleKeys.document_plugins_linkPreview_linkPreviewMenu_copyLink - .tr(); - case removeLink: - return LocaleKeys - .document_plugins_linkPreview_linkPreviewMenu_removeLink - .tr(); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart deleted file mode 100644 index 28a698dde2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -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/delta/text_delta_extension.dart'; -import 'package:appflowy/plugins/trash/application/prelude.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'mention_page_bloc.freezed.dart'; - -typedef MentionPageStatus = (ViewPB? view, bool isInTrash, bool isDeleted); - -class MentionPageBloc extends Bloc { - MentionPageBloc({ - required this.pageId, - this.blockId, - bool isSubPage = false, - }) : _isSubPage = isSubPage, - super(MentionPageState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - final (view, isInTrash, isDeleted) = - await ViewBackendService.getMentionPageStatus(pageId); - - String? blockContent; - if (!_isSubPage) { - blockContent = await _getBlockContent(); - } - - emit( - state.copyWith( - view: view, - isLoading: false, - isInTrash: isInTrash, - isDeleted: isDeleted, - blockContent: blockContent ?? '', - ), - ); - - if (view != null) { - _startListeningView(); - _startListeningTrash(); - - if (!_isSubPage) { - _startListeningDocument(); - } - } - }, - didUpdateViewStatus: (view, isDeleted) async { - emit( - state.copyWith( - view: view, - isDeleted: isDeleted ?? state.isDeleted, - ), - ); - }, - didUpdateTrashStatus: (isInTrash) async => - emit(state.copyWith(isInTrash: isInTrash)), - didUpdateBlockContent: (content) { - emit( - state.copyWith( - blockContent: content, - ), - ); - }, - ); - }, - ); - } - - @override - Future close() { - _viewListener?.stop(); - _trashListener?.close(); - _documentListener?.stop(); - return super.close(); - } - - final _documentService = DocumentService(); - - final String pageId; - final String? blockId; - final bool _isSubPage; - - ViewListener? _viewListener; - TrashListener? _trashListener; - - DocumentListener? _documentListener; - BlockPB? _block; - String? _blockTextId; - Delta? _initialDelta; - - void _startListeningView() { - _viewListener = ViewListener(viewId: pageId) - ..start( - onViewUpdated: (view) => add( - MentionPageEvent.didUpdateViewStatus(view: view, isDeleted: false), - ), - onViewDeleted: (_) => - add(const MentionPageEvent.didUpdateViewStatus(isDeleted: true)), - ); - } - - void _startListeningTrash() { - _trashListener = TrashListener() - ..start( - trashUpdated: (trashOrFailed) { - final trash = trashOrFailed.toNullable(); - if (trash != null) { - final isInTrash = trash.any((t) => t.id == pageId); - add(MentionPageEvent.didUpdateTrashStatus(isInTrash: isInTrash)); - } - }, - ); - } - - Future _convertDeltaToText(Delta? delta) async { - if (delta == null) { - return _initialDelta?.toPlainText() ?? ''; - } - - return delta.toText( - getMentionPageName: (mentionedPageId) async { - if (mentionedPageId == pageId) { - // if the mention page is the current page, return the view name - return state.view?.name ?? ''; - } else { - // if the mention page is not the current page, return the mention page name - final viewResult = await ViewBackendService.getView(mentionedPageId); - final name = viewResult.fold((l) => l.name, (f) => ''); - return name; - } - }, - ); - } - - Future _getBlockContent() async { - if (blockId == null) { - return null; - } - - final documentNodeResult = await _documentService.getDocumentNode( - documentId: pageId, - blockId: blockId!, - ); - final documentNode = documentNodeResult.fold((l) => l, (f) => null); - if (documentNode == null) { - Log.error( - 'unable to get the document node for block $blockId in page $pageId', - ); - return null; - } - - final block = documentNode.$2; - final node = documentNode.$3; - - _blockTextId = (node.externalValues as ExternalValues?)?.externalId; - _initialDelta = node.delta; - _block = block; - - return _convertDeltaToText(_initialDelta); - } - - void _startListeningDocument() { - // only observe the block content if the block id is not null - if (blockId == null || - _blockTextId == null || - _initialDelta == null || - _block == null) { - return; - } - - _documentListener = DocumentListener(id: pageId) - ..start( - onDocEventUpdate: (docEvent) { - for (final block in docEvent.events) { - for (final event in block.event) { - if (event.id == _blockTextId) { - if (event.command == DeltaTypePB.Updated) { - _updateBlockContent(event.value); - } else if (event.command == DeltaTypePB.Removed) { - add(const MentionPageEvent.didUpdateBlockContent('')); - } - } - } - } - }, - ); - } - - Future _updateBlockContent(String deltaJson) async { - if (_initialDelta == null || _block == null) { - return; - } - - try { - final incremental = Delta.fromJson(jsonDecode(deltaJson)); - final delta = _initialDelta!.compose(incremental); - final content = await _convertDeltaToText(delta); - add(MentionPageEvent.didUpdateBlockContent(content)); - _initialDelta = delta; - } catch (e) { - Log.error('failed to update block content: $e'); - } - } -} - -@freezed -class MentionPageEvent with _$MentionPageEvent { - const factory MentionPageEvent.initial() = _Initial; - const factory MentionPageEvent.didUpdateViewStatus({ - @Default(null) ViewPB? view, - @Default(null) bool? isDeleted, - }) = _DidUpdateViewStatus; - const factory MentionPageEvent.didUpdateTrashStatus({ - required bool isInTrash, - }) = _DidUpdateTrashStatus; - const factory MentionPageEvent.didUpdateBlockContent( - String content, - ) = _DidUpdateBlockContent; -} - -@freezed -class MentionPageState with _$MentionPageState { - const factory MentionPageState({ - required bool isLoading, - required bool isInTrash, - required bool isDeleted, - // non-null case: - // - page is found - // - page is in trash - // null case: - // - page is deleted - required ViewPB? view, - // the plain text content of the block - // it doesn't contain any formatting - required String blockContent, - }) = _MentionSubPageState; - - factory MentionPageState.initial() => const MentionPageState( - isLoading: true, - isInTrash: false, - isDeleted: false, - view: null, - blockContent: '', - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart index ede690eb30..6755be9690 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart @@ -1,46 +1,31 @@ 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/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' show - ApplyOptions, Delta, EditorState, Node, - NodeIterator, - Path, - Position, - Selection, - SelectionType, + PlatformExtension, TextInsert, TextTransaction, paragraphNode; 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:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'package:provider/provider.dart'; final pageMemorizer = {}; @@ -49,30 +34,24 @@ Node pageMentionNode(String viewId) { delta: Delta( operations: [ TextInsert( - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: viewId, - blockId: null, - ), + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: viewId, + }, + }, ), ], ), ); } -class ReferenceState { - ReferenceState(this.isReference); - - final bool isReference; -} - class MentionPageBlock extends StatefulWidget { const MentionPageBlock({ super.key, required this.editorState, required this.pageId, - required this.blockId, required this.node, required this.textStyle, required this.index, @@ -80,7 +59,6 @@ class MentionPageBlock extends StatefulWidget { final EditorState editorState; final String pageId; - final String? blockId; final Node node; final TextStyle? textStyle; @@ -92,162 +70,138 @@ class MentionPageBlock extends StatefulWidget { } class _MentionPageBlockState extends State { + late final EditorState editorState; + late final ViewListener viewListener = ViewListener(viewId: widget.pageId); + late Future viewPBFuture; + @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => MentionPageBloc( - pageId: widget.pageId, - blockId: widget.blockId, - )..add(const MentionPageEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final view = state.view; - if (state.isLoading) { - return const SizedBox.shrink(); - } + void initState() { + super.initState(); - if (state.isDeleted || view == null) { - return _NoAccessMentionPageBlock( - textStyle: widget.textStyle, - ); - } - - if (UniversalPlatform.isMobile) { - return _MobileMentionPageBlock( - view: view, - content: state.blockContent, - textStyle: widget.textStyle, - handleTap: () => handleMentionBlockTap( - context, - widget.editorState, - view, - blockId: widget.blockId, - ), - handleDoubleTap: () => _handleDoubleTap( - context, - widget.editorState, - view.id, - widget.node, - widget.index, - ), - ); - } else { - return _DesktopMentionPageBlock( - view: view, - content: state.blockContent, - textStyle: widget.textStyle, - showTrashHint: state.isInTrash, - handleTap: () => handleMentionBlockTap( - context, - widget.editorState, - view, - blockId: widget.blockId, - ), - ); - } - }, - ), + editorState = context.read(); + viewPBFuture = fetchView(widget.pageId); + viewListener.start( + onViewUpdated: (p0) { + pageMemorizer[p0.id] = p0; + viewPBFuture = fetchView(widget.pageId); + editorState.reload(); + }, ); } - void updateSelection() { - WidgetsBinding.instance.addPostFrameCallback( - (_) => widget.editorState - .updateSelectionWithReason(widget.editorState.selection), - ); - } -} - -class MentionSubPageBlock extends StatefulWidget { - const MentionSubPageBlock({ - super.key, - required this.editorState, - required this.pageId, - required this.node, - required this.textStyle, - required this.index, - }); - - final EditorState editorState; - final String pageId; - final Node node; - final TextStyle? textStyle; - - // Used to update the block - final int index; - @override - State createState() => _MentionSubPageBlockState(); -} - -class _MentionSubPageBlockState extends State { - late bool isHandlingPaste = context.read().isHandlingPaste; + void dispose() { + viewListener.stop(); + super.dispose(); + } @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => MentionPageBloc(pageId: widget.pageId, isSubPage: true) - ..add(const MentionPageEvent.initial()), - child: BlocConsumer( - listener: (context, state) async { - if (state.view != null) { - final currentViewId = getIt().latestOpenView?.id; - if (currentViewId == null) { - return; - } + return FutureBuilder( + initialData: pageMemorizer[widget.pageId], + future: viewPBFuture, + builder: (context, state) { + final view = state.data; + // memorize the result + pageMemorizer[widget.pageId] = view; + if (view == null) { + return const SizedBox.shrink(); + } - if (state.view!.parentViewId != currentViewId) { - SchedulerBinding.instance.addPostFrameCallback((_) { - if (context.mounted) { - turnIntoPageRef(); - } - }); - } - } - }, - builder: (context, state) { - final view = state.view; - if (state.isLoading || isHandlingPaste) { - return const SizedBox.shrink(); - } - - if (state.isDeleted || view == null) { - return _DeletedPageBlock(textStyle: widget.textStyle); - } - - if (UniversalPlatform.isMobile) { - return _MobileMentionPageBlock( - view: view, - showTrashHint: state.isInTrash, - textStyle: widget.textStyle, - handleTap: () => - handleMentionBlockTap(context, widget.editorState, view), - isChildPage: true, - content: '', - handleDoubleTap: () => _handleDoubleTap( - context, - widget.editorState, - view.id, - widget.node, - widget.index, + final iconSize = widget.textStyle?.fontSize ?? 16.0; + final child = GestureDetector( + onTap: handleTap, + onDoubleTap: handleDoubleTap, + behavior: HitTestBehavior.translucent, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const HSpace(4), + view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 12, + textAlign: TextAlign.center, + lineHeight: 1.3, + ) + : FlowySvg( + view.layout.icon, + size: Size.square(iconSize + 2.0), + ), + const HSpace(2), + FlowyText( + view.name, + decoration: TextDecoration.underline, + fontSize: widget.textStyle?.fontSize, + fontWeight: widget.textStyle?.fontWeight, ), - ); - } else { - return _DesktopMentionPageBlock( - view: view, - showTrashHint: state.isInTrash, - content: null, - textStyle: widget.textStyle, - isChildPage: true, - handleTap: () => - handleMentionBlockTap(context, widget.editorState, view), - ); - } - }, - ), + const HSpace(2), + ], + ), + ); + + if (PlatformExtension.isMobile) { + return child; + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: FlowyHover( + cursor: SystemMouseCursors.click, + child: child, + ), + ); + }, ); } + Future handleTap() async { + final view = await fetchView(widget.pageId); + if (view == null) { + Log.error('Page(${widget.pageId}) not found'); + return; + } + + if (PlatformExtension.isMobile && mounted) { + await context.pushView(view); + } else { + getIt().add( + TabsEvent.openPlugin(plugin: view.plugin(), view: view), + ); + } + } + + Future handleDoubleTap() async { + if (!PlatformExtension.isMobile) { + return; + } + + final currentViewId = context.read().documentId; + final viewId = await showPageSelectorSheet( + context, + currentViewId: currentViewId, + selectedViewId: widget.pageId, + ); + + if (viewId != null) { + // Update this nodes pageId + final transaction = widget.editorState.transaction + ..formatText( + widget.node, + widget.index, + 1, + { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: viewId, + }, + }, + ); + + await widget.editorState.apply(transaction, withUpdateSelection: false); + } + } + Future fetchView(String pageId) async { final view = await ViewBackendService.getView(pageId).then( (value) => value.toNullable(), @@ -271,404 +225,10 @@ class _MentionSubPageBlockState extends State { } void updateSelection() { - WidgetsBinding.instance.addPostFrameCallback( - (_) => widget.editorState - .updateSelectionWithReason(widget.editorState.selection), - ); - } - - void turnIntoPageRef() { - final transaction = widget.editorState.transaction - ..formatText( - widget.node, - widget.index, - MentionBlockKeys.mentionChar.length, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: widget.pageId, - blockId: null, - ), + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + editorState.updateSelectionWithReason( + editorState.selection, ); - - widget.editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions( - recordUndo: false, - ), - ); - } -} - -Path? _findNodePathByBlockId(EditorState editorState, String blockId) { - final document = editorState.document; - final startNode = document.root.children.firstOrNull; - if (startNode == null) { - return null; - } - - final nodeIterator = NodeIterator( - document: document, - startNode: startNode, - ); - while (nodeIterator.moveNext()) { - final node = nodeIterator.current; - if (node.id == blockId) { - return node.path; - } - } - - return null; -} - -Future handleMentionBlockTap( - BuildContext context, - EditorState editorState, - ViewPB view, { - String? blockId, -}) async { - final currentViewId = context.read().documentId; - if (currentViewId == view.id && blockId != null) { - // same page - final path = _findNodePathByBlockId(editorState, blockId); - if (path != null) { - editorState.scrollService?.jumpTo(path.first); - await editorState.updateSelectionWithReason( - Selection.collapsed(Position(path: path)), - customSelectionType: SelectionType.block, - ); - } - return; - } - - if (UniversalPlatform.isMobile) { - if (context.mounted && currentViewId != view.id) { - await context.pushView( - view, - blockId: blockId, - tabs: [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ].map((e) => e.name).toList(), - ); - } - } else { - final action = NavigationAction( - objectId: view.id, - arguments: { - ActionArgumentKeys.view: view, - ActionArgumentKeys.blockId: blockId, - }, - ); - getIt().add( - ActionNavigationEvent.performAction( - action: action, - ), - ); - } -} - -Future _handleDoubleTap( - BuildContext context, - EditorState editorState, - String viewId, - Node node, - int index, -) async { - if (!UniversalPlatform.isMobile) { - return; - } - - final currentViewId = context.read().documentId; - final newView = await showPageSelectorSheet( - context, - currentViewId: currentViewId, - selectedViewId: viewId, - ); - - if (newView != null) { - // Update this nodes pageId - final transaction = editorState.transaction - ..formatText( - node, - index, - 1, - MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: newView.id, - blockId: null, - ), - ); - - await editorState.apply(transaction, withUpdateSelection: false); - } -} - -class _MentionPageBlockContent extends StatelessWidget { - const _MentionPageBlockContent({ - required this.view, - required this.textStyle, - this.content, - this.showTrashHint = false, - this.isChildPage = false, - }); - - final ViewPB view; - final TextStyle? textStyle; - final String? content; - final bool showTrashHint; - final bool isChildPage; - - @override - Widget build(BuildContext context) { - final text = _getDisplayText(context, view, content); - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - ..._buildPrefixIcons(context, view, content, isChildPage), - const HSpace(4), - Flexible( - child: FlowyText( - text, - decoration: TextDecoration.underline, - fontSize: textStyle?.fontSize, - fontWeight: textStyle?.fontWeight, - lineHeight: textStyle?.height, - overflow: TextOverflow.ellipsis, - ), - ), - if (showTrashHint) ...[ - FlowyText( - LocaleKeys.document_mention_trashHint.tr(), - fontSize: textStyle?.fontSize, - fontWeight: textStyle?.fontWeight, - lineHeight: textStyle?.height, - color: Theme.of(context).disabledColor, - decoration: TextDecoration.underline, - decorationColor: AFThemeExtension.of(context).textColor, - ), - ], - const HSpace(4), - ], - ); - } - - List _buildPrefixIcons( - BuildContext context, - ViewPB view, - String? content, - bool isChildPage, - ) { - final isSameDocument = _isSameDocument(context, view.id); - final shouldDisplayViewName = _shouldDisplayViewName( - context, - view.id, - content, - ); - final isBlockContentEmpty = content == null || content.isEmpty; - final emojiSize = textStyle?.fontSize ?? 12.0; - final iconSize = textStyle?.fontSize ?? 16.0; - - // if the block is from the same doc, display the paragraph mark icon '¶' - if (isSameDocument && !isBlockContentEmpty) { - return [ - const HSpace(2), - FlowySvg( - FlowySvgs.paragraph_mark_s, - size: Size.square(iconSize - 2.0), - color: Theme.of(context).hintColor, - ), - ]; - } else if (shouldDisplayViewName) { - return [ - const HSpace(4), - Stack( - children: [ - view.icon.value.isNotEmpty - ? EmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: emojiSize, - ) - : view.defaultIcon(size: Size.square(iconSize + 2.0)), - if (!isChildPage) ...[ - const Positioned( - right: 0, - bottom: 0, - child: FlowySvg( - FlowySvgs.referenced_page_s, - blendMode: BlendMode.dstIn, - ), - ), - ], - ], - ), - ]; - } - - return []; - } - - String _getDisplayText( - BuildContext context, - ViewPB view, - String? blockContent, - ) { - final shouldDisplayViewName = _shouldDisplayViewName( - context, - view.id, - blockContent, - ); - - if (blockContent == null || blockContent.isEmpty) { - return shouldDisplayViewName - ? view.name - .orDefault(LocaleKeys.menuAppHeader_defaultNewPageName.tr()) - : ''; - } - - return shouldDisplayViewName - ? '${view.name} - $blockContent' - : blockContent; - } - - // display the view name or not - // if the block is from the same doc, - // 1. block content is not empty, display the **block content only**. - // 2. block content is empty, display the **view name**. - // if the block is from another doc, - // 1. block content is not empty, display the **view name and block content**. - // 2. block content is empty, display the **view name**. - bool _shouldDisplayViewName( - BuildContext context, - String viewId, - String? blockContent, - ) { - if (_isSameDocument(context, viewId)) { - return blockContent == null || blockContent.isEmpty; - } - return true; - } - - bool _isSameDocument(BuildContext context, String viewId) { - final currentViewId = context.read()?.documentId; - return viewId == currentViewId; - } -} - -class _NoAccessMentionPageBlock extends StatelessWidget { - const _NoAccessMentionPageBlock({required this.textStyle}); - - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - return FlowyHover( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: FlowyText( - LocaleKeys.document_mention_noAccess.tr(), - color: Theme.of(context).disabledColor, - decoration: TextDecoration.underline, - fontSize: textStyle?.fontSize, - fontWeight: textStyle?.fontWeight, - ), - ), - ); - } -} - -class _DeletedPageBlock extends StatelessWidget { - const _DeletedPageBlock({required this.textStyle}); - - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - return FlowyHover( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: FlowyText( - LocaleKeys.document_mention_deletedPage.tr(), - color: Theme.of(context).disabledColor, - decoration: TextDecoration.underline, - fontSize: textStyle?.fontSize, - fontWeight: textStyle?.fontWeight, - ), - ), - ); - } -} - -class _MobileMentionPageBlock extends StatelessWidget { - const _MobileMentionPageBlock({ - required this.view, - required this.content, - required this.textStyle, - required this.handleTap, - required this.handleDoubleTap, - this.showTrashHint = false, - this.isChildPage = false, - }); - - final TextStyle? textStyle; - final ViewPB view; - final String content; - final VoidCallback handleTap; - final VoidCallback handleDoubleTap; - final bool showTrashHint; - final bool isChildPage; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: handleTap, - onDoubleTap: handleDoubleTap, - behavior: HitTestBehavior.opaque, - child: _MentionPageBlockContent( - view: view, - content: content, - textStyle: textStyle, - showTrashHint: showTrashHint, - isChildPage: isChildPage, - ), - ); - } -} - -class _DesktopMentionPageBlock extends StatelessWidget { - const _DesktopMentionPageBlock({ - required this.view, - required this.textStyle, - required this.handleTap, - required this.content, - this.showTrashHint = false, - this.isChildPage = false, - }); - - final TextStyle? textStyle; - final ViewPB view; - final String? content; - final VoidCallback handleTap; - final bool showTrashHint; - final bool isChildPage; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: handleTap, - behavior: HitTestBehavior.opaque, - child: FlowyHover( - cursor: SystemMouseCursors.click, - child: _MentionPageBlockContent( - view: view, - content: content, - textStyle: textStyle, - showTrashHint: showTrashHint, - isChildPage: isChildPage, - ), - ), - ); + }); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart index f3578f185e..d15d24aab7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart @@ -1,34 +1,30 @@ +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/flowy_search_text_field.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -Future showPageSelectorSheet( +Future showPageSelectorSheet( BuildContext context, { String? currentViewId, String? selectedViewId, - bool Function(ViewPB view)? filter, }) async { - filter ??= (v) => !v.isSpace && v.parentViewId.isNotEmpty; - - return showMobileBottomSheet( + return showMobileBottomSheet( context, title: LocaleKeys.document_mobilePageSelector_title.tr(), showHeader: true, showCloseButton: true, showDragHandle: true, - useSafeArea: false, - backgroundColor: Theme.of(context).colorScheme.surface, - builder: (context) => ConstrainedBox( + builder: (context) => Container( + margin: const EdgeInsets.only(top: 12.0), constraints: const BoxConstraints( maxHeight: 340, minHeight: 80, @@ -36,22 +32,16 @@ Future showPageSelectorSheet( child: _MobilePageSelectorBody( currentViewId: currentViewId, selectedViewId: selectedViewId, - filter: filter, ), ), ); } class _MobilePageSelectorBody extends StatefulWidget { - const _MobilePageSelectorBody({ - this.currentViewId, - this.selectedViewId, - this.filter, - }); + const _MobilePageSelectorBody({this.currentViewId, this.selectedViewId}); final String? currentViewId; final String? selectedViewId; - final bool Function(ViewPB view)? filter; @override State<_MobilePageSelectorBody> createState() => @@ -89,10 +79,7 @@ class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { ); } - final views = snapshot.data! - .where((v) => widget.filter?.call(v) ?? true) - .toList(); - + final views = snapshot.data!; if (widget.currentViewId != null) { views.removeWhere((v) => v.id == widget.currentViewId); } @@ -114,27 +101,27 @@ class _MobilePageSelectorBodyState extends State<_MobilePageSelectorBody> { } return Flexible( - child: ListView.builder( - itemCount: filtered.length, - itemBuilder: (context, index) { - final view = filtered.elementAt(index); - return FlowyOptionTile.checkbox( - leftIcon: view.icon.value.isNotEmpty - ? RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 18, - ) - : FlowySvg( - view.layout.icon, - size: const Size.square(20), - ), - text: view.name, - showTopBorder: index != 0, - showBottomBorder: false, - isSelected: view.id == widget.selectedViewId, - onTap: () => Navigator.of(context).pop(view), - ); - }, + child: ListView( + children: filtered + .map( + (view) => FlowyOptionTile.checkbox( + leftIcon: view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 18, + textAlign: TextAlign.center, + lineHeight: 1.3, + ) + : FlowySvg( + view.layout.icon, + size: const Size.square(20), + ), + text: view.name, + isSelected: view.id == widget.selectedViewId, + onTap: () => Navigator.of(context).pop(view.id), + ), + ) + .toList(), ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart new file mode 100644 index 0000000000..8216ea5ab3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart @@ -0,0 +1,47 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; + +SelectionMenuItem dateMenuItem = SelectionMenuItem( + getName: LocaleKeys.document_plugins_insertDate.tr, + icon: (_, isSelected, style) => FlowySvg( + FlowySvgs.date_s, + color: isSelected + ? style.selectionMenuItemSelectedIconColor + : style.selectionMenuItemIconColor, + ), + keywords: ['insert date', 'date', 'time'], + handler: (editorState, menuService, context) => + _insertDateReference(editorState), +); + +Future _insertDateReference(EditorState editorState) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return; + } + + final node = editorState.getNodeAtPath(selection.end.path); + final delta = node?.delta; + if (node == null || delta == null) { + return; + } + + final transaction = editorState.transaction + ..replaceText( + node, + selection.start.offset, + 0, + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: DateTime.now().toIso8601String(), + }, + }, + ); + + await editorState.apply(transaction); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart deleted file mode 100644 index 7dcd21f423..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/menu/menu_extension.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -extension MenuExtension on EditorState { - MenuPosition? calculateMenuOffset({ - Rect? rect, - required double menuWidth, - required double menuHeight, - Offset menuOffset = const Offset(0, 10), - }) { - final selectionService = service.selectionService; - final selectionRects = selectionService.selectionRects; - late Rect startRect; - if (rect != null) { - startRect = rect; - } else { - if (selectionRects.isEmpty) return null; - startRect = selectionRects.first; - } - - final editorOffset = renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = renderBox!.size.height; - final editorWidth = renderBox!.size.width; - - // show below default - Alignment alignment = Alignment.topLeft; - final bottomRight = startRect.bottomRight; - final topRight = startRect.topRight; - var startOffset = bottomRight + menuOffset; - Offset offset = Offset( - startOffset.dx, - startOffset.dy, - ); - - // show above - if (startOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { - startOffset = topRight - menuOffset; - alignment = Alignment.bottomLeft; - - offset = Offset( - startOffset.dx, - editorHeight + editorOffset.dy - startOffset.dy, - ); - } - - // show on right - if (offset.dx + menuWidth < editorOffset.dx + editorWidth) { - offset = Offset( - offset.dx, - offset.dy, - ); - } else if (startOffset.dx - editorOffset.dx > menuWidth) { - // show on left - alignment = alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - offset = Offset( - editorWidth - offset.dx + editorOffset.dx, - offset.dy, - ); - } - return MenuPosition(align: alignment, offset: offset); - } -} - -class MenuPosition { - MenuPosition({ - required this.align, - required this.offset, - }); - - final Alignment align; - final Offset offset; - - LTRB get ltrb { - double? left, top, right, bottom; - switch (align) { - case Alignment.topLeft: - left = offset.dx; - top = offset.dy; - break; - case Alignment.bottomLeft: - left = offset.dx; - bottom = offset.dy; - break; - case Alignment.topRight: - right = offset.dx; - top = offset.dy; - break; - case Alignment.bottomRight: - right = offset.dx; - bottom = offset.dy; - break; - } - - return LTRB(left: left, top: top, right: right, bottom: bottom); - } -} - -class LTRB { - LTRB({this.left, this.top, this.right, this.bottom}); - - final double? left; - final double? top; - final double? right; - final double? bottom; - - Positioned buildPositioned({required Widget child}) => Positioned( - left: left, - top: top, - right: right, - bottom: bottom, - child: child, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart index 41bb8ce873..664e2143b6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/migration/editor_migration.dart @@ -2,13 +2,11 @@ import 'dart:convert'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; import 'package:string_validator/string_validator.dart'; @@ -54,9 +52,7 @@ class EditorMigration { node = pageNode(children: children); } } else if (id == 'callout') { - final icon = nodeV0.attributes[CalloutBlockKeys.icon] ?? '📌'; - final iconType = nodeV0.attributes[CalloutBlockKeys.iconType] ?? - FlowyIconType.emoji.name; + final emoji = nodeV0.attributes['emoji'] ?? '📌'; final delta = nodeV0.children.whereType().fold(Delta(), (p, e) { final delta = migrateDelta(e.delta); @@ -66,18 +62,8 @@ class EditorMigration { } return p..insert('\n'); }); - EmojiIconData? emojiIconData; - try { - emojiIconData = - EmojiIconData(FlowyIconType.values.byName(iconType), icon); - } catch (e) { - Log.error( - 'migrateNode get EmojiIconData error with :${nodeV0.attributes}', - e, - ); - } node = calloutNode( - emoji: emojiIconData, + emoji: emoji, delta: delta, ); } else if (id == 'divider') { @@ -178,17 +164,18 @@ class EditorMigration { // Now, the cover is stored in the view.ext. static void migrateCoverIfNeeded( ViewPB view, - Attributes attributes, { + EditorState editorState, { bool overwrite = false, }) async { if (view.extra.isNotEmpty && !overwrite) { return; } + final root = editorState.document.root; final coverType = CoverType.fromString( - attributes[DocumentHeaderBlockKeys.coverType], + root.attributes[DocumentHeaderBlockKeys.coverType], ); - final coverDetails = attributes[DocumentHeaderBlockKeys.coverDetails]; + final coverDetails = root.attributes[DocumentHeaderBlockKeys.coverDetails]; Map extra = {}; @@ -240,14 +227,6 @@ class EditorMigration { }, }; } - } else { - extra = { - ViewExtKeys.coverKey: { - ViewExtKeys.coverTypeKey: - PageStyleCoverImageType.localImage.toString(), - ViewExtKeys.coverValueKey: coverDetails, - }, - }; } break; default: diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart index ba170e8d24..e3b320a63d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -24,7 +23,7 @@ List buildMobileFloatingToolbarItems( ContextMenuButtonItem( label: LocaleKeys.editor_copy.tr(), onPressed: () { - customCopyCommand.execute(editorState); + copyCommand.execute(editorState); closeToolbar(); }, ), @@ -35,7 +34,7 @@ List buildMobileFloatingToolbarItems( ContextMenuButtonItem( label: LocaleKeys.editor_paste.tr(), onPressed: () { - customPasteCommand.execute(editorState); + pasteCommand.execute(editorState); closeToolbar(); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart index 8e1a8533e0..57670afadd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart @@ -6,8 +6,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockKeys, quoteNode; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart index 6ec777429c..438aa1264b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart @@ -83,10 +83,10 @@ class _TextColorAndBackgroundColorState ), ), const VSpace(6.0), - EditorTextColorWidget( + _TextColors( selectedColor: selectedTextColor?.tryToColor(), onSelectedColor: (textColor) async { - final hex = textColor.a == 0 ? null : textColor.toHex(); + final hex = textColor.alpha == 0 ? null : textColor.toHex(); final selection = widget.selection; if (selection.isCollapsed) { widget.editorState.updateToggledStyle( @@ -120,10 +120,11 @@ class _TextColorAndBackgroundColorState ), ), const VSpace(6.0), - EditorBackgroundColors( + _BackgroundColors( selectedColor: selectedBackgroundColor?.tryToColor(), onSelectedColor: (backgroundColor) async { - final hex = backgroundColor.a == 0 ? null : backgroundColor.toHex(); + final hex = + backgroundColor.alpha == 0 ? null : backgroundColor.toHex(); final selection = widget.selection; if (selection.isCollapsed) { widget.editorState.updateToggledStyle( @@ -151,9 +152,8 @@ class _TextColorAndBackgroundColorState } } -class EditorBackgroundColors extends StatelessWidget { - const EditorBackgroundColors({ - super.key, +class _BackgroundColors extends StatelessWidget { + const _BackgroundColors({ this.selectedColor, required this.onSelectedColor, }); @@ -225,9 +225,8 @@ class _BackgroundColorItem extends StatelessWidget { } } -class EditorTextColorWidget extends StatelessWidget { - EditorTextColorWidget({ - super.key, +class _TextColors extends StatelessWidget { + _TextColors({ this.selectedColor, required this.onSelectedColor, }); @@ -295,7 +294,7 @@ class _TextColorItem extends StatelessWidget { child: FlowyText( 'A', fontSize: 24, - color: color.a == 0 ? null : color, + color: color.alpha == 0 ? null : color, ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart index 96431996f5..b1004a3eae 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart @@ -75,7 +75,7 @@ class FontFamilyItem extends StatelessWidget { } }); }, - text: (fontFamily ?? systemFonFamily).fontFamilyDisplayName, + text: (fontFamily ?? systemFonFamily).parseFontFamilyName(), fontFamily: fontFamily ?? systemFonFamily, backgroundColor: theme.toolbarMenuItemBackgroundColor, isSelected: false, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_attachment_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_attachment_item.dart deleted file mode 100644 index e31d9f68a6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_attachment_item.dart +++ /dev/null @@ -1,237 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:image_picker/image_picker.dart'; - -@visibleForTesting -const addAttachmentToolbarItemKey = ValueKey('add_attachment_toolbar_item'); - -final addAttachmentItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, service, _, onAction) { - return AppFlowyMobileToolbarIconItem( - key: addAttachmentToolbarItemKey, - editorState: editorState, - icon: FlowySvgs.media_s, - onTap: () { - final documentId = context.read().documentId; - final isLocalMode = context.read().isLocalMode; - - final selection = editorState.selection; - service.closeKeyboard(); - - // delay to wait the keyboard closed. - Future.delayed(const Duration(milliseconds: 100), () async { - unawaited( - editorState.updateSelectionWithReason( - selection, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDisableFloatingToolbar: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ), - ); - - keepEditorFocusNotifier.increase(); - final didAddAttachment = await showAddAttachmentMenu( - AppGlobals.rootNavKey.currentContext!, - documentId: documentId, - isLocalMode: isLocalMode, - editorState: editorState, - selection: selection!, - ); - - if (didAddAttachment != true) { - unawaited(editorState.updateSelectionWithReason(selection)); - } - }); - }, - ); - }, -); - -Future showAddAttachmentMenu( - BuildContext context, { - required String documentId, - required bool isLocalMode, - required EditorState editorState, - required Selection selection, -}) async => - showMobileBottomSheet( - context, - showDragHandle: true, - barrierColor: Colors.transparent, - backgroundColor: - ToolbarColorExtension.of(context).toolbarMenuBackgroundColor, - elevation: 20, - isScrollControlled: false, - enableDraggableScrollable: true, - builder: (_) => _AddAttachmentMenu( - documentId: documentId, - isLocalMode: isLocalMode, - editorState: editorState, - selection: selection, - ), - ); - -class _AddAttachmentMenu extends StatelessWidget { - const _AddAttachmentMenu({ - required this.documentId, - required this.isLocalMode, - required this.editorState, - required this.selection, - }); - - final String documentId; - final bool isLocalMode; - final EditorState editorState; - final Selection selection; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - MobileQuickActionButton( - text: LocaleKeys.document_attachmentMenu_choosePhoto.tr(), - icon: FlowySvgs.image_rounded_s, - iconSize: const Size.square(20), - onTap: () async => selectPhoto(context), - ), - const MobileQuickActionDivider(), - MobileQuickActionButton( - text: LocaleKeys.document_attachmentMenu_takePicture.tr(), - icon: FlowySvgs.camera_s, - iconSize: const Size.square(20), - onTap: () async => selectCamera(context), - ), - const MobileQuickActionDivider(), - MobileQuickActionButton( - text: LocaleKeys.document_attachmentMenu_chooseFile.tr(), - icon: FlowySvgs.file_s, - iconSize: const Size.square(20), - onTap: () async => selectFile(context), - ), - ], - ), - ); - } - - Future _insertNode(Node node) async { - Future.delayed( - const Duration(milliseconds: 100), - () async { - // if current selected block is a empty paragraph block, replace it with the new block. - if (selection.isCollapsed) { - final path = selection.end.path; - final currentNode = editorState.getNodeAtPath(path); - final text = currentNode?.delta?.toPlainText(); - if (currentNode != null && - currentNode.type == ParagraphBlockKeys.type && - text != null && - text.isEmpty) { - final transaction = editorState.transaction; - transaction.insertNode(path.next, node); - transaction.deleteNode(currentNode); - transaction.afterSelection = - Selection.collapsed(Position(path: path)); - transaction.selectionExtraInfo = {}; - return editorState.apply(transaction); - } - } - - await editorState.insertBlockAfterCurrentSelection(selection, node); - }, - ); - } - - Future insertImage(BuildContext context, XFile image) async { - CustomImageType type = CustomImageType.local; - String? path; - if (isLocalMode) { - path = await saveImageToLocalStorage(image.path); - } else { - (path, _) = await saveImageToCloudStorage(image.path, documentId); - type = CustomImageType.internal; - } - - if (path != null) { - final node = customImageNode(url: path, type: type); - await _insertNode(node); - } - } - - Future selectPhoto(BuildContext context) async { - final image = await ImagePicker().pickImage(source: ImageSource.gallery); - - if (image != null && context.mounted) { - await insertImage(context, image); - } - - if (context.mounted) { - Navigator.pop(context); - } - } - - Future selectCamera(BuildContext context) async { - final cameraPermission = - await PermissionChecker.checkCameraPermission(context); - if (!cameraPermission) { - Log.error('Has no permission to access the camera'); - return; - } - - final image = await ImagePicker().pickImage(source: ImageSource.camera); - - if (image != null && context.mounted) { - await insertImage(context, image); - } - - if (context.mounted) { - Navigator.pop(context); - } - } - - Future selectFile(BuildContext context) async { - final result = await getIt().pickFiles(); - final file = result?.files.first.xFile; - if (file != null) { - FileUrlType type = FileUrlType.local; - String? path; - if (isLocalMode) { - path = await saveFileToLocalStorage(file.path); - } else { - (path, _) = await saveFileToCloudStorage(file.path, documentId); - type = FileUrlType.cloud; - } - - if (path != null) { - final node = fileNode(url: path, type: type, name: file.name); - await _insertNode(node); - } - } - - if (context.mounted) { - Navigator.pop(context); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart deleted file mode 100644 index b3df4dfd39..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_menu_item_builder.dart +++ /dev/null @@ -1,481 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.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/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -class AddBlockMenuItemBuilder { - AddBlockMenuItemBuilder({ - required this.editorState, - required this.selection, - }); - - final EditorState editorState; - final Selection selection; - - List> buildTypeOptionMenuItemValues( - BuildContext context, - ) { - if (selection.isCollapsed) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node?.parentTableCellNode != null) { - return _buildTableTypeOptionMenuItemValues(context); - } - } - return _buildDefaultTypeOptionMenuItemValues(context); - } - - /// Build the default type option menu item values. - - List> _buildDefaultTypeOptionMenuItemValues( - BuildContext context, - ) { - final colorMap = _colorMap(context); - return [ - ..._buildHeadingMenuItems(colorMap), - ..._buildParagraphMenuItems(colorMap), - ..._buildTodoListMenuItems(colorMap), - ..._buildTableMenuItems(colorMap), - ..._buildQuoteMenuItems(colorMap), - ..._buildListMenuItems(colorMap), - ..._buildToggleHeadingMenuItems(colorMap), - ..._buildImageMenuItems(colorMap), - ..._buildPhotoGalleryMenuItems(colorMap), - ..._buildFileMenuItems(colorMap), - ..._buildMentionMenuItems(context, colorMap), - ..._buildDividerMenuItems(colorMap), - ..._buildCalloutMenuItems(colorMap), - ..._buildCodeMenuItems(colorMap), - ..._buildMathEquationMenuItems(colorMap), - ]; - } - - /// Build the table type option menu item values. - List> _buildTableTypeOptionMenuItemValues( - BuildContext context, - ) { - final colorMap = _colorMap(context); - return [ - ..._buildHeadingMenuItems(colorMap), - ..._buildParagraphMenuItems(colorMap), - ..._buildTodoListMenuItems(colorMap), - ..._buildQuoteMenuItems(colorMap), - ..._buildListMenuItems(colorMap), - ..._buildToggleHeadingMenuItems(colorMap), - ..._buildImageMenuItems(colorMap), - ..._buildFileMenuItems(colorMap), - ..._buildMentionMenuItems(context, colorMap), - ..._buildDividerMenuItems(colorMap), - ..._buildCalloutMenuItems(colorMap), - ..._buildCodeMenuItems(colorMap), - ..._buildMathEquationMenuItems(colorMap), - ]; - } - - List> _buildHeadingMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: HeadingBlockKeys.type, - backgroundColor: colorMap[HeadingBlockKeys.type]!, - text: LocaleKeys.editor_heading1.tr(), - icon: FlowySvgs.m_add_block_h1_s, - onTap: (_, __) => _insertBlock(headingNode(level: 1)), - ), - TypeOptionMenuItemValue( - value: HeadingBlockKeys.type, - backgroundColor: colorMap[HeadingBlockKeys.type]!, - text: LocaleKeys.editor_heading2.tr(), - icon: FlowySvgs.m_add_block_h2_s, - onTap: (_, __) => _insertBlock(headingNode(level: 2)), - ), - TypeOptionMenuItemValue( - value: HeadingBlockKeys.type, - backgroundColor: colorMap[HeadingBlockKeys.type]!, - text: LocaleKeys.editor_heading3.tr(), - icon: FlowySvgs.m_add_block_h3_s, - onTap: (_, __) => _insertBlock(headingNode(level: 3)), - ), - ]; - } - - List> _buildParagraphMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: ParagraphBlockKeys.type, - backgroundColor: colorMap[ParagraphBlockKeys.type]!, - text: LocaleKeys.editor_text.tr(), - icon: FlowySvgs.m_add_block_paragraph_s, - onTap: (_, __) => _insertBlock(paragraphNode()), - ), - ]; - } - - List> _buildTodoListMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: TodoListBlockKeys.type, - backgroundColor: colorMap[TodoListBlockKeys.type]!, - text: LocaleKeys.editor_checkbox.tr(), - icon: FlowySvgs.m_add_block_checkbox_s, - onTap: (_, __) => _insertBlock(todoListNode(checked: false)), - ), - ]; - } - - List> _buildTableMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: SimpleTableBlockKeys.type, - backgroundColor: colorMap[SimpleTableBlockKeys.type]!, - text: LocaleKeys.editor_table.tr(), - icon: FlowySvgs.slash_menu_icon_simple_table_s, - onTap: (_, __) => _insertBlock( - createSimpleTableBlockNode(columnCount: 2, rowCount: 2), - ), - ), - ]; - } - - List> _buildQuoteMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: QuoteBlockKeys.type, - backgroundColor: colorMap[QuoteBlockKeys.type]!, - text: LocaleKeys.editor_quote.tr(), - icon: FlowySvgs.m_add_block_quote_s, - onTap: (_, __) => _insertBlock(quoteNode()), - ), - ]; - } - - List> _buildListMenuItems( - Map colorMap, - ) { - return [ - // bulleted list, numbered list, toggle list - TypeOptionMenuItemValue( - value: BulletedListBlockKeys.type, - backgroundColor: colorMap[BulletedListBlockKeys.type]!, - text: LocaleKeys.editor_bulletedListShortForm.tr(), - icon: FlowySvgs.m_add_block_bulleted_list_s, - onTap: (_, __) => _insertBlock(bulletedListNode()), - ), - TypeOptionMenuItemValue( - value: NumberedListBlockKeys.type, - backgroundColor: colorMap[NumberedListBlockKeys.type]!, - text: LocaleKeys.editor_numberedListShortForm.tr(), - icon: FlowySvgs.m_add_block_numbered_list_s, - onTap: (_, __) => _insertBlock(numberedListNode()), - ), - TypeOptionMenuItemValue( - value: ToggleListBlockKeys.type, - backgroundColor: colorMap[ToggleListBlockKeys.type]!, - text: LocaleKeys.editor_toggleListShortForm.tr(), - icon: FlowySvgs.m_add_block_toggle_s, - onTap: (_, __) => _insertBlock(toggleListBlockNode()), - ), - ]; - } - - List> _buildToggleHeadingMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: ToggleListBlockKeys.type, - backgroundColor: colorMap[ToggleListBlockKeys.type]!, - text: LocaleKeys.editor_toggleHeading1ShortForm.tr(), - icon: FlowySvgs.toggle_heading1_s, - iconPadding: const EdgeInsets.all(3), - onTap: (_, __) => _insertBlock(toggleHeadingNode()), - ), - TypeOptionMenuItemValue( - value: ToggleListBlockKeys.type, - backgroundColor: colorMap[ToggleListBlockKeys.type]!, - text: LocaleKeys.editor_toggleHeading2ShortForm.tr(), - icon: FlowySvgs.toggle_heading2_s, - iconPadding: const EdgeInsets.all(3), - onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 2)), - ), - TypeOptionMenuItemValue( - value: ToggleListBlockKeys.type, - backgroundColor: colorMap[ToggleListBlockKeys.type]!, - text: LocaleKeys.editor_toggleHeading3ShortForm.tr(), - icon: FlowySvgs.toggle_heading3_s, - iconPadding: const EdgeInsets.all(3), - onTap: (_, __) => _insertBlock(toggleHeadingNode(level: 3)), - ), - ]; - } - - List> _buildImageMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: ImageBlockKeys.type, - backgroundColor: colorMap[ImageBlockKeys.type]!, - text: LocaleKeys.editor_image.tr(), - icon: FlowySvgs.m_add_block_image_s, - onTap: (_, __) async { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed(const Duration(milliseconds: 400), () async { - final imagePlaceholderKey = GlobalKey(); - await editorState.insertEmptyImageBlock(imagePlaceholderKey); - }); - }, - ), - ]; - } - - List> _buildPhotoGalleryMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: MultiImageBlockKeys.type, - backgroundColor: colorMap[ImageBlockKeys.type]!, - text: LocaleKeys.document_plugins_photoGallery_name.tr(), - icon: FlowySvgs.m_add_block_photo_gallery_s, - onTap: (_, __) async { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed(const Duration(milliseconds: 400), () async { - final imagePlaceholderKey = GlobalKey(); - await editorState.insertEmptyMultiImageBlock(imagePlaceholderKey); - }); - }, - ), - ]; - } - - List> _buildFileMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: FileBlockKeys.type, - backgroundColor: colorMap[ImageBlockKeys.type]!, - text: LocaleKeys.document_plugins_file_name.tr(), - icon: FlowySvgs.media_s, - onTap: (_, __) async { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed(const Duration(milliseconds: 400), () async { - final fileGlobalKey = GlobalKey(); - await editorState.insertEmptyFileBlock(fileGlobalKey); - }); - }, - ), - ]; - } - - List> _buildMentionMenuItems( - BuildContext context, - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: ParagraphBlockKeys.type, - backgroundColor: colorMap[MentionBlockKeys.type]!, - text: LocaleKeys.editor_date.tr(), - icon: FlowySvgs.m_add_block_date_s, - onTap: (_, __) => _insertBlock(dateMentionNode()), - ), - TypeOptionMenuItemValue( - value: ParagraphBlockKeys.type, - backgroundColor: colorMap[MentionBlockKeys.type]!, - text: LocaleKeys.editor_page.tr(), - icon: FlowySvgs.icon_document_s, - onTap: (_, __) async { - AppGlobals.rootNavKey.currentContext?.pop(true); - - final currentViewId = getIt().latestOpenView?.id; - final view = await showPageSelectorSheet( - context, - currentViewId: currentViewId, - ); - - if (view != null) { - Future.delayed(const Duration(milliseconds: 100), () { - editorState.insertBlockAfterCurrentSelection( - selection, - pageMentionNode(view.id), - ); - }); - } - }, - ), - ]; - } - - List> _buildDividerMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: DividerBlockKeys.type, - backgroundColor: colorMap[DividerBlockKeys.type]!, - text: LocaleKeys.editor_divider.tr(), - icon: FlowySvgs.m_add_block_divider_s, - onTap: (_, __) { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed(const Duration(milliseconds: 100), () { - editorState.insertDivider(selection); - }); - }, - ), - ]; - } - - // callout, code, math equation - List> _buildCalloutMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: CalloutBlockKeys.type, - backgroundColor: colorMap[CalloutBlockKeys.type]!, - text: LocaleKeys.document_plugins_callout.tr(), - icon: FlowySvgs.m_add_block_callout_s, - onTap: (_, __) => _insertBlock(calloutNode()), - ), - ]; - } - - List> _buildCodeMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: CodeBlockKeys.type, - backgroundColor: colorMap[CodeBlockKeys.type]!, - text: LocaleKeys.editor_codeBlockShortForm.tr(), - icon: FlowySvgs.m_add_block_code_s, - onTap: (_, __) => _insertBlock(codeBlockNode()), - ), - ]; - } - - List> _buildMathEquationMenuItems( - Map colorMap, - ) { - return [ - TypeOptionMenuItemValue( - value: MathEquationBlockKeys.type, - backgroundColor: colorMap[MathEquationBlockKeys.type]!, - text: LocaleKeys.editor_mathEquationShortForm.tr(), - icon: FlowySvgs.m_add_block_formula_s, - onTap: (_, __) { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed(const Duration(milliseconds: 100), () { - editorState.insertMathEquation(selection); - }); - }, - ), - ]; - } - - Map _colorMap(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - if (isDarkMode) { - return { - HeadingBlockKeys.type: const Color(0xFF5465A1), - ParagraphBlockKeys.type: const Color(0xFF5465A1), - TodoListBlockKeys.type: const Color(0xFF4BB299), - SimpleTableBlockKeys.type: const Color(0xFF4BB299), - QuoteBlockKeys.type: const Color(0xFFBAAC74), - BulletedListBlockKeys.type: const Color(0xFFA35F94), - NumberedListBlockKeys.type: const Color(0xFFA35F94), - ToggleListBlockKeys.type: const Color(0xFFA35F94), - ImageBlockKeys.type: const Color(0xFFBAAC74), - MentionBlockKeys.type: const Color(0xFF40AAB8), - DividerBlockKeys.type: const Color(0xFF4BB299), - CalloutBlockKeys.type: const Color(0xFF66599B), - CodeBlockKeys.type: const Color(0xFF66599B), - MathEquationBlockKeys.type: const Color(0xFF66599B), - }; - } - return { - HeadingBlockKeys.type: const Color(0xFFBECCFF), - ParagraphBlockKeys.type: const Color(0xFFBECCFF), - TodoListBlockKeys.type: const Color(0xFF98F4CD), - SimpleTableBlockKeys.type: const Color(0xFF98F4CD), - QuoteBlockKeys.type: const Color(0xFFFDEDA7), - BulletedListBlockKeys.type: const Color(0xFFFFB9EF), - NumberedListBlockKeys.type: const Color(0xFFFFB9EF), - ToggleListBlockKeys.type: const Color(0xFFFFB9EF), - ImageBlockKeys.type: const Color(0xFFFDEDA7), - MentionBlockKeys.type: const Color(0xFF91EAF5), - DividerBlockKeys.type: const Color(0xFF98F4CD), - CalloutBlockKeys.type: const Color(0xFFCABDFF), - CodeBlockKeys.type: const Color(0xFFCABDFF), - MathEquationBlockKeys.type: const Color(0xFFCABDFF), - }; - } - - Future _insertBlock(Node node) async { - AppGlobals.rootNavKey.currentContext?.pop(true); - Future.delayed( - const Duration(milliseconds: 100), - () async { - // if current selected block is a empty paragraph block, replace it with the new block. - if (selection.isCollapsed) { - final currentNode = editorState.getNodeAtPath(selection.end.path); - final text = currentNode?.delta?.toPlainText(); - if (currentNode != null && - currentNode.type == ParagraphBlockKeys.type && - text != null && - text.isEmpty) { - final transaction = editorState.transaction; - transaction.insertNode( - selection.end.path.next, - node, - ); - transaction.deleteNode(currentNode); - if (node.type == SimpleTableBlockKeys.type) { - transaction.afterSelection = Selection.collapsed( - Position( - // table -> row -> cell -> paragraph - path: selection.end.path + [0, 0, 0], - ), - ); - } else { - transaction.afterSelection = Selection.collapsed( - Position(path: selection.end.path), - ); - } - transaction.selectionExtraInfo = {}; - await editorState.apply(transaction); - return; - } - } - - await editorState.insertBlockAfterCurrentSelection(selection, node); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart index c09368ff95..d0be5af466 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart @@ -1,25 +1,29 @@ import 'dart:async'; +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/type_option_menu_item.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mobile_page_selector_sheet.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'add_block_menu_item_builder.dart'; - -@visibleForTesting -const addBlockToolbarItemKey = ValueKey('add_block_toolbar_item'); +import 'package:go_router/go_router.dart'; final addBlockToolbarItem = AppFlowyMobileToolbarItem( itemBuilder: (context, editorState, service, __, onAction) { return AppFlowyMobileToolbarIconItem( - key: addBlockToolbarItemKey, editorState: editorState, icon: FlowySvgs.m_toolbar_add_m, onTap: () { @@ -71,13 +75,12 @@ Future showAddBlockMenu( enableDraggableScrollable: true, builder: (_) => Padding( padding: EdgeInsets.all(16 * context.scale), - child: AddBlockMenu(selection: selection, editorState: editorState), + child: _AddBlockMenu(selection: selection, editorState: editorState), ), ); -class AddBlockMenu extends StatelessWidget { - const AddBlockMenu({ - super.key, +class _AddBlockMenu extends StatelessWidget { + const _AddBlockMenu({ required this.selection, required this.editorState, }); @@ -87,13 +90,243 @@ class AddBlockMenu extends StatelessWidget { @override Widget build(BuildContext context) { - final builder = AddBlockMenuItemBuilder( - editorState: editorState, - selection: selection, - ); return TypeOptionMenu( - values: builder.buildTypeOptionMenuItemValues(context), + values: buildTypeOptionMenuItemValues(context), scaleFactor: context.scale, ); } + + Future _insertBlock(Node node) async { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed( + const Duration(milliseconds: 100), + () => editorState.insertBlockAfterCurrentSelection(selection, node), + ); + } + + List> buildTypeOptionMenuItemValues( + BuildContext context, + ) { + final colorMap = _colorMap(context); + return [ + // heading 1 - 3 + TypeOptionMenuItemValue( + value: HeadingBlockKeys.type, + backgroundColor: colorMap[HeadingBlockKeys.type]!, + text: LocaleKeys.editor_heading1.tr(), + icon: FlowySvgs.m_add_block_h1_s, + onTap: (_, __) => _insertBlock(headingNode(level: 1)), + ), + TypeOptionMenuItemValue( + value: HeadingBlockKeys.type, + backgroundColor: colorMap[HeadingBlockKeys.type]!, + text: LocaleKeys.editor_heading2.tr(), + icon: FlowySvgs.m_add_block_h2_s, + onTap: (_, __) => _insertBlock(headingNode(level: 2)), + ), + TypeOptionMenuItemValue( + value: HeadingBlockKeys.type, + backgroundColor: colorMap[HeadingBlockKeys.type]!, + text: LocaleKeys.editor_heading3.tr(), + icon: FlowySvgs.m_add_block_h3_s, + onTap: (_, __) => _insertBlock(headingNode(level: 3)), + ), + + // paragraph + TypeOptionMenuItemValue( + value: ParagraphBlockKeys.type, + backgroundColor: colorMap[ParagraphBlockKeys.type]!, + text: LocaleKeys.editor_text.tr(), + icon: FlowySvgs.m_add_block_paragraph_s, + onTap: (_, __) => _insertBlock(paragraphNode()), + ), + + // checkbox + TypeOptionMenuItemValue( + value: TodoListBlockKeys.type, + backgroundColor: colorMap[TodoListBlockKeys.type]!, + text: LocaleKeys.editor_checkbox.tr(), + icon: FlowySvgs.m_add_block_checkbox_s, + onTap: (_, __) => _insertBlock(todoListNode(checked: false)), + ), + + // quote + TypeOptionMenuItemValue( + value: QuoteBlockKeys.type, + backgroundColor: colorMap[QuoteBlockKeys.type]!, + text: LocaleKeys.editor_quote.tr(), + icon: FlowySvgs.m_add_block_quote_s, + onTap: (_, __) => _insertBlock(quoteNode()), + ), + + // bulleted list, numbered list, toggle list + TypeOptionMenuItemValue( + value: BulletedListBlockKeys.type, + backgroundColor: colorMap[BulletedListBlockKeys.type]!, + text: LocaleKeys.editor_bulletedListShortForm.tr(), + icon: FlowySvgs.m_add_block_bulleted_list_s, + onTap: (_, __) => _insertBlock(bulletedListNode()), + ), + TypeOptionMenuItemValue( + value: NumberedListBlockKeys.type, + backgroundColor: colorMap[NumberedListBlockKeys.type]!, + text: LocaleKeys.editor_numberedListShortForm.tr(), + icon: FlowySvgs.m_add_block_numbered_list_s, + onTap: (_, __) => _insertBlock(numberedListNode()), + ), + TypeOptionMenuItemValue( + value: ToggleListBlockKeys.type, + backgroundColor: colorMap[ToggleListBlockKeys.type]!, + text: LocaleKeys.editor_toggleListShortForm.tr(), + icon: FlowySvgs.m_add_block_toggle_s, + onTap: (_, __) => _insertBlock(toggleListBlockNode()), + ), + + // image + TypeOptionMenuItemValue( + value: ImageBlockKeys.type, + backgroundColor: colorMap[ImageBlockKeys.type]!, + text: LocaleKeys.editor_image.tr(), + icon: FlowySvgs.m_add_block_image_s, + onTap: (_, __) async { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 400), () async { + final imagePlaceholderKey = GlobalKey(); + await editorState.insertEmptyImageBlock(imagePlaceholderKey); + }); + }, + ), + + // date + TypeOptionMenuItemValue( + value: ParagraphBlockKeys.type, + backgroundColor: colorMap[MentionBlockKeys.type]!, + text: LocaleKeys.editor_date.tr(), + icon: FlowySvgs.m_add_block_date_s, + onTap: (_, __) => _insertBlock(dateMentionNode()), + ), + // page + TypeOptionMenuItemValue( + value: ParagraphBlockKeys.type, + backgroundColor: colorMap[MentionBlockKeys.type]!, + text: LocaleKeys.editor_page.tr(), + icon: FlowySvgs.document_s, + onTap: (_, __) async { + AppGlobals.rootNavKey.currentContext?.pop(true); + + final currentViewId = getIt().latestOpenView?.id; + final viewId = await showPageSelectorSheet( + context, + currentViewId: currentViewId, + ); + + if (viewId != null) { + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertBlockAfterCurrentSelection( + selection, + pageMentionNode(viewId), + ); + }); + } + }, + ), + + // divider + TypeOptionMenuItemValue( + value: DividerBlockKeys.type, + backgroundColor: colorMap[DividerBlockKeys.type]!, + text: LocaleKeys.editor_divider.tr(), + icon: FlowySvgs.m_add_block_divider_s, + onTap: (_, __) { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertDivider(selection); + }); + }, + ), + + // callout, code, math equation + TypeOptionMenuItemValue( + value: CalloutBlockKeys.type, + backgroundColor: colorMap[CalloutBlockKeys.type]!, + text: LocaleKeys.document_plugins_callout.tr(), + icon: FlowySvgs.m_add_block_callout_s, + onTap: (_, __) => _insertBlock(calloutNode()), + ), + TypeOptionMenuItemValue( + value: CodeBlockKeys.type, + backgroundColor: colorMap[CodeBlockKeys.type]!, + text: LocaleKeys.editor_codeBlockShortForm.tr(), + icon: FlowySvgs.m_add_block_code_s, + onTap: (_, __) => _insertBlock(codeBlockNode()), + ), + TypeOptionMenuItemValue( + value: MathEquationBlockKeys.type, + backgroundColor: colorMap[MathEquationBlockKeys.type]!, + text: LocaleKeys.editor_mathEquationShortForm.tr(), + icon: FlowySvgs.m_add_block_formula_s, + onTap: (_, __) { + AppGlobals.rootNavKey.currentContext?.pop(true); + Future.delayed(const Duration(milliseconds: 100), () { + editorState.insertMathEquation(selection); + }); + }, + ), + ]; + } + + Map _colorMap(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + if (isDarkMode) { + return { + HeadingBlockKeys.type: const Color(0xFF5465A1), + ParagraphBlockKeys.type: const Color(0xFF5465A1), + TodoListBlockKeys.type: const Color(0xFF4BB299), + QuoteBlockKeys.type: const Color(0xFFBAAC74), + BulletedListBlockKeys.type: const Color(0xFFA35F94), + NumberedListBlockKeys.type: const Color(0xFFA35F94), + ToggleListBlockKeys.type: const Color(0xFFA35F94), + ImageBlockKeys.type: const Color(0xFFBAAC74), + MentionBlockKeys.type: const Color(0xFF40AAB8), + DividerBlockKeys.type: const Color(0xFF4BB299), + CalloutBlockKeys.type: const Color(0xFF66599B), + CodeBlockKeys.type: const Color(0xFF66599B), + MathEquationBlockKeys.type: const Color(0xFF66599B), + }; + } + return { + HeadingBlockKeys.type: const Color(0xFFBECCFF), + ParagraphBlockKeys.type: const Color(0xFFBECCFF), + TodoListBlockKeys.type: const Color(0xFF98F4CD), + QuoteBlockKeys.type: const Color(0xFFFDEDA7), + BulletedListBlockKeys.type: const Color(0xFFFFB9EF), + NumberedListBlockKeys.type: const Color(0xFFFFB9EF), + ToggleListBlockKeys.type: const Color(0xFFFFB9EF), + ImageBlockKeys.type: const Color(0xFFFDEDA7), + MentionBlockKeys.type: const Color(0xFF91EAF5), + DividerBlockKeys.type: const Color(0xFF98F4CD), + CalloutBlockKeys.type: const Color(0xFFCABDFF), + CodeBlockKeys.type: const Color(0xFFCABDFF), + MathEquationBlockKeys.type: const Color(0xFFCABDFF), + }; + } +} + +extension on EditorState { + Future insertBlockAfterCurrentSelection( + Selection selection, + Node node, + ) async { + final path = selection.end.path.next; + final transaction = this.transaction; + transaction.insertNode( + path, + node, + ); + transaction.afterSelection = Selection.collapsed( + Position(path: path), + ); + transaction.selectionExtraInfo = {}; + await apply(transaction); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart index 787ccfda9f..95cd855667 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'dart:math'; import 'package:appflowy/plugins/document/application/document_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart'; @@ -10,7 +9,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,7 +18,6 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; abstract class AppFlowyMobileToolbarWidgetService { void closeItemMenu(); - void closeKeyboard(); PropertyValueNotifier get showMenuNotifier; @@ -180,13 +177,7 @@ class _MobileToolbarState extends State<_MobileToolbar> // but in this case, we don't want to update the cached keyboard height. // This is because we want to keep the same height when the menu is shown. bool canUpdateCachedKeyboardHeight = true; - - /// when the [_MobileToolbar] disposed before the keyboard height can be updated in time, - /// there will be an issue with the height being 0 - /// this is used to globally record the height. - static double _globalCachedKeyboardHeight = 0.0; - ValueNotifier cachedKeyboardHeight = - ValueNotifier(_globalCachedKeyboardHeight); + ValueNotifier cachedKeyboardHeight = ValueNotifier(0.0); // used to check if click the same item again int? selectedMenuIndex; @@ -252,12 +243,12 @@ class _MobileToolbarState extends State<_MobileToolbar> children: [ const Divider( height: 0.5, - color: Color(0x7FEDEDED), + color: Color(0xFFEDEDED), ), _buildToolbar(context), const Divider( height: 0.5, - color: Color(0x7FEDEDED), + color: Color(0xFFEDEDED), ), _buildMenuOrSpacer(context), ], @@ -283,9 +274,7 @@ class _MobileToolbarState extends State<_MobileToolbar> if (!closeKeyboardInitiative && cachedKeyboardHeight.value != 0 && height == 0) { - if (!widget.editorState.isDisposed) { - widget.editorState.selection = null; - } + widget.editorState.selection = null; } // if the menu is shown and the height is not 0, we need to close the menu @@ -295,14 +284,6 @@ class _MobileToolbarState extends State<_MobileToolbar> if (canUpdateCachedKeyboardHeight) { cachedKeyboardHeight.value = height; - - if (defaultTargetPlatform == TargetPlatform.android) { - // cache the keyboard height with the view padding in Android - if (cachedKeyboardHeight.value != 0) { - cachedKeyboardHeight.value += - MediaQuery.of(context).viewPadding.bottom; - } - } } if (height == 0) { @@ -404,22 +385,13 @@ class _MobileToolbarState extends State<_MobileToolbar> return ValueListenableBuilder( valueListenable: showMenuNotifier, builder: (_, showingMenu, __) { - var keyboardHeight = height; - if (defaultTargetPlatform == TargetPlatform.android) { - if (!showingMenu) { - // take the max value of the keyboard height and the view padding - // to make sure the toolbar is above the keyboard - keyboardHeight = max( - keyboardHeight, - MediaQuery.of(context).viewInsets.bottom, - ); - } - } - if (keyboardHeight > 0) { - _globalCachedKeyboardHeight = keyboardHeight; + var paddingHeight = height; + if (Platform.isAndroid) { + paddingHeight = + height + MediaQuery.of(context).viewPadding.bottom; } return SizedBox( - height: keyboardHeight, + height: paddingHeight, child: (showingMenu && selectedMenuIndex != null) ? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call( context, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart index 12e7d1bef7..d138e644cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -107,9 +106,9 @@ class _AppFlowyMobileToolbarIconItemState final enable = widget.enable?.call() ?? true; return Padding( padding: const EdgeInsets.symmetric(vertical: 5), - child: AnimatedGestureDetector( - scaleFactor: 0.95, - onTapUp: () { + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { widget.onTap(); _rebuild(); }, @@ -135,7 +134,7 @@ class _AppFlowyMobileToolbarIconItemState } void _rebuild() { - if (!mounted) { + if (!context.mounted) { return; } setState(() { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/basic_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biusc_toolbar_item.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/basic_toolbar_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biusc_toolbar_item.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart index 35a3c37e74..adb1feeb35 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart @@ -12,7 +12,6 @@ final _defaultToolbarItems = [ aaToolbarItem, todoListToolbarItem, bulletedListToolbarItem, - addAttachmentItem, numberedListToolbarItem, boldToolbarItem, italicToolbarItem, @@ -36,7 +35,6 @@ final _listToolbarItems = [ underlineToolbarItem, strikethroughToolbarItem, colorToolbarItem, - addAttachmentItem, undoToolbarItem, redoToolbarItem, ]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart index 37444cd6e1..a016a0863f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class MobileToolbarMenuItemWrapper extends StatelessWidget { const MobileToolbarMenuItemWrapper({ @@ -238,7 +239,7 @@ extension MobileToolbarEditorState on EditorState { needToDeleteChildren = true; transaction.insertNodes( selection.end.path.next, - node.children.map((e) => e.deepCopy()), + node.children.map((e) => e.copyWith()), ); await apply(transaction); } @@ -338,21 +339,4 @@ extension MobileToolbarEditorState on EditorState { await apply(transaction); } - - Future insertBlockAfterCurrentSelection( - Selection selection, - Node node, - ) async { - final path = selection.end.path.next; - final transaction = this.transaction; - transaction.insertNode( - path, - node, - ); - transaction.afterSelection = Selection.collapsed( - Position(path: path), - ); - transaction.selectionExtraInfo = {}; - await apply(transaction); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart index f77083d21d..8e63873641 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/numbered_list/numbered_list_icon.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,51 +8,36 @@ class NumberedListIcon extends StatelessWidget { super.key, required this.node, required this.textDirection, - this.textStyle, }); final Node node; final TextDirection textDirection; - final TextStyle? textStyle; @override Widget build(BuildContext context) { - final textStyleConfiguration = - context.read().editorStyle.textStyleConfiguration; - final height = - textStyleConfiguration.text.height ?? textStyleConfiguration.lineHeight; - final combinedTextStyle = textStyle?.combine(textStyleConfiguration.text) ?? - textStyleConfiguration.text; - final adjustedTextStyle = combinedTextStyle.copyWith( - height: height, - fontFeatures: [const FontFeature.tabularFigures()], - ); - - return Padding( - padding: const EdgeInsets.only(left: 6.0, right: 10.0), - child: Text( - node.buildLevelString(context), - style: adjustedTextStyle, - strutStyle: StrutStyle.fromTextStyle(combinedTextStyle), - textHeightBehavior: TextHeightBehavior( - applyHeightToFirstAscent: - textStyleConfiguration.applyHeightToFirstAscent, - applyHeightToLastDescent: - textStyleConfiguration.applyHeightToLastDescent, - leadingDistribution: textStyleConfiguration.leadingDistribution, + final textStyle = + context.read().editorStyle.textStyleConfiguration.text; + return Container( + constraints: const BoxConstraints( + minWidth: 22, + minHeight: 22, + ), + margin: const EdgeInsets.only(right: 8.0), + alignment: Alignment.center, + child: Center( + child: Text( + node.levelString, + style: textStyle, + textDirection: textDirection, ), - textDirection: textDirection, ), ); } } -extension NumberedListNodeIndex on Node { - String buildLevelString(BuildContext context) { - final builder = NumberedListIndexBuilder( - editorState: context.read(), - node: this, - ); +extension on Node { + String get levelString { + final builder = _NumberedListIconBuilder(node: this); final indexInRootLevel = builder.indexInRootLevel; final indexInSameLevel = builder.indexInSameLevel; final level = indexInRootLevel % 3; @@ -66,13 +50,11 @@ extension NumberedListNodeIndex on Node { } } -class NumberedListIndexBuilder { - NumberedListIndexBuilder({ - required this.editorState, +class _NumberedListIconBuilder { + _NumberedListIconBuilder({ required this.node, }); - final EditorState editorState; final Node node; // the level of the current node @@ -94,13 +76,7 @@ class NumberedListIndexBuilder { Node? previous = node.previous; // if the previous one is not a numbered list, then it is the first one - final aiNodeExternalValues = - node.externalValues?.unwrapOrNull(); - - if (previous == null || - previous.type != NumberedListBlockKeys.type || - (aiNodeExternalValues != null && - aiNodeExternalValues.isFirstNumberedListNode)) { + if (previous == null || previous.type != NumberedListBlockKeys.type) { return node.attributes[NumberedListBlockKeys.number] ?? level; } @@ -109,17 +85,10 @@ class NumberedListIndexBuilder { startNumber = previous.attributes[NumberedListBlockKeys.number] as int?; level++; previous = previous.previous; - - // break the loop if the start number is found when the current node is an AI node - if (aiNodeExternalValues != null && startNumber != null) { - return startNumber + level - 1; - } } - if (startNumber != null) { - level = startNumber + level - 1; + return startNumber + level - 1; } - return level; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart new file mode 100644 index 0000000000..d682a82f08 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/error.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'error.freezed.dart'; +part 'error.g.dart'; + +@freezed +class OpenAIError with _$OpenAIError { + const factory OpenAIError({ + String? code, + required String message, + }) = _OpenAIError; + + factory OpenAIError.fromJson(Map json) => + _$OpenAIErrorFromJson(json); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart new file mode 100644 index 0000000000..eb688b2c56 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart @@ -0,0 +1,283 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:http/http.dart' as http; + +import 'error.dart'; +import 'text_completion.dart'; + +enum OpenAIRequestType { + textCompletion, + textEdit, + imageGenerations; + + Uri get uri { + switch (this) { + case OpenAIRequestType.textCompletion: + return Uri.parse('https://api.openai.com/v1/completions'); + case OpenAIRequestType.textEdit: + return Uri.parse('https://api.openai.com/v1/chat/completions'); + case OpenAIRequestType.imageGenerations: + return Uri.parse('https://api.openai.com/v1/images/generations'); + } + } +} + +abstract class OpenAIRepository { + /// Get completions from GPT-3 + /// + /// [prompt] is the prompt text + /// [suffix] is the suffix text + /// [maxTokens] is the maximum number of tokens to generate + /// [temperature] is the temperature of the model + /// + Future> getCompletions({ + required String prompt, + String? suffix, + int maxTokens = 2048, + double temperature = .3, + }); + + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required Future Function() onEnd, + required void Function(OpenAIError error) onError, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + bool useAction = false, + }); + + /// Get edits from GPT-3 + /// + /// [input] is the input text + /// [instruction] is the instruction text + /// [temperature] is the temperature of the model + /// + Future> getEdits({ + required String input, + required String instruction, + double temperature = 0.3, + }); + + /// Generate image from GPT-3 + /// + /// [prompt] is the prompt text + /// [n] is the number of images to generate + /// + /// the result is a list of urls + Future, OpenAIError>> generateImage({ + required String prompt, + int n = 1, + }); +} + +class HttpOpenAIRepository implements OpenAIRepository { + const HttpOpenAIRepository({ + required this.client, + required this.apiKey, + }); + + final http.Client client; + final String apiKey; + + Map get headers => { + 'Authorization': 'Bearer $apiKey', + 'Content-Type': 'application/json', + }; + + @override + Future> getCompletions({ + required String prompt, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + }) async { + final parameters = { + 'model': 'gpt-3.5-turbo-instruct', + 'prompt': prompt, + 'suffix': suffix, + 'max_tokens': maxTokens, + 'temperature': temperature, + 'stream': false, + }; + + final response = await client.post( + OpenAIRequestType.textCompletion.uri, + headers: headers, + body: json.encode(parameters), + ); + + if (response.statusCode == 200) { + return FlowyResult.success( + TextCompletionResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); + } else { + return FlowyResult.failure( + OpenAIError.fromJson(json.decode(response.body)['error']), + ); + } + } + + @override + Future getStreamedCompletions({ + required String prompt, + required Future Function() onStart, + required Future Function(TextCompletionResponse response) onProcess, + required Future Function() onEnd, + required void Function(OpenAIError error) onError, + String? suffix, + int maxTokens = 2048, + double temperature = 0.3, + bool useAction = false, + }) async { + final parameters = { + 'model': 'gpt-3.5-turbo-instruct', + 'prompt': prompt, + 'suffix': suffix, + 'max_tokens': maxTokens, + 'temperature': temperature, + 'stream': true, + }; + + final request = http.Request('POST', OpenAIRequestType.textCompletion.uri); + request.headers.addAll(headers); + request.body = jsonEncode(parameters); + + final response = await client.send(request); + + // NEED TO REFACTOR. + // WHY OPENAI USE TWO LINES TO INDICATE THE START OF THE STREAMING RESPONSE? + // AND WHY OPENAI USE [DONE] TO INDICATE THE END OF THE STREAMING RESPONSE? + int syntax = 0; + var previousSyntax = ''; + if (response.statusCode == 200) { + await for (final chunk in response.stream + .transform(const Utf8Decoder()) + .transform(const LineSplitter())) { + syntax += 1; + if (!useAction) { + if (syntax == 3) { + await onStart(); + continue; + } else if (syntax < 3) { + continue; + } + } else { + if (syntax == 2) { + await onStart(); + continue; + } else if (syntax < 2) { + continue; + } + } + final data = chunk.trim().split('data: '); + if (data.length > 1) { + if (data[1] != '[DONE]') { + final response = TextCompletionResponse.fromJson( + json.decode(data[1]), + ); + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + if (text == previousSyntax && text == '\n') { + continue; + } + await onProcess(response); + previousSyntax = response.choices.first.text; + } + } else { + await onEnd(); + } + } + } + } else { + final body = await response.stream.bytesToString(); + onError( + OpenAIError.fromJson(json.decode(body)['error']), + ); + } + return; + } + + @override + Future> getEdits({ + required String input, + required String instruction, + double temperature = 0.3, + int n = 1, + }) async { + final parameters = { + 'model': 'gpt-4', + 'input': input, + 'instruction': instruction, + 'temperature': temperature, + 'n': n, + }; + + final response = await client.post( + OpenAIRequestType.textEdit.uri, + headers: headers, + body: json.encode(parameters), + ); + + if (response.statusCode == 200) { + return FlowyResult.success( + TextEditResponse.fromJson( + json.decode( + utf8.decode(response.bodyBytes), + ), + ), + ); + } else { + return FlowyResult.failure( + OpenAIError.fromJson(json.decode(response.body)['error']), + ); + } + } + + @override + Future, OpenAIError>> generateImage({ + required String prompt, + int n = 1, + }) async { + final parameters = { + 'prompt': prompt, + 'n': n, + 'size': '512x512', + }; + + try { + final response = await client.post( + OpenAIRequestType.imageGenerations.uri, + headers: headers, + body: json.encode(parameters), + ); + + if (response.statusCode == 200) { + final data = json.decode( + utf8.decode(response.bodyBytes), + )['data'] as List; + final urls = data + .map((e) => e.values) + .expand((e) => e) + .map((e) => e.toString()) + .toList(); + return FlowyResult.success(urls); + } else { + return FlowyResult.failure( + OpenAIError.fromJson(json.decode(response.body)['error']), + ); + } + } catch (error) { + return FlowyResult.failure(OpenAIError(message: error.toString())); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart new file mode 100644 index 0000000000..067049adbf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'text_completion.freezed.dart'; +part 'text_completion.g.dart'; + +@freezed +class TextCompletionChoice with _$TextCompletionChoice { + factory TextCompletionChoice({ + required String text, + required int index, + // ignore: invalid_annotation_target + @JsonKey(name: 'finish_reason') String? finishReason, + }) = _TextCompletionChoice; + + factory TextCompletionChoice.fromJson(Map json) => + _$TextCompletionChoiceFromJson(json); +} + +@freezed +class TextCompletionResponse with _$TextCompletionResponse { + const factory TextCompletionResponse({ + required List choices, + }) = _TextCompletionResponse; + + factory TextCompletionResponse.fromJson(Map json) => + _$TextCompletionResponseFromJson(json); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart new file mode 100644 index 0000000000..52cce9da4f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart @@ -0,0 +1,24 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'text_edit.freezed.dart'; +part 'text_edit.g.dart'; + +@freezed +class TextEditChoice with _$TextEditChoice { + factory TextEditChoice({ + required String text, + required int index, + }) = _TextEditChoice; + + factory TextEditChoice.fromJson(Map json) => + _$TextEditChoiceFromJson(json); +} + +@freezed +class TextEditResponse with _$TextEditResponse { + const factory TextEditResponse({ + required List choices, + }) = _TextEditResponse; + + factory TextEditResponse.fromJson(Map json) => + _$TextEditResponseFromJson(json); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart new file mode 100644 index 0000000000..17e89b1bca --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart @@ -0,0 +1,8 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; + +const String learnMoreUrl = + 'https://docs.appflowy.io/docs/appflowy/product/appflowy-x-openai'; + +Future openLearnMorePage() async { + await afLaunchUrlString(learnMoreUrl); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart new file mode 100644 index 0000000000..4d4ed15a1f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/auto_completion_node_widget.dart @@ -0,0 +1,557 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; + +class AutoCompletionBlockKeys { + const AutoCompletionBlockKeys._(); + + static const String type = 'auto_completion'; + static const String prompt = 'prompt'; + static const String startSelection = 'start_selection'; + static const String generationCount = 'generation_count'; +} + +Node autoCompletionNode({ + String prompt = '', + required Selection start, +}) { + return Node( + type: AutoCompletionBlockKeys.type, + attributes: { + AutoCompletionBlockKeys.prompt: prompt, + AutoCompletionBlockKeys.startSelection: start.toJson(), + AutoCompletionBlockKeys.generationCount: 0, + }, + ); +} + +SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node( + getName: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr, + iconData: Icons.generating_tokens, + keywords: ['ai', 'openai' 'writer', 'autogenerator'], + nodeBuilder: (editorState, _) { + final node = autoCompletionNode(start: editorState.selection!); + return node; + }, + replace: (_, node) => false, +); + +class AutoCompletionBlockComponentBuilder extends BlockComponentBuilder { + AutoCompletionBlockComponentBuilder(); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return AutoCompletionBlockComponent( + key: node.key, + node: node, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + bool validate(Node node) { + return node.children.isEmpty && + node.attributes[AutoCompletionBlockKeys.prompt] is String && + node.attributes[AutoCompletionBlockKeys.startSelection] is Map; + } +} + +class AutoCompletionBlockComponent extends BlockComponentStatefulWidget { + const AutoCompletionBlockComponent({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => + _AutoCompletionBlockComponentState(); +} + +class _AutoCompletionBlockComponentState + extends State { + final controller = TextEditingController(); + final textFieldFocusNode = FocusNode(); + + late final editorState = context.read(); + late final SelectionGestureInterceptor interceptor; + + String get prompt => widget.node.attributes[AutoCompletionBlockKeys.prompt]; + int get generationCount => + widget.node.attributes[AutoCompletionBlockKeys.generationCount] ?? 0; + Selection? get startSelection { + final selection = + widget.node.attributes[AutoCompletionBlockKeys.startSelection]; + if (selection != null) { + return Selection.fromJson(selection); + } + return null; + } + + @override + void initState() { + super.initState(); + + _subscribeSelectionGesture(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + editorState.selection = null; + textFieldFocusNode.requestFocus(); + }); + } + + @override + void dispose() { + _onExit(); + _unsubscribeSelectionGesture(); + controller.dispose(); + textFieldFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 5, + color: Theme.of(context).colorScheme.surface, + child: Container( + margin: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const AutoCompletionHeader(), + const Space(0, 10), + if (prompt.isEmpty && generationCount < 1) ...[ + _buildInputWidget(context), + const Space(0, 10), + AutoCompletionInputFooter( + onGenerate: _onGenerate, + onExit: _onExit, + ), + ] else ...[ + AutoCompletionFooter( + onKeep: _onExit, + onRewrite: _onRewrite, + onDiscard: _onDiscard, + ), + ], + ], + ), + ), + ); + } + + Widget _buildInputWidget(BuildContext context) { + return FlowyTextField( + hintText: LocaleKeys.document_plugins_autoGeneratorHintText.tr(), + controller: controller, + maxLines: 5, + focusNode: textFieldFocusNode, + autoFocus: false, + hintTextConstraints: const BoxConstraints(), + ); + } + + Future _onExit() async { + final transaction = editorState.transaction..deleteNode(widget.node); + await editorState.apply( + transaction, + options: const ApplyOptions( + recordUndo: false, + ), + ); + } + + Future _onGenerate() async { + final loading = Loading(context); + await loading.start(); + + await _updateEditingText(); + + final userProfile = await UserBackendService.getCurrentUserProfile() + .then((value) => value.toNullable()); + if (userProfile == null) { + await loading.stop(); + if (mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(), + showCancel: true, + ); + } + return; + } + + final textRobot = TextRobot(editorState: editorState); + BarrierDialog? barrierDialog; + final openAIRepository = HttpOpenAIRepository( + client: http.Client(), + apiKey: userProfile.openaiKey, + ); + await openAIRepository.getStreamedCompletions( + prompt: controller.text, + onStart: () async { + await loading.stop(); + if (mounted) { + barrierDialog = BarrierDialog(context); + await barrierDialog?.show(); + await _makeSurePreviousNodeIsEmptyParagraphNode(); + } + }, + onProcess: (response) async { + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + await textRobot.autoInsertText( + text, + delay: Duration.zero, + ); + } + }, + onEnd: () async { + await barrierDialog?.dismiss(); + }, + onError: (error) async { + await loading.stop(); + if (mounted) { + showSnackBarMessage( + context, + error.message, + showCancel: true, + ); + } + }, + ); + await _updateGenerationCount(); + } + + Future _onDiscard() async { + final selection = startSelection; + if (selection != null) { + final start = selection.start.path; + final end = widget.node.previous?.path; + if (end != null) { + final transaction = editorState.transaction; + transaction.deleteNodesAtPath( + start, + end.last - start.last + 1, + ); + await editorState.apply(transaction); + await _makeSurePreviousNodeIsEmptyParagraphNode(); + } + } + return _onExit(); + } + + Future _onRewrite() async { + final previousOutput = _getPreviousOutput(); + if (previousOutput == null) { + return; + } + + final loading = Loading(context); + await loading.start(); + // clear previous response + final selection = startSelection; + if (selection != null) { + final start = selection.start.path; + final end = widget.node.previous?.path; + if (end != null) { + final transaction = editorState.transaction; + transaction.deleteNodesAtPath( + start, + end.last - start.last + 1, + ); + await editorState.apply(transaction); + } + } + // generate new response + final userProfile = await UserBackendService.getCurrentUserProfile() + .then((value) => value.toNullable()); + if (userProfile == null) { + await loading.stop(); + if (mounted) { + showSnackBarMessage( + context, + LocaleKeys.document_plugins_autoGeneratorCantGetOpenAIKey.tr(), + showCancel: true, + ); + } + return; + } + final textRobot = TextRobot(editorState: editorState); + final openAIRepository = HttpOpenAIRepository( + client: http.Client(), + apiKey: userProfile.openaiKey, + ); + await openAIRepository.getStreamedCompletions( + prompt: _rewritePrompt(previousOutput), + onStart: () async { + await loading.stop(); + await _makeSurePreviousNodeIsEmptyParagraphNode(); + }, + onProcess: (response) async { + if (response.choices.isNotEmpty) { + final text = response.choices.first.text; + await textRobot.autoInsertText( + text, + delay: Duration.zero, + ); + } + }, + onEnd: () async {}, + onError: (error) async { + await loading.stop(); + if (mounted) { + showSnackBarMessage( + context, + error.message, + showCancel: true, + ); + } + }, + ); + await _updateGenerationCount(); + } + + String? _getPreviousOutput() { + final startSelection = this.startSelection; + if (startSelection != null) { + final end = widget.node.previous?.path; + + if (end != null) { + final result = editorState + .getNodesInSelection( + startSelection.copyWith(end: Position(path: end)), + ) + .fold( + '', + (previousValue, element) { + final delta = element.delta; + if (delta != null) { + return "$previousValue\n${delta.toPlainText()}"; + } else { + return previousValue; + } + }, + ); + return result.trim(); + } + } + return null; + } + + Future _updateEditingText() async { + final transaction = editorState.transaction; + transaction.updateNode( + widget.node, + { + AutoCompletionBlockKeys.prompt: controller.text, + }, + ); + await editorState.apply(transaction); + } + + Future _updateGenerationCount() async { + final transaction = editorState.transaction; + transaction.updateNode( + widget.node, + { + AutoCompletionBlockKeys.generationCount: generationCount + 1, + }, + ); + await editorState.apply(transaction); + } + + String _rewritePrompt(String previousOutput) { + return 'I am not satisfied with your previous response ($previousOutput) to the query ($prompt). Please provide an alternative response.'; + } + + Future _makeSurePreviousNodeIsEmptyParagraphNode() async { + // make sure the previous node is a empty paragraph node without any styles. + final transaction = editorState.transaction; + final previous = widget.node.previous; + final Selection selection; + if (previous == null || + previous.type != ParagraphBlockKeys.type || + (previous.delta?.toPlainText().isNotEmpty ?? false)) { + selection = Selection.single( + path: widget.node.path, + startOffset: 0, + ); + transaction.insertNode( + widget.node.path, + paragraphNode(), + ); + } else { + selection = Selection.single( + path: previous.path, + startOffset: 0, + ); + } + transaction.updateNode(widget.node, { + AutoCompletionBlockKeys.startSelection: selection.toJson(), + }); + transaction.afterSelection = selection; + await editorState.apply(transaction); + } + + void _subscribeSelectionGesture() { + interceptor = SelectionGestureInterceptor( + key: AutoCompletionBlockKeys.type, + canTap: (details) { + if (!context.isOffsetInside(details.globalPosition)) { + if (prompt.isNotEmpty || controller.text.isNotEmpty) { + // show dialog + showDialog( + context: context, + builder: (context) { + return DiscardDialog( + onConfirm: () => _onDiscard(), + onCancel: () {}, + ); + }, + ); + } else if (controller.text.isEmpty) { + _onExit(); + } + } + editorState.service.keyboardService?.disable(); + return false; + }, + ); + editorState.service.selectionService.registerGestureInterceptor( + interceptor, + ); + } + + void _unsubscribeSelectionGesture() { + editorState.service.selectionService.unregisterGestureInterceptor( + AutoCompletionBlockKeys.type, + ); + } +} + +class AutoCompletionHeader extends StatelessWidget { + const AutoCompletionHeader({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + FlowyText.medium( + LocaleKeys.document_plugins_autoGeneratorTitleName.tr(), + fontSize: 14, + ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.regular( + LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), + ), + onTap: () async { + await openLearnMorePage(); + }, + ), + ], + ); + } +} + +class AutoCompletionInputFooter extends StatelessWidget { + const AutoCompletionInputFooter({ + super.key, + required this.onGenerate, + required this.onExit, + }); + + final VoidCallback onGenerate; + final VoidCallback onExit; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + PrimaryTextButton( + LocaleKeys.button_generate.tr(), + onPressed: onGenerate, + ), + const Space(10, 0), + SecondaryTextButton( + LocaleKeys.button_cancel.tr(), + onPressed: onExit, + ), + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: FlowyText.regular( + LocaleKeys.document_plugins_warning.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } +} + +class AutoCompletionFooter extends StatelessWidget { + const AutoCompletionFooter({ + super.key, + required this.onKeep, + required this.onRewrite, + required this.onDiscard, + }); + + final VoidCallback onKeep; + final VoidCallback onRewrite; + final VoidCallback onDiscard; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + PrimaryTextButton( + LocaleKeys.button_keep.tr(), + onPressed: onKeep, + ), + const Space(10, 0), + SecondaryTextButton( + LocaleKeys.document_plugins_autoGeneratorRewrite.tr(), + onPressed: onRewrite, + ), + const Space(10, 0), + SecondaryTextButton( + LocaleKeys.button_discard.tr(), + onPressed: onDiscard, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart new file mode 100644 index 0000000000..4c0c2bc91a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart @@ -0,0 +1,28 @@ +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; + +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; + +import 'package:easy_localization/easy_localization.dart'; + +class DiscardDialog extends StatelessWidget { + const DiscardDialog({ + super.key, + required this.onConfirm, + required this.onCancel, + }); + + final VoidCallback onConfirm; + final VoidCallback onCancel; + + @override + Widget build(BuildContext context) { + return NavigatorOkCancelDialog( + message: LocaleKeys.document_plugins_discardResponse.tr(), + okTitle: LocaleKeys.button_discard.tr(), + cancelTitle: LocaleKeys.button_cancel.tr(), + onOkPressed: onConfirm, + onCancelPressed: onCancel, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart new file mode 100644 index 0000000000..076a7877cd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class Loading { + Loading(this.context); + + BuildContext? loadingContext; + final BuildContext context; + + Future start() async => showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + loadingContext = context; + return const SimpleDialog( + elevation: 0.0, + backgroundColor: + Colors.transparent, // can change this to your preferred color + children: [ + Center( + child: CircularProgressIndicator(), + ), + ], + ); + }, + ); + + Future stop() async { + if (loadingContext != null) { + Navigator.of(loadingContext!).pop(); + loadingContext = null; + } + } +} + +class BarrierDialog { + BarrierDialog(this.context); + + late BuildContext loadingContext; + final BuildContext context; + + Future show() async => showDialog( + context: context, + barrierDismissible: false, + barrierColor: Colors.transparent, + builder: (BuildContext context) { + loadingContext = context; + return const SizedBox.shrink(); + }, + ); + + Future dismiss() async => Navigator.of(loadingContext).pop(); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart new file mode 100644 index 0000000000..70affa002a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart @@ -0,0 +1,77 @@ +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; + +enum SmartEditAction { + summarize, + fixSpelling, + improveWriting, + makeItLonger; + + String get toInstruction { + switch (this) { + case SmartEditAction.summarize: + return 'Tl;dr'; + case SmartEditAction.fixSpelling: + return 'Correct this to standard English:'; + case SmartEditAction.improveWriting: + return 'Rewrite this in your own words:'; + case SmartEditAction.makeItLonger: + return 'Make this text longer:'; + } + } + + String prompt(String input) { + switch (this) { + case SmartEditAction.summarize: + return '$input\n\nTl;dr'; + case SmartEditAction.fixSpelling: + return 'Correct this to standard English:\n\n$input'; + case SmartEditAction.improveWriting: + return 'Rewrite this:\n\n$input'; + case SmartEditAction.makeItLonger: + return 'Make this text longer:\n\n$input'; + } + } + + static SmartEditAction from(int index) { + switch (index) { + case 0: + return SmartEditAction.summarize; + case 1: + return SmartEditAction.fixSpelling; + case 2: + return SmartEditAction.improveWriting; + case 3: + return SmartEditAction.makeItLonger; + } + return SmartEditAction.fixSpelling; + } + + String get name { + switch (this) { + case SmartEditAction.summarize: + return LocaleKeys.document_plugins_smartEditSummarize.tr(); + case SmartEditAction.fixSpelling: + return LocaleKeys.document_plugins_smartEditFixSpelling.tr(); + case SmartEditAction.improveWriting: + return LocaleKeys.document_plugins_smartEditImproveWriting.tr(); + case SmartEditAction.makeItLonger: + return LocaleKeys.document_plugins_smartEditMakeLonger.tr(); + } + } +} + +class SmartEditActionWrapper extends ActionCell { + SmartEditActionWrapper(this.inner); + + final SmartEditAction inner; + + Widget? icon(Color iconColor) => null; + + @override + String get name { + return inner.name; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart new file mode 100644 index 0000000000..f023d56abe --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_node_widget.dart @@ -0,0 +1,467 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; + +class SmartEditBlockKeys { + const SmartEditBlockKeys._(); + + static const type = 'smart_edit'; + + /// The instruction of the smart edit. + /// + /// It is a [SmartEditAction] value. + static const action = 'action'; + + /// The input of the smart edit. + static const content = 'content'; +} + +Node smartEditNode({ + required SmartEditAction action, + required String content, +}) { + return Node( + type: SmartEditBlockKeys.type, + attributes: { + SmartEditBlockKeys.action: action.index, + SmartEditBlockKeys.content: content, + }, + ); +} + +class SmartEditBlockComponentBuilder extends BlockComponentBuilder { + SmartEditBlockComponentBuilder(); + + @override + BlockComponentWidget build(BlockComponentContext blockComponentContext) { + final node = blockComponentContext.node; + return SmartEditBlockComponentWidget( + key: node.key, + node: node, + showActions: showActions(node), + actionBuilder: (context, state) => actionBuilder( + blockComponentContext, + state, + ), + ); + } + + @override + bool validate(Node node) => + node.attributes[SmartEditBlockKeys.action] is int && + node.attributes[SmartEditBlockKeys.content] is String; +} + +class SmartEditBlockComponentWidget extends BlockComponentStatefulWidget { + const SmartEditBlockComponentWidget({ + super.key, + required super.node, + super.showActions, + super.actionBuilder, + super.configuration = const BlockComponentConfiguration(), + }); + + @override + State createState() => + _SmartEditBlockComponentWidgetState(); +} + +class _SmartEditBlockComponentWidgetState + extends State { + final popoverController = PopoverController(); + final key = GlobalKey(debugLabel: 'smart_edit_input'); + + late final editorState = context.read(); + + @override + void initState() { + super.initState(); + + // todo: don't use a popover to show the content of the smart edit. + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + popoverController.show(); + }); + } + + @override + void reassemble() { + super.reassemble(); + + final transaction = editorState.transaction..deleteNode(widget.node); + editorState.apply(transaction); + } + + @override + Widget build(BuildContext context) { + final width = _getEditorWidth(); + + return AppFlowyPopover( + controller: popoverController, + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + margin: EdgeInsets.zero, + constraints: BoxConstraints(maxWidth: width), + decoration: FlowyDecoration.decoration( + Colors.transparent, + Colors.transparent, + ), + child: const SizedBox( + width: double.infinity, + ), + canClose: () async { + final completer = Completer(); + final state = key.currentState as _SmartEditInputWidgetState; + if (state.result.isEmpty) { + completer.complete(true); + } else { + await showDialog( + context: context, + builder: (context) { + return DiscardDialog( + onConfirm: () => completer.complete(true), + onCancel: () => completer.complete(false), + ); + }, + ); + } + return completer.future; + }, + onClose: () { + final transaction = editorState.transaction..deleteNode(widget.node); + editorState.apply(transaction); + }, + popupBuilder: (BuildContext popoverContext) { + return SmartEditInputWidget( + key: key, + node: widget.node, + editorState: editorState, + ); + }, + ); + } + + double _getEditorWidth() { + var width = double.infinity; + final editorSize = editorState.renderBox?.size; + final padding = editorState.editorStyle.padding; + if (editorSize != null) { + width = editorSize.width - padding.left - padding.right; + } + return width; + } +} + +class SmartEditInputWidget extends StatefulWidget { + const SmartEditInputWidget({ + required super.key, + required this.node, + required this.editorState, + }); + + final Node node; + final EditorState editorState; + + @override + State createState() => _SmartEditInputWidgetState(); +} + +class _SmartEditInputWidgetState extends State { + final focusNode = FocusNode(); + final client = http.Client(); + + SmartEditAction get action => SmartEditAction.from( + widget.node.attributes[SmartEditBlockKeys.action], + ); + String get content => widget.node.attributes[SmartEditBlockKeys.content]; + EditorState get editorState => widget.editorState; + + bool loading = true; + String result = ''; + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + editorState.service.keyboardService?.disable(); + // editorState.selection = null; + }); + + focusNode.requestFocus(); + _requestCompletions(); + } + + @override + void dispose() { + client.close(); + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Card( + elevation: 5, + color: Theme.of(context).colorScheme.surface, + child: Container( + margin: const EdgeInsets.all(10), + child: _buildSmartEditPanel(context), + ), + ); + } + + Widget _buildSmartEditPanel(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeaderWidget(context), + const Space(0, 10), + _buildResultWidget(context), + const Space(0, 10), + _buildInputFooterWidget(context), + ], + ); + } + + Widget _buildHeaderWidget(BuildContext context) { + return Row( + children: [ + FlowyText.medium( + '${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}', + fontSize: 14, + ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: FlowyText.regular( + LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(), + ), + onTap: () async { + await openLearnMorePage(); + }, + ), + ], + ); + } + + Widget _buildResultWidget(BuildContext context) { + final loadingWidget = Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: SizedBox.fromSize( + size: const Size.square(14), + child: const CircularProgressIndicator(), + ), + ); + if (result.isEmpty || loading) { + return loadingWidget; + } + return Flexible( + child: Text( + result, + ), + ); + } + + Widget _buildInputFooterWidget(BuildContext context) { + return Row( + children: [ + FlowyRichTextButton( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + onPressed: () => _requestCompletions(rewrite: true), + ), + const Space(10, 0), + FlowyRichTextButton( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.button_replace.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + onPressed: () async { + await _onReplace(); + await _onExit(); + }, + ), + const Space(10, 0), + FlowyRichTextButton( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.button_insertBelow.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + onPressed: () async { + await _onInsertBelow(); + await _onExit(); + }, + ), + const Space(10, 0), + FlowyRichTextButton( + TextSpan( + children: [ + TextSpan( + text: LocaleKeys.button_cancel.tr(), + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + onPressed: () async => _onExit(), + ), + const Spacer(), + Expanded( + child: Container( + alignment: Alignment.centerRight, + child: FlowyText.regular( + LocaleKeys.document_plugins_warning.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ); + } + + Future _onReplace() async { + final selection = editorState.selection?.normalized; + if (selection == null) { + return; + } + final nodes = editorState.getNodesInSelection(selection); + if (nodes.isEmpty || !nodes.every((element) => element.delta != null)) { + return; + } + final replaceTexts = result.split('\n') + ..removeWhere((element) => element.isEmpty); + final transaction = editorState.transaction; + transaction.replaceTexts( + nodes, + selection, + replaceTexts, + ); + await editorState.apply(transaction); + + int endOffset = replaceTexts.last.length; + if (replaceTexts.length == 1) { + endOffset += selection.start.offset; + } + + editorState.selection = Selection( + start: selection.start, + end: Position( + path: [selection.start.path.first + replaceTexts.length - 1], + offset: endOffset, + ), + ); + } + + Future _onInsertBelow() async { + final selection = editorState.selection?.normalized; + if (selection == null) { + return; + } + final insertedText = result.split('\n') + ..removeWhere((element) => element.isEmpty); + final transaction = editorState.transaction; + transaction.insertNodes( + selection.end.path.next, + insertedText.map( + (e) => paragraphNode( + text: e, + ), + ), + ); + transaction.afterSelection = Selection( + start: Position(path: selection.end.path.next), + end: Position( + path: [selection.end.path.next.first + insertedText.length], + ), + ); + await editorState.apply(transaction); + } + + Future _onExit() async { + final transaction = editorState.transaction..deleteNode(widget.node); + return editorState.apply( + transaction, + options: const ApplyOptions( + recordUndo: false, + ), + ); + } + + Future _requestCompletions({bool rewrite = false}) async { + if (rewrite) { + setState(() { + loading = true; + result = ""; + }); + } + final openAIRepository = await getIt.getAsync(); + + var lines = content.split('\n\n'); + if (action == SmartEditAction.summarize) { + lines = [lines.join('\n')]; + } + for (var i = 0; i < lines.length; i++) { + final element = lines[i]; + await openAIRepository.getStreamedCompletions( + useAction: true, + prompt: action.prompt(element), + onStart: () async { + setState(() { + loading = false; + }); + }, + onProcess: (response) async { + setState(() { + if (response.choices.first.text != '\n') { + result += response.choices.first.text; + } + }); + }, + onEnd: () async { + setState(() { + if (i != lines.length - 1) { + result += '\n'; + } + }); + }, + onError: (error) async { + showSnackBarMessage( + context, + error.message, + showCancel: true, + ); + await _onExit(); + }, + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart new file mode 100644 index 0000000000..a0f7002889 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_toolbar_item.dart @@ -0,0 +1,119 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +final ToolbarItem smartEditItem = ToolbarItem( + id: 'appflowy.editor.smart_edit', + group: 0, + isActive: onlyShowInSingleSelectionAndTextType, + builder: (context, editorState, _, __) => SmartEditActionList( + editorState: editorState, + ), +); + +class SmartEditActionList extends StatefulWidget { + const SmartEditActionList({ + super.key, + required this.editorState, + }); + + final EditorState editorState; + + @override + State createState() => _SmartEditActionListState(); +} + +class _SmartEditActionListState extends State { + bool isOpenAIEnabled = false; + + @override + void initState() { + super.initState(); + + UserBackendService.getCurrentUserProfile().then((value) { + setState(() { + isOpenAIEnabled = value.fold( + (s) => s.openaiKey.isNotEmpty, + (_) => false, + ); + }); + }); + } + + @override + Widget build(BuildContext context) { + return PopoverActionList( + direction: PopoverDirection.bottomWithLeftAligned, + actions: SmartEditAction.values + .map((action) => SmartEditActionWrapper(action)) + .toList(), + onClosed: () => keepEditorFocusNotifier.decrease(), + buildChild: (controller) { + keepEditorFocusNotifier.increase(); + return FlowyIconButton( + hoverColor: Colors.transparent, + tooltipText: isOpenAIEnabled + ? LocaleKeys.document_plugins_smartEdit.tr() + : LocaleKeys.document_plugins_smartEditDisabled.tr(), + preferBelow: false, + icon: const Icon( + Icons.lightbulb_outline, + size: 15, + color: Colors.white, + ), + onPressed: () { + if (isOpenAIEnabled) { + controller.show(); + } else { + showSnackBarMessage( + context, + LocaleKeys.document_plugins_smartEditDisabled.tr(), + showCancel: true, + ); + } + }, + ); + }, + onSelected: (action, controller) { + controller.close(); + _insertSmartEditNode(action); + }, + ); + } + + Future _insertSmartEditNode( + SmartEditActionWrapper actionWrapper, + ) async { + final selection = widget.editorState.selection?.normalized; + if (selection == null) { + return; + } + final input = widget.editorState.getTextInSelection(selection); + while (input.last.isEmpty) { + input.removeLast(); + } + final transaction = widget.editorState.transaction; + transaction.insertNode( + selection.normalized.end.path.next, + smartEditNode( + action: actionWrapper.inner, + content: input.join('\n\n'), + ), + ); + await widget.editorState.apply( + transaction, + options: const ApplyOptions( + recordUndo: false, + ), + withUpdateSelection: false, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart index e3120356d9..c564a61e27 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/outline/outline_block_component.dart @@ -2,13 +2,12 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; class OutlineBlockKeys { const OutlineBlockKeys._(); @@ -18,6 +17,15 @@ class OutlineBlockKeys { static const String depth = 'depth'; } +// defining the callout block menu item for selection +SelectionMenuItem outlineItem = SelectionMenuItem.node( + getName: LocaleKeys.document_selectionMenu_outline.tr, + iconData: Icons.list_alt, + keywords: ['outline', 'table of contents'], + nodeBuilder: (editorState, _) => outlineBlockNode(), + replace: (_, node) => node.delta?.isEmpty ?? false, +); + Node outlineBlockNode() { return Node( type: OutlineBlockKeys.type, @@ -30,11 +38,6 @@ enum _OutlineBlockStatus { success; } -final _availableBlockTypes = [ - HeadingBlockKeys.type, - ToggleListBlockKeys.type, -]; - class OutlineBlockComponentBuilder extends BlockComponentBuilder { OutlineBlockComponentBuilder({ super.configuration, @@ -52,15 +55,11 @@ class OutlineBlockComponentBuilder extends BlockComponentBuilder { blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; + bool validate(Node node) => node.children.isEmpty; } class OutlineBlockWidget extends BlockComponentStatefulWidget { @@ -69,7 +68,6 @@ class OutlineBlockWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), }); @@ -81,9 +79,7 @@ class _OutlineBlockWidgetState extends State with BlockComponentConfigurable, BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin, - DefaultSelectableMixin, - SelectableMixin { + BlockComponentBackgroundColorMixin { // Change the value if the heading block type supports heading levels greater than '3' static const maxVisibleDepth = 6; @@ -95,18 +91,8 @@ class _OutlineBlockWidgetState extends State @override late EditorState editorState = context.read(); - late Stream stream = editorState.transactionStream; - - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: OutlineBlockKeys.type, - ); - - @override - GlobalKey> get containerKey => widget.node.key; - - @override - GlobalKey> get forwardKey => widget.node.key; + late Stream<(TransactionTime, Transaction)> stream = + editorState.transactionStream; @override Widget build(BuildContext context) { @@ -115,36 +101,19 @@ class _OutlineBlockWidgetState extends State builder: (context, snapshot) { Widget child = _buildOutlineBlock(); - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - remoteSelection: editorState.remoteSelections, - blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, - supportTypes: const [ - BlockSelectionType.block, - ], - child: child, - ); - - if (UniversalPlatform.isDesktopOrWeb) { + if (PlatformExtension.isDesktopOrWeb) { if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: widget.node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } } else { - child = Padding( - padding: padding, - child: MobileBlockActionButtons( - node: node, - editorState: editorState, - child: child, - ), + child = MobileBlockActionButtons( + node: node, + editorState: editorState, + child: child, ); } @@ -202,11 +171,10 @@ class _OutlineBlockWidgetState extends State } return Container( - key: blockComponentKey, constraints: const BoxConstraints( minHeight: 40.0, ), - padding: UniversalPlatform.isMobile ? EdgeInsets.zero : padding, + padding: padding, child: Container( padding: const EdgeInsets.symmetric( vertical: 2.0, @@ -234,43 +202,22 @@ class _OutlineBlockWidgetState extends State } (_OutlineBlockStatus, Iterable) getHeadingNodes() { - final nodes = NodeIterator( - document: editorState.document, - startNode: editorState.document.root, - ).toList(); - final level = node.attributes[OutlineBlockKeys.depth] ?? maxVisibleDepth; - var headings = nodes.where( - (e) => _isHeadingNode(e), + final children = editorState.document.root.children; + final int level = + node.attributes[OutlineBlockKeys.depth] ?? maxVisibleDepth; + var headings = children.where( + (e) => e.type == HeadingBlockKeys.type && e.delta?.isNotEmpty == true, ); if (headings.isEmpty) { return (_OutlineBlockStatus.noHeadings, []); } - headings = headings.where( - (e) => - (e.type == HeadingBlockKeys.type && - e.attributes[HeadingBlockKeys.level] <= level) || - (e.type == ToggleListBlockKeys.type && - e.attributes[ToggleListBlockKeys.level] <= level), - ); + headings = + headings.where((e) => e.attributes[HeadingBlockKeys.level] <= level); if (headings.isEmpty) { return (_OutlineBlockStatus.noMatchHeadings, []); } return (_OutlineBlockStatus.success, headings); } - - bool _isHeadingNode(Node node) { - if (node.type == HeadingBlockKeys.type && node.delta?.isNotEmpty == true) { - return true; - } - - if (node.type == ToggleListBlockKeys.type && - node.delta?.isNotEmpty == true && - node.attributes[ToggleListBlockKeys.level] != null) { - return true; - } - - return false; - } } class OutlineItemWidget extends StatelessWidget { @@ -279,7 +226,7 @@ class OutlineItemWidget extends StatelessWidget { required this.node, required this.textDirection, }) { - assert(_availableBlockTypes.contains(node.type)); + assert(node.type == HeadingBlockKeys.type); } final Node node; @@ -290,22 +237,31 @@ class OutlineItemWidget extends StatelessWidget { final editorState = context.read(); final textStyle = editorState.editorStyle.textStyleConfiguration; final style = textStyle.href.combine(textStyle.text); - return FlowyButton( - onTap: () => scrollToBlock(context), - text: Row( - textDirection: textDirection, - children: [ - HSpace(node.leftIndent), - Flexible( - child: Text( - node.outlineItemText, - textDirection: textDirection, - style: style, - overflow: TextOverflow.ellipsis, - ), - ), - ], + return FlowyHover( + style: HoverStyle( + hoverColor: Theme.of(context).hoverColor, ), + builder: (context, onHover) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => scrollToBlock(context), + child: Row( + textDirection: textDirection, + children: [ + HSpace(node.leftIndent), + Text( + node.outlineItemText, + textDirection: textDirection, + style: style.copyWith( + color: onHover + ? Theme.of(context).colorScheme.onSecondary + : null, + ), + ), + ], + ), + ); + }, ); } @@ -324,20 +280,13 @@ class OutlineItemWidget extends StatelessWidget { extension on Node { double get leftIndent { - assert(_availableBlockTypes.contains(type)); - - if (!_availableBlockTypes.contains(type)) { + assert(type == HeadingBlockKeys.type); + if (type != HeadingBlockKeys.type) { return 0.0; } - - final level = attributes[HeadingBlockKeys.level] ?? - attributes[ToggleListBlockKeys.level]; - if (level != null) { - final indent = (level - 1) * 15.0; - return indent; - } - - return 0.0; + final level = attributes[HeadingBlockKeys.level]; + final indent = (level - 1) * 15.0 + 10.0; + return indent; } String get outlineItemText { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart deleted file mode 100644 index 731ba4c7cd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_block/custom_page_block_component.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/block_component/base_component/widget/ignore_parent_gesture.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/flutter/scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class CustomPageBlockComponentBuilder extends BlockComponentBuilder { - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - return CustomPageBlockComponent( - key: blockComponentContext.node.key, - node: blockComponentContext.node, - header: blockComponentContext.header, - footer: blockComponentContext.footer, - ); - } -} - -class CustomPageBlockComponent extends BlockComponentStatelessWidget { - const CustomPageBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - this.header, - this.footer, - }); - - final Widget? header; - final Widget? footer; - - @override - Widget build(BuildContext context) { - final editorState = context.read(); - final scrollController = context.read(); - final items = node.children; - - if (scrollController == null || scrollController.shrinkWrap) { - return SingleChildScrollView( - child: Builder( - builder: (context) { - final scroller = Scrollable.maybeOf(context); - if (scroller != null) { - editorState.updateAutoScroller(scroller); - } - return Column( - children: [ - if (header != null) header!, - ...items.map( - (e) => Container( - constraints: BoxConstraints( - maxWidth: - editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: editorState.editorStyle.padding, - child: editorState.renderer.build(context, e), - ), - ), - if (footer != null) footer!, - ], - ); - }, - ), - ); - } else { - int extentCount = 0; - if (header != null) extentCount++; - if (footer != null) extentCount++; - - return ScrollablePositionedList.builder( - shrinkWrap: scrollController.shrinkWrap, - itemCount: items.length + extentCount, - itemBuilder: (context, index) { - editorState.updateAutoScroller(Scrollable.of(context)); - if (header != null && index == 0) { - return IgnoreEditorSelectionGesture( - child: header!, - ); - } - - if (footer != null && index == (items.length - 1) + extentCount) { - return IgnoreEditorSelectionGesture( - child: footer!, - ); - } - - final childNode = items[index - (header != null ? 1 : 0)]; - final isOverflowType = overflowTypes.contains(childNode.type); - - final item = Container( - constraints: BoxConstraints( - maxWidth: editorState.editorStyle.maxWidth ?? double.infinity, - ), - padding: isOverflowType - ? EdgeInsets.zero - : editorState.editorStyle.padding, - child: editorState.renderer.build( - context, - childNode, - ), - ); - - return isOverflowType ? item : Center(child: item); - }, - itemScrollController: scrollController.itemScrollController, - scrollOffsetController: scrollController.scrollOffsetController, - itemPositionsListener: scrollController.itemPositionsListener, - scrollOffsetListener: scrollController.scrollOffsetListener, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 45a23bc6ac..fd0aa86fa7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart'; @@ -25,6 +23,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; @@ -32,10 +31,8 @@ import 'package:image_picker/image_picker.dart'; class PageStyleCoverImage extends StatelessWidget { PageStyleCoverImage({ super.key, - required this.documentId, }); - final String documentId; late final ImagePicker _imagePicker = ImagePicker(); @override @@ -226,29 +223,33 @@ class PageStyleCoverImage extends StatelessWidget { (f) => null, ); final isAppFlowyCloud = - userProfile?.workspaceAuthType == AuthTypePB.Server; + userProfile?.authenticator == AuthenticatorPB.AppFlowyCloud; final PageStyleCoverImageType type; if (!isAppFlowyCloud) { result = await saveImageToLocalStorage(path); type = PageStyleCoverImageType.localImage; } else { // else we should save the image to cloud storage - (result, _) = await saveImageToCloudStorage(path, documentId); + (result, _) = await saveImageToCloudStorage(path); type = PageStyleCoverImageType.customImage; } if (!context.mounted) { return; } if (result == null) { - return showSnapBar( + showSnapBar( context, - LocaleKeys.document_plugins_image_imageUploadFailed.tr(), + LocaleKeys.document_plugins_image_imageUploadFailed, ); + return; } context.read().add( DocumentPageStyleEvent.updateCoverImage( - PageStyleCover(type: type, value: result), + PageStyleCover( + type: type, + value: result, + ), ), ); } @@ -279,7 +280,10 @@ class PageStyleCoverImage extends StatelessWidget { }, builder: (_) { return ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80), + constraints: BoxConstraints( + maxHeight: maxHeight, + minHeight: 80, + ), child: BlocProvider.value( value: pageStyleBloc, child: Padding( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index a71b083c81..3f3ed87522 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -1,28 +1,24 @@ 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/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/icon/icon_selector.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.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 '../../../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'package:go_router/go_router.dart'; class PageStyleIcon extends StatefulWidget { const PageStyleIcon({ super.key, required this.view, - required this.tabs, }); final ViewPB view; - final List tabs; @override State createState() => _PageStyleIconState(); @@ -36,9 +32,9 @@ class _PageStyleIconState extends State { ..add(const PageStyleIconEvent.initial()), child: BlocBuilder( builder: (context, state) { - final icon = state.icon; + final icon = state.icon ?? ''; return GestureDetector( - onTap: () => icon == null ? null : _showIconSelector(context, icon), + onTap: () => _showIconSelector(context, icon), behavior: HitTestBehavior.opaque, child: Container( height: 52, @@ -51,15 +47,11 @@ class _PageStyleIconState extends State { const HSpace(16.0), FlowyText(LocaleKeys.document_plugins_emoji.tr()), const Spacer(), - (icon?.isEmpty ?? true) - ? FlowyText( - LocaleKeys.pageStyle_none.tr(), - fontSize: 16.0, - ) - : RawEmojiIconWidget( - emoji: icon!, - emojiSize: 16.0, - ), + FlowyText( + icon.isNotEmpty ? icon : LocaleKeys.pageStyle_none.tr(), + color: icon.isEmpty ? context.pageStyleTextColor : null, + fontSize: icon.isNotEmpty ? 22.0 : 16.0, + ), const HSpace(6.0), const FlowySvg(FlowySvgs.m_page_style_arrow_right_s), const HSpace(12.0), @@ -72,34 +64,37 @@ class _PageStyleIconState extends State { ); } - void _showIconSelector(BuildContext context, EmojiIconData icon) { - Navigator.pop(context); + void _showIconSelector(BuildContext context, String selectedIcon) { + context.pop(); + final pageStyleIconBloc = PageStyleIconBloc(view: widget.view) ..add(const PageStyleIconEvent.initial()); showMobileBottomSheet( context, showDragHandle: true, showDivider: false, + showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, - scrollableWidgetBuilder: (ctx, controller) { + showRemoveButton: true, + onRemove: () { + pageStyleIconBloc.add( + const PageStyleIconEvent.updateIcon('', true), + ); + }, + scrollableWidgetBuilder: (_, controller) { return BlocProvider.value( value: pageStyleIconBloc, child: Expanded( - child: FlowyIconEmojiPicker( - initialType: icon.type.toPickerTabType(), - documentId: widget.view.id, - tabs: widget.tabs, - onSelectedEmoji: (r) { - pageStyleIconBloc.add( - PageStyleIconEvent.updateIcon(r.data, true), - ); - if (!r.keepOpen) Navigator.pop(ctx); - }, + child: Scrollbar( + controller: controller, + child: IconSelector( + scrollController: controller, + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart index b2cd77f312..4d8b0ebf81 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon_bloc.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -18,7 +17,7 @@ class PageStyleIconBloc extends Bloc { initial: () async { add( PageStyleIconEvent.updateIcon( - view.icon.toEmojiIconData(), + view.icon.value, false, ), ); @@ -26,7 +25,7 @@ class PageStyleIconBloc extends Bloc { onViewUpdated: (view) { add( PageStyleIconEvent.updateIcon( - view.icon.toEmojiIconData(), + view.icon.value, false, ), ); @@ -34,10 +33,14 @@ class PageStyleIconBloc extends Bloc { ); }, updateIcon: (icon, shouldUpdateRemote) async { - emit(state.copyWith(icon: icon)); + emit( + state.copyWith( + icon: icon, + ), + ); if (shouldUpdateRemote && icon != null) { await ViewBackendService.updateViewIcon( - view: view, + viewId: view.id, viewIcon: icon, ); } @@ -60,9 +63,8 @@ class PageStyleIconBloc extends Bloc { @freezed class PageStyleIconEvent with _$PageStyleIconEvent { const factory PageStyleIconEvent.initial() = Initial; - const factory PageStyleIconEvent.updateIcon( - EmojiIconData? icon, + String? icon, bool shouldUpdateRemote, ) = UpdateIconInner; } @@ -70,7 +72,7 @@ class PageStyleIconEvent with _$PageStyleIconEvent { @freezed class PageStyleIconState with _$PageStyleIconState { const factory PageStyleIconState({ - @Default(null) EmojiIconData? icon, + @Default(null) String? icon, }) = _PageStyleIconState; factory PageStyleIconState.initial() => const PageStyleIconState(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart index 013a056a7c..29dec9ad67 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart @@ -3,7 +3,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.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'; @@ -13,11 +12,9 @@ class PageStyleBottomSheet extends StatelessWidget { const PageStyleBottomSheet({ super.key, required this.view, - required this.tabs, }); final ViewPB view; - final List tabs; @override Widget build(BuildContext context) { @@ -33,7 +30,7 @@ class PageStyleBottomSheet extends StatelessWidget { fontSize: 14.0, ), const VSpace(8.0), - PageStyleCoverImage(documentId: view.id), + PageStyleCoverImage(), const VSpace(20.0), // layout: font size, line height and font family. FlowyText( @@ -53,7 +50,6 @@ class PageStyleBottomSheet extends StatelessWidget { const VSpace(8.0), PageStyleIcon( view: view, - tabs: tabs, ), ], ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart index d9cf060e3b..867fcf236f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/callout_node_parser.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; class CalloutNodeParser extends NodeParser { @@ -10,6 +9,8 @@ class CalloutNodeParser extends NodeParser { @override String transform(Node node, DocumentMarkdownEncoder? encoder) { + assert(node.children.isEmpty); + final icon = node.attributes[CalloutBlockKeys.icon]; final delta = node.delta ?? Delta() ..insert(''); final String markdown = DeltaMarkdownEncoder() @@ -17,15 +18,9 @@ class CalloutNodeParser extends NodeParser { .split('\n') .map((e) => '> $e') .join('\n'); - final type = node.attributes[CalloutBlockKeys.iconType]; - final icon = type == FlowyIconType.emoji.name || type == null || type == "" - ? node.attributes[CalloutBlockKeys.icon] - : null; - - final content = icon == null ? markdown : "> $icon\n$markdown"; - return ''' -$content +> $icon +$markdown '''; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart index d4b6bb444f..a33d99fd82 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_image_node_parser.dart @@ -1,11 +1,4 @@ -import 'dart:io'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:archive/archive.dart'; -import 'package:path/path.dart' as p; - -import '../image/custom_image_block_component/custom_image_block_component.dart'; class CustomImageNodeParser extends NodeParser { const CustomImageNodeParser(); @@ -16,69 +9,8 @@ class CustomImageNodeParser extends NodeParser { @override String transform(Node node, DocumentMarkdownEncoder? encoder) { assert(node.children.isEmpty); - final url = node.attributes[CustomImageBlockKeys.url]; + final url = node.attributes[ImageBlockKeys.url]; assert(url != null); return '![]($url)\n'; } } - -class CustomImageNodeFileParser extends NodeParser { - const CustomImageNodeFileParser(this.files, this.dirPath); - - final List> files; - final String dirPath; - - @override - String get id => ImageBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - assert(node.children.isEmpty); - final url = node.attributes[CustomImageBlockKeys.url]; - final hasFile = File(url).existsSync(); - if (hasFile) { - final bytes = File(url).readAsBytesSync(); - files.add( - Future.value( - ArchiveFile(p.join(dirPath, p.basename(url)), bytes.length, bytes), - ), - ); - return '![](${p.join(dirPath, p.basename(url))})\n'; - } - assert(url != null); - return '![]($url)\n'; - } -} - -class CustomMultiImageNodeFileParser extends NodeParser { - const CustomMultiImageNodeFileParser(this.files, this.dirPath); - - final List> files; - final String dirPath; - - @override - String get id => MultiImageBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - assert(node.children.isEmpty); - final images = node.attributes[MultiImageBlockKeys.images] as List; - final List markdownImages = []; - for (final image in images) { - final String url = image['url'] ?? ''; - if (url.isEmpty) continue; - final hasFile = File(url).existsSync(); - if (hasFile) { - final bytes = File(url).readAsBytesSync(); - final filePath = p.join(dirPath, p.basename(url)); - files.add( - Future.value(ArchiveFile(filePath, bytes.length, bytes)), - ); - markdownImages.add('![]($filePath)'); - } else { - markdownImages.add('![]($url)'); - } - } - return markdownImages.join('\n'); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart deleted file mode 100644 index b7d7674137..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/custom_paragraph_node_parser.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class CustomParagraphNodeParser extends NodeParser { - const CustomParagraphNodeParser(); - - @override - String get id => ParagraphBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final delta = node.delta; - if (delta != null) { - for (final o in delta) { - final attribute = o.attributes ?? {}; - final Map? mention = attribute[MentionBlockKeys.mention] ?? {}; - if (mention == null) continue; - - /// filter date reminder node, and return it - final String date = mention[MentionBlockKeys.date] ?? ''; - if (date.isNotEmpty) { - final dateTime = DateTime.tryParse(date); - if (dateTime == null) continue; - return '${DateFormat.yMMMd().format(dateTime)}\n'; - } - - /// filter reference page - final String pageId = mention[MentionBlockKeys.pageId] ?? ''; - if (pageId.isNotEmpty) { - return '[]($pageId)\n'; - } - } - } - return const TextNodeParser().transform(node, encoder); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart deleted file mode 100644 index 3ba599d491..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/database_node_parser.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/database/database_view_block_component.dart'; -import 'package:appflowy/workspace/application/settings/share/export_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:archive/archive.dart'; -import 'package:path/path.dart' as p; - -abstract class DatabaseNodeParser extends NodeParser { - DatabaseNodeParser(this.files, this.dirPath); - - final List> files; - final String dirPath; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final String viewId = node.attributes[DatabaseBlockKeys.viewID] ?? ''; - if (viewId.isEmpty) return ''; - files.add(_convertDatabaseToCSV(viewId)); - return '[](${p.join(dirPath, '$viewId.csv')})\n'; - } - - Future _convertDatabaseToCSV(String viewId) async { - final result = await BackendExportService.exportDatabaseAsCSV(viewId); - final filePath = p.join(dirPath, '$viewId.csv'); - ArchiveFile file = ArchiveFile.string(filePath, ''); - result.fold( - (s) => file = ArchiveFile.string(filePath, s.data), - (f) => Log.error('convertDatabaseToCSV error with $viewId, error: $f'), - ); - return file; - } -} - -class GridNodeParser extends DatabaseNodeParser { - GridNodeParser(super.files, super.dirPath); - - @override - String get id => DatabaseBlockKeys.gridType; -} - -class BoardNodeParser extends DatabaseNodeParser { - BoardNodeParser(super.files, super.dirPath); - - @override - String get id => DatabaseBlockKeys.boardType; -} - -class CalendarNodeParser extends DatabaseNodeParser { - CalendarNodeParser(super.files, super.dirPath); - - @override - String get id => DatabaseBlockKeys.calendarType; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart index c0a15629b8..0b694f396e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart @@ -1,9 +1,4 @@ export 'callout_node_parser.dart'; export 'custom_image_node_parser.dart'; -export 'custom_paragraph_node_parser.dart'; -export 'database_node_parser.dart'; -export 'file_block_node_parser.dart'; -export 'link_preview_node_parser.dart'; export 'math_equation_node_parser.dart'; -export 'simple_table_node_parser.dart'; export 'toggle_list_node_parser.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/file_block_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/file_block_node_parser.dart deleted file mode 100644 index e57ededcec..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/file_block_node_parser.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -class FileBlockNodeParser extends NodeParser { - const FileBlockNodeParser(); - - @override - String get id => FileBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final name = node.attributes[FileBlockKeys.name]; - final url = node.attributes[FileBlockKeys.url]; - if (name == null || url == null) { - return ''; - } - return '[$name]($url)\n'; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/link_preview_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/link_preview_node_parser.dart deleted file mode 100644 index c7ce69d221..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/link_preview_node_parser.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; - -class LinkPreviewNodeParser extends NodeParser { - const LinkPreviewNodeParser(); - - @override - String get id => LinkPreviewBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final href = node.attributes[LinkPreviewBlockKeys.url]; - if (href == null) { - return ''; - } - return '[$href]($href)\n'; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart deleted file mode 100644 index d756c25d6b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:markdown/markdown.dart' as md; - -class MarkdownCodeBlockParser extends CustomMarkdownParser { - const MarkdownCodeBlockParser(); - - @override - List transform( - md.Node element, - List parsers, { - MarkdownListType listType = MarkdownListType.unknown, - int? startNumber, - }) { - if (element is! md.Element) { - return []; - } - - if (element.tag != 'pre') { - return []; - } - - final ec = element.children; - if (ec == null || ec.isEmpty) { - return []; - } - - final code = ec.first; - if (code is! md.Element || code.tag != 'code') { - return []; - } - - String? language; - if (code.attributes.containsKey('class')) { - final classes = code.attributes['class']!.split(' '); - final languageClass = classes.firstWhere( - (c) => c.startsWith('language-'), - orElse: () => '', - ); - language = languageClass.substring('language-'.length); - } - - return [ - codeBlockNode( - language: language, - delta: Delta()..insert(code.textContent.trimRight()), - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart index 4ad7734643..3e4fc3a764 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart @@ -1,2 +1,3 @@ -export 'markdown_code_parser.dart'; -export 'markdown_simple_table_parser.dart'; +export 'callout_node_parser.dart'; +export 'math_equation_node_parser.dart'; +export 'toggle_list_node_parser.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart deleted file mode 100644 index 09973021f1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:markdown/markdown.dart' as md; -import 'package:universal_platform/universal_platform.dart'; - -class MarkdownSimpleTableParser extends CustomMarkdownParser { - const MarkdownSimpleTableParser({ - this.tableWidth, - }); - - final double? tableWidth; - - @override - List transform( - md.Node element, - List parsers, { - MarkdownListType listType = MarkdownListType.unknown, - int? startNumber, - }) { - if (element is! md.Element) { - return []; - } - - if (element.tag != 'table') { - return []; - } - - final ec = element.children; - if (ec == null || ec.isEmpty) { - return []; - } - - final th = ec - .whereType() - .where((e) => e.tag == 'thead') - .firstOrNull - ?.children - ?.whereType() - .where((e) => e.tag == 'tr') - .expand((e) => e.children?.whereType().toList() ?? []) - .where((e) => e.tag == 'th') - .toList(); - - final tr = ec - .whereType() - .where((e) => e.tag == 'tbody') - .firstOrNull - ?.children - ?.whereType() - .where((e) => e.tag == 'tr') - .toList(); - - if (th == null || tr == null || th.isEmpty || tr.isEmpty) { - return []; - } - - final rows = []; - - // Add header cells - - rows.add( - simpleTableRowBlockNode( - children: th - .map( - (e) => simpleTableCellBlockNode( - children: [ - paragraphNode( - delta: DeltaMarkdownDecoder().convertNodes(e.children), - ), - ], - ), - ) - .toList(), - ), - ); - - // Add body cells - for (var i = 0; i < tr.length; i++) { - final td = tr[i] - .children - ?.whereType() - .where((e) => e.tag == 'td') - .toList(); - - if (td == null || td.isEmpty) { - continue; - } - - rows.add( - simpleTableRowBlockNode( - children: td - .map( - (e) => simpleTableCellBlockNode( - children: [ - paragraphNode( - delta: DeltaMarkdownDecoder().convertNodes(e.children), - ), - ], - ), - ) - .toList(), - ), - ); - } - - return [ - simpleTableBlockNode( - children: rows, - enableHeaderRow: true, - columnWidths: UniversalPlatform.isMobile || tableWidth == null - ? null - : {for (var i = 0; i < th.length; i++) i.toString(): tableWidth!}, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_node_parser.dart deleted file mode 100644 index b01e797595..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_node_parser.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// Parser for converting SimpleTable nodes to markdown format -class SimpleTableNodeParser extends NodeParser { - const SimpleTableNodeParser(); - - @override - String get id => SimpleTableBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - try { - final tableData = _extractTableData(node, encoder); - if (tableData.isEmpty) { - return ''; - } - return _buildMarkdownTable(tableData); - } catch (e) { - return ''; - } - } - - /// Extracts table data from the node structure into a 2D list of strings - /// Each inner list represents a row, and each string represents a cell's content - List> _extractTableData( - Node node, - DocumentMarkdownEncoder? encoder, - ) { - final tableData = >[]; - final rows = node.children; - - for (final row in rows) { - final rowData = _extractRowData(row, encoder); - tableData.add(rowData); - } - - return tableData; - } - - /// Extracts data from a single table row - List _extractRowData(Node row, DocumentMarkdownEncoder? encoder) { - final rowData = []; - final cells = row.children; - - for (final cell in cells) { - final content = _extractCellContent(cell, encoder); - rowData.add(content); - } - - return rowData; - } - - /// Extracts and formats content from a single table cell - String _extractCellContent(Node cell, DocumentMarkdownEncoder? encoder) { - final contentBuffer = StringBuffer(); - - for (final child in cell.children) { - final delta = child.delta; - - // if the node doesn't contain delta, fallback to the encoder - final content = delta != null - ? DeltaMarkdownEncoder().convert(delta) - : encoder?.convertNodes([child]).trim() ?? ''; - - // Escape pipe characters to prevent breaking markdown table structure - contentBuffer.write(content.replaceAll('|', '\\|')); - } - - return contentBuffer.toString(); - } - - /// Builds a markdown table string from the extracted table data - /// First row is treated as header, followed by separator row and data rows - String _buildMarkdownTable(List> tableData) { - final markdown = StringBuffer(); - final columnCount = tableData[0].length; - - // Add header row - markdown.writeln('|${tableData[0].join('|')}|'); - - // Add separator row - markdown.writeln('|${List.filled(columnCount, '---').join('|')}|'); - - // Add data rows (skip header row) - for (int i = 1; i < tableData.length; i++) { - markdown.writeln('|${tableData[i].join('|')}|'); - } - - return markdown.toString(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart deleted file mode 100644 index 1cf0c569bc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -class SubPageNodeParser extends NodeParser { - const SubPageNodeParser(); - - @override - String get id => SubPageBlockKeys.type; - - @override - String transform(Node node, DocumentMarkdownEncoder? encoder) { - final String viewId = node.attributes[SubPageBlockKeys.viewId] ?? ''; - if (viewId.isNotEmpty) { - final view = pageMemorizer[viewId]; - return '[$viewId](${view?.name ?? ''})\n'; - } - return ''; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 4161036a08..8afef3ec0f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -1,19 +1,10 @@ export 'actions/block_action_list.dart'; -export 'actions/option/option_actions.dart'; -export 'ai/ai_writer_block_component.dart'; -export 'ai/ai_writer_toolbar_item.dart'; +export 'actions/option_action.dart'; export 'align_toolbar_item/align_toolbar_item.dart'; -export 'base/backtick_character_command.dart'; -export 'base/cover_title_command.dart'; export 'base/toolbar_extension.dart'; export 'bulleted_list/bulleted_list_icon.dart'; export 'callout/callout_block_component.dart'; export 'code_block/code_block_language_selector.dart'; -export 'code_block/code_block_menu_item.dart'; -export 'columns/simple_column_block_component.dart'; -export 'columns/simple_column_block_width_resizer.dart'; -export 'columns/simple_column_node_extension.dart'; -export 'columns/simple_columns_block_component.dart'; export 'context_menu/custom_context_menu.dart'; export 'copy_and_paste/custom_copy_command.dart'; export 'copy_and_paste/custom_cut_command.dart'; @@ -23,34 +14,27 @@ export 'database/inline_database_menu_item.dart'; export 'database/referenced_database_menu_item.dart'; export 'error/error_block_component_builder.dart'; export 'extensions/flowy_tint_extension.dart'; -export 'file/file_block.dart'; export 'find_and_replace/find_and_replace_menu.dart'; export 'font/customize_font_toolbar_item.dart'; export 'header/cover_editor_bloc.dart'; export 'header/custom_cover_picker.dart'; -export 'header/document_cover_widget.dart'; -export 'heading/heading_toolbar_item.dart'; -export 'image/custom_image_block_component/custom_image_block_component.dart'; -export 'image/custom_image_block_component/image_menu.dart'; +export 'header/document_header_node_widget.dart'; +export 'image/image_menu.dart'; export 'image/image_selection_menu.dart'; export 'image/mobile_image_toolbar_item.dart'; -export 'image/multi_image_block_component/multi_image_block_component.dart'; -export 'image/multi_image_block_component/multi_image_menu.dart'; export 'inline_math_equation/inline_math_equation.dart'; export 'inline_math_equation/inline_math_equation_toolbar_item.dart'; -export 'keyboard_interceptor/keyboard_interceptor.dart'; export 'link_preview/custom_link_preview.dart'; +export 'link_preview/link_preview_cache.dart'; export 'link_preview/link_preview_menu.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; -export 'mention/mention_block.dart'; export 'mobile_floating_toolbar/custom_mobile_floating_toolbar.dart'; export 'mobile_toolbar_v3/aa_toolbar_item.dart'; -export 'mobile_toolbar_v3/add_attachment_item.dart'; export 'mobile_toolbar_v3/add_block_toolbar_item.dart'; export 'mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; export 'mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart'; -export 'mobile_toolbar_v3/basic_toolbar_item.dart'; +export 'mobile_toolbar_v3/biusc_toolbar_item.dart'; export 'mobile_toolbar_v3/indent_outdent_toolbar_item.dart'; export 'mobile_toolbar_v3/list_toolbar_item.dart'; export 'mobile_toolbar_v3/more_toolbar_item.dart'; @@ -58,21 +42,13 @@ export 'mobile_toolbar_v3/toolbar_item_builder.dart'; export 'mobile_toolbar_v3/undo_redo_toolbar_item.dart'; export 'mobile_toolbar_v3/util.dart'; export 'numbered_list/numbered_list_icon.dart'; +export 'openai/widgets/auto_completion_node_widget.dart'; +export 'openai/widgets/smart_edit_node_widget.dart'; +export 'openai/widgets/smart_edit_toolbar_item.dart'; export 'outline/outline_block_component.dart'; -export 'parsers/document_markdown_parsers.dart'; export 'parsers/markdown_parsers.dart'; -export 'parsers/markdown_simple_table_parser.dart'; -export 'quote/quote_block_component.dart'; -export 'quote/quote_block_shortcuts.dart'; -export 'shortcuts/character_shortcuts.dart'; -export 'shortcuts/command_shortcuts.dart'; -export 'simple_table/simple_table.dart'; -export 'slash_menu/slash_command.dart'; -export 'slash_menu/slash_menu_items_builder.dart'; -export 'sub_page/sub_page_block_component.dart'; export 'table/table_menu.dart'; export 'table/table_option_action.dart'; export 'todo_list/todo_list_icon.dart'; export 'toggle/toggle_block_component.dart'; -export 'toggle/toggle_block_shortcuts.dart'; -export 'video/video_block_component.dart'; +export 'toggle/toggle_block_shortcut_event.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart deleted file mode 100644 index 39ab2c5327..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_component.dart +++ /dev/null @@ -1,324 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// In memory cache of the quote block height to avoid flashing when the quote block is updated. -Map _quoteBlockHeightCache = {}; - -typedef QuoteBlockIconBuilder = Widget Function( - BuildContext context, - Node node, -); - -class QuoteBlockKeys { - const QuoteBlockKeys._(); - - static const String type = 'quote'; - - static const String delta = blockComponentDelta; - - static const String backgroundColor = blockComponentBackgroundColor; - - static const String textDirection = blockComponentTextDirection; -} - -Node quoteNode({ - Delta? delta, - String? textDirection, - Attributes? attributes, - Iterable? children, -}) { - attributes ??= {'delta': (delta ?? Delta()).toJson()}; - return Node( - type: QuoteBlockKeys.type, - attributes: { - ...attributes, - if (textDirection != null) QuoteBlockKeys.textDirection: textDirection, - }, - children: children ?? [], - ); -} - -class QuoteBlockComponentBuilder extends BlockComponentBuilder { - QuoteBlockComponentBuilder({ - super.configuration, - this.iconBuilder, - }); - - final QuoteBlockIconBuilder? iconBuilder; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return QuoteBlockComponentWidget( - key: node.key, - node: node, - configuration: configuration, - iconBuilder: iconBuilder, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.delta != null; -} - -class QuoteBlockComponentWidget extends BlockComponentStatefulWidget { - const QuoteBlockComponentWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - this.iconBuilder, - }); - - final QuoteBlockIconBuilder? iconBuilder; - - @override - State createState() => - _QuoteBlockComponentWidgetState(); -} - -class _QuoteBlockComponentWidgetState extends State - with - SelectableMixin, - DefaultSelectableMixin, - BlockComponentConfigurable, - BlockComponentBackgroundColorMixin, - BlockComponentTextDirectionMixin, - BlockComponentAlignMixin, - NestedBlockComponentStatefulWidgetMixin { - @override - final forwardKey = GlobalKey(debugLabel: 'flowy_rich_text'); - - @override - GlobalKey> get containerKey => widget.node.key; - - @override - GlobalKey> blockComponentKey = GlobalKey( - debugLabel: QuoteBlockKeys.type, - ); - - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - late ValueNotifier quoteBlockHeightNotifier = ValueNotifier( - _quoteBlockHeightCache[node.id] ?? 0, - ); - - StreamSubscription? _transactionSubscription; - - final GlobalKey layoutBuilderKey = GlobalKey(); - - @override - void initState() { - super.initState(); - - _updateQuoteBlockHeight(); - } - - @override - void dispose() { - _transactionSubscription?.cancel(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return NotificationListener( - key: layoutBuilderKey, - onNotification: (notification) { - _updateQuoteBlockHeight(); - return true; - }, - child: SizeChangedLayoutNotifier( - child: node.children.isEmpty - ? buildComponent(context) - : buildComponentWithChildren(context), - ), - ); - } - - @override - Widget buildComponentWithChildren(BuildContext context) { - final Widget child = Stack( - children: [ - Positioned.fill( - left: UniversalPlatform.isMobile ? padding.left : cachedLeft, - right: UniversalPlatform.isMobile ? padding.right : 0, - child: Container( - color: backgroundColor, - ), - ), - NestedListWidget( - indentPadding: indentPadding, - child: buildComponent(context, withBackgroundColor: false), - children: editorState.renderer.buildList( - context, - widget.node.children, - ), - ), - ], - ); - - return child; - } - - @override - Widget buildComponent( - BuildContext context, { - bool withBackgroundColor = true, - }) { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - - Widget child = AppFlowyRichText( - key: forwardKey, - delegate: this, - node: widget.node, - editorState: editorState, - textAlign: alignment?.toTextAlign ?? textAlign, - placeholderText: placeholderText, - textSpanDecorator: (textSpan) => textSpan.updateTextStyle( - textStyleWithTextSpan(textSpan: textSpan), - ), - placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle( - placeholderTextStyleWithTextSpan(textSpan: textSpan), - ), - textDirection: textDirection, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - cursorWidth: editorState.editorStyle.cursorWidth, - ); - - child = Container( - width: double.infinity, - alignment: alignment, - child: IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - textDirection: textDirection, - children: [ - widget.iconBuilder != null - ? widget.iconBuilder!(context, node) - : ValueListenableBuilder( - valueListenable: quoteBlockHeightNotifier, - builder: (context, height, child) { - return QuoteIcon(height: height); - }, - ), - Flexible( - child: child, - ), - ], - ), - ), - ); - - child = Container( - color: withBackgroundColor ? backgroundColor : null, - child: Padding( - key: blockComponentKey, - padding: padding, - child: child, - ), - ); - - child = BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - remoteSelection: editorState.remoteSelections, - blockColor: editorState.editorStyle.selectionColor, - selectionAboveBlock: true, - supportTypes: const [ - BlockSelectionType.block, - ], - child: child, - ); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - return child; - } - - void _updateQuoteBlockHeight() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final renderObject = layoutBuilderKey.currentContext?.findRenderObject(); - double height = _quoteBlockHeightCache[node.id] ?? 0; - if (renderObject != null && renderObject is RenderBox) { - if (UniversalPlatform.isMobile) { - height = renderObject.size.height - padding.top; - } else { - height = renderObject.size.height - padding.top * 2; - } - } else { - height = 0; - } - - quoteBlockHeightNotifier.value = height; - _quoteBlockHeightCache[node.id] = height; - }); - } -} - -class QuoteIcon extends StatelessWidget { - const QuoteIcon({ - super.key, - this.height = 0, - }); - - final double height; - - @override - Widget build(BuildContext context) { - final textScaleFactor = - context.read().editorStyle.textScaleFactor; - return Container( - alignment: Alignment.center, - constraints: - const BoxConstraints(minWidth: 22, minHeight: 22, maxHeight: 22) * - textScaleFactor, - padding: const EdgeInsets.only(right: 6.0), - child: SizedBox( - width: 3 * textScaleFactor, - // use overflow box to ensure the container can overflow the height so that the children of the quote block can have the quote - child: OverflowBox( - alignment: Alignment.topCenter, - maxHeight: height, - child: Container( - width: 3 * textScaleFactor, - height: height, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart deleted file mode 100644 index 47c6549923..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/quote/quote_block_shortcuts.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/services.dart'; - -/// Pressing Enter in a quote block will insert a newline (\n) within the quote, -/// while pressing Shift+Enter in a quote will insert a new paragraph next to the quote. -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -final CharacterShortcutEvent insertNewLineInQuoteBlock = CharacterShortcutEvent( - key: 'insert a new line in quote block', - character: '\n', - handler: _insertNewLineHandler, -); - -CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async { - final selection = editorState.selection?.normalized; - if (selection == null) { - return false; - } - - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.type != QuoteBlockKeys.type) { - return false; - } - - // delete the selection - await editorState.deleteSelection(selection); - - if (HardwareKeyboard.instance.isShiftPressed) { - // ignore the shift+enter event, fallback to the default behavior - return false; - } else if (node.children.isEmpty && - selection.endIndex == node.delta?.length) { - // insert a new paragraph within the callout block - final path = node.path.child(0); - final transaction = editorState.transaction; - transaction.insertNode( - path, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed( - Position( - path: path, - ), - ); - await editorState.apply(transaction); - return true; - } - - return false; -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart deleted file mode 100644 index 40d4c54163..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// Shared context for the editor plugins. -/// -/// For example, the backspace command requires the focus node of the cover title. -/// so we need to use the shared context to get the focus node. -/// -class SharedEditorContext { - SharedEditorContext() : _coverTitleFocusNode = FocusNode(); - - // The focus node of the cover title. - final FocusNode _coverTitleFocusNode; - - bool requestCoverTitleFocus = false; - - bool isInDatabaseRowPage = false; - - FocusNode get coverTitleFocusNode => _coverTitleFocusNode; - - void dispose() { - _coverTitleFocusNode.dispose(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart deleted file mode 100644 index 13b2fea5ee..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/character_shortcuts.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_shortcuts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_command.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; - -List buildCharacterShortcutEvents( - BuildContext context, - DocumentBloc documentBloc, - EditorStyleCustomizer styleCustomizer, - InlineActionsService inlineActionsService, - SlashMenuItemsBuilder slashMenuItemsBuilder, -) { - return [ - // code block - formatBacktickToCodeBlock, - ...codeBlockCharacterEvents, - - // callout block - insertNewLineInCalloutBlock, - - // quote block - insertNewLineInQuoteBlock, - - // toggle list - formatGreaterToToggleList, - insertChildNodeInsideToggleList, - - // customize the slash menu command - customAppFlowySlashCommand( - itemsBuilder: slashMenuItemsBuilder, - style: styleCustomizer.selectionMenuStyleBuilder(), - supportSlashMenuNodeTypes: supportSlashMenuNodeTypes, - ), - - customFormatGreaterEqual, - customFormatDashGreater, - customFormatDoubleHyphenEmDash, - - customFormatNumberToNumberedList, - customFormatSignToHeading, - - ...standardCharacterShortcutEvents - ..removeWhere( - (shortcut) => [ - slashCommand, // Remove default slash command - formatGreaterEqual, // Overridden by customFormatGreaterEqual - formatNumberToNumberedList, // Overridden by customFormatNumberToNumberedList - formatSignToHeading, // Overridden by customFormatSignToHeading - formatDoubleHyphenEmDash, // Overridden by customFormatDoubleHyphenEmDash - ].contains(shortcut), - ), - - /// Inline Actions - /// - Reminder - /// - Inline-page reference - inlineActionsCommand( - inlineActionsService, - style: styleCustomizer.inlineActionsMenuStyleBuilder(), - ), - - /// Inline page menu - /// - Using `[[` - pageReferenceShortcutBrackets( - context, - documentBloc.documentId, - styleCustomizer.inlineActionsMenuStyleBuilder(), - ), - - /// - Using `+` - pageReferenceShortcutPlusSign( - context, - documentBloc.documentId, - styleCustomizer.inlineActionsMenuStyleBuilder(), - ), - - /// show emoji list - /// - Using `:` - emojiCommand(context), - ]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart deleted file mode 100644 index aedfcff432..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'exit_edit_mode_command.dart'; - -final List defaultCommandShortcutEvents = [ - ...commandShortcutEvents.map((e) => e.copyWith()), -]; - -// Command shortcuts are order-sensitive. Verify order when modifying. -List commandShortcutEvents = [ - ...simpleTableCommands, - - customExitEditingCommand, - backspaceToTitle, - removeToggleHeadingStyle, - - arrowUpToTitle, - arrowLeftToTitle, - - toggleToggleListCommand, - - ...localizedCodeBlockCommands, - - customCopyCommand, - customPasteCommand, - customPastePlainTextCommand, - customCutCommand, - customUndoCommand, - customRedoCommand, - - ...customTextAlignCommands, - - customDeleteCommand, - insertInlineMathEquationCommand, - - // remove standard shortcuts for copy, cut, paste, todo - ...standardCommandShortcutEvents - ..removeWhere( - (shortcut) => [ - copyCommand, - cutCommand, - pasteCommand, - pasteTextWithoutFormattingCommand, - toggleTodoListCommand, - undoCommand, - redoCommand, - exitEditingCommand, - ...tableCommands, - deleteCommand, - ].contains(shortcut), - ), - - emojiShortcutEvent, -]; - -final _codeBlockLocalization = CodeBlockLocalizations( - codeBlockNewParagraph: - LocaleKeys.settings_shortcutsPage_commands_codeBlockNewParagraph.tr(), - codeBlockIndentLines: - LocaleKeys.settings_shortcutsPage_commands_codeBlockIndentLines.tr(), - codeBlockOutdentLines: - LocaleKeys.settings_shortcutsPage_commands_codeBlockOutdentLines.tr(), - codeBlockSelectAll: - LocaleKeys.settings_shortcutsPage_commands_codeBlockSelectAll.tr(), - codeBlockPasteText: - LocaleKeys.settings_shortcutsPage_commands_codeBlockPasteText.tr(), - codeBlockAddTwoSpaces: - LocaleKeys.settings_shortcutsPage_commands_codeBlockAddTwoSpaces.tr(), -); - -final localizedCodeBlockCommands = codeBlockCommands( - localizations: _codeBlockLocalization, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart deleted file mode 100644 index e0663c2bcf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/custom_delete_command.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -/// Delete key event. -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customDeleteCommand = CommandShortcutEvent( - key: 'Delete Key', - getDescription: () => AppFlowyEditorL10n.current.cmdDeleteRight, - command: 'delete, shift+delete', - handler: _deleteCommandHandler, -); - -CommandShortcutEventHandler _deleteCommandHandler = (editorState) { - final selection = editorState.selection; - final selectionType = editorState.selectionType; - if (selection == null) { - return KeyEventResult.ignored; - } - if (selectionType == SelectionType.block) { - return _deleteInBlockSelection(editorState); - } else if (selection.isCollapsed) { - return _deleteInCollapsedSelection(editorState); - } else { - return _deleteInNotCollapsedSelection(editorState); - } -}; - -/// Handle delete key event when selection is collapsed. -CommandShortcutEventHandler _deleteInCollapsedSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return KeyEventResult.ignored; - } - - final position = selection.start; - final node = editorState.getNodeAtPath(position.path); - final delta = node?.delta; - if (node == null || delta == null) { - return KeyEventResult.ignored; - } - - final transaction = editorState.transaction; - - if (position.offset == delta.length) { - final Node? tableParent = - node.findParent((element) => element.type == SimpleTableBlockKeys.type); - Node? nextTableParent; - final next = node.findDownward((element) { - nextTableParent = element - .findParent((element) => element.type == SimpleTableBlockKeys.type); - // break if only one is in a table or they're in different tables - return tableParent != nextTableParent || - // merge the next node with delta - element.delta != null; - }); - // table nodes should be deleted using the table menu - // in-table paragraphs should only be deleted inside the table - if (next != null && tableParent == nextTableParent) { - if (next.children.isNotEmpty) { - final path = node.path + [node.children.length]; - transaction.insertNodes(path, next.children); - } - transaction - ..deleteNode(next) - ..mergeText( - node, - next, - ); - editorState.apply(transaction); - return KeyEventResult.handled; - } - } else { - final nextIndex = delta.nextRunePosition(position.offset); - if (nextIndex <= delta.length) { - transaction.deleteText( - node, - position.offset, - nextIndex - position.offset, - ); - editorState.apply(transaction); - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; -}; - -/// Handle delete key event when selection is not collapsed. -CommandShortcutEventHandler _deleteInNotCollapsedSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return KeyEventResult.ignored; - } - editorState.deleteSelection(selection); - return KeyEventResult.handled; -}; - -CommandShortcutEventHandler _deleteInBlockSelection = (editorState) { - final selection = editorState.selection; - if (selection == null || editorState.selectionType != SelectionType.block) { - return KeyEventResult.ignored; - } - final transaction = editorState.transaction; - transaction.deleteNodesAtPath(selection.start.path); - editorState - .apply(transaction) - .then((value) => editorState.selectionType = null); - - return KeyEventResult.handled; -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/exit_edit_mode_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/exit_edit_mode_command.dart deleted file mode 100644 index 4eaa131390..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/exit_edit_mode_command.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -/// End key event. -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customExitEditingCommand = CommandShortcutEvent( - key: 'exit the editing mode', - getDescription: () => AppFlowyEditorL10n.current.cmdExitEditing, - command: 'escape', - handler: _exitEditingCommandHandler, -); - -CommandShortcutEventHandler _exitEditingCommandHandler = (editorState) { - if (editorState.selection == null) { - return KeyEventResult.ignored; - } - editorState.selection = null; - editorState.service.keyboardService?.closeKeyboard(); - return KeyEventResult.handled; -}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart deleted file mode 100644 index a65cd61c83..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/heading_block_shortcuts.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// Convert '# ' to bulleted list -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -CharacterShortcutEvent customFormatSignToHeading = CharacterShortcutEvent( - key: 'format sign to heading list', - character: ' ', - handler: (editorState) async => formatMarkdownSymbol( - editorState, - (node) => true, - (_, text, selection) { - final characters = text.split(''); - // only supports h1 to h6 levels - // if the characters is empty, the every function will return true directly - return characters.isNotEmpty && - characters.every((element) => element == '#') && - characters.length < 7; - }, - (text, node, delta) { - final numberOfSign = text.split('').length; - final type = node.type; - - // if current node is toggle block, try to convert it to toggle heading block. - if (type == ToggleListBlockKeys.type) { - final collapsed = - node.attributes[ToggleListBlockKeys.collapsed] as bool?; - return [ - toggleHeadingNode( - level: numberOfSign, - delta: delta.compose(Delta()..delete(numberOfSign)), - collapsed: collapsed ?? false, - children: node.children.map((child) => child.deepCopy()), - ), - ]; - } - return [ - headingNode( - level: numberOfSign, - delta: delta.compose(Delta()..delete(numberOfSign)), - ), - if (node.children.isNotEmpty) ...node.children, - ]; - }, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart deleted file mode 100644 index b0e271fbe8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/numbered_list_block_shortcuts.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// Convert 'num. ' to bulleted list -/// -/// - support -/// - desktop -/// - mobile -/// -/// In heading block and toggle heading block, this shortcut will be ignored. -CharacterShortcutEvent customFormatNumberToNumberedList = - CharacterShortcutEvent( - key: 'format number to numbered list', - character: ' ', - handler: (editorState) async => formatMarkdownSymbol( - editorState, - (node) => node.type != NumberedListBlockKeys.type, - (node, text, selection) { - final shouldBeIgnored = _shouldBeIgnored(node); - if (shouldBeIgnored) { - return false; - } - - final match = numberedListRegex.firstMatch(text); - if (match == null) { - return false; - } - - final matchText = match.group(0); - final numberText = match.group(1); - - if (matchText == null || numberText == null) { - return false; - } - - // if the previous one is numbered list, - // we should check the current number is the next number of the previous one - Node? previous = node.previous; - int level = 0; - int? startNumber; - while (previous != null && previous.type == NumberedListBlockKeys.type) { - startNumber = previous.attributes[NumberedListBlockKeys.number] as int?; - level++; - previous = previous.previous; - } - if (startNumber != null) { - final currentNumber = int.tryParse(numberText); - if (currentNumber == null || currentNumber != startNumber + level) { - return false; - } - } - - return selection.endIndex == matchText.length; - }, - (text, node, delta) { - final match = numberedListRegex.firstMatch(text); - final matchText = match?.group(0); - if (matchText == null) { - return [node]; - } - - final number = matchText.substring(0, matchText.length - 1); - final composedDelta = delta.compose( - Delta()..delete(matchText.length), - ); - return [ - node.copyWith( - type: NumberedListBlockKeys.type, - attributes: { - NumberedListBlockKeys.delta: composedDelta.toJson(), - NumberedListBlockKeys.number: int.tryParse(number), - }, - ), - ]; - }, - ), -); - -bool _shouldBeIgnored(Node node) { - final type = node.type; - - // ignore heading block - if (type == HeadingBlockKeys.type) { - return true; - } - - // ignore toggle heading block - final level = node.attributes[ToggleListBlockKeys.level] as int?; - if (type == ToggleListBlockKeys.type && level != null) { - return true; - } - - return false; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart deleted file mode 100644 index 4454e9efaf..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'simple_table_block_component.dart'; -export 'simple_table_cell_block_component.dart'; -export 'simple_table_constants.dart'; -export 'simple_table_more_action.dart'; -export 'simple_table_operations/simple_table_operations.dart'; -export 'simple_table_row_block_component.dart'; -export 'simple_table_shortcuts/simple_table_commands.dart'; -export 'simple_table_widgets/widgets.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart deleted file mode 100644 index ee6020793c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart +++ /dev/null @@ -1,353 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -typedef SimpleTableColumnWidthMap = Map; -typedef SimpleTableRowAlignMap = Map; -typedef SimpleTableColumnAlignMap = Map; -typedef SimpleTableColorMap = Map; -typedef SimpleTableAttributeMap = Map; - -class SimpleTableBlockKeys { - const SimpleTableBlockKeys._(); - - static const String type = 'simple_table'; - - /// enable header row - /// it's a bool value, default is false - static const String enableHeaderRow = 'enable_header_row'; - - /// enable column header - /// it's a bool value, default is false - static const String enableHeaderColumn = 'enable_header_column'; - - /// column colors - /// it's a [SimpleTableColorMap] value, {column_index: color, ...} - /// the number of colors should be the same as the number of columns - static const String columnColors = 'column_colors'; - - /// row colors - /// it's a [SimpleTableColorMap] value, {row_index: color, ...} - /// the number of colors should be the same as the number of rows - static const String rowColors = 'row_colors'; - - /// column alignments - /// it's a [SimpleTableColumnAlignMap] value, {column_index: align, ...} - /// the value should be one of the following: 'left', 'center', 'right' - static const String columnAligns = 'column_aligns'; - - /// row alignments - /// it's a [SimpleTableRowAlignMap] value, {row_index: align, ...} - /// the value should be one of the following: 'top', 'center', 'bottom' - static const String rowAligns = 'row_aligns'; - - /// column bold attributes - /// it's a [SimpleTableAttributeMap] value, {column_index: attribute, ...} - /// the attribute should be one of the following: true, false - static const String columnBoldAttributes = 'column_bold_attributes'; - - /// row bold attributes - /// it's a [SimpleTableAttributeMap] value, {row_index: true, ...} - /// the attribute should be one of the following: true, false - static const String rowBoldAttributes = 'row_bold_attributes'; - - /// column text color attributes - /// it's a [SimpleTableColorMap] value, {column_index: color_hex_code, ...} - /// the attribute should be the color hex color or appflowy_theme_color - static const String columnTextColors = 'column_text_colors'; - - /// row text color attributes - /// it's a [SimpleTableColorMap] value, {row_index: color_hex_code, ...} - /// the attribute should be the color hex color or appflowy_theme_color - static const String rowTextColors = 'row_text_colors'; - - /// column widths - /// it's a [SimpleTableColumnWidthMap] value, {column_index: width, ...} - static const String columnWidths = 'column_widths'; - - /// distribute column widths evenly - /// if the user distributed the column widths evenly before, the value should be true, - /// and for the newly added column, using the width of the previous column. - /// it's a bool value, default is false - static const String distributeColumnWidthsEvenly = - 'distribute_column_widths_evenly'; -} - -Node simpleTableBlockNode({ - bool enableHeaderRow = false, - bool enableHeaderColumn = false, - SimpleTableColorMap? columnColors, - SimpleTableColorMap? rowColors, - SimpleTableColumnAlignMap? columnAligns, - SimpleTableRowAlignMap? rowAligns, - SimpleTableColumnWidthMap? columnWidths, - required List children, -}) { - assert(children.every((e) => e.type == SimpleTableRowBlockKeys.type)); - - return Node( - type: SimpleTableBlockKeys.type, - attributes: { - SimpleTableBlockKeys.enableHeaderRow: enableHeaderRow, - SimpleTableBlockKeys.enableHeaderColumn: enableHeaderColumn, - SimpleTableBlockKeys.columnColors: columnColors, - SimpleTableBlockKeys.rowColors: rowColors, - SimpleTableBlockKeys.columnAligns: columnAligns, - SimpleTableBlockKeys.rowAligns: rowAligns, - SimpleTableBlockKeys.columnWidths: columnWidths, - }, - children: children, - ); -} - -/// Create a simple table block node with the given column and row count. -/// -/// The table will have cells filled with paragraph nodes. -/// -/// For example, if you want to create a table with 2 columns and 3 rows, you can use: -/// ```dart -/// final table = createSimpleTableBlockNode(columnCount: 2, rowCount: 3); -/// ``` -/// -/// | cell 1 | cell 2 | -/// | cell 3 | cell 4 | -/// | cell 5 | cell 6 | -Node createSimpleTableBlockNode({ - required int columnCount, - required int rowCount, - String? defaultContent, - String Function(int rowIndex, int columnIndex)? contentBuilder, -}) { - final rows = List.generate(rowCount, (rowIndex) { - final cells = List.generate( - columnCount, - (columnIndex) => simpleTableCellBlockNode( - children: [ - paragraphNode( - text: defaultContent ?? contentBuilder?.call(rowIndex, columnIndex), - ), - ], - ), - ); - return simpleTableRowBlockNode(children: cells); - }); - - return simpleTableBlockNode(children: rows); -} - -class SimpleTableBlockComponentBuilder extends BlockComponentBuilder { - SimpleTableBlockComponentBuilder({ - super.configuration, - this.alwaysDistributeColumnWidths = false, - }); - - final bool alwaysDistributeColumnWidths; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return SimpleTableBlockWidget( - key: node.key, - node: node, - configuration: configuration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isNotEmpty; -} - -class SimpleTableBlockWidget extends BlockComponentStatefulWidget { - const SimpleTableBlockWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - required this.alwaysDistributeColumnWidths, - }); - - final bool alwaysDistributeColumnWidths; - - @override - State createState() => _SimpleTableBlockWidgetState(); -} - -class _SimpleTableBlockWidgetState extends State - with - SelectableMixin, - BlockComponentConfigurable, - BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - @override - late EditorState editorState = context.read(); - - final tableKey = GlobalKey(); - - final simpleTableContext = SimpleTableContext(); - final scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - - editorState.selectionNotifier.addListener(_onSelectionChanged); - } - - @override - void dispose() { - simpleTableContext.dispose(); - editorState.selectionNotifier.removeListener(_onSelectionChanged); - scrollController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child = SimpleTableWidget( - node: node, - simpleTableContext: simpleTableContext, - alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths, - ); - - if (UniversalPlatform.isDesktop) { - child = Transform.translate( - offset: Offset( - -SimpleTableConstants.tableLeftPadding, - 0, - ), - child: child, - ); - } - - child = Container( - alignment: Alignment.topLeft, - padding: padding, - child: child, - ); - - if (UniversalPlatform.isDesktop) { - child = Provider.value( - value: simpleTableContext, - child: MouseRegion( - onEnter: (event) => - simpleTableContext.isHoveringOnTableBlock.value = true, - onExit: (event) { - simpleTableContext.isHoveringOnTableBlock.value = false; - }, - child: child, - ), - ); - } - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - return child; - } - - void _onSelectionChanged() { - final selection = editorState.selectionNotifier.value; - final selectionType = editorState.selectionType; - if (selectionType == SelectionType.block && - widget.node.path.inSelection(selection)) { - simpleTableContext.isSelectingTable.value = true; - } else { - simpleTableContext.isSelectingTable.value = false; - } - } - - RenderBox get _renderBox => context.findRenderObject() as RenderBox; - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - final parentBox = context.findRenderObject(); - final tableBox = tableKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && tableBox is RenderBox) { - return [ - (shiftWithBaseOffset - ? tableBox.localToGlobal(Offset.zero, ancestor: parentBox) - : Offset.zero) & - tableBox.size, - ]; - } - return [Offset.zero & _renderBox.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => Selection.single( - path: widget.node.path, - startOffset: 0, - endOffset: 1, - ); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Offset localToGlobal( - Offset offset, { - bool shiftWithBaseOffset = false, - }) => - _renderBox.localToGlobal(offset); - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - return getRectsInSelection(Selection.invalid()).first; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final size = _renderBox.size; - return Rect.fromLTWH(-size.width / 2.0, 0, size.width, size.height); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart deleted file mode 100644 index 29b3c3455f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart +++ /dev/null @@ -1,601 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SimpleTableCellBlockKeys { - const SimpleTableCellBlockKeys._(); - - static const String type = 'simple_table_cell'; -} - -Node simpleTableCellBlockNode({ - List? children, -}) { - // Default children is a paragraph node. - children ??= [ - paragraphNode(), - ]; - - return Node( - type: SimpleTableCellBlockKeys.type, - children: children, - ); -} - -class SimpleTableCellBlockComponentBuilder extends BlockComponentBuilder { - SimpleTableCellBlockComponentBuilder({ - super.configuration, - this.alwaysDistributeColumnWidths = false, - }); - - final bool alwaysDistributeColumnWidths; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return SimpleTableCellBlockWidget( - key: node.key, - node: node, - configuration: configuration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (node) => true; -} - -class SimpleTableCellBlockWidget extends BlockComponentStatefulWidget { - const SimpleTableCellBlockWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - required this.alwaysDistributeColumnWidths, - }); - - final bool alwaysDistributeColumnWidths; - - @override - State createState() => - SimpleTableCellBlockWidgetState(); -} - -@visibleForTesting -class SimpleTableCellBlockWidgetState extends State - with - BlockComponentConfigurable, - BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - @override - late EditorState editorState = context.read(); - - late SimpleTableContext? simpleTableContext = - context.read(); - late final borderBuilder = SimpleTableBorderBuilder( - context: context, - simpleTableContext: simpleTableContext!, - node: node, - ); - - /// Notify if the cell is editing. - ValueNotifier isEditingCellNotifier = ValueNotifier(false); - - /// Notify if the cell is hit by the reordering offset. - /// - /// This value is only available on mobile. - ValueNotifier isReorderingHitCellNotifier = ValueNotifier(false); - - @override - void initState() { - super.initState(); - - simpleTableContext?.isSelectingTable.addListener(_onSelectingTableChanged); - simpleTableContext?.reorderingOffset - .addListener(_onReorderingOffsetChanged); - node.parentTableNode?.addListener(_onSelectingTableChanged); - editorState.selectionNotifier.addListener(_onSelectionChanged); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _onSelectionChanged(); - }); - } - - @override - void dispose() { - simpleTableContext?.isSelectingTable.removeListener( - _onSelectingTableChanged, - ); - simpleTableContext?.reorderingOffset.removeListener( - _onReorderingOffsetChanged, - ); - node.parentTableNode?.removeListener(_onSelectingTableChanged); - editorState.selectionNotifier.removeListener(_onSelectionChanged); - isEditingCellNotifier.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (simpleTableContext == null) { - return const SizedBox.shrink(); - } - - Widget child = Stack( - clipBehavior: Clip.none, - children: [ - _buildCell(), - if (editorState.editable) ...[ - if (node.columnIndex == 0) - Positioned( - // if the cell is in the first row, add padding to the top of the cell - // to make the row action button clickable. - top: node.rowIndex == 0 - ? SimpleTableConstants.tableHitTestTopPadding - : 0, - bottom: 0, - left: -SimpleTableConstants.tableLeftPadding, - child: _buildRowMoreActionButton(), - ), - if (node.rowIndex == 0) - Positioned( - left: node.columnIndex == 0 - ? SimpleTableConstants.tableHitTestLeftPadding - : 0, - right: 0, - child: _buildColumnMoreActionButton(), - ), - if (node.columnIndex == 0 && node.rowIndex == 0) - Positioned( - left: 2, - top: 2, - child: _buildTableActionMenu(), - ), - Positioned( - right: 0, - top: node.rowIndex == 0 - ? SimpleTableConstants.tableHitTestTopPadding - : 0, - bottom: 0, - child: SimpleTableColumnResizeHandle( - node: node, - ), - ), - ], - ], - ); - - if (UniversalPlatform.isDesktop) { - child = MouseRegion( - hitTestBehavior: HitTestBehavior.opaque, - onEnter: (event) => simpleTableContext!.hoveringTableCell.value = node, - child: child, - ); - } - - return child; - } - - Widget _buildCell() { - if (simpleTableContext == null) { - return const SizedBox.shrink(); - } - - return UniversalPlatform.isDesktop - ? _buildDesktopCell() - : _buildMobileCell(); - } - - Widget _buildDesktopCell() { - return Padding( - // add padding to the top of the cell if it is the first row, otherwise the - // column action button is not clickable. - // issue: https://github.com/flutter/flutter/issues/75747 - padding: EdgeInsets.only( - top: node.rowIndex == 0 - ? SimpleTableConstants.tableHitTestTopPadding - : 0, - left: node.columnIndex == 0 - ? SimpleTableConstants.tableHitTestLeftPadding - : 0, - ), - // TODO(Lucas): find a better way to handle the multiple value listenable builder - // There's flutter pub can do that. - child: ValueListenableBuilder( - valueListenable: isEditingCellNotifier, - builder: (context, isEditingCell, child) { - return ValueListenableBuilder( - valueListenable: simpleTableContext!.selectingColumn, - builder: (context, selectingColumn, _) { - return ValueListenableBuilder( - valueListenable: simpleTableContext!.selectingRow, - builder: (context, selectingRow, _) { - return ValueListenableBuilder( - valueListenable: simpleTableContext!.hoveringTableCell, - builder: (context, hoveringTableCell, _) { - return DecoratedBox( - decoration: _buildDecoration(), - child: child!, - ); - }, - ); - }, - ); - }, - ); - }, - child: Container( - padding: SimpleTableConstants.cellEdgePadding, - constraints: const BoxConstraints( - minWidth: SimpleTableConstants.minimumColumnWidth, - ), - width: widget.alwaysDistributeColumnWidths ? null : node.columnWidth, - child: node.children.isEmpty - ? Column( - children: [ - // expand the cell to make the empty cell content clickable - Expanded( - child: _buildEmptyCellContent(), - ), - ], - ) - : Column( - children: [ - ...node.children.map(_buildCellContent), - _buildEmptyCellContent(height: 12), - ], - ), - ), - ), - ); - } - - Widget _buildMobileCell() { - return Padding( - padding: EdgeInsets.only( - top: node.rowIndex == 0 - ? SimpleTableConstants.tableHitTestTopPadding - : 0, - left: node.columnIndex == 0 - ? SimpleTableConstants.tableHitTestLeftPadding - : 0, - ), - child: ValueListenableBuilder( - valueListenable: isEditingCellNotifier, - builder: (context, isEditingCell, child) { - return ValueListenableBuilder( - valueListenable: simpleTableContext!.selectingColumn, - builder: (context, selectingColumn, _) { - return ValueListenableBuilder( - valueListenable: simpleTableContext!.selectingRow, - builder: (context, selectingRow, _) { - return ValueListenableBuilder( - valueListenable: isReorderingHitCellNotifier, - builder: (context, isReorderingHitCellNotifier, _) { - final previousCell = node.getPreviousCellInSameRow(); - return Stack( - children: [ - DecoratedBox( - decoration: _buildDecoration(), - child: child!, - ), - Positioned( - right: 0, - top: 0, - bottom: 0, - child: SimpleTableColumnResizeHandle( - node: node, - ), - ), - if (node.columnIndex != 0 && previousCell != null) - Positioned( - left: 0, - top: 0, - bottom: 0, - // pass the previous node to the resize handle - // to make the resize handle work correctly - child: SimpleTableColumnResizeHandle( - node: previousCell, - isPreviousCell: true, - ), - ), - ], - ); - }, - ); - }, - ); - }, - ); - }, - child: Container( - padding: SimpleTableConstants.cellEdgePadding, - constraints: const BoxConstraints( - minWidth: SimpleTableConstants.minimumColumnWidth, - ), - width: node.columnWidth, - child: node.children.isEmpty - ? _buildEmptyCellContent() - : Column( - children: node.children.map(_buildCellContent).toList(), - ), - ), - ), - ); - } - - Widget _buildCellContent(Node childNode) { - final alignment = _buildAlignment(); - - Widget child = IntrinsicWidth( - child: editorState.renderer.build(context, childNode), - ); - - final notSupportAlignmentBlocks = [ - DividerBlockKeys.type, - CalloutBlockKeys.type, - MathEquationBlockKeys.type, - CodeBlockKeys.type, - SubPageBlockKeys.type, - FileBlockKeys.type, - CustomImageBlockKeys.type, - ]; - if (notSupportAlignmentBlocks.contains(childNode.type)) { - child = SizedBox( - width: double.infinity, - child: child, - ); - } else { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - } - - Widget _buildEmptyCellContent({ - double? height, - }) { - // if the table cell is empty, we should allow the user to tap on it to create a new paragraph. - final lastChild = node.children.lastOrNull; - if (lastChild != null && lastChild.delta?.isEmpty != null) { - return const SizedBox.shrink(); - } - - Widget child = GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - final transaction = editorState.transaction; - final length = node.children.length; - final path = node.path.child(length); - transaction - ..insertNode( - path, - paragraphNode(), - ) - ..afterSelection = Selection.collapsed(Position(path: path)); - editorState.apply(transaction); - }, - ); - - if (height != null) { - child = SizedBox( - height: height, - child: child, - ); - } - - return child; - } - - Widget _buildRowMoreActionButton() { - final rowIndex = node.rowIndex; - - return SimpleTableMoreActionMenu( - tableCellNode: node, - index: rowIndex, - type: SimpleTableMoreActionType.row, - ); - } - - Widget _buildColumnMoreActionButton() { - final columnIndex = node.columnIndex; - - return SimpleTableMoreActionMenu( - tableCellNode: node, - index: columnIndex, - type: SimpleTableMoreActionType.column, - ); - } - - Widget _buildTableActionMenu() { - final tableNode = node.parentTableNode; - - // the table action menu is only available on mobile platform. - if (tableNode == null || UniversalPlatform.isDesktop) { - return const SizedBox.shrink(); - } - - return SimpleTableActionMenu( - tableNode: tableNode, - editorState: editorState, - ); - } - - Alignment _buildAlignment() { - Alignment alignment = Alignment.topLeft; - if (node.columnAlign != TableAlign.left) { - alignment = node.columnAlign.alignment; - } else if (node.rowAlign != TableAlign.left) { - alignment = node.rowAlign.alignment; - } - return alignment; - } - - Decoration _buildDecoration() { - final backgroundColor = _buildBackgroundColor(); - final border = borderBuilder.buildBorder( - isEditingCell: isEditingCellNotifier.value, - ); - - return BoxDecoration( - border: border, - color: backgroundColor, - ); - } - - Color? _buildBackgroundColor() { - // Priority: highlight color > column color > row color > header color > default color - final isSelectingTable = - simpleTableContext?.isSelectingTable.value ?? false; - if (isSelectingTable) { - return Theme.of(context).colorScheme.primary.withValues(alpha: 0.1); - } - - final columnColor = node.buildColumnColor(context); - if (columnColor != null && columnColor != Colors.transparent) { - return columnColor; - } - - final rowColor = node.buildRowColor(context); - if (rowColor != null && rowColor != Colors.transparent) { - return rowColor; - } - - // Check if the cell is in the header. - // If the cell is in the header, set the background color to the default header color. - // Otherwise, set the background color to null. - if (_isInHeader()) { - return context.simpleTableDefaultHeaderColor; - } - - return Colors.transparent; - } - - bool _isInHeader() { - final isHeaderColumnEnabled = node.isHeaderColumnEnabled; - final isHeaderRowEnabled = node.isHeaderRowEnabled; - final cellPosition = node.cellPosition; - final isFirstColumn = cellPosition.$1 == 0; - final isFirstRow = cellPosition.$2 == 0; - - return isHeaderColumnEnabled && isFirstRow || - isHeaderRowEnabled && isFirstColumn; - } - - void _onSelectingTableChanged() { - if (mounted) { - setState(() {}); - } - } - - void _onSelectionChanged() { - final selection = editorState.selection; - - // check if the selection is in the cell - if (selection != null && - node.path.isAncestorOf(selection.start.path) && - node.path.isAncestorOf(selection.end.path)) { - isEditingCellNotifier.value = true; - simpleTableContext?.isEditingCell.value = node; - } else { - isEditingCellNotifier.value = false; - } - - // if the selection is null or the selection is collapsed, set the isEditingCell to null. - if (selection == null) { - simpleTableContext?.isEditingCell.value = null; - } else if (selection.isCollapsed) { - // if the selection is collapsed, check if the selection is in the cell. - final selectedNode = - editorState.getNodesInSelection(selection).firstOrNull; - if (selectedNode != null) { - final tableNode = selectedNode.parentTableNode; - if (tableNode == null || tableNode.id != node.parentTableNode?.id) { - simpleTableContext?.isEditingCell.value = null; - } - } else { - simpleTableContext?.isEditingCell.value = null; - } - } - } - - /// Calculate if the cell is hit by the reordering offset. - /// If the cell is hit, set the isReorderingCell to true. - void _onReorderingOffsetChanged() { - final simpleTableContext = this.simpleTableContext; - if (UniversalPlatform.isDesktop || simpleTableContext == null) { - return; - } - - final isReordering = simpleTableContext.isReordering; - if (!isReordering) { - return; - } - - final isReorderingColumn = simpleTableContext.isReorderingColumn.value.$1; - final isReorderingRow = simpleTableContext.isReorderingRow.value.$1; - if (!isReorderingColumn && !isReorderingRow) { - return; - } - - final reorderingOffset = simpleTableContext.reorderingOffset.value; - - final renderBox = node.renderBox; - if (renderBox == null) { - return; - } - - final cellRect = renderBox.localToGlobal(Offset.zero) & renderBox.size; - - bool isHitCurrentCell = false; - if (isReorderingColumn) { - isHitCurrentCell = cellRect.left < reorderingOffset.dx && - cellRect.right > reorderingOffset.dx; - } else if (isReorderingRow) { - isHitCurrentCell = cellRect.top < reorderingOffset.dy && - cellRect.bottom > reorderingOffset.dy; - } - - isReorderingHitCellNotifier.value = isHitCurrentCell; - if (isHitCurrentCell) { - if (isReorderingColumn) { - if (simpleTableContext.isReorderingHitIndex.value != node.columnIndex) { - HapticFeedback.lightImpact(); - - simpleTableContext.isReorderingHitIndex.value = node.columnIndex; - } - } else if (isReorderingRow) { - if (simpleTableContext.isReorderingHitIndex.value != node.rowIndex) { - HapticFeedback.lightImpact(); - - simpleTableContext.isReorderingHitIndex.value = node.rowIndex; - } - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart deleted file mode 100644 index 295a636e09..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart +++ /dev/null @@ -1,325 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -const _enableTableDebugLog = false; - -class SimpleTableContext { - SimpleTableContext() { - if (_enableTableDebugLog) { - isHoveringOnColumnsAndRows.addListener( - _onHoveringOnColumnsAndRowsChanged, - ); - isHoveringOnTableArea.addListener( - _onHoveringOnTableAreaChanged, - ); - hoveringTableCell.addListener(_onHoveringTableNodeChanged); - selectingColumn.addListener(_onSelectingColumnChanged); - selectingRow.addListener(_onSelectingRowChanged); - isSelectingTable.addListener(_onSelectingTableChanged); - isHoveringOnTableBlock.addListener(_onHoveringOnTableBlockChanged); - isReorderingColumn.addListener(_onDraggingColumnChanged); - isReorderingRow.addListener(_onDraggingRowChanged); - } - } - - /// the area only contains the columns and rows, - /// the add row button, add column button, and add column and row button are not part of the table area - final ValueNotifier isHoveringOnColumnsAndRows = ValueNotifier(false); - - /// the table area contains the columns and rows, - /// the add row button, add column button, and add column and row button are not part of the table area, - /// not including the selection area and padding - final ValueNotifier isHoveringOnTableArea = ValueNotifier(false); - - /// the table block area contains the table area and the add row button, add column button, and add column and row button - /// also, the table block area contains the selection area and padding - final ValueNotifier isHoveringOnTableBlock = ValueNotifier(false); - - /// the hovering table cell is the cell that the mouse is hovering on - final ValueNotifier hoveringTableCell = ValueNotifier(null); - - /// the hovering on resize handle is the resize handle that the mouse is hovering on - final ValueNotifier hoveringOnResizeHandle = ValueNotifier(null); - - /// the selecting column is the column that the user is selecting - final ValueNotifier selectingColumn = ValueNotifier(null); - - /// the selecting row is the row that the user is selecting - final ValueNotifier selectingRow = ValueNotifier(null); - - /// the is selecting table is the table that the user is selecting - final ValueNotifier isSelectingTable = ValueNotifier(false); - - /// isReorderingColumn is a tuple of (isReordering, columnIndex) - final ValueNotifier<(bool, int)> isReorderingColumn = - ValueNotifier((false, -1)); - - /// isReorderingRow is a tuple of (isReordering, rowIndex) - final ValueNotifier<(bool, int)> isReorderingRow = ValueNotifier((false, -1)); - - /// reorderingOffset is the offset of the reordering - // - /// This value is only available when isReordering is true - final ValueNotifier reorderingOffset = ValueNotifier(Offset.zero); - - /// isDraggingRow to expand the rows of the table - bool isDraggingRow = false; - - /// isDraggingColumn to expand the columns of the table - bool isDraggingColumn = false; - - bool get isReordering => - isReorderingColumn.value.$1 || isReorderingRow.value.$1; - - /// isEditingCell is the cell that the user is editing - /// - /// This value is available on mobile only - final ValueNotifier isEditingCell = ValueNotifier(null); - - /// isReorderingHitCell is the cell that the user is reordering - /// - /// This value is available on mobile only - final ValueNotifier isReorderingHitIndex = ValueNotifier(null); - - /// resizingCell is the cell that the user is resizing - /// - /// This value is available on mobile only - final ValueNotifier resizingCell = ValueNotifier(null); - - /// Scroll controller for the table - ScrollController? horizontalScrollController; - - void _onHoveringOnColumnsAndRowsChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isHoveringOnTable: ${isHoveringOnColumnsAndRows.value}'); - } - - void _onHoveringTableNodeChanged() { - if (!_enableTableDebugLog) { - return; - } - - final node = hoveringTableCell.value; - if (node == null) { - return; - } - - Log.debug('hoveringTableNode: $node, ${node.cellPosition}'); - } - - void _onSelectingColumnChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('selectingColumn: ${selectingColumn.value}'); - } - - void _onSelectingRowChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('selectingRow: ${selectingRow.value}'); - } - - void _onSelectingTableChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isSelectingTable: ${isSelectingTable.value}'); - } - - void _onHoveringOnTableBlockChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isHoveringOnTableBlock: ${isHoveringOnTableBlock.value}'); - } - - void _onHoveringOnTableAreaChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isHoveringOnTableArea: ${isHoveringOnTableArea.value}'); - } - - void _onDraggingColumnChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isDraggingColumn: ${isReorderingColumn.value}'); - } - - void _onDraggingRowChanged() { - if (!_enableTableDebugLog) { - return; - } - - Log.debug('isDraggingRow: ${isReorderingRow.value}'); - } - - void dispose() { - isHoveringOnColumnsAndRows.dispose(); - isHoveringOnTableBlock.dispose(); - isHoveringOnTableArea.dispose(); - hoveringTableCell.dispose(); - hoveringOnResizeHandle.dispose(); - selectingColumn.dispose(); - selectingRow.dispose(); - isSelectingTable.dispose(); - isReorderingColumn.dispose(); - isReorderingRow.dispose(); - reorderingOffset.dispose(); - isEditingCell.dispose(); - isReorderingHitIndex.dispose(); - resizingCell.dispose(); - } -} - -class SimpleTableConstants { - /// Table - static const defaultColumnWidth = 160.0; - static const minimumColumnWidth = 36.0; - - static const defaultRowHeight = 36.0; - - static double get tableHitTestTopPadding => - UniversalPlatform.isDesktop ? 8.0 : 24.0; - static double get tableHitTestLeftPadding => - UniversalPlatform.isDesktop ? 0.0 : 24.0; - static double get tableLeftPadding => UniversalPlatform.isDesktop ? 8.0 : 0.0; - - static const tableBottomPadding = - addRowButtonHeight + 3 * addRowButtonPadding; - static const tableRightPadding = - addColumnButtonWidth + 2 * SimpleTableConstants.addColumnButtonPadding; - - static EdgeInsets get tablePadding => EdgeInsets.only( - // don't add padding to the top of the table, the first row will have padding - // to make the column action button clickable. - bottom: tableBottomPadding, - left: tableLeftPadding, - right: tableRightPadding, - ); - - static double get tablePageOffset => UniversalPlatform.isMobile - ? EditorStyleCustomizer.optionMenuWidth + - EditorStyleCustomizer.nodeHorizontalPadding * 2 - : EditorStyleCustomizer.optionMenuWidth + 12; - - // Add row button - static const addRowButtonHeight = 16.0; - static const addRowButtonPadding = 4.0; - static const addRowButtonRadius = 4.0; - static const addRowButtonRightPadding = - addColumnButtonWidth + addColumnButtonPadding * 2; - - // Add column button - static const addColumnButtonWidth = 16.0; - static const addColumnButtonPadding = 2.0; - static const addColumnButtonRadius = 4.0; - static const addColumnButtonBottomPadding = - addRowButtonHeight + 3 * addRowButtonPadding; - - // Add column and row button - static const addColumnAndRowButtonWidth = addColumnButtonWidth; - static const addColumnAndRowButtonHeight = addRowButtonHeight; - static const addColumnAndRowButtonCornerRadius = addColumnButtonWidth / 2.0; - static const addColumnAndRowButtonBottomPadding = 2.5 * addRowButtonPadding; - - // Table cell - static EdgeInsets get cellEdgePadding => UniversalPlatform.isDesktop - ? const EdgeInsets.symmetric( - horizontal: 9.0, - vertical: 2.0, - ) - : const EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: 6.0, - ); - static const cellBorderWidth = 1.0; - static const resizeHandleWidth = 3.0; - - static const borderType = SimpleTableBorderRenderType.cell; - - // Table more action - static const moreActionHeight = 34.0; - static const moreActionPadding = EdgeInsets.symmetric(vertical: 2.0); - static const moreActionHorizontalMargin = - EdgeInsets.symmetric(horizontal: 6.0); - - /// Only displaying the add row / add column / add column and row button - /// when hovering on the last row / last column / last cell. - static const enableHoveringLogicV2 = true; - - /// Enable the drag to expand the table - static const enableDragToExpandTable = false; - - /// Action sheet hit test area on Mobile - static const rowActionSheetHitTestAreaWidth = 24.0; - static const columnActionSheetHitTestAreaHeight = 24.0; - - static const actionSheetQuickActionSectionHeight = 44.0; - static const actionSheetInsertSectionHeight = 52.0; - static const actionSheetContentSectionHeight = 44.0; - static const actionSheetNormalActionSectionHeight = 48.0; - static const actionSheetButtonRadius = 12.0; - - static const actionSheetBottomSheetHeight = 320.0; -} - -enum SimpleTableBorderRenderType { - cell, - table, -} - -extension SimpleTableColors on BuildContext { - Color get simpleTableBorderColor => Theme.of(this).isLightMode - ? const Color(0xFFE4E5E5) - : const Color(0xFF3A3F49); - - Color get simpleTableDividerColor => Theme.of(this).isLightMode - ? const Color(0x141F2329) - : const Color(0xFF23262B).withValues(alpha: 0.5); - - Color get simpleTableMoreActionBackgroundColor => Theme.of(this).isLightMode - ? const Color(0xFFF2F3F5) - : const Color(0xFF2D3036); - - Color get simpleTableMoreActionBorderColor => Theme.of(this).isLightMode - ? const Color(0xFFCFD3D9) - : const Color(0xFF44484E); - - Color get simpleTableMoreActionHoverColor => Theme.of(this).isLightMode - ? const Color(0xFF00C8FF) - : const Color(0xFF00C8FF); - - Color get simpleTableDefaultHeaderColor => Theme.of(this).isLightMode - ? const Color(0xFFF2F2F2) - : const Color(0x08FFFFFF); - - Color get simpleTableActionButtonBackgroundColor => Theme.of(this).isLightMode - ? const Color(0xFFFFFFFF) - : const Color(0xFF2D3036); - - Color get simpleTableInsertActionBackgroundColor => Theme.of(this).isLightMode - ? const Color(0xFFF2F2F7) - : const Color(0xFF2D3036); - - Color? get simpleTableQuickActionBackgroundColor => - Theme.of(this).isLightMode ? null : const Color(0xFFBBC3CD); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart deleted file mode 100644 index 4906ed85eb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart +++ /dev/null @@ -1,500 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -enum SimpleTableMoreActionType { - column, - row; - - List buildDesktopActions({ - required int index, - required int columnLength, - required int rowLength, - }) { - // there're two special cases: - // 1. if the table only contains one row or one column, remove the delete action - // 2. if the index is 0, add the enable header action - switch (this) { - case SimpleTableMoreActionType.row: - return [ - SimpleTableMoreAction.insertAbove, - SimpleTableMoreAction.insertBelow, - SimpleTableMoreAction.divider, - if (index == 0) SimpleTableMoreAction.enableHeaderRow, - SimpleTableMoreAction.backgroundColor, - SimpleTableMoreAction.align, - SimpleTableMoreAction.divider, - SimpleTableMoreAction.setToPageWidth, - SimpleTableMoreAction.distributeColumnsEvenly, - SimpleTableMoreAction.divider, - SimpleTableMoreAction.duplicate, - SimpleTableMoreAction.clearContents, - if (rowLength > 1) SimpleTableMoreAction.delete, - ]; - case SimpleTableMoreActionType.column: - return [ - SimpleTableMoreAction.insertLeft, - SimpleTableMoreAction.insertRight, - SimpleTableMoreAction.divider, - if (index == 0) SimpleTableMoreAction.enableHeaderColumn, - SimpleTableMoreAction.backgroundColor, - SimpleTableMoreAction.align, - SimpleTableMoreAction.divider, - SimpleTableMoreAction.setToPageWidth, - SimpleTableMoreAction.distributeColumnsEvenly, - SimpleTableMoreAction.divider, - SimpleTableMoreAction.duplicate, - SimpleTableMoreAction.clearContents, - if (columnLength > 1) SimpleTableMoreAction.delete, - ]; - } - } - - List> buildMobileActions({ - required int index, - required int columnLength, - required int rowLength, - }) { - // the actions on mobile are not the same as the desktop ones - // the mobile actions are grouped into different sections - switch (this) { - case SimpleTableMoreActionType.row: - return [ - if (index == 0) [SimpleTableMoreAction.enableHeaderRow], - [ - SimpleTableMoreAction.setToPageWidth, - SimpleTableMoreAction.distributeColumnsEvenly, - ], - [ - SimpleTableMoreAction.duplicateRow, - SimpleTableMoreAction.clearContents, - ], - ]; - case SimpleTableMoreActionType.column: - return [ - if (index == 0) [SimpleTableMoreAction.enableHeaderColumn], - [ - SimpleTableMoreAction.setToPageWidth, - SimpleTableMoreAction.distributeColumnsEvenly, - ], - [ - SimpleTableMoreAction.duplicateColumn, - SimpleTableMoreAction.clearContents, - ], - ]; - } - } - - FlowySvgData get reorderIconSvg { - switch (this) { - case SimpleTableMoreActionType.column: - return FlowySvgs.table_reorder_column_s; - case SimpleTableMoreActionType.row: - return FlowySvgs.table_reorder_row_s; - } - } - - @override - String toString() { - return switch (this) { - SimpleTableMoreActionType.column => 'column', - SimpleTableMoreActionType.row => 'row', - }; - } -} - -enum SimpleTableMoreAction { - insertLeft, - insertRight, - insertAbove, - insertBelow, - duplicate, - clearContents, - delete, - align, - backgroundColor, - enableHeaderColumn, - enableHeaderRow, - setToPageWidth, - distributeColumnsEvenly, - divider, - - // these actions are only available on mobile - duplicateRow, - duplicateColumn, - cut, - copy, - paste, - bold, - textColor, - textBackgroundColor, - duplicateTable, - copyLinkToBlock; - - String get name { - return switch (this) { - SimpleTableMoreAction.align => - LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - SimpleTableMoreAction.backgroundColor => - LocaleKeys.document_plugins_simpleTable_moreActions_color.tr(), - SimpleTableMoreAction.enableHeaderColumn => - LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn.tr(), - SimpleTableMoreAction.enableHeaderRow => - LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(), - SimpleTableMoreAction.insertLeft => - LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(), - SimpleTableMoreAction.insertRight => - LocaleKeys.document_plugins_simpleTable_moreActions_insertRight.tr(), - SimpleTableMoreAction.insertBelow => - LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow.tr(), - SimpleTableMoreAction.insertAbove => - LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove.tr(), - SimpleTableMoreAction.clearContents => - LocaleKeys.document_plugins_simpleTable_moreActions_clearContents.tr(), - SimpleTableMoreAction.delete => - LocaleKeys.document_plugins_simpleTable_moreActions_delete.tr(), - SimpleTableMoreAction.duplicate => - LocaleKeys.document_plugins_simpleTable_moreActions_duplicate.tr(), - SimpleTableMoreAction.setToPageWidth => - LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth.tr(), - SimpleTableMoreAction.distributeColumnsEvenly => LocaleKeys - .document_plugins_simpleTable_moreActions_distributeColumnsWidth - .tr(), - SimpleTableMoreAction.duplicateRow => - LocaleKeys.document_plugins_simpleTable_moreActions_duplicateRow.tr(), - SimpleTableMoreAction.duplicateColumn => LocaleKeys - .document_plugins_simpleTable_moreActions_duplicateColumn - .tr(), - SimpleTableMoreAction.duplicateTable => - LocaleKeys.document_plugins_simpleTable_moreActions_duplicateTable.tr(), - SimpleTableMoreAction.copyLinkToBlock => - LocaleKeys.document_plugins_optionAction_copyLinkToBlock.tr(), - SimpleTableMoreAction.bold || - SimpleTableMoreAction.textColor || - SimpleTableMoreAction.textBackgroundColor || - SimpleTableMoreAction.cut || - SimpleTableMoreAction.copy || - SimpleTableMoreAction.paste => - throw UnimplementedError(), - SimpleTableMoreAction.divider => throw UnimplementedError(), - }; - } - - FlowySvgData get leftIconSvg { - return switch (this) { - SimpleTableMoreAction.insertLeft => FlowySvgs.table_insert_left_s, - SimpleTableMoreAction.insertRight => FlowySvgs.table_insert_right_s, - SimpleTableMoreAction.insertAbove => FlowySvgs.table_insert_above_s, - SimpleTableMoreAction.insertBelow => FlowySvgs.table_insert_below_s, - SimpleTableMoreAction.duplicate => FlowySvgs.duplicate_s, - SimpleTableMoreAction.clearContents => FlowySvgs.table_clear_content_s, - SimpleTableMoreAction.delete => FlowySvgs.trash_s, - SimpleTableMoreAction.setToPageWidth => - FlowySvgs.table_set_to_page_width_s, - SimpleTableMoreAction.distributeColumnsEvenly => - FlowySvgs.table_distribute_columns_evenly_s, - SimpleTableMoreAction.enableHeaderColumn => - FlowySvgs.table_header_column_s, - SimpleTableMoreAction.enableHeaderRow => FlowySvgs.table_header_row_s, - SimpleTableMoreAction.duplicateRow => FlowySvgs.m_table_duplicate_s, - SimpleTableMoreAction.duplicateColumn => FlowySvgs.m_table_duplicate_s, - SimpleTableMoreAction.cut => FlowySvgs.m_table_quick_action_cut_s, - SimpleTableMoreAction.copy => FlowySvgs.m_table_quick_action_copy_s, - SimpleTableMoreAction.paste => FlowySvgs.m_table_quick_action_paste_s, - SimpleTableMoreAction.bold => FlowySvgs.m_aa_bold_s, - SimpleTableMoreAction.duplicateTable => FlowySvgs.m_table_duplicate_s, - SimpleTableMoreAction.copyLinkToBlock => FlowySvgs.m_copy_link_s, - SimpleTableMoreAction.align => FlowySvgs.m_aa_align_left_s, - SimpleTableMoreAction.textColor => - throw UnsupportedError('text color icon is not supported'), - SimpleTableMoreAction.textBackgroundColor => - throw UnsupportedError('text background color icon is not supported'), - SimpleTableMoreAction.divider => - throw UnsupportedError('divider icon is not supported'), - SimpleTableMoreAction.backgroundColor => - throw UnsupportedError('background color icon is not supported'), - }; - } -} - -class SimpleTableMoreActionMenu extends StatefulWidget { - const SimpleTableMoreActionMenu({ - super.key, - required this.index, - required this.type, - required this.tableCellNode, - }); - - final int index; - final SimpleTableMoreActionType type; - final Node tableCellNode; - - @override - State createState() => - _SimpleTableMoreActionMenuState(); -} - -class _SimpleTableMoreActionMenuState extends State { - ValueNotifier isShowingMenu = ValueNotifier(false); - ValueNotifier isEditingCellNotifier = ValueNotifier(false); - - late final editorState = context.read(); - late final simpleTableContext = context.read(); - - @override - void initState() { - super.initState(); - - editorState.selectionNotifier.addListener(_onSelectionChanged); - } - - @override - void dispose() { - isShowingMenu.dispose(); - isEditingCellNotifier.dispose(); - - editorState.selectionNotifier.removeListener(_onSelectionChanged); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Align( - alignment: widget.type == SimpleTableMoreActionType.row - ? UniversalPlatform.isDesktop - ? Alignment.centerLeft - : Alignment.centerRight - : Alignment.topCenter, - child: UniversalPlatform.isDesktop - ? _buildDesktopMenu() - : _buildMobileMenu(), - ); - } - - // On desktop, the menu is a popup and only shows when hovering. - Widget _buildDesktopMenu() { - return ValueListenableBuilder( - valueListenable: isShowingMenu, - builder: (context, isShowingMenu, child) { - return ValueListenableBuilder( - valueListenable: simpleTableContext.hoveringTableCell, - builder: (context, hoveringTableNode, child) { - final reorderingIndex = switch (widget.type) { - SimpleTableMoreActionType.column => - simpleTableContext.isReorderingColumn.value.$2, - SimpleTableMoreActionType.row => - simpleTableContext.isReorderingRow.value.$2, - }; - final isReordering = simpleTableContext.isReordering; - if (isReordering) { - // when reordering, hide the menu for another column or row that is not the current dragging one. - if (reorderingIndex != widget.index) { - return const SizedBox.shrink(); - } else { - return child!; - } - } - - final hoveringIndex = - widget.type == SimpleTableMoreActionType.column - ? hoveringTableNode?.columnIndex - : hoveringTableNode?.rowIndex; - - if (hoveringIndex != widget.index && !isShowingMenu) { - return const SizedBox.shrink(); - } - - return child!; - }, - child: SimpleTableMoreActionPopup( - index: widget.index, - isShowingMenu: this.isShowingMenu, - type: widget.type, - ), - ); - }, - ); - } - - // On mobile, the menu is a action sheet and always shows. - Widget _buildMobileMenu() { - return ValueListenableBuilder( - valueListenable: isShowingMenu, - builder: (context, isShowingMenu, child) { - return ValueListenableBuilder( - valueListenable: simpleTableContext.isEditingCell, - builder: (context, isEditingCell, child) { - if (isShowingMenu) { - return child!; - } - - if (isEditingCell == null) { - return const SizedBox.shrink(); - } - - final columnIndex = isEditingCell.columnIndex; - final rowIndex = isEditingCell.rowIndex; - - switch (widget.type) { - case SimpleTableMoreActionType.column: - if (columnIndex != widget.index) { - return const SizedBox.shrink(); - } - case SimpleTableMoreActionType.row: - if (rowIndex != widget.index) { - return const SizedBox.shrink(); - } - } - - return child!; - }, - child: SimpleTableMobileDraggableReorderButton( - index: widget.index, - type: widget.type, - cellNode: widget.tableCellNode, - isShowingMenu: this.isShowingMenu, - editorState: editorState, - simpleTableContext: simpleTableContext, - ), - ); - }, - ); - } - - void _onSelectionChanged() { - final selection = editorState.selection; - - // check if the selection is in the cell - if (selection != null && - widget.tableCellNode.path.isAncestorOf(selection.start.path) && - widget.tableCellNode.path.isAncestorOf(selection.end.path)) { - isEditingCellNotifier.value = true; - } else { - isEditingCellNotifier.value = false; - } - } -} - -/// This widget is only used on mobile -class SimpleTableActionMenu extends StatelessWidget { - const SimpleTableActionMenu({ - super.key, - required this.tableNode, - required this.editorState, - }); - - final Node tableNode; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - final simpleTableContext = context.read(); - return ValueListenableBuilder( - valueListenable: simpleTableContext.isEditingCell, - builder: (context, isEditingCell, child) { - if (isEditingCell == null) { - return const SizedBox.shrink(); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - editorState.service.keyboardService?.closeKeyboard(); - // delay the bottom sheet show to make sure the keyboard is closed - Future.delayed(Durations.short3, () { - if (context.mounted) { - _showTableActionBottomSheet(context); - } - }); - }, - child: Container( - width: 20, - height: 20, - alignment: Alignment.center, - child: const FlowySvg( - FlowySvgs.drag_element_s, - size: Size.square(18.0), - ), - ), - ); - }, - ); - } - - Future _showTableActionBottomSheet(BuildContext context) async { - // check if the table node is a simple table - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - Log.error('The table node is not a simple table'); - return; - } - - final beforeSelection = editorState.selection; - - // increase the keep editor focus notifier to prevent the editor from losing focus - keepEditorFocusNotifier.increase(); - - unawaited( - editorState.updateSelectionWithReason( - Selection.collapsed( - Position( - path: tableNode.path, - ), - ), - customSelectionType: SelectionType.block, - extraInfo: { - selectionExtraInfoDisableMobileToolbarKey: true, - selectionExtraInfoDoNotAttachTextService: true, - }, - ), - ); - - if (!context.mounted) { - return; - } - - final simpleTableContext = context.read(); - - simpleTableContext.isSelectingTable.value = true; - - // show the bottom sheet - await showMobileBottomSheet( - context, - showDragHandle: true, - showDivider: false, - useSafeArea: false, - enablePadding: false, - builder: (context) => Provider.value( - value: simpleTableContext, - child: SimpleTableBottomSheet( - tableNode: tableNode, - editorState: editorState, - ), - ), - ); - - simpleTableContext.isSelectingTable.value = false; - keepEditorFocusNotifier.decrease(); - - // remove the extra info - if (beforeSelection != null) { - await editorState.updateSelectionWithReason( - beforeSelection, - customSelectionType: SelectionType.inline, - reason: SelectionUpdateReason.uiEvent, - extraInfo: {}, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart deleted file mode 100644 index c545036f35..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart +++ /dev/null @@ -1,419 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension TableContentOperation on EditorState { - /// Clear the content of the column at the given index. - /// - /// Before: - /// Given column index: 0 - /// Row 1: | 0 | 1 | ← The content of these cells will be cleared - /// Row 2: | 2 | 3 | - /// - /// Call this function with column index 0 will clear the first column of the table. - /// - /// After: - /// Row 1: | | | - /// Row 2: | 2 | 3 | - Future clearContentAtRowIndex({ - required Node tableNode, - required int rowIndex, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - if (rowIndex < 0 || rowIndex >= tableNode.rowLength) { - Log.warn('clear content in row: index out of range: $rowIndex'); - return; - } - - Log.info('clear content in row: $rowIndex in table ${tableNode.id}'); - - final transaction = this.transaction; - - final row = tableNode.children[rowIndex]; - for (var i = 0; i < row.children.length; i++) { - final cell = row.children[i]; - transaction.insertNode(cell.path.next, simpleTableCellBlockNode()); - transaction.deleteNode(cell); - } - await apply(transaction); - } - - /// Clear the content of the row at the given index. - /// - /// Before: - /// Given row index: 1 - /// ↓ The content of these cells will be cleared - /// Row 1: | 0 | 1 | - /// Row 2: | 2 | 3 | - /// - /// Call this function with row index 1 will clear the second row of the table. - /// - /// After: - /// Row 1: | 0 | | - /// Row 2: | 2 | | - Future clearContentAtColumnIndex({ - required Node tableNode, - required int columnIndex, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - if (columnIndex < 0 || columnIndex >= tableNode.columnLength) { - Log.warn('clear content in column: index out of range: $columnIndex'); - return; - } - - Log.info('clear content in column: $columnIndex in table ${tableNode.id}'); - - final transaction = this.transaction; - for (var i = 0; i < tableNode.rowLength; i++) { - final row = tableNode.children[i]; - final cell = columnIndex >= row.children.length - ? row.children.last - : row.children[columnIndex]; - transaction.insertNode(cell.path.next, simpleTableCellBlockNode()); - transaction.deleteNode(cell); - } - await apply(transaction); - } - - /// Clear the content of the table. - Future clearAllContent({ - required Node tableNode, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - for (var i = 0; i < tableNode.rowLength; i++) { - await clearContentAtRowIndex(tableNode: tableNode, rowIndex: i); - } - } - - /// Copy the selected column to the clipboard. - /// - /// If the [clearContent] is true, the content of the column will be cleared after - /// copying. - Future copyColumn({ - required Node tableNode, - required int columnIndex, - bool clearContent = false, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - if (columnIndex < 0 || columnIndex >= tableNode.columnLength) { - Log.warn('copy column: index out of range: $columnIndex'); - return null; - } - - // the plain text content of the column - final List content = []; - - // the cells of the column - final List cells = []; - - for (var i = 0; i < tableNode.rowLength; i++) { - final row = tableNode.children[i]; - final cell = columnIndex >= row.children.length - ? row.children.last - : row.children[columnIndex]; - final startNode = cell.getFirstFocusableChild(); - final endNode = cell.getLastFocusableChild(); - if (startNode == null || endNode == null) { - continue; - } - final plainText = getTextInSelection( - Selection( - start: Position(path: startNode.path), - end: Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ), - ), - ); - content.add(plainText.join('\n')); - cells.add(cell.deepCopy()); - } - - final plainText = content.join('\n'); - final document = Document.blank()..insert([0], cells); - - if (clearContent) { - await clearContentAtColumnIndex( - tableNode: tableNode, - columnIndex: columnIndex, - ); - } - - return ClipboardServiceData( - plainText: plainText, - tableJson: jsonEncode(document.toJson()), - ); - } - - /// Copy the selected row to the clipboard. - /// - /// If the [clearContent] is true, the content of the row will be cleared after - /// copying. - Future copyRow({ - required Node tableNode, - required int rowIndex, - bool clearContent = false, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - if (rowIndex < 0 || rowIndex >= tableNode.rowLength) { - Log.warn('copy row: index out of range: $rowIndex'); - return null; - } - - // the plain text content of the row - final List content = []; - - // the cells of the row - final List cells = []; - - final row = tableNode.children[rowIndex]; - for (var i = 0; i < row.children.length; i++) { - final cell = row.children[i]; - final startNode = cell.getFirstFocusableChild(); - final endNode = cell.getLastFocusableChild(); - if (startNode == null || endNode == null) { - continue; - } - final plainText = getTextInSelection( - Selection( - start: Position(path: startNode.path), - end: Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ), - ), - ); - content.add(plainText.join('\n')); - cells.add(cell.deepCopy()); - } - - final plainText = content.join('\n'); - final document = Document.blank()..insert([0], cells); - - if (clearContent) { - await clearContentAtRowIndex( - tableNode: tableNode, - rowIndex: rowIndex, - ); - } - - return ClipboardServiceData( - plainText: plainText, - tableJson: jsonEncode(document.toJson()), - ); - } - - /// Copy the selected table to the clipboard. - Future copyTable({ - required Node tableNode, - bool clearContent = false, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - // the plain text content of the table - final List content = []; - - // the cells of the table - final List cells = []; - - for (var i = 0; i < tableNode.rowLength; i++) { - final row = tableNode.children[i]; - for (var j = 0; j < row.children.length; j++) { - final cell = row.children[j]; - final startNode = cell.getFirstFocusableChild(); - final endNode = cell.getLastFocusableChild(); - if (startNode == null || endNode == null) { - continue; - } - final plainText = getTextInSelection( - Selection( - start: Position(path: startNode.path), - end: Position( - path: endNode.path, - offset: endNode.delta?.length ?? 0, - ), - ), - ); - content.add(plainText.join('\n')); - cells.add(cell.deepCopy()); - } - } - - final plainText = content.join('\n'); - final document = Document.blank()..insert([0], cells); - - if (clearContent) { - await clearAllContent(tableNode: tableNode); - } - - return ClipboardServiceData( - plainText: plainText, - tableJson: jsonEncode(document.toJson()), - ); - } - - /// Paste the clipboard content to the table column. - Future pasteColumn({ - required Node tableNode, - required int columnIndex, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - if (columnIndex < 0 || columnIndex >= tableNode.columnLength) { - Log.warn('paste column: index out of range: $columnIndex'); - return; - } - - final clipboardData = await getIt().getData(); - final tableJson = clipboardData.tableJson; - if (tableJson == null) { - return; - } - - try { - final document = Document.fromJson(jsonDecode(tableJson)); - final cells = document.root.children; - final transaction = this.transaction; - for (var i = 0; i < tableNode.rowLength; i++) { - final nodes = i < cells.length ? cells[i].children : []; - final row = tableNode.children[i]; - final cell = columnIndex >= row.children.length - ? row.children.last - : row.children[columnIndex]; - if (nodes.isNotEmpty) { - transaction.insertNodes( - cell.path.child(0), - nodes, - ); - transaction.deleteNodes(cell.children); - } - } - await apply(transaction); - } catch (e) { - Log.error('paste column: failed to paste: $e'); - } - } - - /// Paste the clipboard content to the table row. - Future pasteRow({ - required Node tableNode, - required int rowIndex, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - if (rowIndex < 0 || rowIndex >= tableNode.rowLength) { - Log.warn('paste row: index out of range: $rowIndex'); - return; - } - - final clipboardData = await getIt().getData(); - final tableJson = clipboardData.tableJson; - if (tableJson == null) { - return; - } - - try { - final document = Document.fromJson(jsonDecode(tableJson)); - final cells = document.root.children; - final transaction = this.transaction; - final row = tableNode.children[rowIndex]; - for (var i = 0; i < row.children.length; i++) { - final nodes = i < cells.length ? cells[i].children : []; - final cell = row.children[i]; - if (nodes.isNotEmpty) { - transaction.insertNodes( - cell.path.child(0), - nodes, - ); - transaction.deleteNodes(cell.children); - } - } - await apply(transaction); - } catch (e) { - Log.error('paste row: failed to paste: $e'); - } - } - - /// Paste the clipboard content to the table. - Future pasteTable({ - required Node tableNode, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - final clipboardData = await getIt().getData(); - final tableJson = clipboardData.tableJson; - if (tableJson == null) { - return; - } - - try { - final document = Document.fromJson(jsonDecode(tableJson)); - final cells = document.root.children; - final transaction = this.transaction; - for (var i = 0; i < tableNode.rowLength; i++) { - final row = tableNode.children[i]; - for (var j = 0; j < row.children.length; j++) { - final cell = row.children[j]; - final node = i + j < cells.length ? cells[i + j] : null; - if (node != null && node.children.isNotEmpty) { - transaction.insertNodes( - cell.path.child(0), - node.children, - ); - transaction.deleteNodes(cell.children); - } - } - } - await apply(transaction); - } catch (e) { - Log.error('paste row: failed to paste: $e'); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart deleted file mode 100644 index d5dfc02474..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension TableDeletionOperations on EditorState { - /// Delete a row at the given index. - /// - /// Before: - /// Given index: 0 - /// Row 1: | | | | ← This row will be deleted - /// Row 2: | | | | - /// - /// Call this function with index 0 will delete the first row of the table. - /// - /// After: - /// Row 1: | | | | - Future deleteRowInTable( - Node tableNode, - int rowIndex, { - bool inMemoryUpdate = false, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - final rowLength = tableNode.rowLength; - if (rowIndex < 0 || rowIndex >= rowLength) { - Log.warn( - 'delete row: index out of range: $rowIndex, row length: $rowLength', - ); - return; - } - - Log.info('delete row: $rowIndex in table ${tableNode.id}'); - - final attributes = tableNode.mapTableAttributes( - tableNode, - type: TableMapOperationType.deleteRow, - index: rowIndex, - ); - - final row = tableNode.children[rowIndex]; - final transaction = this.transaction; - transaction.deleteNode(row); - if (attributes != null) { - transaction.updateNode(tableNode, attributes); - } - await apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - ), - ); - } - - /// Delete a column at the given index. - /// - /// Before: - /// Given index: 2 - /// ↓ This column will be deleted - /// Row 1: | 0 | 1 | 2 | - /// Row 2: | | | | - /// - /// Call this function with index 2 will delete the third column of the table. - /// - /// After: - /// Row 1: | 0 | 1 | - /// Row 2: | | | - Future deleteColumnInTable( - Node tableNode, - int columnIndex, { - bool inMemoryUpdate = false, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - final rowLength = tableNode.rowLength; - final columnLength = tableNode.columnLength; - if (columnIndex < 0 || columnIndex >= columnLength) { - Log.warn( - 'delete column: index out of range: $columnIndex, column length: $columnLength', - ); - return; - } - - Log.info('delete column: $columnIndex in table ${tableNode.id}'); - - final attributes = tableNode.mapTableAttributes( - tableNode, - type: TableMapOperationType.deleteColumn, - index: columnIndex, - ); - - final transaction = this.transaction; - for (var i = 0; i < rowLength; i++) { - final row = tableNode.children[i]; - transaction.deleteNode(row.children[columnIndex]); - } - if (attributes != null) { - transaction.updateNode(tableNode, attributes); - } - await apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart deleted file mode 100644 index a2fdac4ca2..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart +++ /dev/null @@ -1,121 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension TableDuplicationOperations on EditorState { - /// Duplicate a row at the given index. - /// - /// Before: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | ← This row will be duplicated - /// - /// Call this function with index 1 will duplicate the second row of the table. - /// - /// After: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// | 3 | 4 | 5 | ← New row - Future duplicateRowInTable(Node node, int index) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - final columnLength = node.columnLength; - final rowLength = node.rowLength; - - if (index < 0 || index >= rowLength) { - Log.warn( - 'duplicate row: index out of range: $index, row length: $rowLength', - ); - return; - } - - Log.info( - 'duplicate row in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength', - ); - - final attributes = node.mapTableAttributes( - node, - type: TableMapOperationType.duplicateRow, - index: index, - ); - - final newRow = node.children[index].deepCopy(); - final transaction = this.transaction; - final path = index >= columnLength - ? node.children.last.path.next - : node.children[index].path; - transaction.insertNode(path, newRow); - if (attributes != null) { - transaction.updateNode(node, attributes); - } - await apply(transaction); - } - - Future duplicateColumnInTable(Node node, int index) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - final columnLength = node.columnLength; - final rowLength = node.rowLength; - - if (index < 0 || index >= columnLength) { - Log.warn( - 'duplicate column: index out of range: $index, column length: $columnLength', - ); - return; - } - - Log.info( - 'duplicate column in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength', - ); - - final attributes = node.mapTableAttributes( - node, - type: TableMapOperationType.duplicateColumn, - index: index, - ); - - final transaction = this.transaction; - for (var i = 0; i < rowLength; i++) { - final row = node.children[i]; - final path = index >= rowLength - ? row.children.last.path.next - : row.children[index].path; - final newCell = row.children[index].deepCopy(); - transaction.insertNode( - path, - newCell, - ); - } - if (attributes != null) { - transaction.updateNode(node, attributes); - } - await apply(transaction); - } - - /// Duplicate the table. - /// - /// This function will duplicate the table and insert it after the original table. - Future duplicateTable({ - required Node tableNode, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - final transaction = this.transaction; - final newTable = tableNode.deepCopy(); - transaction.insertNode(tableNode.path.next, newTable); - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart deleted file mode 100644 index a60ece2c2c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension TableHeaderOperation on EditorState { - /// Toggle the enable header column of the table. - Future toggleEnableHeaderColumn({ - required Node tableNode, - required bool enable, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - Log.info( - 'toggle enable header column: $enable in table ${tableNode.id}', - ); - - final columnColors = tableNode.columnColors; - - final transaction = this.transaction; - transaction.updateNode(tableNode, { - SimpleTableBlockKeys.enableHeaderColumn: enable, - // remove the previous background color if the header column is enable again - if (enable) - SimpleTableBlockKeys.columnColors: columnColors - ..removeWhere((key, _) => key == '0'), - }); - await apply(transaction); - } - - /// Toggle the enable header row of the table. - Future toggleEnableHeaderRow({ - required Node tableNode, - required bool enable, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - Log.info('toggle enable header row: $enable in table ${tableNode.id}'); - - final rowColors = tableNode.rowColors; - - final transaction = this.transaction; - transaction.updateNode(tableNode, { - SimpleTableBlockKeys.enableHeaderRow: enable, - // remove the previous background color if the header row is enable again - if (enable) - SimpleTableBlockKeys.rowColors: rowColors - ..removeWhere((key, _) => key == '0'), - }); - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart deleted file mode 100644 index 7a92aa3c7e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart +++ /dev/null @@ -1,213 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension TableInsertionOperations on EditorState { - /// Add a row at the end of the table. - /// - /// Before: - /// Row 1: | | | | - /// Row 2: | | | | - /// - /// Call this function will add a row at the end of the table. - /// - /// After: - /// Row 1: | | | | - /// Row 2: | | | | - /// Row 3: | | | | ← New row - /// - Future addRowInTable(Node tableNode) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - Log.warn('node is not a table node: ${tableNode.type}'); - return; - } - - await insertRowInTable(tableNode, tableNode.rowLength); - } - - /// Add a column at the end of the table. - /// - /// Before: - /// Row 1: | | | | - /// Row 2: | | | | - /// - /// Call this function will add a column at the end of the table. - /// - /// After: - /// ↓ New column - /// Row 1: | | | | | - /// Row 2: | | | | | - Future addColumnInTable(Node node) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - Log.warn('node is not a table node: ${node.type}'); - return; - } - - await insertColumnInTable(node, node.columnLength); - } - - /// Add a column and a row at the end of the table. - /// - /// Before: - /// Row 1: | | | | - /// Row 2: | | | | - /// - /// Call this function will add a column and a row at the end of the table. - /// - /// After: - /// ↓ New column - /// Row 1: | | | | | - /// Row 2: | | | | | - /// Row 3: | | | | | ← New row - Future addColumnAndRowInTable(Node node) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - await addColumnInTable(node); - await addRowInTable(node); - } - - /// Add a column at the given index. - /// - /// Before: - /// Given index: 1 - /// Row 1: | 0 | 1 | - /// Row 2: | | | - /// - /// Call this function with index 1 will add a column at the second position of the table. - /// - /// After: ↓ New column - /// Row 1: | 0 | | 1 | - /// Row 2: | | | | - Future insertColumnInTable( - Node node, - int index, { - bool inMemoryUpdate = false, - }) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - Log.warn('node is not a table node: ${node.type}'); - return; - } - - final columnLength = node.rowLength; - final rowLength = node.columnLength; - - Log.info( - 'add column in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength', - ); - - if (index < 0) { - Log.warn( - 'insert column: index out of range: $index, column length: $columnLength', - ); - return; - } - - final attributes = node.mapTableAttributes( - node, - type: TableMapOperationType.insertColumn, - index: index, - ); - - final transaction = this.transaction; - for (var i = 0; i < columnLength; i++) { - final row = node.children[i]; - // if the index is greater than the row length, we add the new column at the end of the row. - final path = index >= rowLength - ? row.children.last.path.next - : row.children[index].path; - transaction.insertNode( - path, - simpleTableCellBlockNode(), - ); - } - if (attributes != null) { - transaction.updateNode(node, attributes); - } - await apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - ), - ); - } - - /// Add a row at the given index. - /// - /// Before: - /// Given index: 1 - /// Row 1: | | | - /// Row 2: | | | - /// - /// Call this function with index 1 will add a row at the second position of the table. - /// - /// After: - /// Row 1: | | | - /// Row 2: | | | - /// Row 3: | | | ← New row - Future insertRowInTable( - Node node, - int index, { - bool inMemoryUpdate = false, - }) async { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - return; - } - - if (index < 0) { - Log.warn( - 'insert row: index out of range: $index', - ); - return; - } - - final columnLength = node.rowLength; - final rowLength = node.columnLength; - - Log.info( - 'insert row in table ${node.id} at index: $index, column length: $columnLength, row length: $rowLength', - ); - - final newRow = simpleTableRowBlockNode( - children: [ - for (var i = 0; i < rowLength; i++) simpleTableCellBlockNode(), - ], - ); - - final attributes = node.mapTableAttributes( - node, - type: TableMapOperationType.insertRow, - index: index, - ); - - final transaction = this.transaction; - final path = index >= columnLength - ? node.children.last.path.next - : node.children[index].path; - transaction.insertNode(path, newRow); - if (attributes != null) { - transaction.updateNode(node, attributes); - } - await apply( - transaction, - options: ApplyOptions( - inMemoryUpdate: inMemoryUpdate, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart deleted file mode 100644 index 875da5fffe..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart +++ /dev/null @@ -1,1085 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -enum TableMapOperationType { - insertRow, - deleteRow, - insertColumn, - deleteColumn, - duplicateRow, - duplicateColumn, - reorderColumn, - reorderRow, -} - -extension TableMapOperation on Node { - Attributes? mapTableAttributes( - Node node, { - required TableMapOperationType type, - required int index, - // Only used for reorder column operation - int? toIndex, - }) { - assert(this.type == SimpleTableBlockKeys.type); - - if (this.type != SimpleTableBlockKeys.type) { - return null; - } - - Attributes? attributes; - - switch (type) { - case TableMapOperationType.insertRow: - attributes = _mapRowInsertionAttributes(index); - case TableMapOperationType.insertColumn: - attributes = _mapColumnInsertionAttributes(index); - case TableMapOperationType.duplicateRow: - attributes = _mapRowDuplicationAttributes(index); - case TableMapOperationType.duplicateColumn: - attributes = _mapColumnDuplicationAttributes(index); - case TableMapOperationType.deleteRow: - attributes = _mapRowDeletionAttributes(index); - case TableMapOperationType.deleteColumn: - attributes = _mapColumnDeletionAttributes(index); - case TableMapOperationType.reorderColumn: - if (toIndex != null) { - attributes = _mapColumnReorderingAttributes(index, toIndex); - } - case TableMapOperationType.reorderRow: - if (toIndex != null) { - attributes = _mapRowReorderingAttributes(index, toIndex); - } - } - - // clear the attributes that are null - attributes?.removeWhere( - (key, value) => value == null, - ); - - return attributes; - } - - /// Map the attributes of a row insertion operation. - /// - /// When inserting a row, the attributes of the table after the index should be updated - /// For example: - /// Before: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | ← insert a new row here - /// - /// The original attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// } - /// } - /// - /// Insert a row at index 1: - /// | 0 | 1 | 2 | - /// | | | | ← new row - /// | 3 | 4 | 5 | - /// - /// The new attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 2: "#00FF00", ← The attributes of the original second row - /// } - /// } - Attributes? _mapRowInsertionAttributes(int index) { - final attributes = this.attributes; - try { - final rowColors = _remapSource( - this.rowColors, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final rowAligns = _remapSource( - this.rowAligns, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final rowBoldAttributes = _remapSource( - this.rowBoldAttributes, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final rowTextColors = _remapSource( - this.rowTextColors, - index, - comparator: (iKey, index) => iKey >= index, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.rowColors, - rowColors, - ) - .mergeValues( - SimpleTableBlockKeys.rowAligns, - rowAligns, - ) - .mergeValues( - SimpleTableBlockKeys.rowBoldAttributes, - rowBoldAttributes, - ) - .mergeValues( - SimpleTableBlockKeys.rowTextColors, - rowTextColors, - ); - } catch (e) { - Log.warn('Failed to map row insertion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a column insertion operation. - /// - /// When inserting a column, the attributes of the table after the index should be updated - /// For example: - /// Before: - /// | 0 | 1 | - /// | 2 | 3 | - /// - /// The original attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// } - /// } - /// - /// Insert a column at index 1: - /// | 0 | | 1 | - /// | 2 | | 3 | - /// - /// The new attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 2: "#00FF00", ← The attributes of the original second column - /// } - /// } - Attributes? _mapColumnInsertionAttributes(int index) { - final attributes = this.attributes; - try { - final columnColors = _remapSource( - this.columnColors, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final columnAligns = _remapSource( - this.columnAligns, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final columnWidths = _remapSource( - this.columnWidths, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final columnBoldAttributes = _remapSource( - this.columnBoldAttributes, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final columnTextColors = _remapSource( - this.columnTextColors, - index, - comparator: (iKey, index) => iKey >= index, - ); - - final bool distributeColumnWidthsEvenly = - attributes[SimpleTableBlockKeys.distributeColumnWidthsEvenly] ?? - false; - - if (distributeColumnWidthsEvenly) { - // if the distribute column widths evenly flag is true, - // we should distribute the column widths evenly - columnWidths[index.toString()] = columnWidths.values.firstOrNull; - } - - return attributes - .mergeValues( - SimpleTableBlockKeys.columnColors, - columnColors, - ) - .mergeValues( - SimpleTableBlockKeys.columnAligns, - columnAligns, - ) - .mergeValues( - SimpleTableBlockKeys.columnWidths, - columnWidths, - ) - .mergeValues( - SimpleTableBlockKeys.columnBoldAttributes, - columnBoldAttributes, - ) - .mergeValues( - SimpleTableBlockKeys.columnTextColors, - columnTextColors, - ); - } catch (e) { - Log.warn('Failed to map row insertion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a row duplication operation. - /// - /// When duplicating a row, the attributes of the table after the index should be updated - /// For example: - /// Before: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// - /// The original attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// } - /// } - /// - /// Duplicate the row at index 1: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// | 3 | 4 | 5 | ← duplicated row - /// - /// The new attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// 2: "#00FF00", ← The attributes of the original second row - /// } - /// } - Attributes? _mapRowDuplicationAttributes(int index) { - final attributes = this.attributes; - try { - final (rowColors, duplicatedRowColor) = _findDuplicatedEntryAndRemap( - this.rowColors, - index, - ); - - final (rowAligns, duplicatedRowAlign) = _findDuplicatedEntryAndRemap( - this.rowAligns, - index, - ); - - final (rowBoldAttributes, duplicatedRowBoldAttribute) = - _findDuplicatedEntryAndRemap( - this.rowBoldAttributes, - index, - ); - - final (rowTextColors, duplicatedRowTextColor) = - _findDuplicatedEntryAndRemap( - this.rowTextColors, - index, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.rowColors, - rowColors, - duplicatedEntry: duplicatedRowColor, - ) - .mergeValues( - SimpleTableBlockKeys.rowAligns, - rowAligns, - duplicatedEntry: duplicatedRowAlign, - ) - .mergeValues( - SimpleTableBlockKeys.rowBoldAttributes, - rowBoldAttributes, - duplicatedEntry: duplicatedRowBoldAttribute, - ) - .mergeValues( - SimpleTableBlockKeys.rowTextColors, - rowTextColors, - duplicatedEntry: duplicatedRowTextColor, - ); - } catch (e) { - Log.warn('Failed to map row insertion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a column duplication operation. - /// - /// When duplicating a column, the attributes of the table after the index should be updated - /// For example: - /// Before: - /// | 0 | 1 | - /// | 2 | 3 | - /// - /// The original attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// } - /// } - /// - /// Duplicate the column at index 1: - /// | 0 | 1 | 1 | ← duplicated column - /// | 2 | 3 | 2 | ← duplicated column - /// - /// The new attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// 2: "#00FF00", ← The attributes of the original second column - /// } - /// } - Attributes? _mapColumnDuplicationAttributes(int index) { - final attributes = this.attributes; - try { - final (columnColors, duplicatedColumnColor) = - _findDuplicatedEntryAndRemap( - this.columnColors, - index, - ); - - final (columnAligns, duplicatedColumnAlign) = - _findDuplicatedEntryAndRemap( - this.columnAligns, - index, - ); - - final (columnWidths, duplicatedColumnWidth) = - _findDuplicatedEntryAndRemap( - this.columnWidths, - index, - ); - - final (columnBoldAttributes, duplicatedColumnBoldAttribute) = - _findDuplicatedEntryAndRemap( - this.columnBoldAttributes, - index, - ); - - final (columnTextColors, duplicatedColumnTextColor) = - _findDuplicatedEntryAndRemap( - this.columnTextColors, - index, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.columnColors, - columnColors, - duplicatedEntry: duplicatedColumnColor, - ) - .mergeValues( - SimpleTableBlockKeys.columnAligns, - columnAligns, - duplicatedEntry: duplicatedColumnAlign, - ) - .mergeValues( - SimpleTableBlockKeys.columnWidths, - columnWidths, - duplicatedEntry: duplicatedColumnWidth, - ) - .mergeValues( - SimpleTableBlockKeys.columnBoldAttributes, - columnBoldAttributes, - duplicatedEntry: duplicatedColumnBoldAttribute, - ) - .mergeValues( - SimpleTableBlockKeys.columnTextColors, - columnTextColors, - duplicatedEntry: duplicatedColumnTextColor, - ); - } catch (e) { - Log.warn('Failed to map column duplication attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a column deletion operation. - /// - /// When deleting a column, the attributes of the table after the index should be updated - /// - /// For example: - /// Before: - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// - /// The original attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 2: "#00FF00", - /// } - /// } - /// - /// Delete the column at index 1: - /// | 0 | 2 | - /// | 3 | 5 | - /// - /// The new attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", ← The attributes of the original second column - /// } - /// } - Attributes? _mapColumnDeletionAttributes(int index) { - final attributes = this.attributes; - try { - final columnColors = _remapSource( - this.columnColors, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final columnAligns = _remapSource( - this.columnAligns, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final columnWidths = _remapSource( - this.columnWidths, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final columnBoldAttributes = _remapSource( - this.columnBoldAttributes, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final columnTextColors = _remapSource( - this.columnTextColors, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.columnColors, - columnColors, - ) - .mergeValues( - SimpleTableBlockKeys.columnAligns, - columnAligns, - ) - .mergeValues( - SimpleTableBlockKeys.columnWidths, - columnWidths, - ) - .mergeValues( - SimpleTableBlockKeys.columnBoldAttributes, - columnBoldAttributes, - ) - .mergeValues( - SimpleTableBlockKeys.columnTextColors, - columnTextColors, - ); - } catch (e) { - Log.warn('Failed to map column deletion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a row deletion operation. - /// - /// When deleting a row, the attributes of the table after the index should be updated - /// - /// For example: - /// Before: - /// | 0 | 1 | 2 | ← delete this row - /// | 3 | 4 | 5 | - /// - /// The original attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// } - /// } - /// - /// Delete the row at index 0: - /// | 3 | 4 | 5 | - /// - /// The new attributes of the table: - /// { - /// "rowColors": { - /// 0: "#00FF00", - /// } - /// } - Attributes? _mapRowDeletionAttributes(int index) { - final attributes = this.attributes; - try { - final rowColors = _remapSource( - this.rowColors, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final rowAligns = _remapSource( - this.rowAligns, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final rowBoldAttributes = _remapSource( - this.rowBoldAttributes, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - final rowTextColors = _remapSource( - this.rowTextColors, - index, - increment: false, - comparator: (iKey, index) => iKey > index, - filterIndex: index, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.rowColors, - rowColors, - ) - .mergeValues( - SimpleTableBlockKeys.rowAligns, - rowAligns, - ) - .mergeValues( - SimpleTableBlockKeys.rowBoldAttributes, - rowBoldAttributes, - ) - .mergeValues( - SimpleTableBlockKeys.rowTextColors, - rowTextColors, - ); - } catch (e) { - Log.warn('Failed to map row deletion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a column reordering operation. - /// - /// - /// Examples: - /// Case 1: - /// - /// When reordering a column, if the from index is greater than the to index, - /// the attributes of the table before the from index should be updated. - /// - /// Before: - /// ↓ reorder this column from index 1 to index 0 - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// - /// The original attributes of the table: - /// { - /// "rowColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// 2: "#0000FF", - /// } - /// } - /// - /// After reordering: - /// | 1 | 0 | 2 | - /// | 4 | 3 | 5 | - /// - /// The new attributes of the table: - /// { - /// "rowColors": { - /// 0: "#00FF00", ← The attributes of the original second column - /// 1: "#FF0000", ← The attributes of the original first column - /// 2: "#0000FF", - /// } - /// } - /// - /// Case 2: - /// - /// When reordering a column, if the from index is less than the to index, - /// the attributes of the table after the from index should be updated. - /// - /// Before: - /// ↓ reorder this column from index 1 to index 2 - /// | 0 | 1 | 2 | - /// | 3 | 4 | 5 | - /// - /// The original attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#00FF00", - /// 2: "#0000FF", - /// } - /// } - /// - /// After reordering: - /// | 0 | 2 | 1 | - /// | 3 | 5 | 4 | - /// - /// The new attributes of the table: - /// { - /// "columnColors": { - /// 0: "#FF0000", - /// 1: "#0000FF", ← The attributes of the original third column - /// 2: "#00FF00", ← The attributes of the original second column - /// } - /// } - Attributes? _mapColumnReorderingAttributes(int fromIndex, int toIndex) { - final attributes = this.attributes; - try { - final duplicatedColumnColor = this.columnColors[fromIndex.toString()]; - final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()]; - final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()]; - final duplicatedColumnBoldAttribute = - this.columnBoldAttributes[fromIndex.toString()]; - final duplicatedColumnTextColor = - this.columnTextColors[fromIndex.toString()]; - - /// Case 1: fromIndex > toIndex - /// Before: - /// Row 0: | 0 | 1 | 2 | - /// Row 1: | 3 | 4 | 5 | - /// Row 2: | 6 | 7 | 8 | - /// - /// columnColors = { - /// "0": "#FF0000", - /// "1": "#00FF00", - /// "2": "#0000FF" ← Move this column (index 2) - /// } - /// - /// Move column 2 to index 0: - /// Row 0: | 2 | 0 | 1 | - /// Row 1: | 5 | 3 | 4 | - /// Row 2: | 8 | 6 | 7 | - /// - /// columnColors = { - /// "0": "#0000FF", ← Moved here - /// "1": "#FF0000", - /// "2": "#00FF00" - /// } - /// - /// Case 2: fromIndex < toIndex - /// Before: - /// Row 0: | 0 | 1 | 2 | - /// Row 1: | 3 | 4 | 5 | - /// Row 2: | 6 | 7 | 8 | - /// - /// columnColors = { - /// "0": "#FF0000" ← Move this column (index 0) - /// "1": "#00FF00", - /// "2": "#0000FF" - /// } - /// - /// Move column 0 to index 2: - /// Row 0: | 1 | 2 | 0 | - /// Row 1: | 4 | 5 | 3 | - /// Row 2: | 7 | 8 | 6 | - /// - /// columnColors = { - /// "0": "#00FF00", - /// "1": "#0000FF", - /// "2": "#FF0000" ← Moved here - /// } - final columnColors = _remapSource( - this.columnColors, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final columnAligns = _remapSource( - this.columnAligns, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final columnWidths = _remapSource( - this.columnWidths, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final columnBoldAttributes = _remapSource( - this.columnBoldAttributes, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final columnTextColors = _remapSource( - this.columnTextColors, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.columnColors, - columnColors, - duplicatedEntry: duplicatedColumnColor != null - ? MapEntry( - toIndex.toString(), - duplicatedColumnColor, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.columnAligns, - columnAligns, - duplicatedEntry: duplicatedColumnAlign != null - ? MapEntry( - toIndex.toString(), - duplicatedColumnAlign, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.columnWidths, - columnWidths, - duplicatedEntry: duplicatedColumnWidth != null - ? MapEntry( - toIndex.toString(), - duplicatedColumnWidth, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.columnBoldAttributes, - columnBoldAttributes, - duplicatedEntry: duplicatedColumnBoldAttribute != null - ? MapEntry( - toIndex.toString(), - duplicatedColumnBoldAttribute, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.columnTextColors, - columnTextColors, - duplicatedEntry: duplicatedColumnTextColor != null - ? MapEntry( - toIndex.toString(), - duplicatedColumnTextColor, - ) - : null, - removeNullValue: true, - ); - } catch (e) { - Log.warn('Failed to map column deletion attributes: $e'); - return attributes; - } - } - - /// Map the attributes of a row reordering operation. - /// - /// See [_mapColumnReorderingAttributes] for more details. - Attributes? _mapRowReorderingAttributes(int fromIndex, int toIndex) { - final attributes = this.attributes; - try { - final duplicatedRowColor = this.rowColors[fromIndex.toString()]; - final duplicatedRowAlign = this.rowAligns[fromIndex.toString()]; - final duplicatedRowBoldAttribute = - this.rowBoldAttributes[fromIndex.toString()]; - final duplicatedRowTextColor = this.rowTextColors[fromIndex.toString()]; - - /// Example: - /// Case 1: fromIndex > toIndex - /// Before: - /// Row 0: | 0 | 1 | 2 | - /// Row 1: | 3 | 4 | 5 | ← Move this row (index 1) - /// Row 2: | 6 | 7 | 8 | - /// - /// rowColors = { - /// "0": "#FF0000", - /// "1": "#00FF00", ← This will be moved - /// "2": "#0000FF" - /// } - /// - /// Move row 1 to index 0: - /// Row 0: | 3 | 4 | 5 | ← Moved here - /// Row 1: | 0 | 1 | 2 | - /// Row 2: | 6 | 7 | 8 | - /// - /// rowColors = { - /// "0": "#00FF00", ← Moved here - /// "1": "#FF0000", - /// "2": "#0000FF" - /// } - /// - /// Case 2: fromIndex < toIndex - /// Before: - /// Row 0: | 0 | 1 | 2 | - /// Row 1: | 3 | 4 | 5 | ← Move this row (index 1) - /// Row 2: | 6 | 7 | 8 | - /// - /// rowColors = { - /// "0": "#FF0000", - /// "1": "#00FF00", ← This will be moved - /// "2": "#0000FF" - /// } - /// - /// Move row 1 to index 2: - /// Row 0: | 0 | 1 | 2 | - /// Row 1: | 3 | 4 | 5 | - /// Row 2: | 6 | 7 | 8 | ← Moved here - /// - /// rowColors = { - /// "0": "#FF0000", - /// "1": "#0000FF", - /// "2": "#00FF00" ← Moved here - /// } - final rowColors = _remapSource( - this.rowColors, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final rowAligns = _remapSource( - this.rowAligns, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final rowBoldAttributes = _remapSource( - this.rowBoldAttributes, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - final rowTextColors = _remapSource( - this.rowTextColors, - fromIndex, - increment: fromIndex > toIndex, - comparator: (iKey, index) { - if (fromIndex > toIndex) { - return iKey < fromIndex && iKey >= toIndex; - } else { - return iKey > fromIndex && iKey <= toIndex; - } - }, - filterIndex: fromIndex, - ); - - return attributes - .mergeValues( - SimpleTableBlockKeys.rowColors, - rowColors, - duplicatedEntry: duplicatedRowColor != null - ? MapEntry( - toIndex.toString(), - duplicatedRowColor, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.rowAligns, - rowAligns, - duplicatedEntry: duplicatedRowAlign != null - ? MapEntry( - toIndex.toString(), - duplicatedRowAlign, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.rowBoldAttributes, - rowBoldAttributes, - duplicatedEntry: duplicatedRowBoldAttribute != null - ? MapEntry( - toIndex.toString(), - duplicatedRowBoldAttribute, - ) - : null, - removeNullValue: true, - ) - .mergeValues( - SimpleTableBlockKeys.rowTextColors, - rowTextColors, - duplicatedEntry: duplicatedRowTextColor != null - ? MapEntry( - toIndex.toString(), - duplicatedRowTextColor, - ) - : null, - removeNullValue: true, - ); - } catch (e) { - Log.warn('Failed to map row reordering attributes: $e'); - return attributes; - } - } -} - -/// Find the duplicated entry and remap the source. -/// -/// All the entries after the index will be remapped to the new index. -(Map newSource, MapEntry? duplicatedEntry) - _findDuplicatedEntryAndRemap( - Map source, - int index, { - bool increment = true, -}) { - MapEntry? duplicatedEntry; - final newSource = source.map((key, value) { - final iKey = int.parse(key); - if (iKey == index) { - duplicatedEntry = MapEntry(key, value); - } - if (iKey >= index) { - return MapEntry((iKey + (increment ? 1 : -1)).toString(), value); - } - return MapEntry(key, value); - }); - return (newSource, duplicatedEntry); -} - -/// Remap the source to the new index. -/// -/// All the entries after the index will be remapped to the new index. -Map _remapSource( - Map source, - int index, { - bool increment = true, - required bool Function(int iKey, int index) comparator, - int? filterIndex, -}) { - var newSource = {...source}; - if (filterIndex != null) { - newSource.remove(filterIndex.toString()); - } - newSource = newSource.map((key, value) { - final iKey = int.parse(key); - if (comparator(iKey, index)) { - return MapEntry((iKey + (increment ? 1 : -1)).toString(), value); - } - return MapEntry(key, value); - }); - return newSource; -} - -extension TableMapOperationAttributes on Attributes { - Attributes mergeValues( - String key, - Map newSource, { - MapEntry? duplicatedEntry, - bool removeNullValue = false, - }) { - final result = {...this}; - - if (duplicatedEntry != null) { - newSource[duplicatedEntry.key] = duplicatedEntry.value; - } - - if (removeNullValue) { - // remove the null value - newSource.removeWhere((key, value) => value == null); - } - - result[key] = newSource; - - return result; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart deleted file mode 100644 index 1a2e21c305..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart +++ /dev/null @@ -1,878 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -typedef TableCellPosition = (int, int); - -enum TableAlign { - left, - center, - right; - - static TableAlign fromString(String align) { - return TableAlign.values.firstWhere( - (e) => e.key.toLowerCase() == align.toLowerCase(), - orElse: () => TableAlign.left, - ); - } - - String get name => switch (this) { - TableAlign.left => 'Left', - TableAlign.center => 'Center', - TableAlign.right => 'Right', - }; - - // The key used in the attributes of the table node. - // - // Example: - // - // attributes[SimpleTableBlockKeys.columnAligns] = {0: 'left', 1: 'center', 2: 'right'} - String get key => switch (this) { - TableAlign.left => 'left', - TableAlign.center => 'center', - TableAlign.right => 'right', - }; - - FlowySvgData get leftIconSvg => switch (this) { - TableAlign.left => FlowySvgs.table_align_left_s, - TableAlign.center => FlowySvgs.table_align_center_s, - TableAlign.right => FlowySvgs.table_align_right_s, - }; - - Alignment get alignment => switch (this) { - TableAlign.left => Alignment.topLeft, - TableAlign.center => Alignment.topCenter, - TableAlign.right => Alignment.topRight, - }; - - TextAlign get textAlign => switch (this) { - TableAlign.left => TextAlign.left, - TableAlign.center => TextAlign.center, - TableAlign.right => TextAlign.right, - }; -} - -extension TableNodeExtension on Node { - /// The number of rows in the table. - /// - /// The acceptable node is a table node, table row node or table cell node. - /// - /// Example: - /// - /// Row 1: | | | | - /// Row 2: | | | | - /// - /// The row length is 2. - int get rowLength { - final parentTableNode = this.parentTableNode; - - if (parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return -1; - } - - return parentTableNode.children.length; - } - - /// The number of rows in the table. - /// - /// The acceptable node is a table node, table row node or table cell node. - /// - /// Example: - /// - /// Row 1: | | | | - /// Row 2: | | | | - /// - /// The column length is 3. - int get columnLength { - final parentTableNode = this.parentTableNode; - - if (parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return -1; - } - - return parentTableNode.children.firstOrNull?.children.length ?? 0; - } - - TableCellPosition get cellPosition { - assert(type == SimpleTableCellBlockKeys.type); - return (rowIndex, columnIndex); - } - - int get rowIndex { - if (type == SimpleTableCellBlockKeys.type) { - if (path.parent.isEmpty) { - return -1; - } - return path.parent.last; - } else if (type == SimpleTableRowBlockKeys.type) { - return path.last; - } - return -1; - } - - int get columnIndex { - assert(type == SimpleTableCellBlockKeys.type); - if (path.isEmpty) { - return -1; - } - return path.last; - } - - bool get isHeaderColumnEnabled { - try { - return parentTableNode - ?.attributes[SimpleTableBlockKeys.enableHeaderColumn] ?? - false; - } catch (e) { - Log.warn('get is header column enabled: $e'); - return false; - } - } - - bool get isHeaderRowEnabled { - try { - return parentTableNode - ?.attributes[SimpleTableBlockKeys.enableHeaderRow] ?? - false; - } catch (e) { - Log.warn('get is header row enabled: $e'); - return false; - } - } - - TableAlign get rowAlign { - final parentTableNode = this.parentTableNode; - - if (parentTableNode == null) { - return TableAlign.left; - } - - try { - final rowAligns = - parentTableNode.attributes[SimpleTableBlockKeys.rowAligns]; - final align = rowAligns?[rowIndex.toString()]; - return TableAlign.values.firstWhere( - (e) => e.key == align, - orElse: () => TableAlign.left, - ); - } catch (e) { - Log.warn('get row align: $e'); - return TableAlign.left; - } - } - - TableAlign get columnAlign { - final parentTableNode = this.parentTableNode; - - if (parentTableNode == null) { - return TableAlign.left; - } - - try { - final columnAligns = - parentTableNode.attributes[SimpleTableBlockKeys.columnAligns]; - final align = columnAligns?[columnIndex.toString()]; - return TableAlign.values.firstWhere( - (e) => e.key == align, - orElse: () => TableAlign.left, - ); - } catch (e) { - Log.warn('get column align: $e'); - return TableAlign.left; - } - } - - Node? get parentTableNode { - Node? tableNode; - - if (type == SimpleTableBlockKeys.type) { - tableNode = this; - } else if (type == SimpleTableRowBlockKeys.type) { - tableNode = parent; - } else if (type == SimpleTableCellBlockKeys.type) { - tableNode = parent?.parent; - } else { - return parent?.parentTableNode; - } - - if (tableNode == null || tableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - return tableNode; - } - - Node? get parentTableCellNode { - Node? tableCellNode; - - if (type == SimpleTableCellBlockKeys.type) { - tableCellNode = this; - } else { - return parent?.parentTableCellNode; - } - - return tableCellNode; - } - - /// Whether the current node is in a table. - bool get isInTable { - return parentTableNode != null; - } - - double get columnWidth { - final parentTableNode = this.parentTableNode; - - if (parentTableNode == null) { - return SimpleTableConstants.defaultColumnWidth; - } - - try { - final columnWidths = - parentTableNode.attributes[SimpleTableBlockKeys.columnWidths]; - final width = columnWidths?[columnIndex.toString()] as Object?; - if (width == null) { - return SimpleTableConstants.defaultColumnWidth; - } - return width.toDouble( - defaultValue: SimpleTableConstants.defaultColumnWidth, - ); - } catch (e) { - Log.warn('get column width: $e'); - return SimpleTableConstants.defaultColumnWidth; - } - } - - /// Build the row color. - /// - /// Default is null. - Color? buildRowColor(BuildContext context) { - try { - final rawRowColors = - parentTableNode?.attributes[SimpleTableBlockKeys.rowColors]; - if (rawRowColors == null) { - return null; - } - final color = rawRowColors[rowIndex.toString()]; - if (color == null) { - return null; - } - return buildEditorCustomizedColor(context, this, color); - } catch (e) { - Log.warn('get row color: $e'); - return null; - } - } - - /// Build the column color. - /// - /// Default is null. - Color? buildColumnColor(BuildContext context) { - try { - final columnColors = - parentTableNode?.attributes[SimpleTableBlockKeys.columnColors]; - if (columnColors == null) { - return null; - } - final color = columnColors[columnIndex.toString()]; - if (color == null) { - return null; - } - return buildEditorCustomizedColor(context, this, color); - } catch (e) { - Log.warn('get column color: $e'); - return null; - } - } - - /// Whether the current node is in the header column. - /// - /// Default is false. - bool get isInHeaderColumn { - final parentTableNode = parent?.parentTableNode; - if (parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return false; - } - return parentTableNode.isHeaderColumnEnabled && - parentTableCellNode?.columnIndex == 0; - } - - /// Whether the current cell is bold in the column. - /// - /// Default is false. - bool get isInBoldColumn { - final parentTableCellNode = this.parentTableCellNode; - final parentTableNode = this.parentTableNode; - if (parentTableCellNode == null || - parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return false; - } - - final columnIndex = parentTableCellNode.columnIndex; - final columnBoldAttributes = parentTableNode.columnBoldAttributes; - return columnBoldAttributes[columnIndex.toString()] ?? false; - } - - /// Whether the current cell is bold in the row. - /// - /// Default is false. - bool get isInBoldRow { - final parentTableCellNode = this.parentTableCellNode; - final parentTableNode = this.parentTableNode; - if (parentTableCellNode == null || - parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return false; - } - - final rowIndex = parentTableCellNode.rowIndex; - final rowBoldAttributes = parentTableNode.rowBoldAttributes; - return rowBoldAttributes[rowIndex.toString()] ?? false; - } - - /// Get the text color of the current cell in the column. - /// - /// Default is null. - String? get textColorInColumn { - final parentTableCellNode = this.parentTableCellNode; - final parentTableNode = this.parentTableNode; - if (parentTableCellNode == null || - parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - final columnIndex = parentTableCellNode.columnIndex; - return parentTableNode.columnTextColors[columnIndex.toString()]; - } - - /// Get the text color of the current cell in the row. - /// - /// Default is null. - String? get textColorInRow { - final parentTableCellNode = this.parentTableCellNode; - final parentTableNode = this.parentTableNode; - if (parentTableCellNode == null || - parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return null; - } - - final rowIndex = parentTableCellNode.rowIndex; - return parentTableNode.rowTextColors[rowIndex.toString()]; - } - - /// Whether the current node is in the header row. - /// - /// Default is false. - bool get isInHeaderRow { - final parentTableNode = parent?.parentTableNode; - if (parentTableNode == null || - parentTableNode.type != SimpleTableBlockKeys.type) { - return false; - } - return parentTableNode.isHeaderRowEnabled && - parentTableCellNode?.rowIndex == 0; - } - - /// Get the row aligns. - SimpleTableRowAlignMap get rowAligns { - final rawRowAligns = - parentTableNode?.attributes[SimpleTableBlockKeys.rowAligns]; - if (rawRowAligns == null) { - return SimpleTableRowAlignMap(); - } - try { - return SimpleTableRowAlignMap.from(rawRowAligns); - } catch (e) { - Log.warn('get row aligns: $e'); - return SimpleTableRowAlignMap(); - } - } - - /// Get the row colors. - SimpleTableColorMap get rowColors { - final rawRowColors = - parentTableNode?.attributes[SimpleTableBlockKeys.rowColors]; - if (rawRowColors == null) { - return SimpleTableColorMap(); - } - try { - return SimpleTableColorMap.from(rawRowColors); - } catch (e) { - Log.warn('get row colors: $e'); - return SimpleTableColorMap(); - } - } - - /// Get the column colors. - SimpleTableColorMap get columnColors { - final rawColumnColors = - parentTableNode?.attributes[SimpleTableBlockKeys.columnColors]; - if (rawColumnColors == null) { - return SimpleTableColorMap(); - } - try { - return SimpleTableColorMap.from(rawColumnColors); - } catch (e) { - Log.warn('get column colors: $e'); - return SimpleTableColorMap(); - } - } - - /// Get the column aligns. - SimpleTableColumnAlignMap get columnAligns { - final rawColumnAligns = - parentTableNode?.attributes[SimpleTableBlockKeys.columnAligns]; - if (rawColumnAligns == null) { - return SimpleTableRowAlignMap(); - } - try { - return SimpleTableRowAlignMap.from(rawColumnAligns); - } catch (e) { - Log.warn('get column aligns: $e'); - return SimpleTableRowAlignMap(); - } - } - - /// Get the column widths. - SimpleTableColumnWidthMap get columnWidths { - final rawColumnWidths = - parentTableNode?.attributes[SimpleTableBlockKeys.columnWidths]; - if (rawColumnWidths == null) { - return SimpleTableColumnWidthMap(); - } - try { - return SimpleTableColumnWidthMap.from(rawColumnWidths); - } catch (e) { - Log.warn('get column widths: $e'); - return SimpleTableColumnWidthMap(); - } - } - - /// Get the column text colors - SimpleTableColorMap get columnTextColors { - final rawColumnTextColors = - parentTableNode?.attributes[SimpleTableBlockKeys.columnTextColors]; - if (rawColumnTextColors == null) { - return SimpleTableColorMap(); - } - try { - return SimpleTableColorMap.from(rawColumnTextColors); - } catch (e) { - Log.warn('get column text colors: $e'); - return SimpleTableColorMap(); - } - } - - /// Get the row text colors - SimpleTableColorMap get rowTextColors { - final rawRowTextColors = - parentTableNode?.attributes[SimpleTableBlockKeys.rowTextColors]; - if (rawRowTextColors == null) { - return SimpleTableColorMap(); - } - try { - return SimpleTableColorMap.from(rawRowTextColors); - } catch (e) { - Log.warn('get row text colors: $e'); - return SimpleTableColorMap(); - } - } - - /// Get the column bold attributes - SimpleTableAttributeMap get columnBoldAttributes { - final rawColumnBoldAttributes = - parentTableNode?.attributes[SimpleTableBlockKeys.columnBoldAttributes]; - if (rawColumnBoldAttributes == null) { - return SimpleTableAttributeMap(); - } - try { - return SimpleTableAttributeMap.from(rawColumnBoldAttributes); - } catch (e) { - Log.warn('get column bold attributes: $e'); - return SimpleTableAttributeMap(); - } - } - - /// Get the row bold attributes - SimpleTableAttributeMap get rowBoldAttributes { - final rawRowBoldAttributes = - parentTableNode?.attributes[SimpleTableBlockKeys.rowBoldAttributes]; - if (rawRowBoldAttributes == null) { - return SimpleTableAttributeMap(); - } - try { - return SimpleTableAttributeMap.from(rawRowBoldAttributes); - } catch (e) { - Log.warn('get row bold attributes: $e'); - return SimpleTableAttributeMap(); - } - } - - /// Get the width of the table. - double get width { - double currentColumnWidth = 0; - for (var i = 0; i < columnLength; i++) { - final columnWidth = - columnWidths[i.toString()] ?? SimpleTableConstants.defaultColumnWidth; - currentColumnWidth += columnWidth; - } - return currentColumnWidth; - } - - /// Get the previous cell in the same column. If the row index is 0, it will return the same cell. - Node? getPreviousCellInSameColumn() { - assert(type == SimpleTableCellBlockKeys.type); - final parentTableNode = this.parentTableNode; - if (parentTableNode == null) { - return null; - } - - final columnIndex = this.columnIndex; - final rowIndex = this.rowIndex; - - if (rowIndex == 0) { - return this; - } - - final previousColumn = parentTableNode.children[rowIndex - 1]; - final previousCell = previousColumn.children[columnIndex]; - return previousCell; - } - - /// Get the next cell in the same column. If the row index is the last row, it will return the same cell. - Node? getNextCellInSameColumn() { - assert(type == SimpleTableCellBlockKeys.type); - final parentTableNode = this.parentTableNode; - if (parentTableNode == null) { - return null; - } - - final columnIndex = this.columnIndex; - final rowIndex = this.rowIndex; - - if (rowIndex == parentTableNode.rowLength - 1) { - return this; - } - - final nextColumn = parentTableNode.children[rowIndex + 1]; - final nextCell = nextColumn.children[columnIndex]; - return nextCell; - } - - /// Get the right cell in the same row. If the column index is the last column, it will return the same cell. - Node? getNextCellInSameRow() { - assert(type == SimpleTableCellBlockKeys.type); - final parentTableNode = this.parentTableNode; - if (parentTableNode == null) { - return null; - } - - final columnIndex = this.columnIndex; - final rowIndex = this.rowIndex; - - // the last cell - if (columnIndex == parentTableNode.columnLength - 1 && - rowIndex == parentTableNode.rowLength - 1) { - return this; - } - - if (columnIndex == parentTableNode.columnLength - 1) { - final nextRow = parentTableNode.children[rowIndex + 1]; - final nextCell = nextRow.children.first; - return nextCell; - } - - final nextColumn = parentTableNode.children[rowIndex]; - final nextCell = nextColumn.children[columnIndex + 1]; - return nextCell; - } - - /// Get the previous cell in the same row. If the column index is 0, it will return the same cell. - Node? getPreviousCellInSameRow() { - assert(type == SimpleTableCellBlockKeys.type); - final parentTableNode = this.parentTableNode; - if (parentTableNode == null) { - return null; - } - - final columnIndex = this.columnIndex; - final rowIndex = this.rowIndex; - - if (columnIndex == 0 && rowIndex == 0) { - return this; - } - - if (columnIndex == 0) { - final previousRow = parentTableNode.children[rowIndex - 1]; - final previousCell = previousRow.children.last; - return previousCell; - } - - final previousColumn = parentTableNode.children[rowIndex]; - final previousCell = previousColumn.children[columnIndex - 1]; - return previousCell; - } - - /// Get the previous focusable sibling. - /// - /// If the current node is the first child of its parent, it will return itself. - Node? getPreviousFocusableSibling() { - final parent = this.parent; - if (parent == null) { - return null; - } - final parentTableNode = this.parentTableNode; - if (parentTableNode == null) { - return null; - } - if (parentTableNode.path == [0]) { - return this; - } - final previous = parentTableNode.previous; - if (previous == null) { - return null; - } - var children = previous.children; - if (children.isEmpty) { - return previous; - } - while (children.isNotEmpty) { - children = children.last.children; - } - return children.lastWhere((c) => c.delta != null); - } - - /// Get the next focusable sibling. - /// - /// If the current node is the last child of its parent, it will return itself. - Node? getNextFocusableSibling() { - final next = this.next; - if (next == null) { - return null; - } - return next; - } - - /// Is the last cell in the table. - bool get isLastCellInTable { - return columnIndex + 1 == parentTableNode?.columnLength && - rowIndex + 1 == parentTableNode?.rowLength; - } - - /// Is the first cell in the table. - bool get isFirstCellInTable { - return columnIndex == 0 && rowIndex == 0; - } - - /// Get the table cell node by the row index and column index. - /// - /// If the current node is not a table cell node, it will return null. - /// Or if the row index or column index is out of range, it will return null. - Node? getTableCellNode({ - required int rowIndex, - required int columnIndex, - }) { - assert(type == SimpleTableBlockKeys.type); - - if (type != SimpleTableBlockKeys.type) { - return null; - } - - if (rowIndex < 0 || rowIndex >= rowLength) { - return null; - } - - if (columnIndex < 0 || columnIndex >= columnLength) { - return null; - } - - return children[rowIndex].children[columnIndex]; - } - - String? getTableCellContent({ - required int rowIndex, - required int columnIndex, - }) { - final cell = getTableCellNode(rowIndex: rowIndex, columnIndex: columnIndex); - if (cell == null) { - return null; - } - final content = cell.children - .map((e) => e.delta?.toPlainText()) - .where((e) => e != null) - .join('\n'); - return content; - } - - /// Return the first empty row in the table from bottom to top. - /// - /// Example: - /// - /// | A | B | C | - /// | | | | - /// | E | F | G | - /// | H | I | J | - /// | | | | <--- The first empty row is the row at index 3. - /// | | | | - /// - /// The first empty row is the row at index 3. - (int, Node)? getFirstEmptyRowFromBottom() { - assert(type == SimpleTableBlockKeys.type); - - if (type != SimpleTableBlockKeys.type) { - return null; - } - - (int, Node)? result; - - for (var i = children.length - 1; i >= 0; i--) { - final row = children[i]; - - // Check if all cells in this row are empty - final hasContent = row.children.any((cell) { - final content = getTableCellContent( - rowIndex: i, - columnIndex: row.children.indexOf(cell), - ); - return content != null && content.isNotEmpty; - }); - - if (!hasContent) { - if (result != null) { - final (index, _) = result; - if (i <= index) { - result = (i, row); - } - } else { - result = (i, row); - } - } - } - - return result; - } - - /// Return the first empty column in the table from right to left. - /// - /// Example: - /// ↓ The first empty column is the column at index 3. - /// | A | C | | E | | | - /// | B | D | | F | | | - /// - /// The first empty column is the column at index 3. - int? getFirstEmptyColumnFromRight() { - assert(type == SimpleTableBlockKeys.type); - - if (type != SimpleTableBlockKeys.type) { - return null; - } - - int? result; - - for (var i = columnLength - 1; i >= 0; i--) { - bool hasContent = false; - for (var j = 0; j < rowLength; j++) { - final content = getTableCellContent( - rowIndex: j, - columnIndex: i, - ); - if (content != null && content.isNotEmpty) { - hasContent = true; - } - } - if (!hasContent) { - if (result != null) { - final index = result; - if (i <= index) { - result = i; - } - } else { - result = i; - } - } - } - - return result; - } - - /// Get first focusable child in the table cell. - /// - /// If the current node is not a table cell node, it will return null. - Node? getFirstFocusableChild() { - if (children.isEmpty) { - return this; - } - return children.first.getFirstFocusableChild(); - } - - /// Get last focusable child in the table cell. - /// - /// If the current node is not a table cell node, it will return null. - Node? getLastFocusableChild() { - if (children.isEmpty) { - return this; - } - return children.last.getLastFocusableChild(); - } - - /// Get table align of column - /// - /// If one of the align is not same as the others, it will return TableAlign.left. - TableAlign get allColumnAlign { - final alignSet = columnAligns.values.toSet(); - if (alignSet.length == 1) { - return TableAlign.fromString(alignSet.first); - } - return TableAlign.left; - } - - /// Get table align of row - /// - /// If one of the align is not same as the others, it will return TableAlign.left. - TableAlign get allRowAlign { - final alignSet = rowAligns.values.toSet(); - if (alignSet.length == 1) { - return TableAlign.fromString(alignSet.first); - } - return TableAlign.left; - } - - /// Get table align of the table. - /// - /// If one of the align is not same as the others, it will return TableAlign.left. - TableAlign get tableAlign { - if (allColumnAlign != TableAlign.left) { - return allColumnAlign; - } else if (allRowAlign != TableAlign.left) { - return allRowAlign; - } - return TableAlign.left; - } -} - -extension on Object { - double toDouble({double defaultValue = 0}) { - if (this is double) { - return this as double; - } - if (this is String) { - return double.tryParse(this as String) ?? defaultValue; - } - if (this is int) { - return (this as int).toDouble(); - } - return defaultValue; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart deleted file mode 100644 index c5bb8bac83..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart +++ /dev/null @@ -1,9 +0,0 @@ -export 'simple_table_content_operation.dart'; -export 'simple_table_delete_operation.dart'; -export 'simple_table_duplicate_operation.dart'; -export 'simple_table_header_operation.dart'; -export 'simple_table_insert_operation.dart'; -export 'simple_table_map_operation.dart'; -export 'simple_table_node_extension.dart'; -export 'simple_table_reorder_operation.dart'; -export 'simple_table_style_operation.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_reorder_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_reorder_operation.dart deleted file mode 100644 index 02e384ac02..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_reorder_operation.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -extension SimpleTableReorderOperation on EditorState { - /// Reorder the column of the table. - /// - /// If the from index is equal to the to index, do nothing. - /// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type]. - Future reorderColumn( - Node node, { - required int fromIndex, - required int toIndex, - }) async { - if (fromIndex == toIndex) { - return; - } - - final tableNode = node.parentTableNode; - - if (tableNode == null) { - assert(tableNode == null); - return; - } - - final columnLength = tableNode.columnLength; - final rowLength = tableNode.rowLength; - - if (fromIndex < 0 || - fromIndex >= columnLength || - toIndex < 0 || - toIndex >= columnLength) { - Log.warn( - 'reorder column: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength', - ); - return; - } - - Log.info( - 'reorder column in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength', - ); - - final attributes = tableNode.mapTableAttributes( - tableNode, - type: TableMapOperationType.reorderColumn, - index: fromIndex, - toIndex: toIndex, - ); - - final transaction = this.transaction; - for (var i = 0; i < rowLength; i++) { - final row = tableNode.children[i]; - final from = row.children[fromIndex]; - final to = row.children[toIndex]; - final path = fromIndex < toIndex ? to.path.next : to.path; - transaction.insertNode(path, from.deepCopy()); - transaction.deleteNode(from); - } - if (attributes != null) { - transaction.updateNode(tableNode, attributes); - } - await apply(transaction); - } - - /// Reorder the row of the table. - /// - /// If the from index is equal to the to index, do nothing. - /// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type]. - Future reorderRow( - Node node, { - required int fromIndex, - required int toIndex, - }) async { - if (fromIndex == toIndex) { - return; - } - - final tableNode = node.parentTableNode; - - if (tableNode == null) { - assert(tableNode == null); - return; - } - - final columnLength = tableNode.columnLength; - final rowLength = tableNode.rowLength; - - if (fromIndex < 0 || - fromIndex >= rowLength || - toIndex < 0 || - toIndex >= rowLength) { - Log.warn( - 'reorder row: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, row length: $rowLength', - ); - return; - } - - Log.info( - 'reorder row in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength', - ); - - final attributes = tableNode.mapTableAttributes( - tableNode, - type: TableMapOperationType.reorderRow, - index: fromIndex, - toIndex: toIndex, - ); - - final transaction = this.transaction; - final from = tableNode.children[fromIndex]; - final to = tableNode.children[toIndex]; - final path = fromIndex < toIndex ? to.path.next : to.path; - transaction.insertNode(path, from.deepCopy()); - transaction.deleteNode(from); - if (attributes != null) { - transaction.updateNode(tableNode, attributes); - } - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart deleted file mode 100644 index 7afa7da66b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart +++ /dev/null @@ -1,445 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:universal_platform/universal_platform.dart'; - -extension TableOptionOperation on EditorState { - /// Update the column width of the table in memory. Call this function when dragging the table column. - /// - /// The deltaX is the change of the column width. - Future updateColumnWidthInMemory({ - required Node tableCellNode, - required double deltaX, - }) async { - assert(tableCellNode.type == SimpleTableCellBlockKeys.type); - - if (tableCellNode.type != SimpleTableCellBlockKeys.type) { - return; - } - - // when dragging the table column, we need to update the column width in memory. - // so that the table can render the column with the new width. - // but don't need to persist to the database immediately. - // only persist to the database when the drag is completed. - final columnIndex = tableCellNode.columnIndex; - final parentTableNode = tableCellNode.parentTableNode; - if (parentTableNode == null) { - Log.warn('parent table node is null'); - return; - } - - final width = tableCellNode.columnWidth + deltaX; - - try { - final columnWidths = - parentTableNode.attributes[SimpleTableBlockKeys.columnWidths] ?? - SimpleTableColumnWidthMap(); - final newAttributes = { - ...parentTableNode.attributes, - SimpleTableBlockKeys.columnWidths: { - ...columnWidths, - columnIndex.toString(): width.clamp( - SimpleTableConstants.minimumColumnWidth, - double.infinity, - ), - }, - }; - - parentTableNode.updateAttributes(newAttributes); - } catch (e) { - Log.warn('update column width in memory: $e'); - } - } - - /// Update the column width of the table. Call this function after the drag is completed. - Future updateColumnWidth({ - required Node tableCellNode, - required double width, - }) async { - assert(tableCellNode.type == SimpleTableCellBlockKeys.type); - - if (tableCellNode.type != SimpleTableCellBlockKeys.type) { - return; - } - - final columnIndex = tableCellNode.columnIndex; - final parentTableNode = tableCellNode.parentTableNode; - if (parentTableNode == null) { - Log.warn('parent table node is null'); - return; - } - - final transaction = this.transaction; - final columnWidths = - parentTableNode.attributes[SimpleTableBlockKeys.columnWidths] ?? - SimpleTableColumnWidthMap(); - transaction.updateNode(parentTableNode, { - SimpleTableBlockKeys.columnWidths: { - ...columnWidths, - columnIndex.toString(): width.clamp( - SimpleTableConstants.minimumColumnWidth, - double.infinity, - ), - }, - // reset the distribute column widths evenly flag - SimpleTableBlockKeys.distributeColumnWidthsEvenly: false, - }); - await apply(transaction); - } - - /// Update the align of the column at the index where the table cell node is located. - /// - /// Before: - /// Given table cell node: - /// Row 1: | 0 | 1 | - /// Row 2: |2 |3 | ← This column will be updated - /// - /// Call this function will update the align of the column where the table cell node is located. - /// - /// After: - /// Row 1: | 0 | 1 | - /// Row 2: | 2 | 3 | ← This column is updated, texts are aligned to the center - Future updateColumnAlign({ - required Node tableCellNode, - required TableAlign align, - }) async { - await clearColumnTextAlign(tableCellNode: tableCellNode); - - final columnIndex = tableCellNode.columnIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.columnAligns, - source: tableCellNode.columnAligns, - duplicatedEntry: MapEntry(columnIndex.toString(), align.key), - ); - } - - /// Update the align of the row at the index where the table cell node is located. - /// - /// Before: - /// Given table cell node: - /// ↓ This row will be updated - /// Row 1: | 0 |1 | - /// Row 2: | 2 |3 | - /// - /// Call this function will update the align of the row where the table cell node is located. - /// - /// After: - /// ↓ This row is updated, texts are aligned to the center - /// Row 1: | 0 | 1 | - /// Row 2: | 2 | 3 | - Future updateRowAlign({ - required Node tableCellNode, - required TableAlign align, - }) async { - await clearRowTextAlign(tableCellNode: tableCellNode); - - final rowIndex = tableCellNode.rowIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.rowAligns, - source: tableCellNode.rowAligns, - duplicatedEntry: MapEntry(rowIndex.toString(), align.key), - ); - } - - /// Update the align of the table. - /// - /// This function will update the align of the table. - /// - /// The align is the align to be updated. - Future updateTableAlign({ - required Node tableNode, - required TableAlign align, - }) async { - assert(tableNode.type == SimpleTableBlockKeys.type); - - if (tableNode.type != SimpleTableBlockKeys.type) { - return; - } - - final transaction = this.transaction; - Attributes attributes = tableNode.attributes; - for (var i = 0; i < tableNode.columnLength; i++) { - attributes = attributes.mergeValues( - SimpleTableBlockKeys.columnAligns, - attributes[SimpleTableBlockKeys.columnAligns] ?? - SimpleTableColumnAlignMap(), - duplicatedEntry: MapEntry(i.toString(), align.key), - ); - } - transaction.updateNode(tableNode, attributes); - await apply(transaction); - } - - /// Update the background color of the column at the index where the table cell node is located. - Future updateColumnBackgroundColor({ - required Node tableCellNode, - required String color, - }) async { - final columnIndex = tableCellNode.columnIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.columnColors, - source: tableCellNode.columnColors, - duplicatedEntry: MapEntry(columnIndex.toString(), color), - ); - } - - /// Update the background color of the row at the index where the table cell node is located. - Future updateRowBackgroundColor({ - required Node tableCellNode, - required String color, - }) async { - final rowIndex = tableCellNode.rowIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.rowColors, - source: tableCellNode.rowColors, - duplicatedEntry: MapEntry(rowIndex.toString(), color), - ); - } - - /// Set the column width of the table to the page width. - /// - /// Example: - /// - /// Before: - /// | 0 | 1 | - /// | 3 | 4 | - /// - /// After: - /// | 0 | 1 | <- the column's width will be expanded based on the percentage of the page width - /// | 3 | 4 | - /// - /// This function will update the table width. - Future setColumnWidthToPageWidth({ - required Node tableNode, - }) async { - final columnLength = tableNode.columnLength; - double? pageWidth = tableNode.renderBox?.size.width; - if (pageWidth == null) { - Log.warn('table node render box is null'); - return; - } - pageWidth -= SimpleTableConstants.tablePageOffset; - - final transaction = this.transaction; - final columnWidths = tableNode.columnWidths; - final ratio = pageWidth / tableNode.width; - for (var i = 0; i < columnLength; i++) { - final columnWidth = - columnWidths[i.toString()] ?? SimpleTableConstants.defaultColumnWidth; - columnWidths[i.toString()] = (columnWidth * ratio).clamp( - SimpleTableConstants.minimumColumnWidth, - double.infinity, - ); - } - transaction.updateNode(tableNode, { - SimpleTableBlockKeys.columnWidths: columnWidths, - SimpleTableBlockKeys.distributeColumnWidthsEvenly: false, - }); - await apply(transaction); - } - - /// Distribute the column width of the table to the page width. - /// - /// Example: - /// - /// Before: - /// Before: - /// | 0 | 1 | - /// | 3 | 4 | - /// - /// After: - /// | 0 | 1 | <- the column's width will be expanded based on the percentage of the page width - /// | 3 | 4 | - /// - /// This function will not update table width. - Future distributeColumnWidthToPageWidth({ - required Node tableNode, - }) async { - // Disable in mobile - if (UniversalPlatform.isMobile) { - return; - } - - final columnLength = tableNode.columnLength; - final tableWidth = tableNode.width; - final columnWidth = (tableWidth / columnLength).clamp( - SimpleTableConstants.minimumColumnWidth, - double.infinity, - ); - final transaction = this.transaction; - final columnWidths = tableNode.columnWidths; - for (var i = 0; i < columnLength; i++) { - columnWidths[i.toString()] = columnWidth; - } - transaction.updateNode(tableNode, { - SimpleTableBlockKeys.columnWidths: columnWidths, - SimpleTableBlockKeys.distributeColumnWidthsEvenly: true, - }); - await apply(transaction); - } - - /// Update the bold attribute of the column - Future toggleColumnBoldAttribute({ - required Node tableCellNode, - required bool isBold, - }) async { - final columnIndex = tableCellNode.columnIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.columnBoldAttributes, - source: tableCellNode.columnBoldAttributes, - duplicatedEntry: MapEntry(columnIndex.toString(), isBold), - ); - } - - /// Update the bold attribute of the row - Future toggleRowBoldAttribute({ - required Node tableCellNode, - required bool isBold, - }) async { - final rowIndex = tableCellNode.rowIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.rowBoldAttributes, - source: tableCellNode.rowBoldAttributes, - duplicatedEntry: MapEntry(rowIndex.toString(), isBold), - ); - } - - /// Update the text color of the column - Future updateColumnTextColor({ - required Node tableCellNode, - required String color, - }) async { - final columnIndex = tableCellNode.columnIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.columnTextColors, - source: tableCellNode.columnTextColors, - duplicatedEntry: MapEntry(columnIndex.toString(), color), - ); - } - - /// Update the text color of the row - Future updateRowTextColor({ - required Node tableCellNode, - required String color, - }) async { - final rowIndex = tableCellNode.rowIndex; - await _updateTableAttributes( - tableCellNode: tableCellNode, - attributeKey: SimpleTableBlockKeys.rowTextColors, - source: tableCellNode.rowTextColors, - duplicatedEntry: MapEntry(rowIndex.toString(), color), - ); - } - - /// Update the attributes of the table. - /// - /// This function is used to update the attributes of the table. - /// - /// The attribute key is the key of the attribute to be updated. - /// The source is the original value of the attribute. - /// The duplicatedEntry is the entry of the attribute to be updated. - Future _updateTableAttributes({ - required Node tableCellNode, - required String attributeKey, - required Map source, - MapEntry? duplicatedEntry, - }) async { - assert(tableCellNode.type == SimpleTableCellBlockKeys.type); - - final parentTableNode = tableCellNode.parentTableNode; - - if (parentTableNode == null) { - Log.warn('parent table node is null'); - return; - } - - final columnIndex = tableCellNode.columnIndex; - - Log.info( - 'update $attributeKey: $source at column $columnIndex in table ${parentTableNode.id}', - ); - - final transaction = this.transaction; - final attributes = parentTableNode.attributes.mergeValues( - attributeKey, - source, - duplicatedEntry: duplicatedEntry, - ); - transaction.updateNode(parentTableNode, attributes); - await apply(transaction); - } - - /// Clear the text align of the column at the index where the table cell node is located. - Future clearColumnTextAlign({ - required Node tableCellNode, - }) async { - final parentTableNode = tableCellNode.parentTableNode; - if (parentTableNode == null) { - Log.warn('parent table node is null'); - return; - } - final columnIndex = tableCellNode.columnIndex; - final transaction = this.transaction; - for (var i = 0; i < parentTableNode.rowLength; i++) { - final cell = parentTableNode.getTableCellNode( - rowIndex: i, - columnIndex: columnIndex, - ); - if (cell == null) { - continue; - } - for (final child in cell.children) { - transaction.updateNode(child, { - blockComponentAlign: null, - }); - } - } - if (transaction.operations.isNotEmpty) { - await apply(transaction); - } - } - - /// Clear the text align of the row at the index where the table cell node is located. - Future clearRowTextAlign({ - required Node tableCellNode, - }) async { - final parentTableNode = tableCellNode.parentTableNode; - if (parentTableNode == null) { - Log.warn('parent table node is null'); - return; - } - final rowIndex = tableCellNode.rowIndex; - final transaction = this.transaction; - for (var i = 0; i < parentTableNode.columnLength; i++) { - final cell = parentTableNode.getTableCellNode( - rowIndex: rowIndex, - columnIndex: i, - ); - if (cell == null) { - continue; - } - for (final child in cell.children) { - transaction.updateNode( - child, - { - blockComponentAlign: null, - }, - ); - } - } - if (transaction.operations.isNotEmpty) { - await apply(transaction); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart deleted file mode 100644 index 99f23d1ee9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class SimpleTableRowBlockKeys { - const SimpleTableRowBlockKeys._(); - - static const String type = 'simple_table_row'; -} - -Node simpleTableRowBlockNode({ - List children = const [], -}) { - return Node( - type: SimpleTableRowBlockKeys.type, - children: children, - ); -} - -class SimpleTableRowBlockComponentBuilder extends BlockComponentBuilder { - SimpleTableRowBlockComponentBuilder({ - super.configuration, - this.alwaysDistributeColumnWidths = false, - }); - - final bool alwaysDistributeColumnWidths; - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return SimpleTableRowBlockWidget( - key: node.key, - node: node, - configuration: configuration, - alwaysDistributeColumnWidths: alwaysDistributeColumnWidths, - showActions: showActions(node), - actionBuilder: (context, state) => actionBuilder( - blockComponentContext, - state, - ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), - ); - } - - @override - BlockComponentValidate get validate => (_) => true; -} - -class SimpleTableRowBlockWidget extends BlockComponentStatefulWidget { - const SimpleTableRowBlockWidget({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - required this.alwaysDistributeColumnWidths, - }); - - final bool alwaysDistributeColumnWidths; - - @override - State createState() => - _SimpleTableRowBlockWidgetState(); -} - -class _SimpleTableRowBlockWidgetState extends State - with - BlockComponentConfigurable, - BlockComponentTextDirectionMixin, - BlockComponentBackgroundColorMixin { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - @override - late EditorState editorState = context.read(); - - @override - Widget build(BuildContext context) { - if (node.children.isEmpty) { - return const SizedBox.shrink(); - } - - return IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildCells(), - ), - ); - } - - List _buildCells() { - final List cells = []; - - for (var i = 0; i < node.children.length; i++) { - // border - if (i == 0 && - SimpleTableConstants.borderType == - SimpleTableBorderRenderType.table) { - cells.add(const SimpleTableRowDivider()); - } - - final child = editorState.renderer.build(context, node.children[i]); - cells.add( - widget.alwaysDistributeColumnWidths ? Flexible(child: child) : child, - ); - - // border - if (SimpleTableConstants.borderType == - SimpleTableBorderRenderType.table) { - cells.add(const SimpleTableRowDivider()); - } - } - - return cells; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart deleted file mode 100644 index 70b2b07660..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -final CommandShortcutEvent arrowDownInTableCell = CommandShortcutEvent( - key: 'Press arrow down in table cell', - getDescription: () => - AppFlowyEditorL10n.current.cmdTableMoveToDownCellAtSameOffset, - command: 'arrow down', - handler: _arrowDownInTableCellHandler, -); - -/// Move the selection to the next cell in the same column. -/// -/// Only handle the case when the selection is in the first line of the cell. -KeyEventResult _arrowDownInTableCellHandler(EditorState editorState) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return KeyEventResult.ignored; - } - - final isInLastLine = node.path.last + 1 == node.parent?.children.length; - if (!isInLastLine) { - return KeyEventResult.ignored; - } - - Selection? newSelection = editorState.selection; - final rowIndex = tableCellNode.rowIndex; - final parentTableNode = tableCellNode.parentTableNode; - if (parentTableNode == null) { - return KeyEventResult.ignored; - } - - if (rowIndex == parentTableNode.rowLength - 1) { - // focus on the next block - final nextNode = tableCellNode.next; - final nextBlock = tableCellNode.parentTableNode?.next; - if (nextNode != null) { - final nextFocusableSibling = parentTableNode.getNextFocusableSibling(); - if (nextFocusableSibling != null) { - final length = nextFocusableSibling.delta?.length ?? 0; - newSelection = Selection.collapsed( - Position( - path: nextFocusableSibling.path, - offset: length, - ), - ); - } - } else if (nextBlock != null) { - if (nextBlock.type != SimpleTableBlockKeys.type) { - newSelection = Selection.collapsed( - Position( - path: nextBlock.path, - ), - ); - } else { - return tableNavigationArrowDownCommand.handler(editorState); - } - } - } else { - // focus on next cell in the same column - final nextCell = tableCellNode.getNextCellInSameColumn(); - if (nextCell != null) { - final offset = selection.end.offset; - // get the first children of the next cell - final firstChild = nextCell.children.firstWhereOrNull( - (c) => c.delta != null, - ); - if (firstChild != null) { - final length = firstChild.delta?.length ?? 0; - newSelection = Selection.collapsed( - Position( - path: firstChild.path, - offset: offset.clamp(0, length), - ), - ); - } - } - } - - if (newSelection != null) { - editorState.updateSelectionWithReason(newSelection); - } - - return KeyEventResult.handled; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart deleted file mode 100644 index f9a80ced5e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final CommandShortcutEvent arrowLeftInTableCell = CommandShortcutEvent( - key: 'Press arrow left in table cell', - getDescription: () => AppFlowyEditorL10n - .current.cmdTableMoveToRightCellIfItsAtTheEndOfCurrentCell, - command: 'arrow left', - handler: (editorState) => editorState.moveToPreviousCell( - editorState, - (result) { - // only handle the case when the selection is at the beginning of the cell - if (0 != result.$2?.end.offset) { - return false; - } - return true; - }, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart deleted file mode 100644 index 196357b5b0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final CommandShortcutEvent arrowRightInTableCell = CommandShortcutEvent( - key: 'Press arrow right in table cell', - getDescription: () => AppFlowyEditorL10n - .current.cmdTableMoveToRightCellIfItsAtTheEndOfCurrentCell, - command: 'arrow right', - handler: (editorState) => editorState.moveToNextCell( - editorState, - (result) { - // only handle the case when the selection is at the end of the cell - final node = result.$4; - final length = node?.delta?.length ?? 0; - final selection = result.$2; - return selection?.end.offset == length; - }, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart deleted file mode 100644 index f6919f3b04..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -final CommandShortcutEvent arrowUpInTableCell = CommandShortcutEvent( - key: 'Press arrow up in table cell', - getDescription: () => - AppFlowyEditorL10n.current.cmdTableMoveToUpCellAtSameOffset, - command: 'arrow up', - handler: _arrowUpInTableCellHandler, -); - -/// Move the selection to the previous cell in the same column. -/// -/// Only handle the case when the selection is in the first line of the cell. -KeyEventResult _arrowUpInTableCellHandler(EditorState editorState) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return KeyEventResult.ignored; - } - - final isInFirstLine = node.path.last == 0; - if (!isInFirstLine) { - return KeyEventResult.ignored; - } - - Selection? newSelection = editorState.selection; - final rowIndex = tableCellNode.rowIndex; - if (rowIndex == 0) { - // focus on the previous block - final previousNode = tableCellNode.parentTableNode; - if (previousNode != null) { - final previousFocusableSibling = - previousNode.getPreviousFocusableSibling(); - if (previousFocusableSibling != null) { - final length = previousFocusableSibling.delta?.length ?? 0; - newSelection = Selection.collapsed( - Position( - path: previousFocusableSibling.path, - offset: length, - ), - ); - } - } - } else { - // focus on previous cell in the same column - final previousCell = tableCellNode.getPreviousCellInSameColumn(); - if (previousCell != null) { - final offset = selection.end.offset; - // get the last children of the previous cell - final lastChild = previousCell.children.lastWhereOrNull( - (c) => c.delta != null, - ); - if (lastChild != null) { - final length = lastChild.delta?.length ?? 0; - newSelection = Selection.collapsed( - Position( - path: lastChild.path, - offset: offset.clamp(0, length), - ), - ); - } - } - } - - if (newSelection != null) { - editorState.updateSelectionWithReason(newSelection); - } - - return KeyEventResult.handled; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart deleted file mode 100644 index f4a9bd9946..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/material.dart'; - -final CommandShortcutEvent backspaceInTableCell = CommandShortcutEvent( - key: 'Press backspace in table cell', - getDescription: () => 'Ignore the backspace key in table cell', - command: 'backspace', - handler: _backspaceInTableCellHandler, -); - -KeyEventResult _backspaceInTableCellHandler(EditorState editorState) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return KeyEventResult.ignored; - } - - final onlyContainsOneChild = tableCellNode.children.length == 1; - final firstChild = tableCellNode.children.first; - final isParagraphNode = firstChild.type == ParagraphBlockKeys.type; - final isCodeBlock = firstChild.type == CodeBlockKeys.type; - if (onlyContainsOneChild && - selection.isCollapsed && - selection.end.offset == 0) { - if (isParagraphNode && firstChild.children.isEmpty) { - return KeyEventResult.skipRemainingHandlers; - } else if (isCodeBlock) { - // replace the codeblock with a paragraph - final transaction = editorState.transaction; - transaction.insertNode(node.path, paragraphNode()); - transaction.deleteNode(node); - transaction.afterSelection = Selection.collapsed( - Position( - path: node.path, - ), - ); - editorState.apply(transaction); - return KeyEventResult.handled; - } - } - - return KeyEventResult.ignored; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart deleted file mode 100644 index 1fb8e07603..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -typedef IsInTableCellResult = ( - bool isInTableCell, - Selection? selection, - Node? tableCellNode, - Node? node, -); - -extension TableCommandExtension on EditorState { - /// Return a tuple, the first element is a boolean indicating whether the current selection is in a table cell, - /// the second element is the node that is the parent of the table cell if the current selection is in a table cell, - /// otherwise it is null. - /// The third element is the node that is the current selection. - IsInTableCellResult isCurrentSelectionInTableCell() { - final selection = this.selection; - if (selection == null) { - return (false, null, null, null); - } - - if (selection.isCollapsed) { - // if the selection is collapsed, check if the node is in a table cell - final node = document.nodeAtPath(selection.end.path); - final tableCellParent = node?.findParent( - (node) => node.type == SimpleTableCellBlockKeys.type, - ); - final isInTableCell = tableCellParent != null; - return (isInTableCell, selection, tableCellParent, node); - } else { - // if the selection is not collapsed, check if the start and end nodes are in a table cell - final startNode = document.nodeAtPath(selection.start.path); - final endNode = document.nodeAtPath(selection.end.path); - final startNodeInTableCell = startNode?.findParent( - (node) => node.type == SimpleTableCellBlockKeys.type, - ); - final endNodeInTableCell = endNode?.findParent( - (node) => node.type == SimpleTableCellBlockKeys.type, - ); - final isInSameTableCell = startNodeInTableCell != null && - endNodeInTableCell != null && - startNodeInTableCell.path.equals(endNodeInTableCell.path); - return (isInSameTableCell, selection, startNodeInTableCell, endNode); - } - } - - /// Move the selection to the previous cell - KeyEventResult moveToPreviousCell( - EditorState editorState, - bool Function(IsInTableCellResult result) shouldHandle, - ) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return KeyEventResult.ignored; - } - - if (!shouldHandle((isInTableCell, selection, tableCellNode, node))) { - return KeyEventResult.ignored; - } - - if (isOutdentable(editorState)) { - return outdentCommand.execute(editorState); - } - - Selection? newSelection; - - final previousCell = tableCellNode.getPreviousCellInSameRow(); - if (previousCell != null && !previousCell.path.equals(tableCellNode.path)) { - // get the last children of the previous cell - final lastChild = previousCell.children.lastWhereOrNull( - (c) => c.delta != null, - ); - if (lastChild != null) { - newSelection = Selection.collapsed( - Position( - path: lastChild.path, - offset: lastChild.delta?.length ?? 0, - ), - ); - } - } else { - // focus on the previous block - final previousNode = tableCellNode.parentTableNode; - if (previousNode != null) { - final previousFocusableSibling = - previousNode.getPreviousFocusableSibling(); - if (previousFocusableSibling != null) { - final length = previousFocusableSibling.delta?.length ?? 0; - newSelection = Selection.collapsed( - Position( - path: previousFocusableSibling.path, - offset: length, - ), - ); - } - } - } - - if (newSelection != null) { - editorState.updateSelectionWithReason(newSelection); - } - - return KeyEventResult.handled; - } - - /// Move the selection to the next cell - KeyEventResult moveToNextCell( - EditorState editorState, - bool Function(IsInTableCellResult result) shouldHandle, - ) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null) { - return KeyEventResult.ignored; - } - - if (!shouldHandle((isInTableCell, selection, tableCellNode, node))) { - return KeyEventResult.ignored; - } - - Selection? newSelection; - - if (isIndentable(editorState)) { - return indentCommand.execute(editorState); - } - - final nextCell = tableCellNode.getNextCellInSameRow(); - if (nextCell != null && !nextCell.path.equals(tableCellNode.path)) { - // get the first children of the next cell - final firstChild = nextCell.children.firstWhereOrNull( - (c) => c.delta != null, - ); - if (firstChild != null) { - newSelection = Selection.collapsed( - Position( - path: firstChild.path, - ), - ); - } - } else { - // focus on the previous block - final nextNode = tableCellNode.parentTableNode; - if (nextNode != null) { - final nextFocusableSibling = nextNode.getNextFocusableSibling(); - nextNode.getNextFocusableSibling(); - if (nextFocusableSibling != null) { - newSelection = Selection.collapsed( - Position( - path: nextFocusableSibling.path, - ), - ); - } - } - } - - if (newSelection != null) { - editorState.updateSelectionWithReason(newSelection); - } - - return KeyEventResult.handled; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart deleted file mode 100644 index 1cc278ca99..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart'; - -final simpleTableCommands = [ - tableNavigationArrowDownCommand, - arrowUpInTableCell, - arrowDownInTableCell, - arrowLeftInTableCell, - arrowRightInTableCell, - tabInTableCell, - shiftTabInTableCell, - backspaceInTableCell, - selectAllInTableCellCommand, - enterInTableCell, -]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart deleted file mode 100644 index bfc31e8abc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_enter_command.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -final CommandShortcutEvent enterInTableCell = CommandShortcutEvent( - key: 'Press enter in table cell', - getDescription: () => 'Press the enter key in table cell', - command: 'enter', - handler: _enterInTableCellHandler, -); - -KeyEventResult _enterInTableCellHandler(EditorState editorState) { - final (isInTableCell, selection, tableCellNode, node) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || - selection == null || - tableCellNode == null || - node == null || - !selection.isCollapsed) { - return KeyEventResult.ignored; - } - - // check if the shift key is pressed, if so, we should return false to let the system handle it. - final isShiftPressed = HardwareKeyboard.instance.isShiftPressed; - if (isShiftPressed) { - return KeyEventResult.ignored; - } - - final delta = node.delta; - if (!indentableBlockTypes.contains(node.type) || delta == null) { - return KeyEventResult.ignored; - } - - if (selection.startIndex == 0 && delta.isEmpty) { - // clear the style - if (node.parent?.type != SimpleTableCellBlockKeys.type) { - if (outdentCommand.execute(editorState) == KeyEventResult.handled) { - return KeyEventResult.handled; - } - } - if (node.type != CalloutBlockKeys.type) { - return convertToParagraphCommand.execute(editorState); - } - } - - return KeyEventResult.ignored; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart deleted file mode 100644 index 534879f56f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_navigation_command.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -final CommandShortcutEvent tableNavigationArrowDownCommand = - CommandShortcutEvent( - key: 'table navigation', - getDescription: () => 'table navigation', - command: 'arrow down', - handler: _tableNavigationArrowDownHandler, -); - -KeyEventResult _tableNavigationArrowDownHandler(EditorState editorState) { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return KeyEventResult.ignored; - } - - final nextNode = editorState.getNodeAtPath(selection.start.path.next); - if (nextNode == null) { - return KeyEventResult.ignored; - } - - if (nextNode.type == SimpleTableBlockKeys.type) { - final firstCell = nextNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - if (firstCell != null) { - final firstFocusableChild = firstCell.getFirstFocusableChild(); - if (firstFocusableChild != null) { - editorState.updateSelectionWithReason( - Selection.collapsed( - Position(path: firstFocusableChild.path), - ), - ); - return KeyEventResult.handled; - } - } - } - - return KeyEventResult.ignored; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart deleted file mode 100644 index a33b08f66e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; - -final CommandShortcutEvent selectAllInTableCellCommand = CommandShortcutEvent( - key: 'Select all contents in table cell', - getDescription: () => 'Select all contents in table cell', - command: 'ctrl+a', - macOSCommand: 'cmd+a', - handler: _selectAllInTableCellHandler, -); - -KeyEventResult _selectAllInTableCellHandler(EditorState editorState) { - final (isInTableCell, selection, tableCellNode, _) = - editorState.isCurrentSelectionInTableCell(); - if (!isInTableCell || selection == null || tableCellNode == null) { - return KeyEventResult.ignored; - } - - final firstFocusableChild = tableCellNode.children.firstWhereOrNull( - (e) => e.delta != null, - ); - final lastFocusableChild = tableCellNode.lastChildWhere( - (e) => e.delta != null, - ); - if (firstFocusableChild == null || lastFocusableChild == null) { - return KeyEventResult.ignored; - } - - final afterSelection = Selection( - start: Position(path: firstFocusableChild.path), - end: Position( - path: lastFocusableChild.path, - offset: lastFocusableChild.delta?.length ?? 0, - ), - ); - - if (afterSelection == editorState.selection) { - // Focus on the cell already - return KeyEventResult.ignored; - } else { - editorState.selection = afterSelection; - return KeyEventResult.handled; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart deleted file mode 100644 index 93b072fa88..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final CommandShortcutEvent tabInTableCell = CommandShortcutEvent( - key: 'Press tab in table cell', - getDescription: () => 'Move the selection to the next cell', - command: 'tab', - handler: (editorState) => editorState.moveToNextCell( - editorState, - (result) { - final tableCellNode = result.$3; - if (tableCellNode?.isLastCellInTable ?? false) { - return false; - } - return true; - }, - ), -); - -final CommandShortcutEvent shiftTabInTableCell = CommandShortcutEvent( - key: 'Press shift + tab in table cell', - getDescription: () => 'Move the selection to the previous cell', - command: 'shift+tab', - handler: (editorState) => editorState.moveToPreviousCell( - editorState, - (result) { - final tableCellNode = result.$3; - if (tableCellNode?.isFirstCellInTable ?? false) { - return false; - } - return true; - }, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart deleted file mode 100644 index 187dbaf31d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_desktop_simple_table_widget.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class DesktopSimpleTableWidget extends StatefulWidget { - const DesktopSimpleTableWidget({ - super.key, - required this.simpleTableContext, - required this.node, - this.enableAddColumnButton = true, - this.enableAddRowButton = true, - this.enableAddColumnAndRowButton = true, - this.enableHoverEffect = true, - this.isFeedback = false, - this.alwaysDistributeColumnWidths = false, - }); - - /// Refer to [SimpleTableWidget.node]. - final Node node; - - /// Refer to [SimpleTableWidget.simpleTableContext]. - final SimpleTableContext simpleTableContext; - - /// Refer to [SimpleTableWidget.enableAddColumnButton]. - final bool enableAddColumnButton; - - /// Refer to [SimpleTableWidget.enableAddRowButton]. - final bool enableAddRowButton; - - /// Refer to [SimpleTableWidget.enableAddColumnAndRowButton]. - final bool enableAddColumnAndRowButton; - - /// Refer to [SimpleTableWidget.enableHoverEffect]. - final bool enableHoverEffect; - - /// Refer to [SimpleTableWidget.isFeedback]. - final bool isFeedback; - - /// Refer to [SimpleTableWidget.alwaysDistributeColumnWidths]. - final bool alwaysDistributeColumnWidths; - - @override - State createState() => - _DesktopSimpleTableWidgetState(); -} - -class _DesktopSimpleTableWidgetState extends State { - SimpleTableContext get simpleTableContext => widget.simpleTableContext; - - final scrollController = ScrollController(); - late final editorState = context.read(); - - @override - void initState() { - super.initState(); - - simpleTableContext.horizontalScrollController = scrollController; - } - - @override - void dispose() { - simpleTableContext.horizontalScrollController = null; - scrollController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.isFeedback ? _buildFeedbackTable() : _buildDesktopTable(); - } - - Widget _buildFeedbackTable() { - return Provider.value( - value: simpleTableContext, - child: IntrinsicWidth( - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildRows(), - ), - ), - ), - ); - } - - Widget _buildDesktopTable() { - // table content - // IntrinsicHeight is used to make the table size fit the content. - Widget child = IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _buildRows(), - ), - ); - - if (widget.alwaysDistributeColumnWidths) { - child = Padding( - padding: SimpleTableConstants.tablePadding, - child: child, - ); - } else { - child = Scrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: Padding( - padding: SimpleTableConstants.tablePadding, - // IntrinsicWidth is used to make the table size fit the content. - child: IntrinsicWidth(child: child), - ), - ), - ); - } - - if (widget.enableHoverEffect) { - child = MouseRegion( - onEnter: (event) => - simpleTableContext.isHoveringOnTableArea.value = true, - onExit: (event) { - simpleTableContext.isHoveringOnTableArea.value = false; - }, - child: Provider.value( - value: simpleTableContext, - child: Stack( - children: [ - MouseRegion( - hitTestBehavior: HitTestBehavior.opaque, - onEnter: (event) => - simpleTableContext.isHoveringOnColumnsAndRows.value = true, - onExit: (event) { - simpleTableContext.isHoveringOnColumnsAndRows.value = false; - simpleTableContext.hoveringTableCell.value = null; - }, - child: child, - ), - if (editorState.editable) ...[ - if (widget.enableAddColumnButton) - SimpleTableAddColumnHoverButton( - editorState: editorState, - tableNode: widget.node, - ), - if (widget.enableAddRowButton) - SimpleTableAddRowHoverButton( - editorState: editorState, - tableNode: widget.node, - ), - if (widget.enableAddColumnAndRowButton) - SimpleTableAddColumnAndRowHoverButton( - editorState: editorState, - node: widget.node, - ), - ], - ], - ), - ), - ); - } - - return child; - } - - List _buildRows() { - final List rows = []; - - if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) { - rows.add(const SimpleTableColumnDivider()); - } - - for (final child in widget.node.children) { - rows.add(editorState.renderer.build(context, child)); - - if (SimpleTableConstants.borderType == - SimpleTableBorderRenderType.table) { - rows.add(const SimpleTableColumnDivider()); - } - } - - return rows; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart deleted file mode 100644 index bf8720b88a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_mobile_simple_table_widget.dart +++ /dev/null @@ -1,126 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class MobileSimpleTableWidget extends StatefulWidget { - const MobileSimpleTableWidget({ - super.key, - required this.simpleTableContext, - required this.node, - this.enableAddColumnButton = true, - this.enableAddRowButton = true, - this.enableAddColumnAndRowButton = true, - this.enableHoverEffect = true, - this.isFeedback = false, - this.alwaysDistributeColumnWidths = false, - }); - - /// Refer to [SimpleTableWidget.node]. - final Node node; - - /// Refer to [SimpleTableWidget.simpleTableContext]. - final SimpleTableContext simpleTableContext; - - /// Refer to [SimpleTableWidget.enableAddColumnButton]. - final bool enableAddColumnButton; - - /// Refer to [SimpleTableWidget.enableAddRowButton]. - final bool enableAddRowButton; - - /// Refer to [SimpleTableWidget.enableAddColumnAndRowButton]. - final bool enableAddColumnAndRowButton; - - /// Refer to [SimpleTableWidget.enableHoverEffect]. - final bool enableHoverEffect; - - /// Refer to [SimpleTableWidget.isFeedback]. - final bool isFeedback; - - /// Refer to [SimpleTableWidget.alwaysDistributeColumnWidths]. - final bool alwaysDistributeColumnWidths; - - @override - State createState() => - _MobileSimpleTableWidgetState(); -} - -class _MobileSimpleTableWidgetState extends State { - SimpleTableContext get simpleTableContext => widget.simpleTableContext; - - final scrollController = ScrollController(); - late final editorState = context.read(); - - @override - void initState() { - super.initState(); - - simpleTableContext.horizontalScrollController = scrollController; - } - - @override - void dispose() { - simpleTableContext.horizontalScrollController = null; - scrollController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.isFeedback ? _buildFeedbackTable() : _buildMobileTable(); - } - - Widget _buildFeedbackTable() { - return Provider.value( - value: simpleTableContext, - child: IntrinsicWidth( - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildRows(), - ), - ), - ), - ); - } - - Widget _buildMobileTable() { - return Provider.value( - value: simpleTableContext, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: IntrinsicWidth( - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _buildRows(), - ), - ), - ), - ), - ); - } - - List _buildRows() { - final List rows = []; - - if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) { - rows.add(const SimpleTableColumnDivider()); - } - - for (final child in widget.node.children) { - rows.add(editorState.renderer.build(context, child)); - - if (SimpleTableConstants.borderType == - SimpleTableBorderRenderType.table) { - rows.add(const SimpleTableColumnDivider()); - } - } - - return rows; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart deleted file mode 100644 index b81ff89ee8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart +++ /dev/null @@ -1,1297 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/option/option_actions.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -/// Base class for all simple table bottom sheet actions -abstract class ISimpleTableBottomSheetActions extends StatelessWidget { - const ISimpleTableBottomSheetActions({ - super.key, - required this.type, - required this.cellNode, - required this.editorState, - }); - - final SimpleTableMoreActionType type; - final Node cellNode; - final EditorState editorState; -} - -/// Quick actions for the table cell -/// -/// - Copy -/// - Paste -/// - Cut -/// - Delete -class SimpleTableCellQuickActions extends ISimpleTableBottomSheetActions { - const SimpleTableCellQuickActions({ - super.key, - required super.type, - required super.cellNode, - required super.editorState, - }); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: SimpleTableConstants.actionSheetQuickActionSectionHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SimpleTableQuickAction( - type: SimpleTableMoreAction.cut, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.cut, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.copy, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.copy, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.paste, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.paste, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.delete, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.delete, - ), - ), - ], - ), - ); - } - - void _onActionTap(BuildContext context, SimpleTableMoreAction action) { - final tableNode = cellNode.parentTableNode; - if (tableNode == null) { - Log.error('unable to find table node when performing action: $action'); - return; - } - - switch (action) { - case SimpleTableMoreAction.cut: - _onCut(tableNode); - case SimpleTableMoreAction.copy: - _onCopy(tableNode); - case SimpleTableMoreAction.paste: - _onPaste(tableNode); - case SimpleTableMoreAction.delete: - _onDelete(tableNode); - default: - assert(false, 'Unsupported action: $type'); - } - - // close the action menu - Navigator.of(context).pop(); - } - - Future _onCut(Node tableNode) async { - ClipboardServiceData? data; - - switch (type) { - case SimpleTableMoreActionType.column: - data = await editorState.copyColumn( - tableNode: tableNode, - columnIndex: cellNode.columnIndex, - clearContent: true, - ); - case SimpleTableMoreActionType.row: - data = await editorState.copyRow( - tableNode: tableNode, - rowIndex: cellNode.rowIndex, - clearContent: true, - ); - } - - if (data != null) { - await getIt().setData(data); - } - } - - Future _onCopy( - Node tableNode, - ) async { - ClipboardServiceData? data; - - switch (type) { - case SimpleTableMoreActionType.column: - data = await editorState.copyColumn( - tableNode: tableNode, - columnIndex: cellNode.columnIndex, - ); - case SimpleTableMoreActionType.row: - data = await editorState.copyRow( - tableNode: tableNode, - rowIndex: cellNode.rowIndex, - ); - } - - if (data != null) { - await getIt().setData(data); - } - } - - void _onPaste(Node tableNode) { - switch (type) { - case SimpleTableMoreActionType.column: - editorState.pasteColumn( - tableNode: tableNode, - columnIndex: cellNode.columnIndex, - ); - case SimpleTableMoreActionType.row: - editorState.pasteRow( - tableNode: tableNode, - rowIndex: cellNode.rowIndex, - ); - } - } - - void _onDelete(Node tableNode) { - switch (type) { - case SimpleTableMoreActionType.column: - editorState.deleteColumnInTable( - tableNode, - cellNode.columnIndex, - ); - case SimpleTableMoreActionType.row: - editorState.deleteRowInTable( - tableNode, - cellNode.rowIndex, - ); - } - } -} - -class SimpleTableQuickAction extends StatelessWidget { - const SimpleTableQuickAction({ - super.key, - required this.type, - required this.onTap, - this.isEnabled = true, - }); - - final SimpleTableMoreAction type; - final VoidCallback onTap; - final bool isEnabled; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: isEnabled ? 1.0 : 0.5, - child: AnimatedGestureDetector( - onTapUp: isEnabled ? onTap : null, - child: FlowySvg( - type.leftIconSvg, - blendMode: - type == SimpleTableMoreAction.delete ? null : BlendMode.srcIn, - size: const Size.square(24), - color: context.simpleTableQuickActionBackgroundColor, - ), - ), - ); - } -} - -/// Insert actions -/// -/// - Column: Insert left or insert right -/// - Row: Insert above or insert below -class SimpleTableInsertActions extends ISimpleTableBottomSheetActions { - const SimpleTableInsertActions({ - super.key, - required super.type, - required super.cellNode, - required super.editorState, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16), - height: SimpleTableConstants.actionSheetInsertSectionHeight, - child: _buildAction(context), - ); - } - - Widget _buildAction(BuildContext context) { - return switch (type) { - SimpleTableMoreActionType.row => Row( - children: [ - SimpleTableInsertAction( - type: SimpleTableMoreAction.insertAbove, - enableLeftBorder: true, - onTap: (increaseCounter) async => _onActionTap( - context, - type: SimpleTableMoreAction.insertAbove, - increaseCounter: increaseCounter, - ), - ), - const HSpace(2), - SimpleTableInsertAction( - type: SimpleTableMoreAction.insertBelow, - enableRightBorder: true, - onTap: (increaseCounter) async => _onActionTap( - context, - type: SimpleTableMoreAction.insertBelow, - increaseCounter: increaseCounter, - ), - ), - ], - ), - SimpleTableMoreActionType.column => Row( - children: [ - SimpleTableInsertAction( - type: SimpleTableMoreAction.insertLeft, - enableLeftBorder: true, - onTap: (increaseCounter) async => _onActionTap( - context, - type: SimpleTableMoreAction.insertLeft, - increaseCounter: increaseCounter, - ), - ), - const HSpace(2), - SimpleTableInsertAction( - type: SimpleTableMoreAction.insertRight, - enableRightBorder: true, - onTap: (increaseCounter) async => _onActionTap( - context, - type: SimpleTableMoreAction.insertRight, - increaseCounter: increaseCounter, - ), - ), - ], - ), - }; - } - - Future _onActionTap( - BuildContext context, { - required SimpleTableMoreAction type, - required int increaseCounter, - }) async { - final simpleTableContext = context.read(); - final tableNode = cellNode.parentTableNode; - if (tableNode == null) { - Log.error('unable to find table node when performing action: $type'); - return; - } - - switch (type) { - case SimpleTableMoreAction.insertAbove: - // update the highlight status for the selecting row - simpleTableContext.selectingRow.value = cellNode.rowIndex + 1; - await editorState.insertRowInTable( - tableNode, - cellNode.rowIndex, - ); - case SimpleTableMoreAction.insertBelow: - await editorState.insertRowInTable( - tableNode, - cellNode.rowIndex + 1, - ); - // scroll to the next cell position - editorState.scrollService?.scrollTo( - SimpleTableConstants.defaultRowHeight, - duration: Durations.short3, - ); - case SimpleTableMoreAction.insertLeft: - // update the highlight status for the selecting column - simpleTableContext.selectingColumn.value = cellNode.columnIndex + 1; - await editorState.insertColumnInTable( - tableNode, - cellNode.columnIndex, - ); - case SimpleTableMoreAction.insertRight: - await editorState.insertColumnInTable( - tableNode, - cellNode.columnIndex + 1, - ); - final horizontalScrollController = - simpleTableContext.horizontalScrollController; - if (horizontalScrollController != null) { - final previousWidth = horizontalScrollController.offset; - horizontalScrollController.jumpTo( - previousWidth + SimpleTableConstants.defaultColumnWidth, - ); - } - - default: - assert(false, 'Unsupported action: $type'); - } - } -} - -class SimpleTableInsertAction extends StatefulWidget { - const SimpleTableInsertAction({ - super.key, - required this.type, - this.enableLeftBorder = false, - this.enableRightBorder = false, - required this.onTap, - }); - - final SimpleTableMoreAction type; - final bool enableLeftBorder; - final bool enableRightBorder; - final ValueChanged onTap; - - @override - State createState() => - _SimpleTableInsertActionState(); -} - -class _SimpleTableInsertActionState extends State { - // used to count how many times the action is tapped - int increaseCounter = 0; - - @override - Widget build(BuildContext context) { - return Expanded( - child: DecoratedBox( - decoration: ShapeDecoration( - color: context.simpleTableInsertActionBackgroundColor, - shape: _buildBorder(), - ), - child: AnimatedGestureDetector( - onTapUp: () => widget.onTap(increaseCounter++), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(1), - child: FlowySvg( - widget.type.leftIconSvg, - size: const Size.square(22), - ), - ), - FlowyText( - widget.type.name, - fontSize: 12, - figmaLineHeight: 16, - ), - ], - ), - ), - ), - ); - } - - RoundedRectangleBorder _buildBorder() { - const radius = Radius.circular( - SimpleTableConstants.actionSheetButtonRadius, - ); - return RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: widget.enableLeftBorder ? radius : Radius.zero, - bottomLeft: widget.enableLeftBorder ? radius : Radius.zero, - topRight: widget.enableRightBorder ? radius : Radius.zero, - bottomRight: widget.enableRightBorder ? radius : Radius.zero, - ), - ); - } -} - -/// Cell Action buttons -/// -/// - Distribute columns evenly -/// - Set to page width -/// - Duplicate row -/// - Duplicate column -/// - Clear contents -class SimpleTableCellActionButtons extends ISimpleTableBottomSheetActions { - const SimpleTableCellActionButtons({ - super.key, - required super.type, - required super.cellNode, - required super.editorState, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: _buildActions(context), - ), - ); - } - - List _buildActions(BuildContext context) { - // the actions are grouped into different sections - // we need to get the index of the table cell node - // and the length of the columns and rows - final (index, columnLength, rowLength) = switch (type) { - SimpleTableMoreActionType.row => ( - cellNode.rowIndex, - cellNode.columnLength, - cellNode.rowLength, - ), - SimpleTableMoreActionType.column => ( - cellNode.columnIndex, - cellNode.rowLength, - cellNode.columnLength, - ), - }; - final actionGroups = type.buildMobileActions( - index: index, - columnLength: columnLength, - rowLength: rowLength, - ); - final List widgets = []; - - for (final (actionGroupIndex, actionGroup) in actionGroups.indexed) { - for (final (index, action) in actionGroup.indexed) { - widgets.add( - // enable the corner border if the cell is the first or last in the group - switch (action) { - SimpleTableMoreAction.enableHeaderColumn => - SimpleTableHeaderActionButton( - type: action, - isEnabled: cellNode.isHeaderColumnEnabled, - onTap: (value) => _onActionTap( - context, - action: action, - toggleHeaderValue: value, - ), - ), - SimpleTableMoreAction.enableHeaderRow => - SimpleTableHeaderActionButton( - type: action, - isEnabled: cellNode.isHeaderRowEnabled, - onTap: (value) => _onActionTap( - context, - action: action, - toggleHeaderValue: value, - ), - ), - _ => SimpleTableActionButton( - type: action, - enableTopBorder: index == 0, - enableBottomBorder: index == actionGroup.length - 1, - onTap: () => _onActionTap( - context, - action: action, - ), - ), - }, - ); - // if the action is not the first or last in the group, add a divider - if (index != actionGroup.length - 1) { - widgets.add(const FlowyDivider()); - } - } - - // add padding to separate the action groups - if (actionGroupIndex != actionGroups.length - 1) { - widgets.add(const VSpace(16)); - } - } - - return widgets; - } - - void _onActionTap( - BuildContext context, { - required SimpleTableMoreAction action, - bool toggleHeaderValue = false, - }) { - final tableNode = cellNode.parentTableNode; - if (tableNode == null) { - Log.error('unable to find table node when performing action: $action'); - return; - } - - switch (action) { - case SimpleTableMoreAction.enableHeaderColumn: - editorState.toggleEnableHeaderColumn( - tableNode: tableNode, - enable: toggleHeaderValue, - ); - case SimpleTableMoreAction.enableHeaderRow: - editorState.toggleEnableHeaderRow( - tableNode: tableNode, - enable: toggleHeaderValue, - ); - case SimpleTableMoreAction.distributeColumnsEvenly: - editorState.distributeColumnWidthToPageWidth(tableNode: tableNode); - case SimpleTableMoreAction.setToPageWidth: - editorState.setColumnWidthToPageWidth(tableNode: tableNode); - case SimpleTableMoreAction.duplicateRow: - editorState.duplicateRowInTable( - tableNode, - cellNode.rowIndex, - ); - case SimpleTableMoreAction.duplicateColumn: - editorState.duplicateColumnInTable( - tableNode, - cellNode.columnIndex, - ); - case SimpleTableMoreAction.clearContents: - switch (type) { - case SimpleTableMoreActionType.column: - editorState.clearContentAtColumnIndex( - tableNode: tableNode, - columnIndex: cellNode.columnIndex, - ); - case SimpleTableMoreActionType.row: - editorState.clearContentAtRowIndex( - tableNode: tableNode, - rowIndex: cellNode.rowIndex, - ); - } - default: - assert(false, 'Unsupported action: $action'); - break; - } - - // close the action menu - // keep the action menu open if the action is enable header row or enable header column - if (action != SimpleTableMoreAction.enableHeaderRow && - action != SimpleTableMoreAction.enableHeaderColumn) { - Navigator.of(context).pop(); - } - } -} - -/// Header action button -/// -/// - Enable header column -/// - Enable header row -/// -/// Notes: These actions are only available for the first column or first row -class SimpleTableHeaderActionButton extends StatefulWidget { - const SimpleTableHeaderActionButton({ - super.key, - required this.isEnabled, - required this.type, - this.onTap, - }); - - final bool isEnabled; - final SimpleTableMoreAction type; - final void Function(bool value)? onTap; - - @override - State createState() => - _SimpleTableHeaderActionButtonState(); -} - -class _SimpleTableHeaderActionButtonState - extends State { - bool value = false; - - @override - void initState() { - super.initState(); - - value = widget.isEnabled; - } - - @override - Widget build(BuildContext context) { - return SimpleTableActionButton( - type: widget.type, - enableTopBorder: true, - enableBottomBorder: true, - onTap: _toggle, - rightIconBuilder: (context) { - return Container( - width: 36, - height: 24, - margin: const EdgeInsets.only(right: 16), - child: FittedBox( - fit: BoxFit.fill, - child: CupertinoSwitch( - value: value, - activeTrackColor: Theme.of(context).colorScheme.primary, - onChanged: (_) => _toggle(), - ), - ), - ); - }, - ); - } - - void _toggle() { - setState(() { - value = !value; - }); - - widget.onTap?.call(value); - } -} - -/// Align text action button -/// -/// - Align text to left -/// - Align text to center -/// - Align text to right -/// -/// Notes: These actions are only available for the table -class SimpleTableAlignActionButton extends StatefulWidget { - const SimpleTableAlignActionButton({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - State createState() => - _SimpleTableAlignActionButtonState(); -} - -class _SimpleTableAlignActionButtonState - extends State { - @override - Widget build(BuildContext context) { - return SimpleTableActionButton( - type: SimpleTableMoreAction.align, - enableTopBorder: true, - enableBottomBorder: true, - onTap: widget.onTap, - rightIconBuilder: (context) { - return const Padding( - padding: EdgeInsets.only(right: 16), - child: FlowySvg(FlowySvgs.m_aa_arrow_right_s), - ); - }, - ); - } -} - -class SimpleTableActionButton extends StatelessWidget { - const SimpleTableActionButton({ - super.key, - required this.type, - this.enableTopBorder = false, - this.enableBottomBorder = false, - this.rightIconBuilder, - this.onTap, - }); - - final SimpleTableMoreAction type; - final bool enableTopBorder; - final bool enableBottomBorder; - final WidgetBuilder? rightIconBuilder; - final void Function()? onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: Container( - height: SimpleTableConstants.actionSheetNormalActionSectionHeight, - decoration: ShapeDecoration( - color: context.simpleTableActionButtonBackgroundColor, - shape: _buildBorder(), - ), - child: Row( - children: [ - const HSpace(16), - Padding( - padding: const EdgeInsets.all(1.0), - child: FlowySvg( - type.leftIconSvg, - size: const Size.square(20), - ), - ), - const HSpace(12), - FlowyText( - type.name, - fontSize: 14, - figmaLineHeight: 20, - ), - const Spacer(), - rightIconBuilder?.call(context) ?? const SizedBox.shrink(), - ], - ), - ), - ); - } - - RoundedRectangleBorder _buildBorder() { - const radius = Radius.circular( - SimpleTableConstants.actionSheetButtonRadius, - ); - return RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: enableTopBorder ? radius : Radius.zero, - topRight: enableTopBorder ? radius : Radius.zero, - bottomLeft: enableBottomBorder ? radius : Radius.zero, - bottomRight: enableBottomBorder ? radius : Radius.zero, - ), - ); - } -} - -class SimpleTableContentActions extends ISimpleTableBottomSheetActions { - const SimpleTableContentActions({ - super.key, - required super.type, - required super.cellNode, - required super.editorState, - required this.onTextColorSelected, - required this.onCellBackgroundColorSelected, - required this.onAlignTap, - this.selectedTextColor, - this.selectedCellBackgroundColor, - this.selectedAlign, - }); - - final VoidCallback onTextColorSelected; - final VoidCallback onCellBackgroundColorSelected; - final ValueChanged onAlignTap; - - final Color? selectedTextColor; - final Color? selectedCellBackgroundColor; - final TableAlign? selectedAlign; - - @override - Widget build(BuildContext context) { - return Container( - height: SimpleTableConstants.actionSheetContentSectionHeight, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - SimpleTableContentBoldAction( - isBold: type == SimpleTableMoreActionType.column - ? cellNode.isInBoldColumn - : cellNode.isInBoldRow, - toggleBold: _toggleBold, - ), - const HSpace(2), - SimpleTableContentTextColorAction( - onTap: onTextColorSelected, - selectedTextColor: selectedTextColor, - ), - const HSpace(2), - SimpleTableContentCellBackgroundColorAction( - onTap: onCellBackgroundColorSelected, - selectedCellBackgroundColor: selectedCellBackgroundColor, - ), - const HSpace(16), - SimpleTableContentAlignmentAction( - align: selectedAlign ?? TableAlign.left, - onTap: onAlignTap, - ), - ], - ), - ); - } - - void _toggleBold(bool isBold) { - switch (type) { - case SimpleTableMoreActionType.column: - editorState.toggleColumnBoldAttribute( - tableCellNode: cellNode, - isBold: isBold, - ); - case SimpleTableMoreActionType.row: - editorState.toggleRowBoldAttribute( - tableCellNode: cellNode, - isBold: isBold, - ); - } - } -} - -class SimpleTableContentBoldAction extends StatefulWidget { - const SimpleTableContentBoldAction({ - super.key, - required this.toggleBold, - required this.isBold, - }); - - final ValueChanged toggleBold; - final bool isBold; - - @override - State createState() => - _SimpleTableContentBoldActionState(); -} - -class _SimpleTableContentBoldActionState - extends State { - bool isBold = false; - - @override - void initState() { - super.initState(); - - isBold = widget.isBold; - } - - @override - Widget build(BuildContext context) { - return Expanded( - child: SimpleTableContentActionDecorator( - backgroundColor: isBold ? Theme.of(context).colorScheme.primary : null, - enableLeftBorder: true, - child: AnimatedGestureDetector( - onTapUp: () { - setState(() { - isBold = !isBold; - }); - widget.toggleBold.call(isBold); - }, - child: FlowySvg( - FlowySvgs.m_aa_bold_s, - size: const Size.square(24), - color: isBold ? Theme.of(context).colorScheme.onPrimary : null, - ), - ), - ), - ); - } -} - -class SimpleTableContentTextColorAction extends StatelessWidget { - const SimpleTableContentTextColorAction({ - super.key, - required this.onTap, - this.selectedTextColor, - }); - - final VoidCallback onTap; - final Color? selectedTextColor; - - @override - Widget build(BuildContext context) { - return Expanded( - child: SimpleTableContentActionDecorator( - child: AnimatedGestureDetector( - onTapUp: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowySvg( - FlowySvgs.m_table_text_color_m, - color: selectedTextColor, - ), - const HSpace(10), - const FlowySvg( - FlowySvgs.m_aa_arrow_right_s, - size: Size.square(12), - ), - ], - ), - ), - ), - ); - } -} - -class SimpleTableContentCellBackgroundColorAction extends StatelessWidget { - const SimpleTableContentCellBackgroundColorAction({ - super.key, - required this.onTap, - this.selectedCellBackgroundColor, - }); - - final VoidCallback onTap; - final Color? selectedCellBackgroundColor; - - @override - Widget build(BuildContext context) { - return Expanded( - child: SimpleTableContentActionDecorator( - enableRightBorder: true, - child: AnimatedGestureDetector( - onTapUp: onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildTextBackgroundColorPreview(), - const HSpace(10), - FlowySvg( - FlowySvgs.m_aa_arrow_right_s, - size: const Size.square(12), - color: selectedCellBackgroundColor, - ), - ], - ), - ), - ), - ); - } - - Widget _buildTextBackgroundColorPreview() { - return Container( - width: 24, - height: 24, - decoration: ShapeDecoration( - color: selectedCellBackgroundColor ?? const Color(0xFFFFE6FD), - shape: RoundedRectangleBorder( - side: const BorderSide( - color: Color(0xFFCFD3D9), - ), - borderRadius: BorderRadius.circular(100), - ), - ), - ); - } -} - -class SimpleTableContentAlignmentAction extends StatelessWidget { - const SimpleTableContentAlignmentAction({ - super.key, - this.align = TableAlign.left, - required this.onTap, - }); - - final TableAlign align; - final ValueChanged onTap; - - @override - Widget build(BuildContext context) { - return Expanded( - child: SimpleTableContentActionDecorator( - enableLeftBorder: true, - enableRightBorder: true, - child: AnimatedGestureDetector( - onTapUp: _onTap, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FlowySvg( - align.leftIconSvg, - size: const Size.square(24), - ), - const HSpace(10), - const FlowySvg( - FlowySvgs.m_aa_arrow_right_s, - size: Size.square(12), - ), - ], - ), - ), - ), - ); - } - - void _onTap() { - final nextAlign = switch (align) { - TableAlign.left => TableAlign.center, - TableAlign.center => TableAlign.right, - TableAlign.right => TableAlign.left, - }; - - onTap(nextAlign); - } -} - -class SimpleTableContentActionDecorator extends StatelessWidget { - const SimpleTableContentActionDecorator({ - super.key, - this.enableLeftBorder = false, - this.enableRightBorder = false, - this.backgroundColor, - required this.child, - }); - - final bool enableLeftBorder; - final bool enableRightBorder; - final Color? backgroundColor; - final Widget child; - - @override - Widget build(BuildContext context) { - return Container( - height: SimpleTableConstants.actionSheetNormalActionSectionHeight, - padding: const EdgeInsets.symmetric(vertical: 10), - decoration: ShapeDecoration( - color: - backgroundColor ?? context.simpleTableInsertActionBackgroundColor, - shape: _buildBorder(), - ), - child: child, - ); - } - - RoundedRectangleBorder _buildBorder() { - const radius = Radius.circular( - SimpleTableConstants.actionSheetButtonRadius, - ); - return RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: enableLeftBorder ? radius : Radius.zero, - topRight: enableRightBorder ? radius : Radius.zero, - bottomLeft: enableLeftBorder ? radius : Radius.zero, - bottomRight: enableRightBorder ? radius : Radius.zero, - ), - ); - } -} - -class SimpleTableActionButtons extends StatelessWidget { - const SimpleTableActionButtons({ - super.key, - required this.tableNode, - required this.editorState, - required this.onAlignTap, - }); - - final Node tableNode; - final EditorState editorState; - final VoidCallback onAlignTap; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: _buildActions(context), - ), - ); - } - - List _buildActions(BuildContext context) { - final actionGroups = [ - [ - SimpleTableMoreAction.setToPageWidth, - SimpleTableMoreAction.distributeColumnsEvenly, - ], - [ - SimpleTableMoreAction.align, - ], - [ - SimpleTableMoreAction.duplicateTable, - SimpleTableMoreAction.copyLinkToBlock, - ] - ]; - final List widgets = []; - - for (final (actionGroupIndex, actionGroup) in actionGroups.indexed) { - for (final (index, action) in actionGroup.indexed) { - widgets.add( - // enable the corner border if the cell is the first or last in the group - switch (action) { - SimpleTableMoreAction.align => SimpleTableAlignActionButton( - onTap: () => _onActionTap( - context, - action: action, - ), - ), - _ => SimpleTableActionButton( - type: action, - enableTopBorder: index == 0, - enableBottomBorder: index == actionGroup.length - 1, - onTap: () => _onActionTap( - context, - action: action, - ), - ), - }, - ); - // if the action is not the first or last in the group, add a divider - if (index != actionGroup.length - 1) { - widgets.add(const FlowyDivider()); - } - } - - // add padding to separate the action groups - if (actionGroupIndex != actionGroups.length - 1) { - widgets.add(const VSpace(16)); - } - } - - return widgets; - } - - void _onActionTap( - BuildContext context, { - required SimpleTableMoreAction action, - }) { - final optionCubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - switch (action) { - case SimpleTableMoreAction.setToPageWidth: - editorState.setColumnWidthToPageWidth(tableNode: tableNode); - case SimpleTableMoreAction.distributeColumnsEvenly: - editorState.distributeColumnWidthToPageWidth(tableNode: tableNode); - case SimpleTableMoreAction.duplicateTable: - optionCubit.handleAction(OptionAction.duplicate, tableNode); - case SimpleTableMoreAction.copyLinkToBlock: - optionCubit.handleAction(OptionAction.copyLinkToBlock, tableNode); - case SimpleTableMoreAction.align: - onAlignTap(); - default: - assert(false, 'Unsupported action: $action'); - break; - } - - // close the action menu - if (action != SimpleTableMoreAction.align) { - Navigator.of(context).pop(); - } - } -} - -class SimpleTableContentAlignAction extends StatefulWidget { - const SimpleTableContentAlignAction({ - super.key, - required this.isSelected, - required this.align, - required this.onTap, - }); - - final bool isSelected; - final VoidCallback onTap; - final TableAlign align; - - @override - State createState() => - _SimpleTableContentAlignActionState(); -} - -class _SimpleTableContentAlignActionState - extends State { - @override - Widget build(BuildContext context) { - return Expanded( - child: SimpleTableContentActionDecorator( - backgroundColor: - widget.isSelected ? Theme.of(context).colorScheme.primary : null, - enableLeftBorder: widget.align == TableAlign.left, - enableRightBorder: widget.align == TableAlign.right, - child: AnimatedGestureDetector( - onTapUp: widget.onTap, - child: FlowySvg( - widget.align.leftIconSvg, - size: const Size.square(24), - color: widget.isSelected - ? Theme.of(context).colorScheme.onPrimary - : null, - ), - ), - ), - ); - } -} - -/// Quick actions for the table -/// -/// - Copy -/// - Paste -/// - Cut -/// - Delete -class SimpleTableQuickActions extends StatelessWidget { - const SimpleTableQuickActions({ - super.key, - required this.tableNode, - required this.editorState, - }); - - final Node tableNode; - final EditorState editorState; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: SimpleTableConstants.actionSheetQuickActionSectionHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - SimpleTableQuickAction( - type: SimpleTableMoreAction.cut, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.cut, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.copy, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.copy, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.paste, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.paste, - ), - ), - SimpleTableQuickAction( - type: SimpleTableMoreAction.delete, - onTap: () => _onActionTap( - context, - SimpleTableMoreAction.delete, - ), - ), - ], - ), - ); - } - - void _onActionTap(BuildContext context, SimpleTableMoreAction action) { - switch (action) { - case SimpleTableMoreAction.cut: - _onCut(tableNode); - case SimpleTableMoreAction.copy: - _onCopy(tableNode); - case SimpleTableMoreAction.paste: - _onPaste(tableNode); - case SimpleTableMoreAction.delete: - _onDelete(tableNode); - default: - assert(false, 'Unsupported action: $action'); - } - - // close the action menu - Navigator.of(context).pop(); - } - - Future _onCut(Node tableNode) async { - final data = await editorState.copyTable( - tableNode: tableNode, - clearContent: true, - ); - if (data != null) { - await getIt().setData(data); - } - } - - Future _onCopy(Node tableNode) async { - final data = await editorState.copyTable( - tableNode: tableNode, - ); - if (data != null) { - await getIt().setData(data); - } - } - - void _onPaste(Node tableNode) => editorState.pasteTable( - tableNode: tableNode, - ); - - void _onDelete(Node tableNode) { - final transaction = editorState.transaction; - transaction.deleteNode(tableNode); - editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_action_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_action_sheet.dart deleted file mode 100644 index 269498b341..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_action_sheet.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; - -class SimpleTableMobileDraggableReorderButton extends StatelessWidget { - const SimpleTableMobileDraggableReorderButton({ - super.key, - required this.cellNode, - required this.index, - required this.isShowingMenu, - required this.type, - required this.editorState, - required this.simpleTableContext, - }); - - final Node cellNode; - final int index; - final ValueNotifier isShowingMenu; - final SimpleTableMoreActionType type; - final EditorState editorState; - final SimpleTableContext simpleTableContext; - - @override - Widget build(BuildContext context) { - return Draggable( - data: index, - onDragStarted: () => _startDragging(), - onDragUpdate: (details) => _onDragUpdate(details), - onDragEnd: (_) => _stopDragging(), - feedback: SimpleTableFeedback( - editorState: editorState, - node: cellNode, - type: type, - index: index, - ), - child: SimpleTableMobileReorderButton( - index: index, - type: type, - node: cellNode, - isShowingMenu: isShowingMenu, - ), - ); - } - - void _startDragging() { - HapticFeedback.lightImpact(); - - isShowingMenu.value = true; - editorState.selection = null; - - switch (type) { - case SimpleTableMoreActionType.column: - simpleTableContext.isReorderingColumn.value = (true, index); - - case SimpleTableMoreActionType.row: - simpleTableContext.isReorderingRow.value = (true, index); - } - } - - void _onDragUpdate(DragUpdateDetails details) { - simpleTableContext.reorderingOffset.value = details.globalPosition; - } - - void _stopDragging() { - isShowingMenu.value = false; - - switch (type) { - case SimpleTableMoreActionType.column: - _reorderColumn(); - case SimpleTableMoreActionType.row: - _reorderRow(); - } - - simpleTableContext.reorderingOffset.value = Offset.zero; - simpleTableContext.isReorderingHitIndex.value = null; - - switch (type) { - case SimpleTableMoreActionType.column: - simpleTableContext.isReorderingColumn.value = (false, -1); - break; - case SimpleTableMoreActionType.row: - simpleTableContext.isReorderingRow.value = (false, -1); - break; - } - } - - void _reorderColumn() { - final fromIndex = simpleTableContext.isReorderingColumn.value.$2; - final toIndex = simpleTableContext.isReorderingHitIndex.value; - if (toIndex == null) { - return; - } - - editorState.reorderColumn( - cellNode, - fromIndex: fromIndex, - toIndex: toIndex, - ); - } - - void _reorderRow() { - final fromIndex = simpleTableContext.isReorderingRow.value.$2; - final toIndex = simpleTableContext.isReorderingHitIndex.value; - if (toIndex == null) { - return; - } - - editorState.reorderRow( - cellNode, - fromIndex: fromIndex, - toIndex: toIndex, - ); - } -} - -class SimpleTableMobileReorderButton extends StatefulWidget { - const SimpleTableMobileReorderButton({ - super.key, - required this.index, - required this.type, - required this.node, - required this.isShowingMenu, - }); - - final int index; - final SimpleTableMoreActionType type; - final Node node; - final ValueNotifier isShowingMenu; - - @override - State createState() => - _SimpleTableMobileReorderButtonState(); -} - -class _SimpleTableMobileReorderButtonState - extends State { - late final EditorState editorState = context.read(); - late final SimpleTableContext simpleTableContext = - context.read(); - - @override - void initState() { - super.initState(); - - simpleTableContext.selectingRow.addListener(_onUpdateShowingMenu); - simpleTableContext.selectingColumn.addListener(_onUpdateShowingMenu); - } - - @override - void dispose() { - simpleTableContext.selectingRow.removeListener(_onUpdateShowingMenu); - simpleTableContext.selectingColumn.removeListener(_onUpdateShowingMenu); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () async => _onSelecting(), - behavior: HitTestBehavior.opaque, - child: SizedBox( - height: widget.type == SimpleTableMoreActionType.column - ? SimpleTableConstants.columnActionSheetHitTestAreaHeight - : null, - width: widget.type == SimpleTableMoreActionType.row - ? SimpleTableConstants.rowActionSheetHitTestAreaWidth - : null, - child: Align( - child: SimpleTableReorderButton( - isShowingMenu: widget.isShowingMenu, - type: widget.type, - ), - ), - ), - ); - } - - Future _onSelecting() async { - widget.isShowingMenu.value = true; - - // update the selecting row or column - switch (widget.type) { - case SimpleTableMoreActionType.column: - simpleTableContext.selectingColumn.value = widget.index; - simpleTableContext.selectingRow.value = null; - break; - case SimpleTableMoreActionType.row: - simpleTableContext.selectingRow.value = widget.index; - simpleTableContext.selectingColumn.value = null; - } - - editorState.selection = null; - - // show the bottom sheet - await showMobileBottomSheet( - context, - useSafeArea: false, - showDragHandle: true, - showDivider: false, - enablePadding: false, - builder: (context) => Provider.value( - value: simpleTableContext, - child: SimpleTableCellBottomSheet( - type: widget.type, - cellNode: widget.node, - editorState: editorState, - ), - ), - ); - - // reset the selecting row or column - simpleTableContext.selectingRow.value = null; - simpleTableContext.selectingColumn.value = null; - - widget.isShowingMenu.value = false; - } - - void _onUpdateShowingMenu() { - // highlight the reorder button when the row or column is selected - final selectingRow = simpleTableContext.selectingRow.value; - final selectingColumn = simpleTableContext.selectingColumn.value; - - if (selectingRow == widget.index && - widget.type == SimpleTableMoreActionType.row) { - widget.isShowingMenu.value = true; - } else if (selectingColumn == widget.index && - widget.type == SimpleTableMoreActionType.column) { - widget.isShowingMenu.value = true; - } else { - widget.isShowingMenu.value = false; - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_and_row_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_and_row_button.dart deleted file mode 100644 index 1655920ef5..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_and_row_button.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:provider/provider.dart'; - -class SimpleTableAddColumnAndRowHoverButton extends StatelessWidget { - const SimpleTableAddColumnAndRowHoverButton({ - super.key, - required this.editorState, - required this.node, - }); - - final EditorState editorState; - final Node node; - - @override - Widget build(BuildContext context) { - assert(node.type == SimpleTableBlockKeys.type); - - if (node.type != SimpleTableBlockKeys.type) { - return const SizedBox.shrink(); - } - - return ValueListenableBuilder( - valueListenable: context.read().isHoveringOnTableArea, - builder: (context, isHoveringOnTableArea, child) { - return ValueListenableBuilder( - valueListenable: context.read().hoveringTableCell, - builder: (context, hoveringTableCell, child) { - bool shouldShow = isHoveringOnTableArea; - if (hoveringTableCell != null && - SimpleTableConstants.enableHoveringLogicV2) { - shouldShow = hoveringTableCell.isLastCellInTable; - } - return shouldShow - ? Positioned( - bottom: - SimpleTableConstants.addColumnAndRowButtonBottomPadding, - right: SimpleTableConstants.addColumnButtonPadding, - child: SimpleTableAddColumnAndRowButton( - onTap: () { - // cancel the selection to avoid flashing the selection - editorState.selection = null; - - editorState.addColumnAndRowInTable(node); - }, - ), - ) - : const SizedBox.shrink(); - }, - ); - }, - ); - } -} - -class SimpleTableAddColumnAndRowButton extends StatelessWidget { - const SimpleTableAddColumnAndRowButton({ - super.key, - this.onTap, - }); - - final VoidCallback? onTap; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.document_plugins_simpleTable_clickToAddNewRowAndColumn - .tr(), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onTap, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - width: SimpleTableConstants.addColumnAndRowButtonWidth, - height: SimpleTableConstants.addColumnAndRowButtonHeight, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - SimpleTableConstants.addColumnAndRowButtonCornerRadius, - ), - color: context.simpleTableMoreActionBackgroundColor, - ), - child: const FlowySvg( - FlowySvgs.add_s, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_button.dart deleted file mode 100644 index 29a24ba623..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_column_button.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:provider/provider.dart'; - -class SimpleTableAddColumnHoverButton extends StatefulWidget { - const SimpleTableAddColumnHoverButton({ - super.key, - required this.editorState, - required this.tableNode, - }); - - final EditorState editorState; - final Node tableNode; - - @override - State createState() => - _SimpleTableAddColumnHoverButtonState(); -} - -class _SimpleTableAddColumnHoverButtonState - extends State { - late final interceptorKey = - 'simple_table_add_column_hover_button_${widget.tableNode.id}'; - - SelectionGestureInterceptor? interceptor; - - Offset? startDraggingOffset; - int? initialColumnCount; - - @override - void initState() { - super.initState(); - - interceptor = SelectionGestureInterceptor( - key: interceptorKey, - canTap: (details) => !_isTapInBounds(details.globalPosition), - ); - widget.editorState.service.selectionService - .registerGestureInterceptor(interceptor!); - } - - @override - void dispose() { - widget.editorState.service.selectionService.unregisterGestureInterceptor( - interceptorKey, - ); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - assert(widget.tableNode.type == SimpleTableBlockKeys.type); - - if (widget.tableNode.type != SimpleTableBlockKeys.type) { - return const SizedBox.shrink(); - } - - return ValueListenableBuilder( - valueListenable: context.read().isHoveringOnTableArea, - builder: (context, isHoveringOnTableArea, _) { - return ValueListenableBuilder( - valueListenable: context.read().hoveringTableCell, - builder: (context, hoveringTableCell, _) { - bool shouldShow = isHoveringOnTableArea; - if (hoveringTableCell != null && - SimpleTableConstants.enableHoveringLogicV2) { - shouldShow = hoveringTableCell.columnIndex + 1 == - hoveringTableCell.columnLength; - } - return Positioned( - top: SimpleTableConstants.tableHitTestTopPadding - - SimpleTableConstants.cellBorderWidth, - bottom: SimpleTableConstants.addColumnButtonBottomPadding, - right: 0, - child: Opacity( - opacity: shouldShow ? 1.0 : 0.0, - child: SimpleTableAddColumnButton( - onTap: () { - // cancel the selection to avoid flashing the selection - widget.editorState.selection = null; - - widget.editorState.addColumnInTable(widget.tableNode); - }, - onHorizontalDragStart: (details) { - context.read().isDraggingColumn = true; - startDraggingOffset = details.globalPosition; - initialColumnCount = widget.tableNode.columnLength; - }, - onHorizontalDragEnd: (details) { - context.read().isDraggingColumn = false; - }, - onHorizontalDragUpdate: (details) { - _insertColumnInMemory(details); - }, - ), - ), - ); - }, - ); - }, - ); - } - - bool _isTapInBounds(Offset offset) { - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null) { - return false; - } - - final localPosition = renderBox.globalToLocal(offset); - final result = renderBox.paintBounds.contains(localPosition); - - return result; - } - - void _insertColumnInMemory(DragUpdateDetails details) { - if (!SimpleTableConstants.enableDragToExpandTable) { - return; - } - - if (startDraggingOffset == null || initialColumnCount == null) { - return; - } - - // calculate the horizontal offset from the start dragging offset - final horizontalOffset = - details.globalPosition.dx - startDraggingOffset!.dx; - - const columnWidth = SimpleTableConstants.defaultColumnWidth; - final columnDelta = (horizontalOffset / columnWidth).round(); - - // if the change is less than 1 column, skip the operation - if (columnDelta.abs() < 1) { - return; - } - - final firstEmptyColumnFromRight = - widget.tableNode.getFirstEmptyColumnFromRight(); - if (firstEmptyColumnFromRight == null) { - return; - } - - final currentColumnCount = widget.tableNode.columnLength; - final targetColumnCount = initialColumnCount! + columnDelta; - - // There're 3 cases that we don't want to proceed: - // 1. targetColumnCount < 0: the table at least has 1 column - // 2. targetColumnCount == currentColumnCount: the table has no change - // 3. targetColumnCount <= initialColumnCount: the table has less columns than the initial column count - if (targetColumnCount <= 0 || - targetColumnCount == currentColumnCount || - targetColumnCount <= firstEmptyColumnFromRight) { - return; - } - - if (targetColumnCount > currentColumnCount) { - widget.editorState.insertColumnInTable( - widget.tableNode, - targetColumnCount, - inMemoryUpdate: true, - ); - } else { - widget.editorState.deleteColumnInTable( - widget.tableNode, - targetColumnCount, - inMemoryUpdate: true, - ); - } - } -} - -class SimpleTableAddColumnButton extends StatelessWidget { - const SimpleTableAddColumnButton({ - super.key, - this.onTap, - required this.onHorizontalDragStart, - required this.onHorizontalDragEnd, - required this.onHorizontalDragUpdate, - }); - - final VoidCallback? onTap; - final void Function(DragStartDetails) onHorizontalDragStart; - final void Function(DragEndDetails) onHorizontalDragEnd; - final void Function(DragUpdateDetails) onHorizontalDragUpdate; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.document_plugins_simpleTable_clickToAddNewColumn.tr(), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onTap, - onHorizontalDragStart: onHorizontalDragStart, - onHorizontalDragEnd: onHorizontalDragEnd, - onHorizontalDragUpdate: onHorizontalDragUpdate, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - width: SimpleTableConstants.addColumnButtonWidth, - margin: const EdgeInsets.symmetric( - horizontal: SimpleTableConstants.addColumnButtonPadding, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - SimpleTableConstants.addColumnButtonRadius, - ), - color: context.simpleTableMoreActionBackgroundColor, - ), - child: const FlowySvg( - FlowySvgs.add_s, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_row_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_row_button.dart deleted file mode 100644 index 00ca444afd..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_add_row_button.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:provider/provider.dart'; - -class SimpleTableAddRowHoverButton extends StatefulWidget { - const SimpleTableAddRowHoverButton({ - super.key, - required this.editorState, - required this.tableNode, - }); - - final EditorState editorState; - final Node tableNode; - - @override - State createState() => - _SimpleTableAddRowHoverButtonState(); -} - -class _SimpleTableAddRowHoverButtonState - extends State { - late final interceptorKey = - 'simple_table_add_row_hover_button_${widget.tableNode.id}'; - - SelectionGestureInterceptor? interceptor; - - Offset? startDraggingOffset; - int? initialRowCount; - - @override - void initState() { - super.initState(); - - interceptor = SelectionGestureInterceptor( - key: interceptorKey, - canTap: (details) => !_isTapInBounds(details.globalPosition), - ); - widget.editorState.service.selectionService - .registerGestureInterceptor(interceptor!); - } - - @override - void dispose() { - widget.editorState.service.selectionService.unregisterGestureInterceptor( - interceptorKey, - ); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - assert(widget.tableNode.type == SimpleTableBlockKeys.type); - - if (widget.tableNode.type != SimpleTableBlockKeys.type) { - return const SizedBox.shrink(); - } - - final simpleTableContext = context.read(); - return ValueListenableBuilder( - valueListenable: simpleTableContext.isHoveringOnTableArea, - builder: (context, isHoveringOnTableArea, child) { - return ValueListenableBuilder( - valueListenable: simpleTableContext.hoveringTableCell, - builder: (context, hoveringTableCell, _) { - bool shouldShow = isHoveringOnTableArea; - if (hoveringTableCell != null && - SimpleTableConstants.enableHoveringLogicV2) { - shouldShow = - hoveringTableCell.rowIndex + 1 == hoveringTableCell.rowLength; - } - if (simpleTableContext.isDraggingRow) { - shouldShow = true; - } - return shouldShow ? child! : const SizedBox.shrink(); - }, - ); - }, - child: Positioned( - bottom: 2 * SimpleTableConstants.addRowButtonPadding, - left: SimpleTableConstants.tableLeftPadding - - SimpleTableConstants.cellBorderWidth, - right: SimpleTableConstants.addRowButtonRightPadding, - child: SimpleTableAddRowButton( - onTap: () { - // cancel the selection to avoid flashing the selection - widget.editorState.selection = null; - - widget.editorState.addRowInTable( - widget.tableNode, - ); - }, - onVerticalDragStart: (details) { - context.read().isDraggingRow = true; - startDraggingOffset = details.globalPosition; - initialRowCount = widget.tableNode.children.length; - }, - onVerticalDragEnd: (details) { - context.read().isDraggingRow = false; - }, - onVerticalDragUpdate: (details) { - _insertRowInMemory(details); - }, - ), - ), - ); - } - - bool _isTapInBounds(Offset offset) { - final renderBox = context.findRenderObject() as RenderBox?; - if (renderBox == null) { - return false; - } - - final localPosition = renderBox.globalToLocal(offset); - final result = renderBox.paintBounds.contains(localPosition); - - return result; - } - - void _insertRowInMemory(DragUpdateDetails details) { - if (!SimpleTableConstants.enableDragToExpandTable) { - return; - } - - if (startDraggingOffset == null || initialRowCount == null) { - return; - } - - // calculate the vertical offset from the start dragging offset - final verticalOffset = details.globalPosition.dy - startDraggingOffset!.dy; - - const rowHeight = SimpleTableConstants.defaultRowHeight; - final rowDelta = (verticalOffset / rowHeight).round(); - - // if the change is less than 1 row, skip the operation - if (rowDelta.abs() < 1) { - return; - } - - final firstEmptyRowFromBottom = - widget.tableNode.getFirstEmptyRowFromBottom(); - if (firstEmptyRowFromBottom == null) { - return; - } - - final currentRowCount = widget.tableNode.children.length; - final targetRowCount = initialRowCount! + rowDelta; - - // There're 3 cases that we don't want to proceed: - // 1. targetRowCount < 0: the table at least has 1 row - // 2. targetRowCount == currentRowCount: the table has no change - // 3. targetRowCount <= initialRowCount: the table has less rows than the initial row count - if (targetRowCount <= 0 || - targetRowCount == currentRowCount || - targetRowCount <= firstEmptyRowFromBottom.$1) { - return; - } - - if (targetRowCount > currentRowCount) { - widget.editorState.insertRowInTable( - widget.tableNode, - targetRowCount, - inMemoryUpdate: true, - ); - } else { - widget.editorState.deleteRowInTable( - widget.tableNode, - targetRowCount, - inMemoryUpdate: true, - ); - } - } -} - -class SimpleTableAddRowButton extends StatelessWidget { - const SimpleTableAddRowButton({ - super.key, - this.onTap, - required this.onVerticalDragStart, - required this.onVerticalDragEnd, - required this.onVerticalDragUpdate, - }); - - final VoidCallback? onTap; - final void Function(DragStartDetails) onVerticalDragStart; - final void Function(DragEndDetails) onVerticalDragEnd; - final void Function(DragUpdateDetails) onVerticalDragUpdate; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.document_plugins_simpleTable_clickToAddNewRow.tr(), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - onVerticalDragStart: onVerticalDragStart, - onVerticalDragEnd: onVerticalDragEnd, - onVerticalDragUpdate: onVerticalDragUpdate, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - height: SimpleTableConstants.addRowButtonHeight, - margin: const EdgeInsets.symmetric( - vertical: SimpleTableConstants.addColumnButtonPadding, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - SimpleTableConstants.addRowButtonRadius, - ), - color: context.simpleTableMoreActionBackgroundColor, - ), - child: const FlowySvg( - FlowySvgs.add_s, - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_align_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_align_button.dart deleted file mode 100644 index a042e632ea..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_align_button.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.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:provider/provider.dart'; - -class SimpleTableAlignMenu extends StatefulWidget { - const SimpleTableAlignMenu({ - super.key, - required this.type, - required this.tableCellNode, - this.mutex, - }); - - final SimpleTableMoreActionType type; - final Node tableCellNode; - final PopoverMutex? mutex; - - @override - State createState() => _SimpleTableAlignMenuState(); -} - -class _SimpleTableAlignMenuState extends State { - @override - Widget build(BuildContext context) { - final align = switch (widget.type) { - SimpleTableMoreActionType.column => widget.tableCellNode.columnAlign, - SimpleTableMoreActionType.row => widget.tableCellNode.rowAlign, - }; - return AppFlowyPopover( - mutex: widget.mutex, - child: SimpleTableBasicButton( - leftIconSvg: align.leftIconSvg, - text: LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(), - onTap: () {}, - ), - popupBuilder: (popoverContext) { - void onClose() => PopoverContainer.of(popoverContext).closeAll(); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildAlignButton(context, TableAlign.left, onClose), - _buildAlignButton(context, TableAlign.center, onClose), - _buildAlignButton(context, TableAlign.right, onClose), - ], - ); - }, - ); - } - - Widget _buildAlignButton( - BuildContext context, - TableAlign align, - VoidCallback onClose, - ) { - return SimpleTableBasicButton( - leftIconSvg: align.leftIconSvg, - text: align.name, - onTap: () { - switch (widget.type) { - case SimpleTableMoreActionType.column: - context.read().updateColumnAlign( - tableCellNode: widget.tableCellNode, - align: align, - ); - break; - case SimpleTableMoreActionType.row: - context.read().updateRowAlign( - tableCellNode: widget.tableCellNode, - align: align, - ); - break; - } - - onClose(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_background_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_background_menu.dart deleted file mode 100644 index ef55081a14..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_background_menu.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.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:provider/provider.dart'; - -class SimpleTableBackgroundColorMenu extends StatefulWidget { - const SimpleTableBackgroundColorMenu({ - super.key, - required this.type, - required this.tableCellNode, - this.mutex, - }); - - final SimpleTableMoreActionType type; - final Node tableCellNode; - final PopoverMutex? mutex; - - @override - State createState() => - _SimpleTableBackgroundColorMenuState(); -} - -class _SimpleTableBackgroundColorMenuState - extends State { - @override - Widget build(BuildContext context) { - final theme = AFThemeExtension.of(context); - final backgroundColor = switch (widget.type) { - SimpleTableMoreActionType.row => - widget.tableCellNode.buildRowColor(context), - SimpleTableMoreActionType.column => - widget.tableCellNode.buildColumnColor(context), - }; - return AppFlowyPopover( - mutex: widget.mutex, - popupBuilder: (popoverContext) { - return _buildColorOptionMenu( - context, - theme: theme, - onClose: () => PopoverContainer.of(popoverContext).closeAll(), - ); - }, - direction: PopoverDirection.rightWithCenterAligned, - child: SimpleTableBasicButton( - leftIconBuilder: (onHover) => ColorOptionIcon( - color: backgroundColor ?? Colors.transparent, - ), - text: LocaleKeys.document_plugins_simpleTable_moreActions_color.tr(), - onTap: () {}, - ), - ); - } - - Widget _buildColorOptionMenu( - BuildContext context, { - required AFThemeExtension theme, - required VoidCallback onClose, - }) { - final colors = [ - // reset to default background color - FlowyColorOption( - color: Colors.transparent, - i18n: LocaleKeys.document_plugins_optionAction_defaultColor.tr(), - id: optionActionColorDefaultColor, - ), - ...FlowyTint.values.map( - (e) => FlowyColorOption( - color: e.color(context, theme: theme), - i18n: e.tintName(AppFlowyEditorL10n.current), - id: e.id, - ), - ), - ]; - - return FlowyColorPicker( - colors: colors, - border: Border.all( - color: theme.onBackground, - ), - onTap: (option, index) { - switch (widget.type) { - case SimpleTableMoreActionType.column: - context.read().updateColumnBackgroundColor( - tableCellNode: widget.tableCellNode, - color: option.id, - ); - break; - case SimpleTableMoreActionType.row: - context.read().updateRowBackgroundColor( - tableCellNode: widget.tableCellNode, - color: option.id, - ); - break; - } - - onClose(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_basic_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_basic_button.dart deleted file mode 100644 index f9df88ccf0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_basic_button.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class SimpleTableBasicButton extends StatelessWidget { - const SimpleTableBasicButton({ - super.key, - required this.text, - required this.onTap, - this.leftIconSvg, - this.leftIconBuilder, - this.rightIcon, - }); - - final FlowySvgData? leftIconSvg; - final String text; - final VoidCallback onTap; - final Widget Function(bool onHover)? leftIconBuilder; - final Widget? rightIcon; - - @override - Widget build(BuildContext context) { - return Container( - height: SimpleTableConstants.moreActionHeight, - padding: SimpleTableConstants.moreActionPadding, - child: FlowyIconTextButton( - margin: SimpleTableConstants.moreActionHorizontalMargin, - leftIconBuilder: _buildLeftIcon, - iconPadding: 10.0, - textBuilder: (onHover) => FlowyText.regular( - text, - fontSize: 14.0, - figmaLineHeight: 18.0, - ), - onTap: onTap, - rightIconBuilder: (onHover) => rightIcon ?? const SizedBox.shrink(), - ), - ); - } - - Widget _buildLeftIcon(bool onHover) { - if (leftIconBuilder != null) { - return leftIconBuilder!(onHover); - } - return leftIconSvg != null - ? FlowySvg(leftIconSvg!) - : const SizedBox.shrink(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_border_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_border_builder.dart deleted file mode 100644 index e241f8bf72..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_border_builder.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SimpleTableBorderBuilder { - SimpleTableBorderBuilder({ - required this.context, - required this.simpleTableContext, - required this.node, - }); - - final BuildContext context; - final SimpleTableContext simpleTableContext; - final Node node; - - /// Build the border for the cell. - Border? buildBorder({ - bool isEditingCell = false, - }) { - if (SimpleTableConstants.borderType != SimpleTableBorderRenderType.cell) { - return null; - } - - // check if the cell is in the selected column - final isCellInSelectedColumn = - node.columnIndex == simpleTableContext.selectingColumn.value; - - // check if the cell is in the selected row - final isCellInSelectedRow = - node.rowIndex == simpleTableContext.selectingRow.value; - - final isReordering = simpleTableContext.isReordering && - (simpleTableContext.isReorderingColumn.value.$1 || - simpleTableContext.isReorderingRow.value.$1); - - final editorState = context.read(); - final editable = editorState.editable; - - if (!editable) { - return buildCellBorder(); - } else if (isReordering) { - return buildReorderingBorder(); - } else if (simpleTableContext.isSelectingTable.value) { - return buildSelectingTableBorder(); - } else if (isCellInSelectedColumn) { - return buildColumnHighlightBorder(); - } else if (isCellInSelectedRow) { - return buildRowHighlightBorder(); - } else if (isEditingCell) { - return buildEditingBorder(); - } else { - return buildCellBorder(); - } - } - - /// the column border means the `VERTICAL` border of the cell - /// - /// ____ - /// | 1 | 2 | - /// | 3 | 4 | - /// |___| - /// - /// the border wrapping the cell 2 and cell 4 is the column border - Border buildColumnHighlightBorder() { - return Border( - left: _buildHighlightBorderSide(), - right: _buildHighlightBorderSide(), - top: node.rowIndex == 0 - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - ); - } - - /// the row border means the `HORIZONTAL` border of the cell - /// - /// ________ - /// | 1 | 2 | - /// |_______| - /// | 3 | 4 | - /// - /// the border wrapping the cell 1 and cell 2 is the row border - Border buildRowHighlightBorder() { - return Border( - top: _buildHighlightBorderSide(), - bottom: _buildHighlightBorderSide(), - left: node.columnIndex == 0 - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - right: node.columnIndex + 1 == node.parentTableNode?.columnLength - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - ); - } - - /// Build the border for the reordering state. - /// - /// For example, when reordering a column, we should highlight the border of the - /// current column we're hovering. - Border buildReorderingBorder() { - final isReorderingColumn = simpleTableContext.isReorderingColumn.value.$1; - final isReorderingRow = simpleTableContext.isReorderingRow.value.$1; - - if (isReorderingColumn) { - return _buildColumnReorderingBorder(); - } else if (isReorderingRow) { - return _buildRowReorderingBorder(); - } - - return buildCellBorder(); - } - - /// Build the border for the cell without any state. - Border buildCellBorder() { - return Border( - top: node.rowIndex == 0 - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - left: node.columnIndex == 0 - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - right: node.columnIndex + 1 == node.parentTableNode?.columnLength - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - ); - } - - /// Build the border for the editing state. - Border buildEditingBorder() { - return Border.all( - color: Theme.of(context).colorScheme.primary, - width: 2, - ); - } - - /// Build the border for the selecting table state. - Border buildSelectingTableBorder() { - final rowIndex = node.rowIndex; - final columnIndex = node.columnIndex; - - return Border( - top: - rowIndex == 0 ? _buildHighlightBorderSide() : _buildLightBorderSide(), - bottom: rowIndex + 1 == node.parentTableNode?.rowLength - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - left: columnIndex == 0 - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - right: columnIndex + 1 == node.parentTableNode?.columnLength - ? _buildHighlightBorderSide() - : _buildLightBorderSide(), - ); - } - - Border _buildColumnReorderingBorder() { - assert(simpleTableContext.isReordering); - - final isDraggingInCurrentColumn = - simpleTableContext.isReorderingColumn.value.$2 == node.columnIndex; - // if the dragging column is the current column, don't show the highlight border - if (isDraggingInCurrentColumn) { - return buildCellBorder(); - } - - bool isHitCurrentCell = false; - - if (UniversalPlatform.isDesktop) { - // On desktop, we use the dragging column index to determine the highlight border - // Check if the hovering table cell column index hit the current node column index - isHitCurrentCell = - simpleTableContext.hoveringTableCell.value?.columnIndex == - node.columnIndex; - } else if (UniversalPlatform.isMobile) { - // On mobile, we use the isReorderingHitIndex to determine the highlight border - isHitCurrentCell = - simpleTableContext.isReorderingHitIndex.value == node.columnIndex; - } - - // if the hovering column is not the current column, don't show the highlight border - if (!isHitCurrentCell) { - return buildCellBorder(); - } - - // if the dragging column index is less than the current column index, show the - // highlight border on the left side - final isLeftSide = - simpleTableContext.isReorderingColumn.value.$2 > node.columnIndex; - // if the dragging column index is greater than the current column index, show - // the highlight border on the right side - final isRightSide = - simpleTableContext.isReorderingColumn.value.$2 < node.columnIndex; - - return Border( - top: node.rowIndex == 0 - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - left: isLeftSide ? _buildHighlightBorderSide() : _buildLightBorderSide(), - right: - isRightSide ? _buildHighlightBorderSide() : _buildLightBorderSide(), - ); - } - - Border _buildRowReorderingBorder() { - assert(simpleTableContext.isReordering); - - final isDraggingInCurrentRow = - simpleTableContext.isReorderingRow.value.$2 == node.rowIndex; - // if the dragging row is the current row, don't show the highlight border - if (isDraggingInCurrentRow) { - return buildCellBorder(); - } - - bool isHitCurrentCell = false; - - if (UniversalPlatform.isDesktop) { - // On desktop, we use the dragging row index to determine the highlight border - // Check if the hovering table cell row index hit the current node row index - isHitCurrentCell = - simpleTableContext.hoveringTableCell.value?.rowIndex == node.rowIndex; - } else if (UniversalPlatform.isMobile) { - // On mobile, we use the isReorderingHitIndex to determine the highlight border - isHitCurrentCell = - simpleTableContext.isReorderingHitIndex.value == node.rowIndex; - } - - if (!isHitCurrentCell) { - return buildCellBorder(); - } - - // For the row reordering, we only need to update the top and bottom border - final isTopSide = - simpleTableContext.isReorderingRow.value.$2 > node.rowIndex; - final isBottomSide = - simpleTableContext.isReorderingRow.value.$2 < node.rowIndex; - - return Border( - top: isTopSide ? _buildHighlightBorderSide() : _buildLightBorderSide(), - bottom: - isBottomSide ? _buildHighlightBorderSide() : _buildLightBorderSide(), - left: node.columnIndex == 0 - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - right: node.columnIndex + 1 == node.parentTableNode?.columnLength - ? _buildDefaultBorderSide() - : _buildLightBorderSide(), - ); - } - - BorderSide _buildHighlightBorderSide() { - return BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2, - ); - } - - BorderSide _buildLightBorderSide() { - return BorderSide( - color: context.simpleTableBorderColor, - width: 0.5, - ); - } - - BorderSide _buildDefaultBorderSide() { - return BorderSide( - color: context.simpleTableBorderColor, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart deleted file mode 100644 index 97519422ec..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_bottom_sheet.dart +++ /dev/null @@ -1,486 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -enum _SimpleTableBottomSheetMenuState { - cellActionMenu, - textColor, - textBackgroundColor, - tableActionMenu, - align, -} - -/// This bottom sheet is used for the column or row action menu. -/// When selecting a cell and tapping the action menu button around the cell, -/// this bottom sheet will be shown. -/// -/// Note: This widget is only used for mobile. -class SimpleTableCellBottomSheet extends StatefulWidget { - const SimpleTableCellBottomSheet({ - super.key, - required this.type, - required this.cellNode, - required this.editorState, - this.scrollController, - }); - - final SimpleTableMoreActionType type; - final Node cellNode; - final EditorState editorState; - final ScrollController? scrollController; - - @override - State createState() => - _SimpleTableCellBottomSheetState(); -} - -class _SimpleTableCellBottomSheetState - extends State { - _SimpleTableBottomSheetMenuState menuState = - _SimpleTableBottomSheetMenuState.cellActionMenu; - - Color? selectedTextColor; - Color? selectedCellBackgroundColor; - TableAlign? selectedAlign; - - @override - void initState() { - super.initState(); - - selectedTextColor = switch (widget.type) { - SimpleTableMoreActionType.column => - widget.cellNode.textColorInColumn?.tryToColor(), - SimpleTableMoreActionType.row => - widget.cellNode.textColorInRow?.tryToColor(), - }; - - selectedCellBackgroundColor = switch (widget.type) { - SimpleTableMoreActionType.column => - widget.cellNode.buildColumnColor(context), - SimpleTableMoreActionType.row => widget.cellNode.buildRowColor(context), - }; - - selectedAlign = switch (widget.type) { - SimpleTableMoreActionType.column => widget.cellNode.columnAlign, - SimpleTableMoreActionType.row => widget.cellNode.rowAlign, - }; - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // header - _buildHeader(), - - // content - ...menuState == _SimpleTableBottomSheetMenuState.cellActionMenu - ? _buildScrollableContent() - : _buildNonScrollableContent(), - ], - ); - } - - Widget _buildHeader() { - switch (menuState) { - case _SimpleTableBottomSheetMenuState.cellActionMenu: - return BottomSheetHeader( - showBackButton: false, - showCloseButton: true, - showDoneButton: false, - showRemoveButton: false, - title: widget.type.name.capitalize(), - onClose: () => Navigator.pop(context), - ); - case _SimpleTableBottomSheetMenuState.textColor || - _SimpleTableBottomSheetMenuState.textBackgroundColor: - return BottomSheetHeader( - showBackButton: false, - showCloseButton: true, - showDoneButton: true, - showRemoveButton: false, - title: widget.type.name.capitalize(), - onClose: () => setState(() { - menuState = _SimpleTableBottomSheetMenuState.cellActionMenu; - }), - onDone: (_) => Navigator.pop(context), - ); - default: - throw UnimplementedError('Unsupported menu state: $menuState'); - } - } - - List _buildScrollableContent() { - return [ - SizedBox( - height: SimpleTableConstants.actionSheetBottomSheetHeight, - child: Scrollbar( - controller: widget.scrollController, - child: SingleChildScrollView( - controller: widget.scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ..._buildContent(), - - // safe area padding - VSpace(context.bottomSheetPadding() * 2), - ], - ), - ), - ), - ), - ]; - } - - List _buildNonScrollableContent() { - return [ - ..._buildContent(), - - // safe area padding - VSpace(context.bottomSheetPadding()), - ]; - } - - List _buildContent() { - switch (menuState) { - case _SimpleTableBottomSheetMenuState.cellActionMenu: - return _buildActionButtons(); - case _SimpleTableBottomSheetMenuState.textColor: - return _buildTextColor(); - case _SimpleTableBottomSheetMenuState.textBackgroundColor: - return _buildTextBackgroundColor(); - default: - throw UnimplementedError('Unsupported menu state: $menuState'); - } - } - - List _buildActionButtons() { - return [ - // copy, cut, paste, delete - SimpleTableCellQuickActions( - type: widget.type, - cellNode: widget.cellNode, - editorState: widget.editorState, - ), - const VSpace(12), - - // insert row, insert column - SimpleTableInsertActions( - type: widget.type, - cellNode: widget.cellNode, - editorState: widget.editorState, - ), - const VSpace(12), - - // content actions - SimpleTableContentActions( - type: widget.type, - cellNode: widget.cellNode, - editorState: widget.editorState, - selectedAlign: selectedAlign, - selectedTextColor: selectedTextColor, - selectedCellBackgroundColor: selectedCellBackgroundColor, - onTextColorSelected: () { - setState(() { - menuState = _SimpleTableBottomSheetMenuState.textColor; - }); - }, - onCellBackgroundColorSelected: () { - setState(() { - menuState = _SimpleTableBottomSheetMenuState.textBackgroundColor; - }); - }, - onAlignTap: _onAlignTap, - ), - const VSpace(16), - - // action buttons - SimpleTableCellActionButtons( - type: widget.type, - cellNode: widget.cellNode, - editorState: widget.editorState, - ), - ]; - } - - List _buildTextColor() { - return [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: FlowyText( - LocaleKeys.document_plugins_simpleTable_moreActions_textColor.tr(), - fontSize: 14.0, - ), - ), - const VSpace(12.0), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: EditorTextColorWidget( - onSelectedColor: _onTextColorSelected, - selectedColor: selectedTextColor, - ), - ), - ]; - } - - List _buildTextBackgroundColor() { - return [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: FlowyText( - LocaleKeys - .document_plugins_simpleTable_moreActions_cellBackgroundColor - .tr(), - fontSize: 14.0, - ), - ), - const VSpace(12.0), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - ), - child: EditorBackgroundColors( - onSelectedColor: _onCellBackgroundColorSelected, - selectedColor: selectedCellBackgroundColor, - ), - ), - ]; - } - - void _onTextColorSelected(Color color) { - final hex = color.a == 0 ? null : color.toHex(); - switch (widget.type) { - case SimpleTableMoreActionType.column: - widget.editorState.updateColumnTextColor( - tableCellNode: widget.cellNode, - color: hex ?? '', - ); - case SimpleTableMoreActionType.row: - widget.editorState.updateRowTextColor( - tableCellNode: widget.cellNode, - color: hex ?? '', - ); - } - - setState(() { - selectedTextColor = color; - }); - } - - void _onCellBackgroundColorSelected(Color color) { - final hex = color.a == 0 ? null : color.toHex(); - switch (widget.type) { - case SimpleTableMoreActionType.column: - widget.editorState.updateColumnBackgroundColor( - tableCellNode: widget.cellNode, - color: hex ?? '', - ); - case SimpleTableMoreActionType.row: - widget.editorState.updateRowBackgroundColor( - tableCellNode: widget.cellNode, - color: hex ?? '', - ); - } - - setState(() { - selectedCellBackgroundColor = color; - }); - } - - void _onAlignTap(TableAlign align) { - switch (widget.type) { - case SimpleTableMoreActionType.column: - widget.editorState.updateColumnAlign( - tableCellNode: widget.cellNode, - align: align, - ); - case SimpleTableMoreActionType.row: - widget.editorState.updateRowAlign( - tableCellNode: widget.cellNode, - align: align, - ); - } - - setState(() { - selectedAlign = align; - }); - } -} - -/// This bottom sheet is used for the table action menu. -/// When selecting a table and tapping the action menu button on the top-left corner of the table, -/// this bottom sheet will be shown. -/// -/// Note: This widget is only used for mobile. -class SimpleTableBottomSheet extends StatefulWidget { - const SimpleTableBottomSheet({ - super.key, - required this.tableNode, - required this.editorState, - this.scrollController, - }); - - final Node tableNode; - final EditorState editorState; - final ScrollController? scrollController; - - @override - State createState() => _SimpleTableBottomSheetState(); -} - -class _SimpleTableBottomSheetState extends State { - _SimpleTableBottomSheetMenuState menuState = - _SimpleTableBottomSheetMenuState.tableActionMenu; - - TableAlign? selectedAlign; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // header - _buildHeader(), - - // content - SizedBox( - height: SimpleTableConstants.actionSheetBottomSheetHeight, - child: Scrollbar( - controller: widget.scrollController, - child: SingleChildScrollView( - controller: widget.scrollController, - child: Column( - children: [ - // content - ..._buildContent(), - - // safe area padding - VSpace(context.bottomSheetPadding() * 2), - ], - ), - ), - ), - ), - ], - ); - } - - Widget _buildHeader() { - switch (menuState) { - case _SimpleTableBottomSheetMenuState.tableActionMenu: - return BottomSheetHeader( - showBackButton: false, - showCloseButton: true, - showDoneButton: false, - showRemoveButton: false, - title: LocaleKeys.document_plugins_simpleTable_headerName_table.tr(), - onClose: () => Navigator.pop(context), - ); - case _SimpleTableBottomSheetMenuState.align: - return BottomSheetHeader( - showBackButton: true, - showCloseButton: false, - showDoneButton: true, - showRemoveButton: false, - title: LocaleKeys.document_plugins_simpleTable_headerName_table.tr(), - onBack: () => setState(() { - menuState = _SimpleTableBottomSheetMenuState.tableActionMenu; - }), - onDone: (_) => Navigator.pop(context), - ); - default: - throw UnimplementedError('Unsupported menu state: $menuState'); - } - } - - List _buildContent() { - switch (menuState) { - case _SimpleTableBottomSheetMenuState.tableActionMenu: - return _buildActionButtons(); - case _SimpleTableBottomSheetMenuState.align: - return _buildAlign(); - default: - throw UnimplementedError('Unsupported menu state: $menuState'); - } - } - - List _buildActionButtons() { - return [ - // quick actions - // copy, cut, paste, delete - SimpleTableQuickActions( - tableNode: widget.tableNode, - editorState: widget.editorState, - ), - const VSpace(24), - - // action buttons - SimpleTableActionButtons( - tableNode: widget.tableNode, - editorState: widget.editorState, - onAlignTap: _onTapAlignButton, - ), - ]; - } - - List _buildAlign() { - return [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: Row( - children: [ - _buildAlignButton(TableAlign.left), - const HSpace(2), - _buildAlignButton(TableAlign.center), - const HSpace(2), - _buildAlignButton(TableAlign.right), - ], - ), - ), - ]; - } - - Widget _buildAlignButton(TableAlign align) { - return SimpleTableContentAlignAction( - onTap: () => _onTapAlign(align), - align: align, - isSelected: selectedAlign == align, - ); - } - - void _onTapAlignButton() { - setState(() { - menuState = _SimpleTableBottomSheetMenuState.align; - }); - } - - void _onTapAlign(TableAlign align) { - setState(() { - selectedAlign = align; - }); - - widget.editorState.updateTableAlign( - tableNode: widget.tableNode, - align: align, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_column_resize_handle.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_column_resize_handle.dart deleted file mode 100644 index ab941cb4d1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_column_resize_handle.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'dart:ui'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SimpleTableColumnResizeHandle extends StatefulWidget { - const SimpleTableColumnResizeHandle({ - super.key, - required this.node, - this.isPreviousCell = false, - }); - - final Node node; - final bool isPreviousCell; - - @override - State createState() => - _SimpleTableColumnResizeHandleState(); -} - -class _SimpleTableColumnResizeHandleState - extends State { - late final simpleTableContext = context.read(); - - bool isStartDragging = false; - - // record the previous position of the drag, only used on mobile - double previousDx = 0; - - @override - Widget build(BuildContext context) { - return UniversalPlatform.isMobile - ? _buildMobileResizeHandle() - : _buildDesktopResizeHandle(); - } - - Widget _buildDesktopResizeHandle() { - return MouseRegion( - cursor: SystemMouseCursors.resizeColumn, - onEnter: (_) => _onEnterHoverArea(), - onExit: (event) => _onExitHoverArea(), - child: GestureDetector( - onHorizontalDragStart: _onHorizontalDragStart, - onHorizontalDragUpdate: _onHorizontalDragUpdate, - onHorizontalDragEnd: _onHorizontalDragEnd, - child: ValueListenableBuilder( - valueListenable: simpleTableContext.hoveringOnResizeHandle, - builder: (context, hoveringOnResizeHandle, child) { - // when reordering a column, the resize handle should not be shown - final isSameRowIndex = hoveringOnResizeHandle?.columnIndex == - widget.node.columnIndex && - !simpleTableContext.isReordering; - return Opacity( - opacity: isSameRowIndex ? 1.0 : 0.0, - child: child, - ); - }, - child: Container( - height: double.infinity, - width: SimpleTableConstants.resizeHandleWidth, - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ); - } - - Widget _buildMobileResizeHandle() { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onLongPressStart: _onLongPressStart, - onLongPressMoveUpdate: _onLongPressMoveUpdate, - onLongPressEnd: _onLongPressEnd, - onLongPressCancel: _onLongPressCancel, - child: ValueListenableBuilder( - valueListenable: simpleTableContext.resizingCell, - builder: (context, resizingCell, child) { - final isSameColumnIndex = - widget.node.columnIndex == resizingCell?.columnIndex; - if (!isSameColumnIndex) { - return child!; - } - return Container( - width: 10, - alignment: !widget.isPreviousCell - ? Alignment.centerRight - : Alignment.centerLeft, - child: Container( - width: 2, - color: Theme.of(context).colorScheme.primary, - ), - ); - }, - child: Container( - width: 10, - color: Colors.transparent, - ), - ), - ); - } - - void _onEnterHoverArea() { - simpleTableContext.hoveringOnResizeHandle.value = widget.node; - } - - void _onExitHoverArea() { - Future.delayed(const Duration(milliseconds: 100), () { - // the onExit event will be triggered before dragging started. - // delay the hiding of the resize handle to avoid flickering. - if (!isStartDragging) { - simpleTableContext.hoveringOnResizeHandle.value = null; - } - }); - } - - void _onHorizontalDragStart(DragStartDetails details) { - // disable the two-finger drag on trackpad - if (details.kind == PointerDeviceKind.trackpad) { - return; - } - - isStartDragging = true; - } - - void _onLongPressStart(LongPressStartDetails details) { - isStartDragging = true; - simpleTableContext.resizingCell.value = widget.node; - - HapticFeedback.lightImpact(); - } - - void _onHorizontalDragUpdate(DragUpdateDetails details) { - if (!isStartDragging) { - return; - } - - // only update the column width in memory, - // the actual update will be applied in _onHorizontalDragEnd - context.read().updateColumnWidthInMemory( - tableCellNode: widget.node, - deltaX: details.delta.dx, - ); - } - - void _onLongPressMoveUpdate(LongPressMoveUpdateDetails details) { - if (!isStartDragging) { - return; - } - - // only update the column width in memory, - // the actual update will be applied in _onHorizontalDragEnd - context.read().updateColumnWidthInMemory( - tableCellNode: widget.node, - deltaX: details.offsetFromOrigin.dx - previousDx, - ); - - previousDx = details.offsetFromOrigin.dx; - } - - void _onHorizontalDragEnd(DragEndDetails details) { - if (!isStartDragging) { - return; - } - - isStartDragging = false; - context.read().hoveringOnResizeHandle.value = null; - - // apply the updated column width - context.read().updateColumnWidth( - tableCellNode: widget.node, - width: widget.node.columnWidth, - ); - } - - void _onLongPressEnd(LongPressEndDetails details) { - if (!isStartDragging) { - return; - } - - isStartDragging = false; - - // apply the updated column width - context.read().updateColumnWidth( - tableCellNode: widget.node, - width: widget.node.columnWidth, - ); - - previousDx = 0; - - simpleTableContext.resizingCell.value = null; - } - - void _onLongPressCancel() { - isStartDragging = false; - simpleTableContext.resizingCell.value = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_divider.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_divider.dart deleted file mode 100644 index 0de08d6e75..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_divider.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:flutter/material.dart'; - -class SimpleTableRowDivider extends StatelessWidget { - const SimpleTableRowDivider({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return VerticalDivider( - color: context.simpleTableBorderColor, - width: 1.0, - ); - } -} - -class SimpleTableColumnDivider extends StatelessWidget { - const SimpleTableColumnDivider({super.key}); - - @override - Widget build(BuildContext context) { - return Divider( - color: context.simpleTableBorderColor, - height: 1.0, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart deleted file mode 100644 index 2467d45395..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class SimpleTableFeedback extends StatefulWidget { - const SimpleTableFeedback({ - super.key, - required this.editorState, - required this.node, - required this.type, - required this.index, - }); - - /// The node of the table. - /// Its type must be one of the following: - /// [SimpleTableBlockKeys.type], [SimpleTableRowBlockKeys.type], [SimpleTableCellBlockKeys.type]. - final Node node; - - /// The type of the more action. - /// - /// If the type is [SimpleTableMoreActionType.column], the feedback will use index as column index. - /// If the type is [SimpleTableMoreActionType.row], the feedback will use index as row index. - final SimpleTableMoreActionType type; - - /// The index of the column or row. - final int index; - - final EditorState editorState; - - @override - State createState() => _SimpleTableFeedbackState(); -} - -class _SimpleTableFeedbackState extends State { - final simpleTableContext = SimpleTableContext(); - late final Node dummyNode; - - @override - void initState() { - super.initState(); - - assert( - [ - SimpleTableBlockKeys.type, - SimpleTableRowBlockKeys.type, - SimpleTableCellBlockKeys.type, - ].contains(widget.node.type), - 'The node type must be one of the following: ' - '[SimpleTableBlockKeys.type], [SimpleTableRowBlockKeys.type], [SimpleTableCellBlockKeys.type].', - ); - - simpleTableContext.isSelectingTable.value = true; - dummyNode = _buildDummyNode(); - } - - @override - void dispose() { - simpleTableContext.dispose(); - dummyNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: Provider.value( - value: widget.editorState, - child: SimpleTableWidget( - node: dummyNode, - simpleTableContext: simpleTableContext, - enableAddColumnButton: false, - enableAddRowButton: false, - enableAddColumnAndRowButton: false, - enableHoverEffect: false, - isFeedback: true, - ), - ), - ); - } - - /// Build the dummy node for the feedback. - /// - /// For example, - /// - /// If the type is [SimpleTableMoreActionType.row], we should build the dummy table node using the data from the first row of the table node. - /// If the type is [SimpleTableMoreActionType.column], we should build the dummy table node using the data from the first column of the table node. - Node _buildDummyNode() { - // deep copy the table node to avoid mutating the original node - final tableNode = widget.node.parentTableNode?.deepCopy(); - if (tableNode == null) { - return simpleTableBlockNode(children: []); - } - - switch (widget.type) { - case SimpleTableMoreActionType.row: - if (widget.index >= tableNode.rowLength || widget.index < 0) { - return simpleTableBlockNode(children: []); - } - - final row = tableNode.children[widget.index]; - return tableNode.copyWith( - children: [row], - attributes: { - ...tableNode.attributes, - if (widget.index != 0) SimpleTableBlockKeys.enableHeaderRow: false, - }, - ); - case SimpleTableMoreActionType.column: - if (widget.index >= tableNode.columnLength || widget.index < 0) { - return simpleTableBlockNode(children: []); - } - - final rows = tableNode.children.map((row) { - final cell = row.children[widget.index]; - return simpleTableRowBlockNode(children: [cell]); - }).toList(); - - final columnWidth = tableNode.columnWidths[widget.index.toString()] ?? - SimpleTableConstants.defaultColumnWidth; - - return tableNode.copyWith( - children: rows, - attributes: { - ...tableNode.attributes, - SimpleTableBlockKeys.columnWidths: { - '0': columnWidth, - }, - if (widget.index != 0) - SimpleTableBlockKeys.enableHeaderColumn: false, - }, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart deleted file mode 100644 index d4df194c05..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_more_action_popup.dart +++ /dev/null @@ -1,596 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class SimpleTableMoreActionPopup extends StatefulWidget { - const SimpleTableMoreActionPopup({ - super.key, - required this.index, - required this.isShowingMenu, - required this.type, - }); - - final int index; - final ValueNotifier isShowingMenu; - final SimpleTableMoreActionType type; - - @override - State createState() => - _SimpleTableMoreActionPopupState(); -} - -class _SimpleTableMoreActionPopupState - extends State { - late final editorState = context.read(); - - SelectionGestureInterceptor? gestureInterceptor; - - RenderBox? get renderBox => context.findRenderObject() as RenderBox?; - - late final simpleTableContext = context.read(); - Node? tableNode; - Node? tableCellNode; - - @override - void initState() { - super.initState(); - - tableCellNode = context.read().hoveringTableCell.value; - tableNode = tableCellNode?.parentTableNode; - gestureInterceptor = SelectionGestureInterceptor( - key: 'simple_table_more_action_popup_interceptor_${tableCellNode?.id}', - canTap: (details) => !_isTapInBounds(details.globalPosition), - ); - editorState.service.selectionService.registerGestureInterceptor( - gestureInterceptor!, - ); - } - - @override - void dispose() { - if (gestureInterceptor != null) { - editorState.service.selectionService.unregisterGestureInterceptor( - gestureInterceptor!.key, - ); - } - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (tableNode == null) { - return const SizedBox.shrink(); - } - - return AppFlowyPopover( - onOpen: () => _onOpen(tableCellNode: tableCellNode), - onClose: () => _onClose(), - canClose: () async { - return true; - }, - direction: widget.type == SimpleTableMoreActionType.row - ? PopoverDirection.bottomWithCenterAligned - : PopoverDirection.bottomWithLeftAligned, - offset: widget.type == SimpleTableMoreActionType.row - ? const Offset(24, 14) - : const Offset(-14, 8), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (_) => _buildPopup(tableCellNode: tableCellNode), - child: SimpleTableDraggableReorderButton( - editorState: editorState, - simpleTableContext: simpleTableContext, - node: tableNode!, - index: widget.index, - isShowingMenu: widget.isShowingMenu, - type: widget.type, - ), - ); - } - - Widget _buildPopup({Node? tableCellNode}) { - if (tableCellNode == null) { - return const SizedBox.shrink(); - } - return MultiProvider( - providers: [ - Provider.value( - value: context.read(), - ), - Provider.value( - value: context.read(), - ), - ], - child: SimpleTableMoreActionList( - type: widget.type, - index: widget.index, - tableCellNode: tableCellNode, - ), - ); - } - - void _onOpen({Node? tableCellNode}) { - widget.isShowingMenu.value = true; - - switch (widget.type) { - case SimpleTableMoreActionType.column: - context.read().selectingColumn.value = - tableCellNode?.columnIndex; - case SimpleTableMoreActionType.row: - context.read().selectingRow.value = - tableCellNode?.rowIndex; - } - - // Workaround to clear the selection after the menu is opened. - Future.delayed(Durations.short3, () { - if (!editorState.isDisposed) { - editorState.selection = null; - } - }); - } - - void _onClose() { - widget.isShowingMenu.value = false; - - // clear the selecting index - context.read().selectingColumn.value = null; - context.read().selectingRow.value = null; - } - - bool _isTapInBounds(Offset offset) { - if (renderBox == null) { - return false; - } - - final localPosition = renderBox!.globalToLocal(offset); - final result = renderBox!.paintBounds.contains(localPosition); - if (result) { - editorState.selection = null; - } - return result; - } -} - -class SimpleTableMoreActionList extends StatefulWidget { - const SimpleTableMoreActionList({ - super.key, - required this.type, - required this.index, - required this.tableCellNode, - this.mutex, - }); - - final SimpleTableMoreActionType type; - final int index; - final Node tableCellNode; - final PopoverMutex? mutex; - - @override - State createState() => - _SimpleTableMoreActionListState(); -} - -class _SimpleTableMoreActionListState extends State { - // ensure the background color menu and align menu exclusive - final mutex = PopoverMutex(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: widget.type - .buildDesktopActions( - index: widget.index, - columnLength: widget.tableCellNode.columnLength, - rowLength: widget.tableCellNode.rowLength, - ) - .map( - (action) => SimpleTableMoreActionItem( - type: widget.type, - action: action, - tableCellNode: widget.tableCellNode, - popoverMutex: mutex, - ), - ) - .toList(), - ); - } -} - -class SimpleTableMoreActionItem extends StatefulWidget { - const SimpleTableMoreActionItem({ - super.key, - required this.type, - required this.action, - required this.tableCellNode, - required this.popoverMutex, - }); - - final SimpleTableMoreActionType type; - final SimpleTableMoreAction action; - final Node tableCellNode; - final PopoverMutex popoverMutex; - - @override - State createState() => - _SimpleTableMoreActionItemState(); -} - -class _SimpleTableMoreActionItemState extends State { - final isEnableHeader = ValueNotifier(false); - - @override - void initState() { - super.initState(); - - _initEnableHeader(); - } - - @override - void dispose() { - isEnableHeader.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (widget.action == SimpleTableMoreAction.divider) { - return _buildDivider(context); - } else if (widget.action == SimpleTableMoreAction.align) { - return _buildAlignMenu(context); - } else if (widget.action == SimpleTableMoreAction.backgroundColor) { - return _buildBackgroundColorMenu(context); - } else if (widget.action == SimpleTableMoreAction.enableHeaderColumn) { - return _buildEnableHeaderButton(context); - } else if (widget.action == SimpleTableMoreAction.enableHeaderRow) { - return _buildEnableHeaderButton(context); - } - - return _buildActionButton(context); - } - - Widget _buildDivider(BuildContext context) { - return const FlowyDivider( - padding: EdgeInsets.symmetric( - vertical: 4.0, - ), - ); - } - - Widget _buildAlignMenu(BuildContext context) { - return SimpleTableAlignMenu( - type: widget.type, - tableCellNode: widget.tableCellNode, - mutex: widget.popoverMutex, - ); - } - - Widget _buildBackgroundColorMenu(BuildContext context) { - return SimpleTableBackgroundColorMenu( - type: widget.type, - tableCellNode: widget.tableCellNode, - mutex: widget.popoverMutex, - ); - } - - Widget _buildEnableHeaderButton(BuildContext context) { - return SimpleTableBasicButton( - text: widget.action.name, - leftIconSvg: widget.action.leftIconSvg, - rightIcon: ValueListenableBuilder( - valueListenable: isEnableHeader, - builder: (context, isEnableHeader, child) { - return Toggle( - value: isEnableHeader, - onChanged: (value) => _toggleEnableHeader(), - padding: EdgeInsets.zero, - ); - }, - ), - onTap: _toggleEnableHeader, - ); - } - - Widget _buildActionButton(BuildContext context) { - return Container( - height: SimpleTableConstants.moreActionHeight, - padding: SimpleTableConstants.moreActionPadding, - child: FlowyIconTextButton( - margin: SimpleTableConstants.moreActionHorizontalMargin, - leftIconBuilder: (onHover) => FlowySvg( - widget.action.leftIconSvg, - color: widget.action == SimpleTableMoreAction.delete && onHover - ? Theme.of(context).colorScheme.error - : null, - ), - iconPadding: 10.0, - textBuilder: (onHover) => FlowyText.regular( - widget.action.name, - fontSize: 14.0, - figmaLineHeight: 18.0, - color: widget.action == SimpleTableMoreAction.delete && onHover - ? Theme.of(context).colorScheme.error - : null, - ), - onTap: _onAction, - ), - ); - } - - void _onAction() { - switch (widget.action) { - case SimpleTableMoreAction.delete: - switch (widget.type) { - case SimpleTableMoreActionType.column: - _deleteColumn(); - break; - case SimpleTableMoreActionType.row: - _deleteRow(); - break; - } - case SimpleTableMoreAction.insertLeft: - _insertColumnLeft(); - case SimpleTableMoreAction.insertRight: - _insertColumnRight(); - case SimpleTableMoreAction.insertAbove: - _insertRowAbove(); - case SimpleTableMoreAction.insertBelow: - _insertRowBelow(); - case SimpleTableMoreAction.clearContents: - _clearContent(); - case SimpleTableMoreAction.duplicate: - switch (widget.type) { - case SimpleTableMoreActionType.column: - _duplicateColumn(); - break; - case SimpleTableMoreActionType.row: - _duplicateRow(); - break; - } - case SimpleTableMoreAction.setToPageWidth: - _setToPageWidth(); - case SimpleTableMoreAction.distributeColumnsEvenly: - _distributeColumnsEvenly(); - default: - break; - } - - PopoverContainer.of(context).close(); - } - - void _setToPageWidth() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, _, _) = value; - final editorState = context.read(); - editorState.setColumnWidthToPageWidth(tableNode: table); - } - - void _distributeColumnsEvenly() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, _, _) = value; - final editorState = context.read(); - editorState.distributeColumnWidthToPageWidth(tableNode: table); - } - - void _duplicateRow() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final editorState = context.read(); - editorState.duplicateRowInTable(table, node.rowIndex); - } - - void _duplicateColumn() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final editorState = context.read(); - editorState.duplicateColumnInTable(table, node.columnIndex); - } - - void _toggleEnableHeader() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - - isEnableHeader.value = !isEnableHeader.value; - - final (table, _, _) = value; - final editorState = context.read(); - switch (widget.type) { - case SimpleTableMoreActionType.column: - editorState.toggleEnableHeaderColumn( - tableNode: table, - enable: isEnableHeader.value, - ); - case SimpleTableMoreActionType.row: - editorState.toggleEnableHeaderRow( - tableNode: table, - enable: isEnableHeader.value, - ); - } - - PopoverContainer.of(context).close(); - } - - void _clearContent() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final editorState = context.read(); - if (widget.type == SimpleTableMoreActionType.column) { - editorState.clearContentAtColumnIndex( - tableNode: table, - columnIndex: node.columnIndex, - ); - } else if (widget.type == SimpleTableMoreActionType.row) { - editorState.clearContentAtRowIndex( - tableNode: table, - rowIndex: node.rowIndex, - ); - } - } - - void _insertColumnLeft() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final columnIndex = node.columnIndex; - final editorState = context.read(); - editorState.insertColumnInTable(table, columnIndex); - - final cell = table.getTableCellNode( - rowIndex: 0, - columnIndex: columnIndex, - ); - if (cell == null) { - return; - } - - // update selection - editorState.selection = Selection.collapsed( - Position( - path: cell.path.child(0), - ), - ); - } - - void _insertColumnRight() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final columnIndex = node.columnIndex; - final editorState = context.read(); - editorState.insertColumnInTable(table, columnIndex + 1); - - final cell = table.getTableCellNode( - rowIndex: 0, - columnIndex: columnIndex + 1, - ); - if (cell == null) { - return; - } - - // update selection - editorState.selection = Selection.collapsed( - Position( - path: cell.path.child(0), - ), - ); - } - - void _insertRowAbove() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final rowIndex = node.rowIndex; - final editorState = context.read(); - editorState.insertRowInTable(table, rowIndex); - - final cell = table.getTableCellNode(rowIndex: rowIndex, columnIndex: 0); - if (cell == null) { - return; - } - - // update selection - editorState.selection = Selection.collapsed( - Position( - path: cell.path.child(0), - ), - ); - } - - void _insertRowBelow() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final rowIndex = node.rowIndex; - final editorState = context.read(); - editorState.insertRowInTable(table, rowIndex + 1); - - final cell = table.getTableCellNode(rowIndex: rowIndex + 1, columnIndex: 0); - if (cell == null) { - return; - } - - // update selection - editorState.selection = Selection.collapsed( - Position( - path: cell.path.child(0), - ), - ); - } - - void _deleteRow() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final rowIndex = node.rowIndex; - final editorState = context.read(); - editorState.deleteRowInTable(table, rowIndex); - } - - void _deleteColumn() { - final value = _getTableAndTableCellAndCellPosition(); - if (value == null) { - return; - } - final (table, node, _) = value; - final columnIndex = node.columnIndex; - final editorState = context.read(); - editorState.deleteColumnInTable(table, columnIndex); - } - - (Node, Node, TableCellPosition)? _getTableAndTableCellAndCellPosition() { - final cell = widget.tableCellNode; - final table = cell.parent?.parent; - if (table == null || table.type != SimpleTableBlockKeys.type) { - return null; - } - return (table, cell, cell.cellPosition); - } - - void _initEnableHeader() { - final value = _getTableAndTableCellAndCellPosition(); - if (value != null) { - final (table, _, _) = value; - if (widget.type == SimpleTableMoreActionType.column) { - isEnableHeader.value = table - .attributes[SimpleTableBlockKeys.enableHeaderColumn] as bool? ?? - false; - } else if (widget.type == SimpleTableMoreActionType.row) { - isEnableHeader.value = - table.attributes[SimpleTableBlockKeys.enableHeaderRow] as bool? ?? - false; - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_reorder_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_reorder_button.dart deleted file mode 100644 index bb87196051..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_reorder_button.dart +++ /dev/null @@ -1,148 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_feedback.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -class SimpleTableDraggableReorderButton extends StatelessWidget { - const SimpleTableDraggableReorderButton({ - super.key, - required this.node, - required this.index, - required this.isShowingMenu, - required this.type, - required this.editorState, - required this.simpleTableContext, - }); - - final Node node; - final int index; - final ValueNotifier isShowingMenu; - final SimpleTableMoreActionType type; - final EditorState editorState; - final SimpleTableContext simpleTableContext; - - @override - Widget build(BuildContext context) { - return Draggable( - data: index, - onDragStarted: () => _startDragging(), - onDragUpdate: (details) => _onDragUpdate(details), - onDragEnd: (_) => _stopDragging(), - feedback: SimpleTableFeedback( - editorState: editorState, - node: node, - type: type, - index: index, - ), - child: SimpleTableReorderButton( - isShowingMenu: isShowingMenu, - type: type, - ), - ); - } - - void _startDragging() { - switch (type) { - case SimpleTableMoreActionType.column: - simpleTableContext.isReorderingColumn.value = (true, index); - break; - case SimpleTableMoreActionType.row: - simpleTableContext.isReorderingRow.value = (true, index); - break; - } - } - - void _onDragUpdate(DragUpdateDetails details) { - simpleTableContext.reorderingOffset.value = details.globalPosition; - } - - void _stopDragging() { - switch (type) { - case SimpleTableMoreActionType.column: - _reorderColumn(); - case SimpleTableMoreActionType.row: - _reorderRow(); - } - - simpleTableContext.reorderingOffset.value = Offset.zero; - - switch (type) { - case SimpleTableMoreActionType.column: - simpleTableContext.isReorderingColumn.value = (false, -1); - break; - case SimpleTableMoreActionType.row: - simpleTableContext.isReorderingRow.value = (false, -1); - break; - } - } - - void _reorderColumn() { - final fromIndex = simpleTableContext.isReorderingColumn.value.$2; - final toIndex = simpleTableContext.hoveringTableCell.value?.columnIndex; - if (toIndex == null) { - return; - } - - editorState.reorderColumn( - node, - fromIndex: fromIndex, - toIndex: toIndex, - ); - } - - void _reorderRow() { - final fromIndex = simpleTableContext.isReorderingRow.value.$2; - final toIndex = simpleTableContext.hoveringTableCell.value?.rowIndex; - if (toIndex == null) { - return; - } - - editorState.reorderRow( - node, - fromIndex: fromIndex, - toIndex: toIndex, - ); - } -} - -class SimpleTableReorderButton extends StatelessWidget { - const SimpleTableReorderButton({ - super.key, - required this.isShowingMenu, - required this.type, - }); - - final ValueNotifier isShowingMenu; - final SimpleTableMoreActionType type; - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: isShowingMenu, - builder: (context, isShowingMenu, child) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - decoration: BoxDecoration( - color: isShowingMenu - ? context.simpleTableMoreActionHoverColor - : Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(8.0), - border: Border.all( - color: context.simpleTableMoreActionBorderColor, - ), - ), - height: 16.0, - width: 16.0, - child: FlowySvg( - type.reorderIconSvg, - color: isShowingMenu ? Colors.white : null, - size: const Size.square(16.0), - ), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart deleted file mode 100644 index 98d1e9f246..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '_desktop_simple_table_widget.dart'; -import '_mobile_simple_table_widget.dart'; - -class SimpleTableWidget extends StatefulWidget { - const SimpleTableWidget({ - super.key, - required this.simpleTableContext, - required this.node, - this.enableAddColumnButton = true, - this.enableAddRowButton = true, - this.enableAddColumnAndRowButton = true, - this.enableHoverEffect = true, - this.isFeedback = false, - this.alwaysDistributeColumnWidths = false, - }); - - /// The node of the table. - /// - /// Its type must be [SimpleTableBlockKeys.type]. - final Node node; - - /// The context of the simple table. - final SimpleTableContext simpleTableContext; - - /// Whether to show the add column button. - /// - /// For the feedback widget builder, it should be false. - final bool enableAddColumnButton; - - /// Whether to show the add row button. - /// - /// For the feedback widget builder, it should be false. - final bool enableAddRowButton; - - /// Whether to show the add column and row button. - /// - /// For the feedback widget builder, it should be false. - final bool enableAddColumnAndRowButton; - - /// Whether to enable the hover effect. - /// - /// For the feedback widget builder, it should be false. - final bool enableHoverEffect; - - /// Whether the widget is a feedback widget. - final bool isFeedback; - - /// Whether the columns should ignore their widths and fill available space - final bool alwaysDistributeColumnWidths; - - @override - State createState() => _SimpleTableWidgetState(); -} - -class _SimpleTableWidgetState extends State { - @override - Widget build(BuildContext context) { - return UniversalPlatform.isDesktop - ? DesktopSimpleTableWidget( - simpleTableContext: widget.simpleTableContext, - node: widget.node, - enableAddColumnButton: widget.enableAddColumnButton, - enableAddRowButton: widget.enableAddRowButton, - enableAddColumnAndRowButton: widget.enableAddColumnAndRowButton, - enableHoverEffect: widget.enableHoverEffect, - isFeedback: widget.isFeedback, - alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths, - ) - : MobileSimpleTableWidget( - simpleTableContext: widget.simpleTableContext, - node: widget.node, - enableAddColumnButton: widget.enableAddColumnButton, - enableAddRowButton: widget.enableAddRowButton, - enableAddColumnAndRowButton: widget.enableAddColumnAndRowButton, - enableHoverEffect: widget.enableHoverEffect, - isFeedback: widget.isFeedback, - alwaysDistributeColumnWidths: widget.alwaysDistributeColumnWidths, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/widgets.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/widgets.dart deleted file mode 100644 index 322e3e6a89..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/widgets.dart +++ /dev/null @@ -1,14 +0,0 @@ -export 'simple_table_action_sheet.dart'; -export 'simple_table_add_column_and_row_button.dart'; -export 'simple_table_add_column_button.dart'; -export 'simple_table_add_row_button.dart'; -export 'simple_table_align_button.dart'; -export 'simple_table_background_menu.dart'; -export 'simple_table_basic_button.dart'; -export 'simple_table_border_builder.dart'; -export 'simple_table_bottom_sheet.dart'; -export 'simple_table_column_resize_handle.dart'; -export 'simple_table_divider.dart'; -export 'simple_table_more_action_popup.dart'; -export 'simple_table_reorder_button.dart'; -export 'simple_table_widget.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_command.dart deleted file mode 100644 index 3d6fe113c1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_command.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu.dart'; -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -typedef SlashMenuItemsBuilder = List Function( - EditorState editorState, - Node node, -); - -/// Show the slash menu -/// -/// - support -/// - desktop -/// -final CharacterShortcutEvent appFlowySlashCommand = CharacterShortcutEvent( - key: 'show the slash menu', - character: '/', - handler: (editorState) async => _showSlashMenu( - editorState, - itemsBuilder: (_, __) => standardSelectionMenuItems, - supportSlashMenuNodeTypes: supportSlashMenuNodeTypes, - ), -); - -CharacterShortcutEvent customAppFlowySlashCommand({ - required SlashMenuItemsBuilder itemsBuilder, - bool shouldInsertSlash = true, - bool deleteKeywordsByDefault = false, - bool singleColumn = true, - SelectionMenuStyle style = SelectionMenuStyle.light, - required Set supportSlashMenuNodeTypes, -}) { - return CharacterShortcutEvent( - key: 'show the slash menu', - character: '/', - handler: (editorState) => _showSlashMenu( - editorState, - shouldInsertSlash: shouldInsertSlash, - deleteKeywordsByDefault: deleteKeywordsByDefault, - singleColumn: singleColumn, - style: style, - supportSlashMenuNodeTypes: supportSlashMenuNodeTypes, - itemsBuilder: itemsBuilder, - ), - ); -} - -SelectionMenuService? _selectionMenuService; - -Future _showSlashMenu( - EditorState editorState, { - required SlashMenuItemsBuilder itemsBuilder, - bool shouldInsertSlash = true, - bool singleColumn = true, - bool deleteKeywordsByDefault = false, - SelectionMenuStyle style = SelectionMenuStyle.light, - required Set supportSlashMenuNodeTypes, -}) async { - final selection = editorState.selection; - if (selection == null) { - return false; - } - - // delete the selection - if (!selection.isCollapsed) { - await editorState.deleteSelection(selection); - } - - final afterSelection = editorState.selection; - if (afterSelection == null || !afterSelection.isCollapsed) { - assert(false, 'the selection should be collapsed'); - return true; - } - - final node = editorState.getNodeAtPath(selection.start.path); - - // only enable in white-list nodes - if (node == null || - !_isSupportSlashMenuNode(node, supportSlashMenuNodeTypes)) { - return false; - } - - final items = itemsBuilder(editorState, node); - - // insert the slash character - if (shouldInsertSlash) { - keepEditorFocusNotifier.increase(); - await editorState.insertTextAtPosition('/', position: selection.start); - } - - // show the slash menu - - final context = editorState.getNodeAtPath(selection.start.path)?.context; - if (context != null && context.mounted) { - final isLight = Theme.of(context).brightness == Brightness.light; - _selectionMenuService?.dismiss(); - _selectionMenuService = UniversalPlatform.isMobile - ? MobileSelectionMenu( - context: context, - editorState: editorState, - selectionMenuItems: items, - deleteSlashByDefault: shouldInsertSlash, - deleteKeywordsByDefault: deleteKeywordsByDefault, - singleColumn: singleColumn, - style: isLight - ? MobileSelectionMenuStyle.light - : MobileSelectionMenuStyle.dark, - startOffset: editorState.selection?.start.offset ?? 0, - ) - : SelectionMenu( - context: context, - editorState: editorState, - selectionMenuItems: items, - deleteSlashByDefault: shouldInsertSlash, - deleteKeywordsByDefault: deleteKeywordsByDefault, - singleColumn: singleColumn, - style: style, - ); - - // disable the keyboard service - editorState.service.keyboardService?.disable(); - - await _selectionMenuService?.show(); - // enable the keyboard service - editorState.service.keyboardService?.enable(); - } - - if (shouldInsertSlash) { - WidgetsBinding.instance.addPostFrameCallback( - (timeStamp) => keepEditorFocusNotifier.decrease(), - ); - } - - return true; -} - -bool _isSupportSlashMenuNode( - Node node, - Set supportSlashMenuNodeWhiteList, -) { - // Check if current node type is supported - if (!supportSlashMenuNodeWhiteList.contains(node.type)) { - return false; - } - - // If node has a parent and level > 1, recursively check parent nodes - if (node.level > 1 && node.parent != null) { - return _isSupportSlashMenuNode( - node.parent!, - supportSlashMenuNodeWhiteList, - ); - } - - return true; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart deleted file mode 100644 index 46d1b4eabb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/ai_writer_item.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'ai', - 'openai', - 'writer', - 'ai writer', - 'autogenerator', -]; - -SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_aiWriter.tr, - keywords: [ - ..._keywords, - LocaleKeys.document_slashMenu_name_aiWriter.tr(), - ], - handler: (editorState, _, __) async => - _insertAiWriter(editorState, AiWriterCommand.userQuestion), - icon: (_, isSelected, style) => SelectableSvgWidget( - data: AiWriterCommand.userQuestion.icon, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, -); - -SelectionMenuItem continueWritingSlashMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_plugins_aiWriter_continueWriting.tr, - keywords: [ - ..._keywords, - LocaleKeys.document_plugins_aiWriter_continueWriting.tr(), - ], - handler: (editorState, _, __) async => - _insertAiWriter(editorState, AiWriterCommand.continueWriting), - icon: (_, isSelected, style) => SelectableSvgWidget( - data: AiWriterCommand.continueWriting.icon, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, -); - -Future _insertAiWriter( - EditorState editorState, - AiWriterCommand action, -) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null || node.delta == null) { - return; - } - final newNode = aiWriterNode( - selection: selection, - command: action, - ); - - // default insert after - final path = node.path.next; - final transaction = editorState.transaction - ..insertNode(path, newNode) - ..afterSelection = null; - - await editorState.apply( - transaction, - options: const ApplyOptions( - recordUndo: false, - inMemoryUpdate: true, - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/bulleted_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/bulleted_list_item.dart deleted file mode 100644 index 3fee0d0adb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/bulleted_list_item.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'bulleted list', - 'list', - 'unordered list', - 'ul', -]; - -/// Bulleted list menu item -final bulletedListSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_bulletedList.tr(), - keywords: _keywords, - handler: (editorState, _, __) { - insertBulletedListAfterSelection(editorState); - }, - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_bulleted_list_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/callout_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/callout_item.dart deleted file mode 100644 index 6f553f5216..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/callout_item.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'callout', -]; - -/// Callout menu item -SelectionMenuItem calloutSlashMenuItem = SelectionMenuItem.node( - getName: LocaleKeys.document_plugins_callout.tr, - keywords: _keywords, - nodeBuilder: (editorState, context) => - calloutNode(defaultColor: Colors.transparent), - replace: (_, node) => node.delta?.isEmpty ?? false, - updateSelection: (_, path, __, ___) { - return Selection.single(path: path, startOffset: 0); - }, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_callout_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/code_block_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/code_block_item.dart deleted file mode 100644 index 44c346a302..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/code_block_item.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; - -final _keywords = [ - 'code', - 'code block', - 'codeblock', -]; - -// code block menu item -SelectionMenuItem codeBlockSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_code.tr(), - keywords: _keywords, - nodeBuilder: (_, __) => codeBlockNode(), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_code_block_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/database_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/database_items.dart deleted file mode 100644 index 176b67586a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/database_items.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/link_to_page_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/workspace/application/view/view_service.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 'slash_menu_item_builder.dart'; - -final _gridKeywords = ['grid', 'database']; -final _kanbanKeywords = ['board', 'kanban', 'database']; -final _calendarKeywords = ['calendar', 'database']; - -final _linkedDocKeywords = [ - 'page', - 'notes', - 'referenced page', - 'referenced document', - 'referenced database', - 'link to database', - 'link to document', - 'link to page', - 'link to grid', - 'link to board', - 'link to calendar', -]; -final _linkedGridKeywords = [ - 'referenced', - 'grid', - 'database', - 'linked', -]; -final _linkedKanbanKeywords = [ - 'referenced', - 'board', - 'kanban', - 'linked', -]; -final _linkedCalendarKeywords = [ - 'referenced', - 'calendar', - 'database', - 'linked', -]; - -/// Grid menu item -SelectionMenuItem gridSlashMenuItem(DocumentBloc documentBloc) { - return SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_grid.tr(), - keywords: _gridKeywords, - handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( - parentViewId: parentViewId, - name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layoutType: ViewLayoutPB.Grid, - ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); - }, - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_grid_s, - isSelected: onSelected, - style: style, - ), - ); -} - -SelectionMenuItem kanbanSlashMenuItem(DocumentBloc documentBloc) { - return SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_kanban.tr(), - keywords: _kanbanKeywords, - handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( - parentViewId: parentViewId, - name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layoutType: ViewLayoutPB.Board, - ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); - }, - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_kanban_s, - isSelected: onSelected, - style: style, - ), - ); -} - -SelectionMenuItem calendarSlashMenuItem(DocumentBloc documentBloc) { - return SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_calendar.tr(), - keywords: _calendarKeywords, - handler: (editorState, menuService, context) async { - // create the view inside current page - final parentViewId = documentBloc.documentId; - final value = await ViewBackendService.createView( - parentViewId: parentViewId, - name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layoutType: ViewLayoutPB.Calendar, - ); - value.map((r) => editorState.insertInlinePage(parentViewId, r)); - }, - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_calendar_s, - isSelected: onSelected, - style: style, - ), - ); -} - -// linked doc menu item -final linkToPageSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_linkedDoc.tr(), - keywords: _linkedDocKeywords, - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - // enable database and document references - insertPage: false, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_doc_s, - isSelected: isSelected, - style: style, - ), -); - -// linked grid & board & calendar menu item -SelectionMenuItem referencedGridSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_linkedGrid.tr(), - keywords: _linkedGridKeywords, - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Grid, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_grid_s, - isSelected: onSelected, - style: style, - ), -); - -SelectionMenuItem referencedKanbanSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_linkedKanban.tr(), - keywords: _linkedKanbanKeywords, - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Board, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_kanban_s, - isSelected: onSelected, - style: style, - ), -); - -SelectionMenuItem referencedCalendarSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_linkedCalendar.tr(), - keywords: _linkedCalendarKeywords, - handler: (editorState, menuService, context) => showLinkToPageMenu( - editorState, - menuService, - pageType: ViewLayoutPB.Calendar, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_calendar_s, - isSelected: onSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart deleted file mode 100644 index 04a8ee1b7e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/date_item.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -final _keywords = [ - 'insert date', - 'date', - 'time', - 'reminder', - 'schedule', -]; - -// date or reminder menu item -SelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_dateOrReminder.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertDateReference(), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_date_or_reminder_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future insertDateReference() async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - final transaction = this.transaction - ..replaceText( - node, - selection.start.offset, - 0, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: DateTime.now().toIso8601String(), - reminderId: null, - reminderOption: null, - includeTime: false, - ), - ); - - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/divider_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/divider_item.dart deleted file mode 100644 index a6c4001a68..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/divider_item.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'divider', - 'separator', - 'line', - 'break', - 'horizontal line', -]; - -/// Divider menu item -final dividerSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_divider.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertDividerBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_divider_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future insertDividerBlock() async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - final path = selection.end.path; - final node = getNodeAtPath(path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - final insertedPath = delta.isEmpty ? path : path.next; - final transaction = this.transaction - ..insertNode(insertedPath, dividerNode()) - ..insertNode(insertedPath, paragraphNode()) - ..afterSelection = Selection.collapsed(Position(path: insertedPath.next)); - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart deleted file mode 100644 index 890ba113cc..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/emoji_item.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; -import 'package:appflowy/plugins/emoji/emoji_menu.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'emoji', - 'reaction', - 'emoticon', -]; - -// emoji menu item -SelectionMenuItem emojiSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_emoji.tr(), - keywords: _keywords, - handler: (editorState, menuService, context) => editorState.showEmojiPicker( - context, - menuService: menuService, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_emoji_picker_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future showEmojiPicker( - BuildContext context, { - required SelectionMenuService menuService, - }) async { - final container = Overlay.of(context); - menuService.dismiss(); - if (UniversalPlatform.isMobile || selection == null) { - return; - } - - final node = getNodeAtPath(selection!.end.path); - final delta = node?.delta; - if (node == null || delta == null || node.type == CodeBlockKeys.type) { - return; - } - emojiMenuService = EmojiMenu(editorState: this, overlay: container); - emojiMenuService?.show(''); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/file_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/file_item.dart deleted file mode 100644 index 64c529bee8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/file_item.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_items.dart'; - -final _keywords = [ - 'file upload', - 'pdf', - 'zip', - 'archive', - 'upload', - 'attachment', -]; - -// file menu item -SelectionMenuItem fileSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_file.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertFileBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_file_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future insertFileBlock() async { - final fileGlobalKey = GlobalKey(); - await insertEmptyFileBlock(fileGlobalKey); - - WidgetsBinding.instance.addPostFrameCallback((_) { - fileGlobalKey.currentState?.controller.show(); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/heading_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/heading_items.dart deleted file mode 100644 index 115ef22abe..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/heading_items.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _h1Keywords = [ - 'heading 1', - 'h1', - 'heading1', -]; -final _h2Keywords = [ - 'heading 2', - 'h2', - 'heading2', -]; -final _h3Keywords = [ - 'heading 3', - 'h3', - 'heading3', -]; - -final _toggleH1Keywords = [ - 'toggle heading 1', - 'toggle h1', - 'toggle heading1', - 'toggleheading1', - 'toggleh1', -]; -final _toggleH2Keywords = [ - 'toggle heading 2', - 'toggle h2', - 'toggle heading2', - 'toggleheading2', - 'toggleh2', -]; -final _toggleH3Keywords = [ - 'toggle heading 3', - 'toggle h3', - 'toggle heading3', - 'toggleheading3', - 'toggleh3', -]; - -// heading 1 - 3 menu items -final heading1SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_heading1.tr(), - keywords: _h1Keywords, - handler: (editorState, _, __) async => insertHeadingAfterSelection( - editorState, - 1, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_h1_s, - isSelected: isSelected, - style: style, - ), -); - -final heading2SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_heading2.tr(), - keywords: _h2Keywords, - handler: (editorState, _, __) async => insertHeadingAfterSelection( - editorState, - 2, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_h2_s, - isSelected: isSelected, - style: style, - ), -); - -final heading3SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_heading3.tr(), - keywords: _h3Keywords, - handler: (editorState, _, __) async => insertHeadingAfterSelection( - editorState, - 3, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_h3_s, - isSelected: isSelected, - style: style, - ), -); - -// toggle heading 1 menu item -// heading 1 - 3 menu items -final toggleHeading1SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_toggleHeading1.tr(), - keywords: _toggleH1Keywords, - handler: (editorState, _, __) async => insertNodeAfterSelection( - editorState, - toggleHeadingNode(), - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.toggle_heading1_s, - isSelected: isSelected, - style: style, - ), -); - -final toggleHeading2SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_toggleHeading2.tr(), - keywords: _toggleH2Keywords, - handler: (editorState, _, __) async => insertNodeAfterSelection( - editorState, - toggleHeadingNode(level: 2), - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.toggle_heading2_s, - isSelected: isSelected, - style: style, - ), -); - -final toggleHeading3SlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_toggleHeading3.tr(), - keywords: _toggleH3Keywords, - handler: (editorState, _, __) async => insertNodeAfterSelection( - editorState, - toggleHeadingNode(level: 3), - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.toggle_heading3_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart deleted file mode 100644 index 844b73c3e1..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/image_item.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'image', - 'photo', - 'picture', - 'img', -]; - -/// Image menu item -final imageSlashMenuItem = buildImageSlashMenuItem(); - -SelectionMenuItem buildImageSlashMenuItem({FlowySvgData? svg}) => - SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_image.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertImageBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: svg ?? FlowySvgs.slash_menu_icon_image_s, - isSelected: isSelected, - style: style, - ), - ); - -extension on EditorState { - Future insertImageBlock() async { - // use the key to retrieve the state of the image block to show the popover automatically - final imagePlaceholderKey = GlobalKey(); - await insertEmptyImageBlock(imagePlaceholderKey); - - WidgetsBinding.instance.addPostFrameCallback((_) { - imagePlaceholderKey.currentState?.controller.show(); - }); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/math_equation_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/math_equation_item.dart deleted file mode 100644 index 3274e4ab95..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/math_equation_item.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'tex', - 'latex', - 'katex', - 'math equation', - 'formula', -]; - -// math equation -SelectionMenuItem mathEquationSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_mathEquation.tr(), - keywords: _keywords, - nodeBuilder: (editorState, _) => mathEquationNode(), - replace: (_, node) => node.delta?.isEmpty ?? false, - updateSelection: (editorState, path, __, ___) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final mathEquationState = - editorState.getNodeAtPath(path)?.key.currentState; - if (mathEquationState != null && - mathEquationState is MathEquationBlockComponentWidgetState) { - mathEquationState.showEditingDialog(); - } - }); - return null; - }, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_math_equation_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart deleted file mode 100644 index b71b54ad40..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/mobile_items.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/selection_menu/mobile_selection_menu_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_items.dart'; - -final List mobileItems = [ - textStyleMobileSlashMenuItem, - listMobileSlashMenuItem, - toggleListMobileSlashMenuItem, - fileAndMediaMobileSlashMenuItem, - mobileTableSlashMenuItem, - visualsMobileSlashMenuItem, - dateOrReminderSlashMenuItem, - buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), - advancedMobileSlashMenuItem, -]; - -final List mobileItemsInTale = [ - textStyleMobileSlashMenuItem, - listMobileSlashMenuItem, - toggleListMobileSlashMenuItem, - fileAndMediaMobileSlashMenuItem, - visualsMobileSlashMenuItem, - dateOrReminderSlashMenuItem, - buildSubpageSlashMenuItem(svg: FlowySvgs.type_page_m), - advancedMobileSlashMenuItem, -]; - -SelectionMenuItemHandler _handler = (_, __, ___) {}; - -MobileSelectionMenuItem textStyleMobileSlashMenuItem = MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_textStyle.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_text_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - paragraphSlashMenuItem, - heading1SlashMenuItem, - heading2SlashMenuItem, - heading3SlashMenuItem, - ], -); - -MobileSelectionMenuItem listMobileSlashMenuItem = MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_list.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_bulleted_list_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - todoListSlashMenuItem, - bulletedListSlashMenuItem, - numberedListSlashMenuItem, - ], -); - -MobileSelectionMenuItem toggleListMobileSlashMenuItem = MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_toggle.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_toggle_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - toggleListSlashMenuItem, - toggleHeading1SlashMenuItem, - toggleHeading2SlashMenuItem, - toggleHeading3SlashMenuItem, - ], -); - -MobileSelectionMenuItem fileAndMediaMobileSlashMenuItem = - MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_fileAndMedia.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_file_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - buildImageSlashMenuItem(svg: FlowySvgs.slash_menu_image_m), - photoGallerySlashMenuItem, - fileSlashMenuItem, - ], -); - -MobileSelectionMenuItem visualsMobileSlashMenuItem = MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_visuals.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_visuals_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - calloutSlashMenuItem, - dividerSlashMenuItem, - quoteSlashMenuItem, - ], -); - -MobileSelectionMenuItem advancedMobileSlashMenuItem = MobileSelectionMenuItem( - getName: LocaleKeys.document_slashMenu_name_advanced.tr, - handler: _handler, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.drag_element_s, - isSelected: isSelected, - style: style, - ), - nameBuilder: slashMenuItemNameBuilder, - children: [ - codeBlockSlashMenuItem, - mathEquationSlashMenuItem, - ], -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/numbered_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/numbered_list_item.dart deleted file mode 100644 index 2bb04156d6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/numbered_list_item.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'numbered list', - 'list', - 'ordered list', - 'ol', -]; - -/// Numbered list menu item -final numberedListSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_numberedList.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => insertNumberedListAfterSelection( - editorState, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_numbered_list_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/outline_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/outline_item.dart deleted file mode 100644 index 795f27bc0f..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/outline_item.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'outline', - 'table of contents', - 'toc', - 'tableofcontents', -]; - -/// Outline menu item -SelectionMenuItem outlineSlashMenuItem = SelectionMenuItem( - getName: LocaleKeys.document_selectionMenu_outline.tr, - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertOutline(), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, onSelected, style) { - return Icon( - Icons.list_alt, - color: onSelected - ? style.selectionMenuItemSelectedIconColor - : style.selectionMenuItemIconColor, - size: 16.0, - ); - }, -); - -extension on EditorState { - Future insertOutline() async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - final transaction = this.transaction; - final bReplace = node.delta?.isEmpty ?? false; - - //default insert after - var path = node.path.next; - if (bReplace) { - path = node.path; - } - - final nextNode = getNodeAtPath(path.next); - - transaction - ..insertNodes( - path, - [ - outlineBlockNode(), - if (nextNode == null || nextNode.delta == null) paragraphNode(), - ], - ) - ..afterSelection = Selection.collapsed( - Position(path: path.next), - ); - - if (bReplace) { - transaction.deleteNode(node); - } - - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/paragraph_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/paragraph_item.dart deleted file mode 100644 index 5ad8df64b0..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/paragraph_item.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'text', - 'paragraph', -]; - -// paragraph menu item -final paragraphSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_text.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => insertNodeAfterSelection( - editorState, - paragraphNode(), - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_text_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/photo_gallery_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/photo_gallery_item.dart deleted file mode 100644 index 95ef1a123b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/photo_gallery_item.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -import 'slash_menu_items.dart'; - -final _keywords = [ - LocaleKeys.document_plugins_photoGallery_imageKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_imageGalleryKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_photoKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_photoBrowserKeyword.tr(), - LocaleKeys.document_plugins_photoGallery_galleryKeyword.tr(), -]; - -// photo gallery menu item -SelectionMenuItem photoGallerySlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_photoGallery.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertPhotoGalleryBlock(), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_photo_gallery_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future insertPhotoGalleryBlock() async { - final imagePlaceholderKey = GlobalKey(); - await insertEmptyMultiImageBlock(imagePlaceholderKey); - - WidgetsBinding.instance.addPostFrameCallback( - (_) => imagePlaceholderKey.currentState?.controller.show(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/quote_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/quote_item.dart deleted file mode 100644 index 7c7f887a67..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/quote_item.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'quote', - 'refer', - 'blockquote', - 'citation', -]; - -/// Quote menu item -final quoteSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_quote.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => insertQuoteAfterSelection(editorState), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_quote_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart deleted file mode 100644 index 8609b76e70..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_columns_item.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -final _baseKeywords = [ - 'columns', - 'column block', -]; - -final _twoColumnsKeywords = [ - ..._baseKeywords, - 'two columns', - '2 columns', -]; - -final _threeColumnsKeywords = [ - ..._baseKeywords, - 'three columns', - '3 columns', -]; - -final _fourColumnsKeywords = [ - ..._baseKeywords, - 'four columns', - '4 columns', -]; - -// 2 columns menu item -SelectionMenuItem twoColumnsSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_twoColumns.tr(), - keywords: _twoColumnsKeywords, - nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 2), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_two_columns_s, - isSelected: isSelected, - style: style, - ), - updateSelection: (_, path, __, ___) { - return Selection.single( - path: path.child(0).child(0), - startOffset: 0, - ); - }, -); - -// 3 columns menu item -SelectionMenuItem threeColumnsSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_threeColumns.tr(), - keywords: _threeColumnsKeywords, - nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 3), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_three_columns_s, - isSelected: isSelected, - style: style, - ), - updateSelection: (_, path, __, ___) { - return Selection.single( - path: path.child(0).child(0), - startOffset: 0, - ); - }, -); - -// 4 columns menu item -SelectionMenuItem fourColumnsSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_fourColumns.tr(), - keywords: _fourColumnsKeywords, - nodeBuilder: (editorState, __) => _buildColumnsNode(editorState, 4), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_four_columns_s, - isSelected: isSelected, - style: style, - ), - updateSelection: (_, path, __, ___) { - return Selection.single( - path: path.child(0).child(0), - startOffset: 0, - ); - }, -); - -Node _buildColumnsNode(EditorState editorState, int columnCount) { - return simpleColumnsNode( - columnCount: columnCount, - ratio: 1.0 / columnCount, - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_table_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_table_item.dart deleted file mode 100644 index 9e16571d39..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/simple_table_item.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'table', - 'rows', - 'columns', - 'data', -]; - -// table menu item -SelectionMenuItem tableSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_table.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertSimpleTable(), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_simple_table_s, - isSelected: isSelected, - style: style, - ), -); - -SelectionMenuItem mobileTableSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.document_slashMenu_name_simpleTable.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => editorState.insertSimpleTable(), - nameBuilder: slashMenuItemNameBuilder, - icon: (_, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_simple_table_s, - isSelected: isSelected, - style: style, - ), -); - -extension on EditorState { - Future insertSimpleTable() async { - final selection = this.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final currentNode = getNodeAtPath(selection.end.path); - if (currentNode == null) { - return; - } - - // create a simple table with 2 columns and 2 rows - final tableNode = createSimpleTableBlockNode( - columnCount: 2, - rowCount: 2, - ); - - final transaction = this.transaction; - final delta = currentNode.delta; - if (delta != null && delta.isEmpty) { - final path = selection.end.path; - transaction - ..insertNode(path, tableNode) - ..deleteNode(currentNode); - transaction.afterSelection = Selection.collapsed( - Position( - // table -> row -> cell -> paragraph - path: path + [0, 0, 0], - ), - ); - } else { - final path = selection.end.path.next; - transaction.insertNode(path, tableNode); - transaction.afterSelection = Selection.collapsed( - Position( - // table -> row -> cell -> paragraph - path: path + [0, 0, 0], - ), - ); - } - - await apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart deleted file mode 100644 index fada0addd9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_item_builder.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// Builder function for the slash menu item. -Widget slashMenuItemNameBuilder( - String name, - SelectionMenuStyle style, - bool isSelected, -) { - return SlashMenuItemNameBuilder( - name: name, - style: style, - isSelected: isSelected, - ); -} - -Widget slashMenuItemIconBuilder( - FlowySvgData data, - bool isSelected, - SelectionMenuStyle style, -) { - return SelectableSvgWidget( - data: data, - isSelected: isSelected, - style: style, - ); -} - -/// Build the name of the slash menu item. -class SlashMenuItemNameBuilder extends StatelessWidget { - const SlashMenuItemNameBuilder({ - super.key, - required this.name, - required this.style, - required this.isSelected, - }); - - /// The name of the slash menu item. - final String name; - - /// The style of the slash menu item. - final SelectionMenuStyle style; - - /// Whether the slash menu item is selected. - final bool isSelected; - - @override - Widget build(BuildContext context) { - final isMobile = UniversalPlatform.isMobile; - return FlowyText.regular( - name, - fontSize: isMobile ? 16.0 : 12.0, - figmaLineHeight: 15.0, - color: isSelected - ? style.selectionMenuItemSelectedTextColor - : style.selectionMenuItemTextColor, - ); - } -} - -/// Build the icon of the slash menu item. -class SlashMenuIconBuilder extends StatelessWidget { - const SlashMenuIconBuilder({ - super.key, - required this.data, - required this.isSelected, - required this.style, - }); - - /// The data of the icon. - final FlowySvgData data; - - /// Whether the slash menu item is selected. - final bool isSelected; - - /// The style of the slash menu item. - final SelectionMenuStyle style; - - @override - Widget build(BuildContext context) { - final isMobile = UniversalPlatform.isMobile; - return SelectableSvgWidget( - data: data, - isSelected: isSelected, - size: isMobile ? Size.square(20) : null, - style: style, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart deleted file mode 100644 index 27be8e4f03..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/slash_menu_items.dart +++ /dev/null @@ -1,23 +0,0 @@ -export 'ai_writer_item.dart'; -export 'bulleted_list_item.dart'; -export 'callout_item.dart'; -export 'code_block_item.dart'; -export 'database_items.dart'; -export 'date_item.dart'; -export 'divider_item.dart'; -export 'emoji_item.dart'; -export 'file_item.dart'; -export 'heading_items.dart'; -export 'image_item.dart'; -export 'math_equation_item.dart'; -export 'numbered_list_item.dart'; -export 'outline_item.dart'; -export 'paragraph_item.dart'; -export 'photo_gallery_item.dart'; -export 'quote_item.dart'; -export 'simple_columns_item.dart'; -export 'simple_table_item.dart'; -export 'slash_menu_item_builder.dart'; -export 'sub_page_item.dart'; -export 'todo_list_item.dart'; -export 'toggle_list_item.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart deleted file mode 100644 index 1052dbbe3e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/sub_page_item.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'slash_menu_items.dart'; - -final _keywords = [ - LocaleKeys.document_slashMenu_subPage_keyword1.tr(), - LocaleKeys.document_slashMenu_subPage_keyword2.tr(), - LocaleKeys.document_slashMenu_subPage_keyword3.tr(), - LocaleKeys.document_slashMenu_subPage_keyword4.tr(), - LocaleKeys.document_slashMenu_subPage_keyword5.tr(), - LocaleKeys.document_slashMenu_subPage_keyword6.tr(), - LocaleKeys.document_slashMenu_subPage_keyword7.tr(), - LocaleKeys.document_slashMenu_subPage_keyword8.tr(), -]; - -// Sub-page menu item -SelectionMenuItem subPageSlashMenuItem = buildSubpageSlashMenuItem(); - -SelectionMenuItem buildSubpageSlashMenuItem({FlowySvgData? svg}) => - SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_subPage_name.tr(), - keywords: _keywords, - updateSelection: (editorState, path, __, ___) { - final context = editorState.document.root.context; - if (context != null) { - final isInDatabase = - context.read().isInDatabaseRowPage; - if (isInDatabase) { - Navigator.of(context).pop(); - } - } - return Selection.collapsed(Position(path: path)); - }, - replace: (_, node) => node.delta?.isEmpty ?? false, - nodeBuilder: (_, __) => subPageNode(), - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (_, isSelected, style) => SelectableSvgWidget( - data: svg ?? FlowySvgs.insert_document_s, - isSelected: isSelected, - style: style, - ), - ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart deleted file mode 100644 index 518dccb35e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/todo_list_item.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'checkbox', - 'todo', - 'list', - 'to-do', - 'task', -]; - -final todoListSlashMenuItem = SelectionMenuItem( - getName: () => LocaleKeys.editor_checkbox.tr(), - keywords: _keywords, - handler: (editorState, _, __) async => insertCheckboxAfterSelection( - editorState, - ), - nameBuilder: slashMenuItemNameBuilder, - icon: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_checkbox_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/toggle_list_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/toggle_list_item.dart deleted file mode 100644 index d93dd3c738..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items/toggle_list_item.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; - -import 'slash_menu_item_builder.dart'; - -final _keywords = [ - 'collapsed list', - 'toggle list', - 'list', - 'dropdown', -]; - -// toggle menu item -SelectionMenuItem toggleListSlashMenuItem = SelectionMenuItem.node( - getName: () => LocaleKeys.document_slashMenu_name_toggleList.tr(), - keywords: _keywords, - nodeBuilder: (editorState, _) => toggleListBlockNode(), - replace: (_, node) => node.delta?.isEmpty ?? false, - nameBuilder: slashMenuItemNameBuilder, - iconBuilder: (editorState, isSelected, style) => SelectableSvgWidget( - data: FlowySvgs.slash_menu_icon_toggle_s, - isSelected: isSelected, - style: style, - ), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart deleted file mode 100644 index 137f592902..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items_builder.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_node_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'slash_menu_items/mobile_items.dart'; -import 'slash_menu_items/slash_menu_items.dart'; - -/// Build slash menu items -/// -List slashMenuItemsBuilder({ - bool isLocalMode = false, - DocumentBloc? documentBloc, - EditorState? editorState, - Node? node, - ViewPB? view, -}) { - final isInTable = node != null && node.parentTableCellNode != null; - final isMobile = UniversalPlatform.isMobile; - bool isEmpty = false; - if (editorState == null || editorState.isEmptyForContinueWriting()) { - if (view == null || view.name.isEmpty) { - isEmpty = true; - } - } - if (isMobile) { - if (isInTable) { - return mobileItemsInTale; - } else { - return mobileItems; - } - } else { - if (isInTable) { - return _simpleTableSlashMenuItems(); - } else { - return _defaultSlashMenuItems( - isLocalMode: isLocalMode, - documentBloc: documentBloc, - isEmpty: isEmpty, - ); - } - } -} - -/// The default slash menu items are used in the text-based block. -/// -/// Except for the simple table block, the slash menu items in the table block are -/// built by the `tableSlashMenuItem` function. -/// If in local mode, disable the ai writer feature -/// -/// The linked database relies on the documentBloc, so it's required to pass in -/// the documentBloc when building the slash menu items. If the documentBloc is -/// not provided, the linked database items will be disabled. -/// -/// -List _defaultSlashMenuItems({ - bool isLocalMode = false, - DocumentBloc? documentBloc, - bool isEmpty = false, -}) { - return [ - // ai - if (!isLocalMode) ...[ - if (!isEmpty) continueWritingSlashMenuItem, - aiWriterSlashMenuItem, - ], - - paragraphSlashMenuItem, - - // heading 1-3 - heading1SlashMenuItem, - heading2SlashMenuItem, - heading3SlashMenuItem, - - // image - imageSlashMenuItem, - - // list - bulletedListSlashMenuItem, - numberedListSlashMenuItem, - todoListSlashMenuItem, - - // divider - dividerSlashMenuItem, - - // quote - quoteSlashMenuItem, - - // simple table - tableSlashMenuItem, - - // link to page - linkToPageSlashMenuItem, - - // columns - // 2-4 columns - twoColumnsSlashMenuItem, - threeColumnsSlashMenuItem, - fourColumnsSlashMenuItem, - - // grid - if (documentBloc != null) gridSlashMenuItem(documentBloc), - referencedGridSlashMenuItem, - - // kanban - if (documentBloc != null) kanbanSlashMenuItem(documentBloc), - referencedKanbanSlashMenuItem, - - // calendar - if (documentBloc != null) calendarSlashMenuItem(documentBloc), - referencedCalendarSlashMenuItem, - - // callout - calloutSlashMenuItem, - - // outline - outlineSlashMenuItem, - - // math equation - mathEquationSlashMenuItem, - - // code block - codeBlockSlashMenuItem, - - // toggle list - toggle headings - toggleListSlashMenuItem, - toggleHeading1SlashMenuItem, - toggleHeading2SlashMenuItem, - toggleHeading3SlashMenuItem, - - // emoji - emojiSlashMenuItem, - - // date or reminder - dateOrReminderSlashMenuItem, - - // photo gallery - photoGallerySlashMenuItem, - - // file - fileSlashMenuItem, - - // sub page - subPageSlashMenuItem, - ]; -} - -/// The slash menu items in the simple table block. -/// -/// There're some blocks should be excluded in the slash menu items. -/// -/// - Database Items -/// - Image Gallery -List _simpleTableSlashMenuItems() { - return [ - paragraphSlashMenuItem, - - // heading 1-3 - heading1SlashMenuItem, - heading2SlashMenuItem, - heading3SlashMenuItem, - - // image - imageSlashMenuItem, - - // list - bulletedListSlashMenuItem, - numberedListSlashMenuItem, - todoListSlashMenuItem, - - // divider - dividerSlashMenuItem, - - // quote - quoteSlashMenuItem, - - // link to page - linkToPageSlashMenuItem, - - // callout - calloutSlashMenuItem, - - // math equation - mathEquationSlashMenuItem, - - // code block - codeBlockSlashMenuItem, - - // toggle list - toggle headings - toggleListSlashMenuItem, - toggleHeading1SlashMenuItem, - toggleHeading2SlashMenuItem, - toggleHeading3SlashMenuItem, - - // emoji - emojiSlashMenuItem, - - // date or reminder - dateOrReminderSlashMenuItem, - - // file - fileSlashMenuItem, - - // sub page - subPageSlashMenuItem, - ]; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart new file mode 100644 index 0000000000..c3cbd11c97 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:http/http.dart' as http; + +enum StabilityAIRequestType { + imageGenerations; + + Uri get uri { + switch (this) { + case StabilityAIRequestType.imageGenerations: + return Uri.parse( + 'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image', + ); + } + } +} + +abstract class StabilityAIRepository { + /// Generate image from Stability AI + /// + /// [prompt] is the prompt text + /// [n] is the number of images to generate + /// + /// the return value is a list of base64 encoded images + Future, StabilityAIRequestError>> generateImage({ + required String prompt, + int n = 1, + }); +} + +class HttpStabilityAIRepository implements StabilityAIRepository { + const HttpStabilityAIRepository({ + required this.client, + required this.apiKey, + }); + + final http.Client client; + final String apiKey; + + Map get headers => { + 'Authorization': 'Bearer $apiKey', + 'Content-Type': 'application/json', + }; + + @override + Future, StabilityAIRequestError>> generateImage({ + required String prompt, + int n = 1, + }) async { + final parameters = { + 'text_prompts': [ + { + 'text': prompt, + } + ], + 'samples': n, + }; + + try { + final response = await client.post( + StabilityAIRequestType.imageGenerations.uri, + headers: headers, + body: json.encode(parameters), + ); + + final data = json.decode( + utf8.decode(response.bodyBytes), + ); + if (response.statusCode == 200) { + final artifacts = data['artifacts'] as List; + final base64Images = artifacts + .map( + (e) => e['base64'].toString(), + ) + .toList(); + return FlowyResult.success(base64Images); + } else { + return FlowyResult.failure( + StabilityAIRequestError( + data['message'].toString(), + ), + ); + } + } catch (error) { + return FlowyResult.failure( + StabilityAIRequestError( + error.toString(), + ), + ); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart new file mode 100644 index 0000000000..c699237762 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart @@ -0,0 +1,10 @@ +class StabilityAIRequestError { + StabilityAIRequestError(this.message); + + final String message; + + @override + String toString() { + return 'StabilityAIRequestError{message: $message}'; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart deleted file mode 100644 index a549e87f83..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/block_transaction_handler.dart +++ /dev/null @@ -1,300 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/block_transaction_handler/block_transaction_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart'; -import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class SubPageBlockTransactionHandler extends BlockTransactionHandler { - SubPageBlockTransactionHandler() : super(blockType: SubPageBlockKeys.type); - - final List _beingCreated = []; - - @override - void onRedo( - BuildContext context, - EditorState editorState, - List before, - List after, - ) { - _handleUndoRedo(context, editorState, before, after); - } - - @override - void onUndo( - BuildContext context, - EditorState editorState, - List before, - List after, - ) { - _handleUndoRedo(context, editorState, before, after); - } - - void _handleUndoRedo( - BuildContext context, - EditorState editorState, - List before, - List after, - ) { - final additions = after.where((e) => !before.contains(e)).toList(); - final removals = before.where((e) => !after.contains(e)).toList(); - - // Removals goes to trash - for (final node in removals) { - _subPageDeleted(context, editorState, node); - } - - // Additions are moved to this view - for (final node in additions) { - _subPageAdded(context, editorState, node); - } - } - - @override - Future onTransaction( - BuildContext context, - EditorState editorState, - List added, - List removed, { - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - String? parentViewId, - }) async { - if (isDraggingNode) { - return; - } - - for (final node in removed) { - if (!context.mounted) return; - await _subPageDeleted(context, editorState, node); - } - - for (final node in added) { - if (!context.mounted) return; - await _subPageAdded( - context, - editorState, - node, - isPaste: isPaste, - parentViewId: parentViewId, - ); - } - } - - Future _subPageDeleted( - BuildContext context, - EditorState editorState, - Node node, - ) async { - if (node.type != blockType) { - return; - } - - final view = node.attributes[SubPageBlockKeys.viewId]; - if (view == null) { - return; - } - - // We move the view to Trash - final result = await ViewBackendService.deleteView(viewId: view); - result.fold( - (_) {}, - (error) { - Log.error(error); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedDeletePage.tr(), - ); - } - }, - ); - } - - Future _subPageAdded( - BuildContext context, - EditorState editorState, - Node node, { - bool isPaste = false, - String? parentViewId, - }) async { - if (node.type != blockType || _beingCreated.contains(node.id)) { - return; - } - - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (viewId == null && parentViewId != null) { - _beingCreated.add(node.id); - - // This is a new Node, we need to create the view - final viewOrResult = await ViewBackendService.createView( - layoutType: ViewLayoutPB.Document, - parentViewId: parentViewId, - name: '', - ); - - await viewOrResult.fold( - (view) async { - final transaction = editorState.transaction - ..updateNode(node, {SubPageBlockKeys.viewId: view.id}); - await editorState - .apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ) - .then((_) async { - editorState.reload(); - - // Open the new page - if (UniversalPlatform.isDesktop) { - getIt().openPlugin(view); - } else { - if (context.mounted) { - await context.pushView(view); - } - } - }); - }, - (error) async { - Log.error(error); - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedCreatePage.tr(), - ); - - // Remove the node because it failed - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - }, - ); - - _beingCreated.remove(node.id); - } else if (isPaste) { - final wasCut = node.attributes[SubPageBlockKeys.wasCut]; - if (wasCut == true && parentViewId != null) { - // Just in case, we try to put back from trash before moving - await TrashService.putback(viewId); - - final viewOrResult = await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: parentViewId, - prevViewId: null, - ); - - viewOrResult.fold( - (_) {}, - (error) { - Log.error(error); - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedMovePage.tr(), - ); - }, - ); - } else { - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (viewId == null) { - return; - } - - final viewOrResult = await ViewBackendService.getView(viewId); - return viewOrResult.fold( - (view) async { - final duplicatedViewOrResult = await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: false, - includeChildren: true, - syncAfterDuplicate: true, - parentViewId: parentViewId, - ); - - return duplicatedViewOrResult.fold( - (view) async { - final transaction = editorState.transaction - ..updateNode(node, { - SubPageBlockKeys.viewId: view.id, - SubPageBlockKeys.wasCut: false, - SubPageBlockKeys.wasCopied: false, - }); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - editorState.reload(); - }, - (error) { - Log.error(error); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys - .document_plugins_subPage_errors_failedDuplicatePage - .tr(), - ); - } - }, - ); - }, - (error) async { - Log.error(error); - - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - editorState.reload(); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys - .document_plugins_subPage_errors_failedDuplicateFindView - .tr(), - ); - } - }, - ); - } - } else { - // Try to restore from trash, and move to parent view - await TrashService.putback(viewId); - - // Check if View needs to be moved - if (parentViewId != null) { - final view = pageMemorizer[viewId] ?? - (await ViewBackendService.getView(viewId)).toNullable(); - if (view == null) { - return Log.error('View not found: $viewId'); - } - - if (view.parentViewId == parentViewId) { - return; - } - - await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: parentViewId, - prevViewId: null, - ); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart deleted file mode 100644 index 0ce2b74a74..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart +++ /dev/null @@ -1,419 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy/plugins/trash/application/trash_listener.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; - -Node subPageNode({String? viewId}) { - return Node( - type: SubPageBlockKeys.type, - attributes: {SubPageBlockKeys.viewId: viewId}, - ); -} - -class SubPageBlockKeys { - const SubPageBlockKeys._(); - - static const String type = 'sub_page'; - - /// The ID of the View which is being linked to. - /// - static const String viewId = "view_id"; - - /// Signifies whether the block was inserted after a Copy operation. - /// - static const String wasCopied = "was_copied"; - - /// Signifies whether the block was inserted after a Cut operation. - /// - static const String wasCut = "was_cut"; -} - -class SubPageBlockComponentBuilder extends BlockComponentBuilder { - SubPageBlockComponentBuilder({super.configuration}); - - @override - BlockComponentWidget build(BlockComponentContext blockComponentContext) { - final node = blockComponentContext.node; - return SubPageBlockComponent( - key: node.key, - node: node, - showActions: showActions(node), - configuration: configuration, - actionBuilder: (_, state) => actionBuilder(blockComponentContext, state), - ); - } - - @override - BlockComponentValidate get validate => (node) => node.children.isEmpty; -} - -class SubPageBlockComponent extends BlockComponentStatefulWidget { - const SubPageBlockComponent({ - super.key, - required super.node, - super.showActions, - super.actionBuilder, - super.actionTrailingBuilder, - super.configuration = const BlockComponentConfiguration(), - }); - - @override - State createState() => SubPageBlockComponentState(); -} - -class SubPageBlockComponentState extends State - with SelectableMixin, BlockComponentConfigurable { - @override - BlockComponentConfiguration get configuration => widget.configuration; - - @override - Node get node => widget.node; - - RenderBox? get _renderBox => context.findRenderObject() as RenderBox?; - - final subPageKey = GlobalKey(); - - ViewListener? viewListener; - TrashListener? trashListener; - Future? viewFuture; - - bool isHovering = false; - bool isHandlingPaste = false; - - EditorState get editorState => context.read(); - - String? parentId; - - @override - void initState() { - super.initState(); - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (viewId != null) { - viewFuture = fetchView(viewId); - viewListener = ViewListener(viewId: viewId) - ..start(onViewUpdated: onViewUpdated); - trashListener = TrashListener()..start(trashUpdated: didUpdateTrash); - } - } - - @override - void didUpdateWidget(SubPageBlockComponent oldWidget) { - final viewId = node.attributes[SubPageBlockKeys.viewId]; - final oldViewId = viewListener?.viewId ?? - oldWidget.node.attributes[SubPageBlockKeys.viewId]; - if (viewId != null && (viewId != oldViewId || viewListener == null)) { - viewFuture = fetchView(viewId); - viewListener?.stop(); - viewListener = ViewListener(viewId: viewId) - ..start(onViewUpdated: onViewUpdated); - } - super.didUpdateWidget(oldWidget); - } - - void didUpdateTrash(FlowyResult, FlowyError> trashOrFailed) { - final trashList = trashOrFailed.toNullable(); - if (trashList == null) { - return; - } - - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (trashList.any((t) => t.id == viewId)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - final transaction = editorState.transaction..deleteNode(node); - editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - } - }); - } - } - - void onViewUpdated(ViewPB view) { - pageMemorizer[view.id] = view; - viewFuture = Future.value(view); - editorState.reload(); - - if (parentId != view.parentViewId && parentId != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - final transaction = editorState.transaction..deleteNode(node); - editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - } - }); - } - } - - @override - void dispose() { - viewListener?.stop(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - initialData: pageMemorizer[node.attributes[SubPageBlockKeys.viewId]], - future: viewFuture, - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const SizedBox.shrink(); - } - - final view = snapshot.data; - if (view == null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - final transaction = editorState.transaction..deleteNode(node); - editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - } - }); - - return const SizedBox.shrink(); - } - - final textStyle = textStyleWithTextSpan(); - - Widget child = Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - opaque: false, - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - ), - child: BlockSelectionContainer( - node: node, - delegate: this, - listenable: editorState.selectionNotifier, - remoteSelection: editorState.remoteSelections, - blockColor: editorState.editorStyle.selectionColor, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - supportTypes: const [ - BlockSelectionType.block, - BlockSelectionType.cursor, - BlockSelectionType.selection, - ], - child: GestureDetector( - // TODO(Mathias): Handle mobile tap - onTap: - isHandlingPaste ? null : () => _openSubPage(view: view), - child: DecoratedBox( - decoration: BoxDecoration( - color: isHovering - ? Theme.of(context).colorScheme.secondary - : null, - borderRadius: BorderRadius.circular(4), - ), - child: SizedBox( - height: 32, - child: Row( - children: [ - const HSpace(10), - view.icon.value.isNotEmpty - ? RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: textStyle.fontSize ?? 16.0, - ) - : view.defaultIcon(), - const HSpace(6), - Flexible( - child: FlowyText( - view.nameOrDefault, - fontSize: textStyle.fontSize, - fontWeight: textStyle.fontWeight, - decoration: TextDecoration.underline, - lineHeight: textStyle.height, - overflow: TextOverflow.ellipsis, - ), - ), - if (isHandlingPaste) ...[ - FlowyText( - LocaleKeys - .document_plugins_subPage_handlingPasteHint - .tr(), - fontSize: textStyle.fontSize, - fontWeight: textStyle.fontWeight, - lineHeight: textStyle.height, - color: Theme.of(context).hintColor, - ), - const HSpace(10), - const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - strokeWidth: 1.5, - ), - ), - ], - const HSpace(10), - ], - ), - ), - ), - ), - ), - ), - ), - ); - - if (widget.showActions && widget.actionBuilder != null) { - child = BlockComponentActionWrapper( - node: node, - actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, - child: child, - ); - } - - if (UniversalPlatform.isMobile) { - child = Padding( - padding: padding, - child: child, - ); - } - - return child; - }, - ); - } - - Future fetchView(String pageId) async { - final view = await ViewBackendService.getView(pageId).then( - (res) => res.toNullable(), - ); - - if (view == null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - final transaction = editorState.transaction..deleteNode(node); - editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - } - }); - } - - parentId = view?.parentViewId; - return view; - } - - @override - Position start() => Position(path: widget.node.path); - - @override - Position end() => Position(path: widget.node.path, offset: 1); - - @override - Position getPositionInOffset(Offset start) => end(); - - @override - bool get shouldCursorBlink => false; - - @override - CursorStyle get cursorStyle => CursorStyle.cover; - - @override - Rect getBlockRect({ - bool shiftWithBaseOffset = false, - }) { - return getRectsInSelection(Selection.invalid()).first; - } - - @override - Rect? getCursorRectInPosition( - Position position, { - bool shiftWithBaseOffset = false, - }) { - final rects = getRectsInSelection( - Selection.collapsed(position), - shiftWithBaseOffset: shiftWithBaseOffset, - ); - return rects.firstOrNull; - } - - @override - List getRectsInSelection( - Selection selection, { - bool shiftWithBaseOffset = false, - }) { - if (_renderBox == null) { - return []; - } - final parentBox = context.findRenderObject(); - final renderBox = subPageKey.currentContext?.findRenderObject(); - if (parentBox is RenderBox && renderBox is RenderBox) { - return [ - renderBox.localToGlobal(Offset.zero, ancestor: parentBox) & - renderBox.size, - ]; - } - return [Offset.zero & _renderBox!.size]; - } - - @override - Selection getSelectionInRange(Offset start, Offset end) => - Selection.single(path: widget.node.path, startOffset: 0, endOffset: 1); - - @override - Offset localToGlobal(Offset offset, {bool shiftWithBaseOffset = false}) => - _renderBox!.localToGlobal(offset); - - void _openSubPage({ - required ViewPB view, - }) { - if (UniversalPlatform.isDesktop) { - final isInDatabase = - context.read().isInDatabaseRowPage; - if (isInDatabase) { - Navigator.of(context).pop(); - } - - getIt().add( - TabsEvent.openPlugin( - plugin: view.plugin(), - view: view, - ), - ); - } else if (UniversalPlatform.isMobile) { - context.pushView(view); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart deleted file mode 100644 index c5c7398bdb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart'; -import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; - -class SubPageTransactionHandler extends BlockTransactionHandler { - SubPageTransactionHandler() : super(type: SubPageBlockKeys.type); - - final List _beingCreated = []; - - @override - Future onTransaction( - BuildContext context, - String viewId, - EditorState editorState, - List added, - List removed, { - bool isCut = false, - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - bool isTurnInto = false, - String? parentViewId, - }) async { - if (isDraggingNode || isTurnInto) { - return; - } - - for (final node in removed) { - if (!context.mounted) return; - await _subPageDeleted(context, node); - } - - for (final node in added) { - if (!context.mounted) return; - await _subPageAdded( - context, - editorState, - node, - isCut: isCut, - isPaste: isPaste, - parentViewId: parentViewId, - ); - } - } - - Future _subPageDeleted( - BuildContext context, - Node node, - ) async { - if (node.type != type) { - return; - } - - final view = node.attributes[SubPageBlockKeys.viewId]; - if (view == null) { - return; - } - - final result = await ViewBackendService.deleteView(viewId: view); - result.fold( - (_) {}, - (error) { - Log.error(error); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedDeletePage.tr(), - ); - } - }, - ); - } - - Future _subPageAdded( - BuildContext context, - EditorState editorState, - Node node, { - bool isCut = false, - bool isPaste = false, - String? parentViewId, - }) async { - if (node.type != type || _beingCreated.contains(node.id)) { - return; - } - - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (viewId == null && parentViewId != null) { - _beingCreated.add(node.id); - - // This is a new Node, we need to create the view - final viewOrResult = await ViewBackendService.createView( - name: '', - layoutType: ViewLayoutPB.Document, - parentViewId: parentViewId, - ); - - await viewOrResult.fold( - (view) async { - final transaction = editorState.transaction - ..updateNode(node, {SubPageBlockKeys.viewId: view.id}); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - editorState.reload(); - - // Open view - getIt().openPlugin(view); - }, - (error) async { - Log.error(error); - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedCreatePage.tr(), - ); - - // Remove the node because it failed - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - }, - ); - - _beingCreated.remove(node.id); - } else if (isPaste) { - if (isCut && parentViewId != null) { - await TrashService.putback(viewId); - - final viewOrResult = await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: parentViewId, - prevViewId: null, - ); - - viewOrResult.fold( - (_) {}, - (error) { - Log.error(error); - showSnapBar( - context, - LocaleKeys.document_plugins_subPage_errors_failedMovePage.tr(), - ); - }, - ); - } else { - final viewId = node.attributes[SubPageBlockKeys.viewId]; - if (viewId == null) { - return; - } - - final viewOrResult = await ViewBackendService.getView(viewId); - return viewOrResult.fold( - (view) async { - final duplicatedViewOrResult = await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: false, - includeChildren: true, - syncAfterDuplicate: true, - parentViewId: parentViewId, - ); - - return duplicatedViewOrResult.fold( - (view) async { - final transaction = editorState.transaction - ..updateNode(node, { - SubPageBlockKeys.viewId: view.id, - SubPageBlockKeys.wasCut: false, - SubPageBlockKeys.wasCopied: false, - }); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - editorState.reload(); - }, - (error) { - Log.error(error); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys - .document_plugins_subPage_errors_failedDuplicatePage - .tr(), - ); - } - }, - ); - }, - (error) async { - Log.error(error); - - final transaction = editorState.transaction..deleteNode(node); - await editorState.apply( - transaction, - withUpdateSelection: false, - options: const ApplyOptions(recordUndo: false), - ); - editorState.reload(); - if (context.mounted) { - showSnapBar( - context, - LocaleKeys - .document_plugins_subPage_errors_failedDuplicateFindView - .tr(), - ); - } - }, - ); - } - } else { - // Try to restore from trash, and move to parent view - await TrashService.putback(viewId); - - // Check if View needs to be moved - if (parentViewId != null) { - final view = (await ViewBackendService.getView(viewId)).toNullable(); - if (view == null) { - return Log.error('View not found: $viewId'); - } - - if (view.parentViewId == parentViewId) { - return; - } - - await ViewBackendService.moveViewV2( - viewId: viewId, - newParentId: parentViewId, - prevViewId: null, - ); - } - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart index 0abba733fb..b1fac34423 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart @@ -1,11 +1,10 @@ -import 'dart:math' as math; - +import 'package:flutter/material.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_option_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; +import 'dart:math' as math; const tableActions = [ TableOptionAction.addAfter, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart index 993ee9b5a7..6d0597319c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/extensions/flowy_tint_extension.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart index 95841051d7..85972a3c2c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/todo_list/todo_list_icon.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,13 +17,9 @@ class TodoListIcon extends StatelessWidget { @override Widget build(BuildContext context) { - // the icon height should be equal to the text height * text font size - final textStyle = - context.read().editorStyle.textStyleConfiguration; - final fontSize = textStyle.text.fontSize ?? 16.0; - final height = textStyle.text.height ?? textStyle.lineHeight; - final iconSize = fontSize * height; - + final iconPadding = PlatformExtension.isMobile + ? context.read().state.iconPadding + : 0.0; final checked = node.attributes[TodoListBlockKeys.checked] ?? false; return GestureDetector( behavior: HitTestBehavior.opaque, @@ -31,18 +28,16 @@ class TodoListIcon extends StatelessWidget { onCheck(); }, child: Container( - constraints: BoxConstraints( - minWidth: iconSize, - minHeight: iconSize, + constraints: const BoxConstraints( + minWidth: 22, + minHeight: 22, ), - margin: const EdgeInsets.only(right: 8.0), - alignment: Alignment.center, + margin: EdgeInsets.only(top: iconPadding, right: 8.0), child: FlowySvg( checked ? FlowySvgs.m_todo_list_checked_s : FlowySvgs.m_todo_list_unchecked_s, blendMode: checked ? null : BlendMode.srcIn, - size: Size.square(iconSize * 0.9), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart index 9e93f80ce4..638efe7cf1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart @@ -1,11 +1,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart' hide TextDirection; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; class ToggleListBlockKeys { const ToggleListBlockKeys._(); @@ -23,11 +20,6 @@ class ToggleListBlockKeys { /// The value is a bool. static const String collapsed = 'collapsed'; - - /// The value is a int. - /// - /// If this value is not null, the block represent a toggle heading. - static const String level = 'level'; } Node toggleListBlockNode({ @@ -38,41 +30,17 @@ Node toggleListBlockNode({ Attributes? attributes, Iterable? children, }) { - delta ??= Delta()..insert(text ?? ''); return Node( type: ToggleListBlockKeys.type, - children: children ?? [], attributes: { + ToggleListBlockKeys.collapsed: collapsed, + ToggleListBlockKeys.delta: + (delta ?? (Delta()..insert(text ?? ''))).toJson(), + if (attributes != null) ...attributes, if (textDirection != null) ToggleListBlockKeys.textDirection: textDirection, - ToggleListBlockKeys.collapsed: collapsed, - ToggleListBlockKeys.delta: delta.toJson(), - if (attributes != null) ...attributes, - }, - ); -} - -Node toggleHeadingNode({ - int level = 1, - String? text, - Delta? delta, - bool collapsed = false, - String? textDirection, - Attributes? attributes, - Iterable? children, -}) { - // only support level 1 - 6 - level = level.clamp(1, 6); - return toggleListBlockNode( - text: text, - delta: delta, - collapsed: collapsed, - textDirection: textDirection, - children: children, - attributes: { - if (attributes != null) ...attributes, - ToggleListBlockKeys.level: level, }, + children: children ?? [], ); } @@ -89,14 +57,10 @@ class ToggleListBlockComponentBuilder extends BlockComponentBuilder { ToggleListBlockComponentBuilder({ super.configuration, this.padding = const EdgeInsets.all(0), - this.textStyleBuilder, }); final EdgeInsets padding; - /// The text style of the toggle heading block. - final TextStyle Function(int level)? textStyleBuilder; - @override BlockComponentWidget build(BlockComponentContext blockComponentContext) { final node = blockComponentContext.node; @@ -105,21 +69,16 @@ class ToggleListBlockComponentBuilder extends BlockComponentBuilder { node: node, configuration: configuration, padding: padding, - textStyleBuilder: textStyleBuilder, showActions: showActions(node), actionBuilder: (context, state) => actionBuilder( blockComponentContext, state, ), - actionTrailingBuilder: (context, state) => actionTrailingBuilder( - blockComponentContext, - state, - ), ); } @override - BlockComponentValidate get validate => (node) => node.delta != null; + bool validate(Node node) => node.delta != null; } class ToggleListBlockComponentWidget extends BlockComponentStatefulWidget { @@ -128,14 +87,11 @@ class ToggleListBlockComponentWidget extends BlockComponentStatefulWidget { required super.node, super.showActions, super.actionBuilder, - super.actionTrailingBuilder, super.configuration = const BlockComponentConfiguration(), this.padding = const EdgeInsets.all(0), - this.textStyleBuilder, }); final EdgeInsets padding; - final TextStyle Function(int level)? textStyleBuilder; @override State createState() => @@ -180,8 +136,6 @@ class _ToggleListBlockComponentWidgetState bool get collapsed => node.attributes[ToggleListBlockKeys.collapsed] ?? false; - int? get level => node.attributes[ToggleListBlockKeys.level] as int?; - @override Widget build(BuildContext context) { return collapsed @@ -189,41 +143,72 @@ class _ToggleListBlockComponentWidgetState : buildComponentWithChildren(context); } - @override - Widget buildComponentWithChildren(BuildContext context) { - return Stack( - children: [ - if (backgroundColor != Colors.transparent) - Positioned.fill( - left: cachedLeft, - top: padding.top, - child: Container( - width: double.infinity, - color: backgroundColor, - ), - ), - Provider( - create: (context) => - DatabasePluginWidgetBuilderSize(horizontalPadding: 0.0), - child: NestedListWidget( - indentPadding: indentPadding, - child: buildComponent(context), - children: editorState.renderer.buildList( - context, - widget.node.children, - ), - ), - ), - ], - ); - } - @override Widget buildComponent( BuildContext context, { bool withBackgroundColor = false, }) { - Widget child = _buildToggleBlock(); + final textDirection = calculateTextDirection( + layoutDirection: Directionality.maybeOf(context), + ); + + Widget child = Container( + color: backgroundColor, + width: double.infinity, + alignment: alignment, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + textDirection: textDirection, + children: [ + // the emoji picker button for the note + Container( + constraints: const BoxConstraints(minWidth: 26, minHeight: 22), + padding: const EdgeInsets.only(right: 4.0), + child: AnimatedRotation( + turns: collapsed ? 0.0 : 0.25, + duration: const Duration(milliseconds: 200), + child: FlowyIconButton( + width: 18.0, + icon: const Icon( + Icons.arrow_right, + size: 18.0, + ), + onPressed: onCollapsed, + ), + ), + ), + + Flexible( + child: AppFlowyRichText( + key: forwardKey, + delegate: this, + node: widget.node, + editorState: editorState, + placeholderText: placeholderText, + lineHeight: 1.5, + textSpanDecorator: (textSpan) => textSpan.updateTextStyle( + textStyle, + ), + placeholderTextSpanDecorator: (textSpan) => + textSpan.updateTextStyle( + placeholderTextStyle, + ), + textDirection: textDirection, + textAlign: alignment?.toTextAlign, + cursorColor: editorState.editorStyle.cursorColor, + selectionColor: editorState.editorStyle.selectionColor, + ), + ), + ], + ), + ); + + child = Padding( + key: blockComponentKey, + padding: padding, + child: child, + ); child = BlockSelectionContainer( node: node, @@ -236,23 +221,10 @@ class _ToggleListBlockComponentWidgetState child: child, ); - child = Padding( - padding: padding, - child: Container( - key: blockComponentKey, - color: withBackgroundColor || - (backgroundColor != Colors.transparent && collapsed) - ? backgroundColor - : null, - child: child, - ), - ); - if (widget.showActions && widget.actionBuilder != null) { child = BlockComponentActionWrapper( node: node, actionBuilder: widget.actionBuilder!, - actionTrailingBuilder: widget.actionTrailingBuilder, child: child, ); } @@ -260,173 +232,11 @@ class _ToggleListBlockComponentWidgetState return child; } - Widget _buildToggleBlock() { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - final crossAxisAlignment = textDirection == TextDirection.ltr - ? CrossAxisAlignment.start - : CrossAxisAlignment.end; - - return Container( - width: double.infinity, - alignment: alignment, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: crossAxisAlignment, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - textDirection: textDirection, - children: [ - _buildExpandIcon(), - Flexible( - child: _buildRichText(), - ), - ], - ), - _buildPlaceholder(), - ], - ), - ); - } - - Widget _buildPlaceholder() { - // if the toggle block is collapsed or it contains children, don't show the - // placeholder. - if (collapsed || node.children.isNotEmpty) { - return const SizedBox.shrink(); - } - - return Padding( - padding: UniversalPlatform.isMobile - ? const EdgeInsets.symmetric(horizontal: 26.0) - : indentPadding, - child: FlowyButton( - text: FlowyText( - buildPlaceholderText(), - color: Theme.of(context).hintColor, - ), - margin: const EdgeInsets.symmetric(horizontal: 3.0, vertical: 8), - onTap: onAddContent, - ), - ); - } - - Widget _buildRichText() { - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - final level = node.attributes[ToggleListBlockKeys.level]; - return AppFlowyRichText( - key: forwardKey, - delegate: this, - node: widget.node, - editorState: editorState, - placeholderText: placeholderText, - lineHeight: 1.5, - textSpanDecorator: (textSpan) { - var result = textSpan.updateTextStyle( - textStyleWithTextSpan(textSpan: textSpan), - ); - if (level != null) { - result = result.updateTextStyle( - widget.textStyleBuilder?.call(level), - ); - } - return result; - }, - placeholderTextSpanDecorator: (textSpan) { - var result = textSpan.updateTextStyle( - textStyleWithTextSpan(textSpan: textSpan), - ); - if (level != null && widget.textStyleBuilder != null) { - result = result.updateTextStyle( - widget.textStyleBuilder?.call(level), - ); - } - return result.updateTextStyle( - placeholderTextStyleWithTextSpan(textSpan: textSpan), - ); - }, - textDirection: textDirection, - textAlign: alignment?.toTextAlign ?? textAlign, - cursorColor: editorState.editorStyle.cursorColor, - selectionColor: editorState.editorStyle.selectionColor, - ); - } - - Widget _buildExpandIcon() { - double buttonHeight = UniversalPlatform.isDesktop ? 22.0 : 26.0; - final textDirection = calculateTextDirection( - layoutDirection: Directionality.maybeOf(context), - ); - - if (level != null) { - // top padding * 2 + button height = height of the heading text - final textStyle = widget.textStyleBuilder?.call(level ?? 1); - final fontSize = textStyle?.fontSize; - final lineHeight = textStyle?.height ?? 1.5; - - if (fontSize != null) { - buttonHeight = fontSize * lineHeight; - } - } - - final turns = switch (textDirection) { - TextDirection.ltr => collapsed ? 0.0 : 0.25, - TextDirection.rtl => collapsed ? -0.5 : -0.75, - }; - - return Container( - constraints: BoxConstraints( - minWidth: 26, - minHeight: buttonHeight, - ), - alignment: Alignment.center, - child: FlowyButton( - margin: const EdgeInsets.all(2.0), - useIntrinsicWidth: true, - onTap: onCollapsed, - text: AnimatedRotation( - turns: turns, - duration: const Duration(milliseconds: 200), - child: const Icon( - Icons.arrow_right, - size: 18.0, - ), - ), - ), - ); - } - Future onCollapsed() async { final transaction = editorState.transaction ..updateNode(node, { ToggleListBlockKeys.collapsed: !collapsed, }); - transaction.afterSelection = editorState.selection; await editorState.apply(transaction); } - - Future onAddContent() async { - final transaction = editorState.transaction; - final path = node.path.child(0); - transaction.insertNode( - path, - paragraphNode(), - ); - transaction.afterSelection = Selection.collapsed(Position(path: path)); - await editorState.apply(transaction); - } - - String buildPlaceholderText() { - if (level != null) { - return LocaleKeys.document_plugins_emptyToggleHeading.tr( - args: [level.toString()], - ); - } - return LocaleKeys.document_plugins_emptyToggleList.tr(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart new file mode 100644 index 0000000000..db5eae3218 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart @@ -0,0 +1,147 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +const _greater = '>'; + +/// Convert '> ' to toggle list +/// +/// - support +/// - desktop +/// - mobile +/// - web +/// +CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent( + key: 'format greater to quote', + character: ' ', + handler: (editorState) async => formatMarkdownSymbol( + editorState, + (node) => node.type != ToggleListBlockKeys.type, + (_, text, __) => text == _greater, + (_, node, delta) => [ + toggleListBlockNode( + delta: delta.compose(Delta()..delete(_greater.length)), + ), + ], + ), +); + +/// Press enter key to insert child node inside the toggle list +/// +/// - support +/// - desktop +/// - mobile +/// - web +CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( + key: 'insert child node inside toggle list', + character: '\n', + handler: (editorState) async { + final selection = editorState.selection; + if (selection == null || !selection.isCollapsed) { + return false; + } + final node = editorState.getNodeAtPath(selection.start.path); + final delta = node?.delta; + if (node == null || + node.type != ToggleListBlockKeys.type || + delta == null) { + return false; + } + final slicedDelta = delta.slice(selection.start.offset); + final transaction = editorState.transaction; + final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool; + if (collapsed) { + // if the delta is empty, clear the format + if (delta.isEmpty) { + transaction + ..insertNode( + selection.start.path.next, + paragraphNode(), + ) + ..deleteNode(node) + ..afterSelection = Selection.collapsed( + Position(path: selection.start.path), + ); + } else if (selection.startIndex == 0) { + // insert a paragraph block above the current toggle list block + transaction.insertNode(selection.start.path, paragraphNode()); + transaction.afterSelection = Selection.collapsed( + Position(path: selection.start.path.next), + ); + } else { + // insert a toggle list block below the current toggle list block + transaction + ..deleteText(node, selection.startIndex, slicedDelta.length) + ..insertNodes( + selection.start.path.next, + [ + toggleListBlockNode(collapsed: true, delta: slicedDelta), + paragraphNode(), + ], + ) + ..afterSelection = Selection.collapsed( + Position(path: selection.start.path.next), + ); + } + } else { + // insert a paragraph block inside the current toggle list block + transaction + ..deleteText(node, selection.startIndex, slicedDelta.length) + ..insertNode( + selection.start.path + [0], + paragraphNode(delta: slicedDelta), + ) + ..afterSelection = Selection.collapsed( + Position(path: selection.start.path + [0]), + ); + } + await editorState.apply(transaction); + return true; + }, +); + +/// cmd/ctrl + enter to close or open the toggle list +/// +/// - support +/// - desktop +/// - web +/// + +// toggle the todo list +final CommandShortcutEvent toggleToggleListCommand = CommandShortcutEvent( + key: 'toggle the toggle list', + getDescription: () => AppFlowyEditorL10n.current.cmdToggleTodoList, + command: 'ctrl+enter', + macOSCommand: 'cmd+enter', + handler: _toggleToggleListCommandHandler, +); + +CommandShortcutEventHandler _toggleToggleListCommandHandler = (editorState) { + if (PlatformExtension.isMobile) { + assert(false, 'enter key is not supported on mobile platform.'); + return KeyEventResult.ignored; + } + + final selection = editorState.selection; + if (selection == null) { + return KeyEventResult.ignored; + } + final nodes = editorState.getNodesInSelection(selection); + if (nodes.isEmpty || nodes.length > 1) { + return KeyEventResult.ignored; + } + + final node = nodes.first; + if (node.type != ToggleListBlockKeys.type) { + return KeyEventResult.ignored; + } + + final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool; + final transaction = editorState.transaction; + transaction.updateNode(node, { + ToggleListBlockKeys.collapsed: !collapsed, + }); + transaction.afterSelection = selection; + editorState.apply(transaction); + return KeyEventResult.handled; +}; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart deleted file mode 100644 index f3059bf1be..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart +++ /dev/null @@ -1,303 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_component.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -const _greater = '>'; - -/// Convert '> ' to toggle list -/// -/// - support -/// - desktop -/// - mobile -/// - web -/// -CharacterShortcutEvent formatGreaterToToggleList = CharacterShortcutEvent( - key: 'format greater to toggle list', - character: ' ', - handler: (editorState) async => _formatGreaterSymbol( - editorState, - (node) => node.type != ToggleListBlockKeys.type, - (_, text, __) => text == _greater, - (text, node, delta, afterSelection) async => _formatGreaterToToggleHeading( - editorState, - text, - node, - delta, - afterSelection, - ), - ), -); - -Future _formatGreaterToToggleHeading( - EditorState editorState, - String text, - Node node, - Delta delta, - Selection afterSelection, -) async { - final type = node.type; - int? level; - if (type == ToggleListBlockKeys.type) { - level = node.attributes[ToggleListBlockKeys.level] as int?; - } else if (type == HeadingBlockKeys.type) { - level = node.attributes[HeadingBlockKeys.level] as int?; - } - delta = delta.compose(Delta()..delete(_greater.length)); - // if the previous block is heading block, convert it to toggle heading block - if (type == HeadingBlockKeys.type && level != null) { - await BlockActionOptionCubit.turnIntoSingleToggleHeading( - type: ToggleListBlockKeys.type, - selectedNodes: [node], - level: level, - delta: delta, - editorState: editorState, - afterSelection: afterSelection, - ); - return; - } - - final transaction = editorState.transaction; - transaction - ..insertNode( - node.path, - toggleListBlockNode( - delta: delta, - children: node.children.map((e) => e.deepCopy()).toList(), - ), - ) - ..deleteNode(node); - transaction.afterSelection = afterSelection; - await editorState.apply(transaction); -} - -/// Press enter key to insert child node inside the toggle list -/// -/// - support -/// - desktop -/// - mobile -/// - web -CharacterShortcutEvent insertChildNodeInsideToggleList = CharacterShortcutEvent( - key: 'insert child node inside toggle list', - character: '\n', - handler: (editorState) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return false; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || - node.type != ToggleListBlockKeys.type || - delta == null) { - return false; - } - final slicedDelta = delta.slice(selection.start.offset); - final transaction = editorState.transaction; - final bool collapsed = - node.attributes[ToggleListBlockKeys.collapsed] ?? false; - if (collapsed) { - // if the delta is empty, clear the format - if (delta.isEmpty) { - transaction - ..insertNode( - selection.start.path.next, - paragraphNode(), - ) - ..deleteNode(node) - ..afterSelection = Selection.collapsed( - Position(path: selection.start.path), - ); - } else if (selection.startIndex == 0) { - // insert a paragraph block above the current toggle list block - transaction.insertNode(selection.start.path, paragraphNode()); - transaction.afterSelection = Selection.collapsed( - Position(path: selection.start.path.next), - ); - } else { - // insert a toggle list block below the current toggle list block - transaction - ..deleteText(node, selection.startIndex, slicedDelta.length) - ..insertNodes( - selection.start.path.next, - [ - toggleListBlockNode(collapsed: true, delta: slicedDelta), - ], - ) - ..afterSelection = Selection.collapsed( - Position(path: selection.start.path.next), - ); - } - } else { - // insert a paragraph block inside the current toggle list block - transaction - ..deleteText(node, selection.startIndex, slicedDelta.length) - ..insertNode( - selection.start.path + [0], - paragraphNode(delta: slicedDelta), - ) - ..afterSelection = Selection.collapsed( - Position(path: selection.start.path + [0]), - ); - } - await editorState.apply(transaction); - return true; - }, -); - -/// cmd/ctrl + enter to close or open the toggle list -/// -/// - support -/// - desktop -/// - web -/// - -// toggle the todo list -final CommandShortcutEvent toggleToggleListCommand = CommandShortcutEvent( - key: 'toggle the toggle list', - getDescription: () => AppFlowyEditorL10n.current.cmdToggleTodoList, - command: 'ctrl+enter', - macOSCommand: 'cmd+enter', - handler: _toggleToggleListCommandHandler, -); - -CommandShortcutEventHandler _toggleToggleListCommandHandler = (editorState) { - if (UniversalPlatform.isMobile) { - assert(false, 'enter key is not supported on mobile platform.'); - return KeyEventResult.ignored; - } - - final selection = editorState.selection; - if (selection == null) { - return KeyEventResult.ignored; - } - final nodes = editorState.getNodesInSelection(selection); - if (nodes.isEmpty || nodes.length > 1) { - return KeyEventResult.ignored; - } - - final node = nodes.first; - if (node.type != ToggleListBlockKeys.type) { - return KeyEventResult.ignored; - } - - final collapsed = node.attributes[ToggleListBlockKeys.collapsed] as bool; - final transaction = editorState.transaction; - transaction.updateNode(node, { - ToggleListBlockKeys.collapsed: !collapsed, - }); - transaction.afterSelection = selection; - editorState.apply(transaction); - return KeyEventResult.handled; -}; - -/// Press the backspace at the first position of first line to go to the title -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent removeToggleHeadingStyle = CommandShortcutEvent( - key: 'remove toggle heading style', - command: 'backspace', - getDescription: () => 'remove toggle heading style', - handler: (editorState) => _removeToggleHeadingStyle( - editorState: editorState, - ), -); - -// convert the toggle heading block to heading block -KeyEventResult _removeToggleHeadingStyle({ - required EditorState editorState, -}) { - final selection = editorState.selection; - if (selection == null || - !selection.isCollapsed || - selection.start.offset != 0) { - return KeyEventResult.ignored; - } - - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.type != ToggleListBlockKeys.type) { - return KeyEventResult.ignored; - } - - final level = node.attributes[ToggleListBlockKeys.level] as int?; - if (level == null) { - return KeyEventResult.ignored; - } - - final transaction = editorState.transaction; - transaction.updateNode(node, { - ToggleListBlockKeys.level: null, - }); - transaction.afterSelection = selection; - editorState.apply(transaction); - - return KeyEventResult.handled; -} - -/// Formats the current node to specified markdown style. -/// -/// For example, -/// bulleted list: '- ' -/// numbered list: '1. ' -/// quote: '" ' -/// ... -/// -/// The [nodeBuilder] can return a list of nodes, which will be inserted -/// into the document. -/// For example, when converting a bulleted list to a heading and the heading is -/// not allowed to contain children, then the [nodeBuilder] should return a list -/// of nodes, which contains the heading node and the children nodes. -Future _formatGreaterSymbol( - EditorState editorState, - bool Function(Node node) shouldFormat, - bool Function( - Node node, - String text, - Selection selection, - ) predicate, - Future Function( - String text, - Node node, - Delta delta, - Selection afterSelection, - ) onFormat, -) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return false; - } - - final position = selection.end; - final node = editorState.getNodeAtPath(position.path); - - if (node == null || !shouldFormat(node)) { - return false; - } - - // Get the text from the start of the document until the selection. - final delta = node.delta; - if (delta == null) { - return false; - } - final text = delta.toPlainText().substring(0, selection.end.offset); - - // If the text doesn't match the predicate, then we don't want to - // format it. - if (!predicate(node, text, selection)) { - return false; - } - - final afterSelection = Selection.collapsed( - Position( - path: node.path, - ), - ); - - await onFormat(text, node, delta, afterSelection); - - return true; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart deleted file mode 100644 index d4f3d21f46..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_format_toolbar_items.dart +++ /dev/null @@ -1,135 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flutter/material.dart'; - -import 'custom_placeholder_toolbar_item.dart'; -import 'toolbar_id_enum.dart'; - -final List customMarkdownFormatItems = [ - _FormatToolbarItem( - id: ToolbarId.bold, - name: 'bold', - svg: FlowySvgs.toolbar_bold_m, - ), - group1PaddingItem, - _FormatToolbarItem( - id: ToolbarId.underline, - name: 'underline', - svg: FlowySvgs.toolbar_underline_m, - ), - group1PaddingItem, - _FormatToolbarItem( - id: ToolbarId.italic, - name: 'italic', - svg: FlowySvgs.toolbar_inline_italic_m, - ), -]; - -final ToolbarItem customInlineCodeItem = _FormatToolbarItem( - id: ToolbarId.code, - name: 'code', - svg: FlowySvgs.toolbar_inline_code_m, - group: 2, -); - -class _FormatToolbarItem extends ToolbarItem { - _FormatToolbarItem({ - required ToolbarId id, - required String name, - required FlowySvgData svg, - super.group = 1, - }) : super( - id: id.id, - isActive: showInAnyTextType, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection( - selection, - (delta) => - delta.isNotEmpty && - delta.everyAttributes((attr) => attr[name] == true), - ); - - final hoverColor = isHighlight - ? highlightColor - : EditorStyleCustomizer.toolbarHoverColor(context); - final isDark = !Theme.of(context).isLightMode; - final theme = AppFlowyTheme.of(context); - - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: hoverColor, - isSelected: isHighlight, - icon: FlowySvg( - svg, - size: Size.square(20.0), - color: (isDark && isHighlight) - ? Color(0xFF282E3A) - : theme.iconColorScheme.primary, - ), - onPressed: () => editorState.toggleAttribute( - name, - selection: selection, - ), - ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - id.id, - _getTooltipText(id), - child, - ); - } - return child; - }, - ); -} - -String _getTooltipText(ToolbarId id) { - switch (id) { - case ToolbarId.underline: - return '${LocaleKeys.toolbar_underline.tr()}${shortcutTooltips( - '⌘ + U', - 'CTRL + U', - 'CTRL + U', - )}'; - case ToolbarId.bold: - return '${LocaleKeys.toolbar_bold.tr()}${shortcutTooltips( - '⌘ + B', - 'CTRL + B', - 'CTRL + B', - )}'; - case ToolbarId.italic: - return '${LocaleKeys.toolbar_italic.tr()}${shortcutTooltips( - '⌘ + I', - 'CTRL + I', - 'CTRL + I', - )}'; - case ToolbarId.code: - return '${LocaleKeys.document_toolbar_inlineCode.tr()}${shortcutTooltips( - '⌘ + E', - 'CTRL + E', - 'CTRL + E', - )}'; - default: - return ''; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart deleted file mode 100644 index 46f2c02c5a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_hightlight_color_toolbar_item.dart +++ /dev/null @@ -1,227 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -String? _customHighlightColorHex; - -final customHighlightColorItem = ToolbarItem( - id: ToolbarId.highlightColor.id, - group: 1, - isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => - HighlightColorPickerWidget( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ), -); - -class HighlightColorPickerWidget extends StatefulWidget { - const HighlightColorPickerWidget({ - super.key, - required this.editorState, - this.tooltipBuilder, - required this.highlightColor, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => - _HighlightColorPickerWidgetState(); -} - -class _HighlightColorPickerWidgetState - extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - if (editorState.selection == null) { - return const SizedBox.shrink(); - } - final selectionRectList = editorState.selectionRects(); - final top = - selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, top), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - margin: EdgeInsets.zero, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: SizedBox( - width: 20, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_highlight_m, - size: Size(20, 16), - color: iconColor, - ), - buildColorfulDivider(iconColor), - ], - ), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.highlightColor.id, - AppFlowyEditorL10n.current.highlightColor, - child, - ) ?? - child; - } - - Widget buildColorfulDivider(Color? iconColor) { - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.backgroundColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - - final colorLength = colors.length; - if (colors.isEmpty || !isHighLight) { - return Container( - width: 20, - height: 4, - color: iconColor, - ); - } - return SizedBox( - width: 20, - height: 4, - child: Row( - children: List.generate(colorLength, (index) { - final currentColor = int.tryParse(colors[index]); - return Container( - width: 20 / colorLength, - height: 4, - color: currentColor == null ? iconColor : Color(currentColor), - ); - }), - ), - ); - } - - Widget buildPopoverContent() { - final List colors = []; - - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attributes) { - final highlightColorHex = - attributes[AppFlowyRichTextKeys.backgroundColor]; - if (highlightColorHex != null) colors.add(highlightColorHex); - return highlightColorHex != null; - }); - }); - bool showClearButton = false; - nodes.allSatisfyInSelection(selection, (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element.attributes?[AppFlowyRichTextKeys.backgroundColor] != - null; - }, - ); - } - return true; - }); - return MouseRegion( - child: ColorPicker( - title: AppFlowyEditorL10n.current.highlightColor, - showClearButton: showClearButton, - selectedColorHex: - (colors.length == 1 && isHighlight) ? colors.first : null, - customColorHex: _customHighlightColorHex, - colorOptions: generateHighlightColorOptions(), - onSubmittedColorHex: (color, isCustomColor) { - if (isCustomColor) { - _customHighlightColorHex = color; - } - formatHighlightColor( - editorState, - editorState.selection, - color, - withUpdateSelection: true, - ); - hidePopover(); - }, - resetText: AppFlowyEditorL10n.current.clearHighlightColor, - resetIconName: 'clear_highlight_color', - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void hidePopover() { - popoverController.close(); - keepEditorFocusNotifier.decrease(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart deleted file mode 100644 index 8c9e6b69da..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_link_toolbar_item.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'toolbar_id_enum.dart'; - -const kIsPageLink = 'is_page_link'; - -final customLinkItem = ToolbarItem( - id: ToolbarId.link.id, - group: 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) { - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHref = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[AppFlowyRichTextKeys.href] != null, - ); - }); - - final isDark = !Theme.of(context).isLightMode; - final hoverColor = isHref - ? highlightColor - : EditorStyleCustomizer.toolbarHoverColor(context); - final theme = AppFlowyTheme.of(context); - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: hoverColor, - isSelected: isHref, - icon: FlowySvg( - FlowySvgs.toolbar_link_m, - size: Size.square(20.0), - color: (isDark && isHref) - ? Color(0xFF282E3A) - : theme.iconColorScheme.primary, - ), - onPressed: () { - getIt().hideToolbar(); - if (!isHref) { - final viewId = context.read()?.documentId ?? ''; - showLinkCreateMenu(context, editorState, selection, viewId); - } else { - WidgetsBinding.instance.addPostFrameCallback((_) { - getIt() - .call(HoverTriggerKey(nodes.first.id, selection)); - }); - } - }, - ); - - if (tooltipBuilder != null) { - return tooltipBuilder( - context, - ToolbarId.highlightColor.id, - AppFlowyEditorL10n.current.link, - child, - ); - } - - return child; - }, -); - -extension AttributeExtension on Attributes { - bool get isPage { - if (this[kIsPageLink] is bool) { - return this[kIsPageLink]; - } - return false; - } -} - -enum LinkMenuAlignment { - topLeft, - topRight, - bottomLeft, - bottomRight, -} - -extension LinkMenuAlignmentExtension on LinkMenuAlignment { - bool get isTop => - this == LinkMenuAlignment.topLeft || this == LinkMenuAlignment.topRight; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart deleted file mode 100644 index e087731c82..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_placeholder_toolbar_item.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/ai_writer_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customPlaceholderItem = ToolbarItem( - id: ToolbarId.placeholder.id, - group: -1, - isActive: (editorState) => true, - builder: (context, __, ___, ____, _____) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 5), - child: Container( - width: 1, - color: Color(0xffE8ECF3).withAlpha(isDark ? 40 : 255), - ), - ); - }, -); - -ToolbarItem buildPaddingPlaceholderItem( - int group, { - bool Function(EditorState editorState)? isActive, -}) => - ToolbarItem( - id: ToolbarId.paddingPlaceHolder.id, - group: group, - isActive: isActive, - builder: (context, __, ___, ____, _____) => HSpace(4), - ); - -ToolbarItem group0PaddingItem = buildPaddingPlaceholderItem( - 0, - isActive: onlyShowInTextTypeAndExcludeTable, -); - -ToolbarItem group1PaddingItem = - buildPaddingPlaceholderItem(1, isActive: showInAnyTextType); - -ToolbarItem group4PaddingItem = buildPaddingPlaceholderItem( - 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart deleted file mode 100644 index efaff532f4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_align_toolbar_item.dart +++ /dev/null @@ -1,215 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/toolbar_extension.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customTextAlignItem = ToolbarItem( - id: ToolbarId.textAlign.id, - group: 4, - isActive: (state) => - !isNarrowWindow(state) && onlyShowInSingleSelectionAndTextType(state), - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return TextAlignActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ); - }, -); - -class TextAlignActionList extends StatefulWidget { - const TextAlignActionList({ - super.key, - required this.editorState, - required this.highlightColor, - this.tooltipBuilder, - this.child, - this.onSelect, - this.popoverController, - this.popoverDirection = PopoverDirection.bottomWithLeftAligned, - this.showOffset = const Offset(0, 2), - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - final Widget? child; - final VoidCallback? onSelect; - final PopoverController? popoverController; - final PopoverDirection popoverDirection; - final Offset showOffset; - - @override - State createState() => _TextAlignActionListState(); -} - -class _TextAlignActionListState extends State { - late PopoverController popoverController = - widget.popoverController ?? PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: widget.popoverDirection, - offset: widget.showOffset, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: widget.child ?? buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_alignment_m, - size: Size.square(20), - color: iconColor, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textAlign.id, - LocaleKeys.document_toolbar_textAlign.tr(), - child, - ) ?? - child; - } - - Widget buildPopoverContent() { - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: List.generate(TextAlignCommand.values.length, (index) { - final command = TextAlignCommand.values[index]; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.every( - (n) => n.attributes[blockComponentAlign] == command.name, - ); - - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - iconPadding: 12, - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: - isHighlight ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () { - command.onAlignChanged(editorState); - widget.onSelect?.call(); - popoverController.close(); - }, - ), - ); - }), - ), - ); - } -} - -enum TextAlignCommand { - left(FlowySvgs.toolbar_text_align_left_m), - center(FlowySvgs.toolbar_text_align_center_m), - right(FlowySvgs.toolbar_text_align_right_m); - - const TextAlignCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case left: - return LocaleKeys.document_toolbar_alignLeft.tr(); - case center: - return LocaleKeys.document_toolbar_alignCenter.tr(); - case right: - return LocaleKeys.document_toolbar_alignRight.tr(); - } - } - - Future onAlignChanged(EditorState editorState) async { - final selection = editorState.selection!; - - await editorState.updateNode( - selection, - (node) => node.copyWith( - attributes: { - ...node.attributes, - blockComponentAlign: name, - }, - ), - selectionExtraInfo: { - selectionExtraInfoDoNotAttachTextService: true, - selectionExtraInfoDisableFloatingToolbar: true, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart deleted file mode 100644 index 9f5a917b89..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/custom_text_color_toolbar_item.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/color_picker.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' hide ColorPicker; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'toolbar_id_enum.dart'; - -String? _customColorHex; - -final customTextColorItem = ToolbarItem( - id: ToolbarId.textColor.id, - group: 1, - isActive: showInAnyTextType, - builder: (context, editorState, highlightColor, iconColor, tooltipBuilder) => - TextColorPickerWidget( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ), -); - -class TextColorPickerWidget extends StatefulWidget { - const TextColorPickerWidget({ - super.key, - required this.editorState, - this.tooltipBuilder, - required this.highlightColor, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => _TextColorPickerWidgetState(); -} - -class _TextColorPickerWidgetState extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - if (editorState.selection == null) { - return const SizedBox.shrink(); - } - final selectionRectList = editorState.selectionRects(); - final top = - selectionRectList.isEmpty ? 0.0 : selectionRectList.first.height; - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: Offset(0, top), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - margin: EdgeInsets.zero, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 36, - height: 32, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: SizedBox( - width: 20, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_color_m, - size: Size(20, 16), - color: iconColor, - ), - buildColorfulDivider(iconColor), - ], - ), - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textColor.id, - LocaleKeys.document_toolbar_textColor.tr(), - child, - ) ?? - child; - } - - Widget buildColorfulDivider(Color? iconColor) { - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.textColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - - final colorLength = colors.length; - if (colors.isEmpty || !isHighLight) { - return Container( - width: 20, - height: 4, - color: iconColor, - ); - } - return SizedBox( - width: 20, - height: 4, - child: Row( - children: List.generate(colorLength, (index) { - final currentColor = int.tryParse(colors[index]); - return Container( - width: 20 / colorLength, - height: 4, - color: currentColor == null ? iconColor : Color(currentColor), - ); - }), - ), - ); - } - - Widget buildPopoverContent() { - bool showClearButton = false; - final List colors = []; - final selection = editorState.selection!; - final nodes = editorState.getNodesInSelection(selection); - final isHighLight = nodes.allSatisfyInSelection(selection, (delta) { - if (delta.everyAttributes((attr) => attr.isEmpty)) { - return false; - } - - return delta.everyAttributes((attr) { - final textColorHex = attr[AppFlowyRichTextKeys.textColor]; - if (textColorHex != null) colors.add(textColorHex); - return (textColorHex != null); - }); - }); - nodes.allSatisfyInSelection( - selection, - (delta) { - if (!showClearButton) { - showClearButton = delta.whereType().any( - (element) { - return element.attributes?[AppFlowyRichTextKeys.textColor] != - null; - }, - ); - } - return true; - }, - ); - return MouseRegion( - child: ColorPicker( - title: LocaleKeys.document_toolbar_textColor.tr(), - showClearButton: showClearButton, - selectedColorHex: - (colors.length == 1 && isHighLight) ? colors.first : null, - customColorHex: _customColorHex, - colorOptions: generateTextColorOptions(), - onSubmittedColorHex: (color, isCustomColor) { - if (isCustomColor) { - _customColorHex = color; - } - formatFontColor( - editorState, - editorState.selection, - color, - withUpdateSelection: true, - ); - hidePopover(); - }, - resetText: AppFlowyEditorL10n.current.resetToDefaultColor, - resetIconName: 'reset_text_color', - ), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - void hidePopover() { - popoverController.close(); - keepEditorFocusNotifier.decrease(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart deleted file mode 100644 index 46b707a8d3..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/more_option_toolbar_item.dart +++ /dev/null @@ -1,455 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_page.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_create_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -// ignore: implementation_imports -import 'package:appflowy_editor/src/editor/toolbar/desktop/items/utils/tooltip_util.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 'custom_text_align_toolbar_item.dart'; -import 'text_suggestions_toolbar_item.dart'; - -const _kMoreOptionItemId = 'editor.more_option'; -const kFontToolbarItemId = 'editor.font'; - -@visibleForTesting -const kFontFamilyToolbarItemKey = ValueKey('FontFamilyToolbarItem'); - -final ToolbarItem moreOptionItem = ToolbarItem( - id: _kMoreOptionItemId, - group: 5, - isActive: showInAnyTextType, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return MoreOptionActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - highlightColor: highlightColor, - ); - }, -); - -class MoreOptionActionList extends StatefulWidget { - const MoreOptionActionList({ - super.key, - required this.editorState, - required this.highlightColor, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Color highlightColor; - - @override - State createState() => _MoreOptionActionListState(); -} - -class _MoreOptionActionListState extends State { - final popoverController = PopoverController(); - PopoverController fontPopoverController = PopoverController(); - PopoverController suggestionsPopoverController = PopoverController(); - PopoverController textAlignPopoverController = PopoverController(); - - bool isSelected = false; - - EditorState get editorState => widget.editorState; - - Color get highlightColor => widget.highlightColor; - - MoreOptionCommand? tappedCommand; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - fontPopoverController.close(); - suggestionsPopoverController.close(); - textAlignPopoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final iconColor = Theme.of(context).iconTheme.color; - final child = FlowyIconButton( - width: 36, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: FlowySvg( - FlowySvgs.toolbar_more_m, - size: Size.square(20), - color: iconColor, - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - _kMoreOptionItemId, - LocaleKeys.document_toolbar_moreOptions.tr(), - child, - ) ?? - child; - } - - Color? getFormulaColor() { - if (isFormulaHighlight(editorState)) { - return widget.highlightColor; - } - return null; - } - - Color? getStrikethroughColor() { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return null; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return null; - } - - final nodes = editorState.getNodesInSelection(selection); - final isHighlight = nodes.allSatisfyInSelection( - selection, - (delta) => - delta.isNotEmpty && - delta.everyAttributes( - (attr) => attr[MoreOptionCommand.strikethrough.name] == true, - ), - ); - return isHighlight ? widget.highlightColor : null; - } - - Widget buildPopoverContent() { - final showFormula = onlyShowInSingleSelectionAndTextType(editorState); - const fontColor = Color(0xff99A1A8); - final isNarrow = isNarrowWindow(editorState); - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: [ - if (isNarrow) ...[ - buildTurnIntoSelector(), - buildCommandItem(MoreOptionCommand.link), - buildTextAlignSelector(), - ], - buildFontSelector(), - buildCommandItem( - MoreOptionCommand.strikethrough, - rightIcon: FlowyText( - shortcutTooltips( - '⌘⇧S', - 'Ctrl⇧S', - 'Ctrl⇧S', - ).trim(), - color: fontColor, - fontSize: 12, - figmaLineHeight: 16, - fontWeight: FontWeight.w400, - ), - ), - if (showFormula) - buildCommandItem( - MoreOptionCommand.formula, - rightIcon: FlowyText( - shortcutTooltips( - '⌘⇧E', - 'Ctrl⇧E', - 'Ctrl⇧E', - ).trim(), - color: fontColor, - fontSize: 12, - figmaLineHeight: 16, - fontWeight: FontWeight.w400, - ), - ), - ], - ), - ); - } - - Widget buildCommandItem( - MoreOptionCommand command, { - Widget? rightIcon, - VoidCallback? onTap, - }) { - final isFontCommand = command == MoreOptionCommand.font; - return SizedBox( - height: 36, - child: FlowyButton( - key: isFontCommand ? kFontFamilyToolbarItemKey : null, - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - rightIcon: rightIcon, - iconPadding: 12, - text: FlowyText( - command.title, - figmaLineHeight: 20, - fontWeight: FontWeight.w400, - ), - onTap: onTap ?? - () { - command.onExecute(editorState, context); - hideOtherPopovers(command); - if (command != MoreOptionCommand.font) { - popoverController.close(); - } - }, - ), - ); - } - - Widget buildFontSelector() { - final selection = editorState.selection!; - final String? currentFontFamily = editorState - .getDeltaAttributeValueInSelection(AppFlowyRichTextKeys.fontFamily); - return FontFamilyDropDown( - currentFontFamily: currentFontFamily ?? '', - offset: const Offset(-240, 0), - popoverController: fontPopoverController, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () => keepEditorFocusNotifier.decrease(), - onFontFamilyChanged: (fontFamily) async { - fontPopoverController.close(); - popoverController.close(); - try { - await editorState.formatDelta(selection, { - AppFlowyRichTextKeys.fontFamily: fontFamily, - }); - } catch (e) { - Log.error('Failed to set font family: $e'); - } - }, - onResetFont: () async { - fontPopoverController.close(); - popoverController.close(); - await editorState - .formatDelta(selection, {AppFlowyRichTextKeys.fontFamily: null}); - }, - child: buildCommandItem( - MoreOptionCommand.font, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - ), - ); - } - - Widget buildTurnIntoSelector() { - final selectionRects = editorState.selectionRects(); - double height = -6; - if (selectionRects.isNotEmpty) height = selectionRects.first.height; - return SuggestionsActionList( - editorState: editorState, - popoverController: suggestionsPopoverController, - popoverDirection: PopoverDirection.leftWithTopAligned, - showOffset: Offset(-8, height), - onSelect: () => getIt().hideToolbar(), - child: buildCommandItem( - MoreOptionCommand.suggestions, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - onTap: () { - if (tappedCommand == MoreOptionCommand.suggestions) return; - hideOtherPopovers(MoreOptionCommand.suggestions); - keepEditorFocusNotifier.increase(); - suggestionsPopoverController.show(); - }, - ), - ); - } - - Widget buildTextAlignSelector() { - return TextAlignActionList( - editorState: editorState, - popoverController: textAlignPopoverController, - popoverDirection: PopoverDirection.leftWithTopAligned, - showOffset: Offset(-8, 0), - onSelect: () => getIt().hideToolbar(), - highlightColor: highlightColor, - child: buildCommandItem( - MoreOptionCommand.textAlign, - rightIcon: FlowySvg(FlowySvgs.toolbar_arrow_right_m), - onTap: () { - if (tappedCommand == MoreOptionCommand.textAlign) return; - hideOtherPopovers(MoreOptionCommand.textAlign); - keepEditorFocusNotifier.increase(); - textAlignPopoverController.show(); - }, - ), - ); - } - - void hideOtherPopovers(MoreOptionCommand currentCommand) { - if (tappedCommand == currentCommand) return; - if (tappedCommand == MoreOptionCommand.font) { - fontPopoverController.close(); - fontPopoverController = PopoverController(); - } else if (tappedCommand == MoreOptionCommand.suggestions) { - suggestionsPopoverController.close(); - suggestionsPopoverController = PopoverController(); - } else if (tappedCommand == MoreOptionCommand.textAlign) { - textAlignPopoverController.close(); - textAlignPopoverController = PopoverController(); - } - tappedCommand = currentCommand; - } -} - -enum MoreOptionCommand { - suggestions(FlowySvgs.turninto_s), - link(FlowySvgs.toolbar_link_m), - textAlign( - FlowySvgs.toolbar_alignment_m, - ), - font(FlowySvgs.type_font_m), - strikethrough(FlowySvgs.type_strikethrough_m), - formula(FlowySvgs.type_formula_m); - - const MoreOptionCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case suggestions: - return LocaleKeys.document_toolbar_turnInto.tr(); - case link: - return LocaleKeys.document_toolbar_link.tr(); - case textAlign: - return LocaleKeys.button_align.tr(); - case font: - return LocaleKeys.document_toolbar_font.tr(); - case strikethrough: - return LocaleKeys.editor_strikethrough.tr(); - case formula: - return LocaleKeys.document_toolbar_equation.tr(); - } - } - - Future onExecute(EditorState editorState, BuildContext context) async { - final selection = editorState.selection!; - if (this == link) { - final nodes = editorState.getNodesInSelection(selection); - final isHref = nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[AppFlowyRichTextKeys.href] != null, - ); - }); - getIt().hideToolbar(); - if (isHref) { - getIt().call( - HoverTriggerKey(nodes.first.id, selection), - ); - } else { - final viewId = context.read()?.documentId ?? ''; - showLinkCreateMenu(context, editorState, selection, viewId); - } - } else if (this == strikethrough) { - await editorState.toggleAttribute(name); - } else if (this == formula) { - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - final transaction = editorState.transaction; - final isHighlight = isFormulaHighlight(editorState); - if (isHighlight) { - final formula = delta - .slice(selection.startIndex, selection.endIndex) - .whereType() - .firstOrNull - ?.attributes?[InlineMathEquationKeys.formula]; - assert(formula != null); - if (formula == null) { - return; - } - // clear the format - transaction.replaceText( - node, - selection.startIndex, - selection.length, - formula, - attributes: {}, - ); - } else { - final text = editorState.getTextInSelection(selection).join(); - transaction.replaceText( - node, - selection.startIndex, - selection.length, - MentionBlockKeys.mentionChar, - attributes: { - InlineMathEquationKeys.formula: text, - }, - ); - } - await editorState.apply(transaction); - } - } -} - -bool isFormulaHighlight(EditorState editorState) { - final selection = editorState.selection; - if (selection == null || selection.isCollapsed) { - return false; - } - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return false; - } - - final nodes = editorState.getNodesInSelection(selection); - return nodes.allSatisfyInSelection(selection, (delta) { - return delta.everyAttributes( - (attributes) => attributes[InlineMathEquationKeys.formula] != null, - ); - }); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart deleted file mode 100644 index 5778b6b8a4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_heading_toolbar_item.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -import 'toolbar_id_enum.dart'; - -final ToolbarItem customTextHeadingItem = ToolbarItem( - id: ToolbarId.textHeading.id, - group: 1, - isActive: onlyShowInSingleTextTypeSelectionAndExcludeTable, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return TextHeadingActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ); - }, -); - -class TextHeadingActionList extends StatefulWidget { - const TextHeadingActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - - @override - State createState() => _TextHeadingActionListState(); -} - -class _TextHeadingActionListState extends State { - final popoverController = PopoverController(); - - bool isSelected = false; - - @override - void dispose() { - super.dispose(); - popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 2.0), - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - popupBuilder: (context) => buildPopoverContent(), - child: buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyIconButton( - width: 48, - height: 32, - isSelected: isSelected, - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - icon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowySvg( - FlowySvgs.toolbar_text_format_m, - size: Size.square(20), - color: iconColor, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.textHeading.id, - LocaleKeys.document_toolbar_textSize.tr(), - child, - ) ?? - child; - } - - Widget buildPopoverContent() { - final selectingCommand = getSelectingCommand(); - return MouseRegion( - child: SeparatedColumn( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const VSpace(4.0), - children: List.generate(TextHeadingCommand.values.length, (index) { - final command = TextHeadingCommand.values[index]; - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(command.svg), - iconPadding: 12, - text: FlowyText( - command.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: selectingCommand == command - ? FlowySvg(FlowySvgs.toolbar_check_m) - : null, - onTap: () { - if (command == selectingCommand) return; - command.onExecute(widget.editorState); - popoverController.close(); - }, - ), - ); - }), - ), - ); - } - - TextHeadingCommand? getSelectingCommand() { - final editorState = widget.editorState; - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return null; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.delta == null) { - return null; - } - final nodeType = node.type; - if (nodeType == ParagraphBlockKeys.type) return TextHeadingCommand.text; - if (nodeType == HeadingBlockKeys.type) { - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level == 1) return TextHeadingCommand.h1; - if (level == 2) return TextHeadingCommand.h2; - if (level == 3) return TextHeadingCommand.h3; - } - return null; - } -} - -enum TextHeadingCommand { - text(FlowySvgs.type_text_m), - h1(FlowySvgs.type_h1_m), - h2(FlowySvgs.type_h2_m), - h3(FlowySvgs.type_h3_m); - - const TextHeadingCommand(this.svg); - - final FlowySvgData svg; - - String get title { - switch (this) { - case text: - return AppFlowyEditorL10n.current.text; - case h1: - return LocaleKeys.document_toolbar_h1.tr(); - case h2: - return LocaleKeys.document_toolbar_h2.tr(); - case h3: - return LocaleKeys.document_toolbar_h3.tr(); - } - } - - void onExecute(EditorState state) { - switch (this) { - case text: - formatNodeToText(state); - break; - case h1: - _turnInto(state, 1); - break; - case h2: - _turnInto(state, 2); - break; - case h3: - _turnInto(state, 3); - break; - } - } - - Future _turnInto(EditorState state, int level) async { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - await BlockActionOptionCubit.turnIntoBlock( - HeadingBlockKeys.type, - node, - state, - level: level, - keepSelection: true, - ); - } -} - -void formatNodeToText(EditorState editorState) { - final selection = editorState.selection!; - final node = editorState.getNodeAtPath(selection.start.path)!; - final delta = (node.delta ?? Delta()).toJson(); - editorState.formatNode( - selection, - (node) => node.copyWith( - type: ParagraphBlockKeys.type, - attributes: { - blockComponentDelta: delta, - blockComponentBackgroundColor: - node.attributes[blockComponentBackgroundColor], - blockComponentTextDirection: - node.attributes[blockComponentTextDirection], - }, - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart deleted file mode 100644 index 48f5d3f403..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/text_suggestions_toolbar_item.dart +++ /dev/null @@ -1,536 +0,0 @@ -import 'dart:collection'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide QuoteBlockComponentBuilder, quoteNode, QuoteBlockKeys; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import 'text_heading_toolbar_item.dart'; -import 'toolbar_id_enum.dart'; - -@visibleForTesting -const kSuggestionsItemKey = ValueKey('SuggestionsItem'); - -@visibleForTesting -const kSuggestionsItemListKey = ValueKey('SuggestionsItemList'); - -final ToolbarItem suggestionsItem = ToolbarItem( - id: ToolbarId.suggestions.id, - group: 3, - isActive: enableSuggestions, - builder: ( - context, - editorState, - highlightColor, - iconColor, - tooltipBuilder, - ) { - return SuggestionsActionList( - editorState: editorState, - tooltipBuilder: tooltipBuilder, - ); - }, -); - -class SuggestionsActionList extends StatefulWidget { - const SuggestionsActionList({ - super.key, - required this.editorState, - this.tooltipBuilder, - this.child, - this.onSelect, - this.popoverController, - this.popoverDirection = PopoverDirection.bottomWithLeftAligned, - this.showOffset = const Offset(0, 2), - }); - - final EditorState editorState; - final ToolbarTooltipBuilder? tooltipBuilder; - final Widget? child; - final VoidCallback? onSelect; - final PopoverController? popoverController; - final PopoverDirection popoverDirection; - final Offset showOffset; - - @override - State createState() => _SuggestionsActionListState(); -} - -class _SuggestionsActionListState extends State { - late PopoverController popoverController = - widget.popoverController ?? PopoverController(); - - bool isSelected = false; - - final List suggestionItems = suggestions.sublist(0, 4); - final List turnIntoItems = - suggestions.sublist(4, suggestions.length); - - EditorState get editorState => widget.editorState; - - SuggestionItem currentSuggestionItem = textSuggestionItem; - - @override - void initState() { - super.initState(); - refreshSuggestions(); - editorState.selectionNotifier.addListener(refreshSuggestions); - } - - @override - void dispose() { - editorState.selectionNotifier.removeListener(refreshSuggestions); - popoverController.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - controller: popoverController, - direction: widget.popoverDirection, - offset: widget.showOffset, - onOpen: () => keepEditorFocusNotifier.increase(), - onClose: () { - setState(() { - isSelected = false; - }); - keepEditorFocusNotifier.decrease(); - }, - constraints: const BoxConstraints(maxWidth: 240, maxHeight: 400), - popupBuilder: (context) => buildPopoverContent(context), - child: widget.child ?? buildChild(context), - ); - } - - void showPopover() { - keepEditorFocusNotifier.increase(); - popoverController.show(); - } - - Widget buildChild(BuildContext context) { - final theme = AppFlowyTheme.of(context), - iconColor = theme.iconColorScheme.primary; - final child = FlowyHover( - isSelected: () => isSelected, - style: HoverStyle( - hoverColor: EditorStyleCustomizer.toolbarHoverColor(context), - foregroundColorOnHover: Theme.of(context).iconTheme.color, - ), - resetHoverOnRebuild: false, - child: FlowyTooltip( - preferBelow: true, - child: RawMaterialButton( - key: kSuggestionsItemKey, - constraints: BoxConstraints(maxHeight: 32, minWidth: 60), - clipBehavior: Clip.antiAlias, - hoverElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder(borderRadius: Corners.s6Border), - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, - onPressed: () { - setState(() { - isSelected = true; - }); - showPopover(); - }, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - currentSuggestionItem.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - HSpace(4), - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - size: Size(12, 20), - color: iconColor, - ), - ], - ), - ), - ), - ), - ); - - return widget.tooltipBuilder?.call( - context, - ToolbarId.suggestions.id, - currentSuggestionItem.title, - child, - ) ?? - child; - } - - Widget buildPopoverContent(BuildContext context) { - final textColor = Color(0xff99A1A8); - return MouseRegion( - child: SingleChildScrollView( - key: kSuggestionsItemListKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - buildSubTitle( - LocaleKeys.document_toolbar_suggestions.tr(), - textColor, - ), - ...List.generate(suggestionItems.length, (index) { - return buildItem(suggestionItems[index]); - }), - buildSubTitle(LocaleKeys.document_toolbar_turnInto.tr(), textColor), - ...List.generate(turnIntoItems.length, (index) { - return buildItem(turnIntoItems[index]); - }), - ], - ), - ), - ); - } - - Widget buildItem(SuggestionItem item) { - final isSelected = item.type == currentSuggestionItem.type; - return SizedBox( - height: 36, - child: FlowyButton( - leftIconSize: const Size.square(20), - leftIcon: FlowySvg(item.svg), - iconPadding: 12, - text: FlowyText( - item.title, - fontWeight: FontWeight.w400, - figmaLineHeight: 20, - ), - rightIcon: isSelected ? FlowySvg(FlowySvgs.toolbar_check_m) : null, - onTap: () { - item.onTap(widget.editorState, true); - widget.onSelect?.call(); - popoverController.close(); - }, - ), - ); - } - - Widget buildSubTitle(String text, Color color) { - return Container( - height: 32, - margin: EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment.centerLeft, - child: FlowyText.semibold( - text, - color: color, - figmaLineHeight: 16, - ), - ), - ); - } - - void refreshSuggestions() { - final selection = editorState.selection; - if (selection == null || !selection.isSingle) { - return; - } - final node = editorState.getNodeAtPath(selection.start.path); - if (node == null || node.delta == null) { - return; - } - final nodeType = node.type; - SuggestionType? suggestionType; - if (nodeType == HeadingBlockKeys.type) { - final level = node.attributes[HeadingBlockKeys.level] ?? 1; - if (level == 1) { - suggestionType = SuggestionType.h1; - } else if (level == 2) { - suggestionType = SuggestionType.h2; - } else if (level == 3) { - suggestionType = SuggestionType.h3; - } - } else if (nodeType == ToggleListBlockKeys.type) { - final level = node.attributes[ToggleListBlockKeys.level]; - if (level == null) { - suggestionType = SuggestionType.toggle; - } else if (level == 1) { - suggestionType = SuggestionType.toggleH1; - } else if (level == 2) { - suggestionType = SuggestionType.toggleH2; - } else if (level == 3) { - suggestionType = SuggestionType.toggleH3; - } - } else { - suggestionType = nodeType2SuggestionType[nodeType]; - } - if (suggestionType == null) return; - suggestionItems.clear(); - turnIntoItems.clear(); - for (final item in suggestions) { - if (item.type.group == suggestionType.group && - item.type != suggestionType) { - suggestionItems.add(item); - } else { - turnIntoItems.add(item); - } - } - currentSuggestionItem = - suggestions.where((item) => item.type == suggestionType).first; - if (mounted) setState(() {}); - } -} - -class SuggestionItem { - SuggestionItem({ - required this.type, - required this.title, - required this.svg, - required this.onTap, - }); - - final SuggestionType type; - final String title; - final FlowySvgData svg; - final Function(EditorState state, bool keepSelection) onTap; -} - -enum SuggestionGroup { textHeading, list, toggle, quote, page } - -enum SuggestionType { - text(SuggestionGroup.textHeading), - h1(SuggestionGroup.textHeading), - h2(SuggestionGroup.textHeading), - h3(SuggestionGroup.textHeading), - checkbox(SuggestionGroup.list), - bulleted(SuggestionGroup.list), - numbered(SuggestionGroup.list), - toggle(SuggestionGroup.toggle), - toggleH1(SuggestionGroup.toggle), - toggleH2(SuggestionGroup.toggle), - toggleH3(SuggestionGroup.toggle), - callOut(SuggestionGroup.quote), - quote(SuggestionGroup.quote), - page(SuggestionGroup.page); - - const SuggestionType(this.group); - - final SuggestionGroup group; -} - -final textSuggestionItem = SuggestionItem( - type: SuggestionType.text, - title: AppFlowyEditorL10n.current.text, - svg: FlowySvgs.type_text_m, - onTap: (state, _) => formatNodeToText(state), -); - -final h1SuggestionItem = SuggestionItem( - type: SuggestionType.h1, - title: LocaleKeys.document_toolbar_h1.tr(), - svg: FlowySvgs.type_h1_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 1, - keepSelection: keepSelection, - ), -); - -final h2SuggestionItem = SuggestionItem( - type: SuggestionType.h2, - title: LocaleKeys.document_toolbar_h2.tr(), - svg: FlowySvgs.type_h2_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 2, - keepSelection: keepSelection, - ), -); - -final h3SuggestionItem = SuggestionItem( - type: SuggestionType.h3, - title: LocaleKeys.document_toolbar_h3.tr(), - svg: FlowySvgs.type_h3_m, - onTap: (state, keepSelection) => _turnInto( - state, - HeadingBlockKeys.type, - level: 3, - keepSelection: keepSelection, - ), -); - -final checkboxSuggestionItem = SuggestionItem( - type: SuggestionType.checkbox, - title: LocaleKeys.editor_checkbox.tr(), - svg: FlowySvgs.type_todo_m, - onTap: (state, keepSelection) => _turnInto( - state, - TodoListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final bulletedSuggestionItem = SuggestionItem( - type: SuggestionType.bulleted, - title: LocaleKeys.editor_bulletedListShortForm.tr(), - svg: FlowySvgs.type_bulleted_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - BulletedListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final numberedSuggestionItem = SuggestionItem( - type: SuggestionType.numbered, - title: LocaleKeys.editor_numberedListShortForm.tr(), - svg: FlowySvgs.type_numbered_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - NumberedListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final toggleSuggestionItem = SuggestionItem( - type: SuggestionType.toggle, - title: LocaleKeys.editor_toggleListShortForm.tr(), - svg: FlowySvgs.type_toggle_list_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final toggleH1SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH1, - title: LocaleKeys.editor_toggleHeading1ShortForm.tr(), - svg: FlowySvgs.type_toggle_h1_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 1, - keepSelection: keepSelection, - ), -); - -final toggleH2SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH2, - title: LocaleKeys.editor_toggleHeading2ShortForm.tr(), - svg: FlowySvgs.type_toggle_h2_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 2, - keepSelection: keepSelection, - ), -); - -final toggleH3SuggestionItem = SuggestionItem( - type: SuggestionType.toggleH3, - title: LocaleKeys.editor_toggleHeading3ShortForm.tr(), - svg: FlowySvgs.type_toggle_h3_m, - onTap: (state, keepSelection) => _turnInto( - state, - ToggleListBlockKeys.type, - level: 3, - keepSelection: keepSelection, - ), -); - -final callOutSuggestionItem = SuggestionItem( - type: SuggestionType.callOut, - title: LocaleKeys.document_plugins_callout.tr(), - svg: FlowySvgs.type_callout_m, - onTap: (state, keepSelection) => _turnInto( - state, - CalloutBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final quoteSuggestionItem = SuggestionItem( - type: SuggestionType.quote, - title: LocaleKeys.editor_quote.tr(), - svg: FlowySvgs.type_quote_m, - onTap: (state, keepSelection) => _turnInto( - state, - QuoteBlockKeys.type, - keepSelection: keepSelection, - ), -); - -final pateItem = SuggestionItem( - type: SuggestionType.page, - title: LocaleKeys.editor_page.tr(), - svg: FlowySvgs.icon_document_s, - onTap: (state, keepSelection) => _turnInto( - state, - SubPageBlockKeys.type, - viewId: getIt().latestOpenView?.id, - keepSelection: keepSelection, - ), -); - -Future _turnInto( - EditorState state, - String type, { - int? level, - String? viewId, - bool keepSelection = true, -}) async { - final selection = state.selection!; - final node = state.getNodeAtPath(selection.start.path)!; - await BlockActionOptionCubit.turnIntoBlock( - type, - node, - state, - level: level, - currentViewId: viewId, - keepSelection: keepSelection, - ); -} - -final suggestions = UnmodifiableListView([ - textSuggestionItem, - h1SuggestionItem, - h2SuggestionItem, - h3SuggestionItem, - checkboxSuggestionItem, - bulletedSuggestionItem, - numberedSuggestionItem, - toggleSuggestionItem, - toggleH1SuggestionItem, - toggleH2SuggestionItem, - toggleH3SuggestionItem, - callOutSuggestionItem, - quoteSuggestionItem, - pateItem, -]); - -final nodeType2SuggestionType = UnmodifiableMapView({ - ParagraphBlockKeys.type: SuggestionType.text, - NumberedListBlockKeys.type: SuggestionType.numbered, - BulletedListBlockKeys.type: SuggestionType.bulleted, - QuoteBlockKeys.type: SuggestionType.quote, - TodoListBlockKeys.type: SuggestionType.checkbox, - CalloutBlockKeys.type: SuggestionType.callOut, -}); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart deleted file mode 100644 index 8a97bb6648..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/toolbar_item/toolbar_id_enum.dart +++ /dev/null @@ -1,19 +0,0 @@ -enum ToolbarId { - bold, - underline, - italic, - code, - highlightColor, - textColor, - link, - placeholder, - paddingPlaceHolder, - textAlign, - moreOption, - textHeading, - suggestions, -} - -extension ToolbarIdExtension on ToolbarId { - String get id => 'editor.$name'; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart deleted file mode 100644 index 34b0bdc8f9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/block_transaction_handler.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// A handler for transactions that involve a Block Component. -/// -/// This is a subclass of [EditorTransactionHandler] that is used for block components. -/// Specifically this transaction handler only needs to concern itself with changes to -/// a [Node], and doesn't care about text deltas. -/// -abstract class BlockTransactionHandler extends EditorTransactionHandler { - const BlockTransactionHandler({required super.type}) - : super(livesInDelta: false); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart deleted file mode 100644 index 243532e8ce..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// A handler for transactions that involve a Block Component. -/// The [T] type is the type of data that this transaction handler takes. -/// -/// In case of a block component, the [T] type should be a [Node]. -/// In case of a mention component, the [T] type should be a [Map]. -/// -abstract class EditorTransactionHandler { - const EditorTransactionHandler({ - required this.type, - this.livesInDelta = false, - }); - - /// The type of the block/mention that this handler is built for. - /// It's used to determine whether to call any of the handlers on certain transactions. - /// - final String type; - - /// If the block is a "mention" type, it lives inside the [Delta] of a [Node]. - /// - final bool livesInDelta; - - Future onTransaction( - BuildContext context, - String viewId, - EditorState editorState, - List added, - List removed, { - bool isCut = false, - bool isUndoRedo = false, - bool isPaste = false, - bool isDraggingNode = false, - bool isTurnInto = false, - String? parentViewId, - }); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart deleted file mode 100644 index b56066ae8b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_service.dart +++ /dev/null @@ -1,331 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/child_page_transaction_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/date_transaction_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_transaction_handler.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; -import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'mention_transaction_handler.dart'; - -final _transactionHandlers = [ - if (FeatureFlag.inlineSubPageMention.isOn) ...[ - SubPageTransactionHandler(), - ChildPageTransactionHandler(), - ], - DateTransactionHandler(), -]; - -/// Handles delegating transactions to appropriate handlers. -/// -/// Such as the [ChildPageTransactionHandler] for inline child pages. -/// -class EditorTransactionService extends StatefulWidget { - const EditorTransactionService({ - super.key, - required this.viewId, - required this.editorState, - required this.child, - }); - - final String viewId; - final EditorState editorState; - final Widget child; - - @override - State createState() => - _EditorTransactionServiceState(); -} - -class _EditorTransactionServiceState extends State { - StreamSubscription? transactionSubscription; - - bool isUndoRedo = false; - bool isPaste = false; - bool isDraggingNode = false; - bool isTurnInto = false; - - @override - void initState() { - super.initState(); - transactionSubscription = - widget.editorState.transactionStream.listen(onEditorTransaction); - EditorNotification.addListener(onEditorNotification); - } - - @override - void dispose() { - EditorNotification.removeListener(onEditorNotification); - transactionSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } - - void onEditorNotification(EditorNotificationType type) { - if ([EditorNotificationType.undo, EditorNotificationType.redo] - .contains(type)) { - isUndoRedo = true; - } else if (type == EditorNotificationType.paste) { - isPaste = true; - } else if (type == EditorNotificationType.dragStart) { - isDraggingNode = true; - } else if (type == EditorNotificationType.dragEnd) { - isDraggingNode = false; - } else if (type == EditorNotificationType.turnInto) { - isTurnInto = true; - } - - if (type == EditorNotificationType.undo) { - undoCommand.execute(widget.editorState); - } else if (type == EditorNotificationType.redo) { - redoCommand.execute(widget.editorState); - } else if (type == EditorNotificationType.exitEditing && - widget.editorState.selection != null) { - // If the editor is disposed, we don't need to reset the selection. - if (!widget.editorState.isDisposed) { - widget.editorState.selection = null; - } - } - } - - /// Collects all nodes of a certain type, including those that are nested. - /// - List collectMatchingNodes( - Node node, - String type, { - bool livesInDelta = false, - }) { - final List matchingNodes = []; - if (node.type == type) { - matchingNodes.add(node); - } - - if (livesInDelta && node.attributes[blockComponentDelta] != null) { - final deltas = node.attributes[blockComponentDelta]; - if (deltas is List) { - for (final delta in deltas) { - if (delta['attributes'] != null && - delta['attributes'][type] != null) { - matchingNodes.add(node); - } - } - } - } - - for (final child in node.children) { - matchingNodes.addAll( - collectMatchingNodes( - child, - type, - livesInDelta: livesInDelta, - ), - ); - } - - return matchingNodes; - } - - void onEditorTransaction(EditorTransactionValue event) { - final time = event.$1; - final transaction = event.$2; - - if (time == TransactionTime.before) { - return; - } - - final Map added = { - for (final handler in _transactionHandlers) - handler.type: handler.livesInDelta ? [] : [], - }; - final Map removed = { - for (final handler in _transactionHandlers) - handler.type: handler.livesInDelta ? [] : [], - }; - - // based on the type of the transaction handler - final uniqueTransactionHandlers = {}; - for (final handler in _transactionHandlers) { - uniqueTransactionHandlers.putIfAbsent(handler.type, () => handler); - } - - for (final op in transaction.operations) { - if (op is InsertOperation) { - for (final n in op.nodes) { - for (final handler in uniqueTransactionHandlers.values) { - if (handler.livesInDelta) { - added[handler.type]! - .addAll(extractMentionsForType(n, handler.type)); - } else { - added[handler.type]! - .addAll(collectMatchingNodes(n, handler.type)); - } - } - } - } else if (op is DeleteOperation) { - for (final n in op.nodes) { - for (final handler in uniqueTransactionHandlers.values) { - if (handler.livesInDelta) { - removed[handler.type]!.addAll( - extractMentionsForType(n, handler.type, false), - ); - } else { - removed[handler.type]! - .addAll(collectMatchingNodes(n, handler.type)); - } - } - } - } else if (op is UpdateOperation) { - final node = widget.editorState.getNodeAtPath(op.path); - if (node == null) { - continue; - } - - if (op.attributes[blockComponentDelta] is! List || - op.oldAttributes[blockComponentDelta] is! List) { - continue; - } - - final deltaBefore = - Delta.fromJson(op.oldAttributes[blockComponentDelta]); - final deltaAfter = Delta.fromJson(op.attributes[blockComponentDelta]); - - final (add, del) = diffDeltas(deltaBefore, deltaAfter); - - bool fetchedMentions = false; - for (final handler in _transactionHandlers) { - if (!handler.livesInDelta || fetchedMentions) { - continue; - } - - if (add.isNotEmpty) { - final mentionBlockDatas = - getMentionBlockData(handler.type, node, add); - - added[handler.type]!.addAll(mentionBlockDatas); - } - - if (del.isNotEmpty) { - final mentionBlockDatas = getMentionBlockData( - handler.type, - node, - del, - ); - - removed[handler.type]!.addAll(mentionBlockDatas); - } - - fetchedMentions = true; - } - } - } - - for (final handler in _transactionHandlers) { - final additions = added[handler.type] ?? []; - final removals = removed[handler.type] ?? []; - - if (additions.isEmpty && removals.isEmpty) { - continue; - } - - handler.onTransaction( - context, - widget.viewId, - widget.editorState, - additions, - removals, - isCut: context.read().isCut, - isUndoRedo: isUndoRedo, - isPaste: isPaste, - isDraggingNode: isDraggingNode, - isTurnInto: isTurnInto, - parentViewId: widget.viewId, - ); - } - - isUndoRedo = false; - isPaste = false; - isTurnInto = false; - } - - /// Takes an iterable of [TextInsert] and returns a list of [MentionBlockData]. - /// This is used to extract mentions from a list of text inserts, of a certain type. - List getMentionBlockData( - String type, - Node node, - Iterable textInserts, - ) { - // Additions contain all the text inserts that were added in this - // transaction, we only care about the ones that fit the handlers type. - - // Filter out the text inserts where the attribute for the handler type is present. - final relevantTextInserts = - textInserts.where((ti) => ti.attributes?[type] != null); - - // Map it to a list of MentionBlockData. - final mentionBlockDatas = relevantTextInserts.map((ti) { - // For some text inserts (mostly additions), we might need to modify them after the transaction, - // so we pass the index of the delta to the handler. - final index = node.delta?.toList().indexOf(ti) ?? -1; - return (node, ti.attributes![type], index); - }).toList(); - - return mentionBlockDatas; - } - - List extractMentionsForType( - Node node, - String mentionType, [ - bool includeIndex = true, - ]) { - final changes = []; - - final nodesWithDelta = collectMatchingNodes( - node, - mentionType, - livesInDelta: true, - ); - - for (final paragraphNode in nodesWithDelta) { - final textInserts = paragraphNode.attributes[blockComponentDelta]; - if (textInserts == null || textInserts is! List || textInserts.isEmpty) { - continue; - } - - for (final (index, textInsert) in textInserts.indexed) { - if (textInsert['attributes'] != null && - textInsert['attributes'][mentionType] != null) { - changes.add( - ( - paragraphNode, - textInsert['attributes'][mentionType], - includeIndex ? index : -1, - ), - ); - } - } - } - - return changes; - } - - (Iterable, Iterable) diffDeltas( - Delta before, - Delta after, - ) { - final diff = before.diff(after); - final inverted = diff.invert(before); - final del = inverted.whereType(); - final add = diff.whereType(); - - return (add, del); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/mention_transaction_handler.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/mention_transaction_handler.dart deleted file mode 100644 index d08ce05510..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/transaction_handler/mention_transaction_handler.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/transaction_handler/editor_transaction_handler.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -/// The data used to handle transactions for mentions. -/// -/// [Node] is the block node. -/// [Map] is the data of the mention block. -/// [int] is the index of the mention block in the list of deltas (after transaction apply). -/// -typedef MentionBlockData = (Node, Map, int); - -abstract class MentionTransactionHandler - extends EditorTransactionHandler { - const MentionTransactionHandler() - : super(type: MentionBlockKeys.mention, livesInDelta: true); -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart deleted file mode 100644 index 36ea3d2704..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -/// Undo -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customUndoCommand = CommandShortcutEvent( - key: 'undo', - getDescription: () => AppFlowyEditorL10n.current.cmdUndo, - command: 'ctrl+z', - macOSCommand: 'cmd+z', - handler: (editorState) { - final context = editorState.document.root.context; - if (context == null) { - return KeyEventResult.ignored; - } - final editorContext = context.read(); - if (editorContext.coverTitleFocusNode.hasFocus) { - return KeyEventResult.ignored; - } - - EditorNotification.undo().post(); - return KeyEventResult.handled; - }, -); - -/// Redo -/// -/// - support -/// - desktop -/// - web -/// -final CommandShortcutEvent customRedoCommand = CommandShortcutEvent( - key: 'redo', - getDescription: () => AppFlowyEditorL10n.current.cmdRedo, - command: 'ctrl+y,ctrl+shift+z', - macOSCommand: 'cmd+shift+z', - handler: (editorState) { - final context = editorState.document.root.context; - if (context == null) { - return KeyEventResult.ignored; - } - final editorContext = context.read(); - if (editorContext.coverTitleFocusNode.hasFocus) { - return KeyEventResult.ignored; - } - - EditorNotification.redo().post(); - return KeyEventResult.handled; - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart deleted file mode 100644 index f41d4526ea..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/video/video_block_component.dart +++ /dev/null @@ -1,6 +0,0 @@ -class VideoBlockKeys { - const VideoBlockKeys._(); - - static const String type = 'video'; - static const String url = 'url'; -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index cd9d7bb5e8..c691e7d821 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -1,82 +1,44 @@ -import 'dart:io'; - import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/font_colors.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/util/font_family_extension.dart'; -import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'editor_plugins/toolbar_item/more_option_toolbar_item.dart'; class EditorStyleCustomizer { - EditorStyleCustomizer({ - required this.context, - required this.padding, - this.width, - this.editorState, - }); + EditorStyleCustomizer({required this.context, required this.padding}); final BuildContext context; final EdgeInsets padding; - final double? width; - final EditorState? editorState; - - static const double maxDocumentWidth = 480 * 4; - static const double minDocumentWidth = 480; - - static EdgeInsets get documentPadding => UniversalPlatform.isMobile - ? EdgeInsets.zero - : EdgeInsets.only( - left: 40, - right: 40 + EditorStyleCustomizer.optionMenuWidth, - ); - - static double get nodeHorizontalPadding => - UniversalPlatform.isMobile ? 24 : 0; - - static EdgeInsets get documentPaddingWithOptionMenu => - documentPadding + EdgeInsets.only(left: optionMenuWidth); - - static double get optionMenuWidth => UniversalPlatform.isMobile ? 0 : 44; - - static Color? toolbarHoverColor(BuildContext context) { - return Theme.of(context).brightness == Brightness.dark - ? Theme.of(context).colorScheme.secondary - : AFThemeExtension.of(context).toolbarHoverColor; - } EditorStyle style() { - if (UniversalPlatform.isDesktopOrWeb) { + if (PlatformExtension.isDesktopOrWeb) { return desktop(); - } else if (UniversalPlatform.isMobile) { + } else if (PlatformExtension.isMobile) { return mobile(); } throw UnimplementedError(); } + static EdgeInsets get documentPadding => PlatformExtension.isMobile + ? const EdgeInsets.only(left: 24, right: 24) + : const EdgeInsets.only(left: 40, right: 40 + 44); + EditorStyle desktop() { final theme = Theme.of(context); final afThemeExtension = AFThemeExtension.of(context); @@ -88,15 +50,10 @@ class EditorStyleCustomizer { fontFamily = appearanceFont; } - final cursorColor = (editorState?.editable ?? true) - ? (appearance.cursorColor ?? - DefaultAppearanceSettings.getDefaultCursorColor(context)) - : Colors.transparent; - return EditorStyle.desktop( padding: padding, - maxWidth: width, - cursorColor: cursorColor, + cursorColor: appearance.cursorColor ?? + DefaultAppearanceSettings.getDefaultCursorColor(context), selectionColor: appearance.selectionColor ?? DefaultAppearanceSettings.getDefaultSelectionColor(context), defaultTextDirection: appearance.defaultTextDirection, @@ -127,15 +84,13 @@ class EditorStyleCustomizer { fontSize: fontSize, fontWeight: FontWeight.normal, color: Colors.red, - backgroundColor: - theme.colorScheme.inverseSurface.withValues(alpha: 0.8), + backgroundColor: theme.colorScheme.inverseSurface.withOpacity(0.8), ), ), ), textSpanDecorator: customizeAttributeDecorator, textScaleFactor: context.watch().state.textScaleFactor, - textSpanOverlayBuilder: _buildTextSpanOverlay, ); } @@ -145,8 +100,7 @@ class EditorStyleCustomizer { final theme = Theme.of(context); final fontSize = pageStyle.fontLayout.fontSize; final lineHeight = pageStyle.lineHeightLayout.lineHeight; - final fontFamily = pageStyle.fontFamily ?? - context.read().state.font; + final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; final defaultTextDirection = context.read().state.defaultTextDirection; final textScaleFactor = @@ -176,18 +130,18 @@ class EditorStyleCustomizer { textStyle: baseTextStyle.copyWith( fontSize: fontSize, fontWeight: FontWeight.normal, + fontStyle: FontStyle.italic, color: Colors.red, - backgroundColor: Colors.grey.withValues(alpha: 0.3), + backgroundColor: Colors.grey.withOpacity(0.3), ), ), applyHeightToFirstAscent: true, applyHeightToLastDescent: true, ), textSpanDecorator: customizeAttributeDecorator, + mobileDragHandleBallSize: const Size.square(12.0), magnifierSize: const Size(144, 96), textScaleFactor: textScaleFactor, - mobileDragHandleLeftExtend: 12.0, - mobileDragHandleWidthExtend: 24.0, ); } @@ -195,7 +149,9 @@ class EditorStyleCustomizer { final String? fontFamily; final List fontSizes; final double fontSize; - if (UniversalPlatform.isMobile) { + final FontWeight fontWeight = + level <= 2 ? FontWeight.w700 : FontWeight.w600; + if (PlatformExtension.isMobile) { final state = context.read().state; fontFamily = state.fontFamily; fontSize = state.fontLayout.fontSize; @@ -212,45 +168,28 @@ class EditorStyleCustomizer { fontSize, ]; } - return baseTextStyle(fontFamily, fontWeight: FontWeight.w600).copyWith( + return baseTextStyle(fontFamily, fontWeight: fontWeight).copyWith( fontSize: fontSizes.elementAtOrNull(level - 1) ?? fontSize, ); } - CodeBlockStyle codeBlockStyleBuilder() { + TextStyle codeBlockStyleBuilder() { final fontSize = context.read().state.fontSize; final fontFamily = context.read().state.codeFontFamily; - - return CodeBlockStyle( - textStyle: baseTextStyle(fontFamily).copyWith( - fontSize: fontSize, - height: 1.5, - color: AFThemeExtension.of(context).onBackground, - ), - backgroundColor: AFThemeExtension.of(context).calloutBGColor, - foregroundColor: AFThemeExtension.of(context).textColor.withAlpha(155), + return baseTextStyle(fontFamily).copyWith( + fontSize: fontSize, + height: 1.5, + color: AFThemeExtension.of(context).onBackground, ); } TextStyle calloutBlockStyleBuilder() { - if (UniversalPlatform.isMobile) { - final afThemeExtension = AFThemeExtension.of(context); - final pageStyle = context.read().state; - final fontSize = pageStyle.fontLayout.fontSize; - final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; - final baseTextStyle = this.baseTextStyle(fontFamily); - return baseTextStyle.copyWith( - fontSize: fontSize, - color: afThemeExtension.onBackground, - ); - } else { - final fontSize = context.read().state.fontSize; - return baseTextStyle(null).copyWith( - fontSize: fontSize, - height: 1.5, - ); - } + final fontSize = context.read().state.fontSize; + return baseTextStyle(null).copyWith( + fontSize: fontSize, + height: 1.5, + ); } TextStyle outlineBlockPlaceholderStyleBuilder() { @@ -259,28 +198,10 @@ class EditorStyleCustomizer { fontFamily: defaultFontFamily, fontSize: fontSize, height: 1.5, - color: AFThemeExtension.of(context).onBackground.withValues(alpha: 0.6), + color: AFThemeExtension.of(context).onBackground.withOpacity(0.6), ); } - TextStyle subPageBlockTextStyleBuilder() { - if (UniversalPlatform.isMobile) { - final pageStyle = context.read().state; - final fontSize = pageStyle.fontLayout.fontSize; - final fontFamily = pageStyle.fontFamily ?? defaultFontFamily; - final baseTextStyle = this.baseTextStyle(fontFamily); - return baseTextStyle.copyWith( - fontSize: fontSize, - ); - } else { - final fontSize = context.read().state.fontSize; - return baseTextStyle(null).copyWith( - fontSize: fontSize, - height: 1.5, - ); - } - } - SelectionMenuStyle selectionMenuStyleBuilder() { final theme = Theme.of(context); final afThemeExtension = AFThemeExtension.of(context); @@ -291,15 +212,6 @@ class EditorStyleCustomizer { selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface, selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface, selectionMenuItemSelectedColor: afThemeExtension.greyHover, - selectionMenuUnselectedLabelColor: afThemeExtension.onBackground, - selectionMenuDividerColor: afThemeExtension.greyHover, - selectionMenuLinkBorderColor: afThemeExtension.greyHover, - selectionMenuInvalidLinkColor: afThemeExtension.onBackground, - selectionMenuButtonColor: afThemeExtension.greyHover, - selectionMenuButtonTextColor: afThemeExtension.onBackground, - selectionMenuButtonIconColor: afThemeExtension.onBackground, - selectionMenuButtonBorderColor: afThemeExtension.greyHover, - selectionMenuTabIndicatorColor: afThemeExtension.greyHover, ); } @@ -308,20 +220,21 @@ class EditorStyleCustomizer { final afThemeExtension = AFThemeExtension.of(context); return InlineActionsMenuStyle( backgroundColor: theme.cardColor, - groupTextColor: afThemeExtension.onBackground.withValues(alpha: .8), + groupTextColor: afThemeExtension.onBackground.withOpacity(.8), menuItemTextColor: afThemeExtension.onBackground, menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, ); } - TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { - if (fontFamily == null) { - return TextStyle(fontWeight: fontWeight); - } else if (fontFamily == defaultFontFamily) { - return TextStyle(fontFamily: fontFamily, fontWeight: fontWeight); - } + FloatingToolbarStyle floatingToolbarStyleBuilder() => FloatingToolbarStyle( + backgroundColor: Theme.of(context).colorScheme.onTertiary, + ); + TextStyle baseTextStyle(String? fontFamily, {FontWeight? fontWeight}) { + if (fontFamily == null || fontFamily == defaultFontFamily) { + return TextStyle(fontWeight: fontWeight); + } try { return getGoogleFontSafely(fontFamily, fontWeight: fontWeight); } on Exception { @@ -346,11 +259,6 @@ class EditorStyleCustomizer { return before; } - final suggestion = attributes[AiWriterBlockKeys.suggestion] as String?; - final newStyle = suggestion == null - ? after.style - : _styleSuggestion(after.style, suggestion); - if (attributes.backgroundColor != null) { final color = EditorFontColors.fromBuiltInColors( context, @@ -359,7 +267,7 @@ class EditorStyleCustomizer { if (color != null) { return TextSpan( text: before.text, - style: newStyle?.merge( + style: after.style?.merge( TextStyle(backgroundColor: color), ), ); @@ -374,7 +282,7 @@ class EditorStyleCustomizer { } else { return TextSpan( text: before.text, - style: newStyle?.merge( + style: after.style?.merge( getGoogleFontSafely(attributes.fontFamily!), ), ); @@ -391,7 +299,7 @@ class EditorStyleCustomizer { final type = mention[MentionBlockKeys.type]; return WidgetSpan( alignment: PlaceholderAlignment.middle, - style: newStyle, + style: after.style, child: MentionBlock( key: ValueKey( switch (type) { @@ -403,7 +311,7 @@ class EditorStyleCustomizer { node: node, index: index, mention: mention, - textStyle: newStyle, + textStyle: after.style, ), ); } @@ -412,20 +320,19 @@ class EditorStyleCustomizer { final formula = attributes[InlineMathEquationKeys.formula]; if (formula is String) { return WidgetSpan( - style: after.style, alignment: PlaceholderAlignment.middle, child: InlineMathEquation( node: node, index: index, formula: formula, - textStyle: after.style ?? style().textStyleConfiguration.text, + textStyle: style().textStyleConfiguration.text, ), ); } // customize the link on mobile final href = attributes[AppFlowyRichTextKeys.href] as String?; - if (UniversalPlatform.isMobile && href != null) { + if (PlatformExtension.isMobile && href != null) { return TextSpan( style: before.style, text: text.text, @@ -466,175 +373,13 @@ class EditorStyleCustomizer { ); } - if (suggestion != null) { - return TextSpan( - text: before.text, - style: newStyle, - ); - } - - if (href != null) { - return TextSpan( - style: before.style, - text: text.text, - mouseCursor: SystemMouseCursors.click, - ); - } else { - return before; - } - } - - Widget buildToolbarItemTooltip( - BuildContext context, - String id, - String message, - Widget child, - ) { - final tooltipMessage = _buildTooltipMessage(id, message); - child = FlowyTooltip( - richMessage: tooltipMessage, - preferBelow: false, - verticalOffset: 24, - child: child, + return defaultTextSpanDecoratorForAttribute( + context, + node, + index, + text, + before, + after, ); - - // the align/font toolbar item doesn't need the hover effect - final toolbarItemsWithoutHover = { - kFontToolbarItemId, - kAlignToolbarItemId, - }; - - if (!toolbarItemsWithoutHover.contains(id)) { - child = Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: FlowyHover( - style: HoverStyle( - hoverColor: Colors.grey.withValues(alpha: 0.3), - ), - child: child, - ), - ); - } - - return child; - } - - TextSpan _buildTooltipMessage(String id, String message) { - final markdownItemTooltips = { - 'underline': (LocaleKeys.toolbar_underline.tr(), 'U'), - 'bold': (LocaleKeys.toolbar_bold.tr(), 'B'), - 'italic': (LocaleKeys.toolbar_italic.tr(), 'I'), - 'strikethrough': (LocaleKeys.toolbar_strike.tr(), 'Shift+S'), - 'code': (LocaleKeys.toolbar_inlineCode.tr(), 'E'), - 'editor.inline_math_equation': ( - LocaleKeys.document_plugins_createInlineMathEquation.tr(), - 'Shift+E' - ), - }; - - final markdownItemIds = markdownItemTooltips.keys.toSet(); - // the items without shortcuts - if (!markdownItemIds.contains(id)) { - return TextSpan( - text: message, - style: context.tooltipTextStyle(), - ); - } - - final tooltip = markdownItemTooltips[id]; - if (tooltip == null) { - return TextSpan( - text: message, - style: context.tooltipTextStyle(), - ); - } - - final textSpan = TextSpan( - children: [ - TextSpan( - text: '${tooltip.$1}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: (Platform.isMacOS ? '⌘+' : 'Ctrl+') + tooltip.$2, - style: context.tooltipTextStyle()?.copyWith( - color: Theme.of(context).hintColor, - ), - ), - ], - ); - - return textSpan; - } - - TextStyle? _styleSuggestion(TextStyle? style, String suggestion) { - if (style == null) { - return null; - } - final isLight = Theme.of(context).isLightMode; - final textColor = isLight ? Color(0xFF007296) : Color(0xFF49CFF4); - final underlineColor = isLight ? Color(0x33005A7A) : Color(0x3349CFF4); - return switch (suggestion) { - AiWriterBlockKeys.suggestionOriginal => style.copyWith( - color: Theme.of(context).disabledColor, - decoration: TextDecoration.lineThrough, - ), - AiWriterBlockKeys.suggestionReplacement => style.copyWith( - color: textColor, - decoration: TextDecoration.underline, - decorationColor: underlineColor, - decorationThickness: 1.0, - ), - _ => style, - }; - } - - List _buildTextSpanOverlay( - BuildContext context, - Node node, - SelectableMixin delegate, - ) { - if (UniversalPlatform.isMobile) return []; - final delta = node.delta; - if (delta == null) return []; - final widgets = []; - final textInserts = delta.whereType(); - int index = 0; - final editorState = context.read(); - for (final textInsert in textInserts) { - if (textInsert.attributes?.href != null) { - final nodeSelection = Selection( - start: Position(path: node.path, offset: index), - end: Position( - path: node.path, - offset: index + textInsert.length, - ), - ); - final rectList = delegate.getRectsInSelection(nodeSelection); - if (rectList.isNotEmpty) { - for (final rect in rectList) { - widgets.add( - Positioned( - left: rect.left, - top: rect.top, - child: SizedBox( - width: rect.width, - height: rect.height, - child: LinkHoverTrigger( - editorState: editorState, - selection: nodeSelection, - attribute: textInsert.attributes!, - node: node, - size: rect.size, - ), - ), - ), - ); - } - } - } - index += textInsert.length; - } - return widgets; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart new file mode 100644 index 0000000000..eb82f3d1fa --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -0,0 +1,207 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_share_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy/workspace/application/export/document_exporter.dart'; +import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DocumentShareButton extends StatelessWidget { + const DocumentShareButton({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: view), + child: BlocListener( + listener: (context, state) { + state.mapOrNull( + finish: (state) { + state.successOrFail.fold( + (data) => _handleExportData(context, data), + _handleExportError, + ); + }, + ); + }, + child: BlocBuilder( + builder: (context, state) => SizedBox( + height: 32.0, + child: IntrinsicWidth(child: ShareActionList(view: view)), + ), + ), + ), + ); + } + + void _handleExportData(BuildContext context, ExportDataPB exportData) { + switch (exportData.exportType) { + case ExportType.Markdown: + showSnackBarMessage( + context, + LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + break; + case ExportType.Link: + case ExportType.Text: + break; + case ExportType.HTML: + showSnackBarMessage( + context, + LocaleKeys.settings_files_exportFileSuccess.tr(), + ); + break; + } + } + + void _handleExportError(FlowyError error) { + showMessageToast(error.msg); + } +} + +class ShareActionList extends StatefulWidget { + const ShareActionList({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + State createState() => ShareActionListState(); +} + +@visibleForTesting +class ShareActionListState extends State { + late String name; + late final ViewListener viewListener = ViewListener(viewId: widget.view.id); + + @override + void initState() { + super.initState(); + listenOnViewUpdated(); + } + + @override + void dispose() { + viewListener.stop(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final docShareBloc = context.read(); + return PopoverActionList( + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 8), + actions: ShareAction.values + .map((action) => ShareActionWrapper(action)) + .toList(), + buildChild: (controller) => Listener( + onPointerDown: (_) => controller.show(), + child: RoundedTextButton( + title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + onPressed: () {}, + fontSize: 14.0, + textColor: Theme.of(context).colorScheme.onPrimary, + ), + ), + onSelected: (action, controller) async { + switch (action.inner) { + case ShareAction.markdown: + final exportPath = await getIt().saveFile( + dialogTitle: '', + // encode the file name in case it contains special characters + fileName: '${name.toFileName()}.md', + ); + if (exportPath != null) { + docShareBloc.add( + DocumentShareEvent.share( + DocumentShareType.markdown, + exportPath, + ), + ); + } + break; + case ShareAction.html: + final exportPath = await getIt().saveFile( + dialogTitle: '', + fileName: '${name.toFileName()}.html', + ); + if (exportPath != null) { + docShareBloc.add( + DocumentShareEvent.share( + DocumentShareType.html, + exportPath, + ), + ); + } + break; + case ShareAction.clipboard: + final documentExporter = DocumentExporter(widget.view); + final result = + await documentExporter.export(DocumentExportType.markdown); + result.fold( + (markdown) => getIt() + .setData(ClipboardServiceData(plainText: markdown)), + (error) => showMessageToast(error.msg), + ); + break; + } + controller.close(); + }, + ); + } + + void listenOnViewUpdated() { + name = widget.view.name; + viewListener.start( + onViewUpdated: (view) { + name = view.name; + }, + ); + } +} + +enum ShareAction { + markdown, + html, + clipboard, +} + +class ShareActionWrapper extends ActionCell { + ShareActionWrapper(this.inner); + + final ShareAction inner; + + Widget? icon(Color iconColor) => null; + + @override + String get name { + switch (inner) { + case ShareAction.markdown: + return LocaleKeys.shareAction_markdown.tr(); + case ShareAction.html: + return LocaleKeys.shareAction_html.tr(); + case ShareAction.clipboard: + return LocaleKeys.shareAction_clipboard.tr(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart deleted file mode 100644 index c116680c2e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_actions_command.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'emoji_menu.dart'; - -const _emojiCharacter = ':'; -final _letterRegExp = RegExp(r'^[a-zA-Z]$'); - -CharacterShortcutEvent emojiCommand(BuildContext context) => - CharacterShortcutEvent( - key: 'Opens Emoji Menu', - character: '', - regExp: _letterRegExp, - handler: (editorState) async { - return false; - }, - handlerWithCharacter: (editorState, character) { - emojiMenuService = EmojiMenu( - overlay: Overlay.of(context), - editorState: editorState, - ); - return emojiCommandHandler(editorState, context, character); - }, - ); - -EmojiMenuService? emojiMenuService; - -Future emojiCommandHandler( - EditorState editorState, - BuildContext context, - String character, -) async { - final selection = editorState.selection; - - if (UniversalPlatform.isMobile || selection == null) { - return false; - } - - final node = editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null || node.type == CodeBlockKeys.type) { - return false; - } - - if (selection.end.offset > 0) { - final plain = delta.toPlainText(); - - final previousCharacter = plain[selection.end.offset - 1]; - if (previousCharacter != _emojiCharacter) return false; - if (!context.mounted) return false; - - if (!selection.isCollapsed) return false; - - await editorState.insertTextAtPosition( - character, - position: selection.start, - ); - - emojiMenuService?.show(character); - return true; - } - - return false; -} diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart deleted file mode 100644 index b1b1e7cdbb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_handler.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flowy_infra/size.dart'; - -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; - -import 'emoji_menu.dart'; - -class EmojiHandler extends StatefulWidget { - const EmojiHandler({ - super.key, - required this.editorState, - required this.menuService, - required this.onDismiss, - required this.onSelectionUpdate, - required this.onEmojiSelect, - this.cancelBySpaceHandler, - this.initialSearchText = '', - }); - - final EditorState editorState; - final EmojiMenuService menuService; - final VoidCallback onDismiss; - final VoidCallback onSelectionUpdate; - final SelectEmojiItemHandler onEmojiSelect; - final String initialSearchText; - final bool Function()? cancelBySpaceHandler; - - @override - State createState() => _EmojiHandlerState(); -} - -class _EmojiHandlerState extends State { - final focusNode = FocusNode(debugLabel: 'emoji_menu_handler'); - final scrollController = ScrollController(); - late EmojiData emojiData; - final List searchedEmojis = []; - bool loaded = false; - int invalidCounter = 0; - late int startOffset; - late String _search = widget.initialSearchText; - double emojiHeight = 36.0; - final configuration = EmojiPickerConfiguration( - defaultSkinTone: lastSelectedEmojiSkinTone ?? EmojiSkinTone.none, - ); - - int get startCharAmount => widget.initialSearchText.length; - - set search(String search) { - _search = search; - _doSearch(); - } - - final ValueNotifier selectedIndexNotifier = ValueNotifier(0); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback( - (_) => focusNode.requestFocus(), - ); - - startOffset = - (widget.editorState.selection?.endIndex ?? 0) - startCharAmount; - - if (kCachedEmojiData != null) { - loadEmojis(kCachedEmojiData!); - } else { - EmojiData.builtIn().then( - (value) { - kCachedEmojiData = value; - loadEmojis(value); - }, - ); - } - } - - @override - void dispose() { - focusNode.dispose(); - selectedIndexNotifier.dispose(); - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final noEmojis = searchedEmojis.isEmpty; - return Focus( - focusNode: focusNode, - onKeyEvent: onKeyEvent, - child: Container( - constraints: const BoxConstraints(maxHeight: 392, maxWidth: 360), - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6.0), - color: Theme.of(context).cardColor, - boxShadow: [ - BoxShadow( - blurRadius: 5, - spreadRadius: 1, - color: Colors.black.withAlpha(25), - ), - ], - ), - child: noEmojis ? buildLoading() : buildEmojis(), - ), - ); - } - - Widget buildLoading() { - return SizedBox( - width: 400, - height: 40, - child: Center( - child: SizedBox.square( - dimension: 20, - child: CircularProgressIndicator(), - ), - ), - ); - } - - Widget buildEmojis() { - return SizedBox( - height: - (searchedEmojis.length / configuration.perLine).ceil() * emojiHeight, - child: GridView.builder( - controller: scrollController, - itemCount: searchedEmojis.length, - padding: const EdgeInsets.symmetric(horizontal: 16), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: configuration.perLine, - ), - itemBuilder: (context, index) { - final currentEmoji = searchedEmojis[index]; - final emojiId = currentEmoji.id; - final emoji = emojiData.getEmojiById( - emojiId, - skinTone: configuration.defaultSkinTone, - ); - return ValueListenableBuilder( - valueListenable: selectedIndexNotifier, - builder: (context, value, child) { - final isSelected = value == index; - return SizedBox.square( - dimension: emojiHeight, - child: FlowyButton( - isSelected: isSelected, - margin: EdgeInsets.zero, - radius: Corners.s8Border, - text: ManualTooltip( - key: ValueKey('$emojiId-$isSelected'), - message: currentEmoji.name, - showAutomaticlly: isSelected, - preferBelow: false, - child: FlowyText.emoji( - emoji, - fontSize: configuration.emojiSize, - ), - ), - onTap: () => onSelect(index), - ), - ); - }, - ); - }, - ), - ); - } - - void changeSelectedIndex(int index) => selectedIndexNotifier.value = index; - - void loadEmojis(EmojiData data) { - emojiData = data; - searchedEmojis.clear(); - searchedEmojis.addAll(emojiData.emojis.values); - if (mounted) { - setState(() { - loaded = true; - }); - } - WidgetsBinding.instance.addPostFrameCallback((_) { - _doSearch(); - }); - } - - void _doSearch() { - if (!loaded || !mounted) return; - final enableEmptySearch = widget.initialSearchText.isEmpty; - if ((_search.startsWith(' ') || _search.isEmpty) && !enableEmptySearch) { - widget.onDismiss.call(); - return; - } - final searchEmojiData = emojiData.filterByKeyword(_search); - setState(() { - searchedEmojis.clear(); - searchedEmojis.addAll(searchEmojiData.emojis.values); - changeSelectedIndex(0); - _scrollToItem(); - }); - if (searchedEmojis.isEmpty) { - widget.onDismiss.call(); - } - } - - KeyEventResult onKeyEvent(focus, KeyEvent event) { - if (event is! KeyDownEvent && event is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - const moveKeys = [ - LogicalKeyboardKey.arrowUp, - LogicalKeyboardKey.arrowDown, - LogicalKeyboardKey.arrowLeft, - LogicalKeyboardKey.arrowRight, - ]; - - if (event.logicalKey == LogicalKeyboardKey.enter) { - onSelect(selectedIndexNotifier.value); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.escape) { - // Workaround to bring focus back to editor - widget.editorState - .updateSelectionWithReason(widget.editorState.selection); - widget.onDismiss.call(); - } else if (event.logicalKey == LogicalKeyboardKey.backspace) { - if (_search.isEmpty) { - if (widget.initialSearchText.isEmpty) { - widget.onDismiss.call(); - return KeyEventResult.handled; - } - if (_canDeleteLastCharacter()) { - widget.editorState.deleteBackward(); - } else { - // Workaround for editor regaining focus - widget.editorState.apply( - widget.editorState.transaction - ..afterSelection = widget.editorState.selection, - ); - } - widget.onDismiss.call(); - } else { - widget.onSelectionUpdate(); - widget.editorState.deleteBackward(); - _deleteCharacterAtSelection(); - } - - return KeyEventResult.handled; - } else if (event.character != null && - !moveKeys.contains(event.logicalKey)) { - /// Prevents dismissal of context menu by notifying the parent - /// that the selection change occurred from the handler. - widget.onSelectionUpdate(); - - if (event.logicalKey == LogicalKeyboardKey.space) { - final cancelBySpaceHandler = widget.cancelBySpaceHandler; - if (cancelBySpaceHandler != null && cancelBySpaceHandler()) { - return KeyEventResult.handled; - } - } - - // Interpolation to avoid having a getter for private variable - _insertCharacter(event.character!); - return KeyEventResult.handled; - } else if (moveKeys.contains(event.logicalKey)) { - _moveSelection(event.logicalKey); - return KeyEventResult.handled; - } - - return KeyEventResult.handled; - } - - void onSelect(int index) { - widget.onEmojiSelect.call( - context, - (startOffset - startCharAmount, startOffset + _search.length), - emojiData.getEmojiById(searchedEmojis[index].id), - ); - widget.onDismiss.call(); - } - - void _insertCharacter(String character) { - widget.editorState.insertTextAtCurrentSelection(character); - - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final delta = widget.editorState.getNodeAtPath(selection.end.path)?.delta; - if (delta == null) { - return; - } - - search = widget.editorState - .getTextInSelection( - selection.copyWith( - start: selection.start.copyWith(offset: startOffset), - end: selection.start - .copyWith(offset: startOffset + _search.length + 1), - ), - ) - .join(); - } - - void _moveSelection(LogicalKeyboardKey key) { - final index = selectedIndexNotifier.value, - perLine = configuration.perLine, - remainder = index % perLine, - length = searchedEmojis.length, - currentLine = index ~/ perLine, - maxLine = (length / perLine).ceil(); - - final heightBefore = currentLine * emojiHeight; - if (key == LogicalKeyboardKey.arrowUp) { - if (currentLine == 0) { - final exceptLine = max(0, maxLine - 1); - changeSelectedIndex(min(exceptLine * perLine + remainder, length - 1)); - } else if (currentLine > 0) { - changeSelectedIndex(index - perLine); - } - } else if (key == LogicalKeyboardKey.arrowDown) { - if (currentLine == maxLine - 1) { - changeSelectedIndex(remainder); - } else if (currentLine < maxLine - 1) { - changeSelectedIndex(min(index + perLine, length - 1)); - } - } else if (key == LogicalKeyboardKey.arrowLeft) { - if (index == 0) { - changeSelectedIndex(length - 1); - } else if (index > 0) { - changeSelectedIndex(index - 1); - } - } else if (key == LogicalKeyboardKey.arrowRight) { - if (index == length - 1) { - changeSelectedIndex(0); - } else if (index < length - 1) { - changeSelectedIndex(index + 1); - } - } - final heightAfter = - (selectedIndexNotifier.value ~/ configuration.perLine) * emojiHeight; - - if (mounted && (heightAfter != heightBefore)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _scrollToItem(); - }); - } - } - - void _scrollToItem() { - final noEmojis = searchedEmojis.isEmpty; - if (noEmojis || !mounted) return; - final currentItem = selectedIndexNotifier.value; - final exceptHeight = (currentItem ~/ configuration.perLine) * emojiHeight; - final maxExtent = scrollController.position.maxScrollExtent; - final jumpTo = (exceptHeight - maxExtent > 10 * emojiHeight) - ? exceptHeight - : min(exceptHeight, maxExtent); - scrollController.animateTo( - jumpTo, - duration: Duration(milliseconds: 300), - curve: Curves.linear, - ); - } - - void _deleteCharacterAtSelection() { - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = widget.editorState.getNodeAtPath(selection.end.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - search = delta.toPlainText().substring( - startOffset, - startOffset + _search.length - 1, - ); - } - - bool _canDeleteLastCharacter() { - final selection = widget.editorState.selection; - if (selection == null || !selection.isCollapsed) { - return false; - } - - final delta = widget.editorState.getNodeAtPath(selection.start.path)?.delta; - if (delta == null) { - return false; - } - - return delta.isNotEmpty; - } -} - -typedef SelectEmojiItemHandler = void Function( - BuildContext context, - (int start, int end) replacement, - String emoji, -); diff --git a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart b/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart deleted file mode 100644 index 29f130d77d..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/emoji/emoji_menu.dart +++ /dev/null @@ -1,231 +0,0 @@ -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; - -import 'emoji_actions_command.dart'; -import 'emoji_handler.dart'; - -abstract class EmojiMenuService { - void show(String character); - - void dismiss(); -} - -class EmojiMenu extends EmojiMenuService { - EmojiMenu({ - required this.overlay, - required this.editorState, - this.cancelBySpaceHandler, - this.menuHeight = 400, - this.menuWidth = 300, - }); - - final EditorState editorState; - final double menuHeight; - final double menuWidth; - final OverlayState overlay; - final bool Function()? cancelBySpaceHandler; - - Offset _offset = Offset.zero; - Alignment _alignment = Alignment.topLeft; - OverlayEntry? _menuEntry; - bool selectionChangedByMenu = false; - String initialCharacter = ''; - - @override - void dismiss() { - if (_menuEntry != null) { - editorState.service.keyboardService?.enable(); - editorState.service.scrollService?.enable(); - keepEditorFocusNotifier.decrease(); - } - - _menuEntry?.remove(); - _menuEntry = null; - - // workaround: SelectionService has been released after hot reload. - final isSelectionDisposed = - editorState.service.selectionServiceKey.currentState == null; - if (!isSelectionDisposed) { - final selectionService = editorState.service.selectionService; - selectionService.currentSelection.removeListener(_onSelectionChange); - } - emojiMenuService = null; - } - - void _onSelectionUpdate() => selectionChangedByMenu = true; - - @override - void show(String character) { - initialCharacter = character; - WidgetsBinding.instance.addPostFrameCallback((_) => _show()); - } - - void _show() { - final selectionService = editorState.service.selectionService; - final selectionRects = selectionService.selectionRects; - if (selectionRects.isEmpty) { - return; - } - - final Size editorSize = editorState.renderBox!.size; - - calculateSelectionMenuOffset(selectionRects.first); - - final (left, top, right, bottom) = _getPosition(); - - _menuEntry = OverlayEntry( - builder: (context) => SizedBox( - height: editorSize.height, - width: editorSize.width, - - // GestureDetector handles clicks outside of the context menu, - // to dismiss the context menu. - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: dismiss, - child: Stack( - children: [ - Positioned( - top: top, - bottom: bottom, - left: left, - right: right, - child: EmojiHandler( - editorState: editorState, - menuService: this, - onDismiss: dismiss, - onSelectionUpdate: _onSelectionUpdate, - cancelBySpaceHandler: cancelBySpaceHandler, - initialSearchText: initialCharacter, - onEmojiSelect: ( - BuildContext context, - (int, int) replacement, - String emoji, - ) async { - final selection = editorState.selection; - - if (selection == null) return; - final node = - editorState.document.nodeAtPath(selection.end.path); - if (node == null) return; - final transaction = editorState.transaction - ..deleteText( - node, - replacement.$1, - replacement.$2 - replacement.$1, - ) - ..insertText( - node, - replacement.$1, - emoji, - ); - await editorState.apply(transaction); - }, - ), - ), - ], - ), - ), - ), - ); - - overlay.insert(_menuEntry!); - - keepEditorFocusNotifier.increase(); - editorState.service.keyboardService?.disable(showCursor: true); - editorState.service.scrollService?.disable(); - selectionService.currentSelection.addListener(_onSelectionChange); - } - - void _onSelectionChange() { - // workaround: SelectionService has been released after hot reload. - final isSelectionDisposed = - editorState.service.selectionServiceKey.currentState == null; - if (!isSelectionDisposed) { - final selectionService = editorState.service.selectionService; - if (selectionService.currentSelection.value == null) { - return; - } - } - - if (!selectionChangedByMenu) { - return dismiss(); - } - - selectionChangedByMenu = false; - } - - (double? left, double? top, double? right, double? bottom) _getPosition() { - double? left, top, right, bottom; - switch (_alignment) { - case Alignment.topLeft: - left = _offset.dx; - top = _offset.dy; - break; - case Alignment.bottomLeft: - left = _offset.dx; - bottom = _offset.dy; - break; - case Alignment.topRight: - right = _offset.dx; - top = _offset.dy; - break; - case Alignment.bottomRight: - right = _offset.dx; - bottom = _offset.dy; - break; - } - - return (left, top, right, bottom); - } - - void calculateSelectionMenuOffset(Rect rect) { - // Workaround: We can customize the padding through the [EditorStyle], - // but the coordinates of overlay are not properly converted currently. - // Just subtract the padding here as a result. - const menuOffset = Offset(0, 10); - final editorOffset = - editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; - final editorHeight = editorState.renderBox!.size.height; - final editorWidth = editorState.renderBox!.size.width; - - // show below default - _alignment = Alignment.topLeft; - final bottomRight = rect.bottomRight; - final topRight = rect.topRight; - var offset = bottomRight + menuOffset; - _offset = Offset( - offset.dx, - offset.dy, - ); - - // show above - if (offset.dy + menuHeight >= editorOffset.dy + editorHeight) { - offset = topRight - menuOffset; - _alignment = Alignment.bottomLeft; - - _offset = Offset( - offset.dx, - editorHeight + editorOffset.dy - offset.dy, - ); - } - - // show on right - if (_offset.dx + menuWidth < editorOffset.dx + editorWidth) { - _offset = Offset( - _offset.dx, - _offset.dy, - ); - } else if (offset.dx - editorOffset.dx > menuWidth) { - // show on left - _alignment = _alignment == Alignment.topLeft - ? Alignment.topRight - : Alignment.bottomRight; - - _offset = Offset( - editorWidth - _offset.dx + editorOffset.dx, - _offset.dy, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart deleted file mode 100644 index 6dbd38affb..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/child_page.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; -import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/service_handler.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy_backend/log.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:flutter/material.dart'; - -class InlineChildPageService extends InlineActionsDelegate { - InlineChildPageService({required this.currentViewId}); - - final String currentViewId; - - @override - Future search(String? search) async { - final List results = []; - if (search != null && search.isNotEmpty) { - results.add( - InlineActionsMenuItem( - label: LocaleKeys.inlineActions_createPage.tr(args: [search]), - iconBuilder: (_) => const FlowySvg(FlowySvgs.add_s), - onSelected: (context, editorState, service, replacement) => - _onSelected(context, editorState, service, replacement, search), - ), - ); - } - - return InlineActionsResult(results: results); - } - - Future _onSelected( - BuildContext context, - EditorState editorState, - InlineActionsMenuService service, - (int, int) replacement, - String? search, - ) async { - final selection = editorState.selection; - if (selection == null || !selection.isCollapsed) { - return; - } - - final node = editorState.getNodeAtPath(selection.start.path); - final delta = node?.delta; - if (node == null || delta == null) { - return; - } - - final view = (await ViewBackendService.createView( - layoutType: ViewLayoutPB.Document, - parentViewId: currentViewId, - name: search!, - )) - .toNullable(); - - if (view == null) { - return Log.error('Failed to create view'); - } - - // preload the page info - pageMemorizer[view.id] = view; - final transaction = editorState.transaction - ..replaceText( - node, - replacement.$1, - replacement.$2, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.childPage, - pageId: view.id, - blockId: null, - ), - ); - - await editorState.apply(transaction); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart index 747c8667f8..6f3bf087a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/date_reference.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/date/date_service.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; @@ -6,7 +8,6 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/service_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; final _keywords = [ LocaleKeys.inlineActions_date.tr().toLowerCase(), @@ -121,13 +122,13 @@ class DateReferenceService extends InlineActionsDelegate { node, start, end, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - includeTime: false, - reminderId: null, - reminderOption: null, - ), + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + }, + }, ); await editorState.apply(transaction); @@ -138,36 +139,22 @@ class DateReferenceService extends InlineActionsDelegate { final tomorrow = today.add(const Duration(days: 1)); final yesterday = today.subtract(const Duration(days: 1)); - late InlineActionsMenuItem todayItem; - late InlineActionsMenuItem tomorrowItem; - late InlineActionsMenuItem yesterdayItem; - - try { - todayItem = _itemFromDate( + _allOptions = [ + _itemFromDate( today, LocaleKeys.relativeDates_today.tr(), [DateFormat.yMd(_locale).format(today)], - ); - tomorrowItem = _itemFromDate( + ), + _itemFromDate( tomorrow, LocaleKeys.relativeDates_tomorrow.tr(), [DateFormat.yMd(_locale).format(tomorrow)], - ); - yesterdayItem = _itemFromDate( + ), + _itemFromDate( yesterday, LocaleKeys.relativeDates_yesterday.tr(), [DateFormat.yMd(_locale).format(yesterday)], - ); - } catch (e) { - todayItem = _itemFromDate(today); - tomorrowItem = _itemFromDate(tomorrow); - yesterdayItem = _itemFromDate(yesterday); - } - - _allOptions = [ - todayItem, - tomorrowItem, - yesterdayItem, + ), ]; } @@ -187,17 +174,7 @@ class DateReferenceService extends InlineActionsDelegate { String? label, List? keywords, ]) { - late String labelStr; - if (label != null) { - labelStr = label; - } else { - try { - labelStr = DateFormat.yMd(_locale).format(date); - } catch (e) { - // fallback to en-US - labelStr = DateFormat.yMd('en-US').format(date); - } - } + final labelStr = label ?? DateFormat.yMd(_locale).format(date); return InlineActionsMenuItem( label: labelStr.capitalize(), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart index 9853d6757c..4da27109d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/inline_page_reference.dart @@ -1,16 +1,13 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/insert_page_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/service_handler.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -20,6 +17,7 @@ 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/widget/dialog/styled_dialogs.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; // const _channel = "InlinePageReference"; @@ -66,9 +64,11 @@ class InlinePageReferenceService extends InlineActionsDelegate { _recentViewsInitialized = true; - final sectionViews = await _recentService.recentViews(); - final views = - sectionViews.unique((e) => e.item.id).map((e) => e.item).toList(); + final views = (await _recentService.recentViews()) + .reversed + .map((e) => e.item) + .toSet() + .toList(); // Filter by viewLayout views.retainWhere( @@ -126,15 +126,7 @@ class InlinePageReferenceService extends InlineActionsDelegate { items = allViews .where( - (view) => - view.id != currentViewId && - view.name.toLowerCase().contains(search.toLowerCase()) || - (view.name.isEmpty && search.isEmpty) || - (view.name.isEmpty && - LocaleKeys.menuAppHeader_defaultNewPageName - .tr() - .toLowerCase() - .contains(search.toLowerCase())), + (view) => view.name.toLowerCase().contains(search.toLowerCase()), ) .take(limitResults) .map((view) => _fromView(view)) @@ -186,8 +178,9 @@ class InlinePageReferenceService extends InlineActionsDelegate { if (context.mounted) { return Dialogs.show( context, - child: AppFlowyErrorPage( - error: e, + child: FlowyErrorPage.message( + e.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), ); } @@ -220,47 +213,43 @@ class InlinePageReferenceService extends InlineActionsDelegate { node, replace.$1, replace.$2, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionPageAttributes( - mentionType: MentionType.page, - pageId: view.id, - blockId: null, - ), + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.page.name, + MentionBlockKeys.pageId: view.id, + }, + }, ); await editorState.apply(transaction); } InlineActionsMenuItem _fromView(ViewPB view) => InlineActionsMenuItem( - keywords: [view.nameOrDefault.toLowerCase()], - label: view.nameOrDefault, - iconBuilder: (onSelected) { - final child = view.icon.value.isNotEmpty - ? RawEmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 16.0, - lineHeight: 18.0 / 16.0, - ) - : view.defaultIcon(size: const Size(16, 16)); - return SizedBox( - width: 16, - child: child, - ); - }, + keywords: [view.name.toLowerCase()], + label: view.name, + icon: (onSelected) => view.icon.value.isNotEmpty + ? EmojiText( + emoji: view.icon.value, + fontSize: 12, + textAlign: TextAlign.center, + lineHeight: 1.3, + ) + : view.defaultIcon(), onSelected: (context, editorState, menu, replace) => insertPage ? _onInsertPageRef(view, context, editorState, replace) : _onInsertLinkRef(view, context, editorState, menu, replace), ); -// Future _fromSearchResult( -// SearchResultPB result, -// ) async { -// final viewRes = await ViewBackendService.getView(result.viewId); -// final view = viewRes.toNullable(); -// if (view == null) { -// return null; -// } + // Future _fromSearchResult( + // SearchResultPB result, + // ) async { + // final viewRes = await ViewBackendService.getView(result.viewId); + // final view = viewRes.toNullable(); + // if (view == null) { + // return null; + // } -// return _fromView(view); -// } + // return _fromView(view); + // } } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart index 471f1c9211..319e6091c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/handlers/reminder_reference.dart @@ -147,13 +147,15 @@ class ReminderReferenceService extends InlineActionsDelegate { node, start, end, - MentionBlockKeys.mentionChar, - attributes: MentionBlockKeys.buildMentionDateAttributes( - date: date.toIso8601String(), - reminderId: reminder.id, - reminderOption: ReminderOption.atTimeOfEvent.name, - includeTime: false, - ), + '\$', + attributes: { + MentionBlockKeys.mention: { + MentionBlockKeys.type: MentionType.date.name, + MentionBlockKeys.date: date.toIso8601String(), + MentionBlockKeys.reminderId: reminder.id, + MentionBlockKeys.reminderOption: ReminderOption.atTimeOfEvent.name, + }, + }, ); await editorState.apply(transaction); @@ -168,32 +170,17 @@ class ReminderReferenceService extends InlineActionsDelegate { final tomorrow = today.add(const Duration(days: 1)); final oneWeek = today.add(const Duration(days: 7)); - late InlineActionsMenuItem todayItem; - late InlineActionsMenuItem oneWeekItem; - - try { - todayItem = _itemFromDate( + _allOptions = [ + _itemFromDate( tomorrow, LocaleKeys.relativeDates_tomorrow.tr(), [DateFormat.yMd(_locale).format(tomorrow)], - ); - } catch (e) { - todayItem = _itemFromDate(today); - } - - try { - oneWeekItem = _itemFromDate( + ), + _itemFromDate( oneWeek, LocaleKeys.relativeDates_oneWeek.tr(), [DateFormat.yMd(_locale).format(oneWeek)], - ); - } catch (e) { - oneWeekItem = _itemFromDate(oneWeek); - } - - _allOptions = [ - todayItem, - oneWeekItem, + ), ]; } @@ -213,17 +200,7 @@ class ReminderReferenceService extends InlineActionsDelegate { String? label, List? keywords, ]) { - late String labelStr; - if (label != null) { - labelStr = label; - } else { - try { - labelStr = DateFormat.yMd(_locale).format(date); - } catch (e) { - // fallback to en-US - labelStr = DateFormat.yMd('en-US').format(date); - } - } + final labelStr = label ?? DateFormat.yMd(_locale).format(date); return InlineActionsMenuItem( label: labelStr.capitalize(), @@ -242,8 +219,6 @@ class ReminderReferenceService extends InlineActionsDelegate { meta: { ReminderMetaKeys.includeTime: false.toString(), ReminderMetaKeys.blockId: node.id, - ReminderMetaKeys.createdAt: - DateTime.now().millisecondsSinceEpoch.toString(), }, scheduledAt: Int64(date.millisecondsSinceEpoch ~/ 1000), isAck: date.isBefore(DateTime.now()), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart index e0e03e7dec..845eaf8c69 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_command.dart @@ -1,9 +1,7 @@ -import 'package:appflowy/mobile/presentation/inline_actions/mobile_inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:universal_platform/universal_platform.dart'; const inlineActionCharacter = '@'; @@ -22,14 +20,13 @@ CharacterShortcutEvent inlineActionsCommand( ); InlineActionsMenuService? selectionMenuService; - Future inlineActionsCommandHandler( EditorState editorState, InlineActionsService service, InlineActionsMenuStyle style, ) async { final selection = editorState.selection; - if (selection == null) { + if (PlatformExtension.isMobile || selection == null) { return false; } @@ -52,31 +49,15 @@ Future inlineActionsCommandHandler( } if (service.context != null) { - keepEditorFocusNotifier.increase(); - selectionMenuService?.dismiss(); - selectionMenuService = UniversalPlatform.isMobile - ? MobileInlineActionsMenu( - context: service.context!, - editorState: editorState, - service: service, - initialResults: initialResults, - style: style, - ) - : InlineActionsMenu( - context: service.context!, - editorState: editorState, - service: service, - initialResults: initialResults, - style: style, - ); + selectionMenuService = InlineActionsMenu( + context: service.context!, + editorState: editorState, + service: service, + initialResults: initialResults, + style: style, + ); - // disable the keyboard service - editorState.service.keyboardService?.disable(); - - await selectionMenuService?.show(); - - // enable the keyboard service - editorState.service.keyboardService?.enable(); + selectionMenuService?.show(); } return true; diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart index 651e739abc..3521a889e8 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_menu.dart @@ -1,16 +1,14 @@ -import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_service.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter/material.dart'; abstract class InlineActionsMenuService { InlineActionsMenuStyle get style; - Future show(); - + void show(); void dismiss(); } @@ -22,14 +20,12 @@ class InlineActionsMenu extends InlineActionsMenuService { required this.initialResults, required this.style, this.startCharAmount = 1, - this.cancelBySpaceHandler, }); final BuildContext context; final EditorState editorState; final InlineActionsService service; final List initialResults; - final bool Function()? cancelBySpaceHandler; @override final InlineActionsMenuStyle style; @@ -44,7 +40,6 @@ class InlineActionsMenu extends InlineActionsMenuService { if (_menuEntry != null) { editorState.service.keyboardService?.enable(); editorState.service.scrollService?.enable(); - keepEditorFocusNotifier.decrease(); } _menuEntry?.remove(); @@ -62,13 +57,8 @@ class InlineActionsMenu extends InlineActionsMenuService { void _onSelectionUpdate() => selectionChangedByMenu = true; @override - Future show() { - final completer = Completer(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - _show(); - completer.complete(); - }); - return completer.future; + void show() { + WidgetsBinding.instance.addPostFrameCallback((_) => _show()); } void _show() { @@ -147,7 +137,6 @@ class InlineActionsMenu extends InlineActionsMenuService { onSelectionUpdate: _onSelectionUpdate, style: style, startCharAmount: startCharAmount, - cancelBySpaceHandler: cancelBySpaceHandler, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart index 1fe2703870..c1f1a9e237 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/inline_actions_result.dart @@ -12,20 +12,20 @@ typedef SelectItemHandler = void Function( class InlineActionsMenuItem { InlineActionsMenuItem({ required this.label, - this.iconBuilder, + this.icon, this.keywords, this.onSelected, }); final String label; - final Widget Function(bool onSelected)? iconBuilder; + final Widget Function(bool onSelected)? icon; final List? keywords; final SelectItemHandler? onSelected; } class InlineActionsResult { InlineActionsResult({ - this.title, + required this.title, required this.results, this.startsWithKeywords, }); @@ -33,9 +33,7 @@ class InlineActionsResult { /// Localized title to be displayed above the results /// of the current group. /// - /// If null, no title will be displayed. - /// - final String? title; + final String title; /// List of results that will be displayed for this group /// made up of [SelectionMenuItem]s. diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 63ccb04839..94c9d62a74 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; @@ -9,16 +12,13 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; /// All heights are in physical pixels const double _groupTextHeight = 14; // 12 height + 2 bottom spacing const double _groupBottomSpacing = 6; const double _itemHeight = 30; // 26 height + 4 vertical spacing (2*2) -const double kInlineMenuHeight = 300; -const double kInlineMenuWidth = 400; +const double _menuHeight = 300; const double _contentHeight = 260; extension _StartWithsSort on List { @@ -49,7 +49,7 @@ extension _StartWithsSort on List { ); } -const _invalidSearchesAmount = 10; +const _invalidSearchesAmount = 20; class InlineActionsHandler extends StatefulWidget { const InlineActionsHandler({ @@ -62,7 +62,6 @@ class InlineActionsHandler extends StatefulWidget { required this.onSelectionUpdate, required this.style, this.startCharAmount = 1, - this.cancelBySpaceHandler, }); final InlineActionsService service; @@ -73,7 +72,6 @@ class InlineActionsHandler extends StatefulWidget { final VoidCallback onSelectionUpdate; final InlineActionsMenuStyle style; final int startCharAmount; - final bool Function()? cancelBySpaceHandler; @override State createState() => _InlineActionsHandlerState(); @@ -83,6 +81,8 @@ class _InlineActionsHandlerState extends State { final _focusNode = FocusNode(debugLabel: 'inline_actions_menu_handler'); final _scrollController = ScrollController(); + Timer? _debounce; + late List results = widget.results; int invalidCounter = 0; late int startOffset; @@ -90,7 +90,8 @@ class _InlineActionsHandlerState extends State { String _search = ''; set search(String search) { _search = search; - _doSearch(); + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 200), _doSearch); } Future _doSearch() async { @@ -108,13 +109,10 @@ class _InlineActionsHandlerState extends State { : 0; if (invalidCounter >= _invalidSearchesAmount) { - widget.onDismiss(); - // Workaround to bring focus back to editor await widget.editorState .updateSelectionWithReason(widget.editorState.selection); - - return; + return widget.onDismiss(); } _resetSelection(); @@ -145,6 +143,7 @@ class _InlineActionsHandlerState extends State { void dispose() { _scrollController.dispose(); _focusNode.dispose(); + _debounce?.cancel(); super.dispose(); } @@ -154,10 +153,7 @@ class _InlineActionsHandlerState extends State { focusNode: _focusNode, onKeyEvent: onKeyEvent, child: Container( - constraints: const BoxConstraints( - maxHeight: kInlineMenuHeight, - minWidth: kInlineMenuWidth, - ), + constraints: BoxConstraints.loose(const Size(200, _menuHeight)), decoration: BoxDecoration( color: widget.style.backgroundColor, borderRadius: BorderRadius.circular(6.0), @@ -165,7 +161,7 @@ class _InlineActionsHandlerState extends State { BoxShadow( blurRadius: 5, spreadRadius: 1, - color: Colors.black.withValues(alpha: 0.1), + color: Colors.black.withOpacity(0.1), ), ], ), @@ -290,17 +286,12 @@ class _InlineActionsHandlerState extends State { /// that the selection change occurred from the handler. widget.onSelectionUpdate(); - if (event.logicalKey == LogicalKeyboardKey.space) { - final cancelBySpaceHandler = widget.cancelBySpaceHandler; - if (cancelBySpaceHandler != null && cancelBySpaceHandler()) { - return KeyEventResult.handled; - } - } - // Interpolation to avoid having a getter for private variable _insertCharacter(event.character!); return KeyEventResult.handled; - } else if (moveKeys.contains(event.logicalKey)) { + } + + if (moveKeys.contains(event.logicalKey)) { _moveSelection(event.logicalKey); return KeyEventResult.handled; } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart index 123cfc1177..1392dd9b21 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_menu_group.dart @@ -1,6 +1,5 @@ import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; -import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -41,10 +40,8 @@ class InlineActionsGroup extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (result.title != null) ...[ - FlowyText.medium(result.title!, color: style.groupTextColor), - const SizedBox(height: 4), - ], + FlowyText.medium(result.title, color: style.groupTextColor), + const SizedBox(height: 4), ...result.results.mapIndexed( (index, item) => InlineActionsWidget( item: item, @@ -92,30 +89,14 @@ class InlineActionsWidget extends StatefulWidget { class _InlineActionsWidgetState extends State { @override Widget build(BuildContext context) { - final iconBuilder = widget.item.iconBuilder; - final hasIcon = iconBuilder != null; return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: SizedBox( - width: kInlineMenuWidth, + width: 200, child: FlowyButton( - expand: true, isSelected: widget.isSelected, - text: Row( - children: [ - if (hasIcon) ...[ - iconBuilder.call(widget.isSelected), - SizedBox(width: 12), - ], - Flexible( - child: FlowyText.regular( - widget.item.label, - figmaLineHeight: 18, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), + leftIcon: widget.item.icon?.call(widget.isSelected), + text: FlowyText.regular(widget.item.label), onTap: _onPressed, ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/cover_type_ext.dart b/frontend/appflowy_flutter/lib/plugins/shared/cover_type_ext.dart deleted file mode 100644 index a826ae0253..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/cover_type_ext.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; - -extension IntoCoverTypePB on CoverType { - CoverTypePB into() => switch (this) { - CoverType.color => CoverTypePB.ColorCover, - CoverType.asset => CoverTypePB.AssetCover, - CoverType.file => CoverTypePB.FileCover, - _ => CoverTypePB.FileCover, - }; -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart deleted file mode 100644 index 803c9867c9..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/_shared.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/plugins/shared/share/share_menu.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ShareMenuButton extends StatelessWidget { - const ShareMenuButton({ - super.key, - required this.tabs, - }); - - final List tabs; - - @override - Widget build(BuildContext context) { - final shareBloc = context.read(); - final databaseBloc = context.read(); - final userWorkspaceBloc = context.read(); - return BlocBuilder( - builder: (context, state) { - return SizedBox( - height: 32.0, - child: IntrinsicWidth( - child: AppFlowyPopover( - direction: PopoverDirection.bottomWithRightAligned, - constraints: const BoxConstraints( - maxWidth: 500, - ), - offset: const Offset(0, 8), - onOpen: () { - context - .read() - .add(const ShareEvent.updatePublishStatus()); - }, - popupBuilder: (_) { - return MultiBlocProvider( - providers: [ - if (databaseBloc != null) - BlocProvider.value( - value: databaseBloc, - ), - BlocProvider.value(value: shareBloc), - BlocProvider.value(value: userWorkspaceBloc), - ], - child: ShareMenu( - tabs: tabs, - viewName: state.viewName, - ), - ); - }, - child: PrimaryRoundedButton( - text: LocaleKeys.shareAction_buttonText.tr(), - figmaLineHeight: 16, - ), - ), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/constants.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/constants.dart deleted file mode 100644 index 649d7c0883..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/constants.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/startup/startup.dart'; - -class ShareConstants { - static const String testBaseWebDomain = 'test.appflowy.com'; - static const String defaultBaseWebDomain = 'https://appflowy.com'; - - static String buildPublishUrl({ - required String nameSpace, - required String publishName, - }) { - final baseShareDomain = - getIt().appflowyCloudConfig.base_web_domain; - final url = '$baseShareDomain/$nameSpace/$publishName'.addSchemaIfNeeded(); - return url; - } - - static String buildNamespaceUrl({ - required String nameSpace, - bool withHttps = false, - }) { - final baseShareDomain = - getIt().appflowyCloudConfig.base_web_domain; - String url = baseShareDomain.addSchemaIfNeeded(); - if (!withHttps) { - url = url.replaceFirst('https://', ''); - } - return '$url/$nameSpace'; - } - - static String buildShareUrl({ - required String workspaceId, - required String viewId, - String? blockId, - }) { - final baseShareDomain = - getIt().appflowyCloudConfig.base_web_domain; - final url = '$baseShareDomain/app/$workspaceId/$viewId'.addSchemaIfNeeded(); - if (blockId == null || blockId.isEmpty) { - return url; - } - return '$url?blockId=$blockId'; - } -} - -extension on String { - String addSchemaIfNeeded() { - final schema = Uri.parse(this).scheme; - // if the schema is empty, add https schema by default - if (schema.isEmpty) { - return 'https://$this'; - } - return this; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart deleted file mode 100644 index 9d6adee7df..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/export/document_exporter.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ExportTab extends StatelessWidget { - const ExportTab({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final view = context.read().view; - - if (view.layout == ViewLayoutPB.Document) { - return _buildDocumentExportTab(context); - } - - return _buildDatabaseExportTab(context); - } - - Widget _buildDocumentExportTab(BuildContext context) { - return Column( - children: [ - const VSpace(10), - _ExportButton( - title: LocaleKeys.shareAction_html.tr(), - svg: FlowySvgs.export_html_s, - onTap: () => _exportHTML(context), - ), - const VSpace(10), - _ExportButton( - title: LocaleKeys.shareAction_markdown.tr(), - svg: FlowySvgs.export_markdown_s, - onTap: () => _exportMarkdown(context), - ), - const VSpace(10), - _ExportButton( - title: LocaleKeys.shareAction_clipboard.tr(), - svg: FlowySvgs.duplicate_s, - onTap: () => _exportToClipboard(context), - ), - if (kDebugMode) ...[ - const VSpace(10), - _ExportButton( - title: 'JSON (Debug Mode)', - svg: FlowySvgs.duplicate_s, - onTap: () => _exportJSON(context), - ), - ], - ], - ); - } - - Widget _buildDatabaseExportTab(BuildContext context) { - return Column( - children: [ - const VSpace(10), - _ExportButton( - title: LocaleKeys.shareAction_csv.tr(), - svg: FlowySvgs.database_layout_s, - onTap: () => _exportCSV(context), - ), - if (kDebugMode) ...[ - const VSpace(10), - _ExportButton( - title: 'Raw Database Data (Debug Mode)', - svg: FlowySvgs.duplicate_s, - onTap: () => _exportRawDatabaseData(context), - ), - ], - ], - ); - } - - Future _exportHTML(BuildContext context) async { - final viewName = context.read().state.viewName; - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${viewName.toFileName()}.html', - ); - if (context.mounted && exportPath != null) { - context.read().add( - ShareEvent.share( - ShareType.html, - exportPath, - ), - ); - } - } - - Future _exportMarkdown(BuildContext context) async { - final viewName = context.read().state.viewName; - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${viewName.toFileName()}.zip', - ); - if (context.mounted && exportPath != null) { - context.read().add( - ShareEvent.share( - ShareType.markdown, - exportPath, - ), - ); - } - } - - Future _exportJSON(BuildContext context) async { - final viewName = context.read().state.viewName; - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${viewName.toFileName()}.json', - ); - if (context.mounted && exportPath != null) { - context.read().add( - ShareEvent.share( - ShareType.json, - exportPath, - ), - ); - } - } - - Future _exportCSV(BuildContext context) async { - final viewName = context.read().state.viewName; - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${viewName.toFileName()}.csv', - ); - if (context.mounted && exportPath != null) { - context.read().add( - ShareEvent.share( - ShareType.csv, - exportPath, - ), - ); - } - } - - Future _exportRawDatabaseData(BuildContext context) async { - final viewName = context.read().state.viewName; - final exportPath = await getIt().saveFile( - dialogTitle: '', - fileName: '${viewName.toFileName()}.json', - ); - if (context.mounted && exportPath != null) { - context.read().add( - ShareEvent.share( - ShareType.rawDatabaseData, - exportPath, - ), - ); - } - } - - Future _exportToClipboard(BuildContext context) async { - final documentExporter = DocumentExporter(context.read().view); - final result = await documentExporter.export(DocumentExportType.markdown); - result.fold( - (markdown) { - getIt().setData( - ClipboardServiceData(plainText: markdown), - ); - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - }, - (error) => showToastNotification(message: error.msg), - ); - } -} - -class _ExportButton extends StatelessWidget { - const _ExportButton({ - required this.title, - required this.svg, - required this.onTap, - }); - - final String title; - final FlowySvgData svg; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final color = Theme.of(context).isLightMode - ? const Color(0x1E14171B) - : Colors.white.withValues(alpha: 0.1); - final radius = BorderRadius.circular(10.0); - return FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), - iconPadding: 12, - decoration: BoxDecoration( - border: Border.all( - color: color, - ), - borderRadius: radius, - ), - radius: radius, - text: FlowyText( - title, - lineHeight: 1.0, - ), - leftIcon: FlowySvg(svg), - onTap: onTap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart deleted file mode 100644 index 1c957016e4..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_color_extension.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -class ShareMenuColors { - static Color borderColor(BuildContext context) { - final borderColor = Theme.of(context).isLightMode - ? const Color(0x1E14171B) - : Colors.white.withValues(alpha: 0.1); - return borderColor; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart deleted file mode 100644 index eae1d56a18..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_name_generator.dart +++ /dev/null @@ -1,19 +0,0 @@ -String replaceInvalidChars(String input) { - final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]'); - return input.replaceAll(invalidCharsRegex, '-'); -} - -Future generateNameSpace() async { - return ''; -} - -// The backend limits the publish name to a maximum of 120 characters. -// If the combined length of the ID and the name exceeds 120 characters, -// we will truncate the name to ensure the final result is within the limit. -// The name should only contain alphanumeric characters and hyphens. -Future generatePublishName(String id, String name) async { - if (name.length >= 120 - id.length) { - name = name.substring(0, 120 - id.length); - } - return replaceInvalidChars('$name-$id'); -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart deleted file mode 100644 index 244ded0bf6..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ /dev/null @@ -1,699 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/plugins/shared/share/publish_color_extension.dart'; -import 'package:appflowy/plugins/shared/share/publish_name_generator.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/shared/error_code/error_code_map.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/widget/rounded_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PublishTab extends StatelessWidget { - const PublishTab({ - super.key, - required this.viewName, - }); - - final String viewName; - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - _showToast(context, state); - }, - builder: (context, state) { - if (state.isPublished) { - return _PublishedWidget( - url: state.url, - pathName: state.pathName, - namespace: state.namespace, - onVisitSite: (url) => afLaunchUrlString(url), - onUnPublish: () { - context.read().add(const ShareEvent.unPublish()); - }, - ); - } else { - return _PublishWidget( - onPublish: (selectedViews) async { - final id = context.read().view.id; - final lastPublishName = context.read().state.pathName; - final publishName = lastPublishName.orDefault( - await generatePublishName( - id, - viewName, - ), - ); - - if (selectedViews.isNotEmpty) { - Log.info( - 'Publishing views: ${selectedViews.map((e) => e.name)}', - ); - } - - if (context.mounted) { - context.read().add( - ShareEvent.publish( - '', - publishName, - selectedViews.map((e) => e.id).toList(), - ), - ); - } - }, - ); - } - }, - ); - } - - void _showToast(BuildContext context, ShareState state) { - if (state.publishResult != null) { - state.publishResult!.fold( - (value) => showToastNotification( - message: LocaleKeys.publish_publishSuccessfully.tr(), - ), - (error) => showToastNotification( - message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}', - type: ToastificationType.error, - ), - ); - } else if (state.unpublishResult != null) { - state.unpublishResult!.fold( - (value) => showToastNotification( - message: LocaleKeys.publish_unpublishSuccessfully.tr(), - ), - (error) => showToastNotification( - message: LocaleKeys.publish_unpublishFailed.tr(), - description: error.msg, - type: ToastificationType.error, - ), - ); - } else if (state.updatePathNameResult != null) { - state.updatePathNameResult!.fold( - (value) => showToastNotification( - message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ), - (error) { - Log.error('update path name failed: $error'); - - showToastNotification( - message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), - type: ToastificationType.error, - description: error.code.publishErrorMessage, - ); - }, - ); - } - } -} - -class _PublishedWidget extends StatefulWidget { - const _PublishedWidget({ - required this.url, - required this.pathName, - required this.namespace, - required this.onVisitSite, - required this.onUnPublish, - }); - - final String url; - final String pathName; - final String namespace; - final void Function(String url) onVisitSite; - final VoidCallback onUnPublish; - - @override - State<_PublishedWidget> createState() => _PublishedWidgetState(); -} - -class _PublishedWidgetState extends State<_PublishedWidget> { - final controller = TextEditingController(); - - @override - void initState() { - super.initState(); - controller.text = widget.pathName; - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(16), - const _PublishTabHeader(), - const VSpace(16), - _PublishUrl( - namespace: widget.namespace, - controller: controller, - onCopy: (_) { - final url = context.read().state.url; - - getIt().setData( - ClipboardServiceData(plainText: url), - ); - - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - }, - onSubmitted: (pathName) { - context.read().add(ShareEvent.updatePathName(pathName)); - }, - ), - const VSpace(16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Spacer(), - UnPublishButton( - onUnPublish: widget.onUnPublish, - ), - const HSpace(6), - _buildVisitSiteButton(), - ], - ), - ], - ); - } - - Widget _buildVisitSiteButton() { - return RoundedTextButton( - width: 108, - height: 36, - onPressed: () { - final url = context.read().state.url; - widget.onVisitSite(url); - }, - title: LocaleKeys.shareAction_visitSite.tr(), - borderRadius: const BorderRadius.all(Radius.circular(10)), - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - textColor: Theme.of(context).colorScheme.onPrimary, - ); - } -} - -class UnPublishButton extends StatelessWidget { - const UnPublishButton({ - super.key, - required this.onUnPublish, - }); - - final VoidCallback onUnPublish; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: 108, - height: 36, - child: FlowyButton( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: ShareMenuColors.borderColor(context)), - ), - radius: BorderRadius.circular(10), - text: FlowyText.regular( - lineHeight: 1.0, - LocaleKeys.shareAction_unPublish.tr(), - textAlign: TextAlign.center, - ), - onTap: onUnPublish, - ), - ); - } -} - -class _PublishWidget extends StatefulWidget { - const _PublishWidget({ - required this.onPublish, - }); - - final void Function(List selectedViews) onPublish; - - @override - State<_PublishWidget> createState() => _PublishWidgetState(); -} - -class _PublishWidgetState extends State<_PublishWidget> { - List _selectedViews = []; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(16), - const _PublishTabHeader(), - const VSpace(16), - // if current view is a database, show the database selector - if (context.read().view.layout.isDatabaseView) ...[ - _PublishDatabaseSelector( - view: context.read().view, - onSelected: (selectedDatabases) { - _selectedViews = selectedDatabases; - }, - ), - const VSpace(16), - ], - PublishButton( - onPublish: () { - if (context.read().view.layout.isDatabaseView) { - // check if any database is selected - if (_selectedViews.isEmpty) { - showToastNotification( - message: LocaleKeys.publish_noDatabaseSelected.tr(), - ); - return; - } - } - - widget.onPublish(_selectedViews); - }, - ), - ], - ); - } -} - -class PublishButton extends StatelessWidget { - const PublishButton({ - super.key, - required this.onPublish, - }); - - final VoidCallback onPublish; - - @override - Widget build(BuildContext context) { - return PrimaryRoundedButton( - text: LocaleKeys.shareAction_publish.tr(), - useIntrinsicWidth: false, - margin: const EdgeInsets.symmetric(vertical: 9.0), - fontSize: 14.0, - figmaLineHeight: 18.0, - onTap: onPublish, - ); - } -} - -class _PublishTabHeader extends StatelessWidget { - const _PublishTabHeader(); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - const FlowySvg(FlowySvgs.share_publish_s), - const HSpace(6), - FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()), - ], - ), - const VSpace(4), - FlowyText.regular( - LocaleKeys.shareAction_publishToTheWebHint.tr(), - fontSize: 12, - maxLines: 3, - color: Theme.of(context).hintColor, - ), - ], - ); - } -} - -class _PublishUrl extends StatefulWidget { - const _PublishUrl({ - required this.namespace, - required this.controller, - required this.onCopy, - required this.onSubmitted, - }); - - final String namespace; - final TextEditingController controller; - final void Function(String url) onCopy; - final void Function(String url) onSubmitted; - - @override - State<_PublishUrl> createState() => _PublishUrlState(); -} - -class _PublishUrlState extends State<_PublishUrl> { - final focusNode = FocusNode(); - bool showSaveButton = false; - - @override - void initState() { - super.initState(); - focusNode.addListener(_onFocusChanged); - } - - void _onFocusChanged() => setState(() => showSaveButton = focusNode.hasFocus); - - @override - void dispose() { - focusNode.removeListener(_onFocusChanged); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 36, - child: FlowyTextField( - autoFocus: false, - controller: widget.controller, - focusNode: focusNode, - enableBorderColor: ShareMenuColors.borderColor(context), - prefixIcon: _buildPrefixIcon(context), - suffixIcon: _buildSuffixIcon(context), - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14, - height: 18.0 / 14.0, - ), - ), - ); - } - - Widget _buildPrefixIcon(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 230), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(8.0), - Flexible( - child: FlowyText.regular( - ShareConstants.buildNamespaceUrl( - nameSpace: '${widget.namespace}/', - ), - fontSize: 14, - figmaLineHeight: 18.0, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - const Padding( - padding: EdgeInsets.symmetric(vertical: 2.0), - child: VerticalDivider( - thickness: 1.0, - width: 1.0, - ), - ), - const HSpace(6.0), - ], - ), - ); - } - - Widget _buildSuffixIcon(BuildContext context) { - return showSaveButton - ? _buildSaveButton(context) - : _buildCopyLinkIcon(context); - } - - Widget _buildSaveButton(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowyText.regular( - LocaleKeys.button_save.tr(), - figmaLineHeight: 18.0, - ), - onTap: () { - widget.onSubmitted(widget.controller.text); - focusNode.unfocus(); - }, - ), - ); - } - - Widget _buildCopyLinkIcon(BuildContext context) { - return FlowyHover( - style: const HoverStyle( - contentMargin: EdgeInsets.all(4), - ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => widget.onCopy(widget.controller.text), - child: Container( - width: 32, - height: 32, - alignment: Alignment.center, - padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - border: Border(left: BorderSide(color: Color(0x141F2329))), - ), - child: const FlowySvg( - FlowySvgs.m_toolbar_link_m, - ), - ), - ), - ); - } -} - -// used to select which database view should be published -class _PublishDatabaseSelector extends StatefulWidget { - const _PublishDatabaseSelector({ - required this.view, - required this.onSelected, - }); - - final ViewPB view; - final void Function(List selectedDatabases) onSelected; - - @override - State<_PublishDatabaseSelector> createState() => - _PublishDatabaseSelectorState(); -} - -class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> { - final PropertyValueNotifier> _databaseStatus = - PropertyValueNotifier>([]); - late final _borderColor = Theme.of(context).hintColor.withValues(alpha: 0.3); - - @override - void initState() { - super.initState(); - - _databaseStatus.addListener(_onDatabaseStatusChanged); - _databaseStatus.value = context - .read() - .state - .tabBars - .map((e) => (e.view, true)) - .toList(); - } - - void _onDatabaseStatusChanged() { - final selectedDatabases = - _databaseStatus.value.where((e) => e.$2).map((e) => e.$1).toList(); - widget.onSelected(selectedDatabases); - } - - @override - void dispose() { - _databaseStatus.removeListener(_onDatabaseStatusChanged); - _databaseStatus.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(color: _borderColor), - borderRadius: BorderRadius.circular(8), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(10), - _buildSelectedDatabaseCount(context), - const VSpace(10), - _buildDivider(context), - const VSpace(10), - ...state.tabBars.map( - (e) => _buildDatabaseSelector(context, e), - ), - ], - ), - ); - }, - ); - } - - Widget _buildDivider(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Divider( - color: _borderColor, - thickness: 1, - height: 1, - ), - ); - } - - Widget _buildSelectedDatabaseCount(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _databaseStatus, - builder: (context, selectedDatabases, child) { - final count = selectedDatabases.where((e) => e.$2).length; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: FlowyText( - LocaleKeys.publish_database.plural(count).tr(), - color: Theme.of(context).hintColor, - fontSize: 13, - ), - ); - }, - ); - } - - Widget _buildDatabaseSelector(BuildContext context, DatabaseTabBar tabBar) { - final isPrimaryDatabase = tabBar.view.id == widget.view.id; - return ValueListenableBuilder( - valueListenable: _databaseStatus, - builder: (context, selectedDatabases, child) { - final isSelected = selectedDatabases.any( - (e) => e.$1.id == tabBar.view.id && e.$2, - ); - return _DatabaseSelectorItem( - tabBar: tabBar, - isSelected: isSelected, - isPrimaryDatabase: isPrimaryDatabase, - onTap: () { - // unable to deselect the primary database - if (isPrimaryDatabase) { - showToastNotification( - message: - LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(), - ); - return; - } - - // toggle the selection status - _databaseStatus.value = _databaseStatus.value - .map( - (e) => - e.$1.id == tabBar.view.id ? (e.$1, !e.$2) : (e.$1, e.$2), - ) - .toList(); - }, - ); - }, - ); - } -} - -class _DatabaseSelectorItem extends StatelessWidget { - const _DatabaseSelectorItem({ - required this.tabBar, - required this.isSelected, - required this.onTap, - required this.isPrimaryDatabase, - }); - - final DatabaseTabBar tabBar; - final bool isSelected; - final VoidCallback onTap; - final bool isPrimaryDatabase; - - @override - Widget build(BuildContext context) { - Widget child = _buildItem(context); - - if (!isPrimaryDatabase) { - child = FlowyHover( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: child, - ), - ); - } else { - child = FlowyTooltip( - message: LocaleKeys.publish_mustSelectPrimaryDatabase.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.forbidden, - child: child, - ), - ); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: child, - ); - } - - Widget _buildItem(BuildContext context) { - final svg = isPrimaryDatabase - ? FlowySvgs.unable_select_s - : isSelected - ? FlowySvgs.check_filled_s - : FlowySvgs.uncheck_s; - final blendMode = isPrimaryDatabase ? BlendMode.srcIn : null; - return Container( - height: 30, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - FlowySvg( - svg, - blendMode: blendMode, - size: const Size.square(18), - ), - const HSpace(9.0), - FlowySvg( - tabBar.view.layout.icon, - size: const Size.square(16), - ), - const HSpace(6.0), - FlowyText.regular( - tabBar.view.name, - fontSize: 14, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart deleted file mode 100644 index e683518526..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_bloc.dart +++ /dev/null @@ -1,448 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/export/document_exporter.dart'; -import 'package:appflowy/workspace/application/settings/share/export_service.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'constants.dart'; - -part 'share_bloc.freezed.dart'; - -class ShareBloc extends Bloc { - ShareBloc({ - required this.view, - }) : super(ShareState.initial()) { - on((event, emit) async { - await event.when( - initial: () async { - viewListener = ViewListener(viewId: view.id) - ..start( - onViewUpdated: (value) { - add(ShareEvent.updateViewName(value.name, value.id)); - }, - onViewMoveToTrash: (p0) { - add(const ShareEvent.setPublishStatus(false)); - }, - ); - - add(const ShareEvent.updatePublishStatus()); - }, - share: (type, path) async => _share( - type, - path, - emit, - ), - publish: (nameSpace, publishName, selectedViewIds) => _publish( - nameSpace, - publishName, - selectedViewIds, - emit, - ), - unPublish: () async => _unpublish(emit), - updatePublishStatus: () async => _updatePublishStatus(emit), - updateViewName: (viewName, viewId) async { - emit( - state.copyWith( - viewName: viewName, - viewId: viewId, - updatePathNameResult: null, - publishResult: null, - unpublishResult: null, - ), - ); - }, - setPublishStatus: (isPublished) { - emit( - state.copyWith( - isPublished: isPublished, - url: isPublished ? state.url : '', - ), - ); - }, - updatePathName: (pathName) async => _updatePathName( - pathName, - emit, - ), - clearPathNameResult: () async { - emit( - state.copyWith( - updatePathNameResult: null, - ), - ); - }, - ); - }); - } - - final ViewPB view; - late final ViewListener viewListener; - - late final documentExporter = DocumentExporter(view); - - @override - Future close() async { - await viewListener.stop(); - return super.close(); - } - - Future _share( - ShareType type, - String? path, - Emitter emit, - ) async { - if (ShareType.unimplemented.contains(type)) { - Log.error('DocumentShareType $type is not implemented'); - return; - } - - emit(state.copyWith(isLoading: true)); - - final result = await _export(type, path); - - emit( - state.copyWith( - isLoading: false, - exportResult: result, - ), - ); - } - - Future _publish( - String nameSpace, - String publishName, - List selectedViewIds, - Emitter emit, - ) async { - // set space name - try { - final result = - await ViewBackendService.getPublishNameSpace().getOrThrow(); - - await ViewBackendService.publish( - view, - name: publishName, - selectedViewIds: selectedViewIds, - ).getOrThrow(); - - emit( - state.copyWith( - isPublished: true, - publishResult: FlowySuccess(null), - unpublishResult: null, - namespace: result.namespace, - pathName: publishName, - url: ShareConstants.buildPublishUrl( - nameSpace: result.namespace, - publishName: publishName, - ), - ), - ); - - Log.info('publish success: ${result.namespace}/$publishName'); - } catch (e) { - Log.error('publish error: $e'); - - emit( - state.copyWith( - isPublished: false, - publishResult: FlowyResult.failure( - FlowyError(msg: 'publish error: $e'), - ), - unpublishResult: null, - url: '', - ), - ); - } - } - - Future _unpublish(Emitter emit) async { - emit( - state.copyWith( - publishResult: null, - unpublishResult: null, - ), - ); - - final result = await ViewBackendService.unpublish(view); - final isPublished = !result.isSuccess; - result.onFailure((f) { - Log.error('unpublish error: $f'); - }); - - emit( - state.copyWith( - isPublished: isPublished, - publishResult: null, - unpublishResult: result, - url: result.fold((_) => '', (_) => state.url), - ), - ); - } - - Future _updatePublishStatus(Emitter emit) async { - final publishInfo = await ViewBackendService.getPublishInfo(view); - final enablePublish = await UserBackendService.getCurrentUserProfile().fold( - (v) => v.workspaceAuthType == AuthTypePB.Server, - (p) => false, - ); - - // skip the "Record not found" error, it's because the view is not published yet - publishInfo.fold( - (s) { - Log.info( - 'get publish info success: $publishInfo for view: ${view.name}(${view.id})', - ); - }, - (f) { - if (![ - ErrorCode.RecordNotFound, - ErrorCode.LocalVersionNotSupport, - ].contains(f.code)) { - Log.info( - 'get publish info failed: $f for view: ${view.name}(${view.id})', - ); - } - }, - ); - - String workspaceId = state.workspaceId; - if (workspaceId.isEmpty) { - workspaceId = await UserBackendService.getCurrentWorkspace().fold( - (s) => s.id, - (f) => '', - ); - } - - final (isPublished, namespace, pathName, url) = publishInfo.fold( - (s) { - return ( - // if the unpublishedAtTimestampSec is not set, it means the view is not unpublished. - !s.hasUnpublishedAtTimestampSec(), - s.namespace, - s.publishName, - ShareConstants.buildPublishUrl( - nameSpace: s.namespace, - publishName: s.publishName, - ), - ); - }, - (f) => (false, '', '', ''), - ); - - emit( - state.copyWith( - isPublished: isPublished, - namespace: namespace, - pathName: pathName, - url: url, - viewName: view.name, - enablePublish: enablePublish, - workspaceId: workspaceId, - viewId: view.id, - ), - ); - } - - Future _updatePathName( - String pathName, - Emitter emit, - ) async { - emit( - state.copyWith( - updatePathNameResult: null, - ), - ); - - if (pathName.isEmpty) { - emit( - state.copyWith( - updatePathNameResult: FlowyResult.failure( - FlowyError( - code: ErrorCode.ViewNameInvalid, - msg: 'Path name is invalid', - ), - ), - ), - ); - return; - } - - final request = SetPublishNamePB() - ..viewId = view.id - ..newName = pathName; - final result = await FolderEventSetPublishName(request).send(); - emit( - state.copyWith( - updatePathNameResult: result, - publishResult: null, - unpublishResult: null, - pathName: result.fold( - (_) => pathName, - (f) => state.pathName, - ), - url: result.fold( - (s) => ShareConstants.buildPublishUrl( - nameSpace: state.namespace, - publishName: pathName, - ), - (f) => state.url, - ), - ), - ); - } - - Future> _export( - ShareType type, - String? path, - ) async { - final FlowyResult result; - if (type == ShareType.csv) { - final exportResult = await BackendExportService.exportDatabaseAsCSV( - view.id, - ); - result = exportResult.fold( - (s) => FlowyResult.success(s.data), - (f) => FlowyResult.failure(f), - ); - } else if (type == ShareType.rawDatabaseData) { - final exportResult = await BackendExportService.exportDatabaseAsRawData( - view.id, - ); - result = exportResult.fold( - (s) => FlowyResult.success(s.data), - (f) => FlowyResult.failure(f), - ); - } else { - result = - await documentExporter.export(type.documentExportType, path: path); - } - return result.fold( - (s) { - if (path != null) { - switch (type) { - case ShareType.html: - case ShareType.csv: - case ShareType.json: - case ShareType.rawDatabaseData: - File(path).writeAsStringSync(s); - return FlowyResult.success(type); - case ShareType.markdown: - return FlowyResult.success(type); - default: - break; - } - } - return FlowyResult.failure(FlowyError()); - }, - (f) => FlowyResult.failure(f), - ); - } -} - -enum ShareType { - // available in document - markdown, - html, - text, - link, - json, - - // only available in database - csv, - rawDatabaseData; - - static List get unimplemented => [link]; - - DocumentExportType get documentExportType { - switch (this) { - case ShareType.markdown: - return DocumentExportType.markdown; - case ShareType.html: - return DocumentExportType.html; - case ShareType.text: - return DocumentExportType.text; - case ShareType.json: - return DocumentExportType.json; - case ShareType.csv: - throw UnsupportedError('DocumentShareType.csv is not supported'); - case ShareType.link: - throw UnsupportedError('DocumentShareType.link is not supported'); - case ShareType.rawDatabaseData: - throw UnsupportedError( - 'DocumentShareType.rawDatabaseData is not supported', - ); - } - } -} - -@freezed -class ShareEvent with _$ShareEvent { - const factory ShareEvent.initial() = _Initial; - - const factory ShareEvent.share( - ShareType type, - String? path, - ) = _Share; - - const factory ShareEvent.publish( - String nameSpace, - String pageId, - List selectedViewIds, - ) = _Publish; - - const factory ShareEvent.unPublish() = _UnPublish; - - const factory ShareEvent.updateViewName(String name, String viewId) = - _UpdateViewName; - - const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus; - - const factory ShareEvent.setPublishStatus(bool isPublished) = - _SetPublishStatus; - - const factory ShareEvent.updatePathName(String pathName) = _UpdatePathName; - - const factory ShareEvent.clearPathNameResult() = _ClearPathNameResult; -} - -@freezed -class ShareState with _$ShareState { - const factory ShareState({ - required bool isPublished, - required bool isLoading, - required String url, - required String viewName, - required bool enablePublish, - FlowyResult? exportResult, - FlowyResult? publishResult, - FlowyResult? unpublishResult, - FlowyResult? updatePathNameResult, - required String viewId, - required String workspaceId, - required String namespace, - required String pathName, - }) = _ShareState; - - factory ShareState.initial() => const ShareState( - isLoading: false, - isPublished: false, - enablePublish: true, - url: '', - viewName: '', - viewId: '', - workspaceId: '', - namespace: '', - pathName: '', - ); -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart deleted file mode 100644 index 9020441b4e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_button.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; -import 'package:appflowy/plugins/shared/share/_shared.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/plugins/shared/share/share_menu.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ShareButton extends StatelessWidget { - const ShareButton({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => - getIt(param1: view)..add(const ShareEvent.initial()), - ), - if (view.layout.isDatabaseView) - BlocProvider( - create: (context) => DatabaseTabBarBloc( - view: view, - compactModeId: view.id, - enableCompactMode: false, - )..add(const DatabaseTabBarEvent.initial()), - ), - ], - child: BlocListener( - listener: (context, state) { - if (!state.isLoading && state.exportResult != null) { - state.exportResult!.fold( - (data) => _handleExportSuccess(context, data), - (error) => _handleExportError(context, error), - ); - } - }, - child: BlocBuilder( - builder: (context, state) { - final tabs = [ - if (state.enablePublish) ...[ - // share the same permission with publish - ShareMenuTab.share, - ShareMenuTab.publish, - ], - ShareMenuTab.exportAs, - ]; - - return ShareMenuButton(tabs: tabs); - }, - ), - ), - ); - } - - void _handleExportSuccess(BuildContext context, ShareType shareType) { - switch (shareType) { - case ShareType.markdown: - case ShareType.html: - case ShareType.csv: - showToastNotification( - message: LocaleKeys.settings_files_exportFileSuccess.tr(), - ); - break; - default: - break; - } - } - - void _handleExportError(BuildContext context, FlowyError error) { - showToastNotification( - message: - '${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}', - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart deleted file mode 100644 index 4decb1c092..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_menu.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; -import 'package:appflowy/plugins/shared/share/export_tab.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/plugins/shared/share/share_tab.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'publish_tab.dart'; - -enum ShareMenuTab { - share, - publish, - exportAs; - - String get i18n { - switch (this) { - case ShareMenuTab.share: - return LocaleKeys.shareAction_shareTab.tr(); - case ShareMenuTab.publish: - return LocaleKeys.shareAction_publishTab.tr(); - case ShareMenuTab.exportAs: - return LocaleKeys.shareAction_exportAsTab.tr(); - } - } -} - -class ShareMenu extends StatefulWidget { - const ShareMenu({ - super.key, - required this.tabs, - required this.viewName, - }); - - final List tabs; - final String viewName; - - @override - State createState() => _ShareMenuState(); -} - -class _ShareMenuState extends State - with SingleTickerProviderStateMixin { - late ShareMenuTab selectedTab = widget.tabs.first; - late final tabController = TabController( - length: widget.tabs.length, - vsync: this, - initialIndex: widget.tabs.indexOf(selectedTab), - ); - - @override - Widget build(BuildContext context) { - if (widget.tabs.isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const VSpace(10), - Container( - alignment: Alignment.centerLeft, - height: 30, - child: _buildTabBar(context), - ), - Divider( - color: Theme.of(context).dividerColor, - height: 1, - thickness: 1, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 14.0), - child: _buildTab(context), - ), - const VSpace(20), - ], - ); - } - - @override - void dispose() { - tabController.dispose(); - super.dispose(); - } - - Widget _buildTabBar(BuildContext context) { - final children = [ - for (final tab in widget.tabs) - Padding( - padding: const EdgeInsets.only(bottom: 10), - child: _Segment( - tab: tab, - isSelected: selectedTab == tab, - ), - ), - ]; - return TabBar( - indicatorSize: TabBarIndicatorSize.label, - indicator: RoundUnderlineTabIndicator( - width: 68.0, - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 3, - ), - insets: const EdgeInsets.only(bottom: -2), - ), - isScrollable: true, - controller: tabController, - tabs: children, - onTap: (index) { - setState(() { - selectedTab = widget.tabs[index]; - }); - }, - ); - } - - Widget _buildTab(BuildContext context) { - switch (selectedTab) { - case ShareMenuTab.publish: - return PublishTab( - viewName: widget.viewName, - ); - case ShareMenuTab.exportAs: - return const ExportTab(); - case ShareMenuTab.share: - return const ShareTab(); - } - } -} - -class _Segment extends StatefulWidget { - const _Segment({ - required this.tab, - required this.isSelected, - }); - - final bool isSelected; - final ShareMenuTab tab; - - @override - State<_Segment> createState() => _SegmentState(); -} - -class _SegmentState extends State<_Segment> { - bool isHovered = false; - - @override - Widget build(BuildContext context) { - Color? textColor = Theme.of(context).hintColor; - if (isHovered) { - textColor = const Color(0xFF00BCF0); - } else if (widget.isSelected) { - textColor = null; - } - - Widget child = MouseRegion( - onEnter: (_) => setState(() => isHovered = true), - onExit: (_) => setState(() => isHovered = false), - child: FlowyText( - widget.tab.i18n, - textAlign: TextAlign.center, - color: textColor, - ), - ); - - if (widget.tab == ShareMenuTab.publish) { - final isPublished = context.watch().state.isPublished; - // show checkmark icon if published - if (isPublished) { - child = Row( - children: [ - const FlowySvg( - FlowySvgs.published_checkmark_s, - blendMode: null, - ), - const HSpace(6), - child, - ], - ); - } - } - - return child; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart deleted file mode 100644 index 190fe9ddd8..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/share_tab.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/share_bloc.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.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 'constants.dart'; - -class ShareTab extends StatelessWidget { - const ShareTab({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return const Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - VSpace(18), - _ShareTabHeader(), - VSpace(2), - _ShareTabDescription(), - VSpace(14), - _ShareTabContent(), - ], - ); - } -} - -class _ShareTabHeader extends StatelessWidget { - const _ShareTabHeader(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const FlowySvg(FlowySvgs.share_tab_icon_s), - const HSpace(6), - FlowyText.medium( - LocaleKeys.shareAction_shareTabTitle.tr(), - figmaLineHeight: 18.0, - ), - ], - ); - } -} - -class _ShareTabDescription extends StatelessWidget { - const _ShareTabDescription(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 2.0), - child: FlowyText.regular( - LocaleKeys.shareAction_shareTabDescription.tr(), - fontSize: 13.0, - figmaLineHeight: 18.0, - color: Theme.of(context).hintColor, - ), - ); - } -} - -class _ShareTabContent extends StatelessWidget { - const _ShareTabContent(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final shareUrl = ShareConstants.buildShareUrl( - workspaceId: state.workspaceId, - viewId: state.viewId, - ); - return Row( - children: [ - Expanded( - child: SizedBox( - height: 36, - child: FlowyTextField( - text: shareUrl, // todo: add workspace id + view id - readOnly: true, - borderRadius: BorderRadius.circular(10), - ), - ), - ), - const HSpace(8.0), - PrimaryRoundedButton( - margin: const EdgeInsets.symmetric( - vertical: 9.0, - horizontal: 14.0, - ), - text: LocaleKeys.button_copyLink.tr(), - figmaLineHeight: 18.0, - leftIcon: FlowySvg( - FlowySvgs.share_tab_copy_s, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: () => _copy(context, shareUrl), - ), - ], - ); - }, - ); - } - - void _copy(BuildContext context, String url) { - getIt().setData( - ClipboardServiceData(plainText: url), - ); - - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart index 24755b4aa7..64ce2940c5 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_bloc.dart @@ -42,7 +42,7 @@ class TrashBloc extends Bloc { emit(state.copyWith(objects: e.trash)); }, putback: (e) async { - final result = await TrashService.putback(e.trashId); + final result = await _service.putback(e.trashId); await _handleResult(result, emit); }, delete: (e) async { diff --git a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart index 69fe613d82..b6d319009b 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/application/trash_service.dart @@ -10,7 +10,7 @@ class TrashService { return FolderEventListTrashItems().send(); } - static Future> putback(String trashId) { + Future> putback(String trashId) { final id = TrashIdPB.create()..id = trashId; return FolderEventRestoreTrashItem(id).send(); diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart index f3fb4a8bbe..e2f312373e 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash.dart @@ -1,18 +1,18 @@ +export "./src/sizes.dart"; +export "./src/trash_cell.dart"; +export "./src/trash_header.dart"; + import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'trash_page.dart'; -export "./src/sizes.dart"; -export "./src/trash_cell.dart"; -export "./src/trash_header.dart"; - class TrashPluginBuilder extends PluginBuilder { @override Plugin build(dynamic data) { @@ -53,14 +53,11 @@ class TrashPlugin extends Plugin { } class TrashPluginDisplay extends PluginWidgetBuilder { - @override - String? get viewName => LocaleKeys.trash_text.tr(); - @override Widget get leftBarItem => FlowyText.medium(LocaleKeys.trash_text.tr()); @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem; + Widget tabBarItem(String pluginId) => leftBarItem; @override Widget? get rightBarItem => null; @@ -69,9 +66,10 @@ class TrashPluginDisplay extends PluginWidgetBuilder { Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }) => - const TrashPage(key: ValueKey('TrashPage')); + const TrashPage( + key: ValueKey('TrashPage'), + ); @override List get navigationItems => [this]; diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart index 16e82a3089..b50f95342a 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart @@ -1,11 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/trash/src/sizes.dart'; import 'package:appflowy/plugins/trash/src/trash_header.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -14,6 +12,7 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -102,37 +101,33 @@ class _TrashPageState extends State { const Spacer(), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.trash_restoreAll.tr(), - lineHeight: 1.0, - ), + text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()), leftIcon: const FlowySvg(FlowySvgs.restore_s), - onTap: () => showCancelAndConfirmDialog( - context: context, - confirmLabel: LocaleKeys.trash_restore.tr(), - title: LocaleKeys.trash_confirmRestoreAll_title.tr(), - description: LocaleKeys.trash_confirmRestoreAll_caption.tr(), - onConfirm: () => context - .read() - .add(const TrashEvent.restoreAll()), - ), + onTap: () { + NavigatorAlertDialog( + title: LocaleKeys.trash_confirmRestoreAll_title.tr(), + confirm: () { + context + .read() + .add(const TrashEvent.restoreAll()); + }, + ).show(context); + }, ), ), const HSpace(6), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium( - LocaleKeys.trash_deleteAll.tr(), - lineHeight: 1.0, - ), + text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()), leftIcon: const FlowySvg(FlowySvgs.delete_s), - onTap: () => showConfirmDeletionDialog( - context: context, - name: LocaleKeys.trash_confirmDeleteAll_title.tr(), - description: LocaleKeys.trash_confirmDeleteAll_caption.tr(), - onConfirm: () => - context.read().add(const TrashEvent.deleteAll()), - ), + onTap: () { + NavigatorAlertDialog( + title: LocaleKeys.trash_confirmDeleteAll_title.tr(), + confirm: () { + context.read().add(const TrashEvent.deleteAll()); + }, + ).show(context); + }, ), ), ], @@ -157,26 +152,24 @@ class _TrashPageState extends State { height: 42, child: TrashCell( object: object, - onRestore: () => showCancelAndConfirmDialog( - context: context, - title: - LocaleKeys.trash_restorePage_title.tr(args: [object.name]), - description: LocaleKeys.trash_restorePage_caption.tr(), - confirmLabel: LocaleKeys.trash_restore.tr(), - onConfirm: () => context - .read() - .add(TrashEvent.putback(object.id)), - ), - onDelete: () => showConfirmDeletionDialog( - context: context, - name: object.name.trim().isEmpty - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : object.name, - description: - LocaleKeys.deletePagePrompt_deletePermanentDescription.tr(), - onConfirm: () => - context.read().add(TrashEvent.delete(object)), - ), + onRestore: () { + NavigatorAlertDialog( + title: LocaleKeys.deletePagePrompt_restore.tr(), + confirm: () { + context + .read() + .add(TrashEvent.putback(object.id)); + }, + ).show(context); + }, + onDelete: () { + NavigatorAlertDialog( + title: LocaleKeys.deletePagePrompt_deletePermanent.tr(), + confirm: () { + context.read().add(TrashEvent.delete(object)); + }, + ).show(context); + }, ), ); }, diff --git a/frontend/appflowy_flutter/lib/shared/af_image.dart b/frontend/appflowy_flutter/lib/shared/af_image.dart deleted file mode 100644 index 702c0f7764..0000000000 --- a/frontend/appflowy_flutter/lib/shared/af_image.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; - -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/file_entities.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; - -class AFImage extends StatelessWidget { - const AFImage({ - super.key, - required this.url, - required this.uploadType, - this.height, - this.width, - this.fit = BoxFit.cover, - this.userProfile, - this.borderRadius, - }) : assert( - uploadType != FileUploadTypePB.CloudFile || userProfile != null, - 'userProfile must be provided for accessing files from AF Cloud', - ); - - final String url; - final FileUploadTypePB uploadType; - final double? height; - final double? width; - final BoxFit fit; - final UserProfilePB? userProfile; - final BorderRadius? borderRadius; - - @override - Widget build(BuildContext context) { - if (uploadType == FileUploadTypePB.CloudFile && userProfile == null) { - return const SizedBox.shrink(); - } - - Widget child; - if (uploadType == FileUploadTypePB.NetworkFile) { - child = Image.network( - url, - height: height, - width: width, - fit: fit, - isAntiAlias: true, - errorBuilder: (context, error, stackTrace) { - return const SizedBox.shrink(); - }, - ); - } else if (uploadType == FileUploadTypePB.LocalFile) { - child = Image.file( - File(url), - height: height, - width: width, - fit: fit, - isAntiAlias: true, - errorBuilder: (context, error, stackTrace) { - return const SizedBox.shrink(); - }, - ); - } else { - child = FlowyNetworkImage( - url: url, - userProfilePB: userProfile, - height: height, - width: width, - errorWidgetBuilder: (context, url, error) { - return const SizedBox.shrink(); - }, - ); - } - - if (borderRadius != null) { - child = ClipRRect( - clipBehavior: Clip.antiAliasWithSaveLayer, - borderRadius: borderRadius!, - child: child, - ); - } - - return child; - } -} diff --git a/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart b/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart deleted file mode 100644 index 2632c22d49..0000000000 --- a/frontend/appflowy_flutter/lib/shared/af_user_profile_extension.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; - -extension UserProfilePBExtension on UserProfilePB { - String? get authToken { - try { - final map = jsonDecode(token) as Map; - return map['access_token'] as String?; - } catch (e) { - Log.error('Failed to decode auth token: $e'); - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index 090db27ddc..d37b2e0838 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -1,21 +1,17 @@ import 'dart:convert'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/uuid.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:string_validator/string_validator.dart'; /// This widget handles the downloading and caching of either internal or network images. +/// /// It will append the access token to the URL if the URL is internal. -class FlowyNetworkImage extends StatefulWidget { +class FlowyNetworkImage extends StatelessWidget { const FlowyNetworkImage({ super.key, this.userProfilePB, @@ -25,252 +21,57 @@ class FlowyNetworkImage extends StatefulWidget { this.progressIndicatorBuilder, this.errorWidgetBuilder, required this.url, - this.maxRetries = 5, - this.retryDuration = const Duration(seconds: 6), - this.retryErrorCodes = const {404}, - this.onImageLoaded, }); - /// The URL of the image. - final String url; - - /// The width of the image. - final double? width; - - /// The height of the image. - final double? height; - - /// The fit of the image. - final BoxFit fit; - - /// The user profile. - /// - /// If the userProfilePB is not null, the image will be downloaded with the access token. final UserProfilePB? userProfilePB; - - /// The progress indicator builder. + final String url; + final double? width; + final double? height; + final BoxFit fit; final ProgressIndicatorBuilder? progressIndicatorBuilder; - - /// The error widget builder. final LoadingErrorWidgetBuilder? errorWidgetBuilder; - /// Retry loading the image if it fails. - final int maxRetries; - - /// Retry duration - final Duration retryDuration; - - /// Retry error codes. - final Set retryErrorCodes; - - final void Function(bool isImageInCache)? onImageLoaded; - - @override - FlowyNetworkImageState createState() => FlowyNetworkImageState(); -} - -class FlowyNetworkImageState extends State { - final manager = CustomImageCacheManager(); - final retryCounter = FlowyNetworkRetryCounter(); - - // This is used to clear the retry count when the widget is disposed in case of the url is the same. - String? retryTag; - - @override - void initState() { - super.initState(); - - assert(isURL(widget.url)); - - if (widget.url.isAppFlowyCloudUrl) { - assert( - widget.userProfilePB != null && widget.userProfilePB!.token.isNotEmpty, - ); - } - - retryTag = retryCounter.add(widget.url); - - manager.getFileFromCache(widget.url).then((file) { - widget.onImageLoaded?.call( - file != null && - file.file.path.isNotEmpty && - file.originalUrl == widget.url, - ); - }); - } - - @override - void reassemble() { - super.reassemble(); - - if (retryTag != null) { - retryCounter.clear( - tag: retryTag!, - url: widget.url, - maxRetries: widget.maxRetries, - ); - } - } - - @override - void dispose() { - if (retryTag != null) { - retryCounter.clear( - tag: retryTag!, - url: widget.url, - maxRetries: widget.maxRetries, - ); - } - - super.dispose(); - } - @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: retryCounter, - builder: (context, child) { - final retryCount = retryCounter.getRetryCount(widget.url); - return CachedNetworkImage( - key: ValueKey('${widget.url}_$retryCount'), - cacheManager: manager, - httpHeaders: _buildRequestHeader(), - imageUrl: widget.url, - fit: widget.fit, - width: widget.width, - height: widget.height, - progressIndicatorBuilder: widget.progressIndicatorBuilder, - errorWidget: _errorWidgetBuilder, - errorListener: (value) async { - Log.error( - 'Unable to load image: ${value.toString()} - retryCount: $retryCount', - ); + assert(isURL(url)); - // clear the cache and retry - await manager.removeFile(widget.url); - _retryLoadImage(); - }, - ); + if (url.isAppFlowyCloudUrl) { + assert(userProfilePB != null && userProfilePB!.token.isNotEmpty); + } + + final manager = CustomImageCacheManager(); + + return CachedNetworkImage( + cacheManager: manager, + httpHeaders: _header(), + imageUrl: url, + fit: fit, + width: width, + height: height, + progressIndicatorBuilder: progressIndicatorBuilder, + errorWidget: (context, url, error) => + errorWidgetBuilder?.call(context, url, error) ?? + const SizedBox.shrink(), + errorListener: (value) { + // try to clear the image cache. + manager.removeFile(url); + + Log.error(value.toString()); }, ); } - /// if the error is 404 and the retry count is less than the max retries, it return a loading indicator. - Widget _errorWidgetBuilder(BuildContext context, String url, Object error) { - final retryCount = retryCounter.getRetryCount(url); - if (error is HttpExceptionWithStatus) { - if (widget.retryErrorCodes.contains(error.statusCode) && - retryCount < widget.maxRetries) { - final fakeDownloadProgress = DownloadProgress(url, null, 0); - return widget.progressIndicatorBuilder?.call( - context, - url, - fakeDownloadProgress, - ) ?? - const Center( - child: _SensitiveContent(), - ); - } - - if (error.statusCode == 422) { - // Unprocessable Entity: Used when the server understands the request but cannot process it due to - //semantic issues (e.g., sensitive keywords). - return const _SensitiveContent(); - } - } - - return widget.errorWidgetBuilder?.call(context, url, error) ?? - const SizedBox.shrink(); - } - - Map _buildRequestHeader() { + Map _header() { final header = {}; - final token = widget.userProfilePB?.token; + final token = userProfilePB?.token; if (token != null) { try { final decodedToken = jsonDecode(token); header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; } catch (e) { - Log.error('Unable to decode token: $e'); + Log.error('unable to decode token: $e'); } } return header; } - - void _retryLoadImage() { - final retryCount = retryCounter.getRetryCount(widget.url); - if (retryCount < widget.maxRetries) { - Future.delayed(widget.retryDuration, () { - Log.debug( - 'Retry load image: ${widget.url}, retry count: $retryCount', - ); - // Increment the retry count for the URL to trigger the image rebuild. - retryCounter.increment(widget.url); - }); - } - } -} - -/// This class is used to count the number of retries for a given URL. -@visibleForTesting -class FlowyNetworkRetryCounter with ChangeNotifier { - FlowyNetworkRetryCounter._(); - - factory FlowyNetworkRetryCounter() => _instance; - static final _instance = FlowyNetworkRetryCounter._(); - - final Map _values = {}; - Map get values => {..._values}; - - /// Get the retry count for a given URL. - int getRetryCount(String url) => _values[url] ?? 0; - - /// Add a new URL to the retry counter. Don't call notifyListeners() here. - /// - /// This function will return a tag, use it to clear the retry count. - /// Because the url may be the same, we need to add a unique tag to the url. - String add(String url) { - _values.putIfAbsent(url, () => 0); - return url + uuid(); - } - - /// Increment the retry count for a given URL. - void increment(String url) { - final count = _values[url]; - if (count == null) { - _values[url] = 1; - } else { - _values[url] = count + 1; - } - notifyListeners(); - } - - /// Clear the retry count for a given tag. - void clear({ - required String tag, - required String url, - int? maxRetries, - }) { - _values.remove(tag); - - final retryCount = _values[url]; - if (maxRetries == null || - (retryCount != null && retryCount >= maxRetries)) { - _values.remove(url); - } - } - - /// Reset the retry counter. - void reset() { - _values.clear(); - } -} - -class _SensitiveContent extends StatelessWidget { - const _SensitiveContent(); - - @override - Widget build(BuildContext context) { - return FlowyText(LocaleKeys.ai_contentPolicyViolation.tr()); - } } diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart deleted file mode 100644 index 33c3bb2c0a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_svg.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; - -import 'custom_image_cache_manager.dart'; - -class FlowyNetworkSvg extends StatefulWidget { - FlowyNetworkSvg( - this.url, { - Key? key, - this.cacheKey, - this.placeholder, - this.errorWidget, - this.width, - this.height, - this.headers, - this.fit = BoxFit.contain, - this.alignment = Alignment.center, - this.matchTextDirection = false, - this.allowDrawingOutsideViewBox = false, - this.semanticsLabel, - this.excludeFromSemantics = false, - this.theme = const SvgTheme(), - this.fadeDuration = Duration.zero, - this.colorFilter, - this.placeholderBuilder, - BaseCacheManager? cacheManager, - }) : cacheManager = cacheManager ?? CustomImageCacheManager(), - super(key: key ?? ValueKey(url)); - - final String url; - final String? cacheKey; - final Widget? placeholder; - final Widget? errorWidget; - final double? width; - final double? height; - final ColorFilter? colorFilter; - final Map? headers; - final BoxFit fit; - final AlignmentGeometry alignment; - final bool matchTextDirection; - final bool allowDrawingOutsideViewBox; - final String? semanticsLabel; - final bool excludeFromSemantics; - final SvgTheme theme; - final Duration fadeDuration; - final WidgetBuilder? placeholderBuilder; - final BaseCacheManager cacheManager; - - @override - State createState() => _FlowyNetworkSvgState(); - - static Future preCache( - String imageUrl, { - String? cacheKey, - BaseCacheManager? cacheManager, - }) { - final key = cacheKey ?? _generateKeyFromUrl(imageUrl); - cacheManager ??= DefaultCacheManager(); - return cacheManager.downloadFile(key); - } - - static Future clearCacheForUrl( - String imageUrl, { - String? cacheKey, - BaseCacheManager? cacheManager, - }) { - final key = cacheKey ?? _generateKeyFromUrl(imageUrl); - cacheManager ??= DefaultCacheManager(); - return cacheManager.removeFile(key); - } - - static Future clearCache({BaseCacheManager? cacheManager}) { - cacheManager ??= DefaultCacheManager(); - return cacheManager.emptyCache(); - } - - static String _generateKeyFromUrl(String url) => url.split('?').first; -} - -class _FlowyNetworkSvgState extends State - with SingleTickerProviderStateMixin { - bool _isLoading = false; - bool _isError = false; - File? _imageFile; - late String _cacheKey; - - late final AnimationController _controller; - late final Animation _animation; - - @override - void initState() { - super.initState(); - _cacheKey = - widget.cacheKey ?? FlowyNetworkSvg._generateKeyFromUrl(widget.url); - _controller = AnimationController( - vsync: this, - duration: widget.fadeDuration, - ); - _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); - _loadImage(); - } - - Future _loadImage() async { - try { - _setToLoadingAfter15MsIfNeeded(); - - var file = (await widget.cacheManager.getFileFromMemory(_cacheKey))?.file; - - file ??= await widget.cacheManager.getSingleFile( - widget.url, - key: _cacheKey, - headers: widget.headers ?? {}, - ); - - _imageFile = file; - _isLoading = false; - - _setState(); - - await _controller.forward(); - } catch (e) { - log('CachedNetworkSVGImage: $e'); - - _isError = true; - _isLoading = false; - - _setState(); - } - } - - void _setToLoadingAfter15MsIfNeeded() => Future.delayed( - const Duration(milliseconds: 15), - () { - if (!_isLoading && _imageFile == null && !_isError) { - _isLoading = true; - _setState(); - } - }, - ); - - void _setState() => mounted ? setState(() {}) : null; - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - width: widget.width, - height: widget.height, - child: _buildImage(), - ); - } - - Widget _buildImage() { - if (_isLoading) return _buildPlaceholderWidget(); - - if (_isError) return _buildErrorWidget(); - - return FadeTransition( - opacity: _animation, - child: _buildSVGImage(), - ); - } - - Widget _buildPlaceholderWidget() => - Center(child: widget.placeholder ?? const SizedBox()); - - Widget _buildErrorWidget() => - Center(child: widget.errorWidget ?? const SizedBox()); - - Widget _buildSVGImage() { - if (_imageFile == null) return const SizedBox(); - - return SvgPicture.file( - _imageFile!, - fit: widget.fit, - width: widget.width, - height: widget.height, - alignment: widget.alignment, - matchTextDirection: widget.matchTextDirection, - allowDrawingOutsideViewBox: widget.allowDrawingOutsideViewBox, - colorFilter: widget.colorFilter, - semanticsLabel: widget.semanticsLabel, - excludeFromSemantics: widget.excludeFromSemantics, - placeholderBuilder: widget.placeholderBuilder, - theme: widget.theme, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/clipboard_state.dart b/frontend/appflowy_flutter/lib/shared/clipboard_state.dart deleted file mode 100644 index dfee65e420..0000000000 --- a/frontend/appflowy_flutter/lib/shared/clipboard_state.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/foundation.dart'; - -/// Class to hold the state of the Clipboard. -/// -/// Essentially for document in-app json paste, we need to be able -/// to differentiate between a cut-paste and a copy-paste. -/// -/// When a cut-pase has occurred, the next paste operation should be -/// seen as a copy-paste. -/// -class ClipboardState { - ClipboardState(); - - bool _isCut = false; - - bool get isCut => _isCut; - - final ValueNotifier isHandlingPasteNotifier = ValueNotifier(false); - bool get isHandlingPaste => isHandlingPasteNotifier.value; - - final Set _handlingPasteIds = {}; - - void dispose() { - isHandlingPasteNotifier.dispose(); - } - - void didCut() { - _isCut = true; - } - - void didPaste() { - _isCut = false; - } - - void startHandlingPaste(String id) { - _handlingPasteIds.add(id); - isHandlingPasteNotifier.value = true; - } - - void endHandlingPaste(String id) { - _handlingPasteIds.remove(id); - if (_handlingPasteIds.isEmpty) { - isHandlingPasteNotifier.value = false; - } - } -} diff --git a/frontend/appflowy_flutter/lib/shared/colors.dart b/frontend/appflowy_flutter/lib/shared/colors.dart deleted file mode 100644 index 4f6e1ecfeb..0000000000 --- a/frontend/appflowy_flutter/lib/shared/colors.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -extension SharedColors on BuildContext { - Color get proPrimaryColor { - return Theme.of(this).isLightMode - ? const Color(0xFF653E8C) - : const Color(0xFFE8E2EE); - } - - Color get proSecondaryColor { - return Theme.of(this).isLightMode - ? const Color(0xFFE8E2EE) - : const Color(0xFF653E8C); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/error_code/error_code_map.dart b/frontend/appflowy_flutter/lib/shared/error_code/error_code_map.dart deleted file mode 100644 index 2d54c93cbe..0000000000 --- a/frontend/appflowy_flutter/lib/shared/error_code/error_code_map.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; - -extension PublishNameErrorCodeMap on ErrorCode { - String? get publishErrorMessage { - return switch (this) { - ErrorCode.PublishNameAlreadyExists => - LocaleKeys.settings_sites_error_publishNameAlreadyInUse.tr(), - ErrorCode.PublishNameInvalidCharacter => LocaleKeys - .settings_sites_error_publishNameContainsInvalidCharacters - .tr(), - ErrorCode.PublishNameTooLong => - LocaleKeys.settings_sites_error_publishNameTooLong.tr(), - ErrorCode.UserUnauthorized => - LocaleKeys.settings_sites_error_publishPermissionDenied.tr(), - ErrorCode.ViewNameInvalid => - LocaleKeys.settings_sites_error_publishNameCannotBeEmpty.tr(), - _ => null, - }; - } -} - -extension DomainErrorCodeMap on ErrorCode { - String? get namespaceErrorMessage { - return switch (this) { - ErrorCode.CustomNamespaceRequirePlanUpgrade => - LocaleKeys.settings_sites_error_proPlanLimitation.tr(), - ErrorCode.CustomNamespaceAlreadyTaken => - LocaleKeys.settings_sites_error_namespaceAlreadyInUse.tr(), - ErrorCode.InvalidNamespace || - ErrorCode.InvalidRequest => - LocaleKeys.settings_sites_error_invalidNamespace.tr(), - ErrorCode.CustomNamespaceTooLong => - LocaleKeys.settings_sites_error_namespaceTooLong.tr(), - ErrorCode.CustomNamespaceTooShort => - LocaleKeys.settings_sites_error_namespaceTooShort.tr(), - ErrorCode.CustomNamespaceReserved => - LocaleKeys.settings_sites_error_namespaceIsReserved.tr(), - ErrorCode.CustomNamespaceInvalidCharacter => - LocaleKeys.settings_sites_error_namespaceContainsInvalidCharacters.tr(), - _ => null, - }; - } -} diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index 7ea66076df..31e61ebb08 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -35,12 +35,6 @@ enum FeatureFlag { // used for controlling whether to show plan+billing options in settings planBilling, - // used for space design - spaceDesign, - - // used for the inline sub-page mention - inlineSubPageMention, - // used for ignore the conflicted feature flag unknown; @@ -94,9 +88,6 @@ enum FeatureFlag { bool get isOn { if ([ - FeatureFlag.planBilling, - // release this feature in version 0.6.1 - FeatureFlag.spaceDesign, // release this feature in version 0.5.9 FeatureFlag.search, // release this feature in version 0.5.6 @@ -105,7 +96,6 @@ enum FeatureFlag { // release this feature in version 0.5.4 FeatureFlag.syncDatabase, FeatureFlag.syncDocument, - FeatureFlag.inlineSubPageMention, ].contains(this)) { return true; } @@ -115,17 +105,15 @@ enum FeatureFlag { } switch (this) { + case FeatureFlag.collaborativeWorkspace: + case FeatureFlag.membersSettings: case FeatureFlag.planBilling: + case FeatureFlag.unknown: + return false; case FeatureFlag.search: case FeatureFlag.syncDocument: case FeatureFlag.syncDatabase: - case FeatureFlag.spaceDesign: - case FeatureFlag.inlineSubPageMention: return true; - case FeatureFlag.collaborativeWorkspace: - case FeatureFlag.membersSettings: - case FeatureFlag.unknown: - return false; } } @@ -143,10 +131,6 @@ enum FeatureFlag { return 'if it\'s on, the command palette and search button will be available'; case FeatureFlag.planBilling: return 'if it\'s on, plan and billing pages will be available in Settings'; - case FeatureFlag.spaceDesign: - return 'if it\'s on, the space design feature will be available'; - case FeatureFlag.inlineSubPageMention: - return 'if it\'s on, the inline sub-page mention feature will be available'; case FeatureFlag.unknown: return ''; } diff --git a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart b/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart deleted file mode 100644 index 5942271206..0000000000 --- a/frontend/appflowy_flutter/lib/shared/flowy_error_page.dart +++ /dev/null @@ -1,166 +0,0 @@ -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/base/animated_gesture.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class AppFlowyErrorPage extends StatelessWidget { - const AppFlowyErrorPage({ - super.key, - this.error, - }); - - final FlowyError? error; - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isMobile) { - return _MobileSyncErrorPage(error: error); - } else { - return _DesktopSyncErrorPage(error: error); - } - } -} - -class _MobileSyncErrorPage extends StatelessWidget { - const _MobileSyncErrorPage({ - this.error, - }); - - final FlowyError? error; - - @override - Widget build(BuildContext context) { - return AnimatedGestureDetector( - scaleFactor: 0.99, - onTapUp: () { - getIt().setPlainText(error.toString()); - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - bottomPadding: 0, - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.icon_warning_xl, - blendMode: null, - ), - const VSpace(16.0), - FlowyText.medium( - LocaleKeys.error_syncError.tr(), - fontSize: 15, - ), - const VSpace(8.0), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: FlowyText.regular( - LocaleKeys.error_syncErrorHint.tr(), - fontSize: 13, - color: Theme.of(context).hintColor, - textAlign: TextAlign.center, - maxLines: 10, - ), - ), - const VSpace(2.0), - FlowyText.regular( - '(${LocaleKeys.error_clickToCopy.tr()})', - fontSize: 13, - color: Theme.of(context).hintColor, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} - -class _DesktopSyncErrorPage extends StatelessWidget { - const _DesktopSyncErrorPage({ - this.error, - }); - - final FlowyError? error; - - @override - Widget build(BuildContext context) { - return AnimatedGestureDetector( - scaleFactor: 0.995, - onTapUp: () { - getIt().setPlainText(error.toString()); - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - bottomPadding: 0, - ); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const FlowySvg( - FlowySvgs.icon_warning_xl, - blendMode: null, - ), - const VSpace(16.0), - FlowyText.medium( - error?.code.toString() ?? '', - fontSize: 16, - ), - const VSpace(8.0), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.errorDialog_howToFixFallbackHint1.tr(), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).hintColor, - ), - ), - TextSpan( - text: 'Github', - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.primary, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - 'https://github.com/AppFlowy-IO/AppFlowy/issues/new?template=bug_report.yaml', - ); - }, - ), - TextSpan( - text: LocaleKeys.errorDialog_howToFixFallbackHint2.tr(), - style: TextStyle( - fontSize: 14, - color: Theme.of(context).hintColor, - ), - ), - ], - ), - ), - const VSpace(8.0), - FlowyText.regular( - '(${LocaleKeys.error_clickToCopy.tr()})', - fontSize: 14, - color: Theme.of(context).hintColor, - textAlign: TextAlign.center, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart b/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart index 3e6a69153a..c5cb5df786 100644 --- a/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart +++ b/frontend/appflowy_flutter/lib/shared/google_fonts_extension.dart @@ -1,4 +1,5 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -37,7 +38,11 @@ TextStyle getGoogleFontSafely( letterSpacing: letterSpacing, height: lineHeight, ); - } catch (_) {} + } catch (e) { + Log.error( + 'Font family $fontFamily is not available, using default font family instead', + ); + } } return TextStyle( diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart deleted file mode 100644 index 40b9c1d6fa..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/colors.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -extension PickerColors on BuildContext { - Color get pickerTextColor { - return Theme.of(this).isLightMode - ? const Color(0x80171717) - : Colors.white.withValues(alpha: 0.5); - } - - Color get pickerIconColor { - return Theme.of(this).isLightMode ? const Color(0xFF171717) : Colors.white; - } - - Color get pickerSearchBarBorderColor { - return Theme.of(this).isLightMode - ? const Color(0x1E171717) - : Colors.white.withValues(alpha: 0.12); - } - - Color get pickerButtonBoarderColor { - return Theme.of(this).isLightMode - ? const Color(0x1E171717) - : Colors.white.withValues(alpha: 0.12); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart deleted file mode 100644 index b04b38a45a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'dart:math'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/icon.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; -import 'package:flutter/services.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'icon_uploader.dart'; - -extension ToProto on FlowyIconType { - ViewIconTypePB toProto() { - switch (this) { - case FlowyIconType.emoji: - return ViewIconTypePB.Emoji; - case FlowyIconType.icon: - return ViewIconTypePB.Icon; - case FlowyIconType.custom: - return ViewIconTypePB.Url; - } - } -} - -extension FromProto on ViewIconTypePB { - FlowyIconType fromProto() { - switch (this) { - case ViewIconTypePB.Emoji: - return FlowyIconType.emoji; - case ViewIconTypePB.Icon: - return FlowyIconType.icon; - case ViewIconTypePB.Url: - return FlowyIconType.custom; - default: - return FlowyIconType.custom; - } - } -} - -extension ToEmojiIconData on ViewIconPB { - EmojiIconData toEmojiIconData() => EmojiIconData(ty.fromProto(), value); -} - -enum FlowyIconType { - emoji, - icon, - custom; -} - -extension FlowyIconTypeToPickerTabType on FlowyIconType { - PickerTabType? toPickerTabType() => name.toPickerTabType(); -} - -class EmojiIconData { - factory EmojiIconData.none() => const EmojiIconData(FlowyIconType.icon, ''); - - factory EmojiIconData.emoji(String emoji) => - EmojiIconData(FlowyIconType.emoji, emoji); - - factory EmojiIconData.icon(IconsData icon) => - EmojiIconData(FlowyIconType.icon, icon.iconString); - - factory EmojiIconData.custom(String url) => - EmojiIconData(FlowyIconType.custom, url); - - const EmojiIconData( - this.type, - this.emoji, - ); - - final FlowyIconType type; - final String emoji; - - static EmojiIconData fromViewIconPB(ViewIconPB v) { - return EmojiIconData(v.ty.fromProto(), v.value); - } - - ViewIconPB toViewIcon() { - return ViewIconPB() - ..ty = type.toProto() - ..value = emoji; - } - - bool get isEmpty => emoji.isEmpty; - - bool get isNotEmpty => emoji.isNotEmpty; -} - -class SelectedEmojiIconResult { - SelectedEmojiIconResult(this.data, this.keepOpen); - - final EmojiIconData data; - final bool keepOpen; - - FlowyIconType get type => data.type; - - String get emoji => data.emoji; -} - -extension EmojiIconDataToSelectedResultExtension on EmojiIconData { - SelectedEmojiIconResult toSelectedResult({bool keepOpen = false}) => - SelectedEmojiIconResult(this, keepOpen); -} - -class FlowyIconEmojiPicker extends StatefulWidget { - const FlowyIconEmojiPicker({ - super.key, - this.onSelectedEmoji, - this.initialType, - this.documentId, - this.enableBackgroundColorSelection = true, - this.tabs = const [ - PickerTabType.emoji, - PickerTabType.icon, - ], - }); - - final ValueChanged? onSelectedEmoji; - final bool enableBackgroundColorSelection; - final List tabs; - final PickerTabType? initialType; - final String? documentId; - - @override - State createState() => _FlowyIconEmojiPickerState(); -} - -class _FlowyIconEmojiPickerState extends State - with SingleTickerProviderStateMixin { - late TabController controller; - int currentIndex = 0; - - @override - void initState() { - super.initState(); - final initialType = widget.initialType; - if (initialType != null) { - currentIndex = max(widget.tabs.indexOf(initialType), 0); - } - controller = TabController( - initialIndex: currentIndex, - length: widget.tabs.length, - vsync: this, - ); - controller.addListener(() { - final currentType = widget.tabs[currentIndex]; - if (currentType == PickerTabType.custom) { - SystemChannels.textInput.invokeMethod('TextInput.hide'); - } - }); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 46, - padding: const EdgeInsets.only(left: 4.0, right: 12.0), - child: Row( - children: [ - Expanded( - child: PickerTab( - controller: controller, - tabs: widget.tabs, - onTap: (index) => currentIndex = index, - ), - ), - _RemoveIconButton( - onTap: () { - widget.onSelectedEmoji - ?.call(EmojiIconData.none().toSelectedResult()); - }, - ), - ], - ), - ), - const FlowyDivider(), - Expanded( - child: TabBarView( - controller: controller, - children: widget.tabs.map((tab) { - switch (tab) { - case PickerTabType.emoji: - return _buildEmojiPicker(); - case PickerTabType.icon: - return _buildIconPicker(); - case PickerTabType.custom: - return _buildIconUploader(); - } - }).toList(), - ), - ), - ], - ); - } - - Widget _buildEmojiPicker() { - return FlowyEmojiPicker( - ensureFocus: true, - emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (r) { - widget.onSelectedEmoji?.call( - EmojiIconData.emoji(r.emoji).toSelectedResult(keepOpen: r.isRandom), - ); - SystemChannels.textInput.invokeMethod('TextInput.hide'); - }, - ); - } - - int _getEmojiPerLine(BuildContext context) { - if (UniversalPlatform.isDesktopOrWeb) { - return 9; - } - final width = MediaQuery.of(context).size.width; - return width ~/ 40.0; // the size of the emoji - } - - Widget _buildIconPicker() { - return FlowyIconPicker( - ensureFocus: true, - enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: (r) { - widget.onSelectedEmoji?.call( - r.data.toEmojiIconData().toSelectedResult(keepOpen: r.isRandom), - ); - SystemChannels.textInput.invokeMethod('TextInput.hide'); - }, - ); - } - - Widget _buildIconUploader() { - return IconUploader( - documentId: widget.documentId ?? '', - ensureFocus: true, - onUrl: (url) { - widget.onSelectedEmoji - ?.call(SelectedEmojiIconResult(EmojiIconData.custom(url), false)); - }, - ); - } -} - -class _RemoveIconButton extends StatelessWidget { - const _RemoveIconButton({required this.onTap}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: FlowyButton( - onTap: onTap, - useIntrinsicWidth: true, - text: FlowyText( - fontSize: 14.0, - figmaLineHeight: 16.0, - fontWeight: FontWeight.w500, - LocaleKeys.button_remove.tr(), - color: Theme.of(context).hintColor, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart deleted file mode 100644 index a053595bbd..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'icon.g.dart'; - -@JsonSerializable() -class IconGroup { - factory IconGroup.fromJson(Map json) { - final group = _$IconGroupFromJson(json); - // Set the iconGroup reference for each icon - for (final icon in group.icons) { - icon.iconGroup = group; - } - return group; - } - - factory IconGroup.fromMapEntry(MapEntry entry) => - IconGroup.fromJson({ - 'name': entry.key, - 'icons': entry.value, - }); - - IconGroup({ - required this.name, - required this.icons, - }) { - // Set the iconGroup reference for each icon - for (final icon in icons) { - icon.iconGroup = this; - } - } - - final String name; - final List icons; - - String get displayName => name.replaceAll('_', ' '); - - IconGroup filter(String keyword) { - final lowercaseKey = keyword.toLowerCase(); - final filteredIcons = icons - .where( - (icon) => - icon.keywords - .any((k) => k.toLowerCase().contains(lowercaseKey)) || - icon.name.toLowerCase().contains(lowercaseKey), - ) - .toList(); - return IconGroup(name: name, icons: filteredIcons); - } - - String? getSvgContent(String iconName) { - final icon = icons.firstWhere( - (icon) => icon.name == iconName, - ); - return icon.content; - } - - Map toJson() => _$IconGroupToJson(this); -} - -@JsonSerializable() -class Icon { - factory Icon.fromJson(Map json) => _$IconFromJson(json); - - Icon({ - required this.name, - required this.keywords, - required this.content, - }); - - final String name; - final List keywords; - final String content; - - // Add reference to parent IconGroup - IconGroup? iconGroup; - - String get displayName => name.replaceAll('-', ' '); - - Map toJson() => _$IconToJson(this); - - String get iconPath { - if (iconGroup == null) { - return ''; - } - return '${iconGroup!.name}/$name'; - } -} - -class RecentIcon { - factory RecentIcon.fromJson(Map json) => - RecentIcon(_$IconFromJson(json), json['groupName'] ?? ''); - - RecentIcon(this.icon, this.groupName); - - final Icon icon; - final String groupName; - - String get name => icon.name; - - List get keywords => icon.keywords; - - String get content => icon.content; - - Map toJson() => _$IconToJson( - Icon(name: name, keywords: keywords, content: content), - )..addAll({'groupName': groupName}); -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_color_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_color_picker.dart deleted file mode 100644 index b4221ff42a..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_color_picker.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -class IconColorPicker extends StatelessWidget { - const IconColorPicker({ - super.key, - required this.onSelected, - }); - - final void Function(String color) onSelected; - - @override - Widget build(BuildContext context) { - return GridView.count( - shrinkWrap: true, - crossAxisCount: 6, - mainAxisSpacing: 4.0, - children: builtInSpaceColors.map((color) { - return FlowyHover( - style: HoverStyle(borderRadius: BorderRadius.circular(8.0)), - child: GestureDetector( - onTap: () => onSelected(color), - child: Container( - width: 34, - height: 34, - padding: const EdgeInsets.all(5.0), - child: Container( - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: Color(int.parse(color)), - shape: RoundedRectangleBorder( - side: const BorderSide(color: Color(0x2D333333)), - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - ), - ); - }).toList(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart deleted file mode 100644 index 0d57d12d3c..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_picker.dart +++ /dev/null @@ -1,537 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_search_bar.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/util/debounce.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide Icon; -import 'package:flutter/services.dart'; - -import 'colors.dart'; -import 'icon_color_picker.dart'; - -// cache the icon groups to avoid loading them multiple times -List? kIconGroups; -const _kRecentIconGroupName = 'Recent'; - -extension IconGroupFilter on List { - String? findSvgContent(String key) { - final values = key.split('/'); - if (values.length != 2) { - return null; - } - final groupName = values[0]; - final iconName = values[1]; - final svgString = kIconGroups - ?.firstWhereOrNull( - (group) => group.name == groupName, - ) - ?.icons - .firstWhereOrNull( - (icon) => icon.name == iconName, - ) - ?.content; - return svgString; - } - - (IconGroup, Icon) randomIcon() { - final random = Random(); - final group = this[random.nextInt(length)]; - final icon = group.icons[random.nextInt(group.icons.length)]; - return (group, icon); - } -} - -Future> loadIconGroups() async { - if (kIconGroups != null) { - return kIconGroups!; - } - - final stopwatch = Stopwatch()..start(); - final jsonString = await rootBundle.loadString('assets/icons/icons.json'); - try { - final json = jsonDecode(jsonString) as Map; - final iconGroups = json.entries.map(IconGroup.fromMapEntry).toList(); - kIconGroups = iconGroups; - return iconGroups; - } catch (e) { - Log.error('Failed to decode icons.json', e); - return []; - } finally { - stopwatch.stop(); - Log.info('Loaded icon groups in ${stopwatch.elapsedMilliseconds}ms'); - } -} - -class IconPickerResult { - IconPickerResult(this.data, this.isRandom); - - final IconsData data; - final bool isRandom; -} - -extension IconsDataToIconPickerResultExtension on IconsData { - IconPickerResult toResult({bool isRandom = false}) => - IconPickerResult(this, isRandom); -} - -class FlowyIconPicker extends StatefulWidget { - const FlowyIconPicker({ - super.key, - required this.onSelectedIcon, - required this.enableBackgroundColorSelection, - this.iconPerLine = 9, - this.ensureFocus = false, - }); - - final bool enableBackgroundColorSelection; - final ValueChanged onSelectedIcon; - final int iconPerLine; - final bool ensureFocus; - - @override - State createState() => _FlowyIconPickerState(); -} - -class _FlowyIconPickerState extends State { - final List iconGroups = []; - bool loaded = false; - final ValueNotifier keyword = ValueNotifier(''); - final debounce = Debounce(duration: const Duration(milliseconds: 150)); - - Future loadIcons() async { - final localIcons = await loadIconGroups(); - final recentIcons = await RecentIcons.getIcons(); - if (recentIcons.isNotEmpty) { - final filterRecentIcons = recentIcons - .sublist( - 0, - min(recentIcons.length, widget.iconPerLine), - ) - .skipWhile((e) => e.groupName.isEmpty) - .map((e) => e.icon) - .toList(); - if (filterRecentIcons.isNotEmpty) { - iconGroups.add( - IconGroup( - name: _kRecentIconGroupName, - icons: filterRecentIcons, - ), - ); - } - } - iconGroups.addAll(localIcons); - if (mounted) { - setState(() { - loaded = true; - }); - } - } - - @override - void initState() { - super.initState(); - loadIcons(); - } - - @override - void dispose() { - keyword.dispose(); - debounce.dispose(); - iconGroups.clear(); - loaded = false; - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: IconSearchBar( - ensureFocus: widget.ensureFocus, - onRandomTap: () { - final value = kIconGroups?.randomIcon(); - if (value == null) { - return; - } - final color = widget.enableBackgroundColorSelection - ? generateRandomSpaceColor() - : null; - widget.onSelectedIcon( - IconsData( - value.$1.name, - value.$2.name, - color, - ).toResult(isRandom: true), - ); - RecentIcons.putIcon(RecentIcon(value.$2, value.$1.name)); - }, - onKeywordChanged: (keyword) => { - debounce.call(() { - this.keyword.value = keyword; - }), - }, - ), - ), - Expanded( - child: loaded - ? _buildIcons(iconGroups) - : const Center( - child: SizedBox.square( - dimension: 24.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - ), - ), - ), - ), - ], - ); - } - - Widget _buildIcons(List iconGroups) { - return ValueListenableBuilder( - valueListenable: keyword, - builder: (_, keyword, __) { - if (keyword.isNotEmpty) { - final filteredIconGroups = iconGroups - .map((iconGroup) => iconGroup.filter(keyword)) - .where((iconGroup) => iconGroup.icons.isNotEmpty) - .toList(); - return IconPicker( - iconGroups: filteredIconGroups, - enableBackgroundColorSelection: - widget.enableBackgroundColorSelection, - onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), - iconPerLine: widget.iconPerLine, - ); - } - return IconPicker( - iconGroups: iconGroups, - enableBackgroundColorSelection: widget.enableBackgroundColorSelection, - onSelectedIcon: (r) => widget.onSelectedIcon.call(r.toResult()), - iconPerLine: widget.iconPerLine, - ); - }, - ); - } -} - -class IconsData { - IconsData(this.groupName, this.iconName, this.color); - - final String groupName; - final String iconName; - final String? color; - - String get iconString => jsonEncode({ - 'groupName': groupName, - 'iconName': iconName, - if (color != null) 'color': color, - }); - - EmojiIconData toEmojiIconData() => EmojiIconData.icon(this); - - IconsData noColor() => IconsData(groupName, iconName, null); - - static IconsData fromJson(dynamic json) { - return IconsData( - json['groupName'], - json['iconName'], - json['color'], - ); - } - - String? get svgString => kIconGroups - ?.firstWhereOrNull((group) => group.name == groupName) - ?.icons - .firstWhereOrNull((icon) => icon.name == iconName) - ?.content; -} - -class IconPicker extends StatefulWidget { - const IconPicker({ - super.key, - required this.onSelectedIcon, - required this.enableBackgroundColorSelection, - required this.iconGroups, - required this.iconPerLine, - }); - - final List iconGroups; - final int iconPerLine; - final bool enableBackgroundColorSelection; - final ValueChanged onSelectedIcon; - - @override - State createState() => _IconPickerState(); -} - -class _IconPickerState extends State { - final mutex = PopoverMutex(); - PopoverController? childPopoverController; - - @override - void dispose() { - super.dispose(); - childPopoverController = null; - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: hideColorSelector, - child: NotificationListener( - onNotification: (notificationInfo) { - if (notificationInfo is ScrollStartNotification) { - hideColorSelector(); - } - return true; - }, - child: ListView.builder( - itemCount: widget.iconGroups.length, - padding: const EdgeInsets.symmetric(horizontal: 16.0), - itemBuilder: (context, index) { - final iconGroup = widget.iconGroups[index]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - iconGroup.displayName.capitalize(), - fontSize: 12, - figmaLineHeight: 18.0, - color: context.pickerTextColor, - ), - const VSpace(4.0), - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: widget.iconPerLine, - ), - itemCount: iconGroup.icons.length, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, index) { - final icon = iconGroup.icons[index]; - return widget.enableBackgroundColorSelection - ? _Icon( - icon: icon, - mutex: mutex, - onOpen: (childPopoverController) { - this.childPopoverController = - childPopoverController; - }, - onSelectedColor: (context, color) { - String groupName = iconGroup.name; - if (groupName == _kRecentIconGroupName) { - groupName = getGroupName(index); - } - widget.onSelectedIcon( - IconsData( - groupName, - icon.name, - color, - ), - ); - RecentIcons.putIcon(RecentIcon(icon, groupName)); - PopoverContainer.of(context).close(); - }, - ) - : _IconNoBackground( - icon: icon, - onSelectedIcon: () { - String groupName = iconGroup.name; - if (groupName == _kRecentIconGroupName) { - groupName = getGroupName(index); - } - widget.onSelectedIcon( - IconsData( - groupName, - icon.name, - null, - ), - ); - RecentIcons.putIcon(RecentIcon(icon, groupName)); - }, - ); - }, - ), - const VSpace(12.0), - if (index == widget.iconGroups.length - 1) ...[ - const StreamlinePermit(), - const VSpace(12.0), - ], - ], - ); - }, - ), - ), - ); - } - - void hideColorSelector() { - childPopoverController?.close(); - childPopoverController = null; - } - - String getGroupName(int index) { - final recentIcons = RecentIcons.getIconsSync(); - try { - return recentIcons[index].groupName; - } catch (e) { - Log.error('getGroupName with index: $index error', e); - return ''; - } - } -} - -class _IconNoBackground extends StatelessWidget { - const _IconNoBackground({ - required this.icon, - required this.onSelectedIcon, - this.isSelected = false, - }); - - final Icon icon; - final bool isSelected; - final VoidCallback onSelectedIcon; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: icon.displayName, - preferBelow: false, - child: FlowyButton( - isSelected: isSelected, - useIntrinsicWidth: true, - onTap: () => onSelectedIcon(), - margin: const EdgeInsets.all(8.0), - text: Center( - child: FlowySvg.string( - icon.content, - size: const Size.square(20), - color: context.pickerIconColor, - opacity: 0.7, - ), - ), - ), - ); - } -} - -class _Icon extends StatefulWidget { - const _Icon({ - required this.icon, - required this.mutex, - required this.onSelectedColor, - this.onOpen, - }); - - final Icon icon; - final PopoverMutex mutex; - final void Function(BuildContext context, String color) onSelectedColor; - final ValueChanged? onOpen; - - @override - State<_Icon> createState() => _IconState(); -} - -class _IconState extends State<_Icon> { - final PopoverController _popoverController = PopoverController(); - bool isSelected = false; - - @override - void dispose() { - super.dispose(); - _popoverController.close(); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - controller: _popoverController, - offset: const Offset(0, 6), - mutex: widget.mutex, - onClose: () { - updateIsSelected(false); - }, - clickHandler: PopoverClickHandler.gestureDetector, - child: _IconNoBackground( - icon: widget.icon, - isSelected: isSelected, - onSelectedIcon: () { - updateIsSelected(true); - _popoverController.show(); - widget.onOpen?.call(_popoverController); - }, - ), - popupBuilder: (context) { - return Container( - padding: const EdgeInsets.all(6.0), - child: IconColorPicker( - onSelected: (color) => widget.onSelectedColor(context, color), - ), - ); - }, - ); - } - - void updateIsSelected(bool isSelected) { - setState(() { - this.isSelected = isSelected; - }); - } -} - -class StreamlinePermit extends StatelessWidget { - const StreamlinePermit({ - super.key, - }); - - @override - Widget build(BuildContext context) { - // Open source icons from Streamline - final textStyle = TextStyle( - fontSize: 12.0, - height: 18.0 / 12.0, - fontWeight: FontWeight.w500, - color: context.pickerTextColor, - ); - return RichText( - text: TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.emoji_openSourceIconsFrom.tr()} ', - style: textStyle, - ), - TextSpan( - text: 'Streamline', - style: textStyle.copyWith( - decoration: TextDecoration.underline, - color: Theme.of(context).colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString('https://www.streamlinehq.com/'); - }, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart deleted file mode 100644 index a12be47684..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_search_bar.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'colors.dart'; - -typedef IconKeywordChangedCallback = void Function(String keyword); -typedef EmojiSkinToneChanged = void Function(EmojiSkinTone skinTone); - -class IconSearchBar extends StatefulWidget { - const IconSearchBar({ - super.key, - required this.onRandomTap, - required this.onKeywordChanged, - this.ensureFocus = false, - }); - - final VoidCallback onRandomTap; - final bool ensureFocus; - final IconKeywordChangedCallback onKeywordChanged; - - @override - State createState() => _IconSearchBarState(); -} - -class _IconSearchBarState extends State { - final TextEditingController controller = TextEditingController(); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.symmetric( - vertical: 12.0, - horizontal: UniversalPlatform.isDesktopOrWeb ? 0.0 : 8.0, - ), - child: Row( - children: [ - Expanded( - child: _SearchTextField( - onKeywordChanged: widget.onKeywordChanged, - ensureFocus: widget.ensureFocus, - ), - ), - const HSpace(8.0), - _RandomIconButton( - onRandomTap: widget.onRandomTap, - ), - ], - ), - ); - } -} - -class _RandomIconButton extends StatelessWidget { - const _RandomIconButton({ - required this.onRandomTap, - }); - - final VoidCallback onRandomTap; - - @override - Widget build(BuildContext context) { - return Container( - width: 36, - height: 36, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(color: context.pickerButtonBoarderColor), - borderRadius: BorderRadius.circular(8), - ), - ), - child: FlowyTooltip( - message: LocaleKeys.emoji_random.tr(), - child: FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.icon_shuffle_s, - ), - onTap: onRandomTap, - ), - ), - ); - } -} - -class _SearchTextField extends StatefulWidget { - const _SearchTextField({ - required this.onKeywordChanged, - this.ensureFocus = false, - }); - - final IconKeywordChangedCallback onKeywordChanged; - final bool ensureFocus; - - @override - State<_SearchTextField> createState() => _SearchTextFieldState(); -} - -class _SearchTextFieldState extends State<_SearchTextField> { - final TextEditingController controller = TextEditingController(); - final FocusNode focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - - /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] - /// this is to ensure that focus can be regained within a short period of time - if (widget.ensureFocus) { - Future.delayed(const Duration(milliseconds: 200), () { - if (!mounted || focusNode.hasFocus) return; - focusNode.requestFocus(); - }); - } - } - - @override - void dispose() { - controller.dispose(); - focusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 36.0, - child: FlowyTextField( - focusNode: focusNode, - hintText: LocaleKeys.search_label.tr(), - hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: 14.0, - fontWeight: FontWeight.w400, - color: Theme.of(context).hintColor, - ), - enableBorderColor: context.pickerSearchBarBorderColor, - controller: controller, - onChanged: widget.onKeywordChanged, - prefixIcon: const Padding( - padding: EdgeInsets.only( - left: 14.0, - right: 8.0, - ), - child: FlowySvg( - FlowySvgs.search_s, - ), - ), - prefixIconConstraints: const BoxConstraints( - maxHeight: 20.0, - ), - suffixIcon: Padding( - padding: const EdgeInsets.all(4.0), - child: FlowyButton( - text: const FlowySvg( - FlowySvgs.m_app_bar_close_s, - ), - margin: EdgeInsets.zero, - useIntrinsicWidth: true, - onTap: () { - if (controller.text.isNotEmpty) { - controller.clear(); - widget.onKeywordChanged(''); - } else { - focusNode.unfocus(); - } - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart deleted file mode 100644 index c303160ffe..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/icon_uploader.dart +++ /dev/null @@ -1,466 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_drop_manager.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/appflowy_network_svg.dart'; -import 'package:appflowy/shared/custom_image_cache_manager.dart'; -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; -import 'package:appflowy/shared/permission/permission_checker.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/default_extensions.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:desktop_drop/desktop_drop.dart'; -import 'package:dotted_border/dotted_border.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_svg/flowy_svg.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; - -@visibleForTesting -class IconUploader extends StatefulWidget { - const IconUploader({ - super.key, - required this.onUrl, - required this.documentId, - this.ensureFocus = false, - }); - - final ValueChanged onUrl; - final String documentId; - final bool ensureFocus; - - @override - State createState() => _IconUploaderState(); -} - -class _IconUploaderState extends State { - bool isActive = false; - bool isHovering = false; - bool isUploading = false; - - final List<_Image> pickedImages = []; - final FocusNode focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - - /// Sometimes focus is lost due to the [SelectionGestureInterceptor] in [KeyboardServiceWidgetState] - /// this is to ensure that focus can be regained within a short period of time - if (widget.ensureFocus) { - Future.delayed(const Duration(milliseconds: 200), () { - if (!mounted || focusNode.hasFocus) return; - focusNode.requestFocus(); - }); - } - WidgetsBinding.instance.addPostFrameCallback((_) { - enableDocumentDragNotifier.value = false; - }); - } - - @override - void dispose() { - super.dispose(); - WidgetsBinding.instance.addPostFrameCallback((_) { - enableDocumentDragNotifier.value = true; - }); - focusNode.dispose(); - } - - @override - Widget build(BuildContext context) { - return Shortcuts( - shortcuts: { - LogicalKeySet( - Platform.isMacOS - ? LogicalKeyboardKey.meta - : LogicalKeyboardKey.control, - LogicalKeyboardKey.keyV, - ): _PasteIntent(), - }, - child: Actions( - actions: { - _PasteIntent: CallbackAction<_PasteIntent>( - onInvoke: (intent) => pasteAsAnImage(), - ), - }, - child: Focus( - autofocus: true, - focusNode: focusNode, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Expanded( - child: DropTarget( - onDragEntered: (_) => setState(() => isActive = true), - onDragExited: (_) => setState(() => isActive = false), - onDragDone: (details) => loadImage(details.files), - child: MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: pickImage, - child: DottedBorder( - dashPattern: const [3, 3], - radius: const Radius.circular(8), - borderType: BorderType.RRect, - color: isActive - ? Theme.of(context).colorScheme.primary - : Theme.of(context).hintColor, - child: Container( - alignment: Alignment.center, - decoration: isHovering - ? BoxDecoration( - color: Color(0x0F1F2329), - borderRadius: BorderRadius.circular(8), - ) - : null, - child: pickedImages.isEmpty - ? (isActive - ? hoveringWidget() - : dragHint(context)) - : previewImage(), - ), - ), - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16), - child: Row( - children: [ - Spacer(), - if (pickedImages.isNotEmpty) - Padding( - padding: EdgeInsets.only(right: 8), - child: _ChangeIconButton( - onTap: pickImage, - ), - ), - _ConfirmButton( - onTap: uploadImage, - enable: pickedImages.isNotEmpty, - ), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } - - Widget hoveringWidget() { - return Container( - color: Color(0xffE0F8FF), - child: Center( - child: FlowyText( - LocaleKeys.emojiIconPicker_iconUploader_dropToUpload.tr(), - ), - ), - ); - } - - Widget dragHint(BuildContext context) { - final style = TextStyle( - fontSize: 14, - color: Color(0xff666D76), - fontWeight: FontWeight.w500, - ); - return Padding( - padding: EdgeInsets.symmetric(horizontal: 32), - child: RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: - LocaleKeys.emojiIconPicker_iconUploader_placeholderLeft.tr(), - ), - TextSpan( - text: LocaleKeys.emojiIconPicker_iconUploader_placeholderUpload - .tr(), - style: style.copyWith(color: Color(0xff00BCF0)), - ), - TextSpan( - text: - LocaleKeys.emojiIconPicker_iconUploader_placeholderRight.tr(), - mouseCursor: SystemMouseCursors.click, - ), - ], - style: style, - ), - ), - ); - } - - Widget previewImage() { - final image = pickedImages.first; - final url = image.url; - if (image is _FileImage) { - if (url.endsWith(_svgSuffix)) { - return SvgPicture.file( - File(url), - width: 200, - height: 200, - ); - } - return Image.file( - File(url), - width: 200, - height: 200, - ); - } else if (image is _NetworkImage) { - if (url.endsWith(_svgSuffix)) { - return FlowyNetworkSvg( - url, - width: 200, - height: 200, - ); - } - return FlowyNetworkImage( - width: 200, - height: 200, - url: url, - ); - } - return const SizedBox.shrink(); - } - - void loadImage(List files) { - final imageFiles = files - .where( - (file) => - file.mimeType?.startsWith('image/') ?? - false || - imgExtensionRegex.hasMatch(file.name) || - file.name.endsWith(_svgSuffix), - ) - .toList(); - if (imageFiles.isEmpty) return; - if (mounted) { - setState(() { - pickedImages.clear(); - pickedImages.add(_FileImage(imageFiles.first.path)); - }); - } - } - - Future pickImage() async { - if (UniversalPlatform.isDesktopOrWeb) { - // on desktop, the users can pick a image file from folder - final result = await getIt().pickFiles( - dialogTitle: '', - type: FileType.custom, - allowedExtensions: List.of(defaultImageExtensions)..add('svg'), - ); - loadImage(result?.files.map((f) => f.xFile).toList() ?? const []); - } else { - final photoPermission = - await PermissionChecker.checkPhotoPermission(context); - if (!photoPermission) { - Log.error('Has no permission to access the photo library'); - return; - } - // on mobile, the users can pick a image file from camera or image library - final result = await ImagePicker().pickMultiImage(); - loadImage(result); - } - } - - Future uploadImage() async { - if (pickedImages.isEmpty || isUploading) return; - isUploading = true; - String? result; - final userProfileResult = await UserBackendService.getCurrentUserProfile(); - final userProfile = userProfileResult.fold( - (userProfile) => userProfile, - (l) => null, - ); - final isLocalMode = (userProfile?.workspaceAuthType ?? AuthTypePB.Local) == - AuthTypePB.Local; - if (isLocalMode) { - result = await pickedImages.first.saveToLocal(); - } else { - result = await pickedImages.first.uploadToCloud(widget.documentId); - } - isUploading = false; - if (result?.isNotEmpty ?? false) { - widget.onUrl.call(result!); - } - } - - Future pasteAsAnImage() async { - final data = await getIt().getData(); - final plainText = data.plainText; - Log.info('pasteAsAnImage plainText:$plainText'); - if (plainText == null) return; - if (isURL(plainText) && (await validateImage(plainText))) { - setState(() { - pickedImages.clear(); - pickedImages.add(_NetworkImage(plainText)); - }); - } - } - - Future validateImage(String imageUrl) async { - Response res; - try { - res = await get(Uri.parse(imageUrl)); - } catch (e) { - return false; - } - if (res.statusCode != 200) return false; - final Map data = res.headers; - return checkIfImage(data['content-type']); - } - - bool checkIfImage(String? param) { - if (param == 'image/jpeg' || - param == 'image/png' || - param == 'image/gif' || - param == 'image/tiff' || - param == 'image/webp' || - param == 'image/svg+xml' || - param == 'image/svg') { - return true; - } - return false; - } -} - -class _ChangeIconButton extends StatelessWidget { - const _ChangeIconButton({required this.onTap}); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return SizedBox( - height: 32, - width: 84, - child: FlowyButton( - text: FlowyText( - LocaleKeys.emojiIconPicker_iconUploader_change.tr(), - fontSize: 14.0, - fontWeight: FontWeight.w500, - figmaLineHeight: 20.0, - color: isDark ? Colors.white : Color(0xff1F2329), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - margin: const EdgeInsets.symmetric(horizontal: 14.0), - backgroundColor: Theme.of(context).colorScheme.surface, - hoverColor: - (isDark ? Colors.white : Color(0xffD1D8E0)).withValues(alpha: 0.9), - decoration: BoxDecoration( - border: Border.all(color: isDark ? Colors.white : Color(0xffD1D8E0)), - borderRadius: BorderRadius.circular(10), - ), - onTap: onTap, - ), - ); - } -} - -class _ConfirmButton extends StatelessWidget { - const _ConfirmButton({required this.onTap, this.enable = true}); - - final VoidCallback onTap; - final bool enable; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: Opacity( - opacity: enable ? 1.0 : 0.5, - child: PrimaryRoundedButton( - text: LocaleKeys.button_confirm.tr(), - figmaLineHeight: 20.0, - onTap: enable ? onTap : null, - ), - ), - ); - } -} - -const _svgSuffix = '.svg'; - -class _PasteIntent extends Intent {} - -abstract class _Image { - String get url; - - Future saveToLocal(); - - Future uploadToCloud(String documentId); - - String get pureUrl => url.split('?').first; -} - -class _FileImage extends _Image { - _FileImage(this.url); - - @override - final String url; - - @override - Future saveToLocal() => saveImageToLocalStorage(url); - - @override - Future uploadToCloud(String documentId) async { - final (url, errorMsg) = await saveImageToCloudStorage( - this.url, - documentId, - ); - if (errorMsg?.isNotEmpty ?? false) { - Log.error('upload icon image :${this.url} error :$errorMsg'); - } - return url; - } -} - -class _NetworkImage extends _Image { - _NetworkImage(this.url); - - @override - final String url; - - @override - Future saveToLocal() async { - final file = await CustomImageCacheManager().downloadFile(pureUrl); - return file.file.path; - } - - @override - Future uploadToCloud(String documentId) async { - final file = await CustomImageCacheManager().downloadFile(pureUrl); - final (url, errorMsg) = await saveImageToCloudStorage( - file.file.path, - documentId, - ); - if (errorMsg?.isNotEmpty ?? false) { - Log.error('upload icon image :${this.url} error :$errorMsg'); - } - return url; - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart deleted file mode 100644 index 8c5891bb6e..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/recent_icons.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/foundation.dart'; - -import '../../core/config/kv.dart'; -import '../../core/config/kv_keys.dart'; -import '../../startup/startup.dart'; -import 'flowy_icon_emoji_picker.dart'; - -class RecentIcons { - static final Map> _dataMap = {}; - static bool _loaded = false; - static const maxLength = 20; - - /// To prevent the Recent Icon feature from affecting the unit tests of the Icon Selector. - @visibleForTesting - static bool enable = true; - - static Future putEmoji(String id) async { - await _put(FlowyIconType.emoji, id); - } - - static Future putIcon(RecentIcon icon) async { - await _put( - FlowyIconType.icon, - jsonEncode(icon.toJson()), - ); - } - - static Future> getEmojiIds() async { - await _load(); - return _dataMap[FlowyIconType.emoji.name] ?? []; - } - - static Future> getIcons() async { - await _load(); - return getIconsSync(); - } - - static List getIconsSync() { - final iconList = _dataMap[FlowyIconType.icon.name] ?? []; - try { - final List result = []; - for (final map in iconList) { - final recentIcon = - RecentIcon.fromJson(jsonDecode(map) as Map); - if (recentIcon.groupName.isEmpty) { - continue; - } - result.add(recentIcon); - } - return result; - } catch (e) { - Log.error('RecentIcons getIcons with :$iconList', e); - } - return []; - } - - @visibleForTesting - static void clear() { - _dataMap.clear(); - getIt().remove(KVKeys.recentIcons); - } - - static Future _save() async { - await getIt().set( - KVKeys.recentIcons, - jsonEncode(_dataMap), - ); - } - - static Future _load() async { - if (_loaded || !enable) { - return; - } - final storage = getIt(); - final value = await storage.get(KVKeys.recentIcons); - if (value == null || value.isEmpty) { - _loaded = true; - return; - } - try { - final data = jsonDecode(value) as Map; - _dataMap - ..clear() - ..addAll( - Map>.from( - data.map((k, v) => MapEntry(k, List.from(v))), - ), - ); - } catch (e) { - Log.error('RecentIcons load failed with: $value', e); - } - _loaded = true; - } - - static Future _put(FlowyIconType key, String value) async { - await _load(); - if (!enable) return; - final list = _dataMap[key.name] ?? []; - list.remove(value); - list.insert(0, value); - if (list.length > maxLength) list.removeLast(); - _dataMap[key.name] = list; - await _save(); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart b/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart deleted file mode 100644 index f28ae0f9a8..0000000000 --- a/frontend/appflowy_flutter/lib/shared/icon_emoji_picker/tab.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart'; -import 'package:flutter/material.dart'; - -enum PickerTabType { - emoji, - icon, - custom; - - String get tr { - switch (this) { - case PickerTabType.emoji: - return 'Emojis'; - case PickerTabType.icon: - return 'Icons'; - case PickerTabType.custom: - return 'Upload'; - } - } -} - -extension StringToPickerTabType on String { - PickerTabType? toPickerTabType() { - try { - return PickerTabType.values.byName(this); - } on ArgumentError { - return null; - } - } -} - -class PickerTab extends StatelessWidget { - const PickerTab({ - super.key, - this.onTap, - required this.controller, - required this.tabs, - }); - - final List tabs; - final TabController controller; - final ValueChanged? onTap; - - @override - Widget build(BuildContext context) { - final baseStyle = Theme.of(context).textTheme.bodyMedium; - final style = baseStyle?.copyWith( - fontWeight: FontWeight.w500, - fontSize: 14.0, - height: 16.0 / 14.0, - ); - return TabBar( - controller: controller, - indicatorSize: TabBarIndicatorSize.label, - indicatorColor: Theme.of(context).colorScheme.primary, - isScrollable: true, - labelStyle: style, - labelColor: baseStyle?.color, - labelPadding: const EdgeInsets.symmetric(horizontal: 12.0), - unselectedLabelStyle: style?.copyWith( - color: Theme.of(context).hintColor, - ), - overlayColor: WidgetStateProperty.all(Colors.transparent), - indicator: RoundUnderlineTabIndicator( - width: 34.0, - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 3, - ), - ), - onTap: onTap, - tabs: tabs - .map( - (tab) => Tab( - text: tab.tr, - ), - ) - .toList(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/loading.dart b/frontend/appflowy_flutter/lib/shared/loading.dart deleted file mode 100644 index f8d1c6fc86..0000000000 --- a/frontend/appflowy_flutter/lib/shared/loading.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -class Loading { - Loading(this.context); - - BuildContext? loadingContext; - final BuildContext context; - - bool hasStopped = false; - - void start() => unawaited( - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - loadingContext = context; - - if (hasStopped) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(loadingContext!).maybePop(); - loadingContext = null; - }); - } - - return const SimpleDialog( - elevation: 0.0, - backgroundColor: - Colors.transparent, // can change this to your preferred color - children: [ - Center( - child: CircularProgressIndicator(), - ), - ], - ); - }, - ), - ); - - void stop() { - if (loadingContext != null) { - Navigator.of(loadingContext!).pop(); - loadingContext = null; - } - - hasStopped = true; - } -} diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart deleted file mode 100644 index 912f96bd05..0000000000 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/sub_page_node_parser.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:archive/archive.dart'; -import 'package:flutter/foundation.dart'; -import 'package:path/path.dart' as p; -import 'package:share_plus/share_plus.dart'; - -Document customMarkdownToDocument( - String markdown, { - double? tableWidth, -}) { - return markdownToDocument( - markdown, - markdownParsers: [ - const MarkdownCodeBlockParser(), - MarkdownSimpleTableParser(tableWidth: tableWidth), - ], - ); -} - -Future customDocumentToMarkdown( - Document document, { - String path = '', - AsyncValueSetter? onArchive, - String lineBreak = '', -}) async { - final List> fileFutures = []; - - /// create root Archive and directory - final id = document.root.id, - archive = Archive(), - resourceDir = ArchiveFile('$id/', 0, null)..isFile = false, - fileName = p.basenameWithoutExtension(path), - dirName = resourceDir.name; - - String markdown = ''; - try { - markdown = documentToMarkdown( - document, - lineBreak: lineBreak, - customParsers: [ - const MathEquationNodeParser(), - const CalloutNodeParser(), - const ToggleListNodeParser(), - CustomImageNodeFileParser(fileFutures, dirName), - CustomMultiImageNodeFileParser(fileFutures, dirName), - GridNodeParser(fileFutures, dirName), - BoardNodeParser(fileFutures, dirName), - CalendarNodeParser(fileFutures, dirName), - const CustomParagraphNodeParser(), - const SubPageNodeParser(), - const SimpleTableNodeParser(), - const LinkPreviewNodeParser(), - const FileBlockNodeParser(), - ], - ); - } catch (e) { - Log.error('documentToMarkdown error: $e'); - } - - /// create resource directory - if (fileFutures.isNotEmpty) archive.addFile(resourceDir); - - for (final fileFuture in fileFutures) { - archive.addFile(await fileFuture); - } - - /// add markdown file to Archive - final dataBytes = utf8.encode(markdown); - archive.addFile(ArchiveFile('$fileName-$id.md', dataBytes.length, dataBytes)); - - if (archive.isNotEmpty && path.isNotEmpty) { - if (onArchive == null) { - final zipEncoder = ZipEncoder(); - final zip = zipEncoder.encode(archive); - if (zip != null) { - final zipFile = await File(path).writeAsBytes(zip); - if (Platform.isIOS) { - await Share.shareUri(zipFile.uri); - await zipFile.delete(); - } else if (Platform.isAndroid) { - await Share.shareXFiles([XFile(zipFile.path)]); - await zipFile.delete(); - } - Log.info('documentToMarkdownFiles to $path'); - } - } else { - await onArchive.call(archive); - } - } - return markdown; -} diff --git a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart index 862f9c4778..fb9cd9f226 100644 --- a/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart +++ b/frontend/appflowy_flutter/lib/shared/patterns/common_patterns.dart @@ -7,15 +7,12 @@ final hrefRegex = RegExp(_hrefPattern); /// This pattern allows for both HTTP and HTTPS Scheme /// It allows for query parameters -/// It only allows the following image extensions: .png, .jpg, .jpeg, .gif, .webm, .webp, .bmp +/// It only allows the following image extensions: .png, .jpg, .gif, .webm /// const _imgUrlPattern = - r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.jpeg|.gif|.webm|.webp|.bmp)(\?[^\s[",><]*)?'; + r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm)(\?[^\s[",><]*)?'; final imgUrlRegex = RegExp(_imgUrlPattern); -const _singleLineMarkdownImagePattern = "^!\\[.*\\]\\(($_hrefPattern)\\)\$"; -final singleLineMarkdownImageRegex = RegExp(_singleLineMarkdownImagePattern); - /// This pattern allows for both HTTP and HTTPS Scheme /// It allows for query parameters /// It only allows the following video extensions: @@ -38,20 +35,3 @@ final camelCaseRegex = RegExp(_camelCasePattern); const _macOSVolumesPattern = '^/Volumes/[^/]+'; final macOSVolumesRegex = RegExp(_macOSVolumesPattern); - -const appflowySharePageLinkPattern = - r'^https://appflowy\.com/app/([^/]+)/([^?]+)(?:\?blockId=(.+))?$'; -final appflowySharePageLinkRegex = RegExp(appflowySharePageLinkPattern); - -const _numberedListPattern = r'^(\d+)\.'; -final numberedListRegex = RegExp(_numberedListPattern); - -const _localPathPattern = r'^(file:\/\/|\/|\\|[a-zA-Z]:[/\\]|\.{1,2}[/\\])'; -final localPathRegex = RegExp(_localPathPattern, caseSensitive: false); - -const _wordPattern = r"\S+"; -final wordRegex = RegExp(_wordPattern); - -const _appleNotesPattern = - r'\s*'; -final appleNotesRegex = RegExp(_appleNotesPattern); diff --git a/frontend/appflowy_flutter/lib/shared/patterns/file_type_patterns.dart b/frontend/appflowy_flutter/lib/shared/patterns/file_type_patterns.dart deleted file mode 100644 index 418dd47f3d..0000000000 --- a/frontend/appflowy_flutter/lib/shared/patterns/file_type_patterns.dart +++ /dev/null @@ -1,29 +0,0 @@ -/// This pattern matches a file extension that is an image. -/// -const _imgExtensionPattern = r'\.(gif|jpe?g|tiff?|png|webp|bmp)$'; -final imgExtensionRegex = RegExp(_imgExtensionPattern); - -/// This pattern matches a file extension that is a video. -/// -const _videoExtensionPattern = r'\.(mp4|mov|avi|webm|flv|m4v|mpeg|h264)$'; -final videoExtensionRegex = RegExp(_videoExtensionPattern); - -/// This pattern matches a file extension that is an audio. -/// -const _audioExtensionPattern = r'\.(mp3|wav|ogg|flac|aac|wma|alac|aiff)$'; -final audioExtensionRegex = RegExp(_audioExtensionPattern); - -/// This pattern matches a file extension that is a document. -/// -const _documentExtensionPattern = r'\.(pdf|doc|docx)$'; -final documentExtensionRegex = RegExp(_documentExtensionPattern); - -/// This pattern matches a file extension that is an archive. -/// -const _archiveExtensionPattern = r'\.(zip|tar|gz|7z|rar)$'; -final archiveExtensionRegex = RegExp(_archiveExtensionPattern); - -/// This pattern matches a file extension that is a text. -/// -const _textExtensionPattern = r'\.(txt|md|html|css|js|json|xml|csv)$'; -final textExtensionRegex = RegExp(_textExtensionPattern); diff --git a/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart b/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart index 4b5ad56ab1..7ae91fc73e 100644 --- a/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart +++ b/frontend/appflowy_flutter/lib/shared/permission/permission_checker.dart @@ -9,9 +9,9 @@ import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_d import 'package:appflowy/startup/tasks/device_info_task.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'; import 'package:permission_handler/permission_handler.dart'; -import 'package:universal_platform/universal_platform.dart'; class PermissionChecker { static Future checkPhotoPermission(BuildContext context) async { @@ -48,7 +48,7 @@ class PermissionChecker { } else if (status.isDenied) { // https://github.com/Baseflow/flutter-permission-handler/issues/1262#issuecomment-2006340937 Permission permission = Permission.photos; - if (UniversalPlatform.isAndroid && + if (defaultTargetPlatform == TargetPlatform.android && ApplicationInfo.androidSDKVersion <= 32) { permission = Permission.storage; } @@ -61,43 +61,4 @@ class PermissionChecker { return true; } - - static Future checkCameraPermission(BuildContext context) async { - // check the permission first - final status = await Permission.camera.status; - // if the permission is permanently denied, we should open the app settings - if (status.isPermanentlyDenied && context.mounted) { - unawaited( - showFlowyMobileConfirmDialog( - context, - title: FlowyText.semibold( - LocaleKeys.pageStyle_cameraPermissionTitle.tr(), - maxLines: 3, - textAlign: TextAlign.center, - ), - content: FlowyText( - LocaleKeys.pageStyle_cameraPermissionDescription.tr(), - maxLines: 5, - textAlign: TextAlign.center, - fontSize: 12.0, - ), - actionAlignment: ConfirmDialogActionAlignment.vertical, - actionButtonTitle: LocaleKeys.pageStyle_openSettings.tr(), - actionButtonColor: Colors.blue, - cancelButtonTitle: LocaleKeys.pageStyle_doNotAllow.tr(), - cancelButtonColor: Colors.blue, - onActionButtonPressed: openAppSettings, - ), - ); - - return false; - } else if (status.isDenied) { - final newStatus = await Permission.camera.request(); - if (newStatus.isDenied) { - return false; - } - } - - return true; - } } diff --git a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart b/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart deleted file mode 100644 index 786d666060..0000000000 --- a/frontend/appflowy_flutter/lib/shared/popup_menu/appflowy_popup_menu.dart +++ /dev/null @@ -1,1667 +0,0 @@ -// This file is copied from Flutter source code, -// and modified to fit AppFlowy's needs. - -// changes: -// 1. remove the default ink effect -// 2. remove the tooltip -// 3. support customize transition animation - -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:appflowy/mobile/presentation/base/animated_gesture.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/scheduler.dart'; - -// Examples can assume: -// enum Commands { heroAndScholar, hurricaneCame } -// late bool _heroAndScholar; -// late dynamic _selection; -// late BuildContext context; -// void setState(VoidCallback fn) { } -// enum Menu { itemOne, itemTwo, itemThree, itemFour } - -const Duration _kMenuDuration = Duration(milliseconds: 300); -const double _kMenuCloseIntervalEnd = 2.0 / 3.0; -const double _kMenuDividerHeight = 16.0; -const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; -const double _kMenuMinWidth = 2.0 * _kMenuWidthStep; -const double _kMenuVerticalPadding = 8.0; -const double _kMenuWidthStep = 56.0; -const double _kMenuScreenPadding = 8.0; - -GlobalKey<_PopupMenuState>? _kPopupMenuKey; -void closePopupMenu() { - _kPopupMenuKey?.currentState?.dismiss(); - _kPopupMenuKey = null; -} - -/// A base class for entries in a Material Design popup menu. -/// -/// The popup menu widget uses this interface to interact with the menu items. -/// To show a popup menu, use the [showMenu] function. To create a button that -/// shows a popup menu, consider using [PopupMenuButton]. -/// -/// The type `T` is the type of the value(s) the entry represents. All the -/// entries in a given menu must represent values with consistent types. -/// -/// A [PopupMenuEntry] may represent multiple values, for example a row with -/// several icons, or a single entry, for example a menu item with an icon (see -/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]). -/// -/// See also: -/// -/// * [PopupMenuItem], a popup menu entry for a single value. -/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. -/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. -/// * [showMenu], a method to dynamically show a popup menu at a given location. -/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when -/// it is tapped. -abstract class PopupMenuEntry extends StatefulWidget { - /// Abstract const constructor. This constructor enables subclasses to provide - /// const constructors so that they can be used in const expressions. - const PopupMenuEntry({super.key}); - - /// The amount of vertical space occupied by this entry. - /// - /// This value is used at the time the [showMenu] method is called, if the - /// `initialValue` argument is provided, to determine the position of this - /// entry when aligning the selected entry over the given `position`. It is - /// otherwise ignored. - double get height; - - /// Whether this entry represents a particular value. - /// - /// This method is used by [showMenu], when it is called, to align the entry - /// representing the `initialValue`, if any, to the given `position`, and then - /// later is called on each entry to determine if it should be highlighted (if - /// the method returns true, the entry will have its background color set to - /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then - /// this method is not called. - /// - /// If the [PopupMenuEntry] represents a single value, this should return true - /// if the argument matches that value. If it represents multiple values, it - /// should return true if the argument matches any of them. - bool represents(T? value); -} - -/// A horizontal divider in a Material Design popup menu. -/// -/// This widget adapts the [Divider] for use in popup menus. -/// -/// See also: -/// -/// * [PopupMenuItem], for the kinds of items that this widget divides. -/// * [showMenu], a method to dynamically show a popup menu at a given location. -/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when -/// it is tapped. -class PopupMenuDivider extends PopupMenuEntry { - /// Creates a horizontal divider for a popup menu. - /// - /// By default, the divider has a height of 16 logical pixels. - const PopupMenuDivider({super.key, this.height = _kMenuDividerHeight}); - - /// The height of the divider entry. - /// - /// Defaults to 16 pixels. - @override - final double height; - - @override - bool represents(void value) => false; - - @override - State createState() => _PopupMenuDividerState(); -} - -class _PopupMenuDividerState extends State { - @override - Widget build(BuildContext context) => Divider(height: widget.height); -} - -// This widget only exists to enable _PopupMenuRoute to save the sizes of -// each menu item. The sizes are used by _PopupMenuRouteLayout to compute the -// y coordinate of the menu's origin so that the center of selected menu -// item lines up with the center of its PopupMenuButton. -class _MenuItem extends SingleChildRenderObjectWidget { - const _MenuItem({ - required this.onLayout, - required super.child, - }); - - final ValueChanged onLayout; - - @override - RenderObject createRenderObject(BuildContext context) { - return _RenderMenuItem(onLayout); - } - - @override - void updateRenderObject( - BuildContext context, - covariant _RenderMenuItem renderObject, - ) { - renderObject.onLayout = onLayout; - } -} - -class _RenderMenuItem extends RenderShiftedBox { - _RenderMenuItem(this.onLayout, [RenderBox? child]) : super(child); - - ValueChanged onLayout; - - @override - Size computeDryLayout(BoxConstraints constraints) { - return child?.getDryLayout(constraints) ?? Size.zero; - } - - @override - void performLayout() { - if (child == null) { - size = Size.zero; - } else { - child!.layout(constraints, parentUsesSize: true); - size = constraints.constrain(child!.size); - final BoxParentData childParentData = child!.parentData! as BoxParentData; - childParentData.offset = Offset.zero; - } - onLayout(size); - } -} - -/// An item in a Material Design popup menu. -/// -/// To show a popup menu, use the [showMenu] function. To create a button that -/// shows a popup menu, consider using [PopupMenuButton]. -/// -/// To show a checkmark next to a popup menu item, consider using -/// [CheckedPopupMenuItem]. -/// -/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More -/// elaborate menus with icons can use a [ListTile]. By default, a -/// [PopupMenuItem] is [kMinInteractiveDimension] pixels high. If you use a widget -/// with a different height, it must be specified in the [height] property. -/// -/// {@tool snippet} -/// -/// Here, a [Text] widget is used with a popup menu item. The `Menu` type -/// is an enum, not shown here. -/// -/// ```dart -/// const PopupMenuItem( -/// value: Menu.itemOne, -/// child: Text('Item 1'), -/// ) -/// ``` -/// {@end-tool} -/// -/// See the example at [PopupMenuButton] for how this example could be used in a -/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to -/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child] -/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem] -/// that use a [ListTile] in their [child] slot. -/// -/// See also: -/// -/// * [PopupMenuDivider], which can be used to divide items from each other. -/// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark. -/// * [showMenu], a method to dynamically show a popup menu at a given location. -/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when -/// it is tapped. -class PopupMenuItem extends PopupMenuEntry { - /// Creates an item for a popup menu. - /// - /// By default, the item is [enabled]. - const PopupMenuItem({ - super.key, - this.value, - this.onTap, - this.enabled = true, - this.height = kMinInteractiveDimension, - this.padding, - this.textStyle, - this.labelTextStyle, - this.mouseCursor, - required this.child, - }); - - /// The value that will be returned by [showMenu] if this entry is selected. - final T? value; - - /// Called when the menu item is tapped. - final VoidCallback? onTap; - - /// Whether the user is permitted to select this item. - /// - /// Defaults to true. If this is false, then the item will not react to - /// touches. - final bool enabled; - - /// The minimum height of the menu item. - /// - /// Defaults to [kMinInteractiveDimension] pixels. - @override - final double height; - - /// The padding of the menu item. - /// - /// The [height] property may interact with the applied padding. For example, - /// If a [height] greater than the height of the sum of the padding and [child] - /// is provided, then the padding's effect will not be visible. - /// - /// If this is null and [ThemeData.useMaterial3] is true, the horizontal padding - /// defaults to 12.0 on both sides. - /// - /// If this is null and [ThemeData.useMaterial3] is false, the horizontal padding - /// defaults to 16.0 on both sides. - final EdgeInsets? padding; - - /// The text style of the popup menu item. - /// - /// If this property is null, then [PopupMenuThemeData.textStyle] is used. - /// If [PopupMenuThemeData.textStyle] is also null, then [TextTheme.titleMedium] - /// of [ThemeData.textTheme] is used. - final TextStyle? textStyle; - - /// The label style of the popup menu item. - /// - /// When [ThemeData.useMaterial3] is true, this styles the text of the popup menu item. - /// - /// If this property is null, then [PopupMenuThemeData.labelTextStyle] is used. - /// If [PopupMenuThemeData.labelTextStyle] is also null, then [TextTheme.labelLarge] - /// is used with the [ColorScheme.onSurface] color when popup menu item is enabled and - /// the [ColorScheme.onSurface] color with 0.38 opacity when the popup menu item is disabled. - final WidgetStateProperty? labelTextStyle; - - /// {@template flutter.material.popupmenu.mouseCursor} - /// The cursor for a mouse pointer when it enters or is hovering over the - /// widget. - /// - /// If [mouseCursor] is a [MaterialStateProperty], - /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: - /// - /// * [WidgetState.hovered]. - /// * [WidgetState.focused]. - /// * [WidgetState.disabled]. - /// {@endtemplate} - /// - /// If null, then the value of [PopupMenuThemeData.mouseCursor] is used. If - /// that is also null, then [WidgetStateMouseCursor.clickable] is used. - final MouseCursor? mouseCursor; - - /// The widget below this widget in the tree. - /// - /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An - /// appropriate [DefaultTextStyle] is put in scope for the child. In either - /// case, the text should be short enough that it won't wrap. - final Widget? child; - - @override - bool represents(T? value) => value == this.value; - - @override - PopupMenuItemState> createState() => - PopupMenuItemState>(); -} - -/// The [State] for [PopupMenuItem] subclasses. -/// -/// By default this implements the basic styling and layout of Material Design -/// popup menu items. -/// -/// The [buildChild] method can be overridden to adjust exactly what gets placed -/// in the menu. By default it returns [PopupMenuItem.child]. -/// -/// The [handleTap] method can be overridden to adjust exactly what happens when -/// the item is tapped. By default, it uses [Navigator.pop] to return the -/// [PopupMenuItem.value] from the menu route. -/// -/// This class takes two type arguments. The second, `W`, is the exact type of -/// the [Widget] that is using this [State]. It must be a subclass of -/// [PopupMenuItem]. The first, `T`, must match the type argument of that widget -/// class, and is the type of values returned from this menu. -class PopupMenuItemState> extends State { - /// The menu item contents. - /// - /// Used by the [build] method. - /// - /// By default, this returns [PopupMenuItem.child]. Override this to put - /// something else in the menu entry. - @protected - Widget? buildChild() => widget.child; - - /// The handler for when the user selects the menu item. - /// - /// Used by the [InkWell] inserted by the [build] method. - /// - /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from - /// the menu route. - @protected - void handleTap() { - // Need to pop the navigator first in case onTap may push new route onto navigator. - Navigator.pop(context, widget.value); - - widget.onTap?.call(); - } - - @override - Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); - final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - final PopupMenuThemeData defaults = theme.useMaterial3 - ? _PopupMenuDefaultsM3(context) - : _PopupMenuDefaultsM2(context); - final Set states = { - if (!widget.enabled) WidgetState.disabled, - }; - - TextStyle style = theme.useMaterial3 - ? (widget.labelTextStyle?.resolve(states) ?? - popupMenuTheme.labelTextStyle?.resolve(states)! ?? - defaults.labelTextStyle!.resolve(states)!) - : (widget.textStyle ?? popupMenuTheme.textStyle ?? defaults.textStyle!); - - if (!widget.enabled && !theme.useMaterial3) { - style = style.copyWith(color: theme.disabledColor); - } - - Widget item = AnimatedDefaultTextStyle( - style: style, - duration: kThemeChangeDuration, - child: Container( - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints(minHeight: widget.height), - padding: widget.padding ?? - (theme.useMaterial3 - ? _PopupMenuDefaultsM3.menuHorizontalPadding - : _PopupMenuDefaultsM2.menuHorizontalPadding), - child: buildChild(), - ), - ); - - if (!widget.enabled) { - final bool isDark = theme.brightness == Brightness.dark; - item = IconTheme.merge( - data: IconThemeData(opacity: isDark ? 0.5 : 0.38), - child: item, - ); - } - - return MergeSemantics( - child: Semantics( - enabled: widget.enabled, - button: true, - child: GestureDetector( - onTap: widget.enabled ? handleTap : null, - behavior: HitTestBehavior.opaque, - child: ListTileTheme.merge( - contentPadding: EdgeInsets.zero, - titleTextStyle: style, - child: item, - ), - ), - ), - ); - } -} - -/// An item with a checkmark in a Material Design popup menu. -/// -/// To show a popup menu, use the [showMenu] function. To create a button that -/// shows a popup menu, consider using [PopupMenuButton]. -/// -/// A [CheckedPopupMenuItem] is kMinInteractiveDimension pixels high, which -/// matches the default minimum height of a [PopupMenuItem]. The horizontal -/// layout uses [ListTile]; the checkmark is an [Icons.done] icon, shown in the -/// [ListTile.leading] position. -/// -/// {@tool snippet} -/// -/// Suppose a `Commands` enum exists that lists the possible commands from a -/// particular popup menu, including `Commands.heroAndScholar` and -/// `Commands.hurricaneCame`, and further suppose that there is a -/// `_heroAndScholar` member field which is a boolean. The example below shows a -/// menu with one menu item with a checkmark that can toggle the boolean, and -/// one menu item without a checkmark for selecting the second option. (It also -/// shows a divider placed between the two menu items.) -/// -/// ```dart -/// PopupMenuButton( -/// onSelected: (Commands result) { -/// switch (result) { -/// case Commands.heroAndScholar: -/// setState(() { _heroAndScholar = !_heroAndScholar; }); -/// case Commands.hurricaneCame: -/// // ...handle hurricane option -/// break; -/// // ...other items handled here -/// } -/// }, -/// itemBuilder: (BuildContext context) => >[ -/// CheckedPopupMenuItem( -/// checked: _heroAndScholar, -/// value: Commands.heroAndScholar, -/// child: const Text('Hero and scholar'), -/// ), -/// const PopupMenuDivider(), -/// const PopupMenuItem( -/// value: Commands.hurricaneCame, -/// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')), -/// ), -/// // ...other items listed here -/// ], -/// ) -/// ``` -/// {@end-tool} -/// -/// In particular, observe how the second menu item uses a [ListTile] with a -/// blank [Icon] in the [ListTile.leading] position to get the same alignment as -/// the item with the checkmark. -/// -/// See also: -/// -/// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to -/// toggling a value). -/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. -/// * [showMenu], a method to dynamically show a popup menu at a given location. -/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when -/// it is tapped. -class CheckedPopupMenuItem extends PopupMenuItem { - /// Creates a popup menu item with a checkmark. - /// - /// By default, the menu item is [enabled] but unchecked. To mark the item as - /// checked, set [checked] to true. - const CheckedPopupMenuItem({ - super.key, - super.value, - this.checked = false, - super.enabled, - super.padding, - super.height, - super.labelTextStyle, - super.mouseCursor, - super.child, - super.onTap, - }); - - /// Whether to display a checkmark next to the menu item. - /// - /// Defaults to false. - /// - /// When true, an [Icons.done] checkmark is displayed. - /// - /// When this popup menu item is selected, the checkmark will fade in or out - /// as appropriate to represent the implied new state. - final bool checked; - - /// The widget below this widget in the tree. - /// - /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for - /// the child. The text should be short enough that it won't wrap. - /// - /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose - /// [ListTile.leading] slot is an [Icons.done] icon. - @override - Widget? get child => super.child; - - @override - PopupMenuItemState> createState() => - _CheckedPopupMenuItemState(); -} - -class _CheckedPopupMenuItemState - extends PopupMenuItemState> - with SingleTickerProviderStateMixin { - static const Duration _fadeDuration = Duration(milliseconds: 150); - late AnimationController _controller; - Animation get _opacity => _controller.view; - - @override - void initState() { - super.initState(); - _controller = AnimationController(duration: _fadeDuration, vsync: this) - ..value = widget.checked ? 1.0 : 0.0 - ..addListener(_updateState); - } - - // Called when animation changed - void _updateState() => setState(() {}); - - @override - void dispose() { - _controller.removeListener(_updateState); - _controller.dispose(); - super.dispose(); - } - - @override - void handleTap() { - // This fades the checkmark in or out when tapped. - if (widget.checked) { - _controller.reverse(); - } else { - _controller.forward(); - } - super.handleTap(); - } - - @override - Widget buildChild() { - final ThemeData theme = Theme.of(context); - final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - final PopupMenuThemeData defaults = theme.useMaterial3 - ? _PopupMenuDefaultsM3(context) - : _PopupMenuDefaultsM2(context); - final Set states = { - if (widget.checked) WidgetState.selected, - }; - final WidgetStateProperty? effectiveLabelTextStyle = - widget.labelTextStyle ?? - popupMenuTheme.labelTextStyle ?? - defaults.labelTextStyle; - return IgnorePointer( - child: ListTileTheme.merge( - contentPadding: EdgeInsets.zero, - child: ListTile( - enabled: widget.enabled, - titleTextStyle: effectiveLabelTextStyle?.resolve(states), - leading: FadeTransition( - opacity: _opacity, - child: Icon(_controller.isDismissed ? null : Icons.done), - ), - title: widget.child, - ), - ), - ); - } -} - -class _PopupMenu extends StatefulWidget { - const _PopupMenu({ - super.key, - required this.itemKeys, - required this.route, - required this.semanticLabel, - this.constraints, - required this.clipBehavior, - }); - - final List itemKeys; - final _PopupMenuRoute route; - final String? semanticLabel; - final BoxConstraints? constraints; - final Clip clipBehavior; - - @override - State<_PopupMenu> createState() => _PopupMenuState(); -} - -class _PopupMenuState extends State<_PopupMenu> { - @override - Widget build(BuildContext context) { - final double unit = 1.0 / - (widget.route.items.length + - 1.5); // 1.0 for the width and 0.5 for the last item's fade. - final List children = []; - final ThemeData theme = Theme.of(context); - final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - final PopupMenuThemeData defaults = theme.useMaterial3 - ? _PopupMenuDefaultsM3(context) - : _PopupMenuDefaultsM2(context); - - for (int i = 0; i < widget.route.items.length; i += 1) { - final double start = (i + 1) * unit; - final double end = clampDouble(start + 1.5 * unit, 0.0, 1.0); - final CurvedAnimation opacity = CurvedAnimation( - parent: widget.route.animation!, - curve: Interval(start, end), - ); - Widget item = widget.route.items[i]; - if (widget.route.initialValue != null && - widget.route.items[i].represents(widget.route.initialValue)) { - item = ColoredBox( - color: Theme.of(context).highlightColor, - child: item, - ); - } - children.add( - _MenuItem( - onLayout: (Size size) { - widget.route.itemSizes[i] = size; - }, - child: FadeTransition( - key: widget.itemKeys[i], - opacity: opacity, - child: item, - ), - ), - ); - } - - final _CurveTween opacity = - _CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); - final _CurveTween width = _CurveTween(curve: Interval(0.0, unit)); - final _CurveTween height = - _CurveTween(curve: Interval(0.0, unit * widget.route.items.length)); - - final Widget child = ConstrainedBox( - constraints: widget.constraints ?? - const BoxConstraints( - minWidth: _kMenuMinWidth, - maxWidth: _kMenuMaxWidth, - ), - child: IntrinsicWidth( - stepWidth: _kMenuWidthStep, - child: Semantics( - scopesRoute: true, - namesRoute: true, - explicitChildNodes: true, - label: widget.semanticLabel, - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - vertical: _kMenuVerticalPadding, - ), - child: ListBody(children: children), - ), - ), - ), - ); - - return AnimatedBuilder( - animation: widget.route.animation!, - builder: (BuildContext context, Widget? child) { - return FadeTransition( - opacity: opacity.animate(widget.route.animation!), - child: Material( - shape: widget.route.shape ?? popupMenuTheme.shape ?? defaults.shape, - color: widget.route.color ?? popupMenuTheme.color ?? defaults.color, - clipBehavior: widget.clipBehavior, - type: MaterialType.card, - elevation: widget.route.elevation ?? - popupMenuTheme.elevation ?? - defaults.elevation!, - shadowColor: widget.route.shadowColor ?? - popupMenuTheme.shadowColor ?? - defaults.shadowColor, - surfaceTintColor: widget.route.surfaceTintColor ?? - popupMenuTheme.surfaceTintColor ?? - defaults.surfaceTintColor, - child: Align( - alignment: AlignmentDirectional.topEnd, - widthFactor: width.evaluate(widget.route.animation!), - heightFactor: height.evaluate(widget.route.animation!), - child: child, - ), - ), - ); - }, - child: child, - ); - } - - @override - void dispose() { - _kPopupMenuKey = null; - super.dispose(); - } - - void dismiss() { - if (_kPopupMenuKey == null) { - return; - } - - Navigator.of(context).pop(); - _kPopupMenuKey = null; - } -} - -// Positioning of the menu on the screen. -class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { - _PopupMenuRouteLayout( - this.position, - this.itemSizes, - this.selectedItemIndex, - this.textDirection, - this.padding, - this.avoidBounds, - ); - - // Rectangle of underlying button, relative to the overlay's dimensions. - final RelativeRect position; - - // The sizes of each item are computed when the menu is laid out, and before - // the route is laid out. - List itemSizes; - - // The index of the selected item, or null if PopupMenuButton.initialValue - // was not specified. - final int? selectedItemIndex; - - // Whether to prefer going to the left or to the right. - final TextDirection textDirection; - - // The padding of unsafe area. - EdgeInsets padding; - - // List of rectangles that we should avoid overlapping. Unusable screen area. - final Set avoidBounds; - - // We put the child wherever position specifies, so long as it will fit within - // the specified parent size padded (inset) by 8. If necessary, we adjust the - // child's position so that it fits. - - @override - BoxConstraints getConstraintsForChild(BoxConstraints constraints) { - // The menu can be at most the size of the overlay minus 8.0 pixels in each - // direction. - return BoxConstraints.loose(constraints.biggest).deflate( - const EdgeInsets.all(_kMenuScreenPadding) + padding, - ); - } - - @override - Offset getPositionForChild(Size size, Size childSize) { - final double y = position.top; - - // Find the ideal horizontal position. - // size: The size of the overlay. - // childSize: The size of the menu, when fully open, as determined by - // getConstraintsForChild. - double x; - if (position.left > position.right) { - // Menu button is closer to the right edge, so grow to the left, aligned to the right edge. - x = size.width - position.right - childSize.width; - } else if (position.left < position.right) { - // Menu button is closer to the left edge, so grow to the right, aligned to the left edge. - x = position.left; - } else { - // Menu button is equidistant from both edges, so grow in reading direction. - x = switch (textDirection) { - TextDirection.rtl => size.width - position.right - childSize.width, - TextDirection.ltr => position.left, - }; - } - final Offset wantedPosition = Offset(x, y); - final Offset originCenter = position.toRect(Offset.zero & size).center; - final Iterable subScreens = - DisplayFeatureSubScreen.subScreensInBounds( - Offset.zero & size, - avoidBounds, - ); - final Rect subScreen = _closestScreen(subScreens, originCenter); - return _fitInsideScreen(subScreen, childSize, wantedPosition); - } - - Rect _closestScreen(Iterable screens, Offset point) { - Rect closest = screens.first; - for (final Rect screen in screens) { - if ((screen.center - point).distance < - (closest.center - point).distance) { - closest = screen; - } - } - return closest; - } - - Offset _fitInsideScreen(Rect screen, Size childSize, Offset wantedPosition) { - double x = wantedPosition.dx; - double y = wantedPosition.dy; - // Avoid going outside an area defined as the rectangle 8.0 pixels from the - // edge of the screen in every direction. - if (x < screen.left + _kMenuScreenPadding + padding.left) { - x = screen.left + _kMenuScreenPadding + padding.left; - } else if (x + childSize.width > - screen.right - _kMenuScreenPadding - padding.right) { - x = screen.right - childSize.width - _kMenuScreenPadding - padding.right; - } - if (y < screen.top + _kMenuScreenPadding + padding.top) { - y = _kMenuScreenPadding + padding.top; - } else if (y + childSize.height > - screen.bottom - _kMenuScreenPadding - padding.bottom) { - y = screen.bottom - - childSize.height - - _kMenuScreenPadding - - padding.bottom; - } - - return Offset(x, y); - } - - @override - bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { - // If called when the old and new itemSizes have been initialized then - // we expect them to have the same length because there's no practical - // way to change length of the items list once the menu has been shown. - assert(itemSizes.length == oldDelegate.itemSizes.length); - - return position != oldDelegate.position || - selectedItemIndex != oldDelegate.selectedItemIndex || - textDirection != oldDelegate.textDirection || - !listEquals(itemSizes, oldDelegate.itemSizes) || - padding != oldDelegate.padding || - !setEquals(avoidBounds, oldDelegate.avoidBounds); - } -} - -class _PopupMenuRoute extends PopupRoute { - _PopupMenuRoute({ - required this.position, - required this.items, - required this.itemKeys, - this.initialValue, - this.elevation, - this.surfaceTintColor, - this.shadowColor, - required this.barrierLabel, - this.semanticLabel, - this.shape, - this.color, - required this.capturedThemes, - this.constraints, - required this.clipBehavior, - super.settings, - this.popUpAnimationStyle, - }) : itemSizes = List.filled(items.length, null), - // Menus always cycle focus through their items irrespective of the - // focus traversal edge behavior set in the Navigator. - super(traversalEdgeBehavior: TraversalEdgeBehavior.closedLoop); - - final RelativeRect position; - final List> items; - final List itemKeys; - final List itemSizes; - final T? initialValue; - final double? elevation; - final Color? surfaceTintColor; - final Color? shadowColor; - final String? semanticLabel; - final ShapeBorder? shape; - final Color? color; - final CapturedThemes capturedThemes; - final BoxConstraints? constraints; - final Clip clipBehavior; - final AnimationStyle? popUpAnimationStyle; - - @override - Animation createAnimation() { - if (popUpAnimationStyle != AnimationStyle.noAnimation) { - return CurvedAnimation( - parent: super.createAnimation(), - curve: popUpAnimationStyle?.curve ?? Curves.easeInBack, - reverseCurve: popUpAnimationStyle?.reverseCurve ?? - const Interval(0.0, _kMenuCloseIntervalEnd), - ); - } - return super.createAnimation(); - } - - void scrollTo(int selectedItemIndex) { - SchedulerBinding.instance.addPostFrameCallback((_) { - if (itemKeys[selectedItemIndex].currentContext != null) { - Scrollable.ensureVisible(itemKeys[selectedItemIndex].currentContext!); - } - }); - } - - @override - Duration get transitionDuration => - popUpAnimationStyle?.duration ?? _kMenuDuration; - - @override - bool get barrierDismissible => true; - - @override - Color? get barrierColor => null; - - @override - final String barrierLabel; - - @override - Widget buildTransitions( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - if (!animation.isCompleted) { - final screenWidth = MediaQuery.of(context).size.width; - final screenHeight = MediaQuery.of(context).size.height; - final size = position.toSize(Size(screenWidth, screenHeight)); - final center = size.width / 2.0; - final alignment = FractionalOffset( - (screenWidth - position.right - center) / screenWidth, - (screenHeight - position.bottom - center) / screenHeight, - ); - child = FadeTransition( - opacity: animation, - child: ScaleTransition( - alignment: alignment, - scale: animation, - child: child, - ), - ); - } - return child; - } - - @override - Widget buildPage( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - ) { - int? selectedItemIndex; - if (initialValue != null) { - for (int index = 0; - selectedItemIndex == null && index < items.length; - index += 1) { - if (items[index].represents(initialValue)) { - selectedItemIndex = index; - } - } - } - if (selectedItemIndex != null) { - scrollTo(selectedItemIndex); - } - - _kPopupMenuKey ??= GlobalKey<_PopupMenuState>(); - final Widget menu = _PopupMenu( - key: _kPopupMenuKey, - route: this, - itemKeys: itemKeys, - semanticLabel: semanticLabel, - constraints: constraints, - clipBehavior: clipBehavior, - ); - final MediaQueryData mediaQuery = MediaQuery.of(context); - return MediaQuery.removePadding( - context: context, - removeTop: true, - removeBottom: true, - removeLeft: true, - removeRight: true, - child: Builder( - builder: (BuildContext context) { - return CustomSingleChildLayout( - delegate: _PopupMenuRouteLayout( - position, - itemSizes, - selectedItemIndex, - Directionality.of(context), - mediaQuery.padding, - _avoidBounds(mediaQuery), - ), - child: capturedThemes.wrap(menu), - ); - }, - ), - ); - } - - Set _avoidBounds(MediaQueryData mediaQuery) { - return DisplayFeatureSubScreen.avoidBounds(mediaQuery).toSet(); - } -} - -/// Show a popup menu that contains the `items` at `position`. -/// -/// The `items` parameter must not be empty. -/// -/// If `initialValue` is specified then the first item with a matching value -/// will be highlighted and the value of `position` gives the rectangle whose -/// vertical center will be aligned with the vertical center of the highlighted -/// item (when possible). -/// -/// If `initialValue` is not specified then the top of the menu will be aligned -/// with the top of the `position` rectangle. -/// -/// In both cases, the menu position will be adjusted if necessary to fit on the -/// screen. -/// -/// Horizontally, the menu is positioned so that it grows in the direction that -/// has the most room. For example, if the `position` describes a rectangle on -/// the left edge of the screen, then the left edge of the menu is aligned with -/// the left edge of the `position`, and the menu grows to the right. If both -/// edges of the `position` are equidistant from the opposite edge of the -/// screen, then the ambient [Directionality] is used as a tie-breaker, -/// preferring to grow in the reading direction. -/// -/// The positioning of the `initialValue` at the `position` is implemented by -/// iterating over the `items` to find the first whose -/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then -/// summing the values of [PopupMenuEntry.height] for all the preceding widgets -/// in the list. -/// -/// The `elevation` argument specifies the z-coordinate at which to place the -/// menu. The elevation defaults to 8, the appropriate elevation for popup -/// menus. -/// -/// The `context` argument is used to look up the [Navigator] and [Theme] for -/// the menu. It is only used when the method is called. Its corresponding -/// widget can be safely removed from the tree before the popup menu is closed. -/// -/// The `useRootNavigator` argument is used to determine whether to push the -/// menu to the [Navigator] furthest from or nearest to the given `context`. It -/// is `false` by default. -/// -/// The `semanticLabel` argument is used by accessibility frameworks to -/// announce screen transitions when the menu is opened and closed. If this -/// label is not provided, it will default to -/// [MaterialLocalizations.popupMenuLabel]. -/// -/// The `clipBehavior` argument is used to clip the shape of the menu. Defaults to -/// [Clip.none]. -/// -/// See also: -/// -/// * [PopupMenuItem], a popup menu entry for a single value. -/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. -/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. -/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by -/// calling this method automatically. -/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered -/// semantics. -Future showMenu({ - required BuildContext context, - required RelativeRect position, - required List> items, - T? initialValue, - double? elevation, - Color? shadowColor, - Color? surfaceTintColor, - String? semanticLabel, - ShapeBorder? shape, - Color? color, - bool useRootNavigator = false, - BoxConstraints? constraints, - Clip clipBehavior = Clip.none, - RouteSettings? routeSettings, - AnimationStyle? popUpAnimationStyle, -}) { - assert(items.isNotEmpty); - assert(debugCheckHasMaterialLocalizations(context)); - - switch (Theme.of(context).platform) { - case TargetPlatform.iOS: - case TargetPlatform.macOS: - break; - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - semanticLabel ??= MaterialLocalizations.of(context).popupMenuLabel; - } - - final List menuItemKeys = - List.generate(items.length, (int index) => GlobalKey()); - final NavigatorState navigator = - Navigator.of(context, rootNavigator: useRootNavigator); - return navigator.push( - _PopupMenuRoute( - position: position, - items: items, - itemKeys: menuItemKeys, - initialValue: initialValue, - elevation: elevation, - shadowColor: shadowColor, - surfaceTintColor: surfaceTintColor, - semanticLabel: semanticLabel, - barrierLabel: MaterialLocalizations.of(context).menuDismissLabel, - shape: shape, - color: color, - capturedThemes: - InheritedTheme.capture(from: context, to: navigator.context), - constraints: constraints, - clipBehavior: clipBehavior, - settings: routeSettings, - popUpAnimationStyle: popUpAnimationStyle, - ), - ); -} - -/// Signature for the callback invoked when a menu item is selected. The -/// argument is the value of the [PopupMenuItem] that caused its menu to be -/// dismissed. -/// -/// Used by [PopupMenuButton.onSelected]. -typedef PopupMenuItemSelected = void Function(T value); - -/// Signature for the callback invoked when a [PopupMenuButton] is dismissed -/// without selecting an item. -/// -/// Used by [PopupMenuButton.onCanceled]. -typedef PopupMenuCanceled = void Function(); - -/// Signature used by [PopupMenuButton] to lazily construct the items shown when -/// the button is pressed. -/// -/// Used by [PopupMenuButton.itemBuilder]. -typedef PopupMenuItemBuilder = List> Function( - BuildContext context, -); - -/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed -/// because an item was selected. The value passed to [onSelected] is the value of -/// the selected menu item. -/// -/// One of [child] or [icon] may be provided, but not both. If [icon] is provided, -/// then [PopupMenuButton] behaves like an [IconButton]. -/// -/// If both are null, then a standard overflow icon is created (depending on the -/// platform). -/// -/// ## Updating to [MenuAnchor] -/// -/// There is a Material 3 component, -/// [MenuAnchor] that is preferred for applications that are configured -/// for Material 3 (see [ThemeData.useMaterial3]). -/// The [MenuAnchor] widget's visuals -/// are a little bit different, see the Material 3 spec at -/// for -/// more details. -/// -/// The [MenuAnchor] widget's API is also slightly different. -/// [MenuAnchor]'s were built to be lower level interface for -/// creating menus that are displayed from an anchor. -/// -/// There are a few steps you would take to migrate from -/// [PopupMenuButton] to [MenuAnchor]: -/// -/// 1. Instead of using the [PopupMenuButton.itemBuilder] to build -/// a list of [PopupMenuEntry]s, you would use the [MenuAnchor.menuChildren] -/// which takes a list of [Widget]s. Usually, you would use a list of -/// [MenuItemButton]s as shown in the example below. -/// -/// 2. Instead of using the [PopupMenuButton.onSelected] callback, you would -/// set individual callbacks for each of the [MenuItemButton]s using the -/// [MenuItemButton.onPressed] property. -/// -/// 3. To anchor the [MenuAnchor] to a widget, you would use the [MenuAnchor.builder] -/// to return the widget of choice - usually a [TextButton] or an [IconButton]. -/// -/// 4. You may want to style the [MenuItemButton]s, see the [MenuItemButton] -/// documentation for details. -/// -/// Use the sample below for an example of migrating from [PopupMenuButton] to -/// [MenuAnchor]. -/// -/// {@tool dartpad} -/// This example shows a menu with three items, selecting between an enum's -/// values and setting a `selectedMenu` field based on the selection. -/// -/// ** See code in examples/api/lib/material/popup_menu/popup_menu.0.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This example shows how to migrate the above to a [MenuAnchor]. -/// -/// ** See code in examples/api/lib/material/menu_anchor/menu_anchor.2.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This sample shows the creation of a popup menu, as described in: -/// https://m3.material.io/components/menus/overview -/// -/// ** See code in examples/api/lib/material/popup_menu/popup_menu.1.dart ** -/// {@end-tool} -/// -/// {@tool dartpad} -/// This sample showcases how to override the [PopupMenuButton] animation -/// curves and duration using [AnimationStyle]. -/// -/// ** See code in examples/api/lib/material/popup_menu/popup_menu.2.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [PopupMenuItem], a popup menu entry for a single value. -/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line. -/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. -/// * [showMenu], a method to dynamically show a popup menu at a given location. -class PopupMenuButton extends StatefulWidget { - /// Creates a button that shows a popup menu. - const PopupMenuButton({ - super.key, - required this.itemBuilder, - this.initialValue, - this.onOpened, - this.onSelected, - this.onCanceled, - this.tooltip, - this.elevation, - this.shadowColor, - this.surfaceTintColor, - this.padding = const EdgeInsets.all(8.0), - this.child, - this.splashRadius, - this.icon, - this.iconSize, - this.offset = Offset.zero, - this.enabled = true, - this.shape, - this.color, - this.iconColor, - this.enableFeedback, - this.constraints, - this.position, - this.clipBehavior = Clip.none, - this.useRootNavigator = false, - this.popUpAnimationStyle, - this.routeSettings, - this.style, - }) : assert( - !(child != null && icon != null), - 'You can only pass [child] or [icon], not both.', - ); - - /// Called when the button is pressed to create the items to show in the menu. - final PopupMenuItemBuilder itemBuilder; - - /// The value of the menu item, if any, that should be highlighted when the menu opens. - final T? initialValue; - - /// Called when the popup menu is shown. - final VoidCallback? onOpened; - - /// Called when the user selects a value from the popup menu created by this button. - /// - /// If the popup menu is dismissed without selecting a value, [onCanceled] is - /// called instead. - final PopupMenuItemSelected? onSelected; - - /// Called when the user dismisses the popup menu without selecting an item. - /// - /// If the user selects a value, [onSelected] is called instead. - final PopupMenuCanceled? onCanceled; - - /// Text that describes the action that will occur when the button is pressed. - /// - /// This text is displayed when the user long-presses on the button and is - /// used for accessibility. - final String? tooltip; - - /// The z-coordinate at which to place the menu when open. This controls the - /// size of the shadow below the menu. - /// - /// Defaults to 8, the appropriate elevation for popup menus. - final double? elevation; - - /// The color used to paint the shadow below the menu. - /// - /// If null then the ambient [PopupMenuThemeData.shadowColor] is used. - /// If that is null too, then the overall theme's [ThemeData.shadowColor] - /// (default black) is used. - final Color? shadowColor; - - /// The color used as an overlay on [color] to indicate elevation. - /// - /// This is not recommended for use. [Material 3 spec](https://m3.material.io/styles/color/the-color-system/color-roles) - /// introduced a set of tone-based surfaces and surface containers in its [ColorScheme], - /// which provide more flexibility. The intention is to eventually remove surface tint color from - /// the framework. - /// - /// If null, [PopupMenuThemeData.surfaceTintColor] is used. If that - /// is also null, the default value is [Colors.transparent]. - /// - /// See [Material.surfaceTintColor] for more details on how this - /// overlay is applied. - final Color? surfaceTintColor; - - /// Matches IconButton's 8 dps padding by default. In some cases, notably where - /// this button appears as the trailing element of a list item, it's useful to be able - /// to set the padding to zero. - final EdgeInsetsGeometry padding; - - /// The splash radius. - /// - /// If null, default splash radius of [InkWell] or [IconButton] is used. - final double? splashRadius; - - /// If provided, [child] is the widget used for this button - /// and the button will utilize an [InkWell] for taps. - final Widget? child; - - /// If provided, the [icon] is used for this button - /// and the button will behave like an [IconButton]. - final Widget? icon; - - /// The offset is applied relative to the initial position - /// set by the [position]. - /// - /// When not set, the offset defaults to [Offset.zero]. - final Offset offset; - - /// Whether this popup menu button is interactive. - /// - /// Defaults to true. - /// - /// If true, the button will respond to presses by displaying the menu. - /// - /// If false, the button is styled with the disabled color from the - /// current [Theme] and will not respond to presses or show the popup - /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called. - /// - /// This can be useful in situations where the app needs to show the button, - /// but doesn't currently have anything to show in the menu. - final bool enabled; - - /// If provided, the shape used for the menu. - /// - /// If this property is null, then [PopupMenuThemeData.shape] is used. - /// If [PopupMenuThemeData.shape] is also null, then the default shape for - /// [MaterialType.card] is used. This default shape is a rectangle with - /// rounded edges of BorderRadius.circular(2.0). - final ShapeBorder? shape; - - /// If provided, the background color used for the menu. - /// - /// If this property is null, then [PopupMenuThemeData.color] is used. - /// If [PopupMenuThemeData.color] is also null, then - /// [ThemeData.cardColor] is used in Material 2. In Material3, defaults to - /// [ColorScheme.surfaceContainer]. - final Color? color; - - /// If provided, this color is used for the button icon. - /// - /// If this property is null, then [PopupMenuThemeData.iconColor] is used. - /// If [PopupMenuThemeData.iconColor] is also null then defaults to - /// [IconThemeData.color]. - final Color? iconColor; - - /// Whether detected gestures should provide acoustic and/or haptic feedback. - /// - /// For example, on Android a tap will produce a clicking sound and a - /// long-press will produce a short vibration, when feedback is enabled. - /// - /// See also: - /// - /// * [Feedback] for providing platform-specific feedback to certain actions. - final bool? enableFeedback; - - /// If provided, the size of the [Icon]. - /// - /// If this property is null, then [IconThemeData.size] is used. - /// If [IconThemeData.size] is also null, then - /// default size is 24.0 pixels. - final double? iconSize; - - /// Optional size constraints for the menu. - /// - /// When unspecified, defaults to: - /// ```dart - /// const BoxConstraints( - /// minWidth: 2.0 * 56.0, - /// maxWidth: 5.0 * 56.0, - /// ) - /// ``` - /// - /// The default constraints ensure that the menu width matches maximum width - /// recommended by the Material Design guidelines. - /// Specifying this parameter enables creation of menu wider than - /// the default maximum width. - final BoxConstraints? constraints; - - /// Whether the popup menu is positioned over or under the popup menu button. - /// - /// [offset] is used to change the position of the popup menu relative to the - /// position set by this parameter. - /// - /// If this property is `null`, then [PopupMenuThemeData.position] is used. If - /// [PopupMenuThemeData.position] is also `null`, then the position defaults - /// to [PopupMenuPosition.over] which makes the popup menu appear directly - /// over the button that was used to create it. - final PopupMenuPosition? position; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// The [clipBehavior] argument is used the clip shape of the menu. - /// - /// Defaults to [Clip.none]. - final Clip clipBehavior; - - /// Used to determine whether to push the menu to the [Navigator] furthest - /// from or nearest to the given `context`. - /// - /// Defaults to false. - final bool useRootNavigator; - - /// Used to override the default animation curves and durations of the popup - /// menu's open and close transitions. - /// - /// If [AnimationStyle.curve] is provided, it will be used to override - /// the default popup animation curve. Otherwise, defaults to [Curves.linear]. - /// - /// If [AnimationStyle.reverseCurve] is provided, it will be used to - /// override the default popup animation reverse curve. Otherwise, defaults to - /// `Interval(0.0, 2.0 / 3.0)`. - /// - /// If [AnimationStyle.duration] is provided, it will be used to override - /// the default popup animation duration. Otherwise, defaults to 300ms. - /// - /// To disable the theme animation, use [AnimationStyle.noAnimation]. - /// - /// If this is null, then the default animation will be used. - final AnimationStyle? popUpAnimationStyle; - - /// Optional route settings for the menu. - /// - /// See [RouteSettings] for details. - final RouteSettings? routeSettings; - - /// Customizes this icon button's appearance. - /// - /// The [style] is only used for Material 3 [IconButton]s. If [ThemeData.useMaterial3] - /// is set to true, [style] is preferred for icon button customization, and any - /// parameters defined in [style] will override the same parameters in [IconButton]. - /// - /// Null by default. - final ButtonStyle? style; - - @override - PopupMenuButtonState createState() => PopupMenuButtonState(); -} - -/// The [State] for a [PopupMenuButton]. -/// -/// See [showButtonMenu] for a way to programmatically open the popup menu -/// of your button state. -class PopupMenuButtonState extends State> { - /// A method to show a popup menu with the items supplied to - /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. - /// - /// By default, it is called when the user taps the button and [PopupMenuButton.enabled] - /// is set to `true`. Moreover, you can open the button by calling the method manually. - /// - /// You would access your [PopupMenuButtonState] using a [GlobalKey] and - /// show the menu of the button with `globalKey.currentState.showButtonMenu`. - void showButtonMenu() { - final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - final RenderBox button = context.findRenderObject()! as RenderBox; - final RenderBox overlay = - Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox; - final PopupMenuPosition popupMenuPosition = - widget.position ?? popupMenuTheme.position ?? PopupMenuPosition.over; - late Offset offset; - switch (popupMenuPosition) { - case PopupMenuPosition.over: - offset = widget.offset; - case PopupMenuPosition.under: - offset = Offset(0.0, button.size.height) + widget.offset; - if (widget.child == null) { - // Remove the padding of the icon button. - offset -= Offset(0.0, widget.padding.vertical / 2); - } - } - final RelativeRect position = RelativeRect.fromRect( - Rect.fromPoints( - button.localToGlobal(offset, ancestor: overlay), - button.localToGlobal( - button.size.bottomRight(Offset.zero) + offset, - ancestor: overlay, - ), - ), - Offset.zero & overlay.size, - ); - final List> items = widget.itemBuilder(context); - // Only show the menu if there is something to show - if (items.isNotEmpty) { - var popUpAnimationStyle = widget.popUpAnimationStyle; - if (popUpAnimationStyle == null && - defaultTargetPlatform == TargetPlatform.iOS) { - popUpAnimationStyle = AnimationStyle( - curve: Curves.easeInOut, - duration: const Duration(milliseconds: 300), - ); - } - widget.onOpened?.call(); - showMenu( - context: context, - elevation: widget.elevation ?? popupMenuTheme.elevation, - shadowColor: widget.shadowColor ?? popupMenuTheme.shadowColor, - surfaceTintColor: - widget.surfaceTintColor ?? popupMenuTheme.surfaceTintColor, - items: items, - initialValue: widget.initialValue, - position: position, - shape: widget.shape ?? popupMenuTheme.shape, - color: widget.color ?? popupMenuTheme.color, - constraints: widget.constraints, - clipBehavior: widget.clipBehavior, - useRootNavigator: widget.useRootNavigator, - popUpAnimationStyle: popUpAnimationStyle, - routeSettings: widget.routeSettings, - ).then((T? newValue) { - if (!mounted) { - return null; - } - if (newValue == null) { - widget.onCanceled?.call(); - return null; - } - widget.onSelected?.call(newValue); - }); - } - } - - @override - Widget build(BuildContext context) { - final IconThemeData iconTheme = IconTheme.of(context); - final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); - final bool enableFeedback = widget.enableFeedback ?? - PopupMenuTheme.of(context).enableFeedback ?? - true; - - assert(debugCheckHasMaterialLocalizations(context)); - - if (widget.child != null) { - return AnimatedGestureDetector( - scaleFactor: 0.95, - onTapUp: widget.enabled ? showButtonMenu : null, - child: widget.child!, - ); - } - - return IconButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), - padding: widget.padding, - splashRadius: widget.splashRadius, - iconSize: widget.iconSize ?? popupMenuTheme.iconSize ?? iconTheme.size, - color: widget.iconColor ?? popupMenuTheme.iconColor ?? iconTheme.color, - tooltip: - widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, - onPressed: widget.enabled ? showButtonMenu : null, - enableFeedback: enableFeedback, - style: widget.style, - ); - } -} - -class _PopupMenuDefaultsM2 extends PopupMenuThemeData { - _PopupMenuDefaultsM2(this.context) : super(elevation: 8.0); - - final BuildContext context; - late final ThemeData _theme = Theme.of(context); - late final TextTheme _textTheme = _theme.textTheme; - - @override - TextStyle? get textStyle => _textTheme.titleMedium; - - static EdgeInsets menuHorizontalPadding = - const EdgeInsets.symmetric(horizontal: 16.0); -} - -// BEGIN GENERATED TOKEN PROPERTIES - PopupMenu - -// Do not edit by hand. The code between the "BEGIN GENERATED" and -// "END GENERATED" comments are generated from data in the Material -// Design token database by the script: -// dev/tools/gen_defaults/bin/gen_defaults.dart. - -class _PopupMenuDefaultsM3 extends PopupMenuThemeData { - _PopupMenuDefaultsM3(this.context) : super(elevation: 3.0); - - final BuildContext context; - late final ThemeData _theme = Theme.of(context); - late final ColorScheme _colors = _theme.colorScheme; - late final TextTheme _textTheme = _theme.textTheme; - - @override - WidgetStateProperty? get labelTextStyle { - return WidgetStateProperty.resolveWith((Set states) { - final TextStyle style = _textTheme.labelLarge!; - if (states.contains(WidgetState.disabled)) { - return style.apply(color: _colors.onSurface.withValues(alpha: 0.38)); - } - return style.apply(color: _colors.onSurface); - }); - } - - @override - Color? get color => _colors.surfaceContainer; - - @override - Color? get shadowColor => _colors.shadow; - - @override - Color? get surfaceTintColor => Colors.transparent; - - @override - ShapeBorder? get shape => const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(4.0)), - ); - - // TODO(tahatesser): This is taken from https://m3.material.io/components/menus/specs - // Update this when the token is available. - static EdgeInsets menuHorizontalPadding = - const EdgeInsets.symmetric(horizontal: 12.0); -} -// END GENERATED TOKEN PROPERTIES - PopupMenu - -extension PopupMenuColors on BuildContext { - Color get popupMenuBackgroundColor { - if (Theme.of(this).brightness == Brightness.light) { - return Theme.of(this).colorScheme.surface; - } - return const Color(0xFF23262B); - } -} - -class _CurveTween extends Animatable { - /// Creates a curve tween. - _CurveTween({required this.curve}); - - /// The curve to use when transforming the value of the animation. - Curve curve; - - @override - double transform(double t) { - return curve.transform(t.clamp(0, 1)); - } - - @override - String toString() => - '${objectRuntimeType(this, 'CurveTween')}(curve: $curve)'; -} diff --git a/frontend/appflowy_flutter/lib/shared/red_dot.dart b/frontend/appflowy_flutter/lib/shared/red_dot.dart deleted file mode 100644 index 149cadae04..0000000000 --- a/frontend/appflowy_flutter/lib/shared/red_dot.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class NotificationRedDot extends StatelessWidget { - const NotificationRedDot({ - super.key, - this.size = 6, - }); - - final double size; - - @override - Widget build(BuildContext context) { - return Container( - width: size, - height: size, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - color: const Color(0xFFFF2214), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/settings/show_settings.dart b/frontend/appflowy_flutter/lib/shared/settings/show_settings.dart deleted file mode 100644 index 81831346e9..0000000000 --- a/frontend/appflowy_flutter/lib/shared/settings/show_settings.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; -import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' - show UserProfilePB; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -final GlobalKey _settingsDialogKey = GlobalKey(); - -// show settings dialog with user profile for fully customized settings dialog -void showSettingsDialog( - BuildContext context, - UserProfilePB userProfile, [ - UserWorkspaceBloc? bloc, - SettingsPage? initPage, -]) { - AFFocusManager.of(context).notifyLoseFocus(); - showDialog( - context: context, - builder: (dialogContext) => MultiBlocProvider( - key: _settingsDialogKey, - providers: [ - BlocProvider.value( - value: BlocProvider.of(dialogContext), - ), - BlocProvider.value(value: bloc ?? context.read()), - ], - child: SettingsDialog( - userProfile, - initPage: initPage, - didLogout: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - dismissDialog: () { - if (Navigator.of(dialogContext).canPop()) { - return Navigator.of(dialogContext).pop(); - } - Log.warn("Can't pop dialog context"); - }, - restartApp: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - ), - ), - ); -} - -// show settings dialog without user profile for simple settings dialog -// only support -// - language -// - self-host -// - support -void showSimpleSettingsDialog(BuildContext context) { - showDialog( - context: context, - builder: (dialogContext) => const SimpleSettingsDialog(), - ); -} diff --git a/frontend/appflowy_flutter/lib/shared/text_field/text_filed_with_metric_lines.dart b/frontend/appflowy_flutter/lib/shared/text_field/text_filed_with_metric_lines.dart deleted file mode 100644 index b8db272a4b..0000000000 --- a/frontend/appflowy_flutter/lib/shared/text_field/text_filed_with_metric_lines.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:flutter/material.dart'; - -class TextFieldWithMetricLines extends StatefulWidget { - const TextFieldWithMetricLines({ - super.key, - this.controller, - this.focusNode, - this.maxLines, - this.style, - this.decoration, - this.onLineCountChange, - this.enabled = true, - }); - - final TextEditingController? controller; - final FocusNode? focusNode; - final int? maxLines; - final TextStyle? style; - final InputDecoration? decoration; - final void Function(int count)? onLineCountChange; - final bool enabled; - - @override - State createState() => - _TextFieldWithMetricLinesState(); -} - -class _TextFieldWithMetricLinesState extends State { - final key = GlobalKey(); - late final controller = widget.controller ?? TextEditingController(); - - @override - void initState() { - super.initState(); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - updateDisplayedLineCount(context); - }); - } - - @override - void dispose() { - if (widget.controller == null) { - // dispose the controller if it was created by this widget - controller.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - key: key, - enabled: widget.enabled, - controller: widget.controller, - focusNode: widget.focusNode, - maxLines: widget.maxLines, - style: widget.style, - decoration: widget.decoration, - onChanged: (_) => updateDisplayedLineCount(context), - ); - } - - // calculate the number of lines that would be displayed in the text field - void updateDisplayedLineCount(BuildContext context) { - if (widget.onLineCountChange == null) { - return; - } - - final renderObject = key.currentContext?.findRenderObject(); - if (renderObject == null || renderObject is! RenderBox) { - return; - } - - final size = renderObject.size; - final text = controller.buildTextSpan( - context: context, - style: widget.style, - withComposing: false, - ); - final textPainter = TextPainter( - text: text, - textDirection: Directionality.of(context), - ); - - textPainter.layout(minWidth: size.width, maxWidth: size.width); - - final lines = textPainter.computeLineMetrics().length; - widget.onLineCountChange?.call(lines); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/time_format.dart b/frontend/appflowy_flutter/lib/shared/time_format.dart deleted file mode 100644 index ba9ce0fabd..0000000000 --- a/frontend/appflowy_flutter/lib/shared/time_format.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:provider/provider.dart'; -import 'package:time/time.dart'; - -String formatTimestampWithContext( - BuildContext context, { - required int timestamp, - String? prefix, -}) { - final now = DateTime.now(); - final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp); - final difference = now.difference(dateTime); - final String date; - - final dateFormat = context.read().state.dateFormat; - final timeFormat = context.read().state.timeFormat; - - 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 = dateFormat.formatDate(dateTime, false); - } - - if (difference.inHours >= 1 && prefix != null) { - return '$prefix $date'; - } - - return date; -} diff --git a/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart b/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart deleted file mode 100644 index 6e50a922a7..0000000000 --- a/frontend/appflowy_flutter/lib/shared/version_checker/version_checker.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:auto_updater/auto_updater.dart'; -import 'package:collection/collection.dart'; -import 'package:http/http.dart' as http; -import 'package:universal_platform/universal_platform.dart'; -import 'package:xml/xml.dart' as xml; - -final versionChecker = VersionChecker(); - -/// Version checker class to handle update checks using appcast XML feeds -class VersionChecker { - factory VersionChecker() => _instance; - - VersionChecker._internal(); - String? _feedUrl; - - static final VersionChecker _instance = VersionChecker._internal(); - - /// Sets the appcast XML feed URL - void setFeedUrl(String url) { - _feedUrl = url; - - if (UniversalPlatform.isWindows || UniversalPlatform.isMacOS) { - autoUpdater.setFeedURL(url); - // disable the auto update check - autoUpdater.setScheduledCheckInterval(0); - } - } - - /// Checks for updates by fetching and parsing the appcast XML - /// Returns a list of [AppcastItem] or throws an exception if the feed URL is not set - Future checkForUpdateInformation() async { - if (_feedUrl == null) { - Log.error('Feed URL is not set'); - return null; - } - - try { - final response = await http.get(Uri.parse(_feedUrl!)); - if (response.statusCode != 200) { - Log.info('Failed to fetch appcast XML: ${response.statusCode}'); - return null; - } - - // Parse XML content - final document = xml.XmlDocument.parse(response.body); - final items = document.findAllElements('item'); - - // Convert XML items to AppcastItem objects - return items - .map(_parseAppcastItem) - .nonNulls - .firstWhereOrNull((e) => e.os == ApplicationInfo.os); - } catch (e) { - Log.info('Failed to check for updates: $e'); - } - - return null; - } - - /// For Windows and macOS, calling this API will trigger the auto updater to check for updates - /// For Linux, it will open the official website in the browser if there is a new version - - Future checkForUpdate() async { - if (UniversalPlatform.isLinux) { - // open the official website in the browser - await afLaunchUrlString('https://appflowy.com/download'); - } else { - await autoUpdater.checkForUpdates(); - } - } - - AppcastItem? _parseAppcastItem(xml.XmlElement item) { - final enclosure = item.findElements('enclosure').firstOrNull; - return AppcastItem.fromJson({ - 'title': item.findElements('title').firstOrNull?.innerText, - 'versionString': item - .findElements('sparkle:shortVersionString') - .firstOrNull - ?.innerText, - 'displayVersionString': item - .findElements('sparkle:shortVersionString') - .firstOrNull - ?.innerText, - 'releaseNotesUrl': - item.findElements('releaseNotesLink').firstOrNull?.innerText, - 'pubDate': item.findElements('pubDate').firstOrNull?.innerText, - 'fileURL': enclosure?.getAttribute('url') ?? '', - 'os': enclosure?.getAttribute('sparkle:os') ?? '', - 'criticalUpdate': - enclosure?.getAttribute('sparkle:criticalUpdate') ?? false, - }); - } -} diff --git a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart index 4738be78f3..1640383588 100644 --- a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart @@ -1,6 +1,7 @@ -import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:window_manager/window_manager.dart'; class WindowsButtonListener extends WindowListener { @@ -37,33 +38,31 @@ class _WindowTitleBarState extends State { void initState() { super.initState(); - if (UniversalPlatform.isWindows || UniversalPlatform.isLinux) { + if (PlatformExtension.isWindows || PlatformExtension.isLinux) { windowsButtonListener = WindowsButtonListener(); windowManager.addListener(windowsButtonListener!); - windowsButtonListener!.isMaximized.addListener(_isMaximizedChanged); + windowsButtonListener!.isMaximized.addListener(() { + if (mounted) { + setState( + () => isMaximized = windowsButtonListener!.isMaximized.value, + ); + } + }); } else { windowsButtonListener = null; } - windowManager - .isMaximized() - .then((v) => mounted ? setState(() => isMaximized = v) : null); - } - - void _isMaximizedChanged() { - if (mounted) { - setState(() => isMaximized = windowsButtonListener!.isMaximized.value); - } + windowManager.isMaximized().then( + (v) => mounted ? setState(() => isMaximized = v) : null, + ); } @override void dispose() { if (windowsButtonListener != null) { windowManager.removeListener(windowsButtonListener!); - windowsButtonListener!.isMaximized.removeListener(_isMaximizedChanged); windowsButtonListener?.dispose(); } - super.dispose(); } diff --git a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart index 5a8c0fa651..cf95d8f8a0 100644 --- a/frontend/appflowy_flutter/lib/startup/deps_resolver.dart +++ b/frontend/appflowy_flutter/lib/startup/deps_resolver.dart @@ -3,6 +3,8 @@ import 'package:appflowy/core/network_monitor.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart'; import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; @@ -10,9 +12,11 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/auth/supabase_auth_service.dart'; import 'package:appflowy/user/application/prelude.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/user_listener.dart'; +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_panel_bloc.dart'; @@ -32,12 +36,13 @@ import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.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:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/file_picker/file_picker_impl.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get_it/get_it.dart'; -import 'package:universal_platform/universal_platform.dart'; +import 'package:http/http.dart' as http; class DependencyResolver { static Future resolve( @@ -80,13 +85,47 @@ void _resolveCommonService( () => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(), ); + getIt.registerFactoryAsync( + () async { + final result = await UserBackendService.getCurrentUserProfile(); + return result.fold( + (s) { + return HttpOpenAIRepository( + client: http.Client(), + apiKey: s.openaiKey, + ); + }, + (e) { + throw Exception('Failed to get user profile: ${e.msg}'); + }, + ); + }, + ); + + getIt.registerFactoryAsync( + () async { + final result = await UserBackendService.getCurrentUserProfile(); + return result.fold( + (s) { + return HttpStabilityAIRepository( + client: http.Client(), + apiKey: s.stabilityAiKey, + ); + }, + (e) { + throw Exception('Failed to get user profile: ${e.msg}'); + }, + ); + }, + ); + getIt.registerFactory( () => ClipboardService(), ); // theme getIt.registerFactory( - () => UniversalPlatform.isMobile ? MobileAppearance() : DesktopAppearance(), + () => PlatformExtension.isMobile ? MobileAppearance() : DesktopAppearance(), ); getIt.registerFactory( @@ -102,10 +141,13 @@ void _resolveUserDeps(GetIt getIt, IntegrationMode mode) { case AuthenticatorType.local: getIt.registerFactory( () => BackendAuthService( - AuthTypePB.Local, + AuthenticatorPB.Local, ), ); break; + case AuthenticatorType.supabase: + getIt.registerFactory(() => SupabaseAuthService()); + break; case AuthenticatorType.appflowyCloud: case AuthenticatorType.appflowyCloudSelfHost: case AuthenticatorType.appflowyCloudDevelop: @@ -142,8 +184,8 @@ void _resolveHomeDeps(GetIt getIt) { ); // share - getIt.registerFactoryParam( - (view, _) => ShareBloc(view: view), + getIt.registerFactoryParam( + (view, _) => DocumentShareBloc(view: view), ); getIt.registerSingleton(ActionNavigationBloc()); @@ -168,6 +210,11 @@ void _resolveFolderDeps(GetIt getIt) { ), ); + // Settings + getIt.registerFactoryParam( + (user, _) => SettingsDialogBloc(user), + ); + // User getIt.registerFactoryParam( (user, _) => SettingsUserViewBloc(user), diff --git a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart index 5bb08e3fdf..b81860cd99 100644 --- a/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart +++ b/frontend/appflowy_flutter/lib/startup/plugin/plugin.dart @@ -1,12 +1,13 @@ -library; +library flowy_plugin; + +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter/widgets.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flutter/widgets.dart'; export "./src/sandbox.dart"; @@ -75,7 +76,6 @@ abstract class PluginWidgetBuilder with NavigationItem { Widget buildWidget({ required PluginContext context, required bool shrinkWrap, - Map? data, }); } diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 7a282b3856..3dac4f229c 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -2,12 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/desktop_floating_toolbar.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart'; -import 'package:appflowy/util/expand_views.dart'; +import 'package:appflowy/startup/tasks/feature_flag_task.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; @@ -17,7 +14,6 @@ import 'deps_resolver.dart'; import 'entry_point.dart'; import 'launch_configuration.dart'; import 'plugin/plugin.dart'; -import 'tasks/file_storage_task.dart'; import 'tasks/prelude.dart'; final getIt = GetIt.instance; @@ -33,8 +29,6 @@ class FlowyRunnerContext { } Future runAppFlowy({bool isAnon = false}) async { - Log.info('restart AppFlowy: isAnon: $isAnon'); - if (kReleaseMode) { await FlowyRunner.run( AppFlowyApplication(), @@ -115,13 +109,11 @@ class FlowyRunner { [ // this task should be first task, for handling platform errors. // don't catch errors in test mode - if (!mode.isUnitTest && !mode.isIntegrationTest) - const PlatformErrorCatcherTask(), - if (!mode.isUnitTest) const InitSentryTask(), + if (!mode.isUnitTest) const PlatformErrorCatcherTask(), // this task should be second task, for handling memory leak. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. MemoryLeakDetectorTask(), - DebugTask(), + const DebugTask(), const FeatureFlagTask(), // localization @@ -132,7 +124,6 @@ class FlowyRunner { InitRustSDKTask(customApplicationPath: applicationDataDirectory), // Load Plugins, like document, grid ... const PluginLoadTask(), - const FileStorageTask(), // init the app widget // ignore in test mode @@ -140,9 +131,8 @@ class FlowyRunner { // The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information. // It is unable to get the device information from the test environment. const ApplicationInfoTask(), - // The auto update task should be placed after the ApplicationInfoTask to fetch the latest version. - if (!mode.isIntegrationTest) AutoUpdateTask(), const HotKeyTask(), + if (isSupabaseEnabled) InitSupabaseTask(), if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), const InitAppWidgetTask(), const InitPlatformServiceTask(), @@ -186,11 +176,6 @@ Future initGetIt( }, ); getIt.registerSingleton(PluginSandbox()); - getIt.registerSingleton(ViewExpanderRegistry()); - getIt.registerSingleton(LinkHoverTriggers()); - getIt.registerSingleton( - FloatingToolbarController(), - ); await DependencyResolver.resolve(getIt, mode); } @@ -216,7 +201,6 @@ abstract class LaunchTask { LaunchTaskType get type => LaunchTaskType.dataProcessing; Future initialize(LaunchContext context); - Future dispose(); } @@ -258,9 +242,7 @@ enum IntegrationMode { // test mode bool get isTest => isUnitTest || isIntegrationTest; - bool get isUnitTest => this == IntegrationMode.unitTest; - bool get isIntegrationTest => this == IntegrationMode.integrationTest; // release mode diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 48e76cecbc..19bab6ec4b 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -2,37 +2,30 @@ import 'dart:io'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/shared/clipboard_state.dart'; import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; -import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/notification/notification_service.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; -import 'package:toastification/toastification.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'prelude.dart'; @@ -48,8 +41,6 @@ class InitAppWidgetTask extends LaunchTask { await NotificationService.initialize(); - await loadIconGroups(); - final widget = context.getIt().create(context.config); final appearanceSetting = await UserSettingsBackendService().getAppearanceSetting(); @@ -66,6 +57,7 @@ class InitAppWidgetTask extends LaunchTask { child: widget, ); + Bloc.observer = ApplicationBlocObserver(); runApp( EasyLocalization( supportedLocales: const [ @@ -82,7 +74,6 @@ class InitAppWidgetTask extends LaunchTask { Locale('el', 'GR'), Locale('fr', 'FR'), Locale('fr', 'CA'), - Locale('he'), Locale('hu', 'HU'), Locale('id', 'ID'), Locale('it', 'IT'), @@ -101,7 +92,6 @@ class InitAppWidgetTask extends LaunchTask { Locale('zh', 'TW'), Locale('fa'), Locale('hin'), - Locale('mr', 'IN'), ], path: 'assets/translations', fallbackLocale: const Locale('en'), @@ -140,8 +130,6 @@ class _ApplicationWidgetState extends State { final _commandPaletteNotifier = ValueNotifier(false); - final themeBuilder = AppFlowyDefaultTheme(); - @override void initState() { super.initState(); @@ -176,6 +164,9 @@ class _ApplicationWidgetState extends State { ), BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), + BlocProvider.value( + value: getIt()..add(const ReminderEvent.started()), + ), ], child: BlocListener( listenWhen: (_, curr) => curr.action != null, @@ -183,32 +174,24 @@ class _ApplicationWidgetState extends State { final action = state.action; WidgetsBinding.instance.addPostFrameCallback((_) { if (action?.type == ActionType.openView && - UniversalPlatform.isDesktop) { - final view = - action!.arguments?[ActionArgumentKeys.view] as ViewPB?; + PlatformExtension.isDesktop) { + final view = action!.arguments?[ActionArgumentKeys.view]; final nodePath = action.arguments?[ActionArgumentKeys.nodePath]; - final blockId = action.arguments?[ActionArgumentKeys.blockId]; if (view != null) { getIt().openPlugin( - view, - arguments: { - PluginArgumentKeys.selection: nodePath, - PluginArgumentKeys.blockId: blockId, - }, + view.plugin(), + arguments: {PluginArgumentKeys.selection: nodePath}, ); } } else if (action?.type == ActionType.openRow && - UniversalPlatform.isMobile) { + PlatformExtension.isMobile) { final view = action!.arguments?[ActionArgumentKeys.view]; if (view != null) { final view = action.arguments?[ActionArgumentKeys.view]; final rowId = action.arguments?[ActionArgumentKeys.rowId]; - AppGlobals.rootNavKey.currentContext?.pushView( - view, - arguments: { - PluginArgumentKeys.rowId: rowId, - }, - ); + AppGlobals.rootNavKey.currentContext?.pushView(view, { + PluginArgumentKeys.rowId: rowId, + }); } } }); @@ -216,60 +199,31 @@ class _ApplicationWidgetState extends State { child: BlocBuilder( builder: (context, state) { _setSystemOverlayStyle(state); - return Provider( - create: (_) => ClipboardState(), - dispose: (_, state) => state.dispose(), - child: ToastificationWrapper( - child: Listener( - onPointerSignal: (pointerSignal) { - /// This is a workaround to deal with below question: - /// When the mouse hovers over the tooltip, the scroll event is intercepted by it - /// Here, we listen for the scroll event and then remove the tooltip to avoid that situation - if (pointerSignal is PointerScrollEvent) { - Tooltip.dismissAllToolTips(); - } - }, - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - theme: state.lightTheme, - darkTheme: state.darkTheme, - themeMode: state.themeMode, - localizationsDelegates: context.localizationDelegates, - supportedLocales: context.supportedLocales, - locale: state.locale, - routerConfig: routerConfig, - builder: (context, child) { - final brightness = Theme.of(context).brightness; - final fontFamily = - state.font.orDefault(defaultFontFamily); - - return AppFlowyTheme( - data: brightness == Brightness.light - ? themeBuilder.light(fontFamily: fontFamily) - : themeBuilder.dark(fontFamily: fontFamily), - child: MediaQuery( - // use the 1.0 as the textScaleFactor to avoid the text size - // affected by the system setting. - data: MediaQuery.of(context).copyWith( - textScaler: - TextScaler.linear(state.textScaleFactor), - ), - child: overlayManagerBuilder( - context, - !UniversalPlatform.isMobile && - FeatureFlag.search.isOn - ? CommandPalette( - notifier: _commandPaletteNotifier, - child: child, - ) - : child, - ), - ), - ); - }, - ), + return MaterialApp.router( + builder: (context, child) => MediaQuery( + // use the 1.0 as the textScaleFactor to avoid the text size + // affected by the system setting. + data: MediaQuery.of(context).copyWith( + textScaler: TextScaler.linear(state.textScaleFactor), + ), + child: overlayManagerBuilder( + context, + !PlatformExtension.isMobile && FeatureFlag.search.isOn + ? CommandPalette( + notifier: _commandPaletteNotifier, + child: child, + ) + : child, ), ), + debugShowCheckedModeBanner: false, + theme: state.lightTheme, + darkTheme: state.darkTheme, + themeMode: state.themeMode, + localizationsDelegates: context.localizationDelegates, + supportedLocales: context.supportedLocales, + locale: state.locale, + routerConfig: routerConfig, ); }, ), @@ -294,12 +248,18 @@ class _ApplicationWidgetState extends State { class AppGlobals { static GlobalKey rootNavKey = GlobalKey(); - static NavigatorState get nav => rootNavKey.currentState!; - static BuildContext get context => rootNavKey.currentContext!; } +class ApplicationBlocObserver extends BlocObserver { + @override + void onError(BlocBase bloc, Object error, StackTrace stackTrace) { + Log.debug(error); + super.onError(bloc, error, stackTrace); + } +} + Future appTheme(String themeName) async { if (themeName.isEmpty) { return AppTheme.fallback; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart index 5636ed70cb..6c5bea392c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_window_size_manager.dart @@ -6,16 +6,12 @@ import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; class WindowSizeManager { - static const double minWindowHeight = 640.0; - static const double minWindowWidth = 640.0; + static const double minWindowHeight = 600.0; + static const double minWindowWidth = 800.0; // Preventing failed assertion due to Texture Descriptor Validation static const double maxWindowHeight = 8192.0; static const double maxWindowWidth = 8192.0; - // Default windows size - static const double defaultWindowHeight = 960.0; - static const double defaultWindowWidth = 1280.0; - static const double maxScaleFactor = 2.0; static const double minScaleFactor = 0.5; @@ -39,10 +35,7 @@ class WindowSizeManager { Future getSize() async { final defaultWindowSize = jsonEncode( - { - WindowSizeManager.height: defaultWindowHeight, - WindowSizeManager.width: defaultWindowWidth, - }, + {WindowSizeManager.height: 600.0, WindowSizeManager.width: 800.0}, ); final windowSize = await getIt().get(KVKeys.windowSize); final size = json.decode( @@ -90,19 +83,4 @@ class WindowSizeManager { '${scaleFactor.clamp(minScaleFactor, maxScaleFactor)}', ); } - - /// Set the window maximized status - Future setWindowMaximized(bool isMaximized) async { - await getIt() - .set(KVKeys.windowMaximized, isMaximized.toString()); - } - - /// Get the window maximized status - Future getWindowMaximized() async { - return await getIt().getWithFormat( - KVKeys.windowMaximized, - (v) => bool.tryParse(v) ?? false, - ) ?? - false; - } } diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 362b27a85a..bd5fedf526 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -1,70 +1,69 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:app_links/app_links.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; +import 'package:appflowy/startup/tasks/supabase_task.dart'; import 'package:appflowy/user/application/auth/auth_error.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; import 'package:appflowy/user/application/user_auth_listener.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; import 'package:appflowy/workspace/presentation/home/toast.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-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/material.dart'; import 'package:url_protocol/url_protocol.dart'; -const appflowyDeepLinkSchema = 'appflowy-flutter'; - class AppFlowyCloudDeepLink { AppFlowyCloudDeepLink() { - _deepLinkSubscription = _AppLinkWrapper.instance.listen( - (Uri? uri) async { - Log.info('onDeepLink: ${uri.toString()}'); - await _handleUri(uri); - }, - onError: (Object err, StackTrace stackTrace) { - Log.error('on DeepLink stream error: ${err.toString()}', stackTrace); - _deepLinkSubscription.cancel(); - }, - ); - if (Platform.isWindows) { - // register deep link for Windows - registerProtocolHandler(appflowyDeepLinkSchema); + if (_deeplinkSubscription == null) { + _deeplinkSubscription = _appLinks.uriLinkStream.listen( + (Uri? uri) async { + Log.info('onDeepLink: ${uri.toString()}'); + await _handleUri(uri); + }, + onError: (Object err, StackTrace stackTrace) { + Log.error('on DeepLink stream error: ${err.toString()}', stackTrace); + _deeplinkSubscription?.cancel(); + _deeplinkSubscription = null; + }, + ); + if (Platform.isWindows) { + // register deep link for Windows + registerProtocolHandler(appflowyDeepLinkSchema); + } + } else { + _deeplinkSubscription?.resume(); } } - ValueNotifier? _stateNotifier = ValueNotifier(null); + final _appLinks = AppLinks(); + ValueNotifier? _stateNotifier = ValueNotifier(null); Completer>? _completer; - set completer(Completer>? value) { - Log.debug('AppFlowyCloudDeepLink: $hashCode completer'); - _completer = value; - } - - late final StreamSubscription _deepLinkSubscription; + // The AppLinks is a singleton, so we need to cancel the previous subscription + // before creating a new one. + static StreamSubscription? _deeplinkSubscription; Future dispose() async { - Log.debug('AppFlowyCloudDeepLink: $hashCode dispose'); - await _deepLinkSubscription.cancel(); - + _deeplinkSubscription?.pause(); _stateNotifier?.dispose(); _stateNotifier = null; - completer = null; } void registerCompleter( Completer> completer, ) { - this.completer = completer; + _completer = completer; } VoidCallback subscribeDeepLinkLoadingState( @@ -83,13 +82,6 @@ class AppFlowyCloudDeepLink { void unsubscribeDeepLinkLoadingState(VoidCallback listener) => _stateNotifier?.removeListener(listener); - Future passGotrueTokenResponse( - GotrueTokenResponsePB gotrueTokenResponse, - ) async { - final uri = _buildDeepLinkUri(gotrueTokenResponse); - await _handleUri(uri); - } - Future _handleUri( Uri? uri, ) async { @@ -98,21 +90,19 @@ class AppFlowyCloudDeepLink { if (uri == null) { Log.error('onDeepLinkError: Unexpected empty deep link callback'); _completer?.complete(FlowyResult.failure(AuthError.emptyDeepLink)); - completer = null; + _completer = null; return; } if (_isPaymentSuccessUri(uri)) { - Log.debug("Payment success deep link: ${uri.toString()}"); - final plan = uri.queryParameters['plan']; - return getIt().onPaymentSuccess(plan); + return getIt().onPaymentSuccess(); } return _isAuthCallbackDeepLink(uri).fold( (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, + authenticator: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: uri.toString(), AuthServiceMapKeys.deviceId: deviceId, @@ -135,15 +125,16 @@ class AppFlowyCloudDeepLink { Log.error(err); final context = AppGlobals.rootNavKey.currentState?.context; if (context != null) { - showToastNotification( - message: err.msg, + showSnackBarMessage( + context, + err.msg, ); } }, ); } else { _completer?.complete(result); - completer = null; + _completer = null; } }, (err) { @@ -158,7 +149,7 @@ class AppFlowyCloudDeepLink { } } else { _completer?.complete(FlowyResult.failure(err)); - completer = null; + _completer = null; } }, ); @@ -179,57 +170,6 @@ class AppFlowyCloudDeepLink { bool _isPaymentSuccessUri(Uri uri) { return uri.host == 'payment-success'; } - - Uri? _buildDeepLinkUri(GotrueTokenResponsePB gotrueTokenResponse) { - final params = {}; - - if (gotrueTokenResponse.hasAccessToken() && - gotrueTokenResponse.accessToken.isNotEmpty) { - params['access_token'] = gotrueTokenResponse.accessToken; - } - - if (gotrueTokenResponse.hasExpiresAt()) { - params['expires_at'] = gotrueTokenResponse.expiresAt.toString(); - } - - if (gotrueTokenResponse.hasExpiresIn()) { - params['expires_in'] = gotrueTokenResponse.expiresIn.toString(); - } - - if (gotrueTokenResponse.hasProviderRefreshToken() && - gotrueTokenResponse.providerRefreshToken.isNotEmpty) { - params['provider_refresh_token'] = - gotrueTokenResponse.providerRefreshToken; - } - - if (gotrueTokenResponse.hasProviderAccessToken() && - gotrueTokenResponse.providerAccessToken.isNotEmpty) { - params['provider_token'] = gotrueTokenResponse.providerAccessToken; - } - - if (gotrueTokenResponse.hasRefreshToken() && - gotrueTokenResponse.refreshToken.isNotEmpty) { - params['refresh_token'] = gotrueTokenResponse.refreshToken; - } - - if (gotrueTokenResponse.hasTokenType() && - gotrueTokenResponse.tokenType.isNotEmpty) { - params['token_type'] = gotrueTokenResponse.tokenType; - } - - if (params.isEmpty) { - return null; - } - - final fragment = params.entries - .map( - (e) => - '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}', - ) - .join('&'); - - return Uri.parse('appflowy-flutter://login-callback#$fragment'); - } } class InitAppFlowyCloudTask extends LaunchTask { @@ -278,35 +218,3 @@ enum DeepLinkState { loading, finish, } - -// wrapper for AppLinks to support multiple listeners -class _AppLinkWrapper { - _AppLinkWrapper._() { - _appLinkSubscription = _appLinks.uriLinkStream.listen((event) { - _streamSubscription.sink.add(event); - }); - } - - static final _AppLinkWrapper instance = _AppLinkWrapper._(); - - final AppLinks _appLinks = AppLinks(); - final _streamSubscription = StreamController.broadcast(); - late final StreamSubscription _appLinkSubscription; - - StreamSubscription listen( - void Function(Uri?) listener, { - Function? onError, - bool? cancelOnError, - }) { - return _streamSubscription.stream.listen( - listener, - onError: onError, - cancelOnError: cancelOnError, - ); - } - - void dispose() { - _streamSubscription.close(); - _appLinkSubscription.cancel(); - } -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart deleted file mode 100644 index b666392544..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/auto_update_task.dart +++ /dev/null @@ -1,205 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/version_checker/version_checker.dart'; -import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:auto_updater/auto_updater.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../startup.dart'; - -class AutoUpdateTask extends LaunchTask { - AutoUpdateTask(); - - static const _feedUrl = - 'https://github.com/AppFlowy-IO/AppFlowy/releases/latest/download/appcast-{os}-{arch}.xml'; - final _listener = _AppFlowyAutoUpdaterListener(); - - @override - Future initialize(LaunchContext context) async { - // the auto updater is not supported on mobile - if (UniversalPlatform.isMobile) { - return; - } - - // don't use await here, because the auto updater is not a blocking operation - unawaited(_setupAutoUpdater()); - - ApplicationInfo.isCriticalUpdateNotifier.addListener( - _showCriticalUpdateDialog, - ); - } - - @override - Future dispose() async { - autoUpdater.removeListener(_listener); - - ApplicationInfo.isCriticalUpdateNotifier.removeListener( - _showCriticalUpdateDialog, - ); - } - - // On macOS and windows, we use auto_updater to check for updates. - // On linux, we use the version checker to check for updates because the auto_updater is not supported. - Future _setupAutoUpdater() async { - Log.info( - '[AutoUpdate] current version: ${ApplicationInfo.applicationVersion}, current cpu architecture: ${ApplicationInfo.architecture}', - ); - - // Since the appcast.xml is not supported the arch, we separate the feed url by os and arch. - final feedUrl = _feedUrl - .replaceAll('{os}', ApplicationInfo.os) - .replaceAll('{arch}', ApplicationInfo.architecture); - - // the auto updater is only supported on macOS and windows, so we don't need to check the platform - if (UniversalPlatform.isMacOS || UniversalPlatform.isWindows) { - autoUpdater.addListener(_listener); - } - - Log.info('[AutoUpdate] feed url: $feedUrl'); - - versionChecker.setFeedUrl(feedUrl); - final item = await versionChecker.checkForUpdateInformation(); - if (item != null) { - ApplicationInfo.latestAppcastItem = item; - ApplicationInfo.latestVersionNotifier.value = - item.displayVersionString ?? ''; - } - } - - void _showCriticalUpdateDialog() { - showCustomConfirmDialog( - context: AppGlobals.rootNavKey.currentContext!, - title: LocaleKeys.autoUpdate_criticalUpdateTitle.tr(), - description: LocaleKeys.autoUpdate_criticalUpdateDescription.tr( - namedArgs: { - 'currentVersion': ApplicationInfo.applicationVersion, - 'newVersion': ApplicationInfo.latestVersion, - }, - ), - builder: (context) => const SizedBox.shrink(), - // if the update is critical, dont allow the user to dismiss the dialog - barrierDismissible: false, - showCloseButton: false, - enableKeyboardListener: false, - closeOnConfirm: false, - confirmLabel: LocaleKeys.autoUpdate_criticalUpdateButton.tr(), - onConfirm: () async { - await versionChecker.checkForUpdate(); - }, - ); - } -} - -class _AppFlowyAutoUpdaterListener extends UpdaterListener { - @override - void onUpdaterBeforeQuitForUpdate(AppcastItem? item) {} - - @override - void onUpdaterCheckingForUpdate(Appcast? appcast) { - // Due to the reason documented in the following link, the update will not be found if the user has skipped the update. - // We have to check the skipped version manually. - // https://sparkle-project.org/documentation/api-reference/Classes/SPUUpdater.html#/c:objc(cs)SPUUpdater(im)checkForUpdateInformation - final items = appcast?.items; - if (items != null) { - final String? currentPlatform; - if (UniversalPlatform.isMacOS) { - currentPlatform = 'macos'; - } else if (UniversalPlatform.isWindows) { - currentPlatform = 'windows'; - } else { - currentPlatform = null; - } - - final matchingItem = items.firstWhereOrNull( - (item) => item.os == currentPlatform, - ); - - if (matchingItem != null) { - _updateVersionNotifier(matchingItem); - - Log.info( - '[AutoUpdate] latest version: ${matchingItem.displayVersionString}', - ); - } - } - } - - @override - void onUpdaterError(UpdaterError? error) { - Log.error('[AutoUpdate] On update error: $error'); - } - - @override - void onUpdaterUpdateNotAvailable(UpdaterError? error) { - Log.info('[AutoUpdate] Update not available $error'); - } - - @override - void onUpdaterUpdateAvailable(AppcastItem? item) { - _updateVersionNotifier(item); - - Log.info('[AutoUpdate] Update available: ${item?.displayVersionString}'); - } - - @override - void onUpdaterUpdateDownloaded(AppcastItem? item) { - Log.info('[AutoUpdate] Update downloaded: ${item?.displayVersionString}'); - } - - @override - void onUpdaterUpdateCancelled(AppcastItem? item) { - _updateVersionNotifier(item); - - Log.info('[AutoUpdate] Update cancelled: ${item?.displayVersionString}'); - } - - @override - void onUpdaterUpdateInstalled(AppcastItem? item) { - _updateVersionNotifier(item); - - Log.info('[AutoUpdate] Update installed: ${item?.displayVersionString}'); - } - - @override - void onUpdaterUpdateSkipped(AppcastItem? item) { - _updateVersionNotifier(item); - - Log.info('[AutoUpdate] Update skipped: ${item?.displayVersionString}'); - } - - void _updateVersionNotifier(AppcastItem? item) { - if (item != null) { - ApplicationInfo.latestAppcastItem = item; - ApplicationInfo.latestVersionNotifier.value = - item.displayVersionString ?? ''; - } - } -} - -class AppFlowyAutoUpdateVersion { - AppFlowyAutoUpdateVersion({ - required this.latestVersion, - required this.currentVersion, - required this.isForceUpdate, - }); - - factory AppFlowyAutoUpdateVersion.initial() => AppFlowyAutoUpdateVersion( - latestVersion: '0.0.0', - currentVersion: '0.0.0', - isForceUpdate: false, - ); - - final String latestVersion; - final String currentVersion; - - final bool isForceUpdate; - - bool get isUpdateAvailable => latestVersion != currentVersion; -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart index 9a34e84f70..3fe2513565 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/debug_task.dart @@ -1,45 +1,18 @@ -import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:talker/talker.dart'; -import 'package:talker_bloc_logger/talker_bloc_logger.dart'; -import 'package:universal_platform/universal_platform.dart'; + +import '../startup.dart'; class DebugTask extends LaunchTask { - DebugTask(); - - final Talker talker = Talker(); + const DebugTask(); @override Future initialize(LaunchContext context) async { - // hide the keyboard on mobile - if (UniversalPlatform.isMobile && kDebugMode) { + // the hotkey manager is not supported on mobile + if (PlatformExtension.isMobile && kDebugMode) { await SystemChannels.textInput.invokeMethod('TextInput.hide'); } - - // log the bloc events - if (kDebugMode) { - Bloc.observer = TalkerBlocObserver( - talker: talker, - settings: TalkerBlocLoggerSettings( - // Disabled by default to prevent mixing with AppFlowy logs - // Enable to observe all bloc events - enabled: false, - printEventFullData: false, - printStateFullData: false, - printChanges: true, - printClosings: true, - printCreations: true, - transitionFilter: (_, transition) { - // By default, observe all transitions - // You can add your own filter here if needed - // when you want to observer a specific bloc - return true; - }, - ), - ); - } } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart index 2c90afbdda..61e1f52460 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart @@ -1,11 +1,8 @@ import 'dart:io'; import 'package:appflowy_backend/log.dart'; -import 'package:auto_updater/auto_updater.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:version/version.dart'; import '../startup.dart'; @@ -14,42 +11,6 @@ class ApplicationInfo { static String applicationVersion = ''; static String buildNumber = ''; static String deviceId = ''; - static String architecture = ''; - static String os = ''; - - // macOS major version - static int? macOSMajorVersion; - static int? macOSMinorVersion; - - // latest version - static ValueNotifier latestVersionNotifier = ValueNotifier(''); - // the version number is like 0.9.0 - static String get latestVersion => latestVersionNotifier.value; - - // If the latest version is greater than the current version, it means there is an update available - static bool get isUpdateAvailable { - try { - if (latestVersion.isEmpty) { - return false; - } - return Version.parse(latestVersion) > Version.parse(applicationVersion); - } catch (e) { - return false; - } - } - - // the latest appcast item - static AppcastItem? _latestAppcastItem; - static AppcastItem? get latestAppcastItem => _latestAppcastItem; - static set latestAppcastItem(AppcastItem? value) { - _latestAppcastItem = value; - - isCriticalUpdateNotifier.value = value?.criticalUpdate == true; - } - - // is critical update - static ValueNotifier isCriticalUpdateNotifier = ValueNotifier(false); - static bool get isCriticalUpdate => isCriticalUpdateNotifier.value; } class ApplicationInfoTask extends LaunchTask { @@ -60,65 +21,43 @@ class ApplicationInfoTask extends LaunchTask { final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); final PackageInfo packageInfo = await PackageInfo.fromPlatform(); - if (Platform.isMacOS) { - final macInfo = await deviceInfoPlugin.macOsInfo; - ApplicationInfo.macOSMajorVersion = macInfo.majorVersion; - ApplicationInfo.macOSMinorVersion = macInfo.minorVersion; - } - if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt; } - ApplicationInfo.applicationVersion = packageInfo.version; - ApplicationInfo.buildNumber = packageInfo.buildNumber; + if (Platform.isAndroid || Platform.isIOS) { + ApplicationInfo.applicationVersion = packageInfo.version; + ApplicationInfo.buildNumber = packageInfo.buildNumber; + } String? deviceId; - String? architecture; - String? os; try { if (Platform.isAndroid) { final AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; deviceId = androidInfo.device; - architecture = androidInfo.supportedAbis.firstOrNull; - os = 'android'; } else if (Platform.isIOS) { final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; deviceId = iosInfo.identifierForVendor; - architecture = iosInfo.utsname.machine; - os = 'ios'; } else if (Platform.isMacOS) { final MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo; deviceId = macInfo.systemGUID; - architecture = macInfo.arch; - os = 'macos'; } else if (Platform.isWindows) { final WindowsDeviceInfo windowsInfo = await deviceInfoPlugin.windowsInfo; deviceId = windowsInfo.deviceId; - // we only support x86_64 on Windows - architecture = 'x86_64'; - os = 'windows'; } else if (Platform.isLinux) { final LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo; deviceId = linuxInfo.machineId; - // we only support x86_64 on Linux - architecture = 'x86_64'; - os = 'linux'; } else { deviceId = null; - architecture = null; - os = null; } } catch (e) { Log.error('Failed to get platform version, $e'); } ApplicationInfo.deviceId = deviceId ?? ''; - ApplicationInfo.architecture = architecture ?? ''; - ApplicationInfo.os = os ?? ''; } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart deleted file mode 100644 index 0695ceeab5..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/file_storage_task.dart +++ /dev/null @@ -1,151 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:ffi'; -import 'dart:isolate'; - -import 'package:flutter/foundation.dart'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-storage/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart'; - -import '../startup.dart'; - -class FileStorageTask extends LaunchTask { - const FileStorageTask(); - - @override - Future initialize(LaunchContext context) async { - context.getIt.registerSingleton( - FileStorageService(), - dispose: (service) async => service.dispose(), - ); - } - - @override - Future dispose() async {} -} - -class FileStorageService { - FileStorageService() { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - (event) { - final fileProgress = FileProgress.fromJsonString(event); - if (fileProgress != null) { - Log.debug( - "FileStorageService upload file: ${fileProgress.fileUrl} ${fileProgress.progress}", - ); - final notifier = _notifierList[fileProgress.fileUrl]; - if (notifier != null) { - notifier.value = fileProgress; - } - } - }, - ); - - if (!integrationMode().isTest) { - final payload = RegisterStreamPB() - ..port = Int64(_port.sendPort.nativePort); - FileStorageEventRegisterStream(payload).send(); - } - } - - final Map> _notifierList = {}; - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - - AutoRemoveNotifier onFileProgress({required String fileUrl}) { - _notifierList.remove(fileUrl)?.dispose(); - - final notifier = AutoRemoveNotifier( - FileProgress(fileUrl: fileUrl, progress: 0), - notifierList: _notifierList, - fileId: fileUrl, - ); - _notifierList[fileUrl] = notifier; - - // trigger the initial file state - getFileState(fileUrl); - - return notifier; - } - - Future> getFileState(String url) { - final payload = QueryFilePB()..url = url; - return FileStorageEventQueryFile(payload).send(); - } - - Future dispose() async { - // dispose all notifiers - for (final notifier in _notifierList.values) { - notifier.dispose(); - } - - await _controller.close(); - await _subscription.cancel(); - _port.close(); - } -} - -class FileProgress { - FileProgress({ - required this.fileUrl, - required this.progress, - this.error, - }); - - static FileProgress? fromJson(Map? json) { - if (json == null) { - return null; - } - - try { - if (json.containsKey('file_url') && json.containsKey('progress')) { - return FileProgress( - fileUrl: json['file_url'] as String, - progress: (json['progress'] as num).toDouble(), - error: json['error'] as String?, - ); - } - } catch (e) { - Log.error('unable to parse file progress: $e'); - } - return null; - } - - // Method to parse a JSON string and return a FileProgress object or null - static FileProgress? fromJsonString(String jsonString) { - try { - final Map jsonMap = jsonDecode(jsonString); - return FileProgress.fromJson(jsonMap); - } catch (e) { - return null; - } - } - - final double progress; - final String fileUrl; - final String? error; -} - -class AutoRemoveNotifier extends ValueNotifier { - AutoRemoveNotifier( - super.value, { - required this.fileId, - required Map> notifierList, - }) : _notifierList = notifierList; - - final String fileId; - final Map> _notifierList; - - @override - void dispose() { - _notifierList.remove(fileId); - super.dispose(); - } -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index e64e0f98de..65096984bd 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -10,14 +10,12 @@ import 'package:appflowy/mobile/presentation/database/mobile_calendar_events_scr 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/favorite/mobile_favorite_page.dart'; -import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart'; -import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart'; +import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_page.dart'; import 'package:appflowy/mobile/presentation/presentation.dart'; import 'package:appflowy/mobile/presentation/setting/cloud/appflowy_cloud_page.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/language/language_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; -import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart'; import 'package:appflowy/plugins/base/color/color_picker_screen.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_language_screen.dart'; @@ -29,16 +27,12 @@ import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/time/duration.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sheet/route.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../shared/icon_emoji_picker/tab.dart'; GoRouter generateRouter(Widget child) { return GoRouter( @@ -51,11 +45,12 @@ GoRouter generateRouter(Widget child) { // Routes in both desktop and mobile _signInScreenRoute(), _skipLogInScreenRoute(), + _encryptSecretScreenRoute(), _workspaceErrorScreenRoute(), // Desktop only - if (UniversalPlatform.isDesktop) _desktopHomeScreenRoute(), + if (!PlatformExtension.isMobile) _desktopHomeScreenRoute(), // Mobile only - if (UniversalPlatform.isMobile) ...[ + if (PlatformExtension.isMobile) ...[ // settings _mobileHomeSettingPageRoute(), _mobileCloudSettingAppFlowyCloudPageRoute(), @@ -97,12 +92,6 @@ GoRouter generateRouter(Widget child) { _mobileCalendarEventsPageRoute(), _mobileBlockSettingsPageRoute(), - - // notifications - _mobileNotificationMultiSelectPageRoute(), - - // invite members - _mobileInviteMembersPageRoute(), ], // Desktop and Mobile @@ -119,6 +108,18 @@ GoRouter generateRouter(Widget child) { ); }, ), + GoRoute( + path: SignUpScreen.routeName, + pageBuilder: (context, state) { + return CustomTransitionPage( + child: SignUpScreen( + router: getIt(), + ), + transitionsBuilder: _buildFadeTransition, + transitionDuration: _slowDuration, + ); + }, + ), ], ); } @@ -158,11 +159,33 @@ StatefulShellRoute _mobileHomeScreenWithNavigationBarRoute() { ), ], ), + // Enable search feature after we have a search page. + // StatefulShellBranch( + // routes: [ + // GoRoute( + // path: '/d', + // builder: (BuildContext context, GoRouterState state) => + // const RootPlaceholderScreen( + // label: 'Search', + // detailsPath: '/d/details', + // ), + // routes: [ + // GoRoute( + // path: 'details', + // builder: (BuildContext context, GoRouterState state) => + // const DetailsPlaceholderScreen( + // label: 'Search Page details', + // ), + // ), + // ], + // ), + // ], + // ), StatefulShellBranch( routes: [ GoRoute( - path: MobileNotificationsScreenV2.routeName, - builder: (_, __) => const MobileNotificationsScreenV2(), + path: MobileNotificationsScreen.routeName, + builder: (_, __) => const MobileNotificationsScreen(), ), ], ), @@ -180,30 +203,6 @@ GoRoute _mobileHomeSettingPageRoute() { ); } -GoRoute _mobileNotificationMultiSelectPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: MobileNotificationsMultiSelectScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: MobileNotificationsMultiSelectScreen(), - ); - }, - ); -} - -GoRoute _mobileInviteMembersPageRoute() { - return GoRoute( - parentNavigatorKey: AppGlobals.rootNavKey, - path: InviteMembersScreen.routeName, - pageBuilder: (context, state) { - return const MaterialExtendedPage( - child: InviteMembersScreen(), - ); - }, - ); -} - GoRoute _mobileCloudSettingAppFlowyCloudPageRoute() { return GoRoute( parentNavigatorKey: AppGlobals.rootNavKey, @@ -271,35 +270,10 @@ GoRoute _mobileEmojiPickerPageRoute() { pageBuilder: (context, state) { final title = state.uri.queryParameters[MobileEmojiPickerScreen.pageTitle]; - final selectTabs = - state.uri.queryParameters[MobileEmojiPickerScreen.selectTabs] ?? ''; - final selectedType = state - .uri.queryParameters[MobileEmojiPickerScreen.iconSelectedType] - ?.toPickerTabType(); - final documentId = - state.uri.queryParameters[MobileEmojiPickerScreen.uploadDocumentId]; - List tabs = []; - try { - tabs = selectTabs - .split('-') - .map((e) => PickerTabType.values.byName(e)) - .toList(); - } on ArgumentError catch (e) { - Log.error('convert selectTabs to pickerTab error', e); - } return MaterialExtendedPage( - child: tabs.isEmpty - ? MobileEmojiPickerScreen( - title: title, - selectedType: selectedType, - documentId: documentId, - ) - : MobileEmojiPickerScreen( - title: title, - selectedType: selectedType, - tabs: tabs, - documentId: documentId, - ), + child: MobileEmojiPickerScreen( + title: title, + ), ); }, ); @@ -458,6 +432,23 @@ GoRoute _workspaceErrorScreenRoute() { ); } +GoRoute _encryptSecretScreenRoute() { + return GoRoute( + path: EncryptSecretScreen.routeName, + pageBuilder: (context, state) { + final args = state.extra as Map; + return CustomTransitionPage( + child: EncryptSecretScreen( + user: args[EncryptSecretScreen.argUser], + key: args[EncryptSecretScreen.argKey], + ), + transitionsBuilder: _buildFadeTransition, + transitionDuration: _slowDuration, + ); + }, + ); +} + GoRoute _skipLogInScreenRoute() { return GoRoute( path: SkipLogInScreen.routeName, @@ -491,39 +482,9 @@ GoRoute _mobileEditorScreenRoute() { pageBuilder: (context, state) { final id = state.uri.queryParameters[MobileDocumentScreen.viewId]!; final title = state.uri.queryParameters[MobileDocumentScreen.viewTitle]; - final showMoreButton = bool.tryParse( - state.uri.queryParameters[MobileDocumentScreen.viewShowMoreButton] ?? - 'true', - ); - final fixedTitle = - state.uri.queryParameters[MobileDocumentScreen.viewFixedTitle]; - final blockId = - state.uri.queryParameters[MobileDocumentScreen.viewBlockId]; - - final selectTabs = - state.uri.queryParameters[MobileDocumentScreen.viewSelectTabs] ?? ''; - List tabs = []; - try { - tabs = selectTabs - .split('-') - .map((e) => PickerTabType.values.byName(e)) - .toList(); - } on ArgumentError catch (e) { - Log.error('convert selectTabs to pickerTab error', e); - } - if (tabs.isEmpty) { - tabs = const [PickerTabType.emoji, PickerTabType.icon]; - } return MaterialExtendedPage( - child: MobileDocumentScreen( - id: id, - title: title, - showMoreButton: showMoreButton ?? true, - fixedTitle: fixedTitle, - blockId: blockId, - tabs: tabs, - ), + child: MobileDocumentScreen(id: id, title: title), ); }, ); @@ -603,25 +564,10 @@ GoRoute _mobileCardDetailScreenRoute() { parentNavigatorKey: AppGlobals.rootNavKey, path: MobileRowDetailPage.routeName, pageBuilder: (context, state) { - var extra = state.extra as Map?; - - if (kDebugMode && extra == null) { - extra = _dynamicValues; - } - - if (extra == null) { - return const MaterialExtendedPage( - child: SizedBox.shrink(), - ); - } - + final args = state.extra as Map; final databaseController = - extra[MobileRowDetailPage.argDatabaseController]; - final rowId = extra[MobileRowDetailPage.argRowId]!; - - if (kDebugMode) { - _dynamicValues = extra; - } + args[MobileRowDetailPage.argDatabaseController]; + final rowId = args[MobileRowDetailPage.argRowId]!; return MaterialExtendedPage( child: MobileRowDetailPage( @@ -666,7 +612,7 @@ GoRoute _rootRoute(Widget child) { (user) => DesktopHomeScreen.routeName, (error) => null, ); - if (routeName != null && !UniversalPlatform.isMobile) return routeName; + if (routeName != null && !PlatformExtension.isMobile) return routeName; return null; }, @@ -689,8 +635,3 @@ Widget _buildFadeTransition( Duration _slowDuration = Duration( milliseconds: RouteDurations.slow.inMilliseconds.round(), ); - -// ONLY USE IN DEBUG MODE -// this is a workaround for the issue of GoRouter not supporting extra with complex types -// https://github.com/flutter/flutter/issues/137248 -Map _dynamicValues = {}; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart b/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart index 9e23a0017c..0475d9de77 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/hot_key.dart @@ -1,5 +1,5 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:universal_platform/universal_platform.dart'; import '../startup.dart'; @@ -9,7 +9,7 @@ class HotKeyTask extends LaunchTask { @override Future initialize(LaunchContext context) async { // the hotkey manager is not supported on mobile - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return; } await hotKeyManager.unregisterAll(); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart b/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart index c2c64536b2..9d088bb5d4 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/platform_error_catcher.dart @@ -1,7 +1,5 @@ import 'package:appflowy_backend/log.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import '../startup.dart'; @@ -19,23 +17,6 @@ class PlatformErrorCatcherTask extends LaunchTask { return true; }; } - - ErrorWidget.builder = (details) { - if (kDebugMode) { - return Container( - width: double.infinity, - height: 30, - color: Colors.red, - child: FlowyText( - 'ERROR: ${details.exceptionAsString()}', - color: Colors.white, - ), - ); - } - - // hide the error widget in release mode - return const SizedBox.shrink(); - }; } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart index 9e8f9df49a..84c379da24 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/prelude.dart @@ -1,9 +1,7 @@ export 'app_widget.dart'; export 'appflowy_cloud_task.dart'; -export 'auto_update_task.dart'; export 'debug_task.dart'; export 'device_info_task.dart'; -export 'feature_flag_task.dart'; export 'generate_router.dart'; export 'hot_key.dart'; export 'load_plugin.dart'; @@ -11,7 +9,7 @@ export 'localization.dart'; export 'memory_leak_detector.dart'; export 'platform_error_catcher.dart'; export 'platform_service.dart'; -export 'recent_service_task.dart'; export 'rust_sdk.dart'; -export 'sentry.dart'; +export 'supabase_task.dart'; export 'windows.dart'; +export 'recent_service_task.dart'; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart index c406dd161a..c02b450d79 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/rust_sdk.dart @@ -5,8 +5,9 @@ import 'package:appflowy/env/backend_env.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/user/application/auth/device_id.dart'; import 'package:appflowy_backend/appflowy_backend.dart'; -import 'package:path/path.dart' as path; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:path/path.dart' as path; import '../startup.dart'; @@ -28,6 +29,7 @@ class InitRustSDKTask extends LaunchTask { final dir = customApplicationPath ?? applicationPath; final deviceId = await getDeviceId(); + debugPrint('application path: ${applicationPath.path}'); // Pass the environment variables to the Rust SDK final env = _makeAppFlowyConfiguration( root.path, @@ -61,6 +63,7 @@ AppFlowyConfiguration _makeAppFlowyConfiguration( device_id: deviceId, platform: Platform.operatingSystem, authenticator_type: env.authenticatorType.value, + supabase_config: env.supabaseConfig, appflowy_cloud_config: env.appflowyCloudConfig, envs: rustEnvs, ); @@ -73,10 +76,10 @@ Future appFlowyApplicationDataDirectory() async { case IntegrationMode.develop: final Directory documentsDir = await getApplicationSupportDirectory() .then((directory) => directory.create()); - return Directory(path.join(documentsDir.path, 'data_dev')); + return Directory(path.join(documentsDir.path, 'data_dev')).create(); case IntegrationMode.release: final Directory documentsDir = await getApplicationSupportDirectory(); - return Directory(path.join(documentsDir.path, 'data')); + return Directory(path.join(documentsDir.path, 'data')).create(); case IntegrationMode.unitTest: case IntegrationMode.integrationTest: return Directory(path.join(Directory.current.path, '.sandbox')); diff --git a/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart b/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart deleted file mode 100644 index 9076569a9c..0000000000 --- a/frontend/appflowy_flutter/lib/startup/tasks/sentry.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:appflowy/env/env.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; - -import '../startup.dart'; - -class InitSentryTask extends LaunchTask { - const InitSentryTask(); - - @override - Future initialize(LaunchContext context) async { - const dsn = Env.sentryDsn; - if (dsn.isEmpty) { - Log.info('Sentry DSN is not set, skipping initialization'); - return; - } - - Log.info('Initializing Sentry'); - - await SentryFlutter.init( - (options) { - options.dsn = dsn; - options.tracesSampleRate = 0.1; - options.profilesSampleRate = 0.1; - }, - ); - } - - @override - Future dispose() async {} -} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart new file mode 100644 index 0000000000..cb8981acdd --- /dev/null +++ b/frontend/appflowy_flutter/lib/startup/tasks/supabase_task.dart @@ -0,0 +1,118 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/user/application/supabase_realtime.dart'; +import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:path/path.dart' as p; +import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:url_protocol/url_protocol.dart'; + +import '../startup.dart'; + +// ONLY supports in macOS and Windows now. +// +// If you need to update the schema, please update the following files: +// - appflowy_flutter/macos/Runner/Info.plist (macOS) +// - the callback url in Supabase dashboard +const appflowyDeepLinkSchema = 'appflowy-flutter'; +const supabaseLoginCallback = '$appflowyDeepLinkSchema://login-callback'; + +const hiveBoxName = 'appflowy_supabase_authentication'; + +// Used to store the session of the supabase in case of the user switch the different folder. +Supabase? supabase; +SupabaseRealtimeService? realtimeService; + +class InitSupabaseTask extends LaunchTask { + @override + Future initialize(LaunchContext context) async { + if (!isSupabaseEnabled) { + return; + } + + await supabase?.dispose(); + supabase = null; + final initializedSupabase = await Supabase.initialize( + url: getIt().supabaseConfig.url, + anonKey: getIt().supabaseConfig.anon_key, + debug: kDebugMode, + authOptions: const FlutterAuthClientOptions( + localStorage: SupabaseLocalStorage(), + ), + ); + + if (realtimeService != null) { + await realtimeService?.dispose(); + realtimeService = null; + } + realtimeService = SupabaseRealtimeService(supabase: initializedSupabase); + + supabase = initializedSupabase; + + if (Platform.isWindows) { + // register deep link for Windows + registerProtocolHandler(appflowyDeepLinkSchema); + } + } + + @override + Future dispose() async { + await realtimeService?.dispose(); + realtimeService = null; + await supabase?.dispose(); + supabase = null; + } +} + +/// customize the supabase auth storage +/// +/// We don't use the default one because it always save the session in the document directory. +/// When we switch to the different folder, the session still exists. +class SupabaseLocalStorage extends LocalStorage { + const SupabaseLocalStorage(); + + @override + Future initialize() async { + HiveCipher? encryptionCipher; + + // customize the path for Hive + final path = await getIt().getPath(); + Hive.init(p.join(path, 'supabase_auth')); + await Hive.openBox( + hiveBoxName, + encryptionCipher: encryptionCipher, + ); + } + + @override + Future hasAccessToken() { + return Future.value( + Hive.box(hiveBoxName).containsKey( + supabasePersistSessionKey, + ), + ); + } + + @override + Future accessToken() { + return Future.value( + Hive.box(hiveBoxName).get(supabasePersistSessionKey) as String?, + ); + } + + @override + Future removePersistedSession() { + return Hive.box(hiveBoxName).delete(supabasePersistSessionKey); + } + + @override + Future persistSession(String persistSessionString) { + return Hive.box(hiveBoxName).put( + supabasePersistSessionKey, + persistSessionString, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart index 20b8b0b56e..4d7bd47a33 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/windows.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/windows.dart @@ -1,20 +1,24 @@ import 'dart:async'; import 'dart:ui'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/core/helpers/helpers.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:scaled_app/scaled_app.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:universal_platform/universal_platform.dart'; class InitAppWindowTask extends LaunchTask with WindowListener { - InitAppWindowTask({this.title = 'AppFlowy'}); + InitAppWindowTask({ + this.title = 'AppFlowy', + }); final String title; + final windowSizeManager = WindowSizeManager(); @override @@ -43,8 +47,8 @@ class InitAppWindowTask extends LaunchTask with WindowListener { final position = await windowSizeManager.getPosition(); - if (UniversalPlatform.isWindows) { - doWhenWindowReady(() async { + if (PlatformExtension.isWindows) { + doWhenWindowReady(() { appWindow.minSize = windowOptions.minimumSize; appWindow.maxSize = windowOptions.maximumSize; appWindow.size = windowSize; @@ -54,19 +58,17 @@ class InitAppWindowTask extends LaunchTask with WindowListener { } appWindow.show(); - - /// on Windows we maximize the window if it was previously closed - /// from a maximized state. - final isMaximized = await windowSizeManager.getWindowMaximized(); - if (isMaximized) { - appWindow.maximize(); - } }); } else { await windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.show(); await windowManager.focus(); + if (PlatformExtension.isWindows) { + // Hide title bar on Windows, we implement a custom solution elsewhere + await windowManager.setTitleBarStyle(TitleBarStyle.hidden); + } + if (position != null) { await windowManager.setPosition(position); } @@ -80,38 +82,6 @@ class InitAppWindowTask extends LaunchTask with WindowListener { ); } - @override - Future onWindowMaximize() async { - super.onWindowMaximize(); - await windowSizeManager.setWindowMaximized(true); - await windowSizeManager.setPosition(Offset.zero); - } - - @override - Future onWindowUnmaximize() async { - super.onWindowUnmaximize(); - await windowSizeManager.setWindowMaximized(false); - - final position = await windowManager.getPosition(); - return windowSizeManager.setPosition(position); - } - - @override - void onWindowEnterFullScreen() async { - super.onWindowEnterFullScreen(); - await windowSizeManager.setWindowMaximized(true); - await windowSizeManager.setPosition(Offset.zero); - } - - @override - Future onWindowLeaveFullScreen() async { - super.onWindowLeaveFullScreen(); - await windowSizeManager.setWindowMaximized(false); - - final position = await windowManager.getPosition(); - return windowSizeManager.setPosition(position); - } - @override Future onWindowResize() async { super.onWindowResize(); @@ -120,6 +90,14 @@ class InitAppWindowTask extends LaunchTask with WindowListener { return windowSizeManager.setSize(currentWindowSize); } + @override + void onWindowMaximize() async { + super.onWindowMaximize(); + + final currentWindowSize = await windowManager.getSize(); + return windowSizeManager.setSize(currentWindowSize); + } + @override void onWindowMoved() async { super.onWindowMoved(); @@ -129,7 +107,5 @@ class InitAppWindowTask extends LaunchTask with WindowListener { } @override - Future dispose() async { - windowManager.removeListener(this); - } + Future dispose() async {} } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index 4f4cece9bb..81dd8ed9cf 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -18,7 +18,7 @@ class AppFlowyCloudAuthService implements AuthService { AppFlowyCloudAuthService(); final BackendAuthService _backendAuthService = BackendAuthService( - AuthTypePB.Server, + AuthenticatorPB.AppFlowyCloud, ); @override @@ -32,17 +32,12 @@ class AppFlowyCloudAuthService implements AuthService { } @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, }) async { - return _backendAuthService.signInWithEmailPassword( - email: email, - password: password, - params: params, - ); + throw UnimplementedError(); } @override @@ -61,7 +56,7 @@ class AppFlowyCloudAuthService implements AuthService { (data) async { // Open the webview with oauth url final uri = Uri.parse(data.oauthUrl); - final isSuccess = await afLaunchUri( + final isSuccess = await afLaunchUrl( uri, mode: LaunchMode.externalApplication, webOnlyWindowName: '_self', @@ -111,17 +106,6 @@ class AppFlowyCloudAuthService implements AuthService { ); } - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - return _backendAuthService.signInWithPasscode( - email: email, - passcode: passcode, - ); - } - @override Future> getUser() async { return UserBackendService.getCurrentUserProfile(); @@ -137,8 +121,6 @@ extension ProviderTypePBExtension on ProviderTypePB { return ProviderTypePB.Google; case 'discord': return ProviderTypePB.Discord; - case 'apple': - return ProviderTypePB.Apple; default: throw UnimplementedError(); } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart index 8be71dc648..deba0f3700 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_mock_auth_service.dart @@ -10,7 +10,6 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/material.dart'; /// Only used for testing. class AppFlowyCloudMockAuthService implements AuthService { @@ -20,7 +19,7 @@ class AppFlowyCloudMockAuthService implements AuthService { final String userEmail; final BackendAuthService _appFlowyAuthService = - BackendAuthService(AuthTypePB.Server); + BackendAuthService(AuthenticatorPB.Supabase); @override Future> signUp({ @@ -33,8 +32,7 @@ class AppFlowyCloudMockAuthService implements AuthService { } @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -48,7 +46,7 @@ class AppFlowyCloudMockAuthService implements AuthService { Map params = const {}, }) async { final payload = SignInUrlPayloadPB.create() - ..authenticator = AuthTypePB.Server + ..authenticator = AuthenticatorPB.AppFlowyCloud // don't use nanoid here, the gotrue server will transform the email ..email = userEmail; @@ -58,7 +56,7 @@ class AppFlowyCloudMockAuthService implements AuthService { return getSignInURLResult.fold( (urlPB) async { final payload = OauthSignInPB( - authenticator: AuthTypePB.Server, + authenticator: AuthenticatorPB.AppFlowyCloud, map: { AuthServiceMapKeys.signInURL: urlPB.signInUrl, AuthServiceMapKeys.deviceId: deviceId, @@ -66,18 +64,12 @@ class AppFlowyCloudMockAuthService implements AuthService { ); Log.info("UserEventOauthSignIn with payload: $payload"); return UserEventOauthSignIn(payload).send().then((value) { - value.fold( - (l) => null, - (err) { - debugPrint("mock auth service Error: $err"); - Log.error(err); - }, - ); + value.fold((l) => null, (err) => Log.error(err)); return value; }); }, (r) { - debugPrint("mock auth service error: $r"); + Log.error(r); return FlowyResult.failure(r); }, ); @@ -107,12 +99,4 @@ class AppFlowyCloudMockAuthService implements AuthService { Future> getUser() async { return UserBackendService.getCurrentUserProfile(); } - - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - throw UnimplementedError(); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart index 14d1ed42d6..06ddd90238 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -2,6 +2,22 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; class AuthError { + static final supabaseSignInError = FlowyError() + ..msg = 'supabase sign in error -10001' + ..code = ErrorCode.UserUnauthorized; + + static final supabaseSignUpError = FlowyError() + ..msg = 'supabase sign up error -10002' + ..code = ErrorCode.UserUnauthorized; + + static final supabaseSignInWithOauthError = FlowyError() + ..msg = 'supabase sign in with oauth error -10003' + ..code = ErrorCode.UserUnauthorized; + + static final supabaseGetUserError = FlowyError() + ..msg = 'unable to get user from supabase -10004' + ..code = ErrorCode.UserUnauthorized; + static final signInWithOauthError = FlowyError() ..msg = 'sign in with oauth error -10003' ..code = ErrorCode.UserUnauthorized; diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart index 9879b9a18e..4a69e847d5 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_service.dart @@ -1,10 +1,12 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pbserver.dart'; import 'package:appflowy_result/appflowy_result.dart'; class AuthServiceMapKeys { const AuthServiceMapKeys._(); + // for supabase auth use only. + static const String uuid = 'uuid'; static const String email = 'email'; static const String deviceId = 'device_id'; static const String signInURL = 'sign_in_url'; @@ -23,8 +25,7 @@ abstract class AuthService { /// /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params, @@ -76,17 +77,6 @@ abstract class AuthService { Map params, }); - /// Authenticates a user with a passcode sent to their email. - /// - /// - `email`: The email address of the user. - /// - `passcode`: The passcode of the user. - /// - /// Returns [UserProfilePB] if the user is authenticated, otherwise returns [FlowyError]. - Future> signInWithPasscode({ - required String email, - required String passcode, - }); - /// Signs out the currently authenticated user. Future signOut(); diff --git a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart index cab8cd170c..9147fb4fb9 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/backend_auth_service.dart @@ -6,9 +6,9 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/auth.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; import '../../../generated/locale_keys.g.dart'; import 'device_id.dart'; @@ -16,11 +16,10 @@ import 'device_id.dart'; class BackendAuthService implements AuthService { BackendAuthService(this.authType); - final AuthTypePB authType; + final AuthenticatorPB authType; @override - Future> - signInWithEmailPassword({ + Future> signInWithEmailPassword({ required String email, required String password, Map params = const {}, @@ -30,7 +29,8 @@ class BackendAuthService implements AuthService { ..password = password ..authType = authType ..deviceId = await getDeviceId(); - return UserEventSignInWithEmailPassword(request).send(); + final response = UserEventSignInWithEmailPassword(request).send(); + return response.then((value) => value); } @override @@ -65,14 +65,15 @@ class BackendAuthService implements AuthService { Map params = const {}, }) async { const password = "Guest!@123456"; - final userEmail = "anon@appflowy.io"; + final uid = uuid(); + final userEmail = "$uid@appflowy.io"; final request = SignUpPayloadPB.create() ..name = LocaleKeys.defaultUsername.tr() ..email = userEmail ..password = password // When sign up as guest, the auth type is always local. - ..authType = AuthTypePB.Local + ..authType = AuthenticatorPB.Local ..deviceId = await getDeviceId(); final response = await UserEventSignUp(request).send().then( (value) => value, @@ -83,7 +84,7 @@ class BackendAuthService implements AuthService { @override Future> signUpWithOAuth({ required String platform, - AuthTypePB authType = AuthTypePB.Local, + AuthenticatorPB authType = AuthenticatorPB.Local, Map params = const {}, }) async { return FlowyResult.failure( @@ -106,12 +107,4 @@ class BackendAuthService implements AuthService { // No need to pass the redirect URL. return UserBackendService.signInWithMagicLink(email, ''); } - - @override - Future> signInWithPasscode({ - required String email, - required String passcode, - }) async { - return UserBackendService.signInWithPasscode(email, passcode); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart new file mode 100644 index 0000000000..0dc48d7ef7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_auth_service.dart @@ -0,0 +1,252 @@ +import 'dart:async'; + +import 'package:appflowy/startup/tasks/prelude.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/auth/backend_auth_service.dart'; +import 'package:appflowy/user/application/auth/device_id.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/foundation.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'auth_error.dart'; + +class SupabaseAuthService implements AuthService { + SupabaseAuthService(); + + SupabaseClient get _client => Supabase.instance.client; + GoTrueClient get _auth => _client.auth; + + final BackendAuthService _backendAuthService = BackendAuthService( + AuthenticatorPB.Supabase, + ); + + @override + Future> signUp({ + required String name, + required String email, + required String password, + Map params = const {}, + }) async { + // fetch the uuid from supabase. + final response = await _auth.signUp( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return FlowyResult.failure(AuthError.supabaseSignUpError); + } + // assign the uuid to our backend service. + // and will transfer this logic to backend later. + return _backendAuthService.signUp( + name: name, + email: email, + password: password, + params: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } + + @override + Future> signInWithEmailPassword({ + required String email, + required String password, + Map params = const {}, + }) async { + try { + final response = await _auth.signInWithPassword( + email: email, + password: password, + ); + final uuid = response.user?.id; + if (uuid == null) { + return FlowyResult.failure(AuthError.supabaseSignInError); + } + return _backendAuthService.signInWithEmailPassword( + email: email, + password: password, + params: { + AuthServiceMapKeys.uuid: uuid, + }, + ); + } on AuthException catch (e) { + Log.error(e); + return FlowyResult.failure(AuthError.supabaseSignInError); + } + } + + @override + Future> signUpWithOAuth({ + required String platform, + Map params = const {}, + }) async { + // Before signing in, sign out any existing users. Otherwise, the callback will be triggered even if the user doesn't click the 'Sign In' button on the website + if (_auth.currentUser != null) { + await _auth.signOut(); + } + + final provider = platform.toProvider(); + final completer = supabaseLoginCompleter( + onSuccess: (userId, userEmail) async { + return _setupAuth( + map: { + AuthServiceMapKeys.uuid: userId, + AuthServiceMapKeys.email: userEmail, + AuthServiceMapKeys.deviceId: await getDeviceId(), + }, + ); + }, + ); + + final response = await _auth.signInWithOAuth( + provider, + queryParams: queryParamsForProvider(provider), + redirectTo: supabaseLoginCallback, + ); + if (!response) { + completer.complete( + FlowyResult.failure(AuthError.supabaseSignInWithOauthError), + ); + } + return completer.future; + } + + @override + Future signOut() async { + await _auth.signOut(); + await _backendAuthService.signOut(); + } + + @override + Future> signUpAsGuest({ + Map params = const {}, + }) async { + // supabase don't support guest login. + // so, just forward to our backend. + return _backendAuthService.signUpAsGuest(); + } + + @override + Future> signInWithMagicLink({ + required String email, + Map params = const {}, + }) async { + final completer = supabaseLoginCompleter( + onSuccess: (userId, userEmail) async { + return _setupAuth( + map: { + AuthServiceMapKeys.uuid: userId, + AuthServiceMapKeys.email: userEmail, + AuthServiceMapKeys.deviceId: await getDeviceId(), + }, + ); + }, + ); + + await _auth.signInWithOtp( + email: email, + emailRedirectTo: kIsWeb ? null : supabaseLoginCallback, + ); + return completer.future; + } + + @override + Future> getUser() async { + return UserBackendService.getCurrentUserProfile(); + } + + Future> getSupabaseUser() async { + final user = _auth.currentUser; + if (user == null) { + return FlowyResult.failure(AuthError.supabaseGetUserError); + } + return FlowyResult.success(user); + } + + Future> _setupAuth({ + required Map map, + }) async { + final payload = OauthSignInPB( + authenticator: AuthenticatorPB.Supabase, + map: map, + ); + + return UserEventOauthSignIn(payload).send().then((value) => value); + } +} + +extension on String { + OAuthProvider toProvider() { + switch (this) { + case 'github': + return OAuthProvider.github; + case 'google': + return OAuthProvider.google; + case 'discord': + return OAuthProvider.discord; + default: + throw UnimplementedError(); + } + } +} + +/// Creates a completer that listens to Supabase authentication state changes and +/// completes when a user signs in. +/// +/// This function sets up a listener on Supabase's authentication state. When a user +/// signs in, it triggers the provided [onSuccess] callback with the user's `id` and +/// `email`. Once the [onSuccess] callback is executed and a response is received, +/// the completer completes with the response, and the listener is canceled. +/// +/// Parameters: +/// - [onSuccess]: A callback function that's executed when a user signs in. It +/// should take in a user's `id` and `email` and return a `Future` containing either +/// a `FlowyError` or a `UserProfilePB`. +/// +/// Returns: +/// A completer of type `FlowyResult`. This completer completes +/// with the response from the [onSuccess] callback when a user signs in. +Completer> supabaseLoginCompleter({ + required Future> Function( + String userId, + String userEmail, + ) onSuccess, +}) { + final completer = Completer>(); + late final StreamSubscription subscription; + final auth = Supabase.instance.client.auth; + + subscription = auth.onAuthStateChange.listen((event) async { + final user = event.session?.user; + if (event.event == AuthChangeEvent.signedIn && user != null) { + final response = await onSuccess( + user.id, + user.email ?? user.newEmail ?? '', + ); + // Only cancel the subscription if the Event is signedIn. + await subscription.cancel(); + completer.complete(response); + } + }); + return completer; +} + +Map queryParamsForProvider(OAuthProvider provider) { + switch (provider) { + case OAuthProvider.google: + return { + 'access_type': 'offline', + 'prompt': 'consent', + }; + case OAuthProvider.github: + case OAuthProvider.discord: + default: + return {}; + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart new file mode 100644 index 0000000000..bd2620caaa --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/auth/supabase_mock_auth_service.dart @@ -0,0 +1,113 @@ +import 'dart:async'; + +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/auth/backend_auth_service.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'auth_error.dart'; + +/// Only used for testing. +class SupabaseMockAuthService implements AuthService { + SupabaseMockAuthService(); + static OauthSignInPB? signInPayload; + + SupabaseClient get _client => Supabase.instance.client; + GoTrueClient get _auth => _client.auth; + + final BackendAuthService _appFlowyAuthService = + BackendAuthService(AuthenticatorPB.Supabase); + + @override + Future> signUp({ + required String name, + required String email, + required String password, + Map params = const {}, + }) async { + throw UnimplementedError(); + } + + @override + Future> signInWithEmailPassword({ + required String email, + required String password, + Map params = const {}, + }) async { + throw UnimplementedError(); + } + + @override + Future> signUpWithOAuth({ + required String platform, + Map params = const {}, + }) async { + const password = "AppFlowyTest123!"; + const email = "supabase_integration_test@appflowy.io"; + try { + if (_auth.currentSession == null) { + try { + await _auth.signInWithPassword( + password: password, + email: email, + ); + } catch (e) { + Log.error(e); + return FlowyResult.failure(AuthError.supabaseSignUpError); + } + } + // Check if the user is already logged in. + final session = _auth.currentSession!; + final uuid = session.user.id; + + // Create the OAuth sign-in payload. + final payload = OauthSignInPB( + authenticator: AuthenticatorPB.Supabase, + map: { + AuthServiceMapKeys.uuid: uuid, + AuthServiceMapKeys.email: email, + AuthServiceMapKeys.deviceId: 'MockDeviceId', + }, + ); + + // Send the sign-in event and handle the response. + return UserEventOauthSignIn(payload).send().then((value) => value); + } on AuthException catch (e) { + Log.error(e); + return FlowyResult.failure(AuthError.supabaseSignInError); + } + } + + @override + Future signOut() async { + // await _auth.signOut(); + await _appFlowyAuthService.signOut(); + } + + @override + Future> signUpAsGuest({ + Map params = const {}, + }) async { + // supabase don't support guest login. + // so, just forward to our backend. + return _appFlowyAuthService.signUpAsGuest(); + } + + @override + Future> signInWithMagicLink({ + required String email, + Map params = const {}, + }) async { + throw UnimplementedError(); + } + + @override + Future> getUser() async { + return UserBackendService.getCurrentUserProfile(); + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart new file mode 100644 index 0000000000..19b8101ae8 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/encrypt_secret_bloc.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'auth/auth_service.dart'; + +part 'encrypt_secret_bloc.freezed.dart'; + +class EncryptSecretBloc extends Bloc { + EncryptSecretBloc({required this.user}) + : super(EncryptSecretState.initial()) { + _dispatch(); + } + + final UserProfilePB user; + + void _dispatch() { + on((event, emit) async { + await event.when( + setEncryptSecret: (secret) async { + if (isLoading()) { + return; + } + + final payload = UserSecretPB.create() + ..encryptionSecret = secret + ..encryptionSign = user.encryptionSign + ..encryptionType = user.encryptionType + ..userId = user.id; + final result = await UserEventSetEncryptionSecret(payload).send(); + if (!isClosed) { + add(EncryptSecretEvent.didFinishCheck(result)); + } + emit( + state.copyWith( + loadingState: const LoadingState.loading(), + successOrFail: null, + ), + ); + }, + cancelInputSecret: () async { + await getIt().signOut(); + emit( + state.copyWith( + successOrFail: null, + isSignOut: true, + ), + ); + }, + didFinishCheck: (result) { + result.fold( + (unit) { + emit( + state.copyWith( + loadingState: const LoadingState.loading(), + successOrFail: result, + ), + ); + }, + (err) { + emit( + state.copyWith( + loadingState: LoadingState.finish(FlowyResult.failure(err)), + successOrFail: result, + ), + ); + }, + ); + }, + ); + }); + } + + bool isLoading() { + final loadingState = state.loadingState; + if (loadingState != null) { + return loadingState.when( + loading: () => true, + finish: (_) => false, + idle: () => false, + ); + } + return false; + } +} + +@freezed +class EncryptSecretEvent with _$EncryptSecretEvent { + const factory EncryptSecretEvent.setEncryptSecret(String secret) = + _SetEncryptSecret; + const factory EncryptSecretEvent.didFinishCheck( + FlowyResult result, + ) = _DidFinishCheck; + const factory EncryptSecretEvent.cancelInputSecret() = _CancelInputSecret; +} + +@freezed +class EncryptSecretState with _$EncryptSecretState { + const factory EncryptSecretState({ + required FlowyResult? successOrFail, + required bool isSignOut, + LoadingState? loadingState, + }) = _EncryptSecretState; + + factory EncryptSecretState.initial() => const EncryptSecretState( + successOrFail: null, + isSignOut: false, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart b/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart deleted file mode 100644 index 80dd5ca3c9..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/password/password_bloc.dart +++ /dev/null @@ -1,241 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/user/application/password/password_http_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'password_bloc.freezed.dart'; - -class PasswordBloc extends Bloc { - PasswordBloc(this.userProfile) : super(PasswordState.initial()) { - on( - (event, emit) async { - await event.when( - init: () async => _init(), - changePassword: (oldPassword, newPassword) async => _onChangePassword( - emit, - oldPassword: oldPassword, - newPassword: newPassword, - ), - setupPassword: (newPassword) async => _onSetupPassword( - emit, - newPassword: newPassword, - ), - forgotPassword: (email) async => _onForgotPassword( - emit, - email: email, - ), - checkHasPassword: () async => _onCheckHasPassword( - emit, - ), - cancel: () {}, - ); - }, - ); - } - - final UserProfilePB userProfile; - late final PasswordHttpService passwordHttpService; - - bool _isInitialized = false; - - Future _init() async { - if (userProfile.workspaceAuthType == AuthTypePB.Local) { - Log.debug('PasswordBloc: skip init because user is local authenticator'); - return; - } - - final baseUrl = await getAppFlowyCloudUrl(); - try { - final authToken = jsonDecode(userProfile.token)['access_token']; - passwordHttpService = PasswordHttpService( - baseUrl: baseUrl, - authToken: authToken, - ); - _isInitialized = true; - } catch (e) { - Log.error('PasswordBloc: _init: error: $e'); - } - } - - Future _onChangePassword( - Emitter emit, { - required String oldPassword, - required String newPassword, - }) async { - if (!_isInitialized) { - Log.info('changePassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('changePassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.changePassword( - currentPassword: oldPassword, - newPassword: newPassword, - ); - - emit( - state.copyWith( - isSubmitting: false, - changePasswordResult: result, - ), - ); - } - - Future _onSetupPassword( - Emitter emit, { - required String newPassword, - }) async { - if (!_isInitialized) { - Log.info('setupPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('setupPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.setupPassword( - newPassword: newPassword, - ); - - emit( - state.copyWith( - isSubmitting: false, - hasPassword: result.fold( - (success) => true, - (error) => false, - ), - setupPasswordResult: result, - ), - ); - } - - Future _onForgotPassword( - Emitter emit, { - required String email, - }) async { - if (!_isInitialized) { - Log.info('forgotPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('forgotPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.forgotPassword(email: email); - - emit( - state.copyWith( - isSubmitting: false, - forgotPasswordResult: result, - ), - ); - } - - Future _onCheckHasPassword(Emitter emit) async { - if (!_isInitialized) { - Log.info('checkHasPassword: not initialized'); - return; - } - - if (state.isSubmitting) { - Log.info('checkHasPassword: already submitting'); - return; - } - - _clearState(emit, true); - - final result = await passwordHttpService.checkHasPassword(); - - emit( - state.copyWith( - isSubmitting: false, - hasPassword: result.fold( - (success) => success, - (error) => false, - ), - checkHasPasswordResult: result, - ), - ); - } - - void _clearState(Emitter emit, bool isSubmitting) { - emit( - state.copyWith( - isSubmitting: isSubmitting, - changePasswordResult: null, - setupPasswordResult: null, - forgotPasswordResult: null, - checkHasPasswordResult: null, - ), - ); - } -} - -@freezed -class PasswordEvent with _$PasswordEvent { - const factory PasswordEvent.init() = Init; - - // Change password - const factory PasswordEvent.changePassword({ - required String oldPassword, - required String newPassword, - }) = ChangePassword; - - // Setup password - const factory PasswordEvent.setupPassword({ - required String newPassword, - }) = SetupPassword; - - // Forgot password - const factory PasswordEvent.forgotPassword({ - required String email, - }) = ForgotPassword; - - // Check has password - const factory PasswordEvent.checkHasPassword() = CheckHasPassword; - - // Cancel operation - const factory PasswordEvent.cancel() = Cancel; -} - -@freezed -class PasswordState with _$PasswordState { - const factory PasswordState({ - required bool isSubmitting, - required bool hasPassword, - required FlowyResult? changePasswordResult, - required FlowyResult? setupPasswordResult, - required FlowyResult? forgotPasswordResult, - required FlowyResult? checkHasPasswordResult, - }) = _PasswordState; - - factory PasswordState.initial() => const PasswordState( - isSubmitting: false, - hasPassword: false, - changePasswordResult: null, - setupPasswordResult: null, - forgotPasswordResult: null, - checkHasPasswordResult: null, - ); -} diff --git a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart b/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart deleted file mode 100644 index c56c4f595d..0000000000 --- a/frontend/appflowy_flutter/lib/user/application/password/password_http_service.dart +++ /dev/null @@ -1,189 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:http/http.dart' as http; - -enum PasswordEndpoint { - changePassword, - forgotPassword, - setupPassword, - checkHasPassword; - - String get path { - switch (this) { - case PasswordEndpoint.changePassword: - return '/gotrue/user/change-password'; - case PasswordEndpoint.forgotPassword: - return '/gotrue/user/recover'; - case PasswordEndpoint.setupPassword: - return '/gotrue/user/change-password'; - case PasswordEndpoint.checkHasPassword: - return '/gotrue/user/auth-info'; - } - } - - String get method { - switch (this) { - case PasswordEndpoint.changePassword: - case PasswordEndpoint.setupPassword: - case PasswordEndpoint.forgotPassword: - return 'POST'; - case PasswordEndpoint.checkHasPassword: - return 'GET'; - } - } - - Uri uri(String baseUrl) => Uri.parse('$baseUrl$path'); -} - -class PasswordHttpService { - PasswordHttpService({ - required this.baseUrl, - required this.authToken, - }); - - final String baseUrl; - final String authToken; - - final http.Client client = http.Client(); - - Map get headers => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $authToken', - }; - - /// Changes the user's password - /// - /// [currentPassword] - The user's current password - /// [newPassword] - The new password to set - Future> changePassword({ - required String currentPassword, - required String newPassword, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.changePassword, - body: { - 'current_password': currentPassword, - 'password': newPassword, - }, - errorMessage: 'Failed to change password', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Sends a password reset email to the user - /// - /// [email] - The email address of the user - Future> forgotPassword({ - required String email, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.forgotPassword, - body: {'email': email}, - errorMessage: 'Failed to send password reset email', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Sets up a password for a user that doesn't have one - /// - /// [newPassword] - The new password to set - Future> setupPassword({ - required String newPassword, - }) async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.setupPassword, - body: {'password': newPassword}, - errorMessage: 'Failed to setup password', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Checks if the user has a password set - Future> checkHasPassword() async { - final result = await _makeRequest( - endpoint: PasswordEndpoint.checkHasPassword, - errorMessage: 'Failed to check password status', - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['has_password'] ?? false), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to check password status: $e'), - ); - } - } - - /// Makes a request to the specified endpoint with the given body - Future> _makeRequest({ - required PasswordEndpoint endpoint, - Map? body, - String errorMessage = 'Request failed', - }) async { - try { - final uri = endpoint.uri(baseUrl); - http.Response response; - - if (endpoint.method == 'POST') { - response = await client.post( - uri, - headers: headers, - body: body != null ? jsonEncode(body) : null, - ); - } else if (endpoint.method == 'GET') { - response = await client.get( - uri, - headers: headers, - ); - } else { - return FlowyResult.failure( - FlowyError(msg: 'Invalid request method: ${endpoint.method}'), - ); - } - - if (response.statusCode == 200) { - if (response.body.isNotEmpty) { - return FlowyResult.success(jsonDecode(response.body)); - } - return FlowyResult.success(true); - } else { - final errorBody = - response.body.isNotEmpty ? jsonDecode(response.body) : {}; - - Log.info( - '${endpoint.name} request failed: ${response.statusCode}, $errorBody ', - ); - - return FlowyResult.failure( - FlowyError( - msg: errorBody['msg'] ?? errorMessage, - ), - ); - } - } catch (e) { - Log.error('${endpoint.name} request failed: error: $e'); - - return FlowyResult.failure( - FlowyError(msg: 'Network error: ${e.toString()}'), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart index 24f53e48e8..d50c6fc795 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_bloc.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/user/application/reminder/reminder_service.dart'; @@ -18,14 +17,11 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; part 'reminder_bloc.freezed.dart'; class ReminderBloc extends Bloc { ReminderBloc() : super(ReminderState()) { - Log.info('ReminderBloc created'); - _actionBloc = getIt(); _reminderService = const ReminderService(); timer = _periodicCheck(); @@ -41,59 +37,55 @@ class ReminderBloc extends Bloc { on( (event, emit) async { await event.when( + markAllRead: () async { + final unreadReminders = + state.pastReminders.where((reminder) => !reminder.isRead); + + final reminders = [...state.reminders]; + final updatedReminders = []; + for (final reminder in unreadReminders) { + reminders.remove(reminder); + + reminder.isRead = true; + await _reminderService.updateReminder(reminder: reminder); + + updatedReminders.add(reminder); + } + + reminders.addAll(updatedReminders); + emit(state.copyWith(reminders: reminders)); + }, started: () async { - Log.info('Start fetching reminders'); + final remindersOrFailure = await _reminderService.fetchReminders(); - final result = await _reminderService.fetchReminders(); - - result.fold( - (reminders) { - Log.info('Fetched reminders on startup: ${reminders.length}'); - emit(state.copyWith(reminders: reminders)); - }, - (error) => Log.error('Failed to fetch reminders: $error'), + remindersOrFailure.fold( + (reminders) => emit(state.copyWith(reminders: reminders)), + (error) => Log.error(error), ); }, remove: (reminderId) async { - final result = await _reminderService.removeReminder( - reminderId: reminderId, - ); + final unitOrFailure = + await _reminderService.removeReminder(reminderId: reminderId); - result.fold( + unitOrFailure.fold( (_) { - Log.info('Removed reminder: $reminderId'); final reminders = [...state.reminders]; reminders.removeWhere((e) => e.id == reminderId); emit(state.copyWith(reminders: reminders)); }, - (error) => Log.error( - 'Failed to remove reminder($reminderId): $error', - ), + (error) => Log.error(error), ); }, add: (reminder) async { - // check the timestamp in the reminder - if (reminder.createdAt == null) { - reminder.freeze(); - reminder = reminder.rebuild((update) { - update.meta[ReminderMetaKeys.createdAt] = - DateTime.now().millisecondsSinceEpoch.toString(); - }); - } + final unitOrFailure = + await _reminderService.addReminder(reminder: reminder); - final result = await _reminderService.addReminder( - reminder: reminder, - ); - - return result.fold( + return unitOrFailure.fold( (_) { - Log.info('Added reminder: ${reminder.id}'); - Log.info('Before adding reminder: ${state.reminders.length}'); final reminders = [...state.reminders, reminder]; - Log.info('After adding reminder: ${reminders.length}'); emit(state.copyWith(reminders: reminders)); }, - (error) => Log.error('Failed to add reminder: $error'), + (error) => Log.error(error), ); }, addById: (reminderId, objectId, scheduledAt, meta) async => add( @@ -110,9 +102,8 @@ class ReminderBloc extends Bloc { ), ), update: (updateObject) async { - final reminder = state.reminders.firstWhereOrNull( - (r) => r.id == updateObject.id, - ); + final reminder = state.reminders + .firstWhereOrNull((r) => r.id == updateObject.id); if (reminder == null) { return; @@ -120,23 +111,18 @@ class ReminderBloc extends Bloc { final newReminder = updateObject.merge(a: reminder); final failureOrUnit = await _reminderService.updateReminder( - reminder: newReminder, + reminder: updateObject.merge(a: reminder), ); - Log.info('Updating reminder: ${reminder.id}'); - failureOrUnit.fold( (_) { - Log.info('Updated reminder: ${reminder.id}'); final index = state.reminders.indexWhere((r) => r.id == reminder.id); final reminders = [...state.reminders]; reminders.replaceRange(index, index + 1, [newReminder]); emit(state.copyWith(reminders: reminders)); }, - (error) => Log.error( - 'Failed to update reminder(${reminder.id}): $error', - ), + (error) => Log.error(error), ); }, pressReminder: (reminderId, path, view) { @@ -185,167 +171,11 @@ class ReminderBloc extends Bloc { ); } }, - markAsRead: (reminderIds) async { - final reminders = await _onMarkAsRead(reminderIds: reminderIds); - - Log.info('Marked reminders as read: $reminderIds'); - - emit( - state.copyWith( - reminders: reminders, - ), - ); - }, - archive: (reminderIds) async { - final reminders = await _onArchived( - isArchived: true, - reminderIds: reminderIds, - ); - - Log.info('Archived reminders: $reminderIds'); - - emit( - state.copyWith( - reminders: reminders, - ), - ); - }, - markAllRead: () async { - final reminders = await _onMarkAsRead(); - - Log.info('Marked all reminders as read'); - - emit( - state.copyWith( - reminders: reminders, - ), - ); - }, - archiveAll: () async { - final reminders = await _onArchived(isArchived: true); - - Log.info('Archived all reminders'); - - emit( - state.copyWith( - reminders: reminders, - ), - ); - }, - unarchiveAll: () async { - final reminders = await _onArchived(isArchived: false); - emit( - state.copyWith( - reminders: reminders, - ), - ); - }, - refresh: () async { - final result = await _reminderService.fetchReminders(); - - result.fold( - (reminders) { - Log.info('Fetched reminders on refresh: ${reminders.length}'); - emit(state.copyWith(reminders: reminders)); - }, - (error) => Log.error('Failed to fetch reminders: $error'), - ); - }, ); }, ); } - /// Mark the reminder as read - /// - /// If the [reminderIds] is null, all unread reminders will be marked as read - /// Otherwise, only the reminders with the given IDs will be marked as read - Future> _onMarkAsRead({ - List? reminderIds, - }) async { - final Iterable remindersToUpdate; - - if (reminderIds != null) { - remindersToUpdate = state.reminders.where( - (reminder) => reminderIds.contains(reminder.id) && !reminder.isRead, - ); - } else { - // Get all reminders that are not matching the isArchived flag - remindersToUpdate = state.reminders.where( - (reminder) => !reminder.isRead, - ); - } - - for (final reminder in remindersToUpdate) { - reminder.isRead = true; - - await _reminderService.updateReminder(reminder: reminder); - Log.info('Mark reminder ${reminder.id} as read'); - } - - return state.reminders.map((e) { - if (reminderIds != null && !reminderIds.contains(e.id)) { - return e; - } - - if (e.isRead) { - return e; - } - - e.freeze(); - return e.rebuild((update) { - update.isRead = true; - }); - }).toList(); - } - - /// Archive or unarchive reminders - /// - /// If the [reminderIds] is null, all reminders will be archived - /// Otherwise, only the reminders with the given IDs will be archived or unarchived - Future> _onArchived({ - required bool isArchived, - List? reminderIds, - }) async { - final Iterable remindersToUpdate; - - if (reminderIds != null) { - remindersToUpdate = state.reminders.where( - (reminder) => - reminderIds.contains(reminder.id) && - reminder.isArchived != isArchived, - ); - } else { - // Get all reminders that are not matching the isArchived flag - remindersToUpdate = state.reminders.where( - (reminder) => reminder.isArchived != isArchived, - ); - } - - for (final reminder in remindersToUpdate) { - reminder.isRead = isArchived; - reminder.meta[ReminderMetaKeys.isArchived] = isArchived.toString(); - await _reminderService.updateReminder(reminder: reminder); - Log.info('Reminder ${reminder.id} is archived: $isArchived'); - } - - return state.reminders.map((e) { - if (reminderIds != null && !reminderIds.contains(e.id)) { - return e; - } - - if (e.isArchived == isArchived) { - return e; - } - - e.freeze(); - return e.rebuild((update) { - update.isRead = isArchived; - update.meta[ReminderMetaKeys.isArchived] = isArchived.toString(); - }); - }).toList(); - } - Timer _periodicCheck() { return Timer.periodic( const Duration(minutes: 1), @@ -409,31 +239,14 @@ class ReminderEvent with _$ReminderEvent { // Update a reminder (eg. isAck, isRead, etc.) const factory ReminderEvent.update(ReminderUpdate update) = _Update; - // Event to mark specific reminders as read, takes a list of reminder IDs - const factory ReminderEvent.markAsRead(List reminderIds) = - _MarkAsRead; - - // Event to mark all unread reminders as read + // Mark all unread reminders as read const factory ReminderEvent.markAllRead() = _MarkAllRead; - // Event to archive specific reminders, takes a list of reminder IDs - const factory ReminderEvent.archive(List reminderIds) = _Archive; - - // Event to archive all reminders - const factory ReminderEvent.archiveAll() = _ArchiveAll; - - // Event to unarchive all reminders - const factory ReminderEvent.unarchiveAll() = _UnarchiveAll; - - // Event to handle reminder press action const factory ReminderEvent.pressReminder({ required String reminderId, @Default(null) int? path, @Default(null) ViewPB? view, }) = _PressReminder; - - // Event to refresh reminders - const factory ReminderEvent.refresh() = _Refresh; } /// Object used to merge updates with @@ -446,8 +259,6 @@ class ReminderUpdate { this.isRead, this.scheduledAt, this.includeTime, - this.isArchived, - this.date, }); final String id; @@ -455,27 +266,17 @@ class ReminderUpdate { final bool? isRead; final DateTime? scheduledAt; final bool? includeTime; - final bool? isArchived; - final DateTime? date; ReminderPB merge({required ReminderPB a}) { final isAcknowledged = isAck == null && scheduledAt != null ? scheduledAt!.isBefore(DateTime.now()) : a.isAck; - final meta = {...a.meta}; + final meta = a.meta; if (includeTime != a.includeTime) { meta[ReminderMetaKeys.includeTime] = includeTime.toString(); } - if (isArchived != a.isArchived) { - meta[ReminderMetaKeys.isArchived] = isArchived.toString(); - } - - if (date != a.date && date != null) { - meta[ReminderMetaKeys.date] = date!.millisecondsSinceEpoch.toString(); - } - return ReminderPB( id: a.id, objectId: a.objectId, @@ -526,7 +327,7 @@ class ReminderState { } late final List _reminders; - List get reminders => _reminders.unique((e) => e.id); + List get reminders => _reminders; late final List pastReminders; late final List upcomingReminders; diff --git a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart index 1b5aeaeb43..94bf638de5 100644 --- a/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart +++ b/frontend/appflowy_flutter/lib/user/application/reminder/reminder_extension.dart @@ -4,15 +4,6 @@ class ReminderMetaKeys { static String includeTime = "include_time"; static String blockId = "block_id"; static String rowId = "row_id"; - static String createdAt = "created_at"; - static String isArchived = "is_archived"; - static String date = "date"; -} - -enum ReminderType { - past, - today, - other, } extension ReminderExtension on ReminderPB { @@ -21,46 +12,4 @@ extension ReminderExtension on ReminderPB { return includeTimeStr != null ? includeTimeStr == true.toString() : null; } - - String? get blockId => meta[ReminderMetaKeys.blockId]; - - String? get rowId => meta[ReminderMetaKeys.rowId]; - - int? get createdAt { - final t = meta[ReminderMetaKeys.createdAt]; - return t != null ? int.tryParse(t) : null; - } - - bool get isArchived { - final t = meta[ReminderMetaKeys.isArchived]; - return t != null ? t == true.toString() : false; - } - - DateTime? get date { - final t = meta[ReminderMetaKeys.date]; - return t != null ? DateTime.fromMillisecondsSinceEpoch(int.parse(t)) : null; - } - - ReminderType get type { - final date = this.date?.millisecondsSinceEpoch; - - if (date == null) { - return ReminderType.other; - } - - final now = DateTime.now().millisecondsSinceEpoch; - - if (date < now) { - return ReminderType.past; - } - - final difference = date - now; - const oneDayInMilliseconds = 24 * 60 * 60 * 1000; - - if (difference < oneDayInMilliseconds) { - return ReminderType.today; - } - - return ReminderType.other; - } } diff --git a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart index 9691a1269b..52a103899c 100644 --- a/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/sign_in_bloc.dart @@ -1,16 +1,16 @@ +import 'package:flutter/foundation.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -30,26 +30,12 @@ class SignInBloc extends Bloc { on( (event, emit) async { await event.when( - signInWithEmailAndPassword: (email, password) async => - _onSignInWithEmailAndPassword( - emit, - email: email, - password: password, - ), - signInWithOAuth: (platform) async => _onSignInWithOAuth( - emit, - platform: platform, - ), - signInAsGuest: () async => _onSignInAsGuest(emit), - signInWithMagicLink: (email) async => _onSignInWithMagicLink( - emit, - email: email, - ), - signInWithPasscode: (email, passcode) async => _onSignInWithPasscode( - emit, - email: email, - passcode: passcode, - ), + signedInWithUserEmailAndPassword: () async => _onSignIn(emit), + signedInWithOAuth: (platform) async => + _onSignInWithOAuth(emit, platform), + signedInAsGuest: () async => _onSignInAsGuest(emit), + signedWithMagicLink: (email) async => + _onSignInWithMagicLink(emit, email), deepLinkStateChange: (result) => _onDeepLinkStateChange(emit, result), cancel: () { emit( @@ -133,34 +119,26 @@ class SignInBloc extends Bloc { } } - Future _onSignInWithEmailAndPassword( - Emitter emit, { - required String email, - required String password, - }) async { + Future _onSignIn(Emitter emit) async { final result = await authService.signInWithEmailPassword( - email: email, - password: password, + email: state.email ?? '', + password: state.password ?? '', ); emit( result.fold( - (gotrueTokenResponse) { - getIt().passGotrueTokenResponse( - gotrueTokenResponse, - ); - return state.copyWith( - isSubmitting: false, - ); - }, + (userProfile) => state.copyWith( + isSubmitting: false, + successOrFail: FlowyResult.success(userProfile), + ), (error) => _stateFromCode(error), ), ); } Future _onSignInWithOAuth( - Emitter emit, { - required String platform, - }) async { + Emitter emit, + String platform, + ) async { emit( state.copyWith( isSubmitting: true, @@ -183,16 +161,9 @@ class SignInBloc extends Bloc { } Future _onSignInWithMagicLink( - Emitter emit, { - required String email, - }) async { - if (state.isSubmitting) { - Log.error('Sign in with magic link is already in progress'); - return; - } - - Log.info('Sign in with magic link: $email'); - + Emitter emit, + String email, + ) async { emit( state.copyWith( isSubmitting: true, @@ -206,50 +177,7 @@ class SignInBloc extends Bloc { emit( result.fold( - (userProfile) => state.copyWith( - isSubmitting: false, - ), - (error) => _stateFromCode(error), - ), - ); - } - - Future _onSignInWithPasscode( - Emitter emit, { - required String email, - required String passcode, - }) async { - if (state.isSubmitting) { - Log.error('Sign in with passcode is already in progress'); - return; - } - - Log.info('Sign in with passcode: $email, $passcode'); - - emit( - state.copyWith( - isSubmitting: true, - emailError: null, - passwordError: null, - successOrFail: null, - ), - ); - - final result = await authService.signInWithPasscode( - email: email, - passcode: passcode, - ); - - emit( - result.fold( - (gotrueTokenResponse) { - getIt().passGotrueTokenResponse( - gotrueTokenResponse, - ); - return state.copyWith( - isSubmitting: false, - ); - }, + (userProfile) => state.copyWith(isSubmitting: true), (error) => _stateFromCode(error), ), ); @@ -280,8 +208,6 @@ class SignInBloc extends Bloc { } SignInState _stateFromCode(FlowyError error) { - Log.error('SignInState _stateFromCode: ${error.msg}'); - switch (error.code) { case ErrorCode.EmailFormatInvalid: return state.copyWith( @@ -296,20 +222,10 @@ class SignInBloc extends Bloc { emailError: null, ); case ErrorCode.UserUnauthorized: - final errorMsg = error.msg; - String msg = LocaleKeys.signIn_generalError.tr(); - if (errorMsg.contains('rate limit') || - errorMsg.contains('For security purposes')) { - msg = LocaleKeys.signIn_tooFrequentVerificationCodeRequest.tr(); - } else if (errorMsg.contains('invalid')) { - msg = LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(); - } else if (errorMsg.contains('Invalid login credentials')) { - msg = LocaleKeys.signIn_invalidLoginCredentials.tr(); - } return state.copyWith( isSubmitting: false, successOrFail: FlowyResult.failure( - FlowyError(msg: msg), + FlowyError(msg: LocaleKeys.signIn_limitRateError.tr()), ), ); default: @@ -325,35 +241,19 @@ class SignInBloc extends Bloc { @freezed class SignInEvent with _$SignInEvent { - // Sign in methods - const factory SignInEvent.signInWithEmailAndPassword({ - required String email, - required String password, - }) = SignInWithEmailAndPassword; - const factory SignInEvent.signInWithOAuth({ - required String platform, - }) = SignInWithOAuth; - const factory SignInEvent.signInAsGuest() = SignInAsGuest; - const factory SignInEvent.signInWithMagicLink({ - required String email, - }) = SignInWithMagicLink; - const factory SignInEvent.signInWithPasscode({ - required String email, - required String passcode, - }) = SignInWithPasscode; - - // Event handlers - const factory SignInEvent.emailChanged({ - required String email, - }) = EmailChanged; - const factory SignInEvent.passwordChanged({ - required String password, - }) = PasswordChanged; + const factory SignInEvent.signedInWithUserEmailAndPassword() = + SignedInWithUserEmailAndPassword; + const factory SignInEvent.signedInWithOAuth(String platform) = + SignedInWithOAuth; + const factory SignInEvent.signedInAsGuest() = SignedInAsGuest; + const factory SignInEvent.signedWithMagicLink(String email) = + SignedWithMagicLink; + const factory SignInEvent.emailChanged(String email) = EmailChanged; + const factory SignInEvent.passwordChanged(String password) = PasswordChanged; const factory SignInEvent.deepLinkStateChange(DeepLinkResult result) = DeepLinkStateChange; - - const factory SignInEvent.cancel() = Cancel; - const factory SignInEvent.switchLoginType(LoginType type) = SwitchLoginType; + const factory SignInEvent.cancel() = _Cancel; + const factory SignInEvent.switchLoginType(LoginType type) = _SwitchLoginType; } // we support sign in directly without sign up, but we want to allow the users to sign up if they want to diff --git a/frontend/appflowy_flutter/lib/user/application/supabase_realtime.dart b/frontend/appflowy_flutter/lib/user/application/supabase_realtime.dart new file mode 100644 index 0000000000..ba3dcb6cb7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/application/supabase_realtime.dart @@ -0,0 +1,125 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/user_auth_listener.dart'; +import 'package:appflowy/user/application/user_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// A service to manage realtime interactions with Supabase. +/// +/// `SupabaseRealtimeService` handles subscribing to table changes in Supabase +/// based on the authentication state of a user. The service is initialized with +/// a reference to a Supabase instance and sets up the necessary subscriptions +/// accordingly. +class SupabaseRealtimeService { + SupabaseRealtimeService({required this.supabase}) { + _subscribeAuthState(); + _subscribeTablesChanges(); + + _authStateListener.start( + didSignIn: () { + _subscribeTablesChanges(); + isLoggingOut = false; + }, + onInvalidAuth: (message) async { + Log.error(message); + await channel?.unsubscribe(); + channel = null; + if (!isLoggingOut) { + isLoggingOut = true; + await runAppFlowy(); + } + }, + ); + } + + final Supabase supabase; + final _authStateListener = UserAuthStateListener(); + + bool isLoggingOut = false; + + RealtimeChannel? channel; + StreamSubscription? authStateSubscription; + + void _subscribeAuthState() { + final auth = Supabase.instance.client.auth; + authStateSubscription = auth.onAuthStateChange.listen((state) async { + Log.info("Supabase auth state change: ${state.event}"); + }); + } + + Future _subscribeTablesChanges() async { + final result = await UserBackendService.getCurrentUserProfile(); + result.fold( + (userProfile) { + Log.info("Start listening supabase table changes"); + + // https://supabase.com/docs/guides/realtime/postgres-changes + + const ops = RealtimeChannelConfig(ack: true); + channel?.unsubscribe(); + channel = supabase.client.channel("table-db-changes", opts: ops); + for (final name in [ + "document", + "folder", + "database", + "database_row", + "w_database", + ]) { + channel?.onPostgresChanges( + event: PostgresChangeEvent.insert, + schema: 'public', + table: 'af_collab_update_$name', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.eq, + column: 'uid', + value: userProfile.id, + ), + callback: _onPostgresChangesCallback, + ); + } + + channel?.onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + table: 'af_user', + filter: PostgresChangeFilter( + type: PostgresChangeFilterType.eq, + column: 'uid', + value: userProfile.id, + ), + callback: _onPostgresChangesCallback, + ); + + channel?.subscribe( + (status, [err]) { + Log.info( + "subscribe channel statue: $status, err: $err", + ); + }, + ); + }, + (_) => null, + ); + } + + Future dispose() async { + await _authStateListener.stop(); + await authStateSubscription?.cancel(); + await channel?.unsubscribe(); + } + + void _onPostgresChangesCallback(PostgresChangePayload payload) { + try { + final jsonStr = jsonEncode(payload); + final pb = RealtimePayloadPB.create()..jsonStr = jsonStr; + UserEventPushRealtimeEvent(pb).send(); + } catch (e) { + Log.error(e); + } + } +} diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index d3ebe0201b..81a081b4e3 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/foundation.dart'; import 'package:appflowy/core/notification/folder_notification.dart'; @@ -12,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart' as user; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; @@ -23,9 +23,6 @@ typedef DidUpdateUserWorkspacesCallback = void Function( RepeatedUserWorkspacePB workspaces, ); typedef UserProfileNotifyValue = FlowyResult; -typedef DidUpdateUserWorkspaceSetting = void Function( - WorkspaceSettingsPB settings, -); class UserListener { UserListener({ @@ -40,26 +37,28 @@ class UserListener { /// Update notification about _all_ of the users workspaces /// - DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated; + DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces; /// Update notification about _one_ workspace /// - DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated; - DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated; + DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace; void start({ void Function(UserProfileNotifyValue)? onProfileUpdated, - DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated, - void Function(UserWorkspacePB)? onUserWorkspaceUpdated, - DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated, + void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces, + void Function(UserWorkspacePB)? didUpdateUserWorkspace, }) { if (onProfileUpdated != null) { _profileNotifier?.addPublishListener(onProfileUpdated); } - this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated; - this.onUserWorkspaceUpdated = onUserWorkspaceUpdated; - this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated; + if (didUpdateUserWorkspaces != null) { + this.didUpdateUserWorkspaces = didUpdateUserWorkspaces; + } + + if (didUpdateUserWorkspace != null) { + this.didUpdateUserWorkspace = didUpdateUserWorkspace; + } _userParser = UserNotificationParser( id: _userProfile.id.toString(), @@ -93,18 +92,13 @@ class UserListener { result.map( (r) { final value = RepeatedUserWorkspacePB.fromBuffer(r); - onUserWorkspaceListUpdated?.call(value); + didUpdateUserWorkspaces?.call(value); }, ); break; case user.UserNotification.DidUpdateUserWorkspace: result.map( - (r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)), - ); - case user.UserNotification.DidUpdateWorkspaceSetting: - result.map( - (r) => onUserWorkspaceSettingUpdated - ?.call(WorkspaceSettingsPB.fromBuffer(r)), + (r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)), ); break; default: @@ -113,21 +107,22 @@ class UserListener { } } -typedef WorkspaceLatestNotifyValue = FlowyResult; +typedef WorkspaceSettingNotifyValue + = FlowyResult; -class FolderListener { - FolderListener(); +class UserWorkspaceListener { + UserWorkspaceListener(); - final PublishNotifier _latestChangedNotifier = + final PublishNotifier _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(WorkspaceLatestNotifyValue)? onLatestUpdated, + void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { - if (onLatestUpdated != null) { - _latestChangedNotifier.addPublishListener(onLatestUpdated); + if (onSettingUpdated != null) { + _settingChangedNotifier.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -145,9 +140,9 @@ class FolderListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _latestChangedNotifier.value = - FlowyResult.success(WorkspaceLatestPB.fromBuffer(payload)), - (error) => _latestChangedNotifier.value = FlowyResult.failure(error), + (payload) => _settingChangedNotifier.value = + FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), + (error) => _settingChangedNotifier.value = FlowyResult.failure(error), ); break; default: @@ -157,6 +152,6 @@ class FolderListener { Future stop() async { await _listener?.stop(); - _latestChangedNotifier.dispose(); + _settingChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index ff1cfb6575..172ea91a8b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -1,29 +1,22 @@ import 'dart:async'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flutter/foundation.dart'; abstract class IUserBackendService { - Future> cancelSubscription( - String workspaceId, - SubscriptionPlanPB plan, - String? reason, - ); + Future> cancelSubscription(String workspaceId); Future> createSubscription( String workspaceId, SubscriptionPlanPB plan, ); } -const _baseBetaUrl = 'https://beta.appflowy.com'; -const _baseProdUrl = 'https://appflowy.com'; - class UserBackendService implements IUserBackendService { UserBackendService({required this.userId}); @@ -40,6 +33,8 @@ class UserBackendService implements IUserBackendService { String? password, String? email, String? iconUrl, + String? openAIKey, + String? stabilityAiKey, }) { final payload = UpdateUserProfilePayloadPB.create()..id = userId; @@ -59,6 +54,14 @@ class UserBackendService implements IUserBackendService { payload.iconUrl = iconUrl; } + if (openAIKey != null) { + payload.openaiKey = openAIKey; + } + + if (stabilityAiKey != null) { + payload.stabilityAiKey = stabilityAiKey; + } + return UserEventUpdateUserProfile(payload).send(); } @@ -76,26 +79,6 @@ class UserBackendService implements IUserBackendService { return UserEventMagicLinkSignIn(payload).send(); } - static Future> - signInWithPasscode( - String email, - String passcode, - ) async { - final payload = PasscodeSignInPB(email: email, passcode: passcode); - return UserEventPasscodeSignIn(payload).send(); - } - - Future> signInWithPassword( - String email, - String password, - ) { - final payload = SignInPayloadPB( - email: email, - password: password, - ); - return UserEventSignInWithEmailPassword(payload).send(); - } - static Future> signOut() { return UserEventSignOut().send(); } @@ -121,17 +104,12 @@ class UserBackendService implements IUserBackendService { }); } - Future> openWorkspace( - String workspaceId, - AuthTypePB authType, - ) { - final payload = OpenUserWorkspacePB() - ..workspaceId = workspaceId - ..workspaceAuthType = authType; + Future> openWorkspace(String workspaceId) { + final payload = UserWorkspaceIdPB.create()..workspaceId = workspaceId; return UserEventOpenWorkspace(payload).send(); } - static Future> getCurrentWorkspace() { + Future> getCurrentWorkspace() { return FolderEventReadCurrentWorkspace().send().then((result) { return result.fold( (workspace) => FlowyResult.success(workspace), @@ -140,13 +118,25 @@ class UserBackendService implements IUserBackendService { }); } + Future> createWorkspace( + String name, + String desc, + ) { + final request = CreateWorkspacePayloadPB.create() + ..name = name + ..desc = desc; + return FolderEventCreateFolderWorkspace(request).send().then((result) { + return result.fold( + (workspace) => FlowyResult.success(workspace), + (error) => FlowyResult.failure(error), + ); + }); + } + Future> createUserWorkspace( String name, - AuthTypePB authType, ) { - final request = CreateWorkspacePB.create() - ..name = name - ..authType = authType; + final request = CreateWorkspacePB.create()..name = name; return UserEventCreateWorkspace(request).send(); } @@ -238,10 +228,16 @@ class UserBackendService implements IUserBackendService { return UserEventLeaveWorkspace(data).send(); } - static Future> - getWorkspaceSubscriptionInfo(String workspaceId) { - final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId; - return UserEventGetWorkspaceSubscriptionInfo(params).send(); + static Future> + getWorkspaceSubscriptions() { + return UserEventGetWorkspaceSubscriptions().send(); + } + + Future> + getWorkspaceMember() async { + final data = WorkspaceMemberIdPB.create()..uid = userId; + + return UserEventGetMemberInfo(data).send(); } @override @@ -254,42 +250,15 @@ class UserBackendService implements IUserBackendService { ..recurringInterval = RecurringIntervalPB.Year ..workspaceSubscriptionPlan = plan ..successUrl = - '${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}'; + '${getIt().appflowyCloudConfig.base_url}/web/payment-success'; return UserEventSubscribeWorkspace(request).send(); } @override Future> cancelSubscription( String workspaceId, - SubscriptionPlanPB plan, [ - String? reason, - ]) { - final request = CancelWorkspaceSubscriptionPB() - ..workspaceId = workspaceId - ..plan = plan; - - if (reason != null) { - request.reason = reason; - } - + ) { + final request = UserWorkspaceIdPB()..workspaceId = workspaceId; return UserEventCancelWorkspaceSubscription(request).send(); } - - Future> updateSubscriptionPeriod( - String workspaceId, - SubscriptionPlanPB plan, - RecurringIntervalPB interval, - ) { - final request = UpdateWorkspaceSubscriptionPaymentPeriodPB() - ..workspaceId = workspaceId - ..plan = plan - ..recurringInterval = interval; - - return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send(); - } - - // NOTE: This function is irreversible and will delete the current user's account. - static Future> deleteCurrentAccount() { - return UserEventDeleteAccount().send(); - } } diff --git a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart index 7ff50dbd02..ce51fdd10b 100644 --- a/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart +++ b/frontend/appflowy_flutter/lib/user/application/workspace_error_bloc.dart @@ -1,7 +1,9 @@ import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -20,10 +22,20 @@ class WorkspaceErrorBloc void _dispatch() { on( (event, emit) async { - event.when( + await event.when( init: () { // _loadSnapshots(); }, + resetWorkspace: () async { + emit(state.copyWith(loadingState: const LoadingState.loading())); + final payload = ResetWorkspacePB.create() + ..workspaceId = userFolder.workspaceId + ..uid = userFolder.uid; + final result = await UserEventResetWorkspace(payload).send(); + if (!isClosed) { + add(WorkspaceErrorEvent.didResetWorkspace(result)); + } + }, didResetWorkspace: (result) { result.fold( (_) { @@ -56,6 +68,7 @@ class WorkspaceErrorBloc class WorkspaceErrorEvent with _$WorkspaceErrorEvent { const factory WorkspaceErrorEvent.init() = _Init; const factory WorkspaceErrorEvent.logout() = _DidLogout; + const factory WorkspaceErrorEvent.resetWorkspace() = _ResetWorkspace; const factory WorkspaceErrorEvent.didResetWorkspace( FlowyResult result, ) = _DidResetWorkspace; diff --git a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart index ddb1a07f96..a9b11cb42e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/anon_user.dart @@ -74,8 +74,9 @@ class AnonUserItem extends StatelessWidget { @override Widget build(BuildContext context) { final icon = isSelected ? const FlowySvg(FlowySvgs.check_s) : null; - final isDisabled = isSelected || user.workspaceAuthType != AuthTypePB.Local; - final desc = "${user.name}\t ${user.workspaceAuthType}\t"; + final isDisabled = + isSelected || user.authenticator != AuthenticatorPB.Local; + final desc = "${user.name}\t ${user.authenticator}\t"; final child = SizedBox( height: 30, child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart index ccad6c0a26..3ecacf0961 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_open_workspace_error.dart @@ -1,10 +1,10 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/router.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:flutter/material.dart'; void handleOpenWorkspaceError(BuildContext context, FlowyError error) { @@ -15,22 +15,24 @@ void handleOpenWorkspaceError(BuildContext context, FlowyError error) { getIt().pushWorkspaceErrorScreen(context, userFolder, error); break; case ErrorCode.InvalidEncryptSecret: - case ErrorCode.NetworkError: - showToastNotification( - message: error.msg, - type: ToastificationType.error, + showSnapBar( + context, + error.msg, ); break; + case ErrorCode.HttpError: + showSnapBar( + context, + error.msg, + ); default: - showToastNotification( - message: error.msg, - type: ToastificationType.error, - callbacks: ToastificationCallbacks( - onDismissed: (_) { - getIt().signOut(); - runAppFlowy(); - }, - ), + showSnapBar( + context, + error.msg, + onClosed: () { + getIt().signOut(); + runAppFlowy(); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart new file mode 100644 index 0000000000..9abd417df3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/handle_user_profile_result.dart @@ -0,0 +1,25 @@ +import 'package:appflowy/user/presentation/helpers/helpers.dart'; +import 'package:appflowy/user/presentation/presentation.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter/material.dart'; + +void handleUserProfileResult( + FlowyResult userProfileResult, + BuildContext context, + AuthRouter authRouter, +) { + userProfileResult.fold( + (userProfile) { + if (userProfile.encryptionType == EncryptionTypePB.Symmetric) { + authRouter.pushEncryptionScreen(context, userProfile); + } else { + authRouter.goHomeScreen(context, userProfile); + } + }, + (error) { + handleOpenWorkspaceError(context, error); + }, + ); +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart index 11f321232e..084a360666 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/helpers/helpers.dart @@ -1 +1,2 @@ export 'handle_open_workspace_error.dart'; +export 'handle_user_profile_result.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/router.dart b/frontend/appflowy_flutter/lib/user/presentation/router.dart index 339c2f29f7..a93f6e449b 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/router.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/router.dart @@ -4,12 +4,12 @@ import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; class AuthRouter { void pushForgetPasswordScreen(BuildContext context) {} @@ -21,6 +21,10 @@ class AuthRouter { getIt().pushWorkspaceStartScreen(context, userProfile); } + void pushSignUpScreen(BuildContext context) { + context.push(SignUpScreen.routeName); + } + /// Navigates to the home screen based on the current workspace and platform. /// /// This function takes in a [BuildContext] and a [UserProfilePB] object to @@ -43,7 +47,7 @@ class AuthRouter { (workspaceSetting) { // Replace SignInScreen or SkipLogInScreen as root page. // If user click back button, it will exit app rather than go back to SignInScreen or SkipLogInScreen - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { context.go( MobileHomeScreen.routeName, ); @@ -57,6 +61,20 @@ class AuthRouter { ); } + void pushEncryptionScreen( + BuildContext context, + UserProfilePB userProfile, + ) { + // After log in,push EncryptionScreen on the top SignInScreen + context.push( + EncryptSecretScreen.routeName, + extra: { + EncryptSecretScreen.argUser: userProfile, + EncryptSecretScreen.argKey: ValueKey(userProfile.id), + }, + ); + } + Future pushWorkspaceErrorScreen( BuildContext context, UserFolderPB userFolder, @@ -96,7 +114,7 @@ class SplashRouter { void pushHomeScreen( BuildContext context, ) { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { context.push( MobileHomeScreen.routeName, ); @@ -110,7 +128,7 @@ class SplashRouter { void goHomeScreen( BuildContext context, ) { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { context.go( MobileHomeScreen.routeName, ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart new file mode 100644 index 0000000000..f0b79ed9d2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/presentation/helpers/helpers.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.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:flowy_infra_ui/widget/buttons/secondary_button.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../application/encrypt_secret_bloc.dart'; + +class EncryptSecretScreen extends StatefulWidget { + const EncryptSecretScreen({required this.user, super.key}); + + final UserProfilePB user; + + static const routeName = '/EncryptSecretScreen'; + + // arguments used in GoRouter + static const argUser = 'user'; + static const argKey = 'key'; + + @override + State createState() => _EncryptSecretScreenState(); +} + +class _EncryptSecretScreenState extends State { + final TextEditingController _textEditingController = TextEditingController(); + + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: BlocProvider( + create: (context) => EncryptSecretBloc(user: widget.user), + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (previous, current) => + previous.isSignOut != current.isSignOut, + listener: (context, state) async { + if (state.isSignOut) { + await runAppFlowy(); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.successOrFail != current.successOrFail, + listener: (context, state) async { + await state.successOrFail?.fold( + (unit) async { + await runAppFlowy(); + }, + (error) { + handleOpenWorkspaceError(context, error); + }, + ); + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + final indicator = state.loadingState?.when( + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), + finish: (result) => const SizedBox.shrink(), + idle: () => const SizedBox.shrink(), + ) ?? + const SizedBox.shrink(); + return Center( + child: SizedBox( + width: 300, + height: 160, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Opacity( + opacity: 0.6, + child: FlowyText.medium( + "${LocaleKeys.settings_menu_inputEncryptPrompt.tr()} ${widget.user.email}", + fontSize: 14, + maxLines: 10, + ), + ), + const VSpace(6), + SizedBox( + width: 300, + child: FlowyTextField( + controller: _textEditingController, + hintText: + LocaleKeys.settings_menu_inputTextFieldHint.tr(), + onChanged: (_) {}, + ), + ), + OkCancelButton( + alignment: MainAxisAlignment.end, + onOkPressed: () => + context.read().add( + EncryptSecretEvent.setEncryptSecret( + _textEditingController.text, + ), + ), + onCancelPressed: () => context + .read() + .add(const EncryptSecretEvent.cancelInputSecret()), + mode: TextButtonMode.normal, + ), + const VSpace(6), + indicator, + ], + ), + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart index 2aeba87995..088da38978 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/screens.dart @@ -1,5 +1,7 @@ export 'sign_in_screen/sign_in_screen.dart'; export 'skip_log_in_screen.dart'; export 'splash_screen.dart'; +export 'sign_up_screen.dart'; +export 'encrypt_secret_screen.dart'; export 'workspace_error_screen.dart'; export 'workspace_start_screen/workspace_start_screen.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart index 40901e92e1..7b0cf7c5fe 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart @@ -1,76 +1,78 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/settings/show_settings.dart'; import 'package:appflowy/shared/window_title_bar.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart'; -import 'package:appflowy_ui/appflowy_ui.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:universal_platform/universal_platform.dart'; class DesktopSignInScreen extends StatelessWidget { - const DesktopSignInScreen({ - super.key, - }); + const DesktopSignInScreen({super.key}); @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - + const indicatorMinHeight = 4.0; return BlocBuilder( builder: (context, state) { - final bottomPadding = UniversalPlatform.isDesktop ? 20.0 : 24.0; return Scaffold( - appBar: _buildAppBar(), + appBar: PreferredSize( + preferredSize: + Size.fromHeight(PlatformExtension.isWindows ? 40 : 60), + child: PlatformExtension.isWindows + ? const WindowTitleBar() + : const MoveWindowDetector(), + ), body: Center( child: AuthFormContainer( children: [ - const Spacer(), - - // logo and title + const VSpace(20), FlowyLogoTitle( title: LocaleKeys.welcomeText.tr(), - logoSize: Size.square(36), + logoSize: const Size(60, 60), ), - VSpace(theme.spacing.xxl), + const VSpace(20), - // continue with email and password - isLocalAuthEnabled - ? const SignInAnonymousButtonV3() - : const ContinueWithEmailAndPassword(), - - VSpace(theme.spacing.xxl), + const SignInWithMagicLinkButtons(), // third-party sign in. + const VSpace(20), + if (isAuthEnabled) ...[ const _OrDivider(), - VSpace(theme.spacing.xxl), + const VSpace(10), const ThirdPartySignInButtons(), - VSpace(theme.spacing.xxl), ], + const VSpace(20), - // sign in agreement - const SignInAgreement(), + // anonymous sign in + const SignInAnonymousButtonV2(), + const VSpace(10), - const Spacer(), - - // anonymous sign in and settings - const Row( - mainAxisSize: MainAxisSize.min, - children: [ - DesktopSignInSettingsButton(), - HSpace(20), - SignInAnonymousButtonV2(), - ], + SwitchSignInSignUpButton( + onTap: () { + final type = state.loginType == LoginType.signIn + ? LoginType.signUp + : LoginType.signIn; + context + .read() + .add(SignInEvent.switchLoginType(type)); + }, ), - VSpace(bottomPadding), + + // loading status + const VSpace(indicatorMinHeight), + state.isSubmitting + ? const LinearProgressIndicator( + minHeight: indicatorMinHeight, + ) + : const VSpace(indicatorMinHeight), + const VSpace(20), ], ), ), @@ -78,45 +80,6 @@ class DesktopSignInScreen extends StatelessWidget { }, ); } - - PreferredSize _buildAppBar() { - return PreferredSize( - preferredSize: Size.fromHeight(UniversalPlatform.isWindows ? 40 : 60), - child: UniversalPlatform.isWindows - ? const WindowTitleBar() - : const MoveWindowDetector(), - ); - } -} - -class DesktopSignInSettingsButton extends StatelessWidget { - const DesktopSignInSettingsButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFGhostIconTextButton( - text: LocaleKeys.signIn_settings.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, - ), - onTap: () => showSimpleSettingsDialog(context), - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.settings_s, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); - }, - ); - } } class _OrDivider extends StatelessWidget { @@ -124,30 +87,14 @@ class _OrDivider extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Row( children: [ - Flexible( - child: Divider( - thickness: 1, - color: theme.borderColorScheme.greyTertiary, - ), - ), + const Flexible(child: Divider(thickness: 1)), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - LocaleKeys.signIn_or.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), - ), - Flexible( - child: Divider( - thickness: 1, - color: theme.borderColorScheme.greyTertiary, - ), + child: FlowyText.regular(LocaleKeys.signIn_or.tr()), ), + const Flexible(child: Divider(thickness: 1)), ], ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart index 9eb7d5a965..2e2e1e8f39 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart @@ -1,17 +1,13 @@ -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'; import 'package:appflowy/mobile/presentation/setting/launch_settings_page.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/user/presentation/widgets/flowy_logo_title.dart'; -import 'package:appflowy_ui/appflowy_ui.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'; @@ -22,29 +18,40 @@ class MobileSignInScreen extends StatelessWidget { @override Widget build(BuildContext context) { + const double spacing = 16; + final colorScheme = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { - final theme = AppFlowyTheme.of(context); return Scaffold( - resizeToAvoidBottomInset: false, body: Padding( - padding: const EdgeInsets.symmetric(vertical: 38, horizontal: 40), + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 40), child: Column( children: [ - const Spacer(), - FlowyLogoTitle(title: LocaleKeys.welcomeText.tr()), - VSpace(theme.spacing.xxl), - isLocalAuthEnabled - ? const SignInAnonymousButtonV3() - : const ContinueWithEmailAndPassword(), - VSpace(theme.spacing.xxl), - if (isAuthEnabled) ...[ - _buildThirdPartySignInButtons(context), - VSpace(theme.spacing.xxl), - ], - const SignInAgreement(), - const Spacer(), + const Spacer(flex: 4), + _buildLogo(), + const VSpace(spacing * 2), + _buildWelcomeText(), + _buildAppNameText(colorScheme), + const VSpace(spacing * 2), + const SignInWithMagicLinkButtons(), + const VSpace(spacing), + if (isAuthEnabled) _buildThirdPartySignInButtons(colorScheme), + const VSpace(spacing), + const SignInAnonymousButtonV2(), + const VSpace(spacing), + SwitchSignInSignUpButton( + onTap: () { + final type = state.loginType == LoginType.signIn + ? LoginType.signUp + : LoginType.signIn; + context.read().add( + SignInEvent.switchLoginType(type), + ); + }, + ), + const VSpace(spacing), _buildSettingsButton(context), + if (!isAuthEnabled) const Spacer(flex: 2), ], ), ), @@ -53,8 +60,34 @@ class MobileSignInScreen extends StatelessWidget { ); } - Widget _buildThirdPartySignInButtons(BuildContext context) { - final theme = AppFlowyTheme.of(context); + Widget _buildWelcomeText() { + return FlowyText( + LocaleKeys.welcomeTo.tr(), + textAlign: TextAlign.center, + fontSize: 32, + fontWeight: FontWeight.w700, + ); + } + + Widget _buildLogo() { + return const FlowySvg( + FlowySvgs.flowy_logo_xl, + size: Size.square(64), + blendMode: null, + ); + } + + Widget _buildAppNameText(ColorScheme colorScheme) { + return FlowyText( + LocaleKeys.appName.tr(), + textAlign: TextAlign.center, + fontSize: 32, + color: const Color(0xFF00BCF0), + fontWeight: FontWeight.w700, + ); + } + + Widget _buildThirdPartySignInButtons(ColorScheme colorScheme) { return Column( children: [ Row( @@ -63,57 +96,32 @@ class MobileSignInScreen extends StatelessWidget { const Expanded(child: Divider()), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( + child: FlowyText( LocaleKeys.signIn_or.tr(), - style: TextStyle( - fontSize: 16, - color: theme.textColorScheme.secondary, - ), + color: colorScheme.onSecondary, ), ), const Expanded(child: Divider()), ], ), const VSpace(16), - // expand third-party sign in buttons on Android by default. - // on iOS, the github and discord buttons are collapsed by default. - ThirdPartySignInButtons( - expanded: Platform.isAndroid, - ), + const ThirdPartySignInButtons(), ], ); } Widget _buildSettingsButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - AFGhostIconTextButton( - text: LocaleKeys.signIn_settings.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, - ), - onTap: () => context.push(MobileLaunchSettingsPage.routeName), - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.settings_s, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); - }, - ), - const HSpace(24), - isLocalAuthEnabled - ? const ChangeCloudModeButton() - : const SignInAnonymousButtonV2(), - ], + return FlowyButton( + text: FlowyText( + LocaleKeys.signIn_settings.tr(), + textAlign: TextAlign.center, + fontSize: 12.0, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + ), + onTap: () { + context.push(MobileLaunchSettingsPage.routeName); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart index b359b2e217..731faed73e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/sign_in_screen.dart @@ -1,12 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_loading_screen.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/mobile_sign_in_screen.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter/material.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; + +import '../../helpers/helpers.dart'; class SignInScreen extends StatelessWidget { const SignInScreen({super.key}); @@ -18,27 +21,26 @@ class SignInScreen extends StatelessWidget { return BlocProvider( create: (context) => getIt(), child: BlocConsumer( - listener: _showSignInError, + listener: (context, state) { + final successOrFail = state.successOrFail; + if (successOrFail != null) { + handleUserProfileResult( + successOrFail, + context, + getIt(), + ); + } + }, builder: (context, state) { - return UniversalPlatform.isDesktop - ? const DesktopSignInScreen() - : const MobileSignInScreen(); + final isLoading = context.read().state.isSubmitting; + if (PlatformExtension.isMobile) { + return isLoading + ? const MobileLoadingScreen() + : const MobileSignInScreen(); + } + return const DesktopSignInScreen(); }, ), ); } - - void _showSignInError(BuildContext context, SignInState state) { - final successOrFail = state.successOrFail; - if (successOrFail != null) { - successOrFail.fold( - (userProfile) { - getIt().goHomeScreen(context, userProfile); - }, - (error) { - Log.error('Sign in error: $error'); - }, - ); - } - } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart deleted file mode 100644 index a7a1b9722d..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/anon_user_bloc.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SignInAnonymousButtonV3 extends StatelessWidget { - const SignInAnonymousButtonV3({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, signInState) { - return BlocProvider( - create: (context) => AnonUserBloc() - ..add( - const AnonUserEvent.initial(), - ), - child: BlocListener( - listener: (context, state) async { - if (state.openedAnonUser != null) { - await runAppFlowy(); - } - }, - child: BlocBuilder( - builder: (context, state) { - final text = LocaleKeys.signIn_continueWithLocalModel.tr(); - final onTap = state.anonUsers.isEmpty - ? () { - context - .read() - .add(const SignInEvent.signInAsGuest()); - } - : () { - final bloc = context.read(); - final user = bloc.state.anonUsers.first; - bloc.add(AnonUserEvent.openAnonUser(user)); - }; - return AFFilledTextButton.primary( - text: text, - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - }, - ), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart deleted file mode 100644 index 351527137f..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/anonymous_sign_in_button/anonymous_sign_in_button.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class AnonymousSignInButton extends StatelessWidget { - const AnonymousSignInButton({super.key}); - - @override - Widget build(BuildContext context) { - return AFGhostButton.normal( - onTap: () {}, - builder: (context, isHovering, disabled) { - return const Placeholder(); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart deleted file mode 100644 index c4cf504ef5..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; - -class ContinueWithEmail extends StatelessWidget { - const ContinueWithEmail({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AFFilledTextButton.primary( - text: LocaleKeys.signIn_continueWithEmail.tr(), - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart deleted file mode 100644 index 5027874418..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart'; -import 'package:appflowy_ui/appflowy_ui.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:string_validator/string_validator.dart'; - -class ContinueWithEmailAndPassword extends StatefulWidget { - const ContinueWithEmailAndPassword({super.key}); - - @override - State createState() => - _ContinueWithEmailAndPasswordState(); -} - -class _ContinueWithEmailAndPasswordState - extends State { - final controller = TextEditingController(); - final focusNode = FocusNode(); - final emailKey = GlobalKey(); - - bool _hasPushedContinueWithMagicLinkOrPasscodePage = false; - - @override - void dispose() { - controller.dispose(); - focusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - // only push the continue with magic link or passcode page if the magic link is sent successfully - if (successOrFail != null) { - successOrFail.fold( - (_) => emailKey.currentState?.clearError(), - (error) => emailKey.currentState?.syncError( - errorText: error.msg, - ), - ); - } else if (successOrFail == null && !state.isSubmitting) { - emailKey.currentState?.clearError(); - } - }, - child: Column( - children: [ - AFTextField( - key: emailKey, - controller: controller, - hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), - onSubmitted: (value) => _signInWithEmail( - context, - value, - ), - ), - VSpace(theme.spacing.l), - ContinueWithEmail( - onTap: () => _signInWithEmail( - context, - controller.text, - ), - ), - VSpace(theme.spacing.l), - ContinueWithPassword( - onTap: () { - final email = controller.text; - - if (!isEmail(email)) { - emailKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidEmail.tr(), - ); - return; - } - - _pushContinueWithPasswordPage( - context, - email, - ); - }, - ), - ], - ), - ); - } - - void _signInWithEmail(BuildContext context, String email) { - if (!isEmail(email)) { - emailKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidEmail.tr(), - ); - return; - } - - context - .read() - .add(SignInEvent.signInWithMagicLink(email: email)); - - _pushContinueWithMagicLinkOrPasscodePage( - context, - email, - ); - } - - void _pushContinueWithMagicLinkOrPasscodePage( - BuildContext context, - String email, - ) { - if (_hasPushedContinueWithMagicLinkOrPasscodePage) { - return; - } - - final signInBloc = context.read(); - - // push the a continue with magic link or passcode screen - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: signInBloc, - child: ContinueWithMagicLinkOrPasscodePage( - email: email, - backToLogin: () { - Navigator.pop(context); - - emailKey.currentState?.clearError(); - - _hasPushedContinueWithMagicLinkOrPasscodePage = false; - }, - onEnterPasscode: (passcode) { - signInBloc.add( - SignInEvent.signInWithPasscode( - email: email, - passcode: passcode, - ), - ); - }, - ), - ), - ), - ); - - _hasPushedContinueWithMagicLinkOrPasscodePage = true; - } - - void _pushContinueWithPasswordPage( - BuildContext context, - String email, - ) { - final signInBloc = context.read(); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => BlocProvider.value( - value: signInBloc, - child: ContinueWithPasswordPage( - email: email, - backToLogin: () { - emailKey.currentState?.clearError(); - Navigator.pop(context); - }, - onEnterPassword: (password) => signInBloc.add( - SignInEvent.signInWithEmailAndPassword( - email: email, - password: password, - ), - ), - onForgotPassword: () { - // todo: implement forgot password - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart deleted file mode 100644 index c29a18ea30..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_magic_link_or_passcode_page.dart +++ /dev/null @@ -1,270 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ContinueWithMagicLinkOrPasscodePage extends StatefulWidget { - const ContinueWithMagicLinkOrPasscodePage({ - super.key, - required this.backToLogin, - required this.email, - required this.onEnterPasscode, - }); - - final String email; - final VoidCallback backToLogin; - final ValueChanged onEnterPasscode; - - @override - State createState() => - _ContinueWithMagicLinkOrPasscodePageState(); -} - -class _ContinueWithMagicLinkOrPasscodePageState - extends State { - final passcodeController = TextEditingController(); - - bool isEnteringPasscode = false; - - ToastificationItem? toastificationItem; - - final inputPasscodeKey = GlobalKey(); - - bool isSubmitting = false; - - @override - void dispose() { - passcodeController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null && successOrFail.isFailure) { - successOrFail.onFailure((error) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_tokenHasExpiredOrInvalid.tr(), - ); - }); - } - - if (state.isSubmitting != isSubmitting) { - setState(() => isSubmitting = state.isSubmitting); - } - }, - child: Scaffold( - body: Center( - child: SizedBox( - width: 320, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo, title and description - ..._buildLogoTitleAndDescription(), - - // Enter code manually - ..._buildEnterCodeManually(), - - // Back to login - ..._buildBackToLogin(), - ], - ), - ), - ), - ), - ); - } - - List _buildEnterCodeManually() { - // todo: ask designer to provide the spacing - final spacing = VSpace(20); - final textStyle = AFButtonSize.l.buildTextStyle(context); - final textHeight = textStyle.height; - final textFontSize = textStyle.fontSize; - - // the indicator height is the height of the text style. - double indicatorHeight = 20; - if (textHeight != null && textFontSize != null) { - indicatorHeight = textHeight * textFontSize; - } - - if (!isEnteringPasscode) { - return [ - AFFilledTextButton.primary( - text: LocaleKeys.signIn_enterCodeManually.tr(), - onTap: () => setState(() => isEnteringPasscode = true), - size: AFButtonSize.l, - alignment: Alignment.center, - ), - spacing, - ]; - } - - return [ - // Enter code manually - AFTextField( - key: inputPasscodeKey, - controller: passcodeController, - hintText: LocaleKeys.signIn_enterCode.tr(), - keyboardType: TextInputType.number, - autoFocus: true, - onSubmitted: (passcode) { - if (passcode.isEmpty) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), - ); - } else { - widget.onEnterPasscode(passcode); - } - }, - ), - // todo: ask designer to provide the spacing - VSpace(12), - - // continue to login - !isSubmitting - ? _buildContinueButton(textStyle: textStyle) - : _buildIndicator(indicatorHeight: indicatorHeight), - - spacing, - ]; - } - - Widget _buildContinueButton({ - required TextStyle textStyle, - }) { - return AFFilledTextButton.primary( - text: LocaleKeys.signIn_continueToSignIn.tr(), - onTap: () { - final passcode = passcodeController.text; - if (passcode.isEmpty) { - inputPasscodeKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidVerificationCode.tr(), - ); - } else { - widget.onEnterPasscode(passcode); - } - }, - textStyle: textStyle.copyWith( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - size: AFButtonSize.l, - alignment: Alignment.center, - ); - } - - Widget _buildIndicator({ - required double indicatorHeight, - }) { - return AFFilledButton.disabled( - size: AFButtonSize.l, - builder: (context, isHovering, disabled) { - return Align( - child: SizedBox.square( - dimension: indicatorHeight, - child: CircularProgressIndicator( - strokeWidth: 3.0, - ), - ), - ); - }, - ); - } - - List _buildBackToLogin() { - return [ - AFGhostTextButton( - text: LocaleKeys.signIn_backToLogin.tr(), - size: AFButtonSize.s, - onTap: widget.backToLogin, - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ]; - } - - List _buildLogoTitleAndDescription() { - final theme = AppFlowyTheme.of(context); - final spacing = VSpace(theme.spacing.xxl); - if (!isEnteringPasscode) { - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_checkYourEmail.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // description - Text( - LocaleKeys.signIn_temporaryVerificationLinkSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } else { - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_enterCode.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // description - Text( - LocaleKeys.signIn_temporaryVerificationCodeSent.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - Text( - widget.email, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - textAlign: TextAlign.center, - ), - spacing, - ]; - } - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart deleted file mode 100644 index 5bfd191e22..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ContinueWithPassword extends StatelessWidget { - const ContinueWithPassword({ - super.key, - required this.onTap, - }); - - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AFOutlinedTextButton.normal( - text: 'Continue with password', - size: AFButtonSize.l, - alignment: Alignment.center, - onTap: onTap, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart deleted file mode 100644 index 1e2ed6e100..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_password_page.dart +++ /dev/null @@ -1,196 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class ContinueWithPasswordPage extends StatefulWidget { - const ContinueWithPasswordPage({ - super.key, - required this.backToLogin, - required this.email, - required this.onEnterPassword, - required this.onForgotPassword, - }); - - final String email; - final VoidCallback backToLogin; - final ValueChanged onEnterPassword; - final VoidCallback onForgotPassword; - - @override - State createState() => - _ContinueWithPasswordPageState(); -} - -class _ContinueWithPasswordPageState extends State { - final passwordController = TextEditingController(); - final inputPasswordKey = GlobalKey(); - - @override - void dispose() { - passwordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: SizedBox( - width: 320, - child: BlocListener( - listener: (context, state) { - final successOrFail = state.successOrFail; - if (successOrFail != null && successOrFail.isFailure) { - successOrFail.onFailure((error) { - inputPasswordKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), - ); - }); - } else if (state.passwordError != null) { - inputPasswordKey.currentState?.syncError( - errorText: LocaleKeys.signIn_invalidLoginCredentials.tr(), - ); - } else { - inputPasswordKey.currentState?.clearError(); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Logo and title - ..._buildLogoAndTitle(), - - // Password input and buttons - ..._buildPasswordSection(), - - // Back to login - ..._buildBackToLogin(), - ], - ), - ), - ), - ), - ); - } - - List _buildLogoAndTitle() { - final theme = AppFlowyTheme.of(context); - final spacing = VSpace(theme.spacing.xxl); - return [ - // logo - const AFLogo(), - spacing, - - // title - Text( - LocaleKeys.signIn_enterPassword.tr(), - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, - ), - ), - spacing, - - // email display - RichText( - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.signIn_loginAs.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: ' ${widget.email}', - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - ], - ), - ), - spacing, - ]; - } - - List _buildPasswordSection() { - final theme = AppFlowyTheme.of(context); - final iconSize = 20.0; - return [ - // Password input - AFTextField( - key: inputPasswordKey, - controller: passwordController, - hintText: LocaleKeys.signIn_enterPassword.tr(), - autoFocus: true, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - inputPasswordKey.currentState?.syncObscured(!isObscured); - }, - ), - onSubmitted: widget.onEnterPassword, - ), - // todo: ask designer to provide the spacing - VSpace(8), - - // Forgot password button - Align( - alignment: Alignment.centerLeft, - child: AFGhostTextButton( - text: LocaleKeys.signIn_forgotPassword.tr(), - size: AFButtonSize.s, - padding: EdgeInsets.zero, - onTap: widget.onForgotPassword, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ), - VSpace(20), - - // Continue button - AFFilledTextButton.primary( - text: LocaleKeys.web_continue.tr(), - onTap: () => widget.onEnterPassword(passwordController.text), - size: AFButtonSize.l, - alignment: Alignment.center, - ), - VSpace(20), - ]; - } - - List _buildBackToLogin() { - return [ - AFGhostTextButton( - text: LocaleKeys.signIn_backToLogin.tr(), - size: AFButtonSize.s, - onTap: widget.backToLogin, - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart deleted file mode 100644 index 8e126db7ad..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flutter/material.dart'; - -class AFLogo extends StatelessWidget { - const AFLogo({ - super.key, - this.size = const Size.square(36), - }); - - final Size size; - - @override - Widget build(BuildContext context) { - return FlowySvg( - FlowySvgs.app_logo_xl, - blendMode: null, - size: size, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart index 45e4fe7273..b6d5639ee0 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart @@ -1,13 +1,14 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:string_validator/string_validator.dart'; -import 'package:universal_platform/universal_platform.dart'; class SignInWithMagicLinkButtons extends StatefulWidget { const SignInWithMagicLinkButtons({super.key}); @@ -20,12 +21,10 @@ class SignInWithMagicLinkButtons extends StatefulWidget { class _SignInWithMagicLinkButtonsState extends State { final controller = TextEditingController(); - final FocusNode _focusNode = FocusNode(); @override void dispose() { controller.dispose(); - _focusNode.dispose(); super.dispose(); } @@ -35,23 +34,13 @@ class _SignInWithMagicLinkButtonsState crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - height: UniversalPlatform.isMobile ? 38.0 : 48.0, + height: 48.0, child: FlowyTextField( autoFocus: false, - focusNode: _focusNode, controller: controller, - borderRadius: BorderRadius.circular(4.0), hintText: LocaleKeys.signIn_pleaseInputYourEmail.tr(), - hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14.0, - color: Theme.of(context).hintColor, - ), - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14.0, - ), keyboardType: TextInputType.emailAddress, onSubmitted: (_) => _sendMagicLink(context, controller.text), - onTapOutside: (_) => _focusNode.unfocus(), ), ), const VSpace(12), @@ -64,21 +53,18 @@ class _SignInWithMagicLinkButtonsState void _sendMagicLink(BuildContext context, String email) { if (!isEmail(email)) { - showToastNotification( - message: LocaleKeys.signIn_invalidEmail.tr(), - type: ToastificationType.error, + return showSnackBarMessage( + context, + LocaleKeys.signIn_invalidEmail.tr(), + duration: const Duration(seconds: 8), ); - return; } - context - .read() - .add(SignInEvent.signInWithMagicLink(email: email)); - - showConfirmDialog( - context: context, - title: LocaleKeys.signIn_magicLinkSent.tr(), - description: LocaleKeys.signIn_magicLinkSentDescription.tr(), + context.read().add(SignInEvent.signedWithMagicLink(email)); + showSnackBarMessage( + context, + LocaleKeys.signIn_magicLinkSent.tr(), + duration: const Duration(seconds: 1000), ); } } @@ -98,17 +84,17 @@ class _ConfirmButton extends StatelessWidget { LoginType.signIn => LocaleKeys.signIn_signInWithMagicLink.tr(), LoginType.signUp => LocaleKeys.signIn_signUpWithMagicLink.tr(), }; - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return ElevatedButton( style: ElevatedButton.styleFrom( - minimumSize: const Size(double.infinity, 32), - maximumSize: const Size(double.infinity, 38), + minimumSize: const Size(double.infinity, 56), ), onPressed: onTap, child: FlowyText( name, fontSize: 14, color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, ), ); } else { @@ -121,7 +107,6 @@ class _ConfirmButton extends StatelessWidget { text: FlowyText.medium( name, textAlign: TextAlign.center, - color: Theme.of(context).colorScheme.onPrimary, ), radius: Corners.s6Border, ), diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart deleted file mode 100644 index 76ce87ffc1..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_agreement.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; - -class SignInAgreement extends StatelessWidget { - const SignInAgreement({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final textStyle = theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ); - final underlinedTextStyle = theme.textStyle.caption.underline( - color: theme.textColorScheme.secondary, - ); - return RichText( - textAlign: TextAlign.center, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.web_signInAgreement.tr(), - style: textStyle, - ), - TextSpan( - text: '${LocaleKeys.web_termOfUse.tr()} ', - style: underlinedTextStyle, - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.com/terms'), - ), - TextSpan( - text: '${LocaleKeys.web_and.tr()} ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.web_privacyPolicy.tr(), - style: underlinedTextStyle, - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer() - ..onTap = () => afLaunchUrlString('https://appflowy.com/privacy'), - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart index 33ef1d7bb0..e8d6bac536 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_anonymous_button.dart @@ -1,14 +1,91 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/anon_user_bloc.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +/// Used in DesktopSignInScreen and MobileSignInScreen +class SignInAnonymousButton extends StatelessWidget { + const SignInAnonymousButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final isMobile = PlatformExtension.isMobile; + + return BlocBuilder( + builder: (context, signInState) { + return BlocProvider( + create: (context) => AnonUserBloc() + ..add( + const AnonUserEvent.initial(), + ), + child: BlocListener( + listener: (context, state) async { + if (state.openedAnonUser != null) { + await runAppFlowy(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final text = state.anonUsers.isEmpty + ? LocaleKeys.signIn_loginStartWithAnonymous.tr() + : LocaleKeys.signIn_continueAnonymousUser.tr(); + final onTap = state.anonUsers.isEmpty + ? () { + context + .read() + .add(const SignInEvent.signedInAsGuest()); + } + : () { + final bloc = context.read(); + final user = bloc.state.anonUsers.first; + bloc.add(AnonUserEvent.openAnonUser(user)); + }; + // SignInAnonymousButton in mobile + if (isMobile) { + return ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 56), + ), + onPressed: onTap, + child: FlowyText( + LocaleKeys.signIn_loginStartWithAnonymous.tr(), + fontSize: 14, + color: Theme.of(context).colorScheme.onPrimary, + fontWeight: FontWeight.w500, + ), + ); + } + // SignInAnonymousButton in desktop + return SizedBox( + height: 48, + child: FlowyButton( + isSelected: true, + disable: signInState.isSubmitting, + text: FlowyText.medium( + text, + textAlign: TextAlign.center, + ), + radius: Corners.s6Border, + onTap: onTap, + ), + ); + }, + ), + ), + ); + }, + ); + } +} + class SignInAnonymousButtonV2 extends StatelessWidget { const SignInAnonymousButtonV2({ super.key, @@ -31,35 +108,30 @@ class SignInAnonymousButtonV2 extends StatelessWidget { }, child: BlocBuilder( builder: (context, state) { - final theme = AppFlowyTheme.of(context); + final text = state.anonUsers.isEmpty + ? LocaleKeys.signIn_loginStartWithAnonymous.tr() + : LocaleKeys.signIn_continueAnonymousUser.tr(); final onTap = state.anonUsers.isEmpty ? () { context .read() - .add(const SignInEvent.signInAsGuest()); + .add(const SignInEvent.signedInAsGuest()); } : () { final bloc = context.read(); final user = bloc.state.anonUsers.first; bloc.add(AnonUserEvent.openAnonUser(user)); }; - return AFGhostIconTextButton( - text: LocaleKeys.signIn_anonymousMode.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onTap, + child: FlowyText( + text, + color: Colors.blue, + fontSize: 12, + ), ), - size: AFButtonSize.s, - onTap: onTap, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.anonymous_mode_m, - color: theme.textColorScheme.secondary, - ); - }, ); }, ), @@ -69,39 +141,3 @@ class SignInAnonymousButtonV2 extends StatelessWidget { ); } } - -class ChangeCloudModeButton extends StatelessWidget { - const ChangeCloudModeButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFGhostIconTextButton( - text: LocaleKeys.signIn_switchToAppFlowyCloud.tr(), - textColor: (context, isHovering, disabled) { - return theme.textColorScheme.secondary; - }, - size: AFButtonSize.s, - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.m, - vertical: theme.spacing.xs, - ), - onTap: () async { - await useAppFlowyBetaCloudWithURL( - kAppflowyCloudUrl, - AuthenticatorType.appflowyCloud, - ); - await runAppFlowy(); - }, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.cloud_mode_m, - size: Size.square(20), - color: theme.textColorScheme.secondary, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart index 7067844500..e25fcf3a35 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart @@ -1,36 +1,64 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -class MobileLogoutButton extends StatelessWidget { - const MobileLogoutButton({ +class MobileSignInOrLogoutButton extends StatelessWidget { + const MobileSignInOrLogoutButton({ super.key, this.icon, - required this.text, - this.textColor, + required this.labelText, required this.onPressed, }); final FlowySvgData? icon; - final String text; - final Color? textColor; + final String labelText; final VoidCallback onPressed; @override Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: text, + final style = Theme.of(context); + return GestureDetector( onTap: onPressed, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - if (icon == null) { - return const SizedBox.shrink(); - } - return FlowySvg( - icon!, - size: Size.square(18), - ); - }, + child: Container( + height: 48, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(4), + ), + border: Border.all( + color: style.colorScheme.outline, + width: 0.5, + ), + ), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + SizedBox( + // The icon could be in different height as original aspect ratio, we use a fixed sizebox to wrap it to make sure they all occupy the same space. + width: 30, + height: 30, + child: Center( + child: SizedBox( + width: 24, + child: FlowySvg( + icon!, + blendMode: null, + ), + ), + ), + ), + const HSpace(8), + ], + FlowyText( + labelText, + fontSize: 14.0, + fontWeight: FontWeight.w500, + ), + ], + ), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart deleted file mode 100644 index 9a7234ab6b..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_button.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -enum ThirdPartySignInButtonType { - apple, - google, - github, - discord, - anonymous; - - String get provider { - switch (this) { - case ThirdPartySignInButtonType.apple: - return 'apple'; - case ThirdPartySignInButtonType.google: - return 'google'; - case ThirdPartySignInButtonType.github: - return 'github'; - case ThirdPartySignInButtonType.discord: - return 'discord'; - case ThirdPartySignInButtonType.anonymous: - throw UnsupportedError('Anonymous session does not have a provider'); - } - } - - FlowySvgData get icon { - switch (this) { - case ThirdPartySignInButtonType.apple: - return FlowySvgs.m_apple_icon_xl; - case ThirdPartySignInButtonType.google: - return FlowySvgs.m_google_icon_xl; - case ThirdPartySignInButtonType.github: - return FlowySvgs.m_github_icon_xl; - case ThirdPartySignInButtonType.discord: - return FlowySvgs.m_discord_icon_xl; - case ThirdPartySignInButtonType.anonymous: - return FlowySvgs.m_discord_icon_xl; - } - } - - String get labelText { - switch (this) { - case ThirdPartySignInButtonType.apple: - return LocaleKeys.signIn_signInWithApple.tr(); - case ThirdPartySignInButtonType.google: - return LocaleKeys.signIn_signInWithGoogle.tr(); - case ThirdPartySignInButtonType.github: - return LocaleKeys.signIn_signInWithGithub.tr(); - case ThirdPartySignInButtonType.discord: - return LocaleKeys.signIn_signInWithDiscord.tr(); - case ThirdPartySignInButtonType.anonymous: - return 'Anonymous session'; - } - } - - // https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple - Color backgroundColor(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - switch (this) { - case ThirdPartySignInButtonType.apple: - return isDarkMode ? Colors.white : Colors.black; - case ThirdPartySignInButtonType.google: - case ThirdPartySignInButtonType.github: - case ThirdPartySignInButtonType.discord: - case ThirdPartySignInButtonType.anonymous: - return isDarkMode ? Colors.black : Colors.grey.shade100; - } - } - - Color textColor(BuildContext context) { - final isDarkMode = Theme.of(context).brightness == Brightness.dark; - switch (this) { - case ThirdPartySignInButtonType.apple: - return isDarkMode ? Colors.black : Colors.white; - case ThirdPartySignInButtonType.google: - case ThirdPartySignInButtonType.github: - case ThirdPartySignInButtonType.discord: - case ThirdPartySignInButtonType.anonymous: - return isDarkMode ? Colors.white : Colors.black; - } - } - - BlendMode? get blendMode { - switch (this) { - case ThirdPartySignInButtonType.apple: - case ThirdPartySignInButtonType.github: - return BlendMode.srcIn; - default: - return null; - } - } -} - -class MobileThirdPartySignInButton extends StatelessWidget { - const MobileThirdPartySignInButton({ - super.key, - this.height = 38, - this.fontSize = 14.0, - required this.onTap, - required this.type, - }); - - final VoidCallback onTap; - final double height; - final double fontSize; - final ThirdPartySignInButtonType type; - - @override - Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: type.labelText, - onTap: onTap, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - type.icon, - size: Size.square(16), - blendMode: type.blendMode, - ); - }, - ); - } -} - -class DesktopThirdPartySignInButton extends StatelessWidget { - const DesktopThirdPartySignInButton({ - super.key, - required this.type, - required this.onTap, - }); - - final ThirdPartySignInButtonType type; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return AFOutlinedIconTextButton.normal( - text: type.labelText, - onTap: onTap, - size: AFButtonSize.l, - iconBuilder: (context, isHovering, disabled) { - return FlowySvg( - type.icon, - size: Size.square(18), - blendMode: type.blendMode, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart deleted file mode 100644 index 8d27846c46..0000000000 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_button/third_party_sign_in_buttons.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/user/application/sign_in_bloc.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import 'third_party_sign_in_button.dart'; - -typedef _SignInCallback = void Function(ThirdPartySignInButtonType signInType); - -@visibleForTesting -const Key signInWithGoogleButtonKey = Key('signInWithGoogleButton'); - -class ThirdPartySignInButtons extends StatelessWidget { - /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin - const ThirdPartySignInButtons({ - super.key, - this.expanded = false, - }); - - final bool expanded; - - @override - Widget build(BuildContext context) { - if (UniversalPlatform.isDesktopOrWeb) { - return _DesktopThirdPartySignIn( - onSignIn: (type) => _signIn(context, type.provider), - ); - } else { - return _MobileThirdPartySignIn( - isExpanded: expanded, - onSignIn: (type) => _signIn(context, type.provider), - ); - } - } - - void _signIn(BuildContext context, String provider) { - context.read().add( - SignInEvent.signInWithOAuth(platform: provider), - ); - } -} - -class _DesktopThirdPartySignIn extends StatefulWidget { - const _DesktopThirdPartySignIn({ - required this.onSignIn, - }); - - final _SignInCallback onSignIn; - - @override - State<_DesktopThirdPartySignIn> createState() => - _DesktopThirdPartySignInState(); -} - -class _DesktopThirdPartySignInState extends State<_DesktopThirdPartySignIn> { - bool isExpanded = false; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - children: [ - DesktopThirdPartySignInButton( - key: signInWithGoogleButtonKey, - type: ThirdPartySignInButtonType.google, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), - ), - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( - type: ThirdPartySignInButtonType.apple, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), - ), - ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), - ], - ); - } - - List _buildExpandedButtons() { - final theme = AppFlowyTheme.of(context); - return [ - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( - type: ThirdPartySignInButtonType.github, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), - ), - VSpace(theme.spacing.l), - DesktopThirdPartySignInButton( - type: ThirdPartySignInButtonType.discord, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), - ), - ]; - } - - List _buildCollapsedButtons() { - final theme = AppFlowyTheme.of(context); - return [ - VSpace(theme.spacing.l), - AFGhostTextButton( - text: 'More options', - padding: EdgeInsets.zero, - textColor: (context, isHovering, disabled) { - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, - ), - ]; - } -} - -class _MobileThirdPartySignIn extends StatefulWidget { - const _MobileThirdPartySignIn({ - required this.isExpanded, - required this.onSignIn, - }); - - final bool isExpanded; - final _SignInCallback onSignIn; - - @override - State<_MobileThirdPartySignIn> createState() => - _MobileThirdPartySignInState(); -} - -class _MobileThirdPartySignInState extends State<_MobileThirdPartySignIn> { - static const padding = 8.0; - - bool isExpanded = false; - - @override - void initState() { - super.initState(); - - isExpanded = widget.isExpanded; - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - // only display apple sign in button on iOS - if (Platform.isIOS) ...[ - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.apple, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.apple), - ), - const VSpace(padding), - ], - MobileThirdPartySignInButton( - key: signInWithGoogleButtonKey, - type: ThirdPartySignInButtonType.google, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.google), - ), - ...isExpanded ? _buildExpandedButtons() : _buildCollapsedButtons(), - ], - ); - } - - List _buildExpandedButtons() { - return [ - const VSpace(padding), - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.github, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.github), - ), - const VSpace(padding), - MobileThirdPartySignInButton( - type: ThirdPartySignInButtonType.discord, - onTap: () => widget.onSignIn(ThirdPartySignInButtonType.discord), - ), - ]; - } - - List _buildCollapsedButtons() { - final theme = AppFlowyTheme.of(context); - return [ - const VSpace(padding * 2), - AFGhostTextButton( - text: 'More options', - textColor: (context, isHovering, disabled) { - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.textColorScheme.theme; - }, - onTap: () { - setState(() { - isExpanded = !isExpanded; - }); - }, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart new file mode 100644 index 0000000000..b7fe53a8ea --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/presentation.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ThirdPartySignInButtons extends StatelessWidget { + /// Used in DesktopSignInScreen, MobileSignInScreen and SettingThirdPartyLogin + const ThirdPartySignInButtons({super.key}); + + @override + Widget build(BuildContext context) { + // Get themeMode from AppearanceSettingsCubit + // When user changes themeMode, it changes the state in AppearanceSettingsCubit, but the themeMode for the MaterialApp won't change, it only got updated(get value from AppearanceSettingsCubit) when user open the app again. Thus, we should get themeMode from AppearanceSettingsCubit rather than MediaQuery. + + final themeModeFromCubit = + context.watch().state.themeMode; + + final isDarkMode = themeModeFromCubit == ThemeMode.system + ? MediaQuery.of(context).platformBrightness == Brightness.dark + : themeModeFromCubit == ThemeMode.dark; + + return BlocBuilder( + builder: (context, state) { + final (googleText, githubText, discordText) = switch (state.loginType) { + LoginType.signIn => ( + LocaleKeys.signIn_signInWithGoogle.tr(), + LocaleKeys.signIn_signInWithGithub.tr(), + LocaleKeys.signIn_signInWithDiscord.tr() + ), + LoginType.signUp => ( + LocaleKeys.signIn_signUpWithGoogle.tr(), + LocaleKeys.signIn_signUpWithGithub.tr(), + LocaleKeys.signIn_signUpWithDiscord.tr() + ), + }; + return Column( + children: [ + _ThirdPartySignInButton( + key: const Key('signInWithGoogleButton'), + icon: FlowySvgs.google_mark_xl, + labelText: googleText, + onPressed: () { + _signInWithGoogle(context); + }, + ), + const VSpace(8), + _ThirdPartySignInButton( + icon: isDarkMode + ? FlowySvgs.github_mark_white_xl + : FlowySvgs.github_mark_black_xl, + labelText: githubText, + onPressed: () { + _signInWithGithub(context); + }, + ), + const VSpace(8), + _ThirdPartySignInButton( + icon: isDarkMode + ? FlowySvgs.discord_mark_white_xl + : FlowySvgs.discord_mark_blurple_xl, + labelText: discordText, + onPressed: () { + _signInWithDiscord(context); + }, + ), + ], + ); + }, + ); + } +} + +class _ThirdPartySignInButton extends StatelessWidget { + /// Build button based on current Platform(mobile or desktop). + const _ThirdPartySignInButton({ + super.key, + required this.icon, + required this.labelText, + required this.onPressed, + }); + + final FlowySvgData icon; + final String labelText; + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + if (PlatformExtension.isMobile) { + return MobileSignInOrLogoutButton( + icon: icon, + labelText: labelText, + onPressed: onPressed, + ); + } else { + return _DesktopSignInButton( + icon: icon, + labelText: labelText, + onPressed: onPressed, + ); + } + } +} + +class _DesktopSignInButton extends StatelessWidget { + const _DesktopSignInButton({ + required this.icon, + required this.labelText, + required this.onPressed, + }); + + final FlowySvgData icon; + final String labelText; + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final style = Theme.of(context); + // In desktop, the width of button is limited by [AuthFormContainer] + return SizedBox( + height: 48, + width: AuthFormContainer.width, + child: OutlinedButton.icon( + // In order to align all the labels vertically in a relatively centered position to the button, we use a fixed width container to wrap the icon(align to the right), then use another container to align the label to left. + icon: Container( + width: AuthFormContainer.width / 4, + alignment: Alignment.centerRight, + child: SizedBox( + // Some icons are not square, so we just use a fixed width here. + width: 24, + child: FlowySvg( + icon, + blendMode: null, + ), + ), + ), + label: Container( + padding: const EdgeInsets.only(left: 8), + alignment: Alignment.centerLeft, + child: FlowyText( + labelText, + fontSize: 14, + ), + ), + style: ButtonStyle( + overlayColor: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.hovered)) { + return style.colorScheme.onSecondaryContainer; + } + return null; + }, + ), + shape: WidgetStateProperty.all( + const RoundedRectangleBorder( + borderRadius: Corners.s6Border, + ), + ), + side: WidgetStateProperty.all( + BorderSide( + color: style.dividerColor, + ), + ), + ), + onPressed: onPressed, + ), + ); + } +} + +void _signInWithGoogle(BuildContext context) { + context.read().add( + const SignInEvent.signedInWithOAuth('google'), + ); +} + +void _signInWithGithub(BuildContext context) { + context.read().add(const SignInEvent.signedInWithOAuth('github')); +} + +void _signInWithDiscord(BuildContext context) { + context + .read() + .add(const SignInEvent.signedInWithOAuth('discord')); +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart index 6d79b896c1..974e2b5927 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/widgets.dart @@ -1,7 +1,5 @@ -export 'continue_with/continue_with_email_and_password.dart'; -export 'sign_in_agreement.dart'; +export 'magic_link_sign_in_buttons.dart'; export 'sign_in_anonymous_button.dart'; export 'sign_in_or_logout_button.dart'; -export 'third_party_sign_in_button/third_party_sign_in_button.dart'; -// export 'switch_sign_in_sign_up_button.dart'; -export 'third_party_sign_in_button/third_party_sign_in_buttons.dart'; +export 'switch_sign_in_sign_up_button.dart'; +export 'third_party_sign_in_buttons.dart'; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart new file mode 100644 index 0000000000..8aea8dde55 --- /dev/null +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_up_screen.dart @@ -0,0 +1,220 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/sign_up_bloc.dart'; +import 'package:appflowy/user/presentation/router.dart'; +import 'package:appflowy/user/presentation/widgets/widgets.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SignUpScreen extends StatelessWidget { + const SignUpScreen({ + super.key, + required this.router, + }); + + static const routeName = '/SignUpScreen'; + final AuthRouter router; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(), + child: BlocListener( + listener: (context, state) { + final successOrFail = state.successOrFail; + if (successOrFail != null) { + _handleSuccessOrFail(context, successOrFail); + } + }, + child: const Scaffold(body: SignUpForm()), + ), + ); + } + + void _handleSuccessOrFail( + BuildContext context, + FlowyResult result, + ) { + result.fold( + (user) => router.pushWorkspaceStartScreen(context, user), + (error) => showSnapBar(context, error.msg), + ); + } +} + +class SignUpForm extends StatelessWidget { + const SignUpForm({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Align( + child: AuthFormContainer( + children: [ + FlowyLogoTitle( + title: LocaleKeys.signUp_title.tr(), + logoSize: const Size(60, 60), + ), + const VSpace(30), + const EmailTextField(), + const VSpace(5), + const PasswordTextField(), + const VSpace(5), + const RepeatPasswordTextField(), + const VSpace(30), + const SignUpButton(), + const VSpace(10), + const SignUpPrompt(), + if (context.read().state.isSubmitting) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(), + ], + ], + ), + ); + } +} + +class SignUpPrompt extends StatelessWidget { + const SignUpPrompt({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText.medium( + LocaleKeys.signUp_alreadyHaveAnAccount.tr(), + color: Theme.of(context).hintColor, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + onPressed: () => Navigator.pop(context), + child: FlowyText.medium( + LocaleKeys.signIn_buttonText.tr(), + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ); + } +} + +class SignUpButton extends StatelessWidget { + const SignUpButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return RoundedTextButton( + title: LocaleKeys.signUp_getStartedText.tr(), + height: 48, + onPressed: () { + context + .read() + .add(const SignUpEvent.signUpWithUserEmailAndPassword()); + }, + ); + } +} + +class PasswordTextField extends StatelessWidget { + const PasswordTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.passwordError != current.passwordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: const FlowySvg(FlowySvgs.hide_m), + obscureHideIcon: const FlowySvg(FlowySvgs.show_m), + hintText: LocaleKeys.signUp_passwordHint.tr(), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.passwordError ?? '', + onChanged: (value) => context + .read() + .add(SignUpEvent.passwordChanged(value)), + ); + }, + ); + } +} + +class RepeatPasswordTextField extends StatelessWidget { + const RepeatPasswordTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.repeatPasswordError != current.repeatPasswordError, + builder: (context, state) { + return RoundedInputField( + obscureText: true, + obscureIcon: const FlowySvg(FlowySvgs.hide_m), + obscureHideIcon: const FlowySvg(FlowySvgs.show_m), + hintText: LocaleKeys.signUp_repeatPasswordHint.tr(), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.repeatPasswordError ?? '', + onChanged: (value) => context + .read() + .add(SignUpEvent.repeatPasswordChanged(value)), + ); + }, + ); + } +} + +class EmailTextField extends StatelessWidget { + const EmailTextField({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + buildWhen: (previous, current) => + previous.emailError != current.emailError, + builder: (context, state) { + return RoundedInputField( + hintText: LocaleKeys.signUp_emailHint.tr(), + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + normalBorderColor: Theme.of(context).colorScheme.outline, + errorBorderColor: Theme.of(context).colorScheme.error, + cursorColor: Theme.of(context).colorScheme.primary, + errorText: context.read().state.emailError ?? '', + onChanged: (value) => + context.read().add(SignUpEvent.emailChanged(value)), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart index ee089dfce0..3b3ad9707e 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/skip_log_in_screen.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -9,13 +11,13 @@ import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/widgets/widgets.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; class SkipLogInScreen extends StatefulWidget { const SkipLogInScreen({super.key}); @@ -45,7 +47,7 @@ class _SkipLogInScreenState extends State { const Spacer(), FlowyLogoTitle( title: LocaleKeys.welcomeText.tr(), - logoSize: Size.square(UniversalPlatform.isMobile ? 80 : 40), + logoSize: Size.square(PlatformExtension.isMobile ? 80 : 40), ), const VSpace(32), GoButton( @@ -101,7 +103,7 @@ class SkipLoginPageFooter extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (!UniversalPlatform.isMobile) const HSpace(placeholderWidth), + if (!PlatformExtension.isMobile) const HSpace(placeholderWidth), const Expanded(child: SubscribeButtons()), const SizedBox( width: placeholderWidth, diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 4062cedf8e..679464fa2d 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/startup/startup.dart'; @@ -8,10 +10,10 @@ import 'package:appflowy/user/presentation/helpers/helpers.dart'; import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:flutter/material.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; class SplashScreen extends StatelessWidget { /// Root Page of the app. @@ -60,21 +62,38 @@ class SplashScreen extends StatelessWidget { BuildContext context, Authenticated authenticated, ) async { - final result = await FolderEventGetCurrentWorkspaceSetting().send(); - result.fold( - (workspaceSetting) { - // After login, replace Splash screen by corresponding home screen - getIt().goHomeScreen( - context, - ); + final userProfile = authenticated.userProfile; + + /// After a user is authenticated, this function checks if encryption is required. + final result = await UserEventCheckEncryptionSign().send(); + await result.fold( + (check) async { + /// If encryption is needed, the user is navigated to the encryption screen. + /// Otherwise, it fetches the current workspace for the user and navigates them + if (check.requireSecret) { + getIt().pushEncryptionScreen(context, userProfile); + } else { + final result = await FolderEventGetCurrentWorkspaceSetting().send(); + result.fold( + (workspaceSetting) { + // After login, replace Splash screen by corresponding home screen + getIt().goHomeScreen( + context, + ); + }, + (error) => handleOpenWorkspaceError(context, error), + ); + } + }, + (err) { + Log.error(err); }, - (error) => handleOpenWorkspaceError(context, error), ); } void _handleUnauthenticated(BuildContext context, Unauthenticated result) { // replace Splash screen as root page - if (isAuthEnabled || UniversalPlatform.isMobile) { + if (isAuthEnabled || PlatformExtension.isMobile) { context.go(SignInScreen.routeName); } else { // if the env is not configured, we will skip to the 'skip login screen'. @@ -96,8 +115,8 @@ class Body extends StatelessWidget { Widget build(BuildContext context) { return Container( alignment: Alignment.center, - child: UniversalPlatform.isMobile - ? const FlowySvg(FlowySvgs.app_logo_xl, blendMode: null) + child: PlatformExtension.isMobile + ? const FlowySvg(FlowySvgs.flowy_logo_xl, blendMode: null) : const _DesktopSplashBody(), ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart index af6d4ad770..d79127e04c 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_error_screen.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -85,6 +86,7 @@ class WorkspaceErrorScreen extends StatelessWidget { const VSpace(50), const LogoutButton(), const VSpace(20), + const ResetWorkspaceButton(), ]); return Center( @@ -155,3 +157,43 @@ class LogoutButton extends StatelessWidget { ); } } + +class ResetWorkspaceButton extends StatelessWidget { + const ResetWorkspaceButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 200, + height: 40, + child: BlocBuilder( + builder: (context, state) { + final isLoading = state.loadingState?.isLoading() ?? false; + final icon = isLoading + ? const Center( + child: CircularProgressIndicator.adaptive(), + ) + : null; + + return FlowyButton( + text: FlowyText.medium( + LocaleKeys.workspace_reset.tr(), + textAlign: TextAlign.center, + ), + onTap: () { + NavigatorAlertDialog( + title: LocaleKeys.workspace_resetWorkspacePrompt.tr(), + confirm: () { + context.read().add( + const WorkspaceErrorEvent.resetWorkspace(), + ); + }, + ).show(context); + }, + rightIcon: icon, + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart index af5e7367e5..2be9ed6484 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/desktop_workspace_start_screen.dart @@ -1,10 +1,10 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/workspace/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.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:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -33,10 +33,9 @@ class DesktopWorkspaceStartScreen extends StatelessWidget { Widget _renderBody(WorkspaceState state) { final body = state.successOrFailure.fold( (_) => _renderList(state.workspaces), - (error) => Center( - child: AppFlowyErrorPage( - error: error, - ), + (error) => FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), ); return body; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index a6124da60b..b3c1f1cd0a 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -1,10 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; import 'package:appflowy/workspace/application/workspace/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -57,7 +57,7 @@ class _MobileWorkspaceStartScreenState children: [ const Spacer(), const FlowySvg( - FlowySvgs.app_logo_xl, + FlowySvgs.flowy_logo_xl, size: Size.square(64), blendMode: null, ), @@ -129,10 +129,9 @@ class _MobileWorkspaceStartScreenState ); }, (error) { - return Center( - child: AppFlowyErrorPage( - error: error, - ), + return FlowyErrorPage.message( + error.toString(), + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ); }, ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart index a8a9305539..a03b3bcf63 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/workspace_start_screen.dart @@ -3,9 +3,9 @@ import 'package:appflowy/user/presentation/screens/workspace_start_screen/deskto import 'package:appflowy/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart'; import 'package:appflowy/workspace/application/workspace/workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; // For future use class WorkspaceStartScreen extends StatelessWidget { @@ -24,7 +24,7 @@ class WorkspaceStartScreen extends StatelessWidget { ..add(const WorkspaceEvent.initial()), child: BlocBuilder( builder: (context, state) { - if (UniversalPlatform.isMobile) { + if (PlatformExtension.isMobile) { return MobileWorkspaceStartScreen( workspaceState: state, ); diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart index c0b8e7e5ae..9927ee2457 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/auth_form_container.dart @@ -1,23 +1,24 @@ import 'package:flutter/material.dart'; class AuthFormContainer extends StatelessWidget { - const AuthFormContainer({ - super.key, - required this.children, - }); + const AuthFormContainer({super.key, required this.children}); final List children; - static const double width = 320; + static const double width = 340; @override Widget build(BuildContext context) { return SizedBox( width: width, - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: children, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart index 14b1c896a9..c2a13eac82 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/widgets/flowy_logo_title.dart @@ -1,7 +1,8 @@ -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/logo/logo.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; class FlowyLogoTitle extends StatelessWidget { const FlowyLogoTitle({ @@ -15,20 +16,25 @@ class FlowyLogoTitle extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return SizedBox( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - AFLogo(size: logoSize), - const VSpace(20), - Text( - title, - style: theme.textStyle.heading3.enhanced( - color: theme.textColorScheme.primary, + SizedBox.fromSize( + size: logoSize, + child: const FlowySvg( + FlowySvgs.flowy_logo_xl, + blendMode: null, ), ), + const VSpace(20), + FlowyText.regular( + title, + fontSize: FontSizes.s24, + fontFamily: + GoogleFonts.poppins(fontWeight: FontWeight.w500).fontFamily, + color: Theme.of(context).colorScheme.tertiary, + ), ], ), ); diff --git a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart index f1bf9262a0..6777beb0e1 100644 --- a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart +++ b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart @@ -1,28 +1,9 @@ import 'package:flutter/material.dart'; -// the color set generated from AI -final _builtInColorSet = [ - (const Color(0xFF8A2BE2), const Color(0xFFF0E6FF)), - (const Color(0xFF2E8B57), const Color(0xFFE0FFF0)), - (const Color(0xFF1E90FF), const Color(0xFFE6F3FF)), - (const Color(0xFFFF7F50), const Color(0xFFFFF0E6)), - (const Color(0xFFFF69B4), const Color(0xFFFFE6F0)), - (const Color(0xFF20B2AA), const Color(0xFFE0FFFF)), - (const Color(0xFFDC143C), const Color(0xFFFFE6E6)), - (const Color(0xFF8B4513), const Color(0xFFFFF0E6)), -]; - extension type ColorGenerator(String value) { Color toColor() { final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit); final double hue = (hash % 360).toDouble(); return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor(); } - - // shuffle a color from the built-in color set, for the same name, the result should be the same - (Color, Color) randomColor() { - final hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit); - final index = hash % _builtInColorSet.length; - return _builtInColorSet[index]; - } } diff --git a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart index 61694367bb..34925235cb 100644 --- a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart +++ b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart @@ -5,17 +5,12 @@ import 'package:flutter/material.dart'; extension ColorExtension on Color { /// return a hex string in 0xff000000 format String toHexString() { - final alpha = (a * 255).toInt().toRadixString(16).padLeft(2, '0'); - final red = (r * 255).toInt().toRadixString(16).padLeft(2, '0'); - final green = (g * 255).toInt().toRadixString(16).padLeft(2, '0'); - final blue = (b * 255).toInt().toRadixString(16).padLeft(2, '0'); - - return '0x$alpha$red$green$blue'.toLowerCase(); + return '0x${value.toRadixString(16).padLeft(8, '0')}'; } /// return a random color static Color random({double opacity = 1.0}) { return Color((math.Random().nextDouble() * 0xFFFFFF).toInt()) - .withValues(alpha: opacity); + .withOpacity(opacity); } } diff --git a/frontend/appflowy_flutter/lib/util/default_extensions.dart b/frontend/appflowy_flutter/lib/util/default_extensions.dart deleted file mode 100644 index 603a66d6cf..0000000000 --- a/frontend/appflowy_flutter/lib/util/default_extensions.dart +++ /dev/null @@ -1,23 +0,0 @@ -/// List of default file extensions used for images. -/// -/// This is used to make sure that only images that are allowed are picked/uploaded. The extensions -/// should be supported by Flutter, to avoid causing issues. -/// -/// See [Image-class documentation](https://api.flutter.dev/flutter/widgets/Image-class.html) -/// -const List defaultImageExtensions = [ - 'jpg', - 'png', - 'jpeg', - 'gif', - 'webp', - 'bmp', -]; - -bool isNotImageUrl(String url) { - final nonImageSuffixRegex = RegExp( - r'(\.(io|html|php|json|txt|js|css|xml|md|log)(\?.*)?(#.*)?$)|/$', - caseSensitive: false, - ); - return nonImageSuffixRegex.hasMatch(url); -} diff --git a/frontend/appflowy_flutter/lib/util/expand_views.dart b/frontend/appflowy_flutter/lib/util/expand_views.dart deleted file mode 100644 index 115c0d2d29..0000000000 --- a/frontend/appflowy_flutter/lib/util/expand_views.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -class ViewExpanderRegistry { - /// the key is view id - final Map> _viewExpanders = {}; - - bool isViewExpanded(String id) => getExpander(id)?.isViewExpanded ?? false; - - void register(String id, ViewExpander expander) { - final expanders = _viewExpanders[id] ?? {}; - expanders.add(expander); - _viewExpanders[id] = expanders; - } - - void unregister(String id, ViewExpander expander) { - final expanders = _viewExpanders[id] ?? {}; - expanders.remove(expander); - if (expanders.isEmpty) { - _viewExpanders.remove(id); - } else { - _viewExpanders[id] = expanders; - } - } - - ViewExpander? getExpander(String id) { - final expanders = _viewExpanders[id] ?? {}; - return expanders.isEmpty ? null : expanders.first; - } -} - -class ViewExpander { - ViewExpander(this._isExpandedCallback, this._expandCallback); - - final ValueGetter _isExpandedCallback; - final VoidCallback _expandCallback; - - bool get isViewExpanded => _isExpandedCallback.call(); - - void expand() => _expandCallback.call(); -} diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index 4ef11bf5c6..f36c2fe264 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -4,7 +4,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:protobuf/protobuf.dart'; extension FieldTypeExtension on FieldType { String get i18n => switch (this) { @@ -25,7 +24,6 @@ extension FieldTypeExtension on FieldType { FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(), FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(), FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(), - FieldType.Media => LocaleKeys.grid_field_mediaFieldName.tr(), _ => throw UnimplementedError(), }; @@ -38,13 +36,12 @@ extension FieldTypeExtension on FieldType { FieldType.Checkbox => FlowySvgs.checkbox_s, FieldType.URL => FlowySvgs.url_s, FieldType.Checklist => FlowySvgs.checklist_s, - FieldType.LastEditedTime => FlowySvgs.time_s, - FieldType.CreatedTime => FlowySvgs.time_s, + FieldType.LastEditedTime => FlowySvgs.last_modified_s, + FieldType.CreatedTime => FlowySvgs.created_at_s, FieldType.Relation => FlowySvgs.relation_s, FieldType.Summary => FlowySvgs.ai_summary_s, FieldType.Time => FlowySvgs.timer_start_s, FieldType.Translate => FlowySvgs.ai_translate_s, - FieldType.Media => FlowySvgs.media_s, _ => throw UnimplementedError(), }; @@ -69,7 +66,6 @@ extension FieldTypeExtension on FieldType { FieldType.Summary => const Color(0xFFBECCFF), FieldType.Time => const Color(0xFFFDEDA7), FieldType.Translate => const Color(0xFFBECCFF), - FieldType.Media => const Color(0xFF91EBF5), _ => throw UnimplementedError(), }; @@ -89,78 +85,6 @@ extension FieldTypeExtension on FieldType { FieldType.Summary => const Color(0xFF6859A7), FieldType.Time => const Color(0xFFFDEDA7), FieldType.Translate => const Color(0xFF6859A7), - FieldType.Media => const Color(0xFF91EBF5), _ => throw UnimplementedError(), }; - - bool get canBeGroup => switch (this) { - FieldType.URL || - FieldType.Checkbox || - FieldType.MultiSelect || - FieldType.SingleSelect || - FieldType.DateTime => - true, - _ => false - }; - - bool get canCreateFilter => switch (this) { - FieldType.Number || - FieldType.Checkbox || - FieldType.MultiSelect || - FieldType.RichText || - FieldType.SingleSelect || - FieldType.Checklist || - FieldType.URL || - FieldType.DateTime || - FieldType.CreatedTime || - FieldType.LastEditedTime => - true, - _ => false - }; - - bool get canCreateSort => switch (this) { - FieldType.RichText || - FieldType.Checkbox || - FieldType.Number || - FieldType.DateTime || - FieldType.SingleSelect || - FieldType.MultiSelect || - FieldType.LastEditedTime || - FieldType.CreatedTime || - FieldType.Checklist || - FieldType.URL || - FieldType.Time => - true, - _ => false - }; - - bool get canEditHeader => switch (this) { - FieldType.MultiSelect => true, - FieldType.SingleSelect => true, - _ => false, - }; - - bool get canCreateNewGroup => switch (this) { - FieldType.MultiSelect => true, - FieldType.SingleSelect => true, - _ => false, - }; - - bool get canDeleteGroup => switch (this) { - FieldType.URL || - FieldType.SingleSelect || - FieldType.MultiSelect || - FieldType.DateTime => - true, - _ => false, - }; - - List get groupConditions { - switch (this) { - case FieldType.DateTime: - return DateConditionPB.values; - default: - return []; - } - } } diff --git a/frontend/appflowy_flutter/lib/util/navigator_context_extension.dart b/frontend/appflowy_flutter/lib/util/navigator_context_extension.dart deleted file mode 100644 index b5cfeb64fe..0000000000 --- a/frontend/appflowy_flutter/lib/util/navigator_context_extension.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter/material.dart'; - -extension NavigatorContext on BuildContext { - void popToHome() { - Navigator.of(this).popUntil((route) { - if (route.settings.name == '/') { - return true; - } - return false; - }); - } -} diff --git a/frontend/appflowy_flutter/lib/util/share_log_files.dart b/frontend/appflowy_flutter/lib/util/share_log_files.dart index b8dd390627..9838eb4a40 100644 --- a/frontend/appflowy_flutter/lib/util/share_log_files.dart +++ b/frontend/appflowy_flutter/lib/util/share_log_files.dart @@ -1,8 +1,7 @@ import 'dart:io'; -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:archive/archive_io.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -24,9 +23,9 @@ Future shareLogFiles(BuildContext? context) async { if (archiveLogFiles.isEmpty) { if (context != null && context.mounted) { - showToastNotification( - message: LocaleKeys.noLogFiles.tr(), - type: ToastificationType.error, + showSnackBarMessage( + context, + LocaleKeys.noLogFiles.tr(), ); } return; @@ -39,40 +38,20 @@ Future shareLogFiles(BuildContext? context) async { final zip = zipEncoder.encode(archive); if (zip == null) { - if (context != null && context.mounted) { - showToastNotification( - message: LocaleKeys.noLogFiles.tr(), - type: ToastificationType.error, - ); - } return; } // create a zipped appflowy logs file - try { - final tempDirectory = await getTemporaryDirectory(); - final path = Platform.isAndroid ? tempDirectory.path : dir.path; - final zipFile = - await File(p.join(path, 'appflowy_logs.zip')).writeAsBytes(zip); + final path = Platform.isAndroid ? '/storage/emulated/0/Download' : dir.path; + final zipFile = + await File(p.join(path, 'appflowy_logs.zip')).writeAsBytes(zip); - if (Platform.isIOS) { - await Share.shareUri(zipFile.uri); - // delete the zipped appflowy logs file - await zipFile.delete(); - } else if (Platform.isAndroid) { - await Share.shareXFiles([XFile(zipFile.path)]); - // delete the zipped appflowy logs file - await zipFile.delete(); - } else { - // open the directory - await afLaunchUri(zipFile.uri); - } - } catch (e) { - if (context != null && context.mounted) { - showToastNotification( - message: e.toString(), - type: ToastificationType.error, - ); - } + if (Platform.isIOS) { + await Share.shareUri(zipFile.uri); + } else { + await Share.shareXFiles([XFile(zipFile.path)]); } + + // delete the zipped appflowy logs file + await zipFile.delete(); } diff --git a/frontend/appflowy_flutter/lib/util/string_extension.dart b/frontend/appflowy_flutter/lib/util/string_extension.dart index 84dba35e1a..cadd27fe80 100644 --- a/frontend/appflowy_flutter/lib/util/string_extension.dart +++ b/frontend/appflowy_flutter/lib/util/string_extension.dart @@ -1,12 +1,9 @@ import 'dart:io'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/shared/patterns/common_patterns.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide Icon; +import 'package:flutter/material.dart'; extension StringExtension on String { static const _specialCharacters = r'\/:*?"<>| '; @@ -48,47 +45,4 @@ extension StringExtension on String { // if it fails, try to parse the color as a hex string return FlowyTint.fromId(this)?.color(context) ?? tryToColor(); } - - String orDefault(String defaultValue) { - return isEmpty ? defaultValue : this; - } -} - -extension NullableStringExtension on String? { - String orDefault(String defaultValue) { - return this?.isEmpty ?? true ? defaultValue : this ?? ''; - } -} - -extension IconExtension on String { - Icon? get icon { - final values = split('/'); - if (values.length != 2) { - return null; - } - final iconGroup = IconGroup(name: values.first, icons: []); - if (kDebugMode) { - // Ensure the icon group and icon exist - assert(kIconGroups!.any((group) => group.name == values.first)); - assert( - kIconGroups! - .firstWhere((group) => group.name == values.first) - .icons - .any((icon) => icon.name == values.last), - ); - } - return Icon( - content: values.last, - name: values.last, - keywords: [], - )..iconGroup = iconGroup; - } -} - -extension CounterExtension on String { - Counters getCounter() { - final wordCount = wordRegex.allMatches(this).length; - final charCount = runes.length; - return Counters(wordCount: wordCount, charCount: charCount); - } } diff --git a/frontend/appflowy_flutter/lib/util/throttle.dart b/frontend/appflowy_flutter/lib/util/throttle.dart index 0aaa9f2d3a..c8c6dcf0ca 100644 --- a/frontend/appflowy_flutter/lib/util/throttle.dart +++ b/frontend/appflowy_flutter/lib/util/throttle.dart @@ -16,10 +16,6 @@ class Throttler { }); } - void cancel() { - _timer?.cancel(); - } - void dispose() { _timer?.cancel(); _timer = null; diff --git a/frontend/appflowy_flutter/lib/util/xfile_ext.dart b/frontend/appflowy_flutter/lib/util/xfile_ext.dart deleted file mode 100644 index 593ea337c1..0000000000 --- a/frontend/appflowy_flutter/lib/util/xfile_ext.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:appflowy/shared/patterns/file_type_patterns.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pbenum.dart'; -import 'package:cross_file/cross_file.dart'; - -enum FileType { - other, - image, - link, - document, - archive, - video, - audio, - text; -} - -extension TypeRecognizer on XFile { - FileType get fileType { - // Prefer mime over using regexp as it is more reliable. - // Refer to Microsoft Documentation for common mime types: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types - if (mimeType?.isNotEmpty == true) { - if (mimeType!.contains('image')) { - return FileType.image; - } - if (mimeType!.contains('video')) { - return FileType.video; - } - if (mimeType!.contains('audio')) { - return FileType.audio; - } - if (mimeType!.contains('text')) { - return FileType.text; - } - if (mimeType!.contains('application')) { - if (mimeType!.contains('pdf') || - mimeType!.contains('doc') || - mimeType!.contains('docx')) { - return FileType.document; - } - if (mimeType!.contains('zip') || - mimeType!.contains('tar') || - mimeType!.contains('gz') || - mimeType!.contains('7z') || - // archive is used in eg. Java archives (jar) - mimeType!.contains('archive') || - mimeType!.contains('rar')) { - return FileType.archive; - } - if (mimeType!.contains('rtf')) { - return FileType.text; - } - } - - return FileType.other; - } - - // Check if the file is an image - if (imgExtensionRegex.hasMatch(path)) { - return FileType.image; - } - - // Check if the file is a video - if (videoExtensionRegex.hasMatch(path)) { - return FileType.video; - } - - // Check if the file is an audio - if (audioExtensionRegex.hasMatch(path)) { - return FileType.audio; - } - - // Check if the file is a document - if (documentExtensionRegex.hasMatch(path)) { - return FileType.document; - } - - // Check if the file is an archive - if (archiveExtensionRegex.hasMatch(path)) { - return FileType.archive; - } - - // Check if the file is a text - if (textExtensionRegex.hasMatch(path)) { - return FileType.text; - } - - return FileType.other; - } -} - -extension ToMediaFileTypePB on FileType { - MediaFileTypePB toMediaFileTypePB() { - switch (this) { - case FileType.image: - return MediaFileTypePB.Image; - case FileType.video: - return MediaFileTypePB.Video; - case FileType.audio: - return MediaFileTypePB.Audio; - case FileType.document: - return MediaFileTypePB.Document; - case FileType.archive: - return MediaFileTypePB.Archive; - case FileType.text: - return MediaFileTypePB.Text; - default: - return MediaFileTypePB.Other; - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart index 52663ea219..ee68ea7c0d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/action_navigation/navigation_action.dart @@ -7,7 +7,6 @@ enum ActionType { class ActionArgumentKeys { static String view = "view"; static String nodePath = "node_path"; - static String blockId = "block_id"; static String rowId = "row_id"; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart index c3190a8e40..d900afd6eb 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/appearance_defaults.dart @@ -1,6 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/theme.dart'; -import 'package:flutter/material.dart'; /// A class for the default appearance settings for the app class DefaultAppearanceSettings { @@ -14,6 +15,6 @@ class DefaultAppearanceSettings { } static Color getDefaultSelectionColor(BuildContext context) { - return Theme.of(context).colorScheme.primary.withValues(alpha: 0.2); + return Theme.of(context).colorScheme.primary.withOpacity(0.2); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart index 01f638fe7a..8064f7038c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/command_palette_bloc.dart @@ -1,348 +1,199 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; + import 'package:appflowy/plugins/trash/application/trash_listener.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/workspace/application/command_palette/search_listener.dart'; import 'package:appflowy/workspace/application/command_palette/search_service.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:bloc/bloc.dart'; -import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'command_palette_bloc.freezed.dart'; -class Debouncer { - Debouncer({required this.delay}); - - final Duration delay; - Timer? _timer; - - void run(void Function() action) { - _timer?.cancel(); - _timer = Timer(delay, action); - } - - void dispose() { - _timer?.cancel(); - } -} +const _searchChannel = 'CommandPalette'; class CommandPaletteBloc extends Bloc { CommandPaletteBloc() : super(CommandPaletteState.initial()) { - on<_SearchChanged>(_onSearchChanged); - on<_PerformSearch>(_onPerformSearch); - on<_NewSearchStream>(_onNewSearchStream); - on<_ResultsChanged>(_onResultsChanged); - on<_TrashChanged>(_onTrashChanged); - on<_WorkspaceChanged>(_onWorkspaceChanged); - on<_ClearSearch>(_onClearSearch); + _searchListener.start( + onResultsChanged: _onResultsChanged, + ); _initTrash(); + _dispatch(); } - final Debouncer _searchDebouncer = Debouncer( - delay: const Duration(milliseconds: 300), - ); + Timer? _debounceOnChanged; final TrashService _trashService = TrashService(); + final SearchListener _searchListener = SearchListener( + channel: _searchChannel, + ); final TrashListener _trashListener = TrashListener(); - String? _activeQuery; + String? _oldQuery; String? _workspaceId; + int _messagesReceived = 0; @override Future close() { _trashListener.close(); - _searchDebouncer.dispose(); - state.searchResponseStream?.dispose(); + _searchListener.stop(); return super.close(); } + void _dispatch() { + on((event, emit) async { + event.when( + searchChanged: _debounceOnSearchChanged, + trashChanged: (trash) async { + if (trash != null) { + return emit(state.copyWith(trash: trash)); + } + + final trashOrFailure = await _trashService.readTrash(); + final trashRes = trashOrFailure.fold( + (trash) => trash, + (error) => null, + ); + + if (trashRes != null) { + emit(state.copyWith(trash: trashRes.items)); + } + }, + performSearch: (search) async { + if (search.isNotEmpty && search != state.query) { + _oldQuery = state.query; + emit(state.copyWith(query: search, isLoading: true)); + await SearchBackendService.performSearch( + search, + workspaceId: _workspaceId, + channel: _searchChannel, + ); + } else { + emit(state.copyWith(query: null, isLoading: false, results: [])); + } + }, + resultsChanged: (results, max) { + if (state.query != _oldQuery) { + emit(state.copyWith(results: [])); + _oldQuery = state.query; + _messagesReceived = 0; + } + + _messagesReceived++; + + final searchResults = _filterDuplicates(results.items); + searchResults.sort((a, b) => b.score.compareTo(a.score)); + + emit( + state.copyWith( + results: searchResults, + isLoading: _messagesReceived != max, + ), + ); + }, + workspaceChanged: (workspaceId) { + _workspaceId = workspaceId; + emit(state.copyWith(results: [], query: '', isLoading: false)); + }, + clearSearch: () { + emit(state.copyWith(results: [], query: '', isLoading: false)); + }, + ); + }); + } + Future _initTrash() async { _trashListener.start( - trashUpdated: (trashOrFailed) => add( - CommandPaletteEvent.trashChanged( - trash: trashOrFailed.toNullable(), - ), - ), + trashUpdated: (trashOrFailed) { + final trash = trashOrFailed.toNullable(); + add(CommandPaletteEvent.trashChanged(trash: trash)); + }, ); final trashOrFailure = await _trashService.readTrash(); - trashOrFailure.fold( - (trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)), - (error) => debugPrint('Failed to load trash: $error'), + final trash = trashOrFailure.toNullable(); + + add(CommandPaletteEvent.trashChanged(trash: trash?.items)); + } + + void _debounceOnSearchChanged(String value) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer( + const Duration(milliseconds: 300), + () => _performSearch(value), ); } - FutureOr _onSearchChanged( - _SearchChanged event, - Emitter emit, - ) { - _searchDebouncer.run( - () { - if (!isClosed) { - add(CommandPaletteEvent.performSearch(search: event.search)); - } - }, - ); - } + List _filterDuplicates(List results) { + final currentItems = [...state.results]; + final res = [...results]; - FutureOr _onPerformSearch( - _PerformSearch event, - Emitter emit, - ) async { - if (event.search.isEmpty && event.search != state.query) { - emit( - state.copyWith( - query: null, - searching: false, - serverResponseItems: [], - localResponseItems: [], - combinedResponseItems: {}, - resultSummaries: [], - generatingAIOverview: false, - ), - ); - } else { - emit(state.copyWith(query: event.search, searching: true)); - _activeQuery = event.search; + for (final item in results) { + if (item.data.trim().isEmpty) { + continue; + } - unawaited( - SearchBackendService.performSearch( - event.search, - workspaceId: _workspaceId, - ).then( - (result) => result.fold( - (stream) { - if (!isClosed && _activeQuery == event.search) { - add(CommandPaletteEvent.newSearchStream(stream: stream)); - } - }, - (error) { - debugPrint('Search error: $error'); - if (!isClosed) { - add( - CommandPaletteEvent.resultsChanged( - searchId: '', - searching: false, - generatingAIOverview: false, - ), - ); - } - }, - ), - ), - ); - } - } + final duplicateIndex = currentItems.indexWhere((a) => a.id == item.id); + if (duplicateIndex == -1) { + continue; + } - FutureOr _onNewSearchStream( - _NewSearchStream event, - Emitter emit, - ) { - state.searchResponseStream?.dispose(); - emit( - state.copyWith( - searchId: event.stream.searchId, - searchResponseStream: event.stream, - ), - ); - - event.stream.listen( - onLocalItems: (items, searchId) => _handleResultsUpdate( - searchId: searchId, - localItems: items, - ), - onServerItems: (items, searchId, searching, generatingAIOverview) => - _handleResultsUpdate( - searchId: searchId, - serverItems: items, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - onSummaries: (summaries, searchId, searching, generatingAIOverview) => - _handleResultsUpdate( - searchId: searchId, - summaries: summaries, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - onFinished: (searchId) => _handleResultsUpdate( - searchId: searchId, - searching: false, - ), - ); - } - - void _handleResultsUpdate({ - required String searchId, - List? serverItems, - List? localItems, - List? summaries, - bool searching = true, - bool generatingAIOverview = false, - }) { - if (_isActiveSearch(searchId)) { - add( - CommandPaletteEvent.resultsChanged( - searchId: searchId, - serverItems: serverItems, - localItems: localItems, - summaries: summaries, - searching: searching, - generatingAIOverview: generatingAIOverview, - ), - ); - } - } - - FutureOr _onResultsChanged( - _ResultsChanged event, - Emitter emit, - ) async { - if (state.searchId != event.searchId) return; - - final combinedItems = {}; - for (final item in event.serverItems ?? state.serverResponseItems) { - combinedItems[item.id] = SearchResultItem( - id: item.id, - icon: item.icon, - displayName: item.displayName, - content: item.content, - workspaceId: item.workspaceId, - ); + final duplicate = currentItems[duplicateIndex]; + if (item.score < duplicate.score) { + res.remove(item); + } else { + currentItems.remove(duplicate); + } } - for (final item in event.localItems ?? state.localResponseItems) { - combinedItems.putIfAbsent( - item.id, - () => SearchResultItem( - id: item.id, - icon: item.icon, - displayName: item.displayName, - content: '', - workspaceId: item.workspaceId, - ), - ); - } - - emit( - state.copyWith( - serverResponseItems: event.serverItems ?? state.serverResponseItems, - localResponseItems: event.localItems ?? state.localResponseItems, - resultSummaries: event.summaries ?? state.resultSummaries, - combinedResponseItems: combinedItems, - searching: event.searching, - generatingAIOverview: event.generatingAIOverview, - ), - ); + return res..addAll(currentItems); } - FutureOr _onTrashChanged( - _TrashChanged event, - Emitter emit, - ) async { - if (event.trash != null) { - emit(state.copyWith(trash: event.trash!)); - } else { - final trashOrFailure = await _trashService.readTrash(); - trashOrFailure.fold((trash) { - emit(state.copyWith(trash: trash.items)); - }, (error) { - // Optionally handle error; otherwise, we simply do nothing. - }); - } - } + void _performSearch(String value) => + add(CommandPaletteEvent.performSearch(search: value)); - FutureOr _onWorkspaceChanged( - _WorkspaceChanged event, - Emitter emit, - ) { - _workspaceId = event.workspaceId; - emit( - state.copyWith( - query: '', - serverResponseItems: [], - localResponseItems: [], - combinedResponseItems: {}, - resultSummaries: [], - searching: false, - generatingAIOverview: false, - ), - ); - } - - FutureOr _onClearSearch( - _ClearSearch event, - Emitter emit, - ) { - emit(CommandPaletteState.initial().copyWith(trash: state.trash)); - } - - bool _isActiveSearch(String searchId) => - !isClosed && state.searchId == searchId; + void _onResultsChanged(RepeatedSearchResultPB results) => + add(CommandPaletteEvent.resultsChanged(results: results)); } @freezed class CommandPaletteEvent with _$CommandPaletteEvent { const factory CommandPaletteEvent.searchChanged({required String search}) = _SearchChanged; + const factory CommandPaletteEvent.performSearch({required String search}) = _PerformSearch; - const factory CommandPaletteEvent.newSearchStream({ - required SearchResponseStream stream, - }) = _NewSearchStream; + const factory CommandPaletteEvent.resultsChanged({ - required String searchId, - required bool searching, - required bool generatingAIOverview, - List? serverItems, - List? localItems, - List? summaries, + required RepeatedSearchResultPB results, + @Default(1) int max, }) = _ResultsChanged; const factory CommandPaletteEvent.trashChanged({ @Default(null) List? trash, }) = _TrashChanged; + const factory CommandPaletteEvent.workspaceChanged({ @Default(null) String? workspaceId, }) = _WorkspaceChanged; + const factory CommandPaletteEvent.clearSearch() = _ClearSearch; } -class SearchResultItem { - const SearchResultItem({ - required this.id, - required this.icon, - required this.content, - required this.displayName, - this.workspaceId, - }); - - final String id; - final String content; - final ResultIconPB icon; - final String displayName; - final String? workspaceId; -} - @freezed class CommandPaletteState with _$CommandPaletteState { const CommandPaletteState._(); + const factory CommandPaletteState({ @Default(null) String? query, - @Default([]) List serverResponseItems, - @Default([]) List localResponseItems, - @Default({}) Map combinedResponseItems, - @Default([]) List resultSummaries, - @Default(null) SearchResponseStream? searchResponseStream, - required bool searching, - required bool generatingAIOverview, + required List results, + required bool isLoading, @Default([]) List trash, - @Default(null) String? searchId, }) = _CommandPaletteState; - factory CommandPaletteState.initial() => const CommandPaletteState( - searching: false, - generatingAIOverview: false, - ); + factory CommandPaletteState.initial() => + const CommandPaletteState(results: [], isLoading: false); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart new file mode 100644 index 0000000000..ef7e59e695 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_listener.dart @@ -0,0 +1,74 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/search_notification.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/notification.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; + +// Do not modify! +const _searchObjectId = "SEARCH_IDENTIFIER"; + +class SearchListener { + SearchListener({this.channel}); + + /// Use this to filter out search results from other channels. + /// + /// If null, it will receive search results from all + /// channels, otherwise it will only receive search results from the specified + /// channel. + /// + final String? channel; + + PublishNotifier? _updateNotifier = PublishNotifier(); + PublishNotifier? _updateDidCloseNotifier = + PublishNotifier(); + SearchNotificationListener? _listener; + + void start({ + void Function(RepeatedSearchResultPB)? onResultsChanged, + void Function(RepeatedSearchResultPB)? onResultsClosed, + }) { + if (onResultsChanged != null) { + _updateNotifier?.addPublishListener(onResultsChanged); + } + + if (onResultsClosed != null) { + _updateDidCloseNotifier?.addPublishListener(onResultsClosed); + } + + _listener = SearchNotificationListener( + objectId: _searchObjectId, + handler: _handler, + channel: channel, + ); + } + + void _handler( + SearchNotification ty, + FlowyResult result, + ) { + switch (ty) { + case SearchNotification.DidUpdateResults: + result.fold( + (payload) => _updateNotifier?.value = + RepeatedSearchResultPB.fromBuffer(payload), + (err) => Log.error(err), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _updateNotifier?.dispose(); + _updateNotifier = null; + _updateDidCloseNotifier?.dispose(); + _updateDidCloseNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart index 6b6ea6d5c0..aedc6fc03e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -1,54 +1,31 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -extension GetIcon on ResultIconPB { - Widget? getIcon() { - final iconValue = value, iconType = ty; - if (iconType == ResultIconTypePB.Emoji) { - return iconValue.isNotEmpty - ? FlowyText.emoji(iconValue, fontSize: 18) - : null; - } else if (ty == ResultIconTypePB.Icon) { - if (_resultIconValueTypes.contains(iconValue)) { - return FlowySvg(getViewSvg(), size: const Size.square(18)); - } - return RawEmojiIconWidget( - emoji: EmojiIconData(iconType.toFlowyIconType(), value), - emojiSize: 18, - ); - } - return null; - } -} +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -extension ResultIconTypePBToFlowyIconType on ResultIconTypePB { - FlowyIconType toFlowyIconType() { - switch (this) { - case ResultIconTypePB.Emoji: - return FlowyIconType.emoji; - case ResultIconTypePB.Icon: - return FlowyIconType.icon; - case ResultIconTypePB.Url: - return FlowyIconType.custom; - default: - return FlowyIconType.custom; +extension GetIcon on SearchResultPB { + Widget? getIcon() { + if (icon.ty == ResultIconTypePB.Emoji) { + return icon.value.isNotEmpty + ? Text( + icon.value, + style: const TextStyle(fontSize: 18.0), + ) + : null; + } else if (icon.ty == ResultIconTypePB.Icon) { + return FlowySvg(icon.getViewSvg(), size: const Size.square(20)); } + + return null; } } extension _ToViewIcon on ResultIconPB { FlowySvgData getViewSvg() => switch (value) { - "0" => FlowySvgs.icon_document_s, - "1" => FlowySvgs.icon_grid_s, - "2" => FlowySvgs.icon_board_s, - "3" => FlowySvgs.icon_calendar_s, - "4" => FlowySvgs.chat_ai_page_s, - _ => FlowySvgs.icon_document_s, + "0" => FlowySvgs.document_s, + "1" => FlowySvgs.grid_s, + "2" => FlowySvgs.board_s, + "3" => FlowySvgs.date_s, + _ => FlowySvgs.document_s, }; } - -const _resultIconValueTypes = {'0', '1', '2', '3', '4'}; diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart deleted file mode 100644 index e5953ae61b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_list_bloc.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'search_result_list_bloc.freezed.dart'; - -class SearchResultListBloc - extends Bloc { - SearchResultListBloc() : super(SearchResultListState.initial()) { - // Register event handlers - on<_OnHoverSummary>(_onHoverSummary); - on<_OnHoverResult>(_onHoverResult); - on<_OpenPage>(_onOpenPage); - } - - FutureOr _onHoverSummary( - _OnHoverSummary event, - Emitter emit, - ) { - emit( - state.copyWith( - hoveredSummary: event.summary, - hoveredResult: null, - userHovered: event.userHovered, - openPageId: null, - ), - ); - } - - FutureOr _onHoverResult( - _OnHoverResult event, - Emitter emit, - ) { - emit( - state.copyWith( - hoveredSummary: null, - hoveredResult: event.item, - userHovered: event.userHovered, - openPageId: null, - ), - ); - } - - FutureOr _onOpenPage( - _OpenPage event, - Emitter emit, - ) { - emit(state.copyWith(openPageId: event.pageId)); - } -} - -@freezed -class SearchResultListEvent with _$SearchResultListEvent { - const factory SearchResultListEvent.onHoverSummary({ - required SearchSummaryPB summary, - required bool userHovered, - }) = _OnHoverSummary; - const factory SearchResultListEvent.onHoverResult({ - required SearchResultItem item, - required bool userHovered, - }) = _OnHoverResult; - - const factory SearchResultListEvent.openPage({ - required String pageId, - }) = _OpenPage; -} - -@freezed -class SearchResultListState with _$SearchResultListState { - const SearchResultListState._(); - const factory SearchResultListState({ - @Default(null) SearchSummaryPB? hoveredSummary, - @Default(null) SearchResultItem? hoveredResult, - @Default(null) String? openPageId, - @Default(false) bool userHovered, - }) = _SearchResultListState; - - factory SearchResultListState.initial() => const SearchResultListState(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart index 89e5b604f8..53a229ae66 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_service.dart @@ -1,131 +1,22 @@ -import 'dart:async'; -import 'dart:ffi'; -import 'dart:isolate'; -import 'dart:typed_data'; - import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/query.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/search_filter.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:nanoid/nanoid.dart'; -import 'package:fixnum/fixnum.dart'; class SearchBackendService { - static Future> performSearch( + static Future> performSearch( String keyword, { String? workspaceId, + String? channel, }) async { - final searchId = nanoid(6); - final stream = SearchResponseStream(searchId: searchId); - final filter = SearchFilterPB(workspaceId: workspaceId); final request = SearchQueryPB( search: keyword, filter: filter, - searchId: searchId, - streamPort: Int64(stream.nativePort), + channel: channel, ); - unawaited(SearchEventSearch(request).send()); - return FlowyResult.success(stream); - } -} - -class SearchResponseStream { - SearchResponseStream({required this.searchId}) { - _port.handler = _controller.add; - _subscription = _controller.stream.listen( - (Uint8List data) => _onResultsChanged(data), - ); - } - - final String searchId; - final RawReceivePort _port = RawReceivePort(); - final StreamController _controller = StreamController.broadcast(); - late StreamSubscription _subscription; - void Function( - List items, - String searchId, - bool searching, - bool generatingAIOverview, - )? _onServerItems; - void Function( - List summaries, - String searchId, - bool searching, - bool generatingAIOverview, - )? _onSummaries; - - void Function( - List items, - String searchId, - )? _onLocalItems; - - void Function(String searchId)? _onFinished; - int get nativePort => _port.sendPort.nativePort; - - Future dispose() async { - await _subscription.cancel(); - _port.close(); - } - - void _onResultsChanged(Uint8List data) { - final searchState = SearchStatePB.fromBuffer(data); - - if (searchState.hasResponse()) { - if (searchState.response.hasSearchResult()) { - _onServerItems?.call( - searchState.response.searchResult.items, - searchId, - searchState.response.searching, - searchState.response.generatingAiSummary, - ); - } - if (searchState.response.hasSearchSummary()) { - _onSummaries?.call( - searchState.response.searchSummary.items, - searchId, - searchState.response.searching, - searchState.response.generatingAiSummary, - ); - } - - if (searchState.response.hasLocalSearchResult()) { - _onLocalItems?.call( - searchState.response.localSearchResult.items, - searchId, - ); - } - } else { - _onFinished?.call(searchId); - } - } - - void listen({ - required void Function( - List items, - String searchId, - bool isLoading, - bool generatingAIOverview, - )? onServerItems, - required void Function( - List summaries, - String searchId, - bool isLoading, - bool generatingAIOverview, - )? onSummaries, - required void Function( - List items, - String searchId, - )? onLocalItems, - required void Function(String searchId)? onFinished, - }) { - _onServerItems = onServerItems; - _onSummaries = onSummaries; - _onLocalItems = onLocalItems; - _onFinished = onFinished; + return SearchEventSearch(request).send(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart index a17b5741bc..74b8316a4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -3,13 +3,20 @@ import 'dart:convert'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/prelude.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; +const List _customParsers = [ + MathEquationNodeParser(), + CalloutNodeParser(), + ToggleListNodeParser(), + CustomImageNodeParser(), +]; + enum DocumentExportType { json, markdown, @@ -25,13 +32,12 @@ class DocumentExporter { final ViewPB view; Future> export( - DocumentExportType type, { - String? path, - }) async { + DocumentExportType type, + ) async { final documentService = DocumentService(); final result = await documentService.openDocument(documentId: view.id); return result.fold( - (r) async { + (r) { final document = r.toDocument(); if (document == null) { return FlowyResult.failure( @@ -44,14 +50,11 @@ class DocumentExporter { case DocumentExportType.json: return FlowyResult.success(jsonEncode(document)); case DocumentExportType.markdown: - if (path != null) { - await customDocumentToMarkdown(document, path: path); - return FlowyResult.success(''); - } else { - return FlowyResult.success( - await customDocumentToMarkdown(document), - ); - } + final markdown = documentToMarkdown( + document, + customParsers: _customParsers, + ); + return FlowyResult.success(markdown); case DocumentExportType.text: throw UnimplementedError(); case DocumentExportType.html: diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart index 546b9ba13d..13322807b3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -18,7 +18,6 @@ class FavoriteBloc extends Bloc { final _service = FavoriteService(); final _listener = FavoriteListener(); - bool isReordering = false; @override Future close() async { @@ -69,7 +68,10 @@ class FavoriteBloc extends Bloc { await _service.pinFavorite(view); } - await _service.toggleFavorite(view.id); + await _service.toggleFavorite( + view.id, + !view.isFavorite, + ); }, pin: (view) async { await _service.pinFavorite(view); @@ -79,23 +81,6 @@ class FavoriteBloc extends Bloc { await _service.unpinFavorite(view); add(const FavoriteEvent.fetchFavorites()); }, - reorder: (oldIndex, newIndex) async { - /// TODO: this is a workaround to reorder the favorite views - isReordering = true; - final pinnedViews = state.pinnedViews.toList(); - if (oldIndex < newIndex) newIndex -= 1; - final target = pinnedViews.removeAt(oldIndex); - pinnedViews.insert(newIndex, target); - emit(state.copyWith(pinnedViews: pinnedViews)); - for (final view in pinnedViews) { - await _service.toggleFavorite(view.item.id); - await _service.toggleFavorite(view.item.id); - } - if (!isClosed) { - add(const FavoriteEvent.fetchFavorites()); - } - isReordering = false; - }, ); }, ); @@ -105,29 +90,20 @@ class FavoriteBloc extends Bloc { FlowyResult favoriteOrFailed, bool didFavorite, ) { - if (!isReordering) { - favoriteOrFailed.fold( - (favorite) => add(const FetchFavorites()), - (error) => Log.error(error), - ); - } + favoriteOrFailed.fold( + (favorite) => add(const FetchFavorites()), + (error) => Log.error(error), + ); } } @freezed class FavoriteEvent with _$FavoriteEvent { const factory FavoriteEvent.initial() = Initial; - const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; - const factory FavoriteEvent.fetchFavorites() = FetchFavorites; - const factory FavoriteEvent.pin(ViewPB view) = PinFavorite; - const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite; - - const factory FavoriteEvent.reorder(int oldIndex, int newIndex) = - ReorderFavorite; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart index 7f0f844dda..2ff57bd80f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart @@ -25,7 +25,10 @@ class FavoriteService { }); } - Future> toggleFavorite(String viewId) async { + Future> toggleFavorite( + String viewId, + bool favoriteStatus, + ) async { final id = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventToggleFavorite(id).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart index 531e797ff5..2df1c95c1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_bloc.dart @@ -1,22 +1,20 @@ import 'package:appflowy/user/application/user_listener.dart'; -import 'package:appflowy/workspace/application/view/view_ext.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' - show WorkspaceLatestPB; + show WorkspaceSettingPB; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; - part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { - HomeBloc(WorkspaceLatestPB workspaceSetting) - : _workspaceListener = FolderListener(), + HomeBloc(WorkspaceSettingPB workspaceSetting) + : _workspaceListener = UserWorkspaceListener(), super(HomeState.initial(workspaceSetting)) { _dispatch(workspaceSetting); } - final FolderListener _workspaceListener; + final UserWorkspaceListener _workspaceListener; @override Future close() async { @@ -24,7 +22,7 @@ class HomeBloc extends Bloc { return super.close(); } - void _dispatch(WorkspaceLatestPB workspaceSetting) { + void _dispatch(WorkspaceSettingPB workspaceSetting) { on( (event, emit) async { await event.map( @@ -36,9 +34,10 @@ class HomeBloc extends Bloc { }); _workspaceListener.start( - onLatestUpdated: (result) { + onSettingUpdated: (result) { result.fold( - (latest) => add(HomeEvent.didReceiveWorkspaceSetting(latest)), + (setting) => + add(HomeEvent.didReceiveWorkspaceSetting(setting)), (r) => Log.error(r), ); }, @@ -48,17 +47,10 @@ class HomeBloc extends Bloc { emit(state.copyWith(isLoading: e.isLoading)); }, didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { - // the latest view is shared across all the members of the workspace. - final latestView = value.setting.hasLatestView() ? value.setting.latestView : state.latestView; - if (latestView != null && latestView.isSpace) { - // If the latest view is a space, we don't need to open it. - return; - } - emit( state.copyWith( workspaceSetting: value.setting, @@ -77,7 +69,7 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; const factory HomeEvent.didReceiveWorkspaceSetting( - WorkspaceLatestPB setting, + WorkspaceSettingPB setting, ) = _DidReceiveWorkspaceSetting; } @@ -85,11 +77,11 @@ class HomeEvent with _$HomeEvent { class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, ViewPB? latestView, }) = _HomeState; - factory HomeState.initial(WorkspaceLatestPB workspaceSetting) => HomeState( + factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( isLoading: false, workspaceSetting: workspaceSetting, ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart index cde67045b9..171bf634a7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/home/home_setting_bloc.dart @@ -2,7 +2,7 @@ import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/workspace/application/edit_panel/edit_context.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart' - show WorkspaceLatestPB; + show WorkspaceSettingPB; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -12,10 +12,10 @@ part 'home_setting_bloc.freezed.dart'; class HomeSettingBloc extends Bloc { HomeSettingBloc( - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsCubit appearanceSettingsCubit, double screenWidthPx, - ) : _listener = FolderListener(), + ) : _listener = UserWorkspaceListener(), _appearanceSettingsCubit = appearanceSettingsCubit, super( HomeSettingState.initial( @@ -27,7 +27,7 @@ class HomeSettingBloc extends Bloc { _dispatch(); } - final FolderListener _listener; + final UserWorkspaceListener _listener; final AppearanceSettingsCubit _appearanceSettingsCubit; @override @@ -86,7 +86,7 @@ class HomeSettingBloc extends Bloc { }, editPanelResized: (_EditPanelResized e) { final newPosition = - (state.resizeStart + e.offset).clamp(0, 200).toDouble(); + (e.offset + state.resizeStart).clamp(-50, 200).toDouble(); if (state.resizeOffset != newPosition) { emit(state.copyWith(resizeOffset: newPosition)); } @@ -124,7 +124,7 @@ class HomeSettingEvent with _$HomeSettingEvent { _ShowEditPanel; const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel; const factory HomeSettingEvent.didReceiveWorkspaceSetting( - WorkspaceLatestPB setting, + WorkspaceSettingPB setting, ) = _DidReceiveWorkspaceSetting; const factory HomeSettingEvent.collapseMenu() = _CollapseMenu; const factory HomeSettingEvent.checkScreenSize(double screenWidthPx) = @@ -139,7 +139,7 @@ class HomeSettingEvent with _$HomeSettingEvent { class HomeSettingState with _$HomeSettingState { const factory HomeSettingState({ required EditPanelContext? panelContext, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, required bool keepMenuCollapsed, @@ -150,7 +150,7 @@ class HomeSettingState with _$HomeSettingState { }) = _HomeSettingState; factory HomeSettingState.initial( - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, AppearanceSettingsState appearanceSettingsState, double screenWidthPx, ) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart index a9e4d28a3a..249144096c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/menu_user_bloc.dart @@ -13,7 +13,7 @@ part 'menu_user_bloc.freezed.dart'; class MenuUserBloc extends Bloc { MenuUserBloc(this.userProfile) : _userListener = UserListener(userProfile: userProfile), - _userWorkspaceListener = FolderListener(), + _userWorkspaceListener = UserWorkspaceListener(), _userService = UserBackendService(userId: userProfile.id), super(MenuUserState.initial(userProfile)) { _dispatch(); @@ -21,7 +21,7 @@ class MenuUserBloc extends Bloc { final UserBackendService _userService; final UserListener _userListener; - final FolderListener _userWorkspaceListener; + final UserWorkspaceListener _userWorkspaceListener; final UserProfilePB userProfile; @override diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart index d6a6a73578..e38f4cf0da 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -54,11 +54,9 @@ class SidebarSectionsBloc _initial(userProfile, workspaceId); final sectionViews = await _getSectionViews(); if (sectionViews != null) { - final containsSpace = _containsSpace(sectionViews); emit( state.copyWith( section: sectionViews, - containsSpace: containsSpace, ), ); } @@ -67,19 +65,18 @@ class SidebarSectionsBloc _reset(userProfile, workspaceId); final sectionViews = await _getSectionViews(); if (sectionViews != null) { - final containsSpace = _containsSpace(sectionViews); emit( state.copyWith( section: sectionViews, - containsSpace: containsSpace, ), ); } }, - createRootViewInSection: (name, section, index) async { + createRootViewInSection: (name, section, desc, index) async { final result = await _workspaceService.createView( name: name, viewSection: section, + desc: desc, index: index, ); result.fold( @@ -105,8 +102,6 @@ class SidebarSectionsBloc case ViewSectionPB.Public: emit( state.copyWith( - containsSpace: state.containsSpace || - sectionViews.views.any((view) => view.isSpace), section: state.section.copyWith( publicViews: sectionViews.views, ), @@ -115,8 +110,6 @@ class SidebarSectionsBloc case ViewSectionPB.Private: emit( state.copyWith( - containsSpace: state.containsSpace || - sectionViews.views.any((view) => view.isSpace), section: state.section.copyWith( privateViews: sectionViews.views, ), @@ -167,11 +160,9 @@ class SidebarSectionsBloc _initial(userProfile, workspaceId); final sectionViews = await _getSectionViews(); if (sectionViews != null) { - final containsSpace = _containsSpace(sectionViews); emit( state.copyWith( section: sectionViews, - containsSpace: containsSpace, ), ); // try to open the fist view in public section or private section @@ -238,16 +229,8 @@ class SidebarSectionsBloc } } - bool _containsSpace(SidebarSection section) { - return section.publicViews.any((view) => view.isSpace) || - section.privateViews.any((view) => view.isSpace); - } - void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService( - workspaceId: workspaceId, - userId: userProfile.id, - ); + _workspaceService = WorkspaceService(workspaceId: workspaceId); _listener = WorkspaceSectionsListener( user: userProfile, @@ -285,6 +268,7 @@ class SidebarSectionsEvent with _$SidebarSectionsEvent { const factory SidebarSectionsEvent.createRootViewInSection({ required String name, required ViewSectionPB viewSection, + String? desc, int? index, }) = _CreateRootViewInSection; const factory SidebarSectionsEvent.moveRootView({ @@ -308,7 +292,6 @@ class SidebarSectionsState with _$SidebarSectionsState { required SidebarSection section, @Default(null) ViewPB? lastCreatedRootView, FlowyResult? createRootViewResult, - @Default(true) bool containsSpace, }) = _SidebarSectionsState; factory SidebarSectionsState.initial() => const SidebarSectionsState( diff --git a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart index 3f9657c5cf..5418eb2b1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/notification/notification_service.dart @@ -1,11 +1,8 @@ import 'package:flutter/foundation.dart'; + import 'package:local_notifier/local_notifier.dart'; -/// The app name used in the local notification. -/// -/// DO NOT Use i18n here, because the i18n plugin is not ready -/// before the local notification is initialized. -const _localNotifierAppName = 'AppFlowy'; +const _appName = "AppFlowy"; /// Manages Local Notifications /// @@ -16,11 +13,7 @@ const _localNotifierAppName = 'AppFlowy'; /// class NotificationService { static Future initialize() async { - await localNotifier.setup( - appName: _localNotifierAppName, - // Don't create a shortcut on Windows, because the setup.exe will create a shortcut - shortcutPolicy: ShortcutPolicy.requireNoCreate, - ); + await localNotifier.setup(appName: _appName); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart index a5381ce17f..5445d105f9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/recent/cached_recent_service.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:appflowy/shared/list_extension.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/recent/recent_listener.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; @@ -8,7 +7,6 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart'; /// This is a lazy-singleton to share recent views across the application. @@ -33,13 +31,13 @@ class CachedRecentService { final _listener = RecentViewsListener(); Future> recentViews() async { - if (_isInitialized || _completer.isCompleted) return _recentViews; + if (_isInitialized) return _recentViews; _isInitialized = true; _listener.start(recentViewsUpdated: _recentViewsUpdated); _recentViews = await _readRecentViews().fold( - (s) => s.items.unique((e) => e.item.id), + (s) => s.items, (_) => [], ); _completer.complete(); @@ -70,16 +68,12 @@ class CachedRecentService { Future> _readRecentViews() async { - final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100)); - final result = await FolderEventReadRecentViews(payload).send(); + final result = await FolderEventReadRecentViews().send(); return result.fold( (recentViews) { return FlowyResult.success( RepeatedRecentViewPB( - // filter the space view and the orphan view - items: recentViews.items.where( - (e) => !e.item.isSpace && e.item.id != e.item.parentViewId, - ), + items: recentViews.items.where((e) => !e.item.isSpace), ), ); }, @@ -107,7 +101,7 @@ class CachedRecentService { final viewIds = result.toNullable(); if (viewIds != null) { _recentViews = await _readRecentViews().fold( - (s) => s.items.unique((e) => e.item.id), + (s) => s.items, (_) => [], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart deleted file mode 100644 index 492c19ab73..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_bloc.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'local_llm_listener.dart'; - -part 'local_ai_bloc.freezed.dart'; - -class LocalAiPluginBloc extends Bloc { - LocalAiPluginBloc() : super(const LoadingLocalAiPluginState()) { - on(_handleEvent); - _startListening(); - _getLocalAiState(); - } - - final listener = LocalAIStateListener(); - - @override - Future close() async { - await listener.stop(); - return super.close(); - } - - Future _handleEvent( - LocalAiPluginEvent event, - Emitter emit, - ) async { - if (isClosed) { - return; - } - - await event.when( - didReceiveAiState: (aiState) { - emit( - LocalAiPluginState.ready( - isEnabled: aiState.enabled, - version: aiState.pluginVersion, - runningState: aiState.state, - lackOfResource: - aiState.hasLackOfResource() ? aiState.lackOfResource : null, - ), - ); - }, - didReceiveLackOfResources: (resources) { - state.maybeMap( - ready: (readyState) { - emit(readyState.copyWith(lackOfResource: resources)); - }, - orElse: () {}, - ); - }, - toggle: () async { - emit(LocalAiPluginState.loading()); - await AIEventToggleLocalAI().send().fold( - (aiState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - } - }, - Log.error, - ); - }, - restart: () async { - emit(LocalAiPluginState.loading()); - await AIEventRestartLocalAI().send(); - }, - ); - } - - void _startListening() { - listener.start( - stateCallback: (pluginState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(pluginState)); - } - }, - resourceCallback: (data) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveLackOfResources(data)); - } - }, - ); - } - - void _getLocalAiState() { - AIEventGetLocalAIState().send().fold( - (aiState) { - if (!isClosed) { - add(LocalAiPluginEvent.didReceiveAiState(aiState)); - } - }, - Log.error, - ); - } -} - -@freezed -class LocalAiPluginEvent with _$LocalAiPluginEvent { - const factory LocalAiPluginEvent.didReceiveAiState(LocalAIPB aiState) = - _DidReceiveAiState; - const factory LocalAiPluginEvent.didReceiveLackOfResources( - LackOfAIResourcePB resources, - ) = _DidReceiveLackOfResources; - const factory LocalAiPluginEvent.toggle() = _Toggle; - const factory LocalAiPluginEvent.restart() = _Restart; -} - -@freezed -class LocalAiPluginState with _$LocalAiPluginState { - const LocalAiPluginState._(); - - const factory LocalAiPluginState.ready({ - required bool isEnabled, - required String version, - required RunningStatePB runningState, - required LackOfAIResourcePB? lackOfResource, - }) = ReadyLocalAiPluginState; - - const factory LocalAiPluginState.loading() = LoadingLocalAiPluginState; - - bool get isEnabled { - return maybeWhen( - ready: (isEnabled, _, __, ___) => isEnabled, - orElse: () => false, - ); - } - - bool get showIndicator { - return maybeWhen( - ready: (isEnabled, _, runningState, lackOfResource) => - runningState != RunningStatePB.Running || lackOfResource != null, - orElse: () => false, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart deleted file mode 100644 index 3bb26a182b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'local_ai_on_boarding_bloc.freezed.dart'; - -class LocalAIOnBoardingBloc - extends Bloc { - LocalAIOnBoardingBloc( - this.userProfile, - this.currentWorkspaceMemberRole, - this.workspaceId, - ) : super(const LocalAIOnBoardingState()) { - _userService = UserBackendService(userId: userProfile.id); - _successListenable = getIt(); - _successListenable.addListener(_onPaymentSuccessful); - _dispatch(); - } - - void _onPaymentSuccessful() { - if (isClosed) { - return; - } - - add( - LocalAIOnBoardingEvent.paymentSuccessful( - _successListenable.subscribedPlan, - ), - ); - } - - final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; - final String workspaceId; - late final IUserBackendService _userService; - late final SubscriptionSuccessListenable _successListenable; - - @override - Future close() async { - _successListenable.removeListener(_onPaymentSuccessful); - await super.close(); - } - - void _dispatch() { - on((event, emit) { - event.when( - started: () { - _loadSubscriptionPlans(); - }, - addSubscription: (plan) async { - emit(state.copyWith(isLoading: true)); - final result = await _userService.createSubscription( - workspaceId, - plan, - ); - - result.fold( - (pl) => afLaunchUrlString(pl.paymentLink), - (f) => Log.error( - 'Failed to fetch paymentlink for $plan: ${f.msg}', - f, - ), - ); - }, - didGetSubscriptionPlans: (result) { - result.fold( - (workspaceSubInfo) { - final isPurchaseAILocal = workspaceSubInfo.addOns.any((addOn) { - return addOn.type == WorkspaceAddOnPBType.AddOnAiLocal; - }); - - emit( - state.copyWith(isPurchaseAILocal: isPurchaseAILocal), - ); - }, - (err) { - Log.warn("Failed to get subscription plans: $err"); - }, - ); - }, - paymentSuccessful: (SubscriptionPlanPB? plan) { - if (plan == SubscriptionPlanPB.AiLocal) { - emit(state.copyWith(isPurchaseAILocal: true, isLoading: false)); - } - }, - ); - }); - } - - void _loadSubscriptionPlans() { - final payload = UserWorkspaceIdPB()..workspaceId = workspaceId; - UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) { - if (!isClosed) { - add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result)); - } - }); - } -} - -@freezed -class LocalAIOnBoardingEvent with _$LocalAIOnBoardingEvent { - const factory LocalAIOnBoardingEvent.started() = _Started; - const factory LocalAIOnBoardingEvent.addSubscription( - SubscriptionPlanPB plan, - ) = _AddSubscription; - const factory LocalAIOnBoardingEvent.paymentSuccessful( - SubscriptionPlanPB? plan, - ) = _PaymentSuccessful; - const factory LocalAIOnBoardingEvent.didGetSubscriptionPlans( - FlowyResult result, - ) = _LoadSubscriptionPlans; -} - -@freezed -class LocalAIOnBoardingState with _$LocalAIOnBoardingState { - const factory LocalAIOnBoardingState({ - @Default(false) bool isPurchaseAILocal, - @Default(false) bool isLoading, - }) = _LocalAIOnBoardingState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart deleted file mode 100644 index 99c90faeb5..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/local_llm_listener.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -typedef PluginStateCallback = void Function(LocalAIPB state); -typedef PluginResourceCallback = void Function(LackOfAIResourcePB data); - -class LocalAIStateListener { - LocalAIStateListener() { - _parser = - ChatNotificationParser(id: "appflowy_ai_plugin", callback: _callback); - _subscription = RustStreamReceiver.listen( - (observable) => _parser?.parse(observable), - ); - } - - StreamSubscription? _subscription; - ChatNotificationParser? _parser; - - PluginStateCallback? stateCallback; - PluginResourceCallback? resourceCallback; - - void start({ - PluginStateCallback? stateCallback, - PluginResourceCallback? resourceCallback, - }) { - this.stateCallback = stateCallback; - this.resourceCallback = resourceCallback; - } - - void _callback( - ChatNotification ty, - FlowyResult result, - ) { - result.map((r) { - switch (ty) { - case ChatNotification.UpdateLocalAIState: - stateCallback?.call(LocalAIPB.fromBuffer(r)); - break; - case ChatNotification.LocalAIResourceUpdated: - resourceCallback?.call(LackOfAIResourcePB.fromBuffer(r)); - break; - default: - break; - } - }); - } - - Future stop() async { - await _subscription?.cancel(); - _subscription = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart deleted file mode 100644 index f5c4209028..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/ollama_setting_bloc.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:equatable/equatable.dart'; - -part 'ollama_setting_bloc.freezed.dart'; - -class OllamaSettingBloc extends Bloc { - OllamaSettingBloc() : super(const OllamaSettingState()) { - on(_handleEvent); - } - - Future _handleEvent( - OllamaSettingEvent event, - Emitter emit, - ) async { - event.when( - started: () { - AIEventGetLocalAISetting().send().fold( - (setting) { - if (!isClosed) { - add(OllamaSettingEvent.didLoadSetting(setting)); - } - }, - Log.error, - ); - }, - didLoadSetting: (setting) => _updateSetting(setting, emit), - updateSetting: (setting) => _updateSetting(setting, emit), - onEdit: (content, settingType) { - final updatedSubmittedItems = state.submittedItems - .map( - (item) => item.settingType == settingType - ? SubmittedItem( - content: content, - settingType: item.settingType, - ) - : item, - ) - .toList(); - - // Convert both lists to maps: {settingType: content} - final updatedMap = { - for (final item in updatedSubmittedItems) - item.settingType: item.content, - }; - - final inputMap = { - for (final item in state.inputItems) item.settingType: item.content, - }; - - // Compare maps instead of lists - final isEdited = !const MapEquality() - .equals(updatedMap, inputMap); - - emit( - state.copyWith( - submittedItems: updatedSubmittedItems, - isEdited: isEdited, - ), - ); - }, - submit: () { - final setting = LocalAISettingPB(); - final settingUpdaters = { - SettingType.serverUrl: (value) => setting.serverUrl = value, - SettingType.chatModel: (value) => setting.chatModelName = value, - SettingType.embeddingModel: (value) => - setting.embeddingModelName = value, - }; - - for (final item in state.submittedItems) { - settingUpdaters[item.settingType]?.call(item.content); - } - add(OllamaSettingEvent.updateSetting(setting)); - AIEventUpdateLocalAISetting(setting).send().fold( - (_) => Log.info('AI setting updated successfully'), - (err) => Log.error("update ai setting failed: $err"), - ); - }, - ); - } - - void _updateSetting( - LocalAISettingPB setting, - Emitter emit, - ) { - emit( - state.copyWith( - setting: setting, - inputItems: _createInputItems(setting), - submittedItems: _createSubmittedItems(setting), - isEdited: false, // Reset to false when the setting is loaded/updated. - ), - ); - } - - List _createInputItems(LocalAISettingPB setting) => [ - SettingItem( - content: setting.serverUrl, - hintText: 'http://localhost:11434', - settingType: SettingType.serverUrl, - ), - SettingItem( - content: setting.chatModelName, - hintText: 'llama3.1', - settingType: SettingType.chatModel, - ), - SettingItem( - content: setting.embeddingModelName, - hintText: 'nomic-embed-text', - settingType: SettingType.embeddingModel, - ), - ]; - - List _createSubmittedItems(LocalAISettingPB setting) => [ - SubmittedItem( - content: setting.serverUrl, - settingType: SettingType.serverUrl, - ), - SubmittedItem( - content: setting.chatModelName, - settingType: SettingType.chatModel, - ), - SubmittedItem( - content: setting.embeddingModelName, - settingType: SettingType.embeddingModel, - ), - ]; -} - -// Create an enum for setting type. -enum SettingType { - serverUrl, - chatModel, - embeddingModel; // semicolon needed after the enum values - - String get title { - switch (this) { - case SettingType.serverUrl: - return 'Ollama server url'; - case SettingType.chatModel: - return 'Chat model name'; - case SettingType.embeddingModel: - return 'Embedding model name'; - } - } -} - -class SettingItem extends Equatable { - const SettingItem({ - required this.content, - required this.hintText, - required this.settingType, - }); - final String content; - final String hintText; - final SettingType settingType; - @override - List get props => [content, settingType]; -} - -class SubmittedItem extends Equatable { - const SubmittedItem({ - required this.content, - required this.settingType, - }); - final String content; - final SettingType settingType; - - @override - List get props => [content, settingType]; -} - -@freezed -class OllamaSettingEvent with _$OllamaSettingEvent { - const factory OllamaSettingEvent.started() = _Started; - const factory OllamaSettingEvent.didLoadSetting(LocalAISettingPB setting) = - _DidLoadSetting; - const factory OllamaSettingEvent.updateSetting(LocalAISettingPB setting) = - _UpdateSetting; - const factory OllamaSettingEvent.onEdit( - String content, - SettingType settingType, - ) = _OnEdit; - const factory OllamaSettingEvent.submit() = _OnSubmit; -} - -@freezed -class OllamaSettingState with _$OllamaSettingState { - const factory OllamaSettingState({ - LocalAISettingPB? setting, - @Default([ - SettingItem( - content: 'http://localhost:11434', - hintText: 'http://localhost:11434', - settingType: SettingType.serverUrl, - ), - SettingItem( - content: 'llama3.1', - hintText: 'llama3.1', - settingType: SettingType.chatModel, - ), - SettingItem( - content: 'nomic-embed-text', - hintText: 'nomic-embed-text', - settingType: SettingType.embeddingModel, - ), - ]) - List inputItems, - @Default([]) List submittedItems, - @Default(false) bool isEdited, - }) = _PluginStateState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart deleted file mode 100644 index 0141283765..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/ai/settings_ai_bloc.dart +++ /dev/null @@ -1,190 +0,0 @@ -import 'package:appflowy/plugins/ai_chat/application/ai_model_switch_listener.dart'; -import 'package:appflowy/user/application/user_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'settings_ai_bloc.freezed.dart'; - -const String aiModelsGlobalActiveModel = "ai_models_global_active_model"; - -class SettingsAIBloc extends Bloc { - SettingsAIBloc( - this.userProfile, - this.workspaceId, - ) : _userListener = UserListener(userProfile: userProfile), - _aiModelSwitchListener = - AIModelSwitchListener(objectId: aiModelsGlobalActiveModel), - super( - SettingsAIState( - userProfile: userProfile, - ), - ) { - _aiModelSwitchListener.start( - onUpdateSelectedModel: (model) { - if (!isClosed) { - _loadModelList(); - } - }, - ); - _dispatch(); - } - - final UserListener _userListener; - final UserProfilePB userProfile; - final String workspaceId; - final AIModelSwitchListener _aiModelSwitchListener; - - @override - Future close() async { - await _userListener.stop(); - await _aiModelSwitchListener.stop(); - return super.close(); - } - - void _dispatch() { - on((event, emit) async { - await event.when( - started: () { - _userListener.start( - onProfileUpdated: _onProfileUpdated, - onUserWorkspaceSettingUpdated: (settings) { - if (!isClosed) { - add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); - } - }, - ); - _loadModelList(); - _loadUserWorkspaceSetting(); - }, - didReceiveUserProfile: (userProfile) { - emit(state.copyWith(userProfile: userProfile)); - }, - toggleAISearch: () { - emit( - state.copyWith(enableSearchIndexing: !state.enableSearchIndexing), - ); - _updateUserWorkspaceSetting( - disableSearchIndexing: - !(state.aiSettings?.disableSearchIndexing ?? false), - ); - }, - selectModel: (AIModelPB model) async { - if (!model.isLocal) { - await _updateUserWorkspaceSetting(model: model.name); - } - await AIEventUpdateSelectedModel( - UpdateSelectedModelPB( - source: aiModelsGlobalActiveModel, - selectedModel: model, - ), - ).send(); - }, - didLoadWorkspaceSetting: (WorkspaceSettingsPB settings) { - emit( - state.copyWith( - aiSettings: settings, - enableSearchIndexing: !settings.disableSearchIndexing, - ), - ); - }, - didLoadAvailableModels: (AvailableModelsPB models) { - emit( - state.copyWith( - availableModels: models, - ), - ); - }, - ); - }); - } - - Future> _updateUserWorkspaceSetting({ - bool? disableSearchIndexing, - String? model, - }) async { - final payload = UpdateUserWorkspaceSettingPB( - workspaceId: workspaceId, - ); - if (disableSearchIndexing != null) { - payload.disableSearchIndexing = disableSearchIndexing; - } - if (model != null) { - payload.aiModel = model; - } - final result = await UserEventUpdateWorkspaceSetting(payload).send(); - result.fold( - (ok) => Log.info('Update workspace setting success'), - (err) => Log.error('Update workspace setting failed: $err'), - ); - return result; - } - - void _onProfileUpdated( - FlowyResult userProfileOrFailed, - ) => - userProfileOrFailed.fold( - (profile) => add(SettingsAIEvent.didReceiveUserProfile(profile)), - (err) => Log.error(err), - ); - - void _loadModelList() { - AIEventGetServerAvailableModels().send().then((result) { - result.fold((models) { - if (!isClosed) { - add(SettingsAIEvent.didLoadAvailableModels(models)); - } - }, (err) { - Log.error(err); - }); - }); - } - - void _loadUserWorkspaceSetting() { - final payload = UserWorkspaceIdPB(workspaceId: workspaceId); - UserEventGetWorkspaceSetting(payload).send().then((result) { - result.fold((settings) { - if (!isClosed) { - add(SettingsAIEvent.didLoadWorkspaceSetting(settings)); - } - }, (err) { - Log.error(err); - }); - }); - } -} - -@freezed -class SettingsAIEvent with _$SettingsAIEvent { - const factory SettingsAIEvent.started() = _Started; - const factory SettingsAIEvent.didLoadWorkspaceSetting( - WorkspaceSettingsPB settings, - ) = _DidLoadWorkspaceSetting; - - const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch; - - const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel; - - const factory SettingsAIEvent.didReceiveUserProfile( - UserProfilePB newUserProfile, - ) = _DidReceiveUserProfile; - - const factory SettingsAIEvent.didLoadAvailableModels( - AvailableModelsPB models, - ) = _DidLoadAvailableModels; -} - -@freezed -class SettingsAIState with _$SettingsAIState { - const factory SettingsAIState({ - required UserProfilePB userProfile, - WorkspaceSettingsPB? aiSettings, - AvailableModelsPB? availableModels, - @Default(true) bool enableSearchIndexing, - }) = _SettingsAIState; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart index 99b9eaa2c9..a034558110 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/appearance_cubit.dart @@ -2,13 +2,11 @@ import 'dart:async'; 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/startup/startup.dart'; import 'package:appflowy/user/application/user_settings_service.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/application/appearance_defaults.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; @@ -19,7 +17,6 @@ import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:universal_platform/universal_platform.dart'; part 'appearance_cubit.freezed.dart'; @@ -100,19 +97,7 @@ class AppearanceSettingsCubit extends Cubit { Future setTheme(String themeName) async { _appearanceSettings.theme = themeName; unawaited(_saveAppearanceSettings()); - try { - final theme = await AppTheme.fromName(themeName); - emit(state.copyWith(appTheme: theme)); - } catch (e) { - Log.error("Error setting theme: $e"); - if (UniversalPlatform.isMacOS) { - showToastNotification( - message: - LocaleKeys.settings_workspacePage_theme_failedToLoadThemes.tr(), - type: ToastificationType.error, - ); - } - } + emit(state.copyWith(appTheme: await AppTheme.fromName(themeName))); } /// Reset the current user selected theme back to the default @@ -144,8 +129,9 @@ class AppearanceSettingsCubit extends Cubit { emit(state.copyWith(layoutDirection: layoutDirection)); } - void setTextDirection(AppFlowyTextDirection textDirection) { - _appearanceSettings.textDirection = textDirection.toTextDirectionPB(); + void setTextDirection(AppFlowyTextDirection? textDirection) { + _appearanceSettings.textDirection = + textDirection?.toTextDirectionPB() ?? TextDirectionPB.FALLBACK; _saveAppearanceSettings(); emit(state.copyWith(textDirection: textDirection)); } @@ -324,6 +310,7 @@ ThemeModePB _themeModeToPB(ThemeMode themeMode) { case ThemeMode.dark: return ThemeModePB.Dark; case ThemeMode.system: + default: return ThemeModePB.System; } } @@ -349,7 +336,7 @@ enum AppFlowyTextDirection { rtl, auto; - static AppFlowyTextDirection fromTextDirectionPB( + static AppFlowyTextDirection? fromTextDirectionPB( TextDirectionPB? textDirectionPB, ) { switch (textDirectionPB) { @@ -360,7 +347,7 @@ enum AppFlowyTextDirection { case TextDirectionPB.AUTO: return AppFlowyTextDirection.auto; default: - return AppFlowyTextDirection.ltr; + return null; } } @@ -372,6 +359,8 @@ enum AppFlowyTextDirection { return TextDirectionPB.RTL; case AppFlowyTextDirection.auto: return TextDirectionPB.AUTO; + default: + return TextDirectionPB.FALLBACK; } } } @@ -385,7 +374,7 @@ class AppearanceSettingsState with _$AppearanceSettingsState { required ThemeMode themeMode, required String font, required LayoutDirection layoutDirection, - required AppFlowyTextDirection textDirection, + required AppFlowyTextDirection? textDirection, required bool enableRtlToolbarItems, required Locale locale, required bool isMenuCollapsed, @@ -435,7 +424,6 @@ class AppearanceSettingsState with _$AppearanceSettingsState { } ThemeData get lightTheme => _getThemeData(Brightness.light); - ThemeData get darkTheme => _getThemeData(Brightness.dark); ThemeData _getThemeData(Brightness brightness) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart index 6be53cf158..952f1e18f9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -1,4 +1,5 @@ import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; @@ -30,7 +31,8 @@ abstract class BaseAppearance { double? lineHeight, }) { fontSize = fontSize ?? FontSizes.s14; - fontWeight = fontWeight ?? FontWeight.w400; + fontWeight = fontWeight ?? + (PlatformExtension.isDesktopOrWeb ? FontWeight.w500 : FontWeight.w400); letterSpacing = fontSize * (letterSpacing ?? 0.005); final textStyle = TextStyle( diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index c1e539cf58..f09e08a3d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class DesktopAppearance extends BaseAppearance { @override @@ -14,10 +15,9 @@ class DesktopAppearance extends BaseAppearance { ) { assert(codeFontFamily.isNotEmpty); - fontFamily = fontFamily.isEmpty ? defaultFontFamily : fontFamily; - - final isLight = brightness == Brightness.light; - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; + final theme = brightness == Brightness.light + ? appTheme.lightTheme + : appTheme.darkTheme; final colorScheme = ColorScheme( brightness: brightness, @@ -49,7 +49,6 @@ class DesktopAppearance extends BaseAppearance { // Due to Desktop version has multiple themes, it relies on the current theme to build the ThemeData return ThemeData( - visualDensity: VisualDensity.standard, useMaterial3: false, brightness: brightness, dialogBackgroundColor: theme.surface, @@ -57,11 +56,6 @@ class DesktopAppearance extends BaseAppearance { fontFamily: fontFamily, fontColor: theme.text, ), - textButtonTheme: const TextButtonThemeData( - style: ButtonStyle( - minimumSize: WidgetStatePropertyAll(Size.zero), - ), - ), textSelectionTheme: TextSelectionThemeData( cursorColor: theme.main2, selectionHandleColor: theme.main2, @@ -81,12 +75,18 @@ class DesktopAppearance extends BaseAppearance { contentTextStyle: TextStyle(color: colorScheme.onSurface), ), scrollbarTheme: ScrollbarThemeData( - thumbColor: WidgetStateProperty.resolveWith( - (states) => states.any(scrollbarInteractiveStates.contains) - ? theme.scrollbarHoverColor - : theme.scrollbarColor, - ), - thickness: WidgetStateProperty.resolveWith((_) => 4.0), + thumbColor: WidgetStateProperty.resolveWith((states) { + if (states.any(scrollbarInteractiveStates.contains)) { + return theme.shader3; + } + return theme.shader5; + }), + thickness: WidgetStateProperty.resolveWith((states) { + if (states.any(scrollbarInteractiveStates.contains)) { + return 4; + } + return 3.0; + }), crossAxisMargin: 0.0, mainAxisMargin: 6.0, radius: Corners.s10Radius, @@ -147,11 +147,6 @@ class DesktopAppearance extends BaseAppearance { ), onBackground: theme.text, background: theme.surface, - borderColor: theme.borderColor, - scrollbarColor: theme.scrollbarColor, - scrollbarHoverColor: theme.scrollbarHoverColor, - lightIconColor: theme.lightIconColor, - toolbarHoverColor: theme.toolbarHoverColor, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 46eddd53ab..e2f1ee0006 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; + // ThemeData in mobile import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { static const _primaryColor = Color(0xFF00BCF0); //primary 100 @@ -28,12 +29,13 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, ); - final isLight = brightness == Brightness.light; final codeFontStyle = getFontStyle(fontFamily: codeFontFamily); - final theme = isLight ? appTheme.lightTheme : appTheme.darkTheme; + final theme = brightness == Brightness.light + ? appTheme.lightTheme + : appTheme.darkTheme; - final colorTheme = isLight + final colorTheme = brightness == Brightness.light ? ColorScheme( brightness: brightness, primary: _primaryColor, @@ -48,11 +50,11 @@ class MobileAppearance extends BaseAppearance { error: const Color(0xffFB006D), onError: const Color(0xffFB006D), outline: const Color(0xffe3e3e3), - outlineVariant: const Color(0xffCBD5E0).withValues(alpha: 0.24), + outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24), //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color - surfaceContainerHighest: theme.sidebarBg, + surfaceContainerHighest: const Color.fromARGB(255, 216, 216, 216), ) : ColorScheme( brightness: brightness, @@ -68,11 +70,14 @@ class MobileAppearance extends BaseAppearance { //Snack bar surface: const Color(0xFF171A1F), onSurface: const Color(0xffC5C6C7), // text/body color - surfaceContainerHighest: theme.sidebarBg, ); - final hintColor = isLight ? const Color(0x991F2329) : _hintColorInDarkMode; - final onBackground = isLight ? _onBackgroundColor : Colors.white; - final background = isLight ? Colors.white : const Color(0xff121212); + final hintColor = brightness == Brightness.light + ? const Color(0x991F2329) + : _hintColorInDarkMode; + final onBackground = + brightness == Brightness.light ? _onBackgroundColor : Colors.white; + final background = + brightness == Brightness.light ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, @@ -271,11 +276,6 @@ class MobileAppearance extends BaseAppearance { ), onBackground: onBackground, background: background, - borderColor: theme.borderColor, - scrollbarColor: theme.scrollbarColor, - scrollbarHoverColor: theme.scrollbarHoverColor, - lightIconColor: theme.lightIconColor, - toolbarHoverColor: theme.toolbarHoverColor, ), ToolbarColorExtension.fromBrightness(brightness), ], diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart index febb89727a..fdb53f4e43 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_setting_bloc.dart @@ -1,4 +1,3 @@ -import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/cloud_setting_listener.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -15,7 +14,7 @@ class AppFlowyCloudSettingBloc extends Bloc { AppFlowyCloudSettingBloc(CloudSettingPB setting) : _listener = UserCloudConfigListener(), - super(AppFlowyCloudSettingState.initial(setting, false)) { + super(AppFlowyCloudSettingState.initial(setting)) { _dispatch(); } @@ -32,10 +31,6 @@ class AppFlowyCloudSettingBloc (event, emit) async { await event.when( initial: () async { - await getSyncLogEnabled().then((value) { - emit(state.copyWith(isSyncLogEnabled: value)); - }); - _listener.start( onSettingChanged: (result) { if (isClosed) { @@ -53,10 +48,6 @@ class AppFlowyCloudSettingBloc final config = UpdateCloudConfigPB.create()..enableSync = isEnable; await UserEventSetCloudConfig(config).send(); }, - enableSyncLog: (isEnable) async { - await setSyncLogEnabled(isEnable); - emit(state.copyWith(isSyncLogEnabled: isEnable)); - }, didReceiveSetting: (CloudSettingPB setting) { emit( state.copyWith( @@ -76,8 +67,6 @@ class AppFlowyCloudSettingEvent with _$AppFlowyCloudSettingEvent { const factory AppFlowyCloudSettingEvent.initial() = _Initial; const factory AppFlowyCloudSettingEvent.enableSync(bool isEnable) = _EnableSync; - const factory AppFlowyCloudSettingEvent.enableSyncLog(bool isEnable) = - _EnableSyncLog; const factory AppFlowyCloudSettingEvent.didReceiveSetting( CloudSettingPB setting, ) = _DidUpdateSetting; @@ -88,17 +77,12 @@ class AppFlowyCloudSettingState with _$AppFlowyCloudSettingState { const factory AppFlowyCloudSettingState({ required CloudSettingPB setting, required bool showRestartHint, - required bool isSyncLogEnabled, }) = _AppFlowyCloudSettingState; - factory AppFlowyCloudSettingState.initial( - CloudSettingPB setting, - bool isSyncLogEnabled, - ) => + factory AppFlowyCloudSettingState.initial(CloudSettingPB setting) => AppFlowyCloudSettingState( setting: setting, showRestartHint: setting.serverUrl.isNotEmpty, - isSyncLogEnabled: isSyncLogEnabled, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart index 5652904180..998e6d632f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appflowy_cloud_urls_bloc.dart @@ -24,15 +24,6 @@ class AppFlowyCloudURLsBloc ), ); }, - updateBaseWebDomain: (url) { - emit( - state.copyWith( - updatedBaseWebDomain: url, - urlError: null, - showRestartHint: url.isNotEmpty, - ), - ); - }, confirmUpdate: () async { if (state.updatedServerUrl.isEmpty) { emit( @@ -44,27 +35,13 @@ class AppFlowyCloudURLsBloc ), ); } else { - bool isSuccess = false; - - await validateUrl(state.updatedServerUrl).fold( + validateUrl(state.updatedServerUrl).fold( (url) async { await useSelfHostedAppFlowyCloudWithURL(url); - isSuccess = true; + add(const AppFlowyCloudURLsEvent.didSaveConfig()); }, - (err) async => emit(state.copyWith(urlError: err)), + (err) => emit(state.copyWith(urlError: err)), ); - - await validateUrl(state.updatedBaseWebDomain).fold( - (url) async { - await useBaseWebDomain(url); - isSuccess = true; - }, - (err) async => emit(state.copyWith(urlError: err)), - ); - - if (isSuccess) { - add(const AppFlowyCloudURLsEvent.didSaveConfig()); - } } }, didSaveConfig: () { @@ -85,8 +62,6 @@ class AppFlowyCloudURLsEvent with _$AppFlowyCloudURLsEvent { const factory AppFlowyCloudURLsEvent.initial() = _Initial; const factory AppFlowyCloudURLsEvent.updateServerUrl(String text) = _ServerUrl; - const factory AppFlowyCloudURLsEvent.updateBaseWebDomain(String text) = - _UpdateBaseWebDomain; const factory AppFlowyCloudURLsEvent.confirmUpdate() = _UpdateConfig; const factory AppFlowyCloudURLsEvent.didSaveConfig() = _DidSaveConfig; } @@ -96,7 +71,6 @@ class AppFlowyCloudURLsState with _$AppFlowyCloudURLsState { const factory AppFlowyCloudURLsState({ required AppFlowyCloudConfiguration config, required String updatedServerUrl, - required String updatedBaseWebDomain, required String? urlError, required bool restartApp, required bool showRestartHint, @@ -107,8 +81,6 @@ class AppFlowyCloudURLsState with _$AppFlowyCloudURLsState { urlError: null, updatedServerUrl: getIt().appflowyCloudConfig.base_url, - updatedBaseWebDomain: - getIt().appflowyCloudConfig.base_web_domain, showRestartHint: getIt() .appflowyCloudConfig .base_url diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart index df880891e9..81c96b3232 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/billing/settings_billing_bloc.dart @@ -1,24 +1,15 @@ -import 'dart:async'; - import 'package:flutter/foundation.dart'; -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:fixnum/fixnum.dart'; +import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; part 'settings_billing_bloc.freezed.dart'; @@ -26,274 +17,86 @@ class SettingsBillingBloc extends Bloc { SettingsBillingBloc({ required this.workspaceId, - required Int64 userId, }) : super(const _Initial()) { - _userService = UserBackendService(userId: userId); - _service = WorkspaceService(workspaceId: workspaceId, userId: userId); - _successListenable = getIt(); - _successListenable.addListener(_onPaymentSuccessful); + _service = WorkspaceService(workspaceId: workspaceId); on((event, emit) async { await event.when( started: () async { emit(const SettingsBillingState.loading()); + final snapshots = await Future.wait([ + UserBackendService.getWorkspaceSubscriptions(), + _service.getBillingPortal(), + ]); + FlowyError? error; - final result = await UserBackendService.getWorkspaceSubscriptionInfo( - workspaceId, - ); - - final subscriptionInfo = result.fold( - (s) => s, + final subscription = snapshots.first.fold( + (s) => + (s as RepeatedWorkspaceSubscriptionPB) + .items + .firstWhereOrNull((i) => i.workspaceId == workspaceId) ?? + WorkspaceSubscriptionPB( + workspaceId: workspaceId, + subscriptionPlan: SubscriptionPlanPB.None, + isActive: true, + ), (e) { + // Not a Cjstomer yet + if (e.code == ErrorCode.InvalidParams) { + return WorkspaceSubscriptionPB( + workspaceId: workspaceId, + subscriptionPlan: SubscriptionPlanPB.None, + isActive: true, + ); + } + error = e; return null; }, ); - if (subscriptionInfo == null || error != null) { - return emit(SettingsBillingState.error(error: error)); - } + final billingPortalResult = snapshots.last; + final billingPortal = billingPortalResult.fold( + (s) => s as BillingPortalPB, + (e) { + // Not a customer yet + if (e.code == ErrorCode.InvalidParams) { + return BillingPortalPB(); + } - if (!_billingPortalCompleter.isCompleted) { - unawaited(_fetchBillingPortal()); - unawaited( - _billingPortalCompleter.future.then( - (result) { - if (isClosed) return; - - result.fold( - (portal) { - _billingPortal = portal; - add( - SettingsBillingEvent.billingPortalFetched( - billingPortal: portal, - ), - ); - }, - (e) => Log.error('Error fetching billing portal: $e'), - ); - }, - ), - ); - } - - emit( - SettingsBillingState.ready( - subscriptionInfo: subscriptionInfo, - billingPortal: _billingPortal, - ), - ); - }, - billingPortalFetched: (billingPortal) async => state.maybeWhen( - orElse: () {}, - ready: (subscriptionInfo, _, plan, isLoading) => emit( - SettingsBillingState.ready( - subscriptionInfo: subscriptionInfo, - billingPortal: billingPortal, - successfulPlanUpgrade: plan, - isLoading: isLoading, - ), - ), - ), - openCustomerPortal: () async { - if (_billingPortalCompleter.isCompleted && _billingPortal != null) { - return afLaunchUrlString(_billingPortal!.url); - } - await _billingPortalCompleter.future; - if (_billingPortal != null) { - await afLaunchUrlString(_billingPortal!.url); - } - }, - addSubscription: (plan) async { - final result = - await _userService.createSubscription(workspaceId, plan); - - result.fold( - (link) => afLaunchUrlString(link.paymentLink), - (f) => Log.error(f.msg, f), - ); - }, - cancelSubscription: (plan, reason) async { - final s = state.mapOrNull(ready: (s) => s); - if (s == null) { - return; - } - - emit(s.copyWith(isLoading: true)); - - final result = - await _userService.cancelSubscription(workspaceId, plan, reason); - final successOrNull = result.fold( - (_) => true, - (f) { - Log.error( - 'Failed to cancel subscription of ${plan.label}: ${f.msg}', - f, - ); + error = e; return null; }, ); - if (successOrNull != true) { - return; + if (subscription == null || billingPortal == null || error != null) { + return emit(SettingsBillingState.error(error: error)); } - final subscriptionInfo = state.mapOrNull( - ready: (s) => s.subscriptionInfo, - ); - - // This is impossible, but for good measure - if (subscriptionInfo == null) { - return; - } - - subscriptionInfo.freeze(); - final newInfo = subscriptionInfo.rebuild((value) { - if (plan.isAddOn) { - value.addOns.removeWhere( - (addon) => addon.addOnSubscription.subscriptionPlan == plan, - ); - } - - if (plan == WorkspacePlanPB.ProPlan && - value.plan == WorkspacePlanPB.ProPlan) { - value.plan = WorkspacePlanPB.FreePlan; - value.planSubscription.freeze(); - value.planSubscription = value.planSubscription.rebuild((sub) { - sub.status = WorkspaceSubscriptionStatusPB.Active; - sub.subscriptionPlan = SubscriptionPlanPB.Free; - }); - } - }); - emit( SettingsBillingState.ready( - subscriptionInfo: newInfo, - billingPortal: _billingPortal, + subscription: subscription, + billingPortal: billingPortal, ), ); }, - paymentSuccessful: (plan) async { - final result = await UserBackendService.getWorkspaceSubscriptionInfo( - workspaceId, - ); - - final subscriptionInfo = result.toNullable(); - if (subscriptionInfo != null) { - emit( - SettingsBillingState.ready( - subscriptionInfo: subscriptionInfo, - billingPortal: _billingPortal, - ), - ); - } - }, - updatePeriod: (plan, interval) async { - final s = state.mapOrNull(ready: (s) => s); - if (s == null) { - return; - } - - emit(s.copyWith(isLoading: true)); - - final result = await _userService.updateSubscriptionPeriod( - workspaceId, - plan, - interval, - ); - final successOrNull = result.fold((_) => true, (f) { - Log.error( - 'Failed to update subscription period of ${plan.label}: ${f.msg}', - f, - ); - return null; - }); - - if (successOrNull != true) { - return emit(s.copyWith(isLoading: false)); - } - - // Fetch new subscription info - final newResult = - await UserBackendService.getWorkspaceSubscriptionInfo( - workspaceId, - ); - - final newSubscriptionInfo = newResult.toNullable(); - if (newSubscriptionInfo != null) { - emit( - SettingsBillingState.ready( - subscriptionInfo: newSubscriptionInfo, - billingPortal: _billingPortal, - ), - ); - } - }, ); }); } late final String workspaceId; late final WorkspaceService _service; - late final UserBackendService _userService; - final _billingPortalCompleter = - Completer>(); - - BillingPortalPB? _billingPortal; - late final SubscriptionSuccessListenable _successListenable; - - @override - Future close() { - _successListenable.removeListener(_onPaymentSuccessful); - return super.close(); - } - - Future _fetchBillingPortal() async { - final billingPortalResult = await _service.getBillingPortal(); - _billingPortalCompleter.complete(billingPortalResult); - } - - Future _onPaymentSuccessful() async => add( - SettingsBillingEvent.paymentSuccessful( - plan: _successListenable.subscribedPlan, - ), - ); } @freezed class SettingsBillingEvent with _$SettingsBillingEvent { const factory SettingsBillingEvent.started() = _Started; - - const factory SettingsBillingEvent.billingPortalFetched({ - required BillingPortalPB billingPortal, - }) = _BillingPortalFetched; - - const factory SettingsBillingEvent.openCustomerPortal() = _OpenCustomerPortal; - - const factory SettingsBillingEvent.addSubscription(SubscriptionPlanPB plan) = - _AddSubscription; - - const factory SettingsBillingEvent.cancelSubscription( - SubscriptionPlanPB plan, { - @Default(null) String? reason, - }) = _CancelSubscription; - - const factory SettingsBillingEvent.paymentSuccessful({ - SubscriptionPlanPB? plan, - }) = _PaymentSuccessful; - - const factory SettingsBillingEvent.updatePeriod({ - required SubscriptionPlanPB plan, - required RecurringIntervalPB interval, - }) = _UpdatePeriod; } @freezed -class SettingsBillingState extends Equatable with _$SettingsBillingState { - const SettingsBillingState._(); - +class SettingsBillingState with _$SettingsBillingState { const factory SettingsBillingState.initial() = _Initial; const factory SettingsBillingState.loading() = _Loading; @@ -303,22 +106,7 @@ class SettingsBillingState extends Equatable with _$SettingsBillingState { }) = _Error; const factory SettingsBillingState.ready({ - required WorkspaceSubscriptionInfoPB subscriptionInfo, + required WorkspaceSubscriptionPB subscription, required BillingPortalPB? billingPortal, - @Default(null) SubscriptionPlanPB? successfulPlanUpgrade, - @Default(false) bool isLoading, }) = _Ready; - - @override - List get props => maybeWhen( - orElse: () => const [], - error: (error) => [error], - ready: (subscription, billingPortal, plan, isLoading) => [ - subscription, - billingPortal, - plan, - isLoading, - ...subscription.addOns, - ], - ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart index 76fec2ecfc..863482cca8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/date_format_ext.dart @@ -1,21 +1,14 @@ import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:easy_localization/easy_localization.dart'; -const _localFmt = 'MM/dd/y'; -const _usFmt = 'y/MM/dd'; -const _isoFmt = 'y-MM-dd'; -const _friendlyFmt = 'MMM dd, y'; -const _dmyFmt = 'dd/MM/y'; +const _localFmt = 'M/d/y'; +const _usFmt = 'y/M/d'; +const _isoFmt = 'y-M-d'; +const _friendlyFmt = 'MMM d, y'; +const _dmyFmt = 'd/M/y'; extension DateFormatter on UserDateFormatPB { - DateFormat get toFormat { - try { - return DateFormat(_toFormat[this] ?? _friendlyFmt); - } catch (_) { - // fallback to en-US - return DateFormat(_toFormat[this] ?? _friendlyFmt, 'en-US'); - } - } + DateFormat get toFormat => DateFormat(_toFormat[this] ?? _friendlyFmt); String formatDate( DateTime date, diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/file_storage/file_storage_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/file_storage/file_storage_listener.dart deleted file mode 100644 index 58560bae03..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/file_storage/file_storage_listener.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/notification_helper.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-storage/notification.pb.dart'; -import 'package:appflowy_backend/rust_stream.dart'; -import 'package:appflowy_result/appflowy_result.dart'; - -class StoregeNotificationParser - extends NotificationParser { - StoregeNotificationParser({ - super.id, - required super.callback, - }) : super( - tyParser: (ty, source) => - source == "storage" ? StorageNotification.valueOf(ty) : null, - errorParser: (bytes) => FlowyError.fromBuffer(bytes), - ); -} - -class StoreageNotificationListener { - StoreageNotificationListener({ - void Function(FlowyError error)? onError, - }) : _parser = StoregeNotificationParser( - callback: ( - StorageNotification ty, - FlowyResult result, - ) { - result.fold( - (data) { - try { - switch (ty) { - case StorageNotification.FileStorageLimitExceeded: - onError?.call(FlowyError.fromBuffer(data)); - break; - case StorageNotification.SingleFileLimitExceeded: - onError?.call(FlowyError.fromBuffer(data)); - break; - } - } catch (e) { - Log.error( - "$StoreageNotificationListener deserialize PB fail", - e, - ); - } - }, - (err) { - Log.error("Error in StoreageNotificationListener", err); - }, - ); - }, - ) { - _subscription = - RustStreamReceiver.listen((observable) => _parser?.parse(observable)); - } - - StoregeNotificationParser? _parser; - StreamSubscription? _subscription; - - Future stop() async { - _parser = null; - await _subscription?.cancel(); - _subscription = null; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart index 26975b00ff..fd5587e322 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/settings_plan_bloc.dart @@ -3,18 +3,18 @@ import 'package:flutter/foundation.dart'; import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart'; import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; import 'package:fixnum/fixnum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; part 'settings_plan_bloc.freezed.dart'; @@ -23,46 +23,65 @@ class SettingsPlanBloc extends Bloc { required this.workspaceId, required Int64 userId, }) : super(const _Initial()) { - _service = WorkspaceService( - workspaceId: workspaceId, - userId: userId, - ); + _service = WorkspaceService(workspaceId: workspaceId); _userService = UserBackendService(userId: userId); _successListenable = getIt(); _successListenable.addListener(_onPaymentSuccessful); on((event, emit) async { await event.when( - started: (withSuccessfulUpgrade, shouldLoad) async { - if (shouldLoad) { - emit(const SettingsPlanState.loading()); - } + started: (withShowSuccessful) async { + emit(const SettingsPlanState.loading()); final snapshots = await Future.wait([ _service.getWorkspaceUsage(), - UserBackendService.getWorkspaceSubscriptionInfo(workspaceId), + UserBackendService.getWorkspaceSubscriptions(), + _service.getBillingPortal(), ]); FlowyError? error; final usageResult = snapshots.first.fold( - (s) => s as WorkspaceUsagePB?, + (s) => s as WorkspaceUsagePB, (f) { error = f; return null; }, ); - final subscriptionInfo = snapshots[1].fold( - (s) => s as WorkspaceSubscriptionInfoPB, + final subscription = snapshots[1].fold( + (s) => + (s as RepeatedWorkspaceSubscriptionPB) + .items + .firstWhereOrNull((i) => i.workspaceId == workspaceId) ?? + WorkspaceSubscriptionPB( + workspaceId: workspaceId, + subscriptionPlan: SubscriptionPlanPB.None, + isActive: true, + ), (f) { error = f; return null; }, ); + final billingPortalResult = snapshots.last; + final billingPortal = billingPortalResult.fold( + (s) => s as BillingPortalPB, + (e) { + // Not a customer yet + if (e.code == ErrorCode.InvalidParams) { + return BillingPortalPB(); + } + + error = e; + return null; + }, + ); + if (usageResult == null || - subscriptionInfo == null || + subscription == null || + billingPortal == null || error != null) { return emit(SettingsPlanState.error(error: error)); } @@ -70,16 +89,18 @@ class SettingsPlanBloc extends Bloc { emit( SettingsPlanState.ready( workspaceUsage: usageResult, - subscriptionInfo: subscriptionInfo, - successfulPlanUpgrade: withSuccessfulUpgrade, + subscription: subscription, + billingPortal: billingPortal, + showSuccessDialog: withShowSuccessful, ), ); - if (withSuccessfulUpgrade != null) { + if (withShowSuccessful) { emit( SettingsPlanState.ready( workspaceUsage: usageResult, - subscriptionInfo: subscriptionInfo, + subscription: subscription, + billingPortal: billingPortal, ), ); } @@ -87,96 +108,25 @@ class SettingsPlanBloc extends Bloc { addSubscription: (plan) async { final result = await _userService.createSubscription( workspaceId, - plan, + SubscriptionPlanPB.Pro, ); result.fold( (pl) => afLaunchUrlString(pl.paymentLink), - (f) => Log.error( - 'Failed to fetch paymentlink for $plan: ${f.msg}', - f, - ), + (f) => Log.error(f.msg, f), ); }, - cancelSubscription: (reason) async { - final newState = state - .mapOrNull(ready: (state) => state) - ?.copyWith(downgradeProcessing: true); - emit(newState ?? state); - - // We can hardcode the subscription plan here because we cannot cancel addons - // on the Plan page - final result = await _userService.cancelSubscription( - workspaceId, - SubscriptionPlanPB.Pro, - reason, - ); - - final successOrNull = result.fold( - (_) => true, - (f) { - Log.error('Failed to cancel subscription of Pro: ${f.msg}', f); - return null; - }, - ); - - if (successOrNull != true) { - return; - } - - final subscriptionInfo = state.mapOrNull( - ready: (s) => s.subscriptionInfo, - ); - - // This is impossible, but for good measure - if (subscriptionInfo == null) { - return; - } - - // We assume their new plan is Free, since we only have Pro plan - // at the moment. - subscriptionInfo.freeze(); - final newInfo = subscriptionInfo.rebuild((value) { - value.plan = WorkspacePlanPB.FreePlan; - value.planSubscription.freeze(); - value.planSubscription = value.planSubscription.rebuild((sub) { - sub.status = WorkspaceSubscriptionStatusPB.Active; - sub.subscriptionPlan = SubscriptionPlanPB.Free; - }); - }); - - // We need to remove unlimited indicator for storage and - // AI usage, if they don't have an addon that changes this behavior. - final usage = state.mapOrNull(ready: (s) => s.workspaceUsage)!; - - usage.freeze(); - final newUsage = usage.rebuild((value) { - if (!newInfo.hasAIMax) { - value.aiResponsesUnlimited = false; - } - - value.storageBytesUnlimited = false; - }); - - emit( - SettingsPlanState.ready( - subscriptionInfo: newInfo, - workspaceUsage: newUsage, - ), - ); + cancelSubscription: () async { + await _userService.cancelSubscription(workspaceId); + add(const SettingsPlanEvent.started()); }, - paymentSuccessful: (plan) { + paymentSuccessful: () { final readyState = state.mapOrNull(ready: (state) => state); if (readyState == null) { return; } - add( - SettingsPlanEvent.started( - withSuccessfulUpgrade: plan, - shouldLoad: false, - ), - ); + add(const SettingsPlanEvent.started(withShowSuccessful: true)); }, ); }); @@ -187,11 +137,9 @@ class SettingsPlanBloc extends Bloc { late final IUserBackendService _userService; late final SubscriptionSuccessListenable _successListenable; - Future _onPaymentSuccessful() async => add( - SettingsPlanEvent.paymentSuccessful( - plan: _successListenable.subscribedPlan, - ), - ); + void _onPaymentSuccessful() { + add(const SettingsPlanEvent.paymentSuccessful()); + } @override Future close() async { @@ -203,20 +151,12 @@ class SettingsPlanBloc extends Bloc { @freezed class SettingsPlanEvent with _$SettingsPlanEvent { const factory SettingsPlanEvent.started({ - @Default(null) SubscriptionPlanPB? withSuccessfulUpgrade, - @Default(true) bool shouldLoad, + @Default(false) bool withShowSuccessful, }) = _Started; - const factory SettingsPlanEvent.addSubscription(SubscriptionPlanPB plan) = _AddSubscription; - - const factory SettingsPlanEvent.cancelSubscription({ - @Default(null) String? reason, - }) = _CancelSubscription; - - const factory SettingsPlanEvent.paymentSuccessful({ - @Default(null) SubscriptionPlanPB? plan, - }) = _PaymentSuccessful; + const factory SettingsPlanEvent.cancelSubscription() = _CancelSubscription; + const factory SettingsPlanEvent.paymentSuccessful() = _PaymentSuccessful; } @freezed @@ -231,8 +171,8 @@ class SettingsPlanState with _$SettingsPlanState { const factory SettingsPlanState.ready({ required WorkspaceUsagePB workspaceUsage, - required WorkspaceSubscriptionInfoPB subscriptionInfo, - @Default(null) SubscriptionPlanPB? successfulPlanUpgrade, - @Default(false) bool downgradeProcessing, + required WorkspaceSubscriptionPB subscription, + required BillingPortalPB? billingPortal, + @Default(false) bool showSuccessDialog, }) = _Ready; } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart index 9d91ade4d3..d6dde9e9c1 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_subscription_ext.dart @@ -1,123 +1,26 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pbserver.dart'; import 'package:easy_localization/easy_localization.dart'; -extension SubscriptionInfoHelpers on WorkspaceSubscriptionInfoPB { - String get label => switch (plan) { - WorkspacePlanPB.FreePlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(), - WorkspacePlanPB.ProPlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(), - WorkspacePlanPB.TeamPlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(), - _ => 'N/A', - }; - - String get info => switch (plan) { - WorkspacePlanPB.FreePlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(), - WorkspacePlanPB.ProPlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(), - WorkspacePlanPB.TeamPlan => - LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(), - _ => 'N/A', - }; - - bool get isBillingPortalEnabled { - if (plan != WorkspacePlanPB.FreePlan || addOns.isNotEmpty) { - return true; - } - - return false; - } -} - -extension AllSubscriptionLabels on SubscriptionPlanPB { - String get label => switch (this) { - SubscriptionPlanPB.Free => +extension SubscriptionLabels on WorkspaceSubscriptionPB { + String get label => switch (subscriptionPlan) { + SubscriptionPlanPB.None => LocaleKeys.settings_planPage_planUsage_currentPlan_freeTitle.tr(), SubscriptionPlanPB.Pro => LocaleKeys.settings_planPage_planUsage_currentPlan_proTitle.tr(), SubscriptionPlanPB.Team => LocaleKeys.settings_planPage_planUsage_currentPlan_teamTitle.tr(), - SubscriptionPlanPB.AiMax => - LocaleKeys.settings_billingPage_addons_aiMax_label.tr(), - SubscriptionPlanPB.AiLocal => - LocaleKeys.settings_billingPage_addons_aiOnDevice_label.tr(), + _ => 'N/A', + }; + + String get info => switch (subscriptionPlan) { + SubscriptionPlanPB.None => + LocaleKeys.settings_planPage_planUsage_currentPlan_freeInfo.tr(), + SubscriptionPlanPB.Pro => + LocaleKeys.settings_planPage_planUsage_currentPlan_proInfo.tr(), + SubscriptionPlanPB.Team => + LocaleKeys.settings_planPage_planUsage_currentPlan_teamInfo.tr(), _ => 'N/A', }; } - -extension WorkspaceSubscriptionStatusExt on WorkspaceSubscriptionInfoPB { - bool get isCanceled => - planSubscription.status == WorkspaceSubscriptionStatusPB.Canceled; -} - -extension WorkspaceAddonsExt on WorkspaceSubscriptionInfoPB { - bool get hasAIMax => - addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiMax); - - bool get hasAIOnDevice => - addOns.any((addon) => addon.type == WorkspaceAddOnPBType.AddOnAiLocal); -} - -/// These have to match [SubscriptionSuccessListenable.subscribedPlan] labels -extension ToRecognizable on SubscriptionPlanPB { - String? toRecognizable() => switch (this) { - SubscriptionPlanPB.Free => 'free', - SubscriptionPlanPB.Pro => 'pro', - SubscriptionPlanPB.Team => 'team', - SubscriptionPlanPB.AiMax => 'ai_max', - SubscriptionPlanPB.AiLocal => 'ai_local', - _ => null, - }; -} - -extension PlanHelper on SubscriptionPlanPB { - /// Returns true if the plan is an add-on and not - /// a workspace plan. - /// - bool get isAddOn => switch (this) { - SubscriptionPlanPB.AiMax => true, - SubscriptionPlanPB.AiLocal => true, - _ => false, - }; - - String get priceMonthBilling => switch (this) { - SubscriptionPlanPB.Free => 'US\$0', - SubscriptionPlanPB.Pro => 'US\$12.5', - SubscriptionPlanPB.Team => 'US\$15', - SubscriptionPlanPB.AiMax => 'US\$10', - SubscriptionPlanPB.AiLocal => 'US\$10', - _ => 'US\$0', - }; - - String get priceAnnualBilling => switch (this) { - SubscriptionPlanPB.Free => 'US\$0', - SubscriptionPlanPB.Pro => 'US\$10', - SubscriptionPlanPB.Team => 'US\$12.5', - SubscriptionPlanPB.AiMax => 'US\$8', - SubscriptionPlanPB.AiLocal => 'US\$8', - _ => 'US\$0', - }; -} - -extension IntervalLabel on RecurringIntervalPB { - String get label => switch (this) { - RecurringIntervalPB.Month => - LocaleKeys.settings_billingPage_monthlyInterval.tr(), - RecurringIntervalPB.Year => - LocaleKeys.settings_billingPage_annualInterval.tr(), - _ => LocaleKeys.settings_billingPage_monthlyInterval.tr(), - }; - - String get priceInfo => switch (this) { - RecurringIntervalPB.Month => - LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(), - RecurringIntervalPB.Year => - LocaleKeys.settings_billingPage_annualPriceInfo.tr(), - _ => LocaleKeys.settings_billingPage_monthlyPriceInfo.tr(), - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart index ddaca15f5c..bc309b60c5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/plan/workspace_usage_ext.dart @@ -1,25 +1,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:intl/intl.dart'; - -final _storageNumberFormat = NumberFormat() - ..maximumFractionDigits = 2 - ..minimumFractionDigits = 0; extension PresentableUsage on WorkspaceUsagePB { - String get totalBlobInGb { - if (storageBytesLimit == 0) { - return '0'; - } - return _storageNumberFormat - .format(storageBytesLimit.toInt() / (1024 * 1024 * 1024)); - } - - /// We use [NumberFormat] to format the current blob in GB. - /// - /// Where the [totalBlobBytes] is the total blob bytes in bytes. - /// And [NumberFormat.maximumFractionDigits] is set to 2. - /// And [NumberFormat.minimumFractionDigits] is set to 0. - /// + String get totalBlobInGb => + (totalBlobBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString(); String get currentBlobInGb => - _storageNumberFormat.format(storageBytes.toInt() / 1024 / 1024 / 1024); + (totalBlobBytes.toInt() / 1024 / 1024 / 1024).round().toString(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart index 7a1d3efc45..517b943b35 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/setting_file_importer_bloc.dart @@ -1,7 +1,4 @@ -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/plugins/database/application/defines.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -22,17 +19,9 @@ class SettingFileImportBloc importAppFlowyDataFolder: (String path) async { final formattedDate = DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now()); - final spaceId = - await getIt().get(KVKeys.lastOpenedSpaceId); - final payload = ImportAppFlowyDataPB.create() ..path = path - ..importContainerName = "import_$formattedDate"; - - if (spaceId != null) { - payload.parentViewId = spaceId; - } - + ..importContainerName = "appflowy_import_$formattedDate"; emit( state.copyWith(loadingState: const LoadingState.loading()), ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart index 83588f0079..7395dce731 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -1,11 +1,8 @@ import 'package:appflowy/user/application/user_listener.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -16,31 +13,25 @@ enum SettingsPage { account, workspace, manageData, - shortcuts, - ai, plan, billing, - sites, // OLD notifications, cloud, + shortcuts, member, featureFlags, } class SettingsDialogBloc extends Bloc { - SettingsDialogBloc( - this.userProfile, - this.currentWorkspaceMemberRole, { - SettingsPage? initPage, - }) : _userListener = UserListener(userProfile: userProfile), - super(SettingsDialogState.initial(userProfile, initPage)) { + SettingsDialogBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsDialogState.initial(userProfile)) { _dispatch(); } final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; final UserListener _userListener; @override @@ -55,14 +46,6 @@ class SettingsDialogBloc await event.when( initial: () async { _userListener.start(onProfileUpdated: _profileUpdated); - - final isBillingEnabled = await _isBillingEnabled( - userProfile, - currentWorkspaceMemberRole, - ); - if (isBillingEnabled) { - emit(state.copyWith(isBillingEnabled: true)); - } }, didReceiveUserProfile: (UserProfilePB newUserProfile) { emit(state.copyWith(userProfile: newUserProfile)); @@ -84,42 +67,6 @@ class SettingsDialogBloc (err) => Log.error(err), ); } - - Future _isBillingEnabled( - UserProfilePB userProfile, [ - AFRolePB? currentWorkspaceMemberRole, - ]) async { - if ([ - AuthTypePB.Local, - ].contains(userProfile.workspaceAuthType)) { - return false; - } - - if (currentWorkspaceMemberRole == null || - currentWorkspaceMemberRole != AFRolePB.Owner) { - return false; - } - - if (kDebugMode) { - return true; - } - - final result = await UserEventGetCloudConfig().send(); - return result.fold( - (cloudSetting) { - final whiteList = [ - "https://beta.appflowy.cloud", - "https://test.appflowy.cloud", - ]; - - return whiteList.contains(cloudSetting.serverUrl); - }, - (err) { - Log.error("Failed to get cloud config: $err"); - return false; - }, - ); - } } @freezed @@ -137,16 +84,11 @@ class SettingsDialogState with _$SettingsDialogState { const factory SettingsDialogState({ required UserProfilePB userProfile, required SettingsPage page, - required bool isBillingEnabled, }) = _SettingsDialogState; - factory SettingsDialogState.initial( - UserProfilePB userProfile, - SettingsPage? page, - ) => + factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState( userProfile: userProfile, - page: page ?? SettingsPage.account, - isBillingEnabled: false, + page: SettingsPage.account, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart index e890959949..7cf81b3bfb 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/share/export_service.dart @@ -12,12 +12,4 @@ class BackendExportService { final payload = DatabaseViewIdPB.create()..value = viewId; return DatabaseEventExportCSV(payload).send(); } - - static Future> - exportDatabaseAsRawData( - String viewId, - ) async { - final payload = DatabaseViewIdPB.create()..value = viewId; - return DatabaseEventExportRawDatabaseData(payload).send(); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/share/import_service.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/share/import_service.dart index 205a61f7e3..34ea16e52f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/share/import_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/share/import_service.dart @@ -1,42 +1,37 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; import 'package:appflowy_result/appflowy_result.dart'; -class ImportPayload { - ImportPayload({ - required this.name, - required this.data, - required this.layout, - }); - - final String name; - final List data; - final ViewLayoutPB layout; -} - class ImportBackendService { - static Future> importPages( + static Future> importData( + List data, + String name, String parentViewId, - List values, + ImportTypePB importType, ) async { - final request = ImportPayloadPB( - parentViewId: parentViewId, - items: values, - ); - - return FolderEventImportData(request).send(); - } - - static Future> importZipFiles( - List values, - ) async { - for (final value in values) { - final result = await FolderEventImportZipFile(value).send(); - if (result.isFailure) { - return result; - } - } - return FlowyResult.success(null); + final payload = ImportPB.create() + ..data = data + ..parentViewId = parentViewId + ..viewLayout = importType.toLayout() + ..name = name + ..importType = importType; + return FolderEventImportData(payload).send(); + } +} + +extension on ImportTypePB { + ViewLayoutPB toLayout() { + switch (this) { + case ImportTypePB.HistoryDocument: + return ViewLayoutPB.Document; + case ImportTypePB.HistoryDatabase || + ImportTypePB.CSV || + ImportTypePB.RawDatabase: + return ViewLayoutPB.Grid; + default: + throw UnimplementedError('Unsupported import type $this'); + } } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart index 569b4a4ea4..07794e05ac 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -124,11 +124,9 @@ class ShortcutsCubit extends Cubit { // check if currentShortcut is a codeblock shortcut. final isCodeBlockCommand = currentShortcut.isCodeBlockCommand; - for (final shortcut in state.commandShortcutEvents) { - final keybindings = shortcut.command.split(','); - if (keybindings.contains(command) && - shortcut.isCodeBlockCommand == isCodeBlockCommand) { - return shortcut; + for (final e in state.commandShortcutEvents) { + if (e.command == command && e.isCodeBlockCommand == isCodeBlockCommand) { + return e; } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart index af95d5af5a..35809ac585 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_service.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/application_data_storage.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -53,8 +53,8 @@ class SettingsShortcutService { } } - // Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List]. - // This list needs to be converted to List. This function is intended to facilitate the same. + /// Extracts shortcuts from the saved json file. The shortcuts in the saved file consist of [List]. + /// This list needs to be converted to List. This function is intended to facilitate the same. List getShortcutsFromJson(String savedJson) { final shortcuts = EditorShortcuts.fromJson(jsonDecode(savedJson)); return shortcuts.commandShortcuts; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart new file mode 100644 index 0000000000..9308a06a98 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_setting_bloc.dart @@ -0,0 +1,103 @@ +import 'package:appflowy/env/backend_env.dart'; +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/plugins/database/application/defines.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'cloud_setting_listener.dart'; + +part 'supabase_cloud_setting_bloc.freezed.dart'; + +class SupabaseCloudSettingBloc + extends Bloc { + SupabaseCloudSettingBloc({ + required CloudSettingPB setting, + }) : _listener = UserCloudConfigListener(), + super(SupabaseCloudSettingState.initial(setting)) { + _dispatch(); + } + + final UserCloudConfigListener _listener; + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } + + void _dispatch() { + on( + (event, emit) async { + await event.when( + initial: () async { + _listener.start( + onSettingChanged: (result) { + if (isClosed) { + return; + } + result.fold( + (setting) => + add(SupabaseCloudSettingEvent.didReceiveSetting(setting)), + (error) => Log.error(error), + ); + }, + ); + }, + enableSync: (bool enable) async { + final update = UpdateCloudConfigPB.create()..enableSync = enable; + await updateCloudConfig(update); + }, + didReceiveSetting: (CloudSettingPB setting) { + emit( + state.copyWith( + setting: setting, + loadingState: LoadingState.finish(FlowyResult.success(null)), + ), + ); + }, + enableEncrypt: (bool enable) { + final update = UpdateCloudConfigPB.create()..enableEncrypt = enable; + updateCloudConfig(update); + emit(state.copyWith(loadingState: const LoadingState.loading())); + }, + ); + }, + ); + } + + Future updateCloudConfig(UpdateCloudConfigPB setting) async { + await UserEventSetCloudConfig(setting).send(); + } +} + +@freezed +class SupabaseCloudSettingEvent with _$SupabaseCloudSettingEvent { + const factory SupabaseCloudSettingEvent.initial() = _Initial; + const factory SupabaseCloudSettingEvent.didReceiveSetting( + CloudSettingPB setting, + ) = _DidSyncSupabaseConfig; + const factory SupabaseCloudSettingEvent.enableSync(bool enable) = _EnableSync; + const factory SupabaseCloudSettingEvent.enableEncrypt(bool enable) = + _EnableEncrypt; +} + +@freezed +class SupabaseCloudSettingState with _$SupabaseCloudSettingState { + const factory SupabaseCloudSettingState({ + required LoadingState loadingState, + required SupabaseConfiguration config, + required CloudSettingPB setting, + }) = _SupabaseCloudSettingState; + + factory SupabaseCloudSettingState.initial(CloudSettingPB setting) => + SupabaseCloudSettingState( + loadingState: LoadingState.finish(FlowyResult.success(null)), + setting: setting, + config: getIt().supabaseConfig, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart new file mode 100644 index 0000000000..fdd4cbef21 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/supabase_cloud_urls_bloc.dart @@ -0,0 +1,128 @@ +import 'package:appflowy/env/backend_env.dart'; +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'appflowy_cloud_setting_bloc.dart'; + +part 'supabase_cloud_urls_bloc.freezed.dart'; + +class SupabaseCloudURLsBloc + extends Bloc { + SupabaseCloudURLsBloc() : super(SupabaseCloudURLsState.initial()) { + on((event, emit) async { + await event.when( + updateUrl: (String url) { + emit( + state.copyWith( + updatedUrl: url, + showRestartHint: url.isNotEmpty && state.upatedAnonKey.isNotEmpty, + urlError: null, + ), + ); + }, + updateAnonKey: (String anonKey) { + emit( + state.copyWith( + upatedAnonKey: anonKey, + showRestartHint: + anonKey.isNotEmpty && state.updatedUrl.isNotEmpty, + anonKeyError: null, + ), + ); + }, + confirmUpdate: () async { + if (state.updatedUrl.isEmpty) { + emit( + state.copyWith( + urlError: + LocaleKeys.settings_menu_cloudSupabaseUrlCanNotBeEmpty.tr(), + anonKeyError: null, + restartApp: false, + ), + ); + return; + } + + if (state.upatedAnonKey.isEmpty) { + emit( + state.copyWith( + urlError: null, + anonKeyError: LocaleKeys + .settings_menu_cloudSupabaseAnonKeyCanNotBeEmpty + .tr(), + restartApp: false, + ), + ); + return; + } + + validateUrl(state.updatedUrl).fold( + (_) async { + await useSupabaseCloud( + url: state.updatedUrl, + anonKey: state.upatedAnonKey, + ); + + add(const SupabaseCloudURLsEvent.didSaveConfig()); + }, + (error) => emit(state.copyWith(urlError: error)), + ); + }, + didSaveConfig: () { + emit( + state.copyWith( + urlError: null, + anonKeyError: null, + restartApp: true, + ), + ); + }, + ); + }); + } + + Future updateCloudConfig(UpdateCloudConfigPB setting) async { + await UserEventSetCloudConfig(setting).send(); + } +} + +@freezed +class SupabaseCloudURLsEvent with _$SupabaseCloudURLsEvent { + const factory SupabaseCloudURLsEvent.updateUrl(String text) = _UpdateUrl; + const factory SupabaseCloudURLsEvent.updateAnonKey(String text) = + _UpdateAnonKey; + const factory SupabaseCloudURLsEvent.confirmUpdate() = _UpdateConfig; + const factory SupabaseCloudURLsEvent.didSaveConfig() = _DidSaveConfig; +} + +@freezed +class SupabaseCloudURLsState with _$SupabaseCloudURLsState { + const factory SupabaseCloudURLsState({ + required SupabaseConfiguration config, + required String updatedUrl, + required String upatedAnonKey, + required String? urlError, + required String? anonKeyError, + required bool restartApp, + required bool showRestartHint, + }) = _SupabaseCloudURLsState; + + factory SupabaseCloudURLsState.initial() { + final config = getIt().supabaseConfig; + return SupabaseCloudURLsState( + updatedUrl: config.url, + upatedAnonKey: config.anon_key, + urlError: null, + anonKeyError: null, + restartApp: false, + showRestartHint: config.url.isNotEmpty && config.anon_key.isNotEmpty, + config: config, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index b8081cb2d5..3655f8a20d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -22,7 +22,7 @@ class WorkspaceSettingsBloc try { final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); + await _userService!.getCurrentWorkspace().getOrThrow(); final workspaces = await _userService!.getWorkspaces().getOrThrow(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart deleted file mode 100644 index 56d6ae8cc8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/billing/sidebar_plan_bloc.dart +++ /dev/null @@ -1,245 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/settings/file_storage/file_storage_listener.dart'; -import 'package:appflowy/workspace/application/subscription_success_listenable/subscription_success_listenable.dart'; -import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/dispatch/error.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; -import 'package:bloc/bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'sidebar_plan_bloc.freezed.dart'; - -class SidebarPlanBloc extends Bloc { - SidebarPlanBloc() : super(const SidebarPlanState()) { - // 1. Listen to user subscription payment callback. After user client 'Open AppFlowy', this listenable will be triggered. - _subscriptionListener = getIt(); - _subscriptionListener.addListener(_onPaymentSuccessful); - - // 2. Listen to the storage notification - _storageListener = StoreageNotificationListener( - onError: (error) { - if (!isClosed) { - add(SidebarPlanEvent.receiveError(error)); - } - }, - ); - - // 3. Listen to specific error codes - _globalErrorListener = GlobalErrorCodeNotifier.add( - onError: (error) { - if (!isClosed) { - add(SidebarPlanEvent.receiveError(error)); - } - }, - onErrorIf: (error) { - const relevantErrorCodes = { - ErrorCode.AIResponseLimitExceeded, - ErrorCode.FileStorageLimitExceeded, - }; - return relevantErrorCodes.contains(error.code); - }, - ); - - on(_handleEvent); - } - - void _onPaymentSuccessful() { - final plan = _subscriptionListener.subscribedPlan; - Log.info("Subscription success listenable triggered: $plan"); - - if (!isClosed) { - // Notify the user that they have switched to a new plan. It would be better if we use websocket to - // notify the client when plan switching. - if (state.workspaceId != null) { - final payload = SuccessWorkspaceSubscriptionPB( - workspaceId: state.workspaceId, - ); - - if (plan != null) { - payload.plan = plan; - } - - UserEventNotifyDidSwitchPlan(payload).send().then((result) { - result.fold( - // After the user has switched to a new plan, we need to refresh the workspace usage. - (_) => _checkWorkspaceUsage(), - (error) => Log.error("NotifyDidSwitchPlan failed: $error"), - ); - }); - } else { - Log.error( - "Unexpected empty workspace id when subscription success listenable triggered. It should not happen. If happens, it must be a bug", - ); - } - } - } - - Future dispose() async { - if (_globalErrorListener != null) { - GlobalErrorCodeNotifier.remove(_globalErrorListener!); - } - _subscriptionListener.removeListener(_onPaymentSuccessful); - await _storageListener?.stop(); - _storageListener = null; - } - - ErrorListener? _globalErrorListener; - StoreageNotificationListener? _storageListener; - late final SubscriptionSuccessListenable _subscriptionListener; - - Future _handleEvent( - SidebarPlanEvent event, - Emitter emit, - ) async { - await event.when( - receiveError: (FlowyError error) async { - if (error.code == ErrorCode.AIResponseLimitExceeded) { - emit( - state.copyWith( - tierIndicator: const SidebarToastTierIndicator.aiMaxiLimitHit(), - ), - ); - } else if (error.code == ErrorCode.FileStorageLimitExceeded) { - emit( - state.copyWith( - tierIndicator: const SidebarToastTierIndicator.storageLimitHit(), - ), - ); - } else if (error.code == ErrorCode.SingleUploadLimitExceeded) { - emit( - state.copyWith( - tierIndicator: - const SidebarToastTierIndicator.singleFileLimitHit(), - ), - ); - } else { - Log.error("Unhandle Unexpected error: $error"); - } - }, - init: (String workspaceId, UserProfilePB userProfile) { - emit( - state.copyWith( - workspaceId: workspaceId, - userProfile: userProfile, - ), - ); - - _checkWorkspaceUsage(); - }, - updateWorkspaceUsage: (WorkspaceUsagePB usage) { - // when the user's storage bytes are limited, show the upgrade tier button - if (!usage.storageBytesUnlimited) { - if (usage.storageBytes >= usage.storageBytesLimit) { - add( - const SidebarPlanEvent.updateTierIndicator( - SidebarToastTierIndicator.storageLimitHit(), - ), - ); - - /// Checks if the user needs to upgrade to the Pro Plan. - /// If the user needs to upgrade, it means they don't need to enable the AI max tier. - /// This function simply returns without performing any further actions. - return; - } - } - - // when user's AI responses are limited, show the AI max tier button. - if (!usage.aiResponsesUnlimited) { - if (usage.aiResponsesCount >= usage.aiResponsesCountLimit) { - add( - const SidebarPlanEvent.updateTierIndicator( - SidebarToastTierIndicator.aiMaxiLimitHit(), - ), - ); - return; - } - } - - // hide the tier indicator - add( - const SidebarPlanEvent.updateTierIndicator( - SidebarToastTierIndicator.loading(), - ), - ); - }, - updateTierIndicator: (SidebarToastTierIndicator indicator) { - emit( - state.copyWith( - tierIndicator: indicator, - ), - ); - }, - changedWorkspace: (workspaceId) { - emit(state.copyWith(workspaceId: workspaceId)); - _checkWorkspaceUsage(); - }, - ); - } - - Future _checkWorkspaceUsage() async { - if (state.workspaceId == null || state.userProfile == null) { - return; - } - - await WorkspaceService( - workspaceId: state.workspaceId!, - userId: state.userProfile!.id, - ).getWorkspaceUsage().then((result) { - result.fold( - (usage) { - if (!isClosed && usage != null) { - add(SidebarPlanEvent.updateWorkspaceUsage(usage)); - } - }, - (error) => Log.error("Failed to get workspace usage: $error"), - ); - }); - } -} - -@freezed -class SidebarPlanEvent with _$SidebarPlanEvent { - const factory SidebarPlanEvent.init( - String workspaceId, - UserProfilePB userProfile, - ) = _Init; - const factory SidebarPlanEvent.updateWorkspaceUsage( - WorkspaceUsagePB usage, - ) = _UpdateWorkspaceUsage; - const factory SidebarPlanEvent.updateTierIndicator( - SidebarToastTierIndicator indicator, - ) = _UpdateTierIndicator; - const factory SidebarPlanEvent.receiveError(FlowyError error) = _ReceiveError; - - const factory SidebarPlanEvent.changedWorkspace({ - required String workspaceId, - }) = _ChangedWorkspace; -} - -@freezed -class SidebarPlanState with _$SidebarPlanState { - const factory SidebarPlanState({ - FlowyError? error, - UserProfilePB? userProfile, - String? workspaceId, - WorkspaceUsagePB? usage, - @Default(SidebarToastTierIndicator.loading()) - SidebarToastTierIndicator tierIndicator, - }) = _SidebarPlanState; -} - -@freezed -class SidebarToastTierIndicator with _$SidebarToastTierIndicator { - const factory SidebarToastTierIndicator.storageLimitHit() = _StorageLimitHit; - const factory SidebarToastTierIndicator.singleFileLimitHit() = - _SingleFileLimitHit; - const factory SidebarToastTierIndicator.aiMaxiLimitHit() = _aiMaxLimitHit; - const factory SidebarToastTierIndicator.loading() = _Loading; -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart index 609b9ce0ae..c85f3bd0b0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -12,8 +12,7 @@ part 'folder_bloc.freezed.dart'; enum FolderSpaceType { favorite, private, - public, - unknown; + public; ViewSectionPB get toViewSectionPB { switch (this) { @@ -22,7 +21,6 @@ enum FolderSpaceType { case FolderSpaceType.public: return ViewSectionPB.Public; case FolderSpaceType.favorite: - case FolderSpaceType.unknown: throw UnimplementedError(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart index 9de0c582cd..63872ff12f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_bloc.dart @@ -3,28 +3,26 @@ import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/shared/list_extension.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/application/workspace/prelude.dart'; import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:universal_platform/universal_platform.dart'; part 'space_bloc.freezed.dart'; @@ -62,19 +60,19 @@ class SidebarSection { /// The [SpaceBloc] is responsible for /// managing the root views in different sections of the workspace. class SpaceBloc extends Bloc { - SpaceBloc({ - required this.userProfile, - required this.workspaceId, - }) : super(SpaceState.initial()) { + SpaceBloc() : super(SpaceState.initial()) { on( (event, emit) async { await event.when( - initial: (openFirstPage) async { - this.openFirstPage = openFirstPage; - + initial: (userProfile, workspaceId, openFirstPage) async { _initial(userProfile, workspaceId); final (spaces, publicViews, privateViews) = await _getSpaces(); + final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog( + spaces: spaces, + publicViews: publicViews, + privateViews: privateViews, + ); final currentSpace = await _getLastOpenedSpace(spaces); final isExpanded = await _getSpaceExpandStatus(currentSpace); @@ -83,16 +81,19 @@ class SpaceBloc extends Bloc { spaces: spaces, currentSpace: currentSpace, isExpanded: isExpanded, - shouldShowUpgradeDialog: false, - isInitialized: true, + shouldShowUpgradeDialog: shouldShowUpgradeDialog, ), ); + if (shouldShowUpgradeDialog) { + if (!integrationMode().isTest) { + add(const SpaceEvent.migrate()); + } + } + if (openFirstPage) { if (currentSpace != null) { - if (!isClosed) { - add(SpaceEvent.open(currentSpace)); - } + add(SpaceEvent.open(currentSpace)); } } }, @@ -102,7 +103,6 @@ class SpaceBloc extends Bloc { iconColor, permission, createNewPageByDefault, - openAfterCreate, ) async { final space = await _createSpace( name: name, @@ -110,9 +110,6 @@ class SpaceBloc extends Bloc { iconColor: iconColor, permission: permission, ); - - Log.info('create space: $space'); - if (space != null) { emit( state.copyWith( @@ -121,18 +118,14 @@ class SpaceBloc extends Bloc { ), ); add(SpaceEvent.open(space)); - Log.info('open space: ${space.name}(${space.id})'); if (createNewPageByDefault) { add( SpaceEvent.createPage( - name: '', + name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), index: 0, - layout: ViewLayoutPB.Document, - openAfterCreate: openAfterCreate, ), ); - Log.info('create page: ${space.name}(${space.id})'); } } }, @@ -140,40 +133,21 @@ class SpaceBloc extends Bloc { if (state.spaces.length <= 1) { return; } - final deletedSpace = space ?? state.currentSpace; if (deletedSpace == null) { return; } - - await ViewBackendService.deleteView(viewId: deletedSpace.id); - - Log.info('delete space: ${deletedSpace.name}(${deletedSpace.id})'); + await ViewBackendService.delete(viewId: deletedSpace.id); }, rename: (space, name) async { - add( - SpaceEvent.update( - space: space, - name: name, - icon: space.spaceIcon, - iconColor: space.spaceIconColor, - permission: space.spacePermission, - ), - ); + add(SpaceEvent.update(name: name)); }, - changeIcon: (space, icon, iconColor) async { - add( - SpaceEvent.update( - space: space, - icon: icon, - iconColor: iconColor, - ), - ); + changeIcon: (icon, iconColor) async { + add(SpaceEvent.update(icon: icon, iconColor: iconColor)); }, - update: (space, name, icon, iconColor, permission) async { - space ??= state.currentSpace; + update: (name, icon, iconColor, permission) async { + final space = state.currentSpace; if (space == null) { - Log.error('update space failed, space is null'); return; } @@ -202,29 +176,6 @@ class SpaceBloc extends Bloc { viewId: space.id, extra: jsonEncode(merged), ); - - Log.info( - 'update space: ${space.name}(${space.id}), merged: $merged', - ); - } catch (e) { - Log.error('Failed to migrating cover: $e'); - } - } else if (icon == null) { - try { - final extra = space.extra; - final Map current = extra.isNotEmpty == true - ? jsonDecode(extra) - : {}; - current.remove(ViewExtKeys.spaceIconKey); - current.remove(ViewExtKeys.spaceIconColorKey); - await ViewBackendService.updateView( - viewId: space.id, - extra: jsonEncode(current), - ); - - Log.info( - 'update space: ${space.name}(${space.id}), current: $current', - ); } catch (e) { Log.error('Failed to migrating cover: $e'); } @@ -240,31 +191,13 @@ class SpaceBloc extends Bloc { open: (space) async { await _openSpace(space); final isExpanded = await _getSpaceExpandStatus(space); - final views = await ViewBackendService.getChildViews( - viewId: space.id, - ); - final currentSpace = views.fold( - (views) { - space.freeze(); - return space.rebuild((b) { - b.childViews.clear(); - b.childViews.addAll(views); - }); - }, - (_) => space, - ); - emit( - state.copyWith( - currentSpace: currentSpace, - isExpanded: isExpanded, - ), - ); + emit(state.copyWith(currentSpace: space, isExpanded: isExpanded)); // don't open the page automatically on mobile - if (UniversalPlatform.isDesktop) { + if (PlatformExtension.isDesktop) { // open the first page by default - if (currentSpace.childViews.isNotEmpty) { - final firstPage = currentSpace.childViews.first; + if (space.childViews.isNotEmpty) { + final firstPage = space.childViews.first; emit( state.copyWith( lastCreatedPage: firstPage, @@ -283,7 +216,7 @@ class SpaceBloc extends Bloc { await _setSpaceExpandStatus(space, isExpanded); emit(state.copyWith(isExpanded: isExpanded)); }, - createPage: (name, layout, index, openAfterCreate) async { + createPage: (name, index) async { final parentViewId = state.currentSpace?.id; if (parentViewId == null) { return; @@ -291,16 +224,15 @@ class SpaceBloc extends Bloc { final result = await ViewBackendService.createView( name: name, - layoutType: layout, + layoutType: ViewLayoutPB.Document, parentViewId: parentViewId, index: index, - openAfterCreate: openAfterCreate, ); result.fold( (view) { emit( state.copyWith( - lastCreatedPage: openAfterCreate ? view : null, + lastCreatedPage: view, createPageResult: FlowyResult.success(null), ), ); @@ -318,7 +250,6 @@ class SpaceBloc extends Bloc { didReceiveSpaceUpdate: () async { final (spaces, _, _) = await _getSpaces(); final currentSpace = await _getLastOpenedSpace(spaces); - emit( state.copyWith( spaces: spaces, @@ -326,8 +257,8 @@ class SpaceBloc extends Bloc { ), ); }, - reset: (userProfile, workspaceId, openFirstPage) async { - if (this.workspaceId == workspaceId) { + reset: (userProfile, workspaceId) async { + if (workspaceId == _workspaceId) { return; } @@ -335,7 +266,9 @@ class SpaceBloc extends Bloc { add( SpaceEvent.initial( - openFirstPage: openFirstPage, + userProfile, + workspaceId, + openFirstPage: true, ), ); }, @@ -358,36 +291,14 @@ class SpaceBloc extends Bloc { final nextSpace = spaces[nextIndex]; add(SpaceEvent.open(nextSpace)); }, - duplicate: (space) async { - space ??= state.currentSpace; - if (space == null) { - Log.error('duplicate space failed, space is null'); - return; - } - - Log.info('duplicate space: ${space.name}(${space.id})'); - - emit(state.copyWith(isDuplicatingSpace: true)); - - final newSpace = await _duplicateSpace(space); - // open the duplicated space - if (newSpace != null) { - add(const SpaceEvent.didReceiveSpaceUpdate()); - add(SpaceEvent.open(newSpace)); - } - - emit(state.copyWith(isDuplicatingSpace: false)); - }, ); }, ); } late WorkspaceService _workspaceService; - late String workspaceId; - late UserProfilePB userProfile; + String? _workspaceId; WorkspaceSectionsListener? _listener; - bool openFirstPage = false; @override Future close() async { @@ -401,9 +312,8 @@ class SpaceBloc extends Bloc { if (sectionViews == null || sectionViews.views.isEmpty) { return ([], [], []); } - - final publicViews = sectionViews.publicViews.unique((e) => e.id); - final privateViews = sectionViews.privateViews.unique((e) => e.id); + final publicViews = sectionViews.publicViews; + final privateViews = sectionViews.privateViews; final publicSpaces = publicViews.where((e) => e.isSpace); final privateSpaces = privateViews.where((e) => e.isSpace); @@ -423,22 +333,25 @@ class SpaceBloc extends Bloc { SpacePermission.private => ViewSectionPB.Private, }; - final extra = { - ViewExtKeys.isSpaceKey: true, - ViewExtKeys.spaceIconKey: icon, - ViewExtKeys.spaceIconColorKey: iconColor, - ViewExtKeys.spacePermissionKey: permission.index, - ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch, - }; final result = await _workspaceService.createView( name: name, viewSection: section, - setAsCurrent: true, + setAsCurrent: false, viewId: viewId, - extra: jsonEncode(extra), ); return await result.fold((space) async { Log.info('Space created: $space'); + final extra = { + ViewExtKeys.isSpaceKey: true, + ViewExtKeys.spaceIconKey: icon, + ViewExtKeys.spaceIconColorKey: iconColor, + ViewExtKeys.spacePermissionKey: permission.index, + ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch, + }; + await ViewBackendService.updateView( + viewId: space.id, + extra: jsonEncode(extra), + ); return space; }, (error) { Log.error('Failed to create space: $error'); @@ -474,22 +387,14 @@ class SpaceBloc extends Bloc { } void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService( - workspaceId: workspaceId, - userId: userProfile.id, - ); - - this.userProfile = userProfile; - this.workspaceId = workspaceId; + _workspaceService = WorkspaceService(workspaceId: workspaceId); + _workspaceId = workspaceId; _listener = WorkspaceSectionsListener( user: userProfile, workspaceId: workspaceId, )..start( sectionChanged: (result) async { - if (isClosed) { - return; - } add(const SpaceEvent.didReceiveSpaceUpdate()); }, ); @@ -499,8 +404,7 @@ class SpaceBloc extends Bloc { _listener?.stop(); _listener = null; - this.userProfile = userProfile; - this.workspaceId = workspaceId; + _initial(userProfile, workspaceId); } Future _getLastOpenedSpace(List spaces) async { @@ -545,7 +449,7 @@ class SpaceBloc extends Bloc { Future _getSpaceExpandStatus(ViewPB? space) async { if (space == null) { - return true; + return false; } return getIt().get(KVKeys.expandedViews).then((result) { @@ -558,12 +462,15 @@ class SpaceBloc extends Bloc { } Future migrate({bool auto = true}) async { + if (_workspaceId == null) { + return false; + } try { final user = await UserBackendService.getCurrentUserProfile().getOrThrow(); final service = UserBackendService(userId: user.id); final members = - await service.getWorkspaceMembers(workspaceId).getOrThrow(); + await service.getWorkspaceMembers(_workspaceId!).getOrThrow(); final isOwner = members.items .any((e) => e.role == AFRolePB.Owner && e.email == user.email); @@ -581,23 +488,13 @@ class SpaceBloc extends Bloc { (e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll, ); publicViews = publicViews.where((e) => !e.isSpace).toList(); - - for (final view in publicViews) { - Log.info( - 'migrating: the public view should be migrated: ${view.name}(${view.id})', - ); - } - // if there is already a public space, don't migrate the public space // only migrate the public space if there are any public views if (publicViews.isEmpty || containsPublicSpace) { return true; } - final viewId = fixedUuid( - user.id.toInt() + workspaceId.hashCode, - UuidType.publicSpace, - ); + final viewId = fixedUuid(user.id.toInt(), UuidType.publicSpace); final publicSpace = await _createSpace( name: 'Shared', icon: builtInSpaceIcons.first, @@ -633,13 +530,6 @@ class SpaceBloc extends Bloc { (e) => e.isSpace && e.spacePermission == SpacePermission.private, ); privateViews = privateViews.where((e) => !e.isSpace).toList(); - - for (final view in privateViews) { - Log.info( - 'migrating: the private view should be migrated: ${view.name}(${view.id})', - ); - } - if (privateViews.isEmpty || containsPrivateSpace) { return true; } @@ -697,42 +587,13 @@ class SpaceBloc extends Bloc { return false; } - - Future _duplicateSpace(ViewPB space) async { - // if the space is not duplicated, try to create a new space - final icon = space.spaceIcon.orDefault(builtInSpaceIcons.first); - final iconColor = space.spaceIconColor.orDefault(builtInSpaceColors.first); - final newSpace = await _createSpace( - name: '${space.name} (copy)', - icon: icon, - iconColor: iconColor, - permission: space.spacePermission, - ); - - if (newSpace == null) { - return null; - } - - for (final view in space.childViews) { - await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: true, - syncAfterDuplicate: true, - includeChildren: true, - parentViewId: newSpace.id, - suffix: '', - ); - } - - Log.info('Space duplicated: $newSpace'); - - return newSpace; - } } @freezed class SpaceEvent with _$SpaceEvent { - const factory SpaceEvent.initial({ + const factory SpaceEvent.initial( + UserProfilePB userProfile, + String workspaceId, { required bool openFirstPage, }) = _Initial; const factory SpaceEvent.create({ @@ -741,22 +602,11 @@ class SpaceEvent with _$SpaceEvent { required String iconColor, required SpacePermission permission, required bool createNewPageByDefault, - required bool openAfterCreate, }) = _Create; - const factory SpaceEvent.rename({ - required ViewPB space, - required String name, - }) = _Rename; - const factory SpaceEvent.changeIcon({ - ViewPB? space, - String? icon, - String? iconColor, - }) = _ChangeIcon; - const factory SpaceEvent.duplicate({ - ViewPB? space, - }) = _Duplicate; + const factory SpaceEvent.rename(ViewPB space, String name) = _Rename; + const factory SpaceEvent.changeIcon(String icon, String iconColor) = + _ChangeIcon; const factory SpaceEvent.update({ - ViewPB? space, String? name, String? icon, String? iconColor, @@ -766,16 +616,13 @@ class SpaceEvent with _$SpaceEvent { const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand; const factory SpaceEvent.createPage({ required String name, - required ViewLayoutPB layout, int? index, - required bool openAfterCreate, }) = _CreatePage; const factory SpaceEvent.delete(ViewPB? space) = _Delete; const factory SpaceEvent.didReceiveSpaceUpdate() = _DidReceiveSpaceUpdate; const factory SpaceEvent.reset( UserProfilePB userProfile, String workspaceId, - bool openFirstPage, ) = _Reset; const factory SpaceEvent.migrate() = _Migrate; const factory SpaceEvent.switchToNextSpace() = _SwitchToNextSpace; @@ -791,8 +638,6 @@ class SpaceState with _$SpaceState { @Default(null) ViewPB? lastCreatedPage, FlowyResult? createPageResult, @Default(false) bool shouldShowUpgradeDialog, - @Default(false) bool isDuplicatingSpace, - @Default(false) bool isInitialized, }) = _SpaceState; factory SpaceState.initial() => const SpaceState(); diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_search_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_search_bloc.dart deleted file mode 100644 index 04e3ad7896..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/space/space_search_bloc.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'space_search_bloc.freezed.dart'; - -class SpaceSearchBloc extends Bloc { - SpaceSearchBloc() : super(SpaceSearchState.initial()) { - on( - (event, emit) async { - await event.when( - initial: () async { - _allViews = await ViewBackendService.getAllViews().fold( - (s) => s.items, - (_) => [], - ); - }, - search: (query) { - if (query.isEmpty) { - emit( - state.copyWith( - queryResults: null, - ), - ); - } else { - final queryResults = _allViews.where( - (view) => view.name.toLowerCase().contains(query.toLowerCase()), - ); - emit( - state.copyWith( - queryResults: queryResults.toList(), - ), - ); - } - }, - ); - }, - ); - } - - late final List _allViews; -} - -@freezed -class SpaceSearchEvent with _$SpaceSearchEvent { - const factory SpaceSearchEvent.initial() = _Initial; - const factory SpaceSearchEvent.search(String query) = _Search; -} - -@freezed -class SpaceSearchState with _$SpaceSearchState { - const factory SpaceSearchState({ - List? queryResults, - }) = _SpaceSearchState; - - factory SpaceSearchState.initial() => const SpaceSearchState(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart b/frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart index 0cf436630f..b53c8237a6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/subscription_success_listenable/subscription_success_listenable.dart @@ -1,25 +1,7 @@ -import 'package:appflowy_backend/log.dart'; import 'package:flutter/foundation.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; - class SubscriptionSuccessListenable extends ChangeNotifier { SubscriptionSuccessListenable(); - String? _plan; - - SubscriptionPlanPB? get subscribedPlan => switch (_plan) { - 'free' => SubscriptionPlanPB.Free, - 'pro' => SubscriptionPlanPB.Pro, - 'team' => SubscriptionPlanPB.Team, - 'ai_max' => SubscriptionPlanPB.AiMax, - 'ai_local' => SubscriptionPlanPB.AiLocal, - _ => null, - }; - - void onPaymentSuccess(String? plan) { - Log.info("Payment success: $plan"); - _plan = plan; - notifyListeners(); - } + void onPaymentSuccess() => notifyListeners(); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart index f27539cddd..36d64e6989 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -1,24 +1,19 @@ -import 'dart:convert'; +import 'package:flutter/foundation.dart'; -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/core/config/kv_keys.dart'; -import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/expand_views.dart'; +import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:bloc/bloc.dart'; -import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'tabs_bloc.freezed.dart'; +part 'tabs_event.dart'; +part 'tabs_state.dart'; class TabsBloc extends Bloc { TabsBloc() : super(TabsState()) { @@ -42,166 +37,27 @@ class TabsBloc extends Bloc { if (index != state.currentIndex && index >= 0 && index < state.pages) { - emit(state.copyWith(currentIndex: index)); + emit(state.copyWith(newIndex: index)); _setLatestOpenView(); } }, moveTab: () {}, closeTab: (String pluginId) { - final pm = state._pageManagers - .firstWhereOrNull((pm) => pm.plugin.id == pluginId); - if (pm?.isPinned == true) { - return; - } - emit(state.closeView(pluginId)); _setLatestOpenView(); }, closeCurrentTab: () { - if (state.currentPageManager.isPinned) { - return; - } - emit(state.closeView(state.currentPageManager.plugin.id)); _setLatestOpenView(); }, openTab: (Plugin plugin, ViewPB view) { - state.currentPageManager - ..hideSecondaryPlugin() - ..setSecondaryPlugin(BlankPagePlugin()); - emit(state.openView(plugin)); + emit(state.openView(plugin, view)); _setLatestOpenView(view); }, openPlugin: (Plugin plugin, ViewPB? view, bool setLatest) { - state.currentPageManager - ..hideSecondaryPlugin() - ..setSecondaryPlugin(BlankPagePlugin()); emit(state.openPlugin(plugin: plugin, setLatest: setLatest)); if (setLatest) { - // the space view should be filtered out. - if (view != null && view.isSpace) { - return; - } _setLatestOpenView(view); - if (view != null) _expandAncestors(view); - } - }, - closeOtherTabs: (String pluginId) { - final pageManagers = [ - ...state._pageManagers - .where((pm) => pm.plugin.id == pluginId || pm.isPinned), - ]; - - int newIndex; - if (state.currentPageManager.isPinned) { - // Retain current index if it's already pinned - newIndex = state.currentIndex; - } else { - final pm = state._pageManagers - .firstWhereOrNull((pm) => pm.plugin.id == pluginId); - newIndex = pm != null ? pageManagers.indexOf(pm) : 0; - } - - emit( - state.copyWith( - currentIndex: newIndex, - pageManagers: pageManagers, - ), - ); - - _setLatestOpenView(); - }, - togglePin: (String pluginId) { - final pm = state._pageManagers - .firstWhereOrNull((pm) => pm.plugin.id == pluginId); - if (pm != null) { - final index = state._pageManagers.indexOf(pm); - - int newIndex = state.currentIndex; - if (pm.isPinned) { - // Unpinning logic - final indexOfFirstUnpinnedTab = - state._pageManagers.indexWhere((tab) => !tab.isPinned); - - // Determine the correct insertion point - final newUnpinnedIndex = indexOfFirstUnpinnedTab != -1 - ? indexOfFirstUnpinnedTab // Insert before the first unpinned tab - : state._pageManagers - .length; // Append at the end if no unpinned tabs exist - - state._pageManagers.removeAt(index); - - final adjustedUnpinnedIndex = newUnpinnedIndex > index - ? newUnpinnedIndex - 1 - : newUnpinnedIndex; - - state._pageManagers.insert(adjustedUnpinnedIndex, pm); - newIndex = _adjustCurrentIndex( - currentIndex: state.currentIndex, - tabIndex: index, - newIndex: adjustedUnpinnedIndex, - ); - } else { - // Pinning logic - final indexOfLastPinnedTab = - state._pageManagers.lastIndexWhere((tab) => tab.isPinned); - final newPinnedIndex = indexOfLastPinnedTab + 1; - - state._pageManagers.removeAt(index); - - final adjustedPinnedIndex = newPinnedIndex > index - ? newPinnedIndex - 1 - : newPinnedIndex; - - state._pageManagers.insert(adjustedPinnedIndex, pm); - newIndex = _adjustCurrentIndex( - currentIndex: state.currentIndex, - tabIndex: index, - newIndex: adjustedPinnedIndex, - ); - } - - pm.isPinned = !pm.isPinned; - - emit( - state.copyWith( - currentIndex: newIndex, - pageManagers: [...state._pageManagers], - ), - ); - } - }, - openSecondaryPlugin: (plugin, view) { - state.currentPageManager - ..setSecondaryPlugin(plugin) - ..showSecondaryPlugin(); - }, - closeSecondaryPlugin: () { - final pageManager = state.currentPageManager; - pageManager.hideSecondaryPlugin(); - }, - expandSecondaryPlugin: () { - final pageManager = state.currentPageManager; - pageManager - ..hideSecondaryPlugin() - ..expandSecondaryPlugin(); - _setLatestOpenView(); - }, - switchWorkspace: (workspaceId) { - final pluginId = state.currentPageManager.plugin.id; - - // Close all tabs except current - final pagesToClose = [ - ...state._pageManagers - .where((pm) => pm.plugin.id != pluginId && !pm.isPinned), - ]; - - if (pagesToClose.isNotEmpty) { - final newstate = state; - for (final pm in pagesToClose) { - newstate.closeView(pm.plugin.id); - } - emit(newstate.copyWith(currentIndex: 0)); } }, ); @@ -215,55 +71,12 @@ class TabsBloc extends Bloc { } else { final pageManager = state.currentPageManager; final notifier = pageManager.plugin.notifier; - if (notifier is ViewPluginNotifier && - menuSharedState.latestOpenView?.id != notifier.view.id) { + if (notifier is ViewPluginNotifier) { menuSharedState.latestOpenView = notifier.view; } } } - Future _expandAncestors(ViewPB view) async { - final viewExpanderRegistry = getIt.get(); - if (viewExpanderRegistry.isViewExpanded(view.parentViewId)) return; - final value = await getIt().get(KVKeys.expandedViews); - try { - final Map expandedViews = value == null ? {} : jsonDecode(value); - final List ancestors = - await ViewBackendService.getViewAncestors(view.id) - .fold((s) => s.items.map((e) => e.id).toList(), (f) => []); - ViewExpander? viewExpander; - for (final id in ancestors) { - expandedViews[id] = true; - final expander = viewExpanderRegistry.getExpander(id); - if (expander == null) continue; - if (!expander.isViewExpanded && viewExpander == null) { - viewExpander = expander; - } - } - await getIt() - .set(KVKeys.expandedViews, jsonEncode(expandedViews)); - viewExpander?.expand(); - } catch (e) { - Log.error('expandAncestors error', e); - } - } - - int _adjustCurrentIndex({ - required int currentIndex, - required int tabIndex, - required int newIndex, - }) { - if (tabIndex < currentIndex && newIndex >= currentIndex) { - return currentIndex - 1; // Tab moved forward, shift currentIndex back - } else if (tabIndex > currentIndex && newIndex <= currentIndex) { - return currentIndex + 1; // Tab moved backward, shift currentIndex forward - } else if (tabIndex == currentIndex) { - return newIndex; // Tab is the current tab, update to newIndex - } - - return currentIndex; - } - /// Adds a [TabsEvent.openTab] event for the provided [ViewPB] void openTab(ViewPB view) => add(TabsEvent.openTab(plugin: view.plugin(), view: view)); @@ -279,157 +92,8 @@ class TabsBloc extends Bloc { view: view, ), ); - } -} - -@freezed -class TabsEvent with _$TabsEvent { - const factory TabsEvent.moveTab() = _MoveTab; - - const factory TabsEvent.closeTab(String pluginId) = _CloseTab; - - const factory TabsEvent.closeOtherTabs(String pluginId) = _CloseOtherTabs; - - const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab; - - const factory TabsEvent.selectTab(int index) = _SelectTab; - - const factory TabsEvent.togglePin(String pluginId) = _TogglePin; - - const factory TabsEvent.openTab({ - required Plugin plugin, - required ViewPB view, - }) = _OpenTab; - - const factory TabsEvent.openPlugin({ - required Plugin plugin, - ViewPB? view, - @Default(true) bool setLatest, - }) = _OpenPlugin; - - const factory TabsEvent.openSecondaryPlugin({ - required Plugin plugin, - ViewPB? view, - }) = _OpenSecondaryPlugin; - - const factory TabsEvent.closeSecondaryPlugin() = _CloseSecondaryPlugin; - - const factory TabsEvent.expandSecondaryPlugin() = _ExpandSecondaryPlugin; - - const factory TabsEvent.switchWorkspace(String workspaceId) = - _SwitchWorkspace; -} - -class TabsState { - TabsState({ - this.currentIndex = 0, - List? pageManagers, - }) : _pageManagers = pageManagers ?? [PageManager()]; - - final int currentIndex; - final List _pageManagers; - - int get pages => _pageManagers.length; - - PageManager get currentPageManager => _pageManagers[currentIndex]; - - List get pageManagers => _pageManagers; - - bool get isAllPinned => _pageManagers.every((pm) => pm.isPinned); - - /// This opens a new tab given a [Plugin]. - /// - /// If the [Plugin.id] is already associated with an open tab, - /// then it selects that tab. - /// - TabsState openView(Plugin plugin) { - final selectExistingPlugin = _selectPluginIfOpen(plugin.id); - - if (selectExistingPlugin == null) { - _pageManagers.add(PageManager()..setPlugin(plugin, true)); - - return copyWith( - currentIndex: pages - 1, - pageManagers: [..._pageManagers], - ); - } - - return selectExistingPlugin; - } - - TabsState closeView(String pluginId) { - // Avoid closing the only open tab - if (_pageManagers.length == 1) { - return this; - } - - _pageManagers.removeWhere((pm) => pm.plugin.id == pluginId); - - /// If currentIndex is greater than the amount of allowed indices - /// And the current selected tab isn't the first (index 0) - /// as currentIndex cannot be -1 - /// Then decrease currentIndex by 1 - final newIndex = currentIndex > pages - 1 && currentIndex > 0 - ? currentIndex - 1 - : currentIndex; - - return copyWith( - currentIndex: newIndex, - pageManagers: [..._pageManagers], - ); - } - - /// This opens a plugin in the current selected tab, - /// due to how Document currently works, only one tab - /// per plugin can currently be active. - /// - /// If the plugin is already open in a tab, then that tab - /// will become selected. - /// - TabsState openPlugin({required Plugin plugin, bool setLatest = true}) { - final selectExistingPlugin = _selectPluginIfOpen(plugin.id); - - if (selectExistingPlugin == null) { - final pageManagers = [..._pageManagers]; - pageManagers[currentIndex].setPlugin(plugin, setLatest); - - return copyWith(pageManagers: pageManagers); - } - - return selectExistingPlugin; - } - - /// Checks if a [Plugin.id] is already associated with an open tab. - /// Returns a [TabState] with new index if there is a match. - /// - /// If no match it returns null - /// - TabsState? _selectPluginIfOpen(String id) { - final index = _pageManagers.indexWhere((pm) => pm.plugin.id == id); - - if (index == -1) { - return null; - } - - if (index == currentIndex) { - return this; - } - - return copyWith(currentIndex: index); - } - - TabsState copyWith({ - int? currentIndex, - List? pageManagers, - }) => - TabsState( - currentIndex: currentIndex ?? this.currentIndex, - pageManagers: pageManagers ?? _pageManagers, - ); - - void dispose() { - for (final manager in pageManagers) { - manager.dispose(); - } + + // Update recent views + getIt().updateRecentViews([view.id], true); } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart new file mode 100644 index 0000000000..335c383f2e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_event.dart @@ -0,0 +1,18 @@ +part of 'tabs_bloc.dart'; + +@freezed +class TabsEvent with _$TabsEvent { + const factory TabsEvent.moveTab() = _MoveTab; + const factory TabsEvent.closeTab(String pluginId) = _CloseTab; + const factory TabsEvent.closeCurrentTab() = _CloseCurrentTab; + const factory TabsEvent.selectTab(int index) = _SelectTab; + const factory TabsEvent.openTab({ + required Plugin plugin, + required ViewPB view, + }) = _OpenTab; + const factory TabsEvent.openPlugin({ + required Plugin plugin, + ViewPB? view, + @Default(true) bool setLatest, + }) = _OpenPlugin; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart new file mode 100644 index 0000000000..cf4092eaaa --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart @@ -0,0 +1,107 @@ +part of 'tabs_bloc.dart'; + +class TabsState { + TabsState({ + this.currentIndex = 0, + List? pageManagers, + }) : _pageManagers = pageManagers ?? [PageManager()]; + + final int currentIndex; + final List _pageManagers; + int get pages => _pageManagers.length; + PageManager get currentPageManager => _pageManagers[currentIndex]; + List get pageManagers => _pageManagers; + + /// This opens a new tab given a [Plugin] and a [View]. + /// + /// If the [Plugin.id] is already associated with an open tab, + /// then it selects that tab. + /// + TabsState openView(Plugin plugin, ViewPB view) { + final selectExistingPlugin = _selectPluginIfOpen(plugin.id); + + if (selectExistingPlugin == null) { + _pageManagers.add(PageManager()..setPlugin(plugin, true)); + + return copyWith(newIndex: pages - 1, pageManagers: [..._pageManagers]); + } + + return selectExistingPlugin; + } + + TabsState closeView(String pluginId) { + // Avoid closing the only open tab + if (_pageManagers.length == 1) { + return this; + } + + _pageManagers.removeWhere((pm) => pm.plugin.id == pluginId); + + /// If currentIndex is greater than the amount of allowed indices + /// And the current selected tab isn't the first (index 0) + /// as currentIndex cannot be -1 + /// Then decrease currentIndex by 1 + final newIndex = currentIndex > pages - 1 && currentIndex > 0 + ? currentIndex - 1 + : currentIndex; + + return copyWith( + newIndex: newIndex, + pageManagers: [..._pageManagers], + ); + } + + /// This opens a plugin in the current selected tab, + /// due to how Document currently works, only one tab + /// per plugin can currently be active. + /// + /// If the plugin is already open in a tab, then that tab + /// will become selected. + /// + TabsState openPlugin({required Plugin plugin, bool setLatest = true}) { + final selectExistingPlugin = _selectPluginIfOpen(plugin.id); + + if (selectExistingPlugin == null) { + final pageManagers = [..._pageManagers]; + pageManagers[currentIndex].setPlugin(plugin, setLatest); + + return copyWith(pageManagers: pageManagers); + } + + return selectExistingPlugin; + } + + /// Checks if a [Plugin.id] is already associated with an open tab. + /// Returns a [TabState] with new index if there is a match. + /// + /// If no match it returns null + /// + TabsState? _selectPluginIfOpen(String id) { + final index = _pageManagers.indexWhere((pm) => pm.plugin.id == id); + + if (index == -1) { + return null; + } + + if (index == currentIndex) { + return this; + } + + return copyWith(newIndex: index); + } + + TabsState copyWith({ + int? newIndex, + List? pageManagers, + }) => + TabsState( + currentIndex: newIndex ?? currentIndex, + pageManagers: pageManagers ?? _pageManagers, + ); + + void dispose() { + for (final manager in pageManagers) { + manager.dispose(); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart index 2f62177661..59127bdab0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/settings_user_bloc.dart @@ -54,17 +54,26 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - updateUserEmail: (String email) { - _userService.updateUserProfile(email: email).then((result) { + removeUserIcon: () { + // Empty Icon URL = No icon + _userService.updateUserProfile(iconUrl: "").then((result) { result.fold( (l) => null, (err) => Log.error(err), ); }); }, - updateUserPassword: (String oldPassword, String newPassword) { + updateUserOpenAIKey: (openAIKey) { + _userService.updateUserProfile(openAIKey: openAIKey).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + updateUserStabilityAIKey: (stabilityAIKey) { _userService - .updateUserProfile(password: newPassword) + .updateUserProfile(stabilityAiKey: stabilityAIKey) .then((result) { result.fold( (l) => null, @@ -72,9 +81,8 @@ class SettingsUserViewBloc extends Bloc { ); }); }, - removeUserIcon: () { - // Empty Icon URL = No icon - _userService.updateUserProfile(iconUrl: "").then((result) { + updateUserEmail: (String email) { + _userService.updateUserProfile(email: email).then((result) { result.fold( (l) => null, (err) => Log.error(err), @@ -114,20 +122,16 @@ class SettingsUserViewBloc extends Bloc { @freezed class SettingsUserEvent with _$SettingsUserEvent { const factory SettingsUserEvent.initial() = _Initial; - const factory SettingsUserEvent.updateUserName({ - required String name, - }) = _UpdateUserName; - const factory SettingsUserEvent.updateUserEmail({ - required String email, - }) = _UpdateEmail; - const factory SettingsUserEvent.updateUserIcon({ - required String iconUrl, - }) = _UpdateUserIcon; - const factory SettingsUserEvent.updateUserPassword({ - required String oldPassword, - required String newPassword, - }) = _UpdateUserPassword; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.updateUserEmail(String email) = _UpdateEmail; + const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) = + _UpdateUserIcon; const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon; + const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) = + _UpdateUserOpenaiKey; + const factory SettingsUserEvent.updateUserStabilityAIKey( + String stabilityAIKey, + ) = _UpdateUserStabilityAIKey; const factory SettingsUserEvent.didReceiveUserProfile( UserProfilePB newUserProfile, ) = _DidReceiveUserProfile; diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index d14f258462..2f79701707 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/user/application/user_listener.dart'; @@ -10,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -28,9 +29,9 @@ class UserWorkspaceBloc extends Bloc { await event.when( initial: () async { _listener.start( - onUserWorkspaceListUpdated: (workspaces) => + didUpdateUserWorkspaces: (workspaces) => add(UserWorkspaceEvent.updateWorkspaces(workspaces)), - onUserWorkspaceUpdated: (workspace) { + didUpdateUserWorkspace: (workspace) { // If currentWorkspace is updated, eg. Icon or Name, we should notify // the UI to render the updated information. final currentWorkspace = state.currentWorkspace; @@ -44,7 +45,7 @@ class UserWorkspaceBloc extends Bloc { final currentWorkspace = result.$1; final workspaces = result.$2; final isCollabWorkspaceOn = - userProfile.userAuthType == AuthTypePB.Server && + userProfile.authenticator == AuthenticatorPB.AppFlowyCloud && FeatureFlag.collaborativeWorkspace.isOn; Log.info( 'init workspace, current workspace: ${currentWorkspace?.workspaceId}, ' @@ -52,17 +53,23 @@ class UserWorkspaceBloc extends Bloc { ); if (currentWorkspace != null && result.$3 == true) { Log.info('init open workspace: ${currentWorkspace.workspaceId}'); - await _userService.openWorkspace( - currentWorkspace.workspaceId, - currentWorkspace.workspaceAuthType, - ); + await _userService.openWorkspace(currentWorkspace.workspaceId); } + WorkspaceMemberPB? currentWorkspaceMember; + final workspaceMemberResult = + await _userService.getWorkspaceMember(); + currentWorkspaceMember = workspaceMemberResult.fold( + (s) => s, + (e) => null, + ); + emit( state.copyWith( currentWorkspace: currentWorkspace, workspaces: workspaces, isCollabWorkspaceOn: isCollabWorkspaceOn, + currentWorkspaceMember: currentWorkspaceMember, actionResult: null, ), ); @@ -78,26 +85,12 @@ class UserWorkspaceBloc extends Bloc { emit( state.copyWith( + currentWorkspace: currentWorkspace, workspaces: workspaces, ), ); - - // try to open the workspace if the current workspace is not the same - if (currentWorkspace != null && - currentWorkspace.workspaceId != - state.currentWorkspace?.workspaceId) { - Log.info( - 'fetch workspaces: try to open workspace: ${currentWorkspace.workspaceId}', - ); - add( - OpenWorkspace( - currentWorkspace.workspaceId, - currentWorkspace.workspaceAuthType, - ), - ); - } }, - createWorkspace: (name, authType) async { + createWorkspace: (name) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -107,10 +100,7 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.createUserWorkspace( - name, - authType, - ); + final result = await _userService.createUserWorkspace(name); final workspaces = result.fold( (s) => [...state.workspaces, s], (e) => state.workspaces, @@ -129,12 +119,7 @@ class UserWorkspaceBloc extends Bloc { result ..onSuccess((s) { Log.info('create workspace success: $s'); - add( - OpenWorkspace( - s.workspaceId, - s.workspaceAuthType, - ), - ); + add(OpenWorkspace(s.workspaceId)); }) ..onFailure((f) { Log.error('create workspace error: $f'); @@ -176,37 +161,24 @@ class UserWorkspaceBloc extends Bloc { } final result = await _userService.deleteWorkspaceById(workspaceId); - // fetch the workspaces again to check if the current workspace is deleted - final workspacesResult = await _fetchWorkspaces(); - final workspaces = workspacesResult.$2; - final containsDeletedWorkspace = workspaces.any( - (e) => e.workspaceId == workspaceId, + final workspaces = result.fold( + // remove the deleted workspace from the list instead of fetching + // the workspaces again + (s) => state.workspaces + .where((e) => e.workspaceId != workspaceId) + .toList(), + (e) => state.workspaces, ); result ..onSuccess((_) { Log.info('delete workspace success: $workspaceId'); // if the current workspace is deleted, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); + add(OpenWorkspace(workspaces.first.workspaceId)); } }) ..onFailure((f) { Log.error('delete workspace error: $f'); - // if the workspace is deleted but return an error, we need to - // open the first workspace - if (!containsDeletedWorkspace) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); - } }); emit( state.copyWith( @@ -219,7 +191,7 @@ class UserWorkspaceBloc extends Bloc { ), ); }, - openWorkspace: (workspaceId, authType) async { + openWorkspace: (workspaceId) async { emit( state.copyWith( actionResult: const UserWorkspaceActionResult( @@ -229,10 +201,7 @@ class UserWorkspaceBloc extends Bloc { ), ), ); - final result = await _userService.openWorkspace( - workspaceId, - authType, - ); + final result = await _userService.openWorkspace(workspaceId); final currentWorkspace = result.fold( (s) => state.workspaces.firstWhereOrNull( (e) => e.workspaceId == workspaceId, @@ -240,6 +209,14 @@ class UserWorkspaceBloc extends Bloc { (e) => state.currentWorkspace, ); + WorkspaceMemberPB? currentWorkspaceMember; + final workspaceMemberResult = + await _userService.getWorkspaceMember(); + currentWorkspaceMember = workspaceMemberResult.fold( + (s) => s, + (e) => null, + ); + result ..onSuccess((s) { Log.info( @@ -253,6 +230,7 @@ class UserWorkspaceBloc extends Bloc { emit( state.copyWith( currentWorkspace: currentWorkspace, + currentWorkspaceMember: currentWorkspaceMember, actionResult: UserWorkspaceActionResult( actionType: UserWorkspaceActionType.open, isLoading: false, @@ -303,14 +281,6 @@ class UserWorkspaceBloc extends Bloc { ); }, updateWorkspaceIcon: (workspaceId, icon) async { - final workspace = state.workspaces.firstWhere( - (e) => e.workspaceId == workspaceId, - ); - if (icon == workspace.icon) { - Log.info('ignore same icon update'); - return; - } - final result = await _userService.updateWorkspaceIcon( workspaceId, icon, @@ -366,12 +336,7 @@ class UserWorkspaceBloc extends Bloc { Log.info('leave workspace success: $workspaceId'); // if leaving the current workspace, open the first workspace if (state.currentWorkspace?.workspaceId == workspaceId) { - add( - OpenWorkspace( - workspaces.first.workspaceId, - workspaces.first.workspaceAuthType, - ), - ); + add(OpenWorkspace(workspaces.first.workspaceId)); } }) ..onFailure((f) { @@ -441,7 +406,7 @@ class UserWorkspaceBloc extends Bloc { )> _fetchWorkspaces() async { try { final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); + await _userService.getCurrentWorkspace().getOrThrow(); final workspaces = await _userService.getWorkspaces().getOrThrow(); if (workspaces.isEmpty) { workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace)); @@ -475,16 +440,12 @@ class UserWorkspaceBloc extends Bloc { class UserWorkspaceEvent with _$UserWorkspaceEvent { const factory UserWorkspaceEvent.initial() = Initial; const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; - const factory UserWorkspaceEvent.createWorkspace( - String name, - AuthTypePB authType, - ) = CreateWorkspace; + const factory UserWorkspaceEvent.createWorkspace(String name) = + CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; - const factory UserWorkspaceEvent.openWorkspace( - String workspaceId, - AuthTypePB authType, - ) = OpenWorkspace; + const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = + OpenWorkspace; const factory UserWorkspaceEvent.renameWorkspace( String workspaceId, String name, @@ -524,11 +485,6 @@ class UserWorkspaceActionResult { final UserWorkspaceActionType actionType; final bool isLoading; final FlowyResult? result; - - @override - String toString() { - return 'UserWorkspaceActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)'; - } } @freezed @@ -538,6 +494,7 @@ class UserWorkspaceState with _$UserWorkspaceState { const factory UserWorkspaceState({ @Default(null) UserWorkspacePB? currentWorkspace, @Default([]) List workspaces, + @Default(null) WorkspaceMemberPB? currentWorkspaceMember, @Default(null) UserWorkspaceActionResult? actionResult, @Default(false) bool isCollabWorkspaceOn, }) = _UserWorkspaceState; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 553317f4e4..bf243f8a1c 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -1,23 +1,17 @@ -import 'dart:async'; import 'dart:convert'; 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/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/expand_views.dart'; import 'package:appflowy/workspace/application/favorite/favorite_listener.dart'; import 'package:appflowy/workspace/application/recent/cached_recent_service.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; @@ -25,22 +19,12 @@ import 'package:protobuf/protobuf.dart'; part 'view_bloc.freezed.dart'; class ViewBloc extends Bloc { - ViewBloc({ - required this.view, - this.shouldLoadChildViews = true, - this.engagedInExpanding = false, - }) : viewBackendSvc = ViewBackendService(), + ViewBloc({required this.view, this.shouldLoadChildViews = true}) + : viewBackendSvc = ViewBackendService(), listener = ViewListener(viewId: view.id), favoriteListener = FavoriteListener(), super(ViewState.init(view)) { _dispatch(); - if (engagedInExpanding) { - expander = ViewExpander( - () => state.isExpanded, - () => add(const ViewEvent.setIsExpanded(true)), - ); - getIt().register(view.id, expander); - } } final ViewPB view; @@ -48,16 +32,11 @@ class ViewBloc extends Bloc { final ViewListener listener; final FavoriteListener favoriteListener; final bool shouldLoadChildViews; - final bool engagedInExpanding; - late ViewExpander expander; @override Future close() async { await listener.stop(); await favoriteListener.stop(); - if (engagedInExpanding) { - getIt().unregister(view.id, expander); - } return super.close(); } @@ -147,35 +126,23 @@ class ViewBloc extends Bloc { final newView = view.rebuild( (b) => b.name = e.newName, ); - Log.info('rename view: ${newView.id} to ${newView.name}'); return state.copyWith( successOrFailure: FlowyResult.success(null), view: newView, ); }, - (error) { - Log.error('rename view failed: $error'); - return state.copyWith( - successOrFailure: FlowyResult.failure(error), - ); - }, + (error) => state.copyWith( + successOrFailure: FlowyResult.failure(error), + ), ), ); }, delete: (e) async { - // unpublish the page and all its child pages if they are published - await _unpublishPage(view); - - final result = await ViewBackendService.deleteView(viewId: view.id); - + final result = await ViewBackendService.delete(viewId: view.id); emit( result.fold( - (l) { - return state.copyWith( - successOrFailure: FlowyResult.success(null), - isDeleted: true, - ); - }, + (l) => + state.copyWith(successOrFailure: FlowyResult.success(null)), (error) => state.copyWith( successOrFailure: FlowyResult.failure(error), ), @@ -187,13 +154,7 @@ class ViewBloc extends Bloc { ); }, duplicate: (e) async { - final result = await ViewBackendService.duplicate( - view: view, - openAfterDuplicate: true, - syncAfterDuplicate: true, - includeChildren: true, - suffix: ' (${LocaleKeys.menuAppHeader_pageNameSuffix.tr()})', - ); + final result = await ViewBackendService.duplicate(view: view); emit( result.fold( (l) => @@ -214,11 +175,8 @@ class ViewBloc extends Bloc { ); emit( result.fold( - (l) { - return state.copyWith( - successOrFailure: FlowyResult.success(null), - ); - }, + (l) => + state.copyWith(successOrFailure: FlowyResult.success(null)), (error) => state.copyWith( successOrFailure: FlowyResult.failure(error), ), @@ -229,6 +187,7 @@ class ViewBloc extends Bloc { final result = await ViewBackendService.createView( parentViewId: view.id, name: e.name, + desc: '', layoutType: e.layoutType, ext: {}, openAfterCreate: e.openAfterCreated, @@ -262,8 +221,8 @@ class ViewBloc extends Bloc { }, updateIcon: (value) async { await ViewBackendService.updateViewIcon( - view: view, - viewIcon: view.icon.toEmojiIconData(), + viewId: view.id, + viewIcon: value.icon ?? '', ); }, collapseAllPages: (value) async { @@ -272,13 +231,6 @@ class ViewBloc extends Bloc { } add(const ViewEvent.setIsExpanded(false)); }, - unpublish: (value) async { - if (value.sync) { - await _unpublishPage(view); - } else { - unawaited(_unpublishPage(view)); - } - }, ); }, ); @@ -404,7 +356,7 @@ class ViewBloc extends Bloc { }); } - if (update.updateChildViews.isNotEmpty && update.parentViewId.isNotEmpty) { + if (update.updateChildViews.isNotEmpty) { final view = await ViewBackendService.getView(update.parentViewId); final childViews = view.fold((l) => l.childViews, (r) => []); bool isSameOrder = true; @@ -426,20 +378,6 @@ class ViewBloc extends Bloc { return null; } - // unpublish the page and all its child pages - Future _unpublishPage(ViewPB views) async { - final (_, publishedPages) = await ViewBackendService.containPublishedPage( - view, - ); - - await Future.wait( - publishedPages.map((view) async { - Log.info('unpublishing page: ${view.id}, ${view.name}'); - await ViewBackendService.unpublish(view); - }), - ); - } - bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) { return _hash(from) == _hash(to); } @@ -457,17 +395,11 @@ class ViewBloc extends Bloc { @freezed class ViewEvent with _$ViewEvent { const factory ViewEvent.initial() = Initial; - const factory ViewEvent.setIsEditing(bool isEditing) = SetEditing; - const factory ViewEvent.setIsExpanded(bool isExpanded) = SetIsExpanded; - const factory ViewEvent.rename(String newName) = Rename; - const factory ViewEvent.delete() = Delete; - const factory ViewEvent.duplicate() = Duplicate; - const factory ViewEvent.move( ViewPB from, String newParentId, @@ -475,7 +407,6 @@ class ViewEvent with _$ViewEvent { ViewSectionPB? fromSection, ViewSectionPB? toSection, ) = Move; - const factory ViewEvent.createView( String name, ViewLayoutPB layoutType, { @@ -483,25 +414,17 @@ class ViewEvent with _$ViewEvent { @Default(true) bool openAfterCreated, ViewSectionPB? section, }) = CreateView; - const factory ViewEvent.viewDidUpdate( FlowyResult result, ) = ViewDidUpdate; - const factory ViewEvent.viewUpdateChildView(ViewPB result) = ViewUpdateChildView; - const factory ViewEvent.updateViewVisibility( ViewPB view, bool isPublic, ) = UpdateViewVisibility; - const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; - const factory ViewEvent.collapseAllPages() = CollapseAllPages; - - // this event will unpublish the page and all its child pages if they are published - const factory ViewEvent.unpublish({required bool sync}) = Unpublish; } @freezed @@ -511,7 +434,6 @@ class ViewState with _$ViewState { required bool isEditing, required bool isExpanded, required FlowyResult successOrFailure, - @Default(false) bool isDeleted, @Default(true) bool isLoading, @Default(null) ViewPB? lastCreatedView, }) = _ViewState; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index fcd991fcf9..cfb1690e5e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/ai_chat/chat.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; @@ -10,19 +9,15 @@ import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/mobile_grid_page.dart'; import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart'; import 'package:appflowy/plugins/document/document.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; class PluginArgumentKeys { static String selection = "selection"; static String rowId = "row_id"; - static String blockId = "block_id"; } class ViewExtKeys { @@ -52,41 +47,16 @@ class ViewExtKeys { static String spacePermissionKey = 'space_permission'; } -extension MinimalViewExtension on FolderViewMinimalPB { - Widget defaultIcon({Size? size}) => FlowySvg( - switch (layout) { - ViewLayoutPB.Board => FlowySvgs.icon_board_s, - ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s, - ViewLayoutPB.Grid => FlowySvgs.icon_grid_s, - ViewLayoutPB.Document => FlowySvgs.icon_document_s, - ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, - _ => FlowySvgs.icon_document_s, - }, - size: size, - ); -} - extension ViewExtension on ViewPB { - String get nameOrDefault => - name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name; - - bool get isDocument => pluginType == PluginType.document; - bool get isDatabase => [ - PluginType.grid, - PluginType.board, - PluginType.calendar, - ].contains(pluginType); - - Widget defaultIcon({Size? size}) => FlowySvg( + Widget defaultIcon() => FlowySvg( switch (layout) { ViewLayoutPB.Board => FlowySvgs.icon_board_s, ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s, ViewLayoutPB.Grid => FlowySvgs.icon_grid_s, ViewLayoutPB.Document => FlowySvgs.icon_document_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, - _ => FlowySvgs.icon_document_s, + _ => FlowySvgs.document_s, }, - size: size, ); PluginType get pluginType => switch (layout) { @@ -115,13 +85,11 @@ extension ViewExtension on ViewPB { case ViewLayoutPB.Document: final Selection? initialSelection = arguments[PluginArgumentKeys.selection]; - final String? initialBlockId = arguments[PluginArgumentKeys.blockId]; return DocumentPlugin( view: this, pluginType: pluginType, initialSelection: initialSelection, - initialBlockId: initialBlockId, ); case ViewLayoutPB.Chat: return AIChatPagePlugin(view: this); @@ -147,10 +115,6 @@ extension ViewExtension on ViewPB { bool get isSpace { try { - if (extra.isEmpty) { - return false; - } - final ext = jsonDecode(extra); final isSpace = ext[ViewExtKeys.isSpaceKey] ?? false; return isSpace; @@ -169,51 +133,21 @@ extension ViewExtension on ViewPB { } } - FlowySvg? buildSpaceIconSvg(BuildContext context, {Size? size}) { + FlowySvg get spaceIconSvg { try { - if (extra.isEmpty) { - return null; - } - final ext = jsonDecode(extra); final icon = ext[ViewExtKeys.spaceIconKey]; final color = ext[ViewExtKeys.spaceIconColorKey]; if (icon == null || color == null) { - return null; + return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null); } - // before version 0.6.7 - if (icon.contains('space_icon')) { - return FlowySvg( - FlowySvgData('assets/flowy_icons/16x/$icon.svg'), - color: Theme.of(context).colorScheme.surface, - ); - } - - final values = icon.split('/'); - if (values.length != 2) { - return null; - } - final groupName = values[0]; - final iconName = values[1]; - final svgString = kIconGroups - ?.firstWhereOrNull( - (group) => group.name == groupName, - ) - ?.icons - .firstWhereOrNull( - (icon) => icon.name == iconName, - ) - ?.content; - if (svgString == null) { - return null; - } - return FlowySvg.string( - svgString, - color: Theme.of(context).colorScheme.surface, - size: size, + return FlowySvg( + FlowySvgData('assets/flowy_icons/16x/$icon.svg'), + color: Color(int.parse(color)), + blendMode: BlendMode.srcOut, ); } catch (e) { - return null; + return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null); } } @@ -251,11 +185,6 @@ extension ViewExtension on ViewPB { if (layout != ViewLayoutPB.Document) { return null; } - - if (extra.isEmpty) { - return null; - } - try { final ext = jsonDecode(extra); final cover = ext[ViewExtKeys.coverKey] ?? {}; @@ -300,12 +229,12 @@ extension ViewExtension on ViewPB { extension ViewLayoutExtension on ViewLayoutPB { FlowySvgData get icon => switch (this) { - ViewLayoutPB.Board => FlowySvgs.icon_board_s, - ViewLayoutPB.Calendar => FlowySvgs.icon_calendar_s, - ViewLayoutPB.Grid => FlowySvgs.icon_grid_s, - ViewLayoutPB.Document => FlowySvgs.icon_document_s, + ViewLayoutPB.Grid => FlowySvgs.grid_s, + ViewLayoutPB.Board => FlowySvgs.board_s, + ViewLayoutPB.Calendar => FlowySvgs.date_s, + ViewLayoutPB.Document => FlowySvgs.document_s, ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s, - _ => FlowySvgs.icon_document_s, + _ => throw Exception('Unknown layout type'), }; bool get isDocumentView => switch (this) { @@ -326,24 +255,6 @@ extension ViewLayoutExtension on ViewLayoutPB { ViewLayoutPB.Document || ViewLayoutPB.Chat => false, _ => throw Exception('Unknown layout type'), }; - - String get defaultName => switch (this) { - ViewLayoutPB.Document => '', - _ => LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - }; - - bool get shrinkWrappable => switch (this) { - ViewLayoutPB.Grid => true, - ViewLayoutPB.Board => true, - _ => false, - }; - - double get pluginHeight => switch (this) { - ViewLayoutPB.Document || ViewLayoutPB.Board || ViewLayoutPB.Chat => 450, - ViewLayoutPB.Calendar => 650, - ViewLayoutPB.Grid => double.infinity, - _ => throw UnimplementedError(), - }; } extension ViewFinder on List { diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart deleted file mode 100644 index 251131d849..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_lock_status_bloc.dart +++ /dev/null @@ -1,123 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart'; - -part 'view_lock_status_bloc.freezed.dart'; - -class ViewLockStatusBloc - extends Bloc { - ViewLockStatusBloc({ - required this.view, - }) : viewBackendSvc = ViewBackendService(), - listener = ViewListener(viewId: view.id), - super(ViewLockStatusState.init(view)) { - on( - (event, emit) async { - await event.when( - initial: () async { - listener.start( - onViewUpdated: (view) async { - add(ViewLockStatusEvent.updateLockStatus(view.isLocked)); - }, - ); - - final result = await ViewBackendService.getView(view.id); - final latestView = result.fold( - (view) => view, - (_) => view, - ); - emit( - state.copyWith( - view: latestView, - isLocked: latestView.isLocked, - isLoadingLockStatus: false, - ), - ); - }, - lock: () async { - final result = await ViewBackendService.lockView(view.id); - final isLocked = result.fold( - (_) => true, - (_) => false, - ); - add( - ViewLockStatusEvent.updateLockStatus( - isLocked, - ), - ); - }, - unlock: () async { - final result = await ViewBackendService.unlockView(view.id); - final isLocked = result.fold( - (_) => false, - (_) => true, - ); - add( - ViewLockStatusEvent.updateLockStatus( - isLocked, - lockCounter: state.lockCounter + 1, - ), - ); - }, - updateLockStatus: (isLocked, lockCounter) { - state.view.freeze(); - final updatedView = state.view.rebuild( - (update) => update.isLocked = isLocked, - ); - emit( - state.copyWith( - view: updatedView, - isLocked: isLocked, - lockCounter: lockCounter ?? state.lockCounter, - ), - ); - }, - ); - }, - ); - } - - final ViewPB view; - final ViewBackendService viewBackendSvc; - final ViewListener listener; - - @override - Future close() async { - await listener.stop(); - - return super.close(); - } -} - -@freezed -class ViewLockStatusEvent with _$ViewLockStatusEvent { - const factory ViewLockStatusEvent.initial() = Initial; - const factory ViewLockStatusEvent.lock() = Lock; - const factory ViewLockStatusEvent.unlock() = Unlock; - const factory ViewLockStatusEvent.updateLockStatus( - bool isLocked, { - int? lockCounter, - }) = UpdateLockStatus; -} - -@freezed -class ViewLockStatusState with _$ViewLockStatusState { - const factory ViewLockStatusState({ - required ViewPB view, - required bool isLocked, - required int lockCounter, - @Default(true) bool isLoadingLockStatus, - }) = _ViewLockStatusState; - - factory ViewLockStatusState.init(ViewPB view) => ViewLockStatusState( - view: view, - isLocked: false, - lockCounter: 0, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index ea74f1861e..d123e72c4f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -1,15 +1,9 @@ import 'dart:async'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_bloc.dart'; -import 'package:appflowy/plugins/trash/application/trash_service.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; class ViewBackendService { static Future> createView({ @@ -21,6 +15,7 @@ class ViewBackendService { /// The [name] is the name of the view. required String name, + String? desc, /// The default value of [openAfterCreate] is false, meaning the view will /// not be opened nor set as the current view. However, if set to true, the @@ -48,6 +43,7 @@ class ViewBackendService { final payload = CreateViewPayloadPB.create() ..parentViewId = parentViewId ..name = name + ..desc = desc ?? "" ..layout = layoutType ..setAsCurrent = openAfterCreate ..initialData = initialDataBytes ?? []; @@ -56,6 +52,10 @@ class ViewBackendService { payload.meta.addAll(ext); } + if (desc != null) { + payload.desc = desc; + } + if (index != null) { payload.index = index; } @@ -87,6 +87,7 @@ class ViewBackendService { final payload = CreateOrphanViewPayloadPB.create() ..viewId = viewId ..name = name + ..desc = desc ?? "" ..layout = layoutType ..initialData = initialDataBytes ?? []; @@ -111,12 +112,6 @@ class ViewBackendService { static Future, FlowyError>> getChildViews({ required String viewId, }) { - if (viewId.isEmpty) { - return Future.value( - FlowyResult, FlowyError>.success([]), - ); - } - final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send().then((result) { @@ -127,6 +122,13 @@ class ViewBackendService { }); } + static Future> delete({ + required String viewId, + }) { + final request = RepeatedViewIdPB.create()..items.add(viewId); + return FolderEventDeleteView(request).send(); + } + static Future> deleteView({ required String viewId, }) { @@ -134,37 +136,10 @@ class ViewBackendService { return FolderEventDeleteView(request).send(); } - static Future> deleteViews({ - required List viewIds, - }) { - final request = RepeatedViewIdPB.create()..items.addAll(viewIds); - return FolderEventDeleteView(request).send(); - } - - static Future> duplicate({ + static Future> duplicate({ required ViewPB view, - required bool openAfterDuplicate, - // should include children views - required bool includeChildren, - String? parentViewId, - String? suffix, - required bool syncAfterDuplicate, }) { - final payload = DuplicateViewPayloadPB.create() - ..viewId = view.id - ..openAfterDuplicate = openAfterDuplicate - ..includeChildren = includeChildren - ..syncAfterCreate = syncAfterDuplicate; - - if (parentViewId != null) { - payload.parentViewId = parentViewId; - } - - if (suffix != null) { - payload.suffix = suffix; - } - - return FolderEventDuplicateView(payload).send(); + return FolderEventDuplicateView(view).send(); } static Future> favorite({ @@ -198,25 +173,17 @@ class ViewBackendService { } static Future> updateViewIcon({ - required ViewPB view, - required EmojiIconData viewIcon, + required String viewId, + required String viewIcon, + ViewIconTypePB iconType = ViewIconTypePB.Emoji, }) { - final viewId = view.id; - final oldIcon = view.icon.toEmojiIconData(); - final icon = viewIcon.toViewIcon(); + final icon = ViewIconPB() + ..ty = iconType + ..value = viewIcon; final payload = UpdateViewIconPayloadPB.create() ..viewId = viewId ..icon = icon; - if (oldIcon.type == FlowyIconType.custom && - viewIcon.emoji != oldIcon.emoji) { - DocumentEventDeleteFile( - DeleteFilePB(url: oldIcon.emoji), - ).send().onFailure((e) { - Log.error( - 'updateViewIcon error while deleting :${oldIcon.emoji}, error: ${e.msg}, ${e.code}', - ); - }); - } + return FolderEventUpdateViewIcon(payload).send(); } @@ -268,40 +235,10 @@ class ViewBackendService { static Future> getView( String viewId, ) async { - if (viewId.isEmpty) { - Log.error('ViewId is empty'); - } final payload = ViewIdPB.create()..value = viewId; return FolderEventGetView(payload).send(); } - static Future getMentionPageStatus(String pageId) async { - final view = await ViewBackendService.getView(pageId).then( - (value) => value.toNullable(), - ); - - // found the page - if (view != null) { - return (view, false, false); - } - - // if the view is not found, try to fetch from trash - final trashViews = await TrashService().readTrash(); - final trash = trashViews.fold( - (l) => l.items.firstWhereOrNull((element) => element.id == pageId), - (r) => null, - ); - if (trash != null) { - final trashView = ViewPB() - ..id = trash.id - ..name = trash.name; - return (trashView, true, false); - } - - // the page was deleted - return (null, false, true); - } - static Future> getViewAncestors( String viewId, ) async { @@ -334,94 +271,4 @@ class ViewBackendService { ); return FolderEventUpdateViewVisibilityStatus(payload).send(); } - - static Future> getPublishInfo( - ViewPB view, - ) async { - final payload = ViewIdPB()..value = view.id; - return FolderEventGetPublishInfo(payload).send(); - } - - static Future> publish( - ViewPB view, { - String? name, - List? selectedViewIds, - }) async { - final payload = PublishViewParamsPB()..viewId = view.id; - - if (name != null) { - payload.publishName = name; - } - - if (selectedViewIds != null && selectedViewIds.isNotEmpty) { - payload.selectedViewIds = RepeatedViewIdPB(items: selectedViewIds); - } - - return FolderEventPublishView(payload).send(); - } - - static Future> unpublish( - ViewPB view, - ) async { - final payload = UnpublishViewsPayloadPB(viewIds: [view.id]); - return FolderEventUnpublishViews(payload).send(); - } - - static Future> setPublishNameSpace( - String name, - ) async { - final payload = SetPublishNamespacePayloadPB()..newNamespace = name; - return FolderEventSetPublishNamespace(payload).send(); - } - - static Future> - getPublishNameSpace() async { - return FolderEventGetPublishNamespace().send(); - } - - static Future> getAllChildViews(ViewPB view) async { - final views = []; - - final childViews = - await ViewBackendService.getChildViews(viewId: view.id).fold( - (s) => s, - (f) => [], - ); - - for (final child in childViews) { - // filter the view itself - if (child.id == view.id) { - continue; - } - views.add(child); - views.addAll(await getAllChildViews(child)); - } - - return views; - } - - static Future<(bool, List)> containPublishedPage(ViewPB view) async { - final childViews = await ViewBackendService.getAllChildViews(view); - final views = [view, ...childViews]; - final List publishedPages = []; - - for (final view in views) { - final publishInfo = await ViewBackendService.getPublishInfo(view); - if (publishInfo.isSuccess) { - publishedPages.add(view); - } - } - - return (publishedPages.isNotEmpty, publishedPages); - } - - static Future> lockView(String viewId) async { - final payload = ViewIdPB()..value = viewId; - return FolderEventLockView(payload).send(); - } - - static Future> unlockView(String viewId) async { - final payload = ViewIdPB()..value = viewId; - return FolderEventUnlockView(payload).send(); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart index 27540622ba..b24c4f6a9a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view_info/view_info_bloc.dart @@ -1,5 +1,4 @@ import 'package:appflowy/util/int64_extension.dart'; -import 'package:appflowy/util/string_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:bloc/bloc.dart'; @@ -12,12 +11,7 @@ class ViewInfoBloc extends Bloc { on((event, emit) { event.when( started: () { - emit( - state.copyWith( - createdAt: view.createTime.toDateTime(), - titleCounters: view.name.getCounter(), - ), - ); + emit(state.copyWith(createdAt: view.createTime.toDateTime())); }, unregisterEditorState: () { _clearWordCountService(); @@ -42,13 +36,6 @@ class ViewInfoBloc extends Bloc { ), ); }, - titleChanged: (s) { - emit( - state.copyWith( - titleCounters: s.getCounter(), - ), - ); - }, ); }); } @@ -84,21 +71,17 @@ class ViewInfoEvent with _$ViewInfoEvent { }) = _RegisterEditorState; const factory ViewInfoEvent.wordCountChanged() = _WordCountChanged; - - const factory ViewInfoEvent.titleChanged(String title) = _TitleChanged; } @freezed class ViewInfoState with _$ViewInfoState { const factory ViewInfoState({ required Counters? documentCounters, - required Counters? titleCounters, required DateTime? createdAt, }) = _ViewInfoState; factory ViewInfoState.initial() => const ViewInfoState( documentCounters: null, - titleCounters: null, createdAt: null, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart index 1530c96d32..491ff36786 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart @@ -1,6 +1,4 @@ -import 'package:appflowy/plugins/trash/application/prelude.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,83 +7,41 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'view_title_bar_bloc.freezed.dart'; class ViewTitleBarBloc extends Bloc { - ViewTitleBarBloc({required this.view}) : super(ViewTitleBarState.initial()) { + ViewTitleBarBloc({ + required this.view, + }) : super(ViewTitleBarState.initial()) { on( (event, emit) async { await event.when( + initial: () async { + add(const ViewTitleBarEvent.reload()); + }, reload: () async { final List ancestors = await ViewBackendService.getViewAncestors(view.id).fold( (s) => s.items, (f) => [], ); - - final isDeleted = (await trashService.readTrash()).fold( - (s) => s.items.any((t) => t.id == view.id), - (f) => false, - ); - - emit(state.copyWith(ancestors: ancestors, isDeleted: isDeleted)); - }, - trashUpdated: (trash) { - if (trash.any((t) => t.id == view.id)) { - emit(state.copyWith(isDeleted: true)); - } + emit(state.copyWith(ancestors: ancestors)); }, ); }, ); - - trashService = TrashService(); - viewListener = ViewListener(viewId: view.id) - ..start( - onViewChildViewsUpdated: (_) { - if (!isClosed) { - add(const ViewTitleBarEvent.reload()); - } - }, - ); - trashListener = TrashListener() - ..start( - trashUpdated: (trashOrFailed) { - final trash = trashOrFailed.toNullable(); - if (trash != null && !isClosed) { - add(ViewTitleBarEvent.trashUpdated(trash: trash)); - } - }, - ); - - if (!isClosed) { - add(const ViewTitleBarEvent.reload()); - } } final ViewPB view; - late final TrashService trashService; - late final ViewListener viewListener; - late final TrashListener trashListener; - - @override - Future close() { - trashListener.close(); - viewListener.stop(); - return super.close(); - } } @freezed class ViewTitleBarEvent with _$ViewTitleBarEvent { + const factory ViewTitleBarEvent.initial() = Initial; const factory ViewTitleBarEvent.reload() = Reload; - const factory ViewTitleBarEvent.trashUpdated({ - required List trash, - }) = TrashUpdated; } @freezed class ViewTitleBarState with _$ViewTitleBarState { const factory ViewTitleBarState({ required List ancestors, - @Default(false) bool isDeleted, }) = _ViewTitleBarState; factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []); diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart index fdb9dc9321..384e2773f9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart @@ -3,8 +3,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - part 'view_title_bloc.freezed.dart'; class ViewTitleBloc extends Bloc { @@ -19,7 +17,7 @@ class ViewTitleBloc extends Bloc { emit( state.copyWith( name: view.name, - icon: view.icon.toEmojiIconData(), + icon: view.icon.value, view: view, ), ); @@ -29,7 +27,7 @@ class ViewTitleBloc extends Bloc { add( ViewTitleEvent.updateNameOrIcon( view.name, - view.icon.toEmojiIconData(), + view.icon.value, view, ), ); @@ -63,10 +61,9 @@ class ViewTitleBloc extends Bloc { @freezed class ViewTitleEvent with _$ViewTitleEvent { const factory ViewTitleEvent.initial() = Initial; - const factory ViewTitleEvent.updateNameOrIcon( String name, - EmojiIconData icon, + String icon, ViewPB? view, ) = UpdateNameOrIcon; } @@ -75,12 +72,9 @@ class ViewTitleEvent with _$ViewTitleEvent { class ViewTitleState with _$ViewTitleState { const factory ViewTitleState({ required String name, - required EmojiIconData icon, + required String icon, @Default(null) ViewPB? view, }) = _ViewTitleState; - factory ViewTitleState.initial() => ViewTitleState( - name: '', - icon: EmojiIconData.none(), - ); + factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: ''); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart index ed06f16c8f..d8d5db45b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_bloc.dart @@ -2,7 +2,6 @@ import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -65,8 +64,7 @@ class WorkspaceBloc extends Bloc { String desc, Emitter emit, ) async { - final result = - await userService.createUserWorkspace(name, AuthTypePB.Server); + final result = await userService.createWorkspace(name, desc); emit( result.fold( (workspace) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index ae6220994e..8da9ad4854 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -1,27 +1,24 @@ import 'dart:async'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; -import 'package:fixnum/fixnum.dart' as fixnum; class WorkspaceService { - WorkspaceService({required this.workspaceId, required this.userId}); + WorkspaceService({required this.workspaceId}); final String workspaceId; - final fixnum.Int64 userId; Future> createView({ required String name, required ViewSectionPB viewSection, + String? desc, int? index, ViewLayoutPB? layout, bool? setAsCurrent, String? viewId, - String? extra, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = workspaceId @@ -29,6 +26,10 @@ class WorkspaceService { ..layout = layout ?? ViewLayoutPB.Document ..section = viewSection; + if (desc != null) { + payload.desc = desc; + } + if (index != null) { payload.index = index; } @@ -41,10 +42,6 @@ class WorkspaceService { payload.viewId = viewId; } - if (extra != null) { - payload.extra = extra; - } - return FolderEventCreateView(payload).send(); } @@ -85,18 +82,7 @@ class WorkspaceService { return FolderEventMoveView(payload).send(); } - Future> getWorkspaceUsage() async { - final request = WorkspaceMemberIdPB()..uid = userId; - final result = await UserEventGetMemberInfo(request).send(); - final isOwner = result.fold( - (member) => member.role.isOwner, - (_) => false, - ); - - if (!isOwner) { - return FlowyResult.success(null); - } - + Future> getWorkspaceUsage() { final payload = UserWorkspaceIdPB(workspaceId: workspaceId); return UserEventGetWorkspaceUsage(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart index 648712bd15..ae61f9d7a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/command_palette.dart @@ -1,14 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart'; import 'package:appflowy/workspace/presentation/command_palette/widgets/search_results_list.dart'; +import 'package:appflowy_editor/appflowy_editor.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/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; class CommandPalette extends InheritedWidget { CommandPalette({ @@ -55,7 +57,7 @@ class _CommandPaletteController extends StatefulWidget { } class _CommandPaletteControllerState extends State<_CommandPaletteController> { - late ValueNotifier _toggleNotifier = widget.notifier; + late final ValueNotifier _toggleNotifier = widget.notifier; bool _isOpen = false; @override @@ -70,16 +72,6 @@ class _CommandPaletteControllerState extends State<_CommandPaletteController> { super.dispose(); } - @override - void didUpdateWidget(_CommandPaletteController oldWidget) { - if (oldWidget.notifier != widget.notifier) { - oldWidget.notifier.removeListener(_onToggle); - _toggleNotifier = widget.notifier; - _toggleNotifier.addListener(_onToggle); - } - super.didUpdateWidget(oldWidget); - } - void _onToggle() { if (_toggleNotifier.value && !_isOpen) { _isOpen = true; @@ -113,7 +105,7 @@ class _CommandPaletteControllerState extends State<_CommandPaletteController> { }, shortcuts: { LogicalKeySet( - UniversalPlatform.isMacOS + PlatformExtension.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.keyP, @@ -134,17 +126,13 @@ class CommandPaletteModal extends StatelessWidget { builder: (context, state) => FlowyDialog( alignment: Alignment.topCenter, insetPadding: const EdgeInsets.only(top: 100), - constraints: const BoxConstraints( - maxHeight: 600, - maxWidth: 800, - minHeight: 600, - ), + constraints: const BoxConstraints(maxHeight: 420, maxWidth: 510), expandHeight: false, child: shortcutBuilder( - // Change mainAxisSize to max so Expanded works correctly. Column( + mainAxisSize: MainAxisSize.min, children: [ - SearchField(query: state.query, isLoading: state.searching), + SearchField(query: state.query, isLoading: state.isLoading), if (state.query?.isEmpty ?? true) ...[ const Divider(height: 0), Flexible( @@ -153,26 +141,22 @@ class CommandPaletteModal extends StatelessWidget { ), ), ], - if (state.combinedResponseItems.isNotEmpty && - (state.query?.isNotEmpty ?? false)) ...[ + if (state.results.isNotEmpty) ...[ const Divider(height: 0), Flexible( - child: SearchResultList( + child: SearchResultsList( trash: state.trash, - resultItems: state.combinedResponseItems.values.toList(), - resultSummaries: state.resultSummaries, + results: state.results, ), ), - ] - // When there are no results and the query is not empty and not loading, - // show the no results message, centered in the available space. - else if ((state.query?.isNotEmpty ?? false) && - !state.searching) ...[ - const Divider(height: 0), - Expanded( - child: const _NoResultsHint(), - ), + ] else if ((state.query?.isNotEmpty ?? false) && + !state.isLoading) ...[ + const _NoResultsHint(), ], + _CommandPaletteFooter( + shouldShow: state.results.isNotEmpty && + (state.query?.isNotEmpty ?? false), + ), ], ), ), @@ -181,16 +165,57 @@ class CommandPaletteModal extends StatelessWidget { } } -/// Updated _NoResultsHint now centers its content. class _NoResultsHint extends StatelessWidget { const _NoResultsHint(); @override Widget build(BuildContext context) { - return Center( - child: FlowyText.regular( - LocaleKeys.commandPalette_noResultsHint.tr(), - textAlign: TextAlign.center, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider(height: 0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FlowyText.regular( + LocaleKeys.commandPalette_noResultsHint.tr(), + textAlign: TextAlign.left, + ), + ), + ], + ); + } +} + +class _CommandPaletteFooter extends StatelessWidget { + const _CommandPaletteFooter({required this.shouldShow}); + + final bool shouldShow; + + @override + Widget build(BuildContext context) { + if (!shouldShow) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + border: Border(top: BorderSide(color: Theme.of(context).dividerColor)), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: const FlowyText.semibold('TAB', fontSize: 10), + ), + const HSpace(4), + FlowyText(LocaleKeys.commandPalette_navigateHint.tr(), fontSize: 11), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart new file mode 100644 index 0000000000..7ac439dcba --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_view_tile.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class RecentViewTile extends StatelessWidget { + const RecentViewTile({ + super.key, + required this.icon, + required this.view, + required this.onSelected, + }); + + final Widget icon; + final ViewPB view; + final VoidCallback onSelected; + + @override + Widget build(BuildContext context) { + return ListTile( + dense: true, + title: Row( + children: [ + icon, + const HSpace(6), + FlowyText(view.name), + ], + ), + focusColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + onTap: () { + onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: view.id), + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart index 3bc160ee81..8e13462130 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/recent_views_list.dart @@ -1,14 +1,13 @@ +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/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RecentViewsList extends StatelessWidget { @@ -25,7 +24,7 @@ class RecentViewsList extends StatelessWidget { builder: (context, state) { // We remove duplicates by converting the list to a set first final List recentViews = - state.views.map((e) => e.item).toSet().toList(); + state.views.reversed.map((e) => e.item).toSet().toList(); return ListView.separated( shrinkWrap: true, @@ -46,13 +45,13 @@ class RecentViewsList extends StatelessWidget { final view = recentViews[index - 1]; final icon = view.icon.value.isNotEmpty - ? EmojiIconWidget( - emoji: view.icon.toEmojiIconData(), - emojiSize: 18.0, + ? Text( + view.icon.value, + style: const TextStyle(fontSize: 18.0), ) : FlowySvg(view.iconData, size: const Size.square(20)); - return SearchRecentViewCell( + return RecentViewTile( icon: SizedBox(width: 24, child: icon), view: view, onSelected: onSelected, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart index 1586ab0a7e..022be101c2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_field.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -24,31 +25,28 @@ class SearchField extends StatefulWidget { class _SearchFieldState extends State { late final FocusNode focusNode; - late final TextEditingController controller; + late final controller = TextEditingController(text: widget.query); @override void initState() { super.initState(); - controller = TextEditingController(text: widget.query); - focusNode = FocusNode(onKeyEvent: _handleKeyEvent); - focusNode.requestFocus(); - // Update the text selection after the first frame - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.selection = TextSelection( - baseOffset: 0, - extentOffset: controller.text.length, - ); - }); - } + focusNode = FocusNode( + onKeyEvent: (node, event) { + if (node.hasFocus && + event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.arrowDown) { + node.nextFocus(); + return KeyEventResult.handled; + } - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (node.hasFocus && - event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.arrowDown) { - node.nextFocus(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; + return KeyEventResult.ignored; + }, + ); + focusNode.requestFocus(); + controller.selection = TextSelection( + baseOffset: 0, + extentOffset: controller.text.length, + ); } @override @@ -58,83 +56,21 @@ class _SearchFieldState extends State { super.dispose(); } - Widget _buildSuffixIcon(BuildContext context) { - return ValueListenableBuilder( - valueListenable: controller, - builder: (context, value, _) { - final hasText = value.text.trim().isNotEmpty; - final clearIcon = Container( - padding: const EdgeInsets.all(1), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AFThemeExtension.of(context).lightGreyHover, - ), - child: const FlowySvg( - FlowySvgs.close_s, - size: Size.square(16), - ), - ); - return AnimatedOpacity( - opacity: hasText ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: hasText - ? FlowyTooltip( - message: LocaleKeys.commandPalette_clearSearchTooltip.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: _clearSearch, - child: clearIcon, - ), - ), - ) - : clearIcon, - ); - }, - ); - } - @override Widget build(BuildContext context) { - // Cache theme and text styles - final theme = Theme.of(context); - final textStyle = theme.textTheme.bodySmall?.copyWith(fontSize: 14); - final hintStyle = theme.textTheme.bodySmall?.copyWith( - fontSize: 14, - color: theme.hintColor, - ); - - // Choose the leading icon based on loading state - final Widget leadingIcon = widget.isLoading - ? FlowyTooltip( - message: LocaleKeys.commandPalette_loadingTooltip.tr(), - child: const SizedBox( - width: 20, - height: 20, - child: Padding( - padding: EdgeInsets.all(3.0), - child: CircularProgressIndicator(strokeWidth: 2.0), - ), - ), - ) - : SizedBox( - width: 20, - height: 20, - child: FlowySvg( - FlowySvgs.search_m, - color: theme.hintColor, - ), - ); - return Row( children: [ const HSpace(12), - leadingIcon, + FlowySvg( + FlowySvgs.search_m, + color: Theme.of(context).hintColor, + ), Expanded( child: FlowyTextField( focusNode: focusNode, controller: controller, - textStyle: textStyle, + textStyle: + Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 14), decoration: InputDecoration( constraints: const BoxConstraints(maxHeight: 48), contentPadding: const EdgeInsets.symmetric(horizontal: 12), @@ -144,14 +80,73 @@ class _SearchFieldState extends State { ), isDense: false, hintText: LocaleKeys.commandPalette_placeholder.tr(), - hintStyle: hintStyle, - errorStyle: theme.textTheme.bodySmall! - .copyWith(color: theme.colorScheme.error), + hintStyle: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 14, + color: Theme.of(context).hintColor, + ), + errorStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.error), suffix: Row( mainAxisSize: MainAxisSize.min, children: [ - _buildSuffixIcon(context), + AnimatedOpacity( + opacity: controller.text.trim().isNotEmpty ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Builder( + builder: (context) { + final icon = Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AFThemeExtension.of(context).lightGreyHover, + ), + child: const FlowySvg( + FlowySvgs.close_s, + size: Size.square(16), + ), + ); + if (controller.text.isEmpty) { + return icon; + } + + return FlowyTooltip( + message: + LocaleKeys.commandPalette_clearSearchTooltip.tr(), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: controller.text.trim().isNotEmpty + ? _clearSearch + : null, + child: icon, + ), + ), + ); + }, + ), + ), const HSpace(8), + // TODO(Mathias): Remove beta when support database search + FlowyTooltip( + message: LocaleKeys.commandPalette_betaTooltip.tr(), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 5, + vertical: 2, + ), + decoration: BoxDecoration( + color: AFThemeExtension.of(context).lightGreyHover, + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText.semibold( + LocaleKeys.commandPalette_betaLabel.tr(), + fontSize: 11, + lineHeight: 1.2, + ), + ), + ), ], ), counterText: "", @@ -161,7 +156,9 @@ class _SearchFieldState extends State { ), errorBorder: OutlineInputBorder( borderRadius: Corners.s8Border, - borderSide: BorderSide(color: theme.colorScheme.error), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + ), ), ), onChanged: (value) => context @@ -169,6 +166,17 @@ class _SearchFieldState extends State { .add(CommandPaletteEvent.searchChanged(search: value)), ), ), + if (widget.isLoading) ...[ + FlowyTooltip( + message: LocaleKeys.commandPalette_loadingTooltip.tr(), + child: const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2.5), + ), + ), + const HSpace(12), + ], ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart deleted file mode 100644 index a803f9b44c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; - -class SearchRecentViewCell extends StatelessWidget { - const SearchRecentViewCell({ - super.key, - required this.icon, - required this.view, - required this.onSelected, - }); - - final Widget icon; - final ViewPB view; - final VoidCallback onSelected; - - @override - Widget build(BuildContext context) { - return ListTile( - dense: true, - title: Row( - children: [ - icon, - const HSpace(6), - Expanded( - child: FlowyText( - view.nameOrDefault, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - focusColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - hoverColor: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - onTap: () { - onSelected(); - - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: view.id), - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart deleted file mode 100644 index 2485da4a69..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_cell.dart +++ /dev/null @@ -1,235 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SearchResultCell extends StatefulWidget { - const SearchResultCell({ - super.key, - required this.item, - this.isTrashed = false, - this.isHovered = false, - }); - - final SearchResultItem item; - final bool isTrashed; - final bool isHovered; - - @override - State createState() => _SearchResultCellState(); -} - -class _SearchResultCellState extends State { - bool _hasFocus = false; - final focusNode = FocusNode(); - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - /// Helper to handle the selection action. - void _handleSelection() { - context.read().add( - SearchResultListEvent.openPage(pageId: widget.item.id), - ); - } - - /// Helper to clean up preview text. - String _cleanPreview(String preview) { - return preview.replaceAll('\n', ' ').trim(); - } - - @override - Widget build(BuildContext context) { - final title = widget.item.displayName.orDefault( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - ); - final icon = widget.item.icon.getIcon(); - final cleanedPreview = _cleanPreview(widget.item.content); - final hasPreview = cleanedPreview.isNotEmpty; - final trashHintText = - widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null; - - // Build the tile content based on preview availability. - Widget tileContent; - if (hasPreview) { - tileContent = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.isTrashed) - FlowyText( - trashHintText!, - color: AFThemeExtension.of(context) - .textColor - .withAlpha(175), - fontSize: 10, - ), - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - const VSpace(4), - _DocumentPreview(preview: cleanedPreview), - ], - ); - } else { - tileContent = Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) ...[ - SizedBox(width: 24, child: icon), - const HSpace(6), - ], - Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.isTrashed) - FlowyText( - trashHintText!, - color: - AFThemeExtension.of(context).textColor.withAlpha(175), - fontSize: 10, - ), - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ); - } - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: _handleSelection, - child: Focus( - focusNode: focusNode, - onKeyEvent: (node, event) { - if (event is! KeyDownEvent) return KeyEventResult.ignored; - if (event.logicalKey == LogicalKeyboardKey.enter) { - _handleSelection(); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - onFocusChange: (hasFocus) { - setState(() { - context.read().add( - SearchResultListEvent.onHoverResult( - item: widget.item, - userHovered: true, - ), - ); - _hasFocus = hasFocus; - }); - }, - child: FlowyHover( - onHover: (value) { - context.read().add( - SearchResultListEvent.onHoverResult( - item: widget.item, - userHovered: true, - ), - ); - }, - isSelected: () => _hasFocus || widget.isHovered, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 6), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30), - child: tileContent, - ), - ), - ), - ), - ); - } -} - -class _DocumentPreview extends StatelessWidget { - const _DocumentPreview({required this.preview}); - - final String preview; - - @override - Widget build(BuildContext context) { - // Combine the horizontal padding for clarity: - return Padding( - padding: const EdgeInsets.fromLTRB(30, 0, 16, 0), - child: FlowyText.regular( - preview, - color: Theme.of(context).hintColor, - fontSize: 12, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ); - } -} - -class SearchResultPreview extends StatelessWidget { - const SearchResultPreview({ - super.key, - required this.data, - }); - - final SearchResultItem data; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_pagePreview.tr(), - fontSize: 12, - ), - ), - const VSpace(6), - Expanded( - child: FlowyText( - data.content, - maxLines: 30, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart new file mode 100644 index 0000000000..770f37beee --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_result_tile.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; +import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; +import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class SearchResultTile extends StatefulWidget { + const SearchResultTile({ + super.key, + required this.result, + required this.onSelected, + this.isTrashed = false, + }); + + final SearchResultPB result; + final VoidCallback onSelected; + final bool isTrashed; + + @override + State createState() => _SearchResultTileState(); +} + +class _SearchResultTileState extends State { + bool _hasFocus = false; + + final focusNode = FocusNode(); + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final icon = widget.result.getIcon(); + final cleanedPreview = _cleanPreview(widget.result.preview); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + widget.onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.result.viewId), + ), + ); + }, + child: Focus( + onKeyEvent: (node, event) { + if (event is! KeyDownEvent) { + return KeyEventResult.ignored; + } + + if (event.logicalKey == LogicalKeyboardKey.enter) { + widget.onSelected(); + + getIt().add( + ActionNavigationEvent.performAction( + action: NavigationAction(objectId: widget.result.viewId), + ), + ); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + onFocusChange: (hasFocus) => setState(() => _hasFocus = hasFocus), + child: FlowyHover( + isSelected: () => _hasFocus, + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), + foregroundColorOnHover: AFThemeExtension.of(context).textColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (icon != null) ...[ + SizedBox(width: 24, child: icon), + const HSpace(6), + ], + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.isTrashed) ...[ + FlowyText( + LocaleKeys.commandPalette_fromTrashHint.tr(), + color: AFThemeExtension.of(context) + .textColor + .withAlpha(175), + fontSize: 10, + ), + ], + FlowyText(widget.result.data), + ], + ), + ], + ), + if (cleanedPreview.isNotEmpty) ...[ + const VSpace(4), + _DocumentPreview(preview: cleanedPreview), + ], + ], + ), + ), + ), + ), + ); + } + + String _cleanPreview(String preview) { + return preview.replaceAll('\n', ' ').trim(); + } +} + +class _DocumentPreview extends StatelessWidget { + const _DocumentPreview({required this.preview}); + + final String preview; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16) + + const EdgeInsets.only(left: 14), + child: FlowyText.regular( + preview, + color: Theme.of(context).hintColor, + fontSize: 12, + maxLines: 3, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart index d90888e3e9..ed9becf29e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_results_list.dart @@ -1,278 +1,47 @@ -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'search_result_cell.dart'; -import 'search_summary_cell.dart'; - -class SearchResultList extends StatefulWidget { - const SearchResultList({ - required this.trash, - required this.resultItems, - required this.resultSummaries, +class SearchResultsList extends StatelessWidget { + const SearchResultsList({ super.key, + required this.trash, + required this.results, }); final List trash; - final List resultItems; - final List resultSummaries; + final List results; @override - State createState() => _SearchResultListState(); -} - -class _SearchResultListState extends State { - late final SearchResultListBloc bloc; - - @override - void initState() { - super.initState(); - bloc = SearchResultListBloc(); - } - - @override - void dispose() { - bloc.close(); - super.dispose(); - } - - Widget _buildSectionHeader(String title) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8) + - const EdgeInsets.only(left: 8), - child: Opacity( - opacity: 0.6, - child: FlowyText(title, fontSize: 12), - ), - ); - - Widget _buildAIOverviewSection(BuildContext context) { - final state = context.read().state; - - if (state.generatingAIOverview) { - return Row( - children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), - const HSpace(10), - const AIOverviewIndicator(), - ], - ); - } - - if (widget.resultSummaries.isNotEmpty) { - if (!bloc.state.userHovered) { - WidgetsBinding.instance.addPostFrameCallback( - (_) { - bloc.add( - SearchResultListEvent.onHoverSummary( - summary: widget.resultSummaries[0], - userHovered: false, - ), - ); - }, - ); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionHeader(LocaleKeys.commandPalette_aiOverview.tr()), - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.resultSummaries.length, - separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) => SearchSummaryCell( - summary: widget.resultSummaries[index], - isHovered: bloc.state.hoveredSummary != null, + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + separatorBuilder: (_, __) => const Divider(height: 0), + itemCount: results.length + 1, + itemBuilder: (_, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8) + + const EdgeInsets.only(left: 16), + child: FlowyText( + LocaleKeys.commandPalette_bestMatches.tr(), ), - ), - ], - ); - } - - return const SizedBox.shrink(); - } - - Widget _buildResultsSection(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 8), - _buildSectionHeader(LocaleKeys.commandPalette_bestMatches.tr()), - ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: widget.resultItems.length, - separatorBuilder: (_, __) => const Divider(height: 0), - itemBuilder: (_, index) { - final item = widget.resultItems[index]; - return SearchResultCell( - item: item, - isTrashed: widget.trash.any((t) => t.id == item.id), - isHovered: bloc.state.hoveredResult?.id == item.id, - ); - }, - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: BlocProvider.value( - value: bloc, - child: BlocListener( - listener: (context, state) { - if (state.openPageId != null) { - FlowyOverlay.pop(context); - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction(objectId: state.openPageId!), - ), - ); - } - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - flex: 7, - child: BlocBuilder( - buildWhen: (previous, current) => - previous.hoveredResult != current.hoveredResult || - previous.hoveredSummary != current.hoveredSummary, - builder: (context, state) { - return ListView( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - children: [ - _buildAIOverviewSection(context), - const VSpace(10), - if (widget.resultItems.isNotEmpty) - _buildResultsSection(context), - ], - ); - }, - ), - ), - const HSpace(10), - if (widget.resultItems - .any((item) => item.content.isNotEmpty)) ...[ - const VerticalDivider( - thickness: 1.0, - ), - Flexible( - flex: 3, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - child: const SearchCellPreview(), - ), - ), - ], - ], - ), - ), - ), - ); - } -} - -class SearchCellPreview extends StatelessWidget { - const SearchCellPreview({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.hoveredSummary != null) { - return SearchSummaryPreview(summary: state.hoveredSummary!); - } else if (state.hoveredResult != null) { - return SearchResultPreview(data: state.hoveredResult!); + ); } - return const SizedBox.shrink(); + + final result = results[index - 1]; + return SearchResultTile( + result: result, + onSelected: () => FlowyOverlay.pop(context), + isTrashed: trash.any((t) => t.id == result.viewId), + ); }, ); } } - -class AIOverviewIndicator extends StatelessWidget { - const AIOverviewIndicator({ - super.key, - this.duration = const Duration(seconds: 1), - }); - - final Duration duration; - - @override - Widget build(BuildContext context) { - final slice = Duration(milliseconds: duration.inMilliseconds ~/ 5); - return SelectionContainer.disabled( - child: SizedBox( - height: 20, - width: 100, - child: SeparatedRow( - separatorBuilder: () => const HSpace(4), - children: [ - buildDot(const Color(0xFF9327FF)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(duration: slice * 2, begin: 0, end: 0), - buildDot(const Color(0xFFFB006D)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice, begin: 0, end: 0) - .then() - .slideY(begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0) - .then() - .slideY(begin: 0, end: 0), - buildDot(const Color(0xFFFFCE00)) - .animate(onPlay: (controller) => controller.repeat()) - .slideY(duration: slice * 2, begin: 0, end: 0) - .then() - .slideY(duration: slice, begin: 0, end: -1) - .then() - .slideY(begin: -1, end: 1) - .then() - .slideY(begin: 1, end: 0), - ], - ), - ), - ); - } - - Widget buildDot(Color color) { - return SizedBox.square( - dimension: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(2), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart b/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart deleted file mode 100644 index 84b8f6646b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/command_palette/widgets/search_summary_cell.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; -import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SearchSummaryCell extends StatelessWidget { - const SearchSummaryCell({ - required this.summary, - required this.isHovered, - super.key, - }); - - final SearchSummaryPB summary; - final bool isHovered; - - @override - Widget build(BuildContext context) { - return FlowyHover( - isSelected: () => isHovered, - onHover: (value) { - context.read().add( - SearchResultListEvent.onHoverSummary( - summary: summary, - userHovered: true, - ), - ); - }, - style: HoverStyle( - borderRadius: BorderRadius.circular(8), - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - foregroundColorOnHover: AFThemeExtension.of(context).textColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: FlowyText( - summary.content, - maxLines: 20, - ), - ), - ); - } -} - -class SearchSummaryPreview extends StatelessWidget { - const SearchSummaryPreview({ - required this.summary, - super.key, - }); - - final SearchSummaryPB summary; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (summary.highlights.isNotEmpty) ...[ - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_aiOverviewMoreDetails.tr(), - fontSize: 12, - ), - ), - const VSpace(6), - SearchSummaryHighlight(text: summary.highlights), - const VSpace(36), - ], - - Opacity( - opacity: 0.5, - child: FlowyText( - LocaleKeys.commandPalette_aiOverviewSource.tr(), - fontSize: 12, - ), - ), - // Sources - const VSpace(6), - ...summary.sources.map((e) => SearchSummarySource(source: e)), - ], - ); - } -} - -class SearchSummaryHighlight extends StatelessWidget { - const SearchSummaryHighlight({ - required this.text, - super.key, - }); - - final String text; - - @override - Widget build(BuildContext context) { - return AIMarkdownText(markdown: text); - } -} - -class SearchSummarySource extends StatelessWidget { - const SearchSummarySource({ - required this.source, - super.key, - }); - - final SearchSourcePB source; - - @override - Widget build(BuildContext context) { - final icon = source.icon.getIcon(); - return FlowyTooltip( - message: LocaleKeys.commandPalette_clickToOpenPage.tr(), - child: SizedBox( - height: 30, - child: FlowyButton( - leftIcon: icon, - hoverColor: - Theme.of(context).colorScheme.primary.withValues(alpha: 0.1), - text: FlowyText(source.displayName), - onTap: () { - context.read().add( - SearchResultListEvent.openPage(pageId: source.id), - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 619ee4e229..5db9efb251 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -1,3 +1,6 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; @@ -24,14 +27,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_infra_ui/style_widget/container.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:sentry/sentry.dart'; import 'package:sized_context/sized_context.dart'; import 'package:styled_widget/styled_widget.dart'; import '../widgets/edit_panel/edit_panel.dart'; -import '../widgets/sidebar_resizer.dart'; + import 'home_layout.dart'; import 'home_stack.dart'; @@ -52,11 +53,10 @@ class DesktopHomeScreen extends StatelessWidget { return _buildLoading(); } - final workspaceLatest = snapshots.data?[0].fold( - (workspaceLatestPB) => workspaceLatestPB as WorkspaceLatestPB, + final workspaceSetting = snapshots.data?[0].fold( + (workspaceSettingPB) => workspaceSettingPB as WorkspaceSettingPB, (error) => null, ); - final userProfile = snapshots.data?[1].fold( (userProfilePB) => userProfilePB as UserProfilePB, (error) => null, @@ -64,33 +64,23 @@ class DesktopHomeScreen extends StatelessWidget { // In the unlikely case either of the above is null, eg. // when a workspace is already open this can happen. - if (workspaceLatest == null || userProfile == null) { + if (workspaceSetting == null || userProfile == null) { return const WorkspaceFailedScreen(); } - Sentry.configureScope( - (scope) => scope.setUser( - SentryUser( - id: userProfile.id.toString(), - ), - ), - ); - return AFFocusManager( child: MultiBlocProvider( key: ValueKey(userProfile.id), providers: [ - BlocProvider.value( - value: getIt(), - ), + BlocProvider.value(value: getIt()), BlocProvider.value(value: getIt()), BlocProvider( create: (_) => - HomeBloc(workspaceLatest)..add(const HomeEvent.initial()), + HomeBloc(workspaceSetting)..add(const HomeEvent.initial()), ), BlocProvider( create: (_) => HomeSettingBloc( - workspaceLatest, + workspaceSetting, context.read(), context.widthPx, )..add(const HomeSettingEvent.initial()), @@ -137,7 +127,7 @@ class DesktopHomeScreen extends StatelessWidget { child: _buildBody( context, userProfile, - workspaceLatest, + workspaceSetting, ), ), ), @@ -157,7 +147,7 @@ class DesktopHomeScreen extends StatelessWidget { Widget _buildBody( BuildContext context, UserProfilePB userProfile, - WorkspaceLatestPB workspaceSetting, + WorkspaceSettingPB workspaceSetting, ) { final layout = HomeLayout(context); final homeStack = HomeStack( @@ -165,21 +155,19 @@ class DesktopHomeScreen extends StatelessWidget { delegate: DesktopHomeScreenStackAdaptor(context), userProfile: userProfile, ); - final sidebar = _buildHomeSidebar( + final menu = _buildHomeSidebar( context, layout: layout, userProfile: userProfile, workspaceSetting: workspaceSetting, ); - - final homeMenuResizer = - layout.showMenu ? const SidebarResizer() : const SizedBox.shrink(); + final homeMenuResizer = _buildHomeMenuResizer(context, layout: layout); final editPanel = _buildEditPanel(context, layout: layout); return _layoutWidgets( layout: layout, homeStack: homeStack, - sidebar: sidebar, + homeMenu: menu, editPanel: editPanel, bubble: const QuestionBubble(), homeMenuResizer: homeMenuResizer, @@ -190,7 +178,7 @@ class DesktopHomeScreen extends StatelessWidget { BuildContext context, { required HomeLayout layout, required UserProfilePB userProfile, - required WorkspaceLatestPB workspaceSetting, + required WorkspaceSettingPB workspaceSetting, }) { final homeMenu = HomeSideBar( userProfile: userProfile, @@ -227,9 +215,42 @@ class DesktopHomeScreen extends StatelessWidget { ); } + Widget _buildHomeMenuResizer( + BuildContext context, { + required HomeLayout layout, + }) { + if (!layout.showMenu) { + return const SizedBox.shrink(); + } + + return MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: GestureDetector( + dragStartBehavior: DragStartBehavior.down, + onHorizontalDragStart: (details) => context + .read() + .add(const HomeSettingEvent.editPanelResizeStart()), + onHorizontalDragUpdate: (details) => context + .read() + .add(HomeSettingEvent.editPanelResized(details.localPosition.dx)), + onHorizontalDragEnd: (details) => context + .read() + .add(const HomeSettingEvent.editPanelResizeEnd()), + onHorizontalDragCancel: () => context + .read() + .add(const HomeSettingEvent.editPanelResizeEnd()), + behavior: HitTestBehavior.translucent, + child: SizedBox( + width: 10, + height: MediaQuery.of(context).size.height, + ), + ), + ); + } + Widget _layoutWidgets({ required HomeLayout layout, - required Widget sidebar, + required Widget homeMenu, required Widget homeStack, required Widget editPanel, required Widget bubble, @@ -263,7 +284,7 @@ class DesktopHomeScreen extends StatelessWidget { bottom: 0, width: layout.editPanelWidth, ), - sidebar + homeMenu .animatedPanelX( closeX: -layout.menuWidth, isClosed: !layout.showMenu, @@ -272,7 +293,7 @@ class DesktopHomeScreen extends StatelessWidget { ) .positioned(left: 0, top: 0, width: layout.menuWidth, bottom: 0), homeMenuResizer - .positioned(left: layout.menuWidth) + .positioned(left: layout.menuWidth - 5) .animate(layout.animDuration, Curves.easeOutQuad), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_layout.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_layout.dart index 98139f1db7..1da2ca32fa 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_layout.dart @@ -1,5 +1,4 @@ import 'dart:io' show Platform; -import 'dart:math'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:flowy_infra/size.dart'; @@ -14,11 +13,8 @@ class HomeLayout { HomeLayout(BuildContext context) { final homeSetting = context.read().state; showEditPanel = homeSetting.panelContext != null; - - menuWidth = max( - HomeSizes.minimumSidebarWidth + homeSetting.resizeOffset, - HomeSizes.minimumSidebarWidth, - ); + menuWidth = Sizes.sideBarWidth; + menuWidth += homeSetting.resizeOffset; final screenWidthPx = context.widthPx; context diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index 18d76057a2..b35ae64ac5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -8,7 +8,6 @@ class HomeSizes { static const double workspaceSectionHeight = 32; static const double searchSectionHeight = 30; static const double newPageSectionHeight = 30; - static const double minimumSidebarWidth = 268; } class HomeInsets { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 464394cd39..405360f55b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -1,16 +1,8 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:math'; - +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy/core/frameless_window.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/blank/blank.dart'; -import 'package:appflowy/shared/window_title_bar.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/navigation.dart'; @@ -18,16 +10,12 @@ import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; -import 'package:universal_platform/universal_platform.dart'; import 'home_layout.dart'; @@ -37,7 +25,7 @@ abstract class HomeStackDelegate { void didDeleteStackWidget(ViewPB view, int? index); } -class HomeStack extends StatefulWidget { +class HomeStack extends StatelessWidget { const HomeStack({ super.key, required this.delegate, @@ -49,122 +37,39 @@ class HomeStack extends StatefulWidget { final HomeLayout layout; final UserProfilePB userProfile; - @override - State createState() => _HomeStackState(); -} - -class _HomeStackState extends State { - int selectedIndex = 0; - @override Widget build(BuildContext context) { + final pageController = PageController(); + return BlocProvider.value( value: getIt(), child: BlocBuilder( - builder: (context, state) => Column( - children: [ - if (UniversalPlatform.isWindows) - Column( - mainAxisSize: MainAxisSize.min, - children: [ - WindowTitleBar( - leftChildren: [_buildToggleMenuButton(context)], - ), - ], + builder: (context, state) { + return Column( + children: [ + Padding( + padding: EdgeInsets.only(left: layout.menuSpacing), + child: TabsManager(pageController: pageController), ), - Padding( - padding: EdgeInsets.only(left: widget.layout.menuSpacing), - child: TabsManager( - onIndexChanged: (index) { - if (selectedIndex != index) { - // Unfocus editor to hide selection toolbar - FocusScope.of(context).unfocus(); - - context.read().add(TabsEvent.selectTab(index)); - setState(() => selectedIndex = index); - } - }, + state.currentPageManager.stackTopBar(layout: layout), + Expanded( + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: pageController, + children: state.pageManagers + .map( + (pm) => PageStack( + pageManager: pm, + delegate: delegate, + userProfile: userProfile, + ), + ) + .toList(), + ), ), - ), - Expanded( - child: IndexedStack( - index: selectedIndex, - children: state.pageManagers - .map( - (pm) => LayoutBuilder( - builder: (context, constraints) { - return Row( - children: [ - Expanded( - child: Column( - children: [ - pm.stackTopBar(layout: widget.layout), - Expanded( - child: PageStack( - pageManager: pm, - delegate: widget.delegate, - userProfile: widget.userProfile, - ), - ), - ], - ), - ), - SecondaryView( - pageManager: pm, - adaptedPercentageWidth: - constraints.maxWidth * 3 / 7, - ), - ], - ); - }, - ), - ) - .toList(), - ), - ), - ], - ), - ), - ); - } - - Widget _buildToggleMenuButton(BuildContext context) { - if (!context.read().state.isMenuCollapsed) { - return const SizedBox.shrink(); - } - - final textSpan = TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.sideBar_openSidebar.tr()}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', - style: context - .tooltipTextStyle() - ?.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), - ), - ), - ), + ], + ); + }, ), ); } @@ -179,6 +84,7 @@ class PageStack extends StatefulWidget { }); final PageManager pageManager; + final HomeStackDelegate delegate; final UserProfilePB userProfile; @@ -209,349 +115,6 @@ class _PageStackState extends State bool get wantKeepAlive => true; } -class SecondaryView extends StatefulWidget { - const SecondaryView({ - super.key, - required this.pageManager, - required this.adaptedPercentageWidth, - }); - - final PageManager pageManager; - final double adaptedPercentageWidth; - - @override - State createState() => _SecondaryViewState(); -} - -class _SecondaryViewState extends State - with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { - final overlayController = OverlayPortalController(); - final layerLink = LayerLink(); - - late final ValueNotifier widthNotifier; - - late final AnimationController animationController; - late Animation widthAnimation; - late final Animation offsetAnimation; - - late bool hasSecondaryView; - - CurvedAnimation get curveAnimation => CurvedAnimation( - parent: animationController, - curve: Curves.easeOut, - ); - - @override - void initState() { - super.initState(); - widget.pageManager.showSecondaryPluginNotifier - .addListener(onShowSecondaryChanged); - final width = widget.pageManager.showSecondaryPluginNotifier.value - ? max(450.0, widget.adaptedPercentageWidth) - : 0.0; - widthNotifier = ValueNotifier(width) - ..addListener(updateWidthAnimation); - - animationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - - widthAnimation = Tween( - begin: 0.0, - end: width, - ).animate(curveAnimation); - offsetAnimation = Tween( - begin: const Offset(1.0, 0.0), - end: Offset.zero, - ).animate(curveAnimation); - - widget.pageManager.secondaryNotifier.addListener(onSecondaryViewChanged); - onSecondaryViewChanged(); - - overlayController.show(); - } - - @override - void dispose() { - widget.pageManager.showSecondaryPluginNotifier - .removeListener(onShowSecondaryChanged); - widget.pageManager.secondaryNotifier.removeListener(onSecondaryViewChanged); - widthNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final isLightMode = Theme.of(context).isLightMode; - return OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return ValueListenableBuilder( - valueListenable: widget.pageManager.showSecondaryPluginNotifier, - builder: (context, isShowing, child) { - return CompositedTransformFollower( - link: layerLink, - followerAnchor: Alignment.topRight, - offset: const Offset(0.0, 120.0), - child: Align( - alignment: AlignmentDirectional.topEnd, - child: AnimatedSwitcher( - duration: 150.milliseconds, - transitionBuilder: (child, animation) { - return NonClippingSizeTransition( - sizeFactor: animation, - axis: Axis.horizontal, - axisAlignment: -1, - child: child, - ); - }, - child: isShowing || !hasSecondaryView - ? const SizedBox.shrink() - : GestureDetector( - onTap: () => widget.pageManager - .showSecondaryPluginNotifier.value = true, - child: Container( - height: 36, - width: 36, - decoration: BoxDecoration( - borderRadius: getBorderRadius(), - color: Theme.of(context).colorScheme.surface, - boxShadow: [ - BoxShadow( - offset: const Offset(0, 4), - blurRadius: 20, - color: isLightMode - ? const Color(0x1F1F2329) - : Theme.of(context) - .shadowColor - .withValues(alpha: 0.08), - ), - ], - ), - child: FlowyHover( - style: HoverStyle( - borderRadius: getBorderRadius(), - border: getBorder(context), - ), - child: const Center( - child: FlowySvg( - FlowySvgs.rename_s, - size: Size.square(16.0), - ), - ), - ), - ), - ), - ), - ), - ); - }, - ); - }, - child: CompositedTransformTarget( - link: layerLink, - child: Container( - color: Theme.of(context).colorScheme.surface, - child: FocusTraversalGroup( - child: ValueListenableBuilder( - valueListenable: widthNotifier, - builder: (context, value, child) { - return AnimatedBuilder( - animation: Listenable.merge([ - widthAnimation, - offsetAnimation, - ]), - builder: (context, child) { - return Container( - width: widthAnimation.value, - alignment: Alignment( - offsetAnimation.value.dx, - offsetAnimation.value.dy, - ), - child: OverflowBox( - alignment: AlignmentDirectional.centerStart, - maxWidth: value, - child: SecondaryViewResizer( - pageManager: widget.pageManager, - notifier: widthNotifier, - child: Column( - children: [ - widget.pageManager.stackSecondaryTopBar(value), - Expanded( - child: widget.pageManager - .stackSecondaryWidget(value), - ), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ), - ), - ), - ); - } - - BoxBorder getBorder(BuildContext context) { - final isLightMode = Theme.of(context).isLightMode; - final borderSide = BorderSide( - color: isLightMode - ? const Color(0x141F2329) - : Theme.of(context).dividerColor, - ); - - return Border( - left: borderSide, - top: borderSide, - bottom: borderSide, - ); - } - - BorderRadius getBorderRadius() { - return const BorderRadius.only( - topLeft: Radius.circular(12.0), - bottomLeft: Radius.circular(12.0), - ); - } - - void onSecondaryViewChanged() { - hasSecondaryView = widget.pageManager.secondaryNotifier.plugin.pluginType != - PluginType.blank; - } - - void onShowSecondaryChanged() async { - if (widget.pageManager.showSecondaryPluginNotifier.value) { - widthNotifier.value = max(450.0, widget.adaptedPercentageWidth); - updateWidthAnimation(); - await animationController.forward(); - } else { - updateWidthAnimation(); - await animationController.reverse(); - setState(() => widthNotifier.value = 0.0); - } - } - - void updateWidthAnimation() { - widthAnimation = Tween( - begin: 0.0, - end: widthNotifier.value, - ).animate(curveAnimation); - } - - @override - bool get wantKeepAlive => true; -} - -class SecondaryViewResizer extends StatefulWidget { - const SecondaryViewResizer({ - super.key, - required this.pageManager, - required this.notifier, - required this.child, - }); - - final PageManager pageManager; - final ValueNotifier notifier; - final Widget child; - - @override - State createState() => _SecondaryViewResizerState(); -} - -class _SecondaryViewResizerState extends State { - final overlayController = OverlayPortalController(); - final layerLink = LayerLink(); - - bool isHover = false; - bool isDragging = false; - Timer? showHoverTimer; - - @override - void initState() { - super.initState(); - overlayController.show(); - } - - @override - Widget build(BuildContext context) { - return OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return CompositedTransformFollower( - showWhenUnlinked: false, - link: layerLink, - targetAnchor: Alignment.center, - followerAnchor: Alignment.center, - child: Center( - child: MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - onEnter: (_) { - showHoverTimer = Timer(const Duration(milliseconds: 500), () { - setState(() => isHover = true); - }); - }, - onExit: (_) { - showHoverTimer?.cancel(); - setState(() => isHover = false); - }, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragStart: (_) => setState(() => isDragging = true), - onHorizontalDragUpdate: (details) { - final newWidth = MediaQuery.sizeOf(context).width - - details.globalPosition.dx; - if (newWidth >= 450.0) { - widget.notifier.value = newWidth; - } - }, - onHorizontalDragEnd: (_) => setState(() => isDragging = false), - child: TweenAnimationBuilder( - tween: ColorTween( - end: isHover || isDragging - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - duration: const Duration(milliseconds: 100), - builder: (context, color, child) { - return SizedBox( - width: 11, - child: Center( - child: Container( - color: color, - width: 2, - ), - ), - ); - }, - ), - ), - ), - ), - ); - }, - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - CompositedTransformTarget( - link: layerLink, - child: Container( - width: 1, - color: Theme.of(context).dividerColor, - ), - ), - Flexible(child: widget.child), - ], - ), - ); - } -} - class FadingIndexedStack extends StatefulWidget { const FadingIndexedStack({ super.key, @@ -592,17 +155,18 @@ class FadingIndexedStackState extends State { return TweenAnimationBuilder( duration: _targetOpacity > 0 ? widget.duration : 0.milliseconds, tween: Tween(begin: 0, end: _targetOpacity), - builder: (_, value, child) => Opacity(opacity: value, child: child), + builder: (_, value, child) { + return Opacity(opacity: value, child: child); + }, child: IndexedStack(index: widget.index, children: widget.children), ); } } abstract mixin class NavigationItem { - String? get viewName; Widget get leftBarItem; Widget? get rightBarItem => null; - Widget tabBarItem(String pluginId, [bool shortForm = false]); + Widget tabBarItem(String pluginId); NavigationCallback get action => (id) => throw UnimplementedError(); } @@ -615,23 +179,17 @@ class PageNotifier extends ChangeNotifier { Widget get titleWidget => _plugin.widgetBuilder.leftBarItem; - Widget tabBarWidget( - String pluginId, [ - bool shortForm = false, - ]) => - _plugin.widgetBuilder.tabBarItem(pluginId, shortForm); + Widget tabBarWidget(String pluginId) => + _plugin.widgetBuilder.tabBarItem(pluginId); - void setPlugin( - Plugin newPlugin, { - required bool setLatest, - bool disposeExisting = true, - }) { - if (newPlugin.id != plugin.id && disposeExisting) { - _plugin.dispose(); - } + /// This is the only place where the plugin is set. + /// No need compare the old plugin with the new plugin. Just set it. + void setPlugin(Plugin newPlugin, bool setLatest) { + _plugin.dispose(); + newPlugin.init(); // Set the plugin view as the latest view. - if (setLatest && newPlugin.id.isNotEmpty) { + if (setLatest) { FolderEventSetLatestView(ViewIdPB(value: newPlugin.id)).send(); } @@ -647,52 +205,32 @@ class PageManager { PageManager(); final PageNotifier _notifier = PageNotifier(); - final PageNotifier _secondaryNotifier = PageNotifier(); PageNotifier get notifier => _notifier; - PageNotifier get secondaryNotifier => _secondaryNotifier; - bool isPinned = false; - - final showSecondaryPluginNotifier = ValueNotifier(false); + Widget title() { + return _notifier.plugin.widgetBuilder.leftBarItem; + } Plugin get plugin => _notifier.plugin; - void setPlugin(Plugin newPlugin, bool setLatest, [bool init = true]) { - if (init) { - newPlugin.init(); - } - _notifier.setPlugin(newPlugin, setLatest: setLatest); + void setPlugin(Plugin newPlugin, bool setLatest) { + _notifier.setPlugin(newPlugin, setLatest); } - void setSecondaryPlugin(Plugin newPlugin) { - newPlugin.init(); - _secondaryNotifier.setPlugin(newPlugin, setLatest: false); - } - - void expandSecondaryPlugin() { - _notifier.setPlugin(_secondaryNotifier.plugin, setLatest: true); - _secondaryNotifier.setPlugin( - BlankPagePlugin(), - setLatest: false, - disposeExisting: false, - ); - } - - void showSecondaryPlugin() { - showSecondaryPluginNotifier.value = true; - } - - void hideSecondaryPlugin() { - showSecondaryPluginNotifier.value = false; + void setStackWithId(String id) { + // Navigate to the page with id } Widget stackTopBar({required HomeLayout layout}) { - return ChangeNotifierProvider.value( - value: _notifier, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _notifier), + ], child: Selector( selector: (context, notifier) => notifier.titleWidget, builder: (_, __, child) => MoveWindowDetector( + showTitleBar: true, child: HomeTopBar(layout: layout), ), ), @@ -703,14 +241,10 @@ class PageManager { required UserProfilePB userProfile, required Function(ViewPB, int?) onDeleted, }) { - return ChangeNotifierProvider.value( - value: _notifier, - child: Consumer( - builder: (_, notifier, __) { - if (notifier.plugin.pluginType == PluginType.blank) { - return const BlankPage(); - } - + return MultiProvider( + providers: [ChangeNotifierProvider.value(value: _notifier)], + child: Consumer( + builder: (_, PageNotifier notifier, __) { return FadingIndexedStack( index: getIt().indexOf(notifier.plugin.pluginType), children: getIt().supportPluginTypes.map( @@ -725,6 +259,7 @@ class PageManager { shrinkWrap: false, ); + // TODO(Xazin): Board should fill up full width return Padding( padding: builder.contentPadding, child: pluginWidget, @@ -740,77 +275,18 @@ class PageManager { ); } - Widget stackSecondaryWidget(double width) { - return ValueListenableBuilder( - valueListenable: showSecondaryPluginNotifier, - builder: (context, value, child) { - if (width == 0.0) { - return const SizedBox.shrink(); - } - - return child!; - }, - child: ChangeNotifierProvider.value( - value: _secondaryNotifier, - child: Selector( - selector: (context, notifier) => notifier.plugin.widgetBuilder, - builder: (_, widgetBuilder, __) { - return widgetBuilder.buildWidget( - context: PluginContext(), - shrinkWrap: false, - ); - }, - ), - ), - ); - } - - Widget stackSecondaryTopBar(double width) { - return ValueListenableBuilder( - valueListenable: showSecondaryPluginNotifier, - builder: (context, value, child) { - if (width == 0.0) { - return const SizedBox.shrink(); - } - - return child!; - }, - child: ChangeNotifierProvider.value( - value: _secondaryNotifier, - child: Selector( - selector: (context, notifier) => notifier.plugin.widgetBuilder, - builder: (_, widgetBuilder, __) { - return const MoveWindowDetector( - child: HomeSecondaryTopBar(), - ); - }, - ), - ), - ); - } - void dispose() { _notifier.dispose(); - _secondaryNotifier.dispose(); - showSecondaryPluginNotifier.dispose(); } } -class HomeTopBar extends StatefulWidget { +class HomeTopBar extends StatelessWidget { const HomeTopBar({super.key, required this.layout}); final HomeLayout layout; - @override - State createState() => _HomeTopBarState(); -} - -class _HomeTopBarState extends State - with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { - super.build(context); - return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, @@ -823,7 +299,7 @@ class _HomeTopBarState extends State ), child: Row( children: [ - HSpace(widget.layout.menuSpacing), + HSpace(layout.menuSpacing), const FlowyNavigation(), const HSpace(16), ChangeNotifierProvider.value( @@ -839,191 +315,4 @@ class _HomeTopBarState extends State ), ); } - - @override - bool get wantKeepAlive => true; -} - -class HomeSecondaryTopBar extends StatelessWidget { - const HomeSecondaryTopBar({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: HomeInsets.topBarTitleHorizontalPadding, - vertical: HomeInsets.topBarTitleVerticalPadding, - ), - child: Row( - children: [ - FlowyIconButton( - width: 24, - tooltipText: LocaleKeys.sideBar_closeSidebar.tr(), - radius: const BorderRadius.all(Radius.circular(8.0)), - icon: const FlowySvg( - FlowySvgs.show_menu_s, - size: Size.square(16), - ), - onPressed: () { - getIt().add(const TabsEvent.closeSecondaryPlugin()); - }, - ), - const HSpace(8.0), - FlowyIconButton( - width: 24, - tooltipText: LocaleKeys.sideBar_expandSidebar.tr(), - radius: const BorderRadius.all(Radius.circular(8.0)), - icon: const FlowySvg( - FlowySvgs.full_view_s, - size: Size.square(16), - ), - onPressed: () { - getIt().add(const TabsEvent.expandSecondaryPlugin()); - }, - ), - Expanded( - child: Align( - alignment: AlignmentDirectional.centerEnd, - child: ChangeNotifierProvider.value( - value: Provider.of(context, listen: false), - child: Consumer( - builder: (_, PageNotifier notifier, __) => - notifier.plugin.widgetBuilder.rightBarItem ?? - const SizedBox.shrink(), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -/// A version of Flutter's built in SizeTransition widget that clips the child -/// more sparingly than the original. -class NonClippingSizeTransition extends AnimatedWidget { - const NonClippingSizeTransition({ - super.key, - this.axis = Axis.vertical, - required Animation sizeFactor, - this.axisAlignment = 0.0, - this.fixedCrossAxisSizeFactor, - this.child, - }) : assert( - fixedCrossAxisSizeFactor == null || fixedCrossAxisSizeFactor >= 0.0, - ), - super(listenable: sizeFactor); - - /// [Axis.horizontal] if [sizeFactor] modifies the width, otherwise - /// [Axis.vertical]. - final Axis axis; - - /// The animation that controls the (clipped) size of the child. - /// - /// The width or height (depending on the [axis] value) of this widget will be - /// its intrinsic width or height multiplied by [sizeFactor]'s value at the - /// current point in the animation. - /// - /// If the value of [sizeFactor] is less than one, the child will be clipped - /// in the appropriate axis. - Animation get sizeFactor => listenable as Animation; - - /// Describes how to align the child along the axis that [sizeFactor] is - /// modifying. - /// - /// A value of -1.0 indicates the top when [axis] is [Axis.vertical], and the - /// start when [axis] is [Axis.horizontal]. The start is on the left when the - /// text direction in effect is [TextDirection.ltr] and on the right when it - /// is [TextDirection.rtl]. - /// - /// A value of 1.0 indicates the bottom or end, depending upon the [axis]. - /// - /// A value of 0.0 (the default) indicates the center for either [axis] value. - final double axisAlignment; - - /// The factor by which to multiply the cross axis size of the child. - /// - /// If the value of [fixedCrossAxisSizeFactor] is less than one, the child - /// will be clipped along the appropriate axis. - /// - /// If `null` (the default), the cross axis size is as large as the parent. - final double? fixedCrossAxisSizeFactor; - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget? child; - - @override - Widget build(BuildContext context) { - final AlignmentDirectional alignment; - final Edge edge; - if (axis == Axis.vertical) { - alignment = AlignmentDirectional(-1.0, axisAlignment); - edge = switch (axisAlignment) { -1.0 => Edge.bottom, _ => Edge.top }; - } else { - alignment = AlignmentDirectional(axisAlignment, -1.0); - edge = switch (axisAlignment) { -1.0 => Edge.right, _ => Edge.left }; - } - return ClipRect( - clipper: EdgeRectClipper(edge: edge, margin: 20), - child: Align( - alignment: alignment, - heightFactor: axis == Axis.vertical - ? max(sizeFactor.value, 0.0) - : fixedCrossAxisSizeFactor, - widthFactor: axis == Axis.horizontal - ? max(sizeFactor.value, 0.0) - : fixedCrossAxisSizeFactor, - child: child, - ), - ); - } -} - -class EdgeRectClipper extends CustomClipper { - const EdgeRectClipper({ - required this.edge, - required this.margin, - }); - - final Edge edge; - final double margin; - - @override - Rect getClip(Size size) { - return switch (edge) { - Edge.left => - Rect.fromLTRB(0.0, -margin, size.width + margin, size.height + margin), - Edge.right => - Rect.fromLTRB(-margin, -margin, size.width, size.height + margin), - Edge.top => - Rect.fromLTRB(-margin, 0.0, size.width + margin, size.height + margin), - Edge.bottom => Rect.fromLTRB(-margin, -margin, size.width, size.height), - }; - } - - @override - bool shouldReclip(covariant CustomClipper oldClipper) => false; -} - -enum Edge { - left, - top, - right, - bottom; - - bool get isHorizontal => switch (this) { - left || right => true, - _ => false, - }; - - bool get isVertical => !isHorizontal; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index af3db13d53..ac2a57ec80 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -17,18 +17,6 @@ import 'package:scaled_app/scaled_app.dart'; typedef KeyDownHandler = void Function(HotKey hotKey); ValueNotifier switchToTheNextSpace = ValueNotifier(0); -ValueNotifier createNewPageNotifier = ValueNotifier(0); - -@visibleForTesting -final zoomInKeyCodes = [KeyCode.equal, KeyCode.numpadAdd, KeyCode.add]; -@visibleForTesting -final zoomOutKeyCodes = [KeyCode.minus, KeyCode.numpadSubtract]; -@visibleForTesting -final resetZoomKeyCodes = [KeyCode.digit0, KeyCode.numpad0]; - -// Use a global value to store the zoom level and update it in the hotkeys. -@visibleForTesting -double appflowyScaleFactor = 1.0; /// Helper class that utilizes the global [HotKeyManager] to easily /// add a [HotKey] with different handlers. @@ -148,45 +136,32 @@ class _HomeHotKeysState extends State { ), // Scale up/down the app - // In some keyboards, the system returns equal as + keycode, while others may return add as + keycode, so add them both as zoom in key. - ...zoomInKeyCodes.map( - (keycode) => HotKeyItem( - hotKey: HotKey( - keycode, - modifiers: [ - Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, - ], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => _scaleWithStep(0.1), + HotKeyItem( + hotKey: HotKey( + KeyCode.equal, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + scope: HotKeyScope.inapp, ), + keyDownHandler: (_) => _scaleWithStep(0.1), ), - ...zoomOutKeyCodes.map( - (keycode) => HotKeyItem( - hotKey: HotKey( - keycode, - modifiers: [ - Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, - ], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => _scaleWithStep(-0.1), + HotKeyItem( + hotKey: HotKey( + KeyCode.minus, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + scope: HotKeyScope.inapp, ), + keyDownHandler: (_) => _scaleWithStep(-0.1), ), // Reset app scaling - ...resetZoomKeyCodes.map( - (keycode) => HotKeyItem( - hotKey: HotKey( - keycode, - modifiers: [ - Platform.isMacOS ? KeyModifier.meta : KeyModifier.control, - ], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => _scale(1), + HotKeyItem( + hotKey: HotKey( + KeyCode.digit0, + modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], + scope: HotKeyScope.inapp, ), + keyDownHandler: (_) => _scaleToSize(1), ), // Switch to the next space @@ -199,16 +174,6 @@ class _HomeHotKeysState extends State { keyDownHandler: (_) => switchToTheNextSpace.value++, ), - // Create a new page - HotKeyItem( - hotKey: HotKey( - KeyCode.keyN, - modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], - scope: HotKeyScope.inapp, - ), - keyDownHandler: (_) => createNewPageNotifier.value++, - ), - // Open settings dialog openSettingsHotKey(context, widget.userProfile), ]; @@ -216,12 +181,14 @@ class _HomeHotKeysState extends State { @override void initState() { super.initState(); + _registerHotKeys(context); } @override void didChangeDependencies() { super.didChangeDependencies(); + _registerHotKeys(context); } @@ -248,19 +215,11 @@ class _HomeHotKeysState extends State { Log.info('scale the app from $currentScaleFactor to $textScale'); - await _scale(textScale); + await _scaleToSize(textScale); } - Future _scale(double scaleFactor) async { - if (FlowyRunner.currentMode == IntegrationMode.integrationTest) { - // The integration test will fail if we check the scale factor in the test. - // #0 ScaledWidgetsFlutterBinding.Eval () - // #1 ScaledWidgetsFlutterBinding.instance (package:scaled_app/scaled_app.dart:66:62) - appflowyScaleFactor = scaleFactor; - } else { - ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => scaleFactor; - } - - await windowSizeManager.setScaleFactor(scaleFactor); + Future _scaleToSize(double size) async { + ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => size; + await windowSizeManager.setScaleFactor(size); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart index ca0773bf72..f73fa04cd0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart @@ -3,7 +3,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; @@ -12,12 +11,16 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FavoriteFolder extends StatefulWidget { - const FavoriteFolder({super.key, required this.views}); + const FavoriteFolder({ + super.key, + required this.views, + }); final List views; @@ -55,7 +58,8 @@ class _FavoriteFolderState extends State { .read() .add(const FolderEvent.expandOrUnExpand()), ), - buildReorderListView(context, state), + // pages + ..._buildViews(context, state, isHovered), if (state.isExpanded) ...[ // more button const VSpace(2), @@ -69,95 +73,62 @@ class _FavoriteFolderState extends State { ); } - Widget buildReorderListView( + Iterable _buildViews( BuildContext context, FolderState state, + ValueNotifier isHovered, ) { - if (!state.isExpanded) return const SizedBox.shrink(); - - final favoriteBloc = context.read(); - final pinnedViews = - favoriteBloc.state.pinnedViews.map((e) => e.item).toList(); - - if (pinnedViews.isEmpty) return const SizedBox.shrink(); - if (pinnedViews.length == 1) { - return buildViewItem(pinnedViews.first); + if (!state.isExpanded) { + return []; } - return Theme( - data: Theme.of(context).copyWith( - canvasColor: Colors.transparent, - shadowColor: Colors.transparent, - ), - child: ReorderableListView.builder( - shrinkWrap: true, - buildDefaultDragHandles: false, - itemCount: pinnedViews.length, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, i) { - final view = pinnedViews[i]; - return ReorderableDragStartListener( - key: ValueKey(view.id), - index: i, - child: DecoratedBox( - decoration: const BoxDecoration(color: Colors.transparent), - child: buildViewItem(view), + return context + .read() + .state + .pinnedViews + .map((e) => e.item) + .map( + (view) => ViewItem( + key: ValueKey( + '${FolderSpaceType.favorite.name} ${view.id}', ), - ); - }, - onReorder: (oldIndex, newIndex) { - favoriteBloc.add(FavoriteEvent.reorder(oldIndex, newIndex)); - }, - ), - ); - } + spaceType: FolderSpaceType.favorite, + isDraggable: false, + isFirstChild: view.id == widget.views.first.id, + isFeedback: false, + view: view, + leftPadding: HomeSpaceViewSizes.leftPadding, + leftIconBuilder: (_, __) => + const HSpace(HomeSpaceViewSizes.leftPadding), + level: 0, + isHovered: isHovered, + rightIconsBuilder: (context, view) => [ + FavoriteMoreActions(view: view), + const HSpace(8.0), + FavoritePinAction(view: view), + const HSpace(4.0), + ], + shouldRenderChildren: false, + shouldLoadChildViews: false, + onTertiarySelected: (_, view) => + context.read().openTab(view), + onSelected: (_, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } - Widget buildViewItem(ViewPB view) { - return ViewItem( - key: ValueKey('${FolderSpaceType.favorite.name} ${view.id}'), - spaceType: FolderSpaceType.favorite, - isDraggable: false, - isFirstChild: view.id == widget.views.first.id, - isFeedback: false, - view: view, - enableRightClickContext: true, - leftPadding: HomeSpaceViewSizes.leftPadding, - leftIconBuilder: (_, __) => const HSpace(HomeSpaceViewSizes.leftPadding), - level: 0, - isHovered: isHovered, - rightIconsBuilder: (context, view) => [ - Listener( - child: FavoriteMoreActions(view: view), - onPointerDown: (e) { - context.read().add(const ViewEvent.setIsEditing(true)); - }, - ), - const HSpace(8.0), - Listener( - child: FavoritePinAction(view: view), - onPointerDown: (e) { - context.read().add(const ViewEvent.setIsEditing(true)); - }, - ), - const HSpace(4.0), - ], - shouldRenderChildren: false, - shouldLoadChildViews: false, - onTertiarySelected: (_, view) => context.read().openTab(view), - onSelected: (_, view) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - - context.read().openPlugin(view); - }, - ); + context.read().openPlugin(view); + }, + ), + ); } } class FavoriteHeader extends StatelessWidget { - const FavoriteHeader({super.key, required this.onPressed}); + const FavoriteHeader({ + super.key, + required this.onPressed, + }); final VoidCallback onPressed; @@ -201,18 +172,30 @@ class FavoriteMoreButton extends StatelessWidget { constraints: const BoxConstraints( minWidth: minWidth, ), - popupBuilder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value(value: favoriteBloc), - BlocProvider.value(value: tabsBloc), - ], - child: const FavoriteMenu(minWidth: minWidth), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + borderRadius: 10.0, ), + popupBuilder: (_) { + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: favoriteBloc), + BlocProvider.value(value: tabsBloc), + ], + child: const FavoriteMenu(minWidth: minWidth), + ); + }, margin: EdgeInsets.zero, child: FlowyButton( + onTap: () {}, margin: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 7.0), - leftIcon: const FlowySvg(FlowySvgs.workspace_three_dots_s), - text: FlowyText.regular(LocaleKeys.button_more.tr()), + leftIcon: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + text: FlowyText.regular( + LocaleKeys.button_more.tr(), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart index 1bf6635037..71c36a0619 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart @@ -6,11 +6,12 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -40,7 +41,7 @@ class FavoriteMenu extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ const VSpace(4), - SpaceSearchField( + _FavoriteSearchField( width: minWidth - 2 * _kHorizontalPadding, onSearch: (context, text) { context @@ -136,7 +137,6 @@ class _FavoriteGroups extends StatelessWidget { state.otherViews, LocaleKeys.sideBar_others.tr(), ); - return Container( width: minWidth - 2 * _kHorizontalPadding, constraints: const BoxConstraints( @@ -149,18 +149,15 @@ class _FavoriteGroups extends StatelessWidget { children: [ if (today.isNotEmpty) ...[ ...today, + const VSpace(8), + const Divider(height: 1), + const VSpace(8), ], if (thisWeek.isNotEmpty) ...[ - if (today.isNotEmpty) ...[ - const FlowyDivider(), - const VSpace(16), - ], ...thisWeek, - ], - if ((thisWeek.isNotEmpty || today.isNotEmpty) && - others.isNotEmpty) ...[ - const FlowyDivider(), - const VSpace(16), + const VSpace(8), + const Divider(height: 1), + const VSpace(8), ], ...others.isNotEmpty && (today.isNotEmpty || thisWeek.isNotEmpty) ? others @@ -185,10 +182,13 @@ class _FavoriteGroups extends StatelessWidget { return [ if (views.isNotEmpty) ...[ if (showHeader) - FlowyText( - title, - fontSize: 12.0, - color: Theme.of(context).hintColor, + SizedBox( + height: 24, + child: FlowyText( + title, + fontSize: 12.0, + color: Theme.of(context).hintColor, + ), ), const VSpace(2), _FavoriteGroupedViews(views: views), @@ -197,3 +197,72 @@ class _FavoriteGroups extends StatelessWidget { ]; } } + +class _FavoriteSearchField extends StatefulWidget { + const _FavoriteSearchField({ + required this.width, + required this.onSearch, + }); + + final double width; + final void Function(BuildContext context, String text) onSearch; + + @override + State<_FavoriteSearchField> createState() => _FavoriteSearchFieldState(); +} + +class _FavoriteSearchFieldState extends State<_FavoriteSearchField> { + final focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + focusNode.requestFocus(); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + height: 30, + width: widget.width, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1.20, + strokeAlign: BorderSide.strokeAlignOutside, + color: Color(0xFF00BCF0), + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: CupertinoSearchTextField( + onChanged: (text) => widget.onSearch(context, text), + padding: EdgeInsets.zero, + focusNode: focusNode, + placeholder: LocaleKeys.search_label.tr(), + prefixIcon: const FlowySvg(FlowySvgs.m_search_m), + prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), + suffixIcon: const Icon(Icons.close), + suffixInsets: const EdgeInsets.only(right: 8.0), + itemSize: 16.0, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w400, + ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart index 443e8a9840..eb5d271836 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart @@ -73,19 +73,16 @@ class FavoriteMenuBloc extends Bloc { (List, List, List, List) _getViews( RepeatedFavoriteViewPB source, ) { - final now = DateTime.now(); - final List views = source.items.map((v) => v.item).toList(); final List todayViews = []; final List thisWeekViews = []; final List otherViews = []; - for (final favoriteView in source.items) { final view = favoriteView.item; final date = DateTime.fromMillisecondsSinceEpoch( favoriteView.timestamp.toInt() * 1000, ); - final diff = now.difference(date).inDays; + final diff = DateTime.now().difference(date).inDays; if (diff == 0) { todayViews.add(view); } else if (diff < 7) { @@ -94,7 +91,6 @@ class FavoriteMenuBloc extends Bloc { otherViews.add(view); } } - return (views, todayViews, thisWeekViews, otherViews); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart index 09b8a44842..2fc1b311ad 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart @@ -1,11 +1,11 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; @@ -13,9 +13,7 @@ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FavoriteMoreActions extends StatelessWidget { @@ -27,10 +25,9 @@ class FavoriteMoreActions extends StatelessWidget { Widget build(BuildContext context) { return FlowyTooltip( message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), - child: ViewMoreActionPopover( + child: ViewMoreActionButton( view: view, spaceType: FolderSpaceType.favorite, - isExpanded: false, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), onAction: (action, _) { @@ -44,7 +41,7 @@ class FavoriteMoreActions extends StatelessWidget { NavigatorTextFieldDialog( title: LocaleKeys.disclosureAction_rename.tr(), autoSelectAllText: true, - value: view.nameOrDefault, + value: view.name, maxLength: 256, onConfirm: (newValue, _) { // can not use bloc here because it has been disposed. @@ -66,13 +63,6 @@ class FavoriteMoreActions extends StatelessWidget { throw UnsupportedError('$action is not supported'); } }, - buildChild: (popover) => FlowyIconButton( - width: 24, - icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), - onPressed: () { - popover.show(); - }, - ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart index 3bd2ffe67f..2a849b7e8d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart @@ -5,6 +5,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,8 +21,8 @@ class FavoritePinAction extends StatelessWidget { : LocaleKeys.favorite_addToSidebar.tr(); final icon = FlowySvg( view.isPinned - ? FlowySvgs.favorite_section_unpin_s - : FlowySvgs.favorite_section_pin_s, + ? FlowySvgs.favorite_section_pin_s + : FlowySvgs.favorite_section_unpin_s, ); return FlowyTooltip( message: tooltip, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart index a8717e28bc..c957a664c2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -1,11 +1,14 @@ +import 'package:appflowy/generated/locale_keys.g.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/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.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/services.dart'; @@ -34,7 +37,7 @@ class SectionFolder extends StatefulWidget { } class _SectionFolderState extends State { - final isHovered = ValueNotifier(false); + final ValueNotifier isHovered = ValueNotifier(false); @override void dispose() { @@ -48,19 +51,23 @@ class _SectionFolderState extends State { onEnter: (_) => isHovered.value = true, onExit: (_) => isHovered.value = false, child: BlocProvider( - create: (_) => FolderBloc(type: widget.spaceType) - ..add(const FolderEvent.initial()), - child: BlocBuilder( - builder: (context, state) => Column( - children: [ - _buildHeader(context), - // Pages - const VSpace(4.0), - ..._buildViews(context, state, isHovered), - // Add a placeholder if there are no views - _buildDraggablePlaceholder(context), - ], + create: (context) => FolderBloc(type: widget.spaceType) + ..add( + const FolderEvent.initial(), ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + _buildHeader(context), + // Pages + const VSpace(4.0), + ..._buildViews(context, state, isHovered), + // Add a placeholder if there are no views + _buildDraggablePlaceholder(context), + ], + ); + }, ), ), ); @@ -75,17 +82,27 @@ class _SectionFolderState extends State { onPressed: () => context.read().add(const FolderEvent.expandOrUnExpand()), onAdded: () { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: '', - index: 0, - viewSection: widget.spaceType.toViewSectionPB, - ), - ); + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: widget.spaceType.toViewSectionPB, + ), + ); - context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)); + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, + ), + ); + } + }, + ); }, ); } @@ -103,14 +120,12 @@ class _SectionFolderState extends State { (view) => ViewItem( key: ValueKey('${widget.spaceType.name} ${view.id}'), spaceType: widget.spaceType, - engagedInExpanding: true, isFirstChild: view.id == widget.views.first.id, view: view, level: 0, leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, isHovered: isHovered, - enableRightClickContext: true, onSelected: (viewContext, view) { if (HardwareKeyboard.instance.isControlPressed) { context.read().openTab(view); @@ -129,15 +144,21 @@ class _SectionFolderState extends State { if (widget.views.isNotEmpty) { return const SizedBox.shrink(); } - final parentViewId = - context.read().state.currentWorkspace?.workspaceId; return ViewItem( spaceType: widget.spaceType, - view: ViewPB(parentViewId: parentViewId ?? ''), + view: ViewPB( + parentViewId: context + .read() + .state + .currentWorkspace + ?.workspaceId ?? + '', + ), level: 0, leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, onSelected: (_, __) {}, + onTertiarySelected: (_, __) {}, isHoverEnabled: widget.isHoverEnabled, isPlaceholder: true, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart index f8c3a30488..8af1c0eb9e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -1,94 +1,65 @@ -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/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.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/home/menu/sidebar/footer/sidebar_toast.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.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 'sidebar_footer_button.dart'; - class SidebarFooter extends StatelessWidget { const SidebarFooter({super.key}); @override Widget build(BuildContext context) { - return Column( + return const Row( children: [ - if (FeatureFlag.planBilling.isOn) - BillingGateGuard( - builder: (context) { - return const SidebarToast(); - }, - ), - Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Expanded(child: SidebarTemplateButton()), - _buildVerticalDivider(context), - const Expanded(child: SidebarTrashButton()), - ], - ), + Expanded(child: SidebarTrashButton()), + // Enable it when the widget button is ready + // SizedBox( + // height: 16, + // child: VerticalDivider(width: 1, color: Color(0x141F2329)), + // ), + // Expanded(child: SidebarWidgetButton()), ], ); } - - Widget _buildVerticalDivider(BuildContext context) { - return Container( - width: 1.0, - height: 14, - margin: const EdgeInsets.symmetric(horizontal: 4), - color: AFThemeExtension.of(context).borderColor, - ); - } -} - -class SidebarTemplateButton extends StatelessWidget { - const SidebarTemplateButton({super.key}); - - @override - Widget build(BuildContext context) { - return SidebarFooterButton( - leftIconSize: const Size.square(16.0), - leftIcon: const FlowySvg( - FlowySvgs.icon_template_s, - ), - text: LocaleKeys.template_label.tr(), - onTap: () => afLaunchUrlString('https://appflowy.com/templates'), - ); - } } class SidebarTrashButton extends StatelessWidget { - const SidebarTrashButton({super.key}); + const SidebarTrashButton({ + super.key, + }); @override Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, child) { - return SidebarFooterButton( - leftIconSize: const Size.square(18.0), - leftIcon: const FlowySvg( - FlowySvgs.icon_delete_s, - ), - text: LocaleKeys.trash_text.tr(), - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - ); - }, + return SizedBox( + height: HomeSizes.workspaceSectionHeight, + child: ValueListenableBuilder( + valueListenable: getIt().notifier, + builder: (context, value, child) { + return FlowyButton( + leftIcon: const FlowySvg(FlowySvgs.sidebar_footer_trash_m), + leftIconSize: const Size.square(24.0), + iconPadding: 8.0, + margin: const EdgeInsets.all(4.0), + text: FlowyText.regular( + LocaleKeys.trash_text.tr(), + lineHeight: 1.15, + ), + onTap: () { + getIt().latestOpenView = null; + getIt().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.trash), + ), + ); + }, + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart deleted file mode 100644 index cbb969d191..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -// This button style is used in -// - Trash button -// - Template button -class SidebarFooterButton extends StatelessWidget { - const SidebarFooterButton({ - super.key, - required this.leftIcon, - required this.leftIconSize, - required this.text, - required this.onTap, - }); - - final Widget leftIcon; - final Size leftIconSize; - final String text; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: HomeSizes.workspaceSectionHeight, - child: FlowyButton( - leftIcon: leftIcon, - leftIconSize: leftIconSize, - margin: const EdgeInsets.all(4.0), - expandText: false, - text: Padding( - padding: const EdgeInsets.only(right: 6.0), - child: FlowyText( - text, - fontWeight: FontWeight.w400, - figmaLineHeight: 18.0, - ), - ), - onTap: onTap, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart deleted file mode 100644 index 05e6d46957..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_toast.dart +++ /dev/null @@ -1,299 +0,0 @@ -import 'dart:io'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/billing/sidebar_plan_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.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'; - -class SidebarToast extends StatelessWidget { - const SidebarToast({super.key}); - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (_, state) { - // Show a dialog when the user hits the storage limit, After user click ok, it will navigate to the plan page. - // Even though the dislog is dissmissed, if the user triggers the storage limit again, the dialog will show again. - state.tierIndicator.maybeWhen( - storageLimitHit: () => WidgetsBinding.instance.addPostFrameCallback( - (_) => _showStorageLimitDialog(context), - ), - singleFileLimitHit: () => - WidgetsBinding.instance.addPostFrameCallback( - (_) => _showSingleFileLimitDialog(context), - ), - orElse: () {}, - ); - }, - builder: (_, state) { - return state.tierIndicator.when( - loading: () => const SizedBox.shrink(), - storageLimitHit: () => PlanIndicator( - planName: SubscriptionPlanPB.Free.label, - text: LocaleKeys.sideBar_upgradeToPro.tr(), - onTap: () => _handleOnTap(context, SubscriptionPlanPB.Pro), - reason: LocaleKeys.sideBar_storageLimitDialogTitle.tr(), - ), - aiMaxiLimitHit: () => PlanIndicator( - planName: SubscriptionPlanPB.AiMax.label, - text: LocaleKeys.sideBar_upgradeToAIMax.tr(), - onTap: () => _handleOnTap(context, SubscriptionPlanPB.AiMax), - reason: LocaleKeys.sideBar_aiResponseLimitTitle.tr(), - ), - singleFileLimitHit: () => const SizedBox.shrink(), - ); - }, - ); - } - - void _showStorageLimitDialog(BuildContext context) => showConfirmDialog( - context: context, - title: LocaleKeys.sideBar_purchaseStorageSpace.tr(), - description: LocaleKeys.sideBar_storageLimitDialogTitle.tr(), - confirmLabel: - LocaleKeys.settings_comparePlanDialog_actions_upgrade.tr(), - onConfirm: () { - WidgetsBinding.instance.addPostFrameCallback( - (_) => _handleOnTap(context, SubscriptionPlanPB.Pro), - ); - }, - ); - - void _showSingleFileLimitDialog(BuildContext context) => showConfirmDialog( - context: context, - title: LocaleKeys.sideBar_upgradeToPro.tr(), - description: - LocaleKeys.sideBar_singleFileProPlanLimitationDescription.tr(), - confirmLabel: - LocaleKeys.settings_comparePlanDialog_actions_upgrade.tr(), - onConfirm: () { - WidgetsBinding.instance.addPostFrameCallback( - (_) => _handleOnTap(context, SubscriptionPlanPB.Pro), - ); - }, - ); - - void _handleOnTap(BuildContext context, SubscriptionPlanPB plan) { - final userProfile = context.read().state.userProfile; - if (userProfile == null) { - return Log.error( - 'UserProfile is null, this should NOT happen! Please file a bug report', - ); - } - - final userWorkspaceBloc = context.read(); - final role = userWorkspaceBloc.state.currentWorkspace?.role; - if (role == null) { - return Log.error( - "Member is null. It should not happen. If you see this error, it's a bug", - ); - } - - // Only if the user is the workspace owner will we navigate to the plan page. - if (role.isOwner) { - showSettingsDialog( - context, - userProfile: userProfile, - userWorkspaceBloc: userWorkspaceBloc, - initPage: SettingsPage.plan, - ); - } else { - final String message; - if (plan == SubscriptionPlanPB.AiMax) { - message = Platform.isIOS - ? LocaleKeys.sideBar_askOwnerToUpgradeToAIMaxIOS.tr() - : LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(); - } else { - message = Platform.isIOS - ? LocaleKeys.sideBar_askOwnerToUpgradeToProIOS.tr() - : LocaleKeys.sideBar_askOwnerToUpgradeToPro.tr(); - } - - showDialog( - context: context, - barrierDismissible: false, - useRootNavigator: false, - builder: (dialogContext) => _AskOwnerToChangePlan( - message: message, - onOkPressed: () {}, - ), - ); - } - } -} - -class PlanIndicator extends StatefulWidget { - const PlanIndicator({ - super.key, - required this.planName, - required this.text, - required this.onTap, - required this.reason, - }); - - final String planName; - final String reason; - final String text; - final Function() onTap; - - @override - State createState() => _PlanIndicatorState(); -} - -class _PlanIndicatorState extends State { - final popoverController = PopoverController(); - - @override - void dispose() { - popoverController.close(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - const textGradient = LinearGradient( - begin: Alignment.bottomLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF8032FF), Color(0xFFEF35FF)], - stops: [0.1545, 0.8225], - ); - - final backgroundGradient = LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - const Color(0xFF8032FF).withValues(alpha: .1), - const Color(0xFFEF35FF).withValues(alpha: .1), - ], - ); - - return AppFlowyPopover( - controller: popoverController, - direction: PopoverDirection.rightWithBottomAligned, - offset: const Offset(10, -12), - popupBuilder: (context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText( - widget.text, - color: AFThemeExtension.of(context).strongText, - ), - const VSpace(12), - Opacity( - opacity: 0.7, - child: FlowyText.regular( - widget.reason, - maxLines: null, - lineHeight: 1.3, - textAlign: TextAlign.center, - ), - ), - const VSpace(12), - Row( - children: [ - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - popoverController.close(); - widget.onTap(); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(9), - ), - child: Center( - child: FlowyText( - LocaleKeys - .settings_comparePlanDialog_actions_upgrade - .tr(), - color: Colors.white, - fontSize: 12, - strutStyle: const StrutStyle( - forceStrutHeight: true, - ), - ), - ), - ), - ), - ), - ), - ], - ), - ], - ), - ); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - gradient: backgroundGradient, - borderRadius: BorderRadius.circular(10), - ), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.upgrade_storage_s, - blendMode: null, - ), - const HSpace(6), - ShaderMask( - shaderCallback: (bounds) => textGradient.createShader(bounds), - blendMode: BlendMode.srcIn, - child: FlowyText( - widget.text, - color: AFThemeExtension.of(context).strongText, - ), - ), - ], - ), - ), - ), - ); - } -} - -class _AskOwnerToChangePlan extends StatelessWidget { - const _AskOwnerToChangePlan({ - required this.message, - required this.onOkPressed, - }); - final String message; - final VoidCallback onOkPressed; - - @override - Widget build(BuildContext context) { - return NavigatorOkCancelDialog( - message: message, - okTitle: LocaleKeys.button_ok.tr(), - onOkPressed: onOkPressed, - titleUpperCase: false, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart deleted file mode 100644 index abe3ffd354..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.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 SidebarUpgradeApplicationButton extends StatelessWidget { - const SidebarUpgradeApplicationButton({ - super.key, - required this.onUpdateButtonTap, - required this.onCloseButtonTap, - }); - - final VoidCallback onUpdateButtonTap; - final VoidCallback onCloseButtonTap; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: context.sidebarUpgradeButtonBackground, - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // title - _buildTitle(), - const VSpace(2), - // description - _buildDescription(), - const VSpace(10), - // update button - _buildUpdateButton(), - ], - ), - ); - } - - Widget _buildTitle() { - return Row( - children: [ - const FlowySvg( - FlowySvgs.sidebar_upgrade_version_s, - blendMode: null, - ), - const HSpace(6), - FlowyText.medium( - LocaleKeys.autoUpdate_bannerUpdateTitle.tr(), - fontSize: 14, - figmaLineHeight: 18, - ), - const Spacer(), - FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.upgrade_close_s), - onTap: onCloseButtonTap, - ), - ], - ); - } - - Widget _buildDescription() { - return Opacity( - opacity: 0.7, - child: FlowyText( - LocaleKeys.autoUpdate_bannerUpdateDescription.tr(), - fontSize: 13, - figmaLineHeight: 16, - maxLines: null, - ), - ); - } - - Widget _buildUpdateButton() { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: onUpdateButtonTap, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - decoration: ShapeDecoration( - color: const Color(0xFFA44AFD), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(9), - ), - ), - child: FlowyText.medium( - LocaleKeys.autoUpdate_settingsUpdateButton.tr(), - color: Colors.white, - fontSize: 12.0, - figmaLineHeight: 15.0, - ), - ), - ), - ); - } -} - -extension on BuildContext { - Color get sidebarUpgradeButtonBackground => Theme.of(this).isLightMode - ? const Color(0xB2EBE4FF) - : const Color(0xB239275B); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 67930c336a..674febd610 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -3,15 +3,16 @@ import 'dart:io' show Platform; import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy_editor/appflowy_editor.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/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; /// Sidebar top menu is the top bar of the sidebar. /// @@ -30,7 +31,7 @@ class SidebarTopMenu extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, _) => SizedBox( - height: !UniversalPlatform.isWindows ? HomeSizes.topBarHeight : 45, + height: !PlatformExtension.isWindows ? HomeSizes.topBarHeight : 45, child: MoveWindowDetector( child: Row( children: [ @@ -50,8 +51,8 @@ class SidebarTopMenu extends StatelessWidget { } final svgData = Theme.of(context).brightness == Brightness.dark - ? FlowySvgs.app_logo_with_text_dark_xl - : FlowySvgs.app_logo_with_text_light_xl; + ? FlowySvgs.flowy_logo_dark_mode_xl + : FlowySvgs.flowy_logo_text_xl; return Padding( padding: const EdgeInsets.only(top: 12.0, left: 8), @@ -64,17 +65,20 @@ class SidebarTopMenu extends StatelessWidget { } Widget _buildCollapseMenuButton(BuildContext context) { + final color = Theme.of(context).isLightMode ? Colors.white : Colors.black; final textSpan = TextSpan( children: [ TextSpan( text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', - style: context.tooltipTextStyle(), + style: + Theme.of(context).tooltipTheme.textStyle!.copyWith(color: color), ), TextSpan( text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), + style: Theme.of(context) + .tooltipTheme + .textStyle! + .copyWith(color: Theme.of(context).hintColor), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart index 716002e917..32a86dbb86 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_panel.dart @@ -1,19 +1,19 @@ import 'dart:convert'; import 'dart:io'; - -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/share/import_service.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart'; + import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/container.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/services.dart'; import 'package:path/path.dart' as p; @@ -67,12 +67,10 @@ class ImportPanel extends StatefulWidget { class _ImportPanelState extends State { final flowyContainerFocusNode = FocusNode(); - final ValueNotifier showLoading = ValueNotifier(false); @override void dispose() { flowyContainerFocusNode.dispose(); - showLoading.dispose(); super.dispose(); } @@ -89,52 +87,37 @@ class _ImportPanelState extends State { FlowyOverlay.pop(context); } }, - child: Stack( - children: [ - FlowyContainer( - Theme.of(context).colorScheme.surface, - height: height, - width: width, - child: GridView.count( - childAspectRatio: 1 / .2, - crossAxisCount: 2, - children: ImportType.values - .where((element) => element.enableOnRelease) - .map( - (e) => Card( - child: FlowyButton( - leftIcon: e.icon(context), - leftIconSize: const Size.square(20), - text: FlowyText.medium( - e.toString(), - fontSize: 15, - overflow: TextOverflow.ellipsis, - color: Theme.of(context).colorScheme.tertiary, - ), - onTap: () async { - await _importFile(widget.parentViewId, e); - if (context.mounted) { - FlowyOverlay.pop(context); - } - }, - ), + child: FlowyContainer( + Theme.of(context).colorScheme.surface, + height: height, + width: width, + child: GridView.count( + childAspectRatio: 1 / .2, + crossAxisCount: 2, + children: ImportType.values + .where((element) => element.enableOnRelease) + .map( + (e) => Card( + child: FlowyButton( + leftIcon: e.icon(context), + leftIconSize: const Size.square(20), + text: FlowyText.medium( + e.toString(), + fontSize: 15, + overflow: TextOverflow.ellipsis, + color: Theme.of(context).colorScheme.tertiary, ), - ) - .toList(), - ), - ), - ValueListenableBuilder( - valueListenable: showLoading, - builder: (context, showLoading, child) { - if (!showLoading) { - return const SizedBox.shrink(); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, - ), - ], + onTap: () async { + await _importFile(widget.parentViewId, e); + if (context.mounted) { + FlowyOverlay.pop(context); + } + }, + ), + ), + ) + .toList(), + ), ), ); } @@ -149,84 +132,68 @@ class _ImportPanelState extends State { return; } - showLoading.value = true; - - final importValues = []; for (final file in result.files) { final path = file.path; if (path == null) { continue; } + final data = await File(path).readAsString(); final name = p.basenameWithoutExtension(path); switch (importType) { - case ImportType.historyDatabase: - final data = await File(path).readAsString(); - importValues.add( - ImportItemPayloadPB.create() - ..name = name - ..data = utf8.encode(data) - ..viewLayout = ViewLayoutPB.Grid - ..importType = ImportTypePB.HistoryDatabase, - ); - break; - case ImportType.historyDocument: case ImportType.markdownOrText: - final data = await File(path).readAsString(); + case ImportType.historyDocument: final bytes = _documentDataFrom(importType, data); if (bytes != null) { - importValues.add( - ImportItemPayloadPB.create() - ..name = name - ..data = bytes - ..viewLayout = ViewLayoutPB.Document - ..importType = ImportTypePB.Markdown, + await ImportBackendService.importData( + bytes, + name, + parentViewId, + ImportTypePB.HistoryDocument, ); } break; - case ImportType.csv: - final data = await File(path).readAsString(); - importValues.add( - ImportItemPayloadPB.create() - ..name = name - ..data = utf8.encode(data) - ..viewLayout = ViewLayoutPB.Grid - ..importType = ImportTypePB.CSV, + case ImportType.historyDatabase: + await ImportBackendService.importData( + utf8.encode(data), + name, + parentViewId, + ImportTypePB.HistoryDatabase, ); break; - case ImportType.afDatabase: - final data = await File(path).readAsString(); - importValues.add( - ImportItemPayloadPB.create() - ..name = name - ..data = utf8.encode(data) - ..viewLayout = ViewLayoutPB.Grid - ..importType = ImportTypePB.AFDatabase, + case ImportType.databaseRawData: + await ImportBackendService.importData( + utf8.encode(data), + name, + parentViewId, + ImportTypePB.RawDatabase, ); break; + case ImportType.databaseCSV: + await ImportBackendService.importData( + utf8.encode(data), + name, + parentViewId, + ImportTypePB.CSV, + ); + break; + default: + assert(false, 'Unsupported Type $importType'); } } - if (importValues.isNotEmpty) { - await ImportBackendService.importPages( - parentViewId, - importValues, - ); - } - - showLoading.value = false; widget.importCallback(importType, '', null); } } Uint8List? _documentDataFrom(ImportType importType, String data) { switch (importType) { + case ImportType.markdownOrText: + final document = markdownToDocument(data); + return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); case ImportType.historyDocument: final document = EditorMigration.migrateDocument(data); return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); - case ImportType.markdownOrText: - final document = customMarkdownToDocument(data); - return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer(); default: assert(false, 'Unsupported Type $importType'); return null; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.dart index 5c7c297327..3728cbee7b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/import/import_type.dart @@ -8,8 +8,8 @@ enum ImportType { historyDocument, historyDatabase, markdownOrText, - csv, - afDatabase; + databaseCSV, + databaseRawData; @override String toString() { @@ -20,9 +20,9 @@ enum ImportType { return LocaleKeys.importPanel_databaseFromV010.tr(); case ImportType.markdownOrText: return LocaleKeys.importPanel_textAndMarkdown.tr(); - case ImportType.csv: + case ImportType.databaseCSV: return LocaleKeys.importPanel_csv.tr(); - case ImportType.afDatabase: + case ImportType.databaseRawData: return LocaleKeys.importPanel_database.tr(); } } @@ -33,8 +33,8 @@ enum ImportType { case ImportType.historyDatabase: svg = FlowySvgs.document_s; case ImportType.historyDocument: - case ImportType.csv: - case ImportType.afDatabase: + case ImportType.databaseCSV: + case ImportType.databaseRawData: svg = FlowySvgs.board_s; case ImportType.markdownOrText: svg = FlowySvgs.text_s; @@ -48,9 +48,7 @@ enum ImportType { bool get enableOnRelease { switch (this) { - case ImportType.historyDatabase: - case ImportType.historyDocument: - case ImportType.afDatabase: + case ImportType.databaseRawData: return kDebugMode; default: return true; @@ -62,11 +60,11 @@ enum ImportType { case ImportType.historyDocument: return ['afdoc']; case ImportType.historyDatabase: - case ImportType.afDatabase: + case ImportType.databaseRawData: return ['afdb']; case ImportType.markdownOrText: return ['md', 'txt']; - case ImportType.csv: + case ImportType.databaseCSV: return ['csv']; } } @@ -74,9 +72,9 @@ enum ImportType { bool get allowMultiSelect { switch (this) { case ImportType.historyDocument: + case ImportType.databaseCSV: + case ImportType.databaseRawData: case ImportType.historyDatabase: - case ImportType.csv: - case ImportType.afDatabase: case ImportType.markdownOrText: return true; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart deleted file mode 100644 index 631e20f14a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/move_to/move_page_menu.dart +++ /dev/null @@ -1,172 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.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/sidebar/space/space_search_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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'; - -typedef MovePageMenuOnSelected = void Function(ViewPB space, ViewPB view); - -class MovePageMenu extends StatefulWidget { - const MovePageMenu({ - super.key, - required this.sourceView, - required this.onSelected, - }); - - final ViewPB sourceView; - final MovePageMenuOnSelected onSelected; - - @override - State createState() => _MovePageMenuState(); -} - -class _MovePageMenuState extends State { - final isExpandedNotifier = PropertyValueNotifier(true); - final isHoveredNotifier = ValueNotifier(true); - - @override - void dispose() { - isExpandedNotifier.dispose(); - isHoveredNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SpaceSearchBloc()..add(const SpaceSearchEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - final space = state.currentSpace; - if (space == null) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - SpaceSearchField( - width: 240, - onSearch: (context, value) => context - .read() - .add(SpaceSearchEvent.search(value)), - ), - const VSpace(10), - BlocBuilder( - builder: (context, state) { - if (state.queryResults == null) { - return Expanded(child: _buildSpace(space)); - } - return Expanded( - child: _buildGroupedViews(space, state.queryResults!), - ); - }, - ), - ], - ); - }, - ), - ); - } - - Widget _buildGroupedViews(ViewPB space, List views) { - final groupedViews = views - .where((v) => !_shouldIgnoreView(v, widget.sourceView) && !v.isSpace) - .toList(); - return _MovePageGroupedViews( - views: groupedViews, - onSelected: (view) => widget.onSelected(space, view), - ); - } - - Column _buildSpace(ViewPB space) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpacePopup( - useIntrinsicWidth: false, - expand: true, - height: 30, - showCreateButton: false, - child: FlowyTooltip( - message: LocaleKeys.space_switchSpace.tr(), - child: CurrentSpace( - // move the page to current space - onTapBlankArea: () => widget.onSelected(space, space), - space: space, - ), - ), - ), - Expanded( - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: SpacePages( - key: ValueKey(space.id), - space: space, - isHovered: isHoveredNotifier, - isExpandedNotifier: isExpandedNotifier, - shouldIgnoreView: (view) { - if (_shouldIgnoreView(view, widget.sourceView)) { - return IgnoreViewType.hide; - } - if (view.layout != ViewLayoutPB.Document) { - return IgnoreViewType.disable; - } - return IgnoreViewType.none; - }, - // hide the hover status and disable the editing actions - disableSelectedStatus: true, - // hide the ... and + buttons - rightIconsBuilder: (context, view) => [], - onSelected: (_, view) => widget.onSelected(space, view), - ), - ), - ), - ], - ); - } -} - -class _MovePageGroupedViews extends StatelessWidget { - const _MovePageGroupedViews({required this.views, required this.onSelected}); - - final List views; - final void Function(ViewPB view) onSelected; - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: views - .map( - (view) => ViewItem( - key: ValueKey(view.id), - view: view, - spaceType: FolderSpaceType.unknown, - level: 0, - onSelected: (_, view) => onSelected(view), - isFeedback: false, - isDraggable: false, - shouldRenderChildren: false, - leftIconBuilder: (_, __) => const HSpace(0.0), - rightIconsBuilder: (_, view) => [], - ), - ) - .toList(), - ), - ); - } -} - -bool _shouldIgnoreView(ViewPB view, ViewPB sourceView) { - return view.id == sourceView.id; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart new file mode 100644 index 0000000000..bf18df1a98 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart @@ -0,0 +1,35 @@ +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/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +/// Creates a new view and shows the rename dialog if needed. +/// +/// If the user has enabled the setting to show the rename dialog when creating a new view, +/// this function will show the rename dialog. +/// +/// Otherwise, it will just create the view with default name. +Future createViewAndShowRenameDialogIfNeeded( + BuildContext context, + String dialogTitle, + void Function(String viewName, BuildContext context) createView, +) async { + final value = await getIt().getWithFormat( + KVKeys.showRenameDialogWhenCreatingNewFile, + (value) => bool.parse(value), + ); + final showRenameDialog = value ?? false; + if (context.mounted && showRenameDialog) { + await NavigatorTextFieldDialog( + title: dialogTitle, + value: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + autoSelectAllText: true, + onConfirm: createView, + ).show(context); + } else if (context.mounted) { + createView(LocaleKeys.menuAppHeader_defaultNewPageName.tr(), context); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart index d35c4cd148..1417930b1e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart @@ -4,42 +4,25 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; -import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.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 SidebarNewPageButton extends StatefulWidget { +class SidebarNewPageButton extends StatelessWidget { const SidebarNewPageButton({ super.key, }); - @override - State createState() => _SidebarNewPageButtonState(); -} - -class _SidebarNewPageButtonState extends State { - @override - void initState() { - super.initState(); - createNewPageNotifier.addListener(_createNewPage); - } - - @override - void dispose() { - createNewPageNotifier.removeListener(_createNewPage); - super.dispose(); - } - @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8), height: HomeSizes.newPageSectionHeight, child: FlowyButton( - onTap: () async => _createNewPage(), + onTap: () async => _createNewPage(context), leftIcon: const FlowySvg( FlowySvgs.new_app_m, blendMode: null, @@ -55,29 +38,36 @@ class _SidebarNewPageButtonState extends State { ); } - Future _createNewPage() async { - // if the workspace is collaborative, create the view in the private section by default. - final section = context.read().state.isCollabWorkspaceOn - ? ViewSectionPB.Private - : ViewSectionPB.Public; - final spaceState = context.read().state; - if (spaceState.spaces.isNotEmpty) { - context.read().add( - const SpaceEvent.createPage( - name: '', - index: 0, - layout: ViewLayoutPB.Document, - openAfterCreate: true, - ), - ); - } else { - context.read().add( - SidebarSectionsEvent.createRootViewInSection( - name: '', - viewSection: section, - index: 0, - ), - ); - } + Future _createNewPage(BuildContext context) async { + return createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + // if the workspace is collaborative, create the view in the private section by default. + final section = + context.read().state.isCollabWorkspaceOn + ? ViewSectionPB.Private + : ViewSectionPB.Public; + final spaceState = context.read().state; + if (spaceState.spaces.isNotEmpty) { + context.read().add( + SpaceEvent.createPage( + name: viewName, + index: 0, + ), + ); + } else { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + viewSection: section, + index: 0, + ), + ); + } + } + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 0bd5dafe91..945976ef79 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -1,9 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/af_focus_manager.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; @@ -11,12 +11,12 @@ import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:universal_platform/universal_platform.dart'; final GlobalKey _settingsDialogKey = GlobalKey(); @@ -29,12 +29,12 @@ HotKeyItem openSettingsHotKey( KeyCode.comma, scope: HotKeyScope.inapp, modifiers: [ - UniversalPlatform.isMacOS ? KeyModifier.meta : KeyModifier.control, + PlatformExtension.isMacOS ? KeyModifier.meta : KeyModifier.control, ], ), keyDownHandler: (_) { if (_settingsDialogKey.currentContext == null) { - showSettingsDialog(context, userProfile: userProfile); + showSettingsDialog(context, userProfile); } else { Navigator.of(context, rootNavigator: true) .popUntil((route) => route.isFirst); @@ -43,14 +43,9 @@ HotKeyItem openSettingsHotKey( ); class UserSettingButton extends StatefulWidget { - const UserSettingButton({ - super.key, - required this.userProfile, - this.isHover = false, - }); + const UserSettingButton({required this.userProfile, super.key}); final UserProfilePB userProfile; - final bool isHover; @override State createState() => _UserSettingButtonState(); @@ -58,55 +53,35 @@ class UserSettingButton extends StatefulWidget { class _UserSettingButtonState extends State { late UserWorkspaceBloc _userWorkspaceBloc; - late PasswordBloc _passwordBloc; @override void initState() { super.initState(); - _userWorkspaceBloc = context.read(); - _passwordBloc = PasswordBloc(widget.userProfile) - ..add(PasswordEvent.init()) - ..add(PasswordEvent.checkHasPassword()); } @override void didChangeDependencies() { _userWorkspaceBloc = context.read(); - super.didChangeDependencies(); } - @override - void dispose() { - _passwordBloc.close(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return SizedBox.square( dimension: 24.0, child: FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), - child: BlocProvider.value( - value: _passwordBloc, - child: FlowyButton( - onTap: () => showSettingsDialog( - context, - userProfile: widget.userProfile, - userWorkspaceBloc: _userWorkspaceBloc, - passwordBloc: _passwordBloc, - ), - margin: EdgeInsets.zero, - text: FlowySvg( - FlowySvgs.settings_s, - color: widget.isHover - ? Theme.of(context).colorScheme.onSurface - : null, - opacity: 0.7, - ), + child: FlowyButton( + onTap: () => showSettingsDialog( + context, + widget.userProfile, + _userWorkspaceBloc, + ), + margin: EdgeInsets.zero, + text: const FlowySvg( + FlowySvgs.settings_s, + opacity: 0.7, ), ), ), @@ -115,37 +90,23 @@ class _UserSettingButtonState extends State { } void showSettingsDialog( - BuildContext context, { - required UserProfilePB userProfile, - UserWorkspaceBloc? userWorkspaceBloc, - PasswordBloc? passwordBloc, - SettingsPage? initPage, -}) { - AFFocusManager.maybeOf(context)?.notifyLoseFocus(); + BuildContext context, + UserProfilePB userProfile, [ + UserWorkspaceBloc? bloc, +]) { + AFFocusManager.of(context).notifyLoseFocus(); showDialog( context: context, builder: (dialogContext) => MultiBlocProvider( key: _settingsDialogKey, providers: [ - passwordBloc != null - ? BlocProvider.value( - value: passwordBloc, - ) - : BlocProvider( - create: (context) => PasswordBloc(userProfile) - ..add(PasswordEvent.init()) - ..add(PasswordEvent.checkHasPassword()), - ), BlocProvider.value( value: BlocProvider.of(dialogContext), ), - BlocProvider.value( - value: userWorkspaceBloc ?? context.read(), - ), + BlocProvider.value(value: bloc ?? context.read()), ], child: SettingsDialog( userProfile, - initPage: initPage, didLogout: () async { // Pop the dialog using the dialog context Navigator.of(dialogContext).pop(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 9c19184217..3f5f0057a3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,15 +1,10 @@ import 'dart:async'; -import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/blank/blank.dart'; -import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/shared/feature_flags.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/shared/version_checker/version_checker.dart'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; @@ -17,7 +12,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/favorite/prelude.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/billing/sidebar_plan_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; @@ -25,7 +19,6 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_upgarde_application_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; @@ -38,12 +31,12 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; 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:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -Loading? _duplicateSpaceLoading; - /// Home Sidebar is the left side bar of the home page. /// /// in the sidebar, we have: @@ -60,7 +53,7 @@ class HomeSideBar extends StatelessWidget { final UserProfilePB userProfile; - final WorkspaceLatestPB workspaceSetting; + final WorkspaceSettingPB workspaceSetting; @override Widget build(BuildContext context) { @@ -79,140 +72,128 @@ class HomeSideBar extends StatelessWidget { // +-- Public Or Private Section: control the sections of the workspace // | // +-- Trash Section - return BlocProvider( - create: (context) => SidebarPlanBloc() - ..add(SidebarPlanEvent.init(workspaceSetting.workspaceId, userProfile)), - child: BlocConsumer( - listenWhen: (prev, curr) => - prev.currentWorkspace?.workspaceId != - curr.currentWorkspace?.workspaceId, - listener: (context, state) { - if (FeatureFlag.search.isOn) { - // Notify command palette that workspace has changed - context.read().add( - CommandPaletteEvent.workspaceChanged( - workspaceId: state.currentWorkspace?.workspaceId, - ), - ); - } - - if (state.currentWorkspace != null) { - context.read().add( - SidebarPlanEvent.changedWorkspace( - workspaceId: state.currentWorkspace!.workspaceId, - ), - ); - } - - // Re-initialize workspace-specific services - getIt().reset(); - }, - // Rebuild the whole sidebar when the current workspace changes - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - builder: (context, state) { - if (state.currentWorkspace == null) { - return const SizedBox.shrink(); - } - - final workspaceId = state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId; - return MultiBlocProvider( - providers: [ - BlocProvider.value(value: getIt()), - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add(SidebarSectionsEvent.initial(userProfile, workspaceId)), - ), - BlocProvider( - create: (_) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - ), - ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => context.read().add( - TabsEvent.openPlugin( - plugin: state.lastCreatedRootView!.plugin(), - ), - ), + return BlocConsumer( + listenWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + listener: (context, state) { + if (FeatureFlag.search.isOn) { + // Notify command palette that workspace has changed + context.read().add( + CommandPaletteEvent.workspaceChanged( + workspaceId: state.currentWorkspace?.workspaceId, ), - BlocListener( - listenWhen: (prev, curr) => - prev.lastCreatedPage?.id != curr.lastCreatedPage?.id || - prev.isDuplicatingSpace != curr.isDuplicatingSpace, - listener: (context, state) { - final page = state.lastCreatedPage; - if (page == null || page.id.isEmpty) { - // open the blank page - context - .read() - .add(TabsEvent.openPlugin(plugin: BlankPagePlugin())); + ); + } + + // Re-initialize workspace-specific services + getIt().reset(); + }, + // Rebuild the whole sidebar when the current workspace changes + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: getIt()), + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ), + ), + ), + BlocProvider( + create: (_) => SpaceBloc() + ..add( + SpaceEvent.initial( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + openFirstPage: false, + ), + ), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => context.read().add( + TabsEvent.openPlugin( + plugin: state.lastCreatedRootView!.plugin(), + ), + ), + ), + BlocListener( + listenWhen: (p, c) => + p.lastCreatedPage?.id != c.lastCreatedPage?.id, + listener: (context, state) { + final page = state.lastCreatedPage; + if (page == null || page.id.isEmpty) { + // open the blank page + context.read().add( + TabsEvent.openPlugin( + plugin: BlankPagePlugin(), + ), + ); + } else { + context.read().add( + TabsEvent.openPlugin( + plugin: state.lastCreatedPage!.plugin(), + ), + ); + } + }, + ), + BlocListener( + listenWhen: (_, curr) => curr.action != null, + listener: _onNotificationAction, + ), + BlocListener( + listener: (context, state) { + final actionType = state.actionResult?.actionType; + + if (actionType == UserWorkspaceActionType.create || + actionType == UserWorkspaceActionType.delete || + actionType == UserWorkspaceActionType.open) { + if (context.read().state.spaces.isEmpty) { + context.read().add( + SidebarSectionsEvent.reload( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ), + ); } else { - context.read().add( - TabsEvent.openPlugin( - plugin: state.lastCreatedPage!.plugin(), + context.read().add( + SpaceEvent.reset( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, ), ); } - if (state.isDuplicatingSpace) { - _duplicateSpaceLoading ??= Loading(context); - _duplicateSpaceLoading?.start(); - } else if (_duplicateSpaceLoading != null) { - _duplicateSpaceLoading?.stop(); - _duplicateSpaceLoading = null; - } - }, - ), - BlocListener( - listenWhen: (_, curr) => curr.action != null, - listener: _onNotificationAction, - ), - BlocListener( - listener: (context, state) { - final actionType = state.actionResult?.actionType; - - if (actionType == UserWorkspaceActionType.create || - actionType == UserWorkspaceActionType.delete || - actionType == UserWorkspaceActionType.open) { - if (context.read().state.spaces.isEmpty) { - context.read().add( - SidebarSectionsEvent.reload( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - ), - ); - } else { - context.read().add( - SpaceEvent.reset( - userProfile, - state.currentWorkspace?.workspaceId ?? - workspaceSetting.workspaceId, - true, - ), - ); - } - - context - .read() - .add(const FavoriteEvent.fetchFavorites()); - } - }, - ), - ], - child: _Sidebar(userProfile: userProfile), - ), - ); - }, - ), + context + .read() + .add(const FavoriteEvent.fetchFavorites()); + } + }, + ), + ], + child: _Sidebar(userProfile: userProfile), + ), + ); + }, ); } @@ -232,11 +213,6 @@ class HomeSideBar extends StatelessWidget { ); } - final blockId = action.arguments?[ActionArgumentKeys.blockId]; - if (blockId != null) { - arguments[PluginArgumentKeys.blockId] = blockId; - } - final rowId = action.arguments?[ActionArgumentKeys.rowId]; if (rowId != null) { arguments[PluginArgumentKeys.rowId] = rowId; @@ -264,9 +240,6 @@ class _SidebarState extends State<_Sidebar> { final _isHovered = ValueNotifier(false); final _scrollOffset = ValueNotifier(0); - // mute the update button during the current application lifecycle. - final _muteUpdateButton = ValueNotifier(false); - @override void initState() { super.initState(); @@ -286,6 +259,7 @@ class _SidebarState extends State<_Sidebar> { @override Widget build(BuildContext context) { const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 8); + final userState = context.read().state; return MouseRegion( onEnter: (_) => _isHovered.value = true, onExit: (_) => _isHovered.value = false, @@ -307,15 +281,15 @@ class _SidebarState extends State<_Sidebar> { ), ), // user or workspace, setting - BlocBuilder( - builder: (context, state) => Container( - height: HomeSizes.workspaceSectionHeight, - padding: menuHorizontalInset - const EdgeInsets.only(right: 6), - // if the workspaces are empty, show the user profile instead - child: state.isCollabWorkspaceOn && state.workspaces.isNotEmpty - ? SidebarWorkspace(userProfile: widget.userProfile) - : SidebarUser(userProfile: widget.userProfile), - ), + Container( + height: HomeSizes.workspaceSectionHeight, + padding: menuHorizontalInset - const EdgeInsets.only(right: 6), + child: + // if the workspaces are empty, show the user profile instead + userState.isCollabWorkspaceOn && + userState.workspaces.isNotEmpty + ? SidebarWorkspace(userProfile: widget.userProfile) + : SidebarUser(userProfile: widget.userProfile), ), if (FeatureFlag.search.isOn) ...[ const VSpace(6), @@ -334,11 +308,16 @@ class _SidebarState extends State<_Sidebar> { padding: const EdgeInsets.symmetric(horizontal: 12.0), child: ValueListenableBuilder( valueListenable: _scrollOffset, - builder: (_, offset, child) => Opacity( - opacity: offset > 0 ? 1 : 0, - child: child, + builder: (_, offset, child) { + return Opacity( + opacity: offset > 0 ? 1 : 0, + child: child, + ); + }, + child: const Divider( + color: Color(0x141F2329), + height: 0.5, ), - child: const FlowyDivider(), ), ), @@ -348,13 +327,10 @@ class _SidebarState extends State<_Sidebar> { Padding( padding: menuHorizontalInset + const EdgeInsets.symmetric(horizontal: 4.0), - child: const FlowyDivider(), + child: const Divider(height: 0.5, color: Color(0x141F2329)), ), const VSpace(8), - _renderUpgradeSpaceButton(menuHorizontalInset), - _buildUpgradeApplicationButton(menuHorizontalInset), - const VSpace(8), Padding( padding: menuHorizontalInset + @@ -371,24 +347,10 @@ class _SidebarState extends State<_Sidebar> { Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) { final spaceState = context.read().state; final workspaceState = context.read().state; - - if (!spaceState.isInitialized) { - return const SizedBox.shrink(); - } - // there's no space or the workspace is not collaborative, // show the folder section (Workspace, Private, Personal) // otherwise, show the space - final sidebarSectionBloc = context.watch(); - final containsSpace = sidebarSectionBloc.state.containsSpace; - - if (containsSpace && spaceState.spaces.isEmpty) { - context.read().add(const SpaceEvent.didReceiveSpaceUpdate()); - } - - return !containsSpace || - spaceState.spaces.isEmpty || - !workspaceState.isCollabWorkspaceOn + return spaceState.spaces.isEmpty || !workspaceState.isCollabWorkspaceOn ? Expanded( child: Padding( padding: menuHorizontalInset - const EdgeInsets.only(right: 6), @@ -406,16 +368,13 @@ class _SidebarState extends State<_Sidebar> { : Expanded( child: Padding( padding: menuHorizontalInset - const EdgeInsets.only(right: 6), - child: FlowyScrollbar( + child: SingleChildScrollView( + padding: const EdgeInsets.only(right: 6), controller: _scrollController, - child: SingleChildScrollView( - padding: const EdgeInsets.only(right: 6), - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: SidebarSpace( - userProfile: widget.userProfile, - isHoverEnabled: !_isScrolling, - ), + physics: const ClampingScrollPhysics(), + child: SidebarSpace( + userProfile: widget.userProfile, + isHoverEnabled: !_isScrolling, ), ), ), @@ -439,42 +398,6 @@ class _SidebarState extends State<_Sidebar> { ); } - Widget _buildUpgradeApplicationButton(EdgeInsets menuHorizontalInset) { - return ValueListenableBuilder( - valueListenable: _muteUpdateButton, - builder: (_, mute, child) { - if (mute) { - return const SizedBox.shrink(); - } - - return ValueListenableBuilder( - valueListenable: ApplicationInfo.latestVersionNotifier, - builder: (_, latestVersion, child) { - if (!ApplicationInfo.isUpdateAvailable) { - return const SizedBox.shrink(); - } - - return Padding( - padding: menuHorizontalInset + - const EdgeInsets.only( - left: 4.0, - right: 4.0, - ), - child: SidebarUpgradeApplicationButton( - onUpdateButtonTap: () { - versionChecker.checkForUpdate(); - }, - onCloseButtonTap: () { - _muteUpdateButton.value = true; - }, - ), - ); - }, - ); - }, - ); - } - void _onScrollChanged() { setState(() => _isScrolling = true); @@ -497,32 +420,12 @@ class _SidebarSearchButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - richMessage: TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.search_sidebarSearchIcon.tr()}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: Platform.isMacOS ? '⌘+P' : 'Ctrl+P', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ), - child: FlowyButton( - onTap: () { - // exit editing mode when doing search to avoid the toolbar showing up - EditorNotification.exitEditing().post(); - CommandPalette.of(context).toggle(); - }, - leftIcon: const FlowySvg(FlowySvgs.search_s), - iconPadding: 12.0, - margin: const EdgeInsets.only(left: 8.0), - text: FlowyText.regular(LocaleKeys.search_label.tr()), - ), + return FlowyButton( + onTap: () => CommandPalette.of(context).toggle(), + leftIcon: const FlowySvg(FlowySvgs.search_s), + iconPadding: 12.0, + margin: const EdgeInsets.only(left: 8.0), + text: FlowyText.regular(LocaleKeys.search_label.tr()), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/_extension.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/_extension.dart deleted file mode 100644 index 40f20f098a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/_extension.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:appflowy/util/theme_extension.dart'; -import 'package:flutter/material.dart'; - -extension SpacePermissionColorExtension on BuildContext { - Color get enableBorderColor => Theme.of(this).isLightMode - ? const Color(0x1E171717) - : const Color(0xFF3A3F49); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart index e3ce26e835..328c969729 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart @@ -1,8 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/_extension.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -18,30 +16,27 @@ class CreateSpacePopup extends StatefulWidget { class _CreateSpacePopupState extends State { String spaceName = LocaleKeys.space_defaultSpaceName.tr(); - String? spaceIcon = kDefaultSpaceIconId; - String? spaceIconColor = builtInSpaceColors.first; + String spaceIcon = builtInSpaceIcons.first; + String spaceIconColor = builtInSpaceColors.first; SpacePermission spacePermission = SpacePermission.publicToAll; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), - width: 524, + width: 500, child: Column( mainAxisSize: MainAxisSize.min, children: [ FlowyText( LocaleKeys.space_createNewSpace.tr(), fontSize: 18.0, - figmaLineHeight: 24.0, ), - const VSpace(2.0), - FlowyText( + const VSpace(6.0), + FlowyText.regular( LocaleKeys.space_createSpaceDescription.tr(), fontSize: 14.0, - fontWeight: FontWeight.w300, color: Theme.of(context).hintColor, - figmaLineHeight: 18.0, maxLines: 2, ), const VSpace(16.0), @@ -81,12 +76,10 @@ class _CreateSpacePopupState extends State { context.read().add( SpaceEvent.create( name: spaceName, - // fixme: space issue - icon: spaceIcon!, - iconColor: spaceIconColor!, + icon: spaceIcon, + iconColor: spaceIconColor, permission: spacePermission, createNewPageByDefault: true, - openAfterCreate: true, ), ); @@ -113,16 +106,14 @@ class _SpaceNameTextField extends StatelessWidget { LocaleKeys.space_spaceName.tr(), fontSize: 14.0, color: Theme.of(context).hintColor, - figmaLineHeight: 18.0, ), const VSpace(6.0), SizedBox( height: 40, child: FlowyTextField( - hintText: LocaleKeys.space_spaceNamePlaceholder.tr(), + hintText: LocaleKeys.space_spaceName.tr(), onChanged: onChanged, onSubmitted: onSubmitted, - enableBorderColor: context.enableBorderColor, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart index eb8c54025d..b677b4056c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart @@ -77,7 +77,7 @@ class _SpaceNameTextField extends StatelessWidget { }); final void Function(String name) onNameChanged; - final void Function(String? icon, String? color) onIconChanged; + final void Function(String icon, String color) onIconChanged; @override Widget build(BuildContext context) { @@ -99,7 +99,6 @@ class _SpaceNameTextField extends StatelessWidget { SizedBox.square( dimension: 40, child: SpaceIconPopup( - space: space, cornerRadius: 12, icon: space?.spaceIcon, iconColor: space?.spaceIconColor, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart index 95130b029e..d4d7ab6772 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/shared_widget.dart @@ -1,27 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/util/theme_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/workspace/presentation/home/menu/sidebar/space/_extension.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; class SpacePermissionSwitch extends StatefulWidget { const SpacePermissionSwitch({ @@ -54,7 +39,6 @@ class _SpacePermissionSwitchState extends State { LocaleKeys.space_permission.tr(), fontSize: 14.0, color: Theme.of(context).hintColor, - figmaLineHeight: 18.0, ), const VSpace(6.0), AppFlowyPopover( @@ -63,11 +47,16 @@ class _SpacePermissionSwitchState extends State { constraints: const BoxConstraints(maxWidth: 500), offset: const Offset(0, 4), margin: EdgeInsets.zero, + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + borderRadius: 10, + ), popupBuilder: (_) => _buildPermissionButtons(), child: DecoratedBox( decoration: ShapeDecoration( shape: RoundedRectangleBorder( - side: BorderSide(color: context.enableBorderColor), + side: BorderSide(color: Theme.of(context).colorScheme.outline), borderRadius: BorderRadius.circular(10), ), ), @@ -143,13 +132,9 @@ class SpacePermissionButton extends StatelessWidget { radius: BorderRadius.circular(10), iconPadding: 16.0, leftIcon: FlowySvg(icon), - leftIconSize: const Size.square(20), rightIcon: showArrow ? const FlowySvg(FlowySvgs.space_permission_dropdown_s) : null, - borderColor: Theme.of(context).isLightMode - ? const Color(0x1E171717) - : const Color(0xFF3A3F49), text: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -174,542 +159,105 @@ class SpaceCancelOrConfirmButton extends StatelessWidget { required this.onConfirm, required this.confirmButtonName, this.confirmButtonColor, - this.confirmButtonBuilder, }); final VoidCallback onCancel; final VoidCallback onConfirm; final String confirmButtonName; final Color? confirmButtonColor; - final WidgetBuilder? confirmButtonBuilder; + @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - AFOutlinedTextButton.normal( - text: LocaleKeys.button_cancel.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, + DecoratedBox( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x1E14171B)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + text: FlowyText.regular(LocaleKeys.button_cancel.tr()), + onTap: onCancel, ), - onTap: onCancel, ), const HSpace(12.0), - if (confirmButtonBuilder != null) ...[ - confirmButtonBuilder!(context), - ] else ...[ - DecoratedBox( - decoration: ShapeDecoration( - color: - confirmButtonColor ?? Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), - radius: BorderRadius.circular(8), - text: FlowyText.regular( - confirmButtonName, - lineHeight: 1.0, - color: Theme.of(context).colorScheme.onPrimary, - ), - onTap: onConfirm, + DecoratedBox( + decoration: ShapeDecoration( + color: confirmButtonColor ?? Theme.of(context).colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), ), - ], - ], - ); - } -} - -class SpaceOkButton extends StatelessWidget { - const SpaceOkButton({ - super.key, - required this.onConfirm, - required this.confirmButtonName, - this.confirmButtonColor, - }); - - final VoidCallback onConfirm; - final String confirmButtonName; - final Color? confirmButtonColor; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - PrimaryRoundedButton( - text: confirmButtonName, - margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), - radius: 8.0, - onTap: onConfirm, + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), + radius: BorderRadius.circular(8), + text: FlowyText.regular( + confirmButtonName, + color: Colors.white, + ), + onTap: onConfirm, + ), ), ], ); } } -enum ConfirmPopupStyle { - onlyOk, - cancelAndOk, -} - -class ConfirmPopupColor { - static Color titleColor(BuildContext context) { - return AppFlowyTheme.of(context).textColorScheme.primary; - } - - static Color descriptionColor(BuildContext context) { - return AppFlowyTheme.of(context).textColorScheme.primary; - } -} - -class ConfirmPopup extends StatefulWidget { - const ConfirmPopup({ - super.key, - this.style = ConfirmPopupStyle.cancelAndOk, - required this.title, - required this.description, - required this.onConfirm, - this.onCancel, - this.confirmLabel, - this.confirmButtonColor, - this.confirmButtonBuilder, - this.child, - this.closeOnAction = true, - this.showCloseButton = true, - this.enableKeyboardListener = true, - }); - - final String title; - final String description; - final VoidCallback onConfirm; - final VoidCallback? onCancel; - final Color? confirmButtonColor; - final ConfirmPopupStyle style; - - /// The label of the confirm button. - /// - /// Defaults to 'Delete' for [ConfirmPopupStyle.cancelAndOk] style. - /// Defaults to 'Ok' for [ConfirmPopupStyle.onlyOk] style. - /// - final String? confirmLabel; - - /// Allows to add a child to the popup. - /// - /// This is useful when you want to add more content to the popup. - /// The child will be placed below the description. - /// - final Widget? child; - - /// Decides whether the popup should be closed when the confirm button is clicked. - /// Defaults to true. - /// - final bool closeOnAction; - - /// Show close button. - /// Defaults to true. - /// - final bool showCloseButton; - - /// Enable keyboard listener. - /// Defaults to true. - /// - final bool enableKeyboardListener; - - /// Allows to build a custom confirm button. - /// - final WidgetBuilder? confirmButtonBuilder; - - @override - State createState() => _ConfirmPopupState(); -} - -class _ConfirmPopupState extends State { - final focusNode = FocusNode(); +class DeleteSpacePopup extends StatelessWidget { + const DeleteSpacePopup({super.key}); @override Widget build(BuildContext context) { - return KeyboardListener( - focusNode: focusNode, - autofocus: true, - onKeyEvent: (event) { - if (widget.enableKeyboardListener) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.of(context).pop(); - } else if (event is KeyUpEvent && - event.logicalKey == LogicalKeyboardKey.enter) { - widget.onConfirm(); - if (widget.closeOnAction) { - Navigator.of(context).pop(); - } - } - } - }, - child: Container( - padding: const EdgeInsets.all(20), - color: UniversalPlatform.isDesktop - ? null - : Theme.of(context).colorScheme.surface, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(), - if (widget.description.isNotEmpty) ...[ - const VSpace(6), - _buildDescription(), - ], - if (widget.child != null) ...[ - const VSpace(12), - widget.child!, - ], - const VSpace(20), - _buildStyledButton(context), - ], - ), + final space = context.read().state.currentSpace; + final name = space != null ? space.name : ''; + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 20.0, + horizontal: 20.0, ), - ); - } - - Widget _buildTitle() { - final theme = AppFlowyTheme.of(context); - return Row( - children: [ - Expanded( - child: Text( - widget.title, - style: theme.textStyle.heading4.prominent( - color: ConfirmPopupColor.titleColor(context), - ), - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - if (widget.showCloseButton) ...[ - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), - ), - ), - ], - ], - ); - } - - Widget _buildDescription() { - if (widget.description.isEmpty) { - return const SizedBox.shrink(); - } - - final theme = AppFlowyTheme.of(context); - - return Text( - widget.description, - style: theme.textStyle.body.standard( - color: ConfirmPopupColor.descriptionColor(context), - ), - maxLines: 5, - ); - } - - Widget _buildStyledButton(BuildContext context) { - switch (widget.style) { - case ConfirmPopupStyle.onlyOk: - if (widget.confirmButtonBuilder != null) { - return widget.confirmButtonBuilder!(context); - } - - return SpaceOkButton( - onConfirm: () { - widget.onConfirm(); - if (widget.closeOnAction) { - Navigator.of(context).pop(); - } - }, - confirmButtonName: widget.confirmLabel ?? LocaleKeys.button_ok.tr(), - confirmButtonColor: widget.confirmButtonColor ?? - Theme.of(context).colorScheme.primary, - ); - case ConfirmPopupStyle.cancelAndOk: - return SpaceCancelOrConfirmButton( - onCancel: () { - widget.onCancel?.call(); - Navigator.of(context).pop(); - }, - onConfirm: () { - widget.onConfirm(); - if (widget.closeOnAction) { - Navigator.of(context).pop(); - } - }, - confirmButtonName: - widget.confirmLabel ?? LocaleKeys.space_delete.tr(), - confirmButtonColor: - widget.confirmButtonColor ?? Theme.of(context).colorScheme.error, - confirmButtonBuilder: widget.confirmButtonBuilder, - ); - } - } -} - -class SpacePopup extends StatelessWidget { - const SpacePopup({ - super.key, - this.height, - this.useIntrinsicWidth = true, - this.expand = false, - required this.showCreateButton, - required this.child, - }); - - final bool showCreateButton; - final bool useIntrinsicWidth; - final bool expand; - final double? height; - final Widget child; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: height ?? HomeSizes.workspaceSectionHeight, - child: AppFlowyPopover( - constraints: const BoxConstraints(maxWidth: 260), - direction: PopoverDirection.bottomWithLeftAligned, - clickHandler: PopoverClickHandler.gestureDetector, - offset: const Offset(0, 4), - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: SidebarSpaceMenu( - showCreateButton: showCreateButton, - ), - ), - child: FlowyButton( - useIntrinsicWidth: useIntrinsicWidth, - expand: expand, - margin: const EdgeInsets.only(left: 3.0, right: 4.0), - iconPadding: 10.0, - text: child, - ), - ), - ); - } -} - -class CurrentSpace extends StatelessWidget { - const CurrentSpace({ - super.key, - this.onTapBlankArea, - required this.space, - this.isHovered = false, - }); - - final ViewPB space; - final VoidCallback? onTapBlankArea; - final bool isHovered; - - @override - Widget build(BuildContext context) { - final child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - SpaceIcon( - dimension: 22, - space: space, - svgSize: 12, - cornerRadius: 8.0, - ), - const HSpace(10), - Flexible( - child: FlowyText.medium( - space.name, - fontSize: 14.0, - figmaLineHeight: 18.0, - overflow: TextOverflow.ellipsis, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), - ), - const HSpace(4.0), - FlowySvg( - context.read().state.isExpanded - ? FlowySvgs.workspace_drop_down_menu_show_s - : FlowySvgs.workspace_drop_down_menu_hide_s, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), - ], - ); - - if (onTapBlankArea != null) { - return Row( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 2, - child: FlowyHover( - child: Padding( - padding: const EdgeInsets.all(2.0), - child: child, + Row( + children: [ + FlowyText( + LocaleKeys.space_deleteConfirmation.tr() + name, + fontSize: 14.0, ), - ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg(FlowySvgs.upgrade_close_s), + onTap: () => Navigator.of(context).pop(), + ), + ], ), - Expanded( - child: FlowyTooltip( - message: LocaleKeys.space_movePageToSpace.tr(), - child: GestureDetector( - onTap: onTapBlankArea, - ), - ), + const VSpace(8.0), + FlowyText.regular( + LocaleKeys.space_deleteConfirmationDescription.tr(), + fontSize: 12.0, + color: Theme.of(context).hintColor, + maxLines: 3, + lineHeight: 1.4, + ), + const VSpace(20.0), + SpaceCancelOrConfirmButton( + onCancel: () => Navigator.of(context).pop(), + onConfirm: () { + context.read().add(const SpaceEvent.delete(null)); + Navigator.of(context).pop(); + }, + confirmButtonName: LocaleKeys.space_delete.tr(), + confirmButtonColor: Theme.of(context).colorScheme.error, ), ], - ); - } - - return child; - } -} - -class SpacePages extends StatelessWidget { - const SpacePages({ - super.key, - required this.space, - required this.isHovered, - required this.isExpandedNotifier, - required this.onSelected, - this.rightIconsBuilder, - this.disableSelectedStatus = false, - this.onTertiarySelected, - this.shouldIgnoreView, - }); - - final ViewPB space; - final ValueNotifier isHovered; - final PropertyValueNotifier isExpandedNotifier; - final bool disableSelectedStatus; - final ViewItemRightIconsBuilder? rightIconsBuilder; - final ViewItemOnSelected onSelected; - final ViewItemOnSelected? onTertiarySelected; - final IgnoreViewType Function(ViewPB view)? shouldIgnoreView; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ViewBloc(view: space)..add(const ViewEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - // filter the child views that should be ignored - List childViews = state.view.childViews; - if (shouldIgnoreView != null) { - childViews = childViews - .where((v) => shouldIgnoreView!(v) != IgnoreViewType.hide) - .toList(); - } - return Column( - mainAxisSize: MainAxisSize.min, - children: childViews - .map( - (view) => ViewItem( - key: ValueKey('${space.id} ${view.id}'), - spaceType: - space.spacePermission == SpacePermission.publicToAll - ? FolderSpaceType.public - : FolderSpaceType.private, - isFirstChild: view.id == childViews.first.id, - view: view, - level: 0, - leftPadding: HomeSpaceViewSizes.leftPadding, - isFeedback: false, - isHovered: isHovered, - enableRightClickContext: !disableSelectedStatus, - disableSelectedStatus: disableSelectedStatus, - isExpandedNotifier: isExpandedNotifier, - rightIconsBuilder: rightIconsBuilder, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - shouldIgnoreView: shouldIgnoreView, - ), - ) - .toList(), - ); - }, - ), - ); - } -} - -class SpaceSearchField extends StatefulWidget { - const SpaceSearchField({ - super.key, - required this.width, - required this.onSearch, - }); - - final double width; - final void Function(BuildContext context, String text) onSearch; - - @override - State createState() => _SpaceSearchFieldState(); -} - -class _SpaceSearchFieldState extends State { - final focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - focusNode.requestFocus(); - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Container( - height: 30, - width: widget.width, - clipBehavior: Clip.antiAlias, - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide( - width: 1.20, - strokeAlign: BorderSide.strokeAlignOutside, - color: Color(0xFF00BCF0), - ), - borderRadius: BorderRadius.circular(8), - ), - ), - child: CupertinoSearchTextField( - onChanged: (text) => widget.onSearch(context, text), - padding: EdgeInsets.zero, - focusNode: focusNode, - placeholder: LocaleKeys.search_label.tr(), - prefixIcon: const FlowySvg(FlowySvgs.magnifier_s), - prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), - suffixIcon: const Icon(Icons.close), - suffixInsets: const EdgeInsets.only(right: 8.0), - itemSize: 16.0, - decoration: const BoxDecoration( - color: Colors.transparent, - ), - placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).hintColor, - fontWeight: FontWeight.w400, - ), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w400, - ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart index e4be64d5b9..e5fe8d2596 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart @@ -1,17 +1,23 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_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/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.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/services.dart'; @@ -30,31 +36,34 @@ class SidebarSpace extends StatelessWidget { @override Widget build(BuildContext context) { + // const sectionPadding = 16.0; return ValueListenableBuilder( valueListenable: getIt().notifier, - builder: (_, __, ___) => Provider.value( - value: userProfile, - child: Column( - children: [ - const VSpace(4.0), - // favorite - BlocBuilder( - builder: (context, state) { - if (state.views.isEmpty) { - return const SizedBox.shrink(); - } - return FavoriteFolder( - views: state.views.map((e) => e.item).toList(), - ); - }, - ), - const VSpace(16.0), - // spaces - const _Space(), - const VSpace(200), - ], - ), - ), + builder: (context, value, child) { + return Provider.value( + value: userProfile, + child: Column( + children: [ + const VSpace(4.0), + // favorite + BlocBuilder( + builder: (context, state) { + if (state.views.isEmpty) { + return const SizedBox.shrink(); + } + return FavoriteFolder( + views: state.views.map((e) => e.item).toList(), + ); + }, + ), + const VSpace(16.0), + // spaces + const _Space(), + const VSpace(200), + ], + ), + ); + }, ); } } @@ -67,8 +76,9 @@ class _Space extends StatefulWidget { } class _SpaceState extends State<_Space> { - final isHovered = ValueNotifier(false); - final isExpandedNotifier = PropertyValueNotifier(false); + final ValueNotifier isHovered = ValueNotifier(false); + final PropertyValueNotifier isExpandedNotifier = + PropertyValueNotifier(false); @override void initState() { @@ -79,15 +89,11 @@ class _SpaceState extends State<_Space> { @override void dispose() { switchToTheNextSpace.removeListener(_switchToNextSpace); - isHovered.dispose(); - isExpandedNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final currentWorkspace = - context.watch().state.currentWorkspace; return BlocBuilder( builder: (context, state) { if (state.spaces.isEmpty) { @@ -101,11 +107,7 @@ class _SpaceState extends State<_Space> { SidebarSpaceHeader( isExpanded: state.isExpanded, space: currentSpace, - onAdded: (layout) => _showCreatePagePopup( - context, - currentSpace, - layout, - ), + onAdded: () => _showCreatePagePopup(context, currentSpace), onCreateNewSpace: () => _showCreateSpaceDialog(context), onCollapseAllPages: () => isExpandedNotifier.value = true, ), @@ -113,24 +115,11 @@ class _SpaceState extends State<_Space> { MouseRegion( onEnter: (_) => isHovered.value = true, onExit: (_) => isHovered.value = false, - child: SpacePages( - key: ValueKey( - Object.hashAll([ - currentWorkspace?.workspaceId ?? '', - currentSpace.id, - ]), - ), + child: _Pages( + key: ValueKey(currentSpace.id), isExpandedNotifier: isExpandedNotifier, space: currentSpace, isHovered: isHovered, - onSelected: (context, view) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - context.read().openPlugin(view); - }, - onTertiarySelected: (context, view) => - context.read().openTab(view), ), ), ], @@ -143,36 +132,94 @@ class _SpaceState extends State<_Space> { final spaceBloc = context.read(); showDialog( context: context, - builder: (_) => Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: BlocProvider.value( - value: spaceBloc, - child: const CreateSpacePopup(), - ), - ), + builder: (_) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: BlocProvider.value( + value: spaceBloc, + child: const CreateSpacePopup(), + ), + ); + }, ); } - void _showCreatePagePopup( - BuildContext context, - ViewPB space, - ViewLayoutPB layout, - ) { - context.read().add( - SpaceEvent.createPage( - name: '', - layout: layout, - index: 0, - openAfterCreate: true, - ), - ); + void _showCreatePagePopup(BuildContext context, ViewPB space) { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SpaceEvent.createPage( + name: viewName, + index: 0, + ), + ); - context.read().add(SpaceEvent.expand(space, true)); + context.read().add(SpaceEvent.expand(space, true)); + } + }, + ); } void _switchToNextSpace() { context.read().add(const SpaceEvent.switchToNextSpace()); } } + +class _Pages extends StatelessWidget { + const _Pages({ + super.key, + required this.space, + required this.isHovered, + required this.isExpandedNotifier, + }); + + final ViewPB space; + final ValueNotifier isHovered; + final PropertyValueNotifier isExpandedNotifier; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + ViewBloc(view: space)..add(const ViewEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: state.view.childViews + .map( + (view) => ViewItem( + key: ValueKey('${space.id} ${view.id}'), + spaceType: + space.spacePermission == SpacePermission.publicToAll + ? FolderSpaceType.public + : FolderSpaceType.private, + isFirstChild: view.id == state.view.childViews.first.id, + view: view, + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + isHovered: isHovered, + isExpandedNotifier: isExpandedNotifier, + onSelected: (viewContext, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); + }, + onTertiarySelected: (viewContext, view) => + context.read().openTab(view), + ), + ) + .toList(), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart index cf4a2aa5b1..6c277736cc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart @@ -1,22 +1,24 @@ -import 'dart:convert'; import 'dart:io'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/manage_space_popup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarSpaceHeader extends StatefulWidget { @@ -30,7 +32,7 @@ class SidebarSpaceHeader extends StatefulWidget { }); final ViewPB space; - final void Function(ViewLayoutPB layout) onAdded; + final VoidCallback onAdded; final VoidCallback onCreateNewSpace; final VoidCallback onCollapseAllPages; final bool isExpanded; @@ -54,112 +56,133 @@ class _SidebarSpaceHeaderState extends State { Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: isHovered, - builder: (context, onHover, child) { - return MouseRegion( + child: SizedBox( + height: HomeSizes.workspaceSectionHeight, + child: MouseRegion( onEnter: (_) => isHovered.value = true, onExit: (_) => isHovered.value = false, - child: GestureDetector( - onTap: () => context - .read() - .add(SpaceEvent.expand(widget.space, !widget.isExpanded)), - child: _buildSpaceName(onHover), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + left: 3, + top: 3, + bottom: 3, + child: SizedBox( + height: HomeSizes.workspaceSectionHeight, + child: AppFlowyPopover( + constraints: const BoxConstraints(maxWidth: 252), + direction: PopoverDirection.bottomWithLeftAligned, + clickHandler: PopoverClickHandler.gestureDetector, + offset: const Offset(0, 4), + popupBuilder: (_) => BlocProvider.value( + value: context.read(), + child: const SidebarSpaceMenu(), + ), + child: FlowyButton( + useIntrinsicWidth: true, + margin: const EdgeInsets.only(left: 3.0, right: 4.0), + iconPadding: 10.0, + text: _buildChild(), + ), + ), + ), + ), + Positioned( + right: 4, + child: _buildRightIcon(), + ), + ], + ), + ), + ), + builder: (context, isHovered, child) { + final style = HoverStyle( + hoverColor: isHovered + ? Theme.of(context).colorScheme.secondary + : Colors.transparent, + ); + return GestureDetector( + onTap: () => context + .read() + .add(SpaceEvent.expand(widget.space, !widget.isExpanded)), + child: FlowyHoverContainer( + style: style, + child: child!, ), ); }, ); } - Widget _buildSpaceName(bool isHovered) { - return Container( - height: HomeSizes.workspaceSectionHeight, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(6)), - color: isHovered ? Theme.of(context).colorScheme.secondary : null, - ), - child: Stack( - alignment: Alignment.center, + Widget _buildChild() { + final color = Theme.of(context).isLightMode ? Colors.white : Colors.black; + final textSpan = TextSpan( + children: [ + TextSpan( + text: '${LocaleKeys.space_quicklySwitch.tr()}\n', + style: + Theme.of(context).tooltipTheme.textStyle!.copyWith(color: color), + ), + TextSpan( + text: Platform.isMacOS ? '⌘+O' : 'Ctrl+O', + style: Theme.of(context) + .tooltipTheme + .textStyle! + .copyWith(color: Theme.of(context).hintColor), + ), + ], + ); + return FlowyTooltip( + richMessage: textSpan, + child: Row( children: [ - ValueListenableBuilder( - valueListenable: onEditing, - builder: (context, onEditing, child) => Positioned( - left: 3, - top: 3, - bottom: 3, - right: isHovered || onEditing ? 88 : 0, - child: SpacePopup( - showCreateButton: true, - child: _buildChild(isHovered), - ), + SpaceIcon( + dimension: 20, + space: widget.space, + cornerRadius: 6.0, + ), + const HSpace(10), + Flexible( + child: FlowyText.medium( + widget.space.name, + lineHeight: 1.15, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, ), ), - Positioned( - right: 4, - child: _buildRightIcon(isHovered), + const HSpace(4.0), + FlowySvg( + widget.isExpanded + ? FlowySvgs.workspace_drop_down_menu_show_s + : FlowySvgs.workspace_drop_down_menu_hide_s, ), ], ), ); } - Widget _buildChild(bool isHovered) { - final textSpan = TextSpan( - children: [ - TextSpan( - text: '${LocaleKeys.space_quicklySwitch.tr()}\n', - style: context.tooltipTextStyle(), - ), - TextSpan( - text: Platform.isMacOS ? '⌘+O' : 'Ctrl+O', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), - ), - ], - ); - return FlowyTooltip( - richMessage: textSpan, - child: CurrentSpace( - space: widget.space, - isHovered: isHovered, - ), - ); - } - - Widget _buildRightIcon(bool isHovered) { + Widget _buildRightIcon() { return ValueListenableBuilder( valueListenable: onEditing, - builder: (context, onEditing, child) => Opacity( - opacity: isHovered || onEditing ? 1 : 0, + builder: (context, onEditing, child) => ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, onHover, child) => + Opacity(opacity: onHover || onEditing ? 1 : 0, child: child), child: Row( children: [ SpaceMorePopup( space: widget.space, onEditing: (value) => this.onEditing.value = value, onAction: _onAction, - isHovered: isHovered, ), const HSpace(8.0), - FlowyTooltip( - message: LocaleKeys.sideBar_addAPage.tr(), - child: ViewAddButton( - parentViewId: widget.space.id, - onEditing: (_) {}, - onSelected: ( - pluginBuilder, - name, - initialDataBytes, - openAfterCreated, - createNewView, - ) { - if (pluginBuilder.layoutType == ViewLayoutPB.Document) { - name = ''; - } - if (createNewView) { - widget.onAdded(pluginBuilder.layoutType!); - } - }, - isHovered: isHovered, - ), + FlowyIconButton( + width: 24, + tooltipText: LocaleKeys.sideBar_addAPage.tr(), + iconPadding: const EdgeInsets.all(4.0), + icon: const FlowySvg(FlowySvgs.view_item_add_s), + onPressed: widget.onAdded, ), ], ), @@ -173,24 +196,8 @@ class _SidebarSpaceHeaderState extends State { await _showRenameDialog(); break; case SpaceMoreActionType.changeIcon: - if (data is SelectedEmojiIconResult) { - if (data.type == FlowyIconType.icon) { - try { - final iconsData = IconsData.fromJson(jsonDecode(data.emoji)); - context.read().add( - SpaceEvent.changeIcon( - icon: '${iconsData.groupName}/${iconsData.iconName}', - iconColor: iconsData.color, - ), - ); - } on FormatException catch (e) { - context - .read() - .add(const SpaceEvent.changeIcon(icon: '')); - Log.warn('SidebarSpaceHeader changeIcon error:$e'); - } - } - } + final (String icon, String iconColor) = data; + context.read().add(SpaceEvent.changeIcon(icon, iconColor)); break; case SpaceMoreActionType.manage: _showManageSpaceDialog(context); @@ -204,9 +211,6 @@ class _SidebarSpaceHeaderState extends State { case SpaceMoreActionType.delete: _showDeleteSpaceDialog(context); break; - case SpaceMoreActionType.duplicate: - context.read().add(const SpaceEvent.duplicate()); - break; case SpaceMoreActionType.divider: break; } @@ -219,12 +223,7 @@ class _SidebarSpaceHeaderState extends State { autoSelectAllText: true, hintText: LocaleKeys.space_spaceName.tr(), onConfirm: (name, _) { - context.read().add( - SpaceEvent.rename( - space: widget.space, - name: name, - ), - ); + context.read().add(SpaceEvent.rename(widget.space, name)); }, ).show(context); } @@ -249,14 +248,18 @@ class _SidebarSpaceHeaderState extends State { void _showDeleteSpaceDialog(BuildContext context) { final spaceBloc = context.read(); - final space = spaceBloc.state.currentSpace; - final name = space != null ? space.name : ''; - showConfirmDeletionDialog( + showDialog( context: context, - name: name, - description: LocaleKeys.space_deleteConfirmationDescription.tr(), - onConfirm: () { - context.read().add(const SpaceEvent.delete(null)); + builder: (_) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12.0), + ), + child: BlocProvider.value( + value: spaceBloc, + child: const SizedBox(width: 440, child: DeleteSpacePopup()), + ), + ); }, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart index f4d910700d..82027a1bdd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart @@ -6,18 +6,15 @@ import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/create_space_popup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarSpaceMenu extends StatelessWidget { - const SidebarSpaceMenu({ - super.key, - required this.showCreateButton, - }); - - final bool showCreateButton; + const SidebarSpaceMenu({super.key}); @override Widget build(BuildContext context) { @@ -30,21 +27,21 @@ class SidebarSpaceMenu extends StatelessWidget { for (final space in state.spaces) SizedBox( height: HomeSpaceViewSizes.viewHeight, - child: SidebarSpaceMenuItem( + child: _SidebarSpaceMenuItem( space: space, isSelected: state.currentSpace?.id == space.id, ), ), - if (showCreateButton) ...[ - const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: FlowyDivider(), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider( + height: 0.5, ), - const SizedBox( - height: HomeSpaceViewSizes.viewHeight, - child: _CreateSpaceButton(), - ), - ], + ), + const SizedBox( + height: HomeSpaceViewSizes.viewHeight, + child: _CreateSpaceButton(), + ), ], ); }, @@ -52,9 +49,8 @@ class SidebarSpaceMenu extends StatelessWidget { } } -class SidebarSpaceMenuItem extends StatelessWidget { - const SidebarSpaceMenuItem({ - super.key, +class _SidebarSpaceMenuItem extends StatelessWidget { + const _SidebarSpaceMenuItem({ required this.space, required this.isSelected, }); @@ -67,12 +63,7 @@ class SidebarSpaceMenuItem extends StatelessWidget { return FlowyButton( text: Row( children: [ - Flexible( - child: FlowyText.regular( - space.name, - overflow: TextOverflow.ellipsis, - ), - ), + FlowyText.regular(space.name), const HSpace(6.0), if (space.spacePermission == SpacePermission.private) FlowyTooltip( @@ -87,7 +78,6 @@ class SidebarSpaceMenuItem extends StatelessWidget { leftIcon: SpaceIcon( dimension: 20, space: space, - svgSize: 12.0, cornerRadius: 6.0, ), leftIconSize: const Size.square(20), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart index be0eadd8ed..83472b7a80 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_action_type.dart @@ -11,7 +11,6 @@ enum SpaceMoreActionType { divider, addNewSpace, manage, - duplicate, } extension ViewMoreActionTypeExtension on SpaceMoreActionType { @@ -29,31 +28,27 @@ extension ViewMoreActionTypeExtension on SpaceMoreActionType { return LocaleKeys.space_addNewSpace.tr(); case SpaceMoreActionType.manage: return LocaleKeys.space_manage.tr(); - case SpaceMoreActionType.duplicate: - return LocaleKeys.space_duplicate.tr(); case SpaceMoreActionType.divider: return ''; } } - FlowySvgData get leftIconSvg { + Widget get leftIcon { switch (this) { case SpaceMoreActionType.delete: - return FlowySvgs.trash_s; + return const FlowySvg(FlowySvgs.trash_s, blendMode: null); case SpaceMoreActionType.rename: - return FlowySvgs.view_item_rename_s; + return const FlowySvg(FlowySvgs.view_item_rename_s); case SpaceMoreActionType.changeIcon: - return FlowySvgs.change_icon_s; + return const FlowySvg(FlowySvgs.change_icon_s); case SpaceMoreActionType.collapseAllPages: - return FlowySvgs.collapse_all_page_s; + return const FlowySvg(FlowySvgs.collapse_all_page_s); case SpaceMoreActionType.addNewSpace: - return FlowySvgs.space_add_s; + return const FlowySvg(FlowySvgs.space_add_s); case SpaceMoreActionType.manage: - return FlowySvgs.space_manage_s; - case SpaceMoreActionType.duplicate: - return FlowySvgs.duplicate_s; + return const FlowySvg(FlowySvgs.space_manage_s); case SpaceMoreActionType.divider: - throw UnsupportedError('Divider does not have an icon'); + return const SizedBox.shrink(); } } @@ -66,7 +61,6 @@ extension ViewMoreActionTypeExtension on SpaceMoreActionType { case SpaceMoreActionType.delete: case SpaceMoreActionType.addNewSpace: case SpaceMoreActionType.manage: - case SpaceMoreActionType.duplicate: return const SizedBox.shrink(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart index ad9e5e8f0a..871dc0e475 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon.dart @@ -1,153 +1,26 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class SpaceIcon extends StatelessWidget { const SpaceIcon({ super.key, required this.dimension, - this.textDimension, this.cornerRadius = 0, required this.space, - this.svgSize, }); final double dimension; - final double? textDimension; final double cornerRadius; final ViewPB space; - final double? svgSize; @override Widget build(BuildContext context) { - final (icon, color) = _buildSpaceIcon(context); - - return ClipRRect( - borderRadius: BorderRadius.circular(cornerRadius), - child: Container( - width: dimension, - height: dimension, - color: color, - child: Center( - child: icon, - ), - ), - ); - } - - (Widget, Color?) _buildSpaceIcon(BuildContext context) { - final spaceIcon = space.spaceIcon; - if (spaceIcon == null || spaceIcon.isEmpty == true) { - // if space icon is null, use the first character of space name as icon - return _buildEmptySpaceIcon(context); - } else { - return _buildCustomSpaceIcon(context); - } - } - - (Widget, Color?) _buildEmptySpaceIcon(BuildContext context) { - final name = space.name.isNotEmpty ? space.name.capitalize()[0] : ''; - final icon = FlowyText.medium( - name, - color: Theme.of(context).colorScheme.surface, - fontSize: svgSize, - figmaLineHeight: textDimension ?? dimension, - ); - Color? color; - try { - final defaultColor = builtInSpaceColors.firstOrNull; - if (defaultColor != null) { - color = Color(int.parse(defaultColor)); - } - } catch (e) { - Log.error('Failed to parse default space icon color: $e'); - } - return (icon, color); - } - - (Widget, Color?) _buildCustomSpaceIcon(BuildContext context) { - final spaceIconColor = space.spaceIconColor; - - final svg = space.buildSpaceIconSvg( - context, - size: svgSize != null ? Size.square(svgSize!) : null, - ); - Widget icon; - if (svg == null) { - icon = const SizedBox.shrink(); - } else { - icon = svgSize == null || - space.spaceIcon?.contains(ViewExtKeys.spaceIconKey) == true - ? svg - : SizedBox.square(dimension: svgSize!, child: svg); - } - - Color color = Colors.transparent; - if (spaceIconColor != null && spaceIconColor.isNotEmpty) { - try { - color = Color(int.parse(spaceIconColor)); - } catch (e) { - Log.error( - 'Failed to parse space icon color: $e, value: $spaceIconColor', - ); - } - } - - return (icon, color); - } -} - -const kDefaultSpaceIconId = 'interface_essential/home-3'; - -class DefaultSpaceIcon extends StatelessWidget { - const DefaultSpaceIcon({ - super.key, - required this.dimension, - required this.iconDimension, - this.cornerRadius = 0, - }); - - final double dimension; - final double cornerRadius; - final double iconDimension; - - @override - Widget build(BuildContext context) { - final svgContent = kIconGroups?.findSvgContent( - kDefaultSpaceIconId, - ); - - final Widget svg; - if (svgContent != null) { - svg = FlowySvg.string( - svgContent, - size: Size.square(iconDimension), - color: Theme.of(context).colorScheme.surface, - ); - } else { - svg = FlowySvg( - FlowySvgData('assets/flowy_icons/16x/${builtInSpaceIcons.first}.svg'), - color: Theme.of(context).colorScheme.surface, - size: Size.square(iconDimension), - ); - } - - final color = Color(int.parse(builtInSpaceColors.first)); - return ClipRRect( - borderRadius: BorderRadius.circular(cornerRadius), - child: Container( - width: dimension, - height: dimension, - color: color, - child: Center( - child: svg, - ), + return SizedBox.square( + dimension: dimension, + child: ClipRRect( + borderRadius: BorderRadius.circular(cornerRadius), + child: space.spaceIconSvg, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart index 82410b387e..feeea05e8c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart @@ -1,17 +1,10 @@ -import 'dart:convert'; -import 'dart:math'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart' hide Icon; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; final builtInSpaceColors = [ '0xFFA34AFD', @@ -28,11 +21,6 @@ final builtInSpaceColors = [ '0xFFFF8933', ]; -String generateRandomSpaceColor() { - final random = Random(); - return builtInSpaceColors[random.nextInt(builtInSpaceColors.length)]; -} - final builtInSpaceIcons = List.generate(15, (index) => 'space_icon_${index + 1}'); @@ -42,14 +30,12 @@ class SpaceIconPopup extends StatefulWidget { this.icon, this.iconColor, this.cornerRadius = 16, - this.space, required this.onIconChanged, }); final String? icon; final String? iconColor; - final ViewPB? space; - final void Function(String? icon, String? color) onIconChanged; + final void Function(String icon, String color) onIconChanged; final double cornerRadius; @override @@ -57,12 +43,10 @@ class SpaceIconPopup extends StatefulWidget { } class _SpaceIconPopupState extends State { - late ValueNotifier selectedIcon = ValueNotifier( - widget.icon, - ); - late ValueNotifier selectedColor = ValueNotifier( - widget.iconColor ?? builtInSpaceColors.first, - ); + late ValueNotifier selectedColor = + ValueNotifier(widget.iconColor ?? builtInSpaceColors.first); + late ValueNotifier selectedIcon = + ValueNotifier(widget.icon ?? builtInSpaceIcons.first); @override void dispose() { @@ -75,34 +59,24 @@ class _SpaceIconPopupState extends State { Widget build(BuildContext context) { return AppFlowyPopover( offset: const Offset(0, 4), - constraints: BoxConstraints.loose(const Size(360, 432)), - margin: const EdgeInsets.all(0), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + borderRadius: 10, + ), + constraints: const BoxConstraints(maxWidth: 220), + margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), direction: PopoverDirection.bottomWithCenterAligned, child: _buildPreview(), - popupBuilder: (context) { - return FlowyIconEmojiPicker( - tabs: const [PickerTabType.icon], - onSelectedEmoji: (r) { - if (r.type == FlowyIconType.icon) { - try { - final iconsData = IconsData.fromJson(jsonDecode(r.emoji)); - final color = iconsData.color; - selectedIcon.value = - '${iconsData.groupName}/${iconsData.iconName}'; - if (color != null) { - selectedColor.value = color; - } - widget.onIconChanged(selectedIcon.value, selectedColor.value); - } on FormatException catch (e) { - selectedIcon.value = ''; - widget.onIconChanged(selectedIcon.value, selectedColor.value); - Log.warn('SpaceIconPopup onSelectedEmoji error:$e'); - } - } - PopoverContainer.of(context).close(); - }, - ); - }, + popupBuilder: (_) => SpaceIconPicker( + icon: selectedIcon.value, + iconColor: selectedColor.value, + onIconChanged: (icon, iconColor) { + selectedIcon.value = icon; + selectedColor.value = iconColor; + widget.onIconChanged(icon, iconColor); + }, + ), ); } @@ -119,59 +93,15 @@ class _SpaceIconPopupState extends State { builder: (_, color, __) { return ValueListenableBuilder( valueListenable: selectedIcon, - builder: (_, value, __) { - Widget child; - if (value == null) { - if (widget.space == null) { - child = DefaultSpaceIcon( - cornerRadius: widget.cornerRadius, - dimension: 32, - iconDimension: 32, - ); - } else { - child = SpaceIcon( - dimension: 32, - space: widget.space!, - svgSize: 24, - cornerRadius: widget.cornerRadius, - ); - } - } else if (value.contains('space_icon')) { - child = ClipRRect( - borderRadius: BorderRadius.circular(widget.cornerRadius), - child: Container( - color: Color(int.parse(color)), - child: Align( - child: FlowySvg( - FlowySvgData('assets/flowy_icons/16x/$value.svg'), - size: const Size.square(42), - color: Theme.of(context).colorScheme.surface, - ), - ), - ), - ); - } else { - final content = kIconGroups?.findSvgContent(value); - if (content == null) { - child = const SizedBox.shrink(); - } else { - child = ClipRRect( - borderRadius: - BorderRadius.circular(widget.cornerRadius), - child: Container( - color: Color(int.parse(color)), - child: Align( - child: FlowySvg.string( - content, - size: const Size.square(24), - color: Theme.of(context).colorScheme.surface, - ), - ), - ), - ); - } - } - + builder: (_, icon, __) { + final child = ClipRRect( + borderRadius: BorderRadius.circular(widget.cornerRadius), + child: FlowySvg( + FlowySvgData('assets/flowy_icons/16x/$icon.svg'), + color: Color(int.parse(color)), + blendMode: BlendMode.srcOut, + ), + ); if (onHover) { return Stack( children: [ @@ -230,24 +160,18 @@ class _SpaceIconPickerState extends State { widget.onIconChanged(selectedIcon.value, selectedColor.value); } - selectedColor.addListener(_onColorChanged); - selectedIcon.addListener(_onIconChanged); - } + selectedColor.addListener(() { + widget.onIconChanged(selectedIcon.value, selectedColor.value); + }); - void _onColorChanged() { - widget.onIconChanged(selectedIcon.value, selectedColor.value); - } - - void _onIconChanged() { - widget.onIconChanged(selectedIcon.value, selectedColor.value); + selectedIcon.addListener(() { + widget.onIconChanged(selectedIcon.value, selectedColor.value); + }); } @override void dispose() { - selectedColor.removeListener(_onColorChanged); selectedColor.dispose(); - - selectedIcon.removeListener(_onIconChanged); selectedIcon.dispose(); super.dispose(); } @@ -265,7 +189,9 @@ class _SpaceIconPickerState extends State { const VSpace(10.0), _Colors( selectedColor: selectedColor.value, - onColorSelected: (color) => selectedColor.value = color, + onColorSelected: (color) { + selectedColor.value = color; + }, ), const VSpace(12.0), FlowyText.regular( @@ -278,7 +204,9 @@ class _SpaceIconPickerState extends State { builder: (_, value, ___) => _Icons( selectedColor: value, selectedIcon: selectedIcon.value, - onIconSelected: (icon) => selectedIcon.value = icon, + onIconSelected: (icon) { + selectedIcon.value = icon; + }, ), ), ], @@ -311,7 +239,9 @@ class _ColorsState extends State<_Colors> { children: builtInSpaceColors.map((color) { return GestureDetector( onTap: () { - setState(() => selectedColor = color); + setState(() { + selectedColor = color; + }); widget.onColorSelected(color); }, @@ -371,7 +301,9 @@ class _IconsState extends State<_Icons> { children: builtInSpaceIcons.map((icon) { return GestureDetector( onTap: () { - setState(() => selectedIcon = icon); + setState(() { + selectedIcon = icon; + }); widget.onIconSelected(icon); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart index 10ef94ba01..905082721d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_migration.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SpaceMigration extends StatefulWidget { @@ -71,8 +70,14 @@ class _SpaceMigrationState extends State { const linearGradient = LinearGradient( begin: Alignment.bottomLeft, end: Alignment.bottomRight, - colors: [Color(0xFF8032FF), Color(0xFFEF35FF)], - stops: [0.1545, 0.8225], + colors: [ + Color(0xFF8032FF), + Color(0xFFEF35FF), + ], + stops: [ + 0.1545, + 0.8225, + ], ); return GestureDetector( behavior: HitTestBehavior.translucent, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart index 4b13062c3e..00b0bc3307 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart @@ -1,16 +1,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,13 +20,11 @@ class SpaceMorePopup extends StatelessWidget { required this.space, required this.onAction, required this.onEditing, - this.isHovered = false, }); final ViewPB space; final void Function(SpaceMoreActionType type, dynamic data) onAction; final void Function(bool value) onEditing; - final bool isHovered; @override Widget build(BuildContext context) { @@ -41,10 +39,7 @@ class SpaceMorePopup extends StatelessWidget { buildChild: (popover) { return FlowyIconButton( width: 24, - icon: FlowySvg( - FlowySvgs.workspace_three_dots_s, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), + icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), tooltipText: LocaleKeys.space_manage.tr(), onPressed: () { onEditing(true); @@ -74,7 +69,6 @@ class SpaceMorePopup extends StatelessWidget { SpaceMoreActionType.rename, SpaceMoreActionType.changeIcon, SpaceMoreActionType.manage, - SpaceMoreActionType.duplicate, SpaceMoreActionType.divider, SpaceMoreActionType.addNewSpace, SpaceMoreActionType.collapseAllPages, @@ -91,11 +85,7 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell { final void Function(PopoverController controller, dynamic data) onTap; @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { + Widget buildWithContext(BuildContext context, PopoverController controller) { if (inner == SpaceMoreActionType.divider) { return _buildDivider(); } else if (inner == SpaceMoreActionType.changeIcon) { @@ -117,17 +107,20 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell { PopoverController controller, ) { final child = _buildActionButton(context, null); + final spaceBloc = context.read(); + final color = spaceBloc.state.currentSpace?.spaceIconColor; + return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(360, 432)), - margin: const EdgeInsets.all(0), + constraints: BoxConstraints.loose(const Size(216, 256)), + margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), clickHandler: PopoverClickHandler.gestureDetector, - offset: const Offset(0, -40), - popupBuilder: (context) { - return FlowyIconEmojiPicker( - tabs: const [PickerTabType.icon], - onSelectedEmoji: (r) => onTap(controller, r), - ); - }, + popupBuilder: (_) => SpaceIconPicker( + iconColor: color, + skipFirstNotification: true, + onIconChanged: (icon, color) { + onTap(controller, (icon, color)); + }, + ), child: child, ); } @@ -135,7 +128,7 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell { Widget _buildDivider() { return const Padding( padding: EdgeInsets.all(8.0), - child: FlowyDivider(), + child: Divider(height: 1.0), ); } @@ -147,24 +140,13 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell { final spaces = spaceBloc.state.spaces; final currentSpace = spaceBloc.state.currentSpace; - final isOwner = context - .read() - ?.state - .currentWorkspace - ?.role - .isOwner ?? - false; - final isPageCreator = - currentSpace?.createdBy == context.read().id; - final allowToDelete = isOwner || isPageCreator; - bool disable = false; var message = ''; if (inner == SpaceMoreActionType.delete) { if (spaces.length <= 1) { disable = true; message = LocaleKeys.space_unableToDeleteLastSpace.tr(); - } else if (!allowToDelete) { + } else if (currentSpace?.createdBy != context.read().id) { disable = true; message = LocaleKeys.space_unableToDeleteSpaceNotCreatedByYou.tr(); } @@ -175,26 +157,22 @@ class SpaceMoreActionTypeWrapper extends CustomActionCell { padding: const EdgeInsets.symmetric(vertical: 2.0), child: Opacity( opacity: disable ? 0.3 : 1.0, - child: FlowyIconTextButton( + child: FlowyButton( disable: disable, margin: const EdgeInsets.symmetric(horizontal: 6), + leftIcon: inner.leftIcon, + rightIcon: inner.rightIcon, iconPadding: 10.0, + text: SizedBox( + // height: 16.0, + child: FlowyText.regular( + inner.name, + color: inner == SpaceMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, + ), + ), onTap: onTap, - leftIconBuilder: (onHover) => FlowySvg( - inner.leftIconSvg, - color: inner == SpaceMoreActionType.delete && onHover - ? Theme.of(context).colorScheme.error - : null, - ), - rightIconBuilder: (_) => inner.rightIcon, - textBuilder: (onHover) => FlowyText.regular( - inner.name, - fontSize: 14.0, - figmaLineHeight: 18.0, - color: inner == SpaceMoreActionType.delete && onHover - ? Theme.of(context).colorScheme.error - : null, - ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_import_notion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_import_notion.dart deleted file mode 100644 index f8b17c23c1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_import_notion.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/share/import_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class NotionImporter extends StatelessWidget { - const NotionImporter({required this.filePath, super.key}); - - final String filePath; - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints(minHeight: 30, maxHeight: 200), - child: FutureBuilder( - future: _uploadFile(), - builder: (context, snapshots) { - if (!snapshots.hasData) { - return const _Uploading(); - } - - final result = snapshots.data; - if (result == null) { - return const _UploadSuccess(); - } else { - return result.fold( - (_) => const _UploadSuccess(), - (err) => _UploadError(error: err), - ); - } - }, - ), - ); - } - - Future> _uploadFile() async { - final importResult = await ImportBackendService.importZipFiles( - [ImportZipPB()..filePath = filePath], - ); - - return importResult; - } -} - -class _UploadSuccess extends StatelessWidget { - const _UploadSuccess(); - - @override - Widget build(BuildContext context) { - return FlowyText( - fontSize: 16, - LocaleKeys.settings_common_uploadNotionSuccess.tr(), - maxLines: 10, - ); - } -} - -class _Uploading extends StatelessWidget { - const _Uploading(); - - @override - Widget build(BuildContext context) { - return IntrinsicHeight( - child: Center( - child: Column( - children: [ - const CircularProgressIndicator.adaptive(), - const VSpace(12), - FlowyText( - fontSize: 16, - LocaleKeys.settings_common_uploadingFile.tr(), - maxLines: null, - ), - ], - ), - ), - ); - } -} - -class _UploadError extends StatelessWidget { - const _UploadError({required this.error}); - - final FlowyError error; - - @override - Widget build(BuildContext context) { - return FlowyText(error.msg, maxLines: 10); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 44f558fc17..53baa2599e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -6,6 +6,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -18,23 +19,13 @@ enum WorkspaceMoreAction { divider, } -class WorkspaceMoreActionList extends StatefulWidget { +class WorkspaceMoreActionList extends StatelessWidget { const WorkspaceMoreActionList({ super.key, required this.workspace, - required this.popoverMutex, }); final UserWorkspacePB workspace; - final PopoverMutex popoverMutex; - - @override - State createState() => - _WorkspaceMoreActionListState(); -} - -class _WorkspaceMoreActionListState extends State { - bool isPopoverOpen = false; @override Widget build(BuildContext context) { @@ -53,22 +44,9 @@ class _WorkspaceMoreActionListState extends State { return PopoverActionList<_WorkspaceMoreActionWrapper>( direction: PopoverDirection.bottomWithLeftAligned, actions: actions - .map( - (action) => _WorkspaceMoreActionWrapper( - action, - widget.workspace, - () => PopoverContainer.of(context).closeAll(), - ), - ) + .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), - mutex: widget.popoverMutex, constraints: const BoxConstraints(minWidth: 220), - animationDuration: Durations.short3, - slideDistance: 2, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - onClosed: () => isPopoverOpen = false, - asBarrier: true, buildChild: (controller) { return SizedBox.square( dimension: 24.0, @@ -78,10 +56,7 @@ class _WorkspaceMoreActionListState extends State { FlowySvgs.workspace_three_dots_s, ), onTap: () { - if (!isPopoverOpen) { - controller.show(); - isPopoverOpen = true; - } + controller.show(); }, ), ); @@ -92,22 +67,13 @@ class _WorkspaceMoreActionListState extends State { } class _WorkspaceMoreActionWrapper extends CustomActionCell { - _WorkspaceMoreActionWrapper( - this.inner, - this.workspace, - this.closeWorkspaceMenu, - ); + _WorkspaceMoreActionWrapper(this.inner, this.workspace); final WorkspaceMoreAction inner; final UserWorkspacePB workspace; - final VoidCallback closeWorkspaceMenu; @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { + Widget buildWithContext(BuildContext context, PopoverController controller) { if (inner == WorkspaceMoreAction.divider) { return const Divider(); } @@ -119,39 +85,34 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { BuildContext context, PopoverController controller, ) { - return FlowyIconTextButton( - leftIconBuilder: (onHover) => buildLeftIcon(context, onHover), + return FlowyButton( + leftIcon: buildLeftIcon(context), iconPadding: 10.0, - textBuilder: (onHover) => FlowyText.regular( + text: FlowyText.regular( name, fontSize: 14.0, - figmaLineHeight: 18.0, color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave] - .contains(inner) && - onHover + .contains(inner) ? Theme.of(context).colorScheme.error : null, ), margin: const EdgeInsets.all(6), onTap: () async { PopoverContainer.of(context).closeAll(); - closeWorkspaceMenu(); final workspaceBloc = context.read(); switch (inner) { case WorkspaceMoreAction.divider: break; case WorkspaceMoreAction.delete: - await showConfirmDeletionDialog( - context: context, - name: workspace.name, - description: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), - onConfirm: () { + await NavigatorAlertDialog( + title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), + confirm: () { workspaceBloc.add( UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId), ); }, - ); + ).show(context); case WorkspaceMoreAction.rename: await NavigatorTextFieldDialog( title: LocaleKeys.workspace_renameWorkspace.tr(), @@ -168,17 +129,17 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { }, ).show(context); case WorkspaceMoreAction.leave: - await showConfirmDialog( + await showDialog( context: context, - title: LocaleKeys.workspace_leaveCurrentWorkspace.tr(), - description: - LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), - confirmLabel: LocaleKeys.button_yes.tr(), - onConfirm: () { - workspaceBloc.add( - UserWorkspaceEvent.leaveWorkspace(workspace.workspaceId), - ); - }, + builder: (_) => NavigatorOkCancelDialog( + message: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), + onOkPressed: () { + workspaceBloc.add( + UserWorkspaceEvent.leaveWorkspace(workspace.workspaceId), + ); + }, + okTitle: LocaleKeys.button_yes.tr(), + ), ); } }, @@ -198,19 +159,19 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { } } - Widget buildLeftIcon(BuildContext context, bool onHover) { + Widget buildLeftIcon(BuildContext context) { switch (inner) { case WorkspaceMoreAction.delete: return FlowySvg( - FlowySvgs.trash_s, - color: onHover ? Theme.of(context).colorScheme.error : null, + FlowySvgs.delete_s, + color: Theme.of(context).colorScheme.error, ); case WorkspaceMoreAction.rename: return const FlowySvg(FlowySvgs.view_item_rename_s); case WorkspaceMoreAction.leave: return FlowySvg( FlowySvgs.logout_s, - color: onHover ? Theme.of(context).colorScheme.error : null, + color: Theme.of(context).colorScheme.error, ); case WorkspaceMoreAction.divider: return const SizedBox.shrink(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 1f9f4b03b8..491e1c42cf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,15 +1,11 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; +import 'dart:math'; + +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '../../../../../../shared/icon_emoji_picker/tab.dart'; class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ @@ -22,8 +18,6 @@ class WorkspaceIcon extends StatefulWidget { this.borderRadius = 4, this.emojiSize, this.alignment, - required this.figmaLineHeight, - this.showBorder = true, }); final UserWorkspacePB workspace; @@ -31,11 +25,9 @@ class WorkspaceIcon extends StatefulWidget { final bool enableEdit; final double fontSize; final double? emojiSize; - final void Function(EmojiIconData) onSelected; + final void Function(EmojiPickerResult) onSelected; final double borderRadius; final Alignment? alignment; - final double figmaLineHeight; - final bool showBorder; @override State createState() => _WorkspaceIconState(); @@ -46,59 +38,47 @@ class _WorkspaceIconState extends State { @override Widget build(BuildContext context) { - final color = ColorGenerator(widget.workspace.name).randomColor(); Widget child = widget.workspace.icon.isNotEmpty - ? FlowyText.emoji( - widget.workspace.icon, - fontSize: widget.emojiSize, - figmaLineHeight: widget.figmaLineHeight, - optimizeEmojiAlign: true, + ? Container( + width: widget.iconSize, + alignment: widget.alignment ?? Alignment.center, + child: FlowyText.emoji( + widget.workspace.icon, + fontSize: widget.emojiSize ?? widget.iconSize, + ), ) - : FlowyText.semibold( - widget.workspace.name.isEmpty - ? '' - : widget.workspace.name.substring(0, 1), - fontSize: widget.fontSize, - color: color.$1, + : Container( + alignment: Alignment.center, + width: widget.iconSize, + height: min(widget.iconSize, 24), + decoration: BoxDecoration( + color: ColorGenerator(widget.workspace.name).toColor(), + borderRadius: BorderRadius.circular(widget.borderRadius), + border: Border.all( + color: const Color(0xa1717171), + width: 0.5, + ), + ), + child: FlowyText.semibold( + widget.workspace.name.isEmpty + ? '' + : widget.workspace.name.substring(0, 1), + fontSize: widget.fontSize, + color: Colors.black, + ), ); - child = Container( - alignment: Alignment.center, - width: widget.iconSize, - height: widget.iconSize, - decoration: BoxDecoration( - color: widget.workspace.icon.isNotEmpty ? null : color.$2, - borderRadius: BorderRadius.circular(widget.borderRadius), - border: widget.showBorder - ? Border.all( - color: const Color(0x1A717171), - ) - : null, - ), - child: child, - ); - if (widget.enableEdit) { - child = _buildEditableIcon(child); - } - - return child; - } - - Widget _buildEditableIcon(Widget child) { - if (UniversalPlatform.isDesktopOrWeb) { - return AppFlowyPopover( + child = AppFlowyPopover( offset: const Offset(0, 8), controller: controller, direction: PopoverDirection.bottomWithLeftAligned, constraints: BoxConstraints.loose(const Size(364, 356)), clickHandler: PopoverClickHandler.gestureDetector, - margin: const EdgeInsets.all(0), - popupBuilder: (_) => FlowyIconEmojiPicker( - tabs: const [PickerTabType.emoji], - onSelectedEmoji: (r) { - widget.onSelected(r.data); - if (!r.keepOpen) controller.close(); + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) { + widget.onSelected(result); + controller.close(); }, ), child: MouseRegion( @@ -107,24 +87,6 @@ class _WorkspaceIconState extends State { ), ); } - - return GestureDetector( - onTap: () async { - final result = await context.push( - Uri( - path: MobileEmojiPickerScreen.routeName, - queryParameters: { - MobileEmojiPickerScreen.pageTitle: - LocaleKeys.settings_workspacePage_workspaceIcon_title.tr(), - MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name], - }, - ).toString(), - ); - if (result != null) { - widget.onSelected(result); - } - }, - child: child, - ); + return child; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index 4ff5ccbf67..8a1b172aff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'dart:io'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.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'; @@ -11,22 +11,17 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; - -import '_sidebar_import_notion.dart'; @visibleForTesting const createWorkspaceButtonKey = ValueKey('createWorkspaceButton'); -@visibleForTesting -const importNotionButtonKey = ValueKey('importNotinoButton'); - -class WorkspacesMenu extends StatefulWidget { +class WorkspacesMenu extends StatelessWidget { const WorkspacesMenu({ super.key, required this.userProfile, @@ -38,13 +33,6 @@ class WorkspacesMenu extends StatefulWidget { final UserWorkspacePB currentWorkspace; final List workspaces; - @override - State createState() => _WorkspacesMenuState(); -} - -class _WorkspacesMenuState extends State { - final popoverMutex = PopoverMutex(); - @override Widget build(BuildContext context) { return Column( @@ -53,7 +41,7 @@ class _WorkspacesMenuState extends State { children: [ // user email Padding( - padding: const EdgeInsets.only(left: 10.0, top: 6.0, right: 10.0), + padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( children: [ Expanded( @@ -65,64 +53,39 @@ class _WorkspacesMenuState extends State { ), ), const HSpace(4.0), - WorkspaceMoreButton( - popoverMutex: popoverMutex, - ), + const _WorkspaceMoreButton(), const HSpace(8.0), ], ), ), const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 6.0), + padding: EdgeInsets.symmetric(vertical: 8.0), child: Divider(height: 1.0), ), // workspace list - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final workspace in widget.workspaces) ...[ - WorkspaceMenuItem( - key: ValueKey(workspace.workspaceId), - workspace: workspace, - userProfile: widget.userProfile, - isSelected: workspace.workspaceId == - widget.currentWorkspace.workspaceId, - popoverMutex: popoverMutex, - ), - const VSpace(6.0), - ], - ], - ), - ), - ), - // add new workspace - const Padding( - padding: EdgeInsets.symmetric(horizontal: 6.0), - child: _CreateWorkspaceButton(), - ), - - if (UniversalPlatform.isDesktop) ...[ - const Padding( - padding: EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0), - child: _ImportNotionButton(), + for (final workspace in workspaces) ...[ + WorkspaceMenuItem( + key: ValueKey(workspace.workspaceId), + workspace: workspace, + userProfile: userProfile, + isSelected: workspace.workspaceId == currentWorkspace.workspaceId, ), + const VSpace(6.0), ], - + // add new workspace + const _CreateWorkspaceButton(), const VSpace(6.0), ], ); } String _getUserInfo() { - if (widget.userProfile.email.isNotEmpty) { - return widget.userProfile.email; + if (userProfile.email.isNotEmpty) { + return userProfile.email; } - if (widget.userProfile.name.isNotEmpty) { - return widget.userProfile.name; + if (userProfile.name.isNotEmpty) { + return userProfile.name; } return LocaleKeys.defaultUsername.tr(); @@ -135,13 +98,11 @@ class WorkspaceMenuItem extends StatefulWidget { required this.workspace, required this.userProfile, required this.isSelected, - required this.popoverMutex, }); final UserProfilePB userProfile; final UserWorkspacePB workspace; final bool isSelected; - final PopoverMutex popoverMutex; @override State createState() => _WorkspaceMenuItemState(); @@ -195,47 +156,55 @@ class _WorkspaceMenuItemState extends State { } Widget _buildLeftIcon(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.document_plugins_cover_changeIcon.tr(), - child: WorkspaceIcon( - workspace: widget.workspace, - iconSize: 36, - emojiSize: 24.0, - fontSize: 18.0, - figmaLineHeight: 26.0, - borderRadius: 12.0, - enableEdit: true, - onSelected: (result) => context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - widget.workspace.workspaceId, - result.emoji, + return Container( + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x01717171).withOpacity(0.12), + width: 0.8, + ), + ), + child: FlowyTooltip( + message: LocaleKeys.document_plugins_cover_changeIcon.tr(), + child: WorkspaceIcon( + workspace: widget.workspace, + iconSize: 22, + fontSize: 16, + enableEdit: true, + onSelected: (result) => context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + widget.workspace.workspaceId, + result.emoji, + ), ), - ), + ), ), ); } Widget _buildRightIcon(BuildContext context, ValueNotifier isHovered) { + // only the owner can update or delete workspace. + if (context.read().state.isLoading) { + return const SizedBox.shrink(); + } + return Row( children: [ - // only the owner can update or delete workspace. - if (!context.read().state.isLoading) - ValueListenableBuilder( - valueListenable: isHovered, - builder: (context, value, child) { - return Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Opacity( - opacity: value ? 1.0 : 0.0, - child: child, - ), - ); - }, - child: WorkspaceMoreActionList( - workspace: widget.workspace, - popoverMutex: widget.popoverMutex, - ), - ), + ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, value, child) { + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Opacity( + opacity: value ? 1.0 : 0.0, + child: child, + ), + ); + }, + child: WorkspaceMoreActionList(workspace: widget.workspace), + ), const HSpace(8.0), if (widget.isSelected) ...[ const Padding( @@ -264,55 +233,53 @@ class _WorkspaceInfo extends StatelessWidget { @override Widget build(BuildContext context) { - final memberCount = workspace.memberCount.toInt(); - return FlowyButton( - onTap: () => _openWorkspace(context), - iconPadding: 10.0, - leftIconSize: const Size.square(32), - leftIcon: const SizedBox.square(dimension: 32), - rightIcon: const HSpace(32.0), - text: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // workspace name - FlowyText.medium( - workspace.name, - fontSize: 14.0, - figmaLineHeight: 17.0, - overflow: TextOverflow.ellipsis, - withTooltip: true, + return BlocBuilder( + builder: (context, state) { + final members = state.members; + return FlowyButton( + onTap: () => _openWorkspace(context), + iconPadding: 10.0, + leftIconSize: const Size.square(32), + leftIcon: const SizedBox.square(dimension: 32), + rightIcon: const HSpace(32.0), + text: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // workspace name + FlowyText.medium( + workspace.name, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + withTooltip: true, + ), + if (Platform.isMacOS) const VSpace(2.0), + // workspace members count + FlowyText.regular( + state.isLoading + ? '' + : LocaleKeys.settings_appearance_members_membersCount + .plural( + members.length, + ), + fontSize: 10.0, + color: Theme.of(context).hintColor, + ), + ], ), - // workspace members count - FlowyText.regular( - memberCount == 0 - ? '' - : LocaleKeys.settings_appearance_members_membersCount.plural( - memberCount, - ), - fontSize: 10.0, - figmaLineHeight: 12.0, - color: Theme.of(context).hintColor, - ), - ], - ), + ); + }, ); } void _openWorkspace(BuildContext context) { if (!isSelected) { Log.info('open workspace: ${workspace.workspaceId}'); - - // Persist and close other tabs when switching workspace, restore tabs for new workspace - getIt().add(TabsEvent.switchWorkspace(workspace.workspaceId)); - context.read().add( UserWorkspaceEvent.openWorkspace( workspace.workspaceId, - workspace.workspaceAuthType, ), ); - PopoverContainer.of(context).closeAll(); } } @@ -355,10 +322,8 @@ class _CreateWorkspaceButton extends StatelessWidget { text: Row( children: [ _buildLeftIcon(context), - const HSpace(8.0), - FlowyText.regular( - LocaleKeys.workspace_create.tr(), - ), + const HSpace(10.0), + FlowyText.regular(LocaleKeys.workspace_create.tr()), ], ), ), @@ -367,13 +332,13 @@ class _CreateWorkspaceButton extends StatelessWidget { Widget _buildLeftIcon(BuildContext context) { return Container( - width: 36.0, - height: 36.0, + width: 32.0, + height: 32.0, padding: const EdgeInsets.all(7.0), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(8), border: Border.all( - color: const Color(0x01717171).withValues(alpha: 0.12), + color: const Color(0x01717171).withOpacity(0.12), width: 0.8, ), ), @@ -386,120 +351,21 @@ class _CreateWorkspaceButton extends StatelessWidget { final workspaceBloc = context.read(); await CreateWorkspaceDialog( onConfirm: (name) { - workspaceBloc.add( - UserWorkspaceEvent.createWorkspace( - name, - AuthTypePB.Server, - ), - ); + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); }, ).show(context); } } } -class _ImportNotionButton extends StatelessWidget { - const _ImportNotionButton(); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 40, - child: FlowyButton( - key: importNotionButtonKey, - onTap: () { - _showImportNotinoDialog(context); - }, - margin: const EdgeInsets.symmetric(horizontal: 4.0), - text: Row( - children: [ - _buildLeftIcon(context), - const HSpace(8.0), - FlowyText.regular( - LocaleKeys.workspace_importFromNotion.tr(), - ), - ], - ), - rightIcon: FlowyTooltip( - message: LocaleKeys.workspace_learnMore.tr(), - preferBelow: true, - child: FlowyIconButton( - icon: const FlowySvg( - FlowySvgs.information_s, - ), - onPressed: () { - afLaunchUrlString( - 'https://docs.appflowy.io/docs/guides/import-from-notion', - ); - }, - ), - ), - ), - ); - } - - Widget _buildLeftIcon(BuildContext context) { - return Container( - width: 36.0, - height: 36.0, - padding: const EdgeInsets.all(7.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: const Color(0x01717171).withValues(alpha: 0.12), - width: 0.8, - ), - ), - child: const FlowySvg(FlowySvgs.add_workspace_s), - ); - } - - Future _showImportNotinoDialog(BuildContext context) async { - final result = await getIt().pickFiles( - type: FileType.custom, - allowedExtensions: ['zip'], - ); - - if (result == null || result.files.isEmpty) { - return; - } - - final path = result.files.first.path; - if (path == null) { - return; - } - - if (context.mounted) { - PopoverContainer.of(context).closeAll(); - await NavigatorCustomDialog( - hideCancelButton: true, - confirm: () {}, - child: NotionImporter( - filePath: path, - ), - ).show(context); - } else { - Log.error('context is not mounted when showing import notion dialog'); - } - } -} - -@visibleForTesting -class WorkspaceMoreButton extends StatelessWidget { - const WorkspaceMoreButton({ - super.key, - required this.popoverMutex, - }); - - final PopoverMutex popoverMutex; +class _WorkspaceMoreButton extends StatelessWidget { + const _WorkspaceMoreButton(); @override Widget build(BuildContext context) { return AppFlowyPopover( direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 6), - mutex: popoverMutex, - asBarrier: true, popupBuilder: (_) => FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 50ea9d83c7..2fa91a8da1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -1,15 +1,16 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/loading.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.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'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -27,15 +28,6 @@ class SidebarWorkspace extends StatefulWidget { class _SidebarWorkspaceState extends State { Loading? loadingIndicator; - final ValueNotifier onHover = ValueNotifier(false); - - @override - void dispose() { - onHover.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocConsumer( @@ -47,41 +39,19 @@ class _SidebarWorkspaceState extends State { if (currentWorkspace == null) { return const SizedBox.shrink(); } - return MouseRegion( - onEnter: (_) => onHover.value = true, - onExit: (_) => onHover.value = false, - child: ValueListenableBuilder( - valueListenable: onHover, - builder: (_, onHover, child) { - return Container( - margin: const EdgeInsets.only(right: 8.0), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.0), - color: onHover - ? Theme.of(context).colorScheme.secondary - : Colors.transparent, - ), - child: Row( - children: [ - Expanded( - child: SidebarSwitchWorkspaceButton( - userProfile: widget.userProfile, - currentWorkspace: currentWorkspace, - isHover: onHover, - ), - ), - UserSettingButton( - userProfile: widget.userProfile, - isHover: onHover, - ), - const HSpace(8.0), - NotificationButton(isHover: onHover), - const HSpace(4.0), - ], - ), - ); - }, - ), + return Row( + children: [ + Expanded( + child: SidebarSwitchWorkspaceButton( + userProfile: widget.userProfile, + currentWorkspace: currentWorkspace, + ), + ), + UserSettingButton(userProfile: widget.userProfile), + const HSpace(8.0), + const NotificationButton(), + const HSpace(12.0), + ], ); }, ); @@ -168,13 +138,7 @@ class _SidebarWorkspaceState extends State { } if (message != null) { - showToastNotification( - message: message, - type: result.fold( - (_) => ToastificationType.success, - (_) => ToastificationType.error, - ), - ); + showSnackBarMessage(context, message); } } } @@ -184,12 +148,10 @@ class SidebarSwitchWorkspaceButton extends StatefulWidget { super.key, required this.userProfile, required this.currentWorkspace, - this.isHover = false, }); final UserWorkspacePB currentWorkspace; final UserProfilePB userProfile; - final bool isHover; @override State createState() => @@ -198,26 +160,22 @@ class SidebarSwitchWorkspaceButton extends StatefulWidget { class _SidebarSwitchWorkspaceButtonState extends State { - final PopoverController _popoverController = PopoverController(); + final ValueNotifier _isWorkSpaceMenuExpanded = ValueNotifier(false); @override Widget build(BuildContext context) { return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 5), constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), - margin: EdgeInsets.zero, - animationDuration: Durations.short3, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - controller: _popoverController, - triggerActions: PopoverTriggerFlags.none, onOpen: () { + _isWorkSpaceMenuExpanded.value = true; context .read() .add(const UserWorkspaceEvent.fetchWorkspaces()); }, onClose: () { + _isWorkSpaceMenuExpanded.value = false; Log.info('close workspace menu'); }, popupBuilder: (_) { @@ -240,78 +198,37 @@ class _SidebarSwitchWorkspaceButtonState ), ); }, - child: _SideBarSwitchWorkspaceButtonChild( - currentWorkspace: widget.currentWorkspace, - popoverController: _popoverController, - isHover: widget.isHover, - ), - ); - } -} - -class _SideBarSwitchWorkspaceButtonChild extends StatelessWidget { - const _SideBarSwitchWorkspaceButtonChild({ - required this.popoverController, - required this.currentWorkspace, - required this.isHover, - }); - - final PopoverController popoverController; - final UserWorkspacePB currentWorkspace; - final bool isHover; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - context.read().add( - const UserWorkspaceEvent.fetchWorkspaces(), - ); - popoverController.show(); - }, - behavior: HitTestBehavior.opaque, - child: SizedBox( + child: FlowyButton( + margin: EdgeInsets.zero, + text: SizedBox( height: 30, child: Row( children: [ const HSpace(4.0), WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 26, + workspace: widget.currentWorkspace, + iconSize: 24, fontSize: 16, - emojiSize: 20, + emojiSize: 18, enableEdit: false, borderRadius: 8.0, - figmaLineHeight: 18.0, - showBorder: false, onSelected: (result) => context.read().add( UserWorkspaceEvent.updateWorkspaceIcon( - currentWorkspace.workspaceId, + widget.currentWorkspace.workspaceId, result.emoji, ), ), ), - const HSpace(6), + const HSpace(8), Flexible( child: FlowyText.medium( - currentWorkspace.name, - color: - isHover ? Theme.of(context).colorScheme.onSurface : null, + widget.currentWorkspace.name, overflow: TextOverflow.ellipsis, withTooltip: true, fontSize: 15.0, ), ), - if (isHover) ...[ - const HSpace(4), - FlowySvg( - FlowySvgs.workspace_drop_down_menu_show_s, - color: - isHover ? Theme.of(context).colorScheme.onSurface : null, - ), - ], + const HSpace(4), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index c604fae432..881d926df3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -3,10 +3,10 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; enum DraggableHoverPosition { none, @@ -54,7 +54,7 @@ class _DraggableViewItemState extends State { // add top border if the draggable item is on the top of the list // highlight the draggable item if the draggable item is on the center // add bottom border if the draggable item is on the bottom of the list - final child = UniversalPlatform.isMobile + final child = PlatformExtension.isMobile ? _buildMobileDraggableItem() : _buildDesktopDraggableItem(); @@ -65,11 +65,6 @@ class _DraggableViewItemState extends State { onMove: (data) { final renderBox = context.findRenderObject() as RenderBox; final offset = renderBox.globalToLocal(data.offset); - - if (offset.dx > renderBox.size.width) { - return; - } - final position = _computeHoverPosition(offset, renderBox.size); if (!_shouldAccept(data.data, position)) { return; @@ -81,8 +76,13 @@ class _DraggableViewItemState extends State { ), onAcceptWithDetails: (details) { final data = details.data; - _move(data, widget.view); - _updatePosition(DraggableHoverPosition.none); + _move( + data, + widget.view, + ); + _updatePosition( + DraggableHoverPosition.none, + ); }, feedback: IntrinsicWidth( child: Opacity( @@ -111,8 +111,7 @@ class _DraggableViewItemState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - hoverColor.withValues(alpha: 0.5) + ? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5) : Colors.transparent, ), child: widget.child, @@ -151,10 +150,7 @@ class _DraggableViewItemState extends State { borderRadius: BorderRadius.circular(4.0), color: position == DraggableHoverPosition.center ? widget.centerHighlightColor ?? - Theme.of(context) - .colorScheme - .secondary - .withValues(alpha: 0.5) + Theme.of(context).colorScheme.secondary.withOpacity(0.5) : Colors.transparent, ), child: widget.child, @@ -178,10 +174,12 @@ class _DraggableViewItemState extends State { } void _updatePosition(DraggableHoverPosition position) { - if (UniversalPlatform.isMobile && position != this.position) { + if (PlatformExtension.isMobile && position != this.position) { HapticFeedback.mediumImpact(); } - setState(() => this.position = position); + setState( + () => this.position = position, + ); } void _move(ViewPB from, ViewPB to) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart index d4f91b67d9..3065eb7495 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -17,14 +17,6 @@ enum ViewMoreActionType { divider, lastModified, created, - lockPage; - - static const disableInLockedView = [ - delete, - rename, - moveTo, - changeIcon, - ]; } extension ViewMoreActionTypeExtension on ViewMoreActionType { @@ -50,8 +42,6 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return LocaleKeys.disclosureAction_changeIcon.tr(); case ViewMoreActionType.collapseAllPages: return LocaleKeys.disclosureAction_collapseAllPages.tr(); - case ViewMoreActionType.lockPage: - return LocaleKeys.disclosureAction_lockPage.tr(); case ViewMoreActionType.divider: case ViewMoreActionType.lastModified: case ViewMoreActionType.created: @@ -59,33 +49,32 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { } } - FlowySvgData get leftIconSvg { + Widget get leftIcon { switch (this) { case ViewMoreActionType.delete: - return FlowySvgs.trash_s; + return const FlowySvg(FlowySvgs.trash_s, blendMode: null); case ViewMoreActionType.favorite: - return FlowySvgs.favorite_s; + return const FlowySvg(FlowySvgs.favorite_s); case ViewMoreActionType.unFavorite: - return FlowySvgs.unfavorite_s; + return const FlowySvg(FlowySvgs.unfavorite_s); case ViewMoreActionType.duplicate: - return FlowySvgs.duplicate_s; + return const FlowySvg(FlowySvgs.duplicate_s); + case ViewMoreActionType.copyLink: + return const Icon(Icons.copy); case ViewMoreActionType.rename: - return FlowySvgs.view_item_rename_s; + return const FlowySvg(FlowySvgs.view_item_rename_s); case ViewMoreActionType.moveTo: - return FlowySvgs.move_to_s; + return const FlowySvg(FlowySvgs.move_to_s); case ViewMoreActionType.openInNewTab: - return FlowySvgs.view_item_open_in_new_tab_s; + return const FlowySvg(FlowySvgs.view_item_open_in_new_tab_s); case ViewMoreActionType.changeIcon: - return FlowySvgs.change_icon_s; + return const FlowySvg(FlowySvgs.change_icon_s); case ViewMoreActionType.collapseAllPages: - return FlowySvgs.collapse_all_page_s; - case ViewMoreActionType.lockPage: - return FlowySvgs.lock_page_s; + return const FlowySvg(FlowySvgs.collapse_all_page_s); case ViewMoreActionType.divider: case ViewMoreActionType.lastModified: - case ViewMoreActionType.copyLink: case ViewMoreActionType.created: - throw UnsupportedError('No left icon for $this'); + return const SizedBox.shrink(); } } @@ -104,7 +93,6 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { case ViewMoreActionType.delete: case ViewMoreActionType.lastModified: case ViewMoreActionType.created: - case ViewMoreActionType.lockPage: return const SizedBox.shrink(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart index d0b99e2a5d..5d01b2a71f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -5,6 +5,7 @@ import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -15,7 +16,6 @@ class ViewAddButton extends StatelessWidget { required this.parentViewId, required this.onEditing, required this.onSelected, - this.isHovered = false, }); final String parentViewId; @@ -27,7 +27,6 @@ class ViewAddButton extends StatelessWidget { bool openAfterCreated, bool createNewView, ) onSelected; - final bool isHovered; List get _actions { return [ @@ -58,10 +57,7 @@ class ViewAddButton extends StatelessWidget { buildChild: (popover) { return FlowyIconButton( width: 24, - icon: FlowySvg( - FlowySvgs.view_item_add_s, - color: isHovered ? Theme.of(context).colorScheme.onSurface : null, - ), + icon: const FlowySvg(FlowySvgs.view_item_add_s), onPressed: () { onEditing(true); popover.show(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 22182f7429..290f5ba4e0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,36 +1,30 @@ -import 'dart:async'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.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/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -44,8 +38,6 @@ typedef ViewItemRightIconsBuilder = List Function( ViewPB view, ); -enum IgnoreViewType { none, hide, disable } - class ViewItem extends StatelessWidget { const ViewItem({ super.key, @@ -60,7 +52,7 @@ class ViewItem extends StatelessWidget { this.isDraggable = true, required this.isFeedback, this.height = HomeSpaceViewSizes.viewHeight, - this.isHoverEnabled = false, + this.isHoverEnabled = true, this.isPlaceholder = false, this.isHovered, this.shouldRenderChildren = true, @@ -69,10 +61,6 @@ class ViewItem extends StatelessWidget { this.shouldLoadChildViews = true, this.isExpandedNotifier, this.extendBuilder, - this.disableSelectedStatus, - this.shouldIgnoreView, - this.engagedInExpanding = false, - this.enableRightClickContext = false, }); final ViewPB view; @@ -120,7 +108,6 @@ class ViewItem extends StatelessWidget { // custom the left icon widget, if it's null, the default expand/collapse icon will be used final ViewItemLeftIconBuilder? leftIconBuilder; - // custom the right icon widget, if it's null, the default ... and + button will be used final ViewItemRightIconsBuilder? rightIconsBuilder; @@ -129,27 +116,12 @@ class ViewItem extends StatelessWidget { final List Function(ViewPB view)? extendBuilder; - // disable the selected status of the view item - final bool? disableSelectedStatus; - - // ignore the views when rendering the child views - final IgnoreViewType Function(ViewPB view)? shouldIgnoreView; - - /// Whether to add right-click to show the view action context menu - /// - final bool enableRightClickContext; - - /// to record the ViewBlock which is expanded or collapsed - final bool engagedInExpanding; - @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => ViewBloc( - view: view, - shouldLoadChildViews: shouldLoadChildViews, - engagedInExpanding: engagedInExpanding, - )..add(const ViewEvent.initial()), + create: (_) => + ViewBloc(view: view, shouldLoadChildViews: shouldLoadChildViews) + ..add(const ViewEvent.initial()), child: BlocConsumer( listenWhen: (p, c) => c.lastCreatedView != null && @@ -157,25 +129,15 @@ class ViewItem extends StatelessWidget { listener: (context, state) => context.read().openPlugin(state.lastCreatedView!), builder: (context, state) { - // filter the child views that should be ignored - List childViews = state.view.childViews; - if (shouldIgnoreView != null) { - childViews = childViews - .where((v) => shouldIgnoreView!(v) != IgnoreViewType.hide) - .toList(); - } - - final Widget child = InnerViewItem( + return InnerViewItem( view: state.view, parentView: parentView, - childViews: childViews, + childViews: state.view.childViews, spaceType: spaceType, level: level, leftPadding: leftPadding, showActions: state.isEditing, - enableRightClickContext: enableRightClickContext, isExpanded: state.isExpanded, - disableSelectedStatus: disableSelectedStatus, onSelected: onSelected, onTertiarySelected: onTertiarySelected, isFirstChild: isFirstChild, @@ -190,31 +152,13 @@ class ViewItem extends StatelessWidget { rightIconsBuilder: rightIconsBuilder, isExpandedNotifier: isExpandedNotifier, extendBuilder: extendBuilder, - shouldIgnoreView: shouldIgnoreView, - engagedInExpanding: engagedInExpanding, ); - - if (shouldIgnoreView?.call(view) == IgnoreViewType.disable) { - return Opacity( - opacity: 0.5, - child: FlowyTooltip( - message: LocaleKeys.space_cannotMovePageToDatabase.tr(), - child: MouseRegion( - cursor: SystemMouseCursors.forbidden, - child: IgnorePointer(child: child), - ), - ), - ); - } - - return child; }, ), ); } } -// TODO: We shouldn't have local global variables bool _isDragging = false; class InnerViewItem extends StatefulWidget { @@ -229,7 +173,6 @@ class InnerViewItem extends StatefulWidget { required this.level, required this.leftPadding, required this.showActions, - this.enableRightClickContext = false, required this.onSelected, this.onTertiarySelected, this.isFirstChild = false, @@ -243,9 +186,6 @@ class InnerViewItem extends StatefulWidget { required this.rightIconsBuilder, this.isExpandedNotifier, required this.extendBuilder, - this.disableSelectedStatus, - this.engagedInExpanding = false, - required this.shouldIgnoreView, }); final ViewPB view; @@ -256,7 +196,6 @@ class InnerViewItem extends StatefulWidget { final bool isDraggable; final bool isExpanded; final bool isFirstChild; - // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; @@ -264,14 +203,12 @@ class InnerViewItem extends StatefulWidget { final double leftPadding; final bool showActions; - final bool enableRightClickContext; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; final double height; final bool isHoverEnabled; final bool isPlaceholder; - final bool? disableSelectedStatus; final ValueNotifier? isHovered; final bool shouldRenderChildren; final ViewItemLeftIconBuilder? leftIconBuilder; @@ -279,8 +216,6 @@ class InnerViewItem extends StatefulWidget { final PropertyValueNotifier? isExpandedNotifier; final List Function(ViewPB view)? extendBuilder; - final IgnoreViewType Function(ViewPB view)? shouldIgnoreView; - final bool engagedInExpanding; @override State createState() => _InnerViewItemState(); @@ -301,34 +236,24 @@ class _InnerViewItemState extends State { @override Widget build(BuildContext context) { - Widget child = ValueListenableBuilder( - valueListenable: getIt().notifier, - builder: (context, value, _) { - final isSelected = value?.id == widget.view.id; - return SingleInnerViewItem( - view: widget.view, - parentView: widget.parentView, - level: widget.level, - showActions: widget.showActions, - enableRightClickContext: widget.enableRightClickContext, - spaceType: widget.spaceType, - onSelected: widget.onSelected, - onTertiarySelected: widget.onTertiarySelected, - isExpanded: widget.isExpanded, - isDraggable: widget.isDraggable, - leftPadding: widget.leftPadding, - isFeedback: widget.isFeedback, - height: widget.height, - isPlaceholder: widget.isPlaceholder, - isHovered: widget.isHovered, - leftIconBuilder: widget.leftIconBuilder, - rightIconsBuilder: widget.rightIconsBuilder, - extendBuilder: widget.extendBuilder, - disableSelectedStatus: widget.disableSelectedStatus, - shouldIgnoreView: widget.shouldIgnoreView, - isSelected: isSelected, - ); - }, + Widget child = SingleInnerViewItem( + view: widget.view, + parentView: widget.parentView, + level: widget.level, + showActions: widget.showActions, + spaceType: widget.spaceType, + onSelected: widget.onSelected, + onTertiarySelected: widget.onTertiarySelected, + isExpanded: widget.isExpanded, + isDraggable: widget.isDraggable, + leftPadding: widget.leftPadding, + isFeedback: widget.isFeedback, + height: widget.height, + isPlaceholder: widget.isPlaceholder, + isHovered: widget.isHovered, + leftIconBuilder: widget.leftIconBuilder, + rightIconsBuilder: widget.rightIconsBuilder, + extendBuilder: widget.extendBuilder, ); // if the view is expanded and has child views, render its child views @@ -343,11 +268,9 @@ class _InnerViewItemState extends State { isFirstChild: childView.id == widget.childViews.first.id, view: childView, level: widget.level + 1, - enableRightClickContext: widget.enableRightClickContext, onSelected: widget.onSelected, onTertiarySelected: widget.onTertiarySelected, isDraggable: widget.isDraggable, - disableSelectedStatus: widget.disableSelectedStatus, leftPadding: widget.leftPadding, isFeedback: widget.isFeedback, isPlaceholder: widget.isPlaceholder, @@ -355,14 +278,15 @@ class _InnerViewItemState extends State { leftIconBuilder: widget.leftIconBuilder, rightIconsBuilder: widget.rightIconsBuilder, extendBuilder: widget.extendBuilder, - shouldIgnoreView: widget.shouldIgnoreView, - engagedInExpanding: widget.engagedInExpanding, ); }).toList(); child = Column( mainAxisSize: MainAxisSize.min, - children: [child, ...children], + children: [ + child, + ...children, + ], ); } @@ -372,43 +296,37 @@ class _InnerViewItemState extends State { child = DraggableViewItem( isFirstChild: widget.isFirstChild, view: widget.view, - onDragging: (isDragging) => _isDragging = isDragging, + onDragging: (isDragging) { + _isDragging = isDragging; + }, onMove: widget.isPlaceholder - ? (from, to) => moveViewCrossSpace( - context, - null, - widget.view, - widget.parentView, - widget.spaceType, - from, - to.parentViewId, - ) + ? (from, to) => _moveViewCrossSection(context, from, to) : null, - feedback: (context) => Container( - width: 250, - decoration: BoxDecoration( - color: Brightness.light == Theme.of(context).brightness - ? Colors.white - : Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: ViewItem( - view: widget.view, - parentView: widget.parentView, - spaceType: widget.spaceType, - level: widget.level, - onSelected: widget.onSelected, - onTertiarySelected: widget.onTertiarySelected, - isDraggable: false, - leftPadding: widget.leftPadding, - isFeedback: true, - enableRightClickContext: widget.enableRightClickContext, - leftIconBuilder: widget.leftIconBuilder, - rightIconsBuilder: widget.rightIconsBuilder, - extendBuilder: widget.extendBuilder, - shouldIgnoreView: widget.shouldIgnoreView, - ), - ), + feedback: (context) { + return Container( + width: 250, + decoration: BoxDecoration( + color: Brightness.light == Theme.of(context).brightness + ? Colors.white + : Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: ViewItem( + view: widget.view, + parentView: widget.parentView, + spaceType: widget.spaceType, + level: widget.level, + onSelected: widget.onSelected, + onTertiarySelected: widget.onTertiarySelected, + isDraggable: false, + leftPadding: widget.leftPadding, + isFeedback: true, + leftIconBuilder: widget.leftIconBuilder, + rightIconsBuilder: widget.rightIconsBuilder, + extendBuilder: widget.extendBuilder, + ), + ); + }, child: child, ); } else { @@ -427,6 +345,37 @@ class _InnerViewItemState extends State { context.read().add(const ViewEvent.collapseAllPages()); } } + + void _moveViewCrossSection( + BuildContext context, + ViewPB from, + ViewPB to, + ) { + if (isReferencedDatabaseView(widget.view, widget.parentView)) { + return; + } + final fromSection = widget.spaceType == FolderSpaceType.public + ? ViewSectionPB.Private + : ViewSectionPB.Public; + final toSection = widget.spaceType == FolderSpaceType.public + ? ViewSectionPB.Public + : ViewSectionPB.Private; + context.read().add( + ViewEvent.move( + from, + to.parentViewId, + null, + fromSection, + toSection, + ), + ); + context.read().add( + ViewEvent.updateViewVisibility( + from, + widget.spaceType == FolderSpaceType.public, + ), + ); + } } class SingleInnerViewItem extends StatefulWidget { @@ -440,7 +389,6 @@ class SingleInnerViewItem extends StatefulWidget { this.isDraggable = true, required this.spaceType, required this.showActions, - this.enableRightClickContext = false, required this.onSelected, this.onTertiarySelected, required this.isFeedback, @@ -451,15 +399,11 @@ class SingleInnerViewItem extends StatefulWidget { required this.leftIconBuilder, required this.rightIconsBuilder, required this.extendBuilder, - required this.disableSelectedStatus, - required this.shouldIgnoreView, - required this.isSelected, }); final ViewPB view; final ViewPB? parentView; final bool isExpanded; - // identify if the view item is rendered as feedback widget inside DraggableItem final bool isFeedback; @@ -468,7 +412,6 @@ class SingleInnerViewItem extends StatefulWidget { final bool isDraggable; final bool showActions; - final bool enableRightClickContext; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; final FolderSpaceType spaceType; @@ -476,14 +419,11 @@ class SingleInnerViewItem extends StatefulWidget { final bool isHoverEnabled; final bool isPlaceholder; - final bool? disableSelectedStatus; final ValueNotifier? isHovered; final ViewItemLeftIconBuilder? leftIconBuilder; final ViewItemRightIconsBuilder? rightIconsBuilder; final List Function(ViewPB view)? extendBuilder; - final IgnoreViewType Function(ViewPB view)? shouldIgnoreView; - final bool isSelected; @override State createState() => _SingleInnerViewItemState(); @@ -491,20 +431,18 @@ class SingleInnerViewItem extends StatefulWidget { class _SingleInnerViewItemState extends State { final controller = PopoverController(); - final viewMoreActionController = PopoverController(); - bool isIconPickerOpened = false; @override Widget build(BuildContext context) { - bool isSelected = widget.isSelected; - - if (widget.disableSelectedStatus == true) { - isSelected = false; - } + final isSelected = + getIt().latestOpenView?.id == widget.view.id; if (widget.isPlaceholder) { - return const SizedBox(height: 4, width: double.infinity); + return const SizedBox( + height: 4, + width: double.infinity, + ); } if (widget.isFeedback || !widget.isHoverEnabled) { @@ -515,21 +453,21 @@ class _SingleInnerViewItemState extends State { } return FlowyHover( - style: HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary), + style: HoverStyle( + hoverColor: Theme.of(context).colorScheme.secondary, + ), resetHoverOnRebuild: widget.showActions || !isIconPickerOpened, buildWhenOnHover: () => !widget.showActions && !_isDragging && !isIconPickerOpened, - isSelected: () => widget.showActions || isSelected, builder: (_, onHover) => _buildViewItem(onHover, isSelected), + isSelected: () => widget.showActions || isSelected, ); } Widget _buildViewItem(bool onHover, [bool isSelected = false]) { final name = FlowyText.regular( - widget.view.nameOrDefault, + widget.view.name, overflow: TextOverflow.ellipsis, - fontSize: 14.0, - figmaLineHeight: 18.0, ); final children = [ const HSpace(2), @@ -540,16 +478,16 @@ class _SingleInnerViewItemState extends State { _buildViewIconButton(), const HSpace(6), // title - Expanded( - child: widget.extendBuilder != null - ? Row( + widget.extendBuilder != null + ? Expanded( + child: Row( children: [ - Flexible(child: name), + name, ...widget.extendBuilder!(widget.view), ], - ) - : name, - ), + ), + ) + : Expanded(child: name), ]; // hover action @@ -558,20 +496,7 @@ class _SingleInnerViewItemState extends State { children.addAll(widget.rightIconsBuilder!(context, widget.view)); } else { // ··· more action button - children.add( - _buildViewMoreActionButton( - context, - viewMoreActionController, - (_) => FlowyTooltip( - message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), - child: FlowyIconButton( - width: 24, - icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), - onPressed: viewMoreActionController.show, - ), - ), - ), - ); + children.add(_buildViewMoreActionButton(context)); // only support add button for document layout if (widget.view.layout == ViewLayoutPB.Document) { // + button @@ -591,18 +516,8 @@ class _SingleInnerViewItemState extends State { height: widget.height, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), - child: Listener( - onPointerDown: (event) { - if (event.buttons == kSecondaryMouseButton && - widget.enableRightClickContext) { - viewMoreActionController.showAt( - // We add some horizontal offset - event.position + const Offset(4, 0), - ); - } - }, - behavior: HitTestBehavior.opaque, - child: Row(children: children), + child: Row( + children: children, ), ), ), @@ -616,9 +531,9 @@ class _SingleInnerViewItemState extends State { offset: const Offset(0, 5), direction: PopoverDirection.bottomWithLeftAligned, popupBuilder: (_) => RenameViewPopover( - view: widget.view, + viewId: widget.view.id, name: widget.view.name, - emoji: widget.view.icon.toEmojiIconData(), + emoji: widget.view.icon.value, popoverController: popoverController, showIconChanger: false, ), @@ -630,21 +545,18 @@ class _SingleInnerViewItemState extends State { } Widget _buildViewIconButton() { - final iconData = widget.view.icon.toEmojiIconData(); - final icon = iconData.isNotEmpty - ? RawEmojiIconWidget( - emoji: iconData, - emojiSize: 16.0, - lineHeight: 18.0 / 16.0, + final icon = widget.view.icon.value.isNotEmpty + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 16.0, ) : Opacity(opacity: 0.6, child: widget.view.defaultIcon()); - final Widget child = AppFlowyPopover( + return AppFlowyPopover( offset: const Offset(20, 0), controller: controller, direction: PopoverDirection.rightWithCenterAligned, constraints: BoxConstraints.loose(const Size(364, 356)), - margin: const EdgeInsets.all(0), onClose: () => setState(() => isIconPickerOpened = false), child: GestureDetector( // prevent the tap event from being passed to the parent widget @@ -656,27 +568,52 @@ class _SingleInnerViewItemState extends State { ), popupBuilder: (context) { isIconPickerOpened = true; - return FlowyIconEmojiPicker( - initialType: iconData.type.toPickerTabType(), - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - documentId: widget.view.id, - onSelectedEmoji: (r) { + return FlowyIconPicker( + onSelected: (result) { ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: r.data, + viewId: widget.view.id, + viewIcon: result.emoji, + iconType: result.type.toProto(), ); - if (!r.keepOpen) controller.close(); + controller.close(); }, ); }, ); + } - if (widget.view.isLocked) { - return LockPageButtonWrapper( + // > button or · button + // show > if the view is expandable. + // show · if the view can't contain child views. + Widget _buildLeftIcon() { + if (isReferencedDatabaseView(widget.view, widget.parentView)) { + return const _DotIconWidget(); + } + + if (context.read().state.view.childViews.isEmpty) { + return HSpace(widget.leftPadding); + } + + final child = FlowyHover( + child: GestureDetector( + child: FlowySvg( + widget.isExpanded + ? FlowySvgs.view_item_expand_s + : FlowySvgs.view_item_unexpand_s, + size: const Size.square(16.0), + ), + onTap: () => context + .read() + .add(ViewEvent.setIsExpanded(!widget.isExpanded)), + ), + ); + + if (widget.isHovered != null) { + return ValueListenableBuilder( + valueListenable: widget.isHovered!, + builder: (_, isHovered, child) { + return Opacity(opacity: isHovered ? 1.0 : 0.0, child: child); + }, child: child, ); } @@ -684,77 +621,58 @@ class _SingleInnerViewItemState extends State { return child; } - // > button or · button - // show > if the view is expandable. - // show · if the view can't contain child views. - Widget _buildLeftIcon() { - return ViewItemDefaultLeftIcon( - view: widget.view, - parentView: widget.parentView, - isExpanded: widget.isExpanded, - leftPadding: widget.leftPadding, - isHovered: widget.isHovered, - ); - } - // + button Widget _buildViewAddButton(BuildContext context) { + final viewBloc = context.read(); return FlowyTooltip( message: LocaleKeys.menuAppHeader_addPageTooltip.tr(), child: ViewAddButton( parentViewId: widget.view.id, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), - onSelected: _onSelected, + onSelected: ( + pluginBuilder, + name, + initialDataBytes, + openAfterCreated, + createNewView, + ) { + if (createNewView) { + createViewAndShowRenameDialogIfNeeded( + context, + _convertLayoutToHintText(pluginBuilder.layoutType!), + (viewName, _) { + if (viewName.isNotEmpty) { + viewBloc.add( + ViewEvent.createView( + viewName, + pluginBuilder.layoutType!, + openAfterCreated: openAfterCreated, + section: widget.spaceType.toViewSectionPB, + ), + ); + } + }, + ); + } + viewBloc.add( + const ViewEvent.setIsExpanded(true), + ); + }, ), ); } - void _onSelected( - PluginBuilder pluginBuilder, - String? name, - List? initialDataBytes, - bool openAfterCreated, - bool createNewView, - ) { - final viewBloc = context.read(); - - // the name of new document should be empty - final viewName = pluginBuilder.layoutType != ViewLayoutPB.Document - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : ''; - viewBloc.add( - ViewEvent.createView( - viewName, - pluginBuilder.layoutType!, - openAfterCreated: openAfterCreated, - section: widget.spaceType.toViewSectionPB, - ), - ); - - viewBloc.add(const ViewEvent.setIsExpanded(true)); - } - // ··· more action button - Widget _buildViewMoreActionButton( - BuildContext context, - PopoverController controller, - Widget Function(PopoverController) buildChild, - ) { - return BlocProvider( - create: (context) => SpaceBloc( - userProfile: context.read().userProfile, - workspaceId: context.read().workspaceId, - )..add(const SpaceEvent.initial(openFirstPage: false)), - child: ViewMoreActionPopover( + Widget _buildViewMoreActionButton(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), + child: ViewMoreActionButton( view: widget.view, - controller: controller, - isExpanded: widget.isExpanded, spaceType: widget.spaceType, onEditing: (value) => context.read().add(ViewEvent.setIsEditing(value)), - buildChild: buildChild, - onAction: (action, data) async { + onAction: (action, data) { switch (action) { case ViewMoreActionType.favorite: case ViewMoreActionType.unFavorite: @@ -763,33 +681,18 @@ class _SingleInnerViewItemState extends State { .add(FavoriteEvent.toggle(widget.view)); break; case ViewMoreActionType.rename: - unawaited( - NavigatorTextFieldDialog( - title: LocaleKeys.disclosureAction_rename.tr(), - autoSelectAllText: true, - value: widget.view.nameOrDefault, - maxLength: 256, - onConfirm: (newValue, _) { - context.read().add(ViewEvent.rename(newValue)); - }, - ).show(context), - ); + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: widget.view.name, + maxLength: 256, + onConfirm: (newValue, _) { + context.read().add(ViewEvent.rename(newValue)); + }, + ).show(context); break; case ViewMoreActionType.delete: - // get if current page contains published child views - final (containPublishedPage, _) = - await ViewBackendService.containPublishedPage(widget.view); - if (containPublishedPage && context.mounted) { - await showConfirmDeletionDialog( - context: context, - name: widget.view.name, - description: LocaleKeys.publish_containsPublishedPage.tr(), - onConfirm: () => - context.read().add(const ViewEvent.delete()), - ); - } else if (context.mounted) { - context.read().add(const ViewEvent.delete()); - } + context.read().add(const ViewEvent.delete()); break; case ViewMoreActionType.duplicate: context.read().add(const ViewEvent.duplicate()); @@ -801,30 +704,16 @@ class _SingleInnerViewItemState extends State { context.read().add(const ViewEvent.collapseAllPages()); break; case ViewMoreActionType.changeIcon: - if (data is! SelectedEmojiIconResult) { + if (data is! EmojiPickerResult) { return; } - await ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: data.data, + final result = data; + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: result.emoji, + iconType: result.type.toProto(), ); break; - case ViewMoreActionType.moveTo: - final value = data; - if (value is! (ViewPB, ViewPB)) { - return; - } - final space = value.$1; - final target = value.$2; - moveViewCrossSpace( - context, - space, - widget.view, - widget.parentView, - widget.spaceType, - widget.view, - target.id, - ); default: throw UnsupportedError('$action is not supported'); } @@ -832,6 +721,22 @@ class _SingleInnerViewItemState extends State { ), ); } + + String _convertLayoutToHintText(ViewLayoutPB layout) { + switch (layout) { + case ViewLayoutPB.Document: + return LocaleKeys.newDocumentText.tr(); + case ViewLayoutPB.Grid: + return LocaleKeys.newGridText.tr(); + case ViewLayoutPB.Board: + return LocaleKeys.newBoardText.tr(); + case ViewLayoutPB.Calendar: + return LocaleKeys.newCalendarText.tr(); + case ViewLayoutPB.Chat: + return LocaleKeys.chat_newChat.tr(); + } + return LocaleKeys.newPageText.tr(); + } } class _DotIconWidget extends StatelessWidget { @@ -860,85 +765,3 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) { } return view.layout.isDatabaseView && parentView.layout.isDatabaseView; } - -void moveViewCrossSpace( - BuildContext context, - ViewPB? toSpace, - ViewPB view, - ViewPB? parentView, - FolderSpaceType spaceType, - ViewPB from, - String toId, -) { - if (isReferencedDatabaseView(view, parentView)) { - return; - } - - if (from.id == toId) { - return; - } - - final currentSpace = context.read().state.currentSpace; - if (currentSpace != null && - toSpace != null && - currentSpace.id != toSpace.id) { - Log.info( - 'Move view(${from.name}) to another space(${toSpace.name}), unpublish the view', - ); - context.read().add(const ViewEvent.unpublish(sync: false)); - } - - context.read().add(ViewEvent.move(from, toId, null, null, null)); -} - -class ViewItemDefaultLeftIcon extends StatelessWidget { - const ViewItemDefaultLeftIcon({ - super.key, - required this.view, - required this.parentView, - required this.isExpanded, - required this.leftPadding, - required this.isHovered, - }); - - final ViewPB view; - final ViewPB? parentView; - final bool isExpanded; - final double leftPadding; - final ValueNotifier? isHovered; - - @override - Widget build(BuildContext context) { - if (isReferencedDatabaseView(view, parentView)) { - return const _DotIconWidget(); - } - - if (context.read().state.view.childViews.isEmpty) { - return HSpace(leftPadding); - } - - final child = FlowyHover( - child: GestureDetector( - child: FlowySvg( - isExpanded - ? FlowySvgs.view_item_expand_s - : FlowySvgs.view_item_unexpand_s, - size: const Size.square(16.0), - ), - onTap: () => - context.read().add(ViewEvent.setIsExpanded(!isExpanded)), - ), - ); - - if (isHovered != null) { - return ValueListenableBuilder( - valueListenable: isHovered!, - builder: (_, isHovered, child) => - Opacity(opacity: isHovered ? 1.0 : 0.0, child: child), - child: child, - ); - } - - return child; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 5b531c2f28..a5ef7e6703 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -1,76 +1,64 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.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/presentation/home/menu/sidebar/move_to/move_page_menu.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; /// ··· button beside the view name -class ViewMoreActionPopover extends StatelessWidget { - const ViewMoreActionPopover({ +class ViewMoreActionButton extends StatelessWidget { + const ViewMoreActionButton({ super.key, required this.view, - this.controller, required this.onEditing, required this.onAction, required this.spaceType, - required this.isExpanded, - required this.buildChild, - this.showAtCursor = false, }); final ViewPB view; - final PopoverController? controller; final void Function(bool value) onEditing; final void Function(ViewMoreActionType type, dynamic data) onAction; final FolderSpaceType spaceType; - final bool isExpanded; - final Widget Function(PopoverController) buildChild; - final bool showAtCursor; @override Widget build(BuildContext context) { final wrappers = _buildActionTypeWrappers(); return PopoverActionList( - controller: controller, direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 8), actions: wrappers, - constraints: const BoxConstraints(minWidth: 260), - onPopupBuilder: () => onEditing(true), - buildChild: buildChild, + constraints: const BoxConstraints( + minWidth: 260, + ), + buildChild: (popover) { + return FlowyIconButton( + width: 24, + icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), + onPressed: () { + onEditing(true); + popover.show(); + }, + ); + }, onSelected: (_, __) {}, onClosed: () => onEditing(false), - showAtCursor: showAtCursor, ); } List _buildActionTypeWrappers() { final actionTypes = _buildActionTypes(); - return actionTypes.map( - (e) { - final actionWrapper = - ViewMoreActionTypeWrapper(e, view, (controller, data) { - onEditing(false); - onAction(e, data); - bool enableClose = true; - if (data is SelectedEmojiIconResult) { - if (data.keepOpen) enableClose = false; - } - if (enableClose) controller.close(); - }); - - return actionWrapper; - }, - ).toList(); + return actionTypes + .map( + (e) => ViewMoreActionTypeWrapper(e, (controller, data) { + onEditing(false); + onAction(e, data); + controller.close(); + }), + ) + .toList(); } List _buildActionTypes() { @@ -104,16 +92,12 @@ class ViewMoreActionPopover extends StatelessWidget { } actionTypes.addAll([ - ViewMoreActionType.moveTo, ViewMoreActionType.delete, ViewMoreActionType.divider, ]); // Chat doesn't change collapse - // Only show collapse all pages if the view has child views - if (view.layout != ViewLayoutPB.Chat && - view.childViews.isNotEmpty && - isExpanded) { + if (view.layout != ViewLayoutPB.Chat) { actionTypes.add(ViewMoreActionType.collapseAllPages); actionTypes.add(ViewMoreActionType.divider); } @@ -126,52 +110,24 @@ class ViewMoreActionPopover extends StatelessWidget { } class ViewMoreActionTypeWrapper extends CustomActionCell { - ViewMoreActionTypeWrapper( - this.inner, - this.sourceView, - this.onTap, { - this.moveActionDirection, - this.moveActionOffset, - }); + ViewMoreActionTypeWrapper(this.inner, this.onTap); final ViewMoreActionType inner; - final ViewPB sourceView; final void Function(PopoverController controller, dynamic data) onTap; - // custom the move to action button - final PopoverDirection? moveActionDirection; - final Offset? moveActionOffset; - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - Widget child; - + Widget buildWithContext(BuildContext context, PopoverController controller) { if (inner == ViewMoreActionType.divider) { - child = _buildDivider(); + return _buildDivider(); } else if (inner == ViewMoreActionType.lastModified) { - child = _buildLastModified(context); + return _buildLastModified(context); } else if (inner == ViewMoreActionType.created) { - child = _buildCreated(context); + return _buildCreated(context); } else if (inner == ViewMoreActionType.changeIcon) { - child = _buildEmojiActionButton(context, controller); - } else if (inner == ViewMoreActionType.moveTo) { - child = _buildMoveToActionButton(context, controller); + return _buildEmojiActionButton(context, controller); } else { - child = _buildNormalActionButton(context, controller); + return _buildNormalActionButton(context, controller); } - - if (ViewMoreActionType.disableInLockedView.contains(inner) && - sourceView.isLocked) { - child = LockPageButtonWrapper( - child: child, - ); - } - - return child; } Widget _buildNormalActionButton( @@ -189,71 +145,19 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { return AppFlowyPopover( constraints: BoxConstraints.loose(const Size(364, 356)), - margin: const EdgeInsets.all(0), + margin: const EdgeInsets.symmetric(horizontal: 14.0, vertical: 12.0), clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (_) => FlowyIconEmojiPicker( - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], - documentId: sourceView.id, - initialType: sourceView.icon.toEmojiIconData().type.toPickerTabType(), - onSelectedEmoji: (result) => onTap(controller, result), + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) => onTap(controller, result), ), child: child, ); } - Widget _buildMoveToActionButton( - BuildContext context, - PopoverController controller, - ) { - final userProfile = context.read().userProfile; - // move to feature doesn't support in local mode - if (userProfile.workspaceAuthType != AuthTypePB.Server) { - return const SizedBox.shrink(); - } - return BlocProvider.value( - value: context.read(), - child: BlocBuilder( - builder: (context, state) { - final child = _buildActionButton(context, null); - return AppFlowyPopover( - constraints: const BoxConstraints( - maxWidth: 260, - maxHeight: 345, - ), - margin: const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ), - clickHandler: PopoverClickHandler.gestureDetector, - direction: - moveActionDirection ?? PopoverDirection.rightWithTopAligned, - offset: moveActionOffset, - popupBuilder: (_) { - return BlocProvider.value( - value: context.read(), - child: MovePageMenu( - sourceView: sourceView, - onSelected: (space, view) { - onTap(controller, (space, view)); - }, - ), - ); - }, - child: child, - ); - }, - ), - ); - } - Widget _buildDivider() { return const Padding( padding: EdgeInsets.all(8.0), - child: FlowyDivider(), + child: Divider(height: 1.0), ); } @@ -286,27 +190,21 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { return Container( height: 34, padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( + child: FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 6), - onTap: onTap, - // show the error color when delete is hovered - leftIconBuilder: (onHover) => FlowySvg( - inner.leftIconSvg, - color: inner == ViewMoreActionType.delete && onHover - ? Theme.of(context).colorScheme.error - : null, - ), - rightIconBuilder: (_) => inner.rightIcon, + leftIcon: inner.leftIcon, + rightIcon: inner.rightIcon, iconPadding: 10.0, - textBuilder: (onHover) => FlowyText.regular( - inner.name, - fontSize: 14.0, - lineHeight: 1.0, - figmaLineHeight: 18.0, - color: inner == ViewMoreActionType.delete && onHover - ? Theme.of(context).colorScheme.error - : null, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + inner.name, + color: inner == ViewMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, + ), ), + onTap: onTap, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index d588e512b0..a90c12565a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -2,8 +2,10 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -13,7 +15,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:styled_widget/styled_widget.dart'; -import 'package:universal_platform/universal_platform.dart'; class NavigationNotifier with ChangeNotifier { NavigationNotifier({required this.navigationItems}); @@ -63,18 +64,24 @@ class FlowyNavigation extends StatelessWidget { return BlocBuilder( buildWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed, builder: (context, state) { - if (!UniversalPlatform.isWindows && state.isMenuCollapsed) { + if (!PlatformExtension.isWindows && state.isMenuCollapsed) { + final color = + Theme.of(context).isLightMode ? Colors.white : Colors.black; final textSpan = TextSpan( children: [ TextSpan( text: '${LocaleKeys.sideBar_openSidebar.tr()}\n', - style: context.tooltipTextStyle(), + style: Theme.of(context) + .tooltipTheme + .textStyle! + .copyWith(color: color), ), TextSpan( text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', - style: context - .tooltipTextStyle() - ?.copyWith(color: Theme.of(context).hintColor), + style: Theme.of(context) + .tooltipTheme + .textStyle! + .copyWith(color: Theme.of(context).hintColor), ), ], ); @@ -162,13 +169,13 @@ class EllipsisNaviItem extends NavigationItem { final List items; @override - String? get viewName => null; + Widget get leftBarItem => FlowyText.medium( + '...', + fontSize: FontSizes.s16, + ); @override - Widget get leftBarItem => FlowyText.medium('...', fontSize: FontSizes.s16); - - @override - Widget tabBarItem(String pluginId, [bool shortForm = false]) => leftBarItem; + Widget tabBarItem(String pluginId) => leftBarItem; @override NavigationCallback get action => (id) {}; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart index 7e4a5f8df1..4760378f4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart @@ -1,15 +1,10 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; class FlowyTab extends StatefulWidget { @@ -17,107 +12,56 @@ class FlowyTab extends StatefulWidget { super.key, required this.pageManager, required this.isCurrent, - required this.onTap, - required this.isAllPinned, }); final PageManager pageManager; final bool isCurrent; - final VoidCallback onTap; - - /// Signifies whether all tabs are pinned - /// - final bool isAllPinned; @override State createState() => _FlowyTabState(); } class _FlowyTabState extends State { - final controller = PopoverController(); + bool _isHovering = false; @override Widget build(BuildContext context) { - return SizedBox( - width: widget.pageManager.isPinned ? 54 : null, - child: _wrapInTooltip( - widget.pageManager.plugin.widgetBuilder.viewName, - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - borderRadius: BorderRadius.zero, - backgroundColor: widget.isCurrent - ? Theme.of(context).colorScheme.surface - : Theme.of(context).colorScheme.surfaceContainerHighest, - hoverColor: - widget.isCurrent ? Theme.of(context).colorScheme.surface : null, + return GestureDetector( + onTertiaryTapUp: _closeTab, + child: MouseRegion( + onEnter: (_) => _setHovering(true), + onExit: (_) => _setHovering(), + child: Container( + width: HomeSizes.tabBarWidth, + height: HomeSizes.tabBarHeight, + decoration: BoxDecoration( + color: _getBackgroundColor(), ), - builder: (context, isHovering) => AppFlowyPopover( - controller: controller, - offset: const Offset(4, 4), - triggerActions: PopoverTriggerFlags.secondaryClick, - showAtCursor: true, - popupBuilder: (_) => BlocProvider.value( - value: context.read(), - child: TabMenu( - controller: controller, - pageId: widget.pageManager.plugin.id, - isPinned: widget.pageManager.isPinned, - isAllPinned: widget.isAllPinned, - ), - ), - child: ChangeNotifierProvider.value( - value: widget.pageManager.notifier, - child: Consumer( - builder: (context, value, _) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - // We use a Listener to avoid gesture detector onPanStart debounce - child: Listener( - onPointerDown: (event) { - if (event.buttons == kPrimaryButton) { - widget.onTap(); - } - }, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - // Stop move window detector - onPanStart: (_) {}, - child: Container( - constraints: BoxConstraints( - maxWidth: HomeSizes.tabBarWidth, - minWidth: widget.pageManager.isPinned ? 54 : 100, - ), - height: HomeSizes.tabBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: widget.pageManager.notifier.tabBarWidget( - widget.pageManager.plugin.id, - widget.pageManager.isPinned, - ), - ), - if (!widget.pageManager.isPinned) ...[ - Visibility( - visible: isHovering, - child: SizedBox( - width: 26, - height: 26, - child: FlowyIconButton( - onPressed: () => _closeTab(context), - icon: const FlowySvg( - FlowySvgs.close_s, - size: Size.square(22), - ), - ), - ), - ), - ], - ], + child: ChangeNotifierProvider.value( + value: widget.pageManager.notifier, + child: Consumer( + builder: (context, value, child) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: widget.pageManager.notifier + .tabBarWidget(widget.pageManager.plugin.id), + ), + Visibility( + visible: _isHovering, + child: FlowyIconButton( + onPressed: _closeTab, + hoverColor: Theme.of(context).hoverColor, + iconColorOnHover: + Theme.of(context).colorScheme.onSurface, + icon: const FlowySvg( + FlowySvgs.close_s, + size: Size.fromWidth(16), ), ), ), - ), + ], ), ), ), @@ -127,111 +71,25 @@ class _FlowyTabState extends State { ); } - void _closeTab(BuildContext context) => context + void _setHovering([bool isHovering = false]) { + if (mounted) { + setState(() => _isHovering = isHovering); + } + } + + Color _getBackgroundColor() { + if (widget.isCurrent) { + return Theme.of(context).colorScheme.onSecondaryContainer; + } + + if (_isHovering) { + return AFThemeExtension.of(context).lightGreyHover; + } + + return Theme.of(context).colorScheme.surfaceContainerHighest; + } + + void _closeTab([TapUpDetails? details]) => context .read() .add(TabsEvent.closeTab(widget.pageManager.plugin.id)); - - Widget _wrapInTooltip(String? viewName, {required Widget child}) { - if (viewName != null) { - return FlowyTooltip( - message: viewName, - child: child, - ); - } - - return child; - } -} - -@visibleForTesting -class TabMenu extends StatelessWidget { - const TabMenu({ - super.key, - required this.controller, - required this.pageId, - required this.isPinned, - required this.isAllPinned, - }); - - final PopoverController controller; - final String pageId; - final bool isPinned; - final bool isAllPinned; - - @override - Widget build(BuildContext context) { - return SeparatedColumn( - separatorBuilder: () => const VSpace(4), - mainAxisSize: MainAxisSize.min, - children: [ - Opacity( - opacity: isPinned ? 0.5 : 1, - child: _wrapInTooltip( - shouldWrap: isPinned, - message: LocaleKeys.tabMenu_closeDisabledHint.tr(), - child: FlowyButton( - text: FlowyText.regular(LocaleKeys.tabMenu_close.tr()), - onTap: () => _closeTab(context), - disable: isPinned, - ), - ), - ), - Opacity( - opacity: isAllPinned ? 0.5 : 1, - child: _wrapInTooltip( - shouldWrap: true, - message: isAllPinned - ? LocaleKeys.tabMenu_closeOthersDisabledHint.tr() - : LocaleKeys.tabMenu_closeOthersHint.tr(), - child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.tabMenu_closeOthers.tr(), - ), - onTap: () => _closeOtherTabs(context), - disable: isAllPinned, - ), - ), - ), - const Divider(height: 0.5), - FlowyButton( - text: FlowyText.regular( - isPinned - ? LocaleKeys.tabMenu_unpinTab.tr() - : LocaleKeys.tabMenu_pinTab.tr(), - ), - onTap: () => _togglePin(context), - ), - ], - ); - } - - Widget _wrapInTooltip({ - required bool shouldWrap, - String? message, - required Widget child, - }) { - if (shouldWrap) { - return FlowyTooltip( - message: message, - child: child, - ); - } - - return child; - } - - void _closeTab(BuildContext context) { - context.read().add(TabsEvent.closeTab(pageId)); - controller.close(); - } - - void _closeOtherTabs(BuildContext context) { - context.read().add(TabsEvent.closeOtherTabs(pageId)); - controller.close(); - } - - void _togglePin(BuildContext context) { - context.read().add(TabsEvent.togglePin(pageId)); - controller.close(); - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart index 38ede2421e..064d64477f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -1,62 +1,100 @@ -import 'package:appflowy/core/frameless_window.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class TabsManager extends StatelessWidget { - const TabsManager({super.key, required this.onIndexChanged}); +class TabsManager extends StatefulWidget { + const TabsManager({super.key, required this.pageController}); - final void Function(int) onIndexChanged; + final PageController pageController; + + @override + State createState() => _TabsManagerState(); +} + +class _TabsManagerState extends State + with TickerProviderStateMixin { + late TabController _controller; + + @override + void initState() { + super.initState(); + _controller = TabController(vsync: this, length: 1); + } @override Widget build(BuildContext context) { - return BlocConsumer( - listenWhen: (prev, curr) => - prev.currentIndex != curr.currentIndex || prev.pages != curr.pages, - listener: (context, state) => onIndexChanged(state.currentIndex), - builder: (context, state) { - if (state.pages == 1) { - return const SizedBox.shrink(); - } + return BlocProvider.value( + value: BlocProvider.of(context), + child: BlocListener( + listener: (context, state) { + if (_controller.length != state.pages) { + _controller.dispose(); + _controller = TabController( + vsync: this, + initialIndex: state.currentIndex, + length: state.pages, + ); + } - final isAllPinned = state.isAllPinned; + if (state.currentIndex != widget.pageController.page) { + // Unfocus editor to hide selection toolbar + FocusScope.of(context).unfocus(); - return Container( - alignment: Alignment.bottomLeft, - height: HomeSizes.tabBarHeight, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - ), - child: MoveWindowDetector( - child: Row( - children: state.pageManagers.map((pm) { - return Flexible( - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: HomeSizes.tabBarWidth, - ), - child: FlowyTab( - key: ValueKey('tab-${pm.plugin.id}'), - pageManager: pm, - isCurrent: state.currentPageManager == pm, - isAllPinned: isAllPinned, - onTap: () { - if (state.currentPageManager != pm) { - final index = state.pageManagers.indexOf(pm); - onIndexChanged(index); - } - }, - ), - ), - ); - }).toList(), - ), - ), - ); - }, + widget.pageController.animateToPage( + state.currentIndex, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + if (_controller.length == 1) { + return const SizedBox.shrink(); + } + + return Container( + alignment: Alignment.bottomLeft, + height: HomeSizes.tabBarHeight, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + ), + + /// TODO(Xazin): Custom Reorderable TabBar + child: TabBar( + padding: EdgeInsets.zero, + labelPadding: EdgeInsets.zero, + indicator: BoxDecoration( + border: Border.all(width: 0, color: Colors.transparent), + ), + indicatorWeight: 0, + dividerColor: Colors.transparent, + isScrollable: true, + controller: _controller, + onTap: (newIndex) => + context.read().add(TabsEvent.selectTab(newIndex)), + tabs: state.pageManagers + .map( + (pm) => FlowyTab( + key: UniqueKey(), + pageManager: pm, + isCurrent: state.currentPageManager == pm, + ), + ) + .toList(), + ), + ); + }, + ), + ), ); } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 22906ce724..60c19a3051 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -1,11 +1,11 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:universal_platform/universal_platform.dart'; class FlowyMessageToast extends StatelessWidget { const FlowyMessageToast({required this.message, super.key}); @@ -71,7 +71,7 @@ void showSnackBarMessage( content: FlowyText( message, maxLines: 2, - fontSize: UniversalPlatform.isDesktop ? 14 : 12, + fontSize: PlatformExtension.isDesktop ? 14 : 12, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart index 7a14b6b1c5..1394758fe1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart @@ -28,25 +28,25 @@ class NotificationDialog extends StatefulWidget { class _NotificationDialogState extends State with SingleTickerProviderStateMixin { - late final TabController controller = TabController(length: 2, vsync: this); - final PopoverMutex mutex = PopoverMutex(); - final ReminderBloc reminderBloc = getIt(); + late final TabController _controller = TabController(length: 2, vsync: this); + final PopoverMutex _mutex = PopoverMutex(); + final ReminderBloc _reminderBloc = getIt(); @override void initState() { super.initState(); // Get all the past and upcoming reminders - reminderBloc.add(const ReminderEvent.started()); - controller.addListener(updateState); + _reminderBloc.add(const ReminderEvent.started()); + _controller.addListener(_updateState); } - void updateState() => setState(() {}); + void _updateState() => setState(() {}); @override void dispose() { - mutex.dispose(); - controller.removeListener(updateState); - controller.dispose(); + _mutex.close(); + _controller.removeListener(_updateState); + _controller.dispose(); super.dispose(); } @@ -54,7 +54,7 @@ class _NotificationDialogState extends State Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider.value(value: reminderBloc), + BlocProvider.value(value: _reminderBloc), BlocProvider( create: (_) => NotificationFilterBloc(), ), @@ -63,43 +63,41 @@ class _NotificationDialogState extends State builder: (context, filterState) => BlocBuilder( builder: (context, state) { - List pastReminders = - state.pastReminders.sortByScheduledAt(); - if (filterState.showUnreadsOnly) { - pastReminders = pastReminders.where((r) => !r.isRead).toList(); - } + final List pastReminders = state.pastReminders + .where((r) => filterState.showUnreadsOnly ? !r.isRead : true) + .sortByScheduledAt(); - final upcomingReminders = + final List upcomingReminders = state.upcomingReminders.sortByScheduledAt(); - final hasUnreads = pastReminders.any((r) => !r.isRead); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const NotificationHubTitle(), - NotificationTabBar(tabController: controller), + NotificationTabBar(tabController: _controller), Expanded( child: TabBarView( - controller: controller, + controller: _controller, children: [ NotificationsView( shownReminders: pastReminders, - reminderBloc: reminderBloc, + reminderBloc: _reminderBloc, views: widget.views, - onAction: onAction, + onDelete: _onDelete, + onAction: _onAction, onReadChanged: _onReadChanged, actionBar: InboxActionBar( - hasUnreads: hasUnreads, + hasUnreads: state.hasUnreads, showUnreadsOnly: filterState.showUnreadsOnly, ), ), NotificationsView( shownReminders: upcomingReminders, - reminderBloc: reminderBloc, + reminderBloc: _reminderBloc, views: widget.views, isUpcoming: true, - onAction: onAction, + onAction: _onAction, ), ], ), @@ -112,16 +110,20 @@ class _NotificationDialogState extends State ); } - void onAction(ReminderPB reminder, int? path, ViewPB? view) { - reminderBloc.add( + void _onAction(ReminderPB reminder, int? path, ViewPB? view) { + _reminderBloc.add( ReminderEvent.pressReminder(reminderId: reminder.id, path: path), ); widget.mutex.close(); } + void _onDelete(ReminderPB reminder) { + _reminderBloc.add(ReminderEvent.remove(reminderId: reminder.id)); + } + void _onReadChanged(ReminderPB reminder, bool isRead) { - reminderBloc.add( + _reminderBloc.add( ReminderEvent.update(ReminderUpdate(id: reminder.id, isRead: isRead)), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart index 87d839d71a..93b696c67f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/flowy_tab.dart @@ -1,7 +1,7 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; class FlowyTabItem extends StatelessWidget { const FlowyTabItem({ @@ -22,9 +22,9 @@ class FlowyTabItem extends StatelessWidget { @override Widget build(BuildContext context) { return Tab( - height: UniversalPlatform.isMobile ? mobileHeight : desktopHeight, + height: PlatformExtension.isMobile ? mobileHeight : desktopHeight, child: Padding( - padding: UniversalPlatform.isMobile ? mobilePadding : desktopPadding, + padding: PlatformExtension.isMobile ? mobilePadding : desktopPadding, child: FlowyText.regular( label, color: isSelected diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index 6f29c8e2aa..e28ebfb1c2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -1,81 +1,58 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.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/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/presentation/notifications/notification_dialog.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class NotificationButton extends StatefulWidget { +class NotificationButton extends StatelessWidget { const NotificationButton({ super.key, - this.isHover = false, }); - final bool isHover; - - @override - State createState() => _NotificationButtonState(); -} - -class _NotificationButtonState extends State { - final mutex = PopoverMutex(); - - @override - void initState() { - super.initState(); - getIt().add(const ReminderEvent.started()); - } - - @override - void dispose() { - mutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final views = context.watch().state.section.views; + final mutex = PopoverMutex(); return BlocProvider.value( value: getIt(), child: BlocBuilder( builder: (notificationSettingsContext, notificationSettingsState) { return BlocBuilder( - builder: (context, state) { - final hasUnreads = state.pastReminders.any((r) => !r.isRead); - return notificationSettingsState.isShowNotificationsIconEnabled - ? FlowyTooltip( - message: LocaleKeys.notificationHub_title.tr(), - child: AppFlowyPopover( - mutex: mutex, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: - const BoxConstraints(maxHeight: 500, maxWidth: 425), - windowPadding: EdgeInsets.zero, - margin: EdgeInsets.zero, - popupBuilder: (_) => - NotificationDialog(views: views, mutex: mutex), - child: SizedBox.square( - dimension: 24.0, - child: FlowyButton( - useIntrinsicWidth: true, - margin: EdgeInsets.zero, - text: _buildNotificationIcon( - context, - hasUnreads, - ), - ), + builder: (context, state) => notificationSettingsState + .isShowNotificationsIconEnabled + ? FlowyTooltip( + message: LocaleKeys.notificationHub_title.tr(), + child: AppFlowyPopover( + mutex: mutex, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: + const BoxConstraints(maxHeight: 500, maxWidth: 425), + windowPadding: EdgeInsets.zero, + margin: EdgeInsets.zero, + popupBuilder: (_) => + NotificationDialog(views: views, mutex: mutex), + child: SizedBox.square( + dimension: 24.0, + child: FlowyButton( + useIntrinsicWidth: true, + margin: EdgeInsets.zero, + text: + _buildNotificationIcon(context, state.hasUnreads), ), ), - ) - : const SizedBox.shrink(); - }, + ), + ) + : const SizedBox.shrink(), ); }, ), @@ -85,20 +62,22 @@ class _NotificationButtonState extends State { Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) { return Stack( children: [ - Center( + const Center( child: FlowySvg( FlowySvgs.notification_s, - color: - widget.isHover ? Theme.of(context).colorScheme.onSurface : null, opacity: 0.7, ), ), if (hasUnreads) - const Positioned( - top: 4, - right: 6, - child: NotificationRedDot( - size: 5, + Positioned( + bottom: 2, + right: 2, + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AFThemeExtension.of(context).warning, + ), + child: const SizedBox(height: 8, width: 8), ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart index 3d12a6afb7..0f69d0e679 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_item.dart @@ -1,11 +1,12 @@ 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:appflowy/plugins/document/presentation/editor_configuration.dart'; +import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_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_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flowy_infra/size.dart'; @@ -13,12 +14,11 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:universal_platform/universal_platform.dart'; class NotificationItem extends StatefulWidget { const NotificationItem({ super.key, - required this.reminder, + required this.reminderId, required this.title, required this.scheduled, required this.body, @@ -27,11 +27,12 @@ class NotificationItem extends StatefulWidget { this.includeTime = false, this.readOnly = false, this.onAction, + this.onDelete, this.onReadChanged, this.view, }); - final ReminderPB reminder; + final String reminderId; final String title; final Int64 scheduled; final String body; @@ -49,6 +50,7 @@ class NotificationItem extends StatefulWidget { final bool readOnly; final void Function(int? path)? onAction; + final VoidCallback? onDelete; final void Function(bool isRead)? onReadChanged; @override @@ -69,12 +71,6 @@ class _NotificationItemState extends State { infoString = _buildInfoString(); } - @override - void dispose() { - mutex.dispose(); - super.dispose(); - } - String _buildInfoString() { String scheduledString = _scheduledString(widget.scheduled, widget.includeTime); @@ -102,7 +98,7 @@ class _NotificationItemState extends State { child: DecoratedBox( decoration: BoxDecoration( border: Border( - bottom: UniversalPlatform.isMobile + bottom: PlatformExtension.isMobile ? BorderSide( color: AFThemeExtension.of(context).calloutBGColor, ) @@ -120,7 +116,7 @@ class _NotificationItemState extends State { ? null : Border( left: BorderSide( - width: UniversalPlatform.isMobile ? 4 : 2, + width: PlatformExtension.isMobile ? 4 : 2, color: Theme.of(context).colorScheme.primary, ), ), @@ -136,7 +132,7 @@ class _NotificationItemState extends State { FlowySvg( FlowySvgs.time_s, size: Size.square( - UniversalPlatform.isMobile ? 24 : 20, + PlatformExtension.isMobile ? 24 : 20, ), color: AFThemeExtension.of(context).textColor, ), @@ -148,13 +144,14 @@ class _NotificationItemState extends State { FlowyText.semibold( widget.title, fontSize: - UniversalPlatform.isMobile ? 16 : 14, + PlatformExtension.isMobile ? 16 : 14, color: AFThemeExtension.of(context).textColor, ), + // TODO(Xazin): Relative time FlowyText.regular( infoString, fontSize: - UniversalPlatform.isMobile ? 12 : 10, + PlatformExtension.isMobile ? 12 : 10, ), const VSpace(5), Container( @@ -166,7 +163,6 @@ class _NotificationItemState extends State { ), child: _NotificationContent( block: widget.block, - reminder: widget.reminder, body: widget.body, ), ), @@ -181,13 +177,14 @@ class _NotificationItemState extends State { ), ), ), - if (UniversalPlatform.isMobile && !widget.readOnly || + if (PlatformExtension.isMobile && !widget.readOnly || _isHovering && !widget.readOnly) Positioned( - right: UniversalPlatform.isMobile ? 8 : 4, - top: UniversalPlatform.isMobile ? 8 : 4, + right: PlatformExtension.isMobile ? 8 : 4, + top: PlatformExtension.isMobile ? 8 : 4, child: NotificationItemActions( isRead: widget.isRead, + onDelete: widget.onDelete, onReadChanged: widget.onReadChanged, ), ), @@ -211,12 +208,10 @@ class _NotificationItemState extends State { class _NotificationContent extends StatelessWidget { const _NotificationContent({ required this.body, - required this.reminder, required this.block, }); final String body; - final ReminderPB reminder; final Future? block; @override @@ -228,10 +223,29 @@ class _NotificationContent extends StatelessWidget { return FlowyText.regular(body, maxLines: 4); } - return IntrinsicHeight( - child: NotificationDocumentContent( - nodes: [snapshot.data!], - reminder: reminder, + final editorState = EditorState( + document: Document(root: snapshot.data!), + ); + + final styleCustomizer = EditorStyleCustomizer( + context: context, + padding: EdgeInsets.zero, + ); + + return Transform.scale( + scale: .9, + alignment: Alignment.centerLeft, + child: AppFlowyEditor( + editorState: editorState, + editorStyle: styleCustomizer.style(), + editable: false, + shrinkWrap: true, + blockComponentBuilders: getEditorBuilderMap( + context: context, + editorState: editorState, + styleCustomizer: styleCustomizer, + editable: false, + ), ), ); }, @@ -243,15 +257,17 @@ class NotificationItemActions extends StatelessWidget { const NotificationItemActions({ super.key, required this.isRead, + this.onDelete, this.onReadChanged, }); final bool isRead; + final VoidCallback? onDelete; final void Function(bool isRead)? onReadChanged; @override Widget build(BuildContext context) { - final double size = UniversalPlatform.isMobile ? 40.0 : 30.0; + final double size = PlatformExtension.isMobile ? 40.0 : 30.0; return Container( height: size, @@ -269,7 +285,6 @@ class NotificationItemActions extends StatelessWidget { FlowyIconButton( height: size, width: size, - radius: BorderRadius.circular(4), tooltipText: LocaleKeys.reminderNotification_tooltipMarkUnread.tr(), icon: const FlowySvg(FlowySvgs.restore_s), @@ -280,7 +295,6 @@ class NotificationItemActions extends StatelessWidget { FlowyIconButton( height: size, width: size, - radius: BorderRadius.circular(4), tooltipText: LocaleKeys.reminderNotification_tooltipMarkRead.tr(), iconColorOnHover: Theme.of(context).colorScheme.onSurface, @@ -288,6 +302,23 @@ class NotificationItemActions extends StatelessWidget { onPressed: () => onReadChanged?.call(true), ), ], + VerticalDivider( + width: 3, + thickness: 1, + indent: 2, + endIndent: 2, + color: PlatformExtension.isMobile + ? Theme.of(context).colorScheme.outline + : Theme.of(context).dividerColor, + ), + FlowyIconButton( + height: size, + width: size, + tooltipText: LocaleKeys.reminderNotification_tooltipDelete.tr(), + icon: const FlowySvg(FlowySvgs.delete_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + onPressed: onDelete, + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart index 0465256f60..42e5d50bfd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_view.dart @@ -26,6 +26,7 @@ class NotificationsView extends StatelessWidget { required this.views, this.isUpcoming = false, this.onAction, + this.onDelete, this.onReadChanged, this.actionBar, }); @@ -35,6 +36,7 @@ class NotificationsView extends StatelessWidget { final List views; final bool isUpcoming; final Function(ReminderPB reminder, int? path, ViewPB? view)? onAction; + final Function(ReminderPB reminder)? onDelete; final Function(ReminderPB reminder, bool isRead)? onReadChanged; final Widget? actionBar; @@ -74,7 +76,7 @@ class NotificationsView extends StatelessWidget { final view = views.findView(reminder.objectId); return NotificationItem( - reminder: reminder, + reminderId: reminder.id, key: ValueKey(reminder.id), title: reminder.title, scheduled: reminder.scheduledAt, @@ -85,6 +87,7 @@ class NotificationsView extends StatelessWidget { readOnly: isUpcoming, onReadChanged: (isRead) => onReadChanged?.call(reminder, isRead), + onDelete: () => onDelete?.call(reminder), onAction: (path) => onAction?.call(reminder, path, view), view: view, ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart deleted file mode 100644 index 2125ea4b66..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/about/app_version.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/version_checker/version_checker.dart'; -import 'package:appflowy/startup/tasks/device_info_task.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class SettingsAppVersion extends StatelessWidget { - const SettingsAppVersion({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return ApplicationInfo.isUpdateAvailable - ? const _UpdateAppSection() - : _buildIsUpToDate(context); - } - - Widget _buildIsUpToDate(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.settings_accountPage_isUpToDate.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - const VSpace(4), - Text( - LocaleKeys.settings_accountPage_officialVersion.tr( - namedArgs: { - 'version': ApplicationInfo.applicationVersion, - }, - ), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ), - ), - ], - ); - } -} - -class _UpdateAppSection extends StatelessWidget { - const _UpdateAppSection(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded(child: _buildDescription(context)), - _buildUpdateButton(), - ], - ); - } - - Widget _buildUpdateButton() { - return PrimaryRoundedButton( - text: LocaleKeys.autoUpdate_settingsUpdateButton.tr(), - margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), - fontWeight: FontWeight.w500, - radius: 8.0, - onTap: () { - Log.info('[AutoUpdater] Checking for updates'); - versionChecker.checkForUpdate(); - }, - ); - } - - Widget _buildDescription(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - _buildRedDot(), - const HSpace(6), - Flexible( - child: FlowyText.medium( - LocaleKeys.autoUpdate_settingsUpdateTitle.tr( - namedArgs: { - 'newVersion': ApplicationInfo.latestVersion, - }, - ), - figmaLineHeight: 17, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const VSpace(4), - _buildCurrentVersionAndLatestVersion(context), - ], - ); - } - - Widget _buildCurrentVersionAndLatestVersion(BuildContext context) { - return Row( - children: [ - Flexible( - child: Opacity( - opacity: 0.7, - child: FlowyText.regular( - LocaleKeys.autoUpdate_settingsUpdateDescription.tr( - namedArgs: { - 'currentVersion': ApplicationInfo.applicationVersion, - 'newVersion': ApplicationInfo.latestVersion, - }, - ), - fontSize: 12, - figmaLineHeight: 13, - overflow: TextOverflow.ellipsis, - ), - ), - ), - const HSpace(6), - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () { - afLaunchUrlString('https://www.appflowy.io/what-is-new'); - }, - child: FlowyText.regular( - LocaleKeys.autoUpdate_settingsUpdateWhatsNew.tr(), - decoration: TextDecoration.underline, - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - figmaLineHeight: 13, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ); - } - - Widget _buildRedDot() { - return Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - color: Color(0xFFFB006D), - shape: BoxShape.circle, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account.dart deleted file mode 100644 index 7b337d8d78..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'account_deletion.dart'; -export 'account_sign_in_out.dart'; -export 'account_user_profile.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart deleted file mode 100644 index 04d078ec0d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_deletion.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -const _acceptableConfirmTexts = [ - 'delete my account', - 'deletemyaccount', - 'DELETE MY ACCOUNT', - 'DELETEMYACCOUNT', -]; - -class AccountDeletionButton extends StatefulWidget { - const AccountDeletionButton({ - super.key, - }); - - @override - State createState() => _AccountDeletionButtonState(); -} - -class _AccountDeletionButtonState extends State { - final textEditingController = TextEditingController(); - final isCheckedNotifier = ValueNotifier(false); - - @override - void dispose() { - textEditingController.dispose(); - isCheckedNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.button_deleteAccount.tr(), - style: theme.textStyle.heading4.enhanced( - color: theme.textColorScheme.primary, - ), - ), - const VSpace(8), - Row( - children: [ - Expanded( - child: Text( - LocaleKeys.newSettings_myAccount_deleteAccount_description.tr(), - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ), - ), - ), - AFOutlinedTextButton.destructive( - text: LocaleKeys.button_deleteAccount.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.error, - weight: FontWeight.w400, - ), - onTap: () { - isCheckedNotifier.value = false; - textEditingController.clear(); - - showCancelAndDeleteDialog( - context: context, - title: - LocaleKeys.newSettings_myAccount_deleteAccount_title.tr(), - description: '', - builder: (_) => _AccountDeletionDialog( - controller: textEditingController, - isChecked: isCheckedNotifier, - ), - onDelete: () => deleteMyAccount( - context, - textEditingController.text.trim(), - isCheckedNotifier.value, - onSuccess: () { - context.popToHome(); - }, - ), - ); - }, - ), - ], - ), - ], - ); - } -} - -class _AccountDeletionDialog extends StatelessWidget { - const _AccountDeletionDialog({ - required this.controller, - required this.isChecked, - }); - - final TextEditingController controller; - final ValueNotifier isChecked; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - FlowyText.regular( - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint1.tr(), - fontSize: 14.0, - figmaLineHeight: 18.0, - maxLines: 2, - color: ConfirmPopupColor.descriptionColor(context), - ), - const VSpace(12.0), - FlowyTextField( - hintText: - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(), - controller: controller, - ), - const VSpace(16), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () => isChecked.value = !isChecked.value, - child: ValueListenableBuilder( - valueListenable: isChecked, - builder: (context, isChecked, _) { - return FlowySvg( - isChecked ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - size: const Size.square(16.0), - blendMode: isChecked ? null : BlendMode.srcIn, - ); - }, - ), - ), - const HSpace(6.0), - Expanded( - child: FlowyText.regular( - LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint2 - .tr(), - fontSize: 14.0, - figmaLineHeight: 16.0, - maxLines: 3, - color: ConfirmPopupColor.descriptionColor(context), - ), - ), - ], - ), - ], - ); - } -} - -bool _isConfirmTextValid(String text) { - // don't convert the text to lower case or upper case, - // just check if the text is in the list - return _acceptableConfirmTexts.contains(text) || - text == LocaleKeys.newSettings_myAccount_deleteAccount_confirmHint3.tr(); -} - -Future deleteMyAccount( - BuildContext context, - String confirmText, - bool isChecked, { - VoidCallback? onSuccess, - VoidCallback? onFailure, -}) async { - final bottomPadding = UniversalPlatform.isMobile - ? MediaQuery.of(context).viewInsets.bottom - : 0.0; - - if (!isChecked) { - showToastNotification( - type: ToastificationType.warning, - bottomPadding: bottomPadding, - message: LocaleKeys - .newSettings_myAccount_deleteAccount_checkToConfirmError - .tr(), - ); - return; - } - if (!context.mounted) { - return; - } - - if (confirmText.isEmpty || !_isConfirmTextValid(confirmText)) { - showToastNotification( - type: ToastificationType.warning, - bottomPadding: bottomPadding, - message: LocaleKeys - .newSettings_myAccount_deleteAccount_confirmTextValidationFailed - .tr(), - ); - return; - } - - final loading = Loading(context)..start(); - - await UserBackendService.deleteCurrentAccount().fold( - (s) { - Log.info('account deletion success'); - - loading.stop(); - showToastNotification( - message: LocaleKeys - .newSettings_myAccount_deleteAccount_deleteAccountSuccess - .tr(), - ); - - // delay 1 second to make sure the toast notification is shown - Future.delayed(const Duration(seconds: 1), () async { - onSuccess?.call(); - - // restart the application - await runAppFlowy(); - }); - }, - (f) { - Log.error('account deletion failed, error: $f'); - - loading.stop(); - showToastNotification( - type: ToastificationType.error, - bottomPadding: bottomPadding, - message: f.msg, - ); - - onFailure?.call(); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart deleted file mode 100644 index 78f1aaf16e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_sign_in_out.dart +++ /dev/null @@ -1,309 +0,0 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/user/application/prelude.dart'; -import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/continue_with/continue_with_email_and_password.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/change_password.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/setup_password.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.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 AccountSignInOutSection extends StatelessWidget { - const AccountSignInOutSection({ - super.key, - required this.userProfile, - required this.onAction, - this.signIn = true, - }); - - final UserProfilePB userProfile; - final VoidCallback onAction; - final bool signIn; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - children: [ - Text( - LocaleKeys.settings_accountPage_login_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - AccountSignInOutButton( - userProfile: userProfile, - onAction: onAction, - signIn: signIn, - ), - ], - ); - } -} - -class AccountSignInOutButton extends StatelessWidget { - const AccountSignInOutButton({ - super.key, - required this.userProfile, - required this.onAction, - this.signIn = true, - }); - - final UserProfilePB userProfile; - final VoidCallback onAction; - final bool signIn; - - @override - Widget build(BuildContext context) { - return AFFilledTextButton.primary( - text: signIn - ? LocaleKeys.settings_accountPage_login_loginLabel.tr() - : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - onTap: () => - signIn ? _showSignInDialog(context) : _showLogoutDialog(context), - ); - } - - void _showLogoutDialog(BuildContext context) { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), - description: LocaleKeys.settings_menu_logoutPrompt.tr(), - onConfirm: () async { - await getIt().signOut(); - onAction(); - }, - ); - } - - Future _showSignInDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (context) => BlocProvider( - create: (context) => getIt(), - child: const FlowyDialog( - constraints: BoxConstraints(maxHeight: 485, maxWidth: 375), - child: _SignInDialogContent(), - ), - ), - ); - } -} - -class ChangePasswordSection extends StatelessWidget { - const ChangePasswordSection({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - Text( - LocaleKeys.newSettings_myAccount_password_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - state.hasPassword - ? AFFilledTextButton.primary( - text: LocaleKeys - .newSettings_myAccount_password_changePassword - .tr(), - onTap: () => _showChangePasswordDialog(context), - ) - : AFFilledTextButton.primary( - text: LocaleKeys - .newSettings_myAccount_password_setupPassword - .tr(), - onTap: () => _showSetPasswordDialog(context), - ), - ], - ); - }, - ); - } - - Future _showChangePasswordDialog(BuildContext context) async { - final theme = AppFlowyTheme.of(context); - await showDialog( - context: context, - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: getIt(), - ), - ], - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - ), - child: ChangePasswordDialogContent( - userProfile: userProfile, - ), - ), - ), - ); - } - - Future _showSetPasswordDialog(BuildContext context) async { - await showDialog( - context: context, - builder: (_) => MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: getIt(), - ), - ], - child: Dialog( - child: SetupPasswordDialogContent( - userProfile: userProfile, - ), - ), - ), - ); - } -} - -class _SignInDialogContent extends StatelessWidget { - const _SignInDialogContent(); - - @override - Widget build(BuildContext context) { - return ScaffoldMessenger( - child: Scaffold( - body: Padding( - padding: const EdgeInsets.all(24), - child: SingleChildScrollView( - child: Column( - children: [ - const _DialogHeader(), - const _DialogTitle(), - const VSpace(16), - const ContinueWithEmailAndPassword(), - if (isAuthEnabled) ...[ - const VSpace(20), - const _OrDivider(), - const VSpace(10), - SettingThirdPartyLogin( - didLogin: () { - context.popToHome(); - }, - ), - ], - ], - ), - ), - ), - ), - ); - } -} - -class _DialogHeader extends StatelessWidget { - const _DialogHeader(); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildBackButton(context), - _buildCloseButton(context), - ], - ); - } - - Widget _buildBackButton(BuildContext context) { - return GestureDetector( - onTap: Navigator.of(context).pop, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Row( - children: [ - const FlowySvg(FlowySvgs.arrow_back_m, size: Size.square(24)), - const HSpace(8), - FlowyText.semibold(LocaleKeys.button_back.tr(), fontSize: 16), - ], - ), - ), - ); - } - - Widget _buildCloseButton(BuildContext context) { - return GestureDetector( - onTap: Navigator.of(context).pop, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: FlowySvg( - FlowySvgs.m_close_m, - size: const Size.square(20), - color: Theme.of(context).colorScheme.outline, - ), - ), - ); - } -} - -class _DialogTitle extends StatelessWidget { - const _DialogTitle(); - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: FlowyText.medium( - LocaleKeys.settings_accountPage_login_loginLabel.tr(), - fontSize: 22, - color: Theme.of(context).colorScheme.tertiary, - maxLines: null, - ), - ), - ], - ); - } -} - -class _OrDivider extends StatelessWidget { - const _OrDivider(); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const Flexible(child: Divider(thickness: 1)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: FlowyText.regular(LocaleKeys.signIn_or.tr()), - ), - const Flexible(child: Divider(thickness: 1)), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart deleted file mode 100644 index 62a6232c4a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/account_user_profile.dart +++ /dev/null @@ -1,185 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; -import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../../../../shared/icon_emoji_picker/tab.dart'; - -// Account name and account avatar -class AccountUserProfile extends StatefulWidget { - const AccountUserProfile({ - super.key, - required this.name, - required this.iconUrl, - this.onSave, - }); - - final String name; - final String iconUrl; - final void Function(String)? onSave; - - @override - State createState() => _AccountUserProfileState(); -} - -class _AccountUserProfileState extends State { - late final TextEditingController nameController = - TextEditingController(text: widget.name); - final FocusNode focusNode = FocusNode(); - bool isEditing = false; - bool isHovering = false; - - @override - void initState() { - super.initState(); - - focusNode - ..addListener(_handleFocusChange) - ..onKeyEvent = _handleKeyEvent; - } - - @override - void dispose() { - nameController.dispose(); - focusNode.removeListener(_handleFocusChange); - focusNode.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAvatar(), - const HSpace(16), - Flexible( - child: isEditing ? _buildEditingField() : _buildNameDisplay(), - ), - ], - ); - } - - Widget _buildAvatar() { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _showIconPickerDialog(context), - child: FlowyHover( - resetHoverOnRebuild: false, - onHover: (state) => setState(() => isHovering = state), - style: HoverStyle( - hoverColor: Colors.transparent, - borderRadius: BorderRadius.circular(100), - ), - child: FlowyTooltip( - message: - LocaleKeys.settings_accountPage_general_changeProfilePicture.tr(), - child: UserAvatar( - iconUrl: widget.iconUrl, - name: widget.name, - size: 48, - fontSize: 20, - isHovering: isHovering, - ), - ), - ), - ); - } - - Widget _buildNameDisplay() { - final theme = AppFlowyTheme.of(context); - return Padding( - padding: const EdgeInsets.only(top: 12), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - widget.name, - overflow: TextOverflow.ellipsis, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - ), - ), - const HSpace(4), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => setState(() => isEditing = true), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.toolbar_link_edit_m, - size: const Size.square(20), - ), - ), - ], - ), - ); - } - - Widget _buildEditingField() { - return SettingsInputField( - textController: nameController, - value: widget.name, - focusNode: focusNode..requestFocus(), - onCancel: () => setState(() => isEditing = false), - onSave: (_) => _saveChanges(), - ); - } - - Future _showIconPickerDialog(BuildContext context) { - return showDialog( - context: context, - builder: (dialogContext) => SimpleDialog( - children: [ - Container( - height: 380, - width: 360, - margin: const EdgeInsets.all(0), - child: FlowyIconEmojiPicker( - tabs: const [PickerTabType.emoji], - onSelectedEmoji: (r) { - context - .read() - .add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji)); - Navigator.of(dialogContext).pop(); - }, - ), - ), - ], - ), - ); - } - - void _handleFocusChange() { - if (!focusNode.hasFocus && isEditing && mounted) { - _saveChanges(); - } - } - - KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape && - isEditing && - mounted) { - setState(() => isEditing = false); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - void _saveChanges() { - widget.onSave?.call(nameController.text); - setState(() => isEditing = false); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart deleted file mode 100644 index d606f870ff..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/email/email_section.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; - -class SettingsEmailSection extends StatelessWidget { - const SettingsEmailSection({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.settings_accountPage_email_title.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - VSpace(theme.spacing.s), - Text( - userProfile.email, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart deleted file mode 100644 index 194254c869..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/change_password.dart +++ /dev/null @@ -1,330 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.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 ChangePasswordDialogContent extends StatefulWidget { - const ChangePasswordDialogContent({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - State createState() => - _ChangePasswordDialogContentState(); -} - -class _ChangePasswordDialogContentState - extends State { - final currentPasswordTextFieldKey = GlobalKey(); - final newPasswordTextFieldKey = GlobalKey(); - final confirmPasswordTextFieldKey = GlobalKey(); - - final currentPasswordController = TextEditingController(); - final newPasswordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); - - final iconSize = 20.0; - - @override - void dispose() { - currentPasswordController.dispose(); - newPasswordController.dispose(); - confirmPasswordController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocListener( - listener: _onPasswordStateChanged, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - constraints: const BoxConstraints(maxWidth: 400), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(context), - VSpace(theme.spacing.l), - ..._buildCurrentPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildNewPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildConfirmPasswordFields(context), - VSpace(theme.spacing.l), - _buildSubmitButton(context), - ], - ), - ), - ); - } - - Widget _buildTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Change password', - style: theme.textStyle.heading4.prominent( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), - ), - ), - ], - ); - } - - List _buildCurrentPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_currentPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: currentPasswordTextFieldKey, - controller: currentPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_enterYourCurrentPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - currentPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildNewPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_newPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: newPasswordTextFieldKey, - controller: newPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_enterYourNewPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - newPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildConfirmPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - LocaleKeys.newSettings_myAccount_password_confirmNewPassword.tr(), - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: confirmPasswordTextFieldKey, - controller: confirmPasswordController, - hintText: LocaleKeys - .newSettings_myAccount_password_hint_confirmYourNewPassword - .tr(), - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - Widget _buildSubmitButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AFOutlinedTextButton.normal( - text: LocaleKeys.button_cancel.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - weight: FontWeight.w400, - ), - onTap: () => Navigator.of(context).pop(), - ), - const HSpace(16), - AFFilledTextButton.primary( - text: LocaleKeys.button_save.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.onFill, - weight: FontWeight.w400, - ), - onTap: () => _save(context), - ), - ], - ); - } - - void _save(BuildContext context) async { - _resetError(); - - final currentPassword = currentPasswordController.text; - final newPassword = newPasswordController.text; - final confirmPassword = confirmPasswordController.text; - - if (newPassword.isEmpty) { - newPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsRequired - .tr(), - ); - return; - } - - if (confirmPassword.isEmpty) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_confirmPasswordIsRequired - .tr(), - ); - return; - } - - if (newPassword != confirmPassword) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_passwordsDoNotMatch - .tr(), - ); - return; - } - - if (newPassword == currentPassword) { - newPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsSameAsCurrent - .tr(), - ); - return; - } - - // all the verification passed, save the new password - context.read().add( - PasswordEvent.changePassword( - oldPassword: currentPassword, - newPassword: newPassword, - ), - ); - } - - void _resetError() { - currentPasswordTextFieldKey.currentState?.clearError(); - newPasswordTextFieldKey.currentState?.clearError(); - confirmPasswordTextFieldKey.currentState?.clearError(); - } - - void _onPasswordStateChanged(BuildContext context, PasswordState state) { - bool hasError = false; - String message = ''; - String description = ''; - - final changePasswordResult = state.changePasswordResult; - final setPasswordResult = state.setupPasswordResult; - - if (changePasswordResult != null) { - changePasswordResult.fold( - (success) { - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordUpdatedSuccessfully - .tr(); - }, - (error) { - hasError = true; - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordUpdatedFailed - .tr(); - description = error.msg; - }, - ); - } else if (setPasswordResult != null) { - setPasswordResult.fold( - (success) { - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordSetupSuccessfully - .tr(); - }, - (error) { - hasError = true; - message = LocaleKeys - .newSettings_myAccount_password_toast_passwordSetupFailed - .tr(); - description = error.msg; - }, - ); - } - - if (!state.isSubmitting && message.isNotEmpty) { - showToastNotification( - message: message, - description: description, - type: hasError ? ToastificationType.error : ToastificationType.success, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart deleted file mode 100644 index 5417b1a0eb..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class PasswordSuffixIcon extends StatelessWidget { - const PasswordSuffixIcon({ - super.key, - required this.isObscured, - required this.onTap, - }); - - final bool isObscured; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Padding( - padding: EdgeInsets.only(right: theme.spacing.m), - child: GestureDetector( - onTap: onTap, - child: FlowySvg( - isObscured ? FlowySvgs.show_s : FlowySvgs.hide_s, - color: theme.textColorScheme.secondary, - size: const Size.square(20), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart deleted file mode 100644 index 2fdfd8b934..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/account/password/setup_password.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/password/password_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/password/password_suffix_icon.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_ui/appflowy_ui.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 SetupPasswordDialogContent extends StatefulWidget { - const SetupPasswordDialogContent({ - super.key, - required this.userProfile, - }); - - final UserProfilePB userProfile; - - @override - State createState() => - _SetupPasswordDialogContentState(); -} - -class _SetupPasswordDialogContentState - extends State { - final passwordTextFieldKey = GlobalKey(); - final confirmPasswordTextFieldKey = GlobalKey(); - - final passwordController = TextEditingController(); - final confirmPasswordController = TextEditingController(); - - final iconSize = 20.0; - - @override - void dispose() { - passwordController.dispose(); - confirmPasswordController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return BlocListener( - listener: _onPasswordStateChanged, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - constraints: const BoxConstraints(maxWidth: 400), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(context), - VSpace(theme.spacing.l), - ..._buildPasswordFields(context), - VSpace(theme.spacing.l), - ..._buildConfirmPasswordFields(context), - VSpace(theme.spacing.l), - _buildSubmitButton(context), - ], - ), - ), - ); - } - - Widget _buildTitle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - LocaleKeys.newSettings_myAccount_password_setupPassword.tr(), - style: theme.textStyle.heading4.prominent( - color: theme.textColorScheme.primary, - ), - ), - const Spacer(), - AFGhostButton.normal( - size: AFButtonSize.s, - padding: EdgeInsets.all(theme.spacing.xs), - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) => FlowySvg( - FlowySvgs.password_close_m, - size: const Size.square(20), - ), - ), - ], - ); - } - - List _buildPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - 'Password', - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: passwordTextFieldKey, - controller: passwordController, - hintText: 'Enter your password', - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - passwordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - List _buildConfirmPasswordFields(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return [ - Text( - 'Confirm password', - style: theme.textStyle.caption.enhanced( - color: theme.textColorScheme.secondary, - ), - ), - VSpace(theme.spacing.xs), - AFTextField( - key: confirmPasswordTextFieldKey, - controller: confirmPasswordController, - hintText: 'Confirm your password', - keyboardType: TextInputType.visiblePassword, - obscureText: true, - suffixIconConstraints: BoxConstraints.tightFor( - width: iconSize + theme.spacing.m, - height: iconSize, - ), - suffixIconBuilder: (context, isObscured) => PasswordSuffixIcon( - isObscured: isObscured, - onTap: () { - confirmPasswordTextFieldKey.currentState?.syncObscured(!isObscured); - }, - ), - ), - ]; - } - - Widget _buildSubmitButton(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - AFOutlinedTextButton.normal( - text: 'Cancel', - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - weight: FontWeight.w400, - ), - onTap: () => Navigator.of(context).pop(), - ), - const HSpace(16), - AFFilledTextButton.primary( - text: 'Save', - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.onFill, - weight: FontWeight.w400, - ), - onTap: () => _save(context), - ), - ], - ); - } - - void _save(BuildContext context) async { - _resetError(); - - final password = passwordController.text; - final confirmPassword = confirmPasswordController.text; - - if (password.isEmpty) { - passwordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_newPasswordIsRequired - .tr(), - ); - return; - } - - if (confirmPassword.isEmpty) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_confirmPasswordIsRequired - .tr(), - ); - return; - } - - if (password != confirmPassword) { - confirmPasswordTextFieldKey.currentState?.syncError( - errorText: LocaleKeys - .newSettings_myAccount_password_error_passwordsDoNotMatch - .tr(), - ); - return; - } - - // all the verification passed, save the password - context.read().add( - PasswordEvent.setupPassword( - newPassword: password, - ), - ); - } - - void _resetError() { - passwordTextFieldKey.currentState?.clearError(); - confirmPasswordTextFieldKey.currentState?.clearError(); - } - - void _onPasswordStateChanged(BuildContext context, PasswordState state) { - bool hasError = false; - String message = ''; - String description = ''; - - final setPasswordResult = state.setupPasswordResult; - - if (setPasswordResult != null) { - setPasswordResult.fold( - (success) { - message = 'Password set'; - description = 'Your password has been set'; - }, - (error) { - hasError = true; - message = 'Failed to set password'; - description = error.msg; - }, - ); - } - - if (!state.isSubmitting && message.isNotEmpty) { - showToastNotification( - message: message, - description: description, - type: hasError ? ToastificationType.error : ToastificationType.success, - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart deleted file mode 100644 index 2cecb25b30..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/fix_data_widget.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; - -class FixDataWidget extends StatelessWidget { - const FixDataWidget({super.key}); - - @override - Widget build(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_manageDataPage_data_fixYourData.tr(), - children: [ - SingleSettingAction( - labelMaxLines: 4, - label: LocaleKeys.settings_manageDataPage_data_fixYourDataDescription - .tr(), - buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(), - onPressed: () { - WorkspaceDataManager.checkWorkspaceHealth(dryRun: true); - }, - ), - ], - ); - } -} - -class WorkspaceDataManager { - static Future checkWorkspaceHealth({ - required bool dryRun, - }) async { - try { - final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); - // get all the views in the workspace - final result = await ViewBackendService.getAllViews().getOrThrow(); - final allViews = result.items; - - // dump all the views in the workspace - dumpViews('all views', allViews); - - // get the workspace - final workspaces = allViews.where( - (e) => e.parentViewId == '' && e.id == currentWorkspace.id, - ); - dumpViews('workspaces', workspaces.toList()); - - if (workspaces.length != 1) { - Log.error('Failed to fix workspace: workspace not found'); - // there should be only one workspace - return; - } - - final workspace = workspaces.first; - - // check the health of the spaces - await checkSpaceHealth(workspace: workspace, allViews: allViews); - - // check the health of the views - await checkViewHealth(workspace: workspace, allViews: allViews); - - // add other checks here - // ... - } catch (e) { - Log.error('Failed to fix space relation: $e'); - } - } - - static Future checkSpaceHealth({ - required ViewPB workspace, - required List allViews, - bool dryRun = true, - }) async { - try { - final workspaceChildViews = - await ViewBackendService.getChildViews(viewId: workspace.id) - .getOrThrow(); - final workspaceChildViewIds = - workspaceChildViews.map((e) => e.id).toSet(); - final spaces = allViews.where((e) => e.isSpace).toList(); - - for (final space in spaces) { - // the space is the top level view, so its parent view id should be the workspace id - // and the workspace should have the space in its child views - if (space.parentViewId != workspace.id || - !workspaceChildViewIds.contains(space.id)) { - Log.info('found an issue: space is not in the workspace: $space'); - if (!dryRun) { - // move the space to the workspace if it is not in the workspace - await ViewBackendService.moveViewV2( - viewId: space.id, - newParentId: workspace.id, - prevViewId: null, - ); - } - workspaceChildViewIds.add(space.id); - } - } - } catch (e) { - Log.error('Failed to check space health: $e'); - } - } - - static Future> checkViewHealth({ - ViewPB? workspace, - List? allViews, - bool dryRun = true, - }) async { - // Views whose parent view does not have the view in its child views - final List unlistedChildViews = []; - // Views whose parent is not in allViews - final List orphanViews = []; - // Row pages - final List rowPageViews = []; - - try { - if (workspace == null || allViews == null) { - final currentWorkspace = - await UserBackendService.getCurrentWorkspace().getOrThrow(); - // get all the views in the workspace - final result = await ViewBackendService.getAllViews().getOrThrow(); - allViews = result.items; - workspace = allViews.firstWhereOrNull( - (e) => e.id == currentWorkspace.id, - ); - } - - for (final view in allViews) { - if (view.parentViewId == '') { - continue; - } - - final parentView = allViews.firstWhereOrNull( - (e) => e.id == view.parentViewId, - ); - - if (parentView == null) { - orphanViews.add(view); - continue; - } - - if (parentView.id == view.id) { - rowPageViews.add(view); - continue; - } - - final childViewsOfParent = - await ViewBackendService.getChildViews(viewId: parentView.id) - .getOrThrow(); - final result = childViewsOfParent.any((e) => e.id == view.id); - if (!result) { - unlistedChildViews.add(view); - } - } - } catch (e) { - Log.error('Failed to check space health: $e'); - return []; - } - - for (final view in unlistedChildViews) { - Log.info( - '[workspace] found an issue: view is not in the parent view\'s child views, view: ${view.toProto3Json()}}', - ); - } - - for (final view in orphanViews) { - Log.info('[workspace] orphanViews: ${view.toProto3Json()}'); - } - - for (final view in rowPageViews) { - Log.info('[workspace] rowPageViews: ${view.toProto3Json()}'); - } - - if (!dryRun && unlistedChildViews.isNotEmpty) { - Log.info( - '[workspace] start to fix ${unlistedChildViews.length} unlistedChildViews ...', - ); - for (final view in unlistedChildViews) { - // move the view to the parent view if it is not in the parent view's child views - Log.info( - '[workspace] move view: $view to its parent view ${view.parentViewId}', - ); - await ViewBackendService.moveViewV2( - viewId: view.id, - newParentId: view.parentViewId, - prevViewId: null, - ); - } - - Log.info('[workspace] end to fix unlistedChildViews'); - } - - if (unlistedChildViews.isEmpty && orphanViews.isEmpty) { - Log.info('[workspace] all views are healthy'); - } - - Log.info('[workspace] done checking view health'); - - return unlistedChildViews; - } - - static void dumpViews(String prefix, List views) { - for (int i = 0; i < views.length; i++) { - final view = views[i]; - Log.info('$prefix $i: $view)'); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart deleted file mode 100644 index 4992864f99..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart +++ /dev/null @@ -1,143 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:expandable/expandable.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import 'ollama_setting.dart'; -import 'plugin_status_indicator.dart'; - -class LocalAISetting extends StatefulWidget { - const LocalAISetting({super.key}); - - @override - State createState() => _LocalAISettingState(); -} - -class _LocalAISettingState extends State { - final expandableController = ExpandableController(initialExpanded: false); - - @override - void dispose() { - expandableController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => LocalAiPluginBloc(), - child: BlocConsumer( - listener: (context, state) { - expandableController.value = state.isEnabled; - }, - builder: (context, state) { - return ExpandablePanel( - controller: expandableController, - theme: ExpandableThemeData( - tapBodyToCollapse: false, - hasIcon: false, - tapBodyToExpand: false, - tapHeaderToExpand: false, - ), - header: LocalAiSettingHeader( - isEnabled: state.isEnabled, - ), - collapsed: const SizedBox.shrink(), - expanded: Padding( - padding: EdgeInsets.only(top: 12), - child: LocalAISettingPanel(), - ), - ); - }, - ), - ); - } -} - -class LocalAiSettingHeader extends StatelessWidget { - const LocalAiSettingHeader({ - super.key, - required this.isEnabled, - }); - - final bool isEnabled; - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_localAIToggleTitle.tr(), - ), - const VSpace(4), - FlowyText( - LocaleKeys.settings_aiPage_keys_localAIToggleSubTitle.tr(), - maxLines: 3, - fontSize: 12, - ), - ], - ), - ), - Toggle( - value: isEnabled, - onChanged: (value) { - _onToggleChanged(value, context); - }, - ), - ], - ); - } - - void _onToggleChanged(bool value, BuildContext context) { - if (value) { - context.read().add(const LocalAiPluginEvent.toggle()); - } else { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_aiPage_keys_disableLocalAITitle.tr(), - description: - LocaleKeys.settings_aiPage_keys_disableLocalAIDescription.tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () { - context - .read() - .add(const LocalAiPluginEvent.toggle()); - }, - ); - } - } -} - -class LocalAISettingPanel extends StatelessWidget { - const LocalAISettingPanel({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is! ReadyLocalAiPluginState) { - return const SizedBox.shrink(); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const LocalAIStatusIndicator(), - const VSpace(10), - OllamaSettingPage(), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart deleted file mode 100644 index e90c42444f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/local_settings_ai_view.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalSettingsAIView extends StatelessWidget { - const LocalSettingsAIView({ - super.key, - required this.userProfile, - required this.workspaceId, - }); - - final UserProfilePB userProfile; - final String workspaceId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SettingsAIBloc(userProfile, workspaceId) - ..add(const SettingsAIEvent.started()), - child: SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: "", - children: [ - const LocalAISetting(), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart deleted file mode 100644 index 7357c2951c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class AIModelSelection extends StatelessWidget { - const AIModelSelection({super.key}); - static const double height = 49; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => - previous.availableModels != current.availableModels, - builder: (context, state) { - final models = state.availableModels?.models; - if (models == null) { - return const SizedBox( - // Using same height as SettingsDropdown to avoid layout shift - height: height, - ); - } - - final localModels = models.where((model) => model.isLocal).toList(); - final cloudModels = models.where((model) => !model.isLocal).toList(); - final selectedModel = state.availableModels!.selectedModel; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - children: [ - Expanded( - child: FlowyText.medium( - LocaleKeys.settings_aiPage_keys_llmModelType.tr(), - overflow: TextOverflow.ellipsis, - ), - ), - Flexible( - child: SettingsDropdown( - key: const Key('_AIModelSelection'), - onChanged: (model) => context - .read() - .add(SettingsAIEvent.selectModel(model)), - selectedOption: selectedModel, - selectOptionCompare: (left, right) => - left?.name == right?.name, - options: [...localModels, ...cloudModels] - .map( - (model) => buildDropdownMenuEntry( - context, - value: model, - label: - model.isLocal ? "${model.i18n} 🔐" : model.i18n, - subLabel: model.desc, - maximumHeight: height, - ), - ) - .toList(), - ), - ), - ], - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart deleted file mode 100644 index 6f38043927..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/ollama_setting.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:appflowy/workspace/application/settings/ai/ollama_setting_bloc.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/style_widget/text_field.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class OllamaSettingPage extends StatelessWidget { - const OllamaSettingPage({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => - OllamaSettingBloc()..add(const OllamaSettingEvent.started()), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.inputItems != current.inputItems || - previous.isEdited != current.isEdited, - builder: (context, state) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - padding: EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 10, - children: [ - for (final item in state.inputItems) - _SettingItemWidget(item: item), - _SaveButton(isEdited: state.isEdited), - ], - ), - ); - }, - ), - ); - } -} - -class _SettingItemWidget extends StatelessWidget { - const _SettingItemWidget({required this.item}); - - final SettingItem item; - - @override - Widget build(BuildContext context) { - return Column( - key: ValueKey(item.content + item.settingType.title), - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - item.settingType.title, - fontSize: 12, - figmaLineHeight: 16, - ), - const VSpace(4), - SizedBox( - height: 32, - child: FlowyTextField( - autoFocus: false, - hintText: item.hintText, - text: item.content, - onChanged: (content) { - context.read().add( - OllamaSettingEvent.onEdit(content, item.settingType), - ); - }, - ), - ), - ], - ); - } -} - -class _SaveButton extends StatelessWidget { - const _SaveButton({required this.isEdited}); - - final bool isEdited; - - @override - Widget build(BuildContext context) { - return Align( - alignment: AlignmentDirectional.centerEnd, - child: FlowyTooltip( - message: isEdited ? null : 'No changes', - child: SizedBox( - child: FlowyButton( - text: FlowyText( - 'Apply', - figmaLineHeight: 20, - color: Theme.of(context).colorScheme.onPrimary, - ), - disable: !isEdited, - expandText: false, - margin: EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0), - backgroundColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primary.withAlpha(200), - onTap: () { - if (isEdited) { - context - .read() - .add(const OllamaSettingEvent.submit()); - } - }, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart deleted file mode 100644 index a280cf0644..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/plugin_status_indicator.dart +++ /dev/null @@ -1,363 +0,0 @@ -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/util/theme_extension.dart'; -import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LocalAIStatusIndicator extends StatelessWidget { - const LocalAIStatusIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return state.maybeWhen( - ready: (_, version, runningState, lackOfResource) { - if (lackOfResource != null) { - return _LackOfResource(resource: lackOfResource); - } - - return switch (runningState) { - RunningStatePB.ReadyToRun => const _ReadyToRun(), - RunningStatePB.Connecting || - RunningStatePB.Connected => - _Initializing(), - RunningStatePB.Running => _LocalAIRunning(version: version), - RunningStatePB.Stopped => const _RestartPluginButton(), - _ => const SizedBox.shrink(), - }; - }, - orElse: () => const SizedBox.shrink(), - ); - }, - ); - } -} - -class _ReadyToRun extends StatelessWidget { - const _ReadyToRun(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const SizedBox.square( - dimension: 20.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - strokeAlign: BorderSide.strokeAlignInside, - ), - ), - const HSpace(8.0), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIStart.tr(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -class _Initializing extends StatelessWidget { - const _Initializing(); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - children: [ - const SizedBox.square( - dimension: 20.0, - child: CircularProgressIndicator( - strokeWidth: 2.0, - strokeAlign: BorderSide.strokeAlignInside, - ), - ), - HSpace(8), - Expanded( - child: FlowyText( - LocaleKeys.settings_aiPage_keys_localAIInitializing.tr(), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ); - } -} - -class _RestartPluginButton extends StatelessWidget { - const _RestartPluginButton(); - - @override - Widget build(BuildContext context) { - final textStyle = - Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.toast_error_filled_s, - size: Size.square(20.0), - blendMode: null, - ), - const HSpace(8), - Expanded( - child: RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: - LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_restartLocalAI.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - context - .read() - .add(const LocalAiPluginEvent.restart()); - }, - ), - ], - ), - ), - ), - ], - ), - ); - } -} - -class _LocalAIRunning extends StatelessWidget { - const _LocalAIRunning({ - required this.version, - }); - - final String version; - - @override - Widget build(BuildContext context) { - final runningText = LocaleKeys.settings_aiPage_keys_localAIRunning.tr(); - final text = version.isEmpty ? runningText : "$runningText ($version)"; - - return Container( - decoration: const BoxDecoration( - color: Color(0xFFEDF7ED), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - const FlowySvg( - FlowySvgs.download_success_s, - color: Color(0xFF2E7D32), - ), - const HSpace(6), - Expanded( - child: FlowyText( - text, - color: const Color(0xFF1E4620), - maxLines: 3, - ), - ), - ], - ), - ); - } -} - -class _LackOfResource extends StatelessWidget { - const _LackOfResource({required this.resource}); - - final LackOfAIResourcePB resource; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0x80FFE7EE) - : const Color(0x80591734), - borderRadius: BorderRadius.all(Radius.circular(8.0)), - ), - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - FlowySvg( - FlowySvgs.toast_error_filled_s, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(8), - Expanded( - child: switch (resource.resourceType) { - LackOfAIResourceTypePB.PluginExecutableNotReady => - _buildNoLAI(context), - LackOfAIResourceTypePB.OllamaServerNotReady => - _buildNoOllama(context), - LackOfAIResourceTypePB.MissingModel => - _buildNoModel(context, resource.missingModelNames), - _ => const SizedBox.shrink(), - }, - ), - ], - ), - ); - } - - TextStyle? _textStyle(BuildContext context) { - return Theme.of(context).textTheme.bodyMedium?.copyWith(height: 1.5); - } - - Widget _buildNoLAI(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_laiNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: _textStyle(context)), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoOllama(BuildContext context) { - final textStyle = _textStyle(context); - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_ollamaNotReady.tr(), - style: textStyle, - ), - TextSpan(text: ' ', style: textStyle), - ..._downloadInstructions(textStyle), - ], - ), - ); - } - - Widget _buildNoModel(BuildContext context, List modelNames) { - final textStyle = _textStyle(context); - - return RichText( - maxLines: 3, - text: TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_modelsMissing.tr(), - style: textStyle, - ), - TextSpan( - text: modelNames.join(', '), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_instructions.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_downloadModel.tr(), - style: textStyle, - ), - ], - ), - ); - } - - List _downloadInstructions(TextStyle? textStyle) { - return [ - TextSpan( - text: LocaleKeys.settings_aiPage_keys_pleaseFollowThese.tr(), - style: textStyle, - ), - TextSpan( - text: ' ', - style: textStyle, - ), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_instructions.tr(), - style: textStyle?.copyWith( - fontWeight: FontWeight.w600, - decoration: TextDecoration.underline, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - afLaunchUrlString( - "https://appflowy.com/guide/appflowy-local-ai-ollama", - ); - }, - ), - TextSpan(text: ' ', style: textStyle), - TextSpan( - text: LocaleKeys.settings_aiPage_keys_installOllamaLai.tr(), - style: textStyle, - ), - ]; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart deleted file mode 100644 index c2e75ff2f2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_setting.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SettingsAIView extends StatelessWidget { - const SettingsAIView({ - super.key, - required this.userProfile, - required this.currentWorkspaceMemberRole, - required this.workspaceId, - }); - - final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; - final String workspaceId; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => SettingsAIBloc(userProfile, workspaceId) - ..add(const SettingsAIEvent.started()), - child: SettingsBody( - title: LocaleKeys.settings_aiPage_title.tr(), - description: LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(), - children: [ - const AIModelSelection(), - const _AISearchToggle(value: false), - const LocalAISetting(), - ], - ), - ); - } -} - -class _AISearchToggle extends StatelessWidget { - const _AISearchToggle({required this.value}); - - final bool value; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(), - ), - const Spacer(), - BlocBuilder( - builder: (context, state) { - if (state.aiSettings == null) { - return const Padding( - padding: EdgeInsets.only(top: 6), - child: SizedBox( - height: 26, - width: 26, - child: CircularProgressIndicator.adaptive(), - ), - ); - } else { - return Toggle( - value: state.enableSearchIndexing, - onChanged: (_) => context - .read() - .add(const SettingsAIEvent.toggleAISearch()), - ); - } - }, - ), - ], - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index d7afb03e87..e4df04647e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,16 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/application/prelude.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/magic_link_sign_in_buttons.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/about/app_version.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/account/email/email_section.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_third_party_login.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/auth.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsAccountView extends StatefulWidget { @@ -45,13 +57,12 @@ class _SettingsAccountViewState extends State { child: BlocBuilder( builder: (context, state) { return SettingsBody( - title: LocaleKeys.newSettings_myAccount_title.tr(), + title: LocaleKeys.settings_accountPage_title.tr(), children: [ - // user profile SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myProfile.tr(), + title: LocaleKeys.settings_accountPage_general_title.tr(), children: [ - AccountUserProfile( + UserProfileSetting( name: userName, iconUrl: state.userProfile.iconUrl, onSave: (newName) { @@ -61,67 +72,134 @@ class _SettingsAccountViewState extends State { setState(() => userName = newName); context .read() - .add(SettingsUserEvent.updateUserName(name: newName)); + .add(SettingsUserEvent.updateUserName(newName)); }, ), ], ), - // user email // Only show email if the user is authenticated and not using local auth if (isAuthEnabled && - state.userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( - title: LocaleKeys.newSettings_myAccount_myAccount.tr(), + title: LocaleKeys.settings_accountPage_email_title.tr(), children: [ - SettingsEmailSection( - userProfile: state.userProfile, - ), - ChangePasswordSection( - userProfile: state.userProfile, - ), - AccountSignInOutSection( - userProfile: state.userProfile, - onAction: state.userProfile.workspaceAuthType == - AuthTypePB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.workspaceAuthType == - AuthTypePB.Local, - ), + FlowyText.regular(state.userProfile.email), + // Enable when/if we need change email feature + // SingleSettingAction( + // label: state.userProfile.email, + // buttonLabel: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // onPressed: () => SettingsAlertDialog( + // title: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // confirmLabel: LocaleKeys.button_save.tr(), + // confirm: () { + // context.read().add( + // SettingsUserEvent.updateUserEmail( + // _emailController.text, + // ), + // ); + // Navigator.of(context).pop(); + // }, + // children: [ + // SettingsInputField( + // label: LocaleKeys.settings_accountPage_email_title + // .tr(), + // value: state.userProfile.email, + // hideActions: true, + // textController: _emailController, + // ), + // ], + // ).show(context), + // ), ], ), ], - if (isAuthEnabled && - state.userProfile.workspaceAuthType == AuthTypePB.Local) ...[ - SettingsCategory( - title: LocaleKeys.settings_accountPage_login_title.tr(), - children: [ - AccountSignInOutSection( - userProfile: state.userProfile, - onAction: state.userProfile.workspaceAuthType == - AuthTypePB.Local - ? widget.didLogin - : widget.didLogout, - signIn: state.userProfile.workspaceAuthType == - AuthTypePB.Local, - ), - ], - ), - ], - - // App version + /// Enable when we have change password feature and 2FA + // const SettingsCategorySpacer(), + // SettingsCategory( + // title: 'Account & security', + // children: [ + // SingleSettingAction( + // label: '**********', + // buttonLabel: 'Change password', + // onPressed: () {}, + // ), + // SingleSettingAction( + // label: '2-step authentication', + // buttonLabel: 'Enable 2FA', + // onPressed: () {}, + // ), + // ], + // ), SettingsCategory( - title: LocaleKeys.newSettings_myAccount_aboutAppFlowy.tr(), - children: const [ - SettingsAppVersion(), + title: LocaleKeys.settings_accountPage_keys_title.tr(), + children: [ + SettingsInputField( + label: + LocaleKeys.settings_accountPage_keys_openAILabel.tr(), + tooltip: + LocaleKeys.settings_accountPage_keys_openAITooltip.tr(), + placeholder: + LocaleKeys.settings_accountPage_keys_openAIHint.tr(), + value: state.userProfile.openaiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserOpenAIKey(key)), + ), + SettingsInputField( + label: LocaleKeys.settings_accountPage_keys_stabilityAILabel + .tr(), + tooltip: LocaleKeys + .settings_accountPage_keys_stabilityAITooltip + .tr(), + placeholder: LocaleKeys + .settings_accountPage_keys_stabilityAIHint + .tr(), + value: state.userProfile.stabilityAiKey, + obscureText: true, + onSave: (key) => context + .read() + .add(SettingsUserEvent.updateUserStabilityAIKey(key)), + ), + ], + ), + SettingsCategory( + title: LocaleKeys.settings_accountPage_login_title.tr(), + children: [ + SignInOutButton( + userProfile: state.userProfile, + onAction: + state.userProfile.authenticator == AuthenticatorPB.Local + ? widget.didLogin + : widget.didLogout, + signIn: state.userProfile.authenticator == + AuthenticatorPB.Local, + ), ], ), - // user deletion - if (widget.userProfile.workspaceAuthType == AuthTypePB.Server) - const AccountDeletionButton(), + /// Enable when we can delete accounts + // const SettingsCategorySpacer(), + // SettingsSubcategory( + // title: 'Delete account', + // children: [ + // SingleSettingAction( + // label: + // 'Permanently delete your account and remove access from all teamspaces.', + // labelMaxLines: 4, + // onPressed: () {}, + // buttonLabel: 'Delete my account', + // isDangerous: true, + // fontSize: 12, + // ), + // ], + // ), ], ); }, @@ -129,3 +207,306 @@ class _SettingsAccountViewState extends State { ); } } + +@visibleForTesting +class SignInOutButton extends StatelessWidget { + const SignInOutButton({ + super.key, + required this.userProfile, + required this.onAction, + this.signIn = true, + }); + + final UserProfilePB userProfile; + final VoidCallback onAction; + final bool signIn; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 48, + child: FlowyTextButton( + signIn + ? LocaleKeys.settings_accountPage_login_loginLabel.tr() + : LocaleKeys.settings_accountPage_login_logoutLabel.tr(), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () { + if (signIn) { + _showSignInDialog(context); + } else { + SettingsAlertDialog( + title: LocaleKeys.settings_accountPage_login_logoutLabel.tr(), + subtitle: switch (userProfile.encryptionType) { + EncryptionTypePB.Symmetric => + LocaleKeys.settings_menu_selfEncryptionLogoutPrompt.tr(), + _ => LocaleKeys.settings_menu_logoutPrompt.tr(), + }, + confirm: () async { + await getIt().signOut(); + onAction(); + }, + ).show(context); + } + }, + ), + ), + ], + ); + } + + Future _showSignInDialog(BuildContext context) async { + await showDialog( + context: context, + builder: (context) => BlocProvider( + create: (context) => getIt(), + child: FlowyDialog( + constraints: const BoxConstraints( + maxHeight: 485, + maxWidth: 375, + ), + child: ScaffoldMessenger( + child: Scaffold( + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Row( + children: [ + const FlowySvg( + FlowySvgs.arrow_back_m, + size: Size.square(24), + ), + const HSpace(8), + FlowyText.semibold( + LocaleKeys.button_back.tr(), + fontSize: 16, + ), + ], + ), + ), + ), + const Spacer(), + GestureDetector( + onTap: Navigator.of(context).pop, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: FlowySvg( + FlowySvgs.m_close_m, + size: const Size.square(20), + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: FlowyText.medium( + LocaleKeys.settings_accountPage_login_loginLabel + .tr(), + fontSize: 22, + color: Theme.of(context).colorScheme.tertiary, + maxLines: null, + ), + ), + ], + ), + const VSpace(16), + const SignInWithMagicLinkButtons(), + if (isAuthEnabled) ...[ + const VSpace(20), + Row( + children: [ + const Flexible(child: Divider(thickness: 1)), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + ), + child: FlowyText.regular( + LocaleKeys.signIn_or.tr(), + ), + ), + const Flexible(child: Divider(thickness: 1)), + ], + ), + const VSpace(10), + SettingThirdPartyLogin(didLogin: onAction), + ], + ], + ), + ), + ), + ), + ), + ), + ); + } +} + +@visibleForTesting +class UserProfileSetting extends StatefulWidget { + const UserProfileSetting({ + super.key, + required this.name, + required this.iconUrl, + this.onSave, + }); + + final String name; + final String iconUrl; + final void Function(String)? onSave; + + @override + State createState() => _UserProfileSettingState(); +} + +class _UserProfileSettingState extends State { + late final _nameController = TextEditingController(text: widget.name); + late final FocusNode focusNode; + bool isEditing = false; + bool isHovering = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing && + mounted) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!focusNode.hasFocus && isEditing && mounted) { + widget.onSave?.call(_nameController.text); + setState(() => isEditing = false); + } + }); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _showIconPickerDialog(context), + child: FlowyHover( + resetHoverOnRebuild: false, + onHover: (state) => setState(() => isHovering = state), + style: HoverStyle( + hoverColor: Colors.transparent, + borderRadius: BorderRadius.circular(100), + ), + child: FlowyTooltip( + message: LocaleKeys + .settings_accountPage_general_changeProfilePicture + .tr(), + child: UserAvatar( + iconUrl: widget.iconUrl, + name: widget.name, + size: 48, + fontSize: 20, + isHovering: isHovering, + ), + ), + ), + ), + const HSpace(16), + if (!isEditing) ...[ + Padding( + padding: const EdgeInsets.only(top: 12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: FlowyText.medium( + widget.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ], + ), + ), + ] else ...[ + Flexible( + child: SettingsInputField( + textController: _nameController, + value: widget.name, + focusNode: focusNode..requestFocus(), + onCancel: () => setState(() => isEditing = false), + onSave: (val) { + widget.onSave?.call(val); + setState(() => isEditing = false); + }, + ), + ), + ], + ], + ); + } + + Future _showIconPickerDialog(BuildContext context) { + return showDialog( + context: context, + builder: (dialogContext) => SimpleDialog( + children: [ + Container( + height: 380, + width: 360, + margin: const EdgeInsets.symmetric(horizontal: 12), + child: FlowyIconPicker( + onSelected: (r) { + context + .read() + .add(SettingsUserEvent.updateUserIcon(iconUrl: r.emoji)); + Navigator.of(dialogContext).pop(); + }, + ), + ), + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart index 77c1116319..d742451bb0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_billing_view.dart @@ -1,33 +1,22 @@ -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/util/int64_extension.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:flutter/material.dart'; + +import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/workspace/application/settings/billing/settings_billing_bloc.dart'; -import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart'; import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart'; import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:fixnum/fixnum.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../generated/locale_keys.g.dart'; -const _buttonsMinWidth = 100.0; - -class SettingsBillingView extends StatefulWidget { +class SettingsBillingView extends StatelessWidget { const SettingsBillingView({ super.key, required this.workspaceId, @@ -37,34 +26,12 @@ class SettingsBillingView extends StatefulWidget { final String workspaceId; final UserProfilePB user; - @override - State createState() => _SettingsBillingViewState(); -} - -class _SettingsBillingViewState extends State { - Loading? loadingIndicator; - RecurringIntervalPB? selectedInterval; - final ValueNotifier enablePlanChangeNotifier = ValueNotifier(false); - @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SettingsBillingBloc( - workspaceId: widget.workspaceId, - userId: widget.user.id, - )..add(const SettingsBillingEvent.started()), - child: BlocConsumer( - listenWhen: (previous, current) => - previous.mapOrNull(ready: (s) => s.isLoading) != - current.mapOrNull(ready: (s) => s.isLoading), - listener: (context, state) { - if (state.mapOrNull(ready: (s) => s.isLoading) == true) { - loadingIndicator = Loading(context)..start(); - } else { - loadingIndicator?.stop(); - loadingIndicator = null; - } - }, + create: (context) => SettingsBillingBloc(workspaceId: workspaceId) + ..add(const SettingsBillingEvent.started()), + child: BlocBuilder( builder: (context, state) { return state.map( initial: (_) => const SizedBox.shrink(), @@ -79,10 +46,9 @@ class _SettingsBillingViewState extends State { if (state.error != null) { return Padding( padding: const EdgeInsets.all(16), - child: Center( - child: AppFlowyErrorPage( - error: state.error!, - ), + child: FlowyErrorPage.message( + state.error!.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), ); } @@ -90,8 +56,8 @@ class _SettingsBillingViewState extends State { return ErrorWidget.withDetails(message: 'Something went wrong!'); }, ready: (state) { - final billingPortalEnabled = - state.subscriptionInfo.isBillingPortalEnabled; + final billingPortalEnabled = state.billingPortal != null && + state.billingPortal!.url.isNotEmpty; return SettingsBody( title: LocaleKeys.settings_billingPage_title.tr(), @@ -102,67 +68,27 @@ class _SettingsBillingViewState extends State { SingleSettingAction( onPressed: () => _openPricingDialog( context, - widget.workspaceId, - widget.user.id, - state.subscriptionInfo, + workspaceId, + user.id, + state.subscription, ), fontWeight: FontWeight.w500, - label: state.subscriptionInfo.label, + label: state.subscription.label, buttonLabel: LocaleKeys .settings_billingPage_plan_planButtonLabel .tr(), - minWidth: _buttonsMinWidth, ), if (billingPortalEnabled) SingleSettingAction( - onPressed: () { - SettingsAlertDialog( - title: LocaleKeys - .settings_billingPage_changePeriod - .tr(), - enableConfirmNotifier: enablePlanChangeNotifier, - children: [ - ChangePeriod( - plan: state.subscriptionInfo.planSubscription - .subscriptionPlan, - selectedInterval: state.subscriptionInfo - .planSubscription.interval, - onSelected: (interval) { - enablePlanChangeNotifier.value = interval != - state.subscriptionInfo.planSubscription - .interval; - selectedInterval = interval; - }, - ), - ], - confirm: () { - if (selectedInterval != - state.subscriptionInfo.planSubscription - .interval) { - context.read().add( - SettingsBillingEvent.updatePeriod( - plan: state - .subscriptionInfo - .planSubscription - .subscriptionPlan, - interval: selectedInterval!, - ), - ); - } - Navigator.of(context).pop(); - }, - ).show(context); - }, + onPressed: () => + afLaunchUrlString(state.billingPortal!.url), label: LocaleKeys .settings_billingPage_plan_billingPeriod .tr(), - description: state - .subscriptionInfo.planSubscription.interval.label, fontWeight: FontWeight.w500, buttonLabel: LocaleKeys .settings_billingPage_plan_periodButtonLabel .tr(), - minWidth: _buttonsMinWidth, ), ], ), @@ -173,11 +99,8 @@ class _SettingsBillingViewState extends State { .tr(), children: [ SingleSettingAction( - onPressed: () => context - .read() - .add( - const SettingsBillingEvent.openCustomerPortal(), - ), + onPressed: () => + afLaunchUrlString(state.billingPortal!.url), label: LocaleKeys .settings_billingPage_paymentDetails_methodLabel .tr(), @@ -185,32 +108,9 @@ class _SettingsBillingViewState extends State { buttonLabel: LocaleKeys .settings_billingPage_paymentDetails_methodButtonLabel .tr(), - minWidth: _buttonsMinWidth, ), ], ), - SettingsCategory( - title: LocaleKeys.settings_billingPage_addons_title.tr(), - children: [ - _AITile( - plan: SubscriptionPlanPB.AiMax, - label: LocaleKeys - .settings_billingPage_addons_aiMax_label - .tr(), - description: LocaleKeys - .settings_billingPage_addons_aiMax_description, - activeDescription: LocaleKeys - .settings_billingPage_addons_aiMax_activeDescription, - canceledDescription: LocaleKeys - .settings_billingPage_addons_aiMax_canceledDescription, - subscriptionInfo: - state.subscriptionInfo.addOns.firstWhereOrNull( - (a) => a.type == WorkspaceAddOnPBType.AddOnAiMax, - ), - ), - const SettingsDashedDivider(), - ], - ), ], ); }, @@ -224,355 +124,24 @@ class _SettingsBillingViewState extends State { BuildContext context, String workspaceId, Int64 userId, - WorkspaceSubscriptionInfoPB subscriptionInfo, + WorkspaceSubscriptionPB subscription, ) => showDialog( context: context, builder: (_) => BlocProvider( create: (_) => - SettingsPlanBloc(workspaceId: workspaceId, userId: widget.user.id) + SettingsPlanBloc(workspaceId: workspaceId, userId: user.id) ..add(const SettingsPlanEvent.started()), child: SettingsPlanComparisonDialog( workspaceId: workspaceId, - subscriptionInfo: subscriptionInfo, + subscription: subscription, ), ), ).then((didChangePlan) { - if (didChangePlan == true && context.mounted) { + if (didChangePlan == true) { context .read() .add(const SettingsBillingEvent.started()); } }); } - -class _AITile extends StatefulWidget { - const _AITile({ - required this.label, - required this.description, - required this.canceledDescription, - required this.activeDescription, - required this.plan, - this.subscriptionInfo, - }); - - final String label; - final String description; - final String canceledDescription; - final String activeDescription; - final SubscriptionPlanPB plan; - final WorkspaceAddOnPB? subscriptionInfo; - - @override - State<_AITile> createState() => _AITileState(); -} - -class _AITileState extends State<_AITile> { - RecurringIntervalPB? selectedInterval; - - final enableConfirmNotifier = ValueNotifier(false); - - @override - Widget build(BuildContext context) { - final isCanceled = widget.subscriptionInfo?.addOnSubscription.status == - WorkspaceSubscriptionStatusPB.Canceled; - - final dateFormat = context.read().state.dateFormat; - - return Column( - children: [ - SingleSettingAction( - label: widget.label, - description: widget.subscriptionInfo != null && isCanceled - ? widget.canceledDescription.tr( - args: [ - dateFormat.formatDate( - widget.subscriptionInfo!.addOnSubscription.endDate - .toDateTime(), - false, - ), - ], - ) - : widget.subscriptionInfo != null - ? widget.activeDescription.tr( - args: [ - dateFormat.formatDate( - widget.subscriptionInfo!.addOnSubscription.endDate - .toDateTime(), - false, - ), - ], - ) - : widget.description.tr(), - buttonLabel: widget.subscriptionInfo != null - ? isCanceled - ? LocaleKeys.settings_billingPage_addons_renewLabel.tr() - : LocaleKeys.settings_billingPage_addons_removeLabel.tr() - : LocaleKeys.settings_billingPage_addons_addLabel.tr(), - fontWeight: FontWeight.w500, - minWidth: _buttonsMinWidth, - onPressed: () async { - if (widget.subscriptionInfo != null) { - await showConfirmDialog( - context: context, - style: ConfirmPopupStyle.cancelAndOk, - title: LocaleKeys.settings_billingPage_addons_removeDialog_title - .tr(args: [widget.plan.label]).tr(), - description: LocaleKeys - .settings_billingPage_addons_removeDialog_description - .tr(namedArgs: {"plan": widget.plan.label.tr()}), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () => context - .read() - .add(SettingsBillingEvent.cancelSubscription(widget.plan)), - ); - } else { - // Add the addon - context - .read() - .add(SettingsBillingEvent.addSubscription(widget.plan)); - } - }, - ), - if (widget.subscriptionInfo != null) ...[ - const VSpace(10), - SingleSettingAction( - label: LocaleKeys.settings_billingPage_planPeriod.tr( - args: [ - widget - .subscriptionInfo!.addOnSubscription.subscriptionPlan.label, - ], - ), - description: - widget.subscriptionInfo!.addOnSubscription.interval.label, - buttonLabel: - LocaleKeys.settings_billingPage_plan_periodButtonLabel.tr(), - minWidth: _buttonsMinWidth, - onPressed: () { - enableConfirmNotifier.value = false; - SettingsAlertDialog( - title: LocaleKeys.settings_billingPage_changePeriod.tr(), - enableConfirmNotifier: enableConfirmNotifier, - children: [ - ChangePeriod( - plan: widget - .subscriptionInfo!.addOnSubscription.subscriptionPlan, - selectedInterval: - widget.subscriptionInfo!.addOnSubscription.interval, - onSelected: (interval) { - enableConfirmNotifier.value = interval != - widget.subscriptionInfo!.addOnSubscription.interval; - selectedInterval = interval; - }, - ), - ], - confirm: () { - if (selectedInterval != - widget.subscriptionInfo!.addOnSubscription.interval) { - context.read().add( - SettingsBillingEvent.updatePeriod( - plan: widget.subscriptionInfo!.addOnSubscription - .subscriptionPlan, - interval: selectedInterval!, - ), - ); - } - Navigator.of(context).pop(); - }, - ).show(context); - }, - ), - ], - ], - ); - } -} - -class ChangePeriod extends StatefulWidget { - const ChangePeriod({ - super.key, - required this.plan, - required this.selectedInterval, - required this.onSelected, - }); - - final SubscriptionPlanPB plan; - final RecurringIntervalPB selectedInterval; - final Function(RecurringIntervalPB interval) onSelected; - - @override - State createState() => _ChangePeriodState(); -} - -class _ChangePeriodState extends State { - RecurringIntervalPB? _selectedInterval; - - @override - void initState() { - super.initState(); - _selectedInterval = widget.selectedInterval; - } - - @override - void didChangeDependencies() { - _selectedInterval = widget.selectedInterval; - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - _PeriodSelector( - price: widget.plan.priceMonthBilling, - interval: RecurringIntervalPB.Month, - isSelected: _selectedInterval == RecurringIntervalPB.Month, - isCurrent: widget.selectedInterval == RecurringIntervalPB.Month, - onSelected: () { - widget.onSelected(RecurringIntervalPB.Month); - setState( - () => _selectedInterval = RecurringIntervalPB.Month, - ); - }, - ), - const VSpace(16), - _PeriodSelector( - price: widget.plan.priceAnnualBilling, - interval: RecurringIntervalPB.Year, - isSelected: _selectedInterval == RecurringIntervalPB.Year, - isCurrent: widget.selectedInterval == RecurringIntervalPB.Year, - onSelected: () { - widget.onSelected(RecurringIntervalPB.Year); - setState( - () => _selectedInterval = RecurringIntervalPB.Year, - ); - }, - ), - ], - ); - } -} - -class _PeriodSelector extends StatelessWidget { - const _PeriodSelector({ - required this.price, - required this.interval, - required this.onSelected, - required this.isSelected, - required this.isCurrent, - }); - - final String price; - final RecurringIntervalPB interval; - final VoidCallback onSelected; - final bool isSelected; - final bool isCurrent; - - @override - Widget build(BuildContext context) { - return Opacity( - opacity: isCurrent && !isSelected ? 0.7 : 1, - child: GestureDetector( - onTap: isCurrent ? null : onSelected, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - ), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - FlowyText( - interval.label, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - if (isCurrent) ...[ - const HSpace(8), - DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(6), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 1, - ), - child: FlowyText( - LocaleKeys - .settings_billingPage_currentPeriodBadge - .tr(), - fontSize: 11, - fontWeight: FontWeight.w500, - color: Colors.white, - ), - ), - ), - ], - ], - ), - const VSpace(8), - FlowyText( - price, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - const VSpace(4), - FlowyText( - interval.priceInfo, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ], - ), - const Spacer(), - if (!isCurrent && !isSelected || isSelected) ...[ - DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - width: 1.5, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - ), - ), - child: SizedBox( - height: 22, - width: 22, - child: Center( - child: SizedBox( - width: 10, - height: 10, - child: DecoratedBox( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - ), - ), - ), - ), - ), - ], - ], - ), - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index a2d911ea40..bde6c5bf31 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -1,18 +1,20 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + 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/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; -import 'package:appflowy/util/share_log_files.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/fix_data_widget.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; @@ -20,13 +22,14 @@ import 'package:appflowy/workspace/presentation/settings/widgets/files/settings_ import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:dotted_border/dotted_border.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -61,16 +64,15 @@ class SettingsManageDataView extends StatelessWidget { size: Size.square(20), ), label: LocaleKeys.settings_common_reset.tr(), - onPressed: () => showConfirmDialog( - context: context, - confirmLabel: LocaleKeys.button_confirm.tr(), + onPressed: () => SettingsAlertDialog( title: LocaleKeys .settings_manageDataPage_dataStorage_resetDialog_title .tr(), - description: LocaleKeys + subtitle: LocaleKeys .settings_manageDataPage_dataStorage_resetDialog_description .tr(), - onConfirm: () async { + implyLeading: true, + confirm: () async { final directory = await appFlowyApplicationDataDirectory(); final path = directory.path; @@ -84,8 +86,10 @@ class SettingsManageDataView extends StatelessWidget { .read() .resetDataStoragePathToApplicationDefault(); await runAppFlowy(isAnon: true); + + if (context.mounted) Navigator.of(context).pop(); }, - ), + ).show(context), ), ], children: state @@ -107,26 +111,9 @@ class SettingsManageDataView extends StatelessWidget { if (kDebugMode) ...[ SettingsCategory( title: LocaleKeys.settings_files_exportData.tr(), - children: const [ - SettingsExportFileWidget(), - FixDataWidget(), - ], + children: const [SettingsExportFileWidget()], ), ], - SettingsCategory( - title: LocaleKeys.workspace_errorActions_exportLogFiles.tr(), - children: [ - SingleSettingAction( - labelMaxLines: 4, - label: - LocaleKeys.workspace_errorActions_exportLogFiles.tr(), - buttonLabel: LocaleKeys.settings_files_export.tr(), - onPressed: () { - shareLogFiles(context); - }, - ), - ], - ), SettingsCategory( title: LocaleKeys.settings_manageDataPage_cache_title.tr(), children: [ @@ -137,37 +124,53 @@ class SettingsManageDataView extends StatelessWidget { buttonLabel: LocaleKeys.settings_manageDataPage_cache_title.tr(), onPressed: () { - showCancelAndConfirmDialog( - context: context, + SettingsAlertDialog( title: LocaleKeys .settings_manageDataPage_cache_dialog_title .tr(), - description: LocaleKeys + subtitle: LocaleKeys .settings_manageDataPage_cache_dialog_description .tr(), - confirmLabel: LocaleKeys.button_ok.tr(), - onConfirm: () async { - // clear all cache + confirm: () async { await getIt().clearAllCache(); - - // check the workspace and space health - await WorkspaceDataManager.checkViewHealth( - dryRun: false, - ); - if (context.mounted) { - showToastNotification( - message: LocaleKeys + showSnackBarMessage( + context, + LocaleKeys .settings_manageDataPage_cache_dialog_successHint .tr(), ); + Navigator.of(context).pop(); } }, - ); + ).show(context); }, ), ], ), + // Uncomment if we need to enable encryption + // if (userProfile.authenticator == AuthenticatorPB.Supabase) ...[ + // const SettingsCategorySpacer(), + // BlocProvider( + // create: (_) => EncryptSecretBloc(user: userProfile), + // child: SettingsCategory( + // title: LocaleKeys.settings_manageDataPage_encryption_title + // .tr(), + // tooltip: LocaleKeys + // .settings_manageDataPage_encryption_tooltip + // .tr(), + // description: userProfile.encryptionType == + // EncryptionTypePB.NoEncryption + // ? LocaleKeys + // .settings_manageDataPage_encryption_descriptionNoEncryption + // .tr() + // : LocaleKeys + // .settings_manageDataPage_encryption_descriptionEncrypted + // .tr(), + // children: [_EncryptDataSetting(userProfile: userProfile)], + // ), + // ), + // ], ], ); }, @@ -285,22 +288,65 @@ class _ImportDataFieldState extends State<_ImportDataField> { (_) => _showToast(LocaleKeys.settings_menu_importFailed.tr()), ), builder: (context, state) { - return SingleSettingAction( - label: - LocaleKeys.settings_manageDataPage_importData_description.tr(), - labelMaxLines: 2, - buttonLabel: - LocaleKeys.settings_manageDataPage_importData_action.tr(), - onPressed: () async { - final path = await getIt().getDirectoryPath(); - if (path == null || !context.mounted) { - return; - } + return DottedBorder( + radius: const Radius.circular(8), + dashPattern: const [2, 2], + borderType: BorderType.RRect, + color: Theme.of(context).colorScheme.primary, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // When dragging files are enabled + // FlowyText.regular('Drag file here or'), + // const VSpace(8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + height: 42, + child: FlowyTextButton( + LocaleKeys.settings_manageDataPage_importData_action + .tr(), + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + fontWeight: FontWeight.w600, + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () async { + final path = await getIt() + .getDirectoryPath(); + if (path == null || !context.mounted) { + return; + } - context - .read() - .add(SettingFileImportEvent.importAppFlowyDataFolder(path)); - }, + context.read().add( + SettingFileImportEvent + .importAppFlowyDataFolder( + path, + ), + ); + }, + ), + ), + ], + ), + const VSpace(8), + FlowyText.regular( + LocaleKeys.settings_manageDataPage_importData_description + .tr(), + // 'Supported filetypes:\nCSV, Notion, Text, and Markdown', + maxLines: 3, + lineHeight: 1.5, + textAlign: TextAlign.center, + ), + ], + ), + ), ); }, ), @@ -351,9 +397,9 @@ class _CurrentPathState extends State<_CurrentPath> { resetHoverOnRebuild: false, builder: (_, isHovering) => FlowyText.regular( widget.path, + lineHeight: 1.5, maxLines: 2, overflow: TextOverflow.ellipsis, - lineHeight: 1.5, decoration: isHovering ? TextDecoration.underline : null, color: isLM ? const Color(0xFF005483) @@ -421,13 +467,15 @@ class _DataPathActions extends StatelessWidget { children: [ SizedBox( height: 42, - child: PrimaryRoundedButton( - text: LocaleKeys.settings_manageDataPage_dataStorage_actions_change - .tr(), - margin: const EdgeInsets.symmetric(horizontal: 24), + child: FlowyTextButton( + LocaleKeys.settings_manageDataPage_dataStorage_actions_change.tr(), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), fontWeight: FontWeight.w600, - radius: 12.0, - onTap: () async { + radius: BorderRadius.circular(12), + fillColor: Theme.of(context).colorScheme.primary, + hoverColor: const Color(0xFF005483), + fontHoverColor: Colors.white, + onPressed: () async { final path = await getIt().getDirectoryPath(); if (!context.mounted || path == null || currentPath == path) { return; @@ -448,7 +496,7 @@ class _DataPathActions extends StatelessWidget { label: LocaleKeys.settings_manageDataPage_dataStorage_actions_open.tr(), icon: const FlowySvg(FlowySvgs.folder_m, size: Size.square(20)), - onPressed: () => afLaunchUri(Uri.file(currentPath)), + onPressed: () => afLaunchUrlString('file://$currentPath'), ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart index 420daa8698..f642a92b51 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart @@ -1,29 +1,27 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/shared/loading.dart'; -import 'package:appflowy/util/theme_extension.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart'; import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.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:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../generated/locale_keys.g.dart'; - class SettingsPlanComparisonDialog extends StatefulWidget { const SettingsPlanComparisonDialog({ super.key, required this.workspaceId, - required this.subscriptionInfo, + required this.subscription, }); final String workspaceId; - final WorkspaceSubscriptionInfoPB subscriptionInfo; + final WorkspaceSubscriptionPB subscription; @override State createState() => @@ -35,9 +33,7 @@ class _SettingsPlanComparisonDialogState final horizontalController = ScrollController(); final verticalController = ScrollController(); - late WorkspaceSubscriptionInfoPB currentInfo = widget.subscriptionInfo; - - Loading? loadingIndicator; + late WorkspaceSubscriptionPB currentSubscription = widget.subscription; @override void dispose() { @@ -48,9 +44,7 @@ class _SettingsPlanComparisonDialogState @override Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; - - return BlocConsumer( + return BlocListener( listener: (context, state) { final readyState = state.mapOrNull(ready: (state) => state); @@ -58,29 +52,38 @@ class _SettingsPlanComparisonDialogState return; } - if (readyState.downgradeProcessing) { - loadingIndicator = Loading(context)..start(); - } else { - loadingIndicator?.stop(); - loadingIndicator = null; - } - - if (readyState.successfulPlanUpgrade != null) { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_comparePlanDialog_paymentSuccess_title - .tr(args: [readyState.successfulPlanUpgrade!.label]), - description: LocaleKeys + if (readyState.showSuccessDialog) { + SettingsAlertDialog( + icon: Center( + child: SizedBox( + height: 90, + width: 90, + child: FlowySvg( + FlowySvgs.check_circle_s, + color: AFThemeExtension.of(context).success, + ), + ), + ), + title: + LocaleKeys.settings_comparePlanDialog_paymentSuccess_title.tr( + args: [readyState.subscription.label], + ), + subtitle: LocaleKeys .settings_comparePlanDialog_paymentSuccess_description - .tr(args: [readyState.successfulPlanUpgrade!.label]), + .tr( + args: [readyState.subscription.label], + ), + hideCancelButton: true, + confirm: Navigator.of(context).pop, confirmLabel: LocaleKeys.button_close.tr(), - onConfirm: () {}, - ); + ).show(context); } - setState(() => currentInfo = readyState.subscriptionInfo); + setState(() { + currentSubscription = readyState.subscription; + }); }, - builder: (context, state) => FlowyDialog( + child: FlowyDialog( constraints: const BoxConstraints(maxWidth: 784, minWidth: 674), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -93,26 +96,25 @@ class _SettingsPlanComparisonDialogState FlowyText.semibold( LocaleKeys.settings_comparePlanDialog_title.tr(), fontSize: 24, - color: AFThemeExtension.of(context).strongText, ), const Spacer(), GestureDetector( onTap: () => Navigator.of(context).pop( - currentInfo.plan != widget.subscriptionInfo.plan, + currentSubscription.subscriptionPlan != + widget.subscription.subscriptionPlan, ), child: MouseRegion( cursor: SystemMouseCursors.click, child: FlowySvg( FlowySvgs.m_close_m, size: const Size.square(20), - color: AFThemeExtension.of(context).strongText, + color: Theme.of(context).colorScheme.outline, ), ), ), ], ), ), - const VSpace(16), Flexible( child: SingleChildScrollView( controller: horizontalController, @@ -138,21 +140,19 @@ class _SettingsPlanComparisonDialogState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const VSpace(30), + const VSpace(26), SizedBox( - height: 116, + height: 100, child: FlowyText.semibold( LocaleKeys .settings_comparePlanDialog_planFeatures .tr(), fontSize: 24, maxLines: 2, - color: isLM - ? const Color(0xFF5C3699) - : const Color(0xFFE8E0FF), + color: const Color(0xFF5C3699), ), ), - const SizedBox(height: 116), + const SizedBox(height: 64), const SizedBox(height: 56), ..._planLabels.map( (e) => _ComparisonCell( @@ -172,52 +172,44 @@ class _SettingsPlanComparisonDialogState .tr(), price: LocaleKeys .settings_comparePlanDialog_freePlan_price - .tr( - args: [ - SubscriptionPlanPB.Free.priceMonthBilling, - ], - ), + .tr(), priceInfo: LocaleKeys .settings_comparePlanDialog_freePlan_priceInfo .tr(), cells: _freeLabels, - isCurrent: - currentInfo.plan == WorkspacePlanPB.FreePlan, - buttonType: WorkspacePlanPB.FreePlan.buttonTypeFor( - currentInfo.plan, - ), + isCurrent: currentSubscription.subscriptionPlan == + SubscriptionPlanPB.None, + canDowngrade: + currentSubscription.subscriptionPlan != + SubscriptionPlanPB.None, + currentCanceled: currentSubscription.hasCanceled, onSelected: () async { - if (currentInfo.plan == - WorkspacePlanPB.FreePlan || - currentInfo.isCanceled) { + if (currentSubscription.subscriptionPlan == + SubscriptionPlanPB.None || + currentSubscription.hasCanceled) { return; } - final reason = - await showCancelSurveyDialog(context); - if (reason == null || !context.mounted) { - return; - } - - await showConfirmDialog( - context: context, + await SettingsAlertDialog( title: LocaleKeys .settings_comparePlanDialog_downgradeDialog_title - .tr(args: [currentInfo.label]), - description: LocaleKeys + .tr(args: [currentSubscription.label]), + subtitle: LocaleKeys .settings_comparePlanDialog_downgradeDialog_description .tr(), + isDangerous: true, + confirm: () { + context.read().add( + const SettingsPlanEvent + .cancelSubscription(), + ); + + Navigator.of(context).pop(); + }, confirmLabel: LocaleKeys .settings_comparePlanDialog_downgradeDialog_downgradeLabel .tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () => - context.read().add( - SettingsPlanEvent.cancelSubscription( - reason: reason, - ), - ), - ); + ).show(context); }, ), _PlanTable( @@ -229,20 +221,16 @@ class _SettingsPlanComparisonDialogState .tr(), price: LocaleKeys .settings_comparePlanDialog_proPlan_price - .tr( - args: [SubscriptionPlanPB.Pro.priceAnnualBilling], - ), + .tr(), priceInfo: LocaleKeys .settings_comparePlanDialog_proPlan_priceInfo - .tr( - args: [SubscriptionPlanPB.Pro.priceMonthBilling], - ), + .tr(), cells: _proLabels, - isCurrent: - currentInfo.plan == WorkspacePlanPB.ProPlan, - buttonType: WorkspacePlanPB.ProPlan.buttonTypeFor( - currentInfo.plan, - ), + isCurrent: currentSubscription.subscriptionPlan == + SubscriptionPlanPB.Pro, + canUpgrade: currentSubscription.subscriptionPlan == + SubscriptionPlanPB.None, + currentCanceled: currentSubscription.hasCanceled, onSelected: () => context.read().add( const SettingsPlanEvent.addSubscription( @@ -264,35 +252,6 @@ class _SettingsPlanComparisonDialogState } } -enum _PlanButtonType { - none, - upgrade, - downgrade; - - bool get isDowngrade => this == downgrade; - bool get isUpgrade => this == upgrade; -} - -extension _ButtonTypeFrom on WorkspacePlanPB { - /// Returns the button type for the given plan, taking the - /// current plan as [other]. - /// - _PlanButtonType buttonTypeFor(WorkspacePlanPB other) { - /// Current plan, no action - if (this == other) { - return _PlanButtonType.none; - } - - // Free plan, can downgrade if not on the free plan - if (this == WorkspacePlanPB.FreePlan && other != WorkspacePlanPB.FreePlan) { - return _PlanButtonType.downgrade; - } - - // Else we can assume it's an upgrade - return _PlanButtonType.upgrade; - } -} - class _PlanTable extends StatelessWidget { const _PlanTable({ required this.title, @@ -302,7 +261,9 @@ class _PlanTable extends StatelessWidget { required this.cells, required this.isCurrent, required this.onSelected, - this.buttonType = _PlanButtonType.none, + this.canUpgrade = false, + this.canDowngrade = false, + this.currentCanceled = false, }); final String title; @@ -313,23 +274,24 @@ class _PlanTable extends StatelessWidget { final List<_CellItem> cells; final bool isCurrent; final VoidCallback onSelected; - final _PlanButtonType buttonType; + final bool canUpgrade; + final bool canDowngrade; + final bool currentCanceled; @override Widget build(BuildContext context) { - final highlightPlan = !isCurrent && buttonType == _PlanButtonType.upgrade; - final isLM = Theme.of(context).isLightMode; + final highlightPlan = !isCurrent && !canDowngrade && canUpgrade; return Container( - width: 215, + width: 210, decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), gradient: !highlightPlan ? null - : LinearGradient( + : const LinearGradient( colors: [ - isLM ? const Color(0xFF251D37) : const Color(0xFF7459AD), - isLM ? const Color(0xFF7547C0) : const Color(0xFFDDC8FF), + Color(0xFF251D37), + Color(0xFF7547C0), ], ), ), @@ -349,7 +311,6 @@ class _PlanTable extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (isCurrent) const _CurrentBadge(), - const VSpace(4), _Heading( title: title, description: description, @@ -359,29 +320,37 @@ class _PlanTable extends StatelessWidget { title: price, description: priceInfo, isPrimary: !highlightPlan, + height: 64, ), - if (buttonType == _PlanButtonType.none) ...[ - const SizedBox(height: 56), - ] else ...[ + if (canUpgrade || canDowngrade) ...[ Opacity( - opacity: 1, + opacity: canDowngrade && currentCanceled ? 0.5 : 1, child: Padding( padding: EdgeInsets.only( - left: 12 + (buttonType.isUpgrade ? 12 : 0), + left: 12 + (canUpgrade && !canDowngrade ? 12 : 0), ), child: _ActionButton( - label: buttonType.isUpgrade + label: canUpgrade && !canDowngrade ? LocaleKeys.settings_comparePlanDialog_actions_upgrade .tr() : LocaleKeys .settings_comparePlanDialog_actions_downgrade .tr(), - onPressed: onSelected, - isUpgrade: buttonType.isUpgrade, - useGradientBorder: buttonType.isUpgrade, + onPressed: !canUpgrade && canDowngrade && currentCanceled + ? null + : onSelected, + tooltip: !canUpgrade && canDowngrade && currentCanceled + ? LocaleKeys + .settings_comparePlanDialog_actions_downgradeDisabledTooltip + .tr() + : null, + isUpgrade: canUpgrade && !canDowngrade, + useGradientBorder: !isCurrent && canUpgrade, ), ), ), + ] else ...[ + const SizedBox(height: 56), ], ...cells.map( (cell) => _ComparisonCell( @@ -407,16 +376,14 @@ class _CurrentBadge extends StatelessWidget { height: 22, width: 72, decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0xFF4F3F5F) - : const Color(0xFFE8E0FF), + color: const Color(0xFF4F3F5F), borderRadius: BorderRadius.circular(4), ), child: Center( child: FlowyText.medium( LocaleKeys.settings_comparePlanDialog_current.tr(), fontSize: 12, - color: Theme.of(context).isLightMode ? Colors.white : Colors.black, + color: Colors.white, ), ), ); @@ -425,13 +392,13 @@ class _CurrentBadge extends StatelessWidget { class _ComparisonCell extends StatelessWidget { const _ComparisonCell({ - this.label, + required this.label, this.icon, this.tooltip, this.isHighlighted = false, }); - final String? label; + final String label; final FlowySvgData? icon; final String? tooltip; final bool isHighlighted; @@ -450,28 +417,22 @@ class _ComparisonCell extends StatelessWidget { ), ), child: Row( + mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ - FlowySvg( - icon!, - color: AFThemeExtension.of(context).strongText, - ), - ] else if (label != null) ...[ + FlowySvg(icon!), + ] else ...[ Expanded( child: FlowyText.medium( - label!, + label, lineHeight: 1.2, - color: AFThemeExtension.of(context).strongText, ), ), ], if (tooltip != null) FlowyTooltip( message: tooltip, - child: FlowySvg( - FlowySvgs.information_s, - color: AFThemeExtension.of(context).strongText, - ), + child: const FlowySvg(FlowySvgs.information_s), ), ], ), @@ -482,45 +443,59 @@ class _ComparisonCell extends StatelessWidget { class _ActionButton extends StatelessWidget { const _ActionButton({ required this.label, + this.tooltip, required this.onPressed, required this.isUpgrade, this.useGradientBorder = false, }); final String label; + final String? tooltip; final VoidCallback? onPressed; final bool isUpgrade; final bool useGradientBorder; @override Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; + final isLM = Theme.of(context).brightness == Brightness.light; + final gradientBorder = useGradientBorder && isLM; return SizedBox( height: 56, child: Row( children: [ - GestureDetector( - onTap: onPressed, - child: MouseRegion( - cursor: onPressed != null - ? SystemMouseCursors.click - : MouseCursor.defer, - child: _drawBorder( - context, - isLM: isLM, - isUpgrade: isUpgrade, - child: Container( - height: 36, - width: 148, - decoration: BoxDecoration( - color: useGradientBorder - ? Theme.of(context).cardColor - : Colors.transparent, - border: Border.all(color: Colors.transparent), - borderRadius: BorderRadius.circular(14), + FlowyTooltip( + message: tooltip, + child: GestureDetector( + onTap: onPressed, + child: MouseRegion( + cursor: onPressed != null + ? SystemMouseCursors.click + : MouseCursor.defer, + child: _drawGradientBorder( + isLM: isLM, + child: Container( + height: gradientBorder ? 36 : 40, + width: gradientBorder ? 148 : 152, + decoration: BoxDecoration( + color: useGradientBorder + ? Theme.of(context).cardColor + : Colors.transparent, + border: Border.all( + color: gradientBorder + ? Colors.transparent + : AFThemeExtension.of(context).textColor, + ), + borderRadius: + BorderRadius.circular(gradientBorder ? 14 : 16), + ), + child: Center( + child: _drawText( + label, + isLM, + ), + ), ), - child: Center(child: _drawText(label, isLM, isUpgrade)), ), ), ), @@ -530,13 +505,12 @@ class _ActionButton extends StatelessWidget { ); } - Widget _drawText(String text, bool isLM, bool isUpgrade) { + Widget _drawText(String text, bool isLM) { final child = FlowyText( text, fontSize: 14, lineHeight: 1.2, fontWeight: useGradientBorder ? FontWeight.w600 : FontWeight.w500, - color: isUpgrade ? const Color(0xFFC49BEC) : null, ); if (!useGradientBorder || !isLM) { @@ -548,32 +522,31 @@ class _ActionButton extends StatelessWidget { shaderCallback: (bounds) => const LinearGradient( transform: GradientRotation(-1.55), stops: [0.4, 1], - colors: [Color(0xFF251D37), Color(0xFF7547C0)], + colors: [ + Color(0xFF251D37), + Color(0xFF7547C0), + ], ).createShader(Rect.fromLTWH(0, 0, bounds.width, bounds.height)), child: child, ); } - Widget _drawBorder( - BuildContext context, { - required bool isLM, - required bool isUpgrade, - required Widget child, - }) { + Widget _drawGradientBorder({required bool isLM, required Widget child}) { + if (!useGradientBorder || !isLM) { + return child; + } + return Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( - gradient: isUpgrade - ? LinearGradient( - transform: const GradientRotation(-1.2), - stops: const [0.4, 1], - colors: [ - isLM ? const Color(0xFF251D37) : const Color(0xFF7459AD), - isLM ? const Color(0xFF7547C0) : const Color(0xFFDDC8FF), - ], - ) - : null, - border: isUpgrade ? null : Border.all(color: const Color(0xFF333333)), + gradient: const LinearGradient( + transform: GradientRotation(-1.2), + stops: [0.4, 1], + colors: [ + Color(0xFF251D37), + Color(0xFF7547C0), + ], + ), borderRadius: BorderRadius.circular(16), ), child: child, @@ -586,47 +559,38 @@ class _Heading extends StatelessWidget { required this.title, this.description, this.isPrimary = true, + this.height = 100, }); final String title; final String? description; final bool isPrimary; + final double height; @override Widget build(BuildContext context) { return SizedBox( - width: 185, - height: 116, + width: 175, + height: height, child: Padding( padding: EdgeInsets.only(left: 12 + (!isPrimary ? 12 : 0)), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - child: FlowyText.semibold( - title, - fontSize: 24, - overflow: TextOverflow.ellipsis, - color: isPrimary - ? AFThemeExtension.of(context).strongText - : Theme.of(context).isLightMode - ? const Color(0xFF5C3699) - : const Color(0xFFC49BEC), - ), - ), - ], + FlowyText.semibold( + title, + fontSize: 24, + color: isPrimary + ? AFThemeExtension.of(context).strongText + : const Color(0xFF5C3699), ), if (description != null && description!.isNotEmpty) ...[ const VSpace(4), - Flexible( - child: FlowyText.regular( - description!, - fontSize: 12, - maxLines: 5, - lineHeight: 1.5, - ), + FlowyText.regular( + description!, + fontSize: 12, + maxLines: 3, + lineHeight: 1.5, ), ], ], @@ -652,115 +616,88 @@ final _planLabels = [ ), _PlanItem( label: LocaleKeys.settings_comparePlanDialog_planLabels_itemThree.tr(), + tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipThree.tr(), ), _PlanItem( label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFour.tr(), + tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipFour.tr(), ), _PlanItem( label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFive.tr(), ), - _PlanItem( - label: - LocaleKeys.settings_comparePlanDialog_planLabels_intelligentSearch.tr(), - ), _PlanItem( label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSix.tr(), - tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(), ), _PlanItem( label: LocaleKeys.settings_comparePlanDialog_planLabels_itemSeven.tr(), - tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipSix.tr(), ), _PlanItem( - label: LocaleKeys.settings_comparePlanDialog_planLabels_itemFileUpload.tr(), - ), - _PlanItem( - label: - LocaleKeys.settings_comparePlanDialog_planLabels_customNamespace.tr(), - tooltip: LocaleKeys - .settings_comparePlanDialog_planLabels_customNamespaceTooltip - .tr(), + label: LocaleKeys.settings_comparePlanDialog_planLabels_itemEight.tr(), + tooltip: LocaleKeys.settings_comparePlanDialog_planLabels_tooltipEight.tr(), ), ]; class _CellItem { - const _CellItem({this.label, this.icon}); + const _CellItem(this.label, {this.icon}); - final String? label; + final String label; final FlowySvgData? icon; } final List<_CellItem> _freeLabels = [ _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(), + LocaleKeys.settings_comparePlanDialog_freeLabels_itemOne.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(), + LocaleKeys.settings_comparePlanDialog_freeLabels_itemTwo.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(), + LocaleKeys.settings_comparePlanDialog_freeLabels_itemThree.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(), + LocaleKeys.settings_comparePlanDialog_freeLabels_itemFour.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(), icon: FlowySvgs.check_m, ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFive.tr(), + LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(), icon: FlowySvgs.check_m, ), _CellItem( - label: - LocaleKeys.settings_comparePlanDialog_freeLabels_intelligentSearch.tr(), - icon: FlowySvgs.check_m, - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemSix.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemSeven.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_freeLabels_itemFileUpload.tr(), - ), - const _CellItem( - label: '', + LocaleKeys.settings_comparePlanDialog_freeLabels_itemEight.tr(), ), ]; final List<_CellItem> _proLabels = [ _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(), + LocaleKeys.settings_comparePlanDialog_proLabels_itemOne.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(), + LocaleKeys.settings_comparePlanDialog_proLabels_itemTwo.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(), + LocaleKeys.settings_comparePlanDialog_proLabels_itemThree.tr(), ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(), + LocaleKeys.settings_comparePlanDialog_proLabels_itemFour.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(), + ), + _CellItem( + LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(), icon: FlowySvgs.check_m, ), _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFive.tr(), + LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(), icon: FlowySvgs.check_m, ), _CellItem( - label: - LocaleKeys.settings_comparePlanDialog_proLabels_intelligentSearch.tr(), - icon: FlowySvgs.check_m, - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemSix.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemSeven.tr(), - ), - _CellItem( - label: LocaleKeys.settings_comparePlanDialog_proLabels_itemFileUpload.tr(), - ), - const _CellItem( - label: '', - icon: FlowySvgs.check_m, + LocaleKeys.settings_comparePlanDialog_proLabels_itemEight.tr(), ), ]; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart index 21896ead0e..9aa84d2e67 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_view.dart @@ -1,10 +1,8 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/colors.dart'; -import 'package:appflowy/shared/flowy_error_page.dart'; -import 'package:appflowy/shared/loading.dart'; import 'package:appflowy/util/int64_extension.dart'; -import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy/workspace/application/settings/plan/settings_plan_bloc.dart'; @@ -14,16 +12,16 @@ import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_com import 'package:appflowy/workspace/presentation/settings/shared/flowy_gradient_button.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SettingsPlanView extends StatefulWidget { +class SettingsPlanView extends StatelessWidget { const SettingsPlanView({ super.key, required this.workspaceId, @@ -33,32 +31,14 @@ class SettingsPlanView extends StatefulWidget { final String workspaceId; final UserProfilePB user; - @override - State createState() => _SettingsPlanViewState(); -} - -class _SettingsPlanViewState extends State { - Loading? loadingIndicator; - @override Widget build(BuildContext context) { return BlocProvider( create: (context) => SettingsPlanBloc( - workspaceId: widget.workspaceId, - userId: widget.user.id, + workspaceId: workspaceId, + userId: user.id, )..add(const SettingsPlanEvent.started()), - child: BlocConsumer( - listenWhen: (previous, current) => - previous.mapOrNull(ready: (s) => s.downgradeProcessing) != - current.mapOrNull(ready: (s) => s.downgradeProcessing), - listener: (context, state) { - if (state.mapOrNull(ready: (s) => s.downgradeProcessing) == true) { - loadingIndicator = Loading(context)..start(); - } else { - loadingIndicator?.stop(); - loadingIndicator = null; - } - }, + child: BlocBuilder( builder: (context, state) { return state.map( initial: (_) => const SizedBox.shrink(), @@ -73,69 +53,28 @@ class _SettingsPlanViewState extends State { if (state.error != null) { return Padding( padding: const EdgeInsets.all(16), - child: Center( - child: AppFlowyErrorPage( - error: state.error!, - ), + child: FlowyErrorPage.message( + state.error!.msg, + howToFix: LocaleKeys.errorDialog_howToFixFallback.tr(), ), ); } return ErrorWidget.withDetails(message: 'Something went wrong!'); }, - ready: (state) => SettingsBody( - autoSeparate: false, - title: LocaleKeys.settings_planPage_title.tr(), - children: [ - _PlanUsageSummary( - usage: state.workspaceUsage, - subscriptionInfo: state.subscriptionInfo, - ), - const VSpace(16), - _CurrentPlanBox(subscriptionInfo: state.subscriptionInfo), - const VSpace(16), - FlowyText( - LocaleKeys.settings_planPage_planUsage_addons_title.tr(), - fontSize: 18, - color: AFThemeExtension.of(context).strongText, - fontWeight: FontWeight.w600, - ), - const VSpace(8), - Row( - children: [ - Flexible( - child: _AddOnBox( - title: LocaleKeys - .settings_planPage_planUsage_addons_aiMax_title - .tr(), - description: LocaleKeys - .settings_planPage_planUsage_addons_aiMax_description - .tr(), - price: LocaleKeys - .settings_planPage_planUsage_addons_aiMax_price - .tr( - args: [SubscriptionPlanPB.AiMax.priceAnnualBilling], - ), - priceInfo: LocaleKeys - .settings_planPage_planUsage_addons_aiMax_priceInfo - .tr(), - recommend: '', - buttonText: state.subscriptionInfo.hasAIMax - ? LocaleKeys - .settings_planPage_planUsage_addons_activeLabel - .tr() - : LocaleKeys - .settings_planPage_planUsage_addons_addLabel - .tr(), - isActive: state.subscriptionInfo.hasAIMax, - plan: SubscriptionPlanPB.AiMax, - ), - ), - const HSpace(8), - ], - ), - ], - ), + ready: (state) { + return SettingsBody( + autoSeparate: false, + title: LocaleKeys.settings_planPage_title.tr(), + children: [ + _PlanUsageSummary( + usage: state.workspaceUsage, + subscription: state.subscription, + ), + _CurrentPlanBox(subscription: state.subscription), + ], + ); + }, ); }, ), @@ -143,29 +82,10 @@ class _SettingsPlanViewState extends State { } } -class _CurrentPlanBox extends StatefulWidget { - const _CurrentPlanBox({required this.subscriptionInfo}); +class _CurrentPlanBox extends StatelessWidget { + const _CurrentPlanBox({required this.subscription}); - final WorkspaceSubscriptionInfoPB subscriptionInfo; - - @override - State<_CurrentPlanBox> createState() => _CurrentPlanBoxState(); -} - -class _CurrentPlanBoxState extends State<_CurrentPlanBox> { - late SettingsPlanBloc planBloc; - - @override - void initState() { - super.initState(); - planBloc = context.read(); - } - - @override - void didChangeDependencies() { - planBloc = context.read(); - super.didChangeDependencies(); - } + final WorkspaceSubscriptionPB subscription; @override Widget build(BuildContext context) { @@ -178,67 +98,68 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> { border: Border.all(color: const Color(0xFFBDBDBD)), borderRadius: BorderRadius.circular(16), ), - child: Column( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - Expanded( - flex: 6, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const VSpace(4), - FlowyText.semibold( - widget.subscriptionInfo.label, - fontSize: 24, - color: AFThemeExtension.of(context).strongText, - ), - const VSpace(8), - FlowyText.regular( - widget.subscriptionInfo.info, - fontSize: 14, - color: AFThemeExtension.of(context).strongText, - maxLines: 3, - ), - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const VSpace(4), + FlowyText.semibold( + subscription.label, + fontSize: 24, + color: AFThemeExtension.of(context).strongText, ), - ), - Flexible( - flex: 5, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 220), - child: FlowyGradientButton( - label: LocaleKeys - .settings_planPage_planUsage_currentPlan_upgrade - .tr(), - onPressed: () => _openPricingDialog( - context, - context.read().workspaceId, - widget.subscriptionInfo, - ), - ), - ), - ], + const VSpace(8), + FlowyText.regular( + subscription.info, + fontSize: 16, + color: AFThemeExtension.of(context).strongText, + maxLines: 3, ), - ), - ], - ), - if (widget.subscriptionInfo.isCanceled) ...[ - const VSpace(12), - FlowyText( - LocaleKeys - .settings_planPage_planUsage_currentPlan_canceledInfo - .tr( - args: [_canceledDate(context)], - ), - maxLines: 5, - fontSize: 12, - color: Theme.of(context).colorScheme.error, + const VSpace(16), + FlowyGradientButton( + label: LocaleKeys + .settings_planPage_planUsage_currentPlan_upgrade + .tr(), + onPressed: () => _openPricingDialog( + context, + context.read().workspaceId, + subscription, + ), + ), + if (subscription.hasCanceled) ...[ + const VSpace(12), + FlowyText( + LocaleKeys + .settings_planPage_planUsage_currentPlan_canceledInfo + .tr( + args: [_canceledDate(context)], + ), + maxLines: 5, + fontSize: 12, + color: Theme.of(context).colorScheme.error, + ), + ], + ], ), - ], + ), + const HSpace(16), + Expanded( + child: SeparatedColumn( + separatorBuilder: () => const VSpace(4), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._getPros(subscription.subscriptionPlan).map( + (s) => _ProConItem(label: s), + ), + ..._getCons(subscription.subscriptionPlan).map( + (s) => _ProConItem(label: s, isPro: false), + ), + ], + ), + ), ], ), ), @@ -246,21 +167,14 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> { top: 0, left: 0, child: Container( - height: 30, - padding: const EdgeInsets.symmetric(horizontal: 24), - decoration: const BoxDecoration( - color: Color(0xFF4F3F5F), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(4), - bottomRight: Radius.circular(4), - ), - ), + height: 32, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: const BoxDecoration(color: Color(0xFF4F3F5F)), child: Center( child: FlowyText.semibold( LocaleKeys.settings_planPage_planUsage_currentPlan_bannerLabel .tr(), - fontSize: 14, + fontSize: 16, color: Colors.white, ), ), @@ -273,36 +187,113 @@ class _CurrentPlanBoxState extends State<_CurrentPlanBox> { String _canceledDate(BuildContext context) { final appearance = context.read().state; return appearance.dateFormat.formatDate( - widget.subscriptionInfo.planSubscription.endDate.toDateTime(), - false, + subscription.canceledAt.toDateTime(), + true, + appearance.timeFormat, ); } void _openPricingDialog( BuildContext context, String workspaceId, - WorkspaceSubscriptionInfoPB subscriptionInfo, + WorkspaceSubscriptionPB subscription, ) => showDialog( context: context, builder: (_) => BlocProvider.value( - value: planBloc, + value: context.read(), child: SettingsPlanComparisonDialog( workspaceId: workspaceId, - subscriptionInfo: subscriptionInfo, + subscription: subscription, ), ), ); + + List _getPros(SubscriptionPlanPB plan) => switch (plan) { + SubscriptionPlanPB.Pro => _proPros(), + _ => _freePros(), + }; + + List _getCons(SubscriptionPlanPB plan) => switch (plan) { + SubscriptionPlanPB.Pro => _proCons(), + _ => _freeCons(), + }; + + List _freePros() => [ + LocaleKeys.settings_planPage_planUsage_currentPlan_freeProOne.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeProTwo.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeProThree.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFour.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeProFive.tr(), + ]; + List _freeCons() => [ + LocaleKeys.settings_planPage_planUsage_currentPlan_freeConOne.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeConTwo.tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_freeConThree.tr(), + ]; + + List _proPros() => [ + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProOne + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProTwo + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProThree + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFour + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalProFive + .tr(), + ]; + List _proCons() => [ + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConOne + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConTwo + .tr(), + LocaleKeys.settings_planPage_planUsage_currentPlan_professionalConThree + .tr(), + ]; +} + +class _ProConItem extends StatelessWidget { + const _ProConItem({ + required this.label, + this.isPro = true, + }); + + final String label; + final bool isPro; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + SizedBox( + height: 24, + width: 24, + child: FlowySvg( + isPro ? FlowySvgs.check_m : FlowySvgs.close_s, + color: isPro ? null : const Color(0xFF900000), + ), + ), + const HSpace(4), + Flexible( + child: FlowyText.regular( + label, + fontSize: 12, + color: AFThemeExtension.of(context).strongText, + maxLines: 3, + ), + ), + ], + ); + } } class _PlanUsageSummary extends StatelessWidget { - const _PlanUsageSummary({ - required this.usage, - required this.subscriptionInfo, - }); + const _PlanUsageSummary({required this.usage, required this.subscription}); final WorkspaceUsagePB usage; - final WorkspaceSubscriptionInfoPB subscriptionInfo; + final WorkspaceSubscriptionPB subscription; @override Widget build(BuildContext context) { @@ -318,84 +309,56 @@ class _PlanUsageSummary extends StatelessWidget { ), const VSpace(16), Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: _UsageBox( title: LocaleKeys.settings_planPage_planUsage_storageLabel.tr(), - unlimitedLabel: LocaleKeys - .settings_planPage_planUsage_unlimitedStorageLabel - .tr(), - unlimited: usage.storageBytesUnlimited, label: LocaleKeys.settings_planPage_planUsage_storageUsage.tr( args: [ usage.currentBlobInGb, usage.totalBlobInGb, ], ), - value: usage.storageBytes.toInt() / - usage.storageBytesLimit.toInt(), + value: usage.totalBlobBytes.toInt() / + usage.totalBlobBytesLimit.toInt(), ), ), Expanded( child: _UsageBox( - title: - LocaleKeys.settings_planPage_planUsage_aiResponseLabel.tr(), - label: - LocaleKeys.settings_planPage_planUsage_aiResponseUsage.tr( + title: LocaleKeys.settings_planPage_planUsage_collaboratorsLabel + .tr(), + label: LocaleKeys.settings_planPage_planUsage_collaboratorsUsage + .tr( args: [ - usage.aiResponsesCount.toString(), - usage.aiResponsesCountLimit.toString(), + usage.memberCount.toString(), + usage.memberCountLimit.toString(), ], ), - unlimitedLabel: LocaleKeys - .settings_planPage_planUsage_unlimitedAILabel - .tr(), - unlimited: usage.aiResponsesUnlimited, - value: usage.aiResponsesCount.toInt() / - usage.aiResponsesCountLimit.toInt(), + value: + usage.memberCount.toInt() / usage.memberCountLimit.toInt(), ), ), ], ), const VSpace(16), - SeparatedColumn( + Column( crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const VSpace(4), children: [ - if (subscriptionInfo.plan == WorkspacePlanPB.FreePlan) ...[ - _ToggleMore( - value: false, - label: - LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(), - badgeLabel: - LocaleKeys.settings_planPage_planUsage_proBadge.tr(), - onTap: () async { - context.read().add( - const SettingsPlanEvent.addSubscription( - SubscriptionPlanPB.Pro, - ), - ); - await Future.delayed(const Duration(seconds: 2), () {}); - }, - ), - ], - if (!subscriptionInfo.hasAIMax && !usage.aiResponsesUnlimited) ...[ - _ToggleMore( - value: false, - label: LocaleKeys.settings_planPage_planUsage_aiMaxToggle.tr(), - badgeLabel: - LocaleKeys.settings_planPage_planUsage_aiMaxBadge.tr(), - onTap: () async { - context.read().add( - const SettingsPlanEvent.addSubscription( - SubscriptionPlanPB.AiMax, - ), - ); - await Future.delayed(const Duration(seconds: 2), () {}); - }, - ), - ], + _ToggleMore( + value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro, + label: + LocaleKeys.settings_planPage_planUsage_memberProToggle.tr(), + subscription: subscription, + badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(), + ), + const VSpace(8), + _ToggleMore( + value: subscription.subscriptionPlan == SubscriptionPlanPB.Pro, + label: + LocaleKeys.settings_planPage_planUsage_guestCollabToggle.tr(), + subscription: subscription, + badgeLabel: LocaleKeys.settings_planPage_planUsage_proBadge.tr(), + ), ], ), ], @@ -408,19 +371,12 @@ class _UsageBox extends StatelessWidget { required this.title, required this.label, required this.value, - required this.unlimitedLabel, - this.unlimited = false, }); final String title; final String label; final double value; - final String unlimitedLabel; - - // Replaces the progress bar with an unlimited badge - final bool unlimited; - @override Widget build(BuildContext context) { return Column( @@ -431,29 +387,7 @@ class _UsageBox extends StatelessWidget { fontSize: 11, color: AFThemeExtension.of(context).secondaryTextColor, ), - if (unlimited) ...[ - Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const FlowySvg( - FlowySvgs.check_circle_outlined_s, - color: Color(0xFF9C00FB), - ), - const HSpace(4), - FlowyText( - unlimitedLabel, - fontWeight: FontWeight.w500, - fontSize: 11, - ), - ], - ), - ), - ] else ...[ - const VSpace(4), - _PlanProgressIndicator(label: label, progress: value), - ], + _PlanProgressIndicator(label: label, progress: value), ], ); } @@ -463,14 +397,14 @@ class _ToggleMore extends StatefulWidget { const _ToggleMore({ required this.value, required this.label, + required this.subscription, this.badgeLabel, - this.onTap, }); final bool value; final String label; + final WorkspaceSubscriptionPB subscription; final String? badgeLabel; - final Future Function()? onTap; @override State<_ToggleMore> createState() => _ToggleMoreState(); @@ -481,22 +415,41 @@ class _ToggleMoreState extends State<_ToggleMore> { @override Widget build(BuildContext context) { + final isLM = Brightness.light == Theme.of(context).brightness; + final primaryColor = + isLM ? const Color(0xFF653E8C) : const Color(0xFFE8E2EE); + final secondaryColor = + isLM ? const Color(0xFFE8E2EE) : const Color(0xFF653E8C); + return Row( children: [ Toggle( value: toggleValue, padding: EdgeInsets.zero, - onChanged: (_) async { - if (widget.onTap == null || toggleValue) { - return; - } - + style: ToggleStyle.big, + onChanged: (_) { setState(() => toggleValue = !toggleValue); - await widget.onTap!(); - if (mounted) { - setState(() => toggleValue = !toggleValue); - } + Future.delayed(const Duration(milliseconds: 150), () { + if (mounted) { + showDialog( + context: context, + builder: (_) => BlocProvider.value( + value: context.read(), + child: SettingsPlanComparisonDialog( + workspaceId: context.read().workspaceId, + subscription: widget.subscription, + ), + ), + ).then((_) { + Future.delayed(const Duration(milliseconds: 150), () { + if (mounted) { + setState(() => toggleValue = !toggleValue); + } + }); + }); + } + }); }, ), const HSpace(10), @@ -511,11 +464,11 @@ class _ToggleMoreState extends State<_ToggleMore> { height: 26, child: Badge( padding: const EdgeInsets.symmetric(horizontal: 10), - backgroundColor: context.proSecondaryColor, + backgroundColor: secondaryColor, label: FlowyText.semibold( widget.badgeLabel!, fontSize: 12, - color: context.proPrimaryColor, + color: primaryColor, ), ), ), @@ -543,8 +496,8 @@ class _PlanProgressIndicator extends StatelessWidget { borderRadius: BorderRadius.circular(8), color: AFThemeExtension.of(context).progressBarBGColor, border: Border.all( - color: const Color(0xFFDDF1F7).withValues( - alpha: theme.brightness == Brightness.light ? 1 : 0.1, + color: const Color(0xFFDDF1F7).withOpacity( + theme.brightness == Brightness.light ? 1 : 0.1, ), ), ), @@ -556,9 +509,7 @@ class _PlanProgressIndicator extends StatelessWidget { widthFactor: progress, child: Container( decoration: BoxDecoration( - color: progress >= 1 - ? theme.colorScheme.error - : theme.colorScheme.primary, + color: theme.colorScheme.primary, ), ), ), @@ -579,135 +530,6 @@ class _PlanProgressIndicator extends StatelessWidget { } } -class _AddOnBox extends StatelessWidget { - const _AddOnBox({ - required this.title, - required this.description, - required this.price, - required this.priceInfo, - required this.recommend, - required this.buttonText, - required this.isActive, - required this.plan, - }); - - final String title; - final String description; - final String price; - final String priceInfo; - final String recommend; - final String buttonText; - final bool isActive; - final SubscriptionPlanPB plan; - - @override - Widget build(BuildContext context) { - final isLM = Theme.of(context).isLightMode; - - return Container( - height: 220, - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - border: Border.all( - color: isActive ? const Color(0xFFBDBDBD) : const Color(0xFF9C00FB), - ), - color: const Color(0xFFF7F8FC).withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - title, - fontSize: 14, - color: AFThemeExtension.of(context).strongText, - ), - const VSpace(10), - FlowyText.regular( - description, - fontSize: 12, - color: AFThemeExtension.of(context).secondaryTextColor, - maxLines: 4, - ), - const VSpace(10), - FlowyText( - price, - fontSize: 24, - color: AFThemeExtension.of(context).strongText, - ), - FlowyText( - priceInfo, - fontSize: 12, - color: AFThemeExtension.of(context).strongText, - ), - const VSpace(12), - Row( - children: [ - Expanded( - child: FlowyText( - recommend, - color: AFThemeExtension.of(context).secondaryTextColor, - fontSize: 11, - maxLines: 2, - ), - ), - ], - ), - const Spacer(), - Row( - children: [ - Expanded( - child: FlowyTextButton( - buttonText, - heading: isActive - ? const FlowySvg( - FlowySvgs.check_circle_outlined_s, - color: Color(0xFF9C00FB), - ) - : null, - mainAxisAlignment: MainAxisAlignment.center, - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 7), - fillColor: isActive - ? const Color(0xFFE8E2EE) - : isLM - ? Colors.transparent - : const Color(0xFF5C3699), - constraints: const BoxConstraints(minWidth: 115), - radius: Corners.s16Border, - hoverColor: isActive - ? const Color(0xFFE8E2EE) - : isLM - ? const Color(0xFF5C3699) - : const Color(0xFF4d3472), - fontColor: - isLM || isActive ? const Color(0xFF5C3699) : Colors.white, - fontHoverColor: - isActive ? const Color(0xFF5C3699) : Colors.white, - borderColor: isActive - ? const Color(0xFFE8E2EE) - : isLM - ? const Color(0xFF5C3699) - : const Color(0xFF4d3472), - fontSize: 12, - onPressed: isActive - ? null - : () => context - .read() - .add(SettingsPlanEvent.addSubscription(plan)), - ), - ), - ], - ), - ], - ), - ); - } -} - /// Uncomment if we need it in the future // class _DealBox extends StatelessWidget { // const _DealBox(); @@ -843,7 +665,7 @@ class _AddOnBox extends StatelessWidget { // children: [ // FlowyText.semibold( // LocaleKeys.settings_planPage_planUsage_aiCredit_price -// .tr(args: ['5\$]), +// .tr(), // fontSize: 24, // ), // FlowyText.medium( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart index 0d3716c7dc..27b0596d3a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_shortcuts_view.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; @@ -5,12 +8,9 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/strin import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/custom_paste_command.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/math_equation/math_equation_shortcut.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcuts.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/toggle/toggle_block_shortcut_event.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart'; @@ -22,10 +22,9 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:universal_platform/universal_platform.dart'; class SettingsShortcutsView extends StatefulWidget { const SettingsShortcutsView({super.key}); @@ -57,24 +56,21 @@ class _SettingsShortcutsViewState extends State { ), const HSpace(10), _ResetButton( - onReset: () { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_shortcutsPage_resetDialog_title - .tr(), - description: LocaleKeys - .settings_shortcutsPage_resetDialog_description - .tr(), - confirmLabel: LocaleKeys - .settings_shortcutsPage_resetDialog_buttonLabel - .tr(), - onConfirm: () { - context.read().resetToDefault(); - Navigator.of(context).pop(); - }, - style: ConfirmPopupStyle.cancelAndOk, - ); - }, + onReset: () => SettingsAlertDialog( + isDangerous: true, + title: LocaleKeys.settings_shortcutsPage_resetDialog_title + .tr(), + subtitle: LocaleKeys + .settings_shortcutsPage_resetDialog_description + .tr(), + confirmLabel: LocaleKeys + .settings_shortcutsPage_resetDialog_buttonLabel + .tr(), + confirm: () { + Navigator.of(context).pop(); + context.read().resetToDefault(); + }, + ).show(context), ), ], ), @@ -486,7 +482,7 @@ class KeyBadge extends StatelessWidget { borderRadius: Corners.s4Border, boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.25), + color: Colors.black.withOpacity(0.25), blurRadius: 1, offset: const Offset(0, 1), ), @@ -494,11 +490,14 @@ class KeyBadge extends StatelessWidget { ), child: Center( child: iconData != null - ? FlowySvg(iconData!, color: Colors.black) + ? FlowySvg( + iconData!, + color: AFThemeExtension.of(context).strongText, + ) : FlowyText.medium( keyLabel.toLowerCase(), fontSize: 12, - color: Colors.black, + color: AFThemeExtension.of(context).strongText, ), ), ); @@ -598,10 +597,6 @@ extension CommandLabel on CommandShortcutEvent { label = LocaleKeys.settings_shortcutsPage_keybindings_alignCenter.tr(); } else if (key == customTextRightAlignCommand.key) { label = LocaleKeys.settings_shortcutsPage_keybindings_alignRight.tr(); - } else if (key == insertInlineMathEquationCommand.key) { - label = LocaleKeys - .settings_shortcutsPage_keybindings_insertInlineMathEquation - .tr(); } else if (key == undoCommand.key) { label = LocaleKeys.settings_shortcutsPage_keybindings_undo.tr(); } else if (key == redoCommand.key) { @@ -617,7 +612,7 @@ extension CommandLabel on CommandShortcutEvent { label = LocaleKeys.settings_shortcutsPage_keybindings_deleteLeftSentence.tr(); } else if (key == deleteCommand.key) { - label = UniversalPlatform.isMacOS + label = PlatformExtension.isMacOS ? LocaleKeys.settings_shortcutsPage_keybindings_deleteMacOS.tr() : LocaleKeys.settings_shortcutsPage_keybindings_delete.tr(); } else if (key == deleteRightWordCommand.key) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 78ffd34eef..74ccac5558 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,9 +1,10 @@ import 'dart:async'; +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/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_style.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/util/font_family_extension.dart'; @@ -14,25 +15,25 @@ import 'package:appflowy/workspace/application/settings/date_time/date_format_ex import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart'; import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/language.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; @@ -43,7 +44,7 @@ import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -51,11 +52,11 @@ class SettingsWorkspaceView extends StatelessWidget { const SettingsWorkspaceView({ super.key, required this.userProfile, - this.currentWorkspaceMemberRole, + this.workspaceMember, }); final UserProfilePB userProfile; - final AFRolePB? currentWorkspaceMemberRole; + final WorkspaceMemberPB? workspaceMember; @override Widget build(BuildContext context) { @@ -85,20 +86,14 @@ class SettingsWorkspaceView extends StatelessWidget { return SettingsBody( title: LocaleKeys.settings_workspacePage_title.tr(), description: LocaleKeys.settings_workspacePage_description.tr(), - autoSeparate: false, children: [ // We don't allow changing workspace name/icon for local/offline - if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), - children: [ - _WorkspaceNameSetting( - currentWorkspaceMemberRole: currentWorkspaceMemberRole, - ), - ], + children: [_WorkspaceNameSetting(member: workspaceMember)], ), - const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceIcon_title .tr(), @@ -107,19 +102,16 @@ class SettingsWorkspaceView extends StatelessWidget { .tr(), children: [ _WorkspaceIconSetting( - enableEdit: currentWorkspaceMemberRole?.isOwner ?? false, + enableEdit: workspaceMember?.role.isOwner ?? false, workspace: state.workspace, ), ], ), - const SettingsCategorySpacer(), ], SettingsCategory( title: LocaleKeys.settings_workspacePage_appearance_title.tr(), children: const [AppearanceSelector()], ), - const VSpace(16), - // const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspacePage_theme_title.tr(), description: @@ -128,10 +120,8 @@ class SettingsWorkspaceView extends StatelessWidget { _ThemeDropdown(), _DocumentCursorColorSetting(), _DocumentSelectionColorSetting(), - DocumentPaddingSetting(), ], ), - const SettingsCategorySpacer(), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceFont_title.tr(), @@ -140,9 +130,7 @@ class SettingsWorkspaceView extends StatelessWidget { currentFont: context.read().state.font, ), - SettingsDashedDivider( - color: Theme.of(context).colorScheme.outline, - ), + const SettingsDashedDivider(), SettingsCategory( title: LocaleKeys.settings_workspacePage_textDirection_title .tr(), @@ -153,14 +141,11 @@ class SettingsWorkspaceView extends StatelessWidget { ), ], ), - const VSpace(16), SettingsCategory( title: LocaleKeys.settings_workspacePage_layoutDirection_title .tr(), children: const [_LayoutDirectionSelect()], ), - const SettingsCategorySpacer(), - SettingsCategory( title: LocaleKeys.settings_workspacePage_dateTime_title.tr(), children: [ @@ -172,45 +157,43 @@ class SettingsWorkspaceView extends StatelessWidget { const _DateFormatDropdown(), ], ), - const SettingsCategorySpacer(), - SettingsCategory( title: LocaleKeys.settings_workspacePage_language_title.tr(), children: const [LanguageDropdown()], ), - const SettingsCategorySpacer(), - - if (userProfile.workspaceAuthType != AuthTypePB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), fontSize: 16, fontWeight: FontWeight.w600, - onPressed: () => showConfirmDialog( - context: context, - title: currentWorkspaceMemberRole?.isOwner ?? false + onPressed: () => SettingsAlertDialog( + title: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_deleteWorkspacePrompt_title .tr() : LocaleKeys .settings_workspacePage_leaveWorkspacePrompt_title .tr(), - description: currentWorkspaceMemberRole?.isOwner ?? false + subtitle: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_deleteWorkspacePrompt_content .tr() : LocaleKeys .settings_workspacePage_leaveWorkspacePrompt_content .tr(), - style: ConfirmPopupStyle.cancelAndOk, - onConfirm: () => context.read().add( - currentWorkspaceMemberRole?.isOwner ?? false - ? const WorkspaceSettingsEvent.deleteWorkspace() - : const WorkspaceSettingsEvent.leaveWorkspace(), - ), - ), - buttonType: SingleSettingsButtonType.danger, - buttonLabel: currentWorkspaceMemberRole?.isOwner ?? false + isDangerous: true, + confirm: () { + context.read().add( + workspaceMember?.role.isOwner ?? false + ? const WorkspaceSettingsEvent.deleteWorkspace() + : const WorkspaceSettingsEvent.leaveWorkspace(), + ); + Navigator.of(context).pop(); + }, + ).show(context), + isDangerous: true, + buttonLabel: workspaceMember?.role.isOwner ?? false ? LocaleKeys .settings_workspacePage_manageWorkspace_deleteWorkspace .tr() @@ -228,11 +211,9 @@ class SettingsWorkspaceView extends StatelessWidget { } class _WorkspaceNameSetting extends StatefulWidget { - const _WorkspaceNameSetting({ - this.currentWorkspaceMemberRole, - }); + const _WorkspaceNameSetting({this.member}); - final AFRolePB? currentWorkspaceMemberRole; + final WorkspaceMemberPB? member; @override State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); @@ -260,8 +241,7 @@ class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { } }, builder: (_, state) { - if (widget.currentWorkspaceMemberRole == null || - !widget.currentWorkspaceMemberRole!.isOwner) { + if (widget.member == null || !widget.member!.role.isOwner) { return Padding( padding: const EdgeInsets.symmetric(vertical: 2.5), child: FlowyText.regular( @@ -351,18 +331,19 @@ class _WorkspaceIconSetting extends StatelessWidget { ); } - return SizedBox( + return Container( height: 64, width: 64, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), child: Padding( padding: const EdgeInsets.all(1), child: WorkspaceIcon( workspace: workspace!, - iconSize: 36, - emojiSize: 24.0, - fontSize: 24.0, - figmaLineHeight: 26.0, - borderRadius: 18.0, + iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, + fontSize: 16.0, enableEdit: true, onSelected: (r) => context .read() @@ -381,7 +362,7 @@ class TextDirectionSelect extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final selectedItem = state.textDirection; + final selectedItem = state.textDirection ?? AppFlowyTextDirection.ltr; return SettingsRadioSelect( onChanged: (item) { @@ -436,13 +417,14 @@ class EnableRTLItemsSwitcher extends StatelessWidget { ), const HSpace(16), Toggle( + style: ToggleStyle.big, value: context .watch() .state .enableRtlToolbarItems, onChanged: (value) => context .read() - .setEnableRTLToolbarItems(value), + .setEnableRTLToolbarItems(!value), ), ], ); @@ -582,13 +564,14 @@ class _TimeFormatSwitcher extends StatelessWidget { ), const HSpace(16), Toggle( + style: ToggleStyle.big, value: context.watch().state.timeFormat == UserTimeFormatPB.TwentyFourHour, onChanged: (value) => context.read().setTimeFormat( value - ? UserTimeFormatPB.TwentyFourHour - : UserTimeFormatPB.TwelveHour, + ? UserTimeFormatPB.TwelveHour + : UserTimeFormatPB.TwentyFourHour, ), ), ], @@ -631,7 +614,7 @@ class _ThemeDropdown extends StatelessWidget { ), ), ).then((val) { - if (val != null && context.mounted) { + if (val != null) { showSnackBarMessage( context, LocaleKeys.settings_appearance_themeUpload_uploadSuccess @@ -885,7 +868,16 @@ class _FontSelectorDropdownState extends State<_FontSelectorDropdown> { maxHeight: 150, maxWidth: constraints.maxWidth - 90, ), - borderRadius: const BorderRadius.all(Radius.circular(4.0)), + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: const BorderRadius.all(Radius.circular(4.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.10), + blurRadius: 6, + ), + ], + ), popupBuilder: (_) => _FontListPopup( currentFont: appearance.font, scrollController: _scrollController, @@ -1075,29 +1067,25 @@ class _FontListPopupState extends State<_FontListPopup> { child: ListView.separated( shrinkWrap: _filteredOptions.length < 10, controller: widget.scrollController, - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 6), itemCount: _filteredOptions.length, - separatorBuilder: (_, __) => const VSpace(6), + separatorBuilder: (_, __) => const VSpace(4), itemBuilder: (context, index) { final font = _filteredOptions[index]; final isSelected = widget.currentFont == font; return SizedBox( - height: 29, + height: 28, child: ListTile( - minVerticalPadding: 0, selected: isSelected, dense: true, hoverColor: Theme.of(context) .colorScheme .onSurface - .withValues(alpha: 0.12), - selectedTileColor: Theme.of(context) - .colorScheme - .primary - .withValues(alpha: 0.12), - contentPadding: - const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - minTileHeight: 0, + .withOpacity(0.12), + selectedTileColor: + Theme.of(context).colorScheme.primary.withOpacity(0.12), + contentPadding: const EdgeInsets.symmetric(horizontal: 6), + minTileHeight: 28, onTap: () { context .read() @@ -1111,14 +1099,11 @@ class _FontListPopupState extends State<_FontListPopup> { widget.controller.close(); }, - title: Align( - alignment: AlignmentDirectional.centerStart, - child: Text( - font.fontFamilyDisplayName, - style: TextStyle( - color: AFThemeExtension.of(context).textColor, - fontFamily: getGoogleFontSafely(font).fontFamily, - ), + title: Text( + font.fontFamilyDisplayName, + style: TextStyle( + color: AFThemeExtension.of(context).textColor, + fontFamily: getGoogleFontSafely(font).fontFamily, ), ), trailing: @@ -1146,21 +1131,9 @@ class _DocumentCursorColorSetting extends StatelessWidget { return SettingListTile( label: label, resetButtonKey: const Key('DocumentCursorColorResetButton'), - onResetRequested: () { - showConfirmDialog( - context: context, - title: - LocaleKeys.settings_workspacePage_resetCursorColor_title.tr(), - description: LocaleKeys - .settings_workspacePage_resetCursorColor_description - .tr(), - style: ConfirmPopupStyle.cancelAndOk, - confirmLabel: LocaleKeys.settings_common_reset.tr(), - onConfirm: () => context - ..read().resetDocumentCursorColor() - ..read().syncCursorColor(null), - ); - }, + onResetRequested: () => context + ..read().resetDocumentCursorColor() + ..read().syncCursorColor(null), trailing: [ DocumentColorSettingButton( key: const Key('DocumentCursorColorSettingButton'), @@ -1216,21 +1189,9 @@ class _DocumentSelectionColorSetting extends StatelessWidget { return SettingListTile( label: label, resetButtonKey: const Key('DocumentSelectionColorResetButton'), - onResetRequested: () { - showConfirmDialog( - context: context, - title: LocaleKeys.settings_workspacePage_resetSelectionColor_title - .tr(), - description: LocaleKeys - .settings_workspacePage_resetSelectionColor_description - .tr(), - style: ConfirmPopupStyle.cancelAndOk, - confirmLabel: LocaleKeys.settings_common_reset.tr(), - onConfirm: () => context - ..read().resetDocumentSelectionColor() - ..read().syncSelectionColor(null), - ); - }, + onResetRequested: () => context + ..read().resetDocumentSelectionColor() + ..read().syncSelectionColor(null), trailing: [ DocumentColorSettingButton( currentColor: state.selectionColor ?? @@ -1278,99 +1239,3 @@ class _SelectionColorValueWidget extends StatelessWidget { ); } } - -class DocumentPaddingSetting extends StatelessWidget { - const DocumentPaddingSetting({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Column( - children: [ - Row( - children: [ - FlowyText.medium( - LocaleKeys.settings_appearance_documentSettings_width.tr(), - ), - const Spacer(), - SettingsResetButton( - onResetRequested: () => - context.read().syncWidth(null), - ), - ], - ), - const VSpace(6), - Container( - height: 32, - padding: const EdgeInsets.only(right: 4), - child: _DocumentPaddingSlider( - onPaddingChanged: (value) { - context.read().syncWidth(value); - }, - ), - ), - ], - ); - }, - ); - } -} - -class _DocumentPaddingSlider extends StatefulWidget { - const _DocumentPaddingSlider({ - required this.onPaddingChanged, - }); - - final void Function(double) onPaddingChanged; - - @override - State<_DocumentPaddingSlider> createState() => _DocumentPaddingSliderState(); -} - -class _DocumentPaddingSliderState extends State<_DocumentPaddingSlider> { - late double width; - - @override - void initState() { - super.initState(); - - width = context.read().state.width; - } - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.width != width) { - width = state.width; - } - return SliderTheme( - data: Theme.of(context).sliderTheme.copyWith( - showValueIndicator: ShowValueIndicator.never, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 8, - ), - overlayShape: SliderComponentShape.noThumb, - ), - child: Slider( - value: width.clamp( - EditorStyleCustomizer.minDocumentWidth, - EditorStyleCustomizer.maxDocumentWidth, - ), - min: EditorStyleCustomizer.minDocumentWidth, - max: EditorStyleCustomizer.maxDocumentWidth, - divisions: 10, - onChanged: (value) { - setState(() => width = value); - - widget.onPaddingChanged(value); - }, - ), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart deleted file mode 100644 index 2f03fc052c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/constants.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/startup/startup.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:flutter/material.dart'; - -class SettingsPageSitesConstants { - static const threeDotsButtonWidth = 26.0; - static const alignPadding = 6.0; - - static final dateFormat = DateFormat('MMM d, yyyy'); - - static final publishedViewHeaderTitles = [ - LocaleKeys.settings_sites_publishedPage_page.tr(), - LocaleKeys.settings_sites_publishedPage_pathName.tr(), - LocaleKeys.settings_sites_publishedPage_date.tr(), - ]; - - static final namespaceHeaderTitles = [ - LocaleKeys.settings_sites_namespaceHeader.tr(), - LocaleKeys.settings_sites_homepageHeader.tr(), - ]; - - // the published view name is longer than the other two, so we give it more flex - static final publishedViewItemFlexes = [1, 1, 1]; -} - -class SettingsPageSitesEvent { - static void visitSite( - PublishInfoViewPB publishInfoView, { - String? nameSpace, - }) { - // visit the site - final url = ShareConstants.buildPublishUrl( - nameSpace: nameSpace ?? publishInfoView.info.namespace, - publishName: publishInfoView.info.publishName, - ); - afLaunchUrlString(url); - } - - static void copySiteLink( - BuildContext context, - PublishInfoViewPB publishInfoView, { - String? nameSpace, - }) { - final url = ShareConstants.buildPublishUrl( - nameSpace: nameSpace ?? publishInfoView.info.namespace, - publishName: publishInfoView.info.publishName, - ); - getIt().setData(ClipboardServiceData(plainText: url)); - showToastNotification( - message: LocaleKeys.message_copy_success.tr(), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_header.dart deleted file mode 100644 index 23a1bb2d7f..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_header.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class DomainHeader extends StatelessWidget { - const DomainHeader({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - ...SettingsPageSitesConstants.namespaceHeaderTitles.map( - (title) => Expanded( - child: FlowyText.medium( - title, - fontSize: 14.0, - textAlign: TextAlign.left, - ), - ), - ), - // it used to align the three dots button in the published page item - const HSpace(SettingsPageSitesConstants.threeDotsButtonWidth), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart deleted file mode 100644 index b1d9b9cdae..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_item.dart +++ /dev/null @@ -1,277 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/shared/colors.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.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'; - -class DomainItem extends StatelessWidget { - const DomainItem({ - super.key, - required this.namespace, - required this.homepage, - }); - - final String namespace; - final String homepage; - - @override - Widget build(BuildContext context) { - final namespaceUrl = ShareConstants.buildNamespaceUrl( - nameSpace: namespace, - ); - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // Namespace - Expanded( - child: _buildNamespace(context, namespaceUrl), - ), - // Homepage - Expanded( - child: _buildHomepage(context), - ), - // ... button - DomainMoreAction(namespace: namespace), - ], - ); - } - - Widget _buildNamespace(BuildContext context, String namespaceUrl) { - return Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(right: 12.0), - child: FlowyTooltip( - message: '${LocaleKeys.shareAction_visitSite.tr()}\n$namespaceUrl', - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowyText( - namespaceUrl, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - final namespaceUrl = ShareConstants.buildNamespaceUrl( - nameSpace: namespace, - withHttps: true, - ); - afLaunchUrlString(namespaceUrl); - }, - ), - ), - ); - } - - Widget _buildHomepage(BuildContext context) { - final plan = context.read().state.subscriptionInfo?.plan; - - if (plan == null) { - return const SizedBox.shrink(); - } - - final isFreePlan = plan == WorkspacePlanPB.FreePlan; - if (isFreePlan) { - return const Padding( - padding: EdgeInsets.only( - left: SettingsPageSitesConstants.alignPadding, - ), - child: _FreePlanUpgradeButton(), - ); - } - - return const _HomePageButton(); - } -} - -class _HomePageButton extends StatelessWidget { - const _HomePageButton(); - - @override - Widget build(BuildContext context) { - final settingsSitesState = context.watch().state; - if (settingsSitesState.isLoading) { - return const SizedBox.shrink(); - } - - final isOwner = context - .watch() - .state - .currentWorkspace - ?.role - .isOwner ?? - false; - - final homePageView = settingsSitesState.homePageView; - Widget child = homePageView == null - ? _defaultHomePageButton(context) - : PublishInfoViewItem( - publishInfoView: homePageView, - margin: isOwner ? null : EdgeInsets.zero, - ); - - if (isOwner) { - child = _buildHomePageButtonForOwner( - context, - homePageView: homePageView, - child: child, - ); - } else { - child = _buildHomePageButtonForNonOwner(context, child); - } - - return Container( - alignment: Alignment.centerLeft, - child: child, - ); - } - - Widget _buildHomePageButtonForOwner( - BuildContext context, { - required PublishInfoViewPB? homePageView, - required Widget child, - }) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - constraints: const BoxConstraints( - maxWidth: 260, - maxHeight: 345, - ), - margin: const EdgeInsets.symmetric( - horizontal: 14.0, - vertical: 12.0, - ), - popupBuilder: (_) { - final bloc = context.read(); - return BlocProvider.value( - value: bloc, - child: SelectHomePageMenu( - userProfile: bloc.user, - workspaceId: bloc.workspaceId, - onSelected: (view) {}, - ), - ); - }, - child: child, - ), - ), - if (homePageView != null) - FlowyTooltip( - message: LocaleKeys.settings_sites_clearHomePage.tr(), - child: FlowyButton( - margin: const EdgeInsets.all(4.0), - useIntrinsicWidth: true, - onTap: () { - context.read().add( - const SettingsSitesEvent.removeHomePage(), - ); - }, - text: const FlowySvg( - FlowySvgs.close_m, - size: Size.square(19.0), - ), - ), - ), - ], - ); - } - - Widget _buildHomePageButtonForNonOwner( - BuildContext context, - Widget child, - ) { - return FlowyTooltip( - message: LocaleKeys - .settings_sites_namespace_onlyWorkspaceOwnerCanSetHomePage - .tr(), - child: IgnorePointer( - child: child, - ), - ); - } - - Widget _defaultHomePageButton(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: true, - leftIcon: const FlowySvg( - FlowySvgs.search_s, - ), - leftIconSize: const Size.square(14.0), - text: FlowyText( - LocaleKeys.settings_sites_selectHomePage.tr(), - figmaLineHeight: 18.0, - ), - ); - } -} - -class _FreePlanUpgradeButton extends StatelessWidget { - const _FreePlanUpgradeButton(); - - @override - Widget build(BuildContext context) { - final isOwner = context - .watch() - .state - .currentWorkspace - ?.role - .isOwner ?? - false; - return Container( - alignment: Alignment.centerLeft, - child: FlowyTooltip( - message: LocaleKeys.settings_sites_namespace_upgradeToPro.tr(), - child: PrimaryRoundedButton( - text: 'Pro ↗', - fontSize: 12.0, - figmaLineHeight: 16.0, - fontWeight: FontWeight.w600, - radius: 8.0, - textColor: context.proPrimaryColor, - backgroundColor: context.proSecondaryColor, - margin: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 6.0, - ), - hoverColor: context.proSecondaryColor.withValues(alpha: 0.9), - onTap: () { - if (isOwner) { - showToastNotification( - message: - LocaleKeys.settings_sites_namespace_redirectToPayment.tr(), - type: ToastificationType.info, - ); - - context.read().add( - const SettingsSitesEvent.upgradeSubscription(), - ); - } else { - showToastNotification( - message: LocaleKeys - .settings_sites_namespace_pleaseAskOwnerToSetHomePage - .tr(), - type: ToastificationType.info, - ); - } - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart deleted file mode 100644 index 9c506b22ff..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_more_action.dart +++ /dev/null @@ -1,210 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.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'; - -class DomainMoreAction extends StatefulWidget { - const DomainMoreAction({ - super.key, - required this.namespace, - }); - - final String namespace; - - @override - State createState() => _DomainMoreActionState(); -} - -class _DomainMoreActionState extends State { - @override - void initState() { - super.initState(); - - // update the current workspace to ensure the owner check is correct - context.read().add(const UserWorkspaceEvent.initial()); - } - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: const BoxConstraints(maxWidth: 188), - offset: const Offset(6, 0), - animationDuration: Durations.short3, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - child: const SizedBox( - width: SettingsPageSitesConstants.threeDotsButtonWidth, - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowySvg(FlowySvgs.three_dots_s), - ), - ), - popupBuilder: (builderContext) { - return BlocProvider.value( - value: context.read(), - child: _buildUpdateNamespaceButton( - context, - builderContext, - ), - ); - }, - ); - } - - Widget _buildUpdateNamespaceButton( - BuildContext context, - BuildContext builderContext, - ) { - final child = _buildActionButton( - context, - builderContext, - type: _ActionType.updateNamespace, - ); - - final plan = context.read().state.subscriptionInfo?.plan; - - if (plan != WorkspacePlanPB.ProPlan) { - return _buildForbiddenActionButton( - context, - tooltipMessage: LocaleKeys.settings_sites_namespace_upgradeToPro.tr(), - child: child, - ); - } - - final isOwner = context - .watch() - .state - .currentWorkspace - ?.role - .isOwner ?? - false; - - if (!isOwner) { - return _buildForbiddenActionButton( - context, - tooltipMessage: LocaleKeys - .settings_sites_error_onlyWorkspaceOwnerCanUpdateNamespace - .tr(), - child: child, - ); - } - - return child; - } - - Widget _buildForbiddenActionButton( - BuildContext context, { - required String tooltipMessage, - required Widget child, - }) { - return Opacity( - opacity: 0.5, - child: FlowyTooltip( - message: tooltipMessage, - child: MouseRegion( - cursor: SystemMouseCursors.forbidden, - child: IgnorePointer(child: child), - ), - ), - ); - } - - Widget _buildActionButton( - BuildContext context, - BuildContext builderContext, { - required _ActionType type, - }) { - return Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - iconPadding: 10.0, - onTap: () => _onTap(context, builderContext, type), - leftIconBuilder: (onHover) => FlowySvg( - type.leftIconSvg, - ), - textBuilder: (onHover) => FlowyText.regular( - type.name, - fontSize: 14.0, - figmaLineHeight: 18.0, - overflow: TextOverflow.ellipsis, - ), - ), - ); - } - - void _onTap( - BuildContext context, - BuildContext builderContext, - _ActionType type, - ) { - switch (type) { - case _ActionType.updateNamespace: - _showSettingsDialog( - context, - builderContext, - ); - break; - case _ActionType.removeHomePage: - context.read().add( - const SettingsSitesEvent.removeHomePage(), - ); - break; - } - - PopoverContainer.of(builderContext).closeAll(); - } - - void _showSettingsDialog( - BuildContext context, - BuildContext builderContext, - ) { - showDialog( - context: context, - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 460, - child: DomainSettingsDialog( - namespace: widget.namespace, - ), - ), - ), - ); - }, - ); - } -} - -enum _ActionType { - updateNamespace, - removeHomePage, -} - -extension _ActionTypeExtension on _ActionType { - String get name => switch (this) { - _ActionType.updateNamespace => - LocaleKeys.settings_sites_updateNamespace.tr(), - _ActionType.removeHomePage => - LocaleKeys.settings_sites_removeHomepage.tr(), - }; - - FlowySvgData get leftIconSvg => switch (this) { - _ActionType.updateNamespace => FlowySvgs.view_item_rename_s, - _ActionType.removeHomePage => FlowySvgs.trash_s, - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart deleted file mode 100644 index 9617f2c8d6..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/domain_settings_dialog.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/plugins/shared/share/publish_color_extension.dart'; -import 'package:appflowy/shared/error_code/error_code_map.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.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/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class DomainSettingsDialog extends StatefulWidget { - const DomainSettingsDialog({ - super.key, - required this.namespace, - }); - - final String namespace; - - @override - State createState() => _DomainSettingsDialogState(); -} - -class _DomainSettingsDialogState extends State { - final focusNode = FocusNode(); - final controller = TextEditingController(); - late final controllerText = ValueNotifier(widget.namespace); - String errorHintText = ''; - - @override - void initState() { - super.initState(); - - controller.text = widget.namespace; - controller.addListener(_onTextChanged); - } - - void _onTextChanged() => controllerText.value = controller.text; - - @override - void dispose() { - focusNode.dispose(); - controller.removeListener(_onTextChanged); - controller.dispose(); - controllerText.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: _onListener, - child: KeyboardListener( - focusNode: focusNode, - autofocus: true, - onKeyEvent: (event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.of(context).pop(); - } - }, - child: Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(), - const VSpace(12), - _buildNamespaceDescription(), - const VSpace(20), - _buildNamespaceTextField(), - _buildPreviewNamespace(), - _buildErrorHintText(), - const VSpace(20), - _buildButtons(), - ], - ), - ), - ), - ); - } - - Widget _buildTitle() { - return Row( - children: [ - FlowyText( - LocaleKeys.settings_sites_namespace_updateExistingNamespace.tr(), - fontSize: 16.0, - figmaLineHeight: 22.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - const HSpace(6.0), - FlowyTooltip( - message: LocaleKeys.settings_sites_namespace_tooltip.tr(), - child: const FlowySvg(FlowySvgs.information_s), - ), - const HSpace(6.0), - const Spacer(), - FlowyButton( - margin: const EdgeInsets.all(3), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.upgrade_close_s, - size: Size.square(18.0), - ), - onTap: () => Navigator.of(context).pop(), - ), - ], - ); - } - - Widget _buildNamespaceDescription() { - return FlowyText( - LocaleKeys.settings_sites_namespace_description.tr(), - fontSize: 14.0, - color: Theme.of(context).hintColor, - figmaLineHeight: 16.0, - maxLines: 3, - ); - } - - Widget _buildNamespaceTextField() { - return SizedBox( - height: 36, - child: FlowyTextField( - autoFocus: false, - controller: controller, - enableBorderColor: ShareMenuColors.borderColor(context), - ), - ); - } - - Widget _buildButtons() { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedRoundedButton( - text: LocaleKeys.button_cancel.tr(), - onTap: () => Navigator.of(context).pop(), - ), - const HSpace(12.0), - PrimaryRoundedButton( - text: LocaleKeys.button_save.tr(), - radius: 8.0, - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 9.0, - ), - onTap: _onSave, - ), - ], - ); - } - - Widget _buildErrorHintText() { - if (errorHintText.isEmpty) { - return const SizedBox.shrink(); - } - - return Padding( - padding: const EdgeInsets.only(top: 4.0, left: 2.0), - child: FlowyText( - errorHintText, - fontSize: 12.0, - figmaLineHeight: 18.0, - color: Theme.of(context).colorScheme.error, - ), - ); - } - - Widget _buildPreviewNamespace() { - return ValueListenableBuilder( - valueListenable: controllerText, - builder: (context, value, child) { - final url = ShareConstants.buildNamespaceUrl( - nameSpace: value, - ); - return Padding( - padding: const EdgeInsets.only(top: 4.0, left: 2.0), - child: Opacity( - opacity: 0.8, - child: FlowyText( - url, - fontSize: 14.0, - figmaLineHeight: 18.0, - withTooltip: true, - overflow: TextOverflow.ellipsis, - ), - ), - ); - }, - ); - } - - void _onSave() { - // listen on the result - context - .read() - .add(SettingsSitesEvent.updateNamespace(controller.text)); - } - - void _onListener(BuildContext context, SettingsSitesState state) { - final actionResult = state.actionResult; - final type = actionResult?.actionType; - final result = actionResult?.result; - if (type != SettingsSitesActionType.updateNamespace || result == null) { - return; - } - - result.fold( - (s) { - showToastNotification( - message: LocaleKeys.settings_sites_success_namespaceUpdated.tr(), - ); - - Navigator.of(context).pop(); - }, - (f) { - final basicErrorMessage = - LocaleKeys.settings_sites_error_failedToUpdateNamespace.tr(); - final errorMessage = f.code.namespaceErrorMessage; - - setState(() { - errorHintText = errorMessage.orDefault(basicErrorMessage); - }); - - Log.error('Failed to update namespace: $f'); - - showToastNotification( - message: basicErrorMessage, - type: ToastificationType.error, - description: errorMessage, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart deleted file mode 100644 index f1236c1024..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/domain/home_page_menu.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.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'; - -typedef OnSelectedHomePage = void Function(ViewPB view); - -class SelectHomePageMenu extends StatefulWidget { - const SelectHomePageMenu({ - super.key, - required this.onSelected, - required this.userProfile, - required this.workspaceId, - }); - - final OnSelectedHomePage onSelected; - final UserProfilePB userProfile; - final String workspaceId; - - @override - State createState() => _SelectHomePageMenuState(); -} - -class _SelectHomePageMenuState extends State { - List source = []; - List views = []; - - @override - void initState() { - super.initState(); - - source = context.read().state.publishedViews; - views = [...source]; - } - - @override - Widget build(BuildContext context) { - if (views.isEmpty) { - return _buildNoPublishedViews(); - } - - return _buildMenu(context); - } - - Widget _buildNoPublishedViews() { - return FlowyText.regular( - LocaleKeys.settings_sites_publishedPage_noPublishedPages.tr(), - color: Theme.of(context).hintColor, - overflow: TextOverflow.ellipsis, - ); - } - - Widget _buildMenu(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SpaceSearchField( - width: 240, - onSearch: (context, value) => _onSearch(value), - ), - const VSpace(10), - Expanded( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...views.map( - (view) => Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: PublishInfoViewItem( - publishInfoView: view, - useIntrinsicWidth: false, - onTap: () { - context.read().add( - SettingsSitesEvent.setHomePage(view.info.viewId), - ); - - PopoverContainer.of(context).close(); - }, - ), - ), - ), - ], - ), - ), - ), - ], - ); - } - - void _onSearch(String value) { - setState(() { - if (value.isEmpty) { - views = source; - } else { - views = source - .where( - (view) => - view.view.name.toLowerCase().contains(value.toLowerCase()), - ) - .toList(); - } - }); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart deleted file mode 100644 index 3ba2c7e75e..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/publish_info_view_item.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/util/string_extension.dart'; -import 'package:appflowy/workspace/application/view/view_ext.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'; -import 'package:flutter/material.dart'; - -class PublishInfoViewItem extends StatelessWidget { - const PublishInfoViewItem({ - super.key, - required this.publishInfoView, - this.onTap, - this.useIntrinsicWidth = true, - this.margin, - this.extraTooltipMessage, - }); - - final PublishInfoViewPB publishInfoView; - final VoidCallback? onTap; - final bool useIntrinsicWidth; - final EdgeInsets? margin; - final String? extraTooltipMessage; - - @override - Widget build(BuildContext context) { - final name = publishInfoView.view.name.orDefault( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - ); - final tooltipMessage = - extraTooltipMessage != null ? '$extraTooltipMessage\n$name' : name; - return Container( - alignment: Alignment.centerLeft, - child: FlowyButton( - margin: margin, - useIntrinsicWidth: useIntrinsicWidth, - mainAxisAlignment: MainAxisAlignment.start, - leftIcon: _buildIcon(), - text: FlowyTooltip( - message: tooltipMessage, - child: FlowyText.regular( - name, - fontSize: 14.0, - figmaLineHeight: 18.0, - overflow: TextOverflow.ellipsis, - ), - ), - onTap: onTap, - ), - ); - } - - Widget _buildIcon() { - final icon = publishInfoView.view.icon.toEmojiIconData(); - return icon.isNotEmpty - ? RawEmojiIconWidget(emoji: icon, emojiSize: 16.0) - : publishInfoView.view.defaultIcon(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart deleted file mode 100644 index 99c310c901..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart +++ /dev/null @@ -1,120 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; -import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart'; -import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/publish_info_view_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.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'; -import 'package:flutter/material.dart'; - -class PublishedViewItem extends StatelessWidget { - const PublishedViewItem({ - super.key, - required this.publishInfoView, - }); - - final PublishInfoViewPB publishInfoView; - - @override - Widget build(BuildContext context) { - final flexes = SettingsPageSitesConstants.publishedViewItemFlexes; - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // Published page name - Expanded( - flex: flexes[0], - child: _buildPublishedPageName(context), - ), - - // Published Name - Expanded( - flex: flexes[1], - child: _buildPublishedName(context), - ), - - // Published at - Expanded( - flex: flexes[2], - child: Padding( - padding: const EdgeInsets.only( - left: SettingsPageSitesConstants.alignPadding, - ), - child: _buildPublishedAt(context), - ), - ), - - // More actions - PublishedViewMoreAction( - publishInfoView: publishInfoView, - ), - ], - ); - } - - Widget _buildPublishedPageName(BuildContext context) { - return PublishInfoViewItem( - extraTooltipMessage: - LocaleKeys.settings_sites_publishedPage_clickToOpenPageInApp.tr(), - publishInfoView: publishInfoView, - onTap: () { - context.popToHome(); - - getIt().add( - ActionNavigationEvent.performAction( - action: NavigationAction( - objectId: publishInfoView.view.viewId, - ), - ), - ); - }, - ); - } - - Widget _buildPublishedAt(BuildContext context) { - final formattedDate = SettingsPageSitesConstants.dateFormat.format( - DateTime.fromMillisecondsSinceEpoch( - publishInfoView.info.publishTimestampSec.toInt() * 1000, - ), - ); - return FlowyText( - formattedDate, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ); - } - - Widget _buildPublishedName(BuildContext context) { - return Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.only(right: 48.0), - child: FlowyButton( - useIntrinsicWidth: true, - onTap: () { - final url = ShareConstants.buildPublishUrl( - nameSpace: publishInfoView.info.namespace, - publishName: publishInfoView.info.publishName, - ); - afLaunchUrlString(url); - }, - text: FlowyTooltip( - message: - '${LocaleKeys.settings_sites_publishedPage_clickToOpenPageInBrowser.tr()}\n${publishInfoView.info.publishName}', - child: FlowyText( - publishInfoView.info.publishName, - fontSize: 14.0, - figmaLineHeight: 18.0, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart deleted file mode 100644 index 34fefb4cd8..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; - -class PublishViewItemHeader extends StatelessWidget { - const PublishViewItemHeader({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final items = List.generate( - SettingsPageSitesConstants.publishedViewHeaderTitles.length, - (index) => ( - title: SettingsPageSitesConstants.publishedViewHeaderTitles[index], - flex: SettingsPageSitesConstants.publishedViewItemFlexes[index], - ), - ); - - return Row( - children: [ - ...items.map( - (item) => Expanded( - flex: item.flex, - child: FlowyText.medium( - item.title, - fontSize: 14.0, - textAlign: TextAlign.left, - ), - ), - ), - // it used to align the three dots button in the published page item - const HSpace(SettingsPageSitesConstants.threeDotsButtonWidth), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart deleted file mode 100644 index 6c7b17a70b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_more_action.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.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'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PublishedViewMoreAction extends StatelessWidget { - const PublishedViewMoreAction({ - super.key, - required this.publishInfoView, - }); - - final PublishInfoViewPB publishInfoView; - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: const BoxConstraints(maxWidth: 168), - offset: const Offset(6, 0), - animationDuration: Durations.short3, - beginScaleFactor: 1.0, - beginOpacity: 0.8, - child: const SizedBox( - width: SettingsPageSitesConstants.threeDotsButtonWidth, - child: FlowyButton( - useIntrinsicWidth: true, - text: FlowySvg(FlowySvgs.three_dots_s), - ), - ), - popupBuilder: (builderContext) { - return BlocProvider.value( - value: context.read(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _buildActionButton( - context, - builderContext, - type: _ActionType.viewSite, - ), - _buildActionButton( - context, - builderContext, - type: _ActionType.copySiteLink, - ), - _buildActionButton( - context, - builderContext, - type: _ActionType.unpublish, - ), - _buildActionButton( - context, - builderContext, - type: _ActionType.customUrl, - ), - _buildActionButton( - context, - builderContext, - type: _ActionType.settings, - ), - ], - ), - ); - }, - ); - } - - Widget _buildActionButton( - BuildContext context, - BuildContext builderContext, { - required _ActionType type, - }) { - return Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - iconPadding: 10.0, - onTap: () => _onTap(context, builderContext, type), - leftIconBuilder: (onHover) => FlowySvg( - type.leftIconSvg, - ), - textBuilder: (onHover) => FlowyText.regular( - type.name, - fontSize: 14.0, - figmaLineHeight: 18.0, - ), - ), - ); - } - - void _onTap( - BuildContext context, - BuildContext builderContext, - _ActionType type, - ) { - switch (type) { - case _ActionType.viewSite: - SettingsPageSitesEvent.visitSite( - publishInfoView, - nameSpace: context.read().state.namespace, - ); - break; - case _ActionType.copySiteLink: - SettingsPageSitesEvent.copySiteLink( - context, - publishInfoView, - nameSpace: context.read().state.namespace, - ); - break; - case _ActionType.settings: - _showSettingsDialog( - context, - builderContext, - ); - break; - case _ActionType.unpublish: - context.read().add( - SettingsSitesEvent.unpublishView(publishInfoView.info.viewId), - ); - PopoverContainer.maybeOf(builderContext)?.close(); - break; - case _ActionType.customUrl: - _showSettingsDialog( - context, - builderContext, - ); - break; - } - - PopoverContainer.of(builderContext).closeAll(); - } - - void _showSettingsDialog( - BuildContext context, - BuildContext builderContext, - ) { - showDialog( - context: context, - builder: (_) { - return BlocProvider.value( - value: context.read(), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: PublishedViewSettingsDialog( - publishInfoView: publishInfoView, - ), - ), - ), - ); - }, - ); - } -} - -enum _ActionType { - viewSite, - copySiteLink, - settings, - unpublish, - customUrl; - - String get name => switch (this) { - _ActionType.viewSite => LocaleKeys.shareAction_visitSite.tr(), - _ActionType.copySiteLink => LocaleKeys.shareAction_copyLink.tr(), - _ActionType.settings => LocaleKeys.settings_popupMenuItem_settings.tr(), - _ActionType.unpublish => LocaleKeys.shareAction_unPublish.tr(), - _ActionType.customUrl => LocaleKeys.settings_sites_customUrl.tr(), - }; - - FlowySvgData get leftIconSvg => switch (this) { - _ActionType.viewSite => FlowySvgs.share_publish_s, - _ActionType.copySiteLink => FlowySvgs.copy_s, - _ActionType.settings => FlowySvgs.settings_s, - _ActionType.unpublish => FlowySvgs.delete_s, - _ActionType.customUrl => FlowySvgs.edit_s, - }; -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart deleted file mode 100644 index ad37bae866..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/published_page/published_view_settings_dialog.dart +++ /dev/null @@ -1,221 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/publish_color_extension.dart'; -import 'package:appflowy/shared/error_code/error_code_map.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.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'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PublishedViewSettingsDialog extends StatefulWidget { - const PublishedViewSettingsDialog({ - super.key, - required this.publishInfoView, - }); - - final PublishInfoViewPB publishInfoView; - - @override - State createState() => - _PublishedViewSettingsDialogState(); -} - -class _PublishedViewSettingsDialogState - extends State { - final focusNode = FocusNode(); - final controller = TextEditingController(); - - @override - void initState() { - super.initState(); - - controller.text = widget.publishInfoView.info.publishName; - } - - @override - void dispose() { - focusNode.dispose(); - controller.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: _onListener, - child: KeyboardListener( - focusNode: focusNode, - autofocus: true, - onKeyEvent: (event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - Navigator.of(context).pop(); - } - }, - child: Container( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTitle(), - const VSpace(20), - _buildPublishNameLabel(), - const VSpace(8), - _buildPublishNameTextField(), - const VSpace(20), - _buildButtons(), - ], - ), - ), - ), - ); - } - - Widget _buildTitle() { - return Row( - children: [ - Expanded( - child: FlowyText( - LocaleKeys.settings_sites_publishedPage_settings.tr(), - fontSize: 16.0, - figmaLineHeight: 22.0, - fontWeight: FontWeight.w500, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(6.0), - FlowyButton( - margin: const EdgeInsets.all(3), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.upgrade_close_s, - size: Size.square(18.0), - ), - onTap: () => Navigator.of(context).pop(), - ), - ], - ); - } - - Widget _buildPublishNameLabel() { - return FlowyText( - LocaleKeys.settings_sites_publishedPage_pathName.tr(), - fontSize: 14.0, - color: Theme.of(context).hintColor, - ); - } - - Widget _buildPublishNameTextField() { - return Row( - children: [ - Expanded( - child: SizedBox( - height: 36, - child: FlowyTextField( - autoFocus: false, - controller: controller, - enableBorderColor: ShareMenuColors.borderColor(context), - textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( - height: 1.4, - ), - ), - ), - ), - const HSpace(12.0), - OutlinedRoundedButton( - text: LocaleKeys.button_save.tr(), - radius: 8.0, - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 11.0, - ), - onTap: _savePublishName, - ), - ], - ); - } - - Widget _buildButtons() { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - OutlinedRoundedButton( - text: LocaleKeys.shareAction_unPublish.tr(), - onTap: _unpublishView, - ), - const HSpace(12.0), - PrimaryRoundedButton( - text: LocaleKeys.shareAction_visitSite.tr(), - radius: 8.0, - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 9.0, - ), - onTap: _visitSite, - ), - ], - ); - } - - void _savePublishName() { - context.read().add( - SettingsSitesEvent.updatePublishName( - widget.publishInfoView.info.viewId, - controller.text, - ), - ); - } - - void _unpublishView() { - context.read().add( - SettingsSitesEvent.unpublishView( - widget.publishInfoView.info.viewId, - ), - ); - - Navigator.of(context).pop(); - } - - void _visitSite() { - SettingsPageSitesEvent.visitSite( - widget.publishInfoView, - nameSpace: context.read().state.namespace, - ); - } - - void _onListener(BuildContext context, SettingsSitesState state) { - final actionResult = state.actionResult; - final result = actionResult?.result; - if (actionResult == null || - result == null || - actionResult.actionType != SettingsSitesActionType.updatePublishName) { - return; - } - - result.fold( - (s) { - showToastNotification( - message: LocaleKeys.settings_sites_success_updatePathNameSuccess.tr(), - ); - Navigator.of(context).pop(); - }, - (f) { - Log.error('update path name failed: $f'); - - showToastNotification( - message: LocaleKeys.settings_sites_error_updatePathNameFailed.tr(), - type: ToastificationType.error, - description: f.code.publishErrorMessage, - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart deleted file mode 100644 index e03eed3f46..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart +++ /dev/null @@ -1,372 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:protobuf/protobuf.dart' hide FieldInfo; - -part 'settings_sites_bloc.freezed.dart'; - -// workspaceId -> namespace -Map _namespaceCache = {}; - -class SettingsSitesBloc extends Bloc { - SettingsSitesBloc({ - required this.workspaceId, - required this.user, - }) : super(const SettingsSitesState()) { - on((event, emit) async { - await event.when( - initial: () async => _initial(emit), - upgradeSubscription: () async => _upgradeSubscription(emit), - unpublishView: (viewId) async => _unpublishView( - viewId, - emit, - ), - updateNamespace: (namespace) async => _updateNamespace( - namespace, - emit, - ), - updatePublishName: (viewId, name) async => _updatePublishName( - viewId, - name, - emit, - ), - setHomePage: (viewId) async => _setHomePage( - viewId, - emit, - ), - removeHomePage: () async => _removeHomePage(emit), - ); - }); - } - - final String workspaceId; - final UserProfilePB user; - - Future _initial(Emitter emit) async { - final lastNamespace = _namespaceCache[workspaceId] ?? ''; - emit( - state.copyWith( - isLoading: true, - namespace: lastNamespace, - ), - ); - - // Combine fetching subscription info and namespace - final (subscriptionInfo, namespace) = await ( - _fetchUserSubscription(), - _fetchPublishNamespace(), - ).wait; - - emit( - state.copyWith( - subscriptionInfo: subscriptionInfo, - namespace: namespace, - ), - ); - - // This request is not blocking, render the namespace and subscription info first. - final (publishViews, homePageId) = await ( - _fetchPublishedViews(), - _fetchHomePageView(), - ).wait; - - final homePageView = publishViews.firstWhereOrNull( - (view) => view.info.viewId == homePageId, - ); - - emit( - state.copyWith( - publishedViews: publishViews, - homePageView: homePageView, - isLoading: false, - ), - ); - } - - Future _fetchUserSubscription() async { - final result = await UserBackendService.getWorkspaceSubscriptionInfo( - workspaceId, - ); - return result.fold((s) => s, (f) { - Log.error('Failed to fetch user subscription info: $f'); - return null; - }); - } - - Future _fetchPublishNamespace() async { - final result = await FolderEventGetPublishNamespace().send(); - _namespaceCache[workspaceId] = result.fold((s) => s.namespace, (_) => null); - return _namespaceCache[workspaceId] ?? ''; - } - - Future> _fetchPublishedViews() async { - final result = await FolderEventListPublishedViews().send(); - return result.fold( - // new -> old - (s) => s.items.sorted( - (a, b) => - b.info.publishTimestampSec.toInt() - - a.info.publishTimestampSec.toInt(), - ), - (_) => [], - ); - } - - Future _unpublishView( - String viewId, - Emitter emit, - ) async { - emit( - state.copyWith( - actionResult: const SettingsSitesActionResult( - actionType: SettingsSitesActionType.unpublishView, - isLoading: true, - result: null, - ), - ), - ); - - final request = UnpublishViewsPayloadPB(viewIds: [viewId]); - final result = await FolderEventUnpublishViews(request).send(); - final publishedViews = result.fold( - (_) => state.publishedViews - .where((view) => view.info.viewId != viewId) - .toList(), - (_) => state.publishedViews, - ); - - final isHomepage = result.fold( - (_) => state.homePageView?.info.viewId == viewId, - (_) => false, - ); - - emit( - state.copyWith( - publishedViews: publishedViews, - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.unpublishView, - isLoading: false, - result: result, - ), - homePageView: isHomepage ? null : state.homePageView, - ), - ); - } - - Future _updateNamespace( - String namespace, - Emitter emit, - ) async { - emit( - state.copyWith( - actionResult: const SettingsSitesActionResult( - actionType: SettingsSitesActionType.updateNamespace, - isLoading: true, - result: null, - ), - ), - ); - - final request = SetPublishNamespacePayloadPB()..newNamespace = namespace; - final result = await FolderEventSetPublishNamespace(request).send(); - - emit( - state.copyWith( - namespace: result.fold((_) => namespace, (_) => state.namespace), - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.updateNamespace, - isLoading: false, - result: result, - ), - ), - ); - } - - Future _updatePublishName( - String viewId, - String name, - Emitter emit, - ) async { - emit( - state.copyWith( - actionResult: const SettingsSitesActionResult( - actionType: SettingsSitesActionType.updatePublishName, - isLoading: true, - result: null, - ), - ), - ); - - final request = SetPublishNamePB() - ..viewId = viewId - ..newName = name; - final result = await FolderEventSetPublishName(request).send(); - final publishedViews = result.fold( - (_) => state.publishedViews.map((view) { - view.freeze(); - if (view.info.viewId == viewId) { - view = view.rebuild((b) { - final info = b.info; - info.freeze(); - b.info = info.rebuild((b) => b.publishName = name); - }); - } - return view; - }).toList(), - (_) => state.publishedViews, - ); - - emit( - state.copyWith( - publishedViews: publishedViews, - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.updatePublishName, - isLoading: false, - result: result, - ), - ), - ); - } - - Future _upgradeSubscription(Emitter emit) async { - final userService = UserBackendService(userId: user.id); - final result = await userService.createSubscription( - workspaceId, - SubscriptionPlanPB.Pro, - ); - - result.onSuccess((s) { - afLaunchUrlString(s.paymentLink); - }); - - emit( - state.copyWith( - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.upgradeSubscription, - isLoading: false, - result: result, - ), - ), - ); - } - - Future _setHomePage( - String? viewId, - Emitter emit, - ) async { - if (viewId == null) { - return; - } - final viewIdPB = ViewIdPB()..value = viewId; - final result = await FolderEventSetDefaultPublishView(viewIdPB).send(); - final homePageView = state.publishedViews.firstWhereOrNull( - (view) => view.info.viewId == viewId, - ); - - emit( - state.copyWith( - homePageView: homePageView, - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.setHomePage, - isLoading: false, - result: result, - ), - ), - ); - } - - Future _removeHomePage(Emitter emit) async { - final result = await FolderEventRemoveDefaultPublishView().send(); - - emit( - state.copyWith( - homePageView: result.fold((_) => null, (_) => state.homePageView), - actionResult: SettingsSitesActionResult( - actionType: SettingsSitesActionType.removeHomePage, - isLoading: false, - result: result, - ), - ), - ); - } - - Future _fetchHomePageView() async { - final result = await FolderEventGetDefaultPublishInfo().send(); - return result.fold((s) => s.viewId, (_) => null); - } -} - -@freezed -class SettingsSitesState with _$SettingsSitesState { - const factory SettingsSitesState({ - @Default([]) List publishedViews, - SettingsSitesActionResult? actionResult, - @Default('') String namespace, - @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo, - @Default(true) bool isLoading, - @Default(null) PublishInfoViewPB? homePageView, - }) = _SettingsSitesState; - - factory SettingsSitesState.initial() => const SettingsSitesState(); -} - -@freezed -class SettingsSitesEvent with _$SettingsSitesEvent { - const factory SettingsSitesEvent.initial() = _Initial; - const factory SettingsSitesEvent.unpublishView(String viewId) = - _UnpublishView; - const factory SettingsSitesEvent.updateNamespace(String namespace) = - _UpdateNamespace; - const factory SettingsSitesEvent.updatePublishName( - String viewId, - String name, - ) = _UpdatePublishName; - const factory SettingsSitesEvent.upgradeSubscription() = _UpgradeSubscription; - const factory SettingsSitesEvent.setHomePage(String? viewId) = _SetHomePage; - const factory SettingsSitesEvent.removeHomePage() = _RemoveHomePage; -} - -enum SettingsSitesActionType { - none, - unpublishView, - updateNamespace, - fetchPublishedViews, - updatePublishName, - fetchUserSubscription, - upgradeSubscription, - setHomePage, - removeHomePage, -} - -class SettingsSitesActionResult { - const SettingsSitesActionResult({ - required this.actionType, - required this.isLoading, - required this.result, - }); - - factory SettingsSitesActionResult.none() => const SettingsSitesActionResult( - actionType: SettingsSitesActionType.none, - isLoading: false, - result: null, - ); - - final SettingsSitesActionType actionType; - final FlowyResult? result; - final bool isLoading; - - @override - String toString() { - return 'SettingsSitesActionResult(actionType: $actionType, isLoading: $isLoading, result: $result)'; - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart deleted file mode 100644 index f3845b0896..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/sites/settings_sites_view.dart +++ /dev/null @@ -1,230 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/constants.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_header.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/domain/domain_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/published_page/published_view_item_header.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.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'; - -class SettingsSitesPage extends StatelessWidget { - const SettingsSitesPage({ - super.key, - required this.workspaceId, - required this.user, - }); - - final String workspaceId; - final UserProfilePB user; - - @override - Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => SettingsSitesBloc( - workspaceId: workspaceId, - user: user, - )..add(const SettingsSitesEvent.initial()), - ), - BlocProvider( - create: (context) => UserWorkspaceBloc(userProfile: user) - ..add(const UserWorkspaceEvent.initial()), - ), - ], - child: const _SettingsSitesPageView(), - ); - } -} - -class _SettingsSitesPageView extends StatelessWidget { - const _SettingsSitesPageView(); - - @override - Widget build(BuildContext context) { - return SettingsBody( - title: LocaleKeys.settings_sites_title.tr(), - autoSeparate: false, - children: [ - // Domain / Namespace - _buildNamespaceCategory(context), - const VSpace(36), - // All published pages - _buildPublishedViewsCategory(context), - ], - ); - } - - Widget _buildNamespaceCategory(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_sites_namespaceHeader.tr(), - description: LocaleKeys.settings_sites_namespaceDescription.tr(), - descriptionColor: Theme.of(context).hintColor, - children: [ - const FlowyDivider(), - BlocConsumer( - listener: _onListener, - builder: (context, state) { - return SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const FlowyDivider( - padding: EdgeInsets.symmetric(vertical: 12.0), - ), - children: [ - const DomainHeader(), - ConstrainedBox( - constraints: const BoxConstraints(minHeight: 36.0), - child: Transform.translate( - offset: const Offset( - -SettingsPageSitesConstants.alignPadding, - 0, - ), - child: DomainItem( - namespace: state.namespace, - homepage: '', - ), - ), - ), - ], - ); - }, - ), - ], - ); - } - - Widget _buildPublishedViewsCategory(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_sites_publishedPage_title.tr(), - description: LocaleKeys.settings_sites_publishedPage_description.tr(), - descriptionColor: Theme.of(context).hintColor, - children: [ - const FlowyDivider(), - BlocBuilder( - builder: (context, state) { - return SeparatedColumn( - crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => const FlowyDivider( - padding: EdgeInsets.symmetric(vertical: 12.0), - ), - children: _buildPublishedViewsResult(context, state), - ); - }, - ), - ], - ); - } - - List _buildPublishedViewsResult( - BuildContext context, - SettingsSitesState state, - ) { - final publishedViews = state.publishedViews; - final List children = [ - const PublishViewItemHeader(), - ]; - - if (!state.isLoading) { - if (publishedViews.isEmpty) { - children.add( - FlowyText.regular( - LocaleKeys.settings_sites_publishedPage_emptyHinText.tr(), - color: Theme.of(context).hintColor, - ), - ); - } else { - children.addAll( - publishedViews.map( - (view) => Transform.translate( - offset: const Offset( - -SettingsPageSitesConstants.alignPadding, - 0, - ), - child: PublishedViewItem(publishInfoView: view), - ), - ), - ); - } - } else { - children.add( - const Center( - child: SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator.adaptive(strokeWidth: 3), - ), - ), - ); - } - - return children; - } - - void _onListener(BuildContext context, SettingsSitesState state) { - final actionResult = state.actionResult; - final type = actionResult?.actionType; - final result = actionResult?.result; - if (type == SettingsSitesActionType.upgradeSubscription && result != null) { - result.onFailure((f) { - Log.error('Failed to generate payment link for Pro Plan: ${f.msg}'); - - showToastNotification( - message: - LocaleKeys.settings_sites_error_failedToGeneratePaymentLink.tr(), - type: ToastificationType.error, - ); - }); - } else if (type == SettingsSitesActionType.unpublishView && - result != null) { - result.fold((_) { - showToastNotification( - message: LocaleKeys.publish_unpublishSuccessfully.tr(), - ); - }, (f) { - Log.error('Failed to unpublish view: ${f.msg}'); - - showToastNotification( - message: LocaleKeys.publish_unpublishFailed.tr(), - type: ToastificationType.error, - description: f.msg, - ); - }); - } else if (type == SettingsSitesActionType.setHomePage && result != null) { - result.fold((s) { - showToastNotification( - message: LocaleKeys.settings_sites_success_setHomepageSuccess.tr(), - ); - }, (f) { - Log.error('Failed to set homepage: ${f.msg}'); - - showToastNotification( - message: LocaleKeys.settings_sites_error_setHomepageFailed.tr(), - type: ToastificationType.error, - ); - }); - } else if (type == SettingsSitesActionType.removeHomePage && - result != null) { - result.fold((s) { - showToastNotification( - message: LocaleKeys.settings_sites_success_removeHomePageSuccess.tr(), - ); - }, (f) { - Log.error('Failed to remove homepage: ${f.msg}'); - - showToastNotification( - message: LocaleKeys.settings_sites_error_removeHomePageFailed.tr(), - type: ToastificationType.error, - ); - }); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index cd33c62090..cbe92f1b16 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,75 +1,47 @@ -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/shared/appflowy_cache_manager.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/share_log_files.dart'; -import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; -import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; -import 'package:appflowy/workspace/presentation/settings/pages/sites/settings_sites_view.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_notifications_view.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/web_url_hint_widget.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'pages/setting_ai_view/local_settings_ai_view.dart'; import 'widgets/setting_cloud.dart'; -@visibleForTesting -const kSelfHostedTextInputFieldKey = - ValueKey('self_hosted_url_input_text_field'); -@visibleForTesting -const kSelfHostedWebTextInputFieldKey = - ValueKey('self_hosted_web_url_input_text_field'); - class SettingsDialog extends StatelessWidget { SettingsDialog( this.user, { required this.dismissDialog, required this.didLogout, required this.restartApp, - this.initPage, }) : super(key: ValueKey(user.id)); - final UserProfilePB user; - final SettingsPage? initPage; final VoidCallback dismissDialog; final VoidCallback didLogout; final VoidCallback restartApp; + final UserProfilePB user; @override Widget build(BuildContext context) { - final width = MediaQuery.of(context).size.width * 0.6; return BlocProvider( - create: (context) => SettingsDialogBloc( - user, - context.read().state.currentWorkspace?.role, - initPage: initPage, - )..add(const SettingsDialogEvent.initial()), + create: (context) => getIt(param1: user) + ..add(const SettingsDialogEvent.initial()), child: BlocBuilder( builder: (context, state) => FlowyDialog( - width: width, - constraints: const BoxConstraints(minWidth: 564), + width: MediaQuery.of(context).size.width * 0.7, + constraints: const BoxConstraints(maxWidth: 784, minWidth: 564), child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, @@ -85,7 +57,10 @@ class SettingsDialog extends StatelessWidget { .add(SettingsDialogEvent.setSelectedPage(index)), currentPage: context.read().state.page, - isBillingEnabled: state.isBillingEnabled, + member: context + .read() + .state + .currentWorkspaceMember, ), ), Expanded( @@ -100,8 +75,7 @@ class SettingsDialog extends StatelessWidget { context .read() .state - .currentWorkspace - ?.role, + .currentWorkspaceMember, ), ), ], @@ -117,7 +91,7 @@ class SettingsDialog extends StatelessWidget { String workspaceId, SettingsPage page, UserProfilePB user, - AFRolePB? currentWorkspaceMemberRole, + WorkspaceMemberPB? member, ) { switch (page) { case SettingsPage.account: @@ -129,7 +103,7 @@ class SettingsDialog extends StatelessWidget { case SettingsPage.workspace: return SettingsWorkspaceView( userProfile: user, - currentWorkspaceMemberRole: currentWorkspaceMemberRole, + workspaceMember: member, ); case SettingsPage.manageData: return SettingsManageDataView(userProfile: user); @@ -139,458 +113,16 @@ class SettingsDialog extends StatelessWidget { return SettingCloud(restartAppFlowy: () => restartApp()); case SettingsPage.shortcuts: return const SettingsShortcutsView(); - case SettingsPage.ai: - if (user.workspaceAuthType == AuthTypePB.Server) { - return SettingsAIView( - key: ValueKey(workspaceId), - userProfile: user, - currentWorkspaceMemberRole: currentWorkspaceMemberRole, - workspaceId: workspaceId, - ); - } else { - return LocalSettingsAIView( - key: ValueKey(workspaceId), - userProfile: user, - workspaceId: workspaceId, - ); - } case SettingsPage.member: - return WorkspaceMembersPage( - userProfile: user, - workspaceId: workspaceId, - ); + return WorkspaceMembersPage(userProfile: user); case SettingsPage.plan: - return SettingsPlanView( - workspaceId: workspaceId, - user: user, - ); + return SettingsPlanView(workspaceId: workspaceId, user: user); case SettingsPage.billing: - return SettingsBillingView( - workspaceId: workspaceId, - user: user, - ); - case SettingsPage.sites: - return SettingsSitesPage( - workspaceId: workspaceId, - user: user, - ); + return SettingsBillingView(workspaceId: workspaceId, user: user); case SettingsPage.featureFlags: return const FeatureFlagsPage(); - } - } -} - -class SimpleSettingsDialog extends StatefulWidget { - const SimpleSettingsDialog({super.key}); - - @override - State createState() => _SimpleSettingsDialogState(); -} - -class _SimpleSettingsDialogState extends State { - SettingsPage page = SettingsPage.cloud; - - @override - Widget build(BuildContext context) { - final settings = context.watch().state; - - return FlowyDialog( - width: MediaQuery.of(context).size.width * 0.7, - constraints: const BoxConstraints(maxWidth: 784, minWidth: 564), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // header - FlowyText( - LocaleKeys.signIn_settings.tr(), - fontSize: 36.0, - fontWeight: FontWeight.w600, - ), - const VSpace(18.0), - - // language - _LanguageSettings(key: ValueKey('language${settings.hashCode}')), - const VSpace(22.0), - - // self-host cloud - _SelfHostSettings(key: ValueKey('selfhost${settings.hashCode}')), - const VSpace(22.0), - - // support - _SupportSettings(key: ValueKey('support${settings.hashCode}')), - ], - ), - ), - ), - ); - } -} - -class _LanguageSettings extends StatelessWidget { - const _LanguageSettings({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_workspacePage_language_title.tr(), - children: const [LanguageDropdown()], - ); - } -} - -class _SelfHostSettings extends StatefulWidget { - const _SelfHostSettings({ - super.key, - }); - - @override - State<_SelfHostSettings> createState() => _SelfHostSettingsState(); -} - -class _SelfHostSettingsState extends State<_SelfHostSettings> { - final cloudUrlTextController = TextEditingController(); - final webUrlTextController = TextEditingController(); - - AuthenticatorType type = AuthenticatorType.appflowyCloud; - - @override - void initState() { - super.initState(); - - _fetchUrls(); - } - - @override - void dispose() { - cloudUrlTextController.dispose(); - webUrlTextController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_menu_cloudAppFlowy.tr(), - children: [ - Flexible( - child: SettingsServerDropdownMenu( - selectedServer: type, - onSelected: _onSelected, - ), - ), - if (type == AuthenticatorType.appflowyCloudSelfHost) _buildInputField(), - ], - ); - } - - Widget _buildInputField() { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _SelfHostUrlField( - textFieldKey: kSelfHostedTextInputFieldKey, - textController: cloudUrlTextController, - title: LocaleKeys.settings_menu_cloudURL.tr(), - hintText: LocaleKeys.settings_menu_cloudURLHint.tr(), - onSave: (url) => _saveUrl( - cloudUrl: url, - webUrl: webUrlTextController.text, - type: AuthenticatorType.appflowyCloudSelfHost, - ), - ), - const VSpace(12.0), - _SelfHostUrlField( - textFieldKey: kSelfHostedWebTextInputFieldKey, - textController: webUrlTextController, - title: LocaleKeys.settings_menu_webURL.tr(), - hintText: LocaleKeys.settings_menu_webURLHint.tr(), - hintBuilder: (context) => const WebUrlHintWidget(), - onSave: (url) => _saveUrl( - cloudUrl: cloudUrlTextController.text, - webUrl: url, - type: AuthenticatorType.appflowyCloudSelfHost, - ), - ), - const VSpace(12.0), - _buildSaveButton(), - ], - ); - } - - Widget _buildSaveButton() { - return Container( - height: 36, - constraints: const BoxConstraints(minWidth: 78), - child: OutlinedRoundedButton( - text: LocaleKeys.button_save.tr(), - onTap: () => _saveUrl( - cloudUrl: cloudUrlTextController.text, - webUrl: webUrlTextController.text, - type: AuthenticatorType.appflowyCloudSelfHost, - ), - ), - ); - } - - void _onSelected(AuthenticatorType type) { - if (type == this.type) { - return; - } - - Log.info('Switching server type to $type'); - - setState(() { - this.type = type; - }); - - if (type == AuthenticatorType.appflowyCloud) { - cloudUrlTextController.text = kAppflowyCloudUrl; - webUrlTextController.text = ShareConstants.defaultBaseWebDomain; - _saveUrl( - cloudUrl: kAppflowyCloudUrl, - webUrl: ShareConstants.defaultBaseWebDomain, - type: type, - ); - } - } - - Future _saveUrl({ - required String cloudUrl, - required String webUrl, - required AuthenticatorType type, - }) async { - if (cloudUrl.isEmpty || webUrl.isEmpty) { - showToastNotification( - message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), - type: ToastificationType.error, - ); - return; - } - - final isValid = await _validateUrl(cloudUrl) && await _validateUrl(webUrl); - - if (mounted) { - if (isValid) { - showToastNotification( - message: LocaleKeys.settings_menu_changeUrl.tr(args: [cloudUrl]), - ); - - Navigator.of(context).pop(); - - await useBaseWebDomain(webUrl); - await useAppFlowyBetaCloudWithURL(cloudUrl, type); - - await runAppFlowy(); - } else { - showToastNotification( - message: LocaleKeys.settings_menu_pleaseInputValidURL.tr(), - type: ToastificationType.error, - ); - } - } - } - - Future _validateUrl(String url) async { - return await validateUrl(url).fold( - (url) async { - return true; - }, - (err) { - Log.error(err); - return false; - }, - ); - } - - Future _fetchUrls() async { - await Future.wait([ - getAppFlowyCloudUrl(), - getAppFlowyShareDomain(), - ]).then((values) { - if (values.length != 2) { - return; - } - - cloudUrlTextController.text = values[0]; - webUrlTextController.text = values[1]; - - if (kAppflowyCloudUrl != values[0]) { - setState(() { - type = AuthenticatorType.appflowyCloudSelfHost; - }); - } - }); - } -} - -@visibleForTesting -extension SettingsServerDropdownMenuExtension on AuthenticatorType { - String get label { - switch (this) { - case AuthenticatorType.appflowyCloud: - return LocaleKeys.settings_menu_cloudAppFlowy.tr(); - case AuthenticatorType.appflowyCloudSelfHost: - return LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(); default: - throw Exception('Unsupported server type: $this'); + return const SizedBox.shrink(); } } } - -@visibleForTesting -class SettingsServerDropdownMenu extends StatelessWidget { - const SettingsServerDropdownMenu({ - super.key, - required this.selectedServer, - required this.onSelected, - }); - - final AuthenticatorType selectedServer; - final void Function(AuthenticatorType type) onSelected; - - // in the settings page from sign in page, we only support appflowy cloud and self-hosted - static final supportedServers = [ - AuthenticatorType.appflowyCloud, - AuthenticatorType.appflowyCloudSelfHost, - ]; - - @override - Widget build(BuildContext context) { - return SettingsDropdown( - expandWidth: false, - onChanged: onSelected, - selectedOption: selectedServer, - options: supportedServers - .map( - (serverType) => buildDropdownMenuEntry( - context, - selectedValue: selectedServer, - value: serverType, - label: serverType.label, - ), - ) - .toList(), - ); - } -} - -class _SupportSettings extends StatelessWidget { - const _SupportSettings({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return SettingsCategory( - title: LocaleKeys.settings_mobile_support.tr(), - children: [ - // export logs - Row( - children: [ - FlowyText( - LocaleKeys.workspace_errorActions_exportLogFiles.tr(), - ), - const Spacer(), - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 78), - child: OutlinedRoundedButton( - text: LocaleKeys.settings_files_export.tr(), - onTap: () { - shareLogFiles(context); - }, - ), - ), - ], - ), - // clear cache - Row( - children: [ - FlowyText( - LocaleKeys.settings_files_clearCache.tr(), - ), - const Spacer(), - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 78), - child: OutlinedRoundedButton( - text: LocaleKeys.button_clear.tr(), - onTap: () async { - await getIt().clearAllCache(); - if (context.mounted) { - showToastNotification( - message: LocaleKeys - .settings_manageDataPage_cache_dialog_successHint - .tr(), - ); - } - }, - ), - ), - ], - ), - ], - ); - } -} - -class _SelfHostUrlField extends StatelessWidget { - const _SelfHostUrlField({ - required this.textController, - required this.title, - required this.hintText, - required this.onSave, - this.textFieldKey, - this.hintBuilder, - }); - - final TextEditingController textController; - final String title; - final String hintText; - final ValueChanged onSave; - final Key? textFieldKey; - final WidgetBuilder? hintBuilder; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHintWidget(context), - const VSpace(6.0), - SizedBox( - height: 36, - child: FlowyTextField( - key: textFieldKey, - controller: textController, - autoFocus: false, - textStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - hintText: hintText, - onEditingComplete: () => onSave(textController.text), - ), - ), - ], - ); - } - - Widget _buildHintWidget(BuildContext context) { - return Row( - children: [ - FlowyText( - title, - overflow: TextOverflow.ellipsis, - ), - hintBuilder?.call(context) ?? const SizedBox.shrink(), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index 720f7793f2..c58e3ecc26 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -9,40 +9,14 @@ DropdownMenuEntry buildDropdownMenuEntry( BuildContext context, { required T value, required String label, - String subLabel = '', T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, String? fontFamily, - double maximumHeight = 29, }) { final fontFamilyUsed = fontFamily != null ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily : defaultFontFamily; - Widget? labelWidget; - if (subLabel.isNotEmpty) { - labelWidget = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.regular( - label, - fontSize: 14, - ), - const VSpace(4), - FlowyText.regular( - subLabel, - fontSize: 10, - ), - ], - ); - } else { - labelWidget = FlowyText.regular( - label, - fontSize: 14, - textAlign: TextAlign.start, - fontFamily: fontFamilyUsed, - ); - } return DropdownMenuEntry( style: ButtonStyle( @@ -52,12 +26,20 @@ DropdownMenuEntry buildDropdownMenuEntry( const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), - maximumSize: WidgetStatePropertyAll(Size(double.infinity, maximumHeight)), + maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), ), value: value, label: label, leadingIcon: leadingWidget, - labelWidget: labelWidget, + labelWidget: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: FlowyText.regular( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ), + ), trailingIcon: Row( children: [ if (trailingWidget != null) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart index 9892fd18a8..d770e2e3bb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/document_color_setting_button.dart @@ -1,14 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/util/color_to_hex_string.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flex_color_picker/flex_color_picker.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; @@ -94,11 +90,24 @@ class DocumentColorSettingDialogState late TextEditingController hexController; late TextEditingController opacityController; + void updateSelectedColor() { + if (widget.formKey.currentState!.validate()) { + setState(() { + final colorValue = int.tryParse( + hexController.text.combineHexWithOpacity(opacityController.text), + ); + // colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point + selectedColorOnDialog = Color(colorValue!); + widget.onChanged(selectedColorOnDialog!); + }); + } + } + @override void initState() { super.initState(); selectedColorOnDialog = widget.currentColor; - currentColorHexString = ColorExtension(widget.currentColor).toHexString(); + currentColorHexString = widget.currentColor.toHexString(); hexController = TextEditingController( text: currentColorHexString.extractHex(), ); @@ -136,31 +145,17 @@ class DocumentColorSettingDialogState controller: hexController, labelText: LocaleKeys.editor_hexValue.tr(), hintText: '6fc9e7', - onChanged: (_) => _updateSelectedColor(), - onFieldSubmitted: (_) => _updateSelectedColor(), + onChanged: (_) => updateSelectedColor(), + onFieldSubmitted: (_) => updateSelectedColor(), validator: (v) => validateHexValue(v, opacityController.text), - suffixIcon: Padding( - padding: const EdgeInsets.all(6.0), - child: FlowyIconButton( - onPressed: () => _showColorPickerDialog( - context: context, - currentColor: widget.currentColor, - updateColor: _updateColor, - ), - icon: const FlowySvg( - FlowySvgs.m_aa_color_s, - size: Size.square(20), - ), - ), - ), ), const VSpace(8), _ColorSettingTextField( controller: opacityController, labelText: LocaleKeys.editor_opacity.tr(), hintText: '50', - onChanged: (_) => _updateSelectedColor(), - onFieldSubmitted: (_) => _updateSelectedColor(), + onChanged: (_) => updateSelectedColor(), + onFieldSubmitted: (_) => updateSelectedColor(), validator: (value) => validateOpacityValue(value), ), ], @@ -169,28 +164,6 @@ class DocumentColorSettingDialogState ], ); } - - void _updateSelectedColor() { - if (widget.formKey.currentState!.validate()) { - setState(() { - final colorValue = int.tryParse( - hexController.text.combineHexWithOpacity(opacityController.text), - ); - // colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point - selectedColorOnDialog = Color(colorValue!); - widget.onChanged(selectedColorOnDialog!); - }); - } - } - - void _updateColor(Color color) { - setState(() { - hexController.text = ColorExtension(color).toHexString().extractHex(); - opacityController.text = - ColorExtension(color).toHexString().extractOpacity(); - }); - _updateSelectedColor(); - } } class _ColorSettingTextField extends StatelessWidget { @@ -199,7 +172,6 @@ class _ColorSettingTextField extends StatelessWidget { required this.labelText, required this.hintText, required this.onFieldSubmitted, - this.suffixIcon, this.onChanged, this.validator, }); @@ -208,7 +180,6 @@ class _ColorSettingTextField extends StatelessWidget { final String labelText; final String hintText; final void Function(String) onFieldSubmitted; - final Widget? suffixIcon; final void Function(String)? onChanged; final String? Function(String?)? validator; @@ -220,7 +191,6 @@ class _ColorSettingTextField extends StatelessWidget { decoration: InputDecoration( labelText: labelText, hintText: hintText, - suffixIcon: suffixIcon, border: OutlineInputBorder( borderSide: BorderSide(color: style.colorScheme.outline), ), @@ -271,159 +241,3 @@ String? validateOpacityValue(String? value) { } return null; } - -const _kColorCircleWidth = 32.0; -const _kColorCircleHeight = 32.0; -const _kColorCircleRadius = 20.0; -const _kColorOpacityThumbRadius = 23.0; -const _kDialogButtonPaddingHorizontal = 24.0; -const _kDialogButtonPaddingVertical = 12.0; -const _kColorsColumnSpacing = 12.0; - -class _ColorPicker extends StatelessWidget { - const _ColorPicker({ - required this.selectedColor, - required this.onColorChanged, - }); - - final Color selectedColor; - final void Function(Color) onColorChanged; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return ColorPicker( - width: _kColorCircleWidth, - height: _kColorCircleHeight, - borderRadius: _kColorCircleRadius, - enableOpacity: true, - opacityThumbRadius: _kColorOpacityThumbRadius, - columnSpacing: _kColorsColumnSpacing, - enableTooltips: false, - hasBorder: true, - borderColor: theme.colorScheme.outline, - pickersEnabled: const { - ColorPickerType.both: false, - ColorPickerType.primary: true, - ColorPickerType.accent: true, - ColorPickerType.wheel: true, - }, - subheading: Text( - LocaleKeys.settings_appearance_documentSettings_colorShade.tr(), - style: theme.textTheme.labelLarge, - ), - opacitySubheading: Text( - LocaleKeys.settings_appearance_documentSettings_opacity.tr(), - style: theme.textTheme.labelLarge, - ), - onColorChanged: onColorChanged, - ); - } -} - -class _ColorPickerActions extends StatelessWidget { - const _ColorPickerActions({ - required this.onReset, - required this.onUpdate, - }); - - final VoidCallback onReset; - final VoidCallback onUpdate; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 24, - child: FlowyTextButton( - LocaleKeys.button_cancel.tr(), - padding: const EdgeInsets.symmetric( - horizontal: _kDialogButtonPaddingHorizontal, - vertical: _kDialogButtonPaddingVertical, - ), - fontColor: AFThemeExtension.of(context).textColor, - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - radius: Corners.s12Border, - onPressed: onReset, - ), - ), - const HSpace(8), - SizedBox( - height: 48, - child: FlowyTextButton( - LocaleKeys.button_done.tr(), - padding: const EdgeInsets.symmetric( - horizontal: _kDialogButtonPaddingHorizontal, - vertical: _kDialogButtonPaddingVertical, - ), - radius: Corners.s12Border, - fontHoverColor: Colors.white, - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: const Color(0xFF005483), - onPressed: onUpdate, - ), - ), - ], - ); - } -} - -void _showColorPickerDialog({ - required BuildContext context, - String? title, - required Color currentColor, - required void Function(Color) updateColor, -}) { - Color selectedColor = currentColor; - - showDialog( - context: context, - barrierColor: const Color.fromARGB(128, 0, 0, 0), - builder: (context) => FlowyDialog( - expandHeight: false, - title: Row( - children: [ - const FlowySvg(FlowySvgs.m_aa_color_s), - const HSpace(12), - FlowyText( - title ?? - LocaleKeys.settings_appearance_documentSettings_pickColor.tr(), - fontSize: 20, - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _ColorPicker( - selectedColor: selectedColor, - onColorChanged: (color) => selectedColor = color, - ), - Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const HSpace(8), - _ColorPickerActions( - onReset: () { - updateColor(currentColor); - Navigator.of(context).pop(); - }, - onUpdate: () { - updateColor(selectedColor); - Navigator.of(context).pop(); - }, - ), - ], - ), - ), - ], - ), - ), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart index 68556f8294..2d56c9dce8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/flowy_gradient_button.dart @@ -1,6 +1,7 @@ -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; + class FlowyGradientButton extends StatefulWidget { const FlowyGradientButton({ super.key, @@ -48,7 +49,7 @@ class _FlowyGradientButtonState extends State { boxShadow: [ BoxShadow( blurRadius: 4, - color: Colors.black.withValues(alpha: 0.25), + color: Colors.black.withOpacity(0.25), offset: const Offset(0, 2), ), ], @@ -70,7 +71,7 @@ class _FlowyGradientButtonState extends State { ), ), child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 8), child: FlowyText( widget.label, fontSize: 16, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart index 9c1f0f4fc4..78d8d5223a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_list_tile.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SettingListTile extends StatelessWidget { const SettingListTile({ @@ -12,7 +13,6 @@ class SettingListTile extends StatelessWidget { required this.label, this.hint, this.trailing, - this.subtitle, this.onResetRequested, }); @@ -21,8 +21,7 @@ class SettingListTile extends StatelessWidget { final String? resetTooltipText; final Key? resetButtonKey; final List? trailing; - final List? subtitle; - final VoidCallback? onResetRequested; + final void Function()? onResetRequested; @override Widget build(BuildContext context) { @@ -46,46 +45,26 @@ class SettingListTile extends StatelessWidget { color: Theme.of(context).hintColor, ), ), - if (subtitle != null) ...subtitle!, ], ), ), if (trailing != null) ...trailing!, if (onResetRequested != null) - SettingsResetButton( + FlowyIconButton( + hoverColor: Theme.of(context).colorScheme.secondaryContainer, key: resetButtonKey, - resetTooltipText: resetTooltipText, - onResetRequested: onResetRequested, + width: 24, + icon: FlowySvg( + FlowySvgs.restore_s, + color: Theme.of(context).iconTheme.color, + size: const Size.square(20), + ), + iconColorOnHover: Theme.of(context).colorScheme.onPrimary, + tooltipText: resetTooltipText ?? + LocaleKeys.settings_appearance_resetSetting.tr(), + onPressed: onResetRequested, ), ], ); } } - -class SettingsResetButton extends StatelessWidget { - const SettingsResetButton({ - super.key, - this.resetTooltipText, - this.onResetRequested, - }); - - final String? resetTooltipText; - final VoidCallback? onResetRequested; - - @override - Widget build(BuildContext context) { - return FlowyIconButton( - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - width: 24, - icon: FlowySvg( - FlowySvgs.restore_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(20), - ), - iconColorOnHover: Theme.of(context).colorScheme.onPrimary, - tooltipText: - resetTooltipText ?? LocaleKeys.settings_appearance_resetSetting.tr(), - onPressed: onResetRequested, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart index 6c8eeb9ae4..ba6ae1416f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -12,8 +13,6 @@ class SettingValueDropDown extends StatefulWidget { this.child, this.popoverController, this.offset, - this.boxConstraints, - this.margin = const EdgeInsets.all(6), }); final String currentValue; @@ -23,8 +22,6 @@ class SettingValueDropDown extends StatefulWidget { final Widget? child; final PopoverController? popoverController; final Offset? offset; - final BoxConstraints? boxConstraints; - final EdgeInsets margin; @override State createState() => _SettingValueDropDownState(); @@ -37,14 +34,12 @@ class _SettingValueDropDownState extends State { key: widget.popoverKey, controller: widget.popoverController, direction: PopoverDirection.bottomWithCenterAligned, - margin: widget.margin, popupBuilder: widget.popupBuilder, - constraints: widget.boxConstraints ?? - const BoxConstraints( - minWidth: 80, - maxWidth: 160, - maxHeight: 400, - ), + constraints: const BoxConstraints( + minWidth: 80, + maxWidth: 160, + maxHeight: 400, + ), offset: widget.offset, onClose: widget.onClose, child: widget.child ?? diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart index c56b46eae0..4efc31b06d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_alert_dialog.dart @@ -1,10 +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:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; -import 'package:flutter/material.dart'; class SettingsAlertDialog extends StatefulWidget { const SettingsAlertDialog({ @@ -19,7 +21,6 @@ class SettingsAlertDialog extends StatefulWidget { this.hideCancelButton = false, this.isDangerous = false, this.implyLeading = false, - this.enableConfirmNotifier, }); final Widget? icon; @@ -31,7 +32,6 @@ class SettingsAlertDialog extends StatefulWidget { final String? confirmLabel; final bool hideCancelButton; final bool isDangerous; - final ValueNotifier? enableConfirmNotifier; /// If true, a back button will show in the top left corner final bool implyLeading; @@ -41,37 +41,6 @@ class SettingsAlertDialog extends StatefulWidget { } class _SettingsAlertDialogState extends State { - bool enableConfirm = true; - - @override - void initState() { - super.initState(); - if (widget.enableConfirmNotifier != null) { - widget.enableConfirmNotifier!.addListener(_updateEnableConfirm); - enableConfirm = widget.enableConfirmNotifier!.value; - } - } - - void _updateEnableConfirm() { - setState(() => enableConfirm = widget.enableConfirmNotifier!.value); - } - - @override - void dispose() { - if (widget.enableConfirmNotifier != null) { - widget.enableConfirmNotifier!.removeListener(_updateEnableConfirm); - } - super.dispose(); - } - - @override - void didUpdateWidget(covariant SettingsAlertDialog oldWidget) { - oldWidget.enableConfirmNotifier?.removeListener(_updateEnableConfirm); - widget.enableConfirmNotifier?.addListener(_updateEnableConfirm); - enableConfirm = widget.enableConfirmNotifier?.value ?? true; - super.didUpdateWidget(oldWidget); - } - @override Widget build(BuildContext context) { return StyledDialog( @@ -167,7 +136,6 @@ class _SettingsAlertDialogState extends State { cancel: widget.cancel, confirm: widget.confirm, isDangerous: widget.isDangerous, - enableConfirm: enableConfirm, ), ], ), @@ -182,7 +150,6 @@ class _Actions extends StatelessWidget { this.cancel, this.confirm, this.isDangerous = false, - this.enableConfirm = true, }); final bool hideCancelButton; @@ -190,7 +157,6 @@ class _Actions extends StatelessWidget { final VoidCallback? cancel; final VoidCallback? confirm; final bool isDangerous; - final bool enableConfirm; @override Widget build(BuildContext context) { @@ -199,16 +165,18 @@ class _Actions extends StatelessWidget { children: [ if (!hideCancelButton) ...[ SizedBox( - height: 48, - child: PrimaryRoundedButton( - text: LocaleKeys.button_cancel.tr(), - margin: const EdgeInsets.symmetric( + height: 24, + child: FlowyTextButton( + LocaleKeys.button_cancel.tr(), + padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 12, ), - fontWeight: FontWeight.w600, - radius: 12.0, - onTap: () { + fontColor: AFThemeExtension.of(context).textColor, + fillColor: Colors.transparent, + hoverColor: Colors.transparent, + radius: Corners.s12Border, + onPressed: () { cancel?.call(); Navigator.of(context).pop(); }, @@ -229,18 +197,14 @@ class _Actions extends StatelessWidget { ), radius: Corners.s12Border, fontColor: isDangerous ? Colors.white : null, - fontHoverColor: !enableConfirm ? null : Colors.white, - fillColor: !enableConfirm - ? Theme.of(context).dividerColor - : isDangerous - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.primary, - hoverColor: !enableConfirm - ? Theme.of(context).dividerColor - : isDangerous - ? Theme.of(context).colorScheme.error - : const Color(0xFF005483), - onPressed: enableConfirm ? confirm : null, + fontHoverColor: Colors.white, + fillColor: isDangerous + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.primary, + hoverColor: isDangerous + ? Theme.of(context).colorScheme.error + : const Color(0xFF005483), + onPressed: confirm, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart index 5114218041..041822947f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_body.dart @@ -1,21 +1,20 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_header.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; class SettingsBody extends StatelessWidget { const SettingsBody({ super.key, required this.title, this.description, - this.descriptionBuilder, this.autoSeparate = true, required this.children, }); final String title; final String? description; - final WidgetBuilder? descriptionBuilder; final bool autoSeparate; final List children; @@ -28,18 +27,13 @@ class SettingsBody extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - SettingsHeader( - title: title, - description: description, - descriptionBuilder: descriptionBuilder, - ), - SettingsCategorySpacer(), + SettingsHeader(title: title, description: description), Flexible( child: SeparatedColumn( mainAxisSize: MainAxisSize.min, separatorBuilder: () => autoSeparate ? const SettingsCategorySpacer() - : const SizedBox.shrink(), + : const VSpace(16), crossAxisAlignment: CrossAxisAlignment.start, children: children, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart index 33c81b99e8..34f25dd41e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category.dart @@ -1,8 +1,9 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; + /// Renders a simple category taking a title and the list /// of children (settings) to be rendered. /// @@ -11,7 +12,6 @@ class SettingsCategory extends StatelessWidget { super.key, required this.title, this.description, - this.descriptionColor, this.tooltip, this.actions, required this.children, @@ -19,25 +19,21 @@ class SettingsCategory extends StatelessWidget { final String title; final String? description; - final Color? descriptionColor; final String? tooltip; final List? actions; final List children; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Text( + FlowyText.semibold( title, - style: theme.textStyle.heading4.enhanced( - color: theme.textColorScheme.primary, - ), maxLines: 2, + fontSize: 16, overflow: TextOverflow.ellipsis, ), if (tooltip != null) ...[ @@ -51,14 +47,13 @@ class SettingsCategory extends StatelessWidget { if (actions != null) ...actions!, ], ), - const VSpace(16), + const VSpace(8), if (description?.isNotEmpty ?? false) ...[ FlowyText.regular( description!, maxLines: 4, fontSize: 12, overflow: TextOverflow.ellipsis, - color: descriptionColor, ), const VSpace(8), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart index 1ef7f13d0c..5637fdd20c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_category_spacer.dart @@ -1,4 +1,3 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:flutter/material.dart'; /// This is used to create a uniform space and divider @@ -8,11 +7,6 @@ class SettingsCategorySpacer extends StatelessWidget { const SettingsCategorySpacer({super.key}); @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Divider( - height: theme.spacing.xl * 2.0, - color: theme.borderColorScheme.primary, - ); - } + Widget build(BuildContext context) => + const Divider(height: 32, color: Color(0xFFF2F2F2)); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index e392ed91f0..91d8e1b3e7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/flutter/af_dropdown_menu.dart'; import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -5,7 +7,6 @@ import 'package:appflowy/workspace/application/settings/appearance/base_appearan import 'package:collection/collection.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsDropdown extends StatefulWidget { @@ -16,11 +17,9 @@ class SettingsDropdown extends StatefulWidget { this.onChanged, this.actions, this.expandWidth = true, - this.selectOptionCompare, }); final T selectedOption; - final CompareFunction? selectOptionCompare; final List> options; final void Function(T)? onChanged; final List? actions; @@ -54,7 +53,6 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, - selectOptionCompare: widget.selectOptionCompare, textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith( fontFamily: fontFamilyUsed, fontWeight: FontWeight.w400, @@ -64,7 +62,7 @@ class _SettingsDropdownState extends State> { const WidgetStatePropertyAll(Size(double.infinity, 250)), elevation: const WidgetStatePropertyAll(10), shadowColor: - WidgetStatePropertyAll(Colors.black.withValues(alpha: 0.4)), + WidgetStatePropertyAll(Colors.black.withOpacity(0.4)), backgroundColor: WidgetStatePropertyAll( Theme.of(context).cardColor, ), @@ -72,6 +70,7 @@ class _SettingsDropdownState extends State> { EdgeInsets.symmetric(horizontal: 6, vertical: 8), ), alignment: Alignment.bottomLeft, + visualDensity: VisualDensity.compact, ), inputDecorationTheme: InputDecorationTheme( contentPadding: const EdgeInsets.symmetric( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart index 332b25e686..c028e6886d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_header.dart @@ -1,46 +1,32 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + /// Renders a simple header for the settings view /// class SettingsHeader extends StatelessWidget { - const SettingsHeader({ - super.key, - required this.title, - this.description, - this.descriptionBuilder, - }); + const SettingsHeader({super.key, required this.title, this.description}); final String title; final String? description; - final WidgetBuilder? descriptionBuilder; @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: theme.textStyle.heading2.enhanced( - color: theme.textColorScheme.primary, - ), - ), - if (descriptionBuilder != null) ...[ - VSpace(theme.spacing.xs), - descriptionBuilder!(context), - ] else if (description?.isNotEmpty == true) ...[ - VSpace(theme.spacing.xs), - Text( + FlowyText.semibold(title, fontSize: 24), + if (description?.isNotEmpty == true) ...[ + const VSpace(8), + FlowyText( description!, maxLines: 4, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.secondary, - ), + fontSize: 12, + color: AFThemeExtension.of(context).secondaryTextColor, ), ], + const VSpace(16), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart index d5a81655a5..a41e98dfc8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_input_field.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; /// This is used to describe a settings input field /// diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart index 6b0c920a04..8fc8e33280 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/single_setting_action.dart @@ -1,19 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; - -enum SingleSettingsButtonType { - primary, - danger, - highlight; - - bool get isPrimary => this == primary; - bool get isDangerous => this == danger; - bool get isHighlight => this == highlight; -} /// This is used to describe a single setting action /// @@ -27,18 +18,15 @@ class SingleSettingAction extends StatelessWidget { const SingleSettingAction({ super.key, required this.label, - this.description, this.labelMaxLines, required this.buttonLabel, this.onPressed, - this.buttonType = SingleSettingsButtonType.primary, + this.isDangerous = false, this.fontSize = 14, this.fontWeight = FontWeight.normal, - this.minWidth, }); final String label; - final String? description; final int? labelMaxLines; final String buttonLabel; @@ -48,120 +36,46 @@ class SingleSettingAction extends StatelessWidget { /// final VoidCallback? onPressed; - final SingleSettingsButtonType buttonType; + /// If isDangerous is true, the button will be rendered as a dangerous + /// action, with a red outline. + /// + final bool isDangerous; final double fontSize; final FontWeight fontWeight; - final double? minWidth; @override Widget build(BuildContext context) { return Row( children: [ Expanded( - child: Column( - children: [ - Row( - children: [ - Expanded( - child: FlowyText( - label, - fontSize: fontSize, - fontWeight: fontWeight, - maxLines: labelMaxLines, - overflow: TextOverflow.ellipsis, - color: AFThemeExtension.of(context).secondaryTextColor, - ), - ), - ], - ), - if (description != null) ...[ - const VSpace(4), - Row( - children: [ - Expanded( - child: FlowyText.regular( - description!, - fontSize: 11, - color: AFThemeExtension.of(context).secondaryTextColor, - maxLines: 2, - ), - ), - ], - ), - ], - ], + child: FlowyText( + label, + fontSize: fontSize, + fontWeight: fontWeight, + maxLines: labelMaxLines, + overflow: TextOverflow.ellipsis, + color: AFThemeExtension.of(context).secondaryTextColor, ), ), const HSpace(24), - ConstrainedBox( - constraints: BoxConstraints( - minWidth: minWidth ?? 0.0, - maxHeight: 32, - minHeight: 32, - ), + SizedBox( + height: 32, child: FlowyTextButton( buttonLabel, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 7), - fillColor: fillColor(context), - radius: Corners.s8Border, - hoverColor: hoverColor(context), - fontColor: fontColor(context), - fontHoverColor: fontHoverColor(context), - borderColor: borderColor(context), + fillColor: + isDangerous ? null : Theme.of(context).colorScheme.primary, + radius: Corners.s12Border, + hoverColor: isDangerous ? null : const Color(0xFF005483), + fontColor: isDangerous ? Theme.of(context).colorScheme.error : null, + fontHoverColor: Colors.white, fontSize: 12, - isDangerous: buttonType.isDangerous, + isDangerous: isDangerous, onPressed: onPressed, - lineHeight: 1.0, ), ), ], ); } - - Color? fillColor(BuildContext context) { - if (buttonType.isPrimary) { - return Theme.of(context).colorScheme.primary; - } - return Colors.transparent; - } - - Color? hoverColor(BuildContext context) { - if (buttonType.isDangerous) { - return Theme.of(context).colorScheme.error.withValues(alpha: 0.1); - } - - if (buttonType.isPrimary) { - return Theme.of(context).colorScheme.primary.withValues(alpha: 0.9); - } - - if (buttonType.isHighlight) { - return const Color(0xFF5C3699); - } - return null; - } - - Color? fontColor(BuildContext context) { - if (buttonType.isDangerous) { - return Theme.of(context).colorScheme.error; - } - - if (buttonType.isHighlight) { - return const Color(0xFF5C3699); - } - - return Theme.of(context).colorScheme.onPrimary; - } - - Color? fontHoverColor(BuildContext context) { - return Colors.white; - } - - Color? borderColor(BuildContext context) { - if (buttonType.isHighlight) { - return const Color(0xFF5C3699); - } - - return null; - } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart index d8aa15a944..17f1582c90 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/_restart_app_button.dart @@ -1,9 +1,9 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/sign_in_or_logout_button.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' show PlatformExtension; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; class RestartButton extends StatelessWidget { const RestartButton({ @@ -17,7 +17,7 @@ class RestartButton extends StatelessWidget { @override Widget build(BuildContext context) { - final List children = [_buildRestartButton(context)]; + final List children = [_buildRestartButton()]; if (showRestartHint) { children.add( Padding( @@ -33,42 +33,28 @@ class RestartButton extends StatelessWidget { return Column(children: children); } - Widget _buildRestartButton(BuildContext context) { - if (UniversalPlatform.isDesktopOrWeb) { + Widget _buildRestartButton() { + if (PlatformExtension.isDesktopOrWeb) { return Row( children: [ - SizedBox( - height: 42, - child: PrimaryRoundedButton( - text: LocaleKeys.settings_menu_restartApp.tr(), - margin: const EdgeInsets.symmetric(horizontal: 24), - fontWeight: FontWeight.w600, - radius: 12.0, - onTap: onClick, + FlowyButton( + isSelected: true, + useIntrinsicWidth: true, + margin: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 10, ), + text: FlowyText( + LocaleKeys.settings_menu_restartApp.tr(), + ), + onTap: onClick, ), + const Spacer(), ], ); - // Row( - // children: [ - // FlowyButton( - // isSelected: true, - // useIntrinsicWidth: true, - // margin: const EdgeInsets.symmetric( - // horizontal: 30, - // vertical: 10, - // ), - // text: FlowyText( - // LocaleKeys.settings_menu_restartApp.tr(), - // ), - // onTap: onClick, - // ), - // const Spacer(), - // ], - // ); } else { - return MobileLogoutButton( - text: LocaleKeys.settings_menu_restartApp.tr(), + return MobileSignInOrLogoutButton( + labelText: LocaleKeys.settings_menu_restartApp.tr(), onPressed: onClick, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart deleted file mode 100644 index e606292572..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/cancel_plan_survey_dialog.dart +++ /dev/null @@ -1,431 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; - -Future showCancelSurveyDialog(BuildContext context) { - return showDialog( - context: context, - builder: (_) => const _Survey(), - ); -} - -class _Survey extends StatefulWidget { - const _Survey(); - - @override - State<_Survey> createState() => _SurveyState(); -} - -class _SurveyState extends State<_Survey> { - final PageController pageController = PageController(); - final Map answers = {}; - - @override - void dispose() { - pageController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 674, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Survey title - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: FlowyText( - LocaleKeys.settings_cancelSurveyDialog_title.tr(), - fontSize: 22.0, - overflow: TextOverflow.ellipsis, - color: AFThemeExtension.of(context).strongText, - ), - ), - FlowyButton( - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.upgrade_close_s), - onTap: () => Navigator.of(context).pop(), - ), - ], - ), - const VSpace(12), - // Survey explanation - FlowyText( - LocaleKeys.settings_cancelSurveyDialog_description.tr(), - maxLines: 3, - ), - const VSpace(8), - const Divider(), - const VSpace(8), - // Question "sheet" - SizedBox( - height: 400, - width: 650, - child: PageView.builder( - controller: pageController, - itemCount: _questionsAndAnswers.length, - itemBuilder: (context, index) => _QAPage( - qa: _questionsAndAnswers[index], - isFirstQuestion: index == 0, - isFinalQuestion: - index == _questionsAndAnswers.length - 1, - selectedAnswer: - answers[_questionsAndAnswers[index].question], - onPrevious: () { - if (index > 0) { - pageController.animateToPage( - index - 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - onAnswerChanged: (answer) { - answers[_questionsAndAnswers[index].question] = - answer; - }, - onAnswerSelected: (answer) { - answers[_questionsAndAnswers[index].question] = - answer; - - if (index == _questionsAndAnswers.length - 1) { - Navigator.of(context).pop(jsonEncode(answers)); - } else { - pageController.animateToPage( - index + 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - } -} - -class _QAPage extends StatefulWidget { - const _QAPage({ - required this.qa, - required this.onAnswerSelected, - required this.onAnswerChanged, - required this.onPrevious, - this.selectedAnswer, - this.isFirstQuestion = false, - this.isFinalQuestion = false, - }); - - final _QA qa; - final String? selectedAnswer; - - /// Called when "Next" is pressed - /// - final Function(String) onAnswerSelected; - - /// Called whenever an answer is selected or changed - /// - final Function(String) onAnswerChanged; - final VoidCallback onPrevious; - final bool isFirstQuestion; - final bool isFinalQuestion; - - @override - State<_QAPage> createState() => _QAPageState(); -} - -class _QAPageState extends State<_QAPage> { - final otherController = TextEditingController(); - - int _selectedIndex = -1; - String? answer; - - @override - void initState() { - super.initState(); - if (widget.selectedAnswer != null) { - answer = widget.selectedAnswer; - _selectedIndex = widget.qa.answers.indexOf(widget.selectedAnswer!); - if (_selectedIndex == -1) { - // We assume the last question is "Other" - _selectedIndex = widget.qa.answers.length - 1; - otherController.text = widget.selectedAnswer!; - } - } - } - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText( - widget.qa.question, - fontSize: 16.0, - color: AFThemeExtension.of(context).strongText, - ), - const VSpace(18), - SeparatedColumn( - separatorBuilder: () => const VSpace(6), - crossAxisAlignment: CrossAxisAlignment.start, - children: widget.qa.answers - .mapIndexed( - (index, option) => _AnswerOption( - prefix: _indexToLetter(index), - option: option, - isSelected: _selectedIndex == index, - onTap: () => setState(() { - _selectedIndex = index; - if (_selectedIndex == widget.qa.answers.length - 1 && - widget.qa.lastIsOther) { - answer = otherController.text; - } else { - answer = option; - } - widget.onAnswerChanged(option); - }), - ), - ) - .toList(), - ), - if (widget.qa.lastIsOther && - _selectedIndex == widget.qa.answers.length - 1) ...[ - const VSpace(8), - FlowyTextField( - controller: otherController, - hintText: LocaleKeys.settings_cancelSurveyDialog_otherHint.tr(), - onChanged: (value) => setState(() { - answer = value; - widget.onAnswerChanged(value); - }), - ), - ], - const VSpace(20), - Row( - children: [ - if (!widget.isFirstQuestion) ...[ - DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: const BorderSide(color: Color(0x1E14171B)), - borderRadius: BorderRadius.circular(8), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 9.0, - ), - text: FlowyText.regular(LocaleKeys.button_previous.tr()), - onTap: widget.onPrevious, - ), - ), - const HSpace(12.0), - ], - DecoratedBox( - decoration: ShapeDecoration( - color: Theme.of(context).colorScheme.primary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0), - radius: BorderRadius.circular(8), - text: FlowyText.regular( - widget.isFinalQuestion - ? LocaleKeys.button_submit.tr() - : LocaleKeys.button_next.tr(), - color: Colors.white, - ), - disable: !canProceed(), - onTap: canProceed() - ? () => widget.onAnswerSelected( - answer ?? widget.qa.answers[_selectedIndex], - ) - : null, - ), - ), - ], - ), - ], - ); - } - - bool canProceed() { - if (_selectedIndex == widget.qa.answers.length - 1 && - widget.qa.lastIsOther) { - return answer != null && - answer!.isNotEmpty && - answer != LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(); - } - - return _selectedIndex != -1; - } -} - -class _AnswerOption extends StatelessWidget { - const _AnswerOption({ - required this.prefix, - required this.option, - required this.onTap, - this.isSelected = false, - }); - - final String prefix; - final String option; - final VoidCallback onTap; - final bool isSelected; - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onTap, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - borderRadius: Corners.s8Border, - border: Border.all( - width: 2, - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const HSpace(2), - Container( - width: 24, - height: 24, - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).dividerColor, - borderRadius: Corners.s6Border, - ), - child: Center( - child: FlowyText( - prefix, - color: isSelected ? Colors.white : null, - ), - ), - ), - const HSpace(8), - FlowyText( - option, - fontWeight: FontWeight.w400, - fontSize: 16.0, - color: AFThemeExtension.of(context).strongText, - ), - const HSpace(6), - ], - ), - ), - ), - ); - } -} - -final _questionsAndAnswers = [ - _QA( - question: LocaleKeys.settings_cancelSurveyDialog_questionOne_question.tr(), - answers: [ - LocaleKeys.settings_cancelSurveyDialog_questionOne_answerOne.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionOne_answerTwo.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionOne_answerThree.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFour.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionOne_answerFive.tr(), - LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(), - ], - lastIsOther: true, - ), - _QA( - question: LocaleKeys.settings_cancelSurveyDialog_questionTwo_question.tr(), - answers: [ - LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerOne.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerTwo.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerThree.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFour.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionTwo_answerFive.tr(), - ], - ), - _QA( - question: - LocaleKeys.settings_cancelSurveyDialog_questionThree_question.tr(), - answers: [ - LocaleKeys.settings_cancelSurveyDialog_questionThree_answerOne.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionThree_answerTwo.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionThree_answerThree.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionThree_answerFour.tr(), - LocaleKeys.settings_cancelSurveyDialog_commonOther.tr(), - ], - lastIsOther: true, - ), - _QA( - question: LocaleKeys.settings_cancelSurveyDialog_questionFour_question.tr(), - answers: [ - LocaleKeys.settings_cancelSurveyDialog_questionFour_answerOne.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionFour_answerTwo.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionFour_answerThree.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFour.tr(), - LocaleKeys.settings_cancelSurveyDialog_questionFour_answerFive.tr(), - ], - ), -]; - -class _QA { - const _QA({ - required this.question, - required this.answers, - this.lastIsOther = false, - }); - - final String question; - final List answers; - final bool lastIsOther; -} - -/// Returns the letter corresponding to the index. -/// -/// Eg. 0 -> A, 1 -> B, 2 -> C, ..., and so forth. -/// -String _indexToLetter(int index) { - return String.fromCharCode(65 + index); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart new file mode 100644 index 0000000000..f57c25b351 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -0,0 +1,122 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_svg_widget.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +SelectionMenuItem emojiMenuItem = SelectionMenuItem( + getName: LocaleKeys.document_plugins_emoji.tr, + icon: (editorState, onSelected, style) => SelectableIconWidget( + icon: Icons.emoji_emotions_outlined, + isSelected: onSelected, + style: style, + ), + keywords: ['emoji'], + handler: (editorState, menuService, context) { + final container = Overlay.of(context); + menuService.dismiss(); + showEmojiPickerMenu( + container, + editorState, + menuService.alignment, + menuService.offset, + ); + }, +); + +void showEmojiPickerMenu( + OverlayState container, + EditorState editorState, + Alignment alignment, + Offset offset, +) { + final top = alignment == Alignment.topLeft ? offset.dy : null; + final bottom = alignment == Alignment.bottomLeft ? offset.dy : null; + + keepEditorFocusNotifier.increase(); + late OverlayEntry emojiPickerMenuEntry; + emojiPickerMenuEntry = FullScreenOverlayEntry( + top: top, + bottom: bottom, + left: offset.dx, + dismissCallback: () => keepEditorFocusNotifier.decrease(), + builder: (context) => Material( + type: MaterialType.transparency, + child: Container( + width: 360, + height: 380, + padding: const EdgeInsets.all(4.0), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + ), + child: EmojiSelectionMenu( + onSubmitted: (emoji) { + editorState.insertTextAtCurrentSelection(emoji); + }, + onExit: () { + // close emoji panel + emojiPickerMenuEntry.remove(); + }, + ), + ), + ), + ).build(); + container.insert(emojiPickerMenuEntry); +} + +class EmojiSelectionMenu extends StatefulWidget { + const EmojiSelectionMenu({ + super.key, + required this.onSubmitted, + required this.onExit, + }); + + final void Function(String emoji) onSubmitted; + final void Function() onExit; + + @override + State createState() => _EmojiSelectionMenuState(); +} + +class _EmojiSelectionMenuState extends State { + @override + void initState() { + HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent); + super.initState(); + } + + bool _handleGlobalKeyEvent(KeyEvent event) { + if (event.logicalKey == LogicalKeyboardKey.escape && + event is KeyDownEvent) { + //triggers on esc + widget.onExit(); + return true; + } else { + return false; + } + } + + @override + void deactivate() { + HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent); + super.deactivate(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FlowyEmojiPicker( + onEmojiSelected: (_, emoji) { + widget.onSubmitted(emoji); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart index 6e1f6e239f..a369cc6b87 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart @@ -1,3 +1,4 @@ +export 'emoji_menu_item.dart'; export 'emoji_shortcut_event.dart'; export 'src/emji_picker_config.dart'; export 'src/emoji_picker.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index 6959f69788..078cf64963 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -1,7 +1,5 @@ -import 'package:appflowy/plugins/emoji/emoji_actions_command.dart'; -import 'package:appflowy/plugins/emoji/emoji_menu.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:flutter/material.dart'; final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( @@ -17,16 +15,73 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { if (selection == null) { return KeyEventResult.ignored; } - final node = editorState.getNodeAtPath(selection.start.path); - final context = node?.context; - if (node == null || - context == null || - node.delta == null || - node.type == CodeBlockKeys.type) { + final context = editorState.getNodeAtPath(selection.start.path)?.context; + if (context == null) { return KeyEventResult.ignored; } + final container = Overlay.of(context); - emojiMenuService = EmojiMenu(editorState: editorState, overlay: container); - emojiMenuService?.show(''); + + Alignment alignment = Alignment.topLeft; + Offset offset = Offset.zero; + + final selectionService = editorState.service.selectionService; + final selectionRects = selectionService.selectionRects; + if (selectionRects.isEmpty) { + return KeyEventResult.ignored; + } + final rect = selectionRects.first; + + // Calculate the offset and alignment + // Don't like these values being hardcoded but unsure how to grab the + // values dynamically to match the /emoji command. + const menuHeight = 200.0; + const menuOffset = Offset(10, 10); // Tried (0, 10) but that looked off + + final editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorHeight = editorState.renderBox!.size.height; + final editorWidth = editorState.renderBox!.size.width; + + // show below default + alignment = Alignment.topLeft; + final bottomRight = rect.bottomRight; + final topRight = rect.topRight; + final newOffset = bottomRight + menuOffset; + offset = Offset( + newOffset.dx, + newOffset.dy, + ); + + // show above + if (newOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { + offset = topRight - menuOffset; + alignment = Alignment.bottomLeft; + + offset = Offset( + newOffset.dx, + MediaQuery.of(context).size.height - newOffset.dy, + ); + } + + // show on left + if (offset.dx - editorOffset.dx > editorWidth / 2) { + alignment = alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + offset = Offset( + editorWidth - offset.dx + editorOffset.dx, + offset.dy, + ); + } + + showEmojiPickerMenu( + container, + editorState, + alignment, + offset, + ); + return KeyEventResult.handled; }; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart index 9a4690f240..f2bdbf4faa 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart @@ -1,8 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; - +import 'package:flutter/material.dart'; import 'emoji_picker.dart'; import 'emoji_picker_builder.dart'; import 'models/emoji_category_models.dart'; @@ -31,10 +29,8 @@ class DefaultEmojiPickerViewState extends State @override void initState() { - super.initState(); - - int initCategory = widget.state.emojiCategoryGroupList.indexWhere( - (el) => el.category == widget.config.initCategory, + var initCategory = widget.state.emojiCategoryGroupList.indexWhere( + (element) => element.category == widget.config.initCategory, ); if (initCategory == -1) { initCategory = 0; @@ -46,12 +42,31 @@ class DefaultEmojiPickerViewState extends State ); _pageController = PageController(initialPage: initCategory); _emojiFocusNode.requestFocus(); - _emojiController.addListener(_onEmojiChanged); + + _emojiController.addListener(() { + final String query = _emojiController.text.toLowerCase(); + if (query.isEmpty) { + searchEmojiList.emoji.clear(); + _pageController!.jumpToPage( + _tabController!.index, + ); + } else { + searchEmojiList.emoji.clear(); + for (final element in widget.state.emojiCategoryGroupList) { + searchEmojiList.emoji.addAll( + element.emoji.where((item) { + return item.name.toLowerCase().contains(query); + }).toList(), + ); + } + } + setState(() {}); + }); + super.initState(); } @override void dispose() { - _emojiController.removeListener(_onEmojiChanged); _emojiController.dispose(); _emojiFocusNode.dispose(); _pageController?.dispose(); @@ -60,41 +75,31 @@ class DefaultEmojiPickerViewState extends State super.dispose(); } - void _onEmojiChanged() { - final String query = _emojiController.text.toLowerCase(); - if (query.isEmpty) { - searchEmojiList.emoji.clear(); - _pageController!.jumpToPage(_tabController!.index); - } else { - searchEmojiList.emoji.clear(); - for (final element in widget.state.emojiCategoryGroupList) { - searchEmojiList.emoji.addAll( - element.emoji - .where((item) => item.name.toLowerCase().contains(query)) - .toList(), - ); - } - } - setState(() {}); - } - Widget _buildBackspaceButton() { if (widget.state.onBackspacePressed != null) { return Material( type: MaterialType.transparency, child: IconButton( padding: const EdgeInsets.only(bottom: 2), - icon: Icon(Icons.backspace, color: widget.config.backspaceColor), - onPressed: () => widget.state.onBackspacePressed!(), + icon: Icon( + Icons.backspace, + color: widget.config.backspaceColor, + ), + onPressed: () { + widget.state.onBackspacePressed!(); + }, ), ); } - return const SizedBox.shrink(); } - bool isEmojiSearching() => - searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; + bool isEmojiSearching() { + final bool result = + searchEmojiList.emoji.isNotEmpty || _emojiController.text.isNotEmpty; + + return result; + } @override Widget build(BuildContext context) { @@ -208,9 +213,15 @@ class DefaultEmojiPickerViewState extends State required Widget child, }) { if (widget.config.buttonMode == ButtonMode.MATERIAL) { - return InkWell(onTap: onPressed, child: child); + return InkWell( + onTap: onPressed, + child: child, + ); } - return GestureDetector(onTap: onPressed, child: child); + return GestureDetector( + onTap: onPressed, + child: child, + ); } Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) { @@ -264,7 +275,9 @@ class DefaultEmojiPickerViewState extends State child: FittedBox( child: Text( emoji.emoji, - style: TextStyle(fontSize: emojiSize), + style: TextStyle( + fontSize: emojiSize, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart index f329e9dd1c..22d2bbe034 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/emji_picker_config.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'emoji_picker.dart'; import 'models/emoji_category_models.dart'; +import 'emoji_picker.dart'; part 'emji_picker_config.freezed.dart'; @@ -87,6 +87,8 @@ class EmojiPickerConfig with _$EmojiPickerConfig { return emojiCategoryIcons.flagIcon; case EmojiCategory.SEARCH: return emojiCategoryIcons.searchIcon; + default: + throw Exception('Unsupported EmojiCategory'); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart index 7c8e128ec6..decf74f874 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/util/navigator_context_extension.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; import 'package:appflowy/workspace/application/settings/share/export_service.dart'; @@ -126,14 +125,16 @@ class _FileExporterWidgetState extends State { ); } } - } else if (mounted) { + } else { showSnackBarMessage( context, LocaleKeys.settings_files_exportFileFail.tr(), ); } if (mounted) { - context.popToHome(); + Navigator.of(context).popUntil( + (router) => router.settings.name == '/', + ); } }); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart deleted file mode 100644 index 6f143a83c1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class InviteMemberByLink extends StatelessWidget { - const InviteMemberByLink({super.key}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _Title(), - _Description(), - ], - ), - Spacer(), - _CopyLinkButton(), - ], - ); - } -} - -class _Title extends StatelessWidget { - const _Title(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text( - LocaleKeys.settings_appearance_members_inviteLinkToAddMember.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ); - } -} - -class _Description extends StatelessWidget { - const _Description(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text.rich( - TextSpan( - children: [ - TextSpan( - text: LocaleKeys.settings_appearance_members_clickToCopyLink.tr(), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: ' ${LocaleKeys.settings_appearance_members_or.tr()} ', - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.primary, - ), - ), - TextSpan( - text: LocaleKeys.settings_appearance_members_generateANewLink.tr(), - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.action, - ), - mouseCursor: SystemMouseCursors.click, - recognizer: TapGestureRecognizer() - ..onTap = () => _onGenerateInviteLink(context), - ), - ], - ), - ); - } - - Future _onGenerateInviteLink(BuildContext context) async { - final inviteLink = context.read().state.inviteLink; - if (inviteLink != null) { - // show a dialog to confirm if the user wants to copy the link to the clipboard - await showConfirmDialog( - context: context, - style: ConfirmPopupStyle.cancelAndOk, - title: 'Reset the invite link?', - description: - 'Resetting will deactivate the current link for all space members and generate a new one. The old link will no longer be available.', - confirmLabel: 'Reset', - onConfirm: () { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - }, - confirmButtonBuilder: (_) => AFFilledTextButton.destructive( - text: 'Reset', - onTap: () { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - - Navigator.of(context).pop(); - }, - ), - ); - } else { - context.read().add( - const WorkspaceMemberEvent.generateInviteLink(), - ); - } - } -} - -class _CopyLinkButton extends StatelessWidget { - const _CopyLinkButton(); - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return AFOutlinedTextButton.normal( - text: LocaleKeys.button_copyLink.tr(), - textStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.l, - vertical: theme.spacing.s, - ), - onTap: () { - final link = context.read().state.inviteLink; - if (link != null) { - getIt().setData( - ClipboardServiceData( - plainText: link, - ), - ); - - showToastNotification( - message: LocaleKeys.document_inlineLink_copyLink.tr(), - ); - } else { - showToastNotification( - message: LocaleKeys.shareAction_copyLinkFailed.tr(), - ); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart deleted file mode 100644 index 9f8ce45a97..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_ui/appflowy_ui.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:string_validator/string_validator.dart'; - -class InviteMemberByEmail extends StatefulWidget { - const InviteMemberByEmail({super.key}); - - @override - State createState() => _InviteMemberByEmailState(); -} - -class _InviteMemberByEmailState extends State { - final _emailController = TextEditingController(); - - @override - void dispose() { - _emailController.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - LocaleKeys.settings_appearance_members_inviteMemberByEmail.tr(), - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), - ), - VSpace(theme.spacing.m), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: AFTextField( - controller: _emailController, - hintText: - LocaleKeys.settings_appearance_members_inviteHint.tr(), - onSubmitted: (value) => _inviteMember(), - ), - ), - HSpace(theme.spacing.l), - AFFilledTextButton.primary( - text: LocaleKeys.settings_appearance_members_sendInvite.tr(), - onTap: _inviteMember, - ), - ], - ), - ], - ); - } - - void _inviteMember() { - final email = _emailController.text; - if (!isEmail(email)) { - showToastNotification( - type: ToastificationType.error, - message: LocaleKeys.settings_appearance_members_emailInvalidError.tr(), - ); - return; - } - - context - .read() - .add(WorkspaceMemberEvent.inviteWorkspaceMemberByEmail(email)); - // clear the email field after inviting - _emailController.clear(); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart deleted file mode 100644 index 01d507ea24..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:http/http.dart' as http; - -enum InviteCodeEndpoint { - getInviteCode, - deleteInviteCode, - generateInviteCode; - - String get path { - switch (this) { - case InviteCodeEndpoint.getInviteCode: - case InviteCodeEndpoint.deleteInviteCode: - case InviteCodeEndpoint.generateInviteCode: - return '/api/workspace/{workspaceId}/invite-code'; - } - } - - String get method { - switch (this) { - case InviteCodeEndpoint.getInviteCode: - return 'GET'; - case InviteCodeEndpoint.deleteInviteCode: - return 'DELETE'; - case InviteCodeEndpoint.generateInviteCode: - return 'POST'; - } - } - - Uri uri(String baseUrl, String workspaceId) => - Uri.parse(path.replaceAll('{workspaceId}', workspaceId)).replace( - scheme: Uri.parse(baseUrl).scheme, - host: Uri.parse(baseUrl).host, - port: Uri.parse(baseUrl).port, - ); -} - -class MemberHttpService { - MemberHttpService({ - required this.baseUrl, - required this.authToken, - }); - - final String baseUrl; - final String authToken; - - final http.Client client = http.Client(); - - Map get headers => { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer $authToken', - }; - - /// Gets the invite code for a workspace - Future> getInviteCode({ - required String workspaceId, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.getInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to get invite code', - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['code'] as String), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to get invite code: $e'), - ); - } - } - - /// Deletes the invite code for a workspace - Future> deleteInviteCode({ - required String workspaceId, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.deleteInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to delete invite code', - ); - - return result.fold( - (data) => FlowyResult.success(true), - (error) => FlowyResult.failure(error), - ); - } - - /// Generates a new invite code for a workspace - /// - /// [workspaceId] - The ID of the workspace - Future> generateInviteCode({ - required String workspaceId, - int? validityPeriodHours, - }) async { - final result = await _makeRequest( - endpoint: InviteCodeEndpoint.generateInviteCode, - workspaceId: workspaceId, - errorMessage: 'Failed to generate invite code', - body: { - 'validity_period_hours': validityPeriodHours, - }, - ); - - try { - return result.fold( - (data) => FlowyResult.success(data['data']['code'].toString()), - (error) => FlowyResult.failure(error), - ); - } catch (e) { - return FlowyResult.failure( - FlowyError(msg: 'Failed to generate invite code: $e'), - ); - } - } - - /// Makes a request to the specified endpoint - Future> _makeRequest({ - required InviteCodeEndpoint endpoint, - required String workspaceId, - Map? body, - String errorMessage = 'Request failed', - }) async { - try { - final uri = endpoint.uri(baseUrl, workspaceId); - http.Response response; - - switch (endpoint.method) { - case 'GET': - response = await client.get( - uri, - headers: headers, - ); - break; - case 'DELETE': - response = await client.delete( - uri, - headers: headers, - ); - break; - case 'POST': - response = await client.post( - uri, - headers: headers, - body: body != null ? jsonEncode(body) : null, - ); - break; - default: - return FlowyResult.failure( - FlowyError(msg: 'Invalid request method: ${endpoint.method}'), - ); - } - - if (response.statusCode == 200) { - if (response.body.isNotEmpty) { - return FlowyResult.success(jsonDecode(response.body)); - } - return FlowyResult.success(true); - } else { - final errorBody = - response.body.isNotEmpty ? jsonDecode(response.body) : {}; - - Log.info( - '${endpoint.name} request failed: ${response.statusCode}, $errorBody', - ); - - return FlowyResult.failure( - FlowyError( - msg: errorBody['msg'] ?? errorMessage, - ), - ); - } - } catch (e) { - Log.error('${endpoint.name} request failed: error: $e'); - - return FlowyResult.failure( - FlowyError(msg: 'Network error: ${e.toString()}'), - ); - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index 3fc13c7b18..7b9dc4354b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -1,11 +1,4 @@ -import 'dart:async'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/env/cloud_env.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/shared/af_user_profile_extension.dart'; import 'package:appflowy/user/application/user_service.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/member_http_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -31,266 +24,146 @@ class WorkspaceMemberBloc extends Bloc { WorkspaceMemberBloc({ required this.userProfile, - String? workspaceId, this.workspace, }) : _userBackendService = UserBackendService(userId: userProfile.id), super(WorkspaceMemberState.initial()) { on((event, emit) async { await event.when( - initial: () async => _onInitial(emit, workspaceId), - getWorkspaceMembers: () async => _onGetWorkspaceMembers(emit), - addWorkspaceMember: (email) async => _onAddWorkspaceMember(emit, email), - inviteWorkspaceMemberByEmail: (email) async => - _onInviteWorkspaceMemberByEmail(emit, email), - removeWorkspaceMemberByEmail: (email) async => - _onRemoveWorkspaceMemberByEmail(emit, email), - inviteWorkspaceMemberByLink: (link) async => - _onInviteWorkspaceMemberByLink(emit, link), - generateInviteLink: () async => _onGenerateInviteLink(emit), - updateWorkspaceMember: (email, role) async => - _onUpdateWorkspaceMember(emit, email, role), - updateSubscriptionInfo: (info) async => - _onUpdateSubscriptionInfo(emit, info), - upgradePlan: () async => _onUpgradePlan(), + initial: () async { + await _setCurrentWorkspaceId(); + + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + isLoading: false, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + }, + getWorkspaceMembers: () async { + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + }, + addWorkspaceMember: (email) async { + final result = await _userBackendService.addWorkspaceMember( + _workspaceId, + email, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.add, + result: result, + ), + ), + ); + // the addWorkspaceMember doesn't return the updated members, + // so we need to get the members again + result.onSuccess((s) { + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }); + }, + inviteWorkspaceMember: (email) async { + final result = await _userBackendService.inviteWorkspaceMember( + _workspaceId, + email, + role: AFRolePB.Member, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.invite, + result: result, + ), + ), + ); + }, + removeWorkspaceMember: (email) async { + final result = await _userBackendService.removeWorkspaceMember( + _workspaceId, + email, + ); + final members = result.fold( + (s) => state.members.where((e) => e.email != email).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.remove, + result: result, + ), + ), + ); + }, + updateWorkspaceMember: (email, role) async { + final result = await _userBackendService.updateWorkspaceMember( + _workspaceId, + email, + role, + ); + final members = result.fold( + (s) => state.members.map((e) { + if (e.email == email) { + e.freeze(); + return e.rebuild((p0) { + p0.role = role; + }); + } + return e; + }).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.updateRole, + result: result, + ), + ), + ); + }, ); }); } final UserProfilePB userProfile; + + // if the workspace is null, use the current workspace final UserWorkspacePB? workspace; + late final String _workspaceId; final UserBackendService _userBackendService; - MemberHttpService? _memberHttpService; - - Future _onInitial( - Emitter emit, - String? workspaceId, - ) async { - await _setCurrentWorkspaceId(workspaceId); - - final result = await _userBackendService.getWorkspaceMembers(_workspaceId); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - - if (myRole.isOwner) { - unawaited(_fetchWorkspaceSubscriptionInfo()); - } - - final baseUrl = await getAppFlowyCloudUrl(); - final authToken = userProfile.authToken; - if (authToken != null) { - _memberHttpService = MemberHttpService( - baseUrl: baseUrl, - authToken: authToken, - ); - unawaited( - _memberHttpService?.getInviteCode(workspaceId: _workspaceId).fold( - (s) async { - final inviteLink = await _buildInviteLink(inviteCode: s); - emit(state.copyWith(inviteLink: inviteLink)); - }, - (e) => Log.info('Failed to get invite code: ${e.msg}', e), - ), - ); - } else { - Log.error('Failed to get auth token'); - } - - emit( - state.copyWith( - members: members, - myRole: myRole, - isLoading: false, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - } - - Future _onGetWorkspaceMembers( - Emitter emit, - ) async { - final result = await _userBackendService.getWorkspaceMembers(_workspaceId); - final members = result.fold>( - (s) => s.items, - (e) => [], - ); - final myRole = _getMyRole(members); - emit( - state.copyWith( - members: members, - myRole: myRole, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.get, - result: result, - ), - ), - ); - } - - Future _onAddWorkspaceMember( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.addWorkspaceMember( - _workspaceId, - email, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.addByEmail, - result: result, - ), - ), - ); - // the addWorkspaceMember doesn't return the updated members, - // so we need to get the members again - result.onSuccess((s) { - add(const WorkspaceMemberEvent.getWorkspaceMembers()); - }); - } - - Future _onInviteWorkspaceMemberByEmail( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.inviteWorkspaceMember( - _workspaceId, - email, - role: AFRolePB.Member, - ); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.inviteByEmail, - result: result, - ), - ), - ); - } - - Future _onRemoveWorkspaceMemberByEmail( - Emitter emit, - String email, - ) async { - final result = await _userBackendService.removeWorkspaceMember( - _workspaceId, - email, - ); - final members = result.fold( - (s) => state.members.where((e) => e.email != email).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.removeByEmail, - result: result, - ), - ), - ); - } - - Future _onInviteWorkspaceMemberByLink( - Emitter emit, - String link, - ) async {} - - Future _onGenerateInviteLink(Emitter emit) async { - final result = await _memberHttpService?.generateInviteCode( - workspaceId: _workspaceId, - ); - - await result?.fold( - (s) async { - final inviteLink = await _buildInviteLink(inviteCode: s); - emit( - state.copyWith( - inviteLink: inviteLink, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.generateInviteLink, - result: result, - ), - ), - ); - }, - (e) async { - Log.error('Failed to generate invite link: ${e.msg}', e); - emit( - state.copyWith( - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.generateInviteLink, - result: result, - ), - ), - ); - }, - ); - } - - Future _onUpdateWorkspaceMember( - Emitter emit, - String email, - AFRolePB role, - ) async { - final result = await _userBackendService.updateWorkspaceMember( - _workspaceId, - email, - role, - ); - final members = result.fold( - (s) => state.members.map((e) { - if (e.email == email) { - e.freeze(); - return e.rebuild((p0) => p0.role = role); - } - return e; - }).toList(), - (e) => state.members, - ); - emit( - state.copyWith( - members: members, - actionResult: WorkspaceMemberActionResult( - actionType: WorkspaceMemberActionType.updateRole, - result: result, - ), - ), - ); - } - - Future _onUpdateSubscriptionInfo( - Emitter emit, - WorkspaceSubscriptionInfoPB info, - ) async { - emit(state.copyWith(subscriptionInfo: info)); - } - - Future _onUpgradePlan() async { - final plan = state.subscriptionInfo?.plan; - if (plan == null) { - return Log.error('Failed to upgrade plan: plan is null'); - } - - if (plan == WorkspacePlanPB.FreePlan) { - final checkoutLink = await _userBackendService.createSubscription( - _workspaceId, - SubscriptionPlanPB.Pro, - ); - - checkoutLink.fold( - (pl) => afLaunchUrlString(pl.paymentLink), - (f) => Log.error('Failed to create subscription: ${f.msg}', f), - ); - } - } AFRolePB _getMyRole(List members) { final role = members @@ -305,11 +178,9 @@ class WorkspaceMemberBloc return role; } - Future _setCurrentWorkspaceId(String? workspaceId) async { + Future _setCurrentWorkspaceId() async { if (workspace != null) { _workspaceId = workspace!.workspaceId; - } else if (workspaceId != null && workspaceId.isNotEmpty) { - _workspaceId = workspaceId; } else { final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); currentWorkspace.fold((s) { @@ -321,29 +192,6 @@ class WorkspaceMemberBloc }); } } - - Future _fetchWorkspaceSubscriptionInfo() async { - final result = - await UserBackendService.getWorkspaceSubscriptionInfo(_workspaceId); - - result.fold( - (info) { - if (!isClosed) { - add(WorkspaceMemberEvent.updateSubscriptionInfo(info)); - } - }, - (f) => Log.error('Failed to fetch subscription info: ${f.msg}', f), - ); - } - - Future _buildInviteLink({required String inviteCode}) async { - final baseUrl = await getAppFlowyShareDomain(); - final authToken = userProfile.authToken; - if (authToken != null) { - return '$baseUrl/app/invited/$inviteCode'; - } - return ''; - } } @freezed @@ -353,36 +201,24 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent { GetWorkspaceMembers; const factory WorkspaceMemberEvent.addWorkspaceMember(String email) = AddWorkspaceMember; - const factory WorkspaceMemberEvent.inviteWorkspaceMemberByEmail( - String email, - ) = InviteWorkspaceMemberByEmail; - const factory WorkspaceMemberEvent.removeWorkspaceMemberByEmail( - String email, - ) = RemoveWorkspaceMemberByEmail; - const factory WorkspaceMemberEvent.inviteWorkspaceMemberByLink(String link) = - InviteWorkspaceMemberByLink; - const factory WorkspaceMemberEvent.generateInviteLink() = GenerateInviteLink; + const factory WorkspaceMemberEvent.inviteWorkspaceMember(String email) = + InviteWorkspaceMember; + const factory WorkspaceMemberEvent.removeWorkspaceMember(String email) = + RemoveWorkspaceMember; const factory WorkspaceMemberEvent.updateWorkspaceMember( String email, AFRolePB role, ) = UpdateWorkspaceMember; - const factory WorkspaceMemberEvent.updateSubscriptionInfo( - WorkspaceSubscriptionInfoPB subscriptionInfo, - ) = UpdateSubscriptionInfo; - - const factory WorkspaceMemberEvent.upgradePlan() = UpgradePlan; } enum WorkspaceMemberActionType { none, get, // this event will send an invitation to the member - inviteByEmail, - inviteByLink, - generateInviteLink, + invite, // this event will add the member without sending an invitation - addByEmail, - removeByEmail, + add, + remove, updateRole, } @@ -405,8 +241,6 @@ class WorkspaceMemberState with _$WorkspaceMemberState { @Default(AFRolePB.Guest) AFRolePB myRole, @Default(null) WorkspaceMemberActionResult? actionResult, @Default(true) bool isLoading, - @Default(null) WorkspaceSubscriptionInfoPB? subscriptionInfo, - @Default(null) String? inviteLink, }) = _WorkspaceMemberState; factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); @@ -421,8 +255,6 @@ class WorkspaceMemberState with _$WorkspaceMemberState { return other is WorkspaceMemberState && other.members == members && other.myRole == myRole && - other.subscriptionInfo == subscriptionInfo && - other.inviteLink == inviteLink && identical(other.actionResult, actionResult); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index 3ead104ee3..2d2f2e57b7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -1,33 +1,28 @@ +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/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; -import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/inivite_member_by_link.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/members/inivitation/invite_member_by_email.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:string_validator/string_validator.dart'; class WorkspaceMembersPage extends StatelessWidget { - const WorkspaceMembersPage({ - super.key, - required this.userProfile, - required this.workspaceId, - }); + const WorkspaceMembersPage({super.key, required this.userProfile}); final UserProfilePB userProfile; - final String workspaceId; @override Widget build(BuildContext context) { @@ -39,16 +34,8 @@ class WorkspaceMembersPage extends StatelessWidget { builder: (context, state) { return SettingsBody( title: LocaleKeys.settings_appearance_members_title.tr(), - // Enable it when the backend support admin panel - // descriptionBuilder: _buildDescription, - autoSeparate: false, children: [ - if (state.myRole.canInvite) ...[ - const InviteMemberByLink(), - const SettingsCategorySpacer(), - const InviteMemberByEmail(), - const SettingsCategorySpacer(), - ], + if (state.myRole.canInvite) const _InviteMember(), if (state.members.isNotEmpty) _MemberList( members: state.members, @@ -62,142 +49,6 @@ class WorkspaceMembersPage extends StatelessWidget { ); } - // Enable it when the backend support admin panel - // Widget _buildDescription(BuildContext context) { - // final theme = AppFlowyTheme.of(context); - // return Text.rich( - // TextSpan( - // children: [ - // TextSpan( - // text: - // '${LocaleKeys.settings_appearance_members_memberPageDescription1.tr()} ', - // style: theme.textStyle.caption.standard( - // color: theme.textColorScheme.secondary, - // ), - // ), - // TextSpan( - // text: LocaleKeys.settings_appearance_members_adminPanel.tr(), - // style: theme.textStyle.caption.underline( - // color: theme.textColorScheme.secondary, - // ), - // mouseCursor: SystemMouseCursors.click, - // recognizer: TapGestureRecognizer() - // ..onTap = () async { - // final baseUrl = await getAppFlowyCloudUrl(); - // await afLaunchUrlString(baseUrl); - // }, - // ), - // TextSpan( - // text: - // ' ${LocaleKeys.settings_appearance_members_memberPageDescription2.tr()} ', - // style: theme.textStyle.caption.standard( - // color: theme.textColorScheme.secondary, - // ), - // ), - // ], - // ), - // ); - // } - - // Widget _showMemberLimitWarning( - // BuildContext context, - // WorkspaceMemberState state, - // ) { - // // We promise that state.actionResult != null before calling - // // this method - // final actionResult = state.actionResult!.result; - // final actionType = state.actionResult!.actionType; - - // if (actionType == WorkspaceMemberActionType.inviteByEmail && - // actionResult.isFailure) { - // final error = actionResult.getFailure().code; - // if (error == ErrorCode.WorkspaceMemberLimitExceeded) { - // return Row( - // children: [ - // const FlowySvg( - // FlowySvgs.warning_s, - // blendMode: BlendMode.dst, - // size: Size.square(20), - // ), - // const HSpace(12), - // Expanded( - // child: RichText( - // text: TextSpan( - // children: [ - // if (state.subscriptionInfo?.plan == - // WorkspacePlanPB.ProPlan) ...[ - // TextSpan( - // text: LocaleKeys - // .settings_appearance_members_memberLimitExceededPro - // .tr(), - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: AFThemeExtension.of(context).strongText, - // ), - // ), - // WidgetSpan( - // child: MouseRegion( - // cursor: SystemMouseCursors.click, - // child: GestureDetector( - // // Hardcoded support email, in the future we might - // // want to add this to an environment variable - // onTap: () async => afLaunchUrlString( - // 'mailto:support@appflowy.io', - // ), - // child: FlowyText( - // LocaleKeys - // .settings_appearance_members_memberLimitExceededProContact - // .tr(), - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // ), - // ] else ...[ - // TextSpan( - // text: LocaleKeys - // .settings_appearance_members_memberLimitExceeded - // .tr(), - // style: TextStyle( - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: AFThemeExtension.of(context).strongText, - // ), - // ), - // WidgetSpan( - // child: MouseRegion( - // cursor: SystemMouseCursors.click, - // child: GestureDetector( - // onTap: () => context - // .read() - // .add(const WorkspaceMemberEvent.upgradePlan()), - // child: FlowyText( - // LocaleKeys - // .settings_appearance_members_memberLimitExceededUpgrade - // .tr(), - // fontSize: 14, - // fontWeight: FontWeight.w400, - // color: Theme.of(context).colorScheme.primary, - // ), - // ), - // ), - // ), - // ], - // ], - // ), - // ), - // ), - // ], - // ); - // } - // } - - // return const SizedBox.shrink(); - // } - void _showResultDialog(BuildContext context, WorkspaceMemberState state) { final actionResult = state.actionResult; if (actionResult == null) { @@ -208,12 +59,12 @@ class WorkspaceMembersPage extends StatelessWidget { final result = actionResult.result; // only show the result dialog when the action is WorkspaceMemberActionType.add - if (actionType == WorkspaceMemberActionType.addByEmail) { + if (actionType == WorkspaceMemberActionType.add) { result.fold( (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), ); }, (f) { @@ -227,52 +78,140 @@ class WorkspaceMembersPage extends StatelessWidget { ); }, ); - } else if (actionType == WorkspaceMemberActionType.inviteByEmail) { + } else if (actionType == WorkspaceMemberActionType.invite) { result.fold( (s) { - showToastNotification( - message: - LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_inviteMemberSuccess.tr(), ); }, (f) { Log.error('invite workspace member failed: $f'); final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded - ? LocaleKeys.settings_appearance_members_inviteFailedMemberLimit - .tr() + ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() : LocaleKeys.settings_appearance_members_failedToInviteMember .tr(); - showConfirmDialog( + showDialog( context: context, - title: LocaleKeys - .settings_appearance_members_inviteFailedDialogTitle - .tr(), - description: message, - confirmLabel: LocaleKeys.button_ok.tr(), - ); - }, - ); - } else if (actionType == WorkspaceMemberActionType.generateInviteLink) { - result.fold( - (s) { - showToastNotification( - message: 'Invite link generated successfully', - ); - - // copy the invite link to the clipboard - final inviteLink = state.inviteLink; - if (inviteLink != null) { - getIt().setPlainText(inviteLink); - } - }, - (f) { - Log.error('generate invite link failed: $f'); - showToastNotification( - message: 'Failed to generate invite link', + builder: (context) => NavigatorOkCancelDialog(message: message), ); }, ); } + + result.onFailure((f) { + Log.error( + '[Member] Failed to perform ${actionType.toString()} action: $f', + ); + }); + } +} + +class _InviteMember extends StatefulWidget { + const _InviteMember(); + + @override + State<_InviteMember> createState() => _InviteMemberState(); +} + +class _InviteMemberState extends State<_InviteMember> { + final _emailController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.semibold( + LocaleKeys.settings_appearance_members_inviteMembers.tr(), + fontSize: 16.0, + ), + const VSpace(8.0), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 48.0, + ), + child: FlowyTextField( + hintText: + LocaleKeys.settings_appearance_members_inviteHint.tr(), + controller: _emailController, + onEditingComplete: _inviteMember, + ), + ), + ), + const HSpace(10.0), + SizedBox( + height: 48.0, + child: IntrinsicWidth( + child: RoundedTextButton( + title: LocaleKeys.settings_appearance_members_sendInvite.tr(), + padding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + onPressed: _inviteMember, + ), + ), + ), + ], + ), + /* Enable this when the feature is ready + PrimaryButton( + backgroundColor: const Color(0xFFE0E0E0), + child: Padding( + padding: const EdgeInsets.only( + left: 20, + right: 24, + top: 8, + bottom: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FlowySvg( + FlowySvgs.invite_member_link_m, + color: Colors.black, + ), + const HSpace(8.0), + FlowyText( + LocaleKeys.settings_appearance_members_copyInviteLink.tr(), + color: Colors.black, + ), + ], + ), + ), + onPressed: () { + showSnackBarMessage(context, 'not implemented'); + }, + ), + const VSpace(16.0), + */ + ], + ); + } + + void _inviteMember() { + final email = _emailController.text; + if (!isEmail(email)) { + return showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_emailInvalidError.tr(), + ); + } + context + .read() + .add(WorkspaceMemberEvent.inviteWorkspaceMember(email)); + // clear the email field after inviting + _emailController.clear(); } } @@ -289,12 +228,9 @@ class _MemberList extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); return SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.start, - separatorBuilder: () => Divider( - color: theme.borderColorScheme.primary, - ), + separatorBuilder: () => const Divider(), children: [ const _MemberListHeader(), ...members.map( @@ -314,34 +250,31 @@ class _MemberListHeader extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Text( - LocaleKeys.settings_appearance_members_user.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - ), + FlowyText.semibold( + LocaleKeys.settings_appearance_members_label.tr(), + fontSize: 16.0, ), - Expanded( - child: Text( - LocaleKeys.settings_appearance_members_role.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, + const VSpace(16.0), + Row( + children: [ + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_user.tr(), + fontSize: 14.0, + ), ), - ), - ), - Expanded( - child: Text( - LocaleKeys.settings_accountPage_email_title.tr(), - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, + Expanded( + child: FlowyText.semibold( + LocaleKeys.settings_appearance_members_role.tr(), + fontSize: 14.0, + ), ), - ), + const HSpace(28.0), + ], ), - const HSpace(28.0), ], ); } @@ -360,42 +293,27 @@ class _MemberItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); + final textColor = member.role.isOwner ? Theme.of(context).hintColor : null; return Row( children: [ Expanded( - child: Text( + child: FlowyText.medium( member.name, - style: theme.textStyle.body.enhanced( - color: theme.textColorScheme.primary, - ), + color: textColor, + fontSize: 14.0, ), ), Expanded( child: member.role.isOwner || !myRole.canUpdate - ? Text( + ? FlowyText.medium( member.role.description, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), + color: textColor, + fontSize: 14.0, ) : _MemberRoleActionList( member: member, ), ), - Expanded( - child: FlowyTooltip( - message: member.email, - child: Text( - member.email, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - ), - ), - ), myRole.canDelete && member.email != userProfile.email // can't delete self ? _MemberMoreActionList(member: member) @@ -446,7 +364,7 @@ class _MemberMoreActionList extends StatelessWidget { .settings_appearance_members_areYouSureToRemoveMember .tr(), onOkPressed: () => context.read().add( - WorkspaceMemberEvent.removeWorkspaceMemberByEmail( + WorkspaceMemberEvent.removeWorkspaceMember( action.member.email, ), ), @@ -485,12 +403,106 @@ class _MemberRoleActionList extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return Text( - member.role.description, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), + return PopoverActionList<_MemberRoleActionWrapper>( + asBarrier: true, + direction: PopoverDirection.bottomWithLeftAligned, + actions: [AFRolePB.Member] + .map((e) => _MemberRoleActionWrapper(e, member)) + .toList(), + offset: const Offset(0, 10), + buildChild: (controller) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => controller.show(), + child: Row( + children: [ + FlowyText.medium( + member.role.description, + fontSize: 14.0, + ), + const HSpace(8.0), + const FlowySvg( + FlowySvgs.drop_menu_show_s, + ), + ], + ), + ), + ); + }, + onSelected: (action, controller) async { + switch (action.inner) { + case AFRolePB.Member: + case AFRolePB.Guest: + context.read().add( + WorkspaceMemberEvent.updateWorkspaceMember( + action.member.email, + action.inner, + ), + ); + break; + case AFRolePB.Owner: + break; + } + controller.close(); + }, ); } } + +class _MemberRoleActionWrapper extends ActionCell { + _MemberRoleActionWrapper(this.inner, this.member); + + final AFRolePB inner; + final WorkspaceMemberPB member; + + @override + Widget? rightIcon(Color iconColor) { + return SizedBox( + width: 58.0, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + FlowyTooltip( + message: tooltip, + child: const FlowySvg( + FlowySvgs.information_s, + // color: iconColor, + ), + ), + const Spacer(), + if (member.role == inner) + const FlowySvg( + FlowySvgs.checkmark_tiny_s, + ), + ], + ), + ); + } + + @override + String get name { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guest.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_member.tr(); + case AFRolePB.Owner: + return LocaleKeys.settings_appearance_members_owner.tr(); + } + throw UnimplementedError('Unknown role: $inner'); + } + + String get tooltip { + switch (inner) { + case AFRolePB.Guest: + return LocaleKeys.settings_appearance_members_guestHintText.tr(); + case AFRolePB.Member: + return LocaleKeys.settings_appearance_members_memberHintText.tr(); + case AFRolePB.Owner: + return ''; + } + throw UnimplementedError('Unknown role: $inner'); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 5f158f4ae1..1d8795411d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -1,28 +1,25 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/shared/share/constants.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/web_url_hint_widget.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class AppFlowyCloudViewSetting extends StatelessWidget { @@ -68,19 +65,13 @@ class AppFlowyCloudViewSetting extends StatelessWidget { builder: (context, state) { return Column( children: [ - const VSpace(8), const AppFlowyCloudEnableSync(), - const VSpace(6), - // const AppFlowyCloudSyncLogEnabled(), const VSpace(12), RestartButton( onClick: () { NavigatorAlertDialog( title: LocaleKeys.settings_menu_restartAppTip.tr(), confirm: () async { - await useBaseWebDomain( - ShareConstants.defaultBaseWebDomain, - ); await useAppFlowyBetaCloudWithURL( serverURL, authenticatorType, @@ -130,7 +121,6 @@ class CustomAppFlowyCloudView extends StatelessWidget { final List children = []; children.addAll([ const AppFlowyCloudEnableSync(), - // const AppFlowyCloudSyncLogEnabled(), const VSpace(40), ]); @@ -154,7 +144,6 @@ class CustomAppFlowyCloudView extends StatelessWidget { create: (context) => AppFlowyCloudSettingBloc(setting) ..add(const AppFlowyCloudSettingEvent.initial()), child: Column( - mainAxisSize: MainAxisSize.min, children: children, ), ); @@ -180,10 +169,8 @@ class AppFlowyCloudURLs extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return Column( - mainAxisSize: MainAxisSize.min, children: [ - const AppFlowySelfHostTip(), - const VSpace(12), + const AppFlowySelfhostTip(), CloudURLInput( title: LocaleKeys.settings_menu_cloudURL.tr(), url: state.config.base_url, @@ -197,20 +184,6 @@ class AppFlowyCloudURLs extends StatelessWidget { }, ), const VSpace(8), - CloudURLInput( - title: LocaleKeys.settings_menu_webURL.tr(), - url: state.config.base_web_domain, - hint: LocaleKeys.settings_menu_webURLHint.tr(), - hintBuilder: (context) => const WebUrlHintWidget(), - onChanged: (text) { - context.read().add( - AppFlowyCloudURLsEvent.updateBaseWebDomain( - text, - ), - ); - }, - ), - const VSpace(12), RestartButton( onClick: () { NavigatorAlertDialog( @@ -233,8 +206,8 @@ class AppFlowyCloudURLs extends StatelessWidget { } } -class AppFlowySelfHostTip extends StatelessWidget { - const AppFlowySelfHostTip({super.key}); +class AppFlowySelfhostTip extends StatelessWidget { + const AppFlowySelfhostTip({super.key}); final url = "https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy#build-appflowy-with-a-self-hosted-server"; @@ -279,14 +252,12 @@ class CloudURLInput extends StatefulWidget { required this.url, required this.hint, required this.onChanged, - this.hintBuilder, }); final String title; final String url; final String hint; - final ValueChanged onChanged; - final WidgetBuilder? hintBuilder; + final Function(String) onChanged; @override CloudURLInputState createState() => CloudURLInputState(); @@ -309,55 +280,27 @@ class CloudURLInputState extends State { @override Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHint(context), - SizedBox( - height: 28, - child: TextField( - controller: _controller, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - fontWeight: FontWeight.w400, - ), - decoration: InputDecoration( - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: AFThemeExtension.of(context).onBackground, - ), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - ), - ), - hintText: widget.hint, - errorText: context.read().state.urlError, - ), - onChanged: widget.onChanged, - ), + return TextField( + controller: _controller, + style: const TextStyle(fontSize: 12.0), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(vertical: 6), + labelText: widget.title, + labelStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.w400, fontSize: 16), + enabledBorder: UnderlineInputBorder( + borderSide: + BorderSide(color: AFThemeExtension.of(context).onBackground), ), - ], - ); - } - - Widget _buildHint(BuildContext context) { - final children = [ - FlowyText( - widget.title, - fontSize: 12, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), + ), + hintText: widget.hint, + errorText: context.read().state.urlError, ), - ]; - - if (widget.hintBuilder != null) { - children.add(widget.hintBuilder!(context)); - } - - return Row( - mainAxisSize: MainAxisSize.min, - children: children, + onChanged: widget.onChanged, ); } } @@ -374,10 +317,11 @@ class AppFlowyCloudEnableSync extends StatelessWidget { FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), const Spacer(), Toggle( + style: ToggleStyle.big, value: state.setting.enableSync, onChanged: (value) => context .read() - .add(AppFlowyCloudSettingEvent.enableSync(value)), + .add(AppFlowyCloudSettingEvent.enableSync(!value)), ), ], ); @@ -385,92 +329,3 @@ class AppFlowyCloudEnableSync extends StatelessWidget { ); } } - -class AppFlowyCloudSyncLogEnabled extends StatelessWidget { - const AppFlowyCloudSyncLogEnabled({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Row( - children: [ - FlowyText.medium(LocaleKeys.settings_menu_enableSyncLog.tr()), - const Spacer(), - Toggle( - value: state.isSyncLogEnabled, - onChanged: (value) { - if (value) { - showCancelAndConfirmDialog( - context: context, - title: LocaleKeys.settings_menu_enableSyncLog.tr(), - description: - LocaleKeys.settings_menu_enableSyncLogWarning.tr(), - confirmLabel: LocaleKeys.button_confirm.tr(), - onConfirm: () { - context - .read() - .add(AppFlowyCloudSettingEvent.enableSyncLog(value)); - }, - ); - } else { - context - .read() - .add(AppFlowyCloudSettingEvent.enableSyncLog(value)); - } - }, - ), - ], - ); - }, - ); - } -} - -class BillingGateGuard extends StatelessWidget { - const BillingGateGuard({required this.builder, super.key}); - - final Widget Function(BuildContext context) builder; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: isBillingEnabled(), - builder: (context, snapshot) { - final isBillingEnabled = snapshot.data ?? false; - if (isBillingEnabled && - snapshot.connectionState == ConnectionState.done) { - return builder(context); - } - - // If the billing is not enabled, show nothing - return const SizedBox.shrink(); - }, - ); - } -} - -Future isBillingEnabled() async { - final result = await UserEventGetCloudConfig().send(); - return result.fold( - (cloudSetting) { - final whiteList = [ - "https://beta.appflowy.cloud", - "https://test.appflowy.cloud", - ]; - if (kDebugMode) { - whiteList.add("http://localhost:8000"); - } - - final isWhiteListed = whiteList.contains(cloudSetting.serverUrl); - if (!isWhiteListed) { - Log.warn("Billing is not enabled for server ${cloudSetting.serverUrl}"); - } - return isWhiteListed; - }, - (err) { - Log.error("Failed to get cloud config: $err"); - return false; - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 692be99baa..10e69be87a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -6,26 +6,24 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/settings/cloud_setting_bloc.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_local_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; 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'; -import 'package:universal_platform/universal_platform.dart'; import 'setting_appflowy_cloud.dart'; +import 'setting_supabase_cloud.dart'; class SettingCloud extends StatelessWidget { - const SettingCloud({ - super.key, - required this.restartAppFlowy, - }); + const SettingCloud({required this.restartAppFlowy, super.key}); final VoidCallback restartAppFlowy; @@ -43,10 +41,23 @@ class SettingCloud extends StatelessWidget { builder: (context, state) { return SettingsBody( title: LocaleKeys.settings_menu_cloudSettings.tr(), - autoSeparate: false, children: [ if (Env.enableCustomCloud) - _CloudServerSwitcher(cloudType: state.cloudType), + Row( + children: [ + Expanded( + child: FlowyText.medium( + LocaleKeys.settings_menu_cloudServerType.tr(), + ), + ), + CloudTypeSwitcher( + cloudType: state.cloudType, + onSelected: (type) => context + .read() + .add(CloudSettingEvent.updateCloudType(type)), + ), + ], + ), _viewFromCloudType(state.cloudType), ], ); @@ -63,11 +74,21 @@ class SettingCloud extends StatelessWidget { Widget _viewFromCloudType(AuthenticatorType cloudType) { switch (cloudType) { case AuthenticatorType.local: - return SettingLocalCloud(restartAppFlowy: restartAppFlowy); + return SettingLocalCloud( + restartAppFlowy: restartAppFlowy, + ); + case AuthenticatorType.supabase: + return SettingSupabaseCloudView( + restartAppFlowy: restartAppFlowy, + ); case AuthenticatorType.appflowyCloud: - return AppFlowyCloudViewSetting(restartAppFlowy: restartAppFlowy); + return AppFlowyCloudViewSetting( + restartAppFlowy: restartAppFlowy, + ); case AuthenticatorType.appflowyCloudSelfHost: - return CustomAppFlowyCloudView(restartAppFlowy: restartAppFlowy); + return CustomAppFlowyCloudView( + restartAppFlowy: restartAppFlowy, + ); case AuthenticatorType.appflowyCloudDevelop: return AppFlowyCloudViewSetting( serverURL: "http://localhost", @@ -93,31 +114,36 @@ class CloudTypeSwitcher extends StatelessWidget { final isDevelopMode = integrationMode().isDevelop; // Only show the appflowyCloudDevelop in develop mode final values = AuthenticatorType.values.where((element) { + // Supabase will going to be removed in the future + if (element == AuthenticatorType.supabase) { + return false; + } + return isDevelopMode || element != AuthenticatorType.appflowyCloudDevelop; }).toList(); - return UniversalPlatform.isDesktopOrWeb - ? SettingsDropdown( - selectedOption: cloudType, - onChanged: (type) { - if (type != cloudType) { - NavigatorAlertDialog( - title: LocaleKeys.settings_menu_changeServerTip.tr(), - confirm: () async { - onSelected(type); - }, - hideCancelButton: true, - ).show(context); - } + return PlatformExtension.isDesktopOrWeb + ? AppFlowyPopover( + direction: PopoverDirection.bottomWithRightAligned, + child: FlowyTextButton( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), + titleFromCloudType(cloudType), + fontColor: AFThemeExtension.of(context).onBackground, + fillColor: Colors.transparent, + onPressed: () {}, + ), + popupBuilder: (BuildContext context) { + return ListView.builder( + shrinkWrap: true, + itemBuilder: (context, index) { + return CloudTypeItem( + cloudType: values[index], + currentCloudtype: cloudType, + onSelected: onSelected, + ); + }, + itemCount: values.length, + ); }, - options: values - .map( - (type) => buildDropdownMenuEntry( - context, - value: type, - label: titleFromCloudType(type), - ), - ) - .toList(), ) : FlowyButton( text: FlowyText( @@ -127,28 +153,32 @@ class CloudTypeSwitcher extends StatelessWidget { rightIcon: const Icon( Icons.chevron_right, ), - onTap: () => showMobileBottomSheet( - context, - showHeader: true, - showDragHandle: true, - showDivider: false, - title: LocaleKeys.settings_menu_cloudServerType.tr(), - builder: (context) => Column( - children: values - .mapIndexed( - (i, e) => FlowyOptionTile.checkbox( - text: titleFromCloudType(values[i]), - isSelected: cloudType == values[i], - onTap: () { - onSelected(e); - context.pop(); - }, - showBottomBorder: i == values.length - 1, - ), - ) - .toList(), - ), - ), + onTap: () { + showMobileBottomSheet( + context, + showHeader: true, + showDragHandle: true, + showDivider: false, + title: LocaleKeys.settings_menu_cloudServerType.tr(), + builder: (context) { + return Column( + children: values + .mapIndexed( + (i, e) => FlowyOptionTile.checkbox( + text: titleFromCloudType(values[i]), + isSelected: cloudType == values[i], + onTap: () { + onSelected(e); + context.pop(); + }, + showBottomBorder: i == values.length - 1, + ), + ) + .toList(), + ); + }, + ); + }, ); } } @@ -157,12 +187,12 @@ class CloudTypeItem extends StatelessWidget { const CloudTypeItem({ super.key, required this.cloudType, - required this.currentCloudType, + required this.currentCloudtype, required this.onSelected, }); final AuthenticatorType cloudType; - final AuthenticatorType currentCloudType; + final AuthenticatorType currentCloudtype; final Function(AuthenticatorType) onSelected; @override @@ -173,11 +203,11 @@ class CloudTypeItem extends StatelessWidget { text: FlowyText.medium( titleFromCloudType(cloudType), ), - rightIcon: currentCloudType == cloudType + rightIcon: currentCloudtype == cloudType ? const FlowySvg(FlowySvgs.check_s) : null, onTap: () { - if (currentCloudType != cloudType) { + if (currentCloudtype != cloudType) { NavigatorAlertDialog( title: LocaleKeys.settings_menu_changeServerTip.tr(), confirm: () async { @@ -193,54 +223,12 @@ class CloudTypeItem extends StatelessWidget { } } -class _CloudServerSwitcher extends StatelessWidget { - const _CloudServerSwitcher({ - required this.cloudType, - }); - - final AuthenticatorType cloudType; - - @override - Widget build(BuildContext context) { - return UniversalPlatform.isDesktopOrWeb - ? Row( - children: [ - Expanded( - child: FlowyText.medium( - LocaleKeys.settings_menu_cloudServerType.tr(), - ), - ), - Flexible( - child: CloudTypeSwitcher( - cloudType: cloudType, - onSelected: (type) => context - .read() - .add(CloudSettingEvent.updateCloudType(type)), - ), - ), - ], - ) - : Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FlowyText.medium( - LocaleKeys.settings_menu_cloudServerType.tr(), - ), - CloudTypeSwitcher( - cloudType: cloudType, - onSelected: (type) => context - .read() - .add(CloudSettingEvent.updateCloudType(type)), - ), - ], - ); - } -} - String titleFromCloudType(AuthenticatorType cloudType) { switch (cloudType) { case AuthenticatorType.local: return LocaleKeys.settings_menu_cloudLocal.tr(); + case AuthenticatorType.supabase: + return LocaleKeys.settings_menu_cloudSupabase.tr(); case AuthenticatorType.appflowyCloud: return LocaleKeys.settings_menu_cloudAppFlowy.tr(); case AuthenticatorType.appflowyCloudSelfHost: diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart new file mode 100644 index 0000000000..29fab1805f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart @@ -0,0 +1,345 @@ +import 'package:appflowy/core/helpers/url_launcher.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart'; +import 'package:appflowy/workspace/application/settings/supabase_cloud_urls_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/toast.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/_restart_app_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingSupabaseCloudView extends StatelessWidget { + const SettingSupabaseCloudView({required this.restartAppFlowy, super.key}); + + final VoidCallback restartAppFlowy; + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: UserEventGetCloudConfig().send(), + builder: (context, snapshot) { + if (snapshot.data != null && + snapshot.connectionState == ConnectionState.done) { + return snapshot.data!.fold( + (setting) { + return BlocProvider( + create: (context) => SupabaseCloudSettingBloc( + setting: setting, + )..add(const SupabaseCloudSettingEvent.initial()), + child: Column( + children: [ + BlocBuilder( + builder: (context, state) { + return const Column( + children: [ + SupabaseEnableSync(), + EnableEncrypt(), + ], + ); + }, + ), + const VSpace(40), + const SupabaseSelfhostTip(), + SupabaseCloudURLs( + didUpdateUrls: restartAppFlowy, + ), + ], + ), + ); + }, + (err) { + return FlowyErrorPage.message(err.toString(), howToFix: ""); + }, + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + } +} + +class SupabaseCloudURLs extends StatelessWidget { + const SupabaseCloudURLs({super.key, required this.didUpdateUrls}); + + final VoidCallback didUpdateUrls; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SupabaseCloudURLsBloc(), + child: BlocListener( + listener: (context, state) async { + if (state.restartApp) { + didUpdateUrls(); + } + }, + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + SupabaseInput( + title: LocaleKeys.settings_menu_cloudSupabaseUrl.tr(), + url: state.config.url, + hint: LocaleKeys.settings_menu_cloudURLHint.tr(), + onChanged: (text) { + context + .read() + .add(SupabaseCloudURLsEvent.updateUrl(text)); + }, + error: state.urlError, + ), + SupabaseInput( + title: LocaleKeys.settings_menu_cloudSupabaseAnonKey.tr(), + url: state.config.anon_key, + hint: LocaleKeys.settings_menu_cloudURLHint.tr(), + onChanged: (text) { + context + .read() + .add(SupabaseCloudURLsEvent.updateAnonKey(text)); + }, + error: state.anonKeyError, + ), + const VSpace(20), + RestartButton( + onClick: () => _restartApp(context), + showRestartHint: state.showRestartHint, + ), + ], + ); + }, + ), + ), + ); + } + + void _restartApp(BuildContext context) { + NavigatorAlertDialog( + title: LocaleKeys.settings_menu_restartAppTip.tr(), + confirm: () => context + .read() + .add(const SupabaseCloudURLsEvent.confirmUpdate()), + ).show(context); + } +} + +class EnableEncrypt extends StatelessWidget { + const EnableEncrypt({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final indicator = state.loadingState.when( + loading: () => const CircularProgressIndicator.adaptive(), + finish: (successOrFail) => const SizedBox.shrink(), + idle: () => const SizedBox.shrink(), + ); + + return Column( + children: [ + Row( + children: [ + FlowyText.medium(LocaleKeys.settings_menu_enableEncrypt.tr()), + const Spacer(), + indicator, + const HSpace(3), + Switch.adaptive( + activeColor: Theme.of(context).colorScheme.primary, + onChanged: state.setting.enableEncrypt + ? null + : (bool value) { + context.read().add( + SupabaseCloudSettingEvent.enableEncrypt(value), + ); + }, + value: state.setting.enableEncrypt, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Opacity( + opacity: 0.6, + child: FlowyText.medium( + LocaleKeys.settings_menu_enableEncryptPrompt.tr(), + maxLines: 13, + ), + ), + ), + const VSpace(6), + SizedBox( + height: 40, + child: FlowyTooltip( + message: LocaleKeys.settings_menu_clickToCopySecret.tr(), + child: FlowyButton( + disable: !state.setting.enableEncrypt, + decoration: BoxDecoration( + borderRadius: Corners.s5Border, + border: Border.all( + color: Theme.of(context).colorScheme.secondary, + ), + ), + text: FlowyText.medium(state.setting.encryptSecret), + onTap: () async { + await Clipboard.setData( + ClipboardData(text: state.setting.encryptSecret), + ); + showMessageToast(LocaleKeys.message_copy_success.tr()); + }, + ), + ), + ), + ], + ), + ], + ); + }, + ); + } +} + +class SupabaseEnableSync extends StatelessWidget { + const SupabaseEnableSync({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Row( + children: [ + FlowyText.medium(LocaleKeys.settings_menu_enableSync.tr()), + const Spacer(), + Switch.adaptive( + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (bool value) { + context.read().add( + SupabaseCloudSettingEvent.enableSync(value), + ); + }, + value: state.setting.enableSync, + ), + ], + ); + }, + ); + } +} + +@visibleForTesting +class SupabaseInput extends StatefulWidget { + const SupabaseInput({ + super.key, + required this.title, + required this.url, + required this.hint, + required this.error, + required this.onChanged, + }); + + final String title; + final String url; + final String hint; + final String? error; + final Function(String) onChanged; + + @override + SupabaseInputState createState() => SupabaseInputState(); +} + +class SupabaseInputState extends State { + late TextEditingController _controller; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.url); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: _controller, + style: const TextStyle(fontSize: 12.0), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(vertical: 6), + labelText: widget.title, + labelStyle: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontWeight: FontWeight.w400, fontSize: 16), + enabledBorder: UnderlineInputBorder( + borderSide: + BorderSide(color: AFThemeExtension.of(context).onBackground), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), + ), + hintText: widget.hint, + errorText: widget.error, + ), + onChanged: widget.onChanged, + ); + } +} + +class SupabaseSelfhostTip extends StatelessWidget { + const SupabaseSelfhostTip({super.key}); + + final url = + "https://docs.appflowy.io/docs/guides/appflowy/self-hosting-appflowy-using-supabase"; + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: 0.6, + child: RichText( + text: TextSpan( + children: [ + TextSpan( + text: LocaleKeys.settings_menu_selfHostStart.tr(), + style: Theme.of(context).textTheme.bodySmall!, + ), + TextSpan( + text: " ${LocaleKeys.settings_menu_selfHostContent.tr()} ", + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: FontSizes.s14, + color: Theme.of(context).colorScheme.primary, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => afLaunchUrlString(url), + ), + TextSpan( + text: LocaleKeys.settings_menu_selfHostEnd.tr(), + style: Theme.of(context).textTheme.bodySmall!, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart index 8a85377efe..e623652f8b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_third_party_login.dart @@ -1,7 +1,10 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/sign_in_bloc.dart'; +import 'package:appflowy/user/presentation/router.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -9,14 +12,10 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingThirdPartyLogin extends StatelessWidget { - const SettingThirdPartyLogin({ - super.key, - required this.didLogin, - }); + const SettingThirdPartyLogin({required this.didLogin, super.key}); final VoidCallback didLogin; @@ -63,8 +62,12 @@ class SettingThirdPartyLogin extends StatelessWidget { ) async { result.fold( (user) async { - didLogin(); - await runAppFlowy(); + if (user.encryptionType == EncryptionTypePB.Symmetric) { + getIt().pushEncryptionScreen(context, user); + } else { + didLogin(); + await runAppFlowy(); + } }, (error) => showSnapBar(context, error.msg), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index f628aadc6b..0e89ba8cf7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -1,13 +1,15 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.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/foundation.dart'; -import 'package:flutter/material.dart'; class SettingsMenu extends StatelessWidget { const SettingsMenu({ @@ -15,149 +17,17 @@ class SettingsMenu extends StatelessWidget { required this.changeSelectedPage, required this.currentPage, required this.userProfile, - required this.isBillingEnabled, + this.member, }); final Function changeSelectedPage; final SettingsPage currentPage; final UserProfilePB userProfile; - final bool isBillingEnabled; + final WorkspaceMemberPB? member; @override Widget build(BuildContext context) { // Column > Expanded for full size no matter the content - return Column( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8) + - const EdgeInsets.only(left: 8, right: 4), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: const BorderRadiusDirectional.only( - topStart: Radius.circular(8), - bottomStart: Radius.circular(8), - ), - ), - child: SingleChildScrollView( - // Right padding is added to make the scrollbar centered - // in the space between the menu and the content - padding: const EdgeInsets.only(right: 4) + - const EdgeInsets.symmetric(vertical: 16), - physics: const ClampingScrollPhysics(), - child: SeparatedColumn( - separatorBuilder: () => const VSpace(16), - children: [ - SettingsMenuElement( - page: SettingsPage.account, - selectedPage: currentPage, - label: LocaleKeys.settings_accountPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_user_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.workspace, - selectedPage: currentPage, - label: LocaleKeys.settings_workspacePage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_workspace_m), - changeSelectedPage: changeSelectedPage, - ), - if (FeatureFlag.membersSettings.isOn && - userProfile.workspaceAuthType == AuthTypePB.Server) - SettingsMenuElement( - page: SettingsPage.member, - selectedPage: currentPage, - label: LocaleKeys.settings_appearance_members_label.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_users_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.manageData, - selectedPage: currentPage, - label: LocaleKeys.settings_manageDataPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_database_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.notifications, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_notifications.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_bell_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.cloud, - selectedPage: currentPage, - label: LocaleKeys.settings_menu_cloudSettings.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_cloud_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.shortcuts, - selectedPage: currentPage, - label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_keyboard_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.ai, - selectedPage: currentPage, - label: LocaleKeys.settings_aiPage_menuLabel.tr(), - icon: const FlowySvg( - FlowySvgs.settings_page_ai_m, - size: Size.square(24), - ), - changeSelectedPage: changeSelectedPage, - ), - if (userProfile.workspaceAuthType == AuthTypePB.Server) - SettingsMenuElement( - page: SettingsPage.sites, - selectedPage: currentPage, - label: LocaleKeys.settings_sites_title.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_earth_m), - changeSelectedPage: changeSelectedPage, - ), - if (FeatureFlag.planBilling.isOn && isBillingEnabled) ...[ - SettingsMenuElement( - page: SettingsPage.plan, - selectedPage: currentPage, - label: LocaleKeys.settings_planPage_menuLabel.tr(), - icon: const FlowySvg(FlowySvgs.settings_page_plan_m), - changeSelectedPage: changeSelectedPage, - ), - SettingsMenuElement( - page: SettingsPage.billing, - selectedPage: currentPage, - label: LocaleKeys.settings_billingPage_menuLabel.tr(), - icon: - const FlowySvg(FlowySvgs.settings_page_credit_card_m), - changeSelectedPage: changeSelectedPage, - ), - ], - if (kDebugMode) - SettingsMenuElement( - // no need to translate this page - page: SettingsPage.featureFlags, - selectedPage: currentPage, - label: 'Feature Flags', - icon: const Icon(Icons.flag), - changeSelectedPage: changeSelectedPage, - ), - ], - ), - ), - ), - ), - ], - ); - } -} - -class SimpleSettingsMenu extends StatelessWidget { - const SimpleSettingsMenu({super.key}); - - @override - Widget build(BuildContext context) { return Column( children: [ Expanded( @@ -180,21 +50,86 @@ class SimpleSettingsMenu extends StatelessWidget { child: SeparatedColumn( separatorBuilder: () => const VSpace(16), children: [ + SettingsMenuElement( + page: SettingsPage.account, + selectedPage: currentPage, + label: LocaleKeys.settings_accountPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_account_m), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.workspace, + selectedPage: currentPage, + label: LocaleKeys.settings_workspacePage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_workplace_m), + changeSelectedPage: changeSelectedPage, + ), + if (FeatureFlag.membersSettings.isOn && + userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) + SettingsMenuElement( + page: SettingsPage.member, + selectedPage: currentPage, + label: LocaleKeys.settings_appearance_members_label.tr(), + icon: const Icon(Icons.people), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.manageData, + selectedPage: currentPage, + label: LocaleKeys.settings_manageDataPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_data_m), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.notifications, + selectedPage: currentPage, + label: LocaleKeys.settings_menu_notifications.tr(), + icon: const Icon(Icons.notifications_outlined), + changeSelectedPage: changeSelectedPage, + ), SettingsMenuElement( page: SettingsPage.cloud, - selectedPage: SettingsPage.cloud, + selectedPage: currentPage, label: LocaleKeys.settings_menu_cloudSettings.tr(), icon: const Icon(Icons.sync), - changeSelectedPage: () {}, + changeSelectedPage: changeSelectedPage, ), + SettingsMenuElement( + page: SettingsPage.shortcuts, + selectedPage: currentPage, + label: LocaleKeys.settings_shortcutsPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_shortcuts_m), + changeSelectedPage: changeSelectedPage, + ), + if (FeatureFlag.planBilling.isOn && + userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud && + member != null && + member!.role.isOwner) ...[ + SettingsMenuElement( + page: SettingsPage.plan, + selectedPage: currentPage, + label: LocaleKeys.settings_planPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_plan_m), + changeSelectedPage: changeSelectedPage, + ), + SettingsMenuElement( + page: SettingsPage.billing, + selectedPage: currentPage, + label: LocaleKeys.settings_billingPage_menuLabel.tr(), + icon: const FlowySvg(FlowySvgs.settings_billing_m), + changeSelectedPage: changeSelectedPage, + ), + ], if (kDebugMode) SettingsMenuElement( // no need to translate this page page: SettingsPage.featureFlags, - selectedPage: SettingsPage.cloud, + selectedPage: currentPage, label: 'Feature Flags', icon: const Icon(Icons.flag), - changeSelectedPage: () {}, + changeSelectedPage: changeSelectedPage, ), ], ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart index 29ba2baf5c..f7dcb63507 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_notifications_view.dart @@ -4,7 +4,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/notifications/notification_settings_cubit.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,9 +23,11 @@ class SettingsNotificationsView extends StatelessWidget { hint: LocaleKeys.settings_notifications_enableNotifications_hint .tr(), trailing: [ - Toggle( + Switch( value: state.isNotificationsEnabled, - onChanged: (_) => context + splashRadius: 0, + activeColor: Theme.of(context).colorScheme.primary, + onChanged: (value) => context .read() .toggleNotificationsEnabled(), ), @@ -39,8 +40,10 @@ class SettingsNotificationsView extends StatelessWidget { hint: LocaleKeys.settings_notifications_showNotificationsIcon_hint .tr(), trailing: [ - Toggle( + Switch( value: state.isShowNotificationsIconEnabled, + splashRadius: 0, + activeColor: Theme.of(context).colorScheme.primary, onChanged: (_) => context .read() .toogleShowNotificationIconEnabled(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart index ea8ebfe36b..f3cd25afde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart @@ -16,8 +16,8 @@ class ThemeUploadDecoration extends StatelessWidget { borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), color: Theme.of(context).colorScheme.surface, border: Border.all( - color: AFThemeExtension.of(context).onBackground.withValues( - alpha: ThemeUploadWidget.fadeOpacity, + color: AFThemeExtension.of(context).onBackground.withOpacity( + ThemeUploadWidget.fadeOpacity, ), ), ), @@ -28,7 +28,7 @@ class ThemeUploadDecoration extends StatelessWidget { color: Theme.of(context) .colorScheme .onSurface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), + .withOpacity(ThemeUploadWidget.fadeOpacity), radius: const Radius.circular(ThemeUploadWidget.borderRadius), child: ClipRRect( borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart index a7286bee48..edb382d6ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart @@ -15,7 +15,7 @@ class ThemeUploadFailureWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .error - .withValues(alpha: ThemeUploadWidget.fadeOpacity), + .withOpacity(ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), padding: ThemeUploadWidget.padding, child: Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index bdc5ef0546..d57d2d2a00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -1,13 +1,14 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/error_page/error_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.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:flowy_infra_ui/widget/buttons/secondary_button.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra/theme_extension.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { const ThemeUploadLearnMoreButton({super.key}); @@ -31,7 +32,7 @@ class ThemeUploadLearnMoreButton extends StatelessWidget { ), onPressed: () async { final uri = Uri.parse(learnMoreURL); - await afLaunchUri( + await afLaunchUrl( uri, context: context, onFailure: (_) async { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart index 1d3e7ab0f8..5e0ad15f38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart @@ -14,7 +14,7 @@ class ThemeUploadLoadingWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .surface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), + .withOpacity(ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart index 1b22dba659..f9705843d1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'theme_upload_decoration.dart'; @@ -58,9 +57,8 @@ class _ThemeUploadWidgetState extends State { }); } - Widget child = const UploadNewThemeWidget( - key: Key('upload_new_theme_widget'), - ); + Widget child = + const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart index 02a7c8e7ab..0113d26a37 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart @@ -15,7 +15,7 @@ class UploadNewThemeWidget extends StatelessWidget { color: Theme.of(context) .colorScheme .surface - .withValues(alpha: ThemeUploadWidget.fadeOpacity), + .withOpacity(ThemeUploadWidget.fadeOpacity), padding: ThemeUploadWidget.padding, child: Column( mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/web_url_hint_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/web_url_hint_widget.dart deleted file mode 100644 index ecf3cc7ef7..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/web_url_hint_widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -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:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; - -class WebUrlHintWidget extends StatelessWidget { - const WebUrlHintWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(left: 2), - child: FlowyTooltip( - message: LocaleKeys.workspace_learnMore.tr(), - preferBelow: false, - child: FlowyIconButton( - width: 24, - height: 24, - icon: const FlowySvg( - FlowySvgs.information_s, - ), - onPressed: () { - afLaunchUrlString( - 'https://appflowy.com/docs/self-host-appflowy-run-appflowy-web', - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart new file mode 100644 index 0000000000..9fa92446b4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart @@ -0,0 +1,283 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; + +class OptionGroup { + OptionGroup({required this.options}); + + final List options; +} + +typedef DaySelectedCallback = Function(DateTime, DateTime); +typedef RangeSelectedCallback = Function(DateTime?, DateTime?, DateTime); +typedef IncludeTimeChangedCallback = Function(bool); +typedef TimeChangedCallback = Function(String); + +class AppFlowyDatePicker extends StatefulWidget { + const AppFlowyDatePicker({ + super.key, + required this.includeTime, + required this.onIncludeTimeChanged, + this.rebuildOnDaySelected = true, + this.enableRanges = true, + this.isRange = false, + this.onIsRangeChanged, + required this.dateFormat, + required this.timeFormat, + this.selectedDay, + this.focusedDay, + this.firstDay, + this.lastDay, + this.startDay, + this.endDay, + this.timeStr, + this.endTimeStr, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.popoverMutex, + this.selectedReminderOption = ReminderOption.none, + this.onStartTimeSubmitted, + this.onEndTimeSubmitted, + this.onDaySelected, + this.onRangeSelected, + this.onReminderSelected, + this.options, + this.allowFormatChanges = false, + this.onDateFormatChanged, + this.onTimeFormatChanged, + this.onClearDate, + this.onCalendarCreated, + this.onPageChanged, + }); + + final bool includeTime; + final Function(bool) onIncludeTimeChanged; + + final bool enableRanges; + final bool isRange; + final Function(bool)? onIsRangeChanged; + + final bool rebuildOnDaySelected; + + final DateFormatPB dateFormat; + final TimeFormatPB timeFormat; + + final DateTime? selectedDay; + final DateTime? focusedDay; + final DateTime? firstDay; + final DateTime? lastDay; + + /// Start date in selected range + final DateTime? startDay; + + /// End date in selected range + final DateTime? endDay; + + final String? timeStr; + final String? endTimeStr; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final PopoverMutex? popoverMutex; + final ReminderOption selectedReminderOption; + + final TimeChangedCallback? onStartTimeSubmitted; + final TimeChangedCallback? onEndTimeSubmitted; + final DaySelectedCallback? onDaySelected; + final RangeSelectedCallback? onRangeSelected; + final OnReminderSelected? onReminderSelected; + + /// A list of [OptionGroup] that will be rendered with proper + /// separators, each group can contain multiple options. + /// + /// __Supported on Desktop & Web__ + /// + final List? options; + + /// If this value is true, then [onTimeFormatChanged] and [onDateFormatChanged] + /// cannot be null + /// + final bool allowFormatChanges; + + /// If [allowFormatChanges] is true, this must be provided + /// + final Function(DateFormatPB)? onDateFormatChanged; + + /// If [allowFormatChanges] is true, this must be provided + /// + final Function(TimeFormatPB)? onTimeFormatChanged; + + /// If provided, the ClearDate button will be shown + /// Otherwise it will be hidden + /// + final VoidCallback? onClearDate; + + final void Function(PageController pageController)? onCalendarCreated; + + final void Function(DateTime focusedDay)? onPageChanged; + + @override + State createState() => _AppFlowyDatePickerState(); +} + +class _AppFlowyDatePickerState extends State { + late DateTime? _selectedDay = widget.selectedDay; + late ReminderOption _selectedReminderOption = widget.selectedReminderOption; + + @override + void didUpdateWidget(covariant AppFlowyDatePicker oldWidget) { + _selectedDay = oldWidget.selectedDay != widget.selectedDay + ? widget.selectedDay + : _selectedDay; + _selectedReminderOption = + oldWidget.selectedReminderOption != widget.selectedReminderOption + ? widget.selectedReminderOption + : _selectedReminderOption; + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) => + PlatformExtension.isMobile ? buildMobilePicker() : buildDesktopPicker(); + + Widget buildMobilePicker() { + return DatePicker( + isRange: widget.isRange, + onDaySelected: (selectedDay, focusedDay) { + widget.onDaySelected?.call(selectedDay, focusedDay); + + if (widget.rebuildOnDaySelected) { + setState(() => _selectedDay = selectedDay); + } + }, + onRangeSelected: widget.onRangeSelected, + selectedDay: + widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay, + firstDay: widget.firstDay, + lastDay: widget.lastDay, + startDay: widget.startDay, + endDay: widget.endDay, + onCalendarCreated: widget.onCalendarCreated, + onPageChanged: widget.onPageChanged, + ); + } + + Widget buildDesktopPicker() { + // GestureDetector is a workaround to stop popover from closing + // when clicking on the date picker. + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () {}, + child: Padding( + padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + StartTextField( + includeTime: widget.includeTime, + timeFormat: widget.timeFormat, + timeHintText: widget.timeHintText, + parseEndTimeError: widget.parseEndTimeError, + parseTimeError: widget.parseTimeError, + timeStr: widget.timeStr, + popoverMutex: widget.popoverMutex, + onSubmitted: widget.onStartTimeSubmitted, + ), + EndTextField( + includeTime: widget.includeTime, + timeFormat: widget.timeFormat, + isRange: widget.isRange, + endTimeStr: widget.endTimeStr, + popoverMutex: widget.popoverMutex, + onSubmitted: widget.onEndTimeSubmitted, + ), + DatePicker( + isRange: widget.isRange, + onDaySelected: (selectedDay, focusedDay) { + widget.onDaySelected?.call(selectedDay, focusedDay); + + if (widget.rebuildOnDaySelected) { + setState(() => _selectedDay = selectedDay); + } + }, + onRangeSelected: widget.onRangeSelected, + selectedDay: widget.rebuildOnDaySelected + ? _selectedDay + : widget.selectedDay, + firstDay: widget.firstDay, + lastDay: widget.lastDay, + startDay: widget.startDay, + endDay: widget.endDay, + onCalendarCreated: widget.onCalendarCreated, + onPageChanged: widget.onPageChanged, + ), + const TypeOptionSeparator(spacing: 12.0), + if (widget.enableRanges && widget.onIsRangeChanged != null) ...[ + EndTimeButton( + isRange: widget.isRange, + onChanged: widget.onIsRangeChanged!, + ), + const VSpace(4.0), + ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: IncludeTimeButton( + value: widget.includeTime, + onChanged: widget.onIncludeTimeChanged, + ), + ), + const _GroupSeparator(), + ReminderSelector( + mutex: widget.popoverMutex, + hasTime: widget.includeTime, + timeFormat: widget.timeFormat, + selectedOption: _selectedReminderOption, + onOptionSelected: (option) { + setState(() => _selectedReminderOption = option); + widget.onReminderSelected?.call(option); + }, + ), + if (widget.options?.isNotEmpty ?? false) ...[ + const _GroupSeparator(), + ListView.separated( + shrinkWrap: true, + itemCount: widget.options!.length, + separatorBuilder: (_, __) => const _GroupSeparator(), + itemBuilder: (_, index) => + _renderGroupOptions(widget.options![index].options), + ), + ], + ], + ), + ), + ); + } + + Widget _renderGroupOptions(List options) => ListView.separated( + shrinkWrap: true, + itemCount: options.length, + separatorBuilder: (_, __) => const VSpace(4), + itemBuilder: (_, index) => options[index], + ); +} + +class _GroupSeparator extends StatelessWidget { + const _GroupSeparator(); + + @override + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Container(color: Theme.of(context).dividerColor, height: 1.0), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart deleted file mode 100644 index d965670f77..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart +++ /dev/null @@ -1,334 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/widgets.dart'; - -import 'widgets/reminder_selector.dart'; - -typedef DaySelectedCallback = void Function(DateTime); -typedef RangeSelectedCallback = void Function(DateTime, DateTime); -typedef IsRangeChangedCallback = void Function(bool, DateTime?, DateTime?); -typedef IncludeTimeChangedCallback = void Function(bool, DateTime?, DateTime?); - -abstract class AppFlowyDatePicker extends StatefulWidget { - const AppFlowyDatePicker({ - super.key, - required this.dateTime, - this.endDateTime, - required this.includeTime, - required this.isRange, - this.reminderOption = ReminderOption.none, - required this.dateFormat, - required this.timeFormat, - this.onDaySelected, - this.onRangeSelected, - this.onIncludeTimeChanged, - this.onIsRangeChanged, - this.onReminderSelected, - this.enableDidUpdate = true, - }); - - final DateTime? dateTime; - final DateTime? endDateTime; - - final DateFormatPB dateFormat; - final TimeFormatPB timeFormat; - - /// Called when the date is picked, whether by submitting a date from the top - /// or by selecting a date in the calendar. Will not be called if isRange is - /// true - final DaySelectedCallback? onDaySelected; - - /// Called when a date range is picked. Will not be called if isRange is false - final RangeSelectedCallback? onRangeSelected; - - /// Whether the date picker allows inputting a time in addition to the date - final bool includeTime; - - /// Called when the include time value is changed. This callback has the side - /// effect of changing the dateTime values as well - final IncludeTimeChangedCallback? onIncludeTimeChanged; - - // Whether the date picker supports date ranges - final bool isRange; - - /// Called when the is range value is changed. This callback has the side - /// effect of changing the dateTime values as well - final IsRangeChangedCallback? onIsRangeChanged; - - final ReminderOption reminderOption; - final OnReminderSelected? onReminderSelected; - final bool enableDidUpdate; -} - -abstract class AppFlowyDatePickerState - extends State { - // store date values in the state and refresh the ui upon any changes made, instead of only updating them after receiving update from backend. - late DateTime? dateTime; - late DateTime? startDateTime; - late DateTime? endDateTime; - late bool includeTime; - late bool isRange; - late ReminderOption reminderOption; - - late DateTime focusedDateTime; - PageController? pageController; - - bool justChangedIsRange = false; - - @override - void initState() { - super.initState(); - initData(); - - focusedDateTime = widget.dateTime ?? DateTime.now(); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (widget.enableDidUpdate) { - initData(); - } - if (oldWidget.reminderOption != widget.reminderOption) { - reminderOption = widget.reminderOption; - } - super.didUpdateWidget(oldWidget); - } - - void initData() { - dateTime = widget.dateTime; - startDateTime = widget.isRange ? widget.dateTime : null; - endDateTime = widget.isRange ? widget.endDateTime : null; - includeTime = widget.includeTime; - isRange = widget.isRange; - reminderOption = widget.reminderOption; - } - - void onDateSelectedFromDatePicker( - DateTime? newStartDateTime, - DateTime? newEndDateTime, - ) { - if (newStartDateTime == null) { - return; - } - if (isRange) { - if (newEndDateTime == null) { - if (justChangedIsRange && dateTime != null) { - justChangedIsRange = false; - DateTime start = dateTime!; - DateTime end = combineDateTimes( - DateTime( - newStartDateTime.year, - newStartDateTime.month, - newStartDateTime.day, - ), - start, - ); - if (end.isBefore(start)) { - (start, end) = (end, start); - } - widget.onRangeSelected?.call(start, end); - setState(() { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(newStartDateTime); - }); - } else { - final combined = combineDateTimes(newStartDateTime, dateTime); - setState(() { - dateTime = combined; - startDateTime = combined; - endDateTime = null; - focusedDateTime = getNewFocusedDay(combined); - }); - } - } else { - bool switched = false; - DateTime combinedDateTime = - combineDateTimes(newStartDateTime, dateTime); - DateTime combinedEndDateTime = - combineDateTimes(newEndDateTime, widget.endDateTime); - - if (combinedEndDateTime.isBefore(combinedDateTime)) { - (combinedDateTime, combinedEndDateTime) = - (combinedEndDateTime, combinedDateTime); - switched = true; - } - - widget.onRangeSelected?.call(combinedDateTime, combinedEndDateTime); - - setState(() { - dateTime = switched ? combinedDateTime : combinedEndDateTime; - startDateTime = combinedDateTime; - endDateTime = combinedEndDateTime; - focusedDateTime = getNewFocusedDay(newEndDateTime); - }); - } - } else { - final combinedDateTime = combineDateTimes(newStartDateTime, dateTime); - widget.onDaySelected?.call(combinedDateTime); - - setState(() { - dateTime = combinedDateTime; - focusedDateTime = getNewFocusedDay(combinedDateTime); - }); - } - } - - DateTime combineDateTimes(DateTime date, DateTime? time) { - final timeComponent = time == null - ? Duration.zero - : Duration(hours: time.hour, minutes: time.minute); - - return DateTime(date.year, date.month, date.day).add(timeComponent); - } - - void onDateTimeInputSubmitted(DateTime value) { - if (isRange) { - DateTime end = endDateTime ?? value; - if (end.isBefore(value)) { - (value, end) = (end, value); - } - - widget.onRangeSelected?.call(value, end); - - setState(() { - dateTime = value; - startDateTime = value; - endDateTime = end; - focusedDateTime = getNewFocusedDay(value); - }); - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } - - void onEndDateTimeInputSubmitted(DateTime value) { - if (isRange) { - if (endDateTime == null) { - value = combineDateTimes(value, widget.endDateTime); - } - DateTime start = startDateTime ?? value; - if (value.isBefore(start)) { - (start, value) = (value, start); - } - - widget.onRangeSelected?.call(start, value); - - if (endDateTime == null) { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - setState(() { - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(value); - }); - } else { - setState(() { - dateTime = start; - startDateTime = start; - endDateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } - - DateTime getNewFocusedDay(DateTime dateTime) { - if (focusedDateTime.year != dateTime.year || - focusedDateTime.month != dateTime.month) { - return DateTime(dateTime.year, dateTime.month); - } else { - return focusedDateTime; - } - } - - void onIsRangeChanged(bool value) { - if (value) { - justChangedIsRange = true; - } - - final now = DateTime.now(); - final fillerDate = includeTime - ? DateTime(now.year, now.month, now.day, now.hour, now.minute) - : DateTime(now.year, now.month, now.day); - final newDateTime = dateTime ?? fillerDate; - - if (value) { - widget.onIsRangeChanged!.call(value, newDateTime, newDateTime); - } else { - widget.onIsRangeChanged!.call(value, null, null); - } - - setState(() { - isRange = value; - dateTime = focusedDateTime = newDateTime; - if (value) { - startDateTime = endDateTime = newDateTime; - } else { - startDateTime = endDateTime = null; - } - }); - } - - void onIncludeTimeChanged(bool value) { - late final DateTime? newDateTime; - late final DateTime? newEndDateTime; - - final now = DateTime.now(); - final fillerDate = value - ? DateTime(now.year, now.month, now.day, now.hour, now.minute) - : DateTime(now.year, now.month, now.day); - - if (value) { - // fill date if empty, add time component - newDateTime = dateTime == null - ? fillerDate - : combineDateTimes(dateTime!, fillerDate); - newEndDateTime = isRange - ? endDateTime == null - ? fillerDate - : combineDateTimes(endDateTime!, fillerDate) - : null; - } else { - // fill date if empty, remove time component - newDateTime = dateTime == null - ? fillerDate - : DateTime( - dateTime!.year, - dateTime!.month, - dateTime!.day, - ); - newEndDateTime = isRange - ? endDateTime == null - ? fillerDate - : DateTime( - endDateTime!.year, - endDateTime!.month, - endDateTime!.day, - ) - : null; - } - - widget.onIncludeTimeChanged!.call(value, newDateTime, newEndDateTime); - - setState(() { - includeTime = value; - dateTime = newDateTime ?? dateTime; - if (isRange) { - startDateTime = newDateTime ?? dateTime; - endDateTime = newEndDateTime ?? endDateTime; - } else { - startDateTime = endDateTime = null; - } - }); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart deleted file mode 100644 index fada23e994..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart +++ /dev/null @@ -1,307 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -import 'appflowy_date_picker_base.dart'; -import 'widgets/date_picker.dart'; -import 'widgets/date_time_text_field.dart'; -import 'widgets/end_time_button.dart'; -import 'widgets/reminder_selector.dart'; - -class OptionGroup { - OptionGroup({required this.options}); - - final List options; -} - -class DesktopAppFlowyDatePicker extends AppFlowyDatePicker { - const DesktopAppFlowyDatePicker({ - super.key, - required super.dateTime, - super.endDateTime, - required super.includeTime, - required super.isRange, - super.reminderOption = ReminderOption.none, - required super.dateFormat, - required super.timeFormat, - super.onDaySelected, - super.onRangeSelected, - super.onIncludeTimeChanged, - super.onIsRangeChanged, - super.onReminderSelected, - super.enableDidUpdate, - this.popoverMutex, - this.options = const [], - }); - - final PopoverMutex? popoverMutex; - - final List options; - - @override - State createState() => DesktopAppFlowyDatePickerState(); -} - -@visibleForTesting -class DesktopAppFlowyDatePickerState - extends AppFlowyDatePickerState { - final isTabPressedNotifier = ValueNotifier(false); - final refreshStartTextFieldNotifier = RefreshDateTimeTextFieldController(); - final refreshEndTextFieldNotifier = RefreshDateTimeTextFieldController(); - - @override - void dispose() { - isTabPressedNotifier.dispose(); - refreshStartTextFieldNotifier.dispose(); - refreshEndTextFieldNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - // GestureDetector is a workaround to stop popover from closing - // when clicking on the date picker. - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Padding( - padding: const EdgeInsets.only(top: 18.0, bottom: 12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - DateTimeTextField( - key: const ValueKey('date_time_text_field'), - includeTime: includeTime, - dateTime: isRange ? startDateTime : dateTime, - dateFormat: widget.dateFormat, - timeFormat: widget.timeFormat, - popoverMutex: widget.popoverMutex, - isTabPressed: isTabPressedNotifier, - refreshTextController: refreshStartTextFieldNotifier, - onSubmitted: onDateTimeInputSubmitted, - showHint: true, - ), - if (isRange) ...[ - const VSpace(8), - DateTimeTextField( - key: const ValueKey('end_date_time_text_field'), - includeTime: includeTime, - dateTime: endDateTime, - dateFormat: widget.dateFormat, - timeFormat: widget.timeFormat, - popoverMutex: widget.popoverMutex, - isTabPressed: isTabPressedNotifier, - refreshTextController: refreshEndTextFieldNotifier, - onSubmitted: onEndDateTimeInputSubmitted, - showHint: isRange && !(dateTime != null && endDateTime == null), - ), - ], - const VSpace(14), - Focus( - descendantsAreTraversable: false, - child: _buildDatePickerHeader(), - ), - const VSpace(14), - DatePicker( - isRange: isRange, - onDaySelected: (selectedDay, focusedDay) { - onDateSelectedFromDatePicker(selectedDay, null); - }, - onRangeSelected: (start, end, focusedDay) { - onDateSelectedFromDatePicker(start, end); - }, - selectedDay: dateTime, - startDay: isRange ? startDateTime : null, - endDay: isRange ? endDateTime : null, - focusedDay: focusedDateTime, - onCalendarCreated: (controller) { - pageController = controller; - }, - onPageChanged: (focusedDay) { - setState( - () => focusedDateTime = DateTime( - focusedDay.year, - focusedDay.month, - focusedDay.day, - ), - ); - }, - ), - if (widget.onIsRangeChanged != null || - widget.onIncludeTimeChanged != null) - const TypeOptionSeparator(spacing: 12.0), - if (widget.onIsRangeChanged != null) ...[ - EndTimeButton( - isRange: isRange, - onChanged: onIsRangeChanged, - ), - const VSpace(4.0), - ], - if (widget.onIncludeTimeChanged != null) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: IncludeTimeButton( - includeTime: includeTime, - onChanged: onIncludeTimeChanged, - ), - ), - if (widget.onReminderSelected != null) ...[ - const _GroupSeparator(), - ReminderSelector( - mutex: widget.popoverMutex, - hasTime: widget.includeTime, - timeFormat: widget.timeFormat, - selectedOption: reminderOption, - onOptionSelected: (option) { - widget.onReminderSelected?.call(option); - setState(() => reminderOption = option); - }, - ), - ], - if (widget.options.isNotEmpty) ...[ - const _GroupSeparator(), - ListView.separated( - shrinkWrap: true, - itemCount: widget.options.length, - physics: const NeverScrollableScrollPhysics(), - separatorBuilder: (_, __) => const _GroupSeparator(), - itemBuilder: (_, index) => - _renderGroupOptions(widget.options[index].options), - ), - ], - ], - ), - ), - ); - } - - Widget _buildDatePickerHeader() { - return Padding( - padding: const EdgeInsetsDirectional.only(start: 22.0, end: 18.0), - child: Row( - children: [ - Expanded( - child: FlowyText( - DateFormat.yMMMM().format(focusedDateTime), - ), - ), - FlowyIconButton( - width: 20, - icon: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(20.0), - ), - onPressed: () => pageController?.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ), - ), - const HSpace(4.0), - FlowyIconButton( - width: 20, - icon: FlowySvg( - FlowySvgs.arrow_right_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(20.0), - ), - onPressed: () { - pageController?.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - }, - ), - ], - ), - ); - } - - Widget _renderGroupOptions(List options) => ListView.separated( - shrinkWrap: true, - itemCount: options.length, - separatorBuilder: (_, __) => const VSpace(4), - itemBuilder: (_, index) => options[index], - ); - - @override - void onDateTimeInputSubmitted(DateTime value) { - if (isRange) { - DateTime end = endDateTime ?? value; - if (end.isBefore(value)) { - (value, end) = (end, value); - refreshStartTextFieldNotifier.refresh(); - } - - widget.onRangeSelected?.call(value, end); - - setState(() { - dateTime = value; - startDateTime = value; - endDateTime = end; - }); - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } - - @override - void onEndDateTimeInputSubmitted(DateTime value) { - if (isRange) { - if (endDateTime == null) { - value = combineDateTimes(value, widget.endDateTime); - } - DateTime start = startDateTime ?? value; - if (value.isBefore(start)) { - (start, value) = (value, start); - refreshEndTextFieldNotifier.refresh(); - } - - widget.onRangeSelected?.call(start, value); - - if (endDateTime == null) { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - setState(() { - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(value); - }); - } else { - setState(() { - dateTime = start; - startDateTime = start; - endDateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } -} - -class _GroupSeparator extends StatelessWidget { - const _GroupSeparator(); - - @override - Widget build(BuildContext context) => Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Container(color: Theme.of(context).dividerColor, height: 1.0), - ); -} - -class RefreshDateTimeTextFieldController extends ChangeNotifier { - void refresh() => notifyListeners(); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart new file mode 100644 index 0000000000..12714e04df --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart @@ -0,0 +1,498 @@ +import 'package:flutter/cupertino.dart'; +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_actions.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; +import 'package:appflowy/plugins/base/drag_handler.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:go_router/go_router.dart'; + +class MobileAppFlowyDatePicker extends StatefulWidget { + const MobileAppFlowyDatePicker({ + super.key, + this.selectedDay, + this.startDay, + this.endDay, + this.dateStr, + this.endDateStr, + this.timeStr, + this.endTimeStr, + this.enableRanges = false, + this.isRange = false, + this.rebuildOnDaySelected = false, + this.rebuildOnTimeChanged = false, + required this.includeTime, + required this.use24hFormat, + required this.timeFormat, + this.selectedReminderOption, + required this.onStartTimeChanged, + this.onEndTimeChanged, + required this.onIncludeTimeChanged, + this.onRangeChanged, + this.onDaySelected, + this.onRangeSelected, + this.onClearDate, + this.liveDateFormatter, + this.onReminderSelected, + }); + + final DateTime? selectedDay; + final DateTime? startDay; + final DateTime? endDay; + + final String? dateStr; + final String? endDateStr; + final String? timeStr; + final String? endTimeStr; + + final bool enableRanges; + final bool isRange; + final bool includeTime; + final bool rebuildOnDaySelected; + final bool rebuildOnTimeChanged; + final bool use24hFormat; + + final TimeFormatPB timeFormat; + + final ReminderOption? selectedReminderOption; + + final Function(String? time) onStartTimeChanged; + final Function(String? time)? onEndTimeChanged; + final Function(bool) onIncludeTimeChanged; + final Function(bool)? onRangeChanged; + + final DaySelectedCallback? onDaySelected; + final RangeSelectedCallback? onRangeSelected; + final VoidCallback? onClearDate; + final OnReminderSelected? onReminderSelected; + + final String Function(DateTime)? liveDateFormatter; + + @override + State createState() => + _MobileAppFlowyDatePickerState(); +} + +class _MobileAppFlowyDatePickerState extends State { + late bool _includeTime = widget.includeTime; + late String? _dateStr = widget.dateStr; + late ReminderOption _reminderOption = + widget.selectedReminderOption ?? ReminderOption.none; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + FlowyOptionDecorateBox( + showTopBorder: false, + child: _IncludeTimePicker( + dateStr: + widget.liveDateFormatter != null ? _dateStr : widget.dateStr, + endDateStr: widget.endDateStr, + timeStr: widget.timeStr, + endTimeStr: widget.endTimeStr, + includeTime: _includeTime, + use24hFormat: widget.use24hFormat, + onStartTimeChanged: widget.onStartTimeChanged, + onEndTimeChanged: widget.onEndTimeChanged, + rebuildOnTimeChanged: widget.rebuildOnTimeChanged, + ), + ), + const _Divider(), + FlowyOptionDecorateBox( + child: MobileDatePicker( + isRange: widget.isRange, + selectedDay: widget.selectedDay, + startDay: widget.startDay, + endDay: widget.endDay, + onDaySelected: (selected, focused) { + widget.onDaySelected?.call(selected, focused); + + if (widget.liveDateFormatter != null) { + setState(() => _dateStr = widget.liveDateFormatter!(selected)); + } + }, + onRangeSelected: widget.onRangeSelected, + rebuildOnDaySelected: widget.rebuildOnDaySelected, + ), + ), + const _Divider(), + if (widget.enableRanges && widget.onRangeChanged != null) + _EndDateSwitch( + isRange: widget.isRange, + onRangeChanged: widget.onRangeChanged!, + ), + _IncludeTimeSwitch( + showTopBorder: !widget.enableRanges || widget.onRangeChanged == null, + includeTime: _includeTime, + onIncludeTimeChanged: (includeTime) { + widget.onIncludeTimeChanged(includeTime); + setState(() => _includeTime = includeTime); + }, + ), + if (widget.onReminderSelected != null) ...[ + const _Divider(), + _ReminderSelector( + selectedReminderOption: _reminderOption, + onReminderSelected: (option) { + widget.onReminderSelected!.call(option); + setState(() => _reminderOption = option); + }, + timeFormat: widget.timeFormat, + hasTime: widget.includeTime, + ), + ], + if (widget.onClearDate != null) ...[ + const _Divider(), + _ClearDateButton(onClearDate: widget.onClearDate!), + ], + const _Divider(), + ], + ); + } +} + +class _Divider extends StatelessWidget { + const _Divider(); + + @override + Widget build(BuildContext context) => const VSpace(20.0); +} + +class _ReminderSelector extends StatelessWidget { + const _ReminderSelector({ + this.selectedReminderOption, + required this.onReminderSelected, + required this.timeFormat, + this.hasTime = false, + }); + + final ReminderOption? selectedReminderOption; + final OnReminderSelected onReminderSelected; + final TimeFormatPB timeFormat; + final bool hasTime; + + @override + Widget build(BuildContext context) { + final option = selectedReminderOption ?? ReminderOption.none; + + final availableOptions = [...ReminderOption.values]; + if (option != ReminderOption.custom) { + availableOptions.remove(ReminderOption.custom); + } + + availableOptions.removeWhere( + (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime), + ); + + return FlowyOptionTile.text( + text: LocaleKeys.datePicker_reminderLabel.tr(), + trailing: Row( + children: [ + const HSpace(6.0), + FlowyText( + option.label, + color: Theme.of(context).hintColor, + ), + const HSpace(4.0), + FlowySvg( + FlowySvgs.arrow_right_s, + color: Theme.of(context).hintColor, + size: const Size.square(18.0), + ), + ], + ), + onTap: () => showMobileBottomSheet( + context, + builder: (_) => DraggableScrollableSheet( + expand: false, + snap: true, + initialChildSize: 0.7, + minChildSize: 0.7, + builder: (context, controller) => Column( + children: [ + ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: const Center(child: DragHandle()), + ), + const _ReminderSelectHeader(), + Flexible( + child: SingleChildScrollView( + controller: controller, + child: Column( + children: availableOptions.map( + (o) { + String label = o.label; + if (o.withoutTime && !o.timeExempt) { + const time = "09:00"; + final t = timeFormat == TimeFormatPB.TwelveHour + ? "$time AM" + : time; + + label = "$label ($t)"; + } + + return FlowyOptionTile.text( + text: label, + showTopBorder: o == ReminderOption.none, + onTap: () { + onReminderSelected(o); + context.pop(); + }, + ); + }, + ).toList() + ..insert(0, const _Divider()), + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _ReminderSelectHeader extends StatelessWidget { + const _ReminderSelectHeader(); + + @override + Widget build(BuildContext context) { + return Container( + height: 56, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 120, + child: AppBarCancelButton(onTap: context.pop), + ), + FlowyText.medium( + LocaleKeys.datePicker_selectReminder.tr(), + fontSize: 17.0, + ), + const HSpace(120), + ], + ), + ); + } +} + +class _IncludeTimePicker extends StatefulWidget { + const _IncludeTimePicker({ + required this.includeTime, + this.dateStr, + this.endDateStr, + this.timeStr, + this.endTimeStr, + this.rebuildOnTimeChanged = false, + required this.use24hFormat, + required this.onStartTimeChanged, + required this.onEndTimeChanged, + }); + + final bool includeTime; + + final String? dateStr; + final String? endDateStr; + + final String? timeStr; + final String? endTimeStr; + + final bool rebuildOnTimeChanged; + + final bool use24hFormat; + + final Function(String? time) onStartTimeChanged; + final Function(String? time)? onEndTimeChanged; + + @override + State<_IncludeTimePicker> createState() => _IncludeTimePickerState(); +} + +class _IncludeTimePickerState extends State<_IncludeTimePicker> { + late String? _timeStr = widget.timeStr; + late String? _endTimeStr = widget.endTimeStr; + + @override + Widget build(BuildContext context) { + if (widget.dateStr == null || widget.dateStr!.isEmpty) { + return const Divider(height: 1); + } + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTime( + context, + widget.includeTime, + widget.use24hFormat, + true, + widget.dateStr, + widget.rebuildOnTimeChanged ? _timeStr : widget.timeStr, + ), + VSpace(8.0, color: Theme.of(context).colorScheme.surface), + _buildTime( + context, + widget.includeTime, + widget.use24hFormat, + false, + widget.endDateStr, + widget.rebuildOnTimeChanged ? _endTimeStr : widget.endTimeStr, + ), + ], + ), + ); + } + + Widget _buildTime( + BuildContext context, + bool isIncludeTime, + bool use24hFormat, + bool isStartDay, + String? dateStr, + String? timeStr, + ) { + if (dateStr == null) { + return const SizedBox.shrink(); + } + + final List children = []; + + if (!isIncludeTime) { + children.addAll([ + const HSpace(12.0), + FlowyText(dateStr), + ]); + } else { + children.addAll([ + Expanded(child: FlowyText(dateStr, textAlign: TextAlign.center)), + Container(width: 1, height: 16, color: Colors.grey), + Expanded(child: FlowyText(timeStr ?? '', textAlign: TextAlign.center)), + ]); + } + + return GestureDetector( + onTap: !isIncludeTime + ? null + : () async { + await showMobileBottomSheet( + context, + builder: (context) => ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.time, + use24hFormat: use24hFormat, + onDateTimeChanged: (dateTime) { + final selectedTime = use24hFormat + ? DateFormat('HH:mm').format(dateTime) + : DateFormat('hh:mm a').format(dateTime); + + if (isStartDay) { + widget.onStartTimeChanged(selectedTime); + + if (widget.rebuildOnTimeChanged && mounted) { + setState(() => _timeStr = selectedTime); + } + } else { + widget.onEndTimeChanged?.call(selectedTime); + + if (widget.rebuildOnTimeChanged && mounted) { + setState(() => _endTimeStr = selectedTime); + } + } + }, + ), + ), + ); + }, + child: Container( + constraints: const BoxConstraints(minHeight: 36), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + color: Theme.of(context).colorScheme.secondaryContainer, + border: Border.all( + color: Theme.of(context).colorScheme.outline, + ), + ), + child: Row(children: children), + ), + ); + } +} + +class _EndDateSwitch extends StatelessWidget { + const _EndDateSwitch({ + required this.isRange, + required this.onRangeChanged, + }); + + final bool isRange; + final Function(bool) onRangeChanged; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.toggle( + text: LocaleKeys.grid_field_isRange.tr(), + isSelected: isRange, + onValueChanged: onRangeChanged, + ); + } +} + +class _IncludeTimeSwitch extends StatelessWidget { + const _IncludeTimeSwitch({ + this.showTopBorder = true, + required this.includeTime, + required this.onIncludeTimeChanged, + }); + + final bool showTopBorder; + final bool includeTime; + final Function(bool) onIncludeTimeChanged; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.toggle( + showTopBorder: showTopBorder, + text: LocaleKeys.grid_field_includeTime.tr(), + isSelected: includeTime, + onValueChanged: onIncludeTimeChanged, + ); + } +} + +class _ClearDateButton extends StatelessWidget { + const _ClearDateButton({required this.onClearDate}); + + final VoidCallback onClearDate; + + @override + Widget build(BuildContext context) { + return FlowyOptionTile.text( + text: LocaleKeys.grid_field_clearDate.tr(), + onTap: onClearDate, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart deleted file mode 100644 index e9f3262cc3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart +++ /dev/null @@ -1,558 +0,0 @@ -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_actions.dart'; -import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart'; -import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; -import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -import 'appflowy_date_picker_base.dart'; - -class MobileAppFlowyDatePicker extends AppFlowyDatePicker { - const MobileAppFlowyDatePicker({ - super.key, - required super.dateTime, - super.endDateTime, - required super.includeTime, - required super.isRange, - super.reminderOption = ReminderOption.none, - required super.dateFormat, - required super.timeFormat, - super.onDaySelected, - super.onRangeSelected, - super.onIncludeTimeChanged, - super.onIsRangeChanged, - super.onReminderSelected, - this.onClearDate, - }); - - final VoidCallback? onClearDate; - - @override - State createState() => - _MobileAppFlowyDatePickerState(); -} - -class _MobileAppFlowyDatePickerState - extends AppFlowyDatePickerState { - @override - Widget build(BuildContext context) { - return Column( - children: [ - FlowyOptionDecorateBox( - showTopBorder: false, - child: _TimePicker( - dateTime: isRange ? startDateTime : dateTime, - endDateTime: endDateTime, - includeTime: includeTime, - isRange: isRange, - dateFormat: widget.dateFormat, - timeFormat: widget.timeFormat, - onStartTimeChanged: onDateTimeInputSubmitted, - onEndTimeChanged: onEndDateTimeInputSubmitted, - ), - ), - const _Divider(), - FlowyOptionDecorateBox( - child: MobileDatePicker( - isRange: isRange, - selectedDay: dateTime, - startDay: isRange ? startDateTime : null, - endDay: isRange ? endDateTime : null, - focusedDay: focusedDateTime, - onDaySelected: (selectedDay) { - onDateSelectedFromDatePicker(selectedDay, null); - }, - onRangeSelected: (start, end) { - onDateSelectedFromDatePicker(start, end); - }, - onPageChanged: (focusedDay) { - setState(() => focusedDateTime = focusedDay); - }, - ), - ), - const _Divider(), - if (widget.onIsRangeChanged != null) - _IsRangeSwitch( - isRange: widget.isRange, - onRangeChanged: onIsRangeChanged, - ), - if (widget.onIncludeTimeChanged != null) - _IncludeTimeSwitch( - showTopBorder: widget.onIsRangeChanged == null, - includeTime: includeTime, - onIncludeTimeChanged: onIncludeTimeChanged, - ), - if (widget.onReminderSelected != null) ...[ - const _Divider(), - _ReminderSelector( - selectedReminderOption: reminderOption, - onReminderSelected: (option) { - widget.onReminderSelected!.call(option); - setState(() => reminderOption = option); - }, - timeFormat: widget.timeFormat, - hasTime: widget.includeTime, - ), - ], - if (widget.onClearDate != null) ...[ - const _Divider(), - _ClearDateButton( - onClearDate: () { - widget.onClearDate!.call(); - Navigator.of(context).pop(); - }, - ), - ], - const _Divider(), - ], - ); - } -} - -class _Divider extends StatelessWidget { - const _Divider(); - - @override - Widget build(BuildContext context) => const VSpace(20.0); -} - -class _ReminderSelector extends StatelessWidget { - const _ReminderSelector({ - this.selectedReminderOption, - required this.onReminderSelected, - required this.timeFormat, - this.hasTime = false, - }); - - final ReminderOption? selectedReminderOption; - final OnReminderSelected onReminderSelected; - final TimeFormatPB timeFormat; - final bool hasTime; - - @override - Widget build(BuildContext context) { - final option = selectedReminderOption ?? ReminderOption.none; - - final availableOptions = [...ReminderOption.values]; - if (option != ReminderOption.custom) { - availableOptions.remove(ReminderOption.custom); - } - - availableOptions.removeWhere( - (o) => !o.timeExempt && (!hasTime ? !o.withoutTime : o.requiresNoTime), - ); - - return FlowyOptionTile.text( - text: LocaleKeys.datePicker_reminderLabel.tr(), - trailing: Row( - children: [ - const HSpace(6.0), - FlowyText( - option.label, - color: Theme.of(context).hintColor, - ), - const HSpace(4.0), - FlowySvg( - FlowySvgs.arrow_right_s, - color: Theme.of(context).hintColor, - size: const Size.square(18.0), - ), - ], - ), - onTap: () => showMobileBottomSheet( - context, - builder: (_) => DraggableScrollableSheet( - expand: false, - snap: true, - initialChildSize: 0.7, - minChildSize: 0.7, - builder: (context, controller) => Column( - children: [ - ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: const Center(child: DragHandle()), - ), - const _ReminderSelectHeader(), - Flexible( - child: SingleChildScrollView( - controller: controller, - child: Column( - children: availableOptions.map( - (o) { - String label = o.label; - if (o.withoutTime && !o.timeExempt) { - const time = "09:00"; - final t = timeFormat == TimeFormatPB.TwelveHour - ? "$time AM" - : time; - - label = "$label ($t)"; - } - - return FlowyOptionTile.text( - text: label, - showTopBorder: o == ReminderOption.none, - onTap: () { - onReminderSelected(o); - context.pop(); - }, - ); - }, - ).toList() - ..insert(0, const _Divider()), - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class _ReminderSelectHeader extends StatelessWidget { - const _ReminderSelectHeader(); - - @override - Widget build(BuildContext context) { - return Container( - height: 56, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox( - width: 120, - child: AppBarCancelButton(onTap: context.pop), - ), - FlowyText.medium( - LocaleKeys.datePicker_selectReminder.tr(), - fontSize: 17.0, - ), - const HSpace(120), - ], - ), - ); - } -} - -class _TimePicker extends StatelessWidget { - const _TimePicker({ - required this.dateTime, - required this.endDateTime, - required this.dateFormat, - required this.timeFormat, - required this.includeTime, - required this.isRange, - required this.onStartTimeChanged, - this.onEndTimeChanged, - }); - - final DateTime? dateTime; - final DateTime? endDateTime; - - final bool includeTime; - final bool isRange; - - final DateFormatPB dateFormat; - final TimeFormatPB timeFormat; - - final void Function(DateTime time) onStartTimeChanged; - final void Function(DateTime time)? onEndTimeChanged; - - @override - Widget build(BuildContext context) { - final dateStr = getDateStr(dateTime); - final timeStr = getTimeStr(dateTime); - final endDateStr = getDateStr(endDateTime); - final endTimeStr = getTimeStr(endDateTime); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildTime( - context, - dateStr, - timeStr, - includeTime, - true, - ), - if (isRange) ...[ - VSpace(8.0, color: Theme.of(context).colorScheme.surface), - _buildTime( - context, - endDateStr, - endTimeStr, - includeTime, - false, - ), - ], - ], - ), - ); - } - - Widget _buildTime( - BuildContext context, - String dateStr, - String timeStr, - bool includeTime, - bool isStartDay, - ) { - final List children = []; - - final now = DateTime.now(); - final hintDate = DateTime(now.year, now.month, 1, 9); - - if (!includeTime) { - children.add( - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final result = await _showDateTimePicker( - context, - isStartDay ? dateTime : endDateTime, - use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour, - mode: CupertinoDatePickerMode.date, - ); - handleDateTimePickerResult(result, isStartDay, true); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8, - ), - child: FlowyText( - dateStr.isNotEmpty ? dateStr : getDateStr(hintDate), - color: dateStr.isEmpty ? Theme.of(context).hintColor : null, - ), - ), - ), - ), - ); - } else { - children.addAll([ - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final result = await _showDateTimePicker( - context, - isStartDay ? dateTime : endDateTime, - use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour, - mode: CupertinoDatePickerMode.date, - ); - handleDateTimePickerResult(result, isStartDay, true); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: FlowyText( - dateStr.isNotEmpty ? dateStr : "", - textAlign: TextAlign.center, - ), - ), - ), - ), - Container( - width: 1, - height: 16, - color: Theme.of(context).colorScheme.outline, - ), - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final result = await _showDateTimePicker( - context, - isStartDay ? dateTime : endDateTime, - use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour, - mode: CupertinoDatePickerMode.time, - ); - handleDateTimePickerResult(result, isStartDay, false); - }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: FlowyText( - timeStr.isNotEmpty ? timeStr : "", - textAlign: TextAlign.center, - ), - ), - ), - ), - ]); - } - - return Container( - constraints: const BoxConstraints(minHeight: 36), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Theme.of(context).colorScheme.secondaryContainer, - border: Border.all( - color: Theme.of(context).colorScheme.outline, - ), - ), - child: Row( - children: children, - ), - ); - } - - Future _showDateTimePicker( - BuildContext context, - DateTime? dateTime, { - required CupertinoDatePickerMode mode, - required bool use24hFormat, - }) async { - DateTime? result; - - return showMobileBottomSheet( - context, - builder: (context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: CupertinoDatePicker( - mode: mode, - initialDateTime: dateTime, - use24hFormat: use24hFormat, - onDateTimeChanged: (dateTime) { - result = dateTime; - }, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 36), - child: FlowyTextButton( - LocaleKeys.button_confirm.tr(), - constraints: const BoxConstraints.tightFor(height: 42), - mainAxisAlignment: MainAxisAlignment.center, - fontColor: Theme.of(context).colorScheme.onPrimary, - fillColor: Theme.of(context).primaryColor, - onPressed: () { - Navigator.of(context).pop(result); - }, - ), - ), - const VSpace(18.0), - ], - ), - ); - } - - void handleDateTimePickerResult( - DateTime? result, - bool isStartDay, - bool isDate, - ) { - if (result == null) { - return; - } - - if (isDate) { - final date = isStartDay ? dateTime : endDateTime; - - if (date != null) { - final timeComponent = Duration(hours: date.hour, minutes: date.minute); - result = - DateTime(result.year, result.month, result.day).add(timeComponent); - } - } - - if (isStartDay) { - onStartTimeChanged.call(result); - } else { - onEndTimeChanged?.call(result); - } - } - - String getDateStr(DateTime? dateTime) { - if (dateTime == null) { - return ""; - } - return DateFormat(dateFormat.pattern).format(dateTime); - } - - String getTimeStr(DateTime? dateTime) { - if (dateTime == null || !includeTime) { - return ""; - } - return DateFormat(timeFormat.pattern).format(dateTime); - } -} - -class _IsRangeSwitch extends StatelessWidget { - const _IsRangeSwitch({ - required this.isRange, - required this.onRangeChanged, - }); - - final bool isRange; - final Function(bool) onRangeChanged; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.toggle( - text: LocaleKeys.grid_field_isRange.tr(), - isSelected: isRange, - onValueChanged: onRangeChanged, - ); - } -} - -class _IncludeTimeSwitch extends StatelessWidget { - const _IncludeTimeSwitch({ - this.showTopBorder = true, - required this.includeTime, - required this.onIncludeTimeChanged, - }); - - final bool showTopBorder; - final bool includeTime; - final Function(bool) onIncludeTimeChanged; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.toggle( - showTopBorder: showTopBorder, - text: LocaleKeys.grid_field_includeTime.tr(), - isSelected: includeTime, - onValueChanged: onIncludeTimeChanged, - ); - } -} - -class _ClearDateButton extends StatelessWidget { - const _ClearDateButton({required this.onClearDate}); - - final VoidCallback onClearDate; - - @override - Widget build(BuildContext context) { - return FlowyOptionTile.text( - text: LocaleKeys.grid_field_clearDate.tr(), - onTap: onClearDate, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart index 3eaa674df8..c3157c0933 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart @@ -22,7 +22,7 @@ class ClearDateButton extends StatelessWidget { child: SizedBox( height: DatePickerSize.itemHeight, child: FlowyButton( - text: FlowyText(LocaleKeys.datePicker_clearDate.tr()), + text: FlowyText.medium(LocaleKeys.datePicker_clearDate.tr()), onTap: () { onClearDate(); PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart index 5cbdc2bc43..843852e70e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker.dart @@ -1,8 +1,10 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; import 'package:table_calendar/table_calendar.dart'; -import 'package:universal_platform/universal_platform.dart'; final kFirstDay = DateTime.utc(1970); final kLastDay = DateTime.utc(2100); @@ -15,7 +17,8 @@ class DatePicker extends StatefulWidget { this.startDay, this.endDay, this.selectedDay, - required this.focusedDay, + this.firstDay, + this.lastDay, this.onDaySelected, this.onRangeSelected, this.onCalendarCreated, @@ -29,14 +32,20 @@ class DatePicker extends StatefulWidget { final DateTime? endDay; final DateTime? selectedDay; - final DateTime focusedDay; + /// If not provided, defaults to 1st January 1970 + /// + final DateTime? firstDay; - final void Function( + /// If not provided, defaults to 1st January 2100 + /// + final DateTime? lastDay; + + final Function( DateTime selectedDay, DateTime focusedDay, )? onDaySelected; - final void Function( + final Function( DateTime? start, DateTime? end, DateTime focusedDay, @@ -51,6 +60,7 @@ class DatePicker extends StatefulWidget { } class _DatePickerState extends State { + late DateTime _focusedDay = widget.selectedDay ?? DateTime.now(); late CalendarFormat _calendarFormat = widget.calendarFormat; @override @@ -61,7 +71,7 @@ class _DatePickerState extends State { shape: BoxShape.circle, ); - final calendarStyle = UniversalPlatform.isMobile + final calendarStyle = PlatformExtension.isMobile ? _CalendarStyle.mobile( dowTextStyle: textStyle.copyWith( color: Theme.of(context).hintColor, @@ -69,6 +79,8 @@ class _DatePickerState extends State { ), ) : _CalendarStyle.desktop( + textStyle: textStyle, + iconColor: Theme.of(context).iconTheme.color, dowTextStyle: AFThemeExtension.of(context).caption, selectedColor: Theme.of(context).colorScheme.primary, ); @@ -76,9 +88,9 @@ class _DatePickerState extends State { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TableCalendar( - firstDay: kFirstDay, - lastDay: kLastDay, - focusedDay: widget.focusedDay, + firstDay: widget.firstDay ?? kFirstDay, + lastDay: widget.lastDay ?? kLastDay, + focusedDay: _focusedDay, rowHeight: calendarStyle.rowHeight, calendarFormat: _calendarFormat, daysOfWeekHeight: calendarStyle.dowHeight, @@ -145,6 +157,7 @@ class _DatePickerState extends State { setState(() => _calendarFormat = calendarFormat), onPageChanged: (focusedDay) { widget.onPageChanged?.call(focusedDay); + setState(() => _focusedDay = focusedDay); }, onDaySelected: widget.onDaySelected, onRangeSelected: widget.onRangeSelected, @@ -155,13 +168,26 @@ class _DatePickerState extends State { class _CalendarStyle { _CalendarStyle.desktop({ + required TextStyle textStyle, required this.selectedColor, required this.dowTextStyle, + Color? iconColor, }) : rowHeight = 33, dowHeight = 35, - headerVisible = false, - headerStyle = const HeaderStyle(), - availableGestures = AvailableGestures.horizontalSwipe; + headerVisible = true, + headerStyle = HeaderStyle( + formatButtonVisible: false, + titleCentered: true, + titleTextStyle: textStyle, + leftChevronMargin: EdgeInsets.zero, + leftChevronPadding: EdgeInsets.zero, + leftChevronIcon: FlowySvg(FlowySvgs.arrow_left_s, color: iconColor), + rightChevronPadding: EdgeInsets.zero, + rightChevronMargin: EdgeInsets.zero, + rightChevronIcon: FlowySvg(FlowySvgs.arrow_right_s, color: iconColor), + headerPadding: const EdgeInsets.only(bottom: 8.0), + ), + availableGestures = AvailableGestures.all; _CalendarStyle.mobile({required this.dowTextStyle}) : rowHeight = 48, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index 54fc2fac2a..10eb499292 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -1,5 +1,4 @@ -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; @@ -16,42 +15,58 @@ import 'package:flutter/services.dart'; class DatePickerOptions { DatePickerOptions({ DateTime? focusedDay, + this.popoverMutex, this.selectedDay, + this.firstDay, + this.lastDay, + this.timeStr, + this.endTimeStr, this.includeTime = false, this.isRange = false, + this.enableRanges = true, this.dateFormat = UserDateFormatPB.Friendly, this.timeFormat = UserTimeFormatPB.TwentyFourHour, this.selectedReminderOption, this.onDaySelected, - this.onIncludeTimeChanged, + required this.onIncludeTimeChanged, + this.onStartTimeChanged, + this.onEndTimeChanged, this.onRangeSelected, this.onIsRangeChanged, this.onReminderSelected, }) : focusedDay = focusedDay ?? DateTime.now(); final DateTime focusedDay; + final PopoverMutex? popoverMutex; final DateTime? selectedDay; + final DateTime? firstDay; + final DateTime? lastDay; + final String? timeStr; + final String? endTimeStr; final bool includeTime; final bool isRange; + final bool enableRanges; final UserDateFormatPB dateFormat; final UserTimeFormatPB timeFormat; final ReminderOption? selectedReminderOption; final DaySelectedCallback? onDaySelected; + final IncludeTimeChangedCallback onIncludeTimeChanged; + final TimeChangedCallback? onStartTimeChanged; + final TimeChangedCallback? onEndTimeChanged; final RangeSelectedCallback? onRangeSelected; - final IncludeTimeChangedCallback? onIncludeTimeChanged; - final IsRangeChangedCallback? onIsRangeChanged; + final Function(bool)? onIsRangeChanged; final OnReminderSelected? onReminderSelected; } abstract class DatePickerService { void show(Offset offset, {required DatePickerOptions options}); - void dismiss(); } const double _datePickerWidth = 260; -const double _datePickerHeight = 404; +const double _datePickerHeight = 370; +const double _includeTimeHeight = 32; const double _ySpacing = 15; class DatePickerMenu extends DatePickerService { @@ -59,7 +74,6 @@ class DatePickerMenu extends DatePickerService { final BuildContext context; final EditorState editorState; - PopoverMutex? popoverMutex; OverlayEntry? _menuEntry; @@ -67,9 +81,6 @@ class DatePickerMenu extends DatePickerService { void dismiss() { _menuEntry?.remove(); _menuEntry = null; - popoverMutex?.close(); - popoverMutex?.dispose(); - popoverMutex = null; } @override @@ -100,7 +111,6 @@ class DatePickerMenu extends DatePickerService { } } - popoverMutex = PopoverMutex(); _menuEntry = OverlayEntry( builder: (_) => Material( type: MaterialType.transparency, @@ -123,7 +133,6 @@ class DatePickerMenu extends DatePickerService { offset: Offset(offsetX, offsetY), showBelow: showBelow, options: options, - popoverMutex: popoverMutex, ), ], ), @@ -137,47 +146,67 @@ class DatePickerMenu extends DatePickerService { } } -class _AnimatedDatePicker extends StatelessWidget { +class _AnimatedDatePicker extends StatefulWidget { const _AnimatedDatePicker({ required this.offset, required this.showBelow, required this.options, - this.popoverMutex, }); final Offset offset; final bool showBelow; final DatePickerOptions options; - final PopoverMutex? popoverMutex; + + @override + State<_AnimatedDatePicker> createState() => _AnimatedDatePickerState(); +} + +class _AnimatedDatePickerState extends State<_AnimatedDatePicker> { + late bool _includeTime = widget.options.includeTime; @override Widget build(BuildContext context) { - final dy = offset.dy + (showBelow ? _ySpacing : -_ySpacing); + double dy = widget.offset.dy; + if (!widget.showBelow && _includeTime) { + dy -= _includeTimeHeight; + } + + dy += (widget.showBelow ? _ySpacing : -_ySpacing); return AnimatedPositioned( duration: const Duration(milliseconds: 200), top: dy, - left: offset.dx, + left: widget.offset.dx, child: Container( decoration: FlowyDecoration.decoration( Theme.of(context).cardColor, Theme.of(context).colorScheme.shadow, ), constraints: BoxConstraints.loose(const Size(_datePickerWidth, 465)), - child: DesktopAppFlowyDatePicker( - includeTime: options.includeTime, - onIncludeTimeChanged: options.onIncludeTimeChanged, - isRange: options.isRange, - onIsRangeChanged: options.onIsRangeChanged, - dateFormat: options.dateFormat.simplified, - timeFormat: options.timeFormat.simplified, - dateTime: options.selectedDay, - popoverMutex: popoverMutex, - reminderOption: options.selectedReminderOption ?? ReminderOption.none, - onDaySelected: options.onDaySelected, - onRangeSelected: options.onRangeSelected, - onReminderSelected: options.onReminderSelected, - enableDidUpdate: false, + child: AppFlowyDatePicker( + includeTime: _includeTime, + onIncludeTimeChanged: (includeTime) { + widget.options.onIncludeTimeChanged.call(!includeTime); + setState(() => _includeTime = !includeTime); + }, + enableRanges: widget.options.enableRanges, + isRange: widget.options.isRange, + onIsRangeChanged: widget.options.onIsRangeChanged, + dateFormat: widget.options.dateFormat.simplified, + timeFormat: widget.options.timeFormat.simplified, + selectedDay: widget.options.selectedDay, + focusedDay: widget.options.focusedDay, + firstDay: widget.options.firstDay, + lastDay: widget.options.lastDay, + timeStr: widget.options.timeStr, + endTimeStr: widget.options.endTimeStr, + popoverMutex: widget.options.popoverMutex, + selectedReminderOption: + widget.options.selectedReminderOption ?? ReminderOption.none, + onStartTimeSubmitted: widget.options.onStartTimeChanged, + onDaySelected: widget.options.onDaySelected, + onRangeSelected: widget.options.onRangeSelected, + onReminderSelected: widget.options.onReminderSelected, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart index 7447700fef..0c9c6aaa8e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; class DateTimeSetting extends StatefulWidget { @@ -28,12 +29,6 @@ class _DateTimeSettingState extends State { final timeSettingPopoverMutex = PopoverMutex(); String? overlayIdentifier; - @override - void dispose() { - timeSettingPopoverMutex.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { final List children = [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart deleted file mode 100644 index 553ffb4c0d..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart +++ /dev/null @@ -1,377 +0,0 @@ -import 'package:any_date/any_date.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/size.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../desktop_date_picker.dart'; -import 'date_picker.dart'; - -class DateTimeTextField extends StatefulWidget { - const DateTimeTextField({ - super.key, - required this.dateTime, - required this.includeTime, - required this.dateFormat, - this.timeFormat, - this.onSubmitted, - this.popoverMutex, - this.isTabPressed, - this.refreshTextController, - required this.showHint, - }) : assert(includeTime && timeFormat != null || !includeTime); - - final DateTime? dateTime; - final bool includeTime; - final void Function(DateTime dateTime)? onSubmitted; - final DateFormatPB dateFormat; - final TimeFormatPB? timeFormat; - final PopoverMutex? popoverMutex; - final ValueNotifier? isTabPressed; - final RefreshDateTimeTextFieldController? refreshTextController; - final bool showHint; - - @override - State createState() => _DateTimeTextFieldState(); -} - -class _DateTimeTextFieldState extends State { - late final FocusNode focusNode; - late final FocusNode dateFocusNode; - late final FocusNode timeFocusNode; - - final dateTextController = TextEditingController(); - final timeTextController = TextEditingController(); - - final statesController = WidgetStatesController(); - - bool justSubmitted = false; - - DateFormat get dateFormat => DateFormat(widget.dateFormat.pattern); - DateFormat get timeFormat => DateFormat(widget.timeFormat?.pattern); - - @override - void initState() { - super.initState(); - updateTextControllers(); - - focusNode = FocusNode()..addListener(focusNodeListener); - dateFocusNode = FocusNode(onKeyEvent: textFieldOnKeyEvent) - ..addListener(dateFocusNodeListener); - timeFocusNode = FocusNode(onKeyEvent: textFieldOnKeyEvent) - ..addListener(timeFocusNodeListener); - widget.isTabPressed?.addListener(isTabPressedListener); - widget.refreshTextController?.addListener(updateTextControllers); - widget.popoverMutex?.addPopoverListener(popoverListener); - } - - @override - void didUpdateWidget(covariant oldWidget) { - if (oldWidget.dateTime != widget.dateTime || - oldWidget.dateFormat != widget.dateFormat || - oldWidget.timeFormat != widget.timeFormat) { - statesController.update(WidgetState.error, false); - updateTextControllers(); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - dateTextController.dispose(); - timeTextController.dispose(); - widget.popoverMutex?.removePopoverListener(popoverListener); - widget.isTabPressed?.removeListener(isTabPressedListener); - widget.refreshTextController?.removeListener(updateTextControllers); - dateFocusNode - ..removeListener(dateFocusNodeListener) - ..dispose(); - timeFocusNode - ..removeListener(timeFocusNodeListener) - ..dispose(); - focusNode - ..removeListener(focusNodeListener) - ..dispose(); - statesController.dispose(); - super.dispose(); - } - - void focusNodeListener() { - if (focusNode.hasFocus) { - statesController.update(WidgetState.focused, true); - widget.popoverMutex?.close(); - } else { - statesController.update(WidgetState.focused, false); - } - } - - void isTabPressedListener() { - if (!dateFocusNode.hasFocus && !timeFocusNode.hasFocus) { - return; - } - final controller = - dateFocusNode.hasFocus ? dateTextController : timeTextController; - if (widget.isTabPressed != null && widget.isTabPressed!.value) { - controller.selection = TextSelection( - baseOffset: 0, - extentOffset: controller.text.characters.length, - ); - widget.isTabPressed?.value = false; - } - } - - KeyEventResult textFieldOnKeyEvent(FocusNode node, KeyEvent event) { - if (event is KeyUpEvent && event.logicalKey == LogicalKeyboardKey.tab) { - widget.isTabPressed?.value = true; - } - return KeyEventResult.ignored; - } - - void dateFocusNodeListener() { - if (dateFocusNode.hasFocus || justSubmitted) { - justSubmitted = true; - return; - } - - final expected = widget.dateTime == null - ? "" - : DateFormat(widget.dateFormat.pattern).format(widget.dateTime!); - if (expected != dateTextController.text.trim()) { - onDateTextFieldSubmitted(); - } - } - - void timeFocusNodeListener() { - if (timeFocusNode.hasFocus || widget.timeFormat == null || justSubmitted) { - justSubmitted = true; - return; - } - - final expected = widget.dateTime == null - ? "" - : DateFormat(widget.timeFormat!.pattern).format(widget.dateTime!); - if (expected != timeTextController.text.trim()) { - onTimeTextFieldSubmitted(); - } - } - - void popoverListener() { - if (focusNode.hasFocus) { - focusNode.unfocus(); - } - } - - void updateTextControllers() { - if (widget.dateTime == null) { - dateTextController.clear(); - timeTextController.clear(); - return; - } - - dateTextController.text = dateFormat.format(widget.dateTime!); - timeTextController.text = timeFormat.format(widget.dateTime!); - } - - void onDateTextFieldSubmitted() { - DateTime? dateTime = parseDateTimeStr(dateTextController.text.trim()); - if (dateTime == null) { - statesController.update(WidgetState.error, true); - return; - } - statesController.update(WidgetState.error, false); - if (widget.dateTime != null) { - final timeComponent = Duration( - hours: widget.dateTime!.hour, - minutes: widget.dateTime!.minute, - seconds: widget.dateTime!.second, - ); - dateTime = DateTime( - dateTime.year, - dateTime.month, - dateTime.day, - ).add(timeComponent); - } - widget.onSubmitted?.call(dateTime); - } - - void onTimeTextFieldSubmitted() { - // this happens in the middle of a date range selection - if (widget.dateTime == null) { - widget.refreshTextController?.refresh(); - statesController.update(WidgetState.error, true); - return; - } - final adjustedTimeStr = - "${dateTextController.text} ${timeTextController.text.trim()}"; - final dateTime = parseDateTimeStr(adjustedTimeStr); - - if (dateTime == null) { - statesController.update(WidgetState.error, true); - return; - } - statesController.update(WidgetState.error, false); - widget.onSubmitted?.call(dateTime); - } - - DateTime? parseDateTimeStr(String string) { - final locale = context.locale.toLanguageTag(); - final parser = AnyDate.fromLocale(locale); - final result = parser.tryParse(string); - if (result == null || - result.isBefore(kFirstDay) || - result.isAfter(kLastDay)) { - return null; - } - return result; - } - - late final WidgetStateProperty borderColor = - WidgetStateProperty.resolveWith( - (states) { - if (states.contains(WidgetState.error)) { - return Theme.of(context).colorScheme.errorContainer; - } - if (states.contains(WidgetState.focused)) { - return Theme.of(context).colorScheme.primary; - } - return Theme.of(context).colorScheme.outline; - }, - ); - - @override - Widget build(BuildContext context) { - final now = DateTime.now(); - final hintDate = DateTime(now.year, now.month, 1, 9); - - return Focus( - focusNode: focusNode, - skipTraversal: true, - child: wrapWithGestures( - child: ListenableBuilder( - listenable: statesController, - builder: (context, child) { - final resolved = borderColor.resolve(statesController.value); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: Container( - constraints: const BoxConstraints.tightFor(height: 32), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide( - color: resolved ?? Colors.transparent, - ), - ), - borderRadius: Corners.s8Border, - ), - child: child, - ), - ); - }, - child: widget.includeTime - ? Row( - children: [ - Expanded( - child: TextField( - key: const ValueKey('date_time_text_field_date'), - focusNode: dateFocusNode, - controller: dateTextController, - style: Theme.of(context).textTheme.bodyMedium, - decoration: getInputDecoration( - const EdgeInsetsDirectional.fromSTEB(12, 6, 6, 6), - dateFormat.format(hintDate), - ), - onSubmitted: (value) { - justSubmitted = true; - onDateTextFieldSubmitted(); - }, - ), - ), - VerticalDivider( - indent: 4, - endIndent: 4, - width: 1, - color: Theme.of(context).colorScheme.outline, - ), - Expanded( - child: TextField( - key: const ValueKey('date_time_text_field_time'), - focusNode: timeFocusNode, - controller: timeTextController, - style: Theme.of(context).textTheme.bodyMedium, - maxLength: widget.timeFormat == TimeFormatPB.TwelveHour - ? 8 // 12:34 PM = 8 characters - : 5, // 12:34 = 5 characters - inputFormatters: [ - FilteringTextInputFormatter.allow( - RegExp('[0-9:AaPpMm]'), - ), - ], - decoration: getInputDecoration( - const EdgeInsetsDirectional.fromSTEB(6, 6, 12, 6), - timeFormat.format(hintDate), - ), - onSubmitted: (value) { - justSubmitted = true; - onTimeTextFieldSubmitted(); - }, - ), - ), - ], - ) - : Center( - child: TextField( - key: const ValueKey('date_time_text_field_date'), - focusNode: dateFocusNode, - controller: dateTextController, - style: Theme.of(context).textTheme.bodyMedium, - decoration: getInputDecoration( - const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - dateFormat.format(hintDate), - ), - onSubmitted: (value) { - justSubmitted = true; - onDateTextFieldSubmitted(); - }, - ), - ), - ), - ), - ); - } - - Widget wrapWithGestures({required Widget child}) { - return GestureDetector( - onTapDown: (_) { - statesController.update(WidgetState.pressed, true); - }, - onTapCancel: () { - statesController.update(WidgetState.pressed, false); - }, - onTap: () { - statesController.update(WidgetState.pressed, false); - }, - child: child, - ); - } - - InputDecoration getInputDecoration( - EdgeInsetsGeometry padding, - String? hintText, - ) { - return InputDecoration( - border: InputBorder.none, - contentPadding: padding, - isCollapsed: true, - isDense: true, - hintText: widget.showHint ? hintText : null, - counterText: "", - hintStyle: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).hintColor), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart index 9bb819a243..b4224bb05a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart @@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -38,7 +39,7 @@ class DateTypeOptionButton extends StatelessWidget { child: SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText(title), + text: FlowyText.medium(title), rightIcon: const FlowySvg(FlowySvgs.more_s), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart new file mode 100644 index 0000000000..fae8782af5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +class EndTextField extends StatelessWidget { + const EndTextField({ + super.key, + required this.includeTime, + required this.isRange, + required this.timeFormat, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool includeTime; + final bool isRange; + final TimeFormatPB timeFormat; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: includeTime && isRange + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TimeTextField( + isEndTime: true, + timeFormat: timeFormat, + endTimeStr: endTimeStr, + popoverMutex: popoverMutex, + onSubmitted: onSubmitted, + ), + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart index fdb24fb761..6f60257929 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart @@ -1,12 +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/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; class EndTimeButton extends StatelessWidget { const EndTimeButton({ @@ -33,11 +33,12 @@ class EndTimeButton extends StatelessWidget { color: Theme.of(context).iconTheme.color, ), const HSpace(6), - FlowyText(LocaleKeys.datePicker_isRange.tr()), + FlowyText.medium(LocaleKeys.datePicker_isRange.tr()), const Spacer(), Toggle( value: isRange, onChanged: onChanged, + style: ToggleStyle.big, padding: EdgeInsets.zero, ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart index 4d8176ba5c..60476712e6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -9,32 +10,39 @@ class MobileDatePicker extends StatefulWidget { const MobileDatePicker({ super.key, this.selectedDay, - this.startDay, - this.endDay, - required this.focusedDay, required this.isRange, this.onDaySelected, + this.rebuildOnDaySelected = false, this.onRangeSelected, - this.onPageChanged, + this.firstDay, + this.lastDay, + this.startDay, + this.endDay, }); final DateTime? selectedDay; - final DateTime? startDay; - final DateTime? endDay; - final DateTime focusedDay; final bool isRange; - final void Function(DateTime)? onDaySelected; - final void Function(DateTime?, DateTime?)? onRangeSelected; - final void Function(DateTime)? onPageChanged; + final DaySelectedCallback? onDaySelected; + + final bool rebuildOnDaySelected; + final RangeSelectedCallback? onRangeSelected; + + final DateTime? firstDay; + final DateTime? lastDay; + final DateTime? startDay; + final DateTime? endDay; @override State createState() => _MobileDatePickerState(); } class _MobileDatePickerState extends State { - PageController? pageController; + PageController? _pageController; + + late DateTime _focusedDay = widget.selectedDay ?? DateTime.now(); + late DateTime? _selectedDay = widget.selectedDay; @override Widget build(BuildContext context) { @@ -52,64 +60,60 @@ class _MobileDatePickerState extends State { Widget _buildCalendar(BuildContext context) { return DatePicker( isRange: widget.isRange, - onDaySelected: (selectedDay, _) { - widget.onDaySelected?.call(selectedDay); + onDaySelected: (selectedDay, focusedDay) { + widget.onDaySelected?.call(selectedDay, focusedDay); + + if (widget.rebuildOnDaySelected) { + setState(() => _selectedDay = selectedDay); + } }, - focusedDay: widget.focusedDay, - onRangeSelected: (start, end, focusedDay) { - widget.onRangeSelected?.call(start, end); - }, - selectedDay: widget.selectedDay, + onRangeSelected: widget.onRangeSelected, + selectedDay: + widget.rebuildOnDaySelected ? _selectedDay : widget.selectedDay, + firstDay: widget.firstDay, + lastDay: widget.lastDay, startDay: widget.startDay, endDay: widget.endDay, - onCalendarCreated: (pageController) { - this.pageController = pageController; - }, - onPageChanged: widget.onPageChanged, + onCalendarCreated: (pageController) => _pageController = pageController, + onPageChanged: (focusedDay) => setState(() => _focusedDay = focusedDay), ); } Widget _buildHeader(BuildContext context) { - return Padding( - padding: const EdgeInsetsDirectional.only(start: 16, end: 8), - child: Row( - children: [ - Expanded( - child: FlowyText( - DateFormat.yMMMM().format(widget.focusedDay), - ), + return Row( + children: [ + const HSpace(16.0), + FlowyText( + DateFormat.yMMMM().format(_focusedDay), + ), + const Spacer(), + FlowyButton( + useIntrinsicWidth: true, + text: FlowySvg( + FlowySvgs.arrow_left_s, + color: Theme.of(context).iconTheme.color, + size: const Size.square(24.0), ), - FlowyButton( - useIntrinsicWidth: true, - text: FlowySvg( - FlowySvgs.arrow_left_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(24.0), - ), - onTap: () { - pageController?.previousPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - }, + onTap: () => _pageController?.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, ), - const HSpace(24.0), - FlowyButton( - useIntrinsicWidth: true, - text: FlowySvg( - FlowySvgs.arrow_right_s, - color: Theme.of(context).iconTheme.color, - size: const Size.square(24.0), - ), - onTap: () { - pageController?.nextPage( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - }, + ), + const HSpace(24.0), + FlowyButton( + useIntrinsicWidth: true, + text: FlowySvg( + FlowySvgs.arrow_right_s, + color: Theme.of(context).iconTheme.color, + size: const Size.square(24.0), ), - ], - ), + onTap: () => _pageController?.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ), + ), + const HSpace(8.0), + ], ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart index d795e2ab7d..d7f88e8d12 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart @@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -50,7 +51,7 @@ class ReminderSelector extends StatelessWidget { return SizedBox( height: DatePickerSize.itemHeight, child: FlowyButton( - text: FlowyText(label), + text: FlowyText.medium(label), rightIcon: o == selectedOption ? const FlowySvg(FlowySvgs.check_s) : null, onTap: () { @@ -86,7 +87,7 @@ class ReminderSelector extends StatelessWidget { child: SizedBox( height: DatePickerSize.itemHeight, child: FlowyButton( - text: FlowyText(LocaleKeys.datePicker_reminderLabel.tr()), + text: FlowyText.medium(LocaleKeys.datePicker_reminderLabel.tr()), rightIcon: Row( children: [ FlowyText.regular(selectedOption.label), @@ -124,14 +125,10 @@ enum ReminderOption { required this.time, this.withoutTime = false, this.requiresNoTime = false, - }) : assert(!requiresNoTime || withoutTime); + }); final Duration time; - - /// If true, don't consider the time component of the dateTime final bool withoutTime; - - /// If true, [withoutTime] must be true as well. Will add time instead of subtract to get notification time. final bool requiresNoTime; bool get timeExempt => @@ -194,11 +191,10 @@ enum ReminderOption { _ => ReminderOption.custom, }; - DateTime getNotificationDateTime(DateTime date) { - return withoutTime - ? requiresNoTime + DateTime fromDate(DateTime date) => switch (withoutTime) { + true => requiresNoTime ? date.withoutTime.add(time) - : date.withoutTime.subtract(time) - : date.subtract(time); - } + : date.withoutTime.subtract(time), + _ => date.subtract(time), + }; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart new file mode 100644 index 0000000000..77afa8f7e1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/start_text_field.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; + +class StartTextField extends StatelessWidget { + const StartTextField({ + super.key, + required this.includeTime, + required this.timeFormat, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.timeStr, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool includeTime; + final TimeFormatPB timeFormat; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final String? timeStr; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: includeTime + ? TimeTextField( + isEndTime: false, + timeFormat: timeFormat, + timeHintText: timeHintText, + parseEndTimeError: parseEndTimeError, + parseTimeError: parseTimeError, + timeStr: timeStr, + endTimeStr: endTimeStr, + popoverMutex: popoverMutex, + onSubmitted: onSubmitted, + ) + : const SizedBox.shrink(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart new file mode 100644 index 0000000000..c0f5cc8308 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/text_field.dart'; +import 'package:intl/intl.dart'; + +const _maxLengthTwelveHour = 8; +const _maxLengthTwentyFourHour = 5; + +class TimeTextField extends StatefulWidget { + const TimeTextField({ + super.key, + required this.isEndTime, + required this.timeFormat, + this.timeHintText, + this.parseEndTimeError, + this.parseTimeError, + this.timeStr, + this.endTimeStr, + this.popoverMutex, + this.onSubmitted, + }); + + final bool isEndTime; + final TimeFormatPB timeFormat; + final String? timeHintText; + final String? parseEndTimeError; + final String? parseTimeError; + final String? timeStr; + final String? endTimeStr; + final PopoverMutex? popoverMutex; + final Function(String timeStr)? onSubmitted; + + @override + State createState() => _TimeTextFieldState(); +} + +class _TimeTextFieldState extends State { + final FocusNode _focusNode = FocusNode(); + late final TextEditingController _textController = TextEditingController() + ..text = widget.timeStr ?? ""; + String text = ""; + + @override + void initState() { + super.initState(); + + _textController.text = + (widget.isEndTime ? widget.endTimeStr : widget.timeStr) ?? ""; + + if (!widget.isEndTime && widget.timeStr != null) { + text = widget.timeStr!; + } else if (widget.endTimeStr != null) { + text = widget.endTimeStr!; + } + + if (widget.timeFormat == TimeFormatPB.TwelveHour) { + final twentyFourHourFormat = DateFormat('HH:mm'); + final twelveHourFormat = DateFormat('hh:mm a'); + final date = twentyFourHourFormat.parse(text); + text = twelveHourFormat.format(date); + } + + _focusNode.addListener(_focusNodeListener); + widget.popoverMutex?.listenOnPopoverChanged(_popoverListener); + } + + @override + void dispose() { + widget.popoverMutex?.removePopoverListener(_popoverListener); + _textController.dispose(); + _focusNode.removeListener(_focusNodeListener); + _focusNode.dispose(); + super.dispose(); + } + + void _focusNodeListener() { + if (_focusNode.hasFocus) { + widget.popoverMutex?.close(); + } + } + + void _popoverListener() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: FlowyTextField( + text: text, + keyboardType: TextInputType.datetime, + focusNode: _focusNode, + autoFocus: false, + controller: _textController, + submitOnLeave: true, + hintText: widget.timeHintText, + errorText: + widget.isEndTime ? widget.parseEndTimeError : widget.parseTimeError, + maxLength: widget.timeFormat == TimeFormatPB.TwelveHour + ? _maxLengthTwelveHour + : _maxLengthTwentyFourHour, + showCounter: false, + inputFormatters: [ + if (widget.timeFormat == TimeFormatPB.TwelveHour) ...[ + // Allow for AM/PM if time format is 12-hour + FilteringTextInputFormatter.allow(RegExp('[0-9:aApPmM ]')), + ] else ...[ + // Default allow for hh:mm format + FilteringTextInputFormatter.allow(RegExp('[0-9:]')), + ], + TimeInputFormatter(widget.timeFormat), + ], + onSubmitted: widget.onSubmitted, + ), + ); + } +} + +class TimeInputFormatter extends TextInputFormatter { + TimeInputFormatter(this.timeFormat); + + final TimeFormatPB timeFormat; + static const int colonPosition = 2; + static const int spacePosition = 5; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final oldText = oldValue.text; + final newText = newValue.text; + + // If the user has typed enough for a time separator(:) and hasn't already typed + if (newText.length == colonPosition + 1 && + oldText.length == colonPosition && + !newText.contains(":")) { + return _formatText(newText, colonPosition, ':'); + } + + // If the user has typed enough for an AM/PM separator and hasn't already typed + if (timeFormat == TimeFormatPB.TwelveHour && + newText.length == spacePosition + 1 && + oldText.length == spacePosition && + newText[newText.length - 1] != ' ') { + return _formatText(newText, spacePosition, ' '); + } + + if (timeFormat == TimeFormatPB.TwentyFourHour && + newValue.text.length == 5) { + final prefix = newValue.text.substring(0, 3); + final suffix = newValue.text.length > 5 ? newValue.text.substring(6) : ''; + + final minutes = int.tryParse(newValue.text.substring(3, 5)); + if (minutes == null || minutes <= 0) { + return newValue.copyWith(text: '${prefix}00$suffix'.toUpperCase()); + } else if (minutes > 59) { + return newValue.copyWith(text: '${prefix}59$suffix'.toUpperCase()); + } + } + + return newValue.copyWith(text: newText.toUpperCase()); + } + + TextEditingValue _formatText(String text, int index, String separator) { + return TextEditingValue( + text: '${text.substring(0, index)}$separator${text.substring(index)}', + selection: TextSelection.collapsed(offset: text.length + 1), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart deleted file mode 100644 index 43ab8897e1..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -typedef SimpleAFDialogAction = (String, void Function(BuildContext)?); - -/// A simple dialog with a title, content, and actions. -/// -/// The primary button is a filled button and colored using theme or destructive -/// color depending on the [isDestructive] parameter. The secondary button is an -/// outlined button. -/// -Future showSimpleAFDialog({ - required BuildContext context, - required String title, - required String content, - bool isDestructive = false, - required SimpleAFDialogAction primaryAction, - SimpleAFDialogAction? secondaryAction, - bool barrierDismissible = true, -}) { - final theme = AppFlowyTheme.of(context); - - return showDialog( - context: context, - barrierColor: theme.surfaceColorScheme.overlay, - barrierDismissible: barrierDismissible, - builder: (_) { - return AFModal( - constraints: BoxConstraints( - maxWidth: AFModalDimension.S, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AFModalHeader( - leading: Text( - title, - style: theme.textStyle.heading4.standard( - color: theme.textColorScheme.primary, - ), - ), - trailing: [ - AFGhostButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return FlowySvg( - FlowySvgs.close_s, - size: Size.square(20), - ); - }, - ), - ], - ), - Flexible( - child: ConstrainedBox( - // AFModalDimension.dialogHeight - header - footer - constraints: BoxConstraints(minHeight: 108.0), - child: AFModalBody( - child: Text(content), - ), - ), - ), - AFModalFooter( - trailing: [ - if (secondaryAction != null) - AFOutlinedButton.normal( - onTap: () { - secondaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text(secondaryAction.$1); - }, - ), - isDestructive - ? AFFilledButton.destructive( - onTap: () { - primaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text( - primaryAction.$1, - style: TextStyle( - color: AppFlowyTheme.of(context) - .textColorScheme - .onFill, - ), - ); - }, - ) - : AFFilledButton.primary( - onTap: () { - primaryAction.$2?.call(context); - Navigator.of(context).pop(); - }, - builder: (context, isHovering, disabled) { - return Text(primaryAction.$1); - }, - ), - ], - ), - ], - ), - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 8d65ee23bb..66cad4ab38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,8 +1,7 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; -import 'package:appflowy/util/theme_extension.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; @@ -11,68 +10,8 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:toastification/toastification.dart'; -import 'package:universal_platform/universal_platform.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; -export 'package:toastification/toastification.dart'; - -class NavigatorCustomDialog extends StatefulWidget { - const NavigatorCustomDialog({ - super.key, - required this.child, - this.cancel, - this.confirm, - this.hideCancelButton = false, - }); - - final Widget child; - final void Function()? cancel; - final void Function()? confirm; - final bool hideCancelButton; - - @override - State createState() => _NavigatorCustomDialog(); -} - -class _NavigatorCustomDialog extends State { - @override - Widget build(BuildContext context) { - return StyledDialog( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ...[ - ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - maxHeight: 260, - ), - child: widget.child, - ), - ], - if (widget.confirm != null) ...[ - const VSpace(20), - OkCancelButton( - onOkPressed: () { - widget.confirm?.call(); - Navigator.of(context).pop(); - }, - onCancelPressed: widget.hideCancelButton - ? null - : () { - widget.cancel?.call(); - Navigator.of(context).pop(); - }, - ), - ], - ], - ), - ); - } -} class NavigatorTextFieldDialog extends StatefulWidget { const NavigatorTextFieldDialog({ @@ -155,12 +94,6 @@ class _NavigatorTextFieldDialogState extends State { VSpace(Insets.xl), OkCancelButton( onOkPressed: () { - if (newValue.isEmpty) { - showToastNotification( - message: LocaleKeys.space_spaceNameCannotBeEmpty.tr(), - ); - return; - } widget.onConfirm(newValue, context); Navigator.of(context).pop(); }, @@ -196,6 +129,11 @@ class NavigatorAlertDialog extends StatefulWidget { } class _CreateFlowyAlertDialog extends State { + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { return StyledDialog( @@ -250,8 +188,6 @@ class NavigatorOkCancelDialog extends StatelessWidget { this.title, this.message, this.maxWidth, - this.titleUpperCase = true, - this.autoDismiss = true, }); final VoidCallback? onOkPressed; @@ -261,19 +197,9 @@ class NavigatorOkCancelDialog extends StatelessWidget { final String? title; final String? message; final double? maxWidth; - final bool titleUpperCase; - final bool autoDismiss; @override Widget build(BuildContext context) { - final onCancel = onCancelPressed == null - ? null - : () { - onCancelPressed?.call(); - if (autoDismiss) { - Navigator.of(context).pop(); - } - }; return StyledDialog( maxWidth: maxWidth ?? 500, padding: EdgeInsets.symmetric(horizontal: Insets.xl, vertical: Insets.l), @@ -282,7 +208,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { children: [ if (title != null) ...[ FlowyText.medium( - titleUpperCase ? title!.toUpperCase() : title!, + title!.toUpperCase(), fontSize: FontSizes.s16, maxLines: 3, ), @@ -302,11 +228,12 @@ class NavigatorOkCancelDialog extends StatelessWidget { OkCancelButton( onOkPressed: () { onOkPressed?.call(); - if (autoDismiss) { - Navigator.of(context).pop(); - } + Navigator.of(context).pop(); + }, + onCancelPressed: () { + onCancelPressed?.call(); + Navigator.of(context).pop(); }, - onCancelPressed: onCancel, okTitle: okTitle?.toUpperCase(), cancelTitle: cancelTitle?.toUpperCase(), ), @@ -361,380 +288,3 @@ class OkCancelButton extends StatelessWidget { ); } } - -ToastificationItem showToastNotification({ - String? message, - TextSpan? richMessage, - String? description, - ToastificationType type = ToastificationType.success, - ToastificationCallbacks? callbacks, - double bottomPadding = 100, -}) { - assert( - (message == null) != (richMessage == null), - "Exactly one of message or richMessage must be non-null.", - ); - return toastification.showCustom( - alignment: Alignment.bottomCenter, - autoCloseDuration: const Duration(milliseconds: 3000), - callbacks: callbacks ?? const ToastificationCallbacks(), - builder: (_, item) { - return UniversalPlatform.isMobile - ? _MobileToast( - message: message, - type: type, - bottomPadding: bottomPadding, - description: description, - ) - : DesktopToast( - message: message, - richMessage: richMessage, - type: type, - onDismiss: () => toastification.dismiss(item), - ); - }, - ); -} - -class _MobileToast extends StatelessWidget { - const _MobileToast({ - this.message, - this.type = ToastificationType.success, - this.bottomPadding = 100, - this.description, - }); - - final String? message; - final ToastificationType type; - final double bottomPadding; - final String? description; - - @override - Widget build(BuildContext context) { - if (message == null) { - return const SizedBox.shrink(); - } - final hintText = FlowyText.regular( - message!, - fontSize: 16.0, - figmaLineHeight: 18.0, - color: Colors.white, - maxLines: 10, - ); - final descriptionText = description != null - ? FlowyText.regular( - description!, - fontSize: 12, - color: Colors.white, - maxLines: 10, - ) - : null; - return Container( - alignment: Alignment.bottomCenter, - padding: EdgeInsets.only( - bottom: bottomPadding, - left: 16, - right: 16, - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 13.0, - ), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.0), - color: const Color(0xE5171717), - ), - child: type == ToastificationType.success - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (type == ToastificationType.success) ...[ - const FlowySvg( - FlowySvgs.success_s, - blendMode: null, - ), - const HSpace(8.0), - ], - Expanded(child: hintText), - ], - ), - if (descriptionText != null) ...[ - const VSpace(4.0), - descriptionText, - ], - ], - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - hintText, - if (descriptionText != null) ...[ - const VSpace(4.0), - descriptionText, - ], - ], - ), - ), - ); - } -} - -@visibleForTesting -class DesktopToast extends StatelessWidget { - const DesktopToast({ - super.key, - this.message, - this.richMessage, - required this.type, - this.onDismiss, - }); - - final String? message; - final TextSpan? richMessage; - final ToastificationType type; - final void Function()? onDismiss; - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 360.0), - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - margin: const EdgeInsets.only(bottom: 32.0), - decoration: BoxDecoration( - color: Theme.of(context).isLightMode - ? const Color(0xFF333333) - : const Color(0xFF363D49), - borderRadius: const BorderRadius.all(Radius.circular(8.0)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // icon - FlowySvg( - switch (type) { - ToastificationType.warning => FlowySvgs.toast_warning_filled_s, - ToastificationType.success => FlowySvgs.toast_checked_filled_s, - ToastificationType.error => FlowySvgs.toast_error_filled_s, - _ => throw UnimplementedError(), - }, - size: const Size.square(20.0), - blendMode: null, - ), - const HSpace(8.0), - // text - Flexible( - child: message != null - ? FlowyText( - message!, - maxLines: 2, - figmaLineHeight: 20.0, - overflow: TextOverflow.ellipsis, - color: const Color(0xFFFFFFFF), - ) - : RichText( - text: richMessage!, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - const HSpace(16.0), - // close - MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onDismiss, - child: const SizedBox.square( - dimension: 24.0, - child: Center( - child: FlowySvg( - FlowySvgs.toast_close_s, - size: Size.square(16.0), - color: Color(0xFFBDBDBD), - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -Future showConfirmDeletionDialog({ - required BuildContext context, - required String name, - required String description, - required VoidCallback onConfirm, -}) { - return showDialog( - context: context, - builder: (_) { - final title = LocaleKeys.space_deleteConfirmation.tr() + name; - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: ConfirmPopup( - title: title, - description: description, - onConfirm: onConfirm, - ), - ), - ); - }, - ); -} - -Future showConfirmDialog({ - required BuildContext context, - required String title, - required String description, - VoidCallback? onConfirm, - VoidCallback? onCancel, - String? confirmLabel, - ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk, - WidgetBuilder? confirmButtonBuilder, -}) { - return showDialog( - context: context, - builder: (_) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: ConfirmPopup( - title: title, - description: description, - confirmButtonBuilder: confirmButtonBuilder, - onConfirm: () => onConfirm?.call(), - onCancel: () => onCancel?.call(), - confirmLabel: confirmLabel, - style: style, - ), - ), - ); - }, - ); -} - -Future showCancelAndConfirmDialog({ - required BuildContext context, - required String title, - required String description, - VoidCallback? onConfirm, - VoidCallback? onCancel, - String? confirmLabel, -}) { - return showDialog( - context: context, - builder: (_) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: ConfirmPopup( - title: title, - description: description, - onConfirm: () => onConfirm?.call(), - confirmLabel: confirmLabel, - confirmButtonColor: Theme.of(context).colorScheme.primary, - onCancel: () => onCancel?.call(), - ), - ), - ); - }, - ); -} - -Future showCustomConfirmDialog({ - required BuildContext context, - required String title, - required String description, - required Widget Function(BuildContext) builder, - VoidCallback? onConfirm, - VoidCallback? onCancel, - String? confirmLabel, - ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk, - bool closeOnConfirm = true, - bool showCloseButton = true, - bool enableKeyboardListener = true, - bool barrierDismissible = true, -}) { - return showDialog( - context: context, - barrierDismissible: barrierDismissible, - builder: (context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: ConfirmPopup( - title: title, - description: description, - onConfirm: () => onConfirm?.call(), - onCancel: onCancel, - confirmLabel: confirmLabel, - confirmButtonColor: Theme.of(context).colorScheme.primary, - style: style, - closeOnAction: closeOnConfirm, - showCloseButton: showCloseButton, - enableKeyboardListener: enableKeyboardListener, - child: builder(context), - ), - ), - ); - }, - ); -} - -Future showCancelAndDeleteDialog({ - required BuildContext context, - required String title, - required String description, - Widget Function(BuildContext)? builder, - VoidCallback? onDelete, - String? confirmLabel, - bool closeOnAction = false, -}) { - return showDialog( - context: context, - builder: (_) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: SizedBox( - width: 440, - child: ConfirmPopup( - title: title, - description: description, - onConfirm: () => onDelete?.call(), - closeOnAction: closeOnAction, - confirmLabel: confirmLabel, - confirmButtonColor: Theme.of(context).colorScheme.error, - child: builder?.call(context), - ), - ), - ); - }, - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart index 5b3962cd63..7d245d0320 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/draggable_item/draggable_item.dart @@ -1,10 +1,5 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:universal_platform/universal_platform.dart'; - -/// This value is used to disable the auto scroll when dragging. -/// -/// It is used to prevent the auto scroll when dragging a view item to a document. -bool disableAutoScrollWhenDragging = false; class DraggableItem extends StatefulWidget { const DraggableItem({ @@ -72,7 +67,7 @@ class _DraggableItemState extends State> { childWhenDragging: widget.childWhenDragging ?? widget.child, child: widget.child, onDragUpdate: (details) { - if (widget.enableAutoScroll && !disableAutoScrollWhenDragging) { + if (widget.enableAutoScroll) { dragTarget = details.globalPosition & widget.hitTestSize; autoScroller?.startAutoScrollIfNecessary(dragTarget!); } @@ -93,7 +88,7 @@ class _DraggableItemState extends State> { } void initAutoScrollerIfNeeded(BuildContext context) { - if (!widget.enableAutoScroll || disableAutoScrollWhenDragging) { + if (!widget.enableAutoScroll) { return; } @@ -109,7 +104,7 @@ class _DraggableItemState extends State> { autoScroller = EdgeDraggingAutoScroller( scrollable!, onScrollViewScrolled: () { - if (dragTarget != null && !disableAutoScrollWhenDragging) { + if (dragTarget != null) { autoScroller!.startAutoScrollIfNecessary(dragTarget!); } }, @@ -151,7 +146,7 @@ class _Draggable extends StatelessWidget { @override Widget build(BuildContext context) { - return UniversalPlatform.isMobile + return PlatformExtension.isMobile ? LongPressDraggable( data: data, feedback: feedback, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index e3117c7f86..fde71f9150 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -4,15 +4,17 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; -import 'package:appflowy/workspace/presentation/widgets/float_bubble/social_media_section.dart'; -import 'package:appflowy/workspace/presentation/widgets/float_bubble/version_section.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:styled_widget/styled_widget.dart'; class QuestionBubble extends StatelessWidget { const QuestionBubble({super.key}); @@ -20,7 +22,7 @@ class QuestionBubble extends StatelessWidget { @override Widget build(BuildContext context) { return const SizedBox.square( - dimension: 32.0, + dimension: 36.0, child: BubbleActionList(), ); } @@ -56,9 +58,7 @@ class _BubbleActionListState extends State { actions.addAll( BubbleAction.values.map((action) => BubbleActionWrapper(action)), ); - - actions.add(SocialMediaSection()); - actions.add(FlowyVersionSection()); + actions.add(FlowyVersionDescription()); final (color, borderColor, shadowColor, iconColor) = Theme.of(context).isLightMode @@ -79,19 +79,14 @@ class _BubbleActionListState extends State { direction: PopoverDirection.topWithRightAligned, actions: actions, offset: const Offset(0, -8), - constraints: const BoxConstraints( - minWidth: 200, - maxWidth: 460, - maxHeight: 400, - ), buildChild: (controller) { return FlowyTooltip( - message: LocaleKeys.questionBubble_getSupport.tr(), + message: LocaleKeys.questionBubble_help.tr(), child: MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( child: Container( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(10.0), decoration: ShapeDecoration( color: color, shape: RoundedRectangleBorder( @@ -121,22 +116,22 @@ class _BubbleActionListState extends State { if (action is BubbleActionWrapper) { switch (action.inner) { case BubbleAction.whatsNews: - afLaunchUrlString('https://www.appflowy.io/what-is-new'); + afLaunchUrlString("https://www.appflowy.io/what-is-new"); break; - case BubbleAction.getSupport: - afLaunchUrlString('https://discord.gg/9Q2xaN37tV'); + case BubbleAction.help: + afLaunchUrlString("https://discord.gg/9Q2xaN37tV"); break; case BubbleAction.debug: _DebugToast().show(); break; case BubbleAction.shortcuts: afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/shortcuts', + "https://docs.appflowy.io/docs/appflowy/product/shortcuts", ); break; case BubbleAction.markdown: afLaunchUrlString( - 'https://docs.appflowy.io/docs/appflowy/product/markdown', + "https://docs.appflowy.io/docs/appflowy/product/markdown", ); break; case BubbleAction.github: @@ -144,11 +139,6 @@ class _BubbleActionListState extends State { 'https://github.com/AppFlowy-IO/AppFlowy/issues/new/choose', ); break; - case BubbleAction.helpAndDocumentation: - afLaunchUrlString( - 'https://appflowy.com/guide', - ); - break; } } @@ -160,7 +150,7 @@ class _BubbleActionListState extends State { class _DebugToast { void show() async { - String debugInfo = ''; + String debugInfo = ""; debugInfo += await _getDeviceInfo(); debugInfo += await _getDocumentPath(); await Clipboard.setData(ClipboardData(text: debugInfo)); @@ -173,33 +163,71 @@ class _DebugToast { final deviceInfo = await deviceInfoPlugin.deviceInfo; return deviceInfo.data.entries - .fold('', (prev, el) => '$prev${el.key}: ${el.value}\n'); + .fold('', (prev, el) => "$prev${el.key}: ${el.value}\n"); } Future _getDocumentPath() async { return appFlowyApplicationDataDirectory().then((directory) { final path = directory.path.toString(); - return 'Document: $path\n'; + return "Document: $path\n"; }); } } -enum BubbleAction { - whatsNews, - helpAndDocumentation, - getSupport, - debug, - shortcuts, - markdown, - github, +class FlowyVersionDescription extends CustomActionCell { + @override + Widget buildWithContext(BuildContext context, PopoverController controller) { + return FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + if (snapshot.hasError) { + return FlowyText( + "Error: ${snapshot.error}", + color: Theme.of(context).disabledColor, + ); + } + + final PackageInfo packageInfo = snapshot.data; + final String appName = packageInfo.appName; + final String version = packageInfo.version; + + return SizedBox( + height: 30, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider( + height: 1, + color: Theme.of(context).dividerColor, + thickness: 1.0, + ), + const VSpace(6), + FlowyText( + "$appName $version", + color: Theme.of(context).hintColor, + ), + ], + ).padding( + horizontal: ActionListSizes.itemHPadding, + ), + ); + } else { + return const SizedBox(height: 30); + } + }, + ); + } } +enum BubbleAction { whatsNews, help, debug, shortcuts, markdown, github } + class BubbleActionWrapper extends ActionCell { BubbleActionWrapper(this.inner); final BubbleAction inner; @override - Widget? leftIcon(Color iconColor) => inner.icons; + Widget? leftIcon(Color iconColor) => inner.emoji; @override String get name => inner.name; @@ -210,10 +238,8 @@ extension QuestionBubbleExtension on BubbleAction { switch (this) { case BubbleAction.whatsNews: return LocaleKeys.questionBubble_whatsNew.tr(); - case BubbleAction.helpAndDocumentation: - return LocaleKeys.questionBubble_helpAndDocumentation.tr(); - case BubbleAction.getSupport: - return LocaleKeys.questionBubble_getSupport.tr(); + case BubbleAction.help: + return LocaleKeys.questionBubble_help.tr(); case BubbleAction.debug: return LocaleKeys.questionBubble_debug_name.tr(); case BubbleAction.shortcuts: @@ -225,25 +251,26 @@ extension QuestionBubbleExtension on BubbleAction { } } - Widget? get icons { + Widget get emoji { switch (this) { case BubbleAction.whatsNews: - return const FlowySvg(FlowySvgs.star_s); - case BubbleAction.helpAndDocumentation: - return const FlowySvg( - FlowySvgs.help_and_documentation_s, - size: Size.square(16.0), - ); - case BubbleAction.getSupport: - return const FlowySvg(FlowySvgs.message_support_s); + return const FlowyText.regular('🆕'); + case BubbleAction.help: + return const FlowyText.regular('👥'); case BubbleAction.debug: - return const FlowySvg(FlowySvgs.debug_s); + return const FlowyText.regular('🐛'); case BubbleAction.shortcuts: - return const FlowySvg(FlowySvgs.keyboard_s); + return const FlowyText.regular('📋'); case BubbleAction.markdown: - return const FlowySvg(FlowySvgs.number_s); + return const FlowyText.regular('✨'); case BubbleAction.github: - return const FlowySvg(FlowySvgs.share_feedback_s); + return const Padding( + padding: EdgeInsets.all(3.0), + child: FlowySvg( + FlowySvgs.archive_m, + size: Size.square(12), + ), + ); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart deleted file mode 100644 index 8b58557455..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/social_media_section.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; - -class SocialMediaSection extends CustomActionCell { - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - final List children = [ - Divider( - height: 1, - color: Theme.of(context).dividerColor, - thickness: 1.0, - ), - ]; - - children.addAll( - SocialMedia.values.map( - (social) { - return ActionCellWidget( - action: SocialMediaWrapper(social), - itemHeight: ActionListSizes.itemHeight, - onSelected: (action) { - final url = switch (action.inner) { - SocialMedia.reddit => 'https://www.reddit.com/r/AppFlowy/', - SocialMedia.twitter => 'https://x.com/appflowy', - SocialMedia.forum => 'https://forum.appflowy.com/', - }; - - afLaunchUrlString(url); - }, - ); - }, - ), - ); - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Column( - children: children, - ), - ); - } -} - -enum SocialMedia { forum, twitter, reddit } - -class SocialMediaWrapper extends ActionCell { - SocialMediaWrapper(this.inner); - - final SocialMedia inner; - @override - Widget? leftIcon(Color iconColor) => inner.icons; - - @override - String get name => inner.name; - - @override - Color? textColor(BuildContext context) => inner.textColor(context); -} - -extension QuestionBubbleExtension on SocialMedia { - Color? textColor(BuildContext context) { - switch (this) { - case SocialMedia.reddit: - return Theme.of(context).hintColor; - - case SocialMedia.twitter: - return Theme.of(context).hintColor; - - case SocialMedia.forum: - return Theme.of(context).hintColor; - } - } - - String get name { - switch (this) { - case SocialMedia.forum: - return 'Community Forum'; - case SocialMedia.twitter: - return 'Twitter – @appflowy'; - case SocialMedia.reddit: - return 'Reddit – r/appflowy'; - } - } - - Widget? get icons { - switch (this) { - case SocialMedia.reddit: - return null; - case SocialMedia.twitter: - return null; - case SocialMedia.forum: - return null; - } - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart deleted file mode 100644 index f6a2caa5a2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/version_section.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:appflowy/env/env.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:package_info_plus/package_info_plus.dart'; -import 'package:styled_widget/styled_widget.dart'; - -class FlowyVersionSection extends CustomActionCell { - @override - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ) { - return FutureBuilder( - future: PackageInfo.fromPlatform(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - return FlowyText( - "Error: ${snapshot.error}", - color: Theme.of(context).disabledColor, - ); - } - - final PackageInfo packageInfo = snapshot.data; - final String appName = packageInfo.appName; - final String version = packageInfo.version; - - return SizedBox( - height: 30, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Divider( - height: 1, - color: Theme.of(context).dividerColor, - thickness: 1.0, - ), - const VSpace(6), - GestureDetector( - behavior: HitTestBehavior.opaque, - onDoubleTap: () { - if (Env.internalBuild != '1' && !kDebugMode) { - return; - } - enableDocumentInternalLog = !enableDocumentInternalLog; - showToastNotification( - message: enableDocumentInternalLog - ? 'Enabled Internal Log' - : 'Disabled Internal Log', - ); - }, - child: FlowyText( - '$appName $version', - color: Theme.of(context).hintColor, - fontSize: 12, - ).padding( - horizontal: ActionListSizes.itemHPadding, - ), - ), - ], - ), - ); - } else { - return const SizedBox(height: 30); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart deleted file mode 100644 index b69c56abf2..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/image_provider.dart +++ /dev/null @@ -1,68 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:flutter/widgets.dart'; - -/// Abstract class for providing images to the [InteractiveImageViewer]. -/// -abstract class AFImageProvider { - const AFImageProvider({this.onDeleteImage}); - - /// Provide this callback if you want it to be possible to - /// delete the Image through the [InteractiveImageViewer]. - /// - final Function(int index)? onDeleteImage; - - int get imageCount; - int get initialIndex; - - ImageBlockData getImage(int index); - Widget renderImage( - BuildContext context, - int index, [ - UserProfilePB? userProfile, - ]); -} - -class AFBlockImageProvider implements AFImageProvider { - const AFBlockImageProvider({ - required this.images, - this.initialIndex = 0, - this.onDeleteImage, - }); - - final List images; - - @override - final Function(int)? onDeleteImage; - - @override - final int initialIndex; - - @override - int get imageCount => images.length; - - @override - ImageBlockData getImage(int index) => images[index]; - - @override - Widget renderImage( - BuildContext context, - int index, [ - UserProfilePB? userProfile, - ]) { - final image = getImage(index); - - if (image.type == CustomImageType.local && - localPathRegex.hasMatch(image.url)) { - return Image(image: image.toImageProvider()); - } - - return FlowyNetworkImage( - url: image.url, - userProfilePB: userProfile, - fit: BoxFit.contain, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart deleted file mode 100644 index 765a385b0b..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart +++ /dev/null @@ -1,337 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/file_picker/file_picker_impl.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:path/path.dart'; -import 'package:universal_platform/universal_platform.dart'; - -class InteractiveImageToolbar extends StatelessWidget { - const InteractiveImageToolbar({ - super.key, - required this.currentImage, - required this.imageCount, - required this.isFirstIndex, - required this.isLastIndex, - required this.currentScale, - required this.onPrevious, - required this.onNext, - required this.onZoomIn, - required this.onZoomOut, - required this.onScaleChanged, - this.onDelete, - this.userProfile, - }); - - final ImageBlockData currentImage; - final int imageCount; - final bool isFirstIndex; - final bool isLastIndex; - final int currentScale; - - final VoidCallback onPrevious; - final VoidCallback onNext; - final VoidCallback onZoomIn; - final VoidCallback onZoomOut; - final Function(double scale) onScaleChanged; - final UserProfilePB? userProfile; - final VoidCallback? onDelete; - - @override - Widget build(BuildContext context) { - return Positioned( - bottom: 16, - left: 0, - right: 0, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: SizedBox( - width: 200, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (imageCount > 1) - _renderToolbarItems( - children: [ - _ToolbarItem( - isDisabled: isFirstIndex, - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_previousImageTooltip - .tr(), - icon: FlowySvgs.arrow_left_s, - onTap: () { - if (!isFirstIndex) { - onPrevious(); - } - }, - ), - _ToolbarItem( - isDisabled: isLastIndex, - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_nextImageTooltip - .tr(), - icon: FlowySvgs.arrow_right_s, - onTap: () { - if (!isLastIndex) { - onNext(); - } - }, - ), - ], - ), - const HSpace(10), - _renderToolbarItems( - children: [ - _ToolbarItem( - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_zoomOutTooltip - .tr(), - icon: FlowySvgs.minus_s, - onTap: onZoomOut, - ), - AppFlowyPopover( - offset: const Offset(0, -8), - decorationColor: Colors.transparent, - direction: PopoverDirection.topWithCenterAligned, - constraints: const BoxConstraints(maxHeight: 50), - popupBuilder: (context) => _renderToolbarItems( - children: [ - _ScaleSlider( - currentScale: currentScale, - onScaleChanged: onScaleChanged, - ), - ], - ), - child: FlowyTooltip( - message: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_changeZoomLevelTooltip - .tr(), - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Padding( - padding: const EdgeInsets.all(6), - child: SizedBox( - width: 40, - child: Center( - child: FlowyText( - LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_scalePercentage - .tr(args: [currentScale.toString()]), - color: Colors.white, - ), - ), - ), - ), - ), - ), - ), - _ToolbarItem( - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_zoomInTooltip - .tr(), - icon: FlowySvgs.add_s, - onTap: onZoomIn, - ), - ], - ), - const HSpace(10), - _renderToolbarItems( - children: [ - if (onDelete != null) - _ToolbarItem( - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_deleteImageTooltip - .tr(), - icon: FlowySvgs.delete_s, - onTap: () { - onDelete!(); - Navigator.of(context).pop(); - }, - ), - if (!UniversalPlatform.isMobile) ...[ - _ToolbarItem( - tooltip: currentImage.isNotInternal - ? LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_openLocalImage - .tr() - : LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_downloadImage - .tr(), - icon: currentImage.isNotInternal - ? currentImage.isLocal - ? FlowySvgs.folder_m - : FlowySvgs.m_aa_link_s - : FlowySvgs.download_s, - onTap: () => _locateOrDownloadImage(context), - ), - ], - ], - ), - const HSpace(10), - _renderToolbarItems( - children: [ - _ToolbarItem( - tooltip: LocaleKeys - .document_imageBlock_interactiveViewer_toolbar_closeViewer - .tr(), - icon: FlowySvgs.close_viewer_s, - onTap: () => Navigator.of(context).pop(), - ), - ], - ), - ], - ), - ), - ), - ); - } - - Widget _renderToolbarItems({required List children}) { - return DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(6), - color: Colors.black.withValues(alpha: 0.6), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: SeparatedRow( - mainAxisSize: MainAxisSize.min, - separatorBuilder: () => const HSpace(4), - children: children, - ), - ), - ); - } - - Future _locateOrDownloadImage(BuildContext context) async { - if (currentImage.isLocal || currentImage.isNotInternal) { - /// If the image type is local, we simply open the image - /// - /// // In case of eg. Unsplash images (images without extension type in URL), - // we don't know their mimetype. In the future we can write a parser - // using the Mime package and read the image to get the proper extension. - await afLaunchUrlString(currentImage.url); - } else { - if (userProfile == null) { - return showSnapBar( - context, - LocaleKeys.document_plugins_image_imageDownloadFailedToken.tr(), - ); - } - - final uri = Uri.parse(currentImage.url); - final imgFile = File(uri.pathSegments.last); - final savePath = await FilePicker().saveFile( - fileName: basename(imgFile.path), - ); - - if (savePath != null) { - final uri = Uri.parse(currentImage.url); - - final token = jsonDecode(userProfile!.token)['access_token']; - final response = await http.get( - uri, - headers: {'Authorization': 'Bearer $token'}, - ); - if (response.statusCode == 200) { - final imgFile = File(savePath); - await imgFile.writeAsBytes(response.bodyBytes); - } else if (context.mounted) { - showSnapBar( - context, - LocaleKeys.document_plugins_image_imageDownloadFailed.tr(), - ); - } - } - } - } -} - -class _ToolbarItem extends StatelessWidget { - const _ToolbarItem({ - required this.tooltip, - required this.icon, - required this.onTap, - this.isDisabled = false, - }); - - final String tooltip; - final FlowySvgData icon; - final VoidCallback onTap; - final bool isDisabled; - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: FlowyTooltip( - message: tooltip, - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: isDisabled - ? Colors.transparent - : Colors.white.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - ), - child: Container( - width: 32, - height: 32, - padding: const EdgeInsets.all(8), - child: FlowySvg( - icon, - color: isDisabled ? Colors.grey : Colors.white, - ), - ), - ), - ), - ); - } -} - -class _ScaleSlider extends StatefulWidget { - const _ScaleSlider({ - required this.currentScale, - required this.onScaleChanged, - }); - - final int currentScale; - final Function(double scale) onScaleChanged; - - @override - State<_ScaleSlider> createState() => __ScaleSliderState(); -} - -class __ScaleSliderState extends State<_ScaleSlider> { - late int _currentScale = widget.currentScale; - - @override - Widget build(BuildContext context) { - return Slider( - max: 5.0, - min: 0.5, - value: _currentScale / 100, - onChanged: (scale) { - widget.onScaleChanged(scale); - setState( - () => _currentScale = (scale * 100).toInt(), - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart deleted file mode 100644 index 143c6b1ad3..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart +++ /dev/null @@ -1,240 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart'; -import 'package:appflowy/plugins/document/application/document_bloc.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/image_provider.dart'; -import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/media_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:provider/provider.dart'; - -const double _minScaleFactor = .5; -const double _maxScaleFactor = 5; - -class InteractiveImageViewer extends StatefulWidget { - const InteractiveImageViewer({ - super.key, - this.userProfile, - required this.imageProvider, - }); - - final UserProfilePB? userProfile; - final AFImageProvider imageProvider; - - @override - State createState() => _InteractiveImageViewerState(); -} - -class _InteractiveImageViewerState extends State { - final TransformationController controller = TransformationController(); - final focusNode = FocusNode(); - - int currentScale = 100; - late int currentIndex = widget.imageProvider.initialIndex; - - bool get isLastIndex => currentIndex == widget.imageProvider.imageCount - 1; - bool get isFirstIndex => currentIndex == 0; - - late ImageBlockData currentImage; - - UserProfilePB? userProfile; - - @override - void initState() { - super.initState(); - controller.addListener(_onControllerChanged); - currentImage = widget.imageProvider.getImage(currentIndex); - userProfile = - widget.userProfile ?? context.read().state.userProfilePB; - focusNode.requestFocus(); - } - - void _onControllerChanged() { - final scale = controller.value.getMaxScaleOnAxis(); - final percentage = (scale * 100).toInt(); - setState(() => currentScale = percentage); - } - - @override - void dispose() { - controller.removeListener(_onControllerChanged); - controller.dispose(); - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - - return KeyboardListener( - focusNode: focusNode, - onKeyEvent: (event) { - if (event is! KeyDownEvent) { - return; - } - - if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - _move(-1); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - _move(1); - } else if ([ - LogicalKeyboardKey.add, - LogicalKeyboardKey.numpadAdd, - ].contains(event.logicalKey)) { - _zoom(1.1, size); - } else if ([ - LogicalKeyboardKey.minus, - LogicalKeyboardKey.numpadSubtract, - ].contains(event.logicalKey)) { - _zoom(.9, size); - } else if ([ - LogicalKeyboardKey.numpad0, - LogicalKeyboardKey.digit0, - ].contains(event.logicalKey)) { - controller.value = Matrix4.identity(); - _onControllerChanged(); - } - }, - child: Stack( - fit: StackFit.expand, - children: [ - SizedBox.expand( - child: InteractiveViewer( - boundaryMargin: const EdgeInsets.all(double.infinity), - transformationController: controller, - constrained: false, - minScale: _minScaleFactor, - maxScale: _maxScaleFactor, - scaleFactor: 500, - child: SizedBox( - height: size.height, - width: size.width, - child: GestureDetector( - // We can consider adding zoom behavior instead in a later iteration - onDoubleTap: () => Navigator.of(context).pop(), - child: widget.imageProvider.renderImage( - context, - currentIndex, - userProfile, - ), - ), - ), - ), - ), - InteractiveImageToolbar( - currentImage: currentImage, - imageCount: widget.imageProvider.imageCount, - isFirstIndex: isFirstIndex, - isLastIndex: isLastIndex, - currentScale: currentScale, - userProfile: userProfile, - onPrevious: () => _move(-1), - onNext: () => _move(1), - onZoomIn: () => _zoom(1.1, size), - onZoomOut: () => _zoom(.9, size), - onScaleChanged: (scale) { - final currentScale = controller.value.getMaxScaleOnAxis(); - final scaleStep = scale / currentScale; - _zoom(scaleStep, size); - }, - onDelete: widget.imageProvider.onDeleteImage == null - ? null - : () => widget.imageProvider.onDeleteImage?.call(currentIndex), - ), - ], - ), - ); - } - - void _move(int steps) { - setState(() { - final index = currentIndex + steps; - currentIndex = index.clamp(0, widget.imageProvider.imageCount - 1); - currentImage = widget.imageProvider.getImage(currentIndex); - }); - } - - void _zoom(double scaleStep, Size size) { - final center = Offset(size.width / 2, size.height / 2); - final scenePointBefore = controller.toScene(center); - final currentScale = controller.value.getMaxScaleOnAxis(); - final newScale = (currentScale * scaleStep).clamp( - _minScaleFactor, - _maxScaleFactor, - ); - - // Create a new transformation - final newMatrix = Matrix4.identity() - ..translate(scenePointBefore.dx, scenePointBefore.dy) - ..scale(newScale / currentScale) - ..translate(-scenePointBefore.dx, -scenePointBefore.dy); - - // Apply the new transformation - controller.value = newMatrix * controller.value; - - // Convert the center point to scene coordinates after scaling - final scenePointAfter = controller.toScene(center); - - // Compute difference to keep the same center point - final dx = scenePointAfter.dx - scenePointBefore.dx; - final dy = scenePointAfter.dy - scenePointBefore.dy; - - // Apply the translation - controller.value = Matrix4.identity() - ..translate(-dx, -dy) - ..multiply(controller.value); - - _onControllerChanged(); - } -} - -void openInteractiveViewerFromFile( - BuildContext context, - MediaFilePB file, { - required void Function(int) onDeleteImage, - UserProfilePB? userProfile, -}) => - showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: userProfile, - imageProvider: AFBlockImageProvider( - images: [ - ImageBlockData( - url: file.url, - type: file.uploadType.toCustomImageType(), - ), - ], - onDeleteImage: onDeleteImage, - ), - ), - ); - -void openInteractiveViewerFromFiles( - BuildContext context, - List files, { - required void Function(int) onDeleteImage, - int initialIndex = 0, - UserProfilePB? userProfile, -}) => - showDialog( - context: context, - builder: (_) => InteractiveImageViewer( - userProfile: userProfile, - imageProvider: AFBlockImageProvider( - initialIndex: initialIndex, - images: files - .map( - (f) => ImageBlockData( - url: f.url, - type: f.uploadType.toCustomImageType(), - ), - ) - .toList(), - onDeleteImage: onDeleteImage, - ), - ), - ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 62b3ccc8f3..f9f46cfba9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,22 +1,17 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.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/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart'; -import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -24,16 +19,14 @@ class MoreViewActions extends StatefulWidget { const MoreViewActions({ super.key, required this.view, - this.customActions = const [], + this.isDocument = true, }); /// The view to show the actions for. - /// final ViewPB view; - /// Custom actions to show in the popover, will be laid out at the top. - /// - final List customActions; + /// If false the view is a Database, otherwise it is a Document. + final bool isDocument; @override State createState() => _MoreViewActionsState(); @@ -50,152 +43,74 @@ class _MoreViewActionsState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return AppFlowyPopover( - mutex: popoverMutex, - constraints: const BoxConstraints(maxWidth: 245), - direction: PopoverDirection.bottomWithRightAligned, - offset: const Offset(0, 12), - popupBuilder: (_) => _buildPopup(state), - child: const _ThreeDots(), - ); - }, - ); - } - - Widget _buildPopup(ViewInfoState viewInfoState) { - final userWorkspaceBloc = context.read(); - final userProfile = userWorkspaceBloc.userProfile; - final workspaceId = - userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? ''; - - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => ViewBloc(view: widget.view) - ..add( - const ViewEvent.initial(), - ), - ), - BlocProvider( - create: (_) => ViewLockStatusBloc(view: widget.view) - ..add(ViewLockStatusEvent.initial()), - ), - BlocProvider( - create: (context) => SpaceBloc( - userProfile: userProfile, - workspaceId: workspaceId, - )..add( - const SpaceEvent.initial(openFirstPage: false), - ), - ), - ], - child: BlocBuilder( - builder: (context, viewState) { - return BlocBuilder( - builder: (context, state) { - if (state.spaces.isEmpty && - userProfile.workspaceAuthType == AuthTypePB.Server) { - return const SizedBox.shrink(); - } - - final actions = _buildActions( - context, - viewInfoState, - ); - return ListView.builder( - key: ValueKey(state.spaces.hashCode), - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: actions.length, - physics: StyledScrollPhysics(), - itemBuilder: (_, index) => actions[index], - ); - }, - ); - }, - ), - ); - } - - List _buildActions(BuildContext context, ViewInfoState state) { - final view = context.watch().state.view; final appearanceSettings = context.watch().state; final dateFormat = appearanceSettings.dateFormat; final timeFormat = appearanceSettings.timeFormat; - final viewMoreActionTypes = [ - if (widget.view.layout != ViewLayoutPB.Chat) ViewMoreActionType.duplicate, - ViewMoreActionType.moveTo, - ViewMoreActionType.delete, - ViewMoreActionType.divider, - ]; - - final actions = [ - ...widget.customActions, - if (widget.view.isDocument) ...[ - const FontSizeAction(), - ViewAction( - type: ViewMoreActionType.divider, - view: view, + return BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( mutex: popoverMutex, - ), - ], - if (widget.view.isDocument || widget.view.isDatabase) ...[ - LockPageAction( - view: view, - ), - ViewAction( - type: ViewMoreActionType.divider, - view: view, - mutex: popoverMutex, - ), - ], - ...viewMoreActionTypes.map( - (type) => ViewAction( - type: type, - view: view, - mutex: popoverMutex, - ), - ), - if (state.documentCounters != null || state.createdAt != null) ...[ - ViewMetaInfo( - dateFormat: dateFormat, - timeFormat: timeFormat, - documentCounters: state.documentCounters, - titleCounters: state.titleCounters, - createdAt: state.createdAt, - ), - const VSpace(4.0), - ], - ]; - return actions; - } -} + constraints: BoxConstraints.loose(const Size(215, 400)), + offset: const Offset(0, 30), + popupBuilder: (_) { + final actions = [ + if (widget.isDocument) ...[ + const FontSizeAction(), + const Divider(height: 4), + ], + ...ViewActionType.values.map( + (type) => ViewAction( + type: type, + view: widget.view, + mutex: popoverMutex, + ), + ), + if (state.documentCounters != null || + state.createdAt != null) ...[ + const Divider(height: 4), + ViewMetaInfo( + dateFormat: dateFormat, + timeFormat: timeFormat, + documentCounters: state.documentCounters, + createdAt: state.createdAt, + ), + ], + ]; -class _ThreeDots extends StatelessWidget { - const _ThreeDots(); - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.moreAction_moreOptions.tr(), - child: FlowyHover( - style: HoverStyle( - foregroundColorOnHover: Theme.of(context).colorScheme.onPrimary, - ), - builder: (context, isHovering) => Padding( - padding: const EdgeInsets.all(6), - child: FlowySvg( - FlowySvgs.three_dots_s, - size: const Size.square(18), - color: isHovering - ? Theme.of(context).colorScheme.onSurface - : Theme.of(context).iconTheme.color, + return BlocProvider( + create: (_) => + ViewBloc(view: widget.view)..add(const ViewEvent.initial()), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: actions.length, + separatorBuilder: (_, __) => const VSpace(4), + physics: StyledScrollPhysics(), + itemBuilder: (_, index) => actions[index], + ), + ); + }, + child: FlowyTooltip( + message: LocaleKeys.moreAction_moreOptions.tr(), + child: FlowyHover( + style: HoverStyle( + foregroundColorOnHover: Theme.of(context).colorScheme.onPrimary, + ), + builder: (context, isHovering) => Padding( + padding: const EdgeInsets.all(6), + child: FlowySvg( + FlowySvgs.three_dots_s, + size: const Size.square(18), + color: isHovering + ? Theme.of(context).colorScheme.onSurface + : Theme.of(context).iconTheme.color, + ), + ), + ), ), - ), - ), + ); + }, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart index 2ecec3244c..c54f6169a2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart @@ -1,20 +1,36 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +enum ViewActionType { + delete, + duplicate; + + String get label => switch (this) { + ViewActionType.delete => LocaleKeys.moreAction_deleteView.tr(), + ViewActionType.duplicate => LocaleKeys.moreAction_duplicateView.tr(), + }; + + FlowySvgData get icon => switch (this) { + ViewActionType.delete => FlowySvgs.delete_s, + ViewActionType.duplicate => FlowySvgs.m_duplicate_s, + }; + + ViewEvent get actionEvent => switch (this) { + ViewActionType.delete => const ViewEvent.delete(), + ViewActionType.duplicate => const ViewEvent.duplicate(), + }; +} + class ViewAction extends StatelessWidget { const ViewAction({ super.key, @@ -23,126 +39,28 @@ class ViewAction extends StatelessWidget { this.mutex, }); - final ViewMoreActionType type; + final ViewActionType type; final ViewPB view; final PopoverMutex? mutex; @override Widget build(BuildContext context) { - final wrapper = ViewMoreActionTypeWrapper( - type, - view, - (controller, data) async { - await _onAction(context, data); + return FlowyButton( + onTap: () { + context.read().add(type.actionEvent); mutex?.close(); }, - moveActionDirection: PopoverDirection.leftWithTopAligned, - moveActionOffset: const Offset(-10, 0), - ); - return wrapper.buildWithContext( - context, - // this is a dummy controller, we don't need to control the popover here. - PopoverController(), - null, - ); - } - - Future _onAction( - BuildContext context, - dynamic data, - ) async { - switch (type) { - case ViewMoreActionType.delete: - final (containPublishedPage, _) = - await ViewBackendService.containPublishedPage(view); - - if (containPublishedPage && context.mounted) { - await showConfirmDeletionDialog( - context: context, - name: view.nameOrDefault, - description: LocaleKeys.publish_containsPublishedPage.tr(), - onConfirm: () { - context.read().add(const ViewEvent.delete()); - }, - ); - } else if (context.mounted) { - context.read().add(const ViewEvent.delete()); - } - case ViewMoreActionType.duplicate: - context.read().add(const ViewEvent.duplicate()); - case ViewMoreActionType.moveTo: - final value = data; - if (value is! (ViewPB, ViewPB)) { - return; - } - final space = value.$1; - final target = value.$2; - final result = await ViewBackendService.getView(view.parentViewId); - result.fold( - (parentView) => moveViewCrossSpace( - context, - space, - view, - parentView, - FolderSpaceType.public, - view, - target.id, - ), - (f) => Log.error(f), - ); - - // the move action is handled in the button itself - break; - default: - throw UnimplementedError(); - } - } -} - -class CustomViewAction extends StatelessWidget { - const CustomViewAction({ - super.key, - required this.view, - required this.leftIcon, - required this.label, - this.tooltipMessage, - this.disabled = false, - this.onTap, - this.mutex, - }); - - final ViewPB view; - final FlowySvgData leftIcon; - final String label; - final bool disabled; - final String? tooltipMessage; - final VoidCallback? onTap; - final PopoverMutex? mutex; - - @override - Widget build(BuildContext context) { - return Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyTooltip( - message: tooltipMessage, - child: FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - disable: disabled, - onTap: onTap, - leftIcon: FlowySvg( - leftIcon, - size: const Size.square(16.0), - color: disabled ? Theme.of(context).disabledColor : null, - ), - iconPadding: 10.0, - text: FlowyText( - label, - figmaLineHeight: 18.0, - color: disabled ? Theme.of(context).disabledColor : null, - ), - ), + text: FlowyText.regular( + type.label, + color: AFThemeExtension.of(context).textColor, ), + leftIcon: FlowySvg( + type.icon, + color: Theme.of(context).iconTheme.color, + size: const Size.square(18), + ), + leftIconSize: const Size(18, 18), + hoverColor: AFThemeExtension.of(context).lightGreyHover, ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart index 8e0fa8c43c..c56f93ee90 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/font_size_action.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class FontSizeAction extends StatelessWidget { @@ -29,25 +31,18 @@ class FontSizeAction extends StatelessWidget { ), ); }, - child: Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyButton( - text: FlowyText.regular( - LocaleKeys.moreAction_fontSize.tr(), - fontSize: 14.0, - lineHeight: 1.0, - figmaLineHeight: 18.0, - color: AFThemeExtension.of(context).textColor, - ), - leftIcon: Icon( - Icons.format_size_sharp, - color: Theme.of(context).iconTheme.color, - size: 18, - ), - leftIconSize: const Size(18, 18), - hoverColor: AFThemeExtension.of(context).lightGreyHover, + child: FlowyButton( + text: FlowyText.regular( + LocaleKeys.moreAction_fontSize.tr(), + color: AFThemeExtension.of(context).textColor, ), + leftIcon: Icon( + Icons.format_size_sharp, + color: Theme.of(context).iconTheme.color, + size: 18, + ), + leftIconSize: const Size(18, 18), + hoverColor: AFThemeExtension.of(context).lightGreyHover, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart deleted file mode 100644 index 202919b639..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/lock_page_action.dart +++ /dev/null @@ -1,119 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.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/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class LockPageAction extends StatefulWidget { - const LockPageAction({ - super.key, - required this.view, - }); - - final ViewPB view; - - @override - State createState() => _LockPageActionState(); -} - -class _LockPageActionState extends State { - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => ViewLockStatusBloc(view: widget.view) - ..add( - ViewLockStatusEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return _buildTextButton(context); - }, - ), - ); - } - - Widget _buildTextButton( - BuildContext context, - ) { - return Container( - height: 34, - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: FlowyIconTextButton( - margin: const EdgeInsets.symmetric(horizontal: 6), - onTap: () => _toggle(context), - leftIconBuilder: (onHover) => FlowySvg( - FlowySvgs.lock_page_s, - size: const Size.square(16.0), - ), - iconPadding: 10.0, - textBuilder: (onHover) => FlowyText( - LocaleKeys.disclosureAction_lockPage.tr(), - figmaLineHeight: 18.0, - ), - rightIconBuilder: (_) => _buildSwitch( - context, - ), - ), - ); - } - - Widget _buildSwitch(BuildContext context) { - final lockState = context.read().state; - if (lockState.isLoadingLockStatus) { - return SizedBox.shrink(); - } - - return Container( - width: 30, - height: 20, - margin: const EdgeInsets.only(right: 6), - child: FittedBox( - fit: BoxFit.fill, - child: CupertinoSwitch( - value: lockState.isLocked, - activeTrackColor: Theme.of(context).colorScheme.primary, - onChanged: (_) => _toggle(context), - ), - ), - ); - } - - Future _toggle(BuildContext context) async { - final isLocked = context.read().state.isLocked; - - context.read().add( - isLocked ? ViewLockStatusEvent.unlock() : ViewLockStatusEvent.lock(), - ); - - Log.info('update page(${widget.view.id}) lock status: $isLocked'); - } -} - -class LockPageButtonWrapper extends StatelessWidget { - const LockPageButtonWrapper({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.lockPage_lockedOperationTooltip.tr(), - child: IgnorePointer( - child: Opacity( - opacity: 0.5, - child: child, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart index 27b96d39e9..a5a72964af 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; @@ -5,7 +7,6 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; class ViewMetaInfo extends StatelessWidget { const ViewMetaInfo({ @@ -13,14 +14,12 @@ class ViewMetaInfo extends StatelessWidget { required this.dateFormat, required this.timeFormat, this.documentCounters, - this.titleCounters, this.createdAt, }); final UserDateFormatPB dateFormat; final UserTimeFormatPB timeFormat; final Counters? documentCounters; - final Counters? titleCounters; final DateTime? createdAt; @override @@ -33,43 +32,34 @@ class ViewMetaInfo extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (documentCounters != null && titleCounters != null) ...[ + if (documentCounters != null) ...[ FlowyText.regular( LocaleKeys.moreAction_wordCount.tr( args: [ - numberFormat - .format( - documentCounters!.wordCount + titleCounters!.wordCount, - ) - .toString(), + numberFormat.format(documentCounters!.wordCount).toString(), ], ), - fontSize: 12, + fontSize: 11, color: Theme.of(context).hintColor, ), const VSpace(2), FlowyText.regular( LocaleKeys.moreAction_charCount.tr( args: [ - numberFormat - .format( - documentCounters!.charCount + titleCounters!.charCount, - ) - .toString(), + numberFormat.format(documentCounters!.charCount).toString(), ], ), - fontSize: 12, + fontSize: 11, color: Theme.of(context).hintColor, ), ], if (createdAt != null) ...[ - if (documentCounters != null && titleCounters != null) - const VSpace(2), + if (documentCounters != null) const VSpace(2), FlowyText.regular( LocaleKeys.moreAction_createdAt.tr( args: [dateFormat.formatDate(createdAt!, true, timeFormat)], ), - fontSize: 12, + fontSize: 11, maxLines: 2, color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index fb39d73965..6abc789223 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -1,3 +1,4 @@ +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,6 @@ import 'package:styled_widget/styled_widget.dart'; class PopoverActionList extends StatefulWidget { const PopoverActionList({ super.key, - this.controller, this.popoverMutex, required this.actions, required this.buildChild, @@ -17,21 +17,13 @@ class PopoverActionList extends StatefulWidget { this.direction = PopoverDirection.rightWithTopAligned, this.asBarrier = false, this.offset = Offset.zero, - this.animationDuration = const Duration(), - this.slideDistance = 20, - this.beginScaleFactor = 0.9, - this.endScaleFactor = 1.0, - this.beginOpacity = 0.0, - this.endOpacity = 1.0, this.constraints = const BoxConstraints( minWidth: 120, maxWidth: 460, maxHeight: 300, ), - this.showAtCursor = false, }); - final PopoverController? controller; final PopoverMutex? popoverMutex; final List actions; final Widget Function(PopoverController) buildChild; @@ -43,13 +35,6 @@ class PopoverActionList extends StatefulWidget { final bool asBarrier; final Offset offset; final BoxConstraints constraints; - final Duration animationDuration; - final double slideDistance; - final double beginScaleFactor; - final double endScaleFactor; - final double beginOpacity; - final double endOpacity; - final bool showAtCursor; @override State> createState() => _PopoverActionListState(); @@ -57,37 +42,20 @@ class PopoverActionList extends StatefulWidget { class _PopoverActionListState extends State> { - late PopoverController popoverController = - widget.controller ?? PopoverController(); + late PopoverController popoverController; @override - void dispose() { - if (widget.controller == null) { - popoverController.close(); - } - super.dispose(); - } - - @override - void didUpdateWidget(covariant PopoverActionList oldWidget) { - if (widget.controller != oldWidget.controller) { - popoverController.close(); - popoverController = widget.controller ?? PopoverController(); - } - super.didUpdateWidget(oldWidget); + void initState() { + popoverController = PopoverController(); + super.initState(); } @override Widget build(BuildContext context) { final child = widget.buildChild(popoverController); + return AppFlowyPopover( asBarrier: widget.asBarrier, - animationDuration: widget.animationDuration, - slideDistance: widget.slideDistance, - beginScaleFactor: widget.beginScaleFactor, - endScaleFactor: widget.endScaleFactor, - beginOpacity: widget.beginOpacity, - endOpacity: widget.endOpacity, controller: popoverController, constraints: widget.constraints, direction: widget.direction, @@ -95,8 +63,7 @@ class _PopoverActionListState offset: widget.offset, triggerActions: PopoverTriggerFlags.none, onClose: widget.onClosed, - showAtCursor: widget.showAtCursor, - popupBuilder: (_) { + popupBuilder: (BuildContext popoverContext) { widget.onPopupBuilder?.call(); final List children = widget.actions.map((action) { if (action is ActionCell) { @@ -116,17 +83,15 @@ class _PopoverActionListState ); } else { final custom = action as CustomActionCell; - return custom.buildWithContext( - context, - popoverController, - widget.popoverMutex, - ); + return custom.buildWithContext(context, popoverController); } }).toList(); return IntrinsicHeight( child: IntrinsicWidth( - child: Column(children: children), + child: Column( + children: children, + ), ), ); }, @@ -139,9 +104,6 @@ abstract class ActionCell extends PopoverAction { Widget? leftIcon(Color iconColor) => null; Widget? rightIcon(Color iconColor) => null; String get name; - Color? textColor(BuildContext context) { - return null; - } } typedef PopoverActionCellBuilder = Widget Function( @@ -159,11 +121,7 @@ abstract class PopoverActionCell extends PopoverAction { } abstract class CustomActionCell extends PopoverAction { - Widget buildWithContext( - BuildContext context, - PopoverController controller, - PopoverMutex? mutex, - ); + Widget buildWithContext(BuildContext context, PopoverController controller); } abstract class PopoverAction {} @@ -201,7 +159,6 @@ class ActionCellWidget extends StatelessWidget { leftIcon: leftIcon, rightIcon: rightIcon, name: actionCell.name, - textColor: actionCell.textColor(context), onTap: () => onSelected(action), ); } @@ -265,7 +222,6 @@ class HoverButton extends StatelessWidget { this.leftIcon, required this.name, this.rightIcon, - this.textColor, }); final VoidCallback onTap; @@ -273,7 +229,6 @@ class HoverButton extends StatelessWidget { final Widget? leftIcon; final Widget? rightIcon; final String name; - final Color? textColor; @override Widget build(BuildContext context) { @@ -294,7 +249,6 @@ class HoverButton extends StatelessWidget { name, overflow: TextOverflow.visible, lineHeight: 1.15, - color: textColor, ), ), if (rightIcon != null) ...[ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index 954fc77603..ab67ecb5b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -1,34 +1,28 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; - class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ super.key, - required this.view, + required this.viewId, required this.name, required this.popoverController, required this.emoji, this.icon, this.showIconChanger = true, - this.tabs = const [PickerTabType.emoji, PickerTabType.icon], }); - final ViewPB view; + final String viewId; final String name; final PopoverController popoverController; - final EmojiIconData emoji; + final String emoji; final Widget? icon; final bool showIconChanger; - final List tabs; @override State createState() => _RenameViewPopoverState(); @@ -65,8 +59,6 @@ class _RenameViewPopoverState extends State { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 18), onSubmitted: _updateViewIcon, - documentId: widget.view.id, - tabs: widget.tabs, ), ), const HSpace(6), @@ -89,23 +81,18 @@ class _RenameViewPopoverState extends State { Future _updateViewName(String name) async { if (name.isNotEmpty && name != widget.name) { await ViewBackendService.updateView( - viewId: widget.view.id, + viewId: widget.viewId, name: _controller.text, ); widget.popoverController.close(); } } - Future _updateViewIcon( - SelectedEmojiIconResult r, - PopoverController? _, - ) async { + Future _updateViewIcon(String emoji, PopoverController? _) async { await ViewBackendService.updateViewIcon( - view: widget.view, - viewIcon: r.data, + viewId: widget.viewId, + viewIcon: emoji, ); - if (!r.keepOpen) { - widget.popoverController.close(); - } + widget.popoverController.close(); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/sidebar_resizer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/sidebar_resizer.dart deleted file mode 100644 index f229951e04..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/sidebar_resizer.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SidebarResizer extends StatefulWidget { - const SidebarResizer({super.key}); - - @override - State createState() => _SidebarResizerState(); -} - -class _SidebarResizerState extends State { - final ValueNotifier isHovered = ValueNotifier(false); - final ValueNotifier isDragging = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - isDragging.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - cursor: SystemMouseCursors.resizeLeftRight, - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: GestureDetector( - dragStartBehavior: DragStartBehavior.down, - behavior: HitTestBehavior.translucent, - onHorizontalDragStart: (details) { - isDragging.value = true; - - context - .read() - .add(const HomeSettingEvent.editPanelResizeStart()); - }, - onHorizontalDragUpdate: (details) { - isDragging.value = true; - - context - .read() - .add(HomeSettingEvent.editPanelResized(details.localPosition.dx)); - }, - onHorizontalDragEnd: (details) { - isDragging.value = false; - - context - .read() - .add(const HomeSettingEvent.editPanelResizeEnd()); - }, - onHorizontalDragCancel: () { - isDragging.value = false; - - context - .read() - .add(const HomeSettingEvent.editPanelResizeEnd()); - }, - child: ValueListenableBuilder( - valueListenable: isHovered, - builder: (context, isHovered, _) { - return ValueListenableBuilder( - valueListenable: isDragging, - builder: (context, isDragging, _) { - return Container( - width: 2, - // increase the width of the resizer to make it easier to drag - margin: const EdgeInsets.only(right: 2.0), - height: MediaQuery.of(context).size.height, - color: isHovered || isDragging - ? const Color(0xFF00B5FF) - : Colors.transparent, - ); - }, - ); - }, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart index 88474b20b3..1023553efb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/tab_bar_item.dart @@ -1,20 +1,12 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; -import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart'; -import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class ViewTabBarItem extends StatefulWidget { - const ViewTabBarItem({ - super.key, - required this.view, - this.shortForm = false, - }); + const ViewTabBarItem({super.key, required this.view}); final ViewPB view; - final bool shortForm; @override State createState() => _ViewTabBarItemState(); @@ -46,25 +38,6 @@ class _ViewTabBarItemState extends State { @override Widget build(BuildContext context) { - return Row( - mainAxisAlignment: - widget.shortForm ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - if (widget.view.icon.value.isNotEmpty) - RawEmojiIconWidget( - emoji: widget.view.icon.toEmojiIconData(), - emojiSize: 16, - ), - if (!widget.shortForm && view.icon.value.isNotEmpty) const HSpace(6), - if (!widget.shortForm || view.icon.value.isEmpty) ...[ - Flexible( - child: FlowyText.medium( - view.nameOrDefault, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ], - ); + return FlowyText.medium(view.name); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart index 5cb834cbf3..c788eaeea3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -1,43 +1,16 @@ +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; -class ToggleStyle { - const ToggleStyle({ - required this.height, - required this.width, - required this.thumbRadius, - }); - - const ToggleStyle.big() - : height = 16, - width = 27, - thumbRadius = 14; - - const ToggleStyle.small() - : height = 10, - width = 16, - thumbRadius = 8; - - const ToggleStyle.mobile() - : height = 24, - width = 42, - thumbRadius = 18; - - final double height; - final double width; - final double thumbRadius; -} - class Toggle extends StatelessWidget { const Toggle({ super.key, required this.value, required this.onChanged, - this.style = const ToggleStyle.big(), + required this.style, this.thumbColor, this.activeBackgroundColor, this.inactiveBackgroundColor, - this.duration = const Duration(milliseconds: 150), this.padding = const EdgeInsets.all(8.0), }); @@ -48,16 +21,14 @@ class Toggle extends StatelessWidget { final Color? activeBackgroundColor; final Color? inactiveBackgroundColor; final EdgeInsets padding; - final Duration duration; @override Widget build(BuildContext context) { final backgroundColor = value ? activeBackgroundColor ?? Theme.of(context).colorScheme.primary - : inactiveBackgroundColor ?? - AFThemeExtension.of(context).toggleButtonBGColor; + : activeBackgroundColor ?? AFThemeExtension.of(context).toggleOffFill; return GestureDetector( - onTap: () => onChanged(!value), + onTap: () => onChanged(value), child: Padding( padding: padding, child: Stack( @@ -71,14 +42,14 @@ class Toggle extends StatelessWidget { ), ), AnimatedPositioned( - duration: duration, + duration: const Duration(milliseconds: 150), top: (style.height - style.thumbRadius) / 2, left: value ? style.width - style.thumbRadius - 1 : 1, child: Container( height: style.thumbRadius, width: style.thumbRadius, decoration: BoxDecoration( - color: thumbColor ?? Colors.white, + color: thumbColor ?? Theme.of(context).colorScheme.onPrimary, borderRadius: BorderRadius.circular(style.thumbRadius / 2), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart new file mode 100644 index 0000000000..d11bb5e40e --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/toggle/toggle_style.dart @@ -0,0 +1,20 @@ +class ToggleStyle { + ToggleStyle({ + required this.height, + required this.width, + required this.thumbRadius, + }); + + final double height; + final double width; + final double thumbRadius; + + static ToggleStyle get big => + ToggleStyle(height: 16, width: 27, thumbRadius: 14); + + static ToggleStyle get small => + ToggleStyle(height: 10, width: 16, thumbRadius: 8); + + static ToggleStyle get mobile => + ToggleStyle(height: 24, width: 42, thumbRadius: 18); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 347d95d01d..adb3b1e454 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -90,14 +90,15 @@ class UserAvatar extends StatelessWidget { : null, ), child: ClipRRect( - borderRadius: BorderRadius.circular(size / 2), - child: Image.network( - iconUrl, - width: size, - height: size, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) => - _buildEmptyAvatar(context), + borderRadius: Corners.s5Border, + child: CircleAvatar( + backgroundColor: Colors.transparent, + child: Image.network( + iconUrl, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => + _buildEmptyAvatar(context), + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 3be0973123..ea453e4666 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,26 +1,18 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/icon_emoji_picker/tab.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_lock_status_bloc.dart'; import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart'; import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart'; - -// space name > ... > view_title +// workspace name > ... > view_title class ViewTitleBar extends StatelessWidget { const ViewTitleBar({ super.key, @@ -29,16 +21,12 @@ class ViewTitleBar extends StatelessWidget { final ViewPB view; + // late Future> ancestors; @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => ViewTitleBarBloc(view: view)), - BlocProvider( - create: (_) => ViewLockStatusBloc(view: view) - ..add(const ViewLockStatusEvent.initial()), - ), - ], + return BlocProvider( + create: (_) => + ViewTitleBarBloc(view: view)..add(const ViewTitleBarEvent.initial()), child: BlocBuilder( builder: (context, state) { final ancestors = state.ancestors; @@ -49,16 +37,7 @@ class ViewTitleBar extends StatelessWidget { scrollDirection: Axis.horizontal, child: SizedBox( height: 24, - child: Row( - children: [ - ..._buildViewTitles( - context, - ancestors, - state.isDeleted, - ), - _buildLockPageStatus(context), - ], - ), + child: Row(children: _buildViewTitles(context, ancestors)), ), ); }, @@ -66,38 +45,7 @@ class ViewTitleBar extends StatelessWidget { ); } - Widget _buildLockPageStatus(BuildContext context) { - return BlocConsumer( - listenWhen: (previous, current) => - previous.isLoadingLockStatus == current.isLoadingLockStatus && - current.isLoadingLockStatus == false, - listener: (context, state) { - if (state.isLocked) { - showToastNotification( - message: LocaleKeys.lockPage_pageLockedToast.tr(), - ); - } - }, - builder: (context, state) { - if (state.isLocked) { - return LockedPageStatus(); - } else if (!state.isLocked && state.lockCounter > 0) { - return ReLockedPageStatus(); - } - return const SizedBox.shrink(); - }, - ); - } - - List _buildViewTitles( - BuildContext context, - List views, - bool isDeleted, - ) { - if (isDeleted) { - return _buildDeletedTitle(context, views.last); - } - + List _buildViewTitles(BuildContext context, List views) { // if the level is too deep, only show the last two view, the first one view and the root view // for example: // if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5] @@ -128,13 +76,12 @@ class ViewTitleBar extends StatelessWidget { } final child = FlowyTooltip( - key: ValueKey(view.id), message: view.name, - child: ViewTitle( + child: _ViewTitle( view: view, - behavior: i == views.length - 1 && !view.isLocked - ? ViewTitleBehavior.editable // only the last one is editable - : ViewTitleBehavior.uneditable, // others are not editable + behavior: i == views.length - 1 + ? _ViewTitleBehavior.editable // only the last one is editable + : _ViewTitleBehavior.uneditable, // others are not editable onUpdated: () { context .read() @@ -152,82 +99,29 @@ class ViewTitleBar extends StatelessWidget { } return children; } - - List _buildDeletedTitle(BuildContext context, ViewPB view) { - return [ - const TrashBreadcrumb(), - const FlowySvg(FlowySvgs.title_bar_divider_s), - FlowyTooltip( - key: ValueKey(view.id), - message: view.name, - child: ViewTitle( - view: view, - onUpdated: () => context - .read() - .add(const ViewTitleBarEvent.reload()), - ), - ), - ]; - } } -class TrashBreadcrumb extends StatelessWidget { - const TrashBreadcrumb({super.key}); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 32, - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric(horizontal: 6.0), - onTap: () { - getIt().latestOpenView = null; - getIt().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - text: Row( - children: [ - const FlowySvg(FlowySvgs.trash_s, size: Size.square(14)), - const HSpace(4.0), - FlowyText.regular( - LocaleKeys.trash_text.tr(), - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - figmaLineHeight: 18.0, - ), - ], - ), - ), - ); - } -} - -enum ViewTitleBehavior { +enum _ViewTitleBehavior { editable, uneditable, } -class ViewTitle extends StatefulWidget { - const ViewTitle({ - super.key, +class _ViewTitle extends StatefulWidget { + const _ViewTitle({ required this.view, - this.behavior = ViewTitleBehavior.editable, + this.behavior = _ViewTitleBehavior.editable, required this.onUpdated, }); final ViewPB view; - final ViewTitleBehavior behavior; + final _ViewTitleBehavior behavior; final VoidCallback onUpdated; @override - State createState() => _ViewTitleState(); + State<_ViewTitle> createState() => _ViewTitleState(); } -class _ViewTitleState extends State { +class _ViewTitleState extends State<_ViewTitle> { final popoverController = PopoverController(); final textEditingController = TextEditingController(); @@ -241,19 +135,12 @@ class _ViewTitleState extends State { @override Widget build(BuildContext context) { - final isEditable = widget.behavior == ViewTitleBehavior.editable; + final isEditable = widget.behavior == _ViewTitleBehavior.editable; return BlocProvider( create: (_) => ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()), child: BlocConsumer( - listenWhen: (previous, current) { - if (previous.view == null || current.view == null) { - return false; - } - - return previous.view != current.view; - }, listener: (_, state) { _resetTextEditingController(state); widget.onUpdated(); @@ -283,7 +170,7 @@ class _ViewTitleState extends State { return Container( alignment: Alignment.center, margin: const EdgeInsets.symmetric(horizontal: 6.0), - child: _buildIconAndName(context, state, false), + child: _buildIconAndName(state, false), ); } @@ -295,7 +182,7 @@ class _ViewTitleState extends State { child: FlowyButton( useIntrinsicWidth: true, margin: const EdgeInsets.symmetric(horizontal: 6.0), - text: _buildIconAndName(context, state, false), + text: _buildIconAndName(state, false), ), ), ); @@ -314,16 +201,11 @@ class _ViewTitleState extends State { // icon + textfield _resetTextEditingController(state); return RenameViewPopover( - view: widget.view, + viewId: widget.view.id, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), emoji: state.icon, - tabs: const [ - PickerTabType.emoji, - PickerTabType.icon, - PickerTabType.custom, - ], ); }, child: SizedBox( @@ -331,29 +213,27 @@ class _ViewTitleState extends State { child: FlowyButton( useIntrinsicWidth: true, margin: const EdgeInsets.symmetric(horizontal: 6.0), - text: _buildIconAndName(context, state, true), + text: _buildIconAndName(state, true), ), ), ); } - Widget _buildIconAndName( - BuildContext context, - ViewTitleState state, - bool isEditable, - ) { - final spaceIcon = state.view?.buildSpaceIconSvg(context); + Widget _buildIconAndName(ViewTitleState state, bool isEditable) { return SingleChildScrollView( child: Row( children: [ if (state.icon.isNotEmpty) ...[ - RawEmojiIconWidget(emoji: state.icon, emojiSize: 14.0), + FlowyText.emoji( + state.icon, + fontSize: 14.0, + ), const HSpace(4.0), ], - if (state.view?.isSpace == true && spaceIcon != null) ...[ + if (state.view?.isSpace == true && + state.view?.spaceIconSvg != null) ...[ SpaceIcon( dimension: 14, - svgSize: 8.5, space: state.view!, cornerRadius: 4, ), @@ -362,12 +242,8 @@ class _ViewTitleState extends State { Opacity( opacity: isEditable ? 1.0 : 0.5, child: FlowyText.regular( - state.name.isEmpty - ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() - : state.name, - fontSize: 14.0, + state.name, overflow: TextOverflow.ellipsis, - figmaLineHeight: 18.0, ), ), ], @@ -384,92 +260,3 @@ class _ViewTitleState extends State { ); } } - -class LockedPageStatus extends StatelessWidget { - const LockedPageStatus({super.key}); - - @override - Widget build(BuildContext context) { - final color = const Color(0xFFD95A0B); - return FlowyTooltip( - message: LocaleKeys.lockPage_lockTooltip.tr(), - child: DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(color: color), - borderRadius: BorderRadius.circular(6), - ), - color: context.lockedPageButtonBackground, - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 4.0, - ), - iconPadding: 4.0, - text: FlowyText.regular( - LocaleKeys.lockPage_lockPage.tr(), - color: color, - fontSize: 12.0, - ), - hoverColor: color.withValues(alpha: 0.1), - leftIcon: FlowySvg( - FlowySvgs.lock_page_fill_s, - blendMode: null, - ), - onTap: () => context.read().add( - const ViewLockStatusEvent.unlock(), - ), - ), - ), - ); - } -} - -class ReLockedPageStatus extends StatelessWidget { - const ReLockedPageStatus({super.key}); - - @override - Widget build(BuildContext context) { - final iconColor = const Color(0xFF8F959E); - return DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: BorderSide(color: iconColor), - borderRadius: BorderRadius.circular(6), - ), - color: context.lockedPageButtonBackground, - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric( - horizontal: 4.0, - vertical: 4.0, - ), - iconPadding: 4.0, - text: FlowyText.regular( - LocaleKeys.lockPage_reLockPage.tr(), - fontSize: 12.0, - ), - leftIcon: FlowySvg( - FlowySvgs.unlock_page_s, - color: iconColor, - blendMode: null, - ), - onTap: () => context.read().add( - const ViewLockStatusEvent.lock(), - ), - ), - ); - } -} - -extension on BuildContext { - Color get lockedPageButtonBackground { - if (Theme.of(this).brightness == Brightness.light) { - return Colors.white.withValues(alpha: 0.75); - } - return Color(0xB21B1A22); - } -} diff --git a/frontend/appflowy_flutter/linux/my_application.cc b/frontend/appflowy_flutter/linux/my_application.cc index 2a3a02cac4..25b07c8d9c 100644 --- a/frontend/appflowy_flutter/linux/my_application.cc +++ b/frontend/appflowy_flutter/linux/my_application.cc @@ -28,7 +28,38 @@ static void my_application_activate(GApplication *application) GtkWindow *window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - gtk_window_set_title(window, "AppFlowy"); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) + { + const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) + { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) + { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "AppFlowy"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } + else + { + gtk_window_set_title(window, "AppFlowy"); + } gtk_window_set_default_size(window, 1280, 720); gtk_widget_show(GTK_WIDGET(window)); diff --git a/frontend/appflowy_flutter/linux/packaging/assets/logo.png b/frontend/appflowy_flutter/linux/packaging/assets/logo.png deleted file mode 100644 index f34332a395..0000000000 Binary files a/frontend/appflowy_flutter/linux/packaging/assets/logo.png and /dev/null differ diff --git a/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml b/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml deleted file mode 100644 index 801a5dbc02..0000000000 --- a/frontend/appflowy_flutter/linux/packaging/deb/make_config.yaml +++ /dev/null @@ -1,36 +0,0 @@ -display_name: AppFlowy -package_name: appflowy - -maintainer: - name: AppFlowy - email: support@appflowy.io - -keywords: - - AppFlowy - - Office - - Document - - Database - - Note - - Kanban - - Note - -installed_size: 100000 -icon: linux/packaging/assets/logo.png - -generic_name: AppFlowy - -categories: - - Office - - Productivity - -startup_notify: true -essential: false - -section: x11 -priority: optional - -supportedMimeType: x-scheme-handler/appflowy-flutter - -dependencies: - - libnotify-bin - - libkeybinder-3.0-0 diff --git a/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml b/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml deleted file mode 100644 index 3fcdea03bc..0000000000 --- a/frontend/appflowy_flutter/linux/packaging/rpm/make_config.yaml +++ /dev/null @@ -1,33 +0,0 @@ -display_name: AppFlowy -icon: linux/packaging/assets/logo.png -group: Applications/Office -vendor: AppFlowy -packager: AppFlowy -packagerEmail: support@appflowy.io -license: APGL-3.0 -url: https://github.com/AppFlowy-IO/appflowy - -build_arch: x86_64 - -keywords: - - AppFlowy - - Office - - Document - - Database - - Note - - Kanban - - Note - -generic_name: AppFlowy - -categories: - - Office - - Productivity - -startup_notify: true - -supportedMimeType: x-scheme-handler/appflowy-flutter - -requires: - - libnotify - - keybinder diff --git a/frontend/appflowy_flutter/macos/.gitignore b/frontend/appflowy_flutter/macos/.gitignore index d2fd377230..9aad20e46d 100644 --- a/frontend/appflowy_flutter/macos/.gitignore +++ b/frontend/appflowy_flutter/macos/.gitignore @@ -4,3 +4,4 @@ # Xcode-related **/xcuserdata/ +Podfile.lock \ No newline at end of file diff --git a/frontend/appflowy_flutter/macos/Podfile.lock b/frontend/appflowy_flutter/macos/Podfile.lock deleted file mode 100644 index e06670c5a5..0000000000 --- a/frontend/appflowy_flutter/macos/Podfile.lock +++ /dev/null @@ -1,178 +0,0 @@ -PODS: - - app_links (1.0.0): - - FlutterMacOS - - appflowy_backend (0.0.1): - - FlutterMacOS - - auto_updater_macos (0.0.1): - - FlutterMacOS - - Sparkle - - bitsdojo_window_macos (0.0.1): - - FlutterMacOS - - connectivity_plus (0.0.1): - - FlutterMacOS - - ReachabilitySwift - - desktop_drop (0.0.1): - - FlutterMacOS - - device_info_plus (0.0.1): - - FlutterMacOS - - file_selector_macos (0.0.1): - - FlutterMacOS - - flowy_infra_ui (0.0.1): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - HotKey (0.2.1) - - hotkey_manager (0.0.1): - - FlutterMacOS - - HotKey - - irondash_engine_context (0.0.1): - - FlutterMacOS - - local_notifier (0.1.0): - - FlutterMacOS - - package_info_plus (0.0.1): - - FlutterMacOS - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - ReachabilitySwift (5.2.4) - - screen_retriever_macos (0.0.1): - - FlutterMacOS - - Sentry/HybridSDK (8.35.1) - - sentry_flutter (8.8.0): - - Flutter - - FlutterMacOS - - Sentry/HybridSDK (= 8.35.1) - - share_plus (0.0.1): - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - - Sparkle (2.6.4) - - sqflite_darwin (0.0.4): - - Flutter - - FlutterMacOS - - super_native_extensions (0.0.1): - - FlutterMacOS - - url_launcher_macos (0.0.1): - - FlutterMacOS - - webview_flutter_wkwebview (0.0.1): - - Flutter - - FlutterMacOS - - window_manager (0.2.0): - - FlutterMacOS - -DEPENDENCIES: - - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - - appflowy_backend (from `Flutter/ephemeral/.symlinks/plugins/appflowy_backend/macos`) - - auto_updater_macos (from `Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos`) - - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) - - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) - - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - - flowy_infra_ui (from `Flutter/ephemeral/.symlinks/plugins/flowy_infra_ui/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - hotkey_manager (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos`) - - irondash_engine_context (from `Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos`) - - local_notifier (from `Flutter/ephemeral/.symlinks/plugins/local_notifier/macos`) - - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - - super_native_extensions (from `Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - -SPEC REPOS: - trunk: - - HotKey - - ReachabilitySwift - - Sentry - - Sparkle - -EXTERNAL SOURCES: - app_links: - :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos - appflowy_backend: - :path: Flutter/ephemeral/.symlinks/plugins/appflowy_backend/macos - auto_updater_macos: - :path: Flutter/ephemeral/.symlinks/plugins/auto_updater_macos/macos - bitsdojo_window_macos: - :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos - connectivity_plus: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos - desktop_drop: - :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos - device_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos - file_selector_macos: - :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos - flowy_infra_ui: - :path: Flutter/ephemeral/.symlinks/plugins/flowy_infra_ui/macos - FlutterMacOS: - :path: Flutter/ephemeral - hotkey_manager: - :path: Flutter/ephemeral/.symlinks/plugins/hotkey_manager/macos - irondash_engine_context: - :path: Flutter/ephemeral/.symlinks/plugins/irondash_engine_context/macos - local_notifier: - :path: Flutter/ephemeral/.symlinks/plugins/local_notifier/macos - package_info_plus: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - screen_retriever_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos - sentry_flutter: - :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos - share_plus: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite_darwin: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin - super_native_extensions: - :path: Flutter/ephemeral/.symlinks/plugins/super_native_extensions/macos - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos - webview_flutter_wkwebview: - :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin - window_manager: - :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos - -SPEC CHECKSUMS: - app_links: 9028728e32c83a0831d9db8cf91c526d16cc5468 - appflowy_backend: 464aeb3e5c6966a41641a2111e5ead72ce2695f7 - auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118 - bitsdojo_window_macos: 7959fb0ca65a3ccda30095c181ecb856fae48ea9 - connectivity_plus: e74b9f74717d2d99d45751750e266e55912baeb5 - desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 - device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76 - file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 - flowy_infra_ui: 8760ff42a789de40bf5007a5f176b454722a341e - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 - hotkey_manager: b443f35f4d772162937aa73fd8995e579f8ac4e2 - irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba - local_notifier: ebf072651e35ae5e47280ad52e2707375cb2ae4e - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - ReachabilitySwift: 32793e867593cfc1177f5d16491e3a197d2fccda - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f - Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1 - sentry_flutter: e24b397f9a61fa5bbefd8279c3b2242ca86faa90 - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - Sparkle: 5f8960a7a119aa7d45dacc0d5837017170bc5675 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - webview_flutter_wkwebview: 44d4dee7d7056d5ad185d25b38404436d56c547c - window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c - -PODFILE CHECKSUM: 0532f3f001ca3110b8be345d6491fff690e95823 - -COCOAPODS: 1.16.2 diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj index 88c451bdd9..13448a56c9 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj @@ -28,8 +28,6 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 706E045829F286F600B789F4 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 706E045729F286EC00B789F4 /* libc++.tbd */; }; D7360C6D6177708F7B2D3C9D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD81A6C7244B2318E0BA2E8 /* Pods_Runner.framework */; }; - FB4E0E4F2CC9F3F900C57E87 /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */; }; - FB54062C2D22665000223D60 /* liblzma.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB54062B2D22664200223D60 /* liblzma.tbd */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -77,8 +75,6 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 7D41C30A3910C3A40B6085E3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - FB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; - FB54062B2D22664200223D60 /* liblzma.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = liblzma.tbd; path = usr/lib/liblzma.tbd; sourceTree = SDKROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,9 +82,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - FB4E0E4F2CC9F3F900C57E87 /* libbz2.tbd in Frameworks */, 706E045829F286F600B789F4 /* libc++.tbd in Frameworks */, - FB54062C2D22665000223D60 /* liblzma.tbd in Frameworks */, D7360C6D6177708F7B2D3C9D /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -174,8 +168,6 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - FB54062B2D22664200223D60 /* liblzma.tbd */, - FB4E0E4E2CC9F3E900C57E87 /* libbz2.tbd */, 706E045729F286EC00B789F4 /* libc++.tbd */, 1CD81A6C7244B2318E0BA2E8 /* Pods_Runner.framework */, ); diff --git a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift index c7872aaec9..d53ef64377 100644 --- a/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift +++ b/frontend/appflowy_flutter/macos/Runner/AppDelegate.swift @@ -1,23 +1,9 @@ import Cocoa import FlutterMacOS -@main +@NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return false - } - - override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - if !flag { - for window in sender.windows { - window.makeKeyAndOrderFront(self) - } - } - - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true } } diff --git a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig index d2b3d7e9b3..da469610eb 100644 --- a/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/frontend/appflowy_flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = AppFlowy PRODUCT_BUNDLE_IDENTIFIER = io.appflowy.appflowy // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 AppFlowy.IO. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2024 AppFlowy.IO. All rights reserved. diff --git a/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements b/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements index 4c829c7ab0..71949adefe 100644 --- a/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/DebugProfile.entitlements @@ -1,20 +1,20 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.temporary-exception.files.absolute-path.read-write - - / - - - \ No newline at end of file + + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + + diff --git a/frontend/appflowy_flutter/macos/Runner/Info.plist b/frontend/appflowy_flutter/macos/Runner/Info.plist index cb3d1127a0..cd07887134 100644 --- a/frontend/appflowy_flutter/macos/Runner/Info.plist +++ b/frontend/appflowy_flutter/macos/Runner/Info.plist @@ -1,61 +1,57 @@ - - LSApplicationCategoryType - public.app-category.productivity - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleLocalizations - - en - fr - it - zh - - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleURLTypes - - - CFBundleURLName - - CFBundleURLSchemes - - appflowy-flutter - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSAppTransportSecurity + + LSApplicationCategoryType + public.app-category.productivity + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + fr + it + zh + + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleURLTypes + - NSAllowsArbitraryLoads - + CFBundleURLName + + CFBundleURLSchemes + + appflowy-flutter + - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - SUPublicEDKey - Bs++IOmOwYmNTjMMC2jMqLNldP+mndDp/LwujCg2/kw= - SUAllowsAutomaticUpdates - + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSAppTransportSecurity + + NSAllowsArbitraryLoads + - \ No newline at end of file + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift index 620ad5c9bc..65498b121f 100644 --- a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift +++ b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift @@ -74,13 +74,7 @@ class MainFlutterWindow: NSWindow { self.titleVisibility = .hidden self.styleMask.insert(StyleMask.fullSizeContentView) self.isMovableByWindowBackground = true - - // For the macOS version 15 or higher, set it to true to enable the window tiling - if #available(macOS 15.0, *) { - self.isMovable = true - } else { - self.isMovable = false - } + self.isMovable = false self.layoutTrafficLights() diff --git a/frontend/appflowy_flutter/macos/Runner/Release.entitlements b/frontend/appflowy_flutter/macos/Runner/Release.entitlements index 4c829c7ab0..71949adefe 100644 --- a/frontend/appflowy_flutter/macos/Runner/Release.entitlements +++ b/frontend/appflowy_flutter/macos/Runner/Release.entitlements @@ -1,20 +1,20 @@ - - com.apple.security.app-sandbox - - com.apple.security.files.downloads.read-write - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.network.server - - com.apple.security.temporary-exception.files.absolute-path.read-write - - / - - - \ No newline at end of file + + com.apple.security.app-sandbox + + com.apple.security.files.downloads.read-write + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.temporary-exception.files.absolute-path.read-write + + / + + + diff --git a/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json b/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json deleted file mode 100644 index 87f89b3bbc..0000000000 --- a/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/project/PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1-json +++ /dev/null @@ -1 +0,0 @@ -{"appPreferencesBuildSettings":{},"buildConfigurations":[{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf","ENABLE_STRICT_OBJC_MSGSEND":"YES","ENABLE_TESTABILITY":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_DYNAMIC_NO_PIC":"NO","GCC_NO_COMMON_BLOCKS":"YES","GCC_OPTIMIZATION_LEVEL":"0","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_DEBUG=1 DEBUG=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"12.0","MTL_ENABLE_DEBUG_INFO":"INCLUDE_SOURCE","MTL_FAST_MATH":"YES","ONLY_ACTIVE_ARCH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_ACTIVE_COMPILATION_CONDITIONS":"DEBUG","SWIFT_OPTIMIZATION_LEVEL":"-Onone","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e98814b7e2c3bac55ee99d78eaa8d1ec61e","name":"Debug"},{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","ENABLE_NS_ASSERTIONS":"NO","ENABLE_STRICT_OBJC_MSGSEND":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_NO_COMMON_BLOCKS":"YES","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_PROFILE=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"12.0","MTL_ENABLE_DEBUG_INFO":"NO","MTL_FAST_MATH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_COMPILATION_MODE":"wholemodule","SWIFT_OPTIMIZATION_LEVEL":"-O","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e98c22f26ca3341c3062f2313dc737070d4","name":"Profile"},{"buildSettings":{"ALWAYS_SEARCH_USER_PATHS":"NO","CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED":"YES","CLANG_ANALYZER_NONNULL":"YES","CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION":"YES_AGGRESSIVE","CLANG_CXX_LANGUAGE_STANDARD":"gnu++14","CLANG_CXX_LIBRARY":"libc++","CLANG_ENABLE_MODULES":"YES","CLANG_ENABLE_OBJC_ARC":"YES","CLANG_ENABLE_OBJC_WEAK":"YES","CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING":"YES","CLANG_WARN_BOOL_CONVERSION":"YES","CLANG_WARN_COMMA":"YES","CLANG_WARN_CONSTANT_CONVERSION":"YES","CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS":"YES","CLANG_WARN_DIRECT_OBJC_ISA_USAGE":"YES_ERROR","CLANG_WARN_DOCUMENTATION_COMMENTS":"YES","CLANG_WARN_EMPTY_BODY":"YES","CLANG_WARN_ENUM_CONVERSION":"YES","CLANG_WARN_INFINITE_RECURSION":"YES","CLANG_WARN_INT_CONVERSION":"YES","CLANG_WARN_NON_LITERAL_NULL_CONVERSION":"YES","CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF":"YES","CLANG_WARN_OBJC_LITERAL_CONVERSION":"YES","CLANG_WARN_OBJC_ROOT_CLASS":"YES_ERROR","CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER":"YES","CLANG_WARN_RANGE_LOOP_ANALYSIS":"YES","CLANG_WARN_STRICT_PROTOTYPES":"YES","CLANG_WARN_SUSPICIOUS_MOVE":"YES","CLANG_WARN_UNGUARDED_AVAILABILITY":"YES_AGGRESSIVE","CLANG_WARN_UNREACHABLE_CODE":"YES","CLANG_WARN__DUPLICATE_METHOD_MATCH":"YES","COPY_PHASE_STRIP":"NO","DEBUG_INFORMATION_FORMAT":"dwarf-with-dsym","ENABLE_NS_ASSERTIONS":"NO","ENABLE_STRICT_OBJC_MSGSEND":"YES","GCC_C_LANGUAGE_STANDARD":"gnu11","GCC_NO_COMMON_BLOCKS":"YES","GCC_PREPROCESSOR_DEFINITIONS":"POD_CONFIGURATION_RELEASE=1 $(inherited)","GCC_WARN_64_TO_32_BIT_CONVERSION":"YES","GCC_WARN_ABOUT_RETURN_TYPE":"YES_ERROR","GCC_WARN_UNDECLARED_SELECTOR":"YES","GCC_WARN_UNINITIALIZED_AUTOS":"YES_AGGRESSIVE","GCC_WARN_UNUSED_FUNCTION":"YES","GCC_WARN_UNUSED_VARIABLE":"YES","IPHONEOS_DEPLOYMENT_TARGET":"12.0","MTL_ENABLE_DEBUG_INFO":"NO","MTL_FAST_MATH":"YES","PRODUCT_NAME":"$(TARGET_NAME)","STRIP_INSTALLED_PRODUCT":"NO","SWIFT_COMPILATION_MODE":"wholemodule","SWIFT_OPTIMIZATION_LEVEL":"-O","SWIFT_VERSION":"5.0","SYMROOT":"${SRCROOT}/../build"},"guid":"bfdfe7dc352907fc980b868725387e9828903703a9fe9e3707306e58aab67b51","name":"Release"}],"classPrefix":"","defaultConfigurationName":"Release","developmentRegion":"en","groupTree":{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98d0b25d39b515a574839e998df229c3cb","path":"../Podfile","sourceTree":"SOURCE_ROOT","type":"file"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f986fc29daf4cb723e5ecd0e77c9cc3a","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Classes/AppLinksPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983499ae993d0615bc4a25c6d23d299cd2","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Classes/AppLinksPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9878bdb70529628b051bfb14170b6ce281","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Classes/SwiftAppLinksPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98803a16064d6b26357b9fa2d9c81eb2c0","name":"Classes","path":"Classes","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e982bcf9ac9f04ccd93893e9ae54d152c11","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980af336cd97bd48a5f76247c45af3ca5a","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d4c688735e4b7c4c9af25f0254256c5a","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980f47fa79356e5b78e85637df4eab1e8f","name":"app_links","path":"app_links","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9852951db13a823ccb98a790f496fab4e3","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988786e23fb077ded3affc0f7a37fc275c","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9874ef26f64fe74d7c02d1a94bcde1184c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989187ad07f57154eca7d638acb8377adb","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dc7eee9f7cdf2e623ebc316ac5388a1e","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982c3c6bda68e418bfa0a7d818fbfb0037","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a15f02f386a528bc8eb605ae37642455","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cbbda7998cd576c276e2258fb3bad5a5","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982b881f7fdcc02480bc57ddb51bd8d98f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98aa491e8e0f7d7e2c84ecbe06c2f4df77","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ae796e67846c10ae19147ab24013260d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ee7362a7cb62b913a6f351fa670031fb","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c70d2ff23f2582de73eade3b63ca6732","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cfdf5bb4bb8df40c98fd16d64963a3be","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9827cc495d5b0d43a98c370d69de3ff8ae","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/ios/app_links.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98b1016bc412809827cc7f8ffd7be68d60","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/app_links-6.3.3/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e988f23ea9e443884f6d6a834c279bb42c4","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e989d5bbbb2531f246d5e384ddd39c9ef94","path":"app_links.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985bbf5bd8a81b79ac8b94165aebec6742","path":"app_links-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98a0510b51afa43c66fac3cfa1b303b58d","path":"app_links-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988921fa59229e94f886b598374e0a0a6c","path":"app_links-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988a8ca854307b43eb2b99c9daa9a1cc96","path":"app_links-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986a31ee17a0bf46ce08a50fbab9d7e0d7","path":"app_links.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e981b9d938e8177ba8be7381344ebbf3f72","path":"app_links.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98faf0d598eef28587701cbceba0a82824","path":"ResourceBundle-app_links_ios_privacy-app_links-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9834af3cd6a85915e92c7086a5cef194bd","name":"Support Files","path":"../../../../Pods/Target Support Files/app_links","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985fe1a1ae80c7c29a049e0b0f5a968eb6","name":"app_links","path":"../.symlinks/plugins/app_links/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c9b1d2b694569060c7a89ce432ae59aa","path":"../../../../../../packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d2eb541b9cb3760f4d8a0de1aceac0cd","path":"../../../../../../packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98ff9d3e6fbf7f3ebdf3b2d98a4d52480e","path":"../../../../../../packages/appflowy_backend/ios/Classes/AppFlowyBackendPlugin.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98082f3bd8728bb955d3b231cc205df22a","path":"../../../../../../packages/appflowy_backend/ios/Classes/binding.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e981cf16cdf61f891cb9befc3392b13ff5f","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980ba42de9b682ddbb7d9ecb3a91add63c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ce8a30ee373383f69298444d24489306","name":"appflowy_backend","path":"appflowy_backend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c86a47e2309e0b005ed79419dd093f0d","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98503452eddbee5272ed788d0cc749960e","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98055cce552f828e105a78ac03f0bdd805","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ca2c764817f57c4676d84d72558b3899","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f5501b2ed32cd958d3563e60f001d703","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b9c0603d95a0886bda3e008ea3e3501a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9877a3c2776a0dd48b2c60d087afb651a0","name":"..","path":"../../../../../packages/appflowy_backend/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"archive.ar","guid":"bfdfe7dc352907fc980b868725387e9836c8d2e47d81f8a6899c11848d60e876","path":"../../../../../packages/appflowy_backend/ios/libdart_ffi.a","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d5592f7a99092768eeefc4cfc096cae8","name":"Frameworks","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98c2aceac4d9f7a88a7f2aef9ddb559c68","path":"../../../../../packages/appflowy_backend/ios/appflowy_backend.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e985e17269d373b81baa61bd43ba6a542e5","path":"../../../../../packages/appflowy_backend/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9850866a0df8df8e8e5c4a2fae74fa1e1f","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98e87c56b9467409c44163e34bc882ac5d","path":"appflowy_backend.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e158e9b03f83f13bbdf8668ff066134b","path":"appflowy_backend-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98eaf420deb60cd2d5a76ad867f2138dae","path":"appflowy_backend-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9805ed6ca977fdc6df3bcf5eed84819eb9","path":"appflowy_backend-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983bb1fa0b0f13951c6fa653a914b3fa2c","path":"appflowy_backend-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986cd74f83369370fbe7e403ef8053e619","path":"appflowy_backend.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98403ef0361dbe02708d9acfefd0464e16","path":"appflowy_backend.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a1e14bed5ed1f1fb81e768bd14f444fe","name":"Support Files","path":"../../../../Pods/Target Support Files/appflowy_backend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9835d1d3e8ff3e04e32db8f29bce41a67d","name":"appflowy_backend","path":"../.symlinks/plugins/appflowy_backend/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98624a11fdf6f56961a285723dfd21440e","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ConnectivityPlusPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985824c2dcc3b51cb92cca4c0ee6b76711","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ConnectivityPlusPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e980ce7873e1e41d09d3e0e844e23814078","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ConnectivityProvider.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98880b04837ff2e7c369e7e0eb127f9146","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/PathMonitorConnectivityProvider.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e986c4b5d0d184e7bdaae9f3f567209b6a8","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/ReachabilityConnectivityProvider.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982d898374f337072929ea1137cd9b0531","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/Classes/SwiftConnectivityPlusPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a0e254b7ac4017377847f08d93859c98","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98403bc21ffba3f9ff7918b7c9d3ed9571","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f8845f64df2ea83ab8319d64b1bbadde","name":"connectivity_plus","path":"connectivity_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989eb115a9dec07b1f45a9c5d5afae3331","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9811c71b5edc42403c378790dde71cd61e","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988ba6e533cf0750afe861cf587836d63c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98337f70e0b3205d77252416fe9d53ade5","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d3ba4a1d262c8736c1a3dd2bbee95feb","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98935b95f888c988c238c3ba19998f568c","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9815214da764f00e2aace6e3402b97ff27","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c4748e70a6f1ff450d8ad89293faf1ad","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987ddf25614c5494e3e2e964d3362a7e13","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98652b1ddb86551418439feb3a3cde66c8","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98413da6f3e8f068df77eefa2354cc1260","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b18d54474e5f2acd3f7e93b85008ecaa","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ff9bc5085e73af685f00aee655d65cbd","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ec4569066db3377ae3957a0893989f6a","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e981911c6f4ab7da1f59fb48cce8d267a76","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/ios/connectivity_plus.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e982050d0252ea66aa742807a457832653a","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/connectivity_plus-5.0.2/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e986076caee38a9da764b3dfa1e4e1a5d41","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e986c1079ccb9ad2a0dd836bbde26dfebf0","path":"connectivity_plus.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b6ddb885fad9a4542a6329a470fa2fe4","path":"connectivity_plus-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98e592f2c2e44ef3deac3cd02784cc784d","path":"connectivity_plus-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d207fb51488c7ea7ac1275e911a94150","path":"connectivity_plus-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9898468587a6cab520cdcb8933d0b368ca","path":"connectivity_plus-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9818c5888400978b2fa22d952059454061","path":"connectivity_plus.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98d82fe1b7b4db7bc85ab18441f1e2c0ce","path":"connectivity_plus.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e984b570fd876e00e3c1622ee7a13633dbe","name":"Support Files","path":"../../../../Pods/Target Support Files/connectivity_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a5f07478f75bc3b752964b6b533c114d","name":"connectivity_plus","path":"../.symlinks/plugins/connectivity_plus/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ba19832f3a1fec7b3b56e3071bf4c9f7","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios/Classes/FPPDeviceInfoPlusPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98589f97f59a6ebf70206fa946d33c6a98","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios/Classes/FPPDeviceInfoPlusPlugin.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9830307b7eb9685258ae248d413d28a51f","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b1c115a5b698e82de643427263904f6e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b72e875a2313b2fd273ee6413267e0b7","name":"device_info_plus","path":"device_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a016968d1537de5fee216f39b9b13a4b","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984163ac08562702d9111e13bc58eadee3","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9863be3299f9535e1f37edeca566c56956","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ec71f83543e78b02bb8878f2bdaa1770","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983b57caee95bb0defd7fbea09e4630e95","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9866f39d80aa059eb566807578dd56d3e0","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d5774272732a1c387c19dcb20953a259","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9884b715eaf409b020791c09bd1a45ac40","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989064cf80f6cbf753a3f2f76769676869","name":"..","path":"..","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98b80404716ec61d84484617356e734544","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9853d1bd10368491358c120edf0879a1c1","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cd41f321f927f1501da3ce4c3e1705d3","name":"device_info_plus","path":"device_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c8a696a7575bdfd695b50552bda53e63","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986fad17d5ebb6a1b1e363579811099dac","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9896b7c1d7f56d6b335d559dde146c1187","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b7caa4a8157f551fd5157e38236212ff","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98eb0be9618fdb3c31fc76e6c391bb5a54","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d79e90f30e8071ea0755a235e7bbe47a","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f7e502581e9e563f0e716961fe919778","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ceb019bbffa37850d4328a3b5e80e961","name":"dev","path":"../dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d1e6ecf8a8f5e7866d4a3bf6a028da56","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9803607fd62a02c7946a1d3477b61e1422","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984cfdc50cf761e86a346d06e7bbf2d3a1","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e53458045fdd4d428512566882c92e7e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a82b79305f187eb48310a393f4bfe813","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e987f003eaf8659e7fff511891b3f2afc0d","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/ios/device_info_plus.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e983862a4e9dba3759d8615b149cba8a0dc","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/device_info_plus-10.1.2/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982a7f5deb6a64ae5e5a8c21df8be555e2","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9800ef6f27822088506da77df111adaf81","path":"device_info_plus.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d3729b6804108439237d0ad0b05ddce2","path":"device_info_plus-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e984decbb29f8ccfc95e475df5348ee3959","path":"device_info_plus-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98051abdaf3129a419e9bb3b8c7a83e64b","path":"device_info_plus-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e913211adfbc9d70bfc43b4439f4332c","path":"device_info_plus-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e984bb4748a26339d307020f1110f05e895","path":"device_info_plus.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e981f4d52f92d8b0f360ee8bef71882f85a","path":"device_info_plus.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f354426d7c8f682caf04a755240d2fde","path":"ResourceBundle-device_info_plus_privacy-device_info_plus-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98623cfe9c4e66d55a221902e7792e5d43","name":"Support Files","path":"../../../../Pods/Target Support Files/device_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ba853655f8a54c79510e0b66012fac89","name":"device_info_plus","path":"../.symlinks/plugins/device_info_plus/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e3b3f804d927f2bf6183213a182625c0","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileInfo.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e16f08255892d0f684af6ee4b4380824","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileInfo.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f56ed42bf21b1a52ac8fcb9a82336a65","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FilePickerPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988e93662f755da0279387c09ed20fcb67","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FilePickerPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982542f63e48192694c2301b53dd8da392","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileUtils.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987347d2eccffbba5b92a6737ac7c7f11f","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/FileUtils.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984b1a288da1e6fe981b46f8315944e21d","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/ImageUtils.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e04840a4cc59ee085ade0b92a852884e","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Classes/ImageUtils.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98ed38e9ec1e8cd7f7427a8e3752fa5961","name":"Classes","path":"Classes","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e9830eb81718ca8a4637d18322144cb97c2","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980cdda343b1e3945e28451531e9c1a605","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9845180a5dc5686acdc4e9429fa972713e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9836c9feed71b513f9919701e702f5ef43","name":"file_picker","path":"file_picker","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e5fd1b4108172e277e33c8a87d3fcd70","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9880d01b963c8ef608a0ad04583a5fc312","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9805619a0f02043e6fdb7a2be76189cfff","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9849b67c3d88226e7c72c75fa15e3bbdc0","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e1d9d867fa533872b45a7339a773d9b3","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b0187227de80dc084fabcf302004870b","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9825d0e9fb8adf9bdf05bf7db5c5fc456d","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9800d40be9558dad2dacf5f93047196827","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98abd6b198ade79f4872ee96e030a87e36","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9862b34fd8ad582ae69aae1fce3d396520","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ffed80c94d5fea9d76f4fe35d4478984","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9852117d814a38fe3eba010475bafd632a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e72e01936d9a14e0e9b869c88ad45424","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9875e49cbfeae37a120af486d9eb2d93c5","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98e67d477f82d32d5c7c9d30d2e81d066f","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/ios/file_picker.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e9808d1f81b0d4082e0417db2f18a2acfb6","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/file_picker-8.1.4/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98cbd123e02025168eb151b7d71e556d22","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98b0953817803e455c620122efd668eb5f","path":"file_picker.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9837a1507700518fa7f49a60aac4b7767c","path":"file_picker-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98d8e8a876a8ca0ce915464fbc27689c44","path":"file_picker-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98273092b724e2281270954671470d86c2","path":"file_picker-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98330b1e95388e708f5ad38ed7ce90e28c","path":"file_picker-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98893e38e9593d6a06c8522268bf23390a","path":"file_picker.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e981a76198b195c337f9587084c94a43437","path":"file_picker.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e982406cc43be2014b06d264ea164e97c61","path":"ResourceBundle-file_picker_ios_privacy-file_picker-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a98dabe6e24ef65809527fcab892dd86","name":"Support Files","path":"../../../../Pods/Target Support Files/file_picker","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f62c63ccec2525926d94e2db6b738061","name":"file_picker","path":"../.symlinks/plugins/file_picker/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982640e5da9d4da0b1c1284d679e8bfff4","path":"../../../../../../packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982c61e60ddf03cc04542492a7725f11f3","path":"../../../../../../packages/flowy_infra_ui/ios/Classes/FlowyInfraUIPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a2f3125bf6a20381c479cb67f97aa968","path":"../../../../../../packages/flowy_infra_ui/ios/Classes/SwiftFlowyInfraUIPlugin.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e984ba1576a81594fe8f17aea9519f8b1fd","path":"../../../../../../../packages/flowy_infra_ui/ios/Classes/Event/KeyboardEventHandler.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d5424bf8b4f526e22486e2a27f3268a5","name":"Event","path":"Event","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988d8845aeb0c1ed01cfe07a5f22cc0ec9","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9803f83e7a1d900c14ec7e6905f0092826","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bf611f0f52fc18d7226f7e7f09c38245","name":"flowy_infra_ui","path":"flowy_infra_ui","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981fb46775118dd702b7316a553c95e90e","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98616f2715911b1a74d97ddaada4344fbc","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984cd1c794c9b9ad19f2e4f0ca3f026819","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b37b705e2925a02d2062aaafe5099089","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982add91273b39db2a9cf969fe86dca06a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983b3dd8aad5aabbc2496c663812f03cfd","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d656d9598f5d14273b8b8981109e8b9e","name":"..","path":"../../../../../packages/flowy_infra_ui/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e989d6db7e7b482c3241d9a1acce4813ee8","path":"../../../../../packages/flowy_infra_ui/ios/flowy_infra_ui.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e989fd87faba1a0f13d4d7494191f208760","path":"../../../../../packages/flowy_infra_ui/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98145bf30753fca245e5d38777385d9388","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9893b48c5d4d9ce2656928a0ee3ff7944f","path":"flowy_infra_ui.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985398a052a839596d179b48db347a52c5","path":"flowy_infra_ui-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98c1829b249c76d5ac78b6563b1ee7fde3","path":"flowy_infra_ui-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985b5e2f476270884826aa113914031988","path":"flowy_infra_ui-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983e584fb8f2aede8db256844ee817b410","path":"flowy_infra_ui-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98892689e861aa6009551af6801e83c338","path":"flowy_infra_ui.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98f6af4fb94ad21206a16cbaf1e6453010","path":"flowy_infra_ui.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982cc4e6d82b8278ce278988c26691765e","name":"Support Files","path":"../../../../Pods/Target Support Files/flowy_infra_ui","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f43591bd9ec58ad67ffe4453fafc4a1a","name":"flowy_infra_ui","path":"../.symlinks/plugins/flowy_infra_ui/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98fb18cc16183813fcd8641d3cadfad33b","path":"Flutter.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d37e3d81bea52e1b4801db4886715c89","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9812cdcc535f32a8a76cc3d8e883d2013f","path":"Flutter.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98048531a74a78f7613c93ba91f15c7ef8","path":"Flutter.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98152d9dfbdddbef1340635c08e31152c6","name":"Support Files","path":"../Pods/Target Support Files/Flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988b26d3d01bc6aaf3315f6d51c851186d","name":"Flutter","path":"../Flutter","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b785d8b1f51c643229e4fdd18985825e","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Classes/FluttertoastPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9877147b007839a2216fd57593e0f3ba5f","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Classes/FluttertoastPlugin.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987efa3ca5a1d26929ed3b109a26806afa","name":"Classes","path":"Classes","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98792f001acf47168aaffa10afd959b5c4","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9812c06060cb56dec48c82c3b93bd2fdf2","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989a85c4c34db915b09efc04ad74e71906","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98879342f1ae53be318ccc851c8fb5f741","name":"fluttertoast","path":"fluttertoast","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986e74552a0b437b7d398a0e712dfea078","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cd6a60c3b14da76ce2370d2a8be6b094","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e29b7bd0bc91b1bd5e1a2b480473e1d7","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984dd71cdc0b7c0e396503163601d414ef","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9892ba1e9027f479ab7d1c9354ba3a2aee","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d4cad64b7e33d505b2ecf5d631c9d3e7","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987932e250d513fa720967e0dd955cb5c2","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9869aa178a8c06888c2f7f224a2918a492","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98316da1c1d27426250efb3febcdf6e95a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989a6847ce3a370c3b3d860bf1b58a643e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9898b42eaa404b9ed45fdaf7f308bef4aa","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b9d4812f1ec13b817bb1728f24b63ef3","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9850c5079f1f3a336ba56135604b1311cd","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980ef2416841219f34ad24eb0617d29faa","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e982ae0f7d0422d2f75f6cdde13de9e63ab","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/ios/fluttertoast.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e983d3e3470809b19b23bd82e0a81446281","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/fluttertoast-8.2.10/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d1afec4a601b8594f84cb4f864f71af4","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e983d17b3300e70f7d2c18f92e0d276131b","path":"fluttertoast.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981f77f9386b6ff408a5830dc5ec967554","path":"fluttertoast-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f000b535e938ba4dfcadc2d08b7f9dcb","path":"fluttertoast-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980af04a50d29871944a910af21b08eb54","path":"fluttertoast-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d7bc0cf610c976df935aac7605febe19","path":"fluttertoast-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e984e7d38849f0179ae896bfe33295f4bcb","path":"fluttertoast.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98bfeefd631cf811ef5ce08bdcc7b5e371","path":"fluttertoast.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98740356b6d05058396c4af19281498c88","path":"ResourceBundle-fluttertoast_privacy-fluttertoast-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98781d0486e386be35e6ed9b7fe0e393a9","name":"Support Files","path":"../../../../Pods/Target Support Files/fluttertoast","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988aa84318e78c044eca9d3adcb2083544","name":"fluttertoast","path":"../.symlinks/plugins/fluttertoast/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98d61d652dffc4f8948dc5e51433b55422","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e984a37fbaf4a9886ee0471de51a32ce11c","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98593629a4be85977669ea4c059a1d10a3","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f1b4186d564806fb6d451c19a178c28c","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d5d0ee78f468d5980cfa1ea4ad6dc829","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985041391a222e0b4847d8b2a02427be61","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9875530726d790ea59a3b2315529b92cc3","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989ea0a989d0c2a93f9b189cf1469cea87","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98391dbed2b1e45fe5718c27685e1d7c67","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986721fa987b7c82532939c240860d8345","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fb63c7bf2a24aed477a61c0970de0960","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b2bbb2e48ebc4b126f757c2703266204","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9817ce3811d81e530356a343c80efe8ad4","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9849f3c66c3da79f5b15d867daabb3187b","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d3fa3916bdb222eafea4d7820d6eb9e8","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cd7d949543e498a3b27db6a1893718cd","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e7820fbe6a5f106704f492a88059c536","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerImageUtil.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982369589aa7940b6e167faf588a3802a0","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerMetaDataUtil.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983f29585ff52e44e910a275777a9f20c5","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPhotoAssetUtil.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9859a809cba5c8a38df54a2381d8b2365c","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/FLTImagePickerPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98de28739697251ea419e62df80ffef732","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/FLTPHPickerSaveImageToPathOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98919c321c361ef3f5f9d9f7385fcfcbc8","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/messages.g.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9850445506038bd7f616867a4a123a3ac1","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios-umbrella.h","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9883e47689f14ae5fdf581d0fab5f920e0","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerImageUtil.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9831e620cfa600d0005b2000f1d54a69f4","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerMetaDataUtil.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987f45fcb71f8e934254d2b14ac634b539","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPhotoAssetUtil.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fe75cfd0b755ea439e9f1406676fbfcb","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d0c46c12ac4b3b637f4201b418d7f26f","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTImagePickerPlugin_Test.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9808b4daa14bfdb0a669f7e77d74ae3f17","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/FLTPHPickerSaveImageToPathOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985d17117714f9b0c4ea80392bb9bccf16","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/image_picker_ios/messages.g.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e982e942c8a9d3701363338a82b251071ac","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98502dd1df3bee9584d607bb1b1a902b26","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982056603c1983c7c0b15b4f9d4e839e1e","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9892285f318b670b27f7fbf2287b291843","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986a27670c0d2f4e5b9f45ade587b13d2c","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987b8f4a03e01f25f540c609ad50dc4076","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d97a722bba91079df06c440102468bba","name":"image_picker_ios","path":"image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98af8e2dbb1e8185946e7becdd47b97f9e","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989ae881df58521cd150fc286fffe35603","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9803a91fc4ede7e5b0c2afbd5fab18fee9","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981a39b88b7383525d9d473bd93d1e2f0b","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9811f7fa0b42b0c8fb654345ec4ebb8e66","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98623219eb0a2c578df94d5883bc419325","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981125f09590385ffd9ea5a9fe5aa60ba5","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d31ae9e961eabf22319ec6a1fca4f113","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9817eb7c23f31f300a1c251c4305fda8c1","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ec140a00b7550fc8d6fbc0aaceaa7a89","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bfbf2e646d2c92073ff2e83bebd9af2d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f2692e38d39bd674e71f6bb22b891e4a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9829706d777976177d0bf097f898b5b58f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985a01f741d7f1ce665f3e325d6c6a57f8","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98646c57f09cb01b0c762a0d93ca3f7b78","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98221a992b68c7cffb1a8eb5cbfbb297e9","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9853e18fbd58f9c37e8a3e2681a7b69feb","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios.podspec","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9804dde312afcdb12eedb33fc8bd45d59c","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/ios/image_picker_ios/Sources/image_picker_ios/include/ImagePickerPlugin.modulemap","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e988990923df59cdef8e5b1e19216f2a97a","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98aa0c16b2297382848b598f9592b0c3f0","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98a37a01b5033e0b3fe7a1dbb5d490ef35","path":"image_picker_ios.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fd29ed6fd89520fddcfbd7124ff888d8","path":"image_picker_ios-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e983d86dee1d916fb29b63d5b1bfcc7b5f9","path":"image_picker_ios-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9803f06581ec423d44280cf7868eea5e93","path":"image_picker_ios-prefix.pch","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9862b8fc1ccbc32fefecbae61cf5888907","path":"image_picker_ios.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e985530ede90a4a8c2c7507499317499697","path":"image_picker_ios.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98bd2861c7f94ca0a221c93e59dabe2757","path":"ResourceBundle-image_picker_ios_privacy-image_picker_ios-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9859b1bb772d76fea02e84d0016d099c3a","name":"Support Files","path":"../../../../Pods/Target Support Files/image_picker_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9830cadcc7a1263e98e67a0cf722e5d0ab","name":"image_picker_ios","path":"../.symlinks/plugins/image_picker_ios/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987800ae9687eb588b086e319ea6177659","path":"../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/FLTIntegrationTestRunner.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98edd6a3512846802c34d22c5afb07300b","path":"../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestIosTest.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980fa8dc3bbeb73b3d252e726a70557124","path":"../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980db9a591d712346350116e799f0e85b9","path":"../../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/include/FLTIntegrationTestRunner.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984a0d38d682beee02329dbf3bf514cce1","path":"../../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestIosTest.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ae12dfa62d0f539c9a3638bb6061375d","path":"../../../../../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources/integration_test/include/IntegrationTestPlugin.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9823e5a3226da96ed3914b1c8fcf559df9","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bde8b1c5794795e3b29ba817afae8d21","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b7f37a3e9ff7a757003a42461c25c365","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ede6ae88c67694e327de9c425a7f6132","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987f156cd8efcf8b450a0acccba337affe","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989dcf9316567af671febf7c01cbe9f506","name":"integration_test","path":"integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987cf80e4fed23235d2406a799ca33eeca","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9847a104d2bb397b64bd133d45453210d2","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989392c96daffc1a0e321f346049c20e7e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980176812f8f0fbbedf15c13f3c2ee1f26","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985aa0f0e528439e82d21295d31624797f","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98db8efb64140c28021c5072d8734e4699","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986ba3dab5252afe70f25f2838a01bdd4b","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b67e71ad7e85fc1ac5853ac3428427de","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985f23f974cdcdf9412df80483d5680940","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9873283974ea9ea8d983cfb681aeeee604","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98db229454a7e5041b4830de422e60e499","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989a2ced9ad917530dc3e50756255a1e21","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986b9379d3571941dbae7d47d880a7a296","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985055bbeb70e29da915730a29ed24173f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988070745f3c2f414eef05f58b72da6f5c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9863c8c730e09674801ad705a3ac5f0bb5","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989899d04f24c9ff95f1314d4fabeea094","name":"..","path":"../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e980c15437a062ac65671b286006afd1f2d","path":"../../../../../../../../../../fvm/versions/3.27.4/packages/integration_test/ios/integration_test.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d94ca862be564f152946859a75277d34","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98201d61b0d848a49c65589ceb7506e08d","path":"integration_test.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98af0a4e5755a22cb43543a81c374a2d51","path":"integration_test-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98a8522fb10db2dfa210c208f905afd9f3","path":"integration_test-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c0b24b129604eddd217c08a7d0c062db","path":"integration_test-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9896f6fa393191b60c6487f245272c94e3","path":"integration_test-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98105bcf1dafac8dc8d85eaff566c35643","path":"integration_test.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986a9f0cec960a86a1eea7dbdb4c49fcc4","path":"integration_test.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e981a27bfd3655d560ebee82c3ab7c104f3","name":"Support Files","path":"../../../../Pods/Target Support Files/integration_test","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98051dc09e6cf1b3ea30ba9f39d11035d5","name":"integration_test","path":"../.symlinks/plugins/integration_test/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a8ee44916529381498405522e751f8b0","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/ios/Classes/EngineContextPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984c462c42948edabe677ebac748da5d96","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/ios/Classes/EngineContextPlugin.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98fc9b7f6e025c71f553fe6444717223ed","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98db377227d8253ffe98324018634bc2ba","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986f2a88ed98a92034ce863ada1219ab5f","name":"irondash_engine_context","path":"irondash_engine_context","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98da61f0e74006e8fbd784bf4a3b970e5f","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98635b7210c5b62e20e037994cad1bf111","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c64150b3d0970cb99fece561f5a8799c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d3fdbea6d8cb166625df79788fcad443","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cbeb45be878891b685575f6b25a8528b","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9847ce44e75f1621ef4e0a6a5ae85f248d","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b2ae8d52765d0e5d9bea0d19cd7aebc9","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c84ed3eae59cea51b25d355ea7faec4c","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9899ac6782e67161e5aecf52fb2c668a4c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98710388c0277b6c02dc2bfce47465f2f8","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c5e6f44e93118427f696de099ce587e1","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c97afc961fd21222514666e42cbbfd8f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984e9cef67c7f53259a28a5a877e4a2131","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985b651d4fcaab066c39a7ebe7cff5daa9","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98b58b99914223d1b29b5e344e157a1987","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/ios/irondash_engine_context.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e9830fe1bed9eaa6250033f52ceaadf7d59","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/irondash_engine_context-0.5.4/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e986fa867be18f4a0a3be7766d74146209f","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98425d4148b72c406e4fd6672ecc362707","path":"irondash_engine_context.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d84855c0549724120cd6c3172b83cfa2","path":"irondash_engine_context-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98de048098637f376810dd3bb08c83895c","path":"irondash_engine_context-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b78febae09cf0c8f6fc7540345bee7a3","path":"irondash_engine_context-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9899f4dc54067ed61c6ad87cabdfa23817","path":"irondash_engine_context-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9859e866190126bed1c816e3db8cf733a3","path":"irondash_engine_context.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9837cfa75eec5f21f87364f26642dcde07","path":"irondash_engine_context.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9832c8cd0ed43920be52e135a0de0ef859","name":"Support Files","path":"../../../../Pods/Target Support Files/irondash_engine_context","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98abf194361c2fa0e4ef4dbec1a81fd4cf","name":"irondash_engine_context","path":"../.symlinks/plugins/irondash_engine_context/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e987c73d9371bc96dd532ba087fb5fe818f","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/ios/Classes/KeyboardHeightPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98bc635b84f7af43f9a693a5e68a4759e0","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f0eadcb14d473ce9c1a7c484bf311b2e","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cf80142a4ef1966c0c5c5b502225cf5a","name":"keyboard_height_plugin","path":"keyboard_height_plugin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989d420609dd708f812028e4f49f0e9d67","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9895cace1167e8f8a1209977ad38dfccfb","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98422a86793790ea0d327a52c60169ac60","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9830a1c8fc44b202d4661674a7fad9873c","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e85d16fef38a2779c13639b43f1e3726","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9842be4aab321e08d3762331c112720d13","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d8f5a70339a345d74b94ca7b959328bb","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986d3eca9fadfaabd2c455a69807986068","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d98f50f9676e73dee28b8bb61da9f8ff","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9807172992c96c01947c7f63e7875b1880","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982737307bd1600ca8956d8f22af636541","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98854bc6907e9bd1273078ea0de43aa17e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d53a1d0636736242ed1151c333b5fc9f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980c1dd450aea5e1194ba1fff003416312","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e980bfe355de52abf2bb6aedb754200dccc","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/ios/keyboard_height_plugin.podspec","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e985cbc6607d34630da0312e81033a0eac3","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/keyboard_height_plugin-0.1.5/LICENSE","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98fe47c46f6c2454354c85a993a081845d","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98619ee239792a4f0a0de0a4a0b0a159ed","path":"keyboard_height_plugin.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a419ee90c48f680450b1d3ead4ab82d1","path":"keyboard_height_plugin-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98b22dd52696b4067993e38f2551b63980","path":"keyboard_height_plugin-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ac24e13278278e052df0627e6f2cfe76","path":"keyboard_height_plugin-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988d785c658ed39de0a4c38be45dfbc1bb","path":"keyboard_height_plugin-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98b6b8fa71d08d5539059bc6a5868a679d","path":"keyboard_height_plugin.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e985de3091a8d3281d32c2891e218876b88","path":"keyboard_height_plugin.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98b6f518b56ca945c4e48ea2f80f2dde0e","name":"Support Files","path":"../../../../Pods/Target Support Files/keyboard_height_plugin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98de11b38cb40d0a466e962277af5c13cc","name":"keyboard_height_plugin","path":"../.symlinks/plugins/keyboard_height_plugin/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fa9794d4853209dd9b9dc8e446307a60","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios/Classes/OpenFilePlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fda7caf052d83850aa64439013c284eb","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios/Classes/OpenFilePlugin.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987c5f0895df45e85ab2470a8073a4c8f0","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98edc66a76af5791867252ee9a4fb21910","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9895e2c6870f605df8c86b47a953028581","name":"open_filex","path":"open_filex","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980fa51cd5f979e99f137ab48d9cfe538c","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989c915eac28a038451586120b5e42a4dd","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9844dffd7af5aba7efc4912e98f2c99fa8","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982963e1dbef544728f2fbcde398f7ce6b","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9820c60640726eda7bd6b2af35bd76df61","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bb93350f484f56f7d755b99459ba03a1","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e5b26b9b30e5f21dd0e61b63275d1293","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986605830136cc6fec7889ae13c84cbe94","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986b903bd75146f71f5101abe9a847888e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9835c6d6f45f08bbdd25355fae03109fc9","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989b22dd54825d4836f9aeb5366f4d1942","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fc1763d383faa8bc59b0d7f9aeef3f0a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bc4e9c0a74a0677415efe8c07067e305","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9898e553f7a823f6dc70a807a2f2d230a6","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98ee0863552468cb4b187a007dc6d9a9b2","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9852adb17f9ae796569014283b63994f43","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/open_filex-4.6.0/ios/open_filex.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9881839ee4b95080a8084a269cdb03d9cf","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e985fad692e094e1d0680167d0e9b810fb5","path":"open_filex.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9840ba9a41d5060da79af9271d16ff75b4","path":"open_filex-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98481c5fe7d76a4040e0ad917154c7e4d5","path":"open_filex-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983eb20923999fb0272a7d7981812ac419","path":"open_filex-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98045ac1ed0e248a91c7e040ef0eb573ac","path":"open_filex-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98d756e4a333f24f739d0261675ee1a4ba","path":"open_filex.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e989220364d0a7adb8651f9b9241f5c7291","path":"open_filex.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e984048ca38c41c417f8623767606347b2e","name":"Support Files","path":"../../../../Pods/Target Support Files/open_filex","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d744b5aadcc662d9087e0ff8ecfa7db1","name":"open_filex","path":"../.symlinks/plugins/open_filex/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981d03ed1c16bd8f4f97657768978ceefc","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus/Sources/package_info_plus/FPPPackageInfoPlusPlugin.m","sourceTree":"","type":"file"},{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e989c46f3ac5eacaf84d06c89a1618d018a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus/Sources/package_info_plus/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b8b9ba080e191e0b180722bf92e5f6d0","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus/Sources/package_info_plus/include/package_info_plus/FPPPackageInfoPlusPlugin.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98434c664522ec8a949437da93121dc4ea","name":"package_info_plus","path":"package_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98da4508512de7ed1cf62ba23f6e0a1782","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987977607c1ffbf525bd13f89c1d4d9af6","name":"package_info_plus","path":"package_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a38ddce212e7e062f995ed0c81943891","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98931f5d8098ad1aa7c93eec26372ba236","name":"package_info_plus","path":"package_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98285660b922695a353a0d34c11f224681","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9850cc5da0429ea6a6d2b70a2656fcab30","name":"package_info_plus","path":"package_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987ddc93fc8ade4d1b45b41d4e43f4567c","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98265c9ab7615abe783c697b6cd4393241","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d03678f78b9690ff2b539e1f76337f73","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980ad2375b9e2190a33ef70bfb739550e5","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b7ffa0b49f80dc8ef09e80c7b9ea10d9","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9870b15a53dd8bece6cba98fe4fe76a795","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c4af5d8efdfb0599410662c8d5fbef98","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9875f8bf110ce0646b5fb64007eacf9418","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c89cf11d4dd01c776076b9aba66f6de9","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9856bac1d729bbf1ed2a72d1315cebd361","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9891ba0e8cfe4277c3831c309656922ca3","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980531ebbd14b9b1db9cb595aee441e728","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989afbfb7501eabf645dfdd3b2d5371318","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987d5f78597ba1b1ca57b94d699cac9587","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e0fd6aabdc57252bbd2b8424bc907ff7","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9890c7045574c1805ae1374327a927bd10","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e987939da84758bc78ea493a94d1cea8578","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e981e6125d7e62187e31adc58e8a2ab9ea5","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/package_info_plus-8.1.3/ios/package_info_plus.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a53ea27f341ed23389e7d2e47e722d47","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e989676689969e25b7e2b922fdcc545264e","path":"package_info_plus.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985313e6a90d7b731193c46689110da675","path":"package_info_plus-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98ecaad6d841c680a103b7c06250152c10","path":"package_info_plus-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987e968239394f15ed507902e17a5a5d3b","path":"package_info_plus-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983b443c6a2f8b2d550ac2cdd9baa30e9c","path":"package_info_plus-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9833b0ea6cf67df0b17b6edee85d07e536","path":"package_info_plus.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98fe3643fe82f3241c56cd3e148f9bfcfb","path":"package_info_plus.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98e9bc7cd75e23035e788a0e7c25bf5266","path":"ResourceBundle-package_info_plus_privacy-package_info_plus-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98597a17b9183cd7dd4bf1dc3afc5c61ea","name":"Support Files","path":"../../../../Pods/Target Support Files/package_info_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98aacab2d91d3b0af7ec103ba0eb959971","name":"package_info_plus","path":"../.symlinks/plugins/package_info_plus/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98fa855961429fcf8f989cbc521f984b6b","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98b4c976140e1e1950199f10d209e74f5b","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98eacafab16506b203843ef40c684430b1","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98afc251afea8ca3c7c50e9df03700de10","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e09b0557ec981a89a5d3cdcc6842f0c7","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98848154feecc038860b30933f28fcd571","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e4c4edafb8828d8853578114f39e78ab","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988219086c27550c56b47393a9c0e39e4c","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e704029916b2a9af5f095363d987f0fe","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98357eba85aa36d189cd46406d27f5211c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987a6b9a2268f0d1806de5375f49c7ad34","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f47342e61ef54b290a2f053ba08d9dca","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d0b8c28e93e04c0badf7323d031eae30","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9837afaa61f9ce1c7f4fd458c2cf5adf98","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ca9305ea66f0600f4817de50cde8db74","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9808e63fd1bbc402ab215a50d1d16dcd32","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989546965c4112992540058bf89b75515b","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/messages.g.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985086f0886ccfd52bf38850cbdf060b2f","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources/path_provider_foundation/PathProviderPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f3577bef4fc38afcab726db64a001531","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982b0a750596bd71fbc8cfcf1a9c72ca42","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c1402de7d04e960dca8c1c9a29277190","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b0db58f72b87ea1465b9bbf9fa254cd1","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d11257d51b57c6c8f8403335be4b346e","name":"path_provider_foundation","path":"path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b2fe29e812c42cd3759a2d5cda315ac9","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9866a118eb431e0fc65900b0d1ed264fc9","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c5612f9fa3a0fea2a9cb1e98a04ed834","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987ba29a7b05ab240400ab764ea3778d63","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e6d60acafabcaba2c92afe3f89dccc00","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98957bcf5769cd6c1ddb7b1bacb4045c62","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9857a303219d5ef02905623f5642388e38","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d0517ff6d03f19c2b5d8f5ae77998f57","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c6054740313f5e60a651b62a3a9fddff","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bb43e5bcfafff3075b881ff2a9506820","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98952ef0fb2ed4fabd37dfc72628c7e3ba","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c173f0cea5f6ab214f5630fe12869f9b","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b3e481ae0756598ee53e01ba3ade5dfc","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9815a65875b9784344b15a908a00046200","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9827c3714a1d7d43b116f6f2b348cb54b5","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98600a8661b70b3bcc185abb7a4e9008f4","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98a8b04a5824c42b6f4f609567865faee6","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e981bdde8f32acfbdc05bfd1e4b808852a6","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/darwin/path_provider_foundation.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d9cf578a3f3bd7e82b6d5e62241a3287","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9861842fd411d053b57aced9948ca56491","path":"path_provider_foundation.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cc5a067b653131ca99fde3ba1debf062","path":"path_provider_foundation-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e989dd8857730e4c3db203f2e8d33299b7f","path":"path_provider_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980ba8eca3e056e4a5b65fa675f2756611","path":"path_provider_foundation-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a687ca6ea8584ff3e8089580a0210540","path":"path_provider_foundation-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e989a3ec1c60f383374577efaaaba58a590","path":"path_provider_foundation.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98c312fad1a73dfd52c70c16e3d25e37e6","path":"path_provider_foundation.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98e9d12e6cfc445534a2c16d64aa703cc3","path":"ResourceBundle-path_provider_foundation_privacy-path_provider_foundation-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f3324a2357137a370c65dc36aa758feb","name":"Support Files","path":"../../../../Pods/Target Support Files/path_provider_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986a2b2d2d35d31671cf8d3a967a7db258","name":"path_provider_foundation","path":"../.symlinks/plugins/path_provider_foundation/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e318c8a8d2c9dc308e66d48a374d44d9","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/PermissionHandlerEnums.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d3c436acf3f4f4e33e39114153074d49","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/PermissionHandlerPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987887890697cf58754f65bdc3473cc551","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/PermissionHandlerPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a74eea3ef4e2a60a3951ba0e47037853","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/PermissionManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f80ba72d7ab8de1bbe80c04c71d79ce6","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/PermissionManager.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988f16b65cffacfc53fcc954ce586fc713","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c853197e3241e98fb93603a21ed56089","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AppTrackingTransparencyPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ce0ef02ade9d0a1304458c836d16e93f","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AssistantPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98688a017d26dc611c7c96d1741b382970","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AssistantPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985e535aca2400f5f683c5e24da33fa945","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AudioVideoPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9822f423a57f83424ebc6bfefdee8b3991","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/AudioVideoPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98393b75ed0ceeda793874088622fd76fd","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/BackgroundRefreshStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98df055a47a4e10ed4075cbf80ce6ec767","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/BackgroundRefreshStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98df5b697e834c60412ac60dfaf5b92739","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/BluetoothPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b97a3c6c28463a93a507d1b992be86ea","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/BluetoothPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981d3465f51e1a6151315cb6853ef86eb1","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/ContactPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b85c4c9dd82c2d504573d0304be09cbe","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/ContactPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f6cc08933266f6af7683b3abe5dc3ace","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/CriticalAlertsPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e1614978d545aec846c24669228cfa1c","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/CriticalAlertsPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988634e5ad5b489cfb8621854418e2b38e","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/EventPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980b79f1643454a89c62ad82f48b83100d","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/EventPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98874ffa55cffa9b07dd0dc545368418e8","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/LocationPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d0f2795f5c06163cd56c66c1973fa5e6","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/LocationPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9820710d8f569064b9b4711ed685cbb429","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/MediaLibraryPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98922dbc3046dcd8b009793d4a2382c211","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/MediaLibraryPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985b7826319f99ae1623d37b8cf2f4eba5","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/NotificationPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9805e45e592399c0af6ed4e7318ba5d2c2","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/NotificationPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a0d58fb5a1923e677273beb359f58ab9","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/PermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b1b544d0a39e91ce76da89768ba82cb7","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/PhonePermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985eb1cc6a74764d8f7285312afe97653b","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/PhonePermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984e680efc7f3195ff924eec54756ed68e","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/PhotoPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b6c7b999fdb1f6499108be37ae374b63","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/PhotoPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987f8fe14bbd3ac1b56df04f30ce3befc7","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/SensorPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98af1cc35431393d9bafe812d1459bbdf1","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/SensorPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985c3d41a4498df42a0c6816c971e66b90","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/SpeechPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f22ec1e8227f9be641e5842788c56cf3","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/SpeechPermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986e866dd7a325c4b0ac10a86655c960f4","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/StoragePermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988cf99d004f1e5c25193675ec100372ac","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/StoragePermissionStrategy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9812e94ec5545f5483160eaed2e319fd2c","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/UnknownPermissionStrategy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984d86125607541fcdd295dcf2c90de152","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/strategies/UnknownPermissionStrategy.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98bb6db6682fe6132381afffdd0b60c949","name":"strategies","path":"strategies","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983970b54b6bc58e9c785411c2b48da98a","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/util/Codec.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9852af6ca8a2b5eb428a1585731297f789","path":"../../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Classes/util/Codec.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d5b3b31059ef8de5598d516260c77291","name":"util","path":"util","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b21a21f20e31c9dffd8a301b181e7499","name":"Classes","path":"Classes","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98adf4c31a2e50275af41b8a579edc68e7","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e989ac6ab6d3c4d5bfd6ee635e5bfaf86a2","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986adbf3486d93b28d124272c05d31c535","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9873343d816dbbcd1b184b6d68e52b8f8b","name":"permission_handler_apple","path":"permission_handler_apple","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e29a61315e2ef4299defb08c834d8658","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9813dc0e9e99378f2e9402361650d4f3c8","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e01ef4d5c3bed4f78810b1761f0bfd2b","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982268db3e1a4139e97ecd2061e336e5bb","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985a0dfbbd565a647af8ec44544f29a21d","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98656f043d890499eb834029f3c937d260","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98de17afe181ca6321b521909130e6f662","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986adad874bc346fed5ccaed27e453b741","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980ff2305908cc93c4a0725655e88bdc78","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9857ae62fcfbdc453c8d905748f5c3feee","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a7884dba25b1f761bcc93d0154f12971","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9828b593dc8b9a43bd8236b7d7a05a125d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9840d397c5e3d46385c36e81d47ddb4e1d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b5d7000c1c54eb9b83d1cdaabf7aa111","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98eedb8bb38abe4a753d3e5a4a50166ce0","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98bf01ad6eb9325fb5f924a7a939b9c3d5","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/ios/permission_handler_apple.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e981b1b5aec77e91493469b21f5697a5678","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9895519dd776dc6b6b6a0d5a99939759a6","path":"permission_handler_apple.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d3090640601ef537b4effff19f6956f9","path":"permission_handler_apple-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98693e4bbf87f0173ce83ad9dad9c4ec64","path":"permission_handler_apple-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98db4011adc2801eb0447df5d29a88d434","path":"permission_handler_apple-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9872ca1772cd655bc190d399786a965f06","path":"permission_handler_apple-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98f0f251835fe2b85ba8e20304af40d5bf","path":"permission_handler_apple.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98498a3b2ced2fb3f55d9af8975a87d769","path":"permission_handler_apple.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e986ea3ec8648859708ff7049b01e77d7e6","path":"ResourceBundle-permission_handler_apple_privacy-permission_handler_apple-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e981e5c39acd14afb0de574df88226e3506","name":"Support Files","path":"../../../../Pods/Target Support Files/permission_handler_apple","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98373a889c847b4c599507bdad83234fe7","name":"permission_handler_apple","path":"../.symlinks/plugins/permission_handler_apple/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9881b60c802c0c2009406bdc039b7f0068","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Classes/SaverGalleryPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98dcbc4e0c213bbd743d9703fa7e437dfc","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Classes/SaverGalleryPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98f7bbe1f859cede2224d42b729d4c355e","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Classes/SwiftSaverGalleryPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d5b49ba1d17a7857dc59f1175110cc13","name":"Classes","path":"Classes","sourceTree":"","type":"group"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98c89b00fc416882d2d0d7c9b090fa7c9c","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9820ce2023299a06e421e4a78afc6b0b82","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9865ece6bf5cc7c6cce51ccbe39d149b83","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98541499aef6d248a8301980d2821058d4","name":"saver_gallery","path":"saver_gallery","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9822ec8e7cdcda0404b791382d92111f01","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9864d2f6413ca98b754c7ee8a5171ad1fa","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98230a7213b882ddae22c49a7606136e55","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985321a2bb5473af9a5a2bc5bc4f834a04","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dc31a6400e820a903a3adf3e0472b97d","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986b76ca3af3e88ee3e30fcf7e21bc6d0e","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988541bb9a4c72550c30a45563b09a2e33","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e4d469fa76bd962f8bbbec14965af39f","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988ab99b3f5ad06e51078cf9e780a1d69a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98362dc58617351a4e100aac833e0a9775","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980dde8a02284f890277e1c2802444a83b","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9863783b60069d614fac80a368d82f00e3","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989d4a0878fd359b487c9cea0fa7016714","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980cda4027ee69c106bb442ab08ef4d82b","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98187e4b7080377e470ceccd8012e8f22b","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98f1b23c2ea102d15d2d3364c334ab1150","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/saver_gallery-4.0.1/ios/saver_gallery.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f1988bfa026ce78ad365f9598fb48ccf","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e980dce72be9e98a0e7938fea67d5dec3f0","path":"ResourceBundle-saver_gallery-saver_gallery-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98f8441040a6b86dbd9daae56dbd15f227","path":"saver_gallery.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98bc00fdad72f5f3771416bb7da4f843ac","path":"saver_gallery-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98b3812fe968f19a56dff3b7b21717a930","path":"saver_gallery-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9851858f7e87dff1cd2d14bcfec05bf9e7","path":"saver_gallery-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fdb913e186672d6dd019c490a01906ce","path":"saver_gallery-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986375a6a5aae83e2825b2fa84412a7b38","path":"saver_gallery.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986cfaff9f48d79d9aac38b1a4200ab7b7","path":"saver_gallery.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9870bd2de048ce790265ba57145fada872","name":"Support Files","path":"../../../../Pods/Target Support Files/saver_gallery","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e9f8053722083a22097cca53b68ac915","name":"saver_gallery","path":"../.symlinks/plugins/saver_gallery/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98988b8f30525a0740a56a45f26ec9e5ee","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutter.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b3f62b7cd428533f32ccbe502d30579a","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9826cd8809b1744007cf2cdc49b9ee183e","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9814197e8a8c3142283755e74bb1e1a542","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/Classes/SentryFlutterPluginApple.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98ed1bfbd13db96f1a5a9a509f62b23475","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988a2e2c7191f3342f69288dc5b6f24eac","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986b57f5aa6fe19896cfd444c1d7e3374b","name":"sentry_flutter","path":"sentry_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e95123df781f7246754488867eecad12","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980c8dbba9160b89547d42fa282bc4d833","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98408185297f1419b37849ee4ef61dd14a","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98db140395c21d264a6950ef09ad273630","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a317c3cc4f246a4d86dee21027be9f4b","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982c4e624eda92287d712d01e09bae857c","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988c938458c24c5a8a3de86b13a240257d","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9816b3e0cb5684faafbde3cd04104a8bf1","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982a1a4fb826ee2604d98a1124cda9655e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984814fee408387d1cb1ac9abf04007801","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98646b3421a4027d57df2335874d741e7a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d7d7eb47d8b13b56642d25993bad53b7","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9834206fc222aad06497489cefdfcadd01","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98472caaca9c22e04c9586a3fde6ae90f3","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e988942381687e6a83dfb7f7b51b7114eae","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98d7a35162a09b1bafe73fd4acf4dcd74e","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sentry_flutter-8.8.0/ios/sentry_flutter.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98c631af1f252c1a6185a574de8e401916","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98b2b1e766516fc1973912c9ed7d38fba5","path":"sentry_flutter.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b20710f0afc9a65fd11ac053ab5224c4","path":"sentry_flutter-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98fb88983eae4d4fb5041df897b5eb48b7","path":"sentry_flutter-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98140ec5a195fd3cd2e6672a247b3c50dc","path":"sentry_flutter-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980060be8e96d53611f0f35fc6b03e1144","path":"sentry_flutter-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98898ca6b789d6ebda968b7bf2c32c2927","path":"sentry_flutter.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98eb2509e05e4701c0e7caf39cb9f1a7ad","path":"sentry_flutter.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f6c519cd219bbe493b512e88f6691f63","name":"Support Files","path":"../../../../Pods/Target Support Files/sentry_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9890b2588f7d4f7005fbfcb636532e8266","name":"sentry_flutter","path":"../.symlinks/plugins/sentry_flutter/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fd63b0ee8c393f403cd165102963d89a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus/Sources/share_plus/FPPSharePlusPlugin.m","sourceTree":"","type":"file"},{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e988387a65c6b9b6cb60327ffbe6c59158a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus/Sources/share_plus/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cb436b045699de5c33251ecb6224be07","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus/Sources/share_plus/include/share_plus/FPPSharePlusPlugin.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e988aa5f3177cec22ad24112deb2bd5df88","name":"share_plus","path":"share_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989b5ccde2486e01b9f408c52039e15189","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fb60a3fc698011e0520a294091444220","name":"share_plus","path":"share_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c0adb9f898335bb561ebd6a92c8b5764","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987c61ae0ab682495e830ebfec4bb2d992","name":"share_plus","path":"share_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fec752fcf63dc774ef6492dfb4088824","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98061408887ffc6320474b1b3b9e56e869","name":"share_plus","path":"share_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981ca0473827b2355cd2af25d5d7d10627","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984fc5d20b622f4b201b585186291a8f67","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c1ceacb5959c41905f6d4367a79a62d4","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988d04b2310b73a2f1377c4bab4917fbb0","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9858dedb5230d6424239e49823b8eaf146","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a939846af6f55181341850124da10438","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987236a4c37717175b816032b6e16c35db","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98eefdcc530f2e92f8f7f62696466d831b","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e999dc81b190f4772b868a75138aa75e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989a1108d6d840a33f47b9c0fc3af3275d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988dae09aab63bdbe088b9c18b957886b4","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cdcb3d9a575e64374dc1a1db82409a54","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988352975c34e4d7ee8941ab48a3068611","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9864da257bade8dccae972a95edd8d4097","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981dc40745a0624f3b853ce39f5bd094cd","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98253b8bf66bd8ccf4c977852ef0a7714b","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e981e6f67f07605eeaa639fc9e31c57db8a","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98851d3db594f8658c9786f56478fbb6a5","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/share_plus-10.1.4/ios/share_plus.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98e42d81eea266b46708846d237e6b9514","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e986288ba151af560df873b930937f73b07","path":"ResourceBundle-share_plus_privacy-share_plus-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98f12cd395a2605f620e5facab18ef7617","path":"share_plus.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982db5430b3897df3342086c41d0ab0cc2","path":"share_plus-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98875a85a28fcc15b9fc0d81e86a96d256","path":"share_plus-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e15cc77379f3a5d2d644bb59a33c227a","path":"share_plus-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f3d67ef42ee4a797c3b450c5f684db3a","path":"share_plus-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9874f3142bf7e3eb1d60d86ef7b8277153","path":"share_plus.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e986e07cf0736987a294d8f475ef4545d77","path":"share_plus.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a569546e20b60ea744093ab0ba2e1905","name":"Support Files","path":"../../../../Pods/Target Support Files/share_plus","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988982bbfb1ebde9b684d127cb40ed59ae","name":"share_plus","path":"../.symlinks/plugins/share_plus/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98bbefd940fe64ae1bdba547246e327226","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e980424689f89b333dcef6b831656c09796","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d76e5d546741e7c1bff558bc1604fa29","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9833fd9e8a667e58c3eab9ad18ac331952","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a2f6b7afaf2afa6bd09f4e0f94068d4d","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fb40156ec424853fa943291f5b303d0f","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b1e3c36a262a1ed5ad3b185a0227eb67","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9831ab42991c30549603d3d8ca536224cf","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98508ae9e3aa5011f9d4942b2f89441d82","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985f334cf968485fb19584ceb692110236","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98138f3662de1a399453cb9534c664235e","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9826b71c8c69e21da06f07c82c6cf362d3","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9828e3c8782ecd686251f0f260cb28a2d7","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9806bae6027bc7a368204df9fc3614abdb","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e5fd31bd7e3de1ebf8947d9a8ac4c2ca","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d049289db0016cf5f53369d79d91af15","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98c588eadd95143f0918dce747cd1a1e44","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/messages.g.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985fee1a88b88d89bbd0b0617cbca27ded","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources/shared_preferences_foundation/SharedPreferencesPlugin.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98939dcc4abff9273e20bbabe5b4fd6244","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98642d561628a9b022c32e461ac6978d8d","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c2f343ab24d82d39b3b8df75288becb2","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988e8c1d85da5ff6697e97a0b62ee47e2a","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984be81f35c0b61917ec47fbad62d32af3","name":"shared_preferences_foundation","path":"shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9803adc645aa9991244006b1d25477fdab","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9830ff183ca4264558be2d7888f36dfe2d","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b46089f714cf1702ed485a5fc0b2746c","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dd0a1e90245c099ea267ffdb9fe7727f","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9806173dfed9c5271631519792136dcb97","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9896acf7fc50fc7d46cf4fd58ba1a01a8e","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9877cd8eaf1b44ad950ab2c1de574d545e","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a4a438f84238468ae34a00396733c267","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9826990e339d1e6bfb92c43f2df4021253","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9839d6e5e860b74845e418690de949729d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980437a510b83f078e975ac6e022596220","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985a9c0f13a0e24494a94657ac6f06a09e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f45a8957e29b9d8d573538c7c2a6660e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989bc0f1da02a3a34375b7c9fb790aa6de","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9873814c430069fbcad3cc2b2068a2f2c2","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982a9fea2f1146b6aa8643c58308630eb8","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e987d0661ac6c8236d9bcf05223d8b5321c","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e988319841cde2f015576fce1144b7c103b","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/darwin/shared_preferences_foundation.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98538b20e19bcb07bd154b1117ca2e7f62","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98bf7e5ed8e9ee51963dd29079b941aefc","path":"ResourceBundle-shared_preferences_foundation_privacy-shared_preferences_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98fe484d482b057065dd2bc365ef0b1cbd","path":"shared_preferences_foundation.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987cfa1a168d224a15e60944154b171acd","path":"shared_preferences_foundation-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e9882ef96f96deea78c69f4201bad0a0f36","path":"shared_preferences_foundation-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98321ebbc91006eb23c9f9908d066f0fb5","path":"shared_preferences_foundation-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988ce14ca997c1ee5ad8eed12f49b985ef","path":"shared_preferences_foundation-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9863033d3e663dd173908d510d04d9645f","path":"shared_preferences_foundation.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e983dff215e39548f9188a7b8c8d4c24c32","path":"shared_preferences_foundation.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9843a113f040e59beff6b2bad0d9500e2d","name":"Support Files","path":"../../../../Pods/Target Support Files/shared_preferences_foundation","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9815e66823fa48c84facba1c31c881a7f2","name":"shared_preferences_foundation","path":"../.symlinks/plugins/shared_preferences_foundation/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e989d8d7f21b58b6c6ea6cdbf54b4310500","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98ff6e5bd448ece42e55969cd7ee2dcd22","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986016d04cf905713b9d5b98ae9bb69fd9","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982743918f80430a5f6e3fb467f2d4e58a","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98db5cfaa6497e017576049d2d766fe43d","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9887c4ce35bf05c88dedb678a8a05f7ddb","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fbfd6a91559f348dcd214ab1cffe04a8","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a41b93d2bbcfa5fa03eef8097eb619a2","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9835810861c4e0c37a8ee85af809b7fae6","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e622054adbb0796c1efd972eb720d1ed","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982499ce39bf8d8ed82a87b79418b6050f","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984cfa4ad175bfd7a9f0e02542ff45ab26","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986069180160f76ffe39b94ecee9268997","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98adeb13adbc566e980f9d252124dcea3f","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9820d6e1cf31e5551a3d68c121e504d8d1","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985f4b97fdfbc2f7c5f93d9c36aaac6a3a","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d7c1e03fcf1c567ec11990def452273d","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteCursor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9853d850559207c4384f4df37b5e85a414","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteCursor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98aba81c10b979d23a5281707bb944aebd","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabase.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9868b319ba9a799ba0a2bc5271ebda13ef","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabase.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ca5fc8019efea2c0a2df87621e6bfd20","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabaseAdditions.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98eaab2d07055d05ca402daaf728b22722","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabaseAdditions.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f164259be96c4b6bb6608f7ce49cf3de","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabaseQueue.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985ad6eba05b61ecb94890b1ef3012502a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDatabaseQueue.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982b2273f60e8670b2f5db571354e1ea07","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinDB.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98463be45d0e22ba2b4d01b4ebe090555a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinImport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9872cbb441a98d645c1ab53aa8198b5a91","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinResultSet.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980a984e700108dd111790049ae08ac1e0","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDarwinResultSet.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984fee7f0226744b7cccabb95aa1a2a58d","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDatabase.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9813f225fcdcd4540ad3a526d2488cee7d","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteDatabase.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987dca200d5c95cb57fe2d64a729f10e59","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteImport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9870809aee28a31c0ce2cfe6427a7a84ca","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a82a52d3958fd262c4d9754ada0841a1","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqfliteOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98174fc872150c14b5ae3ef8ed6f674bac","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqflitePlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fe7eed89bf2c775aa63c02aebf8e6b33","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/SqflitePlugin.m","sourceTree":"","type":"file"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c822bb9bb66315e186526fd41e667547","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/include/sqflite_darwin/SqfliteImportPublic.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d179bdefff96b31664c33767a93f03d1","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources/sqflite_darwin/include/sqflite_darwin/SqflitePluginPublic.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9892e5814da7ab09018c72d77ff35a15cf","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a8b633bfa4915a0040bf5c00fcd27ba1","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b7b37f7cc0762d9ca0708a155f22b8eb","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989eba8369512e5541562107b2d8b760a0","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e33f20f0d475d4d5413f81e2e96ca687","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987eef1cebbe3bb46644378bc4631fe02d","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ed219e0fa4be3681e4e695b0532eae32","name":"sqflite_darwin","path":"sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988972fd127184a48f0dc3e6aee302404e","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9877fca5587d318fba37442797f6f705b8","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983ae2fa1946bb36f5e91787fb46d589c5","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984f9f6b5fd902d714436e433e3f764dd9","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dd7c7199347a79274529e6303ba01ef7","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ca3a121f05226c04281b755cc833b917","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9893f425a7aea6b4482e64d0a5bc657629","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e49b35d1a40e4dde1b2357ef1cb8d669","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987e5e76ea8d51c8b8d79c16a34746f33e","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9854e643717fa41262edb3c3f1d725d61a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ed6f9da6b7cdf2cb6886e30e6cdfbc8c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98de4bc8577c1a9bea6d84ede9bedadcbb","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987d9613248b511bc9325f7bcd3f3e4dd2","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988f60c32fce1cfc81f7648e0319f50cb0","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9897a924c3a55a4bbf49fa6473d66861c6","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c43b6316619d3de45f006103a22d7931","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e981d8665643615515f27809c8e1ce6963e","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/LICENSE","sourceTree":"","type":"file"},{"fileType":"net.daringfireball.markdown","guid":"bfdfe7dc352907fc980b868725387e9852fdd6f48c5deae81f51387413e1e002","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/README.md","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98856a830972b607e532cd4373e4bd4903","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.1+1/darwin/sqflite_darwin.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98fe65523c54c78b02ecd1fb77f3e46537","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f79a082bb47738f827607307a0e2452a","path":"ResourceBundle-sqflite_darwin_privacy-sqflite_darwin-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98c804c2a12db2f8031b9549edefec60e1","path":"sqflite_darwin.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987dd296579fd63723f2385fc3e309a485","path":"sqflite_darwin-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98dd8f3f1e4afb05338b44768a2eea883a","path":"sqflite_darwin-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9839943824b9f6edc4e040c5c8b2151f82","path":"sqflite_darwin-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9867c38360f7a8c57f89677b1b8dd0f63b","path":"sqflite_darwin-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e983c5c591d1e2590f943bcf841c7250c29","path":"sqflite_darwin.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ecc2b7f452e75bd86ca68cc787ffd0e6","path":"sqflite_darwin.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9863d40e6f29652cdc2796922cd0db2932","name":"Support Files","path":"../../../../Pods/Target Support Files/sqflite_darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984e01895d459ef3578b9180bd22234ef2","name":"sqflite_darwin","path":"../.symlinks/plugins/sqflite_darwin/darwin","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ddf54a4994459d8d533e977b9d3701b4","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/ios/Classes/SuperNativeExtensionsPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98227f3bf9a2096f16c498e39e527dfd82","path":"../../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/ios/Classes/SuperNativeExtensionsPlugin.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9812ef15f301a3279be9158ecc5e2ee578","name":"Classes","path":"Classes","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9821ac568b46c2e99c638c4468b42476d3","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98288b3abd52cc5524befa714a293f1d78","name":"super_native_extensions","path":"super_native_extensions","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9847c7f918c61997a2cee2375876483e92","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98038dcce1516b7703d28d4d5925b0e167","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e27492f09959f12010d60e1537d0abd4","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989b82d7b34f027e37120d28d733c9bc63","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984e2d0c4c003b18b59d642ab053cd2e85","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98dac882f2bd6878262999058971045bb1","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98906dd99ca335afe3dd9a7c6cc05a2048","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9895327431cbeda356cd4093b0b03dde53","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98b3779ad14c3af16a3d17c6cb4a8af745","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98413ff013f5792a21699c27aa303d4851","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98503a720642223ab224d97e9c5f9bbe9d","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9823726b2431178718aa367a59ee6a520c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f023da7cab226ce319043a4e226d36dd","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e550084af2ca7acaf14461ed1f151852","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/ios","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e988cb8e2ab455bb20dc19f7a3a2246df30","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9896d5871a48fd625e7d723f390e6a98b6","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/super_native_extensions-0.8.24/ios/super_native_extensions.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98eeb382127790a1793c33c9b12ba2e097","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98a18f45e0ed141c453cff3a3c4527ecbd","path":"super_native_extensions.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985c10bd2540461e46ce7f11028e018d75","path":"super_native_extensions-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98fb6de20b66e6b8dbd451f64bf906989c","path":"super_native_extensions-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fdcda392f627466e5edccd1508970413","path":"super_native_extensions-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c698b70fb576aa4fe26ace0b9b8afcdd","path":"super_native_extensions-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ecc61835f7006374d2df8ec0b73025d7","path":"super_native_extensions.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98960e413e1bdfa694f48dc871e7ef827a","path":"super_native_extensions.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f219566d0916d6a77685c33cd16653c8","name":"Support Files","path":"../../../../Pods/Target Support Files/super_native_extensions","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f569f35312eec5d67d042173116f99c8","name":"super_native_extensions","path":"../.symlinks/plugins/super_native_extensions/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e988380eb098928f2df260922e9d961ad9b","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources/url_launcher_ios/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98247df5322ea5db3a84214881c52ee25d","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9887c8fcbc6272382fe1137437a8cfd2aa","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987ab2c88ca66fa6efb7003fcd0a5fecbd","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98aca3ef13270c3be3fdb24dff41e0219a","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988826612973646a4053217e2324d0644f","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980d552426f9b9a7580f9c3386dc024b81","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987cd73363ca2db27832a5adec82341972","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fefce717b106b030e9dc126dd095e251","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9899ed1fd19213f07006bd2b9430d7772b","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9863db1d0c1fc2b855158b6fa51dde21fd","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988e2efb7aff436f31626c71ead15daeef","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c34f07f7b163887aa57af2d6c5172187","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f1d44c68f08f6c906df9811d7caf2ef3","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9832553b1b2fc5c507feabcb35c959e868","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984fb77cebbaaabf4e807dd6ebc12b73f9","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98490bb9f80fa5964176076d4f1e6394fa","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources/url_launcher_ios/Launcher.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98daf626000fc0d232b897ba1dda9c0f31","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources/url_launcher_ios/messages.g.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985162a45777def5ce41cdd4f46ff104a9","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources/url_launcher_ios/URLLauncherPlugin.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e983b3ce4248a360b7aac31c20aa78daf2e","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources/url_launcher_ios/URLLaunchSession.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987ea293b0366abc71ebb96acf17297ef7","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9888d93807e2dc1f667f941643f134e44c","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981798185d018f93fe0298b8aba5d63560","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981019612be8bea02a74431d38ce3b9cbd","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9880eb5ac131bdc80edb26f033f8a72fce","name":"url_launcher_ios","path":"url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9851161dc629e1fb4d2d3fd8bd10ee4ca2","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989fc545117a2c4837792115a214e84a3e","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98cae5b473355e5f3f56b441bc383d1a87","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98025d2d7ef7d681723bfdf777175c2ffd","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9834b9faca7fd3324a1c2164a4cf7e5313","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988303e4a8f1f82c7fb4ea2702bb5cb5dd","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984516cc538c760100f78348eb10326053","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98732e82463b828bce4818d57c1ea975ab","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9880fbcdef07ddfda2f3153be29f969c0f","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981e967b61253c26da24779f0e1571787a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983de9bb36b851937d715c96cb7d26dc7a","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98611614762b8213684ec434869762ea22","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988cb0470925262a8de3be403c8ae31ff0","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9833562037aa4c75db5121f347c9e53091","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e70d73185437e27f145504e3dc9f1033","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98ed5927b53a4d1ccd1f9684043eea4526","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98a5500bec173c13a85e52bd5fff791d8f","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e9856b8b14381dcf0263199d3e9429dbbba","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.2/ios/url_launcher_ios.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98cceb9263cc3f6dd5b9212b964cfa7054","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98604688695bcf046b591ad88eaebd26fc","path":"ResourceBundle-url_launcher_ios_privacy-url_launcher_ios-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e987be471ca7fbb33b50a518eea5dcb87e8","path":"url_launcher_ios.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9871fa081d7f580f4936897eb46aae0878","path":"url_launcher_ios-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e986f18d2a83f59c045caefbc68524cb3d3","path":"url_launcher_ios-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ad745689d7ee76ea50404b77fa12b6f0","path":"url_launcher_ios-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98eec495e5daf9423acf950402315f3522","path":"url_launcher_ios-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98b3b3d080e11ff54026ba894393f62b3a","path":"url_launcher_ios.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e980bab71536c4bda286b310a48eedac214","path":"url_launcher_ios.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98938e093d7e3b952e819ce9ac1448e73b","name":"Support Files","path":"../../../../Pods/Target Support Files/url_launcher_ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9833186a359a29a050b940ca2dbdf952b9","name":"url_launcher_ios","path":"../.symlinks/plugins/url_launcher_ios/ios","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98273c41ad57101f59fb22ee04a3de8ec9","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9877cdee8b856454ff8e4213d5b4b8e81e","name":"Resources","path":"Resources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982e8fed289e7a93127c4e71b0a59be8c1","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989c9e91c05d6fcd60cecbf709fcfae9f8","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e5bb761b833aa1c82ed6c79ade71f8d2","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f9d8e7c800ff72bd4f22b25217d72bd2","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98c7188e90eaef7902fa09bbd3b6c34138","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980d69590ab194d9defd371868c962c163","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987e1eeba79f29d46cbd63b8f82bc2f113","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982e575334bd02bdf114450e05cddb31e6","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987403e6bff419acd09288ab9193a47d46","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983974551ef8940d0a77ce7e5dba345c15","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984bffff6dbf90752f811d1b628435611f","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980016f1720faf508de4c79265c93531f1","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985409f0ee94bca73500af103a1f36ec69","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98863ea06f124e08ce56a9dc6093f14a5e","name":"..","path":".","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"children":[{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9898207ddf6fb048e55cbc82f1767304ad","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FLTWebViewFlutterPlugin.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c78e7356e2edc959fdb6dcf759d9ea32","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFDataConverters.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984555345bd3de9a732d0775aeaa250a8a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFGeneratedWebKitApis.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98dbff9619978e59ca5bd2bd9b78df76e8","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFHTTPCookieStoreHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983ed9e6db451958c4b973973bda4af232","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFInstanceManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9897e653c33b1ca27c845f24161d5df94b","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFNavigationDelegateHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d512e4282ea1aaea1f31ec22635e6a73","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFObjectHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9863ec51aa8f06ccdd336b2b840124b5bd","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFPreferencesHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9852d8f233325638788b9c883bef4c434e","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFScriptMessageHandlerHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9839386e23f7ec2e92ef671f9a65fe9467","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFScrollViewDelegateHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98554327c6feb943c123568d30776d18b5","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFScrollViewHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988abd8337fa8780181224b1c59e11080a","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFUIDelegateHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98bbd5fcab06bd2b4eb52aea88b514bbf5","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFUIViewHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d8f2fa4ff0132b374c1eca3de40fe4ce","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFURLAuthenticationChallengeHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cdfda639596827302a7f54cea81404f7","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFURLCredentialHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982d6b13259c9952197ed43ec8ae880592","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFURLHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984f11e46b828b92865af10c3b4c4c9d68","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFURLProtectionSpaceHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984b54bebd3894ad50c5db03675b718387","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFUserContentControllerHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e0160a9358f35f9d1c0b5a60a3d0f682","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFWebsiteDataStoreHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b6eb829538253c8fd04f8ad89b11abf3","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFWebViewConfigurationHostApi.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a1bb5ceebb3ce6eafef856cbadc7f5d2","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFWebViewFlutterWKWebViewExternalAPI.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a83be649d9b3f6edd971f560f1bdc837","path":"../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/FWFWebViewHostApi.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98dfb923094c09a8b2984e29f4ed12750a","path":"../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview-umbrella.h","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9854b0c1f62864b38122a6cd2adf415fc8","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FLTWebViewFlutterPlugin.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98dba23cc94d8fa0e09dc855fe286cf011","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFDataConverters.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ba40413987e8fe9a668fab4d2f62e968","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFGeneratedWebKitApis.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b41a55f1ff6a49c7cb1bfc6754b0048a","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFHTTPCookieStoreHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98aa71e1ea8eea7861996ce4eea6fa83c7","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFInstanceManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982f56450de8c5ce26b62883f42bd50aff","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFInstanceManager_Test.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bdc5d2d37bbc00acdd9d5e37557c61aa","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFNavigationDelegateHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9876af14ebe27607bf4d780619a0507e5a","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFObjectHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9812c7ccf1eea1c9c80d378418ff91f3ef","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFPreferencesHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ad06b47c00b9bddb782d0454f9545c23","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFScriptMessageHandlerHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989c04773599c925e49e8e9df79338c2cb","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFScrollViewDelegateHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982c3bc52c27128b5552e2f492ee8e9d38","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFScrollViewHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983638c2d3e148c4aff7f56dae69d5bd44","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFUIDelegateHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9852648a53c3238ae232bc1cf732368733","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFUIViewHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983ac2710f7f8419957a975656f4f11010","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFURLAuthenticationChallengeHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f29772b4f240416b82bfca4c7240cc52","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFURLCredentialHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98287942d628c319dfb58242b7a056e501","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFURLHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984b2e4dc50da302b519989286271f986f","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFURLProtectionSpaceHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da68ab6d947fd76c2248fff2397f9474","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFUserContentControllerHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98db1f999a296f5d4b787f4f9b4b82b565","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFWebsiteDataStoreHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98635d8b53208d8a44fe1b27f33c27ad74","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFWebViewConfigurationHostApi.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98de08c86939fbb40b81087f28b271e5b5","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFWebViewFlutterWKWebViewExternalAPI.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98984ca0249b9df8b5209a6dcb1c869dfc","path":"../../../../../../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/webview_flutter_wkwebview/FWFWebViewHostApi.h","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d786586788316e81a204e06a860d9b2d","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986d305bdf63057a110d9a04a946a6a0b3","name":"include","path":"include","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e985af659c04f6cdf4a7c73cb8559d95a55","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98e1e70587e6aba44c0bf6b4fa49bed627","name":"Sources","path":"Sources","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e983b00ae6fdbc30a34dfacc53e5e678acb","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9865432e0f8cd5ef04acb4722323bfaef7","name":"darwin","path":"darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981fe60fe58df7074f3f027dfb308c785b","name":"webview_flutter_wkwebview","path":"webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98299b41c4840e81d29fd9defe63f562c1","name":"plugins","path":"plugins","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98a1a72cddbe31b7904461fb30eabe3f41","name":".symlinks","path":".symlinks","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9866cb0dd0dbb6d8ae7c42bf7e70385636","name":"ios","path":"ios","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982c9395928d2b28bc4832e7eaf6338061","name":"appflowy_flutter","path":"appflowy_flutter","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981cf382f136f655ee4ef53b31836e9976","name":"frontend","path":"frontend","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982695540da12cb3688e9cd89de55b4c12","name":"AppFlowy","path":"AppFlowy","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989cea8ab7b2bc9aafea94b071fcb36fe9","name":"samples","path":"samples","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987cb1234016d9a45a8e6ebfa24a1ca6b6","name":"dev","path":"dev","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e981ba95ad9f21e528ba007f114552e8faa","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989c9004908f380362c00015c90fe116d5","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fe5984c239273e4752feee9c1cd0d417","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98eb7ac77a35fceda15f40d2e6dfe455e1","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98481664ecd25a98fbf478061e4214182c","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984d34857cedf24e0ffba417d4ddb7a5a7","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98f104ab4138053278b0c742e5da3a37b6","name":"..","path":"..","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986ba8636d82c00ef31adc8e2a02c78383","name":"..","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98e4b951171365bdf1469e58e30bb095be","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview/Sources/webview_flutter_wkwebview/include/FlutterWebView.modulemap","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e98f15216db275bd0a141b390a80247ad56","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/LICENSE","sourceTree":"","type":"file"},{"fileType":"text.script.ruby","guid":"bfdfe7dc352907fc980b868725387e98319f1ecfeb379cf849cfb0c9a15fbdf7","path":"../../../../../../../../../../.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.17.0/darwin/webview_flutter_wkwebview.podspec","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98d1afcef86fa9f9ff1253aefecce2db52","name":"Pod","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e980dc040649dc9e2eab53c3da3c40c35b0","path":"ResourceBundle-webview_flutter_wkwebview_privacy-webview_flutter_wkwebview-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e989d9a8732910c4132ba5e9ee6c49a6fd0","path":"webview_flutter_wkwebview.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985debbc2cab2d792fa92242781697aa86","path":"webview_flutter_wkwebview-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f10ce8a5ece3a15020d640ce3ed6466e","path":"webview_flutter_wkwebview-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9818b0186c11c004e985a36edb09183861","path":"webview_flutter_wkwebview-prefix.pch","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98e3c8f3c679654f0d8322c0bfe86cb48c","path":"webview_flutter_wkwebview.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98de306c3f6c2c14f312be69bcd973767d","path":"webview_flutter_wkwebview.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98fb0231873e76e3fdd25c08c4f7142e58","name":"Support Files","path":"../../../../Pods/Target Support Files/webview_flutter_wkwebview","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98daffe5a2e168f85d5660f57f2cc4f3d1","name":"webview_flutter_wkwebview","path":"../.symlinks/plugins/webview_flutter_wkwebview/darwin","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e9880465010b585dd5bc5b7af0a27d58067","name":"Development Pods","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e984bcd4feee9e1dfb73f639f9bac23b2e2","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/AVFoundation.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e987724d547599f4408ee3b3d56fa903a20","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/AVKit.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e98c18474134a48ed556f51c824bfca3246","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CoreTelephony.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e9885770c7fb25aa0db0cfd09c5443f9797","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e9802c3ee0032b21aafbeccc5b7db734fb2","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/ImageIO.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e984497b71acede5531fd260c9ac8b3d9e1","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Photos.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e98d671fdb5a7bd1c3267d3e6f35907cbca","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/QuartzCore.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e9872b55968a8ae9b74a90d6bfa9882dd5a","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/SystemConfiguration.framework","sourceTree":"DEVELOPER_DIR","type":"file"},{"fileType":"wrapper.framework","guid":"bfdfe7dc352907fc980b868725387e98d159ebe55fadf57df5691badabf215cd","path":"Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/UIKit.framework","sourceTree":"DEVELOPER_DIR","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9827102c11d594e53de5adfd4435cd953d","name":"iOS","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986f57df5597ed36b645cb934c885be56d","name":"Frameworks","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e980f256812a9858de27fb60bd1accdd32c","path":"Sources/DKImagePickerController/View/Cell/DKAssetGroupCellItemProtocol.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9837ba02f2eab7de1730b72c879d53f332","path":"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailBaseCell.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981791c992ccf9564106a690e1d96420b8","path":"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailCameraCell.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981149a1d26875e8b68800278ef1ef0dd2","path":"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailImageCell.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98c8875336f053558b1f9ed7c43e262ddd","path":"Sources/DKImagePickerController/View/DKAssetGroupDetailVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98164873936b2ccd15bdc608cc60c2cc65","path":"Sources/DKImagePickerController/View/Cell/DKAssetGroupDetailVideoCell.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a7cecd4700762c7a109513bfb52989bb","path":"Sources/DKImagePickerController/View/DKAssetGroupGridLayout.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989c224ca64eb47c09a922a3d927cf7f10","path":"Sources/DKImagePickerController/View/DKAssetGroupListVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e986c20dce043f12383acfeb98f96470e02","path":"Sources/DKImagePickerController/DKImageAssetExporter.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9855672fea159780d4dd23a1f5a30a7837","path":"Sources/DKImagePickerController/DKImageExtensionController.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98261f313efe36b40a087a9d35206c9607","path":"Sources/DKImagePickerController/DKImagePickerController.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982975067e23a41db0b8f6979b4a4a3b55","path":"Sources/DKImagePickerController/DKImagePickerControllerBaseUIDelegate.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98d331501c9a3bbd1cec21da6d1bed6a79","path":"Sources/DKImagePickerController/View/DKPermissionView.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9851006020f4c7fc277370bdaceee539fd","path":"Sources/DKImagePickerController/DKPopoverViewController.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98f1abbceda566cfe605dbb72a81ac8f8e","name":"Core","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e10cad38a7b8b791b5523321d2112ef0","path":"Sources/DKImageDataManager/Model/DKAsset.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985cdfbee7f0fa63af790094c590a82f83","path":"Sources/DKImageDataManager/Model/DKAsset+Export.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982173071859389679a6c45171b90987da","path":"Sources/DKImageDataManager/Model/DKAsset+Fetch.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a89a84fdf4f961d13d26db8520129232","path":"Sources/DKImageDataManager/Model/DKAssetGroup.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e418cf226491ab81be949451bec00d12","path":"Sources/DKImageDataManager/DKImageBaseManager.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9878488e740dd474df94f5eec1f4ebdef4","path":"Sources/DKImageDataManager/DKImageDataManager.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e01ac6181d083bc327440f955529b52b","path":"Sources/DKImageDataManager/DKImageGroupDataManager.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e981e366accc57fcc9cc1a70ecd199c50e7","name":"ImageDataManager","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982a05c3bf490f4a4639be6133ce46fc50","path":"Sources/Extensions/DKImageExtensionGallery.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9878b271eaafac7e3a83dd4dcb67d7b63e","name":"PhotoGallery","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9892de1f95603b3309408b855b150222e8","path":"Sources/DKImagePickerController/Resource/DKImagePickerControllerResource.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e982cb145326ee0fe9b023de780deed78c8","path":"Sources/DKImagePickerController/Resource/Resources/ar.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98aa1b813ba9ab60abaff79984557a95ae","path":"Sources/DKImagePickerController/Resource/Resources/Base.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e980c9106c6deeaa489bece52872ddc8302","path":"Sources/DKImagePickerController/Resource/Resources/da.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98638a8ead3c4921071ce058b31e0789b1","path":"Sources/DKImagePickerController/Resource/Resources/de.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e9808c6c81d6544958ee5022314e9257bea","path":"Sources/DKImagePickerController/Resource/Resources/en.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e989d1190414344f8164f52c639a0b3923e","path":"Sources/DKImagePickerController/Resource/Resources/es.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98a3612de5bfe6103968b0ad0e24321c06","path":"Sources/DKImagePickerController/Resource/Resources/fr.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e9840393198352d2a493eca64a5e8366781","path":"Sources/DKImagePickerController/Resource/Resources/hu.lproj","sourceTree":"","type":"file"},{"fileType":"folder.assetcatalog","guid":"bfdfe7dc352907fc980b868725387e9866b7b2a063b357116fff6128faab6010","path":"Sources/DKImagePickerController/Resource/Resources/Images.xcassets","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98389b54d175b1ebff57cec1e50f863fcc","path":"Sources/DKImagePickerController/Resource/Resources/it.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98653c7ebf4d1cd10720ef59b098ac5602","path":"Sources/DKImagePickerController/Resource/Resources/ja.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98104b68b4ff0a25a5296d0b83fcc56453","path":"Sources/DKImagePickerController/Resource/Resources/ko.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98d31edcc28dfbbe3d8ce637b5afc30729","path":"Sources/DKImagePickerController/Resource/Resources/nb-NO.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e984482abef551eb7f8c2328ba24695640c","path":"Sources/DKImagePickerController/Resource/Resources/nl.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e987d2482c68c58caea8c964ddc88375bce","path":"Sources/DKImagePickerController/Resource/Resources/pt_BR.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98e82421c9939f56bd2bac60c90c0972a4","path":"Sources/DKImagePickerController/Resource/Resources/ru.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98641e96c7b9979000ba15756d8c4088c5","path":"Sources/DKImagePickerController/Resource/Resources/tr.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e987dbf20be783e65771c0b6b10146e0526","path":"Sources/DKImagePickerController/Resource/Resources/ur.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98496e8767ebc06f02f3a190467b76c669","path":"Sources/DKImagePickerController/Resource/Resources/vi.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e9851d4a9e151fbaba3b43b0c1c328e7692","path":"Sources/DKImagePickerController/Resource/Resources/zh-Hans.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e989af334eda3f4b74b226c99b858e3a8ee","path":"Sources/DKImagePickerController/Resource/Resources/zh-Hant.lproj","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9804dfbef9d9e61df9bb559e2872b5da24","name":"Resources","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d02568085c3a0b85378f96f4eb289f1a","name":"Resource","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e985e388c776c1b334623438d45d3541462","path":"DKImagePickerController.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982407d1e8008282cbe7def1a4ffcc63a5","path":"DKImagePickerController-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98ea7538dde57b4e0f1e4dd4e75d1c0d0c","path":"DKImagePickerController-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b27014ca8a98af66a8fbde29e88273ac","path":"DKImagePickerController-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989c1f453bb5d9dee5b1b66f4e8eb4eb4d","path":"DKImagePickerController-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9806dc5717c6a31d76aad6bd6ece1d0e8a","path":"DKImagePickerController.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e982c698840c90027623e3da75a6ca7c080","path":"DKImagePickerController.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e987e62c3ecefb84e0040e723e52f3a31b8","path":"ResourceBundle-DKImagePickerController-DKImagePickerController-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e983adef97a715cd560344faddd2136ca7c","name":"Support Files","path":"../Target Support Files/DKImagePickerController","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986a64e2ff11892e14ae7fcb90015c6a37","name":"DKImagePickerController","path":"DKImagePickerController","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9863e802284c73bd79713e7ebbd9884a03","path":"DKPhotoGallery/DKPhotoGallery.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e980341a413639d3324d70cd433b477ad56","path":"DKPhotoGallery/DKPhotoGalleryContentVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989de872349f4afb6a3ec10554d20a0824","path":"DKPhotoGallery/Transition/DKPhotoGalleryInteractiveTransition.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9891931bbad7bcd3c15555c1ab3b5e9eda","path":"DKPhotoGallery/DKPhotoGalleryScrollView.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989304b9d8408d53cfd4dc626445b7aa78","path":"DKPhotoGallery/Transition/DKPhotoGalleryTransitionController.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9851ce307e43c7abf9a92db60b62d92eed","path":"DKPhotoGallery/Transition/DKPhotoGalleryTransitionDismiss.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98bb524791f17b91cf8d8e69a04b9d6241","path":"DKPhotoGallery/Transition/DKPhotoGalleryTransitionPresent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9817d140dcad4deb3eb9494e9b79f6a54f","path":"DKPhotoGallery/DKPhotoIncrementalIndicator.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b08fe598889329654b388a853ab11345","path":"DKPhotoGallery/DKPhotoPreviewFactory.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98abec4317337fcb2bdb4f60b9acc75558","name":"Core","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981fe52c027e9b931957c809c3731fe5e8","path":"DKPhotoGallery/DKPhotoGalleryItem.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98790064f3a8ebed4c57f29d168a40864a","name":"Model","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98c8c44b9bad588a8cbce8ae16059cc044","path":"DKPhotoGallery/Preview/PDFPreview/DKPDFView.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989def67d6c082310eb82979d6e6048439","path":"DKPhotoGallery/Preview/ImagePreview/DKPhotoBaseImagePreviewVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e64926604b2aa420c550979dce302c11","path":"DKPhotoGallery/Preview/DKPhotoBasePreviewVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985f43f00297e493a6c1ff3f01846dd6ab","path":"DKPhotoGallery/Preview/DKPhotoContentAnimationView.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982b1b24b667eeb0aa9df1641d1afbf02f","path":"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageDownloader.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b7c4392f573762b73d261ae2c1ad25f5","path":"DKPhotoGallery/Preview/ImagePreview/DKPhotoImagePreviewVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98036bb4411bb8d063d059db4e041547ed","path":"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageUtility.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b73272e18580fd7065bff4ab280cf9d1","path":"DKPhotoGallery/Preview/ImagePreview/DKPhotoImageView.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b16bf7005681538906dcee4437e1937d","path":"DKPhotoGallery/Preview/PDFPreview/DKPhotoPDFPreviewVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989a757ab8e8a9fc630d1a57794cb67b77","path":"DKPhotoGallery/Preview/PlayerPreview/DKPhotoPlayerPreviewVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e988a849dc79b3d29aaf31294b33a51b25b","path":"DKPhotoGallery/Preview/DKPhotoProgressIndicator.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a03d72595a015012babd53f62ce9fe7b","path":"DKPhotoGallery/Preview/DKPhotoProgressIndicatorProtocol.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9895fb6b2018a434a37a846c5e29fe4c20","path":"DKPhotoGallery/Preview/QRCode/DKPhotoQRCodeResultVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9806b510aa1c18127530918269f3b10ecd","path":"DKPhotoGallery/Preview/QRCode/DKPhotoWebVC.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e988b5a7957a4c4d63e718fe2f2bfa1a142","path":"DKPhotoGallery/Preview/PlayerPreview/DKPlayerView.swift","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98a4f4a3f314a53e8278137c97d48a2a7d","name":"Preview","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982c920e4b6a548594014fc19d89752af0","path":"DKPhotoGallery/Resource/DKPhotoGalleryResource.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98322362f55daedd42cf2628aab1e7ee5a","path":"DKPhotoGallery/Resource/Resources/Base.lproj","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e98257fc87c20dddd559c3a65cb29ef2a8c","path":"DKPhotoGallery/Resource/Resources/en.lproj","sourceTree":"","type":"file"},{"fileType":"folder.assetcatalog","guid":"bfdfe7dc352907fc980b868725387e98632a77eb6e8a5c194970ffbb837e3163","path":"DKPhotoGallery/Resource/Resources/Images.xcassets","sourceTree":"","type":"file"},{"fileType":"folder","guid":"bfdfe7dc352907fc980b868725387e981cdbb40ea77c1fcafa54f43ddd5ead0e","path":"DKPhotoGallery/Resource/Resources/zh-Hans.lproj","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9833f8b21cf1e493b32aa79c353bb36c59","name":"Resources","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e987e4621e932260350f9fe18d776062789","name":"Resource","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98669eec05ea75fd44fb0e2666721dbfac","path":"DKPhotoGallery.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986ea1aeb64b4ee3b54e278e3a7146573c","path":"DKPhotoGallery-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e984490f039b0eb3468d94b0ab2e6a714c8","path":"DKPhotoGallery-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9827c39759d14ae68710c6e6f3caaa10e9","path":"DKPhotoGallery-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986024dcc5015a387313d8adc4bce7b36a","path":"DKPhotoGallery-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98b1d81c0ffe868a64db997ed9da144cf0","path":"DKPhotoGallery.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e988204bc98e0d67983074132a4f5665a6b","path":"DKPhotoGallery.release.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e987ab489cc4f30435cd7ded82f0c6eae68","path":"ResourceBundle-DKPhotoGallery-DKPhotoGallery-Info.plist","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987ec59054c088e47e413206915ef9028f","name":"Support Files","path":"../Target Support Files/DKPhotoGallery","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e986c674d50d3b10e235f4f35f7a35d5ae8","name":"DKPhotoGallery","path":"DKPhotoGallery","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9896448ec1f9cbf14857b3fe6b95caa011","path":"Sources/Reachability.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e989bacf7eba26d0bb486de3cbfe5ea13c2","path":"ReachabilitySwift.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b6244bc1a6a40e6115703cb3d00b8446","path":"ReachabilitySwift-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e987b7b6b5547766ec62d38737cd24e68cd","path":"ReachabilitySwift-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98547e39e19bf28c52f14233768cc21bd9","path":"ReachabilitySwift-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988aeea5b451523377c4d21395c5903a7c","path":"ReachabilitySwift-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98901baae58940cfd27e962f9d5011362f","path":"ReachabilitySwift.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e989a874d2419abc53ad8b0cd3d2a5318ae","path":"ReachabilitySwift.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9896d1894bae44836917da92d3aab54f93","name":"Support Files","path":"../Target Support Files/ReachabilitySwift","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98bebab75b96c37c53dc67122f6031b002","name":"ReachabilitySwift","path":"ReachabilitySwift","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cbbfeea198fc4bce239c47fc57d8a961","path":"SDWebImage/Private/NSBezierPath+SDRoundedCorners.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a4610931d9a303cf4a5413361d1ada97","path":"SDWebImage/Private/NSBezierPath+SDRoundedCorners.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980e908f1ceaaa33a36d5403f550e8b9aa","path":"SDWebImage/Core/NSButton+WebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9817f5ea7520f6058c0504b71e2865c594","path":"SDWebImage/Core/NSButton+WebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984f06f7f319f35e24d3947bb38a46b4b9","path":"SDWebImage/Core/NSData+ImageContentType.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987b53a8893f5f3cdd6faadb0d3bfc4d62","path":"SDWebImage/Core/NSData+ImageContentType.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9890edfb9455d8d050d856d19de294b76c","path":"SDWebImage/Core/NSImage+Compatibility.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987853d28d16fa30841c1a55b61c447d01","path":"SDWebImage/Core/NSImage+Compatibility.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c8d140d68f858c8de679df2d49e39ce1","path":"SDWebImage/Core/SDAnimatedImage.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b0ebe90ca974f5cad05396d5c0c407cc","path":"SDWebImage/Core/SDAnimatedImage.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9809df879cdb20a2d85e42e0ec9467d874","path":"SDWebImage/Core/SDAnimatedImagePlayer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e2ed6b92aae530671fb6a85f4968036a","path":"SDWebImage/Core/SDAnimatedImagePlayer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f116af502c254813038fb0fa9b6d7e43","path":"SDWebImage/Core/SDAnimatedImageRep.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987a40b9365faa88cb247c44544207a3ce","path":"SDWebImage/Core/SDAnimatedImageRep.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985026f7f77e36035ebbdce01168f1c703","path":"SDWebImage/Core/SDAnimatedImageView.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d1c51ae1c8f49cab13fbed609b95490d","path":"SDWebImage/Core/SDAnimatedImageView.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9853c881de2f36eb982e91b6111edd2159","path":"SDWebImage/Core/SDAnimatedImageView+WebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98229f3dff826db691d58cbf3df3ae4b2c","path":"SDWebImage/Core/SDAnimatedImageView+WebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a7fb3d4eb5f455ce911e0e84b77ee5df","path":"SDWebImage/Private/SDAssociatedObject.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9889c0b922f6710e9bbaad47d90e458796","path":"SDWebImage/Private/SDAssociatedObject.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9870a50afed1d5bb331637a22e1b2cdab6","path":"SDWebImage/Private/SDAsyncBlockOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989a88b4352ddd2268b71af2cececb468f","path":"SDWebImage/Private/SDAsyncBlockOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9894060044116c365175a4d8c9d018b47b","path":"SDWebImage/Private/SDDeviceHelper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98379407cdd5ec5f0842afe1dfb96cddaf","path":"SDWebImage/Private/SDDeviceHelper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9820bbf68e6d4b7e185246c728464b69b4","path":"SDWebImage/Core/SDDiskCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98556c8c3201769e199eb76c9fc9d5a0de","path":"SDWebImage/Core/SDDiskCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9822703fb114a75f5c5f2f254bb3f6484c","path":"SDWebImage/Private/SDDisplayLink.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982e5995722797bf81f987a7b34e8d29db","path":"SDWebImage/Private/SDDisplayLink.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98688ef71815752ca7f6167d44ae846d81","path":"SDWebImage/Private/SDFileAttributeHelper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d4442f69c6514bc98fae925092574979","path":"SDWebImage/Private/SDFileAttributeHelper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cc8572110877f8beb99b8973afcea556","path":"SDWebImage/Core/SDGraphicsImageRenderer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d990503ee20a63ae3a6ac274eb15e500","path":"SDWebImage/Core/SDGraphicsImageRenderer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a81bf725f34f12d4633981ff1a9be615","path":"SDWebImage/Core/SDImageAPNGCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a93d56dd2776df0d9323eaf235ac4573","path":"SDWebImage/Core/SDImageAPNGCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98063a7a83ef11a2830cff9aef0f246a46","path":"SDWebImage/Private/SDImageAssetManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9878a035a3947c0bfb2c74de160efc90f8","path":"SDWebImage/Private/SDImageAssetManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ba45c50b7e93c71a1094293aeb130683","path":"SDWebImage/Core/SDImageAWebPCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988b1062c96ec96331da01cae06e95e9bb","path":"SDWebImage/Core/SDImageAWebPCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98adff88ced90105be82ec5c254de38c39","path":"SDWebImage/Core/SDImageCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b59fb4d17416bbf73623c9ca148d4781","path":"SDWebImage/Core/SDImageCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da4185ba0b8e0c270b9045f1c0ea7f34","path":"SDWebImage/Core/SDImageCacheConfig.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f690a5caa9ac98baa8bfaca0d8119a97","path":"SDWebImage/Core/SDImageCacheConfig.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984fc8806592e30baa20155c119711a319","path":"SDWebImage/Core/SDImageCacheDefine.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98aa465b740f26e7c857d665d9e948f68f","path":"SDWebImage/Core/SDImageCacheDefine.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98025e98798e009017d38c6b8f9f98dd3b","path":"SDWebImage/Core/SDImageCachesManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989babcbb0bec790231c9ea03d45485895","path":"SDWebImage/Core/SDImageCachesManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9848a18db96f9cc4192f5855b2c6ec65fc","path":"SDWebImage/Private/SDImageCachesManagerOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988c4bfa1a1a41475204073a9c37cc4138","path":"SDWebImage/Private/SDImageCachesManagerOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9878c179b604ae00d511bcfe7b1c26229a","path":"SDWebImage/Core/SDImageCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e79c00adee724aeb4ee07faf25c36816","path":"SDWebImage/Core/SDImageCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9899370db25d5ccec85c5bc3841d9150c3","path":"SDWebImage/Core/SDImageCoderHelper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9864cb7481f2ac08dacad300433c1e0e01","path":"SDWebImage/Core/SDImageCoderHelper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98466aa14d7abf59b152f093b8602bd34b","path":"SDWebImage/Core/SDImageCodersManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a351c522c640e5c3836b0b3b0ff15805","path":"SDWebImage/Core/SDImageCodersManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989938d9775335a6ffdff595f50234b88b","path":"SDWebImage/Core/SDImageFrame.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a8ca264bae9407c5ab570cb5c4eaa6e8","path":"SDWebImage/Core/SDImageFrame.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989a78ff5eec8840469dfa52c4df920d84","path":"SDWebImage/Core/SDImageGIFCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984d22bab198a56a739526a9f9fac69088","path":"SDWebImage/Core/SDImageGIFCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b137bd091f2422235254ba87eeeebb86","path":"SDWebImage/Core/SDImageGraphics.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987ec2e6e40d48d5e1c573c77074e16edf","path":"SDWebImage/Core/SDImageGraphics.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9817f302504ade0c532160f76082a200fc","path":"SDWebImage/Core/SDImageHEICCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cec361cb5581e3f861f0c9ebc7eb79b3","path":"SDWebImage/Core/SDImageHEICCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9863c1633045c8f4e6a7fefb974e14ca36","path":"SDWebImage/Core/SDImageIOAnimatedCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98728975404c77f683b561feae9c1d429f","path":"SDWebImage/Core/SDImageIOAnimatedCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9840158ea8f0a0e2c720c2f9a0c43bc29e","path":"SDWebImage/Private/SDImageIOAnimatedCoderInternal.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981a3f021a5e50a888a5ae1abc4e2e57fe","path":"SDWebImage/Core/SDImageIOCoder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9810ea71fbe3808e0193ef3ebfe97b71ed","path":"SDWebImage/Core/SDImageIOCoder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e3114679d79d3ddb667c3e6fb257d6fb","path":"SDWebImage/Core/SDImageLoader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d8bfeefcb91662f7491bdee227ce85b8","path":"SDWebImage/Core/SDImageLoader.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987a235e20b10d513a95f2fe5a4faa9fb2","path":"SDWebImage/Core/SDImageLoadersManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ae292dc3f6734ffa8a30d1eb750b312a","path":"SDWebImage/Core/SDImageLoadersManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98555b647b7b68b3c59175202ef82bc955","path":"SDWebImage/Core/SDImageTransformer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98379f432747c66b556608cfcb163dd08a","path":"SDWebImage/Core/SDImageTransformer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984b63b84d1f79f14a6dc3a12cac2809ed","path":"SDWebImage/Private/SDInternalMacros.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98aad7fdfe00389f16c787afd556ef9f8e","path":"SDWebImage/Private/SDInternalMacros.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98209eac57b1854edf591b16e3f80fec69","path":"SDWebImage/Core/SDMemoryCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982b6d0c6ce7e156d1ba21129855543471","path":"SDWebImage/Core/SDMemoryCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9898c0fd4390df5133c07e348bdbd55d9c","path":"SDWebImage/Private/SDmetamacros.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988e65ef0d735983c43262cb848c637768","path":"SDWebImage/Private/SDWeakProxy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c1a6e6955d89f3463af21ab50142f735","path":"SDWebImage/Private/SDWeakProxy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9802c2dd1c8e6745d6ad8dfa6ac15a50df","path":"WebImage/SDWebImage.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98583254b6e10a610161162b2462c9a243","path":"SDWebImage/Core/SDWebImageCacheKeyFilter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d67745e1c95b9a3b0b6afc23ba0e53d1","path":"SDWebImage/Core/SDWebImageCacheKeyFilter.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e6013c78eea89a230352d0a58977ee65","path":"SDWebImage/Core/SDWebImageCacheSerializer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c798c0650901d3f454e84b38b32b7c14","path":"SDWebImage/Core/SDWebImageCacheSerializer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987bcdc97ee957f7d3dcc46ccd3df087d6","path":"SDWebImage/Core/SDWebImageCompat.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98175d683f5876f4bef28a5c568a03ee0e","path":"SDWebImage/Core/SDWebImageCompat.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a1f302e052441cf2364ea9fdbb633665","path":"SDWebImage/Core/SDWebImageDefine.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9801960d18f6952523241592bfcf8df74f","path":"SDWebImage/Core/SDWebImageDefine.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da783a4512ad557f5bed33b07819428b","path":"SDWebImage/Core/SDWebImageDownloader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988bf2ba3046f2a1dc82c3ae3236bb5c29","path":"SDWebImage/Core/SDWebImageDownloader.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987349aeb8db30aa64526eda7973f155b2","path":"SDWebImage/Core/SDWebImageDownloaderConfig.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987233e0726650facd69de48131fbfb887","path":"SDWebImage/Core/SDWebImageDownloaderConfig.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981f0f6fc0eca1c4df41989a0e01130390","path":"SDWebImage/Core/SDWebImageDownloaderDecryptor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b1e56d48b1b3c795cd043d008ac2d69f","path":"SDWebImage/Core/SDWebImageDownloaderDecryptor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986735d6c802483fa77c3db72c66398b93","path":"SDWebImage/Core/SDWebImageDownloaderOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986a5410248346eb3a5db888eeaf016ed3","path":"SDWebImage/Core/SDWebImageDownloaderOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980f2c0526259c08fb215f33283d062669","path":"SDWebImage/Core/SDWebImageDownloaderRequestModifier.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986771283dce17b4fb9bd4b5fe643ed3b9","path":"SDWebImage/Core/SDWebImageDownloaderRequestModifier.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ae78c144f7dd1ffa4151c834e1856244","path":"SDWebImage/Core/SDWebImageDownloaderResponseModifier.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cd959e9548caf1d3b10d572543028967","path":"SDWebImage/Core/SDWebImageDownloaderResponseModifier.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981f2f1b5fb7e7a0259d587693c70e17dc","path":"SDWebImage/Core/SDWebImageError.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d7b5236ded30e2d2a5ff450a32f7bd62","path":"SDWebImage/Core/SDWebImageError.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98db171e7f43254466428cd7d160d4bc66","path":"SDWebImage/Core/SDWebImageIndicator.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988059612109acc8adb78283f1ba5f6c8e","path":"SDWebImage/Core/SDWebImageIndicator.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989a4f6041648958d4aff810b46b83ec9d","path":"SDWebImage/Core/SDWebImageManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985ec38f9cb9f5d4978c0311d1008e04cc","path":"SDWebImage/Core/SDWebImageManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982c27b087e8cd840abc3ef59829b80efa","path":"SDWebImage/Core/SDWebImageOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989ab8963206759569d84f30727fa4cba2","path":"SDWebImage/Core/SDWebImageOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b72cfadd9fa624c843ceaca86f14fba7","path":"SDWebImage/Core/SDWebImageOptionsProcessor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b5e7d24f1e11dbac304e87bddaff0bab","path":"SDWebImage/Core/SDWebImageOptionsProcessor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982a13b238680429b9cb2b8bf0839d5f79","path":"SDWebImage/Core/SDWebImagePrefetcher.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985fdfabd035ec7a467b035a8ce350edd5","path":"SDWebImage/Core/SDWebImagePrefetcher.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987fbe1b7021e7099d47e3d29bb0d246b0","path":"SDWebImage/Core/SDWebImageTransition.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d898bb0c82f19d5c90770a13e1992bc4","path":"SDWebImage/Core/SDWebImageTransition.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e46d3f1b4958c657b86076222797d146","path":"SDWebImage/Private/SDWebImageTransitionInternal.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989b6a95fea066801733ba0d9eb7781a8d","path":"SDWebImage/Core/UIButton+WebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a7d08c8d1537b994f19071149f068bb7","path":"SDWebImage/Core/UIButton+WebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9845d7911932c79e171dd089fd4628b5b8","path":"SDWebImage/Private/UIColor+SDHexString.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98008deca9be811cf53f205b0ddcc4983a","path":"SDWebImage/Private/UIColor+SDHexString.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986bb62f370bbe234b6bd0d7c77da71782","path":"SDWebImage/Core/UIImage+ExtendedCacheData.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98aa1127430a2307c0512ba13f3695fe9e","path":"SDWebImage/Core/UIImage+ExtendedCacheData.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ccd7d7396bf3843012af71140da2a49c","path":"SDWebImage/Core/UIImage+ForceDecode.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9863a4e0085b4eca2b418099edfec4340a","path":"SDWebImage/Core/UIImage+ForceDecode.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b5c221940cf71976a9792c00ded2a77c","path":"SDWebImage/Core/UIImage+GIF.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d05fd41d05faa2360aeaf46fb5065514","path":"SDWebImage/Core/UIImage+GIF.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98662eda270b9081b770cb1f2a02e387b2","path":"SDWebImage/Core/UIImage+MemoryCacheCost.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fd8dd526fbbf1c35d004173853ebc817","path":"SDWebImage/Core/UIImage+MemoryCacheCost.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986357ed01b566bb1d1be0954a632e35e0","path":"SDWebImage/Core/UIImage+Metadata.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986a263fb34dde4d01069f152b021094c7","path":"SDWebImage/Core/UIImage+Metadata.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98deca6db8fdc22a81efbb6cd75f7c9144","path":"SDWebImage/Core/UIImage+MultiFormat.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981f8ce8389cf620585da7cd0d5ae68c6d","path":"SDWebImage/Core/UIImage+MultiFormat.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f79a5a836cf1d081150335f28fe35761","path":"SDWebImage/Core/UIImage+Transform.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c1367cac0329507a751b00959f8b1fe9","path":"SDWebImage/Core/UIImage+Transform.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9836fad0c1818f3ebd7ea94ecdb2cda58b","path":"SDWebImage/Core/UIImageView+HighlightedWebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e869a02fe4a657b9ddb0f148af11b090","path":"SDWebImage/Core/UIImageView+HighlightedWebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fc1f6b2f5b528239f7eb374a16707dd2","path":"SDWebImage/Core/UIImageView+WebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987a0efb32908f89fd6eaf93a60386fe37","path":"SDWebImage/Core/UIImageView+WebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d835b0cd54e8e5c3eefa756ca6a8cf4f","path":"SDWebImage/Core/UIView+WebCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98da978efa2c20cf80e6a593952a64f214","path":"SDWebImage/Core/UIView+WebCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982999b5735c33706130ed303f0efe7372","path":"SDWebImage/Core/UIView+WebCacheOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983963a49bc38ac28ca13543440440738c","path":"SDWebImage/Core/UIView+WebCacheOperation.m","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e986e6118052c889416524a66863865f2d2","name":"Core","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98b569f5963060c97f2f2ba38385fe0047","path":"SDWebImage.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e582145da4c7bad45fd0fea722620980","path":"SDWebImage-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98f69556d85c791f9f7bb4864067304e9e","path":"SDWebImage-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a5d7cf445f11b8a851603791bb80dccd","path":"SDWebImage-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9849c916368f02890a1ae21d78a5082dce","path":"SDWebImage-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98cbf68d1f922bc4817004dd6a2700f369","path":"SDWebImage.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e982a978c9504e7f820a9a62749b4639eb7","path":"SDWebImage.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98c5c4f2160b2c64c56cc35e1563f69dfa","name":"Support Files","path":"../Target Support Files/SDWebImage","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e989ab633a406a0ba749c8183ca48f70ca1","name":"SDWebImage","path":"SDWebImage","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98198cb7323cf38f49c099a19af424e38e","path":"Sources/Swift/Metrics/BucketsMetricsAggregator.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9820b096789e7bd1bfa4b58fa13bc5b50f","path":"Sources/Swift/Metrics/CounterMetric.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98853fa02d04d0430ad3f2d7b333874af4","path":"Sources/Swift/Metrics/DistributionMetric.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98ac0780da839edf981809953008673060","path":"Sources/Swift/Metrics/EncodeMetrics.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982bd4f7e001b4dd1cf079308ca514554b","path":"Sources/Swift/Metrics/GaugeMetric.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9824d6ee3483a76d2eb18b2c33833c5c8a","path":"Sources/Swift/Tools/HTTPHeaderSanitizer.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98382df2b05839d9ea3f03c148af3ae692","path":"Sources/Swift/Metrics/LocalMetricsAggregator.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981b4b3945f043de125e7b8c8aaeacc7c3","path":"Sources/Swift/Metrics/Metric.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9830b09011eccca71f62b2f66238867f76","path":"Sources/Swift/Metrics/MetricsAggregator.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98aa15c1b3d311498677dcd94b75280f47","path":"Sources/Sentry/include/NSArray+SentrySanitize.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982ad87d7afe24ffd2d6bd213296aea811","path":"Sources/Sentry/NSArray+SentrySanitize.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f4b859c713f2344c6fc1477b46cbafeb","path":"Sources/Sentry/include/NSLocale+Sentry.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987f7b4c8e450d6bc934891b572509dd5c","path":"Sources/Sentry/NSLocale+Sentry.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b9346543c1b98901f22f3c7e23aff838","path":"Sources/Swift/Extensions/NSLock.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989b00bcbca3893520fc4f39ff18d0b1f5","path":"Sources/Sentry/include/NSMutableDictionary+Sentry.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98902ebd6036a41aaacac357ee5ccce1b9","path":"Sources/Sentry/NSMutableDictionary+Sentry.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9885b82b46b9dd005b768f8ef3c751040f","path":"Sources/Swift/Extensions/NumberExtensions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a716e75ccb528744d3ffde348c74054d","path":"Sources/Sentry/include/HybridPublic/PrivateSentrySDKOnly.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9810ac796ebca563e59fec2d96f029829c","path":"Sources/Sentry/PrivateSentrySDKOnly.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980ed4d648fc835701dd40045ac4f0eb42","path":"Sources/Sentry/include/HybridPublic/PrivatesHeader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9825d3d80b5d5f47d8c3577be50d208f17","path":"Sources/Sentry/Public/Sentry.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9881e78b26fce5be69601e8f39debf1815","path":"Sources/Sentry/include/SentryANRTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988933b4bc3332ec51f502704c510ed48f","path":"Sources/Sentry/SentryANRTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c4aec8c696347d8a5fd3da2c9ae3b34b","path":"Sources/Sentry/include/SentryANRTrackerV2.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98143f06533f29ffe1f4e62e249ee7c61a","path":"Sources/Sentry/SentryANRTrackerV2.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9805b65a9fd5a17f8804adb0010a2bb994","path":"Sources/Swift/Integrations/ANR/SentryANRTrackerV2Delegate.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a9377495765c6b05f4938e987beaecc4","path":"Sources/Sentry/include/SentryANRTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9803a9b0b910a8e69ce5d48707b2f7b0ff","path":"Sources/Sentry/SentryANRTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9853bb97ef1af68a2e8a6cd65845220a21","path":"Sources/Sentry/include/SentryANRTrackingIntegrationV2.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f85156689aeaf18e00084edae9890433","path":"Sources/Sentry/SentryANRTrackingIntegrationV2.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982f3eaf81b6a498c005395bd74ff9b40c","path":"Sources/Sentry/include/HybridPublic/SentryAppStartMeasurement.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98573cf1fba6ec7158ac9e5d30872d3358","path":"Sources/Sentry/SentryAppStartMeasurement.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da6d3a4aa155bc2b99c1c5776fa1d7ab","path":"Sources/Sentry/include/SentryAppStartTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9858a59bcb56abe22c97af586957612f8a","path":"Sources/Sentry/SentryAppStartTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9897bfe385980cf04d6052b9a9a48626bd","path":"Sources/Sentry/include/SentryAppStartTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984c7e16723e7d6c5950d220f08a0f3a1b","path":"Sources/Sentry/SentryAppStartTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98711c5fa9ee620eb44232227aea5b4980","path":"Sources/Sentry/include/SentryAppState.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e102ca54129581ea6ac1fd19ca2f900d","path":"Sources/Sentry/SentryAppState.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f4aff5850320b2892171d2f9050e54f0","path":"Sources/Sentry/include/SentryAppStateManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98945bba539ff28910dddfef800d776212","path":"Sources/Sentry/SentryAppStateManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c7abc40702780507f67178f200c91e33","path":"Sources/Sentry/include/SentryAsynchronousOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f2c93c0f5d5274544f463c3dd147b69e","path":"Sources/Sentry/SentryAsynchronousOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9848c4b2580de96341fc0bfc5e6780e93a","path":"Sources/Sentry/SentryAsyncSafeLog.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98503f56b704c70eeeaa87f393ca606f46","path":"Sources/Sentry/SentryAsyncSafeLog.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9839b12ad72defb9e70cd9c490eab0f56b","path":"Sources/Sentry/Public/SentryAttachment.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9817625a85b70840a0b0718c986516528e","path":"Sources/Sentry/SentryAttachment.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98999febd8a8da745b5ea4843c406440a6","path":"Sources/Sentry/include/SentryAttachment+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980c19c6b2f6ae9fa9cbfb42827276a528","path":"Sources/Sentry/include/SentryAutoBreadcrumbTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982375cf366eb9fa65b8bba986be274ede","path":"Sources/Sentry/SentryAutoBreadcrumbTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986e0e69f1089a15308ed825159b9e98f7","path":"Sources/Sentry/include/SentryAutoSessionTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a4037e5e9e7feba04419a743f2be779b","path":"Sources/Sentry/SentryAutoSessionTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e981de7f79c30c8772c5fa58d8ea6d1b1b6","path":"Sources/Sentry/SentryBacktrace.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e9891ba718e9d3cff3ab8d9bfee8bd0462b","path":"Sources/Sentry/include/SentryBacktrace.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989f61aa00b8a1516208dca7f64cd6a75c","path":"Sources/Sentry/Public/SentryBaggage.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b20f7baef1201383ed42eb1c42b1ef2a","path":"Sources/Sentry/SentryBaggage.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e983e4f391d969af2e5ce4e2e2dd4a41dfb","path":"Sources/Swift/Helper/SentryBaggageSerialization.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985a62059eff00244af5c87c6ddd480877","path":"Sources/Sentry/include/SentryBaseIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987a9f6073313425fbba5130148b9b3a65","path":"Sources/Sentry/SentryBaseIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ccba6be3c8cd08597e295bd9856dd7e3","path":"Sources/Sentry/include/HybridPublic/SentryBinaryImageCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98963cbdfa2428d743619bf4f2c4cdf206","path":"Sources/Sentry/SentryBinaryImageCache.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98963224f46996165eb8c62129394cfe81","path":"Sources/Sentry/Public/SentryBreadcrumb.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e6b4f1738c9e7b78c11959faf476bb67","path":"Sources/Sentry/SentryBreadcrumb.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98647433b4e086798b973e947311127f7c","path":"Sources/Sentry/include/HybridPublic/SentryBreadcrumb+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b337f47392c6e73cb95920efdda0b675","path":"Sources/Sentry/include/SentryBreadcrumbDelegate.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fd297ac79b8c53f88805194c653eb324","path":"Sources/Sentry/include/SentryBreadcrumbTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98917b84e096fb7a3cee094b304c0d1f1e","path":"Sources/Sentry/SentryBreadcrumbTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987869ca5f51cd67f10fb06cf6d2a072e1","path":"Sources/Sentry/include/SentryBuildAppStartSpans.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b16fdba02a928575106b9d1ac1686733","path":"Sources/Sentry/SentryBuildAppStartSpans.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9899a99bf57fcbf0c333b505bdb2e414cc","path":"Sources/Sentry/include/SentryByteCountFormatter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e29dfdbdcbf120e148ffa7d4a4c248a4","path":"Sources/Sentry/SentryByteCountFormatter.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98be06b9e44be8b3f68cdabea1eab410fb","path":"Sources/Sentry/Public/SentryClient.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981f87b3406b001d130316dbc8ada38a00","path":"Sources/Sentry/SentryClient.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987b01f905126992881fc2dce8d8608866","path":"Sources/Sentry/include/SentryClient+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987f499c7f3f52d80336b1b533a1394782","path":"Sources/Sentry/include/SentryClientReport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a4dc6d699fd7d93e90a761eda46c4f64","path":"Sources/Sentry/SentryClientReport.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9830ba7417bb270438177deb73683c5536","path":"Sources/Sentry/include/SentryCompiler.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985f16ffcf4dfbc88b10ce30fd3e633708","path":"Sources/Sentry/include/SentryConcurrentRateLimitsDictionary.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98869ae30e47af8e56237caf99320cf284","path":"Sources/Sentry/SentryConcurrentRateLimitsDictionary.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f66f182cfe191dbc0994d7db36647922","path":"Sources/Sentry/include/SentryContinuousProfiler.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98fe03e1800d75ed025acc7c9f643b8f04","path":"Sources/Sentry/Profiling/SentryContinuousProfiler.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9860162861b3210d454479d7e1ac71d658","path":"Sources/Sentry/include/SentryCoreDataSwizzling.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9828efb6a05eb4ef1519bb559204168266","path":"Sources/Sentry/SentryCoreDataSwizzling.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9813f1af6eada9adbb72a6632c8d2b2829","path":"Sources/Sentry/include/SentryCoreDataTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980e9d7ced294ae24069ba05792c403f21","path":"Sources/Sentry/SentryCoreDataTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98373aa59d4a768c9ebed68430e50f9fd4","path":"Sources/Sentry/include/SentryCoreDataTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d656120ee1d06ad362116f5cd2c4da69","path":"Sources/Sentry/SentryCoreDataTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98971753585da08da03da7583249217f27","path":"Sources/Sentry/include/SentryCPU.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e71ce65934ced440e8087b210d26e9dd","path":"Sources/SentryCrash/Recording/SentryCrash.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9810ab6f5523c4427b61de387291c7a269","path":"Sources/SentryCrash/Recording/SentryCrash.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e983e0adaf7be093e8acf73fd7ca3cd8020","path":"Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986c59b016150adc6042a09462cf5f2949","path":"Sources/SentryCrash/Recording/SentryCrashBinaryImageCache.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ba2cf898e20c56ff809039433c33cfd0","path":"Sources/Sentry/include/SentryCrashBinaryImageProvider.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e982f6e693d7a2699f92a735b00a3b748ec","path":"Sources/SentryCrash/Recording/SentryCrashC.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988880cf5ad48383745a739a9820534574","path":"Sources/SentryCrash/Recording/SentryCrashC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e981c394d73a0449af8d0295e4f249392e4","path":"Sources/SentryCrash/Recording/SentryCrashCachedData.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984943986cd47c7fbf04e945d0f7ce23dc","path":"Sources/SentryCrash/Recording/SentryCrashCachedData.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e985af7183f551d475ec21df7099e52ba19","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9819e75d68bf8cdd116d83607aa0c87f45","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a8b934d46e8ace1a43b18243a1180d46","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_Apple.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e986e6252e39b90f3ce76d83e5d6c3974b2","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_arm.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98a418c37e4b8f0cae47ee4e0ef8a63b52","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_arm64.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9854f64306da17dca29bd7ff5a1c475bbe","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_x86_32.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9858b9351f994445a2cced9788b61b0857","path":"Sources/SentryCrash/Recording/Tools/SentryCrashCPU_x86_64.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9815c5e7deb3496c383b03ad2cb8c63c54","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDate.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98920a4695f60aba5f7a2d55e19953a7c3","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDate.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e982e916154b2f4a8ee629c521943af2be5","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDebug.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fa25002e2a2a304eb735465f026645f3","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDebug.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9862a562ca672b78057b80f0d6b1381746","path":"Sources/Sentry/include/SentryCrashDefaultBinaryImageProvider.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986d3ae0f245877d1a34a61568db103431","path":"Sources/Sentry/SentryCrashDefaultBinaryImageProvider.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989f539a8dd7e053b0be13bac966b57726","path":"Sources/Sentry/include/SentryCrashDefaultMachineContextWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98834119e584475e9bbe8dde34e6124eed","path":"Sources/Sentry/SentryCrashDefaultMachineContextWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987c323ed9eabfe9ca185b337c9308d491","path":"Sources/SentryCrash/Recording/SentryCrashDoctor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e86a5d681f8907d2cfc0b4615cae7aa6","path":"Sources/SentryCrash/Recording/SentryCrashDoctor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98872202ab332a20c50d416271f9d2372a","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDynamicLinker.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ba5fbfcc911db7d4ae22629456fd9144","path":"Sources/SentryCrash/Recording/Tools/SentryCrashDynamicLinker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98328deee97d6c62d2e5b511634b98f69a","path":"Sources/Sentry/Public/SentryCrashExceptionApplication.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ced46e42add5fe763f983e5bb41852eb","path":"Sources/Sentry/SentryCrashExceptionApplication.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9851df5069bd644d351f378d5a24e99531","path":"Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ee672a6996837477d96028b68c1168d1","path":"Sources/SentryCrash/Recording/Tools/SentryCrashFileUtils.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9884856fe388c064570227a322157015ee","path":"Sources/SentryCrash/Recording/Tools/SentryCrashID.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9895df3868e47b3489c27cba402f91a418","path":"Sources/SentryCrash/Recording/Tools/SentryCrashID.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98eafd27653478c04bf42d5a09c9475684","path":"Sources/SentryCrash/Installations/SentryCrashInstallation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98785810b1ca7d160b372c70ed55188bdb","path":"Sources/SentryCrash/Installations/SentryCrashInstallation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98789ed62e82862651822eb9e968b4183b","path":"Sources/SentryCrash/Installations/SentryCrashInstallation+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987b19143bc7df2526be6323a84f13d0cd","path":"Sources/Sentry/include/SentryCrashInstallationReporter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9843bd4e1a8c710f4388f1611823aa8703","path":"Sources/Sentry/SentryCrashInstallationReporter.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fa237d860d80a5137ce637c70f1d45ae","path":"Sources/Sentry/include/SentryCrashIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984f9135a1bacbd9b7d700d830329cfa18","path":"Sources/Sentry/SentryCrashIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ed40eb6a8415d9209bc656e3e9003d20","path":"Sources/Sentry/include/SentryCrashIsAppImage.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e983ace9e6326466c6006d7903e835f2298","path":"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984db0ba7ef8f9c62a4768e1244e0175a5","path":"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodec.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980949e62d558d1222c68ac80103de072a","path":"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodecObjC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fb551adfbd11861983f13d4ea4ac5672","path":"Sources/SentryCrash/Recording/Tools/SentryCrashJSONCodecObjC.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98ebe01a592e2d14b2852a1fb5fbd2aec6","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMach.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e63f4dd5624ba45cbd9550973bc7a64b","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMach.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98ac0ec3dc837141d75865164b98357241","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982d7b304e56d6074933295f9cbe624568","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b5dcb1a00343dc6a7015edc3f087c60a","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMachineContext_Apple.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9890a37d86794ca14855675168cd3383ad","path":"Sources/Sentry/include/SentryCrashMachineContextWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e982bc91af71f89cf0a1a1788830d8adeba","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMemory.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bad5c89792bfd17e382bb695ad8ed3f7","path":"Sources/SentryCrash/Recording/Tools/SentryCrashMemory.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98cb3e637b4b9c0fd20f7ea80210d8b3c8","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f6864c5d37412b769927b5d5c74fe602","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e981c4aa5985300ab845f08441631c136e0","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986db33830a858c9794d31e24b7b5cdb20","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_AppState.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e98928789958ea08294471edb78122163bf","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d88503a787c4bef8d3f64a16f8ac7431","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_CPPException.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e985631641a5bf876e0a4464325cd89f9f0","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f9cccecbb59fac96ef00765633935089","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_MachException.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981f0f66e36982c6f19b8196b5549c1151","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_NSException.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9854c3e488a79f2baa48bf1bde77c46cd4","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_NSException.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98fc0ee9ce543fb5029fa8d77a33a2a927","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ef5045f7ebe869b4e0ded07a2e0b854f","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_Signal.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986d521257b90b2beb37d3cbeb2b852f91","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_System.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989b790d6fb8d01f7933ddbca6b6c2e119","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitor_System.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b15723d60c434030589a9c72cc466027","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e984023b76db5ad50719b293030bedfa158","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e0d4db171ade52ab9810b83e9e46027a","path":"Sources/SentryCrash/Recording/Monitors/SentryCrashMonitorType.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9858f76db325762543559f151e9cde7092","path":"Sources/SentryCrash/Recording/Tools/SentryCrashNSErrorUtil.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98036351eb99bc61418fa7904beb886a4b","path":"Sources/SentryCrash/Recording/Tools/SentryCrashNSErrorUtil.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98ec8643356b925433575de2c29645e403","path":"Sources/SentryCrash/Recording/Tools/SentryCrashObjC.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d43045f94dfb5f8084c33ff0795a4fb3","path":"Sources/SentryCrash/Recording/Tools/SentryCrashObjC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983ff24760fdf3ff3241a74acaab8abd66","path":"Sources/SentryCrash/Recording/Tools/SentryCrashObjCApple.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e52be506e681f88f6f6ca68d2f91cc4e","path":"Sources/SentryCrash/Recording/Tools/SentryCrashPlatformSpecificDefines.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98ebb3d6360d98f69e909523c8039f5805","path":"Sources/SentryCrash/Recording/SentryCrashReport.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9882eca2c2727b3da70dc9991523763b33","path":"Sources/SentryCrash/Recording/SentryCrashReport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9857e1affb989c53e044956e4452fdbcea","path":"Sources/Sentry/include/SentryCrashReportConverter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982deeb46c4817dcdcd9ac3ff4d08481d4","path":"Sources/Sentry/SentryCrashReportConverter.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982aad48539d3cdacf06eba4d8e46a2584","path":"Sources/SentryCrash/Recording/SentryCrashReportFields.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981e9d85b65beaeba8501a1c62b8febe5b","path":"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980d8271e53cbfcf85f6cd065e708f2de2","path":"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilterBasic.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9840d849ddebfb811a2974576cbaeecf80","path":"Sources/SentryCrash/Reporting/Filters/SentryCrashReportFilterBasic.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98a9d5de5d3dfe92644037d71ee5190757","path":"Sources/SentryCrash/Recording/SentryCrashReportFixer.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f69b87a8cdb3abcbac109f20e35935f7","path":"Sources/SentryCrash/Recording/SentryCrashReportFixer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9875fc97b0b53f66ab6b047f69319bae1f","path":"Sources/Sentry/include/SentryCrashReportSink.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98f0c689d1974688e39dbfb4912fa84a98","path":"Sources/Sentry/SentryCrashReportSink.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9896c2e55b5af11c135ee2e941bdd0d7f7","path":"Sources/SentryCrash/Recording/SentryCrashReportStore.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9837c90d38984770cab39b73a4ada41ded","path":"Sources/SentryCrash/Recording/SentryCrashReportStore.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f92815bb8d5e910d80c83c261ef2665a","path":"Sources/SentryCrash/Recording/SentryCrashReportVersion.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982b8d6627048f9dc97cf8b1ea4105f91b","path":"Sources/SentryCrash/Recording/SentryCrashReportWriter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d078af0694b1dfb7d9ccde999b44e4fb","path":"Sources/Sentry/include/SentryCrashScopeObserver.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98078b4b6983c2612f332996e0802c4c78","path":"Sources/Sentry/SentryCrashScopeObserver.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98fc0f9f2e8fb1eb0f1596f10d50e88f2f","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983af198c0f2730501f9ce1047fe9febf5","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSignalInfo.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e982f50e0ec35d87b0e7665d862c74ad244","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980ab8a94243d149d379bbd54e70a367d2","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e989255de6ce3a4e6431e337169ea12941c","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_Backtrace.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98227f0ab7c55a7b2a8c57667847371c5c","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_Backtrace.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e981ded049e9550dae6bfea340e4044795f","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985481f9043d0f6eec88a8931509d27ca7","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_MachineContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98172b9a0d48d2f67d576708e2c15c5681","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980331a56274b42b435802d99adc0f0030","path":"Sources/SentryCrash/Recording/Tools/SentryCrashStackCursor_SelfThread.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98616e193ad92d875887b6acd5f702b997","path":"Sources/Sentry/include/SentryCrashStackEntryMapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fcbe81bbd35a2c2ad89d2e20c97dcc80","path":"Sources/Sentry/SentryCrashStackEntryMapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98cac8c3e4cc76215e854ea8e9fd0b4fbd","path":"Sources/SentryCrash/Recording/Tools/SentryCrashString.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982bc1b6754e56222800d995cd75f8c2bb","path":"Sources/SentryCrash/Recording/Tools/SentryCrashString.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98e542917dc6ed22f0dc8928e137282679","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSymbolicator.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9842aa558befabfc401dc6c66d86529e41","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSymbolicator.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9805e8aec9ac44b4f557bd1c2c0ed33d7e","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSysCtl.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987d038ee2ee6d13d961dcc8f8cef0e8f9","path":"Sources/SentryCrash/Recording/Tools/SentryCrashSysCtl.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9814ac97981888d650562ae1699fbed050","path":"Sources/SentryCrash/Recording/Tools/SentryCrashThread.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983761e96361f55650fcff3ec415c306c0","path":"Sources/SentryCrash/Recording/Tools/SentryCrashThread.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98cf8824f371b9de403b95fafde5e63a58","path":"Sources/SentryCrash/Recording/Tools/SentryCrashUUIDConversion.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9824542f73d2e98c60530e9c276356c21d","path":"Sources/SentryCrash/Recording/Tools/SentryCrashUUIDConversion.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f8bc45a2584bffecf7c9393d70e3ac1b","path":"Sources/SentryCrash/Reporting/Filters/Tools/SentryCrashVarArgs.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d8def6fa7b0dff52e0288684681cd235","path":"Sources/Sentry/include/SentryCrashWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98844dfbd5d2a8a41ef38196b8c228710e","path":"Sources/Sentry/SentryCrashWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98beaf5cf161791d03e65fca4463133522","path":"Sources/Swift/Helper/SentryCurrentDateProvider.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9820c8946792aadcff4c6b7911cd3509b1","path":"Sources/Sentry/include/SentryDataCategory.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9897ecba504c62f71a990fd084fdd0aa08","path":"Sources/Sentry/include/SentryDataCategoryMapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98993a135e0861037578f65e78f6f19fa5","path":"Sources/Sentry/SentryDataCategoryMapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fb2c2fb737cef61e3aa32bc173200734","path":"Sources/Sentry/include/SentryDateUtil.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980da3c3a8a5a8e75a2b495f25b864ffff","path":"Sources/Sentry/SentryDateUtil.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982cfb6141f563ef3b224e37c23dfd6ee7","path":"Sources/Sentry/include/SentryDateUtils.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981e501be4e9e4ce930febba0a67b92f83","path":"Sources/Sentry/SentryDateUtils.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986de643aa7e4d68dd257a682eb4ecf094","path":"Sources/Sentry/Public/SentryDebugImageProvider.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988714d874b4c7d300f8820a9568aac3b9","path":"Sources/Sentry/SentryDebugImageProvider.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ccd67327f2cef046d47a060a50e2b033","path":"Sources/Sentry/Public/SentryDebugMeta.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d62e6ab2a2d7467600f6feb7e2671a52","path":"Sources/Sentry/SentryDebugMeta.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98057cb7e081eab9c13bc45a99e2046c64","path":"Sources/Sentry/include/SentryDefaultObjCRuntimeWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98755befdde596ed98608c0f49e465f2e9","path":"Sources/Sentry/SentryDefaultObjCRuntimeWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987fb8b09d68a8675e669ccaa2b3d70f8b","path":"Sources/Sentry/include/SentryDefaultRateLimits.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98284e598b5703764339c9c4dd2d39c047","path":"Sources/Sentry/SentryDefaultRateLimits.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a8b66c1f8b8fe6afdb3bd5083ac798dd","path":"Sources/Sentry/Public/SentryDefines.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982b9ddd57b7d448702427527f15078729","path":"Sources/Sentry/include/SentryDelayedFrame.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b80297ad046a9e2ee2fc01b38f886866","path":"Sources/Sentry/SentryDelayedFrame.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c933c980d9fd2d6b18ab7660be826a32","path":"Sources/Sentry/include/SentryDelayedFramesTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9883f0c58fdda69214b888d7a89386438d","path":"Sources/Sentry/SentryDelayedFramesTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988e3fca574186ef9a3fbf60c7936614dd","path":"Sources/Sentry/include/HybridPublic/SentryDependencyContainer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988e075ecfc8e39865eebcf99287896107","path":"Sources/Sentry/SentryDependencyContainer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ab472310b921cb357b3403f16d6bcc91","path":"Sources/Sentry/include/SentryDevice.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98ce8faf244cb4f73da113703dbb0fd493","path":"Sources/Sentry/SentryDevice.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f496c08b830fbf3baf100455494f579e","path":"Sources/SentryCrash/Reporting/Filters/Tools/SentryDictionaryDeepSearch.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ed54e2f0091c3b6700b4882495308ca7","path":"Sources/SentryCrash/Reporting/Filters/Tools/SentryDictionaryDeepSearch.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c50f7a6b19aff7f4ca9db7611b185ffc","path":"Sources/Sentry/include/SentryDiscardedEvent.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982b532fff8e4b1e30be209e64ad5f2328","path":"Sources/Sentry/SentryDiscardedEvent.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989c9e6c345c44f79bfb78251b2dbb8c7b","path":"Sources/Sentry/include/SentryDiscardReason.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c183e6d53e8117f062c3f4b7cd156530","path":"Sources/Sentry/include/SentryDiscardReasonMapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986c8a3f333ed0b95751e7fbf78d7c98b9","path":"Sources/Sentry/SentryDiscardReasonMapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bba374cd8c0dc6a0b88cd9da50d55613","path":"Sources/Sentry/include/SentryDispatchFactory.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b61addc447754e7e5ec64e70a8a8d043","path":"Sources/Sentry/SentryDispatchFactory.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98181ef37baebf7620fdb3ed512d9c5b2c","path":"Sources/Sentry/include/SentryDispatchQueueWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fdfd3d660615791f5b19cb7a270b3d33","path":"Sources/Sentry/SentryDispatchQueueWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98edef8192fe71dc5c3aa5e7605d8bc231","path":"Sources/Sentry/include/SentryDispatchSourceWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ad912bcb51aa320ac7d9ae0d6b1c29d0","path":"Sources/Sentry/SentryDispatchSourceWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985d9e7a648cec7e7fc0ad3cf75672ea68","path":"Sources/Sentry/include/SentryDisplayLinkWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cebef9f2b8583ad43ec280bc3f601765","path":"Sources/Sentry/include/SentryDisplayLinkWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98eb4e19f7af34fa2f92906307734634ab","path":"Sources/Sentry/Public/SentryDsn.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98bc58b953a2bf19f0ba887c35a848c43d","path":"Sources/Sentry/SentryDsn.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98629100bfbc75470514774b17e16c6ed2","path":"Sources/Swift/Helper/SentryEnabledFeaturesBuilder.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f5d9dabdb733a787122fe8ba4ca78980","path":"Sources/Sentry/include/HybridPublic/SentryEnvelope.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989ef35f84679b5286380254cf3782d3fc","path":"Sources/Sentry/SentryEnvelope.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982792cab6ec953388de02a2ff6935ac57","path":"Sources/Sentry/include/SentryEnvelope+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f6cedb8fc664cf2a66556a10e0939328","path":"Sources/Sentry/include/SentryEnvelopeAttachmentHeader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987e289e99946da7963f37deadbe959fdd","path":"Sources/Sentry/SentryEnvelopeAttachmentHeader.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98141370ca334e2965d382e9f5cee9eb40","path":"Sources/Sentry/Public/SentryEnvelopeItemHeader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987942c3b2f4ab61de1e45f7316786b84c","path":"Sources/Sentry/SentryEnvelopeItemHeader.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98781a30887bd009417ad1a4112c2fb3fe","path":"Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9895aa5c265104e54b26110f7557073d7b","path":"Sources/Sentry/include/SentryEnvelopeRateLimit.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d5a56c7cb96529d8791d765d11773537","path":"Sources/Sentry/SentryEnvelopeRateLimit.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bafc78f7024b690ecf9471384a9902b4","path":"Sources/Sentry/Public/SentryError.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98ca1115e3b61c2229c1c80bd36f06de1b","path":"Sources/Sentry/SentryError.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98413720c3decab33490a7c5172007d190","path":"Sources/Sentry/Public/SentryEvent.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983743f3d27289e139c0b954aaa4e0628a","path":"Sources/Sentry/SentryEvent.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a8f47a674006a0bd05b6739dc62b4f4f","path":"Sources/Sentry/include/SentryEvent+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987159465a79e7a721e0255b49a250409e","path":"Sources/Sentry/Public/SentryException.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9895825e81ff4e9350316aa59835550564","path":"Sources/Sentry/SentryException.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9862683b73d213711a5794050fbedb940e","path":"Sources/Swift/SentryExperimentalOptions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a207c835059cd1e43b5179b933097323","path":"Sources/Sentry/SentryExtraContextProvider.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e15159df27cf33be5cf5b699238e167f","path":"Sources/Sentry/SentryExtraContextProvider.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e61558513e2bb03c67587242c1ae2320","path":"Sources/Swift/Helper/SentryFileContents.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9891743b33ff8051d960b0188178ff8d18","path":"Sources/Sentry/include/SentryFileIOTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9811f125cb4ea42b0ee92dca25f4b88671","path":"Sources/Sentry/SentryFileIOTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c2c1ff1a63d933d631b4428ce2e38740","path":"Sources/Sentry/include/SentryFileManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987dc757a9b6a393b594b0e7f493805852","path":"Sources/Sentry/SentryFileManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c2ef2bd076cd4dd059f7e298bf852014","path":"Sources/Sentry/include/HybridPublic/SentryFormatter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9852b362bca7cc0c9e4f0ac4357b8926c2","path":"Sources/Sentry/Public/SentryFrame.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98cb0c8f2e66fc06982ca30a2cf3eb9bfc","path":"Sources/Sentry/SentryFrame.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981a64e72c9b8c3c97fd22013edcdae5ca","path":"Sources/Sentry/include/SentryFrameRemover.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9887e6e078e6301374b2fef621090a7c44","path":"Sources/Sentry/SentryFrameRemover.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98dba5b9e2f053ff10581a7a52a1336881","path":"Sources/Swift/Integrations/FramesTracking/SentryFramesDelayResult.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980aa82f1dc751a60b99dfdc678c1d417c","path":"Sources/Sentry/include/HybridPublic/SentryFramesTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9851e95210c8e69b336ec75546cb826a45","path":"Sources/Sentry/SentryFramesTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98efaa72ef40eaa86dff924769099fe63f","path":"Sources/Sentry/include/SentryFramesTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98732b4c467888ee6e063ecfd31db00fc1","path":"Sources/Sentry/SentryFramesTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9891ed9d2029d00cffa36a1f5fc06398a1","path":"Sources/Sentry/Public/SentryGeo.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988d6d449e12ef4dade75902629ee9c5e5","path":"Sources/Sentry/SentryGeo.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ca8e4cce263f1f47c0f162e4c586c4f8","path":"Sources/Sentry/include/SentryGlobalEventProcessor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ed988e9fdf3783f895274a6a759ea0e0","path":"Sources/Sentry/SentryGlobalEventProcessor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986e3e378fc6f7cc204b68b3caa7236f27","path":"Sources/Sentry/include/SentryHttpDateParser.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a9f385e05164f46553c2ee2cf481b7d4","path":"Sources/Sentry/SentryHttpDateParser.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c3bfc35223f1b5b6943049d2fb6c4389","path":"Sources/Sentry/Public/SentryHttpStatusCodeRange.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98768627ada7e32a0631bc8af27cb1d1ff","path":"Sources/Sentry/SentryHttpStatusCodeRange.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cca6fefe9cb49705f068665278f8ac02","path":"Sources/Sentry/include/SentryHttpStatusCodeRange+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989de0a39761e8a1050f2257cbe8713a04","path":"Sources/Sentry/include/SentryHttpTransport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985ee2ceb1a7cef57f920d5af9053a5d61","path":"Sources/Sentry/SentryHttpTransport.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98836f5531819312d8b361c85d8e8b18a4","path":"Sources/Sentry/Public/SentryHub.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98252ac7b4e2a6feb7cd60fdd1a4f09dc4","path":"Sources/Sentry/SentryHub.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98638a129b104a232f59a5551902b1f3f4","path":"Sources/Sentry/include/SentryHub+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98118a67a7239494acc31abf04261df084","path":"Sources/Swift/Protocol/SentryId.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98070046bf0c41b80e5921e2353abd1714","path":"Sources/Sentry/include/SentryInAppLogic.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c06ead7e0dbabb3e7c310df0e456432b","path":"Sources/Sentry/SentryInAppLogic.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e38ea514f750e6ef90ab207cb16028c2","path":"Sources/Sentry/include/SentryInstallation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981b188529a60da3e88d8e87a3cef822ea","path":"Sources/Sentry/SentryInstallation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98f1ecb074aa05d438550e8283fcf8840e","path":"Sources/Swift/Protocol/SentryIntegrationProtocol.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9880707aac6a0b662d864e53db8ec3e873","path":"Sources/Sentry/include/SentryInternalCDefines.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b48ba94d494b08c87d61b93a678e3dd2","path":"Sources/Sentry/include/SentryInternalDefines.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9827d2c767a3050b20c39203308cb87f8d","path":"Sources/Sentry/include/SentryInternalNotificationNames.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9849c7220f6bfe0eeb615b9a7accf8b533","path":"Sources/Sentry/include/SentryInternalSerializable.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98def9cc150a565d2550979ec5b69998b1","path":"Sources/Sentry/include/SentryLaunchProfiling.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98911fdc6061a2135c748153f2e4a05aa3","path":"Sources/Sentry/Profiling/SentryLaunchProfiling.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989df9dc056cea8fcf0f59af8f85cd8282","path":"Sources/Swift/Helper/Log/SentryLevel.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983c8f81c7f1106715ce45bc09d968a4cb","path":"Sources/Sentry/include/SentryLevelHelper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9827103d0e236a2917fac90b75c47f4fec","path":"Sources/Sentry/SentryLevelHelper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98844dd3ba156013068dbf7f35651a5957","path":"Sources/Sentry/include/SentryLevelMapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986b5d8de5042f9b6c6debab299d006332","path":"Sources/Sentry/SentryLevelMapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988d673b72b0119aacb08d60c172d07c94","path":"Sources/Sentry/include/SentryLog.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9828d4fd172c6650cc894dc077f2713654","path":"Sources/Swift/Tools/SentryLog.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c9238e73552f603c4ec002a1af34b21c","path":"Sources/Sentry/include/SentryLogC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980bbcb909a9d03a552f1277782e18761a","path":"Sources/Sentry/SentryLogC.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e986b40c6c48b55db5046d6db511942fa38","path":"Sources/Swift/Tools/SentryLogOutput.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e9849d90f9a6a3c8d0dad49a3899b83cec1","path":"Sources/Sentry/SentryMachLogging.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e9800ab9a3ecaf9fb561465a8384a1a4232","path":"Sources/Sentry/include/SentryMachLogging.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982d535deadc12989119b0615b9dd28f36","path":"Sources/Sentry/Public/SentryMeasurementUnit.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d114189b8a76ae18f56bcfc4aaa7ea75","path":"Sources/Sentry/SentryMeasurementUnit.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980ca01cfbb872cb4259cd1e35227961de","path":"Sources/Sentry/include/SentryMeasurementValue.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987666c44d8e4bd63cee7a88e33191a7e6","path":"Sources/Sentry/SentryMeasurementValue.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9851202d61adfe91c437af0f1c67e03d04","path":"Sources/Sentry/Public/SentryMechanism.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e451424b1cdbc8b40cd309ecd4bbbf52","path":"Sources/Sentry/SentryMechanism.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98bab24acd4702a28a3c4c9063e4ef0dce","path":"Sources/Sentry/Public/SentryMechanismMeta.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982968cc739e708fdcb3738f171b8efd43","path":"Sources/Sentry/SentryMechanismMeta.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9830744347f699402adbc18f50caf4e2d7","path":"Sources/Sentry/Public/SentryMessage.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981082286186bf8efb07f5f2ce82e71bfc","path":"Sources/Sentry/SentryMessage.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9899ce478e31a9826614e13502b4400b26","path":"Sources/Sentry/include/SentryMeta.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980d00aea86836a212dca15301d0f6be80","path":"Sources/Sentry/SentryMeta.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ead309d87cf3d8ca9c2bc918c7485105","path":"Sources/Sentry/include/SentryMetricKitIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982f522d156c7a74cefb6fc138d7ed9eea","path":"Sources/Sentry/SentryMetricKitIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987cd2cb4adcd0f896a6a9aa250d475f99","path":"Sources/Sentry/include/SentryMetricProfiler.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9869f27a6b12c3ecdb44b5cf7f02588341","path":"Sources/Sentry/SentryMetricProfiler.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e986ce9c2df801c62045926d8864e84a2a1","path":"Sources/Swift/Metrics/SentryMetricsAPI.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981e970cc41f9443b9aec9727b6001ea4c","path":"Sources/Swift/Metrics/SentryMetricsClient.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982f7d0a3333cb2dcfcbe9c5afd9ec20f5","path":"Sources/Sentry/include/SentryMigrateSessionInit.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c0abc314e5dda9b6db74ec0670b9817b","path":"Sources/Sentry/SentryMigrateSessionInit.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984f295a6e7dc5aee192e374504ed7fe03","path":"Sources/Sentry/include/SentryMsgPackSerializer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9886c992067744c43e9998edd72ad5e859","path":"Sources/Sentry/SentryMsgPackSerializer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98571b885c8f8846fcc4fd4db493b7816d","path":"Sources/Swift/MetricKit/SentryMXCallStackTree.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e987aff5927f8aad996cb87fb760614dbfd","path":"Sources/Swift/MetricKit/SentryMXManager.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e6c11cbc922ee1e497e67191309e6292","path":"Sources/Sentry/include/SentryNetworkTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9845b3a6e7f24267ede88073dada9a10f1","path":"Sources/Sentry/SentryNetworkTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98940d6066dd7b5b655ab91100255b0082","path":"Sources/Sentry/include/SentryNetworkTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e7ed27e60fec75e818894a08259699a4","path":"Sources/Sentry/SentryNetworkTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983581727c987fa4cca0954306818b12b7","path":"Sources/Sentry/include/SentryNoOpSpan.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9805654afa1f82958dd293c561ac9ab271","path":"Sources/Sentry/SentryNoOpSpan.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9833e522abc1d5b99c6068adbf830967c2","path":"Sources/Sentry/include/SentryNSDataSwizzling.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9833178f1b00328efc29f49ca21f6fa919","path":"Sources/Sentry/SentryNSDataSwizzling.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a32f1dc5a94f948c465e56c4010cf7e2","path":"Sources/Sentry/include/SentryNSDataTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984d57188a0ae1e5a026a97e63d1e37dd4","path":"Sources/Sentry/SentryNSDataTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987e9ef0b68a6618bff633d8257bbe4807","path":"Sources/Sentry/include/SentryNSDataUtils.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9846770a46bcf2b781d57ce3c925c8d341","path":"Sources/Sentry/SentryNSDataUtils.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981b65e1d892bcf2bcb3c5f20ec5972415","path":"Sources/Sentry/include/SentryNSDictionarySanitize.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986c8f20ef0729cf34cac9cba109e20051","path":"Sources/Sentry/SentryNSDictionarySanitize.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983337f11aeab3a289f5c8c19c3908b921","path":"Sources/Sentry/Public/SentryNSError.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984e9914a4b4293c692a90ba05b97965b5","path":"Sources/Sentry/SentryNSError.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9880ca6abb4fff5d7995dc2123497283ad","path":"Sources/Sentry/include/SentryNSNotificationCenterWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986f2b065fb1bca703459a4977185b8d05","path":"Sources/Sentry/SentryNSNotificationCenterWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98663c96b18dbadd2be6f08f5b8ea31c5e","path":"Sources/Sentry/include/SentryNSProcessInfoWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9894ae4b1a56d92b5ab6ed329af307db93","path":"Sources/Sentry/SentryNSProcessInfoWrapper.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9878f37430fba46c9dd937dbfac8cf691c","path":"Sources/Sentry/include/SentryNSTimerFactory.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98692460fbec7a54719df44466b20bcd96","path":"Sources/Sentry/SentryNSTimerFactory.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98680d2dc7ef64f30c303c18b4117568fc","path":"Sources/Sentry/include/SentryNSURLRequest.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e288c4e7a426b08b04b780e40e1f743f","path":"Sources/Sentry/SentryNSURLRequest.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c1a99b0cf864330bb01057e2bffc60fc","path":"Sources/Sentry/include/SentryNSURLRequestBuilder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d4217e23e64d40642d3a033d561f8d35","path":"Sources/Sentry/SentryNSURLRequestBuilder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989354f19ee7a39a29a3b6520194111f66","path":"Sources/Sentry/include/SentryNSURLSessionTaskSearch.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985cf2ec8edccb99946f64c83878c3c803","path":"Sources/Sentry/SentryNSURLSessionTaskSearch.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98537f34ec2613ecc57890ea95d1c7af35","path":"Sources/Sentry/include/SentryObjCRuntimeWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e987bcb2b2868ee46b3d212b5e9a35106d4","path":"Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989e50a47e4c378c019942525106400715","path":"Sources/Sentry/Public/SentryOptions.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98063d68f2c40f62f6810d0ed406bb382e","path":"Sources/Sentry/SentryOptions.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98eeec8be2c9ebaa5285c5472c3b36bf2e","path":"Sources/Sentry/include/HybridPublic/SentryOptions+HybridSDKs.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984bba6bc40fc60e061c7795ed9d14710d","path":"Sources/Sentry/include/SentryOptions+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984efbed9b76890e16efc0580a6c7f5776","path":"Sources/Sentry/include/SentryPerformanceTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989a15e09aff4d797481eb78d448d68934","path":"Sources/Sentry/SentryPerformanceTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981abbc71e31b4628810dbd6cd1779a6ab","path":"Sources/Sentry/include/SentryPerformanceTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d4b7a5dd39a2fec7b7ddc0ee48570ad5","path":"Sources/Sentry/SentryPerformanceTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e988b84a4cbbf82db3ee0783089fc6f3bba","path":"Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9855c159eb37097297bb381315fef4af83","path":"Sources/Sentry/include/SentryPredicateDescriptor.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d4338a3e8437f3b1d1acce2c48ddc4fb","path":"Sources/Sentry/SentryPredicateDescriptor.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cb932407441bd3d3ca9e06a6841edf04","path":"Sources/Sentry/include/SentryPrivate.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a5d56cb9cee92ba68a856453c30f353d","path":"Sources/Sentry/include/SentryProfiledTracerConcurrency.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e987291ecb593ccdba0e5fc6a98cbf5fdae","path":"Sources/Sentry/Profiling/SentryProfiledTracerConcurrency.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e989733858144eb7e903d25bb39b2aa1d69","path":"Sources/Sentry/SentryProfiler.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98023f426c062e3f41cac6b9ff2674eb7c","path":"Sources/Sentry/include/SentryProfiler+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981591d1e000f7e36a693757459cc651de","path":"Sources/Sentry/Profiling/SentryProfilerDefines.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9834c19d0343e8b716ae4009d27cd5343f","path":"Sources/Sentry/include/SentryProfilerSerialization.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98e9464d6a322ac39a174f4614fab171f4","path":"Sources/Sentry/Profiling/SentryProfilerSerialization.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986d6c0901f1b81ea1582095356892c849","path":"Sources/Sentry/Profiling/SentryProfilerSerialization+Test.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9849170326f9e7324f758fad5c8fda7c26","path":"Sources/Sentry/include/SentryProfilerState.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e988624d29c0763de3bcc218c7c4acf5c0e","path":"Sources/Sentry/Profiling/SentryProfilerState.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c97c721f87e21f23b35f49ab3480ddd9","path":"Sources/Sentry/include/SentryProfilerState+ObjCpp.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9803b8fbdce4e2ed80e49b511afbfdf7ba","path":"Sources/Sentry/include/SentryProfilerTestHelpers.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9857b858be807d990227d8cc8a467799eb","path":"Sources/Sentry/Profiling/SentryProfilerTestHelpers.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d57d7ff8850a7f68bc5127a2de78e1c6","path":"Sources/Sentry/include/SentryProfileTimeseries.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98cf09f39b101036d9039a5e9fdd5d4910","path":"Sources/Sentry/SentryProfileTimeseries.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980fa47d49e2bb65216878780f3a37d5f1","path":"Sources/Sentry/Public/SentryProfilingConditionals.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98dbbe1e35eb18685486a5c8b6577bbbe9","path":"Sources/Sentry/SentryPropagationContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9888a2658ee08672b053eb8cc3642a4e76","path":"Sources/Sentry/SentryPropagationContext.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989bdf26ad09027d76fb370df8816c6eb9","path":"Sources/Sentry/include/SentryQueueableRequestManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c2d294613ba90e126869f812c1ab6325","path":"Sources/Sentry/SentryQueueableRequestManager.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983454e6f421a0345f33e633ea310f2406","path":"Sources/Sentry/include/SentryRandom.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980f3118093a755d081ed6850275016cf3","path":"Sources/Sentry/SentryRandom.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a60f227949bd23fe3b07ff7b9601fc22","path":"Sources/Sentry/include/SentryRateLimitParser.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fe5ac5ab93a3da03e0e408b697c6a079","path":"Sources/Sentry/SentryRateLimitParser.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989d3798a7794bb34165683bfa0463151d","path":"Sources/Sentry/include/SentryRateLimits.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986a01eacf4fd9c5cbbfa18985456424be","path":"Sources/Sentry/include/SentryReachability.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d3daca73881c4a4b4874c14922476273","path":"Sources/Sentry/SentryReachability.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98b22a5ba1237fd87037d77d3641902363","path":"Sources/Swift/Protocol/SentryRedactOptions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9842a47bed079d580cd20989b9075294e3","path":"Sources/Swift/Integrations/SessionReplay/SentryReplayEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985726026a1a1c31e50325233986a3b382","path":"Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9805701d4fccb49684aa1b55a60f18fb3c","path":"Sources/Swift/Integrations/SessionReplay/SentryReplayRecording.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e989cc077d015a9f88874f161283b85d1af","path":"Sources/Swift/Integrations/SessionReplay/SentryReplayType.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9857bc555ad325fec19f683147a099c7e1","path":"Sources/Swift/Integrations/SessionReplay/SentryReplayVideoMaker.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98de5de5142f58314b7d12a2e669c41266","path":"Sources/Sentry/Public/SentryRequest.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9871b012a3b5f1462b9d197c7012741e34","path":"Sources/Sentry/SentryRequest.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985df69af6f60d3c7f31989a95e3f3a9e8","path":"Sources/Sentry/include/SentryRequestManager.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987657341f336ac02acfcf382525f1509c","path":"Sources/Sentry/include/SentryRequestOperation.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d263d257b6329d0a2dfee2e60a97fb5c","path":"Sources/Sentry/SentryRequestOperation.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987942c8ac2f032c0940a6a38ab91bf734","path":"Sources/Sentry/include/SentryRetryAfterHeaderParser.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ac9e44632d815a3c81252e6452f71035","path":"Sources/Sentry/SentryRetryAfterHeaderParser.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98f50457f417b0f79a0e1b0de834cf9aca","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebBreadcrumbEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98cb2f125ee7e524b3c4b954ba30af5d66","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebCustomEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e988d52ddcc81c8bca32b6adcfe81c6b681","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98078862f44140b3e99c42e7d1ab12699b","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebMetaEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9846929a8c54eb68f7ae47aba553065cd4","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebSpanEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98fbbaa747f79a3331a947c4a688bc013a","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebTouchEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98cb215688c9705038d49294cb6576f234","path":"Sources/Swift/Integrations/SessionReplay/RRWeb/SentryRRWebVideoEvent.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cf48b342ad07ad2abb993872033b47d0","path":"Sources/Sentry/include/SentrySample.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983cab1af79491ca23a25ea7c74d671896","path":"Sources/Sentry/Profiling/SentrySample.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984d00b5598fb7c97a8153da8d65037dfa","path":"Sources/Sentry/Public/SentrySampleDecision.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986c69e66e55629c2c42ad5d27ff70b570","path":"Sources/Sentry/SentrySampleDecision.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981142043015590766f312d36cdd419e39","path":"Sources/Sentry/include/SentrySampleDecision+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987eb65cfe85a754fabdf367fc17bec2d2","path":"Sources/Sentry/include/SentrySamplerDecision.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9894d865a928ae3975ad2746f118b4468b","path":"Sources/Sentry/SentrySamplerDecision.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ea213eb10687e172d1d3aded3bd41ae7","path":"Sources/Sentry/include/SentrySampling.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98662bd827119a1617f1274adce996caf7","path":"Sources/Sentry/SentrySampling.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987d47f3af45086a117a1cc24b2df9365a","path":"Sources/Sentry/Public/SentrySamplingContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989ff077c3356757e30d11f375da1ea1eb","path":"Sources/Sentry/SentrySamplingContext.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e981a82de91e8f2e5e3932cf0c7756ee711","path":"Sources/Sentry/SentrySamplingProfiler.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e985cd5b426c8a926a07fa8d251d5dab624","path":"Sources/Sentry/include/SentrySamplingProfiler.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988185ba6a88b996493cdf38ca379baabb","path":"Sources/Sentry/Public/SentryScope.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a51dc90ee182f3f234e31a6c3f115461","path":"Sources/Sentry/SentryScope.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98918a5c81a8e0b096accec29a8a6d8bb3","path":"Sources/Sentry/include/SentryScope+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e441058a2a5fac727c3f9be0e75a7e8e","path":"Sources/Sentry/include/SentryScopeObserver.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e98ff6e8cbde607cb070aaf521c95fb961a","path":"Sources/Sentry/SentryScopeSyncC.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9881d0f7b202c7e8a98e9936b29655c95d","path":"Sources/Sentry/include/SentryScopeSyncC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e989a369f24a3d33c353bfec1e7f64b3ea6","path":"Sources/Sentry/include/HybridPublic/SentryScreenFrames.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e46fe301c2548cc68a7e96ce8366b59a","path":"Sources/Sentry/SentryScreenFrames.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d09812498914ddbf97a6798491555ab6","path":"Sources/Sentry/include/SentryScreenshot.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e04f95ec1867f216e77231ddd1dda094","path":"Sources/Sentry/SentryScreenshot.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b1ff561b93fc4c5e149438256f956387","path":"Sources/Sentry/include/SentryScreenshotIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989e9f3ce88297e823291af52c5ad53475","path":"Sources/Sentry/SentryScreenshotIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9826acbc0b304b2a0ad2003d840178f0ff","path":"Sources/Sentry/Public/SentrySDK.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980e9d69924caebf6884c96f9ce74bc334","path":"Sources/Sentry/SentrySDK.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c7d38ced97e72f78e89610670b1b3fb3","path":"Sources/Sentry/include/SentrySDK+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984d5ae9898138c992b2021f68ec8babf6","path":"Sources/Sentry/include/SentrySdkInfo.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98faa847346efca28ffffb35376ea3a952","path":"Sources/Sentry/SentrySdkInfo.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cf616d9f6cd818fe3348d29c0c916aa2","path":"Sources/Sentry/Public/SentrySerializable.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e1dd28873beb5b2dcb1af5beb95d271b","path":"Sources/Sentry/include/SentrySerialization.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980d6f259ec16188737b09287bd283b367","path":"Sources/Sentry/SentrySerialization.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f0fafebc44b49d7ff8414673d3cef1c9","path":"Sources/Sentry/include/SentrySession.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980b8b57442bb9cab2603b9a999081af38","path":"Sources/Sentry/SentrySession.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e982116f2e7bb2c8e2ac20df990e0f81236","path":"Sources/Sentry/include/SentrySession+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983c8c985666c5352b6af3c7433239a020","path":"Sources/Sentry/include/SentrySessionCrashedHandler.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b12e7dd603ab333ae8345872f96182a8","path":"Sources/Sentry/SentrySessionCrashedHandler.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98928b60c2da7f7825875efa439d557e6b","path":"Sources/Swift/Protocol/SentrySessionListener.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9828b59ccaf8ff87aa8c8c271b9b5bf184","path":"Sources/Swift/Integrations/SessionReplay/SentrySessionReplay.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98aed94b002bc11a182b8826fa79b3c5e5","path":"Sources/Sentry/include/SentrySessionReplayIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98fa40304186e2c99ee5ccbfc8d92800fc","path":"Sources/Sentry/SentrySessionReplayIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e987cc66d7b02be2cb3d51c5934a525009a","path":"Sources/Sentry/include/SentrySessionReplayIntegration+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ada9c823043bb19207c45e2346ff5a6a","path":"Sources/Sentry/include/HybridPublic/SentrySessionReplayIntegration-Hybrid.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.c","guid":"bfdfe7dc352907fc980b868725387e9894dee7e19fea06bda4dbaaf5c1d9d35b","path":"Sources/Sentry/SentrySessionReplaySyncC.c","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9864e08ba6ae71bf1d1c7b269fd8a8fe0f","path":"Sources/Sentry/include/SentrySessionReplaySyncC.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e28c0aa12b28173b3abf435b1fa19a34","path":"Sources/Sentry/include/SentrySessionTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c0c10fbaa1cb5e98abbbef20d36e7b41","path":"Sources/Sentry/SentrySessionTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c6bf18b7cbf57a94d6c0d96399d10e5c","path":"Sources/Sentry/include/SentrySpan.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d09bbbc9887ced326bec2d6d7246dce1","path":"Sources/Sentry/SentrySpan.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98f21c40f0c349752b7f501e01c22fa48b","path":"Sources/Sentry/include/SentrySpan+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ecbbe5eadcd9001b4c2fc21e078f887a","path":"Sources/Sentry/Public/SentrySpanContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980572088b35705ae125400e5eaf1ca5c0","path":"Sources/Sentry/SentrySpanContext.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9853c456af0a27c58300581e4f45771983","path":"Sources/Sentry/include/SentrySpanContext+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c593d7d98c929c6a2b0f7d4d0589a1d4","path":"Sources/Sentry/Public/SentrySpanId.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9826f4fe2fca7fe75078c0e9e4e65a2d02","path":"Sources/Sentry/SentrySpanId.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980a3fbb743bd3ac9e28a18b2bd756f025","path":"Sources/Sentry/include/SentrySpanOperations.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9871bde8593bddf6bbbc9f5418fd2e33c4","path":"Sources/Sentry/Public/SentrySpanProtocol.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9878c897661c68cc4ff7868fa62493291e","path":"Sources/Sentry/Public/SentrySpanStatus.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c18eedf9441788560deccc91341f4ef1","path":"Sources/Sentry/SentrySpanStatus.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98786a47c67a0dc6ec0050a48c5a5e4e3d","path":"Sources/Sentry/include/SentrySpotlightTransport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98571366b1f9a87519f69133aa94ea1592","path":"Sources/Sentry/SentrySpotlightTransport.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e985987b898ae3639d21a5bfb374115d528","path":"Sources/Swift/Integrations/SessionReplay/SentrySRDefaultBreadcrumbConverter.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e98a1f775d7fcfd5f7d435fe7abffb91fb3","path":"Sources/Sentry/include/SentryStackBounds.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e98ea3cc80c8121c0454193a096db3b0641","path":"Sources/Sentry/include/SentryStackFrame.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988ebb06d6eb20ea09e0d5cce625106b6e","path":"Sources/Sentry/Public/SentryStacktrace.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9803eaef84f8cf151765e8f2dacff23bae","path":"Sources/Sentry/SentryStacktrace.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9863ffcb1f013b44034ba0bfda59953036","path":"Sources/Sentry/include/SentryStacktraceBuilder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985614a6bc35ea979167d2101aaf82ca72","path":"Sources/Sentry/SentryStacktraceBuilder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e70d13d7772d677d98ce5f06ab6a2bc7","path":"Sources/Sentry/include/SentryStatsdClient.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e988a1606defceb762419fa23b4bb125351","path":"Sources/Sentry/SentryStatsdClient.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e05ba937b597322372e39fc0358edb5f","path":"Sources/Sentry/include/SentrySubClassFinder.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989c1b2a8ac0bcddb420b0941ef640d0eb","path":"Sources/Sentry/SentrySubClassFinder.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e984d4ee0056f4fbde7672fb570961a8583","path":"Sources/Sentry/include/SentrySwift.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980bdb04ca4987d86965932ea3417f118b","path":"Sources/Sentry/include/SentrySwiftAsyncIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a09e471bf53c0e8d38c894fb2f23deda","path":"Sources/Sentry/SentrySwiftAsyncIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9864929f6d55b4bf658809924b262e5263","path":"Sources/Sentry/include/HybridPublic/SentrySwizzle.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d97ee0c09e9e900424b1603d10be7375","path":"Sources/Sentry/SentrySwizzle.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9877c9f1b3633c0e198960f68aa0c0810f","path":"Sources/Sentry/include/SentrySwizzleWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98711558a1dd411bb478cd26d8f3f745f2","path":"Sources/Sentry/SentrySwizzleWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98993f9b39cd9919f8ec83374ff13b1328","path":"Sources/Sentry/include/SentrySysctl.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e985deb0edc70afd30a8360f3dc8e856686","path":"Sources/Sentry/SentrySysctl.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ff36fbd967725834366ae2130fb2bed0","path":"Sources/Sentry/include/SentrySystemEventBreadcrumbs.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98a94a3a2ea869ff4381e8499b0d762c37","path":"Sources/Sentry/SentrySystemEventBreadcrumbs.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e988821b58f8dee309405f48b809d77c389","path":"Sources/Sentry/include/SentrySystemWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98e7b36496f0059bd09052fe0723a1482e","path":"Sources/Sentry/SentrySystemWrapper.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9846f8b71aa0d77e70aad6baf2780ede40","path":"Sources/Sentry/Public/SentryThread.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98333f641f00d2f2094339ae17ba284755","path":"Sources/Sentry/SentryThread.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e98f983405d6088b8487df98cbf1cf08289","path":"Sources/Sentry/SentryThreadHandle.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e9804a7db4ceccb6811912f7206f78e9447","path":"Sources/Sentry/include/SentryThreadHandle.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9862bc8fb6ad96b53f2b26b863c155ac1e","path":"Sources/Sentry/include/SentryThreadInspector.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9880761a78208e56b6a4707d7016a455cc","path":"Sources/Sentry/SentryThreadInspector.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.cpp","guid":"bfdfe7dc352907fc980b868725387e98bebf1492e8f631b28d50d71933f1372b","path":"Sources/Sentry/SentryThreadMetadataCache.cpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e9826a9f2e728b361a2543a40d1abedebd0","path":"Sources/Sentry/include/SentryThreadMetadataCache.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.h","guid":"bfdfe7dc352907fc980b868725387e98706ef18fd5b8c82cc56ff430d4229cb2","path":"Sources/Sentry/include/SentryThreadState.hpp","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980755b34b65dc8dfadeb1feee1a3cf958","path":"Sources/Sentry/include/SentryThreadWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e980b6ae641edc0eb8755e4ed4a2488c062","path":"Sources/Sentry/SentryThreadWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da71b3b7edd671b3d5c2cd01db469838","path":"Sources/Sentry/include/SentryTime.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e989800de374e76adb4a26b0bcddeecc2fd","path":"Sources/Sentry/SentryTime.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980cb9271b7f81c1a2d6b0f37452893839","path":"Sources/Sentry/include/SentryTimeToDisplayTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c7ad08268e0d328f29de3b10cd63ddc2","path":"Sources/Sentry/SentryTimeToDisplayTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e982a75db7baa0413a63ae20e85adb1d22b","path":"Sources/Swift/Integrations/SessionReplay/SentryTouchTracker.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a2a1ef82a610bc27d79d0c54c67af7f0","path":"Sources/Sentry/Public/SentryTraceContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9887cc63e57392bb0beca2e79621425813","path":"Sources/Sentry/SentryTraceContext.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9892f3522efd5a3f19de8e39f523c688d6","path":"Sources/Sentry/Public/SentryTraceHeader.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98df898a62101ed695fd27677ba8483559","path":"Sources/Sentry/SentryTraceHeader.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c6f2a07c2d050533d09cf4c390de1f8c","path":"Sources/Sentry/include/SentryTraceOrigins.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98b7d38aba29d59ffea50820df6a97c3d1","path":"Sources/Sentry/include/SentryTraceProfiler.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e98d882e4234aaaf8e0069c816fdd59f579","path":"Sources/Sentry/Profiling/SentryTraceProfiler.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981398c4eb84cdf497790fb1fe607e2e63","path":"Sources/Sentry/include/SentryTracer.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e982bf9fe4d6f2326a7fccee18a75faa388","path":"Sources/Sentry/SentryTracer.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98872ffad4b49352b651b261a73dad0060","path":"Sources/Sentry/include/SentryTracer+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9810cd37e42899add31fcc33f4bb739aac","path":"Sources/Sentry/include/SentryTracerConfiguration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e986e709c30b562ffcb2b1eda4a0ad426d4","path":"Sources/Sentry/SentryTracerConfiguration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9866a291b84196da3f36541b841f97bd0e","path":"Sources/Sentry/include/SentryTransaction.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98221eaf92859927540a5d4291e050542e","path":"Sources/Sentry/SentryTransaction.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98da808eab441d9e75217ff3d80a5e32a4","path":"Sources/Sentry/Public/SentryTransactionContext.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.cpp.objcpp","guid":"bfdfe7dc352907fc980b868725387e9841ff7a58c398688fb8514f02d4c41c49","path":"Sources/Sentry/SentryTransactionContext.mm","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98cb1d9988a5b4400c251fe18f3b1dd801","path":"Sources/Sentry/include/SentryTransactionContext+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9836194029038246f4ec8bf1410be089cc","path":"Sources/Swift/Integrations/Performance/SentryTransactionNameSource.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98e2f9ec322d655c0ea0950183b52221d8","path":"Sources/Sentry/include/SentryTransport.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d6a32bf0e617b57a20d2c33c46eb7914","path":"Sources/Sentry/include/SentryTransportAdapter.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e984be035c6e10d36e66be434b1c21a073b","path":"Sources/Sentry/SentryTransportAdapter.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98954fa85d06420d5ec52380486fd5cb33","path":"Sources/Sentry/include/SentryTransportFactory.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b676ca7f4497f4d1fe4138d1f5e18cf7","path":"Sources/Sentry/SentryTransportFactory.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e986a376fd26c1eb7097e69832b6f02467c","path":"Sources/Sentry/include/SentryUIApplication.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98ad8cadda5d19b22d7d5e391f3f2e5773","path":"Sources/Sentry/SentryUIApplication.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9894a25cc56626f69559a458a15ac02efd","path":"Sources/Sentry/include/SentryUIDeviceWrapper.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983ef575e6594a639f646ed7e15b622f05","path":"Sources/Sentry/SentryUIDeviceWrapper.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98d16e83a31b866dde2772c9ce54293036","path":"Sources/Sentry/include/SentryUIEventTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c7648303d42528f7384cd7e96635cb91","path":"Sources/Sentry/SentryUIEventTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98727154a288bb63516c37f191b7f989fc","path":"Sources/Sentry/include/SentryUIEventTrackerMode.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ec06dab8b33576c79bb7f8e7c442836d","path":"Sources/Sentry/include/SentryUIEventTrackerTransactionMode.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e4854f9e24894360225810635de170d4","path":"Sources/Sentry/SentryUIEventTrackerTransactionMode.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98521d00d65862188e87f09af5fdd2b143","path":"Sources/Sentry/include/SentryUIEventTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c9a689aa4dad67a6b1ca280f2f6ebea4","path":"Sources/Sentry/SentryUIEventTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980e3a99429d2074eae3ed0f01f406b861","path":"Sources/Sentry/include/SentryUIViewControllerPerformanceTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e9845042217e88518e3e855ac6610a6bd28","path":"Sources/Sentry/SentryUIViewControllerPerformanceTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9889e3af4a95fcd5340aa42b761c61304c","path":"Sources/Sentry/include/SentryUIViewControllerSwizzling.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e983176fb557a25d7b21f81988edd0f03bb","path":"Sources/Sentry/SentryUIViewControllerSwizzling.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9893fe513507e3ed2aee8e215050e2e0d7","path":"Sources/Sentry/Public/SentryUser.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e981599944e2dac3b6e693aef6d13475bc3","path":"Sources/Sentry/SentryUser.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98306548f7ff648626ef748253421486b8","path":"Sources/Sentry/include/HybridPublic/SentryUser+Private.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983d67248f1449eda03bef4a9ab05fe6a4","path":"Sources/Sentry/Public/SentryUserFeedback.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98aea6546754779f273d8124b565989345","path":"Sources/Sentry/SentryUserFeedback.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9821d0b241774df222e3b7377348f85f63","path":"Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980bb1557164f81c9e1bfbb7f8363a7867","path":"Sources/Sentry/include/SentryViewHierarchy.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98299ac7ba68df1b240a6b268ff9a86b76","path":"Sources/Sentry/SentryViewHierarchy.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980afc7a0cee689b8418b92fe22b0c3615","path":"Sources/Sentry/include/SentryViewHierarchyIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98b53e903d92672c16fb54d8ff0231cef9","path":"Sources/Sentry/SentryViewHierarchyIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98817181aab5ecfaf90b5b85e17c49dec8","path":"Sources/Swift/Tools/SentryViewPhotographer.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9889f837ea12a895bd661390ab3127aa6d","path":"Sources/Swift/Tools/SentryViewScreenshotProvider.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98831b777b6c8ddefb76a396274e08cf65","path":"Sources/Sentry/include/SentryWatchdogTerminationLogic.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98018b75d110252e6219e0ee545362ac37","path":"Sources/Sentry/SentryWatchdogTerminationLogic.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981b4d68ef1663be58c2c7d79a11afa4df","path":"Sources/Sentry/include/SentryWatchdogTerminationScopeObserver.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98364193a20b48ba275110ac3d6d5e9cda","path":"Sources/Sentry/SentryWatchdogTerminationScopeObserver.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98fd01427898ca74956b565fd99d02dc1c","path":"Sources/Sentry/include/SentryWatchdogTerminationTracker.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e987e91816c36c7362ce51491a1a85d80cd","path":"Sources/Sentry/SentryWatchdogTerminationTracker.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98780c1b9a14bc63cecf76b3990a59877b","path":"Sources/Sentry/include/SentryWatchdogTerminationTrackingIntegration.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98491b83da9ba737ee9f38229ad6e420d5","path":"Sources/Sentry/SentryWatchdogTerminationTrackingIntegration.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ed78e714a1400e941f04e4a22e3d5b2e","path":"Sources/Sentry/Public/SentryWithoutUIKit.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e981fe228c1e3eb9e98e34f508e02afeb4d","path":"Sources/Swift/Metrics/SetMetric.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98fa7dc020d34d966016e6329a66dd8e9f","path":"Sources/Swift/Extensions/StringExtensions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98d9c9b6d5fc746b0b6e694918a128dda3","path":"Sources/Swift/SwiftDescriptor.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98cb607a81a62d906f3b39c57798758ac5","path":"Sources/Swift/Tools/UIImageHelper.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e988bedd1aa102f8e0c2b086358fe8c4265","path":"Sources/Swift/Tools/UIRedactBuilder.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98199af03d7bbe972473ba296e49e9e28f","path":"Sources/Sentry/include/UIViewController+Sentry.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c67c9dd02046243d9509ed2cd4798160","path":"Sources/Sentry/UIViewController+Sentry.m","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98a9bdf6e3af1025f342b74ed83538e99f","path":"Sources/Swift/Extensions/UIViewExtensions.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9847561447b26da1538d79830f9f25d534","path":"Sources/Swift/Tools/UrlSanitized.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e0f7059daf81ae00dbaea4f94fd0940e","path":"Sources/Swift/Tools/URLSessionTaskHelper.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"text.xml","guid":"bfdfe7dc352907fc980b868725387e98720140f27bb1633c64c1b1da36ff7150","path":"Sources/Resources/PrivacyInfo.xcprivacy","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e984e8e4b6b4cdf0c8e787fc62391dd92fc","name":"Resources","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98fde7c7fa201b545fa7346c2dbc65d01e","name":"HybridSDK","path":"","sourceTree":"","type":"group"},{"children":[{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e98ef1bcc008e87a9a25e802f910bd3b338","path":"ResourceBundle-Sentry-Sentry-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98379c05280a321bf688329fe3aef3105b","path":"Sentry.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98743f2bd1841f8f09decb589fed2636c8","path":"Sentry-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e983b019ef6e78b06a8a837bc6d70936ca6","path":"Sentry-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98a244d1195fc83689d6ce3ffc5e36f6e2","path":"Sentry-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e983938971700c25e06de2a31eeb4ea8038","path":"Sentry-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98008eb440e134dedec2875572805c0b95","path":"Sentry.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9802292b204c9bad4ac7f8961219de2b07","path":"Sentry.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e986fb9ef72a20518718c75331fd4883cdd","name":"Support Files","path":"../Target Support Files/Sentry","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e982359f49a15cf54f5a5642b64cc3eb2e3","name":"Sentry","path":"Sentry","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98d16ccbe7206fb0e2ba6426ac4ec38dd9","path":"SwiftyGif/NSImage+SwiftyGif.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9889573bb4021c2391fbff0a825ae4178b","path":"SwiftyGif/NSImageView+SwiftyGif.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e9835fe91271911430f451a39e60aa1986c","path":"SwiftyGif/ObjcAssociatedWeakObject.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98ee06371f21d2935a5ee20d4d5bb4e44f","path":"SwiftyGif/SwiftyGif.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98e7350beaaae8ffbf946713feea0a4648","path":"SwiftyGif/SwiftyGifManager.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98595dbe4b870633814d56f32dc4618746","path":"SwiftyGif/UIImage+SwiftyGif.swift","sourceTree":"","type":"file"},{"fileType":"sourcecode.swift","guid":"bfdfe7dc352907fc980b868725387e98d770d4ce570ae939fbded8f93e00ff8e","path":"SwiftyGif/UIImageView+SwiftyGif.swift","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e9875483e8f93a2b38f9b1fe471ba33c3a7","path":"SwiftyGif.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98c6490049da5fff353f3196233e5aa86b","path":"SwiftyGif-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e988ad7a0fe1ae6306e9a821e4ed13fff44","path":"SwiftyGif-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e981915be642f44f92bbb0c663859b70dfe","path":"SwiftyGif-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e980626803cd8030a41b813ca8f28fffea0","path":"SwiftyGif-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ed5df08130fde981e5066c74824ca0ad","path":"SwiftyGif.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98be3854286ad572fdc485c3f4251ca09f","path":"SwiftyGif.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e98810193ceb3c555979788ed2a5c372602","name":"Support Files","path":"../Target Support Files/SwiftyGif","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e980ae2061b06cbc4928ef7854c13f38740","name":"SwiftyGif","path":"SwiftyGif","sourceTree":"","type":"group"},{"children":[{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98632304d423ab2bf91c956eac7a76a9c7","path":"Toast-Framework/Toast.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9890a726412a53fe90ba2295a50dfd22a2","path":"Toast/UIView+Toast.h","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e989752157b2cd16354797296f760ae1aef","path":"Toast/UIView+Toast.m","sourceTree":"","type":"file"},{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e989938462c628741b000a566d13b66d51d","path":"Toast.modulemap","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98e3d5685abb47d94d856240e992947bc4","path":"Toast-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e989038dc03edeabc512af644ec374031bf","path":"Toast-Info.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e985032ba3070912dc548c2a9e4e7a902a9","path":"Toast-prefix.pch","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e98c66dfb77b319009b7b9b119ff175df41","path":"Toast-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e9863fe2c666a46f95fbbe4c0f7414d7d8b","path":"Toast.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98ca2bbfca057495e362d7fe67afdfd6b9","path":"Toast.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e987533233d249f8595cd310d0355eed423","name":"Support Files","path":"../Target Support Files/Toast","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e984ea7b243f231f45db36c593b6182c199","name":"Toast","path":"Toast","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e988cfe4b62ae2e73f7f7be4602344a0f59","name":"Pods","path":"","sourceTree":"","type":"group"},{"guid":"bfdfe7dc352907fc980b868725387e9879aabb07c832697b70c102efdf5309db","name":"Products","path":"","sourceTree":"","type":"group"},{"children":[{"children":[{"fileType":"sourcecode.module-map","guid":"bfdfe7dc352907fc980b868725387e98bc9b33687cf974a825db433d5a4ce66f","path":"Pods-Runner.modulemap","sourceTree":"","type":"file"},{"fileType":"text","guid":"bfdfe7dc352907fc980b868725387e987edd064bed57826d8d017ae149a3d52e","path":"Pods-Runner-acknowledgements.markdown","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e987fb378bbcd6c5ed58c790a44f8de1ad8","path":"Pods-Runner-acknowledgements.plist","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.objc","guid":"bfdfe7dc352907fc980b868725387e98d314b0e0b0622c4b6805a741f2686226","path":"Pods-Runner-dummy.m","sourceTree":"","type":"file"},{"fileType":"text.script.sh","guid":"bfdfe7dc352907fc980b868725387e98c868b1c7c8b6f1d4f98ebdaeb296a675","path":"Pods-Runner-frameworks.sh","sourceTree":"","type":"file"},{"fileType":"text.plist.xml","guid":"bfdfe7dc352907fc980b868725387e986fd78883b66d5d9077a96975628773ce","path":"Pods-Runner-Info.plist","sourceTree":"","type":"file"},{"fileType":"text.script.sh","guid":"bfdfe7dc352907fc980b868725387e983a42af4f91479a2ff6a94702f9452725","path":"Pods-Runner-resources.sh","sourceTree":"","type":"file"},{"fileType":"sourcecode.c.h","guid":"bfdfe7dc352907fc980b868725387e9847c36cf1fe2165ccb62332bdeff26348","path":"Pods-Runner-umbrella.h","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e985f6ec3891a0be6525111807007bbcf76","path":"Pods-Runner.debug.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e988db807c74690161e03dc575ee7464945","path":"Pods-Runner.profile.xcconfig","sourceTree":"","type":"file"},{"fileType":"text.xcconfig","guid":"bfdfe7dc352907fc980b868725387e98778a51cd43710d278531fd4c4e5ed80f","path":"Pods-Runner.release.xcconfig","sourceTree":"","type":"file"}],"guid":"bfdfe7dc352907fc980b868725387e9863ac39dbb3c5e6697e1e483c96fdcf12","name":"Pods-Runner","path":"Target Support Files/Pods-Runner","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98d87df373aa945c7cffc56572b5b5c1d0","name":"Targets Support Files","path":"","sourceTree":"","type":"group"}],"guid":"bfdfe7dc352907fc980b868725387e98677e601b37074db53aff90e47c8f96d1","name":"Pods","path":"","sourceTree":"","type":"group"},"guid":"bfdfe7dc352907fc980b868725387e98","path":"/Users/morn/dev/samples/AppFlowy/frontend/appflowy_flutter/ios/Pods/Pods.xcodeproj","projectDirectory":"/Users/morn/dev/samples/AppFlowy/frontend/appflowy_flutter/ios/Pods","targets":["TARGET@v11_hash=8afac9aa7f2f7ae8a47d13626813e422","TARGET@v11_hash=dba02e4185326fadc962f9bcc306afff","TARGET@v11_hash=9ed764e6a36ac0463803ed04679a098b","TARGET@v11_hash=343abb3a1787caba848689df913b48cc","TARGET@v11_hash=18cc54ce823da0aaace1f19b54169b79","TARGET@v11_hash=455e18a547e744c268f0ab0be67b484f","TARGET@v11_hash=ce18edbb47580206399cf4783eec7ed5","TARGET@v11_hash=50777e38e58c4490a53094c0b174a83e","TARGET@v11_hash=be4bfd549192ab886b16634e611a5cfb","TARGET@v11_hash=bc8a844879af7a4b46ae41879f040474","TARGET@v11_hash=41f53d07703b6cdfcf27ef7c9560cf8c","TARGET@v11_hash=fe89e7ef4549c341d03d86fb3e6322bd","TARGET@v11_hash=8fb782e24ce265c815ff1b320853f917","TARGET@v11_hash=532913e38c7e06e5eea62089d1193ee4","TARGET@v11_hash=3c05d2ce5e305ec83a99f3301a5236aa","TARGET@v11_hash=a588ecdf5bfe2f1ad6c5d9a6bb9940f1","TARGET@v11_hash=c433bd69b99230b9785c1be714ce0175","TARGET@v11_hash=2438ab2bbd7e02a80b06e65e631e1ee9","TARGET@v11_hash=559c3084339e631943ea8fbb0ff14658","TARGET@v11_hash=78c419d7e36f388dac9ad87ec6534e43","TARGET@v11_hash=72545b20ee6d4e64d463a237167e469f","TARGET@v11_hash=7931e16ef4631bcfa5b05077cd140cef","TARGET@v11_hash=91393af516387dfbbafa2eb5029109fc","TARGET@v11_hash=c51f85455c2588dcd567c74a4396fcbf","TARGET@v11_hash=a1a63d5178cbcd2daae2e0cba9b032e5","TARGET@v11_hash=03dea4a492a969d9433ed28b4b2a0aec","TARGET@v11_hash=1dcf7cc21e4184e0f28a9789b4c382c9","TARGET@v11_hash=eaabb77f2569c0713fe5909f5362b3fa","TARGET@v11_hash=817de712cb6fac2be24baa7ec42aaf97","TARGET@v11_hash=fbd6377f91e5f0cc1620995c99b99ff0","TARGET@v11_hash=b5817aa8a8a5b233abd08d304efe013d","TARGET@v11_hash=86aab23948c9cd257baaff836f7414a1","TARGET@v11_hash=29ec1227ae80fa5e85545dce343417e5","TARGET@v11_hash=ab8b0fc009ec3b369e9ae605936ce603","TARGET@v11_hash=64039072b063670902e1ef354134e49d","TARGET@v11_hash=5449496bc380a949b05257eb8db9d316","TARGET@v11_hash=3cca9ca389e095b67ce3af588be9188d","TARGET@v11_hash=f78ac38fdf215d89c0281e470c44b101","TARGET@v11_hash=692a56120a7530dd608fbaa413d3d410","TARGET@v11_hash=bd3c446e66dacbac35fda866591052c9","TARGET@v11_hash=7d8a079c75bc93528df1276ff6c1a06e","TARGET@v11_hash=22014fcd8061c49ec1a7011011fa29d2","TARGET@v11_hash=0c4ac04efd08ba24acda74bf403c30fe","TARGET@v11_hash=6d6f324e26347bf163f3e2dcaa278075","TARGET@v11_hash=36e48dc34e49b20eaf26c3a4d1213a82","TARGET@v11_hash=583d53cd439ec89e8ee070a321e88f4e","TARGET@v11_hash=65477760b7bea77bb4f50c90f24afed5","TARGET@v11_hash=40f8368e8026f113aa896b4bd218efee","TARGET@v11_hash=78da11ebe0789216e22d2e6aaa220c0a"]} \ No newline at end of file diff --git a/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=9b6915bad2214bcc5eb58b855fe7b55a-json b/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=9b6915bad2214bcc5eb58b855fe7b55a-json deleted file mode 100644 index 0d35e5fa9a..0000000000 --- a/frontend/appflowy_flutter/macos/build/ios/XCBuildData/PIFCache/workspace/WORKSPACE@v11_hash=(null)_subobjects=9b6915bad2214bcc5eb58b855fe7b55a-json +++ /dev/null @@ -1 +0,0 @@ -{"guid":"dc4b70c03e8043e50e38f2068887b1d4","name":"Pods","path":"/Users/morn/dev/samples/AppFlowy/frontend/appflowy_flutter/ios/Pods/Pods.xcodeproj/project.xcworkspace","projects":["PROJECT@v11_mod=a7fbf46937053896f73cc7c7ec6baefb_hash=bfdfe7dc352907fc980b868725387e98plugins=1OJSG6M1FOV3XYQCBH7Z29RZ0FPR9XDE1"]} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_backend/example/lib/main.dart index a3fb8967a5..39759cfbff 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/lib/main.dart @@ -1,8 +1,7 @@ +import 'package:flutter/material.dart'; import 'dart:async'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:appflowy_backend/appflowy_backend.dart'; void main() { @@ -37,15 +36,21 @@ class _MyAppState extends State { // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; - setState(() => _platformVersion = platformVersion); + setState(() { + _platformVersion = platformVersion; + }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( - appBar: AppBar(title: const Text('Plugin example app')), - body: Center(child: Text('Running on: $_platformVersion\n')), + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), ), ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile index 012a925033..8c77835677 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.13' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj index b6c5559634..ac3debdf8e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 51; objects = { /* Begin PBXAggregateTarget section */ @@ -26,7 +26,11 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; + 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49D7864808B727FDFB82A4C2 /* Pods_Runner.framework */; }; + D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; + D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -46,6 +50,8 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, + 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -65,6 +71,7 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -73,6 +80,7 @@ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; A82DF8E6F43DF0AD4D0653DC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -80,6 +88,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, + 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, B7C2E82907836001B5A6F548 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -135,6 +145,8 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + D73912EF22F37F9E000D13A0 /* App.framework */, + 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -203,7 +215,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 0930; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -256,7 +268,6 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -270,7 +281,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -403,7 +414,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -486,7 +497,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -533,7 +544,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 758981e665..3d8205cf56 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_backend/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { return true } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index f69fd16927..9c1a1de27c 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -1,18 +1,15 @@ +export 'package:async/async.dart'; import 'dart:async'; import 'dart:convert'; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:isolate'; - -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/rust_stream.dart'; -import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; - +import 'dart:ffi'; import 'ffi.dart' as ffi; - -export 'package:async/async.dart'; +import 'package:ffi/ffi.dart'; +import 'dart:isolate'; +import 'dart:io'; +import 'package:logger/logger.dart'; enum ExceptionType { AppearanceSettingsIsEmpty, @@ -62,15 +59,27 @@ class RustLogStreamReceiver { late StreamController _streamController; late StreamSubscription _subscription; int get port => _ffiPort.sendPort.nativePort; + late Logger _logger; RustLogStreamReceiver._internal() { _ffiPort = RawReceivePort(); _streamController = StreamController(); _ffiPort.handler = _streamController.add; + _logger = Logger( + printer: PrettyPrinter( + methodCount: 0, // number of method calls to be displayed + errorMethodCount: 8, // number of method calls if stacktrace is provided + lineLength: 120, // width of the output + colors: false, // Colorful log messages + printEmojis: false, // Print an emoji for each log message + printTime: false, // Should each log print contain a timestamp + ), + level: kDebugMode ? Level.trace : Level.info, + ); _subscription = _streamController.stream.listen((data) { String decodedString = utf8.decode(data); - Log.info(decodedString); + _logger.i(decodedString); }); } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart index 12fdd60ccf..cd95941a15 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/dispatch.dart @@ -16,14 +16,15 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-search/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-storage/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-chat/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:ffi/ffi.dart'; import 'package:isolates/isolates.dart'; import 'package:isolates/ports.dart'; import 'package:protobuf/protobuf.dart'; +import '../protobuf/flowy-config/entities.pb.dart'; +import '../protobuf/flowy-config/event_map.pb.dart'; import '../protobuf/flowy-date/entities.pb.dart'; import '../protobuf/flowy-date/event_map.pb.dart'; @@ -33,10 +34,10 @@ part 'dart_event/flowy-folder/dart_event.dart'; part 'dart_event/flowy-user/dart_event.dart'; part 'dart_event/flowy-database2/dart_event.dart'; part 'dart_event/flowy-document/dart_event.dart'; +part 'dart_event/flowy-config/dart_event.dart'; part 'dart_event/flowy-date/dart_event.dart'; part 'dart_event/flowy-search/dart_event.dart'; -part 'dart_event/flowy-ai/dart_event.dart'; -part 'dart_event/flowy-storage/dart_event.dart'; +part 'dart_event/flowy-chat/dart_event.dart'; enum FFIException { RequestIsEmpty, @@ -72,9 +73,7 @@ Future> _extractPayload( case FFIStatusCode.Ok: return FlowySuccess(Uint8List.fromList(response.payload)); case FFIStatusCode.Err: - final errorBytes = Uint8List.fromList(response.payload); - GlobalErrorCodeNotifier.receiveErrorBytes(errorBytes); - return FlowyFailure(errorBytes); + return FlowyFailure(Uint8List.fromList(response.payload)); case FFIStatusCode.Internal: final error = utf8.decode(response.payload); Log.error("Dispatch internal error: $error"); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart index 639945f102..4019f6723f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/dispatch/error.dart @@ -1,8 +1,4 @@ -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/dart-ffi/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; -import 'package:flutter/foundation.dart'; class FlowyInternalError { late FFIStatusCode _statusCode; @@ -24,13 +20,15 @@ class FlowyInternalError { return "$_statusCode: $_error"; } - FlowyInternalError({ - required FFIStatusCode statusCode, - required String error, - }) { + FlowyInternalError( + {required FFIStatusCode statusCode, required String error}) { _statusCode = statusCode; _error = error; } + + factory FlowyInternalError.from(FFIResponse resp) { + return FlowyInternalError(statusCode: resp.code, error: ""); + } } class StackTraceError { @@ -50,70 +48,3 @@ class StackTraceError { return '${error.runtimeType}. Stack trace: $trace'; } } - -typedef void ErrorListener(); - -/// Receive error when Rust backend send error message back to the flutter frontend -/// -class GlobalErrorCodeNotifier extends ChangeNotifier { - // Static instance with lazy initialization - static final GlobalErrorCodeNotifier _instance = - GlobalErrorCodeNotifier._internal(); - - FlowyError? _error; - - // Private internal constructor - GlobalErrorCodeNotifier._internal(); - - // Factory constructor to return the same instance - factory GlobalErrorCodeNotifier() { - return _instance; - } - - static void receiveError(FlowyError error) { - if (_instance._error?.code != error.code) { - _instance._error = error; - _instance.notifyListeners(); - } - } - - static void receiveErrorBytes(Uint8List bytes) { - try { - final error = FlowyError.fromBuffer(bytes); - if (_instance._error?.code != error.code) { - _instance._error = error; - _instance.notifyListeners(); - } - } catch (e) { - Log.error("Can not parse error bytes: $e"); - } - } - - static ErrorListener add({ - required void Function(FlowyError error) onError, - bool Function(FlowyError code)? onErrorIf, - }) { - void listener() { - final error = _instance._error; - if (error != null) { - if (onErrorIf == null || onErrorIf(error)) { - onError(error); - } - } - } - - _instance.addListener(listener); - return listener; - } - - static void remove(ErrorListener listener) { - _instance.removeListener(listener); - } -} - -extension FlowyErrorExtension on FlowyError { - bool get isAIResponseLimitExceeded => - code == ErrorCode.AIResponseLimitExceeded; - - bool get isStorageLimitExceeded => code == ErrorCode.FileStorageLimitExceeded; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart index a1eb8947df..91c4149f64 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart @@ -2,7 +2,6 @@ import 'dart:ffi'; import 'dart:io'; - // ignore: import_of_legacy_library_into_null_safe import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart' as Foundation; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart index ce0a4e2248..dc79e655d1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/log.dart @@ -1,85 +1,48 @@ // ignore: import_of_legacy_library_into_null_safe import 'dart:ffi'; - import 'package:ffi/ffi.dart' as ffi; import 'package:flutter/foundation.dart'; -import 'package:talker/talker.dart'; +import 'package:logger/logger.dart'; import 'ffi.dart'; class Log { static final shared = Log(); - - late Talker _logger; - - bool enableFlutterLog = true; - - // used to disable log in tests - @visibleForTesting - bool disableLog = false; + // ignore: unused_field + late Logger _logger; Log() { - _logger = Talker( - filter: LogLevelTalkerFilter(), + _logger = Logger( + printer: PrettyPrinter( + methodCount: 2, // number of method calls to be displayed + errorMethodCount: 8, // number of method calls if stacktrace is provided + lineLength: 120, // width of the output + colors: true, // Colorful log messages + printEmojis: true, // Print an emoji for each log message + printTime: false, // Should each log print contain a timestamp + ), + level: kDebugMode ? Level.trace : Level.info, ); } - // Generic internal logging function to reduce code duplication - static void _log( - LogLevel level, - int rustLevel, - dynamic msg, [ - dynamic error, - StackTrace? stackTrace, - ]) { - // only forward logs to flutter in debug mode, otherwise log to rust to - // persist logs in the file system - if (shared.enableFlutterLog && kDebugMode) { - shared._logger.log(msg, logLevel: level, stackTrace: stackTrace); - } else { - String formattedMessage = _formatMessageWithStackTrace(msg, stackTrace); - rust_log(rustLevel, toNativeUtf8(formattedMessage)); - } - } - static void info(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; - } - - _log(LogLevel.info, 0, msg, error, stackTrace); + rust_log(0, toNativeUtf8(msg)); } static void debug(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; - } - - _log(LogLevel.debug, 1, msg, error, stackTrace); + rust_log(1, toNativeUtf8(msg)); } static void warn(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; - } - - _log(LogLevel.warning, 3, msg, error, stackTrace); + rust_log(3, toNativeUtf8(msg)); } static void trace(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; - } - - _log(LogLevel.verbose, 2, msg, error, stackTrace); + rust_log(2, toNativeUtf8(msg)); } static void error(dynamic msg, [dynamic error, StackTrace? stackTrace]) { - if (shared.disableLog) { - return; - } - - _log(LogLevel.error, 4, msg, error, stackTrace); + rust_log(4, toNativeUtf8(msg)); } } @@ -87,22 +50,6 @@ bool isReleaseVersion() { return kReleaseMode; } -// Utility to convert a message to native Utf8 (used in rust_log) Pointer toNativeUtf8(dynamic msg) { return "$msg".toNativeUtf8(); } - -String _formatMessageWithStackTrace(dynamic msg, StackTrace? stackTrace) { - if (stackTrace != null) { - return "$msg\nStackTrace:\n$stackTrace"; // Append the stack trace to the message - } - return msg.toString(); -} - -class LogLevelTalkerFilter implements TalkerFilter { - @override - bool filter(TalkerData data) { - // filter out the debug logs in release mode - return kDebugMode ? true : data.logLevel != LogLevel.debug; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml index 18aea4838b..c014da75df 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_backend/pubspec.yaml @@ -14,16 +14,20 @@ dependencies: ffi: ^2.0.2 isolates: ^3.0.3+8 protobuf: ^3.1.0 - talker: ^4.7.1 + freezed_annotation: + logger: ^2.0.0 plugin_platform_interface: ^2.1.3 + json_annotation: ^4.7.0 appflowy_result: path: ../appflowy_result - fixnum: ^1.1.0 - async: ^2.11.0 dev_dependencies: flutter_test: sdk: flutter + build_runner: + freezed: + flutter_lints: ^3.0.1 + json_serializable: ^6.6.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml index 75b15a0f70..a5744c1cfb 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/analysis_options.yaml @@ -1,60 +1,4 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. - include: package:flutter_lints/flutter.yaml -analyzer: - exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - - require_trailing_commas - - - prefer_collection_literals - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - - sized_box_for_whitespace - - use_decorated_box - - - unnecessary_parenthesis - - unnecessary_await_in_return - - unnecessary_raw_strings - - - avoid_unnecessary_containers - - avoid_redundant_argument_values - - avoid_unused_constructor_parameters - - - always_declare_return_types - - - sort_constructors_first - - unawaited_futures - - - prefer_single_quotes - # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options - -errors: - invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml index 75b15a0f70..61b6c4de17 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/analysis_options.yaml @@ -7,14 +7,8 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. - include: package:flutter_lints/flutter.yaml -analyzer: - exclude: - - "**/*.g.dart" - - "**/*.freezed.dart" - linter: # The lint rules applied to this project can be customized in the # section below to disable rules from the `package:flutter_lints/flutter.yaml` @@ -28,33 +22,8 @@ linter: # `// ignore_for_file: name_of_lint` syntax on the line or in the file # producing the lint. rules: - - require_trailing_commas - - - prefer_collection_literals - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - - sized_box_for_whitespace - - use_decorated_box - - - unnecessary_parenthesis - - unnecessary_await_in_return - - unnecessary_raw_strings - - - avoid_unnecessary_containers - - avoid_redundant_argument_values - - avoid_unused_constructor_parameters - - - always_declare_return_types - - - sort_constructors_first - - unawaited_futures - - - prefer_single_quotes + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options - -errors: - invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist index 7c56964006..8d4492f977 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 9.0 diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj index 2d5f6c6b7c..6edd238e7c 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ @@ -127,7 +127,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -171,12 +171,10 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -187,7 +185,6 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -275,7 +272,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -352,7 +349,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +398,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5e31d3d342..c87d15a335 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ CADisableMinimumFrameDurationOnPhone - UIApplicationSupportsIndirectInputEvents - diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart index 12bfd87ac3..fa9d3fe9aa 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/example_button.dart @@ -1,5 +1,5 @@ -import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; class PopoverMenu extends StatefulWidget { const PopoverMenu({super.key}); @@ -14,32 +14,43 @@ class _PopoverMenuState extends State { @override Widget build(BuildContext context) { return Material( - type: MaterialType.transparency, - child: Container( - width: 200, - height: 200, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(8)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.5), - spreadRadius: 5, - blurRadius: 7, - offset: const Offset(0, 3), // changes position of shadow - ), - ], - ), - child: ListView( - children: [ + type: MaterialType.transparency, + child: Container( + width: 200, + height: 200, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(8)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 5, + blurRadius: 7, + offset: const Offset(0, 3), // changes position of shadow + ), + ], + ), + child: ListView(children: [ Container( margin: const EdgeInsets.all(8), - child: const Text( - 'Popover', - style: TextStyle( - fontSize: 14, - color: Colors.black, - ), + child: const Text("Popover", + style: TextStyle( + fontSize: 14, + color: Colors.black, + fontStyle: null, + decoration: null)), + ), + Popover( + triggerActions: + PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + mutex: popOverMutex, + offset: const Offset(10, 0), + popupBuilder: (BuildContext context) { + return const PopoverMenu(); + }, + child: TextButton( + onPressed: () {}, + child: const Text("First"), ), ), Popover( @@ -47,62 +58,38 @@ class _PopoverMenuState extends State { PopoverTriggerFlags.hover | PopoverTriggerFlags.click, mutex: popOverMutex, offset: const Offset(10, 0), - asBarrier: true, - debugId: 'First', popupBuilder: (BuildContext context) { return const PopoverMenu(); }, child: TextButton( onPressed: () {}, - child: const Text('First'), + child: const Text("Second"), ), ), - Popover( - triggerActions: - PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - mutex: popOverMutex, - asBarrier: true, - debugId: 'Second', - offset: const Offset(10, 0), - popupBuilder: (BuildContext context) { - return const PopoverMenu(); - }, - child: TextButton( - onPressed: () {}, - child: const Text('Second'), - ), - ), - ], - ), - ), - ); + ]), + )); } } class ExampleButton extends StatelessWidget { + final String label; + final Offset? offset; + final PopoverDirection? direction; + const ExampleButton({ super.key, required this.label, - required this.direction, + this.direction, this.offset = Offset.zero, }); - final String label; - final Offset? offset; - final PopoverDirection direction; - @override Widget build(BuildContext context) { return Popover( triggerActions: PopoverTriggerFlags.click, - animationDuration: Durations.medium1, offset: offset, - direction: direction, - debugId: label, - child: TextButton( - child: Text(label), - onPressed: () {}, - ), + direction: direction ?? PopoverDirection.rightWithTopAligned, + child: TextButton(child: Text(label), onPressed: () {}), popupBuilder: (BuildContext context) { return const PopoverMenu(); }, diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart index 80c4dc6f72..f4e017aa8f 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart @@ -1,7 +1,6 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; - -import './example_button.dart'; +import "./example_button.dart"; void main() { runApp(const MyApp()); @@ -10,11 +9,21 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); + // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'AppFlowy Popover Example'), @@ -25,6 +34,15 @@ class MyApp extends StatelessWidget { class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + final String title; @override @@ -34,82 +52,79 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { @override Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), - body: const Padding( - padding: EdgeInsets.symmetric(horizontal: 48.0, vertical: 24.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ExampleButton( - label: 'Left top', - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithLeftAligned, - ), - ExampleButton( - label: 'Left Center', - offset: Offset(0, -10), - direction: PopoverDirection.rightWithCenterAligned, - ), - ExampleButton( - label: 'Left bottom', - offset: Offset(0, -10), - direction: PopoverDirection.topWithLeftAligned, - ), - ], + body: const Row( + children: [ + Column(children: [ + ExampleButton( + label: "Left top", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithLeftAligned, ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Expanded(child: SizedBox.shrink()), + ExampleButton( + label: "Left bottom", + offset: Offset(0, -10), + direction: PopoverDirection.topWithLeftAligned, + ), + ]), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ ExampleButton( - label: 'Top', + label: "Top", offset: Offset(0, 10), direction: PopoverDirection.bottomWithCenterAligned, ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ExampleButton( - label: 'Central', - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithCenterAligned, - ), - ], + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ExampleButton( + label: "Central", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithCenterAligned, + ), + ], + ), ), ExampleButton( - label: 'Bottom', + label: "Bottom", offset: Offset(0, -10), direction: PopoverDirection.topWithCenterAligned, ), ], ), - Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ExampleButton( - label: 'Right top', - offset: Offset(0, 10), - direction: PopoverDirection.bottomWithRightAligned, - ), - ExampleButton( - label: 'Right Center', - offset: Offset(0, 10), - direction: PopoverDirection.leftWithCenterAligned, - ), - ExampleButton( - label: 'Right bottom', - offset: Offset(0, -10), - direction: PopoverDirection.topWithRightAligned, - ), - ], - ), - ], - ), + ), + Column( + children: [ + ExampleButton( + label: "Right top", + offset: Offset(0, 10), + direction: PopoverDirection.bottomWithRightAligned, + ), + Expanded(child: SizedBox.shrink()), + ExampleButton( + label: "Right bottom", + offset: Offset(0, -10), + direction: PopoverDirection.topWithRightAligned, + ), + ], + ) + ], ), ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj index d9ae3f484a..c84862c675 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 51; objects = { /* Begin PBXAggregateTarget section */ @@ -182,7 +182,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -235,7 +235,6 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -345,7 +344,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -424,7 +423,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -471,7 +470,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.11; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 5b055a3a37..fb7259e177 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ =3.3.0 <4.0.0" + sdk: ">=2.17.0 <3.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart index 0b4208e6fc..925b9cad02 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/appflowy_popover.dart @@ -1,5 +1,5 @@ /// AppFlowyBoard library -library; +library appflowy_popover; export 'src/mutex.dart'; export 'src/popover.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart index 1a61851a71..ff54eaac61 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/follower.dart @@ -27,9 +27,7 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower { @override void updateRenderObject( - BuildContext context, - PopoverRenderFollowerLayer renderObject, - ) { + BuildContext context, PopoverRenderFollowerLayer renderObject) { final screenSize = MediaQuery.of(context).size; renderObject ..screenSize = screenSize @@ -42,6 +40,8 @@ class PopoverCompositedTransformFollower extends CompositedTransformFollower { } class PopoverRenderFollowerLayer extends RenderFollowerLayer { + Size screenSize; + PopoverRenderFollowerLayer({ required super.link, super.showWhenUnlinked = true, @@ -52,8 +52,6 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer { required this.screenSize, }); - Size screenSize; - @override void paint(PaintingContext context, Offset offset) { super.paint(context, offset); @@ -61,6 +59,13 @@ class PopoverRenderFollowerLayer extends RenderFollowerLayer { if (link.leader == null) { return; } + + if (link.leader!.offset.dx + link.leaderSize!.width + size.width > + screenSize.width) { + debugPrint("over flow"); + } + debugPrint( + "right: ${link.leader!.offset.dx + link.leaderSize!.width + size.width}, screen with: ${screenSize.width}"); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart index f297e901c8..85a6e326e2 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/layout.dart @@ -1,33 +1,20 @@ import 'dart:math' as math; - import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; - import './popover.dart'; class PopoverLayoutDelegate extends SingleChildLayoutDelegate { - PopoverLayoutDelegate({ - required this.link, - required this.direction, - required this.offset, - required this.windowPadding, - this.position, - this.showAtCursor = false, - }); - PopoverLink link; PopoverDirection direction; final Offset offset; final EdgeInsets windowPadding; - /// Required when [showAtCursor] is true. - /// - final Offset? position; - - /// If true, the popover will be shown at the cursor position. - /// This will ignore the [direction], and the child size. - /// - final bool showAtCursor; + PopoverLayoutDelegate({ + required this.link, + required this.direction, + required this.offset, + required this.windowPadding, + }); @override bool shouldRelayout(PopoverLayoutDelegate oldDelegate) { @@ -65,180 +52,262 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { maxHeight: constraints.maxHeight - windowPadding.top - windowPadding.bottom, ); + // assert(link.leaderSize != null); + // // if (link.leaderSize == null) { + // // return constraints.loosen(); + // // } + // final anchorRect = Rect.fromLTWH( + // link.leaderOffset!.dx, + // link.leaderOffset!.dy, + // link.leaderSize!.width, + // link.leaderSize!.height, + // ); + // BoxConstraints childConstraints; + // switch (direction) { + // case PopoverDirection.topLeft: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // anchorRect.top, + // )); + // break; + // case PopoverDirection.topRight: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.right, + // anchorRect.top, + // )); + // break; + // case PopoverDirection.bottomLeft: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // constraints.maxHeight - anchorRect.bottom, + // )); + // break; + // case PopoverDirection.bottomRight: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.right, + // constraints.maxHeight - anchorRect.bottom, + // )); + // break; + // case PopoverDirection.center: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth, + // constraints.maxHeight, + // )); + // break; + // case PopoverDirection.topWithLeftAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.left, + // anchorRect.top, + // )); + // break; + // case PopoverDirection.topWithCenterAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth, + // anchorRect.top, + // )); + // break; + // case PopoverDirection.topWithRightAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.right, + // anchorRect.top, + // )); + // break; + // case PopoverDirection.rightWithTopAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.right, + // constraints.maxHeight - anchorRect.top, + // )); + // break; + // case PopoverDirection.rightWithCenterAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.right, + // constraints.maxHeight, + // )); + // break; + // case PopoverDirection.rightWithBottomAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth - anchorRect.right, + // anchorRect.bottom, + // )); + // break; + // case PopoverDirection.bottomWithLeftAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // constraints.maxHeight - anchorRect.bottom, + // )); + // break; + // case PopoverDirection.bottomWithCenterAligned: + // childConstraints = BoxConstraints.loose(Size( + // constraints.maxWidth, + // constraints.maxHeight - anchorRect.bottom, + // )); + // break; + // case PopoverDirection.bottomWithRightAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.right, + // constraints.maxHeight - anchorRect.bottom, + // )); + // break; + // case PopoverDirection.leftWithTopAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // constraints.maxHeight - anchorRect.top, + // )); + // break; + // case PopoverDirection.leftWithCenterAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // constraints.maxHeight, + // )); + // break; + // case PopoverDirection.leftWithBottomAligned: + // childConstraints = BoxConstraints.loose(Size( + // anchorRect.left, + // anchorRect.bottom, + // )); + // break; + // case PopoverDirection.custom: + // childConstraints = constraints.loosen(); + // break; + // default: + // throw UnimplementedError(); + // } + // return childConstraints; } @override Offset getPositionForChild(Size size, Size childSize) { - final effectiveOffset = link.leaderOffset; - final leaderSize = link.leaderSize; - - if (effectiveOffset == null || leaderSize == null) { + if (link.leaderSize == null) { return Offset.zero; } - + final anchorRect = Rect.fromLTWH( + link.leaderOffset!.dx + offset.dx, + link.leaderOffset!.dy + offset.dy, + link.leaderSize!.width, + link.leaderSize!.height, + ); Offset position; - if (showAtCursor && this.position != null) { - position = this.position! + - Offset( - effectiveOffset.dx + offset.dx, - effectiveOffset.dy + offset.dy, - ); - } else { - final anchorRect = Rect.fromLTWH( - effectiveOffset.dx + offset.dx, - effectiveOffset.dy + offset.dy, - leaderSize.width, - leaderSize.height, - ); - - switch (direction) { - case PopoverDirection.topLeft: - position = Offset( - anchorRect.left - childSize.width, - anchorRect.top - childSize.height, - ); - break; - case PopoverDirection.topRight: - position = Offset( - anchorRect.right, - anchorRect.top - childSize.height, - ); - break; - case PopoverDirection.bottomLeft: - position = Offset( - anchorRect.left - childSize.width, - anchorRect.bottom, - ); - break; - case PopoverDirection.bottomRight: - position = Offset( - anchorRect.right, - anchorRect.bottom, - ); - break; - case PopoverDirection.center: - position = anchorRect.center; - break; - case PopoverDirection.topWithLeftAligned: - position = Offset( - anchorRect.left, - anchorRect.top - childSize.height, - ); - break; - case PopoverDirection.topWithCenterAligned: - position = Offset( - anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, - anchorRect.top - childSize.height, - ); - break; - case PopoverDirection.topWithRightAligned: - position = Offset( - anchorRect.right - childSize.width, - anchorRect.top - childSize.height, - ); - break; - case PopoverDirection.rightWithTopAligned: - position = Offset(anchorRect.right, anchorRect.top); - break; - case PopoverDirection.rightWithCenterAligned: - position = Offset( - anchorRect.right, - anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, - ); - break; - case PopoverDirection.rightWithBottomAligned: - position = Offset( - anchorRect.right, - anchorRect.bottom - childSize.height, - ); - break; - case PopoverDirection.bottomWithLeftAligned: - position = Offset( - anchorRect.left, - anchorRect.bottom, - ); - break; - case PopoverDirection.bottomWithCenterAligned: - position = Offset( - anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, - anchorRect.bottom, - ); - break; - case PopoverDirection.bottomWithRightAligned: - position = Offset( - anchorRect.right - childSize.width, - anchorRect.bottom, - ); - break; - case PopoverDirection.leftWithTopAligned: - position = Offset( - anchorRect.left - childSize.width, - anchorRect.top, - ); - break; - case PopoverDirection.leftWithCenterAligned: - position = Offset( - anchorRect.left - childSize.width, - anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, - ); - break; - case PopoverDirection.leftWithBottomAligned: - position = Offset( - anchorRect.left - childSize.width, - anchorRect.bottom - childSize.height, - ); - break; - default: - throw UnimplementedError(); - } + switch (direction) { + case PopoverDirection.topLeft: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top - childSize.height, + ); + break; + case PopoverDirection.topRight: + position = Offset( + anchorRect.right, + anchorRect.top - childSize.height, + ); + break; + case PopoverDirection.bottomLeft: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.bottom, + ); + break; + case PopoverDirection.bottomRight: + position = Offset( + anchorRect.right, + anchorRect.bottom, + ); + break; + case PopoverDirection.center: + position = anchorRect.center; + break; + case PopoverDirection.topWithLeftAligned: + position = Offset( + anchorRect.left, + anchorRect.top - childSize.height, + ); + break; + case PopoverDirection.topWithCenterAligned: + position = Offset( + anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, + anchorRect.top - childSize.height, + ); + break; + case PopoverDirection.topWithRightAligned: + position = Offset( + anchorRect.right - childSize.width, + anchorRect.top - childSize.height, + ); + break; + case PopoverDirection.rightWithTopAligned: + position = Offset(anchorRect.right, anchorRect.top); + break; + case PopoverDirection.rightWithCenterAligned: + position = Offset( + anchorRect.right, + anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, + ); + break; + case PopoverDirection.rightWithBottomAligned: + position = Offset( + anchorRect.right, + anchorRect.bottom - childSize.height, + ); + break; + case PopoverDirection.bottomWithLeftAligned: + position = Offset( + anchorRect.left, + anchorRect.bottom, + ); + break; + case PopoverDirection.bottomWithCenterAligned: + position = Offset( + anchorRect.left + anchorRect.width / 2.0 - childSize.width / 2.0, + anchorRect.bottom, + ); + break; + case PopoverDirection.bottomWithRightAligned: + position = Offset( + anchorRect.right - childSize.width, + anchorRect.bottom, + ); + break; + case PopoverDirection.leftWithTopAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top, + ); + break; + case PopoverDirection.leftWithCenterAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.top + anchorRect.height / 2.0 - childSize.height / 2.0, + ); + break; + case PopoverDirection.leftWithBottomAligned: + position = Offset( + anchorRect.left - childSize.width, + anchorRect.bottom - childSize.height, + ); + break; + default: + throw UnimplementedError(); } - return Offset( math.max( - windowPadding.left, - math.min( - windowPadding.left + size.width - childSize.width, - position.dx, - ), - ), + windowPadding.left, + math.min( + windowPadding.left + size.width - childSize.width, position.dx)), math.max( - windowPadding.top, - math.min( - windowPadding.top + size.height - childSize.height, - position.dy, - ), - ), - ); - } - - PopoverLayoutDelegate copyWith({ - PopoverLink? link, - PopoverDirection? direction, - Offset? offset, - EdgeInsets? windowPadding, - Offset? position, - bool? showAtCursor, - }) { - return PopoverLayoutDelegate( - link: link ?? this.link, - direction: direction ?? this.direction, - offset: offset ?? this.offset, - windowPadding: windowPadding ?? this.windowPadding, - position: position ?? this.position, - showAtCursor: showAtCursor ?? this.showAtCursor, + windowPadding.top, + math.min( + windowPadding.top + size.height - childSize.height, position.dy)), ); } } class PopoverTarget extends SingleChildRenderObjectWidget { + final PopoverLink link; const PopoverTarget({ super.key, super.child, required this.link, }); - final PopoverLink link; - @override PopoverTargetRenderBox createRenderObject(BuildContext context) { return PopoverTargetRenderBox( @@ -248,20 +317,14 @@ class PopoverTarget extends SingleChildRenderObjectWidget { @override void updateRenderObject( - BuildContext context, - PopoverTargetRenderBox renderObject, - ) { + BuildContext context, PopoverTargetRenderBox renderObject) { renderObject.link = link; } } class PopoverTargetRenderBox extends RenderProxyBox { - PopoverTargetRenderBox({ - required this.link, - RenderBox? child, - }) : super(child); - PopoverLink link; + PopoverTargetRenderBox({required this.link, RenderBox? child}) : super(child); @override bool get alwaysNeedsCompositing => true; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart index 8e740cb6d2..f68fe95445 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart @@ -3,94 +3,75 @@ import 'dart:collection'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flutter/material.dart'; -typedef _EntryMap = LinkedHashMap; +typedef EntryMap = LinkedHashMap; class RootOverlayEntry { - final _EntryMap _entries = _EntryMap(); - - bool contains(PopoverState state) => _entries.containsKey(state); - - bool get isEmpty => _entries.isEmpty; - bool get isNotEmpty => _entries.isNotEmpty; + final EntryMap _entries = EntryMap(); + RootOverlayEntry(); void addEntry( BuildContext context, - String id, PopoverState newState, OverlayEntry entry, bool asBarrier, - AnimationController animationController, ) { - _entries[newState] = OverlayEntryContext( - id, - entry, - newState, - asBarrier, - animationController, - ); + _entries[newState] = OverlayEntryContext(entry, newState, asBarrier); Overlay.of(context).insert(entry); } - void removeEntry(PopoverState state) { - final removedEntry = _entries.remove(state); + bool contains(PopoverState oldState) { + return _entries.containsKey(oldState); + } + + void removeEntry(PopoverState oldState) { + if (_entries.isEmpty) return; + + final removedEntry = _entries.remove(oldState); removedEntry?.overlayEntry.remove(); } - OverlayEntryContext? popEntry() { - if (isEmpty) { - return null; - } + bool get isEmpty => _entries.isEmpty; + + bool get isNotEmpty => _entries.isNotEmpty; + + bool hasEntry() { + return _entries.isNotEmpty; + } + + PopoverState? popEntry() { + if (_entries.isEmpty) return null; final lastEntry = _entries.values.last; _entries.remove(lastEntry.popoverState); - lastEntry.animationController.reverse().then((_) { - lastEntry.overlayEntry.remove(); - lastEntry.popoverState.widget.onClose?.call(); - }); + lastEntry.overlayEntry.remove(); + lastEntry.popoverState.widget.onClose?.call(); - return lastEntry.asBarrier ? lastEntry : popEntry(); - } - - bool isLastEntryAsBarrier() { - if (isEmpty) { - return false; + if (lastEntry.asBarrier) { + return lastEntry.popoverState; + } else { + return popEntry(); } - - return _entries.values.last.asBarrier; } } class OverlayEntryContext { + final bool asBarrier; + final PopoverState popoverState; + final OverlayEntry overlayEntry; + OverlayEntryContext( - this.id, this.overlayEntry, this.popoverState, this.asBarrier, - this.animationController, ); - - final String id; - final OverlayEntry overlayEntry; - final PopoverState popoverState; - final bool asBarrier; - final AnimationController animationController; - - @override - String toString() { - return 'OverlayEntryContext(id: $id, asBarrier: $asBarrier, popoverState: ${popoverState.widget.debugId})'; - } } class PopoverMask extends StatelessWidget { - const PopoverMask({ - super.key, - required this.onTap, - this.decoration, - }); - - final VoidCallback onTap; + final void Function() onTap; final Decoration? decoration; + const PopoverMask({super.key, required this.onTap, this.decoration}); + @override Widget build(BuildContext context) { return GestureDetector( diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart index be20803eba..8ab88e98e1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mutex.dart @@ -5,18 +5,22 @@ import 'popover.dart'; /// If multiple popovers are exclusive, /// pass the same mutex to them. class PopoverMutex { - PopoverMutex(); - final _PopoverStateNotifier _stateNotifier = _PopoverStateNotifier(); - - void addPopoverListener(VoidCallback listener) { - _stateNotifier.addListener(listener); - } + PopoverMutex(); void removePopoverListener(VoidCallback listener) { _stateNotifier.removeListener(listener); } + VoidCallback listenOnPopoverChanged(VoidCallback callback) { + listenerCallback() { + callback(); + } + + _stateNotifier.addListener(listenerCallback); + return listenerCallback; + } + void close() => _stateNotifier.state?.close(); PopoverState? get state => _stateNotifier.state; diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 7af2bdae48..405db2bee8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,5 +1,4 @@ import 'package:appflowy_popover/src/layout.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,16 +8,19 @@ import 'mutex.dart'; class PopoverController { PopoverState? _state; - void close() => _state?.close(); - void show() => _state?.showOverlay(); - void showAt(Offset position) => _state?.showOverlay(position); + void close() { + _state?.close(); + } + + void show() { + _state?.showOverlay(); + } } class PopoverTriggerFlags { static const int none = 0x00; static const int click = 0x01; static const int hover = 0x02; - static const int secondaryClick = 0x04; } enum PopoverDirection { @@ -52,35 +54,6 @@ enum PopoverClickHandler { } class Popover extends StatefulWidget { - const Popover({ - super.key, - required this.child, - required this.popupBuilder, - this.controller, - this.offset, - this.triggerActions = 0, - this.direction = PopoverDirection.rightWithTopAligned, - this.mutex, - this.windowPadding, - this.onOpen, - this.onClose, - this.canClose, - this.asBarrier = false, - this.clickHandler = PopoverClickHandler.listener, - this.skipTraversal = false, - this.animationDuration = const Duration(milliseconds: 200), - this.beginOpacity = 0.0, - this.endOpacity = 1.0, - this.beginScaleFactor = 1.0, - this.endScaleFactor = 1.0, - this.slideDistance = 5.0, - this.debugId, - this.maskDecoration = const BoxDecoration( - color: Color.fromARGB(0, 244, 67, 54), - ), - this.showAtCursor = false, - }); - final PopoverController? controller; /// The offset from the [child] where the popover will be drawn @@ -120,82 +93,115 @@ class Popover extends StatefulWidget { final bool skipTraversal; - /// Animation time of the popover. - final Duration animationDuration; - - /// The distance of the popover's slide animation. - final double slideDistance; - - /// The scale factor of the popover's scale animation. - final double beginScaleFactor; - final double endScaleFactor; - - /// The opacity of the popover's fade animation. - final double beginOpacity; - final double endOpacity; - - final String? debugId; - - /// Whether the popover should be shown at the cursor position. - /// - /// This only works when using [PopoverClickHandler.listener] as the click handler. - /// - /// Alternatively for having a normal popover, and use the cursor position only on - /// secondary click, consider showing the popover programatically with [PopoverController.showAt]. - /// - final bool showAtCursor; - /// The content area of the popover. final Widget child; + const Popover({ + super.key, + required this.child, + required this.popupBuilder, + this.controller, + this.offset, + this.maskDecoration = const BoxDecoration( + color: Color.fromARGB(0, 244, 67, 54), + ), + this.triggerActions = 0, + this.direction = PopoverDirection.rightWithTopAligned, + this.mutex, + this.windowPadding, + this.onOpen, + this.onClose, + this.canClose, + this.asBarrier = false, + this.clickHandler = PopoverClickHandler.listener, + this.skipTraversal = false, + }); + @override State createState() => PopoverState(); } -class PopoverState extends State with SingleTickerProviderStateMixin { - static final RootOverlayEntry rootEntry = RootOverlayEntry(); - +class PopoverState extends State { + static final RootOverlayEntry _rootEntry = RootOverlayEntry(); final PopoverLink popoverLink = PopoverLink(); - late PopoverLayoutDelegate layoutDelegate = PopoverLayoutDelegate( - direction: widget.direction, - link: popoverLink, - offset: widget.offset ?? Offset.zero, - windowPadding: widget.windowPadding ?? EdgeInsets.zero, - ); - - late AnimationController animationController; - late Animation fadeAnimation; - late Animation scaleAnimation; - late Animation slideAnimation; - - // If the widget is disposed, prevent the animation from being called. - bool isDisposed = false; - - Offset? cursorPosition; @override void initState() { super.initState(); - widget.controller?._state = this; - _buildAnimations(); + } + + void showOverlay() { + close(); + + if (widget.mutex != null) { + widget.mutex?.state = this; + } + final shouldAddMask = _rootEntry.isEmpty; + final newEntry = OverlayEntry(builder: (context) { + final children = []; + if (shouldAddMask) { + children.add( + PopoverMask( + decoration: widget.maskDecoration, + onTap: () async { + if (!(await widget.canClose?.call() ?? true)) { + return; + } + _removeRootOverlay(); + }, + ), + ); + } + + children.add( + PopoverContainer( + direction: widget.direction, + popoverLink: popoverLink, + offset: widget.offset ?? Offset.zero, + windowPadding: widget.windowPadding ?? EdgeInsets.zero, + popupBuilder: widget.popupBuilder, + onClose: close, + onCloseAll: _removeRootOverlay, + skipTraversal: widget.skipTraversal, + ), + ); + + return CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay, + }, + child: FocusScope(child: Stack(children: children)), + ); + }); + _rootEntry.addEntry(context, this, newEntry, widget.asBarrier); + } + + void close({ + bool notify = true, + }) { + if (_rootEntry.contains(this)) { + _rootEntry.removeEntry(this); + if (notify) { + widget.onClose?.call(); + } + } + } + + void _removeRootOverlay() { + _rootEntry.popEntry(); + + if (widget.mutex?.state == this) { + widget.mutex?.removeState(); + } } @override void deactivate() { close(notify: false); - super.deactivate(); } - @override - void dispose() { - isDisposed = true; - animationController.dispose(); - - super.dispose(); - } - @override Widget build(BuildContext context) { return PopoverTarget( @@ -204,324 +210,75 @@ class PopoverState extends State with SingleTickerProviderStateMixin { ); } - @override - void reassemble() { - // clear the overlay - while (rootEntry.isNotEmpty) { - rootEntry.popEntry(); - } - - super.reassemble(); - } - - void showOverlay([Offset? position]) { - close(withAnimation: true); - - if (widget.mutex != null) { - widget.mutex?.state = this; - } - - if (position != null) { - final RenderBox? renderBox = context.findRenderObject() as RenderBox?; - final offset = renderBox?.globalToLocal(position); - layoutDelegate = layoutDelegate.copyWith( - position: offset ?? position, - windowPadding: EdgeInsets.zero, - showAtCursor: true, - ); - } - - final shouldAddMask = rootEntry.isEmpty; - rootEntry.addEntry( - context, - widget.debugId ?? '', - this, - OverlayEntry(builder: (_) => _buildOverlayContent(shouldAddMask)), - widget.asBarrier, - animationController, - ); - - if (widget.animationDuration != Duration.zero) { - animationController.forward(); - } - } - - void close({ - bool notify = true, - bool withAnimation = false, - }) { - if (rootEntry.contains(this)) { - void callback() { - rootEntry.removeEntry(this); - if (notify) { - widget.onClose?.call(); - } - } - - if (isDisposed || - !withAnimation || - widget.animationDuration == Duration.zero) { - callback(); - } else { - animationController.reverse().then((_) => callback()); - } - } - } - - void _removeRootOverlay() { - rootEntry.popEntry(); - - if (widget.mutex?.state == this) { - widget.mutex?.removeState(); - } - } - Widget _buildChild(BuildContext context) { - Widget child = widget.child; - if (widget.triggerActions == 0) { - return child; + return widget.child; } - child = _buildClickHandler( - child, - () { - widget.onOpen?.call(); - if (widget.triggerActions & PopoverTriggerFlags.none != 0) { - return; + return MouseRegion( + onEnter: (event) { + if (widget.triggerActions & PopoverTriggerFlags.hover != 0) { + showOverlay(); } - - showOverlay(cursorPosition); }, + child: _buildClickHandler( + widget.child, + () { + widget.onOpen?.call(); + if (widget.triggerActions & PopoverTriggerFlags.click != 0) { + showOverlay(); + } + }, + ), ); - - if (widget.triggerActions & PopoverTriggerFlags.hover != 0) { - child = MouseRegion( - onEnter: (event) => showOverlay(), - child: child, - ); - } - - return child; } Widget _buildClickHandler(Widget child, VoidCallback handler) { - return switch (widget.clickHandler) { - PopoverClickHandler.listener => Listener( - onPointerDown: (event) { - cursorPosition = widget.showAtCursor ? event.position : null; - - if (event.buttons == kSecondaryMouseButton && - widget.triggerActions & PopoverTriggerFlags.secondaryClick != - 0) { - return _callHandler(handler); - } - - if (event.buttons == kPrimaryMouseButton && - widget.triggerActions & PopoverTriggerFlags.click != 0) { - return _callHandler(handler); - } - }, + switch (widget.clickHandler) { + case PopoverClickHandler.listener: + return Listener( + onPointerDown: (_) => _callHandler(handler), child: child, - ), - PopoverClickHandler.gestureDetector => GestureDetector( - onTap: () { - if (widget.triggerActions & PopoverTriggerFlags.click != 0) { - return _callHandler(handler); - } - }, - onSecondaryTap: () { - if (widget.triggerActions & PopoverTriggerFlags.secondaryClick != - 0) { - return _callHandler(handler); - } - }, + ); + case PopoverClickHandler.gestureDetector: + return GestureDetector( + onTap: () => _callHandler(handler), child: child, - ), - }; + ); + } } void _callHandler(VoidCallback handler) { - if (rootEntry.contains(this)) { + if (_rootEntry.contains(this)) { close(); } else { handler(); } } - - Widget _buildOverlayContent(bool shouldAddMask) { - return CallbackShortcuts( - bindings: { - const SingleActivator(LogicalKeyboardKey.escape): _removeRootOverlay, - }, - child: FocusScope( - child: Stack( - children: [ - if (shouldAddMask) _buildMask(), - _buildPopoverContainer(), - ], - ), - ), - ); - } - - Widget _buildMask() { - return PopoverMask( - decoration: widget.maskDecoration, - onTap: () async { - if (await widget.canClose?.call() ?? true) { - _removeRootOverlay(); - } - }, - ); - } - - Widget _buildPopoverContainer() { - Widget child = PopoverContainer( - delegate: layoutDelegate, - popupBuilder: widget.popupBuilder, - skipTraversal: widget.skipTraversal, - onClose: close, - onCloseAll: _removeRootOverlay, - ); - - if (widget.animationDuration != Duration.zero) { - child = AnimatedBuilder( - animation: animationController, - builder: (_, child) => Opacity( - opacity: fadeAnimation.value, - child: Transform.scale( - scale: scaleAnimation.value, - child: Transform.translate( - offset: slideAnimation.value, - child: child, - ), - ), - ), - child: child, - ); - } - - return child; - } - - void _buildAnimations() { - animationController = AnimationController( - duration: widget.animationDuration, - vsync: this, - ); - fadeAnimation = _buildFadeAnimation(); - scaleAnimation = _buildScaleAnimation(); - slideAnimation = _buildSlideAnimation(); - } - - Animation _buildFadeAnimation() { - return Tween( - begin: widget.beginOpacity, - end: widget.endOpacity, - ).animate( - CurvedAnimation( - parent: animationController, - curve: Curves.easeInOut, - ), - ); - } - - Animation _buildScaleAnimation() { - return Tween( - begin: widget.beginScaleFactor, - end: widget.endScaleFactor, - ).animate( - CurvedAnimation( - parent: animationController, - curve: Curves.easeInOut, - ), - ); - } - - Animation _buildSlideAnimation() { - final values = _getSlideAnimationValues(); - return Tween( - begin: values.$1, - end: values.$2, - ).animate( - CurvedAnimation( - parent: animationController, - curve: Curves.linear, - ), - ); - } - - (Offset, Offset) _getSlideAnimationValues() { - final slideDistance = widget.slideDistance; - - switch (widget.direction) { - case PopoverDirection.bottomWithLeftAligned: - return ( - Offset(-slideDistance, -slideDistance), - Offset.zero, - ); - case PopoverDirection.bottomWithCenterAligned: - return ( - Offset(0, -slideDistance), - Offset.zero, - ); - case PopoverDirection.bottomWithRightAligned: - return ( - Offset(slideDistance, -slideDistance), - Offset.zero, - ); - case PopoverDirection.topWithLeftAligned: - return ( - Offset(-slideDistance, slideDistance), - Offset.zero, - ); - case PopoverDirection.topWithCenterAligned: - return ( - Offset(0, slideDistance), - Offset.zero, - ); - case PopoverDirection.topWithRightAligned: - return ( - Offset(slideDistance, slideDistance), - Offset.zero, - ); - case PopoverDirection.leftWithTopAligned: - case PopoverDirection.leftWithCenterAligned: - case PopoverDirection.leftWithBottomAligned: - return ( - Offset(slideDistance, 0), - Offset.zero, - ); - case PopoverDirection.rightWithTopAligned: - case PopoverDirection.rightWithCenterAligned: - case PopoverDirection.rightWithBottomAligned: - return ( - Offset(-slideDistance, 0), - Offset.zero, - ); - default: - return (Offset.zero, Offset.zero); - } - } } class PopoverContainer extends StatefulWidget { + final Widget? Function(BuildContext context) popupBuilder; + final PopoverDirection direction; + final PopoverLink popoverLink; + final Offset offset; + final EdgeInsets windowPadding; + final void Function() onClose; + final void Function() onCloseAll; + final bool skipTraversal; + const PopoverContainer({ super.key, required this.popupBuilder, - required this.delegate, + required this.direction, + required this.popoverLink, + required this.offset, + required this.windowPadding, required this.onClose, required this.onCloseAll, required this.skipTraversal, }); - final Widget? Function(BuildContext context) popupBuilder; - final void Function() onClose; - final void Function() onCloseAll; - final bool skipTraversal; - final PopoverLayoutDelegate delegate; - @override State createState() => PopoverContainerState(); @@ -529,7 +286,9 @@ class PopoverContainer extends StatefulWidget { if (context is StatefulElement && context.state is PopoverContainerState) { return context.state as PopoverContainerState; } - return context.findAncestorStateOfType()!; + final PopoverContainerState? result = + context.findAncestorStateOfType(); + return result!; } static PopoverContainerState? maybeOf(BuildContext context) { @@ -547,7 +306,12 @@ class PopoverContainerState extends State { autofocus: true, skipTraversal: widget.skipTraversal, child: CustomSingleChildLayout( - delegate: widget.delegate, + delegate: PopoverLayoutDelegate( + direction: widget.direction, + link: widget.popoverLink, + offset: widget.offset, + windowPadding: widget.windowPadding, + ), child: widget.popupBuilder(context), ), ); diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml index 5d6e335621..9dd11b27ac 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml @@ -4,8 +4,8 @@ version: 0.0.1 homepage: https://appflowy.io environment: - flutter: ">=3.22.0" - sdk: ">=3.3.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.1" dependencies: flutter: @@ -14,4 +14,41 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 + flutter_lints: ^3.0.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart index d91c9e4954..97b81cfe1a 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/appflowy_result.dart @@ -1,5 +1,4 @@ -/// AppFlowyPopover library -library; +library appflowy_result; export 'src/async_result.dart'; export 'src/result.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart index e8d3be8d90..eca6726b9e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart @@ -15,8 +15,8 @@ abstract class FlowyResult { S? toNullable(); - T? onSuccess(T? Function(S s) onSuccess); - T? onFailure(T? Function(F f) onFailure); + void onSuccess(void Function(S s) onSuccess); + void onFailure(void Function(F f) onFailure); S getOrElse(S Function(F failure) onFailure); S getOrThrow(); @@ -70,14 +70,12 @@ class FlowySuccess implements FlowyResult { } @override - T? onSuccess(T? Function(S success) onSuccess) { - return onSuccess(_value); + void onSuccess(void Function(S success) onSuccess) { + onSuccess(_value); } @override - T? onFailure(T? Function(F failure) onFailure) { - return null; - } + void onFailure(void Function(F failure) onFailure) {} @override S getOrElse(S Function(F failure) onFailure) { @@ -141,13 +139,11 @@ class FlowyFailure implements FlowyResult { } @override - T? onSuccess(T? Function(S success) onSuccess) { - return null; - } + void onSuccess(void Function(S success) onSuccess) {} @override - T? onFailure(T? Function(F failure) onFailure) { - return onFailure(_value); + void onFailure(void Function(F failure) onFailure) { + onFailure(_value); } @override diff --git a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml index 5d8f0d88c2..241f437d9b 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_result/pubspec.yaml @@ -1,11 +1,54 @@ name: appflowy_result description: "A new Flutter package project." version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: environment: - sdk: ">=3.3.0 <4.0.0" + sdk: '>=3.3.0 <4.0.0' flutter: ">=1.17.0" +dependencies: + flutter: + sdk: flutter + dev_dependencies: + flutter_test: + sdk: flutter flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore deleted file mode 100644 index da0bb7ce97..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -build/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/.metadata deleted file mode 100644 index 79932b61d5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" - channel: "[user-branch]" - -project_type: package diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md b/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md deleted file mode 100644 index 41cc7d8192..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE b/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE deleted file mode 100644 index ba75c69f7f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/README.md deleted file mode 100644 index 953d3545f1..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# AppFlowy UI - -AppFlowy UI is a Flutter package that provides a collection of reusable UI components following the AppFlowy design system. These components are designed to be consistent, accessible, and easy to use. - -## Features - -- **Design System Components**: Buttons, text fields, and more UI components that follow the AppFlowy design system -- **Theming**: Consistent theming across all components with light and dark mode support - -## Installation - -Add the following to your `pubspec.yaml` file: - -```yaml -dependencies: - appflowy_ui: ^1.0.0 -``` - -## Supported components - -- [x] Button -- [x] TextField -- [ ] Avatar -- [ ] Checkbox -- [ ] Grid -- [ ] Link -- [ ] Loading & Progress Indicator -- [ ] Menu -- [ ] Message Box -- [ ] Navigation Bar -- [ ] Popover -- [ ] Scroll Bar -- [ ] Tab Bar -- [ ] Toggle -- [ ] Tooltip - -## Reference - -Figma: https://www.figma.com/design/aphWa2OgkqyIragpatdk7a/Design-System diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml deleted file mode 100644 index abba19b4fe..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/analysis_options.yaml +++ /dev/null @@ -1,29 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -linter: - rules: - - require_trailing_commas - - - prefer_collection_literals - - prefer_final_fields - - prefer_final_in_for_each - - prefer_final_locals - - - sized_box_for_whitespace - - use_decorated_box - - - unnecessary_parenthesis - - unnecessary_await_in_return - - unnecessary_raw_strings - - - avoid_unnecessary_containers - - avoid_redundant_argument_values - - avoid_unused_constructor_parameters - - - always_declare_return_types - - - sort_constructors_first - - unawaited_futures - -errors: - invalid_annotation_target: ignore diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore deleted file mode 100644 index 79c113f9b5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata b/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata deleted file mode 100644 index 777c932a64..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" - channel: "[user-branch]" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - platform: macos - create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md b/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md deleted file mode 100644 index 2ccc9e658d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# AppFlowy UI Example - -This example demonstrates how to use the `appflowy_ui` package in a Flutter application. - -## Getting Started - -To run this example: - -1. Ensure you have Flutter installed and set up on your machine -2. Clone this repository -3. Navigate to the example directory: - ```bash - cd example - ``` -4. Get the dependencies: - ```bash - flutter pub get - ``` -5. Run the example: - ```bash - flutter run - ``` - -## Features Demonstrated - -- Basic app structure using AppFlowy UI components -- Material 3 design integration -- Responsive layout - -## Project Structure - -- `lib/main.dart`: The main application file -- `pubspec.yaml`: Project dependencies and configuration - -## Additional Resources - -For more information about the AppFlowy UI package, please refer to: - -- The main package documentation -- [AppFlowy Website](https://appflowy.io) -- [AppFlowy GitHub Repository](https://github.com/AppFlowy-IO/AppFlowy) diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml deleted file mode 100644 index 0d2902135c..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart deleted file mode 100644 index 0d23746ebd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/main.dart +++ /dev/null @@ -1,117 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import 'src/buttons/buttons_page.dart'; -import 'src/modal/modal_page.dart'; -import 'src/textfield/textfield_page.dart'; - -enum ThemeMode { - light, - dark, -} - -final themeMode = ValueNotifier(ThemeMode.light); - -void main() { - runApp( - const MyApp(), - ); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: themeMode, - builder: (context, themeMode, child) { - final themeBuilder = AppFlowyDefaultTheme(); - final themeData = - themeMode == ThemeMode.light ? ThemeData.light() : ThemeData.dark(); - - return AnimatedAppFlowyTheme( - data: themeMode == ThemeMode.light - ? themeBuilder.light() - : themeBuilder.dark(), - child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'AppFlowy UI Example', - theme: themeData.copyWith( - visualDensity: VisualDensity.standard, - ), - home: const MyHomePage( - title: 'AppFlowy UI', - ), - ), - ); - }, - ); - } -} - -class MyHomePage extends StatefulWidget { - const MyHomePage({ - super.key, - required this.title, - }); - - final String title; - - @override - State createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - final tabs = [ - Tab(text: 'Button'), - Tab(text: 'TextField'), - Tab(text: 'Modal'), - ]; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return DefaultTabController( - length: tabs.length, - child: Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: Text( - widget.title, - style: theme.textStyle.title.enhanced( - color: theme.textColorScheme.primary, - ), - ), - actions: [ - IconButton( - icon: Icon( - Theme.of(context).brightness == Brightness.light - ? Icons.dark_mode - : Icons.light_mode, - ), - onPressed: _toggleTheme, - tooltip: 'Toggle theme', - ), - ], - ), - body: TabBarView( - children: [ - ButtonsPage(), - TextFieldPage(), - ModalPage(), - ], - ), - bottomNavigationBar: TabBar( - tabs: tabs, - ), - floatingActionButton: null, - ), - ); - } - - void _toggleTheme() { - themeMode.value = - themeMode.value == ThemeMode.light ? ThemeMode.dark : ThemeMode.light; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart deleted file mode 100644 index 0d0c018222..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/buttons/buttons_page.dart +++ /dev/null @@ -1,287 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ButtonsPage extends StatelessWidget { - const ButtonsPage({super.key}); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - 'Filled Text Buttons', - [ - AFFilledTextButton.primary( - text: 'Primary Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.destructive( - text: 'Destructive Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Filled Icon Text Buttons', - [ - AFFilledButton.primary( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.add, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - const SizedBox(width: 8), - Text( - 'Primary Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFFilledButton.destructive( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - const SizedBox(width: 8), - Text( - 'Destructive Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFFilledButton.disabled( - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.block, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - 'Disabled Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Outlined Text Buttons', - [ - AFOutlinedTextButton.normal( - text: 'Normal Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFOutlinedTextButton.destructive( - text: 'Destructive Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFOutlinedTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Outlined Icon Text Buttons', - [ - AFOutlinedButton.normal( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.add, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.primary, - ), - const SizedBox(width: 8), - Text( - 'Normal Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.primary, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFOutlinedButton.destructive( - onTap: () {}, - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.delete, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.error, - ), - const SizedBox(width: 8), - Text( - 'Destructive Button', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.error, - ), - ), - ], - ), - ), - const SizedBox(width: 16), - AFOutlinedButton.disabled( - builder: (context, isHovering, disabled) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.block, - size: 20, - color: AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - const SizedBox(width: 8), - Text( - 'Disabled Button', - style: TextStyle( - color: - AppFlowyTheme.of(context).textColorScheme.tertiary, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Ghost Buttons', - [ - AFGhostTextButton.primary( - text: 'Primary Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFGhostTextButton.disabled( - text: 'Disabled Button', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Button with alignment', - [ - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Left Button', - onTap: () {}, - alignment: Alignment.centerLeft, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Center Button', - onTap: () {}, - alignment: Alignment.center, - ), - ), - const SizedBox(width: 16), - SizedBox( - width: 200, - child: AFFilledTextButton.primary( - text: 'Right Button', - onTap: () {}, - alignment: Alignment.centerRight, - ), - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'Button Sizes', - [ - AFFilledTextButton.primary( - text: 'Small Button', - onTap: () {}, - size: AFButtonSize.s, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Medium Button', - onTap: () {}, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Large Button', - onTap: () {}, - size: AFButtonSize.l, - ), - const SizedBox(width: 16), - AFFilledTextButton.primary( - text: 'Extra Large Button', - onTap: () {}, - size: AFButtonSize.xl, - ), - ], - ), - ], - ), - ); - } - - Widget _buildSection(String title, List children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: children, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart deleted file mode 100644 index 4a9480d1b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/modal/modal_page.dart +++ /dev/null @@ -1,153 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class ModalPage extends StatefulWidget { - const ModalPage({super.key}); - - @override - State createState() => _ModalPageState(); -} - -class _ModalPageState extends State { - double width = AFModalDimension.M; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: Container( - constraints: BoxConstraints(maxWidth: 600), - padding: EdgeInsets.symmetric(horizontal: theme.spacing.xl), - child: Column( - spacing: theme.spacing.l, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - spacing: theme.spacing.m, - mainAxisSize: MainAxisSize.min, - children: [ - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.S), - builder: (context, isHovering, disabled) { - return Text( - 'S', - style: TextStyle( - color: width == AFModalDimension.S - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.M), - builder: (context, isHovering, disabled) { - return Text( - 'M', - style: TextStyle( - color: width == AFModalDimension.M - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - AFGhostButton.normal( - onTap: () => setState(() => width = AFModalDimension.L), - builder: (context, isHovering, disabled) { - return Text( - 'L', - style: TextStyle( - color: width == AFModalDimension.L - ? theme.textColorScheme.theme - : theme.textColorScheme.primary, - ), - ); - }, - ), - ], - ), - AFFilledButton.primary( - builder: (context, isHovering, disabled) { - return Text( - 'Show Modal', - style: TextStyle( - color: AppFlowyTheme.of(context).textColorScheme.onFill, - ), - ); - }, - onTap: () { - showDialog( - context: context, - barrierColor: theme.surfaceColorScheme.overlay, - builder: (context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: AFModal( - constraints: BoxConstraints( - maxWidth: width, - maxHeight: AFModalDimension.dialogHeight, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - AFModalHeader( - leading: Text( - 'Header', - style: theme.textStyle.heading4.standard( - color: theme.textColorScheme.primary, - ), - ), - trailing: [ - AFGhostButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return const Icon(Icons.close); - }, - ) - ], - ), - Expanded( - child: AFModalBody( - child: Text( - 'A dialog briefly presents information or requests confirmation, allowing users to continue their workflow after interaction.'), - ), - ), - AFModalFooter( - trailing: [ - AFOutlinedButton.normal( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return const Text('Cancel'); - }, - ), - AFFilledButton.primary( - onTap: () => Navigator.of(context).pop(), - builder: (context, isHovering, disabled) { - return Text( - 'Apply', - style: TextStyle( - color: AppFlowyTheme.of(context) - .textColorScheme - .onFill, - ), - ); - }, - ), - ], - ) - ], - )), - ); - }, - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart deleted file mode 100644 index 9e3436ecd4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/lib/src/textfield/textfield_page.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class TextFieldPage extends StatelessWidget { - const TextFieldPage({super.key}); - - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSection( - 'TextField Sizes', - [ - AFTextField( - hintText: 'Please enter your name', - size: AFTextFieldSize.m, - ), - AFTextField( - hintText: 'Please enter your name', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with hint text', - [ - AFTextField( - hintText: 'Please enter your name', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with initial text', - [ - AFTextField( - initialText: 'https://appflowy.com', - ), - ], - ), - const SizedBox(height: 32), - _buildSection( - 'TextField with validator ', - [ - AFTextField( - validator: (controller) { - if (controller.text.isEmpty) { - return (true, 'This field is required'); - } - - final emailRegex = - RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); - if (!emailRegex.hasMatch(controller.text)) { - return (true, 'Please enter a valid email address'); - } - - return (false, ''); - }, - ), - ], - ), - ], - ), - ); - } - - Widget _buildSection(String title, List children) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - Wrap( - spacing: 8, - runSpacing: 8, - children: children, - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore deleted file mode 100644 index 746adbb6b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index c2efd0b608..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index c2efd0b608..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 345181d730..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,705 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "appflowy_ui_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* appflowy_ui_example.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/appflowy_ui_example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/appflowy_ui_example"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 04d5b736e6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index b3c1761412..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Cocoa -import FlutterMacOS - -@main -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } - - override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { - return true - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9a3..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba55..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa40f..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226d5..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e09..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72c9..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cfdd..0000000000 Binary files a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4e0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 47821fa6d8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = appflowy_ui_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.appflowyUiExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb234..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a472..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift b/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 61f3bd1fc5..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Cocoa -import FlutterMacOS -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml deleted file mode 100644 index af361ecfab..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: appflowy_ui_example -description: "Example app showcasing AppFlowy UI components and widgets" -publish_to: "none" - -version: 1.0.0+1 - -environment: - flutter: ">=3.27.4" - sdk: ">=3.3.0 <4.0.0" - -dependencies: - flutter: - sdk: flutter - appflowy_ui: - path: ../ - cupertino_icons: ^1.0.6 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - -flutter: - uses-material-design: true diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart b/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart deleted file mode 100644 index 423052a342..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:appflowy_ui_example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart deleted file mode 100644 index 974907f940..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/appflowy_ui.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'src/component/component.dart'; -export 'src/theme/theme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart deleted file mode 100644 index 39d5175af1..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/widgets.dart'; - -enum AFButtonSize { - s, - m, - l, - xl; - - TextStyle buildTextStyle(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => theme.textStyle.body.enhanced(), - AFButtonSize.m => theme.textStyle.body.enhanced(), - AFButtonSize.l => theme.textStyle.body.enhanced(), - AFButtonSize.xl => theme.textStyle.title.enhanced(), - }; - } - - EdgeInsetsGeometry buildPadding(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => EdgeInsets.symmetric( - horizontal: theme.spacing.l, - vertical: theme.spacing.xs, - ), - AFButtonSize.m => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: theme.spacing.s, - ), - AFButtonSize.l => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: 10, // why? - ), - AFButtonSize.xl => EdgeInsets.symmetric( - horizontal: theme.spacing.xl, - vertical: 14, // why? - ), - }; - } - - double buildBorderRadius(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return switch (this) { - AFButtonSize.s => theme.borderRadius.m, - AFButtonSize.m => theme.borderRadius.m, - AFButtonSize.l => 10, // why? - AFButtonSize.xl => theme.borderRadius.xl, - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart deleted file mode 100644 index 9bb36507e8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_button.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFBaseButtonColorBuilder = Color Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -typedef AFBaseButtonBorderColorBuilder = Color Function( - BuildContext context, - bool isHovering, - bool disabled, - bool isFocused, -); - -class AFBaseButton extends StatefulWidget { - const AFBaseButton({ - super.key, - required this.onTap, - required this.builder, - required this.padding, - required this.borderRadius, - this.borderColor, - this.backgroundColor, - this.ringColor, - this.disabled = false, - }); - - final VoidCallback? onTap; - - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonBorderColorBuilder? ringColor; - final AFBaseButtonColorBuilder? backgroundColor; - - final EdgeInsetsGeometry padding; - final double borderRadius; - final bool disabled; - - final Widget Function( - BuildContext context, - bool isHovering, - bool disabled, - ) builder; - - @override - State createState() => _AFBaseButtonState(); -} - -class _AFBaseButtonState extends State { - final FocusNode focusNode = FocusNode(); - - bool isHovering = false; - bool isFocused = false; - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Color borderColor = _buildBorderColor(context); - final Color backgroundColor = _buildBackgroundColor(context); - final Color ringColor = _buildRingColor(context); - - return Actions( - actions: { - ActivateIntent: CallbackAction( - onInvoke: (_) { - if (!widget.disabled) { - widget.onTap?.call(); - } - return; - }, - ), - }, - child: Focus( - focusNode: focusNode, - onFocusChange: (isFocused) { - setState(() => this.isFocused = isFocused); - }, - child: MouseRegion( - cursor: widget.disabled - ? SystemMouseCursors.basic - : SystemMouseCursors.click, - onEnter: (_) => setState(() => isHovering = true), - onExit: (_) => setState(() => isHovering = false), - child: GestureDetector( - onTap: widget.disabled ? null : widget.onTap, - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(widget.borderRadius), - border: isFocused - ? Border.all( - color: ringColor, - width: 2, - strokeAlign: BorderSide.strokeAlignOutside, - ) - : null, - ), - child: DecoratedBox( - decoration: BoxDecoration( - color: backgroundColor, - border: Border.all(color: borderColor), - borderRadius: BorderRadius.circular(widget.borderRadius), - ), - child: Padding( - padding: widget.padding, - child: widget.builder( - context, - isHovering, - widget.disabled, - ), - ), - ), - ), - ), - ), - ), - ); - } - - Color _buildBorderColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return widget.borderColor - ?.call(context, isHovering, widget.disabled, isFocused) ?? - theme.borderColorScheme.greyTertiary; - } - - Color _buildBackgroundColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - return widget.backgroundColor?.call(context, isHovering, widget.disabled) ?? - theme.fillColorScheme.transparent; - } - - Color _buildRingColor(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - if (widget.ringColor != null) { - return widget.ringColor! - .call(context, isHovering, widget.disabled, isFocused); - } - - if (isFocused) { - return theme.borderColorScheme.themeThick.withAlpha(128); - } - - return theme.borderColorScheme.transparent; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart deleted file mode 100644 index 035307d10b..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/base_button/base_text_button.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -class AFBaseTextButton extends StatelessWidget { - const AFBaseTextButton({ - super.key, - required this.text, - required this.onTap, - this.disabled = false, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.textColor, - this.backgroundColor, - this.alignment, - this.textStyle, - }); - - /// The text of the button. - final String text; - - /// Whether the button is disabled. - final bool disabled; - - /// The callback when the button is tapped. - final VoidCallback onTap; - - /// The size of the button. - final AFButtonSize size; - - /// The padding of the button. - final EdgeInsetsGeometry? padding; - - /// The border radius of the button. - final double? borderRadius; - - /// The text color of the button. - final AFBaseButtonColorBuilder? textColor; - - /// The background color of the button. - final AFBaseButtonColorBuilder? backgroundColor; - - /// The alignment of the button. - /// - /// If it's null, the button size will be the size of the text with padding. - final Alignment? alignment; - - /// The text style of the button. - final TextStyle? textStyle; - - @override - Widget build(BuildContext context) { - throw UnimplementedError(); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart deleted file mode 100644 index 31a3a20b5f..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/button.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Base button -export 'base_button/base.dart'; -export 'base_button/base_button.dart'; -export 'base_button/base_text_button.dart'; -// Filled buttons -export 'filled_button/filled_button.dart'; -export 'filled_button/filled_icon_text_button.dart'; -export 'filled_button/filled_text_button.dart'; -// Ghost buttons -export 'ghost_button/ghost_button.dart'; -export 'ghost_button/ghost_icon_text_button.dart'; -export 'ghost_button/ghost_text_button.dart'; -// Outlined buttons -export 'outlined_button/outlined_button.dart'; -export 'outlined_button/outlined_icon_text_button.dart'; -export 'outlined_button/outlined_text_button.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart deleted file mode 100644 index e871626b59..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_button.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFFilledButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFFilledButton extends StatelessWidget { - const AFFilledButton._({ - super.key, - required this.builder, - required this.onTap, - required this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Primary text button. - factory AFFilledButton.primary({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.themeThick; - }, - ); - } - - /// Destructive text button. - factory AFFilledButton.destructive({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.errorThick; - }, - ); - } - - /// Disabled text button. - factory AFFilledButton.disabled({ - Key? key, - required AFFilledButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledButton._( - key: key, - builder: builder, - onTap: () {}, - size: size, - disabled: true, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonColorBuilder? backgroundColor; - final AFFilledButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart deleted file mode 100644 index 04c49d0b01..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_icon_text_button.dart +++ /dev/null @@ -1,199 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFFilledIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFFilledIconTextButton extends StatelessWidget { - const AFFilledIconTextButton._({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - }); - - /// Primary filled text button. - factory AFFilledIconTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.tertiary; - } - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return theme.fillColorScheme.themeThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Destructive filled text button. - factory AFFilledIconTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.tertiary; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Disabled filled text button. - factory AFFilledIconTextButton.disabled({ - Key? key, - required String text, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.fillColorScheme.tertiary; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.onFill; - }, - ); - } - - /// Ghost filled text button with transparent background that shows color on hover. - factory AFFilledIconTextButton.ghost({ - Key? key, - required String text, - required VoidCallback onTap, - required AFFilledIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFFilledIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return Colors.transparent; - } - if (isHovering) { - return theme.fillColorScheme.themeThickHover; - } - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - return theme.textColorScheme.primary; - }, - ); - } - - final String text; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFFilledIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - backgroundColor: backgroundColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.onFill; - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconBuilder(context, isHovering, disabled), - SizedBox(width: theme.spacing.s), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart deleted file mode 100644 index d1b1d868d0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/filled_button/filled_text_button.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFFilledTextButton extends AFBaseTextButton { - const AFFilledTextButton({ - super.key, - required super.text, - required super.onTap, - required super.backgroundColor, - required super.textColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - super.textStyle, - }); - - /// Primary text button. - factory AFFilledTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.onFill, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.themeThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.themeThick; - }, - ); - } - - /// Destructive text button. - factory AFFilledTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.onFill, - backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5; - } - if (isHovering) { - return AppFlowyTheme.of(context).fillColorScheme.errorThickHover; - } - return AppFlowyTheme.of(context).fillColorScheme.errorThick; - }, - ); - } - - /// Disabled text button. - factory AFFilledTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFFilledTextButton( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.tertiary, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.primaryAlpha5, - ); - } - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - AppFlowyTheme.of(context).textColorScheme.onFill; - Widget child = Text( - text, - style: textStyle ?? - size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart deleted file mode 100644 index 6300c6f5a8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_button.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFGhostButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFGhostButton extends StatelessWidget { - const AFGhostButton._({ - super.key, - required this.onTap, - required this.backgroundColor, - required this.builder, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Normal ghost button. - factory AFGhostButton.normal({ - Key? key, - required VoidCallback onTap, - required AFGhostButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFGhostButton._( - key: key, - builder: builder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - /// Disabled ghost button. - factory AFGhostButton.disabled({ - Key? key, - required AFGhostButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFGhostButton._( - key: key, - builder: builder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.transparent, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonColorBuilder? backgroundColor; - final AFGhostButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart deleted file mode 100644 index af65599ea3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_icon_text_button.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFGhostIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFGhostIconTextButton extends StatelessWidget { - const AFGhostIconTextButton({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Primary ghost text button. - factory AFGhostIconTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - required AFGhostIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFGhostIconTextButton( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return Colors.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Disabled ghost text button. - factory AFGhostIconTextButton.disabled({ - Key? key, - required String text, - required AFGhostIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFGhostIconTextButton( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - backgroundColor: (context, isHovering, disabled) { - return Colors.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return theme.textColorScheme.tertiary; - }, - ); - } - - final String text; - final bool disabled; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFGhostIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (context, isHovering, disabled, isFocused) { - return Colors.transparent; - }, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - iconBuilder( - context, - isHovering, - disabled, - ), - SizedBox(width: theme.spacing.m), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart deleted file mode 100644 index d154d67dbd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/ghost_button/ghost_text_button.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFGhostTextButton extends AFBaseTextButton { - const AFGhostTextButton({ - super.key, - required super.text, - required super.onTap, - super.textColor, - super.backgroundColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - }); - - /// Normal ghost text button. - factory AFGhostTextButton.primary({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - }) { - return AFGhostTextButton( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Disabled ghost text button. - factory AFGhostTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - }) { - return AFGhostTextButton( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).textColorScheme.tertiary, - backgroundColor: (context, isHovering, disabled) => - AppFlowyTheme.of(context).fillColorScheme.transparent, - ); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: (_, __, ___, ____) => Colors.transparent, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - - Widget child = Text( - text, - style: size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart deleted file mode 100644 index 205d9931d6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_button.dart +++ /dev/null @@ -1,168 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFOutlinedButtonWidgetBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFOutlinedButton extends StatelessWidget { - const AFOutlinedButton._({ - super.key, - required this.onTap, - required this.builder, - this.borderColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - }); - - /// Normal outlined button. - factory AFOutlinedButton.normal({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFOutlinedButton._( - key: key, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - /// Destructive outlined button. - factory AFOutlinedButton.destructive({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - }) { - return AFOutlinedButton._( - key: key, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorSelect; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedButton.disabled({ - Key? key, - required AFOutlinedButtonWidgetBuilder builder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - }) { - return AFOutlinedButton._( - key: key, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - builder: builder, - ); - } - - final VoidCallback onTap; - final bool disabled; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonColorBuilder? backgroundColor; - - final AFOutlinedButtonWidgetBuilder builder; - - @override - Widget build(BuildContext context) { - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: builder, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart deleted file mode 100644 index 350594cd46..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_icon_text_button.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFOutlinedIconBuilder = Widget Function( - BuildContext context, - bool isHovering, - bool disabled, -); - -class AFOutlinedIconTextButton extends StatelessWidget { - const AFOutlinedIconTextButton._({ - super.key, - required this.text, - required this.onTap, - required this.iconBuilder, - this.borderColor, - this.textColor, - this.backgroundColor, - this.size = AFButtonSize.m, - this.padding, - this.borderRadius, - this.disabled = false, - this.alignment = MainAxisAlignment.center, - }); - - /// Normal outlined text button. - factory AFOutlinedIconTextButton.normal({ - Key? key, - required String text, - required VoidCallback onTap, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - onTap: onTap, - iconBuilder: iconBuilder, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Destructive outlined text button. - factory AFOutlinedIconTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.error - : theme.textColorScheme.error; - }, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedIconTextButton.disabled({ - Key? key, - required String text, - required AFOutlinedIconBuilder iconBuilder, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - MainAxisAlignment alignment = MainAxisAlignment.center, - }) { - return AFOutlinedIconTextButton._( - key: key, - text: text, - iconBuilder: iconBuilder, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.tertiary - : theme.textColorScheme.primary; - }, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - final String text; - final bool disabled; - final VoidCallback onTap; - final AFButtonSize size; - final EdgeInsetsGeometry? padding; - final double? borderRadius; - final MainAxisAlignment alignment; - - final AFOutlinedIconBuilder iconBuilder; - - final AFBaseButtonColorBuilder? textColor; - final AFBaseButtonBorderColorBuilder? borderColor; - final AFBaseButtonColorBuilder? backgroundColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - disabled: disabled, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - return Row( - mainAxisAlignment: alignment, - children: [ - iconBuilder(context, isHovering, disabled), - SizedBox(width: theme.spacing.s), - Text( - text, - style: size.buildTextStyle(context).copyWith( - color: textColor, - ), - ), - ], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart deleted file mode 100644 index d809d981b0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/button/outlined_button/outlined_text_button.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:appflowy_ui/src/component/component.dart'; -import 'package:appflowy_ui/src/theme/appflowy_theme.dart'; -import 'package:flutter/material.dart'; - -class AFOutlinedTextButton extends AFBaseTextButton { - const AFOutlinedTextButton._({ - super.key, - required super.text, - required super.onTap, - this.borderColor, - super.textStyle, - super.textColor, - super.backgroundColor, - super.size = AFButtonSize.m, - super.padding, - super.borderRadius, - super.disabled = false, - super.alignment, - }); - - /// Normal outlined text button. - factory AFOutlinedTextButton.normal({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.textColorScheme.tertiary; - } - if (isHovering) { - return theme.textColorScheme.primary; - } - return theme.textColorScheme.primary; - }, - ); - } - - /// Destructive outlined text button. - factory AFOutlinedTextButton.destructive({ - Key? key, - required String text, - required VoidCallback onTap, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - bool disabled = false, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: onTap, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: disabled, - alignment: alignment, - textStyle: textStyle, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorThickHover; - } - return theme.fillColorScheme.errorThick; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.errorThick; - } - if (isHovering) { - return theme.fillColorScheme.errorSelect; - } - return theme.fillColorScheme.transparent; - }, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.error - : theme.textColorScheme.error; - }, - ); - } - - /// Disabled outlined text button. - factory AFOutlinedTextButton.disabled({ - Key? key, - required String text, - AFButtonSize size = AFButtonSize.m, - EdgeInsetsGeometry? padding, - double? borderRadius, - Alignment? alignment, - TextStyle? textStyle, - }) { - return AFOutlinedTextButton._( - key: key, - text: text, - onTap: () {}, - size: size, - padding: padding, - borderRadius: borderRadius, - disabled: true, - alignment: alignment, - textStyle: textStyle, - textColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - return disabled - ? theme.textColorScheme.tertiary - : theme.textColorScheme.primary; - }, - borderColor: (context, isHovering, disabled, isFocused) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.borderColorScheme.greyTertiary; - } - if (isHovering) { - return theme.borderColorScheme.greyTertiaryHover; - } - return theme.borderColorScheme.greyTertiary; - }, - backgroundColor: (context, isHovering, disabled) { - final theme = AppFlowyTheme.of(context); - if (disabled) { - return theme.fillColorScheme.transparent; - } - if (isHovering) { - return theme.fillColorScheme.primaryAlpha5; - } - return theme.fillColorScheme.transparent; - }, - ); - } - - final AFBaseButtonBorderColorBuilder? borderColor; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return AFBaseButton( - disabled: disabled, - backgroundColor: backgroundColor, - borderColor: borderColor, - padding: padding ?? size.buildPadding(context), - borderRadius: borderRadius ?? size.buildBorderRadius(context), - onTap: onTap, - builder: (context, isHovering, disabled) { - final textColor = this.textColor?.call(context, isHovering, disabled) ?? - theme.textColorScheme.primary; - - Widget child = Text( - text, - style: textStyle ?? - size.buildTextStyle(context).copyWith(color: textColor), - ); - - final alignment = this.alignment; - - if (alignment != null) { - child = Align( - alignment: alignment, - child: child, - ); - } - - return child; - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart deleted file mode 100644 index 99fec83e57..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/component.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'button/button.dart'; -export 'separator/divider.dart'; -export 'modal/modal.dart'; -export 'textfield/textfield.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart deleted file mode 100644 index 72a7dbb5cf..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/dimension.dart +++ /dev/null @@ -1,9 +0,0 @@ -class AFModalDimension { - const AFModalDimension._(); - - static const double S = 400.0; - static const double M = 560.0; - static const double L = 720.0; - - static const double dialogHeight = 200.0; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart deleted file mode 100644 index 4b40aebcbd..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/modal/modal.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -export 'dimension.dart'; - -class AFModal extends StatelessWidget { - const AFModal({ - super.key, - this.constraints = const BoxConstraints(), - required this.child, - }); - - final BoxConstraints constraints; - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Center( - child: Padding( - padding: EdgeInsets.all(theme.spacing.xl), - child: ConstrainedBox( - constraints: constraints, - child: DecoratedBox( - decoration: BoxDecoration( - boxShadow: theme.shadow.medium, - borderRadius: BorderRadius.circular(theme.borderRadius.xl), - color: theme.surfaceColorScheme.primary, - ), - child: Material( - color: Colors.transparent, - child: child, - ), - ), - ), - ), - ); - } -} - -class AFModalHeader extends StatelessWidget { - const AFModalHeader({ - super.key, - required this.leading, - this.trailing = const [], - }); - - final Widget leading; - final List trailing; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.only( - top: theme.spacing.xl, - left: theme.spacing.xxl, - right: theme.spacing.xxl, - ), - child: Row( - spacing: theme.spacing.s, - children: [ - Expanded(child: leading), - ...trailing, - ], - ), - ); - } -} - -class AFModalFooter extends StatelessWidget { - const AFModalFooter({ - super.key, - this.leading = const [], - this.trailing = const [], - }); - - final List leading; - final List trailing; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.only( - bottom: theme.spacing.xl, - left: theme.spacing.xxl, - right: theme.spacing.xxl, - ), - child: Row( - spacing: theme.spacing.l, - children: [ - ...leading, - Spacer(), - ...trailing, - ], - ), - ); - } -} - -class AFModalBody extends StatelessWidget { - const AFModalBody({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - - return Padding( - padding: EdgeInsets.symmetric( - vertical: theme.spacing.l, - horizontal: theme.spacing.xxl, - ), - child: child, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart deleted file mode 100644 index fa5dcd093d..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/separator/divider.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/widgets.dart'; - -class AFDivider extends StatelessWidget { - const AFDivider({ - super.key, - this.axis = Axis.horizontal, - this.color, - this.thickness = 1.0, - this.spacing = 0.0, - this.startIndent = 0.0, - this.endIndent = 0.0, - }) : assert(thickness > 0.0), - assert(spacing >= 0.0), - assert(startIndent >= 0.0), - assert(endIndent >= 0.0); - - final Axis axis; - final double thickness; - final double spacing; - final double startIndent; - final double endIndent; - final Color? color; - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final color = this.color ?? theme.borderColorScheme.greyTertiary; - - return switch (axis) { - Axis.horizontal => Container( - height: thickness, - color: color, - margin: EdgeInsetsDirectional.only( - start: startIndent, - end: endIndent, - top: spacing, - bottom: spacing, - ), - ), - Axis.vertical => Container( - width: thickness, - color: color, - margin: EdgeInsets.only( - left: spacing, - right: spacing, - top: startIndent, - bottom: endIndent, - ), - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart deleted file mode 100644 index 3f5ad4cfed..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/component/textfield/textfield.dart +++ /dev/null @@ -1,254 +0,0 @@ -import 'package:appflowy_ui/src/theme/theme.dart'; -import 'package:flutter/material.dart'; - -typedef AFTextFieldValidator = (bool result, String errorText) Function( - TextEditingController controller, -); - -abstract class AFTextFieldState extends State { - // Error handler - void syncError({required String errorText}) {} - void clearError() {} - - /// Obscure the text. - void syncObscured(bool isObscured) {} -} - -class AFTextField extends StatefulWidget { - const AFTextField({ - super.key, - this.hintText, - this.initialText, - this.keyboardType, - this.size = AFTextFieldSize.l, - this.validator, - this.controller, - this.onChanged, - this.onSubmitted, - this.autoFocus, - this.obscureText = false, - this.suffixIconBuilder, - this.suffixIconConstraints, - }); - - /// The hint text to display when the text field is empty. - final String? hintText; - - /// The initial text to display in the text field. - final String? initialText; - - /// The type of keyboard to display. - final TextInputType? keyboardType; - - /// The size variant of the text field. - final AFTextFieldSize size; - - /// The validator to use for the text field. - final AFTextFieldValidator? validator; - - /// The controller to use for the text field. - /// - /// If it's not provided, the text field will use a new controller. - final TextEditingController? controller; - - /// The callback to call when the text field changes. - final void Function(String)? onChanged; - - /// The callback to call when the text field is submitted. - final void Function(String)? onSubmitted; - - /// Enable auto focus. - final bool? autoFocus; - - /// Obscure the text. - final bool obscureText; - - /// The trailing widget to display. - final Widget Function(BuildContext context, bool isObscured)? - suffixIconBuilder; - - /// The size of the suffix icon. - final BoxConstraints? suffixIconConstraints; - - @override - State createState() => _AFTextFieldState(); -} - -class _AFTextFieldState extends AFTextFieldState { - late final TextEditingController effectiveController; - - bool hasError = false; - String errorText = ''; - - bool isObscured = false; - - @override - void initState() { - super.initState(); - - effectiveController = widget.controller ?? TextEditingController(); - - final initialText = widget.initialText; - if (initialText != null) { - effectiveController.text = initialText; - } - - effectiveController.addListener(_validate); - - isObscured = widget.obscureText; - } - - @override - void dispose() { - effectiveController.removeListener(_validate); - if (widget.controller == null) { - effectiveController.dispose(); - } - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final theme = AppFlowyTheme.of(context); - final borderRadius = widget.size.borderRadius(theme); - final contentPadding = widget.size.contentPadding(theme); - - final errorBorderColor = theme.borderColorScheme.errorThick; - final defaultBorderColor = theme.borderColorScheme.greyTertiary; - - Widget child = TextField( - controller: effectiveController, - keyboardType: widget.keyboardType, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.primary, - ), - obscureText: isObscured, - onChanged: widget.onChanged, - onSubmitted: widget.onSubmitted, - autofocus: widget.autoFocus ?? false, - decoration: InputDecoration( - hintText: widget.hintText, - hintStyle: theme.textStyle.body.standard( - color: theme.textColorScheme.tertiary, - ), - isDense: true, - constraints: BoxConstraints(), - contentPadding: contentPadding, - border: OutlineInputBorder( - borderSide: BorderSide( - color: hasError ? errorBorderColor : defaultBorderColor, - ), - borderRadius: borderRadius, - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: hasError ? errorBorderColor : defaultBorderColor, - ), - borderRadius: borderRadius, - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: hasError - ? errorBorderColor - : theme.borderColorScheme.themeThick, - ), - borderRadius: borderRadius, - ), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorBorderColor, - ), - borderRadius: borderRadius, - ), - focusedErrorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: errorBorderColor, - ), - borderRadius: borderRadius, - ), - hoverColor: theme.borderColorScheme.greyTertiaryHover, - suffixIcon: widget.suffixIconBuilder?.call(context, isObscured), - suffixIconConstraints: widget.suffixIconConstraints, - ), - ); - - if (hasError && errorText.isNotEmpty) { - child = Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - SizedBox(height: theme.spacing.xs), - Text( - errorText, - style: theme.textStyle.caption.standard( - color: theme.textColorScheme.error, - ), - ), - ], - ); - } - - return child; - } - - void _validate() { - final validator = widget.validator; - if (validator != null) { - final result = validator(effectiveController); - setState(() { - hasError = result.$1; - errorText = result.$2; - }); - } - } - - @override - void syncError({ - required String errorText, - }) { - setState(() { - hasError = true; - this.errorText = errorText; - }); - } - - @override - void clearError() { - setState(() { - hasError = false; - errorText = ''; - }); - } - - @override - void syncObscured(bool isObscured) { - setState(() { - this.isObscured = isObscured; - }); - } -} - -enum AFTextFieldSize { - m, - l; - - EdgeInsetsGeometry contentPadding(AppFlowyThemeData theme) { - return EdgeInsets.symmetric( - vertical: switch (this) { - AFTextFieldSize.m => theme.spacing.s, - AFTextFieldSize.l => 10.0, - }, - horizontal: theme.spacing.m, - ); - } - - BorderRadius borderRadius(AppFlowyThemeData theme) { - return BorderRadius.circular( - switch (this) { - AFTextFieldSize.m => theme.borderRadius.m, - AFTextFieldSize.l => 10.0, - }, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart deleted file mode 100644 index b8dc5a1149..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/appflowy_theme.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:appflowy_ui/src/theme/theme.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class AppFlowyTheme extends StatelessWidget { - const AppFlowyTheme({ - super.key, - required this.data, - required this.child, - }); - - final AppFlowyThemeData data; - final Widget child; - - static AppFlowyThemeData of(BuildContext context, {bool listen = true}) { - final provider = maybeOf(context, listen: listen); - if (provider == null) { - throw FlutterError( - ''' - AppFlowyTheme.of() called with a context that does not contain a AppFlowyTheme.\n - No AppFlowyTheme ancestor could be found starting from the context that was passed to AppFlowyTheme.of(). - This can happen because you do not have a AppFlowyTheme widget (which introduces a AppFlowyTheme), - or it can happen if the context you use comes from a widget above this widget.\n - The context used was: $context''', - ); - } - return provider; - } - - static AppFlowyThemeData? maybeOf( - BuildContext context, { - bool listen = true, - }) { - if (listen) { - return context - .dependOnInheritedWidgetOfExactType() - ?.themeData; - } - final provider = context - .getElementForInheritedWidgetOfExactType() - ?.widget; - - return (provider as AppFlowyInheritedTheme?)?.themeData; - } - - @override - Widget build(BuildContext context) { - return AppFlowyInheritedTheme( - themeData: data, - child: child, - ); - } -} - -class AppFlowyInheritedTheme extends InheritedTheme { - const AppFlowyInheritedTheme({ - super.key, - required this.themeData, - required super.child, - }); - - final AppFlowyThemeData themeData; - - @override - Widget wrap(BuildContext context, Widget child) { - return AppFlowyTheme(data: themeData, child: child); - } - - @override - bool updateShouldNotify(AppFlowyInheritedTheme oldWidget) => - themeData != oldWidget.themeData; -} - -/// An interpolation between two [AppFlowyThemeData]s. -/// -/// This class specializes the interpolation of [Tween] to -/// call the [AppFlowyThemeData.lerp] method. -/// -/// See [Tween] for a discussion on how to use interpolation objects. -class AppFlowyThemeDataTween extends Tween { - /// Creates a [AppFlowyThemeData] tween. - /// - /// The [begin] and [end] properties must be non-null before the tween is - /// first used, but the arguments can be null if the values are going to be - /// filled in later. - AppFlowyThemeDataTween({super.begin, super.end}); - - @override - AppFlowyThemeData lerp(double t) => AppFlowyThemeData.lerp(begin!, end!, t); -} - -class AnimatedAppFlowyTheme extends ImplicitlyAnimatedWidget { - /// Creates an animated theme. - /// - /// By default, the theme transition uses a linear curve. - const AnimatedAppFlowyTheme({ - super.key, - required this.data, - super.curve, - super.duration = kThemeAnimationDuration, - super.onEnd, - required this.child, - }); - - /// Specifies the color and typography values for descendant widgets. - final AppFlowyThemeData data; - - /// The widget below this widget in the tree. - /// - /// {@macro flutter.widgets.ProxyWidget.child} - final Widget child; - - @override - AnimatedWidgetBaseState createState() => - _AnimatedThemeState(); -} - -class _AnimatedThemeState - extends AnimatedWidgetBaseState { - AppFlowyThemeDataTween? data; - - @override - void forEachTween(TweenVisitor visitor) { - data = visitor( - data, - widget.data, - (dynamic value) => - AppFlowyThemeDataTween(begin: value as AppFlowyThemeData), - )! as AppFlowyThemeDataTween; - } - - @override - Widget build(BuildContext context) { - return AppFlowyTheme( - data: widget.data, - child: widget.child, - ); - } - - @override - void debugFillProperties(DiagnosticPropertiesBuilder description) { - super.debugFillProperties(description); - description.add( - DiagnosticsProperty( - 'data', - data, - showName: false, - defaultValue: null, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart deleted file mode 100644 index 2bd6d619d8..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/primitive.dart +++ /dev/null @@ -1,658 +0,0 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-19T13:45:56.076897 -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:flutter/material.dart'; - -class AppFlowyPrimitiveTokens { - AppFlowyPrimitiveTokens._(); - - /// #f8faff - static Color get neutral100 => Color(0xFFF8FAFF); - - /// #e4e8f5 - static Color get neutral200 => Color(0xFFE4E8F5); - - /// #ced3e6 - static Color get neutral300 => Color(0xFFCED3E6); - - /// #b5bbd3 - static Color get neutral400 => Color(0xFFB5BBD3); - - /// #989eb7 - static Color get neutral500 => Color(0xFF989EB7); - - /// #6f748c - static Color get neutral600 => Color(0xFF6F748C); - - /// #54596e - static Color get neutral700 => Color(0xFF54596E); - - /// #3c3f4e - static Color get neutral800 => Color(0xFF3C3F4E); - - /// #272930 - static Color get neutral900 => Color(0xFF272930); - - /// #21232a - static Color get neutral1000 => Color(0xFF21232A); - - /// #000000 - static Color get neutralBlack => Color(0xFF000000); - - /// #00000099 - static Color get neutralAlphaBlack60 => Color(0x99000000); - - /// #ffffff - static Color get neutralWhite => Color(0xFFFFFFFF); - - /// #ffffff00 - static Color get neutralAlphaWhite0 => Color(0x00FFFFFF); - - /// #ffffff33 - static Color get neutralAlphaWhite20 => Color(0x33FFFFFF); - - /// #ffffff4d - static Color get neutralAlphaWhite30 => Color(0x4DFFFFFF); - - /// #f9fafd0d - static Color get neutralAlphaGrey10005 => Color(0x0DF9FAFD); - - /// #f9fafd1a - static Color get neutralAlphaGrey10010 => Color(0x1AF9FAFD); - - /// #1f23290d - static Color get neutralAlphaGrey100005 => Color(0x0D1F2329); - - /// #1f23291a - static Color get neutralAlphaGrey100010 => Color(0x1A1F2329); - - /// #1f2329b2 - static Color get neutralAlphaGrey100070 => Color(0xB21F2329); - - /// #1f2329cc - static Color get neutralAlphaGrey100080 => Color(0xCC1F2329); - - /// #e3f6ff - static Color get blue100 => Color(0xFFE3F6FF); - - /// #a9e2ff - static Color get blue200 => Color(0xFFA9E2FF); - - /// #80d2ff - static Color get blue300 => Color(0xFF80D2FF); - - /// #4ec1ff - static Color get blue400 => Color(0xFF4EC1FF); - - /// #00b5ff - static Color get blue500 => Color(0xFF00B5FF); - - /// #0092d6 - static Color get blue600 => Color(0xFF0092D6); - - /// #0078c0 - static Color get blue700 => Color(0xFF0078C0); - - /// #0065a9 - static Color get blue800 => Color(0xFF0065A9); - - /// #00508f - static Color get blue900 => Color(0xFF00508F); - - /// #003c77 - static Color get blue1000 => Color(0xFF003C77); - - /// #00b5ff26 - static Color get blueAlphaBlue50015 => Color(0x2600B5FF); - - /// #ecf9f5 - static Color get green100 => Color(0xFFECF9F5); - - /// #c3e5d8 - static Color get green200 => Color(0xFFC3E5D8); - - /// #9ad1bc - static Color get green300 => Color(0xFF9AD1BC); - - /// #71bd9f - static Color get green400 => Color(0xFF71BD9F); - - /// #48a982 - static Color get green500 => Color(0xFF48A982); - - /// #248569 - static Color get green600 => Color(0xFF248569); - - /// #29725d - static Color get green700 => Color(0xFF29725D); - - /// #2e6050 - static Color get green800 => Color(0xFF2E6050); - - /// #305548 - static Color get green900 => Color(0xFF305548); - - /// #305244 - static Color get green1000 => Color(0xFF305244); - - /// #f1e0ff - static Color get purple100 => Color(0xFFF1E0FF); - - /// #e1b3ff - static Color get purple200 => Color(0xFFE1B3FF); - - /// #d185ff - static Color get purple300 => Color(0xFFD185FF); - - /// #bc58ff - static Color get purple400 => Color(0xFFBC58FF); - - /// #9327ff - static Color get purple500 => Color(0xFF9327FF); - - /// #7a1dcc - static Color get purple600 => Color(0xFF7A1DCC); - - /// #6617b3 - static Color get purple700 => Color(0xFF6617B3); - - /// #55138f - static Color get purple800 => Color(0xFF55138F); - - /// #470c72 - static Color get purple900 => Color(0xFF470C72); - - /// #380758 - static Color get purple1000 => Color(0xFF380758); - - /// #ffe5ef - static Color get magenta100 => Color(0xFFFFE5EF); - - /// #ffb8d1 - static Color get magenta200 => Color(0xFFFFB8D1); - - /// #ff8ab2 - static Color get magenta300 => Color(0xFFFF8AB2); - - /// #ff5c93 - static Color get magenta400 => Color(0xFFFF5C93); - - /// #fb006d - static Color get magenta500 => Color(0xFFFB006D); - - /// #d2005f - static Color get magenta600 => Color(0xFFD2005F); - - /// #d2005f - static Color get magenta700 => Color(0xFFD2005F); - - /// #850040 - static Color get magenta800 => Color(0xFF850040); - - /// #610031 - static Color get magenta900 => Color(0xFF610031); - - /// #400022 - static Color get magenta1000 => Color(0xFF400022); - - /// #ffd2dd - static Color get red100 => Color(0xFFFFD2DD); - - /// #ffa5b4 - static Color get red200 => Color(0xFFFFA5B4); - - /// #ff7d87 - static Color get red300 => Color(0xFFFF7D87); - - /// #ff5050 - static Color get red400 => Color(0xFFFF5050); - - /// #f33641 - static Color get red500 => Color(0xFFF33641); - - /// #e71d32 - static Color get red600 => Color(0xFFE71D32); - - /// #ad1625 - static Color get red700 => Color(0xFFAD1625); - - /// #8c101c - static Color get red800 => Color(0xFF8C101C); - - /// #6e0a1e - static Color get red900 => Color(0xFF6E0A1E); - - /// #4c0a17 - static Color get red1000 => Color(0xFF4C0A17); - - /// #f336411a - static Color get redAlphaRed50010 => Color(0x1AF33641); - - /// #fff3d5 - static Color get orange100 => Color(0xFFFFF3D5); - - /// #ffe4ab - static Color get orange200 => Color(0xFFFFE4AB); - - /// #ffd181 - static Color get orange300 => Color(0xFFFFD181); - - /// #ffbe62 - static Color get orange400 => Color(0xFFFFBE62); - - /// #ffa02e - static Color get orange500 => Color(0xFFFFA02E); - - /// #db7e21 - static Color get orange600 => Color(0xFFDB7E21); - - /// #b75f17 - static Color get orange700 => Color(0xFFB75F17); - - /// #93450e - static Color get orange800 => Color(0xFF93450E); - - /// #7a3108 - static Color get orange900 => Color(0xFF7A3108); - - /// #602706 - static Color get orange1000 => Color(0xFF602706); - - /// #fff9b2 - static Color get yellow100 => Color(0xFFFFF9B2); - - /// #ffec66 - static Color get yellow200 => Color(0xFFFFEC66); - - /// #ffdf1a - static Color get yellow300 => Color(0xFFFFDF1A); - - /// #ffcc00 - static Color get yellow400 => Color(0xFFFFCC00); - - /// #ffce00 - static Color get yellow500 => Color(0xFFFFCE00); - - /// #e6b800 - static Color get yellow600 => Color(0xFFE6B800); - - /// #cc9f00 - static Color get yellow700 => Color(0xFFCC9F00); - - /// #b38a00 - static Color get yellow800 => Color(0xFFB38A00); - - /// #9a7500 - static Color get yellow900 => Color(0xFF9A7500); - - /// #7f6200 - static Color get yellow1000 => Color(0xFF7F6200); - - /// #fcf2f2 - static Color get subtleColorRose100 => Color(0xFFFCF2F2); - - /// #fae3e3 - static Color get subtleColorRose200 => Color(0xFFFAE3E3); - - /// #fad9d9 - static Color get subtleColorRose300 => Color(0xFFFAD9D9); - - /// #edadad - static Color get subtleColorRose400 => Color(0xFFEDADAD); - - /// #cc4e4e - static Color get subtleColorRose500 => Color(0xFFCC4E4E); - - /// #702828 - static Color get subtleColorRose600 => Color(0xFF702828); - - /// #fcf4f0 - static Color get subtleColorPapaya100 => Color(0xFFFCF4F0); - - /// #fae8de - static Color get subtleColorPapaya200 => Color(0xFFFAE8DE); - - /// #fadfd2 - static Color get subtleColorPapaya300 => Color(0xFFFADFD2); - - /// #f0bda3 - static Color get subtleColorPapaya400 => Color(0xFFF0BDA3); - - /// #d67240 - static Color get subtleColorPapaya500 => Color(0xFFD67240); - - /// #6b3215 - static Color get subtleColorPapaya600 => Color(0xFF6B3215); - - /// #fff7ed - static Color get subtleColorTangerine100 => Color(0xFFFFF7ED); - - /// #fcedd9 - static Color get subtleColorTangerine200 => Color(0xFFFCEDD9); - - /// #fae5ca - static Color get subtleColorTangerine300 => Color(0xFFFAE5CA); - - /// #f2cb99 - static Color get subtleColorTangerine400 => Color(0xFFF2CB99); - - /// #db8f2c - static Color get subtleColorTangerine500 => Color(0xFFDB8F2C); - - /// #613b0a - static Color get subtleColorTangerine600 => Color(0xFF613B0A); - - /// #fff9ec - static Color get subtleColorMango100 => Color(0xFFFFF9EC); - - /// #fcf1d7 - static Color get subtleColorMango200 => Color(0xFFFCF1D7); - - /// #fae9c3 - static Color get subtleColorMango300 => Color(0xFFFAE9C3); - - /// #f5d68e - static Color get subtleColorMango400 => Color(0xFFF5D68E); - - /// #e0a416 - static Color get subtleColorMango500 => Color(0xFFE0A416); - - /// #5c4102 - static Color get subtleColorMango600 => Color(0xFF5C4102); - - /// #fffbe8 - static Color get subtleColorLemon100 => Color(0xFFFFFBE8); - - /// #fcf5cf - static Color get subtleColorLemon200 => Color(0xFFFCF5CF); - - /// #faefb9 - static Color get subtleColorLemon300 => Color(0xFFFAEFB9); - - /// #f5e282 - static Color get subtleColorLemon400 => Color(0xFFF5E282); - - /// #e0bb00 - static Color get subtleColorLemon500 => Color(0xFFE0BB00); - - /// #574800 - static Color get subtleColorLemon600 => Color(0xFF574800); - - /// #f9fae6 - static Color get subtleColorOlive100 => Color(0xFFF9FAE6); - - /// #f6f7d0 - static Color get subtleColorOlive200 => Color(0xFFF6F7D0); - - /// #f0f2b3 - static Color get subtleColorOlive300 => Color(0xFFF0F2B3); - - /// #dbde83 - static Color get subtleColorOlive400 => Color(0xFFDBDE83); - - /// #adb204 - static Color get subtleColorOlive500 => Color(0xFFADB204); - - /// #4a4c03 - static Color get subtleColorOlive600 => Color(0xFF4A4C03); - - /// #f6f9e6 - static Color get subtleColorLime100 => Color(0xFFF6F9E6); - - /// #eef5ce - static Color get subtleColorLime200 => Color(0xFFEEF5CE); - - /// #e7f0bb - static Color get subtleColorLime300 => Color(0xFFE7F0BB); - - /// #cfdb91 - static Color get subtleColorLime400 => Color(0xFFCFDB91); - - /// #92a822 - static Color get subtleColorLime500 => Color(0xFF92A822); - - /// #414d05 - static Color get subtleColorLime600 => Color(0xFF414D05); - - /// #f4faeb - static Color get subtleColorGrass100 => Color(0xFFF4FAEB); - - /// #e9f5d7 - static Color get subtleColorGrass200 => Color(0xFFE9F5D7); - - /// #def0c5 - static Color get subtleColorGrass300 => Color(0xFFDEF0C5); - - /// #bfd998 - static Color get subtleColorGrass400 => Color(0xFFBFD998); - - /// #75a828 - static Color get subtleColorGrass500 => Color(0xFF75A828); - - /// #334d0c - static Color get subtleColorGrass600 => Color(0xFF334D0C); - - /// #f1faf0 - static Color get subtleColorForest100 => Color(0xFFF1FAF0); - - /// #e2f5df - static Color get subtleColorForest200 => Color(0xFFE2F5DF); - - /// #d7f0d3 - static Color get subtleColorForest300 => Color(0xFFD7F0D3); - - /// #a8d6a1 - static Color get subtleColorForest400 => Color(0xFFA8D6A1); - - /// #49a33b - static Color get subtleColorForest500 => Color(0xFF49A33B); - - /// #1e4f16 - static Color get subtleColorForest600 => Color(0xFF1E4F16); - - /// #f0faf6 - static Color get subtleColorJade100 => Color(0xFFF0FAF6); - - /// #dff5eb - static Color get subtleColorJade200 => Color(0xFFDFF5EB); - - /// #cef0e1 - static Color get subtleColorJade300 => Color(0xFFCEF0E1); - - /// #90d1b5 - static Color get subtleColorJade400 => Color(0xFF90D1B5); - - /// #1c9963 - static Color get subtleColorJade500 => Color(0xFF1C9963); - - /// #075231 - static Color get subtleColorJade600 => Color(0xFF075231); - - /// #f0f9fa - static Color get subtleColorAqua100 => Color(0xFFF0F9FA); - - /// #dff3f5 - static Color get subtleColorAqua200 => Color(0xFFDFF3F5); - - /// #ccecf0 - static Color get subtleColorAqua300 => Color(0xFFCCECF0); - - /// #83ccd4 - static Color get subtleColorAqua400 => Color(0xFF83CCD4); - - /// #008e9e - static Color get subtleColorAqua500 => Color(0xFF008E9E); - - /// #004e57 - static Color get subtleColorAqua600 => Color(0xFF004E57); - - /// #f0f6fa - static Color get subtleColorAzure100 => Color(0xFFF0F6FA); - - /// #e1eef7 - static Color get subtleColorAzure200 => Color(0xFFE1EEF7); - - /// #d3e6f5 - static Color get subtleColorAzure300 => Color(0xFFD3E6F5); - - /// #88c0eb - static Color get subtleColorAzure400 => Color(0xFF88C0EB); - - /// #0877cc - static Color get subtleColorAzure500 => Color(0xFF0877CC); - - /// #154469 - static Color get subtleColorAzure600 => Color(0xFF154469); - - /// #f0f3fa - static Color get subtleColorDenim100 => Color(0xFFF0F3FA); - - /// #e3ebfa - static Color get subtleColorDenim200 => Color(0xFFE3EBFA); - - /// #d7e2f7 - static Color get subtleColorDenim300 => Color(0xFFD7E2F7); - - /// #9ab6ed - static Color get subtleColorDenim400 => Color(0xFF9AB6ED); - - /// #3267d1 - static Color get subtleColorDenim500 => Color(0xFF3267D1); - - /// #223c70 - static Color get subtleColorDenim600 => Color(0xFF223C70); - - /// #f2f2fc - static Color get subtleColorMauve100 => Color(0xFFF2F2FC); - - /// #e6e6fa - static Color get subtleColorMauve200 => Color(0xFFE6E6FA); - - /// #dcdcf7 - static Color get subtleColorMauve300 => Color(0xFFDCDCF7); - - /// #aeaef5 - static Color get subtleColorMauve400 => Color(0xFFAEAEF5); - - /// #5555e0 - static Color get subtleColorMauve500 => Color(0xFF5555E0); - - /// #36366b - static Color get subtleColorMauve600 => Color(0xFF36366B); - - /// #f6f3fc - static Color get subtleColorLavender100 => Color(0xFFF6F3FC); - - /// #ebe3fa - static Color get subtleColorLavender200 => Color(0xFFEBE3FA); - - /// #e4daf7 - static Color get subtleColorLavender300 => Color(0xFFE4DAF7); - - /// #c1aaf0 - static Color get subtleColorLavender400 => Color(0xFFC1AAF0); - - /// #8153db - static Color get subtleColorLavender500 => Color(0xFF8153DB); - - /// #462f75 - static Color get subtleColorLavender600 => Color(0xFF462F75); - - /// #f7f0fa - static Color get subtleColorLilac100 => Color(0xFFF7F0FA); - - /// #f0e1f7 - static Color get subtleColorLilac200 => Color(0xFFF0E1F7); - - /// #edd7f7 - static Color get subtleColorLilac300 => Color(0xFFEDD7F7); - - /// #d3a9e8 - static Color get subtleColorLilac400 => Color(0xFFD3A9E8); - - /// #9e4cc7 - static Color get subtleColorLilac500 => Color(0xFF9E4CC7); - - /// #562d6b - static Color get subtleColorLilac600 => Color(0xFF562D6B); - - /// #faf0fa - static Color get subtleColorMallow100 => Color(0xFFFAF0FA); - - /// #f5e1f4 - static Color get subtleColorMallow200 => Color(0xFFF5E1F4); - - /// #f5d7f4 - static Color get subtleColorMallow300 => Color(0xFFF5D7F4); - - /// #dea4dc - static Color get subtleColorMallow400 => Color(0xFFDEA4DC); - - /// #b240af - static Color get subtleColorMallow500 => Color(0xFFB240AF); - - /// #632861 - static Color get subtleColorMallow600 => Color(0xFF632861); - - /// #f9eff3 - static Color get subtleColorCamellia100 => Color(0xFFF9EFF3); - - /// #f7e1eb - static Color get subtleColorCamellia200 => Color(0xFFF7E1EB); - - /// #f7d7e5 - static Color get subtleColorCamellia300 => Color(0xFFF7D7E5); - - /// #e5a3c0 - static Color get subtleColorCamellia400 => Color(0xFFE5A3C0); - - /// #c24279 - static Color get subtleColorCamellia500 => Color(0xFFC24279); - - /// #6e2343 - static Color get subtleColorCamellia600 => Color(0xFF6E2343); - - /// #f5f5f5 - static Color get subtleColorSmoke100 => Color(0xFFF5F5F5); - - /// #e8e8e8 - static Color get subtleColorSmoke200 => Color(0xFFE8E8E8); - - /// #dedede - static Color get subtleColorSmoke300 => Color(0xFFDEDEDE); - - /// #b8b8b8 - static Color get subtleColorSmoke400 => Color(0xFFB8B8B8); - - /// #6e6e6e - static Color get subtleColorSmoke500 => Color(0xFF6E6E6E); - - /// #404040 - static Color get subtleColorSmoke600 => Color(0xFF404040); - - /// #f2f4f7 - static Color get subtleColorIron100 => Color(0xFFF2F4F7); - - /// #e6e9f0 - static Color get subtleColorIron200 => Color(0xFFE6E9F0); - - /// #dadee5 - static Color get subtleColorIron300 => Color(0xFFDADEE5); - - /// #b0b5bf - static Color get subtleColorIron400 => Color(0xFFB0B5BF); - - /// #666f80 - static Color get subtleColorIron500 => Color(0xFF666F80); - - /// #394152 - static Color get subtleColorIron600 => Color(0xFF394152); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart deleted file mode 100644 index 3c97c06df3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/appflowy_default/semantic.dart +++ /dev/null @@ -1,332 +0,0 @@ -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: 2025-04-19T13:45:56.089922 -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import '../shared.dart'; -import 'primitive.dart'; - -class AppFlowyDefaultTheme implements AppFlowyThemeBuilder { - @override - AppFlowyThemeData light({ - String? fontFamily, - }) { - final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.light); - - final textColorScheme = AppFlowyTextColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral600, - tertiary: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral200, - inverse: AppFlowyPrimitiveTokens.neutralWhite, - onFill: AppFlowyPrimitiveTokens.neutralWhite, - theme: AppFlowyPrimitiveTokens.blue500, - themeHover: AppFlowyPrimitiveTokens.blue600, - action: AppFlowyPrimitiveTokens.blue500, - actionHover: AppFlowyPrimitiveTokens.blue600, - info: AppFlowyPrimitiveTokens.blue500, - infoHover: AppFlowyPrimitiveTokens.blue600, - success: AppFlowyPrimitiveTokens.green600, - successHover: AppFlowyPrimitiveTokens.green700, - warning: AppFlowyPrimitiveTokens.orange600, - warningHover: AppFlowyPrimitiveTokens.orange700, - error: AppFlowyPrimitiveTokens.red600, - errorHover: AppFlowyPrimitiveTokens.red700, - purple: AppFlowyPrimitiveTokens.purple500, - purpleHover: AppFlowyPrimitiveTokens.purple600, - ); - - final iconColorScheme = AppFlowyIconColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral600, - tertiary: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral200, - white: AppFlowyPrimitiveTokens.neutralWhite, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final borderColorScheme = AppFlowyBorderColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - greyPrimary: AppFlowyPrimitiveTokens.neutral1000, - greyPrimaryHover: AppFlowyPrimitiveTokens.neutral900, - greySecondary: AppFlowyPrimitiveTokens.neutral800, - greySecondaryHover: AppFlowyPrimitiveTokens.neutral700, - greyTertiary: AppFlowyPrimitiveTokens.neutral300, - greyTertiaryHover: AppFlowyPrimitiveTokens.neutral400, - greyQuaternary: AppFlowyPrimitiveTokens.neutral100, - greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral200, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red700, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final fillColorScheme = AppFlowyFillColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - primaryHover: AppFlowyPrimitiveTokens.neutral900, - secondary: AppFlowyPrimitiveTokens.neutral600, - secondaryHover: AppFlowyPrimitiveTokens.neutral500, - tertiary: AppFlowyPrimitiveTokens.neutral300, - tertiaryHover: AppFlowyPrimitiveTokens.neutral400, - quaternary: AppFlowyPrimitiveTokens.neutral100, - quaternaryHover: AppFlowyPrimitiveTokens.neutral200, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey100005, - primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100010, - primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, - primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, - white: AppFlowyPrimitiveTokens.neutralWhite, - whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, - whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, - black: AppFlowyPrimitiveTokens.neutralBlack, - themeLight: AppFlowyPrimitiveTokens.blue100, - themeLightHover: AppFlowyPrimitiveTokens.blue200, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, - infoLight: AppFlowyPrimitiveTokens.blue100, - infoLightHover: AppFlowyPrimitiveTokens.blue200, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successLight: AppFlowyPrimitiveTokens.green100, - successLightHover: AppFlowyPrimitiveTokens.green200, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningLight: AppFlowyPrimitiveTokens.orange100, - warningLightHover: AppFlowyPrimitiveTokens.orange200, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorLight: AppFlowyPrimitiveTokens.red100, - errorLightHover: AppFlowyPrimitiveTokens.red200, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red700, - errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, - purpleLight: AppFlowyPrimitiveTokens.purple100, - purpleLightHover: AppFlowyPrimitiveTokens.purple200, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - purpleThick: AppFlowyPrimitiveTokens.purple500, - ); - - final surfaceColorScheme = AppFlowySurfaceColorScheme( - primary: AppFlowyPrimitiveTokens.neutralWhite, - overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, - ); - - final backgroundColorScheme = AppFlowyBackgroundColorScheme( - primary: AppFlowyPrimitiveTokens.neutralWhite, - secondary: AppFlowyPrimitiveTokens.neutral100, - tertiary: AppFlowyPrimitiveTokens.neutral200, - quaternary: AppFlowyPrimitiveTokens.neutral300, - ); - - final brandColorScheme = AppFlowyBrandColorScheme( - skyline: Color(0xFF00B5FF), - aqua: Color(0xFF00C8FF), - violet: Color(0xFF9327FF), - amethyst: Color(0xFF8427E0), - berry: Color(0xFFE3006D), - coral: Color(0xFFFB006D), - golden: Color(0xFFF7931E), - amber: Color(0xFFFFBD00), - lemon: Color(0xFFFFCE00), - ); - - final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( - textHighlight: AppFlowyPrimitiveTokens.blue200, - ); - - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - } - - @override - AppFlowyThemeData dark({ - String? fontFamily, - }) { - final textStyle = AppFlowyBaseTextStyle.customFontFamily(fontFamily ?? ''); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark); - - final textColorScheme = AppFlowyTextColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - inverse: AppFlowyPrimitiveTokens.neutral1000, - onFill: AppFlowyPrimitiveTokens.neutralWhite, - theme: AppFlowyPrimitiveTokens.blue500, - themeHover: AppFlowyPrimitiveTokens.blue600, - action: AppFlowyPrimitiveTokens.blue500, - actionHover: AppFlowyPrimitiveTokens.blue600, - info: AppFlowyPrimitiveTokens.blue500, - infoHover: AppFlowyPrimitiveTokens.blue600, - success: AppFlowyPrimitiveTokens.green600, - successHover: AppFlowyPrimitiveTokens.green700, - warning: AppFlowyPrimitiveTokens.orange600, - warningHover: AppFlowyPrimitiveTokens.orange700, - error: AppFlowyPrimitiveTokens.red500, - errorHover: AppFlowyPrimitiveTokens.red400, - purple: AppFlowyPrimitiveTokens.purple500, - purpleHover: AppFlowyPrimitiveTokens.purple600, - ); - - final iconColorScheme = AppFlowyIconColorScheme( - primary: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - white: AppFlowyPrimitiveTokens.neutralWhite, - purpleThick: Color(0xFFFFFFFF), - purpleThickHover: Color(0xFFFFFFFF), - ); - - final borderColorScheme = AppFlowyBorderColorScheme( - primary: AppFlowyPrimitiveTokens.neutral800, - greyPrimary: AppFlowyPrimitiveTokens.neutral100, - greyPrimaryHover: AppFlowyPrimitiveTokens.neutral200, - greySecondary: AppFlowyPrimitiveTokens.neutral300, - greySecondaryHover: AppFlowyPrimitiveTokens.neutral400, - greyTertiary: AppFlowyPrimitiveTokens.neutral800, - greyTertiaryHover: AppFlowyPrimitiveTokens.neutral700, - greyQuaternary: AppFlowyPrimitiveTokens.neutral1000, - greyQuaternaryHover: AppFlowyPrimitiveTokens.neutral900, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue600, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorThick: AppFlowyPrimitiveTokens.red500, - errorThickHover: AppFlowyPrimitiveTokens.red400, - purpleThick: AppFlowyPrimitiveTokens.purple500, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - ); - - final fillColorScheme = AppFlowyFillColorScheme( - primary: AppFlowyPrimitiveTokens.neutral100, - primaryHover: AppFlowyPrimitiveTokens.neutral200, - secondary: AppFlowyPrimitiveTokens.neutral300, - secondaryHover: AppFlowyPrimitiveTokens.neutral400, - tertiary: AppFlowyPrimitiveTokens.neutral600, - tertiaryHover: AppFlowyPrimitiveTokens.neutral500, - quaternary: AppFlowyPrimitiveTokens.neutral1000, - quaternaryHover: AppFlowyPrimitiveTokens.neutral900, - transparent: AppFlowyPrimitiveTokens.neutralAlphaWhite0, - primaryAlpha5: AppFlowyPrimitiveTokens.neutralAlphaGrey10005, - primaryAlpha5Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey10010, - primaryAlpha80: AppFlowyPrimitiveTokens.neutralAlphaGrey100080, - primaryAlpha80Hover: AppFlowyPrimitiveTokens.neutralAlphaGrey100070, - white: AppFlowyPrimitiveTokens.neutralWhite, - whiteAlpha: AppFlowyPrimitiveTokens.neutralAlphaWhite20, - whiteAlphaHover: AppFlowyPrimitiveTokens.neutralAlphaWhite30, - black: AppFlowyPrimitiveTokens.neutralBlack, - themeLight: AppFlowyPrimitiveTokens.blue100, - themeLightHover: AppFlowyPrimitiveTokens.blue200, - themeThick: AppFlowyPrimitiveTokens.blue500, - themeThickHover: AppFlowyPrimitiveTokens.blue400, - themeSelect: AppFlowyPrimitiveTokens.blueAlphaBlue50015, - infoLight: AppFlowyPrimitiveTokens.blue100, - infoLightHover: AppFlowyPrimitiveTokens.blue200, - infoThick: AppFlowyPrimitiveTokens.blue500, - infoThickHover: AppFlowyPrimitiveTokens.blue600, - successLight: AppFlowyPrimitiveTokens.green100, - successLightHover: AppFlowyPrimitiveTokens.green200, - successThick: AppFlowyPrimitiveTokens.green600, - successThickHover: AppFlowyPrimitiveTokens.green700, - warningLight: AppFlowyPrimitiveTokens.orange100, - warningLightHover: AppFlowyPrimitiveTokens.orange200, - warningThick: AppFlowyPrimitiveTokens.orange600, - warningThickHover: AppFlowyPrimitiveTokens.orange700, - errorLight: AppFlowyPrimitiveTokens.red100, - errorLightHover: AppFlowyPrimitiveTokens.red200, - errorThick: AppFlowyPrimitiveTokens.red600, - errorThickHover: AppFlowyPrimitiveTokens.red500, - errorSelect: AppFlowyPrimitiveTokens.redAlphaRed50010, - purpleLight: AppFlowyPrimitiveTokens.purple100, - purpleLightHover: AppFlowyPrimitiveTokens.purple200, - purpleThickHover: AppFlowyPrimitiveTokens.purple600, - purpleThick: AppFlowyPrimitiveTokens.purple500, - ); - - final surfaceColorScheme = AppFlowySurfaceColorScheme( - primary: AppFlowyPrimitiveTokens.neutral900, - overlay: AppFlowyPrimitiveTokens.neutralAlphaBlack60, - ); - - final backgroundColorScheme = AppFlowyBackgroundColorScheme( - primary: AppFlowyPrimitiveTokens.neutral1000, - secondary: AppFlowyPrimitiveTokens.neutral900, - tertiary: AppFlowyPrimitiveTokens.neutral800, - quaternary: AppFlowyPrimitiveTokens.neutral700, - ); - - final brandColorScheme = AppFlowyBrandColorScheme( - skyline: Color(0xFF00B5FF), - aqua: Color(0xFF00C8FF), - violet: Color(0xFF9327FF), - amethyst: Color(0xFF8427E0), - berry: Color(0xFFE3006D), - coral: Color(0xFFFB006D), - golden: Color(0xFFF7931E), - amber: Color(0xFFFFBD00), - lemon: Color(0xFFFFCE00), - ); - - final otherColorsColorScheme = AppFlowyOtherColorsColorScheme( - textHighlight: AppFlowyPrimitiveTokens.blue200, - ); - - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart deleted file mode 100644 index 2b29371433..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/built_in_themes.dart +++ /dev/null @@ -1 +0,0 @@ -export 'appflowy_default/semantic.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart deleted file mode 100644 index ca058310b9..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/custom/custom_theme.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:appflowy_ui/appflowy_ui.dart'; - -class CustomTheme implements AppFlowyThemeBuilder { - const CustomTheme({ - required this.lightThemeJson, - required this.darkThemeJson, - }); - - final Map lightThemeJson; - final Map darkThemeJson; - - @override - AppFlowyThemeData light({ - String? fontFamily, - }) { - throw UnimplementedError(); - } - - @override - AppFlowyThemeData dark({ - String? fontFamily, - }) { - throw UnimplementedError(); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart deleted file mode 100644 index c9c3c3adb0..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/data/shared.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:appflowy_ui/src/theme/definition/border_radius/border_radius.dart'; -import 'package:appflowy_ui/src/theme/definition/shadow/shadow.dart'; -import 'package:appflowy_ui/src/theme/definition/spacing/spacing.dart'; -import 'package:flutter/material.dart'; - -class AppFlowySpacingConstant { - static const double spacing100 = 4; - static const double spacing200 = 6; - static const double spacing300 = 8; - static const double spacing400 = 12; - static const double spacing500 = 16; - static const double spacing600 = 20; -} - -class AppFlowyBorderRadiusConstant { - static const double radius100 = 4; - static const double radius200 = 6; - static const double radius300 = 8; - static const double radius400 = 12; - static const double radius500 = 16; - static const double radius600 = 20; -} - -class AppFlowySharedTokens { - const AppFlowySharedTokens(); - - static AppFlowyBorderRadius buildBorderRadius() { - return AppFlowyBorderRadius( - xs: AppFlowyBorderRadiusConstant.radius100, - s: AppFlowyBorderRadiusConstant.radius200, - m: AppFlowyBorderRadiusConstant.radius300, - l: AppFlowyBorderRadiusConstant.radius400, - xl: AppFlowyBorderRadiusConstant.radius500, - xxl: AppFlowyBorderRadiusConstant.radius600, - ); - } - - static AppFlowySpacing buildSpacing() { - return AppFlowySpacing( - xs: AppFlowySpacingConstant.spacing100, - s: AppFlowySpacingConstant.spacing200, - m: AppFlowySpacingConstant.spacing300, - l: AppFlowySpacingConstant.spacing400, - xl: AppFlowySpacingConstant.spacing500, - xxl: AppFlowySpacingConstant.spacing600, - ); - } - - static AppFlowyShadow buildShadow( - Brightness brightness, - ) { - return switch (brightness) { - Brightness.light => AppFlowyShadow( - small: [ - BoxShadow( - offset: Offset(0, 2), - blurRadius: 16, - color: Color(0x1F000000), - ), - ], - medium: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 32, - color: Color(0x1F000000), - ), - ], - ), - Brightness.dark => AppFlowyShadow( - small: [ - BoxShadow( - offset: Offset(0, 2), - blurRadius: 16, - color: Color(0x7A000000), - ), - ], - medium: [ - BoxShadow( - offset: Offset(0, 4), - blurRadius: 32, - color: Color(0x7A000000), - ), - ], - ), - }; - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart deleted file mode 100644 index fb07a5fe64..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/border_radius/border_radius.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowyBorderRadius { - const AppFlowyBorderRadius({ - required this.xs, - required this.s, - required this.m, - required this.l, - required this.xl, - required this.xxl, - }); - - final double xs; - final double s; - final double m; - final double l; - final double xl; - final double xxl; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart deleted file mode 100644 index c7324c34fe..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/background_color_scheme.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBackgroundColorScheme { - const AppFlowyBackgroundColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - - AppFlowyBackgroundColorScheme lerp( - AppFlowyBackgroundColorScheme other, - double t, - ) { - return AppFlowyBackgroundColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart deleted file mode 100644 index ca65ed1fb4..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/border_color_scheme.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBorderColorScheme { - AppFlowyBorderColorScheme({ - required this.primary, - required this.greyPrimary, - required this.greyPrimaryHover, - required this.greySecondary, - required this.greySecondaryHover, - required this.greyTertiary, - required this.greyTertiaryHover, - required this.greyQuaternary, - required this.greyQuaternaryHover, - required this.transparent, - required this.themeThick, - required this.themeThickHover, - required this.infoThick, - required this.infoThickHover, - required this.successThick, - required this.successThickHover, - required this.warningThick, - required this.warningThickHover, - required this.errorThick, - required this.errorThickHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color greyPrimary; - final Color greyPrimaryHover; - final Color greySecondary; - final Color greySecondaryHover; - final Color greyTertiary; - final Color greyTertiaryHover; - final Color greyQuaternary; - final Color greyQuaternaryHover; - final Color transparent; - final Color themeThick; - final Color themeThickHover; - final Color infoThick; - final Color infoThickHover; - final Color successThick; - final Color successThickHover; - final Color warningThick; - final Color warningThickHover; - final Color errorThick; - final Color errorThickHover; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyBorderColorScheme lerp( - AppFlowyBorderColorScheme other, - double t, - ) { - return AppFlowyBorderColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - greyPrimary: Color.lerp(greyPrimary, other.greyPrimary, t)!, - greyPrimaryHover: - Color.lerp(greyPrimaryHover, other.greyPrimaryHover, t)!, - greySecondary: Color.lerp(greySecondary, other.greySecondary, t)!, - greySecondaryHover: - Color.lerp(greySecondaryHover, other.greySecondaryHover, t)!, - greyTertiary: Color.lerp(greyTertiary, other.greyTertiary, t)!, - greyTertiaryHover: - Color.lerp(greyTertiaryHover, other.greyTertiaryHover, t)!, - greyQuaternary: Color.lerp(greyQuaternary, other.greyQuaternary, t)!, - greyQuaternaryHover: - Color.lerp(greyQuaternaryHover, other.greyQuaternaryHover, t)!, - transparent: Color.lerp(transparent, other.transparent, t)!, - themeThick: Color.lerp(themeThick, other.themeThick, t)!, - themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, - infoThick: Color.lerp(infoThick, other.infoThick, t)!, - infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, - successThick: Color.lerp(successThick, other.successThick, t)!, - successThickHover: - Color.lerp(successThickHover, other.successThickHover, t)!, - warningThick: Color.lerp(warningThick, other.warningThick, t)!, - warningThickHover: - Color.lerp(warningThickHover, other.warningThickHover, t)!, - errorThick: Color.lerp(errorThick, other.errorThick, t)!, - errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart deleted file mode 100644 index 4140f6924a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/brand_color_scheme.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyBrandColorScheme { - const AppFlowyBrandColorScheme({ - required this.skyline, - required this.aqua, - required this.violet, - required this.amethyst, - required this.berry, - required this.coral, - required this.golden, - required this.amber, - required this.lemon, - }); - - final Color skyline; - final Color aqua; - final Color violet; - final Color amethyst; - final Color berry; - final Color coral; - final Color golden; - final Color amber; - final Color lemon; - - AppFlowyBrandColorScheme lerp( - AppFlowyBrandColorScheme other, - double t, - ) { - return AppFlowyBrandColorScheme( - skyline: Color.lerp(skyline, other.skyline, t)!, - aqua: Color.lerp(aqua, other.aqua, t)!, - violet: Color.lerp(violet, other.violet, t)!, - amethyst: Color.lerp(amethyst, other.amethyst, t)!, - berry: Color.lerp(berry, other.berry, t)!, - coral: Color.lerp(coral, other.coral, t)!, - golden: Color.lerp(golden, other.golden, t)!, - amber: Color.lerp(amber, other.amber, t)!, - lemon: Color.lerp(lemon, other.lemon, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart deleted file mode 100644 index 01952e1461..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/color_scheme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'background_color_scheme.dart'; -export 'border_color_scheme.dart'; -export 'brand_color_scheme.dart'; -export 'fill_color_scheme.dart'; -export 'icon_color_scheme.dart'; -export 'other_color_scheme.dart'; -export 'surface_color_scheme.dart'; -export 'text_color_scheme.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart deleted file mode 100644 index 3faac64dfc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/fill_color_scheme.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyFillColorScheme { - const AppFlowyFillColorScheme({ - required this.primary, - required this.primaryHover, - required this.secondary, - required this.secondaryHover, - required this.tertiary, - required this.tertiaryHover, - required this.quaternary, - required this.quaternaryHover, - required this.transparent, - required this.primaryAlpha5, - required this.primaryAlpha5Hover, - required this.primaryAlpha80, - required this.primaryAlpha80Hover, - required this.white, - required this.whiteAlpha, - required this.whiteAlphaHover, - required this.black, - required this.themeLight, - required this.themeLightHover, - required this.themeThick, - required this.themeThickHover, - required this.themeSelect, - required this.infoLight, - required this.infoLightHover, - required this.infoThick, - required this.infoThickHover, - required this.successLight, - required this.successLightHover, - required this.successThick, - required this.successThickHover, - required this.warningLight, - required this.warningLightHover, - required this.warningThick, - required this.warningThickHover, - required this.errorLight, - required this.errorLightHover, - required this.errorThick, - required this.errorThickHover, - required this.errorSelect, - required this.purpleLight, - required this.purpleLightHover, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color primaryHover; - final Color secondary; - final Color secondaryHover; - final Color tertiary; - final Color tertiaryHover; - final Color quaternary; - final Color quaternaryHover; - final Color transparent; - final Color primaryAlpha5; - final Color primaryAlpha5Hover; - final Color primaryAlpha80; - final Color primaryAlpha80Hover; - final Color white; - final Color whiteAlpha; - final Color whiteAlphaHover; - final Color black; - final Color themeLight; - final Color themeLightHover; - final Color themeThick; - final Color themeThickHover; - final Color themeSelect; - final Color infoLight; - final Color infoLightHover; - final Color infoThick; - final Color infoThickHover; - final Color successLight; - final Color successLightHover; - final Color successThick; - final Color successThickHover; - final Color warningLight; - final Color warningLightHover; - final Color warningThick; - final Color warningThickHover; - final Color errorLight; - final Color errorLightHover; - final Color errorThick; - final Color errorThickHover; - final Color errorSelect; - final Color purpleLight; - final Color purpleLightHover; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyFillColorScheme lerp( - AppFlowyFillColorScheme other, - double t, - ) { - return AppFlowyFillColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - primaryHover: Color.lerp(primaryHover, other.primaryHover, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - secondaryHover: Color.lerp(secondaryHover, other.secondaryHover, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - tertiaryHover: Color.lerp(tertiaryHover, other.tertiaryHover, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - quaternaryHover: Color.lerp(quaternaryHover, other.quaternaryHover, t)!, - transparent: Color.lerp(transparent, other.transparent, t)!, - primaryAlpha5: Color.lerp(primaryAlpha5, other.primaryAlpha5, t)!, - primaryAlpha5Hover: - Color.lerp(primaryAlpha5Hover, other.primaryAlpha5Hover, t)!, - primaryAlpha80: Color.lerp(primaryAlpha80, other.primaryAlpha80, t)!, - primaryAlpha80Hover: - Color.lerp(primaryAlpha80Hover, other.primaryAlpha80Hover, t)!, - white: Color.lerp(white, other.white, t)!, - whiteAlpha: Color.lerp(whiteAlpha, other.whiteAlpha, t)!, - whiteAlphaHover: Color.lerp(whiteAlphaHover, other.whiteAlphaHover, t)!, - black: Color.lerp(black, other.black, t)!, - themeLight: Color.lerp(themeLight, other.themeLight, t)!, - themeLightHover: Color.lerp(themeLightHover, other.themeLightHover, t)!, - themeThick: Color.lerp(themeThick, other.themeThick, t)!, - themeThickHover: Color.lerp(themeThickHover, other.themeThickHover, t)!, - themeSelect: Color.lerp(themeSelect, other.themeSelect, t)!, - infoLight: Color.lerp(infoLight, other.infoLight, t)!, - infoLightHover: Color.lerp(infoLightHover, other.infoLightHover, t)!, - infoThick: Color.lerp(infoThick, other.infoThick, t)!, - infoThickHover: Color.lerp(infoThickHover, other.infoThickHover, t)!, - successLight: Color.lerp(successLight, other.successLight, t)!, - successLightHover: - Color.lerp(successLightHover, other.successLightHover, t)!, - successThick: Color.lerp(successThick, other.successThick, t)!, - successThickHover: - Color.lerp(successThickHover, other.successThickHover, t)!, - warningLight: Color.lerp(warningLight, other.warningLight, t)!, - warningLightHover: - Color.lerp(warningLightHover, other.warningLightHover, t)!, - warningThick: Color.lerp(warningThick, other.warningThick, t)!, - warningThickHover: - Color.lerp(warningThickHover, other.warningThickHover, t)!, - errorLight: Color.lerp(errorLight, other.errorLight, t)!, - errorLightHover: Color.lerp(errorLightHover, other.errorLightHover, t)!, - errorThick: Color.lerp(errorThick, other.errorThick, t)!, - errorThickHover: Color.lerp(errorThickHover, other.errorThickHover, t)!, - errorSelect: Color.lerp(errorSelect, other.errorSelect, t)!, - purpleLight: Color.lerp(purpleLight, other.purpleLight, t)!, - purpleLightHover: - Color.lerp(purpleLightHover, other.purpleLightHover, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart deleted file mode 100644 index efe59b8b99..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/icon_color_scheme.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyIconColorScheme { - const AppFlowyIconColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - required this.white, - required this.purpleThick, - required this.purpleThickHover, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - final Color white; - final Color purpleThick; - final Color purpleThickHover; - - AppFlowyIconColorScheme lerp( - AppFlowyIconColorScheme other, - double t, - ) { - return AppFlowyIconColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - white: Color.lerp(white, other.white, t)!, - purpleThick: Color.lerp(purpleThick, other.purpleThick, t)!, - purpleThickHover: - Color.lerp(purpleThickHover, other.purpleThickHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart deleted file mode 100644 index 9bb21e54e6..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/other_color_scheme.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'dart:ui'; - -class AppFlowyOtherColorsColorScheme { - const AppFlowyOtherColorsColorScheme({ - required this.textHighlight, - }); - - final Color textHighlight; - - AppFlowyOtherColorsColorScheme lerp( - AppFlowyOtherColorsColorScheme other, - double t, - ) { - return AppFlowyOtherColorsColorScheme( - textHighlight: Color.lerp(textHighlight, other.textHighlight, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart deleted file mode 100644 index 67be450a04..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/surface_color_scheme.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowySurfaceColorScheme { - const AppFlowySurfaceColorScheme({ - required this.primary, - required this.overlay, - }); - - final Color primary; - final Color overlay; - - AppFlowySurfaceColorScheme lerp( - AppFlowySurfaceColorScheme other, - double t, - ) { - return AppFlowySurfaceColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - overlay: Color.lerp(overlay, other.overlay, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart deleted file mode 100644 index 17e1f057ce..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/color_scheme/text_color_scheme.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppFlowyTextColorScheme { - const AppFlowyTextColorScheme({ - required this.primary, - required this.secondary, - required this.tertiary, - required this.quaternary, - required this.inverse, - required this.onFill, - required this.theme, - required this.themeHover, - required this.action, - required this.actionHover, - required this.info, - required this.infoHover, - required this.success, - required this.successHover, - required this.warning, - required this.warningHover, - required this.error, - required this.errorHover, - required this.purple, - required this.purpleHover, - }); - - final Color primary; - final Color secondary; - final Color tertiary; - final Color quaternary; - final Color inverse; - final Color onFill; - final Color theme; - final Color themeHover; - final Color action; - final Color actionHover; - final Color info; - final Color infoHover; - final Color success; - final Color successHover; - final Color warning; - final Color warningHover; - final Color error; - final Color errorHover; - final Color purple; - final Color purpleHover; - - AppFlowyTextColorScheme lerp( - AppFlowyTextColorScheme other, - double t, - ) { - return AppFlowyTextColorScheme( - primary: Color.lerp(primary, other.primary, t)!, - secondary: Color.lerp(secondary, other.secondary, t)!, - tertiary: Color.lerp(tertiary, other.tertiary, t)!, - quaternary: Color.lerp(quaternary, other.quaternary, t)!, - inverse: Color.lerp(inverse, other.inverse, t)!, - onFill: Color.lerp(onFill, other.onFill, t)!, - theme: Color.lerp(theme, other.theme, t)!, - themeHover: Color.lerp(themeHover, other.themeHover, t)!, - action: Color.lerp(action, other.action, t)!, - actionHover: Color.lerp(actionHover, other.actionHover, t)!, - info: Color.lerp(info, other.info, t)!, - infoHover: Color.lerp(infoHover, other.infoHover, t)!, - success: Color.lerp(success, other.success, t)!, - successHover: Color.lerp(successHover, other.successHover, t)!, - warning: Color.lerp(warning, other.warning, t)!, - warningHover: Color.lerp(warningHover, other.warningHover, t)!, - error: Color.lerp(error, other.error, t)!, - errorHover: Color.lerp(errorHover, other.errorHover, t)!, - purple: Color.lerp(purple, other.purple, t)!, - purpleHover: Color.lerp(purpleHover, other.purpleHover, t)!, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart deleted file mode 100644 index 457b86265e..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/shadow/shadow.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/widgets.dart'; - -class AppFlowyShadow { - AppFlowyShadow({ - required this.small, - required this.medium, - }); - - final List small; - final List medium; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart deleted file mode 100644 index ea90784db3..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/spacing/spacing.dart +++ /dev/null @@ -1,17 +0,0 @@ -class AppFlowySpacing { - const AppFlowySpacing({ - required this.xs, - required this.s, - required this.m, - required this.l, - required this.xl, - required this.xxl, - }); - - final double xs; - final double s; - final double m; - final double l; - final double xl; - final double xxl; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart deleted file mode 100644 index 006f364f96..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/base/default_text_style.dart +++ /dev/null @@ -1,537 +0,0 @@ -import 'package:flutter/widgets.dart'; - -abstract class TextThemeType { - const TextThemeType({ - required this.fontFamily, - }); - - final String fontFamily; - - TextStyle standard({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle enhanced({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle prominent({ - String? family, - Color? color, - FontWeight? weight, - }); - - TextStyle underline({ - String? family, - Color? color, - FontWeight? weight, - }); -} - -class TextThemeHeading1 extends TextThemeType { - const TextThemeHeading1({ - required super.fontFamily, - }); - - @override - TextStyle standard({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({ - String? family, - Color? color, - FontWeight? weight, - }) => - _defaultTextStyle( - family: family ?? super.fontFamily, - fontSize: 36, - height: 40 / 36, - color: color, - weight: weight ?? FontWeight.bold, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - required double fontSize, - required double height, - TextDecoration decoration = TextDecoration.none, - Color? color, - FontWeight weight = FontWeight.bold, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading2 extends TextThemeType { - const TextThemeHeading2({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 24, - double height = 32 / 24, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading3 extends TextThemeType { - const TextThemeHeading3({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 20, - double height = 28 / 20, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeading4 extends TextThemeType { - const TextThemeHeading4({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w700, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w400, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 16, - double height = 22 / 16, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.w400, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - color: color, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - ); -} - -class TextThemeHeadline extends TextThemeType { - const TextThemeHeadline({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 24, - double height = 36 / 24, - TextDecoration decoration = TextDecoration.none, - FontWeight weight = FontWeight.normal, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeTitle extends TextThemeType { - const TextThemeTitle({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 20, - double height = 28 / 20, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeBody extends TextThemeType { - const TextThemeBody({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 14, - double height = 20 / 14, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} - -class TextThemeCaption extends TextThemeType { - const TextThemeCaption({ - required super.fontFamily, - }); - - @override - TextStyle standard({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - ); - - @override - TextStyle enhanced({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.w600, - ); - - @override - TextStyle prominent({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.bold, - ); - - @override - TextStyle underline({String? family, Color? color, FontWeight? weight}) => - _defaultTextStyle( - family: family ?? super.fontFamily, - color: color, - weight: weight ?? FontWeight.normal, - decoration: TextDecoration.underline, - ); - - static TextStyle _defaultTextStyle({ - required String family, - double fontSize = 12, - double height = 16 / 12, - FontWeight weight = FontWeight.normal, - TextDecoration decoration = TextDecoration.none, - Color? color, - }) => - TextStyle( - inherit: false, - fontSize: fontSize, - decoration: decoration, - fontStyle: FontStyle.normal, - fontWeight: weight, - height: height, - fontFamily: family, - textBaseline: TextBaseline.alphabetic, - leadingDistribution: TextLeadingDistribution.even, - color: color, - ); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart deleted file mode 100644 index 89c1278d93..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/text_style/text_style.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:appflowy_ui/src/theme/definition/text_style/base/default_text_style.dart'; - -class AppFlowyBaseTextStyle { - factory AppFlowyBaseTextStyle.customFontFamily(String fontFamily) => - AppFlowyBaseTextStyle( - heading1: TextThemeHeading1(fontFamily: fontFamily), - heading2: TextThemeHeading2(fontFamily: fontFamily), - heading3: TextThemeHeading3(fontFamily: fontFamily), - heading4: TextThemeHeading4(fontFamily: fontFamily), - headline: TextThemeHeadline(fontFamily: fontFamily), - title: TextThemeTitle(fontFamily: fontFamily), - body: TextThemeBody(fontFamily: fontFamily), - caption: TextThemeCaption(fontFamily: fontFamily), - ); - - const AppFlowyBaseTextStyle({ - this.heading1 = const TextThemeHeading1(fontFamily: ''), - this.heading2 = const TextThemeHeading2(fontFamily: ''), - this.heading3 = const TextThemeHeading3(fontFamily: ''), - this.heading4 = const TextThemeHeading4(fontFamily: ''), - this.headline = const TextThemeHeadline(fontFamily: ''), - this.title = const TextThemeTitle(fontFamily: ''), - this.body = const TextThemeBody(fontFamily: ''), - this.caption = const TextThemeCaption(fontFamily: ''), - }); - - final TextThemeType heading1; - final TextThemeType heading2; - final TextThemeType heading3; - final TextThemeType heading4; - final TextThemeType headline; - final TextThemeType title; - final TextThemeType body; - final TextThemeType caption; -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart deleted file mode 100644 index 1da45cfd2a..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/definition/theme_data.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'border_radius/border_radius.dart'; -import 'color_scheme/color_scheme.dart'; -import 'shadow/shadow.dart'; -import 'spacing/spacing.dart'; -import 'text_style/text_style.dart'; - -/// [AppFlowyThemeData] defines the structure of the design system, and contains -/// the data that all child widgets will have access to. -class AppFlowyThemeData { - const AppFlowyThemeData({ - required this.textColorScheme, - required this.textStyle, - required this.iconColorScheme, - required this.borderColorScheme, - required this.backgroundColorScheme, - required this.fillColorScheme, - required this.surfaceColorScheme, - required this.borderRadius, - required this.spacing, - required this.shadow, - required this.brandColorScheme, - required this.otherColorsColorScheme, - }); - - final AppFlowyTextColorScheme textColorScheme; - - final AppFlowyBaseTextStyle textStyle; - - final AppFlowyIconColorScheme iconColorScheme; - - final AppFlowyBorderColorScheme borderColorScheme; - - final AppFlowyBackgroundColorScheme backgroundColorScheme; - - final AppFlowyFillColorScheme fillColorScheme; - - final AppFlowySurfaceColorScheme surfaceColorScheme; - - final AppFlowyBorderRadius borderRadius; - - final AppFlowySpacing spacing; - - final AppFlowyShadow shadow; - - final AppFlowyBrandColorScheme brandColorScheme; - - final AppFlowyOtherColorsColorScheme otherColorsColorScheme; - - static AppFlowyThemeData lerp( - AppFlowyThemeData begin, - AppFlowyThemeData end, - double t, - ) { - return AppFlowyThemeData( - textColorScheme: begin.textColorScheme.lerp(end.textColorScheme, t), - textStyle: end.textStyle, - iconColorScheme: begin.iconColorScheme.lerp(end.iconColorScheme, t), - borderColorScheme: begin.borderColorScheme.lerp(end.borderColorScheme, t), - backgroundColorScheme: - begin.backgroundColorScheme.lerp(end.backgroundColorScheme, t), - fillColorScheme: begin.fillColorScheme.lerp(end.fillColorScheme, t), - surfaceColorScheme: - begin.surfaceColorScheme.lerp(end.surfaceColorScheme, t), - borderRadius: end.borderRadius, - spacing: end.spacing, - shadow: end.shadow, - brandColorScheme: begin.brandColorScheme.lerp(end.brandColorScheme, t), - otherColorsColorScheme: - begin.otherColorsColorScheme.lerp(end.otherColorsColorScheme, t), - ); - } -} - -/// [AppFlowyThemeBuilder] is used to build the light and dark themes. Extend -/// this class to create a built-in theme, or use the [CustomTheme] class to -/// create a custom theme from JSON data. -/// -/// See also: -/// -/// - [AppFlowyThemeData] for the main theme data class. -abstract class AppFlowyThemeBuilder { - const AppFlowyThemeBuilder(); - - AppFlowyThemeData light({ - String? fontFamily, - }); - - AppFlowyThemeData dark({ - String? fontFamily, - }); -} diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart deleted file mode 100644 index 000b7a0372..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/lib/src/theme/theme.dart +++ /dev/null @@ -1,8 +0,0 @@ -export 'appflowy_theme.dart'; -export 'data/built_in_themes.dart'; -export 'definition/border_radius/border_radius.dart'; -export 'definition/color_scheme/color_scheme.dart'; -export 'definition/theme_data.dart'; -export 'definition/spacing/spacing.dart'; -export 'definition/shadow/shadow.dart'; -export 'definition/text_style/text_style.dart'; diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml deleted file mode 100644 index 2f5633bb1e..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: appflowy_ui -description: "A Flutter package for AppFlowy UI components and widgets" -version: 1.0.0 -homepage: https://github.com/appflowy-io/appflowy - -environment: - sdk: ^3.6.2 - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_lints: ^5.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json deleted file mode 100644 index c46354b599..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Primitive.Mode 1.tokens.json +++ /dev/null @@ -1,984 +0,0 @@ -{ - "Neutral": { - "100": { - "$type": "color", - "$value": "#f8faff" - }, - "200": { - "$type": "color", - "$value": "#e4e8f5" - }, - "300": { - "$type": "color", - "$value": "#ced3e6" - }, - "400": { - "$type": "color", - "$value": "#b5bbd3" - }, - "500": { - "$type": "color", - "$value": "#989eb7" - }, - "600": { - "$type": "color", - "$value": "#6f748c" - }, - "700": { - "$type": "color", - "$value": "#54596e" - }, - "800": { - "$type": "color", - "$value": "#3c3f4e" - }, - "900": { - "$type": "color", - "$value": "#272930" - }, - "1000": { - "$type": "color", - "$value": "#21232a" - }, - "black": { - "$type": "color", - "$value": "#000000" - }, - "alpha-black-60": { - "$type": "color", - "$value": "#00000099" - }, - "white": { - "$type": "color", - "$value": "#ffffff" - }, - "alpha-white-0": { - "$type": "color", - "$value": "#ffffff00" - }, - "alpha-white-20": { - "$type": "color", - "$value": "#ffffff33" - }, - "alpha-white-30": { - "$type": "color", - "$value": "#ffffff4d" - }, - "alpha-grey-100-05": { - "$type": "color", - "$value": "#f9fafd0d" - }, - "alpha-grey-100-10": { - "$type": "color", - "$value": "#f9fafd1a" - }, - "alpha-grey-1000-05": { - "$type": "color", - "$value": "#1f23290d" - }, - "alpha-grey-1000-10": { - "$type": "color", - "$value": "#1f23291a" - }, - "alpha-grey-1000-70": { - "$type": "color", - "$value": "#1f2329b2" - }, - "alpha-grey-1000-80": { - "$type": "color", - "$value": "#1f2329cc" - } - }, - "Blue": { - "100": { - "$type": "color", - "$value": "#e3f6ff" - }, - "200": { - "$type": "color", - "$value": "#a9e2ff" - }, - "300": { - "$type": "color", - "$value": "#80d2ff" - }, - "400": { - "$type": "color", - "$value": "#4ec1ff" - }, - "500": { - "$type": "color", - "$value": "#00b5ff" - }, - "600": { - "$type": "color", - "$value": "#0092d6" - }, - "700": { - "$type": "color", - "$value": "#0078c0" - }, - "800": { - "$type": "color", - "$value": "#0065a9" - }, - "900": { - "$type": "color", - "$value": "#00508f" - }, - "1000": { - "$type": "color", - "$value": "#003c77" - }, - "alpha-blue-500-15": { - "$type": "color", - "$value": "#00b5ff26" - } - }, - "Green": { - "100": { - "$type": "color", - "$value": "#ecf9f5" - }, - "200": { - "$type": "color", - "$value": "#c3e5d8" - }, - "300": { - "$type": "color", - "$value": "#9ad1bc" - }, - "400": { - "$type": "color", - "$value": "#71bd9f" - }, - "500": { - "$type": "color", - "$value": "#48a982" - }, - "600": { - "$type": "color", - "$value": "#248569" - }, - "700": { - "$type": "color", - "$value": "#29725d" - }, - "800": { - "$type": "color", - "$value": "#2e6050" - }, - "900": { - "$type": "color", - "$value": "#305548" - }, - "1000": { - "$type": "color", - "$value": "#305244" - } - }, - "Purple": { - "100": { - "$type": "color", - "$value": "#f1e0ff" - }, - "200": { - "$type": "color", - "$value": "#e1b3ff" - }, - "300": { - "$type": "color", - "$value": "#d185ff" - }, - "400": { - "$type": "color", - "$value": "#bc58ff" - }, - "500": { - "$type": "color", - "$value": "#9327ff" - }, - "600": { - "$type": "color", - "$value": "#7a1dcc" - }, - "700": { - "$type": "color", - "$value": "#6617b3" - }, - "800": { - "$type": "color", - "$value": "#55138f" - }, - "900": { - "$type": "color", - "$value": "#470c72" - }, - "1000": { - "$type": "color", - "$value": "#380758" - } - }, - "Magenta": { - "100": { - "$type": "color", - "$value": "#ffe5ef" - }, - "200": { - "$type": "color", - "$value": "#ffb8d1" - }, - "300": { - "$type": "color", - "$value": "#ff8ab2" - }, - "400": { - "$type": "color", - "$value": "#ff5c93" - }, - "500": { - "$type": "color", - "$value": "#fb006d" - }, - "600": { - "$type": "color", - "$value": "#d2005f" - }, - "700": { - "$type": "color", - "$value": "#d2005f" - }, - "800": { - "$type": "color", - "$value": "#850040" - }, - "900": { - "$type": "color", - "$value": "#610031" - }, - "1000": { - "$type": "color", - "$value": "#400022" - } - }, - "Red": { - "100": { - "$type": "color", - "$value": "#ffd2dd" - }, - "200": { - "$type": "color", - "$value": "#ffa5b4" - }, - "300": { - "$type": "color", - "$value": "#ff7d87" - }, - "400": { - "$type": "color", - "$value": "#ff5050" - }, - "500": { - "$type": "color", - "$value": "#f33641" - }, - "600": { - "$type": "color", - "$value": "#e71d32" - }, - "700": { - "$type": "color", - "$value": "#ad1625" - }, - "800": { - "$type": "color", - "$value": "#8c101c" - }, - "900": { - "$type": "color", - "$value": "#6e0a1e" - }, - "1000": { - "$type": "color", - "$value": "#4c0a17" - }, - "alpha-red-500-10": { - "$type": "color", - "$value": "#f336411a" - } - }, - "Orange": { - "100": { - "$type": "color", - "$value": "#fff3d5" - }, - "200": { - "$type": "color", - "$value": "#ffe4ab" - }, - "300": { - "$type": "color", - "$value": "#ffd181" - }, - "400": { - "$type": "color", - "$value": "#ffbe62" - }, - "500": { - "$type": "color", - "$value": "#ffa02e" - }, - "600": { - "$type": "color", - "$value": "#db7e21" - }, - "700": { - "$type": "color", - "$value": "#b75f17" - }, - "800": { - "$type": "color", - "$value": "#93450e" - }, - "900": { - "$type": "color", - "$value": "#7a3108" - }, - "1000": { - "$type": "color", - "$value": "#602706" - } - }, - "Yellow": { - "100": { - "$type": "color", - "$value": "#fff9b2" - }, - "200": { - "$type": "color", - "$value": "#ffec66" - }, - "300": { - "$type": "color", - "$value": "#ffdf1a" - }, - "400": { - "$type": "color", - "$value": "#ffcc00" - }, - "500": { - "$type": "color", - "$value": "#ffce00" - }, - "600": { - "$type": "color", - "$value": "#e6b800" - }, - "700": { - "$type": "color", - "$value": "#cc9f00" - }, - "800": { - "$type": "color", - "$value": "#b38a00" - }, - "900": { - "$type": "color", - "$value": "#9a7500" - }, - "1000": { - "$type": "color", - "$value": "#7f6200" - } - }, - "Subtle_Color": { - "Rose": { - "100": { - "$type": "color", - "$value": "#fcf2f2" - }, - "200": { - "$type": "color", - "$value": "#fae3e3" - }, - "300": { - "$type": "color", - "$value": "#fad9d9" - }, - "400": { - "$type": "color", - "$value": "#edadad" - }, - "500": { - "$type": "color", - "$value": "#cc4e4e" - }, - "600": { - "$type": "color", - "$value": "#702828" - } - }, - "Papaya": { - "100": { - "$type": "color", - "$value": "#fcf4f0" - }, - "200": { - "$type": "color", - "$value": "#fae8de" - }, - "300": { - "$type": "color", - "$value": "#fadfd2" - }, - "400": { - "$type": "color", - "$value": "#f0bda3" - }, - "500": { - "$type": "color", - "$value": "#d67240" - }, - "600": { - "$type": "color", - "$value": "#6b3215" - } - }, - "Tangerine": { - "100": { - "$type": "color", - "$value": "#fff7ed" - }, - "200": { - "$type": "color", - "$value": "#fcedd9" - }, - "300": { - "$type": "color", - "$value": "#fae5ca" - }, - "400": { - "$type": "color", - "$value": "#f2cb99" - }, - "500": { - "$type": "color", - "$value": "#db8f2c" - }, - "600": { - "$type": "color", - "$value": "#613b0a" - } - }, - "Mango": { - "100": { - "$type": "color", - "$value": "#fff9ec" - }, - "200": { - "$type": "color", - "$value": "#fcf1d7" - }, - "300": { - "$type": "color", - "$value": "#fae9c3" - }, - "400": { - "$type": "color", - "$value": "#f5d68e" - }, - "500": { - "$type": "color", - "$value": "#e0a416" - }, - "600": { - "$type": "color", - "$value": "#5c4102" - } - }, - "Lemon": { - "100": { - "$type": "color", - "$value": "#fffbe8" - }, - "200": { - "$type": "color", - "$value": "#fcf5cf" - }, - "300": { - "$type": "color", - "$value": "#faefb9" - }, - "400": { - "$type": "color", - "$value": "#f5e282" - }, - "500": { - "$type": "color", - "$value": "#e0bb00" - }, - "600": { - "$type": "color", - "$value": "#574800" - } - }, - "Olive": { - "100": { - "$type": "color", - "$value": "#f9fae6" - }, - "200": { - "$type": "color", - "$value": "#f6f7d0" - }, - "300": { - "$type": "color", - "$value": "#f0f2b3" - }, - "400": { - "$type": "color", - "$value": "#dbde83" - }, - "500": { - "$type": "color", - "$value": "#adb204" - }, - "600": { - "$type": "color", - "$value": "#4a4c03" - } - }, - "Lime": { - "100": { - "$type": "color", - "$value": "#f6f9e6" - }, - "200": { - "$type": "color", - "$value": "#eef5ce" - }, - "300": { - "$type": "color", - "$value": "#e7f0bb" - }, - "400": { - "$type": "color", - "$value": "#cfdb91" - }, - "500": { - "$type": "color", - "$value": "#92a822" - }, - "600": { - "$type": "color", - "$value": "#414d05" - } - }, - "Grass": { - "100": { - "$type": "color", - "$value": "#f4faeb" - }, - "200": { - "$type": "color", - "$value": "#e9f5d7" - }, - "300": { - "$type": "color", - "$value": "#def0c5" - }, - "400": { - "$type": "color", - "$value": "#bfd998" - }, - "500": { - "$type": "color", - "$value": "#75a828" - }, - "600": { - "$type": "color", - "$value": "#334d0c" - } - }, - "Forest": { - "100": { - "$type": "color", - "$value": "#f1faf0" - }, - "200": { - "$type": "color", - "$value": "#e2f5df" - }, - "300": { - "$type": "color", - "$value": "#d7f0d3" - }, - "400": { - "$type": "color", - "$value": "#a8d6a1" - }, - "500": { - "$type": "color", - "$value": "#49a33b" - }, - "600": { - "$type": "color", - "$value": "#1e4f16" - } - }, - "Jade": { - "100": { - "$type": "color", - "$value": "#f0faf6" - }, - "200": { - "$type": "color", - "$value": "#dff5eb" - }, - "300": { - "$type": "color", - "$value": "#cef0e1" - }, - "400": { - "$type": "color", - "$value": "#90d1b5" - }, - "500": { - "$type": "color", - "$value": "#1c9963" - }, - "600": { - "$type": "color", - "$value": "#075231" - } - }, - "Aqua": { - "100": { - "$type": "color", - "$value": "#f0f9fa" - }, - "200": { - "$type": "color", - "$value": "#dff3f5" - }, - "300": { - "$type": "color", - "$value": "#ccecf0" - }, - "400": { - "$type": "color", - "$value": "#83ccd4" - }, - "500": { - "$type": "color", - "$value": "#008e9e" - }, - "600": { - "$type": "color", - "$value": "#004e57" - } - }, - "Azure": { - "100": { - "$type": "color", - "$value": "#f0f6fa" - }, - "200": { - "$type": "color", - "$value": "#e1eef7" - }, - "300": { - "$type": "color", - "$value": "#d3e6f5" - }, - "400": { - "$type": "color", - "$value": "#88c0eb" - }, - "500": { - "$type": "color", - "$value": "#0877cc" - }, - "600": { - "$type": "color", - "$value": "#154469" - } - }, - "Denim": { - "100": { - "$type": "color", - "$value": "#f0f3fa" - }, - "200": { - "$type": "color", - "$value": "#e3ebfa" - }, - "300": { - "$type": "color", - "$value": "#d7e2f7" - }, - "400": { - "$type": "color", - "$value": "#9ab6ed" - }, - "500": { - "$type": "color", - "$value": "#3267d1" - }, - "600": { - "$type": "color", - "$value": "#223c70" - } - }, - "Mauve": { - "100": { - "$type": "color", - "$value": "#f2f2fc" - }, - "200": { - "$type": "color", - "$value": "#e6e6fa" - }, - "300": { - "$type": "color", - "$value": "#dcdcf7" - }, - "400": { - "$type": "color", - "$value": "#aeaef5" - }, - "500": { - "$type": "color", - "$value": "#5555e0" - }, - "600": { - "$type": "color", - "$value": "#36366b" - } - }, - "Lavender": { - "100": { - "$type": "color", - "$value": "#f6f3fc" - }, - "200": { - "$type": "color", - "$value": "#ebe3fa" - }, - "300": { - "$type": "color", - "$value": "#e4daf7" - }, - "400": { - "$type": "color", - "$value": "#c1aaf0" - }, - "500": { - "$type": "color", - "$value": "#8153db" - }, - "600": { - "$type": "color", - "$value": "#462f75" - } - }, - "Lilac": { - "100": { - "$type": "color", - "$value": "#f7f0fa" - }, - "200": { - "$type": "color", - "$value": "#f0e1f7" - }, - "300": { - "$type": "color", - "$value": "#edd7f7" - }, - "400": { - "$type": "color", - "$value": "#d3a9e8" - }, - "500": { - "$type": "color", - "$value": "#9e4cc7" - }, - "600": { - "$type": "color", - "$value": "#562d6b" - } - }, - "Mallow": { - "100": { - "$type": "color", - "$value": "#faf0fa" - }, - "200": { - "$type": "color", - "$value": "#f5e1f4" - }, - "300": { - "$type": "color", - "$value": "#f5d7f4" - }, - "400": { - "$type": "color", - "$value": "#dea4dc" - }, - "500": { - "$type": "color", - "$value": "#b240af" - }, - "600": { - "$type": "color", - "$value": "#632861" - } - }, - "Camellia": { - "100": { - "$type": "color", - "$value": "#f9eff3" - }, - "200": { - "$type": "color", - "$value": "#f7e1eb" - }, - "300": { - "$type": "color", - "$value": "#f7d7e5" - }, - "400": { - "$type": "color", - "$value": "#e5a3c0" - }, - "500": { - "$type": "color", - "$value": "#c24279" - }, - "600": { - "$type": "color", - "$value": "#6e2343" - } - }, - "Smoke": { - "100": { - "$type": "color", - "$value": "#f5f5f5" - }, - "200": { - "$type": "color", - "$value": "#e8e8e8" - }, - "300": { - "$type": "color", - "$value": "#dedede" - }, - "400": { - "$type": "color", - "$value": "#b8b8b8" - }, - "500": { - "$type": "color", - "$value": "#6e6e6e" - }, - "600": { - "$type": "color", - "$value": "#404040" - } - }, - "Iron": { - "100": { - "$type": "color", - "$value": "#f2f4f7" - }, - "200": { - "$type": "color", - "$value": "#e6e9f0" - }, - "300": { - "$type": "color", - "$value": "#dadee5" - }, - "400": { - "$type": "color", - "$value": "#b0b5bf" - }, - "500": { - "$type": "color", - "$value": "#666f80" - }, - "600": { - "$type": "color", - "$value": "#394152" - } - } - }, - "Spacing": { - "0": { - "$type": "dimension", - "$value": "0px" - }, - "100": { - "$type": "dimension", - "$value": "4px" - }, - "200": { - "$type": "dimension", - "$value": "6px" - }, - "300": { - "$type": "dimension", - "$value": "8px" - }, - "400": { - "$type": "dimension", - "$value": "12px" - }, - "500": { - "$type": "dimension", - "$value": "16px" - }, - "600": { - "$type": "dimension", - "$value": "20px" - }, - "1000": { - "$type": "dimension", - "$value": "1000px" - } - }, - "Border-Radius": { - "0": { - "$type": "dimension", - "$value": "0px" - }, - "100": { - "$type": "dimension", - "$value": "4px" - }, - "200": { - "$type": "dimension", - "$value": "6px" - }, - "300": { - "$type": "dimension", - "$value": "8px" - }, - "400": { - "$type": "dimension", - "$value": "12px" - }, - "500": { - "$type": "dimension", - "$value": "16px" - }, - "600": { - "$type": "dimension", - "$value": "20px" - }, - "1000": { - "$type": "dimension", - "$value": "1000px" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json deleted file mode 100644 index 99d266c008..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Dark Mode.tokens.json +++ /dev/null @@ -1,1039 +0,0 @@ -{ - "Text": { - "primary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "inverse": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "on-fill": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "theme": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "action": { - "$type": "color", - "$value": "{Blue.500}" - }, - "action-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-hover": { - "$type": "color", - "$value": "{Red.400}" - }, - "purple": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Icon": { - "primary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "purple-thick": { - "$type": "color", - "$value": "#ffffff" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "#ffffff" - } - }, - "Border": { - "grey-primary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "grey-primary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "grey-secondary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "grey-secondary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "grey-tertiary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "grey-tertiary-hover": { - "$type": "color", - "$value": "{Neutral.700}" - }, - "grey-quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "grey-quaternary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.400}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Fill": { - "primary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "primary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "secondary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary-hover": { - "$type": "color", - "$value": "{Neutral.500}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "quaternary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "primary-alpha-5": { - "$type": "color", - "$value": "{Neutral.alpha-grey-100-05}", - "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." - }, - "primary-alpha-5-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-100-10}" - }, - "primary-alpha-80": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-80}" - }, - "primary-alpha-80-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-70}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "white-alpha": { - "$type": "color", - "$value": "{Neutral.alpha-white-20}" - }, - "white-alpha-hover": { - "$type": "color", - "$value": "{Neutral.alpha-white-30}" - }, - "black": { - "$type": "color", - "$value": "{Neutral.black}" - }, - "theme-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "theme-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.400}" - }, - "theme-select": { - "$type": "color", - "$value": "{Blue.alpha-blue-500-15}" - }, - "info-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "info-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-light": { - "$type": "color", - "$value": "{Green.100}" - }, - "success-light-hover": { - "$type": "color", - "$value": "{Green.200}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-light": { - "$type": "color", - "$value": "{Orange.100}" - }, - "warning-light-hover": { - "$type": "color", - "$value": "{Orange.200}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-light": { - "$type": "color", - "$value": "{Red.100}" - }, - "error-light-hover": { - "$type": "color", - "$value": "{Red.200}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.500}" - }, - "error-select": { - "$type": "color", - "$value": "{Red.alpha-red-500-10}" - }, - "purple-light": { - "$type": "color", - "$value": "{Purple.100}" - }, - "purple-light-hover": { - "$type": "color", - "$value": "{Purple.200}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - } - }, - "Surface": { - "primary": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "overlay": { - "$type": "color", - "$value": "{Neutral.alpha-black-60}" - } - }, - "Background": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.700}" - } - }, - "Badge_Color": { - "Rose": { - "rose-light-1": { - "$type": "color", - "$value": "#fcf2f2" - }, - "rose-light-2": { - "$type": "color", - "$value": "#fae3e3" - }, - "rose-light-3": { - "$type": "color", - "$value": "#fad9d9" - }, - "rose-thick-1": { - "$type": "color", - "$value": "#edadad" - }, - "rose-thick-2": { - "$type": "color", - "$value": "#cc4e4e" - }, - "rose-thick-3": { - "$type": "color", - "$value": "#702828" - } - }, - "Papaya": { - "papaya-light-1": { - "$type": "color", - "$value": "#fcf4f0" - }, - "papaya-light-2": { - "$type": "color", - "$value": "#fae8de" - }, - "papaya-light-3": { - "$type": "color", - "$value": "#fadfd2" - }, - "papaya-thick-1": { - "$type": "color", - "$value": "#f0bda3" - }, - "papaya-thick-2": { - "$type": "color", - "$value": "#d67240" - }, - "papaya-thick-3": { - "$type": "color", - "$value": "#6b3215" - } - }, - "Tangerine": { - "tangerine-light-1": { - "$type": "color", - "$value": "#fff7ed" - }, - "tangerine-light-2": { - "$type": "color", - "$value": "#fcedd9" - }, - "tangerine-light-3": { - "$type": "color", - "$value": "#fae5ca" - }, - "tangerine-thick-1": { - "$type": "color", - "$value": "#f2cb99" - }, - "tangerine-thick-2": { - "$type": "color", - "$value": "#db8f2c" - }, - "tangerine-thick-3": { - "$type": "color", - "$value": "#613b0a" - } - }, - "Mango": { - "mango-light-1": { - "$type": "color", - "$value": "#fff9ec" - }, - "mango-light-2": { - "$type": "color", - "$value": "#fcf1d7" - }, - "mango-light-3": { - "$type": "color", - "$value": "#fae9c3" - }, - "mango-thick-1": { - "$type": "color", - "$value": "#f5d68e" - }, - "mango-thick-2": { - "$type": "color", - "$value": "#e0a416" - }, - "mango-thick-3": { - "$type": "color", - "$value": "#5c4102" - } - }, - "Lemon": { - "lemon-light-1": { - "$type": "color", - "$value": "#fffbe8" - }, - "lemon-light-2": { - "$type": "color", - "$value": "#fcf5cf" - }, - "lemon-light-3": { - "$type": "color", - "$value": "#faefb9" - }, - "lemon-thick-1": { - "$type": "color", - "$value": "#f5e282" - }, - "lemon-thick-2": { - "$type": "color", - "$value": "#e0bb00" - }, - "lemon-thick-3": { - "$type": "color", - "$value": "#574800" - } - }, - "Olive": { - "olive-light-1": { - "$type": "color", - "$value": "#f9fae6" - }, - "olive-light-2": { - "$type": "color", - "$value": "#f6f7d0" - }, - "olive-light-3": { - "$type": "color", - "$value": "#f0f2b3" - }, - "olive-thick-1": { - "$type": "color", - "$value": "#dbde83" - }, - "olive-thick-2": { - "$type": "color", - "$value": "#adb204" - }, - "olive-thick-3": { - "$type": "color", - "$value": "#4a4c03" - } - }, - "Lime": { - "lime-light-1": { - "$type": "color", - "$value": "#f6f9e6" - }, - "lime-light-2": { - "$type": "color", - "$value": "#eef5ce" - }, - "lime-light-3": { - "$type": "color", - "$value": "#e7f0bb" - }, - "lime-thick-1": { - "$type": "color", - "$value": "#cfdb91" - }, - "lime-thick-2": { - "$type": "color", - "$value": "#92a822" - }, - "lime-thick-3": { - "$type": "color", - "$value": "#414d05" - } - }, - "Grass": { - "grass-light-1": { - "$type": "color", - "$value": "#f4faeb" - }, - "grass-light-2": { - "$type": "color", - "$value": "#e9f5d7" - }, - "grass-light-3": { - "$type": "color", - "$value": "#def0c5" - }, - "grass-thick-1": { - "$type": "color", - "$value": "#bfd998" - }, - "grass-thick-2": { - "$type": "color", - "$value": "#75a828" - }, - "grass-thick-3": { - "$type": "color", - "$value": "#334d0c" - } - }, - "Forest": { - "forest-light-1": { - "$type": "color", - "$value": "#f1faf0" - }, - "forest-light-2": { - "$type": "color", - "$value": "#e2f5df" - }, - "forest-light-3": { - "$type": "color", - "$value": "#d7f0d3" - }, - "forest-thick-1": { - "$type": "color", - "$value": "#a8d6a1" - }, - "forest-thick-2": { - "$type": "color", - "$value": "#49a33b" - }, - "forest-thick-3": { - "$type": "color", - "$value": "#1e4f16" - } - }, - "Jade": { - "jade-light-1": { - "$type": "color", - "$value": "#f0faf6" - }, - "jade-light-2": { - "$type": "color", - "$value": "#dff5eb" - }, - "jade-light-3": { - "$type": "color", - "$value": "#cef0e1" - }, - "jade-thick-1": { - "$type": "color", - "$value": "#90d1b5" - }, - "jade-thick-2": { - "$type": "color", - "$value": "#1c9963" - }, - "jade-thick-3": { - "$type": "color", - "$value": "#075231" - } - }, - "Aqua": { - "aqua-light-1": { - "$type": "color", - "$value": "#f0f9fa" - }, - "aqua-light-2": { - "$type": "color", - "$value": "#dff3f5" - }, - "aqua-light-3": { - "$type": "color", - "$value": "#ccecf0" - }, - "aqua-thick-1": { - "$type": "color", - "$value": "#83ccd4" - }, - "aqua-thick-2": { - "$type": "color", - "$value": "#008e9e" - }, - "aqua-thick-3": { - "$type": "color", - "$value": "#004e57" - } - }, - "Azure": { - "azure-light-1": { - "$type": "color", - "$value": "#f0f6fa" - }, - "azure-light-2": { - "$type": "color", - "$value": "#e1eef7" - }, - "azure-light-3": { - "$type": "color", - "$value": "#d3e6f5" - }, - "azure-thick-1": { - "$type": "color", - "$value": "#88c0eb" - }, - "azure-thick-2": { - "$type": "color", - "$value": "#0877cc" - }, - "azure-thick-3": { - "$type": "color", - "$value": "#154469" - } - }, - "Denim": { - "denim-light-1": { - "$type": "color", - "$value": "#f0f3fa" - }, - "denim-light-2": { - "$type": "color", - "$value": "#e3ebfa" - }, - "denim-light-3": { - "$type": "color", - "$value": "#d7e2f7" - }, - "denim-thick-1": { - "$type": "color", - "$value": "#9ab6ed" - }, - "denim-thick-2": { - "$type": "color", - "$value": "#3267d1" - }, - "denim-thick-3": { - "$type": "color", - "$value": "#223c70" - } - }, - "Mauve": { - "mauve-light-1": { - "$type": "color", - "$value": "#f2f2fc" - }, - "mauve-thick-2": { - "$type": "color", - "$value": "#5555e0" - }, - "mauve-thick-3": { - "$type": "color", - "$value": "#36366b" - }, - "mauve-thick-1": { - "$type": "color", - "$value": "#aeaef5" - } - }, - "Lavender": { - "lavender-light-1": { - "$type": "color", - "$value": "#f6f3fc" - }, - "lavender-light-2": { - "$type": "color", - "$value": "#ebe3fa" - }, - "lavender-light-3": { - "$type": "color", - "$value": "#e4daf7" - }, - "lavender-thick-1": { - "$type": "color", - "$value": "#c1aaf0" - }, - "lavender-thick-2": { - "$type": "color", - "$value": "#8153db" - }, - "lavender-thick-3": { - "$type": "color", - "$value": "#462f75" - } - }, - "Lilac": { - "liliac-light-1": { - "$type": "color", - "$value": "#f7f0fa" - }, - "liliac-light-2": { - "$type": "color", - "$value": "#f0e1f7" - }, - "liliac-light-3": { - "$type": "color", - "$value": "#edd7f7" - }, - "liliac-thick-1": { - "$type": "color", - "$value": "#d3a9e8" - }, - "liliac-thick-2": { - "$type": "color", - "$value": "#9e4cc7" - }, - "liliac-thick-3": { - "$type": "color", - "$value": "#562d6b" - } - }, - "Mallow": { - "mallow-light-1": { - "$type": "color", - "$value": "#faf0fa" - }, - "mallow-light-2": { - "$type": "color", - "$value": "#f5e1f4" - }, - "mallow-light-3": { - "$type": "color", - "$value": "#f5d7f4" - }, - "mallow-thick-1": { - "$type": "color", - "$value": "#dea4dc" - }, - "mallow-thick-2": { - "$type": "color", - "$value": "#b240af" - }, - "mallow-thick-3": { - "$type": "color", - "$value": "#632861" - } - }, - "Camellia": { - "camellia-light-1": { - "$type": "color", - "$value": "#f9eff3" - }, - "camellia-light-2": { - "$type": "color", - "$value": "#f7e1eb" - }, - "camellia-light-3": { - "$type": "color", - "$value": "#f7d7e5" - }, - "camellia-thick-1": { - "$type": "color", - "$value": "#e5a3c0" - }, - "camellia-thick-2": { - "$type": "color", - "$value": "#c24279" - }, - "camellia-thick-3": { - "$type": "color", - "$value": "#6e2343" - } - }, - "Smoke": { - "smoke-light-1": { - "$type": "color", - "$value": "#f5f5f5" - }, - "smoke-light-2": { - "$type": "color", - "$value": "#e8e8e8" - }, - "smoke-light-3": { - "$type": "color", - "$value": "#dedede" - }, - "smoke-thick-1": { - "$type": "color", - "$value": "#b8b8b8" - }, - "smoke-thick-2": { - "$type": "color", - "$value": "#6e6e6e" - }, - "smoke-thick-3": { - "$type": "color", - "$value": "#404040" - } - }, - "Iron": { - "icon-light-1": { - "$type": "color", - "$value": "#f2f4f7" - }, - "icon-light-2": { - "$type": "color", - "$value": "#e6e9f0" - }, - "icon-light-3": { - "$type": "color", - "$value": "#dadee5" - }, - "icon-thick-1": { - "$type": "color", - "$value": "#b0b5bf" - }, - "icon-thick-2": { - "$type": "color", - "$value": "#666f80" - }, - "icon-thick-3": { - "$type": "color", - "$value": "#394152" - } - } - }, - "Shadow": { - "sm": { - "$type": "dimension", - "$value": "0px" - }, - "md": { - "$type": "dimension", - "$value": "0px" - } - }, - "Brand": { - "Skyline": { - "$type": "color", - "$value": "#00b5ff" - }, - "Aqua": { - "$type": "color", - "$value": "#00c8ff" - }, - "Violet": { - "$type": "color", - "$value": "#9327ff" - }, - "Amethyst": { - "$type": "color", - "$value": "#8427e0" - }, - "Berry": { - "$type": "color", - "$value": "#e3006d" - }, - "Coral": { - "$type": "color", - "$value": "#fb006d" - }, - "Golden": { - "$type": "color", - "$value": "#f7931e" - }, - "Amber": { - "$type": "color", - "$value": "#ffbd00" - }, - "Lemon": { - "$type": "color", - "$value": "#ffce00" - } - }, - "Other_Colors": { - "text-highlight": { - "$type": "color", - "$value": "{Blue.200}" - } - }, - "Spacing": { - "spacing-0": { - "$type": "dimension", - "$value": "{Spacing.0}" - }, - "spacing-xs": { - "$type": "dimension", - "$value": "{Spacing.100}" - }, - "spacing-s": { - "$type": "dimension", - "$value": "{Spacing.200}" - }, - "spacing-m": { - "$type": "dimension", - "$value": "{Spacing.300}" - }, - "spacing-l": { - "$type": "dimension", - "$value": "{Spacing.400}" - }, - "spacing-xl": { - "$type": "dimension", - "$value": "{Spacing.500}" - }, - "spacing-xxl": { - "$type": "dimension", - "$value": "{Spacing.600}" - }, - "spacing-full": { - "$type": "dimension", - "$value": "{Spacing.1000}" - } - }, - "Border_Radius": { - "border-radius-0": { - "$type": "dimension", - "$value": "{Border-Radius.0}" - }, - "border-radius-xs": { - "$type": "dimension", - "$value": "{Border-Radius.100}" - }, - "border-radius-s": { - "$type": "dimension", - "$value": "{Border-Radius.200}" - }, - "border-radius-m": { - "$type": "dimension", - "$value": "{Border-Radius.300}" - }, - "border-radius-l": { - "$type": "dimension", - "$value": "{Border-Radius.400}" - }, - "border-radius-xl": { - "$type": "dimension", - "$value": "{Border-Radius.500}" - }, - "border-radius-xxl": { - "$type": "dimension", - "$value": "{Border-Radius.600}" - }, - "border-radius-full": { - "$type": "dimension", - "$value": "{Border-Radius.1000}" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json b/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json deleted file mode 100644 index 4e6b0543dc..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/Semantic.Light Mode.tokens.json +++ /dev/null @@ -1,1039 +0,0 @@ -{ - "Text": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "inverse": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "on-fill": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "theme": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "action": { - "$type": "color", - "$value": "{Blue.500}" - }, - "action-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "purple": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Icon": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Border": { - "grey-primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "grey-primary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "grey-secondary": { - "$type": "color", - "$value": "{Neutral.800}" - }, - "grey-secondary-hover": { - "$type": "color", - "$value": "{Neutral.700}" - }, - "grey-tertiary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "grey-tertiary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "grey-quaternary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "grey-quaternary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - } - }, - "Fill": { - "primary": { - "$type": "color", - "$value": "{Neutral.1000}" - }, - "primary-hover": { - "$type": "color", - "$value": "{Neutral.900}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.600}" - }, - "secondary-hover": { - "$type": "color", - "$value": "{Neutral.500}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.300}" - }, - "tertiary-hover": { - "$type": "color", - "$value": "{Neutral.400}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "quaternary-hover": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "transparent": { - "$type": "color", - "$value": "{Neutral.alpha-white-0}" - }, - "primary-alpha-5": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-05}", - "$description": "Used for hover state, eg. button, navigation item, menu item and grid item." - }, - "primary-alpha-5-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-10}" - }, - "primary-alpha-80": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-80}" - }, - "primary-alpha-80-hover": { - "$type": "color", - "$value": "{Neutral.alpha-grey-1000-70}" - }, - "white": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "white-alpha": { - "$type": "color", - "$value": "{Neutral.alpha-white-20}" - }, - "white-alpha-hover": { - "$type": "color", - "$value": "{Neutral.alpha-white-30}" - }, - "black": { - "$type": "color", - "$value": "{Neutral.black}" - }, - "theme-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "theme-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "theme-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "theme-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "theme-select": { - "$type": "color", - "$value": "{Blue.alpha-blue-500-15}" - }, - "info-light": { - "$type": "color", - "$value": "{Blue.100}" - }, - "info-light-hover": { - "$type": "color", - "$value": "{Blue.200}" - }, - "info-thick": { - "$type": "color", - "$value": "{Blue.500}" - }, - "info-thick-hover": { - "$type": "color", - "$value": "{Blue.600}" - }, - "success-light": { - "$type": "color", - "$value": "{Green.100}" - }, - "success-light-hover": { - "$type": "color", - "$value": "{Green.200}" - }, - "success-thick": { - "$type": "color", - "$value": "{Green.600}" - }, - "success-thick-hover": { - "$type": "color", - "$value": "{Green.700}" - }, - "warning-light": { - "$type": "color", - "$value": "{Orange.100}" - }, - "warning-light-hover": { - "$type": "color", - "$value": "{Orange.200}" - }, - "warning-thick": { - "$type": "color", - "$value": "{Orange.600}" - }, - "warning-thick-hover": { - "$type": "color", - "$value": "{Orange.700}" - }, - "error-light": { - "$type": "color", - "$value": "{Red.100}" - }, - "error-light-hover": { - "$type": "color", - "$value": "{Red.200}" - }, - "error-thick": { - "$type": "color", - "$value": "{Red.600}" - }, - "error-thick-hover": { - "$type": "color", - "$value": "{Red.700}" - }, - "error-select": { - "$type": "color", - "$value": "{Red.alpha-red-500-10}" - }, - "purple-light": { - "$type": "color", - "$value": "{Purple.100}" - }, - "purple-light-hover": { - "$type": "color", - "$value": "{Purple.200}" - }, - "purple-thick-hover": { - "$type": "color", - "$value": "{Purple.600}" - }, - "purple-thick": { - "$type": "color", - "$value": "{Purple.500}" - } - }, - "Surface": { - "primary": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "overlay": { - "$type": "color", - "$value": "{Neutral.alpha-black-60}" - } - }, - "Background": { - "primary": { - "$type": "color", - "$value": "{Neutral.white}" - }, - "secondary": { - "$type": "color", - "$value": "{Neutral.100}" - }, - "tertiary": { - "$type": "color", - "$value": "{Neutral.200}" - }, - "quaternary": { - "$type": "color", - "$value": "{Neutral.300}" - } - }, - "Badge_Color": { - "Rose": { - "rose-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Rose.100}" - }, - "rose-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Rose.200}" - }, - "rose-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Rose.300}" - }, - "rose-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Rose.400}" - }, - "rose-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Rose.500}" - }, - "rose-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Rose.600}" - } - }, - "Papaya": { - "papaya-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.100}" - }, - "papaya-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.200}" - }, - "papaya-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.300}" - }, - "papaya-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.400}" - }, - "papaya-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.500}" - }, - "papaya-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Papaya.600}" - } - }, - "Tangerine": { - "tangerine-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.100}" - }, - "tangerine-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.200}" - }, - "tangerine-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.300}" - }, - "tangerine-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.400}" - }, - "tangerine-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.500}" - }, - "tangerine-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Tangerine.600}" - } - }, - "Mango": { - "mango-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mango.100}" - }, - "mango-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Mango.200}" - }, - "mango-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Mango.300}" - }, - "mango-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mango.400}" - }, - "mango-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mango.500}" - }, - "mango-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mango.600}" - } - }, - "Lemon": { - "lemon-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.100}" - }, - "lemon-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.200}" - }, - "lemon-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.300}" - }, - "lemon-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.400}" - }, - "lemon-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.500}" - }, - "lemon-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lemon.600}" - } - }, - "Olive": { - "olive-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Olive.100}" - }, - "olive-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Olive.200}" - }, - "olive-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Olive.300}" - }, - "olive-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Olive.400}" - }, - "olive-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Olive.500}" - }, - "olive-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Olive.600}" - } - }, - "Lime": { - "lime-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lime.100}" - }, - "lime-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lime.200}" - }, - "lime-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lime.300}" - }, - "lime-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lime.400}" - }, - "lime-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lime.500}" - }, - "lime-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lime.600}" - } - }, - "Grass": { - "grass-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Grass.100}" - }, - "grass-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Grass.200}" - }, - "grass-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Grass.300}" - }, - "grass-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Grass.400}" - }, - "grass-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Grass.500}" - }, - "grass-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Grass.600}" - } - }, - "Forest": { - "forest-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Forest.100}" - }, - "forest-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Forest.200}" - }, - "forest-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Forest.300}" - }, - "forest-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Forest.400}" - }, - "forest-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Forest.500}" - }, - "forest-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Forest.600}" - } - }, - "Jade": { - "jade-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Jade.100}" - }, - "jade-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Jade.200}" - }, - "jade-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Jade.300}" - }, - "jade-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Jade.400}" - }, - "jade-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Jade.500}" - }, - "jade-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Jade.600}" - } - }, - "Aqua": { - "aqua-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.100}" - }, - "aqua-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.200}" - }, - "aqua-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.300}" - }, - "aqua-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.400}" - }, - "aqua-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.500}" - }, - "aqua-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Aqua.600}" - } - }, - "Azure": { - "azure-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Azure.100}" - }, - "azure-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Azure.200}" - }, - "azure-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Azure.300}" - }, - "azure-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Azure.400}" - }, - "azure-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Azure.500}" - }, - "azure-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Azure.600}" - } - }, - "Denim": { - "denim-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Denim.100}" - }, - "denim-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Denim.200}" - }, - "denim-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Denim.300}" - }, - "denim-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Denim.400}" - }, - "denim-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Denim.500}" - }, - "denim-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Denim.600}" - } - }, - "Mauve": { - "mauve-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.100}" - }, - "mauve-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.500}" - }, - "mauve-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.600}" - }, - "mauve-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mauve.400}" - } - }, - "Lavender": { - "lavender-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.100}" - }, - "lavender-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.200}" - }, - "lavender-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.300}" - }, - "lavender-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.400}" - }, - "lavender-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.500}" - }, - "lavender-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lavender.600}" - } - }, - "Lilac": { - "liliac-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.100}" - }, - "liliac-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.200}" - }, - "liliac-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.300}" - }, - "liliac-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.400}" - }, - "liliac-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.500}" - }, - "liliac-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Lilac.600}" - } - }, - "Mallow": { - "mallow-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.100}" - }, - "mallow-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.200}" - }, - "mallow-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.300}" - }, - "mallow-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.400}" - }, - "mallow-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.500}" - }, - "mallow-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Mallow.600}" - } - }, - "Camellia": { - "camellia-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.100}" - }, - "camellia-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.200}" - }, - "camellia-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.300}" - }, - "camellia-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.400}" - }, - "camellia-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.500}" - }, - "camellia-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Camellia.600}" - } - }, - "Smoke": { - "smoke-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.100}" - }, - "smoke-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.200}" - }, - "smoke-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.300}" - }, - "smoke-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.400}" - }, - "smoke-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.500}" - }, - "smoke-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Smoke.600}" - } - }, - "Iron": { - "icon-light-1": { - "$type": "color", - "$value": "{Subtle_Color.Iron.100}" - }, - "icon-light-2": { - "$type": "color", - "$value": "{Subtle_Color.Iron.200}" - }, - "icon-light-3": { - "$type": "color", - "$value": "{Subtle_Color.Iron.300}" - }, - "icon-thick-1": { - "$type": "color", - "$value": "{Subtle_Color.Iron.400}" - }, - "icon-thick-2": { - "$type": "color", - "$value": "{Subtle_Color.Iron.500}" - }, - "icon-thick-3": { - "$type": "color", - "$value": "{Subtle_Color.Iron.600}" - } - } - }, - "Shadow": { - "sm": { - "$type": "dimension", - "$value": "0px" - }, - "md": { - "$type": "dimension", - "$value": "0px" - } - }, - "Brand": { - "Skyline": { - "$type": "color", - "$value": "#00b5ff" - }, - "Aqua": { - "$type": "color", - "$value": "#00c8ff" - }, - "Violet": { - "$type": "color", - "$value": "#9327ff" - }, - "Amethyst": { - "$type": "color", - "$value": "#8427e0" - }, - "Berry": { - "$type": "color", - "$value": "#e3006d" - }, - "Coral": { - "$type": "color", - "$value": "#fb006d" - }, - "Golden": { - "$type": "color", - "$value": "#f7931e" - }, - "Amber": { - "$type": "color", - "$value": "#ffbd00" - }, - "Lemon": { - "$type": "color", - "$value": "#ffce00" - } - }, - "Other_Colors": { - "text-highlight": { - "$type": "color", - "$value": "{Blue.200}" - } - }, - "Spacing": { - "spacing-0": { - "$type": "dimension", - "$value": "{Spacing.0}" - }, - "spacing-xs": { - "$type": "dimension", - "$value": "{Spacing.100}" - }, - "spacing-s": { - "$type": "dimension", - "$value": "{Spacing.200}" - }, - "spacing-m": { - "$type": "dimension", - "$value": "{Spacing.300}" - }, - "spacing-l": { - "$type": "dimension", - "$value": "{Spacing.400}" - }, - "spacing-xl": { - "$type": "dimension", - "$value": "{Spacing.500}" - }, - "spacing-xxl": { - "$type": "dimension", - "$value": "{Spacing.600}" - }, - "spacing-full": { - "$type": "dimension", - "$value": "{Spacing.1000}" - } - }, - "Border_Radius": { - "border-radius-0": { - "$type": "dimension", - "$value": "{Border-Radius.0}" - }, - "border-radius-xs": { - "$type": "dimension", - "$value": "{Border-Radius.100}" - }, - "border-radius-s": { - "$type": "dimension", - "$value": "{Border-Radius.200}" - }, - "border-radius-m": { - "$type": "dimension", - "$value": "{Border-Radius.300}" - }, - "border-radius-l": { - "$type": "dimension", - "$value": "{Border-Radius.400}" - }, - "border-radius-xl": { - "$type": "dimension", - "$value": "{Border-Radius.500}" - }, - "border-radius-xxl": { - "$type": "dimension", - "$value": "{Border-Radius.600}" - }, - "border-radius-full": { - "$type": "dimension", - "$value": "{Border-Radius.1000}" - } - } -} \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart b/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart deleted file mode 100644 index bddcdb4eae..0000000000 --- a/frontend/appflowy_flutter/packages/appflowy_ui/script/generate_theme.dart +++ /dev/null @@ -1,300 +0,0 @@ -// ignore_for_file: avoid_print, depend_on_referenced_packages - -import 'dart:convert'; -import 'dart:io'; - -import 'package:collection/collection.dart'; - -void main() { - generatePrimitive(); - generateSemantic(); -} - -void generatePrimitive() { - // 1. Load the JSON file. - final jsonString = - File('script/Primitive.Mode 1.tokens.json').readAsStringSync(); - final jsonData = jsonDecode(jsonString) as Map; - - // 2. Prepare the output code. - final buffer = StringBuffer(); - - buffer.writeln(''' -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: ${DateTime.now().toIso8601String()} -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:flutter/material.dart'; - -class AppFlowyPrimitiveTokens { - AppFlowyPrimitiveTokens._();'''); - - // 3. Process each color category. - jsonData.forEach((categoryName, categoryData) { - categoryData.forEach((tokenName, tokenData) { - processPrimitiveTokenData( - buffer, - tokenData, - '${categoryName}_$tokenName', - ); - }); - }); - - buffer.writeln('}'); - - // 4. Write the output to a Dart file. - final outputFile = File('lib/src/theme/data/appflowy_default/primitive.dart'); - outputFile.writeAsStringSync(buffer.toString()); - - print('Successfully generated ${outputFile.path}'); -} - -void processPrimitiveTokenData( - StringBuffer buffer, - Map tokenData, - final String currentTokenName, -) { - if (tokenData - case { - r'$type': 'color', - r'$value': final String colorValue, - }) { - final dartColorValue = convertColor(colorValue); - final dartTokenName = currentTokenName.replaceAll('-', '_').toCamelCase(); - - buffer.writeln(''' - - /// $colorValue - static Color get $dartTokenName => Color(0x$dartColorValue);'''); - } else { - tokenData.forEach((key, value) { - if (value is Map) { - processPrimitiveTokenData(buffer, value, '${currentTokenName}_$key'); - } - }); - } -} - -void generateSemantic() { - // 1. Load the JSON file. - final lightJsonString = - File('script/Semantic.Light Mode.tokens.json').readAsStringSync(); - final darkJsonString = - File('script/Semantic.Dark Mode.tokens.json').readAsStringSync(); - final lightJsonData = jsonDecode(lightJsonString) as Map; - final darkJsonData = jsonDecode(darkJsonString) as Map; - - // 2. Prepare the output code. - final buffer = StringBuffer(); - - buffer.writeln(''' -// ignore_for_file: constant_identifier_names, non_constant_identifier_names -// -// AUTO-GENERATED - DO NOT EDIT DIRECTLY -// -// This file is auto-generated by the generate_theme.dart script -// Generation time: ${DateTime.now().toIso8601String()} -// -// To modify these colors, edit the source JSON files and run the script: -// -// dart run script/generate_theme.dart -// -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:flutter/material.dart'; - -import '../shared.dart'; -import 'primitive.dart'; - -class AppFlowyDefaultTheme implements AppFlowyThemeBuilder {'''); - - // 3. Process light mode semantic tokens - buffer.writeln(''' - @override - AppFlowyThemeData light() { - final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.light);'''); - - lightJsonData.forEach((categoryName, categoryData) { - if ([ - 'Spacing', - 'Border_Radius', - 'Shadow', - 'Badge_Color', - ].contains(categoryName)) { - return; - } - - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); - - categoryData.forEach((tokenName, tokenData) { - processSemanticTokenData(buffer, tokenData, tokenName); - }); - buffer.writeln(' );'); - }); - - buffer.writeln(); - buffer.writeln(''' - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - }'''); - - buffer.writeln(); - - buffer.writeln(''' - @override - AppFlowyThemeData dark() { - final textStyle = AppFlowyBaseTextStyle(); - final borderRadius = AppFlowySharedTokens.buildBorderRadius(); - final spacing = AppFlowySharedTokens.buildSpacing(); - final shadow = AppFlowySharedTokens.buildShadow(Brightness.dark);'''); - - darkJsonData.forEach((categoryName, categoryData) { - if ([ - 'Spacing', - 'Border_Radius', - 'Shadow', - 'Badge_Color', - ].contains(categoryName)) { - return; - } - - final fullCategoryName = "${categoryName}_color_scheme".toCamelCase(); - final className = 'AppFlowy${fullCategoryName.toCapitalize()}'; - - buffer - ..writeln() - ..writeln(' final $fullCategoryName = $className('); - - categoryData.forEach((tokenName, tokenData) { - if (tokenData is Map) { - processSemanticTokenData(buffer, tokenData, tokenName); - } - }); - buffer.writeln(' );'); - }); - - buffer.writeln(); - - buffer.writeln(''' - return AppFlowyThemeData( - textStyle: textStyle, - textColorScheme: textColorScheme, - borderColorScheme: borderColorScheme, - fillColorScheme: fillColorScheme, - surfaceColorScheme: surfaceColorScheme, - backgroundColorScheme: backgroundColorScheme, - iconColorScheme: iconColorScheme, - brandColorScheme: brandColorScheme, - otherColorsColorScheme: otherColorsColorScheme, - borderRadius: borderRadius, - spacing: spacing, - shadow: shadow, - ); - }'''); - - buffer.writeln('}'); - - // 4. Write the output to a Dart file. - final outputFile = File('lib/src/theme/data/appflowy_default/semantic.dart'); - outputFile.writeAsStringSync(buffer.toString()); - - print('Successfully generated ${outputFile.path}'); -} - -void processSemanticTokenData( - StringBuffer buffer, - Map json, - final String currentTokenName, -) { - if (json - case { - r'$type': 'color', - r'$value': final String value, - }) { - final semanticTokenName = - currentTokenName.replaceAll('-', '_').toCamelCase(); - - final String colorValueOrPrimitiveToken; - if (value.isColor) { - colorValueOrPrimitiveToken = 'Color(0x${convertColor(value)})'; - } else { - final primitiveToken = value - .replaceAll(RegExp(r'\{|\}'), '') - .replaceAll(RegExp(r'\.|-'), '_') - .toCamelCase(); - colorValueOrPrimitiveToken = 'AppFlowyPrimitiveTokens.$primitiveToken'; - } - - buffer.writeln(' $semanticTokenName: $colorValueOrPrimitiveToken,'); - } else { - json.forEach((key, value) { - if (value is Map) { - processSemanticTokenData( - buffer, - value, - '${currentTokenName}_$key', - ); - } - }); - } -} - -String convertColor(String hexColor) { - String color = hexColor.toUpperCase().replaceAll('#', ''); - if (color.length == 6) { - color = 'FF$color'; // Add missing alpha channel - } else if (color.length == 8) { - color = color.substring(6) + color.substring(0, 6); // Rearrange to ARGB - } - return color; -} - -extension on String { - String toCamelCase() { - return split('_').mapIndexed((index, part) { - if (index == 0) { - return part.toLowerCase(); - } else { - return part[0].toUpperCase() + part.substring(1).toLowerCase(); - } - }).join(); - } - - String toCapitalize() { - if (isEmpty) { - return this; - } - return '${this[0].toUpperCase()}${substring(1)}'; - } - - bool get isColor => - startsWith('#') || - (startsWith('0x') && length == 10) || - (startsWith('0xFF') && length == 12); -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 4178edd294..0c672a1f85 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -1,6 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/utils/color_converter.dart'; -import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dandelion.dart'; @@ -86,11 +87,6 @@ class FlowyColorScheme { required this.toggleButtonBGColor, required this.calendarWeekendBGColor, required this.gridRowCountColor, - required this.borderColor, - required this.scrollbarColor, - required this.scrollbarHoverColor, - required this.lightIconColor, - required this.toolbarHoverColor, }); final Color surface; @@ -149,51 +145,8 @@ class FlowyColorScheme { //grid bottom count color final Color gridRowCountColor; - final Color borderColor; - - final Color scrollbarColor; - final Color scrollbarHoverColor; - - final Color lightIconColor; - final Color toolbarHoverColor; - factory FlowyColorScheme.fromJson(Map json) => _$FlowyColorSchemeFromJson(json); Map toJson() => _$FlowyColorSchemeToJson(this); - - /// Merges the given [json] with the default color scheme - /// based on the given [brightness]. - /// - factory FlowyColorScheme.fromJsonSoft( - Map json, [ - Brightness brightness = Brightness.light, - ]) { - final colorScheme = brightness == Brightness.light - ? const DefaultColorScheme.light() - : const DefaultColorScheme.dark(); - final defaultMap = colorScheme.toJson(); - final mergedMap = Map.from(defaultMap)..addAll(json); - - return FlowyColorScheme.fromJson(mergedMap); - } - - /// Useful in validating that a teheme adheres to the default color scheme. - /// Returns the keys that are missing from the [json]. - /// - /// We use this for testing and debugging, and we might make it possible for users to - /// check their themes for missing keys in the future. - /// - /// Sample usage: - /// ```dart - /// final lightJson = await jsonDecode(await light.readAsString()); - /// final lightMissingKeys = FlowyColorScheme.getMissingKeys(lightJson); - /// ``` - /// - static List getMissingKeys(Map json) { - final defaultKeys = const DefaultColorScheme.light().toJson().keys; - final jsonKeys = json.keys; - - return defaultKeys.where((key) => !jsonKeys.contains(key)).toList(); - } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart index 8d49b8dfa1..9ca6bf1045 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/dandelion.dart @@ -1,4 +1,3 @@ -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flutter/material.dart'; import 'colorscheme.dart'; @@ -82,11 +81,6 @@ class DandelionColorScheme extends FlowyColorScheme { toggleButtonBGColor: _lightDandelionYellow, calendarWeekendBGColor: const Color(0xFFFBFBFC), gridRowCountColor: _black, - borderColor: ColorSchemeConstants.lightBorderColor, - scrollbarColor: const Color(0x3F171717), - scrollbarHoverColor: const Color(0x7F171717), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const DandelionColorScheme.dark() @@ -141,10 +135,5 @@ class DandelionColorScheme extends FlowyColorScheme { toggleButtonBGColor: _darkShader1, calendarWeekendBGColor: const Color(0xff121212), gridRowCountColor: _darkMain1, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 0e39de8fa8..e87fbbc424 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -2,48 +2,44 @@ import 'package:flutter/material.dart'; import 'colorscheme.dart'; -class ColorSchemeConstants { - static const white = Color(0xFFFFFFFF); - static const lightHover = Color(0xFFe0f8FF); - static const lightSelector = Color(0xFFf2fcFF); - static const lightBg1 = Color(0xFFf7f8fc); - static const lightBg2 = Color(0x0F1F2329); - static const lightShader1 = Color(0xFF333333); - static const lightShader3 = Color(0xFF828282); - static const lightShader5 = Color(0xFFe0e0e0); - static const lightShader6 = Color(0xFFf2f2f2); - static const lightMain1 = Color(0xFF00bcf0); - static const lightTint9 = Color(0xFFe1fbFF); - static const darkShader1 = Color(0xFF131720); - static const darkShader2 = Color(0xFF1A202C); - static const darkShader3 = Color(0xFF363D49); - static const darkShader5 = Color(0xFFBBC3CD); - static const darkShader6 = Color(0xFFF2F2F2); - static const darkMain1 = Color(0xFF00BCF0); - static const darkMain2 = Color(0xFF00BCF0); - static const darkInput = Color(0xFF282E3A); - static const lightBorderColor = Color(0xFFEDEDEE); - static const darkBorderColor = Color(0xFF3A3F49); -} +const _white = Color(0xFFFFFFFF); +const _lightHover = Color(0xFFe0f8FF); +const _lightSelector = Color(0xFFf2fcFF); +const _lightBg1 = Color(0xFFf7f8fc); +const _lightBg2 = Color(0x0F1F2329); +const _lightShader1 = Color(0xFF333333); +const _lightShader3 = Color(0xFF828282); +const _lightShader5 = Color(0xFFe0e0e0); +const _lightShader6 = Color(0xFFf2f2f2); +const _lightMain1 = Color(0xFF00bcf0); +const _lightTint9 = Color(0xFFe1fbFF); +const _darkShader1 = Color(0xFF131720); +const _darkShader2 = Color(0xFF1A202C); +const _darkShader3 = Color(0xFF363D49); +const _darkShader5 = Color(0xFFBBC3CD); +const _darkShader6 = Color(0xFFF2F2F2); +const _darkMain1 = Color(0xFF00BCF0); +const _darkMain2 = Color(0xFF00BCF0); +const _darkInput = Color(0xFF282E3A); class DefaultColorScheme extends FlowyColorScheme { const DefaultColorScheme.light() : super( - surface: ColorSchemeConstants.white, - hover: ColorSchemeConstants.lightHover, - selector: ColorSchemeConstants.lightSelector, + surface: _white, + hover: _lightHover, + selector: _lightSelector, red: const Color(0xFFfb006d), yellow: const Color(0xFFFFd667), green: const Color(0xFF66cf80), - shader1: ColorSchemeConstants.lightShader1, + shader1: _lightShader1, shader2: const Color(0xFF4f4f4f), - shader3: ColorSchemeConstants.lightShader3, + shader3: _lightShader3, shader4: const Color(0xFFbdbdbd), - shader5: ColorSchemeConstants.lightShader5, - shader6: ColorSchemeConstants.lightShader6, - shader7: ColorSchemeConstants.lightShader1, - bg1: ColorSchemeConstants.lightBg1, - bg2: ColorSchemeConstants.lightBg2, + shader5: _lightShader5, + shader6: _lightShader6, + shader7: _lightShader1, + bg1: _lightBg1, + bg2: _lightBg2, bg3: const Color(0xFFe2e4eb), bg4: const Color(0xFF2c144b), tint1: const Color(0xFFe8e0FF), @@ -54,94 +50,84 @@ class DefaultColorScheme extends FlowyColorScheme { tint6: const Color(0xFFf5FFdc), tint7: const Color(0xFFddFFd6), tint8: const Color(0xFFdeFFf1), - tint9: ColorSchemeConstants.lightTint9, - main1: ColorSchemeConstants.lightMain1, + tint9: _lightTint9, + main1: _lightMain1, main2: const Color(0xFF00b7ea), shadow: const Color.fromRGBO(0, 0, 0, 0.15), - sidebarBg: ColorSchemeConstants.lightBg1, - divider: ColorSchemeConstants.lightShader6, - topbarBg: ColorSchemeConstants.white, - icon: ColorSchemeConstants.lightShader1, - text: ColorSchemeConstants.lightShader1, + sidebarBg: _lightBg1, + divider: _lightShader6, + topbarBg: _white, + icon: _lightShader1, + text: _lightShader1, secondaryText: const Color(0xFF4f4f4f), strongText: Colors.black, - input: ColorSchemeConstants.white, - hint: ColorSchemeConstants.lightShader3, - primary: ColorSchemeConstants.lightMain1, - onPrimary: ColorSchemeConstants.white, - hoverBG1: ColorSchemeConstants.lightBg2, - hoverBG2: ColorSchemeConstants.lightHover, - hoverBG3: ColorSchemeConstants.lightShader6, - hoverFG: ColorSchemeConstants.lightShader1, - questionBubbleBG: ColorSchemeConstants.lightSelector, - progressBarBGColor: ColorSchemeConstants.lightTint9, - toolbarColor: ColorSchemeConstants.lightShader1, - toggleButtonBGColor: ColorSchemeConstants.lightShader5, + input: _white, + hint: _lightShader3, + primary: _lightMain1, + onPrimary: _white, + hoverBG1: _lightBg2, + hoverBG2: _lightHover, + hoverBG3: _lightShader6, + hoverFG: _lightShader1, + questionBubbleBG: _lightSelector, + progressBarBGColor: _lightTint9, + toolbarColor: _lightShader1, + toggleButtonBGColor: _lightShader5, calendarWeekendBGColor: const Color(0xFFFBFBFC), - gridRowCountColor: ColorSchemeConstants.lightShader1, - borderColor: ColorSchemeConstants.lightBorderColor, - scrollbarColor: const Color(0x3F171717), - scrollbarHoverColor: const Color(0x7F171717), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), + gridRowCountColor: _lightShader1, ); const DefaultColorScheme.dark() : super( - surface: ColorSchemeConstants.darkShader2, - hover: ColorSchemeConstants.darkMain1, - selector: ColorSchemeConstants.darkShader2, + surface: _darkShader2, + hover: _darkMain1, + selector: _darkShader2, red: const Color(0xFFfb006d), yellow: const Color(0xFFF7CF46), green: const Color(0xFF66CF80), - shader1: ColorSchemeConstants.darkShader1, - shader2: ColorSchemeConstants.darkShader2, - shader3: ColorSchemeConstants.darkShader3, + shader1: _darkShader1, + shader2: _darkShader2, + shader3: _darkShader3, shader4: const Color(0xFF505469), - shader5: ColorSchemeConstants.darkShader5, - shader6: ColorSchemeConstants.darkShader6, - shader7: ColorSchemeConstants.white, + shader5: _darkShader5, + shader6: _darkShader6, + shader7: _white, bg1: const Color(0xFF1A202C), bg2: const Color(0xFFEDEEF2), - bg3: ColorSchemeConstants.darkMain1, + bg3: _darkMain1, bg4: const Color(0xFF2C144B), - tint1: const Color(0x4D502FD6), - tint2: const Color(0x4DBF1CC0), - tint3: const Color(0x4DC42A53), - tint4: const Color(0x4DD77922), - tint5: const Color(0x4DC59A1A), - tint6: const Color(0x4DA4C824), - tint7: const Color(0x4D23CA2E), - tint8: const Color(0x4D19CCAC), - tint9: const Color(0x4D04A9D7), - main1: ColorSchemeConstants.darkMain2, + tint1: const Color(0x4d9327FF), + tint2: const Color(0x66FC0088), + tint3: const Color(0x4dFC00E2), + tint4: const Color(0x80BE5B00), + tint5: const Color(0x33F8EE00), + tint6: const Color(0x4d6DC300), + tint7: const Color(0x5900BD2A), + tint8: const Color(0x80008890), + tint9: const Color(0x4d0029FF), + main1: _darkMain2, main2: const Color(0xFF00B7EA), shadow: const Color(0xFF0F131C), sidebarBg: const Color(0xFF232B38), - divider: ColorSchemeConstants.darkShader3, - topbarBg: ColorSchemeConstants.darkShader1, - icon: ColorSchemeConstants.darkShader5, - text: ColorSchemeConstants.darkShader5, - secondaryText: ColorSchemeConstants.darkShader5, + divider: _darkShader3, + topbarBg: _darkShader1, + icon: _darkShader5, + text: _darkShader5, + secondaryText: _darkShader5, strongText: Colors.white, - input: ColorSchemeConstants.darkInput, + input: _darkInput, hint: const Color(0xFF59647a), - primary: ColorSchemeConstants.darkMain2, - onPrimary: ColorSchemeConstants.darkShader1, + primary: _darkMain2, + onPrimary: _darkShader1, hoverBG1: const Color(0x1AFFFFFF), - hoverBG2: ColorSchemeConstants.darkMain1, - hoverBG3: ColorSchemeConstants.darkShader3, + hoverBG2: _darkMain1, + hoverBG3: _darkShader3, hoverFG: const Color(0xE5FFFFFF), - questionBubbleBG: ColorSchemeConstants.darkShader3, - progressBarBGColor: ColorSchemeConstants.darkShader3, - toolbarColor: ColorSchemeConstants.darkInput, - toggleButtonBGColor: const Color(0xFF828282), - calendarWeekendBGColor: ColorSchemeConstants.darkShader1, - gridRowCountColor: ColorSchemeConstants.darkShader5, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: ColorSchemeConstants.lightShader6, + questionBubbleBG: _darkShader3, + progressBarBGColor: _darkShader3, + toolbarColor: _darkInput, + toggleButtonBGColor: _darkShader1, + calendarWeekendBGColor: _darkShader1, + gridRowCountColor: _darkShader5, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart index 590d26db3e..01eb66b02d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lavender.dart @@ -1,4 +1,3 @@ -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flutter/material.dart'; import 'colorscheme.dart'; @@ -78,11 +77,6 @@ class LavenderColorScheme extends FlowyColorScheme { toggleButtonBGColor: _lightSelector, calendarWeekendBGColor: const Color(0xFFFBFBFC), gridRowCountColor: _black, - borderColor: ColorSchemeConstants.lightBorderColor, - scrollbarColor: const Color(0x3F171717), - scrollbarHoverColor: const Color(0x7F171717), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const LavenderColorScheme.dark() @@ -137,10 +131,5 @@ class LavenderColorScheme extends FlowyColorScheme { toggleButtonBGColor: _darkShader1, calendarWeekendBGColor: const Color(0xff121212), gridRowCountColor: _darkMain1, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart index 3f39ae4c84..115de1e442 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/lemonade.dart @@ -1,4 +1,3 @@ -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; import 'package:flutter/material.dart'; import 'colorscheme.dart'; @@ -84,68 +83,59 @@ class LemonadeColorScheme extends FlowyColorScheme { toggleButtonBGColor: _lightDandelionYellow, calendarWeekendBGColor: const Color(0xFFFBFBFC), gridRowCountColor: _black, - borderColor: ColorSchemeConstants.lightBorderColor, - scrollbarColor: const Color(0x3F171717), - scrollbarHoverColor: const Color(0x7F171717), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: const Color(0xFFF2F4F7), ); const LemonadeColorScheme.dark() : super( - surface: const Color(0xff292929), - hover: const Color(0xff1f1f1f), - selector: _darkShader2, - red: const Color(0xfffb006d), - yellow: const Color(0xffffd667), - green: const Color(0xff66cf80), - shader1: _white, - shader2: _darkShader2, - shader3: const Color(0xff828282), - shader4: const Color(0xffbdbdbd), - shader5: _darkShader5, - shader6: _darkShader6, - shader7: _white, - bg1: const Color(0xFFD5A200), - bg2: _black, - bg3: _darkMain1, - bg4: const Color(0xff2c144b), - tint1: const Color(0x4d9327FF), - tint2: const Color(0x66FC0088), - tint3: const Color(0x4dFC00E2), - tint4: const Color(0x80BE5B00), - tint5: const Color(0x33F8EE00), - tint6: const Color(0x4d6DC300), - tint7: const Color(0x5900BD2A), - tint8: const Color(0x80008890), - tint9: const Color(0x4d0029FF), - main1: _darkMain1, - main2: _darkMain1, - shadow: _black, - sidebarBg: const Color(0xff232B38), - divider: _darkShader3, - topbarBg: _darkShader1, - icon: _darkShader5, - text: _darkShader5, - secondaryText: _darkShader5, - strongText: Colors.white, - input: _darkInput, - hint: _darkShader5, - primary: _darkMain1, - onPrimary: _darkShader1, - hoverBG1: _darkMain1, - hoverBG2: _darkMain1, - hoverBG3: _darkShader3, - hoverFG: _darkShader1, - questionBubbleBG: _darkShader3, - progressBarBGColor: _darkShader3, - toolbarColor: _darkInput, - toggleButtonBGColor: _darkShader1, - calendarWeekendBGColor: const Color(0xff121212), - gridRowCountColor: _darkMain1, - borderColor: ColorSchemeConstants.darkBorderColor, - scrollbarColor: const Color(0x40FFFFFF), - scrollbarHoverColor: const Color(0x80FFFFFF), - lightIconColor: const Color(0xFF8F959E), - toolbarHoverColor: _lightShader6); + surface: const Color(0xff292929), + hover: const Color(0xff1f1f1f), + selector: _darkShader2, + red: const Color(0xfffb006d), + yellow: const Color(0xffffd667), + green: const Color(0xff66cf80), + shader1: _white, + shader2: _darkShader2, + shader3: const Color(0xff828282), + shader4: const Color(0xffbdbdbd), + shader5: _darkShader5, + shader6: _darkShader6, + shader7: _white, + bg1: const Color(0xFFD5A200), + bg2: _black, + bg3: _darkMain1, + bg4: const Color(0xff2c144b), + tint1: const Color(0x4d9327FF), + tint2: const Color(0x66FC0088), + tint3: const Color(0x4dFC00E2), + tint4: const Color(0x80BE5B00), + tint5: const Color(0x33F8EE00), + tint6: const Color(0x4d6DC300), + tint7: const Color(0x5900BD2A), + tint8: const Color(0x80008890), + tint9: const Color(0x4d0029FF), + main1: _darkMain1, + main2: _darkMain1, + shadow: _black, + sidebarBg: const Color(0xff232B38), + divider: _darkShader3, + topbarBg: _darkShader1, + icon: _darkShader5, + text: _darkShader5, + secondaryText: _darkShader5, + strongText: Colors.white, + input: _darkInput, + hint: _darkShader5, + primary: _darkMain1, + onPrimary: _darkShader1, + hoverBG1: _darkMain1, + hoverBG2: _darkMain1, + hoverBG3: _darkShader3, + hoverFG: _darkShader1, + questionBubbleBG: _darkShader3, + progressBarBGColor: _darkShader3, + toolbarColor: _darkInput, + toggleButtonBGColor: _darkShader1, + calendarWeekendBGColor: const Color(0xff121212), + gridRowCountColor: _darkMain1, + ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart index 1e6f6a99e2..2e4d082761 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/file_picker/file_picker_impl.dart @@ -1,7 +1,5 @@ -import 'package:flutter/services.dart'; - -import 'package:file_picker/file_picker.dart' as fp; import 'package:flowy_infra/file_picker/file_picker_service.dart'; +import 'package:file_picker/file_picker.dart' as fp; class FilePicker implements FilePickerService { @override @@ -37,11 +35,6 @@ class FilePicker implements FilePickerService { return FilePickerResult(result?.files ?? []); } - /// On Desktop it will return the path to which the file should be saved. - /// - /// On Mobile it will return the path to where the file has been saved, and will - /// automatically save it. The [bytes] parameter is required on Mobile. - /// @override Future saveFile({ String? dialogTitle, @@ -50,7 +43,6 @@ class FilePicker implements FilePickerService { FileType type = FileType.any, List? allowedExtensions, bool lockParentWindow = false, - Uint8List? bytes, }) async { final result = await fp.FilePicker.platform.saveFile( dialogTitle: dialogTitle, @@ -59,7 +51,6 @@ class FilePicker implements FilePickerService { type: type, allowedExtensions: allowedExtensions, lockParentWindow: lockParentWindow, - bytes: bytes, ); return result; diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 6f37058f00..f3a6869735 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -48,10 +48,6 @@ String languageFromLocale(Locale locale) { default: return locale.languageCode; } - case "mr": - return "मराठी"; - case "he": - return "עברית"; case "hu": return "Magyar"; case "id": diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/platform_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/platform_extension.dart deleted file mode 100644 index 9e2085e3e4..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/platform_extension.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; - -extension PlatformExtension on Platform { - /// Returns true if the operating system is macOS and not running on Web platform. - static bool get isMacOS { - if (kIsWeb) { - return false; - } - return Platform.isMacOS; - } - - /// Returns true if the operating system is Windows and not running on Web platform. - static bool get isWindows { - if (kIsWeb) { - return false; - } - return Platform.isWindows; - } - - /// Returns true if the operating system is Linux and not running on Web platform. - static bool get isLinux { - if (kIsWeb) { - return false; - } - return Platform.isLinux; - } - - static bool get isDesktopOrWeb { - if (kIsWeb) { - return true; - } - return isDesktop; - } - - static bool get isDesktop { - if (kIsWeb) { - return false; - } - return Platform.isWindows || Platform.isLinux || Platform.isMacOS; - } - - static bool get isMobile { - if (kIsWeb) { - return false; - } - return Platform.isAndroid || Platform.isIOS; - } - - static bool get isNotMobile { - if (kIsWeb) { - return false; - } - return !isMobile; - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart index 0dbfc84564..ee1adee332 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart @@ -24,11 +24,8 @@ class DynamicPluginBloc extends Bloc { } Future onLoadRequested(Emitter emit) async { - emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); } Future addPlugin(Emitter emit) async { @@ -36,41 +33,31 @@ class DynamicPluginBloc extends Bloc { try { final plugin = await FlowyPluginService.pick(); if (plugin == null) { - return emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + return; } await FlowyPluginService.instance.addPlugin(plugin); } on PluginCompilationException catch (exception) { - return emit( - DynamicPluginState.compilationFailure(errorMessage: exception.message), - ); + return emit(DynamicPluginState.compilationFailure( + errorMessage: exception.message)); } emit(const DynamicPluginState.compilationSuccess()); - emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); } Future removePlugin( - Emitter emit, - String name, - ) async { + Emitter emit, String name) async { emit(const DynamicPluginState.processing()); final plugin = await FlowyPluginService.instance.lookup(name: name); if (plugin == null) { - return emit( - DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), - ); + emit(DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins)); + return; } await FlowyPluginService.removePlugin(plugin); @@ -78,8 +65,7 @@ class DynamicPluginBloc extends Bloc { emit(const DynamicPluginState.deletionSuccess()); emit( DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins, - ), + plugins: await FlowyPluginService.instance.plugins), ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart index 2f31ffbf1e..93e972eeba 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart @@ -1,8 +1,6 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:file/memory.dart'; import 'package:flowy_infra/colorscheme/colorscheme.dart'; import 'package:flowy_infra/plugins/service/models/exceptions.dart'; @@ -125,9 +123,8 @@ class FlowyDynamicPlugin { late final FlowyColorScheme darkTheme; try { - lightTheme = FlowyColorScheme.fromJsonSoft( - await jsonDecode(await light.readAsString()), - ); + lightTheme = FlowyColorScheme.fromJson( + await jsonDecode(await light.readAsString())); } catch (e) { throw PluginCompilationException( 'The light theme json file is not valid.', @@ -135,10 +132,8 @@ class FlowyDynamicPlugin { } try { - darkTheme = FlowyColorScheme.fromJsonSoft( - await jsonDecode(await dark.readAsString()), - Brightness.dark, - ); + darkTheme = FlowyColorScheme.fromJson( + await jsonDecode(await dark.readAsString())); } catch (e) { throw PluginCompilationException( 'The dark theme json file is not valid.', diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart index f58dad95b5..b66d836bb3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/size.dart @@ -57,6 +57,8 @@ class Sizes { static double get hit => 40 * hitScale; static double get iconMed => 20; + + static double get sideBarWidth => 250 * hitScale; } class Corners { @@ -80,7 +82,4 @@ class Corners { static const BorderRadius s12Border = BorderRadius.all(s12Radius); static const Radius s12Radius = Radius.circular(12); - - static const BorderRadius s16Border = BorderRadius.all(s16Radius); - static const Radius s16Radius = Radius.circular(16); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 9ce1f0323d..48b4dfe86d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -38,11 +38,6 @@ class AFThemeExtension extends ThemeExtension { required this.gridRowCountColor, required this.background, required this.onBackground, - required this.borderColor, - required this.scrollbarColor, - required this.scrollbarHoverColor, - required this.toolbarHoverColor, - required this.lightIconColor, }); final Color? warning; @@ -79,17 +74,6 @@ class AFThemeExtension extends ThemeExtension { final Color background; final Color onBackground; - /// The color of the border of the widget. - /// - /// This is used in the divider, outline border, etc. - final Color borderColor; - - final Color scrollbarColor; - final Color scrollbarHoverColor; - - final Color toolbarHoverColor; - final Color lightIconColor; - @override AFThemeExtension copyWith({ Color? warning, @@ -121,11 +105,6 @@ class AFThemeExtension extends ThemeExtension { TextStyle? caption, Color? background, Color? onBackground, - Color? borderColor, - Color? scrollbarColor, - Color? scrollbarHoverColor, - Color? lightIconColor, - Color? toolbarHoverColor, }) => AFThemeExtension( warning: warning ?? this.warning, @@ -158,11 +137,6 @@ class AFThemeExtension extends ThemeExtension { caption: caption ?? this.caption, onBackground: onBackground ?? this.onBackground, background: background ?? this.background, - borderColor: borderColor ?? this.borderColor, - scrollbarColor: scrollbarColor ?? this.scrollbarColor, - scrollbarHoverColor: scrollbarHoverColor ?? this.scrollbarHoverColor, - lightIconColor: lightIconColor ?? this.lightIconColor, - toolbarHoverColor: toolbarHoverColor ?? this.toolbarHoverColor, ); @override @@ -214,13 +188,6 @@ class AFThemeExtension extends ThemeExtension { caption: other.caption, onBackground: Color.lerp(onBackground, other.onBackground, t)!, background: Color.lerp(background, other.background, t)!, - borderColor: Color.lerp(borderColor, other.borderColor, t)!, - scrollbarColor: Color.lerp(scrollbarColor, other.scrollbarColor, t)!, - scrollbarHoverColor: - Color.lerp(scrollbarHoverColor, other.scrollbarHoverColor, t)!, - lightIconColor: Color.lerp(lightIconColor, other.lightIconColor, t)!, - toolbarHoverColor: - Color.lerp(toolbarHoverColor, other.toolbarHoverColor, t)!, ); } } @@ -254,17 +221,16 @@ enum FlowyTint { return null; } - Color color(BuildContext context, {AFThemeExtension? theme}) => - switch (this) { - FlowyTint.tint1 => theme?.tint1 ?? AFThemeExtension.of(context).tint1, - FlowyTint.tint2 => theme?.tint2 ?? AFThemeExtension.of(context).tint2, - FlowyTint.tint3 => theme?.tint3 ?? AFThemeExtension.of(context).tint3, - FlowyTint.tint4 => theme?.tint4 ?? AFThemeExtension.of(context).tint4, - FlowyTint.tint5 => theme?.tint5 ?? AFThemeExtension.of(context).tint5, - FlowyTint.tint6 => theme?.tint6 ?? AFThemeExtension.of(context).tint6, - FlowyTint.tint7 => theme?.tint7 ?? AFThemeExtension.of(context).tint7, - FlowyTint.tint8 => theme?.tint8 ?? AFThemeExtension.of(context).tint8, - FlowyTint.tint9 => theme?.tint9 ?? AFThemeExtension.of(context).tint9, + Color color(BuildContext context) => switch (this) { + FlowyTint.tint1 => AFThemeExtension.of(context).tint1, + FlowyTint.tint2 => AFThemeExtension.of(context).tint2, + FlowyTint.tint3 => AFThemeExtension.of(context).tint3, + FlowyTint.tint4 => AFThemeExtension.of(context).tint4, + FlowyTint.tint5 => AFThemeExtension.of(context).tint5, + FlowyTint.tint6 => AFThemeExtension.of(context).tint6, + FlowyTint.tint7 => AFThemeExtension.of(context).tint7, + FlowyTint.tint8 => AFThemeExtension.of(context).tint8, + FlowyTint.tint9 => AFThemeExtension.of(context).tint9, }; String get id => switch (this) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart index 4f80f81e62..19ca90d78f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/utils/color_converter.dart @@ -13,12 +13,5 @@ class ColorConverter implements JsonConverter { } @override - String toJson(Color color) { - final alpha = (color.a * 255).toInt().toRadixString(16).padLeft(2, '0'); - final red = (color.r * 255).toInt().toRadixString(16).padLeft(2, '0'); - final green = (color.g * 255).toInt().toRadixString(16).padLeft(2, '0'); - final blue = (color.b * 255).toInt().toRadixString(16).padLeft(2, '0'); - - return '0x$alpha$red$green$blue'.toLowerCase(); - } + String toJson(Color color) => "0x${color.value.toRadixString(16)}"; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index 9bf0245dc0..0653aacaa5 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -1,5 +1,5 @@ name: flowy_infra -description: AppFlowy Infra. +description: A new Flutter package project. version: 0.0.1 homepage: https://appflowy.io @@ -10,19 +10,59 @@ environment: dependencies: flutter: sdk: flutter + flutter_svg: ^2.0.2 json_annotation: ^4.7.0 path_provider: ^2.0.15 path: ^1.8.2 + textstyle_extensions: "2.0.0-nullsafety" time: ">=2.0.0" uuid: ">=2.2.2" - bloc: ^9.0.0 + bloc: ^8.1.2 freezed_annotation: ^2.1.0 file_picker: ^8.0.2 file: ^7.0.0 - analyzer: 6.11.0 dev_dependencies: - build_runner: ^2.4.9 + flutter_test: + sdk: flutter + build_runner: ^2.2.0 flutter_lints: ^3.0.1 freezed: ^2.4.7 json_serializable: ^6.5.4 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml index 4a8ad910cb..f2e3eb8749 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_platform_interface description: A new Flutter package project. version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: environment: sdk: ">=2.12.0 <3.0.0" @@ -17,3 +17,5 @@ dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^3.0.1 + +flutter: \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml index d4364a6400..bbdac0d2e4 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml @@ -1,7 +1,7 @@ name: flowy_infra_ui_web description: A new Flutter package project. version: 0.0.1 -homepage: https://github.com/appflowy-io/appflowy +homepage: publish_to: none environment: @@ -25,4 +25,4 @@ flutter: platforms: web: pluginClass: FlowyInfraUIPlugin - fileName: flowy_infra_ui_web.dart + fileName: flowy_infra_ui_web.dart \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index e1f58189b1..bbdeda8de3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart @@ -1,5 +1,4 @@ // Basis -export '/widget/flowy_tooltip.dart'; export '/widget/separated_flex.dart'; export '/widget/spacing.dart'; export 'basis.dart'; @@ -13,10 +12,7 @@ export 'src/flowy_overlay/option_overlay.dart'; export 'src/keyboard/keyboard_visibility_detector.dart'; export 'style_widget/button.dart'; export 'style_widget/color_picker.dart'; -export 'style_widget/divider.dart'; export 'style_widget/icon_button.dart'; -export 'style_widget/primary_rounded_button.dart'; -export 'style_widget/scrollbar.dart'; export 'style_widget/scrolling/styled_list.dart'; export 'style_widget/scrolling/styled_scroll_bar.dart'; export 'style_widget/text.dart'; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 6a154d4d48..3014d393dd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -1,27 +1,35 @@ import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; import 'package:flutter/material.dart'; -export 'package:appflowy_popover/appflowy_popover.dart'; - -class ShadowConstants { - ShadowConstants._(); - - static const List lightSmall = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 20, color: Color(0x1A1F2329)), - ]; - static const List lightMedium = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x121F2225)), - ]; - static const List darkSmall = [ - BoxShadow(offset: Offset(0, 2), blurRadius: 16, color: Color(0x7A000000)), - ]; - static const List darkMedium = [ - BoxShadow(offset: Offset(0, 4), blurRadius: 32, color: Color(0x7A000000)), - ]; -} - class AppFlowyPopover extends StatelessWidget { + final Widget child; + final PopoverController? controller; + final Widget Function(BuildContext context) popupBuilder; + final PopoverDirection direction; + final int triggerActions; + final BoxConstraints constraints; + final VoidCallback? onOpen; + final VoidCallback? onClose; + final Future Function()? canClose; + final PopoverMutex? mutex; + final Offset? offset; + final bool asBarrier; + final EdgeInsets margin; + final EdgeInsets windowPadding; + final Decoration? decoration; + + /// The widget that will be used to trigger the popover. + /// + /// Why do we need this? + /// Because if the parent widget of the popover is GestureDetector, + /// the conflict won't be resolve by using Listener, we want these two gestures exclusive. + final PopoverClickHandler clickHandler; + + /// If true the popover will not participate in focus traversal. + /// + final bool skipTraversal; + const AppFlowyPopover({ super.key, required this.child, @@ -38,75 +46,15 @@ class AppFlowyPopover extends StatelessWidget { this.asBarrier = false, this.margin = const EdgeInsets.all(6), this.windowPadding = const EdgeInsets.all(8.0), + this.decoration, this.clickHandler = PopoverClickHandler.listener, this.skipTraversal = false, - this.decorationColor, - this.borderRadius, - this.popoverDecoration, - this.animationDuration = const Duration(), - this.slideDistance = 5.0, - this.beginScaleFactor = 0.9, - this.endScaleFactor = 1.0, - this.beginOpacity = 0.0, - this.endOpacity = 1.0, - this.showAtCursor = false, }); - final Widget child; - final PopoverController? controller; - final Widget Function(BuildContext context) popupBuilder; - final PopoverDirection direction; - final int triggerActions; - final BoxConstraints constraints; - final VoidCallback? onOpen; - final VoidCallback? onClose; - final Future Function()? canClose; - final PopoverMutex? mutex; - final Offset? offset; - final bool asBarrier; - final EdgeInsets margin; - final EdgeInsets windowPadding; - final Color? decorationColor; - final BorderRadius? borderRadius; - final Duration animationDuration; - final double slideDistance; - final double beginScaleFactor; - final double endScaleFactor; - final double beginOpacity; - final double endOpacity; - final Decoration? popoverDecoration; - - /// The widget that will be used to trigger the popover. - /// - /// Why do we need this? - /// Because if the parent widget of the popover is GestureDetector, - /// the conflict won't be resolve by using Listener, we want these two gestures exclusive. - final PopoverClickHandler clickHandler; - - /// If true the popover will not participate in focus traversal. - /// - final bool skipTraversal; - - /// Whether the popover should be shown at the cursor position. - /// If true, the [offset] will be ignored. - /// - /// This only works when using [PopoverClickHandler.listener] as the click handler. - /// - /// Alternatively for having a normal popover, and use the cursor position only on - /// secondary click, consider showing the popover programatically with [PopoverController.showAt]. - /// - final bool showAtCursor; - @override Widget build(BuildContext context) { return Popover( controller: controller, - animationDuration: animationDuration, - slideDistance: slideDistance, - beginScaleFactor: beginScaleFactor, - endScaleFactor: endScaleFactor, - beginOpacity: beginOpacity, - endOpacity: endOpacity, onOpen: onOpen, onClose: onClose, canClose: canClose, @@ -118,15 +66,14 @@ class AppFlowyPopover extends StatelessWidget { offset: offset, clickHandler: clickHandler, skipTraversal: skipTraversal, - popupBuilder: (context) => _PopoverContainer( - constraints: constraints, - margin: margin, - decoration: popoverDecoration, - decorationColor: decorationColor, - borderRadius: borderRadius, - child: popupBuilder(context), - ), - showAtCursor: showAtCursor, + popupBuilder: (context) { + return _PopoverContainer( + constraints: constraints, + margin: margin, + decoration: decoration, + child: popupBuilder(context), + ); + }, child: child, ); } @@ -134,65 +81,33 @@ class AppFlowyPopover extends StatelessWidget { class _PopoverContainer extends StatelessWidget { const _PopoverContainer({ - this.decorationColor, - this.borderRadius, - this.decoration, required this.child, required this.margin, required this.constraints, + required this.decoration, }); final Widget child; final BoxConstraints constraints; final EdgeInsets margin; - final Color? decorationColor; - final BorderRadius? borderRadius; final Decoration? decoration; @override Widget build(BuildContext context) { + final decoration = this.decoration ?? + FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + ); + return Material( type: MaterialType.transparency, child: Container( padding: margin, - decoration: decoration ?? - context.getPopoverDecoration( - color: decorationColor, - borderRadius: borderRadius, - ), + decoration: decoration, constraints: constraints, child: child, ), ); } } - -extension PopoverDecoration on BuildContext { - /// The decoration of the popover. - /// - /// Don't customize the entire decoration of the popover, - /// use the built-in popoverDecoration instead and ask the designer before changing it. - ShapeDecoration getPopoverDecoration({ - Color? color, - BorderRadius? borderRadius, - }) { - final borderColor = Theme.of(this).brightness == Brightness.light - ? ColorSchemeConstants.lightBorderColor - : ColorSchemeConstants.darkBorderColor; - final shadows = Theme.of(this).brightness == Brightness.light - ? ShadowConstants.lightSmall - : ShadowConstants.darkSmall; - return ShapeDecoration( - color: color ?? Theme.of(this).cardColor, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1, - strokeAlign: BorderSide.strokeAlignOutside, - color: color != Colors.transparent ? borderColor : color!, - ), - borderRadius: borderRadius ?? BorderRadius.circular(10), - ), - shadows: shadows, - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart index a5a51a16e6..78d89f0297 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; const _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12); @@ -56,7 +58,9 @@ class FlowyDialog extends StatelessWidget { type: MaterialType.transparency, child: Container( height: expandHeight ? size.height : null, - width: width ?? size.width, + width: width ?? + max(min(size.width, overlayContainerMaxWidth), + overlayContainerMinWidth), constraints: constraints, child: child, ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index ef03bbc3bd..97d368eab6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -2,11 +2,10 @@ import 'dart:ui'; +import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flowy_infra_ui/src/flowy_overlay/layout.dart'; - /// Specifies how overlay are anchored to the SourceWidget enum AnchorDirection { // Corner aligned with a corner of the SourceWidget @@ -342,17 +341,18 @@ class FlowyOverlayState extends State { @override void initState() { - super.initState(); _keyboardShortcutBindings.addAll({ - LogicalKeySet(LogicalKeyboardKey.escape): (identifier) => - remove(identifier), + LogicalKeySet(LogicalKeyboardKey.escape): (identifier) { + remove(identifier); + }, }); + super.initState(); } @override Widget build(BuildContext context) { final overlays = _overlayList.map((item) { - Widget widget = item.widget; + var widget = item.widget; // requestFocus will cause the children weird focus behaviors. // item.focusNode.requestFocus(); @@ -390,11 +390,15 @@ class FlowyOverlayState extends State { return MaterialApp( theme: Theme.of(context), debugShowCheckedModeBanner: false, - home: Stack(children: children..addAll(overlays)), + home: Stack( + children: children..addAll(overlays), + ), ); } - void _handleTapOnBackground() => removeAll(); + void _handleTapOnBackground() { + removeAll(); + } Widget? _renderBackground(List overlays) { Widget? child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart index fb29bb0637..0d4bacde52 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_popover_layout.dart @@ -1,7 +1,5 @@ import 'dart:math' as math; - import 'package:flutter/material.dart'; - import 'flowy_overlay.dart'; class PopoverLayoutDelegate extends SingleChildLayoutDelegate { @@ -135,6 +133,8 @@ class PopoverLayoutDelegate extends SingleChildLayoutDelegate { case AnchorDirection.custom: childConstraints = constraints.loosen(); break; + default: + throw UnimplementedError(); } return childConstraints; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart index 84e7bb8ebd..87dd63b715 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/layout.dart @@ -1,7 +1,5 @@ import 'dart:math' as math; - import 'package:flutter/material.dart'; - import 'flowy_overlay.dart'; class OverlayLayoutDelegate extends SingleChildLayoutDelegate { @@ -135,6 +133,8 @@ class OverlayLayoutDelegate extends SingleChildLayoutDelegate { case AnchorDirection.custom: childConstraints = constraints.loosen(); break; + default: + throw UnimplementedError(); } return childConstraints; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index d1964e0c83..2b61839ac3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -128,7 +128,7 @@ class OverlayContainer extends StatelessWidget { padding: padding, decoration: FlowyDecoration.decoration( Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.shadow.withValues(alpha: 0.15), + Theme.of(context).colorScheme.shadow.withOpacity(0.15), ), constraints: constraints, child: child, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 0273807980..e99ee90f56 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -7,146 +7,12 @@ import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -class FlowyIconTextButton extends StatelessWidget { - final Widget Function(bool onHover) textBuilder; - final VoidCallback? onTap; - final VoidCallback? onSecondaryTap; - final void Function(bool)? onHover; - final EdgeInsets? margin; - final Widget? Function(bool onHover)? leftIconBuilder; - final Widget? Function(bool onHover)? rightIconBuilder; - final Color? hoverColor; - final bool isSelected; - final BorderRadius? radius; - final BoxDecoration? decoration; - final bool useIntrinsicWidth; - final bool disable; - final double disableOpacity; - final Size? leftIconSize; - final bool expandText; - final MainAxisAlignment mainAxisAlignment; - final bool showDefaultBoxDecorationOnMobile; - final double iconPadding; - final bool expand; - final Color? borderColor; - final bool resetHoverOnRebuild; - - const FlowyIconTextButton({ - super.key, - required this.textBuilder, - this.onTap, - this.onSecondaryTap, - this.onHover, - this.margin, - this.leftIconBuilder, - this.rightIconBuilder, - this.hoverColor, - this.isSelected = false, - this.radius, - this.decoration, - this.useIntrinsicWidth = false, - this.disable = false, - this.disableOpacity = 0.5, - this.leftIconSize = const Size.square(16), - this.expandText = true, - this.mainAxisAlignment = MainAxisAlignment.center, - this.showDefaultBoxDecorationOnMobile = false, - this.iconPadding = 6, - this.expand = false, - this.borderColor, - this.resetHoverOnRebuild = true, - }); - - @override - Widget build(BuildContext context) { - final color = hoverColor ?? Theme.of(context).colorScheme.secondary; - - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: disable ? null : onTap, - onSecondaryTap: disable ? null : onSecondaryTap, - child: FlowyHover( - resetHoverOnRebuild: resetHoverOnRebuild, - cursor: - disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click, - style: HoverStyle( - borderRadius: radius ?? Corners.s6Border, - hoverColor: color, - border: borderColor == null ? null : Border.all(color: borderColor!), - ), - onHover: disable ? null : onHover, - isSelected: () => isSelected, - builder: (context, onHover) => _render(context, onHover), - ), - ); - } - - Widget _render(BuildContext context, bool onHover) { - final List children = []; - - final Widget? leftIcon = leftIconBuilder?.call(onHover); - if (leftIcon != null) { - children.add( - SizedBox.fromSize( - size: leftIconSize, - child: leftIcon, - ), - ); - children.add(HSpace(iconPadding)); - } - - if (expandText) { - children.add(Expanded(child: textBuilder(onHover))); - } else { - children.add(textBuilder(onHover)); - } - - final Widget? rightIcon = rightIconBuilder?.call(onHover); - if (rightIcon != null) { - children.add(HSpace(iconPadding)); - // No need to define the size of rightIcon. Just use its intrinsic width - children.add(rightIcon); - } - - Widget child = Row( - mainAxisAlignment: mainAxisAlignment, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, - children: children, - ); - - if (useIntrinsicWidth) { - child = IntrinsicWidth(child: child); - } - - final decoration = this.decoration ?? - (showDefaultBoxDecorationOnMobile && - (Platform.isIOS || Platform.isAndroid) - ? BoxDecoration( - border: Border.all( - color: borderColor ?? - Theme.of(context).colorScheme.surfaceContainerHighest, - width: 1.0, - )) - : null); - - return Container( - decoration: decoration, - child: Padding( - padding: - margin ?? const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: child, - ), - ); - } -} - class FlowyButton extends StatelessWidget { final Widget text; final VoidCallback? onTap; final VoidCallback? onSecondaryTap; final void Function(bool)? onHover; - final EdgeInsetsGeometry? margin; + final EdgeInsets? margin; final Widget? leftIcon; final Widget? rightIcon; final Color? hoverColor; @@ -162,9 +28,6 @@ class FlowyButton extends StatelessWidget { final bool showDefaultBoxDecorationOnMobile; final double iconPadding; final bool expand; - final Color? borderColor; - final Color? backgroundColor; - final bool resetHoverOnRebuild; const FlowyButton({ super.key, @@ -188,9 +51,6 @@ class FlowyButton extends StatelessWidget { this.showDefaultBoxDecorationOnMobile = false, this.iconPadding = 6, this.expand = false, - this.borderColor, - this.backgroundColor, - this.resetHoverOnRebuild = true, }); @override @@ -201,7 +61,6 @@ class FlowyButton extends StatelessWidget { if (Platform.isIOS || Platform.isAndroid) { return InkWell( - splashFactory: Platform.isIOS ? NoSplash.splashFactory : null, onTap: disable ? null : onTap, onSecondaryTap: disable ? null : onSecondaryTap, borderRadius: radius ?? Corners.s6Border, @@ -214,14 +73,11 @@ class FlowyButton extends StatelessWidget { onTap: disable ? null : onTap, onSecondaryTap: disable ? null : onSecondaryTap, child: FlowyHover( - resetHoverOnRebuild: resetHoverOnRebuild, cursor: disable ? SystemMouseCursors.forbidden : SystemMouseCursors.click, style: HoverStyle( borderRadius: radius ?? Corners.s6Border, hoverColor: color, - border: borderColor == null ? null : Border.all(color: borderColor!), - backgroundColor: backgroundColor ?? Colors.transparent, ), onHover: disable ? null : onHover, isSelected: () => isSelected, @@ -266,41 +122,21 @@ class FlowyButton extends StatelessWidget { child = IntrinsicWidth(child: child); } - var decoration = this.decoration; - - if (decoration == null && + final decoration = this.decoration ?? (showDefaultBoxDecorationOnMobile && - (Platform.isIOS || Platform.isAndroid))) { - decoration = BoxDecoration( - color: backgroundColor ?? Theme.of(context).colorScheme.surface, - ); - } - - if (decoration == null && (Platform.isIOS || Platform.isAndroid)) { - if (showDefaultBoxDecorationOnMobile) { - decoration = BoxDecoration( - border: Border.all( - color: borderColor ?? Theme.of(context).colorScheme.outline, - width: 1.0, - ), - borderRadius: radius, - ); - } else if (backgroundColor != null) { - decoration = BoxDecoration( - color: backgroundColor, - borderRadius: radius, - ); - } - } + (Platform.isIOS || Platform.isAndroid) + ? BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + width: 1.0, + )) + : null); return Container( decoration: decoration, child: Padding( - padding: margin ?? - const EdgeInsets.symmetric( - horizontal: 6, - vertical: 4, - ), + padding: + margin ?? const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: child, ), ); @@ -328,41 +164,8 @@ class FlowyTextButton extends StatelessWidget { this.decoration, this.fontFamily, this.isDangerous = false, - this.borderColor, - this.lineHeight, }); - factory FlowyTextButton.primary({ - required BuildContext context, - required String text, - VoidCallback? onPressed, - }) => - FlowyTextButton( - text, - constraints: const BoxConstraints(minHeight: 32), - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: const Color(0xFF005483), - fontColor: Theme.of(context).colorScheme.onPrimary, - fontHoverColor: Colors.white, - onPressed: onPressed, - ); - - factory FlowyTextButton.secondary({ - required BuildContext context, - required String text, - VoidCallback? onPressed, - }) => - FlowyTextButton( - text, - constraints: const BoxConstraints(minHeight: 32), - fillColor: Colors.transparent, - hoverColor: Theme.of(context).colorScheme.primary, - fontColor: Theme.of(context).colorScheme.primary, - borderColor: Theme.of(context).colorScheme.primary, - fontHoverColor: Colors.white, - onPressed: onPressed, - ); - final String text; final FontWeight? fontWeight; final Color? fontColor; @@ -384,8 +187,6 @@ class FlowyTextButton extends StatelessWidget { final String? fontFamily; final bool isDangerous; - final Color? borderColor; - final double? lineHeight; @override Widget build(BuildContext context) { @@ -394,11 +195,7 @@ class FlowyTextButton extends StatelessWidget { children.add(heading!); children.add(const HSpace(8)); } - children.add(Text( - text, - overflow: overflow, - textAlign: TextAlign.center, - )); + children.add(Text(text, overflow: overflow, textAlign: TextAlign.center)); Widget child = Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -409,7 +206,7 @@ class FlowyTextButton extends StatelessWidget { child = ConstrainedBox( constraints: constraints, child: TextButton( - onPressed: onPressed, + onPressed: onPressed ?? () {}, focusNode: FocusNode(skipTraversal: onPressed == null), style: ButtonStyle( overlayColor: const WidgetStatePropertyAll(Colors.transparent), @@ -420,23 +217,20 @@ class FlowyTextButton extends StatelessWidget { shape: WidgetStateProperty.all( RoundedRectangleBorder( side: BorderSide( - color: borderColor ?? - (isDangerous - ? Theme.of(context).colorScheme.error - : Colors.transparent), + color: isDangerous + ? Theme.of(context).colorScheme.error + : Colors.transparent, ), borderRadius: radius ?? Corners.s6Border, ), ), textStyle: WidgetStateProperty.all( - Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: fontWeight ?? FontWeight.w500, - fontSize: fontSize, - color: fontColor ?? Theme.of(context).colorScheme.onPrimary, - decoration: decoration, - fontFamily: fontFamily, - height: lineHeight ?? 1.1, - ), + TextStyle( + fontWeight: fontWeight ?? FontWeight.w500, + fontSize: fontSize, + decoration: decoration, + fontFamily: fontFamily, + ), ), backgroundColor: WidgetStateProperty.resolveWith( (states) { @@ -533,6 +327,7 @@ class FlowyRichTextButton extends StatelessWidget { ); child = RawMaterialButton( + visualDensity: VisualDensity.compact, hoverElevation: 0, highlightElevation: 0, shape: RoundedRectangleBorder(borderRadius: radius ?? Corners.s6Border), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart index 1f8329e041..afb4a787ed 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart @@ -58,18 +58,22 @@ class FlowyColorPicker extends StatelessWidget { checkmark = const FlowySvg(FlowySvgData("grid/checkmark")); } - final colorIcon = ColorOptionIcon( - color: option.color, - iconSize: iconSize, + final colorIcon = SizedBox.square( + dimension: iconSize, + child: DecoratedBox( + decoration: BoxDecoration( + color: option.color, + shape: BoxShape.circle, + ), + ), ); return SizedBox( height: itemHeight, child: FlowyButton( - text: FlowyText(option.i18n), + text: FlowyText.medium(option.i18n), leftIcon: colorIcon, rightIcon: checkmark, - iconPadding: 10, onTap: () { onTap?.call(option, i); }, @@ -77,30 +81,3 @@ class FlowyColorPicker extends StatelessWidget { ); } } - -class ColorOptionIcon extends StatelessWidget { - const ColorOptionIcon({ - super.key, - required this.color, - this.iconSize = 16.0, - }); - - final Color color; - final double iconSize; - - @override - Widget build(BuildContext context) { - return SizedBox.square( - dimension: iconSize, - child: DecoratedBox( - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - border: color == Colors.transparent - ? Border.all(color: const Color(0xFFCFD3D9)) - : null, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart index a3f80cb41c..0902d78835 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart @@ -8,7 +8,6 @@ class FlowyDecoration { double blurRadius = 20, Offset offset = Offset.zero, double borderRadius = 6, - BoxBorder? border, }) { return BoxDecoration( color: boxColor, @@ -21,7 +20,6 @@ class FlowyDecoration { offset: offset, ), ], - border: border, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart deleted file mode 100644 index 7f4b630386..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/divider.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -class FlowyDivider extends StatelessWidget { - const FlowyDivider({ - super.key, - this.padding, - }); - - final EdgeInsets? padding; - - @override - Widget build(BuildContext context) { - return Padding( - padding: padding ?? EdgeInsets.zero, - child: Divider( - height: 1.0, - thickness: 1.0, - color: AFThemeExtension.of(context).borderColor, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart index e734c4bb68..27bff59045 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart @@ -57,21 +57,23 @@ class _FlowyHoverState extends State { return MouseRegion( cursor: widget.cursor != null ? widget.cursor! : SystemMouseCursors.click, opaque: false, - onHover: (_) => _setOnHover(true), - onEnter: (_) => _setOnHover(true), - onExit: (_) => _setOnHover(false), - child: FlowyHoverContainer( - style: widget.style ?? - HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary), - applyStyle: _onHover || (widget.isSelected?.call() ?? false), - child: widget.child ?? widget.builder!(context, _onHover), - ), + onHover: (p) { + if (_onHover) return; + _setOnHover(true); + }, + onEnter: (p) { + if (_onHover) return; + _setOnHover(true); + }, + onExit: (p) { + if (!_onHover) return; + _setOnHover(false); + }, + child: renderWidget(), ); } void _setOnHover(bool isHovering) { - if (isHovering == _onHover) return; - if (widget.buildWhenOnHover?.call() ?? true) { setState(() => _onHover = isHovering); if (widget.onHover != null) { @@ -79,10 +81,36 @@ class _FlowyHoverState extends State { } } } + + Widget renderWidget() { + bool showHover = _onHover; + if (!showHover && widget.isSelected != null) { + showHover = widget.isSelected!(); + } + + final child = widget.child ?? widget.builder!(context, _onHover); + final style = widget.style ?? + HoverStyle(hoverColor: Theme.of(context).colorScheme.secondary); + if (showHover) { + return FlowyHoverContainer( + style: style, + child: child, + ); + } else { + return Container( + decoration: BoxDecoration( + color: style.backgroundColor, + borderRadius: style.borderRadius, + ), + child: child, + ); + } + } } class HoverStyle { - final BoxBorder? border; + final Color borderColor; + final double borderWidth; final Color? hoverColor; final Color? foregroundColorOnHover; final BorderRadius borderRadius; @@ -90,7 +118,8 @@ class HoverStyle { final Color backgroundColor; const HoverStyle({ - this.border, + this.borderColor = Colors.transparent, + this.borderWidth = 0, this.borderRadius = const BorderRadius.all(Radius.circular(6)), this.contentMargin = EdgeInsets.zero, this.backgroundColor = Colors.transparent, @@ -99,28 +128,32 @@ class HoverStyle { }); const HoverStyle.transparent({ + this.borderColor = Colors.transparent, + this.borderWidth = 0, this.borderRadius = const BorderRadius.all(Radius.circular(6)), this.contentMargin = EdgeInsets.zero, this.backgroundColor = Colors.transparent, this.foregroundColorOnHover, - }) : hoverColor = Colors.transparent, - border = null; + }) : hoverColor = Colors.transparent; } class FlowyHoverContainer extends StatelessWidget { final HoverStyle style; final Widget child; - final bool applyStyle; const FlowyHoverContainer({ super.key, required this.child, required this.style, - this.applyStyle = false, }); @override Widget build(BuildContext context) { + final hoverBorder = Border.all( + color: style.borderColor, + width: style.borderWidth, + ); + final theme = Theme.of(context); final textTheme = theme.textTheme; final iconTheme = theme.iconTheme; @@ -139,14 +172,12 @@ class FlowyHoverContainer extends StatelessWidget { return Container( margin: style.contentMargin, decoration: BoxDecoration( - border: style.border, - color: applyStyle - ? style.hoverColor ?? Theme.of(context).colorScheme.secondary - : style.backgroundColor, + border: hoverBorder, + color: style.hoverColor ?? Theme.of(context).colorScheme.secondary, borderRadius: style.borderRadius, ), child: Theme( - data: applyStyle ? hoverTheme : Theme.of(context), + data: hoverTheme, child: child, ), ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index cb3605fe7e..b4a2553814 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -1,11 +1,10 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_svg/flowy_svg.dart'; +import 'package:flutter/material.dart'; class FlowyIconButton extends StatelessWidget { final double width; @@ -63,12 +62,11 @@ class FlowyIconButton extends StatelessWidget { child = FlowyHover( isSelected: isSelected != null ? () => isSelected! : null, style: HoverStyle( - hoverColor: hoverColor, - foregroundColorOnHover: - iconColorOnHover ?? Theme.of(context).iconTheme.color, - borderRadius: radius ?? Corners.s6Border - //Do not set background here. Use [fillColor] instead. - ), + hoverColor: hoverColor, + foregroundColorOnHover: + iconColorOnHover ?? Theme.of(context).iconTheme.color, + //Do not set background here. Use [fillColor] instead. + ), resetHoverOnRebuild: false, child: child, ); @@ -84,8 +82,10 @@ class FlowyIconButton extends StatelessWidget { preferBelow: preferBelow, message: tooltipMessage, richMessage: richTooltipText, + showDuration: Duration.zero, child: RawMaterialButton( clipBehavior: Clip.antiAlias, + visualDensity: VisualDensity.compact, hoverElevation: 0, highlightElevation: 0, shape: diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart deleted file mode 100644 index 61f92fd073..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/primary_rounded_button.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; - -class PrimaryRoundedButton extends StatelessWidget { - const PrimaryRoundedButton({ - super.key, - required this.text, - this.fontSize, - this.fontWeight, - this.color, - this.radius, - this.margin, - this.onTap, - this.hoverColor, - this.backgroundColor, - this.useIntrinsicWidth = true, - this.lineHeight, - this.figmaLineHeight, - this.leftIcon, - this.textColor, - }); - - final String text; - final double? fontSize; - final FontWeight? fontWeight; - final Color? color; - final double? radius; - final EdgeInsets? margin; - final VoidCallback? onTap; - final Color? hoverColor; - final Color? backgroundColor; - final bool useIntrinsicWidth; - final double? lineHeight; - final double? figmaLineHeight; - final Widget? leftIcon; - final Color? textColor; - - @override - Widget build(BuildContext context) { - return FlowyButton( - useIntrinsicWidth: useIntrinsicWidth, - leftIcon: leftIcon, - text: FlowyText( - text, - fontSize: fontSize ?? 14.0, - fontWeight: fontWeight ?? FontWeight.w500, - lineHeight: lineHeight ?? 1.0, - figmaLineHeight: figmaLineHeight, - color: textColor ?? Theme.of(context).colorScheme.onPrimary, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - margin: margin ?? const EdgeInsets.symmetric(horizontal: 14.0), - backgroundColor: backgroundColor ?? Theme.of(context).colorScheme.primary, - hoverColor: hoverColor ?? - Theme.of(context).colorScheme.primary.withValues(alpha: 0.9), - radius: BorderRadius.circular(radius ?? 10.0), - onTap: onTap, - ); - } -} - -class OutlinedRoundedButton extends StatelessWidget { - const OutlinedRoundedButton({ - super.key, - required this.text, - this.onTap, - this.margin, - this.radius, - }); - - final String text; - final VoidCallback? onTap; - final EdgeInsets? margin; - final double? radius; - - @override - Widget build(BuildContext context) { - return DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - side: Theme.of(context).brightness == Brightness.light - ? const BorderSide(color: Color(0x1E14171B)) - : const BorderSide(color: Colors.white10), - borderRadius: BorderRadius.circular(radius ?? 8), - ), - ), - child: FlowyButton( - useIntrinsicWidth: true, - margin: margin ?? - const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 9.0, - ), - radius: BorderRadius.circular(radius ?? 8), - text: FlowyText.regular( - text, - lineHeight: 1.0, - textAlign: TextAlign.center, - ), - onTap: onTap, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrollbar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrollbar.dart deleted file mode 100644 index 01111293ec..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrollbar.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; - -class FlowyScrollbar extends StatefulWidget { - const FlowyScrollbar({ - super.key, - this.controller, - required this.child, - }); - - final ScrollController? controller; - final Widget child; - - @override - State createState() => _FlowyScrollbarState(); -} - -class _FlowyScrollbarState extends State { - final ValueNotifier isHovered = ValueNotifier(false); - - @override - void dispose() { - isHovered.dispose(); - - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => isHovered.value = true, - onExit: (_) => isHovered.value = false, - child: ValueListenableBuilder( - valueListenable: isHovered, - builder: (context, isHovered, child) { - return Scrollbar( - thumbVisibility: isHovered, - // the radius should be fixed to 12 - radius: const Radius.circular(12), - controller: widget.controller, - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - scrollbars: false, - ), - child: child!, - ), - ); - }, - child: widget.child, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart index 92381cff44..a3f262c070 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart @@ -36,7 +36,13 @@ class StyledListView extends StatefulWidget { /// State is public so this can easily be controlled externally class StyledListViewState extends State { - final scrollController = ScrollController(); + late ScrollController scrollController; + + @override + void initState() { + scrollController = ScrollController(); + super.initState(); + } @override void dispose() { @@ -44,6 +50,15 @@ class StyledListViewState extends State { super.dispose(); } + @override + void didUpdateWidget(StyledListView oldWidget) { + if (oldWidget.itemCount != widget.itemCount || + oldWidget.itemExtent != widget.itemExtent) { + setState(() {}); + } + super.didUpdateWidget(oldWidget); + } + @override Widget build(BuildContext context) { final contentSize = (widget.itemCount ?? 0.0) * (widget.itemExtent ?? 00.0); @@ -60,7 +75,7 @@ class StyledListViewState extends State { controller: scrollController, itemExtent: widget.itemExtent, itemCount: widget.itemCount, - itemBuilder: widget.itemBuilder, + itemBuilder: (c, i) => widget.itemBuilder(c, i), ), ); return listContent; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart index ece1801098..da22674152 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/material.dart'; + import 'package:async/async.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/widget/mouse_hover_builder.dart'; -import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; class StyledScrollbar extends StatefulWidget { @@ -119,6 +120,11 @@ class ScrollbarState extends State { ? false : contentExtent > _viewExtent && contentExtent > 0; + // Handle color + var handleColor = widget.handleColor ?? + (Theme.of(context).brightness == Brightness.dark + ? AFThemeExtension.of(context).lightGreyHover + : AFThemeExtension.of(context).greyHover); // Track color var trackColor = widget.trackColor ?? (Theme.of(context).brightness == Brightness.dark @@ -155,24 +161,18 @@ class ScrollbarState extends State { onHorizontalDragUpdate: _handleHorizontalDrag, // HANDLE SHAPE child: MouseHoverBuilder( - builder: (_, isHovered) { - final handleColor = - Theme.of(context).scrollbarTheme.thumbColor?.resolve( - isHovered ? {WidgetState.dragged} : {}, - ); - return Container( - width: widget.axis == Axis.vertical - ? widget.size - : handleExtent, - height: widget.axis == Axis.horizontal - ? widget.size - : handleExtent, - decoration: BoxDecoration( - color: handleColor, - borderRadius: Corners.s3Border, - ), - ); - }, + builder: (_, isHovered) => Container( + width: widget.axis == Axis.vertical + ? widget.size + : handleExtent, + height: widget.axis == Axis.horizontal + ? widget.size + : handleExtent, + decoration: BoxDecoration( + color: handleColor.withOpacity(isHovered ? 1 : .85), + borderRadius: Corners.s3Border, + ), + ), ), ), ) diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index c889496f17..8752a5985f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -9,8 +9,7 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 8000), content: FlowyText( title, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 360578a4a6..a4768969af 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; + import 'package:google_fonts/google_fonts.dart'; class FlowyText extends StatelessWidget { @@ -13,22 +13,14 @@ class FlowyText extends StatelessWidget { final int? maxLines; final Color? color; final TextDecoration? decoration; - final Color? decorationColor; - final double? decorationThickness; + final bool selectable; final String? fontFamily; final List? fallbackFontFamily; + final double? lineHeight; final bool withTooltip; final StrutStyle? strutStyle; final bool isEmoji; - /// this is used to control the line height in Flutter. - final double? lineHeight; - - /// this is used to control the line height from Figma. - final double? figmaLineHeight; - - final bool optimizeEmojiAlign; - const FlowyText( this.text, { super.key, @@ -39,17 +31,13 @@ class FlowyText extends StatelessWidget { this.color, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.fontFamily, this.fallbackFontFamily, - // // https://api.flutter.dev/flutter/painting/TextStyle/height.html this.lineHeight, - this.figmaLineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.optimizeEmojiAlign = false, - this.decorationThickness, }); FlowyText.small( @@ -60,16 +48,13 @@ class FlowyText extends StatelessWidget { this.textAlign, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.figmaLineHeight, - this.optimizeEmojiAlign = false, - this.decorationThickness, }) : fontWeight = FontWeight.w400, fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12; @@ -82,16 +67,13 @@ class FlowyText extends StatelessWidget { this.textAlign, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.figmaLineHeight, - this.optimizeEmojiAlign = false, - this.decorationThickness, }) : fontWeight = FontWeight.w400; const FlowyText.medium( @@ -103,16 +85,13 @@ class FlowyText extends StatelessWidget { this.textAlign, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.figmaLineHeight, - this.optimizeEmojiAlign = false, - this.decorationThickness, }) : fontWeight = FontWeight.w500; const FlowyText.semibold( @@ -124,16 +103,13 @@ class FlowyText extends StatelessWidget { this.textAlign, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.fontFamily, this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, this.isEmoji = false, this.strutStyle, - this.figmaLineHeight, - this.optimizeEmojiAlign = false, - this.decorationThickness, }) : fontWeight = FontWeight.w600; // Some emojis are not supported on Linux and Android, fallback to noto color emoji @@ -146,15 +122,12 @@ class FlowyText extends StatelessWidget { this.textAlign = TextAlign.center, this.maxLines = 1, this.decoration, - this.decorationColor, + this.selectable = false, this.lineHeight, this.withTooltip = false, this.strutStyle = const StrutStyle(forceStrutHeight: true), this.isEmoji = true, this.fontFamily, - this.figmaLineHeight, - this.optimizeEmojiAlign = false, - this.decorationThickness, }) : fontWeight = FontWeight.w400, fallbackFontFamily = null; @@ -173,20 +146,8 @@ class FlowyText extends StatelessWidget { } } - double? lineHeight; - // use figma line height as first priority - if (figmaLineHeight != null) { - lineHeight = figmaLineHeight! / fontSize; - } else if (this.lineHeight != null) { - lineHeight = this.lineHeight!; - } - if (isEmoji && (_useNotoColorEmoji || Platform.isWindows)) { - const scaleFactor = 0.9; - fontSize *= scaleFactor; - if (lineHeight != null) { - lineHeight /= scaleFactor; - } + fontSize = fontSize * 0.8; } final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( @@ -194,34 +155,40 @@ class FlowyText extends StatelessWidget { fontWeight: fontWeight, color: color, decoration: decoration, - decorationColor: decorationColor, - decorationThickness: decorationThickness, fontFamily: fontFamily, fontFamilyFallback: fallbackFontFamily, height: lineHeight, - leadingDistribution: isEmoji && optimizeEmojiAlign - ? TextLeadingDistribution.even - : null, ); - child = Text( - text, - maxLines: maxLines, - textAlign: textAlign, - overflow: overflow ?? TextOverflow.clip, - style: textStyle, - strutStyle: !isEmoji || (isEmoji && optimizeEmojiAlign) - ? StrutStyle.fromTextStyle( - textStyle, - forceStrutHeight: true, - leadingDistribution: TextLeadingDistribution.even, - height: lineHeight, - ) - : null, - ); + if (selectable) { + child = IntrinsicHeight( + child: SelectableText( + text, + maxLines: maxLines, + textAlign: textAlign, + style: textStyle, + ), + ); + } else { + child = Text( + text, + maxLines: maxLines, + textAlign: textAlign, + overflow: overflow ?? TextOverflow.clip, + style: textStyle, + strutStyle: (Platform.isMacOS || Platform.isLinux) & !isEmoji + ? StrutStyle.fromTextStyle( + textStyle, + forceStrutHeight: true, + leadingDistribution: TextLeadingDistribution.even, + height: lineHeight ?? 1.1, + ) + : null, + ); + } if (withTooltip) { - child = FlowyTooltip( + child = Tooltip( message: text, child: child, ); @@ -238,5 +205,5 @@ class FlowyText extends StatelessWidget { return null; } - bool get _useNotoColorEmoji => Platform.isLinux; + bool get _useNotoColorEmoji => Platform.isLinux || Platform.isAndroid; } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index c4f72f2261..d26f353c72 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flowy_infra/size.dart'; + class FlowyTextField extends StatefulWidget { final String? hintText; final String? text; @@ -36,11 +37,6 @@ class FlowyTextField extends StatefulWidget { final List? inputFormatters; final bool obscureText; final bool isDense; - final bool readOnly; - final Color? enableBorderColor; - final BorderRadius? borderRadius; - final void Function()? onTap; - final Function(PointerDownEvent)? onTapOutside; const FlowyTextField({ super.key, @@ -75,11 +71,6 @@ class FlowyTextField extends StatefulWidget { this.inputFormatters, this.obscureText = false, this.isDense = true, - this.readOnly = false, - this.enableBorderColor, - this.borderRadius, - this.onTap, - this.onTapOutside, }); @override @@ -146,6 +137,7 @@ class FlowyTextFieldState extends State { void _onSubmitted(String text) { widget.onSubmitted?.call(text); if (widget.autoClearWhenDone) { + // using `controller.clear()` instead of `controller.text = ''` which will crash on Windows. controller.clear(); } } @@ -153,7 +145,6 @@ class FlowyTextFieldState extends State { @override Widget build(BuildContext context) { return TextField( - readOnly: widget.readOnly, controller: controller, focusNode: focusNode, onChanged: (text) { @@ -163,10 +154,8 @@ class FlowyTextFieldState extends State { _onChanged(text); } }, - onSubmitted: _onSubmitted, + onSubmitted: (text) => _onSubmitted(text), onEditingComplete: widget.onEditingComplete, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, minLines: 1, maxLines: widget.maxLines, maxLength: widget.maxLength, @@ -188,10 +177,9 @@ class FlowyTextFieldState extends State { (widget.maxLines == null || widget.maxLines! > 1) ? 12 : 0, ), enabledBorder: OutlineInputBorder( - borderRadius: widget.borderRadius ?? Corners.s8Border, + borderRadius: Corners.s8Border, borderSide: BorderSide( - color: widget.enableBorderColor ?? - Theme.of(context).colorScheme.outline, + color: Theme.of(context).colorScheme.outline, ), ), isDense: false, @@ -210,25 +198,22 @@ class FlowyTextFieldState extends State { suffixText: widget.showCounter ? _suffixText() : "", counterText: "", focusedBorder: OutlineInputBorder( - borderRadius: widget.borderRadius ?? Corners.s8Border, + borderRadius: Corners.s8Border, borderSide: BorderSide( - color: widget.readOnly - ? widget.enableBorderColor ?? - Theme.of(context).colorScheme.outline - : Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.primary, ), ), errorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, ), - borderRadius: widget.borderRadius ?? Corners.s8Border, + borderRadius: Corners.s8Border, ), focusedErrorBorder: OutlineInputBorder( borderSide: BorderSide( color: Theme.of(context).colorScheme.error, ), - borderRadius: widget.borderRadius ?? Corners.s8Border, + borderRadius: Corners.s8Border, ), prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart index 95fd82363e..8e3bcc9bf5 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart @@ -6,7 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class FlowyFormTextInput extends StatelessWidget { - static EdgeInsets kDefaultTextInputPadding = const EdgeInsets.only(bottom: 2); + static EdgeInsets kDefaultTextInputPadding = + EdgeInsets.only(bottom: Insets.sm, top: 4); final String? label; final bool? autoFocus; @@ -67,7 +68,7 @@ class FlowyFormTextInput extends StatelessWidget { hintStyle: Theme.of(context) .textTheme .bodyMedium! - .copyWith(color: Theme.of(context).hintColor.withValues(alpha: 0.7)), + .copyWith(color: Theme.of(context).hintColor.withOpacity(0.7)), isDense: true, inputBorder: const ThinUnderlineBorder( borderSide: BorderSide(width: 5, color: Colors.red), @@ -161,12 +162,10 @@ class StyledSearchTextInputState extends State { @override void initState() { - super.initState(); _controller = widget.controller ?? TextEditingController(text: widget.initialValue); _focusNode = FocusNode( - debugLabel: widget.label, - canRequestFocus: true, + debugLabel: widget.label ?? '', onKeyEvent: (node, event) { if (event.logicalKey == LogicalKeyboardKey.escape) { widget.onEditingCancel?.call(); @@ -174,23 +173,23 @@ class StyledSearchTextInputState extends State { } return KeyEventResult.ignored; }, + canRequestFocus: true, ); // Listen for focus out events - _focusNode.addListener(_onFocusChanged); + _focusNode + .addListener(() => widget.onFocusChanged?.call(_focusNode.hasFocus)); widget.onFocusCreated?.call(_focusNode); if (widget.autoFocus ?? false) { scheduleMicrotask(() => _focusNode.requestFocus()); } + super.initState(); } - void _onFocusChanged() => widget.onFocusChanged?.call(_focusNode.hasFocus); - @override void dispose() { if (widget.controller == null) { _controller.dispose(); } - _focusNode.removeListener(_onFocusChanged); _focusNode.dispose(); super.dispose(); } @@ -293,10 +292,8 @@ class ThinUnderlineBorder extends InputBorder { bool get isOutline => false; @override - UnderlineInputBorder copyWith({ - BorderSide? borderSide, - BorderRadius? borderRadius, - }) { + UnderlineInputBorder copyWith( + {BorderSide? borderSide, BorderRadius? borderRadius}) { return UnderlineInputBorder( borderSide: borderSide ?? this.borderSide, borderRadius: borderRadius ?? this.borderRadius, @@ -304,12 +301,14 @@ class ThinUnderlineBorder extends InputBorder { } @override - EdgeInsetsGeometry get dimensions => - EdgeInsets.only(bottom: borderSide.width); + EdgeInsetsGeometry get dimensions { + return EdgeInsets.only(bottom: borderSide.width); + } @override - UnderlineInputBorder scale(double t) => - UnderlineInputBorder(borderSide: borderSide.scale(t)); + UnderlineInputBorder scale(double t) { + return UnderlineInputBorder(borderSide: borderSide.scale(t)); + } @override Path getInnerPath(Rect rect, {TextDirection? textDirection}) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart deleted file mode 100644 index 96a22a6f85..0000000000 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/toolbar_button.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; - -class FlowyToolbarButton extends StatelessWidget { - final Widget child; - final VoidCallback? onPressed; - final EdgeInsets padding; - final String? tooltip; - - const FlowyToolbarButton({ - super.key, - this.onPressed, - this.tooltip, - this.padding = const EdgeInsets.symmetric(vertical: 8, horizontal: 6), - required this.child, - }); - - @override - Widget build(BuildContext context) { - final tooltipMessage = tooltip ?? ''; - - return FlowyTooltip( - message: tooltipMessage, - padding: EdgeInsets.zero, - child: RawMaterialButton( - clipBehavior: Clip.antiAlias, - constraints: const BoxConstraints(minWidth: 36, minHeight: 32), - hoverElevation: 0, - highlightElevation: 0, - padding: EdgeInsets.zero, - shape: const RoundedRectangleBorder(borderRadius: Corners.s6Border), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - elevation: 0, - onPressed: onPressed, - child: child, - ), - ); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart index c81c81f356..ee51d400c9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart @@ -53,19 +53,16 @@ class BaseStyledBtnState extends State { void initState() { super.initState(); _focusNode = FocusNode(debugLabel: '', canRequestFocus: true); - _focusNode.addListener(_onFocusChanged); - } - - void _onFocusChanged() { - if (_focusNode.hasFocus != _isFocused) { - setState(() => _isFocused = _focusNode.hasFocus); - widget.onFocusChanged?.call(_isFocused); - } + _focusNode.addListener(() { + if (_focusNode.hasFocus != _isFocused) { + setState(() => _isFocused = _focusNode.hasFocus); + widget.onFocusChanged?.call(_isFocused); + } + }); } @override void dispose() { - _focusNode.removeListener(_onFocusChanged); _focusNode.dispose(); super.dispose(); } @@ -121,7 +118,7 @@ class BaseStyledBtnState extends State { fillColor: Colors.transparent, hoverColor: widget.hoverColor ?? Colors.transparent, highlightColor: widget.highlightColor ?? Colors.transparent, - focusColor: widget.focusColor ?? Colors.grey.withValues(alpha: 0.35), + focusColor: widget.focusColor ?? Colors.grey.withOpacity(0.35), constraints: BoxConstraints( minHeight: widget.minHeight ?? 0, minWidth: widget.minWidth ?? 0), onPressed: widget.onPressed, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart index e2fbd49db3..6207419009 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart @@ -44,7 +44,7 @@ class PrimaryButton extends StatelessWidget { return BaseStyledButton( minWidth: mode.size.width, minHeight: mode.size.height, - contentPadding: const EdgeInsets.symmetric(horizontal: 6), + contentPadding: EdgeInsets.zero, bgColor: backgroundColor ?? Theme.of(context).colorScheme.primary, hoverColor: Theme.of(context).colorScheme.primaryContainer, borderRadius: mode.borderRadius, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index 7048ed32ec..e29f778d84 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -96,7 +96,7 @@ class Dialogs { {required Widget child}) async { return await Navigator.of(context).push( StyledDialogRoute( - barrier: DialogBarrier(color: Colors.black.withValues(alpha: 0.4)), + barrier: DialogBarrier(color: Colors.black.withOpacity(0.4)), pageBuilder: (BuildContext buildContext, Animation animation, Animation secondaryAnimation) { return SafeArea(child: child); diff --git a/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart similarity index 88% rename from frontend/appflowy_flutter/lib/shared/error_page/error_page.dart rename to frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart index 9661fd822a..fd6b734715 100644 --- a/frontend/appflowy_flutter/lib/shared/error_page/error_page.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -1,18 +1,15 @@ import 'dart:io'; -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flowy_svg/flowy_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; class FlowyErrorPage extends StatelessWidget { factory FlowyErrorPage.error( @@ -90,9 +87,7 @@ class FlowyErrorPage extends StatelessWidget { Listener( behavior: HitTestBehavior.translucent, onPointerDown: (_) async { - await getIt().setData( - ClipboardServiceData(plainText: message), - ); + await Clipboard.setData(ClipboardData(text: message)); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -194,8 +189,8 @@ class StackTracePreview extends StatelessWidget { "Copy", ), useIntrinsicWidth: true, - onTap: () => getIt().setData( - ClipboardServiceData(plainText: stackTrace), + onTap: () => Clipboard.setData( + ClipboardData(text: stackTrace), ), ), ), @@ -258,14 +253,18 @@ class GitHubRedirectButton extends StatelessWidget { Widget build(BuildContext context) { return FlowyButton( leftIconSize: const Size.square(_height), - text: FlowyText(LocaleKeys.appName.tr()), + text: const FlowyText( + "AppFlowy", + ), useIntrinsicWidth: true, leftIcon: const Padding( padding: EdgeInsets.all(4.0), child: FlowySvg(FlowySvgData('login/github-mark')), ), onTap: () async { - await afLaunchUri(_gitHubNewBugUri); + if (await canLaunchUrl(_gitHubNewBugUri)) { + await launchUrl(_gitHubNewBugUri); + } }, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart index 5b0b791c6c..1eb8bd0046 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/flowy_tooltip.dart @@ -8,19 +8,17 @@ class FlowyTooltip extends StatelessWidget { this.message, this.richMessage, this.preferBelow, + this.showDuration, this.margin, - this.verticalOffset, - this.padding, this.child, }); final String? message; final InlineSpan? richMessage; final bool? preferBelow; + final Duration? showDuration; final EdgeInsetsGeometry? margin; final Widget? child; - final double? verticalOffset; - final EdgeInsets? padding; @override Widget build(BuildContext context) { @@ -28,120 +26,21 @@ class FlowyTooltip extends StatelessWidget { return child ?? const SizedBox.shrink(); } + final isLightMode = Theme.of(context).brightness == Brightness.light; return Tooltip( margin: margin, - verticalOffset: verticalOffset ?? 16.0, - padding: padding ?? - const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), + verticalOffset: 16.0, + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), decoration: BoxDecoration( - color: context.tooltipBackgroundColor(), - borderRadius: BorderRadius.circular(10.0), + color: isLightMode ? const Color(0xE5171717) : const Color(0xE5E5E5E5), + borderRadius: BorderRadius.circular(8.0), ), waitDuration: _tooltipWaitDuration, message: message, - textStyle: message != null ? context.tooltipTextStyle() : null, richMessage: richMessage, + showDuration: showDuration, preferBelow: preferBelow, child: child, ); } } - -class ManualTooltip extends StatefulWidget { - const ManualTooltip({ - super.key, - this.message, - this.richMessage, - this.preferBelow, - this.margin, - this.verticalOffset, - this.padding, - this.showAutomaticlly = false, - this.child, - }); - - final String? message; - final InlineSpan? richMessage; - final bool? preferBelow; - final EdgeInsetsGeometry? margin; - final Widget? child; - final double? verticalOffset; - final EdgeInsets? padding; - final bool showAutomaticlly; - - @override - State createState() => _ManualTooltipState(); -} - -class _ManualTooltipState extends State { - final key = GlobalKey(); - - @override - void initState() { - if (widget.showAutomaticlly) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) key.currentState?.ensureTooltipVisible(); - }); - } - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Tooltip( - key: key, - margin: widget.margin, - verticalOffset: widget.verticalOffset ?? 16.0, - triggerMode: widget.showAutomaticlly ? TooltipTriggerMode.manual : null, - padding: widget.padding ?? - const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - decoration: BoxDecoration( - color: context.tooltipBackgroundColor(), - borderRadius: BorderRadius.circular(10.0), - ), - waitDuration: _tooltipWaitDuration, - message: widget.message, - textStyle: widget.message != null ? context.tooltipTextStyle() : null, - richMessage: widget.richMessage, - preferBelow: widget.preferBelow, - child: widget.child, - ); - } -} - -extension FlowyToolTipExtension on BuildContext { - double tooltipFontSize() => 14.0; - - double tooltipHeight({double? fontSize}) => - 20.0 / (fontSize ?? tooltipFontSize()); - - Color tooltipFontColor() => Theme.of(this).brightness == Brightness.light - ? Colors.white - : Colors.black; - - TextStyle? tooltipTextStyle({Color? fontColor, double? fontSize}) { - return Theme.of(this).textTheme.bodyMedium?.copyWith( - color: fontColor ?? tooltipFontColor(), - fontSize: fontSize ?? tooltipFontSize(), - fontWeight: FontWeight.w400, - height: tooltipHeight(fontSize: fontSize), - leadingDistribution: TextLeadingDistribution.even, - ); - } - - TextStyle? tooltipHintTextStyle({double? fontSize}) => tooltipTextStyle( - fontColor: tooltipFontColor().withValues(alpha: 0.7), - fontSize: fontSize, - ); - - Color tooltipBackgroundColor() => - Theme.of(this).brightness == Brightness.light - ? const Color(0xFF1D2129) - : const Color(0xE5E5E5E5); -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index 0fa181a1dd..a73d96f454 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -13,7 +13,6 @@ class RoundedTextButton extends StatelessWidget { final Color? hoverColor; final Color? textColor; final double? fontSize; - final FontWeight? fontWeight; final EdgeInsets padding; const RoundedTextButton({ @@ -28,7 +27,6 @@ class RoundedTextButton extends StatelessWidget { this.hoverColor, this.textColor, this.fontSize, - this.fontWeight, this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), }); @@ -44,7 +42,6 @@ class RoundedTextButton extends StatelessWidget { child: SizedBox.expand( child: FlowyTextButton( title ?? '', - fontWeight: fontWeight, onPressed: onPressed, fontSize: fontSize, mainAxisAlignment: MainAxisAlignment.center, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart index de5e3061fd..40978aa461 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:flowy_infra/size.dart'; -import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flowy_infra/time/duration.dart'; +import 'package:flutter/services.dart'; class RoundedInputField extends StatefulWidget { final String? hintText; @@ -61,26 +60,33 @@ class RoundedInputField extends StatefulWidget { class _RoundedInputFieldState extends State { String inputText = ""; - bool obscureText = false; + bool obscuteText = false; @override void initState() { + obscuteText = widget.obscureText; + if (widget.controller != null) { + inputText = widget.controller!.text; + } else { + inputText = widget.initialValue ?? ""; + } + super.initState(); - obscureText = widget.obscureText; - inputText = widget.controller != null - ? widget.controller!.text - : widget.initialValue ?? ""; } - String? _suffixText() => widget.maxLength != null - ? ' ${widget.controller!.text.length}/${widget.maxLength}' - : null; + String? _suffixText() { + if (widget.maxLength != null) { + return ' ${widget.controller!.text.length}/${widget.maxLength}'; + } else { + return null; + } + } @override Widget build(BuildContext context) { - Color borderColor = + var borderColor = widget.normalBorderColor ?? Theme.of(context).colorScheme.outline; - Color focusBorderColor = + var focusBorderColor = widget.focusBorderColor ?? Theme.of(context).colorScheme.primary; if (widget.errorText.isNotEmpty) { @@ -116,7 +122,7 @@ class _RoundedInputFieldState extends State { }, cursorColor: widget.cursorColor ?? Theme.of(context).colorScheme.primary, - obscureText: obscureText, + obscureText: obscuteText, style: widget.style ?? Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( contentPadding: widget.contentPadding, @@ -128,11 +134,17 @@ class _RoundedInputFieldState extends State { suffixText: _suffixText(), counterText: "", enabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: borderColor, width: 1.0), + borderSide: BorderSide( + color: borderColor, + width: 1.0, + ), borderRadius: Corners.s10Border, ), focusedBorder: OutlineInputBorder( - borderSide: BorderSide(color: focusBorderColor, width: 1.0), + borderSide: BorderSide( + color: focusBorderColor, + width: 1.0, + ), borderRadius: Corners.s10Border, ), suffixIcon: obscureIcon(), @@ -174,11 +186,19 @@ class _RoundedInputFieldState extends State { } assert(widget.obscureIcon != null && widget.obscureHideIcon != null); - final icon = obscureText ? widget.obscureIcon! : widget.obscureHideIcon!; + Widget? icon; + if (obscuteText) { + icon = widget.obscureIcon!; + } else { + icon = widget.obscureHideIcon!; + } return RoundedImageButton( size: iconWidth, - press: () => setState(() => obscureText = !obscureText), + press: () { + obscuteText = !obscuteText; + setState(() {}); + }, child: icon, ); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index b5b5c22bc7..5eb1ba066e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -13,8 +13,9 @@ dependencies: sdk: flutter # Thirdparty packages - + provider: ^6.0.5 styled_widget: ^0.4.1 + equatable: ^2.0.5 animations: ^2.0.7 loading_indicator: ^3.1.0 async: @@ -24,6 +25,8 @@ dependencies: # Federated Platform Interface flowy_infra_ui_platform_interface: path: flowy_infra_ui_platform_interface + flowy_infra_ui_web: + path: flowy_infra_ui_web appflowy_popover: path: ../appflowy_popover flowy_infra: @@ -31,11 +34,7 @@ dependencies: flowy_svg: path: ../flowy_svg - analyzer: 6.11.0 - dev_dependencies: - build_runner: ^2.4.9 - provider: ^6.0.5 flutter_test: sdk: flutter flutter_lints: ^3.0.1 diff --git a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml b/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml index 543c78a7d4..315807278e 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml @@ -1 +1,3 @@ +include: package:very_good_analysis/analysis_options.yaml + linter: diff --git a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart index e87bb3fa01..cbf114156d 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/bin/flowy_svg.dart @@ -242,13 +242,7 @@ String varNameFor(File file, Options options) { return simplified; } -const sizeMap = { - r'$16x': 's', - r'$20x': 'm', - r'$24x': 'm', - r'$32x': 'lg', - r'$40x': 'xl' -}; +const sizeMap = {r'$16x': 's', r'$24x': 'm', r'$32x': 'lg', r'$40x': 'xl'}; /// cleans the path segment before rejoining the path into a variable name String clean(String segment) { diff --git a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart index 1f861156eb..aa70ee9d1f 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart +++ b/frontend/appflowy_flutter/packages/flowy_svg/lib/src/flowy_svg.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -export 'package:flutter_svg/flutter_svg.dart'; - /// The class for FlowySvgData that the code generator will implement class FlowySvgData { /// The svg data @@ -25,30 +23,9 @@ class FlowySvg extends StatelessWidget { this.size, this.color, this.blendMode = BlendMode.srcIn, - this.opacity, - this.svgString, + this.opacity = 1.0, }); - /// Construct a FlowySvg Widget from a string - factory FlowySvg.string( - String svgString, { - Key? key, - Size? size, - Color? color, - BlendMode? blendMode = BlendMode.srcIn, - double? opacity, - }) { - return FlowySvg( - const FlowySvgData(''), - key: key, - size: size, - color: color, - blendMode: blendMode, - opacity: opacity, - svgString: svgString, - ); - } - /// The data for the flowy svg. Will be generated by the generator in this /// package within bin/flowy_svg.dart final FlowySvgData svg; @@ -56,9 +33,6 @@ class FlowySvg extends StatelessWidget { /// The size of the svg final Size? size; - /// The svg string - final String? svgString; - /// The color of the svg. /// /// This property will not be applied to the underlying svg widget if the @@ -75,52 +49,30 @@ class FlowySvg extends StatelessWidget { /// The opacity of the svg /// - /// if null then use the opacity of the iconColor - final double? opacity; + /// The default value is 1.0 + final double opacity; @override Widget build(BuildContext context) { - Color? iconColor = color ?? Theme.of(context).iconTheme.color; - if (opacity != null) { - iconColor = iconColor?.withValues(alpha: opacity!); - } - + final iconColor = + (color ?? Theme.of(context).iconTheme.color)?.withOpacity(opacity); final textScaleFactor = MediaQuery.textScalerOf(context).scale(1); - - final Widget svg; - - if (svgString != null) { - svg = SvgPicture.string( - svgString!, - width: size?.width, - height: size?.height, - colorFilter: iconColor != null && blendMode != null - ? ColorFilter.mode( - iconColor, - blendMode!, - ) - : null, - ); - } else { - svg = SvgPicture.asset( - _normalized(), - width: size?.width, - height: size?.height, - colorFilter: iconColor != null && blendMode != null - ? ColorFilter.mode( - iconColor, - blendMode!, - ) - : null, - ); - } - return Transform.scale( scale: textScaleFactor, child: SizedBox( width: size?.width, height: size?.height, - child: svg, + child: SvgPicture.asset( + _normalized(), + width: size?.width, + height: size?.height, + colorFilter: iconColor != null && blendMode != null + ? ColorFilter.mode( + iconColor, + blendMode!, + ) + : null, + ), ), ); } diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 1a393e1180..e066d6e0dd 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -5,23 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "76.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.3" + version: "67.0.0" analyzer: - dependency: "direct main" + dependency: "direct dev" description: name: analyzer - sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.11.0" + version: "6.4.1" animations: dependency: transitive description: @@ -30,54 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.11" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" - url: "https://pub.dev" - source: hosted - version: "2.0.3" - any_date: - dependency: "direct main" - description: - name: any_date - sha256: e9ed245ba44ccebf3c2d6daa3592213f409821128593d448b219a1f8e9bd17a1 - url: "https://pub.dev" - source: hosted - version: "1.1.1" app_links: dependency: "direct main" description: name: app_links - sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb" url: "https://pub.dev" source: hosted - version: "6.3.3" - app_links_linux: - dependency: transitive - description: - name: app_links_linux - sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" - source: hosted - version: "1.0.3" - app_links_platform_interface: - dependency: transitive - description: - name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" - source: hosted - version: "2.0.2" - app_links_web: - dependency: transitive - description: - name: app_links_web - sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" - source: hosted - version: "1.0.4" + version: "3.5.0" appflowy_backend: dependency: "direct main" description: @@ -89,8 +44,8 @@ packages: dependency: "direct main" description: path: "." - ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 - resolved-ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 + ref: "8a6434ae3d02624b614a010af80f775db11bf22e" + resolved-ref: "8a6434ae3d02624b614a010af80f775db11bf22e" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git version: "0.1.2" @@ -98,17 +53,17 @@ packages: dependency: "direct main" description: path: "." - ref: "680222f" - resolved-ref: "680222f503f90d07c08c99c42764f9b08fd0f46c" + ref: "64c0be8" + resolved-ref: "64c0be88a113c2eece5512701527e7d11b8c9239" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "5.1.0" + version: "2.5.1" appflowy_editor_plugins: dependency: "direct main" description: path: "packages/appflowy_editor_plugins" - ref: "4efcff7" - resolved-ref: "4efcff720ed01dd4d0f5f88a9f1ff6f79f423caa" + ref: "87af520732deae1138c12a4c33a62ae56b2aa81f" + resolved-ref: "87af520732deae1138c12a4c33a62ae56b2aa81f" url: "https://github.com/AppFlowy-IO/AppFlowy-plugins.git" source: git version: "0.0.6" @@ -126,29 +81,22 @@ packages: relative: true source: path version: "0.0.1" - appflowy_ui: - dependency: "direct main" - description: - path: "packages/appflowy_ui" - relative: true - source: path - version: "1.0.0" archive: dependency: "direct main" description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "3.4.10" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.4.2" async: dependency: transitive description: @@ -161,69 +109,18 @@ packages: dependency: "direct main" description: name: auto_size_text_field - sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" + sha256: c4ba8714ba4216ca122acac1573581dac499f3162c9218a28b573dca73721b3f url: "https://pub.dev" source: hosted - version: "2.2.4" - auto_updater: - dependency: "direct main" - description: - path: "packages/auto_updater" - ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - resolved-ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - url: "https://github.com/LucasXu0/auto_updater.git" - source: git - version: "1.0.0" - auto_updater_macos: - dependency: "direct overridden" - description: - path: "packages/auto_updater_macos" - ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - resolved-ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - url: "https://github.com/LucasXu0/auto_updater.git" - source: git - version: "1.0.0" - auto_updater_platform_interface: - dependency: "direct overridden" - description: - path: "packages/auto_updater_platform_interface" - ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - resolved-ref: "1d81a824f3633f1d0200ba51b78fe0f9ce429458" - url: "https://github.com/LucasXu0/auto_updater.git" - source: git - version: "1.0.0" - auto_updater_windows: - dependency: transitive - description: - name: auto_updater_windows - sha256: "2bba20a71eee072f49b7267fedd5c4f1406c4b1b1e5b83932c634dbab75b80c9" - url: "https://pub.dev" - source: hosted - version: "1.0.0" + version: "2.2.3" avatar_stack: dependency: "direct main" description: name: avatar_stack - sha256: "354527ba139956fd6439e2c49199d8298d72afdaa6c4cd6f37f26b97faf21f7e" + sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49 url: "https://pub.dev" source: hosted - version: "3.0.0" - barcode: - dependency: transitive - description: - name: barcode - sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" - url: "https://pub.dev" - source: hosted - version: "2.2.9" - bidi: - dependency: transitive - description: - name: bidi - sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" - url: "https://pub.dev" - source: hosted - version: "2.0.12" + version: "1.2.0" bitsdojo_window: dependency: "direct main" description: @@ -268,18 +165,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "8.1.3" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "1dd549e58be35148bc22a9135962106aa29334bc1e3f285994946a1057b29d7b" + sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "9.1.6" boolean_selector: dependency: transitive description: @@ -292,50 +189,50 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.1" build_config: dependency: transitive description: name: build_config - sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.14" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "7.3.0" built_collection: dependency: transitive description: @@ -348,34 +245,34 @@ packages: dependency: transitive description: name: built_value - sha256: "28a712df2576b63c6c005c465989a348604960c0958d28be5303ba9baa841ac2" + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.9.3" + version: "8.9.0" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.3.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.1.1" calendar_view: dependency: "direct main" description: @@ -394,13 +291,13 @@ packages: source: hosted version: "1.3.0" charcode: - dependency: transitive + dependency: "direct main" description: name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -409,6 +306,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + clipboard: + dependency: "direct main" + description: + name: clipboard + sha256: "2ec38f0e59878008ceca0ab122e4bfde98847f88ef0f83331362ba4521f565a9" + url: "https://pub.dev" + source: hosted + version: "0.1.3" clock: dependency: transitive description: @@ -421,18 +326,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.10.0" collection: dependency: "direct main" description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -453,58 +358,50 @@ packages: dependency: transitive description: name: convert - sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.1" coverage: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" url: "https://pub.dev" source: hosted - version: "1.11.1" - cross_cache: + version: "1.7.2" + cross_file: dependency: transitive - description: - name: cross_cache - sha256: "80329477264c73f09945ee780ccdc84df9231f878dc7227d132d301e34ff310b" - url: "https://pub.dev" - source: hosted - version: "0.0.4" - cross_file: - dependency: "direct main" description: name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.4+2" + version: "0.3.4+1" crypto: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.3" csslib: dependency: transitive description: name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.4" dbus: dependency: transitive description: @@ -513,38 +410,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" - defer_pointer: - dependency: "direct main" - description: - name: defer_pointer - sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02 - url: "https://pub.dev" - source: hosted - version: "0.0.2" - desktop_drop: - dependency: "direct main" - description: - name: desktop_drop - sha256: "03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94" - url: "https://pub.dev" - source: hosted - version: "0.5.0" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da" + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 url: "https://pub.dev" source: hosted - version: "11.3.0" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.0.0" diff_match_patch: dependency: transitive description: @@ -554,29 +435,13 @@ packages: source: hosted version: "0.4.1" diffutil_dart: - dependency: "direct main" + dependency: transitive description: name: diffutil_dart sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81" url: "https://pub.dev" source: hosted version: "4.0.1" - dio: - dependency: transitive - description: - name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" - url: "https://pub.dev" - source: hosted - version: "5.7.0" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.dev" - source: hosted - version: "2.0.0" dotted_border: dependency: "direct main" description: @@ -597,10 +462,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.4" easy_logger: dependency: transitive description: @@ -613,34 +478,26 @@ packages: dependency: "direct main" description: name: envied - sha256: "08a9012e5d93e1a816919a52e37c7b8367e73ebb8d52d1ca7dd6fcd875a2cd2c" + sha256: dab29e21452c3d57ec10889d96b06b4a006b01375d4df10b33c9704800c208c4 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.5.3" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: "9a49ca9f3744069661c4f2c06993647699fae2773bca10b593fbb3228d081027" + sha256: b8655d5cb39b4d1d449a79ff6f1367b252c23955ff17ec7c03aacdff938598bd url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.5.3" equatable: dependency: "direct main" description: name: equatable - sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 url: "https://pub.dev" source: hosted - version: "2.0.7" - event_bus: - dependency: "direct main" - description: - name: event_bus - sha256: "1a55e97923769c286d295240048fc180e7b0768902c3c2e869fe059aafa15304" - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "2.0.5" expandable: dependency: "direct main" description: @@ -649,22 +506,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.1" - extended_text_field: - dependency: "direct main" - description: - name: extended_text_field - sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" - url: "https://pub.dev" - source: hosted - version: "16.0.2" - extended_text_library: - dependency: "direct main" - description: - name: extended_text_library - sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" - url: "https://pub.dev" - source: hosted - version: "12.0.1" fake_async: dependency: transitive description: @@ -677,10 +518,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.2" file: dependency: "direct main" description: @@ -690,29 +531,29 @@ packages: source: hosted version: "7.0.0" file_picker: - dependency: "direct overridden" + dependency: transitive description: name: file_picker - sha256: "16dc141db5a2ccc6520ebb6a2eb5945b1b09e95085c021d9f914f8ded7f1465c" + sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.0.3" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.9.3+2" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 url: "https://pub.dev" source: hosted - version: "0.9.4+2" + version: "0.9.3+3" file_selector_platform_interface: dependency: transitive description: @@ -725,34 +566,18 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+1" fixnum: dependency: "direct main" description: name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.1.1" - flex_color_picker: - dependency: "direct main" - description: - name: flex_color_picker - sha256: c083b79f1c57eaeed9f464368be376951230b3cb1876323b784626152a86e480 - url: "https://pub.dev" - source: hosted - version: "3.7.0" - flex_seed_scheme: - dependency: transitive - description: - name: flex_seed_scheme - sha256: d3ba3c5c92d2d79d45e94b4c6c71d01fac3c15017da1545880c53864da5dfeb0 - url: "https://pub.dev" - source: hosted - version: "3.5.0" + version: "1.1.0" flowy_infra: dependency: "direct main" description: @@ -774,6 +599,13 @@ packages: relative: true source: path version: "0.0.1" + flowy_infra_ui_web: + dependency: transitive + description: + path: "packages/flowy_infra_ui/flowy_infra_ui_web" + relative: true + source: path + version: "0.0.1" flowy_svg: dependency: "direct main" description: @@ -790,18 +622,18 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.0" flutter_bloc: dependency: "direct main" description: name: flutter_bloc - sha256: "1046d719fbdf230330d3443187cc33cc11963d15c9089f6cc56faa42a4c5f0cc" + sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "8.1.4" flutter_cache_manager: dependency: "direct main" description: @@ -811,16 +643,8 @@ packages: url: "https://github.com/LucasXu0/flutter_cache_manager.git" source: git version: "3.3.1" - flutter_chat_core: - dependency: "direct main" - description: - name: flutter_chat_core - sha256: "14557aaac7c71b80c279eca41781d214853940cf01727934c742b5845c42dd1e" - url: "https://pub.dev" - source: hosted - version: "0.0.2" flutter_chat_types: - dependency: transitive + dependency: "direct main" description: name: flutter_chat_types sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 @@ -831,10 +655,18 @@ packages: dependency: "direct main" description: name: flutter_chat_ui - sha256: "2afd22eaebaf0f6ec8425048921479c3dd1a229604015dca05b174c6e8e44292" + sha256: "40fb37acc328dd179eadc3d67bf8bd2d950dc0da34464aa8d48e8707e0234c09" url: "https://pub.dev" source: hosted - version: "2.0.0-dev.1" + version: "1.6.13" + flutter_colorpicker: + dependency: "direct main" + description: + name: flutter_colorpicker + sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" + url: "https://pub.dev" + source: hosted + version: "1.0.3" flutter_driver: dependency: transitive description: flutter @@ -844,13 +676,13 @@ packages: dependency: "direct main" description: path: "." - ref: "355aa56" - resolved-ref: "355aa56e9c74a91e00370a882739e0bb98c30bd8" + ref: "4a5cac" + resolved-ref: "4a5cac57e31c0ffd49cd6257a9e078f084ae342c" url: "https://github.com/LucasXu0/emoji_mart.git" source: git version: "1.0.2" flutter_highlight: - dependency: transitive + dependency: "direct main" description: name: flutter_highlight sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" @@ -877,12 +709,12 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "3.0.1" flutter_localizations: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" @@ -890,71 +722,63 @@ packages: dependency: "direct main" description: name: flutter_math_fork - sha256: "284bab89b2fbf1bc3a0baf13d011c1dd324d004e35d177626b77f2fc056366ac" + sha256: "94bee4642892a94939af0748c6a7de0ff8318feee588379dcdfea7dc5cba06c8" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" + flutter_parsed_text: + dependency: transitive + description: + name: flutter_parsed_text + sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" + url: "https://pub.dev" + source: hosted + version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.17" flutter_shaders: dependency: transitive description: name: flutter_shaders - sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.2" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: a857de7ea701f276fd6a6c4c67ae885b60729a3449e42766bb0e655171042801 + sha256: "19ed4813003a6ff4e9c6bcce37e792a2a358919d7603b2b31ff200229191e44c" url: "https://pub.dev" source: hosted - version: "3.1.2" - flutter_staggered_grid_view: - dependency: "direct main" - description: - name: flutter_staggered_grid_view - sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" - url: "https://pub.dev" - source: hosted - version: "0.7.0" + version: "3.0.1" flutter_sticky_header: - dependency: "direct overridden" - description: - name: flutter_sticky_header - sha256: "7f76d24d119424ca0c95c146b8627a457e8de8169b0d584f766c2c545db8f8be" - url: "https://pub.dev" - source: hosted - version: "0.7.0" - flutter_svg: dependency: transitive description: - name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + name: flutter_sticky_header + sha256: "017f398fbb45a589e01491861ca20eb6570a763fd9f3888165a978e11248c709" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "0.6.5" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - flutter_tex: - dependency: "direct main" - description: - name: flutter_tex - sha256: ef7896946052e150514a2afe10f6e33e4fe0e7e4fc51195b65da811cb33c59ab - url: "https://pub.dev" - source: hosted - version: "4.0.13" flutter_web_plugins: dependency: transitive description: flutter @@ -964,47 +788,55 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1" + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 url: "https://pub.dev" source: hosted - version: "8.2.10" + version: "8.2.4" freezed: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.4.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.4.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "3.2.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: "9a0ab83a525c8691a6724746e642de755a299afa04158807787364cd9e718001" + url: "https://pub.dev" + source: hosted + version: "2.0.0" get_it: dependency: "direct main" description: name: get_it - sha256: f126a3e286b7f5b578bf436d5592968706c4c1de28a228b870ce375d9f743103 + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "7.6.7" glob: dependency: transitive description: @@ -1017,26 +849,34 @@ packages: dependency: "direct main" description: name: go_router - sha256: "7c2d40b59890a929824f30d442e810116caf5088482629c894b9e4478c67472d" + sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" url: "https://pub.dev" source: hosted - version: "14.6.3" + version: "13.2.0" google_fonts: dependency: "direct main" description: name: google_fonts - sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8 url: "https://pub.dev" source: hosted - version: "6.2.1" + version: "6.1.0" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9 + url: "https://pub.dev" + source: hosted + version: "2.5.0" graphs: dependency: transitive description: name: graphs - sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.1" gtk: dependency: transitive description: @@ -1054,7 +894,7 @@ packages: source: hosted version: "0.7.0" hive: - dependency: transitive + dependency: "direct main" description: name: hive sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" @@ -1081,90 +921,74 @@ packages: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" url: "https://pub.dev" source: hosted - version: "0.15.5" - html2md: - dependency: "direct main" - description: - name: html2md - sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036" - url: "https://pub.dev" - source: hosted - version: "1.3.2" + version: "0.15.4" http: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.2" - iconsax_flutter: - dependency: transitive + version: "4.0.2" + image_gallery_saver: + dependency: "direct main" description: - name: iconsax_flutter - sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d" + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" url: "https://pub.dev" source: hosted - version: "1.0.0" - image: - dependency: transitive - description: - name: image - sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d - url: "https://pub.dev" - source: hosted - version: "4.3.0" + version: "2.0.3" image_picker: dependency: "direct main" description: name: image_picker - sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.0.7" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: b62d34a506e12bb965e824b6db4fbf709ee4589cf5d3e99b45ab2287b008ee0c + sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" url: "https://pub.dev" source: hosted - version: "0.8.12+20" + version: "0.8.9+3" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" + sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.2" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" + sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 url: "https://pub.dev" source: hosted - version: "0.8.12+2" + version: "0.8.9+1" image_picker_linux: dependency: transitive description: @@ -1185,10 +1009,10 @@ packages: dependency: transitive description: name: image_picker_platform_interface - sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" + sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b url: "https://pub.dev" source: hosted - version: "2.10.1" + version: "2.9.3" image_picker_windows: dependency: transitive description: @@ -1210,32 +1034,40 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + intl_utils: + dependency: transitive + description: + name: intl_utils + sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4 + url: "https://pub.dev" + source: hosted + version: "2.8.7" io: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.4" irondash_engine_context: dependency: transitive description: name: irondash_engine_context - sha256: cd7b769db11a2b5243b037c8a9b1ecaef02e1ae27a2d909ffa78c1dad747bb10 + sha256: "4f5e2629296430cce08cdff42e47cef07b8f74a64fdbdfb0525d147bc1a969a2" url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "0.5.2" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 + sha256: dd581214215dca054bd9873209d690ec3609288c28774cb509dbd86b21180cf8 url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.6.0" isolates: - dependency: transitive + dependency: "direct main" description: name: isolates sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28 @@ -1254,42 +1086,50 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.8.1" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.9.0" + version: "6.7.1" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" keyboard_height_plugin: dependency: "direct main" description: name: keyboard_height_plugin - sha256: "3a51c8ebb43465ebe0b3bad17f3b6d945421e58011f3f5a08134afe69a3d775f" + sha256: bbb32804bf93601249c17c33125cd2e654f5ef650fc6acf1b031d69b478b35ce url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.0.5" leak_tracker: dependency: "direct main" description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: @@ -1326,10 +1166,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "3.0.0" loading_indicator: dependency: transitive description: @@ -1342,34 +1182,34 @@ packages: dependency: "direct main" description: name: local_notifier - sha256: f6cfc933c6fbc961f4e52b5c880f68e41b2d3cd29aad557cc654fd211093a025 + sha256: cc855aa6362c8840e3d3b35b1c3b058a3a8becdb2b03d5a9aa3f3a1e861f0a03 url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "0.1.5" + logger: + dependency: transitive + description: + name: logger + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + url: "https://pub.dev" + source: hosted + version: "2.0.2+1" logging: dependency: transitive description: name: logging - sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.3.0" - macros: + version: "1.2.0" + markdown: dependency: transitive - description: - name: macros - sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" - url: "https://pub.dev" - source: hosted - version: "0.1.3-main.0" - markdown: - dependency: "direct main" description: name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.2.2" markdown_widget: dependency: "direct main" description: @@ -1390,42 +1230,42 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.12.0" mime: - dependency: "direct main" + dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.5" mockito: dependency: transitive description: name: mockito - sha256: f99d8d072e249f719a5531735d146d8cf04c580d93920b04de75bef6dfb2daf6 + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" url: "https://pub.dev" source: hosted - version: "5.4.5" + version: "5.4.4" mocktail: - dependency: "direct dev" + dependency: "direct main" description: name: mocktail - sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.3" nanoid: dependency: "direct main" description: @@ -1462,50 +1302,42 @@ packages: dependency: "direct main" description: name: numerus - sha256: a17a3f34527497e89378696a76f382b40dc534c4a57b3778de246ebc1ce2ca99 + sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.2.0" octo_image: dependency: transitive description: name: octo_image - sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" url: "https://pub.dev" source: hosted - version: "2.1.0" - open_filex: - dependency: "direct main" - description: - name: open_filex - sha256: dcb7bd3d32db8db5260253a62f1564c02c2c8df64bc0187cd213f65f827519bd - url: "https://pub.dev" - source: hosted - version: "4.6.0" + version: "2.0.0" package_config: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "739e0a5c3c4055152520fa321d0645ee98e932718b4c8efeeb51451968fe0790" + sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "6.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "2.0.1" path: dependency: "direct main" description: @@ -1526,34 +1358,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.3.2" path_provider_linux: dependency: transitive description: @@ -1574,26 +1406,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.3.0" - pausable_timer: - dependency: transitive - description: - name: pausable_timer - sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074" - url: "https://pub.dev" - source: hosted - version: "3.1.0+3" - pdf: - dependency: transitive - description: - name: pdf - sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416" - url: "https://pub.dev" - source: hosted - version: "3.11.3" + version: "2.2.1" percent_indicator: dependency: "direct main" description: @@ -1614,34 +1430,34 @@ packages: dependency: transitive description: name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" + sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" url: "https://pub.dev" source: hosted - version: "12.0.13" + version: "12.0.6" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.4" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" url: "https://pub.dev" source: hosted - version: "0.1.3+5" + version: "0.1.1" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.1" permission_handler_windows: dependency: transitive description: @@ -1658,6 +1474,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e" + url: "https://pub.dev" + source: hosted + version: "0.15.0" pixel_snap: dependency: transitive description: @@ -1670,10 +1494,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.4" plugin_platform_interface: dependency: "direct dev" description: @@ -1682,6 +1506,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" + url: "https://pub.dev" + source: hosted + version: "3.7.4" pool: dependency: transitive description: @@ -1690,6 +1522,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: "748ebffffb60b4eaa270955dcf3742a19a2b315344c41ff1b4a0ebcd322b5181" + url: "https://pub.dev" + source: hosted + version: "2.1.0" process: dependency: transitive description: @@ -1718,26 +1558,26 @@ packages: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.5.0" - qr: + version: "1.2.3" + realtime_client: dependency: transitive description: - name: qr - sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + name: realtime_client + sha256: "5831636c19802ba936093a35a7c5b745b130e268fa052e84b4b5290139d2ae03" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "2.0.0" recase: dependency: transitive description: @@ -1749,11 +1589,10 @@ packages: reorderable_tabbar: dependency: "direct main" description: - path: "." - ref: "93c4977" - resolved-ref: "93c4977ffab68906694cdeaea262be6045543c94" - url: "https://github.com/LucasXu0/reorderable_tabbar" - source: git + name: reorderable_tabbar + sha256: dd19d7b6f60f0dec4be02ba0a2c860f9acbe5a392cb8b5b8c1417cbfcbfe923f + url: "https://pub.dev" + source: hosted version: "1.0.6" reorderables: dependency: "direct main" @@ -1763,6 +1602,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" run_with_network_images: dependency: "direct dev" description: @@ -1779,14 +1626,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" - saver_gallery: - dependency: "direct main" - description: - name: saver_gallery - sha256: bf59475e50b73d666630bed7a5fdb621fed92d637f64e3c61ce81653ec6a833c - url: "https://pub.dev" - source: hosted - version: "4.0.1" scaled_app: dependency: "direct main" description: @@ -1799,44 +1638,12 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" url: "https://pub.dev" source: hosted - version: "0.2.0" - screen_retriever_linux: - dependency: transitive - description: - name: screen_retriever_linux - sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_macos: - dependency: transitive - description: - name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_platform_interface: - dependency: transitive - description: - name: screen_retriever_platform_interface - sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 - url: "https://pub.dev" - source: hosted - version: "0.2.0" - screen_retriever_windows: - dependency: transitive - description: - name: screen_retriever_windows - sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.dev" - source: hosted - version: "0.2.0" + version: "0.1.9" scroll_to_index: - dependency: "direct main" + dependency: transitive description: name: scroll_to_index sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 @@ -1851,94 +1658,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" - sentry: - dependency: "direct main" - description: - name: sentry - sha256: "1af8308298977259430d118ab25be8e1dda626cdefa1e6ce869073d530d39271" - url: "https://pub.dev" - source: hosted - version: "8.8.0" - sentry_flutter: - dependency: "direct main" - description: - name: sentry_flutter - sha256: "18fe4d125c2d529bd6127200f0d2895768266a8c60b4fb50b2086fd97e1a4ab2" - url: "https://pub.dev" - source: hosted - version: "8.8.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "3.3.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: a752ce92ea7540fc35a0d19722816e04d0e72828a4200e83a98cf1a1eb524c9a + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: bf808be89fe9dc467475e982c1db6c2faf3d2acf54d526cd5ec37d86c99dbd84 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.3.2" sheet: dependency: "direct main" description: @@ -1952,10 +1743,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_packages_handler: dependency: transitive description: @@ -1968,18 +1759,26 @@ packages: dependency: transitive description: name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "1.0.4" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" simple_gesture_detector: dependency: transitive description: @@ -2000,7 +1799,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.0" + version: "0.0.99" sliver_tools: dependency: transitive description: @@ -2021,26 +1820,26 @@ packages: dependency: transitive description: name: source_helper - sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" url: "https://pub.dev" source: hosted - version: "1.3.5" + version: "1.3.4" source_map_stack_trace: dependency: transitive description: name: source_map_stack_trace - sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" source_maps: dependency: transitive description: name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" url: "https://pub.dev" source: hosted - version: "0.10.13" + version: "0.10.12" source_span: dependency: transitive description: @@ -2061,50 +1860,34 @@ packages: dependency: transitive description: name: sqflite - sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 url: "https://pub.dev" source: hosted - version: "2.4.1" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" - url: "https://pub.dev" - source: hosted - version: "2.4.0" + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" url: "https://pub.dev" source: hosted - version: "2.5.4+6" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" - url: "https://pub.dev" - source: hosted - version: "2.4.1+1" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" + version: "2.5.3" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.11.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 + url: "https://pub.dev" + source: hosted + version: "2.0.1" stream_channel: dependency: transitive description: @@ -2117,26 +1900,26 @@ packages: dependency: transitive description: name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" string_validator: dependency: "direct main" description: name: string_validator - sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 + sha256: "54d4f42cd6878ae72793a58a529d9a18ebfdfbfebd9793bbe55c9b28935e8543" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.2" styled_widget: dependency: "direct main" description: @@ -2145,22 +1928,39 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + supabase: + dependency: transitive + description: + name: supabase + sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + supabase_flutter: + dependency: "direct main" + description: + path: "packages/supabase_flutter" + ref: "9b05eea" + resolved-ref: "9b05eeac559a1f2da6289e1d70b3fa89e262fa3c" + url: "https://github.com/supabase/supabase-flutter" + source: git + version: "2.3.1" super_clipboard: dependency: "direct main" description: name: super_clipboard - sha256: "4a6ae6dfaa282ec1f2bff750976f535517ed8ca842d5deae13985eb11c00ac1f" + sha256: "15d25eb88df8e904e0c2ef77378c6010cc57bbfc0b6f91f2416d08fad5fcca92" url: "https://pub.dev" source: hosted - version: "0.8.24" + version: "0.8.5" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: a433bba8186cd6b707560c42535bf284804665231c00bca86faf1aa4968b7637 + sha256: "530a2118d032483b192713c68ed7105fe64418f22492165f87ed01f9b01d4965" url: "https://pub.dev" source: hosted - version: "0.8.24" + version: "0.8.12" sync_http: dependency: transitive description: @@ -2170,13 +1970,13 @@ packages: source: hosted version: "0.3.1" synchronized: - dependency: "direct main" + dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.1.0+1" tab_indicator_styler: dependency: transitive description: @@ -2189,34 +1989,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63 + sha256: b759eb6caa88dda8e51c70ee43c19d1682f8244458f84cced9138ee35b2ce416 url: "https://pub.dev" source: hosted - version: "3.1.3" - talker: - dependency: "direct main" - description: - name: talker - sha256: "45abef5b92f9b9bd42c3f20133ad4b20ab12e1da2aa206fc0a40ea874bed7c5d" - url: "https://pub.dev" - source: hosted - version: "4.7.1" - talker_bloc_logger: - dependency: "direct main" - description: - name: talker_bloc_logger - sha256: "2214a5f6ef9ff33494dc6149321c270356962725cc8fc1a485d44b1d9b812ddd" - url: "https://pub.dev" - source: hosted - version: "4.7.1" - talker_logger: - dependency: transitive - description: - name: talker_logger - sha256: ed9b20b8c09efff9f6b7c63fc6630ee2f84aa92661ae09e5ba04e77272bf2ad2 - url: "https://pub.dev" - source: hosted - version: "4.7.1" + version: "3.1.1" term_glyph: dependency: transitive description: @@ -2229,50 +2005,50 @@ packages: dependency: transitive description: name: test - sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.25.8" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.0" + textstyle_extensions: + dependency: transitive + description: + name: textstyle_extensions + sha256: b0538352844fb4d1d0eea82f7bc6b96e4dae03a3a071247e4dcc85ec627b2c6c + url: "https://pub.dev" + source: hosted + version: "2.0.0-nullsafety" time: dependency: "direct main" description: name: time - sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.4" timing: dependency: transitive description: name: timing - sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.2" - toastification: - dependency: "direct main" - description: - name: toastification - sha256: "4d97fbfa463dfe83691044cba9f37cb185a79bb9205cfecb655fa1f6be126a13" - url: "https://pub.dev" - source: hosted - version: "2.3.0" + version: "1.0.1" tuple: dependency: transitive description: @@ -2285,10 +2061,10 @@ packages: dependency: transitive description: name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.2" universal_html: dependency: transitive description: @@ -2306,62 +2082,61 @@ packages: source: hosted version: "2.2.2" universal_platform: - dependency: "direct main" + dependency: transitive description: name: universal_platform - sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.0+1" unsplash_client: dependency: "direct main" description: - path: "." - ref: a8411fc - resolved-ref: a8411fcead178834d1f4572f64dc78b059ffa221 - url: "https://github.com/LucasXu0/unsplash_client.git" - source: git + name: unsplash_client + sha256: "9827f4c1036b7a6ac8cb3f404ac179df7441eee69371d9b17f181817fe502fd7" + url: "https://pub.dev" + source: hosted version: "2.2.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.2.6" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.3.14" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.2.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.1.0" url_launcher_platform_interface: dependency: "direct dev" description: @@ -2374,18 +2149,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.3.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.1" url_protocol: dependency: "direct main" description: @@ -2399,42 +2174,42 @@ packages: dependency: "direct overridden" description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.4.0" value_layout_builder: dependency: transitive description: name: value_layout_builder - sha256: c02511ea91ca5c643b514a33a38fa52536f74aa939ec367d02938b5ede6807fa + sha256: "98202ec1807e94ac72725b7f0d15027afde513c55c69ff3f41bcfccb950831bc" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.3.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.13" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -2443,14 +2218,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - version: - dependency: "direct main" - description: - name: version - sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" - url: "https://pub.dev" - source: hosted - version: "3.0.2" visibility_detector: dependency: transitive description: @@ -2463,50 +2230,42 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.2.1" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.0" - web_socket: - dependency: transitive - description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" - url: "https://pub.dev" - source: hosted - version: "0.1.6" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "2.4.5" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: @@ -2515,80 +2274,40 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - webview_flutter: - dependency: transitive - description: - name: webview_flutter - sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" - url: "https://pub.dev" - source: hosted - version: "4.10.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - sha256: d1ee28f44894cbabb1d94cc42f9980297f689ff844d067ec50ff88d86e27d63f - url: "https://pub.dev" - source: hosted - version: "4.3.0" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d - url: "https://pub.dev" - source: hosted - version: "2.10.0" - webview_flutter_plus: - dependency: transitive - description: - name: webview_flutter_plus - sha256: f883dfc94d03b1a2a17441c8e8a8e1941558ed3322f2b586cd06486114e18048 - url: "https://pub.dev" - source: hosted - version: "0.4.10" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - sha256: "4adc14ea9a770cc9e2c8f1ac734536bd40e82615bd0fa6b94be10982de656cc7" - url: "https://pub.dev" - source: hosted - version: "3.17.0" win32: dependency: transitive description: name: win32 - sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29" + sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" url: "https://pub.dev" source: hosted - version: "5.10.0" + version: "5.2.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.2" window_manager: dependency: "direct main" description: name: window_manager - sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.3.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.0.4" xml: - dependency: "direct main" + dependency: transitive description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 @@ -2599,10 +2318,18 @@ packages: dependency: transitive description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.2" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + url: "https://pub.dev" + source: hosted + version: "2.0.0" sdks: - dart: ">=3.6.2 <4.0.0" - flutter: ">=3.27.4" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 1e92765ff6..90853f4c55 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -1,199 +1,196 @@ name: appflowy -description: Bring projects, wikis, and teams together with AI. AppFlowy is an - AI collaborative workspace where you achieve more without losing control of - your data. The best open source alternative to Notion. -publish_to: "none" +description: A new Flutter project. -version: 0.8.9 +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 0.6.1 environment: - flutter: ">=3.27.4" + flutter: ">=3.22.0" sdk: ">=3.3.0 <4.0.0" +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. dependencies: - any_date: ^1.0.4 - app_links: ^6.3.3 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter appflowy_backend: path: packages/appflowy_backend + flowy_infra_ui: + path: packages/flowy_infra_ui + flowy_infra: + path: packages/flowy_infra + flowy_svg: + path: packages/flowy_svg appflowy_board: + # path: ../../../appflowy-board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: e8317c0d1af8d23dc5707b02ea43864536b6de91 - appflowy_editor: - appflowy_editor_plugins: - appflowy_popover: - path: packages/appflowy_popover + ref: 8a6434ae3d02624b614a010af80f775db11bf22e appflowy_result: path: packages/appflowy_result - appflowy_ui: - path: packages/appflowy_ui - archive: ^3.4.10 - auto_size_text_field: ^2.2.3 - auto_updater: ^1.0.0 - avatar_stack: ^3.0.0 + appflowy_editor_plugins: + appflowy_editor: + appflowy_popover: + path: packages/appflowy_popover - # BitsDojo Window for Windows - bitsdojo_window: ^0.1.6 - bloc: ^9.0.0 - cached_network_image: ^3.3.0 + # third party packages + intl: ^0.19.0 + time: ^2.1.3 + equatable: ^2.0.5 + freezed_annotation: ^2.2.0 + get_it: ^7.6.0 + flutter_bloc: ^8.1.3 + flutter_math_fork: ^0.7.2 + provider: ^6.0.5 + path_provider: ^2.0.15 + sized_context: ^1.0.0+4 + styled_widget: ^0.4.1 + expandable: ^5.0.1 + flutter_colorpicker: ^1.0.3 + highlight: ^0.7.0 + package_info_plus: ^6.0.0 + url_launcher: ^6.1.11 + clipboard: ^0.1.3 + connectivity_plus: ^5.0.2 + easy_localization: ^3.0.2 + device_info_plus: ^10.1.0 + fluttertoast: ^8.2.2 + json_annotation: ^4.8.1 + table_calendar: ^3.0.9 + reorderables: ^0.6.0 + linked_scroll_controller: ^0.2.0 + hotkey_manager: ^0.1.7 + fixnum: ^1.1.0 + flutter_svg: ^2.0.7 + protobuf: ^3.1.0 + charcode: ^1.3.1 + collection: ^1.17.1 + bloc: ^8.1.2 + shared_preferences: ^2.2.2 + google_fonts: ^6.1.0 + percent_indicator: ^4.2.3 calendar_view: git: url: https://github.com/Xazin/flutter_calendar_view ref: "6fe0c98" - collection: ^1.17.1 - connectivity_plus: ^5.0.2 - cross_file: ^0.3.4+1 - - # Desktop Drop uses Cross File (XFile) data type - defer_pointer: ^0.0.2 - desktop_drop: ^0.5.0 - device_info_plus: - diffutil_dart: ^4.0.1 + http: ^1.0.0 + path: ^1.8.3 + mocktail: ^1.0.1 + archive: ^3.4.10 + nanoid: ^1.0.0 + supabase_flutter: ^1.10.4 + envied: ^0.5.2 dotted_border: ^2.0.0+3 - easy_localization: ^3.0.2 - envied: ^1.0.1 - equatable: ^2.0.5 - expandable: ^5.0.1 - extended_text_field: ^16.0.2 - extended_text_library: ^12.0.0 - file: ^7.0.0 - fixnum: ^1.1.0 - flex_color_picker: ^3.5.1 - flowy_infra: - path: packages/flowy_infra - flowy_infra_ui: - path: packages/flowy_infra_ui - flowy_svg: - path: packages/flowy_svg - flutter: - sdk: flutter - flutter_animate: ^4.5.0 - flutter_bloc: ^9.1.0 - flutter_cache_manager: ^3.3.1 - flutter_chat_core: 0.0.2 - flutter_chat_ui: ^2.0.0-dev.1 + url_protocol: + hive: ^2.2.3 + hive_flutter: ^1.1.0 + super_clipboard: ^0.8.4 + go_router: ^13.1.0 + string_validator: ^1.0.0 + unsplash_client: ^2.1.1 flutter_emoji_mart: git: url: https://github.com/LucasXu0/emoji_mart.git - ref: "355aa56" - flutter_math_fork: ^0.7.3 - flutter_slidable: ^3.0.0 - - flutter_staggered_grid_view: ^0.7.0 - flutter_tex: ^4.0.9 - fluttertoast: ^8.2.6 - freezed_annotation: ^2.2.0 - get_it: ^8.0.3 - go_router: ^14.2.0 - google_fonts: ^6.1.0 - highlight: ^0.7.0 - hive_flutter: ^1.1.0 - hotkey_manager: ^0.1.7 - html2md: ^1.3.2 - http: ^1.0.0 - image_picker: ^1.0.4 - - # third party packages - intl: ^0.19.0 - json_annotation: ^4.8.1 - keyboard_height_plugin: ^0.1.5 - leak_tracker: ^10.0.0 - linked_scroll_controller: ^0.2.0 + ref: "4a5cac" # Notifications # TODO: Consider implementing custom package # to gather notification handling for all platforms local_notifier: ^0.1.5 - markdown: - markdown_widget: ^2.3.2+6 - mime: ^2.0.0 - nanoid: ^1.0.0 - numerus: ^2.1.2 - # Used to open local files on Mobile - open_filex: ^4.5.0 - package_info_plus: ^8.0.2 - path: ^1.8.3 - path_provider: ^2.0.15 - percent_indicator: 4.2.3 - permission_handler: ^11.3.1 - protobuf: ^3.1.0 - provider: ^6.0.5 - reorderable_tabbar: ^1.0.6 - reorderables: ^0.6.0 - scaled_app: ^2.3.0 - scroll_to_index: ^3.0.1 + app_links: ^3.5.0 + flutter_slidable: ^3.0.0 + image_picker: ^1.0.4 + image_gallery_saver: ^2.0.3 + cached_network_image: ^3.3.0 + leak_tracker: ^10.0.0 + keyboard_height_plugin: ^0.0.5 scrollable_positioned_list: ^0.3.8 - sentry: 8.8.0 - sentry_flutter: 8.8.0 - share_plus: ^10.0.2 - shared_preferences: ^2.2.2 + flutter_cache_manager: ^3.3.1 + share_plus: ^7.2.1 sheet: - sized_context: ^1.0.0+4 - string_validator: ^1.0.0 - styled_widget: ^0.4.1 - super_clipboard: ^0.8.24 - synchronized: ^3.1.0+1 - table_calendar: ^3.0.9 - time: ^2.1.3 - event_bus: ^2.0.1 - - toastification: ^2.0.0 - universal_platform: ^1.1.0 - unsplash_client: ^2.1.1 - url_launcher: ^6.1.11 - url_protocol: + file: ^7.0.0 + avatar_stack: ^1.2.0 + numerus: ^2.1.2 + flutter_animate: ^4.5.0 + permission_handler: ^11.3.1 + flutter_chat_ui: ^1.6.13 + flutter_chat_types: ^3.6.2 + scaled_app: ^2.3.0 + auto_size_text_field: ^2.2.3 + reorderable_tabbar: ^1.0.6 + shimmer: ^3.0.0 + isolates: ^3.0.3+8 + markdown_widget: ^2.3.2+6 # Window Manager for MacOS and Linux - version: ^3.0.2 - xml: ^6.5.0 - window_manager: ^0.4.3 - saver_gallery: ^4.0.1 - talker_bloc_logger: ^4.7.1 - talker: ^4.7.1 + window_manager: ^0.3.9 - analyzer: 6.11.0 + # BitsDojo Window for Windows + bitsdojo_window: ^0.1.6 + flutter_highlight: ^0.7.0 dev_dependencies: - # Introduce talker to log the bloc events, and only log the events in the development mode - - bloc_test: ^10.0.0 - build_runner: ^2.4.9 - envied_generator: ^1.0.1 - flutter_lints: ^5.0.0 + flutter_lints: ^3.0.1 + analyzer: ^6.3.0 flutter_test: sdk: flutter - freezed: ^2.4.7 integration_test: sdk: flutter + build_runner: ^2.4.9 + freezed: ^2.4.7 + bloc_test: ^9.1.2 json_serializable: ^6.7.1 - - mocktail: ^1.0.1 + envied_generator: ^0.5.2 plugin_platform_interface: any - run_with_network_images: ^0.0.1 url_launcher_platform_interface: any + run_with_network_images: ^0.0.1 dependency_overrides: http: ^1.0.0 - device_info_plus: ^11.2.2 + + supabase_flutter: + git: + url: https://github.com/supabase/supabase-flutter + ref: 9b05eea + path: packages/supabase_flutter url_protocol: git: url: https://github.com/LucasXu0/flutter_url_protocol.git - commit: 737681d + commit: 77a8420 appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "680222f" + ref: "64c0be8" appflowy_editor_plugins: git: url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git path: "packages/appflowy_editor_plugins" - ref: "4efcff7" + ref: "87af520732deae1138c12a4c33a62ae56b2aa81f" sheet: git: @@ -209,53 +206,29 @@ dependency_overrides: commit: fbab857b1b1d209240a146d32f496379b9f62276 path: flutter_cache_manager - flutter_sticky_header: ^0.7.0 + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. - reorderable_tabbar: - git: - url: https://github.com/LucasXu0/reorderable_tabbar - ref: 93c4977 - # Don't upgrade file_picker until the issue is fixed - # https://github.com/miguelpruivo/flutter_file_picker/issues/1652 - file_picker: 8.1.4 - - auto_updater: - git: - url: https://github.com/LucasXu0/auto_updater.git - path: packages/auto_updater - ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458 - - auto_updater_macos: - git: - url: https://github.com/LucasXu0/auto_updater.git - path: packages/auto_updater_macos - ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458 - - auto_updater_platform_interface: - git: - url: https://github.com/LucasXu0/auto_updater.git - path: packages/auto_updater_platform_interface - ref: 1d81a824f3633f1d0200ba51b78fe0f9ce429458 - - unsplash_client: - git: - url: https://github.com/LucasXu0/unsplash_client.git - ref: a8411fc - - # auto_updater: - # path: /Users/lucas.xu/Desktop/auto_updater/packages/auto_updater - - # auto_updater_macos: - # path: /Users/lucas.xu/Desktop/auto_updater/packages/auto_updater_macos - - # auto_updater_platform_interface: - # path: /Users/lucas.xu/Desktop/auto_updater/packages/auto_updater_platform_interface +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec +# The following section is specific to Flutter. flutter: + # Automatic code generation for l10n and i18n generate: true + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. uses-material-design: true fonts: + - family: FlowyIconData + fonts: + - asset: assets/fonts/FlowyIconData.ttf - family: Poppins fonts: - asset: assets/google_fonts/Poppins/Poppins-ExtraLight.ttf @@ -281,9 +254,6 @@ flutter: - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf - asset: assets/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf style: italic - # White-label font configuration will be added here - # BEGIN: WHITE_LABEL_FONT - # END: WHITE_LABEL_FONT # To add assets to your application, add an assets section, like this: assets: @@ -292,15 +262,12 @@ flutter: - assets/images/built_in_cover_images/ - assets/flowy_icons/ - assets/flowy_icons/16x/ - - assets/flowy_icons/20x/ - assets/flowy_icons/24x/ - assets/flowy_icons/32x/ - assets/flowy_icons/40x/ - assets/images/emoji/ - assets/images/login/ - assets/translations/ - - assets/icons/icons.json - - assets/fonts/ # The following assets will be excluded in release. # BEGIN: EXCLUDE_IN_RELEASE diff --git a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart deleted file mode 100644 index 46b8118087..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/ai_writer_test/ai_writer_bloc_test.dart +++ /dev/null @@ -1,424 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/ai/operations/ai_writer_entities.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../util.dart'; - -const _aiResponse = 'UPDATED:'; - -class _MockCompletionStream extends Mock implements CompletionStream {} - -class _MockAIRepository extends Mock implements AppFlowyAIService { - @override - Future<(String, CompletionStream)?> streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }) async { - final stream = _MockCompletionStream(); - unawaited( - Future(() async { - await onStart(); - final lines = text.split('\n'); - for (final line in lines) { - if (line.isNotEmpty) { - await processMessage('$_aiResponse $line\n\n'); - } - } - await onEnd(); - }), - ); - return ('mock_id', stream); - } -} - -class _MockAIRepositoryLess extends Mock implements AppFlowyAIService { - @override - Future<(String, CompletionStream)?> streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }) async { - final stream = _MockCompletionStream(); - unawaited( - Future(() async { - await onStart(); - // only return 1 line. - await processMessage('Hello World'); - await onEnd(); - }), - ); - return ('mock_id', stream); - } -} - -class _MockAIRepositoryMore extends Mock implements AppFlowyAIService { - @override - Future<(String, CompletionStream)?> streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }) async { - final stream = _MockCompletionStream(); - unawaited( - Future(() async { - await onStart(); - // return 10 lines - for (var i = 0; i < 10; i++) { - await processMessage('Hello World\n\n'); - } - await onEnd(); - }), - ); - return ('mock_id', stream); - } -} - -class _MockErrorRepository extends Mock implements AppFlowyAIService { - @override - Future<(String, CompletionStream)?> streamCompletion({ - String? objectId, - required String text, - PredefinedFormat? format, - List sourceIds = const [], - List history = const [], - required CompletionTypePB completionType, - required Future Function() onStart, - required Future Function(String text) processMessage, - required Future Function(String text) processAssistMessage, - required Future Function() onEnd, - required void Function(AIError error) onError, - required void Function(LocalAIStreamingState state) - onLocalAIStreamingStateChange, - }) async { - final stream = _MockCompletionStream(); - unawaited( - Future(() async { - await onStart(); - onError( - const AIError( - message: 'Error', - code: AIErrorCode.aiResponseLimitExceeded, - ), - ); - }), - ); - return ('mock_id', stream); - } -} - -void main() { - group('AIWriterCubit:', () { - const text1 = '1. Select text to style using the toolbar menu.'; - const text2 = '2. Discover more styling options in Aa.'; - const text3 = - '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; - - setUp(() { - TestWidgetsFlutterBinding.ensureInitialized(); - }); - - blocTest( - 'send request before the bloc is initialized', - build: () { - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final editorState = EditorState(document: document) - ..selection = selection; - return AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockAIRepository(), - ); - }, - act: (bloc) => bloc.register( - aiWriterNode( - command: AiWriterCommand.explain, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ), - ), - ), - wait: Duration(seconds: 1), - expect: () => [ - isA() - .having((s) => s.markdownText, 'result', isEmpty), - isA() - .having((s) => s.markdownText, 'result', isNotEmpty) - .having((s) => s.markdownText, 'result', contains('UPDATED:')), - isA() - .having((s) => s.markdownText, 'result', isNotEmpty) - .having((s) => s.markdownText, 'result', contains('UPDATED:')), - isA() - .having((s) => s.markdownText, 'result', isNotEmpty) - .having((s) => s.markdownText, 'result', contains('UPDATED:')), - isA() - .having((s) => s.markdownText, 'result', isNotEmpty) - .having((s) => s.markdownText, 'result', contains('UPDATED:')), - ], - ); - - blocTest( - 'exceed the ai response limit', - build: () { - const text1 = '1. Select text to style using the toolbar menu.'; - const text2 = '2. Discover more styling options in Aa.'; - const text3 = - '3. AppFlowy empowers you to beautifully and effortlessly style your content.'; - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - ], - ), - ); - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final editorState = EditorState(document: document) - ..selection = selection; - return AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockErrorRepository(), - ); - }, - act: (bloc) => bloc.register( - aiWriterNode( - command: AiWriterCommand.explain, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ), - ), - ), - wait: Duration(seconds: 1), - expect: () => [ - isA() - .having((s) => s.markdownText, 'result', isEmpty), - isA().having( - (s) => s.error.code, - 'error code', - AIErrorCode.aiResponseLimitExceeded, - ), - ], - ); - - test('improve writing - the result contains the same number of paragraphs', - () async { - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - aiWriterNode( - command: AiWriterCommand.improveWriting, - selection: selection, - ), - ], - ), - ); - final editorState = EditorState(document: document) - ..selection = selection; - final aiNode = editorState.getNodeAtPath([3])!; - final bloc = AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockAIRepository(), - ); - bloc.register(aiNode); - await blocResponseFuture(); - bloc.runResponseAction(SuggestionAction.accept); - await blocResponseFuture(); - expect( - editorState.document.root.children.length, - 3, - ); - expect( - editorState.getNodeAtPath([0])!.delta!.toPlainText(), - '$_aiResponse $text1', - ); - expect( - editorState.getNodeAtPath([1])!.delta!.toPlainText(), - '$_aiResponse $text2', - ); - expect( - editorState.getNodeAtPath([2])!.delta!.toPlainText(), - '$_aiResponse $text3', - ); - }); - - test('improve writing - discard', () async { - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - aiWriterNode( - command: AiWriterCommand.improveWriting, - selection: selection, - ), - ], - ), - ); - final editorState = EditorState(document: document) - ..selection = selection; - final aiNode = editorState.getNodeAtPath([3])!; - final bloc = AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockAIRepository(), - ); - bloc.register(aiNode); - await blocResponseFuture(); - bloc.runResponseAction(SuggestionAction.discard); - await blocResponseFuture(); - expect( - editorState.document.root.children.length, - 3, - ); - expect(editorState.getNodeAtPath([0])!.delta!.toPlainText(), text1); - expect(editorState.getNodeAtPath([1])!.delta!.toPlainText(), text2); - expect(editorState.getNodeAtPath([2])!.delta!.toPlainText(), text3); - }); - - test('improve writing - the result less than the original text', () async { - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - aiWriterNode( - command: AiWriterCommand.improveWriting, - selection: selection, - ), - ], - ), - ); - final editorState = EditorState(document: document) - ..selection = selection; - final aiNode = editorState.getNodeAtPath([3])!; - final bloc = AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockAIRepositoryLess(), - ); - bloc.register(aiNode); - await blocResponseFuture(); - bloc.runResponseAction(SuggestionAction.accept); - await blocResponseFuture(); - expect(editorState.document.root.children.length, 2); - expect( - editorState.getNodeAtPath([0])!.delta!.toPlainText(), - 'Hello World', - ); - }); - - test('improve writing - the result more than the original text', () async { - final selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text3.length), - ); - final document = Document( - root: pageNode( - children: [ - paragraphNode(text: text1), - paragraphNode(text: text2), - paragraphNode(text: text3), - aiWriterNode( - command: AiWriterCommand.improveWriting, - selection: selection, - ), - ], - ), - ); - final editorState = EditorState(document: document) - ..selection = selection; - final aiNode = editorState.getNodeAtPath([3])!; - final bloc = AiWriterCubit( - documentId: '', - editorState: editorState, - aiService: _MockAIRepositoryMore(), - ); - bloc.register(aiNode); - await blocResponseFuture(); - bloc.runResponseAction(SuggestionAction.accept); - await blocResponseFuture(); - expect(editorState.document.root.children.length, 10); - for (var i = 0; i < 10; i++) { - expect( - editorState.getNodeAtPath([i])!.delta!.toPlainText(), - 'Hello World', - ); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart index 9131446347..752ff272da 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/create_or_edit_field_test.dart @@ -34,9 +34,8 @@ void main() { final editorBloc = FieldEditorBloc( viewId: context.gridView.id, - fieldInfo: fieldInfo, + field: fieldInfo.field, fieldController: context.fieldController, - isNew: false, ); await boardResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart index 51bd537159..8208501f06 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart @@ -43,7 +43,7 @@ void main() { ); await boardResponseFuture(); - bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now())); + bloc.add(DateCellEditorEvent.selectDay(DateTime.now())); await boardResponseFuture(); final gridGroupBloc = DatabaseGroupBloc( @@ -89,7 +89,7 @@ void main() { ); await boardResponseFuture(); - bloc.add(DateCellEditorEvent.updateDateTime(DateTime.now())); + bloc.add(DateCellEditorEvent.selectDay(DateTime.now())); await boardResponseFuture(); final gridGroupBloc = DatabaseGroupBloc( @@ -109,8 +109,7 @@ void main() { assert(boardBloc.groupControllers.values.length == 2); assert( - boardBloc.boardController.groupDatas.last.headerData.groupName == - DateTime.now().year.toString(), + boardBloc.boardController.groupDatas.last.headerData.groupName == "2024", ); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart index eb9cdcd443..e29c6a4a73 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/util.dart @@ -54,7 +54,7 @@ Future boardResponseFuture() { return Future.delayed(boardResponseDuration()); } -Duration boardResponseDuration({int milliseconds = 2000}) { +Duration boardResponseDuration({int milliseconds = 200}) { return Duration(milliseconds: milliseconds); } @@ -78,13 +78,14 @@ class BoardTestContext { FieldEditorBloc makeFieldEditor({ required FieldInfo fieldInfo, - }) => - FieldEditorBloc( - viewId: databaseController.viewId, - fieldController: fieldController, - fieldInfo: fieldInfo, - isNew: false, - ); + }) { + final editorBloc = FieldEditorBloc( + viewId: databaseController.viewId, + fieldController: fieldController, + field: fieldInfo.field, + ); + return editorBloc; + } CellController makeCellControllerFromFieldId(String fieldId) { return makeCellController( diff --git a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart index ece0c5e027..29d98416b5 100644 --- a/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart @@ -34,3 +34,11 @@ class AppFlowyChatTest { }); } } + +Future boardResponseFuture() { + return Future.delayed(boardResponseDuration()); +} + +Duration boardResponseDuration({int milliseconds = 200}) { + return Duration(milliseconds: milliseconds); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/checklist_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/checklist_cell_bloc_test.dart deleted file mode 100644 index 441ffea556..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/checklist_cell_bloc_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest cellTest; - - setUpAll(() async { - cellTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('checklist cell bloc:', () { - late GridTestContext context; - late ChecklistCellController cellController; - - setUp(() async { - context = await cellTest.makeDefaultTestGrid(); - await FieldBackendService.createField( - viewId: context.viewId, - fieldType: FieldType.Checklist, - ); - await gridResponseFuture(); - final fieldIndex = context.fieldController.fieldInfos - .indexWhere((field) => field.fieldType == FieldType.Checklist); - cellController = context.makeGridCellController(fieldIndex, 0).as(); - }); - - test('create tasks', () async { - final bloc = ChecklistCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 0); - - bloc.add(const ChecklistCellEvent.createNewTask("B")); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - - bloc.add(const ChecklistCellEvent.createNewTask("A", index: 0)); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 2); - expect(bloc.state.tasks.first.data.name, "A"); - expect(bloc.state.tasks.last.data.name, "B"); - }); - - test('rename task', () async { - final bloc = ChecklistCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 0); - - bloc.add(const ChecklistCellEvent.createNewTask("B")); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - expect(bloc.state.tasks.first.data.name, "B"); - - bloc.add( - ChecklistCellEvent.updateTaskName(bloc.state.tasks.first.data, "A"), - ); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - expect(bloc.state.tasks.first.data.name, "A"); - }); - - test('select task', () async { - final bloc = ChecklistCellBloc(cellController: cellController); - await gridResponseFuture(); - - bloc.add(const ChecklistCellEvent.createNewTask("A")); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - expect(bloc.state.tasks.first.isSelected, false); - - bloc.add(const ChecklistCellEvent.selectTask('A')); - await gridResponseFuture(); - - expect(bloc.state.tasks.first.isSelected, false); - - bloc.add( - ChecklistCellEvent.selectTask(bloc.state.tasks.first.data.id), - ); - await gridResponseFuture(); - - expect(bloc.state.tasks.first.isSelected, true); - }); - - test('delete task', () async { - final bloc = ChecklistCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 0); - - bloc.add(const ChecklistCellEvent.createNewTask("A")); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - expect(bloc.state.tasks.first.isSelected, false); - - bloc.add(const ChecklistCellEvent.deleteTask('A')); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 1); - - bloc.add( - ChecklistCellEvent.deleteTask(bloc.state.tasks.first.data.id), - ); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 0); - }); - - test('reorder task', () async { - final bloc = ChecklistCellBloc(cellController: cellController); - await gridResponseFuture(); - - bloc.add(const ChecklistCellEvent.createNewTask("A")); - await gridResponseFuture(); - bloc.add(const ChecklistCellEvent.createNewTask("B")); - await gridResponseFuture(); - bloc.add(const ChecklistCellEvent.createNewTask("C")); - await gridResponseFuture(); - bloc.add(const ChecklistCellEvent.createNewTask("D")); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 4); - - bloc.add(const ChecklistCellEvent.reorderTask(0, 2)); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 4); - expect(bloc.state.tasks[0].data.name, "B"); - expect(bloc.state.tasks[1].data.name, "A"); - expect(bloc.state.tasks[2].data.name, "C"); - expect(bloc.state.tasks[3].data.name, "D"); - - bloc.add(const ChecklistCellEvent.reorderTask(3, 1)); - await gridResponseFuture(); - - expect(bloc.state.tasks.length, 4); - expect(bloc.state.tasks[0].data.name, "B"); - expect(bloc.state.tasks[1].data.name, "D"); - expect(bloc.state.tasks[2].data.name, "A"); - expect(bloc.state.tasks[3].data.name, "C"); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart deleted file mode 100644 index 71a4dcb3e6..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart +++ /dev/null @@ -1,391 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:time/time.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest cellTest; - - setUpAll(() async { - cellTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('date time cell bloc:', () { - late GridTestContext context; - late DateCellController cellController; - - setUp(() async { - context = await cellTest.makeDefaultTestGrid(); - await FieldBackendService.createField( - viewId: context.viewId, - fieldType: FieldType.DateTime, - ); - await gridResponseFuture(); - final fieldIndex = context.fieldController.fieldInfos - .indexWhere((field) => field.fieldType == FieldType.DateTime); - cellController = context.makeGridCellController(fieldIndex, 0).as(); - }); - - test('select date', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - expect(bloc.state.includeTime, false); - expect(bloc.state.isRange, false); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.updateDateTime(now)); - await gridResponseFuture(); - - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - }); - - test('include time', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.setIncludeTime(true, now, null)); - await gridResponseFuture(); - - expect(bloc.state.includeTime, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime, null); - - bloc.add(const DateCellEditorEvent.setIncludeTime(false, null, null)); - await gridResponseFuture(); - - expect(bloc.state.includeTime, false); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime, null); - }); - - test('end time basic', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.updateDateTime(now)); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime, null); - - bloc.add(const DateCellEditorEvent.setIsRange(true, null, null)); - await gridResponseFuture(); - - expect(bloc.state.isRange, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true); - - bloc.add(const DateCellEditorEvent.setIsRange(false, null, null)); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime, null); - }); - - test('end time from empty', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); - await gridResponseFuture(); - - expect(bloc.state.isRange, true); - expect(bloc.state.dateTime!.isAtSameDayAs(now), true); - expect(bloc.state.endDateTime!.isAtSameDayAs(now), true); - - bloc.add(const DateCellEditorEvent.setIsRange(false, null, null)); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime!.isAtSameDayAs(now), true); - expect(bloc.state.endDateTime, null); - }); - - test('end time unexpected null', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - - final now = DateTime.now(); - // pass in unexpected null as end date time - bloc.add(DateCellEditorEvent.setIsRange(true, now, null)); - await gridResponseFuture(); - - // no changes - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - }); - - test('end time unexpected end', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(bloc.state.isRange, false); - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); - await gridResponseFuture(); - - bloc.add(DateCellEditorEvent.setIsRange(false, now, now)); - await gridResponseFuture(); - - // no change - expect(bloc.state.isRange, true); - expect(bloc.state.dateTime!.isAtSameDayAs(now), true); - expect(bloc.state.endDateTime!.isAtSameDayAs(now), true); - }); - - test('clear date', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - final now = DateTime.now(); - bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); - await gridResponseFuture(); - bloc.add(DateCellEditorEvent.setIncludeTime(true, now, now)); - await gridResponseFuture(); - - expect(bloc.state.isRange, true); - expect(bloc.state.includeTime, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); - expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true); - - bloc.add(const DateCellEditorEvent.clearDate()); - await gridResponseFuture(); - - expect(bloc.state.dateTime, null); - expect(bloc.state.endDateTime, null); - expect(bloc.state.includeTime, false); - expect(bloc.state.isRange, false); - }); - - test('set date format', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect( - bloc.state.dateTypeOptionPB.dateFormat, - DateFormatPB.Friendly, - ); - expect( - bloc.state.dateTypeOptionPB.timeFormat, - TimeFormatPB.TwentyFourHour, - ); - - bloc.add( - const DateCellEditorEvent.setDateFormat(DateFormatPB.ISO), - ); - await gridResponseFuture(); - expect( - bloc.state.dateTypeOptionPB.dateFormat, - DateFormatPB.ISO, - ); - - bloc.add( - const DateCellEditorEvent.setTimeFormat(TimeFormatPB.TwelveHour), - ); - await gridResponseFuture(); - expect( - bloc.state.dateTypeOptionPB.timeFormat, - TimeFormatPB.TwelveHour, - ); - }); - - test('set reminder option', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - expect(reminderBloc.state.reminders.length, 0); - - final now = DateTime.now(); - final threeDaysFromToday = DateTime(now.year, now.month, now.day + 3); - final fourDaysFromToday = DateTime(now.year, now.month, now.day + 4); - final fiveDaysFromToday = DateTime(now.year, now.month, now.day + 5); - - bloc.add(DateCellEditorEvent.updateDateTime(threeDaysFromToday)); - await gridResponseFuture(); - - bloc.add( - const DateCellEditorEvent.setReminderOption( - ReminderOption.onDayOfEvent, - ), - ); - await gridResponseFuture(); - - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - threeDaysFromToday - .add(const Duration(hours: 9)) - .millisecondsSinceEpoch ~/ - 1000, - ), - ); - - reminderBloc.add(const ReminderEvent.refresh()); - await gridResponseFuture(); - - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - threeDaysFromToday - .add(const Duration(hours: 9)) - .millisecondsSinceEpoch ~/ - 1000, - ), - ); - - bloc.add(DateCellEditorEvent.updateDateTime(fourDaysFromToday)); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - fourDaysFromToday - .add(const Duration(hours: 9)) - .millisecondsSinceEpoch ~/ - 1000, - ), - ); - - bloc.add(DateCellEditorEvent.updateDateTime(fiveDaysFromToday)); - await gridResponseFuture(); - reminderBloc.add(const ReminderEvent.refresh()); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - fiveDaysFromToday - .add(const Duration(hours: 9)) - .millisecondsSinceEpoch ~/ - 1000, - ), - ); - - bloc.add( - const DateCellEditorEvent.setReminderOption( - ReminderOption.twoDaysBefore, - ), - ); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - threeDaysFromToday - .add(const Duration(hours: 9)) - .millisecondsSinceEpoch ~/ - 1000, - ), - ); - - bloc.add( - const DateCellEditorEvent.setReminderOption(ReminderOption.none), - ); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 0); - reminderBloc.add(const ReminderEvent.refresh()); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 0); - }); - - test('set reminder option from empty', () async { - final reminderBloc = ReminderBloc(); - final bloc = DateCellEditorBloc( - cellController: cellController, - reminderBloc: reminderBloc, - ); - await gridResponseFuture(); - - final now = DateTime.now(); - final today = DateTime(now.year, now.month, now.day); - bloc.add( - const DateCellEditorEvent.setReminderOption( - ReminderOption.onDayOfEvent, - ), - ); - await gridResponseFuture(); - - expect(bloc.state.dateTime, today); - expect(reminderBloc.state.reminders.length, 1); - expect( - reminderBloc.state.reminders.first.scheduledAt, - Int64( - today.add(const Duration(hours: 9)).millisecondsSinceEpoch ~/ 1000, - ), - ); - - bloc.add(const DateCellEditorEvent.clearDate()); - await gridResponseFuture(); - expect(reminderBloc.state.reminders.length, 0); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart index 4e15d3aaa7..cb5a652c7c 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart @@ -1,6 +1,4 @@ import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,25 +6,20 @@ import 'package:flutter_test/flutter_test.dart'; import '../util.dart'; void main() { - late AppFlowyGridTest cellTest; - + late AppFlowyGridCellTest cellTest; setUpAll(() async { - cellTest = await AppFlowyGridTest.ensureInitialized(); + cellTest = await AppFlowyGridCellTest.ensureInitialized(); }); - group('select cell bloc:', () { - late GridTestContext context; - late SelectOptionCellController cellController; - - setUp(() async { - context = await cellTest.makeDefaultTestGrid(); - await RowBackendService.createRow(viewId: context.viewId); - final fieldIndex = context.fieldController.fieldInfos - .indexWhere((field) => field.fieldType == FieldType.SingleSelect); - cellController = context.makeGridCellController(fieldIndex, 0).as(); - }); - + group('SingleSelectOptionBloc', () { test('create options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -39,6 +32,13 @@ void main() { }); test('update options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -57,6 +57,13 @@ void main() { }); test('delete options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -101,6 +108,13 @@ void main() { }); test('select/unselect option', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -109,7 +123,7 @@ void main() { await gridResponseFuture(); final optionId = bloc.state.options[0].id; - bloc.add(SelectOptionCellEditorEvent.unselectOption(optionId)); + bloc.add(SelectOptionCellEditorEvent.unSelectOption(optionId)); await gridResponseFuture(); assert(bloc.state.selectedOptions.isEmpty); @@ -121,6 +135,13 @@ void main() { }); test('select an option or create one', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -142,6 +163,13 @@ void main() { }); test('select multiple options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); @@ -167,6 +195,13 @@ void main() { }); test('filter options', () async { + await cellTest.createTestGrid(); + await cellTest.createTestRow(); + final cellController = cellTest.makeSelectOptionCellController( + FieldType.SingleSelect, + 0, + ); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await gridResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/text_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/text_cell_bloc_test.dart deleted file mode 100644 index bcd39265d0..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/text_cell_bloc_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/application/row/row_service.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/plugins/database/domain/field_settings_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest cellTest; - - setUpAll(() async { - cellTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('text cell bloc:', () { - late GridTestContext context; - late TextCellController cellController; - - setUp(() async { - context = await cellTest.makeDefaultTestGrid(); - await RowBackendService.createRow(viewId: context.viewId); - final fieldIndex = context.fieldController.fieldInfos - .indexWhere((field) => field.fieldType == FieldType.RichText); - cellController = context.makeGridCellController(fieldIndex, 0).as(); - }); - - test('update text', () async { - final bloc = TextCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.content, ""); - - bloc.add(const TextCellEvent.updateText("A")); - await gridResponseFuture(milliseconds: 600); - - expect(bloc.state.content, "A"); - }); - - test('non-primary text field emoji and hasDocument', () async { - final primaryBloc = TextCellBloc(cellController: cellController); - expect(primaryBloc.state.emoji == null, false); - expect(primaryBloc.state.hasDocument == null, false); - - await primaryBloc.close(); - - await FieldBackendService.createField( - viewId: context.viewId, - fieldName: "Second", - ); - await gridResponseFuture(); - final fieldIndex = context.fieldController.fieldInfos.indexWhere( - (field) => field.fieldType == FieldType.RichText && !field.isPrimary, - ); - cellController = context.makeGridCellController(fieldIndex, 0).as(); - final nonPrimaryBloc = TextCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(nonPrimaryBloc.state.emoji == null, true); - expect(nonPrimaryBloc.state.hasDocument == null, true); - }); - - test('update wrap cell content', () async { - final bloc = TextCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.wrap, true); - - await FieldSettingsBackendService( - viewId: context.viewId, - ).updateFieldSettings( - fieldId: cellController.fieldId, - wrapCellContent: false, - ); - await gridResponseFuture(); - - expect(bloc.state.wrap, false); - }); - - test('update emoji', () async { - final bloc = TextCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.emoji!.value, ""); - - await RowBackendService(viewId: context.viewId) - .updateMeta(rowId: cellController.rowId, iconURL: "dummy"); - await gridResponseFuture(); - - expect(bloc.state.emoji!.value, "dummy"); - }); - - test('update document data', () async { - // This is so fake? - final bloc = TextCellBloc(cellController: cellController); - await gridResponseFuture(); - - expect(bloc.state.hasDocument!.value, false); - - await RowBackendService(viewId: context.viewId) - .updateMeta(rowId: cellController.rowId, isDocumentEmpty: false); - await gridResponseFuture(); - - expect(bloc.state.hasDocument!.value, true); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart new file mode 100644 index 0000000000..32b1d603d9 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/edit_field_test.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../util.dart'; + +Future createEditorBloc(AppFlowyGridTest gridTest) async { + final context = await gridTest.createTestGrid(); + final fieldInfo = context.singleSelectFieldContext(); + return FieldEditorBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + field: fieldInfo.field, + ); +} + +void main() { + late AppFlowyGridTest gridTest; + + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('rename field', () async { + final editorBloc = await createEditorBloc(gridTest); + editorBloc.add(const FieldEditorEvent.renameField('Hello world')); + + await gridResponseFuture(); + expect(editorBloc.state.field.name, equals("Hello world")); + }); + + test('switch to text field', () async { + final editorBloc = await createEditorBloc(gridTest); + + editorBloc.add(const FieldEditorEvent.switchFieldType(FieldType.RichText)); + await gridResponseFuture(); + + // The default length of the fields is 3. The length of the fields + // should not change after switching to other field type + expect(editorBloc.state.field.fieldType, equals(FieldType.RichText)); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart index a1fd6d35cc..c9344a4917 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_cell_bloc_test.dart @@ -11,19 +11,19 @@ void main() { gridTest = await AppFlowyGridTest.ensureInitialized(); }); - group('field cell bloc:', () { + group('$FieldCellBloc', () { late GridTestContext context; late double width; setUp(() async { - context = await gridTest.makeDefaultTestGrid(); + context = await gridTest.createTestGrid(); }); blocTest( 'update field width', build: () => FieldCellBloc( - fieldInfo: context.fieldController.fieldInfos[0], - viewId: context.viewId, + fieldInfo: context.fieldInfos[0], + viewId: context.gridView.id, ), act: (bloc) { width = bloc.state.width; @@ -37,10 +37,10 @@ void main() { ); blocTest( - 'field width should not be less than 50px', + 'field width should not be lesser than 50px', build: () => FieldCellBloc( - viewId: context.viewId, - fieldInfo: context.fieldController.fieldInfos[0], + viewId: context.gridView.id, + fieldInfo: context.fieldInfos[0], ), act: (bloc) { bloc.add(const FieldCellEvent.onResizeStart()); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_editor_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_editor_bloc_test.dart deleted file mode 100644 index c46ea5532b..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/field/field_editor_bloc_test.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:nanoid/nanoid.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('field editor bloc:', () { - late GridTestContext context; - late FieldEditorBloc editorBloc; - - setUp(() async { - context = await gridTest.makeDefaultTestGrid(); - final fieldInfo = context.fieldController.fieldInfos - .firstWhere((field) => field.fieldType == FieldType.SingleSelect); - editorBloc = FieldEditorBloc( - viewId: context.viewId, - fieldController: context.fieldController, - fieldInfo: fieldInfo, - isNew: false, - ); - }); - - test('rename field', () async { - expect(editorBloc.state.field.name, equals("Type")); - - editorBloc.add(const FieldEditorEvent.renameField('Hello world')); - - await gridResponseFuture(); - expect(editorBloc.state.field.name, equals("Hello world")); - }); - - test('edit icon', () async { - expect(editorBloc.state.field.icon, equals("")); - - editorBloc.add(const FieldEditorEvent.updateIcon('emoji/smiley-face')); - - await gridResponseFuture(); - expect(editorBloc.state.field.icon, equals("emoji/smiley-face")); - - editorBloc.add(const FieldEditorEvent.updateIcon("")); - - await gridResponseFuture(); - expect(editorBloc.state.field.icon, equals("")); - }); - - test('switch to text field', () async { - expect(editorBloc.state.field.fieldType, equals(FieldType.SingleSelect)); - - editorBloc.add( - const FieldEditorEvent.switchFieldType(FieldType.RichText), - ); - await gridResponseFuture(); - - expect(editorBloc.state.field.fieldType, equals(FieldType.RichText)); - }); - - test('update field type option', () async { - final selectOption = SelectOptionPB() - ..id = nanoid(4) - ..color = SelectOptionColorPB.Lime - ..name = "New option"; - final typeOptionData = SingleSelectTypeOptionPB() - ..options.addAll([selectOption]); - - editorBloc.add( - FieldEditorEvent.updateTypeOption(typeOptionData.writeToBuffer()), - ); - await gridResponseFuture(); - - final actual = SingleSelectTypeOptionDataParser() - .fromBuffer(editorBloc.state.field.field.typeOptionData); - - expect(actual, equals(typeOptionData)); - }); - - test('update visibility', () async { - expect( - editorBloc.state.field.visibility, - equals(FieldVisibility.AlwaysShown), - ); - - editorBloc.add(const FieldEditorEvent.toggleFieldVisibility()); - await gridResponseFuture(); - - expect( - editorBloc.state.field.visibility, - equals(FieldVisibility.AlwaysHidden), - ); - }); - - test('update wrap cell', () async { - expect( - editorBloc.state.field.wrapCellContent, - equals(true), - ); - - editorBloc.add(const FieldEditorEvent.toggleWrapCellContent()); - await gridResponseFuture(); - - expect( - editorBloc.state.field.wrapCellContent, - equals(false), - ); - }); - - test('insert left and right', () async { - expect( - context.fieldController.fieldInfos.length, - equals(3), - ); - - editorBloc.add(const FieldEditorEvent.insertLeft()); - await gridResponseFuture(); - editorBloc.add(const FieldEditorEvent.insertRight()); - await gridResponseFuture(); - - expect( - context.fieldController.fieldInfos.length, - equals(5), - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart new file mode 100644 index 0000000000..d02a319ab1 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart @@ -0,0 +1,160 @@ +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('create a text filter)', () async { + final context = await gridTest.createTestGrid(); + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + + assert(context.fieldController.filterInfos.length == 1); + }); + + test('delete a text filter)', () async { + final context = await gridTest.createTestGrid(); + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + + final filterInfo = context.fieldController.filterInfos.first; + await service.deleteFilter( + fieldId: textField.id, + filterId: filterInfo.filter.id, + ); + await gridResponseFuture(); + + expect(context.fieldController.filterInfos.length, 0); + }); + + test('filter rows with condition: text is empty', () async { + final context = await gridTest.createTestGrid(); + final service = FilterBackendService(viewId: context.gridView.id); + final gridController = DatabaseController( + view: context.gridView, + ); + final gridBloc = GridBloc( + view: context.gridView, + databaseController: gridController, + )..add(const GridEvent.initial()); + await gridResponseFuture(); + + final textField = context.textFieldContext(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + + expect(gridBloc.state.rowInfos.length, 3); + }); + + test('filter rows with condition: text is empty(After edit the row)', + () async { + final context = await gridTest.createTestGrid(); + final service = FilterBackendService(viewId: context.gridView.id); + final gridController = DatabaseController( + view: context.gridView, + ); + final gridBloc = GridBloc( + view: context.gridView, + databaseController: gridController, + )..add(const GridEvent.initial()); + await gridResponseFuture(); + + final textField = context.textFieldContext(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + + final controller = context.makeTextCellController(0); + await controller.saveCellData("edit text cell content"); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 2); + + await controller.saveCellData(""); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('filter rows with condition: text is not empty', () async { + final context = await gridTest.createTestGrid(); + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + await gridResponseFuture(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsNotEmpty, + content: "", + ); + await gridResponseFuture(); + assert(context.rowInfos.isEmpty); + }); + + test('filter rows with condition: checkbox uncheck', () async { + final context = await gridTest.createTestGrid(); + final checkboxField = context.checkboxFieldContext(); + final service = FilterBackendService(viewId: context.gridView.id); + final gridController = DatabaseController( + view: context.gridView, + ); + final gridBloc = GridBloc( + view: context.gridView, + databaseController: gridController, + )..add(const GridEvent.initial()); + + await gridResponseFuture(); + await service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterConditionPB.IsUnChecked, + ); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('filter rows with condition: checkbox check', () async { + final context = await gridTest.createTestGrid(); + final checkboxField = context.checkboxFieldContext(); + final service = FilterBackendService(viewId: context.gridView.id); + final gridController = DatabaseController( + view: context.gridView, + ); + final gridBloc = GridBloc( + view: context.gridView, + databaseController: gridController, + )..add(const GridEvent.initial()); + + await gridResponseFuture(); + await service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterConditionPB.IsChecked, + ); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.isEmpty); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_editor_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_editor_bloc_test.dart deleted file mode 100644 index f5068282a9..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_editor_bloc_test.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/plugins/database/domain/filter_service.dart'; -import 'package:appflowy/plugins/database/grid/application/filter/filter_editor_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('filter editor bloc:', () { - late GridTestContext context; - late FilterEditorBloc filterBloc; - - setUp(() async { - context = await gridTest.makeDefaultTestGrid(); - filterBloc = FilterEditorBloc( - viewId: context.viewId, - fieldController: context.fieldController, - ); - }); - - FieldInfo getFirstFieldByType(FieldType fieldType) { - return context.fieldController.fieldInfos - .firstWhere((field) => field.fieldType == fieldType); - } - - test('create filter', () async { - expect(filterBloc.state.filters.length, equals(0)); - expect(filterBloc.state.fields.length, equals(3)); - - // through domain directly - final textField = getFirstFieldByType(FieldType.RichText); - final service = FilterBackendService(viewId: context.viewId); - await service.insertTextFilter( - fieldId: textField.id, - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - expect(filterBloc.state.fields.length, equals(3)); - - // through bloc event - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - filterBloc.add(FilterEditorEvent.createFilter(selectOptionField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(2)); - expect(filterBloc.state.filters.first.fieldId, equals(textField.id)); - expect(filterBloc.state.filters[1].fieldId, equals(selectOptionField.id)); - - final filter = filterBloc.state.filters.first as TextFilter; - expect(filter.condition, equals(TextFilterConditionPB.TextIsEmpty)); - expect(filter.content, equals("")); - final filter2 = filterBloc.state.filters[1] as SelectOptionFilter; - expect(filter2.condition, equals(SelectOptionFilterConditionPB.OptionIs)); - expect(filter2.optionIds.length, equals(0)); - expect(filterBloc.state.fields.length, equals(3)); - }); - - test('change filtering field', () async { - final textField = getFirstFieldByType(FieldType.RichText); - final selectField = getFirstFieldByType(FieldType.Checkbox); - filterBloc.add(FilterEditorEvent.createFilter(textField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - expect(filterBloc.state.fields.length, equals(3)); - expect( - filterBloc.state.filters.first.fieldType, - equals(FieldType.RichText), - ); - - final filter = filterBloc.state.filters.first; - filterBloc.add( - FilterEditorEvent.changeFilteringField(filter.filterId, selectField), - ); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - expect( - filterBloc.state.filters.first.fieldType, - equals(FieldType.Checkbox), - ); - expect(filterBloc.state.fields.length, equals(3)); - }); - - test('delete filter', () async { - final textField = getFirstFieldByType(FieldType.RichText); - filterBloc.add(FilterEditorEvent.createFilter(textField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - expect(filterBloc.state.fields.length, equals(3)); - - final filter = filterBloc.state.filters.first; - filterBloc.add(FilterEditorEvent.deleteFilter(filter.filterId)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(0)); - expect(filterBloc.state.fields.length, equals(3)); - }); - - test('update filter', () async { - final service = FilterBackendService(viewId: context.viewId); - final textField = getFirstFieldByType(FieldType.RichText); - - // Create filter - await service.insertTextFilter( - fieldId: textField.id, - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ); - await gridResponseFuture(); - TextFilter filter = filterBloc.state.filters.first as TextFilter; - expect(filter.condition, equals(TextFilterConditionPB.TextIsEmpty)); - - final textFilter = context.fieldController.filters.first; - - // Update the existing filter - await service.insertTextFilter( - fieldId: textField.id, - filterId: textFilter.filterId, - condition: TextFilterConditionPB.TextIs, - content: "ABC", - ); - await gridResponseFuture(); - filter = filterBloc.state.filters.first as TextFilter; - expect(filter.condition, equals(TextFilterConditionPB.TextIs)); - expect(filter.content, equals("ABC")); - }); - - test('update filtering field\'s name', () async { - final textField = getFirstFieldByType(FieldType.RichText); - filterBloc.add(FilterEditorEvent.createFilter(textField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - - expect(filterBloc.state.fields.length, equals(3)); - expect(filterBloc.state.fields.first.name, equals("Name")); - - // edit field - await FieldBackendService( - viewId: context.viewId, - fieldId: textField.id, - ).updateField(name: "New Name"); - await gridResponseFuture(); - expect(filterBloc.state.fields.length, equals(3)); - expect(filterBloc.state.fields.first.name, equals("New Name")); - }); - - test('update field type', () async { - final checkboxField = getFirstFieldByType(FieldType.Checkbox); - filterBloc.add(FilterEditorEvent.createFilter(checkboxField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - - // edit field - await FieldBackendService( - viewId: context.viewId, - fieldId: checkboxField.id, - ).updateType(fieldType: FieldType.DateTime); - await gridResponseFuture(); - - // filter is removed - expect(filterBloc.state.filters.length, equals(0)); - expect(filterBloc.state.fields.length, equals(3)); - expect(filterBloc.state.fields[2].fieldType, FieldType.DateTime); - }); - - test('update filter field', () async { - final checkboxField = getFirstFieldByType(FieldType.Checkbox); - filterBloc.add(FilterEditorEvent.createFilter(checkboxField)); - await gridResponseFuture(); - expect(filterBloc.state.filters.length, equals(1)); - - // edit field - await FieldBackendService( - viewId: context.viewId, - fieldId: checkboxField.id, - ).updateField(name: "HERRO"); - await gridResponseFuture(); - - expect(filterBloc.state.filters.length, equals(1)); - expect(filterBloc.state.fields.length, equals(3)); - expect(filterBloc.state.fields[2].name, "HERRO"); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_entities_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_entities_test.dart deleted file mode 100644 index 12afca48f8..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_entities_test.dart +++ /dev/null @@ -1,602 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/plugins/database/application/field/filter_entities.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:fixnum/fixnum.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('parsing filter entities:', () { - FilterPB createFilterPB( - FieldType fieldType, - Uint8List data, - ) { - return FilterPB( - id: "FT", - filterType: FilterType.Data, - data: FilterDataPB( - fieldId: "FD", - fieldType: fieldType, - data: data, - ), - ); - } - - test('text', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.RichText, - TextFilterPB( - condition: TextFilterConditionPB.TextContains, - content: "c", - ).writeToBuffer(), - ), - ), - equals( - TextFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.RichText, - condition: TextFilterConditionPB.TextContains, - content: "c", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.RichText, - TextFilterPB( - condition: TextFilterConditionPB.TextContains, - content: "", - ).writeToBuffer(), - ), - ), - equals( - TextFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.RichText, - condition: TextFilterConditionPB.TextContains, - content: "", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.RichText, - TextFilterPB( - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ).writeToBuffer(), - ), - ), - equals( - TextFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.RichText, - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.RichText, - TextFilterPB( - condition: TextFilterConditionPB.TextIsEmpty, - content: "", - ).writeToBuffer(), - ), - ), - equals( - TextFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.RichText, - condition: TextFilterConditionPB.TextIsEmpty, - content: "c", - ), - ), - ); - }); - - test('number', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Number, - NumberFilterPB( - condition: NumberFilterConditionPB.GreaterThan, - content: "", - ).writeToBuffer(), - ), - ), - equals( - NumberFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Number, - condition: NumberFilterConditionPB.GreaterThan, - content: "", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Number, - NumberFilterPB( - condition: NumberFilterConditionPB.GreaterThan, - content: "123", - ).writeToBuffer(), - ), - ), - equals( - NumberFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Number, - condition: NumberFilterConditionPB.GreaterThan, - content: "123", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Number, - NumberFilterPB( - condition: NumberFilterConditionPB.NumberIsEmpty, - content: "", - ).writeToBuffer(), - ), - ), - equals( - NumberFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Number, - condition: NumberFilterConditionPB.NumberIsEmpty, - content: "", - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Number, - NumberFilterPB( - condition: NumberFilterConditionPB.NumberIsEmpty, - content: "", - ).writeToBuffer(), - ), - ), - equals( - NumberFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Number, - condition: NumberFilterConditionPB.NumberIsEmpty, - content: "123", - ), - ), - ); - }); - - test('checkbox', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Checkbox, - CheckboxFilterPB( - condition: CheckboxFilterConditionPB.IsChecked, - ).writeToBuffer(), - ), - ), - equals( - const CheckboxFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Checkbox, - condition: CheckboxFilterConditionPB.IsChecked, - ), - ), - ); - }); - - test('checklist', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.Checklist, - ChecklistFilterPB( - condition: ChecklistFilterConditionPB.IsComplete, - ).writeToBuffer(), - ), - ), - equals( - const ChecklistFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.Checklist, - condition: ChecklistFilterConditionPB.IsComplete, - ), - ), - ); - }); - - test('single select option', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.SingleSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: [], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.SingleSelect, - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: const [], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.SingleSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: ['a'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.SingleSelect, - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: const ['a'], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.SingleSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: ['a', 'b'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.SingleSelect, - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: const ['a', 'b'], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.SingleSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: [], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.SingleSelect, - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: const [], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.SingleSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: ['a'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.SingleSelect, - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: const [], - ), - ), - ); - }); - - test('multi select option', () async { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: [], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: const [], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: ['a'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: const ['a'], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: ['a', 'b'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionContains, - optionIds: const ['a', 'b'], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: ['a', 'b'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionIs, - optionIds: const ['a', 'b'], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: [], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: const [], - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.MultiSelect, - SelectOptionFilterPB( - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: ['a'], - ).writeToBuffer(), - ), - ), - equals( - SelectOptionFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.MultiSelect, - condition: SelectOptionFilterConditionPB.OptionIsEmpty, - optionIds: const [], - ), - ), - ); - }); - - test('date time', () { - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateStartsOn, - timestamp: Int64(5), - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateStartsOn, - timestamp: DateTime.fromMillisecondsSinceEpoch(5 * 1000), - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateStartsOn, - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateStartsOn, - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateStartsOn, - start: Int64(5), - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateStartsOn, - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateEndsBetween, - start: Int64(5), - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateEndsBetween, - start: DateTime.fromMillisecondsSinceEpoch(5 * 1000), - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateEndIsNotEmpty, - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateEndIsNotEmpty, - ), - ), - ); - - expect( - DatabaseFilter.fromPB( - createFilterPB( - FieldType.DateTime, - DateFilterPB( - condition: DateFilterConditionPB.DateEndIsNotEmpty, - start: Int64(5), - end: Int64(5), - timestamp: Int64(5), - ).writeToBuffer(), - ), - ), - equals( - DateTimeFilter( - filterId: "FT", - fieldId: "FD", - fieldType: FieldType.DateTime, - condition: DateFilterConditionPB.DateEndIsNotEmpty, - ), - ), - ); - }); - }); - - // group('write to buffer', () { - // test('text', () {}); - // }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart new file mode 100644 index 0000000000..a5aa0c9f58 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart @@ -0,0 +1,68 @@ +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('test filter menu after create a text filter)', () async { + final context = await gridTest.createTestGrid(); + final menuBloc = DatabaseFilterMenuBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const DatabaseFilterMenuEvent.initial()); + await gridResponseFuture(); + assert(menuBloc.state.creatableFields.length == 3); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + assert(menuBloc.state.creatableFields.length == 3); + }); + + test('test filter menu after update existing text filter)', () async { + final context = await gridTest.createTestGrid(); + final menuBloc = DatabaseFilterMenuBloc( + viewId: context.gridView.id, + fieldController: context.fieldController, + )..add(const DatabaseFilterMenuEvent.initial()); + await gridResponseFuture(); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + + // Create filter + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + + final textFilter = context.fieldController.filterInfos.first; + // Update the existing filter + await service.insertTextFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + condition: TextFilterConditionPB.TextIs, + content: "ABC", + ); + await gridResponseFuture(); + assert( + menuBloc.state.filters.first.textFilter()!.condition == + TextFilterConditionPB.TextIs, + ); + assert(menuBloc.state.filters.first.textFilter()!.content == "ABC"); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart new file mode 100644 index 0000000000..675ee8fc57 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_checkbox_test.dart @@ -0,0 +1,55 @@ +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../util.dart'; +import 'filter_util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('filter rows by checkbox is check condition)', () async { + final context = await createTestFilterGrid(gridTest); + final service = FilterBackendService(viewId: context.gridView.id); + + final controller = context.makeCheckboxCellController(0); + await controller.saveCellData("Yes"); + await gridResponseFuture(); + + // create a new filter + final checkboxField = context.checkboxFieldContext(); + await service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterConditionPB.IsChecked, + ); + await gridResponseFuture(); + assert( + context.rowInfos.length == 1, + "expect 1 but receive ${context.rowInfos.length}", + ); + }); + + test('filter rows by checkbox is uncheck condition)', () async { + final context = await createTestFilterGrid(gridTest); + final service = FilterBackendService(viewId: context.gridView.id); + + final controller = context.makeCheckboxCellController(0); + await controller.saveCellData("Yes"); + await gridResponseFuture(); + + // create a new filter + final checkboxField = context.checkboxFieldContext(); + await service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterConditionPB.IsUnChecked, + ); + await gridResponseFuture(); + assert( + context.rowInfos.length == 2, + "expect 2 but receive ${context.rowInfos.length}", + ); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart new file mode 100644 index 0000000000..0af6b18092 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart @@ -0,0 +1,169 @@ +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../util.dart'; +import 'filter_util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('filter rows by text is empty condition)', () async { + final context = await createTestFilterGrid(gridTest); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + // create a new filter + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + assert( + context.fieldController.filterInfos.length == 1, + "expect 1 but receive ${context.fieldController.filterInfos.length}", + ); + assert( + context.rowInfos.length == 1, + "expect 1 but receive ${context.rowInfos.length}", + ); + + // delete the filter + final textFilter = context.fieldController.filterInfos.first; + await service.deleteFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 3); + }); + + test('filter rows by text is not empty condition)', () async { + final context = await createTestFilterGrid(gridTest); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + // create a new filter + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsNotEmpty, + content: "", + ); + await gridResponseFuture(); + assert( + context.rowInfos.length == 2, + "expect 2 but receive ${context.rowInfos.length}", + ); + + // delete the filter + final textFilter = context.fieldController.filterInfos.first; + await service.deleteFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 3); + }); + + test('filter rows by text is empty or is not empty condition)', () async { + final context = await createTestFilterGrid(gridTest); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + // create a new filter + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIsEmpty, + content: "", + ); + await gridResponseFuture(); + assert( + context.fieldController.filterInfos.length == 1, + "expect 1 but receive ${context.fieldController.filterInfos.length}", + ); + assert( + context.rowInfos.length == 1, + "expect 1 but receive ${context.rowInfos.length}", + ); + + // Update the existing filter + final textFilter = context.fieldController.filterInfos.first; + await service.insertTextFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + condition: TextFilterConditionPB.TextIsNotEmpty, + content: "", + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 2); + + // delete the filter + await service.deleteFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 3); + }); + + test('filter rows by text is condition)', () async { + final context = await createTestFilterGrid(gridTest); + + final service = FilterBackendService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + // create a new filter + await service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterConditionPB.TextIs, + content: "A", + ); + await gridResponseFuture(); + assert( + context.rowInfos.length == 1, + "expect 1 but receive ${context.rowInfos.length}", + ); + + // Update the existing filter's content from 'A' to 'B' + final textFilter = context.fieldController.filterInfos.first; + await service.insertTextFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + condition: TextFilterConditionPB.TextIs, + content: "B", + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 1); + + // Update the existing filter's content from 'B' to 'b' + await service.insertTextFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + condition: TextFilterConditionPB.TextIs, + content: "b", + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 1); + + // Update the existing filter with content 'C' + await service.insertTextFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + condition: TextFilterConditionPB.TextIs, + content: "C", + ); + await gridResponseFuture(); + assert(context.rowInfos.isEmpty); + + // delete the filter + await service.deleteFilter( + fieldId: textField.id, + filterId: textFilter.filter.id, + ); + await gridResponseFuture(); + assert(context.rowInfos.length == 3); + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart new file mode 100644 index 0000000000..ee73cf44d9 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_util.dart @@ -0,0 +1,42 @@ +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; + +import '../util.dart'; + +Future createTestFilterGrid(AppFlowyGridTest gridTest) async { + final app = await gridTest.unitTest.createWorkspace(); + final context = await ViewBackendService.createView( + parentViewId: app.id, + name: "Filter Grid", + layoutType: ViewLayoutPB.Grid, + openAfterCreate: true, + ).then((result) { + return result.fold( + (view) async { + final context = GridTestContext( + view, + DatabaseController(view: view), + ); + final result = await context.gridController.open(); + + await editCells(context); + result.fold((l) => null, (r) => throw Exception(r)); + return context; + }, + (error) => throw Exception(), + ); + }); + + return context; +} + +Future editCells(GridTestContext context) async { + final controller0 = context.makeTextCellController(0); + final controller1 = context.makeTextCellController(1); + + await controller0.saveCellData('A'); + await gridResponseFuture(); + await controller1.saveCellData('B'); + await gridResponseFuture(); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart index 8af1a4b7e1..e9ae25b96f 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/grid_bloc_test.dart @@ -7,7 +7,6 @@ import 'util.dart'; void main() { late AppFlowyGridTest gridTest; - setUpAll(() async { gridTest = await AppFlowyGridTest.ensureInitialized(); }); @@ -16,7 +15,7 @@ void main() { late GridTestContext context; setUp(() async { - context = await gridTest.makeDefaultTestGrid(); + context = await gridTest.createTestGrid(); }); // The initial number of rows is 3 for each grid @@ -24,29 +23,32 @@ void main() { blocTest( "create a row", build: () => GridBloc( - view: context.view, - databaseController: DatabaseController(view: context.view), + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), )..add(const GridEvent.initial()), act: (bloc) => bloc.add(const GridEvent.createRow()), - wait: gridResponseDuration(), + wait: const Duration(milliseconds: 300), verify: (bloc) { - expect(bloc.state.rowInfos.length, equals(4)); + assert(bloc.state.rowInfos.length == 4); }, ); blocTest( "delete the last row", build: () => GridBloc( - view: context.view, - databaseController: DatabaseController(view: context.view), + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), )..add(const GridEvent.initial()), act: (bloc) async { await gridResponseFuture(); bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last)); }, - wait: gridResponseDuration(), + wait: const Duration(milliseconds: 300), verify: (bloc) { - expect(bloc.state.rowInfos.length, equals(2)); + assert( + bloc.state.rowInfos.length == 2, + "Expected 2, but receive ${bloc.state.rowInfos.length}", + ); }, ); @@ -57,8 +59,8 @@ void main() { blocTest( 'reorder rows', build: () => GridBloc( - view: context.view, - databaseController: DatabaseController(view: context.view), + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), )..add(const GridEvent.initial()), act: (bloc) async { await gridResponseFuture(); @@ -69,11 +71,10 @@ void main() { bloc.add(const GridEvent.moveRow(0, 2)); }, - wait: gridResponseDuration(), verify: (bloc) { - expect(secondId, equals(bloc.state.rowInfos[0].rowId)); - expect(thirdId, equals(bloc.state.rowInfos[1].rowId)); - expect(firstId, equals(bloc.state.rowInfos[2].rowId)); + expect(secondId, bloc.state.rowInfos[0].rowId); + expect(thirdId, bloc.state.rowInfos[1].rowId); + expect(firstId, bloc.state.rowInfos[2].rowId); }, ); }); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/sort/sort_editor_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/sort/sort_editor_bloc_test.dart deleted file mode 100644 index a0abebd214..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/sort/sort_editor_bloc_test.dart +++ /dev/null @@ -1,204 +0,0 @@ -import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy/plugins/database/domain/field_service.dart'; -import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('sort editor bloc:', () { - late GridTestContext context; - late SortEditorBloc sortBloc; - - setUp(() async { - context = await gridTest.makeDefaultTestGrid(); - sortBloc = SortEditorBloc( - viewId: context.viewId, - fieldController: context.fieldController, - ); - }); - - FieldInfo getFirstFieldByType(FieldType fieldType) { - return context.fieldController.fieldInfos - .firstWhere((field) => field.fieldType == fieldType); - } - - test('create sort', () async { - expect(sortBloc.state.sorts.length, equals(0)); - expect(sortBloc.state.creatableFields.length, equals(3)); - expect(sortBloc.state.allFields.length, equals(3)); - - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - expect(sortBloc.state.sorts.length, 1); - expect(sortBloc.state.sorts.first.fieldId, selectOptionField.id); - expect( - sortBloc.state.sorts.first.condition, - SortConditionPB.Ascending, - ); - expect(sortBloc.state.creatableFields.length, equals(2)); - expect(sortBloc.state.allFields.length, equals(3)); - }); - - test('change sort field', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - - expect( - sortBloc.state.creatableFields - .map((e) => e.id) - .contains(selectOptionField.id), - false, - ); - - final checkboxField = getFirstFieldByType(FieldType.Checkbox); - sortBloc.add( - SortEditorEvent.editSort( - sortId: sortBloc.state.sorts.first.sortId, - fieldId: checkboxField.id, - ), - ); - await gridResponseFuture(); - - expect(sortBloc.state.creatableFields.length, equals(2)); - expect( - sortBloc.state.creatableFields - .map((e) => e.id) - .contains(checkboxField.id), - false, - ); - }); - - test('update sort direction', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - - expect( - sortBloc.state.sorts.first.condition, - SortConditionPB.Ascending, - ); - - sortBloc.add( - SortEditorEvent.editSort( - sortId: sortBloc.state.sorts.first.sortId, - condition: SortConditionPB.Descending, - ), - ); - await gridResponseFuture(); - - expect( - sortBloc.state.sorts.first.condition, - SortConditionPB.Descending, - ); - }); - - test('reorder sorts', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - final checkboxField = getFirstFieldByType(FieldType.Checkbox); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - sortBloc.add(SortEditorEvent.createSort(fieldId: checkboxField.id)); - await gridResponseFuture(); - - expect(sortBloc.state.sorts[0].fieldId, selectOptionField.id); - expect(sortBloc.state.sorts[1].fieldId, checkboxField.id); - expect(sortBloc.state.creatableFields.length, equals(1)); - expect(sortBloc.state.allFields.length, equals(3)); - - sortBloc.add( - const SortEditorEvent.reorderSort(0, 2), - ); - await gridResponseFuture(); - - expect(sortBloc.state.sorts[0].fieldId, checkboxField.id); - expect(sortBloc.state.sorts[1].fieldId, selectOptionField.id); - expect(sortBloc.state.creatableFields.length, equals(1)); - expect(sortBloc.state.allFields.length, equals(3)); - }); - - test('delete sort', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, 1); - - sortBloc.add( - SortEditorEvent.deleteSort(sortBloc.state.sorts.first.sortId), - ); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, 0); - expect(sortBloc.state.creatableFields.length, equals(3)); - expect(sortBloc.state.allFields.length, equals(3)); - }); - - test('delete all sorts', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - final checkboxField = getFirstFieldByType(FieldType.Checkbox); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - sortBloc.add(SortEditorEvent.createSort(fieldId: checkboxField.id)); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, 2); - - sortBloc.add(const SortEditorEvent.deleteAllSorts()); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, 0); - expect(sortBloc.state.creatableFields.length, equals(3)); - expect(sortBloc.state.allFields.length, equals(3)); - }); - - test('update sort field', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, equals(1)); - - // edit field - await FieldBackendService( - viewId: context.viewId, - fieldId: selectOptionField.id, - ).updateField(name: "HERRO"); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, equals(1)); - expect(sortBloc.state.allFields[1].name, "HERRO"); - - expect(sortBloc.state.creatableFields.length, equals(2)); - expect(sortBloc.state.allFields.length, equals(3)); - }); - - test('delete sorting field', () async { - final selectOptionField = getFirstFieldByType(FieldType.SingleSelect); - sortBloc.add(SortEditorEvent.createSort(fieldId: selectOptionField.id)); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, equals(1)); - - // edit field - await FieldBackendService( - viewId: context.viewId, - fieldId: selectOptionField.id, - ).delete(); - await gridResponseFuture(); - - expect(sortBloc.state.sorts.length, equals(0)); - expect(sortBloc.state.creatableFields.length, equals(2)); - expect(sortBloc.state.allFields.length, equals(2)); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart index e80ff1cc4b..66502b44cf 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/util.dart @@ -1,54 +1,91 @@ -import 'dart:convert'; - 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/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_editor_bloc.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/workspace/application/settings/share/import_service.dart'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter/services.dart'; import '../../util.dart'; -const v020GridFileName = "v020.afdb"; -const v069GridFileName = "v069.afdb"; - class GridTestContext { - GridTestContext(this.view, this.databaseController); + GridTestContext(this.gridView, this.gridController); - final ViewPB view; - final DatabaseController databaseController; - - String get viewId => view.id; + final ViewPB gridView; + final DatabaseController gridController; List get rowInfos { - return databaseController.rowCache.rowInfos; + return gridController.rowCache.rowInfos; } - FieldController get fieldController => databaseController.fieldController; + List get fieldInfos => fieldController.fieldInfos; + + FieldController get fieldController { + return gridController.fieldController; + } Future createField(FieldType fieldType) async { final editorBloc = - await createFieldEditor(databaseController: databaseController); + await createFieldEditor(databaseController: gridController); await gridResponseFuture(); editorBloc.add(FieldEditorEvent.switchFieldType(fieldType)); await gridResponseFuture(); - return editorBloc; + return Future(() => editorBloc); } - CellController makeGridCellController(int fieldIndex, int rowIndex) { + FieldInfo singleSelectFieldContext() { + final fieldInfo = fieldInfos + .firstWhere((element) => element.fieldType == FieldType.SingleSelect); + return fieldInfo; + } + + FieldInfo textFieldContext() { + final fieldInfo = fieldInfos + .firstWhere((element) => element.fieldType == FieldType.RichText); + return fieldInfo; + } + + FieldInfo checkboxFieldContext() { + final fieldInfo = fieldInfos + .firstWhere((element) => element.fieldType == FieldType.Checkbox); + return fieldInfo; + } + + SelectOptionCellController makeSelectOptionCellController( + FieldType fieldType, + int rowIndex, + ) { + assert( + fieldType == FieldType.SingleSelect || fieldType == FieldType.MultiSelect, + ); + final field = + fieldInfos.firstWhere((fieldInfo) => fieldInfo.fieldType == fieldType); return makeCellController( - databaseController, - CellContext( - fieldId: fieldController.fieldInfos[fieldIndex].id, - rowId: rowInfos[rowIndex].rowId, - ), + gridController, + CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId), + ).as(); + } + + TextCellController makeTextCellController(int rowIndex) { + final field = fieldInfos + .firstWhere((element) => element.fieldType == FieldType.RichText); + return makeCellController( + gridController, + CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId), + ).as(); + } + + CheckboxCellController makeCheckboxCellController(int rowIndex) { + final field = fieldInfos + .firstWhere((element) => element.fieldType == FieldType.Checkbox); + return makeCellController( + gridController, + CellContext(fieldId: field.id, rowId: rowInfos[rowIndex].rowId), ).as(); } } @@ -65,8 +102,7 @@ Future createFieldEditor({ return FieldEditorBloc( viewId: databaseController.viewId, fieldController: databaseController.fieldController, - fieldInfo: databaseController.fieldController.getField(field.id)!, - isNew: true, + field: field, ); }, (err) => throw Exception(err), @@ -84,74 +120,65 @@ class AppFlowyGridTest { return AppFlowyGridTest(unitTest: inner); } - Future makeDefaultTestGrid() async { - final workspace = await unitTest.createWorkspace(); + Future createTestGrid() async { + final app = await unitTest.createWorkspace(); final context = await ViewBackendService.createView( - parentViewId: workspace.id, + parentViewId: app.id, name: "Test Grid", layoutType: ViewLayoutPB.Grid, openAfterCreate: true, - ).fold( - (view) async { - final databaseController = DatabaseController(view: view); - await databaseController - .open() - .fold((l) => null, (r) => throw Exception(r)); - return GridTestContext( - view, - databaseController, - ); - }, - (error) => throw Exception(), - ); - - return context; - } - - Future makeTestGridFromImportedData( - String fileName, - ) async { - final workspace = await unitTest.createWorkspace(); - - // Don't use the p.join to build the path that used in loadString. It - // is not working on windows. - final data = await rootBundle - .loadString("assets/test/workspaces/database/$fileName"); - - final context = await ImportBackendService.importPages( - workspace.id, - [ - ImportItemPayloadPB() - ..name = fileName - ..data = utf8.encode(data) - ..viewLayout = ViewLayoutPB.Grid - ..importType = ImportTypePB.AFDatabase, - ], - ).fold( - (views) async { - final view = views.items.first; - final databaseController = DatabaseController(view: view); - await databaseController - .open() - .fold((l) => null, (r) => throw Exception(r)); - return GridTestContext( - view, - databaseController, - ); - }, - (err) => throw Exception(), - ); + ).then((result) { + return result.fold( + (view) async { + final context = GridTestContext( + view, + DatabaseController(view: view), + ); + final result = await context.gridController.open(); + result.fold((l) => null, (r) => throw Exception(r)); + return context; + }, + (error) { + throw Exception(); + }, + ); + }); return context; } } -Future gridResponseFuture({int milliseconds = 300}) { - return Future.delayed( - gridResponseDuration(milliseconds: milliseconds), - ); +/// Create a new Grid for cell test +class AppFlowyGridCellTest { + AppFlowyGridCellTest({required this.gridTest}); + + late GridTestContext context; + final AppFlowyGridTest gridTest; + + static Future ensureInitialized() async { + final gridTest = await AppFlowyGridTest.ensureInitialized(); + return AppFlowyGridCellTest(gridTest: gridTest); + } + + Future createTestGrid() async { + context = await gridTest.createTestGrid(); + } + + Future createTestRow() async { + await RowBackendService.createRow(viewId: context.gridView.id); + } + + SelectOptionCellController makeSelectOptionCellController( + FieldType fieldType, + int rowIndex, + ) => + context.makeSelectOptionCellController(fieldType, rowIndex); } -Duration gridResponseDuration({int milliseconds = 300}) { +Future gridResponseFuture({int milliseconds = 400}) { + return Future.delayed(gridResponseDuration(milliseconds: milliseconds)); +} + +Duration gridResponseDuration({int milliseconds = 400}) { return Duration(milliseconds: milliseconds); } diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index 41865b7dd7..d6d0351414 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -182,14 +182,14 @@ void main() { await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); - var workspaceLatest = + var workspaceSetting = await FolderEventGetCurrentWorkspaceSetting().send().then( (result) => result.fold( (l) => l, (r) => throw Exception(), ), ); - workspaceLatest.latestView.id == viewBloc.state.lastCreatedView!.id; + workspaceSetting.latestView.id == viewBloc.state.lastCreatedView!.id; // ignore: unused_local_variable final documentBloc = DocumentBloc(documentId: document.id) @@ -198,13 +198,14 @@ void main() { ); await blocResponseFuture(); - workspaceLatest = await FolderEventGetCurrentWorkspaceSetting().send().then( - (result) => result.fold( - (l) => l, - (r) => throw Exception(), - ), - ); - workspaceLatest.latestView.id == document.id; + workspaceSetting = + await FolderEventGetCurrentWorkspaceSetting().send().then( + (result) => result.fold( + (l) => l, + (r) => throw Exception(), + ), + ); + workspaceSetting.latestView.id == document.id; }); test('create views', () async { diff --git a/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart b/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart index ce57c61bd7..1b896cbd3d 100644 --- a/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/shortcuts_test/shortcuts_cubit_test.dart @@ -1,6 +1,6 @@ import 'dart:ffi'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart'; import 'package:appflowy/workspace/application/settings/shortcuts/settings_shortcuts_service.dart'; import 'package:bloc_test/bloc_test.dart'; diff --git a/frontend/appflowy_flutter/test/unit_test/document/document_diff/document_diff_test.dart b/frontend/appflowy_flutter/test/unit_test/document/document_diff/document_diff_test.dart deleted file mode 100644 index 7124eb93fc..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/document_diff/document_diff_test.dart +++ /dev/null @@ -1,403 +0,0 @@ -import 'package:appflowy/plugins/document/application/document_diff.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('document diff:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - const diff = DocumentDiff(); - - Node createNodeWithId({required String id, required String text}) { - return Node( - id: id, - type: ParagraphBlockKeys.type, - attributes: { - ParagraphBlockKeys.delta: (Delta()..insert(text)).toJson(), - }, - ); - } - - Future applyOperationAndVerifyDocument( - Document before, - Document after, - List operations, - ) async { - final expected = after.toJson(); - final editorState = EditorState(document: before); - final transaction = editorState.transaction; - for (final operation in operations) { - transaction.add(operation); - } - await editorState.apply(transaction); - expect(editorState.document.toJson(), expected); - } - - test('no diff when the document is the same', () async { - // create two nodes with the same id and texts - final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy'); - final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy'); - - final previous = Document.blank()..insert([0], [node1]); - final next = Document.blank()..insert([0], [node2]); - final operations = diff.diffDocument(previous, next); - - expect(operations, isEmpty); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('update text diff with the same id', () async { - final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy'); - final node2 = createNodeWithId(id: '1', text: 'Hello AppFlowy 2'); - - final previous = Document.blank()..insert([0], [node1]); - final next = Document.blank()..insert([0], [node2]); - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 1); - expect(operations[0], isA()); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('delete and insert text diff with different id', () async { - final node1 = createNodeWithId(id: '1', text: 'Hello AppFlowy'); - final node2 = createNodeWithId(id: '2', text: 'Hello AppFlowy 2'); - - final previous = Document.blank()..insert([0], [node1]); - final next = Document.blank()..insert([0], [node2]); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 2); - expect(operations[0], isA()); - expect(operations[1], isA()); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('insert single text diff', () async { - final node1 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node22 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - - final previous = Document.blank()..insert([0], [node1]); - final next = Document.blank()..insert([0], [node21, node22]); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 1); - expect(operations[0], isA()); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('delete single text diff', () async { - final node11 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node12 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - - final previous = Document.blank()..insert([0], [node11, node12]); - final next = Document.blank()..insert([0], [node21]); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 1); - expect(operations[0], isA()); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('insert multiple texts diff', () async { - final node11 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node15 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node22 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - final node23 = createNodeWithId( - id: '3', - text: 'Hello AppFlowy - Third line', - ); - final node24 = createNodeWithId( - id: '4', - text: 'Hello AppFlowy - Fourth line', - ); - final node25 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - - final previous = Document.blank() - ..insert( - [0], - [ - node11, - node15, - ], - ); - final next = Document.blank() - ..insert( - [0], - [ - node21, - node22, - node23, - node24, - node25, - ], - ); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 1); - - final op = operations[0] as InsertOperation; - expect(op.path, [1]); - expect(op.nodes, [node22, node23, node24]); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('delete multiple texts diff', () async { - final node11 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node12 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - final node13 = createNodeWithId( - id: '3', - text: 'Hello AppFlowy - Third line', - ); - final node14 = createNodeWithId( - id: '4', - text: 'Hello AppFlowy - Fourth line', - ); - final node15 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node25 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - - final previous = Document.blank() - ..insert( - [0], - [ - node11, - node12, - node13, - node14, - node15, - ], - ); - final next = Document.blank() - ..insert( - [0], - [ - node21, - node25, - ], - ); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 1); - - final op = operations[0] as DeleteOperation; - expect(op.path, [1]); - expect(op.nodes, [node12, node13, node14]); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('multiple delete and update diff', () async { - final node11 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node12 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - final node13 = createNodeWithId( - id: '3', - text: 'Hello AppFlowy - Third line', - ); - final node14 = createNodeWithId( - id: '4', - text: 'Hello AppFlowy - Fourth line', - ); - final node15 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node22 = createNodeWithId( - id: '2', - text: '', - ); - final node25 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line', - ); - - final previous = Document.blank() - ..insert( - [0], - [ - node11, - node12, - node13, - node14, - node15, - ], - ); - final next = Document.blank() - ..insert( - [0], - [ - node21, - node22, - node25, - ], - ); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 2); - final op1 = operations[0] as UpdateOperation; - expect(op1.path, [1]); - expect(op1.attributes, node22.attributes); - - final op2 = operations[1] as DeleteOperation; - expect(op2.path, [2]); - expect(op2.nodes, [node13, node14]); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - - test('multiple insert and update diff', () async { - final node11 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node12 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line', - ); - final node13 = createNodeWithId( - id: '3', - text: 'Hello AppFlowy - Third line', - ); - final node21 = createNodeWithId( - id: '1', - text: 'Hello AppFlowy - First line', - ); - final node22 = createNodeWithId( - id: '2', - text: 'Hello AppFlowy - Second line - Updated', - ); - final node23 = createNodeWithId( - id: '3', - text: 'Hello AppFlowy - Third line - Updated', - ); - final node24 = createNodeWithId( - id: '4', - text: 'Hello AppFlowy - Fourth line - Updated', - ); - final node25 = createNodeWithId( - id: '5', - text: 'Hello AppFlowy - Fifth line - Updated', - ); - - final previous = Document.blank() - ..insert( - [0], - [ - node11, - node12, - node13, - ], - ); - final next = Document.blank() - ..insert( - [0], - [ - node21, - node22, - node23, - node24, - node25, - ], - ); - - final operations = diff.diffDocument(previous, next); - - expect(operations.length, 3); - final op1 = operations[0] as InsertOperation; - expect(op1.path, [3]); - expect(op1.nodes, [node24, node25]); - - final op2 = operations[1] as UpdateOperation; - expect(op2.path, [1]); - expect(op2.attributes, node22.attributes); - - final op3 = operations[2] as UpdateOperation; - expect(op3.path, [2]); - expect(op3.attributes, node23.attributes); - - await applyOperationAndVerifyDocument(previous, next, operations); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/html/_html_samples.dart b/frontend/appflowy_flutter/test/unit_test/document/html/_html_samples.dart deleted file mode 100644 index 41dea6e01c..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/html/_html_samples.dart +++ /dev/null @@ -1,541 +0,0 @@ -// | Month | Savings | -// | -------- | ------- | -// | January | $250 | -// | February | $80 | -// | March | $420 | -const tableFromNotion = ''' - - - - - - - - - - - - - - - - - - - - -
MonthSavings
January\$250
February\$80
March\$420
-'''; - -// | Month | Savings | -// | -------- | ------- | -// | January | $250 | -// | February | $80 | -// | March | $420 | -const tableFromGoogleDocs = ''' - - -
- - - - - - - - - - - - - - - - - - - - - - - -
-

- - Month - -

-
-

- - Savings - -

-
-

- - January - -

-
-

- - \$250 - -

-
-

- - February - -

-
-

- - \$80 - -

-
-

- - March - -

-
-

- - \$420 - -

-
-
-
-'''; - -// | Month | Savings | -// | -------- | ------- | -// | January | $250 | -// | February | $80 | -// | March | $420 | -const tableFromGoogleSheets = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - -
MonthSavings
January\$250
February\$80
March\$420
-
- -'''; - -// # The Benefits of a Balanced Diet -// A balanced diet is crucial for maintaining overall health and well-being. It provides the necessary nutrients your body needs to function effectively, supports growth and development, and helps prevent chronic diseases. In this guide, we will explore the key benefits of a balanced diet and how it can improve your life. -// --- -// ## Key Components of a Balanced Diet -// A balanced diet consists of various food groups, each providing essential nutrients. The main components include: -// 1. **Carbohydrates** – Provide energy for daily activities. -// 1. **Proteins** – Support growth, muscle repair, and immune function. -// 1. **Fats** – Aid in cell function and energy storage. -// 1. **Vitamins and Minerals** – Essential for immune function, bone health, and overall bodily processes. -// 1. **Fiber** – Promotes healthy digestion and reduces the risk of chronic diseases. -// 1. **Water** – Vital for hydration and proper bodily functions. -// --- -// ## Health Benefits of a Balanced Diet -// Maintaining a balanced diet can have profound effects on your health. Below are some of the most significant benefits: -// --- -// ### 1. **Improved Heart Health** -// A balanced diet rich in fruits, vegetables, and healthy fats helps lower cholesterol levels, reduce inflammation, and maintain a healthy blood pressure. -// ### 2. **Better Weight Management** -// By consuming nutrient-dense foods and avoiding overeating, you can achieve and maintain a healthy weight. -// ### 3. **Enhanced Mental Health** -// Proper nutrition supports brain function, which can improve mood, cognitive performance, and mental well-being. -// ### 4. **Stronger Immune System** -// A diet full of vitamins and minerals strengthens the immune system and helps the body fight off infections. -// --- -// ## Recommended Daily Nutrient Intake -// Below is a table that outlines the recommended daily intake for adults based on the different food groups: -// |Nutrient|Recommended Daily Intake|Example Foods| -// |---|---|---| -// |**Carbohydrates**|45-65% of total calories|Whole grains, fruits, vegetables| -// |**Proteins**|10-35% of total calories|Lean meats, beans, legumes, nuts, dairy| -// |**Fats**|20-35% of total calories|Olive oil, avocado, nuts, fatty fish| -// |**Fiber**|25-30 grams|Whole grains, fruits, vegetables, legumes| -// |**Vitamins & Minerals**|Varies (See below)|Fruits, vegetables, dairy, fortified cereals| -// |**Water**|2-3 liters/day|Water, herbal teas, soups| -// --- -// ## Conclusion -// Incorporating a variety of nutrient-rich foods into your diet is essential for maintaining your health. A balanced diet helps improve your physical and mental well-being, boosts energy levels, and reduces the risk of chronic conditions. By following the guidelines above, you can work toward achieving a healthier and happier life. -const tableFromChatGPT = ''' - -

The Benefits of a Balanced Diet

-

- A balanced diet is crucial for maintaining overall health and well-being. It provides the necessary nutrients your body needs to function effectively, supports growth and development, and helps prevent chronic diseases. In this guide, - we will explore the key benefits of a balanced diet and how it can improve your life. -

-
-

Key Components of a Balanced Diet

-

A balanced diet consists of various food groups, each providing essential nutrients. The main components include:

-
    -
  1. Carbohydrates – Provide energy for daily activities.
  2. -
  3. Proteins – Support growth, muscle repair, and immune function.
  4. -
  5. Fats – Aid in cell function and energy storage.
  6. -
  7. Vitamins and Minerals – Essential for immune function, bone health, and overall bodily processes.
  8. -
  9. Fiber – Promotes healthy digestion and reduces the risk of chronic diseases.
  10. -
  11. Water – Vital for hydration and proper bodily functions.
  12. -
-
-

Health Benefits of a Balanced Diet

-

Maintaining a balanced diet can have profound effects on your health. Below are some of the most significant benefits:

-
-

1. Improved Heart Health

-

A balanced diet rich in fruits, vegetables, and healthy fats helps lower cholesterol levels, reduce inflammation, and maintain a healthy blood pressure.

-

2. Better Weight Management

-

By consuming nutrient-dense foods and avoiding overeating, you can achieve and maintain a healthy weight.

-

3. Enhanced Mental Health

-

Proper nutrition supports brain function, which can improve mood, cognitive performance, and mental well-being.

-

4. Stronger Immune System

-

A diet full of vitamins and minerals strengthens the immune system and helps the body fight off infections.

-
-

Recommended Daily Nutrient Intake

-

Below is a table that outlines the recommended daily intake for adults based on the different food groups:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NutrientRecommended Daily IntakeExample Foods
Carbohydrates45-65% of total caloriesWhole grains, fruits, vegetables
Proteins10-35% of total caloriesLean meats, beans, legumes, nuts, dairy
Fats20-35% of total caloriesOlive oil, avocado, nuts, fatty fish
Fiber25-30 gramsWhole grains, fruits, vegetables, legumes
Vitamins & MineralsVaries (See below)Fruits, vegetables, dairy, fortified cereals
Water2-3 liters/dayWater, herbal teas, soups
-
-

Conclusion

-

- Incorporating a variety of nutrient-rich foods into your diet is essential for maintaining your health. A balanced diet helps improve your physical and mental well-being, boosts energy levels, and reduces the risk of chronic conditions. - By following the guidelines above, you can work toward achieving a healthier and happier life. -

-'''; - -// | Month | Savings | -// | -------- | ------- | -// | January | $250 | -// | February | $80 | -// | March | $420 | -const tableFromAppleNotes = ''' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Month

-
-

Savings

-
-

January

-
-

\$250

-
-

February

-
-

\$80

-
-

March

-
-

\$420

-
- - -'''; diff --git a/frontend/appflowy_flutter/test/unit_test/document/html/paste_from_html_test.dart b/frontend/appflowy_flutter/test/unit_test/document/html/paste_from_html_test.dart deleted file mode 100644 index a36474ef3f..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/html/paste_from_html_test.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '_html_samples.dart'; - -void main() { - group('paste from html:', () { - void checkTable(String html) { - final nodes = EditorState.blank().convertHtmlToNodes(html); - expect(nodes.length, 1); - final table = nodes.first; - expect(table.type, SimpleTableBlockKeys.type); - expect(table.getCellText(0, 0), 'Month'); - expect(table.getCellText(0, 1), 'Savings'); - expect(table.getCellText(1, 0), 'January'); - expect(table.getCellText(1, 1), '\$250'); - expect(table.getCellText(2, 0), 'February'); - expect(table.getCellText(2, 1), '\$80'); - expect(table.getCellText(3, 0), 'March'); - expect(table.getCellText(3, 1), '\$420'); - } - - test('sample 1 - paste table from Notion', () { - checkTable(tableFromNotion); - }); - - test('sample 2 - paste table from Google Docs', () { - checkTable(tableFromGoogleDocs); - }); - - test('sample 3 - paste table from Google Sheets', () { - checkTable(tableFromGoogleSheets); - }); - - test('sample 4 - paste table from ChatGPT', () { - final nodes = EditorState.blank().convertHtmlToNodes(tableFromChatGPT); - final table = - nodes.where((node) => node.type == SimpleTableBlockKeys.type).first; - - expect(table.columnLength, 3); - expect(table.rowLength, 7); - - final dividers = - nodes.where((node) => node.type == DividerBlockKeys.type); - expect(dividers.length, 5); - }); - - test('sample 5 - paste table from Apple Notes', () { - checkTable(tableFromAppleNotes); - }); - }); -} - -extension on Node { - String getCellText( - int row, - int column, { - int index = 0, - }) { - return children[row] - .children[column] - .children[index] - .delta - ?.toPlainText() ?? - ''; - } -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/option_menu/block_action_option_cubit_test.dart b/frontend/appflowy_flutter/test/unit_test/document/option_menu/block_action_option_cubit_test.dart deleted file mode 100644 index 413d830d73..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/option_menu/block_action_option_cubit_test.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('block action option cubit:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('delete blocks', () async { - const text = 'paragraph'; - final document = Document.blank() - ..insert([ - 0, - ], [ - paragraphNode(text: text), - paragraphNode(text: text), - paragraphNode(text: text), - ]); - - final editorState = EditorState(document: document); - final cubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text.length), - ); - editorState.selectionType = SelectionType.block; - - await cubit.handleAction(OptionAction.delete, document.nodeAtPath([0])!); - - // all the nodes should be deleted - expect(document.root.children, isEmpty); - - editorState.dispose(); - }); - - test('duplicate blocks', () async { - const text = 'paragraph'; - final document = Document.blank() - ..insert([ - 0, - ], [ - paragraphNode(text: text), - paragraphNode(text: text), - paragraphNode(text: text), - ]); - - final editorState = EditorState(document: document); - final cubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [2], offset: text.length), - ); - editorState.selectionType = SelectionType.block; - - await cubit.handleAction( - OptionAction.duplicate, - document.nodeAtPath([0])!, - ); - - expect(document.root.children, hasLength(6)); - - editorState.dispose(); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart b/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart deleted file mode 100644 index 21011df540..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/format_shortcut_test.dart +++ /dev/null @@ -1,103 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/format_arrow_character.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('format shortcut:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('turn = + > into ⇒', () async { - final document = Document.blank() - ..insert([ - 0, - ], [ - paragraphNode(text: '='), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: 1), - ); - - final result = await customFormatGreaterEqual.execute(editorState); - expect(result, true); - - expect(editorState.document.root.children.length, 1); - final node = editorState.document.root.children[0]; - expect(node.delta!.toPlainText(), '⇒'); - - // use undo to revert the change - undoCommand.execute(editorState); - expect(editorState.document.root.children.length, 1); - final nodeAfterUndo = editorState.document.root.children[0]; - expect(nodeAfterUndo.delta!.toPlainText(), '=>'); - - editorState.dispose(); - }); - - test('turn - + > into →', () async { - final document = Document.blank() - ..insert([ - 0, - ], [ - paragraphNode(text: '-'), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: 1), - ); - - final result = await customFormatDashGreater.execute(editorState); - expect(result, true); - - expect(editorState.document.root.children.length, 1); - final node = editorState.document.root.children[0]; - expect(node.delta!.toPlainText(), '→'); - - // use undo to revert the change - undoCommand.execute(editorState); - expect(editorState.document.root.children.length, 1); - final nodeAfterUndo = editorState.document.root.children[0]; - expect(nodeAfterUndo.delta!.toPlainText(), '->'); - - editorState.dispose(); - }); - - test('turn -- into —', () async { - final document = Document.blank() - ..insert([ - 0, - ], [ - paragraphNode(text: '-'), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: 1), - ); - - final result = await customFormatDoubleHyphenEmDash.execute(editorState); - expect(result, true); - - expect(editorState.document.root.children.length, 1); - final node = editorState.document.root.children[0]; - expect(node.delta!.toPlainText(), '—'); - - // use undo to revert the change - undoCommand.execute(editorState); - expect(editorState.document.root.children.length, 1); - final nodeAfterUndo = editorState.document.root.children[0]; - expect(nodeAfterUndo.delta!.toPlainText(), '--'); - - editorState.dispose(); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/toggle_list_shortcut_test.dart b/frontend/appflowy_flutter/test/unit_test/document/shortcuts/toggle_list_shortcut_test.dart deleted file mode 100644 index fa84293a33..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/shortcuts/toggle_list_shortcut_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('toggle list shortcut:', () { - Document createDocument(List nodes) { - final document = Document.blank(); - document.insert([0], nodes); - return document; - } - - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - testWidgets('> + #', (tester) async { - const heading1 = '>Heading 1'; - const paragraph1 = 'paragraph 1'; - const paragraph2 = 'paragraph 2'; - - final document = createDocument([ - headingNode(level: 1, text: heading1), - paragraphNode(text: paragraph1), - paragraphNode(text: paragraph2), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: 1), - ); - - final result = await formatGreaterToToggleList.execute(editorState); - expect(result, true); - - expect(editorState.document.root.children.length, 1); - final node = editorState.document.root.children[0]; - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 1); - expect(node.delta!.toPlainText(), 'Heading 1'); - expect(node.children.length, 2); - expect(node.children[0].delta!.toPlainText(), paragraph1); - expect(node.children[1].delta!.toPlainText(), paragraph2); - - editorState.dispose(); - }); - - testWidgets('convert block contains children to toggle list', - (tester) async { - const paragraph1 = '>paragraph 1'; - const paragraph1_1 = 'paragraph 1.1'; - const paragraph1_2 = 'paragraph 1.2'; - - final document = createDocument([ - paragraphNode( - text: paragraph1, - children: [ - paragraphNode(text: paragraph1_1), - paragraphNode(text: paragraph1_2), - ], - ), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: 1), - ); - - final result = await formatGreaterToToggleList.execute(editorState); - expect(result, true); - - expect(editorState.document.root.children.length, 1); - final node = editorState.document.root.children[0]; - expect(node.type, ToggleListBlockKeys.type); - expect(node.delta!.toPlainText(), 'paragraph 1'); - expect(node.children.length, 2); - expect(node.children[0].delta!.toPlainText(), paragraph1_1); - expect(node.children[1].delta!.toPlainText(), paragraph1_2); - - editorState.dispose(); - }); - - testWidgets('press the enter key in empty toggle list', (tester) async { - const text = 'AppFlowy'; - - final document = createDocument([ - toggleListBlockNode(text: text, collapsed: true), - ]); - - final editorState = EditorState(document: document); - editorState.selection = Selection.collapsed( - Position(path: [0], offset: text.length), - ); - - // simulate the enter key press - final result = await insertChildNodeInsideToggleList.execute(editorState); - expect(result, true); - - final nodes = editorState.document.root.children; - expect(nodes.length, 2); - for (var i = 0; i < nodes.length; i++) { - final node = nodes[i]; - expect(node.type, ToggleListBlockKeys.type); - } - - editorState.dispose(); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart deleted file mode 100644 index 8b1b710f4e..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/markdown_text_robot_test.dart +++ /dev/null @@ -1,1153 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/markdown_text_robot.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('markdown text robot:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - Future testLiveRefresh( - List texts, { - required void Function(EditorState) expect, - }) async { - final editorState = EditorState.blank(); - editorState.selection = Selection.collapsed(Position(path: [0])); - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - markdownTextRobot.start(); - for (final text in texts) { - await markdownTextRobot.appendMarkdownText(text); - // mock the delay of the text robot - await Future.delayed(const Duration(milliseconds: 10)); - } - await markdownTextRobot.persist(); - - expect(editorState); - } - - test('parse markdown text (1)', () async { - final editorState = EditorState.blank(); - editorState.selection = Selection.collapsed(Position(path: [0])); - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - - markdownTextRobot.start(); - await markdownTextRobot.appendMarkdownText(_sample1); - await markdownTextRobot.persist(); - - final nodes = editorState.document.root.children; - expect(nodes.length, 4); - - final n1 = nodes[0]; - expect(n1.delta!.toPlainText(), 'The Curious Cat'); - expect(n1.type, HeadingBlockKeys.type); - - final n2 = nodes[1]; - expect(n2.type, ParagraphBlockKeys.type); - expect(n2.delta!.toJson(), [ - {'insert': 'Once upon a time in a '}, - { - 'insert': 'quiet village', - 'attributes': {'bold': true}, - }, - {'insert': ', there lived a curious cat named '}, - { - 'insert': 'Whiskers', - 'attributes': {'italic': true}, - }, - {'insert': '. Unlike other cats, Whiskers had a passion for '}, - { - 'insert': 'exploration', - 'attributes': {'bold': true}, - }, - { - 'insert': - '. Every day, he\'d wander through the village, discovering hidden spots and making new friends with the local animals.', - }, - ]); - - final n3 = nodes[2]; - expect(n3.type, ParagraphBlockKeys.type); - expect(n3.delta!.toJson(), [ - {'insert': 'One sunny morning, Whiskers stumbled upon a mysterious '}, - { - 'insert': 'wooden box', - 'attributes': {'bold': true}, - }, - {'insert': ' behind the old barn. It was covered in '}, - { - 'insert': 'vines and dust', - 'attributes': {'italic': true}, - }, - { - 'insert': - '. Intrigued, he nudged it open with his paw and found a collection of ancient maps. These maps led to secret trails around the village.', - }, - ]); - - final n4 = nodes[3]; - expect(n4.type, ParagraphBlockKeys.type); - expect(n4.delta!.toJson(), [ - { - 'insert': - 'Whiskers became the village\'s hero, guiding everyone on exciting adventures.', - }, - ]); - }); - - // Live refresh - Partial sample - // ## The Decision - // - Aria found an ancient map in her grandmother's attic. - // - The map hinted at a mystical place known as the Enchanted Forest. - // - Legends spoke of the forest as a realm where dreams came to life. - test('live refresh (2)', () async { - await testLiveRefresh( - _liveRefreshSample2, - expect: (editorState) { - final nodes = editorState.document.root.children; - expect(nodes.length, 4); - - final n1 = nodes[0]; - expect(n1.type, HeadingBlockKeys.type); - expect(n1.delta!.toPlainText(), 'The Decision'); - - final n2 = nodes[1]; - expect(n2.type, BulletedListBlockKeys.type); - expect( - n2.delta!.toPlainText(), - 'Aria found an ancient map in her grandmother\'s attic.', - ); - - final n3 = nodes[2]; - expect(n3.type, BulletedListBlockKeys.type); - expect( - n3.delta!.toPlainText(), - 'The map hinted at a mystical place known as the Enchanted Forest.', - ); - - final n4 = nodes[3]; - expect(n4.type, BulletedListBlockKeys.type); - expect( - n4.delta!.toPlainText(), - 'Legends spoke of the forest as a realm where dreams came to life.', - ); - }, - ); - }); - - // Partial sample - // ## The Preparation - // Before embarking on her journey, Aria prepared meticulously: - // 1. Gather Supplies - // - A sturdy backpack - // - A compass and a map - // - Provisions for the week - // 2. Seek Guidance - // - Visited the village elder for advice - // - Listened to tales of past adventurers - // 3. Sharpen Skills - // - Practiced archery and swordsmanship - // - Enhanced survival skills - test('live refresh (3)', () async { - await testLiveRefresh( - _liveRefreshSample3, - expect: (editorState) { - final nodes = editorState.document.root.children; - expect(nodes.length, 5); - - final n1 = nodes[0]; - expect(n1.type, HeadingBlockKeys.type); - expect(n1.delta!.toPlainText(), 'The Preparation'); - - final n2 = nodes[1]; - expect(n2.type, ParagraphBlockKeys.type); - expect( - n2.delta!.toPlainText(), - 'Before embarking on her journey, Aria prepared meticulously:', - ); - - final n3 = nodes[2]; - expect(n3.type, NumberedListBlockKeys.type); - expect( - n3.delta!.toPlainText(), - 'Gather Supplies', - ); - - final n3c1 = n3.children[0]; - expect(n3c1.type, BulletedListBlockKeys.type); - expect(n3c1.delta!.toPlainText(), 'A sturdy backpack'); - - final n3c2 = n3.children[1]; - expect(n3c2.type, BulletedListBlockKeys.type); - expect(n3c2.delta!.toPlainText(), 'A compass and a map'); - - final n3c3 = n3.children[2]; - expect(n3c3.type, BulletedListBlockKeys.type); - expect(n3c3.delta!.toPlainText(), 'Provisions for the week'); - - final n4 = nodes[3]; - expect(n4.type, NumberedListBlockKeys.type); - expect(n4.delta!.toPlainText(), 'Seek Guidance'); - - final n4c1 = n4.children[0]; - expect(n4c1.type, BulletedListBlockKeys.type); - expect( - n4c1.delta!.toPlainText(), - 'Visited the village elder for advice', - ); - - final n4c2 = n4.children[1]; - expect(n4c2.type, BulletedListBlockKeys.type); - expect( - n4c2.delta!.toPlainText(), - 'Listened to tales of past adventurers', - ); - - final n5 = nodes[4]; - expect(n5.type, NumberedListBlockKeys.type); - expect( - n5.delta!.toPlainText(), - 'Sharpen Skills', - ); - - final n5c1 = n5.children[0]; - expect(n5c1.type, BulletedListBlockKeys.type); - expect( - n5c1.delta!.toPlainText(), - 'Practiced archery and swordsmanship', - ); - - final n5c2 = n5.children[1]; - expect(n5c2.type, BulletedListBlockKeys.type); - expect( - n5c2.delta!.toPlainText(), - 'Enhanced survival skills', - ); - }, - ); - }); - - // Partial sample - // Sure, let's provide an alternative Rust implementation for the Two Sum problem, focusing on clarity and efficiency but with a slightly different approach: - // ```rust - // fn two_sum(nums: &[i32], target: i32) -> Vec<(usize, usize)> { - // let mut results = Vec::new(); - // let mut map = std::collections::HashMap::new(); - // - // for (i, &num) in nums.iter().enumerate() { - // let complement = target - num; - // if let Some(&j) = map.get(&complement) { - // results.push((j, i)); - // } - // map.insert(num, i); - // } - // - // results - // } - // - // fn main() { - // let nums = vec![2, 7, 11, 15]; - // let target = 9; - // - // let pairs = two_sum(&nums, target); - // if pairs.is_empty() { - // println!("No two sum solution found"); - // } else { - // for (i, j) in pairs { - // println!("Indices: {}, {}", i, j); - // } - // } - // } - // ``` - test('live refresh (4)', () async { - await testLiveRefresh( - _liveRefreshSample4, - expect: (editorState) { - final nodes = editorState.document.root.children; - expect(nodes.length, 2); - - final n1 = nodes[0]; - expect(n1.type, ParagraphBlockKeys.type); - expect( - n1.delta!.toPlainText(), - '''Sure, let's provide an alternative Rust implementation for the Two Sum problem, focusing on clarity and efficiency but with a slightly different approach:''', - ); - - final n2 = nodes[1]; - expect(n2.type, CodeBlockKeys.type); - expect( - n2.delta!.toPlainText(), - isNotEmpty, - ); - expect(n2.attributes[CodeBlockKeys.language], 'rust'); - }, - ); - }); - }); - - group('markdown text robot - replace in same line:', () { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - Document buildTestDocument() { - return Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1 + text2 + text3)), - ], - ), - ); - } - - // 1. create a document with a paragraph node - // 2. use the text robot to replace the selected content in the same line - // 3. check the document - test('the selection is in the middle of the text', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: text1.length, - ), - end: Position( - path: [0], - offset: text1.length + text2.length, - ), - ); - - final markdownText = - '''Tim Berners-Lee's invention of the **World Wide Web** transformed the internet, making it accessible to _non-technical users_ and opening the floodgates for global mass adoption.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 5); - - final d1 = afterDelta[0] as TextInsert; - expect(d1.text, '${text1}Tim Berners-Lee\'s invention of the '); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'World Wide Web'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect(d3.text, ' transformed the internet, making it accessible to '); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'non-technical users'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect( - d5.text, - ' and opening the floodgates for global mass adoption.$text3', - ); - expect(d5.attributes, null); - }); - - test('replace markdown text with selection from start to middle', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - ), - end: Position( - path: [0], - offset: text1.length, - ), - ); - - final markdownText = - '''The **invention** of the _World Wide Web_ by Tim Berners-Lee transformed how we access information.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 5); - - final d1 = afterDelta[0] as TextInsert; - expect(d1.text, 'The '); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'invention'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect(d3.text, ' of the '); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'World Wide Web'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect( - d5.text, - ' by Tim Berners-Lee transformed how we access information.$text2$text3', - ); - expect(d5.attributes, null); - }); - - test('replace markdown text with selection from middle to end', () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: text1.length + text2.length, - ), - end: Position( - path: [0], - offset: text1.length + text2.length + text3.length, - ), - ); - - final markdownText = - '''**Email** became widespread, and instant messaging services like *ICQ* and **AOL Instant Messenger** gained tremendous popularity, allowing for seamless real-time text communication across the globe.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterDelta = editorState.document.root.children[0].delta!.toList(); - expect(afterDelta.length, 7); - - final d1 = afterDelta[0] as TextInsert; - expect( - d1.text, - text1 + text2, - ); - expect(d1.attributes, null); - - final d2 = afterDelta[1] as TextInsert; - expect(d2.text, 'Email'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = afterDelta[2] as TextInsert; - expect( - d3.text, - ' became widespread, and instant messaging services like ', - ); - expect(d3.attributes, null); - - final d4 = afterDelta[3] as TextInsert; - expect(d4.text, 'ICQ'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = afterDelta[4] as TextInsert; - expect(d5.text, ' and '); - expect(d5.attributes, null); - - final d6 = afterDelta[5] as TextInsert; - expect( - d6.text, - 'AOL Instant Messenger', - ); - expect(d6.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d7 = afterDelta[6] as TextInsert; - expect( - d7.text, - ' gained tremendous popularity, allowing for seamless real-time text communication across the globe.', - ); - expect(d7.attributes, null); - }); - - test('replace markdown text with selection from start to end', () async { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point.'''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption.'''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - final document = Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1)), - paragraphNode(delta: Delta()..insert(text2)), - paragraphNode(delta: Delta()..insert(text3)), - ], - ), - ); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position(path: [0]), - end: Position(path: [0], offset: text1.length), - ); - - final markdownText = '''1. $text1 - -2. $text1 - -3. $text1'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final nodes = editorState.document.root.children; - expect(nodes.length, 5); - - final d1 = nodes[0].delta!.toList()[0] as TextInsert; - expect(d1.text, text1); - expect(d1.attributes, null); - expect(nodes[0].type, NumberedListBlockKeys.type); - - final d2 = nodes[1].delta!.toList()[0] as TextInsert; - expect(d2.text, text1); - expect(d2.attributes, null); - expect(nodes[1].type, NumberedListBlockKeys.type); - - final d3 = nodes[2].delta!.toList()[0] as TextInsert; - expect(d3.text, text1); - expect(d3.attributes, null); - expect(nodes[2].type, NumberedListBlockKeys.type); - - final d4 = nodes[3].delta!.toList()[0] as TextInsert; - expect(d4.text, text2); - expect(d4.attributes, null); - - final d5 = nodes[4].delta!.toList()[0] as TextInsert; - expect(d5.text, text3); - expect(d5.attributes, null); - }); - }); - - group('markdown text robot - replace in multiple lines:', () { - final text1 = - '''The introduction of the World Wide Web in the early 1990s marked a turning point. '''; - final text2 = - '''Tim Berners-Lee's invention made the internet accessible to non-technical users, opening the floodgates for mass adoption. '''; - final text3 = - '''Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity, allowing for real-time text communication.'''; - - Document buildTestDocument() { - return Document( - root: pageNode( - children: [ - paragraphNode(delta: Delta()..insert(text1)), - paragraphNode(delta: Delta()..insert(text2)), - paragraphNode(delta: Delta()..insert(text3)), - ], - ), - ); - } - - // 1. create a document with 3 paragraph nodes - // 2. use the text robot to replace the selected content in the multiple lines - // 3. check the document - test( - 'the selection starts with the first paragraph and ends with the middle of second paragraph', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - ), - end: Position( - path: [1], - offset: text2.length - - ', opening the floodgates for mass adoption. '.length, - ), - ); - - final markdownText = - '''The **introduction** of the World Wide Web in the *early 1990s* marked a significant turning point. - -Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 3); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, 'The '); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, 'introduction'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, ' of the World Wide Web in the '); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, 'early 1990s'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, ' marked a significant turning point.'); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 3); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, "Tim Berners-Lee's "); - expect(d1.attributes, null); - - final d2 = delta2[1] as TextInsert; - expect(d2.text, "revolutionary invention"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta2[2] as TextInsert; - expect( - d3.text, - " made the internet accessible to non-technical users, opening the floodgates for mass adoption. ", - ); - expect(d3.attributes, null); - } - - { - // third paragraph - final delta3 = afterNodes[2].delta!.toList(); - expect(delta3.length, 1); - - final d1 = delta3[0] as TextInsert; - expect(d1.text, text3); - expect(d1.attributes, null); - } - }); - - test( - 'the selection starts with the middle of the first paragraph and ends with the middle of last paragraph', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: 'The introduction of the World Wide Web'.length, - ), - end: Position( - path: [2], - offset: - 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' - .length, - ), - ); - - final markdownText = - ''' in the **early 1990s** marked a *significant turning point* in technological history. - -Tim Berners-Lee's **revolutionary invention** made the internet accessible to non-technical users, opening the floodgates for *unprecedented mass adoption*. - -Email became **widely prevalent**, and instant messaging services like *ICQ* and *AOL Instant Messenger* gained tremendous popularity - '''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 3); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, 'The introduction of the World Wide Web in the '); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, 'early 1990s'); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, ' marked a '); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, 'significant turning point'); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, ' in technological history.'); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 5); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, "Tim Berners-Lee's "); - expect(d1.attributes, null); - - final d2 = delta2[1] as TextInsert; - expect(d2.text, "revolutionary invention"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta2[2] as TextInsert; - expect( - d3.text, - " made the internet accessible to non-technical users, opening the floodgates for ", - ); - expect(d3.attributes, null); - - final d4 = delta2[3] as TextInsert; - expect(d4.text, "unprecedented mass adoption"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta2[4] as TextInsert; - expect(d5.text, "."); - expect(d5.attributes, null); - } - - { - // third paragraph - // third paragraph - final delta3 = afterNodes[2].delta!.toList(); - expect(delta3.length, 7); - - final d1 = delta3[0] as TextInsert; - expect(d1.text, "Email became "); - expect(d1.attributes, null); - - final d2 = delta3[1] as TextInsert; - expect(d2.text, "widely prevalent"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta3[2] as TextInsert; - expect(d3.text, ", and instant messaging services like "); - expect(d3.attributes, null); - - final d4 = delta3[3] as TextInsert; - expect(d4.text, "ICQ"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta3[4] as TextInsert; - expect(d5.text, " and "); - expect(d5.attributes, null); - - final d6 = delta3[5] as TextInsert; - expect(d6.text, "AOL Instant Messenger"); - expect(d6.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d7 = delta3[6] as TextInsert; - expect( - d7.text, - " gained tremendous popularity, allowing for real-time text communication.", - ); - expect(d7.attributes, null); - } - }); - - test( - 'the length of the returned response less than the length of the selected text', - () async { - final document = buildTestDocument(); - final editorState = EditorState(document: document); - - editorState.selection = Selection( - start: Position( - path: [0], - offset: 'The introduction of the World Wide Web'.length, - ), - end: Position( - path: [2], - offset: - 'Email became widespread, and instant messaging services like ICQ and AOL Instant Messenger gained popularity' - .length, - ), - ); - - final markdownText = - ''' in the **early 1990s** marked a *significant turning point* in technological history.'''; - final markdownTextRobot = MarkdownTextRobot( - editorState: editorState, - ); - await markdownTextRobot.replace( - selection: editorState.selection!, - markdownText: markdownText, - ); - - final afterNodes = editorState.document.root.children; - expect(afterNodes.length, 2); - - { - // first paragraph - final delta1 = afterNodes[0].delta!.toList(); - expect(delta1.length, 5); - - final d1 = delta1[0] as TextInsert; - expect(d1.text, "The introduction of the World Wide Web in the "); - expect(d1.attributes, null); - - final d2 = delta1[1] as TextInsert; - expect(d2.text, "early 1990s"); - expect(d2.attributes, {AppFlowyRichTextKeys.bold: true}); - - final d3 = delta1[2] as TextInsert; - expect(d3.text, " marked a "); - expect(d3.attributes, null); - - final d4 = delta1[3] as TextInsert; - expect(d4.text, "significant turning point"); - expect(d4.attributes, {AppFlowyRichTextKeys.italic: true}); - - final d5 = delta1[4] as TextInsert; - expect(d5.text, " in technological history."); - expect(d5.attributes, null); - } - - { - // second paragraph - final delta2 = afterNodes[1].delta!.toList(); - expect(delta2.length, 1); - - final d1 = delta2[0] as TextInsert; - expect(d1.text, ", allowing for real-time text communication."); - expect(d1.attributes, null); - } - }); - }); -} - -const _sample1 = '''# The Curious Cat - -Once upon a time in a **quiet village**, there lived a curious cat named *Whiskers*. Unlike other cats, Whiskers had a passion for **exploration**. Every day, he'd wander through the village, discovering hidden spots and making new friends with the local animals. - -One sunny morning, Whiskers stumbled upon a mysterious **wooden box** behind the old barn. It was covered in _vines and dust_. Intrigued, he nudged it open with his paw and found a collection of ancient maps. These maps led to secret trails around the village. - -Whiskers became the village's hero, guiding everyone on exciting adventures.'''; - -const _liveRefreshSample2 = [ - "##", - " The", - " Decision", - "\n\n", - "-", - " Ar", - "ia", - " found", - " an", - " ancient map", - " in her grandmother", - "'s attic", - ".\n", - "-", - " The map", - " hinted at", - " a", - " mystical", - " place", - " known", - " as", - " the", - " En", - "ch", - "anted", - " Forest", - ".\n", - "-", - " Legends", - " spoke", - " of", - " the", - " forest", - " as", - " a realm", - " where dreams", - " came", - " to", - " life", - ".\n\n", -]; - -const _liveRefreshSample3 = [ - "##", - " The", - " Preparation\n\n", - "Before", - " embarking", - " on", - " her", - " journey", - ", Aria prepared", - " meticulously:\n\n", - "1", - ".", - " **", - "Gather", - " Supplies**", - " \n", - " ", - " -", - " A", - " sturdy", - " backpack", - "\n", - " ", - " -", - " A", - " compass", - " and", - " a map", - "\n ", - " -", - " Pro", - "visions", - " for", - " the", - " week", - "\n\n", - "2", - ".", - " **", - "Seek", - " Guidance", - "**", - " \n", - " ", - " -", - " Vis", - "ited", - " the", - " village", - " elder for advice", - "\n", - " -", - " List", - "ened", - " to", - " tales", - " of past", - " advent", - "urers", - "\n\n", - "3", - ".", - " **", - "Shar", - "pen", - " Skills", - "**", - " \n", - " ", - " -", - " Pract", - "iced", - " arch", - "ery", - " and", - " swordsmanship", - "\n ", - " -", - " Enhanced", - " survival skills", -]; - -const _liveRefreshSample4 = [ - "Sure", - ", let's", - " provide an", - " alternative Rust", - " implementation for the Two", - " Sum", - " problem", - ",", - " focusing", - " on", - " clarity", - " and efficiency", - " but with", - " a slightly", - " different approach", - ":\n\n", - "```", - "rust", - "\nfn two", - "_sum", - "(nums", - ": &[", - "i", - "32", - "],", - " target", - ":", - " i", - "32", - ")", - " ->", - " Vec", - "<(usize", - ", usize", - ")>", - " {\n", - " ", - " let", - " mut results", - " = Vec::", - "new", - "();\n", - " ", - " let mut", - " map", - " =", - " std::collections", - "::", - "HashMap", - "::", - "new", - "();\n\n ", - " for (", - "i,", - " &num", - ") in", - " nums.iter", - "().enumer", - "ate()", - " {\n let", - " complement", - " = target", - " - num", - ";\n", - " ", - " if", - " let", - " Some(&", - "j)", - " =", - " map", - ".get(&", - "complement", - ") {\n", - " results", - ".push((", - "j", - ",", - " i));\n }\n", - " ", - " map", - ".insert", - "(num", - ", i", - ");\n", - " ", - " }\n\n ", - " results\n", - "}\n\n", - "fn", - " main()", - " {\n", - " ", - " let", - " nums", - " =", - " vec![2, ", - "7", - ",", - " 11, 15];\n", - " let", - " target", - " =", - " ", - "9", - ";\n\n", - " ", - " let", - " pairs", - " = two", - "_sum", - "(&", - "nums", - ",", - " target);\n", - " ", - " if", - " pairs", - ".is", - "_empty()", - " {\n ", - " println", - "!(\"", - "No", - " two", - " sum solution", - " found\");\n", - " ", - " }", - " else {\n for", - " (", - "i", - ", j", - ") in", - " pairs {\n", - " println", - "!(\"Indices", - ":", - " {},", - " {}\",", - " i", - ",", - " j", - ");\n ", - " }\n ", - " }\n}\n", - "```\n\n", -]; diff --git a/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart b/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart deleted file mode 100644 index 0186d36c7d..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/text_robot/text_robot_test.dart +++ /dev/null @@ -1,549 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('text robot:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('auto insert text with sentence mode (1)', () async { - final editorState = EditorState.blank(); - editorState.selection = Selection.collapsed(Position(path: [0])); - final textRobot = TextRobot( - editorState: editorState, - ); - for (final text in _sample1) { - await textRobot.autoInsertTextSync( - text, - separator: r'\n\n', - inputType: TextRobotInputType.sentence, - delay: Duration.zero, - ); - } - - final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText(); - final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText(); - final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText(); - - expect( - p1, - 'In a quaint village nestled between rolling hills, a young girl named Elara discovered a hidden garden. She stumbled upon it while chasing a mischievous rabbit through a narrow, winding path. ', - ); - expect( - p2, - 'The garden was a vibrant oasis, brimming with colorful flowers and whispering trees. Elara felt an inexplicable connection to the place, as if it held secrets from a forgotten time. ', - ); - expect( - p3, - 'Determined to uncover its mysteries, she visited daily, unraveling tales of ancient magic and wisdom. The garden transformed her spirit, teaching her the importance of harmony and the beauty of nature\'s wonders.', - ); - }); - - test('auto insert text with sentence mode (2)', () async { - final editorState = EditorState.blank(); - editorState.selection = Selection.collapsed(Position(path: [0])); - final textRobot = TextRobot( - editorState: editorState, - ); - - var breakCount = 0; - for (final text in _sample2) { - if (text.contains('\n\n')) { - breakCount++; - } - await textRobot.autoInsertTextSync( - text, - separator: r'\n\n', - inputType: TextRobotInputType.sentence, - delay: Duration.zero, - ); - } - - final len = editorState.document.root.children.length; - expect(len, breakCount + 1); - expect(len, 7); - - final p1 = editorState.document.nodeAtPath([0])!.delta!.toPlainText(); - final p2 = editorState.document.nodeAtPath([1])!.delta!.toPlainText(); - final p3 = editorState.document.nodeAtPath([2])!.delta!.toPlainText(); - final p4 = editorState.document.nodeAtPath([3])!.delta!.toPlainText(); - final p5 = editorState.document.nodeAtPath([4])!.delta!.toPlainText(); - final p6 = editorState.document.nodeAtPath([5])!.delta!.toPlainText(); - final p7 = editorState.document.nodeAtPath([6])!.delta!.toPlainText(); - - expect( - p1, - 'Once upon a time in the small, whimsical village of Greenhollow, nestled between rolling hills and lush forests, there lived a young girl named Elara. Unlike the other villagers, Elara had a unique gift: she could communicate with animals. This extraordinary ability made her both a beloved and mysterious figure in Greenhollow.', - ); - expect( - p2, - 'One crisp autumn morning, as golden leaves danced in the breeze, Elara heard a distressed call from the forest. Following the sound, she discovered a young fox trapped in a hunter\'s snare. With gentle hands and a calming voice, she freed the frightened creature, who introduced himself as Rufus. Grateful for her help, Rufus promised to assist Elara whenever she needed.', - ); - expect( - p3, - 'Word of Elara\'s kindness spread among the forest animals, and soon she found herself surrounded by a diverse group of animal friends, from wise old owls to playful otters. Together, they shared stories, solved problems, and looked out for one another.', - ); - expect( - p4, - 'One day, the village faced an unexpected threat: a severe drought that threatened their crops and water supply. The villagers grew anxious, unsure of how to cope with the impending scarcity. Elara, determined to help, turned to her animal friends for guidance.', - ); - expect( - p5, - 'The animals led Elara to a hidden spring deep within the forest, a source of fresh water unknown to the villagers. With Rufus\'s clever planning and the otters\' help in directing the flow, they managed to channel the spring water to the village, saving the crops and quenching the villagers\' thirst.', - ); - expect( - p6, - 'Grateful and amazed, the villagers hailed Elara as a hero. They came to understand the importance of living harmoniously with nature and the wonders that could be achieved through kindness and cooperation.', - ); - expect( - p7, - 'From that day on, Greenhollow thrived as a community where humans and animals lived together in harmony, cherishing the bonds that Elara had helped forge. And whenever challenges arose, the villagers knew they could rely on Elara and her extraordinary friends to guide them through, ensuring that the spirit of unity and compassion always prevailed.', - ); - }); - }); -} - -final _sample1 = [ - "In", - " a quaint", - " village", - " nestled", - " between", - " rolling", - " hills", - ",", - " a", - " young", - " girl", - " named", - " El", - "ara discovered", - " a hidden", - " garden", - ".", - " She stumbled", - " upon", - " it", - " while", - " chasing", - " a", - " misch", - "iev", - "ous rabbit", - " through", - " a", - " narrow,", - " winding path", - ".", - " \n\n", - "The", - " garden", - " was", - " a", - " vibrant", - " oasis", - ",", - " br", - "imming with", - " colorful", - " flowers", - " and whisper", - "ing", - " trees", - ".", - " El", - "ara", - " felt", - " an inexp", - "licable", - " connection", - " to", - " the", - " place,", - " as", - " if", - " it held", - " secrets", - " from", - " a", - " forgotten", - " time", - ".", - " \n\n", - "Determ", - "ined to", - " uncover", - " its", - " mysteries", - ",", - " she", - " visited", - " daily,", - " unravel", - "ing", - " tales", - " of", - " ancient", - " magic", - " and", - " wisdom", - ".", - " The", - " garden transformed", - " her", - " spirit", - ", teaching", - " her the", - " importance of harmony and", - " the", - " beauty", - " of", - " nature", - "'s wonders.", -]; - -final _sample2 = [ - "Once", - " upon", - " a", - " time", - " in", - " the small", - ",", - " whimsical", - " village", - " of", - " Green", - "h", - "ollow", - ",", - " nestled", - " between", - " rolling hills", - " and", - " lush", - " forests", - ",", - " there", - " lived", - " a young", - " girl", - " named", - " Elara.", - " Unlike the", - " other", - " villagers", - ",", - " El", - "ara", - " had", - " a unique", - " gift", - ":", - " she could", - " communicate", - " with", - " animals", - ".", - " This", - " extraordinary", - " ability", - " made", - " her both a", - " beloved", - " and", - " mysterious", - " figure", - " in", - " Green", - "h", - "ollow", - ".\n\n", - "One", - " crisp", - " autumn", - " morning,", - " as", - " golden", - " leaves", - " danced", - " in", - " the", - " breeze", - ", El", - "ara heard", - " a distressed", - " call", - " from", - " the", - " forest", - ".", - " Following", - " the", - " sound", - ",", - " she", - " discovered", - " a", - " young", - " fox", - " trapped", - " in", - " a", - " hunter's", - " snare", - ".", - " With", - " gentle", - " hands", - " and", - " a", - " calming", - " voice", - ",", - " she", - " freed", - " the", - " frightened", - " creature", - ", who", - " introduced", - " himself", - " as Ruf", - "us.", - " Gr", - "ateful", - " for", - " her", - " help", - ",", - " Rufus promised", - " to assist", - " Elara", - " whenever", - " she", - " needed.\n\n", - "Word", - " of", - " Elara", - "'s kindness", - " spread among", - " the forest", - " animals", - ",", - " and soon", - " she", - " found", - " herself", - " surrounded", - " by", - " a", - " diverse", - " group", - " of", - " animal", - " friends", - ",", - " from", - " wise", - " old ow", - "ls to playful", - " ot", - "ters.", - " Together,", - " they", - " shared stories", - ",", - " solved problems", - ",", - " and", - " looked", - " out", - " for", - " one", - " another", - ".\n\n", - "One", - " day", - ", the village faced", - " an unexpected", - " threat", - ":", - " a", - " severe", - " drought", - " that", - " threatened", - " their", - " crops", - " and", - " water supply", - ".", - " The", - " villagers", - " grew", - " anxious", - ",", - " unsure", - " of", - " how to", - " cope", - " with", - " the", - " impending", - " scarcity", - ".", - " El", - "ara", - ",", - " determined", - " to", - " help", - ",", - " turned", - " to her", - " animal friends", - " for", - " guidance", - ".\n\nThe", - " animals", - " led", - " El", - "ara", - " to", - " a", - " hidden", - " spring", - " deep", - " within", - " the forest,", - " a source", - " of", - " fresh", - " water unknown", - " to the", - " villagers", - ".", - " With", - " Ruf", - "us's", - " clever planning", - " and the", - " ot", - "ters", - "'", - " help", - " in directing", - " the", - " flow", - ",", - " they", - " managed", - " to", - " channel the", - " spring", - " water", - " to", - " the", - " village,", - " saving the", - " crops", - " and", - " quenching", - " the", - " villagers", - "'", - " thirst", - ".\n\n", - "Gr", - "ateful and", - " amazed,", - " the", - " villagers", - " hailed El", - "ara as", - " a", - " hero", - ".", - " They", - " came", - " to", - " understand the", - " importance", - " of living", - " harmon", - "iously", - " with", - " nature", - " and", - " the", - " wonders", - " that", - " could", - " be", - " achieved", - " through kindness", - " and cooperation", - ".\n\nFrom", - " that day", - " on", - ",", - " Greenh", - "ollow", - " thr", - "ived", - " as", - " a", - " community", - " where", - " humans", - " and", - " animals", - " lived together", - " in", - " harmony", - ",", - " cher", - "ishing", - " the", - " bonds that", - " El", - "ara", - " had", - " helped", - " forge", - ".", - " And whenever", - " challenges arose", - ", the", - " villagers", - " knew", - " they", - " could", - " rely on", - " El", - "ara and", - " her", - " extraordinary", - " friends", - " to", - " guide them", - " through", - ",", - " ensuring", - " that", - " the", - " spirit", - " of", - " unity", - " and", - " compassion", - " always prevailed.", -]; diff --git a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart b/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart deleted file mode 100644 index d2432557eb..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/document/turn_into/turn_into_test.dart +++ /dev/null @@ -1,914 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart' - hide quoteNode, QuoteBlockKeys; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('turn into:', () { - Document createDocument(List nodes) { - final document = Document.blank(); - document.insert([0], nodes); - return document; - } - - Future checkTurnInto( - Document document, - String originalType, - String originalText, { - Selection? selection, - String? toType, - int? level, - void Function(EditorState editorState, Node node)? afterTurnInto, - }) async { - final editorState = EditorState(document: document); - final types = toType == null - ? EditorOptionActionType.turnInto.supportTypes - : [toType]; - for (final type in types) { - if (type == originalType || type == SubPageBlockKeys.type) { - continue; - } - - editorState.selectionType = SelectionType.block; - editorState.selection = - selection ?? Selection.collapsed(Position(path: [0])); - - final node = editorState.getNodeAtPath([0])!; - expect(node.type, originalType); - final result = await BlockActionOptionCubit.turnIntoBlock( - type, - node, - editorState, - level: level, - ); - expect(result, true); - final newNode = editorState.getNodeAtPath([0])!; - expect(newNode.type, type); - expect(newNode.delta!.toPlainText(), originalText); - afterTurnInto?.call( - editorState, - newNode, - ); - - // turn it back the originalType for the next test - editorState.selectionType = SelectionType.block; - editorState.selection = selection ?? - Selection.collapsed( - Position(path: [0]), - ); - await BlockActionOptionCubit.turnIntoBlock( - originalType, - newNode, - editorState, - ); - expect(result, true); - } - } - - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('from heading to another blocks', () async { - const text = 'Heading 1'; - final document = createDocument([headingNode(level: 1, text: text)]); - await checkTurnInto(document, HeadingBlockKeys.type, text); - }); - - test('from paragraph to another blocks', () async { - const text = 'Paragraph'; - final document = createDocument([paragraphNode(text: text)]); - await checkTurnInto( - document, - ParagraphBlockKeys.type, - text, - ); - }); - - test('from quote list to another blocks', () async { - const text = 'Quote'; - final document = createDocument([ - quoteNode( - delta: Delta()..insert(text), - ), - ]); - await checkTurnInto( - document, - QuoteBlockKeys.type, - text, - ); - }); - - test('from todo list to another blocks', () async { - const text = 'Todo'; - final document = createDocument([ - todoListNode( - checked: false, - text: text, - ), - ]); - await checkTurnInto( - document, - TodoListBlockKeys.type, - text, - ); - }); - - test('from bulleted list to another blocks', () async { - const text = 'bulleted list'; - final document = createDocument([ - bulletedListNode( - text: text, - ), - ]); - await checkTurnInto( - document, - BulletedListBlockKeys.type, - text, - ); - }); - - test('from numbered list to another blocks', () async { - const text = 'numbered list'; - final document = createDocument([ - numberedListNode( - delta: Delta()..insert(text), - ), - ]); - await checkTurnInto( - document, - NumberedListBlockKeys.type, - text, - ); - }); - - test('from callout to another blocks', () async { - const text = 'callout'; - final document = createDocument([ - calloutNode( - delta: Delta()..insert(text), - ), - ]); - await checkTurnInto( - document, - CalloutBlockKeys.type, - text, - ); - }); - - for (final type in [ - HeadingBlockKeys.type, - ]) { - test('from nested bulleted list to $type', () async { - const text = 'bulleted list'; - const nestedText1 = 'nested bulleted list 1'; - const nestedText2 = 'nested bulleted list 2'; - const nestedText3 = 'nested bulleted list 3'; - final document = createDocument([ - bulletedListNode( - text: text, - children: [ - bulletedListNode( - text: nestedText1, - ), - bulletedListNode( - text: nestedText2, - ), - bulletedListNode( - text: nestedText3, - ), - ], - ), - ]); - await checkTurnInto( - document, - BulletedListBlockKeys.type, - text, - toType: type, - afterTurnInto: (editorState, node) { - expect(node.type, type); - expect(node.children.length, 0); - expect(node.delta!.toPlainText(), text); - - expect(editorState.document.root.children.length, 4); - expect( - editorState.document.root.children[1].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[1].delta!.toPlainText(), - nestedText1, - ); - expect( - editorState.document.root.children[2].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[2].delta!.toPlainText(), - nestedText2, - ); - expect( - editorState.document.root.children[3].type, - BulletedListBlockKeys.type, - ); - expect( - editorState.document.root.children[3].delta!.toPlainText(), - nestedText3, - ); - }, - ); - }); - } - - for (final type in [ - HeadingBlockKeys.type, - ]) { - test('from nested numbered list to $type', () async { - const text = 'numbered list'; - const nestedText1 = 'nested numbered list 1'; - const nestedText2 = 'nested numbered list 2'; - const nestedText3 = 'nested numbered list 3'; - final document = createDocument([ - numberedListNode( - delta: Delta()..insert(text), - children: [ - numberedListNode( - delta: Delta()..insert(nestedText1), - ), - numberedListNode( - delta: Delta()..insert(nestedText2), - ), - numberedListNode( - delta: Delta()..insert(nestedText3), - ), - ], - ), - ]); - await checkTurnInto( - document, - NumberedListBlockKeys.type, - text, - toType: type, - afterTurnInto: (editorState, node) { - expect(node.type, type); - expect(node.children.length, 0); - expect(node.delta!.toPlainText(), text); - - expect(editorState.document.root.children.length, 4); - expect( - editorState.document.root.children[1].type, - NumberedListBlockKeys.type, - ); - expect( - editorState.document.root.children[1].delta!.toPlainText(), - nestedText1, - ); - expect( - editorState.document.root.children[2].type, - NumberedListBlockKeys.type, - ); - expect( - editorState.document.root.children[2].delta!.toPlainText(), - nestedText2, - ); - expect( - editorState.document.root.children[3].type, - NumberedListBlockKeys.type, - ); - expect( - editorState.document.root.children[3].delta!.toPlainText(), - nestedText3, - ); - }, - ); - }); - } - - for (final type in [ - HeadingBlockKeys.type, - ]) { - // numbered list, bulleted list, todo list - // before - // - numbered list 1 - // - nested list 1 - // - bulleted list 2 - // - nested list 2 - // - todo list 3 - // - nested list 3 - // after - // - heading 1 - // - nested list 1 - // - heading 2 - // - nested list 2 - // - heading 3 - // - nested list 3 - test('from nested mixed list to $type', () async { - const text1 = 'numbered list 1'; - const text2 = 'bulleted list 2'; - const text3 = 'todo list 3'; - const nestedText1 = 'nested list 1'; - const nestedText2 = 'nested list 2'; - const nestedText3 = 'nested list 3'; - final document = createDocument([ - numberedListNode( - delta: Delta()..insert(text1), - children: [ - numberedListNode( - delta: Delta()..insert(nestedText1), - ), - ], - ), - bulletedListNode( - delta: Delta()..insert(text2), - children: [ - bulletedListNode( - delta: Delta()..insert(nestedText2), - ), - ], - ), - todoListNode( - checked: false, - text: text3, - children: [ - todoListNode( - checked: false, - text: nestedText3, - ), - ], - ), - ]); - await checkTurnInto( - document, - NumberedListBlockKeys.type, - text1, - toType: type, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2]), - ), - afterTurnInto: (editorState, node) { - final nodes = editorState.document.root.children; - expect(nodes.length, 6); - final texts = [ - text1, - nestedText1, - text2, - nestedText2, - text3, - nestedText3, - ]; - final types = [ - type, - NumberedListBlockKeys.type, - type, - BulletedListBlockKeys.type, - type, - TodoListBlockKeys.type, - ]; - for (var i = 0; i < 6; i++) { - expect(nodes[i].type, types[i]); - expect(nodes[i].children.length, 0); - expect(nodes[i].delta!.toPlainText(), texts[i]); - } - }, - ); - }); - } - - for (final type in [ - ParagraphBlockKeys.type, - BulletedListBlockKeys.type, - NumberedListBlockKeys.type, - TodoListBlockKeys.type, - QuoteBlockKeys.type, - CalloutBlockKeys.type, - ]) { - // numbered list, bulleted list, todo list - // before - // - numbered list 1 - // - nested list 1 - // - bulleted list 2 - // - nested list 2 - // - todo list 3 - // - nested list 3 - // after - // - new_list_type - // - nested list 1 - // - new_list_type - // - nested list 2 - // - new_list_type - // - nested list 3 - test('from nested mixed list to $type', () async { - const text1 = 'numbered list 1'; - const text2 = 'bulleted list 2'; - const text3 = 'todo list 3'; - const nestedText1 = 'nested list 1'; - const nestedText2 = 'nested list 2'; - const nestedText3 = 'nested list 3'; - final document = createDocument([ - numberedListNode( - delta: Delta()..insert(text1), - children: [ - numberedListNode( - delta: Delta()..insert(nestedText1), - ), - ], - ), - bulletedListNode( - delta: Delta()..insert(text2), - children: [ - bulletedListNode( - delta: Delta()..insert(nestedText2), - ), - ], - ), - todoListNode( - checked: false, - text: text3, - children: [ - todoListNode( - checked: false, - text: nestedText3, - ), - ], - ), - ]); - await checkTurnInto( - document, - NumberedListBlockKeys.type, - text1, - toType: type, - selection: Selection( - start: Position(path: [0]), - end: Position(path: [2]), - ), - afterTurnInto: (editorState, node) { - final nodes = editorState.document.root.children; - expect(nodes.length, 3); - final texts = [ - text1, - text2, - text3, - ]; - final nestedTexts = [ - nestedText1, - nestedText2, - nestedText3, - ]; - final types = [ - NumberedListBlockKeys.type, - BulletedListBlockKeys.type, - TodoListBlockKeys.type, - ]; - for (var i = 0; i < 3; i++) { - expect(nodes[i].type, type); - expect(nodes[i].children.length, 1); - expect(nodes[i].delta!.toPlainText(), texts[i]); - expect(nodes[i].children[0].type, types[i]); - expect(nodes[i].children[0].delta!.toPlainText(), nestedTexts[i]); - } - }, - ); - }); - } - - test('undo, redo', () async { - const text1 = 'numbered list 1'; - const nestedText1 = 'nested list 1'; - final document = createDocument([ - numberedListNode( - delta: Delta()..insert(text1), - children: [ - numberedListNode( - delta: Delta()..insert(nestedText1), - ), - ], - ), - ]); - await checkTurnInto( - document, - NumberedListBlockKeys.type, - text1, - toType: HeadingBlockKeys.type, - afterTurnInto: (editorState, node) { - expect(editorState.document.root.children.length, 2); - editorState.selection = Selection.collapsed( - Position(path: [0]), - ); - KeyEventResult result = undoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 1); - editorState.selection = Selection.collapsed( - Position(path: [0]), - ); - result = redoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 2); - }, - ); - }); - - test('calculate selection when turn into', () { - // Example: - // - bulleted list item 1 - // - bulleted list item 1-1 - // - bulleted list item 1-2 - // - bulleted list item 2 - // - bulleted list item 2-1 - // - bulleted list item 2-2 - // - bulleted list item 3 - // - bulleted list item 3-1 - // - bulleted list item 3-2 - const text = 'bulleted list'; - const nestedText = 'nested bulleted list'; - final document = createDocument([ - bulletedListNode( - text: '$text 1', - children: [ - bulletedListNode(text: '$nestedText 1-1'), - bulletedListNode(text: '$nestedText 1-2'), - ], - ), - bulletedListNode( - text: '$text 2', - children: [ - bulletedListNode(text: '$nestedText 2-1'), - bulletedListNode(text: '$nestedText 2-2'), - ], - ), - bulletedListNode( - text: '$text 3', - children: [ - bulletedListNode(text: '$nestedText 3-1'), - bulletedListNode(text: '$nestedText 3-2'), - ], - ), - ]); - final editorState = EditorState(document: document); - final cubit = BlockActionOptionCubit( - editorState: editorState, - blockComponentBuilder: {}, - ); - - // case 1: collapsed selection and the selection is in the top level - // and tap the turn into button at the [0] - final selection1 = Selection.collapsed( - Position(path: [0], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0])!, - selection1, - ), - selection1, - ); - - // case 2: collapsed selection and the selection is in the nested level - // and tap the turn into button at the [0] - final selection2 = Selection.collapsed( - Position(path: [0, 0], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0])!, - selection2, - ), - Selection.collapsed(Position(path: [0])), - ); - - // case 3, collapsed selection and the selection is in the nested level - // and tap the turn into button at the [0, 0] - final selection3 = Selection.collapsed( - Position(path: [0, 0], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0, 0])!, - selection3, - ), - selection3, - ); - - // case 4, not collapsed selection and the selection is in the top level - // and tap the turn into button at the [0] - final selection4 = Selection( - start: Position(path: [0], offset: 1), - end: Position(path: [1], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0])!, - selection4, - ), - selection4, - ); - - // case 5, not collapsed selection and the selection is in the nested level - // and tap the turn into button at the [0] - final selection5 = Selection( - start: Position(path: [0, 0], offset: 1), - end: Position(path: [0, 1], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0])!, - selection5, - ), - Selection.collapsed(Position(path: [0])), - ); - - // case 6, not collapsed selection and the selection is in the nested level - // and tap the turn into button at the [0, 0] - final selection6 = Selection( - start: Position(path: [0, 0], offset: 1), - end: Position(path: [0, 1], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([0])!, - selection6, - ), - Selection.collapsed(Position(path: [0])), - ); - - // case 7, multiple blocks selection, and tap the turn into button of one of the selected nodes - final selection7 = Selection( - start: Position(path: [0], offset: 1), - end: Position(path: [2], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([1])!, - selection7, - ), - selection7, - ); - - // case 8, multiple blocks selection, and tap the turn into button of one of the non-selected nodes - final selection8 = Selection( - start: Position(path: [0], offset: 1), - end: Position(path: [1], offset: 1), - ); - expect( - cubit.calculateTurnIntoSelection( - editorState.getNodeAtPath([2])!, - selection8, - ), - Selection.collapsed(Position(path: [2])), - ); - }); - - group('turn into toggle list', () { - const heading1 = 'heading 1'; - const heading2 = 'heading 2'; - const heading3 = 'heading 3'; - const paragraph1 = 'paragraph 1'; - const paragraph2 = 'paragraph 2'; - const paragraph3 = 'paragraph 3'; - - test('turn heading 1 block to toggle heading 1 block', () async { - // before - // # Heading 1 - // paragraph 1 - // paragraph 2 - // paragraph 3 - - // after - // > # Heading 1 - // paragraph 1 - // paragraph 2 - // paragraph 3 - final document = createDocument([ - headingNode(level: 1, text: heading1), - paragraphNode(text: paragraph1), - paragraphNode(text: paragraph2), - paragraphNode(text: paragraph3), - ]); - - await checkTurnInto( - document, - HeadingBlockKeys.type, - heading1, - selection: Selection.collapsed(Position(path: [0])), - toType: ToggleListBlockKeys.type, - level: 1, - afterTurnInto: (editorState, node) { - expect(editorState.document.root.children.length, 1); - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 1); - expect(node.children.length, 3); - for (var i = 0; i < 3; i++) { - expect(node.children[i].type, ParagraphBlockKeys.type); - expect( - node.children[i].delta!.toPlainText(), - [paragraph1, paragraph2, paragraph3][i], - ); - } - - // test undo together - final result = undoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 4); - }, - ); - }); - - test('turn toggle heading 1 block to heading 1 block', () async { - // before - // > # Heading 1 - // paragraph 1 - // paragraph 2 - // paragraph 3 - - // after - // # Heading 1 - // paragraph 1 - // paragraph 2 - // paragraph 3 - final document = createDocument([ - toggleHeadingNode( - text: heading1, - children: [ - paragraphNode(text: paragraph1), - paragraphNode(text: paragraph2), - paragraphNode(text: paragraph3), - ], - ), - ]); - - await checkTurnInto( - document, - ToggleListBlockKeys.type, - heading1, - selection: Selection.collapsed( - Position(path: [0]), - ), - toType: HeadingBlockKeys.type, - level: 1, - afterTurnInto: (editorState, node) { - expect(editorState.document.root.children.length, 4); - expect(node.type, HeadingBlockKeys.type); - expect(node.attributes[HeadingBlockKeys.level], 1); - expect(node.children.length, 0); - for (var i = 1; i <= 3; i++) { - final node = editorState.getNodeAtPath([i])!; - expect(node.type, ParagraphBlockKeys.type); - expect( - node.delta!.toPlainText(), - [paragraph1, paragraph2, paragraph3][i - 1], - ); - } - - // test undo together - editorState.selection = Selection.collapsed( - Position(path: [0]), - ); - final result = undoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 1); - final afterNode = editorState.getNodeAtPath([0])!; - expect(afterNode.type, ToggleListBlockKeys.type); - expect(afterNode.attributes[ToggleListBlockKeys.level], 1); - expect(afterNode.children.length, 3); - }, - ); - }); - - test('turn heading 2 block to toggle heading 2 block - case 1', () async { - // before - // ## Heading 2 - // paragraph 1 - // ### Heading 3 - // paragraph 2 - // # Heading 1 <- the heading 1 block will not be converted - // paragraph 3 - - // after - // > ## Heading 2 - // paragraph 1 - // ## Heading 2 - // paragraph 2 - // # Heading 1 - // paragraph 3 - final document = createDocument([ - headingNode(level: 2, text: heading2), - paragraphNode(text: paragraph1), - headingNode(level: 3, text: heading3), - paragraphNode(text: paragraph2), - headingNode(level: 1, text: heading1), - paragraphNode(text: paragraph3), - ]); - - await checkTurnInto( - document, - HeadingBlockKeys.type, - heading2, - selection: Selection.collapsed( - Position(path: [0]), - ), - toType: ToggleListBlockKeys.type, - level: 2, - afterTurnInto: (editorState, node) { - expect(editorState.document.root.children.length, 3); - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 2); - expect(node.children.length, 3); - expect(node.children[0].delta!.toPlainText(), paragraph1); - expect(node.children[1].delta!.toPlainText(), heading3); - expect(node.children[2].delta!.toPlainText(), paragraph2); - - // the heading 1 block will not be converted - final heading1Node = editorState.getNodeAtPath([1])!; - expect(heading1Node.type, HeadingBlockKeys.type); - expect(heading1Node.attributes[HeadingBlockKeys.level], 1); - expect(heading1Node.delta!.toPlainText(), heading1); - - final paragraph3Node = editorState.getNodeAtPath([2])!; - expect(paragraph3Node.type, ParagraphBlockKeys.type); - expect(paragraph3Node.delta!.toPlainText(), paragraph3); - - // test undo together - final result = undoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 6); - }, - ); - }); - - test('turn heading 2 block to toggle heading 2 block - case 2', () async { - // before - // ## Heading 2 - // paragraph 1 - // ## Heading 2 <- the heading 2 block will not be converted - // paragraph 2 - // # Heading 1 <- the heading 1 block will not be converted - // paragraph 3 - - // after - // > ## Heading 2 - // paragraph 1 - // ## Heading 2 - // paragraph 2 - // # Heading 1 - // paragraph 3 - final document = createDocument([ - headingNode(level: 2, text: heading2), - paragraphNode(text: paragraph1), - headingNode(level: 2, text: heading2), - paragraphNode(text: paragraph2), - headingNode(level: 1, text: heading1), - paragraphNode(text: paragraph3), - ]); - - await checkTurnInto( - document, - HeadingBlockKeys.type, - heading2, - selection: Selection.collapsed( - Position(path: [0]), - ), - toType: ToggleListBlockKeys.type, - level: 2, - afterTurnInto: (editorState, node) { - expect(editorState.document.root.children.length, 5); - expect(node.type, ToggleListBlockKeys.type); - expect(node.attributes[ToggleListBlockKeys.level], 2); - expect(node.children.length, 1); - expect(node.children[0].delta!.toPlainText(), paragraph1); - - final heading2Node = editorState.getNodeAtPath([1])!; - expect(heading2Node.type, HeadingBlockKeys.type); - expect(heading2Node.attributes[HeadingBlockKeys.level], 2); - expect(heading2Node.delta!.toPlainText(), heading2); - - final paragraph2Node = editorState.getNodeAtPath([2])!; - expect(paragraph2Node.type, ParagraphBlockKeys.type); - expect(paragraph2Node.delta!.toPlainText(), paragraph2); - - // the heading 1 block will not be converted - final heading1Node = editorState.getNodeAtPath([3])!; - expect(heading1Node.type, HeadingBlockKeys.type); - expect(heading1Node.attributes[HeadingBlockKeys.level], 1); - expect(heading1Node.delta!.toPlainText(), heading1); - - final paragraph3Node = editorState.getNodeAtPath([4])!; - expect(paragraph3Node.type, ParagraphBlockKeys.type); - expect(paragraph3Node.delta!.toPlainText(), paragraph3); - - // test undo together - final result = undoCommand.execute(editorState); - expect(result, KeyEventResult.handled); - expect(editorState.document.root.children.length, 6); - }, - ); - }); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/editor/editor_drop_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/editor_drop_test.dart deleted file mode 100644 index b17588674c..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/editor/editor_drop_test.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_file.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:cross_file/cross_file.dart'; -import 'package:flutter_test/flutter_test.dart'; -import '../../util.dart'; - -void main() { - setUpAll(() async { - await AppFlowyUnitTest.ensureInitialized(); - }); - - group('drop images and files in EditorState', () { - test('dropImages on same path as paragraph node ', () async { - final editorState = EditorState( - document: Document.blank(withInitialText: true), - ); - - expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type); - - const dropPath = [0]; - - const imagePath = 'assets/test/images/sample.jpeg'; - final imageFile = XFile(imagePath); - await editorState.dropImages(dropPath, [imageFile], 'documentId', true); - - final node = editorState.getNodeAtPath(dropPath); - expect(node, isNotNull); - expect(node!.type, CustomImageBlockKeys.type); - expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type); - }); - - test('dropImages should insert image node on empty path', () async { - final editorState = EditorState( - document: Document.blank(withInitialText: true), - ); - - expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type); - - const dropPath = [1]; - - const imagePath = 'assets/test/images/sample.jpeg'; - final imageFile = XFile(imagePath); - await editorState.dropImages(dropPath, [imageFile], 'documentId', true); - - final node = editorState.getNodeAtPath(dropPath); - expect(node, isNotNull); - expect(node!.type, CustomImageBlockKeys.type); - expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type); - expect(editorState.getNodeAtPath([2]), null); - }); - - test('dropFiles on same path as paragraph node ', () async { - final editorState = EditorState( - document: Document.blank(withInitialText: true), - ); - - expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type); - - const dropPath = [0]; - - const filePath = 'assets/test/images/sample.jpeg'; - final file = XFile(filePath); - await editorState.dropFiles(dropPath, [file], 'documentId', true); - - final node = editorState.getNodeAtPath(dropPath); - expect(node, isNotNull); - expect(node!.type, FileBlockKeys.type); - expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type); - }); - - test('dropFiles should insert file node on empty path', () async { - final editorState = EditorState( - document: Document.blank(withInitialText: true), - ); - const dropPath = [1]; - - const filePath = 'assets/test/images/sample.jpeg'; - final file = XFile(filePath); - await editorState.dropFiles(dropPath, [file], 'documentId', true); - - final node = editorState.getNodeAtPath(dropPath); - expect(node, isNotNull); - expect(node!.type, FileBlockKeys.type); - expect(editorState.getNodeAtPath([0])!.type, ParagraphBlockKeys.type); - expect(editorState.getNodeAtPath([2]), null); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart index 9e60c13ed7..93e27bc45b 100644 --- a/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/editor/transaction_adapter_test.dart @@ -1,13 +1,10 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/document/application/document_service.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('TransactionAdapter:', () { + group('TransactionAdapter', () { test('toBlockAction insert node with children operation', () { final editorState = EditorState.blank(); @@ -151,246 +148,5 @@ void main() { reason: '1 - prev id', ); }); - - test('update the external id and external type', () async { - // create a node without external id and external type - // the editing this node, the adapter should generate a new action - // to assign a new external id and external type. - final node = bulletedListNode(text: 'Hello'); - final document = Document( - root: pageNode( - children: [ - node, - ], - ), - ); - - final transactionAdapter = TransactionAdapter( - documentId: '', - documentService: DocumentService(), - ); - - final editorState = EditorState( - document: document, - ); - - final completer = Completer(); - editorState.transactionStream.listen((event) { - final time = event.$1; - if (time == TransactionTime.before) { - final actions = transactionAdapter.transactionToBlockActions( - event.$2, - editorState, - ); - final textActions = - transactionAdapter.filterTextDeltaActions(actions); - final blockActions = transactionAdapter.filterBlockActions(actions); - expect(textActions.length, 1); - expect(blockActions.length, 1); - - // check text operation - final textAction = textActions.first; - final textId = textAction.textDeltaPayloadPB?.textId; - { - expect(textAction.textDeltaType, TextDeltaType.create); - - expect(textId, isNotEmpty); - final delta = textAction.textDeltaPayloadPB?.delta; - expect(delta, equals('[{"insert":"HelloWorld"}]')); - } - - // check block operation - { - final blockAction = blockActions.first; - expect(blockAction.action, BlockActionTypePB.Update); - expect(blockAction.payload.block.id, node.id); - expect( - blockAction.payload.block.externalId, - textId, - ); - expect(blockAction.payload.block.externalType, kExternalTextType); - } - } else if (time == TransactionTime.after) { - completer.complete(); - } - }); - - await editorState.insertText( - 5, - 'World', - node: node, - ); - await completer.future; - }); - - test('use delta from prev attributes if current delta is null', () async { - final node = todoListNode( - checked: false, - delta: Delta()..insert('AppFlowy'), - ); - final document = Document( - root: pageNode( - children: [ - node, - ], - ), - ); - final transactionAdapter = TransactionAdapter( - documentId: '', - documentService: DocumentService(), - ); - - final editorState = EditorState( - document: document, - ); - - final completer = Completer(); - editorState.transactionStream.listen((event) { - final time = event.$1; - if (time == TransactionTime.before) { - final actions = transactionAdapter.transactionToBlockActions( - event.$2, - editorState, - ); - final textActions = - transactionAdapter.filterTextDeltaActions(actions); - final blockActions = transactionAdapter.filterBlockActions(actions); - expect(textActions.length, 1); - expect(blockActions.length, 1); - - // check text operation - final textAction = textActions.first; - final textId = textAction.textDeltaPayloadPB?.textId; - { - expect(textAction.textDeltaType, TextDeltaType.create); - - expect(textId, isNotEmpty); - final delta = textAction.textDeltaPayloadPB?.delta; - expect(delta, equals('[{"insert":"AppFlowy"}]')); - } - - // check block operation - { - final blockAction = blockActions.first; - expect(blockAction.action, BlockActionTypePB.Update); - expect(blockAction.payload.block.id, node.id); - expect( - blockAction.payload.block.externalId, - textId, - ); - expect(blockAction.payload.block.externalType, kExternalTextType); - } - } else if (time == TransactionTime.after) { - completer.complete(); - } - }); - - final transaction = editorState.transaction; - transaction.updateNode(node, {TodoListBlockKeys.checked: true}); - await editorState.apply(transaction); - await completer.future; - }); - - test('text retain with attributes that are false', () async { - final node = paragraphNode( - delta: Delta() - ..insert( - 'Hello AppFlowy', - attributes: { - 'bold': true, - }, - ), - ); - final document = Document( - root: pageNode( - children: [ - node, - ], - ), - ); - final transactionAdapter = TransactionAdapter( - documentId: '', - documentService: DocumentService(), - ); - - final editorState = EditorState( - document: document, - ); - - int counter = 0; - final completer = Completer(); - editorState.transactionStream.listen((event) { - final time = event.$1; - if (time == TransactionTime.before) { - final actions = transactionAdapter.transactionToBlockActions( - event.$2, - editorState, - ); - final textActions = - transactionAdapter.filterTextDeltaActions(actions); - final blockActions = transactionAdapter.filterBlockActions(actions); - expect(textActions.length, 1); - expect(blockActions.length, 1); - if (counter == 1) { - // check text operation - final textAction = textActions.first; - final textId = textAction.textDeltaPayloadPB?.textId; - { - expect(textAction.textDeltaType, TextDeltaType.create); - - expect(textId, isNotEmpty); - final delta = textAction.textDeltaPayloadPB?.delta; - expect( - delta, - equals( - '[{"insert":"Hello","attributes":{"bold":null}},{"insert":" AppFlowy","attributes":{"bold":true}}]', - ), - ); - } - } else if (counter == 3) { - final textAction = textActions.first; - final textId = textAction.textDeltaPayloadPB?.textId; - { - expect(textAction.textDeltaType, TextDeltaType.update); - - expect(textId, isNotEmpty); - final delta = textAction.textDeltaPayloadPB?.delta; - expect( - delta, - equals( - '[{"retain":5,"attributes":{"bold":null}}]', - ), - ); - } - } - } else if (time == TransactionTime.after && counter == 3) { - completer.complete(); - } - }); - - counter = 1; - final insertTransaction = editorState.transaction; - insertTransaction.formatText(node, 0, 5, { - 'bold': false, - }); - - await editorState.apply(insertTransaction); - - counter = 2; - final updateTransaction = editorState.transaction; - updateTransaction.formatText(node, 0, 5, { - 'bold': true, - }); - await editorState.apply(updateTransaction); - - counter = 3; - final formatTransaction = editorState.transaction; - formatTransaction.formatText(node, 0, 5, { - 'bold': false, - }); - await editorState.apply(formatTransaction); - - await completer.future; - }); }); } diff --git a/frontend/appflowy_flutter/test/unit_test/image/appflowy_network_image_test.dart b/frontend/appflowy_flutter/test/unit_test/image/appflowy_network_image_test.dart deleted file mode 100644 index 3c075126db..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/image/appflowy_network_image_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('AppFlowy Network Image:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test( - 'retry count should be clear if the value exceeds max retries', - () async { - const maxRetries = 5; - const fakeUrl = 'https://plus.unsplash.com/premium_photo-1731948132439'; - final retryCounter = FlowyNetworkRetryCounter(); - final tag = retryCounter.add(fakeUrl); - for (var i = 0; i < maxRetries; i++) { - retryCounter.increment(fakeUrl); - expect(retryCounter.getRetryCount(fakeUrl), i + 1); - } - retryCounter.clear( - tag: tag, - url: fakeUrl, - maxRetries: maxRetries, - ); - expect(retryCounter.getRetryCount(fakeUrl), 0); - }, - ); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart b/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart deleted file mode 100644 index 5b6f88801a..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/link_preview/link_preview_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/link_preview/link_parsers/default_parser.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() async { - test( - 'description', - () async { - final links = [ - 'https://www.baidu.com/', - 'https://appflowy.io/', - 'https://github.com/AppFlowy-IO/AppFlowy', - 'https://github.com/', - 'https://www.figma.com/design/3K0ai4FhDOJ3Lts8G3KOVP/Page?node-id=7282-4007&p=f&t=rpfvEvh9K9J9WkIo-0', - 'https://www.figma.com/files/drafts', - 'https://www.youtube.com/watch?v=LyY5Rh9qBvA', - 'https://www.youtube.com/', - 'https://www.youtube.com/watch?v=a6GDT7', - 'http://www.test.com/', - 'https://www.baidu.com/s?wd=test&rsv_spt=1&rsv_iqid=0xb6a7840b00e5324a&issp=1&f=8&rsv_bp=1&rsv_idx=2&ie=utf-8&tn=22073068_7_oem_dg&rsv_dl=tb&rsv_enter=1&rsv_sug3=5&rsv_sug1=4&rsv_sug7=100&rsv_sug2=0&rsv_btype=i&prefixsug=test&rsp=9&inputT=478&rsv_sug4=547', - 'https://www.google.com/', - 'https://www.google.com.hk/search?q=test&oq=test&gs_lcrp=EgZjaHJvbWUyCQgAEEUYORiABDIHCAEQABiABDIHCAIQABiABDIHCAMQABiABDIHCAQQABiABDIHCAUQABiABDIHCAYQABiABDIHCAcQABiABDIHCAgQLhiABDIHCAkQABiABNIBCTE4MDJqMGoxNagCCLACAfEFAQs7K9PprSfxBQELOyvT6a0n&sourceid=chrome&ie=UTF-8', - 'www.baidu.com', - 'baidu.com', - 'com', - 'https://www.baidu.com', - 'https://github.com/AppFlowy-IO/AppFlowy', - 'https://appflowy.com/app/c29fafc4-b7c0-4549-8702-71339b0fd9ea/59f36be8-9b2f-4d3e-b6a1-816c6c2043e5?blockId=GCY_T4', - ]; - - final parser = DefaultParser(); - int i = 1; - for (final link in links) { - final formatLink = LinkInfoParser.formatUrl(link); - final siteInfo = await parser - .parse(Uri.tryParse(formatLink) ?? Uri.parse(formatLink)); - if (siteInfo?.isEmpty() ?? true) { - debugPrint('$i : $formatLink ---- empty \n'); - } else { - debugPrint('$i : $formatLink ---- \n$siteInfo \n'); - } - i++; - } - }, - timeout: const Timeout(Duration(seconds: 120)), - ); -} diff --git a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart b/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart deleted file mode 100644 index 707cc23d4f..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/markdown/markdown_parser_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/image/common.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('export markdown to document', () { - test('file block', () async { - final document = Document.blank() - ..insert( - [0], - [ - fileNode( - name: 'file.txt', - url: 'https://file.com', - ), - ], - ); - final markdown = await customDocumentToMarkdown(document); - expect(markdown, '[file.txt](https://file.com)\n'); - }); - - test('link preview', () async { - final document = Document.blank() - ..insert( - [0], - [linkPreviewNode(url: 'https://www.link_preview.com')], - ); - final markdown = await customDocumentToMarkdown(document); - expect( - markdown, - '[https://www.link_preview.com](https://www.link_preview.com)\n', - ); - }); - - test('multiple images', () async { - const png1 = 'https://www.appflowy.png', - png2 = 'https://www.appflowy2.png'; - final document = Document.blank() - ..insert( - [0], - [ - multiImageNode( - images: [ - ImageBlockData( - url: png1, - type: CustomImageType.external, - ), - ImageBlockData( - url: png2, - type: CustomImageType.external, - ), - ], - ), - ], - ); - final markdown = await customDocumentToMarkdown(document); - expect( - markdown, - '![]($png1)\n![]($png2)', - ); - }); - - test('subpage block', () async { - const testSubpageId = 'testSubpageId'; - final subpageNode = pageMentionNode(testSubpageId); - final document = Document.blank() - ..insert( - [0], - [subpageNode], - ); - final markdown = await customDocumentToMarkdown(document); - expect( - markdown, - '[]($testSubpageId)\n', - ); - }); - - test('date or reminder', () async { - final dateTime = DateTime.now(); - final document = Document.blank() - ..insert( - [0], - [dateMentionNode()], - ); - final markdown = await customDocumentToMarkdown(document); - expect( - markdown, - '${DateFormat.yMMMd().format(dateTime)}\n', - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart b/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart deleted file mode 100644 index 646ce7fbac..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flowy_infra/colorscheme/colorscheme.dart'; -import 'package:flowy_infra/colorscheme/default_colorscheme.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Theme missing keys', () { - test('no missing keys', () { - const colorScheme = DefaultColorScheme.light(); - final toJson = colorScheme.toJson(); - - expect(toJson.containsKey('surface'), true); - - final missingKeys = FlowyColorScheme.getMissingKeys(toJson); - expect(missingKeys.isEmpty, true); - }); - - test('missing surface and bg2', () { - const colorScheme = DefaultColorScheme.light(); - final toJson = colorScheme.toJson() - ..remove('surface') - ..remove('bg2'); - - expect(toJson.containsKey('surface'), false); - expect(toJson.containsKey('bg2'), false); - - final missingKeys = FlowyColorScheme.getMissingKeys(toJson); - expect(missingKeys.length, 2); - expect(missingKeys.contains('surface'), true); - expect(missingKeys.contains('bg2'), true); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart deleted file mode 100644 index 57b8319d06..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart +++ /dev/null @@ -1,216 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table content operation:', () { - void setupDependencyInjection() { - getIt.registerSingleton(ClipboardService()); - } - - setUpAll(() { - Log.shared.disableLog = true; - - setupDependencyInjection(); - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('clear content at row 1', () async { - const defaultContent = 'default content'; - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - defaultContent: defaultContent, - ); - await editorState.clearContentAtRowIndex( - tableNode: tableNode, - rowIndex: 0, - ); - for (var i = 0; i < tableNode.rowLength; i++) { - for (var j = 0; j < tableNode.columnLength; j++) { - expect( - tableNode - .getTableCellNode(rowIndex: i, columnIndex: j) - ?.children - .first - .delta - ?.toPlainText(), - i == 0 ? '' : defaultContent, - ); - } - } - }); - - test('clear content at row 3', () async { - const defaultContent = 'default content'; - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - defaultContent: defaultContent, - ); - await editorState.clearContentAtRowIndex( - tableNode: tableNode, - rowIndex: 2, - ); - for (var i = 0; i < tableNode.rowLength; i++) { - for (var j = 0; j < tableNode.columnLength; j++) { - expect( - tableNode - .getTableCellNode(rowIndex: i, columnIndex: j) - ?.children - .first - .delta - ?.toPlainText(), - i == 2 ? '' : defaultContent, - ); - } - } - }); - - test('clear content at column 1', () async { - const defaultContent = 'default content'; - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - defaultContent: defaultContent, - ); - await editorState.clearContentAtColumnIndex( - tableNode: tableNode, - columnIndex: 0, - ); - for (var i = 0; i < tableNode.rowLength; i++) { - for (var j = 0; j < tableNode.columnLength; j++) { - expect( - tableNode - .getTableCellNode(rowIndex: i, columnIndex: j) - ?.children - .first - .delta - ?.toPlainText(), - j == 0 ? '' : defaultContent, - ); - } - } - }); - - test('clear content at column 4', () async { - const defaultContent = 'default content'; - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - defaultContent: defaultContent, - ); - await editorState.clearContentAtColumnIndex( - tableNode: tableNode, - columnIndex: 3, - ); - for (var i = 0; i < tableNode.rowLength; i++) { - for (var j = 0; j < tableNode.columnLength; j++) { - expect( - tableNode - .getTableCellNode(rowIndex: i, columnIndex: j) - ?.children - .first - .delta - ?.toPlainText(), - j == 3 ? '' : defaultContent, - ); - } - } - }); - - test('copy row 1-2', () async { - const rowCount = 2; - const columnCount = 3; - - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: rowCount, - columnCount: columnCount, - contentBuilder: (rowIndex, columnIndex) => - 'row $rowIndex, column $columnIndex', - ); - - for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) { - final data = await editorState.copyRow( - tableNode: tableNode, - rowIndex: rowIndex, - ); - expect(data, isNotNull); - expect( - data?.plainText, - 'row $rowIndex, column 0\nrow $rowIndex, column 1\nrow $rowIndex, column 2', - ); - } - }); - - test('copy column 1-2', () async { - const rowCount = 2; - const columnCount = 3; - - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: rowCount, - columnCount: columnCount, - contentBuilder: (rowIndex, columnIndex) => - 'row $rowIndex, column $columnIndex', - ); - - for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) { - final data = await editorState.copyColumn( - tableNode: tableNode, - columnIndex: columnIndex, - ); - expect(data, isNotNull); - expect( - data?.plainText, - 'row 0, column $columnIndex\nrow 1, column $columnIndex', - ); - } - }); - - test('cut row 1-2', () async { - const rowCount = 2; - const columnCount = 3; - - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: rowCount, - columnCount: columnCount, - contentBuilder: (rowIndex, columnIndex) => - 'row $rowIndex, column $columnIndex', - ); - - for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) { - final data = await editorState.copyRow( - tableNode: tableNode, - rowIndex: rowIndex, - clearContent: true, - ); - expect(data, isNotNull); - expect( - data?.plainText, - 'row $rowIndex, column 0\nrow $rowIndex, column 1\nrow $rowIndex, column 2', - ); - } - - for (var rowIndex = 0; rowIndex < rowCount; rowIndex++) { - for (var columnIndex = 0; columnIndex < columnCount; columnIndex++) { - expect( - tableNode - .getTableCellNode(rowIndex: rowIndex, columnIndex: columnIndex) - ?.children - .first - .delta - ?.toPlainText(), - '', - ); - } - } - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart deleted file mode 100644 index cb28f955da..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart +++ /dev/null @@ -1,236 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table delete operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('delete 2 rows in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - await editorState.deleteRowInTable(tableNode, 0); - await editorState.deleteRowInTable(tableNode, 0); - expect(tableNode.rowLength, 1); - expect(tableNode.columnLength, 4); - }); - - test('delete 2 columns in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - await editorState.deleteColumnInTable(tableNode, 0); - await editorState.deleteColumnInTable(tableNode, 0); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 2); - }); - - test('delete a row and a column in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - await editorState.deleteColumnInTable(tableNode, 0); - await editorState.deleteRowInTable(tableNode, 0); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 3); - }); - - test('delete a row with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the row 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - await editorState.deleteRowInTable(tableNode, 1); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 4); - expect(tableCellNode.rowColors, {}); - expect(tableNode.rowAligns, {}); - }); - - test('delete a row with background and align (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the row 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - await editorState.deleteRowInTable(tableNode, 0); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 4); - expect(tableCellNode.rowColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.rowAligns, { - '0': TableAlign.center.key, - }); - }); - - test('delete a column with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.columnColors, { - '1': '0xFF0000FF', - }); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - await editorState.deleteColumnInTable(tableNode, 1); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 3); - expect(tableCellNode.columnColors, {}); - expect(tableNode.columnAligns, {}); - }); - - test('delete a column with background (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.columnColors, { - '1': '0xFF0000FF', - }); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - await editorState.deleteColumnInTable(tableNode, 0); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 3); - expect(tableCellNode.columnColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '0': TableAlign.center.key, - }); - }); - - test('delete a column with text color & bold style (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - }); - await editorState.deleteColumnInTable(tableNode, 0); - expect(tableNode.columnTextColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '0': true, - }); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 3); - }); - - test('delete a column with text color & bold style (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // delete the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - }); - await editorState.deleteColumnInTable(tableNode, 1); - expect(tableNode.columnTextColors, {}); - expect(tableNode.columnBoldAttributes, {}); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 3); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart deleted file mode 100644 index 85a1c252c7..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table delete operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('duplicate a row', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - await editorState.duplicateRowInTable(tableNode, 0); - expect(tableNode.rowLength, 4); - expect(tableNode.columnLength, 4); - }); - - test('duplicate a column', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - await editorState.duplicateColumnInTable(tableNode, 0); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 5); - }); - - test('duplicate a row with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the row 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - await editorState.duplicateRowInTable(tableNode, 1); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - '2': '0xFF0000FF', - }); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - '2': TableAlign.center.key, - }); - }); - - test('duplicate a row with background and align (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the row 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 1, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - await editorState.duplicateRowInTable(tableNode, 2); - expect(tableCellNode.rowColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - }); - - test('duplicate a column with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - await editorState.duplicateColumnInTable(tableNode, 1); - expect(tableCellNode.columnColors, { - '1': '0xFF0000FF', - '2': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - '2': TableAlign.center.key, - }); - }); - - test('duplicate a column with background and align (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - await editorState.duplicateColumnInTable(tableNode, 2); - expect(tableCellNode.columnColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - }); - - test('duplicate a column with text color & bold style (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - }); - await editorState.duplicateColumnInTable(tableNode, 1); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - '2': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - '2': true, - }); - }); - - test('duplicate a column with text color & bold style (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 4, - ); - // duplicate the column 1 - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 1); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - }); - await editorState.duplicateColumnInTable(tableNode, 0); - expect(tableNode.columnTextColors, { - '2': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '2': true, - }); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart deleted file mode 100644 index 1f0707cc0d..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table header operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('enable header column in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // default is not header column - expect(tableNode.isHeaderColumnEnabled, false); - await editorState.toggleEnableHeaderColumn( - tableNode: tableNode, - enable: true, - ); - expect(tableNode.isHeaderColumnEnabled, true); - await editorState.toggleEnableHeaderColumn( - tableNode: tableNode, - enable: false, - ); - expect(tableNode.isHeaderColumnEnabled, false); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 3); - }); - - test('enable header row in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // default is not header row - expect(tableNode.isHeaderRowEnabled, false); - await editorState.toggleEnableHeaderRow( - tableNode: tableNode, - enable: true, - ); - expect(tableNode.isHeaderRowEnabled, true); - await editorState.toggleEnableHeaderRow( - tableNode: tableNode, - enable: false, - ); - expect(tableNode.isHeaderRowEnabled, false); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 3); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart deleted file mode 100644 index 86c4236a03..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table insert operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('add 2 rows in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - await editorState.addRowInTable(tableNode); - await editorState.addRowInTable(tableNode); - expect(tableNode.rowLength, 4); - expect(tableNode.columnLength, 3); - }); - - test('add 2 columns in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - await editorState.addColumnInTable(tableNode); - await editorState.addColumnInTable(tableNode); - expect(tableNode.rowLength, 2); - expect(tableNode.columnLength, 5); - }); - - test('add 2 rows and 2 columns in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - await editorState.addColumnAndRowInTable(tableNode); - await editorState.addColumnAndRowInTable(tableNode); - expect(tableNode.rowLength, 4); - expect(tableNode.columnLength, 5); - }); - - test('insert a row at the first position in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - await editorState.insertRowInTable(tableNode, 0); - expect(tableNode.rowLength, 3); - expect(tableNode.columnLength, 3); - }); - - test('insert a column at the first position in table', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - await editorState.insertColumnInTable(tableNode, 0); - expect(tableNode.columnLength, 4); - expect(tableNode.rowLength, 2); - }); - - test('insert a row with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the row at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableNode.rowColors, { - '0': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '0': TableAlign.center.key, - }); - await editorState.insertRowInTable(tableNode, 0); - expect(tableNode.rowColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.rowAligns, { - '1': TableAlign.center.key, - }); - }); - - test('insert a row with background and align (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the row at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - expect(tableNode.rowColors, { - '0': '0xFF0000FF', - }); - await editorState.updateRowAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.rowAligns, { - '0': TableAlign.center.key, - }); - await editorState.insertRowInTable(tableNode, 1); - expect(tableNode.rowColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.rowAligns, { - '0': TableAlign.center.key, - }); - }); - - test('insert a column with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the column at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '0': TableAlign.center.key, - }); - await editorState.insertColumnInTable(tableNode, 0); - expect(tableNode.columnColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '1': TableAlign.center.key, - }); - }); - - test('insert a column with background and align (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the column at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode, - align: TableAlign.center, - ); - expect(tableNode.columnColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '0': TableAlign.center.key, - }); - await editorState.insertColumnInTable(tableNode, 1); - expect(tableNode.columnColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnAligns, { - '0': TableAlign.center.key, - }); - }); - - test('insert a column with text color & bold style (1)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the column at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '0': true, - }); - await editorState.insertColumnInTable(tableNode, 0); - expect(tableNode.columnTextColors, { - '1': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '1': true, - }); - }); - - test('insert a column with text color & bold style (2)', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // insert the column at the first position - final tableCellNode = - tableNode.getTableCellNode(rowIndex: 0, columnIndex: 0); - await editorState.updateColumnTextColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - await editorState.toggleColumnBoldAttribute( - tableCellNode: tableCellNode, - isBold: true, - ); - expect(tableNode.columnTextColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '0': true, - }); - await editorState.insertColumnInTable(tableNode, 1); - expect(tableNode.columnTextColors, { - '0': '0xFF0000FF', - }); - expect(tableNode.columnBoldAttributes, { - '0': true, - }); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart deleted file mode 100644 index 354e6bfa5e..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart +++ /dev/null @@ -1,177 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy/shared/markdown_to_document.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Simple table markdown:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('convert simple table to markdown (1)', () async { - final tableNode = createSimpleTableBlockNode( - columnCount: 7, - rowCount: 11, - contentBuilder: (rowIndex, columnIndex) => - _sampleContents[rowIndex][columnIndex], - ); - final markdown = const SimpleTableNodeParser().transform( - tableNode, - null, - ); - expect(markdown, - '''|Index|Customer Id|First Name|Last Name|Company|City|Country| -|---|---|---|---|---|---|---| -|1|DD37Cf93aecA6Dc|Sheryl|Baxter|Rasmussen Group|East Leonard|Chile| -|2|1Ef7b82A4CAAD10|Preston|Lozano|Vega-Gentry|East Jimmychester|Djibouti| -|3|6F94879bDAfE5a6|Roy|Berry|Murillo-Perry|Isabelborough|Antigua and Barbuda| -|4|5Cef8BFA16c5e3c|Linda|Olsen|Dominguez, Mcmillan and Donovan|Bensonview|Dominican Republic| -|5|053d585Ab6b3159|Joanna|Bender|Martin, Lang and Andrade|West Priscilla|Slovakia (Slovak Republic)| -|6|2d08FB17EE273F4|Aimee|Downs|Steele Group|Chavezborough|Bosnia and Herzegovina| -|7|EAd384DfDbBf77|Darren|Peck|Lester, Woodard and Mitchell|Lake Ana|Pitcairn Islands| -|8|0e04AFde9f225dE|Brett|Mullen|Sanford, Davenport and Giles|Kimport|Bulgaria| -|9|C2dE4dEEc489ae0|Sheryl|Meyers|Browning-Simon|Robersonstad|Cyprus| -|10|8C2811a503C7c5a|Michelle|Gallagher|Beck-Hendrix|Elaineberg|Timor-Leste| -'''); - }); - - test('convert markdown to simple table (1)', () async { - final document = customMarkdownToDocument(_sampleMarkdown1); - expect(document, isNotNull); - final tableNode = document.nodeAtPath([0])!; - expect(tableNode, isNotNull); - expect(tableNode.type, equals(SimpleTableBlockKeys.type)); - expect(tableNode.rowLength, equals(4)); - expect(tableNode.columnLength, equals(4)); - }); - - test('convert markdown to simple table (2)', () async { - final document = customMarkdownToDocument( - _sampleMarkdown1, - tableWidth: 200, - ); - expect(document, isNotNull); - final tableNode = document.nodeAtPath([0])!; - expect(tableNode, isNotNull); - expect(tableNode.type, equals(SimpleTableBlockKeys.type)); - expect(tableNode.columnWidths.length, 4); - for (final entry in tableNode.columnWidths.entries) { - expect(entry.value, equals(200)); - } - }); - }); -} - -const _sampleContents = >[ - [ - "Index", - "Customer Id", - "First Name", - "Last Name", - "Company", - "City", - "Country", - ], - [ - "1", - "DD37Cf93aecA6Dc", - "Sheryl", - "Baxter", - "Rasmussen Group", - "East Leonard", - "Chile", - ], - [ - "2", - "1Ef7b82A4CAAD10", - "Preston", - "Lozano", - "Vega-Gentry", - "East Jimmychester", - "Djibouti", - ], - [ - "3", - "6F94879bDAfE5a6", - "Roy", - "Berry", - "Murillo-Perry", - "Isabelborough", - "Antigua and Barbuda", - ], - [ - "4", - "5Cef8BFA16c5e3c", - "Linda", - "Olsen", - "Dominguez, Mcmillan and Donovan", - "Bensonview", - "Dominican Republic", - ], - [ - "5", - "053d585Ab6b3159", - "Joanna", - "Bender", - "Martin, Lang and Andrade", - "West Priscilla", - "Slovakia (Slovak Republic)", - ], - [ - "6", - "2d08FB17EE273F4", - "Aimee", - "Downs", - "Steele Group", - "Chavezborough", - "Bosnia and Herzegovina", - ], - [ - "7", - "EAd384DfDbBf77", - "Darren", - "Peck", - "Lester, Woodard and Mitchell", - "Lake Ana", - "Pitcairn Islands", - ], - [ - "8", - "0e04AFde9f225dE", - "Brett", - "Mullen", - "Sanford, Davenport and Giles", - "Kimport", - "Bulgaria", - ], - [ - "9", - "C2dE4dEEc489ae0", - "Sheryl", - "Meyers", - "Browning-Simon", - "Robersonstad", - "Cyprus", - ], - [ - "10", - "8C2811a503C7c5a", - "Michelle", - "Gallagher", - "Beck-Hendrix", - "Elaineberg", - "Timor-Leste", - ], -]; - -const _sampleMarkdown1 = '''|A|B|C|| -|---|---|---|---| -|D|E|F|| -|1|2|3|| -||||| -'''; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_reorder_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_reorder_operation_test.dart deleted file mode 100644 index 703a63b40d..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_reorder_operation_test.dart +++ /dev/null @@ -1,335 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table reorder operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - group('reorder column', () { - test('reorder column from index 1 to index 2', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 4, - columnCount: 3, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 2); - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 4); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1), - 'cell 0-2', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2), - 'cell 0-1', - ); - }); - - test('reorder column from index 2 to index 0', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 4, - columnCount: 3, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0); - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 4); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 0-2', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2), - 'cell 0-1', - ); - }); - - test('reorder column with same index', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 4, - columnCount: 3, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 1); - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 4); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1), - 'cell 0-1', - ); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2), - 'cell 0-2', - ); - }); - - test( - 'reorder column from index 0 to index 2 with align/color/width attributes (1)', - () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 4, - columnCount: 3, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - - // before reorder - // Column 0: align: right, color: 0xFF0000, width: 100 - // Column 1: align: center, color: 0x00FF00, width: 150 - // Column 2: align: left, color: 0x0000FF, width: 200 - await updateTableColumnAttributes( - editorState, - tableNode, - columnIndex: 0, - align: TableAlign.right, - color: '#FF0000', - width: 100, - ); - await updateTableColumnAttributes( - editorState, - tableNode, - columnIndex: 1, - align: TableAlign.center, - color: '#00FF00', - width: 150, - ); - await updateTableColumnAttributes( - editorState, - tableNode, - columnIndex: 2, - align: TableAlign.left, - color: '#0000FF', - width: 200, - ); - - // after reorder - // Column 0: align: center, color: 0x00FF00, width: 150 - // Column 1: align: left, color: 0x0000FF, width: 200 - // Column 2: align: right, color: 0xFF0000, width: 100 - await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2); - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 4); - - expect(tableNode.columnAligns, { - "0": TableAlign.center.key, - "1": TableAlign.left.key, - "2": TableAlign.right.key, - }); - expect(tableNode.columnColors, { - "0": '#00FF00', - "1": '#0000FF', - "2": '#FF0000', - }); - expect(tableNode.columnWidths, { - "0": 150, - "1": 200, - "2": 100, - }); - }); - - test( - 'reorder column from index 0 to index 2 and reorder it back to index 0', - () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - - // before reorder - // Column 0: null - // Column 1: align: center, color: 0x0000FF, width: 200 - // Column 2: align: right, color: 0x0000FF, width: 250 - await updateTableColumnAttributes( - editorState, - tableNode, - columnIndex: 1, - align: TableAlign.center, - color: '#FF0000', - width: 200, - ); - await updateTableColumnAttributes( - editorState, - tableNode, - columnIndex: 2, - align: TableAlign.right, - color: '#0000FF', - width: 250, - ); - - // move column from index 0 to index 2 - await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2); - // move column from index 2 to index 0 - await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0); - expect(tableNode.columnLength, 3); - expect(tableNode.rowLength, 2); - - expect(tableNode.columnAligns, { - "1": TableAlign.center.key, - "2": TableAlign.right.key, - }); - expect(tableNode.columnColors, { - "1": '#FF0000', - "2": '#0000FF', - }); - expect(tableNode.columnWidths, { - "1": 200, - "2": 250, - }); - }); - }); - - group('reorder row', () { - test('reorder row from index 1 to index 2', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 2, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 2); - expect(tableNode.columnLength, 2); - expect(tableNode.rowLength, 3); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0), - 'cell 2-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0), - 'cell 1-0', - ); - }); - - test('reorder row from index 2 to index 0', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 2, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderRow(tableNode, fromIndex: 2, toIndex: 0); - expect(tableNode.columnLength, 2); - expect(tableNode.rowLength, 3); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 2-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0), - 'cell 1-0', - ); - }); - - test('reorder row with same', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 2, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 1); - expect(tableNode.columnLength, 2); - expect(tableNode.rowLength, 3); - expect( - tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0), - 'cell 0-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0), - 'cell 1-0', - ); - expect( - tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0), - 'cell 2-0', - ); - }); - - test('reorder row from index 0 to index 2 with align/color attributes', - () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 3, - columnCount: 2, - contentBuilder: (rowIndex, columnIndex) => - 'cell $rowIndex-$columnIndex', - ); - - // before reorder - // Row 0: align: right, color: 0xFF0000 - // Row 1: align: center, color: 0x00FF00 - // Row 2: align: left, color: 0x0000FF - await updateTableRowAttributes( - editorState, - tableNode, - rowIndex: 0, - align: TableAlign.right, - color: '#FF0000', - ); - await updateTableRowAttributes( - editorState, - tableNode, - rowIndex: 1, - align: TableAlign.center, - color: '#00FF00', - ); - await updateTableRowAttributes( - editorState, - tableNode, - rowIndex: 2, - align: TableAlign.left, - color: '#0000FF', - ); - - // after reorder - // Row 0: align: center, color: 0x00FF00 - // Row 1: align: left, color: 0x0000FF - // Row 2: align: right, color: 0xFF0000 - await editorState.reorderRow(tableNode, fromIndex: 0, toIndex: 2); - expect(tableNode.columnLength, 2); - expect(tableNode.rowLength, 3); - expect(tableNode.rowAligns, { - "0": TableAlign.center.key, - "1": TableAlign.left.key, - "2": TableAlign.right.key, - }); - expect(tableNode.rowColors, { - "0": '#00FF00', - "1": '#0000FF', - "2": '#FF0000', - }); - }); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart deleted file mode 100644 index dd127d3d0b..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'simple_table_test_helper.dart'; - -void main() { - group('Simple table style operation:', () { - setUpAll(() { - Log.shared.disableLog = true; - }); - - tearDownAll(() { - Log.shared.disableLog = false; - }); - - test('update column width in memory', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - // check the default column width - expect(tableNode.columnWidths, isEmpty); - final tableCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: 0, - ); - await editorState.updateColumnWidthInMemory( - tableCellNode: tableCellNode!, - deltaX: 100, - ); - expect(tableNode.columnWidths, { - '0': SimpleTableConstants.defaultColumnWidth + 100, - }); - - // set the width less than the minimum column width - await editorState.updateColumnWidthInMemory( - tableCellNode: tableCellNode, - deltaX: -1000, - ); - expect(tableNode.columnWidths, { - '0': SimpleTableConstants.minimumColumnWidth, - }); - }); - - test('update column width', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - expect(tableNode.columnWidths, isEmpty); - - for (var i = 0; i < tableNode.columnLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: i, - ); - await editorState.updateColumnWidth( - tableCellNode: tableCellNode!, - width: 100, - ); - } - expect(tableNode.columnWidths, { - '0': 100, - '1': 100, - '2': 100, - }); - - // set the width less than the minimum column width - for (var i = 0; i < tableNode.columnLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: i, - ); - await editorState.updateColumnWidth( - tableCellNode: tableCellNode!, - width: -1000, - ); - } - expect(tableNode.columnWidths, { - '0': SimpleTableConstants.minimumColumnWidth, - '1': SimpleTableConstants.minimumColumnWidth, - '2': SimpleTableConstants.minimumColumnWidth, - }); - }); - - test('update column align', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - for (var i = 0; i < tableNode.columnLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: i, - ); - await editorState.updateColumnAlign( - tableCellNode: tableCellNode!, - align: TableAlign.center, - ); - } - expect(tableNode.columnAligns, { - '0': TableAlign.center.key, - '1': TableAlign.center.key, - '2': TableAlign.center.key, - }); - }); - - test('update row align', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - - for (var i = 0; i < tableNode.rowLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: i, - columnIndex: 0, - ); - await editorState.updateRowAlign( - tableCellNode: tableCellNode!, - align: TableAlign.center, - ); - } - - expect(tableNode.rowAligns, { - '0': TableAlign.center.key, - '1': TableAlign.center.key, - }); - }); - - test('update column background color', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - - for (var i = 0; i < tableNode.columnLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: i, - ); - await editorState.updateColumnBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - } - expect(tableNode.columnColors, { - '0': '0xFF0000FF', - '1': '0xFF0000FF', - '2': '0xFF0000FF', - }); - }); - - test('update row background color', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - - for (var i = 0; i < tableNode.rowLength; i++) { - final tableCellNode = tableNode.getTableCellNode( - rowIndex: i, - columnIndex: 0, - ); - await editorState.updateRowBackgroundColor( - tableCellNode: tableCellNode!, - color: '0xFF0000FF', - ); - } - - expect(tableNode.rowColors, { - '0': '0xFF0000FF', - '1': '0xFF0000FF', - }); - }); - - test('update table align', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - - for (final align in [ - TableAlign.center, - TableAlign.right, - TableAlign.left, - ]) { - await editorState.updateTableAlign( - tableNode: tableNode, - align: align, - ); - expect(tableNode.tableAlign, align); - } - }); - - test('clear the existing align of the column before updating', () async { - final (editorState, tableNode) = createEditorStateAndTable( - rowCount: 2, - columnCount: 3, - ); - - final firstCellNode = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: 0, - ); - - Node firstParagraphNode = firstCellNode!.children.first; - - // format the first paragraph to center align - final transaction = editorState.transaction; - transaction.updateNode( - firstParagraphNode, - { - blockComponentAlign: TableAlign.right.key, - }, - ); - await editorState.apply(transaction); - - firstParagraphNode = editorState.getNodeAtPath([0, 0, 0, 0])!; - expect( - firstParagraphNode.attributes[blockComponentAlign], - TableAlign.right.key, - ); - - await editorState.updateColumnAlign( - tableCellNode: firstCellNode, - align: TableAlign.center, - ); - - expect( - firstParagraphNode.attributes[blockComponentAlign], - null, - ); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart deleted file mode 100644 index e190925bee..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -(EditorState editorState, Node tableNode) createEditorStateAndTable({ - required int rowCount, - required int columnCount, - String? defaultContent, - String Function(int rowIndex, int columnIndex)? contentBuilder, -}) { - final document = Document.blank() - ..insert( - [0], - [ - createSimpleTableBlockNode( - columnCount: columnCount, - rowCount: rowCount, - defaultContent: defaultContent, - contentBuilder: contentBuilder, - ), - ], - ); - final editorState = EditorState(document: document); - return (editorState, document.nodeAtPath([0])!); -} - -Future updateTableColumnAttributes( - EditorState editorState, - Node tableNode, { - required int columnIndex, - TableAlign? align, - String? color, - double? width, -}) async { - final cell = tableNode.getTableCellNode( - rowIndex: 0, - columnIndex: columnIndex, - )!; - - if (align != null) { - await editorState.updateColumnAlign( - tableCellNode: cell, - align: align, - ); - } - - if (color != null) { - await editorState.updateColumnBackgroundColor( - tableCellNode: cell, - color: color, - ); - } - - if (width != null) { - await editorState.updateColumnWidth( - tableCellNode: cell, - width: width, - ); - } -} - -Future updateTableRowAttributes( - EditorState editorState, - Node tableNode, { - required int rowIndex, - TableAlign? align, - String? color, -}) async { - final cell = tableNode.getTableCellNode( - rowIndex: rowIndex, - columnIndex: 0, - )!; - - if (align != null) { - await editorState.updateRowAlign( - tableCellNode: cell, - align: align, - ); - } - - if (color != null) { - await editorState.updateRowBackgroundColor( - tableCellNode: cell, - color: color, - ); - } -} diff --git a/frontend/appflowy_flutter/test/unit_test/url_launcher/url_launcher_test.dart b/frontend/appflowy_flutter/test/unit_test/url_launcher/url_launcher_test.dart deleted file mode 100644 index feac569127..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/url_launcher/url_launcher_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:appflowy/shared/patterns/common_patterns.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('url launcher unit test', () { - test('launch local uri', () async { - const localUris = [ - 'file://path/to/file.txt', - '/path/to/file.txt', - 'C:\\path\\to\\file.txt', - '../path/to/file.txt', - ]; - for (final uri in localUris) { - final result = localPathRegex.hasMatch(uri); - expect(result, true); - } - }); - }); -} diff --git a/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart b/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart deleted file mode 100644 index a2326a3e33..0000000000 --- a/frontend/appflowy_flutter/test/unit_test/util/recent_icons_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:appflowy/core/config/kv.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon.dart'; -import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart'; -import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -void main() { - setUpAll(() { - TestWidgetsFlutterBinding.ensureInitialized(); - SharedPreferences.setMockInitialValues({}); - getIt.registerFactory(() => DartKeyValue()); - Log.shared.disableLog = true; - }); - - bool equalIcon(RecentIcon a, RecentIcon b) => - a.groupName == b.groupName && - a.name == b.name && - a.keywords.equals(b.keywords) && - a.content == b.content; - - test('putEmoji', () async { - List emojiIds = await RecentIcons.getEmojiIds(); - assert(emojiIds.isEmpty); - - await RecentIcons.putEmoji('1'); - emojiIds = await RecentIcons.getEmojiIds(); - assert(emojiIds.equals(['1'])); - - await RecentIcons.putEmoji('2'); - assert(emojiIds.equals(['2', '1'])); - - await RecentIcons.putEmoji('1'); - emojiIds = await RecentIcons.getEmojiIds(); - assert(emojiIds.equals(['1', '2'])); - - for (var i = 0; i < RecentIcons.maxLength; ++i) { - await RecentIcons.putEmoji('${i + 100}'); - } - emojiIds = await RecentIcons.getEmojiIds(); - assert(emojiIds.length == RecentIcons.maxLength); - assert( - emojiIds.equals( - List.generate(RecentIcons.maxLength, (i) => '${i + 100}') - .reversed - .toList(), - ), - ); - }); - - test('putIcons', () async { - List icons = await RecentIcons.getIcons(); - assert(icons.isEmpty); - await loadIconGroups(); - final groups = kIconGroups!; - final List localIcons = []; - for (final e in groups) { - localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList()); - } - - await RecentIcons.putIcon(localIcons.first); - icons = await RecentIcons.getIcons(); - assert(icons.length == 1); - assert(equalIcon(icons.first, localIcons.first)); - - await RecentIcons.putIcon(localIcons[1]); - icons = await RecentIcons.getIcons(); - assert(icons.length == 2); - assert(equalIcon(icons[0], localIcons[1])); - assert(equalIcon(icons[1], localIcons[0])); - - await RecentIcons.putIcon(localIcons.first); - icons = await RecentIcons.getIcons(); - assert(icons.length == 2); - assert(equalIcon(icons[1], localIcons[1])); - assert(equalIcon(icons[0], localIcons[0])); - - for (var i = 0; i < RecentIcons.maxLength; ++i) { - await RecentIcons.putIcon(localIcons[10 + i]); - } - - icons = await RecentIcons.getIcons(); - assert(icons.length == RecentIcons.maxLength); - - for (var i = 0; i < RecentIcons.maxLength; ++i) { - assert( - equalIcon(icons[RecentIcons.maxLength - i - 1], localIcons[10 + i]), - ); - } - }); - - test('put without group name', () async { - RecentIcons.clear(); - List icons = await RecentIcons.getIcons(); - assert(icons.isEmpty); - await loadIconGroups(); - final groups = kIconGroups!; - final List localIcons = []; - for (final e in groups) { - localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList()); - } - - await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, '')); - icons = await RecentIcons.getIcons(); - assert(icons.isEmpty); - - await RecentIcons.putIcon( - RecentIcon(localIcons.first.icon, 'Test group name'), - ); - icons = await RecentIcons.getIcons(); - assert(icons.isNotEmpty); - }); -} diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 3bb774411b..21c1020a71 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -24,7 +24,7 @@ class AppFlowyUnitTest { _pathProviderInitialized(); await FlowyRunner.run( - AppFlowyApplicationUnitTest(), + AppFlowyApplicationUniTest(), IntegrationMode.unitTest, ); @@ -59,7 +59,7 @@ class AppFlowyUnitTest { WorkspacePB get currentWorkspace => workspace; Future _loadWorkspace() async { - final result = await UserBackendService.getCurrentWorkspace(); + final result = await userService.getCurrentWorkspace(); result.fold( (value) => workspace = value, (error) { @@ -69,10 +69,7 @@ class AppFlowyUnitTest { } Future _initialServices() async { - workspaceService = WorkspaceService( - workspaceId: currentWorkspace.id, - userId: userProfile.id, - ); + workspaceService = WorkspaceService(workspaceId: currentWorkspace.id); } Future createWorkspace() async { @@ -96,7 +93,7 @@ void _pathProviderInitialized() { }); } -class AppFlowyApplicationUnitTest implements EntryPoint { +class AppFlowyApplicationUniTest implements EntryPoint { @override Widget create(LaunchConfiguration config) { return const SizedBox.shrink(); diff --git a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart b/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart deleted file mode 100644 index 8a370f74d5..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/confirm_dialog_test.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart'; -import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -import '../../integration_test/shared/util.dart'; -import 'test_material_app.dart'; - -class _ConfirmPopupMock extends Mock { - void confirm(); -} - -void main() { - setUpAll(() async { - SharedPreferences.setMockInitialValues({}); - EasyLocalization.logger.enableLevels = []; - await EasyLocalization.ensureInitialized(); - }); - - Widget buildDialog(VoidCallback onConfirm) { - return Builder( - builder: (context) { - return TextButton( - child: const Text(""), - onPressed: () { - showDialog( - context: context, - builder: (_) { - return AppFlowyTheme( - data: AppFlowyDefaultTheme().light(), - child: Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12.0), - ), - child: ConfirmPopup( - description: "desc", - title: "title", - onConfirm: onConfirm, - ), - ), - ); - }, - ); - }, - ); - }, - ); - } - - testWidgets('confirm dialog shortcut events', (tester) async { - final callback = _ConfirmPopupMock(); - - // escape - await tester.pumpWidget( - WidgetTestApp( - child: buildDialog(callback.confirm), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmPopup), findsOneWidget); - - await tester.simulateKeyEvent(LogicalKeyboardKey.escape); - verifyNever(() => callback.confirm()); - - verifyNever(() => callback.confirm()); - expect(find.byType(ConfirmPopup), findsNothing); - - // enter - await tester.pumpWidget( - WidgetTestApp( - child: buildDialog(callback.confirm), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap(find.byType(TextButton)); - await tester.pumpAndSettle(); - - expect(find.byType(ConfirmPopup), findsOneWidget); - - await tester.simulateKeyEvent(LogicalKeyboardKey.enter); - verify(() => callback.confirm()).called(1); - - verifyNever(() => callback.confirm()); - expect(find.byType(ConfirmPopup), findsNothing); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart b/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart deleted file mode 100644 index 8e9e916aa7..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart +++ /dev/null @@ -1,913 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; -import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:table_calendar/table_calendar.dart'; -import 'package:time/time.dart'; - -import '../../integration_test/shared/util.dart'; -import 'test_material_app.dart'; - -const _mockDatePickerDelay = Duration(milliseconds: 200); - -class _DatePickerDataStub { - _DatePickerDataStub({ - required this.dateTime, - required this.endDateTime, - required this.includeTime, - required this.isRange, - }); - - _DatePickerDataStub.empty() - : dateTime = null, - endDateTime = null, - includeTime = false, - isRange = false; - - DateTime? dateTime; - DateTime? endDateTime; - bool includeTime; - bool isRange; -} - -class _MockDatePicker extends StatefulWidget { - const _MockDatePicker({ - this.data, - this.dateFormat, - this.timeFormat, - }); - - final _DatePickerDataStub? data; - final DateFormatPB? dateFormat; - final TimeFormatPB? timeFormat; - - @override - State<_MockDatePicker> createState() => _MockDatePickerState(); -} - -class _MockDatePickerState extends State<_MockDatePicker> { - late final _DatePickerDataStub data; - late DateFormatPB dateFormat; - late TimeFormatPB timeFormat; - - @override - void initState() { - super.initState(); - data = widget.data ?? _DatePickerDataStub.empty(); - dateFormat = widget.dateFormat ?? DateFormatPB.Friendly; - timeFormat = widget.timeFormat ?? TimeFormatPB.TwelveHour; - } - - void updateDateFormat(DateFormatPB dateFormat) async { - setState(() { - this.dateFormat = dateFormat; - }); - } - - void updateTimeFormat(TimeFormatPB timeFormat) async { - setState(() { - this.timeFormat = timeFormat; - }); - } - - void updateDateCellData({ - required DateTime? dateTime, - required DateTime? endDateTime, - required bool isRange, - required bool includeTime, - }) { - setState(() { - data.dateTime = dateTime; - data.endDateTime = endDateTime; - data.includeTime = includeTime; - data.isRange = isRange; - }); - } - - @override - Widget build(BuildContext context) { - return DesktopAppFlowyDatePicker( - dateTime: data.dateTime, - endDateTime: data.endDateTime, - includeTime: data.includeTime, - isRange: data.isRange, - dateFormat: dateFormat, - timeFormat: timeFormat, - onDaySelected: (date) async { - await Future.delayed(_mockDatePickerDelay); - setState(() { - data.dateTime = date; - }); - }, - onRangeSelected: (start, end) async { - await Future.delayed(_mockDatePickerDelay); - setState(() { - data.dateTime = start; - data.endDateTime = end; - }); - }, - onIncludeTimeChanged: (value, dateTime, endDateTime) async { - await Future.delayed(_mockDatePickerDelay); - setState(() { - data.includeTime = value; - if (dateTime != null) { - data.dateTime = dateTime; - } - if (endDateTime != null) { - data.endDateTime = endDateTime; - } - }); - }, - onIsRangeChanged: (value, dateTime, endDateTime) async { - await Future.delayed(_mockDatePickerDelay); - setState(() { - data.isRange = value; - if (dateTime != null) { - data.dateTime = dateTime; - } - if (endDateTime != null) { - data.endDateTime = endDateTime; - } - }); - }, - ); - } -} - -void main() { - setUpAll(() async { - SharedPreferences.setMockInitialValues({}); - EasyLocalization.logger.enableLevels = []; - await EasyLocalization.ensureInitialized(); - }); - - Finder dayInDatePicker(int day) { - final findCalendar = find.byType(TableCalendar); - final findDay = find.text(day.toString()); - - return find.descendant( - of: findCalendar, - matching: findDay, - ); - } - - DateTime getLastMonth(DateTime date) { - if (date.month == 1) { - return DateTime(date.year - 1, 12); - } else { - return DateTime(date.year, date.month - 1); - } - } - - _MockDatePickerState getMockState(WidgetTester tester) => - tester.state<_MockDatePickerState>(find.byType(_MockDatePicker)); - - AppFlowyDatePickerState getAfState(WidgetTester tester) => - tester.state( - find.byType(DesktopAppFlowyDatePicker), - ); - - group('AppFlowy date picker:', () { - testWidgets('default state', (tester) async { - await tester.pumpWidget( - const WidgetTestApp( - child: _MockDatePicker(), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget); - expect( - find.byWidgetPredicate( - (w) => w is DateTimeTextField && w.dateTime == null, - ), - findsOneWidget, - ); - expect( - find.byWidgetPredicate((w) => w is DatePicker && w.selectedDay == null), - findsOneWidget, - ); - expect( - find.byWidgetPredicate((w) => w is IncludeTimeButton && !w.includeTime), - findsOneWidget, - ); - expect( - find.byWidgetPredicate((w) => w is EndTimeButton && !w.isRange), - findsOneWidget, - ); - }); - - testWidgets('passed in state', (tester) async { - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: DateTime(2024, 10, 12, 13), - endDateTime: DateTime(2024, 10, 14, 5), - includeTime: true, - isRange: true, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget); - expect(find.byType(DateTimeTextField), findsNWidgets(2)); - expect(find.byType(DatePicker), findsOneWidget); - expect( - find.byWidgetPredicate((w) => w is IncludeTimeButton && w.includeTime), - findsOneWidget, - ); - expect( - find.byWidgetPredicate((w) => w is EndTimeButton && w.isRange), - findsOneWidget, - ); - final afState = getAfState(tester); - expect(afState.focusedDateTime, DateTime(2024, 10, 12, 13)); - }); - - testWidgets('date and time formats', (tester) async { - final date = DateTime(2024, 10, 12, 13); - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - dateFormat: DateFormatPB.Friendly, - timeFormat: TimeFormatPB.TwelveHour, - data: _DatePickerDataStub( - dateTime: date, - endDateTime: null, - includeTime: true, - isRange: false, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - final dateText = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_date')), - matching: - find.text(DateFormat(DateFormatPB.Friendly.pattern).format(date)), - ); - expect(dateText, findsOneWidget); - - final timeText = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_time')), - matching: - find.text(DateFormat(TimeFormatPB.TwelveHour.pattern).format(date)), - ); - expect(timeText, findsOneWidget); - - _MockDatePickerState mockState = getMockState(tester); - mockState.updateDateFormat(DateFormatPB.US); - await tester.pumpAndSettle(); - final dateText2 = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_date')), - matching: find.text(DateFormat(DateFormatPB.US.pattern).format(date)), - ); - expect(dateText2, findsOneWidget); - - mockState = getMockState(tester); - mockState.updateTimeFormat(TimeFormatPB.TwentyFourHour); - await tester.pumpAndSettle(); - final timeText2 = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_time')), - matching: find - .text(DateFormat(TimeFormatPB.TwentyFourHour.pattern).format(date)), - ); - expect(timeText2, findsOneWidget); - }); - - testWidgets('page turn buttons', (tester) async { - await tester.pumpWidget( - const WidgetTestApp( - child: _MockDatePicker(), - ), - ); - await tester.pumpAndSettle(); - - final now = DateTime.now(); - expect( - find.text(DateFormat.yMMMM().format(now)), - findsOneWidget, - ); - - final lastMonth = getLastMonth(now); - await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s)); - await tester.pumpAndSettle(); - expect( - find.text(DateFormat.yMMMM().format(lastMonth)), - findsOneWidget, - ); - - await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s)); - await tester.pumpAndSettle(); - expect( - find.text(DateFormat.yMMMM().format(now)), - findsOneWidget, - ); - }); - - testWidgets('select date', (tester) async { - await tester.pumpWidget( - const WidgetTestApp( - child: _MockDatePicker(), - ), - ); - await tester.pumpAndSettle(); - - final now = DateTime.now(); - final third = dayInDatePicker(3).first; - await tester.tap(third); - await tester.pump(); - - DateTime expected = DateTime(now.year, now.month, 3); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime, expected); - expect(mockState.data.dateTime, null); - - await tester.pumpAndSettle(); - mockState = getMockState(tester); - expect(mockState.data.dateTime, expected); - - final firstOfNextMonth = dayInDatePicker(1); - - // for certain months, the first of next month isn't shown - if (firstOfNextMonth.allCandidates.length == 2) { - await tester.tap(firstOfNextMonth); - await tester.pumpAndSettle(); - - expected = DateTime(now.year, now.month + 1); - afState = getAfState(tester); - expect(afState.dateTime, expected); - expect(afState.focusedDateTime, expected); - } - }); - - testWidgets('select date range', (tester) async { - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: null, - endDateTime: null, - includeTime: false, - isRange: true, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.startDateTime, null); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, null); - expect(mockState.data.endDateTime, null); - - // 3-10 - final now = DateTime.now(); - final third = dayInDatePicker(3).first; - await tester.tap(third); - await tester.pumpAndSettle(); - - final expectedStart = DateTime(now.year, now.month, 3); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedStart); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, null); - expect(mockState.data.endDateTime, null); - - final tenth = dayInDatePicker(10).first; - await tester.tap(tenth); - await tester.pump(); - - final expectedEnd = DateTime(now.year, now.month, 10); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedStart); - expect(afState.endDateTime, expectedEnd); - expect(mockState.data.dateTime, null); - expect(mockState.data.endDateTime, null); - - await tester.pumpAndSettle(); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedStart); - expect(afState.endDateTime, expectedEnd); - expect(mockState.data.dateTime, expectedStart); - expect(mockState.data.endDateTime, expectedEnd); - - // 7-18, backwards - final eighteenth = dayInDatePicker(18).first; - await tester.tap(eighteenth); - await tester.pumpAndSettle(); - - final expectedEnd2 = DateTime(now.year, now.month, 18); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedEnd2); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, expectedStart); - expect(mockState.data.endDateTime, expectedEnd); - - final seventh = dayInDatePicker(7).first; - await tester.tap(seventh); - await tester.pump(); - - final expectedStart2 = DateTime(now.year, now.month, 7); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedStart2); - expect(afState.endDateTime, expectedEnd2); - expect(mockState.data.dateTime, expectedStart); - expect(mockState.data.endDateTime, expectedEnd); - - await tester.pumpAndSettle(); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.startDateTime, expectedStart2); - expect(afState.endDateTime, expectedEnd2); - expect(mockState.data.dateTime, expectedStart2); - expect(mockState.data.endDateTime, expectedEnd2); - }); - - testWidgets('select date range after toggling is range', (tester) async { - final now = DateTime.now(); - final fourteenthDateTime = DateTime(now.year, now.month, 14); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: fourteenthDateTime, - endDateTime: null, - includeTime: false, - isRange: false, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime, fourteenthDateTime); - expect(afState.startDateTime, null); - expect(afState.endDateTime, null); - expect(afState.justChangedIsRange, false); - - await tester.tap( - find.descendant( - of: find.byType(EndTimeButton), - matching: find.byType(Toggle), - ), - ); - await tester.pump(); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.isRange, true); - expect(afState.dateTime, fourteenthDateTime); - expect(afState.startDateTime, fourteenthDateTime); - expect(afState.endDateTime, fourteenthDateTime); - expect(afState.justChangedIsRange, true); - expect(mockState.data.isRange, false); - expect(mockState.data.dateTime, fourteenthDateTime); - expect(mockState.data.endDateTime, null); - - await tester.pumpAndSettle(); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.isRange, true); - expect(afState.dateTime, fourteenthDateTime); - expect(afState.startDateTime, fourteenthDateTime); - expect(afState.endDateTime, fourteenthDateTime); - expect(afState.justChangedIsRange, true); - expect(mockState.data.isRange, true); - expect(mockState.data.dateTime, fourteenthDateTime); - expect(mockState.data.endDateTime, fourteenthDateTime); - - final twentyFirst = dayInDatePicker(21).first; - await tester.tap(twentyFirst); - await tester.pumpAndSettle(); - - final expected = DateTime(now.year, now.month, 21); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, fourteenthDateTime); - expect(afState.startDateTime, fourteenthDateTime); - expect(afState.endDateTime, expected); - expect(afState.justChangedIsRange, false); - expect(mockState.data.dateTime, fourteenthDateTime); - expect(mockState.data.endDateTime, expected); - expect(mockState.data.isRange, true); - }); - - testWidgets('include time and modify', (tester) async { - final now = DateTime.now(); - final fourteenthDateTime = now.copyWith(day: 14); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: DateTime( - fourteenthDateTime.year, - fourteenthDateTime.month, - fourteenthDateTime.day, - ), - endDateTime: null, - includeTime: false, - isRange: false, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime!.isAtSameDayAs(fourteenthDateTime), true); - expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), false); - expect(afState.startDateTime, null); - expect(afState.endDateTime, null); - expect(afState.includeTime, false); - - await tester.tap( - find.descendant( - of: find.byType(IncludeTimeButton), - matching: find.byType(Toggle), - ), - ); - await tester.pump(); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), true); - expect(afState.includeTime, true); - expect( - mockState.data.dateTime!.isAtSameDayAs(fourteenthDateTime), - true, - ); - expect( - mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime), - false, - ); - expect(mockState.data.includeTime, false); - - await tester.pumpAndSettle(300.milliseconds); - mockState = getMockState(tester); - expect( - mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime), - true, - ); - expect(mockState.data.includeTime, true); - - final timeField = find.byKey(const ValueKey('date_time_text_field_time')); - await tester.enterText(timeField, "1"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(300.milliseconds); - - DateTime expected = DateTime( - fourteenthDateTime.year, - fourteenthDateTime.month, - fourteenthDateTime.day, - 1, - ); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, expected); - expect(mockState.data.dateTime, expected); - - final dateText = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_date')), - matching: find - .text(DateFormat(DateFormatPB.Friendly.pattern).format(expected)), - ); - expect(dateText, findsOneWidget); - final timeText = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field_time')), - matching: find - .text(DateFormat(TimeFormatPB.TwelveHour.pattern).format(expected)), - ); - expect(timeText, findsOneWidget); - - final third = dayInDatePicker(3).first; - await tester.tap(third); - await tester.pumpAndSettle(); - - expected = DateTime( - fourteenthDateTime.year, - fourteenthDateTime.month, - 3, - 1, - ); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, expected); - expect(mockState.data.dateTime, expected); - }); - - testWidgets( - 'turn on include time, turn on end date, then select date range', - (tester) async { - final fourteenth = DateTime(2024, 10, 14); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: fourteenth, - endDateTime: null, - includeTime: false, - isRange: false, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - await tester.tap( - find.descendant( - of: find.byType(EndTimeButton), - matching: find.byType(Toggle), - ), - ); - await tester.pumpAndSettle(); - - final now = DateTime.now(); - await tester.tap( - find.descendant( - of: find.byType(IncludeTimeButton), - matching: find.byType(Toggle), - ), - ); - await tester.pumpAndSettle(); - - final third = dayInDatePicker(21).first; - await tester.tap(third); - await tester.pumpAndSettle(); - - final afState = getAfState(tester); - final mockState = getMockState(tester); - final expectedTime = Duration(hours: now.hour, minutes: now.minute); - final expectedStart = fourteenth.add(expectedTime); - final expectedEnd = fourteenth.copyWith(day: 21).add(expectedTime); - expect(afState.justChangedIsRange, false); - expect(afState.includeTime, true); - expect(afState.isRange, true); - expect(afState.dateTime, expectedStart); - expect(afState.startDateTime, expectedStart); - expect(afState.endDateTime, expectedEnd); - expect(mockState.data.dateTime, expectedStart); - expect(mockState.data.endDateTime, expectedEnd); - expect(mockState.data.isRange, true); - }, - ); - - testWidgets('edit text field causes start and end to get swapped', - (tester) async { - final fourteenth = DateTime(2024, 10, 14, 1); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: fourteenth, - endDateTime: fourteenth, - includeTime: true, - isRange: true, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - expect( - find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(fourteenth), - ), - findsNWidgets(2), - ); - - final dateTextField = find.descendant( - of: find.byKey(const ValueKey('date_time_text_field')), - matching: find.byKey(const ValueKey('date_time_text_field_date')), - ); - expect(dateTextField, findsOneWidget); - await tester.enterText(dateTextField, "Nov 30, 2024"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - final bday = DateTime(2024, 11, 30, 1); - - expect( - find.descendant( - of: find.byKey(const ValueKey('date_time_text_field')), - matching: find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(fourteenth), - ), - ), - findsOneWidget, - ); - - expect( - find.descendant( - of: find.byKey(const ValueKey('end_date_time_text_field')), - matching: find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(bday), - ), - ), - findsOneWidget, - ); - - final mockState = getMockState(tester); - expect(mockState.data.dateTime, fourteenth); - expect(mockState.data.endDateTime, bday); - }); - - testWidgets( - 'select start date with calendar and then enter end date with keyboard', - (tester) async { - final fourteenth = DateTime(2024, 10, 14, 1); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: fourteenth, - endDateTime: fourteenth, - includeTime: true, - isRange: true, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - final third = dayInDatePicker(3).first; - await tester.tap(third); - await tester.pumpAndSettle(); - - final start = DateTime(2024, 10, 3, 1); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime, start); - expect(afState.startDateTime, start); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, fourteenth); - expect(mockState.data.endDateTime, fourteenth); - expect(mockState.data.isRange, true); - - final dateTextField = find.descendant( - of: find.byKey(const ValueKey('end_date_time_text_field')), - matching: find.byKey(const ValueKey('date_time_text_field_date')), - ); - expect(dateTextField, findsOneWidget); - await tester.enterText(dateTextField, "Oct 18, 2024"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - final end = DateTime(2024, 10, 18, 1); - - expect( - find.descendant( - of: find.byKey(const ValueKey('date_time_text_field')), - matching: find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(start), - ), - ), - findsOneWidget, - ); - - expect( - find.descendant( - of: find.byKey(const ValueKey('end_date_time_text_field')), - matching: find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(end), - ), - ), - findsOneWidget, - ); - - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, start); - expect(afState.startDateTime, start); - expect(afState.endDateTime, end); - expect(mockState.data.dateTime, start); - expect(mockState.data.endDateTime, end); - - // make sure click counter was reset - final twentyFifth = dayInDatePicker(25).first; - final expected = DateTime(2024, 10, 25, 1); - await tester.tap(twentyFifth); - await tester.pumpAndSettle(); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, expected); - expect(afState.startDateTime, expected); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, start); - expect(mockState.data.endDateTime, end); - }); - - testWidgets('same as above but enter time', (tester) async { - final fourteenth = DateTime(2024, 10, 14, 1); - - await tester.pumpWidget( - WidgetTestApp( - child: _MockDatePicker( - data: _DatePickerDataStub( - dateTime: fourteenth, - endDateTime: fourteenth, - includeTime: true, - isRange: true, - ), - ), - ), - ); - await tester.pumpAndSettle(); - - final third = dayInDatePicker(3).first; - await tester.tap(third); - await tester.pumpAndSettle(); - - final start = DateTime(2024, 10, 3, 1); - - final dateTextField = find.descendant( - of: find.byKey(const ValueKey('end_date_time_text_field')), - matching: find.byKey(const ValueKey('date_time_text_field_time')), - ); - expect(dateTextField, findsOneWidget); - await tester.enterText(dateTextField, "15:00"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - - expect( - find.descendant( - of: find.byKey(const ValueKey('date_time_text_field')), - matching: find.text( - DateFormat(DateFormatPB.Friendly.pattern).format(start), - ), - ), - findsOneWidget, - ); - - expect( - find.descendant( - of: find.byKey(const ValueKey('end_date_time_text_field')), - matching: find.text("15:00"), - ), - findsNothing, - ); - - AppFlowyDatePickerState afState = getAfState(tester); - _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime, start); - expect(afState.startDateTime, start); - expect(afState.endDateTime, null); - expect(mockState.data.dateTime, fourteenth); - expect(mockState.data.endDateTime, fourteenth); - - // select for real now - final twentyFifth = dayInDatePicker(25).first; - final expected = DateTime(2024, 10, 25, 1); - await tester.tap(twentyFifth); - await tester.pumpAndSettle(); - await tester.pumpAndSettle(); - afState = getAfState(tester); - mockState = getMockState(tester); - expect(afState.dateTime, start); - expect(afState.startDateTime, start); - expect(afState.endDateTime, expected); - expect(mockState.data.dateTime, start); - expect(mockState.data.endDateTime, expected); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart index 4d954d1724..d83706f068 100644 --- a/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart @@ -33,7 +33,6 @@ void main() { appearanceSettings = await UserSettingsBackendService().getAppearanceSetting(); dateTimeSettings = await UserSettingsBackendService().getDateTimeSettings(); - registerFallbackValue(AppFlowyTextDirection.ltr); }); testWidgets('TextDirectionSelect update default text direction setting', @@ -130,7 +129,7 @@ void main() { when( () => mockAppearanceSettingsBloc.setTextDirection( - any(), + any(), ), ).thenAnswer((_) async => {}); when( @@ -147,7 +146,7 @@ void main() { verify( () => mockAppearanceSettingsBloc.setTextDirection( - any(), + any(), ), ).called(1); verify( diff --git a/frontend/appflowy_flutter/test/widget_test/spae_cion_test.dart b/frontend/appflowy_flutter/test/widget_test/spae_cion_test.dart deleted file mode 100644 index 491afdbf77..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/spae_cion_test.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:convert'; - -import 'package:appflowy/workspace/application/view/view_ext.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:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('space_icon.dart', () { - testWidgets('space icon is empty', (WidgetTester tester) async { - final emptySpaceIcon = { - ViewExtKeys.spaceIconKey: '', - ViewExtKeys.spaceIconColorKey: '', - }; - final space = ViewPB( - name: 'test', - extra: jsonEncode(emptySpaceIcon), - ); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: SpaceIcon(dimension: 22, space: space), - ), - ), - ); - - // test that the input field exists - expect(find.byType(SpaceIcon), findsOneWidget); - - // use the first character of page name as icon - expect(find.text('T'), findsOneWidget); - }); - - testWidgets('space icon is null', (WidgetTester tester) async { - final emptySpaceIcon = { - ViewExtKeys.spaceIconKey: null, - ViewExtKeys.spaceIconColorKey: null, - }; - final space = ViewPB( - name: 'test', - extra: jsonEncode(emptySpaceIcon), - ); - await tester.pumpWidget( - MaterialApp( - home: Material( - child: SpaceIcon(dimension: 22, space: space), - ), - ), - ); - - expect(find.byType(SpaceIcon), findsOneWidget); - - // use the first character of page name as icon - expect(find.text('T'), findsOneWidget); - }); - }); -} diff --git a/frontend/appflowy_flutter/test/widget_test/test_asset_bundle.dart b/frontend/appflowy_flutter/test/widget_test/test_asset_bundle.dart deleted file mode 100644 index ddcabff61f..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/test_asset_bundle.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:convert'; -import 'dart:ui'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -/// TestAssetBundle is required in order to avoid issues with large assets -/// -/// ref: https://medium.com/@sardox/flutter-test-and-randomly-missing-assets-in-goldens-ea959cdd336a -/// -/// "If your AssetManifest.json file exceeds 10kb, it will be -/// loaded with isolate that (most likely) will cause your -/// test to finish before assets are loaded so goldens will -/// get empty assets." -/// -class TestAssetBundle extends CachingAssetBundle { - @override - Future loadString(String key, {bool cache = true}) async { - // overriding this method to avoid limit of 10KB per asset - try { - final data = await load(key); - return utf8.decode(data.buffer.asUint8List()); - } catch (err) { - throw FlutterError('Unable to load asset: $key'); - } - } - - @override - Future load(String key) async => rootBundle.load(key); -} - -final testAssetBundle = TestAssetBundle(); - -/// Loads from our custom asset bundle -class TestBundleAssetLoader extends AssetLoader { - const TestBundleAssetLoader(); - - String getLocalePath(String basePath, Locale locale) { - return '$basePath/${locale.toStringWithSeparator(separator: "-")}.json'; - } - - @override - Future> load(String path, Locale locale) async { - final localePath = getLocalePath(path, locale); - return json.decode(await testAssetBundle.loadString(localePath)); - } -} diff --git a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart b/frontend/appflowy_flutter/test/widget_test/test_material_app.dart deleted file mode 100644 index 2ebc61a8bc..0000000000 --- a/frontend/appflowy_flutter/test/widget_test/test_material_app.dart +++ /dev/null @@ -1,76 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flutter/material.dart'; - -import 'test_asset_bundle.dart'; - -class WidgetTestApp extends StatelessWidget { - const WidgetTestApp({ - super.key, - required this.child, - }); - - final Widget child; - - @override - Widget build(BuildContext context) { - return EasyLocalization( - supportedLocales: const [Locale('en')], - path: 'assets/translations', - fallbackLocale: const Locale('en'), - useFallbackTranslations: true, - saveLocale: false, - assetLoader: const TestBundleAssetLoader(), - child: Builder( - builder: (context) => MaterialApp( - supportedLocales: const [Locale('en')], - locale: const Locale('en'), - localizationsDelegates: context.localizationDelegates, - theme: ThemeData.light().copyWith( - extensions: const [ - AFThemeExtension( - warning: Colors.transparent, - success: Colors.transparent, - tint1: Colors.transparent, - tint2: Colors.transparent, - tint3: Colors.transparent, - tint4: Colors.transparent, - tint5: Colors.transparent, - tint6: Colors.transparent, - tint7: Colors.transparent, - tint8: Colors.transparent, - tint9: Colors.transparent, - textColor: Colors.transparent, - secondaryTextColor: Colors.transparent, - strongText: Colors.transparent, - greyHover: Colors.transparent, - greySelect: Colors.transparent, - lightGreyHover: Colors.transparent, - toggleOffFill: Colors.transparent, - progressBarBGColor: Colors.transparent, - toggleButtonBGColor: Colors.transparent, - calendarWeekendBGColor: Colors.transparent, - gridRowCountColor: Colors.transparent, - code: TextStyle(), - callout: TextStyle(), - calloutBGColor: Colors.transparent, - tableCellBGColor: Colors.transparent, - caption: TextStyle(), - onBackground: Colors.transparent, - background: Colors.transparent, - borderColor: Colors.transparent, - scrollbarColor: Colors.transparent, - scrollbarHoverColor: Colors.transparent, - lightIconColor: Colors.transparent, - toolbarHoverColor: Colors.transparent, - ), - ], - ), - home: Scaffold( - body: child, - ), - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/windows/runner/Runner.rc b/frontend/appflowy_flutter/windows/runner/Runner.rc index 3477dab755..77795cde10 100644 --- a/frontend/appflowy_flutter/windows/runner/Runner.rc +++ b/frontend/appflowy_flutter/windows/runner/Runner.rc @@ -119,11 +119,3 @@ END ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED - -///////////////////////////////////////////////////////////////////////////// -// -// WinSparkle -// - -// And verify signature using DSA public key: -DSAPub DSAPEM "../../dsa_pub.pem" \ No newline at end of file diff --git a/frontend/appflowy_flutter/windows/runner/flutter_window.cpp b/frontend/appflowy_flutter/windows/runner/flutter_window.cpp index 955ee3038f..8e9deabc69 100644 --- a/frontend/appflowy_flutter/windows/runner/flutter_window.cpp +++ b/frontend/appflowy_flutter/windows/runner/flutter_window.cpp @@ -17,7 +17,7 @@ bool FlutterWindow::OnCreate() { RECT frame = GetClientArea(); // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. +// creation / destruction in the startup path. flutter_controller_ = std::make_unique( frame.right - frame.left, frame.bottom - frame.top, project_); // Ensure that basic setup of the controller was successful. @@ -26,16 +26,6 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - return true; } diff --git a/frontend/appflowy_flutter/windows/runner/main.cpp b/frontend/appflowy_flutter/windows/runner/main.cpp index b1fff72b84..8ac91fd693 100644 --- a/frontend/appflowy_flutter/windows/runner/main.cpp +++ b/frontend/appflowy_flutter/windows/runner/main.cpp @@ -47,12 +47,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - - if (!window.Create(L"AppFlowy", origin, size)) { + if (!window.CreateAndShow(L"AppFlowy", origin, size)) { return EXIT_FAILURE; } - - window.Show(); window.SetQuitOnClose(true); ::MSG msg; diff --git a/frontend/appflowy_flutter/windows/runner/win32_window.cpp b/frontend/appflowy_flutter/windows/runner/win32_window.cpp index 2f78196d35..a46adb6af5 100644 --- a/frontend/appflowy_flutter/windows/runner/win32_window.cpp +++ b/frontend/appflowy_flutter/windows/runner/win32_window.cpp @@ -1,70 +1,60 @@ #include "win32_window.h" -#include #include #include "resource.h" #include "app_links/app_links_plugin_c_api.h" -namespace { +namespace +{ -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + // The number of Win32Window objects that currently exist. + static int g_active_window_count = 0; -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; + // Scale helper to convert logical scaler values to physical using passed in + // scale factor + int Scale(int source, double scale_factor) + { + return static_cast(source * scale_factor); } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} -} // namespace + // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. + // This API is only needed for PerMonitor V1 awareness mode. + void EnableFullDpiSupportIfAvailable(HWND hwnd) + { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) + { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) + { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } + } + +} // namespace // Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: +class WindowClassRegistrar +{ +public: ~WindowClassRegistrar() = default; - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { + // Returns the singleton registar instance. + static WindowClassRegistrar *GetInstance() + { + if (!instance_) + { instance_ = new WindowClassRegistrar(); } return instance_; @@ -72,24 +62,26 @@ class WindowClassRegistrar { // Returns the name of the window class, registering the class if it hasn't // previously been registered. - const wchar_t* GetWindowClass(); + const wchar_t *GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); - private: +private: WindowClassRegistrar() = default; - static WindowClassRegistrar* instance_; + static WindowClassRegistrar *instance_; bool class_registered_ = false; }; -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; +WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { +const wchar_t *WindowClassRegistrar::GetWindowClass() +{ + if (!class_registered_) + { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; @@ -108,31 +100,35 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { return kWindowClassName; } -void WindowClassRegistrar::UnregisterWindowClass() { +void WindowClassRegistrar::UnregisterWindowClass() +{ UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } -Win32Window::Win32Window() { +Win32Window::Win32Window() +{ ++g_active_window_count; } -Win32Window::~Win32Window() { +Win32Window::~Win32Window() +{ --g_active_window_count; Destroy(); } -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - +bool Win32Window::CreateAndShow(const std::wstring &title, + const Point &origin, + const Size &size) +{ if (SendAppLinkToInstance(title)) { return false; } - const wchar_t* window_class = + Destroy(); + + const wchar_t *window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), @@ -142,158 +138,19 @@ bool Win32Window::Create(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); - if (!window) { + if (!window) + { return false; } - UpdateTheme(window); - return OnCreate(); } -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} - bool Win32Window::SendAppLinkToInstance(const std::wstring &title) { // Find our exact window @@ -329,4 +186,140 @@ bool Win32Window::SendAppLinkToInstance(const std::wstring &title) } return false; -} \ No newline at end of file +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept +{ + if (message == WM_NCCREATE) + { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } + else if (Win32Window *that = GetThisFromHandle(window)) + { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept +{ + switch (message) + { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) + { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: + { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: + { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) + { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) + { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() +{ + OnDestroy(); + + if (window_handle_) + { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) + { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept +{ + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) +{ + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() +{ + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() +{ + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) +{ + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() +{ + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() +{ + // No-op; provided for subclasses. +} diff --git a/frontend/appflowy_flutter/windows/runner/win32_window.h b/frontend/appflowy_flutter/windows/runner/win32_window.h index fae0d8a741..4d717b053d 100644 --- a/frontend/appflowy_flutter/windows/runner/win32_window.h +++ b/frontend/appflowy_flutter/windows/runner/win32_window.h @@ -10,15 +10,18 @@ // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling -class Win32Window { - public: - struct Point { +class Win32Window +{ +public: + struct Point + { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; - struct Size { + struct Size + { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) @@ -28,16 +31,19 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates a win32 window with |title| that is positioned and sized using + // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring &title, + const Point &origin, + const Size &size); - // Show the current window. Returns true if the window was successfully shown. - bool Show(); + // Dispatches link if any. + // This method enables our app to be with a single instance too. + bool SendAppLinkToInstance(const std::wstring &title); // Release OS resources associated with window. void Destroy(); @@ -55,11 +61,7 @@ class Win32Window { // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); - // Dispatches link if any. - // This method enables our app to be with a single instance too. - bool SendAppLinkToInstance(const std::wstring &title); - - protected: +protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. @@ -75,13 +77,13 @@ class Win32Window { // Called when Destroy is called. virtual void OnDestroy(); - private: +private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by + // responsponds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -89,10 +91,7 @@ class Win32Window { LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); + static Win32Window *GetThisFromHandle(HWND const window) noexcept; bool quit_on_close_ = false; @@ -103,4 +102,4 @@ class Win32Window { HWND child_content_ = nullptr; }; -#endif // RUNNER_WIN32_WINDOW_H_ +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/frontend/appflowy_tauri/.eslintignore b/frontend/appflowy_tauri/.eslintignore new file mode 100644 index 0000000000..e0ff674834 --- /dev/null +++ b/frontend/appflowy_tauri/.eslintignore @@ -0,0 +1,7 @@ +src/services +src/styles +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json \ No newline at end of file diff --git a/frontend/appflowy_tauri/.eslintrc.cjs b/frontend/appflowy_tauri/.eslintrc.cjs new file mode 100644 index 0000000000..a1160f0bd3 --- /dev/null +++ b/frontend/appflowy_tauri/.eslintrc.cjs @@ -0,0 +1,73 @@ +module.exports = { + // https://eslint.org/docs/latest/use/configure/configuration-files + env: { + browser: true, + es6: true, + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname, + extraFileExtensions: ['.json'], + }, + plugins: ['@typescript-eslint', "react-hooks"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/triple-slash-reference': 'error', + '@typescript-eslint/unified-signatures': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'off', + 'constructor-super': 'error', + eqeqeq: ['error', 'always'], + 'no-cond-assign': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-invalid-this': 'error', + 'no-new-wrappers': 'error', + 'no-param-reassign': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'no-void': 'off', + 'prefer-const': 'error', + 'prefer-spread': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + } + ], + 'padding-line-between-statements': [ + "error", + { blankLine: "always", prev: ["const", "let", "var"], next: "*"}, + { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]}, + { blankLine: "always", prev: "import", next: "*" }, + { blankLine: "any", prev: "import", next: "import" }, + { blankLine: "always", prev: "block-like", next: "*" }, + { blankLine: "always", prev: "block", next: "*" }, + + ] + }, + ignorePatterns: ['src/**/*.test.ts', '**/__tests__/**/*.json', 'package.json'] +}; diff --git a/frontend/appflowy_tauri/.gitignore b/frontend/appflowy_tauri/.gitignore new file mode 100644 index 0000000000..32a3d59bc2 --- /dev/null +++ b/frontend/appflowy_tauri/.gitignore @@ -0,0 +1,33 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +**/src/services/backend/models/ +**/src/services/backend/events/ +**/src/appflowy_app/i18n/translations/ + +coverage +**/AppFlowy-Collab + +.env \ No newline at end of file diff --git a/frontend/appflowy_tauri/.prettierignore b/frontend/appflowy_tauri/.prettierignore new file mode 100644 index 0000000000..d515c1c2f2 --- /dev/null +++ b/frontend/appflowy_tauri/.prettierignore @@ -0,0 +1,19 @@ +.DS_Store +node_modules +/build +/public +/.svelte-kit +/package +/.vscode +.env +.env.* +!.env.example + +# rust and generated ts code +/src-tauri +/src/services + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/frontend/appflowy_tauri/.prettierrc.cjs b/frontend/appflowy_tauri/.prettierrc.cjs new file mode 100644 index 0000000000..f283db53a2 --- /dev/null +++ b/frontend/appflowy_tauri/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + arrowParens: 'always', + bracketSpacing: true, + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxBracketSameLine: false, + jsxSingleQuote: true, + printWidth: 121, + plugins: [require('prettier-plugin-tailwindcss')], + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + vueIndentScriptAndStyle: false, +}; diff --git a/frontend/appflowy_tauri/.vscode/extensions.json b/frontend/appflowy_tauri/.vscode/extensions.json new file mode 100644 index 0000000000..24d7cc6de8 --- /dev/null +++ b/frontend/appflowy_tauri/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] +} diff --git a/frontend/appflowy_tauri/README.md b/frontend/appflowy_tauri/README.md new file mode 100644 index 0000000000..102e366893 --- /dev/null +++ b/frontend/appflowy_tauri/README.md @@ -0,0 +1,7 @@ +# Tauri + React + Typescript + +This template should help get you started developing with Tauri, React and Typescript in Vite. + +## Recommended IDE Setup + +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/frontend/appflowy_tauri/index.html b/frontend/appflowy_tauri/index.html new file mode 100644 index 0000000000..4983fb648b --- /dev/null +++ b/frontend/appflowy_tauri/index.html @@ -0,0 +1,14 @@ + + + + + + + AppFlowy: The Open Source Alternative To Notion + + + +
+ + + diff --git a/frontend/appflowy_tauri/jest.config.cjs b/frontend/appflowy_tauri/jest.config.cjs new file mode 100644 index 0000000000..4939478165 --- /dev/null +++ b/frontend/appflowy_tauri/jest.config.cjs @@ -0,0 +1,21 @@ +const { compilerOptions } = require('./tsconfig.json'); +const { pathsToModuleNameMapper } = require("ts-jest"); +const esModules = ["lodash-es", "nanoid"].join("|"); + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + modulePaths: [compilerOptions.baseUrl], + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths), + "^lodash-es(/(.*)|$)": "lodash$1", + "^nanoid(/(.*)|$)": "nanoid$1", + }, + "transform": { + "(.*)/node_modules/nanoid/.+\\.(j|t)sx?$": "ts-jest" + }, + "transformIgnorePatterns": [`/node_modules/(?!${esModules})`], + "testRegex": "(/__tests__/.*\.(test|spec))\\.(jsx?|tsx?)$", +}; \ No newline at end of file diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json new file mode 100644 index 0000000000..30c7978771 --- /dev/null +++ b/frontend/appflowy_tauri/package.json @@ -0,0 +1,126 @@ +{ + "name": "appflowy_tauri", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "pnpm sync:i18n && tsc && vite build", + "preview": "vite preview", + "format": "prettier --write .", + "test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .", + "test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --ext .js,.ts,.tsx .", + "test:prettier": "pnpm prettier --list-different src", + "tauri:clean": "cargo make --cwd .. tauri_clean", + "tauri:dev": "pnpm sync:i18n && tauri dev", + "sync:i18n": "node scripts/i18n/index.cjs", + "css:variables": "node style-dictionary/config.cjs", + "test": "jest" + }, + "dependencies": { + "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.11.12", + "@mui/system": "^5.14.4", + "@mui/x-date-pickers-pro": "^6.18.2", + "@reduxjs/toolkit": "2.0.0", + "@slate-yjs/core": "^1.0.2", + "@tauri-apps/api": "^1.2.0", + "@types/react-swipeable-views": "^0.13.4", + "dayjs": "^1.11.9", + "emoji-mart": "^5.5.2", + "emoji-regex": "^10.2.1", + "events": "^3.3.0", + "google-protobuf": "^3.15.12", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-resources-to-backend": "^1.1.4", + "is-hotkey": "^0.2.0", + "jest": "^29.5.0", + "js-base64": "^3.7.5", + "katex": "^0.16.7", + "lodash-es": "^4.17.21", + "nanoid": "^4.0.0", + "prismjs": "^1.29.0", + "protoc-gen-ts": "0.8.7", + "quill": "^1.3.7", + "quill-delta": "^5.1.0", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-big-calendar": "^1.8.5", + "react-color": "^2.19.3", + "react-custom-scrollbars": "^4.2.1", + "react-datepicker": "^4.23.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-hot-toast": "^2.4.1", + "react-i18next": "^12.2.0", + "react-katex": "^3.0.1", + "react-redux": "^8.0.5", + "react-router-dom": "^6.8.0", + "react-swipeable-views": "^0.14.0", + "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.20", + "react-vtree": "^2.0.4", + "react-window": "^1.8.10", + "react18-input-otp": "^1.1.2", + "redux": "^4.2.1", + "rxjs": "^7.8.0", + "sass": "^1.70.0", + "slate": "^0.101.4", + "slate-history": "^0.100.0", + "slate-react": "^0.101.3", + "ts-results": "^3.3.0", + "unsplash-js": "^7.0.19", + "utf8": "^3.0.0", + "valtio": "^1.12.1", + "yjs": "^13.5.51" + }, + "devDependencies": { + "@svgr/plugin-svgo": "^8.0.1", + "@tauri-apps/cli": "^1.5.6", + "@types/google-protobuf": "^3.15.12", + "@types/is-hotkey": "^0.1.7", + "@types/jest": "^29.5.3", + "@types/katex": "^0.16.0", + "@types/lodash-es": "^4.17.11", + "@types/node": "^18.7.10", + "@types/prismjs": "^1.26.0", + "@types/quill": "^2.0.10", + "@types/react": "^18.0.15", + "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-color": "^3.0.6", + "@types/react-custom-scrollbars": "^4.0.13", + "@types/react-datepicker": "^4.19.3", + "@types/react-dom": "^18.0.6", + "@types/react-katex": "^3.0.0", + "@types/react-transition-group": "^4.4.6", + "@types/react-window": "^1.8.8", + "@types/utf8": "^3.0.1", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.51.0", + "@typescript-eslint/parser": "^5.51.0", + "@vitejs/plugin-react": "^3.0.0", + "autoprefixer": "^10.4.13", + "babel-jest": "^29.6.2", + "eslint": "^8.34.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "jest-environment-jsdom": "^29.6.2", + "postcss": "^8.4.21", + "prettier": "2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "style-dictionary": "^3.8.0", + "tailwindcss": "^3.2.7", + "ts-jest": "^29.1.1", + "ts-node-dev": "^2.0.0", + "tsconfig-paths-jest": "^0.0.1", + "typescript": "^4.6.4", + "uuid": "^9.0.0", + "vite": "^4.0.0", + "vite-plugin-svgr": "^3.2.0" + } +} diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml new file mode 100644 index 0000000000..d670b8b312 --- /dev/null +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -0,0 +1,7264 @@ +lockfileVersion: '6.0' + +dependencies: + '@emoji-mart/data': + specifier: ^1.1.2 + version: 1.1.2 + '@emoji-mart/react': + specifier: ^1.1.1 + version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) + '@emotion/react': + specifier: ^11.10.6 + version: 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': + specifier: ^11.10.6 + version: 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/icons-material': + specifier: ^5.11.11 + version: 5.11.16(@mui/material@5.13.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/material': + specifier: ^5.11.12 + version: 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': + specifier: ^5.14.4 + version: 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/x-date-pickers-pro': + specifier: ^6.18.2 + version: 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@reduxjs/toolkit': + specifier: 2.0.0 + version: 2.0.0(react-redux@8.0.5)(react@18.2.0) + '@slate-yjs/core': + specifier: ^1.0.2 + version: 1.0.2(slate@0.101.4)(yjs@13.6.1) + '@tauri-apps/api': + specifier: ^1.2.0 + version: 1.3.0 + '@types/react-swipeable-views': + specifier: ^0.13.4 + version: 0.13.4 + dayjs: + specifier: ^1.11.9 + version: 1.11.9 + emoji-mart: + specifier: ^5.5.2 + version: 5.5.2 + emoji-regex: + specifier: ^10.2.1 + version: 10.2.1 + events: + specifier: ^3.3.0 + version: 3.3.0 + google-protobuf: + specifier: ^3.15.12 + version: 3.21.2 + i18next: + specifier: ^22.4.10 + version: 22.4.15 + i18next-browser-languagedetector: + specifier: ^7.0.1 + version: 7.0.1 + i18next-resources-to-backend: + specifier: ^1.1.4 + version: 1.1.4 + is-hotkey: + specifier: ^0.2.0 + version: 0.2.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@18.16.9) + js-base64: + specifier: ^3.7.5 + version: 3.7.5 + katex: + specifier: ^0.16.7 + version: 0.16.7 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + nanoid: + specifier: ^4.0.0 + version: 4.0.2 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 + protoc-gen-ts: + specifier: 0.8.7 + version: 0.8.7 + quill: + specifier: ^1.3.7 + version: 1.3.7 + quill-delta: + specifier: ^5.1.0 + version: 5.1.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) + react-big-calendar: + specifier: ^1.8.5 + version: 1.8.5(react-dom@18.2.0)(react@18.2.0) + react-color: + specifier: ^2.19.3 + version: 2.19.3(react@18.2.0) + react-custom-scrollbars: + specifier: ^4.2.1 + version: 4.2.1(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^3.1.4 + version: 3.1.4(react@18.2.0) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0) + react-i18next: + specifier: ^12.2.0 + version: 12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0) + react-katex: + specifier: ^3.0.1 + version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-redux: + specifier: ^8.0.5 + version: 8.0.5(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + react-router-dom: + specifier: ^6.8.0 + version: 6.11.1(react-dom@18.2.0)(react@18.2.0) + react-swipeable-views: + specifier: ^0.14.0 + version: 0.14.0(react@18.2.0) + react-transition-group: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-virtualized-auto-sizer: + specifier: ^1.0.20 + version: 1.0.20(react-dom@18.2.0)(react@18.2.0) + react-vtree: + specifier: ^2.0.4 + version: 2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0) + react-window: + specifier: ^1.8.10 + version: 1.8.10(react-dom@18.2.0)(react@18.2.0) + react18-input-otp: + specifier: ^1.1.2 + version: 1.1.3(react-dom@18.2.0)(react@18.2.0) + redux: + specifier: ^4.2.1 + version: 4.2.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.1 + sass: + specifier: ^1.70.0 + version: 1.70.0 + slate: + specifier: ^0.101.4 + version: 0.101.4 + slate-history: + specifier: ^0.100.0 + version: 0.100.0(slate@0.101.4) + slate-react: + specifier: ^0.101.3 + version: 0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4) + ts-results: + specifier: ^3.3.0 + version: 3.3.0 + unsplash-js: + specifier: ^7.0.19 + version: 7.0.19 + utf8: + specifier: ^3.0.0 + version: 3.0.0 + valtio: + specifier: ^1.12.1 + version: 1.12.1(@types/react@18.2.6)(react@18.2.0) + yjs: + specifier: ^13.5.51 + version: 13.6.1 + +devDependencies: + '@svgr/plugin-svgo': + specifier: ^8.0.1 + version: 8.0.1(@svgr/core@7.0.0) + '@tauri-apps/cli': + specifier: ^1.5.6 + version: 1.5.6 + '@types/google-protobuf': + specifier: ^3.15.12 + version: 3.15.12 + '@types/is-hotkey': + specifier: ^0.1.7 + version: 0.1.7 + '@types/jest': + specifier: ^29.5.3 + version: 29.5.3 + '@types/katex': + specifier: ^0.16.0 + version: 0.16.0 + '@types/lodash-es': + specifier: ^4.17.11 + version: 4.17.11 + '@types/node': + specifier: ^18.7.10 + version: 18.16.9 + '@types/prismjs': + specifier: ^1.26.0 + version: 1.26.0 + '@types/quill': + specifier: ^2.0.10 + version: 2.0.10 + '@types/react': + specifier: ^18.0.15 + version: 18.2.6 + '@types/react-beautiful-dnd': + specifier: ^13.1.3 + version: 13.1.4 + '@types/react-color': + specifier: ^3.0.6 + version: 3.0.6 + '@types/react-custom-scrollbars': + specifier: ^4.0.13 + version: 4.0.13 + '@types/react-datepicker': + specifier: ^4.19.3 + version: 4.19.3(react-dom@18.2.0)(react@18.2.0) + '@types/react-dom': + specifier: ^18.0.6 + version: 18.2.4 + '@types/react-katex': + specifier: ^3.0.0 + version: 3.0.0 + '@types/react-transition-group': + specifier: ^4.4.6 + version: 4.4.6 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 + '@types/utf8': + specifier: ^3.0.1 + version: 3.0.1 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.1 + '@typescript-eslint/eslint-plugin': + specifier: ^5.51.0 + version: 5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^5.51.0 + version: 5.59.5(eslint@8.40.0)(typescript@4.9.5) + '@vitejs/plugin-react': + specifier: ^3.0.0 + version: 3.1.0(vite@4.3.5) + autoprefixer: + specifier: ^10.4.13 + version: 10.4.14(postcss@8.4.23) + babel-jest: + specifier: ^29.6.2 + version: 29.6.2(@babel/core@7.21.8) + eslint: + specifier: ^8.34.0 + version: 8.40.0 + eslint-plugin-react: + specifier: ^7.32.2 + version: 7.32.2(eslint@8.40.0) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.40.0) + jest-environment-jsdom: + specifier: ^29.6.2 + version: 29.6.2 + postcss: + specifier: ^8.4.21 + version: 8.4.23 + prettier: + specifier: 2.8.4 + version: 2.8.4 + prettier-plugin-tailwindcss: + specifier: ^0.2.2 + version: 0.2.8(prettier@2.8.4) + style-dictionary: + specifier: ^3.8.0 + version: 3.8.0 + tailwindcss: + specifier: ^3.2.7 + version: 3.3.2 + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.21.8)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@18.16.9)(typescript@4.9.5) + tsconfig-paths-jest: + specifier: ^0.0.1 + version: 0.0.1 + typescript: + specifier: ^4.6.4 + version: 4.9.5 + uuid: + specifier: ^9.0.0 + version: 9.0.0 + vite: + specifier: ^4.0.0 + version: 4.3.5(@types/node@18.16.9)(sass@1.70.0) + vite-plugin-svgr: + specifier: ^3.2.0 + version: 3.2.0(vite@4.3.5) + +packages: + + /@alloc/quick-lru@5.2.0: + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + + /@babel/code-frame@7.21.4: + resolution: {integrity: sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.18.6 + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + + /@babel/compat-data@7.21.7: + resolution: {integrity: sha512-KYMqFYTaenzMK4yUtf4EW9wc4N9ef80FsbMtkwool5zpwl4YrT1SdWYSTRcT94KO4hannogdS+LxY7L+arP3gA==} + engines: {node: '>=6.9.0'} + + /@babel/core@7.21.8: + resolution: {integrity: sha512-YeM22Sondbo523Sz0+CirSPnbj9bG3P0CdHcBZdqUuaeOaYEFbOLoGU7lebvGP6P5J/WE9wOn7u7C4J9HvS1xQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.5 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.21.8) + '@babel/helper-module-transforms': 7.21.5 + '@babel/helpers': 7.21.5 + '@babel/parser': 7.21.8 + '@babel/template': 7.20.7 + '@babel/traverse': 7.23.7 + '@babel/types': 7.21.5 + convert-source-map: 1.9.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /@babel/generator@7.21.5: + resolution: {integrity: sha512-SrKK/sRv8GesIW1bDagf9cCG38IOMYZusoe1dfg0D8aiUe3Amvoj1QtjTPAWcfrZFvIwlleLb0gxzQidL9w14w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + jsesc: 2.5.2 + + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.18 + jsesc: 2.5.2 + + /@babel/helper-compilation-targets@7.21.5(@babel/core@7.21.8): + resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/compat-data': 7.21.7 + '@babel/core': 7.21.8 + '@babel/helper-validator-option': 7.21.0 + browserslist: 4.21.5 + lru-cache: 5.1.1 + semver: 6.3.0 + + /@babel/helper-environment-visitor@7.21.5: + resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-module-imports@7.21.4: + resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 + + /@babel/helper-module-transforms@7.21.5: + resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-environment-visitor': 7.21.5 + '@babel/helper-module-imports': 7.21.4 + '@babel/helper-simple-access': 7.21.5 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/helper-validator-identifier': 7.19.1 + '@babel/template': 7.20.7 + '@babel/traverse': 7.23.7 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + + /@babel/helper-plugin-utils@7.21.5: + resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} + engines: {node: '>=6.9.0'} + + /@babel/helper-simple-access@7.21.5: + resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 + + /@babel/helper-split-export-declaration@7.18.6: + resolution: {integrity: sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.21.5 + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + + /@babel/helper-string-parser@7.21.5: + resolution: {integrity: sha512-5pTUx3hAJaZIdW99sJ6ZUUgWq/Y+Hja7TowEnLNMm1VivRgZQL3vpBY3qUACVsvw+yQU6+YgfBVmcbLaZtrA1w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.19.1: + resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.21.0: + resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} + engines: {node: '>=6.9.0'} + + /@babel/helpers@7.21.5: + resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.20.7 + '@babel/traverse': 7.23.7 + '@babel/types': 7.21.5 + transitivePeerDependencies: + - supports-color + + /@babel/highlight@7.18.6: + resolution: {integrity: sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.19.1 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + + /@babel/parser@7.21.8: + resolution: {integrity: sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.21.5 + + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.8): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.8): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.8): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.21.8): + resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.8): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.8): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.8): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.8): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.21.8): + resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + + /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.8): + resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.21.8): + resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@babel/helper-plugin-utils': 7.21.5 + dev: true + + /@babel/runtime@7.0.0: + resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} + dependencies: + regenerator-runtime: 0.12.1 + dev: false + + /@babel/runtime@7.21.5: + resolution: {integrity: sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + + /@babel/runtime@7.22.10: + resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + dev: false + + /@babel/runtime@7.23.4: + resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.0 + + /@babel/template@7.20.7: + resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.21.4 + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + + /@babel/traverse@7.23.7: + resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/types@7.21.5: + resolution: {integrity: sha512-m4AfNvVF2mVC/F7fDEdH2El3HzUg9It/XsCxZiOTTA3m3qYfcSVSbTfM6Q9xG+hYDniZssYhlXKKUMD5m8tF4Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.21.5 + '@babel/helper-validator-identifier': 7.19.1 + to-fast-properties: 2.0.0 + + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@emoji-mart/data@1.1.2: + resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==} + dev: false + + /@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0): + resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} + peerDependencies: + emoji-mart: ^5.2 + react: ^16.8 || ^17 || ^18 + dependencies: + emoji-mart: 5.5.2 + react: 18.2.0 + dev: false + + /@emotion/babel-plugin@11.11.0: + resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + dependencies: + '@babel/helper-module-imports': 7.21.4 + '@babel/runtime': 7.21.5 + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/serialize': 1.1.2 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + dev: false + + /@emotion/cache@11.11.0: + resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/sheet': 1.2.2 + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + stylis: 4.2.0 + dev: false + + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: false + + /@emotion/is-prop-valid@1.2.1: + resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} + dependencies: + '@emotion/memoize': 0.8.1 + dev: false + + /@emotion/memoize@0.8.1: + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + dev: false + + /@emotion/react@11.11.0(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-ZSK3ZJsNkwfjT3JpDAWJZlrGD81Z3ytNDsxw1LKq1o+xkmO5pnWfr6gmCC8gHEFf3nSSX/09YrG67jybNPxSUw==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.6 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@emotion/serialize@1.1.2: + resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.2 + dev: false + + /@emotion/sheet@1.2.2: + resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + dev: false + + /@emotion/styled@11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.1 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/serialize': 1.1.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@types/react': 18.2.6 + react: 18.2.0 + dev: false + + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /@emotion/utils@1.2.1: + resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + dev: false + + /@emotion/weak-memoize@0.3.1: + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + dev: false + + /@esbuild/android-arm64@0.17.19: + resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.17.19: + resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.17.19: + resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.17.19: + resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.17.19: + resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.17.19: + resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.17.19: + resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.17.19: + resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.17.19: + resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.17.19: + resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.17.19: + resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.17.19: + resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.17.19: + resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.17.19: + resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.17.19: + resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.17.19: + resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.17.19: + resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.17.19: + resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.17.19: + resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.17.19: + resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.17.19: + resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.17.19: + resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.40.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.40.0 + eslint-visitor-keys: 3.4.1 + dev: true + + /@eslint-community/regexpp@4.5.1: + resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.0.3: + resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.5.2 + globals: 13.20.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.40.0: + resolution: {integrity: sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@floating-ui/core@1.5.0: + resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==} + dependencies: + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/dom@1.5.3: + resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==} + dependencies: + '@floating-ui/core': 1.5.0 + '@floating-ui/utils': 0.1.6 + dev: false + + /@floating-ui/react-dom@2.0.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.5.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/utils@0.1.6: + resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==} + dev: false + + /@humanwhocodes/config-array@0.11.8: + resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@icons/material@0.2.4(react@18.2.0): + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + /@jest/console@29.5.0: + resolution: {integrity: sha512-NEpkObxPwyw/XxZVLPmAGKE89IQRp4puc6IQRPru6JKd1M3fW9v1xM1AnzIJE65hbCkzQAdnL8P47e9hzhiYLQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + chalk: 4.1.2 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + slash: 3.0.0 + + /@jest/core@29.5.0: + resolution: {integrity: sha512-28UzQc7ulUrOQw1IsN/kv1QES3q2kkbl/wGslyhAclqZ/8cMdB5M68BffkIdSJgKBUt50d3hbwJ92XESlE7LiQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.5.0 + '@jest/reporters': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.5.0 + jest-config: 29.5.0(@types/node@18.16.9) + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-resolve-dependencies: 29.5.0 + jest-runner: 29.5.0 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + jest-watcher: 29.5.0 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + - ts-node + + /@jest/environment@29.5.0: + resolution: {integrity: sha512-5FXw2+wD29YU1d4I2htpRX7jYnAyTRjP2CsXQdo9SAM8g3ifxWPSV0HnClSn71xwctr0U3oZIIH+dtbfmnbXVQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + jest-mock: 29.5.0 + + /@jest/environment@29.6.4: + resolution: {integrity: sha512-sQ0SULEjA1XUTHmkBRl7A1dyITM9yb1yb3ZNKPX3KlTd6IG7mWUe3e2yfExtC2Zz1Q+mMckOLHmL/qLiuQJrBQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 18.16.9 + jest-mock: 29.6.3 + + /@jest/expect-utils@29.5.0: + resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + + /@jest/expect@29.5.0: + resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.5.0 + jest-snapshot: 29.5.0 + transitivePeerDependencies: + - supports-color + + /@jest/fake-timers@29.5.0: + resolution: {integrity: sha512-9ARvuAAQcBwDAqOnglWq2zwNIRUDtk/SCkp/ToGEhFv5r86K21l+VEs0qNTaXtyiY0lEePl3kylijSYJQqdbDg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@sinonjs/fake-timers': 10.1.0 + '@types/node': 18.16.9 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-util: 29.5.0 + + /@jest/fake-timers@29.6.4: + resolution: {integrity: sha512-6UkCwzoBK60edXIIWb0/KWkuj7R7Qq91vVInOe3De6DSpaEiqjKcJw4F7XUet24Wupahj9J6PlR09JqJ5ySDHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.1.0 + '@types/node': 18.16.9 + jest-message-util: 29.6.3 + jest-mock: 29.6.3 + jest-util: 29.6.3 + + /@jest/globals@29.5.0: + resolution: {integrity: sha512-S02y0qMWGihdzNbUiqSAiKSpSozSuHX5UYc7QbnHP+D9Lyw8DgGGCinrN9uSuHPeKgSSzvPom2q1nAtBvUsvPQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.5.0 + '@jest/expect': 29.5.0 + '@jest/types': 29.5.0 + jest-mock: 29.5.0 + transitivePeerDependencies: + - supports-color + + /@jest/reporters@29.5.0: + resolution: {integrity: sha512-D05STXqj/M8bP9hQNSICtPqz97u7ffGzZu+9XLucXhkOFBqKcXe04JLZOgIekOxdb73MAoBUFnqvf7MCpKk5OA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@jridgewell/trace-mapping': 0.3.18 + '@types/node': 18.16.9 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 5.2.1 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + jest-worker: 29.5.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.1.0 + transitivePeerDependencies: + - supports-color + + /@jest/schemas@29.4.3: + resolution: {integrity: sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.25.24 + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + + /@jest/source-map@29.4.3: + resolution: {integrity: sha512-qyt/mb6rLyd9j1jUts4EQncvS6Yy3PM9HghnNv86QBlV+zdL2inCdK1tuVlL+J+lpiw2BI67qXOrX3UurBqQ1w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + /@jest/test-result@29.5.0: + resolution: {integrity: sha512-fGl4rfitnbfLsrfx1uUpDEESS7zM8JdgZgOCQuxQvL1Sn/I6ijeAVQWGfXI9zb1i9Mzo495cIpVZhA0yr60PkQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.5.0 + '@jest/types': 29.5.0 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + + /@jest/test-sequencer@29.5.0: + resolution: {integrity: sha512-yPafQEcKjkSfDXyvtgiV4pevSeyuA6MQr6ZIdVkWJly9vkqjnFfcfhRQqpD5whjoU8EORki752xQmjaqoFjzMQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.6.4 + slash: 3.0.0 + + /@jest/transform@29.5.0: + resolution: {integrity: sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.21.8 + '@jest/types': 29.5.0 + '@jridgewell/trace-mapping': 0.3.18 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.5.0 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + /@jest/transform@29.6.4: + resolution: {integrity: sha512-8thgRSiXUqtr/pPGY/OsyHuMjGyhVnWrFAwoxmIemlBuiMyU1WFs0tXoNxzcr4A4uErs/ABre76SGmrr5ab/AA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.21.8 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.18 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.6.4 + jest-regex-util: 29.6.3 + jest-util: 29.6.3 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + /@jest/types@29.5.0: + resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.16.9 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 18.16.9 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.18 + + /@jridgewell/resolve-uri@3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/sourcemap-codec@1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.18: + resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + + /@mui/base@5.0.0-beta.0(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ap+juKvt8R8n3cBqd/pGtZydQ4v2I/hgJKnvJRGjpSh3RvsvnDHO4rXov8MHQlH6VqpOekwgilFLGxMZjNTucA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@emotion/is-prop-valid': 1.2.1 + '@mui/types': 7.2.4(@types/react@18.2.6) + '@mui/utils': 5.12.3(react@18.2.0) + '@popperjs/core': 2.11.7 + '@types/react': 18.2.6 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + dev: false + + /@mui/base@5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.9(@types/react@18.2.6) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.6 + clsx: 2.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/core-downloads-tracker@5.13.0: + resolution: {integrity: sha512-5nXz2k8Rv2ZjtQY6kXirJVyn2+ODaQuAJmXSJtLDUQDKWp3PFUj6j3bILqR0JGOs9R5ejgwz3crLKsl6GwjwkQ==} + dev: false + + /@mui/icons-material@5.11.16(@mui/material@5.13.0)(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.6 + react: 18.2.0 + dev: false + + /@mui/material@5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ckS+9tCpAzpdJdaTF+btF0b6mF9wbXg/EVKtnoAWYi0UKXoXBAVvEUMNpLGA5xdpCdf+A6fPbVUEHs9TsfU+Yw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/base': 5.0.0-beta.0(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.13.0 + '@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/types': 7.2.4(@types/react@18.2.6) + '@mui/utils': 5.12.3(react@18.2.0) + '@types/react': 18.2.6 + '@types/react-transition-group': 4.4.6 + clsx: 1.2.1 + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/private-theming@5.14.4(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-ISXsHDiQ3z1XA4IuKn+iXDWvDjcz/UcQBiFZqtdoIsEBt8CB7wgdQf3LwcwqO81dl5ofg/vNQBEnXuKfZHrnYA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@mui/utils': 5.14.4(react@18.2.0) + '@types/react': 18.2.6 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine@5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0): + resolution: {integrity: sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/system@5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.22.10 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/private-theming': 5.14.4(@types/react@18.2.6)(react@18.2.0) + '@mui/styled-engine': 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0) + '@mui/types': 7.2.4(@types/react@18.2.6) + '@mui/utils': 5.14.4(react@18.2.0) + '@types/react': 18.2.6 + clsx: 2.0.0 + csstype: 3.1.2 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/types@7.2.4(@types/react@18.2.6): + resolution: {integrity: sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==} + peerDependencies: + '@types/react': '*' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.6 + dev: false + + /@mui/types@7.2.9(@types/react@18.2.6): + resolution: {integrity: sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.6 + dev: false + + /@mui/utils@5.12.3(react@18.2.0): + resolution: {integrity: sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.5 + '@types/prop-types': 15.7.5 + '@types/react-is': 17.0.4 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/utils@5.14.18(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@types/prop-types': 15.7.11 + '@types/react': 18.2.6 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/utils@5.14.4(react@18.2.0): + resolution: {integrity: sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.22.10 + '@types/prop-types': 15.7.5 + '@types/react-is': 18.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@mui/x-date-pickers': 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-license-pro': 6.10.2(@types/react@18.2.6)(react@18.2.0) + clsx: 2.0.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-date-pickers@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + '@types/react-transition-group': 4.4.9 + clsx: 2.0.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-license-pro@6.10.2(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-Baw3shilU+eHgU+QYKNPFUKvfS5rSyNJ98pQx02E0gKA22hWp/XAt88K1qUfUMPlkPpvg/uci6gviQSSLZkuKw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.4 + '@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.15.0 + dev: true + + /@popperjs/core@2.11.7: + resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==} + dev: false + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + /@reduxjs/toolkit@2.0.0(react-redux@8.0.5)(react@18.2.0): + resolution: {integrity: sha512-Kq/a+aO28adYdPoNEu9p800MYPKoUc0tlkYfv035Ief9J7MPq8JvmT7UdpYhvXsoMtOdt567KwZjc9H3Rf8yjg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.3 + react: 18.2.0 + react-redux: 8.0.5(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + redux: 5.0.0 + redux-thunk: 3.1.0(redux@5.0.0) + reselect: 5.0.1 + dev: false + + /@remix-run/router@1.6.1: + resolution: {integrity: sha512-YUkWj+xs0oOzBe74OgErsuR3wVn+efrFhXBWrit50kOiED+pvQe2r6MWY0iJMQU/mSVKxvNzL4ZaYvjdX+G7ZA==} + engines: {node: '>=14'} + dev: false + + /@restart/hooks@0.4.15(react@18.2.0): + resolution: {integrity: sha512-cZFXYTxbpzYcieq/mBwSyXgqnGMHoBVh3J7MU0CCoIB4NRZxV9/TuwTBAaLMqpNhC3zTPMCgkQ5Ey07L02Xmcw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + dequal: 2.0.3 + react: 18.2.0 + dev: false + + /@rollup/pluginutils@5.0.2: + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.1 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@sinclair/typebox@0.25.24: + resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==} + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@sinonjs/commons@3.0.0: + resolution: {integrity: sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==} + dependencies: + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.1.0: + resolution: {integrity: sha512-w1qd368vtrwttm1PRJWPW1QHlbmHrVDGs1eBH/jZvRPUFS4MNXV9Q33EQdjOdeAxZ7O8+3wM7zxztm2nfUSyKw==} + dependencies: + '@sinonjs/commons': 3.0.0 + + /@slate-yjs/core@1.0.2(slate@0.101.4)(yjs@13.6.1): + resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.101.4 + y-protocols: 1.0.6(yjs@13.6.1) + yjs: 13.6.1 + dev: false + + /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + dev: true + + /@svgr/babel-preset@7.0.0(@babel/core@7.21.8): + resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.8 + '@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.21.8) + '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.21.8) + dev: true + + /@svgr/core@7.0.0: + resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.21.8 + '@svgr/babel-preset': 7.0.0(@babel/core@7.21.8) + camelcase: 6.3.0 + cosmiconfig: 8.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@svgr/hast-util-to-babel-ast@7.0.0: + resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==} + engines: {node: '>=14'} + dependencies: + '@babel/types': 7.21.5 + entities: 4.5.0 + dev: true + + /@svgr/plugin-jsx@7.0.0: + resolution: {integrity: sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.21.8 + '@svgr/babel-preset': 7.0.0(@babel/core@7.21.8) + '@svgr/hast-util-to-babel-ast': 7.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@svgr/plugin-svgo@8.0.1(@svgr/core@7.0.0): + resolution: {integrity: sha512-29OJ1QmJgnohQHDAgAuY2h21xWD6TZiXji+hnx+W635RiXTAlHTbjrZDktfqzkN0bOeQEtNe+xgq73/XeWFfSg==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + dependencies: + '@svgr/core': 7.0.0 + cosmiconfig: 8.2.0 + deepmerge: 4.3.1 + svgo: 3.0.2 + dev: true + + /@tauri-apps/api@1.3.0: + resolution: {integrity: sha512-AH+3FonkKZNtfRtGrObY38PrzEj4d+1emCbwNGu0V2ENbXjlLHMZQlUh+Bhu/CRmjaIwZMGJ3yFvWaZZgTHoog==} + engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + dev: false + + /@tauri-apps/cli-darwin-arm64@1.5.6: + resolution: {integrity: sha512-NNvG3XLtciCMsBahbDNUEvq184VZmOveTGOuy0So2R33b/6FDkuWaSgWZsR1mISpOuP034htQYW0VITCLelfqg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-darwin-x64@1.5.6: + resolution: {integrity: sha512-nkiqmtUQw3N1j4WoVjv81q6zWuZFhBLya/RNGUL94oafORloOZoSY0uTZJAoeieb3Y1YK0rCHSDl02MyV2Fi4A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm-gnueabihf@1.5.6: + resolution: {integrity: sha512-z6SPx+axZexmWXTIVPNs4Tg7FtvdJl9EKxYN6JPjOmDZcqA13iyqWBQal2DA/GMZ1Xqo3vyJf6EoEaKaliymPQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-gnu@1.5.6: + resolution: {integrity: sha512-QuQjMQmpsCbzBrmtQiG4uhnfAbdFx3nzm+9LtqjuZlurc12+Mj5MTgqQ3AOwQedH3f7C+KlvbqD2AdXpwTg7VA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-musl@1.5.6: + resolution: {integrity: sha512-8j5dH3odweFeom7bRGlfzDApWVOT4jIq8/214Wl+JeiNVehouIBo9lZGeghZBH3XKFRwEvU23i7sRVjuh2s8mg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-gnu@1.5.6: + resolution: {integrity: sha512-gbFHYHfdEGW0ffk8SigDsoXks6USpilF6wR0nqB/JbWzbzFR/sBuLVNQlJl1RKNakyJHu+lsFxGy0fcTdoX8xA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-musl@1.5.6: + resolution: {integrity: sha512-9v688ogoLkeFYQNgqiSErfhTreLUd8B3prIBSYUt+x4+5Kcw91zWvIh+VSxL1n3KCGGsM7cuXhkGPaxwlEh1ug==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-arm64-msvc@1.5.6: + resolution: {integrity: sha512-DRNDXFNZb6y5IZrw+lhTTA9l4wbzO4TNRBAlHAiXUrH+pRFZ/ZJtv5WEuAj9ocVSahVw2NaK5Yaold4NPAxHog==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-ia32-msvc@1.5.6: + resolution: {integrity: sha512-oUYKNR/IZjF4fsOzRpw0xesl2lOjhsQEyWlgbpT25T83EU113Xgck9UjtI7xemNI/OPCv1tPiaM1e7/ABdg5iA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-x64-msvc@1.5.6: + resolution: {integrity: sha512-RmEf1os9C8//uq2hbjXi7Vgz9ne7798ZxqemAZdUwo1pv3oLVZSz1/IvZmUHPdy2e6zSeySqWu1D0Y3QRNN+dg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli@1.5.6: + resolution: {integrity: sha512-k4Y19oVCnt7WZb2TnDzLqfs7o98Jq0tUoVMv+JQSzuRDJqaVu2xMBZ8dYplEn+EccdR5SOMyzaLBJWu38TVK1A==} + engines: {node: '>= 10'} + hasBin: true + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 1.5.6 + '@tauri-apps/cli-darwin-x64': 1.5.6 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.5.6 + '@tauri-apps/cli-linux-arm64-gnu': 1.5.6 + '@tauri-apps/cli-linux-arm64-musl': 1.5.6 + '@tauri-apps/cli-linux-x64-gnu': 1.5.6 + '@tauri-apps/cli-linux-x64-musl': 1.5.6 + '@tauri-apps/cli-win32-arm64-msvc': 1.5.6 + '@tauri-apps/cli-win32-ia32-msvc': 1.5.6 + '@tauri-apps/cli-win32-x64-msvc': 1.5.6 + dev: true + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/babel__core@7.20.0: + resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==} + dependencies: + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + '@types/babel__generator': 7.6.4 + '@types/babel__template': 7.4.1 + '@types/babel__traverse': 7.18.5 + + /@types/babel__generator@7.6.4: + resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} + dependencies: + '@babel/types': 7.21.5 + + /@types/babel__template@7.4.1: + resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} + dependencies: + '@babel/parser': 7.21.8 + '@babel/types': 7.21.5 + + /@types/babel__traverse@7.18.5: + resolution: {integrity: sha512-enCvTL8m/EHS/zIvJno9nE+ndYPh1/oNFzRYRmtUqJICG2VnCSBzMLW5VN2KCQU91f23tsNKR8v7VJJQMatl7Q==} + dependencies: + '@babel/types': 7.21.5 + + /@types/estree@1.0.1: + resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} + dev: true + + /@types/google-protobuf@3.15.12: + resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} + dev: true + + /@types/graceful-fs@4.1.6: + resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} + dependencies: + '@types/node': 18.16.9 + + /@types/hoist-non-react-statics@3.3.1: + resolution: {integrity: sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==} + dependencies: + '@types/react': 18.2.6 + hoist-non-react-statics: 3.3.2 + dev: false + + /@types/is-hotkey@0.1.10: + resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==} + dev: false + + /@types/is-hotkey@0.1.7: + resolution: {integrity: sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==} + dev: true + + /@types/istanbul-lib-coverage@2.0.4: + resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} + + /@types/istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.4 + + /@types/istanbul-reports@3.0.1: + resolution: {integrity: sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==} + dependencies: + '@types/istanbul-lib-report': 3.0.0 + + /@types/jest@29.5.3: + resolution: {integrity: sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==} + dependencies: + expect: 29.5.0 + pretty-format: 29.5.0 + dev: true + + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 18.16.9 + '@types/tough-cookie': 4.0.2 + parse5: 7.1.2 + dev: true + + /@types/json-schema@7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/katex@0.16.0: + resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} + dev: true + + /@types/lodash-es@4.17.11: + resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==} + dependencies: + '@types/lodash': 4.14.194 + dev: true + + /@types/lodash@4.14.194: + resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==} + dev: true + + /@types/lodash@4.14.202: + resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} + dev: false + + /@types/node@18.16.9: + resolution: {integrity: sha512-IeB32oIV4oGArLrd7znD2rkHQ6EDCM+2Sr76dJnrHwv9OHBTTM6nuDLK9bmikXzPa0ZlWMWtRGo/Uw4mrzQedA==} + + /@types/parse-json@4.0.0: + resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: false + + /@types/prettier@2.7.2: + resolution: {integrity: sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==} + + /@types/prismjs@1.26.0: + resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} + dev: true + + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: false + + /@types/prop-types@15.7.5: + resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} + + /@types/quill@2.0.10: + resolution: {integrity: sha512-L6OHONEj2v4NRbWQOsn7j1N0SyzhRR3M4g1M6j/uuIwIsIW2ShWHhwbqNvH8hSmVktzqu0lITfdnqVOQ4qkrhA==} + dependencies: + parchment: 1.1.4 + quill-delta: 4.2.2 + dev: true + + /@types/react-beautiful-dnd@13.1.4: + resolution: {integrity: sha512-4bIBdzOr0aavN+88q3C7Pgz+xkb7tz3whORYrmSj77wfVEMfiWiooIwVWFR7KM2e+uGTe5BVrXqSfb0aHeflJA==} + dependencies: + '@types/react': 18.2.6 + dev: true + + /@types/react-color@3.0.6: + resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==} + dependencies: + '@types/react': 18.2.6 + '@types/reactcss': 1.2.6 + dev: true + + /@types/react-custom-scrollbars@4.0.13: + resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==} + dependencies: + '@types/react': 18.2.6 + dev: true + + /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} + dependencies: + '@popperjs/core': 2.11.8 + '@types/react': 18.2.6 + date-fns: 2.30.0 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react + - react-dom + dev: true + + /@types/react-dom@18.2.4: + resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} + dependencies: + '@types/react': 18.2.6 + + /@types/react-is@17.0.4: + resolution: {integrity: sha512-FLzd0K9pnaEvKz4D1vYxK9JmgQPiGk1lu23o1kqGsLeT0iPbRSF7b76+S5T9fD8aRa0B8bY7I/3DebEj+1ysBA==} + dependencies: + '@types/react': 17.0.59 + dev: false + + /@types/react-is@18.2.1: + resolution: {integrity: sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==} + dependencies: + '@types/react': 18.2.6 + dev: false + + /@types/react-katex@3.0.0: + resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==} + dependencies: + '@types/react': 18.2.6 + dev: true + + /@types/react-redux@7.1.25: + resolution: {integrity: sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.6 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + + /@types/react-swipeable-views@0.13.4: + resolution: {integrity: sha512-hQV9Oq6oa+9HKdnGd43xkckElwf5dThOiegtQxqE7qX761oHhxnZO07fz6IsKSnUy9J3tzlRQBu3sNyvC8+kYw==} + dependencies: + '@types/react': 18.2.6 + dev: false + + /@types/react-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.6 + + /@types/react-transition-group@4.4.9: + resolution: {integrity: sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==} + dependencies: + '@types/react': 18.2.6 + dev: false + + /@types/react-window@1.8.8: + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + dependencies: + '@types/react': 18.2.6 + + /@types/react@17.0.59: + resolution: {integrity: sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 + dev: false + + /@types/react@18.2.6: + resolution: {integrity: sha512-wRZClXn//zxCFW+ye/D2qY65UsYP1Fpex2YXorHc8awoNamkMZSvBxwxdYVInsHOZZd2Ppq8isnSzJL5Mpf8OA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.2 + + /@types/reactcss@1.2.6: + resolution: {integrity: sha512-qaIzpCuXNWomGR1Xq8SCFTtF4v8V27Y6f+b9+bzHiv087MylI/nTCqqdChNeWS7tslgROmYB7yeiruWX7WnqNg==} + dependencies: + '@types/react': 18.2.6 + dev: true + + /@types/scheduler@0.16.3: + resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} + + /@types/semver@7.5.0: + resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} + dev: true + + /@types/stack-utils@2.0.1: + resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} + + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: true + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: true + + /@types/tough-cookie@4.0.2: + resolution: {integrity: sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==} + dev: true + + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + + /@types/utf8@3.0.1: + resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==} + dev: true + + /@types/uuid@9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + + /@types/warning@3.0.3: + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + dev: false + + /@types/yargs-parser@21.0.0: + resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==} + + /@types/yargs@17.0.24: + resolution: {integrity: sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==} + dependencies: + '@types/yargs-parser': 21.0.0 + + /@typescript-eslint/eslint-plugin@5.59.5(@typescript-eslint/parser@5.59.5)(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.5.1 + '@typescript-eslint/parser': 5.59.5(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 5.59.5 + '@typescript-eslint/type-utils': 5.59.5(eslint@8.40.0)(typescript@4.9.5) + '@typescript-eslint/utils': 5.59.5(eslint@8.40.0)(typescript@4.9.5) + debug: 4.3.4 + eslint: 8.40.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + natural-compare-lite: 1.4.0 + semver: 7.5.1 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@5.59.5(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.59.5 + '@typescript-eslint/types': 5.59.5 + '@typescript-eslint/typescript-estree': 5.59.5(typescript@4.9.5) + debug: 4.3.4 + eslint: 8.40.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@5.59.5: + resolution: {integrity: sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.5 + '@typescript-eslint/visitor-keys': 5.59.5 + dev: true + + /@typescript-eslint/type-utils@5.59.5(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.59.5(typescript@4.9.5) + '@typescript-eslint/utils': 5.59.5(eslint@8.40.0)(typescript@4.9.5) + debug: 4.3.4 + eslint: 8.40.0 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@5.59.5: + resolution: {integrity: sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree@5.59.5(typescript@4.9.5): + resolution: {integrity: sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.59.5 + '@typescript-eslint/visitor-keys': 5.59.5 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.1 + tsutils: 3.21.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@5.59.5(eslint@8.40.0)(typescript@4.9.5): + resolution: {integrity: sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@types/json-schema': 7.0.11 + '@types/semver': 7.5.0 + '@typescript-eslint/scope-manager': 5.59.5 + '@typescript-eslint/types': 5.59.5 + '@typescript-eslint/typescript-estree': 5.59.5(typescript@4.9.5) + eslint: 8.40.0 + eslint-scope: 5.1.1 + semver: 7.5.1 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@5.59.5: + resolution: {integrity: sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.59.5 + eslint-visitor-keys: 3.4.1 + dev: true + + /@vitejs/plugin-react@3.1.0(vite@4.3.5): + resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.1.0-beta.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.8) + magic-string: 0.27.0 + react-refresh: 0.14.0 + vite: 4.3.5(@types/node@18.16.9)(sass@1.70.0) + transitivePeerDependencies: + - supports-color + dev: true + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + dev: true + + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.8.2 + acorn-walk: 8.2.0 + dev: true + + /acorn-jsx@5.3.2(acorn@8.8.2): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.2 + dev: true + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.2 + is-array-buffer: 3.0.2 + dev: true + + /array-includes@3.1.6: + resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.1 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flatmap@1.3.1: + resolution: {integrity: sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + dev: true + + /array.prototype.tosorted@1.1.1: + resolution: {integrity: sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + es-shim-unscopables: 1.0.0 + get-intrinsic: 1.2.1 + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /autoprefixer@10.4.14(postcss@8.4.23): + resolution: {integrity: sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.21.5 + caniuse-lite: 1.0.30001487 + fraction.js: 4.2.0 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.23 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + + /babel-jest@29.6.2(@babel/core@7.21.8): + resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.21.8 + '@jest/transform': 29.6.4 + '@types/babel__core': 7.20.0 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.5.0(@babel/core@7.21.8) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.21.5 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-jest-hoist@29.5.0: + resolution: {integrity: sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.20.7 + '@babel/types': 7.21.5 + '@types/babel__core': 7.20.0 + '@types/babel__traverse': 7.18.5 + + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.23.4 + cosmiconfig: 7.1.0 + resolve: 1.22.2 + dev: false + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.21.8): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.21.8) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.21.8) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.21.8) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.21.8) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.21.8) + + /babel-preset-jest@29.5.0(@babel/core@7.21.8): + resolution: {integrity: sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.21.8 + babel-plugin-jest-hoist: 29.5.0 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.8) + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browserslist@4.21.5: + resolution: {integrity: sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001487 + electron-to-chromium: 1.4.394 + node-releases: 2.0.10 + update-browserslist-db: 1.0.11(browserslist@4.21.5) + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.5.0 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-lite@1.0.30001487: + resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==} + + /capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case-first: 2.0.2 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + + /ci-info@3.8.0: + resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} + engines: {node: '>=8'} + + /cjs-module-lexer@1.2.2: + resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} + + /classnames@2.3.2: + resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + /collect-v8-coverage@1.0.1: + resolution: {integrity: sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==} + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + dev: true + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case: 2.0.2 + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.0 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.1 + dev: false + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.0.2 + dev: true + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.0.2 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype@3.1.2: + resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /date-arithmetic@4.1.0: + resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==} + dev: false + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.23.4 + + /dayjs@1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + + /dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + + /deep-equal@1.1.1: + resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.5 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.0 + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /define-properties@1.2.0: + resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: false + + /derive-valtio@0.1.0(valtio@1.12.1): + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + dependencies: + valtio: 1.12.1(@types/react@18.2.6)(react@18.2.0) + dev: false + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences@29.4.3: + resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /direction@1.0.4: + resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==} + hasBin: true + dev: false + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + dev: false + + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.23.4 + csstype: 3.1.2 + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: true + + /electron-to-chromium@1.4.394: + resolution: {integrity: sha512-0IbC2cfr8w5LxTz+nmn2cJTGafsK9iauV2r5A5scfzyovqLrxuLoxOHE5OBobP3oVIggJT+0JfKnw9sm87c8Hw==} + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + /emoji-mart@5.5.2: + resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + dev: false + + /emoji-regex@10.2.1: + resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.21.2: + resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + es-set-tostringtag: 2.0.1 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.2.1 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-proto: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.10 + is-weakref: 1.0.2 + object-inspect: 1.12.3 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.0 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.7 + string.prototype.trimend: 1.0.6 + string.prototype.trimstart: 1.0.6 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.9 + dev: true + + /es-set-tostringtag@2.0.1: + resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + has-tostringtag: 1.0.0 + dev: true + + /es-shim-unscopables@1.0.0: + resolution: {integrity: sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==} + dependencies: + has: 1.0.3 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.17.19: + resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.17.19 + '@esbuild/android-arm64': 0.17.19 + '@esbuild/android-x64': 0.17.19 + '@esbuild/darwin-arm64': 0.17.19 + '@esbuild/darwin-x64': 0.17.19 + '@esbuild/freebsd-arm64': 0.17.19 + '@esbuild/freebsd-x64': 0.17.19 + '@esbuild/linux-arm': 0.17.19 + '@esbuild/linux-arm64': 0.17.19 + '@esbuild/linux-ia32': 0.17.19 + '@esbuild/linux-loong64': 0.17.19 + '@esbuild/linux-mips64el': 0.17.19 + '@esbuild/linux-ppc64': 0.17.19 + '@esbuild/linux-riscv64': 0.17.19 + '@esbuild/linux-s390x': 0.17.19 + '@esbuild/linux-x64': 0.17.19 + '@esbuild/netbsd-x64': 0.17.19 + '@esbuild/openbsd-x64': 0.17.19 + '@esbuild/sunos-x64': 0.17.19 + '@esbuild/win32-arm64': 0.17.19 + '@esbuild/win32-ia32': 0.17.19 + '@esbuild/win32-x64': 0.17.19 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.40.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.40.0 + dev: true + + /eslint-plugin-react@7.32.2(eslint@8.40.0): + resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.6 + array.prototype.flatmap: 1.3.1 + array.prototype.tosorted: 1.1.1 + doctrine: 2.1.0 + eslint: 8.40.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.3 + minimatch: 3.1.2 + object.entries: 1.1.6 + object.fromentries: 2.0.6 + object.hasown: 1.1.2 + object.values: 1.1.6 + prop-types: 15.8.1 + resolve: 2.0.0-next.4 + semver: 6.3.0 + string.prototype.matchall: 4.0.8 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.0: + resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.1: + resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.40.0: + resolution: {integrity: sha512-bvR+TsP9EHL3TqNtj9sCNJVAFK3fBN8Q7g5waghxyRsPLIMwL73XSKnZFK0hk/O2ANC+iAoq6PWMQ+IfBAJIiQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.40.0) + '@eslint-community/regexpp': 4.5.1 + '@eslint/eslintrc': 2.0.3 + '@eslint/js': 8.40.0 + '@humanwhocodes/config-array': 0.11.8 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.0 + eslint-visitor-keys: 3.4.1 + espree: 9.5.2 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.20.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.4 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.4.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.5.2: + resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.2 + acorn-jsx: 5.3.2(acorn@8.8.2) + eslint-visitor-keys: 3.4.1 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter3@2.0.3: + resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + /expect@29.5.0: + resolution: {integrity: sha512-yM7xqUrCO2JdpFo4XpM82t+PJBFybdqoQuJLDGeDX2ij8NZzqRHyu3Hp188/JX7SWqud+7t4MUdvcgGBICMHZg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.1.2: + resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} + dev: false + + /fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: false + + /fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /fraction.js@4.2.0: + resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==} + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /function.prototype.name@1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + dev: false + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + /globals@13.20.0: + resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.0 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.4 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /goober@2.1.13(csstype@3.1.2): + resolution: {integrity: sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.2 + dev: false + + /google-protobuf@3.21.2: + resolution: {integrity: sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==} + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /grapheme-splitter@1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + dependencies: + capital-case: 1.0.4 + tslib: 2.5.0 + dev: true + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + /i18next-browser-languagedetector@7.0.1: + resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /i18next-resources-to-backend@1.1.4: + resolution: {integrity: sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /i18next@22.4.15: + resolution: {integrity: sha512-yYudtbFrrmWKLEhl6jvKUYyYunj4bTBCe2qIUYAxbXoPusY7YmdwPvOE6fx6UIfWvmlbCWDItr7wIs8KEBZ5Zg==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + dev: true + + /immer@10.0.3: + resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==} + dev: false + + /immutable@4.3.4: + resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.5: + resolution: {integrity: sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-typed-array: 1.1.10 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.12.0: + resolution: {integrity: sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==} + dependencies: + has: 1.0.3 + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + dev: false + + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.10: + resolution: {integrity: sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + + /istanbul-lib-coverage@3.2.0: + resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} + engines: {node: '>=8'} + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.21.8 + '@babel/parser': 7.21.8 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-report@3.0.0: + resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} + engines: {node: '>=8'} + dependencies: + istanbul-lib-coverage: 3.2.0 + make-dir: 3.1.0 + supports-color: 7.2.0 + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + /istanbul-reports@3.1.5: + resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.0 + + /jest-changed-files@29.5.0: + resolution: {integrity: sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + p-limit: 3.1.0 + + /jest-circus@29.5.0: + resolution: {integrity: sha512-gq/ongqeQKAplVxqJmbeUOJJKkW3dDNPY8PjhJ5G0lBRvu0e3EWGxGy5cI4LAGA7gV2UHCtWBI4EMXK8c9nQKA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/expect': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.9 + chalk: 4.1.2 + co: 4.6.0 + dedent: 0.7.0 + is-generator-fn: 2.1.0 + jest-each: 29.5.0 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.6.3 + jest-runtime: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.6.3 + p-limit: 3.1.0 + pretty-format: 29.5.0 + pure-rand: 6.0.2 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - supports-color + + /jest-cli@29.5.0(@types/node@18.16.9): + resolution: {integrity: sha512-L1KcP1l4HtfwdxXNFCL5bmUbLQiKrakMUriBEcc1Vfz6gx31ORKdreuWvmQVBit+1ss9NNR3yxjwfwzZNdQXJw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.1.0 + jest-config: 29.5.0(@types/node@18.16.9) + jest-util: 29.5.0 + jest-validate: 29.5.0 + prompts: 2.4.2 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + + /jest-config@29.5.0(@types/node@18.16.9): + resolution: {integrity: sha512-kvDUKBnNJPNBmFFOhDbm59iu1Fii1Q6SxyhXfvylq3UTHbg6o7j/g8k2dZyXWLvfdKB1vAPxNZnMgtKJcmu3kA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.21.8 + '@jest/test-sequencer': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + babel-jest: 29.6.2(@babel/core@7.21.8) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.5.0 + jest-environment-node: 29.5.0 + jest-get-type: 29.4.3 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-runner: 29.5.0 + jest-util: 29.5.0 + jest-validate: 29.5.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.5.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + /jest-diff@29.5.0: + resolution: {integrity: sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.4.3 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + + /jest-docblock@29.4.3: + resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + + /jest-each@29.5.0: + resolution: {integrity: sha512-HM5kIJ1BTnVt+DQZ2ALp3rzXEl+g726csObrW/jpEGl+CDSSQpOJJX2KE/vEg8cxcMXdyEPu6U4QX5eruQv5hA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.4.3 + jest-util: 29.6.3 + pretty-format: 29.5.0 + + /jest-environment-jsdom@29.6.2: + resolution: {integrity: sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.6.4 + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 18.16.9 + jest-mock: 29.6.3 + jest-util: 29.6.3 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jest-environment-node@29.5.0: + resolution: {integrity: sha512-ExxuIK/+yQ+6PRGaHkKewYtg6hto2uGCgvKdb2nfJfKXgZ17DfXjvbZ+jA1Qt9A8EQSfPnt5FKIfnOO3u1h9qw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.6.4 + '@jest/fake-timers': 29.6.4 + '@jest/types': 29.6.3 + '@types/node': 18.16.9 + jest-mock: 29.6.3 + jest-util: 29.6.3 + + /jest-get-type@29.4.3: + resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map@29.5.0: + resolution: {integrity: sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.16.9 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.4.3 + jest-util: 29.5.0 + jest-worker: 29.5.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + + /jest-haste-map@29.6.4: + resolution: {integrity: sha512-12Ad+VNTDHxKf7k+M65sviyynRoZYuL1/GTuhEVb8RYsNSNln71nANRb/faSyWvx0j+gHcivChXHIoMJrGYjog==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.6 + '@types/node': 18.16.9 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.6.3 + jest-worker: 29.6.4 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.2 + + /jest-leak-detector@29.5.0: + resolution: {integrity: sha512-u9YdeeVnghBUtpN5mVxjID7KbkKE1QU4f6uUwuxiY0vYRi9BUCLKlPEZfDGR67ofdFmDz9oPAy2G92Ujrntmow==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + + /jest-matcher-utils@29.5.0: + resolution: {integrity: sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + pretty-format: 29.5.0 + + /jest-message-util@29.5.0: + resolution: {integrity: sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.21.4 + '@jest/types': 29.5.0 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.5.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + /jest-message-util@29.6.3: + resolution: {integrity: sha512-FtzaEEHzjDpQp51HX4UMkPZjy46ati4T5pEMyM6Ik48ztu4T9LQplZ6OsimHx7EuM9dfEh5HJa6D3trEftu3dA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.21.4 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.6.3 + slash: 3.0.0 + stack-utils: 2.0.6 + + /jest-mock@29.5.0: + resolution: {integrity: sha512-GqOzvdWDE4fAV2bWQLQCkujxYWL7RxjCnj71b5VhDAGOevB3qj3Ovg26A5NI84ZpODxyzaozXLOh2NCgkbvyaw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + jest-util: 29.5.0 + + /jest-mock@29.6.3: + resolution: {integrity: sha512-Z7Gs/mOyTSR4yPsaZ72a/MtuK6RnC3JYqWONe48oLaoEcYwEDxqvbXz85G4SJrm2Z5Ar9zp6MiHF4AlFlRM4Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.16.9 + jest-util: 29.6.3 + + /jest-pnp-resolver@1.2.3(jest-resolve@29.5.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.5.0 + + /jest-regex-util@29.4.3: + resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-resolve-dependencies@29.5.0: + resolution: {integrity: sha512-sjV3GFr0hDJMBpYeUuGduP+YeCRbd7S/ck6IvL3kQ9cpySYKqcqhdLLC2rFwrcL7tz5vYibomBrsFYWkIGGjOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.4.3 + jest-snapshot: 29.5.0 + transitivePeerDependencies: + - supports-color + + /jest-resolve@29.5.0: + resolution: {integrity: sha512-1TzxJ37FQq7J10jPtQjcc+MkCkE3GBpBecsSUWJ0qZNJpmg6m0D9/7II03yJulm3H/fvVjgqLh/k2eYg+ui52w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.5.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.5.0) + jest-util: 29.5.0 + jest-validate: 29.5.0 + resolve: 1.22.2 + resolve.exports: 2.0.2 + slash: 3.0.0 + + /jest-runner@29.5.0: + resolution: {integrity: sha512-m7b6ypERhFghJsslMLhydaXBiLf7+jXy8FwGRHO3BGV1mcQpPbwiqiKUR2zU2NJuNeMenJmlFZCsIqzJCTeGLQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.5.0 + '@jest/environment': 29.5.0 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.4.3 + jest-environment-node: 29.5.0 + jest-haste-map: 29.5.0 + jest-leak-detector: 29.5.0 + jest-message-util: 29.5.0 + jest-resolve: 29.5.0 + jest-runtime: 29.5.0 + jest-util: 29.5.0 + jest-watcher: 29.5.0 + jest-worker: 29.5.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + /jest-runtime@29.5.0: + resolution: {integrity: sha512-1Hr6Hh7bAgXQP+pln3homOiEZtCDZFqwmle7Ew2j8OlbkIu6uE3Y/etJQG8MLQs3Zy90xrp2C0BRrtPHG4zryw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.5.0 + '@jest/fake-timers': 29.5.0 + '@jest/globals': 29.5.0 + '@jest/source-map': 29.4.3 + '@jest/test-result': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.5.0 + jest-message-util: 29.5.0 + jest-mock: 29.5.0 + jest-regex-util: 29.4.3 + jest-resolve: 29.5.0 + jest-snapshot: 29.5.0 + jest-util: 29.5.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + /jest-snapshot@29.5.0: + resolution: {integrity: sha512-x7Wolra5V0tt3wRs3/ts3S6ciSQVypgGQlJpz2rsdQYoUKxMxPNaoHMGJN6qAuPJqS+2iQ1ZUn5kl7HCyls84g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.21.8 + '@babel/generator': 7.21.5 + '@babel/plugin-syntax-jsx': 7.21.4(@babel/core@7.21.8) + '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.21.8) + '@babel/traverse': 7.23.7 + '@babel/types': 7.21.5 + '@jest/expect-utils': 29.5.0 + '@jest/transform': 29.5.0 + '@jest/types': 29.5.0 + '@types/babel__traverse': 7.18.5 + '@types/prettier': 2.7.2 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.21.8) + chalk: 4.1.2 + expect: 29.5.0 + graceful-fs: 4.2.11 + jest-diff: 29.5.0 + jest-get-type: 29.4.3 + jest-matcher-utils: 29.5.0 + jest-message-util: 29.5.0 + jest-util: 29.5.0 + natural-compare: 1.4.0 + pretty-format: 29.5.0 + semver: 7.5.1 + transitivePeerDependencies: + - supports-color + + /jest-util@29.5.0: + resolution: {integrity: sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + /jest-util@29.6.3: + resolution: {integrity: sha512-QUjna/xSy4B32fzcKTSz1w7YYzgiHrjjJjevdRf61HYk998R5vVMMNmrHESYZVDS5DSWs+1srPLPKxXPkeSDOA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.16.9 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + /jest-validate@29.5.0: + resolution: {integrity: sha512-pC26etNIi+y3HV8A+tUGr/lph9B18GnzSRAkPaaZJIE1eFdiYm6/CewuiJQ8/RlfHd1u/8Ioi8/sJ+CmbA+zAQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.5.0 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.4.3 + leven: 3.1.0 + pretty-format: 29.5.0 + + /jest-watcher@29.5.0: + resolution: {integrity: sha512-KmTojKcapuqYrKDpRwfqcQ3zjMlwu27SYext9pt4GlF5FUgB+7XE1mcCnSm6a4uUpFyQIkb6ZhzZvHl+jiBCiA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.5.0 + '@jest/types': 29.5.0 + '@types/node': 18.16.9 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.5.0 + string-length: 4.0.2 + + /jest-worker@29.5.0: + resolution: {integrity: sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.16.9 + jest-util: 29.5.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /jest-worker@29.6.4: + resolution: {integrity: sha512-6dpvFV4WjcWbDVGgHTWo/aupl8/LbBx2NSKfiwqf79xC/yeJjKHT1+StcKy/2KTmW16hE68ccKVOtXf+WZGz7Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 18.16.9 + jest-util: 29.6.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /jest@29.5.0(@types/node@18.16.9): + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.5.0 + '@jest/types': 29.5.0 + import-local: 3.1.0 + jest-cli: 29.5.0(@types/node@18.16.9) + transitivePeerDependencies: + - '@types/node' + - supports-color + - ts-node + + /jiti@1.18.2: + resolution: {integrity: sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==} + hasBin: true + dev: true + + /js-base64@3.7.5: + resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + dev: false + + /js-sdsl@4.4.0: + resolution: {integrity: sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.8.2 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.14.1 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + /jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsx-ast-utils@3.3.3: + resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.6 + object.assign: 4.1.4 + dev: true + + /katex@0.16.7: + resolution: {integrity: sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /keycode@2.2.1: + resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} + dev: false + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lib0@0.2.74: + resolution: {integrity: sha512-roj9i46/JwG5ik5KNTkxP2IytlnrssAkD/OhlAVtE+GqectrdkfR+pttszVLrOzMDeXNs1MPt6yo66MUolWSiA==} + engines: {node: '>=14'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + + /lib0@0.2.88: + resolution: {integrity: sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.5.0 + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + + /material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + dev: false + + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + + /memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /moment-timezone@0.5.44: + resolution: {integrity: sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw==} + dependencies: + moment: 2.30.1 + dev: false + + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.6: + resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + + /natural-compare-lite@1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.5.0 + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + /node-releases@2.0.10: + resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.6: + resolution: {integrity: sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /object.fromentries@2.0.6: + resolution: {integrity: sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /object.hasown@1.1.2: + resolution: {integrity: sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==} + dependencies: + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /object.values@1.1.6: + resolution: {integrity: sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /optionator@0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /parchment@1.1.4: + resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.21.4 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.5: + resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} + engines: {node: '>= 6'} + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /postcss-import@15.1.0(postcss@8.4.23): + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.23 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.2 + dev: true + + /postcss-js@4.0.1(postcss@8.4.23): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.23 + dev: true + + /postcss-load-config@4.0.1(postcss@8.4.23): + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.23 + yaml: 2.2.2 + dev: true + + /postcss-nested@6.0.1(postcss@8.4.23): + resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.23 + postcss-selector-parser: 6.0.12 + dev: true + + /postcss-selector-parser@6.0.12: + resolution: {integrity: sha512-NdxGCAZdRrwVI1sy59+Wzrh+pMMHxapGnpfenDVlMEXoOcvt4pGE0JLK9YY2F5dLxcFYA/YbVQKhcGU+FtSYQg==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.23: + resolution: {integrity: sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-plugin-tailwindcss@0.2.8(prettier@2.8.4): + resolution: {integrity: sha512-KgPcEnJeIijlMjsA6WwYgRs5rh3/q76oInqtMXBA/EMcamrcYJpyhtRhyX1ayT9hnHlHTuO8sIifHF10WuSDKg==} + engines: {node: '>=12.17.0'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@shufo/prettier-plugin-blade': '*' + '@trivago/prettier-plugin-sort-imports': '*' + prettier: '>=2.2.0' + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + prettier-plugin-twig-melody: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@shufo/prettier-plugin-blade': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier-plugin-twig-melody: + optional: true + dependencies: + prettier: 2.8.4 + dev: true + + /prettier@2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@29.5.0: + resolution: {integrity: sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.4.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /pretty-format@29.6.3: + resolution: {integrity: sha512-ZsBgjVhFAj5KeK+nHfF1305/By3lechHQSMWCTl8iHSbfOm2TN5nHEtFc/+W7fAyUeCs2n5iow72gld4gW0xDw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /protoc-gen-ts@0.8.7: + resolution: {integrity: sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==} + hasBin: true + dev: false + + /proxy-compare@2.5.1: + resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} + dev: false + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode@2.3.0: + resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} + engines: {node: '>=6'} + dev: true + + /pure-rand@6.0.2: + resolution: {integrity: sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==} + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quill-delta@3.6.3: + resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} + engines: {node: '>=0.10'} + dependencies: + deep-equal: 1.1.1 + extend: 3.0.2 + fast-diff: 1.1.2 + dev: false + + /quill-delta@4.2.2: + resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==} + dependencies: + fast-diff: 1.2.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: true + + /quill-delta@5.1.0: + resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==} + engines: {node: '>= 12.0.0'} + dependencies: + fast-diff: 1.3.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: false + + /quill@1.3.7: + resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==} + dependencies: + clone: 2.1.2 + deep-equal: 1.1.1 + eventemitter3: 2.0.3 + extend: 3.0.2 + parchment: 1.1.4 + quill-delta: 3.6.3 + dev: false + + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.5 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + + /react-big-calendar@1.8.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cra8WPfoTSQthFfqxi0k9xm/Shv5jWSw19LkNzpSJcnQhP6XGes/eJjd8P8g/iwaJjXIWPpg3+HB5wO5wabRyA==} + peerDependencies: + react: ^16.14.0 || ^17 || ^18 + react-dom: ^16.14.0 || ^17 || ^18 + dependencies: + '@babel/runtime': 7.23.4 + clsx: 1.2.1 + date-arithmetic: 4.1.0 + dayjs: 1.11.9 + dom-helpers: 5.2.1 + globalize: 0.1.1 + invariant: 2.2.4 + lodash: 4.17.21 + lodash-es: 4.17.21 + luxon: 3.4.4 + memoize-one: 6.0.0 + moment: 2.30.1 + moment-timezone: 0.5.44 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-overlays: 5.2.1(react-dom@18.2.0)(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + dev: false + + /react-color@2.19.3(react@18.2.0): + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + dependencies: + '@icons/material': 0.2.4(react@18.2.0) + lodash: 4.17.21 + lodash-es: 4.17.21 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 18.2.0 + reactcss: 1.2.3(react@18.2.0) + tinycolor2: 1.6.0 + dev: false + + /react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-error-boundary@3.1.4(react@18.2.0): + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.21.5 + react: 18.2.0 + dev: false + + /react-event-listener@0.6.6(react@18.2.0): + resolution: {integrity: sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==} + peerDependencies: + react: ^16.3.0 + dependencies: + '@babel/runtime': 7.23.4 + prop-types: 15.8.1 + react: 18.2.0 + warning: 4.0.3 + dev: false + + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + /react-hot-toast@2.4.1(csstype@3.1.2)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.13(csstype@3.1.2) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + + /react-i18next@12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.21.5 + html-parse-stringify: 3.0.1 + i18next: 22.4.15 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + /react-katex@3.0.1(prop-types@15.8.1)(react@18.2.0): + resolution: {integrity: sha512-wIUW1fU5dHlkKvq4POfDkHruQsYp3fM8xNb/jnc8dnQ+nNCnaj0sx5pw7E6UyuEdLRyFKK0HZjmXBo+AtXXy0A==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=15.3.2 <=18' + dependencies: + katex: 0.16.7 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-overlays@5.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + dependencies: + '@babel/runtime': 7.23.4 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.15(react@18.2.0) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.4 + '@types/react-redux': 7.1.25 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + + /react-redux@8.0.5(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): + resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} + peerDependencies: + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@babel/runtime': 7.21.5 + '@types/hoist-non-react-statics': 3.3.1 + '@types/react': 18.2.6 + '@types/react-dom': 18.2.4 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + redux: 4.2.1 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-router-dom@6.11.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-dPC2MhoPeTQ1YUOt5uIK376SMNWbwUxYRWk2ZmTT4fZfwlOvabF8uduRKKJIyfkCZvMgiF0GSCQckmkGGijIrg==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.6.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.11.1(react@18.2.0) + dev: false + + /react-router@6.11.1(react@18.2.0): + resolution: {integrity: sha512-OZINSdjJ2WgvAi7hgNLazrEV8SGn6xrKA+MkJe9wVDMZ3zQ6fdJocUjpCUCI0cNrelWjcvon0S/QK/j0NzL3KA==} + engines: {node: '>=14'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.6.1 + react: 18.2.0 + dev: false + + /react-swipeable-views-core@0.14.0: + resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + warning: 4.0.3 + dev: false + + /react-swipeable-views-utils@0.14.0(react@18.2.0): + resolution: {integrity: sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + keycode: 2.2.1 + prop-types: 15.8.1 + react-event-listener: 0.6.6(react@18.2.0) + react-swipeable-views-core: 0.14.0 + shallow-equal: 1.2.1 + transitivePeerDependencies: + - react + dev: false + + /react-swipeable-views@0.14.0(react@18.2.0): + resolution: {integrity: sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-swipeable-views-core: 0.14.0 + react-swipeable-views-utils: 0.14.0(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.21.5 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-virtualized-auto-sizer@1.0.20(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-vtree@2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0): + resolution: {integrity: sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==} + peerDependencies: + '@types/react-window': ^1.8.2 + react: ^16.13.1 + react-dom: ^16.13.1 + react-window: ^1.8.5 + dependencies: + '@babel/runtime': 7.23.4 + '@types/react-window': 1.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.4 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react18-input-otp@1.1.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-55dZMVX61In2ngUhA4Fv0NMY4j5RZjxrJaSOAnJGJmkAhxKB6puVHYEmipyy2+W2CPydFF7pv+0NKzPUA03EVg==} + peerDependencies: + react: 16.2.0 - 18 + react-dom: 16.2.0 - 18 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /reactcss@1.2.3(react@18.2.0): + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + dependencies: + lodash: 4.17.21 + react: 18.2.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /redux-thunk@3.1.0(redux@5.0.0): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.0 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.21.5 + dev: false + + /redux@5.0.0: + resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==} + dev: false + + /regenerator-runtime@0.12.1: + resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} + dev: false + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + + /regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + + /regexp.prototype.flags@1.5.0: + resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + functions-have-names: 1.2.3 + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /reselect@5.0.1: + resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + /resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true + dependencies: + is-core-module: 2.12.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.4: + resolution: {integrity: sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==} + hasBin: true + dependencies: + is-core-module: 2.12.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup@3.21.7: + resolution: {integrity: sha512-KXPaEuR8FfUoK2uHwNjxTmJ18ApyvD6zJpYv9FOJSqLStmt6xOY84l1IjK2dSolQmoXknrhEFRaPRgOPdqCT5w==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.5.0 + dev: false + + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + is-regex: 1.1.4 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sass@1.70.0: + resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.5.3 + immutable: 4.3.4 + source-map-js: 1.0.2 + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + + /scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + dependencies: + compute-scroll-into-view: 3.1.0 + dev: false + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + dependencies: + no-case: 3.0.4 + tslib: 2.5.0 + upper-case-first: 2.0.2 + dev: true + + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slate-history@0.100.0(slate@0.101.4): + resolution: {integrity: sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==} + peerDependencies: + slate: '>=0.65.3' + dependencies: + is-plain-object: 5.0.0 + slate: 0.101.4 + dev: false + + /slate-react@0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4): + resolution: {integrity: sha512-KMXK9FLeS7HYhhoVcI8SUi4Qp1I9C1lTQ2EgbPH95sVXfH/vq+hbhurEGIGCe0VQ9Opj4rSKJIv/g7De1+nJMA==} + peerDependencies: + react: '>=18.2.0' + react-dom: '>=18.2.0' + slate: '>=0.99.0' + dependencies: + '@juggle/resize-observer': 3.4.0 + '@types/is-hotkey': 0.1.10 + '@types/lodash': 4.14.202 + direction: 1.0.4 + is-hotkey: 0.2.0 + is-plain-object: 5.0.0 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + slate: 0.101.4 + tiny-invariant: 1.3.1 + dev: false + + /slate@0.101.4: + resolution: {integrity: sha512-8LazZrNDsYFKDg1wpb0HouAfX5Pw/UmOZ/vIrtqD2GSCDZvraOkV2nVJ9Ery8kIlsU1jeybwgcaCy4KkVwfvEg==} + dependencies: + immer: 10.0.3 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + dev: false + + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.5.0 + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string.prototype.matchall@4.0.8: + resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + get-intrinsic: 1.2.1 + has-symbols: 1.0.3 + internal-slot: 1.0.5 + regexp.prototype.flags: 1.5.0 + side-channel: 1.0.4 + dev: true + + /string.prototype.trim@1.2.7: + resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimend@1.0.6: + resolution: {integrity: sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /string.prototype.trimstart@1.0.6: + resolution: {integrity: sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.0 + es-abstract: 1.21.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /style-dictionary@3.8.0: + resolution: {integrity: sha512-wHlB/f5eO3mDcYv6WtOz6gvQC477jBKrwuIXe+PtHskTCBsJdAOvL8hCquczJxDui2TnwpeNE+2msK91JJomZg==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + change-case: 4.1.2 + commander: 8.3.0 + fs-extra: 10.1.0 + glob: 7.2.3 + json5: 2.2.3 + jsonc-parser: 3.2.0 + lodash: 4.17.21 + tinycolor2: 1.6.0 + dev: true + + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + + /sucrase@3.32.0: + resolution: {integrity: sha512-ydQOU34rpSyj2TGyz4D2p8rbktIOZ8QY9s+DGLvFU1i5pWJE8vkpruCjGCMHsdXwnD7JDcS+noSwM/a7zyNFDQ==} + engines: {node: '>=8'} + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + commander: 4.1.1 + glob: 7.1.6 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.5 + ts-interface-checker: 0.1.13 + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: true + + /svgo@3.0.2: + resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + csso: 5.0.5 + picocolors: 1.0.0 + dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tailwindcss@3.3.2: + resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.5.3 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.2.12 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.18.2 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.23 + postcss-import: 15.1.0(postcss@8.4.23) + postcss-js: 4.0.1(postcss@8.4.23) + postcss-load-config: 4.0.1(postcss@8.4.23) + postcss-nested: 6.0.1(postcss@8.4.23) + postcss-selector-parser: 6.0.12 + postcss-value-parser: 4.2.0 + resolve: 1.22.2 + sucrase: 3.32.0 + transitivePeerDependencies: + - ts-node + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + dependencies: + any-promise: 1.3.0 + dev: true + + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + /to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + dependencies: + to-space-case: 1.0.0 + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.0 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.0 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + dev: true + + /ts-jest@29.1.1(@babel/core@7.21.8)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.21.8 + babel-jest: 29.6.2(@babel/core@7.21.8) + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@18.16.9) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + + /ts-node-dev@2.0.0(@types/node@18.16.9)(typescript@4.9.5): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.5.3 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.2 + rimraf: 2.7.1 + source-map-support: 0.5.13 + tree-kill: 1.2.2 + ts-node: 10.9.1(@types/node@18.16.9)(typescript@4.9.5) + tsconfig: 7.0.0 + typescript: 4.9.5 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: true + + /ts-node@10.9.1(@types/node@18.16.9)(typescript@4.9.5): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.16.9 + acorn: 8.8.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /ts-results@3.3.0: + resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} + dev: false + + /tsconfig-paths-jest@0.0.1: + resolution: {integrity: sha512-YKhUKqbteklNppC2NqL7dv1cWF8eEobgHVD5kjF1y9Q4ocqpBiaDlYslQ9eMhtbqIPRrA68RIEXqknEjlxdwxw==} + dev: true + + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: true + + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib@2.5.0: + resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + + /tsutils@3.21.0(typescript@4.9.5): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.5 + dev: true + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.2 + for-each: 0.3.3 + is-typed-array: 1.1.10 + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uncontrollable@7.2.1(react@18.2.0): + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + dependencies: + '@babel/runtime': 7.23.4 + '@types/react': 18.2.6 + invariant: 2.2.4 + react: 18.2.0 + react-lifecycles-compat: 3.0.4 + dev: false + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /unsplash-js@7.0.19: + resolution: {integrity: sha512-j6qT2floy5Q2g2d939FJpwey1yw/GpQecFiSouyJtsHQPj3oqmqq3K4rI+GF8vU1zwGCT7ZwIGQd2dtCQLjYJw==} + engines: {node: '>=10'} + dev: false + + /update-browserslist-db@1.0.11(browserslist@4.21.5): + resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.21.5 + escalade: 3.1.1 + picocolors: 1.0.0 + + /upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + dependencies: + tslib: 2.5.0 + dev: true + + /upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + dependencies: + tslib: 2.5.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.0 + dev: true + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /v8-to-istanbul@9.1.0: + resolution: {integrity: sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.18 + '@types/istanbul-lib-coverage': 2.0.4 + convert-source-map: 1.9.0 + + /valtio@1.12.1(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.6 + derive-valtio: 0.1.0(valtio@1.12.1) + proxy-compare: 2.5.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /vite-plugin-svgr@3.2.0(vite@4.3.5): + resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==} + peerDependencies: + vite: ^2.6.0 || 3 || 4 + dependencies: + '@rollup/pluginutils': 5.0.2 + '@svgr/core': 7.0.0 + '@svgr/plugin-jsx': 7.0.0 + vite: 4.3.5(@types/node@18.16.9)(sass@1.70.0) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /vite@4.3.5(@types/node@18.16.9)(sass@1.70.0): + resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.16.9 + esbuild: 0.17.19 + postcss: 8.4.23 + rollup: 3.21.7 + sass: 1.70.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.9: + resolution: {integrity: sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + is-typed-array: 1.1.10 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /word-wrap@1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + /ws@8.14.1: + resolution: {integrity: sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y-protocols@1.0.6(yjs@13.6.1): + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.88 + yjs: 13.6.1 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + + /yaml@2.2.2: + resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} + engines: {node: '>= 14'} + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yjs@13.6.1: + resolution: {integrity: sha512-IyyHL+/v9N2S4YLSjGHMa0vMAfFxq8RDG5Nvb77raTTHJPweU3L/fRlqw6ElZvZUuHWnax3ufHR0Tx0ntfG63Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.74 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/appflowy_tauri/postcss.config.cjs b/frontend/appflowy_tauri/postcss.config.cjs new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/frontend/appflowy_tauri/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt b/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt new file mode 100644 index 0000000000..246c977c9f --- /dev/null +++ b/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf new file mode 100644 index 0000000000..71c0f995ee Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf new file mode 100644 index 0000000000..7aeb58bd1b Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf new file mode 100644 index 0000000000..00559eeb29 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf new file mode 100644 index 0000000000..e61e8e88bd Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf new file mode 100644 index 0000000000..df7093608a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..14d2b375dc Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf new file mode 100644 index 0000000000..e76ec69a65 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf new file mode 100644 index 0000000000..89513d9469 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf new file mode 100644 index 0000000000..12b7b3c40b Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf new file mode 100644 index 0000000000..bc36bcc242 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf new file mode 100644 index 0000000000..9e70be6a9e Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf new file mode 100644 index 0000000000..6bcdcc27f2 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf new file mode 100644 index 0000000000..be67410fd0 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf new file mode 100644 index 0000000000..9f0c71b70a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf new file mode 100644 index 0000000000..74c726e327 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf new file mode 100644 index 0000000000..3e6c942233 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf new file mode 100644 index 0000000000..03e736613a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf new file mode 100644 index 0000000000..e26db5dd3d Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt new file mode 100644 index 0000000000..75b52484ea --- /dev/null +++ b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf new file mode 100644 index 0000000000..61e5303325 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf new file mode 100644 index 0000000000..6df2b25360 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf differ diff --git a/frontend/appflowy_tauri/public/launch_splash.jpg b/frontend/appflowy_tauri/public/launch_splash.jpg new file mode 100644 index 0000000000..7e3bb9cee6 Binary files /dev/null and b/frontend/appflowy_tauri/public/launch_splash.jpg differ diff --git a/frontend/appflowy_tauri/public/tauri.svg b/frontend/appflowy_tauri/public/tauri.svg new file mode 100644 index 0000000000..31b62c9280 --- /dev/null +++ b/frontend/appflowy_tauri/public/tauri.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/public/vite.svg b/frontend/appflowy_tauri/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/frontend/appflowy_tauri/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_tauri/scripts/i18n/index.cjs b/frontend/appflowy_tauri/scripts/i18n/index.cjs new file mode 100644 index 0000000000..c3789e0c56 --- /dev/null +++ b/frontend/appflowy_tauri/scripts/i18n/index.cjs @@ -0,0 +1,63 @@ +const languages = [ + 'ar-SA', + 'ca-ES', + 'de-DE', + 'en', + 'es-VE', + 'eu-ES', + 'fr-FR', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'th-TH', + 'tr-TR', + 'zh-CN', + 'zh-TW', +]; + +const fs = require('fs'); +languages.forEach(language => { + const json = require(`../../../resources/translations/${language}.json`); + const outputJSON = flattenJSON(json); + const output = JSON.stringify(outputJSON); + const isExistDir = fs.existsSync('./src/appflowy_app/i18n/translations'); + if (!isExistDir) { + fs.mkdirSync('./src/appflowy_app/i18n/translations'); + } + fs.writeFile(`./src/appflowy_app/i18n/translations/${language}.json`, new Uint8Array(Buffer.from(output)), (res) => { + if (res) { + console.error(res); + } + }) +}); + + +function flattenJSON(obj, prefix = '') { + let result = {}; + const pluralsKey = ["one", "other", "few", "many", "two", "zero"]; + + for (let key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + + const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`); + result = { ...result, ...nestedKeys }; + } else { + let newKey = `${prefix}${key}`; + let replaceChar = '{' + if (pluralsKey.includes(key)) { + newKey = `${prefix.slice(0, -1)}_${key}`; + } + result[newKey] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}'); + } + } + + return result; +} + diff --git a/frontend/appflowy_tauri/scripts/update_version.cjs b/frontend/appflowy_tauri/scripts/update_version.cjs new file mode 100644 index 0000000000..498b8c3e4f --- /dev/null +++ b/frontend/appflowy_tauri/scripts/update_version.cjs @@ -0,0 +1,31 @@ +const fs = require('fs'); +const path = require('path'); + +if (process.argv.length < 3) { + console.error('Usage: node update-tauri-version.js '); + process.exit(1); +} + +const newVersion = process.argv[2]; + +const tauriConfigPath = path.join(__dirname, '../src-tauri', 'tauri.conf.json'); + +fs.readFile(tauriConfigPath, 'utf8', (err, data) => { + if (err) { + console.error('Error reading tauri.conf.json:', err); + return; + } + + const config = JSON.parse(data); + + config.package.version = newVersion; + + fs.writeFile(tauriConfigPath, JSON.stringify(config, null, 2), 'utf8', (err) => { + if (err) { + console.error('Error writing tauri.conf.json:', err); + return; + } + + console.log(`Tauri version updated to ${newVersion} successfully.`); + }); +}); diff --git a/frontend/appflowy_tauri/src-tauri/.cargo/config.toml b/frontend/appflowy_tauri/src-tauri/.cargo/config.toml new file mode 100644 index 0000000000..bff29e6e17 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/frontend/appflowy_tauri/src-tauri/.gitignore b/frontend/appflowy_tauri/src-tauri/.gitignore new file mode 100644 index 0000000000..61e1bdd46a --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +.env diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock new file mode 100644 index 0000000000..5f50aa7f0d --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -0,0 +1,8164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "accessory" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "log", + "rand 0.7.3", + "wasm-timer", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +dependencies = [ + "cfg-if", + "getrandom 0.2.10", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509" +dependencies = [ + "atomic", + "pin-project", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "getrandom 0.2.10", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tsify", + "url", + "uuid", + "wasm-bindgen", +] + +[[package]] +name = "appflowy-ai-client" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bytes", + "futures", + "serde", + "serde_json", + "serde_repr", + "thiserror", +] + +[[package]] +name = "appflowy_tauri" +version = "0.0.0" +dependencies = [ + "bytes", + "dotenv", + "flowy-chat", + "flowy-config", + "flowy-core", + "flowy-date", + "flowy-document", + "flowy-error", + "flowy-notification", + "flowy-search", + "flowy-user", + "lib-dispatch", + "semver", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-utils", + "tracing", + "uuid", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic_refcell" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.6.2", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.47", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" +dependencies = [ + "borsh-derive", + "hashbrown 0.12.3", +] + +[[package]] +name = "borsh-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytecheck" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.5", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" +dependencies = [ + "chrono", + "chrono-tz-build 0.0.2", + "phf 0.10.1", +] + +[[package]] +name = "chrono-tz" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552" +dependencies = [ + "chrono", + "chrono-tz-build 0.1.0", + "phf 0.11.2", +] + +[[package]] +name = "chrono-tz-build" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" +dependencies = [ + "parse-zoneinfo", + "phf 0.10.1", + "phf_codegen 0.10.0", +] + +[[package]] +name = "chrono-tz-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.2", + "phf_codegen 0.11.2", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "client-api" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "again", + "anyhow", + "app-error", + "async-trait", + "bincode", + "brotli", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", + "database-entity", + "futures-core", + "futures-util", + "getrandom 0.2.10", + "gotrue", + "gotrue-entity", + "mime", + "parking_lot 0.12.1", + "prost", + "reqwest", + "scraper 0.17.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "serde_urlencoded", + "shared-entity", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "wasm-bindgen-futures", + "yrs", +] + +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "cmd_lib" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62" +dependencies = [ + "cmd_lib_macros", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "collab" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "chrono", + "js-sys", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-database" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "collab", + "collab-entity", + "collab-plugins", + "dashmap", + "getrandom 0.2.10", + "js-sys", + "lazy_static", + "nanoid", + "parking_lot 0.12.1", + "rayon", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.2", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "collab-document" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.10", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "collab-entity" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "bytes", + "collab", + "getrandom 0.2.10", + "serde", + "serde_json", + "serde_repr", + "uuid", +] + +[[package]] +name = "collab-folder" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-entity", + "getrandom 0.2.10", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-integrate" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collab", + "collab-entity", + "collab-plugins", + "futures", + "lib-infra", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "collab-plugins" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bincode", + "bytes", + "chrono", + "collab", + "collab-entity", + "futures", + "futures-util", + "getrandom 0.2.10", + "indexed_db_futures", + "js-sys", + "lazy_static", + "parking_lot 0.12.1", + "rand 0.8.5", + "rocksdb", + "serde", + "serde_json", + "similar 2.2.1", + "smallvec", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tracing", + "tracing-wasm", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "collab", + "collab-entity", + "serde", + "thiserror", + "tokio", + "tracing", + "yrs", +] + +[[package]] +name = "collab-user" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.10", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 1.0.6", + "phf 0.8.0", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.47", +] + +[[package]] +name = "csv" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" +dependencies = [ + "csv-core", + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e" +dependencies = [ + "quote", + "syn 2.0.47", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.47", +] + +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "database-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "bincode", + "chrono", + "collab-entity", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "date_time_parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0521d96e513670773ac503e5f5239178c3aef16cffda1e77a3cdbdbe993fb5a" +dependencies = [ + "chrono", + "regex", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + +[[package]] +name = "diesel" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8" +dependencies = [ + "chrono", + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "serde_json", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.47", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d09067bfacaa79114679b279d7f5885b53295b1e2cfb4e79c8e4bd3d633169" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "embed-resource" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80663502655af01a2902dff3f06869330782267924bf1788410b74edcd93770a" +dependencies = [ + "cc", + "rustc_version", + "toml 0.7.5", + "vswhom", + "winreg 0.11.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + +[[package]] +name = "fancy-regex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "fastdivide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +dependencies = [ + "crc32fast", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "flowy-ast" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "flowy-chat" +version = "0.1.0" +dependencies = [ + "allo-isolate", + "bytes", + "dashmap", + "flowy-chat-pub", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-sqlite", + "futures", + "lib-dispatch", + "lib-infra", + "log", + "protobuf", + "strum_macros 0.21.1", + "tokio", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-chat-pub" +version = "0.1.0" +dependencies = [ + "bytes", + "client-api", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-codegen" +version = "0.1.0" +dependencies = [ + "cmd_lib", + "console", + "fancy-regex 0.10.0", + "flowy-ast", + "itertools 0.10.5", + "lazy_static", + "log", + "phf 0.8.0", + "protoc-bin-vendored", + "protoc-rust", + "quote", + "serde", + "serde_json", + "similar 1.3.0", + "syn 1.0.109", + "tera", + "toml 0.5.11", + "walkdir", +] + +[[package]] +name = "flowy-config" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", +] + +[[package]] +name = "flowy-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.5", + "bytes", + "client-api", + "collab", + "collab-entity", + "collab-integrate", + "collab-plugins", + "diesel", + "flowy-chat", + "flowy-chat-pub", + "flowy-config", + "flowy-database-pub", + "flowy-database2", + "flowy-date", + "flowy-document", + "flowy-document-pub", + "flowy-error", + "flowy-folder", + "flowy-folder-pub", + "flowy-search", + "flowy-search-pub", + "flowy-server", + "flowy-server-pub", + "flowy-sqlite", + "flowy-storage", + "flowy-user", + "flowy-user-pub", + "futures", + "futures-core", + "lib-dispatch", + "lib-infra", + "lib-log", + "parking_lot 0.12.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "sysinfo", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "walkdir", +] + +[[package]] +name = "flowy-database-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "client-api", + "collab", + "collab-entity", + "lib-infra", +] + +[[package]] +name = "flowy-database2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bytes", + "chrono", + "chrono-tz 0.8.2", + "collab", + "collab-database", + "collab-entity", + "collab-integrate", + "collab-plugins", + "csv", + "dashmap", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-database-pub", + "flowy-derive", + "flowy-error", + "flowy-notification", + "futures", + "indexmap 2.1.0", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "rayon", + "rust_decimal", + "rusty-money", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.2", + "tokio", + "tracing", + "url", + "validator", +] + +[[package]] +name = "flowy-date" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "date_time_parser", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", + "tracing", +] + +[[package]] +name = "flowy-derive" +version = "0.1.0" +dependencies = [ + "dashmap", + "flowy-ast", + "flowy-codegen", + "lazy_static", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "flowy-document" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "collab", + "collab-document", + "collab-entity", + "collab-integrate", + "collab-plugins", + "dashmap", + "flowy-codegen", + "flowy-derive", + "flowy-document-pub", + "flowy-error", + "flowy-notification", + "flowy-storage", + "futures", + "getrandom 0.2.10", + "indexmap 2.1.0", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "scraper 0.18.1", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-document-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-document", + "flowy-error", + "lib-infra", +] + +[[package]] +name = "flowy-encrypt" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.21.5", + "getrandom 0.2.10", + "hmac", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2", +] + +[[package]] +name = "flowy-error" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "client-api", + "collab-database", + "collab-document", + "collab-folder", + "collab-plugins", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "r2d2", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "tantivy", + "thiserror", + "tokio", + "url", + "validator", +] + +[[package]] +name = "flowy-folder" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-folder-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "collab-folder", + "lib-infra", + "uuid", +] + +[[package]] +name = "flowy-notification" +version = "0.1.0" +dependencies = [ + "bytes", + "dashmap", + "flowy-codegen", + "flowy-derive", + "lazy_static", + "lib-dispatch", + "protobuf", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "flowy-search" +version = "0.1.0" +dependencies = [ + "async-stream", + "bytes", + "collab", + "collab-folder", + "diesel", + "diesel_derives", + "diesel_migrations", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "flowy-user", + "futures", + "lib-dispatch", + "lib-infra", + "protobuf", + "serde", + "serde_json", + "strsim 0.11.0", + "strum_macros 0.26.1", + "tantivy", + "tempfile", + "tokio", + "tracing", + "validator", +] + +[[package]] +name = "flowy-search-pub" +version = "0.1.0" +dependencies = [ + "client-api", + "collab", + "collab-folder", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "client-api", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-plugins", + "flowy-chat-pub", + "flowy-database-pub", + "flowy-document-pub", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-search-pub", + "flowy-server-pub", + "flowy-storage", + "flowy-user-pub", + "futures", + "futures-util", + "hex", + "hyper", + "lazy_static", + "lib-dispatch", + "lib-infra", + "mime_guess", + "parking_lot 0.12.1", + "postgrest", + "rand 0.8.5", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "yrs", +] + +[[package]] +name = "flowy-server-pub" +version = "0.1.0" +dependencies = [ + "flowy-error", + "serde", + "serde_repr", +] + +[[package]] +name = "flowy-sqlite" +version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel_derives", + "diesel_migrations", + "libsqlite3-sys", + "parking_lot 0.12.1", + "r2d2", + "scheduled-thread-pool", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "flowy-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "flowy-error", + "fxhash", + "lib-infra", + "mime", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "flowy-user" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.5", + "bytes", + "chrono", + "collab", + "collab-database", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "collab-user", + "diesel", + "diesel_derives", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-server-pub", + "flowy-sqlite", + "flowy-user-pub", + "lazy_static", + "lib-dispatch", + "lib-infra", + "once_cell", + "parking_lot 0.12.1", + "protobuf", + "semver", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.2", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-user-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.5", + "chrono", + "collab", + "collab-entity", + "flowy-error", + "flowy-folder-pub", + "lib-infra", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.1.1", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.1.1", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.1.1", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.1", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick 0.7.20", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "gotrue" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "futures-util", + "getrandom 0.2.10", + "gotrue-entity", + "infra", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "gotrue-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "chrono", + "jsonwebtoken", + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.1.1", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.6", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5c13fb08e5d4dfc151ee5e88bae63f7773d61852f3bdc73c9f4b9e1bde03148" +dependencies = [ + "log", + "mac", + "markup5ever 0.10.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever 0.11.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.6", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.6", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows 0.48.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", +] + +[[package]] +name = "indexed_db_futures" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "infra" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.5", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kuchiki" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ea8e9c6e031377cff82ee3001dc8026cdf431ed4e2e6b51f98ab8c73484a358" +dependencies = [ + "cssparser 0.27.2", + "html5ever 0.25.2", + "matches", + "selectors 0.22.0", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser 0.27.2", + "html5ever 0.26.0", + "indexmap 1.9.3", + "matches", + "selectors 0.22.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "lib-dispatch" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "derivative", + "dyn-clone", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "getrandom 0.2.10", + "nanoid", + "parking_lot 0.12.1", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "thread-id", + "tokio", + "tracing", + "validator", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "lib-infra" +version = "0.1.0" +dependencies = [ + "allo-isolate", + "anyhow", + "async-trait", + "atomic_refcell", + "bytes", + "chrono", + "futures", + "futures-core", + "md5", + "pin-project", + "tempfile", + "tokio", + "tracing", + "validator", + "walkdir", + "zip", +] + +[[package]] +name = "lib-log" +version = "0.1.0" +dependencies = [ + "chrono", + "lazy_static", + "lib-infra", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-bunyan-formatter", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + +[[package]] +name = "lz4_flex" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +dependencies = [ + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.5", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "murmurhash32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.30.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oneshot" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.27.0+1.1.1v" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e8f197c82d7511c5b014030c9b1efeda40d7d5f99d23b4ceed3524a5e63f02" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "ownedbytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.1.1", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.3.5", + "smallvec", + "windows-targets 0.48.0", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "pest_meta" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.1.0", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", + "uncased", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "plist" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" +dependencies = [ + "base64 0.21.5", + "indexmap 1.9.3", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59871cc5b6cce7eaccca5a802b4173377a1c2ba90654246789a8fa2334426d11" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.7.1", +] + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postgrest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e66400cb23a379592bc8c8bdc9adda652eef4a969b74ab78454a8e8c11330c2b" +dependencies = [ + "reqwest", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9825a04601d60621feed79c4e6b56d65db77cdca55cef43b46b0de1096d1c282" +dependencies = [ + "proc-macro2", + "syn 2.0.47", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools 0.10.5", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.47", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "protobuf-codegen" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protoc" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0218039c514f9e14a5060742ecd50427f8ac4f85a6dc58f2ddb806e318c55ee" +dependencies = [ + "log", + "which", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + +[[package]] +name = "protoc-rust" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f8a182bb17c485f20bdc4274a8c39000a61024cfe461c799b50fec77267838" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc", + "tempfile", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.10", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom 0.2.10", + "redox_syscall 0.2.16", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +dependencies = [ + "aho-corasick 1.0.2", + "memchr", + "regex-syntax 0.7.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" + +[[package]] +name = "rend" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581008d2099240d37fb08d77ad713bcaec2c4d89d50b5b21a8bb1996bbab68ab" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.5", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.50.0", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rkyv" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +dependencies = [ + "bitvec", + "bytecheck", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rust_decimal" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0446843641c69436765a35a5a77088e28c2e6a12da93e84aa3ab1cd4aa5a042" +dependencies = [ + "arrayvec", + "borsh", + "bytecheck", + "byteorder", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca5c398d85f83b9a44de754a2048625a8c5eafcf070da7b8f116b685e2f6608" +dependencies = [ + "quote", + "rust_decimal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.5", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" + +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scraper" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a930e03325234c18c7071fd2b60118307e025d6fff3e12745ffbf63a3d29c" +dependencies = [ + "ahash 0.8.6", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever 0.26.0", + "once_cell", + "selectors 0.25.0", + "smallvec", + "tendril", +] + +[[package]] +name = "scraper" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" +dependencies = [ + "ahash 0.8.6", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever 0.26.0", + "once_cell", + "selectors 0.25.0", + "tendril", +] + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.27.2", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.1.1", + "smallvec", + "thin-slice", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.4.0", + "cssparser 0.31.2", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc 0.3.0", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513" +dependencies = [ + "base64 0.21.5", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "appflowy-ai-client", + "bytes", + "chrono", + "collab-entity", + "database-entity", + "futures", + "gotrue-entity", + "log", + "pin-project", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "similar" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + +[[package]] +name = "smallstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.47", +] + +[[package]] +name = "strum_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.47", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.30.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +dependencies = [ + "cfg-expr 0.15.3", + "heck 0.4.1", + "pkg-config", + "toml 0.7.5", + "version-compare 0.1.1", +] + +[[package]] +name = "tantivy" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +dependencies = [ + "aho-corasick 1.0.2", + "arc-swap", + "async-trait", + "base64 0.21.5", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fs4", + "htmlescape", + "itertools 0.11.0", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +dependencies = [ + "fastdivide", + "fnv", + "itertools 0.11.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +dependencies = [ + "nom", +] + +[[package]] +name = "tantivy-sstable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.4", +] + +[[package]] +name = "tantivy-stacker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +dependencies = [ + "serde", +] + +[[package]] +name = "tao" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6d198e01085564cea63e976ad1566c1ba2c2e4cc79578e35d9f05521505e31" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b27a4bcc5eb524658234589bdffc7e7bfb996dbae6ce9393bfd39cb4159b445" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" + +[[package]] +name = "tauri" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bfe673cf125ef364d6f56b15e8ce7537d9ca7e4dae1cf6fbbdeed2e024db3d9" +dependencies = [ + "anyhow", + "cocoa", + "dirs-next", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http", + "ignore", + "objc", + "once_cell", + "open", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "regex", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-build" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defbfc551bd38ab997e5f8e458f87396d2559d05ce32095076ad6c30f7fc5f9c" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b3475e55acec0b4a50fb96435f19631fb58cbcd31923e1a213de5c382536bbb" +dependencies = [ + "base64 0.21.5", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "regex", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613740228de92d9196b795ac455091d3a5fbdac2654abb8bb07d010b62ab43af" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f8e9e53e00e9f41212c115749e87d5cd2a9eebccafca77a19722eeecd56d43" +dependencies = [ + "gtk", + "http", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8141d72b6b65f2008911e9ef5b98a68d1e3413b7a1464e8f85eb3673bb19a895" +dependencies = [ + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece74810b1d3d44f29f732a7ae09a63183d63949bbdd59c61f8ed2a1b70150db" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever 0.26.0", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.5", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab29bb4f3e256ae6ad5c3e2775aa1f8829f2c0c101fc407bfd3a6df15c60c5" +dependencies = [ + "chrono", + "chrono-tz 0.6.1", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "thread_local", + "unic-segment", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa 1.0.6", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash 0.8.6", + "gethostname", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.3", + "tracing-subscriber", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-serde", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.47", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.10", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna 0.4.0", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.47", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.1.1", +] + +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wry" +version = "0.24.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a70547e8f9d85da0f5af609143f7bde3ac7457a6e1073104d9b73d6c5ac744" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever 0.25.2", + "http", + "kuchiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + +[[package]] +name = "yrs" +version = "0.18.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" +dependencies = [ + "arc-swap", + "atomic_refcell", + "fastrand", + "serde", + "serde_json", + "smallstr", + "smallvec", + "thiserror", +] + +[[package]] +name = "zerocopy" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe 6.0.6", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", +] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml new file mode 100644 index 0000000000..431a37d895 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -0,0 +1,115 @@ +[package] +name = "appflowy_tauri" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.57" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[workspace.dependencies] +anyhow = "1.0" +tracing = "0.1.40" +bytes = "1.5.0" +serde = "1.0" +serde_json = "1.0.108" +protobuf = { version = "2.28.0" } +diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] } +uuid = { version = "1.5.0", features = ["serde", "v4"] } +serde_repr = "0.1" +parking_lot = "0.12" +futures = "0.3.29" +tokio = "1.34.0" +tokio-stream = "0.1.14" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +yrs = "0.18.8" +# Please use the following script to update collab. +# Working directory: frontend +# +# To update the commit ID, run: +# scripts/tool/update_collab_rev.sh new_rev_id +# +# To switch to the local path, run: +# scripts/tool/update_collab_source.sh +# ⚠️⚠️⚠️️ +collab = { version = "0.2" } +collab-entity = { version = "0.2" } +collab-folder = { version = "0.2" } +collab-document = { version = "0.2" } +collab-database = { version = "0.2" } +collab-plugins = { version = "0.2" } +collab-user = { version = "0.2" } + +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" } + +[dependencies] +serde_json.workspace = true +serde.workspace = true +tauri = { version = "1.5", features = [ + "dialog-all", + "clipboard-all", + "fs-all", + "shell-open", +] } +tauri-utils = "1.5.2" +bytes.workspace = true +tracing.workspace = true +lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = [ + "use_serde", +] } +flowy-core = { path = "../../rust-lib/flowy-core", features = [ + "ts", +] } +flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] } +flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] } +flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] } +flowy-chat = { path = "../../rust-lib/flowy-chat", features = ["tauri_ts"] } +flowy-error = { path = "../../rust-lib/flowy-error", features = [ + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_appflowy_cloud", + "impl_from_reqwest", + "impl_from_serde", + "tauri_ts", +] } +flowy-search = { path = "../../rust-lib/flowy-search", features = ["tauri_ts"] } +flowy-document = { path = "../../rust-lib/flowy-document", features = [ + "tauri_ts", +] } +flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ + "tauri_ts", +] } + +uuid = "1.5.0" +tauri-plugin-deep-link = "0.1.2" +dotenv = "0.15.0" +semver = "1.0.23" + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL +default = ["custom-protocol"] +# this feature is used used for production builds where `devPath` points to the filesystem +# DO NOT remove this +custom-protocol = ["tauri/custom-protocol"] + +[patch.crates-io] +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } diff --git a/frontend/appflowy_tauri/src-tauri/Info.plist b/frontend/appflowy_tauri/src-tauri/Info.plist new file mode 100644 index 0000000000..25b430c049 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Info.plist @@ -0,0 +1,19 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + appflowy-flutter + CFBundleURLSchemes + + appflowy-flutter + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src-tauri/build.rs b/frontend/appflowy_tauri/src-tauri/build.rs new file mode 100644 index 0000000000..d860e1e6a7 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/frontend/appflowy_tauri/src-tauri/env.development b/frontend/appflowy_tauri/src-tauri/env.development new file mode 100644 index 0000000000..188835e3d0 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/env.development @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_tauri/src-tauri/env.production b/frontend/appflowy_tauri/src-tauri/env.production new file mode 100644 index 0000000000..b03c328b84 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/env.production @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_tauri/src-tauri/icons/128x128.png b/frontend/appflowy_tauri/src-tauri/icons/128x128.png new file mode 100644 index 0000000000..3a51041313 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/128x128.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/128x128@2x.png b/frontend/appflowy_tauri/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000..9076de3a4b Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/128x128@2x.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/32x32.png b/frontend/appflowy_tauri/src-tauri/icons/32x32.png new file mode 100644 index 0000000000..6ae6683fef Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/32x32.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square107x107Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000..b08dcf7d21 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square107x107Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square142x142Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000..f3e437b76e Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square142x142Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square150x150Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000..6a1dc04864 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square150x150Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square284x284Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000..2f2d9d6fe6 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square284x284Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square30x30Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000..46e3802c0b Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square30x30Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square310x310Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000..230b1abe58 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square310x310Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square44x44Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000..ad188037a3 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square44x44Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square71x71Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000..ceae9ad1bb Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square71x71Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/Square89x89Logo.png b/frontend/appflowy_tauri/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000..123dcea650 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/Square89x89Logo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/StoreLogo.png b/frontend/appflowy_tauri/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000..d7906c3c03 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/StoreLogo.png differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/icon.icns b/frontend/appflowy_tauri/src-tauri/icons/icon.icns new file mode 100644 index 0000000000..74b585f25d Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/icon.icns differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/icon.ico b/frontend/appflowy_tauri/src-tauri/icons/icon.ico new file mode 100644 index 0000000000..cd9ad402d1 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/icon.ico differ diff --git a/frontend/appflowy_tauri/src-tauri/icons/icon.png b/frontend/appflowy_tauri/src-tauri/icons/icon.png new file mode 100644 index 0000000000..7cc3853d67 Binary files /dev/null and b/frontend/appflowy_tauri/src-tauri/icons/icon.png differ diff --git a/frontend/appflowy_tauri/src-tauri/rust-toolchain.toml b/frontend/appflowy_tauri/src-tauri/rust-toolchain.toml new file mode 100644 index 0000000000..6f14058b2e --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.77.2" diff --git a/frontend/appflowy_tauri/src-tauri/rustfmt.toml b/frontend/appflowy_tauri/src-tauri/rustfmt.toml new file mode 100644 index 0000000000..5cb0d67ee5 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/rustfmt.toml @@ -0,0 +1,12 @@ +# https://rust-lang.github.io/rustfmt/?version=master&search= +max_width = 100 +tab_spaces = 2 +newline_style = "Auto" +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +merge_derives = true +edition = "2021" \ No newline at end of file diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs new file mode 100644 index 0000000000..7591ba37ff --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -0,0 +1,66 @@ +use flowy_core::config::AppFlowyCoreConfig; +use flowy_core::{AppFlowyCore, DEFAULT_NAME}; +use lib_dispatch::runtime::AFPluginRuntime; +use std::sync::Arc; + +use dotenv::dotenv; + +pub fn read_env() { + dotenv().ok(); + + let env = if cfg!(debug_assertions) { + include_str!("../env.development") + } else { + include_str!("../env.production") + }; + + for line in env.lines() { + if let Some((key, value)) = line.split_once('=') { + // Check if the environment variable is not already set in the system + let current_value = std::env::var(key).unwrap_or_default(); + if current_value.is_empty() { + std::env::set_var(key, value); + } + } + } +} + +pub fn init_flowy_core() -> AppFlowyCore { + let config_json = include_str!("../tauri.conf.json"); + let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); + + let app_version = config + .package + .version + .clone() + .map(|v| v.to_string()) + .unwrap_or_else(|| "0.5.8".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); + let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); + if cfg!(debug_assertions) { + data_path.push("data_dev"); + } else { + data_path.push("data"); + } + + let custom_application_path = data_path.to_str().unwrap().to_string(); + let application_path = data_path.to_str().unwrap().to_string(); + let device_id = uuid::Uuid::new_v4().to_string(); + + read_env(); + std::env::set_var("RUST_LOG", "trace"); + + let config = AppFlowyCoreConfig::new( + app_version, + custom_application_path, + application_path, + device_id, + "tauri".to_string(), + DEFAULT_NAME.to_string(), + ) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); + + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(async move { AppFlowyCore::new(config, cloned_runtime, None).await }) +} diff --git a/frontend/appflowy_tauri/src-tauri/src/main.rs b/frontend/appflowy_tauri/src-tauri/src/main.rs new file mode 100644 index 0000000000..6a69de07fd --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/main.rs @@ -0,0 +1,71 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +#[allow(dead_code)] +pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter"; +pub const OPEN_DEEP_LINK: &str = "open_deep_link"; + +mod init; +mod notification; +mod request; + +use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; +use init::*; +use notification::*; +use request::*; +use tauri::Manager; +extern crate dotenv; + +fn main() { + tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); + + let flowy_core = init_flowy_core(); + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![invoke_request]) + .manage(flowy_core) + .on_window_event(|_window_event| {}) + .on_menu_event(|_menu| {}) + .on_page_load(|window, _payload| { + let app_handler = window.app_handle(); + // Make sure hot reload won't register the notification sender twice + unregister_all_notification_sender(); + register_notification_sender(TSNotificationSender::new(app_handler.clone())); + // tauri::async_runtime::spawn(async move {}); + + window.listen_global(AF_EVENT, move |event| { + on_event(app_handler.clone(), event); + }); + }) + .setup(|_app| { + let splashscreen_window = _app.get_window("splashscreen").unwrap(); + let window = _app.get_window("main").unwrap(); + let handle = _app.handle(); + + // we perform the initialization code on a new task so the app doesn't freeze + tauri::async_runtime::spawn(async move { + // initialize your app here instead of sleeping :) + std::thread::sleep(std::time::Duration::from_secs(2)); + + // After it's done, close the splashscreen and display the main window + splashscreen_window.close().unwrap(); + window.show().unwrap(); + // If you need macOS support this must be called in .setup() ! + // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + tauri_plugin_deep_link::register( + DEEP_LINK_SCHEME, + move |request| { + dbg!(&request); + handle.emit_all(OPEN_DEEP_LINK, request).unwrap(); + }, + ) + .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */); + }); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/frontend/appflowy_tauri/src-tauri/src/notification.rs b/frontend/appflowy_tauri/src-tauri/src/notification.rs new file mode 100644 index 0000000000..b42541edec --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/notification.rs @@ -0,0 +1,35 @@ +use flowy_notification::entities::SubscribeObject; +use flowy_notification::NotificationSender; +use serde::Serialize; +use tauri::{AppHandle, Event, Manager, Wry}; + +#[allow(dead_code)] +pub const AF_EVENT: &str = "af-event"; +pub const AF_NOTIFICATION: &str = "af-notification"; + +#[tracing::instrument(level = "trace")] +pub fn on_event(app_handler: AppHandle, event: Event) {} + +#[allow(dead_code)] +pub fn send_notification(app_handler: AppHandle, payload: P) { + app_handler.emit_all(AF_NOTIFICATION, payload).unwrap(); +} + +pub struct TSNotificationSender { + handler: AppHandle, +} + +impl TSNotificationSender { + pub fn new(handler: AppHandle) -> Self { + Self { handler } + } +} + +impl NotificationSender for TSNotificationSender { + fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { + self + .handler + .emit_all(AF_NOTIFICATION, subject) + .map_err(|e| format!("{:?}", e)) + } +} diff --git a/frontend/appflowy_tauri/src-tauri/src/request.rs b/frontend/appflowy_tauri/src-tauri/src/request.rs new file mode 100644 index 0000000000..029e71c18c --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/src/request.rs @@ -0,0 +1,45 @@ +use flowy_core::AppFlowyCore; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, +}; +use tauri::{AppHandle, Manager, State, Wry}; + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct AFTauriRequest { + ty: String, + payload: Vec, +} + +impl std::convert::From for AFPluginRequest { + fn from(event: AFTauriRequest) -> Self { + AFPluginRequest::new(event.ty).payload(event.payload) + } +} + +#[derive(Clone, serde::Serialize)] +pub struct AFTauriResponse { + code: StatusCode, + payload: Vec, +} + +impl std::convert::From for AFTauriResponse { + fn from(response: AFPluginEventResponse) -> Self { + Self { + code: response.status_code, + payload: response.payload.to_vec(), + } + } +} + +// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +#[tauri::command] +pub async fn invoke_request( + request: AFTauriRequest, + app_handler: AppHandle, +) -> AFTauriResponse { + let request: AFPluginRequest = request.into(); + let state: State = app_handler.state(); + let dispatcher = state.inner().dispatcher(); + let response = AFPluginDispatcher::async_send(dispatcher.as_ref(), request).await; + response.into() +} diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..11dd7c206c --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -0,0 +1,113 @@ +{ + "build": { + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "pnpm run build", + "devPath": "http://localhost:1420", + "distDir": "../dist", + "withGlobalTauri": false + }, + "package": { + "productName": "AppFlowy", + "version": "0.0.1" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + }, + "fs": { + "all": true, + "scope": [ + "$APPLOCALDATA/**" + ], + "readFile": true, + "writeFile": true, + "readDir": true, + "copyFile": true, + "createDir": true, + "removeDir": true, + "removeFile": true, + "renameFile": true, + "exists": true + }, + "clipboard": { + "all": true, + "writeText": true, + "readText": true + }, + "dialog": { + "all": true, + "ask": true, + "confirm": true, + "message": true, + "open": true, + "save": true + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "externalBin": [], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "identifier": "com.appflowy.tauri", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null, + "minimumSystemVersion": "10.15.0" + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fileDropEnabled": false, + "fullscreen": false, + "height": 800, + "resizable": true, + "title": "AppFlowy", + "width": 1200, + "minWidth": 800, + "minHeight": 600, + "visible": false, + "label": "main" + }, + { + "height": 300, + "width": 549, + "decorations": false, + "url": "launch_splash.jpg", + "label": "splashscreen", + "center": true, + "visible": true + } + ] + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts b/frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts new file mode 100644 index 0000000000..6adbb4a512 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/@types/i18next.d.ts @@ -0,0 +1,8 @@ +import resources from './resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation'; + resources: typeof resources; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/@types/resources.ts b/frontend/appflowy_tauri/src/appflowy_app/@types/resources.ts new file mode 100644 index 0000000000..479f05f013 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/@types/resources.ts @@ -0,0 +1,7 @@ +import translation from '$app/i18n/translations/en.json'; + +const resources = { + translation, +} as const; + +export default resources; diff --git a/frontend/appflowy_tauri/src/appflowy_app/App.tsx b/frontend/appflowy_tauri/src/appflowy_app/App.tsx new file mode 100644 index 0000000000..9381737341 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/App.tsx @@ -0,0 +1,25 @@ +import { BrowserRouter } from 'react-router-dom'; + +import { Provider } from 'react-redux'; +import { store } from './stores/store'; + +import { ErrorHandlerPage } from './components/error/ErrorHandlerPage'; +import '$app/i18n/config'; + +import { ErrorBoundary } from 'react-error-boundary'; + +import AppMain from '$app/AppMain'; + +const App = () => { + return ( + + + + + + + + ); +}; + +export default App; diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts new file mode 100644 index 0000000000..9c46b8ab38 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts @@ -0,0 +1,69 @@ +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { useEffect, useMemo } from 'react'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; +import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice'; +import { createTheme } from '@mui/material/styles'; +import { getDesignTokens } from '$app/utils/mui'; +import { useTranslation } from 'react-i18next'; +import { UserService } from '$app/application/user/user.service'; + +export function useUserSetting() { + const dispatch = useAppDispatch(); + const { i18n } = useTranslation(); + const loginState = useAppSelector((state) => state.currentUser.loginState); + + const { themeMode = ThemeMode.System, theme: themeType = ThemeType.Default } = useAppSelector((state) => { + return { + themeMode: state.currentUser.userSetting.themeMode, + theme: state.currentUser.userSetting.theme, + }; + }); + + const isDark = + themeMode === ThemeMode.Dark || + (themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches); + + useEffect(() => { + if (loginState !== LoginState.Success && loginState !== undefined) return; + void (async () => { + const settings = await UserService.getAppearanceSetting(); + + if (!settings) return; + dispatch(currentUserActions.setUserSetting(settings)); + await i18n.changeLanguage(settings.language); + })(); + }, [dispatch, i18n, loginState]); + + useEffect(() => { + const html = document.documentElement; + + html?.setAttribute('data-dark-mode', String(isDark)); + }, [isDark]); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleSystemThemeChange = () => { + if (themeMode !== ThemeMode.System) return; + dispatch( + currentUserActions.setUserSetting({ + isDark: mediaQuery.matches, + }) + ); + }; + + mediaQuery.addEventListener('change', handleSystemThemeChange); + + return () => { + mediaQuery.removeEventListener('change', handleSystemThemeChange); + }; + }, [dispatch, themeMode]); + + const muiTheme = useMemo(() => createTheme(getDesignTokens(isDark)), [isDark]); + + return { + muiTheme, + themeMode, + themeType, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx b/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx new file mode 100644 index 0000000000..76bdb167b0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; +import { ProtectedRoutes } from '$app/components/auth/ProtectedRoutes'; +import { DatabasePage } from '$app/views/DatabasePage'; + +import { ThemeProvider } from '@mui/material'; +import { useUserSetting } from '$app/AppMain.hooks'; +import TrashPage from '$app/views/TrashPage'; +import DocumentPage from '$app/views/DocumentPage'; +import { Toaster } from 'react-hot-toast'; +import AppFlowyDevTool from '$app/components/_shared/devtool/AppFlowyDevTool'; + +function AppMain() { + const { muiTheme } = useUserSetting(); + + return ( + + + }> + } /> + } /> + } /> + + + + {process.env.NODE_ENV === 'development' && } + + ); +} + +export default AppMain; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_listeners.ts new file mode 100644 index 0000000000..c5c94daebc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_listeners.ts @@ -0,0 +1,48 @@ +import { Database } from '$app/application/database'; +import { getCell } from './cell_service'; + +export function didDeleteCells({ database, rowId, fieldId }: { database: Database; rowId?: string; fieldId?: string }) { + const ids = Object.keys(database.cells); + + ids.forEach((id) => { + const cell = database.cells[id]; + + if (rowId && cell.rowId !== rowId) return; + if (fieldId && cell.fieldId !== fieldId) return; + + delete database.cells[id]; + }); +} + +export async function didUpdateCells({ + viewId, + database, + rowId, + fieldId, +}: { + viewId: string; + database: Database; + rowId?: string; + fieldId?: string; +}) { + const field = database.fields.find((field) => field.id === fieldId); + + if (!field) { + delete database.cells[`${rowId}:${fieldId}`]; + return; + } + + const ids = Object.keys(database.cells); + + ids.forEach((id) => { + const cell = database.cells[id]; + + if (rowId && cell.rowId !== rowId) return; + if (fieldId && cell.fieldId !== fieldId) return; + + void getCell(viewId, cell.rowId, cell.fieldId, field.type).then((data) => { + // cache cell + database.cells[id] = data; + }); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_service.ts new file mode 100644 index 0000000000..950f5becb3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_service.ts @@ -0,0 +1,140 @@ +import { + CellIdPB, + CellChangesetPB, + SelectOptionCellChangesetPB, + ChecklistCellDataChangesetPB, + DateCellChangesetPB, + FieldType, +} from '../../../../services/backend'; +import { + DatabaseEventGetCell, + DatabaseEventUpdateCell, + DatabaseEventUpdateSelectOptionCell, + DatabaseEventUpdateChecklistCell, + DatabaseEventUpdateDateCell, +} from '@/services/backend/events/flowy-database2'; +import { SelectOption } from '../field'; +import { Cell, pbToCell } from './cell_types'; + +export async function getCell(viewId: string, rowId: string, fieldId: string, fieldType?: FieldType): Promise { + const payload = CellIdPB.fromObject({ + view_id: viewId, + row_id: rowId, + field_id: fieldId, + }); + + const result = await DatabaseEventGetCell(payload); + + if (result.ok === false) { + return Promise.reject(result.val); + } + + const value = result.val; + + return pbToCell(value, fieldType); +} + +export async function updateCell(viewId: string, rowId: string, fieldId: string, changeset: string): Promise { + const payload = CellChangesetPB.fromObject({ + view_id: viewId, + row_id: rowId, + field_id: fieldId, + cell_changeset: changeset, + }); + + const result = await DatabaseEventUpdateCell(payload); + + return result.unwrap(); +} + +export async function updateSelectCell( + viewId: string, + rowId: string, + fieldId: string, + data: { + insertOptionIds?: string[]; + deleteOptionIds?: string[]; + } +): Promise { + const payload = SelectOptionCellChangesetPB.fromObject({ + cell_identifier: { + view_id: viewId, + row_id: rowId, + field_id: fieldId, + }, + insert_option_ids: data.insertOptionIds, + delete_option_ids: data.deleteOptionIds, + }); + + const result = await DatabaseEventUpdateSelectOptionCell(payload); + + return result.unwrap(); +} + +export async function updateChecklistCell( + viewId: string, + rowId: string, + fieldId: string, + data: { + insertOptions?: string[]; + selectedOptionIds?: string[]; + deleteOptionIds?: string[]; + updateOptions?: Partial[]; + } +): Promise { + const payload = ChecklistCellDataChangesetPB.fromObject({ + view_id: viewId, + row_id: rowId, + field_id: fieldId, + insert_options: data.insertOptions, + selected_option_ids: data.selectedOptionIds, + delete_option_ids: data.deleteOptionIds, + update_options: data.updateOptions, + }); + + const result = await DatabaseEventUpdateChecklistCell(payload); + + return result.unwrap(); +} + +export async function updateDateCell( + viewId: string, + rowId: string, + fieldId: string, + data: { + // 10-digit timestamp + date?: number; + // time string in format HH:mm + time?: string; + // 10-digit timestamp + endDate?: number; + // time string in format HH:mm + endTime?: string; + includeTime?: boolean; + clearFlag?: boolean; + isRange?: boolean; + } +): Promise { + const payload = DateCellChangesetPB.fromObject({ + cell_id: { + view_id: viewId, + row_id: rowId, + field_id: fieldId, + }, + date: data.date, + time: data.time, + include_time: data.includeTime, + clear_flag: data.clearFlag, + end_date: data.endDate, + end_time: data.endTime, + is_range: data.isRange, + }); + + const result = await DatabaseEventUpdateDateCell(payload); + + if (!result.ok) { + return Promise.reject(typeof result.val.msg === 'string' ? result.val.msg : 'Unknown error'); + } + + return result.val; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_types.ts new file mode 100644 index 0000000000..f36f68ad8b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/cell_types.ts @@ -0,0 +1,163 @@ +import { + CellPB, + CheckboxCellDataPB, + ChecklistCellDataPB, + DateCellDataPB, + FieldType, + SelectOptionCellDataPB, + TimestampCellDataPB, + URLCellDataPB, +} from '../../../../services/backend'; +import { SelectOption, pbToSelectOption } from '../field/select_option/select_option_types'; + +export interface Cell { + rowId: string; + fieldId: string; + fieldType: FieldType; + data: unknown; +} + +export interface TextCell extends Cell { + fieldType: FieldType.RichText; + data: string; +} + +export interface NumberCell extends Cell { + fieldType: FieldType.Number; + data: string; +} + +export interface CheckboxCell extends Cell { + fieldType: FieldType.Checkbox; + data: boolean; +} + +export interface UrlCell extends Cell { + fieldType: FieldType.URL; + data: string; +} + +export interface SelectCell extends Cell { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectCellData; +} + +export interface SelectCellData { + selectedOptionIds?: string[]; +} + +export interface DateTimeCell extends Cell { + fieldType: FieldType.DateTime; + data: DateTimeCellData; +} + +export interface TimeStampCell extends Cell { + fieldType: FieldType.LastEditedTime | FieldType.CreatedTime; + data: TimestampCellData; +} + +export interface DateTimeCellData { + date?: string; + time?: string; + timestamp?: number; + includeTime?: boolean; + endDate?: string; + endTime?: string; + endTimestamp?: number; + isRange?: boolean; +} + +export interface TimestampCellData { + dataTime?: string; + timestamp?: number; +} + +export interface ChecklistCell extends Cell { + fieldType: FieldType.Checklist; + data: ChecklistCellData; +} + +export interface ChecklistCellData { + /** + * link to [SelectOption's id property]{@link SelectOption#id}. + */ + selectedOptions?: string[]; + percentage?: number; + options?: SelectOption[]; +} + +export type UndeterminedCell = + | TextCell + | NumberCell + | DateTimeCell + | SelectCell + | CheckboxCell + | UrlCell + | ChecklistCell; + +const pbToCheckboxCellData = (pb: CheckboxCellDataPB): boolean => ( + pb.is_checked +); + +const pbToDateTimeCellData = (pb: DateCellDataPB): DateTimeCellData => ({ + date: pb.date, + time: pb.time, + timestamp: pb.timestamp, + includeTime: pb.include_time, + endDate: pb.end_date, + endTime: pb.end_time, + endTimestamp: pb.end_timestamp, + isRange: pb.is_range, +}); + +const pbToTimestampCellData = (pb: TimestampCellDataPB): TimestampCellData => ({ + dataTime: pb.date_time, + timestamp: pb.timestamp, +}); + +export const pbToSelectCellData = (pb: SelectOptionCellDataPB): SelectCellData => { + return { + selectedOptionIds: pb.select_options.map((option) => option.id), + }; +}; + +const pbToURLCellData = (pb: URLCellDataPB): string => ( + pb.content +); + +export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({ + selectedOptions: pb.selected_options.map(({ id }) => id), + percentage: pb.percentage, + options: pb.options.map(pbToSelectOption), +}); + +function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) { + switch (fieldType) { + case FieldType.RichText: + case FieldType.Number: + return new TextDecoder().decode(bytes); + case FieldType.Checkbox: + return pbToCheckboxCellData(CheckboxCellDataPB.deserialize(bytes)); + case FieldType.DateTime: + return pbToDateTimeCellData(DateCellDataPB.deserialize(bytes)); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return pbToTimestampCellData(TimestampCellDataPB.deserialize(bytes)); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return pbToSelectCellData(SelectOptionCellDataPB.deserialize(bytes)); + case FieldType.URL: + return pbToURLCellData(URLCellDataPB.deserialize(bytes)); + case FieldType.Checklist: + return pbToChecklistCellData(ChecklistCellDataPB.deserialize(bytes)); + } +} + +export const pbToCell = (pb: CellPB, fieldType: FieldType = pb.field_type): Cell => { + return { + rowId: pb.row_id, + fieldId: pb.field_id, + fieldType: fieldType, + data: bytesToCellData(pb.data, fieldType), + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/index.ts new file mode 100644 index 0000000000..bc6bdc4417 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/cell/index.ts @@ -0,0 +1,3 @@ +export * from './cell_types'; +export * as cellService from './cell_service'; +export * as cellListeners from './cell_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_service.ts new file mode 100644 index 0000000000..74ebfb1df0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_service.ts @@ -0,0 +1,84 @@ +import { DatabaseViewIdPB } from '@/services/backend'; +import { + DatabaseEventGetDatabase, + DatabaseEventGetDatabaseId, + DatabaseEventGetDatabaseSetting, +} from '@/services/backend/events/flowy-database2'; +import { fieldService } from '../field'; +import { pbToFilter } from '../filter'; +import { groupService, pbToGroupSetting } from '../group'; +import { pbToRowMeta } from '../row'; +import { pbToSort } from '../sort'; +import { Database } from './database_types'; + +export async function getDatabaseId(viewId: string): Promise { + const payload = DatabaseViewIdPB.fromObject({ value: viewId }); + + const result = await DatabaseEventGetDatabaseId(payload); + + return result.map((value) => value.value).unwrap(); +} + +export async function getDatabase(viewId: string) { + const payload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + + const result = await DatabaseEventGetDatabase(payload); + + if (!result.ok) return Promise.reject('Failed to get database'); + + return result + .map((value) => { + return { + id: value.id, + isLinked: value.is_linked, + layoutType: value.layout_type, + fieldIds: value.fields.map((field) => field.field_id), + rowMetas: value.rows.map(pbToRowMeta), + }; + }) + .unwrap(); +} + +export async function getDatabaseSetting(viewId: string) { + const payload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + + const result = await DatabaseEventGetDatabaseSetting(payload); + + return result + .map((value) => { + return { + filters: value.filters.items.map(pbToFilter), + sorts: value.sorts.items.map(pbToSort), + groupSettings: value.group_settings.items.map(pbToGroupSetting), + }; + }) + .unwrap(); +} + +export async function openDatabase(viewId: string): Promise { + const { id, isLinked, layoutType, fieldIds, rowMetas } = await getDatabase(viewId); + + const { filters, sorts, groupSettings } = await getDatabaseSetting(viewId); + + const { fields, typeOptions } = await fieldService.getFields(viewId, fieldIds); + + const groups = await groupService.getGroups(viewId); + + return { + id, + isLinked, + layoutType, + fields, + rowMetas, + filters, + sorts, + groups, + groupSettings, + typeOptions, + cells: {}, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_types.ts new file mode 100644 index 0000000000..627cd94013 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/database_types.ts @@ -0,0 +1,21 @@ +import { DatabaseLayoutPB } from '@/services/backend'; +import { Field, UndeterminedTypeOptionData } from '../field'; +import { Filter } from '../filter'; +import { GroupSetting, Group } from '../group'; +import { RowMeta } from '../row'; +import { Sort } from '../sort'; +import { Cell } from '../cell'; + +export interface Database { + id: string; + isLinked: boolean; + layoutType: DatabaseLayoutPB; + fields: Field[]; + rowMetas: RowMeta[]; + filters: Filter[]; + sorts: Sort[]; + groupSettings: GroupSetting[]; + groups: Group[]; + typeOptions: Record; + cells: Record; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/index.ts new file mode 100644 index 0000000000..e656d98287 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/database/index.ts @@ -0,0 +1,2 @@ +export * from './database_types'; +export * as databaseService from './database_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/database_view_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/database_view_service.ts new file mode 100644 index 0000000000..87d99d9b75 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/database_view_service.ts @@ -0,0 +1,75 @@ +import { CreateViewPayloadPB, RepeatedViewIdPB, UpdateViewPayloadPB, ViewIdPB, ViewLayoutPB } from '@/services/backend'; +import { + FolderEventCreateView, + FolderEventDeleteView, + FolderEventGetView, + FolderEventUpdateView, +} from '@/services/backend/events/flowy-folder'; +import { databaseService } from '../database'; +import { Page, parserViewPBToPage } from '$app_reducers/pages/slice'; + +export async function getDatabaseViews(viewId: string): Promise { + const payload = ViewIdPB.fromObject({ value: viewId }); + + const result = await FolderEventGetView(payload); + + if (result.ok) { + return [parserViewPBToPage(result.val), ...result.val.child_views.map(parserViewPBToPage)]; + } + + return Promise.reject(result.val); +} + +export async function createDatabaseView( + viewId: string, + layout: ViewLayoutPB, + name: string, + databaseId?: string +): Promise { + const payload = CreateViewPayloadPB.fromObject({ + parent_view_id: viewId, + name, + layout, + meta: { + database_id: databaseId || (await databaseService.getDatabaseId(viewId)), + }, + }); + + const result = await FolderEventCreateView(payload); + + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.err); +} + +export async function updateView(viewId: string, view: { name?: string; layout?: ViewLayoutPB }): Promise { + const payload = UpdateViewPayloadPB.fromObject({ + view_id: viewId, + name: view.name, + layout: view.layout, + }); + + const result = await FolderEventUpdateView(payload); + + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.err); +} + +export async function deleteView(viewId: string): Promise { + const payload = RepeatedViewIdPB.fromObject({ + items: [viewId], + }); + + const result = await FolderEventDeleteView(payload); + + if (result.ok) { + return; + } + + return Promise.reject(result.err); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/index.ts new file mode 100644 index 0000000000..b2a6e1a5f1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/database_view/index.ts @@ -0,0 +1 @@ +export * as databaseViewService from './database_view_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_listeners.ts new file mode 100644 index 0000000000..ef36daa20c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_listeners.ts @@ -0,0 +1,40 @@ +import { DatabaseFieldChangesetPB, FieldSettingsPB, FieldVisibility } from '@/services/backend'; +import { Database, fieldService } from '$app/application/database'; +import { didDeleteCells, didUpdateCells } from '$app/application/database/cell/cell_listeners'; + +export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) { + const { field_id: fieldId, visibility, width } = settings; + const field = database.fields.find((field) => field.id === fieldId); + + if (!field) return; + field.visibility = visibility; + field.width = width; + // delete cells if field is hidden + if (visibility === FieldVisibility.AlwaysHidden) { + didDeleteCells({ database, fieldId }); + } +} + +export async function didUpdateFields(viewId: string, database: Database, changeset: DatabaseFieldChangesetPB) { + const { fields, typeOptions } = await fieldService.getFields(viewId); + + database.fields = fields; + const deletedFieldIds = Object.keys(changeset.deleted_fields); + const updatedFieldIds = changeset.updated_fields.map((field) => field.id); + + Object.assign(database.typeOptions, typeOptions); + deletedFieldIds.forEach( + (fieldId) => { + // delete cache cells + didDeleteCells({ database, fieldId }); + // delete cache type options + delete database.typeOptions[fieldId]; + }, + [database.typeOptions] + ); + + updatedFieldIds.forEach((fieldId) => { + // delete cache cells + void didUpdateCells({ viewId, database, fieldId }); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_service.ts new file mode 100644 index 0000000000..219aeb3ea5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_service.ts @@ -0,0 +1,215 @@ +import { + CreateFieldPayloadPB, + DeleteFieldPayloadPB, + DuplicateFieldPayloadPB, + FieldChangesetPB, + FieldType, + GetFieldPayloadPB, + MoveFieldPayloadPB, + RepeatedFieldIdPB, + UpdateFieldTypePayloadPB, + FieldSettingsChangesetPB, + FieldVisibility, + DatabaseViewIdPB, + OrderObjectPositionTypePB, +} from '@/services/backend'; +import { + DatabaseEventDuplicateField, + DatabaseEventUpdateField, + DatabaseEventUpdateFieldType, + DatabaseEventMoveField, + DatabaseEventGetFields, + DatabaseEventDeleteField, + DatabaseEventCreateField, + DatabaseEventUpdateFieldSettings, + DatabaseEventGetAllFieldSettings, +} from '@/services/backend/events/flowy-database2'; +import { Field, pbToField } from './field_types'; +import { bytesToTypeOption } from './type_option'; +import { Database } from '$app/application/database'; + +export async function getFields( + viewId: string, + fieldIds?: string[] +): Promise<{ + fields: Field[]; + typeOptions: Database['typeOptions']; +}> { + const payload = GetFieldPayloadPB.fromObject({ + view_id: viewId, + field_ids: fieldIds + ? RepeatedFieldIdPB.fromObject({ + items: fieldIds.map((fieldId) => ({ field_id: fieldId })), + }) + : undefined, + }); + + const result = await DatabaseEventGetFields(payload); + + const getSettingsPayload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + + const settings = await DatabaseEventGetAllFieldSettings(getSettingsPayload); + + if (settings.ok === false || result.ok === false) { + return Promise.reject('Failed to get fields'); + } + + const typeOptions: Database['typeOptions'] = {}; + + const fields = await Promise.all( + result.val.items.map(async (item) => { + const setting = settings.val.items.find((setting) => setting.field_id === item.id); + + const field = pbToField(item); + + const typeOption = bytesToTypeOption(item.type_option_data, item.field_type); + + if (typeOption) { + typeOptions[item.id] = typeOption; + } + + return { + ...field, + visibility: setting?.visibility, + width: setting?.width, + }; + }) + ); + + return { fields, typeOptions }; +} + +export async function createField({ + viewId, + targetFieldId, + fieldPosition, + fieldType, + data, +}: { + viewId: string; + targetFieldId?: string; + fieldPosition?: OrderObjectPositionTypePB; + fieldType?: FieldType; + data?: Uint8Array; +}): Promise { + const payload = CreateFieldPayloadPB.fromObject({ + view_id: viewId, + field_type: fieldType, + type_option_data: data, + field_position: { + position: fieldPosition, + object_id: targetFieldId, + }, + }); + + const result = await DatabaseEventCreateField(payload); + + if (result.ok === false) { + return Promise.reject('Failed to create field'); + } + + return pbToField(result.val); +} + +export async function duplicateField(viewId: string, fieldId: string): Promise { + const payload = DuplicateFieldPayloadPB.fromObject({ + view_id: viewId, + field_id: fieldId, + }); + + const result = await DatabaseEventDuplicateField(payload); + + if (result.ok === false) { + return Promise.reject('Failed to duplicate field'); + } + + return result.val; +} + +export async function updateField( + viewId: string, + fieldId: string, + data: { + name?: string; + desc?: string; + } +): Promise { + const payload = FieldChangesetPB.fromObject({ + view_id: viewId, + field_id: fieldId, + ...data, + }); + + const result = await DatabaseEventUpdateField(payload); + + return result.unwrap(); +} + +export async function updateFieldType(viewId: string, fieldId: string, fieldType: FieldType): Promise { + const payload = UpdateFieldTypePayloadPB.fromObject({ + view_id: viewId, + field_id: fieldId, + field_type: fieldType, + }); + + const result = await DatabaseEventUpdateFieldType(payload); + + return result.unwrap(); +} + +export async function moveField(viewId: string, fromFieldId: string, toFieldId: string): Promise { + const payload = MoveFieldPayloadPB.fromObject({ + view_id: viewId, + from_field_id: fromFieldId, + to_field_id: toFieldId, + }); + + const result = await DatabaseEventMoveField(payload); + + return result.unwrap(); +} + +export async function deleteField(viewId: string, fieldId: string): Promise { + const payload = DeleteFieldPayloadPB.fromObject({ + view_id: viewId, + field_id: fieldId, + }); + + const result = await DatabaseEventDeleteField(payload); + + return result.unwrap(); +} + +export async function updateFieldSetting( + viewId: string, + fieldId: string, + settings: { + visibility?: FieldVisibility; + width?: number; + } +): Promise { + const payload = FieldSettingsChangesetPB.fromObject({ + view_id: viewId, + field_id: fieldId, + ...settings, + }); + + const result = await DatabaseEventUpdateFieldSettings(payload); + + if (result.ok === false) { + return Promise.reject('Failed to update field settings'); + } + + return result.val; +} + +export const reorderFields = (list: Field[], startIndex: number, endIndex: number) => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + + result.splice(endIndex, 0, removed); + + return result; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_types.ts new file mode 100644 index 0000000000..00e7e02d4e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/field_types.ts @@ -0,0 +1,51 @@ +import { FieldPB, FieldType, FieldVisibility } from '@/services/backend'; + +export interface Field { + id: string; + name: string; + type: FieldType; + visibility?: FieldVisibility; + width?: number; + isPrimary: boolean; +} + +export interface NumberField extends Field { + type: FieldType.Number; +} + +export interface DateTimeField extends Field { + type: FieldType.DateTime; +} + +export interface LastEditedTimeField extends Field { + type: FieldType.LastEditedTime; +} + +export interface CreatedTimeField extends Field { + type: FieldType.CreatedTime; +} + +export type UndeterminedDateField = DateTimeField | CreatedTimeField | LastEditedTimeField; + +export interface SelectField extends Field { + type: FieldType.SingleSelect | FieldType.MultiSelect; +} + +export interface ChecklistField extends Field { + type: FieldType.Checklist; +} + +export interface DateTimeField extends Field { + type: FieldType.DateTime; +} + +export type UndeterminedField = NumberField | DateTimeField | SelectField | Field; + +export const pbToField = (pb: FieldPB): Field => { + return { + id: pb.id, + name: pb.name, + type: pb.field_type, + isPrimary: pb.is_primary, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/index.ts new file mode 100644 index 0000000000..fa993023e1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/index.ts @@ -0,0 +1,5 @@ +export * from './select_option'; +export * from './type_option'; +export * from './field_types'; +export * as fieldService from './field_service'; +export * as fieldListeners from './field_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/index.ts new file mode 100644 index 0000000000..f0b9e58852 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/index.ts @@ -0,0 +1,2 @@ +export * from './select_option_types'; +export * as selectOptionService from './select_option_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_service.ts new file mode 100644 index 0000000000..5757b8185d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_service.ts @@ -0,0 +1,58 @@ +import { CreateSelectOptionPayloadPB, RepeatedSelectOptionPayload } from '@/services/backend'; +import { + DatabaseEventCreateSelectOption, + DatabaseEventInsertOrUpdateSelectOption, + DatabaseEventDeleteSelectOption, +} from '@/services/backend/events/flowy-database2'; +import { pbToSelectOption, SelectOption } from './select_option_types'; + +export async function createSelectOption(viewId: string, fieldId: string, optionName: string): Promise { + const payload = CreateSelectOptionPayloadPB.fromObject({ + view_id: viewId, + field_id: fieldId, + option_name: optionName, + }); + + const result = await DatabaseEventCreateSelectOption(payload); + + return result.map(pbToSelectOption).unwrap(); +} + +/** + * @param [rowId] If pass the rowId, the cell will select this option after insert or update. + */ +export async function insertOrUpdateSelectOption( + viewId: string, + fieldId: string, + items: Partial[], + rowId?: string +): Promise { + const payload = RepeatedSelectOptionPayload.fromObject({ + view_id: viewId, + field_id: fieldId, + row_id: rowId, + items: items, + }); + + const result = await DatabaseEventInsertOrUpdateSelectOption(payload); + + return result.unwrap(); +} + +export async function deleteSelectOption( + viewId: string, + fieldId: string, + items: Partial[], + rowId?: string +): Promise { + const payload = RepeatedSelectOptionPayload.fromObject({ + view_id: viewId, + field_id: fieldId, + row_id: rowId, + items, + }); + + const result = await DatabaseEventDeleteSelectOption(payload); + + return result.unwrap(); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_types.ts new file mode 100644 index 0000000000..ec36639a7c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/select_option/select_option_types.ts @@ -0,0 +1,15 @@ +import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend'; + +export interface SelectOption { + id: string; + name: string; + color: SelectOptionColorPB; +} + +export function pbToSelectOption(pb: SelectOptionPB): SelectOption { + return { + id: pb.id, + name: pb.name, + color: pb.color, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/index.ts new file mode 100644 index 0000000000..d0b9122d90 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/index.ts @@ -0,0 +1,2 @@ +export * from './type_option_types'; +export * from './type_option_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_service.ts new file mode 100644 index 0000000000..90a0dd3106 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_service.ts @@ -0,0 +1,26 @@ +import { FieldType, TypeOptionChangesetPB } from '@/services/backend'; +import { + DatabaseEventUpdateFieldTypeOption, +} from '@/services/backend/events/flowy-database2'; +import { UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types'; + +export async function updateTypeOption( + viewId: string, + fieldId: string, + fieldType: FieldType, + data: UndeterminedTypeOptionData +) { + const payload = TypeOptionChangesetPB.fromObject({ + view_id: viewId, + field_id: fieldId, + type_option_data: typeOptionDataToPB(data, fieldType)?.serialize(), + }); + + const result = await DatabaseEventUpdateFieldTypeOption(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_types.ts new file mode 100644 index 0000000000..57de7b828c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/field/type_option/type_option_types.ts @@ -0,0 +1,148 @@ +import { + CheckboxTypeOptionPB, + DateFormatPB, + FieldType, + MultiSelectTypeOptionPB, + NumberFormatPB, + NumberTypeOptionPB, + RichTextTypeOptionPB, + SingleSelectTypeOptionPB, + TimeFormatPB, + ChecklistTypeOptionPB, + DateTypeOptionPB, + TimestampTypeOptionPB, +} from '@/services/backend'; +import { pbToSelectOption, SelectOption } from '../select_option'; + +export interface TextTypeOption { + data?: string; +} + +export interface NumberTypeOption { + format?: NumberFormatPB; + scale?: number; + symbol?: string; + name?: string; +} + +export interface DateTimeTypeOption { + dateFormat?: DateFormatPB; + timeFormat?: TimeFormatPB; + timezoneId?: string; +} +export interface TimeStampTypeOption extends DateTimeTypeOption { + includeTime?: boolean; + fieldType?: FieldType; +} + +export interface SelectTypeOption { + options?: SelectOption[]; + disableColor?: boolean; +} + +export interface CheckboxTypeOption { + isSelected?: boolean; +} + +export interface ChecklistTypeOption { + config?: string; +} + +export type UndeterminedTypeOptionData = + | TextTypeOption + | NumberTypeOption + | SelectTypeOption + | CheckboxTypeOption + | ChecklistTypeOption + | DateTimeTypeOption + | TimeStampTypeOption; + +export function typeOptionDataToPB(data: UndeterminedTypeOptionData, fieldType: FieldType) { + switch (fieldType) { + case FieldType.Number: + return NumberTypeOptionPB.fromObject(data as NumberTypeOption); + case FieldType.DateTime: + return dateTimeTypeOptionToPB(data as DateTimeTypeOption); + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return timestampTypeOptionToPB(data as TimeStampTypeOption); + + default: + return null; + } +} + +function dateTimeTypeOptionToPB(data: DateTimeTypeOption): DateTypeOptionPB { + return DateTypeOptionPB.fromObject({ + time_format: data.timeFormat, + date_format: data.dateFormat, + timezone_id: data.timezoneId, + }); +} + +function timestampTypeOptionToPB(data: TimeStampTypeOption): TimestampTypeOptionPB { + return TimestampTypeOptionPB.fromObject({ + include_time: data.includeTime, + date_format: data.dateFormat, + time_format: data.timeFormat, + field_type: data.fieldType, + }); +} + +function pbToSelectTypeOption(pb: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB): SelectTypeOption { + return { + options: pb.options?.map(pbToSelectOption), + disableColor: pb.disable_color, + }; +} + +function pbToCheckboxTypeOption(pb: CheckboxTypeOptionPB): CheckboxTypeOption { + return { + isSelected: pb.dummy_field, + }; +} + +function pbToChecklistTypeOption(pb: ChecklistTypeOptionPB): ChecklistTypeOption { + return { + config: pb.config, + }; +} + +function pbToDateTypeOption(pb: DateTypeOptionPB): DateTimeTypeOption { + return { + dateFormat: pb.date_format, + timezoneId: pb.timezone_id, + timeFormat: pb.time_format, + }; +} + +function pbToTimeStampTypeOption(pb: TimestampTypeOptionPB): TimeStampTypeOption { + return { + includeTime: pb.include_time, + dateFormat: pb.date_format, + timeFormat: pb.time_format, + fieldType: pb.field_type, + }; +} + +export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) { + switch (fieldType) { + case FieldType.RichText: + return RichTextTypeOptionPB.deserialize(data).toObject() as TextTypeOption; + case FieldType.Number: + return NumberTypeOptionPB.deserialize(data).toObject() as NumberTypeOption; + case FieldType.SingleSelect: + return pbToSelectTypeOption(SingleSelectTypeOptionPB.deserialize(data)); + case FieldType.MultiSelect: + return pbToSelectTypeOption(MultiSelectTypeOptionPB.deserialize(data)); + case FieldType.Checkbox: + return pbToCheckboxTypeOption(CheckboxTypeOptionPB.deserialize(data)); + case FieldType.Checklist: + return pbToChecklistTypeOption(ChecklistTypeOptionPB.deserialize(data)); + case FieldType.DateTime: + return pbToDateTypeOption(DateTypeOptionPB.deserialize(data)); + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return pbToTimeStampTypeOption(TimestampTypeOptionPB.deserialize(data)); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts new file mode 100644 index 0000000000..72526b577f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts @@ -0,0 +1,42 @@ +import { + CheckboxFilterConditionPB, + ChecklistFilterConditionPB, + FieldType, + NumberFilterConditionPB, + SelectOptionFilterConditionPB, + TextFilterConditionPB, +} from '@/services/backend'; +import { UndeterminedFilter } from '$app/application/database'; + +export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data'] | undefined { + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return { + condition: TextFilterConditionPB.TextContains, + content: '', + }; + case FieldType.Number: + return { + condition: NumberFilterConditionPB.NumberIsNotEmpty, + }; + case FieldType.Checkbox: + return { + condition: CheckboxFilterConditionPB.IsUnChecked, + }; + case FieldType.Checklist: + return { + condition: ChecklistFilterConditionPB.IsIncomplete, + }; + case FieldType.SingleSelect: + return { + condition: SelectOptionFilterConditionPB.OptionIs, + }; + case FieldType.MultiSelect: + return { + condition: SelectOptionFilterConditionPB.OptionContains, + }; + default: + return; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts new file mode 100644 index 0000000000..323f8dac82 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts @@ -0,0 +1,8 @@ +import { Database, pbToFilter } from '$app/application/database'; +import { FilterChangesetNotificationPB } from '@/services/backend'; + +export const didUpdateFilter = (database: Database, changeset: FilterChangesetNotificationPB) => { + const filters = changeset.filters.items.map((pb) => pbToFilter(pb)); + + database.filters = filters; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts new file mode 100644 index 0000000000..6283763d28 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts @@ -0,0 +1,88 @@ +import { + DatabaseEventGetAllFilters, + DatabaseEventUpdateDatabaseSetting, + DatabaseSettingChangesetPB, + DatabaseViewIdPB, + FieldType, + FilterPB, +} from '@/services/backend/events/flowy-database2'; +import { Filter, filterDataToPB, UndeterminedFilter } from './filter_types'; + +export async function getAllFilters(viewId: string): Promise { + const payload = DatabaseViewIdPB.fromObject({ value: viewId }); + + const result = await DatabaseEventGetAllFilters(payload); + + return result.map((value) => value.items).unwrap(); +} + +export async function insertFilter({ + viewId, + fieldId, + fieldType, + data, +}: { + viewId: string; + fieldId: string; + fieldType: FieldType; + data?: UndeterminedFilter['data']; +}): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + insert_filter: { + data: { + field_id: fieldId, + field_type: fieldType, + data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined, + }, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return result.val; +} + +export async function updateFilter(viewId: string, filter: UndeterminedFilter): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + update_filter_data: { + filter_id: filter.id, + data: { + field_id: filter.fieldId, + field_type: filter.fieldType, + data: filterDataToPB(filter.data, filter.fieldType)?.serialize(), + }, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return result.val; +} + +export async function deleteFilter(viewId: string, filter: Omit): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + delete_filter: { + filter_id: filter.id, + field_id: filter.fieldId, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return result.val; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts new file mode 100644 index 0000000000..f9f80985e5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts @@ -0,0 +1,202 @@ +import { + CheckboxFilterConditionPB, + CheckboxFilterPB, + FieldType, + FilterPB, + NumberFilterConditionPB, + NumberFilterPB, + SelectOptionFilterConditionPB, + SelectOptionFilterPB, + TextFilterConditionPB, + TextFilterPB, + ChecklistFilterConditionPB, + ChecklistFilterPB, + DateFilterConditionPB, + DateFilterPB, +} from '@/services/backend'; + +export interface Filter { + id: string; + fieldId: string; + fieldType: FieldType; + data: unknown; +} + +export interface TextFilter extends Filter { + fieldType: FieldType.RichText; + data: TextFilterData; +} + +export interface TextFilterData { + condition: TextFilterConditionPB; + content?: string; +} + +export interface SelectFilter extends Filter { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectFilterData; +} + +export interface NumberFilter extends Filter { + fieldType: FieldType.Number; + data: NumberFilterData; +} + +export interface CheckboxFilter extends Filter { + fieldType: FieldType.Checkbox; + data: CheckboxFilterData; +} + +export interface CheckboxFilterData { + condition?: CheckboxFilterConditionPB; +} + +export interface ChecklistFilter extends Filter { + fieldType: FieldType.Checklist; + data: ChecklistFilterData; +} + +export interface DateFilter extends Filter { + fieldType: FieldType.DateTime | FieldType.CreatedTime | FieldType.LastEditedTime; + data: DateFilterData; +} + +export interface ChecklistFilterData { + condition?: ChecklistFilterConditionPB; +} + +export interface SelectFilterData { + condition?: SelectOptionFilterConditionPB; + optionIds?: string[]; +} + +export interface NumberFilterData { + condition: NumberFilterConditionPB; + content?: string; +} + +export interface DateFilterData { + condition: DateFilterConditionPB; + start?: number; + end?: number; + timestamp?: number; +} + +export type UndeterminedFilter = + | TextFilter + | SelectFilter + | NumberFilter + | CheckboxFilter + | ChecklistFilter + | DateFilter; + +export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: FieldType) { + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return TextFilterPB.fromObject({ + condition: (data as TextFilterData).condition, + content: (data as TextFilterData).content, + }); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionFilterPB.fromObject({ + condition: (data as SelectFilterData).condition, + option_ids: (data as SelectFilterData).optionIds, + }); + case FieldType.Number: + return NumberFilterPB.fromObject({ + condition: (data as NumberFilterData).condition, + content: (data as NumberFilterData).content, + }); + case FieldType.Checkbox: + return CheckboxFilterPB.fromObject({ + condition: (data as CheckboxFilterData).condition, + }); + case FieldType.Checklist: + return ChecklistFilterPB.fromObject({ + condition: (data as ChecklistFilterData).condition, + }); + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return DateFilterPB.fromObject({ + condition: (data as DateFilterData).condition, + start: (data as DateFilterData).start, + end: (data as DateFilterData).end, + timestamp: (data as DateFilterData).timestamp, + }); + } +} + +export function pbToTextFilterData(pb: TextFilterPB): TextFilterData { + return { + condition: pb.condition, + content: pb.content, + }; +} + +export function pbToSelectFilterData(pb: SelectOptionFilterPB): SelectFilterData { + return { + condition: pb.condition, + optionIds: pb.option_ids, + }; +} + +export function pbToNumberFilterData(pb: NumberFilterPB): NumberFilterData { + return { + condition: pb.condition, + content: pb.content, + }; +} + +export function pbToCheckboxFilterData(pb: CheckboxFilterPB): CheckboxFilterData { + return { + condition: pb.condition, + }; +} + +export function pbToChecklistFilterData(pb: ChecklistFilterPB): ChecklistFilterData { + return { + condition: pb.condition, + }; +} + +export function pbToDateFilterData(pb: DateFilterPB): DateFilterData { + return { + condition: pb.condition, + start: pb.start, + end: pb.end, + timestamp: pb.timestamp, + }; +} + +export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) { + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return pbToTextFilterData(TextFilterPB.deserialize(bytes)); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return pbToSelectFilterData(SelectOptionFilterPB.deserialize(bytes)); + case FieldType.Number: + return pbToNumberFilterData(NumberFilterPB.deserialize(bytes)); + case FieldType.Checkbox: + return pbToCheckboxFilterData(CheckboxFilterPB.deserialize(bytes)); + case FieldType.Checklist: + return pbToChecklistFilterData(ChecklistFilterPB.deserialize(bytes)); + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return pbToDateFilterData(DateFilterPB.deserialize(bytes)); + } +} + +export function pbToFilter(pb: FilterPB): Filter { + return { + id: pb.id, + fieldId: pb.data.field_id, + fieldType: pb.data.field_type, + data: bytesToFilterData(pb.data.data, pb.data.field_type), + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/index.ts new file mode 100644 index 0000000000..ac10d27d0a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/index.ts @@ -0,0 +1,3 @@ +export * from './filter_types'; +export * as filterService from './filter_service'; +export * as filterListeners from './filter_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_service.ts new file mode 100644 index 0000000000..24f24d65ec --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_service.ts @@ -0,0 +1,64 @@ +import { + DatabaseViewIdPB, + GroupByFieldPayloadPB, + MoveGroupPayloadPB, + UpdateGroupPB, +} from '@/services/backend'; +import { + DatabaseEventGetGroups, + DatabaseEventMoveGroup, + DatabaseEventSetGroupByField, + DatabaseEventUpdateGroup, +} from '@/services/backend/events/flowy-database2'; +import { Group, pbToGroup } from './group_types'; + +export async function getGroups(viewId: string): Promise { + const payload = DatabaseViewIdPB.fromObject({ value: viewId }); + + const result = await DatabaseEventGetGroups(payload); + + return result.map(value => value.items.map(pbToGroup)).unwrap(); +} + +export async function setGroupByField(viewId: string, fieldId: string): Promise { + const payload = GroupByFieldPayloadPB.fromObject({ + view_id: viewId, + field_id: fieldId, + }); + + const result = await DatabaseEventSetGroupByField(payload); + + return result.unwrap(); +} + +export async function updateGroup( + viewId: string, + group: { + id: string, + name?: string, + visible?: boolean, + }, +): Promise { + const payload = UpdateGroupPB.fromObject({ + view_id: viewId, + group_id: group.id, + name: group.name, + visible: group.visible, + }); + + const result = await DatabaseEventUpdateGroup(payload); + + return result.unwrap(); +} + +export async function moveGroup(viewId: string, fromGroupId: string, toGroupId: string): Promise { + const payload = MoveGroupPayloadPB.fromObject({ + view_id: viewId, + from_group_id: fromGroupId, + to_group_id: toGroupId, + }); + + const result = await DatabaseEventMoveGroup(payload); + + return result.unwrap(); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts new file mode 100644 index 0000000000..b75ecc0bd4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts @@ -0,0 +1,32 @@ +import { GroupPB, GroupSettingPB } from '@/services/backend'; +import { pbToRowMeta, RowMeta } from '../row'; + +export interface GroupSetting { + id: string; + fieldId: string; +} + +export interface Group { + id: string; + isDefault: boolean; + isVisible: boolean; + fieldId: string; + rows: RowMeta[]; +} + +export function pbToGroup(pb: GroupPB): Group { + return { + id: pb.group_id, + isDefault: pb.is_default, + isVisible: pb.is_visible, + fieldId: pb.field_id, + rows: pb.rows.map(pbToRowMeta), + }; +} + +export function pbToGroupSetting(pb: GroupSettingPB): GroupSetting { + return { + id: pb.id, + fieldId: pb.field_id, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/group/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/index.ts new file mode 100644 index 0000000000..bb872d6677 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/index.ts @@ -0,0 +1,2 @@ +export * from './group_types'; +export * as groupService from './group_service'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/index.ts new file mode 100644 index 0000000000..f44da5b857 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/index.ts @@ -0,0 +1,8 @@ +export * from './cell'; +export * from './database'; +export * from './database_view'; +export * from './field'; +export * from './filter'; +export * from './group'; +export * from './row'; +export * from './sort'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/index.ts new file mode 100644 index 0000000000..69260223ef --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/index.ts @@ -0,0 +1,3 @@ +export * from './row_types'; +export * as rowService from './row_service'; +export * as rowListeners from './row_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_listeners.ts new file mode 100644 index 0000000000..e8a638403e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_listeners.ts @@ -0,0 +1,95 @@ +import { ReorderAllRowsPB, ReorderSingleRowPB, RowsChangePB, RowsVisibilityChangePB } from '@/services/backend'; +import { Database } from '../database'; +import { pbToRowMeta, RowMeta } from './row_types'; +import { didDeleteCells } from '$app/application/database/cell/cell_listeners'; +import { getDatabase } from '$app/application/database/database/database_service'; + +const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => { + changeset.deleted_rows.forEach((rowId) => { + const index = database.rowMetas.findIndex((row) => row.id === rowId); + + if (index !== -1) { + database.rowMetas.splice(index, 1); + // delete cells + didDeleteCells({ database, rowId }); + } + }); +}; + +const updateRowsFromChangeset = (database: Database, changeset: RowsChangePB) => { + changeset.updated_rows.forEach(({ row_id: rowId, row_meta: rowMetaPB }) => { + const found = database.rowMetas.find((rowMeta) => rowMeta.id === rowId); + + if (found) { + Object.assign(found, rowMetaPB ? pbToRowMeta(rowMetaPB) : {}); + } + }); +}; + +export const didUpdateViewRows = async (viewId: string, database: Database, changeset: RowsChangePB) => { + if (changeset.inserted_rows.length > 0) { + const { rowMetas } = await getDatabase(viewId); + + database.rowMetas = rowMetas; + return; + } + + deleteRowsFromChangeset(database, changeset); + updateRowsFromChangeset(database, changeset); +}; + +export const didReorderRows = (database: Database, changeset: ReorderAllRowsPB) => { + const rowById = database.rowMetas.reduce>((prev, cur) => { + prev[cur.id] = cur; + return prev; + }, {}); + + database.rowMetas = changeset.row_orders.map((rowId) => rowById[rowId]); +}; + +export const didReorderSingleRow = (database: Database, changeset: ReorderSingleRowPB) => { + const { row_id: rowId, new_index: newIndex } = changeset; + + const oldIndex = database.rowMetas.findIndex((rowMeta) => rowMeta.id === rowId); + + if (oldIndex !== -1) { + database.rowMetas.splice(newIndex, 0, database.rowMetas.splice(oldIndex, 1)[0]); + } +}; + +export const didUpdateViewRowsVisibility = async ( + viewId: string, + database: Database, + changeset: RowsVisibilityChangePB +) => { + const { invisible_rows, visible_rows } = changeset; + + let reFetchRows = false; + + for (const rowId of invisible_rows) { + const rowMeta = database.rowMetas.find((rowMeta) => rowMeta.id === rowId); + + if (rowMeta) { + rowMeta.isHidden = true; + } + } + + for (const insertedRow of visible_rows) { + const rowMeta = database.rowMetas.find((rowMeta) => rowMeta.id === insertedRow.row_meta.id); + + if (rowMeta) { + rowMeta.isHidden = false; + } else { + reFetchRows = true; + break; + } + } + + if (reFetchRows) { + const { rowMetas } = await getDatabase(viewId); + + database.rowMetas = rowMetas; + + await didUpdateViewRowsVisibility(viewId, database, changeset); + } +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts new file mode 100644 index 0000000000..029da3b0c9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts @@ -0,0 +1,129 @@ +import { + CreateRowPayloadPB, + MoveGroupRowPayloadPB, + MoveRowPayloadPB, + OrderObjectPositionTypePB, + RowIdPB, + UpdateRowMetaChangesetPB, +} from '@/services/backend'; +import { + DatabaseEventCreateRow, + DatabaseEventDeleteRow, + DatabaseEventDuplicateRow, + DatabaseEventGetRowMeta, + DatabaseEventMoveGroupRow, + DatabaseEventMoveRow, + DatabaseEventUpdateRowMeta, +} from '@/services/backend/events/flowy-database2'; +import { pbToRowMeta, RowMeta } from './row_types'; + +export async function createRow(viewId: string, params?: { + position?: OrderObjectPositionTypePB; + rowId?: string; + groupId?: string; + data?: Record; +}): Promise { + const payload = CreateRowPayloadPB.fromObject({ + view_id: viewId, + row_position: { + position: params?.position, + object_id: params?.rowId, + }, + group_id: params?.groupId, + data: params?.data, + }); + + const result = await DatabaseEventCreateRow(payload); + + return result.map(pbToRowMeta).unwrap(); +} + +export async function duplicateRow(viewId: string, rowId: string, groupId?: string): Promise { + const payload = RowIdPB.fromObject({ + view_id: viewId, + row_id: rowId, + group_id: groupId, + }); + + const result = await DatabaseEventDuplicateRow(payload); + + return result.unwrap(); +} + +export async function deleteRow(viewId: string, rowId: string, groupId?: string): Promise { + const payload = RowIdPB.fromObject({ + view_id: viewId, + row_id: rowId, + group_id: groupId, + }); + + const result = await DatabaseEventDeleteRow(payload); + + return result.unwrap(); +} + +export async function moveRow(viewId: string, fromRowId: string, toRowId: string): Promise { + const payload = MoveRowPayloadPB.fromObject({ + view_id: viewId, + from_row_id: fromRowId, + to_row_id: toRowId, + }); + + const result = await DatabaseEventMoveRow(payload); + + return result.unwrap(); +} + +/** + * Move the row from one group to another group + * + * @param fromRowId + * @param toGroupId + * @param toRowId used to locate the moving row location. + * @returns + */ +export async function moveGroupRow(viewId: string, fromRowId: string, toGroupId: string, toRowId?: string): Promise { + const payload = MoveGroupRowPayloadPB.fromObject({ + view_id: viewId, + from_row_id: fromRowId, + to_group_id: toGroupId, + to_row_id: toRowId, + }); + + const result = await DatabaseEventMoveGroupRow(payload); + + return result.unwrap(); +} + + +export async function getRowMeta(viewId: string, rowId: string, groupId?: string): Promise { + const payload = RowIdPB.fromObject({ + view_id: viewId, + row_id: rowId, + group_id: groupId, + }); + + const result = await DatabaseEventGetRowMeta(payload); + + return result.map(pbToRowMeta).unwrap(); +} + +export async function updateRowMeta( + viewId: string, + rowId: string, + meta: { + iconUrl?: string; + coverUrl?: string; + }, +): Promise { + const payload = UpdateRowMetaChangesetPB.fromObject({ + view_id: viewId, + id: rowId, + icon_url: meta.iconUrl, + cover_url: meta.coverUrl, + }); + + const result = await DatabaseEventUpdateRowMeta(payload); + + return result.unwrap(); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_types.ts new file mode 100644 index 0000000000..1b964a6bb5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_types.ts @@ -0,0 +1,29 @@ +import { RowMetaPB } from '@/services/backend'; + +export interface RowMeta { + id: string; + documentId?: string; + icon?: string; + cover?: string; + isHidden?: boolean; +} + +export function pbToRowMeta(pb: RowMetaPB): RowMeta { + const rowMeta: RowMeta = { + id: pb.id, + }; + + if (pb.document_id) { + rowMeta.documentId = pb.document_id; + } + + if (pb.icon) { + rowMeta.icon = pb.icon; + } + + if (pb.cover) { + rowMeta.cover = pb.cover; + } + + return rowMeta; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/index.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/index.ts new file mode 100644 index 0000000000..6c7d4bd60a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/index.ts @@ -0,0 +1,3 @@ +export * from './sort_types'; +export * as sortService from './sort_service'; +export * as sortListeners from './sort_listeners'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts new file mode 100644 index 0000000000..808c62e0d2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts @@ -0,0 +1,35 @@ +import { SortChangesetNotificationPB } from '@/services/backend'; +import { Database } from '../database'; +import { pbToSort } from './sort_types'; + +const deleteSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => { + const deleteIds = changeset.delete_sorts.map(sort => sort.id); + + if (deleteIds.length) { + database.sorts = database.sorts.filter(sort => !deleteIds.includes(sort.id)); + } +}; + +const insertSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => { + changeset.insert_sorts.forEach(sortPB => { + database.sorts.push(pbToSort(sortPB.sort)); + }); +}; + +const updateSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => { + changeset.update_sorts.forEach(sortPB => { + const found = database.sorts.find(sort => sort.id === sortPB.id); + + if (found) { + const newSort = pbToSort(sortPB); + + Object.assign(found, newSort); + } + }); +}; + +export const didUpdateSort = (database: Database, changeset: SortChangesetNotificationPB) => { + deleteSortsFromChange(database, changeset); + insertSortsFromChange(database, changeset); + updateSortsFromChange(database, changeset); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts new file mode 100644 index 0000000000..2546ec780c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts @@ -0,0 +1,73 @@ +import { + DatabaseViewIdPB, + DatabaseSettingChangesetPB, +} from '@/services/backend'; +import { + DatabaseEventDeleteAllSorts, + DatabaseEventGetAllSorts, DatabaseEventUpdateDatabaseSetting, +} from '@/services/backend/events/flowy-database2'; +import { pbToSort, Sort } from './sort_types'; + +export async function getAllSorts(viewId: string): Promise { + const payload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + + const result = await DatabaseEventGetAllSorts(payload); + + return result.map(value => value.items.map(pbToSort)).unwrap(); +} + +export async function insertSort(viewId: string, sort: Omit): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + update_sort: { + view_id: viewId, + field_id: sort.fieldId, + condition: sort.condition, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + return result.unwrap(); +} + +export async function updateSort(viewId: string, sort: Sort): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + update_sort: { + view_id: viewId, + sort_id: sort.id, + field_id: sort.fieldId, + condition: sort.condition, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + return result.unwrap(); +} + +export async function deleteSort(viewId: string, sort: Sort): Promise { + const payload = DatabaseSettingChangesetPB.fromObject({ + view_id: viewId, + delete_sort: { + view_id: viewId, + sort_id: sort.id, + }, + }); + + const result = await DatabaseEventUpdateDatabaseSetting(payload); + + return result.unwrap(); +} + +export async function deleteAllSorts(viewId: string): Promise { + const payload = DatabaseViewIdPB.fromObject({ + value: viewId, + }); + const result = await DatabaseEventDeleteAllSorts(payload); + + return result.unwrap(); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_types.ts new file mode 100644 index 0000000000..a8089878d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_types.ts @@ -0,0 +1,15 @@ +import { SortConditionPB, SortPB } from '@/services/backend'; + +export interface Sort { + id: string; + fieldId: string; + condition: SortConditionPB; +} + +export function pbToSort(pb: SortPB): Sort { + return { + id: pb.id, + fieldId: pb.field_id, + condition: pb.condition, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts new file mode 100644 index 0000000000..0db128ec7a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts @@ -0,0 +1,284 @@ +import { + ApplyActionPayloadPB, + BlockActionPB, + BlockPB, + CloseDocumentPayloadPB, + ConvertDataToJsonPayloadPB, + ConvertDocumentPayloadPB, + InputType, + OpenDocumentPayloadPB, + TextDeltaPayloadPB, +} from '@/services/backend'; +import { + DocumentEventApplyAction, + DocumentEventApplyTextDeltaEvent, + DocumentEventCloseDocument, + DocumentEventConvertDataToJSON, + DocumentEventConvertDocument, + DocumentEventOpenDocument, +} from '@/services/backend/events/flowy-document'; +import get from 'lodash-es/get'; +import { EditorData, EditorNodeType } from '$app/application/document/document.types'; +import { Log } from '$app/utils/log'; +import { Op } from 'quill-delta'; +import { Element, Text } from 'slate'; +import { generateId, getInlinesWithDelta } from '$app/components/editor/provider/utils/convert'; +import { CustomEditor } from '$app/components/editor/command'; +import { LIST_TYPES } from '$app/components/editor/command/tab'; + +export function blockPB2Node(block: BlockPB) { + let data = {}; + + try { + data = JSON.parse(block.data); + } catch { + Log.error('[Document Open] json parse error', block.data); + } + + return { + id: block.id, + type: block.ty as EditorNodeType, + parent: block.parent_id, + children: block.children_id, + data, + externalId: block.external_id, + externalType: block.external_type, + }; +} + +export const BLOCK_MAP_NAME = 'blocks'; +export const META_NAME = 'meta'; +export const CHILDREN_MAP_NAME = 'children_map'; + +export const TEXT_MAP_NAME = 'text_map'; +export async function openDocument(docId: string): Promise { + const payload = OpenDocumentPayloadPB.fromObject({ + document_id: docId, + }); + + const result = await DocumentEventOpenDocument(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + const documentDataPB = result.val; + + if (!documentDataPB) { + return Promise.reject('documentDataPB is null'); + } + + const data: EditorData = { + viewId: docId, + rootId: documentDataPB.page_id, + nodeMap: {}, + childrenMap: {}, + relativeMap: {}, + deltaMap: {}, + externalIdMap: {}, + }; + + get(documentDataPB, BLOCK_MAP_NAME).forEach((block) => { + Object.assign(data.nodeMap, { + [block.id]: blockPB2Node(block), + }); + data.relativeMap[block.children_id] = block.id; + if (block.external_id) { + data.externalIdMap[block.external_id] = block.id; + } + }); + + get(documentDataPB, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => { + const blockId = data.relativeMap[key]; + + data.childrenMap[blockId] = child.children; + }); + + get(documentDataPB, [META_NAME, TEXT_MAP_NAME]).forEach((delta, key) => { + const blockId = data.externalIdMap[key]; + + data.deltaMap[blockId] = delta ? JSON.parse(delta) : []; + }); + + return data; +} + +export async function closeDocument(docId: string) { + const payload = CloseDocumentPayloadPB.fromObject({ + document_id: docId, + }); + + const result = await DocumentEventCloseDocument(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return result.val; +} + +export async function applyActions(docId: string, actions: ReturnType[]) { + if (actions.length === 0) return; + const payload = ApplyActionPayloadPB.fromObject({ + document_id: docId, + actions: actions, + }); + + const result = await DocumentEventApplyAction(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return result.val; +} + +export async function applyText(docId: string, textId: string, delta: string) { + const payload = TextDeltaPayloadPB.fromObject({ + document_id: docId, + text_id: textId, + delta: delta, + }); + + const res = await DocumentEventApplyTextDeltaEvent(payload); + + if (!res.ok) { + return Promise.reject(res.val); + } + + return res.val; +} + +export async function getClipboardData( + docId: string, + range: { + start: { + blockId: string; + index: number; + length: number; + }; + end?: { + blockId: string; + index: number; + length: number; + }; + } +) { + const payload = ConvertDocumentPayloadPB.fromObject({ + range: { + start: { + block_id: range.start.blockId, + index: range.start.index, + length: range.start.length, + }, + end: range.end + ? { + block_id: range.end.blockId, + index: range.end.index, + length: range.end.length, + } + : undefined, + }, + document_id: docId, + parse_types: { + json: true, + html: true, + text: true, + }, + }); + + const result = await DocumentEventConvertDocument(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return { + html: result.val.html, + text: result.val.text, + json: result.val.json, + }; +} + +export async function convertBlockToJson(data: string, type: InputType) { + const payload = ConvertDataToJsonPayloadPB.fromObject({ + data, + input_type: type, + }); + + const result = await DocumentEventConvertDataToJSON(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + try { + const block = JSON.parse(result.val.json); + + return flattenBlockJson(block); + } catch (e) { + return Promise.reject(e); + } +} + +interface BlockJSON { + type: string; + children: BlockJSON[]; + data: { + [key: string]: boolean | string | number | undefined; + } & { + delta?: Op[]; + }; +} + +function flattenBlockJson(block: BlockJSON) { + const traverse = (block: BlockJSON) => { + const { delta, ...data } = block.data; + + const slateNode: Element = { + type: block.type, + data: data, + children: [], + blockId: generateId(), + }; + const isEmbed = CustomEditor.isEmbedNode(slateNode); + + const textNode: { + type: EditorNodeType.Text; + children: (Text | Element)[]; + textId: string; + } | null = !isEmbed + ? { + type: EditorNodeType.Text, + children: [{ text: '' }], + textId: generateId(), + } + : null; + + if (delta && textNode) { + textNode.children = getInlinesWithDelta(delta); + } + + slateNode.children = block.children.map((child) => traverse(child)); + + if (textNode) { + const texts = CustomEditor.getNodeTextContent(textNode); + + if (texts && !LIST_TYPES.includes(block.type as EditorNodeType) && slateNode.type !== EditorNodeType.Page) { + slateNode.children.unshift(textNode); + } else if (texts) { + slateNode.children.unshift({ + type: EditorNodeType.Paragraph, + children: [textNode], + blockId: generateId(), + }); + } + } + + return slateNode; + }; + + const root = traverse(block); + + return root.children; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts new file mode 100644 index 0000000000..e6eb1d6923 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -0,0 +1,242 @@ +import { Op } from 'quill-delta'; +import { HTMLAttributes } from 'react'; +import { Element } from 'slate'; +import { ViewIconTypePB, ViewLayoutPB } from '@/services/backend'; +import { PageCover } from '$app_reducers/pages/slice'; +import * as Y from 'yjs'; + +export interface EditorNode { + id: string; + type: EditorNodeType; + parent?: string | null; + data?: BlockData; + children?: string; + externalId?: string; + externalType?: string; +} + +export interface TextNode extends Element { + type: EditorNodeType.Text; + textId: string; + blockId: string; +} + +export interface PageNode extends Element { + type: EditorNodeType.Page; +} +export interface ParagraphNode extends Element { + type: EditorNodeType.Paragraph; +} + +export type BlockData = { + [key: string]: string | boolean | number | undefined; + font_color?: string; + bg_color?: string; +}; + +export interface HeadingNode extends Element { + blockId: string; + type: EditorNodeType.HeadingBlock; + data: { + level: number; + } & BlockData; +} + +export interface GridNode extends Element { + blockId: string; + type: EditorNodeType.GridBlock; + data: { + viewId?: string; + } & BlockData; +} + +export interface TodoListNode extends Element { + blockId: string; + type: EditorNodeType.TodoListBlock; + data: { + checked: boolean; + } & BlockData; +} + +export interface CodeNode extends Element { + blockId: string; + type: EditorNodeType.CodeBlock; + data: { + language: string; + } & BlockData; +} + +export interface QuoteNode extends Element { + blockId: string; + type: EditorNodeType.QuoteBlock; +} + +export interface NumberedListNode extends Element { + type: EditorNodeType.NumberedListBlock; + blockId: string; + data: { + number?: number; + } & BlockData; +} + +export interface BulletedListNode extends Element { + type: EditorNodeType.BulletedListBlock; + blockId: string; +} + +export interface ToggleListNode extends Element { + type: EditorNodeType.ToggleListBlock; + blockId: string; + data: { + collapsed: boolean; + } & BlockData; +} + +export interface DividerNode extends Element { + type: EditorNodeType.DividerBlock; + blockId: string; +} + +export interface CalloutNode extends Element { + type: EditorNodeType.CalloutBlock; + blockId: string; + data: { + icon: string; + } & BlockData; +} + +export interface MathEquationNode extends Element { + type: EditorNodeType.EquationBlock; + blockId: string; + data: { + formula?: string; + } & BlockData; +} + +export enum ImageType { + Local = 0, + Internal = 1, + External = 2, +} + +export interface ImageNode extends Element { + type: EditorNodeType.ImageBlock; + blockId: string; + data: { + url?: string; + width?: number; + image_type?: ImageType; + height?: number; + } & BlockData; +} + +export interface FormulaNode extends Element { + type: EditorInlineNodeType.Formula; + data: string; +} + +export interface MentionNode extends Element { + type: EditorInlineNodeType.Mention; + data: Mention; +} + +export interface EditorData { + viewId: string; + rootId: string; + // key: block's id, value: block + nodeMap: Record; + // key: block's children id, value: block's id + childrenMap: Record; + // key: block's children id, value: block's id + relativeMap: Record; + // key: block's externalId, value: delta + deltaMap: Record; + // key: block's externalId, value: block's id + externalIdMap: Record; +} + +export interface MentionPage { + id: string; + name: string; + layout: ViewLayoutPB; + parentId: string; + icon?: { + ty: ViewIconTypePB; + value: string; + }; +} + +export interface EditorProps { + title?: string; + cover?: PageCover; + onTitleChange?: (title: string) => void; + onCoverChange?: (cover?: PageCover) => void; + showTitle?: boolean; + id: string; + disableFocus?: boolean; +} + +export interface LocalEditorProps { + disableFocus?: boolean; + sharedType: Y.XmlText; + id: string; + caretColor?: string; +} + +export enum EditorNodeType { + Text = 'text', + Paragraph = 'paragraph', + Page = 'page', + HeadingBlock = 'heading', + TodoListBlock = 'todo_list', + BulletedListBlock = 'bulleted_list', + NumberedListBlock = 'numbered_list', + ToggleListBlock = 'toggle_list', + CodeBlock = 'code', + EquationBlock = 'math_equation', + QuoteBlock = 'quote', + CalloutBlock = 'callout', + DividerBlock = 'divider', + ImageBlock = 'image', + GridBlock = 'grid', +} + +export enum EditorInlineNodeType { + Mention = 'mention', + Formula = 'formula', +} + +export const inlineNodeTypes: (string | EditorInlineNodeType)[] = [ + EditorInlineNodeType.Mention, + EditorInlineNodeType.Formula, +]; + +export interface EditorElementProps extends HTMLAttributes { + node: T; +} + +export enum EditorMarkFormat { + Bold = 'bold', + Italic = 'italic', + Underline = 'underline', + StrikeThrough = 'strikethrough', + Code = 'code', + Href = 'href', + FontColor = 'font_color', + BgColor = 'bg_color', + Align = 'align', +} + +export enum MentionType { + PageRef = 'page', + Date = 'date', +} + +export interface Mention { + // inline page ref id + page_id?: string; + // reminder date ref id + date?: string; + + type: MentionType; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts new file mode 100644 index 0000000000..7d988b9866 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts @@ -0,0 +1,166 @@ +import { Page, PageIcon, parserViewPBToPage } from '$app_reducers/pages/slice'; +import { + CreateOrphanViewPayloadPB, + CreateViewPayloadPB, + MoveNestedViewPayloadPB, + RepeatedViewIdPB, + UpdateViewIconPayloadPB, + UpdateViewPayloadPB, + ViewIconPB, + ViewIdPB, + ViewPB, +} from '@/services/backend'; +import { + FolderEventCreateOrphanView, + FolderEventCreateView, + FolderEventDeleteView, + FolderEventDuplicateView, + FolderEventGetView, + FolderEventMoveNestedView, + FolderEventUpdateView, + FolderEventUpdateViewIcon, + FolderEventSetLatestView, +} from '@/services/backend/events/flowy-folder'; + +export async function getPage(id: string) { + const payload = new ViewIdPB({ + value: id, + }); + + const result = await FolderEventGetView(payload); + + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.val); +} + +export const createOrphanPage = async ( + params: ReturnType +): Promise => { + const payload = CreateOrphanViewPayloadPB.fromObject(params); + + const result = await FolderEventCreateOrphanView(payload); + + if (result.ok) { + return parserViewPBToPage(result.val); + } + + return Promise.reject(result.val); +}; + +export const duplicatePage = async (page: Page) => { + const payload = ViewPB.fromObject(page); + + const result = await FolderEventDuplicateView(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +}; + +export const deletePage = async (id: string) => { + const payload = new RepeatedViewIdPB({ + items: [id], + }); + + const result = await FolderEventDeleteView(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +}; + +export const createPage = async (params: ReturnType): Promise => { + const payload = CreateViewPayloadPB.fromObject(params); + + const result = await FolderEventCreateView(payload); + + if (result.ok) { + return result.val.id; + } + + return Promise.reject(result.err); +}; + +export const movePage = async (params: ReturnType) => { + const payload = new MoveNestedViewPayloadPB(params); + + const result = await FolderEventMoveNestedView(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +}; + +export const getChildPages = async (id: string): Promise => { + const payload = new ViewIdPB({ + value: id, + }); + + const result = await FolderEventGetView(payload); + + if (result.ok) { + return result.val.child_views.map(parserViewPBToPage); + } + + return []; +}; + +export const updatePage = async (page: { id: string } & Partial) => { + const payload = new UpdateViewPayloadPB(); + + payload.view_id = page.id; + if (page.name !== undefined) { + payload.name = page.name; + } + + const result = await FolderEventUpdateView(payload); + + if (result.ok) { + return result.val.toObject(); + } + + return Promise.reject(result.err); +}; + +export const updatePageIcon = async (viewId: string, icon?: PageIcon) => { + const payload = new UpdateViewIconPayloadPB({ + view_id: viewId, + icon: icon + ? new ViewIconPB({ + ty: icon.ty, + value: icon.value, + }) + : undefined, + }); + + const result = await FolderEventUpdateViewIcon(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +}; + +export async function setLatestOpenedPage(id: string) { + const payload = new ViewIdPB({ + value: id, + }); + + const res = await FolderEventSetLatestView(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/trash.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/trash.service.ts new file mode 100644 index 0000000000..dfbe742ca0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/trash.service.ts @@ -0,0 +1,68 @@ +import { + FolderEventListTrashItems, + FolderEventPermanentlyDeleteAllTrashItem, + FolderEventPermanentlyDeleteTrashItem, + FolderEventRecoverAllTrashItems, + FolderEventRestoreTrashItem, + RepeatedTrashIdPB, + TrashIdPB, +} from '@/services/backend/events/flowy-folder'; + +export const getTrash = async () => { + const res = await FolderEventListTrashItems(); + + if (res.ok) { + return res.val.items; + } + + return []; +}; + +export const putback = async (id: string) => { + const payload = new TrashIdPB({ + id, + }); + + const res = await FolderEventRestoreTrashItem(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +}; + +export const deleteTrashItem = async (ids: string[]) => { + const items = ids.map((id) => new TrashIdPB({ id })); + const payload = new RepeatedTrashIdPB({ + items, + }); + + const res = await FolderEventPermanentlyDeleteTrashItem(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +}; + +export const deleteAll = async () => { + const res = await FolderEventPermanentlyDeleteAllTrashItem(); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +}; + +export const restoreAll = async () => { + const res = await FolderEventRecoverAllTrashItems(); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts new file mode 100644 index 0000000000..fe066b7377 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts @@ -0,0 +1,141 @@ +import { parserViewPBToPage } from '$app_reducers/pages/slice'; +import { + ChangeWorkspaceIconPB, + CreateViewPayloadPB, + GetWorkspaceViewPB, + RenameWorkspacePB, + UserWorkspaceIdPB, + WorkspaceIdPB, +} from '@/services/backend'; +import { + FolderEventCreateView, + FolderEventDeleteWorkspace, + FolderEventGetCurrentWorkspaceSetting, + FolderEventReadCurrentWorkspace, + FolderEventReadWorkspaceViews, +} from '@/services/backend/events/flowy-folder'; +import { + UserEventChangeWorkspaceIcon, + UserEventGetAllWorkspace, + UserEventOpenWorkspace, + UserEventRenameWorkspace, +} from '@/services/backend/events/flowy-user'; + +export async function openWorkspace(id: string) { + const payload = new UserWorkspaceIdPB({ + workspace_id: id, + }); + + const result = await UserEventOpenWorkspace(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} + +export async function deleteWorkspace(id: string) { + const payload = new WorkspaceIdPB({ + value: id, + }); + + const result = await FolderEventDeleteWorkspace(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} + +export async function getWorkspaceChildViews(id: string) { + const payload = new GetWorkspaceViewPB({ + value: id, + }); + + const result = await FolderEventReadWorkspaceViews(payload); + + if (result.ok) { + return result.val.items.map(parserViewPBToPage); + } + + return []; +} + +export async function getWorkspaces() { + const result = await UserEventGetAllWorkspace(); + + if (result.ok) { + return result.val.items.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.name, + })); + } + + return []; +} + +export async function getCurrentWorkspaceSetting() { + const res = await FolderEventGetCurrentWorkspaceSetting(); + + if (res.ok) { + return res.val; + } + + return; +} + +export async function getCurrentWorkspace() { + const result = await FolderEventReadCurrentWorkspace(); + + if (result.ok) { + return result.val.id; + } + + return null; +} + +export async function createCurrentWorkspaceChildView( + params: ReturnType +) { + const payload = CreateViewPayloadPB.fromObject(params); + + const result = await FolderEventCreateView(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} + +export async function renameWorkspace(id: string, name: string) { + const payload = new RenameWorkspacePB({ + workspace_id: id, + new_name: name, + }); + + const result = await UserEventRenameWorkspace(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} + +export async function changeWorkspaceIcon(id: string, icon: string) { + const payload = new ChangeWorkspaceIconPB({ + workspace_id: id, + new_icon: icon, + }); + + const result = await UserEventChangeWorkspaceIcon(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts b/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts new file mode 100644 index 0000000000..c63a5d9823 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts @@ -0,0 +1,157 @@ +import { listen } from '@tauri-apps/api/event'; +import { SubscribeObject } from '@/services/backend/models/flowy-notification'; +import { + DatabaseFieldChangesetPB, + DatabaseNotification, + DocEventPB, + DocumentNotification, + FieldPB, + FieldSettingsPB, + FilterChangesetNotificationPB, + GroupChangesPB, + GroupRowsNotificationPB, + ReorderAllRowsPB, + ReorderSingleRowPB, + RowsChangePB, + RowsVisibilityChangePB, + SortChangesetNotificationPB, + UserNotification, + UserProfilePB, + FolderNotification, + RepeatedViewPB, + ViewPB, + RepeatedTrashPB, + ChildViewUpdatePB, + WorkspacePB, +} from '@/services/backend'; +import { AsyncQueue } from '$app/utils/async_queue'; + +const Notification = { + [DatabaseNotification.DidUpdateViewRowsVisibility]: RowsVisibilityChangePB, + [DatabaseNotification.DidUpdateViewRows]: RowsChangePB, + [DatabaseNotification.DidReorderRows]: ReorderAllRowsPB, + [DatabaseNotification.DidReorderSingleRow]: ReorderSingleRowPB, + [DatabaseNotification.DidUpdateFields]: DatabaseFieldChangesetPB, + [DatabaseNotification.DidGroupByField]: GroupChangesPB, + [DatabaseNotification.DidUpdateNumOfGroups]: GroupChangesPB, + [DatabaseNotification.DidUpdateGroupRow]: GroupRowsNotificationPB, + [DatabaseNotification.DidUpdateField]: FieldPB, + [DatabaseNotification.DidUpdateCell]: null, + [DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB, + [DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB, + [DatabaseNotification.DidUpdateFilter]: FilterChangesetNotificationPB, + [DocumentNotification.DidReceiveUpdate]: DocEventPB, + [FolderNotification.DidUpdateWorkspace]: WorkspacePB, + [FolderNotification.DidUpdateWorkspaceViews]: RepeatedViewPB, + [FolderNotification.DidUpdateView]: ViewPB, + [FolderNotification.DidUpdateChildViews]: ChildViewUpdatePB, + [FolderNotification.DidUpdateTrash]: RepeatedTrashPB, + [UserNotification.DidUpdateUserProfile]: UserProfilePB, +}; + +type NotificationMap = typeof Notification; +export type NotificationEnum = keyof NotificationMap; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type NullableInstanceType any) | null> = K extends abstract new ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...args: any +) => // eslint-disable-next-line @typescript-eslint/no-explicit-any +any + ? InstanceType + : void; +export type NotificationHandler = ( + result: NullableInstanceType +) => void | Promise; + +/** + * Subscribes to a set of notifications. + * + * This function subscribes to notifications defined by the `NotificationEnum` and + * calls the appropriate `NotificationHandler` when each type of notification is received. + * + * @param {Object} callbacks - An object containing handlers for various notification types. + * Each key is a `NotificationEnum` value, and the corresponding value is a `NotificationHandler` function. + * + * @param {Object} [options] - Optional settings for the subscription. + * @param {string} [options.id] - An optional ID. If provided, only notifications with a matching ID will be processed. + * + * @returns {Promise<() => void>} A Promise that resolves to an unsubscribe function. + * + * @example + * subscribeNotifications({ + * [DatabaseNotification.DidUpdateField]: (result) => { + * if (result.err) { + * // process error + * return; + * } + * + * console.log(result.val); // result.val is FieldPB + * }, + * [DatabaseNotification.DidReorderRows]: (result) => { + * if (result.err) { + * // process error + * return; + * } + * + * console.log(result.val); // result.val is ReorderAllRowsPB + * }, + * }, { id: '123' }) + * .then(unsubscribe => { + * // Do something + * // ... + * // To unsubscribe, call `unsubscribe()` + * }); + * + * @throws {Error} Throws an error if unable to subscribe. + */ +export function subscribeNotifications( + callbacks: { + [K in NotificationEnum]?: NotificationHandler; + }, + options?: { id?: string | number } +): Promise<() => void> { + const handler = async (subject: SubscribeObject) => { + const { id, ty } = subject; + + if (options?.id !== undefined && id !== options.id) { + return; + } + + const notification = ty as NotificationEnum; + const pb = Notification[notification]; + const callback = callbacks[notification] as NotificationHandler; + + if (pb === undefined || !callback) { + return; + } + + if (subject.has_error) { + // const error = FlowyError.deserialize(subject.error); + return; + } else { + const { payload } = subject; + + if (pb) { + await callback(pb.deserialize(payload)); + } else { + await callback(); + } + } + }; + + const queue = new AsyncQueue(handler); + + return listen>('af-notification', (event) => { + const subject = SubscribeObject.fromObject(event.payload); + + queue.enqueue(subject); + }); +} + +export function subscribeNotification( + notification: K, + callback: NotificationHandler, + options?: { id?: string } +): Promise<() => void> { + return subscribeNotifications({ [notification]: callback }, options); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts new file mode 100644 index 0000000000..ec258abc87 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts @@ -0,0 +1,92 @@ +import { + SignUpPayloadPB, + OauthProviderPB, + ProviderTypePB, + OauthSignInPB, + AuthenticatorPB, + SignInPayloadPB, +} from '@/services/backend'; +import { + UserEventSignOut, + UserEventSignUp, + UserEventGetOauthURLWithProvider, + UserEventOauthSignIn, + UserEventSignInWithEmailPassword, +} from '@/services/backend/events/flowy-user'; +import { Log } from '$app/utils/log'; + +export const AuthService = { + getOAuthURL: async (provider: ProviderTypePB) => { + const providerDataRes = await UserEventGetOauthURLWithProvider( + OauthProviderPB.fromObject({ + provider, + }) + ); + + if (!providerDataRes.ok) { + Log.error(providerDataRes.val.msg); + throw new Error(providerDataRes.val.msg); + } + + const providerData = providerDataRes.val; + + return providerData.oauth_url; + }, + + signInWithOAuth: async ({ uri, deviceId }: { uri: string; deviceId: string }) => { + const payload = OauthSignInPB.fromObject({ + authenticator: AuthenticatorPB.AppFlowyCloud, + map: { + sign_in_url: uri, + device_id: deviceId, + }, + }); + + const res = await UserEventOauthSignIn(payload); + + if (!res.ok) { + Log.error(res.val.msg); + throw new Error(res.val.msg); + } + + return res.val; + }, + + signUp: async (params: { deviceId: string; name: string; email: string; password: string }) => { + const payload = SignUpPayloadPB.fromObject({ + name: params.name, + email: params.email, + password: params.password, + device_id: params.deviceId, + }); + + const res = await UserEventSignUp(payload); + + if (!res.ok) { + Log.error(res.val.msg); + throw new Error(res.val.msg); + } + + return res.val; + }, + + signOut: () => { + return UserEventSignOut(); + }, + + signIn: async (email: string, password: string) => { + const payload = SignInPayloadPB.fromObject({ + email, + password, + }); + + const res = await UserEventSignInWithEmailPassword(payload); + + if (!res.ok) { + Log.error(res.val.msg); + throw new Error(res.val.msg); + } + + return res.val; + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts new file mode 100644 index 0000000000..ec64fb810c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts @@ -0,0 +1,68 @@ +import { Theme, ThemeMode, UserSetting } from '$app_reducers/current-user/slice'; +import { AppearanceSettingsPB, UpdateUserProfilePayloadPB } from '@/services/backend'; +import { + UserEventGetAppearanceSetting, + UserEventGetUserProfile, + UserEventSetAppearanceSetting, + UserEventUpdateUserProfile, +} from '@/services/backend/events/flowy-user'; + +export const UserService = { + getAppearanceSetting: async (): Promise | undefined> => { + const appearanceSetting = await UserEventGetAppearanceSetting(); + + if (appearanceSetting.ok) { + const res = appearanceSetting.val; + const { locale, theme = Theme.Default, theme_mode = ThemeMode.Light } = res; + let language = 'en'; + + if (locale.language_code && locale.country_code) { + language = `${locale.language_code}-${locale.country_code}`; + } else if (locale.language_code) { + language = locale.language_code; + } + + return { + themeMode: theme_mode, + theme: theme as Theme, + language: language, + }; + } + + return; + }, + + setAppearanceSetting: async (params: ReturnType) => { + const payload = AppearanceSettingsPB.fromObject(params); + + const res = await UserEventSetAppearanceSetting(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); + }, + + getUserProfile: async () => { + const res = await UserEventGetUserProfile(); + + if (res.ok) { + return res.val; + } + + return; + }, + + updateUserProfile: async (params: ReturnType) => { + const payload = UpdateUserProfilePayloadPB.fromObject(params); + + const res = await UserEventUpdateUserProfile(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/add.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/add.svg new file mode 100644 index 0000000000..049be05cec --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/align-center.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/align-center.svg new file mode 100644 index 0000000000..f4f4999514 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/align-center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/align-left.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/align-left.svg new file mode 100644 index 0000000000..23957285c7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/align-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/align-right.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/align-right.svg new file mode 100644 index 0000000000..bca2d14fc7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/align-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg new file mode 100644 index 0000000000..e4ab9068be --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg new file mode 100644 index 0000000000..dc40ae52a6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg new file mode 100644 index 0000000000..0bb0e3fabe --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/board.svg @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/bold.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/bold.svg new file mode 100644 index 0000000000..878b6329b3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg new file mode 100644 index 0000000000..b519b419c0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/copy.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/copy.svg new file mode 100644 index 0000000000..e21e6cb082 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg new file mode 100644 index 0000000000..80d8c4132e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-check.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-check.svg new file mode 100644 index 0000000000..15632e4ea6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-uncheck.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-uncheck.svg new file mode 100644 index 0000000000..6c487795c6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/checkbox-uncheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-attach.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-attach.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checkbox.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checklist.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checklist.svg new file mode 100644 index 0000000000..3a88d236a1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-date.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-last-edited-time.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-last-edited-time.svg new file mode 100644 index 0000000000..634af3e361 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-last-edited-time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-multi-select.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-multi-select.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-multi-select.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-number.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-person.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-person.svg new file mode 100644 index 0000000000..2fc04be065 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-person.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-relation.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-relation.svg new file mode 100644 index 0000000000..f82a41d226 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-relation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-single-select.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-single-select.svg new file mode 100644 index 0000000000..8ccbc9a2e3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-single-select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-text.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-url.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-url.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/database/field-type-url.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/date.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/delete.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/delete.svg new file mode 100644 index 0000000000..9e51636798 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg new file mode 100644 index 0000000000..22c6830916 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg new file mode 100644 index 0000000000..b00e1cfb38 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/document.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg new file mode 100644 index 0000000000..627c959f9f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/drag.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/dropdown.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/dropdown.svg new file mode 100644 index 0000000000..95e4964b53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/dropdown.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/edit.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/edit.svg new file mode 100644 index 0000000000..ae93287114 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/edit.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/eye_close.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/eye_close.svg new file mode 100644 index 0000000000..116c715ca8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/eye_close.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/eye_open.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/eye_open.svg new file mode 100644 index 0000000000..fa3017c04d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/eye_open.svg @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg new file mode 100644 index 0000000000..c397af8130 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/h1.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/h1.svg new file mode 100644 index 0000000000..b33bd52135 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/h2.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/h2.svg new file mode 100644 index 0000000000..7449c57391 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/h3.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/h3.svg new file mode 100644 index 0000000000..0976945974 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/hide-menu.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/hide-menu.svg new file mode 100644 index 0000000000..ce88af8ea7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/hide-menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/hide.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/hide.svg new file mode 100644 index 0000000000..22001ef65d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/hide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg new file mode 100644 index 0000000000..0739605066 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg new file mode 100644 index 0000000000..aeaa6a0f29 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/images/default_cover.jpg differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg new file mode 100644 index 0000000000..37ca4d5837 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/inline-code.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/inline-code.svg new file mode 100644 index 0000000000..3585603096 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/inline-code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/italic.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/italic.svg new file mode 100644 index 0000000000..b295c230f0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/left.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/left.svg new file mode 100644 index 0000000000..0f771a3858 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg new file mode 100644 index 0000000000..f5cd761ba7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/link.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/list-dropdown.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/list-dropdown.svg new file mode 100644 index 0000000000..4a8424c5f8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/list-dropdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/list.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/list.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/mention.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/mention.svg new file mode 100644 index 0000000000..b98318132c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/mention.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/more.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/more.svg new file mode 100644 index 0000000000..b191e64a10 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/numbers.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/numbers.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/numbers.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/open.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/open.svg new file mode 100644 index 0000000000..b443c8b993 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/open.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/quote.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/quote.svg new file mode 100644 index 0000000000..57839231ff --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/react.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/react.svg new file mode 100644 index 0000000000..6c87de9bb3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/right.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/right.svg new file mode 100644 index 0000000000..7d738f4e69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/search.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/search.svg new file mode 100644 index 0000000000..a8a92df509 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg new file mode 100644 index 0000000000..05caec861a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings.svg new file mode 100644 index 0000000000..92140a3c23 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg new file mode 100644 index 0000000000..fddfca7575 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg new file mode 100644 index 0000000000..c6fa56067b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png new file mode 100644 index 0000000000..15a2db5eb8 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png new file mode 100644 index 0000000000..f71e68c6ed Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png new file mode 100644 index 0000000000..597883b7a3 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png new file mode 100644 index 0000000000..60032628a8 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png new file mode 100644 index 0000000000..09b2d9c475 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg new file mode 100644 index 0000000000..2076ea3e2c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/show-menu.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/show-menu.svg new file mode 100644 index 0000000000..8baf55bffd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/show-menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/sort.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/sort.svg new file mode 100644 index 0000000000..e3b6a49a56 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/sort.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/strikethrough.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/strikethrough.svg new file mode 100644 index 0000000000..c118422a15 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/strikethrough.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/text.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/todo-list.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/todo-list.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/todo-list.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/underline.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/underline.svg new file mode 100644 index 0000000000..f5d53f0ec2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg new file mode 100644 index 0000000000..bd8f3067d3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/up.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx new file mode 100644 index 0000000000..1248882238 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx @@ -0,0 +1,33 @@ +import { stringToColor, stringToShortName } from '$app/utils/avatar'; +import { Avatar } from '@mui/material'; +import { useAppSelector } from '$app/stores/store'; + +export const ProfileAvatar = ({ + onClick, + className, + width, + height, +}: { + onClick?: (e: React.MouseEvent) => void; + width?: number; + height?: number; + className?: string; +}) => { + const { displayName = 'Me', iconUrl } = useAppSelector((state) => state.currentUser); + + return ( + + {iconUrl ? iconUrl : stringToShortName(displayName)} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx new file mode 100644 index 0000000000..079342b528 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx @@ -0,0 +1,34 @@ +import { Avatar } from '@mui/material'; +import { stringToColor, stringToShortName } from '$app/utils/avatar'; + +export const WorkplaceAvatar = ({ + workplaceName, + icon, + onClick, + width, + height, + className, +}: { + workplaceName: string; + width: number; + height: number; + className?: string; + icon?: string; + onClick?: (e: React.MouseEvent) => void; +}) => { + return ( + + {icon ? icon : stringToShortName(workplaceName)} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts new file mode 100644 index 0000000000..772056737a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts @@ -0,0 +1,2 @@ +export * from './WorkplaceAvatar'; +export * from './ProfileAvatar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx new file mode 100644 index 0000000000..058335d30c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx @@ -0,0 +1,77 @@ +import React, { useCallback } from 'react'; +import DialogContent from '@mui/material/DialogContent'; +import { Button, DialogProps } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import { useTranslation } from 'react-i18next'; +import { Log } from '$app/utils/log'; + +interface Props extends DialogProps { + open: boolean; + title: string; + subtitle?: string; + onOk?: () => Promise; + onClose: () => void; + onCancel?: () => void; + okText?: string; + cancelText?: string; + container?: HTMLElement | null; +} + +function DeleteConfirmDialog({ open, title, onOk, onCancel, onClose, okText, cancelText, container, ...props }: Props) { + const { t } = useTranslation(); + + const onDone = useCallback(async () => { + try { + await onOk?.(); + onClose(); + } catch (e) { + Log.error(e); + } + }, [onClose, onOk]); + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + void onDone(); + } + }} + onMouseDown={(e) => e.stopPropagation()} + open={open} + onClose={onClose} + {...props} + > + + {title} +
+ + +
+
+
+ ); +} + +export default DeleteConfirmDialog; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx new file mode 100644 index 0000000000..cb8b7a80ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx @@ -0,0 +1,81 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Dialog from '@mui/material/Dialog'; +import { useTranslation } from 'react-i18next'; +import TextField from '@mui/material/TextField'; +import { Button, DialogActions, Divider } from '@mui/material'; + +function RenameDialog({ + defaultValue, + open, + onClose, + onOk, +}: { + defaultValue: string; + open: boolean; + onClose: () => void; + onOk: (val: string) => Promise; +}) { + const { t } = useTranslation(); + const [value, setValue] = useState(defaultValue); + const [error, setError] = useState(false); + + useEffect(() => { + setValue(defaultValue); + setError(false); + }, [defaultValue]); + + const onDone = useCallback(async () => { + try { + await onOk(value); + onClose(); + } catch (e) { + setError(true); + } + }, [onClose, onOk, value]); + + return ( + e.stopPropagation()} open={open} onClose={onClose}> + {t('menuAppHeader.renameDialog')} + + { + e.stopPropagation(); + if (e.key === 'Enter') { + e.preventDefault(); + void onDone(); + } + + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }} + onChange={(e) => { + setValue(e.target.value); + }} + margin='dense' + fullWidth + variant='standard' + /> + + + + + + + + ); +} + +export default RenameDialog; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx new file mode 100644 index 0000000000..5d3ed1e3de --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import SpeedDial from '@mui/material/SpeedDial'; +import SpeedDialIcon from '@mui/material/SpeedDialIcon'; +import SpeedDialAction from '@mui/material/SpeedDialAction'; +import { useMemo } from 'react'; +import { CloseOutlined, BuildOutlined, LoginOutlined, VisibilityOff } from '@mui/icons-material'; +import ManualSignInDialog from '$app/components/_shared/devtool/ManualSignInDialog'; +import { Portal } from '@mui/material'; + +function AppFlowyDevTool() { + const [openManualSignIn, setOpenManualSignIn] = React.useState(false); + const [hidden, setHidden] = React.useState(false); + const actions = useMemo( + () => [ + { + icon: , + name: 'Manual SignIn', + onClick: () => { + setOpenManualSignIn(true); + }, + }, + { + icon: , + name: 'Hide Dev Tool', + onClick: () => { + setHidden(true); + }, + }, + ], + [] + ); + + return ( + + + + ); +} + +export default AppFlowyDevTool; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx new file mode 100644 index 0000000000..364b334a07 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { CircularProgress, DialogActions, DialogProps, Tab, Tabs, TextareaAutosize } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import Button from '@mui/material/Button'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import TextField from '@mui/material/TextField'; + +function ManualSignInDialog(props: DialogProps) { + const [uri, setUri] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const { signInWithOAuth, signInWithEmailPassword } = useAuth(); + const [tab, setTab] = React.useState(0); + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [domain, setDomain] = React.useState(''); + const handleSignIn = async () => { + setLoading(true); + try { + if (tab === 1) { + if (!email || !password) return; + await signInWithEmailPassword(email, password, domain); + } else { + await signInWithOAuth(uri); + } + } finally { + setLoading(false); + } + + props?.onClose?.({}, 'backdropClick'); + }; + + return ( + { + if (e.key === 'Enter') { + e.preventDefault(); + void handleSignIn(); + } + }} + > + + { + setTab(value); + }} + > + + + + {tab === 1 ? ( +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + setDomain(e.target.value)} + /> +
+ ) : ( + { + setUri(e.target.value); + }} + /> + )} +
+ + + + +
+ ); +} + +export default ManualSignInDialog; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts new file mode 100644 index 0000000000..85f0507fff --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts @@ -0,0 +1,87 @@ +import React from 'react'; + +interface Props { + dragId?: string; + onEnd?: (result: { dragId: string; position: 'before' | 'after' | 'inside' }) => void; +} + +function calcPosition(targetRect: DOMRect, clientY: number) { + const top = targetRect.top + targetRect.height / 3; + const bottom = targetRect.bottom - targetRect.height / 3; + + if (clientY < top) return 'before'; + if (clientY > bottom) return 'after'; + return 'inside'; +} + +export function useDrag(props: Props) { + const { dragId, onEnd } = props; + const [isDraggingOver, setIsDraggingOver] = React.useState(false); + const [isDragging, setIsDragging] = React.useState(false); + const [dropPosition, setDropPosition] = React.useState<'before' | 'after' | 'inside'>(); + const onDrop = (e: React.DragEvent) => { + e.stopPropagation(); + setIsDraggingOver(false); + setIsDragging(false); + setDropPosition(undefined); + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; + const dragId = e.dataTransfer.getData('dragId'); + const targetRect = currentTarget.getBoundingClientRect(); + const { clientY } = e; + + const position = calcPosition(targetRect, clientY); + + onEnd && onEnd({ dragId, position }); + }; + + const onDragOver = (e: React.DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (isDragging) return; + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; + setIsDraggingOver(true); + const targetRect = currentTarget.getBoundingClientRect(); + const { clientY } = e; + const position = calcPosition(targetRect, clientY); + + setDropPosition(position); + }; + + const onDragLeave = (e: React.DragEvent) => { + e.stopPropagation(); + setIsDraggingOver(false); + setDropPosition(undefined); + }; + + const onDragStart = (e: React.DragEvent) => { + if (!dragId) return; + e.stopPropagation(); + e.dataTransfer.setData('dragId', dragId); + e.dataTransfer.effectAllowed = 'move'; + setIsDragging(true); + }; + + const onDragEnd = (e: React.DragEvent) => { + e.stopPropagation(); + setIsDragging(false); + setIsDraggingOver(false); + setDropPosition(undefined); + }; + + return { + onDrop, + onDragOver, + onDragLeave, + onDragStart, + isDraggingOver, + isDragging, + onDragEnd, + dropPosition, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/index.ts new file mode 100644 index 0000000000..e0cb540f75 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/index.ts @@ -0,0 +1 @@ +export * from './drag.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.hooks.ts new file mode 100644 index 0000000000..af82c82df5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.hooks.ts @@ -0,0 +1,165 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import emojiData, { EmojiMartData } from '@emoji-mart/data'; +import { init, FrequentlyUsed, getEmojiDataFromNative, Store } from 'emoji-mart'; + +import { PopoverProps } from '@mui/material/Popover'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import chunk from 'lodash-es/chunk'; + +export const EMOJI_SIZE = 32; + +export const PER_ROW_EMOJI_COUNT = 13; + +export const MAX_FREQUENTLY_ROW_COUNT = 2; + +export interface EmojiCategory { + id: string; + emojis: Emoji[]; +} + +interface Emoji { + id: string; + name: string; + native: string; +} + +export function useLoadEmojiData({ onEmojiSelect }: { onEmojiSelect: (emoji: string) => void }) { + const [searchValue, setSearchValue] = useState(''); + const [emojiCategories, setEmojiCategories] = useState([]); + const [skin, setSkin] = useState(() => { + return Number(Store.get('skin')) || 0; + }); + + const onSkinChange = useCallback((val: number) => { + setSkin(val); + Store.set('skin', String(val)); + }, []); + + const loadEmojiData = useCallback( + async (searchVal?: string) => { + const { emojis, categories } = emojiData as EmojiMartData; + + const filteredCategories = categories + .map((category) => { + const { id, emojis: categoryEmojis } = category; + + return { + id, + emojis: categoryEmojis + .filter((emojiId) => { + const emoji = emojis[emojiId]; + + if (!searchVal) return true; + return filterSearchValue(emoji, searchVal); + }) + .map((emojiId) => { + const emoji = emojis[emojiId]; + const { name, skins } = emoji; + + return { + id: emojiId, + name, + native: skins[skin] ? skins[skin].native : skins[0].native, + }; + }), + }; + }) + .filter((category) => category.emojis.length > 0); + + setEmojiCategories(filteredCategories); + }, + [skin] + ); + + useEffect(() => { + void (async () => { + await init({ data: emojiData, maxFrequentRows: MAX_FREQUENTLY_ROW_COUNT, perLine: PER_ROW_EMOJI_COUNT }); + await loadEmojiData(); + })(); + }, [loadEmojiData]); + + useEffect(() => { + void loadEmojiData(searchValue); + }, [loadEmojiData, searchValue]); + + const onSelect = useCallback( + async (native: string) => { + onEmojiSelect(native); + if (!native) { + return; + } + + const data = await getEmojiDataFromNative(native); + + FrequentlyUsed.add(data); + }, + [onEmojiSelect] + ); + + return { + emojiCategories, + setSearchValue, + searchValue, + onSelect, + onSkinChange, + skin, + }; +} + +export function useSelectSkinPopoverProps(): PopoverProps & { + onOpen: (event: React.MouseEvent) => void; + onClose: () => void; +} { + const [anchorEl, setAnchorEl] = useState(undefined); + const onOpen = useCallback((event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }, []); + const onClose = useCallback(() => { + setAnchorEl(undefined); + }, []); + const open = Boolean(anchorEl); + const anchorOrigin = { vertical: 'bottom', horizontal: 'center' } as PopoverOrigin; + const transformOrigin = { vertical: 'top', horizontal: 'center' } as PopoverOrigin; + + return { + anchorEl, + onOpen, + onClose, + open, + anchorOrigin, + transformOrigin, + }; +} + +function filterSearchValue(emoji: emojiData.Emoji, searchValue: string) { + const { name, keywords } = emoji; + const searchValueLowerCase = searchValue.toLowerCase(); + + return ( + name.toLowerCase().includes(searchValueLowerCase) || + (keywords && keywords.some((keyword) => keyword.toLowerCase().includes(searchValueLowerCase))) + ); +} + +export function getRowsWithCategories(emojiCategories: EmojiCategory[], rowSize: number) { + const rows: { + id: string; + type: 'category' | 'emojis'; + emojis?: Emoji[]; + }[] = []; + + emojiCategories.forEach((category) => { + rows.push({ + id: category.id, + type: 'category', + }); + chunk(category.emojis, rowSize).forEach((chunk, index) => { + rows.push({ + type: 'emojis', + emojis: chunk, + id: `${category.id}-${index}`, + }); + }); + }); + return rows; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.tsx new file mode 100644 index 0000000000..b8dcb3f6c7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPicker.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +import { useLoadEmojiData } from './EmojiPicker.hooks'; +import EmojiPickerHeader from './EmojiPickerHeader'; +import EmojiPickerCategories from './EmojiPickerCategories'; + +interface Props { + onEmojiSelect: (emoji: string) => void; + onEscape?: () => void; + defaultEmoji?: string; +} + +function EmojiPicker({ defaultEmoji, onEscape, ...props }: Props) { + const { skin, onSkinChange, emojiCategories, setSearchValue, searchValue, onSelect } = useLoadEmojiData(props); + + return ( +
+ + +
+ ); +} + +export default EmojiPicker; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerCategories.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerCategories.tsx new file mode 100644 index 0000000000..eefea8db11 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerCategories.tsx @@ -0,0 +1,354 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { + EMOJI_SIZE, + EmojiCategory, + getRowsWithCategories, + PER_ROW_EMOJI_COUNT, +} from '$app/components/_shared/emoji_picker/EmojiPicker.hooks'; +import { FixedSizeList } from 'react-window'; +import { useTranslation } from 'react-i18next'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { getDistanceEdge, inView } from '$app/components/_shared/keyboard_navigation/utils'; + +function EmojiPickerCategories({ + emojiCategories, + onEmojiSelect, + onEscape, + defaultEmoji, +}: { + emojiCategories: EmojiCategory[]; + onEmojiSelect: (emoji: string) => void; + onEscape?: () => void; + defaultEmoji?: string; +}) { + const scrollRef = React.useRef(null); + const { t } = useTranslation(); + const [selectCell, setSelectCell] = React.useState({ + row: 1, + column: 0, + }); + const rows = useMemo(() => { + return getRowsWithCategories(emojiCategories, PER_ROW_EMOJI_COUNT); + }, [emojiCategories]); + const mouseY = useRef(null); + const mouseX = useRef(null); + + const ref = React.useRef(null); + + const getCategoryName = useCallback( + (id: string) => { + const i18nName: Record = { + frequent: t('emoji.categories.frequentlyUsed'), + people: t('emoji.categories.people'), + nature: t('emoji.categories.nature'), + foods: t('emoji.categories.food'), + activity: t('emoji.categories.activities'), + places: t('emoji.categories.places'), + objects: t('emoji.categories.objects'), + symbols: t('emoji.categories.symbols'), + flags: t('emoji.categories.flags'), + }; + + return i18nName[id]; + }, + [t] + ); + + useEffect(() => { + scrollRef.current?.scrollTo({ + top: 0, + }); + + setSelectCell({ + row: 1, + column: 0, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rows]); + + const renderRow = useCallback( + ({ index, style }: { index: number; style: React.CSSProperties }) => { + const item = rows[index]; + + return ( +
+ {item.type === 'category' ? ( +
{getCategoryName(item.id)}
+ ) : null} +
+ {item.emojis?.map((emoji, columnIndex) => { + const isSelected = selectCell.row === index && selectCell.column === columnIndex; + + const isDefaultEmoji = defaultEmoji === emoji.native; + + return ( +
{ + onEmojiSelect(emoji.native); + }} + onMouseMove={(e) => { + mouseY.current = e.clientY; + mouseX.current = e.clientX; + }} + onMouseEnter={(e) => { + if (mouseY.current === null || mouseY.current !== e.clientY || mouseX.current !== e.clientX) { + setSelectCell({ + row: index, + column: columnIndex, + }); + } + + mouseX.current = e.clientX; + mouseY.current = e.clientY; + }} + className={`flex cursor-pointer items-center justify-center rounded hover:bg-fill-list-hover ${ + isSelected ? 'bg-fill-list-hover' : 'hover:bg-transparent' + } ${isDefaultEmoji ? 'bg-fill-list-active' : ''}`} + > + {emoji.native} +
+ ); + })} +
+
+ ); + }, + [defaultEmoji, getCategoryName, onEmojiSelect, rows, selectCell.column, selectCell.row] + ); + + const getNewColumnIndex = useCallback( + (rowIndex: number, columnIndex: number): number => { + const row = rows[rowIndex]; + const length = row.emojis?.length; + let newColumnIndex = columnIndex; + + if (length && length <= columnIndex) { + newColumnIndex = length - 1 || 0; + } + + return newColumnIndex; + }, + [rows] + ); + + const findNextRow = useCallback( + (rowIndex: number, columnIndex: number): { row: number; column: number } => { + const rowLength = rows.length; + let nextRowIndex = rowIndex + 1; + + if (nextRowIndex >= rowLength - 1) { + nextRowIndex = rowLength - 1; + } else if (rows[nextRowIndex].type === 'category') { + nextRowIndex = findNextRow(nextRowIndex, columnIndex).row; + } + + const newColumnIndex = getNewColumnIndex(nextRowIndex, columnIndex); + + return { + row: nextRowIndex, + column: newColumnIndex, + }; + }, + [getNewColumnIndex, rows] + ); + + const findPrevRow = useCallback( + (rowIndex: number, columnIndex: number): { row: number; column: number } => { + let prevRowIndex = rowIndex - 1; + + if (prevRowIndex < 1) { + prevRowIndex = 1; + } else if (rows[prevRowIndex].type === 'category') { + prevRowIndex = findPrevRow(prevRowIndex, columnIndex).row; + } + + const newColumnIndex = getNewColumnIndex(prevRowIndex, columnIndex); + + return { + row: prevRowIndex, + column: newColumnIndex, + }; + }, + [getNewColumnIndex, rows] + ); + + const findPrevCell = useCallback( + (row: number, column: number): { row: number; column: number } => { + const prevColumn = column - 1; + + if (prevColumn < 0) { + const prevRow = findPrevRow(row, column).row; + + if (prevRow === row) return { row, column }; + const length = rows[prevRow].emojis?.length || 0; + + return { + row: prevRow, + column: length > 0 ? length - 1 : 0, + }; + } + + return { + row, + column: prevColumn, + }; + }, + [findPrevRow, rows] + ); + + const findNextCell = useCallback( + (row: number, column: number): { row: number; column: number } => { + const nextColumn = column + 1; + + const rowLength = rows[row].emojis?.length || 0; + + if (nextColumn >= rowLength) { + const nextRow = findNextRow(row, column).row; + + if (nextRow === row) return { row, column }; + return { + row: nextRow, + column: 0, + }; + } + + return { + row, + column: nextColumn, + }; + }, + [findNextRow, rows] + ); + + useEffect(() => { + if (!selectCell || !scrollRef.current) return; + const emojiKey = rows[selectCell.row]?.emojis?.[selectCell.column]?.id; + const emojiDom = document.querySelector(`[data-key="${emojiKey}"]`); + + if (emojiDom && !inView(emojiDom as HTMLElement, scrollRef.current as HTMLElement)) { + const distance = getDistanceEdge(emojiDom as HTMLElement, scrollRef.current as HTMLElement); + + scrollRef.current?.scrollTo({ + top: scrollRef.current?.scrollTop + distance, + }); + } + }, [selectCell, rows]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + e.stopPropagation(); + + switch (e.key) { + case 'Escape': + e.preventDefault(); + onEscape?.(); + break; + case 'ArrowUp': { + e.preventDefault(); + + setSelectCell(findPrevRow(selectCell.row, selectCell.column)); + + break; + } + + case 'ArrowDown': { + e.preventDefault(); + + setSelectCell(findNextRow(selectCell.row, selectCell.column)); + + break; + } + + case 'ArrowLeft': { + e.preventDefault(); + + const prevCell = findPrevCell(selectCell.row, selectCell.column); + + setSelectCell(prevCell); + break; + } + + case 'ArrowRight': { + e.preventDefault(); + + const nextCell = findNextCell(selectCell.row, selectCell.column); + + setSelectCell(nextCell); + break; + } + + case 'Enter': { + e.preventDefault(); + const currentRow = rows[selectCell.row]; + const emoji = currentRow.emojis?.[selectCell.column]; + + if (emoji) { + onEmojiSelect(emoji.native); + } + + break; + } + + default: + break; + } + }, + [ + findNextCell, + findPrevCell, + findPrevRow, + findNextRow, + onEmojiSelect, + onEscape, + rows, + selectCell.column, + selectCell.row, + ] + ); + + useEffect(() => { + const focusElement = document.querySelector('.emoji-picker .search-emoji-input') as HTMLInputElement; + + const parentElement = ref.current?.parentElement; + + focusElement?.addEventListener('keydown', handleKeyDown); + parentElement?.addEventListener('keydown', handleKeyDown); + return () => { + focusElement?.removeEventListener('keydown', handleKeyDown); + parentElement?.removeEventListener('keydown', handleKeyDown); + }; + }, [handleKeyDown]); + + return ( +
+ + {({ height, width }: { height: number; width: number }) => ( + + {renderRow} + + )} + +
+ ); +} + +export default EmojiPickerCategories; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerHeader.tsx new file mode 100644 index 0000000000..177ac2e7a0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/emoji_picker/EmojiPickerHeader.tsx @@ -0,0 +1,134 @@ +import React from 'react'; +import { Box, IconButton } from '@mui/material'; +import { Circle, DeleteOutlineRounded, SearchOutlined } from '@mui/icons-material'; +import TextField from '@mui/material/TextField'; +import Tooltip from '@mui/material/Tooltip'; +import { randomEmoji } from '$app/utils/emoji'; +import ShuffleIcon from '@mui/icons-material/Shuffle'; +import Popover from '@mui/material/Popover'; +import { useSelectSkinPopoverProps } from '$app/components/_shared/emoji_picker/EmojiPicker.hooks'; +import { useTranslation } from 'react-i18next'; + +const skinTones = [ + { + value: 0, + color: '#ffc93a', + }, + { + color: '#ffdab7', + value: 1, + }, + { + color: '#e7b98f', + value: 2, + }, + { + color: '#c88c61', + value: 3, + }, + { + color: '#a46134', + value: 4, + }, + { + color: '#5d4437', + value: 5, + }, +]; + +interface Props { + onEmojiSelect: (emoji: string) => void; + skin: number; + onSkinSelect: (skin: number) => void; + searchValue: string; + onSearchChange: (value: string) => void; +} + +function EmojiPickerHeader({ onEmojiSelect, onSkinSelect, searchValue, onSearchChange, skin }: Props) { + const { onOpen, ...popoverProps } = useSelectSkinPopoverProps(); + const { t } = useTranslation(); + + return ( +
+
+ + + { + onSearchChange(e.target.value); + }} + autoFocus={true} + autoCorrect={'off'} + autoComplete={'off'} + spellCheck={false} + className={'search-emoji-input'} + placeholder={t('search.label')} + variant='standard' + /> + + +
+ { + const emoji = randomEmoji(); + + onEmojiSelect(emoji); + }} + > + + +
+
+ +
+ + + +
+
+ +
+ { + onEmojiSelect(''); + }} + > + + +
+
+
+ +
+ {skinTones.map((skinTone) => ( +
+ { + onSkinSelect(skinTone.value); + popoverProps.onClose?.(); + }} + > + + +
+ ))} +
+
+
+ ); +} + +export default EmojiPickerHeader; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/error_boundary/withError.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/error_boundary/withError.tsx new file mode 100644 index 0000000000..9b9ba159fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/error_boundary/withError.tsx @@ -0,0 +1,16 @@ +import { ErrorBoundary } from 'react-error-boundary'; +import { Log } from '$app/utils/log'; +import { Alert } from '@mui/material'; + +export default function withErrorBoundary(WrappedComponent: React.ComponentType) { + return (props: T) => ( + { + Log.error(e); + }} + fallback={Something went wrong} + > + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx new file mode 100644 index 0000000000..34a99007ad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/EmbedLink.tsx @@ -0,0 +1,70 @@ +import React, { useCallback, useState } from 'react'; +import TextField from '@mui/material/TextField'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; + +const urlPattern = /^(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm|.webp|.svg)(\?[^\s[",><]*)?$/; + +export function EmbedLink({ + onDone, + onEscape, + defaultLink, +}: { + defaultLink?: string; + onDone?: (value: string) => void; + onEscape?: () => void; +}) { + const { t } = useTranslation(); + + const [value, setValue] = useState(defaultLink ?? ''); + const [error, setError] = useState(false); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + + setValue(value); + setError(!urlPattern.test(value)); + }, + [setValue, setError] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !error && value) { + e.preventDefault(); + e.stopPropagation(); + onDone?.(value); + } + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onEscape?.(); + } + }, + [error, onDone, onEscape, value] + ); + + return ( +
+ + +
+ ); +} + +export default EmbedLink; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx new file mode 100644 index 0000000000..d94e5f2889 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/LocalImage.tsx @@ -0,0 +1,62 @@ +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; +import { CircularProgress } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ErrorOutline } from '@mui/icons-material'; + +export const LocalImage = forwardRef< + HTMLImageElement, + { + renderErrorNode?: () => React.ReactElement | null; + } & React.ImgHTMLAttributes +>((localImageProps, ref) => { + const { src, renderErrorNode, ...props } = localImageProps; + const imageRef = useRef(null); + const { t } = useTranslation(); + const [imageURL, setImageURL] = useState(''); + const [loading, setLoading] = useState(true); + const [isError, setIsError] = useState(false); + const loadLocalImage = useCallback(async () => { + if (!src) return; + setLoading(true); + setIsError(false); + const { readBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs'); + + try { + const svg = src.endsWith('.svg'); + + const buffer = await readBinaryFile(src, { dir: BaseDirectory.AppLocalData }); + const blob = new Blob([buffer], { type: svg ? 'image/svg+xml' : 'image' }); + + setImageURL(URL.createObjectURL(blob)); + } catch (e) { + setIsError(true); + } + + setLoading(false); + }, [src]); + + useEffect(() => { + void loadLocalImage(); + }, [loadLocalImage]); + + if (loading) { + return ( +
+ + {t('editor.loading')}... +
+ ); + } + + if (isError) { + if (renderErrorNode) return renderErrorNode(); + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + } + + return {'local; +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Unsplash.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Unsplash.tsx new file mode 100644 index 0000000000..01da8323b9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/Unsplash.tsx @@ -0,0 +1,154 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { createApi } from 'unsplash-js'; +import TextField from '@mui/material/TextField'; +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import debounce from 'lodash-es/debounce'; +import { CircularProgress } from '@mui/material'; +import { open } from '@tauri-apps/api/shell'; + +const unsplash = createApi({ + accessKey: '1WxD1JpMOUX86lZKKob4Ca0LMZPyO2rUmAgjpWm9Ids', +}); + +const SEARCH_DEBOUNCE_TIME = 500; + +export function Unsplash({ onDone, onEscape }: { onDone?: (value: string) => void; onEscape?: () => void }) { + const { t } = useTranslation(); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [photos, setPhotos] = useState< + { + thumb: string; + regular: string; + alt: string | null; + id: string; + user: { + name: string; + link: string; + }; + }[] + >([]); + const [searchValue, setSearchValue] = useState(''); + + const handleChange = useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + + setSearchValue(value); + }, []); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onEscape?.(); + } + }, + [onEscape] + ); + + const debounceSearchPhotos = useMemo(() => { + return debounce(async (searchValue: string) => { + const request = searchValue + ? unsplash.search.getPhotos({ query: searchValue ?? undefined, perPage: 32 }) + : unsplash.photos.list({ perPage: 32 }); + + setError(''); + setLoading(true); + await request.then((result) => { + if (result.errors) { + setError(result.errors[0]); + } else { + setPhotos( + result.response.results.map((photo) => ({ + id: photo.id, + thumb: photo.urls.thumb, + regular: photo.urls.regular, + alt: photo.alt_description, + user: { + name: photo.user.name, + link: photo.user.links.html, + }, + })) + ); + } + + setLoading(false); + }); + }, SEARCH_DEBOUNCE_TIME); + }, []); + + useEffect(() => { + void debounceSearchPhotos(searchValue); + return () => { + debounceSearchPhotos.cancel(); + }; + }, [debounceSearchPhotos, searchValue]); + + return ( +
+ + + {loading ? ( +
+ +
{t('editor.loading')}
+
+ ) : error ? ( + + {error} + + ) : ( +
+ {photos.length > 0 ? ( + <> +
+ {photos.map((photo) => ( +
+ { + onDone?.(photo.regular); + }} + src={photo.thumb} + alt={photo.alt ?? ''} + className={'h-20 w-32 rounded object-cover hover:opacity-80'} + /> +
+ by{' '} + { + void open(photo.user.link); + }} + className={'underline hover:text-function-info'} + > + {photo.user.name} + +
+
+ ))} +
+ + {t('findAndReplace.searchMore')} + + + ) : ( + + {t('findAndReplace.noResult')} + + )} +
+ )} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx new file mode 100644 index 0000000000..a6b66a4c1f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadImage.tsx @@ -0,0 +1,95 @@ +import React, { useCallback } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import CloudUploadIcon from '@mui/icons-material/CloudUploadOutlined'; +import { notify } from '$app/components/_shared/notify'; +import { isTauri } from '$app/utils/env'; +import { getFileName, IMAGE_DIR, ALLOWED_IMAGE_EXTENSIONS, MAX_IMAGE_SIZE } from '$app/utils/upload_image'; + +export function UploadImage({ onDone }: { onDone?: (url: string) => void }) { + const { t } = useTranslation(); + + const checkTauriFile = useCallback( + async (url: string) => { + const { readBinaryFile } = await import('@tauri-apps/api/fs'); + + const buffer = await readBinaryFile(url); + const blob = new Blob([buffer]); + + if (blob.size > MAX_IMAGE_SIZE) { + notify.error(t('document.imageBlock.error.invalidImageSize')); + return false; + } + + return true; + }, + [t] + ); + + const uploadTauriLocalImage = useCallback( + async (url: string) => { + const { copyFile, BaseDirectory, exists, createDir } = await import('@tauri-apps/api/fs'); + + const checked = await checkTauriFile(url); + + if (!checked) return; + + try { + const existDir = await exists(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + + if (!existDir) { + await createDir(IMAGE_DIR, { dir: BaseDirectory.AppLocalData }); + } + + const filename = getFileName(url); + + await copyFile(url, `${IMAGE_DIR}/${filename}`, { dir: BaseDirectory.AppLocalData }); + const newUrl = `${IMAGE_DIR}/${filename}`; + + onDone?.(newUrl); + } catch (e) { + notify.error(t('document.plugins.image.imageUploadFailed')); + } + }, + [checkTauriFile, onDone, t] + ); + + const handleClickUpload = useCallback(async () => { + if (!isTauri()) return; + const { open } = await import('@tauri-apps/api/dialog'); + + const url = await open({ + multiple: false, + directory: false, + filters: [ + { + name: 'Image', + extensions: ALLOWED_IMAGE_EXTENSIONS, + }, + ], + }); + + if (!url || typeof url !== 'string') return; + + await uploadTauriLocalImage(url); + }, [uploadTauriLocalImage]); + + return ( +
+ +
+ ); +} + +export default UploadImage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx new file mode 100644 index 0000000000..fb65c709ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/UploadTabs.tsx @@ -0,0 +1,128 @@ +import React, { SyntheticEvent, useCallback, useState } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { TabPanel, ViewTab, ViewTabs } from '$app/components/database/components/tab_bar/ViewTabs'; +import SwipeableViews from 'react-swipeable-views'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; + +export enum TAB_KEY { + Colors = 'colors', + UPLOAD = 'upload', + EMBED_LINK = 'embed_link', + UNSPLASH = 'unsplash', +} + +export type TabOption = { + key: TAB_KEY; + label: string; + Component: React.ComponentType<{ + onDone?: (value: string) => void; + onEscape?: () => void; + }>; + onDone?: (value: string) => void; +}; + +export function UploadTabs({ + tabOptions, + popoverProps, + containerStyle, + extra, +}: { + containerStyle?: React.CSSProperties; + tabOptions: TabOption[]; + popoverProps?: PopoverProps; + extra?: React.ReactNode; +}) { + const [tabValue, setTabValue] = useState(() => { + return tabOptions[0].key; + }); + + const handleTabChange = useCallback((_: SyntheticEvent, newValue: string) => { + setTabValue(newValue as TAB_KEY); + }, []); + + const selectedIndex = tabOptions.findIndex((tab) => tab.key === tabValue); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + popoverProps?.onClose?.({}, 'escapeKeyDown'); + } + + if (e.key === 'Tab') { + e.preventDefault(); + e.stopPropagation(); + setTabValue((prev) => { + const currentIndex = tabOptions.findIndex((tab) => tab.key === prev); + let nextIndex = currentIndex + 1; + + if (e.shiftKey) { + nextIndex = currentIndex - 1; + } + + return tabOptions[nextIndex % tabOptions.length]?.key ?? tabOptions[0].key; + }); + } + }, + [popoverProps, tabOptions] + ); + + return ( + +
+
+ + {tabOptions.map((tab) => { + const { key, label } = tab; + + return ; + })} + + {extra} +
+ +
+ + {tabOptions.map((tab, index) => { + const { key, Component, onDone } = tab; + + return ( + + popoverProps?.onClose?.({}, 'escapeKeyDown')} /> + + ); + })} + +
+
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts new file mode 100644 index 0000000000..28673cae5f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/image_upload/index.ts @@ -0,0 +1,5 @@ +export * from './Unsplash'; +export * from './UploadImage'; +export * from './EmbedLink'; +export * from './UploadTabs'; +export * from './LocalImage'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/KatexMath.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/KatexMath.tsx new file mode 100644 index 0000000000..e6c7cac5ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/KatexMath.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import 'katex/dist/katex.min.css'; +import { BlockMath, InlineMath } from 'react-katex'; +import './index.css'; + +function KatexMath({ latex, isInline = false }: { latex: string; isInline?: boolean }) { + return isInline ? ( + + ) : ( + { + return ( +
+ {error.name}: {error.message} +
+ ); + }} + > + {latex} +
+ ); +} + +export default KatexMath; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/index.css b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/index.css new file mode 100644 index 0000000000..d127dc343b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/katex_math/index.css @@ -0,0 +1,4 @@ + +.katex-html { + white-space: normal; +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx new file mode 100644 index 0000000000..7db90c4e8f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx @@ -0,0 +1,317 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { MenuItem, Typography } from '@mui/material'; +import { scrollIntoView } from '$app/components/_shared/keyboard_navigation/utils'; +import { useTranslation } from 'react-i18next'; + +/** + * The option of the keyboard navigation + * the options will be flattened + * - key: the key of the option + * - content: the content of the option + * - children: the children of the option + */ +export interface KeyboardNavigationOption { + key: T; + content?: React.ReactNode; + children?: KeyboardNavigationOption[]; + disabled?: boolean; +} + +/** + * - scrollRef: the scrollable element + * - focusRef: the element to focus when the keyboard navigation is disabled + * - options: the options to navigate + * - onSelected: called when an option is selected(hovered) + * - onConfirm: called when an option is confirmed + * - onEscape: called when the escape key is pressed + * - onPressRight: called when the right arrow is pressed + * - onPressLeft: called when the left arrow key is pressed + * - disableFocus: disable the focus on the keyboard navigation + * - disableSelect: disable selecting an option when the options are initialized + * - onKeyDown: called when a key is pressed + * - defaultFocusedKey: the default focused key + * - onFocus: called when the keyboard navigation is focused + * - onBlur: called when the keyboard navigation is blurred + */ +export interface KeyboardNavigationProps { + scrollRef?: React.RefObject; + focusRef?: React.RefObject; + options: KeyboardNavigationOption[]; + onSelected?: (optionKey: T) => void; + onConfirm?: (optionKey: T) => void; + onEscape?: () => void; + onPressRight?: (optionKey: T) => void; + onPressLeft?: (optionKey: T) => void; + disableFocus?: boolean; + disableSelect?: boolean; + onKeyDown?: (e: KeyboardEvent) => void; + defaultFocusedKey?: T; + onFocus?: () => void; + onBlur?: () => void; + itemClassName?: string; + itemStyle?: React.CSSProperties; + renderNoResult?: () => React.ReactNode; +} + +function KeyboardNavigation({ + defaultFocusedKey, + onPressRight, + onPressLeft, + onEscape, + onConfirm, + scrollRef, + options, + onSelected, + focusRef, + disableFocus = false, + onKeyDown: onPropsKeyDown, + disableSelect = false, + onBlur, + onFocus, + itemClassName, + itemStyle, + renderNoResult, +}: KeyboardNavigationProps) { + const { t } = useTranslation(); + const ref = useRef(null); + const mouseY = useRef(null); + const defaultKeyRef = useRef(defaultFocusedKey); + // flatten the options + const flattenOptions = useMemo(() => { + return options.flatMap((group) => { + if (group.children) { + return group.children; + } + + return [group]; + }); + }, [options]); + + const [focusedKey, setFocusedKey] = useState(); + + const firstOptionKey = useMemo(() => { + if (disableSelect) return; + const firstOption = flattenOptions.find((option) => !option.disabled); + + return firstOption?.key; + }, [flattenOptions, disableSelect]); + + // set the default focused key when the options are initialized + useEffect(() => { + if (defaultKeyRef.current) { + setFocusedKey(defaultKeyRef.current); + defaultKeyRef.current = undefined; + return; + } + + setFocusedKey(firstOptionKey); + }, [firstOptionKey]); + + // call the onSelected callback when the focused key is changed + useEffect(() => { + if (focusedKey === undefined) return; + onSelected?.(focusedKey); + + const scrollElement = scrollRef?.current; + + if (!scrollElement) return; + + const dom = ref.current?.querySelector(`[data-key="${focusedKey}"]`); + + if (!dom) return; + // scroll the focused option into view + requestAnimationFrame(() => { + scrollIntoView(dom as HTMLDivElement, scrollElement); + }); + }, [focusedKey, onSelected, scrollRef]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + onPropsKeyDown?.(e); + e.stopPropagation(); + const key = e.key; + + if (key === 'Tab') { + e.preventDefault(); + return; + } + + if (key === 'Escape') { + e.preventDefault(); + onEscape?.(); + return; + } + + if (focusedKey === undefined) return; + const focusedIndex = flattenOptions.findIndex((option) => option?.key === focusedKey); + const nextIndex = (focusedIndex + 1) % flattenOptions.length; + const prevIndex = (focusedIndex - 1 + flattenOptions.length) % flattenOptions.length; + + switch (key) { + // move the focus to the previous option + case 'ArrowUp': { + e.preventDefault(); + + const prevKey = flattenOptions[prevIndex]?.key; + + setFocusedKey(prevKey); + + break; + } + + // move the focus to the next option + case 'ArrowDown': { + e.preventDefault(); + const nextKey = flattenOptions[nextIndex]?.key; + + setFocusedKey(nextKey); + break; + } + + case 'ArrowRight': + if (onPressRight) { + e.preventDefault(); + onPressRight(focusedKey); + } + + break; + case 'ArrowLeft': + if (onPressLeft) { + e.preventDefault(); + onPressLeft(focusedKey); + } + + break; + // confirm the focused option + case 'Enter': { + e.preventDefault(); + const disabled = flattenOptions[focusedIndex]?.disabled; + + if (!disabled) { + onConfirm?.(focusedKey); + } + + break; + } + + default: + break; + } + }, + [flattenOptions, focusedKey, onConfirm, onEscape, onPressLeft, onPressRight, onPropsKeyDown] + ); + + const renderOption = useCallback( + (option: KeyboardNavigationOption, index: number) => { + const hasChildren = option.children; + + const isFocused = focusedKey === option.key; + + return ( +
+ {hasChildren ? ( + // render the group name + option.content &&
{option.content}
+ ) : ( + // render the option + { + mouseY.current = e.clientY; + }} + onMouseEnter={(e) => { + onFocus?.(); + if (mouseY.current === null || mouseY.current !== e.clientY) { + setFocusedKey(option.key); + } + + mouseY.current = e.clientY; + }} + onClick={() => { + setFocusedKey(option.key); + if (!option.disabled) { + onConfirm?.(option.key); + } + }} + selected={isFocused} + style={itemStyle} + className={`ml-0 flex w-full items-center justify-start rounded-none px-2 py-1 text-xs ${ + !isFocused ? 'hover:bg-transparent' : '' + } ${itemClassName ?? ''}`} + > + {option.content} + + )} + + {option.children?.map((child, childIndex) => { + return renderOption(child, index + childIndex); + })} +
+ ); + }, + [itemClassName, focusedKey, onConfirm, onFocus, itemStyle] + ); + + useEffect(() => { + const element = ref.current; + + if (!disableFocus && element) { + element.focus(); + element.addEventListener('keydown', onKeyDown); + + return () => { + element.removeEventListener('keydown', onKeyDown); + }; + } else { + let element: HTMLElement | null | undefined = focusRef?.current; + + if (!element) { + element = document.activeElement as HTMLElement; + } + + element?.addEventListener('keydown', onKeyDown); + return () => { + element?.removeEventListener('keydown', onKeyDown); + }; + } + }, [disableFocus, onKeyDown, focusRef]); + + return ( +
{ + e.stopPropagation(); + + onFocus?.(); + }} + onBlur={(e) => { + e.stopPropagation(); + + const target = e.relatedTarget as HTMLElement; + + if (target?.closest('.keyboard-navigation')) { + return; + } + + onBlur?.(); + }} + autoFocus={!disableFocus} + className={'keyboard-navigation flex w-full flex-col gap-1 outline-none'} + ref={ref} + > + {options.length > 0 ? ( + options.map(renderOption) + ) : renderNoResult ? ( + renderNoResult() + ) : ( + + {t('findAndReplace.noResult')} + + )} +
+ ); +} + +export default KeyboardNavigation; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/utils.ts new file mode 100644 index 0000000000..621143869b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/utils.ts @@ -0,0 +1,32 @@ +export function inView(dom: HTMLElement, container: HTMLElement) { + const domRect = dom.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + if (!domRect || !containerRect) return true; + + return domRect?.bottom <= containerRect?.bottom && domRect?.top >= containerRect?.top; +} + +export function getDistanceEdge(dom: HTMLElement, container: HTMLElement) { + const domRect = dom.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + + if (!domRect || !containerRect) return 0; + + const distanceTop = domRect?.top - containerRect?.top; + const distanceBottom = domRect?.bottom - containerRect?.bottom; + + return Math.abs(distanceTop) < Math.abs(distanceBottom) ? distanceTop : distanceBottom; +} + +export function scrollIntoView(dom: HTMLElement, container: HTMLElement) { + const isDomInView = inView(dom, container); + + if (isDomInView) return; + + dom.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + behavior: 'smooth', + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/notify/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/notify/index.ts new file mode 100644 index 0000000000..1086cabdfd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/notify/index.ts @@ -0,0 +1,27 @@ +import toast from 'react-hot-toast'; + +const commonOptions = { + style: { + background: 'var(--bg-base)', + color: 'var(--text-title)', + shadows: 'var(--shadow)', + }, +}; + +export const notify = { + success: (message: string) => { + toast.success(message, commonOptions); + }, + error: (message: string) => { + toast.error(message, commonOptions); + }, + loading: (message: string) => { + toast.loading(message, commonOptions); + }, + info: (message: string) => { + toast(message, commonOptions); + }, + clear: () => { + toast.dismiss(); + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts new file mode 100644 index 0000000000..0fc1b5e61e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts @@ -0,0 +1,237 @@ +import { useState, useEffect, useCallback } from 'react'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +interface PopoverPosition { + anchorOrigin: PopoverOrigin; + transformOrigin: PopoverOrigin; + paperWidth: number; + paperHeight: number; + isEntered: boolean; + anchorPosition?: { left: number; top: number }; +} + +interface UsePopoverAutoPositionProps { + anchorEl?: HTMLElement | null; + anchorPosition?: { left: number; top: number; height: number }; + initialAnchorOrigin?: PopoverOrigin; + initialTransformOrigin?: PopoverOrigin; + initialPaperWidth: number; + initialPaperHeight: number; + marginThreshold?: number; + open: boolean; + anchorSize?: { width: number; height: number }; +} + +const minPaperWidth = 80; +const minPaperHeight = 120; + +function getOffsetLeft( + rect: { + height: number; + width: number; + }, + paperWidth: number, + horizontal: number | 'center' | 'left' | 'right', + transformHorizontal: number | 'center' | 'left' | 'right' +) { + let offset = 0; + + if (typeof horizontal === 'number') { + offset = horizontal; + } else if (horizontal === 'center') { + offset = rect.width / 2; + } else if (horizontal === 'right') { + offset = rect.width; + } + + if (transformHorizontal === 'center') { + offset -= paperWidth / 2; + } else if (transformHorizontal === 'right') { + offset -= paperWidth; + } + + return offset; +} + +function getOffsetTop( + rect: { + height: number; + width: number; + }, + papertHeight: number, + vertical: number | 'center' | 'bottom' | 'top', + transformVertical: number | 'center' | 'bottom' | 'top' +) { + let offset = 0; + + if (typeof vertical === 'number') { + offset = vertical; + } else if (vertical === 'center') { + offset = rect.height / 2; + } else if (vertical === 'bottom') { + offset = rect.height; + } + + if (transformVertical === 'center') { + offset -= papertHeight / 2; + } else if (transformVertical === 'bottom') { + offset -= papertHeight; + } + + return offset; +} + +const defaultAnchorOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +const defaultTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +const usePopoverAutoPosition = ({ + anchorEl, + anchorPosition, + initialAnchorOrigin = defaultAnchorOrigin, + initialTransformOrigin = defaultTransformOrigin, + initialPaperWidth, + initialPaperHeight, + marginThreshold = 16, + open, +}: UsePopoverAutoPositionProps): PopoverPosition & { + calculateAnchorSize: () => void; +} => { + const [position, setPosition] = useState({ + anchorOrigin: initialAnchorOrigin, + transformOrigin: initialTransformOrigin, + paperWidth: initialPaperWidth, + paperHeight: initialPaperHeight, + anchorPosition, + isEntered: false, + }); + + const calculateAnchorSize = useCallback(() => { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + const getAnchorOffset = () => { + if (anchorPosition) { + return { + ...anchorPosition, + width: 0, + }; + } + + return anchorEl ? anchorEl.getBoundingClientRect() : undefined; + }; + + const anchorRect = getAnchorOffset(); + + if (!anchorRect) return; + let newPaperWidth = initialPaperWidth; + let newPaperHeight = initialPaperHeight; + const newAnchorPosition = { + top: anchorRect.top, + left: anchorRect.left, + }; + + // calculate new paper width + const newLeft = + anchorRect.left + + getOffsetLeft(anchorRect, newPaperWidth, initialAnchorOrigin.horizontal, initialTransformOrigin.horizontal); + const newTop = + anchorRect.top + + getOffsetTop(anchorRect, newPaperHeight, initialAnchorOrigin.vertical, initialTransformOrigin.vertical); + + let isExceedViewportRight = false; + let isExceedViewportBottom = false; + let isExceedViewportLeft = false; + let isExceedViewportTop = false; + + // Check if exceed viewport right + if (newLeft + newPaperWidth > viewportWidth - marginThreshold) { + isExceedViewportRight = true; + // Check if exceed viewport left + if (newLeft - newPaperWidth < marginThreshold) { + isExceedViewportLeft = true; + newPaperWidth = Math.max(minPaperWidth, Math.min(newPaperWidth, viewportWidth - newLeft - marginThreshold)); + } + } + + // Check if exceed viewport bottom + if (newTop + newPaperHeight > viewportHeight - marginThreshold) { + isExceedViewportBottom = true; + // Check if exceed viewport top + if (newTop - newPaperHeight < marginThreshold) { + isExceedViewportTop = true; + newPaperHeight = Math.max(minPaperHeight, Math.min(newPaperHeight, viewportHeight - newTop - marginThreshold)); + } + } + + const newPosition = { + anchorOrigin: { ...initialAnchorOrigin }, + transformOrigin: { ...initialTransformOrigin }, + paperWidth: newPaperWidth, + paperHeight: newPaperHeight, + anchorPosition: newAnchorPosition, + }; + + // If exceed viewport, adjust anchor origin and transform origin + if (!isExceedViewportRight && !isExceedViewportLeft) { + if (isExceedViewportBottom && !isExceedViewportTop) { + newPosition.anchorOrigin.vertical = 'top'; + newPosition.transformOrigin.vertical = 'bottom'; + } else if (!isExceedViewportBottom && isExceedViewportTop) { + newPosition.anchorOrigin.vertical = 'bottom'; + newPosition.transformOrigin.vertical = 'top'; + } + } else if (!isExceedViewportBottom && !isExceedViewportTop) { + if (isExceedViewportRight && !isExceedViewportLeft) { + newPosition.anchorOrigin.horizontal = 'left'; + newPosition.transformOrigin.horizontal = 'right'; + } else if (!isExceedViewportRight && isExceedViewportLeft) { + newPosition.anchorOrigin.horizontal = 'right'; + newPosition.transformOrigin.horizontal = 'left'; + } + } + + // anchorPosition is top-left of the anchor element, so we need to adjust it to avoid overlap with the anchor element + if (newPosition.anchorOrigin.vertical === 'bottom' && newPosition.transformOrigin.vertical === 'top') { + newPosition.anchorPosition.top += anchorRect.height; + } + + if ( + isExceedViewportTop && + isExceedViewportBottom && + newPosition.anchorOrigin.vertical === 'top' && + newPosition.transformOrigin.vertical === 'bottom' + ) { + newPosition.paperHeight = newPaperHeight - anchorRect.height; + } + + // Set new position and set isEntered to true + setPosition({ ...newPosition, isEntered: true }); + }, [ + initialAnchorOrigin, + initialTransformOrigin, + initialPaperWidth, + initialPaperHeight, + marginThreshold, + anchorEl, + anchorPosition, + ]); + + useEffect(() => { + if (!open) return; + calculateAnchorSize(); + }, [open, calculateAnchorSize]); + + return { + ...position, + calculateAnchorSize, + }; +}; + +export default usePopoverAutoPosition; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/utils.ts new file mode 100644 index 0000000000..dccaf2f4d4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/utils.ts @@ -0,0 +1,45 @@ +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +export function getOffsetTop(rect: DOMRect, vertical: number | 'center' | 'bottom' | 'top') { + let offset = 0; + + if (typeof vertical === 'number') { + offset = vertical; + } else if (vertical === 'center') { + offset = rect.height / 2; + } else if (vertical === 'bottom') { + offset = rect.height; + } + + return offset; +} + +export function getOffsetLeft(rect: DOMRect, horizontal: number | 'center' | 'left' | 'right') { + let offset = 0; + + if (typeof horizontal === 'number') { + offset = horizontal; + } else if (horizontal === 'center') { + offset = rect.width / 2; + } else if (horizontal === 'right') { + offset = rect.width; + } + + return offset; +} + +export function getAnchorOffset(anchorElement: HTMLElement, anchorOrigin: PopoverOrigin) { + const anchorRect = anchorElement.getBoundingClientRect(); + + return { + top: anchorRect.top + getOffsetTop(anchorRect, anchorOrigin.vertical), + left: anchorRect.left + getOffsetLeft(anchorRect, anchorOrigin.horizontal), + }; +} + +export function getTransformOrigin(elemRect: DOMRect, transformOrigin: PopoverOrigin) { + return { + vertical: getOffsetTop(elemRect, transformOrigin.vertical), + horizontal: getOffsetLeft(elemRect, transformOrigin.horizontal), + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx new file mode 100644 index 0000000000..0527b6cc26 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx @@ -0,0 +1,55 @@ +import { Scrollbars } from 'react-custom-scrollbars'; +import React from 'react'; + +export interface AFScrollerProps { + children: React.ReactNode; + overflowXHidden?: boolean; + overflowYHidden?: boolean; + className?: string; + style?: React.CSSProperties; +} +export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => { + return ( +
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => ( +
+ )} + > + {children} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts new file mode 100644 index 0000000000..7a740a5bb0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts @@ -0,0 +1 @@ +export * from './AFScroller'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx new file mode 100644 index 0000000000..95e44ae9c2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx @@ -0,0 +1,53 @@ +import ViewIconGroup from '$app/components/_shared/view_title/ViewIconGroup'; +import { PageCover, PageIcon } from '$app_reducers/pages/slice'; +import ViewIcon from '$app/components/_shared/view_title/ViewIcon'; +import { ViewCover } from '$app/components/_shared/view_title/cover'; + +function ViewBanner({ + icon, + hover, + onUpdateIcon, + showCover, + cover, + onUpdateCover, +}: { + icon?: PageIcon; + hover: boolean; + onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover?: PageCover) => void; +}) { + return ( +
+ {showCover && cover && } + +
+
+ +
+
+ +
+
+
+ ); +} + +export default ViewBanner; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx new file mode 100644 index 0000000000..009548df53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIcon.tsx @@ -0,0 +1,62 @@ +import React, { useCallback, useState } from 'react'; +import Popover from '@mui/material/Popover'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import { PageIcon } from '$app_reducers/pages/slice'; + +function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon: string) => void }) { + const [anchorPosition, setAnchorPosition] = useState<{ + top: number; + left: number; + }>(); + + const open = Boolean(anchorPosition); + const onOpen = useCallback((event: React.MouseEvent) => { + const rect = event.currentTarget.getBoundingClientRect(); + + setAnchorPosition({ + top: rect.top + rect.height, + left: rect.left, + }); + }, []); + + const onEmojiSelect = useCallback( + (emoji: string) => { + onUpdateIcon(emoji); + if (!emoji) { + setAnchorPosition(undefined); + } + }, + [onUpdateIcon] + ); + + if (!icon) return null; + return ( + <> +
+
+ {icon.value} +
+
+ {open && ( + setAnchorPosition(undefined)} + > + { + setAnchorPosition(undefined); + }} + onEmojiSelect={onEmojiSelect} + /> + + )} + + ); +} + +export default ViewIcon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx new file mode 100644 index 0000000000..54256f8eb1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewIconGroup.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from 'react-i18next'; +import { CoverType, PageCover, PageIcon } from '$app_reducers/pages/slice'; +import React, { useCallback } from 'react'; +import { randomEmoji } from '$app/utils/emoji'; +import { EmojiEmotionsOutlined } from '@mui/icons-material'; +import Button from '@mui/material/Button'; +import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; +import { ImageType } from '$app/application/document/document.types'; + +interface Props { + icon?: PageIcon; + onUpdateIcon: (icon: string) => void; + showCover: boolean; + cover?: PageCover; + onUpdateCover?: (cover: PageCover) => void; +} + +const defaultCover = { + cover_selection_type: CoverType.Asset, + cover_selection: 'app_flowy_abstract_cover_2.jpeg', + image_type: ImageType.Internal, +}; + +function ViewIconGroup({ icon, onUpdateIcon, showCover, cover, onUpdateCover }: Props) { + const { t } = useTranslation(); + + const showAddIcon = !icon?.value; + + const showAddCover = !cover && showCover; + + const onAddIcon = useCallback(() => { + const emoji = randomEmoji(); + + onUpdateIcon(emoji); + }, [onUpdateIcon]); + + const onAddCover = useCallback(() => { + onUpdateCover?.(defaultCover); + }, [onUpdateCover]); + + return ( +
+ {showAddIcon && ( + + )} + {showAddCover && ( + + )} +
+ ); +} + +export default ViewIconGroup; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx new file mode 100644 index 0000000000..8d81b6d4b7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitle.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import ViewBanner from '$app/components/_shared/view_title/ViewBanner'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; +import { ViewIconTypePB } from '@/services/backend'; +import ViewTitleInput from '$app/components/_shared/view_title/ViewTitleInput'; + +interface Props { + view: Page; + showTitle?: boolean; + onTitleChange?: (title: string) => void; + onUpdateIcon?: (icon: PageIcon) => void; + forceHover?: boolean; + showCover?: boolean; + onUpdateCover?: (cover?: PageCover) => void; +} + +function ViewTitle({ + view, + forceHover = false, + onTitleChange, + showTitle = true, + onUpdateIcon: onUpdateIconProp, + showCover = false, + onUpdateCover, +}: Props) { + const [hover, setHover] = useState(false); + const [icon, setIcon] = useState(view.icon); + + useEffect(() => { + setIcon(view.icon); + }, [view.icon]); + + const onUpdateIcon = useCallback( + (icon: string) => { + const newIcon = { + value: icon, + ty: ViewIconTypePB.Emoji, + }; + + setIcon(newIcon); + onUpdateIconProp?.(newIcon); + }, + [onUpdateIconProp] + ); + + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + > + + {showTitle && ( +
+ +
+ )} +
+ ); +} + +export default ViewTitle; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx new file mode 100644 index 0000000000..2c69bb4d76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx @@ -0,0 +1,31 @@ +import React, { FormEventHandler, memo, useCallback, useRef } from 'react'; +import { TextareaAutosize } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function ViewTitleInput({ value, onChange }: { value: string; onChange?: (value: string) => void }) { + const { t } = useTranslation(); + const textareaRef = useRef(null); + + const onTitleChange: FormEventHandler = useCallback( + (e) => { + const value = e.currentTarget.value; + + onChange?.(value); + }, + [onChange] + ); + + return ( + + ); +} + +export default memo(ViewTitleInput); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx new file mode 100644 index 0000000000..78b8bbcc46 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/Colors.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { colorMap } from '$app/utils/color'; + +const colors = Object.entries(colorMap); + +function Colors({ onDone }: { onDone?: (value: string) => void }) { + return ( +
+ {colors.map(([name, value]) => ( +
onDone?.(name)} + /> + ))} +
+ ); +} + +export default Colors; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx new file mode 100644 index 0000000000..bd8c178380 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/CoverPopover.tsx @@ -0,0 +1,112 @@ +import React, { useMemo } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { PopoverOrigin } from '@mui/material/Popover'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY, UploadImage } from '$app/components/_shared/image_upload'; +import { useTranslation } from 'react-i18next'; +import Colors from '$app/components/_shared/view_title/cover/Colors'; +import { ImageType } from '$app/application/document/document.types'; +import Button from '@mui/material/Button'; + +const initialOrigin: { + anchorOrigin: PopoverOrigin; + transformOrigin: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, +}; + +function CoverPopover({ + anchorEl, + open, + onClose, + onUpdateCover, + onRemoveCover, +}: { + anchorEl: HTMLElement | null; + open: boolean; + onClose: () => void; + onUpdateCover?: (cover?: PageCover) => void; + onRemoveCover?: () => void; +}) { + const { t } = useTranslation(); + const tabOptions: TabOption[] = useMemo(() => { + return [ + { + label: t('document.plugins.cover.colors'), + key: TAB_KEY.Colors, + Component: Colors, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Color, + cover_selection: value, + image_type: ImageType.Internal, + }); + }, + }, + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.Local, + }); + onClose(); + }, + }, + { + label: t('document.imageBlock.embedLink.label'), + key: TAB_KEY.EMBED_LINK, + Component: EmbedLink, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + onClose(); + }, + }, + { + key: TAB_KEY.UNSPLASH, + label: t('document.imageBlock.unsplash.label'), + Component: Unsplash, + onDone: (value: string) => { + onUpdateCover?.({ + cover_selection_type: CoverType.Image, + cover_selection: value, + image_type: ImageType.External, + }); + }, + }, + ]; + }, [onClose, onUpdateCover, t]); + + return ( + + {t('button.remove')} + + } + /> + ); +} + +export default CoverPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx new file mode 100644 index 0000000000..f207e07886 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCover.tsx @@ -0,0 +1,80 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { CoverType, PageCover } from '$app_reducers/pages/slice'; +import { renderColor } from '$app/utils/color'; +import ViewCoverActions from '$app/components/_shared/view_title/cover/ViewCoverActions'; +import CoverPopover from '$app/components/_shared/view_title/cover/CoverPopover'; +import DefaultImage from '$app/assets/images/default_cover.jpg'; +import { ImageType } from '$app/application/document/document.types'; +import { LocalImage } from '$app/components/_shared/image_upload'; + +export function ViewCover({ cover, onUpdateCover }: { cover: PageCover; onUpdateCover?: (cover?: PageCover) => void }) { + const { + cover_selection_type: type, + cover_selection: value = '', + image_type: source, + } = useMemo(() => cover || {}, [cover]); + const [showAction, setShowAction] = useState(false); + const actionRef = useRef(null); + const [showPopover, setShowPopover] = useState(false); + + const renderCoverColor = useCallback((color: string) => { + return ( +
+ ); + }, []); + + const renderCoverImage = useCallback((url: string) => { + return {''}; + }, []); + + const handleRemoveCover = useCallback(() => { + onUpdateCover?.(null); + }, [onUpdateCover]); + + const handleClickChange = useCallback(() => { + setShowPopover(true); + }, []); + + return ( +
{ + setShowAction(true); + }} + onMouseLeave={() => { + setShowAction(false); + }} + className={'relative flex h-[255px] w-full'} + > + {source === ImageType.Local ? ( + + ) : ( + <> + {type === CoverType.Asset ? renderCoverImage(DefaultImage) : null} + {type === CoverType.Color ? renderCoverColor(value) : null} + {type === CoverType.Image ? renderCoverImage(value) : null} + + )} + + + {showPopover && ( + setShowPopover(false)} + anchorEl={actionRef.current} + onUpdateCover={onUpdateCover} + onRemoveCover={handleRemoveCover} + /> + )} +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx new file mode 100644 index 0000000000..fbf8063f44 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx @@ -0,0 +1,44 @@ +import React, { forwardRef } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; + +function ViewCoverActions( + { show, onRemove, onClickChange }: { show: boolean; onRemove: () => void; onClickChange: () => void }, + ref: React.ForwardedRef +) { + const { t } = useTranslation(); + + return ( +
+
+
+ +
+
+
+ ); +} + +export default forwardRef(ViewCoverActions); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts new file mode 100644 index 0000000000..8df50bb41e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/index.ts @@ -0,0 +1 @@ +export * from './ViewCover'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx new file mode 100644 index 0000000000..481b80a532 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx @@ -0,0 +1,51 @@ +import Button from '@mui/material/Button'; +import GoogleIcon from '$app/assets/settings/google.png'; +import GithubIcon from '$app/assets/settings/github.png'; +import DiscordIcon from '$app/assets/settings/discord.png'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import { ProviderTypePB } from '@/services/backend'; + +export const LoginButtonGroup = () => { + const { t } = useTranslation(); + + const { signIn } = useAuth(); + + return ( +
+ + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx new file mode 100644 index 0000000000..523f0b5188 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx @@ -0,0 +1,127 @@ +import { Outlet } from 'react-router-dom'; +import { useAuth } from './auth.hooks'; +import Layout from '$app/components/layout/Layout'; +import { useCallback, useEffect, useState } from 'react'; +import { Welcome } from '$app/components/auth/Welcome'; +import { isTauri } from '$app/utils/env'; +import { notify } from '$app/components/_shared/notify'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; +import { CircularProgress, Portal } from '@mui/material'; +import { ReactComponent as Logo } from '$app/assets/logo.svg'; +import { useAppDispatch } from '$app/stores/store'; + +export const ProtectedRoutes = () => { + const { currentUser, checkUser, subscribeToUser, signInWithOAuth } = useAuth(); + const dispatch = useAppDispatch(); + + const isLoading = currentUser?.loginState === LoginState.Loading; + + const [checked, setChecked] = useState(false); + + const checkUserStatus = useCallback(async () => { + await checkUser(); + setChecked(true); + }, [checkUser]); + + useEffect(() => { + void checkUserStatus(); + }, [checkUserStatus]); + + useEffect(() => { + if (currentUser.isAuthenticated) { + return subscribeToUser(); + } + }, [currentUser.isAuthenticated, subscribeToUser]); + + const onDeepLink = useCallback(async () => { + if (!isTauri()) return; + const { event } = await import('@tauri-apps/api'); + + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + return await event.listen('open_deep_link', async (e) => { + const payload = e.payload as string; + + const [, hash] = payload.split('//#'); + const obj = parseHash(hash); + + if (!obj.access_token) { + notify.error('Failed to sign in, the access token is missing'); + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return; + } + + try { + await signInWithOAuth(payload); + } catch (e) { + notify.error('Failed to sign in, please try again'); + } + }); + }, [dispatch, signInWithOAuth]); + + useEffect(() => { + void onDeepLink(); + }, [onDeepLink]); + + return ( +
+ {checked ? ( + + ) : ( +
+ +
+ )} + + {isLoading && } +
+ ); +}; + +const StartLoading = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + const preventDefault = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + dispatch(currentUserActions.resetLoginState()); + } + }; + + document.addEventListener('keydown', preventDefault, true); + + return () => { + document.removeEventListener('keydown', preventDefault, true); + }; + }, [dispatch]); + return ( + +
+ +
+
+ ); +}; + +const SplashScreen = ({ isAuthenticated }: { isAuthenticated: boolean }) => { + if (isAuthenticated) { + return ( + + + + ); + } else { + return ; + } +}; + +function parseHash(hash: string) { + const hashParams = new URLSearchParams(hash); + const hashObject: Record = {}; + + for (const [key, value] of hashParams) { + hashObject[key] = value; + } + + return hashObject; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx new file mode 100644 index 0000000000..eadcf08c21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx @@ -0,0 +1,55 @@ +import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import { Log } from '$app/utils/log'; + +export const Welcome = () => { + const { signInAsAnonymous } = useAuth(); + const { t } = useTranslation(); + + return ( + <> +
e.preventDefault()} method='POST'> +
+
+ +
+ +
+ + {t('welcomeTo')} {t('appName')} + +
+ +
+ +
+
+ {t('signIn.or')} +
+
+
+ +
+
+
+ + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts new file mode 100644 index 0000000000..89b7388e64 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts @@ -0,0 +1,186 @@ +import { currentUserActions, LoginState, parseWorkspaceSettingPBToSetting } from '$app_reducers/current-user/slice'; +import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user'; +import { UserService } from '$app/application/user/user.service'; +import { AuthService } from '$app/application/user/auth.service'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { getCurrentWorkspaceSetting } from '$app/application/folder/workspace.service'; +import { useCallback } from 'react'; +import { subscribeNotifications } from '$app/application/notification'; +import { nanoid } from 'nanoid'; +import { open } from '@tauri-apps/api/shell'; + +export const useAuth = () => { + const dispatch = useAppDispatch(); + const currentUser = useAppSelector((state) => state.currentUser); + + // Subscribe to user update events + const subscribeToUser = useCallback(() => { + const unsubscribePromise = subscribeNotifications({ + [UserNotification.DidUpdateUserProfile]: async (changeset) => { + dispatch( + currentUserActions.updateUser({ + email: changeset.email, + displayName: changeset.name, + iconUrl: changeset.icon_url, + }) + ); + }, + }); + + return () => { + void unsubscribePromise.then((fn) => fn()); + }; + }, [dispatch]); + + const setUser = useCallback( + async (userProfile?: Partial) => { + if (!userProfile) return; + + const workspaceSetting = await getCurrentWorkspaceSetting(); + + const isLocal = userProfile.authenticator === AuthenticatorPB.Local; + + dispatch( + currentUserActions.updateUser({ + id: userProfile.id, + token: userProfile.token, + email: userProfile.email, + displayName: userProfile.name, + iconUrl: userProfile.icon_url, + isAuthenticated: true, + workspaceSetting: workspaceSetting ? parseWorkspaceSettingPBToSetting(workspaceSetting) : undefined, + isLocal, + }) + ); + }, + [dispatch] + ); + + // Check if the user is authenticated + const checkUser = useCallback(async () => { + const userProfile = await UserService.getUserProfile(); + + await setUser(userProfile); + + return userProfile; + }, [setUser]); + + const register = useCallback( + async (email: string, password: string, name: string): Promise => { + const deviceId = currentUser?.deviceId ?? nanoid(8); + const userProfile = await AuthService.signUp({ deviceId, email, password, name }); + + await setUser(userProfile); + + return userProfile; + }, + [setUser, currentUser?.deviceId] + ); + + const logout = useCallback(async () => { + await AuthService.signOut(); + dispatch(currentUserActions.logout()); + }, [dispatch]); + + const signInAsAnonymous = useCallback(async () => { + const fakeEmail = nanoid(8) + '@appflowy.io'; + const fakePassword = 'AppFlowy123@'; + const fakeName = 'Me'; + + await register(fakeEmail, fakePassword, fakeName); + }, [register]); + + const signIn = useCallback( + async (provider: ProviderTypePB) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + try { + const url = await AuthService.getOAuthURL(provider); + + await open(url); + } catch { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + } + }, + [dispatch] + ); + + const signInWithOAuth = useCallback( + async (uri: string) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + try { + const deviceId = currentUser?.deviceId ?? nanoid(8); + + await AuthService.signInWithOAuth({ uri, deviceId }); + const userProfile = await UserService.getUserProfile(); + + await setUser(userProfile); + + return userProfile; + } catch (e) { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return Promise.reject(e); + } + }, + [dispatch, currentUser?.deviceId, setUser] + ); + + // Only for development purposes + const signInWithEmailPassword = useCallback( + async (email: string, password: string, domain?: string) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + + try { + const response = await fetch( + `https://${domain ? domain : 'test.appflowy.cloud'}/gotrue/token?grant_type=password`, + { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + redirect: 'follow', + referrerPolicy: 'no-referrer', + body: JSON.stringify({ + email, + password, + }), + } + ); + + const data = await response.json(); + + let uri = `appflowy-flutter://#`; + const params: string[] = []; + + Object.keys(data).forEach((key) => { + if (typeof data[key] === 'object') { + return; + } + + params.push(`${key}=${data[key]}`); + }); + uri += params.join('&'); + + return signInWithOAuth(uri); + } catch (e) { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return Promise.reject(e); + } + }, + [dispatch, signInWithOAuth] + ); + + return { + currentUser, + checkUser, + register, + logout, + subscribeToUser, + signInAsAnonymous, + signIn, + signInWithOAuth, + signInWithEmailPassword, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts new file mode 100644 index 0000000000..2597c158a1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.hooks.ts @@ -0,0 +1,204 @@ +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { proxy, useSnapshot } from 'valtio'; + +import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend'; +import { subscribeNotifications } from '$app/application/notification'; +import { + Cell, + Database, + databaseService, + cellListeners, + fieldListeners, + rowListeners, + sortListeners, + filterListeners, +} from '$app/application/database'; + +export function useSelectDatabaseView({ viewId }: { viewId?: string }) { + const key = 'v'; + const [searchParams, setSearchParams] = useSearchParams(); + + const selectedViewId = useMemo(() => searchParams.get(key) || viewId, [searchParams, viewId]); + + const onChange = useCallback( + (value: string) => { + setSearchParams({ [key]: value }); + }, + [setSearchParams] + ); + + return { + selectedViewId, + onChange, + }; +} + +const DatabaseContext = createContext({ + id: '', + isLinked: false, + layoutType: DatabaseLayoutPB.Grid, + fields: [], + rowMetas: [], + filters: [], + sorts: [], + groupSettings: [], + groups: [], + typeOptions: {}, + cells: {}, +}); + +export const DatabaseProvider = DatabaseContext.Provider; + +export const useDatabase = () => useSnapshot(useContext(DatabaseContext)); + +export const useSelectorCell = (rowId: string, fieldId: string) => { + const database = useContext(DatabaseContext); + const cells = useSnapshot(database.cells); + + return cells[`${rowId}:${fieldId}`]; +}; + +export const useDispatchCell = () => { + const database = useContext(DatabaseContext); + + const setCell = useCallback( + (cell: Cell) => { + const id = `${cell.rowId}:${cell.fieldId}`; + + database.cells[id] = cell; + }, + [database] + ); + + const deleteCells = useCallback( + ({ rowId, fieldId }: { rowId: string; fieldId?: string }) => { + cellListeners.didDeleteCells({ database, rowId, fieldId }); + }, + [database] + ); + + return { + deleteCells, + setCell, + }; +}; + +export const useDatabaseSorts = () => { + const context = useContext(DatabaseContext); + + return useSnapshot(context.sorts); +}; + +export const useSortsCount = () => { + const { sorts } = useDatabase(); + + return sorts?.length; +}; + +export const useFiltersCount = () => { + const { filters, fields } = useDatabase(); + + // filter fields: if the field is deleted, it will not be displayed + return useMemo( + () => filters?.map((filter) => fields.find((field) => field.id === filter.fieldId)).filter(Boolean).length, + [filters, fields] + ); +}; + +export function useStaticTypeOption(fieldId: string) { + const context = useContext(DatabaseContext); + const typeOptions = context.typeOptions; + + return typeOptions[fieldId] as T; +} + +export function useTypeOption(fieldId: string) { + const context = useContext(DatabaseContext); + const typeOptions = useSnapshot(context.typeOptions); + + return typeOptions[fieldId] as T; +} + +export const useDatabaseVisibilityRows = () => { + const { rowMetas } = useDatabase(); + + return useMemo(() => rowMetas.filter((row) => row && !row.isHidden), [rowMetas]); +}; + +export const useDatabaseVisibilityFields = () => { + const database = useDatabase(); + + return useMemo( + () => database.fields.filter((field) => field.visibility !== FieldVisibility.AlwaysHidden), + [database.fields] + ); +}; + +export const useConnectDatabase = (viewId: string) => { + const database = useMemo(() => { + const proxyDatabase = proxy({ + id: '', + isLinked: false, + layoutType: DatabaseLayoutPB.Grid, + fields: [], + rowMetas: [], + filters: [], + sorts: [], + groupSettings: [], + groups: [], + typeOptions: {}, + cells: {}, + }); + + void databaseService.openDatabase(viewId).then((value) => Object.assign(proxyDatabase, value)); + + return proxyDatabase; + }, [viewId]); + + useEffect(() => { + const unsubscribePromise = subscribeNotifications( + { + [DatabaseNotification.DidUpdateFields]: async (changeset) => { + await fieldListeners.didUpdateFields(viewId, database, changeset); + }, + [DatabaseNotification.DidUpdateFieldSettings]: (changeset) => { + fieldListeners.didUpdateFieldSettings(database, changeset); + }, + [DatabaseNotification.DidUpdateViewRows]: async (changeset) => { + await rowListeners.didUpdateViewRows(viewId, database, changeset); + }, + [DatabaseNotification.DidReorderRows]: (changeset) => { + rowListeners.didReorderRows(database, changeset); + }, + [DatabaseNotification.DidReorderSingleRow]: (changeset) => { + rowListeners.didReorderSingleRow(database, changeset); + }, + + [DatabaseNotification.DidUpdateSort]: (changeset) => { + sortListeners.didUpdateSort(database, changeset); + }, + + [DatabaseNotification.DidUpdateFilter]: (changeset) => { + filterListeners.didUpdateFilter(database, changeset); + }, + [DatabaseNotification.DidUpdateViewRowsVisibility]: async (changeset) => { + await rowListeners.didUpdateViewRowsVisibility(viewId, database, changeset); + }, + }, + { id: viewId } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [viewId, database]); + + return database; +}; + +const DatabaseRenderedContext = createContext<(viewId: string) => void>(() => { + return; +}); + +export const DatabaseRenderedProvider = DatabaseRenderedContext.Provider; + +export const useDatabaseRendered = () => useContext(DatabaseRenderedContext); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx new file mode 100644 index 0000000000..d5e7bba45b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/Database.tsx @@ -0,0 +1,202 @@ +import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useViewId } from '$app/hooks/ViewId.hooks'; +import { databaseViewService } from '$app/application/database'; +import { DatabaseTabBar } from './components'; +import { DatabaseLoader } from './DatabaseLoader'; +import { DatabaseView } from './DatabaseView'; +import { DatabaseCollection } from './components/database_settings'; +import SwipeableViews from 'react-swipeable-views'; +import { TabPanel } from '$app/components/database/components/tab_bar/ViewTabs'; +import DatabaseSettings from '$app/components/database/components/database_settings/DatabaseSettings'; +import { Portal } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { ErrorCode, FolderNotification } from '@/services/backend'; +import ExpandRecordModal from '$app/components/database/components/edit_record/ExpandRecordModal'; +import { subscribeNotifications } from '$app/application/notification'; +import { Page } from '$app_reducers/pages/slice'; +import { getPage } from '$app/application/folder/page.service'; +import './database.scss'; + +interface Props { + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +export const Database = forwardRef(({ selectedViewId, setSelectedViewId }, ref) => { + const innerRef = useRef(); + const databaseRef = (ref ?? innerRef) as React.MutableRefObject; + const viewId = useViewId(); + const [settingDom, setSettingDom] = useState(null); + + const [page, setPage] = useState(null); + const { t } = useTranslation(); + const [notFound, setNotFound] = useState(false); + const [childViews, setChildViews] = useState([]); + const [editRecordRowId, setEditRecordRowId] = useState(null); + const [openCollections, setOpenCollections] = useState([]); + + const handleResetDatabaseViews = useCallback(async (viewId: string) => { + await databaseViewService + .getDatabaseViews(viewId) + .then((value) => { + setChildViews(value); + }) + .catch((err) => { + if (err.code === ErrorCode.RecordNotFound) { + setNotFound(true); + } + }); + }, []); + + const handleGetPage = useCallback(async () => { + try { + const page = await getPage(viewId); + + setPage(page); + } catch (e) { + setNotFound(true); + } + }, [viewId]); + + const parentId = page?.parentId; + + useEffect(() => { + void handleGetPage(); + void handleResetDatabaseViews(viewId); + const unsubscribePromise = subscribeNotifications({ + [FolderNotification.DidUpdateView]: (changeset) => { + if (changeset.parent_view_id !== viewId && changeset.id !== viewId) return; + setChildViews((prev) => { + const index = prev.findIndex((view) => view.id === changeset.id); + + if (index === -1) { + return prev; + } + + const newViews = [...prev]; + + newViews[index] = { + ...newViews[index], + name: changeset.name, + }; + + return newViews; + }); + }, + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.parent_view_id !== viewId && changeset.parent_view_id !== parentId) return; + if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) { + return; + } + + void handleResetDatabaseViews(viewId); + }, + }); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [handleGetPage, handleResetDatabaseViews, viewId, parentId]); + + const value = useMemo(() => { + return Math.max( + 0, + childViews.findIndex((view) => view.id === (selectedViewId ?? viewId)) + ); + }, [childViews, selectedViewId, viewId]); + + const onToggleCollection = useCallback( + (id: string, forceOpen?: boolean) => { + if (forceOpen) { + setOpenCollections((prev) => { + if (prev.includes(id)) { + return prev; + } + + return [...prev, id]; + }); + return; + } + + if (openCollections.includes(id)) { + setOpenCollections((prev) => prev.filter((item) => item !== id)); + } else { + setOpenCollections((prev) => [...prev, id]); + } + }, + [openCollections, setOpenCollections] + ); + + const onEditRecord = useCallback( + (rowId: string) => { + setEditRecordRowId(rowId); + }, + [setEditRecordRowId] + ); + + if (notFound) { + return ( +
+

{t('deletePagePrompt.text')}

+
+ ); + } + + return ( +
+ + + {childViews.map((view, index) => ( + + + {selectedViewId === view.id && ( + <> + {settingDom && ( + + onToggleCollection(view.id, forceOpen)} + /> + + )} + + + {editRecordRowId && ( + { + setEditRecordRowId(null); + }} + /> + )} + + )} + + + + + ))} + +
+ ); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx new file mode 100644 index 0000000000..b0aeab10a2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseLoader.tsx @@ -0,0 +1,18 @@ +import { FC, PropsWithChildren } from 'react'; +import { ViewIdProvider } from '$app/hooks'; +import { DatabaseProvider, useConnectDatabase } from './Database.hooks'; + +export interface DatabaseLoaderProps { + viewId: string; +} + +export const DatabaseLoader: FC> = ({ viewId, children }) => { + const database = useConnectDatabase(viewId); + + return ( + + {/* Make sure that the viewId is current */} + {children} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx new file mode 100644 index 0000000000..cd94947d8d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx @@ -0,0 +1,32 @@ +import { FormEventHandler, useCallback } from 'react'; +import { useViewId } from '$app/hooks'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { updatePageName } from '$app_reducers/pages/async_actions'; +import { useTranslation } from 'react-i18next'; + +export const DatabaseTitle = () => { + const viewId = useViewId(); + const { t } = useTranslation(); + const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || ''); + const dispatch = useAppDispatch(); + + const handleInput = useCallback( + (event) => { + const newTitle = (event.target as HTMLInputElement).value; + + void dispatch(updatePageName({ id: viewId, name: newTitle })); + }, + [viewId, dispatch] + ); + + return ( +
+ +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx new file mode 100644 index 0000000000..98b16d5fea --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseView.tsx @@ -0,0 +1,23 @@ +import { DatabaseLayoutPB } from '@/services/backend'; +import { FC } from 'react'; +import { useDatabase } from './Database.hooks'; +import { Grid } from './grid'; +import { Board } from './board'; +import { Calendar } from './calendar'; + +export const DatabaseView: FC<{ + onEditRecord: (rowId: string) => void; +}> = (props) => { + const { layoutType } = useDatabase(); + + switch (layoutType) { + case DatabaseLayoutPB.Grid: + return ; + case DatabaseLayoutPB.Board: + return ; + case DatabaseLayoutPB.Calendar: + return ; + default: + return null; + } +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx new file mode 100644 index 0000000000..01666121cd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/CellText.tsx @@ -0,0 +1,17 @@ +import React, { HTMLAttributes, PropsWithChildren } from 'react'; + +export interface CellTextProps { + className?: string; +} + +export const CellText = React.forwardRef>>( + function CellText(props, ref) { + const { children, className, ...other } = props; + + return ( +
+ {children} +
+ ); + } +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/LinearProgressWithLabel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..7d4c0d1811 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/LinearProgressWithLabel.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; + +function LinearProgressWithLabel({ + value, + count, + selectedCount, +}: { + value: number; + count: number; + selectedCount: number; +}) { + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + const options = useMemo(() => { + return Array.from({ length: count }, (_, i) => ({ + id: i, + checked: i < selectedCount, + })); + }, [count, selectedCount]); + + const isSplit = count < 6; + + return ( +
+
+ {options.map((option) => ( + + ))} +
+
{result}
+
+ ); +} + +export default LinearProgressWithLabel; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts new file mode 100644 index 0000000000..fd1aab7a37 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/constants.ts @@ -0,0 +1,9 @@ +export enum DragType { + Row = 'row', + Field = 'field', +} + +export enum DropPosition { + Before = 0, + After = 1, +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts new file mode 100644 index 0000000000..8954dc733a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/dnd.context.ts @@ -0,0 +1,17 @@ +import { createContext } from 'react'; +import { proxy } from 'valtio'; + +export interface DragItem> { + type: string; + data: T; +} + +export interface DndContextDescriptor { + dragging: DragItem | null, +} + +const defaultDndContext: DndContextDescriptor = proxy({ + dragging: null, +}); + +export const DndContext = createContext(defaultDndContext); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts new file mode 100644 index 0000000000..ce8afe6f31 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drag.hooks.ts @@ -0,0 +1,114 @@ +import { DragEventHandler, useCallback, useContext, useMemo, useRef, useState } from 'react'; +import { DndContext } from './dnd.context'; +import { autoScrollOnEdge, EdgeGap, getScrollParent, ScrollDirection } from './utils'; + +export interface UseDraggableOptions { + type: string; + effectAllowed?: DataTransfer['effectAllowed']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record; + disabled?: boolean; + scrollOnEdge?: { + direction?: ScrollDirection; + getScrollElement?: () => HTMLElement | null; + edgeGap?: number | Partial; + }; +} + +export const useDraggable = ({ + type, + effectAllowed = 'copyMove', + data, + disabled, + scrollOnEdge, +}: UseDraggableOptions) => { + const scrollDirection = scrollOnEdge?.direction; + const edgeGap = scrollOnEdge?.edgeGap; + + const context = useContext(DndContext); + const typeRef = useRef(type); + const dataRef = useRef(data); + const previewRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); + + typeRef.current = type; + dataRef.current = data; + + const setPreviewRef = useCallback((previewElement: null | Element) => { + previewRef.current = previewElement; + }, []); + + const attributes: { + draggable?: boolean; + } = useMemo(() => { + if (disabled) { + return {}; + } + + return { + draggable: true, + }; + }, [disabled]); + + const onDragStart = useCallback( + (event) => { + setIsDragging(true); + context.dragging = { + type: typeRef.current, + data: dataRef.current ?? {}, + }; + + const { dataTransfer } = event; + const previewNode = previewRef.current; + + dataTransfer.effectAllowed = effectAllowed; + + if (previewNode) { + const { clientX, clientY } = event; + const rect = previewNode.getBoundingClientRect(); + + dataTransfer.setDragImage(previewNode, clientX - rect.x, clientY - rect.y); + } + + if (scrollDirection === undefined) { + return; + } + + const scrollParent: HTMLElement | null = + scrollOnEdge?.getScrollElement?.() ?? getScrollParent(event.target as HTMLElement, scrollDirection); + + if (scrollParent) { + autoScrollOnEdge({ + element: scrollParent, + direction: scrollDirection, + edgeGap, + }); + } + }, + [context, effectAllowed, scrollDirection, scrollOnEdge, edgeGap] + ); + + const onDragEnd = useCallback(() => { + setIsDragging(false); + context.dragging = null; + }, [context]); + + const listeners: { + onDragStart?: DragEventHandler; + onDragEnd?: DragEventHandler; + } = useMemo( + () => ({ + onDragStart, + onDragEnd, + }), + [onDragStart, onDragEnd] + ); + + return { + isDragging, + previewRef, + attributes, + listeners, + setPreviewRef, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts new file mode 100644 index 0000000000..7b3d79aeb2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/drop.hooks.ts @@ -0,0 +1,88 @@ +import { DragEventHandler, useContext, useState, useMemo, useCallback } from 'react'; +import { useSnapshot } from 'valtio'; +import { DragItem, DndContext } from './dnd.context'; + +interface UseDroppableOptions { + accept: string; + dropEffect?: DataTransfer['dropEffect']; + disabled?: boolean; + onDragOver?: DragEventHandler, + onDrop?: (data: DragItem) => void; +} + +export const useDroppable = ({ + accept, + dropEffect = 'move', + disabled, + onDragOver: handleDragOver, + onDrop: handleDrop, +}: UseDroppableOptions) => { + const dndContext = useContext(DndContext); + const dndSnapshot = useSnapshot(dndContext); + + const [ dragOver, setDragOver ] = useState(false); + const canDrop = useMemo( + () => !disabled && dndSnapshot.dragging?.type === accept, + [ disabled, accept, dndSnapshot.dragging?.type ], + ); + const isOver = useMemo(()=> canDrop && dragOver, [ canDrop, dragOver ]); + + const onDragEnter = useCallback((event) => { + if (!canDrop) { + return; + } + + event.preventDefault(); + event.dataTransfer.dropEffect = dropEffect; + + setDragOver(true); + }, [ canDrop, dropEffect ]); + + const onDragOver = useCallback((event) => { + if (!canDrop) { + return; + } + + event.preventDefault(); + event.dataTransfer.dropEffect = dropEffect; + + setDragOver(true); + handleDragOver?.(event); + }, [ canDrop, dropEffect, handleDragOver ]); + + const onDragLeave = useCallback(() => { + if (!canDrop) { + return; + } + + setDragOver(false); + }, [ canDrop ]); + + const onDrop = useCallback(() => { + if (!canDrop) { + return; + } + + const dragging = dndSnapshot.dragging; + + if (!dragging) { + return; + } + + setDragOver(false); + handleDrop?.(dragging); + }, [ canDrop, dndSnapshot.dragging, handleDrop ]); + + const listeners = useMemo(() => ({ + onDragEnter, + onDragOver, + onDragLeave, + onDrop, + }), [ onDragEnter, onDragOver, onDragLeave, onDrop ]); + + return { + isOver, + canDrop, + listeners, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts new file mode 100644 index 0000000000..8688534359 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/index.ts @@ -0,0 +1,7 @@ +export * from './dnd.context'; +export * from './drag.hooks'; +export * from './drop.hooks'; +export { + ScrollDirection, + Edge, +} from './utils'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts new file mode 100644 index 0000000000..3aafa6f77c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/dnd/utils.ts @@ -0,0 +1,170 @@ +import { interval } from '$app/utils/tool'; + +export enum Edge { + Top = 'top', + Bottom = 'bottom', + Left = 'left', + Right = 'right', +} + +export enum ScrollDirection { + Horizontal = 'horizontal', + Vertical = 'vertical', +} + +export interface EdgeGap { + top: number; + bottom: number; + left: number; + right: number; +} + +export const isReachEdge = (element: Element, edge: Edge) => { + switch (edge) { + case Edge.Left: + return element.scrollLeft === 0; + case Edge.Right: + return element.scrollLeft + element.clientWidth === element.scrollWidth; + case Edge.Top: + return element.scrollTop === 0; + case Edge.Bottom: + return element.scrollTop + element.clientHeight === element.scrollHeight; + default: + return true; + } +}; + +export const scrollBy = (element: Element, edge: Edge, offset: number) => { + let step = offset; + let prop = edge; + + if (edge === Edge.Left || edge === Edge.Top) { + step = -offset; + } else if (edge === Edge.Right) { + prop = Edge.Left; + } else if (edge === Edge.Bottom) { + prop = Edge.Top; + } + + element.scrollBy({ [prop]: step }); +}; + +export const scrollElement = (element: Element, edge: Edge, offset: number) => { + if (isReachEdge(element, edge)) { + return; + } + + scrollBy(element, edge, offset); +}; + +export const calculateLeaveEdge = ( + { x: mouseX, y: mouseY }: { x: number; y: number }, + rect: DOMRect, + gaps: EdgeGap, + direction: ScrollDirection +) => { + if (direction === ScrollDirection.Horizontal) { + if (mouseX - rect.left < gaps.left) { + return Edge.Left; + } + + if (rect.right - mouseX < gaps.right) { + return Edge.Right; + } + } + + if (direction === ScrollDirection.Vertical) { + if (mouseY - rect.top < gaps.top) { + return Edge.Top; + } + + if (rect.bottom - mouseY < gaps.bottom) { + return Edge.Bottom; + } + } + + return null; +}; + +export const getScrollParent = (element: HTMLElement | null, direction: ScrollDirection): HTMLElement | null => { + if (element === null) { + return null; + } + + if (direction === ScrollDirection.Horizontal && element.scrollWidth > element.clientWidth) { + return element; + } + + if (direction === ScrollDirection.Vertical && element.scrollHeight > element.clientHeight) { + return element; + } + + return getScrollParent(element.parentElement, direction); +}; + +export interface AutoScrollOnEdgeOptions { + element: HTMLElement; + direction: ScrollDirection; + edgeGap?: number | Partial; + step?: number; +} + +const defaultEdgeGap = 30; + +export const autoScrollOnEdge = ({ element, direction, edgeGap, step = 8 }: AutoScrollOnEdgeOptions) => { + const gaps = + typeof edgeGap === 'number' + ? { + top: edgeGap, + bottom: edgeGap, + left: edgeGap, + right: edgeGap, + } + : { + top: defaultEdgeGap, + bottom: defaultEdgeGap, + left: defaultEdgeGap, + right: defaultEdgeGap, + ...edgeGap, + }; + + const keepScroll = interval(scrollElement, 8); + + let leaveEdge: Edge | null = null; + + const onDragOver = (event: DragEvent) => { + const rect = element.getBoundingClientRect(); + + leaveEdge = calculateLeaveEdge({ x: event.clientX, y: event.clientY }, rect, gaps, direction); + + if (leaveEdge) { + keepScroll(element, leaveEdge, step); + } else { + keepScroll.cancel(); + } + }; + + const onDragLeave = () => { + if (!leaveEdge) { + return; + } + + keepScroll(element, leaveEdge, step * 2); + }; + + const cleanup = () => { + keepScroll.cancel(); + + element.removeEventListener('dragover', onDragOver); + element.removeEventListener('dragleave', onDragLeave); + + document.removeEventListener('dragend', cleanup); + }; + + element.addEventListener('dragover', onDragOver); + element.addEventListener('dragleave', onDragLeave); + + document.addEventListener('dragend', cleanup); + + return cleanup; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts new file mode 100644 index 0000000000..6bfa1f812b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/_shared/index.ts @@ -0,0 +1,5 @@ +export * from './constants'; + +export * from './dnd'; + +export * from './CellText'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/board/Board.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/board/Board.tsx new file mode 100644 index 0000000000..790f841701 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/board/Board.tsx @@ -0,0 +1,5 @@ +import { FC } from 'react'; + +export const Board: FC = () => { + return null; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/board/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/board/index.ts new file mode 100644 index 0000000000..9294d869ce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/board/index.ts @@ -0,0 +1 @@ +export * from './Board'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/Calendar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/Calendar.tsx new file mode 100644 index 0000000000..b8473fda25 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/Calendar.tsx @@ -0,0 +1,5 @@ +import { FC } from 'react'; + +export const Calendar: FC = () => { + return null; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/index.ts new file mode 100644 index 0000000000..a723380592 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts new file mode 100644 index 0000000000..76bba7b152 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.hooks.ts @@ -0,0 +1,63 @@ +import { useCallback, useEffect, useState } from 'react'; +import { DatabaseNotification } from '@/services/backend'; +import { useNotification, useViewId } from '$app/hooks'; +import { cellService, Cell, Field } from '$app/application/database'; +import { useDispatchCell, useSelectorCell } from '$app/components/database'; + +export const useCell = (rowId: string, field: Field) => { + const viewId = useViewId(); + const { setCell } = useDispatchCell(); + const [loading, setLoading] = useState(false); + const cell = useSelectorCell(rowId, field.id); + + const fetchCell = useCallback(() => { + setLoading(true); + void cellService.getCell(viewId, rowId, field.id, field.type).then((data) => { + // cache cell + setCell(data); + setLoading(false); + }); + }, [viewId, rowId, field.id, field.type, setCell]); + + useEffect(() => { + // fetch cell if not cached + if (!cell && !loading) { + // fetch cell in next tick to avoid blocking + const timeout = setTimeout(fetchCell, 0); + + return () => { + clearTimeout(timeout); + }; + } + }, [fetchCell, cell, loading, rowId, field.id]); + + useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${field.id}` }); + + return cell; +}; + +export const useInputCell = (cell?: Cell) => { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(''); + const viewId = useViewId(); + const updateCell = useCallback(() => { + if (!cell) return; + const { rowId, fieldId } = cell; + + if (editing) { + if (value !== cell.data) { + void cellService.updateCell(viewId, rowId, fieldId, value); + } + + setEditing(false); + } + }, [cell, editing, value, viewId]); + + return { + updateCell, + editing, + setEditing, + value, + setValue, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx new file mode 100644 index 0000000000..a092a1d75a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/Cell.tsx @@ -0,0 +1,65 @@ +import React, { FC, HTMLAttributes } from 'react'; +import { FieldType } from '@/services/backend'; + +import { Cell as CellType, Field } from '$app/application/database'; +import { useCell } from './Cell.hooks'; +import { TextCell } from './TextCell'; +import { SelectCell } from './SelectCell'; +import { CheckboxCell } from './CheckboxCell'; +import NumberCell from '$app/components/database/components/cell/NumberCell'; +import URLCell from '$app/components/database/components/cell/URLCell'; +import ChecklistCell from '$app/components/database/components/cell/ChecklistCell'; +import DateTimeCell from '$app/components/database/components/cell/DateTimeCell'; +import TimestampCell from '$app/components/database/components/cell/TimestampCell'; + +export interface CellProps extends HTMLAttributes { + rowId: string; + field: Field; + icon?: string; + placeholder?: string; +} + +export interface CellComponentProps extends CellProps { + cell: CellType; +} + +const getCellComponent = (fieldType: FieldType) => { + switch (fieldType) { + case FieldType.RichText: + return TextCell as FC; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectCell as FC; + case FieldType.Checkbox: + return CheckboxCell as FC; + case FieldType.Checklist: + return ChecklistCell as FC; + case FieldType.Number: + return NumberCell as FC; + case FieldType.URL: + return URLCell as FC; + case FieldType.DateTime: + return DateTimeCell as FC; + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampCell as FC; + default: + return null; + } +}; + +export const Cell: FC = ({ rowId, field, ...props }) => { + const cell = useCell(rowId, field); + + const Component = getCellComponent(field.type); + + if (!cell) { + return
; + } + + if (!Component) { + return null; + } + + return ; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx new file mode 100644 index 0000000000..f6f3dcf0d2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/CheckboxCell.tsx @@ -0,0 +1,26 @@ +import React, { FC, useCallback } from 'react'; +import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; +import { useViewId } from '$app/hooks'; +import { cellService, CheckboxCell as CheckboxCellType, Field } from '$app/application/database'; + +export const CheckboxCell: FC<{ + field: Field; + cell: CheckboxCellType; +}> = ({ field, cell }) => { + const viewId = useViewId(); + const checked = cell.data; + + const handleClick = useCallback(() => { + void cellService.updateCell(viewId, cell.rowId, field.id, !checked ? 'Yes' : 'No'); + }, [viewId, cell, field.id, checked]); + + return ( +
+ {checked ? : } +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx new file mode 100644 index 0000000000..5ecac431c4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/ChecklistCell.tsx @@ -0,0 +1,74 @@ +import React, { useState, Suspense, useMemo } from 'react'; +import { ChecklistCell as ChecklistCellType, ChecklistField } from '$app/application/database'; +import ChecklistCellActions from '$app/components/database/components/field_types/checklist/ChecklistCellActions'; +import LinearProgressWithLabel from '$app/components/database/_shared/LinearProgressWithLabel'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; + +interface Props { + field: ChecklistField; + cell: ChecklistCellType; + placeholder?: string; +} + +const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'left', +}; + +const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +function ChecklistCell({ cell, placeholder }: Props) { + const value = cell?.data.percentage ?? 0; + const options = useMemo(() => cell?.data.options ?? [], [cell?.data.options]); + const selectedOptions = useMemo(() => cell?.data.selectedOptions ?? [], [cell?.data.selectedOptions]); + const [anchorEl, setAnchorEl] = useState(undefined); + const open = Boolean(anchorEl); + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(undefined); + }; + + const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 369, + initialPaperHeight: 300, + anchorEl, + initialAnchorOrigin, + initialTransformOrigin, + open, + }); + + return ( + <> +
+ {options.length > 0 ? ( + + ) : ( +
{placeholder}
+ )} +
+ + {open && ( + + )} + + + ); +} + +export default ChecklistCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx new file mode 100644 index 0000000000..aea4f79849 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/DateTimeCell.tsx @@ -0,0 +1,125 @@ +import React, { Suspense, useRef, useState, useMemo, useEffect } from 'react'; +import { DateTimeCell as DateTimeCellType, DateTimeField } from '$app/application/database'; +import DateTimeCellActions from '$app/components/database/components/field_types/date/DateTimeCellActions'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +interface Props { + field: DateTimeField; + cell: DateTimeCellType; + placeholder?: string; +} + +const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'left', +}; + +const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +function DateTimeCell({ field, cell, placeholder }: Props) { + const isRange = cell.data.isRange; + const includeTime = cell.data.includeTime; + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const handleClose = () => { + setOpen(false); + }; + + const handleClick = () => { + setOpen(true); + }; + + const content = useMemo(() => { + const { date, time, endDate, endTime } = cell.data; + + if (date) { + return ( + <> + {date} + {includeTime && time ? ' ' + time : ''} + {isRange && endDate ? ' - ' + endDate : ''} + {isRange && includeTime && endTime ? ' ' + endTime : ''} + + ); + } + + return
{placeholder}
; + }, [cell, includeTime, isRange, placeholder]); + + const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered, calculateAnchorSize } = + usePopoverAutoPosition({ + initialPaperWidth: 248, + initialPaperHeight: 500, + anchorEl: ref.current, + initialAnchorOrigin, + initialTransformOrigin, + open, + marginThreshold: 34, + }); + + useEffect(() => { + if (!open) return; + + const anchorEl = ref.current; + + const parent = anchorEl?.parentElement?.parentElement; + + if (!anchorEl || !parent) return; + + let timeout: NodeJS.Timeout; + const handleObserve = () => { + anchorEl.scrollIntoView({ block: 'nearest' }); + + timeout = setTimeout(() => { + calculateAnchorSize(); + }, 200); + }; + + const observer = new MutationObserver(handleObserve); + + observer.observe(parent, { + childList: true, + subtree: true, + }); + return () => { + observer.disconnect(); + clearTimeout(timeout); + }; + }, [calculateAnchorSize, open]); + + return ( + <> +
+ {content} +
+ + {open && ( + + )} + + + ); +} + +export default DateTimeCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx new file mode 100644 index 0000000000..727e78de3f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/NumberCell.tsx @@ -0,0 +1,50 @@ +import React, { Suspense, useCallback, useMemo, useRef } from 'react'; +import { Field, NumberCell as NumberCellType } from '$app/application/database'; +import { CellText } from '$app/components/database/_shared'; +import EditNumberCellInput from '$app/components/database/components/field_types/number/EditNumberCellInput'; +import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; + +interface Props { + field: Field; + cell: NumberCellType; + placeholder?: string; +} + +function NumberCell({ field, cell, placeholder }: Props) { + const cellRef = useRef(null); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const content = useMemo(() => { + if (typeof cell.data === 'string' && cell.data) { + return cell.data; + } + + return
{placeholder}
; + }, [cell, placeholder]); + + const handleClick = useCallback(() => { + setValue(cell.data); + setEditing(true); + }, [cell, setEditing, setValue]); + + return ( + <> + +
{content}
+
+ + {editing && ( + + )} + + + ); +} + +export default NumberCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx new file mode 100644 index 0000000000..a951ddd9e4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/SelectCell.tsx @@ -0,0 +1,108 @@ +import { FC, useCallback, useMemo, useState, Suspense, lazy } from 'react'; +import { MenuProps } from '@mui/material'; +import { SelectField, SelectCell as SelectCellType, SelectTypeOption } from '$app/application/database'; +import { Tag } from '../field_types/select/Tag'; +import { useTypeOption } from '$app/components/database'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import Popover from '@mui/material/Popover'; + +const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'center', +}; + +const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'center', +}; +const SelectCellActions = lazy( + () => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions') +); +const menuProps: Partial = { + classes: { + list: 'py-5', + }, +}; + +export const SelectCell: FC<{ + field: SelectField; + cell: SelectCellType; + placeholder?: string; +}> = ({ field, cell, placeholder }) => { + const [anchorEl, setAnchorEl] = useState(null); + const selectedIds = useMemo(() => cell.data?.selectedOptionIds ?? [], [cell]); + const open = Boolean(anchorEl); + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const typeOption = useTypeOption(field.id); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected + .map((id) => typeOption.options?.find((option) => option.id === id)) + .map((option) => option && ), + [typeOption] + ); + + const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 369, + initialPaperHeight: 400, + anchorEl, + initialAnchorOrigin, + initialTransformOrigin, + open, + }); + + return ( +
+
{ + setAnchorEl(e.currentTarget); + }} + className={'flex h-full w-full cursor-pointer items-center gap-2 overflow-x-hidden px-2 py-1'} + > + {selectedIds.length === 0 ? ( +
{placeholder}
+ ) : ( + renderSelectedOptions(selectedIds) + )} +
+ + {open ? ( + { + const isInput = (e.target as Element).closest('input'); + + if (isInput) return; + + e.preventDefault(); + e.stopPropagation(); + }} + > + + + ) : null} + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx new file mode 100644 index 0000000000..38927d744b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TextCell.tsx @@ -0,0 +1,59 @@ +import { FC, FormEventHandler, Suspense, lazy, useCallback, useRef, useMemo } from 'react'; +import { TextCell as TextCellType } from '$app/application/database'; +import { CellText } from '../../_shared'; +import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; + +const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput')); + +interface TextCellProps { + cell: TextCellType; + placeholder?: string; +} +export const TextCell: FC = ({ placeholder, cell }) => { + const cellRef = useRef(null); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const handleClose = () => { + if (!cell) return; + updateCell(); + }; + + const handleClick = useCallback(() => { + if (!cell) return; + setValue(cell.data); + setEditing(true); + }, [cell, setEditing, setValue]); + + const handleInput = useCallback>( + (event) => { + setValue((event.target as HTMLTextAreaElement).value); + }, + [setValue] + ); + + const content = useMemo(() => { + if (cell && typeof cell.data === 'string' && cell.data) { + return cell.data; + } + + return
{placeholder}
; + }, [cell, placeholder]); + + return ( + <> + + {content} + + + {editing && ( + + )} + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx new file mode 100644 index 0000000000..5889a13915 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/TimestampCell.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { CreatedTimeField, LastEditedTimeField, TimeStampCell } from '$app/application/database'; + +interface Props { + field: LastEditedTimeField | CreatedTimeField; + cell: TimeStampCell; +} + +function TimestampCell({ cell }: Props) { + return
{cell.data.dataTime}
; +} + +export default TimestampCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx new file mode 100644 index 0000000000..592850ada1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/URLCell.tsx @@ -0,0 +1,81 @@ +import React, { FormEventHandler, lazy, Suspense, useCallback, useMemo, useRef } from 'react'; +import { useInputCell } from '$app/components/database/components/cell/Cell.hooks'; +import { Field, UrlCell as URLCellType } from '$app/application/database'; +import { CellText } from '$app/components/database/_shared'; +import { openUrl } from '$app/utils/open_url'; + +const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput')); + +interface Props { + field: Field; + cell: URLCellType; + placeholder?: string; +} + +function UrlCell({ field, cell, placeholder }: Props) { + const cellRef = useRef(null); + const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell); + const handleClick = useCallback(() => { + setValue(cell.data); + setEditing(true); + }, [cell, setEditing, setValue]); + + const handleClose = () => { + updateCell(); + }; + + const handleInput = useCallback>( + (event) => { + setValue((event.target as HTMLTextAreaElement).value); + }, + [setValue] + ); + + const content = useMemo(() => { + if (cell.data) { + return ( + { + e.stopPropagation(); + openUrl(cell.data); + }} + target={'_blank'} + className={'cursor-pointer text-content-blue-400 underline'} + > + {cell.data} + + ); + } + + return
{placeholder}
; + }, [cell, placeholder]); + + return ( + <> + + {content} + + + {editing && ( + + )} + + + ); +} + +export default UrlCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/index.ts new file mode 100644 index 0000000000..2440976340 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/cell/index.ts @@ -0,0 +1 @@ +export * from './Cell'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx new file mode 100644 index 0000000000..0fe9fb6d5b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseCollection.tsx @@ -0,0 +1,18 @@ +import { Sorts } from '../sort'; +import Filters from '../filter/Filters'; +import React from 'react'; + +interface Props { + open: boolean; +} + +export const DatabaseCollection = ({ open }: Props) => { + return ( +
+
+ + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx new file mode 100644 index 0000000000..ea1378eab8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/DatabaseSettings.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; +import { TextButton } from '$app/components/database/components/tab_bar/TextButton'; +import { useTranslation } from 'react-i18next'; + +import SortSettings from '$app/components/database/components/database_settings/SortSettings'; +import SettingsMenu from '$app/components/database/components/database_settings/SettingsMenu'; +import FilterSettings from '$app/components/database/components/database_settings/FilterSettings'; + +interface Props { + onToggleCollection: (forceOpen?: boolean) => void; +} + +function DatabaseSettings(props: Props) { + const { t } = useTranslation(); + const [settingAnchorEl, setSettingAnchorEl] = useState(null); + + return ( +
+ + + setSettingAnchorEl(e.currentTarget)}> + {t('settings.title')} + + setSettingAnchorEl(null)} + /> +
+ ); +} + +export default DatabaseSettings; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx new file mode 100644 index 0000000000..d0b89208d0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/FilterSettings.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { TextButton } from '$app/components/database/components/tab_bar/TextButton'; +import { useTranslation } from 'react-i18next'; +import { useFiltersCount } from '$app/components/database'; +import FilterFieldsMenu from '$app/components/database/components/filter/FilterFieldsMenu'; + +function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen?: boolean) => void }) { + const { t } = useTranslation(); + const filtersCount = useFiltersCount(); + const highlight = filtersCount > 0; + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const open = Boolean(filterAnchorEl); + + const handleClick = (e: React.MouseEvent) => { + if (highlight) { + onToggleCollection(); + return; + } + + setFilterAnchorEl(e.currentTarget); + }; + + return ( + <> + + {t('grid.settings.filter')} + + onToggleCollection(true)} + open={open} + anchorEl={filterAnchorEl} + onClose={() => setFilterAnchorEl(null)} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + /> + + ); +} + +export default FilterSettings; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx new file mode 100644 index 0000000000..af2bbce218 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/Properties.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from 'react'; +import { useDatabase } from '$app/components/database'; +import { Field as FieldType, fieldService } from '$app/application/database'; +import { Property } from '$app/components/database/components/property'; +import { FieldVisibility } from '@/services/backend'; +import { ReactComponent as EyeOpen } from '$app/assets/eye_open.svg'; +import { ReactComponent as EyeClosed } from '$app/assets/eye_close.svg'; +import { IconButton, MenuItem } from '@mui/material'; +import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'; +import { useViewId } from '$app/hooks'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; + +interface PropertiesProps { + onItemClick: (field: FieldType) => void; +} +function Properties({ onItemClick }: PropertiesProps) { + const { fields } = useDatabase(); + const [state, setState] = useState(fields as FieldType[]); + const viewId = useViewId(); + const [menuPropertyId, setMenuPropertyId] = useState(); + + useEffect(() => { + setState(fields as FieldType[]); + }, [fields]); + + const handleOnDragEnd = async (result: DropResult) => { + const { destination, draggableId, source } = result; + const oldIndex = source.index; + const newIndex = destination?.index; + + if (oldIndex === newIndex) { + return; + } + + if (newIndex === undefined || newIndex === null) { + return; + } + + const newId = fields[newIndex ?? 0].id; + + const newProperties = fieldService.reorderFields(fields as FieldType[], oldIndex, newIndex ?? 0); + + setState(newProperties); + + await fieldService.moveField(viewId, draggableId, newId ?? ''); + }; + + return ( + + + {(dropProvided) => ( +
+ {state.map((field, index) => ( + + {(provided) => { + return ( + { + setMenuPropertyId(field.id); + }} + key={field.id} + > + + + +
+ { + setMenuPropertyId(undefined); + }} + menuOpened={menuPropertyId === field.id} + field={field} + /> +
+ + { + e.stopPropagation(); + onItemClick(field); + }} + className={'ml-2'} + > + {field.visibility !== FieldVisibility.AlwaysHidden ? : } + +
+ ); + }} +
+ ))} + {dropProvided.placeholder} +
+ )} +
+
+ ); +} + +export default Properties; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SettingsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SettingsMenu.tsx new file mode 100644 index 0000000000..c6a9d244f0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SettingsMenu.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { Menu, MenuProps, Popover } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import Properties from '$app/components/database/components/database_settings/Properties'; +import { Field } from '$app/application/database'; +import { FieldVisibility } from '@/services/backend'; +import { updateFieldSetting } from '$app/application/database/field/field_service'; +import { useViewId } from '$app/hooks'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +type SettingsMenuProps = MenuProps; + +function SettingsMenu(props: SettingsMenuProps) { + const viewId = useViewId(); + const ref = useRef(null); + const { t } = useTranslation(); + const [propertiesAnchorElPosition, setPropertiesAnchorElPosition] = useState< + | undefined + | { + top: number; + left: number; + } + >(undefined); + + const openProperties = Boolean(propertiesAnchorElPosition); + + const togglePropertyVisibility = async (field: Field) => { + let visibility = field.visibility; + + if (visibility === FieldVisibility.AlwaysHidden) { + visibility = FieldVisibility.AlwaysShown; + } else { + visibility = FieldVisibility.AlwaysHidden; + } + + await updateFieldSetting(viewId, field.id, { + visibility, + }); + }; + + const options = useMemo(() => { + return [{ key: 'properties', content:
{t('grid.settings.properties')}
}]; + }, [t]); + + const onConfirm = useCallback( + (optionKey: string) => { + if (optionKey === 'properties') { + const target = ref.current?.querySelector(`[data-key=${optionKey}]`) as HTMLElement; + const rect = target.getBoundingClientRect(); + + setPropertiesAnchorElPosition({ + top: rect.top, + left: rect.left + rect.width, + }); + props.onClose?.({}, 'backdropClick'); + } + }, + [props] + ); + + return ( + <> + + { + props.onClose?.({}, 'escapeKeyDown'); + }} + options={options} + /> + + { + setPropertiesAnchorElPosition(undefined); + }} + anchorReference={'anchorPosition'} + anchorPosition={propertiesAnchorElPosition} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + > + + + + ); +} + +export default SettingsMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx new file mode 100644 index 0000000000..7f978120df --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDatabase } from '$app/components/database'; +import { TextButton } from '$app/components/database/components/tab_bar/TextButton'; +import SortFieldsMenu from '$app/components/database/components/sort/SortFieldsMenu'; + +interface Props { + onToggleCollection: (forceOpen?: boolean) => void; +} + +function SortSettings({ onToggleCollection }: Props) { + const { t } = useTranslation(); + const { sorts } = useDatabase(); + + const highlight = sorts && sorts.length > 0; + + const [sortAnchorEl, setSortAnchorEl] = React.useState(null); + const open = Boolean(sortAnchorEl); + const handleClick = (event: React.MouseEvent) => { + if (highlight) { + onToggleCollection(); + return; + } + + setSortAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setSortAnchorEl(null); + }; + + return ( + <> + + {t('grid.settings.sort')} + + onToggleCollection(true)} + open={open} + anchorEl={sortAnchorEl} + onClose={handleClose} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + /> + + ); +} + +export default SortSettings; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts new file mode 100644 index 0000000000..efb89af437 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/index.ts @@ -0,0 +1 @@ +export * from './DatabaseCollection'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx new file mode 100644 index 0000000000..13f29a7dfc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import RecordDocument from '$app/components/database/components/edit_record/RecordDocument'; +import RecordHeader from '$app/components/database/components/edit_record/RecordHeader'; +import { Page } from '$app_reducers/pages/slice'; +import { ErrorCode, ViewLayoutPB } from '@/services/backend'; +import { Log } from '$app/utils/log'; +import { useDatabase } from '$app/components/database'; +import { createOrphanPage, getPage } from '$app/application/folder/page.service'; + +interface Props { + rowId: string; +} + +function EditRecord({ rowId }: Props) { + const { rowMetas } = useDatabase(); + const row = useMemo(() => { + return rowMetas.find((row) => row.id === rowId); + }, [rowMetas, rowId]); + const [page, setPage] = useState(null); + const id = row?.documentId; + + const loadPage = useCallback(async () => { + if (!id) return; + + try { + const page = await getPage(id); + + setPage(page); + } catch (e) { + // Record not found + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + if (e.code === ErrorCode.RecordNotFound) { + try { + const page = await createOrphanPage({ + view_id: id, + name: '', + layout: ViewLayoutPB.Document, + }); + + setPage(page); + } catch (e) { + Log.error(e); + } + } + } + }, [id]); + + useEffect(() => { + void loadPage(); + }, [loadPage]); + + if (!id || !page) return null; + + return ( + <> + + + + ); +} + +export default EditRecord; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx new file mode 100644 index 0000000000..7056cd353d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import { DialogProps, IconButton, Portal } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import { ReactComponent as DetailsIcon } from '$app/assets/details.svg'; +import RecordActions from '$app/components/database/components/edit_record/RecordActions'; +import EditRecord from '$app/components/database/components/edit_record/EditRecord'; +import { AFScroller } from '$app/components/_shared/scroller'; + +interface Props extends DialogProps { + rowId: string; +} + +function ExpandRecordModal({ open, onClose, rowId }: Props) { + const [detailAnchorEl, setDetailAnchorEl] = useState(null); + + return ( + + e.stopPropagation()} + open={open} + onClose={onClose} + PaperProps={{ + className: 'h-[calc(100%-144px)] w-[80%] max-w-[960px] overflow-visible', + }} + > + + + + { + setDetailAnchorEl(e.currentTarget); + }} + > + + + + { + onClose?.({}, 'escapeKeyDown'); + }} + onClose={() => setDetailAnchorEl(null)} + /> + + ); +} + +export default ExpandRecordModal; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx new file mode 100644 index 0000000000..412c1a953e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordActions.tsx @@ -0,0 +1,63 @@ +import React, { useCallback } from 'react'; +import { Icon, Menu, MenuProps } from '@mui/material'; +import { ReactComponent as DelSvg } from '$app/assets/delete.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { useTranslation } from 'react-i18next'; +import { rowService } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import MenuItem from '@mui/material/MenuItem'; + +interface Props extends MenuProps { + rowId: string; + onEscape?: () => void; + onClose?: () => void; +} +function RecordActions({ anchorEl, open, onEscape, onClose, rowId }: Props) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleDelRow = useCallback(() => { + void rowService.deleteRow(viewId, rowId); + onEscape?.(); + }, [viewId, rowId, onEscape]); + + const handleDuplicateRow = useCallback(() => { + void rowService.duplicateRow(viewId, rowId); + onEscape?.(); + }, [viewId, rowId, onEscape]); + + const menuOptions = [ + { + label: t('grid.row.duplicate'), + icon: , + onClick: handleDuplicateRow, + }, + + { + label: t('grid.row.delete'), + icon: , + onClick: handleDelRow, + divider: true, + }, + ]; + + return ( + + {menuOptions.map((option) => ( + { + option.onClick(); + onClose?.(); + onEscape?.(); + }} + > + {option.icon} + {option.label} + + ))} + + ); +} + +export default RecordActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordDocument.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordDocument.tsx new file mode 100644 index 0000000000..653f3c5944 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordDocument.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Editor from '$app/components/editor/Editor'; + +interface Props { + documentId: string; +} + +function RecordDocument({ documentId }: Props) { + return ; +} + +export default RecordDocument; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx new file mode 100644 index 0000000000..d2381ec165 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordHeader.tsx @@ -0,0 +1,39 @@ +import React, { useEffect, useRef } from 'react'; +import RecordTitle from '$app/components/database/components/edit_record/RecordTitle'; +import RecordProperties from '$app/components/database/components/edit_record/record_properties/RecordProperties'; +import { Divider } from '@mui/material'; +import { RowMeta } from '$app/application/database'; +import { Page } from '$app_reducers/pages/slice'; + +interface Props { + page: Page | null; + row: RowMeta; +} +function RecordHeader({ page, row }: Props) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + + if (!el) return; + + const preventSelectionTrigger = (e: MouseEvent) => { + e.stopPropagation(); + }; + + el.addEventListener('mousedown', preventSelectionTrigger); + return () => { + el.removeEventListener('mousedown', preventSelectionTrigger); + }; + }, []); + + return ( +
+ + + +
+ ); +} + +export default RecordHeader; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx new file mode 100644 index 0000000000..c2f195aee2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/RecordTitle.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useMemo } from 'react'; +import { Page, PageIcon } from '$app_reducers/pages/slice'; +import ViewTitle from '$app/components/_shared/view_title/ViewTitle'; +import { ViewIconTypePB } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import { updateRowMeta } from '$app/application/database/row/row_service'; +import { cellService, Field, RowMeta, TextCell } from '$app/application/database'; +import { useDatabase } from '$app/components/database'; +import { useCell } from '$app/components/database/components/cell/Cell.hooks'; + +interface Props { + page: Page | null; + row: RowMeta; +} + +function RecordTitle({ row, page }: Props) { + const { fields } = useDatabase(); + const field = useMemo(() => { + return fields.find((field) => field.isPrimary) as Field; + }, [fields]); + const rowId = row.id; + const cell = useCell(rowId, field) as TextCell; + const title = cell.data; + + const viewId = useViewId(); + + const onTitleChange = useCallback( + async (title: string) => { + try { + await cellService.updateCell(viewId, rowId, field.id, title); + } catch (e) { + // toast.error('Failed to update title'); + } + }, + [field.id, rowId, viewId] + ); + + const onUpdateIcon = useCallback( + async (icon: PageIcon) => { + try { + await updateRowMeta(viewId, rowId, { iconUrl: icon.value }); + } catch (e) { + // toast.error('Failed to update icon'); + } + }, + [rowId, viewId] + ); + + return ( +
+ {page && ( + + )} +
+ ); +} + +export default React.memo(RecordTitle); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx new file mode 100644 index 0000000000..279ee13f68 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/Property.tsx @@ -0,0 +1,55 @@ +import React, { HTMLAttributes } from 'react'; +import PropertyName from '$app/components/database/components/edit_record/record_properties/PropertyName'; +import PropertyValue from '$app/components/database/components/edit_record/record_properties/PropertyValue'; +import { Field } from '$app/application/database'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { useTranslation } from 'react-i18next'; + +interface Props extends HTMLAttributes { + field: Field; + rowId: string; + ishovered: boolean; + onHover: (id: string | null) => void; + menuOpened?: boolean; + onOpenMenu?: () => void; + onCloseMenu?: () => void; +} + +function Property( + { field, rowId, ishovered, onHover, menuOpened, onCloseMenu, onOpenMenu, ...props }: Props, + ref: React.ForwardedRef +) { + const { t } = useTranslation(); + + return ( + <> +
{ + onHover(field.id); + }} + onMouseLeave={() => { + onHover(null); + }} + className={'relative flex items-start gap-6 rounded hover:bg-content-blue-50'} + key={field.id} + {...props} + > + + + {ishovered && ( +
+ + + + + +
+ )} +
+ + ); +} + +export default React.forwardRef(Property); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx new file mode 100644 index 0000000000..138c7543fd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyList.tsx @@ -0,0 +1,55 @@ +import React, { HTMLAttributes, useState } from 'react'; +import { Field } from '$app/application/database'; +import Property from '$app/components/database/components/edit_record/record_properties/Property'; +import { Draggable } from 'react-beautiful-dnd'; + +interface Props extends HTMLAttributes { + properties: Field[]; + rowId: string; + placeholderNode?: React.ReactNode; + openMenuPropertyId?: string; + setOpenMenuPropertyId?: (id?: string) => void; +} + +function PropertyList( + { properties, rowId, placeholderNode, openMenuPropertyId, setOpenMenuPropertyId, ...props }: Props, + ref: React.ForwardedRef +) { + const [hoverId, setHoverId] = useState(null); + + return ( +
+ {properties.map((field, index) => { + return ( + + {(provided) => { + return ( + { + setOpenMenuPropertyId?.(field.id); + }} + onCloseMenu={() => { + if (openMenuPropertyId === field.id) { + setOpenMenuPropertyId?.(undefined); + } + }} + /> + ); + }} + + ); + })} + {placeholderNode} +
+ ); +} + +export default React.forwardRef(PropertyList); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx new file mode 100644 index 0000000000..e7de3f1fb0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyName.tsx @@ -0,0 +1,32 @@ +import React, { useRef } from 'react'; +import { Property } from '$app/components/database/components/property'; +import { Field as FieldType } from '$app/application/database'; + +interface Props { + field: FieldType; + menuOpened?: boolean; + onOpenMenu?: () => void; + onCloseMenu?: () => void; +} +function PropertyName({ field, menuOpened = false, onOpenMenu, onCloseMenu }: Props) { + const ref = useRef(null); + + return ( + <> +
{ + e.stopPropagation(); + e.preventDefault(); + onOpenMenu?.(); + }} + className={'flex min-h-[36px] w-[200px] cursor-pointer items-center'} + onClick={onOpenMenu} + > + +
+ + ); +} + +export default PropertyName; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx new file mode 100644 index 0000000000..4bb33e7f05 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/PropertyValue.tsx @@ -0,0 +1,33 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Cell } from '$app/components/database/components'; +import { Field } from '$app/application/database'; +import { useTranslation } from 'react-i18next'; + +function PropertyValue(props: { rowId: string; field: Field }) { + const { t } = useTranslation(); + const ref = useRef(null); + const [width, setWidth] = useState(props.field.width); + + useEffect(() => { + const el = ref.current; + + if (!el) return; + const width = el.getBoundingClientRect().width; + + setWidth(width); + }, []); + return ( +
+ +
+ ); +} + +export default React.memo(PropertyValue); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx new file mode 100644 index 0000000000..16fc122615 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/RecordProperties.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Field, fieldService, RowMeta } from '$app/application/database'; +import { useDatabase } from '$app/components/database'; +import { FieldVisibility } from '@/services/backend'; + +import PropertyList from '$app/components/database/components/edit_record/record_properties/PropertyList'; +import NewProperty from '$app/components/database/components/property/NewProperty'; +import { useViewId } from '$app/hooks'; +import { DragDropContext, Droppable, DropResult, OnDragEndResponder } from 'react-beautiful-dnd'; +import SwitchPropertiesVisible from '$app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible'; + +interface Props { + row: RowMeta; +} + +function RecordProperties({ row }: Props) { + const viewId = useViewId(); + const { fields } = useDatabase(); + const fieldId = useMemo(() => { + return fields.find((field) => field.isPrimary)?.id; + }, [fields]); + const rowId = row.id; + const [openMenuPropertyId, setOpenMenuPropertyId] = useState(undefined); + const [showHiddenFields, setShowHiddenFields] = useState(false); + + const properties = useMemo(() => { + return fields.filter((field) => { + // exclude the current field, because it's already displayed in the title + // filter out hidden fields if the user doesn't want to see them + return field.id !== fieldId && (showHiddenFields || field.visibility !== FieldVisibility.AlwaysHidden); + }); + }, [fieldId, fields, showHiddenFields]); + + const hiddenFieldsCount = useMemo(() => { + return fields.filter((field) => { + return field.visibility === FieldVisibility.AlwaysHidden; + }).length; + }, [fields]); + + const [state, setState] = useState(properties); + + useEffect(() => { + setState(properties); + }, [properties]); + + // move the field in the state + const handleOnDragEnd: OnDragEndResponder = useCallback( + async (result: DropResult) => { + const { destination, draggableId, source } = result; + const newIndex = destination?.index; + const oldIndex = source.index; + + if (newIndex === undefined || newIndex === null) { + return; + } + + const newId = properties[newIndex ?? 0].id; + + // reorder the properties synchronously to avoid flickering + const newProperties = fieldService.reorderFields(properties, oldIndex, newIndex ?? 0); + + setState(newProperties); + + await fieldService.moveField(viewId, draggableId, newId); + }, + [properties, viewId] + ); + + return ( +
+ + + {(dropProvided) => ( + + )} + + + + + +
+ ); +} + +export default RecordProperties; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx new file mode 100644 index 0000000000..f8937bbf21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/record_properties/SwitchPropertiesVisible.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as EyeClosedSvg } from '$app/assets/eye_close.svg'; +import { ReactComponent as EyeOpenSvg } from '$app/assets/eye_open.svg'; + +function SwitchPropertiesVisible({ + hiddenFieldsCount, + showHiddenFields, + setShowHiddenFields, +}: { + hiddenFieldsCount: number; + showHiddenFields: boolean; + setShowHiddenFields: (showHiddenFields: boolean) => void; +}) { + const { t } = useTranslation(); + + return hiddenFieldsCount > 0 ? ( + + ) : null; +} + +export default SwitchPropertiesVisible; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx new file mode 100644 index 0000000000..027936d280 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/AddNewOption.tsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react'; +import { updateChecklistCell } from '$app/application/database/cell/cell_service'; +import { useViewId } from '$app/hooks'; +import { Button } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function AddNewOption({ + rowId, + fieldId, + onClose, + onFocus, +}: { + rowId: string; + fieldId: string; + onClose: () => void; + onFocus: () => void; +}) { + const { t } = useTranslation(); + const [value, setValue] = useState(''); + const viewId = useViewId(); + const createOption = async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + insertOptions: [value], + }); + setValue(''); + }; + + return ( +
+ { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void createOption(); + return; + } + + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + onClose(); + return; + } + }} + value={value} + spellCheck={false} + onChange={(e) => { + setValue(e.target.value); + }} + /> + +
+ ); +} + +export default AddNewOption; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx new file mode 100644 index 0000000000..61583c4746 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistCellActions.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { Divider } from '@mui/material'; +import { ChecklistCell as ChecklistCellType } from '$app/application/database'; +import ChecklistItem from '$app/components/database/components/field_types/checklist/ChecklistItem'; +import AddNewOption from '$app/components/database/components/field_types/checklist/AddNewOption'; +import LinearProgressWithLabel from '$app/components/database/_shared/LinearProgressWithLabel'; + +function ChecklistCellActions({ + cell, + maxHeight, + maxWidth, + ...props +}: PopoverProps & { + cell: ChecklistCellType; + maxWidth?: number; + maxHeight?: number; +}) { + const { fieldId, rowId } = cell; + const { percentage, selectedOptions = [], options = [] } = cell.data; + + const [focusedId, setFocusedId] = useState(null); + + return ( + +
+ {options.length > 0 && ( + <> +
+ +
+
+ {options?.map((option) => { + return ( + setFocusedId(option.id)} + onClose={() => props.onClose?.({}, 'escapeKeyDown')} + checked={selectedOptions?.includes(option.id) || false} + /> + ); + })} +
+ + + + )} + + { + setFocusedId(null); + }} + onClose={() => props.onClose?.({}, 'escapeKeyDown')} + fieldId={fieldId} + rowId={rowId} + /> +
+
+ ); +} + +export default ChecklistCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx new file mode 100644 index 0000000000..5c6a55fa60 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/ChecklistItem.tsx @@ -0,0 +1,117 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { SelectOption } from '$app/application/database'; +import { IconButton } from '@mui/material'; +import { updateChecklistCell } from '$app/application/database/cell/cell_service'; +import { useViewId } from '$app/hooks'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; +import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; +import isHotkey from 'is-hotkey'; +import debounce from 'lodash-es/debounce'; +import { useTranslation } from 'react-i18next'; + +const DELAY_CHANGE = 200; + +function ChecklistItem({ + checked, + option, + rowId, + fieldId, + onClose, + isSelected, + onFocus, +}: { + checked: boolean; + option: SelectOption; + rowId: string; + fieldId: string; + onClose: () => void; + isSelected: boolean; + onFocus: () => void; +}) { + const inputRef = React.useRef(null); + const { t } = useTranslation(); + const [value, setValue] = useState(option.name); + const viewId = useViewId(); + const updateText = useCallback(async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + updateOptions: [ + { + ...option, + name: value, + }, + ], + }); + }, [fieldId, option, rowId, value, viewId]); + + const onCheckedChange = useMemo(() => { + return debounce( + () => + updateChecklistCell(viewId, rowId, fieldId, { + selectedOptionIds: [option.id], + }), + DELAY_CHANGE + ); + }, [fieldId, option.id, rowId, viewId]); + + const deleteOption = useCallback(async () => { + await updateChecklistCell(viewId, rowId, fieldId, { + deleteOptionIds: [option.id], + }); + }, [fieldId, option.id, rowId, viewId]); + + return ( +
+
+ {checked ? : } +
+ + { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + void updateText(); + onClose(); + return; + } + + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void updateText(); + if (isHotkey('mod+enter', e)) { + void onCheckedChange(); + } + + return; + } + }} + spellCheck={false} + onChange={(e) => { + setValue(e.target.value); + }} + /> +
+ + + +
+
+ ); +} + +export default ChecklistItem; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..e8b9c95a44 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/checklist/LinearProgressWithLabel.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +export function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) { + return ( + + + + + + {`${Math.round(props.value * 100)}%`} + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx new file mode 100644 index 0000000000..4a36498bd2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/CustomCalendar.tsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import DatePicker, { ReactDatePickerCustomHeaderProps } from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import dayjs from 'dayjs'; +import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg'; +import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg'; +import { IconButton } from '@mui/material'; +import './calendar.scss'; + +function CustomCalendar({ + handleChange, + isRange, + timestamp, + endTimestamp, +}: { + handleChange: (params: { date?: number; endDate?: number }) => void; + isRange: boolean; + timestamp?: number; + endTimestamp?: number; +}) { + const [startDate, setStartDate] = useState(() => { + if (!timestamp) return null; + return new Date(timestamp * 1000); + }); + const [endDate, setEndDate] = useState(() => { + if (!endTimestamp) return null; + return new Date(endTimestamp * 1000); + }); + + useEffect(() => { + if (!isRange || !endTimestamp) return; + setEndDate(new Date(endTimestamp * 1000)); + }, [isRange, endTimestamp]); + + useEffect(() => { + if (!timestamp) return; + setStartDate(new Date(timestamp * 1000)); + }, [timestamp]); + + return ( +
+ { + return ( +
+
+ {dayjs(props.date).format('MMMM YYYY')} +
+ +
+ + + + + + +
+
+ ); + }} + selected={startDate} + onChange={(dates) => { + if (!dates) return; + if (isRange && Array.isArray(dates)) { + let start = dates[0] as Date; + let end = dates[1] as Date; + + if (!end && start && startDate && endDate) { + const currentTime = start.getTime(); + const startTimeStamp = startDate.getTime(); + const endTimeStamp = endDate.getTime(); + const isGreaterThanStart = currentTime > startTimeStamp; + const isGreaterThanEnd = currentTime > endTimeStamp; + const isLessThanStart = currentTime < startTimeStamp; + const isLessThanEnd = currentTime < endTimeStamp; + const isEqualsStart = currentTime === startTimeStamp; + const isEqualsEnd = currentTime === endTimeStamp; + + if ((isGreaterThanStart && isLessThanEnd) || isGreaterThanEnd) { + end = start; + start = startDate; + } else if (isEqualsStart || isEqualsEnd) { + end = start; + } else if (isLessThanStart) { + end = endDate; + } + } + + setStartDate(start); + setEndDate(end); + if (!start || !end) return; + handleChange({ + date: start.getTime() / 1000, + endDate: end.getTime() / 1000, + }); + } else { + const date = dates as Date; + + setStartDate(date); + handleChange({ + date: date.getTime() / 1000, + }); + } + }} + startDate={isRange ? startDate : null} + endDate={isRange ? endDate : null} + selectsRange={isRange} + inline + /> +
+ ); +} + +export default CustomCalendar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx new file mode 100644 index 0000000000..fd5ba57889 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateFormat.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MenuItem, Menu } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; + +import { DateFormatPB } from '@/services/backend'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +interface Props { + value: DateFormatPB; + onChange: (value: DateFormatPB) => void; +} + +function DateFormat({ value, onChange }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const renderOptionContent = useCallback( + (option: DateFormatPB, title: string) => { + return ( +
+
{title}
+ {value === option && } +
+ ); + }, + [value] + ); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: DateFormatPB.Friendly, + content: renderOptionContent(DateFormatPB.Friendly, t('grid.field.dateFormatFriendly')), + }, + { + key: DateFormatPB.ISO, + content: renderOptionContent(DateFormatPB.ISO, t('grid.field.dateFormatISO')), + }, + { + key: DateFormatPB.US, + content: renderOptionContent(DateFormatPB.US, t('grid.field.dateFormatUS')), + }, + { + key: DateFormatPB.Local, + content: renderOptionContent(DateFormatPB.Local, t('grid.field.dateFormatLocal')), + }, + { + key: DateFormatPB.DayMonthYear, + content: renderOptionContent(DateFormatPB.DayMonthYear, t('grid.field.dateFormatDayMonthYear')), + }, + ]; + }, [renderOptionContent, t]); + + const handleClick = (option: DateFormatPB) => { + onChange(option); + setOpen(false); + }; + + return ( + <> + setOpen(true)} + > + {t('grid.field.dateFormat')} + + + setOpen(false)} + > + { + setOpen(false); + }} + disableFocus={true} + options={options} + onConfirm={handleClick} + /> + + + ); +} + +export default DateFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx new file mode 100644 index 0000000000..78e3129d4f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeCellActions.tsx @@ -0,0 +1,197 @@ +import React, { useCallback, useMemo } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { DateTimeCell, DateTimeField, DateTimeTypeOption } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { updateDateCell } from '$app/application/database/cell/cell_service'; +import { Divider, MenuItem, MenuList } from '@mui/material'; +import dayjs from 'dayjs'; +import RangeSwitch from '$app/components/database/components/field_types/date/RangeSwitch'; +import CustomCalendar from '$app/components/database/components/field_types/date/CustomCalendar'; +import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch'; +import DateTimeFormatSelect from '$app/components/database/components/field_types/date/DateTimeFormatSelect'; +import DateTimeSet from '$app/components/database/components/field_types/date/DateTimeSet'; +import { useTypeOption } from '$app/components/database'; +import { getDateFormat, getTimeFormat } from '$app/components/database/components/field_types/date/utils'; +import { notify } from '$app/components/_shared/notify'; + +function DateTimeCellActions({ + cell, + field, + maxWidth, + maxHeight, + ...props +}: PopoverProps & { + field: DateTimeField; + cell: DateTimeCell; + maxWidth?: number; + maxHeight?: number; +}) { + const typeOption = useTypeOption(field.id); + + const timeFormat = useMemo(() => { + return getTimeFormat(typeOption.timeFormat); + }, [typeOption.timeFormat]); + + const dateFormat = useMemo(() => { + return getDateFormat(typeOption.dateFormat); + }, [typeOption.dateFormat]); + + const { includeTime } = cell.data; + + const timestamp = useMemo(() => cell.data.timestamp || undefined, [cell.data.timestamp]); + const endTimestamp = useMemo(() => cell.data.endTimestamp || undefined, [cell.data.endTimestamp]); + const time = useMemo(() => cell.data.time || undefined, [cell.data.time]); + const endTime = useMemo(() => cell.data.endTime || undefined, [cell.data.endTime]); + + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleChange = useCallback( + async (params: { + includeTime?: boolean; + date?: number; + endDate?: number; + time?: string; + endTime?: string; + isRange?: boolean; + clearFlag?: boolean; + }) => { + try { + const isRange = params.isRange ?? cell.data.isRange; + + const data = { + date: params.date ?? timestamp, + endDate: isRange ? params.endDate ?? endTimestamp : undefined, + time: params.time ?? time, + endTime: isRange ? params.endTime ?? endTime : undefined, + includeTime: params.includeTime ?? includeTime, + isRange, + clearFlag: params.clearFlag, + }; + + // if isRange and date is greater than endDate, swap date and endDate + if ( + data.isRange && + data.date && + data.endDate && + dayjs(dayjs.unix(data.date).format('YYYY/MM/DD ') + data.time).unix() > + dayjs(dayjs.unix(data.endDate).format('YYYY/MM/DD ') + data.endTime).unix() + ) { + if (params.date || params.time) { + data.endDate = data.date; + data.endTime = data.time; + } + + if (params.endDate || params.endTime) { + data.date = data.endDate; + data.time = data.endTime; + } + } + + await updateDateCell(viewId, cell.rowId, cell.fieldId, data); + } catch (e) { + notify.error(String(e)); + } + }, + [cell, endTime, endTimestamp, includeTime, time, timestamp, viewId] + ); + + const isRange = cell.data.isRange || false; + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + > +
+ + + + + +
+ { + void handleChange({ + isRange: val, + // reset endTime when isRange is changed + endTime: time, + endDate: timestamp, + }); + }} + checked={isRange} + /> + { + void handleChange({ + includeTime: val, + // reset time when includeTime is changed + time: val ? dayjs().format(timeFormat) : undefined, + endTime: val && isRange ? dayjs().format(timeFormat) : undefined, + }); + }} + checked={includeTime} + /> +
+ + + + + + { + await handleChange({ + isRange: false, + includeTime: false, + }); + await handleChange({ + clearFlag: true, + }); + + props.onClose?.({}, 'backdropClick'); + }} + > + {t('grid.field.clearDate')} + + +
+
+ ); +} + +export default DateTimeCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx new file mode 100644 index 0000000000..6c4b41a494 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFieldActions.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { UndeterminedDateField } from '$app/application/database'; +import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat'; +import { Divider } from '@mui/material'; + +function DateTimeFieldActions({ field }: { field: UndeterminedDateField }) { + return ( + <> +
+ +
+ + + ); +} + +export default DateTimeFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx new file mode 100644 index 0000000000..0107997c24 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormat.tsx @@ -0,0 +1,75 @@ +import React, { useCallback } from 'react'; +import DateFormat from '$app/components/database/components/field_types/date/DateFormat'; +import TimeFormat from '$app/components/database/components/field_types/date/TimeFormat'; +import { TimeStampTypeOption, UndeterminedDateField, updateTypeOption } from '$app/application/database'; +import { DateFormatPB, FieldType, TimeFormatPB } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch'; +import { useTypeOption } from '$app/components/database'; + +interface Props { + field: UndeterminedDateField; + showLabel?: boolean; +} + +function DateTimeFormat({ field, showLabel = true }: Props) { + const viewId = useViewId(); + const { t } = useTranslation(); + const showIncludeTime = field.type === FieldType.CreatedTime || field.type === FieldType.LastEditedTime; + const typeOption = useTypeOption(field.id); + const { timeFormat = TimeFormatPB.TwentyFourHour, dateFormat = DateFormatPB.Friendly, includeTime } = typeOption; + const handleChange = useCallback( + async (params: { timeFormat?: TimeFormatPB; dateFormat?: DateFormatPB; includeTime?: boolean }) => { + try { + await updateTypeOption(viewId, field.id, field.type, { + timeFormat: params.timeFormat ?? timeFormat, + dateFormat: params.dateFormat ?? dateFormat, + includeTime: params.includeTime ?? includeTime, + fieldType: field.type, + }); + } catch (e) { + // toast.error(e.message); + } + }, + [dateFormat, field.id, field.type, includeTime, timeFormat, viewId] + ); + + return ( +
+ {showLabel && ( + + {t('grid.field.format')} + + )} + + { + void handleChange({ dateFormat: val }); + }} + /> + { + void handleChange({ timeFormat: val }); + }} + /> + + {showIncludeTime && ( +
+ { + void handleChange({ includeTime: checked }); + }} + /> +
+ )} +
+ ); +} + +export default DateTimeFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx new file mode 100644 index 0000000000..f0393139b0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeFormatSelect.tsx @@ -0,0 +1,55 @@ +import React, { useState, useRef } from 'react'; +import { Menu, MenuItem } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { DateTimeField } from '$app/application/database'; +import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; + +interface Props { + field: DateTimeField; +} + +function DateTimeFormatSelect({ field }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + return ( + <> + setOpen(true)} className={'text-xs font-medium'}> +
+ {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')} +
+ +
+ { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + setOpen(false); + } + }} + onClose={() => setOpen(false)} + MenuListProps={{ + className: 'px-2', + }} + > + + + + ); +} + +export default DateTimeFormatSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx new file mode 100644 index 0000000000..82080b7d25 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeInput.tsx @@ -0,0 +1,86 @@ +import React, { useMemo } from 'react'; +import { DateField, TimeField } from '@mui/x-date-pickers-pro'; +import dayjs from 'dayjs'; +import { Divider } from '@mui/material'; +import debounce from 'lodash-es/debounce'; + +interface Props { + onChange: (params: { date?: number; time?: string }) => void; + date?: number; + time?: string; + timeFormat: string; + dateFormat: string; + includeTime?: boolean; +} + +const sx = { + '& .MuiOutlinedInput-notchedOutline': { + border: 'none', + }, + '& .MuiOutlinedInput-input': { + padding: '0', + }, +}; + +function DateTimeInput({ includeTime, dateFormat, timeFormat, ...props }: Props) { + const date = useMemo(() => { + return props.date ? dayjs.unix(props.date) : undefined; + }, [props.date]); + + const time = useMemo(() => { + return props.time ? dayjs(dayjs().format('YYYY/MM/DD ') + props.time) : undefined; + }, [props.time]); + + const debounceOnChange = useMemo(() => { + return debounce(props.onChange, 500); + }, [props.onChange]); + + return ( +
+ { + if (!date) return; + debounceOnChange({ + date: date.unix(), + }); + }} + inputProps={{ + className: 'text-[12px]', + }} + format={dateFormat} + size={'small'} + sx={sx} + className={'flex-1 pl-2'} + /> + + {includeTime && ( + <> + + { + if (!time) return; + debounceOnChange({ + time: time.format(timeFormat), + }); + }} + format={timeFormat} + size={'small'} + sx={sx} + className={'w-[70px] pl-1'} + /> + + )} +
+ ); +} + +export default DateTimeInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx new file mode 100644 index 0000000000..8e86b952d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/DateTimeSet.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { LocalizationProvider } from '@mui/x-date-pickers-pro'; +import { AdapterDayjs } from '@mui/x-date-pickers-pro/AdapterDayjs'; +import DateTimeInput from '$app/components/database/components/field_types/date/DateTimeInput'; + +interface Props { + onChange: (params: { date?: number; endDate?: number; time?: string; endTime?: string }) => void; + date?: number; + endDate?: number; + time?: string; + endTime?: string; + isRange?: boolean; + timeFormat: string; + dateFormat: string; + includeTime?: boolean; +} +function DateTimeSet({ onChange, date, endDate, time, endTime, isRange, timeFormat, dateFormat, includeTime }: Props) { + return ( +
+ + { + onChange({ + date, + time, + }); + }} + date={date} + time={time} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={includeTime} + /> + {isRange && ( + { + onChange({ + endDate: date, + endTime: time, + }); + }} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={includeTime} + /> + )} + +
+ ); +} + +export default DateTimeSet; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx new file mode 100644 index 0000000000..f40e179ae4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/IncludeTimeSwitch.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch, SwitchProps } from '@mui/material'; +import { ReactComponent as TimeSvg } from '$app/assets/database/field-type-last-edited-time.svg'; +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; + +function IncludeTimeSwitch({ + checked, + onIncludeTimeChange, + ...props +}: SwitchProps & { + onIncludeTimeChange: (checked: boolean) => void; +}) { + const { t } = useTranslation(); + const handleChange = (event: React.ChangeEvent) => { + onIncludeTimeChange(event.target.checked); + }; + + return ( +
+
+ + {t('grid.field.includeTime')} +
+ +
+ ); +} + +export default IncludeTimeSwitch; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx new file mode 100644 index 0000000000..76431af5fa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/RangeSwitch.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Switch, SwitchProps } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg'; + +function RangeSwitch({ + checked, + onIsRangeChange, + ...props +}: SwitchProps & { + onIsRangeChange: (checked: boolean) => void; +}) { + const { t } = useTranslation(); + const handleChange = (event: React.ChangeEvent) => { + onIsRangeChange(event.target.checked); + }; + + return ( +
+
+ + {t('grid.field.isRange')} +
+ +
+ ); +} + +export default RangeSwitch; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx new file mode 100644 index 0000000000..89a9ad1756 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx @@ -0,0 +1,82 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { TimeFormatPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import { Menu, MenuItem } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +interface Props { + value: TimeFormatPB; + onChange: (value: TimeFormatPB) => void; +} +function TimeFormat({ value, onChange }: Props) { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const renderOptionContent = useCallback( + (option: TimeFormatPB, title: string) => { + return ( +
+
{title}
+ {value === option && } +
+ ); + }, + [value] + ); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: TimeFormatPB.TwelveHour, + content: renderOptionContent(TimeFormatPB.TwelveHour, t('grid.field.timeFormatTwelveHour')), + }, + { + key: TimeFormatPB.TwentyFourHour, + content: renderOptionContent(TimeFormatPB.TwentyFourHour, t('grid.field.timeFormatTwentyFourHour')), + }, + ]; + }, [renderOptionContent, t]); + + const handleClick = (option: TimeFormatPB) => { + onChange(option); + setOpen(false); + }; + + return ( + <> + setOpen(true)} + > + {t('grid.field.timeFormat')} + + + setOpen(false)} + > + { + setOpen(false); + }} + disableFocus={true} + options={options} + onConfirm={handleClick} + /> + + + ); +} + +export default TimeFormat; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/calendar.scss b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/calendar.scss new file mode 100644 index 0000000000..257467ed24 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/calendar.scss @@ -0,0 +1,82 @@ + +.react-datepicker__month-container { + width: 100%; + border-radius: 0; +} +.react-datepicker__header { + border-radius: 0; + background: transparent; + border-bottom: 0; + +} +.react-datepicker__day-names { + border: none; +} +.react-datepicker__day-name { + color: var(--text-caption); +} +.react-datepicker__month { + border: none; +} + +.react-datepicker__day { + border: none; + color: var(--text-title); + border-radius: 100%; +} +.react-datepicker__day:hover { + border-radius: 100%; + background: var(--fill-default); + color: var(--content-on-fill); +} +.react-datepicker__day--outside-month { + color: var(--text-caption); +} +.react-datepicker__day--in-range { + background: var(--fill-hover); + color: var(--content-on-fill); +} + + +.react-datepicker__day--today { + border: 1px solid var(--fill-default); + color: var(--text-title); + border-radius: 100%; + background: transparent; + font-weight: 500; + +} + +.react-datepicker__day--today:hover{ + background: var(--fill-default); + color: var(--content-on-fill); +} + +.react-datepicker__day--in-selecting-range, .react-datepicker__day--today.react-datepicker__day--in-range { + background: var(--fill-hover); + color: var(--content-on-fill); + border-color: transparent; +} + +.react-datepicker__day--keyboard-selected { + background: transparent; +} + + +.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected { + &.react-datepicker__day--today { + background: var(--fill-default); + color: var(--content-on-fill); + } + background: var(--fill-default) !important; + color: var(--content-on-fill); +} + +.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover { + background: var(--fill-default); + color: var(--content-on-fill); +} + +.react-swipeable-view-container { + height: 100%; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts new file mode 100644 index 0000000000..129e84c4e7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/utils.ts @@ -0,0 +1,29 @@ +import { DateFormatPB, TimeFormatPB } from '@/services/backend'; + +export function getTimeFormat(timeFormat?: TimeFormatPB) { + switch (timeFormat) { + case TimeFormatPB.TwelveHour: + return 'h:mm A'; + case TimeFormatPB.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormatPB) { + switch (dateFormat) { + case DateFormatPB.Friendly: + return 'MMM DD, YYYY'; + case DateFormatPB.ISO: + return 'YYYY-MMM-DD'; + case DateFormatPB.US: + return 'YYYY/MMM/DD'; + case DateFormatPB.Local: + return 'MMM/DD/YYYY'; + case DateFormatPB.DayMonthYear: + return 'DD/MMM/YYYY'; + default: + return 'YYYY-MMM-DD'; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx new file mode 100644 index 0000000000..d2b538a7e1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/EditNumberCellInput.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import { Popover } from '@mui/material'; +import InputBase from '@mui/material/InputBase'; + +function EditNumberCellInput({ + editing, + anchorEl, + width, + onClose, + value, + onChange, +}: { + editing: boolean; + anchorEl: HTMLDivElement | null; + width: number | undefined; + onClose: () => void; + value: string; + onChange: (value: string) => void; +}) { + const handleInput = (e: React.FormEvent) => { + const value = (e.target as HTMLInputElement).value; + + onChange(value); + }; + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + onClose(); + } + }, + [onClose] + ); + + return ( + + + + ); +} + +export default EditNumberCellInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx new file mode 100644 index 0000000000..eceb128804 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFieldActions.tsx @@ -0,0 +1,34 @@ +import React, { useCallback } from 'react'; +import { NumberField, NumberTypeOption, updateTypeOption } from '$app/application/database'; +import { Divider } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import NumberFormatSelect from '$app/components/database/components/field_types/number/NumberFormatSelect'; +import { NumberFormatPB } from '@/services/backend'; +import { useViewId } from '$app/hooks'; +import { useTypeOption } from '$app/components/database'; + +function NumberFieldActions({ field }: { field: NumberField }) { + const viewId = useViewId(); + const { t } = useTranslation(); + const typeOption = useTypeOption(field.id); + const onChange = useCallback( + async (value: NumberFormatPB) => { + await updateTypeOption(viewId, field.id, field.type, { + format: value, + }); + }, + [field.id, field.type, viewId] + ); + + return ( + <> +
+
{t('grid.field.format')}
+ +
+ + + ); +} + +export default NumberFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx new file mode 100644 index 0000000000..0f9be6a21a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx @@ -0,0 +1,64 @@ +import React, { useCallback, useMemo, useRef } from 'react'; +import { NumberFormatPB } from '@/services/backend'; +import { Menu, MenuProps } from '@mui/material'; +import { formats, formatText } from '$app/components/database/components/field_types/number/const'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +function NumberFormatMenu({ + value, + onChangeFormat, + ...props +}: MenuProps & { + value: NumberFormatPB; + onChangeFormat: (value: NumberFormatPB) => void; +}) { + const scrollRef = useRef(null); + const onConfirm = useCallback( + (format: NumberFormatPB) => { + onChangeFormat(format); + props.onClose?.({}, 'backdropClick'); + }, + [onChangeFormat, props] + ); + + const renderContent = useCallback( + (format: NumberFormatPB) => { + return ( + <> + {formatText(format)} + {value === format && } + + ); + }, + [value] + ); + + const options: KeyboardNavigationOption[] = useMemo( + () => + formats.map((format) => ({ + key: format.value as NumberFormatPB, + content: renderContent(format.value as NumberFormatPB), + })), + [renderContent] + ); + + return ( + +
+ props.onClose?.({}, 'escapeKeyDown')} + /> +
+
+ ); +} + +export default NumberFormatMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx new file mode 100644 index 0000000000..5a02c6759b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatSelect.tsx @@ -0,0 +1,49 @@ +import React, { useRef, useState } from 'react'; +import { MenuItem } from '@mui/material'; +import { NumberFormatPB } from '@/services/backend'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { formatText } from '$app/components/database/components/field_types/number/const'; +import NumberFormatMenu from '$app/components/database/components/field_types/number/NumberFormatMenu'; + +function NumberFormatSelect({ value, onChange }: { value: NumberFormatPB; onChange: (value: NumberFormatPB) => void }) { + const ref = useRef(null); + const [expanded, setExpanded] = useState(false); + + return ( + <> + { + setExpanded(!expanded); + }} + className={'flex w-full justify-between rounded-none'} + > +
{formatText(value)}
+ +
+ setExpanded(false)} + onChangeFormat={onChange} + /> + + ); +} + +export default NumberFormatSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts new file mode 100644 index 0000000000..38621cb114 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/const.ts @@ -0,0 +1,14 @@ +import { NumberFormatPB } from '@/services/backend'; + +export const formats = Object.entries(NumberFormatPB) + .filter(([, value]) => typeof value !== 'string') + .map(([key, value]) => { + return { + key, + value, + }; + }); + +export const formatText = (format: NumberFormatPB) => { + return formats.find((item) => item.value === format)?.key; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx new file mode 100644 index 0000000000..6c6cf37aae --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx @@ -0,0 +1,165 @@ +import { FC, useMemo, useRef, useState } from 'react'; +import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material'; +import { SelectOptionColorPB } from '@/services/backend'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; +import { SelectOption } from '$app/application/database'; +import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants'; +import Button from '@mui/material/Button'; +import { + deleteSelectOption, + insertOrUpdateSelectOption, +} from '$app/application/database/field/select_option/select_option_service'; +import { useViewId } from '$app/hooks'; +import Popover from '@mui/material/Popover'; +import debounce from 'lodash-es/debounce'; +import { useTranslation } from 'react-i18next'; + +interface SelectOptionMenuProps { + fieldId: string; + option: SelectOption; + MenuProps: MenuProps; +} + +const Colors = [ + SelectOptionColorPB.Purple, + SelectOptionColorPB.Pink, + SelectOptionColorPB.LightPink, + SelectOptionColorPB.Orange, + SelectOptionColorPB.Yellow, + SelectOptionColorPB.Lime, + SelectOptionColorPB.Green, + SelectOptionColorPB.Aqua, + SelectOptionColorPB.Blue, +]; + +export const SelectOptionModifyMenu: FC = ({ fieldId, option, MenuProps: menuProps }) => { + const { t } = useTranslation(); + const [tagName, setTagName] = useState(option.name); + const viewId = useViewId(); + const inputRef = useRef(null); + const updateColor = async (color: SelectOptionColorPB) => { + await insertOrUpdateSelectOption(viewId, fieldId, [ + { + ...option, + color, + }, + ]); + }; + + const updateName = useMemo(() => { + return debounce(async (tagName) => { + if (tagName === option.name) return; + + await insertOrUpdateSelectOption(viewId, fieldId, [ + { + ...option, + name: tagName, + }, + ]); + }, 500); + }, [option, viewId, fieldId]); + + const onClose = () => { + menuProps.onClose?.({}, 'backdropClick'); + }; + + const deleteOption = async () => { + await deleteSelectOption(viewId, fieldId, [option]); + onClose(); + }; + + return ( + { + e.stopPropagation(); + }} + onClose={onClose} + onMouseDown={(e) => { + const isInput = inputRef.current?.contains(e.target as Node); + + if (isInput) return; + e.preventDefault(); + e.stopPropagation(); + }} + > + + { + setTagName(e.target.value); + void updateName(e.target.value); + }} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + void updateName(tagName); + onClose(); + } + }} + onClick={(e) => { + e.stopPropagation(); + }} + onMouseDown={(e) => { + e.stopPropagation(); + }} + autoFocus={true} + placeholder={t('grid.selectOption.tagName')} + size='small' + /> + +
+ +
+ + + {t('grid.selectOption.colorPanelTitle')} + + {Colors.map((color) => ( + { + e.preventDefault(); + e.stopPropagation(); + }} + onClick={(e) => { + e.preventDefault(); + void updateColor(color); + }} + key={color} + value={color} + className={'px-1.5'} + > + + {t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)} + {option.color === color && } + + ))} + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/Tag.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/Tag.tsx new file mode 100644 index 0000000000..3e4677d57d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/Tag.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react'; +import { Chip, ChipProps } from '@mui/material'; +import { SelectOptionColorPB } from '@/services/backend'; +import { SelectOptionColorMap } from './constants'; + +export interface TagProps extends Omit { + color?: SelectOptionColorPB | ChipProps['color']; +} + +export const Tag: FC = ({ color, classes, ...props }) => { + + return ( + + ) +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/constants.ts new file mode 100644 index 0000000000..58a42f7dad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/constants.ts @@ -0,0 +1,25 @@ +import { SelectOptionColorPB } from '@/services/backend'; + +export const SelectOptionColorMap = { + [SelectOptionColorPB.Purple]: 'bg-tint-purple', + [SelectOptionColorPB.Pink]: 'bg-tint-pink', + [SelectOptionColorPB.LightPink]: 'bg-tint-red', + [SelectOptionColorPB.Orange]: 'bg-tint-orange', + [SelectOptionColorPB.Yellow]: 'bg-tint-yellow', + [SelectOptionColorPB.Lime]: 'bg-tint-lime', + [SelectOptionColorPB.Green]: 'bg-tint-green', + [SelectOptionColorPB.Aqua]: 'bg-tint-aqua', + [SelectOptionColorPB.Blue]: 'bg-tint-blue', +}; + +export const SelectOptionColorTextMap = { + [SelectOptionColorPB.Purple]: 'purpleColor', + [SelectOptionColorPB.Pink]: 'pinkColor', + [SelectOptionColorPB.LightPink]: 'lightPinkColor', + [SelectOptionColorPB.Orange]: 'orangeColor', + [SelectOptionColorPB.Yellow]: 'yellowColor', + [SelectOptionColorPB.Lime]: 'limeColor', + [SelectOptionColorPB.Green]: 'greenColor', + [SelectOptionColorPB.Aqua]: 'aquaColor', + [SelectOptionColorPB.Blue]: 'blueColor', +} as const; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx new file mode 100644 index 0000000000..5c8acb4759 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SearchInput.tsx @@ -0,0 +1,38 @@ +import React, { FormEvent, useCallback } from 'react'; +import { OutlinedInput } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function SearchInput({ + setNewOptionName, + newOptionName, + inputRef, +}: { + newOptionName: string; + setNewOptionName: (value: string) => void; + inputRef?: React.RefObject; +}) { + const { t } = useTranslation(); + const handleInput = useCallback( + (event: FormEvent) => { + const value = (event.target as HTMLInputElement).value; + + setNewOptionName(value); + }, + [setNewOptionName] + ); + + return ( + + ); +} + +export default SearchInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx new file mode 100644 index 0000000000..e2cd27019f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectCellActions.tsx @@ -0,0 +1,162 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem'; +import { cellService, SelectCell as SelectCellType, SelectField, SelectTypeOption } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import { + createSelectOption, + insertOrUpdateSelectOption, +} from '$app/application/database/field/select_option/select_option_service'; +import { FieldType } from '@/services/backend'; +import { useTypeOption } from '$app/components/database'; +import SearchInput from './SearchInput'; +import { useTranslation } from 'react-i18next'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { Tag } from '$app/components/database/components/field_types/select/Tag'; + +const CREATE_OPTION_KEY = 'createOption'; + +function SelectCellActions({ + field, + cell, + onUpdated, + onClose, +}: { + field: SelectField; + cell: SelectCellType; + onUpdated?: () => void; + onClose?: () => void; +}) { + const { t } = useTranslation(); + const rowId = cell?.rowId; + const viewId = useViewId(); + const typeOption = useTypeOption(field.id); + const options = useMemo(() => typeOption.options ?? [], [typeOption.options]); + const scrollRef = useRef(null); + const inputRef = useRef(null); + const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]); + const [newOptionName, setNewOptionName] = useState(''); + + const filteredOptions: KeyboardNavigationOption[] = useMemo(() => { + const result = options + .filter((option) => { + return option.name.toLowerCase().includes(newOptionName.toLowerCase()); + }) + .map((option) => ({ + key: option.id, + content: ( + + ), + })); + + if (result.length === 0 && newOptionName) { + result.push({ + key: CREATE_OPTION_KEY, + content: , + }); + } + + return result; + }, [newOptionName, options, selectedOptionIds, cell?.fieldId]); + + const shouldCreateOption = filteredOptions.length === 1 && filteredOptions[0].key === 'createOption'; + + const updateCell = useCallback( + async (optionIds: string[]) => { + if (!cell || !rowId) return; + const deleteOptionIds = selectedOptionIds?.filter((id) => optionIds.find((cur) => cur === id) === undefined); + + await cellService.updateSelectCell(viewId, rowId, field.id, { + insertOptionIds: optionIds, + deleteOptionIds, + }); + onUpdated?.(); + }, + [cell, field.id, onUpdated, rowId, selectedOptionIds, viewId] + ); + + const createOption = useCallback(async () => { + const option = await createSelectOption(viewId, field.id, newOptionName); + + if (!option) return; + await insertOrUpdateSelectOption(viewId, field.id, [option]); + setNewOptionName(''); + return option; + }, [viewId, field.id, newOptionName]); + + const onConfirm = useCallback( + async (key: string) => { + let optionId = key; + + if (key === CREATE_OPTION_KEY) { + const option = await createOption(); + + optionId = option?.id || ''; + } + + if (!optionId) return; + + if (field.type === FieldType.SingleSelect) { + const newOptionIds = [optionId]; + + if (selectedOptionIds?.includes(optionId)) { + newOptionIds.pop(); + } + + void updateCell(newOptionIds); + return; + } + + let newOptionIds = []; + + if (!selectedOptionIds) { + newOptionIds.push(optionId); + } else { + const isSelected = selectedOptionIds.includes(optionId); + + if (isSelected) { + newOptionIds = selectedOptionIds.filter((id) => id !== optionId); + } else { + newOptionIds = [...selectedOptionIds, optionId]; + } + } + + void updateCell(newOptionIds); + }, + [createOption, field.type, selectedOptionIds, updateCell] + ); + + return ( +
+ + + {filteredOptions.length > 0 && ( +
+ {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} +
+ )} + +
+ null} + /> +
+
+ ); +} + +export default SelectCellActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx new file mode 100644 index 0000000000..2a855a4085 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx @@ -0,0 +1,55 @@ +import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react'; +import { IconButton } from '@mui/material'; +import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; +import { SelectOption } from '$app/application/database'; +import { SelectOptionModifyMenu } from '../SelectOptionModifyMenu'; +import { Tag } from '../Tag'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; + +export interface SelectOptionItemProps { + option: SelectOption; + fieldId: string; + isSelected?: boolean; +} + +export const SelectOptionItem: FC = ({ isSelected, fieldId, option }) => { + const [open, setOpen] = useState(false); + const anchorEl = useRef(null); + const [hovered, setHovered] = useState(false); + const handleClick = useCallback>((event) => { + event.stopPropagation(); + setOpen(true); + }, []); + + return ( + <> +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > +
+ +
+ {isSelected && !hovered && } + {hovered && ( + + + + )} +
+ {open && ( + setOpen(false), + }} + /> + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx new file mode 100644 index 0000000000..0fb180bb08 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/AddAnOption.tsx @@ -0,0 +1,76 @@ +import React, { useMemo, useState } from 'react'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { OutlinedInput } from '@mui/material'; +import { + createSelectOption, + insertOrUpdateSelectOption, +} from '$app/application/database/field/select_option/select_option_service'; +import { useViewId } from '$app/hooks'; +import { SelectOption } from '$app/application/database'; +import { notify } from '$app/components/_shared/notify'; + +function AddAnOption({ fieldId, options }: { fieldId: string; options: SelectOption[] }) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [edit, setEdit] = useState(false); + const [newOptionName, setNewOptionName] = useState(''); + const exitEdit = () => { + setNewOptionName(''); + setEdit(false); + }; + + const isOptionExist = useMemo(() => { + return options.some((option) => option.name === newOptionName); + }, [options, newOptionName]); + + const createOption = async () => { + if (!newOptionName) return; + if (isOptionExist) { + notify.error(t('grid.field.optionAlreadyExist')); + return; + } + + const option = await createSelectOption(viewId, fieldId, newOptionName); + + if (!option) return; + await insertOrUpdateSelectOption(viewId, fieldId, [option]); + setNewOptionName(''); + }; + + return edit ? ( + { + setNewOptionName(e.target.value); + }} + value={newOptionName} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void createOption(); + return; + } + + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + exitEdit(); + } + }} + className={'mx-2 mb-1'} + placeholder={t('grid.selectOption.typeANewOption')} + size='small' + /> + ) : ( + + ); +} + +export default AddAnOption; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx new file mode 100644 index 0000000000..ad363d4a1d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Option.tsx @@ -0,0 +1,47 @@ +import React, { useRef, useState } from 'react'; +import { ReactComponent as MoreIcon } from '$app/assets/more.svg'; +import { SelectOption } from '$app/application/database'; +// import { ReactComponent as DragIcon } from '$app/assets/drag.svg'; + +import { SelectOptionModifyMenu } from '$app/components/database/components/field_types/select/SelectOptionModifyMenu'; +import Button from '@mui/material/Button'; +import { SelectOptionColorMap } from '$app/components/database/components/field_types/select/constants'; + +function Option({ option, fieldId }: { option: SelectOption; fieldId: string }) { + const [expanded, setExpanded] = useState(false); + const ref = useRef(null); + + return ( + <> + + setExpanded(false), + open: expanded, + transformOrigin: { + vertical: 'center', + horizontal: 'left', + }, + anchorOrigin: { vertical: 'center', horizontal: 'right' }, + }} + option={option} + /> + + ); +} + +export default Option; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx new file mode 100644 index 0000000000..4e06236263 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/Options.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { SelectOption } from '$app/application/database'; +import Option from './Option'; + +interface Props { + options: SelectOption[]; + fieldId: string; +} +function Options({ options, fieldId }: Props) { + return ( +
+ {options.map((option) => { + return
+ ); +} + +export default Options; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx new file mode 100644 index 0000000000..a3d51ceb60 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_field_actions/SelectFieldActions.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import AddAnOption from '$app/components/database/components/field_types/select/select_field_actions/AddAnOption'; +import Options from '$app/components/database/components/field_types/select/select_field_actions/Options'; +import { SelectField, SelectTypeOption } from '$app/application/database'; +import { Divider } from '@mui/material'; +import { useTypeOption } from '$app/components/database'; + +function SelectFieldActions({ field }: { field: SelectField }) { + const typeOption = useTypeOption(field.id); + const options = useMemo(() => typeOption.options ?? [], [typeOption.options]); + const { t } = useTranslation(); + + return ( + <> +
+
{t('grid.field.optionTitle')}
+ + +
+ + + ); +} + +export default SelectFieldActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx new file mode 100644 index 0000000000..005d185c8f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/text/EditTextCellInput.tsx @@ -0,0 +1,69 @@ +import React, { useCallback } from 'react'; +import { Popover, TextareaAutosize } from '@mui/material'; + +interface Props { + editing: boolean; + anchorEl: HTMLDivElement | null; + onClose: () => void; + text: string; + onInput: (event: React.FormEvent) => void; +} + +function EditTextCellInput({ editing, anchorEl, onClose, text, onInput }: Props) { + const handleEnter = (e: React.KeyboardEvent) => { + const shift = e.shiftKey; + + // If shift is pressed, allow the user to enter a new line, otherwise close the popover + if (!shift && e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + }; + + const setRef = useCallback((e: HTMLTextAreaElement | null) => { + if (!e) return; + const selectionStart = e.value.length; + + e.setSelectionRange(selectionStart, selectionStart); + }, []); + + return ( + { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + onClose(); + } + }} + > + + + ); +} + +export default EditTextCellInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx new file mode 100644 index 0000000000..0ca6c42a86 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/ConditionSelect.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import Popover from '@mui/material/Popover'; + +function ConditionSelect({ + conditions, + value, + onChange, +}: { + conditions: { + value: number; + text: string; + }[]; + value: number; + onChange: (condition: number) => void; +}) { + const [anchorEl, setAnchorEl] = useState(null); + const options: KeyboardNavigationOption[] = useMemo(() => { + return conditions.map((condition) => { + return { + key: condition.value, + content: condition.text, + }; + }); + }, [conditions]); + + const handleClose = useCallback(() => { + setAnchorEl(null); + }, []); + + const onConfirm = useCallback( + (key: number) => { + onChange(key); + }, + [onChange] + ); + + const valueText = useMemo(() => { + return conditions.find((condition) => condition.value === value)?.text; + }, [conditions, value]); + + const open = Boolean(anchorEl); + + return ( +
+
{ + setAnchorEl(e.currentTarget); + }} + className={'flex cursor-pointer select-none items-center gap-2 py-2 text-xs'} + > +
{valueText}
+ +
+ + + +
+ ); +} + +export default ConditionSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx new file mode 100644 index 0000000000..fdd7bccb5b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filter.tsx @@ -0,0 +1,200 @@ +import React, { FC, useMemo, useState } from 'react'; +import { + CheckboxFilterData, + ChecklistFilterData, + DateFilterData, + Field as FieldData, + Filter as FilterType, + NumberFilterData, + SelectFilterData, + TextFilterData, + UndeterminedFilter, +} from '$app/application/database'; +import { Chip, Popover } from '@mui/material'; +import { Property } from '$app/components/database/components/property'; +import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; +import TextFilter from './text_filter/TextFilter'; +import { CheckboxFilterConditionPB, ChecklistFilterConditionPB, FieldType } from '@/services/backend'; +import FilterActions from '$app/components/database/components/filter/FilterActions'; +import { updateFilter } from '$app/application/database/filter/filter_service'; +import { useViewId } from '$app/hooks'; +import SelectFilter from './select_filter/SelectFilter'; + +import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter'; +import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect'; +import TextFilterValue from '$app/components/database/components/filter/text_filter/TextFilterValue'; +import SelectFilterValue from '$app/components/database/components/filter/select_filter/SelectFilterValue'; +import NumberFilterValue from '$app/components/database/components/filter/number_filter/NumberFilterValue'; +import { useTranslation } from 'react-i18next'; +import DateFilterValue from '$app/components/database/components/filter/date_filter/DateFilterValue'; + +interface Props { + filter: FilterType; + field: FieldData; +} + +interface FilterComponentProps { + filter: FilterType; + field: FieldData; + onChange: (data: UndeterminedFilter['data']) => void; + onClose?: () => void; +} + +type FilterComponent = FC; +const getFilterComponent = (field: FieldData) => { + switch (field.type) { + case FieldType.RichText: + case FieldType.URL: + case FieldType.Number: + return TextFilter as FilterComponent; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectFilter as FilterComponent; + + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return DateFilter as FilterComponent; + default: + return null; + } +}; + +function Filter({ filter, field }: Props) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const handleClick = (e: React.MouseEvent) => { + setAnchorEl(e.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const onDataChange = async (data: UndeterminedFilter['data']) => { + const newFilter = { + ...filter, + data: { + ...(filter.data || {}), + ...data, + }, + } as UndeterminedFilter; + + try { + await updateFilter(viewId, newFilter); + } catch (e) { + // toast.error(e.message); + } + }; + + const Component = getFilterComponent(field); + + const condition = useMemo(() => { + switch (field.type) { + case FieldType.RichText: + case FieldType.URL: + return (filter.data as TextFilterData).condition; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return (filter.data as SelectFilterData).condition; + case FieldType.Number: + return (filter.data as NumberFilterData).condition; + case FieldType.Checkbox: + return (filter.data as CheckboxFilterData).condition; + case FieldType.Checklist: + return (filter.data as ChecklistFilterData).condition; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return (filter.data as DateFilterData).condition; + default: + return; + } + }, [field, filter]); + + const conditionValue = useMemo(() => { + switch (field.type) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Number: + return ; + case FieldType.Checkbox: + return (filter.data as CheckboxFilterData).condition === CheckboxFilterConditionPB.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked'); + case FieldType.Checklist: + return (filter.data as ChecklistFilterData).condition === ChecklistFilterConditionPB.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted'); + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + default: + return ''; + } + }, [field.id, field.type, filter.data, t]); + + return ( + <> + + + {conditionValue} + +
+ } + onClick={handleClick} + /> + {condition !== undefined && open && ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + handleClose(); + } + }} + > +
+ { + void onDataChange({ + condition, + }); + }} + /> + +
+ {Component && } +
+ )} + + ); +} + +export default Filter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx new file mode 100644 index 0000000000..ebc9e8982c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterActions.tsx @@ -0,0 +1,84 @@ +import React, { useMemo, useState } from 'react'; +import { IconButton, Menu } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/details.svg'; +import { Filter } from '$app/application/database'; +import { useTranslation } from 'react-i18next'; +import { deleteFilter } from '$app/application/database/filter/filter_service'; +import { useViewId } from '$app/hooks'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +function FilterActions({ filter }: { filter: Filter }) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [disableSelect, setDisableSelect] = useState(true); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const onClose = () => { + setDisableSelect(true); + setAnchorEl(null); + }; + + const onDelete = async () => { + try { + await deleteFilter(viewId, filter); + } catch (e) { + // toast.error(e.message); + } + + setDisableSelect(true); + }; + + const options: KeyboardNavigationOption[] = useMemo( + () => [ + { + key: 'delete', + content: t('grid.settings.deleteFilter'), + }, + ], + [t] + ); + + return ( + <> + { + setAnchorEl(e.currentTarget); + }} + className={'mx-2 my-1.5'} + > + + + {open && ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + }} + keepMounted={false} + open={open} + anchorEl={anchorEl} + onClose={onClose} + > + { + if (e.key === 'ArrowDown') { + setDisableSelect(false); + } + }} + disableSelect={disableSelect} + options={options} + onConfirm={onDelete} + onEscape={onClose} + /> + + )} + + ); +} + +export default FilterActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx new file mode 100644 index 0000000000..8b793942da --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx @@ -0,0 +1,224 @@ +import React, { useMemo } from 'react'; +import ConditionSelect from './ConditionSelect'; +import { + CheckboxFilterConditionPB, + ChecklistFilterConditionPB, + DateFilterConditionPB, + FieldType, + NumberFilterConditionPB, + SelectOptionFilterConditionPB, + TextFilterConditionPB, +} from '@/services/backend'; + +import { useTranslation } from 'react-i18next'; + +function FilterConditionSelect({ + name, + condition, + fieldType, + onChange, +}: { + name: string; + condition: number; + fieldType: FieldType; + onChange: (condition: number) => void; +}) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return [ + { + value: TextFilterConditionPB.TextContains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterConditionPB.TextDoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterConditionPB.TextStartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterConditionPB.TextEndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterConditionPB.TextIs, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterConditionPB.TextIsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterConditionPB.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterConditionPB.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + case FieldType.SingleSelect: + return [ + { + value: SelectOptionFilterConditionPB.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + case FieldType.MultiSelect: + return [ + { + value: SelectOptionFilterConditionPB.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterConditionPB.OptionContains, + text: t('grid.selectOptionFilter.contains'), + }, + { + value: SelectOptionFilterConditionPB.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + case FieldType.Number: + return [ + { + value: NumberFilterConditionPB.Equal, + text: '=', + }, + { + value: NumberFilterConditionPB.NotEqual, + text: '!=', + }, + { + value: NumberFilterConditionPB.GreaterThan, + text: '>', + }, + { + value: NumberFilterConditionPB.LessThan, + text: '<', + }, + { + value: NumberFilterConditionPB.GreaterThanOrEqualTo, + text: '>=', + }, + { + value: NumberFilterConditionPB.LessThanOrEqualTo, + text: '<=', + }, + { + value: NumberFilterConditionPB.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterConditionPB.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + case FieldType.Checkbox: + return [ + { + value: CheckboxFilterConditionPB.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterConditionPB.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ]; + case FieldType.Checklist: + return [ + { + value: ChecklistFilterConditionPB.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterConditionPB.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ]; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return [ + { + value: DateFilterConditionPB.DateIs, + text: t('grid.dateFilter.is'), + }, + { + value: DateFilterConditionPB.DateBefore, + text: t('grid.dateFilter.before'), + }, + { + value: DateFilterConditionPB.DateAfter, + text: t('grid.dateFilter.after'), + }, + { + value: DateFilterConditionPB.DateOnOrBefore, + text: t('grid.dateFilter.onOrBefore'), + }, + { + value: DateFilterConditionPB.DateOnOrAfter, + text: t('grid.dateFilter.onOrAfter'), + }, + { + value: DateFilterConditionPB.DateWithIn, + text: t('grid.dateFilter.between'), + }, + { + value: DateFilterConditionPB.DateIsEmpty, + text: t('grid.dateFilter.empty'), + }, + { + value: DateFilterConditionPB.DateIsNotEmpty, + text: t('grid.dateFilter.notEmpty'), + }, + ]; + default: + return []; + } + }, [fieldType, t]); + + return ( +
+
{name}
+ { + onChange(e); + }} + value={condition} + /> +
+ ); +} + +export default FilterConditionSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx new file mode 100644 index 0000000000..e161badbf8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterFieldsMenu.tsx @@ -0,0 +1,59 @@ +import React, { useCallback } from 'react'; +import { MenuProps } from '@mui/material'; +import PropertiesList from '$app/components/database/components/property/PropertiesList'; +import { Field } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { insertFilter } from '$app/application/database/filter/filter_service'; +import { getDefaultFilter } from '$app/application/database/filter/filter_data'; +import Popover from '@mui/material/Popover'; + +function FilterFieldsMenu({ + onInserted, + ...props +}: MenuProps & { + onInserted?: () => void; +}) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const addFilter = useCallback( + async (field: Field) => { + const filterData = getDefaultFilter(field.type); + + await insertFilter({ + viewId, + fieldId: field.id, + fieldType: field.type, + data: filterData, + }); + props.onClose?.({}, 'backdropClick'); + onInserted?.(); + }, + [props, viewId, onInserted] + ); + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + {...props} + > + { + props.onClose?.({}, 'escapeKeyDown'); + }} + showSearch + searchPlaceholder={t('grid.settings.filterBy')} + onItemClick={addFilter} + /> + + ); +} + +export default FilterFieldsMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx new file mode 100644 index 0000000000..860ce9f69f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/Filters.tsx @@ -0,0 +1,51 @@ +import React, { useMemo, useState } from 'react'; +import Filter from '$app/components/database/components/filter/Filter'; +import Button from '@mui/material/Button'; +import FilterFieldsMenu from '$app/components/database/components/filter/FilterFieldsMenu'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useDatabase } from '$app/components/database'; + +function Filters() { + const { t } = useTranslation(); + const { filters, fields } = useDatabase(); + + const options = useMemo(() => { + return filters.map((filter) => { + const field = fields.find((field) => field.id === filter.fieldId); + + return { + filter, + field, + }; + }); + }, [filters, fields]); + + const [filterAnchorEl, setFilterAnchorEl] = useState(null); + const openAddFilterMenu = Boolean(filterAnchorEl); + + const handleClick = (e: React.MouseEvent) => { + setFilterAnchorEl(e.currentTarget); + }; + + return ( +
+ {options.map(({ filter, field }) => (field ? : null))} + + setFilterAnchorEl(null)} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + /> +
+ ); +} + +export default Filters; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx new file mode 100644 index 0000000000..5c96d42b96 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilter.tsx @@ -0,0 +1,94 @@ +import React, { useMemo } from 'react'; +import { + DateFilter as DateFilterType, + DateFilterData, + DateTimeField, + DateTimeTypeOption, +} from '$app/application/database'; +import { DateFilterConditionPB } from '@/services/backend'; +import CustomCalendar from '$app/components/database/components/field_types/date/CustomCalendar'; +import DateTimeSet from '$app/components/database/components/field_types/date/DateTimeSet'; +import { useTypeOption } from '$app/components/database'; +import { getDateFormat, getTimeFormat } from '$app/components/database/components/field_types/date/utils'; + +interface Props { + filter: DateFilterType; + field: DateTimeField; + onChange: (filterData: DateFilterData) => void; +} + +function DateFilter({ filter, field, onChange }: Props) { + const typeOption = useTypeOption(field.id); + + const showCalendar = + filter.data.condition !== DateFilterConditionPB.DateIsEmpty && + filter.data.condition !== DateFilterConditionPB.DateIsNotEmpty; + + const condition = filter.data.condition; + const isRange = condition === DateFilterConditionPB.DateWithIn; + const timestamp = useMemo(() => { + if (isRange) { + return filter.data.start; + } + + return filter.data.timestamp; + }, [filter.data.start, filter.data.timestamp, isRange]); + + const endTimestamp = useMemo(() => { + if (isRange) { + return filter.data.end; + } + + return; + }, [filter.data.end, isRange]); + + const timeFormat = useMemo(() => { + return getTimeFormat(typeOption.timeFormat); + }, [typeOption.timeFormat]); + + const dateFormat = useMemo(() => { + return getDateFormat(typeOption.dateFormat); + }, [typeOption.dateFormat]); + + return ( +
+ {showCalendar && ( + <> +
+ { + onChange({ + condition, + timestamp: date, + start: endDate ? date : undefined, + end: endDate, + }); + }} + date={timestamp} + endDate={endTimestamp} + timeFormat={timeFormat} + dateFormat={dateFormat} + includeTime={false} + isRange={isRange} + /> +
+ { + onChange({ + condition, + timestamp: date, + start: endDate ? date : undefined, + end: endDate, + }); + }} + isRange={isRange} + timestamp={timestamp} + endTimestamp={endTimestamp} + /> + + )} +
+ ); +} + +export default DateFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx new file mode 100644 index 0000000000..dd75d25852 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/date_filter/DateFilterValue.tsx @@ -0,0 +1,52 @@ +import React, { useMemo } from 'react'; +import { DateFilterData } from '$app/application/database'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; +import { DateFilterConditionPB } from '@/services/backend'; + +function DateFilterValue({ data }: { data: DateFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (data.start) { + const end = data.end ?? data.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(data.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(data.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(data.timestamp).format('MMM D'); + + switch (data.condition) { + case DateFilterConditionPB.DateIs: + return `: ${timestamp}`; + case DateFilterConditionPB.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterConditionPB.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterConditionPB.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterConditionPB.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterConditionPB.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterConditionPB.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterConditionPB.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [data, t]); + + return <>{value}; +} + +export default DateFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx new file mode 100644 index 0000000000..658ef13d69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/number_filter/NumberFilterValue.tsx @@ -0,0 +1,39 @@ +import React, { useMemo } from 'react'; +import { NumberFilterData } from '$app/application/database'; +import { NumberFilterConditionPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterValue({ data }: { data: NumberFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.content) { + return ''; + } + + const content = parseInt(data.content); + + switch (data.condition) { + case NumberFilterConditionPB.Equal: + return `= ${content}`; + case NumberFilterConditionPB.NotEqual: + return `!= ${content}`; + case NumberFilterConditionPB.GreaterThan: + return `> ${content}`; + case NumberFilterConditionPB.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterConditionPB.LessThan: + return `< ${content}`; + case NumberFilterConditionPB.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterConditionPB.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterConditionPB.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [data.condition, data.content, t]); + + return <>{value}; +} + +export default NumberFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx new file mode 100644 index 0000000000..bd1d1f239a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx @@ -0,0 +1,92 @@ +import React, { useMemo, useRef } from 'react'; +import { + SelectField, + SelectFilter as SelectFilterType, + SelectFilterData, + SelectTypeOption, +} from '$app/application/database'; +import { Tag } from '$app/components/database/components/field_types/select/Tag'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; +import { SelectOptionFilterConditionPB } from '@/services/backend'; +import { useTypeOption } from '$app/components/database'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +interface Props { + filter: SelectFilterType; + field: SelectField; + onChange: (filterData: SelectFilterData) => void; + onClose?: () => void; +} + +function SelectFilter({ onClose, filter, field, onChange }: Props) { + const scrollRef = useRef(null); + const condition = filter.data.condition; + const typeOption = useTypeOption(field.id); + const options: KeyboardNavigationOption[] = useMemo(() => { + return ( + typeOption?.options?.map((option) => { + return { + key: option.id, + content: ( +
+ + {filter.data.optionIds?.includes(option.id) && } +
+ ), + }; + }) ?? [] + ); + }, [filter.data.optionIds, typeOption?.options]); + + const showOptions = + options.length > 0 && + condition !== SelectOptionFilterConditionPB.OptionIsEmpty && + condition !== SelectOptionFilterConditionPB.OptionIsNotEmpty; + + const handleChange = ({ + condition, + optionIds, + }: { + condition?: SelectFilterData['condition']; + optionIds?: SelectFilterData['optionIds']; + }) => { + onChange({ + condition: condition ?? filter.data.condition, + optionIds: optionIds ?? filter.data.optionIds, + }); + }; + + const handleSelectOption = (optionId: string) => { + const prev = filter.data.optionIds; + let newOptionIds = []; + + if (!prev) { + newOptionIds.push(optionId); + } else { + const isSelected = prev.includes(optionId); + + if (isSelected) { + newOptionIds = prev.filter((id) => id !== optionId); + } else { + newOptionIds = [...prev, optionId]; + } + } + + handleChange({ + condition, + optionIds: newOptionIds, + }); + }; + + if (!showOptions) return null; + + return ( +
+ +
+ ); +} + +export default SelectFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx new file mode 100644 index 0000000000..72576deae1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx @@ -0,0 +1,38 @@ +import React, { useMemo } from 'react'; +import { SelectFilterData, SelectTypeOption } from '$app/application/database'; +import { useStaticTypeOption } from '$app/components/database'; +import { useTranslation } from 'react-i18next'; +import { SelectOptionFilterConditionPB } from '@/services/backend'; + +function SelectFilterValue({ data, fieldId }: { data: SelectFilterData; fieldId: string }) { + const typeOption = useStaticTypeOption(fieldId); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!data.optionIds?.length) return ''; + + const options = data.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (data.condition) { + case SelectOptionFilterConditionPB.OptionIs: + return `: ${options}`; + case SelectOptionFilterConditionPB.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionFilterConditionPB.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionFilterConditionPB.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [data.condition, data.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx new file mode 100644 index 0000000000..0c7eab6e05 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilter.tsx @@ -0,0 +1,50 @@ +import React, { useMemo, useState } from 'react'; +import { TextFilter as TextFilterType, TextFilterData } from '$app/application/database'; +import { TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { TextFilterConditionPB } from '@/services/backend'; +import debounce from 'lodash-es/debounce'; + +interface Props { + filter: TextFilterType; + onChange: (filterData: TextFilterData) => void; +} + +const DELAY = 500; + +function TextFilter({ filter, onChange }: Props) { + const { t } = useTranslation(); + const [content, setContext] = useState(filter.data.content); + const condition = filter.data.condition; + const showField = + condition !== TextFilterConditionPB.TextIsEmpty && condition !== TextFilterConditionPB.TextIsNotEmpty; + + const onConditionChange = useMemo(() => { + return debounce((content: string) => { + onChange({ + content, + condition, + }); + }, DELAY); + }, [condition, onChange]); + + if (!showField) return null; + return ( + { + setContext(e.target.value); + onConditionChange(e.target.value ?? ''); + }} + /> + ); +} + +export default TextFilter; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx new file mode 100644 index 0000000000..5718a3e2b8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx @@ -0,0 +1,34 @@ +import { useMemo } from 'react'; +import { TextFilterData } from '$app/application/database'; +import { TextFilterConditionPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; + +function TextFilterValue({ data }: { data: TextFilterData }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!data.content) return ''; + switch (data.condition) { + case TextFilterConditionPB.TextContains: + case TextFilterConditionPB.TextIs: + return `: ${data.content}`; + case TextFilterConditionPB.TextDoesNotContain: + case TextFilterConditionPB.TextIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${data.content}`; + case TextFilterConditionPB.TextStartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${data.content}`; + case TextFilterConditionPB.TextEndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${data.content}`; + case TextFilterConditionPB.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterConditionPB.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, data]); + + return <>{value}; +} + +export default TextFilterValue; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/index.ts new file mode 100644 index 0000000000..7da8d0eab4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/index.ts @@ -0,0 +1,2 @@ +export * from './tab_bar'; +export * from './cell'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx new file mode 100644 index 0000000000..bb71befa8d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/NewProperty.tsx @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; +import { fieldService } from '$app/application/database'; +import { FieldType } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useViewId } from '$app/hooks'; + +interface NewPropertyProps { + onInserted?: (id: string) => void; +} +function NewProperty({ onInserted }: NewPropertyProps) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleClick = useCallback(async () => { + try { + const field = await fieldService.createField({ + viewId, + fieldType: FieldType.RichText, + }); + + onInserted?.(field.id); + } catch (e) { + // toast.error(t('grid.field.newPropertyFail')); + } + }, [onInserted, viewId]); + + return ( + + ); +} + +export default NewProperty; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx new file mode 100644 index 0000000000..a9865c467f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertiesList.tsx @@ -0,0 +1,92 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { OutlinedInput } from '@mui/material'; +import { Property } from '$app/components/database/components/property/Property'; +import { Field as FieldType } from '$app/application/database'; +import { useDatabase } from '$app/components/database'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +interface FieldListProps { + searchPlaceholder?: string; + showSearch?: boolean; + onItemClick?: (field: FieldType) => void; + onClose?: () => void; +} + +function PropertiesList({ onClose, showSearch, onItemClick, searchPlaceholder }: FieldListProps) { + const { fields } = useDatabase(); + const [fieldsResult, setFieldsResult] = useState(fields as FieldType[]); + + const onInputChange = useCallback( + (event: React.ChangeEvent) => { + const value = event.target.value; + const result = fields.filter((field) => field.name.toLowerCase().includes(value.toLowerCase())); + + setFieldsResult(result); + }, + [fields] + ); + + const inputRef = useRef(null); + + const searchInput = useMemo(() => { + return showSearch ? ( +
+ +
+ ) : null; + }, [onInputChange, searchPlaceholder, showSearch]); + + const scrollRef = useRef(null); + + const options = useMemo(() => { + return fieldsResult.map((field) => { + return { + key: field.id, + content: ( +
+ +
+ ), + }; + }); + }, [fieldsResult]); + + const onConfirm = useCallback( + (key: string) => { + const field = fields.find((field) => field.id === key); + + onItemClick?.(field as FieldType); + }, + [fields, onItemClick] + ); + + return ( +
+ {searchInput} +
+ +
+
+ ); +} + +export default PropertiesList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx new file mode 100644 index 0000000000..3091ba4ea1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/Property.tsx @@ -0,0 +1,95 @@ +import { FC, useEffect, useRef, useState } from 'react'; +import { Field as FieldType } from '$app/application/database'; +import { ProppertyTypeSvg } from './property_type/ProppertyTypeSvg'; +import { PropertyMenu } from '$app/components/database/components/property/PropertyMenu'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +export interface FieldProps { + field: FieldType; + menuOpened?: boolean; + onOpenMenu?: (id: string) => void; + onCloseMenu?: (id: string) => void; + className?: string; +} + +const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'right', +}; + +const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'center', +}; + +export const Property: FC = ({ field, onCloseMenu, className, menuOpened }) => { + const ref = useRef(null); + const [anchorPosition, setAnchorPosition] = useState< + | { + top: number; + left: number; + height: number; + } + | undefined + >(undefined); + + const open = Boolean(anchorPosition && menuOpened); + + useEffect(() => { + if (menuOpened) { + const rect = ref.current?.getBoundingClientRect(); + + if (rect) { + setAnchorPosition({ + top: rect.top + 28, + left: rect.left, + height: rect.height, + }); + return; + } + } + + setAnchorPosition(undefined); + }, [menuOpened]); + + const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 300, + initialPaperHeight: 400, + anchorPosition, + initialAnchorOrigin, + initialTransformOrigin, + open, + }); + + return ( + <> +
+ + {field.name} +
+ + {open && ( + { + onCloseMenu?.(field.id); + }} + transformOrigin={transformOrigin} + anchorOrigin={anchorOrigin} + PaperProps={{ + style: { + maxHeight: paperHeight, + width: paperWidth, + height: 'auto', + }, + className: 'flex h-full flex-col overflow-hidden', + }} + anchorPosition={anchorPosition} + anchorReference={'anchorPosition'} + /> + )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx new file mode 100644 index 0000000000..b319940996 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyActions.tsx @@ -0,0 +1,271 @@ +import React, { RefObject, useCallback, useMemo, useState } from 'react'; + +import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; +import { ReactComponent as HideSvg } from '$app/assets/hide.svg'; +import { ReactComponent as ShowSvg } from '$app/assets/eye_open.svg'; + +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; +import { ReactComponent as RightSvg } from '$app/assets/right.svg'; +import { useViewId } from '$app/hooks'; +import { fieldService } from '$app/application/database'; +import { OrderObjectPositionTypePB, FieldVisibility } from '@/services/backend'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; +import { useTranslation } from 'react-i18next'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { notify } from 'src/appflowy_app/components/_shared/notify'; + +export enum FieldAction { + EditProperty, + Hide, + Show, + Duplicate, + Delete, + InsertLeft, + InsertRight, +} + +const FieldActionSvgMap = { + [FieldAction.EditProperty]: EditSvg, + [FieldAction.Hide]: HideSvg, + [FieldAction.Show]: ShowSvg, + [FieldAction.Duplicate]: CopySvg, + [FieldAction.Delete]: DeleteSvg, + [FieldAction.InsertLeft]: LeftSvg, + [FieldAction.InsertRight]: RightSvg, +}; + +const defaultActions: FieldAction[] = [ + FieldAction.EditProperty, + FieldAction.InsertLeft, + FieldAction.InsertRight, + FieldAction.Hide, + FieldAction.Duplicate, + FieldAction.Delete, +]; + +// prevent default actions for primary fields +const primaryPreventDefaultActions = [FieldAction.Hide, FieldAction.Delete, FieldAction.Duplicate]; + +interface PropertyActionsProps { + fieldId: string; + actions?: FieldAction[]; + isPrimary?: boolean; + inputRef?: RefObject; + onClose?: () => void; + onMenuItemClick?: (action: FieldAction, newFieldId?: string) => void; +} + +function PropertyActions({ + onClose, + inputRef, + fieldId, + onMenuItemClick, + isPrimary, + actions = defaultActions, +}: PropertyActionsProps) { + const viewId = useViewId(); + const { t } = useTranslation(); + const [openConfirm, setOpenConfirm] = useState(false); + const [focusMenu, setFocusMenu] = useState(false); + const menuTextMap = useMemo( + () => ({ + [FieldAction.EditProperty]: t('grid.field.editProperty'), + [FieldAction.Hide]: t('grid.field.hide'), + [FieldAction.Show]: t('grid.field.show'), + [FieldAction.Duplicate]: t('grid.field.duplicate'), + [FieldAction.Delete]: t('grid.field.delete'), + [FieldAction.InsertLeft]: t('grid.field.insertLeft'), + [FieldAction.InsertRight]: t('grid.field.insertRight'), + }), + [t] + ); + + const handleOpenConfirm = () => { + setOpenConfirm(true); + }; + + const handleMenuItemClick = async (action: FieldAction) => { + const preventDefault = isPrimary && primaryPreventDefaultActions.includes(action); + + if (preventDefault) { + return; + } + + switch (action) { + case FieldAction.EditProperty: + break; + case FieldAction.InsertLeft: + case FieldAction.InsertRight: { + const fieldPosition = + action === FieldAction.InsertLeft ? OrderObjectPositionTypePB.Before : OrderObjectPositionTypePB.After; + + const field = await fieldService.createField({ + viewId, + fieldPosition, + targetFieldId: fieldId, + }); + + onMenuItemClick?.(action, field.id); + return; + } + + case FieldAction.Hide: + await fieldService.updateFieldSetting(viewId, fieldId, { + visibility: FieldVisibility.AlwaysHidden, + }); + break; + case FieldAction.Show: + await fieldService.updateFieldSetting(viewId, fieldId, { + visibility: FieldVisibility.AlwaysShown, + }); + break; + case FieldAction.Duplicate: + await fieldService.duplicateField(viewId, fieldId); + break; + case FieldAction.Delete: + handleOpenConfirm(); + return; + } + + onMenuItemClick?.(action); + }; + + const renderActionContent = useCallback((item: { text: string; Icon: React.FC> }) => { + const { Icon, text } = item; + + return ( +
+ +
{text}
+
+ ); + }, []); + + const options: KeyboardNavigationOption[] = useMemo( + () => + [ + { + key: FieldAction.EditProperty, + content: renderActionContent({ + text: menuTextMap[FieldAction.EditProperty], + Icon: FieldActionSvgMap[FieldAction.EditProperty], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.EditProperty), + }, + { + key: FieldAction.InsertLeft, + content: renderActionContent({ + text: menuTextMap[FieldAction.InsertLeft], + Icon: FieldActionSvgMap[FieldAction.InsertLeft], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.InsertLeft), + }, + { + key: FieldAction.InsertRight, + content: renderActionContent({ + text: menuTextMap[FieldAction.InsertRight], + Icon: FieldActionSvgMap[FieldAction.InsertRight], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.InsertRight), + }, + { + key: FieldAction.Hide, + content: renderActionContent({ + text: menuTextMap[FieldAction.Hide], + Icon: FieldActionSvgMap[FieldAction.Hide], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.Hide), + }, + { + key: FieldAction.Show, + content: renderActionContent({ + text: menuTextMap[FieldAction.Show], + Icon: FieldActionSvgMap[FieldAction.Show], + }), + }, + { + key: FieldAction.Duplicate, + content: renderActionContent({ + text: menuTextMap[FieldAction.Duplicate], + Icon: FieldActionSvgMap[FieldAction.Duplicate], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.Duplicate), + }, + { + key: FieldAction.Delete, + content: renderActionContent({ + text: menuTextMap[FieldAction.Delete], + Icon: FieldActionSvgMap[FieldAction.Delete], + }), + disabled: isPrimary && primaryPreventDefaultActions.includes(FieldAction.Delete), + }, + ].filter((option) => actions.includes(option.key)), + [renderActionContent, menuTextMap, isPrimary, actions] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + const isTab = e.key === 'Tab'; + + if (!focusMenu && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + e.stopPropagation(); + notify.clear(); + notify.info(`Press Tab to focus on the menu`); + return; + } + + if (isTab) { + e.preventDefault(); + e.stopPropagation(); + if (focusMenu) { + inputRef?.current?.focus(); + setFocusMenu(false); + } else { + inputRef?.current?.blur(); + setFocusMenu(true); + } + + return; + } + }, + [focusMenu, inputRef] + ); + + return ( + <> + { + setFocusMenu(true); + }} + onBlur={() => { + setFocusMenu(false); + }} + onKeyDown={handleKeyDown} + onConfirm={handleMenuItemClick} + /> + { + await fieldService.deleteField(viewId, fieldId); + }} + onClose={() => { + setOpenConfirm(false); + onClose?.(); + }} + /> + + ); +} + +export default PropertyActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx new file mode 100644 index 0000000000..55b314b821 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyMenu.tsx @@ -0,0 +1,89 @@ +import { Divider } from '@mui/material'; +import { FC, useCallback, useMemo, useRef } from 'react'; +import { useViewId } from '$app/hooks'; +import { Field, fieldService } from '$app/application/database'; +import PropertyTypeMenuExtension from '$app/components/database/components/property/property_type/PropertyTypeMenuExtension'; +import PropertyTypeSelect from '$app/components/database/components/property/property_type/PropertyTypeSelect'; +import { FieldType, FieldVisibility } from '@/services/backend'; +import { Log } from '$app/utils/log'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import PropertyNameInput from '$app/components/database/components/property/PropertyNameInput'; +import PropertyActions, { FieldAction } from '$app/components/database/components/property/PropertyActions'; + +export interface GridFieldMenuProps extends PopoverProps { + field: Field; +} + +export const PropertyMenu: FC = ({ field, ...props }) => { + const viewId = useViewId(); + const inputRef = useRef(null); + + const isPrimary = field.isPrimary; + const actions = useMemo(() => { + const keys = [FieldAction.Duplicate, FieldAction.Delete]; + + if (field.visibility === FieldVisibility.AlwaysHidden) { + keys.unshift(FieldAction.Show); + } else { + keys.unshift(FieldAction.Hide); + } + + return keys; + }, [field.visibility]); + + const onUpdateFieldType = useCallback( + async (type: FieldType) => { + try { + await fieldService.updateFieldType(viewId, field.id, type); + } catch (e) { + // TODO + Log.error(`change field ${field.id} type from '${field.type}' to ${type} fail`, e); + } + }, + [viewId, field] + ); + + return ( + e.stopPropagation()} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + onMouseDown={(e) => { + const isInput = inputRef.current?.contains(e.target as Node); + + if (isInput) return; + + e.stopPropagation(); + e.preventDefault(); + }} + {...props} + > + +
+ {!isPrimary && ( +
+ + +
+ )} + + props.onClose?.({}, 'backdropClick')} + isPrimary={isPrimary} + actions={actions} + onMenuItemClick={() => { + props.onClose?.({}, 'backdropClick'); + }} + fieldId={field.id} + /> +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx new file mode 100644 index 0000000000..4e20531335 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertyNameInput.tsx @@ -0,0 +1,49 @@ +import React, { ChangeEventHandler, useCallback, useMemo, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { fieldService } from '$app/application/database'; +import { Log } from '$app/utils/log'; +import TextField from '@mui/material/TextField'; +import debounce from 'lodash-es/debounce'; + +const PropertyNameInput = React.forwardRef(({ id, name }, ref) => { + const viewId = useViewId(); + const [inputtingName, setInputtingName] = useState(name); + + const handleSubmit = useCallback( + async (newName: string) => { + if (newName !== name) { + try { + await fieldService.updateField(viewId, id, { + name: newName, + }); + } catch (e) { + // TODO + Log.error(`change field ${id} name from '${name}' to ${newName} fail`, e); + } + } + }, + [viewId, id, name] + ); + + const debouncedHandleSubmit = useMemo(() => debounce(handleSubmit, 500), [handleSubmit]); + const handleInput = useCallback>( + (e) => { + setInputtingName(e.target.value); + void debouncedHandleSubmit(e.target.value); + }, + [debouncedHandleSubmit] + ); + + return ( + + ); +}); + +export default PropertyNameInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx new file mode 100644 index 0000000000..0741bbc05b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/PropertySelect.tsx @@ -0,0 +1,84 @@ +import { FC, useCallback, useMemo, useRef, useState } from 'react'; +import { Field as FieldType } from '$app/application/database'; +import { useDatabase } from '../../Database.hooks'; +import { Property } from './Property'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { ReactComponent as DropDownSvg } from '$app/assets/more.svg'; +import Popover from '@mui/material/Popover'; + +export interface FieldSelectProps { + onChange?: (field: FieldType | undefined) => void; + value?: string; +} + +export const PropertySelect: FC = ({ value, onChange }) => { + const { fields } = useDatabase(); + + const scrollRef = useRef(null); + const ref = useRef(null); + const [open, setOpen] = useState(false); + const handleClose = () => { + setOpen(false); + }; + + const options: KeyboardNavigationOption[] = useMemo( + () => + fields.map((field) => { + return { + key: field.id, + content: , + }; + }), + [fields] + ); + + const onConfirm = useCallback( + (optionKey: string) => { + onChange?.(fields.find((field) => field.id === optionKey)); + }, + [onChange, fields] + ); + + const selectedField = useMemo(() => fields.find((field) => field.id === value), [fields, value]); + + return ( + <> +
{ + setOpen(true); + }} + > +
{selectedField ? : null}
+ +
+ {open && ( + +
+ +
+
+ )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts new file mode 100644 index 0000000000..0b338836d6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/index.ts @@ -0,0 +1,4 @@ +export * from './Property'; +export * from './PropertySelect'; +export * from './property_type/PropertyTypeText'; +export * from './property_type/ProppertyTypeSvg'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx new file mode 100644 index 0000000000..e3021249ee --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx @@ -0,0 +1,121 @@ +import { Menu, MenuProps } from '@mui/material'; +import { FC, useCallback, useMemo } from 'react'; +import { FieldType } from '@/services/backend'; +import { PropertyTypeText, ProppertyTypeSvg } from '$app/components/database/components/property'; +import { Field } from '$app/application/database'; +import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import Typography from '@mui/material/Typography'; + +export const PropertyTypeMenu: FC< + MenuProps & { + field: Field; + onClickItem?: (type: FieldType) => void; + } +> = ({ field, onClickItem, ...props }) => { + const PopoverClasses = useMemo( + () => ({ + ...props.PopoverClasses, + paper: ['w-56', props.PopoverClasses?.paper].join(' '), + }), + [props.PopoverClasses] + ); + + const renderGroupContent = useCallback((title: string) => { + return ( + + {title} + + ); + }, []); + + const renderContent = useCallback( + (type: FieldType) => { + return ( + <> + + + + + {type === field.type && } + + ); + }, + [field.type] + ); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: 100, + content: renderGroupContent('Basic'), + children: [ + { + key: FieldType.RichText, + content: renderContent(FieldType.RichText), + }, + { + key: FieldType.Number, + content: renderContent(FieldType.Number), + }, + { + key: FieldType.SingleSelect, + content: renderContent(FieldType.SingleSelect), + }, + { + key: FieldType.MultiSelect, + content: renderContent(FieldType.MultiSelect), + }, + { + key: FieldType.DateTime, + content: renderContent(FieldType.DateTime), + }, + { + key: FieldType.Checkbox, + content: renderContent(FieldType.Checkbox), + }, + { + key: FieldType.Checklist, + content: renderContent(FieldType.Checklist), + }, + { + key: FieldType.URL, + content: renderContent(FieldType.URL), + }, + ], + }, + { + key: 101, + content:
, + children: [], + }, + { + key: 102, + content: renderGroupContent('Advanced'), + children: [ + { + key: FieldType.LastEditedTime, + content: renderContent(FieldType.LastEditedTime), + }, + { + key: FieldType.CreatedTime, + content: renderContent(FieldType.CreatedTime), + }, + ], + }, + ]; + }, [renderContent, renderGroupContent]); + + return ( + + props?.onClose?.({}, 'escapeKeyDown')} + options={options} + disableFocus={true} + onConfirm={onClickItem} + /> + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx new file mode 100644 index 0000000000..b45b670757 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenuExtension.tsx @@ -0,0 +1,26 @@ +import React, { useMemo } from 'react'; +import { FieldType } from '@/services/backend'; +import { DateTimeField, Field, NumberField, SelectField } from '$app/application/database'; +import SelectFieldActions from '$app/components/database/components/field_types/select/select_field_actions/SelectFieldActions'; +import NumberFieldActions from '$app/components/database/components/field_types/number/NumberFieldActions'; +import DateTimeFieldActions from '$app/components/database/components/field_types/date/DateTimeFieldActions'; + +function PropertyTypeMenuExtension({ field }: { field: Field }) { + return useMemo(() => { + switch (field.type) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return ; + default: + return null; + } + }, [field]); +} + +export default PropertyTypeMenuExtension; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx new file mode 100644 index 0000000000..28d62b82c6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeSelect.tsx @@ -0,0 +1,59 @@ +import React, { useRef, useState } from 'react'; +import { ProppertyTypeSvg } from '$app/components/database/components/property/property_type/ProppertyTypeSvg'; +import { MenuItem } from '@mui/material'; +import { Field } from '$app/application/database'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { PropertyTypeMenu } from '$app/components/database/components/property/property_type/PropertyTypeMenu'; +import { FieldType } from '@/services/backend'; +import { PropertyTypeText } from '$app/components/database/components/property/property_type/PropertyTypeText'; + +interface Props { + field: Field; + onUpdateFieldType: (type: FieldType) => void; +} +function PropertyTypeSelect({ field, onUpdateFieldType }: Props) { + const [expanded, setExpanded] = useState(false); + const ref = useRef(null); + + return ( +
+ { + setExpanded(!expanded); + }} + className={'mx-0 rounded-none px-0'} + > +
+ + + + + +
+
+ {expanded && ( + { + setExpanded(false); + }} + /> + )} +
+ ); +} + +export default PropertyTypeSelect; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeText.tsx new file mode 100644 index 0000000000..daae232fde --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeText.tsx @@ -0,0 +1,27 @@ +import { FieldType } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import { useMemo } from 'react'; + +export const PropertyTypeText = ({ type }: { type: FieldType }) => { + const { t } = useTranslation(); + + const text = useMemo(() => { + const map = { + [FieldType.RichText]: t('grid.field.textFieldName'), + [FieldType.Number]: t('grid.field.numberFieldName'), + [FieldType.DateTime]: t('grid.field.dateFieldName'), + [FieldType.SingleSelect]: t('grid.field.singleSelectFieldName'), + [FieldType.MultiSelect]: t('grid.field.multiSelectFieldName'), + [FieldType.Checkbox]: t('grid.field.checkboxFieldName'), + [FieldType.URL]: t('grid.field.urlFieldName'), + [FieldType.Checklist]: t('grid.field.checklistFieldName'), + [FieldType.LastEditedTime]: t('grid.field.updatedAtFieldName'), + [FieldType.CreatedTime]: t('grid.field.createdAtFieldName'), + [FieldType.Relation]: t('grid.field.relationFieldName'), + }; + + return map[type] || 'unknown'; + }, [t, type]); + + return
{text}
; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx new file mode 100644 index 0000000000..7ee4e6f83d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/ProppertyTypeSvg.tsx @@ -0,0 +1,32 @@ +import { FC, memo } from 'react'; +import { FieldType } from '@/services/backend'; +import { ReactComponent as TextSvg } from '$app/assets/database/field-type-text.svg'; +import { ReactComponent as NumberSvg } from '$app/assets/database/field-type-number.svg'; +import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg'; +import { ReactComponent as SingleSelectSvg } from '$app/assets/database/field-type-single-select.svg'; +import { ReactComponent as MultiSelectSvg } from '$app/assets/database/field-type-multi-select.svg'; +import { ReactComponent as ChecklistSvg } from '$app/assets/database/field-type-checklist.svg'; +import { ReactComponent as CheckboxSvg } from '$app/assets/database/field-type-checkbox.svg'; +import { ReactComponent as URLSvg } from '$app/assets/database/field-type-url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '$app/assets/database/field-type-last-edited-time.svg'; +import { ReactComponent as RelationSvg } from '$app/assets/database/field-type-relation.svg'; + +export const FieldTypeSvgMap: Record>> = { + [FieldType.RichText]: TextSvg, + [FieldType.Number]: NumberSvg, + [FieldType.DateTime]: DateSvg, + [FieldType.SingleSelect]: SingleSelectSvg, + [FieldType.MultiSelect]: MultiSelectSvg, + [FieldType.Checkbox]: CheckboxSvg, + [FieldType.URL]: URLSvg, + [FieldType.Checklist]: ChecklistSvg, + [FieldType.LastEditedTime]: LastEditedTimeSvg, + [FieldType.CreatedTime]: LastEditedTimeSvg, + [FieldType.Relation]: RelationSvg, +}; + +export const ProppertyTypeSvg: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { + const Svg = FieldTypeSvgMap[type]; + + return ; +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx new file mode 100644 index 0000000000..fdb508cb8f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortConditionSelect.tsx @@ -0,0 +1,78 @@ +import { FC, useMemo, useRef, useState } from 'react'; +import { SortConditionPB } from '@/services/backend'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { Popover } from '@mui/material'; +import { ReactComponent as DropDownSvg } from '$app/assets/more.svg'; +import { useTranslation } from 'react-i18next'; + +export const SortConditionSelect: FC<{ + onChange?: (value: SortConditionPB) => void; + value?: SortConditionPB; +}> = ({ onChange, value }) => { + const { t } = useTranslation(); + const ref = useRef(null); + const [open, setOpen] = useState(false); + const handleClose = () => { + setOpen(false); + }; + + const options: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: SortConditionPB.Ascending, + content: t('grid.sort.ascending'), + }, + { + key: SortConditionPB.Descending, + content: t('grid.sort.descending'), + }, + ]; + }, [t]); + + const onConfirm = (optionKey: SortConditionPB) => { + onChange?.(optionKey); + handleClose(); + }; + + const selectedField = useMemo(() => options.find((option) => option.key === value), [options, value]); + + return ( + <> +
{ + setOpen(true); + }} + > +
{selectedField?.content}
+ +
+ {open && ( + +
+ +
+
+ )} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx new file mode 100644 index 0000000000..724c28467a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortFieldsMenu.tsx @@ -0,0 +1,53 @@ +import React, { FC, useCallback } from 'react'; +import { MenuProps } from '@mui/material'; +import PropertiesList from '$app/components/database/components/property/PropertiesList'; +import { Field, sortService } from '$app/application/database'; +import { SortConditionPB } from '@/services/backend'; +import { useTranslation } from 'react-i18next'; +import { useViewId } from '$app/hooks'; +import Popover from '@mui/material/Popover'; + +const SortFieldsMenu: FC< + MenuProps & { + onInserted?: () => void; + } +> = ({ onInserted, ...props }) => { + const { t } = useTranslation(); + const viewId = useViewId(); + const addSort = useCallback( + async (field: Field) => { + await sortService.insertSort(viewId, { + fieldId: field.id, + condition: SortConditionPB.Ascending, + }); + props.onClose?.({}, 'backdropClick'); + onInserted?.(); + }, + [props, viewId, onInserted] + ); + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + keepMounted={false} + {...props} + > + { + props.onClose?.({}, 'escapeKeyDown'); + }} + showSearch={true} + onItemClick={addSort} + searchPlaceholder={t('grid.settings.sortBy')} + /> + + ); +}; + +export default SortFieldsMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx new file mode 100644 index 0000000000..fe1074bbde --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortItem.tsx @@ -0,0 +1,55 @@ +import { IconButton, Stack } from '@mui/material'; +import { FC, useCallback } from 'react'; +import { ReactComponent as CloseSvg } from '$app/assets/close.svg'; +import { Field, Sort, sortService } from '$app/application/database'; +import { PropertySelect } from '../property'; +import { SortConditionSelect } from './SortConditionSelect'; +import { useViewId } from '@/appflowy_app/hooks'; +import { SortConditionPB } from '@/services/backend'; + +export interface SortItemProps { + className?: string; + sort: Sort; +} + +export const SortItem: FC = ({ className, sort }) => { + const viewId = useViewId(); + + const handleFieldChange = useCallback( + (field: Field | undefined) => { + if (field) { + void sortService.updateSort(viewId, { + ...sort, + fieldId: field.id, + }); + } + }, + [viewId, sort] + ); + + const handleConditionChange = useCallback( + (value: SortConditionPB) => { + void sortService.updateSort(viewId, { + ...sort, + condition: value, + }); + }, + [viewId, sort] + ); + + const handleClick = useCallback(() => { + void sortService.deleteSort(viewId, sort); + }, [viewId, sort]); + + return ( + + + +
+ + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx new file mode 100644 index 0000000000..88df70b2e4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/SortMenu.tsx @@ -0,0 +1,90 @@ +import { Menu, MenuProps } from '@mui/material'; +import { FC, MouseEventHandler, useCallback, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { sortService } from '$app/application/database'; +import { useDatabaseSorts } from '../../Database.hooks'; +import { SortItem } from './SortItem'; + +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import SortFieldsMenu from '$app/components/database/components/sort/SortFieldsMenu'; +import Button from '@mui/material/Button'; + +export const SortMenu: FC = (props) => { + const { onClose } = props; + const { t } = useTranslation(); + const viewId = useViewId(); + const sorts = useDatabaseSorts(); + const [anchorEl, setAnchorEl] = useState(null); + const openFieldListMenu = Boolean(anchorEl); + const handleClick = useCallback>((event) => { + setAnchorEl(event.currentTarget); + }, []); + + const deleteAllSorts = useCallback(() => { + void sortService.deleteAllSorts(viewId); + onClose?.({}, 'backdropClick'); + }, [viewId, onClose]); + + return ( + <> + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + props.onClose?.({}, 'escapeKeyDown'); + } + }} + keepMounted={false} + MenuListProps={{ + className: 'py-1 w-[360px]', + }} + {...props} + onClose={onClose} + > +
+
+ {sorts.map((sort) => ( + + ))} +
+ +
+ + +
+
+
+ + { + setAnchorEl(null); + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + /> + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/Sorts.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/Sorts.tsx new file mode 100644 index 0000000000..7a4fa57a6f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/Sorts.tsx @@ -0,0 +1,45 @@ +import { Chip, Divider } from '@mui/material'; +import React, { MouseEventHandler, useCallback, useEffect, useState } from 'react'; +import { SortMenu } from './SortMenu'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as SortSvg } from '$app/assets/sort.svg'; +import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg'; +import { useDatabase } from '$app/components/database'; + +export const Sorts = () => { + const { t } = useTranslation(); + const { sorts } = useDatabase(); + + const showSorts = sorts && sorts.length > 0; + const [anchorEl, setAnchorEl] = useState(null); + + const handleClick = useCallback>((event) => { + setAnchorEl(event.currentTarget); + }, []); + + const label = ( +
+ + {t('grid.settings.sort')} + +
+ ); + + const menuOpen = Boolean(anchorEl); + + useEffect(() => { + if (!showSorts) { + setAnchorEl(null); + } + }, [showSorts]); + + if (!showSorts) return null; + + return ( +
+ + + setAnchorEl(null)} /> +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/index.ts new file mode 100644 index 0000000000..e64dba3a6e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/sort/index.ts @@ -0,0 +1,2 @@ +export * from './SortMenu'; +export * from './Sorts'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx new file mode 100644 index 0000000000..717bf1eb18 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/AddViewBtn.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { IconButton } from '@mui/material'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useTranslation } from 'react-i18next'; +import { ViewLayoutPB } from '@/services/backend'; +import { createDatabaseView } from '$app/application/database/database_view/database_view_service'; + +function AddViewBtn({ pageId, onCreated }: { pageId: string; onCreated: (id: string) => void }) { + const { t } = useTranslation(); + const onClick = async () => { + try { + const view = await createDatabaseView(pageId, ViewLayoutPB.Grid, t('editor.table')); + + onCreated(view.id); + } catch (e) { + console.error(e); + } + }; + + return ( +
+ + + +
+ ); +} + +export default AddViewBtn; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx new file mode 100644 index 0000000000..f7375e0c70 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/DatabaseTabBar.tsx @@ -0,0 +1,110 @@ +import { forwardRef, FunctionComponent, SVGProps, useEffect, useMemo, useState } from 'react'; +import { ViewTabs, ViewTab } from './ViewTabs'; +import { useTranslation } from 'react-i18next'; +import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn'; +import { ViewLayoutPB } from '@/services/backend'; +import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; +import { ReactComponent as BoardSvg } from '$app/assets/board.svg'; +import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; +import ViewActions from '$app/components/database/components/tab_bar/ViewActions'; +import { Page } from '$app_reducers/pages/slice'; + +export interface DatabaseTabBarProps { + childViews: Page[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; + pageId: string; +} + +const DatabaseIcons: { + [key in ViewLayoutPB]: FunctionComponent & { title?: string | undefined }>; +} = { + [ViewLayoutPB.Document]: DocumentSvg, + [ViewLayoutPB.Grid]: GridSvg, + [ViewLayoutPB.Board]: BoardSvg, + [ViewLayoutPB.Calendar]: GridSvg, +}; + +export const DatabaseTabBar = forwardRef( + ({ pageId, childViews, selectedViewId, setSelectedViewId }, ref) => { + const { t } = useTranslation(); + const [contextMenuAnchorEl, setContextMenuAnchorEl] = useState(null); + const [contextMenuView, setContextMenuView] = useState(null); + const open = Boolean(contextMenuAnchorEl); + + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); + }; + + useEffect(() => { + if (selectedViewId === undefined && childViews.length > 0) { + setSelectedViewId?.(childViews[0].id); + } + }, [selectedViewId, setSelectedViewId, childViews]); + + const openMenu = (view: Page) => { + return (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setContextMenuView(view); + setContextMenuAnchorEl(e.currentTarget); + }; + }; + + const isSelected = useMemo( + () => childViews.some((view) => view.id === selectedViewId), + [childViews, selectedViewId] + ); + + if (childViews.length === 0) return null; + return ( +
+
+ + {childViews.map((view) => { + const Icon = DatabaseIcons[view.layout]; + + return ( + } + iconPosition='start' + color='inherit' + label={view.name || t('grid.title.placeholder')} + value={view.id} + /> + ); + })} + + setSelectedViewId?.(id)} /> +
+ {open && contextMenuView && ( + { + setContextMenuAnchorEl(null); + setContextMenuView(null); + }} + /> + )} +
+ ); + } +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/TextButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/TextButton.tsx new file mode 100644 index 0000000000..60dfaa2e53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/TextButton.tsx @@ -0,0 +1,9 @@ +import { Button, ButtonProps, styled } from '@mui/material'; + +export const TextButton = styled(Button)(() => ({ + padding: '2px 6px', + fontSize: '0.75rem', + lineHeight: '1rem', + fontWeight: 400, + minWidth: 'unset', +})); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewActions.tsx new file mode 100644 index 0000000000..be545e51e3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewActions.tsx @@ -0,0 +1,103 @@ +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; +import { deleteView } from '$app/application/database/database_view/database_view_service'; +import { MenuProps, Menu } from '@mui/material'; +import RenameDialog from '$app/components/_shared/confirm_dialog/RenameDialog'; +import { Page } from '$app_reducers/pages/slice'; +import { useAppDispatch } from '$app/stores/store'; +import { updatePageName } from '$app_reducers/pages/async_actions'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; + +enum ViewAction { + Rename, + Delete, +} + +function ViewActions({ view, pageId, ...props }: { pageId: string; view: Page } & MenuProps) { + const { t } = useTranslation(); + const viewId = view.id; + const dispatch = useAppDispatch(); + const [openRenameDialog, setOpenRenameDialog] = useState(false); + const renderContent = useCallback((title: string, Icon: React.FC>) => { + return ( +
+ +
{title}
+
+ ); + }, []); + + const onConfirm = useCallback( + async (key: ViewAction) => { + switch (key) { + case ViewAction.Rename: + setOpenRenameDialog(true); + break; + case ViewAction.Delete: + try { + await deleteView(viewId); + props.onClose?.({}, 'backdropClick'); + } catch (e) { + // toast.error(t('error.deleteView')); + } + + break; + default: + break; + } + }, + [viewId, props] + ); + const options = [ + { + key: ViewAction.Rename, + content: renderContent(t('button.rename'), EditSvg), + }, + + { + key: ViewAction.Delete, + content: renderContent(t('button.delete'), DeleteSvg), + disabled: viewId === pageId, + }, + ]; + + return ( + <> + + { + props.onClose?.({}, 'escapeKeyDown'); + }} + /> + + {openRenameDialog && ( + setOpenRenameDialog(false)} + onOk={async (val) => { + try { + await dispatch( + updatePageName({ + id: viewId, + name: val, + immediate: true, + }) + ); + setOpenRenameDialog(false); + props.onClose?.({}, 'backdropClick'); + } catch (e) { + // toast.error(t('error.renameView')); + } + }} + defaultValue={view.name} + /> + )} + + ); +} + +export default ViewActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx new file mode 100644 index 0000000000..e2ff336c73 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/ViewTabs.tsx @@ -0,0 +1,48 @@ +import { styled, Tab, TabProps, Tabs, TabsProps } from '@mui/material'; +import { HTMLAttributes } from 'react'; + +export const ViewTabs = styled((props: TabsProps) => )({ + minHeight: '28px', + + '& .MuiTabs-scroller': { + paddingBottom: '2px', + }, +}); + +export const ViewTab = styled((props: TabProps) => )({ + padding: '6px 12px', + minHeight: '28px', + fontSize: '12px', + lineHeight: '16px', + minWidth: 'unset', + margin: '4px 0', + + '&.Mui-selected': { + color: 'inherit', + }, +}); + +interface TabPanelProps extends HTMLAttributes { + children?: React.ReactNode; + index: number; + value: number; +} + +export function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + const isActivated = value === index; + + return ( + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/index.ts new file mode 100644 index 0000000000..fc0c62963e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/tab_bar/index.ts @@ -0,0 +1 @@ +export * from './DatabaseTabBar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss new file mode 100644 index 0000000000..492ff2a713 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database.scss @@ -0,0 +1,19 @@ +.database-collection { + ::-webkit-scrollbar { + width: 0px; + height: 0px; + } +} + +.checklist-item { + @apply my-1; + .delete-option-button { + display: none; + } + &:hover, &.selected { + background-color: var(--fill-list-hover); + .delete-option-button { + display: block; + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid.tsx new file mode 100644 index 0000000000..beb90c66dc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/Grid.tsx @@ -0,0 +1,6 @@ +import { FC } from 'react'; +import { GridTable, GridTableProps } from './grid_table'; + +export const Grid: FC = (props) => { + return ; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/constants.ts new file mode 100644 index 0000000000..eadfadaa89 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/constants.ts @@ -0,0 +1,86 @@ +import { Field, RowMeta } from '$app/application/database'; + +export const GridCalculateCountHeight = 40; + +export const GRID_ACTIONS_WIDTH = 64; + +export const DEFAULT_FIELD_WIDTH = 150; + +export enum RenderRowType { + Fields = 'fields', + Row = 'row', + NewRow = 'new-row', + CalculateRow = 'calculate-row', +} + +export interface CalculateRenderRow { + type: RenderRowType.CalculateRow; +} + +export interface FieldRenderRow { + type: RenderRowType.Fields; +} + +export interface CellRenderRow { + type: RenderRowType.Row; + data: { + meta: RowMeta; + }; +} + +export interface NewRenderRow { + type: RenderRowType.NewRow; + data: { + groupId?: string; + }; +} + +export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow; + +export const fieldsToColumns = (fields: Field[]): GridColumn[] => { + return [ + { + type: GridColumnType.Action, + width: GRID_ACTIONS_WIDTH, + }, + ...fields.map((field) => ({ + field, + width: field.width || DEFAULT_FIELD_WIDTH, + type: GridColumnType.Field, + })), + { + type: GridColumnType.NewProperty, + width: DEFAULT_FIELD_WIDTH, + }, + ]; +}; + +export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { + return [ + ...rowMetas.map((rowMeta) => ({ + type: RenderRowType.Row, + data: { + meta: rowMeta, + }, + })), + { + type: RenderRowType.NewRow, + data: {}, + }, + { + type: RenderRowType.CalculateRow, + }, + ]; +}; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +export interface GridColumn { + field?: Field; + width: number; + type: GridColumnType; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/GridCalculate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/GridCalculate.tsx new file mode 100644 index 0000000000..beed71fca4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/GridCalculate.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { useDatabaseVisibilityRows } from '$app/components/database'; +import { Field } from '$app/application/database'; +import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants'; +import { useTranslation } from 'react-i18next'; + +interface Props { + field: Field; + index: number; + getContainerRef?: () => React.RefObject; +} + +export function GridCalculate({ field, index }: Props) { + const rowMetas = useDatabaseVisibilityRows(); + const count = rowMetas.length; + const width = index === 0 ? GRID_ACTIONS_WIDTH : field.width ?? DEFAULT_FIELD_WIDTH; + const { t } = useTranslation(); + + return ( +
+ {field.isPrimary ? ( + <> + {t('grid.calculationTypeLabel.count')} + {count} + + ) : null} +
+ ); +} + +export default GridCalculate; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/index.ts new file mode 100644 index 0000000000..2bd3b71b1e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_calculate/index.ts @@ -0,0 +1 @@ +export * from './GridCalculate'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/GridCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/GridCell.tsx new file mode 100644 index 0000000000..042ba1777d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/GridCell.tsx @@ -0,0 +1,66 @@ +import React, { CSSProperties, memo } from 'react'; +import { GridColumn, RenderRow, RenderRowType } from '../constants'; +import GridNewRow from '$app/components/database/grid/grid_new_row/GridNewRow'; +import { GridCalculate } from '$app/components/database/grid/grid_calculate'; +import { areEqual } from 'react-window'; +import { Cell } from '$app/components/database/components'; +import { PrimaryCell } from '$app/components/database/grid/grid_cell'; + +const getRenderRowKey = (row: RenderRow) => { + if (row.type === RenderRowType.Row) { + return `row:${row.data.meta.id}`; + } + + return row.type; +}; + +interface GridCellProps { + row: RenderRow; + column: GridColumn; + columnIndex: number; + style: CSSProperties; + onEditRecord?: (rowId: string) => void; + getContainerRef?: () => React.RefObject; +} + +export const GridCell = memo(({ row, column, columnIndex, style, onEditRecord, getContainerRef }: GridCellProps) => { + const key = getRenderRowKey(row); + + const field = column.field; + + if (!field) return
; + + switch (row.type) { + case RenderRowType.Row: { + const { id: rowId, icon: rowIcon } = row.data.meta; + const renderRowCell = ; + + return ( +
+ {field.isPrimary ? ( + + {renderRowCell} + + ) : ( + renderRowCell + )} +
+ ); + } + + case RenderRowType.NewRow: + return ( +
+ +
+ ); + case RenderRowType.CalculateRow: + return ( +
+ +
+ ); + default: + return null; + } +}, areEqual); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/PrimaryCell.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/PrimaryCell.tsx new file mode 100644 index 0000000000..b9a734de7b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/PrimaryCell.tsx @@ -0,0 +1,51 @@ +import React, { Suspense, useMemo, useRef } from 'react'; +import { ReactComponent as OpenIcon } from '$app/assets/open.svg'; +import { IconButton } from '@mui/material'; + +import { useGridTableHoverState } from '$app/components/database/grid/grid_row_actions'; + +export function PrimaryCell({ + onEditRecord, + icon, + getContainerRef, + rowId, + children, +}: { + rowId: string; + icon?: string; + onEditRecord?: (rowId: string) => void; + getContainerRef?: () => React.RefObject; + children?: React.ReactNode; +}) { + const cellRef = useRef(null); + + const containerRef = getContainerRef?.(); + const { hoverRowId } = useGridTableHoverState(containerRef); + + const showExpandIcon = useMemo(() => { + return hoverRowId === rowId; + }, [hoverRowId, rowId]); + + return ( +
+ {icon &&
{icon}
} + {children} + + {showExpandIcon && ( +
+ onEditRecord?.(rowId)} className={'h-6 w-6 text-sm'}> + + +
+ )} +
+
+ ); +} + +export default PrimaryCell; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/index.ts new file mode 100644 index 0000000000..949d5054bf --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_cell/index.ts @@ -0,0 +1,2 @@ +export * from './GridCell'; +export * from './PrimaryCell'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridField.tsx new file mode 100644 index 0000000000..3c3921abf7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridField.tsx @@ -0,0 +1,179 @@ +import { Button } from '@mui/material'; +import { DragEventHandler, FC, HTMLAttributes, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { DragItem, DropPosition, DragType, useDraggable, useDroppable, ScrollDirection } from '../../_shared'; +import { fieldService, Field } from '$app/application/database'; +import { Property } from '$app/components/database/components/property'; +import { GridResizer, GridFieldMenu } from '$app/components/database/grid/grid_field'; +import { areEqual } from 'react-window'; +import { useOpenMenu } from '$app/components/database/grid/grid_sticky_header/GridStickyHeader.hooks'; +import throttle from 'lodash-es/throttle'; + +export interface GridFieldProps extends HTMLAttributes { + field: Field; + onOpenMenu?: (id: string) => void; + onCloseMenu?: (id: string) => void; + resizeColumnWidth?: (width: number) => void; + getScrollElement?: () => HTMLElement | null; +} + +export const GridField: FC = memo( + ({ getScrollElement, resizeColumnWidth, onOpenMenu, onCloseMenu, field, ...props }) => { + const menuOpened = useOpenMenu(field.id); + const viewId = useViewId(); + const [propertyMenuOpened, setPropertyMenuOpened] = useState(false); + const [dropPosition, setDropPosition] = useState(DropPosition.Before); + + const draggingData = useMemo( + () => ({ + field, + }), + [field] + ); + + const { isDragging, attributes, listeners, setPreviewRef, previewRef } = useDraggable({ + type: DragType.Field, + data: draggingData, + scrollOnEdge: { + direction: ScrollDirection.Horizontal, + getScrollElement, + edgeGap: 80, + }, + }); + + const onDragOver = useMemo(() => { + return throttle((event) => { + const element = previewRef.current; + + if (!element) { + return; + } + + const { left, right } = element.getBoundingClientRect(); + const middle = (left + right) / 2; + + setDropPosition(event.clientX < middle ? DropPosition.Before : DropPosition.After); + }, 20); + }, [previewRef]); + + const onDrop = useCallback( + ({ data }: DragItem) => { + const dragField = data.field as Field; + + if (dragField.id === field.id) { + return; + } + + void fieldService.moveField(viewId, dragField.id, field.id); + }, + [viewId, field] + ); + + const { isOver, listeners: dropListeners } = useDroppable({ + accept: DragType.Field, + disabled: isDragging, + onDragOver, + onDrop, + }); + + const [menuAnchorPosition, setMenuAnchorPosition] = useState< + | { + top: number; + left: number; + } + | undefined + >(undefined); + + const open = Boolean(menuAnchorPosition) && menuOpened; + + const handleClick = useCallback(() => { + onOpenMenu?.(field.id); + }, [onOpenMenu, field.id]); + + const handleMenuClose = useCallback(() => { + onCloseMenu?.(field.id); + }, [onCloseMenu, field.id]); + + useEffect(() => { + if (!menuOpened) { + setMenuAnchorPosition(undefined); + return; + } + + const anchorElement = previewRef.current; + + if (!anchorElement) { + setMenuAnchorPosition(undefined); + return; + } + + anchorElement.scrollIntoView({ block: 'nearest' }); + + const rect = anchorElement.getBoundingClientRect(); + + setMenuAnchorPosition({ + top: rect.top + rect.height, + left: rect.left, + }); + }, [menuOpened, previewRef]); + + const handlePropertyMenuOpen = useCallback(() => { + setPropertyMenuOpened(true); + }, []); + + const handlePropertyMenuClose = useCallback(() => { + setPropertyMenuOpened(false); + }, []); + + return ( +
+ + {open && ( + + )} +
+ ); + }, + areEqual +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridFieldMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridFieldMenu.tsx new file mode 100644 index 0000000000..1407fe30c2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridFieldMenu.tsx @@ -0,0 +1,59 @@ +import React, { useRef } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { Field } from '$app/application/database'; +import PropertyNameInput from '$app/components/database/components/property/PropertyNameInput'; +import { MenuList } from '@mui/material'; +import PropertyActions, { FieldAction } from '$app/components/database/components/property/PropertyActions'; + +interface Props extends PopoverProps { + field: Field; + onOpenPropertyMenu?: () => void; + onOpenMenu?: (fieldId: string) => void; +} + +export function GridFieldMenu({ field, onOpenPropertyMenu, onOpenMenu, onClose, ...props }: Props) { + const inputRef = useRef(null); + + return ( + e.stopPropagation()} + {...props} + onClose={onClose} + keepMounted={false} + onMouseDown={(e) => { + const isInput = inputRef.current?.contains(e.target as Node); + + if (isInput) return; + + e.stopPropagation(); + e.preventDefault(); + }} + > + + + onClose?.({}, 'backdropClick')} + onMenuItemClick={(action, newFieldId?: string) => { + if (action === FieldAction.EditProperty) { + onOpenPropertyMenu?.(); + } else if (newFieldId && (action === FieldAction.InsertLeft || action === FieldAction.InsertRight)) { + onOpenMenu?.(newFieldId); + } + + onClose?.({}, 'backdropClick'); + }} + fieldId={field.id} + /> + + + ); +} + +export default GridFieldMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridNewField.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridNewField.tsx new file mode 100644 index 0000000000..d0b739298a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridNewField.tsx @@ -0,0 +1,35 @@ +import React, { useCallback } from 'react'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { fieldService } from '$app/application/database'; +import { FieldType } from '@/services/backend'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; + +export function GridNewField({ onInserted }: { onInserted?: (id: string) => void }) { + const viewId = useViewId(); + const { t } = useTranslation(); + + const handleClick = useCallback(async () => { + try { + const field = await fieldService.createField({ + viewId, + fieldType: FieldType.RichText, + }); + + onInserted?.(field.id); + } catch (e) { + // toast.error(t('grid.field.newPropertyFail')); + } + }, [onInserted, viewId]); + + return ( + <> + + + ); +} + +export default GridNewField; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridResizer.tsx new file mode 100644 index 0000000000..12aef74996 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/GridResizer.tsx @@ -0,0 +1,84 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { Field, fieldService } from '$app/application/database'; +import { useViewId } from '$app/hooks'; + +interface GridResizerProps { + field: Field; + onWidthChange?: (width: number) => void; +} + +const minWidth = 150; + +export function GridResizer({ field, onWidthChange }: GridResizerProps) { + const viewId = useViewId(); + const fieldId = field.id; + const width = field.width || 0; + const [isResizing, setIsResizing] = useState(false); + const [hover, setHover] = useState(false); + const startX = useRef(0); + const newWidthRef = useRef(width); + const onResize = useCallback( + (e: MouseEvent) => { + const diff = e.clientX - startX.current; + const newWidth = width + diff; + + if (newWidth < minWidth) { + return; + } + + newWidthRef.current = newWidth; + onWidthChange?.(newWidth); + }, + [width, onWidthChange] + ); + + const onResizeEnd = useCallback(() => { + setIsResizing(false); + + void fieldService.updateFieldSetting(viewId, fieldId, { + width: newWidthRef.current, + }); + document.removeEventListener('mousemove', onResize); + document.removeEventListener('mouseup', onResizeEnd); + }, [fieldId, onResize, viewId]); + + const onResizeStart = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + startX.current = e.clientX; + setIsResizing(true); + document.addEventListener('mousemove', onResize); + document.addEventListener('mouseup', onResizeEnd); + }, + [onResize, onResizeEnd] + ); + + return ( +
{ + e.stopPropagation(); + }} + onMouseEnter={() => { + setHover(true); + }} + onMouseLeave={() => { + setHover(false); + }} + style={{ + right: `-3px`, + }} + className={'absolute top-0 z-10 h-full cursor-col-resize'} + > +
+
+ ); +} + +export default GridResizer; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/index.ts new file mode 100644 index 0000000000..384ee2af3b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_field/index.ts @@ -0,0 +1,4 @@ +export * from './GridField'; +export * from './GridFieldMenu'; +export * from './GridNewField'; +export * from './GridResizer'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_new_row/GridNewRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_new_row/GridNewRow.tsx new file mode 100644 index 0000000000..4dc70e21dc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_new_row/GridNewRow.tsx @@ -0,0 +1,68 @@ +import React, { useCallback } from 'react'; +import { rowService } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useTranslation } from 'react-i18next'; + +interface Props { + index: number; + groupId?: string; + getContainerRef?: () => React.RefObject; +} + +const CSS_HIGHLIGHT_PROPERTY = 'bg-content-blue-50'; + +function GridNewRow({ index, groupId, getContainerRef }: Props) { + const viewId = useViewId(); + + const { t } = useTranslation(); + const handleClick = useCallback(() => { + void rowService.createRow(viewId, { + groupId, + }); + }, [viewId, groupId]); + + const toggleCssProperty = useCallback( + (status: boolean) => { + const container = getContainerRef?.()?.current; + + if (!container) return; + + const newRowCells = container.querySelectorAll('.grid-new-row'); + + newRowCells.forEach((cell) => { + if (status) { + cell.classList.add(CSS_HIGHLIGHT_PROPERTY); + } else { + cell.classList.remove(CSS_HIGHLIGHT_PROPERTY); + } + }); + }, + [getContainerRef] + ); + + return ( +
{ + toggleCssProperty(true); + }} + onMouseLeave={() => { + toggleCssProperty(false); + }} + onClick={handleClick} + className={'grid-new-row flex grow cursor-pointer text-text-title'} + > + + + {t('grid.row.newRow')} + +
+ ); +} + +export default GridNewRow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_overlay/GridTableOverlay.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_overlay/GridTableOverlay.tsx new file mode 100644 index 0000000000..07ece5dec2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_overlay/GridTableOverlay.tsx @@ -0,0 +1,75 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + GridRowContextMenu, + GridRowActions, + useGridTableHoverState, +} from '$app/components/database/grid/grid_row_actions'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; +import { useTranslation } from 'react-i18next'; + +function GridTableOverlay({ + containerRef, + getScrollElement, +}: { + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const [hoverRowTop, setHoverRowTop] = useState(); + + const { t } = useTranslation(); + const [openConfirm, setOpenConfirm] = useState(false); + const [confirmModalProps, setConfirmModalProps] = useState< + | { + onOk: () => Promise; + onCancel: () => void; + } + | undefined + >(undefined); + + const { hoverRowId } = useGridTableHoverState(containerRef); + + const handleOpenConfirm = useCallback((onOk: () => Promise, onCancel: () => void) => { + setOpenConfirm(true); + setConfirmModalProps({ onOk, onCancel }); + }, []); + + useEffect(() => { + const container = containerRef.current; + + if (!container) return; + + const cell = container.querySelector(`[data-key="row:${hoverRowId}"]`); + + if (!cell) return; + const top = (cell as HTMLDivElement).style.top; + + setHoverRowTop(top); + }, [containerRef, hoverRowId]); + + return ( +
+ + + {openConfirm && ( + { + setOpenConfirm(false); + }} + {...confirmModalProps} + /> + )} +
+ ); +} + +export default GridTableOverlay; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.hooks.ts new file mode 100644 index 0000000000..a4251c9ed5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.hooks.ts @@ -0,0 +1,244 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useViewId } from '$app/hooks'; +import { rowService } from '$app/application/database'; +import { autoScrollOnEdge, ScrollDirection } from '$app/components/database/_shared/dnd/utils'; +import { useSortsCount } from '$app/components/database'; +import { deleteAllSorts } from '$app/application/database/sort/sort_service'; + +export function getCellsWithRowId(rowId: string, container: HTMLDivElement) { + return Array.from(container.querySelectorAll(`[data-key^="row:${rowId}"]`)); +} + +const SELECTED_ROW_CSS_PROPERTY = 'bg-content-blue-50'; + +export function toggleProperty( + container: HTMLDivElement, + rowId: string, + status: boolean, + property = SELECTED_ROW_CSS_PROPERTY +) { + const rowColumns = getCellsWithRowId(rowId, container); + + rowColumns.forEach((column, index) => { + if (index === 0) return; + if (status) { + column.classList.add(property); + } else { + column.classList.remove(property); + } + }); +} + +function createVirtualDragElement(rowId: string, container: HTMLDivElement) { + const cells = getCellsWithRowId(rowId, container); + + const cell = cells[0] as HTMLDivElement; + + if (!cell) return null; + + const row = document.createElement('div'); + + row.style.display = 'flex'; + row.style.position = 'absolute'; + row.style.top = cell.style.top; + const left = Number(cell.style.left.split('px')[0]) + 64; + + row.style.left = `${left}px`; + row.style.background = 'var(--content-blue-50)'; + cells.forEach((cell) => { + const node = cell.cloneNode(true) as HTMLDivElement; + + if (!node.classList.contains('grid-cell')) return; + + node.style.top = ''; + node.style.position = ''; + node.style.left = ''; + node.style.width = (cell as HTMLDivElement).style.width; + node.style.height = (cell as HTMLDivElement).style.height; + node.className = 'flex items-center border-r border-b border-divider-line opacity-50'; + row.appendChild(node); + }); + + cell.parentElement?.appendChild(row); + return row; +} + +export function useDraggableGridRow( + rowId: string, + containerRef: React.RefObject, + getScrollElement: () => HTMLDivElement | null, + onOpenConfirm: (onOk: () => Promise, onCancel: () => void) => void +) { + const viewId = useViewId(); + const sortsCount = useSortsCount(); + + const [isDragging, setIsDragging] = useState(false); + const dropRowIdRef = useRef(undefined); + const previewRef = useRef(); + + const onDragStart = useCallback( + (e: React.DragEvent) => { + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.dropEffect = 'move'; + const container = containerRef.current; + + if (container) { + const row = createVirtualDragElement(rowId, container); + + if (row) { + previewRef.current = row; + e.dataTransfer.setDragImage(row, 0, 0); + } + } + + const scrollParent = getScrollElement(); + + if (scrollParent) { + autoScrollOnEdge({ + element: scrollParent, + direction: ScrollDirection.Vertical, + edgeGap: 20, + }); + } + + setIsDragging(true); + }, + [containerRef, rowId, getScrollElement] + ); + + const moveRowTo = useCallback( + async (toRowId: string) => { + return rowService.moveRow(viewId, rowId, toRowId); + }, + [viewId, rowId] + ); + + useEffect(() => { + if (!isDragging) { + if (previewRef.current) { + const row = previewRef.current; + + previewRef.current = undefined; + row?.remove(); + } + + return; + } + + const container = containerRef.current; + + if (!container) { + return; + } + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + const target = e.target as HTMLElement; + const cell = target.closest('[data-key]'); + const rowId = cell?.getAttribute('data-key')?.split(':')[1]; + + const oldRowId = dropRowIdRef.current; + + if (oldRowId) { + toggleProperty(container, oldRowId, false); + } + + if (!rowId) return; + + const rowColumns = getCellsWithRowId(rowId, container); + + dropRowIdRef.current = rowId; + if (!rowColumns.length) return; + + toggleProperty(container, rowId, true); + }; + + const onDragEnd = () => { + const oldRowId = dropRowIdRef.current; + + if (oldRowId) { + toggleProperty(container, oldRowId, false); + } + + dropRowIdRef.current = undefined; + setIsDragging(false); + }; + + const onDrop = async (e: DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + const dropRowId = dropRowIdRef.current; + + toggleProperty(container, rowId, false); + if (dropRowId) { + if (sortsCount > 0) { + onOpenConfirm( + async () => { + await deleteAllSorts(viewId); + await moveRowTo(dropRowId); + }, + () => { + void moveRowTo(dropRowId); + } + ); + } else { + void moveRowTo(dropRowId); + } + + toggleProperty(container, dropRowId, false); + } + + setIsDragging(false); + container.removeEventListener('dragover', onDragOver); + container.removeEventListener('dragend', onDragEnd); + container.removeEventListener('drop', onDrop); + }; + + container.addEventListener('dragover', onDragOver); + container.addEventListener('dragend', onDragEnd); + container.addEventListener('drop', onDrop); + }, [isDragging, containerRef, moveRowTo, onOpenConfirm, rowId, sortsCount, viewId]); + + return { + isDragging, + onDragStart, + }; +} + +export function useGridTableHoverState(containerRef?: React.RefObject) { + const [hoverRowId, setHoverRowId] = useState(undefined); + + useEffect(() => { + const container = containerRef?.current; + + if (!container) return; + const onMouseMove = (e: MouseEvent) => { + const target = e.target as HTMLElement; + const cell = target.closest('[data-key]'); + + if (!cell) { + return; + } + + const hoverRowId = cell.getAttribute('data-key')?.split(':')[1]; + + setHoverRowId(hoverRowId); + }; + + const onMouseLeave = () => { + setHoverRowId(undefined); + }; + + container.addEventListener('mousemove', onMouseMove); + container.addEventListener('mouseleave', onMouseLeave); + + return () => { + container.removeEventListener('mousemove', onMouseMove); + container.removeEventListener('mouseleave', onMouseLeave); + }; + }, [containerRef]); + + return { + hoverRowId, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.tsx new file mode 100644 index 0000000000..f4b39e2561 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowActions.tsx @@ -0,0 +1,135 @@ +import React, { useCallback, useState } from 'react'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants'; +import { rowService } from '$app/application/database'; +import { useViewId } from '$app/hooks'; +import { GridRowDragButton, GridRowMenu, toggleProperty } from '$app/components/database/grid/grid_row_actions'; +import { OrderObjectPositionTypePB } from '@/services/backend'; +import { useSortsCount } from '$app/components/database'; +import { useTranslation } from 'react-i18next'; +import { deleteAllSorts } from '$app/application/database/sort/sort_service'; + +export function GridRowActions({ + rowId, + rowTop, + containerRef, + getScrollElement, + onOpenConfirm, +}: { + onOpenConfirm: (onOk: () => Promise, onCancel: () => void) => void; + rowId?: string; + rowTop?: string; + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const { t } = useTranslation(); + const viewId = useViewId(); + const sortsCount = useSortsCount(); + const [menuRowId, setMenuRowId] = useState(undefined); + const [menuPosition, setMenuPosition] = useState< + | { + top: number; + left: number; + } + | undefined + >(undefined); + + const openMenu = Boolean(menuPosition); + + const handleCloseMenu = useCallback(() => { + setMenuPosition(undefined); + if (containerRef.current && menuRowId) { + toggleProperty(containerRef.current, menuRowId, false); + } + }, [containerRef, menuRowId]); + + const handleInsertRecordBelow = useCallback( + async (rowId: string) => { + await rowService.createRow(viewId, { + position: OrderObjectPositionTypePB.After, + rowId: rowId, + }); + handleCloseMenu(); + }, + [viewId, handleCloseMenu] + ); + + const handleOpenMenu = (e: React.MouseEvent) => { + const target = e.target as HTMLButtonElement; + const rect = target.getBoundingClientRect(); + + if (containerRef.current && rowId) { + toggleProperty(containerRef.current, rowId, true); + } + + setMenuRowId(rowId); + setMenuPosition({ + top: rect.top + rect.height / 2, + left: rect.left + rect.width, + }); + }; + + return ( + <> + {rowId && rowTop && ( +
+ + { + if (sortsCount > 0) { + onOpenConfirm( + async () => { + await deleteAllSorts(viewId); + void handleInsertRecordBelow(rowId); + }, + () => { + void handleInsertRecordBelow(rowId); + } + ); + } else { + void handleInsertRecordBelow(rowId); + } + }} + > + + + + +
+ )} + {menuRowId && ( + + )} + + ); +} + +export default GridRowActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowContextMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowContextMenu.tsx new file mode 100644 index 0000000000..a93188ddc4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowContextMenu.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import GridRowMenu from './GridRowMenu'; +import { toggleProperty } from './GridRowActions.hooks'; + +export function GridRowContextMenu({ + containerRef, + hoverRowId, + onOpenConfirm, +}: { + hoverRowId?: string; + onOpenConfirm: (onOk: () => Promise, onCancel: () => void) => void; + containerRef: React.MutableRefObject; +}) { + const [position, setPosition] = useState<{ left: number; top: number } | undefined>(); + + const [rowId, setRowId] = useState(); + + const isContextMenuOpen = useMemo(() => { + return !!position; + }, [position]); + + const closeContextMenu = useCallback(() => { + setPosition(undefined); + const container = containerRef.current; + + if (!container || !rowId) return; + toggleProperty(container, rowId, false); + // setRowId(undefined); + }, [rowId, containerRef]); + + const openContextMenu = useCallback( + (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + const container = containerRef.current; + + if (!container || !hoverRowId) return; + toggleProperty(container, hoverRowId, true); + setRowId(hoverRowId); + setPosition({ + left: event.clientX, + top: event.clientY, + }); + }, + [containerRef, hoverRowId] + ); + + useEffect(() => { + const container = containerRef.current; + + if (!container) { + return; + } + + container.addEventListener('contextmenu', openContextMenu); + return () => { + container.removeEventListener('contextmenu', openContextMenu); + }; + }, [containerRef, openContextMenu]); + + return rowId ? ( + + ) : null; +} + +export default GridRowContextMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowDragButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowDragButton.tsx new file mode 100644 index 0000000000..0790e48183 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowDragButton.tsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import { useDraggableGridRow } from './GridRowActions.hooks'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { useTranslation } from 'react-i18next'; + +export function GridRowDragButton({ + rowId, + containerRef, + onClick, + getScrollElement, + onOpenConfirm, +}: { + onOpenConfirm: (onOk: () => Promise, onCancel: () => void) => void; + rowId: string; + onClick?: (e: React.MouseEvent) => void; + containerRef: React.MutableRefObject; + getScrollElement: () => HTMLDivElement | null; +}) { + const { t } = useTranslation(); + + const [openTooltip, setOpenTooltip] = useState(false); + const { onDragStart, isDragging } = useDraggableGridRow(rowId, containerRef, getScrollElement, onOpenConfirm); + + useEffect(() => { + if (isDragging) { + setOpenTooltip(false); + } + }, [isDragging]); + + return ( + <> + { + setOpenTooltip(true); + }} + onClose={() => { + setOpenTooltip(false); + }} + placement='top' + disableInteractive={true} + title={t('grid.row.dragAndClick')} + > + + + + + + ); +} + +export default GridRowDragButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowMenu.tsx new file mode 100644 index 0000000000..2190e8739b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/GridRowMenu.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as UpSvg } from '$app/assets/up.svg'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { ReactComponent as DelSvg } from '$app/assets/delete.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { useViewId } from '$app/hooks'; +import { useTranslation } from 'react-i18next'; +import { rowService } from '$app/application/database'; +import { OrderObjectPositionTypePB } from '@/services/backend'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { useSortsCount } from '$app/components/database'; +import { deleteAllSorts } from '$app/application/database/sort/sort_service'; + +enum RowAction { + InsertAbove, + InsertBelow, + Duplicate, + Delete, +} +interface Props extends PopoverProps { + rowId: string; + onOpenConfirm?: (onOk: () => Promise, onCancel: () => void) => void; +} + +export function GridRowMenu({ onOpenConfirm, rowId, onClose, ...props }: Props) { + const viewId = useViewId(); + const sortsCount = useSortsCount(); + + const { t } = useTranslation(); + + const handleInsertRecordBelow = useCallback(() => { + void rowService.createRow(viewId, { + position: OrderObjectPositionTypePB.After, + rowId: rowId, + }); + }, [viewId, rowId]); + + const handleInsertRecordAbove = useCallback(() => { + void rowService.createRow(viewId, { + position: OrderObjectPositionTypePB.Before, + rowId: rowId, + }); + }, [rowId, viewId]); + + const handleDelRow = useCallback(() => { + void rowService.deleteRow(viewId, rowId); + }, [viewId, rowId]); + + const handleDuplicateRow = useCallback(() => { + void rowService.duplicateRow(viewId, rowId); + }, [viewId, rowId]); + + const renderContent = useCallback((title: string, Icon: React.FC>) => { + return ( +
+ +
{title}
+
+ ); + }, []); + + const handleAction = useCallback( + (confirmKey?: RowAction) => { + switch (confirmKey) { + case RowAction.InsertAbove: + handleInsertRecordAbove(); + break; + case RowAction.InsertBelow: + handleInsertRecordBelow(); + break; + case RowAction.Duplicate: + handleDuplicateRow(); + break; + case RowAction.Delete: + handleDelRow(); + break; + default: + break; + } + }, + [handleDelRow, handleDuplicateRow, handleInsertRecordAbove, handleInsertRecordBelow] + ); + + const onConfirm = useCallback( + (key: RowAction) => { + if (sortsCount > 0) { + onOpenConfirm?.( + async () => { + await deleteAllSorts(viewId); + handleAction(key); + }, + () => { + handleAction(key); + } + ); + } else { + handleAction(key); + } + + onClose?.({}, 'backdropClick'); + }, + [handleAction, onClose, onOpenConfirm, sortsCount, viewId] + ); + + const options: KeyboardNavigationOption[] = useMemo( + () => [ + { + key: RowAction.InsertAbove, + content: renderContent(t('grid.row.insertRecordAbove'), UpSvg), + }, + { + key: RowAction.InsertBelow, + content: renderContent(t('grid.row.insertRecordBelow'), AddSvg), + }, + { + key: RowAction.Duplicate, + content: renderContent(t('grid.row.duplicate'), CopySvg), + }, + + { + key: 100, + content:
, + children: [], + }, + { + key: RowAction.Delete, + content: renderContent(t('grid.row.delete'), DelSvg), + }, + ], + [renderContent, t] + ); + + return ( + <> + +
+ { + onClose?.({}, 'escapeKeyDown'); + }} + /> +
+
+ + ); +} + +export default GridRowMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/index.ts new file mode 100644 index 0000000000..fb50b6248c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_row_actions/index.ts @@ -0,0 +1,5 @@ +export * from './GridRowActions.hooks'; +export * from './GridRowActions'; +export * from './GridRowContextMenu'; +export * from './GridRowDragButton'; +export * from './GridRowMenu'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.hooks.ts new file mode 100644 index 0000000000..ac5c0688b9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.hooks.ts @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; + +export const OpenMenuContext = createContext(null); + +export const useOpenMenu = (id: string) => { + const context = useContext(OpenMenuContext); + + return context === id; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.tsx new file mode 100644 index 0000000000..e9d01508b1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_sticky_header/GridStickyHeader.tsx @@ -0,0 +1,112 @@ +import React, { useCallback, useState } from 'react'; +import { GridChildComponentProps, GridOnScrollProps, VariableSizeGrid as Grid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { useGridColumn } from '$app/components/database/grid/grid_table'; +import { GridField } from 'src/appflowy_app/components/database/grid/grid_field'; +import NewProperty from '$app/components/database/components/property/NewProperty'; +import { GridColumn, GridColumnType, RenderRow } from '$app/components/database/grid/constants'; +import { OpenMenuContext } from '$app/components/database/grid/grid_sticky_header/GridStickyHeader.hooks'; + +const GridStickyHeader = React.forwardRef< + Grid | null, + { + columns: GridColumn[]; + getScrollElement?: () => HTMLDivElement | null; + onScroll?: (props: GridOnScrollProps) => void; + } +>(({ onScroll, columns, getScrollElement }, ref) => { + const { columnWidth, resizeColumnWidth } = useGridColumn( + columns, + ref as React.MutableRefObject | null> + ); + + const [openMenuId, setOpenMenuId] = useState(null); + + const handleOpenMenu = useCallback((id: string) => { + setOpenMenuId(id); + }, []); + + const handleCloseMenu = useCallback((id: string) => { + setOpenMenuId((prev) => { + if (prev === id) { + return null; + } + + return prev; + }); + }, []); + + const Cell = useCallback( + ({ columnIndex, style, data }: GridChildComponentProps) => { + const column = data[columnIndex]; + + if (!column || column.type === GridColumnType.Action) return
; + if (column.type === GridColumnType.NewProperty) { + const width = (style.width || 0) as number; + + return ( +
+ +
+ ); + } + + const field = column.field; + + if (!field) return
; + + return ( + resizeColumnWidth(columnIndex, width)} + field={field} + getScrollElement={getScrollElement} + /> + ); + }, + [handleCloseMenu, handleOpenMenu, resizeColumnWidth, getScrollElement] + ); + + return ( + + + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={columnWidth} + ref={ref} + onScroll={onScroll} + itemData={columns} + style={{ overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + + + ); +}); + +export default GridStickyHeader; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.hooks.ts new file mode 100644 index 0000000000..0d676f3bb2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.hooks.ts @@ -0,0 +1,67 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH, GridColumn, RenderRow } from '$app/components/database/grid/constants'; +import { VariableSizeGrid as Grid } from 'react-window'; + +export function useGridRow() { + const rowHeight = useCallback(() => { + return 36; + }, []); + + return { + rowHeight, + }; +} + +export function useGridColumn( + columns: GridColumn[], + ref: React.RefObject | null> +) { + const [columnWidths, setColumnWidths] = useState([]); + + useEffect(() => { + setColumnWidths( + columns.map((field, index) => (index === 0 ? GRID_ACTIONS_WIDTH : field.width || DEFAULT_FIELD_WIDTH)) + ); + ref.current?.resetAfterColumnIndex(0); + }, [columns, ref]); + + const resizeColumnWidth = useCallback( + (index: number, width: number) => { + setColumnWidths((columnWidths) => { + if (columnWidths[index] === width) { + return columnWidths; + } + + const newColumnWidths = [...columnWidths]; + + newColumnWidths[index] = width; + + return newColumnWidths; + }); + + if (ref.current) { + ref.current.resetAfterColumnIndex(index); + } + }, + [ref] + ); + + const columnWidth = useCallback( + (index: number) => { + if (index === 0) return GRID_ACTIONS_WIDTH; + return columnWidths[index] || DEFAULT_FIELD_WIDTH; + }, + [columnWidths] + ); + + return { + columnWidth, + resizeColumnWidth, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.tsx new file mode 100644 index 0000000000..0cd17d6a05 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/GridTable.tsx @@ -0,0 +1,161 @@ +import React, { FC, useCallback, useMemo, useRef } from 'react'; +import { RowMeta } from '$app/application/database'; +import { useDatabaseRendered, useDatabaseVisibilityFields, useDatabaseVisibilityRows } from '../../Database.hooks'; +import { fieldsToColumns, GridColumn, RenderRow, RenderRowType, rowMetasToRenderRow } from '../constants'; +import { CircularProgress } from '@mui/material'; +import { GridChildComponentProps, GridOnScrollProps, VariableSizeGrid as Grid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridCell } from 'src/appflowy_app/components/database/grid/grid_cell'; +import { useGridColumn, useGridRow } from './GridTable.hooks'; +import GridStickyHeader from '$app/components/database/grid/grid_sticky_header/GridStickyHeader'; +import GridTableOverlay from '$app/components/database/grid/grid_overlay/GridTableOverlay'; +import ReactDOM from 'react-dom'; +import { useViewId } from '$app/hooks'; + +export interface GridTableProps { + onEditRecord: (rowId: string) => void; +} + +export const GridTable: FC = React.memo(({ onEditRecord }) => { + const rowMetas = useDatabaseVisibilityRows(); + const fields = useDatabaseVisibilityFields(); + const renderRows = useMemo(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); + const columns = useMemo(() => fieldsToColumns(fields), [fields]); + const ref = useRef< + Grid<{ + columns: GridColumn[]; + renderRows: RenderRow[]; + }> + >(null); + const { columnWidth } = useGridColumn( + columns, + ref as React.MutableRefObject | null> + ); + const viewId = useViewId(); + const { rowHeight } = useGridRow(); + const onRendered = useDatabaseRendered(); + + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = renderRows[rowIndex]; + const column = columns[columnIndex]; + + const field = column.field; + + if (row.type === RenderRowType.Row) { + if (field) { + return `${row.data.meta.id}:${field.id}`; + } + + return `${row.data.meta.id}:${column.type}`; + } + + if (field) { + return `${row.type}:${field.id}`; + } + + return `${row.type}:${column.type}`; + }, + [columns, renderRows] + ); + + const getContainerRef = useCallback(() => { + return containerRef; + }, []); + + const Cell = useCallback( + ({ columnIndex, rowIndex, style, data }: GridChildComponentProps) => { + const row = data.renderRows[rowIndex]; + const column = data.columns[columnIndex]; + + return ( + + ); + }, + [getContainerRef, onEditRecord] + ); + + const staticGrid = useRef | null>(null); + + const onScroll = useCallback(({ scrollLeft, scrollUpdateWasRequested }: GridOnScrollProps) => { + if (!scrollUpdateWasRequested) { + staticGrid.current?.scrollTo({ scrollLeft, scrollTop: 0 }); + } + }, []); + + const onHeaderScroll = useCallback(({ scrollLeft }: GridOnScrollProps) => { + ref.current?.scrollTo({ scrollLeft }); + }, []); + + const containerRef = useRef(null); + const scrollElementRef = useRef(null); + + const getScrollElement = useCallback(() => { + return scrollElementRef.current; + }, []); + + return ( +
+ {fields.length === 0 && ( +
+ +
+ )} +
+ +
+ +
+ + {({ height, width }: { height: number; width: number }) => ( + { + scrollElementRef.current = el; + onRendered(viewId); + }} + innerRef={containerRef} + > + {Cell} + + )} + + {containerRef.current + ? ReactDOM.createPortal( + , + containerRef.current + ) + : null} +
+
+ ); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/index.ts new file mode 100644 index 0000000000..dfdb9b7949 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/grid_table/index.ts @@ -0,0 +1,2 @@ +export * from './GridTable'; +export * from './GridTable.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/index.ts new file mode 100644 index 0000000000..762542e7cb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/grid/index.ts @@ -0,0 +1 @@ +export * from './Grid'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts new file mode 100644 index 0000000000..42a6f31592 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/index.ts @@ -0,0 +1,3 @@ +export * from './Database.hooks'; +export * from './Database'; +export * from './DatabaseTitle'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx new file mode 100644 index 0000000000..079a6fd75f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/Document.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import Editor from '$app/components/editor/Editor'; +import { DocumentHeader } from 'src/appflowy_app/components/document/document_header'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { updatePageName } from '$app_reducers/pages/async_actions'; +import { PageCover } from '$app_reducers/pages/slice'; + +export function Document({ id }: { id: string }) { + const page = useAppSelector((state) => state.pages.pageMap[id]); + + const [cover, setCover] = useState(undefined); + const dispatch = useAppDispatch(); + + const onTitleChange = useCallback( + (newTitle: string) => { + void dispatch( + updatePageName({ + id, + name: newTitle, + }) + ); + }, + [dispatch, id] + ); + + const view = useMemo(() => { + return { + ...page, + cover, + }; + }, [page, cover]); + + useEffect(() => { + return () => { + setCover(undefined); + }; + }, [id]); + + if (!page) return null; + + return ( +
+ +
+
+ +
+
+
+ ); +} + +export default Document; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx new file mode 100644 index 0000000000..f6e8736c54 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx @@ -0,0 +1,60 @@ +import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; +import { Page, PageCover, PageIcon } from '$app_reducers/pages/slice'; +import ViewTitle from '$app/components/_shared/view_title/ViewTitle'; +import { updatePageIcon } from '$app/application/folder/page.service'; + +interface DocumentHeaderProps { + page: Page; + onUpdateCover: (cover?: PageCover) => void; +} + +export function DocumentHeader({ page, onUpdateCover }: DocumentHeaderProps) { + const pageId = page.id; + const ref = useRef(null); + + const [forceHover, setForceHover] = useState(false); + const onUpdateIcon = useCallback( + async (icon: PageIcon) => { + await updatePageIcon(pageId, icon.value ? icon : undefined); + }, + [pageId] + ); + + useEffect(() => { + const parent = ref.current?.parentElement; + + if (!parent) return; + + const documentDom = parent.querySelector('.appflowy-editor') as HTMLElement; + + if (!documentDom) return; + + const handleMouseMove = (e: MouseEvent) => { + const isMoveInTitle = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-title')); + const isMoveInHeader = Boolean(e.target instanceof HTMLElement && e.target.closest('.document-header')); + + setForceHover(isMoveInTitle || isMoveInHeader); + }; + + documentDom.addEventListener('mousemove', handleMouseMove); + return () => { + documentDom.removeEventListener('mousemove', handleMouseMove); + }; + }, []); + + if (!page) return null; + return ( +
+ +
+ ); +} + +export default memo(DocumentHeader); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/index.ts new file mode 100644 index 0000000000..00f48716bf --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/index.ts @@ -0,0 +1 @@ +export * from './DocumentHeader'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/document/index.ts new file mode 100644 index 0000000000..a844aa51ad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/index.ts @@ -0,0 +1 @@ +export * from './Document'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.hooks.ts new file mode 100644 index 0000000000..1fc25346d2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.hooks.ts @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; + +export const EditorIdContext = createContext(''); + +export const EditorIdProvider = EditorIdContext.Provider; + +export function useEditorId() { + return useContext(EditorIdContext); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.tsx new file mode 100644 index 0000000000..879dc5f9c0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/Editor.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react'; +import { EditorProps } from '../../application/document/document.types'; + +import { CollaborativeEditor } from '$app/components/editor/components/editor'; +import { EditorIdProvider } from '$app/components/editor/Editor.hooks'; +import './editor.scss'; +import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; + +export function Editor(props: EditorProps) { + return ( +
+ + + +
+ ); +} + +export default withErrorBoundary(memo(Editor)); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts new file mode 100644 index 0000000000..04a2e7c0f1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/formula.ts @@ -0,0 +1,95 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Element, Element as SlateElement, NodeEntry, Range, Transforms } from 'slate'; +import { EditorInlineNodeType, FormulaNode } from '$app/application/document/document.types'; + +export function insertFormula(editor: ReactEditor, formula?: string) { + if (editor.selection) { + wrapFormula(editor, formula); + } +} + +export function updateFormula(editor: ReactEditor, formula: string) { + if (isFormulaActive(editor)) { + Transforms.delete(editor); + wrapFormula(editor, formula); + } +} + +export function deleteFormula(editor: ReactEditor) { + if (isFormulaActive(editor)) { + Transforms.delete(editor); + } +} + +export function wrapFormula(editor: ReactEditor, formula?: string) { + if (isFormulaActive(editor)) { + unwrapFormula(editor); + } + + const { selection } = editor; + + if (!selection) return; + const isCollapsed = selection && Range.isCollapsed(selection); + + const data = formula || editor.string(selection); + const formulaElement = { + type: EditorInlineNodeType.Formula, + data, + children: [ + { + text: '$', + }, + ], + }; + + if (!isCollapsed) { + Transforms.delete(editor); + } + + Transforms.insertNodes(editor, formulaElement, { + select: true, + }); + + const path = editor.selection?.anchor.path; + + if (path) { + editor.select(path); + } +} + +export function unwrapFormula(editor: ReactEditor) { + const [match] = Editor.nodes(editor, { + match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === EditorInlineNodeType.Formula, + }); + + if (!match) return; + + const [node, path] = match as NodeEntry; + const formula = node.data; + const range = Editor.range(editor, match[1]); + const beforePoint = Editor.before(editor, path, { unit: 'character' }); + + Transforms.select(editor, range); + Transforms.delete(editor); + + Transforms.insertText(editor, formula); + + if (!beforePoint) return; + Transforms.select(editor, { + anchor: beforePoint, + focus: { + ...beforePoint, + offset: beforePoint.offset + formula.length, + }, + }); +} + +export function isFormulaActive(editor: ReactEditor) { + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula; + }, + }); + + return Boolean(match); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts new file mode 100644 index 0000000000..557b91f936 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts @@ -0,0 +1,715 @@ +import { ReactEditor } from 'slate-react'; +import { + Editor, + Element, + Node, + NodeEntry, + Point, + Range, + Transforms, + Location, + Path, + EditorBeforeOptions, + Text, + addMark, +} from 'slate'; +import { LIST_TYPES, tabBackward, tabForward } from '$app/components/editor/command/tab'; +import { getAllMarks, isMarkActive, removeMarks, toggleMark } from '$app/components/editor/command/mark'; +import { + deleteFormula, + insertFormula, + isFormulaActive, + unwrapFormula, + updateFormula, +} from '$app/components/editor/command/formula'; +import { + EditorInlineNodeType, + EditorNodeType, + CalloutNode, + Mention, + TodoListNode, + ToggleListNode, + inlineNodeTypes, + FormulaNode, + ImageNode, + EditorMarkFormat, +} from '$app/application/document/document.types'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import { YjsEditor } from '@slate-yjs/core'; + +export const EmbedTypes: string[] = [ + EditorNodeType.DividerBlock, + EditorNodeType.EquationBlock, + EditorNodeType.GridBlock, + EditorNodeType.ImageBlock, +]; + +export const CustomEditor = { + getBlock: (editor: ReactEditor, at?: Location): NodeEntry | undefined => { + return Editor.above(editor, { + at, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + }, + + isInlineNode: (editor: ReactEditor, point: Point): boolean => { + return Boolean( + editor.above({ + at: point, + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && inlineNodeTypes.includes(n.type as EditorInlineNodeType); + }, + }) + ); + }, + + beforeIsInlineNode: (editor: ReactEditor, at: Location, opts?: EditorBeforeOptions): boolean => { + const beforePoint = Editor.before(editor, at, opts); + + if (!beforePoint) return false; + return CustomEditor.isInlineNode(editor, beforePoint); + }, + + afterIsInlineNode: (editor: ReactEditor, at: Location, opts?: EditorBeforeOptions): boolean => { + const afterPoint = Editor.after(editor, at, opts); + + if (!afterPoint) return false; + return CustomEditor.isInlineNode(editor, afterPoint); + }, + + /** + * judge if the selection is multiple block + * @param editor + * @param filterEmptyEndSelection if the filterEmptyEndSelection is true, the function will filter the empty end selection + */ + isMultipleBlockSelected: (editor: ReactEditor, filterEmptyEndSelection?: boolean): boolean => { + const { selection } = editor; + + if (!selection) return false; + + if (Range.isCollapsed(selection)) return false; + const start = Range.start(selection); + const end = Range.end(selection); + const isBackward = Range.isBackward(selection); + const startBlock = CustomEditor.getBlock(editor, start); + const endBlock = CustomEditor.getBlock(editor, end); + + if (!startBlock || !endBlock) return false; + + const [, startPath] = startBlock; + const [, endPath] = endBlock; + + const isSomePath = Path.equals(startPath, endPath); + + // if the start and end path is the same, return false + if (isSomePath) { + return false; + } + + if (!filterEmptyEndSelection) { + return true; + } + + // The end point is at the start of the end block + const focusEndStart = Point.equals(end, editor.start(endPath)); + + if (!focusEndStart) { + return true; + } + + // find the previous block + const previous = editor.previous({ + at: endPath, + match: (n) => Element.isElement(n) && n.blockId !== undefined, + }); + + if (!previous) { + return true; + } + + // backward selection + const newEnd = editor.end(editor.range(previous[1])); + + editor.select({ + anchor: isBackward ? newEnd : start, + focus: isBackward ? start : newEnd, + }); + + return false; + }, + + /** + * turn the current block to a new block + * 1. clone the current block to a new block + * 2. lift the children of the current block if the current block doesn't allow has children + * 3. remove the old block + * 4. insert the new block + * @param editor + * @param newProperties + */ + turnToBlock: (editor: ReactEditor, newProperties: Partial) => { + const selection = editor.selection; + + if (!selection) return; + const match = CustomEditor.getBlock(editor); + + if (!match) return; + + const [node, path] = match as NodeEntry; + + const cloneNode = CustomEditor.cloneBlock(editor, node); + + Object.assign(cloneNode, newProperties); + cloneNode.data = { + ...(node.data || {}), + ...(newProperties.data || {}), + }; + + const isEmbed = editor.isEmbed(cloneNode); + + if (isEmbed) { + editor.splitNodes({ + always: true, + }); + cloneNode.children = []; + + Transforms.removeNodes(editor, { + at: path, + }); + Transforms.insertNodes(editor, cloneNode, { at: path }); + return cloneNode; + } + + const isListType = LIST_TYPES.includes(cloneNode.type as EditorNodeType); + + // if node doesn't allow has children, lift the children before insert the new node and remove the old node + if (!isListType) { + const [textNode, ...children] = cloneNode.children; + + const length = children.length; + + for (let i = 0; i < length; i++) { + editor.liftNodes({ + at: [...path, length - i], + }); + } + + cloneNode.children = [textNode]; + } + + Transforms.removeNodes(editor, { + at: path, + }); + + Transforms.insertNodes(editor, cloneNode, { at: path }); + if (selection) { + editor.select(selection); + } + + return cloneNode; + }, + tabForward, + tabBackward, + toggleMark, + removeMarks, + isMarkActive, + isFormulaActive, + insertFormula, + updateFormula, + deleteFormula, + toggleFormula: (editor: ReactEditor) => { + if (isFormulaActive(editor)) { + unwrapFormula(editor); + } else { + insertFormula(editor); + } + }, + + isBlockActive(editor: ReactEditor, format?: string) { + const match = CustomEditor.getBlock(editor); + + if (match && format !== undefined) { + return match[0].type === format; + } + + return !!match; + }, + + toggleAlign(editor: ReactEditor, format: string) { + const isIncludeRoot = CustomEditor.selectionIncludeRoot(editor); + + if (isIncludeRoot) return; + + const matchNodes = Array.from( + Editor.nodes(editor, { + // Note: we need to select the text node instead of the element node, otherwise the parent node will be selected + match: (n) => Element.isElement(n) && n.type === EditorNodeType.Text, + }) + ); + + if (!matchNodes) return; + + matchNodes.forEach((match) => { + const [, textPath] = match as NodeEntry; + const [node] = editor.parent(textPath) as NodeEntry< + Element & { + data: { + align?: string; + }; + } + >; + const path = ReactEditor.findPath(editor, node); + + const data = (node.data as { align?: string }) || {}; + const newProperties = { + data: { + ...data, + align: data.align === format ? undefined : format, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }); + }, + + getAlign(editor: ReactEditor) { + const match = CustomEditor.getBlock(editor); + + if (!match) return undefined; + + const [node] = match as NodeEntry; + + return (node.data as { align?: string })?.align; + }, + + isInlineActive(editor: ReactEditor) { + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && inlineNodeTypes.includes(n.type as EditorInlineNodeType); + }, + }); + + return !!match; + }, + + formulaActiveNode(editor: ReactEditor) { + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Formula; + }, + }); + + return match ? (match as NodeEntry) : undefined; + }, + + isMentionActive(editor: ReactEditor) { + const [match] = editor.nodes({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorInlineNodeType.Mention; + }, + }); + + return Boolean(match); + }, + + insertMention(editor: ReactEditor, mention: Mention) { + const mentionElement = [ + { + type: EditorInlineNodeType.Mention, + children: [{ text: '$' }], + data: { + ...mention, + }, + }, + ]; + + Transforms.insertNodes(editor, mentionElement, { + select: true, + }); + + editor.collapse({ + edge: 'end', + }); + }, + + toggleTodo(editor: ReactEditor, at?: Location) { + const selection = at || editor.selection; + + if (!selection) return; + + const nodes = Array.from( + editor.nodes({ + at: selection, + match: (n) => Element.isElement(n) && n.type === EditorNodeType.TodoListBlock, + }) + ); + + const matchUnChecked = nodes.some(([node]) => { + return !(node as TodoListNode).data.checked; + }); + + const checked = Boolean(matchUnChecked); + + nodes.forEach(([node, path]) => { + const data = (node as TodoListNode).data || {}; + const newProperties = { + data: { + ...data, + checked: checked, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }); + }, + + toggleToggleList(editor: ReactEditor, node: ToggleListNode) { + const collapsed = node.data.collapsed; + const path = ReactEditor.findPath(editor, node); + const data = node.data || {}; + const newProperties = { + data: { + ...data, + collapsed: !collapsed, + }, + } as Partial; + + const selectMatch = Editor.above(editor, { + match: (n) => Element.isElement(n) && n.blockId !== undefined, + }); + + Transforms.setNodes(editor, newProperties, { at: path }); + + if (selectMatch) { + const [selectNode] = selectMatch; + const selectNodePath = ReactEditor.findPath(editor, selectNode); + + if (Path.isAncestor(path, selectNodePath)) { + editor.select(path); + editor.collapse({ + edge: 'start', + }); + } + } + }, + + setCalloutIcon(editor: ReactEditor, node: CalloutNode, newIcon: string) { + const path = ReactEditor.findPath(editor, node); + const data = node.data || {}; + const newProperties = { + data: { + ...data, + icon: newIcon, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + + setMathEquationBlockFormula(editor: ReactEditor, node: Element, newFormula: string) { + const path = ReactEditor.findPath(editor, node); + const data = node.data || {}; + const newProperties = { + data: { + ...data, + formula: newFormula, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + + setGridBlockViewId(editor: ReactEditor, node: Element, newViewId: string) { + const path = ReactEditor.findPath(editor, node); + const data = node.data || {}; + const newProperties = { + data: { + ...data, + viewId: newViewId, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + + setImageBlockData(editor: ReactEditor, node: Element, newData: ImageNode['data']) { + const path = ReactEditor.findPath(editor, node); + const data = node.data || {}; + const newProperties = { + data: { + ...data, + ...newData, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + + cloneBlock(editor: ReactEditor, block: Element): Element { + const cloneNode: Element = { + ...cloneDeep(block), + blockId: generateId(), + type: block.type === EditorNodeType.Page ? EditorNodeType.Paragraph : block.type, + children: [], + }; + const isEmbed = editor.isEmbed(cloneNode); + + if (isEmbed) { + return cloneNode; + } + + const [firstTextNode, ...children] = block.children as Element[]; + + const textNode = + firstTextNode && firstTextNode.type === EditorNodeType.Text + ? { + textId: generateId(), + type: EditorNodeType.Text, + children: cloneDeep(firstTextNode.children), + } + : undefined; + + if (textNode) { + cloneNode.children.push(textNode); + } + + const cloneChildren = children.map((child) => { + return CustomEditor.cloneBlock(editor, child); + }); + + cloneNode.children.push(...cloneChildren); + + return cloneNode; + }, + + duplicateNode(editor: ReactEditor, node: Element) { + const cloneNode = CustomEditor.cloneBlock(editor, node); + + const path = ReactEditor.findPath(editor, node); + + const nextPath = Path.next(path); + + Transforms.insertNodes(editor, cloneNode, { at: nextPath }); + return cloneNode; + }, + + deleteNode(editor: ReactEditor, node: Node) { + const path = ReactEditor.findPath(editor, node); + + Transforms.removeNodes(editor, { + at: path, + }); + editor.collapse({ + edge: 'start', + }); + }, + + getBlockType: (editor: ReactEditor) => { + const match = CustomEditor.getBlock(editor); + + if (!match) return null; + + const [node] = match as NodeEntry; + + return node.type as EditorNodeType; + }, + + selectionIncludeRoot: (editor: ReactEditor) => { + const [match] = Editor.nodes(editor, { + match: (n) => Element.isElement(n) && n.blockId !== undefined && n.type === EditorNodeType.Page, + }); + + return Boolean(match); + }, + + isCodeBlock: (editor: ReactEditor) => { + return CustomEditor.getBlockType(editor) === EditorNodeType.CodeBlock; + }, + + insertEmptyLine: (editor: ReactEditor & YjsEditor, path: Path) => { + editor.insertNode( + { + type: EditorNodeType.Paragraph, + data: {}, + blockId: generateId(), + children: [ + { + type: EditorNodeType.Text, + textId: generateId(), + children: [ + { + text: '', + }, + ], + }, + ], + }, + { + select: true, + at: path, + } + ); + ReactEditor.focus(editor); + Transforms.move(editor); + }, + + insertEmptyLineAtEnd: (editor: ReactEditor & YjsEditor) => { + CustomEditor.insertEmptyLine(editor, [editor.children.length]); + }, + + focusAtStartOfBlock(editor: ReactEditor) { + const { selection } = editor; + + if (selection && Range.isCollapsed(selection)) { + const match = CustomEditor.getBlock(editor); + const [, path] = match as NodeEntry; + const start = Editor.start(editor, path); + + return match && Point.equals(selection.anchor, start); + } + + return false; + }, + + setBlockColor( + editor: ReactEditor, + node: Element, + data: { + font_color?: string; + bg_color?: string; + } + ) { + const path = ReactEditor.findPath(editor, node); + + const nodeData = node.data || {}; + const newProperties = { + data: { + ...nodeData, + ...data, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + editor.select(path); + }, + + deleteAllText(editor: ReactEditor, node: Element) { + const [textNode] = (node.children || []) as Element[]; + const hasTextNode = textNode && textNode.type === EditorNodeType.Text; + + if (!hasTextNode) return; + const path = ReactEditor.findPath(editor, textNode); + const textLength = editor.string(path).length; + const start = Editor.start(editor, path); + + for (let i = 0; i < textLength; i++) { + editor.select(start); + editor.deleteForward('character'); + } + }, + + getNodeText: (editor: ReactEditor, node: Element) => { + const [textNode] = (node.children || []) as Element[]; + const hasTextNode = textNode && textNode.type === EditorNodeType.Text; + + if (!hasTextNode) return ''; + + const path = ReactEditor.findPath(editor, textNode); + + return editor.string(path); + }, + + isEmptyText: (editor: ReactEditor, node: Element) => { + const [textNode] = (node.children || []) as Element[]; + const hasTextNode = textNode && textNode.type === EditorNodeType.Text; + + if (!hasTextNode) return false; + + return editor.isEmpty(textNode); + }, + + includeInlineBlocks: (editor: ReactEditor) => { + const [match] = Editor.nodes(editor, { + match: (n) => Element.isElement(n) && editor.isInline(n), + }); + + return Boolean(match); + }, + + getNodeTextContent(node: Node): string { + if (Element.isElement(node) && node.type === EditorInlineNodeType.Formula) { + return (node as FormulaNode).data || ''; + } + + if (Text.isText(node)) { + return node.text || ''; + } + + return node.children.map((n) => CustomEditor.getNodeTextContent(n)).join(''); + }, + + isEmbedNode(node: Element): boolean { + return EmbedTypes.includes(node.type); + }, + + getListLevel(editor: ReactEditor, type: EditorNodeType, path: Path) { + let level = 0; + let currentPath = path; + + while (currentPath.length > 0) { + const parent = editor.parent(currentPath); + + if (!parent) { + break; + } + + const [parentNode, parentPath] = parent as NodeEntry; + + if (parentNode.type !== type) { + break; + } + + level += 1; + currentPath = parentPath; + } + + return level; + }, + + getLinks(editor: ReactEditor): string[] { + const marks = getAllMarks(editor); + + if (!marks) return []; + + return Object.entries(marks) + .filter(([key]) => key === 'href') + .map(([_, val]) => val as string); + }, + + extendLineBackward(editor: ReactEditor) { + Transforms.move(editor, { + unit: 'line', + edge: 'focus', + reverse: true, + }); + }, + + extendLineForward(editor: ReactEditor) { + Transforms.move(editor, { unit: 'line', edge: 'focus' }); + }, + + insertPlainText(editor: ReactEditor, text: string) { + const [appendText, ...lines] = text.split('\n'); + + editor.insertText(appendText); + lines.forEach((line) => { + editor.insertBreak(); + editor.insertText(line); + }); + }, + + highlight(editor: ReactEditor) { + addMark(editor, EditorMarkFormat.BgColor, 'appflowy_them_color_tint5'); + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts new file mode 100644 index 0000000000..649eaca564 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts @@ -0,0 +1,137 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Text, Range, Element } from 'slate'; +import { EditorInlineNodeType, EditorMarkFormat } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command/index'; + +export function toggleMark( + editor: ReactEditor, + mark: { + key: EditorMarkFormat; + value: string | boolean; + } +) { + if (CustomEditor.selectionIncludeRoot(editor)) { + return; + } + + const { key, value } = mark; + + const isActive = isMarkActive(editor, key); + + if (isActive || !value) { + Editor.removeMark(editor, key as string); + } else if (value) { + Editor.addMark(editor, key as string, value); + } +} + +/** + * Check if the every text in the selection has the mark. + * @param editor + * @param format + */ +export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | EditorInlineNodeType) { + const selection = editor.selection; + + if (!selection) return false; + + const isExpanded = Range.isExpanded(selection); + + if (isExpanded) { + const texts = getSelectionTexts(editor); + + return texts.every((node) => { + const { text, ...attributes } = node; + + if (!text) return true; + return Boolean((attributes as Record)[format]); + }); + } + + const marks = Editor.marks(editor) as Record | null; + + return marks ? !!marks[format] : false; +} + +export function getSelectionTexts(editor: ReactEditor) { + const selection = editor.selection; + + if (!selection) return []; + + const texts: Text[] = []; + + const isExpanded = Range.isExpanded(selection); + + if (isExpanded) { + let anchor = Range.start(selection); + const focus = Range.end(selection); + const isEnd = Editor.isEnd(editor, anchor, anchor.path); + + if (isEnd) { + const after = Editor.after(editor, anchor); + + if (after) { + anchor = after; + } + } + + Array.from( + Editor.nodes(editor, { + at: { + anchor, + focus, + }, + }) + ).forEach((match) => { + const node = match[0] as Element; + + if (Text.isText(node)) { + texts.push(node); + } else if (Editor.isInline(editor, node)) { + texts.push(...(node.children as Text[])); + } + }); + } + + return texts; +} + +/** + * Get all marks in the current selection. + * @param editor + */ +export function getAllMarks(editor: ReactEditor) { + const selection = editor.selection; + + if (!selection) return null; + + const isExpanded = Range.isExpanded(selection); + + if (isExpanded) { + const texts = getSelectionTexts(editor); + + const marks: Record = {}; + + texts.forEach((node) => { + Object.entries(node).forEach(([key, value]) => { + if (key !== 'text') { + marks[key] = value; + } + }); + }); + + return marks; + } + + return Editor.marks(editor) as Record | null; +} + +export function removeMarks(editor: ReactEditor) { + const marks = getAllMarks(editor); + + if (!marks) return; + + for (const key in marks) { + Editor.removeMark(editor, key); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/tab.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/tab.ts new file mode 100644 index 0000000000..819596f92f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/tab.ts @@ -0,0 +1,82 @@ +import { Path, Element, NodeEntry } from 'slate'; +import { ReactEditor } from 'slate-react'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command/index'; + +export const LIST_TYPES = [ + EditorNodeType.NumberedListBlock, + EditorNodeType.BulletedListBlock, + EditorNodeType.TodoListBlock, + EditorNodeType.ToggleListBlock, + EditorNodeType.QuoteBlock, + EditorNodeType.Paragraph, +]; + +/** + * Indent the current list item + * Conditions: + * 1. The current node must be a list item + * 2. The previous node must be a list + * 3. The previous node must be the same level as the current node + * Result: + * 1. The current node will be the child of the previous node + * 2. The current node will be indented + * 3. The children of the current node will be moved to the children of the previous node + * @param editor + */ +export function tabForward(editor: ReactEditor) { + const match = CustomEditor.getBlock(editor); + + if (!match) return; + + const [node, path] = match as NodeEntry; + + const hasPrevious = Path.hasPrevious(path); + + if (!hasPrevious) return; + + const previousPath = Path.previous(path); + + const previous = editor.node(previousPath); + const [previousNode] = previous as NodeEntry; + + if (!previousNode) return; + + const type = previousNode.type as EditorNodeType; + + if (type === EditorNodeType.Page) return; + // the previous node is not a list + if (!LIST_TYPES.includes(type)) return; + + const toPath = [...previousPath, previousNode.children.length]; + + editor.moveNodes({ + at: path, + to: toPath, + }); + + const length = node.children.length; + + for (let i = length - 1; i > 0; i--) { + editor.liftNodes({ + at: [...toPath, i], + }); + } +} + +export function tabBackward(editor: ReactEditor) { + const match = CustomEditor.getBlock(editor); + + if (!match) return; + + const [node, path] = match as NodeEntry; + + const depth = path.length; + + if (node.type === EditorNodeType.Page) return; + + if (depth === 1) return; + editor.liftNodes({ + at: path, + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/Placeholder.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/Placeholder.tsx new file mode 100644 index 0000000000..d9a60f09ad --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/Placeholder.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Element } from 'slate'; +import PlaceholderContent from '$app/components/editor/components/blocks/_shared/PlaceholderContent'; + +function Placeholder({ node, isEmpty }: { node: Element; isEmpty: boolean }) { + if (!isEmpty) { + return null; + } + + return ; +} + +export default React.memo(Placeholder); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx new file mode 100644 index 0000000000..91645e0051 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx @@ -0,0 +1,130 @@ +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; +import { ReactEditor, useSelected, useSlate } from 'slate-react'; +import { Editor, Element, Range } from 'slate'; +import { EditorNodeType, HeadingNode } from '$app/application/document/document.types'; +import { useTranslation } from 'react-i18next'; + +function PlaceholderContent({ node, ...attributes }: { node: Element; className?: string; style?: CSSProperties }) { + const { t } = useTranslation(); + const editor = useSlate(); + const selected = useSelected() && !!editor.selection && Range.isCollapsed(editor.selection); + const [isComposing, setIsComposing] = useState(false); + const block = useMemo(() => { + const path = ReactEditor.findPath(editor, node); + const match = Editor.above(editor, { + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined && n.type !== undefined; + }, + at: path, + }); + + if (!match) return null; + + return match[0] as Element; + }, [editor, node]); + + const className = useMemo(() => { + return `text-placeholder select-none ${attributes.className ?? ''}`; + }, [attributes.className]); + + const unSelectedPlaceholder = useMemo(() => { + switch (block?.type) { + case EditorNodeType.Paragraph: { + if (editor.children.length === 1) { + return t('editor.slashPlaceHolder'); + } + + return ''; + } + + case EditorNodeType.ToggleListBlock: + return t('blockPlaceholders.bulletList'); + case EditorNodeType.QuoteBlock: + return t('blockPlaceholders.quote'); + case EditorNodeType.TodoListBlock: + return t('blockPlaceholders.todoList'); + case EditorNodeType.NumberedListBlock: + return t('blockPlaceholders.numberList'); + case EditorNodeType.BulletedListBlock: + return t('blockPlaceholders.bulletList'); + case EditorNodeType.HeadingBlock: { + const level = (block as HeadingNode).data.level; + + switch (level) { + case 1: + return t('editor.mobileHeading1'); + case 2: + return t('editor.mobileHeading2'); + case 3: + return t('editor.mobileHeading3'); + default: + return ''; + } + } + + case EditorNodeType.Page: + return t('document.title.placeholder'); + case EditorNodeType.CalloutBlock: + case EditorNodeType.CodeBlock: + return t('editor.typeSomething'); + default: + return ''; + } + }, [block, t, editor.children.length]); + + const selectedPlaceholder = useMemo(() => { + switch (block?.type) { + case EditorNodeType.HeadingBlock: + return unSelectedPlaceholder; + case EditorNodeType.Page: + return t('document.title.placeholder'); + case EditorNodeType.GridBlock: + case EditorNodeType.EquationBlock: + case EditorNodeType.CodeBlock: + case EditorNodeType.DividerBlock: + return ''; + + default: + return t('editor.slashPlaceHolder'); + } + }, [block?.type, t, unSelectedPlaceholder]); + + useEffect(() => { + if (!selected) return; + + const handleCompositionStart = () => { + setIsComposing(true); + }; + + const handleCompositionEnd = () => { + setIsComposing(false); + }; + + const editorDom = ReactEditor.toDOMNode(editor, editor); + + // placeholder should be hidden when composing + editorDom.addEventListener('compositionstart', handleCompositionStart); + editorDom.addEventListener('compositionend', handleCompositionEnd); + editorDom.addEventListener('compositionupdate', handleCompositionStart); + return () => { + editorDom.removeEventListener('compositionstart', handleCompositionStart); + editorDom.removeEventListener('compositionend', handleCompositionEnd); + editorDom.removeEventListener('compositionupdate', handleCompositionStart); + }; + }, [editor, selected]); + + if (isComposing) { + return null; + } + + return ( + + ); +} + +export default PlaceholderContent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/unSupportBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/unSupportBlock.tsx new file mode 100644 index 0000000000..9e9e4fcb38 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/unSupportBlock.tsx @@ -0,0 +1,12 @@ +import React, { forwardRef } from 'react'; +import { Alert } from '@mui/material'; + +export const UnSupportBlock = forwardRef((_, ref) => { + return ( +
+ +
+ ); +}); + +export default UnSupportBlock; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedList.tsx new file mode 100644 index 0000000000..41fce1c9dc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedList.tsx @@ -0,0 +1,14 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, BulletedListNode } from '$app/application/document/document.types'; + +export const BulletedList = memo( + forwardRef>( + ({ node: _, children, className, ...attributes }, ref) => { + return ( +
+ {children} +
+ ); + } + ) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx new file mode 100644 index 0000000000..ea0de80f55 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx @@ -0,0 +1,51 @@ +import React, { useMemo } from 'react'; +import { BulletedListNode } from '$app/application/document/document.types'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; + +enum Letter { + Disc, + Circle, + Square, +} + +function BulletedListIcon({ block, className }: { block: BulletedListNode; className: string }) { + const staticEditor = useSlateStatic(); + const path = ReactEditor.findPath(staticEditor, block); + + const letter = useMemo(() => { + const level = CustomEditor.getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Disc; + } else if (level % 3 === 1) { + return Letter.Circle; + } else { + return Letter.Square; + } + }, [block.type, staticEditor, path]); + + const dataLetter = useMemo(() => { + switch (letter) { + case Letter.Disc: + return '•'; + case Letter.Circle: + return '◦'; + case Letter.Square: + return '▪'; + } + }, [letter]); + + return ( + { + e.preventDefault(); + }} + data-letter={dataLetter} + contentEditable={false} + className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`} + /> + ); +} + +export default BulletedListIcon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/index.ts new file mode 100644 index 0000000000..2095dff308 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/index.ts @@ -0,0 +1 @@ +export * from './BulletedList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx new file mode 100644 index 0000000000..a20300bbc2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/Callout.tsx @@ -0,0 +1,20 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, CalloutNode } from '$app/application/document/document.types'; +import CalloutIcon from '$app/components/editor/components/blocks/callout/CalloutIcon'; + +export const Callout = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + return ( + <> +
+ +
+
+
+ {children} +
+
+ + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/CalloutIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/CalloutIcon.tsx new file mode 100644 index 0000000000..e9bba448a7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -0,0 +1,69 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { IconButton } from '@mui/material'; +import { CalloutNode } from '$app/application/document/document.types'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; + +function CalloutIcon({ node }: { node: CalloutNode }) { + const ref = useRef(null); + const [open, setOpen] = useState(false); + const editor = useSlateStatic(); + + const handleClose = useCallback(() => { + setOpen(false); + const path = ReactEditor.findPath(editor, node); + + ReactEditor.focus(editor); + editor.select(path); + editor.collapse({ + edge: 'start', + }); + }, [editor, node]); + const handleEmojiSelect = useCallback( + (emoji: string) => { + CustomEditor.setCalloutIcon(editor, node, emoji); + handleClose(); + }, + [editor, node, handleClose] + ); + + return ( + <> + { + setOpen(true); + }} + className={`h-8 w-8 p-1`} + > + {node.data.icon} + + {open && ( + + + + )} + + ); +} + +export default React.memo(CalloutIcon); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/index.ts new file mode 100644 index 0000000000..4ca74e4be8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/callout/index.ts @@ -0,0 +1 @@ +export * from './Callout'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.hooks.ts new file mode 100644 index 0000000000..0b043f4579 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.hooks.ts @@ -0,0 +1,27 @@ +import { useCallback } from 'react'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Element as SlateElement, Transforms } from 'slate'; +import { CodeNode } from '$app/application/document/document.types'; + +export function useCodeBlock(node: CodeNode) { + const language = node.data.language; + const editor = useSlateStatic() as ReactEditor; + const handleChangeLanguage = useCallback( + (newLang: string) => { + const path = ReactEditor.findPath(editor, node); + const newProperties = { + data: { + language: newLang, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + [editor, node] + ); + + return { + language, + handleChangeLanguage, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.tsx new file mode 100644 index 0000000000..7fe7b205f4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/Code.tsx @@ -0,0 +1,39 @@ +import { forwardRef, memo, useCallback } from 'react'; +import { EditorElementProps, CodeNode } from '$app/application/document/document.types'; +import LanguageSelect from './SelectLanguage'; + +import { useCodeBlock } from '$app/components/editor/components/blocks/code/Code.hooks'; +import { ReactEditor, useSlateStatic } from 'slate-react'; + +export const Code = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const { language, handleChangeLanguage } = useCodeBlock(node); + + const editor = useSlateStatic(); + const onBlur = useCallback(() => { + const path = ReactEditor.findPath(editor, node); + + ReactEditor.focus(editor); + editor.select(path); + editor.collapse({ + edge: 'start', + }); + }, [editor, node]); + + return ( + <> +
+ +
+
+
+            {children}
+          
+
+ + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx new file mode 100644 index 0000000000..4805233e1d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx @@ -0,0 +1,168 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { TextField, Popover } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { supportLanguage } from './constants'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +const initialOrigin: { + transformOrigin: PopoverOrigin; + anchorOrigin: PopoverOrigin; +} = { + transformOrigin: { + vertical: 'top', + horizontal: 'left', + }, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, +}; + +function SelectLanguage({ + language = 'json', + onChangeLanguage, + onBlur, +}: { + language: string; + onChangeLanguage: (language: string) => void; + onBlur?: () => void; +}) { + const { t } = useTranslation(); + const ref = useRef(null); + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(''); + + const searchRef = useRef(null); + const scrollRef = useRef(null); + const options: KeyboardNavigationOption[] = useMemo(() => { + return supportLanguage + .map((item) => ({ + key: item.id, + content: item.title, + })) + .filter((item) => { + return item.content?.toLowerCase().includes(search.toLowerCase()); + }); + }, [search]); + + const handleClose = useCallback(() => { + setOpen(false); + setSearch(''); + }, []); + + const handleConfirm = useCallback( + (key: string) => { + onChangeLanguage(key); + handleClose(); + }, + [onChangeLanguage, handleClose] + ); + + useEffect(() => { + const element = ref.current; + + if (!element) return; + const handleKeyDown = (e: KeyboardEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (e.key === 'Enter') { + setOpen(true); + return; + } + + onBlur?.(); + }; + + element.addEventListener('keydown', handleKeyDown); + + return () => { + element.removeEventListener('keydown', handleKeyDown); + }; + }, [onBlur]); + + const { paperHeight, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 200, + initialPaperHeight: 220, + anchorEl: ref.current, + initialAnchorOrigin: initialOrigin.anchorOrigin, + initialTransformOrigin: initialOrigin.transformOrigin, + open, + }); + + return ( + <> + { + setOpen(true); + }} + InputProps={{ + readOnly: true, + }} + placeholder={t('document.codeBlock.language.placeholder')} + label={t('document.codeBlock.language.label')} + /> + + {open && ( + +
+ setSearch(e.target.value)} + size={'small'} + autoFocus={true} + variant={'standard'} + className={'px-2 text-xs'} + placeholder={t('search.label')} + /> +
+ +
+
+
+ )} + + ); +} + +export default SelectLanguage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/constants.ts new file mode 100644 index 0000000000..dee71624db --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/constants.ts @@ -0,0 +1,154 @@ +export const supportLanguage = [ + { + id: 'bash', + title: 'Bash', + }, + { + id: 'basic', + title: 'Basic', + }, + { + id: 'c', + title: 'C', + }, + { + id: 'clojure', + title: 'Clojure', + }, + { + id: 'cpp', + title: 'C++', + }, + { + id: 'cs', + title: 'CS', + }, + { + id: 'css', + title: 'CSS', + }, + { + id: 'dart', + title: 'Dart', + }, + { + id: 'elixir', + title: 'Elixir', + }, + { + id: 'elm', + title: 'Elm', + }, + { + id: 'erlang', + title: 'Erlang', + }, + { + id: 'fortran', + title: 'Fortran', + }, + { + id: 'go', + title: 'Go', + }, + { + id: 'graphql', + title: 'GraphQL', + }, + { + id: 'haskell', + title: 'Haskell', + }, + { + id: 'java', + title: 'Java', + }, + { + id: 'javascript', + title: 'JavaScript', + }, + { + id: 'json', + title: 'JSON', + }, + { + id: 'kotlin', + title: 'Kotlin', + }, + { + id: 'lisp', + title: 'Lisp', + }, + { + id: 'lua', + title: 'Lua', + }, + { + id: 'markdown', + title: 'Markdown', + }, + { + id: 'matlab', + title: 'Matlab', + }, + { + id: 'ocaml', + title: 'OCaml', + }, + { + id: 'perl', + title: 'Perl', + }, + { + id: 'php', + title: 'PHP', + }, + { + id: 'powershell', + title: 'Powershell', + }, + { + id: 'python', + title: 'Python', + }, + { + id: 'r', + title: 'R', + }, + { + id: 'ruby', + title: 'Ruby', + }, + { + id: 'rust', + title: 'Rust', + }, + { + id: 'scala', + title: 'Scala', + }, + { + id: 'shell', + title: 'Shell', + }, + { + id: 'sql', + title: 'SQL', + }, + { + id: 'swift', + title: 'Swift', + }, + { + id: 'typescript', + title: 'TypeScript', + }, + { + id: 'xml', + title: 'XML', + }, + { + id: 'yaml', + title: 'YAML', + }, +]; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/index.ts new file mode 100644 index 0000000000..c3aa9443d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/index.ts @@ -0,0 +1 @@ +export * from './Code'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/utils.ts new file mode 100644 index 0000000000..52eeebc8c4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/utils.ts @@ -0,0 +1,132 @@ +import Prism from 'prismjs'; + +import 'prismjs/components/prism-bash'; +import 'prismjs/components/prism-basic'; +import 'prismjs/components/prism-c'; +import 'prismjs/components/prism-clojure'; +import 'prismjs/components/prism-cpp'; +import 'prismjs/components/prism-csp'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-dart'; +import 'prismjs/components/prism-elixir'; +import 'prismjs/components/prism-elm'; +import 'prismjs/components/prism-erlang'; +import 'prismjs/components/prism-fortran'; +import 'prismjs/components/prism-go'; +import 'prismjs/components/prism-graphql'; +import 'prismjs/components/prism-haskell'; +import 'prismjs/components/prism-java'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-json'; +import 'prismjs/components/prism-kotlin'; +import 'prismjs/components/prism-lisp'; +import 'prismjs/components/prism-lua'; +import 'prismjs/components/prism-markdown'; +import 'prismjs/components/prism-matlab'; +import 'prismjs/components/prism-ocaml'; +import 'prismjs/components/prism-perl'; +import 'prismjs/components/prism-php'; +import 'prismjs/components/prism-powershell'; +import 'prismjs/components/prism-python'; +import 'prismjs/components/prism-r'; +import 'prismjs/components/prism-ruby'; +import 'prismjs/components/prism-rust'; +import 'prismjs/components/prism-scala'; +import 'prismjs/components/prism-shell-session'; +import 'prismjs/components/prism-sql'; +import 'prismjs/components/prism-swift'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-xml-doc'; +import 'prismjs/components/prism-yaml'; + +import { BaseRange, NodeEntry, Text, Path } from 'slate'; + +const push_string = ( + token: string | Prism.Token, + path: Path, + start: number, + ranges: BaseRange[], + token_type = 'text' +) => { + let newStart = start; + + ranges.push({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prism_token: token_type, + anchor: { path, offset: newStart }, + focus: { path, offset: newStart + token.length }, + }); + newStart += token.length; + return newStart; +}; + +// This recurses through the Prism.tokenizes result and creates stylized ranges based on the token type +const recurseTokenize = ( + token: string | Prism.Token, + path: Path, + ranges: BaseRange[], + start: number, + parent_tag?: string +) => { + // Uses the parent's token type if a Token only has a string as its content + if (typeof token === 'string') { + return push_string(token, path, start, ranges, parent_tag); + } + + if ('content' in token) { + if (token.content instanceof Array) { + // Calls recurseTokenize on nested Tokens in content + let newStart = start; + + for (const subToken of token.content) { + newStart = recurseTokenize(subToken, path, ranges, newStart, token.type) || 0; + } + + return newStart; + } + + return push_string(token.content, path, start, ranges, token.type); + } +}; + +function switchCodeTheme(isDark: boolean) { + const link = document.getElementById('prism-css'); + + if (link) { + document.head.removeChild(link); + } + + const newLink = document.createElement('link'); + + newLink.rel = 'stylesheet'; + newLink.href = isDark + ? 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-dark.min.css' + : 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism.min.css'; + newLink.id = 'prism-css'; + document.head.appendChild(newLink); +} + +export const decorateCode = ([node, path]: NodeEntry, language: string, isDark: boolean) => { + switchCodeTheme(isDark); + + const ranges: BaseRange[] = []; + + if (!Text.isText(node)) { + return ranges; + } + + try { + const tokens = Prism.tokenize(node.text, Prism.languages[language]); + + let start = 0; + + for (const token of tokens) { + start = recurseTokenize(token, path, ranges, start) || 0; + } + + return ranges; + } catch { + return ranges; + } +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseEmpty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseEmpty.tsx new file mode 100644 index 0000000000..a0f50016e1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseEmpty.tsx @@ -0,0 +1,42 @@ +import React, { useCallback, useRef } from 'react'; +import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder'; + +import { GridNode } from '$app/application/document/document.types'; +import { useTranslation } from 'react-i18next'; + +import Drawer from '$app/components/editor/components/blocks/database/Drawer'; + +function DatabaseEmpty({ node }: { node: GridNode }) { + const { t } = useTranslation(); + const ref = useRef(null); + + const [open, setOpen] = React.useState(false); + + const toggleDrawer = useCallback((open: boolean) => { + return (e: React.MouseEvent | KeyboardEvent | React.FocusEvent) => { + e.stopPropagation(); + setOpen(open); + }; + }, []); + + return ( +
+ +
{t('document.plugins.database.noDataSource')}
+
+ + {t('document.plugins.database.selectADataSource')} + + {t('document.plugins.database.toContinue')} +
+ + +
+ ); +} + +export default React.memo(DatabaseEmpty); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts new file mode 100644 index 0000000000..543b9900ca --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts @@ -0,0 +1,26 @@ +import { useAppSelector } from '$app/stores/store'; +import { ViewLayoutPB } from '@/services/backend'; + +export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) { + const list = useAppSelector((state) => { + const workspaces = state.workspace.workspaces.map((item) => item.id) ?? []; + + return Object.values(state.pages.pageMap).filter((page) => { + if (page.layout !== layout) return false; + const parentId = page.parentId; + + if (!parentId) return false; + + const parent = state.pages.pageMap[parentId]; + const parentLayout = parent?.layout; + + if (!workspaces.includes(parentId) && parentLayout !== ViewLayoutPB.Document) return false; + + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + }); + + return { + list, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.tsx new file mode 100644 index 0000000000..5d06a13c06 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.tsx @@ -0,0 +1,104 @@ +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TextField } from '@mui/material'; +import { useLoadDatabaseList } from '$app/components/editor/components/blocks/database/DatabaseList.hooks'; +import { ViewLayoutPB } from '@/services/backend'; +import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { GridNode } from '$app/application/document/document.types'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { Page } from '$app_reducers/pages/slice'; + +function DatabaseList({ + node, + toggleDrawer, +}: { + node: GridNode; + toggleDrawer: (open: boolean) => (e: React.MouseEvent | KeyboardEvent | React.FocusEvent) => void; +}) { + const scrollRef = React.useRef(null); + + const inputRef = React.useRef(null); + const editor = useSlateStatic(); + const { t } = useTranslation(); + const [searchText, setSearchText] = React.useState(''); + const { list } = useLoadDatabaseList({ + searchText: searchText || '', + layout: ViewLayoutPB.Grid, + }); + + const renderItem = useCallback( + (item: Page) => { + return ( +
+ +
{item.name.trim() || t('menuAppHeader.defaultNewPageName')}
+
+ ); + }, + [t] + ); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return list.map((item) => { + return { + key: item.id, + content: renderItem(item), + }; + }); + }, [list, renderItem]); + + const handleSelected = useCallback( + (id: string) => { + CustomEditor.setGridBlockViewId(editor, node, id); + }, + [editor, node] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + toggleDrawer(false)(e); + } + }, + [toggleDrawer] + ); + + return ( +
+ { + setSearchText((e.currentTarget as HTMLInputElement).value); + }} + inputProps={{ + className: 'py-2 text-sm', + }} + placeholder={t('document.plugins.database.linkToDatabase')} + /> +
+ +
+
+ ); +} + +export default DatabaseList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/Drawer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/Drawer.tsx new file mode 100644 index 0000000000..54c0005027 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/Drawer.tsx @@ -0,0 +1,73 @@ +import React, { useCallback } from 'react'; +import { Button, IconButton } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { createGrid } from '$app/components/editor/components/blocks/database/utils'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import { useEditorId } from '$app/components/editor/Editor.hooks'; +import { GridNode } from '$app/application/document/document.types'; +import { ReactComponent as CloseSvg } from '$app/assets/close.svg'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import DatabaseList from '$app/components/editor/components/blocks/database/DatabaseList'; + +function Drawer({ + open, + toggleDrawer, + node, +}: { + open: boolean; + toggleDrawer: (open: boolean) => (e: React.MouseEvent | KeyboardEvent | React.FocusEvent) => void; + node: GridNode; +}) { + const editor = useSlateStatic(); + const id = useEditorId(); + const { t } = useTranslation(); + const handleCreateGrid = useCallback(async () => { + const gridId = await createGrid(id); + + CustomEditor.setGridBlockViewId(editor, node, gridId); + }, [id, editor, node]); + + return ( +
{ + e.stopPropagation(); + }} + className={'absolute right-0 top-0 h-full transform overflow-hidden'} + style={{ + width: open ? '250px' : '0px', + transition: 'width 0.3s ease-in-out', + }} + onMouseDown={(e) => { + const isInput = (e.target as HTMLElement).closest('input'); + + if (isInput) return; + e.stopPropagation(); + e.preventDefault(); + }} + > +
+
+
{t('document.plugins.database.selectDataSource')}
+ + + +
+
+ {open && } +
+ +
+ +
+
+
+ ); +} + +export default Drawer; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridBlock.tsx new file mode 100644 index 0000000000..936da9c2c8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridBlock.tsx @@ -0,0 +1,31 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, GridNode } from '$app/application/document/document.types'; + +import GridView from '$app/components/editor/components/blocks/database/GridView'; +import DatabaseEmpty from '$app/components/editor/components/blocks/database/DatabaseEmpty'; +import { useSelected } from 'slate-react'; + +export const GridBlock = memo( + forwardRef>(({ node, children, className = '', ...attributes }, ref) => { + const viewId = node.data.viewId; + const selected = useSelected(); + + return ( +
+
+ {children} +
+
+ {viewId ? : } +
+
+ ); + }) +); + +export default GridBlock; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridView.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridView.tsx new file mode 100644 index 0000000000..695482bbd8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/GridView.tsx @@ -0,0 +1,71 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Database, DatabaseRenderedProvider } from '$app/components/database'; +import { ViewIdProvider } from '$app/hooks'; + +function GridView({ viewId }: { viewId: string }) { + const [selectedViewId, onChangeSelectedViewId] = useState(viewId); + + const ref = useRef(null); + + const [rendered, setRendered] = useState<{ viewId: string; rendered: boolean } | undefined>(undefined); + + // delegate wheel event to layout when grid is scrolled to top or bottom + useEffect(() => { + const element = ref.current; + + const viewId = rendered?.viewId; + + if (!viewId || !element) { + return; + } + + const gridScroller = element.querySelector(`[data-view-id="${viewId}"] .grid-scroll-container`) as HTMLDivElement; + + const scrollLayout = gridScroller?.closest('.appflowy-scroll-container') as HTMLDivElement; + + if (!gridScroller || !scrollLayout) { + return; + } + + const onWheel = (event: WheelEvent) => { + const deltaY = event.deltaY; + const deltaX = event.deltaX; + + if (Math.abs(deltaX) > 8) { + return; + } + + const { scrollTop, scrollHeight, clientHeight } = gridScroller; + + const atTop = deltaY < 0 && scrollTop === 0; + const atBottom = deltaY > 0 && scrollTop + clientHeight >= scrollHeight; + + // if at top or bottom, prevent default to allow layout to scroll + if (atTop || atBottom) { + scrollLayout.scrollTop += deltaY; + } + }; + + gridScroller.addEventListener('wheel', onWheel, { passive: false }); + return () => { + gridScroller.removeEventListener('wheel', onWheel); + }; + }, [rendered]); + + const onRendered = useCallback((viewId: string) => { + setRendered({ + viewId, + rendered: true, + }); + }, []); + + return ( + + + + + + ); +} + +export default React.memo(GridView); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/index.ts new file mode 100644 index 0000000000..986343f9df --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/index.ts @@ -0,0 +1 @@ +export * from './GridBlock'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/utils.ts new file mode 100644 index 0000000000..032502b415 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/utils.ts @@ -0,0 +1,12 @@ +import { ViewLayoutPB } from '@/services/backend'; +import { createPage } from '$app/application/folder/page.service'; + +export async function createGrid(pageId: string) { + const newViewId = await createPage({ + layout: ViewLayoutPB.Grid, + name: '', + parent_view_id: pageId, + }); + + return newViewId; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx new file mode 100644 index 0000000000..d7d475199b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx @@ -0,0 +1,28 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, DividerNode as DividerNodeType } from '$app/application/document/document.types'; +import { useSelected } from 'slate-react'; + +export const DividerNode = memo( + forwardRef>( + ({ node: _node, children: children, ...attributes }, ref) => { + const selected = useSelected(); + + const className = useMemo(() => { + return `${attributes.className ?? ''} divider-node relative w-full rounded ${ + selected ? 'bg-content-blue-100' : '' + }`; + }, [attributes.className, selected]); + + return ( +
+
+
+
+
+ {children} +
+
+ ); + } + ) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/index.ts new file mode 100644 index 0000000000..8f6141749a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/index.ts @@ -0,0 +1 @@ +export * from './DividerNode'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/Heading.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/Heading.tsx new file mode 100644 index 0000000000..4d23069c46 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/Heading.tsx @@ -0,0 +1,18 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, HeadingNode } from '$app/application/document/document.types'; +import { getHeadingCssProperty } from '$app/components/editor/plugins/utils'; + +export const Heading = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const level = node.data.level; + const fontSizeCssProperty = getHeadingCssProperty(level); + + const className = `${attributes.className ?? ''} ${fontSizeCssProperty}`; + + return ( +
+ {children} +
+ ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/index.ts new file mode 100644 index 0000000000..6406e7b07f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/heading/index.ts @@ -0,0 +1 @@ +export * from './Heading'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageActions.tsx new file mode 100644 index 0000000000..b3d3575af2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageActions.tsx @@ -0,0 +1,163 @@ +import React, { useMemo, useState } from 'react'; +import { ImageNode } from '$app/application/document/document.types'; +import { ReactComponent as CopyIcon } from '$app/assets/copy.svg'; +import { ReactComponent as AlignLeftIcon } from '$app/assets/align-left.svg'; +import { ReactComponent as AlignCenterIcon } from '$app/assets/align-center.svg'; +import { ReactComponent as AlignRightIcon } from '$app/assets/align-right.svg'; +import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg'; +import { useTranslation } from 'react-i18next'; +import { IconButton } from '@mui/material'; +import { notify } from '$app/components/_shared/notify'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import Popover from '@mui/material/Popover'; +import Tooltip from '@mui/material/Tooltip'; + +enum ImageAction { + Copy = 'copy', + AlignLeft = 'left', + AlignCenter = 'center', + AlignRight = 'right', + Delete = 'delete', +} + +function ImageActions({ node }: { node: ImageNode }) { + const { t } = useTranslation(); + const align = node.data.align; + const editor = useSlateStatic(); + const [alignAnchorEl, setAlignAnchorEl] = useState(null); + const alignOptions = useMemo(() => { + return [ + { + key: ImageAction.AlignLeft, + Icon: AlignLeftIcon, + onClick: () => { + CustomEditor.setImageBlockData(editor, node, { align: 'left' }); + setAlignAnchorEl(null); + }, + }, + { + key: ImageAction.AlignCenter, + Icon: AlignCenterIcon, + onClick: () => { + CustomEditor.setImageBlockData(editor, node, { align: 'center' }); + setAlignAnchorEl(null); + }, + }, + { + key: ImageAction.AlignRight, + Icon: AlignRightIcon, + onClick: () => { + CustomEditor.setImageBlockData(editor, node, { align: 'right' }); + setAlignAnchorEl(null); + }, + }, + ]; + }, [editor, node]); + const options = useMemo(() => { + return [ + { + key: ImageAction.Copy, + Icon: CopyIcon, + tooltip: t('button.copyLink'), + onClick: () => { + if (!node.data.url) return; + void navigator.clipboard.writeText(node.data.url); + notify.success(t('message.copy.success')); + }, + }, + (!align || align === 'left') && { + key: ImageAction.AlignLeft, + Icon: AlignLeftIcon, + tooltip: t('button.align'), + onClick: (e: React.MouseEvent) => { + setAlignAnchorEl(e.currentTarget); + }, + }, + align === 'center' && { + key: ImageAction.AlignCenter, + Icon: AlignCenterIcon, + tooltip: t('button.align'), + onClick: (e: React.MouseEvent) => { + setAlignAnchorEl(e.currentTarget); + }, + }, + align === 'right' && { + key: ImageAction.AlignRight, + Icon: AlignRightIcon, + tooltip: t('button.align'), + onClick: (e: React.MouseEvent) => { + setAlignAnchorEl(e.currentTarget); + }, + }, + { + key: ImageAction.Delete, + Icon: DeleteIcon, + tooltip: t('button.delete'), + onClick: () => { + CustomEditor.deleteNode(editor, node); + }, + }, + ].filter(Boolean) as { + key: ImageAction; + Icon: React.FC>; + tooltip: string; + onClick: (e: React.MouseEvent) => void; + }[]; + }, [align, node, t, editor]); + + return ( +
+ {options.map((option) => { + const { key, Icon, tooltip, onClick } = option; + + return ( + + + + + + ); + })} + {!!alignAnchorEl && ( + setAlignAnchorEl(null)} + > + {alignOptions.map((option) => { + const { key, Icon, onClick } = option; + + return ( + + + + ); + })} + + )} +
+ ); +} + +export default ImageActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx new file mode 100644 index 0000000000..661eb3e3de --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx @@ -0,0 +1,49 @@ +import React, { forwardRef, memo, useCallback, useMemo, useRef } from 'react'; +import { EditorElementProps, ImageNode } from '$app/application/document/document.types'; +import { ReactEditor, useSelected, useSlateStatic } from 'slate-react'; +import ImageRender from '$app/components/editor/components/blocks/image/ImageRender'; +import ImageEmpty from '$app/components/editor/components/blocks/image/ImageEmpty'; + +export const ImageBlock = memo( + forwardRef>(({ node, children, className, ...attributes }, ref) => { + const selected = useSelected(); + const { url, align } = useMemo(() => node.data || {}, [node.data]); + const containerRef = useRef(null); + const editor = useSlateStatic(); + const onFocusNode = useCallback(() => { + ReactEditor.focus(editor); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + }, [editor, node]); + + return ( +
{ + if (!selected) onFocusNode(); + }} + className={`${className} image-block relative w-full cursor-pointer py-1`} + > +
+ {children} +
+
+ {url ? ( + + ) : ( + + )} +
+
+ ); + }) +); + +export default ImageBlock; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx new file mode 100644 index 0000000000..e0b649939e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageEmpty.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; +import { useTranslation } from 'react-i18next'; +import UploadPopover from '$app/components/editor/components/blocks/image/UploadPopover'; +import { EditorNodeType, ImageNode } from '$app/application/document/document.types'; +import { useEditorBlockDispatch, useEditorBlockState } from '$app/components/editor/stores/block'; + +function ImageEmpty({ + containerRef, + onEscape, + node, +}: { + containerRef: React.RefObject; + onEscape: () => void; + node: ImageNode; +}) { + const { t } = useTranslation(); + const state = useEditorBlockState(EditorNodeType.ImageBlock); + const open = Boolean(state?.popoverOpen && state?.blockId === node.blockId && containerRef.current); + const { openPopover, closePopover } = useEditorBlockDispatch(); + + useEffect(() => { + const container = containerRef.current; + + if (!container) { + return; + } + + const handleClick = () => { + openPopover(EditorNodeType.ImageBlock, node.blockId); + }; + + container.addEventListener('click', handleClick); + return () => { + container.removeEventListener('click', handleClick); + }; + }, [containerRef, node.blockId, openPopover]); + return ( + <> +
+ + {t('document.plugins.image.addAnImage')} +
+ {open && ( + { + closePopover(EditorNodeType.ImageBlock); + onEscape(); + }} + /> + )} + + ); +} + +export default ImageEmpty; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx new file mode 100644 index 0000000000..07310b05be --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx @@ -0,0 +1,136 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { ImageNode, ImageType } from '$app/application/document/document.types'; +import { useTranslation } from 'react-i18next'; +import { CircularProgress } from '@mui/material'; +import { ErrorOutline } from '@mui/icons-material'; +import ImageResizer from '$app/components/editor/components/blocks/image/ImageResizer'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import ImageActions from '$app/components/editor/components/blocks/image/ImageActions'; +import { LocalImage } from '$app/components/_shared/image_upload'; +import debounce from 'lodash-es/debounce'; + +const MIN_WIDTH = 100; + +const DELAY = 300; + +function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) { + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + const imgRef = useRef(null); + const editor = useSlateStatic(); + const { url = '', width: imageWidth, image_type: source } = useMemo(() => node.data || {}, [node.data]); + const { t } = useTranslation(); + const blockId = node.blockId; + + const [showActions, setShowActions] = useState(false); + const [initialWidth, setInitialWidth] = useState(null); + const [newWidth, setNewWidth] = useState(imageWidth ?? null); + + const debounceSubmitWidth = useMemo(() => { + return debounce((newWidth: number) => { + CustomEditor.setImageBlockData(editor, node, { + width: newWidth, + }); + }, DELAY); + }, [editor, node]); + + const handleWidthChange = useCallback( + (newWidth: number) => { + setNewWidth(newWidth); + debounceSubmitWidth(newWidth); + }, + [debounceSubmitWidth] + ); + + useEffect(() => { + if (!loading && !hasError && initialWidth === null && imgRef.current) { + setInitialWidth(imgRef.current.offsetWidth); + } + }, [hasError, initialWidth, loading]); + const imageProps: React.ImgHTMLAttributes = useMemo(() => { + return { + style: { width: loading || hasError ? '0' : newWidth ?? '100%', opacity: selected ? 0.8 : 1 }, + className: 'object-cover', + ref: imgRef, + src: url, + draggable: false, + onLoad: () => { + setHasError(false); + setLoading(false); + }, + onError: () => { + setHasError(true); + setLoading(false); + }, + }; + }, [url, newWidth, loading, hasError, selected]); + + const renderErrorNode = useCallback(() => { + return ( +
+ +
{t('editor.imageLoadFailed')}
+
+ ); + }, [t]); + + if (!url) return null; + + return ( +
{ + setShowActions(true); + }} + onMouseLeave={() => { + setShowActions(false); + }} + style={{ + minWidth: MIN_WIDTH, + width: 'fit-content', + }} + className={`image-render relative min-h-[48px] ${ + hasError || (loading && source !== ImageType.Local) ? 'w-full' : '' + }`} + > + {source === ImageType.Local ? ( + { + setHasError(true); + return null; + }} + loading={'lazy'} + /> + ) : ( + {`image-${blockId}`} + )} + + {initialWidth && ( + <> + + + + )} + {showActions && } + {hasError ? ( + renderErrorNode() + ) : loading && source !== ImageType.Local ? ( +
+ +
{t('editor.loading')}
+
+ ) : null} +
+ ); +} + +export default ImageRender; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx new file mode 100644 index 0000000000..e0d272acf3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageResizer.tsx @@ -0,0 +1,61 @@ +import React, { useCallback, useRef } from 'react'; + +function ImageResizer({ + minWidth, + width, + onWidthChange, + isLeft, +}: { + isLeft?: boolean; + minWidth: number; + width: number; + onWidthChange: (newWidth: number) => void; +}) { + const originalWidth = useRef(width); + const startX = useRef(0); + + const onResize = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + const diff = isLeft ? startX.current - e.clientX : e.clientX - startX.current; + const newWidth = originalWidth.current + diff; + + if (newWidth < minWidth) { + return; + } + + onWidthChange(newWidth); + }, + [isLeft, minWidth, onWidthChange] + ); + + const onResizeEnd = useCallback(() => { + document.removeEventListener('mousemove', onResize); + document.removeEventListener('mouseup', onResizeEnd); + }, [onResize]); + + const onResizeStart = useCallback( + (e: React.MouseEvent) => { + startX.current = e.clientX; + originalWidth.current = width; + document.addEventListener('mousemove', onResize); + document.addEventListener('mouseup', onResizeEnd); + }, + [onResize, onResizeEnd, width] + ); + + return ( +
+
+
+ ); +} + +export default ImageResizer; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx new file mode 100644 index 0000000000..0aff9fb0cc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/UploadPopover.tsx @@ -0,0 +1,112 @@ +import React, { useMemo } from 'react'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; + +import { useTranslation } from 'react-i18next'; +import { EmbedLink, Unsplash, UploadTabs, TabOption, TAB_KEY, UploadImage } from '$app/components/_shared/image_upload'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import { ImageNode, ImageType } from '$app/application/document/document.types'; + +const initialOrigin: { + transformOrigin: PopoverOrigin; + anchorOrigin: PopoverOrigin; +} = { + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, +}; + +function UploadPopover({ + open, + anchorEl, + onClose, + node, +}: { + open: boolean; + anchorEl: HTMLDivElement | null; + onClose: () => void; + node: ImageNode; +}) { + const editor = useSlateStatic(); + + const { t } = useTranslation(); + + const { transformOrigin, anchorOrigin, isEntered, paperHeight, paperWidth } = usePopoverAutoPosition({ + initialPaperWidth: 433, + initialPaperHeight: 300, + anchorEl, + initialAnchorOrigin: initialOrigin.anchorOrigin, + initialTransformOrigin: initialOrigin.transformOrigin, + open, + }); + + const tabOptions: TabOption[] = useMemo(() => { + return [ + { + label: t('button.upload'), + key: TAB_KEY.UPLOAD, + Component: UploadImage, + onDone: (link: string) => { + CustomEditor.setImageBlockData(editor, node, { + url: link, + image_type: ImageType.Local, + }); + onClose(); + }, + }, + { + label: t('document.imageBlock.embedLink.label'), + key: TAB_KEY.EMBED_LINK, + Component: EmbedLink, + onDone: (link: string) => { + CustomEditor.setImageBlockData(editor, node, { + url: link, + image_type: ImageType.External, + }); + onClose(); + }, + }, + { + key: TAB_KEY.UNSPLASH, + label: t('document.imageBlock.unsplash.label'), + Component: Unsplash, + onDone: (link: string) => { + CustomEditor.setImageBlockData(editor, node, { + url: link, + image_type: ImageType.External, + }); + onClose(); + }, + }, + ]; + }, [editor, node, onClose, t]); + + return ( + { + e.stopPropagation(); + }, + }} + containerStyle={{ + maxWidth: paperWidth, + maxHeight: paperHeight, + overflow: 'hidden', + }} + tabOptions={tabOptions} + /> + ); +} + +export default UploadPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/index.ts new file mode 100644 index 0000000000..73c3003a92 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/index.ts @@ -0,0 +1 @@ +export * from './ImageBlock'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/EditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/EditPopover.tsx new file mode 100644 index 0000000000..f44158bdf2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/EditPopover.tsx @@ -0,0 +1,165 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import { TextareaAutosize } from '@mui/material'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { MathEquationNode } from '$app/application/document/document.types'; +import katex from 'katex'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; + +const initialOrigin: { + transformOrigin: PopoverOrigin; + anchorOrigin: PopoverOrigin; +} = { + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, +}; + +function EditPopover({ + open, + anchorEl, + onClose, + node, +}: { + open: boolean; + node: MathEquationNode; + anchorEl: HTMLDivElement | null; + onClose: () => void; +}) { + const editor = useSlateStatic(); + + const [error, setError] = useState<{ + name: string; + message: string; + } | null>(null); + const { t } = useTranslation(); + const [value, setValue] = useState(node.data.formula || ''); + const onInput = (event: React.FormEvent) => { + setValue(event.currentTarget.value); + }; + + const handleClose = useCallback(() => { + onClose(); + if (!node) return; + ReactEditor.focus(editor); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + }, [onClose, editor, node]); + + const handleDone = () => { + if (!node || error) return; + if (value !== node.data.formula) { + CustomEditor.setMathEquationBlockFormula(editor, node, value); + } + + handleClose(); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + e.stopPropagation(); + const shift = e.shiftKey; + + // If shift is pressed, allow the user to enter a new line, otherwise close the popover + if (!shift && e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + handleDone(); + } + + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + handleClose(); + } + }; + + useEffect(() => { + try { + katex.render(value, document.createElement('div')); + setError(null); + } catch (e) { + setError( + e as { + name: string; + message: string; + } + ); + } + }, [value]); + + const { transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 300, + initialPaperHeight: 170, + anchorEl, + initialAnchorOrigin: initialOrigin.anchorOrigin, + initialTransformOrigin: initialOrigin.transformOrigin, + open, + }); + + return ( + { + e.stopPropagation(); + }} + onKeyDown={onKeyDown} + > +
+ + + {error && ( +
+ {error.name}: {error.message} +
+ )} + +
+ + +
+
+
+ ); +} + +export default EditPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx new file mode 100644 index 0000000000..ee441be624 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/MathEquation.tsx @@ -0,0 +1,88 @@ +import { forwardRef, memo, useEffect, useRef } from 'react'; +import { EditorElementProps, EditorNodeType, MathEquationNode } from '$app/application/document/document.types'; +import KatexMath from '$app/components/_shared/katex_math/KatexMath'; +import { useTranslation } from 'react-i18next'; +import { FunctionsOutlined } from '@mui/icons-material'; +import EditPopover from '$app/components/editor/components/blocks/math_equation/EditPopover'; +import { ReactEditor, useSelected, useSlateStatic } from 'slate-react'; +import { useEditorBlockDispatch, useEditorBlockState } from '$app/components/editor/stores/block'; + +export const MathEquation = memo( + forwardRef>( + ({ node, children, className, ...attributes }, ref) => { + const formula = node.data.formula; + const { t } = useTranslation(); + const containerRef = useRef(null); + const { openPopover, closePopover } = useEditorBlockDispatch(); + const state = useEditorBlockState(EditorNodeType.EquationBlock); + const open = Boolean(state?.popoverOpen && state?.blockId === node.blockId && containerRef.current); + + const selected = useSelected(); + + const editor = useSlateStatic(); + + useEffect(() => { + const slateDom = ReactEditor.toDOMNode(editor, editor); + + if (!slateDom) return; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + openPopover(EditorNodeType.EquationBlock, node.blockId); + } + }; + + if (selected) { + slateDom.addEventListener('keydown', handleKeyDown); + } + + return () => { + slateDom.removeEventListener('keydown', handleKeyDown); + }; + }, [editor, node.blockId, openPopover, selected]); + + return ( + <> +
{ + openPopover(EditorNodeType.EquationBlock, node.blockId); + }} + className={`${className} math-equation-block relative w-full cursor-pointer py-2`} + > +
+ {formula ? ( + + ) : ( +
+ + {t('document.plugins.mathEquation.addMathEquation')} +
+ )} +
+
+ {children} +
+
+ {open && ( + { + closePopover(EditorNodeType.EquationBlock); + }} + node={node} + open={open} + anchorEl={containerRef.current} + /> + )} + + ); + } + ) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/index.ts new file mode 100644 index 0000000000..ae6eb70209 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/math_equation/index.ts @@ -0,0 +1 @@ +export * from './MathEquation'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx new file mode 100644 index 0000000000..888b46c980 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx @@ -0,0 +1,87 @@ +import React, { useMemo } from 'react'; +import { ReactEditor, useSlate, useSlateStatic } from 'slate-react'; +import { Element, Path } from 'slate'; +import { NumberedListNode } from '$app/application/document/document.types'; +import { letterize, romanize } from '$app/utils/list'; +import { CustomEditor } from '$app/components/editor/command'; + +enum Letter { + Number = 'number', + Letter = 'letter', + Roman = 'roman', +} + +function getLetterNumber(index: number, letter: Letter) { + if (letter === Letter.Number) { + return index; + } else if (letter === Letter.Letter) { + return letterize(index); + } else { + return romanize(index); + } +} + +function NumberListIcon({ block, className }: { block: NumberedListNode; className: string }) { + const editor = useSlate(); + const staticEditor = useSlateStatic(); + + const path = ReactEditor.findPath(editor, block); + const index = useMemo(() => { + let index = 1; + + let topNode; + let prevPath = Path.previous(path); + + while (prevPath) { + const prev = editor.node(prevPath); + + const prevNode = prev[0] as Element; + + if (prevNode.type === block.type) { + index += 1; + topNode = prevNode; + } else { + break; + } + + prevPath = Path.previous(prevPath); + } + + if (!topNode) { + return Number(block.data?.number ?? 1); + } + + const startIndex = (topNode as NumberedListNode).data?.number ?? 1; + + return index + Number(startIndex) - 1; + }, [editor, block, path]); + + const letter = useMemo(() => { + const level = CustomEditor.getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Number; + } else if (level % 3 === 1) { + return Letter.Letter; + } else { + return Letter.Roman; + } + }, [block.type, staticEditor, path]); + + const dataNumber = useMemo(() => { + return getLetterNumber(index, letter); + }, [index, letter]); + + return ( + { + e.preventDefault(); + }} + contentEditable={false} + data-number={dataNumber} + className={`${className} numbered-icon flex w-[24px] min-w-[24px] justify-center pr-1 font-medium`} + /> + ); +} + +export default NumberListIcon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberedList.tsx new file mode 100644 index 0000000000..f3e34e1571 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberedList.tsx @@ -0,0 +1,14 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, NumberedListNode } from '$app/application/document/document.types'; + +export const NumberedList = memo( + forwardRef>( + ({ node: _, children, className, ...attributes }, ref) => { + return ( +
+ {children} +
+ ); + } + ) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/index.ts new file mode 100644 index 0000000000..6e985ae25b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/index.ts @@ -0,0 +1 @@ +export * from './NumberedList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx new file mode 100644 index 0000000000..f93cb897ba --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx @@ -0,0 +1,18 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, PageNode } from '$app/application/document/document.types'; + +export const Page = memo( + forwardRef>(({ node: _, children, ...attributes }, ref) => { + const className = useMemo(() => { + return `${attributes.className ?? ''} document-title pb-3 text-5xl font-bold`; + }, [attributes.className]); + + return ( +
+ {children} +
+ ); + }) +); + +export default Page; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/index.ts new file mode 100644 index 0000000000..d9925d7520 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/index.ts @@ -0,0 +1 @@ +export * from './Page'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/Paragraph.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/Paragraph.tsx new file mode 100644 index 0000000000..96524db239 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/Paragraph.tsx @@ -0,0 +1,14 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, ParagraphNode } from '$app/application/document/document.types'; + +export const Paragraph = memo( + forwardRef>(({ node: _, children, ...attributes }, ref) => { + { + return ( +
+ {children} +
+ ); + } + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/index.ts new file mode 100644 index 0000000000..01752c914c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/paragraph/index.ts @@ -0,0 +1 @@ +export * from './Paragraph'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/Quote.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/Quote.tsx new file mode 100644 index 0000000000..5afc35289b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/Quote.tsx @@ -0,0 +1,16 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, QuoteNode } from '$app/application/document/document.types'; + +export const QuoteList = memo( + forwardRef>(({ node: _, children, ...attributes }, ref) => { + const className = useMemo(() => { + return `flex w-full flex-col ml-3 border-l-[4px] border-fill-default pl-2 ${attributes.className ?? ''}`; + }, [attributes.className]); + + return ( +
+ {children} +
+ ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/index.ts new file mode 100644 index 0000000000..c88e677a53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/quote/index.ts @@ -0,0 +1 @@ +export * from './Quote'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx new file mode 100644 index 0000000000..acf16581f4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -0,0 +1,46 @@ +import React, { FC, useCallback, useMemo } from 'react'; +import { EditorNodeType, TextNode } from '$app/application/document/document.types'; +import { ReactEditor, useSlate } from 'slate-react'; +import { Editor, Element } from 'slate'; +import CheckboxIcon from '$app/components/editor/components/blocks/todo_list/CheckboxIcon'; +import ToggleIcon from '$app/components/editor/components/blocks/toggle_list/ToggleIcon'; +import NumberListIcon from '$app/components/editor/components/blocks/numbered_list/NumberListIcon'; +import BulletedListIcon from '$app/components/editor/components/blocks/bulleted_list/BulletedListIcon'; + +export function useStartIcon(node: TextNode) { + const editor = useSlate(); + const path = ReactEditor.findPath(editor, node); + const block = Editor.parent(editor, path)?.[0] as Element | null; + + const Component = useMemo(() => { + if (!Element.isElement(block)) { + return null; + } + + switch (block.type) { + case EditorNodeType.TodoListBlock: + return CheckboxIcon; + case EditorNodeType.ToggleListBlock: + return ToggleIcon; + case EditorNodeType.NumberedListBlock: + return NumberListIcon; + case EditorNodeType.BulletedListBlock: + return BulletedListIcon; + default: + return null; + } + }, [block]) as FC<{ block: Element; className: string }> | null; + + const renderIcon = useCallback(() => { + if (!Component || !block) { + return null; + } + + return ; + }, [Component, block]); + + return { + hasStartIcon: !!Component, + renderIcon, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx new file mode 100644 index 0000000000..768524394e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx @@ -0,0 +1,29 @@ +import React, { forwardRef, memo } from 'react'; +import Placeholder from '$app/components/editor/components/blocks/_shared/Placeholder'; +import { EditorElementProps, TextNode } from '$app/application/document/document.types'; +import { useSlateStatic } from 'slate-react'; +import { useStartIcon } from '$app/components/editor/components/blocks/text/StartIcon.hooks'; + +export const Text = memo( + forwardRef>(({ node, children, className, ...attributes }, ref) => { + const editor = useSlateStatic(); + const { hasStartIcon, renderIcon } = useStartIcon(node); + const isEmpty = editor.isEmpty(node); + + return ( + + {renderIcon()} + + {children} + + ); + }) +); + +export default Text; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/index.ts new file mode 100644 index 0000000000..b0c76af0b0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/index.ts @@ -0,0 +1 @@ +export * from './Text'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx new file mode 100644 index 0000000000..d98990c886 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react'; +import { TodoListNode } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Location } from 'slate'; +import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; + +function CheckboxIcon({ block, className }: { block: TodoListNode; className: string }) { + const editor = useSlateStatic(); + const { checked } = block.data; + + const toggleTodo = useCallback( + (e: React.MouseEvent) => { + const path = ReactEditor.findPath(editor, block); + const start = editor.start(path); + let at: Location = start; + + if (e.shiftKey) { + const end = editor.end(path); + + at = { + anchor: start, + focus: end, + }; + } + + CustomEditor.toggleTodo(editor, at); + }, + [editor, block] + ); + + return ( + { + e.preventDefault(); + }} + className={`${className} cursor-pointer pr-1 text-xl text-fill-default`} + > + {checked ? : } + + ); +} + +export default CheckboxIcon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/TodoList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/TodoList.tsx new file mode 100644 index 0000000000..c662c48153 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/TodoList.tsx @@ -0,0 +1,19 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, TodoListNode } from '$app/application/document/document.types'; + +export const TodoList = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const { checked = false } = useMemo(() => node.data || {}, [node.data]); + const className = useMemo(() => { + return `flex w-full flex-col ${checked ? 'checked' : ''} ${attributes.className ?? ''}`; + }, [attributes.className, checked]); + + return ( + <> +
+ {children} +
+ + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/index.ts new file mode 100644 index 0000000000..f239f43459 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleIcon.tsx new file mode 100644 index 0000000000..ad27822cb5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleIcon.tsx @@ -0,0 +1,30 @@ +import React, { useCallback } from 'react'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import { ToggleListNode } from '$app/application/document/document.types'; +import { ReactComponent as RightSvg } from '$app/assets/more.svg'; + +function ToggleIcon({ block, className }: { block: ToggleListNode; className: string }) { + const editor = useSlateStatic(); + const { collapsed } = block.data; + + const toggleToggleList = useCallback(() => { + CustomEditor.toggleToggleList(editor, block); + }, [editor, block]); + + return ( + { + e.preventDefault(); + }} + className={`${className} cursor-pointer pr-1 text-xl hover:text-fill-default`} + > + {collapsed ? : } + + ); +} + +export default ToggleIcon; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx new file mode 100644 index 0000000000..809f3b750d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx @@ -0,0 +1,17 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, ToggleListNode } from '$app/application/document/document.types'; + +export const ToggleList = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const { collapsed } = useMemo(() => node.data || {}, [node.data]); + const className = `${attributes.className ?? ''} flex w-full flex-col ${collapsed ? 'collapsed' : ''}`; + + return ( + <> +
+ {children} +
+ + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/index.ts new file mode 100644 index 0000000000..833bdb5210 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/index.ts @@ -0,0 +1 @@ +export * from './ToggleList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx new file mode 100644 index 0000000000..2526df895e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx @@ -0,0 +1,93 @@ +import { memo, useEffect, useMemo, useState } from 'react'; + +import Editor from '$app/components/editor/components/editor/Editor'; +import { EditorProps } from '$app/application/document/document.types'; +import { Provider } from '$app/components/editor/provider'; +import { YXmlText } from 'yjs/dist/src/types/YXmlText'; +import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation'; +import isEqual from 'lodash-es/isEqual'; + +export const CollaborativeEditor = memo( + ({ id, title, cover, showTitle = true, onTitleChange, onCoverChange, ...props }: EditorProps) => { + const [sharedType, setSharedType] = useState(null); + const provider = useMemo(() => { + setSharedType(null); + + return new Provider(id); + }, [id]); + + const root = useMemo(() => { + if (!showTitle || !sharedType || !sharedType.doc) return null; + + return getYTarget(sharedType?.doc, [0]); + }, [sharedType, showTitle]); + + const rootText = useMemo(() => { + if (!root) return null; + return getInsertTarget(root, [0]); + }, [root]); + + useEffect(() => { + if (!rootText || rootText.toString() === title) return; + + if (rootText.length > 0) { + rootText.delete(0, rootText.length); + } + + rootText.insert(0, title || ''); + }, [title, rootText]); + + useEffect(() => { + if (!root) return; + + const originalCover = root.getAttribute('data')?.cover; + + if (cover === undefined) return; + if (isEqual(originalCover, cover)) return; + root.setAttribute('data', { cover: cover ? cover : undefined }); + }, [cover, root]); + + useEffect(() => { + if (!root) return; + const rootId = root.getAttribute('blockId'); + + if (!rootId) return; + + const getCover = () => { + const data = root.getAttribute('data'); + + onCoverChange?.(data?.cover); + }; + + getCover(); + const onChange = () => { + onTitleChange?.(root.toString()); + getCover(); + }; + + root.observeDeep(onChange); + return () => root.unobserveDeep(onChange); + }, [onTitleChange, root, onCoverChange]); + + useEffect(() => { + provider.connect(); + + const handleConnected = () => { + setSharedType(provider.sharedType); + }; + + provider.on('ready', handleConnected); + void provider.initialDocument(showTitle); + return () => { + provider.off('ready', handleConnected); + provider.disconnect(); + }; + }, [provider, showTitle]); + + if (!sharedType || id !== provider.id) { + return null; + } + + return ; + } +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx new file mode 100644 index 0000000000..b0bbe0eb28 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx @@ -0,0 +1,42 @@ +import React, { ComponentProps, useCallback } from 'react'; +import { Editable, useSlate } from 'slate-react'; +import Element from './Element'; +import { Leaf } from './Leaf'; +import { useShortcuts } from '$app/components/editor/plugins/shortcuts'; +import { useInlineKeyDown } from '$app/components/editor/components/editor/Editor.hooks'; + +type CustomEditableProps = Omit, 'renderElement' | 'renderLeaf'> & + Partial, 'renderElement' | 'renderLeaf'>> & { + disableFocus?: boolean; + }; + +export function CustomEditable({ + renderElement = Element, + disableFocus = false, + renderLeaf = Leaf, + ...props +}: CustomEditableProps) { + const editor = useSlate(); + const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor); + const withInlineKeyDown = useInlineKeyDown(editor); + const onKeyDown = useCallback( + (event: React.KeyboardEvent) => { + withInlineKeyDown(event); + onShortcutsKeyDown(event); + }, + [onShortcutsKeyDown, withInlineKeyDown] + ); + + return ( + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.hooks.ts new file mode 100644 index 0000000000..f2443ba44b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.hooks.ts @@ -0,0 +1,137 @@ +import { KeyboardEvent, useCallback, useEffect, useMemo } from 'react'; + +import { BaseRange, createEditor, Editor, Element, NodeEntry, Range, Transforms } from 'slate'; +import { ReactEditor, withReact } from 'slate-react'; +import { withBlockPlugins } from '$app/components/editor/plugins/withBlockPlugins'; +import { withInlines } from '$app/components/editor/components/inline_nodes'; +import { withYHistory, withYjs, YjsEditor } from '@slate-yjs/core'; +import * as Y from 'yjs'; +import { CustomEditor } from '$app/components/editor/command'; +import { CodeNode, EditorNodeType } from '$app/application/document/document.types'; +import { decorateCode } from '$app/components/editor/components/blocks/code/utils'; +import { withMarkdown } from '$app/components/editor/plugins/shortcuts'; +import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function useEditor(sharedType: Y.XmlText) { + const editor = useMemo(() => { + if (!sharedType) return null; + const e = withMarkdown(withBlockPlugins(withInlines(withReact(withYHistory(withYjs(createEditor(), sharedType)))))); + + // Ensure editor always has at least 1 valid child + const { normalizeNode } = e; + + e.normalizeNode = (entry) => { + const [node] = entry; + + if (!Editor.isEditor(node) || node.children.length > 0) { + return normalizeNode(entry); + } + + // Ensure editor always has at least 1 valid child + CustomEditor.insertEmptyLineAtEnd(e as ReactEditor & YjsEditor); + }; + + return e; + }, [sharedType]) as ReactEditor & YjsEditor; + + const initialValue = useMemo(() => { + return []; + }, []); + + // Connect editor in useEffect to comply with concurrent mode requirements. + useEffect(() => { + YjsEditor.connect(editor); + return () => { + YjsEditor.disconnect(editor); + }; + }, [editor]); + + const handleOnClickEnd = useCallback(() => { + const path = [editor.children.length - 1]; + const node = Editor.node(editor, path) as NodeEntry; + const latestNodeIsEmpty = CustomEditor.isEmptyText(editor, node[0]); + + if (latestNodeIsEmpty) { + ReactEditor.focus(editor); + editor.select(path); + editor.collapse({ + edge: 'end', + }); + + return; + } + + CustomEditor.insertEmptyLineAtEnd(editor); + }, [editor]); + + return { + editor, + initialValue, + handleOnClickEnd, + }; +} + +export function useDecorateCodeHighlight(editor: ReactEditor) { + return useCallback( + (entry: NodeEntry): BaseRange[] => { + const path = entry[1]; + + const blockEntry = editor.above({ + at: path, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + + if (!blockEntry) return []; + + const block = blockEntry[0] as CodeNode; + + if (block.type === EditorNodeType.CodeBlock) { + const language = block.data.language; + + return decorateCode(entry, language, false); + } + + return []; + }, + [editor] + ); +} + +export function useInlineKeyDown(editor: ReactEditor) { + return useCallback( + (e: KeyboardEvent) => { + const selection = editor.selection; + + // Default left/right behavior is unit:'character'. + // This fails to distinguish between two cursor positions, such as + // foo vs foo. + // Here we modify the behavior to unit:'offset'. + // This lets the user step into and out of the inline without stepping over characters. + // You may wish to customize this further to only use unit:'offset' in specific cases. + if (selection && Range.isCollapsed(selection)) { + const { nativeEvent } = e; + + if ( + createHotkey(HOT_KEY_NAME.LEFT)(nativeEvent) && + CustomEditor.beforeIsInlineNode(editor, selection, { + unit: 'offset', + }) + ) { + e.preventDefault(); + Transforms.move(editor, { unit: 'offset', reverse: true }); + return; + } + + if ( + createHotkey(HOT_KEY_NAME.RIGHT)(nativeEvent) && + CustomEditor.afterIsInlineNode(editor, selection, { unit: 'offset' }) + ) { + e.preventDefault(); + Transforms.move(editor, { unit: 'offset' }); + return; + } + } + }, + [editor] + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx new file mode 100644 index 0000000000..d87dbe3f35 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx @@ -0,0 +1,78 @@ +import React, { useCallback } from 'react'; +import { useDecorateCodeHighlight, useEditor } from '$app/components/editor/components/editor/Editor.hooks'; +import { Slate } from 'slate-react'; +import { CustomEditable } from '$app/components/editor/components/editor/CustomEditable'; +import { SelectionToolbar } from '$app/components/editor/components/tools/selection_toolbar'; +import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions'; + +import { CircularProgress } from '@mui/material'; +import { NodeEntry } from 'slate'; +import { + DecorateStateProvider, + EditorSelectedBlockProvider, + useInitialEditorState, + SlashStateProvider, + EditorInlineBlockStateProvider, +} from '$app/components/editor/stores'; +import CommandPanel from '../tools/command_panel/CommandPanel'; +import { EditorBlockStateProvider } from '$app/components/editor/stores/block'; +import { LocalEditorProps } from '$app/application/document/document.types'; + +function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) { + const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType); + const decorateCodeHighlight = useDecorateCodeHighlight(editor); + + const { + selectedBlocks, + decorate: decorateCustomRange, + decorateState, + slashState, + inlineBlockState, + blockState, + } = useInitialEditorState(editor); + + const decorate = useCallback( + (entry: NodeEntry) => { + const codeRanges = decorateCodeHighlight(entry); + const customRanges = decorateCustomRange(entry); + + return [...codeRanges, ...customRanges]; + }, + [decorateCodeHighlight, decorateCustomRange] + ); + + if (editor.sharedRoot.length === 0) { + return ; + } + + return ( + + + + + + + + + + + +
+ + + + + + + ); +} + +export default Editor; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.hooks.ts new file mode 100644 index 0000000000..bf7045705d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.hooks.ts @@ -0,0 +1,30 @@ +import { Element } from 'slate'; +import { useContext, useEffect, useMemo } from 'react'; +import { useSnapshot } from 'valtio'; +import { useSelected } from 'slate-react'; + +import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; + +export function useElementState(element: Element) { + const blockId = element.blockId; + const selectedBlockContext = useContext(EditorSelectedBlockContext); + const selected = useSelected(); + + useEffect(() => { + if (!blockId) return; + + if (!selected) { + selectedBlockContext.delete(blockId); + } + }, [blockId, selected, selectedBlockContext]); + + const selectedBlockIds = useSnapshot(selectedBlockContext); + const blockSelected = useMemo(() => { + if (!blockId) return false; + return selectedBlockIds.has(blockId); + }, [blockId, selectedBlockIds]); + + return { + blockSelected, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx new file mode 100644 index 0000000000..1824d8a590 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Element.tsx @@ -0,0 +1,132 @@ +import React, { FC, HTMLAttributes, useMemo } from 'react'; +import { RenderElementProps, useSlateStatic } from 'slate-react'; +import { + BlockData, + EditorElementProps, + EditorInlineNodeType, + EditorNodeType, + TextNode, +} from '$app/application/document/document.types'; +import { Paragraph } from '$app/components/editor/components/blocks/paragraph'; +import { Heading } from '$app/components/editor/components/blocks/heading'; +import { TodoList } from '$app/components/editor/components/blocks/todo_list'; +import { Code } from '$app/components/editor/components/blocks/code'; +import { QuoteList } from '$app/components/editor/components/blocks/quote'; +import { NumberedList } from '$app/components/editor/components/blocks/numbered_list'; +import { BulletedList } from '$app/components/editor/components/blocks/bulleted_list'; +import { DividerNode } from '$app/components/editor/components/blocks/divider'; +import { InlineFormula } from '$app/components/editor/components/inline_nodes/inline_formula'; +import { ToggleList } from '$app/components/editor/components/blocks/toggle_list'; +import { Callout } from '$app/components/editor/components/blocks/callout'; +import { Mention } from '$app/components/editor/components/inline_nodes/mention'; +import { GridBlock } from '$app/components/editor/components/blocks/database'; +import { MathEquation } from '$app/components/editor/components/blocks/math_equation'; +import { ImageBlock } from '$app/components/editor/components/blocks/image'; + +import { Text as TextComponent } from '../blocks/text'; +import { Page } from '../blocks/page'; +import { useElementState } from '$app/components/editor/components/editor/Element.hooks'; +import UnSupportBlock from '$app/components/editor/components/blocks/_shared/unSupportBlock'; +import { renderColor } from '$app/utils/color'; + +function Element({ element, attributes, children }: RenderElementProps) { + const node = element; + + const InlineComponent = useMemo(() => { + switch (node.type) { + case EditorInlineNodeType.Formula: + return InlineFormula; + case EditorInlineNodeType.Mention: + return Mention; + default: + return null; + } + }, [node.type]) as FC; + + const Component = useMemo(() => { + switch (node.type) { + case EditorNodeType.Page: + return Page; + case EditorNodeType.HeadingBlock: + return Heading; + case EditorNodeType.TodoListBlock: + return TodoList; + case EditorNodeType.Paragraph: + return Paragraph; + case EditorNodeType.CodeBlock: + return Code; + case EditorNodeType.QuoteBlock: + return QuoteList; + case EditorNodeType.NumberedListBlock: + return NumberedList; + case EditorNodeType.BulletedListBlock: + return BulletedList; + case EditorNodeType.DividerBlock: + return DividerNode; + case EditorNodeType.ToggleListBlock: + return ToggleList; + case EditorNodeType.CalloutBlock: + return Callout; + case EditorNodeType.GridBlock: + return GridBlock; + case EditorNodeType.EquationBlock: + return MathEquation; + case EditorNodeType.ImageBlock: + return ImageBlock; + default: + return UnSupportBlock; + } + }, [node.type]) as FC>; + + const editor = useSlateStatic(); + const { blockSelected } = useElementState(node); + const isEmbed = editor.isEmbed(node); + + const className = useMemo(() => { + const align = + ( + node.data as { + align: 'left' | 'center' | 'right'; + } + )?.align || 'left'; + + return `block-element flex rounded ${align ? `block-align-${align}` : ''} ${ + blockSelected && !isEmbed ? 'bg-content-blue-100' : '' + }`; + }, [node.data, blockSelected, isEmbed]); + + const style = useMemo(() => { + const data = (node.data as BlockData) || {}; + + return { + backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, + }; + }, [node.data]); + + if (InlineComponent) { + return ( + + {children} + + ); + } + + if (node.type === EditorNodeType.Text) { + return ( + + {children} + + ); + } + + return ( +
+ + {children} + +
+ ); +} + +export default Element; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx new file mode 100644 index 0000000000..188ac33361 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Leaf.tsx @@ -0,0 +1,59 @@ +import React, { CSSProperties } from 'react'; +import { RenderLeafProps } from 'slate-react'; +import { Link } from '$app/components/editor/components/inline_nodes/link'; +import { renderColor } from '$app/utils/color'; + +export function Leaf({ attributes, children, leaf }: RenderLeafProps) { + let newChildren = children; + + const classList = [leaf.prism_token, leaf.prism_token && 'token', leaf.class_name].filter(Boolean); + + if (leaf.code) { + newChildren = ( + + {newChildren} + + ); + } + + if (leaf.underline) { + newChildren = {newChildren}; + } + + if (leaf.strikethrough) { + newChildren = {newChildren}; + } + + if (leaf.italic) { + newChildren = {newChildren}; + } + + if (leaf.bold) { + newChildren = {newChildren}; + } + + const style: CSSProperties = {}; + + if (leaf.font_color) { + style['color'] = renderColor(leaf.font_color); + } + + if (leaf.bg_color) { + style['backgroundColor'] = renderColor(leaf.bg_color); + } + + if (leaf.href) { + newChildren = {newChildren}; + } + + return ( + + {newChildren} + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/index.ts new file mode 100644 index 0000000000..c0c3c728d1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/index.ts @@ -0,0 +1,3 @@ +export * from './CollaborativeEditor'; +export * from './Editor'; +export { useDecorateCodeHighlight } from '$app/components/editor/components/editor/Editor.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/utils.ts new file mode 100644 index 0000000000..cc6960cdf4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/utils.ts @@ -0,0 +1,31 @@ +import { BasePoint, Editor, Transforms } from 'slate'; +import { ReactEditor } from 'slate-react'; + +export function getNodePath(editor: ReactEditor, target: HTMLElement) { + const slateNode = ReactEditor.toSlateNode(editor, target); + const path = ReactEditor.findPath(editor, slateNode); + + return path; +} + +export function moveCursorToNodeEnd(editor: ReactEditor, target: HTMLElement) { + const path = getNodePath(editor, target); + const afterPath = Editor.after(editor, path); + + ReactEditor.focus(editor); + + if (afterPath) { + const afterStart = Editor.start(editor, afterPath); + + moveCursorToPoint(editor, afterStart); + } else { + const beforeEnd = Editor.end(editor, path); + + moveCursorToPoint(editor, beforeEnd); + } +} + +export function moveCursorToPoint(editor: ReactEditor, point: BasePoint) { + ReactEditor.focus(editor); + Transforms.select(editor, point); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx new file mode 100644 index 0000000000..fb32eb18a9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/InlineChromiumBugfix.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +// Put this at the start and end of an inline component to work around this Chromium bug: +// https://bugs.chromium.org/p/chromium/issues/detail?id=1249405 + +export const InlineChromiumBugfix = ({ className }: { className?: string }) => ( + + {String.fromCodePoint(160) /* Non-breaking space */} + +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/index.ts new file mode 100644 index 0000000000..29f27984f7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/index.ts @@ -0,0 +1,2 @@ +export * from './withInline'; +export * from './inline_formula'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover.tsx new file mode 100644 index 0000000000..c60d7af40e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; + +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import { useTranslation } from 'react-i18next'; +import TextField from '@mui/material/TextField'; +import { IconButton } from '@mui/material'; +import { ReactComponent as SelectCheck } from '$app/assets/select-check.svg'; +import { ReactComponent as Clear } from '$app/assets/delete.svg'; +import Tooltip from '@mui/material/Tooltip'; + +function FormulaEditPopover({ + defaultText, + open, + anchorEl, + onClose, + onDone, + onClear, +}: { + defaultText: string; + open: boolean; + anchorEl: HTMLElement | null; + onClose: () => void; + onClear: () => void; + onDone: (formula: string) => void; +}) { + const [text, setText] = useState(defaultText); + const { t } = useTranslation(); + + return ( + +
+ setText(e.target.value)} + fullWidth={true} + onKeyDown={(e) => { + e.stopPropagation(); + if (e.key === 'Enter') { + e.preventDefault(); + onDone(text); + } + + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + + if (e.key === 'Tab') { + e.preventDefault(); + } + }} + /> + + onDone(text)}> + + + + + + + + +
+
+ ); +} + +export default FormulaEditPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf.tsx new file mode 100644 index 0000000000..324d273ab3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import KatexMath from '$app/components/_shared/katex_math/KatexMath'; + +function FormulaLeaf({ formula, children }: { formula: string; children: React.ReactNode }) { + return ( + + + + + + {children} + + ); +} + +export default FormulaLeaf; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx new file mode 100644 index 0000000000..204047304f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx @@ -0,0 +1,133 @@ +import React, { forwardRef, memo, useCallback, MouseEvent, useRef, useEffect } from 'react'; +import { ReactEditor, useSelected, useSlate } from 'slate-react'; +import { Editor, Range, Transforms } from 'slate'; +import { EditorElementProps, FormulaNode } from '$app/application/document/document.types'; +import FormulaLeaf from '$app/components/editor/components/inline_nodes/inline_formula/FormulaLeaf'; +import FormulaEditPopover from '$app/components/editor/components/inline_nodes/inline_formula/FormulaEditPopover'; +import { getNodePath, moveCursorToNodeEnd } from '$app/components/editor/components/editor/utils'; +import { CustomEditor } from '$app/components/editor/command'; +import { useEditorInlineBlockState } from '$app/components/editor/stores'; +import { InlineChromiumBugfix } from '$app/components/editor/components/inline_nodes/InlineChromiumBugfix'; + +export const InlineFormula = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + const editor = useSlate(); + const formula = node.data; + const { popoverOpen = false, setRange, openPopover, closePopover } = useEditorInlineBlockState('formula'); + const anchor = useRef(null); + const selected = useSelected(); + const open = Boolean(popoverOpen && selected); + + const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); + + useEffect(() => { + if (selected && isCollapsed && !open) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, open]); + + const handleClick = useCallback( + (e: MouseEvent) => { + const target = e.currentTarget; + const path = getNodePath(editor, target); + + setRange(path); + openPopover(); + }, + [editor, openPopover, setRange] + ); + + const handleEditPopoverClose = useCallback(() => { + closePopover(); + if (anchor.current === null) { + return; + } + + moveCursorToNodeEnd(editor, anchor.current); + }, [closePopover, editor]); + + const selectNode = useCallback(() => { + if (anchor.current === null) { + return; + } + + const path = getNodePath(editor, anchor.current); + + ReactEditor.focus(editor); + Transforms.select(editor, path); + }, [editor]); + + const onClear = useCallback(() => { + selectNode(); + CustomEditor.toggleFormula(editor); + closePopover(); + }, [selectNode, closePopover, editor]); + + const onDone = useCallback( + (newFormula: string) => { + selectNode(); + if (newFormula === '' && anchor.current) { + const path = getNodePath(editor, anchor.current); + const point = editor.before(path); + + CustomEditor.deleteFormula(editor); + closePopover(); + if (point) { + ReactEditor.focus(editor); + editor.select(point); + } + + return; + } else { + CustomEditor.updateFormula(editor, newFormula); + handleEditPopoverClose(); + } + }, + [closePopover, editor, handleEditPopoverClose, selectNode] + ); + + return ( + <> + { + anchor.current = el; + if (ref) { + if (typeof ref === 'function') { + ref(el); + } else { + ref.current = el; + } + } + }} + contentEditable={false} + onDoubleClick={handleClick} + onClick={handleClick} + className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded px-1 py-0.5 ${ + selected ? 'selected' : '' + }`} + > + + {children} + + + {open && ( + + )} + + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/index.ts new file mode 100644 index 0000000000..5643ae8943 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/index.ts @@ -0,0 +1,2 @@ +export * from './InlineFormula'; +export * from './FormulaLeaf'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx new file mode 100644 index 0000000000..09095480dc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx @@ -0,0 +1,48 @@ +import { memo, useCallback, useRef } from 'react'; +import { ReactEditor, useSlate } from 'slate-react'; +import { getNodePath } from '$app/components/editor/components/editor/utils'; +import { Transforms, Text } from 'slate'; +import { useDecorateDispatch } from '$app/components/editor/stores'; + +export const Link = memo(({ children }: { leaf: Text; children: React.ReactNode }) => { + const { add: addDecorate } = useDecorateDispatch(); + + const editor = useSlate(); + + const ref = useRef(null); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (ref.current === null) { + return; + } + + const path = getNodePath(editor, ref.current); + + ReactEditor.focus(editor); + Transforms.select(editor, path); + + if (!editor.selection) return; + addDecorate({ + range: editor.selection, + class_name: 'bg-content-blue-100 rounded', + type: 'link', + }); + }, + [addDecorate, editor] + ); + + return ( + <> + + {children} + + + ); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx new file mode 100644 index 0000000000..af62a7b28f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx @@ -0,0 +1,179 @@ +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import Typography from '@mui/material/Typography'; +import { addMark, removeMark } from 'slate'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { notify } from 'src/appflowy_app/components/_shared/notify'; +import { CustomEditor } from '$app/components/editor/command'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { ReactComponent as RemoveSvg } from '$app/assets/delete.svg'; +import { ReactComponent as LinkSvg } from '$app/assets/link.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import isHotkey from 'is-hotkey'; +import LinkEditInput from '$app/components/editor/components/inline_nodes/link/LinkEditInput'; +import { openUrl, isUrl } from '$app/utils/open_url'; + +function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaultHref: string }) { + const editor = useSlateStatic(); + const { t } = useTranslation(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Href); + + const [focusMenu, setFocusMenu] = useState(false); + const scrollRef = useRef(null); + const inputRef = useRef(null); + const [link, setLink] = useState(defaultHref); + + const setNodeMark = useCallback(() => { + if (link === '') { + removeMark(editor, EditorMarkFormat.Href); + } else { + addMark(editor, EditorMarkFormat.Href, link); + } + }, [editor, link]); + + const removeNodeMark = useCallback(() => { + onClose(); + editor.removeMark(EditorMarkFormat.Href); + }, [editor, onClose]); + + useEffect(() => { + const input = inputRef.current; + + if (!input) return; + + let isComposing = false; + + const handleCompositionUpdate = () => { + isComposing = true; + }; + + const handleCompositionEnd = () => { + isComposing = false; + }; + + const handleKeyDown = (e: KeyboardEvent) => { + e.stopPropagation(); + + if (e.key === 'Enter') { + e.preventDefault(); + if (isUrl(link)) { + onClose(); + setNodeMark(); + } + + return; + } + + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + return; + } + + if (e.key === 'Tab') { + e.preventDefault(); + setFocusMenu(true); + return; + } + + if (!isComposing && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + notify.clear(); + notify.info(`Press Tab to focus on the menu`); + return; + } + }; + + input.addEventListener('compositionstart', handleCompositionUpdate); + input.addEventListener('compositionend', handleCompositionEnd); + input.addEventListener('compositionupdate', handleCompositionUpdate); + input.addEventListener('keydown', handleKeyDown); + return () => { + input.removeEventListener('keydown', handleKeyDown); + input.removeEventListener('compositionstart', handleCompositionUpdate); + input.removeEventListener('compositionend', handleCompositionEnd); + input.removeEventListener('compositionupdate', handleCompositionUpdate); + }; + }, [link, onClose, setNodeMark]); + + const onConfirm = useCallback( + (key: string) => { + if (key === 'open') { + openUrl(link); + } else if (key === 'copy') { + void navigator.clipboard.writeText(link); + notify.success(t('message.copy.success')); + } else if (key === 'remove') { + removeNodeMark(); + } + }, + [link, removeNodeMark, t] + ); + + const renderOption = useCallback((icon: React.ReactNode, label: string) => { + return ( +
+ {icon} +
{label}
+
+ ); + }, []); + + const editOptions: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: 'open', + disabled: !isUrl(link), + content: renderOption(, t('editor.openLink')), + }, + { + key: 'copy', + content: renderOption(, t('editor.copyLink')), + }, + { + key: 'remove', + content: renderOption(, t('editor.removeLink')), + }, + ]; + }, [link, renderOption, t]); + + return ( + <> + {!isActivated && ( + {t('editor.addYourLink')} + )} + + +
+ {isActivated && ( + { + setFocusMenu(true); + }} + onBlur={() => { + setFocusMenu(false); + }} + disableSelect={!focusMenu} + onEscape={onClose} + onKeyDown={(e) => { + e.stopPropagation(); + if (isHotkey('Tab', e)) { + e.preventDefault(); + setFocusMenu(false); + inputRef.current?.focus(); + } + }} + /> + )} +
+ + ); +} + +export default LinkEditContent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx new file mode 100644 index 0000000000..6e9a0bb497 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, useState } from 'react'; +import { TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { isUrl } from '$app/utils/open_url'; + +function LinkEditInput({ + link, + setLink, + inputRef, +}: { + link: string; + setLink: (link: string) => void; + inputRef: React.RefObject; +}) { + const { t } = useTranslation(); + const [error, setError] = useState(null); + + useEffect(() => { + if (isUrl(link)) { + setError(null); + return; + } + + setError(t('editor.incorrectLink')); + }, [link, t]); + + return ( +
+ setLink(e.target.value)} + spellCheck={false} + inputRef={inputRef} + className={'my-1 p-0'} + placeholder={'https://example.com'} + fullWidth={true} + /> + + ); +} + +export default LinkEditInput; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx new file mode 100644 index 0000000000..2a5e3630da --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; + +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import LinkEditContent from '$app/components/editor/components/inline_nodes/link/LinkEditContent'; + +const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'center', +}; + +const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'center', +}; + +export function LinkEditPopover({ + defaultHref, + open, + onClose, + anchorPosition, + anchorReference, +}: { + defaultHref: string; + open: boolean; + onClose: () => void; + anchorPosition?: { top: number; left: number; height: number }; + anchorReference?: 'anchorPosition' | 'anchorEl'; +}) { + const { + paperHeight, + anchorPosition: newAnchorPosition, + transformOrigin, + anchorOrigin, + } = usePopoverAutoPosition({ + anchorPosition, + open, + initialAnchorOrigin, + initialTransformOrigin, + initialPaperWidth: 340, + initialPaperHeight: 200, + }); + + return ( + { + onClose(); + }} + transformOrigin={transformOrigin} + anchorOrigin={anchorOrigin} + onMouseDown={(e) => e.stopPropagation()} + > +
+ +
+
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/index.ts new file mode 100644 index 0000000000..295683a3bc --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/index.ts @@ -0,0 +1,3 @@ +export * from './Link'; + +export * from './LinkEditPopover'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/Mention.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/Mention.tsx new file mode 100644 index 0000000000..7511147ad0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/Mention.tsx @@ -0,0 +1,18 @@ +import React, { forwardRef, memo } from 'react'; +import { EditorElementProps, MentionNode } from '$app/application/document/document.types'; + +import MentionLeaf from '$app/components/editor/components/inline_nodes/mention/MentionLeaf'; +import { InlineChromiumBugfix } from '$app/components/editor/components/inline_nodes/InlineChromiumBugfix'; + +export const Mention = memo( + forwardRef>(({ node, children, ...attributes }, ref) => { + return ( + + + {children} + + + + ); + }) +); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx new file mode 100644 index 0000000000..10def395c5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx @@ -0,0 +1,141 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { Mention, MentionPage } from '$app/application/document/document.types'; +import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; +import { useTranslation } from 'react-i18next'; +import { getPage } from '$app/application/folder/page.service'; +import { useSelected, useSlate } from 'slate-react'; +import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; +import { notify } from 'src/appflowy_app/components/_shared/notify'; +import { subscribeNotifications } from '$app/application/notification'; +import { FolderNotification } from '@/services/backend'; +import { Editor, Range } from 'slate'; +import { useAppDispatch } from '$app/stores/store'; +import { openPage } from '$app_reducers/pages/async_actions'; + +export function MentionLeaf({ mention }: { mention: Mention }) { + const { t } = useTranslation(); + const [page, setPage] = useState(null); + const [error, setError] = useState(false); + const editor = useSlate(); + const selected = useSelected(); + const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (selected && isCollapsed && page) { + const afterPoint = editor.selection ? editor.after(editor.selection) : undefined; + + const afterStart = afterPoint ? Editor.start(editor, afterPoint) : undefined; + + if (afterStart) { + editor.select(afterStart); + } + } + }, [editor, isCollapsed, selected, page]); + + const loadPage = useCallback(async () => { + setError(true); + // keep old field for backward compatibility + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const pageId = mention.page_id ?? mention.page; + + if (!pageId) return; + try { + const page = await getPage(pageId); + + setPage(page); + setError(false); + } catch { + setPage(null); + setError(true); + } + }, [mention]); + + useEffect(() => { + void loadPage(); + }, [loadPage]); + + const handleOpenPage = useCallback(() => { + if (!page) { + notify.error(t('document.mention.deletedContent')); + return; + } + + void dispatch(openPage(page.id)); + }, [page, dispatch, t]); + + useEffect(() => { + if (!page) return; + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateView]: (changeset) => { + setPage((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + name: changeset.name, + }; + }); + }, + }, + { + id: page.id, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); + + useEffect(() => { + const parentId = page?.parentId; + + if (!parentId) return; + + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.delete_child_views.includes(page.id)) { + setPage(null); + setError(true); + } + }, + }, + { + id: parentId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); + + return ( + + {error ? ( + <> + + {t('document.mention.deleted')} + + ) : ( + page && ( + <> + {page.icon?.value || } + {page.name.trim() || t('menuAppHeader.defaultNewPageName')} + + ) + )} + + ); +} + +export default MentionLeaf; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/index.ts new file mode 100644 index 0000000000..d3ee18034d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/index.ts @@ -0,0 +1 @@ +export * from './Mention'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/withInline.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/withInline.ts new file mode 100644 index 0000000000..2859c1f0a8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/withInline.ts @@ -0,0 +1,31 @@ +import { ReactEditor } from 'slate-react'; +import { EditorInlineNodeType, inlineNodeTypes } from '$app/application/document/document.types'; +import { Element } from 'slate'; + +export function withInlines(editor: ReactEditor) { + const { isInline, isElementReadOnly, isSelectable, isVoid, markableVoid } = editor; + + const matchInlineType = (element: Element) => { + return inlineNodeTypes.includes(element.type as EditorInlineNodeType); + }; + + editor.isInline = (element) => { + return matchInlineType(element) || isInline(element); + }; + + editor.isVoid = (element) => { + return matchInlineType(element) || isVoid(element); + }; + + editor.markableVoid = (element) => { + return matchInlineType(element) || markableVoid(element); + }; + + editor.isElementReadOnly = (element) => + inlineNodeTypes.includes(element.type as EditorInlineNodeType) || isElementReadOnly(element); + + editor.isSelectable = (element) => + !inlineNodeTypes.includes(element.type as EditorInlineNodeType) && isSelectable(element); + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx new file mode 100644 index 0000000000..41dea96f1e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/ColorPicker.tsx @@ -0,0 +1,174 @@ +import React, { useCallback, useRef, useMemo } from 'react'; +import Typography from '@mui/material/Typography'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { useTranslation } from 'react-i18next'; +import { TitleOutlined } from '@mui/icons-material'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { ColorEnum, renderColor } from '$app/utils/color'; + +export interface ColorPickerProps { + onChange?: (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => void; + onEscape?: () => void; + disableFocus?: boolean; +} +export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerProps) { + const { t } = useTranslation(); + + const ref = useRef(null); + + const handleColorChange = useCallback( + (key: string) => { + const [format, , color = ''] = key.split('-'); + const formatKey = format === 'font' ? EditorMarkFormat.FontColor : EditorMarkFormat.BgColor; + + onChange?.(formatKey, color); + }, + [onChange] + ); + + const renderColorItem = useCallback( + (name: string, color: string, backgroundColor?: string) => { + return ( +
{ + handleColorChange(backgroundColor ? backgroundColor : color); + }} + className={'flex w-full cursor-pointer items-center justify-center gap-2'} + > +
+ +
+
{name}
+
+ ); + }, + [handleColorChange] + ); + + const colors: KeyboardNavigationOption[] = useMemo(() => { + return [ + { + key: 'font_color', + content: ( + + {t('editor.textColor')} + + ), + children: [ + { + key: 'font-default', + content: renderColorItem(t('editor.fontColorDefault'), ''), + }, + { + key: `font-gray-rgb(120, 119, 116)`, + content: renderColorItem(t('editor.fontColorGray'), 'rgb(120, 119, 116)'), + }, + { + key: 'font-brown-rgb(159, 107, 83)', + content: renderColorItem(t('editor.fontColorBrown'), 'rgb(159, 107, 83)'), + }, + { + key: 'font-orange-rgb(217, 115, 13)', + content: renderColorItem(t('editor.fontColorOrange'), 'rgb(217, 115, 13)'), + }, + { + key: 'font-yellow-rgb(203, 145, 47)', + content: renderColorItem(t('editor.fontColorYellow'), 'rgb(203, 145, 47)'), + }, + { + key: 'font-green-rgb(68, 131, 97)', + content: renderColorItem(t('editor.fontColorGreen'), 'rgb(68, 131, 97)'), + }, + { + key: 'font-blue-rgb(51, 126, 169)', + content: renderColorItem(t('editor.fontColorBlue'), 'rgb(51, 126, 169)'), + }, + { + key: 'font-purple-rgb(144, 101, 176)', + content: renderColorItem(t('editor.fontColorPurple'), 'rgb(144, 101, 176)'), + }, + { + key: 'font-pink-rgb(193, 76, 138)', + content: renderColorItem(t('editor.fontColorPink'), 'rgb(193, 76, 138)'), + }, + { + key: 'font-red-rgb(212, 76, 71)', + content: renderColorItem(t('editor.fontColorRed'), 'rgb(212, 76, 71)'), + }, + ], + }, + { + key: 'bg_color', + content: ( + + {t('editor.backgroundColor')} + + ), + children: [ + { + key: 'bg-default', + content: renderColorItem(t('editor.backgroundColorDefault'), '', ''), + }, + { + key: `bg-lime-${ColorEnum.Lime}`, + content: renderColorItem(t('editor.backgroundColorLime'), '', ColorEnum.Lime), + }, + { + key: `bg-aqua-${ColorEnum.Aqua}`, + content: renderColorItem(t('editor.backgroundColorAqua'), '', ColorEnum.Aqua), + }, + { + key: `bg-orange-${ColorEnum.Orange}`, + content: renderColorItem(t('editor.backgroundColorOrange'), '', ColorEnum.Orange), + }, + { + key: `bg-yellow-${ColorEnum.Yellow}`, + content: renderColorItem(t('editor.backgroundColorYellow'), '', ColorEnum.Yellow), + }, + { + key: `bg-green-${ColorEnum.Green}`, + content: renderColorItem(t('editor.backgroundColorGreen'), '', ColorEnum.Green), + }, + { + key: `bg-blue-${ColorEnum.Blue}`, + content: renderColorItem(t('editor.backgroundColorBlue'), '', ColorEnum.Blue), + }, + { + key: `bg-purple-${ColorEnum.Purple}`, + content: renderColorItem(t('editor.backgroundColorPurple'), '', ColorEnum.Purple), + }, + { + key: `bg-pink-${ColorEnum.Pink}`, + content: renderColorItem(t('editor.backgroundColorPink'), '', ColorEnum.Pink), + }, + { + key: `bg-red-${ColorEnum.LightPink}`, + content: renderColorItem(t('editor.backgroundColorRed'), '', ColorEnum.LightPink), + }, + ], + }, + ]; + }, [renderColorItem, t]); + + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/CustomColorPicker.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/CustomColorPicker.tsx new file mode 100644 index 0000000000..ae463a2ff3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/CustomColorPicker.tsx @@ -0,0 +1,27 @@ +import React, { useState } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { Color, SketchPicker } from 'react-color'; + +import { Divider } from '@mui/material'; + +export function CustomColorPicker({ + onColorChange, + ...props +}: { + onColorChange?: (color: string) => void; +} & PopoverProps) { + const [color, setColor] = useState(); + + return ( + + { + setColor(color.rgb); + onColorChange?.(color.hex); + }} + color={color} + /> + + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/index.ts new file mode 100644 index 0000000000..00e212aa7f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/_shared/index.ts @@ -0,0 +1,2 @@ +export * from './CustomColorPicker'; +export * from './ColorPicker'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/AddBlockBelow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/AddBlockBelow.tsx new file mode 100644 index 0000000000..eb2675bc71 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/AddBlockBelow.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { ReactEditor, useSlate } from 'slate-react'; +import { IconButton, Tooltip } from '@mui/material'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useTranslation } from 'react-i18next'; +import { Element, Path } from 'slate'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { YjsEditor } from '@slate-yjs/core'; +import { useSlashState } from '$app/components/editor/stores'; + +function AddBlockBelow({ node }: { node?: Element }) { + const { t } = useTranslation(); + const editor = useSlate(); + const { setOpen: setSlashOpen } = useSlashState(); + + const handleAddBelow = () => { + if (!node) return; + ReactEditor.focus(editor); + + const nodePath = ReactEditor.findPath(editor, node); + const nextPath = Path.next(nodePath); + + editor.select(nodePath); + + if (editor.isSelectable(node)) { + editor.collapse({ + edge: 'start', + }); + } + + const isEmptyNode = CustomEditor.isEmptyText(editor, node); + + // if the node is not a paragraph, or it is not empty, insert a new empty line + if (node.type !== EditorNodeType.Paragraph || !isEmptyNode) { + CustomEditor.insertEmptyLine(editor as ReactEditor & YjsEditor, nextPath); + editor.select(nextPath); + } + + requestAnimationFrame(() => { + setSlashOpen(true); + }); + }; + + return ( + <> + + + + + + + ); +} + +export default AddBlockBelow; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActions.tsx new file mode 100644 index 0000000000..f5833e538b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActions.tsx @@ -0,0 +1,30 @@ +import React from 'react'; + +import { Element } from 'slate'; +import AddBlockBelow from '$app/components/editor/components/tools/block_actions/AddBlockBelow'; +import { ReactComponent as DragSvg } from '$app/assets/drag.svg'; +import { IconButton, Tooltip } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export function BlockActions({ + node, + onClickDrag, +}: { + node?: Element; + onClickDrag: (e: React.MouseEvent) => void; +}) { + const { t } = useTranslation(); + + return ( + <> + + + + + + + + ); +} + +export default BlockActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts new file mode 100644 index 0000000000..bc1086dde9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts @@ -0,0 +1,146 @@ +import { RefObject, useCallback, useEffect, useState } from 'react'; +import { ReactEditor, useSlate } from 'slate-react'; +import { findEventNode, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils'; +import { Element, Editor, Range } from 'slate'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { Log } from '$app/utils/log'; + +export function useBlockActionsToolbar(ref: RefObject, contextMenuVisible: boolean) { + const editor = useSlate(); + const [node, setNode] = useState(null); + + const recalculatePosition = useCallback( + (blockElement: HTMLElement) => { + const { top, left } = getBlockActionsPosition(editor, blockElement); + + const slateEditorDom = ReactEditor.toDOMNode(editor, editor); + + if (!ref.current) return; + + ref.current.style.top = `${top + slateEditorDom.offsetTop}px`; + ref.current.style.left = `${left + slateEditorDom.offsetLeft - 64}px`; + }, + [editor, ref] + ); + + const close = useCallback(() => { + const el = ref.current; + + if (!el) return; + + el.style.opacity = '0'; + el.style.pointerEvents = 'none'; + setNode(null); + }, [ref]); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + const el = ref.current; + + if (!el) return; + + const target = e.target as HTMLElement; + + if (target.closest(`[contenteditable="false"]`)) { + return; + } + + let range: Range | null = null; + let node; + + try { + range = ReactEditor.findEventRange(editor, e); + } catch { + const editorDom = ReactEditor.toDOMNode(editor, editor); + const rect = editorDom.getBoundingClientRect(); + const isOverLeftBoundary = e.clientX < rect.left + 64; + const isOverRightBoundary = e.clientX > rect.right - 64; + let newX = e.clientX; + + if (isOverLeftBoundary) { + newX = rect.left + 64; + } + + if (isOverRightBoundary) { + newX = rect.right - 64; + } + + node = findEventNode(editor, { + x: newX, + y: e.clientY, + }); + } + + if (!range && !node) { + Log.warn('No range and node found'); + return; + } else if (range) { + const match = editor.above({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined; + }, + at: range, + }); + + if (!match) { + close(); + return; + } + + node = match[0] as Element; + } + + if (!node) { + close(); + return; + } + + if (node.type === EditorNodeType.Page) return; + const blockElement = ReactEditor.toDOMNode(editor, node); + + if (!blockElement) return; + recalculatePosition(blockElement); + el.style.opacity = '1'; + el.style.pointerEvents = 'auto'; + const slateNode = ReactEditor.toSlateNode(editor, blockElement) as Element; + + setNode(slateNode); + }; + + const dom = ReactEditor.toDOMNode(editor, editor); + + if (!contextMenuVisible) { + dom.addEventListener('mousemove', handleMouseMove); + dom.parentElement?.addEventListener('mouseleave', close); + } + + return () => { + dom.removeEventListener('mousemove', handleMouseMove); + dom.parentElement?.removeEventListener('mouseleave', close); + }; + }, [close, editor, contextMenuVisible, ref, recalculatePosition]); + + useEffect(() => { + let observer: MutationObserver | null = null; + + if (node) { + const dom = ReactEditor.toDOMNode(editor, node); + + if (dom.parentElement) { + observer = new MutationObserver(close); + + observer.observe(dom.parentElement, { + childList: true, + }); + } + } + + return () => { + observer?.disconnect(); + }; + }, [close, editor, node]); + + return { + node: node?.type === EditorNodeType.Page ? null : node, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx new file mode 100644 index 0000000000..729b4df144 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx @@ -0,0 +1,141 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { useBlockActionsToolbar } from './BlockActionsToolbar.hooks'; +import BlockActions from '$app/components/editor/components/tools/block_actions/BlockActions'; + +import { getBlockCssProperty } from '$app/components/editor/components/tools/block_actions/utils'; +import BlockOperationMenu from '$app/components/editor/components/tools/block_actions/BlockOperationMenu'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { PopoverProps } from '@mui/material/Popover'; + +import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; +import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; +import { CustomEditor } from '$app/components/editor/command'; +import isEqual from 'lodash-es/isEqual'; +import { Range } from 'slate'; + +const Toolbar = () => { + const ref = useRef(null); + const [openContextMenu, setOpenContextMenu] = useState(false); + const { node } = useBlockActionsToolbar(ref, openContextMenu); + const cssProperty = node && getBlockCssProperty(node); + const selectedBlockContext = useContext(EditorSelectedBlockContext); + const popoverPropsRef = useRef | undefined>(undefined); + const editor = useSlateStatic(); + + const handleOpen = useCallback(() => { + if (!node || !node.blockId) return; + setOpenContextMenu(true); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + selectedBlockContext.clear(); + selectedBlockContext.add(node.blockId); + }, [editor, node, selectedBlockContext]); + + const handleClose = useCallback(() => { + setOpenContextMenu(false); + selectedBlockContext.clear(); + }, [selectedBlockContext]); + + useEffect(() => { + if (!node) return; + const nodeDom = ReactEditor.toDOMNode(editor, node); + const onContextMenu = (e: MouseEvent) => { + const { clientX, clientY } = e; + + e.stopPropagation(); + + const { selection } = editor; + + const editorRange = ReactEditor.findEventRange(editor, e); + + if (!editorRange || !selection) return; + + const rangeBlock = CustomEditor.getBlock(editor, editorRange); + const selectedBlock = CustomEditor.getBlock(editor, selection); + + if ( + Range.intersection(selection, editorRange) || + (rangeBlock && selectedBlock && isEqual(rangeBlock[1], selectedBlock[1])) + ) { + const windowSelection = window.getSelection(); + const range = windowSelection?.rangeCount ? windowSelection?.getRangeAt(0) : null; + const isCollapsed = windowSelection?.isCollapsed; + + if (windowSelection && !isCollapsed) { + if (range && range.endOffset === 0 && range.startContainer !== range.endContainer) { + const newRange = range.cloneRange(); + + newRange.setEnd(range.startContainer, range.startOffset); + windowSelection.removeAllRanges(); + windowSelection.addRange(newRange); + } + } + + return; + } + + e.preventDefault(); + + popoverPropsRef.current = { + transformOrigin: { + vertical: 'top', + horizontal: 'left', + }, + anchorReference: 'anchorPosition', + anchorPosition: { + top: clientY, + left: clientX, + }, + }; + + handleOpen(); + }; + + nodeDom.addEventListener('contextmenu', onContextMenu); + + return () => { + nodeDom.removeEventListener('contextmenu', onContextMenu); + }; + }, [editor, handleOpen, node]); + return ( + <> +
+ {/* Ensure the toolbar in middle */} +
$
+ { + ) => { + const target = e.currentTarget; + const rect = target.getBoundingClientRect(); + + popoverPropsRef.current = { + transformOrigin: { + vertical: 'center', + horizontal: 'right', + }, + anchorReference: 'anchorPosition', + anchorPosition: { + top: rect.top + rect.height / 2, + left: rect.left, + }, + }; + + handleOpen(); + }} + /> + } +
+ {node && openContextMenu && ( + + )} + + ); +}; + +export const BlockActionsToolbar = withErrorBoundary(Toolbar); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx new file mode 100644 index 0000000000..ade9817503 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx @@ -0,0 +1,209 @@ +import React, { useCallback, useMemo } from 'react'; +import Popover, { PopoverProps } from '@mui/material/Popover'; +import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { useTranslation } from 'react-i18next'; +import { Divider } from '@mui/material'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import { Element, Path } from 'slate'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import KeyboardNavigation, { + KeyboardNavigationOption, +} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { Color } from '$app/components/editor/components/tools/block_actions/color'; +import { getModifier } from '$app/utils/hotkeys'; + +import isHotkey from 'is-hotkey'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; + +export const canSetColorBlocks: EditorNodeType[] = [ + EditorNodeType.Paragraph, + EditorNodeType.HeadingBlock, + EditorNodeType.TodoListBlock, + EditorNodeType.BulletedListBlock, + EditorNodeType.NumberedListBlock, + EditorNodeType.ToggleListBlock, + EditorNodeType.QuoteBlock, + EditorNodeType.CalloutBlock, +]; + +export function BlockOperationMenu({ + node, + ...props +}: { + node: Element; +} & PopoverProps) { + const editor = useSlateStatic(); + const { t } = useTranslation(); + + const canSetColor = useMemo(() => { + return canSetColorBlocks.includes(node.type as EditorNodeType); + }, [node]); + const selectedBlockContext = React.useContext(EditorSelectedBlockContext); + const [openColorMenu, setOpenColorMenu] = React.useState(false); + const ref = React.useRef(null); + const handleClose = useCallback(() => { + props.onClose?.({}, 'backdropClick'); + ReactEditor.focus(editor); + try { + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + } catch (e) { + // do nothing + } + + editor.collapse({ + edge: 'start', + }); + }, [editor, node, props]); + + const onConfirm = useCallback( + (optionKey: string) => { + switch (optionKey) { + case 'delete': { + CustomEditor.deleteNode(editor, node); + break; + } + + case 'duplicate': { + const path = ReactEditor.findPath(editor, node); + const newNode = CustomEditor.duplicateNode(editor, node); + + handleClose(); + + const newBlockId = newNode.blockId; + + if (!newBlockId) return; + requestAnimationFrame(() => { + selectedBlockContext.clear(); + selectedBlockContext.add(newBlockId); + const nextPath = Path.next(path); + + editor.select(nextPath); + editor.collapse({ + edge: 'start', + }); + }); + return; + } + + case 'color': { + setOpenColorMenu(true); + return; + } + } + + handleClose(); + }, + [editor, handleClose, node, selectedBlockContext] + ); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const options: KeyboardNavigationOption[] = useMemo( + () => + [ + { + key: 'block-operation', + children: [ + { + key: 'delete', + content: ( +
+ +
{t('button.delete')}
+
{'Del'}
+
+ ), + }, + { + key: 'duplicate', + content: ( +
+ +
{t('button.duplicate')}
+
{`${getModifier()} + D`}
+
+ ), + }, + ], + }, + canSetColor && { + key: 'color', + content: , + children: [ + { + key: 'color', + content: ( + { + setOpenColorMenu(false); + }} + openPicker={openColorMenu} + onOpenPicker={() => setOpenColorMenu(true)} + /> + ), + }, + ], + }, + ].filter(Boolean), + [node, canSetColor, openColorMenu, t] + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + e.stopPropagation(); + if (isHotkey('mod+d', e)) { + e.preventDefault(); + onConfirm('duplicate'); + } + + if (isHotkey('del', e) || isHotkey('backspace', e)) { + e.preventDefault(); + onConfirm('delete'); + } + }, + [onConfirm] + ); + + return ( + e.stopPropagation()} + {...props} + onClose={handleClose} + > +
+ { + if (key === 'color') { + onConfirm(key); + } else { + handleClose(); + } + }} + options={options} + scrollRef={ref} + onEscape={handleClose} + onConfirm={onConfirm} + /> +
+
+ ); +} + +export default BlockOperationMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/Color.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/Color.tsx new file mode 100644 index 0000000000..499ab95c76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/Color.tsx @@ -0,0 +1,90 @@ +import React, { useCallback, useRef } from 'react'; +import { Element } from 'slate'; +import { Popover } from '@mui/material'; +import ColorLensOutlinedIcon from '@mui/icons-material/ColorLensOutlined'; +import { useTranslation } from 'react-i18next'; +import { ColorPicker } from '$app/components/editor/components/tools/_shared'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { useSlateStatic } from 'slate-react'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; + +const initialOrigin: { + transformOrigin?: PopoverOrigin; + anchorOrigin?: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'center', + horizontal: 'right', + }, + transformOrigin: { + vertical: 'center', + horizontal: 'left', + }, +}; + +export function Color({ + node, + openPicker, + onOpenPicker, + onClosePicker, +}: { + node: Element & { + data?: { + font_color?: string; + bg_color?: string; + }; + }; + openPicker?: boolean; + onOpenPicker?: () => void; + onClosePicker?: () => void; +}) { + const { t } = useTranslation(); + + const editor = useSlateStatic(); + + const ref = useRef(null); + + const onColorChange = useCallback( + (format: 'font_color' | 'bg_color', color: string) => { + CustomEditor.setBlockColor(editor, node, { + [format]: color, + }); + onClosePicker?.(); + }, + [editor, node, onClosePicker] + ); + + return ( + <> +
+ +
{t('editor.color')}
+ +
+ {openPicker && ( + { + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + if (e.key === 'Escape' || e.key === 'ArrowLeft') { + e.preventDefault(); + onClosePicker?.(); + } + }} + onClick={(e) => e.stopPropagation()} + anchorEl={ref.current} + onClose={onClosePicker} + > +
+ +
+
+ )} + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/index.ts new file mode 100644 index 0000000000..0fd619bc86 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/color/index.ts @@ -0,0 +1 @@ +export * from './Color'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/index.ts new file mode 100644 index 0000000000..e8b87721c2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/index.ts @@ -0,0 +1,2 @@ +export * from './BlockActions'; +export * from './BlockActionsToolbar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts new file mode 100644 index 0000000000..b63afe9dc1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts @@ -0,0 +1,58 @@ +import { ReactEditor } from 'slate-react'; +import { getEditorDomNode, getHeadingCssProperty } from '$app/components/editor/plugins/utils'; +import { Element } from 'slate'; +import { EditorNodeType, HeadingNode } from '$app/application/document/document.types'; + +export function getBlockActionsPosition(editor: ReactEditor, blockElement: HTMLElement) { + const editorDom = getEditorDomNode(editor); + const editorDomRect = editorDom.getBoundingClientRect(); + const blockDomRect = blockElement.getBoundingClientRect(); + + const relativeTop = blockDomRect.top - editorDomRect.top; + const relativeLeft = blockDomRect.left - editorDomRect.left; + + return { + top: relativeTop, + left: relativeLeft, + }; +} + +export function getBlockCssProperty(node: Element) { + switch (node.type) { + case EditorNodeType.HeadingBlock: + return `${getHeadingCssProperty((node as HeadingNode).data.level)} mt-1`; + case EditorNodeType.CodeBlock: + case EditorNodeType.CalloutBlock: + case EditorNodeType.EquationBlock: + case EditorNodeType.GridBlock: + return 'my-3'; + case EditorNodeType.DividerBlock: + return 'my-0'; + default: + return 'mt-1'; + } +} + +/** + * @param editor + * @param e + */ +export function findEventNode( + editor: ReactEditor, + { + x, + y, + }: { + x: number; + y: number; + } +) { + const element = document.elementFromPoint(x, y); + const nodeDom = element?.closest('[data-block-type]'); + + if (nodeDom) { + return ReactEditor.toSlateNode(editor, nodeDom) as Element; + } + + return null; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/Command.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/Command.hooks.ts new file mode 100644 index 0000000000..633d09349d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/Command.hooks.ts @@ -0,0 +1,259 @@ +import { useEffect, useState, useCallback, useRef } from 'react'; +import { getPanelPosition } from '$app/components/editor/components/tools/command_panel/utils'; +import { useSlate } from 'slate-react'; +import { PopoverPreventBlurProps } from '$app/components/editor/components/tools/popover'; +import { PopoverProps } from '@mui/material/Popover'; + +import { Editor, Point, Range, Transforms } from 'slate'; +import { CustomEditor } from '$app/components/editor/command'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import { useSlashState } from '$app/components/editor/stores'; + +export enum EditorCommand { + Mention = '@', + SlashCommand = '/', +} + +export const PanelPopoverProps: Partial = { + ...PopoverPreventBlurProps, + anchorReference: 'anchorPosition', +}; + +const commands = Object.values(EditorCommand); + +export interface PanelProps { + anchorPosition?: { left: number; top: number; height: number }; + closePanel: (deleteText?: boolean) => void; + searchText: string; + openPanel: () => void; +} + +export function useCommandPanel() { + const editor = useSlate(); + const { open: slashOpen, setOpen: setSlashOpen } = useSlashState(); + const [command, setCommand] = useState(undefined); + const [anchorPosition, setAnchorPosition] = useState< + | { + top: number; + left: number; + height: number; + } + | undefined + >(undefined); + const startPoint = useRef(); + const endPoint = useRef(); + const open = Boolean(anchorPosition); + const [searchText, setSearchText] = useState(''); + + const closePanel = useCallback( + (deleteText?: boolean) => { + if (deleteText && startPoint.current && endPoint.current) { + const anchor = { + path: startPoint.current.path, + offset: startPoint.current.offset > 0 ? startPoint.current.offset - 1 : 0, + }; + const focus = { + path: endPoint.current.path, + offset: endPoint.current.offset, + }; + + if (!Point.equals(anchor, focus)) { + Transforms.delete(editor, { + at: { + anchor, + focus, + }, + }); + } + } + + setSlashOpen(false); + setCommand(undefined); + setAnchorPosition(undefined); + setSearchText(''); + }, + [editor, setSlashOpen] + ); + + const setPosition = useCallback( + (position?: { left: number; top: number; height: number }) => { + if (!position) { + closePanel(false); + return; + } + + const nodeEntry = CustomEditor.getBlock(editor); + + if (!nodeEntry) return; + + setAnchorPosition(position); + }, + [closePanel, editor] + ); + + const openPanel = useCallback(() => { + const position = getPanelPosition(editor); + + if (position && editor.selection) { + startPoint.current = Editor.start(editor, editor.selection); + endPoint.current = Editor.end(editor, editor.selection); + setPosition(position); + } else { + setPosition(undefined); + } + }, [editor, setPosition]); + + useEffect(() => { + if (!slashOpen && command === EditorCommand.SlashCommand) { + closePanel(); + return; + } + + if (slashOpen && !open) { + setCommand(EditorCommand.SlashCommand); + openPanel(); + return; + } + }, [slashOpen, closePanel, command, open, openPanel]); + /** + * listen to editor insertText and deleteBackward event + */ + useEffect(() => { + const { insertText } = editor; + + /** + * insertText: when insert char at after space or at start of element, show the panel + * open condition: + * 1. open is false + * 2. current block is not code block + * 3. current selection is not include root + * 4. current selection is collapsed + * 5. insert char is command char + * 6. before text is empty or end with space + * --------- start ----------------- + * | - selection point + * @ - panel char + * _ - space + * - - other text + * -------- open panel ---------------- + * ---_@|--- => insert text is panel char and before text is end with space, open the panel + * @|--- => insert text is panel char and before text is empty, open the panel + */ + editor.insertText = (text, opts) => { + if (open || CustomEditor.isCodeBlock(editor) || CustomEditor.selectionIncludeRoot(editor)) { + insertText(text, opts); + return; + } + + const { selection } = editor; + + const command = commands.find((c) => text.endsWith(c)); + const endOfPanelChar = !!command; + + if (command === EditorCommand.SlashCommand) { + setSlashOpen(true); + } + + setCommand(command); + if (!selection || !endOfPanelChar || !Range.isCollapsed(selection)) { + insertText(text, opts); + return; + } + + const block = CustomEditor.getBlock(editor); + const path = block ? block[1] : []; + const { anchor } = selection; + const beforeText = Editor.string(editor, { anchor, focus: Editor.start(editor, path) }) + text.slice(0, -1); + // show the panel when insert char at after space or at start of element + const showPanel = !beforeText || beforeText.endsWith(' '); + + insertText(text, opts); + + if (!showPanel) return; + openPanel(); + }; + + return () => { + editor.insertText = insertText; + }; + }, [open, editor, openPanel, setSlashOpen]); + + /** + * listen to editor onChange event + */ + useEffect(() => { + const { onChange } = editor; + + if (!open) return; + + /** + * onChange: when selection change, update the search text or close the panel + * --------- start ----------------- + * | - selection point + * @ - panel char + * __ - search text + * - - other text + * -------- close panel ---------------- + * --|@--- => selection is backward to start point, close the panel + * ---@__-|--- => selection is forward to end point, close the panel + * -------- update search text ---------------- + * ---@__|--- + * ---@_|_--- => selection is forward to start point and backward to end point, update the search text + * ---@|__--- + * --------- end ----------------- + */ + editor.onChange = (...args) => { + if (!editor.selection || !startPoint.current || !endPoint.current) return; + onChange(...args); + const isSelectionChange = editor.operations.every((op) => op.type === 'set_selection'); + const currentPoint = Editor.end(editor, editor.selection); + const isBackward = currentPoint.offset < startPoint.current.offset; + + if (isBackward) { + closePanel(false); + return; + } + + if (!isSelectionChange) { + if (currentPoint.offset > endPoint.current?.offset) { + endPoint.current = currentPoint; + } + + const text = Editor.string(editor, { + anchor: startPoint.current, + focus: endPoint.current, + }); + + setSearchText(text); + } else { + const isForward = currentPoint.offset > endPoint.current.offset; + + if (isForward) { + closePanel(false); + } + } + }; + + return () => { + editor.onChange = onChange; + }; + }, [open, editor, closePanel]); + + return { + anchorPosition, + closePanel, + searchText, + openPanel, + command, + }; +} + +export const initialTransformOrigin: PopoverOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +export const initialAnchorOrigin: PopoverOrigin = { + vertical: 'bottom', + horizontal: 'right', +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/CommandPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/CommandPanel.tsx new file mode 100644 index 0000000000..db58e2deca --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/CommandPanel.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { SlashCommandPanel } from '$app/components/editor/components/tools/command_panel/slash_command_panel'; +import { MentionPanel } from '$app/components/editor/components/tools/command_panel/mention_panel'; +import { EditorCommand, useCommandPanel } from '$app/components/editor/components/tools/command_panel/Command.hooks'; +import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; + +function CommandPanel() { + const { anchorPosition, searchText, openPanel, closePanel, command } = useCommandPanel(); + + const Component = command === EditorCommand.SlashCommand ? SlashCommandPanel : MentionPanel; + + return ( + + ); +} + +export default withErrorBoundary(CommandPanel); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/index.ts new file mode 100644 index 0000000000..cf07c7d996 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/index.ts @@ -0,0 +1,2 @@ +export * from './mention_panel'; +export * from './slash_command_panel'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx new file mode 100644 index 0000000000..5d83870719 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlate } from 'slate-react'; +import { MentionPage, MentionType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; +// import dayjs from 'dayjs'; + +// enum DateKey { +// Today = 'today', +// Tomorrow = 'tomorrow', +// } +export function useMentionPanel({ + closePanel, + pages, +}: { + pages: MentionPage[]; + closePanel: (deleteText?: boolean) => void; +}) { + const { t } = useTranslation(); + const editor = useSlate(); + + const onConfirm = useCallback( + (key: string) => { + const [, id] = key.split(','); + + closePanel(true); + CustomEditor.insertMention(editor, { + page_id: id, + type: MentionType.PageRef, + }); + }, + [closePanel, editor] + ); + + const renderPage = useCallback( + (page: MentionPage) => { + return { + key: `${MentionType.PageRef},${page.id}`, + content: ( +
+
{page.icon?.value || }
+ +
{page.name.trim() || t('menuAppHeader.defaultNewPageName')}
+
+ ), + }; + }, + [t] + ); + + // const renderDate = useCallback(() => { + // return [ + // { + // key: DateKey.Today, + // content: ( + //
+ // {t('relativeDates.today')} -{' '} + // {dayjs().format('MMM D, YYYY')} + //
+ // ), + // + // children: [], + // }, + // { + // key: DateKey.Tomorrow, + // content: ( + //
+ // {t('relativeDates.tomorrow')} + //
+ // ), + // children: [], + // }, + // ]; + // }, [t]); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return [ + // { + // key: MentionType.Date, + // content:
{t('editor.date')}
, + // children: renderDate(), + // }, + { + key: 'divider', + content:
, + children: [], + }, + + { + key: MentionType.PageRef, + content:
{t('document.mention.page.label')}
, + children: + pages.length > 0 + ? pages.map(renderPage) + : [ + { + key: 'noPage', + content: ( +
{t('findAndReplace.noResult')}
+ ), + children: [], + }, + ], + }, + ]; + }, [pages, renderPage, t]); + + return { + options, + onConfirm, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx new file mode 100644 index 0000000000..6ca0225579 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx @@ -0,0 +1,81 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { + initialAnchorOrigin, + initialTransformOrigin, + PanelPopoverProps, + PanelProps, +} from '$app/components/editor/components/tools/command_panel/Command.hooks'; +import Popover from '@mui/material/Popover'; + +import MentionPanelContent from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { useAppSelector } from '$app/stores/store'; +import { MentionPage } from '$app/application/document/document.types'; + +export function MentionPanel({ anchorPosition, closePanel, searchText }: PanelProps) { + const ref = useRef(null); + const pagesMap = useAppSelector((state) => state.pages.pageMap); + + const pagesRef = useRef([]); + const [recentPages, setPages] = useState([]); + + const loadPages = useCallback(async () => { + const pages = Object.values(pagesMap); + + pagesRef.current = pages; + setPages(pages); + }, [pagesMap]); + + useEffect(() => { + void loadPages(); + }, [loadPages]); + + useEffect(() => { + if (!searchText) { + setPages(pagesRef.current); + return; + } + + const filteredPages = pagesRef.current.filter((page) => { + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + + setPages(filteredPages); + }, [searchText]); + const open = Boolean(anchorPosition); + + const { + paperHeight, + anchorPosition: newAnchorPosition, + paperWidth, + transformOrigin, + anchorOrigin, + isEntered, + } = usePopoverAutoPosition({ + initialPaperWidth: 300, + initialPaperHeight: 360, + anchorPosition, + initialTransformOrigin, + initialAnchorOrigin, + open, + }); + + return ( +
+ {open && ( + closePanel(false)} + > + + + )} +
+ ); +} + +export default MentionPanel; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx new file mode 100644 index 0000000000..36b00ca2b6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx @@ -0,0 +1,45 @@ +import React, { useRef } from 'react'; +import { useMentionPanel } from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks'; + +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { MentionPage } from '$app/application/document/document.types'; + +function MentionPanelContent({ + closePanel, + pages, + maxHeight, + width, +}: { + closePanel: (deleteText?: boolean) => void; + pages: MentionPage[]; + maxHeight: number; + width: number; +}) { + const scrollRef = useRef(null); + + const { options, onConfirm } = useMentionPanel({ + closePanel, + pages, + }); + + return ( +
+ +
+ ); +} + +export default MentionPanelContent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/index.ts new file mode 100644 index 0000000000..bfca34ef9a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/index.ts @@ -0,0 +1,2 @@ +export * from './MentionPanel'; +export * from './MentionPanelContent'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx new file mode 100644 index 0000000000..c2d9445b56 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx @@ -0,0 +1,245 @@ +import { EditorNodeType } from '$app/application/document/document.types'; +import { useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactEditor, useSlate } from 'slate-react'; +import { Path } from 'slate'; +import { getBlock } from '$app/components/editor/plugins/utils'; +import { ReactComponent as TextIcon } from '$app/assets/text.svg'; +import { ReactComponent as TodoListIcon } from '$app/assets/todo-list.svg'; +import { ReactComponent as Heading1Icon } from '$app/assets/h1.svg'; +import { ReactComponent as Heading2Icon } from '$app/assets/h2.svg'; +import { ReactComponent as Heading3Icon } from '$app/assets/h3.svg'; +import { ReactComponent as BulletedListIcon } from '$app/assets/list.svg'; +import { ReactComponent as NumberedListIcon } from '$app/assets/numbers.svg'; +import { ReactComponent as QuoteIcon } from '$app/assets/quote.svg'; +import { ReactComponent as ToggleListIcon } from '$app/assets/show-menu.svg'; +import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; +import { ReactComponent as ImageIcon } from '$app/assets/image.svg'; +import { DataObjectOutlined, FunctionsOutlined, HorizontalRuleOutlined, MenuBookOutlined } from '@mui/icons-material'; +import { CustomEditor } from '$app/components/editor/command'; +import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { YjsEditor } from '@slate-yjs/core'; +import { useEditorBlockDispatch } from '$app/components/editor/stores/block'; +import { + headingTypes, + headingTypeToLevelMap, + reorderSlashOptions, + SlashAliases, + SlashCommandPanelTab, + slashOptionGroup, + slashOptionMapToEditorNodeType, + SlashOptionType, +} from '$app/components/editor/components/tools/command_panel/slash_command_panel/const'; + +export function useSlashCommandPanel({ + searchText, + closePanel, +}: { + searchText: string; + closePanel: (deleteText?: boolean) => void; +}) { + const { openPopover } = useEditorBlockDispatch(); + const { t } = useTranslation(); + const editor = useSlate(); + const onConfirm = useCallback( + (type: SlashOptionType) => { + const node = getBlock(editor); + + if (!node) return; + + const nodeType = slashOptionMapToEditorNodeType[type]; + + if (!nodeType) return; + + const data = {}; + + if (headingTypes.includes(type)) { + Object.assign(data, { + level: headingTypeToLevelMap[type], + }); + } + + if (nodeType === EditorNodeType.CalloutBlock) { + Object.assign(data, { + icon: '📌', + }); + } + + if (nodeType === EditorNodeType.CodeBlock) { + Object.assign(data, { + language: 'json', + }); + } + + if (nodeType === EditorNodeType.ImageBlock) { + Object.assign(data, { + url: '', + }); + } + + closePanel(true); + + const newNode = getBlock(editor); + const block = CustomEditor.getBlock(editor); + + const path = block ? block[1] : null; + + if (!newNode || !path) return; + + const isEmpty = CustomEditor.isEmptyText(editor, newNode); + + if (!isEmpty) { + const nextPath = Path.next(path); + + CustomEditor.insertEmptyLine(editor as ReactEditor & YjsEditor, nextPath); + editor.select(nextPath); + } + + const turnIntoBlock = CustomEditor.turnToBlock(editor, { + type: nodeType, + data, + }); + + setTimeout(() => { + if (turnIntoBlock && turnIntoBlock.blockId) { + if (turnIntoBlock.type === EditorNodeType.ImageBlock || turnIntoBlock.type === EditorNodeType.EquationBlock) { + openPopover(turnIntoBlock.type, turnIntoBlock.blockId); + } + } + }, 0); + }, + [editor, closePanel, openPopover] + ); + + const typeToLabelIconMap = useMemo(() => { + return { + [SlashOptionType.Paragraph]: { + label: t('editor.text'), + Icon: TextIcon, + }, + [SlashOptionType.TodoList]: { + label: t('editor.checkbox'), + Icon: TodoListIcon, + }, + [SlashOptionType.Heading1]: { + label: t('editor.heading1'), + Icon: Heading1Icon, + }, + [SlashOptionType.Heading2]: { + label: t('editor.heading2'), + Icon: Heading2Icon, + }, + [SlashOptionType.Heading3]: { + label: t('editor.heading3'), + Icon: Heading3Icon, + }, + [SlashOptionType.BulletedList]: { + label: t('editor.bulletedList'), + Icon: BulletedListIcon, + }, + [SlashOptionType.NumberedList]: { + label: t('editor.numberedList'), + Icon: NumberedListIcon, + }, + [SlashOptionType.Quote]: { + label: t('editor.quote'), + Icon: QuoteIcon, + }, + [SlashOptionType.ToggleList]: { + label: t('document.plugins.toggleList'), + Icon: ToggleListIcon, + }, + [SlashOptionType.Divider]: { + label: t('editor.divider'), + Icon: HorizontalRuleOutlined, + }, + [SlashOptionType.Callout]: { + label: t('document.plugins.callout'), + Icon: MenuBookOutlined, + }, + [SlashOptionType.Code]: { + label: t('document.selectionMenu.codeBlock'), + Icon: DataObjectOutlined, + }, + [SlashOptionType.Grid]: { + label: t('grid.menuName'), + Icon: GridIcon, + }, + + [SlashOptionType.MathEquation]: { + label: t('document.plugins.mathEquation.name'), + Icon: FunctionsOutlined, + }, + [SlashOptionType.Image]: { + label: t('editor.image'), + Icon: ImageIcon, + }, + }; + }, [t]); + + const groupTypeToLabelMap = useMemo(() => { + return { + [SlashCommandPanelTab.BASIC]: 'Basic', + [SlashCommandPanelTab.ADVANCED]: 'Advanced', + [SlashCommandPanelTab.MEDIA]: 'Media', + [SlashCommandPanelTab.DATABASE]: 'Database', + }; + }, []); + + const renderOptionContent = useCallback( + (type: SlashOptionType) => { + const Icon = typeToLabelIconMap[type].Icon; + + return ( +
+
+ +
+ +
{typeToLabelIconMap[type].label}
+
+ ); + }, + [typeToLabelIconMap] + ); + + const options: KeyboardNavigationOption[] = useMemo(() => { + return slashOptionGroup + .map((group) => { + return { + key: group.key, + content:
{groupTypeToLabelMap[group.key]}
, + children: group.options + + .map((type) => { + return { + key: type, + content: renderOptionContent(type), + }; + }) + .filter((option) => { + if (!searchText) return true; + const label = typeToLabelIconMap[option.key].label; + + let newSearchText = searchText; + + if (searchText.startsWith('/')) { + newSearchText = searchText.slice(1); + } + + return ( + label.toLowerCase().includes(newSearchText.toLowerCase()) || + SlashAliases[option.key].some((alias) => alias.startsWith(newSearchText.toLowerCase())) + ); + }) + .sort(reorderSlashOptions(searchText)), + }; + }) + .filter((group) => group.children.length > 0); + }, [searchText, groupTypeToLabelMap, typeToLabelIconMap, renderOptionContent]); + + return { + options, + onConfirm, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.tsx new file mode 100644 index 0000000000..b09af97b39 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useRef } from 'react'; +import { + initialAnchorOrigin, + initialTransformOrigin, + PanelPopoverProps, + PanelProps, +} from '$app/components/editor/components/tools/command_panel/Command.hooks'; +import Popover from '@mui/material/Popover'; +import SlashCommandPanelContent from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent'; +import { useSlate } from 'slate-react'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; + +export function SlashCommandPanel({ anchorPosition, closePanel, searchText }: PanelProps) { + const ref = useRef(null); + const editor = useSlate(); + + const open = Boolean(anchorPosition); + + const handleClose = useCallback( + (deleteText?: boolean) => { + closePanel(deleteText); + }, + [closePanel] + ); + + const { + paperHeight, + paperWidth, + anchorPosition: newAnchorPosition, + transformOrigin, + anchorOrigin, + isEntered, + } = usePopoverAutoPosition({ + initialPaperWidth: 220, + initialPaperHeight: 360, + anchorPosition, + initialTransformOrigin, + initialAnchorOrigin, + open, + }); + + return ( +
+ {open && ( + { + const selection = editor.selection; + + handleClose(false); + + if (selection) { + editor.select(selection); + } + }} + > + + + )} +
+ ); +} + +export default SlashCommandPanel; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx new file mode 100644 index 0000000000..256e82f811 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useRef } from 'react'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { useSlashCommandPanel } from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks'; +import { useSlateStatic } from 'slate-react'; +import { SlashOptionType } from '$app/components/editor/components/tools/command_panel/slash_command_panel/const'; + +const noResultBuffer = 2; + +function SlashCommandPanelContent({ + closePanel, + searchText, + maxHeight, + width, +}: { + closePanel: (deleteText?: boolean) => void; + searchText: string; + maxHeight: number; + width: number; +}) { + const scrollRef = useRef(null); + + const { options, onConfirm } = useSlashCommandPanel({ + closePanel, + searchText, + }); + + // Used to keep track of how many times the user has typed and not found any result + const noResultCount = useRef(0); + + const editor = useSlateStatic(); + + useEffect(() => { + const { insertText, deleteBackward } = editor; + + editor.insertText = (text, opts) => { + // close panel if track of no result is greater than buffer + if (noResultCount.current >= noResultBuffer) { + closePanel(false); + } + + if (options.length === 0) { + noResultCount.current += 1; + } + + insertText(text, opts); + }; + + editor.deleteBackward = (unit) => { + // reset no result count + if (noResultCount.current > 0) { + noResultCount.current -= 1; + } + + // close panel if no text + if (!searchText) { + closePanel(true); + return; + } + + deleteBackward(unit); + }; + + return () => { + editor.insertText = insertText; + editor.deleteBackward = deleteBackward; + }; + }, [closePanel, editor, searchText, options.length]); + + return ( +
+ onConfirm(key as SlashOptionType)} + options={options} + disableFocus={true} + /> +
+ ); +} + +export default SlashCommandPanelContent; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts new file mode 100644 index 0000000000..7dfaa2b4a0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts @@ -0,0 +1,174 @@ +import { EditorNodeType } from '$app/application/document/document.types'; + +export enum SlashCommandPanelTab { + BASIC = 'basic', + MEDIA = 'media', + DATABASE = 'database', + ADVANCED = 'advanced', +} + +export enum SlashOptionType { + Paragraph, + TodoList, + Heading1, + Heading2, + Heading3, + BulletedList, + NumberedList, + Quote, + ToggleList, + Divider, + Callout, + Code, + Grid, + MathEquation, + Image, +} + +export const slashOptionGroup = [ + { + key: SlashCommandPanelTab.BASIC, + options: [ + SlashOptionType.Paragraph, + SlashOptionType.TodoList, + SlashOptionType.Heading1, + SlashOptionType.Heading2, + SlashOptionType.Heading3, + SlashOptionType.BulletedList, + SlashOptionType.NumberedList, + SlashOptionType.Quote, + SlashOptionType.ToggleList, + SlashOptionType.Divider, + SlashOptionType.Callout, + ], + }, + { + key: SlashCommandPanelTab.MEDIA, + options: [SlashOptionType.Code, SlashOptionType.Image], + }, + { + key: SlashCommandPanelTab.DATABASE, + options: [SlashOptionType.Grid], + }, + { + key: SlashCommandPanelTab.ADVANCED, + options: [SlashOptionType.MathEquation], + }, +]; +export const slashOptionMapToEditorNodeType = { + [SlashOptionType.Paragraph]: EditorNodeType.Paragraph, + [SlashOptionType.TodoList]: EditorNodeType.TodoListBlock, + [SlashOptionType.Heading1]: EditorNodeType.HeadingBlock, + [SlashOptionType.Heading2]: EditorNodeType.HeadingBlock, + [SlashOptionType.Heading3]: EditorNodeType.HeadingBlock, + [SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock, + [SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock, + [SlashOptionType.Quote]: EditorNodeType.QuoteBlock, + [SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock, + [SlashOptionType.Divider]: EditorNodeType.DividerBlock, + [SlashOptionType.Callout]: EditorNodeType.CalloutBlock, + [SlashOptionType.Code]: EditorNodeType.CodeBlock, + [SlashOptionType.Grid]: EditorNodeType.GridBlock, + [SlashOptionType.MathEquation]: EditorNodeType.EquationBlock, + [SlashOptionType.Image]: EditorNodeType.ImageBlock, +}; +export const headingTypeToLevelMap: Record = { + [SlashOptionType.Heading1]: 1, + [SlashOptionType.Heading2]: 2, + [SlashOptionType.Heading3]: 3, +}; +export const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3]; + +export const SlashAliases = { + [SlashOptionType.Paragraph]: ['paragraph', 'text', 'block', 'textblock'], + [SlashOptionType.TodoList]: [ + 'list', + 'todo', + 'todolist', + 'checkbox', + 'block', + 'todoblock', + 'checkboxblock', + 'todolistblock', + ], + [SlashOptionType.Heading1]: ['h1', 'heading1', 'block', 'headingblock', 'h1block'], + [SlashOptionType.Heading2]: ['h2', 'heading2', 'block', 'headingblock', 'h2block'], + [SlashOptionType.Heading3]: ['h3', 'heading3', 'block', 'headingblock', 'h3block'], + [SlashOptionType.BulletedList]: [ + 'list', + 'bulleted', + 'block', + 'bulletedlist', + 'bulletedblock', + 'listblock', + 'bulletedlistblock', + 'bulletelist', + ], + [SlashOptionType.NumberedList]: [ + 'list', + 'numbered', + 'block', + 'numberedlist', + 'numberedblock', + 'listblock', + 'numberedlistblock', + 'numberlist', + ], + [SlashOptionType.Quote]: ['quote', 'block', 'quoteblock'], + [SlashOptionType.ToggleList]: ['list', 'toggle', 'block', 'togglelist', 'toggleblock', 'listblock', 'togglelistblock'], + [SlashOptionType.Divider]: ['divider', 'hr', 'block', 'dividerblock', 'line', 'lineblock'], + [SlashOptionType.Callout]: ['callout', 'info', 'block', 'calloutblock'], + [SlashOptionType.Code]: ['code', 'code', 'block', 'codeblock', 'media'], + [SlashOptionType.Grid]: ['grid', 'table', 'block', 'gridblock', 'database'], + [SlashOptionType.MathEquation]: [ + 'math', + 'equation', + 'block', + 'mathblock', + 'mathequation', + 'mathequationblock', + 'advanced', + ], + [SlashOptionType.Image]: ['img', 'image', 'block', 'imageblock', 'media'], +}; + +export const reorderSlashOptions = (searchText: string) => { + return ( + a: { + key: SlashOptionType; + }, + b: { + key: SlashOptionType; + } + ) => { + const compareIndex = (option: SlashOptionType) => { + const aliases = SlashAliases[option]; + + if (aliases) { + for (const alias of aliases) { + if (alias.startsWith(searchText)) { + return -1; + } + } + } + + return 0; + }; + + const compareLength = (option: SlashOptionType) => { + const aliases = SlashAliases[option]; + + if (aliases) { + for (const alias of aliases) { + if (alias.length < searchText.length) { + return -1; + } + } + } + + return 0; + }; + + return compareIndex(a.key) - compareIndex(b.key) || compareLength(a.key) - compareLength(b.key); + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/index.ts new file mode 100644 index 0000000000..688a6ffb7d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/index.ts @@ -0,0 +1,2 @@ +export * from './SlashCommandPanel'; +export * from './SlashCommandPanelContent'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/utils.ts new file mode 100644 index 0000000000..65a095dc58 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/utils.ts @@ -0,0 +1,31 @@ +import { ReactEditor } from 'slate-react'; + +export function getPanelPosition(editor: ReactEditor) { + const { selection } = editor; + + const isFocused = ReactEditor.isFocused(editor); + + if (!selection || !isFocused) { + return null; + } + + const domSelection = window.getSelection(); + const rangeCount = domSelection?.rangeCount; + + if (!rangeCount) return null; + + const domRange = rangeCount > 0 ? domSelection.getRangeAt(0) : undefined; + + const rect = domRange?.getBoundingClientRect(); + + if (!rect) return null; + const nodeDom = domSelection.anchorNode?.parentElement?.closest('.text-element'); + const height = (nodeDom?.getBoundingClientRect().height ?? 0) + 8; + + return { + ...rect, + height, + top: rect.top, + left: rect.left, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/popover.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/popover.ts new file mode 100644 index 0000000000..2b6a715baa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/popover.ts @@ -0,0 +1,34 @@ +import { PopoverProps } from '@mui/material/Popover'; + +export const PopoverCommonProps: Partial = { + keepMounted: false, + disableAutoFocus: true, + disableEnforceFocus: true, + disableRestoreFocus: true, +}; + +export const PopoverPreventBlurProps: Partial = { + ...PopoverCommonProps, + + onMouseDown: (e) => { + // prevent editor blur + e.preventDefault(); + e.stopPropagation(); + }, +}; + +export const PopoverNoBackdropProps: Partial = { + ...PopoverCommonProps, + sx: { + pointerEvents: 'none', + }, + PaperProps: { + style: { + pointerEvents: 'auto', + }, + }, + onMouseDown: (e) => { + // prevent editor blur + e.stopPropagation(); + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx new file mode 100644 index 0000000000..9d7c19b999 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Paragraph } from '$app/components/editor/components/tools/selection_toolbar/actions/paragraph'; +import { Heading } from '$app/components/editor/components/tools/selection_toolbar/actions/heading'; +import { Divider } from '@mui/material'; +import { Bold } from '$app/components/editor/components/tools/selection_toolbar/actions/bold'; +import { Italic } from '$app/components/editor/components/tools/selection_toolbar/actions/italic'; +import { Underline } from '$app/components/editor/components/tools/selection_toolbar/actions/underline'; +import { StrikeThrough } from '$app/components/editor/components/tools/selection_toolbar/actions/strikethrough'; +import { InlineCode } from '$app/components/editor/components/tools/selection_toolbar/actions/inline_code'; +import { Formula } from '$app/components/editor/components/tools/selection_toolbar/actions/formula'; +import { TodoList } from '$app/components/editor/components/tools/selection_toolbar/actions/todo_list'; +import { Quote } from '$app/components/editor/components/tools/selection_toolbar/actions/quote'; +import { ToggleList } from '$app/components/editor/components/tools/selection_toolbar/actions/toggle_list'; +import { BulletedList } from '$app/components/editor/components/tools/selection_toolbar/actions/bulleted_list'; +import { NumberedList } from '$app/components/editor/components/tools/selection_toolbar/actions/numbered_list'; +import { Href, LinkActions } from '$app/components/editor/components/tools/selection_toolbar/actions/href'; +import { Align } from '$app/components/editor/components/tools/selection_toolbar/actions/align'; +import { Color } from '$app/components/editor/components/tools/selection_toolbar/actions/color'; + +function SelectionActions({ + isAcrossBlocks, + storeSelection, + restoreSelection, + isIncludeRoot, +}: { + storeSelection: () => void; + restoreSelection: () => void; + isAcrossBlocks: boolean; + visible: boolean; + isIncludeRoot: boolean; +}) { + if (isIncludeRoot) return null; + return ( +
+ {!isAcrossBlocks && ( + <> + + + + + )} + + + + + + {!isAcrossBlocks && ( + <> + + + + )} + + {!isAcrossBlocks && ( + <> + + + + + + + + )} + {!isAcrossBlocks && } + + + +
+ ); +} + +export default SelectionActions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts new file mode 100644 index 0000000000..58834db6d5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts @@ -0,0 +1,239 @@ +import { ReactEditor, useFocused, useSlate } from 'slate-react'; +import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { getSelectionPosition } from '$app/components/editor/components/tools/selection_toolbar/utils'; +import debounce from 'lodash-es/debounce'; +import { CustomEditor } from '$app/components/editor/command'; +import { BaseRange, Range as SlateRange } from 'slate'; +import { useDecorateDispatch } from '$app/components/editor/stores/decorate'; + +const DELAY = 300; + +export function useSelectionToolbar(ref: MutableRefObject) { + const editor = useSlate() as ReactEditor; + const isDraggingRef = useRef(false); + const [isAcrossBlocks, setIsAcrossBlocks] = useState(false); + const [visible, setVisible] = useState(false); + const isFocusedEditor = useFocused(); + const isIncludeRoot = CustomEditor.selectionIncludeRoot(editor); + + // paint the selection when the editor is blurred + const { add: addDecorate, clear: clearDecorate, getStaticState } = useDecorateDispatch(); + + // Restore selection after the editor is focused + const restoreSelection = useCallback(() => { + const decorateState = getStaticState(); + + if (!decorateState) return; + + editor.select({ + ...decorateState.range, + }); + + clearDecorate(); + ReactEditor.focus(editor); + }, [getStaticState, clearDecorate, editor]); + + // Store selection when the editor is blurred + const storeSelection = useCallback(() => { + addDecorate({ + range: editor.selection as BaseRange, + class_name: 'bg-content-blue-100', + }); + }, [addDecorate, editor]); + + const closeToolbar = useCallback(() => { + const el = ref.current; + + if (!el) { + return; + } + + restoreSelection(); + + setVisible(false); + el.style.opacity = '0'; + el.style.pointerEvents = 'none'; + }, [ref, restoreSelection]); + + const recalculatePosition = useCallback(() => { + const el = ref.current; + + if (!el) { + return; + } + + const position = getSelectionPosition(editor); + + if (!position) { + closeToolbar(); + return; + } + + const slateEditorDom = ReactEditor.toDOMNode(editor, editor); + + setVisible(true); + el.style.opacity = '1'; + + // if dragging, disable pointer events + if (isDraggingRef.current) { + el.style.pointerEvents = 'none'; + } else { + el.style.pointerEvents = 'auto'; + } + + // If toolbar is out of editor, move it to the top + el.style.top = `${position.top + slateEditorDom.offsetTop - el.offsetHeight}px`; + + const left = position.left + slateEditorDom.offsetLeft; + + // If toolbar is out of editor, move it to the left edge of the editor + if (left < 0) { + el.style.left = '0'; + return; + } + + const right = left + el.offsetWidth; + + // If toolbar is out of editor, move the right edge to the right edge of the editor + if (right > slateEditorDom.offsetWidth) { + el.style.left = `${slateEditorDom.offsetWidth - el.offsetWidth}px`; + return; + } + + el.style.left = `${left}px`; + }, [closeToolbar, editor, ref]); + + const debounceRecalculatePosition = useMemo(() => debounce(recalculatePosition, DELAY), [recalculatePosition]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + const decorateState = getStaticState(); + + if (decorateState) { + setIsAcrossBlocks(false); + return; + } + + const { selection } = editor; + + const close = () => { + debounceRecalculatePosition.cancel(); + closeToolbar(); + }; + + if (isIncludeRoot || !isFocusedEditor || !selection || SlateRange.isCollapsed(selection)) { + close(); + return; + } + + // There has a bug which the text of selection is empty when the selection include inline blocks + const isEmptyText = !CustomEditor.includeInlineBlocks(editor) && editor.string(selection) === ''; + + if (isEmptyText) { + close(); + return; + } + + setIsAcrossBlocks(CustomEditor.isMultipleBlockSelected(editor, true)); + debounceRecalculatePosition(); + }); + + // Update drag status + useEffect(() => { + const el = ReactEditor.toDOMNode(editor, editor); + + const toolbar = ref.current; + + if (!el || !toolbar) { + return; + } + + const onMouseDown = () => { + isDraggingRef.current = true; + }; + + const onMouseUp = () => { + if (visible) { + toolbar.style.pointerEvents = 'auto'; + } + + isDraggingRef.current = false; + }; + + el.addEventListener('mousedown', onMouseDown); + document.addEventListener('mouseup', onMouseUp); + + return () => { + el.removeEventListener('mousedown', onMouseDown); + document.removeEventListener('mouseup', onMouseUp); + }; + }, [visible, editor, ref]); + + // Close toolbar when press ESC + useEffect(() => { + const slateEditorDom = ReactEditor.toDOMNode(editor, editor); + const onKeyDown = (e: KeyboardEvent) => { + // Close toolbar when press ESC + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + editor.collapse({ + edge: 'end', + }); + debounceRecalculatePosition.cancel(); + closeToolbar(); + } + }; + + if (visible) { + slateEditorDom.addEventListener('keydown', onKeyDown); + } else { + slateEditorDom.removeEventListener('keydown', onKeyDown); + } + + return () => { + slateEditorDom.removeEventListener('keydown', onKeyDown); + }; + }, [closeToolbar, debounceRecalculatePosition, editor, visible]); + + // Recalculate position when the scroll container is scrolled + useEffect(() => { + const slateEditorDom = ReactEditor.toDOMNode(editor, editor); + const scrollContainer = slateEditorDom.closest('.appflowy-scroll-container'); + + if (!visible) return; + if (!scrollContainer) return; + const handleScroll = () => { + if (isDraggingRef.current) return; + + const domSelection = window.getSelection(); + const rangeCount = domSelection?.rangeCount; + + if (!rangeCount) return null; + + const domRange = rangeCount > 0 ? domSelection.getRangeAt(0) : undefined; + + const rangeRect = domRange?.getBoundingClientRect(); + + // Stop calculating when the range is out of the window + if (!rangeRect?.bottom || rangeRect.bottom < 0) { + return; + } + + recalculatePosition(); + }; + + scrollContainer.addEventListener('scroll', handleScroll); + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + }; + }, [visible, editor, recalculatePosition]); + + return { + visible, + restoreSelection, + storeSelection, + isAcrossBlocks, + isIncludeRoot, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.tsx new file mode 100644 index 0000000000..d4ca9c9de0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.tsx @@ -0,0 +1,33 @@ +import React, { memo, useRef } from 'react'; +import { useSelectionToolbar } from '$app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks'; +import SelectionActions from '$app/components/editor/components/tools/selection_toolbar/SelectionActions'; +import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; + +const Toolbar = memo(() => { + const ref = useRef(null); + + const { visible, restoreSelection, storeSelection, isAcrossBlocks, isIncludeRoot } = useSelectionToolbar(ref); + + return ( +
{ + // prevent toolbar from taking focus away from editor + e.preventDefault(); + }} + > + +
+ ); +}); + +export const SelectionToolbar = withErrorBoundary(Toolbar); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton.tsx new file mode 100644 index 0000000000..3f86d1eab9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton.tsx @@ -0,0 +1,35 @@ +import React, { forwardRef } from 'react'; +import IconButton, { IconButtonProps } from '@mui/material/IconButton'; +import { Tooltip } from '@mui/material'; + +const ActionButton = forwardRef< + HTMLButtonElement, + { + tooltip: string | React.ReactNode; + onClick?: (e: React.MouseEvent) => void; + children: React.ReactNode; + active?: boolean; + } & IconButtonProps +>(({ tooltip, onClick, disabled, children, active, className, ...props }, ref) => { + return ( + + + {children} + + + ); +}); + +export default ActionButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx new file mode 100644 index 0000000000..23917e146b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import { ReactComponent as AlignLeftSvg } from '$app/assets/align-left.svg'; +import { ReactComponent as AlignCenterSvg } from '$app/assets/align-center.svg'; +import { ReactComponent as AlignRightSvg } from '$app/assets/align-right.svg'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { CustomEditor } from '$app/components/editor/command'; +import { useSlateStatic } from 'slate-react'; +import { IconButton } from '@mui/material'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; + +export function Align() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const align = CustomEditor.getAlign(editor); + const [open, setOpen] = useState(false); + + const handleClose = useCallback(() => { + setOpen(false); + }, []); + + const handleOpen = useCallback(() => { + setOpen(true); + }, []); + + const Icon = useMemo(() => { + switch (align) { + case 'left': + return AlignLeftSvg; + case 'center': + return AlignCenterSvg; + case 'right': + return AlignRightSvg; + default: + return AlignLeftSvg; + } + }, [align]); + + const toggleAlign = useCallback( + (align: string) => { + return () => { + CustomEditor.toggleAlign(editor, align); + handleClose(); + }; + }, + [editor, handleClose] + ); + + const getAlignIcon = useCallback((key: string) => { + switch (key) { + case 'left': + return ; + case 'center': + return ; + case 'right': + return ; + default: + return ; + } + }, []); + + return ( + + {['left', 'center', 'right'].map((key) => { + return ( + + {getAlignIcon(key)} + + ); + })} +
+ } + > + +
+ + +
+
+ + ); +} + +export default Align; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/index.ts new file mode 100644 index 0000000000..6cba19d7bd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/index.ts @@ -0,0 +1 @@ +export * from './Align'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx new file mode 100644 index 0000000000..22be9f970a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/Bold.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as BoldSvg } from '$app/assets/bold.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function Bold() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Bold); + + const modifier = useMemo(() => createHotKeyLabel(HOT_KEY_NAME.BOLD), []); + const onClick = useCallback(() => { + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Bold, + value: true, + }); + }, [editor]); + + return ( + +
{t('toolbar.bold')}
+
{modifier}
+ + } + > + +
+ ); +} + +export default Bold; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/index.ts new file mode 100644 index 0000000000..6ef457faaa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bold/index.ts @@ -0,0 +1 @@ +export * from './Bold'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/BulletedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/BulletedList.tsx new file mode 100644 index 0000000000..f35f2aeeea --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/BulletedList.tsx @@ -0,0 +1,27 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { ReactComponent as BulletedListSvg } from '$app/assets/list.svg'; + +export function BulletedList() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.BulletedListBlock); + + const onClick = useCallback(() => { + CustomEditor.turnToBlock(editor, { + type: EditorNodeType.BulletedListBlock, + }); + }, [editor]); + + return ( + + + + ); +} + +export default BulletedList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/index.ts new file mode 100644 index 0000000000..2095dff308 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/bulleted_list/index.ts @@ -0,0 +1 @@ +export * from './BulletedList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/Color.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/Color.tsx new file mode 100644 index 0000000000..60c44423bd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/Color.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { CustomEditor } from '$app/components/editor/command'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import ColorLensOutlinedIcon from '@mui/icons-material/ColorLensOutlined'; +import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import debounce from 'lodash-es/debounce'; +import ColorPopover from './ColorPopover'; + +export function Color(_: { onOpen?: () => void; onClose?: () => void }) { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const isActivated = + CustomEditor.isMarkActive(editor, EditorMarkFormat.FontColor) || + CustomEditor.isMarkActive(editor, EditorMarkFormat.BgColor); + const debouncedClose = useMemo( + () => + debounce(() => { + setOpen(false); + }, 200), + [] + ); + + const handleOpen = useCallback(() => { + debouncedClose.cancel(); + setOpen(true); + }, [debouncedClose]); + + return ( + <> + +
+ + +
+
+ {open && ref.current && ( + + )} + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/ColorPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/ColorPopover.tsx new file mode 100644 index 0000000000..9f007dc7b5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/ColorPopover.tsx @@ -0,0 +1,92 @@ +import React, { useCallback } from 'react'; +import { PopoverNoBackdropProps } from '$app/components/editor/components/tools/popover'; +import { ColorPicker } from '$app/components/editor/components/tools/_shared'; +import { Popover } from '@mui/material'; +import { PopoverOrigin } from '@mui/material/Popover/Popover'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { addMark, removeMark } from 'slate'; +import { useSlateStatic } from 'slate-react'; +import { DebouncedFunc } from 'lodash-es/debounce'; +import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; + +const initialOrigin: { + transformOrigin?: PopoverOrigin; + anchorOrigin?: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'center', + }, + transformOrigin: { + vertical: 'top', + horizontal: 'center', + }, +}; + +function ColorPopover({ + open, + anchorEl, + debounceClose, +}: { + open: boolean; + onOpen: () => void; + anchorEl: HTMLButtonElement | null; + debounceClose: DebouncedFunc<() => void>; +}) { + const editor = useSlateStatic(); + const handleChange = useCallback( + (format: EditorMarkFormat.FontColor | EditorMarkFormat.BgColor, color: string) => { + if (color) { + addMark(editor, format, color); + } else { + removeMark(editor, format); + } + }, + [editor] + ); + + const { paperHeight, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ + initialPaperWidth: 200, + initialPaperHeight: 420, + anchorEl, + initialAnchorOrigin: initialOrigin.anchorOrigin, + initialTransformOrigin: initialOrigin.transformOrigin, + open, + }); + + return ( + { + e.stopPropagation(); + if (e.key === 'Escape') { + debounceClose(); + } + }} + onMouseDown={(e) => { + e.preventDefault(); + }} + onMouseEnter={() => { + debounceClose.cancel(); + }} + onMouseLeave={debounceClose} + > + + + ); +} + +export default ColorPopover; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/index.ts new file mode 100644 index 0000000000..0fd619bc86 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/color/index.ts @@ -0,0 +1 @@ +export * from './Color'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx new file mode 100644 index 0000000000..c7bfc11352 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/Formula.tsx @@ -0,0 +1,51 @@ +import React, { useCallback } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import Functions from '@mui/icons-material/Functions'; +import { useEditorInlineBlockState } from '$app/components/editor/stores'; + +export function Formula() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivatedMention = CustomEditor.isMentionActive(editor); + + const formulaMatch = CustomEditor.formulaActiveNode(editor); + const isActivated = !isActivatedMention && CustomEditor.isFormulaActive(editor); + + const { setRange, openPopover } = useEditorInlineBlockState('formula'); + const onClick = useCallback(() => { + let selection = editor.selection; + + if (!selection) return; + if (formulaMatch) { + selection = editor.range(formulaMatch[1]); + editor.select(selection); + } else { + CustomEditor.toggleFormula(editor); + } + + requestAnimationFrame(() => { + const selection = editor.selection; + + if (!selection) return; + + setRange(selection); + openPopover(); + }); + }, [editor, formulaMatch, setRange, openPopover]); + + return ( + + + + ); +} + +export default Formula; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/index.ts new file mode 100644 index 0000000000..dc4ad2cd03 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/formula/index.ts @@ -0,0 +1 @@ +export * from './Formula'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/Heading.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/Heading.tsx new file mode 100644 index 0000000000..1fc639c41f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/Heading.tsx @@ -0,0 +1,56 @@ +import React, { useCallback } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { ReactComponent as Heading1Svg } from '$app/assets/h1.svg'; +import { ReactComponent as Heading2Svg } from '$app/assets/h2.svg'; +import { ReactComponent as Heading3Svg } from '$app/assets/h3.svg'; +import { useTranslation } from 'react-i18next'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorNodeType, HeadingNode } from '$app/application/document/document.types'; +import { useSlateStatic } from 'slate-react'; +import { getBlock } from '$app/components/editor/plugins/utils'; + +export function Heading() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const toHeading = useCallback( + (level: number) => { + return () => { + CustomEditor.turnToBlock(editor, { + type: EditorNodeType.HeadingBlock, + data: { + level, + }, + }); + }; + }, + [editor] + ); + + const isActivated = useCallback( + (level: number) => { + const node = getBlock(editor) as HeadingNode; + + if (!node) return false; + const isBlock = CustomEditor.isBlockActive(editor, EditorNodeType.HeadingBlock); + + return isBlock && node.data.level === level; + }, + [editor] + ); + + return ( +
+ + + + + + + + + +
+ ); +} + +export default Heading; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/index.ts new file mode 100644 index 0000000000..6406e7b07f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/heading/index.ts @@ -0,0 +1 @@ +export * from './Heading'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/Href.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/Href.tsx new file mode 100644 index 0000000000..e7412a909e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/Href.tsx @@ -0,0 +1,47 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as LinkSvg } from '$app/assets/link.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { useDecorateDispatch } from '$app/components/editor/stores'; +import { getModifier } from '$app/utils/hotkeys'; + +export function Href() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivatedInline = CustomEditor.isInlineActive(editor); + const isActivated = !isActivatedInline && CustomEditor.isMarkActive(editor, EditorMarkFormat.Href); + + const { add: addDecorate } = useDecorateDispatch(); + const onClick = useCallback(() => { + if (!editor.selection) return; + addDecorate({ + range: editor.selection, + class_name: 'bg-content-blue-100 rounded', + type: 'link', + }); + }, [addDecorate, editor]); + + const tooltip = useMemo(() => { + const modifier = getModifier(); + + return ( + <> +
{t('editor.link')}
+
{`${modifier} + K`}
+ + ); + }, [t]); + + return ( + <> + + + + + ); +} + +export default Href; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx new file mode 100644 index 0000000000..b77a249051 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx @@ -0,0 +1,59 @@ +import React, { useCallback, useMemo } from 'react'; +import { useDecorateDispatch, useDecorateState } from '$app/components/editor/stores'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Editor } from 'slate'; +import { LinkEditPopover } from '$app/components/editor/components/inline_nodes/link'; + +export function LinkActions() { + const editor = useSlateStatic(); + const decorateState = useDecorateState('link'); + const openEditPopover = !!decorateState; + const { clear: clearDecorate } = useDecorateDispatch(); + + const anchorPosition = useMemo(() => { + const range = decorateState?.range; + + if (!range) return; + + const domRange = ReactEditor.toDOMRange(editor, range); + + const rect = domRange.getBoundingClientRect(); + + return { + top: rect.top, + left: rect.left, + height: rect.height, + }; + }, [decorateState?.range, editor]); + + const defaultHref = useMemo(() => { + const range = decorateState?.range; + + if (!range) return ''; + + const marks = Editor.marks(editor); + + return marks?.href || Editor.string(editor, range); + }, [decorateState?.range, editor]); + + const handleEditPopoverClose = useCallback(() => { + const range = decorateState?.range; + + clearDecorate(); + if (range) { + ReactEditor.focus(editor); + editor.select(range); + } + }, [clearDecorate, decorateState?.range, editor]); + + if (!openEditPopover) return null; + return ( + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts new file mode 100644 index 0000000000..9a7210c140 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts @@ -0,0 +1,2 @@ +export * from './Href'; +export * from './LinkActions'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx new file mode 100644 index 0000000000..3cf9c7ed85 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as CodeSvg } from '$app/assets/inline-code.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function InlineCode() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Code); + const modifier = useMemo(() => createHotKeyLabel(HOT_KEY_NAME.CODE), []); + + const onClick = useCallback(() => { + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Code, + value: true, + }); + }, [editor]); + + return ( + +
{t('editor.embedCode')}
+
{modifier}
+ + } + > + +
+ ); +} + +export default InlineCode; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/index.ts new file mode 100644 index 0000000000..9a4c4930c7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/index.ts @@ -0,0 +1 @@ +export * from './InlineCode'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/Italic.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/Italic.tsx new file mode 100644 index 0000000000..89fff40e6f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/Italic.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as ItalicSvg } from '$app/assets/italic.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function Italic() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Italic); + const modifier = useMemo(() => createHotKeyLabel(HOT_KEY_NAME.ITALIC), []); + + const onClick = useCallback(() => { + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Italic, + value: true, + }); + }, [editor]); + + return ( + +
{t('toolbar.italic')}
+
{modifier}
+ + } + > + +
+ ); +} + +export default Italic; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/index.ts new file mode 100644 index 0000000000..70bb069b60 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/italic/index.ts @@ -0,0 +1 @@ +export * from './Italic'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/NumberedList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/NumberedList.tsx new file mode 100644 index 0000000000..006247ca8b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/NumberedList.tsx @@ -0,0 +1,33 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { ReactComponent as NumberedListSvg } from '$app/assets/numbers.svg'; + +export function NumberedList() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.NumberedListBlock); + + const onClick = useCallback(() => { + let type = EditorNodeType.NumberedListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + + CustomEditor.turnToBlock(editor, { + type, + }); + }, [editor, isActivated]); + + return ( + + + + ); +} + +export default NumberedList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/index.ts new file mode 100644 index 0000000000..6e985ae25b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/numbered_list/index.ts @@ -0,0 +1 @@ +export * from './NumberedList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/Paragraph.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/Paragraph.tsx new file mode 100644 index 0000000000..1ac5610787 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/Paragraph.tsx @@ -0,0 +1,33 @@ +import React, { useCallback } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { getBlock } from '$app/components/editor/plugins/utils'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { useSlateStatic } from 'slate-react'; +import { ReactComponent as ParagraphSvg } from '$app/assets/text.svg'; + +export function Paragraph() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + + const onClick = useCallback(() => { + const node = getBlock(editor); + + if (!node) return; + + CustomEditor.turnToBlock(editor, { + type: EditorNodeType.Paragraph, + }); + }, [editor]); + + const isActive = CustomEditor.isBlockActive(editor, EditorNodeType.Paragraph); + + return ( + + + + ); +} + +export default Paragraph; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/index.ts new file mode 100644 index 0000000000..01752c914c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/paragraph/index.ts @@ -0,0 +1 @@ +export * from './Paragraph'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx new file mode 100644 index 0000000000..29ad0de104 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx @@ -0,0 +1,33 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { ReactComponent as QuoteSvg } from '$app/assets/quote.svg'; + +export function Quote() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.QuoteBlock); + + const onClick = useCallback(() => { + let type = EditorNodeType.QuoteBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + + CustomEditor.turnToBlock(editor, { + type, + }); + }, [editor, isActivated]); + + return ( + + + + ); +} + +export default Quote; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/index.ts new file mode 100644 index 0000000000..c88e677a53 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/index.ts @@ -0,0 +1 @@ +export * from './Quote'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx new file mode 100644 index 0000000000..325f6ac55a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as StrikeThroughSvg } from '$app/assets/strikethrough.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function StrikeThrough() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.StrikeThrough); + const modifier = useMemo(() => createHotKeyLabel(HOT_KEY_NAME.STRIKETHROUGH), []); + + const onClick = useCallback(() => { + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.StrikeThrough, + value: true, + }); + }, [editor]); + + return ( + +
{t('editor.strikethrough')}
+
{modifier}
+ + } + > + +
+ ); +} + +export default StrikeThrough; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/index.ts new file mode 100644 index 0000000000..f8314d16e3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/index.ts @@ -0,0 +1 @@ +export * from './StrikeThrough'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/TodoList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/TodoList.tsx new file mode 100644 index 0000000000..cd576edafa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/TodoList.tsx @@ -0,0 +1,37 @@ +import React, { useCallback } from 'react'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { ReactComponent as TodoListSvg } from '$app/assets/todo-list.svg'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; + +export function TodoList() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + + const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.TodoListBlock); + + const onClick = useCallback(() => { + let type = EditorNodeType.TodoListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + + CustomEditor.turnToBlock(editor, { + type, + data: { + checked: false, + }, + }); + }, [editor, isActivated]); + + return ( + + + + ); +} + +export default TodoList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/index.ts new file mode 100644 index 0000000000..f239f43459 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/todo_list/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx new file mode 100644 index 0000000000..4d82652988 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx @@ -0,0 +1,36 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorNodeType } from '$app/application/document/document.types'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { ReactComponent as ToggleListSvg } from '$app/assets/show-menu.svg'; + +export function ToggleList() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.ToggleListBlock); + + const onClick = useCallback(() => { + let type = EditorNodeType.ToggleListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + + CustomEditor.turnToBlock(editor, { + type, + data: { + collapsed: false, + }, + }); + }, [editor, isActivated]); + + return ( + + + + ); +} + +export default ToggleList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/index.ts new file mode 100644 index 0000000000..833bdb5210 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/index.ts @@ -0,0 +1 @@ +export * from './ToggleList'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx new file mode 100644 index 0000000000..b0df70e30e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx @@ -0,0 +1,39 @@ +import React, { useCallback, useMemo } from 'react'; +import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; +import { useTranslation } from 'react-i18next'; +import { useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; +import { ReactComponent as UnderlineSvg } from '$app/assets/underline.svg'; +import { EditorMarkFormat } from '$app/application/document/document.types'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +export function Underline() { + const { t } = useTranslation(); + const editor = useSlateStatic(); + const isActivated = CustomEditor.isMarkActive(editor, EditorMarkFormat.Underline); + const modifier = useMemo(() => createHotKeyLabel(HOT_KEY_NAME.UNDERLINE), []); + + const onClick = useCallback(() => { + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Underline, + value: true, + }); + }, [editor]); + + return ( + +
{t('editor.underline')}
+
{modifier}
+ + } + > + +
+ ); +} + +export default Underline; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/index.ts new file mode 100644 index 0000000000..a1d53a4384 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/index.ts @@ -0,0 +1 @@ +export * from './Underline'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/index.ts new file mode 100644 index 0000000000..a6ced3f248 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/index.ts @@ -0,0 +1 @@ +export * from './SelectionToolbar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/utils.ts new file mode 100644 index 0000000000..178da73df4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/utils.ts @@ -0,0 +1,40 @@ +import { ReactEditor } from 'slate-react'; +import { getEditorDomNode } from '$app/components/editor/plugins/utils'; + +export function getSelectionPosition(editor: ReactEditor) { + const domSelection = window.getSelection(); + const rangeCount = domSelection?.rangeCount; + + if (!rangeCount) return null; + + const domRange = rangeCount > 0 ? domSelection.getRangeAt(0) : undefined; + + const rect = domRange?.getBoundingClientRect(); + + let newRect; + + const domNode = getEditorDomNode(editor); + const domNodeRect = domNode.getBoundingClientRect(); + + // the default height of the toolbar is 30px + const gap = 106; + + if (rect) { + let relativeDomTop = rect.top - domNodeRect.top; + const relativeDomLeft = rect.left - domNodeRect.left; + + // if the range is above the window, move the toolbar to the bottom of range + if (rect.top < gap) { + relativeDomTop = -domNodeRect.top + gap; + } + + newRect = { + top: relativeDomTop, + left: relativeDomLeft, + width: rect.width, + height: rect.height, + }; + } + + return newRect; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss new file mode 100644 index 0000000000..271dd36cda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -0,0 +1,231 @@ + +.block-element { + @apply my-[4px]; + +} + +.block-element .block-element { + @apply mb-0; + margin-left: 24px; +} + +.block-element.block-align-left { + > div > .text-element { + text-align: left; + justify-content: flex-start; + } +} +.block-element.block-align-right { + > div > .text-element { + text-align: right; + justify-content: flex-end; + } +} +.block-element.block-align-center { + > div > .text-element { + text-align: center; + justify-content: center; + } + +} + + +.block-element[data-block-type="todo_list"] .checked > .text-element { + text-decoration: line-through; + color: var(--text-caption); +} + +.block-element .collapsed .block-element { + display: none !important; +} + +[role=textbox] { + .text-element { + &::selection { + @apply bg-transparent; + } + } +} + + + +span[data-slate-placeholder="true"]:not(.inline-block-content) { + @apply text-text-placeholder; + opacity: 1 !important; +} + + +[role="textbox"] { + ::selection { + @apply bg-content-blue-100; + } + .text-content { + &::selection { + @apply bg-transparent; + } + &.selected { + @apply bg-content-blue-100; + } + span { + &::selection { + @apply bg-content-blue-100; + } + } + } +} + + +[data-dark-mode="true"] [role="textbox"]{ + ::selection { + background-color: #1e79a2; + } + + .text-content { + &::selection { + @apply bg-transparent; + } + &.selected { + background-color: #1e79a2; + } + span { + &::selection { + background-color: #1e79a2; + } + } + } +} + + +.text-content, [data-dark-mode="true"] .text-content { + @apply min-w-[1px]; + &.empty-text { + span { + &::selection { + @apply bg-transparent; + } + } + } +} + +.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node { + ::selection { + @apply bg-transparent; + } +} + +.text-placeholder { + @apply absolute left-[5px] transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap; + &:after { + @apply text-text-placeholder absolute top-0; + content: (attr(placeholder)); + } +} + +.block-align-center { + .text-placeholder { + @apply left-[calc(50%+1px)]; + &:after { + @apply left-0; + } + } + .has-start-icon .text-placeholder { + @apply left-[calc(50%+13px)]; + &:after { + @apply left-0; + } + } + +} + +.block-align-left { + .text-placeholder { + &:after { + @apply left-0; + } + } + .has-start-icon .text-placeholder { + &:after { + @apply left-[24px]; + } + } +} + +.block-align-right { + + .text-placeholder { + + @apply relative w-fit h-0 order-2; + &:after { + @apply relative w-fit top-1/2 left-[-6px]; + } + } + .text-content { + @apply order-1; + } + + .has-start-icon .text-placeholder { + &:after { + @apply left-[-6px]; + } + } +} + + +.formula-inline { + &.selected { + @apply rounded bg-content-blue-100; + } +} + +.bulleted-icon { + &:after { + content: attr(data-letter); + } +} + +.numbered-icon { + &:after { + content: attr(data-number) "."; + } +} + + +.grid-block .grid-scroll-container::-webkit-scrollbar { + width: 0; + height: 0; +} + +.image-render { + .image-resizer { + @apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end; + .resize-handle { + @apply h-1/4 w-1/2 transform transition-all duration-500 select-none rounded-full border border-white opacity-0; + background: var(--fill-toolbar); + } + } + &:hover { + .image-resizer{ + .resize-handle { + @apply opacity-90; + } + } + } +} + + +.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block { + ::selection { + @apply bg-transparent; + } + &:hover { + .container-bg { + background: var(--content-blue-100) !important; + } + } +} + +.mention-inline { + &:hover { + @apply bg-fill-list-active rounded; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/index.ts new file mode 100644 index 0000000000..8b7c4c267a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/index.ts @@ -0,0 +1 @@ +export * from './Editor'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/constants.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/constants.ts new file mode 100644 index 0000000000..03e441b1c3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/constants.ts @@ -0,0 +1,3 @@ +import { EditorNodeType } from '$app/application/document/document.types'; + +export const SOFT_BREAK_TYPES = [EditorNodeType.CalloutBlock, EditorNodeType.CodeBlock]; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts new file mode 100644 index 0000000000..bf2b09a1c3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts @@ -0,0 +1,2 @@ +export * from './withCopy'; +export * from './withPasted'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts new file mode 100644 index 0000000000..cb377fece4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts @@ -0,0 +1,311 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Node, Location, Range, Path, Element, Text, Transforms, NodeEntry } from 'slate'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { LIST_TYPES } from '$app/components/editor/command/tab'; + +/** + * Rewrite the insertFragment function to avoid the empty node(doesn't have text node) in the fragment + + * @param editor + * @param fragment + * @param options + */ +export function insertFragment( + editor: ReactEditor, + fragment: (Text | Element)[], + options: { + at?: Location; + hanging?: boolean; + voids?: boolean; + } = {} +) { + Editor.withoutNormalizing(editor, () => { + const { hanging = false, voids = false } = options; + let { at = getDefaultInsertLocation(editor) } = options; + + if (!fragment.length) { + return; + } + + if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at, { voids }); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + const [, end] = Range.edges(at); + + if (!voids && Editor.void(editor, { at: end })) { + return; + } + + const pointRef = Editor.pointRef(editor, end); + + Transforms.delete(editor, { at }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + at = pointRef.unref()!; + } + } else if (Path.isPath(at)) { + at = Editor.start(editor, at); + } + + if (!voids && Editor.void(editor, { at })) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const blockMatch = Editor.above(editor, { + match: (n) => Element.isElement(n) && Editor.isBlock(editor, n) && n.blockId !== undefined, + at, + voids, + })!; + const [block, blockPath] = blockMatch as NodeEntry; + + const isEmbedBlock = Element.isElement(block) && editor.isEmbed(block); + const isPageBlock = Element.isElement(block) && block.type === EditorNodeType.Page; + const isBlockStart = Editor.isStart(editor, at, blockPath); + const isBlockEnd = Editor.isEnd(editor, at, blockPath); + const isBlockEmpty = isBlockStart && isBlockEnd; + + if (isEmbedBlock) { + insertOnEmbedBlock(editor, fragment, blockPath); + return; + } + + if (isBlockEmpty && !isPageBlock) { + const node = fragment[0] as Element; + + if (block.type !== EditorNodeType.Paragraph) { + node.type = block.type; + node.data = { + ...(node.data || {}), + ...(block.data || {}), + }; + } + + insertOnEmptyBlock(editor, fragment, blockPath); + return; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const fragmentRoot: Node = { + children: fragment, + }; + const [, firstPath] = Node.first(fragmentRoot, []); + const [, lastPath] = Node.last(fragmentRoot, []); + const sameBlock = Path.equals(firstPath.slice(0, -1), lastPath.slice(0, -1)); + + if (sameBlock) { + insertTexts( + editor, + isPageBlock + ? ({ + children: [ + { + text: CustomEditor.getNodeTextContent(fragmentRoot), + }, + ], + } as Node) + : fragmentRoot, + at + ); + return; + } + + const isListTypeBlock = LIST_TYPES.includes(block.type as EditorNodeType); + const [, ...blockChildren] = block.children; + + const blockEnd = editor.end([...blockPath, 0]); + const afterRange: Range = { anchor: at, focus: blockEnd }; + + const afterTexts = getTexts(editor, { + children: editor.fragment(afterRange), + } as Node) as (Text | Element)[]; + + Transforms.delete(editor, { at: afterRange }); + + const { startTexts, startChildren, middles } = getFragmentGroup(editor, fragment); + + insertNodes( + editor, + isPageBlock + ? [ + { + text: CustomEditor.getNodeTextContent({ + children: startTexts, + } as Node), + }, + ] + : startTexts, + { + at, + } + ); + + if (isPageBlock) { + insertNodes(editor, [...startChildren, ...middles], { + at: Path.next(blockPath), + select: true, + }); + } else { + if (blockChildren.length > 0) { + const path = [...blockPath, 1]; + + insertNodes(editor, [...startChildren, ...middles], { + at: path, + select: true, + }); + } else { + const newMiddle = [...middles]; + + if (isListTypeBlock) { + const path = [...blockPath, 1]; + + insertNodes(editor, startChildren, { + at: path, + select: newMiddle.length === 0, + }); + } else { + newMiddle.unshift(...startChildren); + } + + insertNodes(editor, newMiddle, { + at: Path.next(blockPath), + select: true, + }); + } + } + + const { selection } = editor; + + if (!selection) return; + + insertNodes(editor, afterTexts, { + at: selection, + }); + }); +} + +function getFragmentGroup(editor: ReactEditor, fragment: Node[]) { + const startTexts = []; + const startChildren = []; + const middles = []; + + const [firstNode, ...otherNodes] = fragment; + const [firstNodeText, ...firstNodeChildren] = (firstNode as Element).children as Element[]; + + startTexts.push(...firstNodeText.children); + startChildren.push(...firstNodeChildren); + + for (const node of otherNodes) { + if (Element.isElement(node) && node.blockId !== undefined) { + middles.push(node); + } + } + + return { + startTexts, + startChildren, + middles, + }; +} + +function getTexts(editor: ReactEditor, fragment: Node) { + const matches = []; + const matcher = ([n]: NodeEntry) => Text.isText(n) || (Element.isElement(n) && editor.isInline(n)); + + for (const entry of Node.nodes(fragment, { pass: matcher })) { + if (matcher(entry)) { + matches.push(entry[0]); + } + } + + return matches; +} + +function insertTexts(editor: ReactEditor, fragmentRoot: Node, at: Location) { + const matches = getTexts(editor, fragmentRoot); + + insertNodes(editor, matches, { + at, + select: true, + }); +} + +function insertOnEmptyBlock(editor: ReactEditor, fragment: Node[], blockPath: Path) { + editor.removeNodes({ + at: blockPath, + }); + + insertNodes(editor, fragment, { + at: blockPath, + select: true, + }); +} + +function insertOnEmbedBlock(editor: ReactEditor, fragment: Node[], blockPath: Path) { + insertNodes(editor, fragment, { + at: Path.next(blockPath), + select: true, + }); +} + +function insertNodes(editor: ReactEditor, nodes: Node[], options: { at?: Location; select?: boolean } = {}) { + try { + Transforms.insertNodes(editor, nodes, options); + } catch (e) { + try { + editor.move({ + distance: 1, + unit: 'line', + }); + } catch (e) { + // do nothing + } + } +} + +/** + * Copy Code from slate/src/utils/get-default-insert-location.ts + * Get the default location to insert content into the editor. + * By default, use the selection as the target location. But if there is + * no selection, insert at the end of the document since that is such a + * common use case when inserting from a non-selected state. + */ +export const getDefaultInsertLocation = (editor: Editor): Location => { + if (editor.selection) { + return editor.selection; + } else if (editor.children.length > 0) { + return Editor.end(editor, []); + } else { + return [0]; + } +}; + +export function transFragment(editor: ReactEditor, fragment: Node[]) { + // flatten the fragment to avoid the empty node(doesn't have text node) in the fragment + const flatMap = (node: Node): Node[] => { + const isInputElement = + !Editor.isEditor(node) && Element.isElement(node) && node.blockId !== undefined && !editor.isEmbed(node); + + if ( + isInputElement && + node.children?.length > 0 && + Element.isElement(node.children[0]) && + node.children[0].type !== EditorNodeType.Text + ) { + return node.children.flatMap((child) => flatMap(child)); + } + + return [node]; + }; + + const fragmentFlatMap = fragment?.flatMap(flatMap); + + // clone the node to avoid the duplicated block id + return fragmentFlatMap.map((item) => CustomEditor.cloneBlock(editor, item as Element)); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts new file mode 100644 index 0000000000..c0daab0a8f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts @@ -0,0 +1,40 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Element, Range } from 'slate'; + +export function withCopy(editor: ReactEditor) { + const { setFragmentData } = editor; + + editor.setFragmentData = (...args) => { + if (!editor.selection) { + setFragmentData(...args); + return; + } + + // selection is collapsed and the node is an embed, we need to set the data manually + if (Range.isCollapsed(editor.selection)) { + const match = Editor.above(editor, { + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + const node = match ? (match[0] as Element) : undefined; + + if (node && editor.isEmbed(node)) { + const fragment = editor.getFragment(); + + if (fragment.length > 0) { + const data = args[0]; + const string = JSON.stringify(fragment); + const encoded = window.btoa(encodeURIComponent(string)); + + const dom = ReactEditor.toDOMNode(editor, node); + + data.setData(`application/x-slate-fragment`, encoded); + data.setData(`text/html`, dom.innerHTML); + } + } + } + + setFragmentData(...args); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts new file mode 100644 index 0000000000..2266ff41c7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts @@ -0,0 +1,59 @@ +import { ReactEditor } from 'slate-react'; +import { insertFragment, transFragment } from './utils'; +import { convertBlockToJson } from '$app/application/document/document.service'; +import { InputType } from '@/services/backend'; +import { CustomEditor } from '$app/components/editor/command'; +import { Log } from '$app/utils/log'; + +export function withPasted(editor: ReactEditor) { + const { insertData } = editor; + + editor.insertData = (data) => { + const fragment = data.getData('application/x-slate-fragment'); + + if (fragment) { + insertData(data); + return; + } + + const html = data.getData('text/html'); + const text = data.getData('text/plain'); + + if (!html && !text) { + insertData(data); + return; + } + + void (async () => { + try { + const nodes = await convertBlockToJson(html, InputType.Html); + + const htmlTransNoText = nodes.every((node) => { + return CustomEditor.getNodeTextContent(node).length === 0; + }); + + if (!htmlTransNoText) { + return editor.insertFragment(nodes); + } + } catch (e) { + Log.warn('pasted html error', e); + // ignore + } + + if (text) { + const nodes = await convertBlockToJson(text, InputType.PlainText); + + editor.insertFragment(nodes); + return; + } + })(); + }; + + editor.insertFragment = (fragment, options = {}) => { + const clonedFragment = transFragment(editor, fragment); + + insertFragment(editor, clonedFragment, options); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts new file mode 100644 index 0000000000..0292784ba5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts @@ -0,0 +1,2 @@ +export * from './shortcuts.hooks'; +export * from './withMarkdown'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts new file mode 100644 index 0000000000..59ff0a8593 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts @@ -0,0 +1,172 @@ +export type MarkdownRegex = { + [key in MarkdownShortcuts]: { + pattern: RegExp; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record; + }[]; +}; + +export type TriggerHotKey = { + [key in MarkdownShortcuts]: string[]; +}; + +export enum MarkdownShortcuts { + Bold, + Italic, + StrikeThrough, + Code, + Equation, + /** block */ + Heading, + BlockQuote, + CodeBlock, + Divider, + /** list */ + BulletedList, + NumberedList, + TodoList, + ToggleList, +} + +const defaultMarkdownRegex: MarkdownRegex = { + [MarkdownShortcuts.Heading]: [ + { + pattern: /^#{1,6}$/, + }, + ], + [MarkdownShortcuts.Bold]: [ + { + pattern: /(\*\*|__)(.*?)(\*\*|__)$/, + }, + ], + [MarkdownShortcuts.Italic]: [ + { + pattern: /([*_])(.*?)([*_])$/, + }, + ], + [MarkdownShortcuts.StrikeThrough]: [ + { + pattern: /(~~)(.*?)(~~)$/, + }, + { + pattern: /(~)(.*?)(~)$/, + }, + ], + [MarkdownShortcuts.Code]: [ + { + pattern: /(`)(.*?)(`)$/, + }, + ], + [MarkdownShortcuts.Equation]: [ + { + pattern: /(\$)(.*?)(\$)$/, + data: { + formula: '', + }, + }, + ], + [MarkdownShortcuts.BlockQuote]: [ + { + pattern: /^([”“"])$/, + }, + ], + [MarkdownShortcuts.CodeBlock]: [ + { + pattern: /^(`{2,})$/, + data: { + language: 'json', + }, + }, + ], + [MarkdownShortcuts.Divider]: [ + { + pattern: /^(([-*]){2,})$/, + }, + ], + + [MarkdownShortcuts.BulletedList]: [ + { + pattern: /^([*\-+])$/, + }, + ], + [MarkdownShortcuts.NumberedList]: [ + { + pattern: /^(\d+)\.$/, + }, + ], + [MarkdownShortcuts.TodoList]: [ + { + pattern: /^(-)?\[ ]$/, + data: { + checked: false, + }, + }, + { + pattern: /^(-)?\[x]$/, + data: { + checked: true, + }, + }, + { + pattern: /^(-)?\[]$/, + data: { + checked: false, + }, + }, + ], + [MarkdownShortcuts.ToggleList]: [ + { + pattern: /^>$/, + data: { + collapsed: false, + }, + }, + ], +}; + +export const defaultTriggerChar: TriggerHotKey = { + [MarkdownShortcuts.Heading]: [' '], + [MarkdownShortcuts.Bold]: ['*', '_'], + [MarkdownShortcuts.Italic]: ['*', '_'], + [MarkdownShortcuts.StrikeThrough]: ['~'], + [MarkdownShortcuts.Code]: ['`'], + [MarkdownShortcuts.BlockQuote]: [' '], + [MarkdownShortcuts.CodeBlock]: ['`'], + [MarkdownShortcuts.Divider]: ['-', '*'], + [MarkdownShortcuts.Equation]: ['$'], + [MarkdownShortcuts.BulletedList]: [' '], + [MarkdownShortcuts.NumberedList]: [' '], + [MarkdownShortcuts.TodoList]: [' '], + [MarkdownShortcuts.ToggleList]: [' '], +}; + +export function isTriggerChar(char: string) { + return Object.values(defaultTriggerChar).some((trigger) => trigger.includes(char)); +} + +export function whatShortcutTrigger(char: string): MarkdownShortcuts[] | null { + const isTrigger = isTriggerChar(char); + + if (!isTrigger) { + return null; + } + + const shortcuts = Object.keys(defaultTriggerChar).map((key) => Number(key) as MarkdownShortcuts); + + return shortcuts.filter((shortcut) => defaultTriggerChar[shortcut].includes(char)); +} + +export function getRegex(shortcut: MarkdownShortcuts) { + return defaultMarkdownRegex[shortcut]; +} + +export function whatShortcutsMatch(text: string) { + const shortcuts = Object.keys(defaultMarkdownRegex).map((key) => Number(key) as MarkdownShortcuts); + + return shortcuts.filter((shortcut) => { + const regexes = defaultMarkdownRegex[shortcut]; + + return regexes.some((regex) => regex.pattern.test(text)); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts new file mode 100644 index 0000000000..45d61f847c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts @@ -0,0 +1,349 @@ +import { ReactEditor } from 'slate-react'; +import { useCallback, KeyboardEvent } from 'react'; +import { EditorMarkFormat, EditorNodeType, ToggleListNode } from '$app/application/document/document.types'; +import { getBlock } from '$app/components/editor/plugins/utils'; +import { SOFT_BREAK_TYPES } from '$app/components/editor/plugins/constants'; +import { CustomEditor } from '$app/components/editor/command'; +import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { openUrl } from '$app/utils/open_url'; +import { Range } from 'slate'; +import { readText } from '@tauri-apps/api/clipboard'; +import { useDecorateDispatch } from '$app/components/editor/stores'; + +function getScrollContainer(editor: ReactEditor) { + const editorDom = ReactEditor.toDOMNode(editor, editor); + + return editorDom.closest('.appflowy-scroll-container') as HTMLDivElement; +} + +export function useShortcuts(editor: ReactEditor) { + const { add: addDecorate } = useDecorateDispatch(); + + const formatLink = useCallback(() => { + const { selection } = editor; + + if (!selection || Range.isCollapsed(selection)) return; + + const isIncludeRoot = CustomEditor.selectionIncludeRoot(editor); + + if (isIncludeRoot) return; + + const isActivatedInline = CustomEditor.isInlineActive(editor); + + if (isActivatedInline) return; + + addDecorate({ + range: selection, + class_name: 'bg-content-blue-100 rounded', + type: 'link', + }); + }, [addDecorate, editor]); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + const event = e.nativeEvent; + const hasEditableTarget = ReactEditor.hasEditableTarget(editor, event.target); + + if (!hasEditableTarget) return; + + const node = getBlock(editor); + + const { selection } = editor; + const isExpanded = selection && Range.isExpanded(selection); + + switch (true) { + /** + * Select all: Mod+A + * Default behavior: Select all text in the editor + * Special case for select all in code block: Only select all text in code block + */ + case createHotkey(HOT_KEY_NAME.SELECT_ALL)(event): + if (node && node.type === EditorNodeType.CodeBlock) { + e.preventDefault(); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + } + + break; + /** + * Escape: Esc + * Default behavior: Deselect editor + */ + case createHotkey(HOT_KEY_NAME.ESCAPE)(event): + editor.deselect(); + break; + /** + * Indent block: Tab + * Default behavior: Indent block + */ + case createHotkey(HOT_KEY_NAME.INDENT_BLOCK)(event): + e.preventDefault(); + if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { + editor.insertText('\t'); + break; + } + + CustomEditor.tabForward(editor); + break; + /** + * Outdent block: Shift+Tab + * Default behavior: Outdent block + */ + case createHotkey(HOT_KEY_NAME.OUTDENT_BLOCK)(event): + e.preventDefault(); + CustomEditor.tabBackward(editor); + break; + /** + * Split block: Enter + * Default behavior: Split block + * Special case for soft break types: Insert \n + */ + case createHotkey(HOT_KEY_NAME.SPLIT_BLOCK)(event): + if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { + e.preventDefault(); + editor.insertText('\n'); + } + + break; + /** + * Insert soft break: Shift+Enter + * Default behavior: Insert \n + * Special case for soft break types: Split block + */ + case createHotkey(HOT_KEY_NAME.INSERT_SOFT_BREAK)(event): + e.preventDefault(); + if (node && SOFT_BREAK_TYPES.includes(node.type as EditorNodeType)) { + editor.splitNodes({ + always: true, + }); + } else { + editor.insertText('\n'); + } + + break; + /** + * Toggle todo: Shift+Enter + * Default behavior: Toggle todo + * Special case for toggle list block: Toggle collapse + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_TODO)(event): + case createHotkey(HOT_KEY_NAME.TOGGLE_COLLAPSE)(event): + e.preventDefault(); + if (node && node.type === EditorNodeType.ToggleListBlock) { + CustomEditor.toggleToggleList(editor, node as ToggleListNode); + } else { + CustomEditor.toggleTodo(editor); + } + + break; + /** + * Backspace: Backspace / Shift+Backspace + * Default behavior: Delete backward + */ + case createHotkey(HOT_KEY_NAME.BACKSPACE)(event): + e.stopPropagation(); + break; + /** + * Open link: Alt + enter + * Default behavior: Open one link in selection + */ + case createHotkey(HOT_KEY_NAME.OPEN_LINK)(event): { + if (!isExpanded) break; + e.preventDefault(); + const links = CustomEditor.getLinks(editor); + + if (links.length === 0) break; + openUrl(links[0]); + break; + } + + /** + * Open links: Alt + Shift + enter + * Default behavior: Open all links in selection + */ + case createHotkey(HOT_KEY_NAME.OPEN_LINKS)(event): { + if (!isExpanded) break; + e.preventDefault(); + const links = CustomEditor.getLinks(editor); + + if (links.length === 0) break; + links.forEach((link) => openUrl(link)); + break; + } + + /** + * Extend line backward: Opt + Shift + right + * Default behavior: Extend line backward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_LINE_BACKWARD)(event): + e.preventDefault(); + CustomEditor.extendLineBackward(editor); + break; + /** + * Extend line forward: Opt + Shift + left + */ + case createHotkey(HOT_KEY_NAME.EXTEND_LINE_FORWARD)(event): + e.preventDefault(); + CustomEditor.extendLineForward(editor); + break; + + /** + * Paste: Mod + Shift + V + * Default behavior: Paste plain text + */ + case createHotkey(HOT_KEY_NAME.PASTE_PLAIN_TEXT)(event): + e.preventDefault(); + void (async () => { + const text = await readText(); + + if (!text) return; + CustomEditor.insertPlainText(editor, text); + })(); + + break; + /** + * Highlight: Mod + Shift + H + * Default behavior: Highlight selected text + */ + case createHotkey(HOT_KEY_NAME.HIGH_LIGHT)(event): + e.preventDefault(); + CustomEditor.highlight(editor); + break; + /** + * Extend document backward: Mod + Shift + Up + * Don't prevent default behavior + * Default behavior: Extend document backward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD)(event): + editor.collapse({ edge: 'start' }); + break; + /** + * Extend document forward: Mod + Shift + Down + * Don't prevent default behavior + * Default behavior: Extend document forward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD)(event): + editor.collapse({ edge: 'end' }); + break; + + /** + * Scroll to top: Home + * Default behavior: Scroll to top + */ + case createHotkey(HOT_KEY_NAME.SCROLL_TO_TOP)(event): { + const scrollContainer = getScrollContainer(editor); + + scrollContainer.scrollTo({ + top: 0, + }); + break; + } + + /** + * Scroll to bottom: End + * Default behavior: Scroll to bottom + */ + case createHotkey(HOT_KEY_NAME.SCROLL_TO_BOTTOM)(event): { + const scrollContainer = getScrollContainer(editor); + + scrollContainer.scrollTo({ + top: scrollContainer.scrollHeight, + }); + break; + } + + /** + * Align left: Control + Shift + L + * Default behavior: Align left + */ + case createHotkey(HOT_KEY_NAME.ALIGN_LEFT)(event): + e.preventDefault(); + CustomEditor.toggleAlign(editor, 'left'); + break; + /** + * Align center: Control + Shift + E + */ + case createHotkey(HOT_KEY_NAME.ALIGN_CENTER)(event): + e.preventDefault(); + CustomEditor.toggleAlign(editor, 'center'); + break; + /** + * Align right: Control + Shift + R + */ + case createHotkey(HOT_KEY_NAME.ALIGN_RIGHT)(event): + e.preventDefault(); + CustomEditor.toggleAlign(editor, 'right'); + break; + /** + * Bold: Mod + B + */ + case createHotkey(HOT_KEY_NAME.BOLD)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Bold, + value: true, + }); + break; + /** + * Italic: Mod + I + */ + case createHotkey(HOT_KEY_NAME.ITALIC)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Italic, + value: true, + }); + break; + /** + * Underline: Mod + U + */ + case createHotkey(HOT_KEY_NAME.UNDERLINE)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Underline, + value: true, + }); + break; + /** + * Strikethrough: Mod + Shift + S / Mod + Shift + X + */ + case createHotkey(HOT_KEY_NAME.STRIKETHROUGH)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.StrikeThrough, + value: true, + }); + break; + /** + * Code: Mod + E + */ + case createHotkey(HOT_KEY_NAME.CODE)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Code, + value: true, + }); + break; + /** + * Format link: Mod + K + */ + case createHotkey(HOT_KEY_NAME.FORMAT_LINK)(event): + formatLink(); + break; + + case createHotkey(HOT_KEY_NAME.FIND_REPLACE)(event): + console.log('find replace'); + break; + + default: + break; + } + }, + [formatLink, editor] + ); + + return { + onKeyDown, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts new file mode 100644 index 0000000000..fd7801204c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts @@ -0,0 +1,239 @@ +import { Range, Element, Editor, NodeEntry, Path } from 'slate'; +import { ReactEditor } from 'slate-react'; +import { + defaultTriggerChar, + getRegex, + MarkdownShortcuts, + whatShortcutsMatch, + whatShortcutTrigger, +} from '$app/components/editor/plugins/shortcuts/markdown'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorMarkFormat, EditorNodeType } from '$app/application/document/document.types'; +import isEqual from 'lodash-es/isEqual'; + +export const withMarkdown = (editor: ReactEditor) => { + const { insertText } = editor; + + editor.insertText = (char) => { + const { selection } = editor; + + insertText(char); + if (!selection || !Range.isCollapsed(selection)) { + return; + } + + const triggerShortcuts = whatShortcutTrigger(char); + + if (!triggerShortcuts) { + return; + } + + const match = CustomEditor.getBlock(editor); + const [node, path] = match as NodeEntry; + + let prevIsNumberedList = false; + + try { + const prevPath = Path.previous(path); + const prev = editor.node(prevPath) as NodeEntry; + + prevIsNumberedList = prev && prev[0].type === EditorNodeType.NumberedListBlock; + } catch (e) { + // do nothing + } + + const start = Editor.start(editor, path); + const beforeRange = { anchor: start, focus: selection.anchor }; + const beforeText = Editor.string(editor, beforeRange); + + const removeBeforeText = (beforeRange: Range) => { + editor.deleteBackward('character'); + editor.delete({ + at: beforeRange, + }); + }; + + const matchBlockShortcuts = whatShortcutsMatch(beforeText); + + for (const shortcut of matchBlockShortcuts) { + const block = whichBlock(shortcut, beforeText); + + // if the block shortcut is matched, remove the before text and turn to the block + // then return + if (block && defaultTriggerChar[shortcut].includes(char)) { + // Don't turn to the block condition + // 1. Heading should be able to co-exist with number list + if (block.type === EditorNodeType.NumberedListBlock && node.type === EditorNodeType.HeadingBlock) { + return; + } + + // 2. If the block is the same type, and data is the same + if (block.type === node.type && isEqual(block.data || {}, node.data || {})) { + return; + } + + // 3. If the block is number list, and the previous block is also number list + if (block.type === EditorNodeType.NumberedListBlock && prevIsNumberedList) { + return; + } + + removeBeforeText(beforeRange); + CustomEditor.turnToBlock(editor, block); + + return; + } + } + + // get the range that matches the mark shortcuts + const markRange = { + anchor: Editor.start(editor, selection.anchor.path), + focus: selection.focus, + }; + const rangeText = Editor.string(editor, markRange) + char; + + if (!rangeText) return; + + // inputting a character that is start of a mark + const isStartTyping = rangeText.indexOf(char) === rangeText.lastIndexOf(char); + + if (isStartTyping) return; + + // if the range text includes a double character mark, and the last one is not finished + const doubleCharNotFinish = + ['*', '_', '~'].includes(char) && + rangeText.indexOf(`${char}${char}`) > -1 && + rangeText.indexOf(`${char}${char}`) === rangeText.lastIndexOf(`${char}${char}`); + + if (doubleCharNotFinish) return; + + const matchMarkShortcuts = whatShortcutsMatch(rangeText); + + for (const shortcut of matchMarkShortcuts) { + const item = getRegex(shortcut).find((p) => p.pattern.test(rangeText)); + const execArr = item?.pattern?.exec(rangeText); + + const removeText = execArr ? execArr[0] : ''; + + const text = execArr ? execArr[2]?.replaceAll(char, '') : ''; + + if (text) { + const index = rangeText.indexOf(removeText); + const removeRange = { + anchor: { + path: markRange.anchor.path, + offset: markRange.anchor.offset + index, + }, + focus: { + path: markRange.anchor.path, + offset: markRange.anchor.offset + index + removeText.length, + }, + }; + + removeBeforeText(removeRange); + insertMark(editor, shortcut, text); + return; + } + } + }; + + return editor; +}; + +function whichBlock(shortcut: MarkdownShortcuts, beforeText: string) { + switch (shortcut) { + case MarkdownShortcuts.Heading: + return { + type: EditorNodeType.HeadingBlock, + data: { + level: beforeText.length, + }, + }; + case MarkdownShortcuts.CodeBlock: + return { + type: EditorNodeType.CodeBlock, + data: { + language: 'json', + }, + }; + case MarkdownShortcuts.BulletedList: + return { + type: EditorNodeType.BulletedListBlock, + data: {}, + }; + case MarkdownShortcuts.NumberedList: + return { + type: EditorNodeType.NumberedListBlock, + data: { + number: Number(beforeText.split('.')[0]) ?? 1, + }, + }; + case MarkdownShortcuts.TodoList: + return { + type: EditorNodeType.TodoListBlock, + data: { + checked: beforeText.includes('[x]'), + }, + }; + case MarkdownShortcuts.BlockQuote: + return { + type: EditorNodeType.QuoteBlock, + data: {}, + }; + case MarkdownShortcuts.Divider: + return { + type: EditorNodeType.DividerBlock, + data: {}, + }; + + case MarkdownShortcuts.ToggleList: + return { + type: EditorNodeType.ToggleListBlock, + data: { + collapsed: false, + }, + }; + + default: + return null; + } +} + +function insertMark(editor: ReactEditor, shortcut: MarkdownShortcuts, text: string) { + switch (shortcut) { + case MarkdownShortcuts.Bold: + case MarkdownShortcuts.Italic: + case MarkdownShortcuts.StrikeThrough: + case MarkdownShortcuts.Code: { + const textNode = { + text, + }; + const attributes = { + [MarkdownShortcuts.Bold]: { + [EditorMarkFormat.Bold]: true, + }, + [MarkdownShortcuts.Italic]: { + [EditorMarkFormat.Italic]: true, + }, + [MarkdownShortcuts.StrikeThrough]: { + [EditorMarkFormat.StrikeThrough]: true, + }, + [MarkdownShortcuts.Code]: { + [EditorMarkFormat.Code]: true, + }, + }; + + Object.assign(textNode, attributes[shortcut]); + + editor.insertNodes(textNode); + return; + } + + case MarkdownShortcuts.Equation: { + CustomEditor.insertFormula(editor, text); + return; + } + + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts new file mode 100644 index 0000000000..62e3ad945a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts @@ -0,0 +1,38 @@ +import { Element, NodeEntry } from 'slate'; +import { ReactEditor } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; + +export function getHeadingCssProperty(level: number) { + switch (level) { + case 1: + return 'text-3xl pt-[10px] pb-[8px] font-bold'; + case 2: + return 'text-2xl pt-[8px] pb-[6px] font-bold'; + case 3: + return 'text-xl pt-[4px] font-bold'; + case 4: + return 'text-lg pt-[4px] font-bold'; + case 5: + return 'text-base pt-[4px] font-bold'; + case 6: + return 'text-sm pt-[4px] font-bold'; + default: + return ''; + } +} + +export function getBlock(editor: ReactEditor) { + const match = CustomEditor.getBlock(editor); + + if (match) { + const [node] = match as NodeEntry; + + return node; + } + + return; +} + +export function getEditorDomNode(editor: ReactEditor) { + return ReactEditor.toDOMNode(editor, editor); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts new file mode 100644 index 0000000000..0bcd0965a9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts @@ -0,0 +1,228 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Element, NodeEntry, Path, Range } from 'slate'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; + +/** + * Delete backward. + * | -> cursor + * + * ------------------- delete backward to its previous sibling lift all children + * 1 1|2 + * |2 3 + * 3 => delete backward => 4 + * 4 5 + * 5 + * ------------------- delete backward to its parent and lift all children + * 1 1|2 + * |2 3 + * 3 => delete backward => 4 + * 4 5 + * 5 + * ------------------- outdent the node if the node has no children + * 1 1 + * 2 2 + * |3 |3 + * 4 => delete backward => 4 + * @param editor + */ +export function withBlockDelete(editor: ReactEditor) { + const { deleteBackward, deleteFragment, mergeNodes } = editor; + + editor.deleteBackward = (unit) => { + const match = CustomEditor.getBlock(editor); + + if (!match || !CustomEditor.focusAtStartOfBlock(editor)) { + deleteBackward(unit); + return; + } + + const [node, path] = match; + + const isEmbed = editor.isEmbed(node); + + if (isEmbed) { + CustomEditor.deleteNode(editor, node); + return; + } + + const previous = editor.previous({ + at: path, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + const [previousNode] = previous || [undefined, undefined]; + + const previousIsPage = previousNode && Element.isElement(previousNode) && previousNode.type === EditorNodeType.Page; + + // merge to document title + if (previousIsPage) { + const textNodePath = [...path, 0]; + const [textNode] = editor.node(textNodePath); + const text = CustomEditor.getNodeTextContent(textNode); + + // clear all attributes + editor.select(textNodePath); + CustomEditor.removeMarks(editor); + editor.insertText(text); + + editor.move({ + distance: text.length, + reverse: true, + }); + } + + // if the current node is not a paragraph, convert it to a paragraph(except code block and callout block) + if ( + ![EditorNodeType.Paragraph, EditorNodeType.CalloutBlock, EditorNodeType.CodeBlock].includes( + node.type as EditorNodeType + ) && + node.type !== EditorNodeType.Page + ) { + CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); + return; + } + + const next = editor.next({ + at: path, + }); + + if (!next && path.length > 1) { + CustomEditor.tabBackward(editor); + return; + } + + const length = node.children.length; + + for (let i = length - 1; i > 0; i--) { + editor.liftNodes({ + at: [...path, i], + }); + } + + // if previous node is an embed, merge the current node to another node which is not an embed + if (Element.isElement(previousNode) && editor.isEmbed(previousNode)) { + const previousTextMatch = editor.previous({ + at: path, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.textId !== undefined, + }); + + if (!previousTextMatch) { + deleteBackward(unit); + return; + } + + const previousTextPath = previousTextMatch[1]; + const textNode = node.children[0] as Element; + + const at = Editor.end(editor, previousTextPath); + + editor.select(at); + editor.insertNodes(textNode.children, { + at, + }); + + editor.removeNodes({ + at: path, + }); + return; + } + + deleteBackward(unit); + }; + + editor.deleteFragment = (...args) => { + beforeDeleteToDocumentTitle(editor); + + deleteFragment(...args); + }; + + editor.mergeNodes = (options) => { + mergeNodes(options); + if (!editor.selection || !options?.at) return; + const nextPath = findNextPath(editor, editor.selection.anchor.path); + + const [nextNode] = editor.node(nextPath); + + if (Element.isElement(nextNode) && nextNode.blockId !== undefined && nextNode.children.length === 0) { + editor.removeNodes({ + at: nextPath, + }); + } + + return; + }; + + return editor; +} + +function beforeDeleteToDocumentTitle(editor: ReactEditor) { + if (!editor.selection) return; + if (Range.isCollapsed(editor.selection)) return; + const start = Range.start(editor.selection); + const end = Range.end(editor.selection); + const startNode = editor.above({ + at: start, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorNodeType.Page, + }); + + const endNode = editor.above({ + at: end, + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + + const startNodeIsPage = !!startNode; + + if (!startNodeIsPage || !endNode) return; + const [node, path] = endNode as NodeEntry; + const selectedText = editor.string({ + anchor: { + path, + offset: 0, + }, + focus: end, + }); + + const nodeChildren = node.children; + const nodeChildrenLength = nodeChildren.length; + + for (let i = nodeChildrenLength - 1; i > 0; i--) { + editor.liftNodes({ + at: [...path, i], + }); + } + + const textNodePath = [...path, 0]; + const [textNode] = editor.node(textNodePath); + const text = CustomEditor.getNodeTextContent(textNode); + + // clear all attributes + editor.select([...path, 0]); + CustomEditor.removeMarks(editor); + editor.insertText(text); + editor.move({ + distance: text.length - selectedText.length, + reverse: true, + }); + editor.select({ + anchor: start, + focus: editor.selection.focus, + }); +} + +function findNextPath(editor: ReactEditor, path: Path): Path { + if (path.length === 0) return path; + const parentPath = Path.parent(path); + + try { + const nextPath = Path.next(path); + const [nextNode] = Editor.node(editor, nextPath); + + if (Element.isElement(nextNode) && nextNode.blockId !== undefined) { + return nextPath; + } + } catch (e) { + // ignore + } + + return findNextPath(editor, parentPath); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts new file mode 100644 index 0000000000..b6f8da0e56 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts @@ -0,0 +1,94 @@ +import { ReactEditor } from 'slate-react'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { Path, Transforms } from 'slate'; +import { YjsEditor } from '@slate-yjs/core'; +import { generateId } from '$app/components/editor/provider/utils/convert'; + +export function withBlockInsertBreak(editor: ReactEditor) { + const { insertBreak } = editor; + + editor.insertBreak = (...args) => { + const block = CustomEditor.getBlock(editor); + + if (!block) return insertBreak(...args); + + const [node, path] = block; + + const isEmbed = editor.isEmbed(node); + + const nextPath = Path.next(path); + + if (isEmbed) { + CustomEditor.insertEmptyLine(editor as ReactEditor & YjsEditor, nextPath); + editor.select(nextPath); + return; + } + + const type = node.type as EditorNodeType; + + const isBeginning = CustomEditor.focusAtStartOfBlock(editor); + + const isEmpty = CustomEditor.isEmptyText(editor, node); + + if (isEmpty) { + const depth = path.length; + let hasNextNode = false; + + try { + hasNextNode = Boolean(editor.node(nextPath)); + } catch (e) { + // do nothing + } + + // if the node is empty and the depth is greater than 1, tab backward + if (depth > 1 && !hasNextNode) { + CustomEditor.tabBackward(editor); + return; + } + + // if the node is empty, convert it to a paragraph + if (type !== EditorNodeType.Paragraph && type !== EditorNodeType.Page) { + CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); + return; + } + } else if (isBeginning) { + // insert line below the current block + const newNodeType = [ + EditorNodeType.TodoListBlock, + EditorNodeType.BulletedListBlock, + EditorNodeType.NumberedListBlock, + ].includes(type) + ? type + : EditorNodeType.Paragraph; + + Transforms.insertNodes( + editor, + { + type: newNodeType, + data: node.data ?? {}, + blockId: generateId(), + children: [ + { + type: EditorNodeType.Text, + textId: generateId(), + children: [ + { + text: '', + }, + ], + }, + ], + }, + { + at: path, + } + ); + return; + } + + insertBreak(...args); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockMove.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockMove.ts new file mode 100644 index 0000000000..814c6e7333 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockMove.ts @@ -0,0 +1,134 @@ +import { ReactEditor } from 'slate-react'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import { Editor, Element, Location, NodeEntry, Path, Node, Transforms } from 'slate'; +import { EditorNodeType } from '$app/application/document/document.types'; + +const matchPath = (editor: Editor, path: Path): ((node: Node) => boolean) => { + const [node] = Editor.node(editor, path); + + return (n) => { + return n === node; + }; +}; + +export function withBlockMove(editor: ReactEditor) { + const { moveNodes } = editor; + + editor.moveNodes = (args) => { + const { to } = args; + + moveNodes(args); + + replaceId(editor, to); + }; + + editor.liftNodes = (args = {}) => { + Editor.withoutNormalizing(editor, () => { + const { at = editor.selection, mode = 'lowest', voids = false } = args; + let { match } = args; + + if (!match) { + match = Path.isPath(at) + ? matchPath(editor, at) + : (n) => Element.isElement(n) && Editor.isBlock(editor, n) && n.blockId !== undefined; + } + + if (!at) { + return; + } + + const matches = Editor.nodes(editor, { at, match, mode, voids }); + + const pathRefs = Array.from(matches, ([, p]) => { + return Editor.pathRef(editor, p); + }); + + for (const pathRef of pathRefs) { + const path = pathRef.unref(); + + if (!path) return; + if (path.length < 2) { + throw new Error(`Cannot lift node at a path [${path}] because it has a depth of less than \`2\`.`); + } + + const parentNodeEntry = Editor.node(editor, Path.parent(path)); + const [parent, parentPath] = parentNodeEntry as NodeEntry; + const index = path[path.length - 1]; + const { length } = parent.children; + + if (length === 1) { + const toPath = Path.next(parentPath); + + Transforms.moveNodes(editor, { at: path, to: toPath, voids }); + Transforms.removeNodes(editor, { at: parentPath, voids }); + } else if (index === 0) { + Transforms.moveNodes(editor, { at: path, to: parentPath, voids }); + } else if (index === length - 1) { + const toPath = Path.next(parentPath); + + Transforms.moveNodes(editor, { at: path, to: toPath, voids }); + } else { + const toPath = Path.next(parentPath); + + const node = parent.children[index] as Element; + const nodeChildrenLength = node.children.length; + + for (let i = length - 1; i > index; i--) { + Transforms.moveNodes(editor, { + at: [...parentPath, i], + to: [...path, nodeChildrenLength], + mode: 'all', + }); + } + + Transforms.moveNodes(editor, { at: path, to: toPath, voids }); + } + } + }); + }; + + return editor; +} + +function replaceId(editor: Editor, at?: Location) { + const newBlockId = generateId(); + const newTextId = generateId(); + + const selection = editor.selection; + + const location = at || selection; + + if (!location) return; + + const [node, path] = editor.node(location) as NodeEntry; + + if (node.blockId === undefined) { + return; + } + + const [textNode, ...children] = node.children as Element[]; + + editor.setNodes( + { + blockId: newBlockId, + }, + { + at, + } + ); + + if (textNode && textNode.type === EditorNodeType.Text) { + editor.setNodes( + { + textId: newTextId, + }, + { + at: [...path, 0], + } + ); + } + + children.forEach((_, index) => { + replaceId(editor, [...path, index + 1]); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts new file mode 100644 index 0000000000..1e9fc7f105 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts @@ -0,0 +1,30 @@ +import { ReactEditor } from 'slate-react'; + +import { withBlockDelete } from '$app/components/editor/plugins/withBlockDelete'; +import { withBlockInsertBreak } from '$app/components/editor/plugins/withBlockInsertBreak'; +import { withSplitNodes } from '$app/components/editor/plugins/withSplitNodes'; +import { withPasted, withCopy } from '$app/components/editor/plugins/copyPasted'; +import { withBlockMove } from '$app/components/editor/plugins/withBlockMove'; +import { CustomEditor } from '$app/components/editor/command'; + +export function withBlockPlugins(editor: ReactEditor) { + const { isElementReadOnly, isEmpty, isSelectable } = editor; + + editor.isElementReadOnly = (element) => { + return CustomEditor.isEmbedNode(element) || isElementReadOnly(element); + }; + + editor.isEmbed = (element) => { + return CustomEditor.isEmbedNode(element); + }; + + editor.isSelectable = (element) => { + return !CustomEditor.isEmbedNode(element) && isSelectable(element); + }; + + editor.isEmpty = (element) => { + return !CustomEditor.isEmbedNode(element) && isEmpty(element); + }; + + return withPasted(withBlockMove(withSplitNodes(withBlockInsertBreak(withBlockDelete(withCopy(editor)))))); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts new file mode 100644 index 0000000000..eee7dd92d0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts @@ -0,0 +1,139 @@ +import { ReactEditor } from 'slate-react'; +import { Transforms, Editor, Element, NodeEntry, Path, Range } from 'slate'; +import { EditorNodeType, ToggleListNode } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import cloneDeep from 'lodash-es/cloneDeep'; +import { SOFT_BREAK_TYPES } from '$app/components/editor/plugins/constants'; + +/** + * Split nodes. + * split text node into two text nodes, and wrap the second text node with a new block node. + * + * Split to the first child condition: + * 1. block type is toggle list block, and the block is not collapsed. + * + * Split to the next sibling condition: + * 1. block type is toggle list block, and the block is collapsed. + * 2. block type is other block type. + * + * Split to a paragraph node: (otherwise split to the same block type) + * 1. block type is heading block. + * 2. block type is quote block. + * 3. block type is page. + * 4. block type is code block and callout block. + * 5. block type is paragraph. + * + * @param editor + */ +export function withSplitNodes(editor: ReactEditor) { + const { splitNodes } = editor; + + editor.splitNodes = (...args) => { + const isInsertBreak = args.length === 1 && JSON.stringify(args[0]) === JSON.stringify({ always: true }); + + if (!isInsertBreak) { + splitNodes(...args); + return; + } + + const selection = editor.selection; + + const isCollapsed = selection && Range.isCollapsed(selection); + + if (!isCollapsed) { + editor.deleteFragment({ direction: 'backward' }); + } + + const match = CustomEditor.getBlock(editor); + + if (!match) { + splitNodes(...args); + return; + } + + const [node, path] = match; + const nodeType = node.type as EditorNodeType; + + const newBlockId = generateId(); + const newTextId = generateId(); + + splitNodes(...args); + + const matchTextNode = editor.above({ + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.type === EditorNodeType.Text, + }); + + if (!matchTextNode) return; + const [textNode, textNodePath] = matchTextNode as NodeEntry; + + editor.removeNodes({ + at: textNodePath, + }); + + const newNodeType = [ + EditorNodeType.HeadingBlock, + EditorNodeType.QuoteBlock, + EditorNodeType.Page, + ...SOFT_BREAK_TYPES, + ].includes(node.type as EditorNodeType) + ? EditorNodeType.Paragraph + : node.type; + + const newNode: Element = { + type: newNodeType, + data: {}, + blockId: newBlockId, + children: [ + { + ...cloneDeep(textNode), + textId: newTextId, + }, + ], + }; + let newNodePath; + + if (nodeType === EditorNodeType.ToggleListBlock) { + const collapsed = (node as ToggleListNode).data.collapsed; + + if (!collapsed) { + newNode.type = EditorNodeType.Paragraph; + newNodePath = textNodePath; + } else { + newNode.type = EditorNodeType.ToggleListBlock; + newNodePath = Path.next(path); + } + + Transforms.insertNodes(editor, newNode, { + at: newNodePath, + }); + + editor.select(newNodePath); + + CustomEditor.removeMarks(editor); + editor.collapse({ + edge: 'start', + }); + return; + } + + newNodePath = textNodePath; + + Transforms.insertNodes(editor, newNode, { + at: newNodePath, + }); + + editor.select(newNodePath); + editor.collapse({ + edge: 'start', + }); + + editor.liftNodes({ + at: newNodePath, + }); + + CustomEditor.removeMarks(editor); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts new file mode 100644 index 0000000000..026ee57222 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts @@ -0,0 +1,124 @@ +import { applyActions } from './utils/mockBackendService'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import { Provider } from '$app/components/editor/provider'; +import * as Y from 'yjs'; +import { BlockActionTypePB } from '@/services/backend'; +import { + generateFormulaInsertTextOp, + generateInsertTextOp, + genersteMentionInsertTextOp, +} from '$app/components/editor/provider/__tests__/utils/convert'; + +describe('Transform events to actions', () => { + let provider: Provider; + beforeEach(() => { + provider = new Provider(generateId()); + provider.initialDocument(true); + provider.connect(); + applyActions.mockClear(); + }); + + afterEach(() => { + provider.disconnect(); + }); + + test('should transform insert event to insert action', () => { + const sharedType = provider.sharedType; + + const insertTextOp = generateInsertTextOp('insert text'); + + sharedType?.applyDelta([{ retain: 2 }, insertTextOp]); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(2); + const textId = actions[0].payload.text_id; + expect(actions[0].action).toBe(BlockActionTypePB.InsertText); + expect(actions[0].payload.delta).toBe('[{"insert":"insert text"}]'); + expect(actions[1].action).toBe(BlockActionTypePB.Insert); + expect(actions[1].payload.block.ty).toBe('paragraph'); + expect(actions[1].payload.block.parent_id).toBe('3EzeCrtxlh'); + expect(actions[1].payload.block.children_id).not.toBeNull(); + expect(actions[1].payload.block.external_id).toBe(textId); + expect(actions[1].payload.parent_id).toBe('3EzeCrtxlh'); + expect(actions[1].payload.prev_id).toBe('2qonPRrNTO'); + }); + + test('should transform delete event to delete action', () => { + const sharedType = provider.sharedType; + + sharedType?.doc?.transact(() => { + sharedType?.applyDelta([{ retain: 4 }, { delete: 1 }]); + }); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(1); + expect(actions[0].action).toBe(BlockActionTypePB.Delete); + expect(actions[0].payload.block.id).toBe('Fn4KACkt1i'); + }); + + test('should transform update event to update action', () => { + const sharedType = provider.sharedType; + + const yText = sharedType?.toDelta()[4].insert as Y.XmlText; + sharedType?.doc?.transact(() => { + yText.setAttribute('data', { + checked: true, + }); + }); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(1); + expect(actions[0].action).toBe(BlockActionTypePB.Update); + expect(actions[0].payload.block.id).toBe('Fn4KACkt1i'); + expect(actions[0].payload.block.data).toBe('{"checked":true}'); + }); + + test('should transform apply delta event to apply delta action (insert text)', () => { + const sharedType = provider.sharedType; + + const blockYText = sharedType?.toDelta()[4].insert as Y.XmlText; + const textYText = blockYText.toDelta()[0].insert as Y.XmlText; + sharedType?.doc?.transact(() => { + textYText.applyDelta([{ retain: 1 }, { insert: 'apply delta' }]); + }); + const textId = textYText.getAttribute('textId'); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(1); + expect(actions[0].action).toBe(BlockActionTypePB.ApplyTextDelta); + expect(actions[0].payload.text_id).toBe(textId); + expect(actions[0].payload.delta).toBe('[{"retain":1},{"insert":"apply delta"}]'); + }); + + test('should transform apply delta event to apply delta action: insert mention', () => { + const sharedType = provider.sharedType; + + const blockYText = sharedType?.toDelta()[4].insert as Y.XmlText; + const yText = blockYText.toDelta()[0].insert as Y.XmlText; + sharedType?.doc?.transact(() => { + yText.applyDelta([{ retain: 1 }, genersteMentionInsertTextOp()]); + }); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(1); + expect(actions[0].action).toBe(BlockActionTypePB.ApplyTextDelta); + expect(actions[0].payload.delta).toBe('[{"retain":1},{"insert":"@","attributes":{"mention":{"page":"page_id"}}}]'); + }); + + test('should transform apply delta event to apply delta action: insert formula', () => { + const sharedType = provider.sharedType; + + const blockYText = sharedType?.toDelta()[4].insert as Y.XmlText; + const yText = blockYText.toDelta()[0].insert as Y.XmlText; + sharedType?.doc?.transact(() => { + yText.applyDelta([{ retain: 1 }, generateFormulaInsertTextOp()]); + }); + + const actions = applyActions.mock.calls[0][1]; + expect(actions).toHaveLength(1); + expect(actions[0].action).toBe(BlockActionTypePB.ApplyTextDelta); + expect(actions[0].payload.delta).toBe('[{"retain":1},{"insert":"= 1 + 1","attributes":{"formula":true}}]'); + }); +}); + +export {}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts new file mode 100644 index 0000000000..0937d265ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts @@ -0,0 +1,43 @@ +import { applyActions } from './utils/mockBackendService'; + +import { Provider } from '$app/components/editor/provider'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import { generateInsertTextOp } from '$app/components/editor/provider/__tests__/utils/convert'; + +export {}; + +describe('Provider connected', () => { + let provider: Provider; + + beforeEach(() => { + provider = new Provider(generateId()); + provider.initialDocument(true); + provider.connect(); + applyActions.mockClear(); + }); + + afterEach(() => { + provider.disconnect(); + }); + + test('should initial document', () => { + const sharedType = provider.sharedType; + expect(sharedType).not.toBeNull(); + expect(sharedType?.length).toBe(25); + expect(sharedType?.getAttribute('blockId')).toBe('3EzeCrtxlh'); + }); + + test('should send actions when the local changed', () => { + const sharedType = provider.sharedType; + + const parentId = sharedType?.getAttribute('blockId') as string; + const insertTextOp = generateInsertTextOp(''); + + sharedType?.applyDelta([{ retain: 2 }, insertTextOp]); + + expect(sharedType?.length).toBe(26); + expect(applyActions).toBeCalledTimes(1); + }); +}); + +export {}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/read_me.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/read_me.ts new file mode 100644 index 0000000000..adb85f2bfd --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/read_me.ts @@ -0,0 +1,437 @@ +export default { + viewId: '04acbc17-2265-4e4d-ac80-392bdc81379f', + rootId: '3EzeCrtxlh', + nodeMap: { + '692ooXzoV-': { + id: '692ooXzoV-', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: '4yIcpjxFTQ', + data: { checked: false }, + externalId: '9L10h3UZ7J', + externalType: 'text', + }, + gCjs671FiD: { + id: 'gCjs671FiD', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'Bj4M6midh3', + data: {}, + externalId: 'uGR_eATq2B', + externalType: 'text', + }, + whGVOpFJzA: { + id: 'whGVOpFJzA', + type: 'code', + parent: '3EzeCrtxlh', + children: 'eYqZSaUSrF', + data: { language: 'rust' }, + externalId: '2H8dnFhOsJ', + externalType: 'text', + }, + PeUTr8lpaW: { + id: 'PeUTr8lpaW', + type: 'divider', + parent: '3EzeCrtxlh', + children: '7gXZ4anHxc', + data: {}, + externalId: '', + externalType: '', + }, + aRUJ8rTJR9: { + id: 'aRUJ8rTJR9', + type: 'quote', + parent: '3EzeCrtxlh', + children: '877leNxAdX', + data: {}, + externalId: 'Qdn9CIuCJb', + externalType: 'text', + }, + '5OZNiernqA': { + id: '5OZNiernqA', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'rnWU0OMG2D', + data: {}, + externalId: '7szeShePbX', + externalType: 'text', + }, + '6okS7LcJz6': { + id: '6okS7LcJz6', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'o2VwjuyMqD', + data: {}, + externalId: 'T7KYfpQzkC', + externalType: 'text', + }, + '2qonPRrNTO': { + id: '2qonPRrNTO', + type: 'heading', + parent: '3EzeCrtxlh', + children: 'EwyNm_pVG3', + data: { level: 1 }, + externalId: 'zatA8Lta9U', + externalType: 'text', + }, + G6SPYNOXyd: { + id: 'G6SPYNOXyd', + type: 'heading', + parent: '3EzeCrtxlh', + children: 'dMAT_mdB3t', + data: { level: 2 }, + externalId: 'EZJU9Ks-XL', + externalType: 'text', + }, + 'i-7TRjZWn4': { + id: 'i-7TRjZWn4', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: '3NsAmPWBLT', + data: { checked: true }, + externalId: 'rG9KOmfyQc', + externalType: 'text', + }, + mAce5pJ5iN: { + id: 'mAce5pJ5iN', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'PavtRGeb_I', + data: {}, + externalId: 'UUBk3lnHDj', + externalType: 'text', + }, + ViWXVLgaux: { + id: 'ViWXVLgaux', + type: 'numbered_list', + parent: '3EzeCrtxlh', + children: 'osPbZZroOQ', + data: {}, + externalId: 'OkO9CIoWYX', + externalType: 'text', + }, + VrW0GWtvmq: { + id: 'VrW0GWtvmq', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: 'hFX8bE3MQ6', + data: { checked: false }, + externalId: 'wBzhBx7bcM', + externalType: 'text', + }, + YzM4q9vJgy: { + id: 'YzM4q9vJgy', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'mlDk334eJ5', + data: {}, + externalId: 'wIXo3cMKpn', + externalType: 'text', + }, + okBQecghDx: { + id: 'okBQecghDx', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: 'r7U-ocUQEj', + data: { checked: false }, + externalId: '8T-1vemF9G', + externalType: 'text', + }, + qJcR2SnePa: { + id: 'qJcR2SnePa', + type: 'callout', + parent: '3EzeCrtxlh', + children: 'X8-C5rdFkI', + data: { icon: '🥰' }, + externalId: 'o3mqcqjEvX', + externalType: 'text', + }, + q8trFTc21J: { + id: 'q8trFTc21J', + type: 'numbered_list', + parent: '3EzeCrtxlh', + children: 'Ye_KrA1Zqb', + data: {}, + externalId: 'E-XQYK1KGP', + externalType: 'text', + }, + '-7trMtJMEt': { + id: '-7trMtJMEt', + type: 'numbered_list', + parent: '3EzeCrtxlh', + children: '5-RQuN9654', + data: {}, + externalId: 'meKXZh3E_1', + externalType: 'text', + }, + Fn4KACkt1i: { + id: 'Fn4KACkt1i', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: 'MM6vCgc7RC', + data: { checked: false }, + externalId: 'v0XWYu0w3F', + externalType: 'text', + }, + TTU0eUzM4G: { + id: 'TTU0eUzM4G', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'Jo1ix-lCgZ', + data: {}, + externalId: 'IRpzcwVaU4', + externalType: 'text', + }, + '6QZZccBfnT': { + id: '6QZZccBfnT', + type: 'heading', + parent: '3EzeCrtxlh', + children: 'bdI2gAQB-G', + data: { level: 2 }, + externalId: 'J-oMw2g2_D', + externalType: 'text', + }, + sZT5qbJvLX: { + id: 'sZT5qbJvLX', + type: 'paragraph', + parent: '3EzeCrtxlh', + children: 'yR1_d6lPtR', + data: {}, + externalId: 'anA255zYMV', + externalType: 'text', + }, + '3EzeCrtxlh': { + id: '3EzeCrtxlh', + type: 'page', + parent: '', + children: 'ChrjyUcqp5', + data: {}, + externalId: 'bGaty5Tv88', + externalType: 'text', + }, + 'b3G76VM-nh': { + id: 'b3G76VM-nh', + type: 'heading', + parent: '3EzeCrtxlh', + children: '7PDV2Ev8pz', + data: { level: 2 }, + externalId: 'baM4S6ohnQ', + externalType: 'text', + }, + CxPil0324P: { + id: 'CxPil0324P', + type: 'todo_list', + parent: '3EzeCrtxlh', + children: 'qJkq_FYLux', + data: { checked: false }, + externalId: 'LGUrob79hg', + externalType: 'text', + }, + }, + childrenMap: { + '-7trMtJMEt': [], + okBQecghDx: [], + PeUTr8lpaW: [], + '6QZZccBfnT': [], + aRUJ8rTJR9: [], + G6SPYNOXyd: [], + VrW0GWtvmq: [], + 'i-7TRjZWn4': [], + '6okS7LcJz6': [], + Fn4KACkt1i: [], + qJcR2SnePa: [], + sZT5qbJvLX: [], + '2qonPRrNTO': [], + CxPil0324P: [], + YzM4q9vJgy: [], + mAce5pJ5iN: [], + gCjs671FiD: [], + q8trFTc21J: [], + whGVOpFJzA: [], + '5OZNiernqA': [], + 'b3G76VM-nh': [], + TTU0eUzM4G: [], + '3EzeCrtxlh': [ + '2qonPRrNTO', + 'b3G76VM-nh', + 'CxPil0324P', + 'Fn4KACkt1i', + 'okBQecghDx', + 'VrW0GWtvmq', + 'i-7TRjZWn4', + '692ooXzoV-', + 'sZT5qbJvLX', + 'PeUTr8lpaW', + 'YzM4q9vJgy', + 'G6SPYNOXyd', + '-7trMtJMEt', + 'q8trFTc21J', + 'ViWXVLgaux', + 'whGVOpFJzA', + 'mAce5pJ5iN', + '6QZZccBfnT', + 'aRUJ8rTJR9', + 'gCjs671FiD', + 'qJcR2SnePa', + '5OZNiernqA', + 'TTU0eUzM4G', + '6okS7LcJz6', + ], + ViWXVLgaux: [], + '692ooXzoV-': [], + }, + relativeMap: { + '4yIcpjxFTQ': '692ooXzoV-', + Bj4M6midh3: 'gCjs671FiD', + eYqZSaUSrF: 'whGVOpFJzA', + '7gXZ4anHxc': 'PeUTr8lpaW', + '877leNxAdX': 'aRUJ8rTJR9', + rnWU0OMG2D: '5OZNiernqA', + o2VwjuyMqD: '6okS7LcJz6', + EwyNm_pVG3: '2qonPRrNTO', + dMAT_mdB3t: 'G6SPYNOXyd', + '3NsAmPWBLT': 'i-7TRjZWn4', + PavtRGeb_I: 'mAce5pJ5iN', + osPbZZroOQ: 'ViWXVLgaux', + hFX8bE3MQ6: 'VrW0GWtvmq', + mlDk334eJ5: 'YzM4q9vJgy', + 'r7U-ocUQEj': 'okBQecghDx', + 'X8-C5rdFkI': 'qJcR2SnePa', + Ye_KrA1Zqb: 'q8trFTc21J', + '5-RQuN9654': '-7trMtJMEt', + MM6vCgc7RC: 'Fn4KACkt1i', + 'Jo1ix-lCgZ': 'TTU0eUzM4G', + 'bdI2gAQB-G': '6QZZccBfnT', + yR1_d6lPtR: 'sZT5qbJvLX', + ChrjyUcqp5: '3EzeCrtxlh', + '7PDV2Ev8pz': 'b3G76VM-nh', + qJkq_FYLux: 'CxPil0324P', + }, + deltaMap: { + gCjs671FiD: [], + G6SPYNOXyd: [{ insert: 'Keyboard shortcuts, markdown, and code block' }], + VrW0GWtvmq: [ + { insert: 'Type ' }, + { insert: '/', attributes: { code: true } }, + { insert: ' followed by ' }, + { insert: '/bullet', attributes: { code: true } }, + { insert: ' or ' }, + { insert: '/num', attributes: { code: true } }, + { insert: ' to create a list.', attributes: { code: false } }, + ], + '-7trMtJMEt': [ + { insert: 'Keyboard shortcuts ' }, + { + insert: 'guide', + attributes: { href: 'https://appflowy.gitbook.io/docs/essential-documentation/shortcuts' }, + }, + ], + okBQecghDx: [ + { insert: 'As soon as you type ' }, + { insert: '/', attributes: { font_color: '0xff00b5ff', code: true } }, + { insert: ' a menu will pop up. Select ' }, + { insert: 'different types', attributes: { bg_color: '0x4d9c27b0' } }, + { insert: ' of content blocks you can add.' }, + ], + qJcR2SnePa: [ + { insert: '\nLike AppFlowy? Follow us:\n' }, + { insert: 'GitHub', attributes: { href: 'https://github.com/AppFlowy-IO/AppFlowy' } }, + { insert: '\n' }, + { insert: 'Twitter', attributes: { href: 'https://twitter.com/appflowy' } }, + { insert: ': @appflowy\n' }, + { insert: 'Newsletter', attributes: { href: 'https://blog-appflowy.ghost.io/' } }, + { insert: '\n' }, + ], + whGVOpFJzA: [ + { + insert: + '// This is the main function.\nfn main() {\n // Print text to the console.\n println!("Hello World!");\n}', + }, + ], + YzM4q9vJgy: [], + '692ooXzoV-': [ + { insert: 'Click ' }, + { insert: '+', attributes: { code: true } }, + { insert: ' next to any page title in the sidebar to ' }, + { insert: 'quickly', attributes: { font_color: '0xff8427e0' } }, + { insert: ' add a new subpage, ' }, + { insert: 'Document', attributes: { code: true } }, + { insert: ', ', attributes: { code: false } }, + { insert: 'Grid', attributes: { code: true } }, + { insert: ', or ', attributes: { code: false } }, + { insert: 'Kanban Board', attributes: { code: true } }, + { insert: '.', attributes: { code: false } }, + ], + q8trFTc21J: [ + { insert: 'Markdown ' }, + { + insert: 'reference', + attributes: { href: 'https://appflowy.gitbook.io/docs/essential-documentation/markdown' }, + }, + ], + ViWXVLgaux: [ + { insert: 'Type ' }, + { insert: '/code', attributes: { code: true } }, + { insert: ' to insert a code block', attributes: { code: false } }, + ], + sZT5qbJvLX: [], + '6QZZccBfnT': [{ insert: 'Have a question❓' }], + '5OZNiernqA': [], + '6okS7LcJz6': [], + mAce5pJ5iN: [], + aRUJ8rTJR9: [ + { insert: 'Click ' }, + { insert: '?', attributes: { code: true } }, + { insert: ' at the bottom right for help and support.' }, + ], + Fn4KACkt1i: [ + { insert: 'Highlight ', attributes: { bg_color: '0x4dffeb3b' } }, + { insert: 'any text, and use the editing menu to ' }, + { insert: 'style', attributes: { italic: true } }, + { insert: ' ' }, + { insert: 'your', attributes: { bold: true } }, + { insert: ' ' }, + { insert: 'writing', attributes: { underline: true } }, + { insert: ' ' }, + { insert: 'however', attributes: { code: true } }, + { insert: ' you ' }, + { insert: 'like.', attributes: { strikethrough: true } }, + ], + '3EzeCrtxlh': [], + '2qonPRrNTO': [{ insert: 'Welcome to AppFlowy!' }], + 'b3G76VM-nh': [{ insert: 'Here are the basics' }], + TTU0eUzM4G: [], + CxPil0324P: [{ insert: 'Click anywhere and just start typing.' }], + 'i-7TRjZWn4': [ + { insert: 'Click ' }, + { insert: '+ New Page ', attributes: { code: true } }, + { insert: 'button at the bottom of your sidebar to add a new page.' }, + ], + }, + externalIdMap: { + '9L10h3UZ7J': '692ooXzoV-', + uGR_eATq2B: 'gCjs671FiD', + '2H8dnFhOsJ': 'whGVOpFJzA', + Qdn9CIuCJb: 'aRUJ8rTJR9', + '7szeShePbX': '5OZNiernqA', + T7KYfpQzkC: '6okS7LcJz6', + zatA8Lta9U: '2qonPRrNTO', + 'EZJU9Ks-XL': 'G6SPYNOXyd', + rG9KOmfyQc: 'i-7TRjZWn4', + UUBk3lnHDj: 'mAce5pJ5iN', + OkO9CIoWYX: 'ViWXVLgaux', + wBzhBx7bcM: 'VrW0GWtvmq', + wIXo3cMKpn: 'YzM4q9vJgy', + '8T-1vemF9G': 'okBQecghDx', + o3mqcqjEvX: 'qJcR2SnePa', + 'E-XQYK1KGP': 'q8trFTc21J', + meKXZh3E_1: '-7trMtJMEt', + v0XWYu0w3F: 'Fn4KACkt1i', + IRpzcwVaU4: 'TTU0eUzM4G', + 'J-oMw2g2_D': '6QZZccBfnT', + anA255zYMV: 'sZT5qbJvLX', + bGaty5Tv88: '3EzeCrtxlh', + baM4S6ohnQ: 'b3G76VM-nh', + LGUrob79hg: 'CxPil0324P', + }, +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/convert.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/convert.ts new file mode 100644 index 0000000000..028fff7419 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/convert.ts @@ -0,0 +1,76 @@ +/** + * @jest-environment jsdom + */ +import { slateNodesToInsertDelta } from '@slate-yjs/core'; +import * as Y from 'yjs'; +import { generateId } from '$app/components/editor/provider/utils/convert'; + +export function slateElementToYText({ + children, + ...attributes +}: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + children: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +}) { + const yElement = new Y.XmlText(); + + Object.entries(attributes).forEach(([key, value]) => { + yElement.setAttribute(key, value); + }); + yElement.applyDelta(slateNodesToInsertDelta(children), { + sanitize: false, + }); + return yElement; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function generateInsertTextOp(text: string) { + const insertYText = slateElementToYText({ + children: [ + { + type: 'text', + textId: generateId(), + children: [ + { + text, + }, + ], + }, + ], + type: 'paragraph', + data: {}, + blockId: generateId(), + }); + + return { + insert: insertYText, + }; +} + +export function genersteMentionInsertTextOp() { + const mentionYText = slateElementToYText({ + children: [{ text: '@' }], + type: 'mention', + data: { + page: 'page_id', + }, + }); + + return { + insert: mentionYText, + }; +} + +export function generateFormulaInsertTextOp() { + const formulaYText = slateElementToYText({ + children: [{ text: '= 1 + 1' }], + type: 'formula', + data: true, + }); + + return { + insert: formulaYText, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/mockBackendService.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/mockBackendService.ts new file mode 100644 index 0000000000..3bd7646268 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/utils/mockBackendService.ts @@ -0,0 +1,21 @@ +import read_me from '$app/components/editor/provider/__tests__/read_me'; + +const applyActions = jest.fn().mockReturnValue(Promise.resolve()); + +jest.mock('$app/application/notification', () => { + return { + subscribeNotification: jest.fn().mockReturnValue(Promise.resolve(() => ({}))), + }; +}); + +jest.mock('nanoid', () => ({ nanoid: jest.fn().mockReturnValue(String(Math.random())) })); + +jest.mock('$app/application/document/document.service', () => { + return { + openDocument: jest.fn().mockReturnValue(Promise.resolve(read_me)), + applyActions, + closeDocument: jest.fn().mockReturnValue(Promise.resolve()), + }; +}); + +export { applyActions }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/data_client.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/data_client.ts new file mode 100644 index 0000000000..bf0ea2c2a7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/data_client.ts @@ -0,0 +1,74 @@ +import { applyActions, closeDocument, openDocument } from '$app/application/document/document.service'; +import { slateNodesToInsertDelta } from '@slate-yjs/core'; +import { convertToSlateValue } from '$app/components/editor/provider/utils/convert'; +import { EventEmitter } from 'events'; +import { BlockActionPB, DocEventPB, DocumentNotification } from '@/services/backend'; +import { AsyncQueue } from '$app/utils/async_queue'; +import { subscribeNotification } from '$app/application/notification'; +import { YDelta } from '$app/components/editor/provider/types/y_event'; +import { DocEvent2YDelta } from '$app/components/editor/provider/utils/delta'; + +export class DataClient extends EventEmitter { + private queue: AsyncQueue[]>; + private unsubscribe: Promise<() => void>; + public rootId?: string; + + constructor(private id: string) { + super(); + this.queue = new AsyncQueue(this.sendActions); + this.unsubscribe = subscribeNotification(DocumentNotification.DidReceiveUpdate, this.sendMessage); + + this.on('update', this.handleReceiveMessage); + } + + public disconnect() { + this.off('update', this.handleReceiveMessage); + void closeDocument(this.id); + void this.unsubscribe.then((unsubscribe) => unsubscribe()); + } + + public async getInsertDelta(includeRoot = true) { + const data = await openDocument(this.id); + + this.rootId = data.rootId; + + const slateValue = convertToSlateValue(data, includeRoot); + + return slateNodesToInsertDelta(slateValue); + } + + public on(event: 'change', listener: (events: YDelta) => void): this; + public on(event: 'update', listener: (actions: ReturnType[]) => void): this; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public on(event: string, listener: (...args: any[]) => void): this { + return super.on(event, listener); + } + + public off(event: 'change', listener: (events: YDelta) => void): this; + public off(event: 'update', listener: (actions: ReturnType[]) => void): this; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public off(event: string, listener: (...args: any[]) => void): this { + return super.off(event, listener); + } + + public emit(event: 'change', events: YDelta): boolean; + public emit(event: 'update', actions: ReturnType[]): boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public emit(event: string, ...args: any[]): boolean { + return super.emit(event, ...args); + } + + private sendMessage = (docEvent: DocEventPB) => { + // transform events to ops + this.emit('change', DocEvent2YDelta(docEvent)); + }; + + private handleReceiveMessage = (actions: ReturnType[]) => { + this.queue.enqueue(actions); + }; + + private sendActions = async (actions: ReturnType[]) => { + if (!actions.length) return; + await applyActions(this.id, actions); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/index.ts new file mode 100644 index 0000000000..03be03e588 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/index.ts @@ -0,0 +1 @@ +export * from './provider'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts new file mode 100644 index 0000000000..727b33ec69 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts @@ -0,0 +1,74 @@ +import * as Y from 'yjs'; + +import { DataClient } from '$app/components/editor/provider/data_client'; +import { YDelta } from '$app/components/editor/provider/types/y_event'; +import { YEvents2BlockActions } from '$app/components/editor/provider/utils/action'; +import { EventEmitter } from 'events'; + +const REMOTE_ORIGIN = 'remote'; + +export class Provider extends EventEmitter { + document: Y.Doc = new Y.Doc(); + sharedType: Y.XmlText | null = null; + dataClient: DataClient; + // get origin data after document updated + backupDoc: Y.Doc = new Y.Doc(); + constructor(public id: string) { + super(); + this.dataClient = new DataClient(id); + this.document.on('update', this.documentUpdate); + } + + initialDocument = async (includeRoot = true) => { + const sharedType = this.document.get('sharedType', Y.XmlText) as Y.XmlText; + // Load the initial value into the yjs document + const delta = await this.dataClient.getInsertDelta(includeRoot); + + sharedType.applyDelta(delta); + + const rootId = this.dataClient.rootId as string; + const root = delta[0].insert as Y.XmlText; + const data = root.getAttribute('data'); + + sharedType.setAttribute('blockId', rootId); + sharedType.setAttribute('data', data); + + this.sharedType = sharedType; + this.sharedType?.observeDeep(this.onChange); + this.emit('ready'); + }; + + connect() { + this.dataClient.on('change', this.onRemoteChange); + return; + } + + disconnect() { + this.dataClient.off('change', this.onRemoteChange); + this.dataClient.disconnect(); + this.sharedType?.unobserveDeep(this.onChange); + this.sharedType = null; + } + + onChange = (events: Y.YEvent[], transaction: Y.Transaction) => { + if (transaction.origin === REMOTE_ORIGIN) { + return; + } + + if (!this.sharedType || !events.length) return; + // transform events to actions + this.dataClient.emit('update', YEvents2BlockActions(this.backupDoc, events)); + }; + + onRemoteChange = (delta: YDelta) => { + if (!delta.length) return; + + this.document.transact(() => { + this.sharedType?.applyDelta(delta); + }, REMOTE_ORIGIN); + }; + + documentUpdate = (update: Uint8Array) => { + Y.applyUpdate(this.backupDoc, update); + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/types/y_event.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/types/y_event.ts new file mode 100644 index 0000000000..36ec97aa39 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/types/y_event.ts @@ -0,0 +1,12 @@ +import { YXmlText } from 'yjs/dist/src/types/YXmlText'; + +export interface YOp { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + insert?: string | object | any[] | YXmlText | undefined; + retain?: number | undefined; + delete?: number | undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + attributes?: { [p: string]: any } | undefined; +} + +export type YDelta = YOp[]; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts new file mode 100644 index 0000000000..447a8f95f9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts @@ -0,0 +1,301 @@ +import * as Y from 'yjs'; +import { BlockActionPB, BlockActionTypePB } from '@/services/backend'; +import { generateId } from '$app/components/editor/provider/utils/convert'; +import { YDelta2Delta } from '$app/components/editor/provider/utils/delta'; +import { YDelta } from '$app/components/editor/provider/types/y_event'; +import { getInsertTarget, getYTarget } from '$app/components/editor/provider/utils/relation'; +import { EditorInlineNodeType, EditorNodeType } from '$app/application/document/document.types'; +import { Log } from '$app/utils/log'; + +export function YEvents2BlockActions( + backupDoc: Readonly, + events: Y.YEvent[] +): ReturnType[] { + const actions: ReturnType[] = []; + + events.forEach((event) => { + const eventActions = YEvent2BlockActions(backupDoc, event); + + if (eventActions.length === 0) return; + + actions.push(...eventActions); + }); + + return actions; +} + +export function YEvent2BlockActions( + backupDoc: Readonly, + event: Y.YEvent +): ReturnType[] { + const { target: yXmlText, keys, delta, path } = event; + const isBlockEvent = !!yXmlText.getAttribute('blockId'); + const sharedType = backupDoc.get('sharedType', Y.XmlText) as Readonly; + const rootId = sharedType.getAttribute('blockId'); + + const backupTarget = getYTarget(backupDoc, path) as Readonly; + const actions = []; + + if ([EditorInlineNodeType.Formula, EditorInlineNodeType.Mention].includes(yXmlText.getAttribute('type'))) { + const parentYXmlText = yXmlText.parent as Y.XmlText; + const parentDelta = parentYXmlText.toDelta() as YDelta; + const index = parentDelta.findIndex((op) => op.insert === yXmlText); + const ops = YDelta2Delta(parentDelta); + + const retainIndex = ops.reduce((acc, op, currentIndex) => { + if (currentIndex < index) { + return acc + (op.insert as string).length ?? 0; + } + + return acc; + }, 0); + + const newDelta = [ + { + retain: retainIndex, + }, + ...delta, + ]; + + actions.push(...generateApplyTextActions(parentYXmlText, newDelta)); + } + + if (yXmlText.getAttribute('type') === 'text') { + actions.push(...textOps2BlockActions(rootId, yXmlText, delta)); + } + + if (keys.size > 0) { + actions.push(...dataOps2BlockActions(yXmlText, keys)); + } + + if (isBlockEvent) { + actions.push(...blockOps2BlockActions(backupTarget, delta)); + } + + return actions; +} + +function textOps2BlockActions( + rootId: string, + yXmlText: Y.XmlText, + ops: YDelta +): ReturnType[] { + if (ops.length === 0) return []; + const blockYXmlText = yXmlText.parent as Y.XmlText; + const blockId = blockYXmlText.getAttribute('blockId'); + + if (blockId === rootId) { + return []; + } + + return generateApplyTextActions(yXmlText, ops); +} + +function dataOps2BlockActions( + yXmlText: Y.XmlText, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keys: Map +) { + const dataUpdated = keys.has('data'); + + if (!dataUpdated) return []; + const data = yXmlText.getAttribute('data'); + + return generateUpdateActions(yXmlText, { + data, + }); +} + +function blockOps2BlockActions( + blockYXmlText: Readonly, + ops: YDelta +): ReturnType[] { + const actions: ReturnType[] = []; + + let index = 0; + + ops.forEach((op) => { + if (op.insert) { + if (op.insert instanceof Y.XmlText) { + const insertYXmlText = op.insert; + const blockId = insertYXmlText.getAttribute('blockId'); + const textId = insertYXmlText.getAttribute('textId'); + + if (!blockId && !textId) { + throw new Error('blockId and textId is not exist'); + } + + if (blockId) { + actions.push(...generateInsertBlockActions(insertYXmlText)); + index += 1; + } + + if (textId) { + const target = getInsertTarget(blockYXmlText, [0]); + + if (target) { + const length = target.length; + + const delta = [{ delete: length }, ...insertYXmlText.toDelta()]; + + // restore textId + insertYXmlText.setAttribute('textId', target.getAttribute('textId')); + actions.push(...generateApplyTextActions(target, delta)); + } + } + } + } else if (op.retain) { + index += op.retain; + } else if (op.delete) { + let i = 0; + + for (; i < op.delete; i++) { + const target = getInsertTarget(blockYXmlText, [i + index]); + + if (target && target !== blockYXmlText) { + const deletedId = target.getAttribute('blockId') as string; + + if (deletedId) { + actions.push( + ...generateDeleteBlockActions({ + ids: [deletedId], + }) + ); + } else { + Log.error('blockOps2BlockActions', 'deletedId is not exist'); + } + } + } + + index += i; + } + }); + + return actions; +} + +export function generateUpdateActions( + yXmlText: Y.XmlText, + { + data, + }: { + data?: Record; + external_id?: string; + } +) { + const id = yXmlText.getAttribute('blockId'); + const parentId = yXmlText.getAttribute('parentId'); + + return [ + { + action: BlockActionTypePB.Update, + payload: { + block: { + id, + data: JSON.stringify(data), + }, + parent_id: parentId, + }, + }, + ]; +} + +export function generateApplyTextActions(yXmlText: Y.XmlText, delta: YDelta) { + const externalId = yXmlText.getAttribute('textId'); + + if (!externalId) return []; + + const deltaString = JSON.stringify(YDelta2Delta(delta)); + + return [ + { + action: BlockActionTypePB.ApplyTextDelta, + payload: { + text_id: externalId, + delta: deltaString, + }, + }, + ]; +} + +export function generateDeleteBlockActions({ ids }: { ids: string[] }) { + return ids.map((id) => ({ + action: BlockActionTypePB.Delete, + payload: { + block: { + id, + }, + parent_id: '', + }, + })); +} + +export function generateInsertTextActions(insertYXmlText: Y.XmlText) { + const textId = insertYXmlText.getAttribute('textId'); + const delta = YDelta2Delta(insertYXmlText.toDelta()); + + return [ + { + action: BlockActionTypePB.InsertText, + payload: { + text_id: textId, + delta: JSON.stringify(delta), + }, + }, + ]; +} + +export function generateInsertBlockActions( + insertYXmlText: Y.XmlText +): ReturnType[] { + const childrenId = generateId(); + + const [textInsert, ...childrenInserts] = (insertYXmlText.toDelta() as YDelta).map((op) => op.insert); + const textInsertActions = textInsert instanceof Y.XmlText ? generateInsertTextActions(textInsert) : []; + const externalId = textInsertActions[0]?.payload.text_id; + const prev = insertYXmlText.prevSibling; + const prevId = prev ? prev.getAttribute('blockId') : null; + const parentId = (insertYXmlText.parent as Y.XmlText).getAttribute('blockId'); + + const data = insertYXmlText.getAttribute('data'); + const type = insertYXmlText.getAttribute('type'); + const id = insertYXmlText.getAttribute('blockId'); + + if (!id) { + Log.error('generateInsertBlockActions', 'id is not exist'); + return []; + } + + if (!type || type === 'text' || Object.values(EditorNodeType).indexOf(type) === -1) { + Log.error('generateInsertBlockActions', 'type is error: ' + type); + return []; + } + + const actions: ReturnType[] = [ + ...textInsertActions, + { + action: BlockActionTypePB.Insert, + payload: { + block: { + id, + data: JSON.stringify(data), + ty: type, + parent_id: parentId, + children_id: childrenId, + external_id: externalId, + external_type: externalId ? 'text' : undefined, + }, + prev_id: prevId, + parent_id: parentId, + }, + }, + ]; + + childrenInserts.forEach((insert) => { + if (insert instanceof Y.XmlText) { + actions.push(...generateInsertBlockActions(insert)); + } + }); + + return actions; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts new file mode 100644 index 0000000000..b4da4b3ca7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/convert.ts @@ -0,0 +1,154 @@ +import { nanoid } from 'nanoid'; +import { EditorData, EditorInlineNodeType, Mention } from '$app/application/document/document.types'; +import { Element, Text } from 'slate'; +import { Op } from 'quill-delta'; + +export function generateId() { + return nanoid(10); +} + +export function transformToInlineElement(op: Op): Element[] { + const attributes = op.attributes; + + if (!attributes) return []; + const { formula, mention, ...attrs } = attributes; + + if (formula) { + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Formula, + data: formula, + children: [ + { + text, + ...attrs, + }, + ], + }; + }); + } + + if (mention) { + const texts = (op.insert as string).split(''); + + return texts.map((text) => { + return { + type: EditorInlineNodeType.Mention, + children: [ + { + text, + ...attrs, + }, + ], + data: { + ...(mention as Mention), + }, + }; + }); + } + + return []; +} + +export function getInlinesWithDelta(delta?: Op[]): (Text | Element)[] { + const newDelta: (Text | Element)[] = []; + + if (!delta || !delta.length) + return [ + { + text: '', + }, + ]; + + delta.forEach((op) => { + const matchInlines = transformToInlineElement(op); + + if (matchInlines.length > 0) { + newDelta.push(...matchInlines); + return; + } + + if (op.attributes) { + if ('font_color' in op.attributes && op.attributes['font_color'] === '') { + delete op.attributes['font_color']; + } + + if ('bg_color' in op.attributes && op.attributes['bg_color'] === '') { + delete op.attributes['bg_color']; + } + + if ('code' in op.attributes && !op.attributes['code']) { + delete op.attributes['code']; + } + } + + newDelta.push({ + text: op.insert as string, + ...op.attributes, + }); + }); + + return newDelta; +} + +export function convertToSlateValue(data: EditorData, includeRoot: boolean): Element[] { + const traverse = (id: string, isRoot = false) => { + const node = data.nodeMap[id]; + const delta = data.deltaMap[id]; + + const slateNode: Element = { + type: node.type, + data: node.data, + children: [], + blockId: id, + }; + + const textNode: Element | null = + !isRoot && node.externalId + ? { + type: 'text', + children: [], + textId: node.externalId, + } + : null; + + const inlineNodes = getInlinesWithDelta(delta); + + textNode?.children.push(...inlineNodes); + + const children = data.childrenMap[id]; + + slateNode.children = children.map((childId) => traverse(childId)); + if (textNode) { + slateNode.children.unshift(textNode); + } + + return slateNode; + }; + + const rootId = data.rootId; + + const root = traverse(rootId, true); + + const nodes = root.children as Element[]; + + if (includeRoot) { + nodes.unshift({ + ...root, + children: [ + { + type: 'text', + children: [ + { + text: '', + }, + ], + }, + ], + }); + } + + return nodes; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/delta.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/delta.ts new file mode 100644 index 0000000000..630b6fbdf5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/delta.ts @@ -0,0 +1,54 @@ +import { YDelta, YOp } from '$app/components/editor/provider/types/y_event'; +import { Op } from 'quill-delta'; +import * as Y from 'yjs'; +import { inlineNodeTypes } from '$app/application/document/document.types'; +import { DocEventPB } from '@/services/backend'; + +export function YDelta2Delta(yDelta: YDelta): Op[] { + const ops: Op[] = []; + + yDelta.forEach((op) => { + if (op.insert instanceof Y.XmlText) { + const type = op.insert.getAttribute('type'); + + if (inlineNodeTypes.includes(type)) { + ops.push(...YInlineOp2Op(op)); + return; + } + } + + ops.push(op as Op); + }); + return ops; +} + +export function YInlineOp2Op(yOp: YOp): Op[] { + if (!(yOp.insert instanceof Y.XmlText)) { + return [ + { + insert: yOp.insert as string, + attributes: yOp.attributes, + }, + ]; + } + + const type = yOp.insert.getAttribute('type'); + const data = yOp.insert.getAttribute('data'); + + const delta = yOp.insert.toDelta() as Op[]; + + return delta.map((op) => ({ + insert: op.insert, + + attributes: { + [type]: data, + ...op.attributes, + }, + })); +} + +export function DocEvent2YDelta(events: DocEventPB): YDelta { + if (!events.is_remote) return []; + + return []; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/relation.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/relation.ts new file mode 100644 index 0000000000..72b2e126df --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/relation.ts @@ -0,0 +1,24 @@ +import * as Y from 'yjs'; + +export function getInsertTarget(root: Y.XmlText, path: (string | number)[]): Y.XmlText { + const delta = root.toDelta(); + const index = path[0]; + + const current = delta[index]; + + if (current && current.insert instanceof Y.XmlText) { + if (path.length === 1) { + return current.insert; + } + + return getInsertTarget(current.insert, path.slice(1)); + } + + return root; +} + +export function getYTarget(doc: Y.Doc, path: (string | number)[]) { + const sharedType = doc.get('sharedType', Y.XmlText) as Y.XmlText; + + return getInsertTarget(sharedType, path); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/block.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/block.ts new file mode 100644 index 0000000000..00992964fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/block.ts @@ -0,0 +1,70 @@ +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { proxy, useSnapshot } from 'valtio'; +import { EditorNodeType } from '$app/application/document/document.types'; + +export interface EditorBlockState { + [EditorNodeType.ImageBlock]: { + popoverOpen: boolean; + blockId?: string; + }; + [EditorNodeType.EquationBlock]: { + popoverOpen: boolean; + blockId?: string; + }; +} + +const initialState = { + [EditorNodeType.ImageBlock]: { + popoverOpen: false, + blockId: undefined, + }, + [EditorNodeType.EquationBlock]: { + popoverOpen: false, + blockId: undefined, + }, +}; + +export const EditorBlockStateContext = createContext(initialState); + +export const EditorBlockStateProvider = EditorBlockStateContext.Provider; + +export function useEditorInitialBlockState() { + const state = useMemo(() => { + return proxy({ + ...initialState, + }); + }, []); + + return state; +} + +export function useEditorBlockState(key: EditorNodeType.ImageBlock | EditorNodeType.EquationBlock) { + const context = useContext(EditorBlockStateContext); + + return useSnapshot(context[key]); +} + +export function useEditorBlockDispatch() { + const context = useContext(EditorBlockStateContext); + + const openPopover = useCallback( + (key: EditorNodeType.ImageBlock | EditorNodeType.EquationBlock, blockId: string) => { + context[key].popoverOpen = true; + context[key].blockId = blockId; + }, + [context] + ); + + const closePopover = useCallback( + (key: EditorNodeType.ImageBlock | EditorNodeType.EquationBlock) => { + context[key].popoverOpen = false; + context[key].blockId = undefined; + }, + [context] + ); + + return { + openPopover, + closePopover, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/decorate.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/decorate.ts new file mode 100644 index 0000000000..078296aade --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/decorate.ts @@ -0,0 +1,89 @@ +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { BaseRange, Editor, NodeEntry, Range } from 'slate'; +import { proxySet } from 'valtio/utils'; +import { useSnapshot } from 'valtio'; +import { ReactEditor } from 'slate-react'; + +export const DecorateStateContext = createContext< + Set<{ + range: BaseRange; + class_name: string; + type?: 'link'; + }> +>(new Set()); +export const DecorateStateProvider = DecorateStateContext.Provider; + +export function useInitialDecorateState(editor: ReactEditor) { + const decorateState = useMemo( + () => + proxySet<{ + range: BaseRange; + class_name: string; + }>([]), + [] + ); + + const ranges = useSnapshot(decorateState); + + const decorate = useCallback( + ([, path]: NodeEntry): BaseRange[] => { + const highlightRanges: (Range & { + class_name: string; + })[] = []; + + ranges.forEach((state) => { + const intersection = Range.intersection(state.range, Editor.range(editor, path)); + + if (intersection) { + highlightRanges.push({ + ...intersection, + class_name: state.class_name, + }); + } + }); + + return highlightRanges; + }, + [editor, ranges] + ); + + return { + decorate, + decorateState, + }; +} + +export function useDecorateState(type?: 'link') { + const context = useContext(DecorateStateContext); + + const state = useSnapshot(context); + + return useMemo(() => { + return Array.from(state).find((s) => !type || s.type === type); + }, [state, type]); +} + +export function useDecorateDispatch() { + const context = useContext(DecorateStateContext); + + const getStaticState = useCallback(() => { + return Array.from(context)[0]; + }, [context]); + + const add = useCallback( + (state: { range: BaseRange; class_name: string; type?: 'link' }) => { + context.add(state); + }, + [context] + ); + + const clear = useCallback(() => { + context.clear(); + }, [context]); + + return { + add, + clear, + getStaticState, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/index.ts new file mode 100644 index 0000000000..22f0bb81be --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/index.ts @@ -0,0 +1,28 @@ +import { ReactEditor } from 'slate-react'; +import { useInitialDecorateState } from '$app/components/editor/stores/decorate'; +import { useInitialSelectedBlocks } from '$app/components/editor/stores/selected'; +import { useInitialSlashState } from '$app/components/editor/stores/slash'; +import { useInitialEditorInlineBlockState } from '$app/components/editor/stores/inline_node'; +import { useEditorInitialBlockState } from '$app/components/editor/stores/block'; + +export * from './decorate'; +export * from './selected'; +export * from './slash'; +export * from './inline_node'; + +export function useInitialEditorState(editor: ReactEditor) { + const { decorate, decorateState } = useInitialDecorateState(editor); + const selectedBlocks = useInitialSelectedBlocks(editor); + const slashState = useInitialSlashState(); + const inlineBlockState = useInitialEditorInlineBlockState(); + const blockState = useEditorInitialBlockState(); + + return { + selectedBlocks, + decorate, + decorateState, + slashState, + inlineBlockState, + blockState, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts new file mode 100644 index 0000000000..6607a546d8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/inline_node.ts @@ -0,0 +1,60 @@ +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { BaseRange, Path } from 'slate'; +import { proxy, useSnapshot } from 'valtio'; + +export interface EditorInlineBlockState { + formula: { + popoverOpen: boolean; + range?: BaseRange; + }; +} +const initialState = { + formula: { + popoverOpen: false, + range: undefined, + }, +}; + +export const EditorInlineBlockStateContext = createContext(initialState); + +export const EditorInlineBlockStateProvider = EditorInlineBlockStateContext.Provider; + +export function useInitialEditorInlineBlockState() { + const state = useMemo(() => { + return proxy({ + ...initialState, + }); + }, []); + + return state; +} + +export function useEditorInlineBlockState(key: 'formula') { + const context = useContext(EditorInlineBlockStateContext); + + const state = useSnapshot(context[key]); + + const openPopover = useCallback(() => { + context[key].popoverOpen = true; + }, [context, key]); + + const closePopover = useCallback(() => { + context[key].popoverOpen = false; + }, [context, key]); + + const setRange = useCallback( + (at: BaseRange | Path) => { + const range = Path.isPath(at) ? { anchor: at, focus: at } : at; + + context[key].range = range as BaseRange; + }, + [context, key] + ); + + return { + ...state, + openPopover, + closePopover, + setRange, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/selected.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/selected.ts new file mode 100644 index 0000000000..803f474723 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/selected.ts @@ -0,0 +1,58 @@ +import { createContext, useEffect, useMemo, useState } from 'react'; +import { proxySet, subscribeKey } from 'valtio/utils'; +import { ReactEditor } from 'slate-react'; +import { Element } from 'slate'; + +export function useInitialSelectedBlocks(editor: ReactEditor) { + const selectedBlocks = useMemo(() => proxySet([]), []); + const [selectedLength, setSelectedLength] = useState(0); + + subscribeKey(selectedBlocks, 'size', (v) => setSelectedLength(v)); + + useEffect(() => { + const { onChange } = editor; + + const onKeydown = (e: KeyboardEvent) => { + if (!ReactEditor.isFocused(editor) && selectedLength > 0) { + e.preventDefault(); + e.stopPropagation(); + const selectedBlockId = selectedBlocks.values().next().value; + const [selectedBlock] = editor.nodes({ + at: [], + match: (n) => Element.isElement(n) && n.blockId === selectedBlockId, + }); + const [, path] = selectedBlock; + + editor.select(path); + ReactEditor.focus(editor); + } + }; + + if (selectedLength > 0) { + editor.onChange = (...args) => { + const isSelectionChange = editor.operations.every((arg) => arg.type === 'set_selection'); + + if (isSelectionChange) { + selectedBlocks.clear(); + } + + onChange(...args); + }; + + document.addEventListener('keydown', onKeydown); + } else { + editor.onChange = onChange; + document.removeEventListener('keydown', onKeydown); + } + + return () => { + editor.onChange = onChange; + document.removeEventListener('keydown', onKeydown); + }; + }, [editor, selectedBlocks, selectedLength]); + + return selectedBlocks; +} + +export const EditorSelectedBlockContext = createContext>(new Set()); +export const EditorSelectedBlockProvider = EditorSelectedBlockContext.Provider; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/slash.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/slash.ts new file mode 100644 index 0000000000..13e9447d0c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/stores/slash.ts @@ -0,0 +1,30 @@ +import { createContext, useCallback, useContext, useMemo } from 'react'; +import { proxyMap } from 'valtio/utils'; +import { useSnapshot } from 'valtio'; + +export const SlashStateContext = createContext>(new Map()); +export const SlashStateProvider = SlashStateContext.Provider; + +export function useInitialSlashState() { + const state = useMemo(() => proxyMap([['open', false]]), []); + + return state; +} + +export function useSlashState() { + const context = useContext(SlashStateContext); + const state = useSnapshot(context); + const open = state.get('open'); + + const setOpen = useCallback( + (open: boolean) => { + context.set('open', open); + }, + [context] + ); + + return { + open, + setOpen, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts new file mode 100644 index 0000000000..ceaa5a51a0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/Error.hooks.ts @@ -0,0 +1,39 @@ +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { errorActions } from '$app_reducers/error/slice'; +import { useCallback, useEffect, useState } from 'react'; + +export const useError = (e: Error) => { + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.error); + const [errorMessage, setErrorMessage] = useState(''); + const [displayError, setDisplayError] = useState(false); + + useEffect(() => { + setDisplayError(error.display); + setErrorMessage(error.message); + }, [error]); + + const showError = useCallback( + (msg: string) => { + dispatch(errorActions.showError(msg)); + }, + [dispatch] + ); + + useEffect(() => { + if (e) { + showError(e.message); + } + }, [e, showError]); + + const hideError = () => { + dispatch(errorActions.hideError()); + }; + + return { + showError, + hideError, + errorMessage, + displayError, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx new file mode 100644 index 0000000000..1bb15f2ca3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorHandlerPage.tsx @@ -0,0 +1,8 @@ +import { useError } from './Error.hooks'; +import { ErrorModal } from './ErrorModal'; + +export const ErrorHandlerPage = ({ error }: { error: Error }) => { + const { hideError, errorMessage, displayError } = useError(error); + + return displayError ? : <>; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx new file mode 100644 index 0000000000..6da2ee96d0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx @@ -0,0 +1,26 @@ +import { ReactComponent as InformationSvg } from '$app/assets/information.svg'; +import { ReactComponent as CloseSvg } from '$app/assets/close.svg'; + +export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { + return ( +
+
+ +
+ +
+

Oops.. something went wrong

+

{message}

+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/index.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/index.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx new file mode 100644 index 0000000000..4f468f5461 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx @@ -0,0 +1,12 @@ +export const FooterPanel = () => { + return ( +
+
+ © 2024 AppFlowy. GitHub +
+ {/*
*/} + {/* */} + {/*
*/} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts new file mode 100644 index 0000000000..807c1e6811 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts @@ -0,0 +1,54 @@ +import { useCallback } from 'react'; +import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions, ThemeMode } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; +import { sidebarActions } from '$app_reducers/sidebar/slice'; + +export function useShortcuts() { + const dispatch = useAppDispatch(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const { isDark } = userSettingState; + + const switchThemeMode = useCallback(() => { + const newSetting = { + themeMode: isDark ? ThemeMode.Light : ThemeMode.Dark, + isDark: !isDark, + }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + void UserService.setAppearanceSetting({ + theme_mode: newSetting.themeMode, + }); + }, [dispatch, isDark]); + + const toggleSidebar = useCallback(() => { + dispatch(sidebarActions.toggleCollapse()); + }, [dispatch]); + + return useCallback( + (e: KeyboardEvent) => { + switch (true) { + /** + * Toggle theme: Mod+L + * Switch between light and dark theme + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_THEME)(e): + switchThemeMode(); + break; + /** + * Toggle sidebar: Mod+. (period) + * Prevent the default behavior of the browser (Exit full screen) + * Collapse or expand the sidebar + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_SIDEBAR)(e): + e.preventDefault(); + toggleSidebar(); + break; + default: + break; + } + }, + [toggleSidebar, switchThemeMode] + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx new file mode 100644 index 0000000000..509aa388cf --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx @@ -0,0 +1,66 @@ +import React, { ReactNode, useEffect, useMemo } from 'react'; +import SideBar from '$app/components/layout/side_bar/SideBar'; +import TopBar from '$app/components/layout/top_bar/TopBar'; +import { useAppSelector } from '$app/stores/store'; +import './layout.scss'; +import { AFScroller } from '../_shared/scroller'; +import { useNavigate } from 'react-router-dom'; +import { pageTypeMap } from '$app_reducers/pages/slice'; +import { useShortcuts } from '$app/components/layout/Layout.hooks'; + +function Layout({ children }: { children: ReactNode }) { + const { isCollapsed, width } = useAppSelector((state) => state.sidebar); + const currentUser = useAppSelector((state) => state.currentUser); + const navigate = useNavigate(); + const { id: latestOpenViewId, layout } = useMemo( + () => + currentUser?.workspaceSetting?.latestView || { + id: undefined, + layout: undefined, + }, + [currentUser?.workspaceSetting?.latestView] + ); + + const onKeyDown = useShortcuts(); + + useEffect(() => { + window.addEventListener('keydown', onKeyDown); + return () => { + window.removeEventListener('keydown', onKeyDown); + }; + }, [onKeyDown]); + + useEffect(() => { + if (latestOpenViewId) { + const pageType = pageTypeMap[layout]; + + navigate(`/page/${pageType}/${latestOpenViewId}`); + } + }, [latestOpenViewId, navigate, layout]); + return ( + <> +
+ +
+ + + {children} + +
+
+ + ); +} + +export default Layout; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx new file mode 100644 index 0000000000..ec9e990cdb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx @@ -0,0 +1,64 @@ +import React, { useCallback } from 'react'; +import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcrumb.hooks'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; +import { Page } from '$app_reducers/pages/slice'; +import { useTranslation } from 'react-i18next'; +import { getPageIcon } from '$app/hooks/page.hooks'; +import { useAppDispatch } from '$app/stores/store'; +import { openPage } from '$app_reducers/pages/async_actions'; + +function Breadcrumb() { + const { t } = useTranslation(); + const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); + const dispatch = useAppDispatch(); + + const navigateToPage = useCallback( + (page: Page) => { + void dispatch(openPage(page.id)); + }, + [dispatch] + ); + + if (!currentPage) { + if (isTrash) { + return {t('trash.text')}; + } + + return null; + } + + return ( + + {pagePath?.map((page: Page, index) => { + if (index === pagePath.length - 1) { + return ( +
+
{getPageIcon(page)}
+ {page.name.trim() || t('menuAppHeader.defaultNewPageName')} +
+ ); + } + + return ( + { + navigateToPage(page); + }} + > +
{getPageIcon(page)}
+ + {page.name.trim() || t('menuAppHeader.defaultNewPageName')} + + ); + })} +
+ ); +} + +export default Breadcrumb; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts new file mode 100644 index 0000000000..f2bec915d9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts @@ -0,0 +1,38 @@ +import { useAppSelector } from '$app/stores/store'; +import { useMemo } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { Page } from '$app_reducers/pages/slice'; + +export function useLoadExpandedPages() { + const params = useParams(); + const location = useLocation(); + const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]); + const currentPageId = params.id; + const currentPage = useAppSelector((state) => (currentPageId ? state.pages.pageMap[currentPageId] : undefined)); + + const pagePath = useAppSelector((state) => { + const result: Page[] = []; + + if (!currentPage) return result; + + const findParent = (page: Page) => { + if (!page.parentId) return; + const parent = state.pages.pageMap[page.parentId]; + + if (parent) { + result.unshift(parent); + findParent(parent); + } + }; + + findParent(currentPage); + result.push(currentPage); + return result; + }); + + return { + pagePath, + currentPage, + isTrash, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx new file mode 100644 index 0000000000..87662a99bb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx @@ -0,0 +1,37 @@ +import React, { useCallback, useMemo } from 'react'; +import { IconButton, Tooltip } from '@mui/material'; + +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { sidebarActions } from '$app_reducers/sidebar/slice'; +import { ReactComponent as ShowMenuIcon } from '$app/assets/show-menu.svg'; +import { useTranslation } from 'react-i18next'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; + +function CollapseMenuButton() { + const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); + const dispatch = useAppDispatch(); + const handleClick = useCallback(() => { + dispatch(sidebarActions.toggleCollapse()); + }, [dispatch]); + + const { t } = useTranslation(); + + const title = useMemo(() => { + return ( +
+
{isCollapsed ? t('sideBar.openSidebar') : t('sideBar.closeSidebar')}
+
{createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)}
+
+ ); + }, [isCollapsed, t]); + + return ( + + + + + + ); +} + +export default CollapseMenuButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss new file mode 100644 index 0000000000..43f4f55892 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -0,0 +1,81 @@ + + +.sketch-picker { + background-color: var(--bg-body) !important; + border-color: transparent !important; + box-shadow: none !important; +} +.sketch-picker .flexbox-fix { + border-color: var(--line-divider) !important; +} +.sketch-picker [id^='rc-editable-input'] { + background-color: var(--bg-body) !important; + border-color: var(--line-divider) !important; + color: var(--text-title) !important; + box-shadow: var(--line-border) 0px 0px 0px 1px inset !important; +} + +.appflowy-date-picker-calendar { + width: 100%; + +} + +.grid-sticky-header::-webkit-scrollbar { + width: 0; + height: 0; +} +.grid-scroll-container::-webkit-scrollbar { + width: 0; + height: 0; +} + + +.appflowy-scroll-container { + &::-webkit-scrollbar { + width: 0; + } +} + +.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical { + background-color: var(--scrollbar-thumb); + border-radius: 4px; + opacity: 60%; +} + +.workspaces { + ::-webkit-scrollbar { + width: 0px; + } +} + + + +.MuiPopover-root, .MuiPaper-root { + ::-webkit-scrollbar { + width: 0; + height: 0; + } +} + +.view-icon { + &:hover { + background-color: rgba(156, 156, 156, 0.20); + } +} + +.theme-mode-item { + @apply relative flex h-[72px] w-[88px] cursor-pointer items-end justify-end rounded border hover:shadow; + background: linear-gradient(150.74deg, rgba(231, 231, 231, 0) 17.95%, #C5C5C5 95.51%); +} + +[data-dark-mode="true"] { + .theme-mode-item { + background: linear-gradient(150.74deg, rgba(128, 125, 125, 0) 17.95%, #4d4d4d 95.51%); + } +} + +.document-header { + .view-banner { + @apply items-center; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx new file mode 100644 index 0000000000..1387f16f4d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx @@ -0,0 +1,65 @@ +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; +import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; +import { ViewLayoutPB } from '@/services/backend'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; + +function AddButton({ + isHovering, + setHovering, + onAddPage, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + onAddPage: (layout: ViewLayoutPB) => void; +}) { + const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'document': + onAddPage(ViewLayoutPB.Document); + break; + case 'grid': + onAddPage(ViewLayoutPB.Grid); + break; + default: + break; + } + }, + [onAddPage] + ); + + const options = useMemo( + () => [ + { + key: 'document', + title: t('document.menuName'), + icon: , + }, + { + key: 'grid', + title: t('grid.menuName'), + icon: , + }, + ], + [t] + ); + + return ( + + + + ); +} + +export default AddButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx new file mode 100644 index 0000000000..4af8a2f2f1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ViewLayoutPB } from '@/services/backend'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; + +function DeleteDialog({ + layout, + open, + onClose, + onOk, +}: { + layout: ViewLayoutPB; + open: boolean; + onClose: () => void; + onOk: () => Promise; +}) { + const { t } = useTranslation(); + + const pageType = { + [ViewLayoutPB.Document]: t('document.menuName'), + [ViewLayoutPB.Grid]: t('grid.menuName'), + [ViewLayoutPB.Board]: t('board.menuName'), + [ViewLayoutPB.Calendar]: t('calendar.menuName'), + }[layout]; + + return ( + + ); +} + +export default DeleteDialog; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx new file mode 100644 index 0000000000..94a86655ac --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx @@ -0,0 +1,130 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; +import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; +import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; +import { ReactComponent as TrashSvg } from '$app/assets/delete.svg'; + +import RenameDialog from '../../_shared/confirm_dialog/RenameDialog'; +import { Page } from '$app_reducers/pages/slice'; +import DeleteDialog from '$app/components/layout/nested_page/DeleteDialog'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; +import { getModifier } from '$app/utils/hotkeys'; +import isHotkey from 'is-hotkey'; + +function MoreButton({ + onDelete, + onDuplicate, + onRename, + page, + isHovering, + setHovering, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + onDelete: () => Promise; + onDuplicate: () => Promise; + onRename: (newName: string) => Promise; + page: Page; +}) { + const [renameDialogOpen, setRenameDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'rename': + setRenameDialogOpen(true); + break; + case 'delete': + setDeleteDialogOpen(true); + break; + case 'duplicate': + void onDuplicate(); + + break; + default: + break; + } + }, + [onDuplicate] + ); + + const options = useMemo( + () => [ + { + title: t('button.rename'), + icon: , + key: 'rename', + }, + { + key: 'delete', + title: t('button.delete'), + icon: , + caption: 'Del', + }, + { + key: 'duplicate', + title: t('button.duplicate'), + icon: , + caption: `${getModifier()}+D`, + }, + ], + [t] + ); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (isHotkey('del', e) || isHotkey('backspace', e)) { + e.preventDefault(); + e.stopPropagation(); + onConfirm('delete'); + return; + } + + if (isHotkey('mod+d', e)) { + e.stopPropagation(); + onConfirm('duplicate'); + return; + } + }, + [onConfirm] + ); + + return ( + <> + + + + + { + setDeleteDialogOpen(false); + }} + onOk={onDelete} + /> + {renameDialogOpen && ( + setRenameDialogOpen(false)} + onOk={onRename} + /> + )} + + ); +} + +export default MoreButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts new file mode 100644 index 0000000000..d43499e801 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts @@ -0,0 +1,147 @@ +import { useCallback, useEffect } from 'react'; +import { pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { FolderNotification, ViewLayoutPB } from '@/services/backend'; +import { useParams } from 'react-router-dom'; +import { openPage, updatePageName } from '$app_reducers/pages/async_actions'; +import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service'; +import { subscribeNotifications } from '$app/application/notification'; + +export function useLoadChildPages(pageId: string) { + const dispatch = useAppDispatch(); + const childPages = useAppSelector((state) => state.pages.relationMap[pageId]); + const collapsed = useAppSelector((state) => !state.pages.expandedIdMap[pageId]); + const toggleCollapsed = useCallback(() => { + if (collapsed) { + dispatch(pagesActions.expandPage(pageId)); + } else { + dispatch(pagesActions.collapsePage(pageId)); + } + }, [dispatch, pageId, collapsed]); + + const loadPageChildren = useCallback( + async (pageId: string) => { + const childPages = await getChildPages(pageId); + + dispatch( + pagesActions.addChildPages({ + id: pageId, + childPages, + }) + ); + }, + [dispatch] + ); + + useEffect(() => { + void loadPageChildren(pageId); + }, [loadPageChildren, pageId]); + + useEffect(() => { + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateView]: async (payload) => { + const childViews = payload.child_views; + + if (childViews.length === 0) { + return; + } + + dispatch( + pagesActions.addChildPages({ + id: pageId, + childPages: childViews.map(parserViewPBToPage), + }) + ); + }, + [FolderNotification.DidUpdateChildViews]: async (payload) => { + if (payload.delete_child_views.length === 0 && payload.create_child_views.length === 0) { + return; + } + + void loadPageChildren(pageId); + }, + }, + { + id: pageId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [pageId, loadPageChildren, dispatch]); + + return { + toggleCollapsed, + collapsed, + childPages, + }; +} + +export function usePageActions(pageId: string) { + const page = useAppSelector((state) => state.pages.pageMap[pageId]); + const dispatch = useAppDispatch(); + const params = useParams(); + const currentPageId = params.id; + + const onPageClick = useCallback(() => { + void dispatch(openPage(pageId)); + }, [dispatch, pageId]); + + const onAddPage = useCallback( + async (layout: ViewLayoutPB) => { + const newViewId = await createPage({ + layout, + name: '', + parent_view_id: pageId, + }); + + dispatch( + pagesActions.addPage({ + page: { + id: newViewId, + parentId: pageId, + layout, + name: '', + }, + isLast: true, + }) + ); + + dispatch(pagesActions.expandPage(pageId)); + await dispatch(openPage(newViewId)); + }, + [dispatch, pageId] + ); + + const onDeletePage = useCallback(async () => { + if (currentPageId === pageId) { + dispatch(pagesActions.setTrashSnackbar(true)); + } + + await deletePage(pageId); + dispatch(pagesActions.deletePages([pageId])); + }, [dispatch, pageId, currentPageId]); + + const onDuplicatePage = useCallback(async () => { + await duplicatePage(page); + }, [page]); + + const onRenamePage = useCallback( + async (name: string) => { + await dispatch(updatePageName({ id: pageId, name })); + }, + [dispatch, pageId] + ); + + return { + onAddPage, + onPageClick, + onRenamePage, + onDeletePage, + onDuplicatePage, + }; +} + +export function useSelectedPage(pageId: string) { + return useParams().id === pageId; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx new file mode 100644 index 0000000000..e423f05517 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useMemo } from 'react'; +import Collapse from '@mui/material/Collapse'; +import { TransitionGroup } from 'react-transition-group'; +import NestedPageTitle from '$app/components/layout/nested_page/NestedPageTitle'; +import { useLoadChildPages, usePageActions } from '$app/components/layout/nested_page/NestedPage.hooks'; +import { useDrag } from 'src/appflowy_app/components/_shared/drag_block'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { movePageThunk } from '$app_reducers/pages/async_actions'; +import { ViewLayoutPB } from '@/services/backend'; + +function NestedPage({ pageId }: { pageId: string }) { + const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId); + const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId); + const dispatch = useAppDispatch(); + const { page, parentLayout } = useAppSelector((state) => { + const page = state.pages.pageMap[pageId]; + const parent = state.pages.pageMap[page?.parentId || '']; + + return { + page, + parentLayout: parent?.layout, + }; + }); + + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); + const children = useMemo(() => { + if (disableChildren) { + return []; + } + + return collapsed ? [] : childPages; + }, [collapsed, childPages, disableChildren]); + + const onDragFinished = useCallback( + (result: { dragId: string; position: 'before' | 'after' | 'inside' }) => { + const { dragId, position } = result; + + if (dragId === pageId) return; + if (position === 'inside' && page?.layout !== ViewLayoutPB.Document) return; + void dispatch( + movePageThunk({ + sourceId: dragId, + targetId: pageId, + insertType: position, + }) + ); + }, + [dispatch, page?.layout, pageId] + ); + + const { onDrop, dropPosition, onDragOver, onDragLeave, onDragStart, onDragEnd, isDraggingOver, isDragging } = useDrag({ + onEnd: onDragFinished, + dragId: pageId, + }); + + const className = useMemo(() => { + const defaultClassName = 'relative flex-1 select-none flex flex-col w-full'; + + if (isDragging) { + return `${defaultClassName} opacity-40`; + } + + if (isDraggingOver && dropPosition === 'inside' && page?.layout === ViewLayoutPB.Document) { + if (dropPosition === 'inside') { + return `${defaultClassName} bg-content-blue-100`; + } + } else { + return defaultClassName; + } + }, [dropPosition, isDragging, isDraggingOver, page?.layout]); + + // Only allow dragging if the parent layout is undefined or a document + const draggable = parentLayout === undefined || parentLayout === ViewLayoutPB.Document; + + return ( +
+
+ { + onPageClick(); + }} + onAddPage={onAddPage} + onDuplicate={onDuplicatePage} + onDelete={onDeletePage} + onRename={onRenamePage} + collapsed={collapsed} + toggleCollapsed={toggleCollapsed} + pageId={pageId} + /> +
+ + {children?.map((pageId) => ( + + + + ))} + +
+
+ ); +} + +export default React.memo(NestedPage); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx new file mode 100644 index 0000000000..948aedcae2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx @@ -0,0 +1,102 @@ +import React, { useMemo, useState } from 'react'; +import { useAppSelector } from '$app/stores/store'; +import AddButton from './AddButton'; +import MoreButton from './MoreButton'; +import { ViewLayoutPB } from '@/services/backend'; +import { useSelectedPage } from '$app/components/layout/nested_page/NestedPage.hooks'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as MoreIcon } from '$app/assets/more.svg'; +import { IconButton } from '@mui/material'; +import { Page } from '$app_reducers/pages/slice'; +import { getPageIcon } from '$app/hooks/page.hooks'; + +function NestedPageTitle({ + pageId, + collapsed, + toggleCollapsed, + onAddPage, + onClick, + onDelete, + onDuplicate, + onRename, +}: { + pageId: string; + collapsed: boolean; + toggleCollapsed: () => void; + onAddPage: (layout: ViewLayoutPB) => void; + onClick: () => void; + onDelete: () => Promise; + onDuplicate: () => Promise; + onRename: (newName: string) => Promise; +}) { + const { t } = useTranslation(); + const page = useAppSelector((state) => { + return state.pages.pageMap[pageId] as Page | undefined; + }); + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); + + const [isHovering, setIsHovering] = useState(false); + const isSelected = useSelectedPage(pageId); + + const pageIcon = useMemo(() => (page ? getPageIcon(page) : null), [page]); + + return ( +
setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > +
+
+ {disableChildren ? ( +
+ ) : ( + { + e.stopPropagation(); + toggleCollapsed(); + }} + style={{ + transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)', + }} + > + + + )} + + {pageIcon} + +
+ {page?.name.trim() || t('menuAppHeader.defaultNewPageName')} +
+
+
e.stopPropagation()} className={'min:w-14 flex items-center justify-end px-2'}> + {page?.layout === ViewLayoutPB.Document && ( + + )} + {page && ( + + )} +
+
+
+ ); +} + +export default NestedPageTitle; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx new file mode 100644 index 0000000000..cef1d3307c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx @@ -0,0 +1,103 @@ +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { IconButton } from '@mui/material'; +import Popover from '@mui/material/Popover'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import Tooltip from '@mui/material/Tooltip'; + +function OperationMenu({ + options, + onConfirm, + isHovering, + setHovering, + children, + tooltip, + onKeyDown, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + options: { + key: string; + title: string; + icon: React.ReactNode; + caption?: string; + }[]; + children: React.ReactNode; + onConfirm: (key: string) => void; + tooltip: string; + onKeyDown?: (e: KeyboardEvent) => void; +}) { + const [anchorEl, setAnchorEl] = useState(null); + const renderItem = useCallback((title: string, icon: ReactNode, caption?: string) => { + return ( +
+ {icon} +
{title}
+
{caption || ''}
+
+ ); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + setHovering(false); + }, [setHovering]); + + const optionList = useMemo(() => { + return options.map((option) => { + return { + key: option.key, + content: renderItem(option.title, option.icon, option.caption), + }; + }); + }, [options, renderItem]); + + const open = Boolean(anchorEl); + + const handleConfirm = useCallback( + (key: string) => { + onConfirm(key); + handleClose(); + }, + [handleClose, onConfirm] + ); + + return ( + <> + + { + setAnchorEl(e.currentTarget); + }} + className={`${!isHovering ? 'invisible' : ''} text-icon-primary`} + > + {children} + + + + + + + + ); +} + +export default OperationMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.hooks.ts new file mode 100644 index 0000000000..b281706848 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.hooks.ts @@ -0,0 +1,12 @@ +import { useParams } from 'react-router-dom'; + +export function useShareConfig() { + const params = useParams(); + const id = params.id; + + const showShareButton = !!id; + + return { + showShareButton, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.tsx new file mode 100644 index 0000000000..1a10cb08e6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/share/Share.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { useShareConfig } from '$app/components/layout/share/Share.hooks'; + +function ShareButton() { + const { showShareButton } = useShareConfig(); + const { t } = useTranslation(); + + if (!showShareButton) return null; + return ; +} + +export default ShareButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx new file mode 100644 index 0000000000..639d5283e0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx @@ -0,0 +1,55 @@ +import React, { useCallback, useRef } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { sidebarActions } from '$app_reducers/sidebar/slice'; + +const minSidebarWidth = 200; + +function Resizer() { + const dispatch = useAppDispatch(); + const width = useAppSelector((state) => state.sidebar.width); + const startX = useRef(0); + const onResize = useCallback( + (e: MouseEvent) => { + e.preventDefault(); + const diff = e.clientX - startX.current; + const newWidth = width + diff; + + if (newWidth < minSidebarWidth) { + return; + } + + dispatch(sidebarActions.changeWidth(newWidth)); + }, + [dispatch, width] + ); + + const onResizeEnd = useCallback(() => { + dispatch(sidebarActions.stopResizing()); + document.removeEventListener('mousemove', onResize); + document.removeEventListener('mouseup', onResizeEnd); + }, [onResize, dispatch]); + + const onResizeStart = useCallback( + (e: React.MouseEvent) => { + startX.current = e.clientX; + dispatch(sidebarActions.startResizing()); + document.addEventListener('mousemove', onResize); + document.addEventListener('mouseup', onResizeEnd); + }, + [onResize, onResizeEnd, dispatch] + ); + + return ( +
+
+
+ ); +} + +export default React.memo(Resizer); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx new file mode 100644 index 0000000000..5cdbfb125b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx @@ -0,0 +1,73 @@ +import React, { useEffect, useRef } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { ReactComponent as AppflowyLogoDark } from '$app/assets/dark-logo.svg'; +import { ReactComponent as AppflowyLogoLight } from '$app/assets/light-logo.svg'; +import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; +import Resizer from '$app/components/layout/side_bar/Resizer'; +import UserInfo from '$app/components/layout/side_bar/UserInfo'; +import WorkspaceManager from '$app/components/layout/workspace_manager/WorkspaceManager'; +import { ThemeMode } from '$app_reducers/current-user/slice'; +import { sidebarActions } from '$app_reducers/sidebar/slice'; + +function SideBar() { + const { isCollapsed, width, isResizing } = useAppSelector((state) => state.sidebar); + const dispatch = useAppDispatch(); + + const themeMode = useAppSelector((state) => state.currentUser?.userSetting?.themeMode); + const isDark = + themeMode === ThemeMode.Dark || + (themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches); + + const lastCollapsedRef = useRef(isCollapsed); + + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + + if (width <= 800 && !isCollapsed) { + lastCollapsedRef.current = false; + dispatch(sidebarActions.setCollapse(true)); + } else if (width > 800 && !lastCollapsedRef.current) { + lastCollapsedRef.current = true; + dispatch(sidebarActions.setCollapse(false)); + } + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, [dispatch, isCollapsed]); + return ( + <> +
+
+
+ {isDark ? ( + + ) : ( + + )} + +
+
+ +
+
+ +
+
+
+ + + ); +} + +export default SideBar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx new file mode 100644 index 0000000000..62763c670e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import { useAppSelector } from '$app/stores/store'; +import { IconButton } from '@mui/material'; +import { ReactComponent as SettingIcon } from '$app/assets/settings.svg'; +import Tooltip from '@mui/material/Tooltip'; +import { useTranslation } from 'react-i18next'; +import { SettingsDialog } from '$app/components/settings/SettingsDialog'; +import { ProfileAvatar } from '$app/components/_shared/avatar'; + +function UserInfo() { + const currentUser = useAppSelector((state) => state.currentUser); + const [showUserSetting, setShowUserSetting] = useState(false); + + const { t } = useTranslation(); + + return ( + <> +
+
+ + {currentUser.displayName} +
+ + + { + setShowUserSetting(!showUserSetting); + }} + > + + + +
+ + {showUserSetting && setShowUserSetting(false)} />} + + ); +} + +export default UserInfo; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx new file mode 100644 index 0000000000..f5638362b9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx @@ -0,0 +1,104 @@ +import React, { useEffect } from 'react'; +import { Alert, Snackbar } from '@mui/material'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { useParams } from 'react-router-dom'; +import { pagesActions } from '$app_reducers/pages/slice'; +import Slide, { SlideProps } from '@mui/material/Slide'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { useTrashActions } from '$app/components/trash/Trash.hooks'; +import { openPage } from '$app_reducers/pages/async_actions'; + +function SlideTransition(props: SlideProps) { + return ; +} + +function DeletePageSnackbar() { + const firstViewId = useAppSelector((state) => { + const workspaceId = state.workspace.currentWorkspaceId; + const children = workspaceId ? state.pages.relationMap[workspaceId] : undefined; + + if (!children) return null; + + return children[0]; + }); + + const showTrashSnackbar = useAppSelector((state) => state.pages.showTrashSnackbar); + const dispatch = useAppDispatch(); + const { onPutback, onDelete } = useTrashActions(); + const { id } = useParams(); + + const { t } = useTranslation(); + + useEffect(() => { + dispatch(pagesActions.setTrashSnackbar(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleBack = () => { + if (firstViewId) { + void dispatch(openPage(firstViewId)); + } + }; + + const handleClose = (toBack = true) => { + dispatch(pagesActions.setTrashSnackbar(false)); + if (toBack) { + handleBack(); + } + }; + + const handleRestore = () => { + if (!id) return; + void onPutback(id); + handleClose(false); + }; + + const handleDelete = () => { + if (!id) return; + void onDelete([id]); + + if (!firstViewId) { + handleClose(false); + return; + } + + handleBack(); + }; + + return ( + + handleClose()} + severity='info' + variant='standard' + sx={{ + width: '100%', + '.MuiAlert-action': { + padding: 0, + }, + }} + > +
+ {t('deletePagePrompt.text')} + + +
+
+
+ ); +} + +export default DeletePageSnackbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/FontSizeConfig.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/FontSizeConfig.tsx new file mode 100644 index 0000000000..4b439cc3fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/FontSizeConfig.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ButtonGroup, Divider } from '@mui/material'; +import Button from '@mui/material/Button'; + +function FontSizeConfig() { + const { t } = useTranslation(); + + return ( + <> +
+
{t('moreAction.fontSize')}
+
+ + + + + +
+
+ + + ); +} + +export default FontSizeConfig; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx new file mode 100644 index 0000000000..d37d1bf060 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx @@ -0,0 +1,32 @@ +import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Drawer, IconButton } from '@mui/material'; +import { ReactComponent as Details2Svg } from '$app/assets/details.svg'; +import Tooltip from '@mui/material/Tooltip'; +import MoreOptions from '$app/components/layout/top_bar/MoreOptions'; +import { useMoreOptionsConfig } from '$app/components/layout/top_bar/MoreOptions.hooks'; + +function MoreButton() { + const { t } = useTranslation(); + const [open, setOpen] = React.useState(false); + const toggleDrawer = useCallback((open: boolean) => { + setOpen(open); + }, []); + const { showMoreButton } = useMoreOptionsConfig(); + + if (!showMoreButton) return null; + return ( + <> + + toggleDrawer(true)} className={'text-icon-primary'}> + + + + toggleDrawer(false)}> + + + + ); +} + +export default MoreButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.hooks.ts new file mode 100644 index 0000000000..63f1173885 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.hooks.ts @@ -0,0 +1,30 @@ +import { useLocation } from 'react-router-dom'; +import { useMemo } from 'react'; + +export function useMoreOptionsConfig() { + const location = useLocation(); + + const { type, pageType } = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, type, pageType, id] = location.pathname.split('/'); + + return { + type, + pageType, + id, + }; + }, [location.pathname]); + + const showMoreButton = useMemo(() => { + return type === 'page'; + }, [type]); + + const showStyleOptions = useMemo(() => { + return type === 'page' && pageType === 'document'; + }, [pageType, type]); + + return { + showMoreButton, + showStyleOptions, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.tsx new file mode 100644 index 0000000000..7f77259212 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreOptions.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import FontSizeConfig from '$app/components/layout/top_bar/FontSizeConfig'; +import { useMoreOptionsConfig } from '$app/components/layout/top_bar/MoreOptions.hooks'; + +function MoreOptions() { + const { showStyleOptions } = useMoreOptionsConfig(); + + return
{showStyleOptions && }
; +} + +export default MoreOptions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx new file mode 100644 index 0000000000..173bf86cab --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; +import { useAppSelector } from '$app/stores/store'; +import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb'; +import DeletePageSnackbar from '$app/components/layout/top_bar/DeletePageSnackbar'; + +function TopBar() { + const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); + + return ( +
+ {sidebarIsCollapsed && ( +
+ +
+ )} +
+
+ +
+
+ +
+ ); +} + +export default TopBar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx new file mode 100644 index 0000000000..ec3335b6b3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useAppSelector } from '$app/stores/store'; +import NestedPage from '$app/components/layout/nested_page/NestedPage'; + +function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) { + const pageIds = useAppSelector((state) => { + return state.pages.relationMap[workspaceId]; + }); + + return ( +
+ {pageIds?.map((pageId) => ( + + ))} +
+ ); +} + +export default WorkspaceNestedPages; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx new file mode 100644 index 0000000000..537b7d2d9a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; + +function NewPageButton({ workspaceId }: { workspaceId: string }) { + const { t } = useTranslation(); + const { newPage } = useWorkspaceActions(workspaceId); + + return ( +
+
+ } + className={'flex w-full items-center justify-start text-xs hover:bg-transparent hover:text-fill-default'} + > + {t('newPageText')} + +
+ ); +} + +export default NewPageButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx new file mode 100644 index 0000000000..984ed6f67f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx @@ -0,0 +1,43 @@ +import React, { useCallback } from 'react'; +import { ReactComponent as TrashSvg } from '$app/assets/delete.svg'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useDrag } from 'src/appflowy_app/components/_shared/drag_block'; +import { deletePage } from '$app/application/folder/page.service'; + +function TrashButton() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const currentPathType = useLocation().pathname.split('/')[1]; + const navigateToTrash = () => { + navigate('/trash'); + }; + + const selected = currentPathType === 'trash'; + + const onEnd = useCallback((result: { dragId: string; position: 'before' | 'after' | 'inside' }) => { + void deletePage(result.dragId); + }, []); + + const { onDrop, onDragOver, onDragLeave, isDraggingOver } = useDrag({ + onEnd, + }); + + return ( +
+ + {t('trash.text')} +
+ ); +} + +export default TrashButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts new file mode 100644 index 0000000000..86bca45ada --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice'; +import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; +import { subscribeNotifications } from '$app/application/notification'; +import { FolderNotification, ViewLayoutPB } from '@/services/backend'; +import * as workspaceService from '$app/application/folder/workspace.service'; +import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; +import { openPage } from '$app_reducers/pages/async_actions'; + +export function useLoadWorkspaces() { + const dispatch = useAppDispatch(); + const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace); + + const currentWorkspace = useMemo(() => { + return workspaces.find((workspace) => workspace.id === currentWorkspaceId); + }, [workspaces, currentWorkspaceId]); + + const initializeWorkspaces = useCallback(async () => { + const workspaces = await workspaceService.getWorkspaces(); + + const currentWorkspaceId = await workspaceService.getCurrentWorkspace(); + + dispatch( + workspaceActions.initWorkspaces({ + workspaces, + currentWorkspaceId, + }) + ); + }, [dispatch]); + + return { + workspaces, + currentWorkspace, + initializeWorkspaces, + }; +} + +export function useLoadWorkspace(workspace: WorkspaceItem) { + const { id } = workspace; + const dispatch = useAppDispatch(); + + const openWorkspace = useCallback(async () => { + await workspaceService.openWorkspace(id); + }, [id]); + + const deleteWorkspace = useCallback(async () => { + await workspaceService.deleteWorkspace(id); + }, [id]); + + const onChildPagesChanged = useCallback( + (childPages: Page[]) => { + dispatch( + pagesActions.addChildPages({ + id, + childPages, + }) + ); + }, + [dispatch, id] + ); + + const initializeWorkspace = useCallback(async () => { + const childPages = await workspaceService.getWorkspaceChildViews(id); + + dispatch( + pagesActions.addChildPages({ + id, + childPages, + }) + ); + }, [dispatch, id]); + + useEffect(() => { + void (async () => { + await initializeWorkspace(); + })(); + }, [initializeWorkspace]); + + useEffect(() => { + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateWorkspace]: async (changeset) => { + dispatch( + workspaceActions.updateWorkspace({ + id: String(changeset.id), + name: changeset.name, + icon: changeset.icon_url, + }) + ); + }, + [FolderNotification.DidUpdateWorkspaceViews]: async (changeset) => { + const res = changeset.items; + + onChildPagesChanged(res.map(parserViewPBToPage)); + }, + }, + { id } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [dispatch, id, onChildPagesChanged]); + + return { + openWorkspace, + deleteWorkspace, + }; +} + +export function useWorkspaceActions(workspaceId: string) { + const dispatch = useAppDispatch(); + const newPage = useCallback(async () => { + const { id } = await createCurrentWorkspaceChildView({ + name: '', + layout: ViewLayoutPB.Document, + parent_view_id: workspaceId, + }); + + dispatch( + pagesActions.addPage({ + page: { + id: id, + parentId: workspaceId, + layout: ViewLayoutPB.Document, + name: '', + }, + isLast: true, + }) + ); + void dispatch(openPage(id)); + }, [dispatch, workspaceId]); + + return { + newPage, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx new file mode 100644 index 0000000000..24fc7be91e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { WorkspaceItem } from '$app_reducers/workspace/slice'; +import NestedViews from '$app/components/layout/workspace_manager/NestedPages'; +import { useLoadWorkspace, useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddIcon } from '$app/assets/add.svg'; +import { IconButton } from '@mui/material'; +import Tooltip from '@mui/material/Tooltip'; +import { WorkplaceAvatar } from '$app/components/_shared/avatar'; + +function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { + useLoadWorkspace(workspace); + const { t } = useTranslation(); + const { newPage } = useWorkspaceActions(workspace.id); + const [showPages, setShowPages] = useState(true); + const [showAdd, setShowAdd] = useState(false); + + return ( + <> +
+
{ + e.stopPropagation(); + e.preventDefault(); + setShowPages((prev) => { + return !prev; + }); + }} + onMouseEnter={() => { + setShowAdd(true); + }} + onMouseLeave={() => { + setShowAdd(false); + }} + className={'mt-2 flex h-[22px] w-full cursor-pointer select-none items-center justify-between px-4'} + > + +
+ {!workspace.name ? ( + t('sideBar.personal') + ) : ( + <> + + {workspace.name} + + )} +
+
+ {showAdd && ( + + { + e.stopPropagation(); + void newPage(); + }} + size={'small'} + > + + + + )} +
+ +
+ +
+
+ + ); +} + +export default Workspace; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx new file mode 100644 index 0000000000..083dd61ec3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx @@ -0,0 +1,38 @@ +import React, { useEffect } from 'react'; +import NewPageButton from '$app/components/layout/workspace_manager/NewPageButton'; +import { useLoadWorkspaces } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import Workspace from './Workspace'; +import TrashButton from '$app/components/layout/workspace_manager/TrashButton'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { LoginState } from '$app_reducers/current-user/slice'; +import { AFScroller } from '$app/components/_shared/scroller'; + +function WorkspaceManager() { + const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces(); + + const loginState = useAppSelector((state) => state.currentUser.loginState); + + useEffect(() => { + if (loginState === LoginState.Success || loginState === undefined) { + void initializeWorkspaces(); + } + }, [initializeWorkspaces, loginState]); + + return ( +
+ +
+ {workspaces.map((workspace) => ( + + ))} +
+
+
+ +
+ {currentWorkspace && } +
+ ); +} + +export default WorkspaceManager; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx new file mode 100644 index 0000000000..d5ecc4bc0c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx @@ -0,0 +1,22 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup'; + +export const Login = ({ onBack }: { onBack?: () => void }) => { + const { t } = useTranslation(); + + return ( +
+ + {t('button.login')} + +
+ + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx new file mode 100644 index 0000000000..1d9f3c0cd9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx @@ -0,0 +1,92 @@ +import { useMemo, useState } from 'react'; +import { Box, Tab, Tabs } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { MyAccount } from '$app/components/settings/my_account'; +import { ReactComponent as AccountIcon } from '$app/assets/settings/account.svg'; +import { ReactComponent as WorkplaceIcon } from '$app/assets/settings/workplace.svg'; +import { Workplace } from '$app/components/settings/workplace'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +export const Settings = ({ onForward }: { onForward: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(0); + + const tabOptions = useMemo(() => { + return [ + { + label: t('newSettings.myAccount.title'), + Icon: AccountIcon, + Component: MyAccount, + }, + { + label: t('newSettings.workplace.name'), + Icon: WorkplaceIcon, + Component: Workplace, + }, + ]; + }, [t]); + + const handleChangeTab = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; + + return ( + + + {tabOptions.map((tab, index) => ( + + + {tab.label} +
+ } + onClick={() => setActiveTab(index)} + sx={{ '&.Mui-selected': { borderColor: 'transparent', backgroundColor: 'var(--fill-list-active)' } }} + /> + ))} + + {tabOptions.map((tab, index) => ( + + + + ))} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx new file mode 100644 index 0000000000..b53f8a6002 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx @@ -0,0 +1,108 @@ +/** + * @figmaUrl https://www.figma.com/file/MF5CWlOzBRuGHp45zAXyUH/Appflowy%3A-Desktop-Settings?type=design&node-id=100%3A2119&mode=design&t=4Wb0Zg5NOFO36kOf-1 + */ + +import Dialog, { DialogProps } from '@mui/material/Dialog'; +import { Settings } from '$app/components/settings/Settings'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import DialogTitle from '@mui/material/DialogTitle'; +import { IconButton } from '@mui/material'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as UpIcon } from '$app/assets/up.svg'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; +import DialogContent from '@mui/material/DialogContent'; +import { Login } from '$app/components/settings/Login'; +import SwipeableViews from 'react-swipeable-views'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; + +export const SettingsDialog = (props: DialogProps) => { + const dispatch = useAppDispatch(); + const [routes, setRoutes] = useState([]); + const loginState = useAppSelector((state) => state.currentUser.loginState); + const lastLoginStateRef = useRef(loginState); + const { t } = useTranslation(); + const handleForward = useCallback((route: SettingsRoutes) => { + setRoutes((prev) => { + return [...prev, route]; + }); + }, []); + + const handleBack = useCallback(() => { + setRoutes((prevState) => { + return prevState.slice(0, -1); + }); + dispatch(currentUserActions.resetLoginState()); + }, [dispatch]); + + const handleClose = useCallback(() => { + dispatch(currentUserActions.resetLoginState()); + props?.onClose?.({}, 'backdropClick'); + }, [dispatch, props]); + + const currentRoute = routes[routes.length - 1]; + + useEffect(() => { + if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) { + handleClose(); + return; + } + + lastLoginStateRef.current = loginState; + }, [loginState, handleClose]); + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + } + }} + scroll={'paper'} + > + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts new file mode 100644 index 0000000000..0f0a2c23f4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts @@ -0,0 +1 @@ +export * from './my_account'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx new file mode 100644 index 0000000000..05b375c920 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import { Divider } from '@mui/material'; +import { DeleteAccount } from '$app/components/settings/my_account/DeleteAccount'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; +import { useAuth } from '$app/components/auth/auth.hooks'; + +export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + const { currentUser, logout } = useAuth(); + + const isLocal = currentUser.isLocal; + + return ( + <> +
+ + {t('newSettings.myAccount.accountLogin')} + + + + +
+ + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx new file mode 100644 index 0000000000..82a909180e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import { DeleteAccountDialog } from '$app/components/settings/my_account/DeleteAccountDialog'; + +export const DeleteAccount = () => { + const { t } = useTranslation(); + + const [openDialog, setOpenDialog] = useState(false); + + return ( +
+
+ + {t('newSettings.myAccount.deleteAccount.title')} + + + {t('newSettings.myAccount.deleteAccount.subtitle')} + +
+
+ +
+ { + setOpenDialog(false); + }} + /> +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx new file mode 100644 index 0000000000..2f8cc37258 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx @@ -0,0 +1,50 @@ +import Dialog, { DialogProps } from '@mui/material/Dialog'; +import { useTranslation } from 'react-i18next'; +import DialogTitle from '@mui/material/DialogTitle'; +import { DialogActions, DialogContentText, IconButton } from '@mui/material'; +import Button from '@mui/material/Button'; +import DialogContent from '@mui/material/DialogContent'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; + +export const DeleteAccountDialog = (props: DialogProps) => { + const { t } = useTranslation(); + + const handleClose = () => { + props?.onClose?.({}, 'backdropClick'); + }; + + const handleOk = () => { + //123 + }; + + return ( + + {t('newSettings.myAccount.deleteAccount.dialogTitle')} + + {t('newSettings.myAccount.deleteAccount.dialogContent1')} + {t('newSettings.myAccount.deleteAccount.dialogContent2')} + + +
+ +
+
+ +
+
+ + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx new file mode 100644 index 0000000000..b3a315994b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { Profile } from './Profile'; +import { AccountLogin } from './AccountLogin'; +import { Divider } from '@mui/material'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; + +export const MyAccount = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + + return ( +
+ + {t('newSettings.myAccount.title')} + + + {t('newSettings.myAccount.subtitle')} + + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx new file mode 100644 index 0000000000..2ac672b0e5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx @@ -0,0 +1,180 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import { IconButton, InputAdornment, OutlinedInput } from '@mui/material'; +import { useAppSelector } from '$app/stores/store'; +import React, { useState } from 'react'; +import { ReactComponent as CheckIcon } from '$app/assets/select-check.svg'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; +import { ReactComponent as EditIcon } from '$app/assets/edit.svg'; + +import Tooltip from '@mui/material/Tooltip'; +import { UserService } from '$app/application/user/user.service'; +import { notify } from '$app/components/_shared/notify'; +import { ProfileAvatar } from '$app/components/_shared/avatar'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import Button from '@mui/material/Button'; + +export const Profile = () => { + const { displayName, id } = useAppSelector((state) => state.currentUser); + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); + const [newName, setNewName] = useState(displayName ?? 'Me'); + const [error, setError] = useState(false); + const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); + const openEmojiPicker = Boolean(emojiPickerAnchor); + const handleSave = async () => { + setError(false); + if (!newName) { + setError(true); + return; + } + + if (newName === displayName) { + setIsEditing(false); + return; + } + + try { + await UserService.updateUserProfile({ + id, + name: newName, + }); + setIsEditing(false); + } catch { + setError(true); + notify.error(t('newSettings.myAccount.updateNameError')); + } + }; + + const handleEmojiSelect = async (emoji: string) => { + try { + await UserService.updateUserProfile({ + id, + icon_url: emoji, + }); + } catch { + notify.error(t('newSettings.myAccount.updateIconError')); + } + }; + + const handleCancel = () => { + setNewName(displayName ?? 'Me'); + setIsEditing(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void handleSave(); + } + + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + handleCancel(); + } + }; + + return ( +
+ + {t('newSettings.myAccount.profileLabel')} + +
+ + +
+ {isEditing ? ( + setNewName(e.target.value)} + spellCheck={false} + autoFocus={true} + autoCorrect={'off'} + autoCapitalize={'off'} + fullWidth + endAdornment={ + +
+ + + + + + + + + + +
+
+ } + sx={{ + '&.MuiOutlinedInput-root': { + borderRadius: '8px', + }, + }} + placeholder={t('newSettings.myAccount.profileNamePlaceholder')} + value={newName} + /> + ) : ( + + {newName} + + setIsEditing(true)} size={'small'} className={'ml-1'}> + + + + + )} +
+
+ {openEmojiPicker && ( + { + setEmojiPickerAnchor(null); + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + { + setEmojiPickerAnchor(null); + }} + onEmojiSelect={handleEmojiSelect} + /> + + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts new file mode 100644 index 0000000000..d923fcefce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts @@ -0,0 +1 @@ +export * from './MyAccount'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx new file mode 100644 index 0000000000..1dc8581dae --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next'; +import { ThemeModeSwitch } from '$app/components/settings/workplace/appearance/ThemeModeSwitch'; +import Typography from '@mui/material/Typography'; +import { Divider } from '@mui/material'; +import { LanguageSetting } from '$app/components/settings/workplace/appearance/LanguageSetting'; + +export const Appearance = () => { + const { t } = useTranslation(); + + return ( + <> + + {t('newSettings.workplace.appearance.name')} + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx new file mode 100644 index 0000000000..8af69eec51 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { WorkplaceDisplay } from '$app/components/settings/workplace/WorkplaceDisplay'; +import { Divider } from '@mui/material'; +import { Appearance } from '$app/components/settings/workplace/Appearance'; + +export const Workplace = () => { + const { t } = useTranslation(); + + return ( +
+ + {t('newSettings.workplace.title')} + + + {t('newSettings.workplace.subtitle')} + + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx new file mode 100644 index 0000000000..3a71c5f070 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx @@ -0,0 +1,155 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { Divider, OutlinedInput } from '@mui/material'; +import React, { useMemo, useState } from 'react'; +import Button from '@mui/material/Button'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { changeWorkspaceIcon, renameWorkspace } from '$app/application/folder/workspace.service'; +import { notify } from '$app/components/_shared/notify'; +import { WorkplaceAvatar } from '$app/components/_shared/avatar'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import { workspaceActions } from '$app_reducers/workspace/slice'; +import debounce from 'lodash-es/debounce'; + +export const WorkplaceDisplay = () => { + const { t } = useTranslation(); + const isLocal = useAppSelector((state) => state.currentUser.isLocal); + const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace); + const workspace = useMemo( + () => workspaces.find((workspace) => workspace.id === currentWorkspaceId), + [workspaces, currentWorkspaceId] + ); + const [name, setName] = useState(workspace?.name ?? ''); + const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); + const openEmojiPicker = Boolean(emojiPickerAnchor); + const dispatch = useAppDispatch(); + + const debounceUpdateWorkspace = useMemo(() => { + return debounce(async ({ id, name, icon }: { id: string; name?: string; icon?: string }) => { + if (!id || !name) return; + + if (icon) { + try { + await changeWorkspaceIcon(id, icon); + } catch { + notify.error(t('newSettings.workplace.updateIconError')); + } + } + + if (name) { + try { + await renameWorkspace(id, name); + } catch { + notify.error(t('newSettings.workplace.renameError')); + } + } + }, 500); + }, [t]); + + const handleSave = async () => { + if (!workspace || !name) return; + dispatch(workspaceActions.updateWorkspace({ id: workspace.id, name })); + + await debounceUpdateWorkspace({ id: workspace.id, name }); + }; + + const handleEmojiSelect = async (icon: string) => { + if (!workspace) return; + dispatch(workspaceActions.updateWorkspace({ id: workspace.id, icon })); + + await debounceUpdateWorkspace({ id: workspace.id, icon }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void handleSave(); + } + }; + + return ( +
+ + {t('newSettings.workplace.workplaceName')} + +
+
+ setName(e.target.value)} + sx={{ + '&.MuiOutlinedInput-root': { + borderRadius: '8px', + }, + }} + placeholder={t('newSettings.workplace.workplaceNamePlaceholder')} + value={name} + /> +
+ +
+ + + {t('newSettings.workplace.workplaceIcon')} + + + {t('newSettings.workplace.workplaceIconSubtitle')} + + + {openEmojiPicker && ( + { + setEmojiPickerAnchor(null); + }} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + { + setEmojiPickerAnchor(null); + }} + onEmojiSelect={handleEmojiSelect} + /> + + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx new file mode 100644 index 0000000000..41a42bd011 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx @@ -0,0 +1,115 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import React, { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; + +const languages = [ + { + key: 'ar-SA', + title: 'العربية', + }, + { key: 'ca-ES', title: 'Català' }, + { key: 'de-DE', title: 'Deutsch' }, + { key: 'en', title: 'English' }, + { key: 'es-VE', title: 'Español (Venezuela)' }, + { key: 'eu-ES', title: 'Español' }, + { key: 'fr-FR', title: 'Français' }, + { key: 'hu-HU', title: 'Magyar' }, + { key: 'id-ID', title: 'Bahasa Indonesia' }, + { key: 'it-IT', title: 'Italiano' }, + { key: 'ja-JP', title: '日本語' }, + { key: 'ko-KR', title: '한국어' }, + { key: 'pl-PL', title: 'Polski' }, + { key: 'pt-BR', title: 'Português' }, + { key: 'pt-PT', title: 'Português' }, + { key: 'ru-RU', title: 'Русский' }, + { key: 'sv', title: 'Svenska' }, + { key: 'th-TH', title: 'ไทย' }, + { key: 'tr-TR', title: 'Türkçe' }, + { key: 'zh-CN', title: '简体中文' }, + { key: 'zh-TW', title: '繁體中文' }, +]; + +export const LanguageSetting = () => { + const { t, i18n } = useTranslation(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const dispatch = useAppDispatch(); + const selectedLanguage = userSettingState.language; + + const [hoverKey, setHoverKey] = React.useState(null); + + const handleChange = useCallback( + (language: string) => { + const newSetting = { ...userSettingState, language }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + const newLanguage = newSetting.language || 'en'; + + void UserService.setAppearanceSetting({ + theme: newSetting.theme, + theme_mode: newSetting.themeMode, + locale: { + language_code: newLanguage.split('-')[0], + country_code: newLanguage.split('-')[1], + }, + }); + }, + [dispatch, userSettingState] + ); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + } + }, []); + + return ( + <> + + {t('newSettings.workplace.appearance.language')} + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx new file mode 100644 index 0000000000..34fdb8e598 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx @@ -0,0 +1,93 @@ +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { useCallback, useMemo } from 'react'; +import { ThemeModePB } from '@/services/backend'; +import darkSrc from '$app/assets/settings/dark.png'; +import lightSrc from '$app/assets/settings/light.png'; +import { currentUserActions, ThemeMode } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; +import { ReactComponent as CheckCircle } from '$app/assets/settings/check_circle.svg'; + +export const ThemeModeSwitch = () => { + const { t } = useTranslation(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const dispatch = useAppDispatch(); + + const selectedMode = userSettingState.themeMode; + const themeModes = useMemo(() => { + return [ + { + name: t('newSettings.workplace.appearance.themeMode.auto'), + value: ThemeModePB.System, + img: ( +
+ + +
+ ), + }, + { + name: t('newSettings.workplace.appearance.themeMode.light'), + value: ThemeModePB.Light, + img: , + }, + { + name: t('newSettings.workplace.appearance.themeMode.dark'), + value: ThemeModePB.Dark, + img: , + }, + ]; + }, [t]); + + const handleChange = useCallback( + (newValue: ThemeModePB) => { + const newSetting = { + ...userSettingState, + ...{ + themeMode: newValue, + isDark: + newValue === ThemeMode.Dark || + (newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches), + }, + }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + + void UserService.setAppearanceSetting({ + theme_mode: newSetting.themeMode, + }); + }, + [dispatch, userSettingState] + ); + + const renderThemeModeItem = useCallback( + (option: { name: string; value: ThemeModePB; img: JSX.Element }) => { + return ( +
handleChange(option.value)} + className={'flex cursor-pointer flex-col items-center gap-2'} + > +
+ {option.img} + +
+
{option.name}
+
+ ); + }, + [handleChange, selectedMode] + ); + + return
{themeModes.map((mode) => renderThemeModeItem(mode))}
; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts new file mode 100644 index 0000000000..075e2744a5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts @@ -0,0 +1,3 @@ +export enum SettingsRoutes { + LOGIN = 'login', +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts new file mode 100644 index 0000000000..a64592ac8b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts @@ -0,0 +1 @@ +export * from './Workplace'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts new file mode 100644 index 0000000000..b6748614b8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts @@ -0,0 +1,79 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store'; +import { trashActions, trashPBToTrash } from '$app_reducers/trash/slice'; +import { subscribeNotifications } from '$app/application/notification'; +import { FolderNotification } from '@/services/backend'; +import { deleteTrashItem, getTrash, putback, deleteAll, restoreAll } from '$app/application/folder/trash.service'; + +export function useLoadTrash() { + const trash = useAppSelector((state) => state.trash.list); + const dispatch = useAppDispatch(); + + const initializeTrash = useCallback(async () => { + const trash = await getTrash(); + + dispatch(trashActions.initTrash(trash.map(trashPBToTrash))); + }, [dispatch]); + + useEffect(() => { + void initializeTrash(); + }, [initializeTrash]); + + useEffect(() => { + const unsubscribePromise = subscribeNotifications({ + [FolderNotification.DidUpdateTrash]: async (changeset) => { + dispatch(trashActions.onTrashChanged(changeset.items.map(trashPBToTrash))); + }, + }); + + return () => { + void unsubscribePromise.then((fn) => fn()); + }; + }, [dispatch]); + + return { + trash, + }; +} + +export function useTrashActions() { + const [restoreAllDialogOpen, setRestoreAllDialogOpen] = useState(false); + const [deleteAllDialogOpen, setDeleteAllDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const [deleteId, setDeleteId] = useState(''); + + const onClickRestoreAll = () => { + setRestoreAllDialogOpen(true); + }; + + const onClickDeleteAll = () => { + setDeleteAllDialogOpen(true); + }; + + const closeDialog = () => { + setRestoreAllDialogOpen(false); + setDeleteAllDialogOpen(false); + setDeleteDialogOpen(false); + }; + + const onClickDelete = (id: string) => { + setDeleteId(id); + setDeleteDialogOpen(true); + }; + + return { + onClickDelete, + deleteDialogOpen, + deleteId, + onPutback: putback, + onDelete: deleteTrashItem, + onDeleteAll: deleteAll, + onRestoreAll: restoreAll, + onClickRestoreAll, + onClickDeleteAll, + restoreAllDialogOpen, + deleteAllDialogOpen, + closeDialog, + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx new file mode 100644 index 0000000000..f10848dc9b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { DeleteOutline, RestoreOutlined } from '@mui/icons-material'; +import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks'; +import { List } from '@mui/material'; +import TrashItem from '$app/components/trash/TrashItem'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; + +function Trash() { + const { t } = useTranslation(); + const { trash } = useLoadTrash(); + const { + onPutback, + onDelete, + onClickRestoreAll, + onClickDeleteAll, + restoreAllDialogOpen, + deleteAllDialogOpen, + onRestoreAll, + onDeleteAll, + closeDialog, + deleteDialogOpen, + deleteId, + onClickDelete, + } = useTrashActions(); + const [hoverId, setHoverId] = useState(''); + + return ( +
+
+
{t('trash.text')}
+
+ + +
+
+
+
{t('trash.pageHeader.fileName')}
+
{t('trash.pageHeader.lastModified')}
+
{t('trash.pageHeader.created')}
+
+
+ + {trash.map((item) => ( + + ))} + + + + onDelete([deleteId])} + onClose={closeDialog} + /> +
+ ); +} + +export default Trash; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx new file mode 100644 index 0000000000..d266005612 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import { IconButton, ListItem } from '@mui/material'; +import { DeleteOutline, RestoreOutlined } from '@mui/icons-material'; +import Tooltip from '@mui/material/Tooltip'; +import { useTranslation } from 'react-i18next'; +import { Trash } from '$app_reducers/trash/slice'; + +function TrashItem({ + item, + hoverId, + setHoverId, + onDelete, + onPutback, +}: { + setHoverId: (id: string) => void; + item: Trash; + hoverId: string; + onPutback: (id: string) => void; + onDelete: (id: string) => void; +}) { + const { t } = useTranslation(); + + return ( + { + setHoverId(item.id); + }} + onMouseLeave={() => { + setHoverId(''); + }} + key={item.id} + style={{ + paddingInline: 0, + }} + > +
+
+ {item.name.trim() || t('menuAppHeader.defaultNewPageName')} +
+
{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}
+
{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}
+
+ + onPutback(item.id)} className={'mr-2'}> + + + + + onDelete(item.id)}> + + + +
+
+
+ ); +} + +export default TrashItem; diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts new file mode 100644 index 0000000000..6711ece8c8 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/ViewId.hooks.ts @@ -0,0 +1,6 @@ +import { createContext, useContext } from 'react'; + +const ViewIdContext = createContext(''); + +export const ViewIdProvider = ViewIdContext.Provider; +export const useViewId = () => useContext(ViewIdContext); diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts new file mode 100644 index 0000000000..c29ddd04aa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './notification.hooks'; +export * from './ViewId.hooks'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts new file mode 100644 index 0000000000..f8669852d3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/notification.hooks.ts @@ -0,0 +1,20 @@ +/* eslint-disable no-redeclare */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect } from 'react'; +import { NotificationEnum, NotificationHandler, subscribeNotification } from '$app/application/notification'; + +export function useNotification( + notification: K, + callback: NotificationHandler, + options: { id?: string } +): void { + const { id } = options; + + useEffect(() => { + const unsubscribePromise = subscribeNotification(notification, callback, { id }); + + return () => { + void unsubscribePromise.then((fn) => fn()); + }; + }, [callback, id, notification]); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx new file mode 100644 index 0000000000..49e01e75c0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx @@ -0,0 +1,26 @@ +import { ViewLayoutPB } from '@/services/backend'; +import React from 'react'; +import { Page } from '$app_reducers/pages/slice'; +import { ReactComponent as DocumentIcon } from '$app/assets/document.svg'; +import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; +import { ReactComponent as BoardIcon } from '$app/assets/board.svg'; +import { ReactComponent as CalendarIcon } from '$app/assets/date.svg'; + +export function getPageIcon(page: Page) { + if (page.icon) { + return page.icon.value; + } + + switch (page.layout) { + case ViewLayoutPB.Document: + return ; + case ViewLayoutPB.Grid: + return ; + case ViewLayoutPB.Board: + return ; + case ViewLayoutPB.Calendar: + return ; + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts b/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts new file mode 100644 index 0000000000..d36dba3bb2 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/i18n/config.ts @@ -0,0 +1,15 @@ +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +void i18next + .use(resourcesToBackend((language: string) => import(`./translations/${language}.json`))) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + lng: 'en', + defaultNS: 'translation', + debug: false, + fallbackLng: 'en', + }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/slate-editor.d.ts b/frontend/appflowy_tauri/src/appflowy_app/slate-editor.d.ts new file mode 100644 index 0000000000..26b713e333 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/slate-editor.d.ts @@ -0,0 +1,44 @@ +import { ReactEditor } from 'slate-react'; + +interface EditorInlineAttributes { + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + font_color?: string; + bg_color?: string; + href?: string; + code?: boolean; + formula?: string; + prism_token?: string; + class_name?: string; + mention?: { + type: string; + // inline page ref id + page?: string; + // reminder date ref id + date?: string; + }; +} + +type CustomElement = { + children: (CustomText | CustomElement)[]; + type: string; + data?: unknown; + blockId?: string; + textId?: string; +}; + +type CustomText = { text: string } & EditorInlineAttributes; + +declare module 'slate' { + interface CustomTypes { + Editor: BaseEditor & ReactEditor; + Element: CustomElement; + Text: CustomText; + } + + interface BaseEditor { + isEmbed: (element: CustomElement) => boolean; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts new file mode 100644 index 0000000000..464b7428a3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts @@ -0,0 +1,99 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace'; +import { ThemeModePB as ThemeMode } from '@/services/backend'; +import { Page, parserViewPBToPage } from '$app_reducers/pages/slice'; + +export { ThemeMode }; + +export interface UserSetting { + theme?: Theme; + themeMode?: ThemeMode; + language?: string; + isDark?: boolean; +} + +export enum Theme { + Default = 'default', + Dandelion = 'dandelion', + Lavender = 'lavender', +} + +export enum LoginState { + Loading = 'loading', + Success = 'success', + Error = 'error', +} + +export interface UserWorkspaceSetting { + workspaceId: string; + latestView?: Page; + hasLatestView: boolean; +} + +export function parseWorkspaceSettingPBToSetting(workspaceSetting: WorkspaceSettingPB): UserWorkspaceSetting { + return { + workspaceId: workspaceSetting.workspace_id, + latestView: workspaceSetting.latest_view ? parserViewPBToPage(workspaceSetting.latest_view) : undefined, + hasLatestView: !!workspaceSetting.latest_view, + }; +} + +export interface ICurrentUser { + id?: number; + deviceId?: string; + displayName?: string; + email?: string; + token?: string; + iconUrl?: string; + isAuthenticated: boolean; + workspaceSetting?: UserWorkspaceSetting; + userSetting: UserSetting; + isLocal: boolean; + loginState?: LoginState; +} + +const initialState: ICurrentUser | null = { + isAuthenticated: false, + userSetting: {}, + isLocal: true, +}; + +export const currentUserSlice = createSlice({ + name: 'currentUser', + initialState: initialState, + reducers: { + updateUser: (state, action: PayloadAction>) => { + return { + ...state, + ...action.payload, + loginState: LoginState.Success, + }; + }, + logout: () => { + return initialState; + }, + setUserSetting: (state, action: PayloadAction>) => { + state.userSetting = { + ...state.userSetting, + ...action.payload, + }; + }, + + setLoginState: (state, action: PayloadAction) => { + state.loginState = action.payload; + }, + + resetLoginState: (state) => { + state.loginState = undefined; + }, + + setLatestView: (state, action: PayloadAction) => { + if (state.workspaceSetting) { + state.workspaceSetting.latestView = action.payload; + state.workspaceSetting.hasLatestView = true; + } + }, + }, +}); + +export const currentUserActions = currentUserSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts new file mode 100644 index 0000000000..9b47df7777 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/error/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface IErrorOptions { + display: boolean; + message: string; +} + +const initialState: IErrorOptions = { + display: false, + message: '', +}; + +export const errorSlice = createSlice({ + name: 'error', + initialState: initialState, + reducers: { + showError(state, action: PayloadAction) { + return { + display: true, + message: action.payload, + }; + }, + hideError() { + return { + display: false, + message: '', + }; + }, + }, +}); + +export const errorActions = errorSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts new file mode 100644 index 0000000000..90014c1e7f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts @@ -0,0 +1,106 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { RootState } from '$app/stores/store'; +import { pagesActions } from '$app_reducers/pages/slice'; +import { movePage, setLatestOpenedPage, updatePage } from '$app/application/folder/page.service'; +import debounce from 'lodash-es/debounce'; +import { currentUserActions } from '$app_reducers/current-user/slice'; + +export const movePageThunk = createAsyncThunk( + 'pages/movePage', + async ( + payload: { + sourceId: string; + targetId: string; + insertType: 'before' | 'after' | 'inside'; + }, + thunkAPI + ) => { + const { sourceId, targetId, insertType } = payload; + const { getState, dispatch } = thunkAPI; + const { pageMap, relationMap } = (getState() as RootState).pages; + const sourcePage = pageMap[sourceId]; + const targetPage = pageMap[targetId]; + + if (!sourcePage || !targetPage) return; + const sourceParentId = sourcePage.parentId; + const targetParentId = targetPage.parentId; + + if (!sourceParentId || !targetParentId) return; + + const targetParentChildren = relationMap[targetParentId] || []; + const targetIndex = targetParentChildren.indexOf(targetId); + + if (targetIndex < 0) return; + + let prevId, parentId; + + if (insertType === 'before') { + const prevIndex = targetIndex - 1; + + parentId = targetParentId; + if (prevIndex >= 0) { + prevId = targetParentChildren[prevIndex]; + } + } else if (insertType === 'after') { + prevId = targetId; + parentId = targetParentId; + } else { + const targetChildren = relationMap[targetId] || []; + + parentId = targetId; + if (targetChildren.length > 0) { + prevId = targetChildren[targetChildren.length - 1]; + } + } + + dispatch(pagesActions.movePage({ id: sourceId, newParentId: parentId, prevId })); + + await movePage({ + view_id: sourceId, + new_parent_id: parentId, + prev_view_id: prevId, + }); + } +); + +const debounceUpdateName = debounce(updatePage, 1000); + +export const updatePageName = createAsyncThunk( + 'pages/updateName', + async (payload: { id: string; name: string; immediate?: boolean }, thunkAPI) => { + const { dispatch, getState } = thunkAPI; + const { pageMap } = (getState() as RootState).pages; + const { id, name, immediate } = payload; + const page = pageMap[id]; + + if (name === page.name) return; + + dispatch( + pagesActions.onPageChanged({ + ...page, + name, + }) + ); + + if (immediate) { + await updatePage({ id, name }); + } else { + await debounceUpdateName({ + id, + name, + }); + } + } +); + +export const openPage = createAsyncThunk('pages/openPage', async (id: string, thunkAPI) => { + const { dispatch, getState } = thunkAPI; + const { pageMap } = (getState() as RootState).pages; + + const page = pageMap[id]; + + if (!page) return; + + dispatch(currentUserActions.setLatestView(page)); + await setLatestOpenedPage(id); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts new file mode 100644 index 0000000000..dbf313ecc1 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -0,0 +1,223 @@ +import { ViewIconTypePB, ViewLayoutPB, ViewPB } from '@/services/backend'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import isEqual from 'lodash-es/isEqual'; +import { ImageType } from '$app/application/document/document.types'; +import { Nullable } from 'unsplash-js/dist/helpers/typescript'; + +export const pageTypeMap = { + [ViewLayoutPB.Document]: 'document', + [ViewLayoutPB.Board]: 'board', + [ViewLayoutPB.Grid]: 'grid', + [ViewLayoutPB.Calendar]: 'calendar', +}; +export interface Page { + id: string; + parentId: string; + name: string; + layout: ViewLayoutPB; + icon?: PageIcon; + cover?: PageCover; +} + +export interface PageIcon { + ty: ViewIconTypePB; + value: string; +} + +export enum CoverType { + Color = 'CoverType.color', + Image = 'CoverType.file', + Asset = 'CoverType.asset', +} +export type PageCover = Nullable<{ + image_type?: ImageType; + cover_selection_type?: CoverType; + cover_selection?: string; +}>; + +export function parserViewPBToPage(view: ViewPB): Page { + const icon = view.icon; + + return { + id: view.id, + name: view.name, + parentId: view.parent_view_id, + layout: view.layout, + icon: icon + ? { + ty: icon.ty, + value: icon.value, + } + : undefined, + }; +} + +export interface PageState { + pageMap: Record; + relationMap: Record; + expandedIdMap: Record; + showTrashSnackbar: boolean; +} + +export const initialState: PageState = { + pageMap: {}, + relationMap: {}, + expandedIdMap: getExpandedPageIds().reduce((acc, id) => { + acc[id] = true; + return acc; + }, {} as Record), + showTrashSnackbar: false, +}; + +export const pagesSlice = createSlice({ + name: 'pages', + initialState, + reducers: { + addChildPages( + state, + action: PayloadAction<{ + childPages: Page[]; + id: string; + }> + ) { + const { childPages, id } = action.payload; + const pageMap: Record = {}; + + const children: string[] = []; + + childPages.forEach((page) => { + pageMap[page.id] = page; + children.push(page.id); + }); + + state.pageMap = { + ...state.pageMap, + ...pageMap, + }; + state.relationMap[id] = children; + }, + + onPageChanged(state, action: PayloadAction) { + const page = action.payload; + + if (!isEqual(state.pageMap[page.id], page)) { + state.pageMap[page.id] = page; + } + }, + + addPage( + state, + action: PayloadAction<{ + page: Page; + isLast?: boolean; + prevId?: string; + }> + ) { + const { page, prevId, isLast } = action.payload; + + state.pageMap[page.id] = page; + state.relationMap[page.id] = []; + + const parentId = page.parentId; + + if (isLast) { + state.relationMap[parentId]?.push(page.id); + } else { + const index = prevId ? state.relationMap[parentId]?.indexOf(prevId) ?? -1 : -1; + + state.relationMap[parentId]?.splice(index + 1, 0, page.id); + } + }, + + deletePages(state, action: PayloadAction) { + const ids = action.payload; + + ids.forEach((id) => { + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + state.relationMap[parentId] = parentChildren && parentChildren.filter((childId) => childId !== id); + delete state.relationMap[id]; + delete state.expandedIdMap[id]; + delete state.pageMap[id]; + }); + }, + + duplicatePage( + state, + action: PayloadAction<{ + id: string; + newId: string; + }> + ) { + const { id, newId } = action.payload; + const page = state.pageMap[id]; + const newPage = { ...page, id: newId }; + + state.pageMap[newPage.id] = newPage; + + const index = state.relationMap[page.parentId]?.indexOf(id); + + state.relationMap[page.parentId]?.splice(index ?? 0, 0, newId); + }, + + movePage( + state, + action: PayloadAction<{ + id: string; + newParentId: string; + prevId?: string; + }> + ) { + const { id, newParentId, prevId } = action.payload; + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + const index = parentChildren?.indexOf(id) ?? -1; + + if (index > -1) { + state.relationMap[parentId]?.splice(index, 1); + } + + state.pageMap[id].parentId = newParentId; + const newParentChildren = state.relationMap[newParentId] || []; + const prevIndex = prevId ? newParentChildren.indexOf(prevId) : -1; + + state.relationMap[newParentId]?.splice(prevIndex + 1, 0, id); + }, + + expandPage(state, action: PayloadAction) { + const id = action.payload; + + state.expandedIdMap[id] = true; + const ids = Object.keys(state.expandedIdMap).filter((id) => state.expandedIdMap[id]); + + storeExpandedPageIds(ids); + }, + + collapsePage(state, action: PayloadAction) { + const id = action.payload; + + state.expandedIdMap[id] = false; + const ids = Object.keys(state.expandedIdMap).filter((id) => state.expandedIdMap[id]); + + storeExpandedPageIds(ids); + }, + + setTrashSnackbar(state, action: PayloadAction) { + state.showTrashSnackbar = action.payload; + }, + }, +}); + +export const pagesActions = pagesSlice.actions; + +function storeExpandedPageIds(expandedPageIds: string[]) { + localStorage.setItem('expandedPageIds', JSON.stringify(expandedPageIds)); +} + +function getExpandedPageIds(): string[] { + const expandedPageIds = localStorage.getItem('expandedPageIds'); + + return expandedPageIds ? JSON.parse(expandedPageIds) : []; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/sidebar/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/sidebar/slice.ts new file mode 100644 index 0000000000..fae1d59214 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/sidebar/slice.ts @@ -0,0 +1,37 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface SidebarState { + isCollapsed: boolean; + width: number; + isResizing: boolean; +} + +const initialState: SidebarState = { + isCollapsed: false, + width: 250, + isResizing: false, +}; + +export const sidebarSlice = createSlice({ + name: 'sidebar', + initialState: initialState, + reducers: { + toggleCollapse(state) { + state.isCollapsed = !state.isCollapsed; + }, + setCollapse(state, action: PayloadAction) { + state.isCollapsed = action.payload; + }, + changeWidth(state, action: PayloadAction) { + state.width = action.payload; + }, + startResizing(state) { + state.isResizing = true; + }, + stopResizing(state) { + state.isResizing = false; + }, + }, +}); + +export const sidebarActions = sidebarSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/trash/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/trash/slice.ts new file mode 100644 index 0000000000..98d850f6fe --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/trash/slice.ts @@ -0,0 +1,41 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { TrashPB } from '@/services/backend'; + +export interface Trash { + id: string; + name: string; + modifiedTime: number; + createTime: number; +} + +export function trashPBToTrash(trash: TrashPB) { + return { + id: trash.id, + name: trash.name, + modifiedTime: trash.modified_time, + createTime: trash.create_time, + }; +} + +interface TrashState { + list: Trash[]; +} + +const initialState: TrashState = { + list: [], +}; + +export const trashSlice = createSlice({ + name: 'trash', + initialState, + reducers: { + initTrash: (state, action: PayloadAction) => { + state.list = action.payload; + }, + onTrashChanged: (state, action: PayloadAction) => { + state.list = action.payload; + }, + }, +}); + +export const trashActions = trashSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts new file mode 100644 index 0000000000..d071de846e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts @@ -0,0 +1,46 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface WorkspaceItem { + id: string; + name: string; + icon?: string; +} + +interface WorkspaceState { + workspaces: WorkspaceItem[]; + currentWorkspaceId: string | null; +} + +const initialState: WorkspaceState = { + workspaces: [], + currentWorkspaceId: null, +}; + +export const workspaceSlice = createSlice({ + name: 'workspace', + initialState, + reducers: { + initWorkspaces: ( + state, + action: PayloadAction<{ + workspaces: WorkspaceItem[]; + currentWorkspaceId: string | null; + }> + ) => { + return action.payload; + }, + + updateWorkspace: (state, action: PayloadAction>) => { + const index = state.workspaces.findIndex((workspace) => workspace.id === action.payload.id); + + if (index !== -1) { + state.workspaces[index] = { + ...state.workspaces[index], + ...action.payload, + }; + } + }, + }, +}); + +export const workspaceActions = workspaceSlice.actions; diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts new file mode 100644 index 0000000000..269f46884c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/store.ts @@ -0,0 +1,51 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { + configureStore, + createListenerMiddleware, + TypedStartListening, + TypedAddListener, + ListenerEffectAPI, + addListener, +} from '@reduxjs/toolkit'; +import { pagesSlice } from './reducers/pages/slice'; +import { currentUserSlice } from './reducers/current-user/slice'; +import { workspaceSlice } from './reducers/workspace/slice'; +import { errorSlice } from './reducers/error/slice'; +import { sidebarSlice } from '$app_reducers/sidebar/slice'; +import { trashSlice } from '$app_reducers/trash/slice'; + +const listenerMiddlewareInstance = createListenerMiddleware({ + onError: () => console.error, +}); + +const store = configureStore({ + reducer: { + [pagesSlice.name]: pagesSlice.reducer, + [currentUserSlice.name]: currentUserSlice.reducer, + [workspaceSlice.name]: workspaceSlice.reducer, + [errorSlice.name]: errorSlice.reducer, + [sidebarSlice.name]: sidebarSlice.reducer, + [trashSlice.name]: trashSlice.reducer, + }, + middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware), +}); + +export { store }; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// @see https://redux-toolkit.js.org/usage/usage-with-typescript#getting-the-dispatch-type +export type AppDispatch = typeof store.dispatch; + +export type AppListenerEffectAPI = ListenerEffectAPI; + +// @see https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +export type AppStartListening = TypedStartListening; +export type AppAddListener = TypedAddListener; + +export const startAppListening = listenerMiddlewareInstance.startListening as AppStartListening; +export const addAppListener = addListener as AppAddListener; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts new file mode 100644 index 0000000000..7e673506de --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/async_queue.ts @@ -0,0 +1,51 @@ +import { Log } from '$app/utils/log'; + +export class AsyncQueue { + private queue: T[] = []; + private isProcessing = false; + private executeFunction: (item: T) => Promise; + + constructor(executeFunction: (item: T) => Promise) { + this.executeFunction = executeFunction; + } + + enqueue(item: T): void { + this.queue.push(item); + this.processQueue(); + } + + private processQueue(): void { + if (this.isProcessing || this.queue.length === 0) { + return; + } + + const item = this.queue.shift(); + + if (!item) { + return; + } + + this.isProcessing = true; + + const executeFn = async (item: T) => { + try { + await this.processItem(item); + } catch (error) { + Log.error('queue processing error:', error); + } finally { + this.isProcessing = false; + this.processQueue(); + } + }; + + void executeFn(item); + } + + private async processItem(item: T): Promise { + try { + await this.executeFunction(item); + } catch (error) { + Log.error('queue processing error:', error); + } + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts new file mode 100644 index 0000000000..a9a752c579 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts @@ -0,0 +1,26 @@ +export function stringToColor(string: string) { + let hash = 0; + let i; + + /* eslint-disable no-bitwise */ + for (i = 0; i < string.length; i += 1) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = '#'; + + for (i = 0; i < 3; i += 1) { + const value = (hash >> (i * 8)) & 0xff; + + color += `00${value.toString(16)}`.slice(-2); + } + /* eslint-enable no-bitwise */ + + return color; +} + +export function stringToShortName(string: string) { + const [firstName, lastName = ''] = string.split(' '); + + return `${firstName.charAt(0)}${lastName.charAt(0)}`; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts new file mode 100644 index 0000000000..57d9f2a370 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/change_notifier.ts @@ -0,0 +1,30 @@ +import { Observable, Subject } from 'rxjs'; + +export class ChangeNotifier { + private isUnsubscribe = false; + private subject = new Subject(); + + notify(value: T) { + this.subject.next(value); + } + + get observer(): Observable | null { + if (this.isUnsubscribe) { + return null; + } + + return this.subject.asObservable(); + } + + // Unsubscribe the subject might cause [UnsubscribedError] error if there is + // ongoing Observable execution. + // + // Maybe you should use the [Subscription] that returned when call subscribe on + // [Observable] to unsubscribe. + unsubscribe = () => { + if (!this.isUnsubscribe) { + this.isUnsubscribe = true; + this.subject.unsubscribe(); + } + }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts new file mode 100644 index 0000000000..025c8c45ed --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts @@ -0,0 +1,50 @@ +export enum ColorEnum { + Purple = 'appflowy_them_color_tint1', + Pink = 'appflowy_them_color_tint2', + LightPink = 'appflowy_them_color_tint3', + Orange = 'appflowy_them_color_tint4', + Yellow = 'appflowy_them_color_tint5', + Lime = 'appflowy_them_color_tint6', + Green = 'appflowy_them_color_tint7', + Aqua = 'appflowy_them_color_tint8', + Blue = 'appflowy_them_color_tint9', +} + +export const colorMap = { + [ColorEnum.Purple]: 'var(--tint-purple)', + [ColorEnum.Pink]: 'var(--tint-pink)', + [ColorEnum.LightPink]: 'var(--tint-red)', + [ColorEnum.Orange]: 'var(--tint-orange)', + [ColorEnum.Yellow]: 'var(--tint-yellow)', + [ColorEnum.Lime]: 'var(--tint-lime)', + [ColorEnum.Green]: 'var(--tint-green)', + [ColorEnum.Aqua]: 'var(--tint-aqua)', + [ColorEnum.Blue]: 'var(--tint-blue)', +}; + +// Convert ARGB to RGBA +// Flutter uses ARGB, but CSS uses RGBA +function argbToRgba(color: string): string { + const hex = color.replace(/^#|0x/, ''); + + const hasAlpha = hex.length === 8; + + if (!hasAlpha) { + return color.replace('0x', '#'); + } + + const r = parseInt(hex.slice(2, 4), 16); + const g = parseInt(hex.slice(4, 6), 16); + const b = parseInt(hex.slice(6, 8), 16); + const a = hasAlpha ? parseInt(hex.slice(0, 2), 16) / 255 : 1; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +export function renderColor(color: string) { + if (colorMap[color as ColorEnum]) { + return colorMap[color as ColorEnum]; + } + + return argbToRgba(color); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/emoji.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/emoji.ts new file mode 100644 index 0000000000..8d5adb5df6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/emoji.ts @@ -0,0 +1,9 @@ +import emojiData, { EmojiMartData } from '@emoji-mart/data'; + +export const randomEmoji = (skin = 0) => { + const emojis = (emojiData as EmojiMartData).emojis; + const keys = Object.keys(emojis); + const randomKey = keys[Math.floor(Math.random() * keys.length)]; + + return emojis[randomKey].skins[skin].native; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/env.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/env.ts new file mode 100644 index 0000000000..064dc042aa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/env.ts @@ -0,0 +1,11 @@ +export function isApple() { + return typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent); +} + +export function isTauri() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const isTauri = window.__TAURI__; + + return isTauri; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts new file mode 100644 index 0000000000..20aa05db27 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts @@ -0,0 +1,134 @@ +import isHotkey from 'is-hotkey'; + +export const isMac = () => { + return navigator.userAgent.includes('Mac OS X'); +}; + +const MODIFIERS = { + control: 'Ctrl', + meta: '⌘', +}; + +export const getModifier = () => { + return isMac() ? MODIFIERS.meta : MODIFIERS.control; +}; + +export enum HOT_KEY_NAME { + LEFT = 'left', + RIGHT = 'right', + SELECT_ALL = 'select-all', + ESCAPE = 'escape', + ALIGN_LEFT = 'align-left', + ALIGN_CENTER = 'align-center', + ALIGN_RIGHT = 'align-right', + BOLD = 'bold', + ITALIC = 'italic', + UNDERLINE = 'underline', + STRIKETHROUGH = 'strikethrough', + CODE = 'code', + TOGGLE_TODO = 'toggle-todo', + TOGGLE_COLLAPSE = 'toggle-collapse', + INDENT_BLOCK = 'indent-block', + OUTDENT_BLOCK = 'outdent-block', + INSERT_SOFT_BREAK = 'insert-soft-break', + SPLIT_BLOCK = 'split-block', + BACKSPACE = 'backspace', + OPEN_LINK = 'open-link', + OPEN_LINKS = 'open-links', + EXTEND_LINE_BACKWARD = 'extend-line-backward', + EXTEND_LINE_FORWARD = 'extend-line-forward', + PASTE = 'paste', + PASTE_PLAIN_TEXT = 'paste-plain-text', + HIGH_LIGHT = 'high-light', + EXTEND_DOCUMENT_BACKWARD = 'extend-document-backward', + EXTEND_DOCUMENT_FORWARD = 'extend-document-forward', + SCROLL_TO_TOP = 'scroll-to-top', + SCROLL_TO_BOTTOM = 'scroll-to-bottom', + FORMAT_LINK = 'format-link', + FIND_REPLACE = 'find-replace', + /** + * Navigation + */ + TOGGLE_THEME = 'toggle-theme', + TOGGLE_SIDEBAR = 'toggle-sidebar', +} + +const defaultHotKeys = { + [HOT_KEY_NAME.ALIGN_LEFT]: ['control+shift+l'], + [HOT_KEY_NAME.ALIGN_CENTER]: ['control+shift+e'], + [HOT_KEY_NAME.ALIGN_RIGHT]: ['control+shift+r'], + [HOT_KEY_NAME.BOLD]: ['mod+b'], + [HOT_KEY_NAME.ITALIC]: ['mod+i'], + [HOT_KEY_NAME.UNDERLINE]: ['mod+u'], + [HOT_KEY_NAME.STRIKETHROUGH]: ['mod+shift+s', 'mod+shift+x'], + [HOT_KEY_NAME.CODE]: ['mod+e'], + [HOT_KEY_NAME.TOGGLE_TODO]: ['mod+enter'], + [HOT_KEY_NAME.TOGGLE_COLLAPSE]: ['mod+enter'], + [HOT_KEY_NAME.SELECT_ALL]: ['mod+a'], + [HOT_KEY_NAME.ESCAPE]: ['esc'], + [HOT_KEY_NAME.INDENT_BLOCK]: ['tab'], + [HOT_KEY_NAME.OUTDENT_BLOCK]: ['shift+tab'], + [HOT_KEY_NAME.SPLIT_BLOCK]: ['enter'], + [HOT_KEY_NAME.INSERT_SOFT_BREAK]: ['shift+enter'], + [HOT_KEY_NAME.BACKSPACE]: ['backspace', 'shift+backspace'], + [HOT_KEY_NAME.OPEN_LINK]: ['opt+enter'], + [HOT_KEY_NAME.OPEN_LINKS]: ['opt+shift+enter'], + [HOT_KEY_NAME.EXTEND_LINE_BACKWARD]: ['opt+shift+left'], + [HOT_KEY_NAME.EXTEND_LINE_FORWARD]: ['opt+shift+right'], + [HOT_KEY_NAME.PASTE]: ['mod+v'], + [HOT_KEY_NAME.PASTE_PLAIN_TEXT]: ['mod+shift+v'], + [HOT_KEY_NAME.HIGH_LIGHT]: ['mod+shift+h'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD]: ['mod+shift+up'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD]: ['mod+shift+down'], + [HOT_KEY_NAME.SCROLL_TO_TOP]: ['home'], + [HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['end'], + [HOT_KEY_NAME.TOGGLE_THEME]: ['mod+shift+l'], + [HOT_KEY_NAME.TOGGLE_SIDEBAR]: ['mod+.'], + [HOT_KEY_NAME.FORMAT_LINK]: ['mod+k'], + [HOT_KEY_NAME.LEFT]: ['left'], + [HOT_KEY_NAME.RIGHT]: ['right'], + [HOT_KEY_NAME.FIND_REPLACE]: ['mod+f'], +}; + +const replaceModifier = (hotkey: string) => { + return hotkey.replace('mod', getModifier()).replace('control', 'ctrl'); +}; + +/** + * Create a hotkey checker. + * @example trigger strike through when user press "Cmd + Shift + S" or "Cmd + Shift + X" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName]; + + return (event: KeyboardEvent) => { + return hotkeys.some((hotkey) => { + return isHotkey(hotkey, event); + }); + }; +}; + +/** + * Create a hotkey label. + * eg. "Ctrl + B / ⌘ + B" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { + const keys = customHotKeys || defaultHotKeys; + const hotkeys = keys[hotkeyName].map((key) => replaceModifier(key)); + + return hotkeys + .map((hotkey) => + hotkey + .split('+') + .map((key) => { + return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1); + }) + .join(' + ') + ) + .join(' / '); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts new file mode 100644 index 0000000000..6e5d22ccda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts @@ -0,0 +1,45 @@ +const romanMap: [number, string][] = [ + [1000, 'M'], + [900, 'CM'], + [500, 'D'], + [400, 'CD'], + [100, 'C'], + [90, 'XC'], + [50, 'L'], + [40, 'XL'], + [10, 'X'], + [9, 'IX'], + [5, 'V'], + [4, 'IV'], + [1, 'I'], +]; + +export function romanize(num: number): string { + let result = ''; + let nextNum = num; + + for (const [value, symbol] of romanMap) { + const count = Math.floor(nextNum / value); + + nextNum -= value * count; + result += symbol.repeat(count); + if (nextNum === 0) break; + } + + return result; +} + +export function letterize(num: number): string { + let nextNum = num; + let letters = ''; + + while (nextNum > 0) { + nextNum--; + const letter = String.fromCharCode((nextNum % 26) + 'a'.charCodeAt(0)); + + letters = letter + letters; + nextNum = Math.floor(nextNum / 26); + } + + return letters; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts new file mode 100644 index 0000000000..daccf21d0a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/log.ts @@ -0,0 +1,20 @@ +export class Log { + static error(...msg: unknown[]) { + console.error(...msg); + } + static info(...msg: unknown[]) { + console.info(...msg); + } + + static debug(...msg: unknown[]) { + console.debug(...msg); + } + + static trace(...msg: unknown[]) { + console.trace(...msg); + } + + static warn(...msg: unknown[]) { + console.warn(...msg); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts new file mode 100644 index 0000000000..94e2cf94d5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -0,0 +1,168 @@ +import { ThemeOptions } from '@mui/material'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export const getDesignTokens = (isDark: boolean): ThemeOptions => { + return { + typography: { + fontFamily: ['Poppins'].join(','), + fontSize: 12, + button: { + textTransform: 'none', + }, + }, + components: { + MuiMenuItem: { + defaultProps: { + sx: { + '&.Mui-selected.Mui-focusVisible': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&.Mui-focusVisible': { + backgroundColor: 'unset', + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + borderRadius: '4px', + padding: '2px', + }, + }, + }, + MuiButton: { + styleOverrides: { + contained: { + color: 'var(--content-on-fill)', + boxShadow: 'var(--shadow)', + }, + containedPrimary: { + '&:hover': { + backgroundColor: 'var(--fill-default)', + }, + }, + containedInherit: { + color: 'var(--text-title)', + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)', + '&:hover': { + backgroundColor: 'var(--bg-body)', + boxShadow: 'var(--shadow)', + }, + }, + outlinedInherit: { + color: 'var(--text-title)', + borderColor: 'var(--line-border)', + '&:hover': { + boxShadow: 'var(--shadow)', + }, + }, + }, + }, + MuiButtonBase: { + defaultProps: { + sx: { + '&.Mui-selected:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + }, + }, + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&:active': { + backgroundColor: 'var(--fill-list-hover)', + }, + borderRadius: '4px', + padding: '2px', + boxShadow: 'none', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiDialog: { + defaultProps: { + sx: { + '& .MuiBackdrop-root': { + backgroundColor: 'var(--bg-mask)', + }, + }, + }, + }, + + MuiTooltip: { + styleOverrides: { + arrow: { + color: 'var(--bg-tips)', + }, + tooltip: { + backgroundColor: 'var(--bg-tips)', + color: 'var(--text-title)', + fontSize: '0.85rem', + borderRadius: '8px', + fontWeight: 400, + }, + }, + }, + MuiInputBase: { + styleOverrides: { + input: { + backgroundColor: 'transparent !important', + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'var(--line-divider)', + }, + }, + }, + }, + palette: { + mode: isDark ? 'dark' : 'light', + primary: { + main: '#00BCF0', + dark: '#00BCF0', + }, + error: { + main: '#FB006D', + dark: '#D32772', + }, + warning: { + main: '#FFC107', + dark: '#E9B320', + }, + info: { + main: '#00BCF0', + dark: '#2E9DBB', + }, + success: { + main: '#66CF80', + dark: '#3BA856', + }, + text: { + primary: isDark ? '#E2E9F2' : '#333333', + secondary: isDark ? '#7B8A9D' : '#828282', + disabled: isDark ? '#363D49' : '#F2F2F2', + }, + divider: isDark ? '#59647A' : '#BDBDBD', + background: { + default: isDark ? '#1A202C' : '#FFFFFF', + paper: isDark ? '#1A202C' : '#FFFFFF', + }, + }, + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts new file mode 100644 index 0000000000..d854be5211 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts @@ -0,0 +1,23 @@ +import { open as openWindow } from '@tauri-apps/api/shell'; + +const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\S*)*\/?(\?[=&\w.%-]*)?(#[\w.\-!~*'()]*)?$/; +const ipPattern = /^(https?:\/\/)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})?$/; + +export function isUrl(str: string) { + return urlPattern.test(str) || ipPattern.test(str); +} + +export function openUrl(str: string) { + if (isUrl(str)) { + const linkPrefix = ['http://', 'https://', 'file://', 'ftp://', 'ftps://', 'mailto:']; + + if (linkPrefix.some((prefix) => str.startsWith(prefix))) { + void openWindow(str); + } else { + void openWindow('https://' + str); + } + } else { + // open google search + void openWindow('https://www.google.com/search?q=' + encodeURIComponent(str)); + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts new file mode 100644 index 0000000000..afcd7a32b4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/tool.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * Creates an interval that repeatedly calls the given function with a specified delay. + * + * @param {Function} fn - The function to be called repeatedly. + * @param {number} [delay] - The delay between function calls in milliseconds. + * @param {Object} [options] - Additional options for the interval. + * @param {boolean} [options.immediate] - Whether to immediately call the function when the interval is created. Default is true. + * + * @return {Function} - The function that runs the interval. + * @return {Function.cancel} - A method to cancel the interval. + * + * @example + * const log = interval((message) => console.log(message), 1000); + * + * log('foo'); // prints 'foo' every second. + * + * log('bar'); // change to prints 'bar' every second. + * + * log.cancel(); // stops the interval. + */ +export function interval any = (...args: any[]) => any>( + fn: T, + delay?: number, + options?: { immediate?: boolean } +): T & { cancel: () => void } { + const { immediate = true } = options || {}; + let intervalId: NodeJS.Timer | null = null; + let parameters: any[] = []; + + function run(...args: Parameters) { + parameters = args; + + if (intervalId !== null) { + return; + } + + immediate && fn.apply(undefined, parameters); + intervalId = setInterval(() => { + fn.apply(undefined, parameters); + }, delay); + } + + function cancel() { + if (intervalId === null) { + return; + } + + clearInterval(intervalId); + intervalId = null; + parameters = []; + } + + run.cancel = cancel; + return run as T & { cancel: () => void }; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts new file mode 100644 index 0000000000..22213ac8b3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/upload_image.ts @@ -0,0 +1,9 @@ +export const MAX_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB +export const ALLOWED_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp']; +export const IMAGE_DIR = 'images'; + +export function getFileName(url: string) { + const [...parts] = url.split('/'); + + return parts.pop() ?? url; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx new file mode 100644 index 0000000000..004fc4355b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DatabasePage.tsx @@ -0,0 +1,24 @@ +import { useParams } from 'react-router-dom'; +import { ViewIdProvider } from '$app/hooks'; +import { Database, DatabaseTitle, useSelectDatabaseView } from '../components/database'; + +export const DatabasePage = () => { + const viewId = useParams().id; + + const { selectedViewId, onChange } = useSelectDatabaseView({ + viewId, + }); + + if (!viewId) { + return null; + } + + return ( +
+ + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx new file mode 100644 index 0000000000..03ba493c10 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/views/DocumentPage.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { Document } from '$app/components/document'; + +function DocumentPage() { + const params = useParams(); + + const documentId = params.id; + + if (!documentId) return null; + return ; +} + +export default DocumentPage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/views/TrashPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/views/TrashPage.tsx new file mode 100644 index 0000000000..78baa9872d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/views/TrashPage.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import Trash from '$app/components/trash/Trash'; + +function TrashPage() { + return ( +
+ +
+ ); +} + +export default TrashPage; diff --git a/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts b/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts new file mode 100644 index 0000000000..b1f45c7866 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/frontend/appflowy_tauri/src/main.tsx b/frontend/appflowy_tauri/src/main.tsx new file mode 100644 index 0000000000..e53dc96c43 --- /dev/null +++ b/frontend/appflowy_tauri/src/main.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './appflowy_app/App'; +import './styles/tailwind.css'; +import './styles/font.css'; +import './styles/template.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/frontend/appflowy_tauri/src/services/backend/index.ts b/frontend/appflowy_tauri/src/services/backend/index.ts new file mode 100644 index 0000000000..1cc17c1e1b --- /dev/null +++ b/frontend/appflowy_tauri/src/services/backend/index.ts @@ -0,0 +1,8 @@ +export * from "./models/flowy-user"; +export * from "./models/flowy-database2"; +export * from "./models/flowy-folder"; +export * from "./models/flowy-document"; +export * from "./models/flowy-error"; +export * from "./models/flowy-config"; +export * from "./models/flowy-date"; +export * from "./models/flowy-search"; diff --git a/frontend/appflowy_tauri/src/styles/font.css b/frontend/appflowy_tauri/src/styles/font.css new file mode 100644 index 0000000000..514b5a6e38 --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/font.css @@ -0,0 +1,125 @@ +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Thin.ttf') format('truetype'); + font-weight: 100; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-ThinItalic.ttf') format('truetype'); + font-weight: 100; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-ExtraLight.ttf') format('truetype'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf') format('truetype'); + font-weight: 200; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Light.ttf') format('truetype'); + font-weight: 300; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-LightItalic.ttf') format('truetype'); + font-weight: 300; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Italic.ttf') format('truetype'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Medium.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-MediumItalic.ttf') format('truetype'); + font-weight: 500; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-SemiBold.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf') format('truetype'); + font-weight: 600; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Bold.ttf') format('truetype'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-BoldItalic.ttf') format('truetype'); + font-weight: 700; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-ExtraBold.ttf') format('truetype'); + font-weight: 800; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf') format('truetype'); + font-weight: 800; + font-style: italic; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-Black.ttf') format('truetype'); + font-weight: 900; + font-style: normal; +} + +@font-face { + font-family: 'Poppins'; + src: url('/google_fonts/Poppins/Poppins-BlackItalic.ttf') format('truetype'); + font-weight: 900; + font-style: italic; +} diff --git a/frontend/appflowy_tauri/src/styles/tailwind.css b/frontend/appflowy_tauri/src/styles/tailwind.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css new file mode 100644 index 0000000000..1bff6bdc76 --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -0,0 +1,60 @@ +@import './variables/index.css'; + +* { + margin: 0; + padding: 0; +} + +/* stop body from scrolling */ +html, +body { + margin: 0; + height: 100%; + overflow: hidden; +} +[contenteditable] { + -webkit-tap-highlight-color: transparent; +} +input, +textarea { + outline: 0; + background: transparent; +} + +body { + font-family: Poppins, serif; +} + +::-webkit-scrollbar { + width: 8px; +} + + + +:root[data-dark-mode=true] body { + scrollbar-color: #fff var(--bg-body); +} + +body { + scrollbar-track-color: var(--bg-body); + scrollbar-shadow-color: var(--bg-body); +} + + +.btn { + @apply rounded-xl border border-line-divider px-4 py-3; +} + +.btn-primary { + @apply bg-fill-default text-text-title hover:bg-fill-list-hover; +} + +.input { + @apply rounded-xl border border-line-divider px-[18px] py-[14px] text-sm; +} + + +th { + @apply text-left font-normal; +} + diff --git a/frontend/appflowy_tauri/src/styles/variables/dark.variables.css b/frontend/appflowy_tauri/src/styles/variables/dark.variables.css new file mode 100644 index 0000000000..ca7544687b --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/variables/dark.variables.css @@ -0,0 +1,121 @@ +/** +* Do not edit directly +* Generated on Tue, 19 Mar 2024 03:48:58 GMT +* Generated from $pnpm css:variables +*/ + +:root[data-dark-mode=true] { + --base-light-neutral-50: #f9fafd; + --base-light-neutral-100: #edeef2; + --base-light-neutral-200: #e2e4eb; + --base-light-neutral-300: #f2f2f2; + --base-light-neutral-400: #e0e0e0; + --base-light-neutral-500: #bdbdbd; + --base-light-neutral-600: #828282; + --base-light-neutral-700: #4f4f4f; + --base-light-neutral-800: #333333; + --base-light-neutral-900: #1f2329; + --base-light-neutral-1000: #000000; + --base-light-neutral-00: #ffffff; + --base-light-blue-50: #f2fcff; + --base-light-blue-100: #e0f8ff; + --base-light-blue-200: #a6ecff; + --base-light-blue-300: #52d1f4; + --base-light-blue-400: #00bcf0; + --base-light-blue-500: #05ade2; + --base-light-blue-600: #009fd1; + --base-light-color-deep-red: #fb006d; + --base-light-color-deep-yellow: #ffd667; + --base-light-color-deep-green: #66cf80; + --base-light-color-deep-blue: #00bcf0; + --base-light-color-light-purple: #e8e0ff; + --base-light-color-light-pink: #ffe7ee; + --base-light-color-light-orange: #ffefe3; + --base-light-color-light-yellow: #fff2cd; + --base-light-color-light-lime: #f5ffdc; + --base-light-color-light-green: #ddffd6; + --base-light-color-light-aqua: #defff1; + --base-light-color-light-blue: #e1fbff; + --base-light-color-light-red: #ffdddd; + --base-black-neutral-100: #252F41; + --base-black-neutral-200: #313c51; + --base-black-neutral-300: #3c4557; + --base-black-neutral-400: #525A69; + --base-black-neutral-500: #59647a; + --base-black-neutral-600: #87A0BF; + --base-black-neutral-700: #99a6b8; + --base-black-neutral-800: #e2e9f2; + --base-black-neutral-900: #eff4fb; + --base-black-neutral-1000: #ffffff; + --base-black-neutral-n50: #232b38; + --base-black-neutral-n00: #1a202c; + --base-black-blue-50: #232b38; + --base-black-blue-100: #005174; + --base-black-blue-200: #a6ecff; + --base-black-blue-300: #52d1f4; + --base-black-blue-400: #00bcf0; + --base-black-blue-500: #05ade2; + --base-black-blue-600: #009fd1; + --base-black-color-deep-red: #d32772; + --base-black-color-deep-yellow: #e9b320; + --base-black-color-deep-green: #3ba856; + --base-black-color-deep-blue: #2e9dbb; + --base-black-color-light-purple: #4D4078; + --base-black-color-light-blue: #2C3B58; + --base-black-color-light-green: #3C5133; + --base-black-color-light-yellow: #695E3E; + --base-black-color-light-pink: #5E3C5E; + --base-black-color-light-red: #56363F; + --base-black-color-light-aqua: #1B3849; + --base-black-color-light-lime: #394027; + --base-black-color-light-orange: #5E3C3C; + --base-else-brand: #2c144b; + --text-title: #e2e9f2; + --text-caption: #87A0BF; + --text-placeholder: #3c4557; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #005174; + --icon-primary: #e2e9f2; + --icon-secondary: #59647a; + --icon-disabled: #525A69; + --icon-on-toolbar: white; + --line-border: #59647a; + --line-divider: #252F41; + --line-on-toolbar: #99a6b8; + --fill-default: #00bcf0; + --fill-hover: #005174; + --fill-toolbar: #0F111C; + --fill-selector: #232b38; + --fill-list-active: #3c4557; + --fill-list-hover: #005174; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #005174; + --content-on-fill: #1a202c; + --content-on-tag: #99a6b8; + --content-blue-50: #232b38; + --bg-body: #1a202c; + --bg-base: #232b38; + --bg-mask: rgba(0,0,0,0.7); + --bg-tips: #005174; + --bg-brand: #2c144b; + --function-error: #d32772; + --function-warning: #e9b320; + --function-success: #3ba856; + --function-info: #2e9dbb; + --tint-red: #56363F; + --tint-green: #3C5133; + --tint-purple: #4D4078; + --tint-blue: #2C3B58; + --tint-yellow: #695E3E; + --tint-pink: #5E3C5E; + --tint-lime: #394027; + --tint-aqua: #1B3849; + --tint-orange: #5E3C3C; + --shadow: 0px 0px 25px 0px rgba(0,0,0,0.3); + --scrollbar-track: #252F41; + --scrollbar-thumb: #3c4557; +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/styles/variables/index.css b/frontend/appflowy_tauri/src/styles/variables/index.css new file mode 100644 index 0000000000..08d6a948f1 --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/variables/index.css @@ -0,0 +1,7 @@ +@import "./light.variables.css"; +@import "./dark.variables.css"; + +:root { + /* resize popover shadow */ + --shadow-resize-popover: 0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12); +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/styles/variables/light.variables.css b/frontend/appflowy_tauri/src/styles/variables/light.variables.css new file mode 100644 index 0000000000..26acc76f0a --- /dev/null +++ b/frontend/appflowy_tauri/src/styles/variables/light.variables.css @@ -0,0 +1,124 @@ +/** +* Do not edit directly +* Generated on Tue, 19 Mar 2024 03:48:58 GMT +* Generated from $pnpm css:variables +*/ + +:root { + --base-light-neutral-50: #f9fafd; + --base-light-neutral-100: #edeef2; + --base-light-neutral-200: #e2e4eb; + --base-light-neutral-300: #f2f2f2; + --base-light-neutral-400: #e0e0e0; + --base-light-neutral-500: #bdbdbd; + --base-light-neutral-600: #828282; + --base-light-neutral-700: #4f4f4f; + --base-light-neutral-800: #333333; + --base-light-neutral-900: #1f2329; + --base-light-neutral-1000: #000000; + --base-light-neutral-00: #ffffff; + --base-light-blue-50: #f2fcff; + --base-light-blue-100: #e0f8ff; + --base-light-blue-200: #a6ecff; + --base-light-blue-300: #52d1f4; + --base-light-blue-400: #00bcf0; + --base-light-blue-500: #05ade2; + --base-light-blue-600: #009fd1; + --base-light-color-deep-red: #fb006d; + --base-light-color-deep-yellow: #ffd667; + --base-light-color-deep-green: #66cf80; + --base-light-color-deep-blue: #00bcf0; + --base-light-color-light-purple: #e8e0ff; + --base-light-color-light-pink: #ffe7ee; + --base-light-color-light-orange: #ffefe3; + --base-light-color-light-yellow: #fff2cd; + --base-light-color-light-lime: #f5ffdc; + --base-light-color-light-green: #ddffd6; + --base-light-color-light-aqua: #defff1; + --base-light-color-light-blue: #e1fbff; + --base-light-color-light-red: #ffdddd; + --base-black-neutral-100: #252F41; + --base-black-neutral-200: #313c51; + --base-black-neutral-300: #3c4557; + --base-black-neutral-400: #525A69; + --base-black-neutral-500: #59647a; + --base-black-neutral-600: #87A0BF; + --base-black-neutral-700: #99a6b8; + --base-black-neutral-800: #e2e9f2; + --base-black-neutral-900: #eff4fb; + --base-black-neutral-1000: #ffffff; + --base-black-neutral-n50: #232b38; + --base-black-neutral-n00: #1a202c; + --base-black-blue-50: #232b38; + --base-black-blue-100: #005174; + --base-black-blue-200: #a6ecff; + --base-black-blue-300: #52d1f4; + --base-black-blue-400: #00bcf0; + --base-black-blue-500: #05ade2; + --base-black-blue-600: #009fd1; + --base-black-color-deep-red: #d32772; + --base-black-color-deep-yellow: #e9b320; + --base-black-color-deep-green: #3ba856; + --base-black-color-deep-blue: #2e9dbb; + --base-black-color-light-purple: #4D4078; + --base-black-color-light-blue: #2C3B58; + --base-black-color-light-green: #3C5133; + --base-black-color-light-yellow: #695E3E; + --base-black-color-light-pink: #5E3C5E; + --base-black-color-light-red: #56363F; + --base-black-color-light-aqua: #1B3849; + --base-black-color-light-lime: #394027; + --base-black-color-light-orange: #5E3C3C; + --base-else-brand: #2c144b; + --text-title: #333333; + --text-caption: #828282; + --text-placeholder: #bdbdbd; + --text-disabled: #e0e0e0; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #e0f8ff; + --icon-primary: #333333; + --icon-secondary: #59647a; + --icon-disabled: #e0e0e0; + --icon-on-toolbar: #ffffff; + --line-border: #bdbdbd; + --line-divider: #edeef2; + --line-on-toolbar: #4f4f4f; + --fill-toolbar: #333333; + --fill-default: #00bcf0; + --fill-hover: #52d1f4; + --fill-pressed: #009fd1; + --fill-active: #e0f8ff; + --fill-list-hover: #e0f8ff; + --fill-list-active: #edeef2; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #e0f8ff; + --content-blue-50: #f2fcff; + --content-on-fill-hover: #00bcf0; + --content-on-fill: #ffffff; + --content-on-tag: #4f4f4f; + --bg-body: #ffffff; + --bg-base: #f9fafd; + --bg-mask: rgba(0,0,0,0.55); + --bg-tips: #e0f8ff; + --bg-brand: #2c144b; + --function-error: #fb006d; + --function-waring: #ffd667; + --function-success: #66cf80; + --function-info: #00bcf0; + --tint-purple: #e8e0ff; + --tint-pink: #ffe7ee; + --tint-red: #ffdddd; + --tint-lime: #f5ffdc; + --tint-green: #ddffd6; + --tint-aqua: #defff1; + --tint-blue: #e1fbff; + --tint-orange: #ffefe3; + --tint-yellow: #fff2cd; + --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); + --scrollbar-thumb: #bdbdbd; + --scrollbar-track: #edeef2; +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/tests/helpers/init.ts b/frontend/appflowy_tauri/src/tests/helpers/init.ts new file mode 100644 index 0000000000..cb0ff5c3b5 --- /dev/null +++ b/frontend/appflowy_tauri/src/tests/helpers/init.ts @@ -0,0 +1 @@ +export {}; diff --git a/frontend/appflowy_tauri/style-dictionary/config.cjs b/frontend/appflowy_tauri/style-dictionary/config.cjs new file mode 100644 index 0000000000..10d7084060 --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/config.cjs @@ -0,0 +1,114 @@ +const StyleDictionary = require('style-dictionary'); +const fs = require('fs'); +const path = require('path'); + +// Add comment header to generated files +StyleDictionary.registerFormat({ + name: 'css/variables', + formatter: function(dictionary, config) { + const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + const allProperties = dictionary.allProperties; + const properties = allProperties.map(prop => { + const { name, value } = prop; + return ` --${name}: ${value};` + }).join('\n'); + // generate tailwind config + generateTailwindConfig(allProperties); + return header + `:root${this.selector} {\n${properties}\n}` + } +}); + +// expand shadow tokens into a single string +StyleDictionary.registerTransform({ + name: 'shadow/spreadShadow', + type: 'value', + matcher: function (prop) { + return prop.type === 'boxShadow'; + }, + transformer: function (prop) { + // destructure shadow values from original token value + const { x, y, blur, spread, color } = prop.original.value; + + return `${x}px ${y}px ${blur}px ${spread}px ${color}`; + }, +}); + +const transforms = ['attribute/cti', 'name/cti/kebab', 'shadow/spreadShadow']; + +// Generate Light CSS variables +StyleDictionary.extend({ + source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/light.json'], + platforms: { + css: { + transformGroup: 'css', + buildPath: './src/styles/variables/', + files: [ + { + format: 'css/variables', + destination: 'light.variables.css', + selector: '', + options: { + outputReferences: true + } + }, + ], + transforms, + }, + }, +}).buildAllPlatforms(); + +// Generate Dark CSS variables +StyleDictionary.extend({ + source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/dark.json'], + platforms: { + css: { + transformGroup: 'css', + buildPath: './src/styles/variables/', + files: [ + { + format: 'css/variables', + destination: 'dark.variables.css', + selector: '[data-dark-mode=true]', + }, + ], + transforms, + }, + }, +}).buildAllPlatforms(); + + +function set(obj, path, value) { + const lastKey = path.pop(); + const lastObj = path.reduce((obj, key) => + obj[key] = obj[key] || {}, + obj); + lastObj[lastKey] = value; +} + +function writeFile (file, data) { + const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + const exportString = `module.exports = ${JSON.stringify(data, null, 2)}`; + fs.writeFileSync(path.join(__dirname, file), header + exportString); +} + +function generateTailwindConfig(allProperties) { + const tailwindColors = {}; + const tailwindBoxShadow = {}; + allProperties.forEach(prop => { + const { path, type, name, value } = prop; + if (path[0] === 'Base') { + return; + } + if (type === 'color') { + if (name.includes('fill')) { + console.log(prop); + } + set(tailwindColors, path, `var(--${name})`); + } + if (type === 'boxShadow') { + set(tailwindBoxShadow, ['md'], `var(--${name})`); + } + }); + writeFile('./tailwind/colors.cjs', tailwindColors); + writeFile('./tailwind/box-shadow.cjs', tailwindBoxShadow); +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs new file mode 100644 index 0000000000..e9d8024320 --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs @@ -0,0 +1,9 @@ +/** +* Do not edit directly +* Generated on Tue, 19 Mar 2024 03:48:58 GMT +* Generated from $pnpm css:variables +*/ + +module.exports = { + "md": "var(--shadow)" +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs new file mode 100644 index 0000000000..bfa25fa56f --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs @@ -0,0 +1,75 @@ +/** +* Do not edit directly +* Generated on Tue, 19 Mar 2024 03:48:58 GMT +* Generated from $pnpm css:variables +*/ + +module.exports = { + "text": { + "title": "var(--text-title)", + "caption": "var(--text-caption)", + "placeholder": "var(--text-placeholder)", + "link-default": "var(--text-link-default)", + "link-hover": "var(--text-link-hover)", + "link-pressed": "var(--text-link-pressed)", + "link-disabled": "var(--text-link-disabled)" + }, + "icon": { + "primary": "var(--icon-primary)", + "secondary": "var(--icon-secondary)", + "disabled": "var(--icon-disabled)", + "on-toolbar": "var(--icon-on-toolbar)" + }, + "line": { + "border": "var(--line-border)", + "divider": "var(--line-divider)", + "on-toolbar": "var(--line-on-toolbar)" + }, + "fill": { + "default": "var(--fill-default)", + "hover": "var(--fill-hover)", + "toolbar": "var(--fill-toolbar)", + "selector": "var(--fill-selector)", + "list": { + "active": "var(--fill-list-active)", + "hover": "var(--fill-list-hover)" + } + }, + "content": { + "blue-400": "var(--content-blue-400)", + "blue-300": "var(--content-blue-300)", + "blue-600": "var(--content-blue-600)", + "blue-100": "var(--content-blue-100)", + "on-fill": "var(--content-on-fill)", + "on-tag": "var(--content-on-tag)", + "blue-50": "var(--content-blue-50)" + }, + "bg": { + "body": "var(--bg-body)", + "base": "var(--bg-base)", + "mask": "var(--bg-mask)", + "tips": "var(--bg-tips)", + "brand": "var(--bg-brand)" + }, + "function": { + "error": "var(--function-error)", + "warning": "var(--function-warning)", + "success": "var(--function-success)", + "info": "var(--function-info)" + }, + "tint": { + "red": "var(--tint-red)", + "green": "var(--tint-green)", + "purple": "var(--tint-purple)", + "blue": "var(--tint-blue)", + "yellow": "var(--tint-yellow)", + "pink": "var(--tint-pink)", + "lime": "var(--tint-lime)", + "aqua": "var(--tint-aqua)", + "orange": "var(--tint-orange)" + }, + "scrollbar": { + "track": "var(--scrollbar-track)", + "thumb": "var(--scrollbar-thumb)" + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json new file mode 100644 index 0000000000..fb58a867b1 --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -0,0 +1,290 @@ +{ + "Base": { + "Light": { + "neutral": { + "50": { + "value": "#f9fafd", + "type": "color" + }, + "100": { + "value": "#dadbdd", + "type": "color" + }, + "200": { + "value": "#e2e4eb", + "type": "color" + }, + "300": { + "value": "#f2f2f2", + "type": "color" + }, + "400": { + "value": "#e0e0e0", + "type": "color" + }, + "500": { + "value": "#bdbdbd", + "type": "color" + }, + "600": { + "value": "#828282", + "type": "color" + }, + "700": { + "value": "#4f4f4f", + "type": "color" + }, + "800": { + "value": "#333333", + "type": "color" + }, + "900": { + "value": "#1f2329", + "type": "color" + }, + "1000": { + "value": "#000000", + "type": "color" + }, + "00": { + "value": "#ffffff", + "type": "color" + } + }, + "blue": { + "50": { + "value": "#f2fcff", + "type": "color" + }, + "100": { + "value": "#e0f8ff", + "type": "color" + }, + "200": { + "value": "#a6ecff", + "type": "color" + }, + "300": { + "value": "#52d1f4", + "type": "color" + }, + "400": { + "value": "#00bcf0", + "type": "color" + }, + "500": { + "value": "#05ade2", + "type": "color" + }, + "600": { + "value": "#009fd1", + "type": "color" + } + }, + "color": { + "deep": { + "red": { + "value": "#fb006d", + "type": "color" + }, + "yellow": { + "value": "#ffd667", + "type": "color" + }, + "green": { + "value": "#66cf80", + "type": "color" + }, + "blue": { + "value": "#00bcf0", + "type": "color" + } + }, + "light": { + "purple": { + "value": "#e8e0ff", + "type": "color" + }, + "pink": { + "value": "#ffe7ee", + "type": "color" + }, + "orange": { + "value": "#ffefe3", + "type": "color" + }, + "yellow": { + "value": "#fff2cd", + "type": "color" + }, + "lime": { + "value": "#f5ffdc", + "type": "color" + }, + "green": { + "value": "#ddffd6", + "type": "color" + }, + "aqua": { + "value": "#defff1", + "type": "color" + }, + "blue": { + "value": "#e1fbff", + "type": "color" + }, + "red": { + "value": "#ffdddd", + "type": "color" + } + } + } + }, + "black": { + "neutral": { + "100": { + "value": "#252F41", + "type": "color" + }, + "200": { + "value": "#313c51", + "type": "color" + }, + "300": { + "value": "#3c4557", + "type": "color" + }, + "400": { + "value": "#525A69", + "type": "color" + }, + "500": { + "value": "#59647a", + "type": "color" + }, + "600": { + "value": "#87A0BF", + "type": "color" + }, + "700": { + "value": "#99a6b8", + "type": "color" + }, + "800": { + "value": "#e2e9f2", + "type": "color" + }, + "900": { + "value": "#eff4fb", + "type": "color" + }, + "1000": { + "value": "#ffffff", + "type": "color" + }, + "N50": { + "value": "#232b38", + "type": "color" + }, + "N00": { + "value": "#1a202c", + "type": "color" + } + }, + "blue": { + "50": { + "value": "#232b38", + "type": "color" + }, + "100": { + "value": "#005174", + "type": "color" + }, + "200": { + "value": "#a6ecff", + "type": "color" + }, + "300": { + "value": "#52d1f4", + "type": "color" + }, + "400": { + "value": "#00bcf0", + "type": "color" + }, + "500": { + "value": "#05ade2", + "type": "color" + }, + "600": { + "value": "#009fd1", + "type": "color" + } + }, + "color": { + "deep": { + "red": { + "value": "#d32772", + "type": "color" + }, + "yellow": { + "value": "#e9b320", + "type": "color" + }, + "green": { + "value": "#3ba856", + "type": "color" + }, + "blue": { + "value": "#2e9dbb", + "type": "color" + } + }, + "light": { + "purple": { + "value": "#4D4078", + "type": "color" + }, + "blue": { + "value": "#2C3B58", + "type": "color" + }, + "green": { + "value": "#3C5133", + "type": "color" + }, + "yellow": { + "value": "#695E3E", + "type": "color" + }, + "pink": { + "value": "#5E3C5E", + "type": "color" + }, + "red": { + "value": "#56363F", + "type": "color" + }, + "aqua": { + "value": "#1B3849", + "type": "color" + }, + "lime": { + "value": "#394027", + "type": "color" + }, + "orange": { + "value": "#5E3C3C", + "type": "color" + } + } + } + }, + "else": { + "brand": { + "value": "#2c144b", + "type": "color" + } + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/dark.json b/frontend/appflowy_tauri/style-dictionary/tokens/dark.json new file mode 100644 index 0000000000..c67af7c9ec --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/tokens/dark.json @@ -0,0 +1,221 @@ +{ + "text": { + "title": { + "value": "{Base.black.neutral.800}", + "type": "color" + }, + "caption": { + "value": "{Base.black.neutral.600}", + "type": "color" + }, + "placeholder": { + "value": "{Base.black.neutral.300}", + "type": "color" + }, + "link-default": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "link-hover": { + "value": "{Base.black.blue.300}", + "type": "color" + }, + "link-pressed": { + "value": "{Base.black.blue.600}", + "type": "color" + }, + "link-disabled": { + "value": "{Base.black.blue.100}", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "{Base.black.neutral.800}", + "type": "color" + }, + "secondary": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.black.neutral.400}", + "type": "color" + }, + "on-toolbar": { + "value": "white", + "type": "color" + } + }, + "line": { + "border": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "divider": { + "value": "{Base.black.neutral.100}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.black.neutral.700}", + "type": "color" + } + }, + "fill": { + "default": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "hover": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "toolbar": { + "value": "#0F111C", + "type": "color" + }, + "selector": { + "value": "{Base.black.blue.50}", + "type": "color" + }, + "list": { + "active": { + "value": "{Base.black.neutral.300}", + "type": "color" + }, + "hover": { + "value": "{Base.black.blue.100}", + "type": "color" + } + } + }, + "content": { + "blue-400": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "blue-300": { + "value": "{Base.black.blue.300}", + "type": "color" + }, + "blue-600": { + "value": "{Base.black.blue.600}", + "type": "color" + }, + "blue-100": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "on-fill": { + "value": "{Base.black.neutral.N00}", + "type": "color" + }, + "on-tag": { + "value": "{Base.black.neutral.700}", + "type": "color" + }, + "blue-50": { + "value": "{Base.black.blue.50}", + "type": "color" + } + }, + "bg": { + "body": { + "value": "{Base.black.neutral.N00}", + "type": "color" + }, + "base": { + "value": "{Base.black.blue.50}", + "type": "color" + }, + "mask": { + "value": "rgba(0,0,0,0.7)", + "type": "color" + }, + "tips": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "brand": { + "value": "{Base.else.brand}", + "type": "color" + } + }, + "function": { + "error": { + "value": "{Base.black.color.deep.red}", + "type": "color" + }, + "warning": { + "value": "{Base.black.color.deep.yellow}", + "type": "color" + }, + "success": { + "value": "#3ba856", + "type": "color" + }, + "info": { + "value": "#2e9dbb", + "type": "color" + } + }, + "tint": { + "red": { + "value": "{Base.black.color.light.red}", + "type": "color" + }, + "green": { + "value": "{Base.black.color.light.green}", + "type": "color" + }, + "purple": { + "value": "{Base.black.color.light.purple}", + "type": "color" + }, + "blue": { + "value": "{Base.black.color.light.blue}", + "type": "color" + }, + "yellow": { + "value": "{Base.black.color.light.yellow}", + "type": "color" + }, + "pink": { + "value": "{Base.black.color.light.pink}", + "type": "color" + }, + "lime": { + "value": "{Base.black.color.light.lime}", + "type": "color" + }, + "aqua": { + "value": "{Base.black.color.light.aqua}", + "type": "color" + }, + "orange": { + "value": "{Base.black.color.light.orange}", + "type": "color" + } + }, + "shadow": { + "value": { + "x": "0", + "y": "0", + "blur": "25", + "spread": "0", + "color": "rgba(0,0,0,0.3)", + "type": "innerShadow" + }, + "type": "boxShadow" + }, + "scrollbar": { + "track": { + "value": "{Base.black.neutral.100}", + "type": "color" + }, + "thumb": { + "value": "{Base.black.neutral.300}", + "type": "color" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/light.json b/frontend/appflowy_tauri/style-dictionary/tokens/light.json new file mode 100644 index 0000000000..173f3d35aa --- /dev/null +++ b/frontend/appflowy_tauri/style-dictionary/tokens/light.json @@ -0,0 +1,233 @@ +{ + "text": { + "title": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "caption": { + "value": "{Base.Light.neutral.600}", + "type": "color" + }, + "placeholder": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.Light.neutral.400}", + "type": "color" + }, + "link-default": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "link-hover": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "link-pressed": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "link-disabled": { + "value": "{Base.Light.blue.100}", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "secondary": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.Light.neutral.400}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.Light.neutral.00}", + "type": "color" + } + }, + "line": { + "border": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "divider": { + "value": "{Base.Light.neutral.100}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.Light.neutral.700}", + "type": "color" + } + }, + "fill": { + "toolbar": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "default": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "hover": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "pressed": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "active": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "list": { + "hover": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "active": { + "value": "{Base.Light.neutral.100}", + "type": "color" + } + } + }, + "content": { + "blue-400": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "blue-300": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "blue-600": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "blue-100": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "blue-50": { + "value": "{Base.Light.blue.50}", + "type": "color" + }, + "on-fill-hover": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "on-fill": { + "value": "{Base.Light.neutral.00}", + "type": "color" + }, + "on-tag": { + "value": "{Base.Light.neutral.700}", + "type": "color" + } + }, + "bg": { + "body": { + "value": "{Base.Light.neutral.00}", + "type": "color" + }, + "base": { + "value": "{Base.Light.neutral.50}", + "type": "color" + }, + "mask": { + "value": "rgba(0,0,0,0.55)", + "type": "color" + }, + "tips": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "brand": { + "value": "{Base.else.brand}", + "type": "color" + } + }, + "function": { + "error": { + "value": "{Base.Light.color.deep.red}", + "type": "color" + }, + "waring": { + "value": "{Base.Light.color.deep.yellow}", + "type": "color" + }, + "success": { + "value": "{Base.Light.color.deep.green}", + "type": "color" + }, + "info": { + "value": "{Base.Light.color.deep.blue}", + "type": "color" + } + }, + "tint": { + "purple": { + "value": "{Base.Light.color.light.purple}", + "type": "color" + }, + "pink": { + "value": "{Base.Light.color.light.pink}", + "type": "color" + }, + "red": { + "value": "{Base.Light.color.light.red}", + "type": "color" + }, + "lime": { + "value": "{Base.Light.color.light.lime}", + "type": "color" + }, + "green": { + "value": "{Base.Light.color.light.green}", + "type": "color" + }, + "aqua": { + "value": "{Base.Light.color.light.aqua}", + "type": "color" + }, + "blue": { + "value": "{Base.Light.color.light.blue}", + "type": "color" + }, + "orange": { + "value": "{Base.Light.color.light.orange}", + "type": "color" + }, + "yellow": { + "value": "{Base.Light.color.light.yellow}", + "type": "color" + } + }, + "shadow": { + "value": { + "x": "0", + "y": "0", + "blur": "10", + "spread": "0", + "color": "rgba(0,0,0,0.1)", + "type": "dropShadow" + }, + "type": "boxShadow" + }, + "scrollbar": { + "thumb": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "track": { + "value": "{Base.Light.neutral.100}", + "type": "color" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/tailwind.config.cjs b/frontend/appflowy_tauri/tailwind.config.cjs new file mode 100644 index 0000000000..06390d938f --- /dev/null +++ b/frontend/appflowy_tauri/tailwind.config.cjs @@ -0,0 +1,20 @@ +const colors = require('./style-dictionary/tailwind/colors.cjs'); +const boxShadow = require('./style-dictionary/tailwind/box-shadow.cjs'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + './node_modules/react-tailwindcss-datepicker/dist/index.esm.js', + ], + important: '#body', + darkMode: 'class', + theme: { + extend: { + colors, + boxShadow, + }, + }, + plugins: [], +}; diff --git a/frontend/appflowy_tauri/tsconfig.json b/frontend/appflowy_tauri/tsconfig.json new file mode 100644 index 0000000000..63b15b6039 --- /dev/null +++ b/frontend/appflowy_tauri/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["node", "jest"], + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + "$app/*": ["src/appflowy_app/*"], + "$app_reducers/*": ["src/appflowy_app/stores/reducers/*"], + "src/*": ["src/*"] + } + }, + "include": ["src", "vite.config.ts"], + "exclude": ["node_modules"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/appflowy_tauri/tsconfig.node.json b/frontend/appflowy_tauri/tsconfig.node.json new file mode 100644 index 0000000000..9d31e2aed9 --- /dev/null +++ b/frontend/appflowy_tauri/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/appflowy_tauri/vite.config.ts b/frontend/appflowy_tauri/vite.config.ts new file mode 100644 index 0000000000..b571cc40de --- /dev/null +++ b/frontend/appflowy_tauri/vite.config.ts @@ -0,0 +1,70 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + svgr({ + svgrOptions: { + prettier: false, + plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], + icon: true, + svgoConfig: { + multipass: true, + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, + svgProps: { + role: 'img', + }, + replaceAttrValues: { + '#333': 'currentColor', + }, + }, + }), + ], + + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + watch: { + ignored: ['**/__tests__/**'], + }, + }, + // to make use of `TAURI_DEBUG` and other env variables + // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand + envPrefix: ['VITE_', 'TAURI_'], + build: { + // Tauri supports es2021 + target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG, + }, + resolve: { + alias: [ + { find: 'src/', replacement: `${__dirname}/src/` }, + { find: '@/', replacement: `${__dirname}/src/` }, + { find: '$app/', replacement: `${__dirname}/src/appflowy_app/` }, + { find: '$app_reducers/', replacement: `${__dirname}/src/appflowy_app/stores/reducers/` }, + ], + }, + optimizeDeps: { + include: ['@mui/material/Tooltip'], + }, +}); diff --git a/frontend/appflowy_tauri/webdriver/selenium/package.json b/frontend/appflowy_tauri/webdriver/selenium/package.json new file mode 100644 index 0000000000..78bbd20aad --- /dev/null +++ b/frontend/appflowy_tauri/webdriver/selenium/package.json @@ -0,0 +1,13 @@ +{ + "name": "selenium", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "mocha" + }, + "dependencies": { + "chai": "^4.3.4", + "mocha": "^9.0.3", + "selenium-webdriver": "^4.0.0-beta.4" + } +} diff --git a/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs b/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs new file mode 100644 index 0000000000..7a57bdbbaf --- /dev/null +++ b/frontend/appflowy_tauri/webdriver/selenium/test/test.cjs @@ -0,0 +1,76 @@ +const os = require("os"); +const path = require("path"); +const { expect } = require("chai"); +const { spawn, spawnSync } = require("child_process"); +const { Builder, By, Capabilities, until } = require("selenium-webdriver"); +const { elementIsVisible, elementLocated } = require("selenium-webdriver/lib/until.js"); + +// create the path to the expected application binary +const application = path.resolve( + __dirname, + "..", + "..", + "..", + "src-tauri", + "target", + "release", + "appflowy_tauri" +); + +// keep track of the webdriver instance we create +let driver; + +// keep track of the tauri-driver process we start +let tauriDriver; + +before(async function() { + // set timeout to 2 minutes to allow the program to build if it needs to + this.timeout(120000); + + // ensure the program has been built + spawnSync("cargo", ["build", "--release"]); + + // start tauri-driver + tauriDriver = spawn( + path.resolve(os.homedir(), ".cargo", "bin", "tauri-driver"), + [], + { stdio: [null, process.stdout, process.stderr] } + ); + + const capabilities = new Capabilities(); + capabilities.set("tauri:options", { application }); + capabilities.setBrowserName("wry"); + + // start the webdriver client + driver = await new Builder() + .withCapabilities(capabilities) + .usingServer("http://localhost:4444/") + .build(); +}); + +after(async function() { + // stop the webdriver session + await driver.quit(); + + // kill the tauri-driver process + tauriDriver.kill(); +}); + +describe("AppFlowy Unit Test", () => { + it("should find get started button", async () => { + // should sign out if already sign in + const getStartedButton = await driver.wait(until.elementLocated(By.xpath("//*[@id=\"root\"]/form/div/div[3]"))); + getStartedButton.click(); + }); + + it("should get sign out button", async (done) => { + // const optionButton = await driver.wait(until.elementLocated(By.css('*[test-id=option-button]'))); + // const optionButton = await driver.wait(until.elementLocated(By.id('option-button'))); + // const optionButton = await driver.wait(until.elementLocated(By.css('[aria-label=option]'))); + + // Currently, only the find className is work + const optionButton = await driver.wait(until.elementLocated(By.className("relative h-8 w-8"))); + optionButton.click(); + await new Promise((resolve) => setTimeout(resolve, 4000)); + }); +}); diff --git a/frontend/appflowy_web/.eslintignore b/frontend/appflowy_web/.eslintignore new file mode 100644 index 0000000000..e0ff674834 --- /dev/null +++ b/frontend/appflowy_web/.eslintignore @@ -0,0 +1,7 @@ +src/services +src/styles +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json \ No newline at end of file diff --git a/frontend/appflowy_web/.eslintrc.cjs b/frontend/appflowy_web/.eslintrc.cjs new file mode 100644 index 0000000000..a1160f0bd3 --- /dev/null +++ b/frontend/appflowy_web/.eslintrc.cjs @@ -0,0 +1,73 @@ +module.exports = { + // https://eslint.org/docs/latest/use/configure/configuration-files + env: { + browser: true, + es6: true, + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname, + extraFileExtensions: ['.json'], + }, + plugins: ['@typescript-eslint', "react-hooks"], + rules: { + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/triple-slash-reference': 'error', + '@typescript-eslint/unified-signatures': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'off', + 'constructor-super': 'error', + eqeqeq: ['error', 'always'], + 'no-cond-assign': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-invalid-this': 'error', + 'no-new-wrappers': 'error', + 'no-param-reassign': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'no-void': 'off', + 'prefer-const': 'error', + 'prefer-spread': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + } + ], + 'padding-line-between-statements': [ + "error", + { blankLine: "always", prev: ["const", "let", "var"], next: "*"}, + { blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]}, + { blankLine: "always", prev: "import", next: "*" }, + { blankLine: "any", prev: "import", next: "import" }, + { blankLine: "always", prev: "block-like", next: "*" }, + { blankLine: "always", prev: "block", next: "*" }, + + ] + }, + ignorePatterns: ['src/**/*.test.ts', '**/__tests__/**/*.json', 'package.json'] +}; diff --git a/frontend/appflowy_web/.gitignore b/frontend/appflowy_web/.gitignore new file mode 100644 index 0000000000..d347429756 --- /dev/null +++ b/frontend/appflowy_web/.gitignore @@ -0,0 +1,29 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +wasm-libs/**/target +**/src/services/backend/models/ +**/src/services/backend/events/ +**/src/appflowy_app/i18n/translations/ diff --git a/frontend/appflowy_web/.prettierignore b/frontend/appflowy_web/.prettierignore new file mode 100644 index 0000000000..d515c1c2f2 --- /dev/null +++ b/frontend/appflowy_web/.prettierignore @@ -0,0 +1,19 @@ +.DS_Store +node_modules +/build +/public +/.svelte-kit +/package +/.vscode +.env +.env.* +!.env.example + +# rust and generated ts code +/src-tauri +/src/services + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/frontend/appflowy_web/.prettierrc.cjs b/frontend/appflowy_web/.prettierrc.cjs new file mode 100644 index 0000000000..f283db53a2 --- /dev/null +++ b/frontend/appflowy_web/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + arrowParens: 'always', + bracketSpacing: true, + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxBracketSameLine: false, + jsxSingleQuote: true, + printWidth: 121, + plugins: [require('prettier-plugin-tailwindcss')], + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + vueIndentScriptAndStyle: false, +}; diff --git a/frontend/appflowy_web/README.md b/frontend/appflowy_web/README.md new file mode 100644 index 0000000000..b92a4c4960 --- /dev/null +++ b/frontend/appflowy_web/README.md @@ -0,0 +1,45 @@ +# AppFlowy Web Project + +## Installation + +```bash +cd appflowy-web + +# Install dependencies +rm -rf node_modules && pnpm install +``` + +## Running the app + +```bash +# development +pnpm run dev + +# production mode +pnpm run build + +# generate wasm +pnpm run wasm +``` + + + +## Run tests in Chrome + +> Before executing the test, you need to install the [Chrome Driver](https://chromedriver.chromium.org/downloads). If +> you are using a Mac, you can easily install it using Homebrew. +> +> ```shell +> brew install chromedriver +> ``` + + +Go to `frontend/appflowy_web/wasm-libs` and run: +```shell +wasm-pack test --chrome +``` + +Run tests in headless Chrome +```shell +wasm-pack test --headless --chrome +``` \ No newline at end of file diff --git a/frontend/appflowy_web/index.html b/frontend/appflowy_web/index.html new file mode 100644 index 0000000000..e4b78eae12 --- /dev/null +++ b/frontend/appflowy_web/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/frontend/appflowy_web/package.json b/frontend/appflowy_web/package.json new file mode 100644 index 0000000000..faafdba154 --- /dev/null +++ b/frontend/appflowy_web/package.json @@ -0,0 +1,41 @@ +{ + "name": "appflowy_web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "build_release_wasm": "cd wasm-libs/af-wasm && wasm-pack build", + "build_dev_wasm": "cd wasm-libs/af-wasm && wasm-pack build --features=\"localhost_dev\"", + "dev": "pnpm run build_dev_wasm && vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "clean": "cargo make --cwd .. web_clean", + "test": "cargo test && wasm-pack test --headless", + "preview": "vite preview" + }, + "dependencies": { + "events": "^3.3.0", + "google-protobuf": "^3.21.2", + "protoc-gen-ts": "^0.8.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "ts-results": "^3.3.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@types/events": "^3.0.3", + "@types/node": "^20.10.6", + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.55.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.5", + "typescript": "^5.2.2", + "vite": "^5.0.8", + "vite-plugin-wasm": "^3.3.0", + "rimraf": "^5.0.5" + } +} diff --git a/frontend/appflowy_web/pnpm-lock.yaml b/frontend/appflowy_web/pnpm-lock.yaml new file mode 100644 index 0000000000..3c23a1ff65 --- /dev/null +++ b/frontend/appflowy_web/pnpm-lock.yaml @@ -0,0 +1,2133 @@ +lockfileVersion: '6.0' + +dependencies: + events: + specifier: ^3.3.0 + version: 3.3.0 + google-protobuf: + specifier: ^3.21.2 + version: 3.21.2 + protoc-gen-ts: + specifier: ^0.8.5 + version: 0.8.7 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + ts-results: + specifier: ^3.3.0 + version: 3.3.0 + uuid: + specifier: ^9.0.1 + version: 9.0.1 + +devDependencies: + '@types/events': + specifier: ^3.0.3 + version: 3.0.3 + '@types/node': + specifier: ^20.10.6 + version: 20.10.6 + '@types/react': + specifier: ^18.2.43 + version: 18.2.43 + '@types/react-dom': + specifier: ^18.2.17 + version: 18.2.17 + '@typescript-eslint/eslint-plugin': + specifier: ^6.14.0 + version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/parser': + specifier: ^6.14.0 + version: 6.14.0(eslint@8.55.0)(typescript@5.2.2) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.0.8) + eslint: + specifier: ^8.55.0 + version: 8.55.0 + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.55.0) + eslint-plugin-react-refresh: + specifier: ^0.4.5 + version: 0.4.5(eslint@8.55.0) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 + typescript: + specifier: ^5.2.2 + version: 5.2.2 + vite: + specifier: ^5.0.8 + version: 5.0.8(@types/node@20.10.6) + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.3.0(vite@5.0.8) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.2.1: + resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/compat-data@7.23.5: + resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.23.7: + resolution: {integrity: sha512-+UpDgowcmqe36d4NwqvKsyPMlOLNGMsfMmQ5WGCu+siCe3t3dfe9njrzGfdN4qq+bcNUt0+Vw6haRxBOycs4dw==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.23.7) + '@babel/helpers': 7.23.7 + '@babel/parser': 7.23.6 + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/generator@7.23.6: + resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + '@jridgewell/gen-mapping': 0.3.3 + '@jridgewell/trace-mapping': 0.3.20 + jsesc: 2.5.2 + dev: true + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.23.5 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.22.2 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-module-imports@7.22.15: + resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-plugin-utils@7.22.5: + resolution: {integrity: sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helpers@7.23.7: + resolution: {integrity: sha512-6AMnjCoC8wjqBzDHkuqpa7jAKwvMo4dC+lr/TFBz+ucfulO1XMpDnwWPGBNwClOKZ8h6xn5N81W/R5OrcKtCbQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.22.15 + '@babel/traverse': 7.23.7 + '@babel/types': 7.23.6 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@babel/parser@7.23.6: + resolution: {integrity: sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@babel/plugin-transform-react-jsx-self@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.23.3(@babel/core@7.23.7): + resolution: {integrity: sha512-91RS0MDnAWDNvGC6Wio5XYkyWI39FMFO+JK9+4AlgaTH+yWwVTsw7/sn6LK0lH7c5F+TFkpv/3LfCJ1Ydwof/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.23.7 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/template@7.22.15: + resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@babel/traverse@7.23.7: + resolution: {integrity: sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.23.5 + '@babel/generator': 7.23.6 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.23.6: + resolution: {integrity: sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.23.4 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + dev: true + + /@esbuild/aix-ppc64@0.19.11: + resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.11: + resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.11: + resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.11: + resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.11: + resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.11: + resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.11: + resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.11: + resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.11: + resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.11: + resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.11: + resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.11: + resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.11: + resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.11: + resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.11: + resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.11: + resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.11: + resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.11: + resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.11: + resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.11: + resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.11: + resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.11: + resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.11: + resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.55.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.55.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.55.0: + resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@jridgewell/gen-mapping@0.3.3: + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.20 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/set-array@1.1.2: + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.20: + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.16.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm-eabi@4.9.2: + resolution: {integrity: sha512-RKzxFxBHq9ysZ83fn8Iduv3A283K7zPPYuhL/z9CQuyFrjwpErJx0h4aeb/bnJ+q29GRLgJpY66ceQ/Wcsn3wA==} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.9.2: + resolution: {integrity: sha512-yZ+MUbnwf3SHNWQKJyWh88ii2HbuHCFQnAYTeeO1Nb8SyEiWASEi5dQUygt3ClHWtA9My9RQAYkjvrsZ0WK8Xg==} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.9.2: + resolution: {integrity: sha512-vqJ/pAUh95FLc/G/3+xPqlSBgilPnauVf2EXOQCZzhZJCXDXt/5A8mH/OzU6iWhb3CNk5hPJrh8pqJUPldN5zw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.9.2: + resolution: {integrity: sha512-otPHsN5LlvedOprd3SdfrRNhOahhVBwJpepVKUN58L0RnC29vOAej1vMEaVU6DadnpjivVsNTM5eNt0CcwTahw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.9.2: + resolution: {integrity: sha512-ewG5yJSp+zYKBYQLbd1CUA7b1lSfIdo9zJShNTyc2ZP1rcPrqyZcNlsHgs7v1zhgfdS+kW0p5frc0aVqhZCiYQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.9.2: + resolution: {integrity: sha512-pL6QtV26W52aCWTG1IuFV3FMPL1m4wbsRG+qijIvgFO/VBsiXJjDPE/uiMdHBAO6YcpV4KvpKtd0v3WFbaxBtg==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.9.2: + resolution: {integrity: sha512-On+cc5EpOaTwPSNetHXBuqylDW+765G/oqB9xGmWU3npEhCh8xu0xqHGUA+4xwZLqBbIZNcBlKSIYfkBm6ko7g==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.9.2: + resolution: {integrity: sha512-Wnx/IVMSZ31D/cO9HSsU46FjrPWHqtdF8+0eyZ1zIB5a6hXaZXghUKpRrC4D5DcRTZOjml2oBhXoqfGYyXKipw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.9.2: + resolution: {integrity: sha512-ym5x1cj4mUAMBummxxRkI4pG5Vht1QMsJexwGP8547TZ0sox9fCLDHw9KCH9c1FO5d9GopvkaJsBIOkTKxksdw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.9.2: + resolution: {integrity: sha512-m0hYELHGXdYx64D6IDDg/1vOJEaiV8f1G/iO+tejvRCJNSwK4jJ15e38JQy5Q6dGkn1M/9KcyEOwqmlZ2kqaZg==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.9.2: + resolution: {integrity: sha512-x1CWburlbN5JjG+juenuNa4KdedBdXLjZMp56nHFSHTOsb/MI2DYiGzLtRGHNMyydPGffGId+VgjOMrcltOksA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.9.2: + resolution: {integrity: sha512-VVzCB5yXR1QlfsH1Xw1zdzQ4Pxuzv+CPr5qpElpKhVxlxD3CRdfubAG9mJROl6/dmj5gVYDDWk8sC+j9BI9/kQ==} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.9.2: + resolution: {integrity: sha512-SYRedJi+mweatroB+6TTnJYLts0L0bosg531xnQWtklOI6dezEagx4Q0qDyvRdK+qgdA3YZpjjGuPFtxBmddBA==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.5 + dev: true + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.23.6 + '@babel/types': 7.23.6 + dev: true + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.23.6 + dev: true + + /@types/events@3.0.3: + resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/node@20.10.6: + resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==} + dependencies: + undici-types: 5.26.5 + dev: true + + /@types/prop-types@15.7.11: + resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + dev: true + + /@types/react-dom@18.2.17: + resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} + dependencies: + '@types/react': 18.2.43 + dev: true + + /@types/react@18.2.43: + resolution: {integrity: sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==} + dependencies: + '@types/prop-types': 15.7.11 + '@types/scheduler': 0.16.8 + csstype: 3.1.3 + dev: true + + /@types/scheduler@0.16.8: + resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} + dev: true + + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.55.0)(typescript@5.2.2): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.14.0(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.14.0(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4 + eslint: 8.55.0 + graphemer: 1.4.0 + ignore: 5.3.0 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.14.0(eslint@8.55.0)(typescript@5.2.2): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4 + eslint: 8.55.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.14.0: + resolution: {integrity: sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 + dev: true + + /@typescript-eslint/type-utils@6.14.0(eslint@8.55.0)(typescript@5.2.2): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) + '@typescript-eslint/utils': 6.14.0(eslint@8.55.0)(typescript@5.2.2) + debug: 4.3.4 + eslint: 8.55.0 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.14.0: + resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.2.2): + resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.14.0(eslint@8.55.0)(typescript@5.2.2): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.2.2) + eslint: 8.55.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.14.0: + resolution: {integrity: sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.14.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.2.1(vite@5.0.8): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.23.7 + '@babel/plugin-transform-react-jsx-self': 7.23.3(@babel/core@7.23.7) + '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.0 + vite: 5.0.8(@types/node@20.10.6) + transitivePeerDependencies: + - supports-color + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browserslist@4.22.2: + resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001572 + electron-to-chromium: 1.4.620 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.22.2) + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /caniuse-lite@1.0.30001572: + resolution: {integrity: sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: true + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /electron-to-chromium@1.4.620: + resolution: {integrity: sha512-a2fcSHOHrqBJsPNXtf6ZCEZpXrFCcbK1FBxfX3txoqWzNgtEDG1f3M59M98iwxhRW4iMKESnSjbJ310/rkrp0g==} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /esbuild@0.19.11: + resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.11 + '@esbuild/android-arm': 0.19.11 + '@esbuild/android-arm64': 0.19.11 + '@esbuild/android-x64': 0.19.11 + '@esbuild/darwin-arm64': 0.19.11 + '@esbuild/darwin-x64': 0.19.11 + '@esbuild/freebsd-arm64': 0.19.11 + '@esbuild/freebsd-x64': 0.19.11 + '@esbuild/linux-arm': 0.19.11 + '@esbuild/linux-arm64': 0.19.11 + '@esbuild/linux-ia32': 0.19.11 + '@esbuild/linux-loong64': 0.19.11 + '@esbuild/linux-mips64el': 0.19.11 + '@esbuild/linux-ppc64': 0.19.11 + '@esbuild/linux-riscv64': 0.19.11 + '@esbuild/linux-s390x': 0.19.11 + '@esbuild/linux-x64': 0.19.11 + '@esbuild/netbsd-x64': 0.19.11 + '@esbuild/openbsd-x64': 0.19.11 + '@esbuild/sunos-x64': 0.19.11 + '@esbuild/win32-arm64': 0.19.11 + '@esbuild/win32-ia32': 0.19.11 + '@esbuild/win32-x64': 0.19.11 + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.55.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-plugin-react-refresh@0.4.5(eslint@8.55.0): + resolution: {integrity: sha512-D53FYKJa+fDmZMtriODxvhwrO+IOqrxoEo21gMA0sjHdU6dPVH4OhyFip9ypl8HOF5RV5KdTo+rBQLvnY2cO8w==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.55.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.55.0: + resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.55.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.16.0: + resolution: {integrity: sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.9 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: true + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /google-protobuf@3.21.2: + resolution: {integrity: sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==} + dev: false + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /postcss@8.4.32: + resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /protoc-gen-ts@0.8.7: + resolution: {integrity: sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==} + hasBin: true + dev: false + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + + /rollup@4.9.2: + resolution: {integrity: sha512-66RB8OtFKUTozmVEh3qyNfH+b+z2RXBVloqO2KCC/pjFaGaHtxP9fVfOQKPSGXg2mElmjmxjW/fZ7iKrEpMH5Q==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.9.2 + '@rollup/rollup-android-arm64': 4.9.2 + '@rollup/rollup-darwin-arm64': 4.9.2 + '@rollup/rollup-darwin-x64': 4.9.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.9.2 + '@rollup/rollup-linux-arm64-gnu': 4.9.2 + '@rollup/rollup-linux-arm64-musl': 4.9.2 + '@rollup/rollup-linux-riscv64-gnu': 4.9.2 + '@rollup/rollup-linux-x64-gnu': 4.9.2 + '@rollup/rollup-linux-x64-musl': 4.9.2 + '@rollup/rollup-win32-arm64-msvc': 4.9.2 + '@rollup/rollup-win32-ia32-msvc': 4.9.2 + '@rollup/rollup-win32-x64-msvc': 4.9.2 + fsevents: 2.3.3 + dev: true + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /ts-api-utils@1.0.3(typescript@5.2.2): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + + /ts-results@3.3.0: + resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} + dev: false + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.22.2): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.22.2 + escalade: 3.1.1 + picocolors: 1.0.0 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + + /vite-plugin-wasm@3.3.0(vite@5.0.8): + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + dependencies: + vite: 5.0.8(@types/node@20.10.6) + dev: true + + /vite@5.0.8(@types/node@20.10.6): + resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.10.6 + esbuild: 0.19.11 + postcss: 8.4.32 + rollup: 4.9.2 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/frontend/appflowy_web/public/vite.svg b/frontend/appflowy_web/public/vite.svg new file mode 100644 index 0000000000..e7b8dfb1b2 --- /dev/null +++ b/frontend/appflowy_web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_web/src/@types/global.d.ts b/frontend/appflowy_web/src/@types/global.d.ts new file mode 100644 index 0000000000..6b17142a09 --- /dev/null +++ b/frontend/appflowy_web/src/@types/global.d.ts @@ -0,0 +1,15 @@ +export interface NotifyArgs { + source: string; + ty: number; + id: string; + payload?: Unit8Array; + error?: Unit8Array; +} + +declare global { + interface Window { + onFlowyNotify: (eventName: string, args: NotifyArgs) => void; + } +} + +export {}; diff --git a/frontend/appflowy_web/src/App.css b/frontend/appflowy_web/src/App.css new file mode 100644 index 0000000000..b9d355df2a --- /dev/null +++ b/frontend/appflowy_web/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/appflowy_web/src/App.tsx b/frontend/appflowy_web/src/App.tsx new file mode 100644 index 0000000000..6acdc9a892 --- /dev/null +++ b/frontend/appflowy_web/src/App.tsx @@ -0,0 +1,58 @@ +import reactLogo from "./assets/react.svg"; +import viteLogo from "/vite.svg"; +import "./App.css"; +import { useEffect } from "react"; +import { initApp } from "@/application/app.ts"; +import { subscribeNotification } from "@/application/notification.ts"; +import { NotifyArgs } from "./@types/global"; +import { init_tracing_log, init_wasm_core } from "../wasm-libs/af-wasm/pkg"; +import { v4 as uuidv4 } from 'uuid'; +import {AddUserPB, UserWasmEventAddUser} from "@/services/backend/events/user"; + +init_tracing_log(); +// FIXME: handle the promise that init_wasm_core returns +init_wasm_core(); + +function App() { + useEffect(() => { + initApp(); + return subscribeNotification((event: NotifyArgs) => { + console.log(event); + }); + }, []); + + const handleClick = async () => { + let email = `${uuidv4()}@example.com`; + let password = "AppFlowy!2024"; + const payload = AddUserPB.fromObject({email: email, password: password }) + let result = await UserWasmEventAddUser(payload); + if (!result.ok) { + + } + }; + + return ( + <> + +

Vite + React

+
+ +

+ Edit src/App.tsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+ + ); +} + +export default App; diff --git a/frontend/appflowy_web/src/application/app.ts b/frontend/appflowy_web/src/application/app.ts new file mode 100644 index 0000000000..918d5583b1 --- /dev/null +++ b/frontend/appflowy_web/src/application/app.ts @@ -0,0 +1,35 @@ + + +import { initEventBus } from "./event_bus.ts"; +import {async_event, register_listener} from "../../wasm-libs/af-wasm/pkg"; + +export function initApp() { + initEventBus(); + register_listener(); +} + +type InvokeArgs = Record; + +export async function invoke(cmd: string, args?: InvokeArgs): Promise { + switch (cmd) { + case "invoke_request": + const request = args?.request as { ty?: unknown, payload?: unknown } | undefined; + if (!request || typeof request !== 'object') { + throw new Error("Invalid or missing 'request' argument in 'invoke_request'"); + } + + const { ty, payload } = request; + + if (typeof ty !== 'string') { + throw new Error("Invalid 'ty' in request for 'invoke_request'"); + } + + if (!(payload instanceof Array)) { + throw new Error("Invalid 'payload' in request for 'invoke_request'"); + } + + return async_event(ty, new Uint8Array(payload)); + default: + throw new Error(`Unknown command: ${cmd}`); + } +} diff --git a/frontend/appflowy_web/src/application/event_bus.ts b/frontend/appflowy_web/src/application/event_bus.ts new file mode 100644 index 0000000000..6a3e95804f --- /dev/null +++ b/frontend/appflowy_web/src/application/event_bus.ts @@ -0,0 +1,34 @@ +import { EventEmitter } from "events"; +import { NotifyArgs } from "../@types/global"; + +const AF_NOTIFICATION = "af-notification"; + +let eventEmitter: EventEmitter; +export function getEventEmitterInstance() { + if (!eventEmitter) { + eventEmitter = new EventEmitter(); + } + return eventEmitter; +} + +export function initEventBus() { + window.onFlowyNotify = (eventName: string, args: NotifyArgs) => { + notify(eventName, args); + }; +} + +export function notify(_eventName: string, args: NotifyArgs) { + const eventEmitter = getEventEmitterInstance(); + eventEmitter.emit(AF_NOTIFICATION, args); +} + +export function onNotify(callback: (args: NotifyArgs) => void) { + const eventEmitter = getEventEmitterInstance(); + eventEmitter.on(AF_NOTIFICATION, callback); + return offNotify; +} + +export function offNotify() { + const eventEmitter = getEventEmitterInstance(); + eventEmitter.removeAllListeners(AF_NOTIFICATION); +} diff --git a/frontend/appflowy_web/src/application/notification.ts b/frontend/appflowy_web/src/application/notification.ts new file mode 100644 index 0000000000..aa9b0188e9 --- /dev/null +++ b/frontend/appflowy_web/src/application/notification.ts @@ -0,0 +1,17 @@ +import { NotifyArgs } from "../@types/global"; +import { onNotify } from "./event_bus.ts"; + +export function subscribeNotification( + callback: (args: NotifyArgs) => void, + options?: { id?: string } +) { + return onNotify((payload) => { + const { id } = payload; + + if (options?.id !== undefined && id !== options.id) { + return; + } + + callback(payload); + }); +} diff --git a/frontend/appflowy_web/src/assets/react.svg b/frontend/appflowy_web/src/assets/react.svg new file mode 100644 index 0000000000..6c87de9bb3 --- /dev/null +++ b/frontend/appflowy_web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_web/src/index.css b/frontend/appflowy_web/src/index.css new file mode 100644 index 0000000000..6119ad9a8f --- /dev/null +++ b/frontend/appflowy_web/src/index.css @@ -0,0 +1,68 @@ +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/frontend/appflowy_web/src/main.tsx b/frontend/appflowy_web/src/main.tsx new file mode 100644 index 0000000000..3d7150da80 --- /dev/null +++ b/frontend/appflowy_web/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/appflowy_web/src/services/backend/index.ts b/frontend/appflowy_web/src/services/backend/index.ts new file mode 100644 index 0000000000..d01244a3b1 --- /dev/null +++ b/frontend/appflowy_web/src/services/backend/index.ts @@ -0,0 +1,5 @@ +export * from "./models/af-user"; +export * from "./models/flowy-error"; +export * from "./models/flowy-folder"; +export * from "./models/flowy-document"; +export * from "./models/flowy-notification"; \ No newline at end of file diff --git a/frontend/appflowy_web/src/vite-env.d.ts b/frontend/appflowy_web/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/frontend/appflowy_web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/appflowy_web/tsconfig.json b/frontend/appflowy_web/tsconfig.json new file mode 100644 index 0000000000..67609fa96f --- /dev/null +++ b/frontend/appflowy_web/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "typeRoots": ["./node_modules/@types", "./src/@types"], + + "baseUrl": "./", + "paths": { + "@/*": ["src/*"], + } + }, + "include": ["src", "vite.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/appflowy_web/tsconfig.node.json b/frontend/appflowy_web/tsconfig.node.json new file mode 100644 index 0000000000..42872c59f5 --- /dev/null +++ b/frontend/appflowy_web/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/appflowy_web/vite.config.ts b/frontend/appflowy_web/vite.config.ts new file mode 100644 index 0000000000..49a8c2b3ad --- /dev/null +++ b/frontend/appflowy_web/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import wasm from "vite-plugin-wasm"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), wasm()], + resolve: { + alias: [ + { find: 'src/', replacement: `${__dirname}/src/` }, + { find: '@/', replacement: `${__dirname}/src/` }, + { find: '$app/', replacement: `${__dirname}/src/application/` }, + ], + }, +}) diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock new file mode 100644 index 0000000000..9354bd8418 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -0,0 +1,5230 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "accessory" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "af-persistence" +version = "0.1.0" +dependencies = [ + "anyhow", + "flowy-error", + "futures-util", + "indexed_db_futures", + "js-sys", + "serde", + "serde_json", + "thiserror", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "af-user" +version = "0.1.0" +dependencies = [ + "af-persistence", + "anyhow", + "bytes", + "collab", + "collab-entity", + "collab-integrate", + "collab-user", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-user-pub", + "lib-dispatch", + "lib-infra", + "protobuf", + "strum_macros 0.25.3", + "tokio", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "af-wasm" +version = "0.1.0" +dependencies = [ + "af-persistence", + "af-user", + "anyhow", + "collab", + "collab-integrate", + "console_error_panic_hook", + "flowy-document", + "flowy-error", + "flowy-folder", + "flowy-notification", + "flowy-server", + "flowy-server-pub", + "flowy-storage", + "flowy-user-pub", + "js-sys", + "lazy_static", + "lib-dispatch", + "lib-infra", + "parking_lot 0.12.1", + "serde", + "serde-wasm-bindgen", + "tokio", + "tokio-stream", + "tracing", + "tracing-core", + "tracing-wasm", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "wee_alloc", +] + +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "log", + "rand 0.7.3", + "wasm-timer", +] + +[[package]] +name = "ahash" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.2.12", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "getrandom 0.2.12", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tsify", + "url", + "uuid", + "wasm-bindgen", +] + +[[package]] +name = "appflowy-ai-client" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bytes", + "futures", + "serde", + "serde_json", + "serde_repr", + "thiserror", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.2", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.2", + "phf_codegen 0.11.2", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "client-api" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "again", + "anyhow", + "app-error", + "async-trait", + "bincode", + "brotli", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", + "database-entity", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "gotrue", + "gotrue-entity", + "mime", + "parking_lot 0.12.1", + "prost", + "reqwest", + "scraper 0.17.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "serde_urlencoded", + "shared-entity", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "wasm-bindgen-futures", + "yrs", +] + +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "cmd_lib" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f4cbdcab51ca635c5b19c85ece4072ea42e0d2360242826a6fc96fb11f0d40" +dependencies = [ + "cmd_lib_macros", + "env_logger", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae881960f7e2a409f91ef0b1c09558cf293031a1d6e8b45f908311f2a43f5fdf" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "collab" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "chrono", + "js-sys", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-document" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-entity" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "bytes", + "collab", + "getrandom 0.2.12", + "serde", + "serde_json", + "serde_repr", + "uuid", +] + +[[package]] +name = "collab-folder" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-integrate" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collab", + "collab-entity", + "collab-plugins", + "futures", + "lib-infra", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "collab-plugins" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bincode", + "bytes", + "chrono", + "collab", + "collab-entity", + "futures", + "futures-util", + "getrandom 0.2.12", + "indexed_db_futures", + "js-sys", + "lazy_static", + "parking_lot 0.12.1", + "rocksdb", + "serde", + "serde_json", + "similar 2.4.0", + "smallvec", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tracing", + "tracing-wasm", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "collab", + "collab-entity", + "serde", + "thiserror", + "tokio", + "tracing", + "yrs", +] + +[[package]] +name = "collab-user" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.8.0", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.48", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if 1.0.0", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "database-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "bincode", + "chrono", + "collab-entity", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deunicode" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae2a35373c5c74340b79ae6780b498b2b183915ec5dacf263aac5a099bf485a" + +[[package]] +name = "diesel" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c6fcf842f17f8c78ecf7c81d75c5ce84436b41ee07e03f490fbb5f5a8731d8" +dependencies = [ + "chrono", + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "serde_json", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8337737574f55a468005a83499da720f20c65586241ffea339db9ecdfd2b44" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.48", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dyn-clone" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + +[[package]] +name = "fancy-regex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flowy-ast" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "flowy-chat-pub" +version = "0.1.0" +dependencies = [ + "bytes", + "client-api", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-codegen" +version = "0.1.0" +dependencies = [ + "cmd_lib", + "console", + "fancy-regex 0.10.0", + "flowy-ast", + "itertools", + "lazy_static", + "log", + "phf 0.8.0", + "protoc-bin-vendored", + "protoc-rust", + "quote", + "serde", + "serde_json", + "similar 1.3.0", + "syn 1.0.109", + "tera", + "toml 0.5.11", + "walkdir", +] + +[[package]] +name = "flowy-database-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "client-api", + "collab", + "collab-entity", + "lib-infra", +] + +[[package]] +name = "flowy-derive" +version = "0.1.0" +dependencies = [ + "dashmap", + "flowy-ast", + "flowy-codegen", + "lazy_static", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "flowy-document" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "collab", + "collab-document", + "collab-entity", + "collab-integrate", + "collab-plugins", + "dashmap", + "flowy-codegen", + "flowy-derive", + "flowy-document-pub", + "flowy-error", + "flowy-notification", + "flowy-storage", + "futures", + "getrandom 0.2.12", + "indexmap", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "scraper 0.18.1", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-document-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-document", + "flowy-error", + "lib-infra", +] + +[[package]] +name = "flowy-encrypt" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.21.7", + "getrandom 0.2.12", + "hmac", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2", +] + +[[package]] +name = "flowy-error" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "client-api", + "collab-document", + "collab-folder", + "collab-plugins", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "lib-dispatch", + "protobuf", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "url", + "validator", +] + +[[package]] +name = "flowy-folder" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-folder-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "collab-folder", + "lib-infra", + "uuid", +] + +[[package]] +name = "flowy-notification" +version = "0.1.0" +dependencies = [ + "bytes", + "dashmap", + "flowy-codegen", + "flowy-derive", + "lazy_static", + "lib-dispatch", + "protobuf", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "flowy-search-pub" +version = "0.1.0" +dependencies = [ + "client-api", + "collab", + "collab-folder", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "client-api", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-plugins", + "flowy-chat-pub", + "flowy-database-pub", + "flowy-document-pub", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-search-pub", + "flowy-server-pub", + "flowy-storage", + "flowy-user-pub", + "futures", + "futures-util", + "hex", + "hyper", + "lazy_static", + "lib-dispatch", + "lib-infra", + "mime_guess", + "parking_lot 0.12.1", + "postgrest", + "rand 0.8.5", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "yrs", +] + +[[package]] +name = "flowy-server-pub" +version = "0.1.0" +dependencies = [ + "flowy-error", + "serde", + "serde_repr", +] + +[[package]] +name = "flowy-sqlite" +version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel_derives", + "diesel_migrations", + "libsqlite3-sys", + "parking_lot 0.12.1", + "r2d2", + "scheduled-thread-pool", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "flowy-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "flowy-error", + "fxhash", + "lib-infra", + "mime", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "flowy-user-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "chrono", + "collab", + "collab-entity", + "flowy-error", + "flowy-folder-pub", + "lib-infra", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gotrue" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "futures-util", + "getrandom 0.2.12", + "gotrue-entity", + "infra", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "gotrue-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "chrono", + "jsonwebtoken", + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "h2" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexed_db_futures" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +dependencies = [ + "accessory", + "cfg-if 1.0.0", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "infra" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lib-dispatch" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "derivative", + "dyn-clone", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "thread-id", + "tokio", + "tracing", + "validator", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "lib-infra" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "atomic_refcell", + "bytes", + "chrono", + "futures", + "futures-core", + "md5", + "pin-project", + "tempfile", + "tokio", + "tracing", + "validator", + "walkdir", + "zip", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if 1.0.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags 2.4.2", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.1+3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fe476c29791a5ca0d1273c697e96085bbabbbea2ef7afd5617e78a4b40332d3" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f200d8d83c44a45b21764d1916299752ca035d15ecd46faca3e9a2a2bf6ad06" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcd6ab1236bbdb3a49027e920e693192ebfe8913f6d60e294de57463a493cfde" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31940305ffc96863a735bef7c7994a00b325a7138fdbc5bda0f1a0476d3275" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pest_meta" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ff62f5259e53b78d1af898941cdcdccfae7385cf7d793a6e55de5d05bb4b7d" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" +dependencies = [ + "reqwest", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" +dependencies = [ + "proc-macro2", + "syn 2.0.48", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.48", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "protobuf-codegen" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protoc" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0218039c514f9e14a5060742ecd50427f8ac4f85a6dc58f2ddb806e318c55ee" +dependencies = [ + "log", + "which", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + +[[package]] +name = "protoc-rust" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f8a182bb17c485f20bdc4274a8c39000a61024cfe461c799b50fec77267838" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc", + "tempfile", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom 0.2.12", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring 0.17.7", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a930e03325234c18c7071fd2b60118307e025d6fff3e12745ffbf63a3d29c" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "smallvec", + "tendril", +] + +[[package]] +name = "scraper" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.7", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.4.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_derive" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "appflowy-ai-client", + "bytes", + "chrono", + "collab-entity", + "database-entity", + "futures", + "gotrue-entity", + "log", + "pin-project", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if 1.0.0", + "fastrand", + "redox_syscall 0.4.1", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.48", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.12", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna 0.4.0", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139bd73305d50e1c1c4333210c0db43d989395b64a237bd35c10ef3832a7f70c" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70072aebfe5da66d2716002c729a14e4aec4da0e23cc2ea66323dac541c93928" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if 1.0.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "yrs" +version = "0.18.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" +dependencies = [ + "arc-swap", + "atomic_refcell", + "fastrand", + "serde", + "serde_json", + "smallstr", + "smallvec", + "thiserror", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] + +[[patch.unused]] +name = "collab-database" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6febf0397e66ebf0a281980a2e7602d7af00c975#6febf0397e66ebf0a281980a2e7602d7af00c975" diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml new file mode 100644 index 0000000000..24b745fd0a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -0,0 +1,77 @@ +[workspace] +members = ["af-wasm", "af-user", "af-persistence"] +resolver = "2" + +[workspace.dependencies] +af-user = { path = "af-user" } +af-persistence = { path = "af-persistence" } +lib-dispatch = { path = "../../rust-lib/lib-dispatch" } +parking_lot = { version = "0.12.1" } +tracing = { version = "0.1.22" } +serde = { version = "1.0.194", features = ["derive"] } +serde_json = "1.0" +collab-integrate = { path = "../../rust-lib/collab-integrate" } +flowy-notification = { path = "../../rust-lib/flowy-notification" } +flowy-user-pub = { path = "../../rust-lib/flowy-user-pub" } +flowy-server = { path = "../../rust-lib/flowy-server" } +flowy-server-pub = { path = "../../rust-lib/flowy-server-pub" } +flowy-error = { path = "../../rust-lib/flowy-error" } +flowy-derive = { path = "../../rust-lib/build-tool/flowy-derive" } +flowy-codegen = { path = "../../rust-lib/build-tool/flowy-codegen" } +flowy-document = { path = "../../rust-lib/flowy-document" } +flowy-folder = { path = "../../rust-lib/flowy-folder" } +flowy-storage = { path = "../../rust-lib/flowy-storage" } +lib-infra = { path = "../../rust-lib/lib-infra" } +bytes = { version = "1.5" } +protobuf = { version = "2.28.0" } +thiserror = "1.0" +anyhow = "1.0" +futures-util = "0.3" +uuid = { version = "1.5", features = ["serde", "v4", "v5"] } +tokio-stream = "0.1" +tokio = { version = "1.35", features = ["sync"] } +wasm-bindgen-futures = "0.4.40" +serde-wasm-bindgen = "0.4" +# Please use the following script to update collab. +# Working directory: frontend +# +# To update the commit ID, run: +# scripts/tool/update_collab_rev.sh new_rev_id +# +# To switch to the local path, run: +# scripts/tool/update_collab_source.sh +# ⚠️⚠️⚠️️ +collab = { version = "0.2" } +collab-entity = { version = "0.2" } +collab-folder = { version = "0.2" } +collab-document = { version = "0.2" } +collab-database = { version = "0.2" } +collab-plugins = { version = "0.2" } +collab-user = { version = "0.2" } +yrs = "0.18.8" + +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" } + +[profile.dev] +opt-level = 0 +lto = false +codegen-units = 16 + +[profile.release] +lto = true +opt-level = 3 +codegen-units = 1 + +[patch.crates-io] +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "6febf0397e66ebf0a281980a2e7602d7af00c975" } diff --git a/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml new file mode 100644 index 0000000000..14825ef54a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "af-persistence" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +indexed_db_futures = { version = "0.4" } +js-sys = "0.3" +wasm-bindgen = "0.2" +web-sys = { version = "0.3", features = ["console", "Window"] } +tokio = { version = "1.26.0", features = ["sync", "rt"] } +flowy-error.workspace = true +thiserror.workspace = true +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +futures-util.workspace = true +wasm-bindgen-futures.workspace = true diff --git a/frontend/appflowy_web/wasm-libs/af-persistence/src/error.rs b/frontend/appflowy_web/wasm-libs/af-persistence/src/error.rs new file mode 100644 index 0000000000..1a6ad08d9a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-persistence/src/error.rs @@ -0,0 +1,30 @@ +use flowy_error::FlowyError; +use web_sys::DomException; + +#[derive(Debug, thiserror::Error)] +pub enum PersistenceError { + #[error(transparent)] + Internal(#[from] anyhow::Error), + + #[error(transparent)] + SerdeError(#[from] serde_json::Error), + + #[error("{0}")] + RecordNotFound(String), +} + +impl From for PersistenceError { + fn from(value: DomException) -> Self { + PersistenceError::Internal(anyhow::anyhow!("DOMException: {:?}", value)) + } +} + +impl From for FlowyError { + fn from(value: PersistenceError) -> Self { + match value { + PersistenceError::Internal(value) => FlowyError::internal().with_context(value), + PersistenceError::SerdeError(value) => FlowyError::serde().with_context(value), + PersistenceError::RecordNotFound(value) => FlowyError::record_not_found().with_context(value), + } + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-persistence/src/lib.rs b/frontend/appflowy_web/wasm-libs/af-persistence/src/lib.rs new file mode 100644 index 0000000000..4f89971020 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-persistence/src/lib.rs @@ -0,0 +1,2 @@ +pub mod error; +pub mod store; diff --git a/frontend/appflowy_web/wasm-libs/af-persistence/src/store.rs b/frontend/appflowy_web/wasm-libs/af-persistence/src/store.rs new file mode 100644 index 0000000000..92335e32b3 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-persistence/src/store.rs @@ -0,0 +1,192 @@ +use crate::error::PersistenceError; +use anyhow::anyhow; +use futures_util::future::LocalBoxFuture; +use indexed_db_futures::idb_object_store::IdbObjectStore; +use indexed_db_futures::idb_transaction::{IdbTransaction, IdbTransactionResult}; +use indexed_db_futures::prelude::IdbOpenDbRequestLike; +use indexed_db_futures::{IdbDatabase, IdbQuerySource, IdbVersionChangeEvent}; +use js_sys::Uint8Array; +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::future::Future; +use std::sync::Arc; +use tokio::sync::RwLock; +use wasm_bindgen::JsValue; +use web_sys::IdbTransactionMode; + +pub trait IndexddbStore { + fn get<'a, T>(&'a self, key: &'a str) -> LocalBoxFuture, PersistenceError>> + where + T: serde::de::DeserializeOwned + Sync; + fn set<'a, T>(&'a self, key: &'a str, value: T) -> LocalBoxFuture> + where + T: serde::Serialize + Sync + 'a; + fn remove<'a>(&'a self, key: &'a str) -> LocalBoxFuture>; +} + +const APPFLOWY_STORE: &str = "appflowy_store"; +pub struct AppFlowyWASMStore { + db: Arc>, +} + +unsafe impl Send for AppFlowyWASMStore {} +unsafe impl Sync for AppFlowyWASMStore {} + +impl AppFlowyWASMStore { + pub async fn new() -> Result { + let mut db_req = IdbDatabase::open_u32("appflowy", 1)?; + db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> { + if !evt.db().object_store_names().any(|n| &n == APPFLOWY_STORE) { + evt.db().create_object_store(APPFLOWY_STORE)?; + } + Ok(()) + })); + let db = Arc::new(RwLock::new(db_req.await?)); + Ok(Self { db }) + } + + pub async fn begin_read_transaction(&self, f: F) -> Result<(), PersistenceError> + where + F: FnOnce(&IndexddbStoreImpl<'_>) -> Fut, + Fut: Future>, + { + let db = self.db.read().await; + let txn = db.transaction_on_one_with_mode(APPFLOWY_STORE, IdbTransactionMode::Readonly)?; + let store = store_from_transaction(&txn)?; + let operation = IndexddbStoreImpl(store); + f(&operation).await?; + transaction_result_to_result(txn.await)?; + Ok(()) + } + + pub async fn begin_write_transaction(&self, f: F) -> Result<(), PersistenceError> + where + F: FnOnce(IndexddbStoreImpl<'_>) -> LocalBoxFuture<'_, Result<(), PersistenceError>>, + { + let db = self.db.write().await; + let txn = db.transaction_on_one_with_mode(APPFLOWY_STORE, IdbTransactionMode::Readwrite)?; + let store = store_from_transaction(&txn)?; + let operation = IndexddbStoreImpl(store); + f(operation).await?; + transaction_result_to_result(txn.await)?; + Ok(()) + } +} + +pub struct IndexddbStoreImpl<'i>(IdbObjectStore<'i>); + +impl<'i> IndexddbStore for IndexddbStoreImpl<'i> { + fn get<'a, T>(&'a self, key: &'a str) -> LocalBoxFuture, PersistenceError>> + where + T: DeserializeOwned + Sync, + { + let js_key = to_js_value(key); + Box::pin(async move { + match self.0.get(&js_key)?.await? { + None => Err(PersistenceError::RecordNotFound(format!( + "Can't find the value for given key: {} ", + key + ))), + Some(value) => { + let bytes = Uint8Array::new(&value).to_vec(); + let object = serde_json::from_slice::(&bytes)?; + Ok(Some(object)) + }, + } + }) + } + + fn set<'a, T>(&'a self, key: &'a str, value: T) -> LocalBoxFuture> + where + T: Serialize + Sync + 'a, + { + let js_key = to_js_value(key); + Box::pin(async move { + let js_value = to_js_value(serde_json::to_vec(&value)?); + self.0.put_key_val(&js_key, &js_value)?.await?; + Ok(()) + }) + } + + fn remove<'a>(&'a self, key: &'a str) -> LocalBoxFuture> { + let js_key = to_js_value(key); + Box::pin(async move { + self.0.delete(&js_key)?.await?; + Ok(()) + }) + } +} + +impl IndexddbStore for AppFlowyWASMStore { + fn get<'a, T>(&'a self, key: &'a str) -> LocalBoxFuture, PersistenceError>> + where + T: serde::de::DeserializeOwned + Sync, + { + let db = Arc::downgrade(&self.db); + Box::pin(async move { + let db = db.upgrade().ok_or_else(|| { + PersistenceError::Internal(anyhow!("Failed to upgrade the database reference")) + })?; + let read_guard = db.read().await; + let txn = + read_guard.transaction_on_one_with_mode(APPFLOWY_STORE, IdbTransactionMode::Readonly)?; + let store = store_from_transaction(&txn)?; + IndexddbStoreImpl(store).get(key).await + }) + } + + fn set<'a, T>(&'a self, key: &'a str, value: T) -> LocalBoxFuture> + where + T: serde::Serialize + Sync + 'a, + { + let db = Arc::downgrade(&self.db); + Box::pin(async move { + let db = db.upgrade().ok_or_else(|| { + PersistenceError::Internal(anyhow!("Failed to upgrade the database reference")) + })?; + let read_guard = db.read().await; + let txn = + read_guard.transaction_on_one_with_mode(APPFLOWY_STORE, IdbTransactionMode::Readwrite)?; + let store = store_from_transaction(&txn)?; + IndexddbStoreImpl(store).set(key, value).await?; + transaction_result_to_result(txn.await)?; + Ok(()) + }) + } + + fn remove<'a>(&'a self, key: &'a str) -> LocalBoxFuture> { + let db = Arc::downgrade(&self.db); + Box::pin(async move { + let db = db.upgrade().ok_or_else(|| { + PersistenceError::Internal(anyhow!("Failed to upgrade the database reference")) + })?; + let read_guard = db.read().await; + let txn = + read_guard.transaction_on_one_with_mode(APPFLOWY_STORE, IdbTransactionMode::Readwrite)?; + let store = store_from_transaction(&txn)?; + IndexddbStoreImpl(store).remove(key).await?; + transaction_result_to_result(txn.await)?; + Ok(()) + }) + } +} + +fn to_js_value>(key: K) -> JsValue { + JsValue::from(Uint8Array::from(key.as_ref())) +} + +fn store_from_transaction<'a>( + txn: &'a IdbTransaction<'a>, +) -> Result, PersistenceError> { + txn + .object_store(APPFLOWY_STORE) + .map_err(PersistenceError::from) +} + +fn transaction_result_to_result(result: IdbTransactionResult) -> Result<(), PersistenceError> { + match result { + IdbTransactionResult::Success => Ok(()), + IdbTransactionResult::Error(err) => Err(PersistenceError::from(err)), + IdbTransactionResult::Abort => Err(PersistenceError::Internal(anyhow!("Transaction aborted"))), + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml new file mode 100644 index 0000000000..27351d8f06 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "af-user" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +af-persistence.workspace = true +lib-dispatch = { workspace = true } +flowy-derive = { workspace = true } +flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_collab_persistence"] } +flowy-user-pub = { workspace = true } +strum_macros = "0.25.2" +tracing.workspace = true +lib-infra = { workspace = true } +collab = { workspace = true } +collab-entity.workspace = true +collab-user.workspace = true +collab-integrate = { workspace = true } +protobuf.workspace = true +bytes.workspace = true +anyhow.workspace = true +wasm-bindgen-futures.workspace = true +tokio = { workspace = true, features = ["sync"] } + +[build-dependencies] +flowy-codegen = { workspace = true, features = ["ts"] } diff --git a/frontend/appflowy_web/wasm-libs/af-user/Flowy.toml b/frontend/appflowy_web/wasm-libs/af-user/Flowy.toml new file mode 100644 index 0000000000..593c05b741 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/Flowy.toml @@ -0,0 +1,3 @@ +# Check out the FlowyConfig (located in flowy_toml.rs) for more details. +proto_input = ["src/entities", "src/event_map.rs"] +event_files = ["src/event_map.rs"] \ No newline at end of file diff --git a/frontend/appflowy_web/wasm-libs/af-user/build.rs b/frontend/appflowy_web/wasm-libs/af-user/build.rs new file mode 100644 index 0000000000..11b5513097 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/build.rs @@ -0,0 +1,17 @@ +use flowy_codegen::Project; + +fn main() { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "user", + Project::Web { + relative_path: "../../../".to_string(), + }, + ); + flowy_codegen::ts_event::gen( + "user", + Project::Web { + relative_path: "../../../".to_string(), + }, + ); +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs new file mode 100644 index 0000000000..57ba448081 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs @@ -0,0 +1,20 @@ +use af_persistence::store::AppFlowyWASMStore; +use flowy_error::FlowyResult; +use flowy_user_pub::session::Session; +use std::rc::Rc; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct AuthenticateUser { + session: Arc>>, + store: Rc, +} + +impl AuthenticateUser { + pub async fn new(store: Rc) -> FlowyResult { + Ok(Self { + session: Arc::new(RwLock::new(None)), + store, + }) + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/define.rs b/frontend/appflowy_web/wasm-libs/af-user/src/define.rs new file mode 100644 index 0000000000..520cf21f34 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/define.rs @@ -0,0 +1,9 @@ +pub(crate) const AF_USER_SESSION_KEY: &str = "af-user-session"; + +pub(crate) fn user_workspace_key(uid: i64) -> String { + format!("af-user-workspaces-{}", uid) +} + +pub(crate) fn user_profile_key(uid: i64) -> String { + format!("af-user-profile-{}", uid) +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/entities/auth.rs b/frontend/appflowy_web/wasm-libs/af-user/src/entities/auth.rs new file mode 100644 index 0000000000..96455adc5f --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/entities/auth.rs @@ -0,0 +1,68 @@ +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_user_pub::entities::Authenticator; +use std::collections::HashMap; + +#[derive(ProtoBuf, Default)] +pub struct OauthSignInPB { + /// Use this field to store the third party auth information. + /// Different auth type has different fields. + /// Supabase: + /// - map: { "uuid": "xxx" } + /// + #[pb(index = 1)] + pub map: HashMap, + + #[pb(index = 2)] + pub authenticator: AuthenticatorPB, +} + +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum AuthenticatorPB { + Local = 0, + Supabase = 1, + AppFlowyCloud = 2, +} + +impl From for AuthenticatorPB { + fn from(auth_type: Authenticator) -> Self { + match auth_type { + Authenticator::Supabase => AuthenticatorPB::Supabase, + Authenticator::Local => AuthenticatorPB::Local, + Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, + } + } +} + +impl From for Authenticator { + fn from(pb: AuthenticatorPB) -> Self { + match pb { + AuthenticatorPB::Supabase => Authenticator::Supabase, + AuthenticatorPB::Local => Authenticator::Local, + AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, + } + } +} + +impl Default for AuthenticatorPB { + fn default() -> Self { + Self::AppFlowyCloud + } +} + +#[derive(ProtoBuf, Default)] +pub struct AddUserPB { + #[pb(index = 1)] + pub email: String, + + #[pb(index = 2)] + pub password: String, +} + +#[derive(ProtoBuf, Default)] +pub struct UserSignInPB { + #[pb(index = 1)] + pub email: String, + + #[pb(index = 2)] + pub password: String, +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/entities/mod.rs b/frontend/appflowy_web/wasm-libs/af-user/src/entities/mod.rs new file mode 100644 index 0000000000..fd9d22da5d --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/entities/mod.rs @@ -0,0 +1,5 @@ +mod auth; +mod user; + +pub use auth::*; +pub use user::*; diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs new file mode 100644 index 0000000000..0aa22d6e7a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/entities/user.rs @@ -0,0 +1,69 @@ +use crate::entities::AuthenticatorPB; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_user_pub::entities::{EncryptionType, UserProfile}; + +#[derive(ProtoBuf, Default, Eq, PartialEq, Debug, Clone)] +pub struct UserProfilePB { + #[pb(index = 1)] + pub id: i64, + + #[pb(index = 2)] + pub email: String, + + #[pb(index = 3)] + pub name: String, + + #[pb(index = 4)] + pub token: String, + + #[pb(index = 5)] + pub icon_url: String, + + #[pb(index = 6)] + pub openai_key: String, + + #[pb(index = 7)] + pub authenticator: AuthenticatorPB, + + #[pb(index = 8)] + pub encryption_sign: String, + + #[pb(index = 9)] + pub workspace_id: String, + + #[pb(index = 10)] + pub stability_ai_key: String, +} + +impl From for UserProfilePB { + fn from(user_profile: UserProfile) -> Self { + let (encryption_sign, _encryption_ty) = match user_profile.encryption_type { + EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), + EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), + }; + Self { + id: user_profile.uid, + email: user_profile.email, + name: user_profile.name, + token: user_profile.token, + icon_url: user_profile.icon_url, + openai_key: user_profile.openai_key, + encryption_sign, + authenticator: user_profile.authenticator.into(), + workspace_id: user_profile.workspace_id, + stability_ai_key: user_profile.stability_ai_key, + } + } +} + +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum EncryptionTypePB { + NoEncryption = 0, + Symmetric = 1, +} + +impl Default for EncryptionTypePB { + fn default() -> Self { + Self::NoEncryption + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs b/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs new file mode 100644 index 0000000000..401f1e98ab --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs @@ -0,0 +1,48 @@ +use crate::entities::*; +use crate::manager::UserManager; +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_infra::box_any::BoxAny; +use std::rc::{Rc, Weak}; + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub async fn oauth_sign_in_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let params = data.into_inner(); + let user_profile = manager.sign_up(BoxAny::new(params.map)).await?; + data_result_ok(user_profile.into()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub async fn add_user_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params = data.into_inner(); + manager.add_user(¶ms.email, ¶ms.password).await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip(data, manager), err)] +pub async fn sign_in_with_password_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let params = data.into_inner(); + let user_profile = manager + .sign_in_with_password(¶ms.email, ¶ms.password) + .await?; + data_result_ok(UserProfilePB::from(user_profile)) +} + +fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { + let manager = manager + .upgrade() + .ok_or(FlowyError::internal().with_context("The user session is already drop"))?; + Ok(manager) +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs b/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs new file mode 100644 index 0000000000..1047760352 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs @@ -0,0 +1,29 @@ +use crate::event_handler::*; +use crate::manager::UserManager; +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use lib_dispatch::prelude::AFPlugin; +use std::rc::Weak; +use strum_macros::Display; + +#[rustfmt::skip] +pub fn init(user_manager: Weak) -> AFPlugin { + AFPlugin::new() + .name("Flowy-User") + .state(user_manager) + .event(UserWasmEvent::OauthSignIn, oauth_sign_in_handler) + .event(UserWasmEvent::AddUser, add_user_handler) + .event(UserWasmEvent::SignInPassword, sign_in_with_password_handler) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] +#[event_err = "FlowyError"] +pub enum UserWasmEvent { + #[event(input = "OauthSignInPB", output = "UserProfilePB")] + OauthSignIn = 0, + + #[event(input = "AddUserPB")] + AddUser = 1, + + #[event(input = "UserSignInPB", output = "UserProfilePB")] + SignInPassword = 2, +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs b/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs new file mode 100644 index 0000000000..d3519c7552 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs @@ -0,0 +1,7 @@ +pub mod authenticate_user; +mod define; +pub mod entities; +mod event_handler; +pub mod event_map; +pub mod manager; +mod protobuf; diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs b/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs new file mode 100644 index 0000000000..3832218ae8 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs @@ -0,0 +1,205 @@ +use crate::authenticate_user::AuthenticateUser; +use crate::define::{user_profile_key, user_workspace_key, AF_USER_SESSION_KEY}; +use af_persistence::store::{AppFlowyWASMStore, IndexddbStore}; +use anyhow::Context; +use collab::core::collab::DataSource; +use collab_entity::CollabType; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; +use collab_integrate::{CollabKVDB, MutexCollab}; +use collab_user::core::{MutexUserAwareness, UserAwareness}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; +use flowy_user_pub::entities::{ + user_awareness_object_id, AuthResponse, Authenticator, UserAuthResponse, UserProfile, + UserWorkspace, +}; +use flowy_user_pub::session::Session; +use lib_infra::box_any::BoxAny; +use lib_infra::future::Fut; +use std::rc::Rc; +use std::sync::{Arc, Mutex, Weak}; +use tracing::{error, instrument, trace}; + +pub trait UserCallback { + fn did_init( + &self, + user_id: i64, + cloud_config: &Option, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut>; + fn did_sign_in(&self, uid: i64, workspace: &UserWorkspace, device_id: &str) -> FlowyResult<()>; + fn did_sign_up( + &self, + is_new_user: bool, + user_profile: &UserProfile, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut>; +} + +pub struct UserManager { + device_id: String, + pub(crate) store: Rc, + pub(crate) cloud_services: Rc, + pub(crate) collab_builder: Weak, + pub(crate) authenticate_user: Rc, + + #[allow(dead_code)] + pub(crate) user_awareness: Rc>>, + pub(crate) collab_db: Arc, + + user_callbacks: Vec>, +} + +impl UserManager { + pub async fn new( + device_id: &str, + store: Rc, + cloud_services: Rc, + authenticate_user: Rc, + collab_builder: Weak, + ) -> Result { + let device_id = device_id.to_string(); + let store = Rc::new(AppFlowyWASMStore::new().await?); + let collab_db = Arc::new(CollabKVDB::new().await?); + Ok(Self { + device_id, + cloud_services, + collab_builder, + store, + authenticate_user, + user_callbacks: vec![], + user_awareness: Rc::new(Default::default()), + collab_db, + }) + } + + pub async fn sign_up(&self, params: BoxAny) -> FlowyResult { + let auth_service = self.cloud_services.get_user_service()?; + let response: AuthResponse = auth_service.sign_up(params).await?; + let new_user_profile = UserProfile::from((&response, &Authenticator::AppFlowyCloud)); + let new_session = Session::from(&response); + + self.prepare_collab(&new_session); + self + .save_auth_data(&response, &new_user_profile, &new_session) + .await?; + + for callback in self.user_callbacks.iter() { + if let Err(e) = callback + .did_sign_up( + response.is_new_user, + &new_user_profile, + &new_session.user_workspace, + &self.device_id, + ) + .await + { + error!("Failed to call did_sign_up callback: {:?}", e); + } + } + + // TODO(nathan): send notification + // send_auth_state_notification(AuthStateChangedPB { + // state: AuthStatePB::AuthStateSignIn, + // message: "Sign in success".to_string(), + // }); + Ok(new_user_profile) + } + + pub(crate) async fn add_user(&self, email: &str, password: &str) -> Result<(), FlowyError> { + let auth_service = self.cloud_services.get_user_service()?; + auth_service.create_user(email, password).await?; + Ok(()) + } + + pub(crate) async fn sign_in_with_password( + &self, + email: &str, + password: &str, + ) -> Result { + let auth_service = self.cloud_services.get_user_service()?; + let user_profile = auth_service.sign_in_with_password(email, password).await?; + Ok(user_profile) + } + + fn prepare_collab(&self, session: &Session) { + let collab_builder = self.collab_builder.upgrade().unwrap(); + collab_builder.initialize(session.user_workspace.id.clone()); + } + + #[instrument(level = "info", skip_all, err)] + async fn save_auth_data( + &self, + response: &impl UserAuthResponse, + user_profile: &UserProfile, + session: &Session, + ) -> Result<(), FlowyError> { + let uid = user_profile.uid; + let user_profile = user_profile.clone(); + let session = session.clone(); + let user_workspace = response.user_workspaces().to_vec(); + self + .store + .begin_write_transaction(|store| { + Box::pin(async move { + store.set(&user_workspace_key(uid), &user_workspace).await?; + store.set(AF_USER_SESSION_KEY, session).await?; + store.set(&user_profile_key(uid), user_profile).await?; + Ok(()) + }) + }) + .await?; + + Ok(()) + } + + pub async fn save_user_session(&self, session: &Session) -> FlowyResult<()> { + self.store.set(AF_USER_SESSION_KEY, session).await?; + Ok(()) + } + + pub async fn save_user_workspaces( + &self, + uid: i64, + user_workspaces: &[UserWorkspace], + ) -> FlowyResult<()> { + self + .store + .set(&user_workspace_key(uid), &user_workspaces.to_vec()) + .await?; + Ok(()) + } + + pub async fn save_user_profile(&self, user_profile: &UserProfile) -> FlowyResult<()> { + let uid = user_profile.uid; + self.store.set(&user_profile_key(uid), user_profile).await?; + Ok(()) + } + + async fn collab_for_user_awareness( + &self, + uid: i64, + object_id: &str, + collab_db: Weak, + raw_data: Vec, + ) -> Result, FlowyError> { + let collab_builder = self.collab_builder.upgrade().ok_or(FlowyError::new( + ErrorCode::Internal, + "Unexpected error: collab builder is not available", + ))?; + let collab = collab_builder + .build( + uid, + object_id, + CollabType::UserAwareness, + DataSource::DocStateV1(raw_data), + collab_db, + CollabBuilderConfig::default().sync_enable(true), + ) + .await + .context("Build collab for user awareness failed")?; + Ok(collab) + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/auth.rs b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/auth.rs new file mode 100644 index 0000000000..6dd0bd20f0 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/auth.rs @@ -0,0 +1,689 @@ +// This file is generated by rust-protobuf 2.28.0. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `auth.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_28_0; + +#[derive(PartialEq,Clone,Default)] +pub struct OauthSignInPB { + // message fields + pub map: ::std::collections::HashMap<::std::string::String, ::std::string::String>, + pub authenticator: AuthenticatorPB, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a OauthSignInPB { + fn default() -> &'a OauthSignInPB { + ::default_instance() + } +} + +impl OauthSignInPB { + pub fn new() -> OauthSignInPB { + ::std::default::Default::default() + } + + // repeated .OauthSignInPB.MapEntry map = 1; + + + pub fn get_map(&self) -> &::std::collections::HashMap<::std::string::String, ::std::string::String> { + &self.map + } + pub fn clear_map(&mut self) { + self.map.clear(); + } + + // Param is passed by value, moved + pub fn set_map(&mut self, v: ::std::collections::HashMap<::std::string::String, ::std::string::String>) { + self.map = v; + } + + // Mutable pointer to the field. + pub fn mut_map(&mut self) -> &mut ::std::collections::HashMap<::std::string::String, ::std::string::String> { + &mut self.map + } + + // Take field + pub fn take_map(&mut self) -> ::std::collections::HashMap<::std::string::String, ::std::string::String> { + ::std::mem::replace(&mut self.map, ::std::collections::HashMap::new()) + } + + // .AuthenticatorPB authenticator = 2; + + + pub fn get_authenticator(&self) -> AuthenticatorPB { + self.authenticator + } + pub fn clear_authenticator(&mut self) { + self.authenticator = AuthenticatorPB::Local; + } + + // Param is passed by value, moved + pub fn set_authenticator(&mut self, v: AuthenticatorPB) { + self.authenticator = v; + } +} + +impl ::protobuf::Message for OauthSignInPB { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_map_into::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeString>(wire_type, is, &mut self.map)?; + }, + 2 => { + ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.authenticator, 2, &mut self.unknown_fields)? + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + my_size += ::protobuf::rt::compute_map_size::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeString>(1, &self.map); + if self.authenticator != AuthenticatorPB::Local { + my_size += ::protobuf::rt::enum_size(2, self.authenticator); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + ::protobuf::rt::write_map_with_cached_sizes::<::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeString>(1, &self.map, os)?; + if self.authenticator != AuthenticatorPB::Local { + os.write_enum(2, ::protobuf::ProtobufEnum::value(&self.authenticator))?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> OauthSignInPB { + OauthSignInPB::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_map_accessor::<_, ::protobuf::types::ProtobufTypeString, ::protobuf::types::ProtobufTypeString>( + "map", + |m: &OauthSignInPB| { &m.map }, + |m: &mut OauthSignInPB| { &mut m.map }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum>( + "authenticator", + |m: &OauthSignInPB| { &m.authenticator }, + |m: &mut OauthSignInPB| { &mut m.authenticator }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "OauthSignInPB", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static OauthSignInPB { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(OauthSignInPB::new) + } +} + +impl ::protobuf::Clear for OauthSignInPB { + fn clear(&mut self) { + self.map.clear(); + self.authenticator = AuthenticatorPB::Local; + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for OauthSignInPB { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for OauthSignInPB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(PartialEq,Clone,Default)] +pub struct AddUserPB { + // message fields + pub email: ::std::string::String, + pub password: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a AddUserPB { + fn default() -> &'a AddUserPB { + ::default_instance() + } +} + +impl AddUserPB { + pub fn new() -> AddUserPB { + ::std::default::Default::default() + } + + // string email = 1; + + + pub fn get_email(&self) -> &str { + &self.email + } + pub fn clear_email(&mut self) { + self.email.clear(); + } + + // Param is passed by value, moved + pub fn set_email(&mut self, v: ::std::string::String) { + self.email = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_email(&mut self) -> &mut ::std::string::String { + &mut self.email + } + + // Take field + pub fn take_email(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.email, ::std::string::String::new()) + } + + // string password = 2; + + + pub fn get_password(&self) -> &str { + &self.password + } + pub fn clear_password(&mut self) { + self.password.clear(); + } + + // Param is passed by value, moved + pub fn set_password(&mut self, v: ::std::string::String) { + self.password = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_password(&mut self) -> &mut ::std::string::String { + &mut self.password + } + + // Take field + pub fn take_password(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.password, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for AddUserPB { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.email)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.password)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.email.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.email); + } + if !self.password.is_empty() { + my_size += ::protobuf::rt::string_size(2, &self.password); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.email.is_empty() { + os.write_string(1, &self.email)?; + } + if !self.password.is_empty() { + os.write_string(2, &self.password)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> AddUserPB { + AddUserPB::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "email", + |m: &AddUserPB| { &m.email }, + |m: &mut AddUserPB| { &mut m.email }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "password", + |m: &AddUserPB| { &m.password }, + |m: &mut AddUserPB| { &mut m.password }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "AddUserPB", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static AddUserPB { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(AddUserPB::new) + } +} + +impl ::protobuf::Clear for AddUserPB { + fn clear(&mut self) { + self.email.clear(); + self.password.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for AddUserPB { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for AddUserPB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(PartialEq,Clone,Default)] +pub struct UserSignInPB { + // message fields + pub email: ::std::string::String, + pub password: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a UserSignInPB { + fn default() -> &'a UserSignInPB { + ::default_instance() + } +} + +impl UserSignInPB { + pub fn new() -> UserSignInPB { + ::std::default::Default::default() + } + + // string email = 1; + + + pub fn get_email(&self) -> &str { + &self.email + } + pub fn clear_email(&mut self) { + self.email.clear(); + } + + // Param is passed by value, moved + pub fn set_email(&mut self, v: ::std::string::String) { + self.email = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_email(&mut self) -> &mut ::std::string::String { + &mut self.email + } + + // Take field + pub fn take_email(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.email, ::std::string::String::new()) + } + + // string password = 2; + + + pub fn get_password(&self) -> &str { + &self.password + } + pub fn clear_password(&mut self) { + self.password.clear(); + } + + // Param is passed by value, moved + pub fn set_password(&mut self, v: ::std::string::String) { + self.password = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_password(&mut self) -> &mut ::std::string::String { + &mut self.password + } + + // Take field + pub fn take_password(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.password, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for UserSignInPB { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.email)?; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.password)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if !self.email.is_empty() { + my_size += ::protobuf::rt::string_size(1, &self.email); + } + if !self.password.is_empty() { + my_size += ::protobuf::rt::string_size(2, &self.password); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if !self.email.is_empty() { + os.write_string(1, &self.email)?; + } + if !self.password.is_empty() { + os.write_string(2, &self.password)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> UserSignInPB { + UserSignInPB::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "email", + |m: &UserSignInPB| { &m.email }, + |m: &mut UserSignInPB| { &mut m.email }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "password", + |m: &UserSignInPB| { &m.password }, + |m: &mut UserSignInPB| { &mut m.password }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "UserSignInPB", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static UserSignInPB { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(UserSignInPB::new) + } +} + +impl ::protobuf::Clear for UserSignInPB { + fn clear(&mut self) { + self.email.clear(); + self.password.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for UserSignInPB { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for UserSignInPB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(Clone,PartialEq,Eq,Debug,Hash)] +pub enum AuthenticatorPB { + Local = 0, + Supabase = 1, + AppFlowyCloud = 2, +} + +impl ::protobuf::ProtobufEnum for AuthenticatorPB { + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(AuthenticatorPB::Local), + 1 => ::std::option::Option::Some(AuthenticatorPB::Supabase), + 2 => ::std::option::Option::Some(AuthenticatorPB::AppFlowyCloud), + _ => ::std::option::Option::None + } + } + + fn values() -> &'static [Self] { + static values: &'static [AuthenticatorPB] = &[ + AuthenticatorPB::Local, + AuthenticatorPB::Supabase, + AuthenticatorPB::AppFlowyCloud, + ]; + values + } + + fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + ::protobuf::reflect::EnumDescriptor::new_pb_name::("AuthenticatorPB", file_descriptor_proto()) + }) + } +} + +impl ::std::marker::Copy for AuthenticatorPB { +} + +impl ::std::default::Default for AuthenticatorPB { + fn default() -> Self { + AuthenticatorPB::Local + } +} + +impl ::protobuf::reflect::ProtobufValue for AuthenticatorPB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self)) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\nauth.proto\"\xaa\x01\n\rOauthSignInPB\x12)\n\x03map\x18\x01\x20\x03(\ + \x0b2\x17.OauthSignInPB.MapEntryR\x03map\x126\n\rauthenticator\x18\x02\ + \x20\x01(\x0e2\x10.AuthenticatorPBR\rauthenticator\x1a6\n\x08MapEntry\ + \x12\x10\n\x03key\x18\x01\x20\x01(\tR\x03key\x12\x14\n\x05value\x18\x02\ + \x20\x01(\tR\x05value:\x028\x01\"=\n\tAddUserPB\x12\x14\n\x05email\x18\ + \x01\x20\x01(\tR\x05email\x12\x1a\n\x08password\x18\x02\x20\x01(\tR\x08p\ + assword\"@\n\x0cUserSignInPB\x12\x14\n\x05email\x18\x01\x20\x01(\tR\x05e\ + mail\x12\x1a\n\x08password\x18\x02\x20\x01(\tR\x08password*=\n\x0fAuthen\ + ticatorPB\x12\t\n\x05Local\x10\0\x12\x0c\n\x08Supabase\x10\x01\x12\x11\n\ + \rAppFlowyCloud\x10\x02b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/event_map.rs b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/event_map.rs new file mode 100644 index 0000000000..f3d5173833 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/event_map.rs @@ -0,0 +1,95 @@ +// This file is generated by rust-protobuf 2.28.0. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `event_map.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_28_0; + +#[derive(Clone,PartialEq,Eq,Debug,Hash)] +pub enum UserWasmEvent { + OauthSignIn = 0, + AddUser = 1, + SignInPassword = 2, +} + +impl ::protobuf::ProtobufEnum for UserWasmEvent { + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(UserWasmEvent::OauthSignIn), + 1 => ::std::option::Option::Some(UserWasmEvent::AddUser), + 2 => ::std::option::Option::Some(UserWasmEvent::SignInPassword), + _ => ::std::option::Option::None + } + } + + fn values() -> &'static [Self] { + static values: &'static [UserWasmEvent] = &[ + UserWasmEvent::OauthSignIn, + UserWasmEvent::AddUser, + UserWasmEvent::SignInPassword, + ]; + values + } + + fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + ::protobuf::reflect::EnumDescriptor::new_pb_name::("UserWasmEvent", file_descriptor_proto()) + }) + } +} + +impl ::std::marker::Copy for UserWasmEvent { +} + +impl ::std::default::Default for UserWasmEvent { + fn default() -> Self { + UserWasmEvent::OauthSignIn + } +} + +impl ::protobuf::reflect::ProtobufValue for UserWasmEvent { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self)) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\x0fevent_map.proto*A\n\rUserWasmEvent\x12\x0f\n\x0bOauthSignIn\x10\0\ + \x12\x0b\n\x07AddUser\x10\x01\x12\x12\n\x0eSignInPassword\x10\x02b\x06pr\ + oto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/mod.rs b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/mod.rs new file mode 100644 index 0000000000..b79dbb09f6 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/mod.rs @@ -0,0 +1,12 @@ +#![cfg_attr(rustfmt, rustfmt::skip)] + #![allow(ambiguous_glob_reexports)] +// Auto-generated, do not edit + +mod event_map; +pub use event_map::*; + +mod auth; +pub use auth::*; + +mod user; +pub use user::*; diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs new file mode 100644 index 0000000000..52f066e2d4 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/protobuf/user.rs @@ -0,0 +1,618 @@ +// This file is generated by rust-protobuf 2.28.0. Do not edit +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(box_pointers)] +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_imports)] +#![allow(unused_results)] +//! Generated file from `user.proto` + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +// const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_28_0; + +#[derive(PartialEq,Clone,Default)] +pub struct UserProfilePB { + // message fields + pub id: i64, + pub email: ::std::string::String, + pub name: ::std::string::String, + pub token: ::std::string::String, + pub icon_url: ::std::string::String, + pub openai_key: ::std::string::String, + pub authenticator: super::auth::AuthenticatorPB, + pub encryption_sign: ::std::string::String, + pub workspace_id: ::std::string::String, + pub stability_ai_key: ::std::string::String, + // special fields + pub unknown_fields: ::protobuf::UnknownFields, + pub cached_size: ::protobuf::CachedSize, +} + +impl<'a> ::std::default::Default for &'a UserProfilePB { + fn default() -> &'a UserProfilePB { + ::default_instance() + } +} + +impl UserProfilePB { + pub fn new() -> UserProfilePB { + ::std::default::Default::default() + } + + // int64 id = 1; + + + pub fn get_id(&self) -> i64 { + self.id + } + pub fn clear_id(&mut self) { + self.id = 0; + } + + // Param is passed by value, moved + pub fn set_id(&mut self, v: i64) { + self.id = v; + } + + // string email = 2; + + + pub fn get_email(&self) -> &str { + &self.email + } + pub fn clear_email(&mut self) { + self.email.clear(); + } + + // Param is passed by value, moved + pub fn set_email(&mut self, v: ::std::string::String) { + self.email = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_email(&mut self) -> &mut ::std::string::String { + &mut self.email + } + + // Take field + pub fn take_email(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.email, ::std::string::String::new()) + } + + // string name = 3; + + + pub fn get_name(&self) -> &str { + &self.name + } + pub fn clear_name(&mut self) { + self.name.clear(); + } + + // Param is passed by value, moved + pub fn set_name(&mut self, v: ::std::string::String) { + self.name = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_name(&mut self) -> &mut ::std::string::String { + &mut self.name + } + + // Take field + pub fn take_name(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.name, ::std::string::String::new()) + } + + // string token = 4; + + + pub fn get_token(&self) -> &str { + &self.token + } + pub fn clear_token(&mut self) { + self.token.clear(); + } + + // Param is passed by value, moved + pub fn set_token(&mut self, v: ::std::string::String) { + self.token = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_token(&mut self) -> &mut ::std::string::String { + &mut self.token + } + + // Take field + pub fn take_token(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.token, ::std::string::String::new()) + } + + // string icon_url = 5; + + + pub fn get_icon_url(&self) -> &str { + &self.icon_url + } + pub fn clear_icon_url(&mut self) { + self.icon_url.clear(); + } + + // Param is passed by value, moved + pub fn set_icon_url(&mut self, v: ::std::string::String) { + self.icon_url = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_icon_url(&mut self) -> &mut ::std::string::String { + &mut self.icon_url + } + + // Take field + pub fn take_icon_url(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.icon_url, ::std::string::String::new()) + } + + // string openai_key = 6; + + + pub fn get_openai_key(&self) -> &str { + &self.openai_key + } + pub fn clear_openai_key(&mut self) { + self.openai_key.clear(); + } + + // Param is passed by value, moved + pub fn set_openai_key(&mut self, v: ::std::string::String) { + self.openai_key = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_openai_key(&mut self) -> &mut ::std::string::String { + &mut self.openai_key + } + + // Take field + pub fn take_openai_key(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.openai_key, ::std::string::String::new()) + } + + // .AuthenticatorPB authenticator = 7; + + + pub fn get_authenticator(&self) -> super::auth::AuthenticatorPB { + self.authenticator + } + pub fn clear_authenticator(&mut self) { + self.authenticator = super::auth::AuthenticatorPB::Local; + } + + // Param is passed by value, moved + pub fn set_authenticator(&mut self, v: super::auth::AuthenticatorPB) { + self.authenticator = v; + } + + // string encryption_sign = 8; + + + pub fn get_encryption_sign(&self) -> &str { + &self.encryption_sign + } + pub fn clear_encryption_sign(&mut self) { + self.encryption_sign.clear(); + } + + // Param is passed by value, moved + pub fn set_encryption_sign(&mut self, v: ::std::string::String) { + self.encryption_sign = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_encryption_sign(&mut self) -> &mut ::std::string::String { + &mut self.encryption_sign + } + + // Take field + pub fn take_encryption_sign(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.encryption_sign, ::std::string::String::new()) + } + + // string workspace_id = 9; + + + pub fn get_workspace_id(&self) -> &str { + &self.workspace_id + } + pub fn clear_workspace_id(&mut self) { + self.workspace_id.clear(); + } + + // Param is passed by value, moved + pub fn set_workspace_id(&mut self, v: ::std::string::String) { + self.workspace_id = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_workspace_id(&mut self) -> &mut ::std::string::String { + &mut self.workspace_id + } + + // Take field + pub fn take_workspace_id(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.workspace_id, ::std::string::String::new()) + } + + // string stability_ai_key = 10; + + + pub fn get_stability_ai_key(&self) -> &str { + &self.stability_ai_key + } + pub fn clear_stability_ai_key(&mut self) { + self.stability_ai_key.clear(); + } + + // Param is passed by value, moved + pub fn set_stability_ai_key(&mut self, v: ::std::string::String) { + self.stability_ai_key = v; + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_stability_ai_key(&mut self) -> &mut ::std::string::String { + &mut self.stability_ai_key + } + + // Take field + pub fn take_stability_ai_key(&mut self) -> ::std::string::String { + ::std::mem::replace(&mut self.stability_ai_key, ::std::string::String::new()) + } +} + +impl ::protobuf::Message for UserProfilePB { + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::ProtobufResult<()> { + while !is.eof()? { + let (field_number, wire_type) = is.read_tag_unpack()?; + match field_number { + 1 => { + if wire_type != ::protobuf::wire_format::WireTypeVarint { + return ::std::result::Result::Err(::protobuf::rt::unexpected_wire_type(wire_type)); + } + let tmp = is.read_int64()?; + self.id = tmp; + }, + 2 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.email)?; + }, + 3 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.name)?; + }, + 4 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.token)?; + }, + 5 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.icon_url)?; + }, + 6 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.openai_key)?; + }, + 7 => { + ::protobuf::rt::read_proto3_enum_with_unknown_fields_into(wire_type, is, &mut self.authenticator, 7, &mut self.unknown_fields)? + }, + 8 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.encryption_sign)?; + }, + 9 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.workspace_id)?; + }, + 10 => { + ::protobuf::rt::read_singular_proto3_string_into(wire_type, is, &mut self.stability_ai_key)?; + }, + _ => { + ::protobuf::rt::read_unknown_or_skip_group(field_number, wire_type, is, self.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u32 { + let mut my_size = 0; + if self.id != 0 { + my_size += ::protobuf::rt::value_size(1, self.id, ::protobuf::wire_format::WireTypeVarint); + } + if !self.email.is_empty() { + my_size += ::protobuf::rt::string_size(2, &self.email); + } + if !self.name.is_empty() { + my_size += ::protobuf::rt::string_size(3, &self.name); + } + if !self.token.is_empty() { + my_size += ::protobuf::rt::string_size(4, &self.token); + } + if !self.icon_url.is_empty() { + my_size += ::protobuf::rt::string_size(5, &self.icon_url); + } + if !self.openai_key.is_empty() { + my_size += ::protobuf::rt::string_size(6, &self.openai_key); + } + if self.authenticator != super::auth::AuthenticatorPB::Local { + my_size += ::protobuf::rt::enum_size(7, self.authenticator); + } + if !self.encryption_sign.is_empty() { + my_size += ::protobuf::rt::string_size(8, &self.encryption_sign); + } + if !self.workspace_id.is_empty() { + my_size += ::protobuf::rt::string_size(9, &self.workspace_id); + } + if !self.stability_ai_key.is_empty() { + my_size += ::protobuf::rt::string_size(10, &self.stability_ai_key); + } + my_size += ::protobuf::rt::unknown_fields_size(self.get_unknown_fields()); + self.cached_size.set(my_size); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::ProtobufResult<()> { + if self.id != 0 { + os.write_int64(1, self.id)?; + } + if !self.email.is_empty() { + os.write_string(2, &self.email)?; + } + if !self.name.is_empty() { + os.write_string(3, &self.name)?; + } + if !self.token.is_empty() { + os.write_string(4, &self.token)?; + } + if !self.icon_url.is_empty() { + os.write_string(5, &self.icon_url)?; + } + if !self.openai_key.is_empty() { + os.write_string(6, &self.openai_key)?; + } + if self.authenticator != super::auth::AuthenticatorPB::Local { + os.write_enum(7, ::protobuf::ProtobufEnum::value(&self.authenticator))?; + } + if !self.encryption_sign.is_empty() { + os.write_string(8, &self.encryption_sign)?; + } + if !self.workspace_id.is_empty() { + os.write_string(9, &self.workspace_id)?; + } + if !self.stability_ai_key.is_empty() { + os.write_string(10, &self.stability_ai_key)?; + } + os.write_unknown_fields(self.get_unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn get_cached_size(&self) -> u32 { + self.cached_size.get() + } + + fn get_unknown_fields(&self) -> &::protobuf::UnknownFields { + &self.unknown_fields + } + + fn mut_unknown_fields(&mut self) -> &mut ::protobuf::UnknownFields { + &mut self.unknown_fields + } + + fn as_any(&self) -> &dyn (::std::any::Any) { + self as &dyn (::std::any::Any) + } + fn as_any_mut(&mut self) -> &mut dyn (::std::any::Any) { + self as &mut dyn (::std::any::Any) + } + fn into_any(self: ::std::boxed::Box) -> ::std::boxed::Box { + self + } + + fn descriptor(&self) -> &'static ::protobuf::reflect::MessageDescriptor { + Self::descriptor_static() + } + + fn new() -> UserProfilePB { + UserProfilePB::new() + } + + fn descriptor_static() -> &'static ::protobuf::reflect::MessageDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::MessageDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + let mut fields = ::std::vec::Vec::new(); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeInt64>( + "id", + |m: &UserProfilePB| { &m.id }, + |m: &mut UserProfilePB| { &mut m.id }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "email", + |m: &UserProfilePB| { &m.email }, + |m: &mut UserProfilePB| { &mut m.email }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "name", + |m: &UserProfilePB| { &m.name }, + |m: &mut UserProfilePB| { &mut m.name }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "token", + |m: &UserProfilePB| { &m.token }, + |m: &mut UserProfilePB| { &mut m.token }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "icon_url", + |m: &UserProfilePB| { &m.icon_url }, + |m: &mut UserProfilePB| { &mut m.icon_url }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "openai_key", + |m: &UserProfilePB| { &m.openai_key }, + |m: &mut UserProfilePB| { &mut m.openai_key }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeEnum>( + "authenticator", + |m: &UserProfilePB| { &m.authenticator }, + |m: &mut UserProfilePB| { &mut m.authenticator }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "encryption_sign", + |m: &UserProfilePB| { &m.encryption_sign }, + |m: &mut UserProfilePB| { &mut m.encryption_sign }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "workspace_id", + |m: &UserProfilePB| { &m.workspace_id }, + |m: &mut UserProfilePB| { &mut m.workspace_id }, + )); + fields.push(::protobuf::reflect::accessor::make_simple_field_accessor::<_, ::protobuf::types::ProtobufTypeString>( + "stability_ai_key", + |m: &UserProfilePB| { &m.stability_ai_key }, + |m: &mut UserProfilePB| { &mut m.stability_ai_key }, + )); + ::protobuf::reflect::MessageDescriptor::new_pb_name::( + "UserProfilePB", + fields, + file_descriptor_proto() + ) + }) + } + + fn default_instance() -> &'static UserProfilePB { + static instance: ::protobuf::rt::LazyV2 = ::protobuf::rt::LazyV2::INIT; + instance.get(UserProfilePB::new) + } +} + +impl ::protobuf::Clear for UserProfilePB { + fn clear(&mut self) { + self.id = 0; + self.email.clear(); + self.name.clear(); + self.token.clear(); + self.icon_url.clear(); + self.openai_key.clear(); + self.authenticator = super::auth::AuthenticatorPB::Local; + self.encryption_sign.clear(); + self.workspace_id.clear(); + self.stability_ai_key.clear(); + self.unknown_fields.clear(); + } +} + +impl ::std::fmt::Debug for UserProfilePB { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::protobuf::text_format::fmt(self, f) + } +} + +impl ::protobuf::reflect::ProtobufValue for UserProfilePB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Message(self) + } +} + +#[derive(Clone,PartialEq,Eq,Debug,Hash)] +pub enum EncryptionTypePB { + NoEncryption = 0, + Symmetric = 1, +} + +impl ::protobuf::ProtobufEnum for EncryptionTypePB { + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(EncryptionTypePB::NoEncryption), + 1 => ::std::option::Option::Some(EncryptionTypePB::Symmetric), + _ => ::std::option::Option::None + } + } + + fn values() -> &'static [Self] { + static values: &'static [EncryptionTypePB] = &[ + EncryptionTypePB::NoEncryption, + EncryptionTypePB::Symmetric, + ]; + values + } + + fn enum_descriptor_static() -> &'static ::protobuf::reflect::EnumDescriptor { + static descriptor: ::protobuf::rt::LazyV2<::protobuf::reflect::EnumDescriptor> = ::protobuf::rt::LazyV2::INIT; + descriptor.get(|| { + ::protobuf::reflect::EnumDescriptor::new_pb_name::("EncryptionTypePB", file_descriptor_proto()) + }) + } +} + +impl ::std::marker::Copy for EncryptionTypePB { +} + +impl ::std::default::Default for EncryptionTypePB { + fn default() -> Self { + EncryptionTypePB::NoEncryption + } +} + +impl ::protobuf::reflect::ProtobufValue for EncryptionTypePB { + fn as_ref(&self) -> ::protobuf::reflect::ReflectValueRef { + ::protobuf::reflect::ReflectValueRef::Enum(::protobuf::ProtobufEnum::descriptor(self)) + } +} + +static file_descriptor_proto_data: &'static [u8] = b"\ + \n\nuser.proto\x1a\nauth.proto\"\xc7\x02\n\rUserProfilePB\x12\x0e\n\x02i\ + d\x18\x01\x20\x01(\x03R\x02id\x12\x14\n\x05email\x18\x02\x20\x01(\tR\x05\ + email\x12\x12\n\x04name\x18\x03\x20\x01(\tR\x04name\x12\x14\n\x05token\ + \x18\x04\x20\x01(\tR\x05token\x12\x19\n\x08icon_url\x18\x05\x20\x01(\tR\ + \x07iconUrl\x12\x1d\n\nopenai_key\x18\x06\x20\x01(\tR\topenaiKey\x126\n\ + \rauthenticator\x18\x07\x20\x01(\x0e2\x10.AuthenticatorPBR\rauthenticato\ + r\x12'\n\x0fencryption_sign\x18\x08\x20\x01(\tR\x0eencryptionSign\x12!\n\ + \x0cworkspace_id\x18\t\x20\x01(\tR\x0bworkspaceId\x12(\n\x10stability_ai\ + _key\x18\n\x20\x01(\tR\x0estabilityAiKey*3\n\x10EncryptionTypePB\x12\x10\ + \n\x0cNoEncryption\x10\0\x12\r\n\tSymmetric\x10\x01b\x06proto3\ +"; + +static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT; + +fn parse_descriptor_proto() -> ::protobuf::descriptor::FileDescriptorProto { + ::protobuf::Message::parse_from_bytes(file_descriptor_proto_data).unwrap() +} + +pub fn file_descriptor_proto() -> &'static ::protobuf::descriptor::FileDescriptorProto { + file_descriptor_proto_lazy.get(|| { + parse_descriptor_proto() + }) +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml new file mode 100644 index 0000000000..db754e681e --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "af-wasm" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +wasm-bindgen = { version = "0.2.89" } +lazy_static = "1.4.0" +lib-dispatch = { workspace = true, features = ["use_serde"] } +parking_lot.workspace = true +tracing.workspace = true +tracing-core = { version = "0.1.32" } +tracing-wasm = "0.2.1" +serde.workspace = true +collab-integrate = { workspace = true } +tokio-stream.workspace = true + +af-user.workspace = true +af-persistence.workspace = true +flowy-storage = { workspace = true } +flowy-notification = { workspace = true, features = ["web_ts"] } +flowy-user-pub = { workspace = true } +flowy-server = { workspace = true } +flowy-server-pub = { workspace = true } +flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "web_ts"] } +flowy-document = { workspace = true, features = ["web_ts"] } +flowy-folder = { workspace = true, features = ["web_ts"] } +lib-infra = { workspace = true } +collab = { workspace = true } +web-sys = "0.3" +wasm-bindgen-futures.workspace = true +uuid.workspace = true +serde-wasm-bindgen.workspace = true +js-sys = "0.3.67" +anyhow = "1.0" + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. However, it is slower than the default +# allocator, so it's not enabled by default. +wee_alloc = { version = "0.4.2", optional = true } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled +# in debug mode. +[target."cfg(debug_assertions)".dependencies] +console_error_panic_hook = "0.1.5" + +[dev-dependencies] +wasm-bindgen-test = "0.3.40" +tokio = { version = "1.0", features = ["sync"] } + +[features] +#default = ["wee_alloc"] +localhost_dev = [] \ No newline at end of file diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs new file mode 100644 index 0000000000..f841a13a73 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs @@ -0,0 +1,89 @@ +use crate::deps_resolve::document_deps::DocumentDepsResolver; +use crate::deps_resolve::folder_deps::FolderDepsResolver; +use crate::integrate::server::ServerProviderWASM; +use af_persistence::store::AppFlowyWASMStore; +use af_user::authenticate_user::AuthenticateUser; +use af_user::manager::UserManager; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, WorkspaceCollabIntegrate}; +use flowy_document::manager::DocumentManager; +use flowy_error::FlowyResult; +use flowy_folder::manager::FolderManager; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_storage::ObjectStorageService; +use lib_dispatch::prelude::AFPluginDispatcher; +use lib_dispatch::runtime::AFPluginRuntime; +use std::rc::Rc; +use std::sync::Arc; + +pub struct AppFlowyWASMCore { + pub collab_builder: Arc, + pub event_dispatcher: Rc, + pub user_manager: Rc, + pub folder_manager: Rc, + pub document_manager: Rc, +} + +impl AppFlowyWASMCore { + pub async fn new(device_id: &str, cloud_config: AFCloudConfiguration) -> FlowyResult { + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let server_provider = Rc::new(ServerProviderWASM::new(device_id, cloud_config)); + let store = Rc::new(AppFlowyWASMStore::new().await?); + let auth_user = Rc::new(AuthenticateUser::new(store.clone()).await?); + let collab_builder = Arc::new(AppFlowyCollabBuilder::new( + device_id.to_string(), + server_provider.clone(), + WorkspaceCollabIntegrateImpl(auth_user.clone()), + )); + + let document_manager = DocumentDepsResolver::resolve( + Rc::downgrade(&auth_user), + collab_builder.clone(), + server_provider.clone(), + Rc::downgrade(&(server_provider.clone() as Rc)), + ) + .await; + + let folder_manager = FolderDepsResolver::resolve( + Rc::downgrade(&auth_user), + document_manager.clone(), + collab_builder.clone(), + server_provider.clone(), + ) + .await; + + let user_manager = Rc::new( + UserManager::new( + device_id, + store, + server_provider.clone(), + auth_user, + Arc::downgrade(&collab_builder), + ) + .await?, + ); + + let event_dispatcher = Rc::new(AFPluginDispatcher::new( + runtime, + vec![af_user::event_map::init(Rc::downgrade(&user_manager))], + )); + Ok(Self { + collab_builder, + event_dispatcher, + user_manager, + folder_manager, + document_manager, + }) + } +} + +struct WorkspaceCollabIntegrateImpl(Rc); +impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { + fn workspace_id(&self) -> Result { + let workspace_id = self.0.workspace_id()?; + Ok(workspace_id) + } + + fn device_id(&self) -> Result { + Ok("fake device id".to_string()) + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs new file mode 100644 index 0000000000..3580bb762f --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs @@ -0,0 +1,19 @@ +use crate::integrate::server::ServerProviderWASM; +use af_user::authenticate_user::AuthenticateUser; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_document::manager::DocumentManager; +use flowy_storage::ObjectStorageService; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +pub struct DocumentDepsResolver; +impl DocumentDepsResolver { + pub async fn resolve( + authenticate_user: Weak, + collab_builder: Arc, + server_provider: Rc, + storage_service: Weak, + ) -> Rc { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs new file mode 100644 index 0000000000..e291b5551a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs @@ -0,0 +1,20 @@ +use crate::integrate::server::ServerProviderWASM; +use af_user::authenticate_user::AuthenticateUser; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_document::manager::DocumentManager; +use flowy_folder::manager::FolderManager; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +pub struct FolderDepsResolver; + +impl FolderDepsResolver { + pub async fn resolve( + authenticate_user: Weak, + document_manager: Rc, + collab_builder: Arc, + server_provider: Rc, + ) -> Rc { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs new file mode 100644 index 0000000000..b210522360 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod document_deps; +pub(crate) mod folder_deps; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/mod.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/mod.rs new file mode 100644 index 0000000000..74f47ad347 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/mod.rs @@ -0,0 +1 @@ +pub mod server; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs new file mode 100644 index 0000000000..6f3c71025a --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs @@ -0,0 +1,122 @@ +use collab::preclude::CollabPlugin; +use collab_integrate::collab_builder::{ + CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, +}; +use flowy_error::FlowyError; +use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::AppFlowyServer; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_storage::{ObjectIdentity, ObjectStorageService, ObjectValue}; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; +use flowy_user_pub::entities::{Authenticator, UserTokenState}; +use lib_infra::future::{to_fut, Fut, FutureResult}; +use parking_lot::RwLock; +use std::rc::Rc; +use std::sync::Arc; +use tokio_stream::wrappers::WatchStream; +use tracing::{info, warn}; + +pub struct ServerProviderWASM { + device_id: String, + config: AFCloudConfiguration, + server: RwLock>>, +} + +impl ServerProviderWASM { + pub fn new(device_id: &str, config: AFCloudConfiguration) -> Self { + info!("Server config: {}", config); + Self { + device_id: device_id.to_string(), + server: RwLock::new(Default::default()), + config, + } + } + + pub fn get_server(&self) -> Rc { + let server = self.server.read().as_ref().cloned(); + match server { + Some(server) => server, + None => { + let server = Rc::new(AppFlowyCloudServer::new( + self.config.clone(), + true, + self.device_id.clone(), + "0.0.1" + )); + *self.server.write() = Some(server.clone()); + server + }, + } + } +} + +impl CollabCloudPluginProvider for ServerProviderWASM { + fn provider_type(&self) -> CollabPluginProviderType { + CollabPluginProviderType::AppFlowyCloud + } + + fn get_plugins(&self, _context: CollabPluginProviderContext) -> Vec> { + vec![] + } + + fn is_sync_enabled(&self) -> bool { + true + } +} + +impl UserCloudServiceProvider for ServerProviderWASM { + fn set_token(&self, token: &str) -> Result<(), FlowyError> { + self.get_server().set_token(token)?; + Ok(()) + } + + fn subscribe_token_state(&self) -> Option> { + self.get_server().subscribe_token_state() + } + + fn set_enable_sync(&self, _uid: i64, _enable_sync: bool) { + warn!("enable sync is not supported in wasm") + } + + fn set_user_authenticator(&self, _authenticator: &Authenticator) { + warn!("set user authenticator is not supported in wasm") + } + + fn get_user_authenticator(&self) -> Authenticator { + Authenticator::AppFlowyCloud + } + + fn set_network_reachable(&self, _reachable: bool) { + warn!("set network reachable is not supported in wasm") + } + + fn set_encrypt_secret(&self, _secret: String) { + warn!("set encrypt secret is not supported in wasm") + } + + fn get_user_service(&self) -> Result, FlowyError> { + Ok(self.get_server().user_service()) + } + + fn service_url(&self) -> String { + self.config.base_url.clone() + } +} + +impl ObjectStorageService for ServerProviderWASM { + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult { + todo!() + } + + fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError> { + todo!() + } + + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError> { + todo!() + } + + fn get_object(&self, url: String) -> FutureResult { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs new file mode 100644 index 0000000000..efe3855f28 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs @@ -0,0 +1,170 @@ +use crate::notification::TSNotificationSender; +use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; +use std::cell::RefCell; +use std::rc::Rc; + +pub mod core; +mod deps_resolve; +mod integrate; +pub mod notification; + +use crate::core::AppFlowyWASMCore; +use lazy_static::lazy_static; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, +}; + +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use tracing::{error, info}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use wasm_bindgen_futures::{future_to_promise, js_sys}; + +lazy_static! { + static ref APPFLOWY_CORE: RefCellAppFlowyCore = RefCellAppFlowyCore::new(); +} + +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console)] + pub fn log(s: &str); + #[wasm_bindgen(js_namespace = window)] + fn onFlowyNotify(event_name: &str, payload: JsValue); +} +#[wasm_bindgen] +pub fn init_tracing_log() { + tracing_wasm::set_as_global_default(); +} + +#[wasm_bindgen] +pub fn init_wasm_core() -> js_sys::Promise { + // It's disabled in release mode so it doesn't bloat up the file size. + #[cfg(debug_assertions)] + console_error_panic_hook::set_once(); + + #[cfg(feature = "localhost_dev")] + let config = AFCloudConfiguration { + base_url: "http://localhost".to_string(), + ws_base_url: "ws://localhost/ws/v1".to_string(), + gotrue_url: "http://localhost/gotrue".to_string(), + }; + + #[cfg(not(feature = "localhost_dev"))] + let config = AFCloudConfiguration { + base_url: "https://beta.appflowy.cloud".to_string(), + ws_base_url: "wss://beta.appflowy.cloud/ws/v1".to_string(), + gotrue_url: "https://beta.appflowy.cloud/gotrue".to_string(), + }; + + let future = async move { + if let Ok(core) = AppFlowyWASMCore::new("device_id", config).await { + *APPFLOWY_CORE.0.borrow_mut() = Some(core); + info!("🔥🔥🔥Initialized AppFlowyWASMCore"); + } else { + error!("Failed to initialize AppFlowyWASMCore") + } + Ok(JsValue::from_str("")) + }; + future_to_promise(future) +} + +#[wasm_bindgen] +pub fn async_event(name: String, payload: Vec) -> js_sys::Promise { + if let Some(dispatcher) = APPFLOWY_CORE.dispatcher() { + let future = async move { + let request = WasmRequest::new(name, payload); + let event_resp = + AFPluginDispatcher::boxed_async_send_with_callback(dispatcher.as_ref(), request, |_| { + Box::pin(async {}) + }) + .await; + + let response = WasmResponse::from(event_resp); + serde_wasm_bindgen::to_value(&response).map_err(error_response) + }; + + future_to_promise(future) + } else { + future_to_promise(async { Err(JsValue::from_str("Dispatcher is not initialized")) }) + } +} + +#[wasm_bindgen] +pub fn register_listener() { + unregister_all_notification_sender(); + register_notification_sender(TSNotificationSender::new()); +} + +pub fn on_event(event_name: &str, args: JsValue) { + onFlowyNotify(event_name, args); +} + +struct RefCellAppFlowyCore(RefCell>); + +/// safety: +/// In a WebAssembly, implement the Sync for RefCellAppFlowyCore is safety +/// since WASM currently operates in a single-threaded environment. +unsafe impl Sync for RefCellAppFlowyCore {} + +impl RefCellAppFlowyCore { + fn new() -> Self { + Self(RefCell::new(None)) + } + + fn dispatcher(&self) -> Option> { + self + .0 + .borrow() + .as_ref() + .map(|core| core.event_dispatcher.clone()) + } +} + +fn error_response(error: serde_wasm_bindgen::Error) -> JsValue { + error!("Error: {}", error); + serde_wasm_bindgen::to_value(&WasmResponse::error(error.to_string())).unwrap() +} + +pub struct WasmRequest { + name: String, + payload: Vec, +} + +impl WasmRequest { + pub fn new(name: String, payload: Vec) -> Self { + Self { name, payload } + } +} + +impl From for AFPluginRequest { + fn from(request: WasmRequest) -> Self { + AFPluginRequest::new(request.name).payload(request.payload) + } +} + +#[derive(serde::Serialize)] +pub struct WasmResponse { + pub code: i8, + pub payload: Vec, +} +impl WasmResponse { + pub fn error(msg: String) -> Self { + Self { + code: StatusCode::Err as i8, + payload: msg.into_bytes(), + } + } +} + +impl From for WasmResponse { + fn from(response: AFPluginEventResponse) -> Self { + Self { + code: response.status_code as i8, + payload: response.payload.to_vec(), + } + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/notification.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/notification.rs new file mode 100644 index 0000000000..d329ee0cb1 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/notification.rs @@ -0,0 +1,19 @@ +use flowy_notification::entities::SubscribeObject; +use flowy_notification::NotificationSender; + +pub const AF_NOTIFICATION: &str = "af-notification"; + +pub struct TSNotificationSender {} + +impl TSNotificationSender { + pub(crate) fn new() -> Self { + TSNotificationSender {} + } +} + +impl NotificationSender for TSNotificationSender { + fn send_subject(&self, _subject: SubscribeObject) -> Result<(), String> { + // on_event(AF_NOTIFICATION, serde_wasm_bindgen::to_value(&subject).unwrap_or(JsValue::UNDEFINED)); + Ok(()) + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/main.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/main.rs new file mode 100644 index 0000000000..0498e45195 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/main.rs @@ -0,0 +1,4 @@ +use wasm_bindgen_test::wasm_bindgen_test_configure; +wasm_bindgen_test_configure!(run_in_browser); +mod user; +pub(crate) mod util; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/event_test.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/event_test.rs new file mode 100644 index 0000000000..d053043e7e --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/event_test.rs @@ -0,0 +1,10 @@ +use crate::util::tester::{unique_email, WASMEventTester}; +use wasm_bindgen_test::wasm_bindgen_test; + +#[wasm_bindgen_test] +async fn sign_up_event_test() { + let tester = WASMEventTester::new().await; + let email = unique_email(); + let user_profile = tester.sign_in_with_email(&email).await.unwrap(); + assert_eq!(user_profile.email, email); +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/mod.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/mod.rs new file mode 100644 index 0000000000..83ac8063ea --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/user/mod.rs @@ -0,0 +1 @@ +mod event_test; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/event_builder.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/event_builder.rs new file mode 100644 index 0000000000..99185a6837 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/event_builder.rs @@ -0,0 +1,132 @@ +use af_wasm::core::AppFlowyWASMCore; +use flowy_error::{internal_error, FlowyError}; +use std::rc::Rc; +use std::{ + convert::TryFrom, + fmt::{Debug, Display}, + hash::Hash, + sync::Arc, +}; + +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, +}; + +#[derive(Clone)] +pub struct EventBuilder { + context: TestContext, +} + +impl EventBuilder { + pub fn new(core: Arc) -> Self { + Self { + context: TestContext::new(core), + } + } + + pub fn payload

(mut self, payload: P) -> Self + where + P: ToBytes, + { + match payload.into_bytes() { + Ok(bytes) => { + let module_request = self.take_request(); + self.context.request = Some(module_request.payload(bytes)) + }, + Err(e) => { + tracing::error!("Set payload failed: {:?}", e); + }, + } + self + } + + pub fn event(mut self, event: Event) -> Self + where + Event: Eq + Hash + Debug + Clone + Display, + { + self.context.request = Some(AFPluginRequest::new(event)); + self + } + + pub async fn async_send(mut self) -> Self { + let request = self.take_request(); + let resp = AFPluginDispatcher::async_send(self.dispatch().as_ref(), request).await; + self.context.response = Some(resp); + self + } + + pub fn parse(self) -> R + where + R: AFPluginFromBytes, + { + let response = self.get_response(); + match response.clone().parse::() { + Ok(Ok(data)) => data, + Ok(Err(e)) => { + panic!( + "Parser {:?} failed: {:?}, response {:?}", + std::any::type_name::(), + e, + response + ) + }, + Err(e) => panic!( + "Dispatch {:?} failed: {:?}, response {:?}", + std::any::type_name::(), + e, + response + ), + } + } + + #[allow(dead_code)] + pub fn try_parse(self) -> Result + where + R: AFPluginFromBytes, + { + let response = self.get_response(); + response.parse::().map_err(internal_error)? + } + + #[allow(dead_code)] + pub fn error(self) -> Option { + let response = self.get_response(); + >::try_from(response.payload) + .ok() + .map(|data| data.into_inner()) + } + + fn dispatch(&self) -> &Rc { + &self.context.sdk.event_dispatcher + } + + fn get_response(&self) -> AFPluginEventResponse { + self + .context + .response + .as_ref() + .expect("must call sync_send/async_send first") + .clone() + } + + fn take_request(&mut self) -> AFPluginRequest { + self.context.request.take().expect("must call event first") + } +} + +#[derive(Clone)] +pub struct TestContext { + pub sdk: Arc, + request: Option, + response: Option, +} + +impl TestContext { + pub fn new(sdk: Arc) -> Self { + Self { + sdk, + request: None, + response: None, + } + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/mod.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/mod.rs new file mode 100644 index 0000000000..8458398ffd --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/mod.rs @@ -0,0 +1,2 @@ +mod event_builder; +pub mod tester; diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs new file mode 100644 index 0000000000..5142d8012f --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs @@ -0,0 +1,105 @@ +use crate::util::event_builder::EventBuilder; +use af_user::entities::*; +use af_user::event_map::UserWasmEvent::*; +use af_wasm::core::AppFlowyWASMCore; +use flowy_error::FlowyResult; + +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use parking_lot::Once; + +use flowy_document::deps::DocumentData; +use flowy_document::entities::{CreateDocumentPayloadPB, DocumentDataPB, OpenDocumentPayloadPB}; +use flowy_document::event_map::DocumentEvent; +use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; +use flowy_folder::event_map::FolderEvent; +use std::sync::Arc; +use uuid::Uuid; + +pub struct WASMEventTester { + core: Arc, +} + +impl WASMEventTester { + pub async fn new() -> Self { + setup_log(); + let config = AFCloudConfiguration { + base_url: "http://localhost".to_string(), + ws_base_url: "ws://localhost/ws/v1".to_string(), + gotrue_url: "http://localhost/gotrue".to_string(), + }; + let core = Arc::new(AppFlowyWASMCore::new("device_id", config).await.unwrap()); + Self { core } + } + + pub async fn sign_in_with_email(&self, email: &str) -> FlowyResult { + let email = email.to_string(); + let password = "AppFlowy!2024".to_string(); + let payload = AddUserPB { + email: email.clone(), + password: password.clone(), + }; + EventBuilder::new(self.core.clone()) + .event(AddUser) + .payload(payload) + .async_send() + .await; + + let payload = UserSignInPB { email, password }; + let user_profile = EventBuilder::new(self.core.clone()) + .event(SignInPassword) + .payload(payload) + .async_send() + .await + .parse::(); + Ok(user_profile) + } + + pub async fn create_and_open_document(&self, parent_id: &str) -> ViewPB { + let payload = CreateViewPayloadPB { + parent_view_id: parent_id.to_string(), + name, + desc: "".to_string(), + thumbnail: None, + layout: ViewLayoutPB::Document, + initial_data, + meta: Default::default(), + set_as_current: true, + index: None, + }; + let view = self + .event_builder() + .event(FolderEvent::CreateView) + .payload(payload) + .async_send() + .await + .parse::(); + + let payload = OpenDocumentPayloadPB { + document_id: view.id.clone(), + }; + + let _ = self + .event_builder() + .event(DocumentEvent::OpenDocument) + .payload(payload) + .async_send() + .await + .parse::(); + view + } + + fn event_builder(&self) -> EventBuilder { + EventBuilder::new(self.core.clone()) + } +} + +pub fn unique_email() -> String { + format!("{}@appflowy.io", Uuid::new_v4()) +} + +pub fn setup_log() { + static START: Once = Once::new(); + START.call_once(|| { + tracing_wasm::set_as_global_default(); + }); +} diff --git a/frontend/appflowy_web/wasm-libs/rust-toolchain.toml b/frontend/appflowy_web/wasm-libs/rust-toolchain.toml new file mode 100644 index 0000000000..6f14058b2e --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.77.2" diff --git a/frontend/appflowy_web/wasm-libs/rustfmt.toml b/frontend/appflowy_web/wasm-libs/rustfmt.toml new file mode 100644 index 0000000000..5cb0d67ee5 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/rustfmt.toml @@ -0,0 +1,12 @@ +# https://rust-lang.github.io/rustfmt/?version=master&search= +max_width = 100 +tab_spaces = 2 +newline_style = "Auto" +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +merge_derives = true +edition = "2021" \ No newline at end of file diff --git a/frontend/appflowy_web_app/.eslintignore b/frontend/appflowy_web_app/.eslintignore new file mode 100644 index 0000000000..b921919753 --- /dev/null +++ b/frontend/appflowy_web_app/.eslintignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json +**/backend/** +vite.config.ts +**/*.cy.tsx +*.config.ts +coverage/ \ No newline at end of file diff --git a/frontend/appflowy_web_app/.eslintignore.web b/frontend/appflowy_web_app/.eslintignore.web new file mode 100644 index 0000000000..44dcf6dda2 --- /dev/null +++ b/frontend/appflowy_web_app/.eslintignore.web @@ -0,0 +1,8 @@ +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json +src/application/services/tauri-services/ +vite.config.ts +coverage/ \ No newline at end of file diff --git a/frontend/appflowy_web_app/.eslintrc.cjs b/frontend/appflowy_web_app/.eslintrc.cjs new file mode 100644 index 0000000000..ff6f405885 --- /dev/null +++ b/frontend/appflowy_web_app/.eslintrc.cjs @@ -0,0 +1,73 @@ +module.exports = { + // https://eslint.org/docs/latest/use/configure/configuration-files + env: { + browser: true, + es6: true, + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname, + extraFileExtensions: ['.json'], + }, + plugins: ['@typescript-eslint', 'react-hooks'], + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/triple-slash-reference': 'error', + '@typescript-eslint/unified-signatures': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'off', + 'constructor-super': 'error', + eqeqeq: ['error', 'always'], + 'no-cond-assign': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-invalid-this': 'error', + 'no-new-wrappers': 'error', + 'no-param-reassign': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'no-void': 'off', + 'prefer-const': 'error', + 'prefer-spread': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + }, + ], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + { blankLine: 'always', prev: 'import', next: '*' }, + { blankLine: 'any', prev: 'import', next: 'import' }, + { blankLine: 'always', prev: 'block-like', next: '*' }, + { blankLine: 'always', prev: 'block', next: '*' }, + + ], + }, + ignorePatterns: ['src/**/*.test.ts', '**/__tests__/**/*.json', 'package.json'], +}; diff --git a/frontend/appflowy_web_app/.gitignore b/frontend/appflowy_web_app/.gitignore new file mode 100644 index 0000000000..5ae4f07836 --- /dev/null +++ b/frontend/appflowy_web_app/.gitignore @@ -0,0 +1,35 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist/** +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +src/@types/translations/*.json + + +src/application/services/tauri-services/backend/models/ +src/application/services/tauri-services/backend/events/ + +.env + +coverage +.nyc_output \ No newline at end of file diff --git a/frontend/appflowy_web_app/.nycrc b/frontend/appflowy_web_app/.nycrc new file mode 100644 index 0000000000..dc571c1abb --- /dev/null +++ b/frontend/appflowy_web_app/.nycrc @@ -0,0 +1,23 @@ +{ + "all": true, + "extends": "@istanbuljs/nyc-config-babel", + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "exclude": [ + "cypress/**/*.*", + "**/*.d.ts", + "**/*.cy.tsx", + "**/*.cy.ts" + ], + "reporter": [ + "text", + "html", + "text-summary", + "json", + "lcov" + ], + "temp-dir": "coverage/.nyc_output", + "report-dir": "coverage/cypress" +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/.prettierrc.cjs b/frontend/appflowy_web_app/.prettierrc.cjs new file mode 100644 index 0000000000..f283db53a2 --- /dev/null +++ b/frontend/appflowy_web_app/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + arrowParens: 'always', + bracketSpacing: true, + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxBracketSameLine: false, + jsxSingleQuote: true, + printWidth: 121, + plugins: [require('prettier-plugin-tailwindcss')], + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + vueIndentScriptAndStyle: false, +}; diff --git a/frontend/appflowy_web_app/Dockerfile b/frontend/appflowy_web_app/Dockerfile new file mode 100644 index 0000000000..97e3a85559 --- /dev/null +++ b/frontend/appflowy_web_app/Dockerfile @@ -0,0 +1,34 @@ +FROM oven/bun:latest + +WORKDIR /app + +RUN apt-get update && \ + apt-get install -y nginx + +RUN bun install cheerio pino axios pino-pretty + +COPY . . + +RUN addgroup --system nginx && \ + adduser --system --no-create-home --disabled-login --ingroup nginx nginx + +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + + +COPY dist/ /usr/share/nginx/html/ + +COPY nginx.conf /etc/nginx/nginx.conf + +COPY nginx-signed.crt /etc/ssl/certs/nginx-signed.crt +COPY nginx-signed.key /etc/ssl/private/nginx-signed.key + +RUN chown -R nginx:nginx /etc/ssl/certs/nginx-signed.crt /etc/ssl/private/nginx-signed.key + +COPY start.sh /app/start.sh + +RUN chmod +x /app/start.sh + + +EXPOSE 80 443 + +CMD ["/app/start.sh"] diff --git a/frontend/appflowy_web_app/README.md b/frontend/appflowy_web_app/README.md new file mode 100644 index 0000000000..c5c8ebf51f --- /dev/null +++ b/frontend/appflowy_web_app/README.md @@ -0,0 +1,284 @@ +

+ +

AppFlowy Web Project

+ +
Welcome to the AppFlowy Web Project, a robust and versatile platform designed to bring the innovative features of +AppFlowy to the web. This project uniquely supports running as a desktop application via Tauri, and offers web +interfaces powered by WebAssembly (WASM). Dive into an exceptional development experience with high performance and +extensive capabilities.
+ +
+ +## 🐑 Features + +- **Cross-Platform Compatibility**: Seamlessly run on desktop environments with Tauri, and on any web browser through + WASM. +- **High Performance**: Leverage the speed and efficiency of WebAssembly for your web interfaces. +- **Tauri Integration**: Build lightweight, secure, and efficient desktop applications. +- **Flexible Development**: Utilize a wide range of AppFlowy's functionalities in your web or desktop projects. + +## 🚀 Getting Started + +### 🛠️ Prerequisites + +Before you begin, ensure you have the following installed: + +- Node.js (v14 or later) +- Rust (latest stable version) +- Tauri prerequisites for your operating system +- PNPM (8.5.0) + +### 🏗️ Installation + +#### Clone the Repository + + ```bash + git clone https://github.com/AppFlowy-IO/AppFlowy + ``` + +#### 🌐 Install the frontend dependencies: + + ```bash + cd frontend/appflowy_web_app + pnpm install + ``` + +#### 🖥️ Desktop Application (Tauri) (Optional) + +> **Note**: if you want to run the web app in the browser, skip this step + +- Follow the instructions [here](https://tauri.app/v1/guides/getting-started/prerequisites/) to install Tauri + +##### Windows and Linux Prerequisites + +###### Windows only + +- Install the Duckscript CLI and vcpkg + + ```bash + cargo install --force duckscript_cli + vcpkg integrate install + ``` + +###### Linux only + +- Install the required dependencies + + ```bash + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + ``` + +- **Get error**: failed to run custom build command for librocksdb-sys v6.11.4 + + ```bash + sudo apt install clang + ``` + +##### Install Tauri Dependencies + +- Install cargo-make + + ```bash + cargo install --force cargo-make + ``` + + +- Install AppFlowy dev tools + + ```bash + # install development tools + # make sure you are in the root directory of the project + cd frontend + cargo make appflowy-tauri-deps-tools + ``` + +- Build the service/dependency + + ```bash + # make sure you are in the root directory of the project + cd frontend/appflowy_web_app + mkdir dist + cd src-tauri + cargo build + ``` + +### 🚀 Running the Application + +#### 🌐 Web Application + +- Run the web application + + ```bash + pnpm run dev + ``` +- Open your browser and navigate to `http://localhost:3000`, You can now interact with the AppFlowy web application + +#### 🖥️ Desktop Application (Tauri) + +**Ensure close web application before running the desktop application** + +- Run the desktop application + + ```bash + pnpm run tauri:dev + ``` +- The AppFlowy desktop application will open, and you can interact with it + +### 🛠️ Development + +#### How to add or modify i18n keys + +- Modify the i18n files in `frontend/resources/translations/en.json` to add or modify i18n keys +- Run the following command to update the i18n keys in the application + + ```bash + pnpm run sync:i18n + ``` + +#### How to modify the theme + +Don't modify the theme file in `frontend/appflowy_web_app/src/styles/variables` directly) + +- Modify the theme file in `frontend/appflowy_web_app/style-dictionary/tokens/base.json( or dark.json or light.json)` to + add or modify theme keys +- Run the following command to update the theme in the application + + ```bash + pnpm run css:variables + ``` + +#### How to add or modify the environment variables + +- Modify the environment file in `frontend/appflowy_web_app/.env` to add or modify environment variables + +#### How to create symlink for the @appflowyinc/client-api-wasm in local development + +- Run the following command to create a symlink for the @appflowyinc/client-api-wasm + + ```bash + # ensure you are in the frontend/appflowy_web_app directory + + pnpm run link:client-api $source_path $target_path + + # Example + # pnpm run link:client-api ../../../AppFlowy-Cloud/libs/client-api-wasm/pkg ./node_modules/@appflowyinc/client-api-wasm + ``` + +### 📝 About the Project + +#### 📁 Directory Structure + +- `frontend/appflowy_web_app`: Contains the web application source code +- `frontend/appflowy_web_app/src`: Contains the app entry point and the source code +- `frontend/appflowy_web_app/src/components`: Contains the react components +- `frontend/appflowy_web_app/src/styles`: Contains the styles for the application +- `frontend/appflowy_web_app/src/utils`: Contains the utility functions +- `frontend/appflowy_web_app/src/i18n`: Contains the i18n files +- `frontend/appflowy_web_app/src/assets`: Contains the assets for the application +- `frontend/appflowy_web_app/src/store`: Contains the redux store +- `frontend/appflowy_web_app/src/@types`: Contains the typescript types +- `frontend/appflowy_web_app/src/applications/services`: Contains the services for the application. In vite.config.ts, + we have defined the alias for the services directory for different environments(Tauri/Web) + ```typescript + resolve: { + alias: [ + // ... + { + find: '$client-services', + replacement: !!process.env.TAURI_PLATFORM + ? `${__dirname}/src/application/services/tauri-services` + : `${__dirname}/src/application/services/js-services`, + }, + ] + } + ``` + +### 📦 Deployment + +Use the AppFlowy CI/CD pipeline to deploy the application to the test and production environments. + +- Push the changes to the main branch +- Deploy Test Environment + - Automatically, the test environment will be deployed if merged to the main branch or build/test branch +- Deploy Production Environment + - Navigate to the Actions tab + - Click on the workflow and select the Run workflow + - Enter the options + - Click on the Run workflow button + +#### 📦 Deployment (Self-Hosted EC2) + +##### Pre-requisites + +Please ensure you have learned about: + +- [Deploy Web application on AWS Cloud using EC2 Instance](https://www.youtube.com/watch?v=gWVIIU1ev0Y) +- [How to Install and Use Rsync Command](https://operavps.com/docs/install-rsync-command-in-linux/) +- [How to Use ssh-keygen to Generate a New SSH Key?](https://www.ssh.com/academy/ssh/keygen) +- [Linux post-installation steps for Docker Engine](https://docs.docker.com/engine/install/linux-postinstall/) +- [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) + +And then follow the steps below: + +1. Ensure you have the following installed on your server: + - Docker: [Install Docker](https://docs.docker.com/engine/install/) + - Rsync: [Install Rsync](https://operavps.com/docs/install-rsync-command-in-linux/) + +2. Create a new user for deploy, and generate an SSH key for the user + + ```bash + sudo adduser appflowy(or any name) + sudo su - appflowy + mkdir ~/.ssh + chmod 700 ~/.ssh + ssh-keygen -t rsa + chmod 600 ~/.ssh/authorized_keys + # add the user to the docker group, to run docker commands without sudo + sudo usermod -aG docker ${USER} + ``` + - visit the `~/.ssh/id_rsa` and `~/.ssh/id_rsa.pub` to get the private and public key respectively + - add the public key to the `~/.ssh/authorized_keys` file + - ensure the private key is kept safe + - exit and login back to the server with the new + user: `ssh -i your-existing-key.pem ec2-user@your-instance-public-dns` + +3. Clone the AppFlowy repository + +4. Set the following secrets in your + repository, have to + know [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) + +> Note: Test Environment: prefix the secret with `WEB_TEST_` and Production Environment: prefix the secret with `WEB_` + +> for example, `WEB_TEST_SSH_PRIVATE_KEY` and `WEB_SSH_PRIVATE_KEY` + +- `SSH_PRIVATE_KEY`: The private key generated in step 2: cat ~/.ssh/id_rsa +- `REMOTE_HOST`: The host of the server: `your-instance-public-dns` or `your-instance-ip` +- `REMOTE_USER`: The user created in step 2: `appflowy` +- `SSL_CERTIFICATE`: The SSL certificate for the + server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) +- `SSL_CERTIFICATE_KEY`: The SSL certificate key for the + server - [Configuring HTTPS servers](https://nginx.org/en/docs/http/configuring_https_servers.html) + +5. Run the deployment workflow to deploy the application(production or test environment) + +> Note: the test server will **automatically** deploy if merged to the main branch or build/test branch + +### 🧪 Testing + +> We use Cypress for end-to-end testing and component testing - [Cypress](https://www.cypress.io/) + +#### 🧪 End-to-End Testing + +> to be continued + +#### 🧪 Component Testing + +Run the following command to run the component tests + +```bash +pnpm run test:components +``` + + diff --git a/frontend/appflowy_web_app/beta.env b/frontend/appflowy_web_app/beta.env new file mode 100644 index 0000000000..ab31b57db7 --- /dev/null +++ b/frontend/appflowy_web_app/beta.env @@ -0,0 +1,3 @@ +AF_WS_URL=wss://beta.appflowy.cloud/ws/v1 +AF_BASE_URL=https://beta.appflowy.cloud +AF_GOTRUE_URL=https://beta.appflowy.cloud/gotrue \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress.config.ts b/frontend/appflowy_web_app/cypress.config.ts new file mode 100644 index 0000000000..df9da9b91d --- /dev/null +++ b/frontend/appflowy_web_app/cypress.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'cypress'; +import registerCodeCoverageTasks from '@cypress/code-coverage/task'; + +export default defineConfig({ + env: { + codeCoverage: { + exclude: ['cypress/**/*.*', '**/__tests__/**/*.*', '**/*.test.*'], + }, + }, + component: { + devServer: { + framework: 'react', + bundler: 'vite', + }, + setupNodeEvents(on, config) { + registerCodeCoverageTasks(on, config); + return config; + }, + supportFile: 'cypress/support/component.ts', + }, + retries: { + // Configure retry attempts for `cypress run` + // Default is 0 + runMode: 2, + // Configure retry attempts for `cypress open` + // Default is 0 + openMode: 0, + }, +}); diff --git a/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json new file mode 100644 index 0000000000..cef4bad369 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/current_workspace.json @@ -0,0 +1,11 @@ +{ + "id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "name": "workspace", + "icon": "", + "owner": { + "id": 0, + "name": "system" + }, + "type": 0, + "workspaceDatabaseId": "375874be-7a4f-4b7c-8b89-1dc9a39838f4" +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/4c658817-20db-4f56-b7f9-0637a22dfeb6.json b/frontend/appflowy_web_app/cypress/fixtures/database/4c658817-20db-4f56-b7f9-0637a22dfeb6.json new file mode 100644 index 0000000000..f2dc3ef4fb --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/4c658817-20db-4f56-b7f9-0637a22dfeb6.json @@ -0,0 +1 @@ +{"data":{"state_vector":[65,128,137,148,150,4,39,132,238,182,192,14,5,134,200,133,143,5,2,135,173,169,205,15,4,137,227,133,241,2,170,1,140,242,215,248,4,35,141,132,223,206,14,5,142,215,187,158,14,10,146,198,138,224,6,18,149,154,146,112,20,150,194,135,131,8,12,154,253,168,186,13,6,157,197,217,249,6,3,158,173,179,170,6,81,160,159,229,236,10,34,162,129,240,225,15,19,165,237,195,173,1,8,168,211,203,155,8,88,171,216,132,162,10,97,174,158,229,225,9,2,175,150,167,163,14,4,174,182,200,164,11,2,177,178,255,174,1,38,178,161,242,226,13,154,1,174,250,146,158,5,2,180,149,168,150,13,10,180,132,165,192,8,1,182,201,218,189,1,12,182,139,168,140,5,36,183,238,200,180,5,6,185,145,225,175,8,157,3,186,204,138,236,4,118,187,163,190,240,15,89,187,159,219,213,8,2,188,252,160,180,14,5,191,215,204,166,13,12,192,183,207,147,14,43,193,174,143,180,7,18,193,140,213,146,2,134,1,200,168,240,223,7,2,201,191,253,157,12,2,200,156,140,203,9,2,202,170,215,178,7,24,203,248,208,163,4,4,206,242,242,141,13,95,209,142,245,200,15,183,5,210,221,238,195,8,20,211,189,178,91,80,211,235,145,81,16,216,247,253,206,7,2,219,179,165,244,8,4,224,218,133,236,10,13,227,170,238,211,14,16,227,250,198,245,13,5,229,168,135,118,243,4,234,232,155,212,3,4,246,154,200,238,10,11,247,149,251,192,4,4,248,220,249,231,6,29,247,187,192,242,6,6,250,147,239,143,1,2,252,220,241,227,14,60,253,149,229,85,14,253,223,254,206,11,2,252,240,184,224,14,24],"doc_state":[65,79,187,163,190,240,15,0,39,0,137,227,133,241,2,3,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,1,40,0,187,163,190,240,15,0,2,105,100,1,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,40,0,187,163,190,240,15,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,187,163,190,240,15,0,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,187,163,190,240,15,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,187,163,190,240,15,0,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,187,163,190,240,15,0,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,187,163,190,240,15,6,1,49,1,40,0,187,163,190,240,15,7,21,104,105,100,101,95,117,110,103,114,111,117,112,101,100,95,99,111,108,117,109,110,1,121,40,0,187,163,190,240,15,7,22,99,111,108,108,97,112,115,101,95,104,105,100,100,101,110,95,103,114,111,117,112,115,1,121,40,0,187,163,190,240,15,0,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,39,0,187,163,190,240,15,0,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,187,163,190,240,15,0,7,102,105,108,116,101,114,115,0,39,0,187,163,190,240,15,0,6,103,114,111,117,112,115,0,39,0,187,163,190,240,15,0,5,115,111,114,116,115,0,39,0,187,163,190,240,15,0,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,187,163,190,240,15,15,4,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,39,0,187,163,190,240,15,0,10,114,111,119,95,111,114,100,101,114,115,0,8,0,187,163,190,240,15,20,3,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,118,2,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,6,104,101,105,103,104,116,125,60,161,187,163,190,240,15,5,1,7,0,187,163,190,240,15,13,1,33,0,187,163,190,240,15,25,8,102,105,101,108,100,95,105,100,1,33,0,187,163,190,240,15,25,2,116,121,1,33,0,187,163,190,240,15,25,7,99,111,110,116,101,110,116,1,33,0,187,163,190,240,15,25,2,105,100,1,33,0,187,163,190,240,15,25,6,103,114,111,117,112,115,1,161,187,163,190,240,15,24,1,161,187,163,190,240,15,30,1,0,1,161,187,163,190,240,15,26,1,161,187,163,190,240,15,29,1,161,187,163,190,240,15,27,1,161,187,163,190,240,15,28,1,39,0,137,227,133,241,2,3,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,1,40,0,187,163,190,240,15,38,2,105,100,1,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,40,0,187,163,190,240,15,38,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,187,163,190,240,15,38,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,187,163,190,240,15,38,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,187,163,190,240,15,38,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,187,163,190,240,15,38,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,187,163,190,240,15,44,1,50,1,40,0,187,163,190,240,15,45,8,102,105,101,108,100,95,105,100,1,119,6,106,87,101,95,116,54,40,0,187,163,190,240,15,45,13,115,104,111,119,95,119,101,101,107,101,110,100,115,1,120,40,0,187,163,190,240,15,45,17,115,104,111,119,95,119,101,101,107,95,110,117,109,98,101,114,115,1,120,40,0,187,163,190,240,15,45,17,102,105,114,115,116,95,100,97,121,95,111,102,95,119,101,101,107,1,122,0,0,0,0,0,0,0,0,40,0,187,163,190,240,15,45,9,108,97,121,111,117,116,95,116,121,1,122,0,0,0,0,0,0,0,0,40,0,187,163,190,240,15,38,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,39,0,187,163,190,240,15,38,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,187,163,190,240,15,38,7,102,105,108,116,101,114,115,0,39,0,187,163,190,240,15,38,6,103,114,111,117,112,115,0,39,0,187,163,190,240,15,38,5,115,111,114,116,115,0,39,0,187,163,190,240,15,38,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,187,163,190,240,15,56,4,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,39,0,187,163,190,240,15,38,10,114,111,119,95,111,114,100,101,114,115,0,8,0,187,163,190,240,15,61,3,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,161,187,163,190,240,15,31,1,136,187,163,190,240,15,19,1,118,1,2,105,100,119,6,106,87,101,95,116,54,39,0,187,163,190,240,15,11,6,106,87,101,95,116,54,1,40,0,187,163,190,240,15,67,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,137,227,133,241,2,67,1,136,137,227,133,241,2,68,1,118,1,2,105,100,119,6,106,87,101,95,116,54,39,0,137,227,133,241,2,43,6,106,87,101,95,116,54,1,40,0,187,163,190,240,15,71,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,187,163,190,240,15,43,1,136,187,163,190,240,15,60,1,118,1,2,105,100,119,6,106,87,101,95,116,54,39,0,187,163,190,240,15,52,6,106,87,101,95,116,54,1,40,0,187,163,190,240,15,75,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,2,6,106,87,101,95,116,54,1,40,0,187,163,190,240,15,77,2,105,100,1,119,6,106,87,101,95,116,54,40,0,187,163,190,240,15,77,4,110,97,109,101,1,119,4,68,97,116,101,40,0,187,163,190,240,15,77,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,178,115,33,0,187,163,190,240,15,77,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,187,163,190,240,15,77,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,187,163,190,240,15,77,2,116,121,1,39,0,187,163,190,240,15,77,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,187,163,190,240,15,84,1,50,1,40,0,187,163,190,240,15,85,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,187,163,190,240,15,85,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,40,0,187,163,190,240,15,85,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,1,162,129,240,225,15,0,161,219,179,165,244,8,3,19,1,135,173,169,205,15,0,161,175,150,167,163,14,3,4,155,5,209,142,245,200,15,0,161,229,168,135,118,214,4,1,161,229,168,135,118,202,4,1,161,209,142,245,200,15,0,1,161,229,168,135,118,204,4,1,161,229,168,135,118,216,4,1,161,229,168,135,118,218,4,1,161,229,168,135,118,217,4,1,161,229,168,135,118,215,4,1,161,209,142,245,200,15,2,1,161,209,142,245,200,15,4,1,161,209,142,245,200,15,7,1,161,209,142,245,200,15,5,1,161,209,142,245,200,15,6,1,161,209,142,245,200,15,8,1,161,209,142,245,200,15,9,1,161,209,142,245,200,15,11,1,161,209,142,245,200,15,12,1,161,209,142,245,200,15,10,1,161,209,142,245,200,15,13,1,161,209,142,245,200,15,1,1,161,209,142,245,200,15,18,1,161,209,142,245,200,15,3,1,161,209,142,245,200,15,15,1,161,209,142,245,200,15,17,1,161,209,142,245,200,15,14,1,161,209,142,245,200,15,16,1,161,209,142,245,200,15,20,1,161,209,142,245,200,15,24,1,161,209,142,245,200,15,25,1,161,209,142,245,200,15,22,1,161,209,142,245,200,15,23,1,161,209,142,245,200,15,26,1,161,209,142,245,200,15,29,1,161,209,142,245,200,15,28,1,161,209,142,245,200,15,27,1,161,209,142,245,200,15,30,1,161,209,142,245,200,15,31,1,161,209,142,245,200,15,19,1,161,209,142,245,200,15,36,1,161,209,142,245,200,15,21,1,161,209,142,245,200,15,32,1,161,209,142,245,200,15,33,1,161,209,142,245,200,15,34,1,161,209,142,245,200,15,35,1,161,209,142,245,200,15,38,1,161,209,142,245,200,15,43,1,161,209,142,245,200,15,42,1,161,209,142,245,200,15,41,1,161,209,142,245,200,15,40,1,161,209,142,245,200,15,44,1,161,209,142,245,200,15,47,1,161,209,142,245,200,15,48,1,161,209,142,245,200,15,45,1,161,209,142,245,200,15,46,1,161,209,142,245,200,15,49,1,168,209,142,245,200,15,37,1,119,4,84,101,120,116,161,209,142,245,200,15,54,1,168,209,142,245,200,15,39,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,53,1,161,209,142,245,200,15,52,1,161,209,142,245,200,15,50,1,161,209,142,245,200,15,51,1,161,209,142,245,200,15,56,1,161,209,142,245,200,15,60,1,161,209,142,245,200,15,58,1,161,209,142,245,200,15,61,1,161,209,142,245,200,15,59,1,168,209,142,245,200,15,62,1,122,0,0,0,0,102,65,132,91,168,209,142,245,200,15,65,1,122,0,0,0,0,0,0,0,36,168,209,142,245,200,15,64,1,122,0,0,0,0,0,0,0,0,168,209,142,245,200,15,63,1,119,0,168,209,142,245,200,15,66,1,119,0,161,168,211,203,155,8,0,1,161,137,227,133,241,2,18,1,161,209,142,245,200,15,72,1,161,137,227,133,241,2,22,1,161,168,211,203,155,8,1,1,161,209,142,245,200,15,74,1,161,209,142,245,200,15,76,1,161,209,142,245,200,15,77,1,161,209,142,245,200,15,78,1,161,182,201,218,189,1,4,1,161,177,178,255,174,1,2,1,0,3,161,177,178,255,174,1,7,1,161,177,178,255,174,1,6,1,161,177,178,255,174,1,1,1,161,177,178,255,174,1,5,1,161,209,142,245,200,15,79,1,161,209,142,245,200,15,73,1,161,209,142,245,200,15,90,1,161,209,142,245,200,15,75,1,161,209,142,245,200,15,80,1,161,209,142,245,200,15,92,1,161,209,142,245,200,15,94,1,161,209,142,245,200,15,95,1,161,209,142,245,200,15,96,1,161,209,142,245,200,15,97,1,161,209,142,245,200,15,91,1,161,209,142,245,200,15,99,1,161,209,142,245,200,15,93,1,161,209,142,245,200,15,98,1,161,209,142,245,200,15,101,1,161,209,142,245,200,15,103,1,161,209,142,245,200,15,104,1,161,209,142,245,200,15,105,1,161,209,142,245,200,15,106,1,161,209,142,245,200,15,100,1,161,209,142,245,200,15,108,1,161,209,142,245,200,15,102,1,161,209,142,245,200,15,107,1,161,209,142,245,200,15,110,1,161,209,142,245,200,15,112,1,161,209,142,245,200,15,113,1,161,209,142,245,200,15,114,1,161,209,142,245,200,15,115,1,161,209,142,245,200,15,109,1,161,209,142,245,200,15,117,1,161,209,142,245,200,15,111,1,161,209,142,245,200,15,116,1,161,209,142,245,200,15,119,1,161,209,142,245,200,15,121,1,161,209,142,245,200,15,122,1,161,209,142,245,200,15,123,1,161,209,142,245,200,15,81,1,168,209,142,245,200,15,89,1,119,6,70,114,115,115,74,100,168,209,142,245,200,15,87,1,119,0,168,209,142,245,200,15,88,1,119,8,103,58,95,51,55,82,110,115,168,209,142,245,200,15,86,1,122,0,0,0,0,0,0,0,3,167,209,142,245,200,15,82,0,8,0,209,142,245,200,15,131,1,4,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,6,70,114,115,115,74,100,118,2,2,105,100,119,4,120,90,48,51,7,118,105,115,105,98,108,101,120,118,2,2,105,100,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,36,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,161,209,142,245,200,15,124,1,161,209,142,245,200,15,118,1,161,209,142,245,200,15,136,1,1,161,209,142,245,200,15,120,1,161,209,142,245,200,15,125,1,161,209,142,245,200,15,138,1,1,161,209,142,245,200,15,140,1,1,161,209,142,245,200,15,141,1,1,161,209,142,245,200,15,142,1,1,161,209,142,245,200,15,143,1,1,161,209,142,245,200,15,137,1,1,161,209,142,245,200,15,145,1,1,161,209,142,245,200,15,139,1,1,161,209,142,245,200,15,144,1,1,161,209,142,245,200,15,147,1,1,161,209,142,245,200,15,149,1,1,161,209,142,245,200,15,150,1,1,161,209,142,245,200,15,151,1,1,161,209,142,245,200,15,152,1,1,161,209,142,245,200,15,146,1,1,161,209,142,245,200,15,154,1,1,161,209,142,245,200,15,148,1,1,161,209,142,245,200,15,153,1,1,161,209,142,245,200,15,156,1,1,161,209,142,245,200,15,158,1,1,161,209,142,245,200,15,159,1,1,161,209,142,245,200,15,160,1,1,161,209,142,245,200,15,161,1,1,168,209,142,245,200,15,155,1,1,119,4,84,121,112,101,161,209,142,245,200,15,163,1,1,168,209,142,245,200,15,157,1,1,122,0,0,0,0,0,0,0,3,161,209,142,245,200,15,162,1,1,161,209,142,245,200,15,165,1,1,161,209,142,245,200,15,167,1,1,168,209,142,245,200,15,168,1,1,122,0,0,0,0,102,65,140,43,168,209,142,245,200,15,169,1,1,119,227,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,120,90,48,51,34,44,34,110,97,109,101,34,58,34,55,55,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,44,123,34,105,100,34,58,34,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,34,44,34,110,97,109,101,34,58,34,57,57,57,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,34,44,34,110,97,109,101,34,58,34,49,48,48,48,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,137,227,133,241,2,162,1,1,161,137,227,133,241,2,160,1,1,161,209,142,245,200,15,172,1,1,161,137,227,133,241,2,164,1,1,39,0,137,227,133,241,2,165,1,1,52,1,33,0,209,142,245,200,15,176,1,7,99,111,110,116,101,110,116,1,161,209,142,245,200,15,174,1,1,40,0,137,227,133,241,2,166,1,7,99,111,110,116,101,110,116,1,119,36,123,34,111,112,116,105,111,110,115,34,58,91,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,182,201,218,189,1,6,1,161,209,142,245,200,15,126,1,161,209,142,245,200,15,178,1,1,161,209,142,245,200,15,177,1,1,161,209,142,245,200,15,182,1,1,161,209,142,245,200,15,173,1,1,161,209,142,245,200,15,184,1,1,161,209,142,245,200,15,175,1,1,161,209,142,245,200,15,183,1,1,161,209,142,245,200,15,186,1,1,161,209,142,245,200,15,188,1,1,161,209,142,245,200,15,189,1,1,161,209,142,245,200,15,190,1,1,161,209,142,245,200,15,191,1,1,161,209,142,245,200,15,185,1,1,161,209,142,245,200,15,193,1,1,161,209,142,245,200,15,187,1,1,161,209,142,245,200,15,192,1,1,161,209,142,245,200,15,195,1,1,161,209,142,245,200,15,197,1,1,161,209,142,245,200,15,198,1,1,161,209,142,245,200,15,199,1,1,161,209,142,245,200,15,200,1,1,161,209,142,245,200,15,194,1,1,161,209,142,245,200,15,202,1,1,161,209,142,245,200,15,196,1,1,161,209,142,245,200,15,201,1,1,161,209,142,245,200,15,204,1,1,161,209,142,245,200,15,206,1,1,161,209,142,245,200,15,207,1,1,161,209,142,245,200,15,208,1,1,161,209,142,245,200,15,209,1,1,161,209,142,245,200,15,203,1,1,161,209,142,245,200,15,211,1,1,161,209,142,245,200,15,205,1,1,161,209,142,245,200,15,210,1,1,161,209,142,245,200,15,213,1,1,161,209,142,245,200,15,215,1,1,161,209,142,245,200,15,216,1,1,161,209,142,245,200,15,217,1,1,161,209,142,245,200,15,218,1,1,161,209,142,245,200,15,212,1,1,161,209,142,245,200,15,220,1,1,161,209,142,245,200,15,214,1,1,161,209,142,245,200,15,219,1,1,161,209,142,245,200,15,222,1,1,161,209,142,245,200,15,224,1,1,161,209,142,245,200,15,225,1,1,161,209,142,245,200,15,226,1,1,161,209,142,245,200,15,227,1,1,161,209,142,245,200,15,221,1,1,161,209,142,245,200,15,229,1,1,161,209,142,245,200,15,223,1,1,161,209,142,245,200,15,228,1,1,161,209,142,245,200,15,231,1,1,161,209,142,245,200,15,233,1,1,161,209,142,245,200,15,234,1,1,161,209,142,245,200,15,235,1,1,161,209,142,245,200,15,236,1,1,161,209,142,245,200,15,230,1,1,161,209,142,245,200,15,238,1,1,161,209,142,245,200,15,232,1,1,161,209,142,245,200,15,237,1,1,161,209,142,245,200,15,240,1,1,161,209,142,245,200,15,242,1,1,161,209,142,245,200,15,243,1,1,161,209,142,245,200,15,244,1,1,161,209,142,245,200,15,245,1,1,161,209,142,245,200,15,239,1,1,161,209,142,245,200,15,247,1,1,161,209,142,245,200,15,241,1,1,161,209,142,245,200,15,246,1,1,161,209,142,245,200,15,249,1,1,161,209,142,245,200,15,251,1,1,161,209,142,245,200,15,252,1,1,161,209,142,245,200,15,253,1,1,161,209,142,245,200,15,254,1,1,161,209,142,245,200,15,248,1,1,161,209,142,245,200,15,128,2,1,161,209,142,245,200,15,250,1,1,161,209,142,245,200,15,255,1,1,161,209,142,245,200,15,130,2,1,161,209,142,245,200,15,132,2,1,161,209,142,245,200,15,133,2,1,161,209,142,245,200,15,134,2,1,161,209,142,245,200,15,135,2,1,161,209,142,245,200,15,129,2,1,161,209,142,245,200,15,137,2,1,161,209,142,245,200,15,131,2,1,161,209,142,245,200,15,136,2,1,161,209,142,245,200,15,139,2,1,161,209,142,245,200,15,141,2,1,161,209,142,245,200,15,142,2,1,161,209,142,245,200,15,143,2,1,161,209,142,245,200,15,144,2,1,161,209,142,245,200,15,138,2,1,161,209,142,245,200,15,146,2,1,161,209,142,245,200,15,140,2,1,161,209,142,245,200,15,145,2,1,161,209,142,245,200,15,148,2,1,161,209,142,245,200,15,150,2,1,161,209,142,245,200,15,151,2,1,161,209,142,245,200,15,152,2,1,161,209,142,245,200,15,153,2,1,161,209,142,245,200,15,147,2,1,161,209,142,245,200,15,155,2,1,161,209,142,245,200,15,149,2,1,161,209,142,245,200,15,154,2,1,161,209,142,245,200,15,157,2,1,161,209,142,245,200,15,159,2,1,161,209,142,245,200,15,160,2,1,161,209,142,245,200,15,161,2,1,161,209,142,245,200,15,162,2,1,161,209,142,245,200,15,156,2,1,161,209,142,245,200,15,164,2,1,161,209,142,245,200,15,158,2,1,161,209,142,245,200,15,163,2,1,161,209,142,245,200,15,166,2,1,161,209,142,245,200,15,168,2,1,161,209,142,245,200,15,169,2,1,161,209,142,245,200,15,170,2,1,161,209,142,245,200,15,171,2,1,161,209,142,245,200,15,165,2,1,161,209,142,245,200,15,173,2,1,161,209,142,245,200,15,167,2,1,161,209,142,245,200,15,172,2,1,161,209,142,245,200,15,175,2,1,161,209,142,245,200,15,177,2,1,161,209,142,245,200,15,178,2,1,161,209,142,245,200,15,179,2,1,161,209,142,245,200,15,180,2,1,161,209,142,245,200,15,174,2,1,161,209,142,245,200,15,182,2,1,161,209,142,245,200,15,176,2,1,161,209,142,245,200,15,181,2,1,161,209,142,245,200,15,184,2,1,161,209,142,245,200,15,186,2,1,161,209,142,245,200,15,187,2,1,161,209,142,245,200,15,188,2,1,161,209,142,245,200,15,189,2,1,161,209,142,245,200,15,183,2,1,161,209,142,245,200,15,191,2,1,161,209,142,245,200,15,185,2,1,161,209,142,245,200,15,190,2,1,161,209,142,245,200,15,193,2,1,161,209,142,245,200,15,195,2,1,161,209,142,245,200,15,196,2,1,161,209,142,245,200,15,197,2,1,161,209,142,245,200,15,198,2,1,161,209,142,245,200,15,192,2,1,161,209,142,245,200,15,200,2,1,161,209,142,245,200,15,194,2,1,161,209,142,245,200,15,199,2,1,161,209,142,245,200,15,202,2,1,161,209,142,245,200,15,204,2,1,161,209,142,245,200,15,205,2,1,161,209,142,245,200,15,206,2,1,161,209,142,245,200,15,207,2,1,161,209,142,245,200,15,201,2,1,161,209,142,245,200,15,209,2,1,161,209,142,245,200,15,203,2,1,161,209,142,245,200,15,208,2,1,161,209,142,245,200,15,211,2,1,161,209,142,245,200,15,213,2,1,161,209,142,245,200,15,214,2,1,161,209,142,245,200,15,215,2,1,161,209,142,245,200,15,216,2,1,161,209,142,245,200,15,210,2,1,161,209,142,245,200,15,218,2,1,161,209,142,245,200,15,212,2,1,161,209,142,245,200,15,217,2,1,161,209,142,245,200,15,220,2,1,161,209,142,245,200,15,222,2,1,161,209,142,245,200,15,223,2,1,161,209,142,245,200,15,224,2,1,161,209,142,245,200,15,225,2,1,161,209,142,245,200,15,219,2,1,161,209,142,245,200,15,227,2,1,161,209,142,245,200,15,221,2,1,161,209,142,245,200,15,226,2,1,161,209,142,245,200,15,229,2,1,161,209,142,245,200,15,231,2,1,161,209,142,245,200,15,232,2,1,161,209,142,245,200,15,233,2,1,161,209,142,245,200,15,234,2,1,161,209,142,245,200,15,228,2,1,161,209,142,245,200,15,236,2,1,161,209,142,245,200,15,230,2,1,161,209,142,245,200,15,235,2,1,161,209,142,245,200,15,238,2,1,161,209,142,245,200,15,240,2,1,161,209,142,245,200,15,241,2,1,161,209,142,245,200,15,242,2,1,161,209,142,245,200,15,243,2,1,161,209,142,245,200,15,237,2,1,161,209,142,245,200,15,245,2,1,161,209,142,245,200,15,239,2,1,161,209,142,245,200,15,244,2,1,161,209,142,245,200,15,247,2,1,161,209,142,245,200,15,249,2,1,161,209,142,245,200,15,250,2,1,161,209,142,245,200,15,251,2,1,161,209,142,245,200,15,252,2,1,161,209,142,245,200,15,246,2,1,161,209,142,245,200,15,254,2,1,161,209,142,245,200,15,248,2,1,161,209,142,245,200,15,253,2,1,161,209,142,245,200,15,128,3,1,161,209,142,245,200,15,130,3,1,161,209,142,245,200,15,131,3,1,161,209,142,245,200,15,132,3,1,161,209,142,245,200,15,133,3,1,161,209,142,245,200,15,255,2,1,161,209,142,245,200,15,135,3,1,161,209,142,245,200,15,129,3,1,161,209,142,245,200,15,134,3,1,161,209,142,245,200,15,137,3,1,161,209,142,245,200,15,139,3,1,161,209,142,245,200,15,140,3,1,161,209,142,245,200,15,141,3,1,161,209,142,245,200,15,142,3,1,161,209,142,245,200,15,136,3,1,161,209,142,245,200,15,144,3,1,161,209,142,245,200,15,138,3,1,161,209,142,245,200,15,143,3,1,161,209,142,245,200,15,146,3,1,161,209,142,245,200,15,148,3,1,161,209,142,245,200,15,149,3,1,161,209,142,245,200,15,150,3,1,161,209,142,245,200,15,151,3,1,161,209,142,245,200,15,145,3,1,161,209,142,245,200,15,153,3,1,168,209,142,245,200,15,147,3,1,122,0,0,0,0,0,0,0,4,161,209,142,245,200,15,152,3,1,161,209,142,245,200,15,155,3,1,161,209,142,245,200,15,157,3,1,161,209,142,245,200,15,158,3,1,168,209,142,245,200,15,159,3,1,119,205,5,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,50,100,54,48,51,48,99,51,45,57,55,49,101,45,52,100,52,53,45,98,53,55,48,45,100,101,57,50,102,100,101,97,100,97,101,54,34,44,34,110,97,109,101,34,58,34,103,104,106,116,117,105,107,34,44,34,99,111,108,111,114,34,58,34,71,114,101,101,110,34,125,44,123,34,105,100,34,58,34,102,99,100,54,101,102,56,99,45,56,99,100,54,45,52,49,98,51,45,57,50,52,53,45,57,57,56,57,51,49,100,52,57,97,49,54,34,44,34,110,97,109,101,34,58,34,103,104,106,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,49,99,52,102,53,52,54,57,45,54,101,49,49,45,52,55,48,51,45,57,48,56,54,45,101,98,98,50,51,57,49,53,100,53,100,56,34,44,34,110,97,109,101,34,58,34,111,111,111,34,44,34,99,111,108,111,114,34,58,34,76,105,109,101,34,125,44,123,34,105,100,34,58,34,57,100,48,48,56,50,51,97,45,100,57,101,50,45,52,102,98,55,45,98,100,98,54,45,99,97,102,54,101,98,99,54,99,49,50,51,34,44,34,110,97,109,101,34,58,34,104,106,107,34,44,34,99,111,108,111,114,34,58,34,76,105,103,104,116,80,105,110,107,34,125,44,123,34,105,100,34,58,34,48,52,48,102,98,48,98,102,45,50,101,100,97,45,52,99,97,51,45,56,54,99,97,45,53,98,57,49,98,55,48,50,102,101,49,54,34,44,34,110,97,109,101,34,58,34,110,106,107,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,44,123,34,105,100,34,58,34,52,49,57,50,51,51,57,51,45,102,55,99,51,45,52,50,51,53,45,98,54,49,51,45,102,57,97,101,56,52,102,102,53,56,56,57,34,44,34,110,97,109,101,34,58,34,107,107,107,34,44,34,99,111,108,111,114,34,58,34,66,108,117,101,34,125,44,123,34,105,100,34,58,34,56,51,51,50,99,52,56,51,45,102,56,57,99,45,52,48,53,55,45,57,101,99,57,45,101,50,53,53,56,54,53,48,52,52,51,56,34,44,34,110,97,109,101,34,58,34,98,110,109,34,44,34,99,111,108,111,114,34,58,34,89,101,108,108,111,119,34,125,44,123,34,105,100,34,58,34,52,53,53,98,100,49,56,51,45,54,54,57,102,45,52,98,49,55,45,56,99,56,57,45,56,102,56,53,48,102,102,50,48,51,54,52,34,44,34,110,97,109,101,34,58,34,118,110,109,34,44,34,99,111,108,111,114,34,58,34,79,114,97,110,103,101,34,125,44,123,34,105,100,34,58,34,57,97,102,51,49,102,100,53,45,98,54,53,52,45,52,54,54,54,45,98,101,101,57,45,101,50,52,55,49,51,55,50,53,49,102,53,34,44,34,110,97,109,101,34,58,34,106,106,109,34,44,34,99,111,108,111,114,34,58,34,65,113,117,97,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,209,142,245,200,15,160,3,1,161,209,142,245,200,15,154,3,1,161,209,142,245,200,15,162,3,1,161,209,142,245,200,15,163,3,1,161,209,142,245,200,15,164,3,1,161,209,142,245,200,15,165,3,1,161,209,142,245,200,15,166,3,1,161,209,142,245,200,15,167,3,1,161,209,142,245,200,15,168,3,1,161,209,142,245,200,15,169,3,1,161,209,142,245,200,15,170,3,1,161,209,142,245,200,15,171,3,1,161,209,142,245,200,15,172,3,1,161,209,142,245,200,15,173,3,1,161,209,142,245,200,15,174,3,1,161,209,142,245,200,15,175,3,1,161,209,142,245,200,15,176,3,1,161,209,142,245,200,15,177,3,1,168,209,142,245,200,15,178,3,1,122,0,0,0,0,102,65,147,48,168,209,142,245,200,15,179,3,1,119,12,109,117,108,116,105,32,115,101,108,101,99,116,161,209,142,245,200,15,181,1,1,136,187,163,190,240,15,66,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,187,163,190,240,15,11,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,184,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,182,201,218,189,1,10,1,136,168,211,203,155,8,63,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,137,227,133,241,2,92,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,188,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,182,201,218,189,1,8,1,136,168,211,203,155,8,71,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,168,211,203,155,8,18,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,192,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,180,1,1,136,187,163,190,240,15,70,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,137,227,133,241,2,43,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,196,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,182,201,218,189,1,2,1,136,168,211,203,155,8,67,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,137,227,133,241,2,133,1,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,200,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,182,201,218,189,1,0,1,136,187,163,190,240,15,74,1,118,1,2,105,100,119,6,55,75,88,95,99,120,39,0,187,163,190,240,15,52,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,204,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,2,6,55,75,88,95,99,120,1,40,0,209,142,245,200,15,206,3,2,105,100,1,119,6,55,75,88,95,99,120,33,0,209,142,245,200,15,206,3,4,110,97,109,101,1,40,0,209,142,245,200,15,206,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,23,60,33,0,209,142,245,200,15,206,3,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,209,142,245,200,15,206,3,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,209,142,245,200,15,206,3,2,116,121,1,39,0,209,142,245,200,15,206,3,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,209,142,245,200,15,213,3,1,57,1,33,0,209,142,245,200,15,214,3,11,100,97,116,101,95,102,111,114,109,97,116,1,33,0,209,142,245,200,15,214,3,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,209,142,245,200,15,214,3,10,102,105,101,108,100,95,116,121,112,101,1,33,0,209,142,245,200,15,214,3,11,116,105,109,101,95,102,111,114,109,97,116,1,161,209,142,245,200,15,182,3,1,136,209,142,245,200,15,183,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,187,163,190,240,15,11,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,221,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,186,3,1,136,209,142,245,200,15,187,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,137,227,133,241,2,92,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,225,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,190,3,1,136,209,142,245,200,15,191,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,168,211,203,155,8,18,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,229,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,194,3,1,136,209,142,245,200,15,195,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,137,227,133,241,2,43,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,233,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,198,3,1,136,209,142,245,200,15,199,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,137,227,133,241,2,133,1,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,237,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,202,3,1,136,209,142,245,200,15,203,3,1,118,1,2,105,100,119,6,76,99,121,68,75,106,39,0,187,163,190,240,15,52,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,241,3,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,2,6,76,99,121,68,75,106,1,40,0,209,142,245,200,15,243,3,2,105,100,1,119,6,76,99,121,68,75,106,40,0,209,142,245,200,15,243,3,4,110,97,109,101,1,119,13,76,97,115,116,32,109,111,100,105,102,105,101,100,40,0,209,142,245,200,15,243,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,23,66,40,0,209,142,245,200,15,243,3,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,67,23,66,40,0,209,142,245,200,15,243,3,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,209,142,245,200,15,243,3,2,116,121,1,122,0,0,0,0,0,0,0,8,39,0,209,142,245,200,15,243,3,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,209,142,245,200,15,250,3,1,56,1,40,0,209,142,245,200,15,251,3,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,209,142,245,200,15,251,3,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,209,142,245,200,15,251,3,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,209,142,245,200,15,251,3,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,161,209,142,245,200,15,210,3,1,161,209,142,245,200,15,208,3,1,161,209,142,245,200,15,128,4,1,161,209,142,245,200,15,212,3,1,161,209,142,245,200,15,217,3,1,161,209,142,245,200,15,215,3,1,161,209,142,245,200,15,216,3,1,161,209,142,245,200,15,218,3,1,161,209,142,245,200,15,130,4,1,161,209,142,245,200,15,132,4,1,161,209,142,245,200,15,134,4,1,161,209,142,245,200,15,135,4,1,161,209,142,245,200,15,133,4,1,161,209,142,245,200,15,136,4,1,161,209,142,245,200,15,140,4,1,161,209,142,245,200,15,138,4,1,161,209,142,245,200,15,139,4,1,161,209,142,245,200,15,137,4,1,161,209,142,245,200,15,141,4,1,161,209,142,245,200,15,129,4,1,161,209,142,245,200,15,146,4,1,161,209,142,245,200,15,131,4,1,161,209,142,245,200,15,143,4,1,161,209,142,245,200,15,145,4,1,161,209,142,245,200,15,144,4,1,161,209,142,245,200,15,142,4,1,161,209,142,245,200,15,148,4,1,161,209,142,245,200,15,153,4,1,161,209,142,245,200,15,152,4,1,161,209,142,245,200,15,150,4,1,161,209,142,245,200,15,151,4,1,161,209,142,245,200,15,154,4,1,161,209,142,245,200,15,155,4,1,161,209,142,245,200,15,157,4,1,161,209,142,245,200,15,156,4,1,161,209,142,245,200,15,158,4,1,161,209,142,245,200,15,159,4,1,161,209,142,245,200,15,147,4,1,161,209,142,245,200,15,164,4,1,161,209,142,245,200,15,149,4,1,161,209,142,245,200,15,163,4,1,161,209,142,245,200,15,162,4,1,161,209,142,245,200,15,160,4,1,161,209,142,245,200,15,161,4,1,161,209,142,245,200,15,166,4,1,161,209,142,245,200,15,171,4,1,161,209,142,245,200,15,170,4,1,161,209,142,245,200,15,168,4,1,161,209,142,245,200,15,169,4,1,161,209,142,245,200,15,172,4,1,161,209,142,245,200,15,176,4,1,161,209,142,245,200,15,174,4,1,161,209,142,245,200,15,175,4,1,161,209,142,245,200,15,173,4,1,161,209,142,245,200,15,177,4,1,168,209,142,245,200,15,165,4,1,119,10,67,114,101,97,116,101,100,32,97,116,161,209,142,245,200,15,182,4,1,168,209,142,245,200,15,167,4,1,122,0,0,0,0,0,0,0,9,161,209,142,245,200,15,181,4,1,161,209,142,245,200,15,180,4,1,161,209,142,245,200,15,178,4,1,161,209,142,245,200,15,179,4,1,161,209,142,245,200,15,184,4,1,161,209,142,245,200,15,186,4,1,161,209,142,245,200,15,189,4,1,161,209,142,245,200,15,188,4,1,161,209,142,245,200,15,187,4,1,168,209,142,245,200,15,190,4,1,122,0,0,0,0,102,67,41,222,168,209,142,245,200,15,192,4,1,122,0,0,0,0,0,0,0,4,168,209,142,245,200,15,191,4,1,121,168,209,142,245,200,15,194,4,1,122,0,0,0,0,0,0,0,0,168,209,142,245,200,15,193,4,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,219,3,1,136,209,142,245,200,15,220,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,187,163,190,240,15,11,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,202,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,223,3,1,136,209,142,245,200,15,224,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,137,227,133,241,2,92,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,206,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,227,3,1,136,209,142,245,200,15,228,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,168,211,203,155,8,18,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,210,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,231,3,1,136,209,142,245,200,15,232,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,137,227,133,241,2,43,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,214,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,235,3,1,136,209,142,245,200,15,236,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,137,227,133,241,2,133,1,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,218,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,239,3,1,136,209,142,245,200,15,240,3,1,118,1,2,105,100,119,6,120,69,81,65,111,75,39,0,187,163,190,240,15,52,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,222,4,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,2,6,120,69,81,65,111,75,1,40,0,209,142,245,200,15,224,4,2,105,100,1,119,6,120,69,81,65,111,75,40,0,209,142,245,200,15,224,4,4,110,97,109,101,1,119,9,67,104,101,99,107,108,105,115,116,40,0,209,142,245,200,15,224,4,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,49,249,40,0,209,142,245,200,15,224,4,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,67,49,249,40,0,209,142,245,200,15,224,4,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,209,142,245,200,15,224,4,2,116,121,1,122,0,0,0,0,0,0,0,7,39,0,209,142,245,200,15,224,4,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,209,142,245,200,15,231,4,1,55,1,161,227,250,198,245,13,0,1,1,0,137,227,133,241,2,58,1,0,3,161,209,142,245,200,15,233,4,1,129,209,142,245,200,15,234,4,1,0,3,161,209,142,245,200,15,238,4,1,0,3,161,209,142,245,200,15,243,4,1,0,3,161,209,142,245,200,15,247,4,3,129,209,142,245,200,15,239,4,1,0,3,161,209,142,245,200,15,253,4,1,129,209,142,245,200,15,254,4,1,0,3,161,209,142,245,200,15,130,5,1,129,209,142,245,200,15,131,5,1,0,3,161,209,142,245,200,15,135,5,4,129,209,142,245,200,15,136,5,1,0,3,161,146,198,138,224,6,15,1,136,182,201,218,189,1,5,1,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,161,209,142,245,200,15,204,4,1,136,182,201,218,189,1,11,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,161,209,142,245,200,15,208,4,1,136,182,201,218,189,1,9,1,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,161,209,142,245,200,15,143,5,1,136,182,201,218,189,1,7,1,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,161,209,142,245,200,15,216,4,1,136,182,201,218,189,1,3,1,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,161,209,142,245,200,15,220,4,1,136,182,201,218,189,1,1,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,161,209,142,245,200,15,154,5,1,161,177,178,255,174,1,12,1,161,177,178,255,174,1,11,1,161,177,178,255,174,1,14,1,161,177,178,255,174,1,13,1,161,209,142,245,200,15,160,5,1,161,177,178,255,174,1,24,1,161,177,178,255,174,1,23,1,161,177,178,255,174,1,25,1,161,177,178,255,174,1,22,1,161,209,142,245,200,15,165,5,1,168,227,250,198,245,13,2,1,119,6,89,80,102,105,50,109,168,227,250,198,245,13,3,1,119,1,56,168,227,250,198,245,13,4,1,119,6,80,78,49,51,122,82,168,227,250,198,245,13,1,1,122,0,0,0,0,0,0,0,5,161,209,142,245,200,15,170,5,1,168,177,178,255,174,1,34,1,119,6,117,106,117,122,75,103,168,177,178,255,174,1,37,1,119,1,54,168,177,178,255,174,1,35,1,122,0,0,0,0,0,0,0,7,168,177,178,255,174,1,36,1,119,6,115,111,118,85,116,69,161,209,142,245,200,15,175,5,3,25,252,220,241,227,14,0,161,227,250,198,245,13,0,1,1,0,137,227,133,241,2,58,1,0,3,161,252,220,241,227,14,0,1,129,252,220,241,227,14,1,1,0,3,161,252,220,241,227,14,5,3,129,252,220,241,227,14,6,1,0,3,161,252,220,241,227,14,12,1,129,252,220,241,227,14,13,1,0,3,161,252,220,241,227,14,17,1,1,0,137,227,133,241,2,56,1,0,6,161,252,220,241,227,14,22,1,129,252,220,241,227,14,23,1,0,6,129,252,220,241,227,14,31,1,0,6,161,252,220,241,227,14,30,1,129,252,220,241,227,14,38,1,0,6,129,252,220,241,227,14,46,1,0,6,1,252,240,184,224,14,0,161,246,154,200,238,10,10,24,1,227,170,238,211,14,0,161,135,173,169,205,15,3,16,1,141,132,223,206,14,0,161,132,238,182,192,14,4,5,1,132,238,182,192,14,0,161,203,248,208,163,4,3,5,5,188,252,160,180,14,0,161,185,145,225,175,8,235,2,1,135,209,142,245,200,15,144,5,1,40,0,188,252,160,180,14,1,8,102,105,101,108,100,95,105,100,1,119,6,89,53,52,81,73,115,40,0,188,252,160,180,14,1,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,0,40,0,188,252,160,180,14,1,2,105,100,1,119,8,115,58,104,97,52,74,106,113,1,175,150,167,163,14,0,161,247,187,192,242,6,5,4,2,142,215,187,158,14,0,161,149,154,146,112,15,6,161,142,215,187,158,14,5,4,1,192,183,207,147,14,0,161,182,139,168,140,5,35,43,5,227,250,198,245,13,0,161,146,198,138,224,6,14,1,161,177,178,255,174,1,29,1,161,177,178,255,174,1,31,1,161,177,178,255,174,1,30,1,161,177,178,255,174,1,28,1,49,178,161,242,226,13,0,161,171,216,132,162,10,92,1,129,185,145,225,175,8,150,3,1,0,6,129,178,161,242,226,13,1,1,0,6,129,178,161,242,226,13,8,1,0,6,129,178,161,242,226,13,15,1,0,6,129,178,161,242,226,13,22,1,0,6,129,178,161,242,226,13,29,1,0,6,161,178,161,242,226,13,0,1,129,178,161,242,226,13,36,1,0,6,129,178,161,242,226,13,44,1,0,6,129,178,161,242,226,13,51,1,0,6,129,178,161,242,226,13,58,1,0,6,129,178,161,242,226,13,65,1,0,6,161,178,161,242,226,13,43,1,129,178,161,242,226,13,72,1,0,6,129,178,161,242,226,13,80,1,0,6,129,178,161,242,226,13,87,1,0,6,129,178,161,242,226,13,94,1,0,6,161,178,161,242,226,13,79,1,129,178,161,242,226,13,101,1,0,6,129,178,161,242,226,13,109,1,0,6,129,178,161,242,226,13,116,1,0,6,161,178,161,242,226,13,108,1,129,178,161,242,226,13,123,1,0,6,129,178,161,242,226,13,131,1,1,0,6,161,178,161,242,226,13,130,1,1,129,178,161,242,226,13,138,1,1,0,6,168,178,161,242,226,13,145,1,1,122,0,0,0,0,102,77,81,51,1,154,253,168,186,13,0,161,162,129,240,225,15,18,6,1,191,215,204,166,13,0,161,210,221,238,195,8,19,12,2,180,149,168,150,13,0,161,165,237,195,173,1,7,1,161,142,215,187,158,14,9,9,1,206,242,242,141,13,0,161,180,132,165,192,8,0,95,1,201,191,253,157,12,0,161,187,159,219,213,8,1,2,1,253,223,254,206,11,0,161,174,158,229,225,9,1,2,1,174,182,200,164,11,0,161,210,221,238,195,8,19,2,1,246,154,200,238,10,0,161,192,183,207,147,14,42,11,1,160,159,229,236,10,0,161,193,174,143,180,7,17,34,1,224,218,133,236,10,0,161,247,149,251,192,4,3,13,76,171,216,132,162,10,0,39,0,137,227,133,241,2,3,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,1,40,0,171,216,132,162,10,0,2,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,171,216,132,162,10,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,171,216,132,162,10,0,4,110,97,109,101,1,119,14,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,171,216,132,162,10,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,171,216,132,162,10,0,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,171,216,132,162,10,0,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,171,216,132,162,10,6,1,49,1,40,0,171,216,132,162,10,7,22,99,111,108,108,97,112,115,101,95,104,105,100,100,101,110,95,103,114,111,117,112,115,1,121,40,0,171,216,132,162,10,7,21,104,105,100,101,95,117,110,103,114,111,117,112,101,100,95,99,111,108,117,109,110,1,121,40,0,171,216,132,162,10,0,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,39,0,171,216,132,162,10,0,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,171,216,132,162,10,0,7,102,105,108,116,101,114,115,0,39,0,171,216,132,162,10,0,6,103,114,111,117,112,115,0,39,0,171,216,132,162,10,0,5,115,111,114,116,115,0,39,0,171,216,132,162,10,0,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,171,216,132,162,10,15,12,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,118,1,2,105,100,119,6,115,111,118,85,116,69,118,1,2,105,100,119,6,54,76,70,72,66,54,118,1,2,105,100,119,6,86,89,52,50,103,49,118,1,2,105,100,119,6,106,87,101,95,116,54,118,1,2,105,100,119,6,55,75,88,95,99,120,118,1,2,105,100,119,6,76,99,121,68,75,106,118,1,2,105,100,119,6,120,69,81,65,111,75,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,171,216,132,162,10,0,10,114,111,119,95,111,114,100,101,114,115,0,8,0,171,216,132,162,10,28,8,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,118,2,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,161,171,216,132,162,10,5,1,7,0,171,216,132,162,10,13,1,33,0,171,216,132,162,10,38,6,103,114,111,117,112,115,1,33,0,171,216,132,162,10,38,2,116,121,1,33,0,171,216,132,162,10,38,7,99,111,110,116,101,110,116,1,33,0,171,216,132,162,10,38,8,102,105,101,108,100,95,105,100,1,33,0,171,216,132,162,10,38,2,105,100,1,161,171,216,132,162,10,37,1,168,171,216,132,162,10,41,1,119,0,167,171,216,132,162,10,39,0,8,0,171,216,132,162,10,46,4,118,2,2,105,100,119,6,70,114,115,115,74,100,7,118,105,115,105,98,108,101,120,118,2,2,105,100,119,4,120,90,48,51,7,118,105,115,105,98,108,101,120,118,2,2,105,100,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,36,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,168,171,216,132,162,10,40,1,122,0,0,0,0,0,0,0,3,168,171,216,132,162,10,42,1,119,6,70,114,115,115,74,100,168,171,216,132,162,10,43,1,119,8,103,58,102,104,55,54,48,95,161,185,145,225,175,8,189,2,1,136,209,142,245,200,15,151,5,1,118,2,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,6,104,101,105,103,104,116,125,60,161,185,145,225,175,8,233,2,1,136,209,142,245,200,15,149,5,1,118,2,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,6,104,101,105,103,104,116,125,60,161,171,216,132,162,10,44,1,136,171,216,132,162,10,36,1,118,2,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,6,104,101,105,103,104,116,125,60,161,188,252,160,180,14,0,1,136,209,142,245,200,15,155,5,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,161,185,145,225,175,8,234,2,1,136,209,142,245,200,15,159,5,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,161,185,145,225,175,8,197,2,1,136,209,142,245,200,15,157,5,1,118,2,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,6,104,101,105,103,104,116,125,60,161,185,145,225,175,8,181,2,1,136,209,142,245,200,15,153,5,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,161,171,216,132,162,10,60,1,161,209,142,245,200,15,167,5,1,161,209,142,245,200,15,169,5,1,161,209,142,245,200,15,166,5,1,161,209,142,245,200,15,168,5,1,168,171,216,132,162,10,54,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,55,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,168,171,216,132,162,10,56,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,57,1,118,2,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,6,104,101,105,103,104,116,125,60,168,171,216,132,162,10,58,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,59,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,161,171,216,132,162,10,68,1,136,171,216,132,162,10,61,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,168,171,216,132,162,10,62,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,63,1,118,2,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,6,104,101,105,103,104,116,125,60,168,171,216,132,162,10,64,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,65,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,168,171,216,132,162,10,66,1,122,0,0,0,0,102,75,60,209,136,171,216,132,162,10,67,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,161,171,216,132,162,10,79,1,168,171,216,132,162,10,70,1,119,1,57,168,171,216,132,162,10,72,1,119,6,70,114,115,115,74,100,168,171,216,132,162,10,71,1,122,0,0,0,0,0,0,0,7,168,171,216,132,162,10,69,1,119,6,67,101,97,68,98,122,161,171,216,132,162,10,87,1,168,209,142,245,200,15,161,5,1,119,6,121,81,77,51,67,56,168,209,142,245,200,15,164,5,1,119,6,89,53,52,81,73,115,168,209,142,245,200,15,163,5,1,119,2,49,48,168,209,142,245,200,15,162,5,1,122,0,0,0,0,0,0,0,5,1,174,158,229,225,9,0,161,227,170,238,211,14,15,2,1,200,156,140,203,9,0,161,250,147,239,143,1,1,2,1,219,179,165,244,8,0,161,140,242,215,248,4,34,4,1,187,159,219,213,8,0,161,200,168,240,223,7,1,2,1,210,221,238,195,8,0,161,211,235,145,81,15,20,1,180,132,165,192,8,0,161,211,189,178,91,79,1,163,1,185,145,225,175,8,0,161,252,220,241,227,14,45,1,129,252,220,241,227,14,53,1,0,6,129,185,145,225,175,8,1,1,0,6,129,185,145,225,175,8,8,1,0,6,161,185,145,225,175,8,0,1,129,185,145,225,175,8,15,1,0,6,129,185,145,225,175,8,23,1,0,6,129,185,145,225,175,8,30,1,0,6,129,185,145,225,175,8,37,1,0,6,161,185,145,225,175,8,22,1,129,185,145,225,175,8,44,1,0,6,129,185,145,225,175,8,52,1,0,6,129,185,145,225,175,8,59,1,0,6,129,185,145,225,175,8,66,1,0,6,129,185,145,225,175,8,73,1,0,6,161,185,145,225,175,8,51,1,129,185,145,225,175,8,80,1,0,6,129,185,145,225,175,8,88,1,0,6,129,185,145,225,175,8,95,1,0,6,129,185,145,225,175,8,102,1,0,6,129,185,145,225,175,8,109,1,0,6,129,185,145,225,175,8,116,1,0,6,161,209,142,245,200,15,182,5,1,129,185,145,225,175,8,123,1,0,6,129,185,145,225,175,8,131,1,1,0,6,129,185,145,225,175,8,138,1,1,0,6,129,185,145,225,175,8,145,1,1,0,6,129,185,145,225,175,8,152,1,1,0,6,129,185,145,225,175,8,159,1,1,0,6,161,185,145,225,175,8,130,1,1,129,185,145,225,175,8,166,1,1,0,6,129,185,145,225,175,8,174,1,1,0,6,129,185,145,225,175,8,181,1,1,0,6,129,185,145,225,175,8,188,1,1,0,6,129,185,145,225,175,8,195,1,1,0,6,129,185,145,225,175,8,202,1,1,0,6,161,185,145,225,175,8,173,1,1,129,185,145,225,175,8,209,1,1,0,6,129,185,145,225,175,8,217,1,1,0,6,129,185,145,225,175,8,224,1,1,0,6,129,185,145,225,175,8,231,1,1,0,6,129,185,145,225,175,8,238,1,1,0,6,129,185,145,225,175,8,245,1,1,0,6,161,185,145,225,175,8,216,1,1,129,185,145,225,175,8,252,1,1,0,6,129,185,145,225,175,8,132,2,1,0,6,129,185,145,225,175,8,139,2,1,0,6,129,185,145,225,175,8,146,2,1,0,6,129,185,145,225,175,8,153,2,1,0,6,129,185,145,225,175,8,160,2,1,0,6,129,185,145,225,175,8,167,2,1,0,6,161,209,142,245,200,15,152,5,1,136,209,142,245,200,15,209,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,168,211,203,155,8,18,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,183,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,185,145,225,175,8,131,2,1,136,209,142,245,200,15,213,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,137,227,133,241,2,43,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,187,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,209,142,245,200,15,150,5,1,136,209,142,245,200,15,205,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,137,227,133,241,2,92,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,191,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,148,5,1,136,209,142,245,200,15,201,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,187,163,190,240,15,11,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,195,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,156,5,1,136,209,142,245,200,15,217,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,137,227,133,241,2,133,1,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,199,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,209,142,245,200,15,158,5,1,136,209,142,245,200,15,221,4,1,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,187,163,190,240,15,52,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,203,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,2,6,52,57,85,69,86,53,1,40,0,185,145,225,175,8,205,2,2,105,100,1,119,6,52,57,85,69,86,53,33,0,185,145,225,175,8,205,2,4,110,97,109,101,1,40,0,185,145,225,175,8,205,2,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,69,129,177,33,0,185,145,225,175,8,205,2,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,185,145,225,175,8,205,2,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,185,145,225,175,8,205,2,2,116,121,1,39,0,185,145,225,175,8,205,2,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,185,145,225,175,8,212,2,1,48,1,40,0,185,145,225,175,8,213,2,4,100,97,116,97,1,119,0,161,185,145,225,175,8,209,2,1,161,185,145,225,175,8,207,2,1,161,185,145,225,175,8,215,2,1,161,185,145,225,175,8,216,2,1,161,185,145,225,175,8,217,2,1,161,185,145,225,175,8,218,2,1,161,185,145,225,175,8,219,2,1,168,185,145,225,175,8,220,2,1,119,4,116,105,109,101,161,185,145,225,175,8,221,2,1,168,185,145,225,175,8,211,2,1,122,0,0,0,0,0,0,0,2,39,0,185,145,225,175,8,212,2,1,50,1,40,0,185,145,225,175,8,225,2,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,185,145,225,175,8,225,2,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,185,145,225,175,8,225,2,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,168,185,145,225,175,8,223,2,1,122,0,0,0,0,102,69,129,187,40,0,185,145,225,175,8,213,2,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,40,0,185,145,225,175,8,213,2,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,185,145,225,175,8,213,2,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,161,185,145,225,175,8,193,2,1,161,185,145,225,175,8,201,2,1,161,185,145,225,175,8,185,2,1,129,185,145,225,175,8,174,2,1,0,6,129,185,145,225,175,8,236,2,1,0,6,129,185,145,225,175,8,243,2,1,0,6,129,185,145,225,175,8,250,2,1,0,6,129,185,145,225,175,8,129,3,1,0,6,129,185,145,225,175,8,136,3,1,0,6,129,185,145,225,175,8,143,3,1,0,6,81,168,211,203,155,8,0,161,137,227,133,241,2,20,1,161,137,227,133,241,2,25,1,161,137,227,133,241,2,150,1,1,168,137,227,133,241,2,115,1,119,6,70,114,115,115,74,100,168,137,227,133,241,2,113,1,119,8,103,58,107,56,113,69,117,118,168,137,227,133,241,2,116,1,122,0,0,0,0,0,0,0,3,168,137,227,133,241,2,114,1,119,0,167,137,227,133,241,2,117,0,8,0,168,211,203,155,8,7,2,118,2,2,105,100,119,6,70,114,115,115,74,100,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,4,120,90,48,51,39,0,137,227,133,241,2,3,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,1,40,0,168,211,203,155,8,10,2,105,100,1,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,40,0,168,211,203,155,8,10,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,168,211,203,155,8,10,4,110,97,109,101,1,119,4,71,114,105,100,40,0,168,211,203,155,8,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,178,5,33,0,168,211,203,155,8,10,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,168,211,203,155,8,10,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,168,211,203,155,8,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,168,211,203,155,8,10,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,168,211,203,155,8,10,7,102,105,108,116,101,114,115,0,39,0,168,211,203,155,8,10,6,103,114,111,117,112,115,0,39,0,168,211,203,155,8,10,5,115,111,114,116,115,0,39,0,168,211,203,155,8,10,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,168,211,203,155,8,22,5,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,118,1,2,105,100,119,6,115,111,118,85,116,69,39,0,168,211,203,155,8,10,10,114,111,119,95,111,114,100,101,114,115,0,8,0,168,211,203,155,8,28,3,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,161,137,227,133,241,2,154,1,1,40,0,137,227,133,241,2,69,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,137,227,133,241,2,69,4,119,114,97,112,1,121,168,137,227,133,241,2,70,1,122,0,0,0,0,0,0,0,2,161,168,211,203,155,8,2,1,136,137,227,133,241,2,151,1,1,118,1,2,105,100,119,6,54,76,70,72,66,54,39,0,137,227,133,241,2,92,6,54,76,70,72,66,54,1,40,0,168,211,203,155,8,38,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,137,227,133,241,2,146,1,1,136,137,227,133,241,2,147,1,1,118,1,2,105,100,119,6,54,76,70,72,66,54,39,0,137,227,133,241,2,133,1,6,54,76,70,72,66,54,1,40,0,168,211,203,155,8,42,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,168,211,203,155,8,15,1,136,168,211,203,155,8,27,1,118,1,2,105,100,119,6,54,76,70,72,66,54,39,0,168,211,203,155,8,18,6,54,76,70,72,66,54,1,40,0,168,211,203,155,8,46,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,168,211,203,155,8,32,1,136,137,227,133,241,2,155,1,1,118,1,2,105,100,119,6,54,76,70,72,66,54,39,0,137,227,133,241,2,43,6,54,76,70,72,66,54,1,40,0,168,211,203,155,8,50,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,2,6,54,76,70,72,66,54,1,40,0,168,211,203,155,8,52,2,105,100,1,119,6,54,76,70,72,66,54,33,0,168,211,203,155,8,52,4,110,97,109,101,1,40,0,168,211,203,155,8,52,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,230,211,33,0,168,211,203,155,8,52,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,168,211,203,155,8,52,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,168,211,203,155,8,52,2,116,121,1,39,0,168,211,203,155,8,52,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,168,211,203,155,8,59,1,48,1,40,0,168,211,203,155,8,60,4,100,97,116,97,1,119,0,161,168,211,203,155,8,36,1,136,168,211,203,155,8,37,1,118,1,2,105,100,119,6,86,89,52,50,103,49,39,0,137,227,133,241,2,92,6,86,89,52,50,103,49,1,40,0,168,211,203,155,8,64,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,168,211,203,155,8,40,1,136,168,211,203,155,8,41,1,118,1,2,105,100,119,6,86,89,52,50,103,49,39,0,137,227,133,241,2,133,1,6,86,89,52,50,103,49,1,40,0,168,211,203,155,8,68,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,168,211,203,155,8,44,1,136,168,211,203,155,8,45,1,118,1,2,105,100,119,6,86,89,52,50,103,49,39,0,168,211,203,155,8,18,6,86,89,52,50,103,49,1,40,0,168,211,203,155,8,72,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,168,211,203,155,8,48,1,136,168,211,203,155,8,49,1,118,1,2,105,100,119,6,86,89,52,50,103,49,39,0,137,227,133,241,2,43,6,86,89,52,50,103,49,1,40,0,168,211,203,155,8,76,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,2,6,86,89,52,50,103,49,1,40,0,168,211,203,155,8,78,2,105,100,1,119,6,86,89,52,50,103,49,33,0,168,211,203,155,8,78,4,110,97,109,101,1,40,0,168,211,203,155,8,78,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,230,213,33,0,168,211,203,155,8,78,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,168,211,203,155,8,78,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,168,211,203,155,8,78,2,116,121,1,39,0,168,211,203,155,8,78,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,168,211,203,155,8,85,1,48,1,40,0,168,211,203,155,8,86,4,100,97,116,97,1,119,0,1,150,194,135,131,8,0,161,206,242,242,141,13,94,12,1,200,168,240,223,7,0,161,180,149,168,150,13,9,2,1,216,247,253,206,7,0,161,141,132,223,206,14,4,2,1,193,174,143,180,7,0,161,150,194,135,131,8,11,18,1,202,170,215,178,7,0,161,191,215,204,166,13,11,24,1,157,197,217,249,6,0,161,224,218,133,236,10,12,3,1,247,187,192,242,6,0,161,134,200,133,143,5,1,6,1,248,220,249,231,6,0,161,200,156,140,203,9,1,29,18,146,198,138,224,6,0,161,137,227,133,241,2,162,1,1,161,137,227,133,241,2,169,1,1,161,137,227,133,241,2,168,1,1,161,137,227,133,241,2,167,1,1,161,146,198,138,224,6,0,1,168,146,198,138,224,6,2,1,122,0,0,0,0,0,0,0,1,168,146,198,138,224,6,1,1,122,0,0,0,0,0,0,0,3,168,146,198,138,224,6,3,1,119,0,161,187,163,190,240,15,81,1,168,187,163,190,240,15,83,1,122,0,0,0,0,0,0,0,10,39,0,187,163,190,240,15,84,2,49,48,1,33,0,146,198,138,224,6,10,11,100,97,116,97,98,97,115,101,95,105,100,1,161,146,198,138,224,6,8,1,40,0,187,163,190,240,15,85,11,100,97,116,97,98,97,115,101,95,105,100,1,119,0,161,234,232,155,212,3,0,1,161,177,178,255,174,1,0,1,168,146,198,138,224,6,12,1,122,0,0,0,0,102,67,52,219,168,146,198,138,224,6,11,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,1,158,173,179,170,6,0,161,183,238,200,180,5,5,81,1,183,238,200,180,5,0,161,160,159,229,236,10,33,6,2,174,250,146,158,5,0,161,216,247,253,206,7,1,1,168,174,250,146,158,5,0,1,122,0,0,0,0,102,88,107,140,1,134,200,133,143,5,0,161,201,191,253,157,12,1,2,1,182,139,168,140,5,0,161,253,223,254,206,11,1,36,1,140,242,215,248,4,0,161,157,197,217,249,6,2,35,2,186,204,138,236,4,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,112,161,186,204,138,236,4,111,6,1,247,149,251,192,4,0,161,248,220,249,231,6,28,4,1,203,248,208,163,4,0,161,202,170,215,178,7,23,4,1,128,137,148,150,4,0,161,154,253,168,186,13,5,39,4,234,232,155,212,3,0,161,177,178,255,174,1,32,1,168,137,227,133,241,2,54,1,122,0,0,0,0,0,0,0,150,168,137,227,133,241,2,55,1,120,168,137,227,133,241,2,53,1,122,0,0,0,0,0,0,0,0,156,1,137,227,133,241,2,0,39,1,4,100,97,116,97,8,100,97,116,97,98,97,115,101,1,40,0,137,227,133,241,2,0,2,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,39,0,137,227,133,241,2,0,6,102,105,101,108,100,115,1,39,0,137,227,133,241,2,0,5,118,105,101,119,115,1,39,0,137,227,133,241,2,0,5,109,101,116,97,115,1,40,0,137,227,133,241,2,4,3,105,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,39,0,137,227,133,241,2,2,6,89,53,52,81,73,115,1,40,0,137,227,133,241,2,6,2,105,100,1,119,6,89,53,52,81,73,115,40,0,137,227,133,241,2,6,4,110,97,109,101,1,119,4,78,97,109,101,40,0,137,227,133,241,2,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,40,0,137,227,133,241,2,6,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,48,108,138,40,0,137,227,133,241,2,6,10,105,115,95,112,114,105,109,97,114,121,1,120,40,0,137,227,133,241,2,6,2,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,6,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,137,227,133,241,2,13,1,48,1,40,0,137,227,133,241,2,14,4,100,97,116,97,1,119,0,39,0,137,227,133,241,2,2,6,70,114,115,115,74,100,1,40,0,137,227,133,241,2,16,2,105,100,1,119,6,70,114,115,115,74,100,33,0,137,227,133,241,2,16,4,110,97,109,101,1,40,0,137,227,133,241,2,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,33,0,137,227,133,241,2,16,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,137,227,133,241,2,16,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,137,227,133,241,2,16,2,116,121,1,39,0,137,227,133,241,2,16,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,137,227,133,241,2,23,1,51,1,33,0,137,227,133,241,2,24,7,99,111,110,116,101,110,116,1,39,0,137,227,133,241,2,2,6,89,80,102,105,50,109,1,40,0,137,227,133,241,2,26,2,105,100,1,119,6,89,80,102,105,50,109,40,0,137,227,133,241,2,26,4,110,97,109,101,1,119,4,68,111,110,101,40,0,137,227,133,241,2,26,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,40,0,137,227,133,241,2,26,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,48,108,138,40,0,137,227,133,241,2,26,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,137,227,133,241,2,26,2,116,121,1,122,0,0,0,0,0,0,0,5,39,0,137,227,133,241,2,26,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,137,227,133,241,2,33,1,53,1,39,0,137,227,133,241,2,3,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,1,40,0,137,227,133,241,2,35,2,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,137,227,133,241,2,35,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,137,227,133,241,2,35,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,137,227,133,241,2,35,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,33,0,137,227,133,241,2,35,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,137,227,133,241,2,35,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,137,227,133,241,2,35,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,35,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,137,227,133,241,2,43,6,70,114,115,115,74,100,1,40,0,137,227,133,241,2,44,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,40,0,137,227,133,241,2,44,4,119,114,97,112,1,121,40,0,137,227,133,241,2,44,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,39,0,137,227,133,241,2,43,6,89,80,102,105,50,109,1,40,0,137,227,133,241,2,48,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,137,227,133,241,2,48,4,119,114,97,112,1,121,40,0,137,227,133,241,2,48,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,43,6,89,53,52,81,73,115,1,33,0,137,227,133,241,2,52,10,118,105,115,105,98,105,108,105,116,121,1,33,0,137,227,133,241,2,52,5,119,105,100,116,104,1,33,0,137,227,133,241,2,52,4,119,114,97,112,1,39,0,137,227,133,241,2,35,7,102,105,108,116,101,114,115,0,39,0,137,227,133,241,2,35,6,103,114,111,117,112,115,0,39,0,137,227,133,241,2,35,5,115,111,114,116,115,0,39,0,137,227,133,241,2,35,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,59,3,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,39,0,137,227,133,241,2,35,10,114,111,119,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,63,3,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,118,2,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,161,137,227,133,241,2,40,1,136,137,227,133,241,2,62,1,118,1,2,105,100,119,6,84,102,117,121,104,84,39,0,137,227,133,241,2,43,6,84,102,117,121,104,84,1,33,0,137,227,133,241,2,69,10,118,105,115,105,98,105,108,105,116,121,1,39,0,137,227,133,241,2,2,6,84,102,117,121,104,84,1,40,0,137,227,133,241,2,71,2,105,100,1,119,6,84,102,117,121,104,84,40,0,137,227,133,241,2,71,4,110,97,109,101,1,119,4,84,101,120,116,40,0,137,227,133,241,2,71,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,111,178,40,0,137,227,133,241,2,71,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,48,111,178,40,0,137,227,133,241,2,71,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,137,227,133,241,2,71,2,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,71,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,137,227,133,241,2,78,1,48,1,40,0,137,227,133,241,2,79,4,100,97,116,97,1,119,0,39,0,137,227,133,241,2,3,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,1,40,0,137,227,133,241,2,81,2,105,100,1,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,40,0,137,227,133,241,2,81,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,137,227,133,241,2,81,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,137,227,133,241,2,81,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,159,33,0,137,227,133,241,2,81,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,137,227,133,241,2,81,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,137,227,133,241,2,87,1,49,1,40,0,137,227,133,241,2,88,21,104,105,100,101,95,117,110,103,114,111,117,112,101,100,95,99,111,108,117,109,110,1,121,40,0,137,227,133,241,2,88,22,99,111,108,108,97,112,115,101,95,104,105,100,100,101,110,95,103,114,111,117,112,115,1,121,40,0,137,227,133,241,2,81,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,39,0,137,227,133,241,2,81,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,137,227,133,241,2,81,7,102,105,108,116,101,114,115,0,39,0,137,227,133,241,2,81,6,103,114,111,117,112,115,0,39,0,137,227,133,241,2,81,5,115,111,114,116,115,0,39,0,137,227,133,241,2,81,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,96,4,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,39,0,137,227,133,241,2,81,10,114,111,119,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,101,3,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,161,137,227,133,241,2,86,1,7,0,137,227,133,241,2,94,1,33,0,137,227,133,241,2,106,2,105,100,1,33,0,137,227,133,241,2,106,6,103,114,111,117,112,115,1,33,0,137,227,133,241,2,106,7,99,111,110,116,101,110,116,1,33,0,137,227,133,241,2,106,8,102,105,101,108,100,95,105,100,1,33,0,137,227,133,241,2,106,2,116,121,1,161,137,227,133,241,2,105,1,161,137,227,133,241,2,107,1,161,137,227,133,241,2,109,1,161,137,227,133,241,2,110,1,161,137,227,133,241,2,111,1,161,137,227,133,241,2,108,1,0,1,39,0,137,227,133,241,2,3,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,1,40,0,137,227,133,241,2,119,2,105,100,1,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,40,0,137,227,133,241,2,119,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,137,227,133,241,2,119,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,137,227,133,241,2,119,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,162,33,0,137,227,133,241,2,119,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,137,227,133,241,2,119,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,137,227,133,241,2,125,1,50,1,40,0,137,227,133,241,2,126,9,108,97,121,111,117,116,95,116,121,1,122,0,0,0,0,0,0,0,0,40,0,137,227,133,241,2,126,17,102,105,114,115,116,95,100,97,121,95,111,102,95,119,101,101,107,1,122,0,0,0,0,0,0,0,0,40,0,137,227,133,241,2,126,8,102,105,101,108,100,95,105,100,1,119,6,115,111,118,85,116,69,40,0,137,227,133,241,2,126,13,115,104,111,119,95,119,101,101,107,101,110,100,115,1,120,40,0,137,227,133,241,2,126,17,115,104,111,119,95,119,101,101,107,95,110,117,109,98,101,114,115,1,120,40,0,137,227,133,241,2,119,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,39,0,137,227,133,241,2,119,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,137,227,133,241,2,119,7,102,105,108,116,101,114,115,0,39,0,137,227,133,241,2,119,6,103,114,111,117,112,115,0,39,0,137,227,133,241,2,119,5,115,111,114,116,115,0,39,0,137,227,133,241,2,119,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,137,1,4,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,39,0,137,227,133,241,2,119,10,114,111,119,95,111,114,100,101,114,115,0,8,0,137,227,133,241,2,142,1,3,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,161,137,227,133,241,2,124,1,136,137,227,133,241,2,141,1,1,118,1,2,105,100,119,6,115,111,118,85,116,69,39,0,137,227,133,241,2,133,1,6,115,111,118,85,116,69,1,40,0,137,227,133,241,2,148,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,137,227,133,241,2,112,1,136,137,227,133,241,2,100,1,118,1,2,105,100,119,6,115,111,118,85,116,69,39,0,137,227,133,241,2,92,6,115,111,118,85,116,69,1,40,0,137,227,133,241,2,152,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,137,227,133,241,2,67,1,136,137,227,133,241,2,68,1,118,1,2,105,100,119,6,115,111,118,85,116,69,39,0,137,227,133,241,2,43,6,115,111,118,85,116,69,1,40,0,137,227,133,241,2,156,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,137,227,133,241,2,2,6,115,111,118,85,116,69,1,40,0,137,227,133,241,2,158,1,2,105,100,1,119,6,115,111,118,85,116,69,33,0,137,227,133,241,2,158,1,4,110,97,109,101,1,40,0,137,227,133,241,2,158,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,162,33,0,137,227,133,241,2,158,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,137,227,133,241,2,158,1,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,137,227,133,241,2,158,1,2,116,121,1,39,0,137,227,133,241,2,158,1,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,137,227,133,241,2,165,1,1,50,1,33,0,137,227,133,241,2,166,1,11,116,105,109,101,122,111,110,101,95,105,100,1,33,0,137,227,133,241,2,166,1,11,116,105,109,101,95,102,111,114,109,97,116,1,33,0,137,227,133,241,2,166,1,11,100,97,116,101,95,102,111,114,109,97,116,1,71,193,140,213,146,2,0,39,0,137,227,133,241,2,3,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,1,40,0,193,140,213,146,2,0,2,105,100,1,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,40,0,193,140,213,146,2,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,193,140,213,146,2,0,4,110,97,109,101,1,119,12,86,105,101,119,32,111,102,32,71,114,105,100,40,0,193,140,213,146,2,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,40,0,193,140,213,146,2,0,11,109,111,100,105,102,105,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,39,0,193,140,213,146,2,0,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,193,140,213,146,2,0,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,193,140,213,146,2,0,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,193,140,213,146,2,0,7,102,105,108,116,101,114,115,0,39,0,193,140,213,146,2,0,6,103,114,111,117,112,115,0,39,0,193,140,213,146,2,0,5,115,111,114,116,115,0,39,0,193,140,213,146,2,0,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,12,12,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,118,1,2,105,100,119,6,115,111,118,85,116,69,118,1,2,105,100,119,6,54,76,70,72,66,54,118,1,2,105,100,119,6,86,89,52,50,103,49,118,1,2,105,100,119,6,106,87,101,95,116,54,118,1,2,105,100,119,6,55,75,88,95,99,120,118,1,2,105,100,119,6,76,99,121,68,75,106,118,1,2,105,100,119,6,120,69,81,65,111,75,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,193,140,213,146,2,0,10,114,111,119,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,25,10,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,118,2,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,118,2,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,6,104,101,105,103,104,116,125,60,39,0,137,227,133,241,2,3,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,1,40,0,193,140,213,146,2,36,2,105,100,1,119,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,40,0,193,140,213,146,2,36,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,193,140,213,146,2,36,4,110,97,109,101,1,119,22,86,105,101,119,32,111,102,32,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,193,140,213,146,2,36,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,193,140,213,146,2,36,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,193,140,213,146,2,36,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,193,140,213,146,2,42,1,49,1,40,0,193,140,213,146,2,43,21,104,105,100,101,95,117,110,103,114,111,117,112,101,100,95,99,111,108,117,109,110,1,121,40,0,193,140,213,146,2,43,22,99,111,108,108,97,112,115,101,95,104,105,100,100,101,110,95,103,114,111,117,112,115,1,121,40,0,193,140,213,146,2,36,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,39,0,193,140,213,146,2,36,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,193,140,213,146,2,36,7,102,105,108,116,101,114,115,0,39,0,193,140,213,146,2,36,6,103,114,111,117,112,115,0,39,0,193,140,213,146,2,36,5,115,111,114,116,115,0,39,0,193,140,213,146,2,36,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,51,12,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,118,1,2,105,100,119,6,115,111,118,85,116,69,118,1,2,105,100,119,6,54,76,70,72,66,54,118,1,2,105,100,119,6,86,89,52,50,103,49,118,1,2,105,100,119,6,106,87,101,95,116,54,118,1,2,105,100,119,6,55,75,88,95,99,120,118,1,2,105,100,119,6,76,99,121,68,75,106,118,1,2,105,100,119,6,120,69,81,65,111,75,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,193,140,213,146,2,36,10,114,111,119,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,64,10,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,118,2,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,161,193,140,213,146,2,41,1,7,0,193,140,213,146,2,49,1,33,0,193,140,213,146,2,76,6,103,114,111,117,112,115,1,33,0,193,140,213,146,2,76,2,116,121,1,33,0,193,140,213,146,2,76,8,102,105,101,108,100,95,105,100,1,33,0,193,140,213,146,2,76,2,105,100,1,33,0,193,140,213,146,2,76,7,99,111,110,116,101,110,116,1,168,193,140,213,146,2,75,1,122,0,0,0,0,102,79,7,25,168,193,140,213,146,2,79,1,119,6,70,114,115,115,74,100,168,193,140,213,146,2,78,1,122,0,0,0,0,0,0,0,3,168,193,140,213,146,2,80,1,119,8,103,58,105,88,95,87,48,73,167,193,140,213,146,2,77,0,8,0,193,140,213,146,2,86,4,118,2,2,105,100,119,6,70,114,115,115,74,100,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,4,120,90,48,51,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,36,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,168,193,140,213,146,2,81,1,119,0,39,0,137,227,133,241,2,3,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,1,40,0,193,140,213,146,2,92,2,105,100,1,119,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,40,0,193,140,213,146,2,92,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,193,140,213,146,2,92,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,193,140,213,146,2,92,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,40,0,193,140,213,146,2,92,11,109,111,100,105,102,105,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,39,0,193,140,213,146,2,92,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,193,140,213,146,2,98,1,50,1,40,0,193,140,213,146,2,99,17,102,105,114,115,116,95,100,97,121,95,111,102,95,119,101,101,107,1,122,0,0,0,0,0,0,0,0,40,0,193,140,213,146,2,99,8,102,105,101,108,100,95,105,100,1,119,6,52,57,85,69,86,53,40,0,193,140,213,146,2,99,9,108,97,121,111,117,116,95,116,121,1,122,0,0,0,0,0,0,0,0,40,0,193,140,213,146,2,99,13,115,104,111,119,95,119,101,101,107,101,110,100,115,1,120,40,0,193,140,213,146,2,99,17,115,104,111,119,95,119,101,101,107,95,110,117,109,98,101,114,115,1,120,40,0,193,140,213,146,2,92,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,39,0,193,140,213,146,2,92,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,193,140,213,146,2,92,7,102,105,108,116,101,114,115,0,39,0,193,140,213,146,2,92,6,103,114,111,117,112,115,0,39,0,193,140,213,146,2,92,5,115,111,114,116,115,0,39,0,193,140,213,146,2,92,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,110,12,118,1,2,105,100,119,6,89,53,52,81,73,115,118,1,2,105,100,119,6,70,114,115,115,74,100,118,1,2,105,100,119,6,89,80,102,105,50,109,118,1,2,105,100,119,6,84,102,117,121,104,84,118,1,2,105,100,119,6,115,111,118,85,116,69,118,1,2,105,100,119,6,54,76,70,72,66,54,118,1,2,105,100,119,6,86,89,52,50,103,49,118,1,2,105,100,119,6,106,87,101,95,116,54,118,1,2,105,100,119,6,55,75,88,95,99,120,118,1,2,105,100,119,6,76,99,121,68,75,106,118,1,2,105,100,119,6,120,69,81,65,111,75,118,1,2,105,100,119,6,52,57,85,69,86,53,39,0,193,140,213,146,2,92,10,114,111,119,95,111,114,100,101,114,115,0,8,0,193,140,213,146,2,123,10,118,2,2,105,100,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,118,2,2,105,100,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,118,2,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,12,182,201,218,189,1,0,161,229,168,135,118,231,4,1,136,229,168,135,118,232,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,161,229,168,135,118,237,4,1,136,229,168,135,118,238,4,1,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,161,229,168,135,118,235,4,1,136,229,168,135,118,236,4,1,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,161,229,168,135,118,233,4,1,136,229,168,135,118,234,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,161,229,168,135,118,241,4,1,136,229,168,135,118,242,4,1,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,161,229,168,135,118,239,4,1,136,229,168,135,118,240,4,1,118,2,2,105,100,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,6,104,101,105,103,104,116,125,60,37,177,178,255,174,1,0,161,187,163,190,240,15,65,1,161,187,163,190,240,15,35,1,161,187,163,190,240,15,32,1,0,2,161,187,163,190,240,15,34,1,161,187,163,190,240,15,37,1,161,187,163,190,240,15,36,1,161,187,163,190,240,15,69,1,39,0,137,227,133,241,2,35,12,99,97,108,99,117,108,97,116,105,111,110,115,0,7,0,177,178,255,174,1,9,1,33,0,177,178,255,174,1,10,2,116,121,1,33,0,177,178,255,174,1,10,2,105,100,1,33,0,177,178,255,174,1,10,8,102,105,101,108,100,95,105,100,1,33,0,177,178,255,174,1,10,17,99,97,108,99,117,108,97,116,105,111,110,95,118,97,108,117,101,1,161,177,178,255,174,1,8,1,135,177,178,255,174,1,10,1,33,0,177,178,255,174,1,16,8,102,105,101,108,100,95,105,100,1,33,0,177,178,255,174,1,16,2,105,100,1,33,0,177,178,255,174,1,16,2,116,121,1,33,0,177,178,255,174,1,16,17,99,97,108,99,117,108,97,116,105,111,110,95,118,97,108,117,101,1,161,177,178,255,174,1,15,1,161,177,178,255,174,1,20,1,161,177,178,255,174,1,18,1,161,177,178,255,174,1,19,1,161,177,178,255,174,1,17,1,161,177,178,255,174,1,21,1,135,177,178,255,174,1,16,1,33,0,177,178,255,174,1,27,2,105,100,1,33,0,177,178,255,174,1,27,2,116,121,1,33,0,177,178,255,174,1,27,17,99,97,108,99,117,108,97,116,105,111,110,95,118,97,108,117,101,1,33,0,177,178,255,174,1,27,8,102,105,101,108,100,95,105,100,1,161,177,178,255,174,1,26,1,135,177,178,255,174,1,27,1,33,0,177,178,255,174,1,33,2,105,100,1,33,0,177,178,255,174,1,33,2,116,121,1,33,0,177,178,255,174,1,33,8,102,105,101,108,100,95,105,100,1,33,0,177,178,255,174,1,33,17,99,97,108,99,117,108,97,116,105,111,110,95,118,97,108,117,101,1,1,165,237,195,173,1,0,161,253,149,229,85,13,8,1,250,147,239,143,1,0,161,158,173,179,170,6,80,2,243,4,229,168,135,118,0,161,168,211,203,155,8,56,1,168,168,211,203,155,8,54,1,119,4,84,101,120,116,161,229,168,135,118,0,1,168,168,211,203,155,8,58,1,122,0,0,0,0,0,0,0,6,39,0,168,211,203,155,8,59,1,54,1,40,0,229,168,135,118,4,3,117,114,108,1,119,0,40,0,229,168,135,118,4,7,99,111,110,116,101,110,116,1,119,0,168,229,168,135,118,2,1,122,0,0,0,0,102,60,204,0,40,0,168,211,203,155,8,60,7,99,111,110,116,101,110,116,1,119,0,40,0,168,211,203,155,8,60,3,117,114,108,1,119,0,161,177,178,255,174,1,0,1,161,187,163,190,240,15,69,1,161,168,211,203,155,8,82,1,161,168,211,203,155,8,80,1,161,229,168,135,118,12,1,161,168,211,203,155,8,84,1,39,0,168,211,203,155,8,85,1,49,1,33,0,229,168,135,118,16,5,115,99,97,108,101,1,33,0,229,168,135,118,16,4,110,97,109,101,1,33,0,229,168,135,118,16,6,102,111,114,109,97,116,1,33,0,229,168,135,118,16,6,115,121,109,98,111,108,1,161,229,168,135,118,14,1,40,0,168,211,203,155,8,86,4,110,97,109,101,1,119,6,78,117,109,98,101,114,40,0,168,211,203,155,8,86,6,115,121,109,98,111,108,1,119,3,82,85,66,40,0,168,211,203,155,8,86,6,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,168,211,203,155,8,86,5,115,99,97,108,101,1,122,0,0,0,0,0,0,0,0,161,229,168,135,118,10,1,161,229,168,135,118,11,1,161,229,168,135,118,21,1,161,229,168,135,118,17,1,161,229,168,135,118,19,1,161,229,168,135,118,20,1,161,229,168,135,118,18,1,161,229,168,135,118,28,1,161,229,168,135,118,13,1,161,229,168,135,118,33,1,161,229,168,135,118,15,1,161,229,168,135,118,32,1,161,229,168,135,118,30,1,161,229,168,135,118,31,1,161,229,168,135,118,29,1,161,229,168,135,118,35,1,161,229,168,135,118,37,1,161,229,168,135,118,39,1,161,229,168,135,118,38,1,161,229,168,135,118,40,1,161,229,168,135,118,41,1,161,229,168,135,118,44,1,161,229,168,135,118,45,1,161,229,168,135,118,42,1,161,229,168,135,118,43,1,161,187,163,190,240,15,73,1,136,187,163,190,240,15,64,1,118,2,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,6,104,101,105,103,104,116,125,60,161,229,168,135,118,27,1,136,137,227,133,241,2,66,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,161,229,168,135,118,26,1,136,187,163,190,240,15,23,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,161,168,211,203,155,8,66,1,136,137,227,133,241,2,145,1,1,118,2,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,6,104,101,105,103,104,116,125,60,161,168,211,203,155,8,62,1,136,137,227,133,241,2,104,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,161,168,211,203,155,8,70,1,136,168,211,203,155,8,31,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,161,229,168,135,118,46,1,161,229,168,135,118,34,1,161,229,168,135,118,63,1,161,229,168,135,118,36,1,161,229,168,135,118,50,1,161,229,168,135,118,47,1,161,229,168,135,118,49,1,161,229,168,135,118,48,1,161,229,168,135,118,65,1,161,229,168,135,118,69,1,161,229,168,135,118,68,1,161,229,168,135,118,67,1,161,229,168,135,118,70,1,161,229,168,135,118,71,1,161,229,168,135,118,74,1,161,229,168,135,118,73,1,161,229,168,135,118,72,1,161,229,168,135,118,75,1,161,229,168,135,118,76,1,161,229,168,135,118,64,1,161,229,168,135,118,81,1,161,229,168,135,118,66,1,161,229,168,135,118,79,1,161,229,168,135,118,80,1,161,229,168,135,118,78,1,161,229,168,135,118,77,1,161,229,168,135,118,83,1,161,229,168,135,118,88,1,161,229,168,135,118,86,1,161,229,168,135,118,87,1,161,229,168,135,118,85,1,161,229,168,135,118,89,1,161,229,168,135,118,91,1,161,229,168,135,118,93,1,161,229,168,135,118,92,1,161,229,168,135,118,90,1,161,229,168,135,118,94,1,161,229,168,135,118,82,1,161,229,168,135,118,99,1,161,229,168,135,118,84,1,161,229,168,135,118,97,1,161,229,168,135,118,95,1,161,229,168,135,118,96,1,161,229,168,135,118,98,1,161,229,168,135,118,101,1,161,229,168,135,118,104,1,161,229,168,135,118,106,1,161,229,168,135,118,105,1,161,229,168,135,118,103,1,161,229,168,135,118,107,1,161,229,168,135,118,111,1,161,229,168,135,118,109,1,161,229,168,135,118,110,1,161,229,168,135,118,108,1,161,229,168,135,118,112,1,161,229,168,135,118,100,1,161,229,168,135,118,117,1,161,229,168,135,118,102,1,161,229,168,135,118,113,1,161,229,168,135,118,115,1,161,229,168,135,118,116,1,161,229,168,135,118,114,1,161,229,168,135,118,119,1,161,229,168,135,118,122,1,161,229,168,135,118,123,1,161,229,168,135,118,121,1,161,229,168,135,118,124,1,161,229,168,135,118,125,1,161,229,168,135,118,128,1,1,161,229,168,135,118,126,1,161,229,168,135,118,129,1,1,161,229,168,135,118,127,1,161,229,168,135,118,130,1,1,161,229,168,135,118,118,1,161,229,168,135,118,135,1,1,161,229,168,135,118,120,1,161,229,168,135,118,131,1,1,161,229,168,135,118,133,1,1,161,229,168,135,118,132,1,1,161,229,168,135,118,134,1,1,161,229,168,135,118,137,1,1,161,229,168,135,118,140,1,1,161,229,168,135,118,139,1,1,161,229,168,135,118,142,1,1,161,229,168,135,118,141,1,1,161,229,168,135,118,143,1,1,161,229,168,135,118,145,1,1,161,229,168,135,118,146,1,1,161,229,168,135,118,147,1,1,161,229,168,135,118,144,1,1,161,229,168,135,118,148,1,1,161,229,168,135,118,136,1,1,161,229,168,135,118,153,1,1,161,229,168,135,118,138,1,1,161,229,168,135,118,149,1,1,161,229,168,135,118,151,1,1,161,229,168,135,118,150,1,1,161,229,168,135,118,152,1,1,161,229,168,135,118,155,1,1,161,229,168,135,118,160,1,1,161,229,168,135,118,159,1,1,161,229,168,135,118,158,1,1,161,229,168,135,118,157,1,1,161,229,168,135,118,161,1,1,161,229,168,135,118,165,1,1,161,229,168,135,118,162,1,1,161,229,168,135,118,164,1,1,161,229,168,135,118,163,1,1,161,229,168,135,118,166,1,1,161,229,168,135,118,154,1,1,161,229,168,135,118,171,1,1,161,229,168,135,118,156,1,1,161,229,168,135,118,170,1,1,161,229,168,135,118,168,1,1,161,229,168,135,118,167,1,1,161,229,168,135,118,169,1,1,161,229,168,135,118,173,1,1,161,229,168,135,118,175,1,1,161,229,168,135,118,176,1,1,161,229,168,135,118,177,1,1,161,229,168,135,118,178,1,1,161,229,168,135,118,179,1,1,161,229,168,135,118,182,1,1,161,229,168,135,118,180,1,1,161,229,168,135,118,183,1,1,161,229,168,135,118,181,1,1,161,229,168,135,118,184,1,1,161,229,168,135,118,172,1,1,161,229,168,135,118,189,1,1,161,229,168,135,118,174,1,1,161,229,168,135,118,185,1,1,161,229,168,135,118,186,1,1,161,229,168,135,118,188,1,1,161,229,168,135,118,187,1,1,161,229,168,135,118,191,1,1,161,229,168,135,118,194,1,1,161,229,168,135,118,195,1,1,161,229,168,135,118,196,1,1,161,229,168,135,118,193,1,1,161,229,168,135,118,197,1,1,161,229,168,135,118,198,1,1,161,229,168,135,118,200,1,1,161,229,168,135,118,201,1,1,161,229,168,135,118,199,1,1,161,229,168,135,118,202,1,1,161,229,168,135,118,190,1,1,161,229,168,135,118,207,1,1,161,229,168,135,118,192,1,1,161,229,168,135,118,204,1,1,161,229,168,135,118,203,1,1,161,229,168,135,118,205,1,1,161,229,168,135,118,206,1,1,161,229,168,135,118,209,1,1,161,229,168,135,118,212,1,1,161,229,168,135,118,213,1,1,161,229,168,135,118,214,1,1,161,229,168,135,118,211,1,1,161,229,168,135,118,215,1,1,161,229,168,135,118,217,1,1,161,229,168,135,118,218,1,1,161,229,168,135,118,219,1,1,161,229,168,135,118,216,1,1,161,229,168,135,118,220,1,1,161,229,168,135,118,208,1,1,161,229,168,135,118,225,1,1,161,229,168,135,118,210,1,1,161,229,168,135,118,223,1,1,161,229,168,135,118,222,1,1,161,229,168,135,118,221,1,1,161,229,168,135,118,224,1,1,161,229,168,135,118,227,1,1,161,229,168,135,118,229,1,1,161,229,168,135,118,231,1,1,161,229,168,135,118,230,1,1,161,229,168,135,118,232,1,1,161,229,168,135,118,233,1,1,161,229,168,135,118,234,1,1,161,229,168,135,118,237,1,1,161,229,168,135,118,235,1,1,161,229,168,135,118,236,1,1,161,229,168,135,118,238,1,1,161,229,168,135,118,226,1,1,161,229,168,135,118,243,1,1,161,229,168,135,118,228,1,1,161,229,168,135,118,242,1,1,161,229,168,135,118,241,1,1,161,229,168,135,118,239,1,1,161,229,168,135,118,240,1,1,161,229,168,135,118,245,1,1,161,229,168,135,118,249,1,1,161,229,168,135,118,247,1,1,161,229,168,135,118,248,1,1,161,229,168,135,118,250,1,1,161,229,168,135,118,251,1,1,161,229,168,135,118,252,1,1,161,229,168,135,118,253,1,1,161,229,168,135,118,254,1,1,161,229,168,135,118,255,1,1,161,229,168,135,118,128,2,1,161,229,168,135,118,244,1,1,161,229,168,135,118,133,2,1,161,229,168,135,118,246,1,1,161,229,168,135,118,129,2,1,161,229,168,135,118,130,2,1,161,229,168,135,118,132,2,1,161,229,168,135,118,131,2,1,161,229,168,135,118,135,2,1,161,229,168,135,118,138,2,1,161,229,168,135,118,137,2,1,161,229,168,135,118,140,2,1,161,229,168,135,118,139,2,1,161,229,168,135,118,141,2,1,161,229,168,135,118,142,2,1,161,229,168,135,118,143,2,1,161,229,168,135,118,145,2,1,161,229,168,135,118,144,2,1,161,229,168,135,118,146,2,1,161,229,168,135,118,134,2,1,161,229,168,135,118,151,2,1,161,229,168,135,118,136,2,1,161,229,168,135,118,150,2,1,161,229,168,135,118,147,2,1,161,229,168,135,118,148,2,1,161,229,168,135,118,149,2,1,161,229,168,135,118,153,2,1,161,229,168,135,118,157,2,1,161,229,168,135,118,156,2,1,161,229,168,135,118,158,2,1,161,229,168,135,118,155,2,1,161,229,168,135,118,159,2,1,161,229,168,135,118,161,2,1,161,229,168,135,118,163,2,1,161,229,168,135,118,160,2,1,161,229,168,135,118,162,2,1,161,229,168,135,118,164,2,1,161,229,168,135,118,152,2,1,161,229,168,135,118,169,2,1,161,229,168,135,118,154,2,1,161,229,168,135,118,166,2,1,161,229,168,135,118,165,2,1,161,229,168,135,118,168,2,1,161,229,168,135,118,167,2,1,161,229,168,135,118,171,2,1,161,229,168,135,118,173,2,1,161,229,168,135,118,174,2,1,161,229,168,135,118,176,2,1,161,229,168,135,118,175,2,1,161,229,168,135,118,177,2,1,161,229,168,135,118,179,2,1,161,229,168,135,118,178,2,1,161,229,168,135,118,181,2,1,161,229,168,135,118,180,2,1,161,229,168,135,118,182,2,1,161,229,168,135,118,170,2,1,161,229,168,135,118,187,2,1,161,229,168,135,118,172,2,1,161,229,168,135,118,186,2,1,161,229,168,135,118,185,2,1,161,229,168,135,118,184,2,1,161,229,168,135,118,183,2,1,161,229,168,135,118,189,2,1,161,229,168,135,118,194,2,1,161,229,168,135,118,193,2,1,161,229,168,135,118,192,2,1,161,229,168,135,118,191,2,1,161,229,168,135,118,195,2,1,161,229,168,135,118,197,2,1,161,229,168,135,118,196,2,1,161,229,168,135,118,198,2,1,161,229,168,135,118,199,2,1,161,229,168,135,118,200,2,1,161,229,168,135,118,188,2,1,161,229,168,135,118,205,2,1,161,229,168,135,118,190,2,1,161,229,168,135,118,203,2,1,161,229,168,135,118,204,2,1,161,229,168,135,118,202,2,1,161,229,168,135,118,201,2,1,161,229,168,135,118,207,2,1,161,229,168,135,118,210,2,1,161,229,168,135,118,209,2,1,161,229,168,135,118,211,2,1,161,229,168,135,118,212,2,1,161,229,168,135,118,213,2,1,161,229,168,135,118,216,2,1,161,229,168,135,118,217,2,1,161,229,168,135,118,215,2,1,161,229,168,135,118,214,2,1,161,229,168,135,118,218,2,1,161,229,168,135,118,206,2,1,161,229,168,135,118,223,2,1,161,229,168,135,118,208,2,1,161,229,168,135,118,219,2,1,161,229,168,135,118,221,2,1,161,229,168,135,118,220,2,1,161,229,168,135,118,222,2,1,161,229,168,135,118,225,2,1,161,229,168,135,118,229,2,1,161,229,168,135,118,230,2,1,161,229,168,135,118,227,2,1,161,229,168,135,118,228,2,1,161,229,168,135,118,231,2,1,161,229,168,135,118,232,2,1,161,229,168,135,118,235,2,1,161,229,168,135,118,233,2,1,161,229,168,135,118,234,2,1,161,229,168,135,118,236,2,1,161,229,168,135,118,224,2,1,161,229,168,135,118,241,2,1,161,229,168,135,118,226,2,1,161,229,168,135,118,239,2,1,161,229,168,135,118,240,2,1,161,229,168,135,118,238,2,1,161,229,168,135,118,237,2,1,161,229,168,135,118,243,2,1,161,229,168,135,118,246,2,1,161,229,168,135,118,247,2,1,161,229,168,135,118,245,2,1,161,229,168,135,118,248,2,1,161,229,168,135,118,249,2,1,161,229,168,135,118,251,2,1,161,229,168,135,118,252,2,1,161,229,168,135,118,253,2,1,161,229,168,135,118,250,2,1,161,229,168,135,118,254,2,1,161,229,168,135,118,242,2,1,161,229,168,135,118,131,3,1,161,229,168,135,118,244,2,1,161,229,168,135,118,255,2,1,161,229,168,135,118,129,3,1,161,229,168,135,118,130,3,1,161,229,168,135,118,128,3,1,161,229,168,135,118,133,3,1,161,229,168,135,118,135,3,1,161,229,168,135,118,138,3,1,161,229,168,135,118,137,3,1,161,229,168,135,118,136,3,1,161,229,168,135,118,139,3,1,161,229,168,135,118,143,3,1,161,229,168,135,118,140,3,1,161,229,168,135,118,141,3,1,161,229,168,135,118,142,3,1,161,229,168,135,118,144,3,1,161,229,168,135,118,132,3,1,161,229,168,135,118,149,3,1,161,229,168,135,118,134,3,1,161,229,168,135,118,147,3,1,161,229,168,135,118,145,3,1,161,229,168,135,118,148,3,1,161,229,168,135,118,146,3,1,161,229,168,135,118,151,3,1,161,229,168,135,118,155,3,1,161,229,168,135,118,153,3,1,161,229,168,135,118,154,3,1,161,229,168,135,118,156,3,1,161,229,168,135,118,157,3,1,161,229,168,135,118,159,3,1,161,229,168,135,118,160,3,1,161,229,168,135,118,158,3,1,161,229,168,135,118,161,3,1,161,229,168,135,118,162,3,1,161,229,168,135,118,150,3,1,161,229,168,135,118,167,3,1,161,229,168,135,118,152,3,1,161,229,168,135,118,163,3,1,161,229,168,135,118,164,3,1,161,229,168,135,118,166,3,1,161,229,168,135,118,165,3,1,161,229,168,135,118,169,3,1,161,229,168,135,118,173,3,1,161,229,168,135,118,171,3,1,161,229,168,135,118,174,3,1,161,229,168,135,118,172,3,1,161,229,168,135,118,175,3,1,161,229,168,135,118,179,3,1,161,229,168,135,118,177,3,1,161,229,168,135,118,176,3,1,161,229,168,135,118,178,3,1,161,229,168,135,118,180,3,1,161,229,168,135,118,168,3,1,161,229,168,135,118,185,3,1,161,229,168,135,118,170,3,1,161,229,168,135,118,181,3,1,161,229,168,135,118,183,3,1,161,229,168,135,118,184,3,1,161,229,168,135,118,182,3,1,161,229,168,135,118,187,3,1,161,229,168,135,118,191,3,1,161,229,168,135,118,189,3,1,161,229,168,135,118,190,3,1,161,229,168,135,118,192,3,1,161,229,168,135,118,193,3,1,161,229,168,135,118,194,3,1,161,229,168,135,118,197,3,1,161,229,168,135,118,196,3,1,161,229,168,135,118,195,3,1,161,229,168,135,118,198,3,1,161,229,168,135,118,186,3,1,161,229,168,135,118,203,3,1,161,229,168,135,118,188,3,1,161,229,168,135,118,202,3,1,161,229,168,135,118,199,3,1,161,229,168,135,118,200,3,1,161,229,168,135,118,201,3,1,161,229,168,135,118,205,3,1,161,229,168,135,118,208,3,1,161,229,168,135,118,209,3,1,161,229,168,135,118,210,3,1,161,229,168,135,118,207,3,1,161,229,168,135,118,211,3,1,161,229,168,135,118,212,3,1,161,229,168,135,118,215,3,1,161,229,168,135,118,213,3,1,161,229,168,135,118,214,3,1,161,229,168,135,118,216,3,1,161,229,168,135,118,204,3,1,161,229,168,135,118,221,3,1,161,229,168,135,118,206,3,1,161,229,168,135,118,218,3,1,161,229,168,135,118,219,3,1,161,229,168,135,118,217,3,1,161,229,168,135,118,220,3,1,161,229,168,135,118,223,3,1,161,229,168,135,118,225,3,1,161,229,168,135,118,227,3,1,161,229,168,135,118,228,3,1,161,229,168,135,118,226,3,1,161,229,168,135,118,229,3,1,161,229,168,135,118,230,3,1,161,229,168,135,118,233,3,1,161,229,168,135,118,231,3,1,161,229,168,135,118,232,3,1,161,229,168,135,118,234,3,1,161,229,168,135,118,222,3,1,161,229,168,135,118,239,3,1,161,229,168,135,118,224,3,1,161,229,168,135,118,238,3,1,161,229,168,135,118,236,3,1,161,229,168,135,118,235,3,1,161,229,168,135,118,237,3,1,161,229,168,135,118,241,3,1,161,229,168,135,118,244,3,1,161,229,168,135,118,243,3,1,161,229,168,135,118,245,3,1,161,229,168,135,118,246,3,1,161,229,168,135,118,247,3,1,161,229,168,135,118,250,3,1,161,229,168,135,118,249,3,1,161,229,168,135,118,248,3,1,161,229,168,135,118,251,3,1,161,229,168,135,118,252,3,1,161,229,168,135,118,240,3,1,161,229,168,135,118,129,4,1,161,229,168,135,118,242,3,1,161,229,168,135,118,254,3,1,161,229,168,135,118,255,3,1,161,229,168,135,118,128,4,1,161,229,168,135,118,253,3,1,161,229,168,135,118,131,4,1,161,229,168,135,118,134,4,1,161,229,168,135,118,136,4,1,161,229,168,135,118,135,4,1,161,229,168,135,118,133,4,1,161,229,168,135,118,137,4,1,161,229,168,135,118,139,4,1,161,229,168,135,118,140,4,1,161,229,168,135,118,141,4,1,161,229,168,135,118,138,4,1,161,229,168,135,118,142,4,1,161,229,168,135,118,130,4,1,161,229,168,135,118,147,4,1,161,229,168,135,118,132,4,1,161,229,168,135,118,143,4,1,161,229,168,135,118,145,4,1,161,229,168,135,118,146,4,1,161,229,168,135,118,144,4,1,161,229,168,135,118,149,4,1,161,229,168,135,118,151,4,1,161,229,168,135,118,152,4,1,161,229,168,135,118,153,4,1,161,229,168,135,118,154,4,1,161,229,168,135,118,155,4,1,161,229,168,135,118,158,4,1,161,229,168,135,118,157,4,1,161,229,168,135,118,156,4,1,161,229,168,135,118,159,4,1,161,229,168,135,118,160,4,1,161,229,168,135,118,148,4,1,161,229,168,135,118,165,4,1,161,229,168,135,118,150,4,1,161,229,168,135,118,162,4,1,161,229,168,135,118,164,4,1,161,229,168,135,118,163,4,1,161,229,168,135,118,161,4,1,161,229,168,135,118,167,4,1,161,229,168,135,118,169,4,1,161,229,168,135,118,171,4,1,161,229,168,135,118,170,4,1,161,229,168,135,118,172,4,1,161,229,168,135,118,173,4,1,161,229,168,135,118,176,4,1,161,229,168,135,118,177,4,1,161,229,168,135,118,174,4,1,161,229,168,135,118,175,4,1,161,229,168,135,118,178,4,1,161,229,168,135,118,166,4,1,161,229,168,135,118,183,4,1,161,229,168,135,118,168,4,1,161,229,168,135,118,181,4,1,161,229,168,135,118,180,4,1,161,229,168,135,118,182,4,1,161,229,168,135,118,179,4,1,161,229,168,135,118,185,4,1,161,229,168,135,118,188,4,1,161,229,168,135,118,189,4,1,161,229,168,135,118,187,4,1,161,229,168,135,118,190,4,1,161,229,168,135,118,191,4,1,161,229,168,135,118,194,4,1,161,229,168,135,118,192,4,1,161,229,168,135,118,193,4,1,161,229,168,135,118,195,4,1,161,229,168,135,118,196,4,1,161,229,168,135,118,184,4,1,161,229,168,135,118,201,4,1,161,229,168,135,118,186,4,1,161,229,168,135,118,199,4,1,161,229,168,135,118,200,4,1,161,229,168,135,118,198,4,1,161,229,168,135,118,197,4,1,161,229,168,135,118,203,4,1,161,229,168,135,118,207,4,1,161,229,168,135,118,205,4,1,161,229,168,135,118,208,4,1,161,229,168,135,118,206,4,1,161,229,168,135,118,209,4,1,161,229,168,135,118,213,4,1,161,229,168,135,118,212,4,1,161,229,168,135,118,210,4,1,161,229,168,135,118,211,4,1,161,229,168,135,118,51,1,136,229,168,135,118,52,1,118,2,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,6,104,101,105,103,104,116,125,60,161,229,168,135,118,53,1,136,229,168,135,118,54,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,161,229,168,135,118,55,1,136,229,168,135,118,56,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,161,229,168,135,118,57,1,136,229,168,135,118,58,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,161,229,168,135,118,59,1,136,229,168,135,118,60,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,161,229,168,135,118,61,1,136,229,168,135,118,62,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,161,229,168,135,118,219,4,1,136,229,168,135,118,220,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,161,229,168,135,118,221,4,1,136,229,168,135,118,222,4,1,118,2,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,6,104,101,105,103,104,116,125,60,161,229,168,135,118,223,4,1,136,229,168,135,118,224,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,161,229,168,135,118,225,4,1,136,229,168,135,118,226,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,161,229,168,135,118,227,4,1,136,229,168,135,118,228,4,1,118,2,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,6,104,101,105,103,104,116,125,60,161,229,168,135,118,229,4,1,136,229,168,135,118,230,4,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,2,149,154,146,112,0,161,186,204,138,236,4,111,1,161,186,204,138,236,4,117,19,1,211,189,178,91,0,161,252,240,184,224,14,23,80,1,253,149,229,85,0,161,142,215,187,158,14,5,14,1,211,235,145,81,0,161,128,137,148,150,4,38,16,65,128,137,148,150,4,1,0,39,132,238,182,192,14,1,0,5,134,200,133,143,5,1,0,2,135,173,169,205,15,1,0,4,137,227,133,241,2,19,18,1,20,1,22,1,25,1,40,1,53,3,67,1,70,1,86,1,105,1,107,12,124,1,146,1,1,150,1,1,154,1,1,160,1,1,162,1,1,164,1,1,167,1,3,140,242,215,248,4,1,0,35,141,132,223,206,14,1,0,5,142,215,187,158,14,1,0,10,146,198,138,224,6,4,0,5,8,1,11,2,14,2,149,154,146,112,1,0,20,150,194,135,131,8,1,0,12,154,253,168,186,13,1,0,6,157,197,217,249,6,1,0,3,158,173,179,170,6,1,0,81,160,159,229,236,10,1,0,34,162,129,240,225,15,1,0,19,165,237,195,173,1,1,0,8,168,211,203,155,8,17,0,3,15,1,32,1,36,1,40,1,44,1,48,1,54,1,56,1,58,1,62,1,66,1,70,1,74,1,80,1,82,1,84,1,171,216,132,162,10,14,5,1,37,1,39,6,54,1,56,1,58,1,60,1,62,1,64,1,66,1,68,5,79,1,87,1,92,1,174,158,229,225,9,1,0,2,175,150,167,163,14,1,0,4,174,182,200,164,11,1,0,2,177,178,255,174,1,5,0,9,11,5,17,10,28,5,34,4,178,161,242,226,13,1,0,153,1,174,250,146,158,5,1,0,1,180,149,168,150,13,1,0,10,180,132,165,192,8,1,0,1,182,201,218,189,1,6,0,1,2,1,4,1,6,1,8,1,10,1,182,139,168,140,5,1,0,36,183,238,200,180,5,1,0,6,185,145,225,175,8,12,0,182,2,185,2,1,189,2,1,193,2,1,197,2,1,201,2,1,207,2,1,209,2,1,211,2,1,215,2,7,223,2,1,233,2,52,186,204,138,236,4,1,0,118,187,163,190,240,15,9,5,1,24,1,26,12,43,1,65,1,69,1,73,1,81,1,83,1,187,159,219,213,8,1,0,2,188,252,160,180,14,1,0,1,191,215,204,166,13,1,0,12,192,183,207,147,14,1,0,43,193,174,143,180,7,1,0,18,193,140,213,146,2,3,41,1,75,1,77,5,200,168,240,223,7,1,0,2,201,191,253,157,12,1,0,2,200,156,140,203,9,1,0,2,202,170,215,178,7,1,0,24,203,248,208,163,4,1,0,4,206,242,242,141,13,1,0,95,209,142,245,200,15,45,0,55,56,1,58,9,72,55,136,1,28,165,1,1,167,1,3,172,1,4,177,1,2,180,1,232,1,157,3,4,162,3,18,182,3,1,186,3,1,190,3,1,194,3,1,198,3,1,202,3,1,208,3,1,210,3,1,212,3,1,215,3,5,223,3,1,227,3,1,231,3,1,235,3,1,239,3,1,128,4,55,184,4,1,186,4,9,200,4,1,204,4,1,208,4,1,212,4,1,216,4,1,220,4,1,233,4,44,150,5,1,152,5,1,154,5,1,156,5,1,158,5,1,160,5,11,175,5,1,180,5,3,210,221,238,195,8,1,0,20,211,189,178,91,1,0,80,211,235,145,81,1,0,16,216,247,253,206,7,1,0,2,219,179,165,244,8,1,0,4,224,218,133,236,10,1,0,13,227,170,238,211,14,1,0,16,227,250,198,245,13,1,0,5,229,168,135,118,22,0,1,2,1,10,6,17,5,26,26,53,1,55,1,57,1,59,1,61,1,63,157,4,221,4,1,223,4,1,225,4,1,227,4,1,229,4,1,231,4,1,233,4,1,235,4,1,237,4,1,239,4,1,241,4,1,234,232,155,212,3,1,0,1,246,154,200,238,10,1,0,11,247,149,251,192,4,1,0,4,248,220,249,231,6,1,0,29,247,187,192,242,6,1,0,6,250,147,239,143,1,1,0,2,252,220,241,227,14,1,0,60,253,149,229,85,1,0,14,253,223,254,206,11,1,0,2,252,240,184,224,14,1,0,24],"version":0,"object_id":"4c658817-20db-4f56-b7f9-0637a22dfeb6"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/87bc006e-c1eb-47fd-9ac6-e39b17956369.json b/frontend/appflowy_web_app/cypress/fixtures/database/87bc006e-c1eb-47fd-9ac6-e39b17956369.json new file mode 100644 index 0000000000..474a10765c --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/87bc006e-c1eb-47fd-9ac6-e39b17956369.json @@ -0,0 +1 @@ +{"data":{"state_vector":[2,144,224,143,199,14,16,201,175,140,129,8,161,6],"doc_state":[2,2,144,224,143,199,14,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,15,168,144,224,143,199,14,14,1,122,0,0,0,0,102,97,139,106,230,3,201,175,140,129,8,0,39,1,4,100,97,116,97,8,100,97,116,97,98,97,115,101,1,40,0,201,175,140,129,8,0,2,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,39,0,201,175,140,129,8,0,6,102,105,101,108,100,115,1,39,0,201,175,140,129,8,0,5,118,105,101,119,115,1,39,0,201,175,140,129,8,0,5,109,101,116,97,115,1,40,0,201,175,140,129,8,4,3,105,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,39,0,201,175,140,129,8,2,6,77,67,57,90,97,69,1,40,0,201,175,140,129,8,6,2,105,100,1,119,6,77,67,57,90,97,69,40,0,201,175,140,129,8,6,4,110,97,109,101,1,119,4,78,97,109,101,40,0,201,175,140,129,8,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,40,0,201,175,140,129,8,6,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,115,63,40,0,201,175,140,129,8,6,10,105,115,95,112,114,105,109,97,114,121,1,120,40,0,201,175,140,129,8,6,2,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,6,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,13,1,48,1,40,0,201,175,140,129,8,14,4,100,97,116,97,1,119,0,39,0,201,175,140,129,8,2,6,53,69,90,81,65,87,1,40,0,201,175,140,129,8,16,2,105,100,1,119,6,53,69,90,81,65,87,40,0,201,175,140,129,8,16,4,110,97,109,101,1,119,4,84,121,112,101,40,0,201,175,140,129,8,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,33,0,201,175,140,129,8,16,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,16,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,201,175,140,129,8,16,2,116,121,1,122,0,0,0,0,0,0,0,3,39,0,201,175,140,129,8,16,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,23,1,51,1,33,0,201,175,140,129,8,24,7,99,111,110,116,101,110,116,1,39,0,201,175,140,129,8,2,6,108,73,72,113,101,57,1,40,0,201,175,140,129,8,26,2,105,100,1,119,6,108,73,72,113,101,57,40,0,201,175,140,129,8,26,4,110,97,109,101,1,119,4,68,111,110,101,40,0,201,175,140,129,8,26,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,40,0,201,175,140,129,8,26,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,115,63,40,0,201,175,140,129,8,26,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,201,175,140,129,8,26,2,116,121,1,122,0,0,0,0,0,0,0,5,39,0,201,175,140,129,8,26,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,33,1,53,1,39,0,201,175,140,129,8,3,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,1,40,0,201,175,140,129,8,35,2,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,40,0,201,175,140,129,8,35,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,201,175,140,129,8,35,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,201,175,140,129,8,35,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,33,0,201,175,140,129,8,35,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,201,175,140,129,8,35,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,201,175,140,129,8,35,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,35,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,201,175,140,129,8,43,6,108,73,72,113,101,57,1,40,0,201,175,140,129,8,44,4,119,114,97,112,1,120,40,0,201,175,140,129,8,44,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,44,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,39,0,201,175,140,129,8,43,6,77,67,57,90,97,69,1,40,0,201,175,140,129,8,48,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,201,175,140,129,8,48,4,119,114,97,112,1,120,40,0,201,175,140,129,8,48,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,43,6,53,69,90,81,65,87,1,40,0,201,175,140,129,8,52,4,119,114,97,112,1,120,40,0,201,175,140,129,8,52,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,201,175,140,129,8,52,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,35,7,102,105,108,116,101,114,115,0,39,0,201,175,140,129,8,35,6,103,114,111,117,112,115,0,39,0,201,175,140,129,8,35,5,115,111,114,116,115,0,39,0,201,175,140,129,8,35,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,201,175,140,129,8,59,3,118,1,2,105,100,119,6,77,67,57,90,97,69,118,1,2,105,100,119,6,53,69,90,81,65,87,118,1,2,105,100,119,6,108,73,72,113,101,57,39,0,201,175,140,129,8,35,10,114,111,119,95,111,114,100,101,114,115,0,8,0,201,175,140,129,8,63,3,118,2,2,105,100,119,36,49,49,49,49,98,49,52,54,45,52,99,54,99,45,52,102,99,54,45,57,53,101,49,45,55,48,99,50,52,54,49,52,55,102,56,102,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,51,101,99,55,98,55,54,99,45,54,56,99,57,45,52,50,55,57,45,57,98,51,51,45,50,51,54,53,51,50,49,101,97,102,52,49,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,57,99,100,101,55,99,49,53,45,51,52,55,99,45,52,52,55,97,45,57,101,97,49,45,55,54,98,99,51,97,56,100,52,101,57,54,161,201,175,140,129,8,40,1,136,201,175,140,129,8,62,1,118,1,2,105,100,119,6,111,121,80,121,97,117,39,0,201,175,140,129,8,43,6,111,121,80,121,97,117,1,40,0,201,175,140,129,8,69,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,2,6,111,121,80,121,97,117,1,40,0,201,175,140,129,8,71,2,105,100,1,119,6,111,121,80,121,97,117,33,0,201,175,140,129,8,71,4,110,97,109,101,1,40,0,201,175,140,129,8,71,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,77,33,0,201,175,140,129,8,71,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,71,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,201,175,140,129,8,71,2,116,121,1,39,0,201,175,140,129,8,71,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,78,1,48,1,40,0,201,175,140,129,8,79,4,100,97,116,97,1,119,0,161,201,175,140,129,8,75,1,168,201,175,140,129,8,77,1,122,0,0,0,0,0,0,0,1,39,0,201,175,140,129,8,78,1,49,1,40,0,201,175,140,129,8,83,6,115,121,109,98,111,108,1,119,3,82,85,66,40,0,201,175,140,129,8,83,6,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,83,5,115,99,97,108,101,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,83,4,110,97,109,101,1,119,6,78,117,109,98,101,114,161,201,175,140,129,8,81,1,40,0,201,175,140,129,8,79,6,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,79,5,115,99,97,108,101,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,79,6,115,121,109,98,111,108,1,119,3,82,85,66,40,0,201,175,140,129,8,79,4,110,97,109,101,1,119,6,78,117,109,98,101,114,161,201,175,140,129,8,67,2,136,201,175,140,129,8,68,1,118,1,2,105,100,119,6,102,116,73,53,52,121,39,0,201,175,140,129,8,43,6,102,116,73,53,52,121,1,40,0,201,175,140,129,8,96,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,2,6,102,116,73,53,52,121,1,40,0,201,175,140,129,8,98,2,105,100,1,119,6,102,116,73,53,52,121,33,0,201,175,140,129,8,98,4,110,97,109,101,1,40,0,201,175,140,129,8,98,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,82,33,0,201,175,140,129,8,98,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,98,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,201,175,140,129,8,98,2,116,121,1,39,0,201,175,140,129,8,98,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,105,1,48,1,40,0,201,175,140,129,8,106,4,100,97,116,97,1,119,0,161,201,175,140,129,8,102,1,168,201,175,140,129,8,104,1,122,0,0,0,0,0,0,0,4,39,0,201,175,140,129,8,105,1,52,1,33,0,201,175,140,129,8,110,7,99,111,110,116,101,110,116,1,161,201,175,140,129,8,108,1,40,0,201,175,140,129,8,106,7,99,111,110,116,101,110,116,1,119,36,123,34,111,112,116,105,111,110,115,34,58,91,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,201,175,140,129,8,94,1,161,201,175,140,129,8,112,1,161,201,175,140,129,8,111,1,161,201,175,140,129,8,115,1,168,201,175,140,129,8,116,1,119,131,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,104,57,106,100,34,44,34,110,97,109,101,34,58,34,111,112,116,105,111,110,45,50,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,111,95,66,104,34,44,34,110,97,109,101,34,58,34,111,112,116,105,111,110,45,49,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,201,175,140,129,8,20,1,161,201,175,140,129,8,25,1,168,201,175,140,129,8,119,1,122,0,0,0,0,102,97,115,111,168,201,175,140,129,8,120,1,119,145,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,71,102,87,50,34,44,34,110,97,109,101,34,58,34,115,105,110,103,108,101,45,111,112,116,105,111,110,45,50,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,111,102,70,102,34,44,34,110,97,109,101,34,58,34,115,105,110,103,108,101,45,111,112,116,105,111,110,45,49,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,201,175,140,129,8,88,1,161,201,175,140,129,8,73,1,161,201,175,140,129,8,123,1,161,201,175,140,129,8,124,1,161,201,175,140,129,8,125,1,161,201,175,140,129,8,126,1,161,201,175,140,129,8,127,1,161,201,175,140,129,8,128,1,1,161,201,175,140,129,8,129,1,1,161,201,175,140,129,8,130,1,1,168,201,175,140,129,8,131,1,1,122,0,0,0,0,102,97,115,117,168,201,175,140,129,8,132,1,1,119,6,110,117,109,98,101,114,161,201,175,140,129,8,117,1,161,201,175,140,129,8,100,1,161,201,175,140,129,8,135,1,1,161,201,175,140,129,8,136,1,1,161,201,175,140,129,8,137,1,1,161,201,175,140,129,8,138,1,1,161,201,175,140,129,8,139,1,1,161,201,175,140,129,8,140,1,1,161,201,175,140,129,8,141,1,1,161,201,175,140,129,8,142,1,1,161,201,175,140,129,8,143,1,1,161,201,175,140,129,8,144,1,1,161,201,175,140,129,8,145,1,1,161,201,175,140,129,8,146,1,1,161,201,175,140,129,8,147,1,1,161,201,175,140,129,8,148,1,1,161,201,175,140,129,8,149,1,1,161,201,175,140,129,8,150,1,1,168,201,175,140,129,8,151,1,1,122,0,0,0,0,102,97,115,124,168,201,175,140,129,8,152,1,1,119,10,109,117,108,116,105,32,116,121,112,101,161,201,175,140,129,8,114,1,136,201,175,140,129,8,95,1,118,1,2,105,100,119,6,87,120,110,102,109,110,39,0,201,175,140,129,8,43,6,87,120,110,102,109,110,1,40,0,201,175,140,129,8,157,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,2,6,87,120,110,102,109,110,1,40,0,201,175,140,129,8,159,1,2,105,100,1,119,6,87,120,110,102,109,110,33,0,201,175,140,129,8,159,1,4,110,97,109,101,1,40,0,201,175,140,129,8,159,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,126,33,0,201,175,140,129,8,159,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,159,1,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,201,175,140,129,8,159,1,2,116,121,1,39,0,201,175,140,129,8,159,1,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,166,1,1,48,1,40,0,201,175,140,129,8,167,1,4,100,97,116,97,1,119,0,161,201,175,140,129,8,163,1,1,168,201,175,140,129,8,165,1,1,122,0,0,0,0,0,0,0,2,39,0,201,175,140,129,8,166,1,1,50,1,40,0,201,175,140,129,8,171,1,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,40,0,201,175,140,129,8,171,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,201,175,140,129,8,171,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,161,201,175,140,129,8,169,1,1,40,0,201,175,140,129,8,167,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,201,175,140,129,8,167,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,201,175,140,129,8,167,1,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,161,201,175,140,129,8,155,1,1,161,201,175,140,129,8,175,1,1,161,201,175,140,129,8,161,1,1,161,201,175,140,129,8,180,1,1,161,201,175,140,129,8,181,1,1,161,201,175,140,129,8,182,1,1,161,201,175,140,129,8,183,1,1,161,201,175,140,129,8,184,1,1,161,201,175,140,129,8,185,1,1,161,201,175,140,129,8,186,1,1,161,201,175,140,129,8,187,1,1,161,201,175,140,129,8,188,1,1,161,201,175,140,129,8,189,1,1,161,201,175,140,129,8,190,1,1,161,201,175,140,129,8,191,1,1,168,201,175,140,129,8,192,1,1,122,0,0,0,0,102,97,115,132,168,201,175,140,129,8,193,1,1,119,4,68,97,116,101,161,201,175,140,129,8,179,1,1,129,201,175,140,129,8,156,1,1,33,0,201,175,140,129,8,43,6,67,108,105,104,117,89,1,0,1,33,0,201,175,140,129,8,2,6,67,108,105,104,117,89,1,0,36,161,201,175,140,129,8,196,1,1,136,201,175,140,129,8,197,1,1,118,1,2,105,100,119,6,84,79,87,83,70,104,39,0,201,175,140,129,8,43,6,84,79,87,83,70,104,1,40,0,201,175,140,129,8,239,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,2,6,84,79,87,83,70,104,1,40,0,201,175,140,129,8,241,1,2,105,100,1,119,6,84,79,87,83,70,104,33,0,201,175,140,129,8,241,1,4,110,97,109,101,1,40,0,201,175,140,129,8,241,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,147,33,0,201,175,140,129,8,241,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,241,1,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,201,175,140,129,8,241,1,2,116,121,1,39,0,201,175,140,129,8,241,1,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,248,1,1,48,1,40,0,201,175,140,129,8,249,1,4,100,97,116,97,1,119,0,161,201,175,140,129,8,245,1,1,168,201,175,140,129,8,247,1,1,122,0,0,0,0,0,0,0,7,39,0,201,175,140,129,8,248,1,1,55,1,161,201,175,140,129,8,237,1,1,136,201,175,140,129,8,238,1,1,118,1,2,105,100,119,6,45,81,77,51,70,50,39,0,201,175,140,129,8,43,6,45,81,77,51,70,50,1,40,0,201,175,140,129,8,128,2,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,2,6,45,81,77,51,70,50,1,40,0,201,175,140,129,8,130,2,2,105,100,1,119,6,45,81,77,51,70,50,33,0,201,175,140,129,8,130,2,4,110,97,109,101,1,40,0,201,175,140,129,8,130,2,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,154,33,0,201,175,140,129,8,130,2,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,201,175,140,129,8,130,2,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,201,175,140,129,8,130,2,2,116,121,1,39,0,201,175,140,129,8,130,2,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,201,175,140,129,8,137,2,1,48,1,40,0,201,175,140,129,8,138,2,4,100,97,116,97,1,119,0,161,201,175,140,129,8,134,2,1,168,201,175,140,129,8,136,2,1,122,0,0,0,0,0,0,0,6,39,0,201,175,140,129,8,137,2,1,54,1,40,0,201,175,140,129,8,142,2,7,99,111,110,116,101,110,116,1,119,0,40,0,201,175,140,129,8,142,2,3,117,114,108,1,119,0,161,201,175,140,129,8,140,2,1,40,0,201,175,140,129,8,138,2,3,117,114,108,1,119,0,40,0,201,175,140,129,8,138,2,7,99,111,110,116,101,110,116,1,119,0,161,201,175,140,129,8,254,1,1,161,201,175,140,129,8,145,2,1,161,201,175,140,129,8,132,2,1,161,201,175,140,129,8,149,2,1,161,201,175,140,129,8,150,2,1,161,201,175,140,129,8,151,2,1,161,201,175,140,129,8,152,2,1,161,201,175,140,129,8,153,2,1,161,201,175,140,129,8,154,2,1,161,201,175,140,129,8,155,2,1,161,201,175,140,129,8,156,2,1,161,201,175,140,129,8,157,2,1,161,201,175,140,129,8,158,2,1,168,201,175,140,129,8,159,2,1,122,0,0,0,0,102,97,115,160,168,201,175,140,129,8,160,2,1,119,3,117,114,108,161,201,175,140,129,8,251,1,1,161,201,175,140,129,8,243,1,1,161,201,175,140,129,8,163,2,1,161,201,175,140,129,8,164,2,1,161,201,175,140,129,8,165,2,1,161,201,175,140,129,8,166,2,1,161,201,175,140,129,8,167,2,1,161,201,175,140,129,8,168,2,1,161,201,175,140,129,8,169,2,1,161,201,175,140,129,8,170,2,1,161,201,175,140,129,8,171,2,1,161,201,175,140,129,8,172,2,1,161,201,175,140,129,8,173,2,1,161,201,175,140,129,8,174,2,1,161,201,175,140,129,8,175,2,1,161,201,175,140,129,8,176,2,1,168,201,175,140,129,8,177,2,1,122,0,0,0,0,102,97,115,164,168,201,175,140,129,8,178,2,1,119,9,67,104,101,99,107,108,105,115,116,161,201,175,140,129,8,148,2,1,129,201,175,140,129,8,255,1,1,33,0,201,175,140,129,8,43,6,121,53,95,75,84,100,1,0,1,33,0,201,175,140,129,8,2,6,121,53,95,75,84,100,1,0,9,161,201,175,140,129,8,181,2,3,1,0,201,175,140,129,8,56,1,0,6,161,201,175,140,129,8,197,2,1,129,201,175,140,129,8,198,2,1,0,6,161,201,175,140,129,8,205,2,1,129,201,175,140,129,8,206,2,1,0,6,161,201,175,140,129,8,213,2,1,129,201,175,140,129,8,214,2,1,0,6,129,201,175,140,129,8,222,2,1,0,6,161,201,175,140,129,8,221,2,1,129,201,175,140,129,8,229,2,1,0,6,129,201,175,140,129,8,237,2,1,0,6,161,201,175,140,129,8,236,2,1,129,201,175,140,129,8,244,2,1,0,6,129,201,175,140,129,8,252,2,1,0,6,129,201,175,140,129,8,131,3,1,0,6,161,201,175,140,129,8,251,2,2,129,201,175,140,129,8,138,3,1,0,6,129,201,175,140,129,8,147,3,1,0,6,129,201,175,140,129,8,154,3,1,0,6,161,201,175,140,129,8,146,3,1,129,201,175,140,129,8,161,3,1,0,6,129,201,175,140,129,8,169,3,1,0,6,129,201,175,140,129,8,176,3,1,0,6,129,201,175,140,129,8,183,3,1,0,6,161,201,175,140,129,8,168,3,1,129,201,175,140,129,8,190,3,1,0,6,129,201,175,140,129,8,198,3,1,0,6,129,201,175,140,129,8,205,3,1,0,6,129,201,175,140,129,8,212,3,1,0,6,161,201,175,140,129,8,197,3,1,129,201,175,140,129,8,219,3,1,0,6,129,201,175,140,129,8,227,3,1,0,6,129,201,175,140,129,8,234,3,1,0,6,129,201,175,140,129,8,241,3,1,0,6,161,201,175,140,129,8,226,3,1,129,201,175,140,129,8,248,3,1,0,6,129,201,175,140,129,8,128,4,1,0,6,129,201,175,140,129,8,135,4,1,0,6,129,201,175,140,129,8,142,4,1,0,6,161,201,175,140,129,8,255,3,1,129,201,175,140,129,8,149,4,1,0,6,129,201,175,140,129,8,157,4,1,0,6,129,201,175,140,129,8,164,4,1,0,6,129,201,175,140,129,8,171,4,1,0,6,129,201,175,140,129,8,178,4,1,0,6,161,201,175,140,129,8,156,4,1,129,201,175,140,129,8,185,4,1,0,6,129,201,175,140,129,8,193,4,1,0,6,129,201,175,140,129,8,200,4,1,0,6,129,201,175,140,129,8,207,4,1,0,6,129,201,175,140,129,8,214,4,1,0,6,161,201,175,140,129,8,192,4,1,129,201,175,140,129,8,221,4,1,0,6,129,201,175,140,129,8,229,4,1,0,6,129,201,175,140,129,8,236,4,1,0,6,129,201,175,140,129,8,243,4,1,0,6,129,201,175,140,129,8,250,4,1,0,6,161,201,175,140,129,8,228,4,1,129,201,175,140,129,8,129,5,1,0,6,129,201,175,140,129,8,137,5,1,0,6,129,201,175,140,129,8,144,5,1,0,6,129,201,175,140,129,8,151,5,1,0,6,129,201,175,140,129,8,158,5,1,0,6,129,201,175,140,129,8,165,5,1,0,6,161,201,175,140,129,8,136,5,1,135,201,175,140,129,8,172,5,1,40,0,201,175,140,129,8,180,5,2,105,100,1,119,6,112,85,95,77,67,70,40,0,201,175,140,129,8,180,5,7,99,111,110,116,101,110,116,1,119,3,49,50,51,40,0,201,175,140,129,8,180,5,8,102,105,101,108,100,95,105,100,1,119,6,77,67,57,90,97,69,40,0,201,175,140,129,8,180,5,2,116,121,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,180,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,180,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,2,135,201,175,140,129,8,180,5,1,40,0,201,175,140,129,8,187,5,2,105,100,1,119,6,115,120,80,56,104,79,40,0,201,175,140,129,8,187,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,187,5,2,116,121,1,122,0,0,0,0,0,0,0,3,40,0,201,175,140,129,8,187,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,5,40,0,201,175,140,129,8,187,5,8,102,105,101,108,100,95,105,100,1,119,6,53,69,90,81,65,87,40,0,201,175,140,129,8,187,5,7,99,111,110,116,101,110,116,1,119,0,135,201,175,140,129,8,187,5,1,40,0,201,175,140,129,8,194,5,2,105,100,1,119,6,90,76,109,68,81,87,40,0,201,175,140,129,8,194,5,8,102,105,101,108,100,95,105,100,1,119,6,108,73,72,113,101,57,40,0,201,175,140,129,8,194,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,194,5,2,116,121,1,122,0,0,0,0,0,0,0,5,40,0,201,175,140,129,8,194,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,194,5,7,99,111,110,116,101,110,116,1,119,0,135,201,175,140,129,8,194,5,1,40,0,201,175,140,129,8,201,5,7,99,111,110,116,101,110,116,1,119,3,54,48,48,40,0,201,175,140,129,8,201,5,2,105,100,1,119,6,108,52,83,54,119,71,40,0,201,175,140,129,8,201,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,201,5,8,102,105,101,108,100,95,105,100,1,119,6,111,121,80,121,97,117,40,0,201,175,140,129,8,201,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,201,5,2,116,121,1,122,0,0,0,0,0,0,0,1,135,201,175,140,129,8,201,5,1,40,0,201,175,140,129,8,208,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,208,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,3,40,0,201,175,140,129,8,208,5,2,116,121,1,122,0,0,0,0,0,0,0,4,40,0,201,175,140,129,8,208,5,7,99,111,110,116,101,110,116,1,119,4,111,95,66,104,40,0,201,175,140,129,8,208,5,2,105,100,1,119,6,103,108,89,79,49,55,40,0,201,175,140,129,8,208,5,8,102,105,101,108,100,95,105,100,1,119,6,102,116,73,53,52,121,135,201,175,140,129,8,208,5,1,40,0,201,175,140,129,8,215,5,11,102,105,108,116,101,114,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,201,175,140,129,8,215,5,8,102,105,101,108,100,95,105,100,1,119,6,84,79,87,83,70,104,40,0,201,175,140,129,8,215,5,2,116,121,1,122,0,0,0,0,0,0,0,7,40,0,201,175,140,129,8,215,5,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,215,5,7,99,111,110,116,101,110,116,1,119,0,40,0,201,175,140,129,8,215,5,2,105,100,1,119,6,122,109,79,103,122,80,161,201,175,140,129,8,179,5,1,136,201,175,140,129,8,66,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,54,100,97,48,102,54,56,45,102,52,49,52,45,52,99,53,57,45,57,53,101,98,45,51,98,52,53,98,52,98,54,49,100,99,51,161,201,175,140,129,8,222,5,1,136,201,175,140,129,8,223,5,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,98,53,101,102,56,50,52,45,52,55,53,99,45,52,56,52,56,45,97,99,102,102,45,52,49,56,101,50,53,57,97,51,100,53,51,161,201,175,140,129,8,224,5,1,136,201,175,140,129,8,225,5,1,118,2,2,105,100,119,36,57,101,53,101,102,101,100,48,45,54,50,50,48,45,52,56,98,101,45,56,55,48,52,45,100,56,101,99,48,49,54,54,55,57,54,99,6,104,101,105,103,104,116,125,60,161,201,175,140,129,8,226,5,1,129,201,175,140,129,8,227,5,1,161,201,175,140,129,8,228,5,1,129,201,175,140,129,8,229,5,1,161,201,175,140,129,8,230,5,1,129,201,175,140,129,8,231,5,1,39,0,201,175,140,129,8,3,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,1,40,0,201,175,140,129,8,234,5,2,105,100,1,119,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,40,0,201,175,140,129,8,234,5,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,201,175,140,129,8,234,5,4,110,97,109,101,1,119,4,71,114,105,100,40,0,201,175,140,129,8,234,5,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,201,175,140,129,8,234,5,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,201,175,140,129,8,234,5,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,201,175,140,129,8,234,5,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,201,175,140,129,8,234,5,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,201,175,140,129,8,234,5,7,102,105,108,116,101,114,115,0,39,0,201,175,140,129,8,234,5,6,103,114,111,117,112,115,0,39,0,201,175,140,129,8,234,5,5,115,111,114,116,115,0,39,0,201,175,140,129,8,234,5,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,201,175,140,129,8,246,5,8,118,1,2,105,100,119,6,77,67,57,90,97,69,118,1,2,105,100,119,6,53,69,90,81,65,87,118,1,2,105,100,119,6,108,73,72,113,101,57,118,1,2,105,100,119,6,111,121,80,121,97,117,118,1,2,105,100,119,6,102,116,73,53,52,121,118,1,2,105,100,119,6,87,120,110,102,109,110,118,1,2,105,100,119,6,84,79,87,83,70,104,118,1,2,105,100,119,6,45,81,77,51,70,50,39,0,201,175,140,129,8,234,5,10,114,111,119,95,111,114,100,101,114,115,0,8,0,201,175,140,129,8,255,5,6,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,49,49,49,49,98,49,52,54,45,52,99,54,99,45,52,102,99,54,45,57,53,101,49,45,55,48,99,50,52,54,49,52,55,102,56,102,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,51,101,99,55,98,55,54,99,45,54,56,99,57,45,52,50,55,57,45,57,98,51,51,45,50,51,54,53,51,50,49,101,97,102,52,49,118,2,2,105,100,119,36,57,99,100,101,55,99,49,53,45,51,52,55,99,45,52,52,55,97,45,57,101,97,49,45,55,54,98,99,51,97,56,100,52,101,57,54,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,49,54,100,97,48,102,54,56,45,102,52,49,52,45,52,99,53,57,45,57,53,101,98,45,51,98,52,53,98,52,98,54,49,100,99,51,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,51,98,53,101,102,56,50,52,45,52,55,53,99,45,52,56,52,56,45,97,99,102,102,45,52,49,56,101,50,53,57,97,51,100,53,51,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,57,101,53,101,102,101,100,48,45,54,50,50,48,45,52,56,98,101,45,56,55,48,52,45,100,56,101,99,48,49,54,54,55,57,54,99,6,104,101,105,103,104,116,125,60,129,201,175,140,129,8,133,6,3,161,201,175,140,129,8,232,5,1,161,201,175,140,129,8,239,5,1,161,201,175,140,129,8,137,6,1,161,201,175,140,129,8,138,6,1,161,201,175,140,129,8,139,6,1,161,201,175,140,129,8,140,6,1,161,201,175,140,129,8,141,6,1,7,0,201,175,140,129,8,58,1,40,0,201,175,140,129,8,144,6,2,105,100,1,119,8,115,58,57,78,84,103,95,117,40,0,201,175,140,129,8,144,6,9,99,111,110,100,105,116,105,111,110,1,122,0,0,0,0,0,0,0,0,40,0,201,175,140,129,8,144,6,8,102,105,101,108,100,95,105,100,1,119,6,111,121,80,121,97,117,161,201,175,140,129,8,143,6,1,136,201,175,140,129,8,233,5,1,118,2,2,105,100,119,36,50,52,50,52,57,54,56,57,45,99,97,100,52,45,52,101,53,51,45,56,99,53,101,45,102,57,101,97,101,99,57,98,102,53,53,56,6,104,101,105,103,104,116,125,60,168,201,175,140,129,8,142,6,1,122,0,0,0,0,102,97,116,208,136,201,175,140,129,8,136,6,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,50,52,50,52,57,54,56,57,45,99,97,100,52,45,52,101,53,51,45,56,99,53,101,45,102,57,101,97,101,99,57,98,102,53,53,56,161,201,175,140,129,8,148,6,1,135,201,175,140,129,8,144,6,1,33,0,201,175,140,129,8,153,6,2,105,100,1,33,0,201,175,140,129,8,153,6,8,102,105,101,108,100,95,105,100,1,33,0,201,175,140,129,8,153,6,9,99,111,110,100,105,116,105,111,110,1,168,201,175,140,129,8,152,6,1,122,0,0,0,0,102,97,139,96,168,201,175,140,129,8,154,6,1,119,8,115,58,105,108,55,118,85,50,168,201,175,140,129,8,155,6,1,119,6,77,67,57,90,97,69,168,201,175,140,129,8,156,6,1,122,0,0,0,0,0,0,0,1,2,144,224,143,199,14,1,0,15,201,175,140,129,8,49,20,1,25,1,40,1,67,1,73,1,75,1,77,1,81,1,88,1,93,2,100,1,102,1,104,1,108,1,111,2,114,4,119,2,123,10,135,1,18,155,1,1,161,1,1,163,1,1,165,1,1,169,1,1,175,1,1,179,1,15,196,1,42,243,1,1,245,1,1,247,1,1,251,1,1,254,1,1,132,2,1,134,2,1,136,2,1,140,2,1,145,2,1,148,2,13,163,2,16,181,2,255,2,222,5,1,224,5,1,226,5,1,228,5,6,239,5,1,134,6,10,148,6,1,152,6,1,154,6,3],"version":0,"object_id":"87bc006e-c1eb-47fd-9ac6-e39b17956369"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json b/frontend/appflowy_web_app/cypress/fixtures/database/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json new file mode 100644 index 0000000000..ceebd01573 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json @@ -0,0 +1 @@ +{"data":{"state_vector":[16,225,154,253,156,5,2,195,240,220,252,7,5,230,172,170,202,7,8,135,161,218,171,7,6,139,153,229,238,6,2,237,201,168,43,16,238,188,221,160,14,2,244,240,200,227,1,33,181,255,217,196,15,125,212,226,138,162,13,39,151,225,131,140,9,2,248,251,128,198,10,7,149,205,253,206,14,12,251,237,143,129,13,7,158,192,169,36,18,190,203,155,67,2],"doc_state":[16,102,181,255,217,196,15,0,39,1,4,100,97,116,97,8,100,97,116,97,98,97,115,101,1,40,0,181,255,217,196,15,0,2,105,100,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,39,0,181,255,217,196,15,0,6,102,105,101,108,100,115,1,39,0,181,255,217,196,15,0,5,118,105,101,119,115,1,39,0,181,255,217,196,15,0,5,109,101,116,97,115,1,40,0,181,255,217,196,15,4,3,105,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,39,0,181,255,217,196,15,2,6,51,111,45,90,115,109,1,40,0,181,255,217,196,15,6,2,105,100,1,119,6,51,111,45,90,115,109,40,0,181,255,217,196,15,6,4,110,97,109,101,1,119,11,68,101,115,99,114,105,112,116,105,111,110,40,0,181,255,217,196,15,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,162,40,0,181,255,217,196,15,6,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,39,162,40,0,181,255,217,196,15,6,10,105,115,95,112,114,105,109,97,114,121,1,120,40,0,181,255,217,196,15,6,2,116,121,1,122,0,0,0,0,0,0,0,0,39,0,181,255,217,196,15,6,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,181,255,217,196,15,13,1,48,1,40,0,181,255,217,196,15,14,4,100,97,116,97,1,119,0,33,0,181,255,217,196,15,2,6,121,52,52,50,48,119,1,0,9,39,0,181,255,217,196,15,3,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,1,40,0,181,255,217,196,15,26,2,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,40,0,181,255,217,196,15,26,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,40,0,181,255,217,196,15,26,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,181,255,217,196,15,26,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,162,33,0,181,255,217,196,15,26,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,181,255,217,196,15,26,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,181,255,217,196,15,32,1,49,1,40,0,181,255,217,196,15,33,21,104,105,100,101,95,117,110,103,114,111,117,112,101,100,95,99,111,108,117,109,110,1,121,40,0,181,255,217,196,15,33,22,99,111,108,108,97,112,115,101,95,104,105,100,100,101,110,95,103,114,111,117,112,115,1,121,40,0,181,255,217,196,15,26,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,39,0,181,255,217,196,15,26,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,181,255,217,196,15,37,6,51,111,45,90,115,109,1,40,0,181,255,217,196,15,38,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,40,0,181,255,217,196,15,38,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,181,255,217,196,15,38,4,119,114,97,112,1,120,33,0,181,255,217,196,15,37,6,121,52,52,50,48,119,1,0,3,39,0,181,255,217,196,15,26,7,102,105,108,116,101,114,115,0,39,0,181,255,217,196,15,26,6,103,114,111,117,112,115,0,39,0,181,255,217,196,15,26,5,115,111,114,116,115,0,39,0,181,255,217,196,15,26,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,181,255,217,196,15,49,1,118,1,2,105,100,119,6,51,111,45,90,115,109,129,181,255,217,196,15,50,1,39,0,181,255,217,196,15,26,10,114,111,119,95,111,114,100,101,114,115,0,8,0,181,255,217,196,15,52,3,118,2,2,105,100,119,36,50,48,56,100,50,52,56,102,45,53,99,48,56,45,52,98,101,53,45,97,48,50,50,45,101,48,97,57,55,99,50,100,55,48,53,101,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,100,51,50,101,52,56,97,52,45,99,102,48,100,45,52,56,97,56,45,57,53,57,57,45,53,51,51,57,97,56,49,53,56,99,53,48,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,101,56,55,56,51,48,55,45,97,98,49,101,45,52,50,101,53,45,56,57,55,98,45,56,97,51,101,97,55,56,97,52,53,49,53,161,181,255,217,196,15,31,1,7,0,181,255,217,196,15,47,1,33,0,181,255,217,196,15,57,6,103,114,111,117,112,115,1,33,0,181,255,217,196,15,57,8,102,105,101,108,100,95,105,100,1,33,0,181,255,217,196,15,57,2,116,121,1,33,0,181,255,217,196,15,57,7,99,111,110,116,101,110,116,1,33,0,181,255,217,196,15,57,2,105,100,1,161,181,255,217,196,15,56,1,161,181,255,217,196,15,58,1,0,4,161,181,255,217,196,15,62,1,161,181,255,217,196,15,60,1,161,181,255,217,196,15,61,1,161,181,255,217,196,15,59,1,39,0,181,255,217,196,15,3,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,1,40,0,181,255,217,196,15,73,2,105,100,1,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,40,0,181,255,217,196,15,73,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,40,0,181,255,217,196,15,73,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,181,255,217,196,15,73,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,181,255,217,196,15,73,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,181,255,217,196,15,73,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,181,255,217,196,15,73,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,181,255,217,196,15,73,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,181,255,217,196,15,73,7,102,105,108,116,101,114,115,0,39,0,181,255,217,196,15,73,6,103,114,111,117,112,115,0,39,0,181,255,217,196,15,73,5,115,111,114,116,115,0,39,0,181,255,217,196,15,73,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,181,255,217,196,15,85,1,118,1,2,105,100,119,6,51,111,45,90,115,109,129,181,255,217,196,15,86,1,39,0,181,255,217,196,15,73,10,114,111,119,95,111,114,100,101,114,115,0,8,0,181,255,217,196,15,88,3,118,2,2,105,100,119,36,50,48,56,100,50,52,56,102,45,53,99,48,56,45,52,98,101,53,45,97,48,50,50,45,101,48,97,57,55,99,50,100,55,48,53,101,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,100,51,50,101,52,56,97,52,45,99,102,48,100,45,52,56,97,56,45,57,53,57,57,45,53,51,51,57,97,56,49,53,56,99,53,48,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,56,101,56,55,56,51,48,55,45,97,98,49,101,45,52,50,101,53,45,56,57,55,98,45,56,97,51,101,97,55,56,97,52,53,49,53,161,181,255,217,196,15,78,1,161,181,255,217,196,15,63,2,161,181,255,217,196,15,92,1,168,181,255,217,196,15,95,1,122,0,0,0,0,102,76,39,194,136,181,255,217,196,15,87,1,118,1,2,105,100,119,6,81,51,56,55,119,49,39,0,181,255,217,196,15,81,6,81,51,56,55,119,49,1,40,0,181,255,217,196,15,98,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,161,181,255,217,196,15,94,1,136,181,255,217,196,15,51,1,118,1,2,105,100,119,6,81,51,56,55,119,49,39,0,181,255,217,196,15,37,6,81,51,56,55,119,49,1,40,0,181,255,217,196,15,102,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,181,255,217,196,15,2,6,81,51,56,55,119,49,1,40,0,181,255,217,196,15,104,2,105,100,1,119,6,81,51,56,55,119,49,40,0,181,255,217,196,15,104,4,110,97,109,101,1,119,8,67,104,101,99,107,98,111,120,40,0,181,255,217,196,15,104,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,194,40,0,181,255,217,196,15,104,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,39,194,40,0,181,255,217,196,15,104,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,181,255,217,196,15,104,2,116,121,1,122,0,0,0,0,0,0,0,5,39,0,181,255,217,196,15,104,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,181,255,217,196,15,111,1,53,1,168,181,255,217,196,15,100,1,122,0,0,0,0,102,76,39,200,168,181,255,217,196,15,69,1,119,8,103,58,85,113,84,54,68,80,168,181,255,217,196,15,70,1,122,0,0,0,0,0,0,0,3,168,181,255,217,196,15,72,1,119,6,121,52,52,50,48,119,168,181,255,217,196,15,71,1,119,0,167,181,255,217,196,15,64,0,8,0,181,255,217,196,15,118,6,118,2,2,105,100,119,6,121,52,52,50,48,119,7,118,105,115,105,98,108,101,120,118,2,2,105,100,119,4,117,76,117,51,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,4,73,113,105,73,118,2,2,105,100,119,4,82,69,88,119,7,118,105,115,105,98,108,101,120,118,2,7,118,105,115,105,98,108,101,120,2,105,100,119,3,89,101,115,118,2,2,105,100,119,2,78,111,7,118,105,115,105,98,108,101,120,1,149,205,253,206,14,0,161,135,161,218,171,7,5,12,1,238,188,221,160,14,0,161,149,205,253,206,14,11,2,1,212,226,138,162,13,0,161,251,237,143,129,13,6,39,1,251,237,143,129,13,0,161,248,251,128,198,10,6,7,1,248,251,128,198,10,0,161,151,225,131,140,9,1,7,1,151,225,131,140,9,0,161,244,240,200,227,1,32,2,1,195,240,220,252,7,0,161,139,153,229,238,6,1,5,2,230,172,170,202,7,0,161,195,240,220,252,7,4,7,168,230,172,170,202,7,6,1,122,0,0,0,0,102,88,25,34,1,135,161,218,171,7,0,161,225,154,253,156,5,1,6,1,139,153,229,238,6,0,161,158,192,169,36,17,2,1,225,154,253,156,5,0,161,237,201,168,43,15,2,1,244,240,200,227,1,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,33,1,190,203,155,67,0,161,135,161,218,171,7,5,2,1,237,201,168,43,0,161,212,226,138,162,13,38,16,1,158,192,169,36,0,161,238,188,221,160,14,1,18,16,225,154,253,156,5,1,0,2,195,240,220,252,7,1,0,5,230,172,170,202,7,1,0,7,135,161,218,171,7,1,0,6,139,153,229,238,6,1,0,2,237,201,168,43,1,0,16,238,188,221,160,14,1,0,2,244,240,200,227,1,1,0,33,181,255,217,196,15,10,16,10,31,1,42,4,51,1,56,1,58,15,78,1,87,1,92,4,100,1,212,226,138,162,13,1,0,39,151,225,131,140,9,1,0,2,248,251,128,198,10,1,0,7,149,205,253,206,14,1,0,12,251,237,143,129,13,1,0,7,158,192,169,36,1,0,18,190,203,155,67,1,0,2],"version":0,"object_id":"ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/ce267d12-3b61-4ebb-bb03-d65272f5f817.json b/frontend/appflowy_web_app/cypress/fixtures/database/ce267d12-3b61-4ebb-bb03-d65272f5f817.json new file mode 100644 index 0000000000..428ca72d5b --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/ce267d12-3b61-4ebb-bb03-d65272f5f817.json @@ -0,0 +1 @@ +{"data":{"state_vector":[20,162,178,170,161,1,6,131,222,171,184,9,39,130,208,239,179,11,2,133,224,179,154,9,2,231,138,159,208,10,2,231,217,162,139,1,5,170,249,160,147,7,21,139,202,180,177,14,33,236,192,251,208,2,9,204,220,240,227,3,212,1,145,151,150,143,2,7,146,214,128,188,1,65,243,175,215,198,3,7,212,178,171,164,8,14,149,159,177,202,15,2,149,178,144,155,15,8,247,242,142,226,14,6,249,247,244,162,15,37,219,228,172,146,1,10,251,157,254,151,3,107],"doc_state":[20,1,149,159,177,202,15,0,161,170,249,160,147,7,20,2,22,249,247,244,162,15,0,39,0,251,157,254,151,3,3,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,1,40,0,249,247,244,162,15,0,2,105,100,1,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,40,0,249,247,244,162,15,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,249,247,244,162,15,0,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,249,247,244,162,15,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,40,0,249,247,244,162,15,0,11,109,111,100,105,102,105,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,39,0,249,247,244,162,15,0,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,249,247,244,162,15,6,1,50,1,40,0,249,247,244,162,15,7,9,108,97,121,111,117,116,95,116,121,1,122,0,0,0,0,0,0,0,0,40,0,249,247,244,162,15,7,17,115,104,111,119,95,119,101,101,107,95,110,117,109,98,101,114,115,1,120,40,0,249,247,244,162,15,7,17,102,105,114,115,116,95,100,97,121,95,111,102,95,119,101,101,107,1,122,0,0,0,0,0,0,0,0,40,0,249,247,244,162,15,7,13,115,104,111,119,95,119,101,101,107,101,110,100,115,1,120,40,0,249,247,244,162,15,7,8,102,105,101,108,100,95,105,100,1,119,6,71,115,66,65,97,76,40,0,249,247,244,162,15,0,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,39,0,249,247,244,162,15,0,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,249,247,244,162,15,0,7,102,105,108,116,101,114,115,0,39,0,249,247,244,162,15,0,6,103,114,111,117,112,115,0,39,0,249,247,244,162,15,0,5,115,111,114,116,115,0,39,0,249,247,244,162,15,0,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,249,247,244,162,15,18,11,118,1,2,105,100,119,6,72,95,74,113,85,76,118,1,2,105,100,119,6,55,85,107,117,54,82,118,1,2,105,100,119,6,95,82,45,112,104,105,118,1,2,105,100,119,6,99,78,53,98,120,74,118,1,2,105,100,119,6,71,115,66,65,97,76,118,1,2,105,100,119,6,71,79,80,107,116,118,118,1,2,105,100,119,6,70,99,112,109,80,101,118,1,2,105,100,119,6,112,70,120,57,67,45,118,1,2,105,100,119,6,101,49,98,55,48,88,118,1,2,105,100,119,6,80,78,113,89,102,76,118,1,2,105,100,119,6,75,71,50,113,74,65,39,0,249,247,244,162,15,0,10,114,111,119,95,111,114,100,101,114,115,0,8,0,249,247,244,162,15,30,6,118,2,2,105,100,119,36,55,55,49,55,48,55,57,98,45,48,53,98,54,45,52,97,48,97,45,56,101,101,52,45,52,56,55,51,57,102,98,102,51,97,53,50,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,97,55,51,54,55,52,97,101,45,51,51,48,49,45,52,53,97,51,45,98,56,48,49,45,51,102,49,50,101,54,102,99,98,53,54,54,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,50,49,53,48,99,102,102,54,45,102,102,56,48,45,52,51,51,52,45,56,99,56,97,45,57,52,101,56,50,97,54,52,51,55,57,97,6,104,101,105,103,104,116,125,60,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,53,49,99,102,48,57,48,54,45,97,100,52,54,45,52,100,97,101,45,97,51,98,57,45,50,101,48,48,51,102,56,51,54,56,99,49,118,2,2,105,100,119,36,97,48,48,101,99,102,55,56,45,97,56,50,51,45,52,51,102,49,45,98,53,52,50,45,101,100,48,55,49,51,57,52,97,55,49,55,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,57,50,97,50,49,51,55,101,45,98,48,48,98,45,52,51,56,56,45,56,53,49,102,45,97,48,101,102,99,51,100,101,55,99,97,51,6,104,101,105,103,104,116,125,60,2,149,178,144,155,15,0,161,231,217,162,139,1,4,7,168,149,178,144,155,15,6,1,122,0,0,0,0,102,88,25,34,1,247,242,142,226,14,0,161,243,175,215,198,3,6,6,1,139,202,180,177,14,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,33,1,130,208,239,179,11,0,161,247,242,142,226,14,5,2,1,231,138,159,208,10,0,161,219,228,172,146,1,9,2,1,131,222,171,184,9,0,161,146,214,128,188,1,64,39,1,133,224,179,154,9,0,161,231,138,159,208,10,1,2,1,212,178,171,164,8,0,161,162,178,170,161,1,5,14,1,170,249,160,147,7,0,161,133,224,179,154,9,1,21,204,1,204,220,240,227,3,0,161,251,157,254,151,3,91,1,136,251,157,254,151,3,74,1,118,2,2,105,100,119,36,97,55,51,54,55,52,97,101,45,51,51,48,49,45,52,53,97,51,45,98,56,48,49,45,51,102,49,50,101,54,102,99,98,53,54,54,6,104,101,105,103,104,116,125,60,161,204,220,240,227,3,0,1,136,204,220,240,227,3,1,1,118,2,2,105,100,119,36,50,49,53,48,99,102,102,54,45,102,102,56,48,45,52,51,51,52,45,56,99,56,97,45,57,52,101,56,50,97,54,52,51,55,57,97,6,104,101,105,103,104,116,125,60,161,204,220,240,227,3,2,1,136,204,220,240,227,3,3,1,118,2,2,105,100,119,36,53,49,99,102,48,57,48,54,45,97,100,52,54,45,52,100,97,101,45,97,51,98,57,45,50,101,48,48,51,102,56,51,54,56,99,49,6,104,101,105,103,104,116,125,60,161,204,220,240,227,3,4,1,136,204,220,240,227,3,5,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,97,48,48,101,99,102,55,56,45,97,56,50,51,45,52,51,102,49,45,98,53,52,50,45,101,100,48,55,49,51,57,52,97,55,49,55,39,0,251,157,254,151,3,3,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,1,40,0,204,220,240,227,3,8,2,105,100,1,119,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,40,0,204,220,240,227,3,8,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,204,220,240,227,3,8,4,110,97,109,101,1,119,4,71,114,105,100,40,0,204,220,240,227,3,8,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,0,0,0,0,33,0,204,220,240,227,3,8,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,204,220,240,227,3,8,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,40,0,204,220,240,227,3,8,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,39,0,204,220,240,227,3,8,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,204,220,240,227,3,8,7,102,105,108,116,101,114,115,0,39,0,204,220,240,227,3,8,6,103,114,111,117,112,115,0,39,0,204,220,240,227,3,8,5,115,111,114,116,115,0,39,0,204,220,240,227,3,8,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,204,220,240,227,3,20,5,118,1,2,105,100,119,6,72,95,74,113,85,76,118,1,2,105,100,119,6,55,85,107,117,54,82,118,1,2,105,100,119,6,95,82,45,112,104,105,118,1,2,105,100,119,6,99,78,53,98,120,74,118,1,2,105,100,119,6,71,115,66,65,97,76,39,0,204,220,240,227,3,8,10,114,111,119,95,111,114,100,101,114,115,0,8,0,204,220,240,227,3,26,5,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,55,55,49,55,48,55,57,98,45,48,53,98,54,45,52,97,48,97,45,56,101,101,52,45,52,56,55,51,57,102,98,102,51,97,53,50,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,97,55,51,54,55,52,97,101,45,51,51,48,49,45,52,53,97,51,45,98,56,48,49,45,51,102,49,50,101,54,102,99,98,53,54,54,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,50,49,53,48,99,102,102,54,45,102,102,56,48,45,52,51,51,52,45,56,99,56,97,45,57,52,101,56,50,97,54,52,51,55,57,97,118,2,2,105,100,119,36,53,49,99,102,48,57,48,54,45,97,100,52,54,45,52,100,97,101,45,97,51,98,57,45,50,101,48,48,51,102,56,51,54,56,99,49,6,104,101,105,103,104,116,125,60,118,2,2,105,100,119,36,97,48,48,101,99,102,55,56,45,97,56,50,51,45,52,51,102,49,45,98,53,52,50,45,101,100,48,55,49,51,57,52,97,55,49,55,6,104,101,105,103,104,116,125,60,161,251,157,254,151,3,85,1,168,251,157,254,151,3,87,1,122,0,0,0,0,0,0,0,6,39,0,251,157,254,151,3,88,1,54,1,40,0,204,220,240,227,3,34,7,99,111,110,116,101,110,116,1,119,0,40,0,204,220,240,227,3,34,3,117,114,108,1,119,0,168,204,220,240,227,3,32,1,122,0,0,0,0,102,77,165,82,40,0,251,157,254,151,3,89,7,99,111,110,116,101,110,116,1,119,0,40,0,251,157,254,151,3,89,3,117,114,108,1,119,0,161,204,220,240,227,3,6,1,161,204,220,240,227,3,13,1,161,204,220,240,227,3,40,1,136,251,157,254,151,3,92,1,118,1,2,105,100,119,6,71,79,80,107,116,118,39,0,251,157,254,151,3,52,6,71,79,80,107,116,118,1,40,0,204,220,240,227,3,44,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,41,1,136,204,220,240,227,3,25,1,118,1,2,105,100,119,6,71,79,80,107,116,118,39,0,204,220,240,227,3,16,6,71,79,80,107,116,118,1,40,0,204,220,240,227,3,48,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,71,79,80,107,116,118,1,40,0,204,220,240,227,3,50,2,105,100,1,119,6,71,79,80,107,116,118,40,0,204,220,240,227,3,50,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,50,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,99,33,0,204,220,240,227,3,50,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,50,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,50,2,116,121,1,39,0,204,220,240,227,3,50,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,57,1,48,1,40,0,204,220,240,227,3,58,4,100,97,116,97,1,119,0,161,204,220,240,227,3,54,1,168,204,220,240,227,3,56,1,122,0,0,0,0,0,0,0,3,39,0,204,220,240,227,3,57,1,51,1,33,0,204,220,240,227,3,62,7,99,111,110,116,101,110,116,1,161,204,220,240,227,3,60,1,40,0,204,220,240,227,3,58,7,99,111,110,116,101,110,116,1,119,36,123,34,111,112,116,105,111,110,115,34,58,91,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,204,220,240,227,3,42,1,161,204,220,240,227,3,46,1,168,251,157,254,151,3,75,1,122,0,0,0,0,102,77,165,108,168,251,157,254,151,3,76,1,119,121,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,110,103,110,85,34,44,34,110,97,109,101,34,58,34,49,49,49,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,73,73,66,100,34,44,34,110,97,109,101,34,58,34,49,50,50,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,204,220,240,227,3,64,1,161,204,220,240,227,3,63,1,168,204,220,240,227,3,70,1,122,0,0,0,0,102,77,165,117,168,204,220,240,227,3,71,1,119,121,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,89,101,75,100,34,44,34,110,97,109,101,34,58,34,51,50,49,34,44,34,99,111,108,111,114,34,58,34,80,105,110,107,34,125,44,123,34,105,100,34,58,34,104,77,109,67,34,44,34,110,97,109,101,34,58,34,49,50,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,100,105,115,97,98,108,101,95,99,111,108,111,114,34,58,102,97,108,115,101,125,161,204,220,240,227,3,66,1,136,204,220,240,227,3,43,1,118,1,2,105,100,119,6,70,99,112,109,80,101,39,0,251,157,254,151,3,52,6,70,99,112,109,80,101,1,40,0,204,220,240,227,3,76,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,67,1,136,204,220,240,227,3,47,1,118,1,2,105,100,119,6,70,99,112,109,80,101,39,0,204,220,240,227,3,16,6,70,99,112,109,80,101,1,40,0,204,220,240,227,3,80,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,70,99,112,109,80,101,1,40,0,204,220,240,227,3,82,2,105,100,1,119,6,70,99,112,109,80,101,40,0,204,220,240,227,3,82,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,82,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,120,33,0,204,220,240,227,3,82,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,82,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,82,2,116,121,1,39,0,204,220,240,227,3,82,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,89,1,48,1,40,0,204,220,240,227,3,90,4,100,97,116,97,1,119,0,168,204,220,240,227,3,86,1,122,0,0,0,0,102,77,165,125,168,204,220,240,227,3,88,1,122,0,0,0,0,0,0,0,5,39,0,204,220,240,227,3,89,1,53,1,161,204,220,240,227,3,74,1,136,204,220,240,227,3,75,1,118,1,2,105,100,119,6,112,70,120,57,67,45,39,0,251,157,254,151,3,52,6,112,70,120,57,67,45,1,40,0,204,220,240,227,3,97,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,78,1,136,204,220,240,227,3,79,1,118,1,2,105,100,119,6,112,70,120,57,67,45,39,0,204,220,240,227,3,16,6,112,70,120,57,67,45,1,40,0,204,220,240,227,3,101,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,112,70,120,57,67,45,1,40,0,204,220,240,227,3,103,2,105,100,1,119,6,112,70,120,57,67,45,40,0,204,220,240,227,3,103,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,103,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,129,33,0,204,220,240,227,3,103,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,103,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,103,2,116,121,1,39,0,204,220,240,227,3,103,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,110,1,48,1,40,0,204,220,240,227,3,111,4,100,97,116,97,1,119,0,168,204,220,240,227,3,107,1,122,0,0,0,0,102,77,165,135,168,204,220,240,227,3,109,1,122,0,0,0,0,0,0,0,7,39,0,204,220,240,227,3,110,1,55,1,161,204,220,240,227,3,95,1,136,204,220,240,227,3,96,1,118,1,2,105,100,119,6,101,49,98,55,48,88,39,0,251,157,254,151,3,52,6,101,49,98,55,48,88,1,40,0,204,220,240,227,3,118,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,99,1,136,204,220,240,227,3,100,1,118,1,2,105,100,119,6,101,49,98,55,48,88,39,0,204,220,240,227,3,16,6,101,49,98,55,48,88,1,40,0,204,220,240,227,3,122,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,101,49,98,55,48,88,1,40,0,204,220,240,227,3,124,2,105,100,1,119,6,101,49,98,55,48,88,40,0,204,220,240,227,3,124,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,124,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,151,33,0,204,220,240,227,3,124,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,124,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,124,2,116,121,1,39,0,204,220,240,227,3,124,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,131,1,1,48,1,40,0,204,220,240,227,3,132,1,4,100,97,116,97,1,119,0,161,204,220,240,227,3,128,1,1,168,204,220,240,227,3,130,1,1,122,0,0,0,0,0,0,0,8,39,0,204,220,240,227,3,131,1,1,56,1,40,0,204,220,240,227,3,136,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,204,220,240,227,3,136,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,8,40,0,204,220,240,227,3,136,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,204,220,240,227,3,136,1,12,105,110,99,108,117,100,101,95,116,105,109,101,1,120,168,204,220,240,227,3,134,1,1,122,0,0,0,0,102,77,165,161,40,0,204,220,240,227,3,132,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,204,220,240,227,3,132,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,8,40,0,204,220,240,227,3,132,1,12,105,110,99,108,117,100,101,95,116,105,109,101,1,120,40,0,204,220,240,227,3,132,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,161,204,220,240,227,3,116,1,161,204,220,240,227,3,120,1,161,204,220,240,227,3,146,1,1,136,204,220,240,227,3,117,1,118,1,2,105,100,119,6,80,78,113,89,102,76,39,0,251,157,254,151,3,52,6,80,78,113,89,102,76,1,40,0,204,220,240,227,3,150,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,147,1,1,136,204,220,240,227,3,121,1,118,1,2,105,100,119,6,80,78,113,89,102,76,39,0,204,220,240,227,3,16,6,80,78,113,89,102,76,1,40,0,204,220,240,227,3,154,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,80,78,113,89,102,76,1,40,0,204,220,240,227,3,156,1,2,105,100,1,119,6,80,78,113,89,102,76,40,0,204,220,240,227,3,156,1,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,156,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,164,33,0,204,220,240,227,3,156,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,156,1,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,156,1,2,116,121,1,39,0,204,220,240,227,3,156,1,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,163,1,1,48,1,40,0,204,220,240,227,3,164,1,4,100,97,116,97,1,119,0,161,204,220,240,227,3,160,1,1,168,204,220,240,227,3,162,1,1,122,0,0,0,0,0,0,0,9,39,0,204,220,240,227,3,163,1,1,57,1,40,0,204,220,240,227,3,168,1,12,105,110,99,108,117,100,101,95,116,105,109,101,1,120,40,0,204,220,240,227,3,168,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,204,220,240,227,3,168,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,204,220,240,227,3,168,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,9,168,204,220,240,227,3,166,1,1,122,0,0,0,0,102,77,165,166,40,0,204,220,240,227,3,164,1,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,204,220,240,227,3,164,1,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,204,220,240,227,3,164,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,9,40,0,204,220,240,227,3,164,1,12,105,110,99,108,117,100,101,95,116,105,109,101,1,120,161,204,220,240,227,3,148,1,1,161,204,220,240,227,3,152,1,1,161,204,220,240,227,3,178,1,1,136,204,220,240,227,3,149,1,1,118,1,2,105,100,119,6,75,71,50,113,74,65,39,0,251,157,254,151,3,52,6,75,71,50,113,74,65,1,40,0,204,220,240,227,3,182,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,161,204,220,240,227,3,179,1,1,136,204,220,240,227,3,153,1,1,118,1,2,105,100,119,6,75,71,50,113,74,65,39,0,204,220,240,227,3,16,6,75,71,50,113,74,65,1,40,0,204,220,240,227,3,186,1,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,2,6,75,71,50,113,74,65,1,40,0,204,220,240,227,3,188,1,2,105,100,1,119,6,75,71,50,113,74,65,40,0,204,220,240,227,3,188,1,4,110,97,109,101,1,119,4,84,101,120,116,40,0,204,220,240,227,3,188,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,168,33,0,204,220,240,227,3,188,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,204,220,240,227,3,188,1,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,204,220,240,227,3,188,1,2,116,121,1,39,0,204,220,240,227,3,188,1,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,204,220,240,227,3,195,1,1,48,1,40,0,204,220,240,227,3,196,1,4,100,97,116,97,1,119,0,161,204,220,240,227,3,192,1,1,168,204,220,240,227,3,194,1,1,122,0,0,0,0,0,0,0,10,39,0,204,220,240,227,3,195,1,2,49,48,1,33,0,204,220,240,227,3,200,1,11,100,97,116,97,98,97,115,101,95,105,100,1,161,204,220,240,227,3,198,1,1,40,0,204,220,240,227,3,196,1,11,100,97,116,97,98,97,115,101,95,105,100,1,119,0,161,204,220,240,227,3,180,1,1,161,204,220,240,227,3,184,1,1,168,204,220,240,227,3,202,1,1,122,0,0,0,0,102,77,165,173,168,204,220,240,227,3,201,1,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,168,204,220,240,227,3,204,1,1,122,0,0,0,0,102,77,187,135,136,204,220,240,227,3,7,1,118,2,6,104,101,105,103,104,116,125,60,2,105,100,119,36,57,50,97,50,49,51,55,101,45,98,48,48,98,45,52,51,56,56,45,56,53,49,102,45,97,48,101,102,99,51,100,101,55,99,97,51,168,204,220,240,227,3,205,1,1,122,0,0,0,0,102,77,187,135,136,204,220,240,227,3,31,1,118,2,2,105,100,119,36,57,50,97,50,49,51,55,101,45,98,48,48,98,45,52,51,56,56,45,56,53,49,102,45,97,48,101,102,99,51,100,101,55,99,97,51,6,104,101,105,103,104,116,125,60,1,243,175,215,198,3,0,161,236,192,251,208,2,8,7,105,251,157,254,151,3,0,39,1,4,100,97,116,97,8,100,97,116,97,98,97,115,101,1,40,0,251,157,254,151,3,0,2,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,39,0,251,157,254,151,3,0,6,102,105,101,108,100,115,1,39,0,251,157,254,151,3,0,5,118,105,101,119,115,1,39,0,251,157,254,151,3,0,5,109,101,116,97,115,1,40,0,251,157,254,151,3,4,3,105,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,39,0,251,157,254,151,3,2,6,72,95,74,113,85,76,1,40,0,251,157,254,151,3,6,2,105,100,1,119,6,72,95,74,113,85,76,40,0,251,157,254,151,3,6,4,110,97,109,101,1,119,5,84,105,116,108,101,40,0,251,157,254,151,3,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,231,40,0,251,157,254,151,3,6,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,101,231,40,0,251,157,254,151,3,6,10,105,115,95,112,114,105,109,97,114,121,1,120,40,0,251,157,254,151,3,6,2,116,121,1,122,0,0,0,0,0,0,0,0,39,0,251,157,254,151,3,6,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,251,157,254,151,3,13,1,48,1,40,0,251,157,254,151,3,14,4,100,97,116,97,1,119,0,39,0,251,157,254,151,3,2,6,55,85,107,117,54,82,1,40,0,251,157,254,151,3,16,2,105,100,1,119,6,55,85,107,117,54,82,40,0,251,157,254,151,3,16,4,110,97,109,101,1,119,4,68,97,116,101,40,0,251,157,254,151,3,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,231,40,0,251,157,254,151,3,16,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,101,231,40,0,251,157,254,151,3,16,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,251,157,254,151,3,16,2,116,121,1,122,0,0,0,0,0,0,0,2,39,0,251,157,254,151,3,16,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,251,157,254,151,3,23,1,50,1,40,0,251,157,254,151,3,24,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,3,40,0,251,157,254,151,3,24,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,40,0,251,157,254,151,3,24,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,39,0,251,157,254,151,3,2,6,95,82,45,112,104,105,1,40,0,251,157,254,151,3,28,2,105,100,1,119,6,95,82,45,112,104,105,40,0,251,157,254,151,3,28,4,110,97,109,101,1,119,4,84,97,103,115,40,0,251,157,254,151,3,28,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,231,33,0,251,157,254,151,3,28,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,251,157,254,151,3,28,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,251,157,254,151,3,28,2,116,121,1,122,0,0,0,0,0,0,0,4,39,0,251,157,254,151,3,28,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,251,157,254,151,3,35,1,52,1,33,0,251,157,254,151,3,36,7,99,111,110,116,101,110,116,1,39,0,251,157,254,151,3,3,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,1,40,0,251,157,254,151,3,38,2,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,40,0,251,157,254,151,3,38,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,251,157,254,151,3,38,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,251,157,254,151,3,38,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,231,33,0,251,157,254,151,3,38,11,109,111,100,105,102,105,101,100,95,97,116,1,39,0,251,157,254,151,3,38,15,108,97,121,111,117,116,95,115,101,116,116,105,110,103,115,1,39,0,251,157,254,151,3,44,1,50,1,40,0,251,157,254,151,3,45,13,115,104,111,119,95,119,101,101,107,101,110,100,115,1,120,40,0,251,157,254,151,3,45,9,108,97,121,111,117,116,95,116,121,1,122,0,0,0,0,0,0,0,0,40,0,251,157,254,151,3,45,8,102,105,101,108,100,95,105,100,1,119,6,55,85,107,117,54,82,40,0,251,157,254,151,3,45,17,102,105,114,115,116,95,100,97,121,95,111,102,95,119,101,101,107,1,122,0,0,0,0,0,0,0,0,40,0,251,157,254,151,3,45,17,115,104,111,119,95,119,101,101,107,95,110,117,109,98,101,114,115,1,120,40,0,251,157,254,151,3,38,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,39,0,251,157,254,151,3,38,14,102,105,101,108,100,95,115,101,116,116,105,110,103,115,1,39,0,251,157,254,151,3,52,6,72,95,74,113,85,76,1,40,0,251,157,254,151,3,53,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,251,157,254,151,3,53,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,0,40,0,251,157,254,151,3,53,4,119,114,97,112,1,120,39,0,251,157,254,151,3,52,6,55,85,107,117,54,82,1,40,0,251,157,254,151,3,57,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,40,0,251,157,254,151,3,57,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,251,157,254,151,3,57,4,119,114,97,112,1,120,39,0,251,157,254,151,3,52,6,95,82,45,112,104,105,1,40,0,251,157,254,151,3,61,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,40,0,251,157,254,151,3,61,5,119,105,100,116,104,1,122,0,0,0,0,0,0,0,150,40,0,251,157,254,151,3,61,4,119,114,97,112,1,120,39,0,251,157,254,151,3,38,7,102,105,108,116,101,114,115,0,39,0,251,157,254,151,3,38,6,103,114,111,117,112,115,0,39,0,251,157,254,151,3,38,5,115,111,114,116,115,0,39,0,251,157,254,151,3,38,12,102,105,101,108,100,95,111,114,100,101,114,115,0,8,0,251,157,254,151,3,68,3,118,1,2,105,100,119,6,72,95,74,113,85,76,118,1,2,105,100,119,6,55,85,107,117,54,82,118,1,2,105,100,119,6,95,82,45,112,104,105,39,0,251,157,254,151,3,38,10,114,111,119,95,111,114,100,101,114,115,0,161,251,157,254,151,3,43,1,8,0,251,157,254,151,3,72,1,118,2,2,105,100,119,36,55,55,49,55,48,55,57,98,45,48,53,98,54,45,52,97,48,97,45,56,101,101,52,45,52,56,55,51,57,102,98,102,51,97,53,50,6,104,101,105,103,104,116,125,60,161,251,157,254,151,3,32,1,161,251,157,254,151,3,37,1,161,251,157,254,151,3,73,1,136,251,157,254,151,3,71,1,118,1,2,105,100,119,6,99,78,53,98,120,74,39,0,251,157,254,151,3,52,6,99,78,53,98,120,74,1,40,0,251,157,254,151,3,79,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,251,157,254,151,3,2,6,99,78,53,98,120,74,1,40,0,251,157,254,151,3,81,2,105,100,1,119,6,99,78,53,98,120,74,40,0,251,157,254,151,3,81,4,110,97,109,101,1,119,4,84,101,120,116,40,0,251,157,254,151,3,81,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,102,8,33,0,251,157,254,151,3,81,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,40,0,251,157,254,151,3,81,10,105,115,95,112,114,105,109,97,114,121,1,121,33,0,251,157,254,151,3,81,2,116,121,1,39,0,251,157,254,151,3,81,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,251,157,254,151,3,88,1,48,1,40,0,251,157,254,151,3,89,4,100,97,116,97,1,119,0,161,251,157,254,151,3,77,1,136,251,157,254,151,3,78,1,118,1,2,105,100,119,6,71,115,66,65,97,76,39,0,251,157,254,151,3,52,6,71,115,66,65,97,76,1,40,0,251,157,254,151,3,93,10,118,105,115,105,98,105,108,105,116,121,1,122,0,0,0,0,0,0,0,1,39,0,251,157,254,151,3,2,6,71,115,66,65,97,76,1,40,0,251,157,254,151,3,95,2,105,100,1,119,6,71,115,66,65,97,76,40,0,251,157,254,151,3,95,4,110,97,109,101,1,119,4,68,97,116,101,40,0,251,157,254,151,3,95,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,102,24,40,0,251,157,254,151,3,95,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,102,24,40,0,251,157,254,151,3,95,10,105,115,95,112,114,105,109,97,114,121,1,121,40,0,251,157,254,151,3,95,2,116,121,1,122,0,0,0,0,0,0,0,2,39,0,251,157,254,151,3,95,11,116,121,112,101,95,111,112,116,105,111,110,1,39,0,251,157,254,151,3,102,1,50,1,40,0,251,157,254,151,3,103,11,116,105,109,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,0,40,0,251,157,254,151,3,103,11,116,105,109,101,122,111,110,101,95,105,100,1,119,0,40,0,251,157,254,151,3,103,11,100,97,116,101,95,102,111,114,109,97,116,1,122,0,0,0,0,0,0,0,1,1,236,192,251,208,2,0,161,145,151,150,143,2,6,9,1,145,151,150,143,2,0,161,131,222,171,184,9,38,7,1,146,214,128,188,1,0,161,212,178,171,164,8,13,65,1,162,178,170,161,1,0,161,139,202,180,177,14,32,6,2,219,228,172,146,1,0,161,247,242,142,226,14,5,1,161,130,208,239,179,11,1,9,1,231,217,162,139,1,0,161,149,159,177,202,15,1,5,19,130,208,239,179,11,1,0,2,162,178,170,161,1,1,0,6,131,222,171,184,9,1,0,39,133,224,179,154,9,1,0,2,231,217,162,139,1,1,0,5,231,138,159,208,10,1,0,2,170,249,160,147,7,1,0,21,139,202,180,177,14,1,0,33,236,192,251,208,2,1,0,9,204,220,240,227,3,39,0,1,2,1,4,1,6,1,13,1,32,1,40,3,46,1,54,1,56,1,60,1,63,2,66,2,70,2,74,1,78,1,86,1,88,1,95,1,99,1,107,1,109,1,116,1,120,1,128,1,1,130,1,1,134,1,1,146,1,3,152,1,1,160,1,1,162,1,1,166,1,1,178,1,3,184,1,1,192,1,1,194,1,1,198,1,1,201,1,2,204,1,2,145,151,150,143,2,1,0,7,146,214,128,188,1,1,0,65,243,175,215,198,3,1,0,7,212,178,171,164,8,1,0,14,149,159,177,202,15,1,0,2,149,178,144,155,15,1,0,7,247,242,142,226,14,1,0,6,219,228,172,146,1,1,0,10,251,157,254,151,3,8,32,1,37,1,43,1,73,1,75,3,85,1,87,1,91,1],"version":0,"object_id":"ce267d12-3b61-4ebb-bb03-d65272f5f817"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/databases.json b/frontend/appflowy_web_app/cypress/fixtures/database/databases.json new file mode 100644 index 0000000000..675bb0b42a --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/databases.json @@ -0,0 +1,80 @@ +[ + { + "database_id": "037a985f-f369-4c4a-8011-620012850a68", + "created_at": "1713429700", + "views": [ + "48c52cf7-bf98-43fa-96ad-b31aade9b071" + ] + }, + { + "database_id": "daea6aee-9365-4703-a8e2-a2fa6a07b214", + "created_at": "1714449533", + "views": [ + "b6347acb-3174-4f0e-98e9-dcce07e5dbf7" + ] + }, + { + "database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6", + "created_at": "0", + "views": [ + "7d2148fc-cace-4452-9c5c-96e52e6bf8b5", + "e410747b-5f2f-45a0-b2f7-890ad3001355", + "2143e95d-5dcb-4e0f-bb2c-50944e6e019f", + "a5566e49-f156-4168-9b2d-17926c5da329", + "135615fa-66f7-4451-9b54-d7e99445fca4", + "b4e77203-5c8b-48df-bbc5-2e1143eb0e61", + "a6af311f-cbc8-42c2-b801-7115619c3776" + ] + }, + { + "database_id": "4c658817-20db-4f56-b7f9-0637a22dfeb6", + "created_at": "0", + "views": [ + "7d2148fc-cace-4452-9c5c-96e52e6bf8b5", + "e97877f5-c365-4025-9e6a-e590c4b19dbb", + "f0c59921-04ee-4971-995c-79b7fd8c00e2", + "7eb697cd-6a55-40bb-96ac-0d4a3bc924b2" + ] + }, + { + "database_id": "ee63da2b-aa2a-4d0b-aab0-59008635363a", + "created_at": "0", + "views": [ + "2c1ee95a-1b09-4a1f-8d5e-501bc4861a9d", + "91ea7c08-f6b3-4b81-aa1e-d3664686186f" + ] + }, + { + "database_id": "e788f014-d0d3-4dfe-81ef-aa1ebb4d6366", + "created_at": "0", + "views": [ + "1b0e322d-4909-4c63-914a-d034fc363097", + "350f425b-b671-4e2d-8182-5998a6e62924" + ] + }, + { + "database_id": "ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d", + "created_at": "0", + "views": [ + "0ce13415-6cce-4497-94c6-475ad96c249e", + "e4c89421-12b2-4d02-863d-20949eec9271" + ] + }, + { + "database_id": "ce267d12-3b61-4ebb-bb03-d65272f5f817", + "created_at": "0", + "views": [ + "ee3ae8ce-959a-4df3-8734-40b535ff88e3", + "66a6f3bc-c78f-4f74-a09e-08d4717bf1fd", + "2bf50c03-f41f-4363-b5b1-101216a6c5cc" + ] + }, + { + "database_id": "87bc006e-c1eb-47fd-9ac6-e39b17956369", + "created_at": "0", + "views": [ + "7f233be4-1b4d-46b2-bcfc-f341b8d75267", + "a734a068-e73d-4b4b-853c-4daffea389c0" + ] + } +] \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6.json b/frontend/appflowy_web_app/cypress/fixtures/database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6.json new file mode 100644 index 0000000000..a55622fdfb --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6.json @@ -0,0 +1 @@ +{"2f944220-9f45-40d9-96b5-e8c0888daf7c":[58,1,230,232,236,161,15,0,161,147,212,241,172,2,1,6,1,144,227,205,159,15,0,161,150,141,187,97,5,10,17,186,193,182,130,15,0,161,237,231,215,147,4,0,1,39,0,128,159,198,124,8,6,115,111,118,85,116,69,1,40,0,186,193,182,130,15,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,9,22,40,0,186,193,182,130,15,1,4,100,97,116,97,1,119,0,40,0,186,193,182,130,15,1,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,40,0,186,193,182,130,15,1,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,40,0,186,193,182,130,15,1,8,105,115,95,114,97,110,103,101,1,121,40,0,186,193,182,130,15,1,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,40,0,186,193,182,130,15,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,186,193,182,130,15,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,67,9,22,161,186,193,182,130,15,0,1,39,0,128,159,198,124,8,6,106,87,101,95,116,54,1,40,0,186,193,182,130,15,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,52,223,39,0,186,193,182,130,15,11,4,100,97,116,97,0,8,0,186,193,182,130,15,13,1,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,40,0,186,193,182,130,15,11,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,10,40,0,186,193,182,130,15,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,67,52,223,1,209,188,177,215,14,0,161,205,239,215,19,1,26,1,167,253,145,211,14,0,161,171,194,204,160,8,3,6,1,170,128,188,181,14,0,161,156,191,219,249,8,1,2,1,131,157,176,150,14,0,161,147,200,155,248,11,63,8,1,229,227,137,140,13,0,161,149,252,241,115,5,5,1,159,212,134,166,12,0,161,134,132,140,164,1,3,10,1,195,193,152,153,12,0,161,222,161,195,148,2,1,24,1,133,166,132,140,12,0,161,159,241,200,168,2,3,5,1,147,200,155,248,11,0,161,254,149,155,218,7,0,64,5,192,252,137,204,11,0,161,139,151,195,245,8,6,1,161,139,151,195,245,8,8,2,168,192,252,137,204,11,0,1,121,161,192,252,137,204,11,2,1,168,192,252,137,204,11,4,1,122,0,0,0,0,102,78,233,162,1,192,211,236,177,11,0,161,245,221,232,242,6,22,8,2,147,146,137,224,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,113,161,147,146,137,224,10,112,6,19,213,209,142,213,10,0,161,237,231,215,147,4,0,1,39,0,128,159,198,124,8,6,54,76,70,72,66,54,1,40,0,213,209,142,213,10,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,60,204,6,33,0,213,209,142,213,10,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,213,209,142,213,10,1,4,100,97,116,97,1,33,0,213,209,142,213,10,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,213,209,142,213,10,0,1,168,213,209,142,213,10,3,1,122,0,0,0,0,0,0,0,6,168,213,209,142,213,10,4,1,119,11,97,112,112,102,108,111,119,121,46,105,111,168,213,209,142,213,10,5,1,122,0,0,0,0,102,60,204,7,161,213,209,142,213,10,6,1,33,0,128,159,198,124,8,6,106,87,101,95,116,54,1,0,8,161,213,209,142,213,10,10,1,0,7,161,213,209,142,213,10,20,1,0,7,161,213,209,142,213,10,28,1,0,7,1,252,175,185,165,10,0,161,157,218,228,199,8,1,2,1,252,249,181,162,10,0,161,135,190,197,222,2,11,6,1,234,188,148,129,10,0,161,131,157,176,150,14,7,10,1,243,149,144,209,9,0,161,151,148,151,141,4,7,19,4,171,231,189,153,9,0,161,235,147,219,255,2,26,1,0,4,161,171,231,189,153,9,0,1,0,5,1,156,191,219,249,8,0,161,238,201,163,245,7,15,2,6,139,151,195,245,8,0,33,0,128,159,198,124,1,36,49,101,100,98,98,102,101,100,45,101,52,51,54,45,53,98,55,51,45,56,49,98,101,45,56,54,98,55,98,50,57,57,49,102,98,49,1,161,186,193,182,130,15,10,2,168,139,151,195,245,8,0,1,119,4,240,159,143,175,161,139,151,195,245,8,2,2,33,0,128,159,198,124,1,36,57,97,53,50,49,56,100,102,45,53,99,54,57,45,53,50,99,54,45,56,102,48,49,45,48,52,102,51,50,52,56,51,49,100,53,51,1,161,139,151,195,245,8,5,2,1,157,218,228,199,8,0,161,225,218,138,252,4,1,2,1,188,146,237,189,8,0,161,230,232,236,161,15,5,4,1,171,194,204,160,8,0,161,195,193,152,153,12,23,4,1,243,186,209,152,8,0,161,167,253,145,211,14,5,2,1,153,186,129,131,8,0,161,243,149,144,209,9,18,19,1,238,201,163,245,7,0,161,195,136,140,158,7,3,16,1,254,149,155,218,7,0,161,153,186,129,131,8,18,1,1,195,136,140,158,7,0,161,188,146,237,189,8,3,4,1,245,221,232,242,6,0,161,170,128,188,181,14,1,23,1,252,139,187,215,6,0,161,229,227,137,140,13,4,8,1,253,240,216,178,6,0,161,134,225,192,253,1,38,11,3,180,238,233,229,5,0,161,147,146,137,224,10,112,1,161,147,146,137,224,10,118,15,161,180,238,233,229,5,15,4,2,185,193,252,188,5,0,161,133,166,132,140,12,4,6,168,185,193,252,188,5,5,1,122,0,0,0,0,102,88,25,34,1,225,218,138,252,4,0,161,129,245,221,210,4,5,2,1,129,245,221,210,4,0,161,252,139,187,215,6,7,6,1,175,245,211,160,4,0,161,250,139,140,49,23,2,6,237,231,215,147,4,0,161,128,159,198,124,9,1,39,0,128,159,198,124,8,6,70,114,115,115,74,100,1,40,0,237,231,215,147,4,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,241,40,0,237,231,215,147,4,1,4,100,97,116,97,1,119,4,120,90,48,51,40,0,237,231,215,147,4,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,237,231,215,147,4,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,48,177,241,1,151,148,151,141,4,0,161,192,211,236,177,11,7,8,1,230,189,235,175,3,0,161,243,186,209,152,8,1,34,1,169,180,242,165,3,0,161,159,212,134,166,12,9,6,1,157,197,206,156,3,0,161,252,249,181,162,10,5,29,27,235,147,219,255,2,0,161,213,209,142,213,10,36,1,39,0,128,159,198,124,8,6,86,89,52,50,103,49,1,40,0,235,147,219,255,2,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,128,254,33,0,235,147,219,255,2,1,4,100,97,116,97,1,33,0,235,147,219,255,2,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,235,147,219,255,2,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,235,147,219,255,2,0,1,168,235,147,219,255,2,3,1,119,13,49,46,57,57,57,57,57,57,57,57,57,57,57,168,235,147,219,255,2,4,1,122,0,0,0,0,0,0,0,1,168,235,147,219,255,2,5,1,122,0,0,0,0,102,65,129,93,161,235,147,219,255,2,6,1,33,0,128,159,198,124,8,6,115,111,118,85,116,69,1,0,4,161,235,147,219,255,2,10,1,39,0,128,159,198,124,8,6,120,69,81,65,111,75,1,40,0,235,147,219,255,2,17,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,49,251,33,0,235,147,219,255,2,17,10,102,105,101,108,100,95,116,121,112,101,1,33,0,235,147,219,255,2,17,4,100,97,116,97,1,33,0,235,147,219,255,2,17,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,235,147,219,255,2,16,1,161,235,147,219,255,2,20,1,161,235,147,219,255,2,19,1,161,235,147,219,255,2,21,1,161,235,147,219,255,2,22,1,168,235,147,219,255,2,23,1,119,83,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,111,84,120,83,34,44,34,110,97,109,101,34,58,34,56,56,57,57,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,93,125,168,235,147,219,255,2,24,1,122,0,0,0,0,0,0,0,7,168,235,147,219,255,2,25,1,122,0,0,0,0,102,67,49,255,1,135,190,197,222,2,0,161,234,188,148,129,10,9,12,1,242,204,147,190,2,0,161,150,141,187,97,5,2,1,147,212,241,172,2,0,161,252,175,185,165,10,1,2,1,159,241,200,168,2,0,161,209,188,177,215,14,25,4,1,222,161,195,148,2,0,161,213,141,134,218,1,3,2,1,134,225,192,253,1,0,161,169,180,242,165,3,5,39,1,213,141,134,218,1,0,161,157,197,206,156,3,28,4,1,134,132,140,164,1,0,161,230,189,235,175,3,33,4,15,128,159,198,124,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,128,159,198,124,0,2,105,100,1,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,40,0,128,159,198,124,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,128,159,198,124,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,128,159,198,124,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,33,0,128,159,198,124,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,128,159,198,124,0,5,99,101,108,108,115,1,161,128,159,198,124,7,1,39,0,128,159,198,124,8,6,89,53,52,81,73,115,1,40,0,128,159,198,124,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,111,158,40,0,128,159,198,124,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,128,159,198,124,10,4,100,97,116,97,1,119,3,49,50,51,40,0,128,159,198,124,10,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,48,111,158,2,149,252,241,115,0,161,180,238,233,229,5,15,1,161,180,238,233,229,5,19,9,1,150,141,187,97,0,161,175,245,211,160,4,1,6,1,250,139,140,49,0,161,253,240,216,178,6,10,24,1,205,239,215,19,0,161,144,227,205,159,15,9,2,58,128,159,198,124,2,7,1,9,1,129,245,221,210,4,1,0,6,254,149,155,218,7,1,0,1,131,157,176,150,14,1,0,8,133,166,132,140,12,1,0,5,134,132,140,164,1,1,0,4,135,190,197,222,2,1,0,12,134,225,192,253,1,1,0,39,139,151,195,245,8,2,0,3,4,5,144,227,205,159,15,1,0,10,147,146,137,224,10,1,0,119,147,212,241,172,2,1,0,2,149,252,241,115,1,0,10,147,200,155,248,11,1,0,64,151,148,151,141,4,1,0,8,150,141,187,97,1,0,6,153,186,129,131,8,1,0,19,156,191,219,249,8,1,0,2,157,197,206,156,3,1,0,29,157,218,228,199,8,1,0,2,159,212,134,166,12,1,0,10,159,241,200,168,2,1,0,4,167,253,145,211,14,1,0,6,169,180,242,165,3,1,0,6,170,128,188,181,14,1,0,2,171,194,204,160,8,1,0,4,171,231,189,153,9,1,0,11,175,245,211,160,4,1,0,2,180,238,233,229,5,1,0,20,185,193,252,188,5,1,0,6,186,193,182,130,15,2,0,1,10,1,188,146,237,189,8,1,0,4,192,211,236,177,11,1,0,8,192,252,137,204,11,2,0,3,4,1,195,136,140,158,7,1,0,4,195,193,152,153,12,1,0,24,205,239,215,19,1,0,2,209,188,177,215,14,1,0,26,213,141,134,218,1,1,0,4,213,209,142,213,10,3,0,1,3,4,10,34,222,161,195,148,2,1,0,2,225,218,138,252,4,1,0,2,229,227,137,140,13,1,0,5,230,232,236,161,15,1,0,6,230,189,235,175,3,1,0,34,234,188,148,129,10,1,0,10,235,147,219,255,2,4,0,1,3,4,10,7,19,8,237,231,215,147,4,1,0,1,238,201,163,245,7,1,0,16,242,204,147,190,2,1,0,2,243,149,144,209,9,1,0,19,243,186,209,152,8,1,0,2,245,221,232,242,6,1,0,23,250,139,140,49,1,0,24,252,175,185,165,10,1,0,2,252,249,181,162,10,1,0,6,253,240,216,178,6,1,0,11,252,139,187,215,6,1,0,8],"318aa415-92ae-489a-a14f-a24692a2efa6":[34,16,133,247,247,224,15,0,161,204,206,244,208,8,26,1,39,0,204,206,244,208,8,9,6,70,114,115,115,74,100,1,40,0,133,247,247,224,15,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,55,40,0,133,247,247,224,15,1,4,100,97,116,97,1,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,40,0,133,247,247,224,15,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,133,247,247,224,15,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,65,140,55,161,133,247,247,224,15,0,1,39,0,204,206,244,208,8,9,6,115,111,118,85,116,69,1,40,0,133,247,247,224,15,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,65,33,0,133,247,247,224,15,7,4,100,97,116,97,1,33,0,133,247,247,224,15,7,10,102,105,101,108,100,95,116,121,112,101,1,33,0,133,247,247,224,15,7,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,133,247,247,224,15,6,1,168,133,247,247,224,15,10,1,122,0,0,0,0,0,0,0,4,168,133,247,247,224,15,9,1,119,73,57,100,48,48,56,50,51,97,45,100,57,101,50,45,52,102,98,55,45,98,100,98,54,45,99,97,102,54,101,98,99,54,99,49,50,51,44,49,99,52,102,53,52,54,57,45,54,101,49,49,45,52,55,48,51,45,57,48,56,54,45,101,98,98,50,51,57,49,53,100,53,100,56,168,133,247,247,224,15,11,1,122,0,0,0,0,102,65,147,66,1,143,148,196,184,15,0,161,241,201,182,143,5,8,2,1,246,154,152,188,14,0,161,200,145,163,182,5,11,6,1,145,225,236,177,14,0,161,160,131,157,175,8,3,2,1,252,203,148,253,13,0,161,170,153,233,132,10,11,26,1,142,228,130,255,12,0,161,143,148,196,184,15,1,6,1,182,246,158,246,11,0,161,246,154,152,188,14,5,29,2,186,166,174,226,11,0,161,212,236,181,165,9,4,6,168,186,166,174,226,11,5,1,122,0,0,0,0,102,88,25,34,1,159,139,140,218,11,0,161,173,216,200,210,1,23,4,1,214,215,253,213,11,0,161,226,183,173,212,7,23,1,1,200,218,157,187,11,0,161,248,139,142,248,1,1,35,1,145,236,232,195,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,8,1,214,213,255,190,10,0,161,222,208,153,250,3,38,7,1,233,137,131,159,10,0,161,145,236,232,195,10,7,10,1,215,229,183,133,10,0,161,214,215,253,213,11,0,48,1,190,196,251,132,10,0,161,200,218,157,187,11,34,4,1,170,153,233,132,10,0,161,142,228,130,255,12,5,12,1,218,242,205,188,9,0,161,190,196,251,132,10,3,10,1,212,236,181,165,9,0,161,176,202,194,187,3,2,5,34,204,206,244,208,8,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,204,206,244,208,8,0,2,105,100,1,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,40,0,204,206,244,208,8,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,204,206,244,208,8,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,204,206,244,208,8,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,204,206,244,208,8,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,11,33,0,204,206,244,208,8,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,204,206,244,208,8,0,5,99,101,108,108,115,1,161,204,206,244,208,8,8,1,39,0,204,206,244,208,8,9,6,86,89,52,50,103,49,1,40,0,204,206,244,208,8,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,15,40,0,204,206,244,208,8,11,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,1,40,0,204,206,244,208,8,11,4,100,97,116,97,1,119,3,54,54,54,40,0,204,206,244,208,8,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,61,247,15,161,204,206,244,208,8,10,1,39,0,204,206,244,208,8,9,6,106,87,101,95,116,54,1,40,0,204,206,244,208,8,17,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,83,33,0,204,206,244,208,8,17,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,204,206,244,208,8,17,8,105,115,95,114,97,110,103,101,1,33,0,204,206,244,208,8,17,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,204,206,244,208,8,17,4,100,97,116,97,1,33,0,204,206,244,208,8,17,10,102,105,101,108,100,95,116,121,112,101,1,33,0,204,206,244,208,8,17,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,204,206,244,208,8,17,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,204,206,244,208,8,16,1,168,204,206,244,208,8,19,1,119,0,161,204,206,244,208,8,23,1,168,204,206,244,208,8,24,1,121,168,204,206,244,208,8,21,1,119,21,51,50,78,50,75,100,121,72,114,104,84,55,83,99,54,76,106,78,90,100,51,161,204,206,244,208,8,22,1,168,204,206,244,208,8,20,1,121,161,204,206,244,208,8,25,1,1,217,249,214,203,8,0,161,142,228,130,255,12,5,2,1,228,182,208,185,8,0,161,227,143,130,249,4,7,10,1,160,131,157,175,8,0,161,182,246,158,246,11,28,4,1,226,183,173,212,7,0,161,233,137,131,159,10,9,24,1,200,145,163,182,5,0,161,228,182,208,185,8,9,12,1,241,201,182,143,5,0,161,214,213,255,190,10,6,9,1,227,143,130,249,4,0,161,215,229,183,133,10,47,8,1,160,249,160,227,4,0,161,159,139,140,218,11,3,6,1,222,208,153,250,3,0,161,189,166,144,23,5,39,1,176,202,194,187,3,0,161,252,203,148,253,13,25,3,1,248,139,142,248,1,0,161,160,249,160,227,4,5,2,1,173,216,200,210,1,0,161,145,225,236,177,14,1,24,5,128,181,139,77,0,168,133,247,247,224,15,12,1,122,0,0,0,0,102,67,57,178,168,204,206,244,208,8,28,1,122,0,0,0,0,0,0,0,10,167,204,206,244,208,8,31,0,8,0,128,181,139,77,2,1,119,36,50,102,57,52,52,50,50,48,45,57,102,52,53,45,52,48,100,57,45,57,54,98,53,45,101,56,99,48,56,56,56,100,97,102,55,99,168,204,206,244,208,8,33,1,122,0,0,0,0,102,67,57,178,1,189,166,144,23,0,161,218,242,205,188,9,9,6,33,133,247,247,224,15,3,0,1,6,1,9,4,200,145,163,182,5,1,0,12,200,218,157,187,11,1,0,35,204,206,244,208,8,7,8,1,10,1,16,1,19,8,28,1,31,1,33,1,142,228,130,255,12,1,0,6,143,148,196,184,15,1,0,2,145,236,232,195,10,1,0,8,145,225,236,177,14,1,0,2,212,236,181,165,9,1,0,5,214,215,253,213,11,1,0,1,215,229,183,133,10,1,0,48,214,213,255,190,10,1,0,7,217,249,214,203,8,1,0,2,218,242,205,188,9,1,0,10,222,208,153,250,3,1,0,39,159,139,140,218,11,1,0,4,160,131,157,175,8,1,0,4,160,249,160,227,4,1,0,6,226,183,173,212,7,1,0,24,227,143,130,249,4,1,0,8,228,182,208,185,8,1,0,10,233,137,131,159,10,1,0,10,170,153,233,132,10,1,0,12,173,216,200,210,1,1,0,24,176,202,194,187,3,1,0,3,241,201,182,143,5,1,0,9,246,154,152,188,14,1,0,6,182,246,158,246,11,1,0,29,248,139,142,248,1,1,0,2,186,166,174,226,11,1,0,6,252,203,148,253,13,1,0,26,189,166,144,23,1,0,6,190,196,251,132,10,1,0,4],"dd6c8d13-4867-41c6-8599-b888350f52ee":[53,1,197,130,149,248,15,0,161,158,234,217,249,6,4,8,1,235,218,250,209,15,0,161,228,141,195,172,1,28,3,1,186,200,234,175,15,0,161,147,160,184,220,9,5,12,1,253,249,233,173,15,0,161,142,253,173,141,11,1,2,1,145,176,227,152,15,0,161,227,177,139,177,7,6,2,1,213,199,181,135,15,0,161,155,234,245,236,5,17,39,9,129,162,211,229,14,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,129,162,211,229,14,0,2,105,100,1,119,36,100,100,54,99,56,100,49,51,45,52,56,54,55,45,52,49,99,54,45,56,53,57,57,45,98,56,56,56,51,53,48,102,53,50,101,101,40,0,129,162,211,229,14,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,129,162,211,229,14,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,129,162,211,229,14,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,33,0,129,162,211,229,14,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,129,162,211,229,14,0,5,99,101,108,108,115,1,1,185,146,147,208,14,0,161,222,239,172,216,10,3,2,1,250,188,188,173,14,0,161,147,160,184,220,9,5,2,1,177,253,176,145,14,0,161,139,215,211,140,4,29,1,1,153,153,172,165,13,0,161,168,180,153,248,6,23,4,2,185,249,173,251,12,0,161,155,214,174,214,9,111,16,161,185,249,173,251,12,15,4,1,138,141,245,198,12,0,161,240,197,174,164,3,5,4,1,163,150,249,138,12,0,161,174,167,159,184,4,3,9,1,225,225,173,133,12,0,161,205,245,156,144,3,7,10,1,240,245,224,143,11,0,161,163,150,249,138,12,8,6,1,142,253,173,141,11,0,161,140,141,210,196,7,15,2,1,222,239,172,216,10,0,161,158,239,153,203,9,25,4,1,160,254,254,214,10,0,161,224,134,128,190,4,5,2,1,242,190,222,204,10,0,161,213,199,181,135,15,38,8,3,212,192,173,181,10,0,161,185,249,173,251,12,15,1,161,185,249,173,251,12,19,5,161,212,192,173,181,10,5,4,1,201,249,157,231,9,0,161,240,245,224,143,11,5,39,1,147,160,184,220,9,0,161,179,235,169,194,8,1,6,1,155,214,174,214,9,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,118,1,158,239,153,203,9,0,161,220,226,182,197,1,5,26,38,150,189,211,186,9,0,161,182,238,220,247,3,24,1,39,0,129,162,211,229,14,8,6,70,114,115,115,74,100,1,40,0,150,189,211,186,9,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,50,33,0,150,189,211,186,9,1,4,100,97,116,97,1,33,0,150,189,211,186,9,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,150,189,211,186,9,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,150,189,211,186,9,0,1,161,150,189,211,186,9,4,1,161,150,189,211,186,9,3,1,161,150,189,211,186,9,5,1,161,150,189,211,186,9,6,1,168,150,189,211,186,9,7,1,122,0,0,0,0,0,0,0,3,168,150,189,211,186,9,8,1,119,36,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,168,150,189,211,186,9,9,1,122,0,0,0,0,102,65,140,51,161,150,189,211,186,9,10,1,39,0,129,162,211,229,14,8,6,115,111,118,85,116,69,1,40,0,150,189,211,186,9,15,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,58,33,0,150,189,211,186,9,15,4,100,97,116,97,1,33,0,150,189,211,186,9,15,10,102,105,101,108,100,95,116,121,112,101,1,33,0,150,189,211,186,9,15,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,150,189,211,186,9,14,1,161,150,189,211,186,9,17,1,161,150,189,211,186,9,18,1,161,150,189,211,186,9,19,1,161,150,189,211,186,9,20,1,161,150,189,211,186,9,22,1,161,150,189,211,186,9,21,1,161,150,189,211,186,9,23,1,161,150,189,211,186,9,24,1,168,150,189,211,186,9,25,1,122,0,0,0,0,0,0,0,4,168,150,189,211,186,9,26,1,119,147,1,49,99,52,102,53,52,54,57,45,54,101,49,49,45,52,55,48,51,45,57,48,56,54,45,101,98,98,50,51,57,49,53,100,53,100,56,44,57,100,48,48,56,50,51,97,45,100,57,101,50,45,52,102,98,55,45,98,100,98,54,45,99,97,102,54,101,98,99,54,99,49,50,51,44,48,52,48,102,98,48,98,102,45,50,101,100,97,45,52,99,97,51,45,56,54,99,97,45,53,98,57,49,98,55,48,50,102,101,49,54,44,52,49,57,50,51,51,57,51,45,102,55,99,51,45,52,50,51,53,45,98,54,49,51,45,102,57,97,101,56,52,102,102,53,56,56,57,168,150,189,211,186,9,27,1,122,0,0,0,0,102,65,147,60,161,150,189,211,186,9,28,1,39,0,129,162,211,229,14,8,6,89,80,102,105,50,109,1,40,0,150,189,211,186,9,33,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,206,40,0,150,189,211,186,9,33,4,100,97,116,97,1,119,3,89,101,115,40,0,150,189,211,186,9,33,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,150,189,211,186,9,33,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,65,147,206,2,141,231,246,230,8,0,161,229,240,171,135,7,4,1,168,141,231,246,230,8,0,1,122,0,0,0,0,102,88,25,35,1,179,235,169,194,8,0,161,137,254,221,87,15,2,1,219,210,242,162,8,0,161,235,218,250,209,15,2,5,1,140,141,210,196,7,0,161,222,168,212,145,3,3,16,1,227,177,139,177,7,0,161,153,153,172,165,13,3,7,1,229,240,171,135,7,0,161,219,210,242,162,8,4,5,1,158,234,217,249,6,0,161,212,192,173,181,10,5,5,1,168,180,153,248,6,0,161,185,146,147,208,14,1,24,1,195,153,238,185,6,0,161,145,176,227,152,15,1,35,1,144,169,186,164,6,0,161,160,254,254,214,10,1,2,1,155,234,245,236,5,0,161,253,249,233,173,15,1,18,1,202,211,156,221,5,0,161,177,253,176,145,14,0,62,1,250,181,208,232,4,0,161,144,169,186,164,6,1,2,2,224,134,128,190,4,0,161,197,130,149,248,15,7,1,161,212,192,173,181,10,9,5,1,174,167,159,184,4,0,161,195,153,238,185,6,34,4,1,139,215,211,140,4,0,161,152,171,228,253,2,9,30,34,182,238,220,247,3,0,161,129,162,211,229,14,7,1,39,0,129,162,211,229,14,8,6,86,89,52,50,103,49,1,40,0,182,238,220,247,3,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,144,110,33,0,182,238,220,247,3,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,182,238,220,247,3,1,4,100,97,116,97,1,33,0,182,238,220,247,3,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,182,238,220,247,3,0,1,168,182,238,220,247,3,4,1,119,4,56,56,56,57,168,182,238,220,247,3,3,1,122,0,0,0,0,0,0,0,1,168,182,238,220,247,3,5,1,122,0,0,0,0,102,61,145,39,161,182,238,220,247,3,6,1,39,0,129,162,211,229,14,8,6,54,76,70,72,66,54,1,40,0,182,238,220,247,3,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,145,93,33,0,182,238,220,247,3,11,10,102,105,101,108,100,95,116,121,112,101,1,33,0,182,238,220,247,3,11,4,100,97,116,97,1,33,0,182,238,220,247,3,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,182,238,220,247,3,10,1,161,182,238,220,247,3,13,1,161,182,238,220,247,3,14,1,161,182,238,220,247,3,15,1,161,182,238,220,247,3,16,1,168,182,238,220,247,3,18,1,119,9,98,97,105,100,117,46,99,111,109,168,182,238,220,247,3,17,1,122,0,0,0,0,0,0,0,6,168,182,238,220,247,3,19,1,122,0,0,0,0,102,61,145,95,161,182,238,220,247,3,20,1,39,0,129,162,211,229,14,8,6,106,87,101,95,116,54,1,40,0,182,238,220,247,3,25,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,78,33,0,182,238,220,247,3,25,10,102,105,101,108,100,95,116,121,112,101,1,40,0,182,238,220,247,3,25,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,33,0,182,238,220,247,3,25,4,100,97,116,97,1,40,0,182,238,220,247,3,25,8,105,115,95,114,97,110,103,101,1,121,40,0,182,238,220,247,3,25,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,40,0,182,238,220,247,3,25,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,33,0,182,238,220,247,3,25,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,1,240,197,174,164,3,0,161,141,225,222,162,2,1,6,1,222,168,212,145,3,0,161,138,141,245,198,12,3,4,1,205,245,156,144,3,0,161,202,211,156,221,5,61,8,1,152,171,228,253,2,0,161,242,190,222,204,10,7,10,1,141,225,222,162,2,0,161,250,181,208,232,4,1,2,5,160,193,167,129,2,0,168,150,189,211,186,9,32,1,122,0,0,0,0,102,67,57,110,168,182,238,220,247,3,27,1,122,0,0,0,0,0,0,0,10,167,182,238,220,247,3,29,0,8,0,160,193,167,129,2,2,1,119,36,51,49,56,97,97,52,49,53,45,57,50,97,101,45,52,56,57,97,45,97,49,52,102,45,97,50,52,54,57,50,97,50,101,102,97,54,168,182,238,220,247,3,33,1,122,0,0,0,0,102,67,57,110,1,220,226,182,197,1,0,161,130,157,172,36,11,6,1,228,141,195,172,1,0,161,186,200,234,175,15,11,29,1,137,254,221,87,0,161,201,249,157,231,9,38,16,1,130,157,172,36,0,161,225,225,173,133,12,9,12,52,129,162,211,229,14,1,7,1,130,157,172,36,1,0,12,195,153,238,185,6,1,0,35,197,130,149,248,15,1,0,8,201,249,157,231,9,1,0,39,138,141,245,198,12,1,0,4,139,215,211,140,4,1,0,30,140,141,210,196,7,1,0,16,205,245,156,144,3,1,0,8,141,225,222,162,2,1,0,2,142,253,173,141,11,1,0,2,144,169,186,164,6,1,0,2,145,176,227,152,15,1,0,2,202,211,156,221,5,1,0,62,137,254,221,87,1,0,16,212,192,173,181,10,1,0,10,213,199,181,135,15,1,0,39,147,160,184,220,9,1,0,6,150,189,211,186,9,5,0,1,3,8,14,1,17,12,32,1,152,171,228,253,2,1,0,10,153,153,172,165,13,1,0,4,141,231,246,230,8,1,0,1,155,214,174,214,9,1,0,118,220,226,182,197,1,1,0,6,155,234,245,236,5,1,0,18,158,239,153,203,9,1,0,26,158,234,217,249,6,1,0,5,224,134,128,190,4,1,0,6,160,254,254,214,10,1,0,2,225,225,173,133,12,1,0,10,222,168,212,145,3,1,0,4,222,239,172,216,10,1,0,4,227,177,139,177,7,1,0,7,163,150,249,138,12,1,0,9,228,141,195,172,1,1,0,29,168,180,153,248,6,1,0,24,219,210,242,162,8,1,0,5,229,240,171,135,7,1,0,5,235,218,250,209,15,1,0,3,174,167,159,184,4,1,0,4,240,197,174,164,3,1,0,6,177,253,176,145,14,1,0,1,242,190,222,204,10,1,0,8,240,245,224,143,11,1,0,6,179,235,169,194,8,1,0,2,182,238,220,247,3,8,0,1,3,4,10,1,13,8,24,1,27,1,29,1,33,1,185,249,173,251,12,1,0,20,250,181,208,232,4,1,0,2,185,146,147,208,14,1,0,2,186,200,234,175,15,1,0,12,253,249,233,173,15,1,0,2,250,188,188,173,14,1,0,2],"0160e587-41f4-4391-abb3-d322b523edb2":[34,1,169,243,176,173,14,0,161,136,210,233,249,3,12,10,1,149,233,213,204,13,0,161,200,193,211,175,13,1,26,1,200,193,211,175,13,0,161,171,146,234,226,2,9,2,1,176,222,150,172,13,0,161,189,136,144,179,10,1,6,1,174,136,245,243,12,0,161,144,251,151,19,5,39,1,145,167,143,155,12,0,161,132,147,219,143,3,6,9,2,204,172,143,149,12,0,161,148,160,235,175,1,4,7,168,204,172,143,149,12,6,1,122,0,0,0,0,102,88,25,35,28,156,179,144,186,11,0,161,214,221,216,208,2,10,1,39,0,214,221,216,208,2,9,6,70,114,115,115,74,100,1,40,0,156,179,144,186,11,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,60,33,0,156,179,144,186,11,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,156,179,144,186,11,1,4,100,97,116,97,1,33,0,156,179,144,186,11,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,156,179,144,186,11,0,1,168,156,179,144,186,11,4,1,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,168,156,179,144,186,11,3,1,122,0,0,0,0,0,0,0,3,168,156,179,144,186,11,5,1,122,0,0,0,0,102,65,140,110,161,156,179,144,186,11,6,1,39,0,214,221,216,208,2,9,6,115,111,118,85,116,69,1,40,0,156,179,144,186,11,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,72,33,0,156,179,144,186,11,11,10,102,105,101,108,100,95,116,121,112,101,1,33,0,156,179,144,186,11,11,4,100,97,116,97,1,33,0,156,179,144,186,11,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,156,179,144,186,11,10,1,161,156,179,144,186,11,14,1,161,156,179,144,186,11,13,1,161,156,179,144,186,11,15,1,161,156,179,144,186,11,16,1,161,156,179,144,186,11,17,1,161,156,179,144,186,11,18,1,161,156,179,144,186,11,19,1,168,156,179,144,186,11,20,1,122,0,0,0,0,102,65,147,75,168,156,179,144,186,11,21,1,119,73,102,99,100,54,101,102,56,99,45,56,99,100,54,45,52,49,98,51,45,57,50,52,53,45,57,57,56,57,51,49,100,52,57,97,49,54,44,57,100,48,48,56,50,51,97,45,100,57,101,50,45,52,102,98,55,45,98,100,98,54,45,99,97,102,54,101,98,99,54,99,49,50,51,168,156,179,144,186,11,22,1,122,0,0,0,0,0,0,0,4,168,156,179,144,186,11,23,1,122,0,0,0,0,102,65,147,75,1,146,233,137,149,11,0,161,216,226,128,250,2,9,12,1,132,204,135,146,11,0,161,246,211,137,45,5,26,1,189,136,144,179,10,0,161,145,167,143,155,12,8,2,1,190,135,128,165,10,0,161,180,231,185,129,2,3,10,1,213,169,224,237,9,0,161,244,148,242,155,4,3,6,1,227,133,204,217,9,0,161,176,222,150,172,13,5,2,1,251,233,161,212,7,0,161,213,169,224,237,9,5,3,1,255,171,204,197,7,0,161,248,180,185,229,2,3,2,1,249,211,146,138,7,0,161,149,233,213,204,13,25,3,1,203,203,186,212,6,0,161,132,132,171,141,1,29,1,1,197,235,146,149,5,0,161,251,233,161,212,7,2,34,1,187,192,226,180,4,0,161,241,252,150,189,1,47,8,1,244,148,242,155,4,0,161,176,135,229,160,1,23,4,1,136,210,233,249,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,13,1,132,147,219,143,3,0,161,174,136,245,243,12,38,7,1,216,226,128,250,2,0,161,187,192,226,180,4,7,10,1,248,180,185,229,2,0,161,132,204,135,146,11,25,4,2,171,146,234,226,2,0,161,176,222,150,172,13,5,1,161,227,133,204,217,9,1,9,16,214,221,216,208,2,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,214,221,216,208,2,0,2,105,100,1,119,36,48,49,54,48,101,53,56,55,45,52,49,102,52,45,52,51,57,49,45,97,98,98,51,45,100,51,50,50,98,53,50,51,101,100,98,50,40,0,214,221,216,208,2,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,214,221,216,208,2,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,214,221,216,208,2,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,214,221,216,208,2,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,182,33,0,214,221,216,208,2,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,214,221,216,208,2,0,5,99,101,108,108,115,1,161,214,221,216,208,2,8,1,39,0,214,221,216,208,2,9,6,86,89,52,50,103,49,1,40,0,214,221,216,208,2,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,198,40,0,214,221,216,208,2,11,4,100,97,116,97,1,119,3,56,56,56,40,0,214,221,216,208,2,11,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,1,40,0,214,221,216,208,2,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,61,247,198,1,180,231,185,129,2,0,161,197,235,146,149,5,33,4,1,241,252,150,189,1,0,161,203,203,186,212,6,0,48,1,148,160,235,175,1,0,161,249,211,146,138,7,2,5,1,176,135,229,160,1,0,161,255,171,204,197,7,1,24,1,132,132,171,141,1,0,161,169,243,176,173,14,9,30,1,246,211,137,45,0,161,146,233,137,149,11,11,6,1,144,251,151,19,0,161,190,135,128,165,10,9,6,34,132,132,171,141,1,1,0,30,132,204,135,146,11,1,0,26,197,235,146,149,5,1,0,34,132,147,219,143,3,1,0,7,136,210,233,249,3,1,0,13,200,193,211,175,13,1,0,2,203,203,186,212,6,1,0,1,204,172,143,149,12,1,0,7,144,251,151,19,1,0,6,145,167,143,155,12,1,0,9,146,233,137,149,11,1,0,12,148,160,235,175,1,1,0,5,213,169,224,237,9,1,0,6,149,233,213,204,13,1,0,26,214,221,216,208,2,2,8,1,10,1,216,226,128,250,2,1,0,10,156,179,144,186,11,4,0,1,3,4,10,1,13,11,227,133,204,217,9,1,0,2,169,243,176,173,14,1,0,10,171,146,234,226,2,1,0,10,174,136,245,243,12,1,0,39,176,222,150,172,13,1,0,6,176,135,229,160,1,1,0,24,241,252,150,189,1,1,0,48,244,148,242,155,4,1,0,4,180,231,185,129,2,1,0,4,246,211,137,45,1,0,6,248,180,185,229,2,1,0,4,249,211,146,138,7,1,0,3,187,192,226,180,4,1,0,8,251,233,161,212,7,1,0,3,189,136,144,179,10,1,0,2,190,135,128,165,10,1,0,10,255,171,204,197,7,1,0,2],"1cb91fa2-638d-40d6-a7c4-394f0d8b1913":[36,1,238,137,157,247,15,0,161,221,232,159,196,3,14,29,1,203,245,161,202,15,0,161,188,205,187,245,3,38,7,1,187,170,199,190,15,0,161,167,237,232,169,3,9,2,1,170,171,213,133,15,0,161,183,186,132,201,5,8,6,1,201,227,232,191,14,0,161,181,253,171,218,8,3,2,54,176,143,254,225,13,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,176,143,254,225,13,0,2,105,100,1,119,36,49,99,98,57,49,102,97,50,45,54,51,56,100,45,52,48,100,54,45,97,55,99,52,45,51,57,52,102,48,100,56,98,49,57,49,51,40,0,176,143,254,225,13,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,176,143,254,225,13,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,176,143,254,225,13,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,176,143,254,225,13,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,145,9,33,0,176,143,254,225,13,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,176,143,254,225,13,0,5,99,101,108,108,115,1,161,176,143,254,225,13,8,1,39,0,176,143,254,225,13,9,6,86,89,52,50,103,49,1,40,0,176,143,254,225,13,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,208,16,33,0,176,143,254,225,13,11,10,102,105,101,108,100,95,116,121,112,101,1,33,0,176,143,254,225,13,11,4,100,97,116,97,1,33,0,176,143,254,225,13,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,176,143,254,225,13,10,1,161,176,143,254,225,13,13,1,161,176,143,254,225,13,14,1,161,176,143,254,225,13,15,1,161,176,143,254,225,13,16,1,39,0,176,143,254,225,13,9,6,106,87,101,95,116,54,1,40,0,176,143,254,225,13,21,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,108,33,0,176,143,254,225,13,21,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,176,143,254,225,13,21,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,176,143,254,225,13,21,10,102,105,101,108,100,95,116,121,112,101,1,33,0,176,143,254,225,13,21,8,105,115,95,114,97,110,103,101,1,33,0,176,143,254,225,13,21,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,176,143,254,225,13,21,4,100,97,116,97,1,33,0,176,143,254,225,13,21,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,176,143,254,225,13,20,1,161,176,143,254,225,13,28,1,161,176,143,254,225,13,25,1,161,176,143,254,225,13,23,1,161,176,143,254,225,13,24,1,161,176,143,254,225,13,26,1,161,176,143,254,225,13,27,1,161,176,143,254,225,13,29,1,161,176,143,254,225,13,30,1,161,176,143,254,225,13,32,1,161,176,143,254,225,13,33,1,161,176,143,254,225,13,31,1,161,176,143,254,225,13,34,1,161,176,143,254,225,13,35,1,161,176,143,254,225,13,36,1,161,176,143,254,225,13,37,1,161,176,143,254,225,13,38,1,168,176,143,254,225,13,39,1,122,0,0,0,0,0,0,0,2,168,176,143,254,225,13,41,1,119,10,49,55,49,54,50,48,49,48,55,48,168,176,143,254,225,13,42,1,121,168,176,143,254,225,13,43,1,120,168,176,143,254,225,13,44,1,119,0,168,176,143,254,225,13,40,1,119,10,49,55,49,54,53,52,54,54,55,48,168,176,143,254,225,13,45,1,122,0,0,0,0,102,61,247,110,1,130,240,184,196,13,0,161,178,163,133,192,10,8,2,1,153,175,162,242,12,0,161,252,192,199,162,9,2,5,1,153,220,165,207,12,0,161,249,139,227,143,7,23,4,1,166,194,251,203,12,0,161,215,220,249,203,2,5,2,1,152,210,190,161,12,0,161,190,172,167,219,5,7,10,6,175,192,222,199,11,0,161,176,143,254,225,13,46,1,39,0,176,143,254,225,13,9,6,89,80,102,105,50,109,1,40,0,175,192,222,199,11,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,250,101,40,0,175,192,222,199,11,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,175,192,222,199,11,1,4,100,97,116,97,1,119,3,89,101,115,40,0,175,192,222,199,11,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,61,250,101,1,184,246,254,146,11,0,161,182,191,217,143,3,5,12,1,215,244,255,242,10,0,161,187,170,199,190,15,1,24,1,178,163,133,192,10,0,161,203,245,161,202,15,6,9,1,170,181,130,222,9,0,161,168,209,143,134,8,0,48,1,252,192,199,162,9,0,161,215,244,255,242,10,23,3,1,185,162,192,153,9,0,161,179,139,229,135,7,11,6,1,181,253,171,218,8,0,161,155,177,225,165,7,25,4,1,168,209,143,134,8,0,161,238,137,157,247,15,28,1,1,155,177,225,165,7,0,161,185,162,192,153,9,5,26,1,249,139,227,143,7,0,161,201,227,232,191,14,1,24,1,179,139,229,135,7,0,161,152,210,190,161,12,9,12,1,234,179,204,154,6,0,161,208,165,135,137,5,5,2,1,190,172,167,219,5,0,161,170,181,130,222,9,47,8,1,183,186,132,201,5,0,161,224,212,129,14,3,9,1,208,165,135,137,5,0,161,153,220,165,207,12,3,6,1,244,213,208,134,5,0,161,234,179,204,154,6,1,34,1,188,205,187,245,3,0,161,170,171,213,133,15,5,39,20,130,147,164,198,3,0,161,175,192,222,199,11,0,1,168,176,143,254,225,13,18,1,119,9,48,46,53,54,54,54,54,54,54,168,176,143,254,225,13,17,1,122,0,0,0,0,0,0,0,1,168,176,143,254,225,13,19,1,122,0,0,0,0,102,65,130,1,161,130,147,164,198,3,0,1,39,0,176,143,254,225,13,9,6,70,114,115,115,74,100,1,40,0,130,147,164,198,3,5,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,53,40,0,130,147,164,198,3,5,4,100,97,116,97,1,119,4,120,90,48,51,40,0,130,147,164,198,3,5,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,130,147,164,198,3,5,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,65,140,53,161,130,147,164,198,3,4,1,39,0,176,143,254,225,13,9,6,115,111,118,85,116,69,1,40,0,130,147,164,198,3,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,63,33,0,130,147,164,198,3,11,4,100,97,116,97,1,33,0,130,147,164,198,3,11,10,102,105,101,108,100,95,116,121,112,101,1,33,0,130,147,164,198,3,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,168,130,147,164,198,3,10,1,122,0,0,0,0,102,65,147,221,168,130,147,164,198,3,14,1,122,0,0,0,0,0,0,0,4,168,130,147,164,198,3,13,1,119,73,56,51,51,50,99,52,56,51,45,102,56,57,99,45,52,48,53,55,45,57,101,99,57,45,101,50,53,53,56,54,53,48,52,52,51,56,44,48,52,48,102,98,48,98,102,45,50,101,100,97,45,52,99,97,51,45,56,54,99,97,45,53,98,57,49,98,55,48,50,102,101,49,54,168,130,147,164,198,3,15,1,122,0,0,0,0,102,65,147,221,1,221,232,159,196,3,0,161,184,246,254,146,11,11,15,2,167,237,232,169,3,0,161,215,220,249,203,2,5,1,161,166,194,251,203,12,1,9,1,182,191,217,143,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,6,1,215,220,249,203,2,0,161,130,240,184,196,13,1,6,2,235,238,250,238,1,0,161,153,175,162,242,12,4,6,168,235,238,250,238,1,5,1,122,0,0,0,0,102,88,25,34,1,224,212,129,14,0,161,244,213,208,134,5,33,4,36,130,240,184,196,13,1,0,2,130,147,164,198,3,4,0,1,4,1,10,1,13,3,201,227,232,191,14,1,0,2,203,245,161,202,15,1,0,7,208,165,135,137,5,1,0,6,215,220,249,203,2,1,0,6,152,210,190,161,12,1,0,10,153,220,165,207,12,1,0,4,215,244,255,242,10,1,0,24,155,177,225,165,7,1,0,26,153,175,162,242,12,1,0,5,221,232,159,196,3,1,0,15,224,212,129,14,1,0,4,166,194,251,203,12,1,0,2,167,237,232,169,3,1,0,10,168,209,143,134,8,1,0,1,170,181,130,222,9,1,0,48,234,179,204,154,6,1,0,2,170,171,213,133,15,1,0,6,235,238,250,238,1,1,0,6,238,137,157,247,15,1,0,29,175,192,222,199,11,1,0,1,176,143,254,225,13,4,8,1,10,1,13,8,23,24,178,163,133,192,10,1,0,9,179,139,229,135,7,1,0,12,244,213,208,134,5,1,0,34,181,253,171,218,8,1,0,4,182,191,217,143,3,1,0,6,183,186,132,201,5,1,0,9,184,246,254,146,11,1,0,12,249,139,227,143,7,1,0,24,185,162,192,153,9,1,0,6,187,170,199,190,15,1,0,2,188,205,187,245,3,1,0,39,252,192,199,162,9,1,0,3,190,172,167,219,5,1,0,8],"3ccd17e0-d78b-44e2-afd1-1bf7cc49cb56":[34,1,206,233,228,195,15,0,161,170,189,188,208,5,5,2,1,253,134,198,190,15,0,161,193,228,168,220,13,3,2,1,153,141,151,158,15,0,161,222,235,157,159,14,8,6,1,148,232,153,164,14,0,161,136,255,156,248,4,24,3,1,222,235,157,159,14,0,161,252,145,253,197,3,3,9,1,182,194,169,138,14,0,161,169,138,231,172,3,5,2,1,193,228,168,220,13,0,161,155,223,214,152,11,25,4,2,215,249,231,218,13,0,161,145,163,160,202,11,4,6,168,215,249,231,218,13,5,1,122,0,0,0,0,102,88,25,35,1,194,189,155,132,13,0,161,144,133,245,185,2,23,1,1,226,198,237,226,12,0,161,158,166,203,46,23,4,1,137,217,190,128,12,0,161,133,194,167,165,11,38,7,42,214,218,172,227,11,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,214,218,172,227,11,0,2,105,100,1,119,36,51,99,99,100,49,55,101,48,45,100,55,56,98,45,52,52,101,50,45,97,102,100,49,45,49,98,102,55,99,99,52,57,99,98,53,54,40,0,214,218,172,227,11,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,214,218,172,227,11,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,214,218,172,227,11,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,214,218,172,227,11,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,32,33,0,214,218,172,227,11,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,214,218,172,227,11,0,5,99,101,108,108,115,1,161,214,218,172,227,11,8,1,39,0,214,218,172,227,11,9,6,86,89,52,50,103,49,1,40,0,214,218,172,227,11,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,36,40,0,214,218,172,227,11,11,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,1,40,0,214,218,172,227,11,11,4,100,97,116,97,1,119,3,55,55,55,40,0,214,218,172,227,11,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,61,247,36,161,214,218,172,227,11,10,1,39,0,214,218,172,227,11,9,6,106,87,101,95,116,54,1,40,0,214,218,172,227,11,17,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,247,92,33,0,214,218,172,227,11,17,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,214,218,172,227,11,17,8,105,115,95,114,97,110,103,101,1,33,0,214,218,172,227,11,17,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,214,218,172,227,11,17,4,100,97,116,97,1,33,0,214,218,172,227,11,17,10,102,105,101,108,100,95,116,121,112,101,1,33,0,214,218,172,227,11,17,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,214,218,172,227,11,17,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,214,218,172,227,11,16,1,161,214,218,172,227,11,21,1,161,214,218,172,227,11,23,1,161,214,218,172,227,11,19,1,161,214,218,172,227,11,20,1,161,214,218,172,227,11,24,1,161,214,218,172,227,11,22,1,161,214,218,172,227,11,25,1,161,214,218,172,227,11,26,1,168,214,218,172,227,11,27,1,119,0,168,214,218,172,227,11,32,1,119,10,49,55,49,53,57,52,49,56,48,48,168,214,218,172,227,11,29,1,120,168,214,218,172,227,11,28,1,122,0,0,0,0,0,0,0,2,168,214,218,172,227,11,30,1,121,168,214,218,172,227,11,31,1,119,21,71,99,83,71,68,56,119,81,82,81,90,116,68,101,122,109,115,122,73,97,74,168,214,218,172,227,11,33,1,122,0,0,0,0,102,61,247,101,1,179,151,213,226,11,0,161,142,245,238,213,8,11,6,1,145,163,160,202,11,0,161,148,232,153,164,14,2,5,1,133,194,167,165,11,0,161,153,141,151,158,15,5,39,1,155,223,214,152,11,0,161,179,151,213,226,11,5,26,1,182,179,204,141,10,0,161,148,207,232,183,3,11,10,1,142,245,238,213,8,0,161,179,170,225,17,9,12,6,158,225,233,217,7,0,161,214,218,172,227,11,34,1,39,0,214,218,172,227,11,9,6,89,80,102,105,50,109,1,40,0,158,225,233,217,7,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,250,105,40,0,158,225,233,217,7,1,4,100,97,116,97,1,119,3,89,101,115,40,0,158,225,233,217,7,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,158,225,233,217,7,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,61,250,105,1,158,231,208,154,7,0,161,148,249,197,139,5,8,2,1,170,189,188,208,5,0,161,226,198,237,226,12,3,6,2,184,177,221,199,5,0,161,169,138,231,172,3,5,1,161,182,194,169,138,14,1,11,1,148,249,197,139,5,0,161,137,217,190,128,12,6,9,1,136,255,156,248,4,0,161,184,177,221,199,5,11,25,16,208,242,145,212,4,0,161,158,225,233,217,7,0,1,39,0,214,218,172,227,11,9,6,70,114,115,115,74,100,1,40,0,208,242,145,212,4,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,57,40,0,208,242,145,212,4,1,4,100,97,116,97,1,119,36,54,49,50,100,50,99,51,98,45,56,50,98,99,45,52,55,51,98,45,98,49,52,53,45,55,102,53,55,49,56,54,101,51,102,55,101,40,0,208,242,145,212,4,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,208,242,145,212,4,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,65,140,57,161,208,242,145,212,4,0,1,39,0,214,218,172,227,11,9,6,115,111,118,85,116,69,1,40,0,208,242,145,212,4,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,68,33,0,208,242,145,212,4,7,10,102,105,101,108,100,95,116,121,112,101,1,33,0,208,242,145,212,4,7,4,100,97,116,97,1,33,0,208,242,145,212,4,7,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,168,208,242,145,212,4,6,1,122,0,0,0,0,102,65,147,69,168,208,242,145,212,4,9,1,122,0,0,0,0,0,0,0,4,168,208,242,145,212,4,10,1,119,73,52,53,53,98,100,49,56,51,45,54,54,57,102,45,52,98,49,55,45,56,99,56,57,45,56,102,56,53,48,102,102,50,48,51,54,52,44,57,97,102,51,49,102,100,53,45,98,54,53,52,45,52,54,54,54,45,98,101,101,57,45,101,50,52,55,49,51,55,50,53,49,102,53,168,208,242,145,212,4,11,1,122,0,0,0,0,102,65,147,69,1,252,145,253,197,3,0,161,222,146,227,251,2,33,4,1,148,207,232,183,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,12,1,169,138,231,172,3,0,161,158,231,208,154,7,1,6,1,222,146,227,251,2,0,161,206,233,228,195,15,1,34,1,144,133,245,185,2,0,161,182,179,204,141,10,9,24,1,198,237,231,132,2,0,161,194,189,155,132,13,0,48,1,153,149,181,61,0,161,198,237,231,132,2,47,8,1,158,166,203,46,0,161,253,134,198,190,15,1,24,1,179,170,225,17,0,161,153,149,181,61,7,10,34,193,228,168,220,13,1,0,4,194,189,155,132,13,1,0,1,133,194,167,165,11,1,0,39,198,237,231,132,2,1,0,48,136,255,156,248,4,1,0,25,137,217,190,128,12,1,0,7,142,245,238,213,8,1,0,12,206,233,228,195,15,1,0,2,144,133,245,185,2,1,0,24,145,163,160,202,11,1,0,5,208,242,145,212,4,3,0,1,6,1,9,3,148,207,232,183,3,1,0,12,148,249,197,139,5,1,0,9,148,232,153,164,14,1,0,3,214,218,172,227,11,4,8,1,10,1,16,1,19,16,215,249,231,218,13,1,0,6,153,149,181,61,1,0,8,153,141,151,158,15,1,0,6,155,223,214,152,11,1,0,26,158,166,203,46,1,0,24,222,146,227,251,2,1,0,34,158,225,233,217,7,1,0,1,222,235,157,159,14,1,0,9,226,198,237,226,12,1,0,4,158,231,208,154,7,1,0,2,169,138,231,172,3,1,0,6,170,189,188,208,5,1,0,6,179,170,225,17,1,0,10,179,151,213,226,11,1,0,6,182,194,169,138,14,1,0,2,182,179,204,141,10,1,0,10,184,177,221,199,5,1,0,12,252,145,253,197,3,1,0,4,253,134,198,190,15,1,0,2],"4b560c2d-3f39-4086-aa3d-c2590d129850":[23,1,171,144,240,132,15,0,161,177,185,133,253,10,8,6,1,132,180,178,130,15,0,161,209,233,132,156,1,11,28,1,156,220,236,216,13,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,17,1,208,149,239,158,11,0,161,168,150,210,229,5,33,4,1,177,185,133,253,10,0,161,208,149,239,158,11,3,9,1,207,140,163,157,10,0,161,213,152,246,243,7,6,9,1,228,231,188,223,9,0,161,230,228,200,164,7,5,2,1,160,143,150,180,9,0,161,244,180,255,255,8,1,6,1,244,180,255,255,8,0,161,207,140,163,157,10,8,2,1,213,152,246,243,7,0,161,141,170,152,151,3,38,7,1,183,242,197,235,7,0,161,160,143,150,180,9,5,2,10,159,171,231,213,7,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,159,171,231,213,7,0,2,105,100,1,119,36,52,98,53,54,48,99,50,100,45,51,102,51,57,45,52,48,56,54,45,97,97,51,100,45,99,50,53,57,48,100,49,50,57,56,53,48,40,0,159,171,231,213,7,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,159,171,231,213,7,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,159,171,231,213,7,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,159,171,231,213,7,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,69,115,240,40,0,159,171,231,213,7,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,69,115,240,39,0,159,171,231,213,7,0,5,99,101,108,108,115,1,1,240,167,210,197,7,0,161,156,220,236,216,13,16,2,1,230,228,200,164,7,0,161,151,235,176,134,1,3,6,2,246,239,129,224,6,0,161,185,162,129,222,6,4,6,168,246,239,129,224,6,5,1,122,0,0,0,0,102,88,25,34,1,185,162,129,222,6,0,161,209,145,212,136,2,2,5,1,209,227,201,148,6,0,161,254,152,161,193,3,1,24,1,168,150,210,229,5,0,161,228,231,188,223,9,1,34,1,254,152,161,193,3,0,161,240,167,210,197,7,1,2,1,141,170,152,151,3,0,161,171,144,240,132,15,5,39,1,209,145,212,136,2,0,161,132,180,178,130,15,27,3,2,209,233,132,156,1,0,161,160,143,150,180,9,5,1,161,183,242,197,235,7,1,11,1,151,235,176,134,1,0,161,209,227,201,148,6,23,4,22,160,143,150,180,9,1,0,6,228,231,188,223,9,1,0,2,132,180,178,130,15,1,0,28,230,228,200,164,7,1,0,6,168,150,210,229,5,1,0,34,171,144,240,132,15,1,0,6,141,170,152,151,3,1,0,39,207,140,163,157,10,1,0,9,240,167,210,197,7,1,0,2,209,227,201,148,6,1,0,24,208,149,239,158,11,1,0,4,177,185,133,253,10,1,0,9,244,180,255,255,8,1,0,2,213,152,246,243,7,1,0,7,209,233,132,156,1,1,0,12,151,235,176,134,1,1,0,4,183,242,197,235,7,1,0,2,209,145,212,136,2,1,0,3,185,162,129,222,6,1,0,5,246,239,129,224,6,1,0,6,156,220,236,216,13,1,0,17,254,152,161,193,3,1,0,2],"88fa36b2-6d72-44de-b0df-d3b2e6d744d6":[18,1,177,156,240,229,15,0,161,147,222,174,199,5,34,4,1,224,147,154,227,15,0,161,247,228,241,157,4,5,2,1,236,146,204,211,15,0,161,177,156,240,229,15,3,10,1,254,245,134,175,14,0,161,194,254,163,142,12,1,5,1,184,249,249,169,13,0,161,245,165,251,164,5,11,25,81,221,254,206,225,12,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,221,254,206,225,12,0,2,105,100,1,119,36,56,56,102,97,51,54,98,50,45,54,100,55,50,45,52,52,100,101,45,98,48,100,102,45,100,51,98,50,101,54,100,55,52,52,100,54,40,0,221,254,206,225,12,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,221,254,206,225,12,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,221,254,206,225,12,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,221,254,206,225,12,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,60,209,33,0,221,254,206,225,12,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,221,254,206,225,12,0,5,99,101,108,108,115,1,39,0,221,254,206,225,12,9,6,70,114,115,115,74,100,1,40,0,221,254,206,225,12,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,221,254,206,225,12,10,4,100,97,116,97,1,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,161,221,254,206,225,12,8,1,39,0,221,254,206,225,12,9,6,84,102,117,121,104,84,1,40,0,221,254,206,225,12,14,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,61,9,40,0,221,254,206,225,12,14,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,221,254,206,225,12,14,4,100,97,116,97,1,119,27,229,150,157,228,186,134,229,165,189,229,164,154,233,133,146,231,157,161,229,144,167,231,157,161,229,144,167,40,0,221,254,206,225,12,14,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,75,61,9,161,221,254,206,225,12,13,1,39,0,221,254,206,225,12,9,6,52,57,85,69,86,53,1,40,0,221,254,206,225,12,20,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,61,9,33,0,221,254,206,225,12,20,4,100,97,116,97,1,33,0,221,254,206,225,12,20,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,221,254,206,225,12,20,8,105,115,95,114,97,110,103,101,1,33,0,221,254,206,225,12,20,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,221,254,206,225,12,20,10,102,105,101,108,100,95,116,121,112,101,1,33,0,221,254,206,225,12,20,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,221,254,206,225,12,20,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,221,254,206,225,12,19,1,161,221,254,206,225,12,23,1,161,221,254,206,225,12,22,1,161,221,254,206,225,12,26,1,161,221,254,206,225,12,27,1,161,221,254,206,225,12,24,1,161,221,254,206,225,12,25,1,161,221,254,206,225,12,28,1,161,221,254,206,225,12,29,1,161,221,254,206,225,12,31,1,161,221,254,206,225,12,34,1,161,221,254,206,225,12,33,1,161,221,254,206,225,12,30,1,161,221,254,206,225,12,32,1,161,221,254,206,225,12,35,1,161,221,254,206,225,12,36,1,161,221,254,206,225,12,37,1,161,221,254,206,225,12,41,1,161,221,254,206,225,12,40,1,161,221,254,206,225,12,43,1,161,221,254,206,225,12,42,1,161,221,254,206,225,12,38,1,161,221,254,206,225,12,39,1,161,221,254,206,225,12,44,1,161,221,254,206,225,12,45,1,161,221,254,206,225,12,48,1,161,221,254,206,225,12,49,1,161,221,254,206,225,12,47,1,161,221,254,206,225,12,50,1,161,221,254,206,225,12,46,1,161,221,254,206,225,12,51,1,161,221,254,206,225,12,52,1,161,221,254,206,225,12,53,1,168,221,254,206,225,12,55,1,122,0,0,0,0,0,0,0,2,168,221,254,206,225,12,56,1,119,0,168,221,254,206,225,12,59,1,121,168,221,254,206,225,12,54,1,119,0,168,221,254,206,225,12,57,1,119,10,49,55,49,54,54,51,56,56,49,52,168,221,254,206,225,12,58,1,121,168,221,254,206,225,12,60,1,122,0,0,0,0,102,75,61,9,161,221,254,206,225,12,61,1,39,0,221,254,206,225,12,9,6,120,69,81,65,111,75,1,40,0,221,254,206,225,12,70,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,61,9,40,0,221,254,206,225,12,70,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,7,40,0,221,254,206,225,12,70,4,100,97,116,97,1,119,79,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,109,100,117,67,34,44,34,110,97,109,101,34,58,34,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,93,125,40,0,221,254,206,225,12,70,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,75,61,9,168,221,254,206,225,12,69,1,122,0,0,0,0,102,75,66,138,39,0,221,254,206,225,12,9,6,89,53,52,81,73,115,1,40,0,221,254,206,225,12,76,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,66,138,40,0,221,254,206,225,12,76,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,221,254,206,225,12,76,4,100,97,116,97,1,119,9,232,129,154,232,129,154,228,188,154,40,0,221,254,206,225,12,76,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,75,66,138,1,194,254,163,142,12,0,161,184,249,249,169,13,24,2,1,169,227,206,252,11,0,161,246,235,254,152,6,12,3,1,139,151,193,189,8,0,161,211,169,247,209,2,8,2,1,195,158,149,248,6,0,161,236,146,204,211,15,9,6,1,246,235,254,152,6,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,13,1,147,222,174,199,5,0,161,169,227,206,252,11,2,35,2,245,165,251,164,5,0,161,247,228,241,157,4,5,1,161,224,147,154,227,15,1,11,1,247,228,241,157,4,0,161,139,151,193,189,8,1,6,1,143,134,194,128,4,0,161,195,158,149,248,6,5,39,1,211,169,247,209,2,0,161,168,233,136,186,2,6,9,1,168,233,136,186,2,0,161,143,134,194,128,4,38,7,2,187,223,143,158,1,0,161,254,245,134,175,14,4,6,168,187,223,143,158,1,5,1,122,0,0,0,0,102,88,25,35,18,224,147,154,227,15,1,0,2,194,254,163,142,12,1,0,2,195,158,149,248,6,1,0,6,168,233,136,186,2,1,0,7,169,227,206,252,11,1,0,3,139,151,193,189,8,1,0,2,236,146,204,211,15,1,0,10,143,134,194,128,4,1,0,39,177,156,240,229,15,1,0,4,147,222,174,199,5,1,0,35,211,169,247,209,2,1,0,9,245,165,251,164,5,1,0,12,246,235,254,152,6,1,0,13,247,228,241,157,4,1,0,6,184,249,249,169,13,1,0,25,187,223,143,158,1,1,0,6,221,254,206,225,12,5,8,1,13,1,19,1,22,40,69,1,254,245,134,175,14,1,0,5],"1047f2d0-3757-4799-bcf2-e8f97464d2b5":[56,1,136,227,164,244,15,0,161,217,223,147,169,3,9,24,1,255,191,221,240,15,0,161,194,230,250,156,15,1,6,1,250,198,240,231,15,0,161,225,252,149,175,6,15,2,1,212,143,130,218,15,0,161,134,233,204,201,4,1,5,1,203,248,239,208,15,0,161,244,182,169,180,5,5,4,1,157,234,181,165,15,0,161,181,209,194,135,15,0,65,1,194,230,250,156,15,0,161,233,156,145,228,3,25,2,1,147,237,217,153,15,0,161,203,130,173,226,5,11,26,1,181,209,194,135,15,0,161,136,227,164,244,15,23,1,1,177,145,243,240,13,0,161,233,249,213,131,12,1,25,1,238,159,247,242,12,0,161,203,248,239,208,15,3,4,1,219,150,244,224,12,0,161,250,198,240,231,15,1,2,1,131,245,207,191,12,0,161,199,180,231,144,7,7,6,1,189,250,148,144,12,0,161,177,145,243,240,13,24,4,1,233,249,213,131,12,0,161,253,161,181,242,3,3,2,16,232,166,159,250,11,0,161,201,208,178,205,1,0,1,39,0,217,152,170,150,10,8,6,70,114,115,115,74,100,1,40,0,232,166,159,250,11,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,140,47,40,0,232,166,159,250,11,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,232,166,159,250,11,1,4,100,97,116,97,1,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,40,0,232,166,159,250,11,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,65,140,47,161,232,166,159,250,11,0,1,39,0,217,152,170,150,10,8,6,115,111,118,85,116,69,1,40,0,232,166,159,250,11,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,65,147,55,33,0,232,166,159,250,11,7,10,102,105,101,108,100,95,116,121,112,101,1,33,0,232,166,159,250,11,7,4,100,97,116,97,1,33,0,232,166,159,250,11,7,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,232,166,159,250,11,6,1,168,232,166,159,250,11,10,1,119,73,50,100,54,48,51,48,99,51,45,57,55,49,101,45,52,100,52,53,45,98,53,55,48,45,100,101,57,50,102,100,101,97,100,97,101,54,44,102,99,100,54,101,102,56,99,45,56,99,100,54,45,52,49,98,51,45,57,50,52,53,45,57,57,56,57,51,49,100,52,57,97,49,54,168,232,166,159,250,11,9,1,122,0,0,0,0,0,0,0,4,168,232,166,159,250,11,11,1,122,0,0,0,0,102,65,147,55,1,196,171,189,241,11,0,161,226,147,204,180,8,1,2,1,146,223,210,202,11,0,161,219,150,244,224,12,1,68,1,226,144,128,160,11,0,161,255,191,221,240,15,5,2,5,140,145,178,142,11,0,40,0,217,152,170,150,10,1,36,53,101,48,55,56,53,50,97,45,102,98,53,100,45,53,49,97,52,45,57,98,48,97,45,98,99,100,53,99,54,52,102,57,49,53,100,1,119,6,226,152,152,239,184,143,161,227,133,179,139,3,0,2,40,0,217,152,170,150,10,1,36,54,53,50,51,98,51,50,55,45,100,100,55,49,45,53,97,53,97,45,56,100,98,56,45,102,99,100,98,97,56,100,101,48,57,97,50,1,121,161,140,145,178,142,11,2,1,168,140,145,178,142,11,4,1,122,0,0,0,0,102,78,229,130,1,201,229,189,239,10,0,161,204,184,157,221,9,7,10,9,217,152,170,150,10,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,217,152,170,150,10,0,2,105,100,1,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,40,0,217,152,170,150,10,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,217,152,170,150,10,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,217,152,170,150,10,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,138,33,0,217,152,170,150,10,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,217,152,170,150,10,0,5,99,101,108,108,115,1,1,174,133,132,239,9,0,161,252,135,150,213,5,1,2,1,204,184,157,221,9,0,161,157,234,181,165,15,64,8,1,228,231,180,195,9,0,161,228,255,253,171,9,5,5,1,228,255,253,171,9,0,161,147,193,234,210,6,15,10,66,216,221,232,222,8,0,161,217,152,170,150,10,7,1,39,0,217,152,170,150,10,8,6,54,76,70,72,66,54,1,40,0,216,221,232,222,8,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,60,201,79,33,0,216,221,232,222,8,1,4,100,97,116,97,1,33,0,216,221,232,222,8,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,216,221,232,222,8,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,216,221,232,222,8,0,1,39,0,217,152,170,150,10,8,6,89,53,52,81,73,115,1,40,0,216,221,232,222,8,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,60,201,92,33,0,216,221,232,222,8,7,10,102,105,101,108,100,95,116,121,112,101,1,33,0,216,221,232,222,8,7,4,100,97,116,97,1,33,0,216,221,232,222,8,7,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,216,221,232,222,8,6,1,161,216,221,232,222,8,9,1,161,216,221,232,222,8,10,1,161,216,221,232,222,8,11,1,161,216,221,232,222,8,12,1,161,216,221,232,222,8,13,1,161,216,221,232,222,8,14,1,161,216,221,232,222,8,15,1,161,216,221,232,222,8,16,1,161,216,221,232,222,8,18,1,161,216,221,232,222,8,17,1,161,216,221,232,222,8,19,1,161,216,221,232,222,8,20,1,161,216,221,232,222,8,21,1,161,216,221,232,222,8,22,1,161,216,221,232,222,8,23,1,161,216,221,232,222,8,24,1,161,216,221,232,222,8,25,1,161,216,221,232,222,8,26,1,161,216,221,232,222,8,27,1,161,216,221,232,222,8,28,1,168,216,221,232,222,8,30,1,122,0,0,0,0,0,0,0,0,168,216,221,232,222,8,29,1,119,72,106,115,106,115,104,100,104,100,104,98,32,115,104,115,104,115,104,104,115,104,115,104,115,106,115,106,115,106,115,106,32,117,115,105,115,105,115,106,115,106,230,128,157,230,128,157,229,167,144,32,85,231,155,190,232,174,176,229,190,151,229,176,177,232,161,140,229,147,136,229,147,136,168,216,221,232,222,8,31,1,122,0,0,0,0,102,60,201,101,161,216,221,232,222,8,32,1,161,216,221,232,222,8,4,1,161,216,221,232,222,8,3,1,161,216,221,232,222,8,5,1,161,216,221,232,222,8,36,1,161,216,221,232,222,8,38,1,161,216,221,232,222,8,37,1,161,216,221,232,222,8,39,1,161,216,221,232,222,8,40,1,168,216,221,232,222,8,42,1,122,0,0,0,0,0,0,0,6,168,216,221,232,222,8,41,1,119,6,104,97,104,97,104,97,168,216,221,232,222,8,43,1,122,0,0,0,0,102,60,204,16,161,216,221,232,222,8,44,1,39,0,217,152,170,150,10,8,6,86,89,52,50,103,49,1,40,0,216,221,232,222,8,49,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,144,108,33,0,216,221,232,222,8,49,10,102,105,101,108,100,95,116,121,112,101,1,33,0,216,221,232,222,8,49,4,100,97,116,97,1,33,0,216,221,232,222,8,49,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,216,221,232,222,8,48,1,161,216,221,232,222,8,51,1,161,216,221,232,222,8,52,1,161,216,221,232,222,8,53,1,161,216,221,232,222,8,54,1,161,216,221,232,222,8,55,1,161,216,221,232,222,8,56,1,161,216,221,232,222,8,57,1,161,216,221,232,222,8,58,1,168,216,221,232,222,8,59,1,122,0,0,0,0,0,0,0,1,168,216,221,232,222,8,60,1,119,6,54,54,54,48,48,48,168,216,221,232,222,8,61,1,122,0,0,0,0,102,61,145,64,1,226,147,204,180,8,0,161,131,245,207,191,12,5,2,1,210,134,148,169,8,0,161,242,252,231,244,2,11,6,1,249,253,252,152,8,0,161,146,223,210,202,11,67,49,1,171,244,155,131,8,0,161,249,253,252,152,8,48,9,1,155,170,193,254,7,0,161,211,253,225,214,2,33,4,1,199,180,231,144,7,0,161,228,231,180,195,9,4,8,1,147,193,234,210,6,0,161,206,133,235,151,4,111,20,1,225,252,149,175,6,0,161,238,159,247,242,12,3,16,3,149,130,129,246,5,0,161,217,152,170,150,10,7,1,33,0,217,152,170,150,10,8,6,89,53,52,81,73,115,1,0,4,2,203,130,173,226,5,0,161,255,191,221,240,15,5,1,161,226,144,128,160,11,1,11,1,252,135,150,213,5,0,161,196,171,189,241,11,1,2,7,233,143,142,198,5,0,161,149,130,129,246,5,0,1,39,0,217,152,170,150,10,8,6,106,87,101,95,116,54,1,40,0,233,143,142,198,5,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,67,54,21,39,0,233,143,142,198,5,1,4,100,97,116,97,0,8,0,233,143,142,198,5,3,1,119,36,49,48,52,55,102,50,100,48,45,51,55,53,55,45,52,55,57,57,45,98,99,102,50,45,101,56,102,57,55,52,54,52,100,50,98,53,40,0,233,143,142,198,5,1,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,10,40,0,233,143,142,198,5,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,67,54,21,1,244,182,169,180,5,0,161,174,133,132,239,9,1,6,1,211,218,202,203,4,0,161,253,204,235,17,8,6,1,134,233,204,201,4,0,161,147,237,217,153,15,25,2,1,206,133,235,151,4,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,118,1,253,161,181,242,3,0,161,180,216,208,192,2,25,4,1,233,156,145,228,3,0,161,173,195,221,80,38,26,1,217,223,147,169,3,0,161,171,244,155,131,8,8,10,2,134,177,139,145,3,0,161,212,143,130,218,15,4,7,168,134,177,139,145,3,6,1,122,0,0,0,0,102,88,25,34,4,227,133,179,139,3,0,161,232,166,159,250,11,12,1,168,201,208,178,205,1,4,1,119,3,89,101,115,168,201,208,178,205,1,3,1,122,0,0,0,0,0,0,0,5,168,201,208,178,205,1,5,1,122,0,0,0,0,102,67,57,98,1,190,184,172,134,3,0,161,189,250,148,144,12,3,7,1,242,252,231,244,2,0,161,201,229,189,239,10,9,12,1,211,253,225,214,2,0,161,143,144,136,34,1,34,1,180,216,208,192,2,0,161,210,134,148,169,8,5,26,6,201,208,178,205,1,0,161,216,221,232,222,8,62,1,39,0,217,152,170,150,10,8,6,89,80,102,105,50,109,1,40,0,201,208,178,205,1,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,61,250,104,33,0,201,208,178,205,1,1,10,102,105,101,108,100,95,116,121,112,101,1,33,0,201,208,178,205,1,1,4,100,97,116,97,1,33,0,201,208,178,205,1,1,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,1,173,195,221,80,0,161,211,218,202,203,4,5,39,1,143,144,136,34,0,161,190,184,172,134,3,6,2,1,253,204,235,17,0,161,155,170,193,254,7,3,9,56,189,250,148,144,12,1,0,4,190,184,172,134,3,1,0,7,194,230,250,156,15,1,0,2,131,245,207,191,12,1,0,6,196,171,189,241,11,1,0,2,134,233,204,201,4,1,0,2,199,180,231,144,7,1,0,8,136,227,164,244,15,1,0,24,201,229,189,239,10,1,0,10,201,208,178,205,1,2,0,1,3,3,203,248,239,208,15,1,0,4,204,184,157,221,9,1,0,8,203,130,173,226,5,1,0,12,206,133,235,151,4,1,0,118,143,144,136,34,1,0,2,140,145,178,142,11,2,1,2,4,1,134,177,139,145,3,1,0,7,146,223,210,202,11,1,0,68,147,193,234,210,6,1,0,20,210,134,148,169,8,1,0,6,211,253,225,214,2,1,0,34,211,218,202,203,4,1,0,6,147,237,217,153,15,1,0,26,212,143,130,218,15,1,0,5,217,223,147,169,3,1,0,10,217,152,170,150,10,1,7,1,219,150,244,224,12,1,0,2,155,170,193,254,7,1,0,4,157,234,181,165,15,1,0,65,216,221,232,222,8,6,0,1,3,4,9,24,36,9,48,1,51,12,149,130,129,246,5,1,0,6,225,252,149,175,6,1,0,16,226,147,204,180,8,1,0,2,226,144,128,160,11,1,0,2,228,255,253,171,9,1,0,10,228,231,180,195,9,1,0,5,227,133,179,139,3,1,0,1,232,166,159,250,11,3,0,1,6,1,9,4,233,249,213,131,12,1,0,2,233,156,145,228,3,1,0,26,171,244,155,131,8,1,0,9,233,143,142,198,5,1,0,1,173,195,221,80,1,0,39,238,159,247,242,12,1,0,4,174,133,132,239,9,1,0,2,177,145,243,240,13,1,0,25,242,252,231,244,2,1,0,12,244,182,169,180,5,1,0,6,181,209,194,135,15,1,0,1,180,216,208,192,2,1,0,26,249,253,252,152,8,1,0,49,250,198,240,231,15,1,0,2,252,135,150,213,5,1,0,2,253,204,235,17,1,0,9,253,161,181,242,3,1,0,4,255,191,221,240,15,1,0,6],"3aadcc41-4b4d-4570-a5de-06ebe3f460ec":[19,1,198,208,139,248,15,0,161,232,249,153,165,14,34,4,2,178,201,178,226,15,0,161,249,156,177,132,11,4,6,168,178,201,178,226,15,5,1,122,0,0,0,0,102,88,25,35,1,217,221,136,169,15,0,161,247,190,164,130,3,1,6,1,239,247,162,248,14,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,8,1,130,223,137,178,14,0,161,238,205,207,218,4,8,6,1,232,249,153,165,14,0,161,187,231,222,18,1,35,1,214,231,164,137,11,0,161,217,221,136,169,15,5,2,1,249,156,177,132,11,0,161,163,196,200,219,4,1,5,1,238,165,252,166,9,0,161,192,192,155,154,2,1,25,1,157,244,237,240,7,0,161,129,142,211,195,2,6,9,1,163,196,200,219,4,0,161,238,165,252,166,9,24,2,1,238,205,207,218,4,0,161,198,208,139,248,15,3,9,1,220,144,217,197,4,0,161,130,223,137,178,14,5,39,2,176,222,138,182,3,0,161,217,221,136,169,15,5,1,161,214,231,164,137,11,1,9,19,217,192,185,135,3,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,217,192,185,135,3,0,2,105,100,1,119,36,51,97,97,100,99,99,52,49,45,52,98,52,100,45,52,53,55,48,45,97,53,100,101,45,48,54,101,98,101,51,102,52,54,48,101,99,40,0,217,192,185,135,3,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,52,99,54,53,56,56,49,55,45,50,48,100,98,45,52,102,53,54,45,98,55,102,57,45,48,54,51,55,97,50,50,100,102,101,98,54,40,0,217,192,185,135,3,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,217,192,185,135,3,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,217,192,185,135,3,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,60,195,33,0,217,192,185,135,3,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,217,192,185,135,3,0,5,99,101,108,108,115,1,39,0,217,192,185,135,3,9,6,70,114,115,115,74,100,1,40,0,217,192,185,135,3,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,217,192,185,135,3,10,4,100,97,116,97,1,119,36,48,52,102,52,55,48,51,55,45,49,56,54,97,45,52,56,55,102,45,98,54,56,101,45,102,49,98,102,97,48,102,101,54,54,53,101,168,217,192,185,135,3,8,1,122,0,0,0,0,102,75,60,207,39,0,217,192,185,135,3,9,6,84,102,117,121,104,84,1,40,0,217,192,185,135,3,14,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,75,60,207,40,0,217,192,185,135,3,14,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,217,192,185,135,3,14,4,100,97,116,97,1,119,18,229,147,136,229,147,136,229,147,136,229,147,136,229,147,136,229,147,136,40,0,217,192,185,135,3,14,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,75,60,207,1,247,190,164,130,3,0,161,157,244,237,240,7,8,2,1,129,142,211,195,2,0,161,220,144,217,197,4,38,7,1,192,192,155,154,2,0,161,176,222,138,182,3,9,2,1,187,231,222,18,0,161,239,247,162,248,14,7,2,19,192,192,155,154,2,1,0,2,129,142,211,195,2,1,0,7,130,223,137,178,14,1,0,6,163,196,200,219,4,1,0,2,198,208,139,248,15,1,0,4,232,249,153,165,14,1,0,35,238,165,252,166,9,1,0,25,238,205,207,218,4,1,0,9,176,222,138,182,3,1,0,10,239,247,162,248,14,1,0,8,178,201,178,226,15,1,0,6,214,231,164,137,11,1,0,2,247,190,164,130,3,1,0,2,217,221,136,169,15,1,0,6,249,156,177,132,11,1,0,5,187,231,222,18,1,0,2,220,144,217,197,4,1,0,39,157,244,237,240,7,1,0,9,217,192,185,135,3,1,8,1]} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/rows/87bc006e-c1eb-47fd-9ac6-e39b17956369.json b/frontend/appflowy_web_app/cypress/fixtures/database/rows/87bc006e-c1eb-47fd-9ac6-e39b17956369.json new file mode 100644 index 0000000000..4eefa98010 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/rows/87bc006e-c1eb-47fd-9ac6-e39b17956369.json @@ -0,0 +1 @@ +{"9cde7c15-347c-447a-9ea1-76bc3a8d4e96":[2,10,179,237,201,251,15,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,179,237,201,251,15,0,2,105,100,1,119,36,57,99,100,101,55,99,49,53,45,51,52,55,99,45,52,52,55,97,45,57,101,97,49,45,55,54,98,99,51,97,56,100,52,101,57,54,40,0,179,237,201,251,15,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,179,237,201,251,15,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,179,237,201,251,15,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,179,237,201,251,15,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,40,0,179,237,201,251,15,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,115,63,39,0,179,237,201,251,15,0,5,99,101,108,108,115,1,2,181,140,221,245,11,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,181,140,221,245,11,4,1,122,0,0,0,0,102,97,116,211,1,181,140,221,245,11,1,0,5],"16da0f68-f414-4c59-95eb-3b45b4b61dc3":[2,38,162,144,245,250,8,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,162,144,245,250,8,0,2,105,100,1,119,36,49,54,100,97,48,102,54,56,45,102,52,49,52,45,52,99,53,57,45,57,53,101,98,45,51,98,52,53,98,52,98,54,49,100,99,51,40,0,162,144,245,250,8,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,162,144,245,250,8,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,162,144,245,250,8,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,162,144,245,250,8,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,234,33,0,162,144,245,250,8,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,162,144,245,250,8,0,5,99,101,108,108,115,1,39,0,162,144,245,250,8,9,6,77,67,57,90,97,69,1,40,0,162,144,245,250,8,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,162,144,245,250,8,10,4,100,97,116,97,1,119,3,49,50,51,39,0,162,144,245,250,8,9,6,108,73,72,113,101,57,1,40,0,162,144,245,250,8,13,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,162,144,245,250,8,13,4,100,97,116,97,1,119,3,89,101,115,39,0,162,144,245,250,8,9,6,111,121,80,121,97,117,1,40,0,162,144,245,250,8,16,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,1,40,0,162,144,245,250,8,16,4,100,97,116,97,1,119,3,54,48,49,39,0,162,144,245,250,8,9,6,53,69,90,81,65,87,1,40,0,162,144,245,250,8,19,4,100,97,116,97,1,119,4,71,102,87,50,40,0,162,144,245,250,8,19,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,161,162,144,245,250,8,8,1,39,0,162,144,245,250,8,9,6,102,116,73,53,52,121,1,40,0,162,144,245,250,8,23,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,46,40,0,162,144,245,250,8,23,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,4,40,0,162,144,245,250,8,23,4,100,97,116,97,1,119,4,104,57,106,100,40,0,162,144,245,250,8,23,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,46,161,162,144,245,250,8,22,1,39,0,162,144,245,250,8,9,6,84,79,87,83,70,104,1,40,0,162,144,245,250,8,29,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,61,33,0,162,144,245,250,8,29,4,100,97,116,97,1,33,0,162,144,245,250,8,29,10,102,105,101,108,100,95,116,121,112,101,1,33,0,162,144,245,250,8,29,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,168,162,144,245,250,8,28,1,122,0,0,0,0,102,97,116,63,168,162,144,245,250,8,31,1,119,88,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,99,55,88,104,34,44,34,110,97,109,101,34,58,34,49,49,49,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,34,99,55,88,104,34,93,125,168,162,144,245,250,8,32,1,122,0,0,0,0,0,0,0,7,168,162,144,245,250,8,33,1,122,0,0,0,0,102,97,116,63,2,161,140,129,164,8,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,161,140,129,164,8,4,1,122,0,0,0,0,102,97,116,213,2,161,140,129,164,8,1,0,5,162,144,245,250,8,4,8,1,22,1,28,1,31,3],"9e5efed0-6220-48be-8704-d8ec0166796c":[2,2,144,209,245,144,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,144,209,245,144,10,4,1,122,0,0,0,0,102,97,116,215,56,246,244,204,133,9,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,246,244,204,133,9,0,2,105,100,1,119,36,57,101,53,101,102,101,100,48,45,54,50,50,48,45,52,56,98,101,45,56,55,48,52,45,100,56,101,99,48,49,54,54,55,57,54,99,40,0,246,244,204,133,9,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,246,244,204,133,9,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,246,244,204,133,9,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,246,244,204,133,9,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,238,33,0,246,244,204,133,9,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,246,244,204,133,9,0,5,99,101,108,108,115,1,39,0,246,244,204,133,9,9,6,108,73,72,113,101,57,1,40,0,246,244,204,133,9,10,4,100,97,116,97,1,119,3,89,101,115,40,0,246,244,204,133,9,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,39,0,246,244,204,133,9,9,6,77,67,57,90,97,69,1,33,0,246,244,204,133,9,13,10,102,105,101,108,100,95,116,121,112,101,1,33,0,246,244,204,133,9,13,4,100,97,116,97,1,39,0,246,244,204,133,9,9,6,111,121,80,121,97,117,1,33,0,246,244,204,133,9,16,10,102,105,101,108,100,95,116,121,112,101,1,33,0,246,244,204,133,9,16,4,100,97,116,97,1,39,0,246,244,204,133,9,9,6,53,69,90,81,65,87,1,40,0,246,244,204,133,9,19,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,246,244,204,133,9,19,4,100,97,116,97,1,119,4,71,102,87,50,161,246,244,204,133,9,8,1,40,0,246,244,204,133,9,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,17,168,246,244,204,133,9,18,1,119,3,54,48,51,168,246,244,204,133,9,17,1,122,0,0,0,0,0,0,0,1,40,0,246,244,204,133,9,16,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,17,161,246,244,204,133,9,22,1,40,0,246,244,204,133,9,13,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,39,168,246,244,204,133,9,14,1,122,0,0,0,0,0,0,0,0,168,246,244,204,133,9,15,1,119,7,49,50,51,57,57,48,48,40,0,246,244,204,133,9,13,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,39,161,246,244,204,133,9,27,1,39,0,246,244,204,133,9,9,6,102,116,73,53,52,121,1,40,0,246,244,204,133,9,33,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,51,40,0,246,244,204,133,9,33,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,4,40,0,246,244,204,133,9,33,4,100,97,116,97,1,119,4,104,57,106,100,40,0,246,244,204,133,9,33,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,51,161,246,244,204,133,9,32,1,39,0,246,244,204,133,9,9,6,84,79,87,83,70,104,1,40,0,246,244,204,133,9,39,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,71,33,0,246,244,204,133,9,39,4,100,97,116,97,1,33,0,246,244,204,133,9,39,10,102,105,101,108,100,95,116,121,112,101,1,33,0,246,244,204,133,9,39,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,246,244,204,133,9,38,1,161,246,244,204,133,9,42,1,161,246,244,204,133,9,41,1,161,246,244,204,133,9,43,1,161,246,244,204,133,9,44,1,161,246,244,204,133,9,46,1,161,246,244,204,133,9,45,1,161,246,244,204,133,9,47,1,168,246,244,204,133,9,48,1,122,0,0,0,0,102,97,116,75,168,246,244,204,133,9,50,1,122,0,0,0,0,0,0,0,7,168,246,244,204,133,9,49,1,119,135,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,102,104,112,70,34,44,34,110,97,109,101,34,58,34,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,44,123,34,105,100,34,58,34,111,105,110,85,34,44,34,110,97,109,101,34,58,34,54,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,34,102,104,112,70,34,44,34,111,105,110,85,34,93,125,168,246,244,204,133,9,51,1,122,0,0,0,0,102,97,116,75,2,144,209,245,144,10,1,0,5,246,244,204,133,9,8,8,1,14,2,17,2,22,1,27,1,32,1,38,1,41,11],"3b5ef824-475c-4848-acff-418e259a3d53":[2,2,219,196,219,154,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,219,196,219,154,10,4,1,122,0,0,0,0,102,97,116,208,60,150,233,209,1,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,150,233,209,1,0,2,105,100,1,119,36,51,98,53,101,102,56,50,52,45,52,55,53,99,45,52,56,52,56,45,97,99,102,102,45,52,49,56,101,50,53,57,97,51,100,53,51,40,0,150,233,209,1,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,150,233,209,1,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,150,233,209,1,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,150,233,209,1,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,237,33,0,150,233,209,1,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,150,233,209,1,0,5,99,101,108,108,115,1,39,0,150,233,209,1,9,6,111,121,80,121,97,117,1,33,0,150,233,209,1,10,10,102,105,101,108,100,95,116,121,112,101,1,33,0,150,233,209,1,10,4,100,97,116,97,1,39,0,150,233,209,1,9,6,53,69,90,81,65,87,1,40,0,150,233,209,1,13,4,100,97,116,97,1,119,4,71,102,87,50,40,0,150,233,209,1,13,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,39,0,150,233,209,1,9,6,77,67,57,90,97,69,1,33,0,150,233,209,1,16,10,102,105,101,108,100,95,116,121,112,101,1,33,0,150,233,209,1,16,4,100,97,116,97,1,39,0,150,233,209,1,9,6,108,73,72,113,101,57,1,40,0,150,233,209,1,19,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,150,233,209,1,19,4,100,97,116,97,1,119,3,89,101,115,161,150,233,209,1,8,1,40,0,150,233,209,1,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,15,161,150,233,209,1,12,1,161,150,233,209,1,11,1,33,0,150,233,209,1,10,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,150,233,209,1,22,1,40,0,150,233,209,1,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,31,168,150,233,209,1,17,1,122,0,0,0,0,0,0,0,0,168,150,233,209,1,18,1,119,6,49,50,51,53,54,55,40,0,150,233,209,1,16,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,31,161,150,233,209,1,27,1,39,0,150,233,209,1,9,6,102,116,73,53,52,121,1,40,0,150,233,209,1,33,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,49,40,0,150,233,209,1,33,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,4,40,0,150,233,209,1,33,4,100,97,116,97,1,119,4,104,57,106,100,40,0,150,233,209,1,33,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,49,161,150,233,209,1,32,1,39,0,150,233,209,1,9,6,84,79,87,83,70,104,1,40,0,150,233,209,1,39,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,65,33,0,150,233,209,1,39,10,102,105,101,108,100,95,116,121,112,101,1,33,0,150,233,209,1,39,4,100,97,116,97,1,33,0,150,233,209,1,39,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,150,233,209,1,38,1,161,150,233,209,1,42,1,161,150,233,209,1,41,1,161,150,233,209,1,43,1,161,150,233,209,1,44,1,161,150,233,209,1,46,1,161,150,233,209,1,45,1,161,150,233,209,1,47,1,161,150,233,209,1,48,1,168,150,233,209,1,49,1,122,0,0,0,0,0,0,0,7,168,150,233,209,1,50,1,119,135,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,45,106,68,117,34,44,34,110,97,109,101,34,58,34,50,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,44,123,34,105,100,34,58,34,76,57,87,81,34,44,34,110,97,109,101,34,58,34,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,34,45,106,68,117,34,44,34,76,57,87,81,34,93,125,168,150,233,209,1,51,1,122,0,0,0,0,102,97,116,68,168,150,233,209,1,52,1,122,0,0,0,0,102,97,116,93,168,150,233,209,1,25,1,122,0,0,0,0,0,0,0,1,168,150,233,209,1,24,1,119,3,54,48,55,168,150,233,209,1,26,1,122,0,0,0,0,102,97,116,93,2,150,233,209,1,8,8,1,11,2,17,2,22,1,24,4,32,1,38,1,41,12,219,196,219,154,10,1,0,5],"24249689-cad4-4e53-8c5e-f9eaec9bf558":[2,10,242,202,217,154,13,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,242,202,217,154,13,0,2,105,100,1,119,36,50,52,50,52,57,54,56,57,45,99,97,100,52,45,52,101,53,51,45,56,99,53,101,45,102,57,101,97,101,99,57,98,102,53,53,56,40,0,242,202,217,154,13,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,242,202,217,154,13,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,242,202,217,154,13,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,242,202,217,154,13,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,116,208,40,0,242,202,217,154,13,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,116,208,39,0,242,202,217,154,13,0,5,99,101,108,108,115,1,2,243,240,149,193,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,243,240,149,193,10,4,1,122,0,0,0,0,102,97,116,218,1,243,240,149,193,10,1,0,5],"1111b146-4c6c-4fc6-95e1-70c246147f8f":[2,10,165,220,194,235,14,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,165,220,194,235,14,0,2,105,100,1,119,36,49,49,49,49,98,49,52,54,45,52,99,54,99,45,52,102,99,54,45,57,53,101,49,45,55,48,99,50,52,54,49,52,55,102,56,102,40,0,165,220,194,235,14,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,165,220,194,235,14,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,165,220,194,235,14,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,165,220,194,235,14,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,40,0,165,220,194,235,14,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,115,63,39,0,165,220,194,235,14,0,5,99,101,108,108,115,1,2,250,172,218,249,7,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,250,172,218,249,7,4,1,122,0,0,0,0,102,97,116,211,1,250,172,218,249,7,1,0,5],"3ec7b76c-68c9-4279-9b33-2365321eaf41":[2,10,243,212,210,152,3,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,243,212,210,152,3,0,2,105,100,1,119,36,51,101,99,55,98,55,54,99,45,54,56,99,57,45,52,50,55,57,45,57,98,51,51,45,50,51,54,53,51,50,49,101,97,102,52,49,40,0,243,212,210,152,3,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,56,55,98,99,48,48,54,101,45,99,49,101,98,45,52,55,102,100,45,57,97,99,54,45,101,51,57,98,49,55,57,53,54,51,54,57,40,0,243,212,210,152,3,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,243,212,210,152,3,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,243,212,210,152,3,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,63,40,0,243,212,210,152,3,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,97,115,63,39,0,243,212,210,152,3,0,5,99,101,108,108,115,1,2,160,128,198,202,2,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,5,168,160,128,198,202,2,4,1,122,0,0,0,0,102,97,116,210,1,160,128,198,202,2,1,0,5]} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/rows/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json b/frontend/appflowy_web_app/cypress/fixtures/database/rows/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json new file mode 100644 index 0000000000..10fc50a811 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/rows/ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d.json @@ -0,0 +1 @@ +{"208d248f-5c08-4be5-a022-e0a97c2d705e":[16,1,162,212,253,234,14,0,161,166,231,212,218,8,3,39,1,245,198,128,205,14,0,161,233,140,128,164,8,5,2,1,165,222,139,132,12,0,161,128,181,233,166,8,1,7,1,179,227,145,238,11,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,10,2,213,228,161,169,9,0,161,233,140,128,164,8,5,1,161,245,198,128,205,14,1,9,2,185,222,141,169,9,0,161,140,225,231,182,6,2,4,168,185,222,141,169,9,3,1,122,0,0,0,0,102,88,52,85,1,138,182,251,229,8,0,161,162,212,253,234,14,38,7,1,166,231,212,218,8,0,161,165,222,139,132,12,6,4,1,128,181,233,166,8,0,161,179,227,145,238,11,9,2,1,233,140,128,164,8,0,161,221,230,177,144,4,1,6,1,239,245,240,149,8,0,161,157,238,145,201,3,1,2,1,140,225,231,182,6,0,161,239,245,240,149,8,1,3,1,246,148,237,174,6,0,161,138,182,251,229,8,6,5,16,221,174,135,220,5,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,221,174,135,220,5,0,2,105,100,1,119,36,50,48,56,100,50,52,56,102,45,53,99,48,56,45,52,98,101,53,45,97,48,50,50,45,101,48,97,57,55,99,50,100,55,48,53,101,40,0,221,174,135,220,5,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,97,100,55,100,99,52,53,98,45,52,52,98,53,45,52,57,56,102,45,98,102,97,50,45,48,102,52,51,98,102,48,53,99,99,48,100,40,0,221,174,135,220,5,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,221,174,135,220,5,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,221,174,135,220,5,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,162,40,0,221,174,135,220,5,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,39,162,39,0,221,174,135,220,5,0,5,99,101,108,108,115,1,39,0,221,174,135,220,5,9,6,121,52,52,50,48,119,1,40,0,221,174,135,220,5,10,4,100,97,116,97,1,119,4,117,76,117,51,40,0,221,174,135,220,5,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,39,0,221,174,135,220,5,9,6,51,111,45,90,115,109,1,40,0,221,174,135,220,5,13,4,100,97,116,97,1,119,6,67,97,114,100,32,49,40,0,221,174,135,220,5,13,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,1,221,230,177,144,4,0,161,246,148,237,174,6,4,2,1,157,238,145,201,3,0,161,213,228,161,169,9,9,2,15,128,181,233,166,8,1,0,2,162,212,253,234,14,1,0,39,165,222,139,132,12,1,0,7,166,231,212,218,8,1,0,4,233,140,128,164,8,1,0,6,138,182,251,229,8,1,0,7,140,225,231,182,6,1,0,3,239,245,240,149,8,1,0,2,179,227,145,238,11,1,0,10,245,198,128,205,14,1,0,2,246,148,237,174,6,1,0,5,213,228,161,169,9,1,0,10,185,222,141,169,9,1,0,4,221,230,177,144,4,1,0,2,157,238,145,201,3,1,0,2]} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/database/rows/ce267d12-3b61-4ebb-bb03-d65272f5f817.json b/frontend/appflowy_web_app/cypress/fixtures/database/rows/ce267d12-3b61-4ebb-bb03-d65272f5f817.json new file mode 100644 index 0000000000..9820d03b24 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/database/rows/ce267d12-3b61-4ebb-bb03-d65272f5f817.json @@ -0,0 +1 @@ +{"a00ecf78-a823-43f1-b542-ed071394a717":[14,1,235,137,137,244,15,0,161,252,139,206,213,10,1,2,1,133,172,162,242,15,0,161,137,192,210,179,15,1,2,2,186,176,205,207,15,0,161,196,230,218,150,10,3,2,168,186,176,205,207,15,1,1,122,0,0,0,0,102,88,31,89,1,137,192,210,179,15,0,161,235,137,137,244,15,1,2,1,245,193,213,231,13,0,161,153,225,207,224,1,1,2,1,246,177,194,222,13,0,161,133,172,162,242,15,1,6,1,205,151,244,151,13,0,161,246,177,194,222,13,5,12,23,239,227,232,242,10,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,239,227,232,242,10,0,2,105,100,1,119,36,97,48,48,101,99,102,55,56,45,97,56,50,51,45,52,51,102,49,45,98,53,52,50,45,101,100,48,55,49,51,57,52,97,55,49,55,40,0,239,227,232,242,10,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,239,227,232,242,10,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,239,227,232,242,10,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,239,227,232,242,10,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,121,252,33,0,239,227,232,242,10,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,239,227,232,242,10,0,5,99,101,108,108,115,1,39,0,239,227,232,242,10,9,6,55,85,107,117,54,82,1,40,0,239,227,232,242,10,10,4,100,97,116,97,1,119,10,49,55,49,54,51,48,55,50,48,48,40,0,239,227,232,242,10,10,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,40,0,239,227,232,242,10,10,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,40,0,239,227,232,242,10,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,239,227,232,242,10,10,8,105,115,95,114,97,110,103,101,1,121,40,0,239,227,232,242,10,10,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,168,239,227,232,242,10,8,1,122,0,0,0,0,102,77,124,84,39,0,239,227,232,242,10,9,6,72,95,74,113,85,76,1,40,0,239,227,232,242,10,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,124,84,40,0,239,227,232,242,10,18,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,239,227,232,242,10,18,4,100,97,116,97,1,119,5,49,49,49,49,49,40,0,239,227,232,242,10,18,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,124,84,1,252,139,206,213,10,0,161,155,206,144,191,3,37,2,1,222,138,222,196,10,0,161,246,177,194,222,13,5,2,1,196,230,218,150,10,0,161,245,193,213,231,13,1,4,1,239,171,159,202,8,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,15,1,155,206,144,191,3,0,161,239,171,159,202,8,14,38,1,153,225,207,224,1,0,161,205,151,244,151,13,11,2,14,235,137,137,244,15,1,0,2,205,151,244,151,13,1,0,12,239,171,159,202,8,1,0,15,196,230,218,150,10,1,0,4,245,193,213,231,13,1,0,2,246,177,194,222,13,1,0,6,133,172,162,242,15,1,0,2,137,192,210,179,15,1,0,2,186,176,205,207,15,1,0,2,153,225,207,224,1,1,0,2,155,206,144,191,3,1,0,38,252,139,206,213,10,1,0,2,222,138,222,196,10,1,0,2,239,227,232,242,10,1,8,1],"a73674ae-3301-45a3-b801-3f12e6fcb566":[16,1,216,136,201,234,15,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,14,8,151,154,187,181,15,0,161,167,192,205,202,8,78,1,161,167,192,205,202,8,54,1,161,167,192,205,202,8,53,1,161,167,192,205,202,8,55,1,168,151,154,187,181,15,0,1,122,0,0,0,0,102,80,8,134,168,151,154,187,181,15,2,1,119,2,104,105,168,151,154,187,181,15,1,1,122,0,0,0,0,0,0,0,0,168,151,154,187,181,15,3,1,122,0,0,0,0,102,80,8,134,1,225,161,205,165,15,0,161,236,244,246,235,2,1,4,1,232,241,163,254,12,0,161,216,136,201,234,15,13,28,1,243,242,150,150,12,0,161,198,181,227,192,1,1,2,1,170,212,149,201,11,0,161,232,241,163,254,12,27,39,1,203,244,164,187,11,0,161,189,165,195,186,4,11,6,2,225,242,132,222,9,0,161,225,161,205,165,15,3,2,168,225,242,132,222,9,1,1,122,0,0,0,0,102,88,31,91,82,167,192,205,202,8,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,167,192,205,202,8,0,2,105,100,1,119,36,97,55,51,54,55,52,97,101,45,51,51,48,49,45,52,53,97,51,45,98,56,48,49,45,51,102,49,50,101,54,102,99,98,53,54,54,40,0,167,192,205,202,8,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,167,192,205,202,8,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,167,192,205,202,8,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,167,192,205,202,8,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,88,135,33,0,167,192,205,202,8,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,167,192,205,202,8,0,5,99,101,108,108,115,1,39,0,167,192,205,202,8,9,6,55,85,107,117,54,82,1,33,0,167,192,205,202,8,10,10,102,105,101,108,100,95,116,121,112,101,1,33,0,167,192,205,202,8,10,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,167,192,205,202,8,10,8,105,115,95,114,97,110,103,101,1,33,0,167,192,205,202,8,10,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,167,192,205,202,8,10,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,167,192,205,202,8,10,4,100,97,116,97,1,161,167,192,205,202,8,8,1,39,0,167,192,205,202,8,9,6,95,82,45,112,104,105,1,40,0,167,192,205,202,8,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,88,139,40,0,167,192,205,202,8,18,4,100,97,116,97,1,119,4,73,73,66,100,40,0,167,192,205,202,8,18,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,4,40,0,167,192,205,202,8,18,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,88,139,161,167,192,205,202,8,17,1,40,0,167,192,205,202,8,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,88,142,168,167,192,205,202,8,13,1,121,168,167,192,205,202,8,11,1,122,0,0,0,0,0,0,0,2,168,167,192,205,202,8,15,1,119,0,168,167,192,205,202,8,14,1,119,0,168,167,192,205,202,8,16,1,119,10,49,55,49,54,54,48,52,49,55,52,168,167,192,205,202,8,12,1,121,40,0,167,192,205,202,8,10,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,88,142,161,167,192,205,202,8,23,1,39,0,167,192,205,202,8,9,6,71,115,66,65,97,76,1,40,0,167,192,205,202,8,33,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,88,150,33,0,167,192,205,202,8,33,8,105,115,95,114,97,110,103,101,1,33,0,167,192,205,202,8,33,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,167,192,205,202,8,33,10,102,105,101,108,100,95,116,121,112,101,1,33,0,167,192,205,202,8,33,4,100,97,116,97,1,33,0,167,192,205,202,8,33,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,167,192,205,202,8,33,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,167,192,205,202,8,33,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,167,192,205,202,8,32,1,168,167,192,205,202,8,39,1,119,0,168,167,192,205,202,8,38,1,119,10,49,55,49,55,49,50,50,53,56,51,168,167,192,205,202,8,37,1,122,0,0,0,0,0,0,0,2,168,167,192,205,202,8,40,1,121,168,167,192,205,202,8,35,1,121,168,167,192,205,202,8,36,1,119,0,168,167,192,205,202,8,41,1,122,0,0,0,0,102,77,88,151,161,167,192,205,202,8,42,1,39,0,167,192,205,202,8,9,6,72,95,74,113,85,76,1,40,0,167,192,205,202,8,51,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,75,33,0,167,192,205,202,8,51,4,100,97,116,97,1,33,0,167,192,205,202,8,51,10,102,105,101,108,100,95,116,121,112,101,1,33,0,167,192,205,202,8,51,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,167,192,205,202,8,50,1,39,0,167,192,205,202,8,9,6,99,78,53,98,120,74,1,40,0,167,192,205,202,8,57,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,89,40,0,167,192,205,202,8,57,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,6,40,0,167,192,205,202,8,57,4,100,97,116,97,1,119,3,49,50,51,40,0,167,192,205,202,8,57,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,89,161,167,192,205,202,8,56,1,39,0,167,192,205,202,8,9,6,71,79,80,107,116,118,1,40,0,167,192,205,202,8,63,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,117,40,0,167,192,205,202,8,63,4,100,97,116,97,1,119,4,89,101,75,100,40,0,167,192,205,202,8,63,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,167,192,205,202,8,63,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,117,161,167,192,205,202,8,62,1,39,0,167,192,205,202,8,9,6,112,70,120,57,67,45,1,40,0,167,192,205,202,8,69,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,142,33,0,167,192,205,202,8,69,10,102,105,101,108,100,95,116,121,112,101,1,33,0,167,192,205,202,8,69,4,100,97,116,97,1,33,0,167,192,205,202,8,69,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,167,192,205,202,8,68,1,161,167,192,205,202,8,71,1,161,167,192,205,202,8,72,1,161,167,192,205,202,8,73,1,161,167,192,205,202,8,74,1,168,167,192,205,202,8,75,1,122,0,0,0,0,0,0,0,7,168,167,192,205,202,8,76,1,119,134,1,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,99,72,80,113,34,44,34,110,97,109,101,34,58,34,51,51,51,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,44,123,34,105,100,34,58,34,74,106,52,74,34,44,34,110,97,109,101,34,58,34,51,51,51,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,34,99,72,80,113,34,93,125,168,167,192,205,202,8,77,1,122,0,0,0,0,102,77,165,145,1,170,185,193,131,8,0,161,210,149,158,230,4,5,2,1,194,218,151,236,5,0,161,170,212,149,201,11,38,2,1,210,149,158,230,4,0,161,143,148,251,251,1,1,6,2,189,165,195,186,4,0,161,210,149,158,230,4,5,1,161,170,185,193,131,8,1,11,1,236,244,246,235,2,0,161,203,244,164,187,11,5,2,1,143,148,251,251,1,0,161,243,242,150,150,12,1,2,1,198,181,227,192,1,0,161,194,218,151,236,5,1,2,16,225,161,205,165,15,1,0,4,194,218,151,236,5,1,0,2,225,242,132,222,9,1,0,2,198,181,227,192,1,1,0,2,167,192,205,202,8,10,8,1,11,7,23,1,32,1,35,8,50,1,53,4,62,1,68,1,71,8,232,241,163,254,12,1,0,28,170,212,149,201,11,1,0,39,170,185,193,131,8,1,0,2,236,244,246,235,2,1,0,2,203,244,164,187,11,1,0,6,143,148,251,251,1,1,0,2,210,149,158,230,4,1,0,6,243,242,150,150,12,1,0,2,151,154,187,181,15,1,0,4,216,136,201,234,15,1,0,14,189,165,195,186,4,1,0,12],"51cf0906-ad46-4dae-a3b9-2e003f8368c1":[15,1,196,176,146,143,13,0,161,211,131,137,205,5,5,12,1,175,214,229,215,11,0,161,164,251,162,159,11,1,4,1,167,131,238,183,11,0,161,218,233,217,251,6,1,2,1,164,251,162,159,11,0,161,135,135,213,129,7,1,2,1,252,183,246,136,10,0,161,211,131,137,205,5,5,2,1,184,218,170,237,8,0,161,226,213,154,133,6,7,16,1,253,213,204,144,8,0,161,254,207,234,185,3,1,2,1,135,135,213,129,7,0,161,196,176,146,143,13,11,2,1,218,233,217,251,6,0,161,213,133,230,230,2,38,2,1,226,213,154,133,6,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,8,1,211,131,137,205,5,0,161,253,213,204,144,8,1,6,2,205,251,166,251,4,0,161,175,214,229,215,11,3,2,168,205,251,166,251,4,1,1,122,0,0,0,0,102,88,31,89,30,239,237,241,245,4,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,239,237,241,245,4,0,2,105,100,1,119,36,53,49,99,102,48,57,48,54,45,97,100,52,54,45,52,100,97,101,45,97,51,98,57,45,50,101,48,48,51,102,56,51,54,56,99,49,40,0,239,237,241,245,4,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,239,237,241,245,4,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,239,237,241,245,4,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,239,237,241,245,4,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,95,173,33,0,239,237,241,245,4,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,239,237,241,245,4,0,5,99,101,108,108,115,1,39,0,239,237,241,245,4,9,6,55,85,107,117,54,82,1,40,0,239,237,241,245,4,10,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,40,0,239,237,241,245,4,10,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,40,0,239,237,241,245,4,10,4,100,97,116,97,1,119,10,49,55,49,54,51,48,55,50,48,48,40,0,239,237,241,245,4,10,8,105,115,95,114,97,110,103,101,1,121,40,0,239,237,241,245,4,10,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,40,0,239,237,241,245,4,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,161,239,237,241,245,4,8,1,39,0,239,237,241,245,4,9,6,72,95,74,113,85,76,1,40,0,239,237,241,245,4,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,79,40,0,239,237,241,245,4,18,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,239,237,241,245,4,18,4,100,97,116,97,1,119,2,48,48,40,0,239,237,241,245,4,18,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,79,168,239,237,241,245,4,17,1,122,0,0,0,0,102,77,165,181,39,0,239,237,241,245,4,9,6,75,71,50,113,74,65,1,40,0,239,237,241,245,4,24,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,181,40,0,239,237,241,245,4,24,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,10,39,0,239,237,241,245,4,24,4,100,97,116,97,0,8,0,239,237,241,245,4,27,1,119,36,100,51,50,101,52,56,97,52,45,99,102,48,100,45,52,56,97,56,45,57,53,57,57,45,53,51,51,57,97,56,49,53,56,99,53,48,40,0,239,237,241,245,4,24,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,181,1,254,207,234,185,3,0,161,167,131,238,183,11,1,2,1,213,133,230,230,2,0,161,184,218,170,237,8,15,39,15,226,213,154,133,6,1,0,8,196,176,146,143,13,1,0,12,164,251,162,159,11,1,0,2,135,135,213,129,7,1,0,2,167,131,238,183,11,1,0,2,205,251,166,251,4,1,0,2,175,214,229,215,11,1,0,4,239,237,241,245,4,2,8,1,17,1,211,131,137,205,5,1,0,6,213,133,230,230,2,1,0,39,184,218,170,237,8,1,0,16,218,233,217,251,6,1,0,2,252,183,246,136,10,1,0,2,253,213,204,144,8,1,0,2,254,207,234,185,3,1,0,2],"92a2137e-b00b-4388-851f-a0efc3de7ca3":[13,1,238,246,169,231,15,0,161,163,149,186,140,1,3,2,1,148,143,229,148,15,0,161,170,241,252,142,10,38,2,1,142,159,154,239,14,0,161,199,176,167,174,6,5,12,1,237,148,148,223,13,0,161,222,150,248,170,3,2,4,1,195,215,232,135,13,0,161,199,176,167,174,6,5,2,1,170,241,252,142,10,0,161,132,169,228,37,13,39,26,227,145,252,193,9,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,227,145,252,193,9,0,2,105,100,1,119,36,57,50,97,50,49,51,55,101,45,98,48,48,98,45,52,51,56,56,45,56,53,49,102,45,97,48,101,102,99,51,100,101,55,99,97,51,40,0,227,145,252,193,9,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,227,145,252,193,9,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,227,145,252,193,9,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,227,145,252,193,9,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,187,135,33,0,227,145,252,193,9,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,227,145,252,193,9,0,5,99,101,108,108,115,1,161,227,145,252,193,9,8,1,39,0,227,145,252,193,9,9,6,72,95,74,113,85,76,1,40,0,227,145,252,193,9,11,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,187,143,33,0,227,145,252,193,9,11,10,102,105,101,108,100,95,116,121,112,101,1,33,0,227,145,252,193,9,11,4,100,97,116,97,1,33,0,227,145,252,193,9,11,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,227,145,252,193,9,10,1,168,227,145,252,193,9,14,1,119,7,57,57,57,57,57,50,50,168,227,145,252,193,9,13,1,122,0,0,0,0,0,0,0,0,168,227,145,252,193,9,15,1,122,0,0,0,0,102,77,187,221,168,227,145,252,193,9,16,1,122,0,0,0,0,102,77,187,222,39,0,227,145,252,193,9,9,6,95,82,45,112,104,105,1,40,0,227,145,252,193,9,21,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,187,222,40,0,227,145,252,193,9,21,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,4,40,0,227,145,252,193,9,21,4,100,97,116,97,1,119,4,73,73,66,100,40,0,227,145,252,193,9,21,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,187,222,1,199,176,167,174,6,0,161,238,246,169,231,15,1,6,1,166,146,188,210,5,0,161,142,159,154,239,14,11,2,1,222,150,248,170,3,0,161,166,146,188,210,5,1,3,2,149,229,205,200,1,0,161,237,148,148,223,13,3,2,168,149,229,205,200,1,1,1,122,0,0,0,0,102,88,31,89,1,163,149,186,140,1,0,161,148,143,229,148,15,1,4,1,132,169,228,37,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,14,13,238,246,169,231,15,1,0,2,195,215,232,135,13,1,0,2,163,149,186,140,1,1,0,4,132,169,228,37,1,0,14,148,143,229,148,15,1,0,2,199,176,167,174,6,1,0,6,166,146,188,210,5,1,0,2,227,145,252,193,9,3,8,1,10,1,13,4,170,241,252,142,10,1,0,39,149,229,205,200,1,1,0,2,237,148,148,223,13,1,0,4,222,150,248,170,3,1,0,3,142,159,154,239,14,1,0,12],"2150cff6-ff80-4334-8c8a-94e82a64379a":[15,35,184,224,238,246,15,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,184,224,238,246,15,0,2,105,100,1,119,36,50,49,53,48,99,102,102,54,45,102,102,56,48,45,52,51,51,52,45,56,99,56,97,45,57,52,101,56,50,97,54,52,51,55,57,97,40,0,184,224,238,246,15,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,184,224,238,246,15,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,184,224,238,246,15,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,184,224,238,246,15,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,95,172,33,0,184,224,238,246,15,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,184,224,238,246,15,0,5,99,101,108,108,115,1,39,0,184,224,238,246,15,9,6,55,85,107,117,54,82,1,40,0,184,224,238,246,15,10,4,100,97,116,97,1,119,10,49,55,49,54,51,48,55,50,48,48,40,0,184,224,238,246,15,10,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,40,0,184,224,238,246,15,10,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,184,224,238,246,15,10,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,40,0,184,224,238,246,15,10,8,105,115,95,114,97,110,103,101,1,121,40,0,184,224,238,246,15,10,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,161,184,224,238,246,15,8,1,39,0,184,224,238,246,15,9,6,72,95,74,113,85,76,1,40,0,184,224,238,246,15,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,76,40,0,184,224,238,246,15,18,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,184,224,238,246,15,18,4,100,97,116,97,1,119,3,104,105,49,40,0,184,224,238,246,15,18,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,76,161,184,224,238,246,15,17,1,39,0,184,224,238,246,15,9,6,70,99,112,109,80,101,1,40,0,184,224,238,246,15,24,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,127,40,0,184,224,238,246,15,24,4,100,97,116,97,1,119,3,89,101,115,40,0,184,224,238,246,15,24,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,184,224,238,246,15,24,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,127,168,184,224,238,246,15,23,1,122,0,0,0,0,102,77,165,148,39,0,184,224,238,246,15,9,6,112,70,120,57,67,45,1,40,0,184,224,238,246,15,30,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,148,40,0,184,224,238,246,15,30,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,7,40,0,184,224,238,246,15,30,4,100,97,116,97,1,119,82,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,119,122,107,74,34,44,34,110,97,109,101,34,58,34,53,53,53,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,93,125,40,0,184,224,238,246,15,30,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,148,2,205,147,147,241,14,0,161,239,184,147,146,4,3,2,168,205,147,147,241,14,1,1,122,0,0,0,0,102,88,31,91,1,246,235,197,205,14,0,161,185,248,189,241,10,1,2,1,194,245,173,211,11,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,8,1,185,248,189,241,10,0,161,241,200,244,209,10,1,2,1,176,221,143,219,10,0,161,194,245,173,211,11,7,21,1,241,200,244,209,10,0,161,166,226,191,247,8,1,2,1,166,226,191,247,8,0,161,240,207,252,192,1,38,2,1,186,199,157,206,8,0,161,203,254,130,163,1,1,2,1,185,239,230,135,7,0,161,189,129,252,252,2,5,2,2,174,181,215,227,6,0,161,189,129,252,252,2,5,1,161,185,239,230,135,7,1,11,1,239,184,147,146,4,0,161,186,199,157,206,8,1,4,1,189,129,252,252,2,0,161,246,235,197,205,14,1,6,1,240,207,252,192,1,0,161,176,221,143,219,10,20,39,1,203,254,130,163,1,0,161,174,181,215,227,6,11,2,15,194,245,173,211,11,1,0,8,166,226,191,247,8,1,0,2,203,254,130,163,1,1,0,2,205,147,147,241,14,1,0,2,174,181,215,227,6,1,0,12,239,184,147,146,4,1,0,4,176,221,143,219,10,1,0,21,240,207,252,192,1,1,0,39,241,200,244,209,10,1,0,2,246,235,197,205,14,1,0,2,184,224,238,246,15,3,8,1,17,1,23,1,185,248,189,241,10,1,0,2,185,239,230,135,7,1,0,2,186,199,157,206,8,1,0,2,189,129,252,252,2,1,0,6],"7717079b-05b6-4a0a-8ee4-48739fbf3a52":[18,1,238,246,246,209,14,0,161,222,139,223,157,3,30,6,1,242,233,195,179,14,0,161,147,233,229,181,2,9,2,1,176,198,177,177,14,0,161,242,233,195,179,14,1,2,1,171,249,223,240,13,0,161,176,198,177,177,14,1,2,1,189,169,216,163,13,0,161,229,212,189,183,1,3,2,1,241,188,132,177,11,0,161,171,249,223,240,13,1,5,2,176,157,175,239,9,0,161,241,188,132,177,11,4,2,168,176,157,175,239,9,1,1,122,0,0,0,0,102,88,31,91,1,244,197,233,193,9,0,161,189,169,216,163,13,1,6,1,231,233,173,168,9,0,161,206,211,220,252,6,33,40,1,206,211,220,252,6,0,161,243,207,130,177,3,8,34,54,237,203,168,145,4,0,161,145,187,128,129,2,39,1,40,0,145,187,128,129,2,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,89,162,161,145,187,128,129,2,13,1,161,145,187,128,129,2,11,1,161,145,187,128,129,2,15,1,161,145,187,128,129,2,12,1,161,145,187,128,129,2,14,1,161,145,187,128,129,2,16,1,33,0,145,187,128,129,2,10,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,237,203,168,145,4,0,1,168,237,203,168,145,4,6,1,119,10,49,55,49,54,52,51,49,54,53,49,168,237,203,168,145,4,4,1,121,168,237,203,168,145,4,7,1,120,168,237,203,168,145,4,2,1,119,0,168,237,203,168,145,4,3,1,122,0,0,0,0,0,0,0,2,168,237,203,168,145,4,5,1,119,10,49,55,49,54,51,52,53,50,53,49,168,237,203,168,145,4,8,1,122,0,0,0,0,102,77,89,163,161,237,203,168,145,4,9,1,168,145,187,128,129,2,27,1,122,0,0,0,0,0,0,0,6,168,145,187,128,129,2,26,1,119,11,97,112,112,102,108,111,119,121,46,105,111,168,145,187,128,129,2,28,1,122,0,0,0,0,102,77,165,88,161,237,203,168,145,4,17,1,168,145,187,128,129,2,20,1,119,9,73,73,66,100,44,110,103,110,85,168,145,187,128,129,2,21,1,122,0,0,0,0,0,0,0,4,168,145,187,128,129,2,22,1,122,0,0,0,0,102,77,165,108,161,237,203,168,145,4,21,1,39,0,145,187,128,129,2,9,6,71,79,80,107,116,118,1,40,0,237,203,168,145,4,26,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,114,40,0,237,203,168,145,4,26,4,100,97,116,97,1,119,4,104,77,109,67,40,0,237,203,168,145,4,26,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,3,40,0,237,203,168,145,4,26,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,114,161,237,203,168,145,4,25,1,39,0,145,187,128,129,2,9,6,70,99,112,109,80,101,1,40,0,237,203,168,145,4,32,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,126,40,0,237,203,168,145,4,32,4,100,97,116,97,1,119,3,89,101,115,40,0,237,203,168,145,4,32,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,5,40,0,237,203,168,145,4,32,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,126,161,237,203,168,145,4,31,1,39,0,145,187,128,129,2,9,6,112,70,120,57,67,45,1,40,0,237,203,168,145,4,38,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,137,33,0,237,203,168,145,4,38,10,102,105,101,108,100,95,116,121,112,101,1,33,0,237,203,168,145,4,38,4,100,97,116,97,1,33,0,237,203,168,145,4,38,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,237,203,168,145,4,37,1,168,237,203,168,145,4,40,1,122,0,0,0,0,0,0,0,7,168,237,203,168,145,4,41,1,119,88,123,34,111,112,116,105,111,110,115,34,58,91,123,34,105,100,34,58,34,106,97,56,104,34,44,34,110,97,109,101,34,58,34,49,50,51,34,44,34,99,111,108,111,114,34,58,34,80,117,114,112,108,101,34,125,93,44,34,115,101,108,101,99,116,101,100,95,111,112,116,105,111,110,95,105,100,115,34,58,91,34,106,97,56,104,34,93,125,168,237,203,168,145,4,42,1,122,0,0,0,0,102,77,165,139,168,237,203,168,145,4,43,1,122,0,0,0,0,102,77,165,176,39,0,145,187,128,129,2,9,6,75,71,50,113,74,65,1,40,0,237,203,168,145,4,48,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,176,40,0,237,203,168,145,4,48,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,10,39,0,237,203,168,145,4,48,4,100,97,116,97,0,8,0,237,203,168,145,4,51,1,119,36,50,48,56,100,50,52,56,102,45,53,99,48,56,45,52,98,101,53,45,97,48,50,50,45,101,48,97,57,55,99,50,100,55,48,53,101,40,0,237,203,168,145,4,48,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,77,165,176,1,235,139,213,209,3,0,161,231,233,173,168,9,39,2,1,243,207,130,177,3,0,161,238,246,246,209,14,5,9,1,222,139,223,157,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,31,1,147,233,229,181,2,0,161,244,197,233,193,9,5,10,45,145,187,128,129,2,0,39,1,4,100,97,116,97,4,100,97,116,97,1,39,1,4,100,97,116,97,4,109,101,116,97,1,39,1,4,100,97,116,97,7,99,111,109,109,101,110,116,0,40,0,145,187,128,129,2,0,2,105,100,1,119,36,55,55,49,55,48,55,57,98,45,48,53,98,54,45,52,97,48,97,45,56,101,101,52,45,52,56,55,51,57,102,98,102,51,97,53,50,40,0,145,187,128,129,2,0,11,100,97,116,97,98,97,115,101,95,105,100,1,119,36,99,101,50,54,55,100,49,50,45,51,98,54,49,45,52,101,98,98,45,98,98,48,51,45,100,54,53,50,55,50,102,53,102,56,49,55,40,0,145,187,128,129,2,0,6,104,101,105,103,104,116,1,122,0,0,0,0,0,0,0,60,40,0,145,187,128,129,2,0,10,118,105,115,105,98,105,108,105,116,121,1,120,40,0,145,187,128,129,2,0,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,247,33,0,145,187,128,129,2,0,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,39,0,145,187,128,129,2,0,5,99,101,108,108,115,1,39,0,145,187,128,129,2,9,6,55,85,107,117,54,82,1,33,0,145,187,128,129,2,10,10,102,105,101,108,100,95,116,121,112,101,1,33,0,145,187,128,129,2,10,4,100,97,116,97,1,33,0,145,187,128,129,2,10,11,114,101,109,105,110,100,101,114,95,105,100,1,33,0,145,187,128,129,2,10,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,33,0,145,187,128,129,2,10,12,105,110,99,108,117,100,101,95,116,105,109,101,1,33,0,145,187,128,129,2,10,8,105,115,95,114,97,110,103,101,1,161,145,187,128,129,2,8,1,39,0,145,187,128,129,2,9,6,95,82,45,112,104,105,1,40,0,145,187,128,129,2,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,101,255,33,0,145,187,128,129,2,18,4,100,97,116,97,1,33,0,145,187,128,129,2,18,10,102,105,101,108,100,95,116,121,112,101,1,33,0,145,187,128,129,2,18,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,145,187,128,129,2,17,1,39,0,145,187,128,129,2,9,6,99,78,53,98,120,74,1,40,0,145,187,128,129,2,24,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,102,14,33,0,145,187,128,129,2,24,4,100,97,116,97,1,33,0,145,187,128,129,2,24,10,102,105,101,108,100,95,116,121,112,101,1,33,0,145,187,128,129,2,24,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,161,145,187,128,129,2,23,1,39,0,145,187,128,129,2,9,6,71,115,66,65,97,76,1,40,0,145,187,128,129,2,30,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,102,25,40,0,145,187,128,129,2,30,8,105,115,95,114,97,110,103,101,1,121,40,0,145,187,128,129,2,30,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,2,40,0,145,187,128,129,2,30,12,105,110,99,108,117,100,101,95,116,105,109,101,1,121,40,0,145,187,128,129,2,30,4,100,97,116,97,1,119,10,49,55,49,54,52,53,53,55,48,53,40,0,145,187,128,129,2,30,11,114,101,109,105,110,100,101,114,95,105,100,1,119,0,40,0,145,187,128,129,2,30,13,101,110,100,95,116,105,109,101,115,116,97,109,112,1,119,0,40,0,145,187,128,129,2,30,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,102,25,161,145,187,128,129,2,29,1,39,0,145,187,128,129,2,9,6,72,95,74,113,85,76,1,40,0,145,187,128,129,2,40,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,102,32,40,0,145,187,128,129,2,40,10,102,105,101,108,100,95,116,121,112,101,1,122,0,0,0,0,0,0,0,0,40,0,145,187,128,129,2,40,4,100,97,116,97,1,119,5,119,111,114,108,100,40,0,145,187,128,129,2,40,13,108,97,115,116,95,109,111,100,105,102,105,101,100,1,122,0,0,0,0,102,76,102,32,1,229,212,189,183,1,0,161,235,139,213,209,3,1,4,1,222,172,192,75,0,161,244,197,233,193,9,5,2,18,229,212,189,183,1,1,0,4,231,233,173,168,9,1,0,40,235,139,213,209,3,1,0,2,171,249,223,240,13,1,0,2,237,203,168,145,4,8,0,1,2,8,17,1,21,1,25,1,31,1,37,1,40,4,238,246,246,209,14,1,0,6,206,211,220,252,6,1,0,34,176,198,177,177,14,1,0,2,241,188,132,177,11,1,0,5,242,233,195,179,14,1,0,2,243,207,130,177,3,1,0,9,244,197,233,193,9,1,0,6,147,233,229,181,2,1,0,10,145,187,128,129,2,5,8,1,11,7,20,4,26,4,39,1,176,157,175,239,9,1,0,2,189,169,216,163,13,1,0,2,222,139,223,157,3,1,0,31,222,172,192,75,1,0,2]} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json b/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json new file mode 100644 index 0000000000..53c4b0f6df --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/document/f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0.json @@ -0,0 +1 @@ +{"data":{"state_vector":[6,248,208,217,159,7,16,226,250,246,177,3,17,211,142,141,147,5,8,196,148,203,38,26,230,150,209,139,14,193,6,167,238,246,72,86],"doc_state":[6,177,5,230,150,209,139,14,0,0,2,0,1,0,5,39,0,196,148,203,38,4,6,122,45,72,76,78,68,2,4,0,230,150,209,139,14,8,4,49,49,49,49,39,0,196,148,203,38,1,6,54,70,57,105,75,107,1,40,0,230,150,209,139,14,13,2,105,100,1,119,6,54,70,57,105,75,107,40,0,230,150,209,139,14,13,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,13,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,13,8,99,104,105,108,100,114,101,110,1,119,6,97,118,105,69,115,45,33,0,230,150,209,139,14,13,4,100,97,116,97,1,40,0,230,150,209,139,14,13,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,13,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,118,105,69,115,45,0,72,196,148,203,38,24,1,119,6,54,70,57,105,75,107,39,0,196,148,203,38,4,6,69,115,57,50,84,74,2,33,0,196,148,203,38,1,6,98,71,84,69,74,116,1,0,7,33,0,196,148,203,38,3,6,87,56,66,102,66,110,1,129,196,148,203,38,24,1,168,230,150,209,139,14,18,1,119,39,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,49,49,49,34,125,93,44,34,108,101,118,101,108,34,58,50,125,4,0,230,150,209,139,14,23,1,35,0,1,132,230,150,209,139,14,35,1,35,0,1,132,230,150,209,139,14,37,1,35,0,1,132,230,150,209,139,14,39,1,35,0,1,39,0,196,148,203,38,4,6,56,76,76,113,48,56,2,39,0,196,148,203,38,1,6,56,70,105,117,112,98,1,40,0,230,150,209,139,14,44,2,105,100,1,119,6,56,70,105,117,112,98,40,0,230,150,209,139,14,44,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,230,150,209,139,14,44,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,44,8,99,104,105,108,100,114,101,110,1,119,6,49,66,67,68,107,53,33,0,230,150,209,139,14,44,4,100,97,116,97,1,40,0,230,150,209,139,14,44,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,44,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,49,66,67,68,107,53,0,200,196,148,203,38,24,230,150,209,139,14,33,1,119,6,56,70,105,117,112,98,4,0,230,150,209,139,14,43,1,50,161,230,150,209,139,14,49,1,132,230,150,209,139,14,54,1,50,161,230,150,209,139,14,55,1,132,230,150,209,139,14,56,1,50,161,230,150,209,139,14,57,1,132,230,150,209,139,14,58,1,50,161,230,150,209,139,14,59,1,39,0,196,148,203,38,4,6,103,76,57,104,70,45,2,39,0,196,148,203,38,1,6,72,99,71,82,109,56,1,40,0,230,150,209,139,14,63,2,105,100,1,119,6,72,99,71,82,109,56,40,0,230,150,209,139,14,63,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,63,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,63,8,99,104,105,108,100,114,101,110,1,119,6,50,114,117,49,72,109,33,0,230,150,209,139,14,63,4,100,97,116,97,1,40,0,230,150,209,139,14,63,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,63,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,50,114,117,49,72,109,0,136,230,150,209,139,14,33,1,119,6,72,99,71,82,109,56,168,230,150,209,139,14,61,1,119,39,123,34,108,101,118,101,108,34,58,52,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,50,50,50,34,125,93,125,1,0,230,150,209,139,14,62,1,161,230,150,209,139,14,68,1,129,230,150,209,139,14,74,1,161,230,150,209,139,14,75,1,129,230,150,209,139,14,76,2,161,230,150,209,139,14,77,1,129,230,150,209,139,14,79,1,161,230,150,209,139,14,80,1,129,230,150,209,139,14,81,1,161,230,150,209,139,14,82,1,68,230,150,209,139,14,74,6,228,189,160,229,165,189,168,230,150,209,139,14,84,1,119,31,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,228,189,160,229,165,189,34,125,93,125,39,0,196,148,203,38,4,6,113,111,68,56,109,111,2,33,0,196,148,203,38,1,6,113,118,66,98,82,83,1,0,7,33,0,196,148,203,38,3,6,118,76,97,51,57,90,1,129,230,150,209,139,14,72,1,1,0,230,150,209,139,14,88,1,0,1,129,230,150,209,139,14,99,1,0,1,129,230,150,209,139,14,101,1,0,4,132,230,150,209,139,14,103,1,91,0,1,132,230,150,209,139,14,108,1,93,0,1,39,0,196,148,203,38,4,6,69,114,99,104,55,72,2,39,0,196,148,203,38,1,6,51,111,95,70,117,67,1,40,0,230,150,209,139,14,113,2,105,100,1,119,6,51,111,95,70,117,67,40,0,230,150,209,139,14,113,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,230,150,209,139,14,113,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,113,8,99,104,105,108,100,114,101,110,1,119,6,68,56,109,75,57,97,33,0,230,150,209,139,14,113,4,100,97,116,97,1,40,0,230,150,209,139,14,113,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,113,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,68,56,109,75,57,97,0,200,230,150,209,139,14,72,230,150,209,139,14,98,1,119,6,51,111,95,70,117,67,4,0,230,150,209,139,14,112,1,49,161,230,150,209,139,14,118,1,132,230,150,209,139,14,123,1,50,161,230,150,209,139,14,124,1,132,230,150,209,139,14,125,1,51,161,230,150,209,139,14,126,1,39,0,196,148,203,38,4,6,122,55,116,111,73,55,2,33,0,196,148,203,38,1,6,120,66,121,81,55,113,1,0,7,33,0,196,148,203,38,3,6,81,98,51,101,57,117,1,129,230,150,209,139,14,98,1,168,230,150,209,139,14,128,1,1,119,44,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,1,0,230,150,209,139,14,129,1,1,0,1,129,230,150,209,139,14,141,1,1,0,1,129,230,150,209,139,14,143,1,1,0,4,132,230,150,209,139,14,145,1,1,45,0,1,39,0,196,148,203,38,4,6,84,119,106,104,109,54,2,39,0,196,148,203,38,1,6,69,76,108,103,56,112,1,40,0,230,150,209,139,14,153,1,2,105,100,1,119,6,69,76,108,103,56,112,40,0,230,150,209,139,14,153,1,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,230,150,209,139,14,153,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,153,1,8,99,104,105,108,100,114,101,110,1,119,6,80,56,121,104,83,86,33,0,230,150,209,139,14,153,1,4,100,97,116,97,1,40,0,230,150,209,139,14,153,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,153,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,80,56,121,104,83,86,0,200,230,150,209,139,14,98,230,150,209,139,14,139,1,1,119,6,69,76,108,103,56,112,4,0,230,150,209,139,14,152,1,1,49,161,230,150,209,139,14,158,1,1,132,230,150,209,139,14,163,1,1,50,161,230,150,209,139,14,164,1,1,132,230,150,209,139,14,165,1,1,51,168,230,150,209,139,14,166,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,39,0,196,148,203,38,4,6,100,53,73,101,79,51,2,33,0,196,148,203,38,1,6,79,100,120,77,110,80,1,0,7,33,0,196,148,203,38,3,6,120,89,113,72,122,98,1,129,230,150,209,139,14,139,1,1,4,0,230,150,209,139,14,169,1,1,49,0,1,132,230,150,209,139,14,180,1,1,46,0,1,39,0,196,148,203,38,4,6,50,78,117,49,104,78,2,39,0,196,148,203,38,1,6,88,110,106,101,115,65,1,40,0,230,150,209,139,14,185,1,2,105,100,1,119,6,88,110,106,101,115,65,40,0,230,150,209,139,14,185,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,185,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,185,1,8,99,104,105,108,100,114,101,110,1,119,6,102,119,53,69,102,68,33,0,230,150,209,139,14,185,1,4,100,97,116,97,1,40,0,230,150,209,139,14,185,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,185,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,102,119,53,69,102,68,0,200,230,150,209,139,14,139,1,230,150,209,139,14,179,1,1,119,6,88,110,106,101,115,65,4,0,230,150,209,139,14,184,1,1,49,161,230,150,209,139,14,190,1,1,132,230,150,209,139,14,195,1,1,50,161,230,150,209,139,14,196,1,1,132,230,150,209,139,14,197,1,1,51,161,230,150,209,139,14,198,1,1,39,0,196,148,203,38,4,6,107,79,86,116,87,117,2,39,0,196,148,203,38,1,6,107,106,98,87,90,118,1,40,0,230,150,209,139,14,202,1,2,105,100,1,119,6,107,106,98,87,90,118,40,0,230,150,209,139,14,202,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,202,1,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,202,1,8,99,104,105,108,100,114,101,110,1,119,6,56,115,49,108,106,52,33,0,230,150,209,139,14,202,1,4,100,97,116,97,1,40,0,230,150,209,139,14,202,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,202,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,56,115,49,108,106,52,0,136,230,150,209,139,14,179,1,1,119,6,107,106,98,87,90,118,168,230,150,209,139,14,200,1,1,119,39,123,34,110,117,109,98,101,114,34,58,49,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,4,0,230,150,209,139,14,201,1,1,51,161,230,150,209,139,14,207,1,1,132,230,150,209,139,14,213,1,1,50,161,230,150,209,139,14,214,1,1,132,230,150,209,139,14,215,1,1,49,168,230,150,209,139,14,216,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,101,56,113,73,98,66,2,33,0,196,148,203,38,1,6,120,83,99,121,67,100,1,0,7,33,0,196,148,203,38,3,6,102,88,53,51,101,80,1,129,230,150,209,139,14,211,1,1,39,0,196,148,203,38,4,6,57,87,73,111,101,83,2,39,0,196,148,203,38,1,6,48,77,97,84,52,54,1,40,0,230,150,209,139,14,231,1,2,105,100,1,119,6,48,77,97,84,52,54,40,0,230,150,209,139,14,231,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,231,1,6,112,97,114,101,110,116,1,119,6,107,106,98,87,90,118,40,0,230,150,209,139,14,231,1,8,99,104,105,108,100,114,101,110,1,119,6,68,70,72,68,101,106,33,0,230,150,209,139,14,231,1,4,100,97,116,97,1,40,0,230,150,209,139,14,231,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,231,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,68,70,72,68,101,106,0,8,0,230,150,209,139,14,210,1,1,119,6,48,77,97,84,52,54,4,0,230,150,209,139,14,230,1,1,51,161,230,150,209,139,14,236,1,1,132,230,150,209,139,14,241,1,1,50,161,230,150,209,139,14,242,1,1,132,230,150,209,139,14,243,1,1,49,168,230,150,209,139,14,244,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,114,104,72,106,57,65,2,33,0,196,148,203,38,1,6,84,45,109,108,72,57,1,0,7,33,0,196,148,203,38,3,6,118,110,77,57,77,84,1,129,230,150,209,139,14,240,1,1,39,0,196,148,203,38,4,6,49,90,55,100,72,105,2,39,0,196,148,203,38,1,6,72,102,88,86,103,83,1,40,0,230,150,209,139,14,131,2,2,105,100,1,119,6,72,102,88,86,103,83,40,0,230,150,209,139,14,131,2,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,230,150,209,139,14,131,2,6,112,97,114,101,110,116,1,119,6,48,77,97,84,52,54,40,0,230,150,209,139,14,131,2,8,99,104,105,108,100,114,101,110,1,119,6,57,122,75,84,78,57,33,0,230,150,209,139,14,131,2,4,100,97,116,97,1,40,0,230,150,209,139,14,131,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,131,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,57,122,75,84,78,57,0,8,0,230,150,209,139,14,239,1,1,119,6,72,102,88,86,103,83,4,0,230,150,209,139,14,130,2,1,51,161,230,150,209,139,14,136,2,1,132,230,150,209,139,14,141,2,1,50,161,230,150,209,139,14,142,2,1,132,230,150,209,139,14,143,2,1,49,168,230,150,209,139,14,144,2,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,65,98,83,76,67,56,2,33,0,196,148,203,38,1,6,97,86,52,120,55,81,1,0,7,33,0,196,148,203,38,3,6,69,87,113,113,77,67,1,129,230,150,209,139,14,140,2,1,39,0,196,148,203,38,4,6,80,82,49,65,99,76,2,33,0,196,148,203,38,1,6,115,66,104,71,113,108,1,0,7,33,0,196,148,203,38,3,6,114,86,95,54,88,121,1,129,230,150,209,139,14,129,2,1,1,0,230,150,209,139,14,158,2,1,0,2,39,0,196,148,203,38,4,6,51,54,82,71,101,79,2,33,0,196,148,203,38,1,6,81,99,88,112,117,56,1,0,7,33,0,196,148,203,38,3,6,106,53,72,95,112,48,1,193,230,150,209,139,14,129,2,230,150,209,139,14,168,2,1,39,0,196,148,203,38,4,6,101,120,109,55,104,73,2,33,0,196,148,203,38,1,6,52,70,81,69,87,98,1,0,7,33,0,196,148,203,38,3,6,79,82,107,84,68,115,1,129,230,150,209,139,14,229,1,1,4,0,230,150,209,139,14,183,2,1,34,0,1,39,0,196,148,203,38,4,6,52,48,110,100,113,55,2,39,0,196,148,203,38,1,6,100,53,57,122,110,74,1,40,0,230,150,209,139,14,197,2,2,105,100,1,119,6,100,53,57,122,110,74,40,0,230,150,209,139,14,197,2,2,116,121,1,119,5,113,117,111,116,101,40,0,230,150,209,139,14,197,2,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,197,2,8,99,104,105,108,100,114,101,110,1,119,6,81,83,101,49,78,65,33,0,230,150,209,139,14,197,2,4,100,97,116,97,1,40,0,230,150,209,139,14,197,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,197,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,81,83,101,49,78,65,0,200,230,150,209,139,14,229,1,230,150,209,139,14,193,2,1,119,6,100,53,57,122,110,74,4,0,230,150,209,139,14,196,2,1,49,161,230,150,209,139,14,202,2,1,132,230,150,209,139,14,207,2,1,50,161,230,150,209,139,14,208,2,1,132,230,150,209,139,14,209,2,1,51,168,230,150,209,139,14,210,2,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,39,0,196,148,203,38,4,6,98,84,120,48,103,106,2,33,0,196,148,203,38,1,6,99,72,87,116,90,66,1,0,7,33,0,196,148,203,38,3,6,54,67,53,67,68,81,1,129,230,150,209,139,14,193,2,1,4,0,230,150,209,139,14,213,2,1,62,0,1,39,0,196,148,203,38,4,6,48,95,102,53,81,87,2,39,0,196,148,203,38,1,6,115,106,51,120,82,67,1,40,0,230,150,209,139,14,227,2,2,105,100,1,119,6,115,106,51,120,82,67,40,0,230,150,209,139,14,227,2,2,116,121,1,119,11,116,111,103,103,108,101,95,108,105,115,116,40,0,230,150,209,139,14,227,2,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,227,2,8,99,104,105,108,100,114,101,110,1,119,6,106,77,71,51,69,107,33,0,230,150,209,139,14,227,2,4,100,97,116,97,1,40,0,230,150,209,139,14,227,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,227,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,106,77,71,51,69,107,0,200,230,150,209,139,14,193,2,230,150,209,139,14,223,2,1,119,6,115,106,51,120,82,67,4,0,230,150,209,139,14,226,2,1,49,161,230,150,209,139,14,232,2,1,132,230,150,209,139,14,237,2,1,50,161,230,150,209,139,14,238,2,1,132,230,150,209,139,14,239,2,1,51,161,230,150,209,139,14,240,2,1,132,230,150,209,139,14,241,2,1,51,161,230,150,209,139,14,242,2,1,39,0,196,148,203,38,4,6,75,108,79,113,102,103,2,39,0,196,148,203,38,1,6,103,102,97,107,74,56,1,40,0,230,150,209,139,14,246,2,2,105,100,1,119,6,103,102,97,107,74,56,40,0,230,150,209,139,14,246,2,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,246,2,6,112,97,114,101,110,116,1,119,6,115,106,51,120,82,67,40,0,230,150,209,139,14,246,2,8,99,104,105,108,100,114,101,110,1,119,6,104,112,106,70,101,78,33,0,230,150,209,139,14,246,2,4,100,97,116,97,1,40,0,230,150,209,139,14,246,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,246,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,104,112,106,70,101,78,0,8,0,230,150,209,139,14,235,2,1,119,6,103,102,97,107,74,56,168,230,150,209,139,14,244,2,1,119,47,123,34,99,111,108,108,97,112,115,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,51,34,125,93,125,4,0,230,150,209,139,14,245,2,1,49,161,230,150,209,139,14,251,2,1,132,230,150,209,139,14,129,3,1,50,161,230,150,209,139,14,130,3,1,132,230,150,209,139,14,131,3,1,51,161,230,150,209,139,14,132,3,1,132,230,150,209,139,14,133,3,1,51,168,230,150,209,139,14,134,3,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,51,34,125,93,125,39,0,196,148,203,38,4,6,112,112,49,122,122,76,2,33,0,196,148,203,38,1,6,49,112,69,74,83,110,1,0,7,33,0,196,148,203,38,3,6,55,53,120,75,111,69,1,129,230,150,209,139,14,255,2,1,39,0,196,148,203,38,4,6,69,89,89,101,73,97,2,39,0,196,148,203,38,1,6,53,73,97,85,112,119,1,40,0,230,150,209,139,14,149,3,2,105,100,1,119,6,53,73,97,85,112,119,40,0,230,150,209,139,14,149,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,149,3,6,112,97,114,101,110,116,1,119,6,103,102,97,107,74,56,40,0,230,150,209,139,14,149,3,8,99,104,105,108,100,114,101,110,1,119,6,97,118,100,122,99,120,33,0,230,150,209,139,14,149,3,4,100,97,116,97,1,40,0,230,150,209,139,14,149,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,149,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,118,100,122,99,120,0,8,0,230,150,209,139,14,254,2,1,119,6,53,73,97,85,112,119,4,0,230,150,209,139,14,148,3,1,51,161,230,150,209,139,14,154,3,1,132,230,150,209,139,14,159,3,1,50,161,230,150,209,139,14,160,3,1,132,230,150,209,139,14,161,3,1,49,168,230,150,209,139,14,162,3,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,50,49,34,125,93,125,39,0,196,148,203,38,4,6,45,71,114,111,55,97,2,33,0,196,148,203,38,1,6,86,52,66,50,115,90,1,0,7,33,0,196,148,203,38,3,6,77,54,103,66,71,77,1,129,230,150,209,139,14,158,3,1,39,0,196,148,203,38,4,6,65,71,102,55,65,110,2,33,0,196,148,203,38,1,6,74,87,105,89,112,50,1,0,7,33,0,196,148,203,38,3,6,49,86,56,87,104,113,1,129,230,150,209,139,14,175,3,1,39,0,196,148,203,38,4,6,88,116,53,71,97,50,2,33,0,196,148,203,38,1,6,115,76,114,115,90,118,1,0,7,33,0,196,148,203,38,3,6,97,113,55,117,111,77,1,129,230,150,209,139,14,147,3,1,39,0,196,148,203,38,4,6,87,109,86,51,97,97,2,33,0,196,148,203,38,1,6,48,85,79,89,119,97,1,0,7,33,0,196,148,203,38,3,6,52,119,71,102,45,122,1,129,230,150,209,139,14,223,2,1,39,0,196,148,203,38,4,6,78,116,76,65,67,112,2,33,0,196,148,203,38,1,6,68,65,117,54,49,114,1,0,7,33,0,196,148,203,38,3,6,81,112,115,98,120,66,1,129,230,150,209,139,14,197,3,1,39,0,196,148,203,38,4,6,85,75,78,70,53,53,2,33,0,196,148,203,38,1,6,80,111,100,107,78,98,1,0,7,33,0,196,148,203,38,3,6,113,66,65,113,81,87,1,129,230,150,209,139,14,208,3,1,39,0,196,148,203,38,1,6,70,45,78,106,49,118,1,40,0,230,150,209,139,14,231,3,2,105,100,1,119,6,70,45,78,106,49,118,40,0,230,150,209,139,14,231,3,2,116,121,1,119,5,105,109,97,103,101,40,0,230,150,209,139,14,231,3,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,231,3,8,99,104,105,108,100,114,101,110,1,119,6,65,121,108,69,98,88,33,0,230,150,209,139,14,231,3,4,100,97,116,97,1,40,0,230,150,209,139,14,231,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,231,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,65,121,108,69,98,88,0,200,230,150,209,139,14,208,3,230,150,209,139,14,230,3,1,119,6,70,45,78,106,49,118,39,0,196,148,203,38,4,6,54,113,56,86,101,76,2,33,0,196,148,203,38,1,6,82,100,68,97,117,119,1,0,7,33,0,196,148,203,38,3,6,111,117,122,65,90,71,1,129,230,150,209,139,14,230,3,1,168,230,150,209,139,14,236,3,1,119,212,1,123,34,97,108,105,103,110,34,58,34,99,101,110,116,101,114,34,44,34,117,114,108,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,53,51,57,48,51,50,49,50,49,51,45,99,56,100,56,56,98,51,101,48,50,52,97,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,89,48,78,68,99,49,78,122,100,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,125,39,0,196,148,203,38,4,6,85,57,76,71,106,107,2,39,0,196,148,203,38,4,6,114,87,117,84,54,65,2,39,0,196,148,203,38,4,6,66,106,53,104,119,48,2,39,0,196,148,203,38,4,6,111,88,109,81,73,90,2,33,0,196,148,203,38,1,6,116,56,115,72,75,74,1,0,7,33,0,196,148,203,38,3,6,52,113,115,55,118,84,1,65,230,150,209,139,14,22,1,39,0,196,148,203,38,4,6,52,66,101,119,114,104,2,39,0,196,148,203,38,1,6,45,45,112,51,53,121,1,40,0,230,150,209,139,14,140,4,2,105,100,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,140,4,2,116,121,1,119,5,116,97,98,108,101,40,0,230,150,209,139,14,140,4,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,140,4,8,99,104,105,108,100,114,101,110,1,119,6,82,73,74,99,48,98,33,0,230,150,209,139,14,140,4,4,100,97,116,97,1,40,0,230,150,209,139,14,140,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,140,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,82,73,74,99,48,98,0,200,230,150,209,139,14,230,3,230,150,209,139,14,251,3,1,119,6,45,45,112,51,53,121,39,0,196,148,203,38,1,6,78,95,121,106,84,95,1,40,0,230,150,209,139,14,150,4,2,105,100,1,119,6,78,95,121,106,84,95,40,0,230,150,209,139,14,150,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,150,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,150,4,8,99,104,105,108,100,114,101,110,1,119,6,73,76,68,100,48,55,33,0,230,150,209,139,14,150,4,4,100,97,116,97,1,40,0,230,150,209,139,14,150,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,150,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,73,76,68,100,48,55,0,8,0,230,150,209,139,14,148,4,1,119,6,78,95,121,106,84,95,39,0,196,148,203,38,1,6,80,80,120,80,86,55,1,40,0,230,150,209,139,14,160,4,2,105,100,1,119,6,80,80,120,80,86,55,40,0,230,150,209,139,14,160,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,160,4,6,112,97,114,101,110,116,1,119,6,78,95,121,106,84,95,40,0,230,150,209,139,14,160,4,8,99,104,105,108,100,114,101,110,1,119,6,67,82,84,77,83,108,33,0,230,150,209,139,14,160,4,4,100,97,116,97,1,40,0,230,150,209,139,14,160,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,160,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,67,82,84,77,83,108,0,8,0,230,150,209,139,14,158,4,1,119,6,80,80,120,80,86,55,39,0,196,148,203,38,1,6,111,57,69,90,109,75,1,40,0,230,150,209,139,14,170,4,2,105,100,1,119,6,111,57,69,90,109,75,40,0,230,150,209,139,14,170,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,170,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,170,4,8,99,104,105,108,100,114,101,110,1,119,6,118,97,67,119,70,45,33,0,230,150,209,139,14,170,4,4,100,97,116,97,1,40,0,230,150,209,139,14,170,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,170,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,118,97,67,119,70,45,0,136,230,150,209,139,14,159,4,1,119,6,111,57,69,90,109,75,39,0,196,148,203,38,1,6,95,114,49,111,86,55,1,40,0,230,150,209,139,14,180,4,2,105,100,1,119,6,95,114,49,111,86,55,40,0,230,150,209,139,14,180,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,180,4,6,112,97,114,101,110,116,1,119,6,111,57,69,90,109,75,40,0,230,150,209,139,14,180,4,8,99,104,105,108,100,114,101,110,1,119,6,97,80,74,89,49,98,33,0,230,150,209,139,14,180,4,4,100,97,116,97,1,40,0,230,150,209,139,14,180,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,180,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,97,80,74,89,49,98,0,8,0,230,150,209,139,14,178,4,1,119,6,95,114,49,111,86,55,39,0,196,148,203,38,1,6,120,118,83,84,69,100,1,40,0,230,150,209,139,14,190,4,2,105,100,1,119,6,120,118,83,84,69,100,40,0,230,150,209,139,14,190,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,190,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,190,4,8,99,104,105,108,100,114,101,110,1,119,6,106,68,100,121,83,101,33,0,230,150,209,139,14,190,4,4,100,97,116,97,1,40,0,230,150,209,139,14,190,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,190,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,106,68,100,121,83,101,0,136,230,150,209,139,14,179,4,1,119,6,120,118,83,84,69,100,39,0,196,148,203,38,1,6,116,69,111,81,110,97,1,40,0,230,150,209,139,14,200,4,2,105,100,1,119,6,116,69,111,81,110,97,40,0,230,150,209,139,14,200,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,200,4,6,112,97,114,101,110,116,1,119,6,120,118,83,84,69,100,40,0,230,150,209,139,14,200,4,8,99,104,105,108,100,114,101,110,1,119,6,103,106,77,67,117,55,33,0,230,150,209,139,14,200,4,4,100,97,116,97,1,40,0,230,150,209,139,14,200,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,200,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,103,106,77,67,117,55,0,8,0,230,150,209,139,14,198,4,1,119,6,116,69,111,81,110,97,39,0,196,148,203,38,1,6,76,112,106,89,74,119,1,40,0,230,150,209,139,14,210,4,2,105,100,1,119,6,76,112,106,89,74,119,40,0,230,150,209,139,14,210,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,210,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,210,4,8,99,104,105,108,100,114,101,110,1,119,6,113,68,56,99,76,101,33,0,230,150,209,139,14,210,4,4,100,97,116,97,1,40,0,230,150,209,139,14,210,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,210,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,113,68,56,99,76,101,0,136,230,150,209,139,14,199,4,1,119,6,76,112,106,89,74,119,39,0,196,148,203,38,1,6,83,50,85,85,71,53,1,40,0,230,150,209,139,14,220,4,2,105,100,1,119,6,83,50,85,85,71,53,40,0,230,150,209,139,14,220,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,220,4,6,112,97,114,101,110,116,1,119,6,76,112,106,89,74,119,40,0,230,150,209,139,14,220,4,8,99,104,105,108,100,114,101,110,1,119,6,57,51,116,87,52,111,33,0,230,150,209,139,14,220,4,4,100,97,116,97,1,40,0,230,150,209,139,14,220,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,220,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,57,51,116,87,52,111,0,8,0,230,150,209,139,14,218,4,1,119,6,83,50,85,85,71,53,4,0,230,150,209,139,14,253,3,1,53,161,230,150,209,139,14,165,4,1,132,230,150,209,139,14,230,4,1,53,161,230,150,209,139,14,231,4,1,132,230,150,209,139,14,232,4,1,53,168,230,150,209,139,14,233,4,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,53,53,53,34,125,93,125,39,0,196,148,203,38,4,6,67,99,84,107,50,83,2,39,0,196,148,203,38,4,6,55,75,51,73,100,102,2,161,230,150,209,139,14,195,4,1,161,230,150,209,139,14,215,4,1,39,0,196,148,203,38,1,6,84,82,74,113,85,51,1,40,0,230,150,209,139,14,240,4,2,105,100,1,119,6,84,82,74,113,85,51,40,0,230,150,209,139,14,240,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,240,4,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,240,4,8,99,104,105,108,100,114,101,110,1,119,6,112,97,86,50,73,50,33,0,230,150,209,139,14,240,4,4,100,97,116,97,1,40,0,230,150,209,139,14,240,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,240,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,112,97,86,50,73,50,0,200,230,150,209,139,14,179,4,230,150,209,139,14,199,4,1,119,6,84,82,74,113,85,51,39,0,196,148,203,38,1,6,69,52,67,51,90,84,1,40,0,230,150,209,139,14,250,4,2,105,100,1,119,6,69,52,67,51,90,84,40,0,230,150,209,139,14,250,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,250,4,6,112,97,114,101,110,116,1,119,6,84,82,74,113,85,51,40,0,230,150,209,139,14,250,4,8,99,104,105,108,100,114,101,110,1,119,6,110,54,120,99,83,88,33,0,230,150,209,139,14,250,4,4,100,97,116,97,1,40,0,230,150,209,139,14,250,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,250,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,110,54,120,99,83,88,0,8,0,230,150,209,139,14,248,4,1,119,6,69,52,67,51,90,84,39,0,196,148,203,38,1,6,100,100,76,78,115,71,1,40,0,230,150,209,139,14,132,5,2,105,100,1,119,6,100,100,76,78,115,71,40,0,230,150,209,139,14,132,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,132,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,132,5,8,99,104,105,108,100,114,101,110,1,119,6,77,72,104,98,74,67,33,0,230,150,209,139,14,132,5,4,100,97,116,97,1,40,0,230,150,209,139,14,132,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,132,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,77,72,104,98,74,67,0,200,230,150,209,139,14,249,4,230,150,209,139,14,199,4,1,119,6,100,100,76,78,115,71,39,0,196,148,203,38,1,6,84,100,49,45,100,117,1,40,0,230,150,209,139,14,142,5,2,105,100,1,119,6,84,100,49,45,100,117,40,0,230,150,209,139,14,142,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,142,5,6,112,97,114,101,110,116,1,119,6,100,100,76,78,115,71,40,0,230,150,209,139,14,142,5,8,99,104,105,108,100,114,101,110,1,119,6,98,90,70,75,49,119,33,0,230,150,209,139,14,142,5,4,100,97,116,97,1,40,0,230,150,209,139,14,142,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,142,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,98,90,70,75,49,119,0,8,0,230,150,209,139,14,140,5,1,119,6,84,100,49,45,100,117,161,230,150,209,139,14,145,4,1,4,0,230,150,209,139,14,236,4,1,57,161,230,150,209,139,14,255,4,1,132,230,150,209,139,14,153,5,1,57,161,230,150,209,139,14,154,5,1,132,230,150,209,139,14,155,5,1,57,168,230,150,209,139,14,156,5,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,57,57,57,34,125,93,125,4,0,230,150,209,139,14,128,4,1,51,161,230,150,209,139,14,205,4,1,132,230,150,209,139,14,159,5,1,51,161,230,150,209,139,14,160,5,1,132,230,150,209,139,14,161,5,1,51,168,230,150,209,139,14,162,5,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,51,51,34,125,93,125,4,0,230,150,209,139,14,237,4,1,56,161,230,150,209,139,14,147,5,1,132,230,150,209,139,14,165,5,1,53,168,230,150,209,139,14,166,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,56,53,34,125,93,125,4,0,230,150,209,139,14,254,3,1,49,161,230,150,209,139,14,185,4,1,132,230,150,209,139,14,169,5,1,50,168,230,150,209,139,14,170,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,34,125,93,125,4,0,230,150,209,139,14,139,4,1,52,161,230,150,209,139,14,225,4,1,132,230,150,209,139,14,173,5,1,54,168,230,150,209,139,14,174,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,54,34,125,93,125,39,0,196,148,203,38,4,6,81,110,48,86,56,77,2,39,0,196,148,203,38,4,6,95,88,68,53,76,66,2,39,0,196,148,203,38,4,6,55,111,66,57,53,51,2,161,230,150,209,139,14,152,5,3,39,0,196,148,203,38,1,6,69,87,51,110,90,107,1,40,0,230,150,209,139,14,183,5,2,105,100,1,119,6,69,87,51,110,90,107,40,0,230,150,209,139,14,183,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,183,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,183,5,8,99,104,105,108,100,114,101,110,1,119,6,99,119,105,70,65,119,33,0,230,150,209,139,14,183,5,4,100,97,116,97,1,40,0,230,150,209,139,14,183,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,183,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,99,119,105,70,65,119,0,200,230,150,209,139,14,179,4,230,150,209,139,14,249,4,1,119,6,69,87,51,110,90,107,39,0,196,148,203,38,1,6,77,109,56,115,79,50,1,40,0,230,150,209,139,14,193,5,2,105,100,1,119,6,77,109,56,115,79,50,40,0,230,150,209,139,14,193,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,193,5,6,112,97,114,101,110,116,1,119,6,69,87,51,110,90,107,40,0,230,150,209,139,14,193,5,8,99,104,105,108,100,114,101,110,1,119,6,101,70,65,80,56,102,33,0,230,150,209,139,14,193,5,4,100,97,116,97,1,40,0,230,150,209,139,14,193,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,193,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,101,70,65,80,56,102,0,8,0,230,150,209,139,14,191,5,1,119,6,77,109,56,115,79,50,39,0,196,148,203,38,1,6,87,78,105,110,55,95,1,40,0,230,150,209,139,14,203,5,2,105,100,1,119,6,87,78,105,110,55,95,40,0,230,150,209,139,14,203,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,203,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,203,5,8,99,104,105,108,100,114,101,110,1,119,6,102,104,97,84,95,74,33,0,230,150,209,139,14,203,5,4,100,97,116,97,1,40,0,230,150,209,139,14,203,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,203,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,102,104,97,84,95,74,0,200,230,150,209,139,14,141,5,230,150,209,139,14,199,4,1,119,6,87,78,105,110,55,95,39,0,196,148,203,38,1,6,86,69,105,86,107,114,1,40,0,230,150,209,139,14,213,5,2,105,100,1,119,6,86,69,105,86,107,114,40,0,230,150,209,139,14,213,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,213,5,6,112,97,114,101,110,116,1,119,6,87,78,105,110,55,95,40,0,230,150,209,139,14,213,5,8,99,104,105,108,100,114,101,110,1,119,6,101,119,80,54,101,102,33,0,230,150,209,139,14,213,5,4,100,97,116,97,1,40,0,230,150,209,139,14,213,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,213,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,101,119,80,54,101,102,0,8,0,230,150,209,139,14,211,5,1,119,6,86,69,105,86,107,114,39,0,196,148,203,38,1,6,75,81,106,70,66,89,1,40,0,230,150,209,139,14,223,5,2,105,100,1,119,6,75,81,106,70,66,89,40,0,230,150,209,139,14,223,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,230,150,209,139,14,223,5,6,112,97,114,101,110,116,1,119,6,45,45,112,51,53,121,40,0,230,150,209,139,14,223,5,8,99,104,105,108,100,114,101,110,1,119,6,55,71,56,98,84,78,33,0,230,150,209,139,14,223,5,4,100,97,116,97,1,40,0,230,150,209,139,14,223,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,223,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,55,71,56,98,84,78,0,136,230,150,209,139,14,219,4,1,119,6,75,81,106,70,66,89,39,0,196,148,203,38,1,6,103,115,109,113,111,48,1,40,0,230,150,209,139,14,233,5,2,105,100,1,119,6,103,115,109,113,111,48,40,0,230,150,209,139,14,233,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,233,5,6,112,97,114,101,110,116,1,119,6,75,81,106,70,66,89,40,0,230,150,209,139,14,233,5,8,99,104,105,108,100,114,101,110,1,119,6,77,54,56,53,90,83,33,0,230,150,209,139,14,233,5,4,100,97,116,97,1,40,0,230,150,209,139,14,233,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,233,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,77,54,56,53,90,83,0,8,0,230,150,209,139,14,231,5,1,119,6,103,115,109,113,111,48,4,0,230,150,209,139,14,178,5,1,57,161,230,150,209,139,14,218,5,1,132,230,150,209,139,14,243,5,1,57,168,230,150,209,139,14,244,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,57,57,34,125,93,125,4,0,230,150,209,139,14,177,5,1,50,161,230,150,209,139,14,198,5,1,132,230,150,209,139,14,247,5,1,50,168,230,150,209,139,14,248,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,50,34,125,93,125,4,0,230,150,209,139,14,179,5,1,52,161,230,150,209,139,14,238,5,1,132,230,150,209,139,14,251,5,1,52,168,230,150,209,139,14,252,5,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,52,34,125,93,125,39,0,196,148,203,38,4,6,84,81,118,97,82,53,2,39,0,196,148,203,38,1,6,53,90,85,81,98,56,1,40,0,230,150,209,139,14,128,6,2,105,100,1,119,6,53,90,85,81,98,56,40,0,230,150,209,139,14,128,6,2,116,121,1,119,7,99,97,108,108,111,117,116,40,0,230,150,209,139,14,128,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,128,6,8,99,104,105,108,100,114,101,110,1,119,6,105,84,113,54,95,110,33,0,230,150,209,139,14,128,6,4,100,97,116,97,1,40,0,230,150,209,139,14,128,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,128,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,105,84,113,54,95,110,0,136,230,150,209,139,14,251,3,1,119,6,53,90,85,81,98,56,4,0,230,150,209,139,14,255,5,1,100,161,230,150,209,139,14,133,6,1,132,230,150,209,139,14,138,6,1,100,161,230,150,209,139,14,139,6,1,132,230,150,209,139,14,140,6,1,100,161,230,150,209,139,14,141,6,1,132,230,150,209,139,14,142,6,1,115,161,230,150,209,139,14,143,6,1,132,230,150,209,139,14,144,6,1,97,161,230,150,209,139,14,145,6,1,168,230,150,209,139,14,155,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,125,168,230,150,209,139,14,245,4,1,119,60,123,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,104,101,105,103,104,116,34,58,51,55,46,48,125,168,230,150,209,139,14,238,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,125,161,230,150,209,139,14,182,5,1,168,230,150,209,139,14,175,4,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,168,230,150,209,139,14,137,5,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,168,230,150,209,139,14,239,4,1,119,60,123,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,161,230,150,209,139,14,151,6,1,168,230,150,209,139,14,188,5,1,119,60,123,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,44,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,125,168,230,150,209,139,14,208,5,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,125,168,230,150,209,139,14,228,5,1,119,60,123,34,104,101,105,103,104,116,34,58,51,55,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,56,48,46,48,125,161,230,150,209,139,14,155,6,1,168,230,150,209,139,14,159,6,1,119,114,123,34,99,111,108,77,105,110,105,109,117,109,87,105,100,116,104,34,58,52,48,46,48,44,34,99,111,108,115,72,101,105,103,104,116,34,58,49,49,57,46,48,44,34,99,111,108,115,76,101,110,34,58,51,44,34,114,111,119,115,76,101,110,34,58,51,44,34,114,111,119,68,101,102,97,117,108,116,72,101,105,103,104,116,34,58,52,48,46,48,44,34,99,111,108,68,101,102,97,117,108,116,87,105,100,116,104,34,58,56,48,46,48,125,39,0,196,148,203,38,4,6,68,98,116,65,114,106,2,4,0,230,150,209,139,14,161,6,6,100,100,100,115,97,50,161,230,150,209,139,14,147,6,1,132,230,150,209,139,14,167,6,1,50,168,230,150,209,139,14,168,6,1,119,69,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,100,100,100,115,97,50,50,34,125,93,44,34,105,99,111,110,34,58,34,240,159,147,140,34,44,34,98,103,67,111,108,111,114,34,58,34,48,120,102,102,102,50,102,50,102,50,34,125,39,0,196,148,203,38,4,6,66,116,102,84,86,95,2,39,0,196,148,203,38,1,6,103,73,113,80,112,117,1,40,0,230,150,209,139,14,172,6,2,105,100,1,119,6,103,73,113,80,112,117,40,0,230,150,209,139,14,172,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,172,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,172,6,8,99,104,105,108,100,114,101,110,1,119,6,108,52,49,118,71,70,40,0,230,150,209,139,14,172,6,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,230,150,209,139,14,172,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,172,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,108,52,49,118,71,70,0,200,230,150,209,139,14,251,3,230,150,209,139,14,137,6,1,119,6,103,73,113,80,112,117,39,0,196,148,203,38,4,6,86,52,45,51,122,45,2,39,0,196,148,203,38,1,6,119,122,102,78,72,69,1,40,0,230,150,209,139,14,183,6,2,105,100,1,119,6,119,122,102,78,72,69,40,0,230,150,209,139,14,183,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,230,150,209,139,14,183,6,6,112,97,114,101,110,116,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,230,150,209,139,14,183,6,8,99,104,105,108,100,114,101,110,1,119,6,100,110,56,106,114,112,40,0,230,150,209,139,14,183,6,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,230,150,209,139,14,183,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,230,150,209,139,14,183,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,6,100,110,56,106,114,112,0,200,230,150,209,139,14,230,3,230,150,209,139,14,149,4,1,119,6,119,122,102,78,72,69,9,248,208,217,159,7,0,0,2,0,1,0,3,0,1,0,3,0,1,0,3,0,1,0,1,1,211,142,141,147,5,0,161,226,250,246,177,3,16,8,1,226,250,246,177,3,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,17,2,167,238,246,72,0,161,211,142,141,147,5,7,85,168,167,238,246,72,84,1,122,0,0,0,0,102,78,252,249,22,196,148,203,38,0,39,1,4,100,97,116,97,8,100,111,99,117,109,101,110,116,1,39,0,196,148,203,38,0,6,98,108,111,99,107,115,1,39,0,196,148,203,38,0,4,109,101,116,97,1,39,0,196,148,203,38,2,12,99,104,105,108,100,114,101,110,95,109,97,112,1,39,0,196,148,203,38,2,8,116,101,120,116,95,109,97,112,1,40,0,196,148,203,38,0,7,112,97,103,101,95,105,100,1,119,10,74,118,87,74,108,105,53,79,117,84,39,0,196,148,203,38,1,10,74,118,87,74,108,105,53,79,117,84,1,40,0,196,148,203,38,6,2,105,100,1,119,10,74,118,87,74,108,105,53,79,117,84,40,0,196,148,203,38,6,2,116,121,1,119,4,112,97,103,101,40,0,196,148,203,38,6,6,112,97,114,101,110,116,1,119,0,40,0,196,148,203,38,6,8,99,104,105,108,100,114,101,110,1,119,10,50,121,100,104,90,109,56,81,67,117,40,0,196,148,203,38,6,4,100,97,116,97,1,119,2,123,125,40,0,196,148,203,38,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,196,148,203,38,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,196,148,203,38,3,10,50,121,100,104,90,109,56,81,67,117,0,33,0,196,148,203,38,1,10,69,115,119,105,88,82,80,121,106,72,1,0,5,0,1,0,1,33,0,196,148,203,38,3,10,76,119,103,107,118,65,79,53,66,103,1,1,0,196,148,203,38,14,1,33,0,196,148,203,38,4,10,104,115,108,107,104,97,102,49,51,111,1,6,248,208,217,159,7,1,0,16,226,250,246,177,3,1,0,17,211,142,141,147,5,1,0,8,196,148,203,38,1,15,11,230,150,209,139,14,121,0,8,18,1,24,10,36,1,38,1,40,1,42,1,49,1,55,1,57,1,59,1,61,1,68,1,74,11,89,19,109,1,111,1,118,1,124,1,126,1,128,1,1,130,1,10,141,1,9,151,1,1,158,1,1,164,1,1,166,1,1,170,1,10,181,1,1,183,1,1,190,1,1,196,1,1,198,1,1,200,1,1,207,1,1,214,1,1,216,1,1,220,1,10,236,1,1,242,1,1,244,1,1,248,1,10,136,2,1,142,2,1,144,2,1,148,2,10,159,2,13,173,2,10,184,2,10,195,2,1,202,2,1,208,2,1,210,2,1,214,2,10,225,2,1,232,2,1,238,2,1,240,2,1,242,2,1,244,2,1,251,2,1,130,3,1,132,3,1,134,3,1,138,3,10,154,3,1,160,3,1,162,3,1,166,3,10,177,3,10,188,3,10,199,3,10,210,3,10,221,3,10,236,3,1,242,3,10,129,4,10,145,4,1,155,4,1,165,4,1,175,4,1,185,4,1,195,4,1,205,4,1,215,4,1,225,4,1,231,4,1,233,4,1,238,4,2,245,4,1,255,4,1,137,5,1,147,5,1,152,5,1,154,5,1,156,5,1,160,5,1,162,5,1,166,5,1,170,5,1,174,5,1,180,5,3,188,5,1,198,5,1,208,5,1,218,5,1,228,5,1,238,5,1,244,5,1,248,5,1,252,5,1,133,6,1,139,6,1,141,6,1,143,6,1,145,6,1,147,6,1,151,6,1,155,6,1,159,6,1,168,6,1,167,238,246,72,1,0,85],"version":0,"object_id":"f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/folder.json b/frontend/appflowy_web_app/cypress/fixtures/folder.json new file mode 100644 index 0000000000..2b1a53a989 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/folder.json @@ -0,0 +1 @@ +{"data":{"state_vector":[160,1,128,252,161,128,4,33,130,180,254,251,6,18,135,232,133,203,9,15,135,240,136,178,10,45,137,226,192,199,6,20,138,202,240,189,2,134,1,139,240,196,145,14,138,2,139,152,215,249,10,34,141,216,158,150,1,3,142,130,192,134,11,3,143,184,153,180,6,6,140,228,230,243,1,2,145,190,137,224,10,2,145,144,146,185,5,12,140,152,206,145,6,165,1,135,166,246,235,6,3,147,206,229,235,1,3,137,164,190,210,1,39,152,158,185,230,10,7,153,130,203,161,6,97,152,252,186,192,1,2,154,244,246,165,8,198,1,156,148,170,169,10,16,157,240,144,231,2,3,158,156,181,152,10,49,158,182,250,251,9,15,160,192,253,131,5,3,161,178,132,150,11,101,159,156,204,250,6,27,163,236,177,169,4,12,164,188,201,172,1,3,158,184,218,165,3,38,162,238,198,212,7,3,170,140,240,234,9,31,171,204,155,217,8,3,171,142,166,254,1,6,173,252,148,184,13,64,176,154,159,227,1,20,178,162,190,217,10,3,179,252,154,44,3,180,230,210,212,13,7,182,172,247,194,5,3,183,226,184,158,8,8,187,220,199,239,8,70,195,242,227,194,8,3,196,154,250,183,6,62,197,254,154,201,10,3,200,142,208,241,4,157,1,202,160,246,212,1,6,203,184,221,173,11,37,206,220,129,131,4,21,207,228,238,162,8,50,209,250,203,254,15,3,210,228,153,221,12,3,211,202,217,232,12,7,211,166,203,229,4,195,1,214,168,149,214,3,15,219,220,239,171,8,10,225,248,138,176,2,49,226,212,179,248,2,15,229,154,128,197,12,24,234,182,182,157,9,8,235,178,165,206,5,72,234,156,130,211,12,12,241,130,161,205,7,12,244,226,228,149,2,30,245,220,194,52,39,247,200,243,247,14,100,248,136,168,181,1,3,248,210,237,129,13,2,250,198,166,187,7,2,248,196,187,185,10,31,252,218,241,167,14,3,255,140,248,220,6,3,128,211,179,216,2,10,133,159,138,205,12,131,1,135,167,156,250,14,16,135,193,208,135,7,16,141,245,194,142,11,7,141,205,220,149,4,3,143,131,148,152,6,15,141,171,170,217,4,2,145,159,164,217,14,3,149,129,169,191,12,16,149,249,242,175,4,31,149,189,189,215,8,3,149,161,132,184,14,221,2,154,243,157,196,14,12,154,193,208,134,10,9,155,165,205,152,11,3,154,235,215,240,4,13,155,159,180,195,15,6,157,207,243,216,6,2,161,239,241,154,13,106,162,159,252,196,11,3,164,155,139,169,7,35,165,139,157,171,15,103,164,203,250,235,13,7,167,131,133,162,9,11,166,201,221,141,13,72,169,197,188,221,3,3,170,255,211,105,38,166,203,155,46,6,173,187,245,170,14,46,174,151,139,93,179,2,175,225,172,150,8,11,175,147,217,214,1,33,177,161,136,243,11,10,178,203,205,182,4,1,175,205,156,228,6,10,180,205,189,133,13,20,181,175,219,209,12,10,182,143,233,195,4,111,177,219,160,167,7,4,184,201,188,172,10,3,184,231,170,67,6,186,197,166,179,15,7,187,173,214,176,15,3,188,171,136,250,8,21,188,237,223,145,6,26,190,139,191,155,1,2,191,157,147,233,9,32,193,249,142,142,4,17,198,189,216,175,6,23,200,205,214,172,10,30,201,129,238,197,4,84,201,191,159,147,14,17,200,203,236,184,2,35,200,159,185,206,9,8,205,149,231,236,11,68,213,161,242,209,13,35,214,139,213,136,8,66,213,255,156,145,1,2,219,227,140,137,6,34,221,147,167,147,15,177,2,222,205,223,235,7,31,223,209,193,147,11,81,227,209,197,253,2,14,229,153,197,202,7,241,6,231,139,244,188,8,13,232,207,157,148,2,2,233,165,139,246,14,43,233,247,183,159,1,4,235,225,184,133,10,3,234,153,236,158,4,73,234,187,164,181,1,24,231,189,134,196,8,77,239,199,189,146,3,24,240,149,229,225,6,15,241,155,213,233,1,52,240,253,240,229,1,79,243,239,182,181,13,30,236,229,225,232,8,116,246,185,174,192,6,90,248,153,216,10,3,251,189,220,155,14,3,252,163,130,200,6,30,253,205,145,137,11,10,252,171,209,175,15,4,255,255,147,249,10,3],"doc_state":[160,1,3,209,250,203,254,15,0,161,226,212,179,248,2,9,1,161,226,212,179,248,2,10,1,136,174,151,139,93,249,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,78,2,155,159,180,195,15,0,161,198,189,216,175,6,22,1,161,253,205,145,137,11,9,5,7,186,197,166,179,15,0,161,161,178,132,150,11,98,1,161,161,178,132,150,11,99,1,129,161,178,132,150,11,100,1,161,161,178,132,150,11,97,1,161,161,178,132,150,11,94,1,161,161,178,132,150,11,95,1,129,186,197,166,179,15,2,1,3,187,173,214,176,15,0,168,244,226,228,149,2,27,1,122,4,56,115,160,190,64,16,0,168,244,226,228,149,2,28,1,122,0,0,0,0,102,32,153,171,136,248,153,216,10,2,1,118,2,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,32,153,171,1,252,171,209,175,15,0,161,128,211,179,216,2,9,4,103,165,139,157,171,15,0,8,0,201,129,238,197,4,52,1,118,1,2,105,100,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,168,201,129,238,197,4,51,1,122,0,0,0,0,102,74,244,14,161,170,140,240,234,9,22,1,39,0,203,184,221,173,11,1,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,1,40,0,165,139,157,171,15,3,2,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,165,139,157,171,15,3,4,110,97,109,101,1,119,14,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,165,139,157,171,15,3,3,98,105,100,1,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,40,0,165,139,157,171,15,3,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,165,139,157,171,15,3,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,0,40,0,165,139,157,171,15,3,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,165,139,157,171,15,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,165,139,157,171,15,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,170,140,240,234,9,21,1,161,165,139,157,171,15,2,1,129,159,156,204,250,6,26,1,136,225,248,138,176,2,17,1,118,1,2,105,100,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,161,225,248,138,176,2,18,1,161,193,249,142,142,4,8,1,39,0,203,184,221,173,11,1,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,1,40,0,165,139,157,171,15,21,2,105,100,1,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,40,0,165,139,157,171,15,21,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,21,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,165,139,157,171,15,21,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,21,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,165,139,157,171,15,21,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,0,40,0,165,139,157,171,15,21,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,21,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,165,139,157,171,15,21,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,165,139,157,171,15,21,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,193,249,142,142,4,13,1,161,165,139,157,171,15,32,1,161,165,139,157,171,15,31,1,129,165,139,157,171,15,17,1,8,0,165,139,157,171,15,28,1,118,1,2,105,100,119,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,168,165,139,157,171,15,27,1,122,0,0,0,0,102,74,246,67,161,165,139,157,171,15,35,1,39,0,203,184,221,173,11,1,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,1,40,0,165,139,157,171,15,40,2,105,100,1,119,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,40,0,165,139,157,171,15,40,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,40,3,98,105,100,1,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,40,0,165,139,157,171,15,40,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,40,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,165,139,157,171,15,40,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,74,246,67,39,0,203,184,221,173,11,4,36,51,53,48,102,52,50,53,98,45,98,54,55,49,45,52,101,50,100,45,56,49,56,50,45,53,57,57,56,97,54,101,54,50,57,50,52,0,40,0,165,139,157,171,15,40,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,40,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,165,139,157,171,15,40,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,74,246,67,40,0,165,139,157,171,15,40,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,165,139,157,171,15,34,1,161,165,139,157,171,15,39,1,129,165,139,157,171,15,36,1,161,176,154,159,227,1,17,1,161,176,154,159,227,1,18,1,40,0,176,154,159,227,1,4,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,165,139,157,171,15,55,1,161,165,139,157,171,15,56,1,129,165,139,157,171,15,54,1,161,165,139,157,171,15,52,1,161,165,139,157,171,15,53,1,129,165,139,157,171,15,60,1,161,165,139,157,171,15,61,1,161,165,139,157,171,15,62,1,129,165,139,157,171,15,63,1,161,165,139,157,171,15,64,1,161,165,139,157,171,15,65,1,129,165,139,157,171,15,66,1,8,0,225,248,138,176,2,27,1,118,1,2,105,100,119,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,168,225,248,138,176,2,26,1,122,0,0,0,0,102,74,248,249,161,225,248,138,176,2,40,1,39,0,203,184,221,173,11,1,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,1,40,0,165,139,157,171,15,73,2,105,100,1,119,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,40,0,165,139,157,171,15,73,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,165,139,157,171,15,73,3,98,105,100,1,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,40,0,165,139,157,171,15,73,4,100,101,115,99,1,119,0,40,0,165,139,157,171,15,73,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,165,139,157,171,15,73,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,74,248,249,39,0,203,184,221,173,11,4,36,57,49,101,97,55,99,48,56,45,102,54,98,51,45,52,98,56,49,45,97,97,49,101,45,100,51,54,54,52,54,56,54,49,56,54,102,0,40,0,165,139,157,171,15,73,4,105,99,111,110,1,119,0,40,0,165,139,157,171,15,73,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,165,139,157,171,15,73,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,74,248,249,40,0,165,139,157,171,15,73,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,225,248,138,176,2,39,1,161,165,139,157,171,15,72,1,129,165,139,157,171,15,69,1,161,165,139,157,171,15,67,1,161,165,139,157,171,15,68,1,129,165,139,157,171,15,87,1,161,165,139,157,171,15,85,1,161,165,139,157,171,15,86,1,129,165,139,157,171,15,90,1,161,159,156,204,250,6,24,1,161,159,156,204,250,6,25,1,129,165,139,157,171,15,93,1,161,165,139,157,171,15,15,1,161,165,139,157,171,15,16,1,129,165,139,157,171,15,96,1,161,165,139,157,171,15,97,1,161,165,139,157,171,15,98,1,129,165,139,157,171,15,99,1,1,221,147,167,147,15,0,161,139,240,196,145,14,137,2,177,2,1,135,167,156,250,14,0,161,188,237,223,145,6,21,16,100,247,200,243,247,14,0,136,140,152,206,145,6,100,1,118,1,2,105,100,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,161,140,152,206,145,6,101,1,161,245,220,194,52,7,1,39,0,203,184,221,173,11,1,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,1,40,0,247,200,243,247,14,3,2,105,100,1,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,33,0,247,200,243,247,14,3,4,110,97,109,101,1,40,0,247,200,243,247,14,3,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,247,200,243,247,14,3,4,100,101,115,99,1,119,0,40,0,247,200,243,247,14,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,247,200,243,247,14,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,86,193,79,39,0,203,184,221,173,11,4,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,0,40,0,247,200,243,247,14,3,4,105,99,111,110,1,119,0,40,0,247,200,243,247,14,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,247,200,243,247,14,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,247,200,243,247,14,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,170,255,211,105,34,1,161,247,200,243,247,14,14,1,161,247,200,243,247,14,13,1,129,135,166,246,235,6,2,1,161,247,200,243,247,14,16,1,161,247,200,243,247,14,17,1,40,0,247,200,243,247,14,3,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,247,200,243,247,14,19,1,161,247,200,243,247,14,20,1,161,247,200,243,247,14,5,1,161,247,200,243,247,14,22,1,161,247,200,243,247,14,23,1,161,247,200,243,247,14,24,1,161,247,200,243,247,14,25,1,161,247,200,243,247,14,26,1,161,247,200,243,247,14,27,1,161,247,200,243,247,14,28,1,161,247,200,243,247,14,29,1,161,247,200,243,247,14,30,1,161,247,200,243,247,14,31,1,161,247,200,243,247,14,32,1,161,247,200,243,247,14,33,1,161,247,200,243,247,14,34,1,161,247,200,243,247,14,35,1,161,247,200,243,247,14,36,1,161,247,200,243,247,14,37,1,161,247,200,243,247,14,38,1,161,247,200,243,247,14,39,1,161,247,200,243,247,14,40,1,161,247,200,243,247,14,41,1,161,247,200,243,247,14,42,1,161,247,200,243,247,14,43,1,161,247,200,243,247,14,44,1,161,247,200,243,247,14,45,1,161,247,200,243,247,14,46,1,161,247,200,243,247,14,47,1,161,247,200,243,247,14,48,1,161,247,200,243,247,14,49,1,161,247,200,243,247,14,50,1,168,247,200,243,247,14,51,1,119,10,116,101,115,116,32,101,118,101,110,116,161,247,200,243,247,14,52,1,161,247,200,243,247,14,53,1,129,247,200,243,247,14,18,1,161,166,201,221,141,13,41,1,161,166,201,221,141,13,42,1,161,166,201,221,141,13,43,1,161,247,200,243,247,14,58,1,161,247,200,243,247,14,59,1,161,247,200,243,247,14,60,1,161,247,200,243,247,14,61,1,161,247,200,243,247,14,62,1,161,247,200,243,247,14,63,1,161,247,200,243,247,14,64,1,161,247,200,243,247,14,65,1,161,247,200,243,247,14,66,1,161,247,200,243,247,14,67,1,161,247,200,243,247,14,68,1,161,247,200,243,247,14,69,1,161,247,200,243,247,14,70,1,161,247,200,243,247,14,71,1,161,247,200,243,247,14,72,1,161,247,200,243,247,14,73,1,161,247,200,243,247,14,74,1,161,247,200,243,247,14,75,1,161,247,200,243,247,14,76,1,161,247,200,243,247,14,77,1,161,247,200,243,247,14,78,1,161,247,200,243,247,14,79,1,161,247,200,243,247,14,80,1,161,247,200,243,247,14,81,1,161,247,200,243,247,14,82,1,161,247,200,243,247,14,83,1,161,247,200,243,247,14,84,1,161,247,200,243,247,14,85,1,161,247,200,243,247,14,86,1,161,247,200,243,247,14,87,1,161,247,200,243,247,14,88,1,161,247,200,243,247,14,89,1,161,247,200,243,247,14,90,1,161,247,200,243,247,14,91,1,161,247,200,243,247,14,92,1,161,247,200,243,247,14,93,1,168,247,200,243,247,14,94,1,122,4,56,115,160,190,64,16,0,168,247,200,243,247,14,95,1,122,0,0,0,0,102,87,4,247,168,247,200,243,247,14,96,1,119,145,1,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,103,114,97,100,105,101,110,116,34,44,34,118,97,108,117,101,34,58,34,97,112,112,102,108,111,119,121,95,116,104,101,109,95,99,111,108,111,114,95,103,114,97,100,105,101,110,116,55,34,125,44,34,102,111,110,116,95,108,97,121,111,117,116,34,58,34,110,111,114,109,97,108,34,44,34,108,105,110,101,95,104,101,105,103,104,116,95,108,97,121,111,117,116,34,58,34,110,111,114,109,97,108,34,44,34,102,111,110,116,34,58,34,65,68,76,97,77,32,68,105,115,112,108,97,121,34,125,42,233,165,139,246,14,0,161,203,184,221,173,11,34,1,161,203,184,221,173,11,33,1,39,0,203,184,221,173,11,6,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,1,0,233,165,139,246,14,2,1,161,233,165,139,246,14,0,1,161,233,165,139,246,14,1,1,129,233,165,139,246,14,3,1,72,203,184,221,173,11,16,1,118,1,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,161,203,184,221,173,11,21,1,161,203,184,221,173,11,22,1,39,0,203,184,221,173,11,1,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,1,40,0,233,165,139,246,14,10,2,105,100,1,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,40,0,233,165,139,246,14,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,233,165,139,246,14,10,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,233,165,139,246,14,10,4,100,101,115,99,1,119,0,40,0,233,165,139,246,14,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,233,165,139,246,14,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,120,204,39,0,203,184,221,173,11,4,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,0,40,0,233,165,139,246,14,10,4,105,99,111,110,1,119,0,40,0,233,165,139,246,14,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,233,165,139,246,14,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,233,165,139,246,14,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,233,165,139,246,14,20,1,129,233,165,139,246,14,6,1,161,233,165,139,246,14,22,1,161,233,165,139,246,14,23,1,129,233,165,139,246,14,24,1,161,233,165,139,246,14,25,1,161,233,165,139,246,14,26,1,129,233,165,139,246,14,27,1,161,233,165,139,246,14,28,1,161,233,165,139,246,14,29,1,129,233,165,139,246,14,30,1,161,233,165,139,246,14,31,1,161,233,165,139,246,14,32,1,129,233,165,139,246,14,33,1,161,233,165,139,246,14,34,1,161,233,165,139,246,14,35,1,129,233,165,139,246,14,36,1,161,233,165,139,246,14,37,1,161,233,165,139,246,14,38,1,129,233,165,139,246,14,39,1,3,145,159,164,217,14,0,161,135,232,133,203,9,12,1,161,135,232,133,203,9,13,1,129,135,232,133,203,9,14,1,12,154,243,157,196,14,0,161,145,159,164,217,14,0,1,161,145,159,164,217,14,1,1,129,145,159,164,217,14,2,1,161,154,243,157,196,14,0,1,161,154,243,157,196,14,1,1,129,154,243,157,196,14,2,1,161,154,243,157,196,14,3,1,161,154,243,157,196,14,4,1,129,154,243,157,196,14,5,1,161,154,243,157,196,14,6,1,161,154,243,157,196,14,7,1,129,154,243,157,196,14,8,1,2,149,161,132,184,14,0,161,235,178,165,206,5,68,219,2,161,149,161,132,184,14,218,2,2,1,173,187,245,170,14,0,161,164,203,250,235,13,6,46,3,252,218,241,167,14,0,161,251,189,220,155,14,0,1,161,251,189,220,155,14,1,1,168,251,189,220,155,14,2,1,119,9,231,186,170,229,191,181,231,137,136,3,251,189,220,155,14,0,161,210,228,153,221,12,0,1,161,210,228,153,221,12,1,1,161,210,228,153,221,12,2,1,17,201,191,159,147,14,0,161,149,189,189,215,8,0,1,161,149,189,189,215,8,1,1,129,149,189,189,215,8,2,1,161,213,161,242,209,13,31,1,161,133,159,138,205,12,124,1,161,133,159,138,205,12,125,1,129,201,191,159,147,14,2,1,161,201,191,159,147,14,4,1,161,201,191,159,147,14,5,1,129,201,191,159,147,14,6,1,161,201,191,159,147,14,0,1,161,201,191,159,147,14,1,1,129,201,191,159,147,14,9,1,161,201,191,159,147,14,3,1,161,201,191,159,147,14,10,1,161,201,191,159,147,14,11,1,129,201,191,159,147,14,12,1,1,139,240,196,145,14,0,161,200,203,236,184,2,34,138,2,1,164,203,250,235,13,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,7,7,180,230,210,212,13,0,161,175,225,172,150,8,8,1,161,175,225,172,150,8,9,1,136,175,225,172,150,8,10,1,118,2,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,41,200,235,161,175,225,172,150,8,7,1,161,175,225,172,150,8,4,1,161,175,225,172,150,8,5,1,129,180,230,210,212,13,2,1,34,213,161,242,209,13,0,161,133,159,138,205,12,128,1,1,161,133,159,138,205,12,129,1,1,129,133,159,138,205,12,130,1,1,136,133,159,138,205,12,108,1,118,1,2,105,100,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,161,133,159,138,205,12,109,1,161,213,161,242,209,13,1,1,39,0,203,184,221,173,11,1,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,1,40,0,213,161,242,209,13,6,2,105,100,1,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,33,0,213,161,242,209,13,6,4,110,97,109,101,1,40,0,213,161,242,209,13,6,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,213,161,242,209,13,6,4,100,101,115,99,1,119,0,40,0,213,161,242,209,13,6,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,213,161,242,209,13,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,178,5,39,0,203,184,221,173,11,4,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,0,40,0,213,161,242,209,13,6,4,105,99,111,110,1,119,0,40,0,213,161,242,209,13,6,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,213,161,242,209,13,6,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,213,161,242,209,13,6,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,213,161,242,209,13,16,1,161,213,161,242,209,13,8,1,161,213,161,242,209,13,18,1,161,213,161,242,209,13,19,1,168,213,161,242,209,13,20,1,119,4,71,114,105,100,168,213,161,242,209,13,21,1,122,4,56,115,160,190,64,16,0,168,213,161,242,209,13,22,1,122,0,0,0,0,102,48,178,16,136,152,158,185,230,10,6,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,48,178,16,2,105,100,119,36,55,101,98,54,57,55,99,100,45,54,97,53,53,45,52,48,98,98,45,57,54,97,99,45,48,100,52,97,51,98,99,57,50,52,98,50,161,133,159,138,205,12,127,1,161,133,159,138,205,12,53,1,161,133,159,138,205,12,58,1,129,213,161,242,209,13,2,1,161,213,161,242,209,13,27,1,161,213,161,242,209,13,0,1,161,213,161,242,209,13,5,1,129,213,161,242,209,13,30,1,64,173,252,148,184,13,0,161,156,148,170,169,10,12,1,161,156,148,170,169,10,9,1,161,156,148,170,169,10,10,1,129,156,148,170,169,10,15,1,161,173,252,148,184,13,0,1,161,164,155,139,169,7,0,1,161,164,155,139,169,7,1,1,129,164,155,139,169,7,34,1,161,173,252,148,184,13,4,1,161,161,178,132,150,11,58,1,161,161,178,132,150,11,59,1,129,173,252,148,184,13,7,1,161,173,252,148,184,13,8,1,161,164,155,139,169,7,32,1,161,164,155,139,169,7,33,1,129,173,252,148,184,13,11,1,161,173,252,148,184,13,12,1,161,164,155,139,169,7,8,1,161,164,155,139,169,7,9,1,129,173,252,148,184,13,15,1,161,173,252,148,184,13,16,1,161,173,252,148,184,13,13,1,161,173,252,148,184,13,14,1,129,173,252,148,184,13,19,1,161,173,252,148,184,13,20,1,161,173,252,148,184,13,9,1,161,173,252,148,184,13,10,1,129,173,252,148,184,13,23,1,161,173,252,148,184,13,24,1,161,173,252,148,184,13,21,1,161,173,252,148,184,13,22,1,129,173,252,148,184,13,27,1,161,173,252,148,184,13,28,1,161,164,155,139,169,7,28,1,161,164,155,139,169,7,29,1,129,173,252,148,184,13,31,1,161,173,252,148,184,13,32,1,161,173,252,148,184,13,29,1,161,173,252,148,184,13,30,1,129,173,252,148,184,13,35,1,136,207,228,238,162,8,22,1,118,1,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,161,173,252,148,184,13,33,1,161,173,252,148,184,13,34,1,168,130,180,254,251,6,6,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,173,252,148,184,13,36,1,161,173,252,148,184,13,41,1,161,173,252,148,184,13,42,1,129,173,252,148,184,13,39,1,161,173,252,148,184,13,44,1,161,173,252,148,184,13,37,1,161,173,252,148,184,13,38,1,129,173,252,148,184,13,47,1,161,173,252,148,184,13,48,1,161,173,252,148,184,13,25,1,161,173,252,148,184,13,26,1,129,173,252,148,184,13,51,1,161,173,252,148,184,13,52,1,161,173,252,148,184,13,49,1,161,173,252,148,184,13,50,1,129,173,252,148,184,13,55,1,161,173,252,148,184,13,56,1,161,173,252,148,184,13,5,1,161,173,252,148,184,13,6,1,129,173,252,148,184,13,59,1,30,243,239,182,181,13,0,136,240,149,229,225,6,0,1,118,1,2,105,100,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,161,240,149,229,225,6,1,1,161,240,149,229,225,6,2,1,39,0,203,184,221,173,11,1,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,1,40,0,243,239,182,181,13,3,2,105,100,1,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,40,0,243,239,182,181,13,3,4,110,97,109,101,1,119,0,40,0,243,239,182,181,13,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,243,239,182,181,13,3,4,100,101,115,99,1,119,0,40,0,243,239,182,181,13,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,243,239,182,181,13,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,141,103,39,0,203,184,221,173,11,4,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,0,40,0,243,239,182,181,13,3,4,105,99,111,110,1,119,0,40,0,243,239,182,181,13,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,243,239,182,181,13,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,141,103,40,0,243,239,182,181,13,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,243,239,182,181,13,0,1,118,1,2,105,100,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,161,243,239,182,181,13,1,1,161,243,239,182,181,13,2,1,39,0,203,184,221,173,11,1,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,1,40,0,243,239,182,181,13,18,2,105,100,1,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,40,0,243,239,182,181,13,18,4,110,97,109,101,1,119,0,40,0,243,239,182,181,13,18,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,243,239,182,181,13,18,4,100,101,115,99,1,119,0,40,0,243,239,182,181,13,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,243,239,182,181,13,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,141,156,39,0,203,184,221,173,11,4,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,0,40,0,243,239,182,181,13,18,4,105,99,111,110,1,119,0,40,0,243,239,182,181,13,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,243,239,182,181,13,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,141,156,40,0,243,239,182,181,13,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,1,161,239,241,154,13,0,161,158,184,218,165,3,37,106,72,166,201,221,141,13,0,161,182,172,247,194,5,0,1,161,182,172,247,194,5,1,1,129,182,172,247,194,5,2,1,161,170,255,211,105,34,1,161,245,220,194,52,9,1,161,245,220,194,52,10,1,136,166,201,221,141,13,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,132,208,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,166,201,221,141,13,4,1,161,166,201,221,141,13,5,1,136,166,201,221,141,13,6,1,118,2,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,132,209,161,166,201,221,141,13,7,1,161,166,201,221,141,13,8,1,136,166,201,221,141,13,9,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,134,151,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,166,201,221,141,13,10,1,161,166,201,221,141,13,11,1,136,166,201,221,141,13,12,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,134,152,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,39,0,203,184,221,173,11,1,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,1,40,0,166,201,221,141,13,16,2,105,100,1,119,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,40,0,166,201,221,141,13,16,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,166,201,221,141,13,16,3,98,105,100,1,119,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,40,0,166,201,221,141,13,16,4,100,101,115,99,1,119,0,40,0,166,201,221,141,13,16,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,166,201,221,141,13,16,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,85,140,82,39,0,203,184,221,173,11,4,36,98,98,101,55,102,100,54,99,45,99,99,56,102,45,53,48,55,57,45,98,48,52,53,45,54,49,57,55,52,99,48,57,57,98,54,100,0,40,0,166,201,221,141,13,16,4,105,99,111,110,1,119,0,40,0,166,201,221,141,13,16,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,166,201,221,141,13,16,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,166,201,221,141,13,16,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,166,201,221,141,13,27,1,122,4,56,115,160,190,64,16,0,168,166,201,221,141,13,26,1,122,0,0,0,0,102,85,140,82,40,0,166,201,221,141,13,16,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,166,201,221,141,13,0,1,161,166,201,221,141,13,1,1,136,166,201,221,141,13,15,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,167,96,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,166,201,221,141,13,3,1,161,166,201,221,141,13,31,1,161,166,201,221,141,13,32,1,136,166,201,221,141,13,33,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,167,97,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,166,201,221,141,13,35,1,161,166,201,221,141,13,36,1,161,245,220,194,52,38,1,161,166,201,221,141,13,38,1,161,166,201,221,141,13,39,1,161,166,201,221,141,13,40,1,136,140,152,206,145,6,100,1,118,1,2,105,100,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,161,140,152,206,145,6,101,1,161,245,220,194,52,7,1,39,0,203,184,221,173,11,1,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,1,40,0,166,201,221,141,13,47,2,105,100,1,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,40,0,166,201,221,141,13,47,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,166,201,221,141,13,47,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,166,201,221,141,13,47,4,100,101,115,99,1,119,0,40,0,166,201,221,141,13,47,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,166,201,221,141,13,47,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,86,193,40,39,0,203,184,221,173,11,4,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,0,40,0,166,201,221,141,13,47,4,105,99,111,110,1,119,0,40,0,166,201,221,141,13,47,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,166,201,221,141,13,47,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,166,201,221,141,13,47,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,166,201,221,141,13,34,1,161,166,201,221,141,13,58,1,161,166,201,221,141,13,57,1,129,166,201,221,141,13,37,1,161,166,201,221,141,13,60,1,161,166,201,221,141,13,61,1,129,166,201,221,141,13,62,1,161,166,201,221,141,13,63,1,161,166,201,221,141,13,64,1,40,0,166,201,221,141,13,47,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,166,201,221,141,13,66,1,161,166,201,221,141,13,67,1,129,166,201,221,141,13,65,1,2,180,205,189,133,13,0,161,200,142,208,241,4,152,1,16,161,180,205,189,133,13,15,4,1,248,210,237,129,13,0,161,223,209,193,147,11,80,2,7,211,202,217,232,12,0,161,141,245,194,142,11,4,1,161,141,245,194,142,11,5,1,129,141,245,194,142,11,6,1,161,141,245,194,142,11,3,1,161,141,245,194,142,11,0,1,161,141,245,194,142,11,1,1,129,211,202,217,232,12,2,1,3,210,228,153,221,12,0,161,202,160,246,212,1,3,1,161,202,160,246,212,1,4,1,161,202,160,246,212,1,5,1,2,234,156,130,211,12,0,161,219,220,239,171,8,9,11,168,234,156,130,211,12,10,1,122,0,0,0,0,102,97,116,231,1,181,175,219,209,12,0,161,200,159,185,206,9,7,10,131,1,133,159,138,205,12,0,161,255,140,248,220,6,0,1,161,255,140,248,220,6,1,1,129,255,140,248,220,6,2,1,161,180,230,210,212,13,3,1,161,161,178,132,150,11,34,1,161,161,178,132,150,11,35,1,129,133,159,138,205,12,2,1,8,0,229,154,128,197,12,16,1,118,1,2,105,100,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,168,229,154,128,197,12,15,1,122,0,0,0,0,102,48,108,125,161,180,230,210,212,13,1,1,39,0,203,184,221,173,11,1,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,1,40,0,133,159,138,205,12,10,2,105,100,1,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,40,0,133,159,138,205,12,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,133,159,138,205,12,10,3,98,105,100,1,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,40,0,133,159,138,205,12,10,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,133,159,138,205,12,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,125,39,0,203,184,221,173,11,4,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,0,40,0,133,159,138,205,12,10,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,3,1,161,133,159,138,205,12,21,1,161,133,159,138,205,12,20,1,129,133,159,138,205,12,6,1,161,133,159,138,205,12,23,1,161,133,159,138,205,12,24,1,129,133,159,138,205,12,25,1,72,229,154,128,197,12,6,1,118,1,2,105,100,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,168,229,154,128,197,12,7,1,122,0,0,0,0,102,48,108,128,168,229,154,128,197,12,8,1,122,0,0,0,0,102,48,108,128,39,0,203,184,221,173,11,1,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,1,40,0,133,159,138,205,12,32,2,105,100,1,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,40,0,133,159,138,205,12,32,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,133,159,138,205,12,32,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,133,159,138,205,12,32,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,32,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,133,159,138,205,12,32,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,108,128,39,0,203,184,221,173,11,4,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,0,40,0,133,159,138,205,12,32,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,32,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,32,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,32,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,22,1,161,133,159,138,205,12,43,1,161,133,159,138,205,12,42,1,129,133,159,138,205,12,28,1,161,133,159,138,205,12,44,1,161,133,159,138,205,12,26,1,161,133,159,138,205,12,27,1,129,133,159,138,205,12,47,1,161,133,159,138,205,12,48,1,161,133,159,138,205,12,0,1,161,133,159,138,205,12,1,1,129,133,159,138,205,12,51,1,136,173,252,148,184,13,40,1,118,1,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,207,228,238,162,8,23,1,161,133,159,138,205,12,54,1,39,0,203,184,221,173,11,1,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,1,40,0,133,159,138,205,12,59,2,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,33,0,133,159,138,205,12,59,4,110,97,109,101,1,40,0,133,159,138,205,12,59,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,133,159,138,205,12,59,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,59,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,133,159,138,205,12,59,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,0,33,0,133,159,138,205,12,59,4,105,99,111,110,1,40,0,133,159,138,205,12,59,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,59,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,59,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,52,1,161,133,159,138,205,12,70,1,161,133,159,138,205,12,69,1,129,133,159,138,205,12,55,1,161,133,159,138,205,12,72,1,161,133,159,138,205,12,73,1,129,133,159,138,205,12,74,1,161,133,159,138,205,12,75,1,161,133,159,138,205,12,76,1,161,133,159,138,205,12,61,1,161,133,159,138,205,12,78,1,161,133,159,138,205,12,79,1,161,133,159,138,205,12,80,1,161,201,129,238,197,4,27,1,161,201,129,238,197,4,28,1,168,133,159,138,205,12,67,1,119,23,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,240,159,141,182,34,125,161,133,159,138,205,12,84,1,161,133,159,138,205,12,85,1,161,133,159,138,205,12,83,1,161,133,159,138,205,12,87,1,161,133,159,138,205,12,88,1,161,133,159,138,205,12,89,1,8,0,133,159,138,205,12,66,1,118,1,2,105,100,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,161,133,159,138,205,12,65,1,161,133,159,138,205,12,91,1,39,0,203,184,221,173,11,1,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,1,40,0,133,159,138,205,12,96,2,105,100,1,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,40,0,133,159,138,205,12,96,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,133,159,138,205,12,96,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,133,159,138,205,12,96,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,96,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,133,159,138,205,12,96,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,159,39,0,203,184,221,173,11,4,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,0,40,0,133,159,138,205,12,96,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,96,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,96,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,96,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,136,133,159,138,205,12,93,1,118,1,2,105,100,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,161,133,159,138,205,12,94,1,161,133,159,138,205,12,95,1,39,0,203,184,221,173,11,1,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,1,40,0,133,159,138,205,12,111,2,105,100,1,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,40,0,133,159,138,205,12,111,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,133,159,138,205,12,111,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,133,159,138,205,12,111,4,100,101,115,99,1,119,0,40,0,133,159,138,205,12,111,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,133,159,138,205,12,111,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,48,177,162,39,0,203,184,221,173,11,4,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,0,40,0,133,159,138,205,12,111,4,105,99,111,110,1,119,0,40,0,133,159,138,205,12,111,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,133,159,138,205,12,111,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,133,159,138,205,12,111,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,133,159,138,205,12,71,1,161,133,159,138,205,12,4,1,161,133,159,138,205,12,5,1,129,201,129,238,197,4,29,1,161,133,159,138,205,12,123,1,161,133,159,138,205,12,90,1,161,133,159,138,205,12,110,1,129,133,159,138,205,12,126,1,23,229,154,128,197,12,0,161,248,153,216,10,0,1,161,248,153,216,10,1,1,129,248,153,216,10,2,1,161,229,154,128,197,12,0,1,161,229,154,128,197,12,1,1,129,229,154,128,197,12,2,1,72,158,156,181,152,10,6,1,118,1,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,161,176,154,159,227,1,2,1,161,176,154,159,227,1,3,1,39,0,203,184,221,173,11,1,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,1,40,0,229,154,128,197,12,9,2,105,100,1,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,40,0,229,154,128,197,12,9,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,229,154,128,197,12,9,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,229,154,128,197,12,9,4,100,101,115,99,1,119,0,40,0,229,154,128,197,12,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,229,154,128,197,12,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,0,40,0,229,154,128,197,12,9,4,105,99,111,110,1,119,0,40,0,229,154,128,197,12,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,229,154,128,197,12,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,229,154,128,197,12,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,229,154,128,197,12,19,1,129,229,154,128,197,12,5,1,16,149,129,169,191,12,0,161,186,197,166,179,15,3,1,161,188,171,136,250,8,0,1,161,188,171,136,250,8,1,1,129,188,171,136,250,8,2,1,161,149,129,169,191,12,0,1,161,200,205,214,172,10,27,1,161,200,205,214,172,10,28,1,129,149,129,169,191,12,3,1,161,149,129,169,191,12,4,1,161,149,129,169,191,12,1,1,161,149,129,169,191,12,2,1,129,149,129,169,191,12,7,1,161,149,129,169,191,12,8,1,161,186,197,166,179,15,0,1,161,186,197,166,179,15,1,1,129,149,129,169,191,12,11,1,10,177,161,136,243,11,0,161,241,155,213,233,1,49,1,161,241,155,213,233,1,50,1,129,214,139,213,136,8,65,1,161,214,139,213,136,8,63,1,161,214,139,213,136,8,64,1,129,177,161,136,243,11,2,1,161,241,155,213,233,1,21,1,161,177,161,136,243,11,3,1,161,177,161,136,243,11,4,1,129,177,161,136,243,11,5,1,1,205,149,231,236,11,0,161,175,147,217,214,1,32,68,3,162,159,252,196,11,0,161,233,247,183,159,1,1,1,161,233,247,183,159,1,2,1,129,233,247,183,159,1,3,1,37,203,184,221,173,11,0,39,1,4,100,97,116,97,6,102,111,108,100,101,114,1,39,0,203,184,221,173,11,0,5,118,105,101,119,115,1,39,0,203,184,221,173,11,0,7,115,101,99,116,105,111,110,1,39,0,203,184,221,173,11,0,4,109,101,116,97,1,39,0,203,184,221,173,11,0,8,114,101,108,97,116,105,111,110,1,39,0,203,184,221,173,11,2,8,102,97,118,111,114,105,116,101,1,39,0,203,184,221,173,11,2,6,114,101,99,101,110,116,1,39,0,203,184,221,173,11,2,5,116,114,97,115,104,1,39,0,203,184,221,173,11,1,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,1,40,0,203,184,221,173,11,8,2,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,203,184,221,173,11,8,4,110,97,109,101,1,119,9,87,111,114,107,115,112,97,99,101,40,0,203,184,221,173,11,8,3,98,105,100,1,119,0,40,0,203,184,221,173,11,8,4,100,101,115,99,1,119,0,40,0,203,184,221,173,11,8,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,203,184,221,173,11,8,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,0,8,0,203,184,221,173,11,15,1,118,1,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,203,184,221,173,11,8,4,105,99,111,110,1,119,0,40,0,203,184,221,173,11,8,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,203,184,221,173,11,8,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,40,0,203,184,221,173,11,8,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,203,184,221,173,11,14,1,161,203,184,221,173,11,19,1,39,0,203,184,221,173,11,1,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,1,40,0,203,184,221,173,11,23,2,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,203,184,221,173,11,23,4,110,97,109,101,1,119,15,71,101,116,116,105,110,103,32,115,116,97,114,116,101,100,40,0,203,184,221,173,11,23,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,203,184,221,173,11,23,4,100,101,115,99,1,119,0,40,0,203,184,221,173,11,23,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,203,184,221,173,11,23,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,0,40,0,203,184,221,173,11,23,4,105,99,111,110,1,119,25,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,226,173,144,239,184,143,34,125,40,0,203,184,221,173,11,23,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,203,184,221,173,11,23,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,203,184,221,173,11,23,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,40,0,203,184,221,173,11,3,17,99,117,114,114,101,110,116,95,119,111,114,107,115,112,97,99,101,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,33,0,203,184,221,173,11,3,12,99,117,114,114,101,110,116,95,118,105,101,119,1,3,155,165,205,152,11,0,161,179,252,154,44,0,1,161,179,252,154,44,1,1,129,179,252,154,44,2,1,101,161,178,132,150,11,0,161,167,131,133,162,9,8,1,161,167,131,133,162,9,9,1,129,167,131,133,162,9,10,1,161,167,131,133,162,9,7,1,161,252,218,241,167,14,0,1,161,252,218,241,167,14,1,1,129,161,178,132,150,11,2,1,161,161,178,132,150,11,3,1,161,161,178,132,150,11,0,1,161,161,178,132,150,11,1,1,129,161,178,132,150,11,6,1,161,161,178,132,150,11,7,1,161,130,180,254,251,6,15,1,161,130,180,254,251,6,16,1,129,161,178,132,150,11,10,1,8,0,130,180,254,251,6,10,1,118,1,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,168,130,180,254,251,6,9,1,122,0,0,0,0,102,32,220,196,161,161,178,132,150,11,13,1,39,0,203,184,221,173,11,1,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,1,40,0,161,178,132,150,11,18,2,105,100,1,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,40,0,161,178,132,150,11,18,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,161,178,132,150,11,18,3,98,105,100,1,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,40,0,161,178,132,150,11,18,4,100,101,115,99,1,119,0,40,0,161,178,132,150,11,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,161,178,132,150,11,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,32,220,196,39,0,203,184,221,173,11,4,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,0,40,0,161,178,132,150,11,18,4,105,99,111,110,1,119,0,40,0,161,178,132,150,11,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,161,178,132,150,11,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,161,178,132,150,11,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,161,178,132,150,11,11,1,161,161,178,132,150,11,29,1,161,161,178,132,150,11,28,1,129,161,178,132,150,11,14,1,161,161,178,132,150,11,31,1,161,161,178,132,150,11,32,1,129,161,178,132,150,11,33,1,161,161,178,132,150,11,30,1,161,161,178,132,150,11,8,1,161,161,178,132,150,11,9,1,129,161,178,132,150,11,36,1,161,161,178,132,150,11,37,1,161,167,131,133,162,9,4,1,161,167,131,133,162,9,5,1,129,161,178,132,150,11,40,1,161,161,178,132,150,11,41,1,161,161,178,132,150,11,38,1,161,161,178,132,150,11,39,1,129,161,178,132,150,11,44,1,161,161,178,132,150,11,45,1,161,161,178,132,150,11,42,1,161,161,178,132,150,11,43,1,129,161,178,132,150,11,48,1,161,161,178,132,150,11,49,1,161,161,178,132,150,11,46,1,161,161,178,132,150,11,47,1,129,161,178,132,150,11,52,1,161,161,178,132,150,11,53,1,161,161,178,132,150,11,4,1,161,161,178,132,150,11,5,1,129,161,178,132,150,11,56,1,161,161,178,132,150,11,57,1,161,161,178,132,150,11,54,1,161,161,178,132,150,11,55,1,129,161,178,132,150,11,60,1,161,161,178,132,150,11,61,1,161,161,178,132,150,11,50,1,161,161,178,132,150,11,51,1,129,161,178,132,150,11,64,1,161,161,178,132,150,11,65,1,161,161,178,132,150,11,62,1,161,161,178,132,150,11,63,1,129,161,178,132,150,11,68,1,161,161,178,132,150,11,69,1,161,161,178,132,150,11,66,1,161,161,178,132,150,11,67,1,129,161,178,132,150,11,72,1,161,161,178,132,150,11,73,1,161,161,178,132,150,11,70,1,161,161,178,132,150,11,71,1,129,161,178,132,150,11,76,1,161,161,178,132,150,11,77,1,161,161,178,132,150,11,74,1,161,161,178,132,150,11,75,1,129,161,178,132,150,11,80,1,161,161,178,132,150,11,81,1,161,161,178,132,150,11,78,1,161,161,178,132,150,11,79,1,129,161,178,132,150,11,84,1,161,161,178,132,150,11,85,1,161,161,178,132,150,11,82,1,161,161,178,132,150,11,83,1,136,161,178,132,150,11,88,1,118,2,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,33,243,102,161,161,178,132,150,11,89,1,161,161,178,132,150,11,86,1,161,161,178,132,150,11,87,1,129,161,178,132,150,11,92,1,161,161,178,132,150,11,93,1,161,167,131,133,162,9,0,1,161,167,131,133,162,9,1,1,129,161,178,132,150,11,96,1,1,223,209,193,147,11,0,161,213,255,156,145,1,1,81,7,141,245,194,142,11,0,161,158,182,250,251,9,12,1,161,158,182,250,251,9,13,1,129,158,182,250,251,9,14,1,161,158,182,250,251,9,11,1,161,158,182,250,251,9,8,1,161,158,182,250,251,9,9,1,129,141,245,194,142,11,2,1,3,253,205,145,137,11,0,161,180,205,189,133,13,15,1,161,180,205,189,133,13,19,5,161,253,205,145,137,11,5,4,3,142,130,192,134,11,0,161,207,228,238,162,8,47,1,161,207,228,238,162,8,48,1,161,207,228,238,162,8,46,1,1,139,152,215,249,10,0,161,231,189,134,196,8,76,34,1,255,255,147,249,10,0,161,182,143,233,195,4,110,3,2,152,158,185,230,10,0,39,0,203,184,221,173,11,7,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,8,0,152,158,185,230,10,0,6,118,2,2,105,100,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,125,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,129,2,105,100,119,36,53,54,54,56,57,101,52,50,45,49,102,101,56,45,52,97,97,102,45,56,50,99,53,45,99,51,100,99,98,102,99,51,98,50,53,52,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,131,2,105,100,119,36,51,53,100,51,57,57,98,57,45,49,57,55,101,45,52,57,99,54,45,97,98,102,56,45,101,51,57,51,49,101,48,97,55,51,49,51,118,2,2,105,100,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,133,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,136,2,105,100,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,241,153,140,2,105,100,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,1,145,190,137,224,10,0,161,155,159,180,195,15,5,2,3,178,162,190,217,10,0,161,184,201,188,172,10,0,1,161,184,201,188,172,10,1,1,161,184,201,188,172,10,2,1,3,197,254,154,201,10,0,161,244,226,228,149,2,1,1,161,244,226,228,149,2,2,1,129,244,226,228,149,2,29,1,1,248,196,187,185,10,0,161,252,171,209,175,15,3,31,1,135,240,136,178,10,0,161,184,231,170,67,5,45,30,200,205,214,172,10,0,161,161,178,132,150,11,12,1,161,161,178,132,150,11,17,1,129,186,197,166,179,15,6,1,161,200,205,214,172,10,0,1,161,200,205,214,172,10,1,1,129,200,205,214,172,10,2,1,161,186,197,166,179,15,4,1,161,186,197,166,179,15,5,1,129,200,205,214,172,10,5,1,161,200,205,214,172,10,3,1,161,200,205,214,172,10,4,1,129,200,205,214,172,10,8,1,161,200,205,214,172,10,9,1,161,200,205,214,172,10,10,1,129,200,205,214,172,10,11,1,161,200,205,214,172,10,6,1,161,200,205,214,172,10,7,1,129,200,205,214,172,10,14,1,161,200,205,214,172,10,12,1,161,200,205,214,172,10,13,1,129,200,205,214,172,10,17,1,161,200,205,214,172,10,15,1,161,200,205,214,172,10,16,1,129,200,205,214,172,10,20,1,161,200,205,214,172,10,18,1,161,200,205,214,172,10,19,1,129,200,205,214,172,10,23,1,161,200,205,214,172,10,21,1,161,200,205,214,172,10,22,1,129,200,205,214,172,10,26,1,3,184,201,188,172,10,0,161,142,130,192,134,11,0,1,161,142,130,192,134,11,1,1,161,142,130,192,134,11,2,1,16,156,148,170,169,10,0,161,211,202,217,232,12,3,1,161,211,202,217,232,12,0,1,161,211,202,217,232,12,1,1,129,211,202,217,232,12,6,1,161,156,148,170,169,10,0,1,161,252,218,241,167,14,0,1,161,252,218,241,167,14,1,1,129,156,148,170,169,10,3,1,161,156,148,170,169,10,4,1,161,156,148,170,169,10,1,1,161,156,148,170,169,10,2,1,129,156,148,170,169,10,7,1,161,156,148,170,169,10,8,1,161,167,131,133,162,9,0,1,161,167,131,133,162,9,1,1,129,156,148,170,169,10,11,1,48,158,156,181,152,10,0,161,206,220,129,131,4,18,1,161,206,220,129,131,4,19,1,129,206,220,129,131,4,20,1,161,206,220,129,131,4,0,1,161,206,220,129,131,4,1,1,129,158,156,181,152,10,2,1,72,206,220,129,131,4,3,1,118,1,2,105,100,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,161,206,220,129,131,4,4,1,161,206,220,129,131,4,5,1,39,0,203,184,221,173,11,1,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,1,40,0,158,156,181,152,10,9,2,105,100,1,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,40,0,158,156,181,152,10,9,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,158,156,181,152,10,9,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,158,156,181,152,10,9,4,100,101,115,99,1,119,0,40,0,158,156,181,152,10,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,158,156,181,152,10,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,0,40,0,158,156,181,152,10,9,4,105,99,111,110,1,119,0,40,0,158,156,181,152,10,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,158,156,181,152,10,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,158,156,181,152,10,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,158,156,181,152,10,19,1,129,158,156,181,152,10,5,1,161,233,165,139,246,14,4,1,161,233,165,139,246,14,5,1,129,158,156,181,152,10,23,1,8,0,203,184,221,173,11,30,1,118,1,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,161,203,184,221,173,11,29,1,161,158,156,181,152,10,25,1,39,0,203,184,221,173,11,1,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,1,40,0,158,156,181,152,10,30,2,105,100,1,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,33,0,158,156,181,152,10,30,4,110,97,109,101,1,40,0,158,156,181,152,10,30,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,158,156,181,152,10,30,4,100,101,115,99,1,119,0,40,0,158,156,181,152,10,30,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,158,156,181,152,10,30,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,129,30,39,0,203,184,221,173,11,4,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,0,40,0,158,156,181,152,10,30,4,105,99,111,110,1,119,0,40,0,158,156,181,152,10,30,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,158,156,181,152,10,30,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,158,156,181,152,10,30,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,203,184,221,173,11,36,1,161,158,156,181,152,10,41,1,161,158,156,181,152,10,40,1,129,158,156,181,152,10,26,1,161,158,156,181,152,10,43,1,161,158,156,181,152,10,44,1,129,158,156,181,152,10,45,1,9,154,193,208,134,10,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,161,154,193,208,134,10,0,1,161,154,193,208,134,10,1,1,129,154,193,208,134,10,2,1,161,154,193,208,134,10,3,1,161,154,193,208,134,10,4,1,129,154,193,208,134,10,5,1,3,235,225,184,133,10,0,161,233,165,139,246,14,40,1,161,233,165,139,246,14,41,1,129,233,165,139,246,14,42,1,15,158,182,250,251,9,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,161,233,247,183,159,1,0,1,161,158,156,181,152,10,3,1,161,158,156,181,152,10,4,1,129,158,182,250,251,9,2,1,161,158,182,250,251,9,3,1,161,158,182,250,251,9,0,1,161,158,182,250,251,9,1,1,129,158,182,250,251,9,6,1,161,158,182,250,251,9,7,1,161,171,204,155,217,8,0,1,161,171,204,155,217,8,1,1,129,158,182,250,251,9,10,1,31,170,140,240,234,9,0,161,201,191,159,147,14,14,1,161,201,191,159,147,14,15,1,129,201,191,159,147,14,16,1,161,201,191,159,147,14,13,1,161,201,191,159,147,14,7,1,161,201,191,159,147,14,8,1,129,170,140,240,234,9,2,1,161,170,140,240,234,9,4,1,161,170,140,240,234,9,5,1,129,170,140,240,234,9,6,1,161,170,140,240,234,9,0,1,161,170,140,240,234,9,1,1,129,170,140,240,234,9,9,1,161,170,140,240,234,9,3,1,161,170,140,240,234,9,10,1,161,170,140,240,234,9,11,1,129,170,140,240,234,9,12,1,161,201,129,238,197,4,56,1,161,201,129,238,197,4,55,1,129,170,140,240,234,9,16,1,161,170,140,240,234,9,13,1,161,170,140,240,234,9,17,1,161,170,140,240,234,9,18,1,129,170,140,240,234,9,19,1,161,170,140,240,234,9,14,1,161,170,140,240,234,9,15,1,129,170,140,240,234,9,23,1,161,170,140,240,234,9,20,1,161,170,140,240,234,9,24,1,161,170,140,240,234,9,25,1,129,170,140,240,234,9,26,1,1,191,157,147,233,9,0,161,190,139,191,155,1,1,32,1,200,159,185,206,9,0,161,231,139,244,188,8,12,8,15,135,232,133,203,9,0,161,248,136,168,181,1,0,1,161,248,136,168,181,1,1,1,129,248,136,168,181,1,2,1,161,135,232,133,203,9,0,1,161,135,232,133,203,9,1,1,129,135,232,133,203,9,2,1,161,135,232,133,203,9,3,1,161,135,232,133,203,9,4,1,129,135,232,133,203,9,5,1,161,135,232,133,203,9,6,1,161,135,232,133,203,9,7,1,129,135,232,133,203,9,8,1,161,135,232,133,203,9,9,1,161,135,232,133,203,9,10,1,129,135,232,133,203,9,11,1,11,167,131,133,162,9,0,161,211,202,217,232,12,4,1,161,211,202,217,232,12,5,1,129,141,205,220,149,4,2,1,161,211,202,217,232,12,3,1,161,158,182,250,251,9,4,1,161,158,182,250,251,9,5,1,129,167,131,133,162,9,2,1,161,167,131,133,162,9,3,1,161,214,168,149,214,3,8,1,161,214,168,149,214,3,9,1,129,167,131,133,162,9,6,1,1,234,182,182,157,9,0,161,161,239,241,154,13,105,8,21,188,171,136,250,8,0,161,200,205,214,172,10,24,1,161,200,205,214,172,10,25,1,129,200,205,214,172,10,29,1,161,188,171,136,250,8,0,1,161,188,171,136,250,8,1,1,129,188,171,136,250,8,2,1,161,164,155,139,169,7,4,1,161,164,155,139,169,7,5,1,129,164,155,139,169,7,6,1,161,164,155,139,169,7,12,1,161,164,155,139,169,7,13,1,129,164,155,139,169,7,14,1,161,188,171,136,250,8,9,1,161,188,171,136,250,8,10,1,129,188,171,136,250,8,11,1,161,188,171,136,250,8,12,1,161,188,171,136,250,8,13,1,129,188,171,136,250,8,14,1,161,164,155,139,169,7,16,1,161,164,155,139,169,7,17,1,129,164,155,139,169,7,18,1,70,187,220,199,239,8,0,168,137,164,190,210,1,36,1,122,4,56,115,160,190,64,16,0,161,137,164,190,210,1,37,1,136,137,164,190,210,1,38,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,51,161,137,164,190,210,1,29,1,161,137,164,190,210,1,30,1,136,187,220,199,239,8,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,57,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,137,164,190,210,1,35,1,168,187,220,199,239,8,3,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,4,1,122,0,0,0,0,102,97,115,57,136,187,220,199,239,8,5,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,115,57,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,136,247,200,243,247,14,0,1,118,1,2,105,100,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,168,247,200,243,247,14,1,1,122,0,0,0,0,102,97,115,63,168,187,220,199,239,8,1,1,122,0,0,0,0,102,97,115,63,39,0,203,184,221,173,11,1,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,1,40,0,187,220,199,239,8,13,2,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,33,0,187,220,199,239,8,13,4,110,97,109,101,1,40,0,187,220,199,239,8,13,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,187,220,199,239,8,13,4,100,101,115,99,1,119,0,40,0,187,220,199,239,8,13,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,187,220,199,239,8,13,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,0,40,0,187,220,199,239,8,13,4,105,99,111,110,1,119,0,40,0,187,220,199,239,8,13,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,187,220,199,239,8,13,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,187,220,199,239,8,13,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,187,220,199,239,8,6,1,161,187,220,199,239,8,24,1,161,187,220,199,239,8,23,1,129,187,220,199,239,8,9,1,161,187,220,199,239,8,26,1,161,187,220,199,239,8,27,1,129,187,220,199,239,8,28,1,161,187,220,199,239,8,29,1,161,187,220,199,239,8,30,1,129,187,220,199,239,8,31,1,161,187,220,199,239,8,32,1,161,187,220,199,239,8,33,1,161,187,220,199,239,8,15,1,161,187,220,199,239,8,35,1,161,187,220,199,239,8,36,1,168,187,220,199,239,8,37,1,119,10,70,105,108,116,101,114,71,114,105,100,8,0,187,220,199,239,8,20,1,118,1,2,105,100,119,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,168,187,220,199,239,8,19,1,122,0,0,0,0,102,97,115,248,161,187,220,199,239,8,39,1,39,0,203,184,221,173,11,1,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,1,40,0,187,220,199,239,8,44,2,105,100,1,119,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,40,0,187,220,199,239,8,44,4,110,97,109,101,1,119,4,71,114,105,100,40,0,187,220,199,239,8,44,3,98,105,100,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,40,0,187,220,199,239,8,44,4,100,101,115,99,1,119,0,40,0,187,220,199,239,8,44,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,187,220,199,239,8,44,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,97,115,248,39,0,203,184,221,173,11,4,36,97,55,51,52,97,48,54,56,45,101,55,51,100,45,52,98,52,98,45,56,53,51,99,45,52,100,97,102,102,101,97,51,56,57,99,48,0,40,0,187,220,199,239,8,44,4,105,99,111,110,1,119,0,40,0,187,220,199,239,8,44,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,187,220,199,239,8,44,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,97,115,248,40,0,187,220,199,239,8,44,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,161,247,200,243,247,14,55,1,161,247,200,243,247,14,56,1,129,187,220,199,239,8,34,1,161,187,220,199,239,8,25,1,168,187,220,199,239,8,56,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,57,1,122,0,0,0,0,102,97,116,215,136,187,220,199,239,8,58,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,116,215,2,105,100,119,36,55,57,100,48,54,51,50,100,45,97,53,97,56,45,52,53,52,48,45,97,100,99,100,45,54,101,101,57,50,50,100,56,54,55,101,100,161,187,220,199,239,8,38,1,161,187,220,199,239,8,43,1,129,187,220,199,239,8,62,1,168,187,220,199,239,8,59,1,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,168,187,220,199,239,8,63,1,122,4,56,115,160,190,64,16,0,168,187,220,199,239,8,64,1,122,0,0,0,0,102,97,116,215,136,187,220,199,239,8,65,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,97,116,215,2,105,100,119,36,55,102,50,51,51,98,101,52,45,49,98,52,100,45,52,54,98,50,45,98,99,102,99,45,102,51,52,49,98,56,100,55,53,50,54,55,1,236,229,225,232,8,0,161,219,227,140,137,6,33,116,3,171,204,155,217,8,0,161,229,154,128,197,12,21,1,161,229,154,128,197,12,22,1,129,229,154,128,197,12,23,1,3,149,189,189,215,8,0,161,160,192,253,131,5,0,1,161,160,192,253,131,5,1,1,129,160,192,253,131,5,2,1,1,231,189,134,196,8,0,161,153,130,203,161,6,96,77,3,195,242,227,194,8,0,161,155,165,205,152,11,0,1,161,155,165,205,152,11,1,1,129,155,165,205,152,11,2,1,1,231,139,244,188,8,0,161,128,252,161,128,4,32,13,1,219,220,239,171,8,0,161,248,196,187,185,10,30,10,198,1,154,244,246,165,8,0,161,174,151,139,93,176,2,1,161,174,151,139,93,177,2,1,129,174,151,139,93,178,2,1,161,174,151,139,93,140,2,1,161,174,151,139,93,141,2,1,129,154,244,246,165,8,2,1,161,174,151,139,93,175,2,1,161,154,244,246,165,8,3,1,161,154,244,246,165,8,4,1,129,154,244,246,165,8,5,1,161,154,244,246,165,8,0,1,161,154,244,246,165,8,1,1,129,154,244,246,165,8,9,1,161,154,244,246,165,8,6,1,161,154,244,246,165,8,10,1,161,154,244,246,165,8,11,1,129,154,244,246,165,8,12,1,161,154,244,246,165,8,7,1,161,154,244,246,165,8,8,1,129,154,244,246,165,8,16,1,161,154,244,246,165,8,13,1,161,154,244,246,165,8,17,1,161,154,244,246,165,8,18,1,129,154,244,246,165,8,19,1,136,213,161,242,209,13,3,1,118,1,2,105,100,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,168,213,161,242,209,13,4,1,122,0,0,0,0,102,79,7,13,161,154,244,246,165,8,15,1,39,0,203,184,221,173,11,1,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,1,40,0,154,244,246,165,8,27,2,105,100,1,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,40,0,154,244,246,165,8,27,4,110,97,109,101,1,119,12,86,105,101,119,32,111,102,32,71,114,105,100,40,0,154,244,246,165,8,27,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,154,244,246,165,8,27,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,27,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,154,244,246,165,8,27,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,13,39,0,203,184,221,173,11,4,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,0,40,0,154,244,246,165,8,27,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,27,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,27,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,27,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,8,0,165,139,157,171,15,10,1,118,1,2,105,100,119,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,168,165,139,157,171,15,9,1,122,0,0,0,0,102,79,7,25,168,174,151,139,93,170,2,1,122,0,0,0,0,102,79,7,25,39,0,203,184,221,173,11,1,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,1,40,0,154,244,246,165,8,42,2,105,100,1,119,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,40,0,154,244,246,165,8,42,4,110,97,109,101,1,119,22,86,105,101,119,32,111,102,32,66,111,97,114,100,32,99,104,101,99,107,98,111,120,40,0,154,244,246,165,8,42,3,98,105,100,1,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,40,0,154,244,246,165,8,42,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,42,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,40,0,154,244,246,165,8,42,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,25,39,0,203,184,221,173,11,4,36,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,0,40,0,154,244,246,165,8,42,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,42,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,154,244,246,165,8,42,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,79,7,25,40,0,154,244,246,165,8,42,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,8,0,201,129,238,197,4,67,1,118,1,2,105,100,119,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,168,201,129,238,197,4,66,1,122,0,0,0,0,102,79,7,32,161,174,151,139,93,106,1,39,0,203,184,221,173,11,1,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,1,40,0,154,244,246,165,8,57,2,105,100,1,119,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,40,0,154,244,246,165,8,57,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,154,244,246,165,8,57,3,98,105,100,1,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,40,0,154,244,246,165,8,57,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,57,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,154,244,246,165,8,57,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,32,39,0,203,184,221,173,11,4,36,97,54,97,102,51,49,49,102,45,99,98,99,56,45,52,50,99,50,45,98,56,48,49,45,55,49,49,53,54,49,57,99,51,55,55,54,0,40,0,154,244,246,165,8,57,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,57,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,154,244,246,165,8,57,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,79,7,32,40,0,154,244,246,165,8,57,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,174,151,139,93,177,1,1,118,1,2,105,100,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,168,174,151,139,93,178,1,1,122,0,0,0,0,102,79,7,50,161,174,151,139,93,179,1,1,39,0,203,184,221,173,11,1,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,1,40,0,154,244,246,165,8,72,2,105,100,1,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,40,0,154,244,246,165,8,72,4,110,97,109,101,1,119,16,86,105,101,119,32,111,102,32,67,97,108,101,110,100,97,114,40,0,154,244,246,165,8,72,3,98,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,40,0,154,244,246,165,8,72,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,72,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,40,0,154,244,246,165,8,72,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,79,7,50,39,0,203,184,221,173,11,4,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,0,40,0,154,244,246,165,8,72,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,72,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,72,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,72,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,154,244,246,165,8,20,1,168,154,244,246,165,8,83,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,82,1,122,0,0,0,0,102,79,23,79,136,154,244,246,165,8,23,1,118,2,2,105,100,119,36,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,79,23,79,161,154,244,246,165,8,84,1,161,174,151,139,93,174,1,1,161,154,244,246,165,8,71,1,129,154,244,246,165,8,87,1,161,154,244,246,165,8,89,1,161,154,244,246,165,8,90,1,129,154,244,246,165,8,91,1,161,154,244,246,165,8,21,1,161,154,244,246,165,8,22,1,129,154,244,246,165,8,94,1,161,154,244,246,165,8,88,1,161,154,244,246,165,8,95,1,161,154,244,246,165,8,96,1,129,154,244,246,165,8,97,1,161,154,244,246,165,8,98,1,161,154,244,246,165,8,38,1,161,154,244,246,165,8,37,1,129,154,244,246,165,8,101,1,161,154,244,246,165,8,14,1,161,154,244,246,165,8,26,1,129,154,244,246,165,8,105,1,161,154,244,246,165,8,102,1,161,154,244,246,165,8,106,1,161,154,244,246,165,8,107,1,129,154,244,246,165,8,108,1,161,154,244,246,165,8,99,1,161,154,244,246,165,8,100,1,129,154,244,246,165,8,112,1,161,154,244,246,165,8,109,1,161,154,244,246,165,8,113,1,161,154,244,246,165,8,114,1,129,154,244,246,165,8,115,1,161,154,244,246,165,8,103,1,161,154,244,246,165,8,104,1,129,154,244,246,165,8,119,1,161,154,244,246,165,8,116,1,168,154,244,246,165,8,120,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,121,1,122,0,0,0,0,102,79,28,199,136,154,244,246,165,8,122,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,79,28,199,2,105,100,119,36,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,161,154,244,246,165,8,123,1,161,174,151,139,93,105,1,161,154,244,246,165,8,56,1,129,154,244,246,165,8,126,1,161,154,244,246,165,8,128,1,1,161,154,244,246,165,8,129,1,1,129,154,244,246,165,8,130,1,1,161,154,244,246,165,8,92,1,161,154,244,246,165,8,93,1,129,154,244,246,165,8,133,1,1,161,154,244,246,165,8,127,1,161,154,244,246,165,8,134,1,1,161,154,244,246,165,8,135,1,1,129,154,244,246,165,8,136,1,1,161,154,244,246,165,8,131,1,1,161,154,244,246,165,8,132,1,1,129,154,244,246,165,8,140,1,1,161,154,244,246,165,8,137,1,1,168,154,244,246,165,8,141,1,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,142,1,1,122,0,0,0,0,102,80,7,127,136,154,244,246,165,8,143,1,1,118,2,2,105,100,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,80,7,127,161,154,244,246,165,8,138,1,1,161,154,244,246,165,8,139,1,1,129,154,244,246,165,8,147,1,1,161,154,244,246,165,8,144,1,1,161,154,244,246,165,8,148,1,1,161,154,244,246,165,8,149,1,1,129,154,244,246,165,8,150,1,1,161,154,244,246,165,8,110,1,161,154,244,246,165,8,111,1,129,154,244,246,165,8,154,1,1,161,154,244,246,165,8,151,1,1,161,154,244,246,165,8,155,1,1,161,154,244,246,165,8,156,1,1,129,154,244,246,165,8,157,1,1,161,174,151,139,93,153,1,1,161,174,151,139,93,154,1,1,129,154,244,246,165,8,161,1,1,161,154,244,246,165,8,158,1,1,161,154,244,246,165,8,162,1,1,161,154,244,246,165,8,163,1,1,129,154,244,246,165,8,164,1,1,161,154,244,246,165,8,152,1,1,161,154,244,246,165,8,153,1,1,129,154,244,246,165,8,168,1,1,161,154,244,246,165,8,165,1,1,161,154,244,246,165,8,169,1,1,161,154,244,246,165,8,170,1,1,129,154,244,246,165,8,171,1,1,161,154,244,246,165,8,117,1,161,154,244,246,165,8,118,1,129,154,244,246,165,8,175,1,1,161,154,244,246,165,8,172,1,1,161,154,244,246,165,8,176,1,1,161,154,244,246,165,8,177,1,1,129,154,244,246,165,8,178,1,1,39,0,203,184,221,173,11,1,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,1,40,0,154,244,246,165,8,183,1,2,105,100,1,119,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,40,0,154,244,246,165,8,183,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,154,244,246,165,8,183,1,3,98,105,100,1,119,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,40,0,154,244,246,165,8,183,1,4,100,101,115,99,1,119,0,40,0,154,244,246,165,8,183,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,154,244,246,165,8,183,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,80,7,153,39,0,203,184,221,173,11,4,36,53,55,98,56,49,55,55,100,45,57,100,52,50,45,53,49,55,56,45,56,99,98,50,45,102,99,53,101,54,57,51,48,102,51,48,97,0,40,0,154,244,246,165,8,183,1,4,105,99,111,110,1,119,0,40,0,154,244,246,165,8,183,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,154,244,246,165,8,183,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,154,244,246,165,8,183,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,154,244,246,165,8,194,1,1,122,4,56,115,160,190,64,16,0,168,154,244,246,165,8,193,1,1,122,0,0,0,0,102,80,7,153,40,0,154,244,246,165,8,183,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,50,207,228,238,162,8,0,136,158,156,181,152,10,27,1,118,1,2,105,100,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,161,158,156,181,152,10,28,1,161,158,156,181,152,10,29,1,39,0,203,184,221,173,11,1,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,1,40,0,207,228,238,162,8,3,2,105,100,1,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,40,0,207,228,238,162,8,3,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,207,228,238,162,8,3,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,207,228,238,162,8,3,4,100,101,115,99,1,119,0,40,0,207,228,238,162,8,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,207,228,238,162,8,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,242,129,194,39,0,203,184,221,173,11,4,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,0,40,0,207,228,238,162,8,3,4,105,99,111,110,1,119,0,40,0,207,228,238,162,8,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,207,228,238,162,8,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,207,228,238,162,8,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,158,156,181,152,10,42,1,161,207,228,238,162,8,14,1,161,207,228,238,162,8,13,1,129,158,156,181,152,10,48,1,161,207,228,238,162,8,16,1,161,207,228,238,162,8,17,1,129,207,228,238,162,8,18,1,136,207,228,238,162,8,0,1,118,1,2,105,100,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,161,207,228,238,162,8,1,1,161,207,228,238,162,8,2,1,39,0,203,184,221,173,11,1,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,1,40,0,207,228,238,162,8,25,2,105,100,1,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,40,0,207,228,238,162,8,25,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,207,228,238,162,8,25,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,207,228,238,162,8,25,4,100,101,115,99,1,119,0,40,0,207,228,238,162,8,25,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,207,228,238,162,8,25,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,242,129,219,39,0,203,184,221,173,11,4,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,0,40,0,207,228,238,162,8,25,4,105,99,111,110,1,119,0,40,0,207,228,238,162,8,25,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,207,228,238,162,8,25,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,207,228,238,162,8,25,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,207,228,238,162,8,15,1,161,207,228,238,162,8,36,1,161,207,228,238,162,8,35,1,129,207,228,238,162,8,21,1,161,207,228,238,162,8,38,1,161,207,228,238,162,8,39,1,129,207,228,238,162,8,40,1,161,158,156,181,152,10,46,1,161,158,156,181,152,10,47,1,161,158,156,181,152,10,32,1,161,207,228,238,162,8,44,1,161,207,228,238,162,8,45,1,129,207,228,238,162,8,43,1,1,183,226,184,158,8,0,161,240,253,240,229,1,78,8,11,175,225,172,150,8,0,161,173,252,148,184,13,61,1,161,173,252,148,184,13,62,1,129,173,252,148,184,13,63,1,161,173,252,148,184,13,60,1,161,173,252,148,184,13,57,1,161,173,252,148,184,13,58,1,129,175,225,172,150,8,2,1,161,175,225,172,150,8,3,1,161,175,225,172,150,8,0,1,161,175,225,172,150,8,1,1,136,175,225,172,150,8,6,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,41,200,233,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,66,214,139,213,136,8,0,161,201,129,238,197,4,9,1,161,201,129,238,197,4,10,1,136,149,249,242,175,4,13,1,118,2,2,105,100,119,36,102,51,53,50,55,48,99,55,45,99,54,54,99,45,52,54,99,101,45,56,101,49,97,45,51,102,54,51,57,102,55,98,48,48,48,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,7,168,214,139,213,136,8,0,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,1,1,122,0,0,0,0,102,77,78,7,161,180,230,210,212,13,0,1,161,133,159,138,205,12,9,1,136,214,139,213,136,8,2,1,118,2,2,105,100,119,36,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,9,168,214,139,213,136,8,5,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,6,1,122,0,0,0,0,102,77,78,9,161,201,129,238,197,4,12,1,161,201,129,238,197,4,13,1,136,214,139,213,136,8,7,1,118,2,2,105,100,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,10,168,214,139,213,136,8,10,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,11,1,122,0,0,0,0,102,77,78,10,161,158,156,181,152,10,0,1,161,158,156,181,152,10,1,1,136,214,139,213,136,8,12,1,118,2,2,105,100,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,11,168,214,139,213,136,8,15,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,16,1,122,0,0,0,0,102,77,78,12,161,161,178,132,150,11,90,1,161,161,178,132,150,11,91,1,136,214,139,213,136,8,17,1,118,2,2,105,100,119,36,99,97,49,50,50,99,48,52,45,100,55,98,51,45,52,102,55,48,45,57,57,53,49,45,57,54,98,102,100,97,57,98,54,98,50,52,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,12,168,214,139,213,136,8,20,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,21,1,122,0,0,0,0,102,77,78,12,161,222,205,223,235,7,7,1,161,222,205,223,235,7,8,1,136,214,139,213,136,8,22,1,118,2,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,29,168,214,139,213,136,8,25,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,26,1,122,0,0,0,0,102,77,78,30,161,207,228,238,162,8,19,1,161,207,228,238,162,8,20,1,136,214,139,213,136,8,27,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,30,2,105,100,119,36,100,97,53,54,102,102,97,48,45,53,51,53,54,45,52,50,56,99,45,98,54,49,98,45,53,55,52,97,49,49,57,99,54,57,57,101,168,214,139,213,136,8,30,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,31,1,122,0,0,0,0,102,77,78,30,161,173,252,148,184,13,17,1,161,173,252,148,184,13,18,1,136,214,139,213,136,8,32,1,118,2,2,105,100,119,36,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,31,168,214,139,213,136,8,35,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,36,1,122,0,0,0,0,102,77,78,31,161,225,248,138,176,2,7,1,161,225,248,138,176,2,8,1,136,214,139,213,136,8,37,1,118,2,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,41,168,214,139,213,136,8,40,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,41,1,122,0,0,0,0,102,77,78,41,161,165,139,157,171,15,91,1,161,165,139,157,171,15,92,1,136,214,139,213,136,8,42,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,44,2,105,100,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,168,214,139,213,136,8,45,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,46,1,122,0,0,0,0,102,77,78,44,161,234,153,236,158,4,3,1,161,234,153,236,158,4,4,1,136,214,139,213,136,8,47,1,118,2,2,105,100,119,36,49,98,48,101,51,50,50,100,45,52,57,48,57,45,52,99,54,51,45,57,49,52,97,45,100,48,51,52,102,99,51,54,51,48,57,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,45,168,214,139,213,136,8,50,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,51,1,122,0,0,0,0,102,77,78,45,161,165,139,157,171,15,58,1,161,165,139,157,171,15,59,1,136,214,139,213,136,8,52,1,118,2,2,105,100,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,77,78,49,168,214,139,213,136,8,55,1,122,4,56,115,160,190,64,16,0,168,214,139,213,136,8,56,1,122,0,0,0,0,102,77,78,49,161,241,155,213,233,1,0,1,161,241,155,213,233,1,1,1,129,241,155,213,233,1,24,1,161,214,139,213,136,8,60,1,161,214,139,213,136,8,61,1,129,214,139,213,136,8,62,1,31,222,205,223,235,7,0,161,137,226,192,199,6,17,1,161,137,226,192,199,6,18,1,129,137,226,192,199,6,19,1,161,201,129,238,197,4,39,1,161,201,129,238,197,4,40,1,129,222,205,223,235,7,2,1,161,137,226,192,199,6,13,1,161,222,205,223,235,7,3,1,161,222,205,223,235,7,4,1,136,222,205,223,235,7,5,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,67,55,119,2,105,100,119,36,98,53,56,48,55,51,52,53,45,53,100,97,55,45,52,53,98,100,45,97,56,97,101,45,101,97,53,54,56,99,55,99,57,49,49,97,161,222,205,223,235,7,0,1,161,222,205,223,235,7,1,1,129,222,205,223,235,7,9,1,161,222,205,223,235,7,6,1,161,222,205,223,235,7,10,1,161,222,205,223,235,7,11,1,129,222,205,223,235,7,12,1,161,137,226,192,199,6,7,1,161,137,226,192,199,6,8,1,129,222,205,223,235,7,16,1,161,222,205,223,235,7,13,1,161,222,205,223,235,7,17,1,161,222,205,223,235,7,18,1,129,222,205,223,235,7,19,1,161,222,205,223,235,7,14,1,161,222,205,223,235,7,15,1,129,222,205,223,235,7,23,1,161,222,205,223,235,7,20,1,161,222,205,223,235,7,24,1,161,222,205,223,235,7,25,1,129,222,205,223,235,7,26,1,3,162,238,198,212,7,0,161,245,220,194,52,6,1,161,247,200,243,247,14,2,1,136,247,200,243,247,14,57,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,58,53,12,241,130,161,205,7,0,161,226,212,179,248,2,12,1,161,226,212,179,248,2,13,1,129,226,212,179,248,2,14,1,161,234,153,236,158,4,61,1,161,234,153,236,158,4,62,1,129,241,130,161,205,7,2,1,161,241,130,161,205,7,0,1,161,241,130,161,205,7,1,1,129,241,130,161,205,7,5,1,168,234,153,236,158,4,39,1,122,4,56,115,160,190,64,16,0,168,234,153,236,158,4,38,1,122,0,0,0,0,102,78,192,25,136,174,151,139,93,243,1,1,118,2,2,105,100,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,78,192,25,1,229,153,197,202,7,0,161,239,199,189,146,3,23,241,6,1,250,198,166,187,7,0,161,152,252,186,192,1,1,2,35,164,155,139,169,7,0,161,149,129,169,191,12,13,1,161,149,129,169,191,12,14,1,129,149,129,169,191,12,15,1,161,149,129,169,191,12,12,1,161,149,129,169,191,12,9,1,161,149,129,169,191,12,10,1,129,164,155,139,169,7,2,1,161,164,155,139,169,7,3,1,161,207,228,238,162,8,41,1,161,207,228,238,162,8,42,1,129,188,171,136,250,8,8,1,161,164,155,139,169,7,7,1,161,188,171,136,250,8,6,1,161,188,171,136,250,8,7,1,129,164,155,139,169,7,10,1,161,164,155,139,169,7,11,1,161,149,129,169,191,12,5,1,161,149,129,169,191,12,6,1,129,188,171,136,250,8,17,1,161,164,155,139,169,7,15,1,161,188,171,136,250,8,15,1,161,188,171,136,250,8,16,1,129,164,155,139,169,7,18,1,161,164,155,139,169,7,19,1,161,164,155,139,169,7,16,1,161,164,155,139,169,7,17,1,129,164,155,139,169,7,22,1,161,164,155,139,169,7,23,1,161,164,155,139,169,7,20,1,161,164,155,139,169,7,21,1,129,188,171,136,250,8,20,1,161,164,155,139,169,7,27,1,161,188,171,136,250,8,18,1,161,188,171,136,250,8,19,1,129,164,155,139,169,7,30,1,1,177,219,160,167,7,0,161,135,240,136,178,10,44,4,2,135,193,208,135,7,0,161,135,167,156,250,14,15,12,161,135,193,208,135,7,11,4,17,130,180,254,251,6,0,129,252,163,130,200,6,15,1,161,252,163,130,200,6,16,1,161,252,163,130,200,6,17,1,39,0,203,184,221,173,11,1,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,1,40,0,130,180,254,251,6,3,2,105,100,1,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,33,0,130,180,254,251,6,3,4,110,97,109,101,1,33,0,130,180,254,251,6,3,3,98,105,100,1,40,0,130,180,254,251,6,3,4,100,101,115,99,1,119,0,40,0,130,180,254,251,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,33,0,130,180,254,251,6,3,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,0,40,0,130,180,254,251,6,3,4,105,99,111,110,1,119,0,40,0,130,180,254,251,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,130,180,254,251,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,130,180,254,251,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,130,180,254,251,6,13,1,168,130,180,254,251,6,5,1,119,5,110,105,115,104,105,27,159,156,204,250,6,0,161,149,249,242,175,4,28,1,161,149,249,242,175,4,29,1,129,149,249,242,175,4,30,1,161,159,156,204,250,6,0,1,161,159,156,204,250,6,1,1,129,159,156,204,250,6,2,1,161,143,184,153,180,6,3,1,161,143,184,153,180,6,4,1,129,143,184,153,180,6,5,1,161,159,156,204,250,6,6,1,161,159,156,204,250,6,7,1,129,159,156,204,250,6,8,1,161,159,156,204,250,6,9,1,161,159,156,204,250,6,10,1,129,159,156,204,250,6,11,1,161,159,156,204,250,6,12,1,161,159,156,204,250,6,13,1,129,159,156,204,250,6,14,1,161,159,156,204,250,6,15,1,161,159,156,204,250,6,16,1,129,159,156,204,250,6,17,1,161,159,156,204,250,6,18,1,161,159,156,204,250,6,19,1,129,159,156,204,250,6,20,1,161,159,156,204,250,6,21,1,161,159,156,204,250,6,22,1,129,159,156,204,250,6,23,1,3,135,166,246,235,6,0,161,245,220,194,52,36,1,161,245,220,194,52,37,1,136,245,220,194,52,32,1,118,2,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,85,170,119,1,175,205,156,228,6,0,161,183,226,184,158,8,7,10,15,240,149,229,225,6,0,136,203,184,221,173,11,16,1,118,1,2,105,100,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,161,158,156,181,152,10,7,1,161,158,156,181,152,10,8,1,39,0,203,184,221,173,11,1,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,1,40,0,240,149,229,225,6,3,2,105,100,1,119,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,40,0,240,149,229,225,6,3,4,110,97,109,101,1,119,0,40,0,240,149,229,225,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,240,149,229,225,6,3,4,100,101,115,99,1,119,0,40,0,240,149,229,225,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,240,149,229,225,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,138,214,39,0,203,184,221,173,11,4,36,101,48,102,101,54,56,54,55,45,50,48,56,102,45,52,51,57,57,45,97,56,56,97,45,101,57,98,97,102,52,56,97,99,48,53,101,0,40,0,240,149,229,225,6,3,4,105,99,111,110,1,119,0,40,0,240,149,229,225,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,240,149,229,225,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,138,214,40,0,240,149,229,225,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,3,255,140,248,220,6,0,161,180,230,210,212,13,4,1,161,180,230,210,212,13,5,1,129,180,230,210,212,13,6,1,1,157,207,243,216,6,0,161,145,190,137,224,10,1,2,30,252,163,130,200,6,0,136,143,131,148,152,6,0,1,118,1,2,105,100,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,161,143,131,148,152,6,1,1,161,143,131,148,152,6,2,1,39,0,203,184,221,173,11,1,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,1,40,0,252,163,130,200,6,3,2,105,100,1,119,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,40,0,252,163,130,200,6,3,4,110,97,109,101,1,119,0,40,0,252,163,130,200,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,252,163,130,200,6,3,4,100,101,115,99,1,119,0,40,0,252,163,130,200,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,252,163,130,200,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,149,3,39,0,203,184,221,173,11,4,36,102,57,101,48,48,54,53,49,45,49,53,48,101,45,52,50,56,56,45,56,48,55,57,45,50,50,100,48,54,97,50,55,54,97,55,49,0,40,0,252,163,130,200,6,3,4,105,99,111,110,1,119,0,40,0,252,163,130,200,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,252,163,130,200,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,149,3,40,0,252,163,130,200,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,136,252,163,130,200,6,0,1,118,1,2,105,100,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,161,252,163,130,200,6,1,1,161,252,163,130,200,6,2,1,39,0,203,184,221,173,11,1,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,1,40,0,252,163,130,200,6,18,2,105,100,1,119,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,40,0,252,163,130,200,6,18,4,110,97,109,101,1,119,0,40,0,252,163,130,200,6,18,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,252,163,130,200,6,18,4,100,101,115,99,1,119,0,40,0,252,163,130,200,6,18,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,252,163,130,200,6,18,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,149,31,39,0,203,184,221,173,11,4,36,56,54,54,57,52,102,97,100,45,54,55,52,97,45,52,54,55,52,45,56,52,100,48,45,51,50,99,52,56,54,97,55,55,48,98,52,0,40,0,252,163,130,200,6,18,4,105,99,111,110,1,119,0,40,0,252,163,130,200,6,18,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,252,163,130,200,6,18,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,149,31,40,0,252,163,130,200,6,18,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,20,137,226,192,199,6,0,161,170,140,240,234,9,28,1,161,170,140,240,234,9,29,1,129,170,140,240,234,9,30,1,161,213,161,242,209,13,28,1,161,213,161,242,209,13,29,1,129,137,226,192,199,6,2,1,161,170,140,240,234,9,27,1,161,137,226,192,199,6,3,1,161,137,226,192,199,6,4,1,129,137,226,192,199,6,5,1,161,137,226,192,199,6,0,1,161,137,226,192,199,6,1,1,129,137,226,192,199,6,9,1,161,137,226,192,199,6,6,1,161,137,226,192,199,6,10,1,161,137,226,192,199,6,11,1,129,137,226,192,199,6,12,1,161,137,226,192,199,6,14,1,161,137,226,192,199,6,15,1,129,137,226,192,199,6,16,1,3,246,185,174,192,6,0,161,135,193,208,135,7,11,1,161,135,193,208,135,7,15,87,161,246,185,174,192,6,87,2,2,196,154,250,183,6,0,161,149,161,132,184,14,218,2,1,161,149,161,132,184,14,220,2,61,6,143,184,153,180,6,0,161,164,188,201,172,1,0,1,161,164,188,201,172,1,1,1,129,164,188,201,172,1,2,1,161,143,184,153,180,6,0,1,161,143,184,153,180,6,1,1,129,143,184,153,180,6,2,1,1,198,189,216,175,6,0,161,234,187,164,181,1,23,23,1,153,130,203,161,6,0,161,248,210,237,129,13,1,97,15,143,131,148,152,6,0,136,243,239,182,181,13,15,1,118,1,2,105,100,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,161,243,239,182,181,13,16,1,161,243,239,182,181,13,17,1,39,0,203,184,221,173,11,1,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,1,40,0,143,131,148,152,6,3,2,105,100,1,119,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,40,0,143,131,148,152,6,3,4,110,97,109,101,1,119,0,40,0,143,131,148,152,6,3,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,143,131,148,152,6,3,4,100,101,115,99,1,119,0,40,0,143,131,148,152,6,3,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,143,131,148,152,6,3,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,143,252,39,0,203,184,221,173,11,4,36,50,57,55,48,52,49,52,101,45,99,99,50,48,45,52,51,50,102,45,97,100,49,54,45,52,50,101,99,55,49,55,51,54,100,53,57,0,40,0,143,131,148,152,6,3,4,105,99,111,110,1,119,0,40,0,143,131,148,152,6,3,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,143,131,148,152,6,3,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,101,241,143,252,40,0,143,131,148,152,6,3,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,1,188,237,223,145,6,0,161,221,147,167,147,15,176,2,26,165,1,140,152,206,145,6,0,161,154,244,246,165,8,180,1,1,161,154,244,246,165,8,181,1,1,129,154,244,246,165,8,182,1,1,161,140,152,206,145,6,0,1,161,140,152,206,145,6,1,1,129,140,152,206,145,6,2,1,161,140,152,206,145,6,3,1,161,140,152,206,145,6,4,1,129,140,152,206,145,6,5,1,161,154,244,246,165,8,159,1,1,161,154,244,246,165,8,160,1,1,129,140,152,206,145,6,8,1,161,154,244,246,165,8,179,1,1,161,140,152,206,145,6,9,1,161,140,152,206,145,6,10,1,129,140,152,206,145,6,11,1,161,140,152,206,145,6,12,1,161,140,152,206,145,6,6,1,161,140,152,206,145,6,7,1,129,140,152,206,145,6,15,1,161,140,152,206,145,6,17,1,161,140,152,206,145,6,18,1,129,140,152,206,145,6,19,1,161,140,152,206,145,6,20,1,161,140,152,206,145,6,21,1,129,140,152,206,145,6,22,1,161,154,244,246,165,8,166,1,1,161,154,244,246,165,8,167,1,1,129,140,152,206,145,6,25,1,161,140,152,206,145,6,16,1,161,140,152,206,145,6,26,1,161,140,152,206,145,6,27,1,129,140,152,206,145,6,28,1,161,154,244,246,165,8,173,1,1,161,154,244,246,165,8,174,1,1,129,140,152,206,145,6,32,1,161,140,152,206,145,6,29,1,161,140,152,206,145,6,33,1,161,140,152,206,145,6,34,1,129,140,152,206,145,6,35,1,161,140,152,206,145,6,13,1,161,140,152,206,145,6,14,1,129,140,152,206,145,6,39,1,161,140,152,206,145,6,36,1,161,140,152,206,145,6,40,1,161,140,152,206,145,6,41,1,129,140,152,206,145,6,42,1,161,140,152,206,145,6,43,1,161,140,152,206,145,6,23,1,161,140,152,206,145,6,24,1,129,140,152,206,145,6,46,1,161,140,152,206,145,6,48,1,161,140,152,206,145,6,49,1,129,140,152,206,145,6,50,1,161,140,152,206,145,6,44,1,161,140,152,206,145,6,45,1,129,140,152,206,145,6,53,1,161,140,152,206,145,6,47,1,161,140,152,206,145,6,54,1,161,140,152,206,145,6,55,1,136,140,152,206,145,6,56,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,230,246,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,51,1,161,140,152,206,145,6,52,1,129,140,152,206,145,6,60,1,161,140,152,206,145,6,57,1,161,140,152,206,145,6,61,1,161,140,152,206,145,6,62,1,129,140,152,206,145,6,63,1,161,140,152,206,145,6,65,1,161,140,152,206,145,6,66,1,161,149,249,242,175,4,2,1,161,140,152,206,145,6,68,1,161,140,152,206,145,6,69,1,161,140,152,206,145,6,70,1,161,140,152,206,145,6,71,1,161,140,152,206,145,6,72,1,161,140,152,206,145,6,73,1,161,140,152,206,145,6,74,1,161,140,152,206,145,6,75,1,161,140,152,206,145,6,76,1,161,140,152,206,145,6,77,1,161,140,152,206,145,6,78,1,161,140,152,206,145,6,79,1,161,140,152,206,145,6,58,1,161,140,152,206,145,6,59,1,136,140,152,206,145,6,67,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,108,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,64,1,161,140,152,206,145,6,83,1,161,140,152,206,145,6,84,1,136,140,152,206,145,6,85,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,109,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,140,152,206,145,6,86,1,161,140,152,206,145,6,80,1,161,140,152,206,145,6,81,1,129,140,152,206,145,6,89,1,161,140,152,206,145,6,91,1,161,140,152,206,145,6,92,1,136,140,152,206,145,6,93,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,241,111,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,140,152,206,145,6,94,1,161,140,152,206,145,6,95,1,161,140,152,206,145,6,82,1,136,241,155,213,233,1,6,1,118,1,2,105,100,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,161,241,155,213,233,1,7,1,161,140,152,206,145,6,98,1,39,0,203,184,221,173,11,1,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,1,40,0,140,152,206,145,6,103,2,105,100,1,119,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,33,0,140,152,206,145,6,103,4,110,97,109,101,1,40,0,140,152,206,145,6,103,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,140,152,206,145,6,103,4,100,101,115,99,1,119,0,40,0,140,152,206,145,6,103,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,140,152,206,145,6,103,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,83,241,173,39,0,203,184,221,173,11,4,36,48,101,55,99,100,101,102,50,45,49,48,99,50,45,52,52,101,99,45,56,98,54,49,45,49,97,100,101,48,98,102,53,100,51,102,102,0,33,0,140,152,206,145,6,103,4,105,99,111,110,1,40,0,140,152,206,145,6,103,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,140,152,206,145,6,103,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,140,152,206,145,6,103,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,140,152,206,145,6,90,1,161,140,152,206,145,6,114,1,161,140,152,206,145,6,113,1,129,140,152,206,145,6,96,1,161,140,152,206,145,6,116,1,161,140,152,206,145,6,117,1,129,140,152,206,145,6,118,1,161,140,152,206,145,6,119,1,161,140,152,206,145,6,120,1,33,0,140,152,206,145,6,103,5,101,120,116,114,97,1,161,140,152,206,145,6,122,1,161,140,152,206,145,6,123,1,129,140,152,206,145,6,121,1,161,140,152,206,145,6,125,1,161,140,152,206,145,6,126,1,161,140,152,206,145,6,105,1,161,140,152,206,145,6,128,1,1,161,140,152,206,145,6,129,1,1,168,140,152,206,145,6,130,1,1,119,2,104,105,161,140,152,206,145,6,131,1,1,161,140,152,206,145,6,132,1,1,161,140,152,206,145,6,124,1,161,140,152,206,145,6,134,1,1,161,140,152,206,145,6,135,1,1,161,140,152,206,145,6,136,1,1,161,140,152,206,145,6,137,1,1,161,140,152,206,145,6,138,1,1,161,140,152,206,145,6,139,1,1,161,140,152,206,145,6,140,1,1,161,140,152,206,145,6,141,1,1,161,140,152,206,145,6,142,1,1,161,140,152,206,145,6,143,1,1,161,140,152,206,145,6,144,1,1,161,140,152,206,145,6,145,1,1,161,140,152,206,145,6,146,1,1,161,140,152,206,145,6,147,1,1,161,140,152,206,145,6,148,1,1,161,140,152,206,145,6,149,1,1,161,140,152,206,145,6,150,1,1,168,140,152,206,145,6,111,1,119,23,123,34,116,121,34,58,48,44,34,118,97,108,117,101,34,58,34,240,159,154,184,34,125,161,140,152,206,145,6,97,1,161,140,152,206,145,6,102,1,136,140,152,206,145,6,127,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,244,231,161,140,152,206,145,6,115,1,161,140,152,206,145,6,155,1,1,161,140,152,206,145,6,156,1,1,136,140,152,206,145,6,157,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,244,231,161,140,152,206,145,6,159,1,1,161,140,152,206,145,6,160,1,1,168,140,152,206,145,6,99,1,119,225,1,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,117,110,115,112,108,97,115,104,34,44,34,118,97,108,117,101,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,52,53,48,56,56,54,50,55,56,56,45,52,52,101,52,53,99,52,51,49,53,100,48,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,89,51,78,122,103,121,77,84,108,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,125,125,1,219,227,140,137,6,0,161,140,228,230,243,1,1,34,1,235,178,165,206,5,0,161,255,255,147,249,10,2,72,3,182,172,247,194,5,0,161,245,220,194,52,30,1,161,245,220,194,52,31,1,129,245,220,194,52,32,1,1,145,144,146,185,5,0,161,175,205,156,228,6,9,12,3,160,192,253,131,5,0,161,195,242,227,194,8,0,1,161,195,242,227,194,8,1,1,129,195,242,227,194,8,2,1,1,200,142,208,241,4,0,161,196,154,250,183,6,61,157,1,1,154,235,215,240,4,0,161,173,187,245,170,14,45,13,1,211,166,203,229,4,0,161,234,182,182,157,9,7,195,1,1,141,171,170,217,4,0,161,191,157,147,233,9,31,2,84,201,129,238,197,4,0,161,161,178,132,150,11,34,1,161,161,178,132,150,11,35,1,129,173,252,148,184,13,51,1,161,133,159,138,205,12,49,1,161,133,159,138,205,12,50,1,129,133,159,138,205,12,77,1,168,201,129,238,197,4,3,1,122,4,56,115,160,190,64,16,0,168,201,129,238,197,4,4,1,122,0,0,0,0,102,48,111,8,136,201,129,238,197,4,5,1,118,2,2,105,100,119,36,98,54,51,52,55,97,99,98,45,51,49,55,52,45,52,102,48,101,45,57,56,101,57,45,100,99,99,101,48,55,101,53,100,98,102,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,48,111,8,161,133,159,138,205,12,45,1,161,133,159,138,205,12,46,1,129,201,129,238,197,4,8,1,161,158,156,181,152,10,21,1,161,244,226,228,149,2,6,1,129,201,129,238,197,4,11,1,161,133,159,138,205,12,81,1,161,133,159,138,205,12,82,1,129,201,129,238,197,4,14,1,161,201,129,238,197,4,15,1,161,201,129,238,197,4,16,1,129,201,129,238,197,4,17,1,161,201,129,238,197,4,18,1,161,201,129,238,197,4,19,1,129,201,129,238,197,4,20,1,161,201,129,238,197,4,21,1,161,201,129,238,197,4,22,1,129,201,129,238,197,4,23,1,161,201,129,238,197,4,24,1,161,201,129,238,197,4,25,1,129,201,129,238,197,4,26,1,161,201,129,238,197,4,27,1,161,201,129,238,197,4,28,1,129,201,129,238,197,4,29,1,161,133,159,138,205,12,90,1,161,133,159,138,205,12,91,1,129,201,129,238,197,4,32,1,161,201,129,238,197,4,33,1,161,201,129,238,197,4,34,1,129,201,129,238,197,4,35,1,161,173,252,148,184,13,53,1,161,173,252,148,184,13,54,1,129,201,129,238,197,4,38,1,8,0,133,159,138,205,12,66,1,118,1,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,161,133,159,138,205,12,65,1,161,201,129,238,197,4,37,1,39,0,203,184,221,173,11,1,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,1,40,0,201,129,238,197,4,45,2,105,100,1,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,40,0,201,129,238,197,4,45,4,110,97,109,101,1,119,5,66,111,97,114,100,40,0,201,129,238,197,4,45,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,201,129,238,197,4,45,4,100,101,115,99,1,119,0,40,0,201,129,238,197,4,45,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,201,129,238,197,4,45,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,0,40,0,201,129,238,197,4,45,4,105,99,111,110,1,119,0,40,0,201,129,238,197,4,45,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,201,129,238,197,4,45,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,201,129,238,197,4,45,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,136,201,129,238,197,4,42,1,118,1,2,105,100,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,161,201,129,238,197,4,43,1,161,201,129,238,197,4,44,1,39,0,203,184,221,173,11,1,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,1,40,0,201,129,238,197,4,60,2,105,100,1,119,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,40,0,201,129,238,197,4,60,4,110,97,109,101,1,119,8,67,97,108,101,110,100,97,114,40,0,201,129,238,197,4,60,3,98,105,100,1,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,40,0,201,129,238,197,4,60,4,100,101,115,99,1,119,0,40,0,201,129,238,197,4,60,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,33,0,201,129,238,197,4,60,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,49,52,51,101,57,53,100,45,53,100,99,98,45,52,101,48,102,45,98,98,50,99,45,53,48,57,52,52,101,54,101,48,49,57,102,0,40,0,201,129,238,197,4,60,4,105,99,111,110,1,119,0,40,0,201,129,238,197,4,60,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,201,129,238,197,4,60,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,201,129,238,197,4,60,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,201,129,238,197,4,36,1,161,201,129,238,197,4,59,1,161,133,159,138,205,12,92,1,161,201,129,238,197,4,72,1,161,201,129,238,197,4,73,1,161,201,129,238,197,4,74,1,161,201,129,238,197,4,75,1,161,201,129,238,197,4,76,1,161,201,129,238,197,4,77,1,161,201,129,238,197,4,78,1,161,201,129,238,197,4,79,1,168,201,129,238,197,4,80,1,119,4,71,114,105,100,1,182,143,233,195,4,0,161,205,149,231,236,11,67,111,1,178,203,205,182,4,0,161,227,209,197,253,2,13,1,31,149,249,242,175,4,0,161,213,161,242,209,13,28,1,161,213,161,242,209,13,29,1,33,0,203,184,221,173,11,23,5,101,120,116,114,97,1,161,149,249,242,175,4,0,1,161,149,249,242,175,4,1,1,129,201,191,159,147,14,2,1,161,133,159,138,205,12,122,1,161,133,159,138,205,12,121,1,136,213,161,242,209,13,26,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,60,201,181,2,105,100,119,36,102,48,99,53,57,57,50,49,45,48,52,101,101,45,52,57,55,49,45,57,57,53,99,45,55,57,98,55,102,100,56,99,48,48,101,50,168,149,249,242,175,4,6,1,122,4,56,115,160,190,64,16,0,168,149,249,242,175,4,7,1,122,0,0,0,0,102,60,201,181,161,133,159,138,205,12,107,1,161,133,159,138,205,12,106,1,136,149,249,242,175,4,8,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,60,201,183,2,105,100,119,36,101,57,55,56,55,55,102,53,45,99,51,54,53,45,52,48,50,53,45,57,101,54,97,45,101,53,57,48,99,52,98,49,57,100,98,98,168,149,249,242,175,4,11,1,122,4,56,115,160,190,64,16,0,168,149,249,242,175,4,12,1,122,0,0,0,0,102,60,201,183,161,201,191,159,147,14,0,1,161,201,191,159,147,14,1,1,129,149,249,242,175,4,5,1,161,149,249,242,175,4,16,1,161,149,249,242,175,4,17,1,129,149,249,242,175,4,18,1,161,149,249,242,175,4,19,1,161,149,249,242,175,4,20,1,129,149,249,242,175,4,21,1,161,149,249,242,175,4,22,1,161,149,249,242,175,4,23,1,129,149,249,242,175,4,24,1,161,149,249,242,175,4,25,1,161,149,249,242,175,4,26,1,129,149,249,242,175,4,27,1,12,163,236,177,169,4,0,161,154,243,157,196,14,9,1,161,154,243,157,196,14,10,1,129,154,243,157,196,14,11,1,161,163,236,177,169,4,0,1,161,163,236,177,169,4,1,1,129,163,236,177,169,4,2,1,161,163,236,177,169,4,3,1,161,163,236,177,169,4,4,1,129,163,236,177,169,4,5,1,161,163,236,177,169,4,6,1,161,163,236,177,169,4,7,1,129,163,236,177,169,4,8,1,73,234,153,236,158,4,0,161,165,139,157,171,15,100,1,161,165,139,157,171,15,101,1,129,165,139,157,171,15,102,1,161,165,139,157,171,15,88,1,161,165,139,157,171,15,89,1,129,234,153,236,158,4,2,1,136,165,139,157,171,15,18,1,118,1,2,105,100,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,161,165,139,157,171,15,19,1,161,165,139,157,171,15,20,1,39,0,203,184,221,173,11,1,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,1,40,0,234,153,236,158,4,9,2,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,33,0,234,153,236,158,4,9,4,110,97,109,101,1,40,0,234,153,236,158,4,9,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,234,153,236,158,4,9,4,100,101,115,99,1,119,0,40,0,234,153,236,158,4,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,2,33,0,234,153,236,158,4,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,0,40,0,234,153,236,158,4,9,4,105,99,111,110,1,119,0,40,0,234,153,236,158,4,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,234,153,236,158,4,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,234,153,236,158,4,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,165,139,157,171,15,33,1,161,234,153,236,158,4,20,1,161,234,153,236,158,4,19,1,129,234,153,236,158,4,5,1,8,0,234,153,236,158,4,16,1,118,1,2,105,100,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,168,234,153,236,158,4,15,1,122,0,0,0,0,102,76,39,166,161,234,153,236,158,4,23,1,39,0,203,184,221,173,11,1,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,1,40,0,234,153,236,158,4,28,2,105,100,1,119,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,40,0,234,153,236,158,4,28,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,234,153,236,158,4,28,3,98,105,100,1,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,40,0,234,153,236,158,4,28,4,100,101,115,99,1,119,0,40,0,234,153,236,158,4,28,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,234,153,236,158,4,28,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,76,39,166,39,0,203,184,221,173,11,4,36,101,52,99,56,57,52,50,49,45,49,50,98,50,45,52,100,48,50,45,56,54,51,100,45,50,48,57,52,57,101,101,99,57,50,55,49,0,40,0,234,153,236,158,4,28,4,105,99,111,110,1,119,0,40,0,234,153,236,158,4,28,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,234,153,236,158,4,28,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,234,153,236,158,4,28,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,234,153,236,158,4,22,1,161,234,153,236,158,4,27,1,129,234,153,236,158,4,24,1,161,234,153,236,158,4,40,1,161,234,153,236,158,4,41,1,168,234,153,236,158,4,11,1,119,14,99,104,101,99,107,98,111,120,32,98,111,97,114,100,161,234,153,236,158,4,43,1,161,234,153,236,158,4,44,1,129,234,153,236,158,4,42,1,161,234,153,236,158,4,46,1,161,234,153,236,158,4,47,1,129,234,153,236,158,4,48,1,161,234,153,236,158,4,49,1,161,234,153,236,158,4,50,1,129,234,153,236,158,4,51,1,161,165,139,157,171,15,94,1,161,165,139,157,171,15,95,1,129,234,153,236,158,4,54,1,161,234,153,236,158,4,55,1,161,234,153,236,158,4,56,1,129,234,153,236,158,4,57,1,161,234,153,236,158,4,0,1,161,234,153,236,158,4,1,1,129,234,153,236,158,4,60,1,161,234,153,236,158,4,58,1,161,234,153,236,158,4,59,1,129,234,153,236,158,4,63,1,161,234,153,236,158,4,64,1,161,234,153,236,158,4,65,1,129,234,153,236,158,4,66,1,161,234,153,236,158,4,67,1,161,234,153,236,158,4,68,1,129,234,153,236,158,4,69,1,3,141,205,220,149,4,0,161,214,168,149,214,3,12,1,161,214,168,149,214,3,13,1,129,214,168,149,214,3,14,1,17,193,249,142,142,4,0,161,141,216,158,150,1,0,1,161,141,216,158,150,1,1,1,129,141,216,158,150,1,2,1,161,225,248,138,176,2,45,1,161,222,205,223,235,7,21,1,161,225,248,138,176,2,19,1,129,193,249,142,142,4,2,1,161,193,249,142,142,4,4,1,161,193,249,142,142,4,5,1,129,193,249,142,142,4,6,1,161,193,249,142,142,4,0,1,161,193,249,142,142,4,1,1,129,193,249,142,142,4,9,1,161,193,249,142,142,4,3,1,161,193,249,142,142,4,10,1,161,193,249,142,142,4,11,1,129,193,249,142,142,4,12,1,20,206,220,129,131,4,0,161,235,225,184,133,10,0,1,161,235,225,184,133,10,1,1,129,235,225,184,133,10,2,1,72,233,165,139,246,14,7,1,118,1,2,105,100,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,161,233,165,139,246,14,8,1,161,233,165,139,246,14,9,1,39,0,203,184,221,173,11,1,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,1,40,0,206,220,129,131,4,6,2,105,100,1,119,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,40,0,206,220,129,131,4,6,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,206,220,129,131,4,6,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,206,220,129,131,4,6,4,100,101,115,99,1,119,0,40,0,206,220,129,131,4,6,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,206,220,129,131,4,6,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,241,122,107,39,0,203,184,221,173,11,4,36,99,55,52,55,53,49,50,51,45,56,50,51,57,45,52,98,98,49,45,56,100,102,53,45,54,56,49,52,56,48,102,101,57,53,52,99,0,40,0,206,220,129,131,4,6,4,105,99,111,110,1,119,0,40,0,206,220,129,131,4,6,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,206,220,129,131,4,6,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,206,220,129,131,4,6,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,206,220,129,131,4,16,1,129,206,220,129,131,4,2,1,1,128,252,161,128,4,0,161,232,207,157,148,2,1,33,3,169,197,188,221,3,0,161,154,193,208,134,10,3,1,161,154,193,208,134,10,4,1,129,154,193,208,134,10,5,1,15,214,168,149,214,3,0,161,169,197,188,221,3,0,1,161,169,197,188,221,3,1,1,129,169,197,188,221,3,2,1,161,233,247,183,159,1,0,1,161,158,156,181,152,10,3,1,161,158,156,181,152,10,4,1,129,154,193,208,134,10,8,1,161,214,168,149,214,3,3,1,161,154,193,208,134,10,6,1,161,154,193,208,134,10,7,1,129,214,168,149,214,3,6,1,161,214,168,149,214,3,7,1,161,171,204,155,217,8,0,1,161,171,204,155,217,8,1,1,129,214,168,149,214,3,10,1,1,158,184,218,165,3,0,161,139,152,215,249,10,33,38,1,239,199,189,146,3,0,161,154,235,215,240,4,12,24,1,227,209,197,253,2,0,161,181,175,219,209,12,9,14,15,226,212,179,248,2,0,161,214,139,213,136,8,63,1,161,214,139,213,136,8,64,1,129,214,139,213,136,8,65,1,161,226,212,179,248,2,0,1,161,226,212,179,248,2,1,1,129,226,212,179,248,2,2,1,161,226,212,179,248,2,3,1,161,226,212,179,248,2,4,1,129,226,212,179,248,2,5,1,161,193,249,142,142,4,7,1,161,241,155,213,233,1,8,1,129,226,212,179,248,2,8,1,161,226,212,179,248,2,6,1,161,226,212,179,248,2,7,1,129,226,212,179,248,2,11,1,3,157,240,144,231,2,0,161,213,161,242,209,13,32,1,161,213,161,242,209,13,33,1,129,213,161,242,209,13,34,1,1,128,211,179,216,2,0,161,236,229,225,232,8,115,10,1,138,202,240,189,2,0,161,229,153,197,202,7,240,6,134,1,1,200,203,236,184,2,0,161,138,202,240,189,2,133,1,35,49,225,248,138,176,2,0,161,171,142,166,254,1,3,1,161,171,142,166,254,1,4,1,129,171,142,166,254,1,5,1,161,222,205,223,235,7,27,1,161,173,252,148,184,13,45,1,161,173,252,148,184,13,46,1,129,225,248,138,176,2,2,1,161,225,248,138,176,2,4,1,161,225,248,138,176,2,5,1,136,225,248,138,176,2,6,1,118,2,2,105,100,119,36,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,241,161,170,140,240,234,9,7,1,161,170,140,240,234,9,8,1,136,225,248,138,176,2,9,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,243,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,161,225,248,138,176,2,3,1,168,225,248,138,176,2,10,1,122,4,56,115,160,190,64,16,0,168,225,248,138,176,2,11,1,122,0,0,0,0,102,68,43,244,136,225,248,138,176,2,12,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,68,43,244,2,105,100,119,36,52,56,99,53,50,99,102,55,45,98,102,57,56,45,52,51,102,97,45,57,54,97,100,45,98,51,49,97,97,100,101,57,98,48,55,49,136,133,159,138,205,12,56,1,118,1,2,105,100,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,161,133,159,138,205,12,57,1,161,222,205,223,235,7,22,1,39,0,203,184,221,173,11,1,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,1,40,0,225,248,138,176,2,20,2,105,100,1,119,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,40,0,225,248,138,176,2,20,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,225,248,138,176,2,20,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,225,248,138,176,2,20,4,100,101,115,99,1,119,0,40,0,225,248,138,176,2,20,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,33,0,225,248,138,176,2,20,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,50,99,49,101,101,57,53,97,45,49,98,48,57,45,52,97,49,102,45,56,100,53,101,45,53,48,49,98,99,52,56,54,49,97,57,100,0,40,0,225,248,138,176,2,20,4,105,99,111,110,1,119,0,40,0,225,248,138,176,2,20,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,225,248,138,176,2,20,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,225,248,138,176,2,20,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,225,248,138,176,2,13,1,161,225,248,138,176,2,31,1,161,225,248,138,176,2,30,1,129,225,248,138,176,2,16,1,161,225,248,138,176,2,33,1,161,225,248,138,176,2,34,1,129,225,248,138,176,2,35,1,161,225,248,138,176,2,36,1,161,225,248,138,176,2,37,1,129,225,248,138,176,2,38,1,161,225,248,138,176,2,0,1,161,225,248,138,176,2,1,1,129,225,248,138,176,2,41,1,161,225,248,138,176,2,32,1,161,225,248,138,176,2,42,1,161,225,248,138,176,2,43,1,129,225,248,138,176,2,44,1,29,244,226,228,149,2,0,39,0,203,184,221,173,11,2,7,112,114,105,118,97,116,101,1,161,158,156,181,152,10,24,1,161,207,228,238,162,8,24,1,129,207,228,238,162,8,49,1,8,0,158,156,181,152,10,16,1,118,1,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,168,158,156,181,152,10,15,1,122,0,0,0,0,101,251,252,70,161,158,156,181,152,10,22,1,39,0,203,184,221,173,11,1,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,1,40,0,244,226,228,149,2,7,2,105,100,1,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,40,0,244,226,228,149,2,7,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,244,226,228,149,2,7,3,98,105,100,1,119,36,100,49,52,50,51,57,101,57,45,50,98,102,55,45,52,56,50,52,45,57,51,101,99,45,52,102,51,99,99,53,54,49,54,55,50,48,40,0,244,226,228,149,2,7,4,100,101,115,99,1,119,0,40,0,244,226,228,149,2,7,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,244,226,228,149,2,7,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,101,251,252,70,39,0,203,184,221,173,11,4,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,0,40,0,244,226,228,149,2,7,4,105,99,111,110,1,119,0,40,0,244,226,228,149,2,7,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,244,226,228,149,2,7,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,244,226,228,149,2,7,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,2,161,244,226,228,149,2,17,1,39,0,244,226,228,149,2,0,18,51,48,52,49,50,48,49,48,57,48,55,49,51,51,57,53,50,48,0,8,0,244,226,228,149,2,21,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,101,251,252,70,2,105,100,119,36,48,53,51,51,50,98,97,52,45,97,54,57,48,45,52,50,57,51,45,57,56,54,54,45,56,52,100,97,99,55,102,101,50,97,101,97,161,207,228,238,162,8,37,1,161,244,226,228,149,2,19,1,161,244,226,228,149,2,20,1,129,244,226,228,149,2,3,1,161,244,226,228,149,2,24,1,161,244,226,228,149,2,25,1,129,244,226,228,149,2,26,1,1,232,207,157,148,2,0,161,141,171,170,217,4,1,2,6,171,142,166,254,1,0,161,143,184,153,180,6,3,1,161,143,184,153,180,6,4,1,129,143,184,153,180,6,5,1,161,171,142,166,254,1,0,1,161,171,142,166,254,1,1,1,129,171,142,166,254,1,2,1,1,140,228,230,243,1,0,161,250,198,166,187,7,1,2,3,147,206,229,235,1,0,161,229,154,128,197,12,3,1,161,229,154,128,197,12,4,1,129,162,159,252,196,11,2,1,52,241,155,213,233,1,0,161,234,153,236,158,4,70,1,161,234,153,236,158,4,71,1,129,234,153,236,158,4,72,1,161,234,153,236,158,4,52,1,161,234,153,236,158,4,53,1,129,241,155,213,233,1,2,1,136,234,153,236,158,4,6,1,118,1,2,105,100,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,161,234,153,236,158,4,7,1,161,234,153,236,158,4,8,1,39,0,203,184,221,173,11,1,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,1,40,0,241,155,213,233,1,9,2,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,33,0,241,155,213,233,1,9,4,110,97,109,101,1,40,0,241,155,213,233,1,9,3,98,105,100,1,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,40,0,241,155,213,233,1,9,4,100,101,115,99,1,119,0,40,0,241,155,213,233,1,9,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,3,33,0,241,155,213,233,1,9,10,99,114,101,97,116,101,100,95,97,116,1,39,0,203,184,221,173,11,4,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,0,40,0,241,155,213,233,1,9,4,105,99,111,110,1,119,0,40,0,241,155,213,233,1,9,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,241,155,213,233,1,9,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,241,155,213,233,1,9,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,234,153,236,158,4,21,1,161,241,155,213,233,1,20,1,161,241,155,213,233,1,19,1,129,241,155,213,233,1,5,1,161,241,155,213,233,1,22,1,161,241,155,213,233,1,23,1,161,241,155,213,233,1,11,1,161,241,155,213,233,1,25,1,161,241,155,213,233,1,26,1,161,241,155,213,233,1,27,1,161,241,155,213,233,1,28,1,161,241,155,213,233,1,29,1,161,241,155,213,233,1,30,1,161,241,155,213,233,1,31,1,161,241,155,213,233,1,32,1,161,241,155,213,233,1,33,1,161,241,155,213,233,1,34,1,161,241,155,213,233,1,35,1,161,241,155,213,233,1,36,1,161,241,155,213,233,1,37,1,161,241,155,213,233,1,38,1,161,241,155,213,233,1,39,1,161,241,155,213,233,1,40,1,161,241,155,213,233,1,41,1,161,241,155,213,233,1,42,1,161,241,155,213,233,1,43,1,161,241,155,213,233,1,44,1,161,241,155,213,233,1,45,1,161,241,155,213,233,1,46,1,161,241,155,213,233,1,47,1,168,241,155,213,233,1,48,1,119,8,67,97,108,101,110,100,97,114,1,240,253,240,229,1,0,161,178,203,205,182,4,0,79,20,176,154,159,227,1,0,33,0,203,184,221,173,11,2,7,112,114,105,118,97,116,101,1,136,130,180,254,251,6,0,1,118,1,2,105,100,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,161,130,180,254,251,6,1,1,161,130,180,254,251,6,2,1,39,0,203,184,221,173,11,1,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,1,40,0,176,154,159,227,1,4,2,105,100,1,119,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,40,0,176,154,159,227,1,4,4,110,97,109,101,1,119,0,40,0,176,154,159,227,1,4,3,98,105,100,1,119,36,57,101,101,98,101,97,48,51,45,51,101,100,53,45,52,50,57,56,45,56,54,98,50,45,97,55,102,55,55,56,53,54,100,52,56,98,40,0,176,154,159,227,1,4,4,100,101,115,99,1,119,0,40,0,176,154,159,227,1,4,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,176,154,159,227,1,4,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,12,194,112,39,0,203,184,221,173,11,4,36,54,53,98,48,54,100,98,56,45,55,48,54,49,45,52,98,102,54,45,98,51,49,53,45,55,53,56,99,48,100,100,50,53,99,100,102,0,40,0,176,154,159,227,1,4,4,105,99,111,110,1,119,0,40,0,176,154,159,227,1,4,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,176,154,159,227,1,4,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,176,154,159,227,1,4,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,161,207,228,238,162,8,37,1,161,176,154,159,227,1,15,1,161,176,154,159,227,1,14,1,129,207,228,238,162,8,49,1,2,175,147,217,214,1,0,161,246,185,174,192,6,87,1,161,246,185,174,192,6,89,32,6,202,160,246,212,1,0,161,178,162,190,217,10,0,1,161,178,162,190,217,10,1,1,161,178,162,190,217,10,2,1,161,202,160,246,212,1,0,1,161,202,160,246,212,1,1,1,161,202,160,246,212,1,2,1,39,137,164,190,210,1,0,168,166,201,221,141,13,69,1,122,4,56,115,160,190,64,16,0,168,166,201,221,141,13,70,1,122,0,0,0,0,102,86,193,57,136,166,201,221,141,13,71,1,118,2,2,105,100,119,36,50,97,54,101,53,101,50,49,45,97,57,51,56,45,52,53,97,53,45,97,52,52,53,45,100,48,98,55,49,52,57,53,98,48,55,55,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,86,193,57,161,174,151,139,93,254,1,1,161,174,151,139,93,255,1,1,136,162,238,198,212,7,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,240,30,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,161,247,200,243,247,14,15,1,168,137,164,190,210,1,3,1,122,4,56,115,160,190,64,16,0,168,137,164,190,210,1,4,1,122,0,0,0,0,102,87,240,30,136,137,164,190,210,1,5,1,118,2,2,105,100,119,36,101,52,49,48,55,52,55,98,45,53,102,50,102,45,52,53,97,48,45,98,50,102,55,45,56,57,48,97,100,51,48,48,49,51,53,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,87,240,30,39,0,203,184,221,173,11,1,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,1,40,0,137,164,190,210,1,10,2,105,100,1,119,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,40,0,137,164,190,210,1,10,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,137,164,190,210,1,10,3,98,105,100,1,119,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,40,0,137,164,190,210,1,10,4,100,101,115,99,1,119,0,40,0,137,164,190,210,1,10,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,137,164,190,210,1,10,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,87,240,32,39,0,203,184,221,173,11,4,36,48,51,50,55,98,54,57,52,45,50,100,101,51,45,53,101,48,48,45,98,51,54,56,45,57,56,99,97,49,56,50,51,48,102,97,57,0,40,0,137,164,190,210,1,10,4,105,99,111,110,1,119,0,40,0,137,164,190,210,1,10,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,137,164,190,210,1,10,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,137,164,190,210,1,10,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,137,164,190,210,1,21,1,122,4,56,115,160,190,64,16,0,168,137,164,190,210,1,20,1,122,0,0,0,0,102,87,240,32,40,0,137,164,190,210,1,10,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,137,164,190,210,1,6,1,161,166,201,221,141,13,13,1,161,166,201,221,141,13,14,1,136,137,164,190,210,1,9,1,118,2,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,24,223,161,137,164,190,210,1,26,1,161,137,164,190,210,1,27,1,136,137,164,190,210,1,28,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,24,223,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,162,238,198,212,7,0,1,161,162,238,198,212,7,1,1,136,137,164,190,210,1,31,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,31,80,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,161,137,164,190,210,1,25,1,161,137,164,190,210,1,32,1,161,137,164,190,210,1,33,1,136,137,164,190,210,1,34,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,31,80,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,1,152,252,186,192,1,0,161,211,166,203,229,4,194,1,2,3,248,136,168,181,1,0,161,197,254,154,201,10,0,1,161,197,254,154,201,10,1,1,129,197,254,154,201,10,2,1,1,234,187,164,181,1,0,161,253,205,145,137,11,5,24,3,164,188,201,172,1,0,161,222,205,223,235,7,28,1,161,222,205,223,235,7,29,1,129,222,205,223,235,7,30,1,4,233,247,183,159,1,0,161,244,226,228,149,2,23,1,161,248,153,216,10,0,1,161,248,153,216,10,1,1,129,187,173,214,176,15,2,1,1,190,139,191,155,1,0,161,157,207,243,216,6,1,2,3,141,216,158,150,1,0,161,225,248,138,176,2,46,1,161,225,248,138,176,2,47,1,129,225,248,138,176,2,48,1,1,213,255,156,145,1,0,161,177,219,160,167,7,3,2,38,170,255,211,105,0,161,140,152,206,145,6,162,1,1,161,140,152,206,145,6,163,1,1,136,140,152,206,145,6,161,1,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,42,161,245,220,194,52,3,1,161,245,220,194,52,4,1,129,245,220,194,52,11,1,161,140,152,206,145,6,158,1,1,161,170,255,211,105,3,1,161,170,255,211,105,4,1,129,170,255,211,105,5,1,161,140,152,206,145,6,37,1,161,140,152,206,145,6,38,1,129,170,255,211,105,9,1,161,170,255,211,105,6,1,161,170,255,211,105,10,1,161,170,255,211,105,11,1,129,170,255,211,105,12,1,161,245,220,194,52,18,1,161,245,220,194,52,19,1,129,170,255,211,105,16,1,161,170,255,211,105,13,1,161,170,255,211,105,17,1,161,170,255,211,105,18,1,129,170,255,211,105,19,1,161,170,255,211,105,14,1,161,170,255,211,105,15,1,129,170,255,211,105,23,1,161,170,255,211,105,20,1,168,170,255,211,105,24,1,122,4,56,115,160,190,64,16,0,168,170,255,211,105,25,1,122,0,0,0,0,102,83,252,197,136,170,255,211,105,26,1,118,2,2,105,100,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,252,197,161,245,220,194,52,24,1,161,245,220,194,52,25,1,129,170,255,211,105,30,1,161,170,255,211,105,27,1,161,170,255,211,105,31,1,161,170,255,211,105,32,1,129,170,255,211,105,33,1,179,2,174,151,139,93,0,161,177,161,136,243,11,7,1,161,177,161,136,243,11,8,1,129,177,161,136,243,11,9,1,161,177,161,136,243,11,0,1,161,177,161,136,243,11,1,1,129,174,151,139,93,2,1,161,177,161,136,243,11,6,1,161,174,151,139,93,3,1,161,174,151,139,93,4,1,129,174,151,139,93,5,1,161,165,139,157,171,15,14,1,161,165,139,157,171,15,13,1,129,174,151,139,93,9,1,161,174,151,139,93,6,1,161,174,151,139,93,10,1,161,174,151,139,93,11,1,129,174,151,139,93,12,1,161,174,151,139,93,13,1,161,174,151,139,93,7,1,161,174,151,139,93,8,1,129,174,151,139,93,16,1,161,174,151,139,93,18,1,161,174,151,139,93,19,1,129,174,151,139,93,20,1,161,241,155,213,233,1,3,1,161,241,155,213,233,1,4,1,129,174,151,139,93,23,1,161,174,151,139,93,17,1,161,174,151,139,93,24,1,161,174,151,139,93,25,1,129,174,151,139,93,26,1,161,174,151,139,93,21,1,161,174,151,139,93,22,1,129,174,151,139,93,30,1,161,174,151,139,93,27,1,161,174,151,139,93,31,1,161,174,151,139,93,32,1,129,174,151,139,93,33,1,161,174,151,139,93,0,1,161,174,151,139,93,1,1,129,174,151,139,93,37,1,161,174,151,139,93,34,1,161,174,151,139,93,38,1,161,174,151,139,93,39,1,129,174,151,139,93,40,1,39,0,203,184,221,173,11,1,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,1,40,0,174,151,139,93,45,2,105,100,1,119,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,40,0,174,151,139,93,45,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,45,3,98,105,100,1,119,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,40,0,174,151,139,93,45,4,100,101,115,99,1,119,0,40,0,174,151,139,93,45,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,45,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,101,46,39,0,203,184,221,173,11,4,36,102,53,54,98,100,102,48,102,45,57,48,99,56,45,53,51,102,98,45,57,55,100,57,45,97,100,53,56,54,48,100,50,98,55,97,48,0,40,0,174,151,139,93,45,4,105,99,111,110,1,119,0,40,0,174,151,139,93,45,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,45,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,45,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,56,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,55,1,122,0,0,0,0,102,77,101,47,40,0,174,151,139,93,45,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,42,1,161,174,151,139,93,43,1,129,174,151,139,93,44,1,161,174,151,139,93,60,1,161,174,151,139,93,61,1,129,174,151,139,93,62,1,161,234,153,236,158,4,61,1,161,234,153,236,158,4,62,1,129,174,151,139,93,65,1,161,174,151,139,93,41,1,161,174,151,139,93,66,1,161,174,151,139,93,67,1,129,174,151,139,93,68,1,161,174,151,139,93,63,1,161,174,151,139,93,64,1,129,174,151,139,93,72,1,161,174,151,139,93,69,1,161,174,151,139,93,73,1,161,174,151,139,93,74,1,129,174,151,139,93,75,1,161,174,151,139,93,70,1,161,174,151,139,93,71,1,129,174,151,139,93,79,1,161,174,151,139,93,76,1,161,174,151,139,93,80,1,161,174,151,139,93,81,1,129,174,151,139,93,82,1,161,174,151,139,93,83,1,161,174,151,139,93,28,1,161,174,151,139,93,29,1,129,174,151,139,93,86,1,161,174,151,139,93,87,1,161,201,129,238,197,4,71,1,161,201,129,238,197,4,70,1,129,174,151,139,93,90,1,161,174,151,139,93,88,1,161,174,151,139,93,89,1,129,174,151,139,93,94,1,161,174,151,139,93,91,1,161,174,151,139,93,35,1,161,174,151,139,93,36,1,129,174,151,139,93,97,1,161,174,151,139,93,99,1,161,174,151,139,93,100,1,129,174,151,139,93,101,1,161,174,151,139,93,92,1,161,174,151,139,93,93,1,129,174,151,139,93,104,1,161,174,151,139,93,102,1,161,174,151,139,93,103,1,129,174,151,139,93,107,1,161,174,151,139,93,108,1,161,174,151,139,93,109,1,129,174,151,139,93,110,1,39,0,203,184,221,173,11,1,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,1,40,0,174,151,139,93,114,2,105,100,1,119,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,40,0,174,151,139,93,114,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,114,3,98,105,100,1,119,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,40,0,174,151,139,93,114,4,100,101,115,99,1,119,0,40,0,174,151,139,93,114,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,114,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,101,175,39,0,203,184,221,173,11,4,36,50,49,97,100,55,99,57,48,45,98,102,53,57,45,53,100,97,56,45,57,97,98,99,45,53,98,50,50,102,56,102,102,54,99,52,101,0,40,0,174,151,139,93,114,4,105,99,111,110,1,119,0,40,0,174,151,139,93,114,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,114,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,114,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,125,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,124,1,122,0,0,0,0,102,77,101,175,40,0,174,151,139,93,114,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,111,1,161,174,151,139,93,112,1,129,174,151,139,93,113,1,161,174,151,139,93,129,1,1,161,174,151,139,93,130,1,1,129,174,151,139,93,131,1,1,161,174,151,139,93,77,1,161,174,151,139,93,78,1,129,174,151,139,93,134,1,1,161,174,151,139,93,98,1,161,174,151,139,93,135,1,1,161,174,151,139,93,136,1,1,129,174,151,139,93,137,1,1,161,174,151,139,93,132,1,1,161,174,151,139,93,133,1,1,129,174,151,139,93,141,1,1,161,174,151,139,93,138,1,1,161,174,151,139,93,142,1,1,161,174,151,139,93,143,1,1,129,174,151,139,93,144,1,1,161,174,151,139,93,95,1,161,174,151,139,93,96,1,129,174,151,139,93,148,1,1,161,174,151,139,93,145,1,1,161,174,151,139,93,149,1,1,161,174,151,139,93,150,1,1,129,174,151,139,93,151,1,1,161,174,151,139,93,146,1,1,161,174,151,139,93,147,1,1,129,174,151,139,93,155,1,1,161,174,151,139,93,152,1,1,161,174,151,139,93,156,1,1,161,174,151,139,93,157,1,1,129,174,151,139,93,158,1,1,161,174,151,139,93,139,1,1,161,174,151,139,93,140,1,1,129,174,151,139,93,162,1,1,161,174,151,139,93,159,1,1,161,174,151,139,93,163,1,1,161,174,151,139,93,164,1,1,129,174,151,139,93,165,1,1,161,174,151,139,93,160,1,1,161,174,151,139,93,161,1,1,129,174,151,139,93,169,1,1,161,174,151,139,93,166,1,1,161,174,151,139,93,170,1,1,161,174,151,139,93,171,1,1,129,174,151,139,93,172,1,1,8,0,241,155,213,233,1,16,1,118,1,2,105,100,119,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,161,241,155,213,233,1,15,1,161,174,151,139,93,175,1,1,39,0,203,184,221,173,11,1,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,1,40,0,174,151,139,93,180,1,2,105,100,1,119,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,40,0,174,151,139,93,180,1,4,110,97,109,101,1,119,4,71,114,105,100,40,0,174,151,139,93,180,1,3,98,105,100,1,119,36,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,40,0,174,151,139,93,180,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,180,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,1,40,0,174,151,139,93,180,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,165,65,39,0,203,184,221,173,11,4,36,54,54,97,54,102,51,98,99,45,99,55,56,102,45,52,102,55,52,45,97,48,57,101,45,48,56,100,52,55,49,55,98,102,49,102,100,0,40,0,174,151,139,93,180,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,180,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,40,0,174,151,139,93,180,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,122,0,0,0,0,102,77,165,65,40,0,174,151,139,93,180,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,39,0,203,184,221,173,11,1,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,1,40,0,174,151,139,93,192,1,2,105,100,1,119,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,40,0,174,151,139,93,192,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,192,1,3,98,105,100,1,119,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,40,0,174,151,139,93,192,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,192,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,192,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,166,20,39,0,203,184,221,173,11,4,36,49,102,98,50,53,48,49,57,45,52,51,57,52,45,53,57,54,53,45,97,49,50,98,45,49,98,48,101,52,99,57,55,55,48,55,99,0,40,0,174,151,139,93,192,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,192,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,192,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,192,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,203,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,202,1,1,122,0,0,0,0,102,77,166,20,40,0,174,151,139,93,192,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,39,0,203,184,221,173,11,1,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,1,40,0,174,151,139,93,207,1,2,105,100,1,119,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,40,0,174,151,139,93,207,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,207,1,3,98,105,100,1,119,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,40,0,174,151,139,93,207,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,207,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,207,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,183,211,39,0,203,184,221,173,11,4,36,54,97,102,100,54,57,56,52,45,48,51,98,52,45,53,53,57,100,45,98,49,52,54,45,52,49,100,98,97,54,49,50,49,48,98,56,0,40,0,174,151,139,93,207,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,207,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,207,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,207,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,218,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,217,1,1,122,0,0,0,0,102,77,183,211,40,0,174,151,139,93,207,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,39,0,203,184,221,173,11,1,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,1,40,0,174,151,139,93,222,1,2,105,100,1,119,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,40,0,174,151,139,93,222,1,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,222,1,3,98,105,100,1,119,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,40,0,174,151,139,93,222,1,4,100,101,115,99,1,119,0,40,0,174,151,139,93,222,1,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,222,1,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,77,187,249,39,0,203,184,221,173,11,4,36,53,54,53,50,101,52,54,54,45,101,52,99,101,45,53,54,57,99,45,98,49,101,54,45,102,100,56,101,48,101,55,100,99,48,100,99,0,40,0,174,151,139,93,222,1,4,105,99,111,110,1,119,0,40,0,174,151,139,93,222,1,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,222,1,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,222,1,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,233,1,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,232,1,1,122,0,0,0,0,102,77,187,249,40,0,174,151,139,93,222,1,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,167,1,1,161,174,151,139,93,168,1,1,129,174,151,139,93,176,1,1,161,174,151,139,93,173,1,1,161,174,151,139,93,237,1,1,161,174,151,139,93,238,1,1,129,174,151,139,93,239,1,1,161,174,151,139,93,241,1,1,161,174,151,139,93,242,1,1,129,241,130,161,205,7,11,1,161,174,151,139,93,244,1,1,161,174,151,139,93,245,1,1,129,174,151,139,93,246,1,1,161,241,130,161,205,7,3,1,161,241,130,161,205,7,4,1,129,174,151,139,93,249,1,1,161,174,151,139,93,240,1,1,161,174,151,139,93,250,1,1,161,174,151,139,93,251,1,1,129,174,151,139,93,252,1,1,161,174,151,139,93,247,1,1,161,174,151,139,93,248,1,1,129,174,151,139,93,128,2,1,161,174,151,139,93,253,1,1,161,174,151,139,93,129,2,1,161,174,151,139,93,130,2,1,129,174,151,139,93,131,2,1,161,226,212,179,248,2,9,1,161,226,212,179,248,2,10,1,129,174,151,139,93,135,2,1,161,174,151,139,93,132,2,1,161,174,151,139,93,136,2,1,161,174,151,139,93,137,2,1,129,174,151,139,93,138,2,1,161,174,151,139,93,133,2,1,161,174,151,139,93,134,2,1,129,174,151,139,93,142,2,1,161,174,151,139,93,139,2,1,161,174,151,139,93,143,2,1,161,174,151,139,93,144,2,1,129,174,151,139,93,145,2,1,39,0,203,184,221,173,11,1,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,1,40,0,174,151,139,93,150,2,2,105,100,1,119,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,40,0,174,151,139,93,150,2,4,110,97,109,101,1,119,8,85,110,116,105,116,108,101,100,40,0,174,151,139,93,150,2,3,98,105,100,1,119,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,40,0,174,151,139,93,150,2,4,100,101,115,99,1,119,0,40,0,174,151,139,93,150,2,6,108,97,121,111,117,116,1,122,0,0,0,0,0,0,0,0,40,0,174,151,139,93,150,2,10,99,114,101,97,116,101,100,95,97,116,1,122,0,0,0,0,102,78,228,14,39,0,203,184,221,173,11,4,36,51,50,48,102,56,48,97,100,45,50,51,52,101,45,53,49,55,50,45,97,49,55,50,45,102,55,56,52,54,52,55,50,55,98,100,97,0,40,0,174,151,139,93,150,2,4,105,99,111,110,1,119,0,40,0,174,151,139,93,150,2,10,99,114,101,97,116,101,100,95,98,121,1,122,4,56,115,160,190,64,16,0,33,0,174,151,139,93,150,2,16,108,97,115,116,95,101,100,105,116,101,100,95,116,105,109,101,1,33,0,174,151,139,93,150,2,14,108,97,115,116,95,101,100,105,116,101,100,95,98,121,1,168,174,151,139,93,161,2,1,122,4,56,115,160,190,64,16,0,168,174,151,139,93,160,2,1,122,0,0,0,0,102,78,228,14,40,0,174,151,139,93,150,2,5,101,120,116,114,97,1,119,36,123,34,99,111,118,101,114,34,58,123,34,116,121,112,101,34,58,34,110,111,110,101,34,44,34,118,97,108,117,101,34,58,34,34,125,125,161,174,151,139,93,14,1,161,174,151,139,93,15,1,129,174,151,139,93,149,2,1,161,174,151,139,93,146,2,1,168,174,151,139,93,165,2,1,122,4,56,115,160,190,64,16,0,161,174,151,139,93,166,2,1,136,174,151,139,93,167,2,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,78,229,144,2,105,100,119,36,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,161,174,151,139,93,147,2,1,161,174,151,139,93,148,2,1,129,174,151,139,93,171,2,1,161,174,151,139,93,168,2,1,161,174,151,139,93,172,2,1,161,174,151,139,93,173,2,1,129,174,151,139,93,174,2,1,1,184,231,170,67,0,161,145,144,146,185,5,11,6,39,245,220,194,52,0,161,209,250,203,254,15,0,1,161,209,250,203,254,15,1,1,136,209,250,203,254,15,2,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,151,161,140,152,206,145,6,152,1,1,161,140,152,206,145,6,153,1,1,129,245,220,194,52,2,1,161,245,220,194,52,0,1,161,245,220,194,52,1,1,136,245,220,194,52,5,1,118,2,2,105,100,119,36,50,54,100,53,99,56,99,49,45,49,99,54,54,45,52,53,57,99,45,98,99,54,99,45,102,52,100,97,49,97,54,54,51,51,52,56,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,202,161,140,152,206,145,6,87,1,161,140,152,206,145,6,88,1,136,245,220,194,52,8,1,118,2,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,83,246,206,2,105,100,119,36,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,161,245,220,194,52,3,1,161,245,220,194,52,4,1,129,245,220,194,52,11,1,161,170,255,211,105,7,1,161,170,255,211,105,8,1,161,140,152,206,145,6,151,1,1,161,245,220,194,52,15,1,161,245,220,194,52,16,1,161,245,220,194,52,17,1,161,170,255,211,105,21,1,161,170,255,211,105,22,1,161,245,220,194,52,20,1,161,245,220,194,52,21,1,161,245,220,194,52,22,1,161,245,220,194,52,23,1,161,170,255,211,105,35,1,161,170,255,211,105,36,1,129,170,255,211,105,37,1,161,245,220,194,52,27,1,161,245,220,194,52,28,1,129,245,220,194,52,29,1,161,245,220,194,52,30,1,161,245,220,194,52,31,1,161,245,220,194,52,26,1,161,245,220,194,52,33,1,161,245,220,194,52,34,1,161,245,220,194,52,35,1,6,166,203,155,46,0,161,140,152,206,145,6,30,1,161,140,152,206,145,6,31,1,129,162,238,198,212,7,2,1,168,166,203,155,46,0,1,122,4,56,115,160,190,64,16,0,168,166,203,155,46,1,1,122,0,0,0,0,102,88,52,80,136,166,203,155,46,2,1,118,2,2,105,100,119,36,48,99,101,49,51,52,49,53,45,54,99,99,101,45,52,52,57,55,45,57,52,99,54,45,52,55,53,97,100,57,54,99,50,52,57,101,9,116,105,109,101,115,116,97,109,112,122,0,0,0,0,102,88,52,80,3,179,252,154,44,0,161,157,240,144,231,2,0,1,161,157,240,144,231,2,1,1,129,157,240,144,231,2,2,1,3,248,153,216,10,0,161,163,236,177,169,4,9,1,161,163,236,177,169,4,10,1,129,163,236,177,169,4,11,1,158,1,128,252,161,128,4,1,0,33,130,180,254,251,6,4,0,3,5,2,9,1,13,4,135,240,136,178,10,1,0,45,135,232,133,203,9,1,0,15,137,226,192,199,6,1,0,20,138,202,240,189,2,1,0,134,1,139,240,196,145,14,1,0,138,2,140,228,230,243,1,1,0,2,139,152,215,249,10,1,0,34,141,216,158,150,1,1,0,3,142,130,192,134,11,1,0,3,143,184,153,180,6,1,0,6,145,190,137,224,10,1,0,2,145,144,146,185,5,1,0,12,140,152,206,145,6,13,0,60,61,24,86,3,90,6,97,3,101,2,105,1,111,1,113,20,134,1,20,155,1,2,158,1,3,162,1,2,135,166,246,235,6,1,0,2,147,206,229,235,1,1,0,3,137,164,190,210,1,7,3,2,6,1,20,2,25,3,29,2,32,2,35,3,152,252,186,192,1,1,0,2,153,130,203,161,6,1,0,97,154,244,246,165,8,10,0,24,26,1,37,2,56,1,71,1,82,3,88,36,127,18,148,1,35,193,1,2,156,148,170,169,10,1,0,16,157,240,144,231,2,1,0,3,158,156,181,152,10,7,0,6,7,2,15,1,19,8,28,2,32,1,40,9,159,156,204,250,6,1,0,27,160,192,253,131,5,1,0,3,161,178,132,150,11,4,0,15,17,1,28,64,93,8,158,182,250,251,9,1,0,15,163,236,177,169,4,1,0,12,164,188,201,172,1,1,0,3,158,184,218,165,3,1,0,38,162,238,198,212,7,1,0,2,170,140,240,234,9,1,0,31,171,204,155,217,8,1,0,3,171,142,166,254,1,1,0,6,173,252,148,184,13,3,0,40,41,2,44,20,176,154,159,227,1,3,0,1,2,2,14,6,178,162,190,217,10,1,0,3,179,252,154,44,1,0,3,180,230,210,212,13,2,0,2,3,4,182,172,247,194,5,1,0,3,183,226,184,158,8,1,0,8,187,220,199,239,8,9,1,1,3,2,6,1,15,1,19,1,23,17,43,1,56,4,63,3,195,242,227,194,8,1,0,3,196,154,250,183,6,1,0,62,197,254,154,201,10,1,0,3,200,142,208,241,4,1,0,157,1,202,160,246,212,1,1,0,6,203,184,221,173,11,6,14,1,19,1,21,2,29,1,33,2,36,1,206,220,129,131,4,3,0,3,4,2,16,5,207,228,238,162,8,4,1,2,13,9,23,2,35,15,209,250,203,254,15,1,0,2,210,228,153,221,12,1,0,3,211,166,203,229,4,1,0,195,1,211,202,217,232,12,1,0,7,214,168,149,214,3,1,0,15,219,220,239,171,8,1,0,10,225,248,138,176,2,6,0,9,10,2,13,1,18,2,26,1,30,19,226,212,179,248,2,1,0,15,229,154,128,197,12,4,0,6,7,2,15,1,19,5,234,182,182,157,9,1,0,8,235,178,165,206,5,1,0,72,234,156,130,211,12,1,0,11,241,130,161,205,7,1,0,9,244,226,228,149,2,4,1,3,6,1,17,4,23,7,245,220,194,52,4,0,2,3,5,9,2,12,27,247,200,243,247,14,5,1,2,5,1,13,8,22,32,55,42,248,136,168,181,1,1,0,3,248,210,237,129,13,1,0,2,250,198,166,187,7,1,0,2,248,196,187,185,10,1,0,31,252,218,241,167,14,1,0,2,255,140,248,220,6,1,0,3,128,211,179,216,2,1,0,10,133,159,138,205,12,14,0,7,9,1,20,9,42,14,57,2,61,1,65,1,67,1,69,17,87,6,94,2,106,2,109,2,121,10,135,167,156,250,14,1,0,16,135,193,208,135,7,1,0,16,141,245,194,142,11,1,0,7,141,205,220,149,4,1,0,3,143,131,148,152,6,1,1,2,141,171,170,217,4,1,0,2,145,159,164,217,14,1,0,3,149,129,169,191,12,1,0,16,149,249,242,175,4,3,0,8,11,2,16,15,149,189,189,215,8,1,0,3,149,161,132,184,14,1,0,221,2,154,243,157,196,14,1,0,12,154,193,208,134,10,1,0,9,155,165,205,152,11,1,0,3,155,159,180,195,15,1,0,6,157,207,243,216,6,1,0,2,154,235,215,240,4,1,0,13,161,239,241,154,13,1,0,106,162,159,252,196,11,1,0,3,164,155,139,169,7,1,0,35,165,139,157,171,15,11,2,1,9,1,13,5,19,2,27,1,31,6,39,1,52,5,58,12,72,1,85,18,164,203,250,235,13,1,0,7,167,131,133,162,9,1,0,11,166,201,221,141,13,11,0,6,7,2,10,2,13,2,26,2,31,2,34,3,38,6,45,2,57,11,69,3,169,197,188,221,3,1,0,3,170,255,211,105,3,0,2,3,25,31,7,166,203,155,46,1,0,3,173,187,245,170,14,1,0,46,174,151,139,93,14,0,45,55,2,60,54,124,2,129,1,48,178,1,2,202,1,2,217,1,2,232,1,2,237,1,41,160,2,2,165,2,4,170,2,1,172,2,7,175,225,172,150,8,1,0,10,175,147,217,214,1,1,0,33,177,161,136,243,11,1,0,10,178,203,205,182,4,1,0,1,175,205,156,228,6,1,0,10,180,205,189,133,13,1,0,20,181,175,219,209,12,1,0,10,182,143,233,195,4,1,0,111,177,219,160,167,7,1,0,4,184,201,188,172,10,1,0,3,184,231,170,67,1,0,6,186,197,166,179,15,1,0,7,188,171,136,250,8,1,0,21,188,237,223,145,6,1,0,26,190,139,191,155,1,1,0,2,191,157,147,233,9,1,0,32,193,249,142,142,4,1,0,17,198,189,216,175,6,1,0,23,200,205,214,172,10,1,0,30,201,129,238,197,4,8,0,6,9,33,43,2,51,1,55,2,58,2,66,1,70,13,201,191,159,147,14,1,0,17,200,203,236,184,2,1,0,35,200,159,185,206,9,1,0,8,205,149,231,236,11,1,0,68,213,161,242,209,13,5,0,3,4,2,8,1,16,7,27,8,214,139,213,136,8,13,0,2,5,2,10,2,15,2,20,2,25,2,30,2,35,2,40,2,45,2,50,2,55,2,60,6,213,255,156,145,1,1,0,2,219,227,140,137,6,1,0,34,221,147,167,147,15,1,0,177,2,222,205,223,235,7,2,0,9,10,21,223,209,193,147,11,1,0,81,227,209,197,253,2,1,0,14,229,153,197,202,7,1,0,241,6,231,139,244,188,8,1,0,13,232,207,157,148,2,1,0,2,233,165,139,246,14,4,0,2,3,4,8,2,20,23,233,247,183,159,1,1,0,4,235,225,184,133,10,1,0,3,234,153,236,158,4,8,0,6,7,2,11,1,15,1,19,6,27,1,38,7,46,27,234,187,164,181,1,1,0,24,231,189,134,196,8,1,0,77,239,199,189,146,3,1,0,24,240,149,229,225,6,1,1,2,241,155,213,233,1,5,0,6,7,2,11,1,15,1,19,32,240,253,240,229,1,1,0,79,243,239,182,181,13,2,1,2,16,2,236,229,225,232,8,1,0,116,246,185,174,192,6,1,0,90,248,153,216,10,1,0,3,251,189,220,155,14,1,0,3,252,163,130,200,6,2,1,2,16,2,253,205,145,137,11,1,0,10,252,171,209,175,15,1,0,4,255,255,147,249,10,1,0,3],"version":0,"object_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/full_doc.json b/frontend/appflowy_web_app/cypress/fixtures/full_doc.json new file mode 100644 index 0000000000..c4eabdadc4 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/full_doc.json @@ -0,0 +1 @@ +{"data":{"state_vector":[74,131,182,180,202,12,53,132,236,218,251,9,14,131,159,159,151,1,72,131,128,202,229,9,1,135,182,134,178,8,51,136,172,186,168,4,182,6,136,199,176,231,9,40,133,181,204,218,3,50,140,167,201,161,14,10,141,151,160,163,4,24,142,211,188,164,13,15,141,178,210,127,3,145,224,235,133,7,3,146,209,153,247,13,186,1,146,216,250,133,2,180,1,146,175,139,236,2,199,1,150,152,188,203,6,20,151,234,142,238,11,27,150,216,171,142,3,188,8,153,236,182,220,1,4,151,254,242,152,9,145,1,155,213,159,176,1,10,161,234,157,145,5,7,164,202,219,213,10,122,165,131,171,211,15,20,168,215,223,235,2,56,171,236,222,251,5,252,4,172,254,181,239,1,15,174,203,157,214,7,6,176,238,158,139,14,175,2,177,239,218,225,4,3,178,187,245,161,14,11,180,189,170,253,8,12,181,150,190,222,14,95,181,156,253,158,6,5,183,182,135,14,227,2,184,146,243,216,14,7,185,164,169,62,90,183,213,134,255,8,28,190,183,139,210,2,110,192,246,139,213,2,35,192,187,174,206,8,223,5,194,228,144,71,76,195,254,251,180,11,58,197,205,192,233,12,9,198,223,206,159,1,145,2,198,234,131,228,11,50,199,130,209,189,2,141,8,204,195,206,156,1,153,9,206,214,243,86,178,1,207,210,187,205,12,8,208,203,223,226,9,81,207,231,154,196,9,3,217,168,198,159,4,7,218,255,204,32,21,219,200,174,197,9,25,220,225,223,240,3,60,223,215,172,155,15,5,224,159,166,178,15,30,226,167,254,250,5,13,227,211,144,195,8,12,228,242,134,215,15,12,229,154,194,35,178,1,226,235,133,189,11,8,236,158,128,159,2,4,237,140,187,206,2,21,236,253,128,205,3,9,239,239,208,251,10,17,240,179,157,219,7,4,241,147,239,232,6,4,238,153,239,204,9,49,243,138,171,183,10,252,1,245,181,155,135,2,23,247,212,219,208,10,46],"doc_state":[74,9,228,242,134,215,15,0,39,0,204,195,206,156,1,4,6,109,86,80,71,80,99,2,4,0,228,242,134,215,15,0,4,104,106,107,100,161,172,254,181,239,1,14,1,132,228,242,134,215,15,4,1,56,161,228,242,134,215,15,5,1,132,228,242,134,215,15,6,1,56,161,228,242,134,215,15,7,1,132,228,242,134,215,15,8,1,56,161,228,242,134,215,15,9,1,18,165,131,171,211,15,0,129,155,213,159,176,1,6,2,161,155,213,159,176,1,7,1,161,155,213,159,176,1,8,1,161,155,213,159,176,1,9,1,161,165,131,171,211,15,2,1,161,165,131,171,211,15,3,1,161,165,131,171,211,15,4,1,161,165,131,171,211,15,5,1,161,165,131,171,211,15,6,1,161,165,131,171,211,15,7,1,129,165,131,171,211,15,1,2,161,165,131,171,211,15,8,1,161,165,131,171,211,15,9,1,161,165,131,171,211,15,10,1,129,165,131,171,211,15,12,1,161,165,131,171,211,15,13,1,161,165,131,171,211,15,14,1,161,165,131,171,211,15,15,1,28,224,159,166,178,15,0,129,165,131,171,211,15,16,1,161,197,205,192,233,12,6,1,161,197,205,192,233,12,7,1,161,197,205,192,233,12,8,1,161,224,159,166,178,15,1,1,161,224,159,166,178,15,2,1,161,224,159,166,178,15,3,1,129,224,159,166,178,15,0,1,161,224,159,166,178,15,4,1,161,224,159,166,178,15,5,1,161,224,159,166,178,15,6,1,129,224,159,166,178,15,7,2,161,224,159,166,178,15,8,1,161,224,159,166,178,15,9,1,161,224,159,166,178,15,10,1,161,224,159,166,178,15,13,1,161,224,159,166,178,15,14,1,161,224,159,166,178,15,15,1,161,224,159,166,178,15,16,1,161,224,159,166,178,15,17,1,161,224,159,166,178,15,18,1,161,224,159,166,178,15,19,1,161,224,159,166,178,15,20,1,161,224,159,166,178,15,21,1,129,224,159,166,178,15,12,2,161,224,159,166,178,15,22,1,161,224,159,166,178,15,23,1,161,224,159,166,178,15,24,1,1,223,215,172,155,15,0,161,185,164,169,62,89,5,71,181,150,190,222,14,0,39,0,204,195,206,156,1,4,6,68,81,108,56,102,54,2,1,0,181,150,190,222,14,0,2,0,6,39,0,204,195,206,156,1,4,6,110,114,88,86,119,98,2,33,0,204,195,206,156,1,1,6,114,119,110,108,70,75,1,0,7,33,0,204,195,206,156,1,3,6,54,105,119,67,105,57,1,193,199,130,209,189,2,191,5,199,130,209,189,2,176,6,1,39,0,204,195,206,156,1,4,6,76,95,120,101,104,45,2,33,0,204,195,206,156,1,1,6,118,52,75,115,74,51,1,0,7,33,0,204,195,206,156,1,3,6,77,54,85,88,53,66,1,193,199,130,209,189,2,191,5,181,150,190,222,14,19,1,39,0,204,195,206,156,1,4,6,105,82,99,102,107,49,2,33,0,204,195,206,156,1,1,6,70,101,106,82,116,48,1,0,7,33,0,204,195,206,156,1,3,6,108,75,113,56,70,69,1,129,199,130,209,189,2,156,6,1,39,0,204,195,206,156,1,4,6,69,114,74,53,80,51,2,39,0,204,195,206,156,1,1,6,115,115,117,107,51,70,1,40,0,181,150,190,222,14,43,2,105,100,1,119,6,115,115,117,107,51,70,40,0,181,150,190,222,14,43,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,181,150,190,222,14,43,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,181,150,190,222,14,43,8,99,104,105,108,100,114,101,110,1,119,6,118,108,89,79,54,57,40,0,181,150,190,222,14,43,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,181,150,190,222,14,43,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,181,150,190,222,14,43,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,118,108,89,79,54,57,0,136,199,130,209,189,2,176,6,1,119,6,115,115,117,107,51,70,39,0,204,195,206,156,1,4,6,98,66,87,54,98,51,2,39,0,204,195,206,156,1,1,6,80,71,48,76,73,113,1,40,0,181,150,190,222,14,54,2,105,100,1,119,6,80,71,48,76,73,113,40,0,181,150,190,222,14,54,2,116,121,1,119,11,116,111,103,103,108,101,95,108,105,115,116,40,0,181,150,190,222,14,54,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,181,150,190,222,14,54,8,99,104,105,108,100,114,101,110,1,119,6,79,69,102,100,51,114,33,0,181,150,190,222,14,54,4,100,97,116,97,1,40,0,181,150,190,222,14,54,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,181,150,190,222,14,54,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,79,69,102,100,51,114,0,136,181,150,190,222,14,52,1,119,6,80,71,48,76,73,113,4,0,181,150,190,222,14,53,1,49,161,181,150,190,222,14,59,1,132,181,150,190,222,14,64,1,49,161,181,150,190,222,14,65,1,132,181,150,190,222,14,66,1,49,161,181,150,190,222,14,67,1,132,181,150,190,222,14,68,1,49,161,181,150,190,222,14,69,1,132,181,150,190,222,14,70,1,49,161,181,150,190,222,14,71,1,39,0,204,195,206,156,1,4,6,75,77,105,49,106,114,2,39,0,204,195,206,156,1,1,6,49,116,120,121,68,99,1,40,0,181,150,190,222,14,75,2,105,100,1,119,6,49,116,120,121,68,99,40,0,181,150,190,222,14,75,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,181,150,190,222,14,75,6,112,97,114,101,110,116,1,119,6,80,71,48,76,73,113,40,0,181,150,190,222,14,75,8,99,104,105,108,100,114,101,110,1,119,6,111,67,65,71,120,67,33,0,181,150,190,222,14,75,4,100,97,116,97,1,40,0,181,150,190,222,14,75,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,181,150,190,222,14,75,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,111,67,65,71,120,67,0,8,0,181,150,190,222,14,62,1,119,6,49,116,120,121,68,99,161,181,150,190,222,14,73,1,4,0,181,150,190,222,14,74,1,54,161,181,150,190,222,14,80,1,132,181,150,190,222,14,86,1,54,161,181,150,190,222,14,87,1,132,181,150,190,222,14,88,1,54,161,181,150,190,222,14,89,1,132,181,150,190,222,14,90,1,54,168,181,150,190,222,14,91,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,54,54,54,54,34,125,93,125,168,181,150,190,222,14,85,1,119,47,123,34,99,111,108,108,97,112,115,101,100,34,58,116,114,117,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,49,49,49,49,34,125,93,125,7,184,146,243,216,14,0,129,217,168,198,159,4,3,1,161,142,211,188,164,13,12,1,161,142,211,188,164,13,13,1,161,142,211,188,164,13,14,1,161,184,146,243,216,14,1,1,161,184,146,243,216,14,2,1,161,184,146,243,216,14,3,1,5,178,187,245,161,14,0,39,0,204,195,206,156,1,4,6,95,104,88,73,115,119,2,4,0,178,187,245,161,14,0,13,229,144,140,228,184,128,228,184,170,106,106,106,57,161,198,223,206,159,1,124,1,132,178,187,245,161,14,7,1,57,161,178,187,245,161,14,8,1,5,140,167,201,161,14,0,0,4,129,204,195,206,156,1,245,5,3,161,198,223,206,159,1,89,1,161,198,223,206,159,1,90,1,161,198,223,206,159,1,91,1,1,176,238,158,139,14,0,161,206,214,243,86,177,1,175,2,1,146,209,153,247,13,0,161,131,159,159,151,1,67,186,1,15,142,211,188,164,13,0,161,217,168,198,159,4,4,1,161,217,168,198,159,4,5,1,161,217,168,198,159,4,6,1,161,142,211,188,164,13,0,1,161,142,211,188,164,13,1,1,161,142,211,188,164,13,2,1,161,142,211,188,164,13,3,1,161,142,211,188,164,13,4,1,161,142,211,188,164,13,5,1,161,142,211,188,164,13,6,1,161,142,211,188,164,13,7,1,161,142,211,188,164,13,8,1,161,142,211,188,164,13,9,1,161,142,211,188,164,13,10,1,161,142,211,188,164,13,11,1,9,197,205,192,233,12,0,161,165,131,171,211,15,17,1,161,165,131,171,211,15,18,1,161,165,131,171,211,15,19,1,161,197,205,192,233,12,0,1,161,197,205,192,233,12,1,1,161,197,205,192,233,12,2,1,161,197,205,192,233,12,3,1,161,197,205,192,233,12,4,1,161,197,205,192,233,12,5,1,1,207,210,187,205,12,0,161,208,203,223,226,9,76,8,47,131,182,180,202,12,0,129,184,146,243,216,14,0,1,161,184,146,243,216,14,4,1,161,184,146,243,216,14,5,1,161,184,146,243,216,14,6,1,129,131,182,180,202,12,0,2,161,131,182,180,202,12,1,1,161,131,182,180,202,12,2,1,161,131,182,180,202,12,3,1,161,131,182,180,202,12,6,1,161,131,182,180,202,12,7,1,161,131,182,180,202,12,8,1,161,131,182,180,202,12,9,1,161,131,182,180,202,12,10,1,161,131,182,180,202,12,11,1,161,131,182,180,202,12,12,1,161,131,182,180,202,12,13,1,161,131,182,180,202,12,14,1,129,131,182,180,202,12,5,3,161,131,182,180,202,12,15,1,161,131,182,180,202,12,16,1,161,131,182,180,202,12,17,1,161,131,182,180,202,12,21,1,161,131,182,180,202,12,22,1,161,131,182,180,202,12,23,1,161,131,182,180,202,12,24,1,161,131,182,180,202,12,25,1,161,131,182,180,202,12,26,1,161,131,182,180,202,12,27,1,161,131,182,180,202,12,28,1,161,131,182,180,202,12,29,1,129,131,182,180,202,12,20,4,161,131,182,180,202,12,30,1,161,131,182,180,202,12,31,1,161,131,182,180,202,12,32,1,161,131,182,180,202,12,37,1,161,131,182,180,202,12,38,1,161,131,182,180,202,12,39,1,129,131,182,180,202,12,36,1,161,131,182,180,202,12,40,1,161,131,182,180,202,12,41,1,161,131,182,180,202,12,42,1,161,131,182,180,202,12,44,1,161,131,182,180,202,12,45,1,161,131,182,180,202,12,46,1,161,131,182,180,202,12,47,1,161,131,182,180,202,12,48,1,161,131,182,180,202,12,49,1,1,151,234,142,238,11,0,161,229,154,194,35,177,1,27,1,198,234,131,228,11,0,161,236,253,128,205,3,8,50,2,226,235,133,189,11,0,161,145,224,235,133,7,2,7,168,226,235,133,189,11,6,1,122,0,0,0,0,102,88,73,73,1,195,254,251,180,11,0,161,183,182,135,14,224,2,58,1,239,239,208,251,10,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,17,81,164,202,219,213,10,0,39,0,204,195,206,156,1,4,6,103,67,116,99,89,115,2,39,0,204,195,206,156,1,4,6,102,105,108,83,57,100,2,4,0,164,202,219,213,10,1,10,116,111,100,111,32,108,105,115,116,32,134,164,202,219,213,10,11,7,109,101,110,116,105,111,110,51,123,34,116,121,112,101,34,58,34,100,97,116,101,34,44,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,56,84,49,52,58,50,53,58,51,50,46,52,53,55,50,55,55,34,125,132,164,202,219,213,10,12,1,36,134,164,202,219,213,10,13,7,109,101,110,116,105,111,110,4,110,117,108,108,132,164,202,219,213,10,14,4,109,101,110,116,33,0,204,195,206,156,1,1,6,88,55,78,102,76,50,1,0,7,33,0,204,195,206,156,1,3,6,112,56,66,76,122,103,1,193,198,223,206,159,1,135,1,199,130,209,189,2,60,1,168,199,130,209,189,2,140,8,1,119,161,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,116,111,100,111,32,108,105,115,116,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,109,101,110,116,105,111,110,34,58,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,56,84,49,52,58,50,53,58,51,50,46,52,53,55,50,55,55,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,44,123,34,105,110,115,101,114,116,34,58,34,109,101,110,116,34,125,93,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,39,0,204,195,206,156,1,4,6,66,120,115,95,114,76,2,33,0,204,195,206,156,1,1,6,111,56,54,77,119,121,1,0,7,33,0,204,195,206,156,1,3,6,67,89,84,109,67,89,1,193,198,223,206,159,1,135,1,164,202,219,213,10,28,1,4,0,164,202,219,213,10,30,1,35,0,1,39,0,204,195,206,156,1,4,6,109,113,102,117,86,95,2,33,0,204,195,206,156,1,1,6,84,100,115,87,90,75,1,0,7,33,0,204,195,206,156,1,3,6,49,115,106,52,120,74,1,193,198,223,206,159,1,135,1,164,202,219,213,10,40,1,4,0,164,202,219,213,10,43,1,49,0,1,132,164,202,219,213,10,54,1,50,0,1,132,164,202,219,213,10,56,1,51,0,1,132,164,202,219,213,10,58,1,32,0,1,129,164,202,219,213,10,60,1,0,1,134,164,202,219,213,10,62,7,109,101,110,116,105,111,110,51,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,57,84,49,54,58,49,51,58,52,57,46,52,49,49,49,54,53,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,132,164,202,219,213,10,64,1,36,134,164,202,219,213,10,65,7,109,101,110,116,105,111,110,4,110,117,108,108,0,1,39,0,204,195,206,156,1,4,6,103,83,52,80,113,73,2,4,0,164,202,219,213,10,68,4,49,50,51,32,134,164,202,219,213,10,72,7,109,101,110,116,105,111,110,51,123,34,116,121,112,101,34,58,34,100,97,116,101,34,44,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,57,84,49,54,58,49,51,58,52,57,46,52,49,49,49,54,53,34,125,132,164,202,219,213,10,73,1,36,134,164,202,219,213,10,74,7,109,101,110,116,105,111,110,4,110,117,108,108,132,164,202,219,213,10,75,1,32,0,1,129,164,202,219,213,10,76,1,0,1,161,204,195,206,156,1,155,1,1,161,204,195,206,156,1,156,1,1,161,204,195,206,156,1,157,1,1,0,1,132,164,202,219,213,10,78,1,32,0,1,132,164,202,219,213,10,84,1,101,0,1,132,164,202,219,213,10,86,1,114,0,1,132,164,202,219,213,10,88,1,32,0,1,132,164,202,219,213,10,90,1,32,0,1,68,164,202,219,213,10,69,1,35,0,1,68,164,202,219,213,10,94,1,35,0,1,39,0,204,195,206,156,1,4,6,120,115,71,80,56,122,2,4,0,164,202,219,213,10,98,4,49,50,51,32,134,164,202,219,213,10,102,7,109,101,110,116,105,111,110,51,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,57,84,49,54,58,49,51,58,52,57,46,52,49,49,49,54,53,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,132,164,202,219,213,10,103,1,36,134,164,202,219,213,10,104,7,109,101,110,116,105,111,110,4,110,117,108,108,132,164,202,219,213,10,105,6,32,32,101,114,32,32,39,0,204,195,206,156,1,1,6,106,97,80,87,115,68,1,40,0,164,202,219,213,10,112,2,105,100,1,119,6,106,97,80,87,115,68,40,0,164,202,219,213,10,112,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,164,202,219,213,10,112,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,164,202,219,213,10,112,8,99,104,105,108,100,114,101,110,1,119,6,106,75,88,90,122,73,33,0,164,202,219,213,10,112,4,100,97,116,97,1,40,0,164,202,219,213,10,112,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,164,202,219,213,10,112,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,106,75,88,90,122,73,0,200,198,223,206,159,1,135,1,164,202,219,213,10,53,1,119,6,106,97,80,87,115,68,1,247,212,219,208,10,0,161,132,236,218,251,9,13,46,240,1,243,138,171,183,10,0,161,150,216,171,142,3,178,5,1,161,150,216,171,142,3,190,5,1,161,150,216,171,142,3,200,5,1,161,150,216,171,142,3,187,6,1,161,150,216,171,142,3,188,6,1,161,150,216,171,142,3,189,6,1,161,150,216,171,142,3,190,6,2,161,150,216,171,142,3,251,5,1,161,150,216,171,142,3,144,6,1,161,150,216,171,142,3,164,6,1,161,243,138,171,183,10,7,1,161,243,138,171,183,10,0,1,161,243,138,171,183,10,1,1,161,243,138,171,183,10,2,1,161,243,138,171,183,10,8,1,161,243,138,171,183,10,9,1,161,243,138,171,183,10,10,1,161,243,138,171,183,10,11,1,161,243,138,171,183,10,3,1,161,243,138,171,183,10,4,1,161,243,138,171,183,10,5,1,161,243,138,171,183,10,18,2,161,243,138,171,183,10,12,1,161,243,138,171,183,10,13,1,161,243,138,171,183,10,14,1,161,243,138,171,183,10,19,1,161,243,138,171,183,10,20,1,161,243,138,171,183,10,21,1,161,243,138,171,183,10,23,1,161,243,138,171,183,10,15,1,161,243,138,171,183,10,16,1,161,243,138,171,183,10,17,1,161,243,138,171,183,10,30,2,161,243,138,171,183,10,24,1,161,243,138,171,183,10,25,1,161,243,138,171,183,10,26,1,161,243,138,171,183,10,27,1,161,243,138,171,183,10,28,1,161,243,138,171,183,10,29,1,161,243,138,171,183,10,35,1,161,243,138,171,183,10,31,1,161,243,138,171,183,10,32,1,161,243,138,171,183,10,33,1,161,243,138,171,183,10,42,2,161,243,138,171,183,10,36,1,161,243,138,171,183,10,37,1,161,243,138,171,183,10,38,1,161,243,138,171,183,10,43,1,161,243,138,171,183,10,44,1,161,243,138,171,183,10,45,1,161,243,138,171,183,10,47,1,161,243,138,171,183,10,39,1,161,243,138,171,183,10,40,1,161,243,138,171,183,10,41,1,161,243,138,171,183,10,54,2,161,243,138,171,183,10,48,1,161,243,138,171,183,10,49,1,161,243,138,171,183,10,50,1,161,243,138,171,183,10,55,1,161,243,138,171,183,10,56,1,161,243,138,171,183,10,57,1,161,243,138,171,183,10,59,1,161,243,138,171,183,10,51,1,161,243,138,171,183,10,52,1,161,243,138,171,183,10,53,1,161,243,138,171,183,10,66,2,161,243,138,171,183,10,60,1,161,243,138,171,183,10,61,1,161,243,138,171,183,10,62,1,161,243,138,171,183,10,63,1,161,243,138,171,183,10,64,1,161,243,138,171,183,10,65,1,161,243,138,171,183,10,71,2,161,243,138,171,183,10,67,1,161,243,138,171,183,10,68,1,161,243,138,171,183,10,69,1,161,243,138,171,183,10,79,1,161,243,138,171,183,10,72,1,161,243,138,171,183,10,73,1,161,243,138,171,183,10,74,1,161,243,138,171,183,10,75,1,161,243,138,171,183,10,76,1,161,243,138,171,183,10,77,1,161,243,138,171,183,10,83,1,161,243,138,171,183,10,80,1,161,243,138,171,183,10,81,1,161,243,138,171,183,10,82,1,161,243,138,171,183,10,90,2,161,146,216,250,133,2,12,1,161,146,216,250,133,2,13,1,161,146,216,250,133,2,14,1,161,146,216,250,133,2,17,1,161,146,216,250,133,2,21,1,161,146,216,250,133,2,22,1,161,146,216,250,133,2,23,1,161,243,138,171,183,10,99,1,161,146,216,250,133,2,18,1,161,146,216,250,133,2,19,1,161,146,216,250,133,2,20,1,161,243,138,171,183,10,103,1,161,146,216,250,133,2,24,1,161,146,216,250,133,2,25,1,161,146,216,250,133,2,26,1,161,146,216,250,133,2,35,1,161,146,216,250,133,2,28,1,161,146,216,250,133,2,29,1,161,146,216,250,133,2,30,1,161,243,138,171,183,10,111,1,161,146,216,250,133,2,32,1,161,146,216,250,133,2,33,1,161,146,216,250,133,2,34,1,161,243,138,171,183,10,115,1,161,146,216,250,133,2,36,1,161,146,216,250,133,2,37,1,161,146,216,250,133,2,38,1,161,146,216,250,133,2,40,1,161,146,216,250,133,2,41,1,161,146,216,250,133,2,42,1,161,146,216,250,133,2,47,1,161,146,216,250,133,2,44,1,161,146,216,250,133,2,45,1,161,146,216,250,133,2,46,1,161,243,138,171,183,10,126,2,161,146,216,250,133,2,48,1,161,146,216,250,133,2,49,1,161,146,216,250,133,2,50,1,161,146,216,250,133,2,59,1,161,146,216,250,133,2,51,1,161,146,216,250,133,2,52,1,161,146,216,250,133,2,53,1,161,243,138,171,183,10,135,1,1,161,146,216,250,133,2,56,1,161,146,216,250,133,2,57,1,161,146,216,250,133,2,58,1,161,243,138,171,183,10,139,1,1,161,146,216,250,133,2,60,1,161,146,216,250,133,2,61,1,161,146,216,250,133,2,62,1,161,146,216,250,133,2,71,1,161,146,216,250,133,2,64,1,161,146,216,250,133,2,65,1,161,146,216,250,133,2,66,1,161,243,138,171,183,10,147,1,1,161,146,216,250,133,2,68,1,161,146,216,250,133,2,69,1,161,146,216,250,133,2,70,1,161,243,138,171,183,10,151,1,1,161,146,216,250,133,2,72,1,161,146,216,250,133,2,73,1,161,146,216,250,133,2,74,1,161,146,216,250,133,2,83,1,161,146,216,250,133,2,76,1,161,146,216,250,133,2,77,1,161,146,216,250,133,2,78,1,161,243,138,171,183,10,159,1,1,161,146,216,250,133,2,80,1,161,146,216,250,133,2,81,1,161,146,216,250,133,2,82,1,161,243,138,171,183,10,163,1,1,161,146,216,250,133,2,84,1,161,146,216,250,133,2,85,1,161,146,216,250,133,2,86,1,161,146,216,250,133,2,95,1,161,146,216,250,133,2,92,1,161,146,216,250,133,2,93,1,161,146,216,250,133,2,94,1,161,243,138,171,183,10,171,1,1,161,146,216,250,133,2,88,1,161,146,216,250,133,2,89,1,161,146,216,250,133,2,90,1,161,243,138,171,183,10,175,1,1,161,146,216,250,133,2,96,1,161,146,216,250,133,2,97,1,161,146,216,250,133,2,98,1,161,146,216,250,133,2,107,1,161,146,216,250,133,2,104,1,161,146,216,250,133,2,105,1,161,146,216,250,133,2,106,1,161,243,138,171,183,10,183,1,1,161,146,216,250,133,2,100,1,161,146,216,250,133,2,101,1,161,146,216,250,133,2,102,1,161,243,138,171,183,10,187,1,1,161,146,216,250,133,2,108,1,161,146,216,250,133,2,109,1,161,146,216,250,133,2,110,1,161,146,216,250,133,2,119,1,161,146,216,250,133,2,112,1,161,146,216,250,133,2,113,1,161,146,216,250,133,2,114,1,161,243,138,171,183,10,195,1,1,161,146,216,250,133,2,116,1,161,146,216,250,133,2,117,1,161,146,216,250,133,2,118,1,161,243,138,171,183,10,199,1,1,161,146,216,250,133,2,120,1,161,146,216,250,133,2,121,1,161,146,216,250,133,2,122,1,161,146,216,250,133,2,131,1,2,161,146,216,250,133,2,124,1,161,146,216,250,133,2,125,1,161,146,216,250,133,2,126,1,161,146,216,250,133,2,128,1,1,161,146,216,250,133,2,129,1,1,161,146,216,250,133,2,130,1,1,161,243,138,171,183,10,208,1,1,161,146,216,250,133,2,132,1,1,161,146,216,250,133,2,133,1,1,161,146,216,250,133,2,134,1,1,161,146,216,250,133,2,143,1,1,161,146,216,250,133,2,136,1,1,161,146,216,250,133,2,137,1,1,161,146,216,250,133,2,138,1,1,161,243,138,171,183,10,219,1,1,161,146,216,250,133,2,140,1,1,161,146,216,250,133,2,141,1,1,161,146,216,250,133,2,142,1,1,161,243,138,171,183,10,223,1,1,161,146,216,250,133,2,144,1,1,161,146,216,250,133,2,145,1,1,161,146,216,250,133,2,146,1,1,161,146,216,250,133,2,155,1,1,161,146,216,250,133,2,148,1,1,161,146,216,250,133,2,149,1,1,161,146,216,250,133,2,150,1,1,161,243,138,171,183,10,231,1,1,161,146,216,250,133,2,152,1,1,161,146,216,250,133,2,153,1,1,161,146,216,250,133,2,154,1,1,161,243,138,171,183,10,235,1,1,161,146,216,250,133,2,156,1,1,161,146,216,250,133,2,157,1,1,161,146,216,250,133,2,158,1,1,161,146,216,250,133,2,167,1,3,161,146,216,250,133,2,160,1,1,161,146,216,250,133,2,161,1,1,161,146,216,250,133,2,162,1,1,161,146,216,250,133,2,164,1,1,161,146,216,250,133,2,165,1,1,161,146,216,250,133,2,166,1,1,1,132,236,218,251,9,0,161,218,255,204,32,20,14,34,136,199,176,231,9,0,39,0,204,195,206,156,1,1,6,74,52,82,97,73,114,1,40,0,136,199,176,231,9,0,2,105,100,1,119,6,74,52,82,97,73,114,40,0,136,199,176,231,9,0,2,116,121,1,119,4,103,114,105,100,40,0,136,199,176,231,9,0,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,136,199,176,231,9,0,8,99,104,105,108,100,114,101,110,1,119,6,86,95,76,83,51,101,40,0,136,199,176,231,9,0,4,100,97,116,97,1,119,101,123,34,118,105,101,119,95,105,100,34,58,34,49,51,53,54,49,53,102,97,45,54,54,102,55,45,52,52,53,49,45,57,98,53,52,45,100,55,101,57,57,52,52,53,102,99,97,52,34,44,34,112,97,114,101,110,116,95,105,100,34,58,34,55,100,50,49,52,56,102,99,45,99,97,99,101,45,52,52,53,50,45,57,99,53,99,45,57,54,101,53,50,101,54,98,102,56,98,53,34,125,40,0,136,199,176,231,9,0,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,136,199,176,231,9,0,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,86,95,76,83,51,101,0,200,204,195,206,156,1,252,1,204,195,206,156,1,253,1,1,119,6,74,52,82,97,73,114,39,0,204,195,206,156,1,1,6,115,74,113,109,112,57,1,40,0,136,199,176,231,9,10,2,105,100,1,119,6,115,74,113,109,112,57,40,0,136,199,176,231,9,10,2,116,121,1,119,5,98,111,97,114,100,40,0,136,199,176,231,9,10,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,136,199,176,231,9,10,8,99,104,105,108,100,114,101,110,1,119,6,87,71,71,122,72,118,40,0,136,199,176,231,9,10,4,100,97,116,97,1,119,101,123,34,112,97,114,101,110,116,95,105,100,34,58,34,97,53,53,54,54,101,52,57,45,102,49,53,54,45,52,49,54,56,45,57,98,50,100,45,49,55,57,50,54,99,53,100,97,51,50,57,34,44,34,118,105,101,119,95,105,100,34,58,34,98,52,101,55,55,50,48,51,45,53,99,56,98,45,52,56,100,102,45,98,98,99,53,45,50,101,49,49,52,51,101,98,48,101,54,49,34,125,40,0,136,199,176,231,9,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,136,199,176,231,9,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,87,71,71,122,72,118,0,200,136,199,176,231,9,9,204,195,206,156,1,253,1,1,119,6,115,74,113,109,112,57,33,0,204,195,206,156,1,1,6,98,118,111,52,85,121,1,0,7,33,0,204,195,206,156,1,3,6,81,122,68,56,119,121,1,193,136,199,176,231,9,19,204,195,206,156,1,253,1,1,39,0,204,195,206,156,1,1,6,71,57,106,76,66,79,1,40,0,136,199,176,231,9,30,2,105,100,1,119,6,71,57,106,76,66,79,40,0,136,199,176,231,9,30,2,116,121,1,119,8,99,97,108,101,110,100,97,114,40,0,136,199,176,231,9,30,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,136,199,176,231,9,30,8,99,104,105,108,100,114,101,110,1,119,6,122,51,54,102,102,100,40,0,136,199,176,231,9,30,4,100,97,116,97,1,119,101,123,34,118,105,101,119,95,105,100,34,58,34,50,98,102,53,48,99,48,51,45,102,52,49,102,45,52,51,54,51,45,98,53,98,49,45,49,48,49,50,49,54,97,54,99,53,99,99,34,44,34,112,97,114,101,110,116,95,105,100,34,58,34,101,101,51,97,101,56,99,101,45,57,53,57,97,45,52,100,102,51,45,56,55,51,52,45,52,48,98,53,51,53,102,102,56,56,101,51,34,125,40,0,136,199,176,231,9,30,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,136,199,176,231,9,30,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,122,51,54,102,102,100,0,200,136,199,176,231,9,29,204,195,206,156,1,253,1,1,119,6,71,57,106,76,66,79,1,131,128,202,229,9,0,161,243,138,171,183,10,245,1,1,1,208,203,223,226,9,0,161,146,209,153,247,13,185,1,81,31,238,153,239,204,9,0,161,183,213,134,255,8,25,1,161,183,213,134,255,8,26,1,161,183,213,134,255,8,27,1,132,183,213,134,255,8,24,1,100,161,238,153,239,204,9,0,1,161,238,153,239,204,9,1,1,161,238,153,239,204,9,2,1,132,238,153,239,204,9,3,1,55,161,238,153,239,204,9,4,1,161,238,153,239,204,9,5,1,161,238,153,239,204,9,6,1,132,238,153,239,204,9,7,1,55,168,238,153,239,204,9,8,1,119,133,1,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,109,101,110,116,105,111,110,34,58,123,34,112,97,103,101,95,105,100,34,58,34,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,34,44,34,116,121,112,101,34,58,34,112,97,103,101,34,125,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,44,123,34,105,110,115,101,114,116,34,58,34,100,55,55,34,125,93,125,168,238,153,239,204,9,9,1,119,10,107,106,48,68,49,121,121,88,78,119,168,238,153,239,204,9,10,1,119,4,116,101,120,116,39,0,204,195,206,156,1,4,6,111,97,103,82,55,77,2,6,0,238,153,239,204,9,15,4,104,114,101,102,13,34,97,112,112,102,108,111,119,121,46,105,111,34,132,238,153,239,204,9,16,11,97,112,112,102,108,111,119,121,46,105,111,134,238,153,239,204,9,27,4,104,114,101,102,4,110,117,108,108,132,238,153,239,204,9,28,1,32,161,151,254,242,152,9,90,1,132,238,153,239,204,9,29,1,49,161,238,153,239,204,9,30,1,39,0,204,195,206,156,1,4,6,53,101,83,117,83,45,2,6,0,238,153,239,204,9,33,4,104,114,101,102,13,34,49,57,50,46,49,54,56,46,49,46,50,34,132,238,153,239,204,9,34,9,99,111,110,116,101,110,116,32,49,134,238,153,239,204,9,43,4,104,114,101,102,4,110,117,108,108,132,238,153,239,204,9,44,1,32,161,151,254,242,152,9,134,1,1,132,238,153,239,204,9,45,1,50,161,238,153,239,204,9,46,1,1,219,200,174,197,9,0,161,161,234,157,145,5,6,25,3,207,231,154,196,9,0,161,204,195,206,156,1,209,1,1,161,204,195,206,156,1,210,1,1,161,204,195,206,156,1,211,1,1,118,151,254,242,152,9,0,39,0,204,195,206,156,1,4,6,65,119,80,77,53,56,2,6,0,151,254,242,152,9,0,7,109,101,110,116,105,111,110,64,123,34,116,121,112,101,34,58,34,112,97,103,101,34,44,34,112,97,103,101,95,105,100,34,58,34,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,34,125,132,151,254,242,152,9,1,1,36,134,151,254,242,152,9,2,7,109,101,110,116,105,111,110,4,110,117,108,108,132,151,254,242,152,9,3,1,104,161,220,225,223,240,3,59,1,129,151,254,242,152,9,4,2,161,151,254,242,152,9,5,1,129,151,254,242,152,9,7,2,161,151,254,242,152,9,8,1,129,151,254,242,152,9,10,1,132,151,254,242,152,9,12,1,104,161,151,254,242,152,9,11,1,196,151,254,242,152,9,4,151,254,242,152,9,6,2,104,104,161,151,254,242,152,9,14,1,132,151,254,242,152,9,13,1,32,161,151,254,242,152,9,17,1,129,151,254,242,152,9,18,1,161,151,254,242,152,9,19,1,134,151,254,242,152,9,20,7,109,101,110,116,105,111,110,123,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,50,53,84,49,55,58,49,50,58,52,51,46,52,51,57,57,48,57,34,44,34,114,101,109,105,110,100,101,114,95,105,100,34,58,34,118,108,95,45,105,57,52,99,82,103,69,85,115,112,84,111,81,95,115,68,86,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,44,34,114,101,109,105,110,100,101,114,95,111,112,116,105,111,110,34,58,34,97,116,84,105,109,101,79,102,69,118,101,110,116,34,125,132,151,254,242,152,9,22,1,36,134,151,254,242,152,9,23,7,109,101,110,116,105,111,110,4,110,117,108,108,168,151,254,242,152,9,21,1,119,171,2,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,109,101,110,116,105,111,110,34,58,123,34,112,97,103,101,95,105,100,34,58,34,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,34,44,34,116,121,112,101,34,58,34,112,97,103,101,34,125,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,44,123,34,105,110,115,101,114,116,34,58,34,104,104,104,104,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,109,101,110,116,105,111,110,34,58,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,50,53,84,49,55,58,49,50,58,52,51,46,52,51,57,57,48,57,34,44,34,114,101,109,105,110,100,101,114,95,105,100,34,58,34,118,108,95,45,105,57,52,99,82,103,69,85,115,112,84,111,81,95,115,68,86,34,44,34,114,101,109,105,110,100,101,114,95,111,112,116,105,111,110,34,58,34,97,116,84,105,109,101,79,102,69,118,101,110,116,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,93,125,39,0,204,195,206,156,1,4,6,107,74,118,98,69,107,2,39,0,204,195,206,156,1,1,6,112,71,75,102,71,113,1,40,0,151,254,242,152,9,27,2,105,100,1,119,6,112,71,75,102,71,113,40,0,151,254,242,152,9,27,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,151,254,242,152,9,27,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,151,254,242,152,9,27,8,99,104,105,108,100,114,101,110,1,119,6,54,97,84,68,85,107,33,0,151,254,242,152,9,27,4,100,97,116,97,1,40,0,151,254,242,152,9,27,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,151,254,242,152,9,27,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,54,97,84,68,85,107,0,200,220,225,223,240,3,45,204,195,206,156,1,251,1,1,119,6,112,71,75,102,71,113,1,0,151,254,242,152,9,26,1,161,151,254,242,152,9,32,1,129,151,254,242,152,9,37,1,161,151,254,242,152,9,38,1,129,151,254,242,152,9,39,1,161,151,254,242,152,9,40,1,129,151,254,242,152,9,41,1,161,151,254,242,152,9,42,1,129,151,254,242,152,9,43,1,161,151,254,242,152,9,44,1,65,151,254,242,152,9,37,6,198,151,254,242,152,9,52,151,254,242,152,9,37,4,104,114,101,102,4,110,117,108,108,161,151,254,242,152,9,46,1,65,151,254,242,152,9,47,1,161,151,254,242,152,9,54,1,193,151,254,242,152,9,55,151,254,242,152,9,47,1,161,151,254,242,152,9,56,1,193,151,254,242,152,9,57,151,254,242,152,9,47,1,161,151,254,242,152,9,58,1,193,151,254,242,152,9,59,151,254,242,152,9,47,1,161,151,254,242,152,9,60,1,193,151,254,242,152,9,61,151,254,242,152,9,47,1,161,151,254,242,152,9,62,1,193,151,254,242,152,9,63,151,254,242,152,9,47,1,161,151,254,242,152,9,64,1,193,151,254,242,152,9,65,151,254,242,152,9,47,1,161,151,254,242,152,9,66,1,193,151,254,242,152,9,67,151,254,242,152,9,47,1,161,151,254,242,152,9,68,1,193,151,254,242,152,9,69,151,254,242,152,9,47,1,161,151,254,242,152,9,70,1,193,151,254,242,152,9,71,151,254,242,152,9,47,1,161,151,254,242,152,9,72,1,193,151,254,242,152,9,73,151,254,242,152,9,47,1,161,151,254,242,152,9,74,1,70,151,254,242,152,9,55,4,104,114,101,102,13,34,97,112,112,102,108,111,119,121,46,105,111,34,196,151,254,242,152,9,77,151,254,242,152,9,55,11,97,112,112,102,108,111,119,121,46,105,111,193,151,254,242,152,9,88,151,254,242,152,9,55,1,161,151,254,242,152,9,76,1,39,0,204,195,206,156,1,4,6,88,82,74,89,90,53,2,39,0,204,195,206,156,1,1,6,72,77,49,70,106,86,1,40,0,151,254,242,152,9,92,2,105,100,1,119,6,72,77,49,70,106,86,40,0,151,254,242,152,9,92,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,151,254,242,152,9,92,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,151,254,242,152,9,92,8,99,104,105,108,100,114,101,110,1,119,6,95,45,107,102,121,108,33,0,151,254,242,152,9,92,4,100,97,116,97,1,40,0,151,254,242,152,9,92,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,151,254,242,152,9,92,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,95,45,107,102,121,108,0,200,151,254,242,152,9,36,204,195,206,156,1,251,1,1,119,6,72,77,49,70,106,86,1,0,151,254,242,152,9,91,1,161,151,254,242,152,9,97,2,129,151,254,242,152,9,102,1,161,151,254,242,152,9,104,1,129,151,254,242,152,9,105,1,161,151,254,242,152,9,106,1,129,151,254,242,152,9,107,1,161,151,254,242,152,9,108,1,129,151,254,242,152,9,109,1,161,151,254,242,152,9,110,1,129,151,254,242,152,9,111,1,161,151,254,242,152,9,112,1,129,151,254,242,152,9,113,1,161,151,254,242,152,9,114,1,129,151,254,242,152,9,115,1,161,151,254,242,152,9,116,1,129,151,254,242,152,9,117,1,161,151,254,242,152,9,118,1,129,151,254,242,152,9,119,1,161,151,254,242,152,9,120,1,198,151,254,242,152,9,102,151,254,242,152,9,105,4,104,114,101,102,13,34,49,57,50,46,49,54,56,46,49,46,50,34,196,151,254,242,152,9,123,151,254,242,152,9,105,9,99,111,110,116,101,110,116,32,49,198,151,254,242,152,9,132,1,151,254,242,152,9,105,4,104,114,101,102,4,110,117,108,108,161,151,254,242,152,9,122,1,129,204,195,206,156,1,131,4,1,161,204,195,206,156,1,56,1,161,204,195,206,156,1,57,1,161,204,195,206,156,1,58,1,161,151,254,242,152,9,136,1,1,161,151,254,242,152,9,137,1,1,161,151,254,242,152,9,138,1,1,168,151,254,242,152,9,139,1,1,119,128,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,63,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,114,105,103,104,116,32,102,111,114,32,104,101,108,112,32,97,110,100,32,115,117,112,112,111,114,116,34,125,93,125,168,151,254,242,152,9,140,1,1,119,10,119,86,82,81,117,71,111,121,116,48,168,151,254,242,152,9,141,1,1,119,4,116,101,120,116,28,183,213,134,255,8,0,129,220,225,223,240,3,6,1,161,220,225,223,240,3,7,1,161,220,225,223,240,3,8,1,161,220,225,223,240,3,9,1,129,183,213,134,255,8,0,1,161,183,213,134,255,8,1,1,161,183,213,134,255,8,2,1,161,183,213,134,255,8,3,1,129,183,213,134,255,8,4,1,161,183,213,134,255,8,5,1,161,183,213,134,255,8,6,1,161,183,213,134,255,8,7,1,129,183,213,134,255,8,8,1,161,183,213,134,255,8,9,1,161,183,213,134,255,8,10,1,161,183,213,134,255,8,11,1,129,183,213,134,255,8,12,1,161,183,213,134,255,8,13,1,161,183,213,134,255,8,14,1,161,183,213,134,255,8,15,1,129,183,213,134,255,8,16,1,161,183,213,134,255,8,17,1,161,183,213,134,255,8,18,1,161,183,213,134,255,8,19,1,129,183,213,134,255,8,20,1,161,183,213,134,255,8,21,1,161,183,213,134,255,8,22,1,161,183,213,134,255,8,23,1,12,180,189,170,253,8,0,168,146,175,139,236,2,0,1,119,68,123,34,119,105,100,116,104,34,58,49,53,48,46,56,53,53,52,54,56,55,53,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,125,168,146,175,139,236,2,1,1,119,68,123,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,119,105,100,116,104,34,58,49,48,53,46,48,53,48,55,56,49,50,53,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,52,54,46,48,125,168,146,175,139,236,2,2,1,119,60,123,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,48,44,34,119,105,100,116,104,34,58,56,48,46,48,125,168,146,175,139,236,2,3,1,119,68,123,34,119,105,100,116,104,34,58,49,53,48,46,56,53,53,52,54,56,55,53,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,125,168,146,175,139,236,2,4,1,119,68,123,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,44,34,119,105,100,116,104,34,58,49,48,53,46,48,53,48,55,56,49,50,53,44,34,104,101,105,103,104,116,34,58,52,54,46,48,125,168,146,175,139,236,2,5,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,49,125,161,146,175,139,236,2,11,1,168,146,175,139,236,2,7,1,119,68,123,34,99,111,108,80,111,115,105,116,105,111,110,34,58,48,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,49,53,48,46,56,53,53,52,54,56,55,53,125,168,146,175,139,236,2,8,1,119,68,123,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,119,105,100,116,104,34,58,49,48,53,46,48,53,48,55,56,49,50,53,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,49,125,168,146,175,139,236,2,9,1,119,60,123,34,119,105,100,116,104,34,58,56,48,46,48,44,34,114,111,119,80,111,115,105,116,105,111,110,34,58,50,44,34,104,101,105,103,104,116,34,58,52,54,46,48,44,34,99,111,108,80,111,115,105,116,105,111,110,34,58,50,125,161,180,189,170,253,8,6,1,168,180,189,170,253,8,10,1,119,114,123,34,99,111,108,77,105,110,105,109,117,109,87,105,100,116,104,34,58,52,48,46,48,44,34,114,111,119,68,101,102,97,117,108,116,72,101,105,103,104,116,34,58,52,48,46,48,44,34,99,111,108,115,76,101,110,34,58,51,44,34,114,111,119,115,76,101,110,34,58,51,44,34,99,111,108,68,101,102,97,117,108,116,87,105,100,116,104,34,58,56,48,46,48,44,34,99,111,108,115,72,101,105,103,104,116,34,58,49,52,54,46,48,125,21,192,187,174,206,8,0,39,0,204,195,206,156,1,4,6,72,101,110,107,82,107,2,161,150,216,171,142,3,187,8,1,1,0,192,187,174,206,8,0,3,129,192,187,174,206,8,4,15,129,192,187,174,206,8,19,45,129,192,187,174,206,8,64,10,134,192,187,174,206,8,74,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,129,192,187,174,206,8,75,110,161,192,187,174,206,8,1,1,65,192,187,174,206,8,2,5,193,192,187,174,206,8,4,192,187,174,206,8,5,14,193,192,187,174,206,8,19,192,187,174,206,8,20,44,193,192,187,174,206,8,64,192,187,174,206,8,65,9,193,192,187,174,206,8,74,192,187,174,206,8,75,110,161,192,187,174,206,8,186,1,2,193,192,187,174,206,8,240,2,192,187,174,206,8,75,180,1,161,192,187,174,206,8,242,2,1,70,192,187,174,206,8,187,1,11,102,111,110,116,95,102,97,109,105,108,121,15,34,65,68,76,97,77,32,68,105,115,112,108,97,121,34,196,192,187,174,206,8,168,4,192,187,174,206,8,187,1,180,1,108,111,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,76,101,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,198,192,187,174,206,8,220,5,192,187,174,206,8,187,1,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,161,192,187,174,206,8,167,4,1,11,227,211,144,195,8,0,161,243,138,171,183,10,240,1,1,161,243,138,171,183,10,241,1,1,161,243,138,171,183,10,242,1,1,161,194,228,144,71,4,1,161,194,228,144,71,5,1,161,194,228,144,71,6,1,161,220,225,223,240,3,34,1,161,220,225,223,240,3,30,1,161,220,225,223,240,3,31,1,161,220,225,223,240,3,32,1,161,227,211,144,195,8,6,2,9,135,182,134,178,8,0,168,141,151,160,163,4,21,1,119,147,2,123,34,99,111,118,101,114,95,115,101,108,101,99,116,105,111,110,95,116,121,112,101,34,58,34,67,111,118,101,114,84,121,112,101,46,102,105,108,101,34,44,34,99,111,118,101,114,95,115,101,108,101,99,116,105,111,110,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,52,53,48,56,56,54,50,55,56,56,45,52,52,101,52,53,99,52,51,49,53,100,48,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,89,51,78,122,103,121,77,84,108,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,44,34,105,109,97,103,101,95,116,121,112,101,34,58,34,49,34,44,34,100,101,108,116,97,34,58,91,93,125,168,141,151,160,163,4,22,1,119,10,112,70,113,76,55,45,79,83,121,86,168,141,151,160,163,4,23,1,119,4,116,101,120,116,70,204,195,206,156,1,209,5,11,102,111,110,116,95,102,97,109,105,108,121,15,34,65,68,76,97,32,77,68,105,115,112,108,97,121,34,196,135,182,134,178,8,3,204,195,206,156,1,209,5,55,67,108,105,99,107,32,97,110,121,119,104,101,114,101,32,97,110,100,32,106,117,115,116,32,115,116,97,114,116,32,116,121,112,105,110,103,229,147,136,229,147,136,229,147,136,46,229,176,177,229,135,160,229,174,182,198,135,182,134,178,8,46,204,195,206,156,1,209,5,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,168,140,167,201,161,14,7,1,119,141,1,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,102,97,109,105,108,121,34,58,34,65,68,76,97,32,77,68,105,115,112,108,97,121,34,125,44,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,97,110,121,119,104,101,114,101,32,97,110,100,32,106,117,115,116,32,115,116,97,114,116,32,116,121,112,105,110,103,229,147,136,229,147,136,229,147,136,46,229,176,177,229,135,160,229,174,182,34,125,93,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,168,140,167,201,161,14,8,1,119,10,119,79,108,117,99,85,55,51,73,76,168,140,167,201,161,14,9,1,119,4,116,101,120,116,1,240,179,157,219,7,0,161,174,203,157,214,7,5,4,1,174,203,157,214,7,0,161,153,236,182,220,1,3,6,1,145,224,235,133,7,0,161,223,215,172,155,15,4,3,1,241,147,239,232,6,0,161,245,181,155,135,2,22,4,1,150,152,188,203,6,0,161,192,246,139,213,2,34,20,2,181,156,253,158,6,0,161,198,223,206,159,1,175,1,4,168,181,156,253,158,6,3,1,119,231,1,123,34,117,114,108,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,50,51,48,51,55,48,48,56,51,50,45,53,55,100,50,98,50,98,57,49,54,98,56,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,77,121,78,84,107,122,78,84,100,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,44,34,119,105,100,116,104,34,58,52,50,56,46,49,57,53,51,49,50,53,44,34,97,108,105,103,110,34,58,34,114,105,103,104,116,34,125,220,2,171,236,222,251,5,0,198,204,195,206,156,1,205,4,204,195,206,156,1,206,4,4,98,111,108,100,4,116,114,117,101,196,171,236,222,251,5,0,204,195,206,156,1,206,4,4,112,97,103,101,198,171,236,222,251,5,4,204,195,206,156,1,206,4,4,98,111,108,100,4,110,117,108,108,168,204,195,206,156,1,119,1,119,222,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,43,32,78,101,119,32,80,97,103,101,32,34,125,44,123,34,105,110,115,101,114,116,34,58,34,98,117,116,116,111,110,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,111,102,32,121,111,117,114,32,115,105,100,101,98,97,114,32,116,111,32,97,100,100,32,97,32,110,101,119,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,111,108,100,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,112,97,103,101,34,125,44,123,34,105,110,115,101,114,116,34,58,34,46,34,125,93,44,34,99,104,101,99,107,101,100,34,58,116,114,117,101,125,168,204,195,206,156,1,120,1,119,10,122,77,121,109,67,97,118,83,107,102,168,204,195,206,156,1,121,1,119,4,116,101,120,116,193,204,195,206,156,1,129,6,204,195,206,156,1,130,6,5,198,171,236,222,251,5,13,204,195,206,156,1,130,6,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,204,195,206,156,1,11,1,161,204,195,206,156,1,12,1,161,204,195,206,156,1,13,1,161,171,236,222,251,5,15,1,161,171,236,222,251,5,16,1,161,171,236,222,251,5,17,1,193,204,195,206,156,1,129,6,171,236,222,251,5,9,5,198,171,236,222,251,5,25,171,236,222,251,5,9,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,18,1,161,171,236,222,251,5,19,1,161,171,236,222,251,5,20,1,193,204,195,206,156,1,129,6,171,236,222,251,5,21,5,198,171,236,222,251,5,34,171,236,222,251,5,21,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,27,1,161,171,236,222,251,5,28,1,161,171,236,222,251,5,29,1,198,204,195,206,156,1,129,6,171,236,222,251,5,30,10,102,111,110,116,95,99,111,108,111,114,12,34,48,120,102,102,100,98,51,54,51,54,34,193,171,236,222,251,5,39,171,236,222,251,5,30,4,198,171,236,222,251,5,43,171,236,222,251,5,30,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,36,1,161,171,236,222,251,5,37,1,161,171,236,222,251,5,38,1,193,171,236,222,251,5,39,171,236,222,251,5,40,5,198,171,236,222,251,5,52,171,236,222,251,5,40,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,45,1,161,171,236,222,251,5,46,1,161,171,236,222,251,5,47,1,193,171,236,222,251,5,39,171,236,222,251,5,48,5,198,171,236,222,251,5,61,171,236,222,251,5,48,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,54,1,161,171,236,222,251,5,55,1,161,171,236,222,251,5,56,1,198,171,236,222,251,5,39,171,236,222,251,5,57,8,98,103,95,99,111,108,111,114,12,34,48,120,102,102,102,102,100,97,101,54,34,196,171,236,222,251,5,66,171,236,222,251,5,57,4,110,101,120,116,198,171,236,222,251,5,70,171,236,222,251,5,57,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,63,1,161,171,236,222,251,5,64,1,161,171,236,222,251,5,65,1,198,204,195,206,156,1,180,6,204,195,206,156,1,181,6,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,193,171,236,222,251,5,75,204,195,206,156,1,181,6,3,198,171,236,222,251,5,78,204,195,206,156,1,181,6,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,161,171,236,222,251,5,72,1,161,171,236,222,251,5,73,1,161,171,236,222,251,5,74,1,198,171,236,222,251,5,75,171,236,222,251,5,76,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,193,171,236,222,251,5,83,171,236,222,251,5,76,3,198,171,236,222,251,5,86,171,236,222,251,5,76,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,161,171,236,222,251,5,80,1,161,171,236,222,251,5,81,1,161,171,236,222,251,5,82,1,198,171,236,222,251,5,83,171,236,222,251,5,84,6,105,116,97,108,105,99,4,116,114,117,101,193,171,236,222,251,5,91,171,236,222,251,5,84,3,198,171,236,222,251,5,94,171,236,222,251,5,84,6,105,116,97,108,105,99,4,110,117,108,108,161,171,236,222,251,5,88,1,161,171,236,222,251,5,89,1,161,171,236,222,251,5,90,1,196,171,236,222,251,5,94,171,236,222,251,5,95,9,230,140,168,233,161,191,230,137,147,161,171,236,222,251,5,96,1,161,171,236,222,251,5,97,1,161,171,236,222,251,5,98,1,39,0,204,195,206,156,1,4,6,68,89,98,118,73,66,2,33,0,204,195,206,156,1,1,6,109,57,74,107,49,75,1,0,7,33,0,204,195,206,156,1,3,6,78,76,50,70,103,95,1,193,204,195,206,156,1,238,1,204,195,206,156,1,239,1,1,168,171,236,222,251,5,102,1,119,204,5,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,43,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,102,102,102,102,100,97,101,54,34,44,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,102,102,100,98,51,54,51,54,34,125,44,34,105,110,115,101,114,116,34,58,34,110,101,120,116,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,116,111,32,97,110,121,32,112,97,103,101,32,116,105,116,108,101,32,105,110,32,116,104,101,32,115,105,100,101,98,97,114,32,116,111,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,102,102,56,52,50,55,101,48,34,125,44,34,105,110,115,101,114,116,34,58,34,113,117,105,99,107,108,121,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,105,116,97,108,105,99,34,58,116,114,117,101,44,34,115,116,114,105,107,101,116,104,114,111,117,103,104,34,58,116,114,117,101,44,34,117,110,100,101,114,108,105,110,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,230,140,168,233,161,191,230,137,147,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,97,32,110,101,119,32,115,117,98,112,97,103,101,44,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,68,111,99,117,109,101,110,116,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,102,97,108,115,101,125,44,34,105,110,115,101,114,116,34,58,34,44,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,71,114,105,100,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,102,97,108,115,101,125,44,34,105,110,115,101,114,116,34,58,34,44,32,111,114,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,75,97,110,98,97,110,32,66,111,97,114,100,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,102,97,108,115,101,125,44,34,105,110,115,101,114,116,34,58,34,46,34,125,93,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,168,171,236,222,251,5,103,1,119,10,98,113,76,109,98,57,111,45,109,109,168,171,236,222,251,5,104,1,119,4,116,101,120,116,39,0,204,195,206,156,1,4,6,68,53,45,65,82,65,2,33,0,204,195,206,156,1,1,6,89,87,119,55,87,53,1,0,7,33,0,204,195,206,156,1,3,6,85,68,79,77,112,98,1,1,0,204,195,206,156,1,14,1,39,0,204,195,206,156,1,4,6,107,90,52,97,119,69,2,33,0,204,195,206,156,1,1,6,108,79,120,55,95,83,1,0,7,33,0,204,195,206,156,1,3,6,50,109,115,116,117,104,1,193,171,236,222,251,5,115,204,195,206,156,1,239,1,1,39,0,204,195,206,156,1,4,6,52,98,104,66,88,113,2,33,0,204,195,206,156,1,1,6,121,105,115,116,115,72,1,0,7,33,0,204,195,206,156,1,3,6,53,67,71,119,50,122,1,129,171,236,222,251,5,129,1,1,39,0,204,195,206,156,1,4,6,87,105,113,49,48,95,2,33,0,204,195,206,156,1,1,6,71,121,98,79,49,81,1,0,7,33,0,204,195,206,156,1,3,6,66,90,69,117,90,106,1,193,171,236,222,251,5,129,1,171,236,222,251,5,151,1,1,39,0,204,195,206,156,1,4,6,106,101,65,53,90,85,2,39,0,204,195,206,156,1,1,6,102,67,65,65,81,117,1,40,0,171,236,222,251,5,164,1,2,105,100,1,119,6,102,67,65,65,81,117,40,0,171,236,222,251,5,164,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,171,236,222,251,5,164,1,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,171,236,222,251,5,164,1,8,99,104,105,108,100,114,101,110,1,119,6,52,74,88,112,120,108,33,0,171,236,222,251,5,164,1,4,100,97,116,97,1,40,0,171,236,222,251,5,164,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,164,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,52,74,88,112,120,108,0,200,171,236,222,251,5,129,1,171,236,222,251,5,162,1,1,119,6,102,67,65,65,81,117,4,0,171,236,222,251,5,163,1,6,228,189,147,233,170,140,129,171,236,222,251,5,175,1,1,161,171,236,222,251,5,169,1,1,198,171,236,222,251,5,175,1,171,236,222,251,5,176,1,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,193,171,236,222,251,5,178,1,171,236,222,251,5,176,1,1,198,171,236,222,251,5,179,1,171,236,222,251,5,176,1,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,161,171,236,222,251,5,177,1,1,198,171,236,222,251,5,178,1,171,236,222,251,5,179,1,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,196,171,236,222,251,5,182,1,171,236,222,251,5,179,1,3,228,184,128,198,171,236,222,251,5,183,1,171,236,222,251,5,179,1,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,168,171,236,222,251,5,181,1,1,119,101,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,228,189,147,233,170,140,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,115,116,114,105,107,101,116,104,114,111,117,103,104,34,58,116,114,117,101,44,34,117,110,100,101,114,108,105,110,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,228,184,128,34,125,93,125,39,0,204,195,206,156,1,4,6,82,74,108,100,72,67,2,33,0,204,195,206,156,1,1,6,53,106,100,78,87,117,1,0,7,33,0,204,195,206,156,1,3,6,67,106,107,76,57,108,1,129,171,236,222,251,5,151,1,1,4,0,171,236,222,251,5,186,1,4,103,104,104,104,0,1,39,0,204,195,206,156,1,4,6,70,117,117,52,113,66,2,4,0,171,236,222,251,5,202,1,4,103,104,104,104,33,0,204,195,206,156,1,1,6,121,112,70,119,50,69,1,0,7,33,0,204,195,206,156,1,3,6,71,77,54,72,55,119,1,193,171,236,222,251,5,151,1,171,236,222,251,5,196,1,1,39,0,204,195,206,156,1,4,6,118,113,76,104,110,70,2,4,0,171,236,222,251,5,217,1,4,103,104,104,104,33,0,204,195,206,156,1,1,6,73,57,73,75,116,115,1,0,7,33,0,204,195,206,156,1,3,6,77,114,102,81,106,87,1,193,171,236,222,251,5,140,1,204,195,206,156,1,239,1,1,39,0,204,195,206,156,1,4,6,89,50,52,104,77,55,2,4,0,171,236,222,251,5,232,1,4,103,104,104,104,33,0,204,195,206,156,1,1,6,107,110,74,87,104,48,1,0,7,33,0,204,195,206,156,1,3,6,55,99,67,68,120,77,1,129,171,236,222,251,5,196,1,1,0,7,39,0,204,195,206,156,1,4,6,109,98,56,104,95,45,2,4,0,171,236,222,251,5,254,1,4,103,104,104,104,33,0,204,195,206,156,1,1,6,103,75,73,116,90,101,1,0,7,33,0,204,195,206,156,1,3,6,118,115,54,50,82,105,1,193,171,236,222,251,5,196,1,171,236,222,251,5,246,1,1,39,0,204,195,206,156,1,4,6,105,67,98,56,102,56,2,4,0,171,236,222,251,5,141,2,4,103,104,104,104,33,0,204,195,206,156,1,1,6,88,113,72,53,122,82,1,0,7,33,0,204,195,206,156,1,3,6,73,111,48,108,119,67,1,193,171,236,222,251,5,196,1,171,236,222,251,5,140,2,1,39,0,204,195,206,156,1,4,6,82,89,116,67,111,86,2,4,0,171,236,222,251,5,156,2,4,103,104,104,104,39,0,204,195,206,156,1,1,6,49,120,78,111,50,76,1,40,0,171,236,222,251,5,161,2,2,105,100,1,119,6,49,120,78,111,50,76,40,0,171,236,222,251,5,161,2,2,116,121,1,119,5,113,117,111,116,101,40,0,171,236,222,251,5,161,2,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,171,236,222,251,5,161,2,8,99,104,105,108,100,114,101,110,1,119,6,67,85,68,115,45,70,33,0,171,236,222,251,5,161,2,4,100,97,116,97,1,40,0,171,236,222,251,5,161,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,161,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,67,85,68,115,45,70,0,200,171,236,222,251,5,196,1,171,236,222,251,5,155,2,1,119,6,49,120,78,111,50,76,39,0,204,195,206,156,1,4,6,122,112,90,112,49,101,2,33,0,204,195,206,156,1,1,6,109,65,112,48,84,75,1,0,7,33,0,204,195,206,156,1,3,6,117,95,113,85,121,95,1,129,171,236,222,251,5,246,1,1,4,0,171,236,222,251,5,171,2,4,104,106,106,106,0,1,39,0,204,195,206,156,1,4,6,79,88,120,110,65,84,2,4,0,171,236,222,251,5,187,2,4,104,106,106,106,33,0,204,195,206,156,1,1,6,109,70,99,75,110,81,1,0,7,33,0,204,195,206,156,1,3,6,72,52,122,104,56,95,1,193,171,236,222,251,5,246,1,171,236,222,251,5,181,2,1,39,0,204,195,206,156,1,4,6,90,97,54,99,87,113,2,4,0,171,236,222,251,5,202,2,4,104,106,106,106,33,0,204,195,206,156,1,1,6,50,110,113,71,98,75,1,0,7,33,0,204,195,206,156,1,3,6,95,69,69,76,53,109,1,193,171,236,222,251,5,246,1,171,236,222,251,5,201,2,1,39,0,204,195,206,156,1,4,6,95,88,107,52,56,116,2,4,0,171,236,222,251,5,217,2,4,104,106,106,106,33,0,204,195,206,156,1,1,6,68,99,97,85,109,79,1,0,7,33,0,204,195,206,156,1,3,6,120,69,54,111,108,48,1,193,171,236,222,251,5,231,1,204,195,206,156,1,239,1,1,39,0,204,195,206,156,1,4,6,82,106,87,98,56,111,2,4,0,171,236,222,251,5,232,2,4,104,106,106,106,33,0,204,195,206,156,1,1,6,87,98,82,107,87,69,1,0,7,33,0,204,195,206,156,1,3,6,89,103,114,112,81,55,1,129,171,236,222,251,5,181,2,1,39,0,204,195,206,156,1,4,6,112,107,78,57,112,83,2,4,0,171,236,222,251,5,247,2,4,104,106,106,106,33,0,204,195,206,156,1,1,6,79,98,74,118,76,57,1,0,7,33,0,204,195,206,156,1,3,6,97,79,100,49,121,82,1,193,171,236,222,251,5,181,2,171,236,222,251,5,246,2,1,39,0,204,195,206,156,1,4,6,99,51,100,115,103,56,2,4,0,171,236,222,251,5,134,3,4,104,106,106,106,33,0,204,195,206,156,1,1,6,109,119,79,85,85,87,1,0,7,33,0,204,195,206,156,1,3,6,98,70,74,53,121,122,1,193,171,236,222,251,5,181,2,171,236,222,251,5,133,3,1,39,0,204,195,206,156,1,4,6,104,82,53,106,71,79,2,1,0,171,236,222,251,5,149,3,4,33,0,204,195,206,156,1,1,6,73,77,51,71,72,50,1,0,7,33,0,204,195,206,156,1,3,6,119,85,111,112,97,102,1,193,171,236,222,251,5,181,2,171,236,222,251,5,148,3,1,0,4,39,0,204,195,206,156,1,4,6,74,66,108,114,84,51,2,33,0,204,195,206,156,1,1,6,76,105,86,56,56,104,1,0,7,33,0,204,195,206,156,1,3,6,112,106,107,107,53,97,1,193,171,236,222,251,5,181,2,171,236,222,251,5,163,3,1,1,0,171,236,222,251,5,168,3,1,0,2,129,171,236,222,251,5,179,3,1,0,1,196,171,236,222,251,5,179,3,171,236,222,251,5,182,3,3,226,128,148,0,1,39,0,204,195,206,156,1,4,6,89,108,67,77,99,111,2,39,0,204,195,206,156,1,1,6,112,69,80,105,117,115,1,40,0,171,236,222,251,5,187,3,2,105,100,1,119,6,112,69,80,105,117,115,40,0,171,236,222,251,5,187,3,2,116,121,1,119,7,100,105,118,105,100,101,114,40,0,171,236,222,251,5,187,3,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,171,236,222,251,5,187,3,8,99,104,105,108,100,114,101,110,1,119,6,49,45,49,107,111,85,40,0,171,236,222,251,5,187,3,4,100,97,116,97,1,119,2,123,125,40,0,171,236,222,251,5,187,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,187,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,49,45,49,107,111,85,0,200,171,236,222,251,5,181,2,171,236,222,251,5,178,3,1,119,6,112,69,80,105,117,115,39,0,204,195,206,156,1,1,6,95,65,78,99,110,51,1,40,0,171,236,222,251,5,197,3,2,105,100,1,119,6,95,65,78,99,110,51,40,0,171,236,222,251,5,197,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,171,236,222,251,5,197,3,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,171,236,222,251,5,197,3,8,99,104,105,108,100,114,101,110,1,119,6,84,45,117,87,109,83,33,0,171,236,222,251,5,197,3,4,100,97,116,97,1,40,0,171,236,222,251,5,197,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,197,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,84,45,117,87,109,83,0,136,171,236,222,251,5,246,2,1,119,6,95,65,78,99,110,51,4,0,171,236,222,251,5,186,3,1,54,161,171,236,222,251,5,202,3,1,132,171,236,222,251,5,207,3,1,54,161,171,236,222,251,5,208,3,1,132,171,236,222,251,5,209,3,1,54,161,171,236,222,251,5,210,3,1,132,171,236,222,251,5,211,3,1,57,168,171,236,222,251,5,212,3,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,54,54,54,57,34,125,93,125,39,0,204,195,206,156,1,1,6,79,77,79,95,52,106,1,40,0,171,236,222,251,5,215,3,2,105,100,1,119,6,79,77,79,95,52,106,40,0,171,236,222,251,5,215,3,2,116,121,1,119,7,100,105,118,105,100,101,114,40,0,171,236,222,251,5,215,3,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,171,236,222,251,5,215,3,8,99,104,105,108,100,114,101,110,1,119,6,99,107,78,65,119,105,40,0,171,236,222,251,5,215,3,4,100,97,116,97,1,119,2,123,125,40,0,171,236,222,251,5,215,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,215,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,99,107,78,65,119,105,0,200,171,236,222,251,5,246,2,171,236,222,251,5,206,3,1,119,6,79,77,79,95,52,106,39,0,204,195,206,156,1,4,6,45,80,118,95,90,87,2,4,0,171,236,222,251,5,225,3,4,103,104,104,104,39,0,204,195,206,156,1,4,6,87,105,54,69,77,70,2,4,0,171,236,222,251,5,230,3,4,54,54,54,57,39,0,204,195,206,156,1,1,6,122,78,116,118,66,84,1,40,0,171,236,222,251,5,235,3,2,105,100,1,119,6,122,78,116,118,66,84,40,0,171,236,222,251,5,235,3,2,116,121,1,119,5,113,117,111,116,101,40,0,171,236,222,251,5,235,3,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,171,236,222,251,5,235,3,8,99,104,105,108,100,114,101,110,1,119,6,105,85,75,111,98,79,33,0,171,236,222,251,5,235,3,4,100,97,116,97,1,40,0,171,236,222,251,5,235,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,235,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,105,85,75,111,98,79,0,200,171,236,222,251,5,231,2,204,195,206,156,1,239,1,1,119,6,122,78,116,118,66,84,33,0,204,195,206,156,1,1,6,57,104,76,90,119,104,1,0,7,33,0,204,195,206,156,1,3,6,120,113,118,100,85,73,1,193,171,236,222,251,5,244,3,204,195,206,156,1,239,1,1,39,0,204,195,206,156,1,4,6,75,122,88,45,65,53,2,1,0,171,236,222,251,5,255,3,4,39,0,204,195,206,156,1,1,6,49,51,100,105,49,69,1,40,0,171,236,222,251,5,132,4,2,105,100,1,119,6,49,51,100,105,49,69,40,0,171,236,222,251,5,132,4,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,171,236,222,251,5,132,4,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,171,236,222,251,5,132,4,8,99,104,105,108,100,114,101,110,1,119,6,106,54,115,52,103,109,33,0,171,236,222,251,5,132,4,4,100,97,116,97,1,40,0,171,236,222,251,5,132,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,171,236,222,251,5,132,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,106,54,115,52,103,109,0,200,171,236,222,251,5,244,3,171,236,222,251,5,254,3,1,119,6,49,51,100,105,49,69,161,171,236,222,251,5,137,4,2,65,171,236,222,251,5,128,4,5,198,171,236,222,251,5,148,4,171,236,222,251,5,128,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,143,4,1,65,171,236,222,251,5,144,4,5,198,171,236,222,251,5,155,4,171,236,222,251,5,144,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,150,4,1,65,171,236,222,251,5,151,4,5,198,171,236,222,251,5,162,4,171,236,222,251,5,151,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,157,4,1,65,171,236,222,251,5,158,4,5,198,171,236,222,251,5,169,4,171,236,222,251,5,158,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,164,4,1,65,171,236,222,251,5,165,4,5,198,171,236,222,251,5,176,4,171,236,222,251,5,165,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,171,4,1,65,171,236,222,251,5,172,4,5,198,171,236,222,251,5,183,4,171,236,222,251,5,172,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,178,4,1,65,171,236,222,251,5,179,4,5,198,171,236,222,251,5,190,4,171,236,222,251,5,179,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,185,4,1,65,171,236,222,251,5,186,4,5,198,171,236,222,251,5,197,4,171,236,222,251,5,186,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,192,4,1,70,171,236,222,251,5,193,4,8,98,103,95,99,111,108,111,114,12,34,48,120,102,102,102,102,100,97,101,54,34,193,171,236,222,251,5,200,4,171,236,222,251,5,193,4,4,198,171,236,222,251,5,204,4,171,236,222,251,5,193,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,199,4,2,193,171,236,222,251,5,200,4,171,236,222,251,5,201,4,5,198,171,236,222,251,5,212,4,171,236,222,251,5,201,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,207,4,1,193,171,236,222,251,5,200,4,171,236,222,251,5,208,4,5,198,171,236,222,251,5,219,4,171,236,222,251,5,208,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,214,4,1,193,171,236,222,251,5,200,4,171,236,222,251,5,215,4,5,198,171,236,222,251,5,226,4,171,236,222,251,5,215,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,221,4,1,193,171,236,222,251,5,200,4,171,236,222,251,5,222,4,5,198,171,236,222,251,5,233,4,171,236,222,251,5,222,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,228,4,1,193,171,236,222,251,5,200,4,171,236,222,251,5,229,4,5,198,171,236,222,251,5,240,4,171,236,222,251,5,229,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,161,171,236,222,251,5,235,4,1,70,171,236,222,251,5,200,4,10,102,111,110,116,95,99,111,108,111,114,12,34,48,120,102,102,101,97,56,102,48,54,34,198,171,236,222,251,5,243,4,171,236,222,251,5,200,4,8,98,103,95,99,111,108,111,114,12,34,48,120,102,102,97,55,100,102,52,97,34,196,171,236,222,251,5,244,4,171,236,222,251,5,200,4,4,54,54,54,57,193,171,236,222,251,5,248,4,171,236,222,251,5,200,4,1,198,171,236,222,251,5,249,4,171,236,222,251,5,200,4,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,168,171,236,222,251,5,242,4,1,119,110,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,102,102,97,55,100,102,52,97,34,44,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,102,102,101,97,56,102,48,54,34,125,44,34,105,110,115,101,114,116,34,58,34,54,54,54,57,34,125,93,125,7,226,167,254,250,5,0,39,0,204,195,206,156,1,4,6,72,97,87,55,95,104,2,4,0,226,167,254,250,5,0,13,229,144,140,228,184,128,228,184,170,106,106,106,56,161,198,223,206,159,1,124,1,132,226,167,254,250,5,7,1,56,161,226,167,254,250,5,8,1,132,226,167,254,250,5,9,1,56,161,226,167,254,250,5,10,1,1,161,234,157,145,5,0,161,247,212,219,208,10,45,7,3,177,239,218,225,4,0,161,207,231,154,196,9,0,1,161,207,231,154,196,9,1,1,161,207,231,154,196,9,2,1,1,136,172,186,168,4,0,161,176,238,158,139,14,174,2,182,6,24,141,151,160,163,4,0,161,177,239,218,225,4,0,1,161,177,239,218,225,4,1,1,161,177,239,218,225,4,2,1,161,141,151,160,163,4,0,1,161,141,151,160,163,4,1,1,161,141,151,160,163,4,2,1,161,141,151,160,163,4,3,1,161,141,151,160,163,4,4,1,161,141,151,160,163,4,5,1,161,141,151,160,163,4,6,1,161,141,151,160,163,4,7,1,161,141,151,160,163,4,8,1,161,141,151,160,163,4,9,1,161,141,151,160,163,4,10,1,161,141,151,160,163,4,11,1,161,141,151,160,163,4,12,1,161,141,151,160,163,4,13,1,161,141,151,160,163,4,14,1,161,141,151,160,163,4,15,1,161,141,151,160,163,4,16,1,161,141,151,160,163,4,17,1,161,141,151,160,163,4,18,1,161,141,151,160,163,4,19,1,161,141,151,160,163,4,20,1,4,217,168,198,159,4,0,129,204,195,206,156,1,154,7,4,161,204,195,206,156,1,227,1,1,161,204,195,206,156,1,228,1,1,161,204,195,206,156,1,229,1,1,51,220,225,223,240,3,0,1,0,204,195,206,156,1,132,4,1,161,204,195,206,156,1,83,1,161,204,195,206,156,1,84,1,161,204,195,206,156,1,85,1,134,220,225,223,240,3,0,7,109,101,110,116,105,111,110,64,123,34,112,97,103,101,95,105,100,34,58,34,100,48,52,57,54,51,50,52,45,53,53,55,48,45,52,48,48,54,45,98,52,101,97,45,100,98,55,53,49,54,100,50,49,50,102,100,34,44,34,116,121,112,101,34,58,34,112,97,103,101,34,125,132,220,225,223,240,3,4,1,36,134,220,225,223,240,3,5,7,109,101,110,116,105,111,110,4,110,117,108,108,161,220,225,223,240,3,1,1,161,220,225,223,240,3,2,1,161,220,225,223,240,3,3,1,39,0,204,195,206,156,1,4,6,50,108,103,80,119,50,2,33,0,204,195,206,156,1,1,6,115,120,112,66,109,100,1,0,7,33,0,204,195,206,156,1,3,6,52,97,66,55,99,78,1,193,204,195,206,156,1,250,1,204,195,206,156,1,251,1,1,1,0,220,225,223,240,3,10,1,0,2,129,220,225,223,240,3,21,1,0,2,161,243,138,171,183,10,249,1,1,161,243,138,171,183,10,250,1,1,161,243,138,171,183,10,251,1,1,161,220,225,223,240,3,27,1,161,220,225,223,240,3,28,1,161,220,225,223,240,3,29,1,161,194,228,144,71,7,2,39,0,204,195,206,156,1,4,6,48,102,55,71,122,95,2,39,0,204,195,206,156,1,1,6,54,118,84,69,79,115,1,40,0,220,225,223,240,3,36,2,105,100,1,119,6,54,118,84,69,79,115,40,0,220,225,223,240,3,36,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,220,225,223,240,3,36,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,220,225,223,240,3,36,8,99,104,105,108,100,114,101,110,1,119,6,106,107,73,81,115,57,33,0,220,225,223,240,3,36,4,100,97,116,97,1,40,0,220,225,223,240,3,36,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,220,225,223,240,3,36,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,106,107,73,81,115,57,0,200,220,225,223,240,3,20,204,195,206,156,1,251,1,1,119,6,54,118,84,69,79,115,1,0,220,225,223,240,3,35,1,161,220,225,223,240,3,41,1,134,220,225,223,240,3,46,7,109,101,110,116,105,111,110,64,123,34,112,97,103,101,95,105,100,34,58,34,52,52,51,53,101,53,55,98,45,99,50,54,51,45,52,101,55,102,45,97,52,51,53,45,50,48,56,55,57,97,54,50,101,54,100,97,34,44,34,116,121,112,101,34,58,34,112,97,103,101,34,125,132,220,225,223,240,3,48,1,36,134,220,225,223,240,3,49,7,109,101,110,116,105,111,110,4,110,117,108,108,161,220,225,223,240,3,47,1,39,0,204,195,206,156,1,4,6,111,89,48,54,98,73,2,161,220,225,223,240,3,51,1,1,0,220,225,223,240,3,52,1,161,220,225,223,240,3,53,1,134,220,225,223,240,3,54,7,109,101,110,116,105,111,110,64,123,34,112,97,103,101,95,105,100,34,58,34,100,100,98,57,51,98,97,55,45,48,54,99,55,45,52,49,55,54,45,57,56,50,97,45,100,55,52,50,51,101,48,57,98,52,52,49,34,44,34,116,121,112,101,34,58,34,112,97,103,101,34,125,132,220,225,223,240,3,56,1,36,134,220,225,223,240,3,57,7,109,101,110,116,105,111,110,4,110,117,108,108,161,220,225,223,240,3,55,1,29,133,181,204,218,3,0,39,0,204,195,206,156,1,4,6,118,122,72,105,108,97,2,6,0,133,181,204,218,3,0,4,104,114,101,102,13,34,49,57,50,46,49,54,56,46,49,46,50,34,132,133,181,204,218,3,1,9,99,111,110,116,101,110,116,32,49,134,133,181,204,218,3,10,4,104,114,101,102,4,110,117,108,108,132,133,181,204,218,3,11,3,32,50,32,161,238,153,239,204,9,48,1,132,133,181,204,218,3,14,1,97,161,133,181,204,218,3,15,1,129,133,181,204,218,3,16,1,132,133,181,204,218,3,18,1,112,161,133,181,204,218,3,17,1,196,133,181,204,218,3,16,133,181,204,218,3,18,1,112,161,133,181,204,218,3,20,1,132,133,181,204,218,3,19,1,102,161,133,181,204,218,3,22,1,132,133,181,204,218,3,23,1,108,161,133,181,204,218,3,24,1,132,133,181,204,218,3,25,1,111,161,133,181,204,218,3,26,1,132,133,181,204,218,3,27,1,119,161,133,181,204,218,3,28,1,132,133,181,204,218,3,29,1,121,168,133,181,204,218,3,30,1,119,95,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,104,114,101,102,34,58,34,49,57,50,46,49,54,56,46,49,46,50,34,125,44,34,105,110,115,101,114,116,34,58,34,99,111,110,116,101,110,116,32,49,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,50,32,97,112,112,102,108,111,119,121,34,125,93,125,39,0,204,195,206,156,1,4,6,68,50,72,75,72,71,2,6,0,133,181,204,218,3,33,4,104,114,101,102,22,34,97,112,112,102,108,111,119,121,46,105,111,47,100,111,119,110,108,111,97,100,34,132,133,181,204,218,3,34,11,97,112,112,102,108,111,119,121,46,105,111,134,133,181,204,218,3,45,4,104,114,101,102,4,110,117,108,108,132,133,181,204,218,3,46,2,32,49,168,238,153,239,204,9,32,1,119,97,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,104,114,101,102,34,58,34,97,112,112,102,108,111,119,121,46,105,111,47,100,111,119,110,108,111,97,100,34,125,44,34,105,110,115,101,114,116,34,58,34,97,112,112,102,108,111,119,121,46,105,111,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,49,34,125,93,125,1,236,253,128,205,3,0,161,240,179,157,219,7,3,9,215,6,150,216,171,142,3,0,161,199,130,209,189,2,177,3,6,161,164,202,219,213,10,80,1,161,164,202,219,213,10,81,1,161,164,202,219,213,10,82,1,161,150,216,171,142,3,6,1,161,150,216,171,142,3,7,1,161,150,216,171,142,3,8,1,129,204,195,206,156,1,207,5,1,161,150,216,171,142,3,9,1,161,150,216,171,142,3,10,1,161,150,216,171,142,3,11,1,129,150,216,171,142,3,12,1,161,150,216,171,142,3,13,1,161,150,216,171,142,3,14,1,161,150,216,171,142,3,15,1,161,150,216,171,142,3,17,1,161,150,216,171,142,3,18,1,161,150,216,171,142,3,19,1,161,150,216,171,142,3,20,1,161,150,216,171,142,3,21,1,161,150,216,171,142,3,22,1,129,150,216,171,142,3,16,1,161,150,216,171,142,3,23,1,161,150,216,171,142,3,24,1,161,150,216,171,142,3,25,1,129,150,216,171,142,3,26,1,161,150,216,171,142,3,27,1,161,150,216,171,142,3,28,1,161,150,216,171,142,3,29,1,129,150,216,171,142,3,30,1,161,150,216,171,142,3,31,1,161,150,216,171,142,3,32,1,161,150,216,171,142,3,33,1,129,150,216,171,142,3,34,1,161,150,216,171,142,3,35,1,161,150,216,171,142,3,36,1,161,150,216,171,142,3,37,1,129,150,216,171,142,3,38,1,161,150,216,171,142,3,39,1,161,150,216,171,142,3,40,1,161,150,216,171,142,3,41,1,129,150,216,171,142,3,42,1,161,150,216,171,142,3,43,1,161,150,216,171,142,3,44,1,161,150,216,171,142,3,45,1,129,150,216,171,142,3,46,1,161,150,216,171,142,3,47,1,161,150,216,171,142,3,48,1,161,150,216,171,142,3,49,1,129,150,216,171,142,3,50,1,161,150,216,171,142,3,51,1,161,150,216,171,142,3,52,1,161,150,216,171,142,3,53,1,129,150,216,171,142,3,54,1,161,150,216,171,142,3,55,1,161,150,216,171,142,3,56,1,161,150,216,171,142,3,57,1,129,150,216,171,142,3,58,1,161,150,216,171,142,3,59,1,161,150,216,171,142,3,60,1,161,150,216,171,142,3,61,1,129,150,216,171,142,3,62,1,161,150,216,171,142,3,63,1,161,150,216,171,142,3,64,1,161,150,216,171,142,3,65,1,129,150,216,171,142,3,66,1,161,150,216,171,142,3,67,1,161,150,216,171,142,3,68,1,161,150,216,171,142,3,69,1,129,150,216,171,142,3,70,1,161,150,216,171,142,3,71,1,161,150,216,171,142,3,72,1,161,150,216,171,142,3,73,1,129,150,216,171,142,3,74,1,161,150,216,171,142,3,75,1,161,150,216,171,142,3,76,1,161,150,216,171,142,3,77,1,129,150,216,171,142,3,78,1,161,150,216,171,142,3,79,1,161,150,216,171,142,3,80,1,161,150,216,171,142,3,81,1,129,150,216,171,142,3,82,1,161,150,216,171,142,3,83,1,161,150,216,171,142,3,84,1,161,150,216,171,142,3,85,1,129,150,216,171,142,3,86,1,161,150,216,171,142,3,87,1,161,150,216,171,142,3,88,1,161,150,216,171,142,3,89,1,129,150,216,171,142,3,90,1,161,150,216,171,142,3,91,1,161,150,216,171,142,3,92,1,161,150,216,171,142,3,93,1,129,150,216,171,142,3,94,1,161,150,216,171,142,3,95,1,161,150,216,171,142,3,96,1,161,150,216,171,142,3,97,1,129,150,216,171,142,3,98,1,161,150,216,171,142,3,99,1,161,150,216,171,142,3,100,1,161,150,216,171,142,3,101,1,129,150,216,171,142,3,102,1,161,150,216,171,142,3,103,1,161,150,216,171,142,3,104,1,161,150,216,171,142,3,105,1,129,150,216,171,142,3,106,1,161,150,216,171,142,3,107,1,161,150,216,171,142,3,108,1,161,150,216,171,142,3,109,1,129,150,216,171,142,3,110,1,161,150,216,171,142,3,111,1,161,150,216,171,142,3,112,1,161,150,216,171,142,3,113,1,129,150,216,171,142,3,114,1,161,150,216,171,142,3,115,1,161,150,216,171,142,3,116,1,161,150,216,171,142,3,117,1,129,150,216,171,142,3,118,1,161,150,216,171,142,3,119,1,161,150,216,171,142,3,120,1,161,150,216,171,142,3,121,1,129,150,216,171,142,3,122,1,161,150,216,171,142,3,123,1,161,150,216,171,142,3,124,1,161,150,216,171,142,3,125,1,129,150,216,171,142,3,126,1,161,150,216,171,142,3,127,1,161,150,216,171,142,3,128,1,1,161,150,216,171,142,3,129,1,1,129,150,216,171,142,3,130,1,1,161,150,216,171,142,3,131,1,1,161,150,216,171,142,3,132,1,1,161,150,216,171,142,3,133,1,1,129,150,216,171,142,3,134,1,1,161,150,216,171,142,3,135,1,1,161,150,216,171,142,3,136,1,1,161,150,216,171,142,3,137,1,1,193,150,216,171,142,3,134,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,139,1,1,161,150,216,171,142,3,140,1,1,161,150,216,171,142,3,141,1,1,193,150,216,171,142,3,142,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,143,1,1,161,150,216,171,142,3,144,1,1,161,150,216,171,142,3,145,1,1,193,150,216,171,142,3,146,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,147,1,1,161,150,216,171,142,3,148,1,1,161,150,216,171,142,3,149,1,1,193,150,216,171,142,3,150,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,151,1,1,161,150,216,171,142,3,152,1,1,161,150,216,171,142,3,153,1,1,193,150,216,171,142,3,154,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,155,1,1,161,150,216,171,142,3,156,1,1,161,150,216,171,142,3,157,1,1,193,150,216,171,142,3,158,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,159,1,1,161,150,216,171,142,3,160,1,1,161,150,216,171,142,3,161,1,1,193,150,216,171,142,3,162,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,163,1,1,161,150,216,171,142,3,164,1,1,161,150,216,171,142,3,165,1,1,193,150,216,171,142,3,166,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,167,1,1,161,150,216,171,142,3,168,1,1,161,150,216,171,142,3,169,1,1,193,150,216,171,142,3,170,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,171,1,1,161,150,216,171,142,3,172,1,1,161,150,216,171,142,3,173,1,1,193,150,216,171,142,3,174,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,175,1,1,161,150,216,171,142,3,176,1,1,161,150,216,171,142,3,177,1,1,193,150,216,171,142,3,178,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,179,1,1,161,150,216,171,142,3,180,1,1,161,150,216,171,142,3,181,1,1,193,150,216,171,142,3,182,1,150,216,171,142,3,138,1,1,161,150,216,171,142,3,183,1,1,161,150,216,171,142,3,184,1,1,161,150,216,171,142,3,185,1,1,129,150,216,171,142,3,138,1,4,161,150,216,171,142,3,187,1,1,161,150,216,171,142,3,188,1,1,161,150,216,171,142,3,189,1,1,129,150,216,171,142,3,193,1,1,161,150,216,171,142,3,194,1,1,161,150,216,171,142,3,195,1,1,161,150,216,171,142,3,196,1,1,161,150,216,171,142,3,198,1,1,161,150,216,171,142,3,199,1,1,161,150,216,171,142,3,200,1,1,161,150,216,171,142,3,201,1,1,161,150,216,171,142,3,202,1,1,161,150,216,171,142,3,203,1,1,161,150,216,171,142,3,204,1,1,161,150,216,171,142,3,205,1,1,161,150,216,171,142,3,206,1,1,129,150,216,171,142,3,197,1,1,161,150,216,171,142,3,207,1,1,161,150,216,171,142,3,208,1,1,161,150,216,171,142,3,209,1,1,129,150,216,171,142,3,210,1,1,161,150,216,171,142,3,211,1,1,161,150,216,171,142,3,212,1,1,161,150,216,171,142,3,213,1,1,129,150,216,171,142,3,214,1,1,161,150,216,171,142,3,215,1,1,161,150,216,171,142,3,216,1,1,161,150,216,171,142,3,217,1,1,129,150,216,171,142,3,218,1,1,161,150,216,171,142,3,219,1,1,161,150,216,171,142,3,220,1,1,161,150,216,171,142,3,221,1,1,129,150,216,171,142,3,222,1,1,161,150,216,171,142,3,223,1,1,161,150,216,171,142,3,224,1,1,161,150,216,171,142,3,225,1,1,129,150,216,171,142,3,226,1,1,161,150,216,171,142,3,227,1,1,161,150,216,171,142,3,228,1,1,161,150,216,171,142,3,229,1,1,129,150,216,171,142,3,230,1,1,161,150,216,171,142,3,231,1,1,161,150,216,171,142,3,232,1,1,161,150,216,171,142,3,233,1,1,129,150,216,171,142,3,234,1,1,161,150,216,171,142,3,235,1,1,161,150,216,171,142,3,236,1,1,161,150,216,171,142,3,237,1,1,129,150,216,171,142,3,238,1,1,161,150,216,171,142,3,239,1,1,161,150,216,171,142,3,240,1,1,161,150,216,171,142,3,241,1,1,129,150,216,171,142,3,242,1,1,161,150,216,171,142,3,243,1,1,161,150,216,171,142,3,244,1,1,161,150,216,171,142,3,245,1,1,129,150,216,171,142,3,246,1,1,161,150,216,171,142,3,247,1,1,161,150,216,171,142,3,248,1,1,161,150,216,171,142,3,249,1,1,129,150,216,171,142,3,250,1,1,161,150,216,171,142,3,251,1,1,161,150,216,171,142,3,252,1,1,161,150,216,171,142,3,253,1,1,129,150,216,171,142,3,254,1,1,161,150,216,171,142,3,255,1,1,161,150,216,171,142,3,128,2,1,161,150,216,171,142,3,129,2,1,129,150,216,171,142,3,130,2,1,161,150,216,171,142,3,131,2,1,161,150,216,171,142,3,132,2,1,161,150,216,171,142,3,133,2,1,129,150,216,171,142,3,134,2,1,161,150,216,171,142,3,135,2,1,161,150,216,171,142,3,136,2,1,161,150,216,171,142,3,137,2,1,129,150,216,171,142,3,138,2,1,161,150,216,171,142,3,139,2,1,161,150,216,171,142,3,140,2,1,161,150,216,171,142,3,141,2,1,161,150,216,171,142,3,143,2,1,161,150,216,171,142,3,144,2,1,161,150,216,171,142,3,145,2,1,129,150,216,171,142,3,142,2,1,161,150,216,171,142,3,146,2,1,161,150,216,171,142,3,147,2,1,161,150,216,171,142,3,148,2,1,161,150,216,171,142,3,150,2,1,161,150,216,171,142,3,151,2,1,161,150,216,171,142,3,152,2,1,161,150,216,171,142,3,153,2,1,161,150,216,171,142,3,154,2,1,161,150,216,171,142,3,155,2,1,161,150,216,171,142,3,156,2,1,161,150,216,171,142,3,157,2,1,161,150,216,171,142,3,158,2,1,129,150,216,171,142,3,149,2,1,161,150,216,171,142,3,159,2,1,161,150,216,171,142,3,160,2,1,161,150,216,171,142,3,161,2,1,129,150,216,171,142,3,162,2,1,161,150,216,171,142,3,163,2,1,161,150,216,171,142,3,164,2,1,161,150,216,171,142,3,165,2,1,129,150,216,171,142,3,166,2,1,161,150,216,171,142,3,167,2,1,161,150,216,171,142,3,168,2,1,161,150,216,171,142,3,169,2,1,129,150,216,171,142,3,170,2,1,161,150,216,171,142,3,171,2,1,161,150,216,171,142,3,172,2,1,161,150,216,171,142,3,173,2,1,129,150,216,171,142,3,174,2,1,161,150,216,171,142,3,175,2,1,161,150,216,171,142,3,176,2,1,161,150,216,171,142,3,177,2,1,129,150,216,171,142,3,178,2,1,161,150,216,171,142,3,179,2,1,161,150,216,171,142,3,180,2,1,161,150,216,171,142,3,181,2,1,129,150,216,171,142,3,182,2,1,161,150,216,171,142,3,183,2,1,161,150,216,171,142,3,184,2,1,161,150,216,171,142,3,185,2,1,129,150,216,171,142,3,186,2,1,161,150,216,171,142,3,187,2,1,161,150,216,171,142,3,188,2,1,161,150,216,171,142,3,189,2,1,129,150,216,171,142,3,190,2,1,161,150,216,171,142,3,191,2,1,161,150,216,171,142,3,192,2,1,161,150,216,171,142,3,193,2,1,129,150,216,171,142,3,194,2,1,161,150,216,171,142,3,195,2,1,161,150,216,171,142,3,196,2,1,161,150,216,171,142,3,197,2,1,129,150,216,171,142,3,198,2,1,161,150,216,171,142,3,199,2,1,161,150,216,171,142,3,200,2,1,161,150,216,171,142,3,201,2,1,193,150,216,171,142,3,198,2,150,216,171,142,3,202,2,1,161,150,216,171,142,3,203,2,1,161,150,216,171,142,3,204,2,1,161,150,216,171,142,3,205,2,1,193,150,216,171,142,3,206,2,150,216,171,142,3,202,2,1,161,150,216,171,142,3,207,2,1,161,150,216,171,142,3,208,2,1,161,150,216,171,142,3,209,2,1,193,150,216,171,142,3,206,2,150,216,171,142,3,210,2,2,161,150,216,171,142,3,211,2,1,161,150,216,171,142,3,212,2,1,161,150,216,171,142,3,213,2,1,193,150,216,171,142,3,215,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,216,2,1,161,150,216,171,142,3,217,2,1,161,150,216,171,142,3,218,2,1,193,150,216,171,142,3,219,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,220,2,1,161,150,216,171,142,3,221,2,1,161,150,216,171,142,3,222,2,1,193,150,216,171,142,3,223,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,224,2,1,161,150,216,171,142,3,225,2,1,161,150,216,171,142,3,226,2,1,193,150,216,171,142,3,227,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,228,2,1,161,150,216,171,142,3,229,2,1,161,150,216,171,142,3,230,2,1,193,150,216,171,142,3,231,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,232,2,1,161,150,216,171,142,3,233,2,1,161,150,216,171,142,3,234,2,1,193,150,216,171,142,3,235,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,236,2,1,161,150,216,171,142,3,237,2,1,161,150,216,171,142,3,238,2,1,193,150,216,171,142,3,239,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,240,2,1,161,150,216,171,142,3,241,2,1,161,150,216,171,142,3,242,2,1,193,150,216,171,142,3,243,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,244,2,1,161,150,216,171,142,3,245,2,1,161,150,216,171,142,3,246,2,1,193,150,216,171,142,3,247,2,150,216,171,142,3,210,2,1,161,150,216,171,142,3,248,2,1,161,150,216,171,142,3,249,2,1,161,150,216,171,142,3,250,2,1,193,150,216,171,142,3,247,2,150,216,171,142,3,251,2,1,161,150,216,171,142,3,252,2,1,161,150,216,171,142,3,253,2,1,161,150,216,171,142,3,254,2,1,193,150,216,171,142,3,255,2,150,216,171,142,3,251,2,1,161,150,216,171,142,3,128,3,1,161,150,216,171,142,3,129,3,1,161,150,216,171,142,3,130,3,1,193,150,216,171,142,3,255,2,150,216,171,142,3,131,3,1,161,150,216,171,142,3,132,3,1,161,150,216,171,142,3,133,3,1,161,150,216,171,142,3,134,3,1,193,150,216,171,142,3,135,3,150,216,171,142,3,131,3,1,161,150,216,171,142,3,136,3,1,161,150,216,171,142,3,137,3,1,161,150,216,171,142,3,138,3,1,193,150,216,171,142,3,139,3,150,216,171,142,3,131,3,1,161,150,216,171,142,3,140,3,1,161,150,216,171,142,3,141,3,1,161,150,216,171,142,3,142,3,1,161,150,216,171,142,3,144,3,1,161,150,216,171,142,3,145,3,1,161,150,216,171,142,3,146,3,1,193,204,195,206,156,1,130,5,204,195,206,156,1,131,5,1,161,150,216,171,142,3,147,3,1,161,150,216,171,142,3,148,3,1,161,150,216,171,142,3,149,3,1,129,150,216,171,142,3,202,2,1,161,150,216,171,142,3,151,3,1,161,150,216,171,142,3,152,3,1,161,150,216,171,142,3,153,3,1,129,150,216,171,142,3,154,3,1,161,150,216,171,142,3,155,3,1,161,150,216,171,142,3,156,3,1,161,150,216,171,142,3,157,3,1,161,150,216,171,142,3,159,3,1,161,150,216,171,142,3,160,3,1,161,150,216,171,142,3,161,3,1,161,150,216,171,142,3,162,3,1,161,150,216,171,142,3,163,3,1,161,150,216,171,142,3,164,3,1,161,150,216,171,142,3,165,3,1,161,150,216,171,142,3,166,3,1,161,150,216,171,142,3,167,3,1,132,150,216,171,142,3,158,3,1,102,161,150,216,171,142,3,168,3,1,161,150,216,171,142,3,169,3,1,161,150,216,171,142,3,170,3,1,132,150,216,171,142,3,171,3,1,117,161,150,216,171,142,3,172,3,1,161,150,216,171,142,3,173,3,1,161,150,216,171,142,3,174,3,1,132,150,216,171,142,3,175,3,1,110,161,150,216,171,142,3,176,3,1,161,150,216,171,142,3,177,3,1,161,150,216,171,142,3,178,3,1,132,150,216,171,142,3,179,3,1,99,161,150,216,171,142,3,180,3,1,161,150,216,171,142,3,181,3,1,161,150,216,171,142,3,182,3,1,132,150,216,171,142,3,183,3,1,116,161,150,216,171,142,3,184,3,1,161,150,216,171,142,3,185,3,1,161,150,216,171,142,3,186,3,1,132,150,216,171,142,3,187,3,1,105,161,150,216,171,142,3,188,3,1,161,150,216,171,142,3,189,3,1,161,150,216,171,142,3,190,3,1,132,150,216,171,142,3,191,3,1,111,161,150,216,171,142,3,192,3,1,161,150,216,171,142,3,193,3,1,161,150,216,171,142,3,194,3,1,132,150,216,171,142,3,195,3,1,110,161,150,216,171,142,3,196,3,1,161,150,216,171,142,3,197,3,1,161,150,216,171,142,3,198,3,1,132,150,216,171,142,3,199,3,1,32,161,150,216,171,142,3,200,3,1,161,150,216,171,142,3,201,3,1,161,150,216,171,142,3,202,3,1,132,150,216,171,142,3,203,3,1,109,161,150,216,171,142,3,204,3,1,161,150,216,171,142,3,205,3,1,161,150,216,171,142,3,206,3,1,132,150,216,171,142,3,207,3,1,97,161,150,216,171,142,3,208,3,1,161,150,216,171,142,3,209,3,1,161,150,216,171,142,3,210,3,1,132,150,216,171,142,3,211,3,1,105,161,150,216,171,142,3,212,3,1,161,150,216,171,142,3,213,3,1,161,150,216,171,142,3,214,3,1,132,150,216,171,142,3,215,3,1,110,161,150,216,171,142,3,216,3,1,161,150,216,171,142,3,217,3,1,161,150,216,171,142,3,218,3,1,132,150,216,171,142,3,219,3,1,40,161,150,216,171,142,3,220,3,1,161,150,216,171,142,3,221,3,1,161,150,216,171,142,3,222,3,1,132,150,216,171,142,3,223,3,1,41,161,150,216,171,142,3,224,3,1,161,150,216,171,142,3,225,3,1,161,150,216,171,142,3,226,3,1,132,150,216,171,142,3,227,3,1,32,161,150,216,171,142,3,228,3,1,161,150,216,171,142,3,229,3,1,161,150,216,171,142,3,230,3,1,132,150,216,171,142,3,231,3,1,123,161,150,216,171,142,3,232,3,1,161,150,216,171,142,3,233,3,1,161,150,216,171,142,3,234,3,1,132,150,216,171,142,3,235,3,1,125,161,150,216,171,142,3,236,3,1,161,150,216,171,142,3,237,3,1,161,150,216,171,142,3,238,3,1,196,150,216,171,142,3,235,3,150,216,171,142,3,239,3,1,10,161,150,216,171,142,3,240,3,1,161,150,216,171,142,3,241,3,1,161,150,216,171,142,3,242,3,1,196,150,216,171,142,3,243,3,150,216,171,142,3,239,3,1,10,161,150,216,171,142,3,244,3,1,161,150,216,171,142,3,245,3,1,161,150,216,171,142,3,246,3,1,196,150,216,171,142,3,243,3,150,216,171,142,3,247,3,2,32,32,161,150,216,171,142,3,248,3,1,161,150,216,171,142,3,249,3,1,161,150,216,171,142,3,250,3,1,196,150,216,171,142,3,252,3,150,216,171,142,3,247,3,1,99,161,150,216,171,142,3,253,3,1,161,150,216,171,142,3,254,3,1,161,150,216,171,142,3,255,3,1,196,150,216,171,142,3,128,4,150,216,171,142,3,247,3,1,111,161,150,216,171,142,3,129,4,1,161,150,216,171,142,3,130,4,1,161,150,216,171,142,3,131,4,1,196,150,216,171,142,3,132,4,150,216,171,142,3,247,3,1,110,161,150,216,171,142,3,133,4,1,161,150,216,171,142,3,134,4,1,161,150,216,171,142,3,135,4,1,196,150,216,171,142,3,136,4,150,216,171,142,3,247,3,1,115,161,150,216,171,142,3,137,4,1,161,150,216,171,142,3,138,4,1,161,150,216,171,142,3,139,4,1,196,150,216,171,142,3,140,4,150,216,171,142,3,247,3,1,111,161,150,216,171,142,3,141,4,1,161,150,216,171,142,3,142,4,1,161,150,216,171,142,3,143,4,1,196,150,216,171,142,3,144,4,150,216,171,142,3,247,3,1,108,161,150,216,171,142,3,145,4,1,161,150,216,171,142,3,146,4,1,161,150,216,171,142,3,147,4,1,196,150,216,171,142,3,148,4,150,216,171,142,3,247,3,1,101,161,150,216,171,142,3,149,4,1,161,150,216,171,142,3,150,4,1,161,150,216,171,142,3,151,4,1,196,150,216,171,142,3,152,4,150,216,171,142,3,247,3,1,46,161,150,216,171,142,3,153,4,1,161,150,216,171,142,3,154,4,1,161,150,216,171,142,3,155,4,1,196,150,216,171,142,3,156,4,150,216,171,142,3,247,3,1,108,161,150,216,171,142,3,157,4,1,161,150,216,171,142,3,158,4,1,161,150,216,171,142,3,159,4,1,196,150,216,171,142,3,160,4,150,216,171,142,3,247,3,1,111,161,150,216,171,142,3,161,4,1,161,150,216,171,142,3,162,4,1,161,150,216,171,142,3,163,4,1,196,150,216,171,142,3,164,4,150,216,171,142,3,247,3,1,103,161,150,216,171,142,3,165,4,1,161,150,216,171,142,3,166,4,1,161,150,216,171,142,3,167,4,1,196,150,216,171,142,3,168,4,150,216,171,142,3,247,3,1,40,161,150,216,171,142,3,169,4,1,161,150,216,171,142,3,170,4,1,161,150,216,171,142,3,171,4,1,196,150,216,171,142,3,172,4,150,216,171,142,3,247,3,1,41,161,150,216,171,142,3,173,4,1,161,150,216,171,142,3,174,4,1,161,150,216,171,142,3,175,4,1,196,150,216,171,142,3,172,4,150,216,171,142,3,176,4,1,34,161,150,216,171,142,3,177,4,1,161,150,216,171,142,3,178,4,1,161,150,216,171,142,3,179,4,1,196,150,216,171,142,3,180,4,150,216,171,142,3,176,4,1,34,161,150,216,171,142,3,181,4,1,161,150,216,171,142,3,182,4,1,161,150,216,171,142,3,183,4,1,196,150,216,171,142,3,180,4,150,216,171,142,3,184,4,1,72,161,150,216,171,142,3,185,4,1,161,150,216,171,142,3,186,4,1,161,150,216,171,142,3,187,4,1,196,150,216,171,142,3,188,4,150,216,171,142,3,184,4,1,101,161,150,216,171,142,3,189,4,1,161,150,216,171,142,3,190,4,1,161,150,216,171,142,3,191,4,1,196,150,216,171,142,3,192,4,150,216,171,142,3,184,4,1,108,161,150,216,171,142,3,193,4,1,161,150,216,171,142,3,194,4,1,161,150,216,171,142,3,195,4,1,196,150,216,171,142,3,196,4,150,216,171,142,3,184,4,1,108,161,150,216,171,142,3,197,4,1,161,150,216,171,142,3,198,4,1,161,150,216,171,142,3,199,4,1,196,150,216,171,142,3,200,4,150,216,171,142,3,184,4,1,111,161,150,216,171,142,3,201,4,1,161,150,216,171,142,3,202,4,1,161,150,216,171,142,3,203,4,1,196,150,216,171,142,3,204,4,150,216,171,142,3,184,4,1,32,161,150,216,171,142,3,205,4,1,161,150,216,171,142,3,206,4,1,161,150,216,171,142,3,207,4,1,196,150,216,171,142,3,208,4,150,216,171,142,3,184,4,1,87,161,150,216,171,142,3,209,4,1,161,150,216,171,142,3,210,4,1,161,150,216,171,142,3,211,4,1,196,150,216,171,142,3,212,4,150,216,171,142,3,184,4,1,111,161,150,216,171,142,3,213,4,1,161,150,216,171,142,3,214,4,1,161,150,216,171,142,3,215,4,1,196,150,216,171,142,3,216,4,150,216,171,142,3,184,4,1,114,161,150,216,171,142,3,217,4,1,161,150,216,171,142,3,218,4,1,161,150,216,171,142,3,219,4,1,196,150,216,171,142,3,220,4,150,216,171,142,3,184,4,1,108,161,150,216,171,142,3,221,4,1,161,150,216,171,142,3,222,4,1,161,150,216,171,142,3,223,4,1,196,150,216,171,142,3,224,4,150,216,171,142,3,184,4,1,100,161,150,216,171,142,3,225,4,1,161,150,216,171,142,3,226,4,1,161,150,216,171,142,3,227,4,1,193,150,216,171,142,3,228,4,150,216,171,142,3,184,4,1,161,150,216,171,142,3,229,4,1,161,150,216,171,142,3,230,4,1,161,150,216,171,142,3,231,4,1,168,150,216,171,142,3,233,4,1,119,132,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,47,47,32,84,104,105,115,32,105,115,32,116,104,101,32,109,97,105,110,32,102,117,110,99,116,105,111,110,46,92,110,102,117,110,99,116,105,111,110,32,109,97,105,110,40,41,32,123,92,110,32,32,99,111,110,115,111,108,101,46,108,111,103,40,92,34,72,101,108,108,111,32,87,111,114,108,100,92,34,41,92,110,125,34,125,93,44,34,108,97,110,103,117,97,103,101,34,58,34,74,97,118,97,115,99,114,105,112,116,34,125,168,150,216,171,142,3,234,4,1,119,10,49,112,115,100,67,122,97,87,104,49,168,150,216,171,142,3,235,4,1,119,4,116,101,120,116,39,0,204,195,206,156,1,4,6,48,100,51,52,82,50,2,39,0,204,195,206,156,1,4,6,45,71,115,81,49,95,2,4,0,150,216,171,142,3,240,4,4,49,50,51,32,134,150,216,171,142,3,244,4,7,109,101,110,116,105,111,110,51,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,57,84,49,54,58,49,51,58,52,57,46,52,49,49,49,54,53,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,132,150,216,171,142,3,245,4,1,36,134,150,216,171,142,3,246,4,7,109,101,110,116,105,111,110,4,110,117,108,108,132,150,216,171,142,3,247,4,6,32,32,101,114,32,32,33,0,204,195,206,156,1,1,6,49,71,114,87,76,99,1,0,7,33,0,204,195,206,156,1,3,6,72,105,104,101,52,114,1,193,164,202,219,213,10,28,199,130,209,189,2,60,1,168,164,202,219,213,10,117,1,119,151,1,123,34,108,101,118,101,108,34,58,50,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,109,101,110,116,105,111,110,34,58,123,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,57,84,49,54,58,49,51,58,52,57,46,52,49,49,49,54,53,34,44,34,116,121,112,101,34,58,34,100,97,116,101,34,125,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,32,101,114,32,32,34,125,93,125,4,0,150,216,171,142,3,239,4,1,35,0,1,132,150,216,171,142,3,137,5,1,35,0,1,132,150,216,171,142,3,139,5,1,35,0,1,39,0,204,195,206,156,1,4,6,115,99,68,45,119,114,2,39,0,204,195,206,156,1,1,6,67,72,115,77,79,98,1,40,0,150,216,171,142,3,144,5,2,105,100,1,119,6,67,72,115,77,79,98,40,0,150,216,171,142,3,144,5,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,150,216,171,142,3,144,5,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,150,216,171,142,3,144,5,8,99,104,105,108,100,114,101,110,1,119,6,97,107,121,80,104,45,33,0,150,216,171,142,3,144,5,4,100,97,116,97,1,40,0,150,216,171,142,3,144,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,144,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,97,107,121,80,104,45,0,200,164,202,219,213,10,28,150,216,171,142,3,135,5,1,119,6,67,72,115,77,79,98,4,0,150,216,171,142,3,143,5,1,49,161,150,216,171,142,3,149,5,1,132,150,216,171,142,3,154,5,1,50,161,150,216,171,142,3,155,5,1,132,150,216,171,142,3,156,5,1,51,161,150,216,171,142,3,157,5,1,168,150,216,171,142,3,5,1,119,11,123,34,100,101,112,116,104,34,58,54,125,39,0,204,195,206,156,1,4,6,112,79,69,118,75,110,2,39,0,204,195,206,156,1,4,6,80,49,49,121,116,108,2,4,0,150,216,171,142,3,162,5,3,49,50,51,33,0,204,195,206,156,1,1,6,97,79,72,54,79,66,1,0,7,33,0,204,195,206,156,1,3,6,105,89,77,80,45,116,1,193,150,216,171,142,3,135,5,199,130,209,189,2,60,1,161,150,216,171,142,3,159,5,1,168,150,216,171,142,3,176,5,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,44,34,108,101,118,101,108,34,58,51,125,161,199,130,209,189,2,212,3,1,161,199,130,209,189,2,232,3,1,161,199,130,209,189,2,159,4,1,161,150,216,171,142,3,179,5,1,161,199,130,209,189,2,144,4,1,161,150,216,171,142,3,181,5,1,161,150,216,171,142,3,182,5,1,161,150,216,171,142,3,180,5,2,161,150,216,171,142,3,183,5,1,161,150,216,171,142,3,184,5,1,161,150,216,171,142,3,186,5,1,161,199,130,209,189,2,252,3,1,161,150,216,171,142,3,188,5,1,161,150,216,171,142,3,189,5,1,39,0,204,195,206,156,1,4,6,70,76,85,90,75,54,2,39,0,204,195,206,156,1,4,6,69,53,84,66,120,118,2,39,0,204,195,206,156,1,1,6,103,79,106,51,90,68,1,40,0,150,216,171,142,3,195,5,2,105,100,1,119,6,103,79,106,51,90,68,40,0,150,216,171,142,3,195,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,150,216,171,142,3,195,5,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,150,216,171,142,3,195,5,8,99,104,105,108,100,114,101,110,1,119,6,116,51,112,87,101,56,33,0,150,216,171,142,3,195,5,4,100,97,116,97,1,40,0,150,216,171,142,3,195,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,195,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,116,51,112,87,101,56,0,136,199,130,209,189,2,148,4,1,119,6,103,79,106,51,90,68,39,0,204,195,206,156,1,1,6,88,56,80,113,67,120,1,40,0,150,216,171,142,3,205,5,2,105,100,1,119,6,88,56,80,113,67,120,40,0,150,216,171,142,3,205,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,150,216,171,142,3,205,5,6,112,97,114,101,110,116,1,119,6,103,79,106,51,90,68,40,0,150,216,171,142,3,205,5,8,99,104,105,108,100,114,101,110,1,119,6,76,55,82,84,78,106,33,0,150,216,171,142,3,205,5,4,100,97,116,97,1,40,0,150,216,171,142,3,205,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,205,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,76,55,82,84,78,106,0,8,0,150,216,171,142,3,203,5,1,119,6,88,56,80,113,67,120,39,0,204,195,206,156,1,1,6,74,48,69,48,67,66,1,40,0,150,216,171,142,3,215,5,2,105,100,1,119,6,74,48,69,48,67,66,40,0,150,216,171,142,3,215,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,150,216,171,142,3,215,5,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,150,216,171,142,3,215,5,8,99,104,105,108,100,114,101,110,1,119,6,73,120,120,49,82,88,33,0,150,216,171,142,3,215,5,4,100,97,116,97,1,40,0,150,216,171,142,3,215,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,215,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,73,120,120,49,82,88,0,136,150,216,171,142,3,204,5,1,119,6,74,48,69,48,67,66,39,0,204,195,206,156,1,1,6,75,78,45,115,87,88,1,40,0,150,216,171,142,3,225,5,2,105,100,1,119,6,75,78,45,115,87,88,40,0,150,216,171,142,3,225,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,150,216,171,142,3,225,5,6,112,97,114,101,110,116,1,119,6,74,48,69,48,67,66,40,0,150,216,171,142,3,225,5,8,99,104,105,108,100,114,101,110,1,119,6,75,115,100,83,116,74,33,0,150,216,171,142,3,225,5,4,100,97,116,97,1,40,0,150,216,171,142,3,225,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,225,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,75,115,100,83,116,74,0,8,0,150,216,171,142,3,223,5,1,119,6,75,78,45,115,87,88,161,150,216,171,142,3,192,5,1,4,0,150,216,171,142,3,193,5,1,55,161,150,216,171,142,3,210,5,1,132,150,216,171,142,3,236,5,1,55,161,150,216,171,142,3,237,5,1,132,150,216,171,142,3,238,5,1,55,161,150,216,171,142,3,239,5,1,39,0,204,195,206,156,1,4,6,71,52,107,110,49,95,2,39,0,204,195,206,156,1,4,6,98,52,97,80,80,53,2,39,0,204,195,206,156,1,4,6,80,111,114,82,81,79,2,161,150,216,171,142,3,235,5,1,39,0,204,195,206,156,1,1,6,80,53,88,89,98,115,1,40,0,150,216,171,142,3,246,5,2,105,100,1,119,6,80,53,88,89,98,115,40,0,150,216,171,142,3,246,5,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,150,216,171,142,3,246,5,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,150,216,171,142,3,246,5,8,99,104,105,108,100,114,101,110,1,119,6,89,55,81,88,109,84,33,0,150,216,171,142,3,246,5,4,100,97,116,97,1,40,0,150,216,171,142,3,246,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,246,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,89,55,81,88,109,84,0,200,199,130,209,189,2,236,3,199,130,209,189,2,128,4,1,119,6,80,53,88,89,98,115,39,0,204,195,206,156,1,1,6,57,49,85,122,51,51,1,40,0,150,216,171,142,3,128,6,2,105,100,1,119,6,57,49,85,122,51,51,40,0,150,216,171,142,3,128,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,150,216,171,142,3,128,6,6,112,97,114,101,110,116,1,119,6,80,53,88,89,98,115,40,0,150,216,171,142,3,128,6,8,99,104,105,108,100,114,101,110,1,119,6,88,73,86,101,114,105,33,0,150,216,171,142,3,128,6,4,100,97,116,97,1,40,0,150,216,171,142,3,128,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,128,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,88,73,86,101,114,105,0,8,0,150,216,171,142,3,254,5,1,119,6,57,49,85,122,51,51,161,150,216,171,142,3,245,5,1,39,0,204,195,206,156,1,1,6,97,105,73,55,114,78,1,40,0,150,216,171,142,3,139,6,2,105,100,1,119,6,97,105,73,55,114,78,40,0,150,216,171,142,3,139,6,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,150,216,171,142,3,139,6,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,150,216,171,142,3,139,6,8,99,104,105,108,100,114,101,110,1,119,6,70,83,77,57,101,119,33,0,150,216,171,142,3,139,6,4,100,97,116,97,1,40,0,150,216,171,142,3,139,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,139,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,70,83,77,57,101,119,0,200,199,130,209,189,2,148,4,150,216,171,142,3,204,5,1,119,6,97,105,73,55,114,78,39,0,204,195,206,156,1,1,6,48,117,114,80,56,120,1,40,0,150,216,171,142,3,149,6,2,105,100,1,119,6,48,117,114,80,56,120,40,0,150,216,171,142,3,149,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,150,216,171,142,3,149,6,6,112,97,114,101,110,116,1,119,6,97,105,73,55,114,78,40,0,150,216,171,142,3,149,6,8,99,104,105,108,100,114,101,110,1,119,6,98,90,114,57,106,101,33,0,150,216,171,142,3,149,6,4,100,97,116,97,1,40,0,150,216,171,142,3,149,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,149,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,98,90,114,57,106,101,0,8,0,150,216,171,142,3,147,6,1,119,6,48,117,114,80,56,120,39,0,204,195,206,156,1,1,6,100,114,110,97,115,68,1,40,0,150,216,171,142,3,159,6,2,105,100,1,119,6,100,114,110,97,115,68,40,0,150,216,171,142,3,159,6,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,150,216,171,142,3,159,6,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,150,216,171,142,3,159,6,8,99,104,105,108,100,114,101,110,1,119,6,76,114,45,49,81,54,33,0,150,216,171,142,3,159,6,4,100,97,116,97,1,40,0,150,216,171,142,3,159,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,159,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,76,114,45,49,81,54,0,136,150,216,171,142,3,224,5,1,119,6,100,114,110,97,115,68,39,0,204,195,206,156,1,1,6,72,69,79,54,86,72,1,40,0,150,216,171,142,3,169,6,2,105,100,1,119,6,72,69,79,54,86,72,40,0,150,216,171,142,3,169,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,150,216,171,142,3,169,6,6,112,97,114,101,110,116,1,119,6,100,114,110,97,115,68,40,0,150,216,171,142,3,169,6,8,99,104,105,108,100,114,101,110,1,119,6,71,118,57,87,108,76,33,0,150,216,171,142,3,169,6,4,100,97,116,97,1,40,0,150,216,171,142,3,169,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,150,216,171,142,3,169,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,71,118,57,87,108,76,0,8,0,150,216,171,142,3,167,6,1,119,6,72,69,79,54,86,72,4,0,150,216,171,142,3,242,5,1,49,161,150,216,171,142,3,133,6,1,132,150,216,171,142,3,179,6,1,48,161,150,216,171,142,3,180,6,1,132,150,216,171,142,3,181,6,1,49,161,150,216,171,142,3,182,6,1,132,150,216,171,142,3,183,6,1,48,161,150,216,171,142,3,184,6,1,161,150,216,171,142,3,187,5,1,161,150,216,171,142,3,191,5,1,161,150,216,171,142,3,220,5,1,161,150,216,171,142,3,138,6,1,39,0,204,195,206,156,1,4,6,54,73,68,55,103,118,2,4,0,150,216,171,142,3,191,6,1,49,168,199,130,209,189,2,169,4,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,34,125,93,125,39,0,204,195,206,156,1,4,6,77,84,68,68,110,107,2,4,0,150,216,171,142,3,194,6,1,50,168,199,130,209,189,2,242,3,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,50,34,125,93,125,39,0,204,195,206,156,1,4,6,88,108,87,102,45,70,2,4,0,150,216,171,142,3,197,6,1,51,168,150,216,171,142,3,186,6,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,51,34,125,93,125,39,0,204,195,206,156,1,4,6,57,98,65,107,106,109,2,4,0,150,216,171,142,3,200,6,1,52,168,199,130,209,189,2,134,4,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,34,125,93,125,39,0,204,195,206,156,1,4,6,99,112,85,122,107,121,2,4,0,150,216,171,142,3,203,6,1,53,168,199,130,209,189,2,177,4,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,53,34,125,93,125,39,0,204,195,206,156,1,4,6,81,102,72,107,77,77,2,4,0,150,216,171,142,3,206,6,1,54,168,150,216,171,142,3,154,6,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,54,34,125,93,125,39,0,204,195,206,156,1,4,6,119,113,82,74,112,76,2,4,0,150,216,171,142,3,209,6,1,55,168,150,216,171,142,3,241,5,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,55,34,125,93,125,39,0,204,195,206,156,1,4,6,69,70,82,71,121,80,2,4,0,150,216,171,142,3,212,6,1,56,168,150,216,171,142,3,230,5,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,56,34,125,93,125,39,0,204,195,206,156,1,4,6,104,109,90,99,72,101,2,1,0,150,216,171,142,3,215,6,1,161,150,216,171,142,3,174,6,2,132,150,216,171,142,3,216,6,1,57,168,150,216,171,142,3,218,6,1,119,26,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,57,34,125,93,125,39,0,204,195,206,156,1,4,6,56,117,111,87,49,119,2,4,0,150,216,171,142,3,221,6,4,119,105,116,104,168,199,130,209,189,2,146,3,1,119,61,123,34,97,108,105,103,110,34,58,34,114,105,103,104,116,34,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,119,105,116,104,34,125,93,125,39,0,204,195,206,156,1,4,6,109,55,80,110,81,54,2,4,0,150,216,171,142,3,227,6,3,108,111,110,129,150,216,171,142,3,230,6,14,132,150,216,171,142,3,244,6,44,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,134,150,216,171,142,3,160,7,11,102,111,110,116,95,102,97,109,105,108,121,22,34,65,68,76,97,77,68,105,115,112,108,97,121,95,114,101,103,117,108,97,114,34,132,150,216,171,142,3,161,7,9,116,101,120,116,110,103,32,116,101,134,150,216,171,142,3,170,7,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,132,150,216,171,142,3,171,7,16,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,129,150,216,171,142,3,187,7,6,132,150,216,171,142,3,193,7,92,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,161,199,130,209,189,2,213,2,1,196,150,216,171,142,3,187,7,150,216,171,142,3,188,7,1,76,161,150,216,171,142,3,158,8,1,196,150,216,171,142,3,193,7,150,216,171,142,3,194,7,1,101,161,150,216,171,142,3,160,8,1,70,204,195,206,156,1,135,7,11,102,111,110,116,95,102,97,109,105,108,121,22,34,65,98,114,105,108,70,97,116,102,97,99,101,95,114,101,103,117,108,97,114,34,193,150,216,171,142,3,163,8,204,195,206,156,1,135,7,3,198,150,216,171,142,3,166,8,204,195,206,156,1,135,7,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,161,199,130,209,189,2,25,1,161,199,130,209,189,2,26,1,161,199,130,209,189,2,27,1,198,150,216,171,142,3,230,6,150,216,171,142,3,231,6,11,102,111,110,116,95,102,97,109,105,108,121,22,34,65,68,76,97,77,68,105,115,112,108,97,121,95,114,101,103,117,108,97,114,34,196,150,216,171,142,3,171,8,150,216,171,142,3,231,6,14,103,32,116,101,120,116,110,103,32,116,101,120,116,110,198,150,216,171,142,3,185,8,150,216,171,142,3,231,6,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,161,150,216,171,142,3,162,8,1,16,146,175,139,236,2,0,161,227,211,144,195,8,0,1,161,227,211,144,195,8,1,1,161,227,211,144,195,8,2,1,161,227,211,144,195,8,3,1,161,227,211,144,195,8,4,1,161,227,211,144,195,8,5,1,161,227,211,144,195,8,11,1,161,227,211,144,195,8,7,1,161,227,211,144,195,8,8,1,161,227,211,144,195,8,9,1,161,146,175,139,236,2,6,2,39,0,204,195,206,156,1,4,6,66,66,65,103,65,56,2,6,0,146,175,139,236,2,12,11,102,111,110,116,95,102,97,109,105,108,121,15,34,65,68,76,97,77,32,68,105,115,112,108,97,121,34,132,146,175,139,236,2,13,183,1,108,111,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,76,101,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,107,107,109,134,146,175,139,236,2,196,1,11,102,111,110,116,95,102,97,109,105,108,121,4,110,117,108,108,168,192,187,174,206,8,222,5,1,119,141,2,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,102,97,109,105,108,121,34,58,34,65,68,76,97,77,32,68,105,115,112,108,97,121,34,125,44,34,105,110,115,101,114,116,34,58,34,108,111,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,76,101,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,107,107,109,34,125,93,125,20,168,215,223,235,2,0,161,150,216,171,142,3,168,8,1,161,150,216,171,142,3,169,8,1,161,150,216,171,142,3,170,8,1,196,150,216,171,142,3,166,8,150,216,171,142,3,167,8,3,87,101,108,132,224,159,166,178,15,26,16,99,111,109,101,32,116,111,32,65,112,112,70,108,111,119,121,39,0,204,195,206,156,1,4,6,74,98,104,77,53,50,2,4,0,168,215,223,235,2,22,20,72,101,114,101,32,97,114,101,32,116,104,101,32,98,97,115,105,99,115,32,168,168,215,223,235,2,0,1,119,120,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,102,97,109,105,108,121,34,58,34,65,98,114,105,108,70,97,116,102,97,99,101,95,114,101,103,117,108,97,114,34,125,44,34,105,110,115,101,114,116,34,58,34,87,101,108,34,125,44,123,34,105,110,115,101,114,116,34,58,34,99,111,109,101,32,116,111,32,65,112,112,70,108,111,119,121,34,125,93,44,34,108,101,118,101,108,34,58,49,125,168,168,215,223,235,2,1,1,119,10,119,88,107,79,72,81,49,50,99,111,168,168,215,223,235,2,2,1,119,4,116,101,120,116,167,204,195,206,156,1,213,1,1,40,0,168,215,223,235,2,46,2,105,100,1,119,10,97,115,74,118,54,70,114,65,82,97,40,0,168,215,223,235,2,46,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,168,215,223,235,2,46,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,168,215,223,235,2,46,8,99,104,105,108,100,114,101,110,1,119,6,51,107,67,87,106,70,40,0,168,215,223,235,2,46,4,100,97,116,97,1,119,55,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,72,101,114,101,32,97,114,101,32,116,104,101,32,98,97,115,105,99,115,32,34,125,93,44,34,108,101,118,101,108,34,58,50,125,40,0,168,215,223,235,2,46,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,168,215,223,235,2,46,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,51,107,67,87,106,70,0,200,204,195,206,156,1,232,1,199,130,209,189,2,181,3,1,119,10,97,115,74,118,54,70,114,65,82,97,1,192,246,139,213,2,0,161,239,239,208,251,10,16,35,1,190,183,139,210,2,0,161,241,147,239,232,6,3,110,2,237,140,187,206,2,0,161,190,183,139,210,2,109,17,161,237,140,187,206,2,16,4,137,6,199,130,209,189,2,0,39,0,204,195,206,156,1,4,6,88,116,53,112,118,55,2,4,0,199,130,209,189,2,0,9,229,144,140,228,184,128,228,184,170,129,199,130,209,189,2,3,5,161,178,187,245,161,14,10,6,132,199,130,209,189,2,8,1,110,161,199,130,209,189,2,14,1,132,199,130,209,189,2,15,1,105,168,199,130,209,189,2,16,1,119,36,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,144,140,228,184,128,228,184,170,110,105,34,125,93,125,161,224,159,166,178,15,27,1,161,224,159,166,178,15,28,1,161,224,159,166,178,15,29,1,161,199,130,209,189,2,19,1,161,199,130,209,189,2,20,1,161,199,130,209,189,2,21,1,161,199,130,209,189,2,22,1,161,199,130,209,189,2,23,1,161,199,130,209,189,2,24,1,0,3,39,0,204,195,206,156,1,4,6,82,74,97,73,54,107,2,4,0,199,130,209,189,2,31,1,116,161,228,242,134,215,15,11,1,132,199,130,209,189,2,32,1,111,161,199,130,209,189,2,33,1,132,199,130,209,189,2,34,1,100,161,199,130,209,189,2,35,1,132,199,130,209,189,2,36,1,111,161,199,130,209,189,2,37,1,132,199,130,209,189,2,38,1,32,161,199,130,209,189,2,39,1,132,199,130,209,189,2,40,1,108,161,199,130,209,189,2,41,1,132,199,130,209,189,2,42,1,105,161,199,130,209,189,2,43,1,132,199,130,209,189,2,44,1,115,161,199,130,209,189,2,45,1,132,199,130,209,189,2,46,1,116,161,199,130,209,189,2,47,1,39,0,204,195,206,156,1,4,6,55,80,118,106,121,81,2,39,0,204,195,206,156,1,1,6,71,118,88,50,102,110,1,40,0,199,130,209,189,2,51,2,105,100,1,119,6,71,118,88,50,102,110,40,0,199,130,209,189,2,51,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,199,130,209,189,2,51,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,51,8,99,104,105,108,100,114,101,110,1,119,6,111,112,68,102,54,95,33,0,199,130,209,189,2,51,4,100,97,116,97,1,40,0,199,130,209,189,2,51,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,51,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,111,112,68,102,54,95,0,200,198,223,206,159,1,135,1,198,223,206,159,1,116,1,119,6,71,118,88,50,102,110,161,199,130,209,189,2,49,1,4,0,199,130,209,189,2,50,1,99,161,199,130,209,189,2,56,1,132,199,130,209,189,2,62,1,104,161,199,130,209,189,2,63,1,132,199,130,209,189,2,64,1,101,161,199,130,209,189,2,65,1,132,199,130,209,189,2,66,1,99,161,199,130,209,189,2,67,1,132,199,130,209,189,2,68,1,107,161,199,130,209,189,2,69,1,132,199,130,209,189,2,70,1,101,161,199,130,209,189,2,71,1,132,199,130,209,189,2,72,1,100,161,199,130,209,189,2,73,1,132,199,130,209,189,2,74,1,32,161,199,130,209,189,2,75,1,132,199,130,209,189,2,76,1,116,161,199,130,209,189,2,77,1,132,199,130,209,189,2,78,1,111,161,199,130,209,189,2,79,1,132,199,130,209,189,2,80,1,100,161,199,130,209,189,2,81,1,132,199,130,209,189,2,82,1,111,161,199,130,209,189,2,83,1,132,199,130,209,189,2,84,1,32,161,199,130,209,189,2,85,1,132,199,130,209,189,2,86,1,108,161,199,130,209,189,2,87,1,132,199,130,209,189,2,88,1,105,161,199,130,209,189,2,89,1,132,199,130,209,189,2,90,1,115,161,199,130,209,189,2,91,1,132,199,130,209,189,2,92,1,116,161,199,130,209,189,2,93,1,39,0,204,195,206,156,1,4,6,117,115,117,45,118,111,2,39,0,204,195,206,156,1,1,6,86,90,80,95,77,113,1,40,0,199,130,209,189,2,97,2,105,100,1,119,6,86,90,80,95,77,113,40,0,199,130,209,189,2,97,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,199,130,209,189,2,97,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,97,8,99,104,105,108,100,114,101,110,1,119,6,54,55,76,56,102,50,33,0,199,130,209,189,2,97,4,100,97,116,97,1,40,0,199,130,209,189,2,97,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,97,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,54,55,76,56,102,50,0,200,199,130,209,189,2,60,198,223,206,159,1,116,1,119,6,86,90,80,95,77,113,161,199,130,209,189,2,95,1,4,0,199,130,209,189,2,96,1,108,161,199,130,209,189,2,102,1,132,199,130,209,189,2,108,1,111,161,199,130,209,189,2,109,1,132,199,130,209,189,2,110,1,110,161,199,130,209,189,2,111,1,132,199,130,209,189,2,112,1,103,161,199,130,209,189,2,113,1,132,199,130,209,189,2,114,1,32,161,199,130,209,189,2,115,1,132,199,130,209,189,2,116,1,116,161,199,130,209,189,2,117,1,132,199,130,209,189,2,118,1,101,161,199,130,209,189,2,119,1,132,199,130,209,189,2,120,1,120,161,199,130,209,189,2,121,1,132,199,130,209,189,2,122,1,116,161,199,130,209,189,2,123,1,129,199,130,209,189,2,124,1,161,199,130,209,189,2,125,2,132,199,130,209,189,2,126,7,110,103,32,116,101,120,116,161,199,130,209,189,2,128,1,1,132,199,130,209,189,2,135,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,136,1,1,132,199,130,209,189,2,143,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,144,1,1,132,199,130,209,189,2,151,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,152,1,1,132,199,130,209,189,2,159,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,160,1,1,132,199,130,209,189,2,167,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,168,1,1,132,199,130,209,189,2,175,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,176,1,1,132,199,130,209,189,2,183,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,184,1,1,132,199,130,209,189,2,191,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,192,1,1,132,199,130,209,189,2,199,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,200,1,1,132,199,130,209,189,2,207,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,208,1,1,132,199,130,209,189,2,215,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,216,1,1,132,199,130,209,189,2,223,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,224,1,1,132,199,130,209,189,2,231,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,232,1,1,132,199,130,209,189,2,239,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,240,1,1,132,199,130,209,189,2,247,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,248,1,1,132,199,130,209,189,2,255,1,7,110,103,32,116,101,120,116,161,199,130,209,189,2,128,2,1,132,199,130,209,189,2,135,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,136,2,1,132,199,130,209,189,2,143,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,144,2,1,132,199,130,209,189,2,151,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,152,2,1,132,199,130,209,189,2,159,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,160,2,1,132,199,130,209,189,2,167,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,168,2,1,132,199,130,209,189,2,175,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,176,2,1,132,199,130,209,189,2,183,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,184,2,1,132,199,130,209,189,2,191,2,7,110,103,32,116,101,120,116,161,199,130,209,189,2,192,2,1,161,199,130,209,189,2,107,1,39,0,204,195,206,156,1,4,6,55,74,97,87,111,56,2,39,0,204,195,206,156,1,1,6,54,87,56,99,101,88,1,40,0,199,130,209,189,2,203,2,2,105,100,1,119,6,54,87,56,99,101,88,40,0,199,130,209,189,2,203,2,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,199,130,209,189,2,203,2,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,203,2,8,99,104,105,108,100,114,101,110,1,119,6,105,55,111,99,51,56,33,0,199,130,209,189,2,203,2,4,100,97,116,97,1,40,0,199,130,209,189,2,203,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,203,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,105,55,111,99,51,56,0,200,199,130,209,189,2,106,198,223,206,159,1,116,1,119,6,54,87,56,99,101,88,161,199,130,209,189,2,200,2,1,132,199,130,209,189,2,94,1,32,161,199,130,209,189,2,201,2,1,129,199,130,209,189,2,214,2,1,161,199,130,209,189,2,215,2,1,129,199,130,209,189,2,216,2,1,161,199,130,209,189,2,217,2,1,129,199,130,209,189,2,218,2,1,161,199,130,209,189,2,219,2,1,129,199,130,209,189,2,220,2,1,161,199,130,209,189,2,221,2,5,129,199,130,209,189,2,222,2,1,161,199,130,209,189,2,227,2,1,129,199,130,209,189,2,228,2,1,161,199,130,209,189,2,229,2,1,129,199,130,209,189,2,230,2,1,161,199,130,209,189,2,231,2,1,129,199,130,209,189,2,232,2,1,161,199,130,209,189,2,233,2,1,129,199,130,209,189,2,234,2,1,161,199,130,209,189,2,235,2,1,129,199,130,209,189,2,236,2,1,161,199,130,209,189,2,237,2,1,129,199,130,209,189,2,238,2,1,161,199,130,209,189,2,239,2,1,134,199,130,209,189,2,240,2,7,102,111,114,109,117,108,97,9,34,102,111,114,109,117,108,97,34,132,199,130,209,189,2,242,2,1,36,134,199,130,209,189,2,243,2,7,102,111,114,109,117,108,97,4,110,117,108,108,168,199,130,209,189,2,241,2,1,119,108,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,101,99,107,101,100,32,116,111,100,111,32,108,105,115,116,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,114,109,117,108,97,34,58,34,102,111,114,109,117,108,97,34,125,44,34,105,110,115,101,114,116,34,58,34,36,34,125,93,44,34,99,104,101,99,107,101,100,34,58,116,114,117,101,125,1,0,199,130,209,189,2,202,2,1,161,199,130,209,189,2,208,2,1,129,199,130,209,189,2,246,2,1,161,199,130,209,189,2,247,2,1,129,199,130,209,189,2,248,2,1,161,199,130,209,189,2,249,2,1,129,199,130,209,189,2,250,2,1,161,199,130,209,189,2,251,2,1,129,199,130,209,189,2,252,2,1,161,199,130,209,189,2,253,2,1,129,199,130,209,189,2,254,2,1,161,199,130,209,189,2,255,2,1,129,199,130,209,189,2,128,3,1,161,199,130,209,189,2,129,3,8,132,199,130,209,189,2,130,3,1,119,161,199,130,209,189,2,138,3,1,132,199,130,209,189,2,139,3,1,105,161,199,130,209,189,2,140,3,1,132,199,130,209,189,2,141,3,1,116,161,199,130,209,189,2,142,3,1,132,199,130,209,189,2,143,3,1,104,161,199,130,209,189,2,144,3,1,39,0,204,195,206,156,1,4,6,55,99,74,88,114,112,2,39,0,204,195,206,156,1,1,6,86,111,54,70,109,81,1,40,0,199,130,209,189,2,148,3,2,105,100,1,119,6,86,111,54,70,109,81,40,0,199,130,209,189,2,148,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,148,3,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,199,130,209,189,2,148,3,8,99,104,105,108,100,114,101,110,1,119,6,106,82,78,118,55,111,33,0,199,130,209,189,2,148,3,4,100,97,116,97,1,40,0,199,130,209,189,2,148,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,148,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,106,82,78,118,55,111,0,136,171,236,222,251,5,206,3,1,119,6,86,111,54,70,109,81,4,0,199,130,209,189,2,147,3,4,240,159,152,131,168,199,130,209,189,2,153,3,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,240,159,152,131,34,125,93,125,39,0,204,195,206,156,1,4,6,103,89,121,119,78,121,2,33,0,204,195,206,156,1,1,6,84,67,99,110,98,71,1,0,7,33,0,204,195,206,156,1,3,6,82,117,68,55,67,100,1,193,204,195,206,156,1,232,1,198,223,206,159,1,149,1,1,39,0,204,195,206,156,1,1,6,101,77,66,121,99,80,1,40,0,199,130,209,189,2,172,3,2,105,100,1,119,6,101,77,66,121,99,80,40,0,199,130,209,189,2,172,3,2,116,121,1,119,7,111,117,116,108,105,110,101,40,0,199,130,209,189,2,172,3,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,172,3,8,99,104,105,108,100,114,101,110,1,119,6,112,72,116,98,67,52,33,0,199,130,209,189,2,172,3,4,100,97,116,97,1,40,0,199,130,209,189,2,172,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,172,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,112,72,116,98,67,52,0,200,204,195,206,156,1,232,1,199,130,209,189,2,171,3,1,119,6,101,77,66,121,99,80,39,0,204,195,206,156,1,4,6,95,97,88,90,88,80,2,33,0,204,195,206,156,1,1,6,81,89,119,70,83,48,1,0,7,33,0,204,195,206,156,1,3,6,88,110,80,104,117,89,1,193,199,130,209,189,2,171,3,198,223,206,159,1,149,1,1,39,0,204,195,206,156,1,4,6,110,90,88,77,89,67,2,39,0,204,195,206,156,1,4,6,85,99,120,53,52,69,2,39,0,204,195,206,156,1,4,6,69,82,71,102,107,88,2,39,0,204,195,206,156,1,4,6,71,108,50,57,116,102,2,39,0,204,195,206,156,1,1,6,120,49,100,100,111,87,1,40,0,199,130,209,189,2,197,3,2,105,100,1,119,6,120,49,100,100,111,87,40,0,199,130,209,189,2,197,3,2,116,121,1,119,5,116,97,98,108,101,40,0,199,130,209,189,2,197,3,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,197,3,8,99,104,105,108,100,114,101,110,1,119,6,100,49,110,86,107,119,33,0,199,130,209,189,2,197,3,4,100,97,116,97,1,40,0,199,130,209,189,2,197,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,197,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,100,49,110,86,107,119,0,200,199,130,209,189,2,171,3,199,130,209,189,2,192,3,1,119,6,120,49,100,100,111,87,39,0,204,195,206,156,1,1,6,121,57,72,73,118,95,1,40,0,199,130,209,189,2,207,3,2,105,100,1,119,6,121,57,72,73,118,95,40,0,199,130,209,189,2,207,3,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,199,130,209,189,2,207,3,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,199,130,209,189,2,207,3,8,99,104,105,108,100,114,101,110,1,119,6,78,104,69,49,119,116,33,0,199,130,209,189,2,207,3,4,100,97,116,97,1,40,0,199,130,209,189,2,207,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,207,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,78,104,69,49,119,116,0,8,0,199,130,209,189,2,205,3,1,119,6,121,57,72,73,118,95,39,0,204,195,206,156,1,1,6,48,83,82,103,66,118,1,40,0,199,130,209,189,2,217,3,2,105,100,1,119,6,48,83,82,103,66,118,40,0,199,130,209,189,2,217,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,217,3,6,112,97,114,101,110,116,1,119,6,121,57,72,73,118,95,40,0,199,130,209,189,2,217,3,8,99,104,105,108,100,114,101,110,1,119,6,107,108,100,67,117,111,33,0,199,130,209,189,2,217,3,4,100,97,116,97,1,40,0,199,130,209,189,2,217,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,217,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,107,108,100,67,117,111,0,8,0,199,130,209,189,2,215,3,1,119,6,48,83,82,103,66,118,39,0,204,195,206,156,1,1,6,95,90,90,78,53,99,1,40,0,199,130,209,189,2,227,3,2,105,100,1,119,6,95,90,90,78,53,99,40,0,199,130,209,189,2,227,3,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,199,130,209,189,2,227,3,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,199,130,209,189,2,227,3,8,99,104,105,108,100,114,101,110,1,119,6,103,89,69,98,121,107,33,0,199,130,209,189,2,227,3,4,100,97,116,97,1,40,0,199,130,209,189,2,227,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,227,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,103,89,69,98,121,107,0,136,199,130,209,189,2,216,3,1,119,6,95,90,90,78,53,99,39,0,204,195,206,156,1,1,6,77,106,74,57,74,76,1,40,0,199,130,209,189,2,237,3,2,105,100,1,119,6,77,106,74,57,74,76,40,0,199,130,209,189,2,237,3,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,237,3,6,112,97,114,101,110,116,1,119,6,95,90,90,78,53,99,40,0,199,130,209,189,2,237,3,8,99,104,105,108,100,114,101,110,1,119,6,95,102,81,84,95,110,33,0,199,130,209,189,2,237,3,4,100,97,116,97,1,40,0,199,130,209,189,2,237,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,237,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,95,102,81,84,95,110,0,8,0,199,130,209,189,2,235,3,1,119,6,77,106,74,57,74,76,39,0,204,195,206,156,1,1,6,78,77,45,104,67,70,1,40,0,199,130,209,189,2,247,3,2,105,100,1,119,6,78,77,45,104,67,70,40,0,199,130,209,189,2,247,3,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,199,130,209,189,2,247,3,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,199,130,209,189,2,247,3,8,99,104,105,108,100,114,101,110,1,119,6,117,118,68,83,80,101,33,0,199,130,209,189,2,247,3,4,100,97,116,97,1,40,0,199,130,209,189,2,247,3,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,247,3,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,117,118,68,83,80,101,0,136,199,130,209,189,2,236,3,1,119,6,78,77,45,104,67,70,39,0,204,195,206,156,1,1,6,69,70,66,45,52,82,1,40,0,199,130,209,189,2,129,4,2,105,100,1,119,6,69,70,66,45,52,82,40,0,199,130,209,189,2,129,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,129,4,6,112,97,114,101,110,116,1,119,6,78,77,45,104,67,70,40,0,199,130,209,189,2,129,4,8,99,104,105,108,100,114,101,110,1,119,6,81,81,77,50,48,66,33,0,199,130,209,189,2,129,4,4,100,97,116,97,1,40,0,199,130,209,189,2,129,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,129,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,81,81,77,50,48,66,0,8,0,199,130,209,189,2,255,3,1,119,6,69,70,66,45,52,82,39,0,204,195,206,156,1,1,6,98,100,95,105,68,101,1,40,0,199,130,209,189,2,139,4,2,105,100,1,119,6,98,100,95,105,68,101,40,0,199,130,209,189,2,139,4,2,116,121,1,119,10,116,97,98,108,101,47,99,101,108,108,40,0,199,130,209,189,2,139,4,6,112,97,114,101,110,116,1,119,6,120,49,100,100,111,87,40,0,199,130,209,189,2,139,4,8,99,104,105,108,100,114,101,110,1,119,6,105,80,89,69,52,56,33,0,199,130,209,189,2,139,4,4,100,97,116,97,1,40,0,199,130,209,189,2,139,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,139,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,105,80,89,69,52,56,0,136,199,130,209,189,2,128,4,1,119,6,98,100,95,105,68,101,39,0,204,195,206,156,1,1,6,55,51,88,69,103,80,1,40,0,199,130,209,189,2,149,4,2,105,100,1,119,6,55,51,88,69,103,80,40,0,199,130,209,189,2,149,4,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,149,4,6,112,97,114,101,110,116,1,119,6,98,100,95,105,68,101,40,0,199,130,209,189,2,149,4,8,99,104,105,108,100,114,101,110,1,119,6,115,45,80,102,105,89,33,0,199,130,209,189,2,149,4,4,100,97,116,97,1,40,0,199,130,209,189,2,149,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,149,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,115,45,80,102,105,89,0,8,0,199,130,209,189,2,147,4,1,119,6,55,51,88,69,103,80,161,199,130,209,189,2,202,3,1,4,0,199,130,209,189,2,193,3,1,56,161,199,130,209,189,2,222,3,1,132,199,130,209,189,2,160,4,1,56,161,199,130,209,189,2,161,4,1,132,199,130,209,189,2,162,4,1,56,161,199,130,209,189,2,163,4,1,132,199,130,209,189,2,164,4,1,56,161,199,130,209,189,2,165,4,1,132,199,130,209,189,2,166,4,1,56,161,199,130,209,189,2,167,4,1,4,0,199,130,209,189,2,196,3,1,57,161,199,130,209,189,2,154,4,1,132,199,130,209,189,2,170,4,1,57,161,199,130,209,189,2,171,4,1,132,199,130,209,189,2,172,4,1,57,161,199,130,209,189,2,173,4,1,132,199,130,209,189,2,174,4,1,57,161,199,130,209,189,2,175,4,1,0,4,39,0,204,195,206,156,1,4,6,56,85,53,118,100,78,2,39,0,204,195,206,156,1,1,6,78,99,104,45,81,78,1,40,0,199,130,209,189,2,183,4,2,105,100,1,119,6,78,99,104,45,81,78,40,0,199,130,209,189,2,183,4,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,199,130,209,189,2,183,4,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,183,4,8,99,104,105,108,100,114,101,110,1,119,6,122,97,90,84,55,68,33,0,199,130,209,189,2,183,4,4,100,97,116,97,1,40,0,199,130,209,189,2,183,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,183,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,122,97,90,84,55,68,0,200,204,195,206,156,1,246,1,204,195,206,156,1,247,1,1,119,6,78,99,104,45,81,78,1,0,199,130,209,189,2,182,4,1,161,199,130,209,189,2,188,4,1,129,199,130,209,189,2,193,4,1,161,199,130,209,189,2,194,4,1,129,199,130,209,189,2,195,4,1,161,199,130,209,189,2,196,4,1,39,0,204,195,206,156,1,4,6,75,102,57,98,106,87,2,33,0,204,195,206,156,1,1,6,119,73,53,75,113,116,1,0,7,33,0,204,195,206,156,1,3,6,115,76,56,78,88,117,1,193,204,195,206,156,1,247,1,204,195,206,156,1,248,1,1,39,0,204,195,206,156,1,4,6,48,72,111,66,111,70,2,39,0,204,195,206,156,1,1,6,84,67,90,121,70,52,1,40,0,199,130,209,189,2,211,4,2,105,100,1,119,6,84,67,90,121,70,52,40,0,199,130,209,189,2,211,4,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,199,130,209,189,2,211,4,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,199,130,209,189,2,211,4,8,99,104,105,108,100,114,101,110,1,119,6,108,99,89,77,103,95,33,0,199,130,209,189,2,211,4,4,100,97,116,97,1,40,0,199,130,209,189,2,211,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,211,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,108,99,89,77,103,95,0,8,0,199,130,209,189,2,191,4,1,119,6,84,67,90,121,70,52,1,0,199,130,209,189,2,210,4,1,161,199,130,209,189,2,216,4,1,129,199,130,209,189,2,221,4,1,161,199,130,209,189,2,222,4,1,129,199,130,209,189,2,223,4,1,161,199,130,209,189,2,224,4,1,39,0,204,195,206,156,1,4,6,108,73,54,101,68,85,2,33,0,204,195,206,156,1,1,6,67,118,56,72,55,83,1,0,7,33,0,204,195,206,156,1,3,6,107,119,71,100,66,65,1,129,199,130,209,189,2,220,4,1,39,0,204,195,206,156,1,4,6,57,83,80,71,121,88,2,39,0,204,195,206,156,1,1,6,52,119,120,102,90,72,1,40,0,199,130,209,189,2,239,4,2,105,100,1,119,6,52,119,120,102,90,72,40,0,199,130,209,189,2,239,4,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,199,130,209,189,2,239,4,6,112,97,114,101,110,116,1,119,6,84,67,90,121,70,52,40,0,199,130,209,189,2,239,4,8,99,104,105,108,100,114,101,110,1,119,6,118,103,105,70,69,106,33,0,199,130,209,189,2,239,4,4,100,97,116,97,1,40,0,199,130,209,189,2,239,4,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,239,4,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,118,103,105,70,69,106,0,8,0,199,130,209,189,2,219,4,1,119,6,52,119,120,102,90,72,1,0,199,130,209,189,2,238,4,1,161,199,130,209,189,2,244,4,1,129,199,130,209,189,2,249,4,1,161,199,130,209,189,2,250,4,1,129,199,130,209,189,2,251,4,1,161,199,130,209,189,2,252,4,1,39,0,204,195,206,156,1,4,6,106,81,55,52,49,100,2,39,0,204,195,206,156,1,1,6,109,102,89,53,57,121,1,40,0,199,130,209,189,2,128,5,2,105,100,1,119,6,109,102,89,53,57,121,40,0,199,130,209,189,2,128,5,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,199,130,209,189,2,128,5,6,112,97,114,101,110,116,1,119,6,84,67,90,121,70,52,40,0,199,130,209,189,2,128,5,8,99,104,105,108,100,114,101,110,1,119,6,71,116,121,76,66,108,33,0,199,130,209,189,2,128,5,4,100,97,116,97,1,40,0,199,130,209,189,2,128,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,128,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,71,116,121,76,66,108,0,136,199,130,209,189,2,248,4,1,119,6,109,102,89,53,57,121,1,0,199,130,209,189,2,255,4,1,161,199,130,209,189,2,133,5,1,129,199,130,209,189,2,138,5,1,161,199,130,209,189,2,139,5,1,129,199,130,209,189,2,140,5,1,161,199,130,209,189,2,141,5,1,39,0,204,195,206,156,1,4,6,99,82,52,54,74,83,2,33,0,204,195,206,156,1,1,6,95,95,82,90,106,107,1,0,7,33,0,204,195,206,156,1,3,6,117,113,87,99,122,50,1,129,199,130,209,189,2,137,5,1,4,0,199,130,209,189,2,144,5,1,49,0,1,132,199,130,209,189,2,155,5,1,50,0,1,132,199,130,209,189,2,157,5,1,51,0,1,39,0,204,195,206,156,1,4,6,84,108,76,116,78,119,2,1,0,199,130,209,189,2,161,5,3,39,0,204,195,206,156,1,1,6,87,57,68,108,99,56,1,40,0,199,130,209,189,2,165,5,2,105,100,1,119,6,87,57,68,108,99,56,40,0,199,130,209,189,2,165,5,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,199,130,209,189,2,165,5,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,199,130,209,189,2,165,5,8,99,104,105,108,100,114,101,110,1,119,6,71,113,89,119,74,81,33,0,199,130,209,189,2,165,5,4,100,97,116,97,1,40,0,199,130,209,189,2,165,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,165,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,71,113,89,119,74,81,0,136,199,130,209,189,2,237,4,1,119,6,87,57,68,108,99,56,129,199,130,209,189,2,164,5,1,161,199,130,209,189,2,170,5,1,129,199,130,209,189,2,175,5,1,161,199,130,209,189,2,176,5,1,129,199,130,209,189,2,177,5,1,161,199,130,209,189,2,178,5,1,39,0,204,195,206,156,1,4,6,105,45,118,52,52,66,2,33,0,204,195,206,156,1,1,6,116,72,104,110,105,69,1,0,7,33,0,204,195,206,156,1,3,6,79,69,107,76,69,106,1,129,199,130,209,189,2,174,5,1,1,0,199,130,209,189,2,181,5,1,0,1,129,199,130,209,189,2,192,5,1,0,3,132,199,130,209,189,2,194,5,1,62,0,1,39,0,204,195,206,156,1,4,6,76,115,116,55,78,103,2,39,0,204,195,206,156,1,1,6,113,90,76,56,88,88,1,40,0,199,130,209,189,2,201,5,2,105,100,1,119,6,113,90,76,56,88,88,40,0,199,130,209,189,2,201,5,2,116,121,1,119,11,116,111,103,103,108,101,95,108,105,115,116,40,0,199,130,209,189,2,201,5,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,199,130,209,189,2,201,5,8,99,104,105,108,100,114,101,110,1,119,6,49,98,68,104,69,101,33,0,199,130,209,189,2,201,5,4,100,97,116,97,1,40,0,199,130,209,189,2,201,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,201,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,49,98,68,104,69,101,0,200,199,130,209,189,2,174,5,199,130,209,189,2,191,5,1,119,6,113,90,76,56,88,88,1,0,199,130,209,189,2,200,5,1,161,199,130,209,189,2,206,5,1,129,199,130,209,189,2,211,5,1,161,199,130,209,189,2,212,5,1,129,199,130,209,189,2,213,5,1,161,199,130,209,189,2,214,5,1,39,0,204,195,206,156,1,4,6,88,103,107,99,56,110,2,39,0,204,195,206,156,1,1,6,105,66,98,109,87,48,1,40,0,199,130,209,189,2,218,5,2,105,100,1,119,6,105,66,98,109,87,48,40,0,199,130,209,189,2,218,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,218,5,6,112,97,114,101,110,116,1,119,6,113,90,76,56,88,88,40,0,199,130,209,189,2,218,5,8,99,104,105,108,100,114,101,110,1,119,6,72,95,104,114,75,71,33,0,199,130,209,189,2,218,5,4,100,97,116,97,1,40,0,199,130,209,189,2,218,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,218,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,72,95,104,114,75,71,0,8,0,199,130,209,189,2,209,5,1,119,6,105,66,98,109,87,48,161,199,130,209,189,2,216,5,1,1,0,199,130,209,189,2,217,5,1,161,199,130,209,189,2,223,5,1,129,199,130,209,189,2,229,5,1,161,199,130,209,189,2,230,5,1,129,199,130,209,189,2,231,5,1,161,199,130,209,189,2,232,5,1,39,0,204,195,206,156,1,4,6,52,90,104,112,86,73,2,33,0,204,195,206,156,1,1,6,78,79,116,108,71,74,1,0,7,33,0,204,195,206,156,1,3,6,52,107,104,115,81,48,1,129,199,130,209,189,2,227,5,1,39,0,204,195,206,156,1,4,6,74,98,106,103,98,105,2,39,0,204,195,206,156,1,1,6,55,71,119,105,74,83,1,40,0,199,130,209,189,2,247,5,2,105,100,1,119,6,55,71,119,105,74,83,40,0,199,130,209,189,2,247,5,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,247,5,6,112,97,114,101,110,116,1,119,6,105,66,98,109,87,48,40,0,199,130,209,189,2,247,5,8,99,104,105,108,100,114,101,110,1,119,6,99,53,87,50,53,102,33,0,199,130,209,189,2,247,5,4,100,97,116,97,1,40,0,199,130,209,189,2,247,5,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,247,5,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,99,53,87,50,53,102,0,8,0,199,130,209,189,2,226,5,1,119,6,55,71,119,105,74,83,1,0,199,130,209,189,2,246,5,1,161,199,130,209,189,2,252,5,1,129,199,130,209,189,2,129,6,1,161,199,130,209,189,2,130,6,1,129,199,130,209,189,2,131,6,1,161,199,130,209,189,2,132,6,1,39,0,204,195,206,156,1,4,6,118,87,56,68,45,102,2,33,0,204,195,206,156,1,1,6,99,88,73,114,105,45,1,0,7,33,0,204,195,206,156,1,3,6,122,70,104,98,74,88,1,129,199,130,209,189,2,128,6,1,39,0,204,195,206,156,1,4,6,86,65,80,82,86,55,2,33,0,204,195,206,156,1,1,6,99,72,102,57,114,111,1,0,7,33,0,204,195,206,156,1,3,6,70,112,56,103,98,56,1,129,199,130,209,189,2,245,5,1,4,0,199,130,209,189,2,146,6,1,49,0,1,132,199,130,209,189,2,157,6,1,50,0,1,132,199,130,209,189,2,159,6,1,51,0,1,39,0,204,195,206,156,1,4,6,95,70,68,79,103,89,2,4,0,199,130,209,189,2,163,6,3,49,50,51,33,0,204,195,206,156,1,1,6,84,69,81,71,120,89,1,0,7,33,0,204,195,206,156,1,3,6,72,120,102,70,78,49,1,129,199,130,209,189,2,191,5,1,68,199,130,209,189,2,193,4,1,98,161,199,130,209,189,2,198,4,1,132,199,130,209,189,2,197,4,1,117,161,199,130,209,189,2,178,6,1,129,199,130,209,189,2,179,6,1,132,199,130,209,189,2,181,6,1,108,161,199,130,209,189,2,180,6,2,132,199,130,209,189,2,182,6,1,108,161,199,130,209,189,2,184,6,1,132,199,130,209,189,2,185,6,1,101,161,199,130,209,189,2,186,6,1,132,199,130,209,189,2,187,6,1,116,161,199,130,209,189,2,188,6,1,132,199,130,209,189,2,189,6,1,101,161,199,130,209,189,2,190,6,1,132,199,130,209,189,2,191,6,1,100,161,199,130,209,189,2,192,6,1,132,199,130,209,189,2,193,6,1,32,161,199,130,209,189,2,194,6,1,132,199,130,209,189,2,195,6,1,108,161,199,130,209,189,2,196,6,1,132,199,130,209,189,2,197,6,1,105,161,199,130,209,189,2,198,6,1,132,199,130,209,189,2,199,6,1,115,161,199,130,209,189,2,200,6,1,132,199,130,209,189,2,201,6,1,116,168,199,130,209,189,2,202,6,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,98,117,108,108,101,116,101,100,32,108,105,115,116,34,125,93,125,68,199,130,209,189,2,221,4,1,99,161,199,130,209,189,2,226,4,1,132,199,130,209,189,2,225,4,1,104,161,199,130,209,189,2,206,6,1,132,199,130,209,189,2,207,6,1,105,161,199,130,209,189,2,208,6,1,132,199,130,209,189,2,209,6,1,108,161,199,130,209,189,2,210,6,1,132,199,130,209,189,2,211,6,1,100,161,199,130,209,189,2,212,6,1,129,199,130,209,189,2,213,6,1,161,199,130,209,189,2,214,6,2,132,199,130,209,189,2,215,6,1,45,161,199,130,209,189,2,217,6,1,132,199,130,209,189,2,218,6,1,49,168,199,130,209,189,2,219,6,1,119,32,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,34,125,93,125,68,199,130,209,189,2,249,4,1,99,161,199,130,209,189,2,254,4,1,132,199,130,209,189,2,253,4,1,104,161,199,130,209,189,2,223,6,1,132,199,130,209,189,2,224,6,1,105,161,199,130,209,189,2,225,6,1,132,199,130,209,189,2,226,6,1,108,161,199,130,209,189,2,227,6,1,132,199,130,209,189,2,228,6,1,100,161,199,130,209,189,2,229,6,1,132,199,130,209,189,2,230,6,1,45,161,199,130,209,189,2,231,6,1,132,199,130,209,189,2,232,6,1,49,161,199,130,209,189,2,233,6,1,132,199,130,209,189,2,234,6,1,45,161,199,130,209,189,2,235,6,1,132,199,130,209,189,2,236,6,1,49,168,199,130,209,189,2,237,6,1,119,34,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,45,49,34,125,93,125,68,199,130,209,189,2,138,5,1,99,161,199,130,209,189,2,143,5,1,132,199,130,209,189,2,142,5,1,104,161,199,130,209,189,2,241,6,1,132,199,130,209,189,2,242,6,1,105,161,199,130,209,189,2,243,6,1,132,199,130,209,189,2,244,6,1,108,161,199,130,209,189,2,245,6,1,132,199,130,209,189,2,246,6,1,100,161,199,130,209,189,2,247,6,1,132,199,130,209,189,2,248,6,1,45,161,199,130,209,189,2,249,6,1,132,199,130,209,189,2,250,6,1,49,161,199,130,209,189,2,251,6,1,132,199,130,209,189,2,252,6,1,45,161,199,130,209,189,2,253,6,1,132,199,130,209,189,2,254,6,1,50,168,199,130,209,189,2,255,6,1,119,34,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,45,50,34,125,93,125,68,199,130,209,189,2,162,5,1,99,161,199,130,209,189,2,180,5,1,132,199,130,209,189,2,179,5,1,104,161,199,130,209,189,2,131,7,1,132,199,130,209,189,2,132,7,1,105,161,199,130,209,189,2,133,7,1,132,199,130,209,189,2,134,7,1,108,161,199,130,209,189,2,135,7,1,132,199,130,209,189,2,136,7,1,100,161,199,130,209,189,2,137,7,1,132,199,130,209,189,2,138,7,1,45,161,199,130,209,189,2,139,7,1,132,199,130,209,189,2,140,7,1,50,168,199,130,209,189,2,141,7,1,119,32,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,50,34,125,93,125,65,199,130,209,189,2,211,5,1,161,199,130,209,189,2,228,5,1,129,199,130,209,189,2,215,5,1,161,199,130,209,189,2,145,7,1,129,199,130,209,189,2,146,7,1,161,199,130,209,189,2,147,7,1,129,199,130,209,189,2,148,7,1,161,199,130,209,189,2,149,7,1,129,199,130,209,189,2,150,7,1,161,199,130,209,189,2,151,7,1,129,199,130,209,189,2,152,7,1,161,199,130,209,189,2,153,7,1,129,199,130,209,189,2,154,7,1,161,199,130,209,189,2,155,7,8,132,199,130,209,189,2,156,7,1,116,161,199,130,209,189,2,164,7,1,132,199,130,209,189,2,165,7,1,111,161,199,130,209,189,2,166,7,1,132,199,130,209,189,2,167,7,1,103,161,199,130,209,189,2,168,7,1,132,199,130,209,189,2,169,7,1,103,161,199,130,209,189,2,170,7,1,132,199,130,209,189,2,171,7,1,108,161,199,130,209,189,2,172,7,1,132,199,130,209,189,2,173,7,1,101,161,199,130,209,189,2,174,7,1,132,199,130,209,189,2,175,7,1,32,161,199,130,209,189,2,176,7,1,132,199,130,209,189,2,177,7,1,108,161,199,130,209,189,2,178,7,1,132,199,130,209,189,2,179,7,1,105,161,199,130,209,189,2,180,7,1,132,199,130,209,189,2,181,7,1,115,161,199,130,209,189,2,182,7,1,132,199,130,209,189,2,183,7,1,116,168,199,130,209,189,2,184,7,1,119,54,123,34,99,111,108,108,97,112,115,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,116,111,103,103,108,101,32,108,105,115,116,34,125,93,125,68,199,130,209,189,2,229,5,1,99,161,199,130,209,189,2,234,5,1,132,199,130,209,189,2,233,5,1,104,161,199,130,209,189,2,188,7,1,132,199,130,209,189,2,189,7,1,105,161,199,130,209,189,2,190,7,1,132,199,130,209,189,2,191,7,1,108,161,199,130,209,189,2,192,7,1,132,199,130,209,189,2,193,7,1,100,161,199,130,209,189,2,194,7,1,132,199,130,209,189,2,195,7,1,45,161,199,130,209,189,2,196,7,1,129,199,130,209,189,2,197,7,1,161,199,130,209,189,2,198,7,2,132,199,130,209,189,2,199,7,1,49,168,199,130,209,189,2,201,7,1,119,32,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,34,125,93,125,68,199,130,209,189,2,129,6,1,99,161,199,130,209,189,2,134,6,1,132,199,130,209,189,2,133,6,1,104,161,199,130,209,189,2,205,7,1,132,199,130,209,189,2,206,7,1,105,161,199,130,209,189,2,207,7,1,132,199,130,209,189,2,208,7,1,108,161,199,130,209,189,2,209,7,1,132,199,130,209,189,2,210,7,1,100,161,199,130,209,189,2,211,7,1,132,199,130,209,189,2,212,7,1,45,161,199,130,209,189,2,213,7,1,132,199,130,209,189,2,214,7,1,49,161,199,130,209,189,2,215,7,1,129,199,130,209,189,2,216,7,1,161,199,130,209,189,2,217,7,1,129,199,130,209,189,2,218,7,1,161,199,130,209,189,2,219,7,3,129,199,130,209,189,2,220,7,1,161,199,130,209,189,2,223,7,1,129,199,130,209,189,2,224,7,1,161,199,130,209,189,2,225,7,3,132,199,130,209,189,2,226,7,1,45,161,199,130,209,189,2,229,7,1,132,199,130,209,189,2,230,7,1,49,168,199,130,209,189,2,231,7,1,119,34,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,45,49,34,125,93,125,39,0,204,195,206,156,1,4,6,55,88,55,105,70,103,2,39,0,204,195,206,156,1,1,6,101,79,68,109,108,65,1,40,0,199,130,209,189,2,235,7,2,105,100,1,119,6,101,79,68,109,108,65,40,0,199,130,209,189,2,235,7,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,199,130,209,189,2,235,7,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,199,130,209,189,2,235,7,8,99,104,105,108,100,114,101,110,1,119,6,109,112,74,69,74,90,33,0,199,130,209,189,2,235,7,4,100,97,116,97,1,40,0,199,130,209,189,2,235,7,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,199,130,209,189,2,235,7,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,109,112,74,69,74,90,0,200,204,195,206,156,1,242,1,204,195,206,156,1,243,1,1,119,6,101,79,68,109,108,65,168,204,195,206,156,1,164,1,1,119,79,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,44,32,109,97,114,107,100,111,119,110,44,32,97,110,100,32,99,111,100,101,32,98,108,111,99,107,34,125,93,44,34,108,101,118,101,108,34,58,50,125,168,204,195,206,156,1,165,1,1,119,10,97,98,100,49,105,117,71,81,109,68,168,204,195,206,156,1,166,1,1,119,4,116,101,120,116,1,0,199,130,209,189,2,234,7,1,161,199,130,209,189,2,240,7,1,168,199,130,209,189,2,249,7,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,132,199,130,209,189,2,48,1,32,161,199,130,209,189,2,61,1,129,199,130,209,189,2,251,7,1,161,199,130,209,189,2,252,7,1,134,199,130,209,189,2,253,7,7,109,101,110,116,105,111,110,51,123,34,116,121,112,101,34,58,34,100,97,116,101,34,44,34,100,97,116,101,34,58,34,50,48,50,52,45,48,52,45,49,56,84,49,52,58,50,53,58,51,50,46,52,53,55,50,55,55,34,125,132,199,130,209,189,2,255,7,1,36,134,199,130,209,189,2,128,8,7,109,101,110,116,105,111,110,4,110,117,108,108,161,199,130,209,189,2,254,7,1,132,199,130,209,189,2,129,8,1,109,161,199,130,209,189,2,130,8,1,132,199,130,209,189,2,131,8,1,101,161,199,130,209,189,2,132,8,1,132,199,130,209,189,2,133,8,1,110,161,199,130,209,189,2,134,8,1,129,199,130,209,189,2,135,8,1,132,199,130,209,189,2,137,8,1,116,161,199,130,209,189,2,136,8,2,1,236,158,128,159,2,0,161,219,200,174,197,9,24,4,1,245,181,155,135,2,0,161,151,234,142,238,11,26,23,176,1,146,216,250,133,2,0,161,243,138,171,183,10,60,1,161,243,138,171,183,10,61,1,161,243,138,171,183,10,62,1,161,243,138,171,183,10,71,1,161,243,138,171,183,10,63,1,161,243,138,171,183,10,64,1,161,243,138,171,183,10,65,1,161,146,216,250,133,2,3,1,161,243,138,171,183,10,67,1,161,243,138,171,183,10,68,1,161,243,138,171,183,10,69,1,161,146,216,250,133,2,7,1,161,243,138,171,183,10,84,1,161,243,138,171,183,10,85,1,161,243,138,171,183,10,86,1,161,243,138,171,183,10,95,3,161,243,138,171,183,10,91,1,161,243,138,171,183,10,92,1,161,243,138,171,183,10,93,1,161,243,138,171,183,10,87,1,161,243,138,171,183,10,88,1,161,243,138,171,183,10,89,1,161,243,138,171,183,10,96,1,161,243,138,171,183,10,97,1,161,243,138,171,183,10,98,1,161,243,138,171,183,10,107,1,161,243,138,171,183,10,100,1,161,243,138,171,183,10,101,1,161,243,138,171,183,10,102,1,161,146,216,250,133,2,27,1,161,243,138,171,183,10,104,1,161,243,138,171,183,10,105,1,161,243,138,171,183,10,106,1,161,146,216,250,133,2,31,1,161,243,138,171,183,10,108,1,161,243,138,171,183,10,109,1,161,243,138,171,183,10,110,1,161,243,138,171,183,10,119,1,161,243,138,171,183,10,112,1,161,243,138,171,183,10,113,1,161,243,138,171,183,10,114,1,161,146,216,250,133,2,39,1,161,243,138,171,183,10,116,1,161,243,138,171,183,10,117,1,161,243,138,171,183,10,118,1,161,146,216,250,133,2,43,1,161,243,138,171,183,10,120,1,161,243,138,171,183,10,121,1,161,243,138,171,183,10,122,1,161,243,138,171,183,10,123,1,161,243,138,171,183,10,124,1,161,243,138,171,183,10,125,1,161,243,138,171,183,10,131,1,2,161,243,138,171,183,10,127,1,161,243,138,171,183,10,128,1,1,161,243,138,171,183,10,129,1,1,161,146,216,250,133,2,55,1,161,243,138,171,183,10,132,1,1,161,243,138,171,183,10,133,1,1,161,243,138,171,183,10,134,1,1,161,243,138,171,183,10,143,1,1,161,243,138,171,183,10,136,1,1,161,243,138,171,183,10,137,1,1,161,243,138,171,183,10,138,1,1,161,146,216,250,133,2,63,1,161,243,138,171,183,10,140,1,1,161,243,138,171,183,10,141,1,1,161,243,138,171,183,10,142,1,1,161,146,216,250,133,2,67,1,161,243,138,171,183,10,144,1,1,161,243,138,171,183,10,145,1,1,161,243,138,171,183,10,146,1,1,161,243,138,171,183,10,155,1,1,161,243,138,171,183,10,148,1,1,161,243,138,171,183,10,149,1,1,161,243,138,171,183,10,150,1,1,161,146,216,250,133,2,75,1,161,243,138,171,183,10,152,1,1,161,243,138,171,183,10,153,1,1,161,243,138,171,183,10,154,1,1,161,146,216,250,133,2,79,1,161,243,138,171,183,10,156,1,1,161,243,138,171,183,10,157,1,1,161,243,138,171,183,10,158,1,1,161,243,138,171,183,10,167,1,1,161,243,138,171,183,10,160,1,1,161,243,138,171,183,10,161,1,1,161,243,138,171,183,10,162,1,1,161,146,216,250,133,2,87,1,161,243,138,171,183,10,164,1,1,161,243,138,171,183,10,165,1,1,161,243,138,171,183,10,166,1,1,161,146,216,250,133,2,91,1,161,243,138,171,183,10,168,1,1,161,243,138,171,183,10,169,1,1,161,243,138,171,183,10,170,1,1,161,243,138,171,183,10,179,1,1,161,243,138,171,183,10,176,1,1,161,243,138,171,183,10,177,1,1,161,243,138,171,183,10,178,1,1,161,146,216,250,133,2,99,1,161,243,138,171,183,10,172,1,1,161,243,138,171,183,10,173,1,1,161,243,138,171,183,10,174,1,1,161,146,216,250,133,2,103,1,161,243,138,171,183,10,180,1,1,161,243,138,171,183,10,181,1,1,161,243,138,171,183,10,182,1,1,161,243,138,171,183,10,191,1,1,161,243,138,171,183,10,188,1,1,161,243,138,171,183,10,189,1,1,161,243,138,171,183,10,190,1,1,161,146,216,250,133,2,111,1,161,243,138,171,183,10,184,1,1,161,243,138,171,183,10,185,1,1,161,243,138,171,183,10,186,1,1,161,146,216,250,133,2,115,1,161,243,138,171,183,10,192,1,1,161,243,138,171,183,10,193,1,1,161,243,138,171,183,10,194,1,1,161,243,138,171,183,10,203,1,1,161,243,138,171,183,10,196,1,1,161,243,138,171,183,10,197,1,1,161,243,138,171,183,10,198,1,1,161,146,216,250,133,2,123,1,161,243,138,171,183,10,200,1,1,161,243,138,171,183,10,201,1,1,161,243,138,171,183,10,202,1,1,161,146,216,250,133,2,127,1,161,243,138,171,183,10,204,1,1,161,243,138,171,183,10,205,1,1,161,243,138,171,183,10,206,1,1,161,243,138,171,183,10,215,1,1,161,243,138,171,183,10,209,1,1,161,243,138,171,183,10,210,1,1,161,243,138,171,183,10,211,1,1,161,146,216,250,133,2,135,1,1,161,243,138,171,183,10,212,1,1,161,243,138,171,183,10,213,1,1,161,243,138,171,183,10,214,1,1,161,146,216,250,133,2,139,1,1,161,243,138,171,183,10,216,1,1,161,243,138,171,183,10,217,1,1,161,243,138,171,183,10,218,1,1,161,243,138,171,183,10,227,1,1,161,243,138,171,183,10,220,1,1,161,243,138,171,183,10,221,1,1,161,243,138,171,183,10,222,1,1,161,146,216,250,133,2,147,1,1,161,243,138,171,183,10,224,1,1,161,243,138,171,183,10,225,1,1,161,243,138,171,183,10,226,1,1,161,146,216,250,133,2,151,1,1,161,243,138,171,183,10,228,1,1,161,243,138,171,183,10,229,1,1,161,243,138,171,183,10,230,1,1,161,243,138,171,183,10,239,1,1,161,243,138,171,183,10,232,1,1,161,243,138,171,183,10,233,1,1,161,243,138,171,183,10,234,1,1,161,146,216,250,133,2,159,1,1,161,243,138,171,183,10,236,1,1,161,243,138,171,183,10,237,1,1,161,243,138,171,183,10,238,1,1,161,146,216,250,133,2,163,1,1,161,146,216,250,133,2,156,1,1,161,146,216,250,133,2,157,1,1,161,146,216,250,133,2,158,1,1,161,146,216,250,133,2,160,1,1,161,146,216,250,133,2,161,1,1,161,146,216,250,133,2,162,1,1,161,146,216,250,133,2,167,1,1,161,146,216,250,133,2,164,1,1,161,146,216,250,133,2,165,1,1,161,146,216,250,133,2,166,1,1,161,146,216,250,133,2,174,1,2,9,172,254,181,239,1,0,39,0,204,195,206,156,1,4,6,108,45,56,109,101,45,2,4,0,172,254,181,239,1,0,4,104,106,107,100,161,198,223,206,159,1,153,1,1,132,172,254,181,239,1,4,2,39,100,161,172,254,181,239,1,5,1,132,172,254,181,239,1,7,2,39,100,161,172,254,181,239,1,8,1,132,172,254,181,239,1,10,2,39,100,161,172,254,181,239,1,11,1,1,153,236,182,220,1,0,161,195,254,251,180,11,57,4,10,155,213,159,176,1,0,161,131,182,180,202,12,50,1,161,131,182,180,202,12,51,1,161,131,182,180,202,12,52,1,161,155,213,159,176,1,0,1,161,155,213,159,176,1,1,1,161,155,213,159,176,1,2,1,129,131,182,180,202,12,43,1,161,155,213,159,176,1,3,1,161,155,213,159,176,1,4,1,161,155,213,159,176,1,5,1,179,1,198,223,206,159,1,0,39,0,204,195,206,156,1,4,6,57,70,53,89,108,75,2,4,0,198,223,206,159,1,0,13,103,104,104,104,229,143,145,230,140,165,229,165,189,168,171,236,222,251,5,166,2,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,103,104,104,104,229,143,145,230,140,165,229,165,189,34,125,93,125,39,0,204,195,206,156,1,4,6,89,50,51,82,99,105,2,4,0,198,223,206,159,1,9,12,229,185,178,230,180,187,229,147,136,229,147,136,168,171,236,222,251,5,240,3,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,185,178,230,180,187,229,147,136,229,147,136,34,125,93,125,0,3,39,0,204,195,206,156,1,4,6,72,90,117,111,112,102,2,33,0,204,195,206,156,1,1,6,67,102,80,66,48,85,1,0,7,33,0,204,195,206,156,1,3,6,81,117,121,48,102,66,1,193,171,236,222,251,5,196,1,171,236,222,251,5,170,2,1,1,0,198,223,206,159,1,18,3,0,2,129,198,223,206,159,1,31,1,0,4,39,0,204,195,206,156,1,4,6,48,82,103,55,103,55,2,33,0,204,195,206,156,1,1,6,72,51,76,88,97,79,1,0,7,33,0,204,195,206,156,1,3,6,75,83,56,80,116,80,1,193,171,236,222,251,5,196,1,198,223,206,159,1,28,1,39,0,204,195,206,156,1,4,6,105,90,51,118,76,100,2,33,0,204,195,206,156,1,1,6,121,102,76,72,69,119,1,0,7,33,0,204,195,206,156,1,3,6,48,80,108,53,77,98,1,193,171,236,222,251,5,196,1,198,223,206,159,1,49,1,39,0,204,195,206,156,1,4,6,72,97,76,66,45,86,2,33,0,204,195,206,156,1,1,6,98,65,77,76,51,82,1,0,7,33,0,204,195,206,156,1,3,6,81,83,99,52,51,111,1,193,171,236,222,251,5,196,1,198,223,206,159,1,60,1,39,0,204,195,206,156,1,4,6,98,86,122,115,102,101,2,39,0,204,195,206,156,1,1,6,52,90,113,105,51,76,1,40,0,198,223,206,159,1,73,2,105,100,1,119,6,52,90,113,105,51,76,40,0,198,223,206,159,1,73,2,116,121,1,119,5,113,117,111,116,101,40,0,198,223,206,159,1,73,6,112,97,114,101,110,116,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,198,223,206,159,1,73,8,99,104,105,108,100,114,101,110,1,119,6,98,50,103,102,70,95,33,0,198,223,206,159,1,73,4,100,97,116,97,1,40,0,198,223,206,159,1,73,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,73,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,98,50,103,102,70,95,0,200,171,236,222,251,5,196,1,198,223,206,159,1,71,1,119,6,52,90,113,105,51,76,4,0,198,223,206,159,1,72,6,231,155,145,230,142,167,168,198,223,206,159,1,78,1,119,31,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,231,155,145,230,142,167,34,125,93,125,193,204,195,206,156,1,244,5,204,195,206,156,1,245,5,3,161,204,195,206,156,1,137,1,1,161,204,195,206,156,1,138,1,1,161,204,195,206,156,1,139,1,1,39,0,204,195,206,156,1,4,6,50,101,101,116,51,53,2,33,0,204,195,206,156,1,1,6,77,103,77,119,109,49,1,0,7,33,0,204,195,206,156,1,3,6,52,76,51,66,86,49,1,193,204,195,206,156,1,232,1,204,195,206,156,1,233,1,1,0,3,39,0,204,195,206,156,1,4,6,100,87,119,54,116,114,2,39,0,204,195,206,156,1,1,6,77,89,55,45,90,70,1,40,0,198,223,206,159,1,107,2,105,100,1,119,6,77,89,55,45,90,70,40,0,198,223,206,159,1,107,2,116,121,1,119,5,113,117,111,116,101,40,0,198,223,206,159,1,107,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,198,223,206,159,1,107,8,99,104,105,108,100,114,101,110,1,119,6,112,88,122,66,110,100,33,0,198,223,206,159,1,107,4,100,97,116,97,1,40,0,198,223,206,159,1,107,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,107,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,112,88,122,66,110,100,0,200,204,195,206,156,1,232,1,198,223,206,159,1,102,1,119,6,77,89,55,45,90,70,4,0,198,223,206,159,1,106,9,229,144,140,228,184,128,228,184,170,161,198,223,206,159,1,112,1,132,198,223,206,159,1,119,3,106,106,106,161,198,223,206,159,1,120,1,39,0,204,195,206,156,1,4,6,71,121,120,95,72,54,2,33,0,204,195,206,156,1,1,6,83,101,74,81,114,75,1,0,7,33,0,204,195,206,156,1,3,6,122,116,99,78,71,87,1,193,204,195,206,156,1,232,1,198,223,206,159,1,116,1,0,3,39,0,204,195,206,156,1,4,6,51,107,108,102,97,80,2,39,0,204,195,206,156,1,1,6,85,72,48,53,51,70,1,40,0,198,223,206,159,1,140,1,2,105,100,1,119,6,85,72,48,53,51,70,40,0,198,223,206,159,1,140,1,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,198,223,206,159,1,140,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,198,223,206,159,1,140,1,8,99,104,105,108,100,114,101,110,1,119,6,52,75,90,73,113,76,33,0,198,223,206,159,1,140,1,4,100,97,116,97,1,40,0,198,223,206,159,1,140,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,140,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,52,75,90,73,113,76,0,200,204,195,206,156,1,232,1,198,223,206,159,1,135,1,1,119,6,85,72,48,53,51,70,4,0,198,223,206,159,1,139,1,3,104,106,107,161,198,223,206,159,1,145,1,1,39,0,204,195,206,156,1,1,6,114,78,78,65,105,82,1,40,0,198,223,206,159,1,154,1,2,105,100,1,119,6,114,78,78,65,105,82,40,0,198,223,206,159,1,154,1,2,116,121,1,119,13,109,97,116,104,95,101,113,117,97,116,105,111,110,40,0,198,223,206,159,1,154,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,198,223,206,159,1,154,1,8,99,104,105,108,100,114,101,110,1,119,6,69,82,69,45,78,66,33,0,198,223,206,159,1,154,1,4,100,97,116,97,1,40,0,198,223,206,159,1,154,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,154,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,69,82,69,45,78,66,0,200,204,195,206,156,1,240,1,204,195,206,156,1,241,1,1,119,6,114,78,78,65,105,82,168,198,223,206,159,1,159,1,1,119,24,123,34,102,111,114,109,117,108,97,34,58,34,105,231,156,139,231,187,143,230,181,142,34,125,39,0,204,195,206,156,1,1,6,68,114,122,68,111,83,1,40,0,198,223,206,159,1,165,1,2,105,100,1,119,6,68,114,122,68,111,83,40,0,198,223,206,159,1,165,1,2,116,121,1,119,5,105,109,97,103,101,40,0,198,223,206,159,1,165,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,198,223,206,159,1,165,1,8,99,104,105,108,100,114,101,110,1,119,6,57,68,97,108,108,97,33,0,198,223,206,159,1,165,1,4,100,97,116,97,1,40,0,198,223,206,159,1,165,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,165,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,57,68,97,108,108,97,0,200,204,195,206,156,1,251,1,204,195,206,156,1,252,1,1,119,6,68,114,122,68,111,83,161,198,223,206,159,1,170,1,1,39,0,204,195,206,156,1,4,6,102,80,55,52,75,113,2,33,0,204,195,206,156,1,1,6,84,120,69,107,78,52,1,0,7,33,0,204,195,206,156,1,3,6,104,109,65,56,45,115,1,193,204,195,206,156,1,244,1,204,195,206,156,1,245,1,1,39,0,204,195,206,156,1,4,6,118,105,52,104,122,104,2,39,0,204,195,206,156,1,1,6,95,98,119,81,76,101,1,40,0,198,223,206,159,1,188,1,2,105,100,1,119,6,95,98,119,81,76,101,40,0,198,223,206,159,1,188,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,198,223,206,159,1,188,1,6,112,97,114,101,110,116,1,119,10,101,110,68,45,73,83,100,100,99,55,40,0,198,223,206,159,1,188,1,8,99,104,105,108,100,114,101,110,1,119,6,104,102,109,108,88,52,33,0,198,223,206,159,1,188,1,4,100,97,116,97,1,40,0,198,223,206,159,1,188,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,188,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,104,102,109,108,88,52,0,8,0,204,195,206,156,1,23,1,119,6,95,98,119,81,76,101,4,0,198,223,206,159,1,187,1,3,105,106,106,168,198,223,206,159,1,193,1,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,105,106,106,34,125,93,125,39,0,204,195,206,156,1,4,6,55,83,79,113,80,69,2,33,0,204,195,206,156,1,1,6,75,119,55,52,104,73,1,0,7,33,0,204,195,206,156,1,3,6,80,82,74,72,65,95,1,129,198,223,206,159,1,197,1,1,39,0,204,195,206,156,1,4,6,78,97,78,121,113,76,2,39,0,204,195,206,156,1,1,6,72,90,88,98,113,104,1,40,0,198,223,206,159,1,214,1,2,105,100,1,119,6,72,90,88,98,113,104,40,0,198,223,206,159,1,214,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,198,223,206,159,1,214,1,6,112,97,114,101,110,116,1,119,6,95,98,119,81,76,101,40,0,198,223,206,159,1,214,1,8,99,104,105,108,100,114,101,110,1,119,6,110,98,72,85,90,106,33,0,198,223,206,159,1,214,1,4,100,97,116,97,1,40,0,198,223,206,159,1,214,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,214,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,110,98,72,85,90,106,0,8,0,198,223,206,159,1,196,1,1,119,6,72,90,88,98,113,104,4,0,198,223,206,159,1,213,1,4,106,107,110,98,168,198,223,206,159,1,219,1,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,106,107,110,98,34,125,93,125,39,0,204,195,206,156,1,4,6,57,56,55,97,106,50,2,33,0,204,195,206,156,1,1,6,110,117,56,75,122,68,1,0,7,33,0,204,195,206,156,1,3,6,85,56,79,113,105,78,1,129,198,223,206,159,1,223,1,1,39,0,204,195,206,156,1,4,6,88,116,82,99,45,53,2,39,0,204,195,206,156,1,1,6,88,52,88,118,49,84,1,40,0,198,223,206,159,1,241,1,2,105,100,1,119,6,88,52,88,118,49,84,40,0,198,223,206,159,1,241,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,198,223,206,159,1,241,1,6,112,97,114,101,110,116,1,119,6,72,90,88,98,113,104,40,0,198,223,206,159,1,241,1,8,99,104,105,108,100,114,101,110,1,119,6,119,77,90,48,100,71,33,0,198,223,206,159,1,241,1,4,100,97,116,97,1,40,0,198,223,206,159,1,241,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,241,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,119,77,90,48,100,71,0,8,0,198,223,206,159,1,222,1,1,119,6,88,52,88,118,49,84,4,0,198,223,206,159,1,240,1,6,232,191,155,230,173,165,161,198,223,206,159,1,246,1,1,132,198,223,206,159,1,252,1,6,230,156,186,228,188,154,168,198,223,206,159,1,253,1,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,232,191,155,230,173,165,230,156,186,228,188,154,34,125,93,125,39,0,204,195,206,156,1,4,6,121,85,99,121,82,100,2,39,0,204,195,206,156,1,1,6,100,121,76,82,53,100,1,40,0,198,223,206,159,1,130,2,2,105,100,1,119,6,100,121,76,82,53,100,40,0,198,223,206,159,1,130,2,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,198,223,206,159,1,130,2,6,112,97,114,101,110,116,1,119,6,72,90,88,98,113,104,40,0,198,223,206,159,1,130,2,8,99,104,105,108,100,114,101,110,1,119,6,55,89,79,70,48,116,33,0,198,223,206,159,1,130,2,4,100,97,116,97,1,40,0,198,223,206,159,1,130,2,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,198,223,206,159,1,130,2,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,55,89,79,70,48,116,0,136,198,223,206,159,1,250,1,1,119,6,100,121,76,82,53,100,4,0,198,223,206,159,1,129,2,12,230,150,164,230,150,164,232,174,161,232,190,131,168,198,223,206,159,1,135,2,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,230,150,164,230,150,164,232,174,161,232,190,131,34,125,93,125,231,2,204,195,206,156,1,0,39,1,4,100,97,116,97,8,100,111,99,117,109,101,110,116,1,39,0,204,195,206,156,1,0,6,98,108,111,99,107,115,1,39,0,204,195,206,156,1,0,4,109,101,116,97,1,39,0,204,195,206,156,1,2,12,99,104,105,108,100,114,101,110,95,109,97,112,1,39,0,204,195,206,156,1,2,8,116,101,120,116,95,109,97,112,1,40,0,204,195,206,156,1,0,7,112,97,103,101,95,105,100,1,119,10,109,54,120,76,118,72,89,48,76,107,39,0,204,195,206,156,1,1,10,77,48,104,84,99,67,120,66,88,82,1,40,0,204,195,206,156,1,6,2,105,100,1,119,10,77,48,104,84,99,67,120,66,88,82,40,0,204,195,206,156,1,6,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,6,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,6,8,99,104,105,108,100,114,101,110,1,119,10,49,87,78,107,89,75,118,109,105,50,33,0,204,195,206,156,1,6,4,100,97,116,97,1,33,0,204,195,206,156,1,6,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,49,87,78,107,89,75,118,109,105,50,0,39,0,204,195,206,156,1,1,10,101,110,68,45,73,83,100,100,99,55,1,40,0,204,195,206,156,1,15,2,105,100,1,119,10,101,110,68,45,73,83,100,100,99,55,40,0,204,195,206,156,1,15,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,204,195,206,156,1,15,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,15,8,99,104,105,108,100,114,101,110,1,119,10,103,106,110,76,109,66,89,118,68,65,40,0,204,195,206,156,1,15,4,100,97,116,97,1,119,2,123,125,40,0,204,195,206,156,1,15,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,102,56,54,108,88,117,88,74,101,54,40,0,204,195,206,156,1,15,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,103,106,110,76,109,66,89,118,68,65,0,39,0,204,195,206,156,1,1,10,113,115,110,89,82,48,74,72,74,56,1,40,0,204,195,206,156,1,24,2,105,100,1,119,10,113,115,110,89,82,48,74,72,74,56,40,0,204,195,206,156,1,24,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,24,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,24,8,99,104,105,108,100,114,101,110,1,119,10,116,79,53,122,78,78,73,82,69,100,40,0,204,195,206,156,1,24,4,100,97,116,97,1,119,17,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,40,0,204,195,206,156,1,24,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,51,72,115,115,121,121,66,84,57,50,40,0,204,195,206,156,1,24,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,116,79,53,122,78,78,73,82,69,100,0,39,0,204,195,206,156,1,1,10,75,54,50,76,100,101,119,53,95,121,1,40,0,204,195,206,156,1,33,2,105,100,1,119,10,75,54,50,76,100,101,119,53,95,121,40,0,204,195,206,156,1,33,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,204,195,206,156,1,33,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,33,8,99,104,105,108,100,114,101,110,1,119,10,57,118,109,120,98,73,71,120,109,73,40,0,204,195,206,156,1,33,4,100,97,116,97,1,119,11,123,34,108,101,118,101,108,34,58,50,125,40,0,204,195,206,156,1,33,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,72,116,114,88,117,57,102,65,95,107,40,0,204,195,206,156,1,33,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,57,118,109,120,98,73,71,120,109,73,0,39,0,204,195,206,156,1,1,10,117,51,120,66,95,83,69,116,53,68,1,40,0,204,195,206,156,1,42,2,105,100,1,119,10,117,51,120,66,95,83,69,116,53,68,40,0,204,195,206,156,1,42,2,116,121,1,119,7,99,97,108,108,111,117,116,40,0,204,195,206,156,1,42,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,42,8,99,104,105,108,100,114,101,110,1,119,10,50,88,118,55,52,84,105,73,70,108,40,0,204,195,206,156,1,42,4,100,97,116,97,1,119,15,123,34,105,99,111,110,34,58,34,240,159,165,176,34,125,40,0,204,195,206,156,1,42,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,108,119,101,104,75,79,117,78,68,67,40,0,204,195,206,156,1,42,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,50,88,118,55,52,84,105,73,70,108,0,39,0,204,195,206,156,1,1,10,78,73,76,105,97,84,121,72,108,112,1,40,0,204,195,206,156,1,51,2,105,100,1,119,10,78,73,76,105,97,84,121,72,108,112,40,0,204,195,206,156,1,51,2,116,121,1,119,5,113,117,111,116,101,40,0,204,195,206,156,1,51,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,51,8,99,104,105,108,100,114,101,110,1,119,10,109,82,95,75,65,57,45,108,110,78,33,0,204,195,206,156,1,51,4,100,97,116,97,1,33,0,204,195,206,156,1,51,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,51,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,109,82,95,75,65,57,45,108,110,78,0,33,0,204,195,206,156,1,1,10,99,108,78,111,66,75,99,119,73,82,1,0,7,33,0,204,195,206,156,1,3,10,117,117,65,100,55,95,119,72,72,106,1,39,0,204,195,206,156,1,1,10,78,89,54,108,121,101,57,108,88,51,1,40,0,204,195,206,156,1,69,2,105,100,1,119,10,78,89,54,108,121,101,57,108,88,51,40,0,204,195,206,156,1,69,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,69,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,69,8,99,104,105,108,100,114,101,110,1,119,10,108,77,72,53,73,113,54,77,68,78,40,0,204,195,206,156,1,69,4,100,97,116,97,1,119,17,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,40,0,204,195,206,156,1,69,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,109,69,119,56,90,66,102,95,100,68,40,0,204,195,206,156,1,69,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,108,77,72,53,73,113,54,77,68,78,0,39,0,204,195,206,156,1,1,10,101,104,73,115,79,74,69,114,55,73,1,40,0,204,195,206,156,1,78,2,105,100,1,119,10,101,104,73,115,79,74,69,114,55,73,40,0,204,195,206,156,1,78,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,204,195,206,156,1,78,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,78,8,99,104,105,108,100,114,101,110,1,119,10,56,116,67,52,100,103,121,98,57,55,33,0,204,195,206,156,1,78,4,100,97,116,97,1,33,0,204,195,206,156,1,78,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,78,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,56,116,67,52,100,103,121,98,57,55,0,39,0,204,195,206,156,1,1,10,68,90,114,95,72,118,106,65,78,107,1,40,0,204,195,206,156,1,87,2,105,100,1,119,10,68,90,114,95,72,118,106,65,78,107,40,0,204,195,206,156,1,87,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,87,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,87,8,99,104,105,108,100,114,101,110,1,119,10,119,95,65,55,90,114,77,89,86,122,40,0,204,195,206,156,1,87,4,100,97,116,97,1,119,17,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,40,0,204,195,206,156,1,87,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,54,74,108,118,72,71,53,111,120,90,40,0,204,195,206,156,1,87,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,119,95,65,55,90,114,77,89,86,122,0,33,0,204,195,206,156,1,1,10,105,90,113,50,95,68,72,49,50,69,1,0,7,33,0,204,195,206,156,1,3,10,119,54,53,71,114,77,54,109,119,69,1,39,0,204,195,206,156,1,1,10,48,105,122,109,122,95,86,65,55,70,1,40,0,204,195,206,156,1,105,2,105,100,1,119,10,48,105,122,109,122,95,86,65,55,70,40,0,204,195,206,156,1,105,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,204,195,206,156,1,105,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,105,8,99,104,105,108,100,114,101,110,1,119,10,65,90,49,50,53,79,88,51,65,97,40,0,204,195,206,156,1,105,4,100,97,116,97,1,119,2,123,125,40,0,204,195,206,156,1,105,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,109,73,73,113,81,111,118,74,105,101,40,0,204,195,206,156,1,105,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,65,90,49,50,53,79,88,51,65,97,0,39,0,204,195,206,156,1,1,10,55,107,121,57,118,72,100,98,90,90,1,40,0,204,195,206,156,1,114,2,105,100,1,119,10,55,107,121,57,118,72,100,98,90,90,40,0,204,195,206,156,1,114,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,114,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,114,8,99,104,105,108,100,114,101,110,1,119,10,118,122,73,48,69,73,102,97,111,55,33,0,204,195,206,156,1,114,4,100,97,116,97,1,33,0,204,195,206,156,1,114,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,114,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,118,122,73,48,69,73,102,97,111,55,0,39,0,204,195,206,156,1,1,10,76,77,51,100,74,90,103,105,119,106,1,40,0,204,195,206,156,1,123,2,105,100,1,119,10,76,77,51,100,74,90,103,105,119,106,40,0,204,195,206,156,1,123,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,204,195,206,156,1,123,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,123,8,99,104,105,108,100,114,101,110,1,119,10,65,49,72,80,70,85,72,104,51,86,40,0,204,195,206,156,1,123,4,100,97,116,97,1,119,2,123,125,40,0,204,195,206,156,1,123,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,120,116,103,85,69,74,52,104,81,95,40,0,204,195,206,156,1,123,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,65,49,72,80,70,85,72,104,51,86,0,39,0,204,195,206,156,1,1,10,109,73,66,54,73,106,49,57,52,77,1,40,0,204,195,206,156,1,132,1,2,105,100,1,119,10,109,73,66,54,73,106,49,57,52,77,40,0,204,195,206,156,1,132,1,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,204,195,206,156,1,132,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,132,1,8,99,104,105,108,100,114,101,110,1,119,10,121,56,100,54,52,108,75,54,81,109,33,0,204,195,206,156,1,132,1,4,100,97,116,97,1,33,0,204,195,206,156,1,132,1,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,132,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,121,56,100,54,52,108,75,54,81,109,0,39,0,204,195,206,156,1,1,10,109,79,82,56,99,51,71,108,104,101,1,40,0,204,195,206,156,1,141,1,2,105,100,1,119,10,109,79,82,56,99,51,71,108,104,101,40,0,204,195,206,156,1,141,1,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,204,195,206,156,1,141,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,141,1,8,99,104,105,108,100,114,101,110,1,119,10,75,50,75,54,117,121,80,56,108,65,40,0,204,195,206,156,1,141,1,4,100,97,116,97,1,119,2,123,125,40,0,204,195,206,156,1,141,1,11,101,120,116,101,114,110,97,108,95,105,100,1,119,10,52,97,84,122,117,113,66,107,110,70,40,0,204,195,206,156,1,141,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,119,4,116,101,120,116,39,0,204,195,206,156,1,3,10,75,50,75,54,117,121,80,56,108,65,0,39,0,204,195,206,156,1,1,10,118,110,69,86,85,50,114,57,65,88,1,40,0,204,195,206,156,1,150,1,2,105,100,1,119,10,118,110,69,86,85,50,114,57,65,88,40,0,204,195,206,156,1,150,1,2,116,121,1,119,4,99,111,100,101,40,0,204,195,206,156,1,150,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,150,1,8,99,104,105,108,100,114,101,110,1,119,10,75,119,115,101,107,79,85,115,115,57,33,0,204,195,206,156,1,150,1,4,100,97,116,97,1,33,0,204,195,206,156,1,150,1,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,150,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,75,119,115,101,107,79,85,115,115,57,0,39,0,204,195,206,156,1,1,10,104,87,121,95,110,110,79,73,101,108,1,40,0,204,195,206,156,1,159,1,2,105,100,1,119,10,104,87,121,95,110,110,79,73,101,108,40,0,204,195,206,156,1,159,1,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,204,195,206,156,1,159,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,159,1,8,99,104,105,108,100,114,101,110,1,119,10,95,74,97,104,108,70,88,117,82,109,33,0,204,195,206,156,1,159,1,4,100,97,116,97,1,33,0,204,195,206,156,1,159,1,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,159,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,95,74,97,104,108,70,88,117,82,109,0,33,0,204,195,206,156,1,1,10,71,45,117,115,79,56,75,107,81,81,1,0,7,33,0,204,195,206,156,1,3,10,56,112,113,84,95,112,120,118,65,78,1,33,0,204,195,206,156,1,1,10,87,114,65,73,121,89,90,76,79,110,1,0,7,33,0,204,195,206,156,1,3,10,89,100,82,106,88,106,109,55,118,114,1,33,0,204,195,206,156,1,1,10,95,90,102,110,119,90,114,87,68,105,1,0,7,33,0,204,195,206,156,1,3,10,49,86,117,68,73,110,45,56,100,114,1,39,0,204,195,206,156,1,1,10,82,50,56,82,106,69,66,70,99,71,1,40,0,204,195,206,156,1,195,1,2,105,100,1,119,10,82,50,56,82,106,69,66,70,99,71,40,0,204,195,206,156,1,195,1,2,116,121,1,119,7,100,105,118,105,100,101,114,40,0,204,195,206,156,1,195,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,195,1,8,99,104,105,108,100,114,101,110,1,119,10,119,115,98,50,74,101,113,52,87,71,40,0,204,195,206,156,1,195,1,4,100,97,116,97,1,119,2,123,125,40,0,204,195,206,156,1,195,1,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,204,195,206,156,1,195,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,10,119,115,98,50,74,101,113,52,87,71,0,39,0,204,195,206,156,1,1,10,109,54,120,76,118,72,89,48,76,107,1,40,0,204,195,206,156,1,204,1,2,105,100,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,204,1,2,116,121,1,119,4,112,97,103,101,40,0,204,195,206,156,1,204,1,6,112,97,114,101,110,116,1,119,0,40,0,204,195,206,156,1,204,1,8,99,104,105,108,100,114,101,110,1,119,10,120,68,48,121,90,73,118,109,51,115,33,0,204,195,206,156,1,204,1,4,100,97,116,97,1,33,0,204,195,206,156,1,204,1,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,204,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,120,68,48,121,90,73,118,109,51,115,0,33,0,204,195,206,156,1,1,10,97,115,74,118,54,70,114,65,82,97,1,0,7,33,0,204,195,206,156,1,3,10,68,75,70,79,99,81,75,54,52,72,1,39,0,204,195,206,156,1,1,10,119,70,86,108,107,88,117,108,104,74,1,40,0,204,195,206,156,1,222,1,2,105,100,1,119,10,119,70,86,108,107,88,117,108,104,74,40,0,204,195,206,156,1,222,1,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,204,195,206,156,1,222,1,6,112,97,114,101,110,116,1,119,10,109,54,120,76,118,72,89,48,76,107,40,0,204,195,206,156,1,222,1,8,99,104,105,108,100,114,101,110,1,119,10,69,113,72,71,75,105,54,115,68,53,33,0,204,195,206,156,1,222,1,4,100,97,116,97,1,33,0,204,195,206,156,1,222,1,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,204,195,206,156,1,222,1,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,204,195,206,156,1,3,10,69,113,72,71,75,105,54,115,68,53,0,8,0,204,195,206,156,1,212,1,1,119,10,119,70,86,108,107,88,117,108,104,74,129,204,195,206,156,1,231,1,1,136,204,195,206,156,1,232,1,6,119,10,109,73,66,54,73,106,49,57,52,77,119,10,78,89,54,108,121,101,57,108,88,51,119,10,68,90,114,95,72,118,106,65,78,107,119,10,113,115,110,89,82,48,74,72,74,56,119,10,55,107,121,57,118,72,100,98,90,90,119,10,77,48,104,84,99,67,120,66,88,82,129,204,195,206,156,1,238,1,1,136,204,195,206,156,1,239,1,1,119,10,82,50,56,82,106,69,66,70,99,71,129,204,195,206,156,1,240,1,1,136,204,195,206,156,1,241,1,1,119,10,104,87,121,95,110,110,79,73,101,108,136,204,195,206,156,1,242,1,2,119,10,48,105,122,109,122,95,86,65,55,70,119,10,101,110,68,45,73,83,100,100,99,55,136,204,195,206,156,1,244,1,2,119,10,109,79,82,56,99,51,71,108,104,101,119,10,118,110,69,86,85,50,114,57,65,88,129,204,195,206,156,1,246,1,1,136,204,195,206,156,1,247,1,3,119,10,75,54,50,76,100,101,119,53,95,121,119,10,78,73,76,105,97,84,121,72,108,112,119,10,101,104,73,115,79,74,69,114,55,73,136,204,195,206,156,1,250,1,1,119,10,117,51,120,66,95,83,69,116,53,68,129,204,195,206,156,1,251,1,1,136,204,195,206,156,1,252,1,1,119,10,76,77,51,100,74,90,103,105,119,106,129,204,195,206,156,1,253,1,1,39,0,204,195,206,156,1,4,10,97,98,100,49,105,117,71,81,109,68,2,4,0,204,195,206,156,1,255,1,44,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,44,32,109,97,114,107,100,111,119,110,44,32,97,110,100,32,99,111,100,101,32,98,108,111,99,107,39,0,204,195,206,156,1,4,10,54,74,108,118,72,71,53,111,120,90,2,4,0,204,195,206,156,1,172,2,20,65,115,32,115,111,111,110,32,97,115,32,121,111,117,32,116,121,112,101,32,134,204,195,206,156,1,192,2,10,102,111,110,116,95,99,111,108,111,114,12,34,48,120,102,102,48,48,98,53,102,102,34,134,204,195,206,156,1,193,2,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,194,2,1,47,134,204,195,206,156,1,195,2,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,134,204,195,206,156,1,196,2,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,197,2,28,32,97,32,109,101,110,117,32,119,105,108,108,32,112,111,112,32,117,112,46,32,83,101,108,101,99,116,32,134,204,195,206,156,1,225,2,8,98,103,95,99,111,108,111,114,12,34,48,120,52,100,57,99,50,55,98,48,34,132,204,195,206,156,1,226,2,15,100,105,102,102,101,114,101,110,116,32,116,121,112,101,115,134,204,195,206,156,1,241,2,8,98,103,95,99,111,108,111,114,4,110,117,108,108,132,204,195,206,156,1,242,2,31,32,111,102,32,99,111,110,116,101,110,116,32,98,108,111,99,107,115,32,121,111,117,32,99,97,110,32,97,100,100,46,39,0,204,195,206,156,1,4,10,51,72,115,115,121,121,66,84,57,50,2,4,0,204,195,206,156,1,146,3,5,84,121,112,101,32,134,204,195,206,156,1,151,3,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,152,3,1,47,134,204,195,206,156,1,153,3,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,154,3,13,32,102,111,108,108,111,119,101,100,32,98,121,32,134,204,195,206,156,1,167,3,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,168,3,7,47,98,117,108,108,101,116,134,204,195,206,156,1,175,3,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,176,3,4,32,111,114,32,134,204,195,206,156,1,180,3,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,181,3,4,47,110,117,109,134,204,195,206,156,1,185,3,4,99,111,100,101,4,110,117,108,108,198,204,195,206,156,1,185,3,204,195,206,156,1,186,3,4,99,111,100,101,5,102,97,108,115,101,196,204,195,206,156,1,187,3,204,195,206,156,1,186,3,18,32,116,111,32,99,114,101,97,116,101,32,97,32,108,105,115,116,46,198,204,195,206,156,1,205,3,204,195,206,156,1,186,3,4,99,111,100,101,4,116,114,117,101,33,0,204,195,206,156,1,4,10,84,82,53,102,106,82,122,115,114,105,1,39,0,204,195,206,156,1,4,10,119,86,82,81,117,71,111,121,116,48,2,4,0,204,195,206,156,1,208,3,6,67,108,105,99,107,32,134,204,195,206,156,1,214,3,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,215,3,1,63,134,204,195,206,156,1,216,3,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,217,3,41,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,114,105,103,104,116,32,102,111,114,32,104,101,108,112,32,97,110,100,32,115,117,112,112,111,114,116,129,204,195,206,156,1,130,4,1,39,0,204,195,206,156,1,4,10,107,106,48,68,49,121,121,88,78,119,2,39,0,204,195,206,156,1,4,10,120,116,103,85,69,74,52,104,81,95,2,39,0,204,195,206,156,1,4,10,112,70,113,76,55,45,79,83,121,86,2,33,0,204,195,206,156,1,4,10,102,114,97,74,99,70,55,54,70,99,1,39,0,204,195,206,156,1,4,10,122,77,121,109,67,97,118,83,107,102,2,4,0,204,195,206,156,1,136,4,6,67,108,105,99,107,32,134,204,195,206,156,1,142,4,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,143,4,11,43,32,78,101,119,32,80,97,103,101,32,134,204,195,206,156,1,154,4,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,155,4,50,98,117,116,116,111,110,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,111,102,32,121,111,117,114,32,115,105,100,101,98,97,114,32,116,111,32,97,100,100,32,97,32,110,101,119,32,129,204,195,206,156,1,205,4,4,132,204,195,206,156,1,209,4,1,46,39,0,204,195,206,156,1,4,10,72,116,114,88,117,57,102,65,95,107,2,4,0,204,195,206,156,1,211,4,18,72,97,118,101,32,97,32,113,117,101,115,116,105,111,110,226,157,147,39,0,204,195,206,156,1,4,10,49,112,115,100,67,122,97,87,104,49,2,4,0,204,195,206,156,1,228,4,30,47,47,32,84,104,105,115,32,105,115,32,116,104,101,32,109,97,105,110,32,102,117,110,99,116,105,111,110,46,10,129,204,195,206,156,1,130,5,77,39,0,204,195,206,156,1,4,10,119,79,108,117,99,85,55,51,73,76,2,1,0,204,195,206,156,1,208,5,36,129,204,195,206,156,1,244,5,1,33,0,204,195,206,156,1,4,10,69,72,117,95,67,112,120,53,67,103,1,39,0,204,195,206,156,1,4,10,98,113,76,109,98,57,111,45,109,109,2,4,0,204,195,206,156,1,247,5,6,67,108,105,99,107,32,134,204,195,206,156,1,253,5,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,254,5,1,43,134,204,195,206,156,1,255,5,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,128,6,1,32,129,204,195,206,156,1,129,6,4,132,204,195,206,156,1,133,6,37,32,116,111,32,97,110,121,32,112,97,103,101,32,116,105,116,108,101,32,105,110,32,116,104,101,32,115,105,100,101,98,97,114,32,116,111,32,134,204,195,206,156,1,170,6,10,102,111,110,116,95,99,111,108,111,114,12,34,48,120,102,102,56,52,50,55,101,48,34,132,204,195,206,156,1,171,6,7,113,117,105,99,107,108,121,134,204,195,206,156,1,178,6,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,132,204,195,206,156,1,179,6,1,32,129,204,195,206,156,1,180,6,3,132,204,195,206,156,1,183,6,16,32,97,32,110,101,119,32,115,117,98,112,97,103,101,44,32,134,204,195,206,156,1,199,6,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,200,6,8,68,111,99,117,109,101,110,116,134,204,195,206,156,1,208,6,4,99,111,100,101,4,110,117,108,108,198,204,195,206,156,1,208,6,204,195,206,156,1,209,6,4,99,111,100,101,5,102,97,108,115,101,196,204,195,206,156,1,210,6,204,195,206,156,1,209,6,2,44,32,198,204,195,206,156,1,212,6,204,195,206,156,1,209,6,4,99,111,100,101,4,116,114,117,101,196,204,195,206,156,1,213,6,204,195,206,156,1,209,6,4,71,114,105,100,198,204,195,206,156,1,217,6,204,195,206,156,1,209,6,4,99,111,100,101,5,102,97,108,115,101,196,204,195,206,156,1,218,6,204,195,206,156,1,209,6,5,44,32,111,114,32,198,204,195,206,156,1,223,6,204,195,206,156,1,209,6,4,99,111,100,101,4,116,114,117,101,196,204,195,206,156,1,224,6,204,195,206,156,1,209,6,12,75,97,110,98,97,110,32,66,111,97,114,100,198,204,195,206,156,1,236,6,204,195,206,156,1,209,6,4,99,111,100,101,5,102,97,108,115,101,196,204,195,206,156,1,237,6,204,195,206,156,1,209,6,1,46,198,204,195,206,156,1,238,6,204,195,206,156,1,209,6,4,99,111,100,101,4,116,114,117,101,39,0,204,195,206,156,1,4,10,102,56,54,108,88,117,88,74,101,54,2,4,0,204,195,206,156,1,240,6,9,77,97,114,107,100,111,119,110,32,134,204,195,206,156,1,249,6,4,104,114,101,102,67,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,109,97,114,107,100,111,119,110,34,132,204,195,206,156,1,250,6,9,114,101,102,101,114,101,110,99,101,134,204,195,206,156,1,131,7,4,104,114,101,102,4,110,117,108,108,33,0,204,195,206,156,1,4,10,89,74,119,52,70,81,88,106,110,84,1,39,0,204,195,206,156,1,4,10,119,88,107,79,72,81,49,50,99,111,2,1,0,204,195,206,156,1,134,7,20,33,0,204,195,206,156,1,4,10,65,108,73,86,97,121,54,119,80,104,1,0,19,39,0,204,195,206,156,1,4,10,109,69,119,56,90,66,102,95,100,68,2,6,0,204,195,206,156,1,175,7,8,98,103,95,99,111,108,111,114,12,34,48,120,52,100,102,102,101,98,51,98,34,132,204,195,206,156,1,176,7,10,72,105,103,104,108,105,103,104,116,32,134,204,195,206,156,1,186,7,8,98,103,95,99,111,108,111,114,4,110,117,108,108,132,204,195,206,156,1,187,7,38,97,110,121,32,116,101,120,116,44,32,97,110,100,32,117,115,101,32,116,104,101,32,101,100,105,116,105,110,103,32,109,101,110,117,32,116,111,32,134,204,195,206,156,1,225,7,6,105,116,97,108,105,99,4,116,114,117,101,132,204,195,206,156,1,226,7,5,115,116,121,108,101,134,204,195,206,156,1,231,7,6,105,116,97,108,105,99,4,110,117,108,108,132,204,195,206,156,1,232,7,1,32,134,204,195,206,156,1,233,7,4,98,111,108,100,4,116,114,117,101,132,204,195,206,156,1,234,7,4,121,111,117,114,134,204,195,206,156,1,238,7,4,98,111,108,100,4,110,117,108,108,132,204,195,206,156,1,239,7,1,32,134,204,195,206,156,1,240,7,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,132,204,195,206,156,1,241,7,7,119,114,105,116,105,110,103,134,204,195,206,156,1,248,7,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,132,204,195,206,156,1,249,7,1,32,134,204,195,206,156,1,250,7,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,251,7,7,104,111,119,101,118,101,114,134,204,195,206,156,1,130,8,4,99,111,100,101,4,110,117,108,108,132,204,195,206,156,1,131,8,5,32,121,111,117,32,134,204,195,206,156,1,136,8,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,132,204,195,206,156,1,137,8,5,108,105,107,101,46,134,204,195,206,156,1,142,8,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,33,0,204,195,206,156,1,4,10,98,122,103,70,79,75,118,99,117,89,1,39,0,204,195,206,156,1,4,10,52,97,84,122,117,113,66,107,110,70,2,4,0,204,195,206,156,1,145,8,5,84,121,112,101,32,134,204,195,206,156,1,150,8,4,99,111,100,101,4,116,114,117,101,132,204,195,206,156,1,151,8,5,47,99,111,100,101,134,204,195,206,156,1,156,8,4,99,111,100,101,4,110,117,108,108,198,204,195,206,156,1,156,8,204,195,206,156,1,157,8,4,99,111,100,101,5,102,97,108,115,101,196,204,195,206,156,1,158,8,204,195,206,156,1,157,8,23,32,116,111,32,105,110,115,101,114,116,32,97,32,99,111,100,101,32,98,108,111,99,107,198,204,195,206,156,1,181,8,204,195,206,156,1,157,8,4,99,111,100,101,4,116,114,117,101,39,0,204,195,206,156,1,4,10,108,119,101,104,75,79,117,78,68,67,2,4,0,204,195,206,156,1,183,8,27,10,76,105,107,101,32,65,112,112,70,108,111,119,121,63,32,70,111,108,108,111,119,32,117,115,58,10,134,204,195,206,156,1,210,8,4,104,114,101,102,41,34,104,116,116,112,115,58,47,47,103,105,116,104,117,98,46,99,111,109,47,65,112,112,70,108,111,119,121,45,73,79,47,65,112,112,70,108,111,119,121,34,132,204,195,206,156,1,211,8,6,71,105,116,72,117,98,134,204,195,206,156,1,217,8,4,104,114,101,102,4,110,117,108,108,132,204,195,206,156,1,218,8,1,10,134,204,195,206,156,1,219,8,4,104,114,101,102,30,34,104,116,116,112,115,58,47,47,116,119,105,116,116,101,114,46,99,111,109,47,97,112,112,102,108,111,119,121,34,132,204,195,206,156,1,220,8,7,84,119,105,116,116,101,114,134,204,195,206,156,1,227,8,4,104,114,101,102,4,110,117,108,108,132,204,195,206,156,1,228,8,12,58,32,64,97,112,112,102,108,111,119,121,10,134,204,195,206,156,1,240,8,4,104,114,101,102,33,34,104,116,116,112,115,58,47,47,98,108,111,103,45,97,112,112,102,108,111,119,121,46,103,104,111,115,116,46,105,111,47,34,132,204,195,206,156,1,241,8,10,78,101,119,115,108,101,116,116,101,114,134,204,195,206,156,1,251,8,4,104,114,101,102,4,110,117,108,108,132,204,195,206,156,1,252,8,1,10,39,0,204,195,206,156,1,4,10,109,73,73,113,81,111,118,74,105,101,2,4,0,204,195,206,156,1,254,8,19,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,32,134,204,195,206,156,1,145,9,4,104,114,101,102,68,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,115,104,111,114,116,99,117,116,115,34,132,204,195,206,156,1,146,9,5,103,117,105,100,101,134,204,195,206,156,1,151,9,4,104,114,101,102,4,110,117,108,108,2,131,159,159,151,1,0,161,237,140,187,206,2,16,1,161,237,140,187,206,2,20,71,1,141,178,210,127,0,0,3,1,206,214,243,86,0,161,236,158,128,159,2,3,178,1,62,194,228,144,71,0,161,243,138,171,183,10,246,1,1,161,243,138,171,183,10,247,1,1,161,243,138,171,183,10,248,1,1,161,131,128,202,229,9,0,1,161,194,228,144,71,0,1,161,194,228,144,71,1,1,161,194,228,144,71,2,1,161,194,228,144,71,3,1,39,0,204,195,206,156,1,4,6,114,114,111,103,100,98,2,33,0,204,195,206,156,1,1,6,85,95,66,110,68,101,1,0,7,33,0,204,195,206,156,1,3,6,79,70,89,50,114,113,1,193,199,130,209,189,2,174,5,199,130,209,189,2,210,5,1,1,0,194,228,144,71,8,1,0,1,129,194,228,144,71,19,1,0,3,39,0,204,195,206,156,1,4,6,103,109,54,79,74,117,2,33,0,204,195,206,156,1,1,6,114,78,78,121,56,74,1,0,7,33,0,204,195,206,156,1,3,6,88,101,115,97,82,119,1,193,199,130,209,189,2,174,5,194,228,144,71,18,1,4,0,194,228,144,71,25,1,62,0,1,39,0,204,195,206,156,1,4,6,99,82,86,69,118,53,2,39,0,204,195,206,156,1,1,6,109,55,85,85,85,68,1,40,0,194,228,144,71,39,2,105,100,1,119,6,109,55,85,85,85,68,40,0,194,228,144,71,39,2,116,121,1,119,11,116,111,103,103,108,101,95,108,105,115,116,40,0,194,228,144,71,39,6,112,97,114,101,110,116,1,119,6,78,99,104,45,81,78,40,0,194,228,144,71,39,8,99,104,105,108,100,114,101,110,1,119,6,120,53,79,107,74,71,33,0,194,228,144,71,39,4,100,97,116,97,1,40,0,194,228,144,71,39,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,194,228,144,71,39,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,120,53,79,107,74,71,0,200,199,130,209,189,2,174,5,194,228,144,71,35,1,119,6,109,55,85,85,85,68,4,0,194,228,144,71,38,1,49,161,194,228,144,71,44,1,132,194,228,144,71,49,1,50,161,194,228,144,71,50,1,132,194,228,144,71,51,1,51,161,194,228,144,71,52,1,39,0,204,195,206,156,1,4,6,105,72,102,106,109,56,2,39,0,204,195,206,156,1,1,6,73,121,89,76,77,104,1,40,0,194,228,144,71,56,2,105,100,1,119,6,73,121,89,76,77,104,40,0,194,228,144,71,56,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,194,228,144,71,56,6,112,97,114,101,110,116,1,119,6,109,55,85,85,85,68,40,0,194,228,144,71,56,8,99,104,105,108,100,114,101,110,1,119,6,79,76,111,88,102,98,33,0,194,228,144,71,56,4,100,97,116,97,1,40,0,194,228,144,71,56,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,194,228,144,71,56,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,204,195,206,156,1,3,6,79,76,111,88,102,98,0,8,0,194,228,144,71,47,1,119,6,73,121,89,76,77,104,161,194,228,144,71,54,1,4,0,194,228,144,71,55,1,52,161,194,228,144,71,61,1,132,194,228,144,71,67,1,52,161,194,228,144,71,68,1,132,194,228,144,71,69,1,52,161,194,228,144,71,70,1,132,194,228,144,71,71,1,52,168,194,228,144,71,72,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,52,52,52,52,34,125,93,125,168,194,228,144,71,66,1,119,45,123,34,99,111,108,108,97,112,115,101,100,34,58,116,114,117,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,125,1,185,164,169,62,0,161,198,234,131,228,11,49,90,1,229,154,194,35,0,161,136,172,186,168,4,181,6,178,1,1,218,255,204,32,0,161,150,152,188,203,6,19,21,1,183,182,135,14,0,161,207,210,187,205,12,7,227,2,73,131,159,159,151,1,1,0,72,132,236,218,251,9,1,0,14,131,182,180,202,12,1,0,53,131,128,202,229,9,1,0,1,133,181,204,218,3,8,15,1,17,2,20,1,22,1,24,1,26,1,28,1,30,1,136,172,186,168,4,1,0,182,6,136,199,176,231,9,1,20,10,140,167,201,161,14,1,0,10,141,151,160,163,4,1,0,24,142,211,188,164,13,1,0,15,141,178,210,127,1,0,3,145,224,235,133,7,1,0,3,146,209,153,247,13,1,0,186,1,146,216,250,133,2,1,0,180,1,146,175,139,236,2,1,0,12,150,152,188,203,6,1,0,20,151,234,142,238,11,1,0,27,150,216,171,142,3,87,0,171,3,172,3,3,176,3,3,180,3,3,184,3,3,188,3,3,192,3,3,196,3,3,200,3,3,204,3,3,208,3,3,212,3,3,216,3,3,220,3,3,224,3,3,228,3,3,232,3,3,236,3,3,240,3,3,244,3,3,248,3,3,253,3,3,129,4,3,133,4,3,137,4,3,141,4,3,145,4,3,149,4,3,153,4,3,157,4,3,161,4,3,165,4,3,169,4,3,173,4,3,177,4,3,181,4,3,185,4,3,189,4,3,193,4,3,197,4,3,201,4,3,205,4,3,209,4,3,213,4,3,217,4,3,221,4,3,225,4,3,229,4,7,254,4,10,138,5,1,140,5,1,142,5,1,149,5,1,155,5,1,157,5,1,159,5,1,166,5,11,178,5,15,200,5,1,210,5,1,220,5,1,230,5,1,235,5,1,237,5,1,239,5,1,241,5,1,245,5,1,251,5,1,133,6,1,138,6,1,144,6,1,154,6,1,164,6,1,174,6,1,180,6,1,182,6,1,184,6,1,186,6,5,216,6,3,231,6,14,188,7,6,158,8,1,160,8,1,162,8,1,164,8,3,168,8,3,187,8,1,153,236,182,220,1,1,0,4,151,254,242,152,9,11,5,8,14,1,17,1,19,3,32,1,37,16,54,23,89,2,97,1,102,21,134,1,8,155,213,159,176,1,1,0,10,161,234,157,145,5,1,0,7,164,202,219,213,10,18,19,10,31,10,42,1,44,10,55,1,57,1,59,1,61,3,67,1,77,7,85,1,87,1,89,1,91,1,93,1,95,1,97,1,117,1,165,131,171,211,15,1,0,20,168,215,223,235,2,1,0,3,171,236,222,251,5,69,9,5,15,11,27,8,36,3,40,4,45,8,54,8,63,3,72,3,76,3,80,3,84,3,88,3,92,3,96,3,102,3,106,10,120,10,131,1,10,142,1,10,153,1,10,169,1,1,176,1,2,179,1,1,181,1,1,187,1,10,201,1,1,207,1,10,222,1,10,237,1,17,131,2,10,146,2,10,166,2,1,172,2,10,186,2,1,192,2,10,207,2,10,222,2,10,237,2,10,252,2,10,139,3,10,150,3,18,169,3,15,185,3,1,202,3,1,208,3,1,210,3,1,212,3,1,240,3,1,245,3,10,128,4,4,137,4,1,142,4,7,150,4,6,157,4,6,164,4,6,171,4,6,178,4,6,185,4,6,192,4,6,199,4,1,201,4,4,206,4,7,214,4,6,221,4,6,228,4,6,235,4,6,242,4,1,249,4,1,172,254,181,239,1,4,5,1,8,1,11,1,14,1,174,203,157,214,7,1,0,6,176,238,158,139,14,1,0,175,2,177,239,218,225,4,1,0,3,178,187,245,161,14,2,8,1,10,1,180,189,170,253,8,2,6,1,10,1,181,150,190,222,14,15,1,8,10,10,21,10,32,10,59,1,65,1,67,1,69,1,71,1,73,1,80,1,85,1,87,1,89,1,91,1,181,156,253,158,6,1,0,4,183,182,135,14,1,0,227,2,184,146,243,216,14,1,0,7,185,164,169,62,1,0,90,183,213,134,255,8,1,0,28,190,183,139,210,2,1,0,110,192,246,139,213,2,1,0,35,192,187,174,206,8,3,1,74,76,220,3,222,5,1,194,228,144,71,13,0,8,9,16,26,10,37,1,44,1,50,1,52,1,54,1,61,1,66,1,68,1,70,1,72,1,195,254,251,180,11,1,0,58,197,205,192,233,12,1,0,9,198,223,206,159,1,25,15,3,19,20,40,10,51,10,62,10,78,1,86,6,93,13,112,1,120,1,124,1,126,13,145,1,1,153,1,1,159,1,1,170,1,1,175,1,1,177,1,10,193,1,1,203,1,10,219,1,1,230,1,10,246,1,1,253,1,1,135,2,1,198,234,131,228,11,1,0,50,199,130,209,189,2,203,1,4,11,16,1,19,12,33,1,35,1,37,1,39,1,41,1,43,1,45,1,47,1,49,1,56,1,61,1,63,1,65,1,67,1,69,1,71,1,73,1,75,1,77,1,79,1,81,1,83,1,85,1,87,1,89,1,91,1,93,1,95,1,102,1,107,1,109,1,111,1,113,1,115,1,117,1,119,1,121,1,123,1,125,4,136,1,1,144,1,1,152,1,1,160,1,1,168,1,1,176,1,1,184,1,1,192,1,1,200,1,1,208,1,1,216,1,1,224,1,1,232,1,1,240,1,1,248,1,1,128,2,1,136,2,1,144,2,1,152,2,1,160,2,1,168,2,1,176,2,1,184,2,1,192,2,1,200,2,2,208,2,1,213,2,1,215,2,27,246,2,21,140,3,1,142,3,1,144,3,1,146,3,1,153,3,1,162,3,10,177,3,1,183,3,10,202,3,1,212,3,1,222,3,1,232,3,1,242,3,1,252,3,1,134,4,1,144,4,1,154,4,1,159,4,1,161,4,1,163,4,1,165,4,1,167,4,1,169,4,1,171,4,1,173,4,1,175,4,1,177,4,5,188,4,1,193,4,6,200,4,10,216,4,1,221,4,6,228,4,10,244,4,1,249,4,6,133,5,1,138,5,6,145,5,10,156,5,1,158,5,1,160,5,1,162,5,3,170,5,1,175,5,6,182,5,16,199,5,1,206,5,1,211,5,6,223,5,1,228,5,7,236,5,10,252,5,1,129,6,6,136,6,10,147,6,10,158,6,1,160,6,1,162,6,1,167,6,10,178,6,1,180,6,2,183,6,2,186,6,1,188,6,1,190,6,1,192,6,1,194,6,1,196,6,1,198,6,1,200,6,1,202,6,1,206,6,1,208,6,1,210,6,1,212,6,1,214,6,4,219,6,1,223,6,1,225,6,1,227,6,1,229,6,1,231,6,1,233,6,1,235,6,1,237,6,1,241,6,1,243,6,1,245,6,1,247,6,1,249,6,1,251,6,1,253,6,1,255,6,1,131,7,1,133,7,1,135,7,1,137,7,1,139,7,1,141,7,1,144,7,21,166,7,1,168,7,1,170,7,1,172,7,1,174,7,1,176,7,1,178,7,1,180,7,1,182,7,1,184,7,1,188,7,1,190,7,1,192,7,1,194,7,1,196,7,1,198,7,4,205,7,1,207,7,1,209,7,1,211,7,1,213,7,1,215,7,1,217,7,13,231,7,1,240,7,1,248,7,2,252,7,3,130,8,1,132,8,1,134,8,1,136,8,2,139,8,2,204,195,206,156,1,30,11,3,56,3,60,9,83,3,96,9,119,3,137,1,3,155,1,3,164,1,3,168,1,27,209,1,3,213,1,9,227,1,3,232,1,1,239,1,1,241,1,1,247,1,1,252,1,1,254,1,1,207,3,1,131,4,1,135,4,1,206,4,4,131,5,77,209,5,38,130,6,4,181,6,3,133,7,1,135,7,40,144,8,1,206,214,243,86,1,0,178,1,207,210,187,205,12,1,0,8,208,203,223,226,9,1,0,81,207,231,154,196,9,1,0,3,217,168,198,159,4,1,0,7,218,255,204,32,1,0,21,219,200,174,197,9,1,0,25,220,225,223,240,3,8,0,4,7,3,11,24,41,1,46,2,51,1,53,3,59,1,223,215,172,155,15,1,0,5,224,159,166,178,15,1,0,30,226,167,254,250,5,3,8,1,10,1,12,1,227,211,144,195,8,1,0,12,228,242,134,215,15,4,5,1,7,1,9,1,11,1,229,154,194,35,1,0,178,1,226,235,133,189,11,1,0,7,236,158,128,159,2,1,0,4,237,140,187,206,2,1,0,21,236,253,128,205,3,1,0,9,239,239,208,251,10,1,0,17,240,179,157,219,7,1,0,4,241,147,239,232,6,1,0,4,238,153,239,204,9,7,0,3,4,3,8,3,30,1,32,1,46,1,48,1,243,138,171,183,10,1,0,252,1,245,181,155,135,2,1,0,23,247,212,219,208,10,1,0,46],"version":0,"object_id":"26d5c8c1-1c66-459c-bc6c-f4da1a663348"},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json b/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json new file mode 100644 index 0000000000..0679311668 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/sign_in_success.json @@ -0,0 +1,66 @@ +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTI4Mjk2MjAsImlhdCI6MTcxMjgyNjAyMCwic3ViIjoiY2JmZjA2MGEtMTk2ZC00MTVhLWFhODAtNzU5YzAxODg2NDY2IiwiZW1haWwiOiJsdUBhcHBmbG93eS5pbyIsInBob25lIjoiIiwiYXBwX21ldGFkYXRhIjp7InByb3ZpZGVyIjoiZ29vZ2xlIiwicHJvdmlkZXJzIjpbImdvb2dsZSJdfSwidXNlcl9tZXRhZGF0YSI6eyJhdmF0YXJfdXJsIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jTEhabVZBczRTb0ZlVFFuWG5CU2JiNTBBVXF0YktHNWx5MGllVHZCSklYZ1o3UmdRPXM5Ni1jIiwiY3VzdG9tX2NsYWltcyI6eyJoZCI6ImFwcGZsb3d5LmlvIn0sImVtYWlsIjoibHVAYXBwZmxvd3kuaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZnVsbF9uYW1lIjoiTHUgSGUiLCJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJuYW1lIjoiTHUgSGUiLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NMSFptVkFzNFNvRmVUUW5YbkJTYmI1MEFVcXRiS0c1bHkwaWVUdkJKSVhnWjdSZ1E9czk2LWMiLCJwcm92aWRlcl9pZCI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSIsInN1YiI6IjEwMTE2OTI1MDgyOTU1NDAyODM4MSJ9LCJyb2xlIjoiIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3MTI4MjYwMjB9XSwic2Vzc2lvbl9pZCI6ImJmMzE5OTRlLTQwMTgtNDhjMS05Yzc0LWVmYzkyMGNjOWQ0NSJ9.QeTrRhsnBjBL1GUS3TIWOgU1SPM6RcaWwxZdMVfcFBU", + "token_type": "bearer", + "expires_in": 3600, + "expires_at": 4869016461, + "refresh_token": "71vp1jJnSAVluZKaXkhG1A", + "user": { + "id": "cbff060a-196d-415a-aa80-759c01886466", + "aud": "", + "role": "", + "email": "lu@appflowy.io", + "email_confirmed_at": "2024-03-13T10:49:53.165361Z", + "phone": "", + "confirmed_at": "2024-03-13T10:49:53.165361Z", + "last_sign_in_at": "2024-04-11T09:00:20.547468985Z", + "app_metadata": { + "provider": "google", + "providers": [ + "google" + ] + }, + "user_metadata": { + "avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", + "custom_claims": { + "hd": "appflowy.io" + }, + "email": "lu@appflowy.io", + "email_verified": true, + "full_name": "Lu He", + "iss": "https://accounts.google.com", + "name": "Lu He", + "phone_verified": false, + "picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", + "provider_id": "101169250829554028381", + "sub": "101169250829554028381" + }, + "identities": [ + { + "identity_id": "e4cf8b69-7f80-42e9-aed2-e25132ad0178", + "id": "101169250829554028381", + "user_id": "cbff060a-196d-415a-aa80-759c01886466", + "identity_data": { + "avatar_url": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", + "custom_claims": { + "hd": "appflowy.io" + }, + "email": "lu@appflowy.io", + "email_verified": true, + "full_name": "Lu He", + "iss": "https://accounts.google.com", + "name": "Lu He", + "phone_verified": false, + "picture": "https://lh3.googleusercontent.com/a/ACg8ocLHZmVAs4SoFeTQnXnBSbb50AUqtbKG5ly0ieTvBJIXgZ7RgQ=s96-c", + "provider_id": "101169250829554028381", + "sub": "101169250829554028381" + }, + "provider": "google", + "last_sign_in_at": "2024-03-13T07:22:43.110504Z", + "created_at": "2024-03-13T07:22:43.110543Z", + "updated_at": "2024-04-04T06:15:14.03093Z" + } + ], + "created_at": "2024-03-13T07:22:43.102586Z", + "updated_at": "2024-04-11T09:00:20.551485Z" + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/simple_doc.json b/frontend/appflowy_web_app/cypress/fixtures/simple_doc.json new file mode 100644 index 0000000000..97bd9c99b5 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/simple_doc.json @@ -0,0 +1 @@ +{"data":{"state_vector":[5,200,244,136,224,7,3,178,246,186,209,6,72,147,128,159,145,14,26,195,133,217,18,167,13,156,139,194,87,4],"doc_state":[5,26,147,128,159,145,14,0,39,1,4,100,97,116,97,8,100,111,99,117,109,101,110,116,1,39,0,147,128,159,145,14,0,6,98,108,111,99,107,115,1,39,0,147,128,159,145,14,0,4,109,101,116,97,1,39,0,147,128,159,145,14,2,12,99,104,105,108,100,114,101,110,95,109,97,112,1,39,0,147,128,159,145,14,2,8,116,101,120,116,95,109,97,112,1,40,0,147,128,159,145,14,0,7,112,97,103,101,95,105,100,1,119,10,85,86,79,107,81,88,110,117,86,114,39,0,147,128,159,145,14,1,10,51,77,108,48,104,78,110,102,79,82,1,40,0,147,128,159,145,14,6,2,105,100,1,119,10,51,77,108,48,104,78,110,102,79,82,40,0,147,128,159,145,14,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,147,128,159,145,14,6,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,147,128,159,145,14,6,8,99,104,105,108,100,114,101,110,1,119,10,73,121,84,67,107,48,105,52,113,114,33,0,147,128,159,145,14,6,4,100,97,116,97,1,33,0,147,128,159,145,14,6,11,101,120,116,101,114,110,97,108,95,105,100,1,33,0,147,128,159,145,14,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,39,0,147,128,159,145,14,3,10,73,121,84,67,107,48,105,52,113,114,0,39,0,147,128,159,145,14,1,10,85,86,79,107,81,88,110,117,86,114,1,40,0,147,128,159,145,14,15,2,105,100,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,147,128,159,145,14,15,2,116,121,1,119,4,112,97,103,101,40,0,147,128,159,145,14,15,6,112,97,114,101,110,116,1,119,0,40,0,147,128,159,145,14,15,8,99,104,105,108,100,114,101,110,1,119,10,67,102,118,66,115,66,84,122,83,105,40,0,147,128,159,145,14,15,4,100,97,116,97,1,119,2,123,125,40,0,147,128,159,145,14,15,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,147,128,159,145,14,15,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,10,67,102,118,66,115,66,84,122,83,105,0,8,0,147,128,159,145,14,23,1,119,10,51,77,108,48,104,78,110,102,79,82,39,0,147,128,159,145,14,4,10,84,97,119,48,120,69,66,121,65,83,2,1,200,244,136,224,7,0,33,1,4,109,101,116,97,12,108,97,115,116,95,115,121,110,99,95,97,116,3,1,178,246,186,209,6,0,161,200,244,136,224,7,2,72,2,156,139,194,87,0,161,178,246,186,209,6,71,3,168,156,139,194,87,2,1,122,0,0,0,0,102,34,168,95,251,5,195,133,217,18,0,4,0,147,128,159,145,14,25,2,85,73,168,147,128,159,145,14,11,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,85,73,34,125,93,125,168,147,128,159,145,14,12,1,119,10,84,97,119,48,120,69,66,121,65,83,168,147,128,159,145,14,13,1,119,4,116,101,120,116,39,0,147,128,159,145,14,4,6,120,52,56,106,57,65,2,39,0,147,128,159,145,14,1,6,53,77,89,104,51,105,1,40,0,195,133,217,18,6,2,105,100,1,119,6,53,77,89,104,51,105,40,0,195,133,217,18,6,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,6,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,6,8,99,104,105,108,100,114,101,110,1,119,6,75,79,49,111,105,114,33,0,195,133,217,18,6,4,100,97,116,97,1,40,0,195,133,217,18,6,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,6,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,75,79,49,111,105,114,0,136,147,128,159,145,14,24,1,119,6,53,77,89,104,51,105,4,0,195,133,217,18,5,3,111,111,111,168,195,133,217,18,11,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,111,111,111,34,125,93,125,39,0,147,128,159,145,14,4,6,73,72,56,87,97,118,2,33,0,147,128,159,145,14,1,6,85,81,88,116,105,65,1,0,7,33,0,147,128,159,145,14,3,6,65,122,48,88,110,77,1,129,195,133,217,18,15,1,39,0,147,128,159,145,14,4,6,82,122,107,73,79,49,2,39,0,147,128,159,145,14,1,6,69,79,113,57,79,119,1,40,0,195,133,217,18,32,2,105,100,1,119,6,69,79,113,57,79,119,40,0,195,133,217,18,32,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,195,133,217,18,32,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,32,8,99,104,105,108,100,114,101,110,1,119,6,105,80,115,106,50,65,33,0,195,133,217,18,32,4,100,97,116,97,1,40,0,195,133,217,18,32,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,32,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,105,80,115,106,50,65,0,200,195,133,217,18,15,195,133,217,18,30,1,119,6,69,79,113,57,79,119,4,0,195,133,217,18,31,6,232,191,155,233,151,168,161,195,133,217,18,37,1,39,0,147,128,159,145,14,4,6,95,56,78,114,97,97,2,39,0,147,128,159,145,14,1,6,86,73,50,122,54,78,1,40,0,195,133,217,18,46,2,105,100,1,119,6,86,73,50,122,54,78,40,0,195,133,217,18,46,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,195,133,217,18,46,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,46,8,99,104,105,108,100,114,101,110,1,119,6,56,118,75,112,112,71,33,0,195,133,217,18,46,4,100,97,116,97,1,40,0,195,133,217,18,46,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,46,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,56,118,75,112,112,71,0,136,195,133,217,18,30,1,119,6,86,73,50,122,54,78,168,195,133,217,18,44,1,119,47,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,232,191,155,233,151,168,34,125,93,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,4,0,195,133,217,18,45,9,229,147,136,229,147,136,229,147,136,161,195,133,217,18,51,1,39,0,147,128,159,145,14,4,6,82,119,103,79,71,104,2,33,0,147,128,159,145,14,1,6,57,82,83,84,76,77,1,0,7,33,0,147,128,159,145,14,3,6,51,85,73,116,84,78,1,129,195,133,217,18,55,1,168,195,133,217,18,60,1,119,50,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,147,136,229,147,136,229,147,136,34,125,93,44,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,125,39,0,147,128,159,145,14,4,6,114,77,45,67,67,70,2,39,0,147,128,159,145,14,1,6,112,116,116,106,121,52,1,40,0,195,133,217,18,74,2,105,100,1,119,6,112,116,116,106,121,52,40,0,195,133,217,18,74,2,116,121,1,119,9,116,111,100,111,95,108,105,115,116,40,0,195,133,217,18,74,6,112,97,114,101,110,116,1,119,6,86,73,50,122,54,78,40,0,195,133,217,18,74,8,99,104,105,108,100,114,101,110,1,119,6,110,57,101,88,110,75,33,0,195,133,217,18,74,4,100,97,116,97,1,40,0,195,133,217,18,74,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,74,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,110,57,101,88,110,75,0,8,0,195,133,217,18,54,1,119,6,112,116,116,106,121,52,4,0,195,133,217,18,73,12,229,129,165,229,186,183,233,130,163,232,190,185,161,195,133,217,18,79,1,39,0,147,128,159,145,14,4,6,79,86,73,85,113,85,2,33,0,147,128,159,145,14,1,6,55,90,111,73,74,109,1,0,7,33,0,147,128,159,145,14,3,6,117,57,107,50,68,78,1,129,195,133,217,18,71,1,39,0,147,128,159,145,14,4,6,85,111,95,84,114,107,2,4,0,195,133,217,18,100,11,49,50,51,32,36,32,32,101,114,32,32,39,0,147,128,159,145,14,4,6,99,102,56,95,106,100,2,4,0,195,133,217,18,112,3,49,50,51,39,0,147,128,159,145,14,4,6,53,87,72,110,88,75,2,4,0,195,133,217,18,116,19,99,104,101,99,107,101,100,32,116,111,100,111,32,108,105,115,116,32,36,39,0,147,128,159,145,14,4,6,45,107,95,95,66,113,2,39,0,147,128,159,145,14,4,6,95,67,82,55,75,53,2,4,0,195,133,217,18,137,1,180,1,108,111,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,76,101,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,39,0,147,128,159,145,14,4,6,122,79,53,86,113,78,2,39,0,147,128,159,145,14,4,6,50,51,68,78,97,74,2,4,0,195,133,217,18,191,2,4,119,105,116,104,39,0,147,128,159,145,14,4,6,95,56,114,66,75,71,2,39,0,147,128,159,145,14,4,6,67,54,45,78,116,106,2,4,0,195,133,217,18,197,2,11,229,144,140,228,184,128,228,184,170,110,105,39,0,147,128,159,145,14,4,6,120,83,103,122,75,55,2,4,0,195,133,217,18,203,2,55,67,108,105,99,107,32,97,110,121,119,104,101,114,101,32,97,110,100,32,106,117,115,116,32,115,116,97,114,116,32,116,121,112,105,110,103,229,147,136,229,147,136,229,147,136,46,229,176,177,229,135,160,229,174,182,39,0,147,128,159,145,14,4,6,53,118,101,50,119,103,2,39,0,147,128,159,145,14,4,6,84,86,66,74,86,72,2,6,0,195,133,217,18,248,2,8,98,103,95,99,111,108,111,114,12,34,48,120,98,51,102,102,101,98,51,98,34,132,195,133,217,18,249,2,10,72,105,103,104,108,105,103,104,116,32,134,195,133,217,18,131,3,8,98,103,95,99,111,108,111,114,4,110,117,108,108,132,195,133,217,18,132,3,38,97,110,121,32,116,101,120,116,44,32,97,110,100,32,117,115,101,32,116,104,101,32,101,100,105,116,105,110,103,32,109,101,110,117,32,116,111,32,134,195,133,217,18,170,3,6,105,116,97,108,105,99,4,116,114,117,101,132,195,133,217,18,171,3,5,115,116,121,108,101,134,195,133,217,18,176,3,6,105,116,97,108,105,99,4,110,117,108,108,132,195,133,217,18,177,3,1,32,134,195,133,217,18,178,3,4,98,111,108,100,4,116,114,117,101,132,195,133,217,18,179,3,4,121,111,117,114,134,195,133,217,18,183,3,4,98,111,108,100,4,110,117,108,108,132,195,133,217,18,184,3,1,32,134,195,133,217,18,185,3,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,132,195,133,217,18,186,3,7,119,114,105,116,105,110,103,134,195,133,217,18,193,3,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,132,195,133,217,18,194,3,1,32,134,195,133,217,18,195,3,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,196,3,7,104,111,119,101,118,101,114,134,195,133,217,18,203,3,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,204,3,5,32,121,111,117,32,134,195,133,217,18,209,3,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,132,195,133,217,18,210,3,5,108,105,107,101,46,134,195,133,217,18,215,3,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,39,0,147,128,159,145,14,4,6,109,119,113,108,50,67,2,39,0,147,128,159,145,14,4,6,67,82,79,71,73,119,2,4,0,195,133,217,18,218,3,20,65,115,32,115,111,111,110,32,97,115,32,121,111,117,32,116,121,112,101,32,134,195,133,217,18,238,3,10,102,111,110,116,95,99,111,108,111,114,11,34,48,120,49,48,48,98,53,102,102,34,132,195,133,217,18,239,3,1,47,134,195,133,217,18,240,3,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,132,195,133,217,18,241,3,28,32,97,32,109,101,110,117,32,119,105,108,108,32,112,111,112,32,117,112,46,32,83,101,108,101,99,116,32,134,195,133,217,18,141,4,8,98,103,95,99,111,108,111,114,12,34,48,120,98,51,57,99,50,55,98,48,34,132,195,133,217,18,142,4,15,100,105,102,102,101,114,101,110,116,32,116,121,112,101,115,134,195,133,217,18,157,4,8,98,103,95,99,111,108,111,114,4,110,117,108,108,132,195,133,217,18,158,4,31,32,111,102,32,99,111,110,116,101,110,116,32,98,108,111,99,107,115,32,121,111,117,32,99,97,110,32,97,100,100,46,39,0,147,128,159,145,14,4,6,53,72,118,84,75,51,2,39,0,147,128,159,145,14,4,6,90,79,118,81,105,51,2,4,0,195,133,217,18,191,4,5,84,121,112,101,32,134,195,133,217,18,196,4,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,197,4,1,47,134,195,133,217,18,198,4,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,199,4,13,32,102,111,108,108,111,119,101,100,32,98,121,32,134,195,133,217,18,212,4,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,213,4,7,47,98,117,108,108,101,116,134,195,133,217,18,220,4,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,221,4,4,32,111,114,32,134,195,133,217,18,225,4,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,226,4,22,47,110,117,109,32,116,111,32,99,114,101,97,116,101,32,97,32,108,105,115,116,46,134,195,133,217,18,248,4,4,99,111,100,101,4,110,117,108,108,39,0,147,128,159,145,14,4,6,79,90,115,66,78,49,2,39,0,147,128,159,145,14,4,6,81,116,69,74,118,51,2,4,0,195,133,217,18,251,4,6,67,108,105,99,107,32,134,195,133,217,18,129,5,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,130,5,11,43,32,78,101,119,32,80,97,103,101,32,134,195,133,217,18,141,5,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,142,5,50,98,117,116,116,111,110,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,111,102,32,121,111,117,114,32,115,105,100,101,98,97,114,32,116,111,32,97,100,100,32,97,32,110,101,119,32,134,195,133,217,18,192,5,4,98,111,108,100,4,116,114,117,101,132,195,133,217,18,193,5,4,112,97,103,101,134,195,133,217,18,197,5,4,98,111,108,100,4,110,117,108,108,132,195,133,217,18,198,5,1,46,39,0,147,128,159,145,14,4,6,84,100,107,119,102,104,2,39,0,147,128,159,145,14,4,6,90,70,108,73,71,121,2,4,0,195,133,217,18,201,5,6,67,108,105,99,107,32,134,195,133,217,18,207,5,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,208,5,1,43,134,195,133,217,18,209,5,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,210,5,1,32,134,195,133,217,18,211,5,10,102,111,110,116,95,99,111,108,111,114,11,34,48,120,49,100,98,51,54,51,54,34,134,195,133,217,18,212,5,8,98,103,95,99,111,108,111,114,11,34,48,120,49,102,102,100,97,101,54,34,132,195,133,217,18,213,5,4,110,101,120,116,134,195,133,217,18,217,5,8,98,103,95,99,111,108,111,114,4,110,117,108,108,134,195,133,217,18,218,5,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,132,195,133,217,18,219,5,37,32,116,111,32,97,110,121,32,112,97,103,101,32,116,105,116,108,101,32,105,110,32,116,104,101,32,115,105,100,101,98,97,114,32,116,111,32,134,195,133,217,18,128,6,10,102,111,110,116,95,99,111,108,111,114,11,34,48,120,49,56,52,50,55,101,48,34,132,195,133,217,18,129,6,7,113,117,105,99,107,108,121,134,195,133,217,18,136,6,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,132,195,133,217,18,137,6,1,32,134,195,133,217,18,138,6,6,105,116,97,108,105,99,4,116,114,117,101,134,195,133,217,18,139,6,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,134,195,133,217,18,140,6,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,132,195,133,217,18,141,6,9,230,140,168,233,161,191,230,137,147,134,195,133,217,18,144,6,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,134,195,133,217,18,145,6,6,105,116,97,108,105,99,4,110,117,108,108,134,195,133,217,18,146,6,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,132,195,133,217,18,147,6,16,32,97,32,110,101,119,32,115,117,98,112,97,103,101,44,32,134,195,133,217,18,163,6,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,164,6,32,68,111,99,117,109,101,110,116,44,32,71,114,105,100,44,32,111,114,32,75,97,110,98,97,110,32,66,111,97,114,100,46,134,195,133,217,18,196,6,4,99,111,100,101,4,110,117,108,108,39,0,147,128,159,145,14,4,6,51,55,75,112,109,74,2,39,0,147,128,159,145,14,4,6,114,49,67,51,121,66,2,4,0,195,133,217,18,199,6,6,228,189,147,233,170,140,134,195,133,217,18,201,6,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,116,114,117,101,134,195,133,217,18,202,6,9,117,110,100,101,114,108,105,110,101,4,116,114,117,101,132,195,133,217,18,203,6,3,228,184,128,134,195,133,217,18,204,6,9,117,110,100,101,114,108,105,110,101,4,110,117,108,108,134,195,133,217,18,205,6,13,115,116,114,105,107,101,116,104,114,111,117,103,104,4,110,117,108,108,39,0,147,128,159,145,14,4,6,104,48,112,77,45,68,2,4,0,195,133,217,18,207,6,6,231,155,145,230,142,167,39,0,147,128,159,145,14,4,6,88,85,57,77,122,75,2,4,0,195,133,217,18,210,6,13,103,104,104,104,229,143,145,230,140,165,229,165,189,39,0,147,128,159,145,14,4,6,88,101,101,105,77,89,2,4,0,195,133,217,18,218,6,4,54,54,54,57,39,0,147,128,159,145,14,4,6,111,121,69,56,121,53,2,4,0,195,133,217,18,223,6,4,240,159,152,131,39,0,147,128,159,145,14,4,6,80,69,50,72,56,68,2,4,0,195,133,217,18,226,6,12,229,185,178,230,180,187,229,147,136,229,147,136,39,0,147,128,159,145,14,4,6,72,80,78,114,99,102,2,6,0,195,133,217,18,231,6,8,98,103,95,99,111,108,111,114,11,34,48,120,49,97,55,100,102,52,97,34,134,195,133,217,18,232,6,10,102,111,110,116,95,99,111,108,111,114,11,34,48,120,49,101,97,56,102,48,54,34,132,195,133,217,18,233,6,4,54,54,54,57,134,195,133,217,18,237,6,10,102,111,110,116,95,99,111,108,111,114,4,110,117,108,108,134,195,133,217,18,238,6,8,98,103,95,99,111,108,111,114,4,110,117,108,108,39,0,147,128,159,145,14,4,6,55,81,111,111,73,112,2,39,0,147,128,159,145,14,4,6,120,117,78,48,102,83,2,4,0,195,133,217,18,241,6,44,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,44,32,109,97,114,107,100,111,119,110,44,32,97,110,100,32,99,111,100,101,32,98,108,111,99,107,39,0,147,128,159,145,14,4,6,108,107,118,90,121,113,2,4,0,195,133,217,18,158,7,19,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,32,134,195,133,217,18,177,7,4,104,114,101,102,68,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,115,104,111,114,116,99,117,116,115,34,132,195,133,217,18,178,7,5,103,117,105,100,101,134,195,133,217,18,183,7,4,104,114,101,102,4,110,117,108,108,39,0,147,128,159,145,14,4,6,97,50,90,104,55,55,2,4,0,195,133,217,18,185,7,9,77,97,114,107,100,111,119,110,32,134,195,133,217,18,194,7,4,104,114,101,102,67,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,109,97,114,107,100,111,119,110,34,132,195,133,217,18,195,7,9,114,101,102,101,114,101,110,99,101,134,195,133,217,18,204,7,4,104,114,101,102,4,110,117,108,108,39,0,147,128,159,145,14,4,6,122,122,70,106,54,119,2,4,0,195,133,217,18,206,7,3,105,106,106,39,0,147,128,159,145,14,4,6,72,85,89,49,86,115,2,4,0,195,133,217,18,210,7,4,106,107,110,98,39,0,147,128,159,145,14,4,6,102,79,45,120,115,55,2,4,0,195,133,217,18,215,7,12,232,191,155,230,173,165,230,156,186,228,188,154,39,0,147,128,159,145,14,4,6,51,110,110,101,106,112,2,4,0,195,133,217,18,220,7,12,230,150,164,230,150,164,232,174,161,232,190,131,39,0,147,128,159,145,14,4,6,109,54,68,111,117,70,2,4,0,195,133,217,18,225,7,5,84,121,112,101,32,134,195,133,217,18,230,7,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,231,7,28,47,99,111,100,101,32,116,111,32,105,110,115,101,114,116,32,97,32,99,111,100,101,32,98,108,111,99,107,134,195,133,217,18,131,8,4,99,111,100,101,4,110,117,108,108,39,0,147,128,159,145,14,4,6,51,119,76,72,119,80,2,4,0,195,133,217,18,133,8,13,98,117,108,108,101,116,101,100,32,108,105,115,116,39,0,147,128,159,145,14,4,6,73,57,55,106,83,103,2,4,0,195,133,217,18,147,8,7,99,104,105,108,100,45,49,39,0,147,128,159,145,14,4,6,114,55,104,73,74,95,2,4,0,195,133,217,18,155,8,9,99,104,105,108,100,45,49,45,49,39,0,147,128,159,145,14,4,6,53,74,52,110,52,56,2,4,0,195,133,217,18,165,8,9,99,104,105,108,100,45,49,45,50,39,0,147,128,159,145,14,4,6,82,105,78,75,118,55,2,4,0,195,133,217,18,175,8,7,99,104,105,108,100,45,50,39,0,147,128,159,145,14,4,6,57,119,57,113,66,45,2,4,0,195,133,217,18,183,8,3,49,50,51,39,0,147,128,159,145,14,4,6,84,81,109,75,119,97,2,4,0,195,133,217,18,187,8,18,72,97,118,101,32,97,32,113,117,101,115,116,105,111,110,226,157,147,39,0,147,128,159,145,14,4,6,50,83,115,67,101,65,2,4,0,195,133,217,18,204,8,6,67,108,105,99,107,32,134,195,133,217,18,210,8,4,99,111,100,101,4,116,114,117,101,132,195,133,217,18,211,8,1,63,134,195,133,217,18,212,8,4,99,111,100,101,4,110,117,108,108,132,195,133,217,18,213,8,42,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,114,105,103,104,116,32,102,111,114,32,104,101,108,112,32,97,110,100,32,115,117,112,112,111,114,116,46,39,0,147,128,159,145,14,1,6,118,71,89,57,89,100,1,40,0,195,133,217,18,128,9,2,105,100,1,119,6,118,71,89,57,89,100,40,0,195,133,217,18,128,9,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,195,133,217,18,128,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,128,9,8,99,104,105,108,100,114,101,110,1,119,6,122,55,68,52,54,115,40,0,195,133,217,18,128,9,4,100,97,116,97,1,119,46,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,32,36,32,32,101,114,32,32,34,125,93,44,34,108,101,118,101,108,34,58,50,125,40,0,195,133,217,18,128,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,128,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,122,55,68,52,54,115,0,200,195,133,217,18,71,195,133,217,18,99,1,119,6,118,71,89,57,89,100,39,0,147,128,159,145,14,1,6,115,45,78,113,116,119,1,40,0,195,133,217,18,138,9,2,105,100,1,119,6,115,45,78,113,116,119,40,0,195,133,217,18,138,9,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,195,133,217,18,138,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,138,9,8,99,104,105,108,100,114,101,110,1,119,6,85,65,51,119,110,54,40,0,195,133,217,18,138,9,4,100,97,116,97,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,51,34,125,93,44,34,108,101,118,101,108,34,58,51,125,40,0,195,133,217,18,138,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,138,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,85,65,51,119,110,54,0,200,195,133,217,18,137,9,195,133,217,18,99,1,119,6,115,45,78,113,116,119,39,0,147,128,159,145,14,1,6,75,55,102,84,65,65,1,40,0,195,133,217,18,148,9,2,105,100,1,119,6,75,55,102,84,65,65,40,0,195,133,217,18,148,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,148,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,148,9,8,99,104,105,108,100,114,101,110,1,119,6,88,112,113,70,83,99,40,0,195,133,217,18,148,9,4,100,97,116,97,1,119,44,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,101,99,107,101,100,32,116,111,100,111,32,108,105,115,116,32,36,34,125,93,125,40,0,195,133,217,18,148,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,148,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,88,112,113,70,83,99,0,200,195,133,217,18,147,9,195,133,217,18,99,1,119,6,75,55,102,84,65,65,39,0,147,128,159,145,14,1,6,113,84,87,120,77,103,1,40,0,195,133,217,18,158,9,2,105,100,1,119,6,113,84,87,120,77,103,40,0,195,133,217,18,158,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,158,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,158,9,8,99,104,105,108,100,114,101,110,1,119,6,115,116,70,77,88,66,40,0,195,133,217,18,158,9,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,158,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,158,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,115,116,70,77,88,66,0,200,195,133,217,18,157,9,195,133,217,18,99,1,119,6,113,84,87,120,77,103,39,0,147,128,159,145,14,1,6,79,116,113,105,98,55,1,40,0,195,133,217,18,168,9,2,105,100,1,119,6,79,116,113,105,98,55,40,0,195,133,217,18,168,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,168,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,168,9,8,99,104,105,108,100,114,101,110,1,119,6,53,56,45,56,70,76,40,0,195,133,217,18,168,9,4,100,97,116,97,1,119,205,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,108,111,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,76,101,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,110,103,32,116,101,120,116,34,125,93,125,40,0,195,133,217,18,168,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,168,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,53,56,45,56,70,76,0,200,195,133,217,18,167,9,195,133,217,18,99,1,119,6,79,116,113,105,98,55,39,0,147,128,159,145,14,1,6,120,87,45,65,56,86,1,40,0,195,133,217,18,178,9,2,105,100,1,119,6,120,87,45,65,56,86,40,0,195,133,217,18,178,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,178,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,178,9,8,99,104,105,108,100,114,101,110,1,119,6,103,84,78,70,76,73,40,0,195,133,217,18,178,9,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,178,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,178,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,103,84,78,70,76,73,0,200,195,133,217,18,177,9,195,133,217,18,99,1,119,6,120,87,45,65,56,86,39,0,147,128,159,145,14,1,6,112,109,117,76,121,114,1,40,0,195,133,217,18,188,9,2,105,100,1,119,6,112,109,117,76,121,114,40,0,195,133,217,18,188,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,188,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,188,9,8,99,104,105,108,100,114,101,110,1,119,6,100,121,111,70,45,65,40,0,195,133,217,18,188,9,4,100,97,116,97,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,119,105,116,104,34,125,93,125,40,0,195,133,217,18,188,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,188,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,100,121,111,70,45,65,0,200,195,133,217,18,187,9,195,133,217,18,99,1,119,6,112,109,117,76,121,114,39,0,147,128,159,145,14,1,6,88,87,120,55,57,115,1,40,0,195,133,217,18,198,9,2,105,100,1,119,6,88,87,120,55,57,115,40,0,195,133,217,18,198,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,198,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,198,9,8,99,104,105,108,100,114,101,110,1,119,6,88,67,102,53,75,117,40,0,195,133,217,18,198,9,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,198,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,198,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,88,67,102,53,75,117,0,200,195,133,217,18,197,9,195,133,217,18,99,1,119,6,88,87,120,55,57,115,39,0,147,128,159,145,14,1,6,87,50,72,99,77,83,1,40,0,195,133,217,18,208,9,2,105,100,1,119,6,87,50,72,99,77,83,40,0,195,133,217,18,208,9,2,116,121,1,119,5,113,117,111,116,101,40,0,195,133,217,18,208,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,208,9,8,99,104,105,108,100,114,101,110,1,119,6,76,82,68,50,65,86,40,0,195,133,217,18,208,9,4,100,97,116,97,1,119,36,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,144,140,228,184,128,228,184,170,110,105,34,125,93,125,40,0,195,133,217,18,208,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,208,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,76,82,68,50,65,86,0,200,195,133,217,18,207,9,195,133,217,18,99,1,119,6,87,50,72,99,77,83,39,0,147,128,159,145,14,1,6,114,45,105,49,57,106,1,40,0,195,133,217,18,218,9,2,105,100,1,119,6,114,45,105,49,57,106,40,0,195,133,217,18,218,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,218,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,218,9,8,99,104,105,108,100,114,101,110,1,119,6,76,97,68,83,112,65,40,0,195,133,217,18,218,9,4,100,97,116,97,1,119,80,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,97,110,121,119,104,101,114,101,32,97,110,100,32,106,117,115,116,32,115,116,97,114,116,32,116,121,112,105,110,103,229,147,136,229,147,136,229,147,136,46,229,176,177,229,135,160,229,174,182,34,125,93,125,40,0,195,133,217,18,218,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,218,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,76,97,68,83,112,65,0,200,195,133,217,18,217,9,195,133,217,18,99,1,119,6,114,45,105,49,57,106,39,0,147,128,159,145,14,1,6,45,77,89,115,65,114,1,40,0,195,133,217,18,228,9,2,105,100,1,119,6,45,77,89,115,65,114,40,0,195,133,217,18,228,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,228,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,228,9,8,99,104,105,108,100,114,101,110,1,119,6,70,122,111,65,105,114,40,0,195,133,217,18,228,9,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,228,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,228,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,70,122,111,65,105,114,0,200,195,133,217,18,227,9,195,133,217,18,99,1,119,6,45,77,89,115,65,114,39,0,147,128,159,145,14,1,6,53,99,99,77,84,71,1,40,0,195,133,217,18,238,9,2,105,100,1,119,6,53,99,99,77,84,71,40,0,195,133,217,18,238,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,238,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,238,9,8,99,104,105,108,100,114,101,110,1,119,6,105,85,97,67,74,107,40,0,195,133,217,18,238,9,4,100,97,116,97,1,119,183,3,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,98,51,102,102,101,98,51,98,34,125,44,34,105,110,115,101,114,116,34,58,34,72,105,103,104,108,105,103,104,116,32,34,125,44,123,34,105,110,115,101,114,116,34,58,34,97,110,121,32,116,101,120,116,44,32,97,110,100,32,117,115,101,32,116,104,101,32,101,100,105,116,105,110,103,32,109,101,110,117,32,116,111,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,105,116,97,108,105,99,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,115,116,121,108,101,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,111,108,100,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,121,111,117,114,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,117,110,100,101,114,108,105,110,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,119,114,105,116,105,110,103,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,104,111,119,101,118,101,114,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,121,111,117,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,115,116,114,105,107,101,116,104,114,111,117,103,104,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,108,105,107,101,46,34,125,93,125,40,0,195,133,217,18,238,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,238,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,105,85,97,67,74,107,0,200,195,133,217,18,237,9,195,133,217,18,99,1,119,6,53,99,99,77,84,71,39,0,147,128,159,145,14,1,6,57,88,75,76,69,115,1,40,0,195,133,217,18,248,9,2,105,100,1,119,6,57,88,75,76,69,115,40,0,195,133,217,18,248,9,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,248,9,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,248,9,8,99,104,105,108,100,114,101,110,1,119,6,82,116,101,71,70,116,40,0,195,133,217,18,248,9,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,248,9,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,248,9,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,82,116,101,71,70,116,0,200,195,133,217,18,247,9,195,133,217,18,99,1,119,6,57,88,75,76,69,115,39,0,147,128,159,145,14,1,6,45,99,53,69,50,113,1,40,0,195,133,217,18,130,10,2,105,100,1,119,6,45,99,53,69,50,113,40,0,195,133,217,18,130,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,130,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,130,10,8,99,104,105,108,100,114,101,110,1,119,6,90,52,77,78,84,95,40,0,195,133,217,18,130,10,4,100,97,116,97,1,119,255,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,65,115,32,115,111,111,110,32,97,115,32,121,111,117,32,116,121,112,101,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,49,48,48,98,53,102,102,34,125,44,34,105,110,115,101,114,116,34,58,34,47,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,97,32,109,101,110,117,32,119,105,108,108,32,112,111,112,32,117,112,46,32,83,101,108,101,99,116,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,98,51,57,99,50,55,98,48,34,125,44,34,105,110,115,101,114,116,34,58,34,100,105,102,102,101,114,101,110,116,32,116,121,112,101,115,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,111,102,32,99,111,110,116,101,110,116,32,98,108,111,99,107,115,32,121,111,117,32,99,97,110,32,97,100,100,46,34,125,93,125,40,0,195,133,217,18,130,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,130,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,90,52,77,78,84,95,0,200,195,133,217,18,129,10,195,133,217,18,99,1,119,6,45,99,53,69,50,113,39,0,147,128,159,145,14,1,6,108,73,53,65,67,48,1,40,0,195,133,217,18,140,10,2,105,100,1,119,6,108,73,53,65,67,48,40,0,195,133,217,18,140,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,140,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,140,10,8,99,104,105,108,100,114,101,110,1,119,6,57,86,108,107,82,87,40,0,195,133,217,18,140,10,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,140,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,140,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,57,86,108,107,82,87,0,200,195,133,217,18,139,10,195,133,217,18,99,1,119,6,108,73,53,65,67,48,39,0,147,128,159,145,14,1,6,115,98,73,99,106,103,1,40,0,195,133,217,18,150,10,2,105,100,1,119,6,115,98,73,99,106,103,40,0,195,133,217,18,150,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,150,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,150,10,8,99,104,105,108,100,114,101,110,1,119,6,56,119,111,119,68,50,40,0,195,133,217,18,150,10,4,100,97,116,97,1,119,228,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,84,121,112,101,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,47,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,102,111,108,108,111,119,101,100,32,98,121,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,47,98,117,108,108,101,116,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,111,114,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,47,110,117,109,32,116,111,32,99,114,101,97,116,101,32,97,32,108,105,115,116,46,34,125,93,125,40,0,195,133,217,18,150,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,150,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,56,119,111,119,68,50,0,200,195,133,217,18,149,10,195,133,217,18,99,1,119,6,115,98,73,99,106,103,39,0,147,128,159,145,14,1,6,65,72,89,121,80,85,1,40,0,195,133,217,18,160,10,2,105,100,1,119,6,65,72,89,121,80,85,40,0,195,133,217,18,160,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,160,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,160,10,8,99,104,105,108,100,114,101,110,1,119,6,70,68,79,70,76,77,40,0,195,133,217,18,160,10,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,160,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,160,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,70,68,79,70,76,77,0,200,195,133,217,18,159,10,195,133,217,18,99,1,119,6,65,72,89,121,80,85,39,0,147,128,159,145,14,1,6,72,110,55,49,119,97,1,40,0,195,133,217,18,170,10,2,105,100,1,119,6,72,110,55,49,119,97,40,0,195,133,217,18,170,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,170,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,170,10,8,99,104,105,108,100,114,101,110,1,119,6,80,55,68,49,81,54,40,0,195,133,217,18,170,10,4,100,97,116,97,1,119,207,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,43,32,78,101,119,32,80,97,103,101,32,34,125,44,123,34,105,110,115,101,114,116,34,58,34,98,117,116,116,111,110,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,111,102,32,121,111,117,114,32,115,105,100,101,98,97,114,32,116,111,32,97,100,100,32,97,32,110,101,119,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,111,108,100,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,112,97,103,101,34,125,44,123,34,105,110,115,101,114,116,34,58,34,46,34,125,93,125,40,0,195,133,217,18,170,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,170,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,80,55,68,49,81,54,0,200,195,133,217,18,169,10,195,133,217,18,99,1,119,6,72,110,55,49,119,97,39,0,147,128,159,145,14,1,6,56,120,67,119,110,83,1,40,0,195,133,217,18,180,10,2,105,100,1,119,6,56,120,67,119,110,83,40,0,195,133,217,18,180,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,180,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,180,10,8,99,104,105,108,100,114,101,110,1,119,6,114,116,113,68,113,100,40,0,195,133,217,18,180,10,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,180,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,180,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,114,116,113,68,113,100,0,200,195,133,217,18,179,10,195,133,217,18,99,1,119,6,56,120,67,119,110,83,39,0,147,128,159,145,14,1,6,67,79,113,95,50,67,1,40,0,195,133,217,18,190,10,2,105,100,1,119,6,67,79,113,95,50,67,40,0,195,133,217,18,190,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,190,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,190,10,8,99,104,105,108,100,114,101,110,1,119,6,109,54,56,82,52,55,40,0,195,133,217,18,190,10,4,100,97,116,97,1,119,233,3,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,43,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,49,102,102,100,97,101,54,34,44,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,49,100,98,51,54,51,54,34,125,44,34,105,110,115,101,114,116,34,58,34,110,101,120,116,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,116,111,32,97,110,121,32,112,97,103,101,32,116,105,116,108,101,32,105,110,32,116,104,101,32,115,105,100,101,98,97,114,32,116,111,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,49,56,52,50,55,101,48,34,125,44,34,105,110,115,101,114,116,34,58,34,113,117,105,99,107,108,121,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,105,116,97,108,105,99,34,58,116,114,117,101,44,34,115,116,114,105,107,101,116,104,114,111,117,103,104,34,58,116,114,117,101,44,34,117,110,100,101,114,108,105,110,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,230,140,168,233,161,191,230,137,147,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,97,32,110,101,119,32,115,117,98,112,97,103,101,44,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,68,111,99,117,109,101,110,116,44,32,71,114,105,100,44,32,111,114,32,75,97,110,98,97,110,32,66,111,97,114,100,46,34,125,93,125,40,0,195,133,217,18,190,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,190,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,109,54,56,82,52,55,0,200,195,133,217,18,189,10,195,133,217,18,99,1,119,6,67,79,113,95,50,67,39,0,147,128,159,145,14,1,6,114,71,120,87,45,114,1,40,0,195,133,217,18,200,10,2,105,100,1,119,6,114,71,120,87,45,114,40,0,195,133,217,18,200,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,200,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,200,10,8,99,104,105,108,100,114,101,110,1,119,6,112,82,70,53,54,68,40,0,195,133,217,18,200,10,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,200,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,200,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,112,82,70,53,54,68,0,200,195,133,217,18,199,10,195,133,217,18,99,1,119,6,114,71,120,87,45,114,39,0,147,128,159,145,14,1,6,102,48,55,89,71,81,1,40,0,195,133,217,18,210,10,2,105,100,1,119,6,102,48,55,89,71,81,40,0,195,133,217,18,210,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,210,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,210,10,8,99,104,105,108,100,114,101,110,1,119,6,78,56,98,56,111,65,40,0,195,133,217,18,210,10,4,100,97,116,97,1,119,101,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,228,189,147,233,170,140,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,115,116,114,105,107,101,116,104,114,111,117,103,104,34,58,116,114,117,101,44,34,117,110,100,101,114,108,105,110,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,228,184,128,34,125,93,125,40,0,195,133,217,18,210,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,210,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,78,56,98,56,111,65,0,200,195,133,217,18,209,10,195,133,217,18,99,1,119,6,102,48,55,89,71,81,39,0,147,128,159,145,14,1,6,68,73,122,109,100,87,1,40,0,195,133,217,18,220,10,2,105,100,1,119,6,68,73,122,109,100,87,40,0,195,133,217,18,220,10,2,116,121,1,119,5,113,117,111,116,101,40,0,195,133,217,18,220,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,220,10,8,99,104,105,108,100,114,101,110,1,119,6,55,114,82,118,114,89,40,0,195,133,217,18,220,10,4,100,97,116,97,1,119,31,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,231,155,145,230,142,167,34,125,93,125,40,0,195,133,217,18,220,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,220,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,55,114,82,118,114,89,0,200,195,133,217,18,219,10,195,133,217,18,99,1,119,6,68,73,122,109,100,87,39,0,147,128,159,145,14,1,6,88,104,87,98,66,48,1,40,0,195,133,217,18,230,10,2,105,100,1,119,6,88,104,87,98,66,48,40,0,195,133,217,18,230,10,2,116,121,1,119,5,113,117,111,116,101,40,0,195,133,217,18,230,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,230,10,8,99,104,105,108,100,114,101,110,1,119,6,69,110,114,122,69,100,40,0,195,133,217,18,230,10,4,100,97,116,97,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,103,104,104,104,229,143,145,230,140,165,229,165,189,34,125,93,125,40,0,195,133,217,18,230,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,230,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,69,110,114,122,69,100,0,200,195,133,217,18,229,10,195,133,217,18,99,1,119,6,88,104,87,98,66,48,39,0,147,128,159,145,14,1,6,117,122,54,88,89,118,1,40,0,195,133,217,18,240,10,2,105,100,1,119,6,117,122,54,88,89,118,40,0,195,133,217,18,240,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,240,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,240,10,8,99,104,105,108,100,114,101,110,1,119,6,80,72,76,53,98,110,40,0,195,133,217,18,240,10,4,100,97,116,97,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,54,54,54,57,34,125,93,125,40,0,195,133,217,18,240,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,240,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,80,72,76,53,98,110,0,200,195,133,217,18,239,10,195,133,217,18,99,1,119,6,117,122,54,88,89,118,39,0,147,128,159,145,14,1,6,51,110,100,90,103,51,1,40,0,195,133,217,18,250,10,2,105,100,1,119,6,51,110,100,90,103,51,40,0,195,133,217,18,250,10,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,250,10,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,250,10,8,99,104,105,108,100,114,101,110,1,119,6,79,120,115,115,72,77,40,0,195,133,217,18,250,10,4,100,97,116,97,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,240,159,152,131,34,125,93,125,40,0,195,133,217,18,250,10,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,250,10,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,79,120,115,115,72,77,0,200,195,133,217,18,249,10,195,133,217,18,99,1,119,6,51,110,100,90,103,51,39,0,147,128,159,145,14,1,6,110,103,95,69,109,50,1,40,0,195,133,217,18,132,11,2,105,100,1,119,6,110,103,95,69,109,50,40,0,195,133,217,18,132,11,2,116,121,1,119,5,113,117,111,116,101,40,0,195,133,217,18,132,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,132,11,8,99,104,105,108,100,114,101,110,1,119,6,102,107,66,48,107,107,40,0,195,133,217,18,132,11,4,100,97,116,97,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,185,178,230,180,187,229,147,136,229,147,136,34,125,93,125,40,0,195,133,217,18,132,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,132,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,102,107,66,48,107,107,0,200,195,133,217,18,131,11,195,133,217,18,99,1,119,6,110,103,95,69,109,50,39,0,147,128,159,145,14,1,6,102,84,86,120,101,99,1,40,0,195,133,217,18,142,11,2,105,100,1,119,6,102,84,86,120,101,99,40,0,195,133,217,18,142,11,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,142,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,142,11,8,99,104,105,108,100,114,101,110,1,119,6,99,107,80,105,100,83,40,0,195,133,217,18,142,11,4,100,97,116,97,1,119,92,123,34,100,101,108,116,97,34,58,91,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,98,103,95,99,111,108,111,114,34,58,34,48,120,49,97,55,100,102,52,97,34,44,34,102,111,110,116,95,99,111,108,111,114,34,58,34,48,120,49,101,97,56,102,48,54,34,125,44,34,105,110,115,101,114,116,34,58,34,54,54,54,57,34,125,93,125,40,0,195,133,217,18,142,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,142,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,99,107,80,105,100,83,0,200,195,133,217,18,141,11,195,133,217,18,99,1,119,6,102,84,86,120,101,99,39,0,147,128,159,145,14,1,6,111,56,70,84,77,117,1,40,0,195,133,217,18,152,11,2,105,100,1,119,6,111,56,70,84,77,117,40,0,195,133,217,18,152,11,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,152,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,152,11,8,99,104,105,108,100,114,101,110,1,119,6,108,119,100,112,111,55,40,0,195,133,217,18,152,11,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,152,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,152,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,108,119,100,112,111,55,0,200,195,133,217,18,151,11,195,133,217,18,99,1,119,6,111,56,70,84,77,117,39,0,147,128,159,145,14,1,6,82,74,81,67,50,82,1,40,0,195,133,217,18,162,11,2,105,100,1,119,6,82,74,81,67,50,82,40,0,195,133,217,18,162,11,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,195,133,217,18,162,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,162,11,8,99,104,105,108,100,114,101,110,1,119,6,119,97,87,79,66,52,40,0,195,133,217,18,162,11,4,100,97,116,97,1,119,79,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,44,32,109,97,114,107,100,111,119,110,44,32,97,110,100,32,99,111,100,101,32,98,108,111,99,107,34,125,93,44,34,108,101,118,101,108,34,58,50,125,40,0,195,133,217,18,162,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,162,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,119,97,87,79,66,52,0,200,195,133,217,18,161,11,195,133,217,18,99,1,119,6,82,74,81,67,50,82,39,0,147,128,159,145,14,1,6,117,70,54,49,55,109,1,40,0,195,133,217,18,172,11,2,105,100,1,119,6,117,70,54,49,55,109,40,0,195,133,217,18,172,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,172,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,172,11,8,99,104,105,108,100,114,101,110,1,119,6,55,118,110,77,69,78,40,0,195,133,217,18,172,11,4,100,97,116,97,1,119,154,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,75,101,121,98,111,97,114,100,32,115,104,111,114,116,99,117,116,115,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,104,114,101,102,34,58,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,115,104,111,114,116,99,117,116,115,34,125,44,34,105,110,115,101,114,116,34,58,34,103,117,105,100,101,34,125,93,125,40,0,195,133,217,18,172,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,172,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,55,118,110,77,69,78,0,200,195,133,217,18,171,11,195,133,217,18,99,1,119,6,117,70,54,49,55,109,39,0,147,128,159,145,14,1,6,76,87,83,67,73,57,1,40,0,195,133,217,18,182,11,2,105,100,1,119,6,76,87,83,67,73,57,40,0,195,133,217,18,182,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,182,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,182,11,8,99,104,105,108,100,114,101,110,1,119,6,111,67,89,117,74,57,40,0,195,133,217,18,182,11,4,100,97,116,97,1,119,147,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,77,97,114,107,100,111,119,110,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,104,114,101,102,34,58,34,104,116,116,112,115,58,47,47,97,112,112,102,108,111,119,121,46,103,105,116,98,111,111,107,46,105,111,47,100,111,99,115,47,101,115,115,101,110,116,105,97,108,45,100,111,99,117,109,101,110,116,97,116,105,111,110,47,109,97,114,107,100,111,119,110,34,125,44,34,105,110,115,101,114,116,34,58,34,114,101,102,101,114,101,110,99,101,34,125,93,125,40,0,195,133,217,18,182,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,182,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,111,67,89,117,74,57,0,200,195,133,217,18,181,11,195,133,217,18,99,1,119,6,76,87,83,67,73,57,39,0,147,128,159,145,14,1,6,110,98,54,83,103,101,1,40,0,195,133,217,18,192,11,2,105,100,1,119,6,110,98,54,83,103,101,40,0,195,133,217,18,192,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,192,11,6,112,97,114,101,110,116,1,119,6,76,87,83,67,73,57,40,0,195,133,217,18,192,11,8,99,104,105,108,100,114,101,110,1,119,6,105,87,100,115,72,89,40,0,195,133,217,18,192,11,4,100,97,116,97,1,119,28,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,105,106,106,34,125,93,125,40,0,195,133,217,18,192,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,192,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,105,87,100,115,72,89,0,8,0,195,133,217,18,190,11,1,119,6,110,98,54,83,103,101,39,0,147,128,159,145,14,1,6,77,73,113,111,117,90,1,40,0,195,133,217,18,202,11,2,105,100,1,119,6,77,73,113,111,117,90,40,0,195,133,217,18,202,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,202,11,6,112,97,114,101,110,116,1,119,6,110,98,54,83,103,101,40,0,195,133,217,18,202,11,8,99,104,105,108,100,114,101,110,1,119,6,88,65,95,122,90,115,40,0,195,133,217,18,202,11,4,100,97,116,97,1,119,29,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,106,107,110,98,34,125,93,125,40,0,195,133,217,18,202,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,202,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,88,65,95,122,90,115,0,8,0,195,133,217,18,200,11,1,119,6,77,73,113,111,117,90,39,0,147,128,159,145,14,1,6,98,112,45,97,121,53,1,40,0,195,133,217,18,212,11,2,105,100,1,119,6,98,112,45,97,121,53,40,0,195,133,217,18,212,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,212,11,6,112,97,114,101,110,116,1,119,6,77,73,113,111,117,90,40,0,195,133,217,18,212,11,8,99,104,105,108,100,114,101,110,1,119,6,73,84,120,118,112,57,40,0,195,133,217,18,212,11,4,100,97,116,97,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,232,191,155,230,173,165,230,156,186,228,188,154,34,125,93,125,40,0,195,133,217,18,212,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,212,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,73,84,120,118,112,57,0,8,0,195,133,217,18,210,11,1,119,6,98,112,45,97,121,53,39,0,147,128,159,145,14,1,6,56,121,102,112,65,112,1,40,0,195,133,217,18,222,11,2,105,100,1,119,6,56,121,102,112,65,112,40,0,195,133,217,18,222,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,222,11,6,112,97,114,101,110,116,1,119,6,77,73,113,111,117,90,40,0,195,133,217,18,222,11,8,99,104,105,108,100,114,101,110,1,119,6,45,109,54,99,76,105,40,0,195,133,217,18,222,11,4,100,97,116,97,1,119,37,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,230,150,164,230,150,164,232,174,161,232,190,131,34,125,93,125,40,0,195,133,217,18,222,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,222,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,45,109,54,99,76,105,0,136,195,133,217,18,221,11,1,119,6,56,121,102,112,65,112,39,0,147,128,159,145,14,1,6,111,109,54,99,122,66,1,40,0,195,133,217,18,232,11,2,105,100,1,119,6,111,109,54,99,122,66,40,0,195,133,217,18,232,11,2,116,121,1,119,13,110,117,109,98,101,114,101,100,95,108,105,115,116,40,0,195,133,217,18,232,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,232,11,8,99,104,105,108,100,114,101,110,1,119,6,68,82,102,101,121,118,40,0,195,133,217,18,232,11,4,100,97,116,97,1,119,99,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,84,121,112,101,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,47,99,111,100,101,32,116,111,32,105,110,115,101,114,116,32,97,32,99,111,100,101,32,98,108,111,99,107,34,125,93,125,40,0,195,133,217,18,232,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,232,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,68,82,102,101,121,118,0,200,195,133,217,18,191,11,195,133,217,18,99,1,119,6,111,109,54,99,122,66,39,0,147,128,159,145,14,1,6,65,120,74,79,67,97,1,40,0,195,133,217,18,242,11,2,105,100,1,119,6,65,120,74,79,67,97,40,0,195,133,217,18,242,11,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,195,133,217,18,242,11,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,242,11,8,99,104,105,108,100,114,101,110,1,119,6,49,52,84,74,100,51,40,0,195,133,217,18,242,11,4,100,97,116,97,1,119,38,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,98,117,108,108,101,116,101,100,32,108,105,115,116,34,125,93,125,40,0,195,133,217,18,242,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,242,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,49,52,84,74,100,51,0,200,195,133,217,18,241,11,195,133,217,18,99,1,119,6,65,120,74,79,67,97,39,0,147,128,159,145,14,1,6,48,55,67,110,83,97,1,40,0,195,133,217,18,252,11,2,105,100,1,119,6,48,55,67,110,83,97,40,0,195,133,217,18,252,11,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,195,133,217,18,252,11,6,112,97,114,101,110,116,1,119,6,65,120,74,79,67,97,40,0,195,133,217,18,252,11,8,99,104,105,108,100,114,101,110,1,119,6,89,108,74,119,83,49,40,0,195,133,217,18,252,11,4,100,97,116,97,1,119,32,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,34,125,93,125,40,0,195,133,217,18,252,11,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,252,11,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,89,108,74,119,83,49,0,8,0,195,133,217,18,250,11,1,119,6,48,55,67,110,83,97,39,0,147,128,159,145,14,1,6,95,57,76,55,68,108,1,40,0,195,133,217,18,134,12,2,105,100,1,119,6,95,57,76,55,68,108,40,0,195,133,217,18,134,12,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,195,133,217,18,134,12,6,112,97,114,101,110,116,1,119,6,48,55,67,110,83,97,40,0,195,133,217,18,134,12,8,99,104,105,108,100,114,101,110,1,119,6,68,90,49,112,114,48,40,0,195,133,217,18,134,12,4,100,97,116,97,1,119,34,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,45,49,34,125,93,125,40,0,195,133,217,18,134,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,134,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,68,90,49,112,114,48,0,8,0,195,133,217,18,132,12,1,119,6,95,57,76,55,68,108,39,0,147,128,159,145,14,1,6,118,109,65,70,53,76,1,40,0,195,133,217,18,144,12,2,105,100,1,119,6,118,109,65,70,53,76,40,0,195,133,217,18,144,12,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,195,133,217,18,144,12,6,112,97,114,101,110,116,1,119,6,48,55,67,110,83,97,40,0,195,133,217,18,144,12,8,99,104,105,108,100,114,101,110,1,119,6,118,87,53,73,57,111,40,0,195,133,217,18,144,12,4,100,97,116,97,1,119,34,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,49,45,50,34,125,93,125,40,0,195,133,217,18,144,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,144,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,118,87,53,73,57,111,0,136,195,133,217,18,143,12,1,119,6,118,109,65,70,53,76,39,0,147,128,159,145,14,1,6,121,55,78,87,55,99,1,40,0,195,133,217,18,154,12,2,105,100,1,119,6,121,55,78,87,55,99,40,0,195,133,217,18,154,12,2,116,121,1,119,13,98,117,108,108,101,116,101,100,95,108,105,115,116,40,0,195,133,217,18,154,12,6,112,97,114,101,110,116,1,119,6,65,120,74,79,67,97,40,0,195,133,217,18,154,12,8,99,104,105,108,100,114,101,110,1,119,6,79,51,103,80,85,76,40,0,195,133,217,18,154,12,4,100,97,116,97,1,119,32,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,99,104,105,108,100,45,50,34,125,93,125,40,0,195,133,217,18,154,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,154,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,79,51,103,80,85,76,0,136,195,133,217,18,133,12,1,119,6,121,55,78,87,55,99,39,0,147,128,159,145,14,1,6,83,102,48,120,100,54,1,40,0,195,133,217,18,164,12,2,105,100,1,119,6,83,102,48,120,100,54,40,0,195,133,217,18,164,12,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,164,12,6,112,97,114,101,110,116,1,119,6,65,120,74,79,67,97,40,0,195,133,217,18,164,12,8,99,104,105,108,100,114,101,110,1,119,6,78,76,97,67,89,115,33,0,195,133,217,18,164,12,4,100,97,116,97,1,40,0,195,133,217,18,164,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,164,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,78,76,97,67,89,115,0,136,195,133,217,18,163,12,1,119,6,83,102,48,120,100,54,39,0,147,128,159,145,14,1,6,54,104,67,112,68,80,1,40,0,195,133,217,18,174,12,2,105,100,1,119,6,54,104,67,112,68,80,40,0,195,133,217,18,174,12,2,116,121,1,119,7,104,101,97,100,105,110,103,40,0,195,133,217,18,174,12,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,174,12,8,99,104,105,108,100,114,101,110,1,119,6,57,77,71,74,107,73,40,0,195,133,217,18,174,12,4,100,97,116,97,1,119,53,123,34,108,101,118,101,108,34,58,50,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,72,97,118,101,32,97,32,113,117,101,115,116,105,111,110,226,157,147,34,125,93,125,40,0,195,133,217,18,174,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,174,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,57,77,71,74,107,73,0,200,195,133,217,18,251,11,195,133,217,18,99,1,119,6,54,104,67,112,68,80,39,0,147,128,159,145,14,1,6,90,68,65,45,49,122,1,40,0,195,133,217,18,184,12,2,105,100,1,119,6,90,68,65,45,49,122,40,0,195,133,217,18,184,12,2,116,121,1,119,5,113,117,111,116,101,40,0,195,133,217,18,184,12,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,184,12,8,99,104,105,108,100,114,101,110,1,119,6,122,50,122,106,95,53,40,0,195,133,217,18,184,12,4,100,97,116,97,1,119,129,1,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,67,108,105,99,107,32,34,125,44,123,34,97,116,116,114,105,98,117,116,101,115,34,58,123,34,99,111,100,101,34,58,116,114,117,101,125,44,34,105,110,115,101,114,116,34,58,34,63,34,125,44,123,34,105,110,115,101,114,116,34,58,34,32,97,116,32,116,104,101,32,98,111,116,116,111,109,32,114,105,103,104,116,32,102,111,114,32,104,101,108,112,32,97,110,100,32,115,117,112,112,111,114,116,46,34,125,93,125,40,0,195,133,217,18,184,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,184,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,122,50,122,106,95,53,0,200,195,133,217,18,183,12,195,133,217,18,99,1,119,6,90,68,65,45,49,122,39,0,147,128,159,145,14,4,6,110,66,48,103,72,72,2,4,0,195,133,217,18,194,12,15,229,129,165,229,186,183,233,130,163,232,190,185,85,73,105,168,195,133,217,18,88,1,119,56,123,34,99,104,101,99,107,101,100,34,58,102,97,108,115,101,44,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,229,129,165,229,186,183,233,130,163,232,190,185,85,73,105,34,125,93,125,39,0,147,128,159,145,14,4,6,112,119,95,118,45,79,2,33,0,147,128,159,145,14,1,6,102,77,98,109,66,116,1,0,7,33,0,147,128,159,145,14,3,6,122,104,84,113,87,83,1,129,195,133,217,18,99,1,39,0,147,128,159,145,14,1,6,69,120,118,84,102,57,1,40,0,195,133,217,18,214,12,2,105,100,1,119,6,69,120,118,84,102,57,40,0,195,133,217,18,214,12,2,116,121,1,119,5,105,109,97,103,101,40,0,195,133,217,18,214,12,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,214,12,8,99,104,105,108,100,114,101,110,1,119,6,57,87,71,65,49,95,33,0,195,133,217,18,214,12,4,100,97,116,97,1,40,0,195,133,217,18,214,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,214,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,57,87,71,65,49,95,0,200,195,133,217,18,99,195,133,217,18,213,12,1,119,6,69,120,118,84,102,57,39,0,147,128,159,145,14,4,6,119,48,80,67,108,103,2,39,0,147,128,159,145,14,1,6,102,100,85,89,106,108,1,40,0,195,133,217,18,225,12,2,105,100,1,119,6,102,100,85,89,106,108,40,0,195,133,217,18,225,12,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,225,12,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,225,12,8,99,104,105,108,100,114,101,110,1,119,6,68,107,98,56,50,87,40,0,195,133,217,18,225,12,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,225,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,225,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,68,107,98,56,50,87,0,136,195,133,217,18,213,12,1,119,6,102,100,85,89,106,108,168,195,133,217,18,219,12,1,119,212,1,123,34,117,114,108,34,58,34,104,116,116,112,115,58,47,47,105,109,97,103,101,115,46,117,110,115,112,108,97,115,104,46,99,111,109,47,112,104,111,116,111,45,49,55,49,51,48,57,56,48,57,56,56,51,51,45,102,52,53,100,49,56,48,99,97,57,97,50,63,99,114,111,112,61,101,110,116,114,111,112,121,38,99,115,61,116,105,110,121,115,114,103,98,38,102,105,116,61,109,97,120,38,102,109,61,106,112,103,38,105,120,105,100,61,77,51,119,49,77,84,69,49,77,122,100,56,77,72,119,120,102,72,74,104,98,109,82,118,98,88,120,56,102,72,120,56,102,72,120,56,102,68,69,51,77,84,77,49,78,68,73,51,78,84,74,56,38,105,120,108,105,98,61,114,98,45,52,46,48,46,51,38,113,61,56,48,38,119,61,49,48,56,48,34,44,34,97,108,105,103,110,34,58,34,99,101,110,116,101,114,34,125,39,0,147,128,159,145,14,4,6,70,80,88,99,109,117,2,39,0,147,128,159,145,14,1,6,109,99,66,107,98,115,1,40,0,195,133,217,18,237,12,2,105,100,1,119,6,109,99,66,107,98,115,40,0,195,133,217,18,237,12,2,116,121,1,119,9,112,97,114,97,103,114,97,112,104,40,0,195,133,217,18,237,12,6,112,97,114,101,110,116,1,119,10,85,86,79,107,81,88,110,117,86,114,40,0,195,133,217,18,237,12,8,99,104,105,108,100,114,101,110,1,119,6,113,49,74,97,114,53,40,0,195,133,217,18,237,12,4,100,97,116,97,1,119,12,123,34,100,101,108,116,97,34,58,91,93,125,40,0,195,133,217,18,237,12,11,101,120,116,101,114,110,97,108,95,105,100,1,126,40,0,195,133,217,18,237,12,13,101,120,116,101,114,110,97,108,95,116,121,112,101,1,126,39,0,147,128,159,145,14,3,6,113,49,74,97,114,53,0,136,195,133,217,18,234,12,1,119,6,109,99,66,107,98,115,39,0,147,128,159,145,14,4,6,66,87,74,50,81,114,2,4,0,195,133,217,18,247,12,2,49,50,168,195,133,217,18,169,12,1,119,27,123,34,100,101,108,116,97,34,58,91,123,34,105,110,115,101,114,116,34,58,34,49,50,34,125,93,125,39,0,147,128,159,145,14,4,6,116,83,114,83,65,45,2,33,0,147,128,159,145,14,1,6,118,71,79,68,118,57,1,0,7,33,0,147,128,159,145,14,3,6,66,122,109,101,85,67,1,129,195,133,217,18,173,12,1,39,0,147,128,159,145,14,4,6,56,82,84,70,84,106,2,33,0,147,128,159,145,14,1,6,100,48,101,105,48,99,1,0,7,33,0,147,128,159,145,14,3,6,55,103,88,67,83,76,1,193,195,133,217,18,251,11,195,133,217,18,183,12,1,39,0,147,128,159,145,14,4,6,86,49,69,99,77,120,2,33,0,147,128,159,145,14,1,6,57,82,76,118,75,83,1,0,7,33,0,147,128,159,145,14,3,6,100,84,102,75,114,54,1,129,195,133,217,18,133,13,1,39,0,147,128,159,145,14,4,6,95,90,85,102,102,106,2,33,0,147,128,159,145,14,1,6,78,102,79,67,79,76,1,0,7,33,0,147,128,159,145,14,3,6,66,52,111,107,80,118,1,193,195,133,217,18,144,13,195,133,217,18,183,12,1,5,200,244,136,224,7,1,0,3,178,246,186,209,6,1,0,72,147,128,159,145,14,1,11,3,195,133,217,18,17,11,1,21,10,37,1,44,1,51,1,60,1,62,10,79,1,88,1,90,10,169,12,1,204,12,10,219,12,1,252,12,10,135,13,10,146,13,10,157,13,10,156,139,194,87,1,0,3],"version":0},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/user.json b/frontend/appflowy_web_app/cypress/fixtures/user.json new file mode 100644 index 0000000000..5b429dcd59 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/user.json @@ -0,0 +1,17 @@ +{ + "data": { + "uid": 304120109071339520, + "uuid": "cbff060a-196d-415a-aa80-759c01886466", + "email": "lu@appflowy.io", + "password": "", + "name": "Kilu", + "metadata": { + "icon_url": "🇽🇰" + }, + "encryption_sign": null, + "latest_workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968", + "updated_at": 1710421586 + }, + "code": 0, + "message": "Operation completed successfully." +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json new file mode 100644 index 0000000000..277b4c972c --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json @@ -0,0 +1 @@ +{"data":{"user_profile":{"uid":304120109071339520,"uuid":"cbff060a-196d-415a-aa80-759c01886466","email":"lu@appflowy.io","password":"","name":"Kilu","metadata":{"icon_url":"🇽🇰"},"encryption_sign":null,"latest_workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","updated_at":1715847453},"visiting_workspace":{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"},"workspaces":[{"workspace_id":"81570fa8-8be9-4b2d-9f1c-1ef4f34079a8","database_storage_id":"6c1f1a2c-e8d5-4bc2-917f-495bce862abb","owner_uid":311828434584080384,"owner_name":"Zack Zi Xiang Fu","workspace_type":0,"workspace_name":"My Workspace","created_at":"2024-04-03T13:53:18.295918Z","icon":""},{"workspace_id":"fcb503f9-9287-4de4-8de0-ea191e680968","database_storage_id":"ae1b82a5-2b93-45c7-901a-f9357c544534","owner_uid":276169796100296704,"owner_name":"Annie Anqi Wang","workspace_type":0,"workspace_name":"AppFlowy Test","created_at":"2023-12-27T04:18:36.372013Z","icon":""},{"workspace_id":"9eebea03-3ed5-4298-86b2-a7f77856d48b","database_storage_id":"375874be-7a4f-4b7c-8b89-1dc9a39838f4","owner_uid":304120109071339520,"owner_name":"Kilu","workspace_type":0,"workspace_name":"Kilu Works","created_at":"2024-03-13T07:23:10.275174Z","icon":"😆"}]},"code":0,"message":"Operation completed successfully."} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/fixtures/verify_token.json b/frontend/appflowy_web_app/cypress/fixtures/verify_token.json new file mode 100644 index 0000000000..503838f0a6 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/verify_token.json @@ -0,0 +1,6 @@ +{ + "code": 0, + "data": { + "is_new": false + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/commands.ts b/frontend/appflowy_web_app/cypress/support/commands.ts new file mode 100644 index 0000000000..f78768001f --- /dev/null +++ b/frontend/appflowy_web_app/cypress/support/commands.ts @@ -0,0 +1,120 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +import { YDoc } from '@/application/collab.type'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { JSDatabaseService } from '@/application/services/js-services/database.service'; +import { JSDocumentService } from '@/application/services/js-services/document.service'; +import { applyYDoc } from '@/application/ydoc/apply'; +import * as Y from 'yjs'; + +Cypress.Commands.add('mockAPI', () => { + cy.fixture('sign_in_success').then((json) => { + cy.intercept('GET', `/api/user/verify/${json.access_token}`, { + fixture: 'verify_token', + }).as('verifyToken'); + cy.intercept('POST', '/gotrue/token?grant_type=password', json).as('loginSuccess'); + cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken'); + }); + cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile'); + cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace'); +}); + +// Example use: +// beforeEach(() => { +// cy.mockAPI(); +// }); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +Cypress.Commands.add('mockCurrentWorkspace', () => { + cy.fixture('current_workspace').then((workspace) => { + cy.stub(JSDatabaseService.prototype, 'currentWorkspace').resolves(workspace); + }); +}); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +Cypress.Commands.add('mockGetWorkspaceDatabases', () => { + cy.fixture('database/databases').then((databases) => { + cy.stub(JSDatabaseService.prototype, 'getWorkspaceDatabases').resolves(databases); + }); +}); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +Cypress.Commands.add('mockDatabase', () => { + cy.mockCurrentWorkspace(); + cy.mockGetWorkspaceDatabases(); + + const ids = [ + '4c658817-20db-4f56-b7f9-0637a22dfeb6', + 'ce267d12-3b61-4ebb-bb03-d65272f5f817', + 'ad7dc45b-44b5-498f-bfa2-0f43bf05cc0d', + ]; + + const mockOpenDatabase = cy.stub(JSDatabaseService.prototype, 'openDatabase'); + + ids.forEach((id) => { + cy.fixture(`database/${id}`).then((database) => { + cy.fixture(`database/rows/${id}`).then((rows) => { + const doc = new Y.Doc(); + const rootRowsDoc = new Y.Doc(); + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const databaseState = new Uint8Array(database.data.doc_state); + + applyYDoc(doc, databaseState); + + Object.keys(rows).forEach((key) => { + const data = rows[key]; + const rowDoc = new Y.Doc(); + + applyYDoc(rowDoc, new Uint8Array(data)); + rowsFolder.set(key, rowDoc); + }); + mockOpenDatabase.withArgs(id).resolves({ + databaseDoc: doc, + rows: rowsFolder, + }); + }); + }); + }); +}); + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +Cypress.Commands.add('mockDocument', (id: string) => { + cy.fixture(`document/${id}`).then((subDocument) => { + const doc = new Y.Doc(); + const state = new Uint8Array(subDocument.data.doc_state); + + applyYDoc(doc, state); + + cy.stub(JSDocumentService.prototype, 'openDocument').withArgs(id).resolves(doc); + }); +}); diff --git a/frontend/appflowy_web_app/cypress/support/component-index.html b/frontend/appflowy_web_app/cypress/support/component-index.html new file mode 100644 index 0000000000..b8b58ae50c --- /dev/null +++ b/frontend/appflowy_web_app/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/component.ts b/frontend/appflowy_web_app/cypress/support/component.ts new file mode 100644 index 0000000000..b86b3d71bb --- /dev/null +++ b/frontend/appflowy_web_app/cypress/support/component.ts @@ -0,0 +1,71 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import '@cypress/code-coverage/support'; +import './commands'; +import './document'; +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18'; + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface Chainable { + mount: typeof mount; + mockAPI: () => void; + mockDatabase: () => void; + mockCurrentWorkspace: () => void; + mockGetWorkspaceDatabases: () => void; + mockDocument: (id: string) => void; + clickOutside: () => void; + getTestingSelector: (testId: string) => Chainable>; + } + } +} + +Cypress.Commands.add('mount', mount); + +Cypress.Commands.add('getTestingSelector', (testId: string) => { + return cy.get(`[data-testid="${testId}"]`); +}); + +Cypress.Commands.add('clickOutside', () => { + cy.document().then((doc) => { + // [0, 0] is the top left corner of the window + const x = 0; + const y = 0; + + const evt = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window, + clientX: x, + clientY: y, + }); + + // Dispatch the event + doc.elementFromPoint(x, y)?.dispatchEvent(evt); + }); +}); +// Example use: +// cy.mount() + diff --git a/frontend/appflowy_web_app/cypress/support/document.ts b/frontend/appflowy_web_app/cypress/support/document.ts new file mode 100644 index 0000000000..757974f14b --- /dev/null +++ b/frontend/appflowy_web_app/cypress/support/document.ts @@ -0,0 +1,75 @@ +import { BlockId, BlockType, YBlocks, YChildrenMap, YjsEditorKey, YTextMap } from '@/application/collab.type'; +import { nanoid } from 'nanoid'; +import * as Y from 'yjs'; + +export class DocumentTest { + public doc: Y.Doc; + + private blocks: YBlocks; + + private childrenMap: YChildrenMap; + + private textMap: YTextMap; + + private pageId: string; + + constructor() { + const doc = new Y.Doc(); + + this.doc = doc; + const collab = doc.getMap(YjsEditorKey.data_section); + const document = new Y.Map(); + const blocks = new Y.Map() as YBlocks; + const pageId = nanoid(8); + const meta = new Y.Map(); + const childrenMap = new Y.Map() as YChildrenMap; + const textMap = new Y.Map() as YTextMap; + + const block = new Y.Map(); + + block.set(YjsEditorKey.block_id, pageId); + block.set(YjsEditorKey.block_type, BlockType.Page); + block.set(YjsEditorKey.block_children, pageId); + block.set(YjsEditorKey.block_external_id, pageId); + block.set(YjsEditorKey.block_external_type, YjsEditorKey.text); + block.set(YjsEditorKey.block_data, ''); + blocks.set(pageId, block); + + document.set(YjsEditorKey.page_id, pageId); + document.set(YjsEditorKey.blocks, blocks); + document.set(YjsEditorKey.meta, meta); + meta.set(YjsEditorKey.children_map, childrenMap); + meta.set(YjsEditorKey.text_map, textMap); + collab.set(YjsEditorKey.document, document); + + this.blocks = blocks; + this.childrenMap = childrenMap; + this.textMap = textMap; + this.pageId = pageId; + } + + insertParagraph(text: string) { + const blockId = nanoid(8); + const block = new Y.Map(); + + block.set(YjsEditorKey.block_id, blockId); + block.set(YjsEditorKey.block_type, BlockType.Paragraph); + block.set(YjsEditorKey.block_children, blockId); + block.set(YjsEditorKey.block_external_id, blockId); + block.set(YjsEditorKey.block_external_type, YjsEditorKey.text); + block.set(YjsEditorKey.block_parent, this.pageId); + block.set(YjsEditorKey.block_data, ''); + this.blocks.set(blockId, block); + const pageChildren = this.childrenMap.get(this.pageId) ?? new Y.Array(); + + pageChildren.push([blockId]); + this.childrenMap.set(this.pageId, pageChildren); + + const blockText = new Y.Text(); + + blockText.insert(0, text); + this.textMap.set(blockId, blockText); + + return blockText; + } +} diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html new file mode 100644 index 0000000000..a71f082fc3 --- /dev/null +++ b/frontend/appflowy_web_app/index.html @@ -0,0 +1,66 @@ + + + + + + + AppFlowy + + + + + + + + + + + + + +
+ + + + + diff --git a/frontend/appflowy_web_app/jest.config.cjs b/frontend/appflowy_web_app/jest.config.cjs new file mode 100644 index 0000000000..b226459c4b --- /dev/null +++ b/frontend/appflowy_web_app/jest.config.cjs @@ -0,0 +1,41 @@ +const { compilerOptions } = require('./tsconfig.json'); +const { pathsToModuleNameMapper } = require('ts-jest'); +const esModules = ['lodash-es', 'nanoid'].join('|'); + +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + roots: [''], + modulePaths: [compilerOptions.baseUrl], + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths), + '^lodash-es(/(.*)|$)': 'lodash$1', + '^nanoid(/(.*)|$)': 'nanoid$1', + }, + 'transform': { + '^.+\\.(j|t)sx?$': 'ts-jest', + '(.*)/node_modules/nanoid/.+\\.(j|t)sx?$': 'ts-jest', + }, + 'transformIgnorePatterns': [`/node_modules/(?!${esModules})`], + testMatch: ['**/*.test.ts', '**/*.test.tsx'], + coverageDirectory: '/coverage/jest', + collectCoverage: true, + coverageProvider: 'v8', + coveragePathIgnorePatterns: [ + '/cypress/', + '/coverage/', + '/node_modules/', + '/__tests__/', + '/__mocks__/', + '/__fixtures__/', + '/__helpers__/', + '/__utils__/', + '/__constants__/', + '/__types__/', + '/__mocks__/', + '/__stubs__/', + '/__fixtures__/', + '/application/folder-yjs/', + ], +}; \ No newline at end of file diff --git a/frontend/appflowy_web_app/nginx.conf b/frontend/appflowy_web_app/nginx.conf new file mode 100644 index 0000000000..729255a778 --- /dev/null +++ b/frontend/appflowy_web_app/nginx.conf @@ -0,0 +1,102 @@ +# nginx.conf +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log notice; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout 65; + + gzip on; + + gzip_static on; + + gzip_http_version 1.0; + + gzip_comp_level 5; + + gzip_vary on; + + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript application/wasm; + + # Existing server block for HTTP + server { + listen 80; + server_name localhost; + #server_name appflowy.com *.appflowy.com; + + location / { + return 301 https://$host$request_uri; + } + + } + + # Additional server block for HTTPS + server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name localhost; + #server_name appflowy.com *.appflowy.com; + + ssl_certificate /etc/ssl/certs/nginx-signed.crt; + ssl_certificate_key /etc/ssl/private/nginx-signed.key; + + ssl_session_cache shared:SSL:1m; + ssl_session_timeout 5m; + + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + location / { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location /static/ { + root /usr/share/nginx/html; + expires 30d; + access_log off; + location ~* \.wasm$ { + types { application/wasm wasm; } + default_type application/wasm; + } + } + + location /appflowy.svg { + root /usr/share/nginx/html; + expires 30d; + access_log off; + } + + error_page 404 /404.html; + location = /404.html { + root /usr/share/nginx/html; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +} diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json new file mode 100644 index 0000000000..f822a6b0f9 --- /dev/null +++ b/frontend/appflowy_web_app/package.json @@ -0,0 +1,167 @@ +{ + "name": "appflowy_web_app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "pnpm run sync:i18n && vite", + "dev:tauri": "pnpm run sync:i18n && vite", + "build": "pnpm run sync:i18n && vite build", + "build:tauri": "vite build", + "lint:tauri": "pnpm run sync:i18n && tsc --noEmit && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore", + "lint": "pnpm run sync:i18n && tsc --noEmit --project tsconfig.web.json && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore.web", + "start": "vite preview --port 3000", + "tauri:dev": "tauri dev", + "css:variables": "node scripts/generateTailwindColors.cjs", + "sync:i18n": "node scripts/i18n.cjs", + "link:client-api": "rm -rf node_modules/.vite && node scripts/create-symlink.cjs", + "analyze": "cross-env ANALYZE_MODE=true vite build", + "cypress:open": "cypress open", + "test": "pnpm run test:unit && pnpm run test:components", + "test:components": "cypress run --component --browser chrome --headless", + "test:unit": "jest --coverage", + "test:cy": "cypress run", + "coverage": "pnpm run test:unit && pnpm run test:components" + }, + "dependencies": { + "@appflowyinc/client-api-wasm": "0.0.3", + "@atlaskit/primitives": "^5.5.3", + "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@jest/globals": "^29.7.0", + "@mui/icons-material": "^5.11.11", + "@mui/material": "6.0.0-alpha.2", + "@mui/x-date-pickers-pro": "^6.18.2", + "@reduxjs/toolkit": "2.0.0", + "@slate-yjs/core": "^1.0.2", + "@tauri-apps/api": "^1.5.3", + "@types/react-swipeable-views": "^0.13.4", + "async-retry": "^1.3.3", + "axios": "^1.6.8", + "colorthief": "^2.4.0", + "dayjs": "^1.11.9", + "decimal.js": "^10.4.3", + "emoji-mart": "^5.5.2", + "emoji-regex": "^10.2.1", + "events": "^3.3.0", + "google-protobuf": "^3.15.12", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-resources-to-backend": "^1.1.4", + "is-hotkey": "^0.2.0", + "jest": "^29.5.0", + "js-base64": "^3.7.5", + "katex": "^0.16.7", + "lodash-es": "^4.17.21", + "nanoid": "^4.0.0", + "numeral": "^2.0.6", + "prismjs": "^1.29.0", + "protoc-gen-ts": "0.8.7", + "quill": "^1.3.7", + "quill-delta": "^5.1.0", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-big-calendar": "^1.8.5", + "react-color": "^2.19.3", + "react-custom-scrollbars": "^4.2.1", + "react-custom-scrollbars-2": "^4.5.0", + "react-datepicker": "^4.23.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.13", + "react-hot-toast": "^2.4.1", + "react-i18next": "^14.1.0", + "react-katex": "^3.0.1", + "react-measure": "^2.5.2", + "react-redux": "^8.0.5", + "react-router-dom": "^6.22.3", + "react-swipeable-views": "^0.14.0", + "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.20", + "react-vtree": "^2.0.4", + "react-window": "^1.8.10", + "react18-input-otp": "^1.1.2", + "redux": "^4.2.1", + "rxjs": "^7.8.0", + "sass": "^1.70.0", + "slate": "^0.101.4", + "slate-history": "^0.100.0", + "slate-react": "^0.101.3", + "smooth-scroll-into-view-if-needed": "^2.0.2", + "ts-results": "^3.3.0", + "unsplash-js": "^7.0.19", + "utf8": "^3.0.0", + "validator": "^13.11.0", + "vite-plugin-wasm": "^3.3.0", + "y-indexeddb": "9.0.12", + "yjs": "^13.6.14" + }, + "devDependencies": { + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@cypress/code-coverage": "^3.12.39", + "@istanbuljs/nyc-config-babel": "^3.0.0", + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@svgr/plugin-svgo": "^8.0.1", + "@tauri-apps/cli": "^1.5.11", + "@testing-library/react": "^16.0.0", + "@types/google-protobuf": "^3.15.12", + "@types/is-hotkey": "^0.1.7", + "@types/jest": "^29.5.3", + "@types/katex": "^0.16.0", + "@types/lodash-es": "^4.17.11", + "@types/node": "^20.11.30", + "@types/numeral": "^2.0.5", + "@types/prismjs": "^1.26.0", + "@types/quill": "^2.0.10", + "@types/react": "^18.2.66", + "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-big-calendar": "^1.8.9", + "@types/react-color": "^3.0.6", + "@types/react-custom-scrollbars": "^4.0.13", + "@types/react-datepicker": "^4.19.3", + "@types/react-dom": "^18.2.22", + "@types/react-katex": "^3.0.0", + "@types/react-measure": "^2.0.12", + "@types/react-transition-group": "^4.4.6", + "@types/react-window": "^1.8.8", + "@types/utf8": "^3.0.1", + "@types/uuid": "^9.0.1", + "@types/validator": "^13.11.9", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.13", + "babel-jest": "^29.6.2", + "chalk": "^4.1.2", + "cross-env": "^7.0.3", + "cypress": "^13.7.2", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "istanbul-lib-coverage": "^3.2.2", + "jest-environment-jsdom": "^29.6.2", + "nyc": "^15.1.0", + "postcss": "^8.4.21", + "prettier": "2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "rollup-plugin-visualizer": "^5.12.0", + "style-dictionary": "^3.9.2", + "tailwindcss": "^3.2.7", + "ts-jest": "^29.1.1", + "ts-node-dev": "^2.0.0", + "tsconfig-paths-jest": "^0.0.1", + "typescript": "4.9.5", + "uuid": "^9.0.0", + "vite": "^5.2.0", + "vite-plugin-compression2": "^1.0.0", + "vite-plugin-importer": "^0.2.5", + "vite-plugin-istanbul": "^6.0.2", + "vite-plugin-svgr": "^3.2.0", + "vite-plugin-terminal": "^1.2.0", + "vite-plugin-total-bundle-size": "^1.0.7" + } +} diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml new file mode 100644 index 0000000000..194beaa5dd --- /dev/null +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -0,0 +1,11420 @@ +lockfileVersion: '6.0' + +dependencies: + '@appflowyinc/client-api-wasm': + specifier: 0.0.3 + version: 0.0.3 + '@atlaskit/primitives': + specifier: ^5.5.3 + version: 5.7.0(@types/react@18.2.66)(react@18.2.0) + '@emoji-mart/data': + specifier: ^1.1.2 + version: 1.2.1 + '@emoji-mart/react': + specifier: ^1.1.1 + version: 1.1.1(emoji-mart@5.6.0)(react@18.2.0) + '@emotion/react': + specifier: ^11.10.6 + version: 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': + specifier: ^11.10.6 + version: 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 + '@mui/icons-material': + specifier: ^5.11.11 + version: 5.15.18(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0) + '@mui/material': + specifier: 6.0.0-alpha.2 + version: 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers-pro': + specifier: ^6.18.2 + version: 6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@reduxjs/toolkit': + specifier: 2.0.0 + version: 2.0.0(react-redux@8.1.3)(react@18.2.0) + '@slate-yjs/core': + specifier: ^1.0.2 + version: 1.0.2(slate@0.101.5)(yjs@13.6.15) + '@tauri-apps/api': + specifier: ^1.5.3 + version: 1.5.6 + '@types/react-swipeable-views': + specifier: ^0.13.4 + version: 0.13.5 + async-retry: + specifier: ^1.3.3 + version: 1.3.3 + axios: + specifier: ^1.6.8 + version: 1.7.2 + colorthief: + specifier: ^2.4.0 + version: 2.4.0 + dayjs: + specifier: ^1.11.9 + version: 1.11.9 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 + emoji-mart: + specifier: ^5.5.2 + version: 5.6.0 + emoji-regex: + specifier: ^10.2.1 + version: 10.3.0 + events: + specifier: ^3.3.0 + version: 3.3.0 + google-protobuf: + specifier: ^3.15.12 + version: 3.21.2 + i18next: + specifier: ^22.4.10 + version: 22.5.1 + i18next-browser-languagedetector: + specifier: ^7.0.1 + version: 7.2.1 + i18next-resources-to-backend: + specifier: ^1.1.4 + version: 1.2.1 + is-hotkey: + specifier: ^0.2.0 + version: 0.2.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@20.11.30) + js-base64: + specifier: ^3.7.5 + version: 3.7.7 + katex: + specifier: ^0.16.7 + version: 0.16.10 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + nanoid: + specifier: ^4.0.0 + version: 4.0.2 + numeral: + specifier: ^2.0.6 + version: 2.0.6 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 + protoc-gen-ts: + specifier: 0.8.7 + version: 0.8.7 + quill: + specifier: ^1.3.7 + version: 1.3.7 + quill-delta: + specifier: ^5.1.0 + version: 5.1.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) + react-big-calendar: + specifier: ^1.8.5 + version: 1.12.2(react-dom@18.2.0)(react@18.2.0) + react-color: + specifier: ^2.19.3 + version: 2.19.3(react@18.2.0) + react-custom-scrollbars: + specifier: ^4.2.1 + version: 4.2.1(react-dom@18.2.0)(react@18.2.0) + react-custom-scrollbars-2: + specifier: ^4.5.0 + version: 4.5.0(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.25.0(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.2.0) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) + react-i18next: + specifier: ^14.1.0 + version: 14.1.2(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0) + react-katex: + specifier: ^3.0.1 + version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-measure: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.2.0)(react@18.2.0) + react-redux: + specifier: ^8.0.5 + version: 8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + react-router-dom: + specifier: ^6.22.3 + version: 6.23.1(react-dom@18.2.0)(react@18.2.0) + react-swipeable-views: + specifier: ^0.14.0 + version: 0.14.0(react@18.2.0) + react-transition-group: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-virtualized-auto-sizer: + specifier: ^1.0.20 + version: 1.0.24(react-dom@18.2.0)(react@18.2.0) + react-vtree: + specifier: ^2.0.4 + version: 2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0) + react-window: + specifier: ^1.8.10 + version: 1.8.10(react-dom@18.2.0)(react@18.2.0) + react18-input-otp: + specifier: ^1.1.2 + version: 1.1.4(react-dom@18.2.0)(react@18.2.0) + redux: + specifier: ^4.2.1 + version: 4.2.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.0 + sass: + specifier: ^1.70.0 + version: 1.77.2 + slate: + specifier: ^0.101.4 + version: 0.101.5 + slate-history: + specifier: ^0.100.0 + version: 0.100.0(slate@0.101.5) + slate-react: + specifier: ^0.101.3 + version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) + smooth-scroll-into-view-if-needed: + specifier: ^2.0.2 + version: 2.0.2 + ts-results: + specifier: ^3.3.0 + version: 3.3.0 + unsplash-js: + specifier: ^7.0.19 + version: 7.0.19 + utf8: + specifier: ^3.0.0 + version: 3.0.0 + validator: + specifier: ^13.11.0 + version: 13.12.0 + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.3.0(vite@5.2.0) + y-indexeddb: + specifier: 9.0.12 + version: 9.0.12(yjs@13.6.15) + yjs: + specifier: ^13.6.14 + version: 13.6.15 + +devDependencies: + '@babel/preset-env': + specifier: ^7.24.7 + version: 7.24.7(@babel/core@7.24.3) + '@babel/preset-react': + specifier: ^7.24.7 + version: 7.24.7(@babel/core@7.24.3) + '@babel/preset-typescript': + specifier: ^7.24.7 + version: 7.24.7(@babel/core@7.24.3) + '@cypress/code-coverage': + specifier: ^3.12.39 + version: 3.12.39(@babel/core@7.24.3)(@babel/preset-env@7.24.7)(babel-loader@9.1.3)(cypress@13.7.2)(webpack@5.91.0) + '@istanbuljs/nyc-config-babel': + specifier: ^3.0.0 + version: 3.0.0(@babel/register@7.24.6)(babel-plugin-istanbul@6.1.1) + '@istanbuljs/nyc-config-typescript': + specifier: ^1.0.2 + version: 1.0.2(nyc@15.1.0) + '@svgr/plugin-svgo': + specifier: ^8.0.1 + version: 8.0.1(@svgr/core@8.1.0)(typescript@4.9.5) + '@tauri-apps/cli': + specifier: ^1.5.11 + version: 1.5.11 + '@testing-library/react': + specifier: ^16.0.0 + version: 16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/google-protobuf': + specifier: ^3.15.12 + version: 3.15.12 + '@types/is-hotkey': + specifier: ^0.1.7 + version: 0.1.7 + '@types/jest': + specifier: ^29.5.3 + version: 29.5.3 + '@types/katex': + specifier: ^0.16.0 + version: 0.16.0 + '@types/lodash-es': + specifier: ^4.17.11 + version: 4.17.11 + '@types/node': + specifier: ^20.11.30 + version: 20.11.30 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 + '@types/prismjs': + specifier: ^1.26.0 + version: 1.26.0 + '@types/quill': + specifier: ^2.0.10 + version: 2.0.10 + '@types/react': + specifier: ^18.2.66 + version: 18.2.66 + '@types/react-beautiful-dnd': + specifier: ^13.1.3 + version: 13.1.3 + '@types/react-big-calendar': + specifier: ^1.8.9 + version: 1.8.9 + '@types/react-color': + specifier: ^3.0.6 + version: 3.0.6 + '@types/react-custom-scrollbars': + specifier: ^4.0.13 + version: 4.0.13 + '@types/react-datepicker': + specifier: ^4.19.3 + version: 4.19.3(react-dom@18.2.0)(react@18.2.0) + '@types/react-dom': + specifier: ^18.2.22 + version: 18.2.22 + '@types/react-katex': + specifier: ^3.0.0 + version: 3.0.0 + '@types/react-measure': + specifier: ^2.0.12 + version: 2.0.12 + '@types/react-transition-group': + specifier: ^4.4.6 + version: 4.4.6 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 + '@types/utf8': + specifier: ^3.0.1 + version: 3.0.1 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.1 + '@types/validator': + specifier: ^13.11.9 + version: 13.11.9 + '@typescript-eslint/eslint-plugin': + specifier: ^7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.2.0) + autoprefixer: + specifier: ^10.4.13 + version: 10.4.13(postcss@8.4.21) + babel-jest: + specifier: ^29.6.2 + version: 29.6.2(@babel/core@7.24.3) + chalk: + specifier: ^4.1.2 + version: 4.1.2 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + cypress: + specifier: ^13.7.2 + version: 13.7.2 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-react: + specifier: ^7.32.2 + version: 7.32.2(eslint@8.57.0) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.57.0) + eslint-plugin-react-refresh: + specifier: ^0.4.6 + version: 0.4.6(eslint@8.57.0) + istanbul-lib-coverage: + specifier: ^3.2.2 + version: 3.2.2 + jest-environment-jsdom: + specifier: ^29.6.2 + version: 29.6.2 + nyc: + specifier: ^15.1.0 + version: 15.1.0 + postcss: + specifier: ^8.4.21 + version: 8.4.21 + prettier: + specifier: 2.8.4 + version: 2.8.4 + prettier-plugin-tailwindcss: + specifier: ^0.2.2 + version: 0.2.2(prettier@2.8.4) + rollup-plugin-visualizer: + specifier: ^5.12.0 + version: 5.12.0 + style-dictionary: + specifier: ^3.9.2 + version: 3.9.2 + tailwindcss: + specifier: ^3.2.7 + version: 3.2.7(postcss@8.4.21) + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.24.3)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.30)(typescript@4.9.5) + tsconfig-paths-jest: + specifier: ^0.0.1 + version: 0.0.1 + typescript: + specifier: 4.9.5 + version: 4.9.5 + uuid: + specifier: ^9.0.0 + version: 9.0.0 + vite: + specifier: ^5.2.0 + version: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + vite-plugin-compression2: + specifier: ^1.0.0 + version: 1.0.0 + vite-plugin-importer: + specifier: ^0.2.5 + version: 0.2.5 + vite-plugin-istanbul: + specifier: ^6.0.2 + version: 6.0.2(vite@5.2.0) + vite-plugin-svgr: + specifier: ^3.2.0 + version: 3.2.0(typescript@4.9.5)(vite@5.2.0) + vite-plugin-terminal: + specifier: ^1.2.0 + version: 1.2.0(vite@5.2.0) + vite-plugin-total-bundle-size: + specifier: ^1.0.7 + version: 1.0.7(vite@5.2.0) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + /@appflowyinc/client-api-wasm@0.0.3: + resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} + dev: false + + /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): + resolution: {integrity: sha512-iO6+hIp09dF4iAZQarVz3vKY1kM5Ij5CExYcK9jgc2q+OH8nv8n+BPFeJTdzGOGopmbUZn5Opj9pYQvge1Gr4Q==} + peerDependencies: + react: ^16.8.0 + dependencies: + react: 18.2.0 + tslib: 2.6.2 + dev: false + + /@atlaskit/analytics-next@9.3.0(react@18.2.0): + resolution: {integrity: sha512-mR5CndP92k2gFl8sWu4DJZZEpEQ4bnp5Z3fWCZE1oySiOKK8iM+KzKH4FMCaSUGOhWW6/5VeuXCcXvaogaAmsA==} + peerDependencies: + react: ^16.8.0 + dependencies: + '@atlaskit/analytics-next-stable-react-context': 1.0.1(react@18.2.0) + '@atlaskit/platform-feature-flags': 0.2.5 + '@babel/runtime': 7.24.1 + prop-types: 15.8.1 + react: 18.2.0 + use-memo-one: 1.1.3(react@18.2.0) + dev: false + + /@atlaskit/app-provider@1.3.2(react@18.2.0): + resolution: {integrity: sha512-tvyMNrydTyu5yJK78zUjqbJwgNRdW5nQ31imWFav5PvWXwc36lfGiTXRX/JIxJNBC3rBJ0gLAyrrb9YMzyWcTw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ~18.2.0 + dependencies: + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@babel/runtime': 7.24.1 + bind-event-listener: 3.0.0 + react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@atlaskit/css@0.1.0(react@18.2.0): + resolution: {integrity: sha512-FQfiLoYJrwTYhjSpa+RA8omPAPlJ5rl0OCJ0NAkMXRGx1o8ItNBW5EcRBwW0wUHaBOZ4oFS5EUshk185E/G/zQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ~18.2.0 + dependencies: + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@babel/runtime': 7.24.1 + '@compiled/react': 0.17.1(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@atlaskit/ds-lib@2.3.1(react@18.2.0): + resolution: {integrity: sha512-DVUE3hYLhdEZy4NnsxqiCqKC5Ym3CM/DGRQlnSPcABFNL0N0FfTXso3pLpkJnMZBtEnd2pn13mPJ2VQlSISRuw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ~18.2.0 + dependencies: + '@babel/runtime': 7.24.1 + bind-event-listener: 3.0.0 + react: 18.2.0 + dev: false + + /@atlaskit/interaction-context@2.1.4(react@18.2.0): + resolution: {integrity: sha512-MTuHN8wLYBPADE83Q+9KF5BcKyMW9/FkmA+lB/XnHwYIL86sMPzMSTM0DPG7crq/JI0JM0jlyY3Xzz0Aba7G+A==} + peerDependencies: + react: ^16.8.0 + dependencies: + '@babel/runtime': 7.24.1 + react: 18.2.0 + dev: false + + /@atlaskit/platform-feature-flags@0.2.5: + resolution: {integrity: sha512-0fD2aDxn2mE59D4acUhVib+YF2HDYuuPH50aYwpQdcV/CsVkAaJsMKy8WhWSulcRFeMYp72kfIfdy0qGdRB7Uw==} + dependencies: + '@babel/runtime': 7.24.6 + dev: false + + /@atlaskit/primitives@5.7.0(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-eCLyHN1BllNpwqA2YqCmYpqwoiNVcW3R6bHrpKmsW8uvPE/+Bd45hOiPwvCPJUPyK1ZNMfnkegWKkoxcmjMYIQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@atlaskit/analytics-next': 9.3.0(react@18.2.0) + '@atlaskit/app-provider': 1.3.2(react@18.2.0) + '@atlaskit/css': 0.1.0(react@18.2.0) + '@atlaskit/ds-lib': 2.3.1(react@18.2.0) + '@atlaskit/interaction-context': 2.1.4(react@18.2.0) + '@atlaskit/tokens': 1.49.1(react@18.2.0) + '@atlaskit/visually-hidden': 1.3.0(@types/react@18.2.66)(react@18.2.0) + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/serialize': 1.1.4 + bind-event-listener: 3.0.0 + react: 18.2.0 + tiny-invariant: 1.3.3 + transitivePeerDependencies: + - '@types/react' + - supports-color + dev: false + + /@atlaskit/tokens@1.49.1(react@18.2.0): + resolution: {integrity: sha512-3SuhRMPUTU6b+nv0zVoGsNoqrUMtwQ/4iBbKhwaylRITanFxlxBwzW8XCCnn4sp1S2JupiT5BksI0h6jRoKN9Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ~18.2.0 + dependencies: + '@atlaskit/ds-lib': 2.3.1(react@18.2.0) + '@atlaskit/platform-feature-flags': 0.2.5 + '@babel/runtime': 7.24.1 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + bind-event-listener: 3.0.0 + react: 18.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@atlaskit/visually-hidden@1.3.0(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-iOHCxRnhNV3gnqOHuyLOnsFibfHpr1T28XUPYZjtN9bDQbn1GdSDYLoIHnLK+2enqdILirsuUWi93mWM3dCCwg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ~18.2.0 + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 + + /@babel/code-frame@7.24.7: + resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.7 + picocolors: 1.0.0 + dev: true + + /@babel/compat-data@7.24.1: + resolution: {integrity: sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==} + engines: {node: '>=6.9.0'} + + /@babel/compat-data@7.24.7: + resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/core@7.24.3: + resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.1 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) + '@babel/helpers': 7.24.1 + '@babel/parser': 7.24.1 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + convert-source-map: 2.0.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /@babel/generator@7.24.1: + resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + /@babel/generator@7.24.7: + resolution: {integrity: sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + dev: true + + /@babel/helper-annotate-as-pure@7.24.7: + resolution: {integrity: sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-builder-binary-assignment-operator-visitor@7.24.7: + resolution: {integrity: sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.1 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + /@babel/helper-compilation-targets@7.24.7: + resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + dev: true + + /@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.3) + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-create-regexp-features-plugin@7.24.6(@babel/core@7.24.3): + resolution: {integrity: sha512-C875lFBIWWwyv6MHZUG9HmRrlTDgOsLWZfYR0nW69gaKJNe0/Mpxx5r0EID2ZdHQkdUmQo2t0uNckTL08/1BgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-create-regexp-features-plugin@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + + /@babel/helper-define-polyfill-provider@0.6.2(@babel/core@7.24.3): + resolution: {integrity: sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + debug: 4.3.4(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + /@babel/helper-environment-visitor@7.24.7: + resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 + + /@babel/helper-function-name@7.24.7: + resolution: {integrity: sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-hoist-variables@7.24.7: + resolution: {integrity: sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-member-expression-to-functions@7.24.7: + resolution: {integrity: sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-imports@7.24.3: + resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-module-imports@7.24.7: + resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.3): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.24.3 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + + /@babel/helper-module-transforms@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-optimise-call-expression@7.24.7: + resolution: {integrity: sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-plugin-utils@7.24.0: + resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-plugin-utils@7.24.7: + resolution: {integrity: sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==} + engines: {node: '>=6.9.0'} + + /@babel/helper-remap-async-to-generator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-wrap-function': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-replace-supers@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-member-expression-to-functions': 7.24.7 + '@babel/helper-optimise-call-expression': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-simple-access@7.24.7: + resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-skip-transparent-expression-wrappers@7.24.7: + resolution: {integrity: sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-split-export-declaration@7.24.7: + resolution: {integrity: sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-string-parser@7.24.6: + resolution: {integrity: sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.24.7: + resolution: {integrity: sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/helper-wrap-function@7.24.7: + resolution: {integrity: sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-function-name': 7.24.7 + '@babel/template': 7.24.7 + '@babel/traverse': 7.24.7 + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/helpers@7.24.1: + resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + transitivePeerDependencies: + - supports-color + + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + + /@babel/highlight@7.24.7: + resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.24.7 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + dev: true + + /@babel/parser@7.24.1: + resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 + dev: true + + /@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.3): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.3): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.3): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-assertions@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-attributes@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.3): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.3): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.3): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.6(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-arrow-functions@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-async-generator-functions@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-async-to-generator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-remap-async-to-generator': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-block-scoped-functions@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-block-scoping@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-class-properties@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-class-static-block@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-classes@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-CFbbBigp8ln4FU6Bpy6g7sE8B/WmCmzvivzUC6xDAdWVsjYTXijpuuGJmYkAaoWAzcItGKT3IOAbxRItZ5HTjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.3) + '@babel/helper-split-export-declaration': 7.24.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-computed-properties@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/template': 7.24.7 + dev: true + + /@babel/plugin-transform-destructuring@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-19eJO/8kdCQ9zISOf+SEUJM/bAUIsvY3YDnXZTupUCQ8LgrWnsG/gFB9dvXqdXnRXMAM8fvt7b0CBKQHNGy1mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-dotall-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-duplicate-keys@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-dynamic-import@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-exponentiation-operator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-export-namespace-from@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-for-of@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-function-name@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-json-strings@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-literals@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-logical-assignment-operators@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-member-expression-literals@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-modules-amd@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-iFI8GDxtevHJ/Z22J5xQpVqFLlMNstcLXh994xifFwxxGslr2ZXXLWgtBeLctOD63UFDArdvN6Tg8RFw+aEmjQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-simple-access': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-systemjs@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-modules-umd@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-named-capturing-groups-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-new-target@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-nullish-coalescing-operator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-numeric-separator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-object-rest-spread@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-object-super@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-optional-catch-binding@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + dev: true + + /@babel/plugin-transform-optional-chaining@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-tK+0N9yd4j+x/4hxF3F0e0fu/VdcxU18y5SevtyM/PCFlQvXbR0Zmlo2eBrKtVipGNFzpq56o8WsIIKcJFUCRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-parameters@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-private-methods@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-private-property-in-object@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-property-literals@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-react-display-name@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-react-jsx-development@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-react-jsx-self@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + + /@babel/plugin-transform-react-jsx@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-module-imports': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.3) + '@babel/types': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-react-pure-annotations@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-regenerator@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + regenerator-transform: 0.15.2 + dev: true + + /@babel/plugin-transform-reserved-words@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-shorthand-properties@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-spread@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-sticky-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-template-literals@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-typeof-symbol@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-VtR8hDy7YLB7+Pet9IarXjg/zgCMSF+1mNS/EQEiEaUPoFXCVsHG64SIxcaaI2zJgRiv+YmgaQESUfWAdbjzgg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-typescript@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-annotate-as-pure': 7.24.7 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + '@babel/plugin-syntax-typescript': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/plugin-transform-unicode-escapes@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-property-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/plugin-transform-unicode-sets-regex@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-create-regexp-features-plugin': 7.24.7(@babel/core@7.24.3) + '@babel/helper-plugin-utils': 7.24.7 + dev: true + + /@babel/preset-env@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-1YZNsc+y6cTvWlDHidMBsQZrZfEFjRIo/BZCT906PMdzOyXtSLTgqGdrpcuTDCXyd11Am5uQULtDIcCfnTc8fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.3 + '@babel/helper-compilation-targets': 7.24.7 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.3) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.3) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-import-assertions': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-syntax-import-attributes': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.3) + '@babel/plugin-transform-arrow-functions': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-async-generator-functions': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-async-to-generator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-block-scoped-functions': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-block-scoping': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-class-properties': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-class-static-block': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-classes': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-computed-properties': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-destructuring': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-dotall-regex': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-duplicate-keys': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-dynamic-import': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-exponentiation-operator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-export-namespace-from': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-for-of': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-function-name': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-json-strings': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-literals': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-logical-assignment-operators': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-member-expression-literals': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-modules-amd': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-modules-systemjs': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-modules-umd': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-new-target': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-numeric-separator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-object-rest-spread': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-object-super': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-optional-catch-binding': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-optional-chaining': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-parameters': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-private-methods': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-private-property-in-object': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-property-literals': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-regenerator': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-reserved-words': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-shorthand-properties': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-spread': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-sticky-regex': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-template-literals': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-typeof-symbol': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-unicode-escapes': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-unicode-property-regex': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-unicode-regex': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-unicode-sets-regex': 7.24.7(@babel/core@7.24.3) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.3) + babel-plugin-polyfill-corejs2: 0.4.11(@babel/core@7.24.3) + babel-plugin-polyfill-corejs3: 0.10.4(@babel/core@7.24.3) + babel-plugin-polyfill-regenerator: 0.6.2(@babel/core@7.24.3) + core-js-compat: 3.37.1 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.3): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/types': 7.24.6 + esutils: 2.0.3 + dev: true + + /@babel/preset-react@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-transform-react-display-name': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-react-jsx': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-react-jsx-development': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-react-pure-annotations': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/preset-typescript@7.24.7(@babel/core@7.24.3): + resolution: {integrity: sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.7 + '@babel/helper-validator-option': 7.24.7 + '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.3) + '@babel/plugin-transform-typescript': 7.24.7(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/register@7.24.6(@babel/core@7.24.3): + resolution: {integrity: sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + clone-deep: 4.0.1 + find-cache-dir: 2.1.0 + make-dir: 2.1.0 + pirates: 4.0.6 + source-map-support: 0.5.21 + dev: true + + /@babel/regjsgen@0.8.0: + resolution: {integrity: sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==} + dev: true + + /@babel/runtime@7.0.0: + resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} + dependencies: + regenerator-runtime: 0.12.1 + dev: false + + /@babel/runtime@7.24.1: + resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + + /@babel/runtime@7.24.6: + resolution: {integrity: sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + + /@babel/template@7.24.7: + resolution: {integrity: sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + dev: true + + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.1 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/traverse@7.24.7: + resolution: {integrity: sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/generator': 7.24.7 + '@babel/helper-environment-visitor': 7.24.7 + '@babel/helper-function-name': 7.24.7 + '@babel/helper-hoist-variables': 7.24.7 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/parser': 7.24.7 + '@babel/types': 7.24.7 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + /@babel/types@7.24.6: + resolution: {integrity: sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.6 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: true + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: true + optional: true + + /@compiled/react@0.17.1(react@18.2.0): + resolution: {integrity: sha512-1CzTOrwNHOUmz9QGYHv8R8J6ejUyaNYiaUN6/dIM0Wu3G5CIam0KgsqvRikfGPrTtBfAQYMmdI9ytzxUKYwJrg==} + peerDependencies: + react: '>= 16.12.0' + dependencies: + csstype: 3.1.3 + react: 18.2.0 + dev: false + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@cypress/code-coverage@3.12.39(@babel/core@7.24.3)(@babel/preset-env@7.24.7)(babel-loader@9.1.3)(cypress@13.7.2)(webpack@5.91.0): + resolution: {integrity: sha512-ja7I/GRmkSAW9e3O7pideWcNUEHao0WT6sRyXQEURoxkJUASJssJ7Kb/bd3eMYmkUCiD5CRFqWR5BGF4mWVaUw==} + peerDependencies: + '@babel/core': ^7.0.1 + '@babel/preset-env': ^7.0.0 + babel-loader: ^8.3 || ^9 + cypress: '*' + webpack: ^4 || ^5 + dependencies: + '@babel/core': 7.24.3 + '@babel/preset-env': 7.24.7(@babel/core@7.24.3) + '@cypress/webpack-preprocessor': 6.0.1(@babel/core@7.24.3)(@babel/preset-env@7.24.7)(babel-loader@9.1.3)(webpack@5.91.0) + babel-loader: 9.1.3(@babel/core@7.24.3)(webpack@5.91.0) + chalk: 4.1.2 + cypress: 13.7.2 + dayjs: 1.11.10 + debug: 4.3.4(supports-color@8.1.1) + execa: 4.1.0 + globby: 11.1.0 + istanbul-lib-coverage: 3.2.2 + js-yaml: 4.1.0 + nyc: 15.1.0 + webpack: 5.91.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@cypress/request@3.0.1: + resolution: {integrity: sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==} + engines: {node: '>= 6'} + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + http-signature: 1.3.6 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.10.4 + safe-buffer: 5.1.2 + tough-cookie: 4.1.3 + tunnel-agent: 0.6.0 + uuid: 8.3.2 + dev: true + + /@cypress/webpack-preprocessor@6.0.1(@babel/core@7.24.3)(@babel/preset-env@7.24.7)(babel-loader@9.1.3)(webpack@5.91.0): + resolution: {integrity: sha512-WVNeFVSnFKxE3WZNRIriduTgqJRpevaiJIPlfqYTTzfXRD7X1Pv4woDE+G4caPV9bJqVKmVFiwzrXMRNeJxpxA==} + peerDependencies: + '@babel/core': ^7.0.1 + '@babel/preset-env': ^7.0.0 + babel-loader: ^8.3 || ^9 + webpack: ^4 || ^5 + dependencies: + '@babel/core': 7.24.3 + '@babel/preset-env': 7.24.7(@babel/core@7.24.3) + babel-loader: 9.1.3(@babel/core@7.24.3)(webpack@5.91.0) + bluebird: 3.7.1 + debug: 4.3.4(supports-color@8.1.1) + lodash: 4.17.21 + webpack: 5.91.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@cypress/xvfb@1.2.4(supports-color@8.1.1): + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@emoji-mart/data@1.2.1: + resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} + dev: false + + /@emoji-mart/react@1.1.1(emoji-mart@5.6.0)(react@18.2.0): + resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} + peerDependencies: + emoji-mart: ^5.2 + react: ^16.8 || ^17 || ^18 + dependencies: + emoji-mart: 5.6.0 + react: 18.2.0 + dev: false + + /@emotion/babel-plugin@11.11.0: + resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + dependencies: + '@babel/helper-module-imports': 7.24.3 + '@babel/runtime': 7.24.1 + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/serialize': 1.1.4 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + dev: false + + /@emotion/cache@11.11.0: + resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/sheet': 1.2.2 + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + stylis: 4.2.0 + dev: false + + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: false + + /@emotion/is-prop-valid@1.2.2: + resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} + dependencies: + '@emotion/memoize': 0.8.1 + dev: false + + /@emotion/memoize@0.8.1: + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + dev: false + + /@emotion/react@11.11.4(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@emotion/serialize@1.1.4: + resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.3 + dev: false + + /@emotion/sheet@1.2.2: + resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + dev: false + + /@emotion/styled@11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.2 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@types/react': 18.2.66 + react: 18.2.0 + dev: false + + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /@emotion/utils@1.2.1: + resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + dev: false + + /@emotion/weak-memoize@0.3.1: + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + dev: false + + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + optional: true + + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@floating-ui/core@1.6.2: + resolution: {integrity: sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==} + dependencies: + '@floating-ui/utils': 0.2.2 + dev: false + + /@floating-ui/dom@1.6.5: + resolution: {integrity: sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==} + dependencies: + '@floating-ui/core': 1.6.2 + '@floating-ui/utils': 0.2.2 + dev: false + + /@floating-ui/react-dom@2.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.5 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/utils@0.2.2: + resolution: {integrity: sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==} + dev: false + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@icons/material@0.2.4(react@18.2.0): + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + /@istanbuljs/nyc-config-babel@3.0.0(@babel/register@7.24.6)(babel-plugin-istanbul@6.1.1): + resolution: {integrity: sha512-mPnSPXfTRWCzYsT64PnuPlce6/hGMCdVVMgU2FenXipbUd+FDwUlqlTihXxpxWzcNVOp8M+L1t/kIcgoC8A7hg==} + engines: {node: '>=8'} + peerDependencies: + '@babel/register': '*' + babel-plugin-istanbul: '>=5' + dependencies: + '@babel/register': 7.24.6(@babel/core@7.24.3) + babel-plugin-istanbul: 6.1.1 + dev: true + + /@istanbuljs/nyc-config-typescript@1.0.2(nyc@15.1.0): + resolution: {integrity: sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==} + engines: {node: '>=8'} + peerDependencies: + nyc: '>=15' + dependencies: + '@istanbuljs/schema': 0.1.3 + nyc: 15.1.0 + dev: true + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.11.30) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.11.30 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 20.11.30 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.3 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.11.30 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + /@jridgewell/source-map@0.3.6: + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + + /@lokesh.dhakar/quantize@1.3.0: + resolution: {integrity: sha512-4KBSyaMj65d8A+2vnzLxtHFu4OmBU4IKO0yLxZ171Itdf9jGV4w+WbG7VsKts2jUdRkFSzsZqpZOz6hTB3qGAw==} + dev: false + + /@mui/base@5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/base@5.0.0-beta.42(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-fWRiUJVCHCPF+mxd5drn08bY2qRw3jj5f1SSQdUXmaJ/yKpk23ys8MgLO2KGVTRtbks/+ctRfgffGPbXifj0Ug==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@floating-ui/react-dom': 2.1.0(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/core-downloads-tracker@6.0.0-dev.240424162023-9968b4889d: + resolution: {integrity: sha512-doh3M3U7HUGSBIWGe1yvesSbfDguMRjP0N09ogWSBM2hovXAlgULhMgcRTepAZLLwfRxFII0bCohq6B9NqoKuw==} + dev: false + + /@mui/icons-material@5.15.18(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-jGhyw02TSLM0NgW+MDQRLLRUD/K4eN9rlK2pTBTL1OtzyZmQ8nB060zK1wA0b7cVrIiG+zyrRmNAvGWXwm2N9Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@mui/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + react: 18.2.0 + dev: false + + /@mui/material@6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-p1GpE1a7dQTns0yp0anSNX/Bh1xafTdUCt0roTyqEuL/3hCBKTURE/9/CDttwwQ+Q8oDm5KcsdtXJXJh1ts6Kw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-beta.42(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 6.0.0-dev.240424162023-9968b4889d + '@mui/system': 6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-transition-group': 4.4.10 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/private-theming@5.15.14(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/private-theming@6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-0iN+hK/OZTaiVfjFYDgWEc/frRB7Z1hfBsSJBniM4KPZnrdeHIArP+3TdYzRT0avh30O2KNkBNk0GG95BnUVEg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine@5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): + resolution: {integrity: sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine@6.0.0-alpha.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0): + resolution: {integrity: sha512-7zJYgbjZRQpGN1SGmLDOgRpJZB26JjPSeqml5m+jA4wAsIONm2im+GHfki4nE3ay0uj1S555OMeNpaQ+sG9LkA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/system@5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/private-theming': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/system@6.0.0-dev.240424162023-9968b4889d(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-Y3yCFUHN1xMK62hJJBqzZb1YQvHNaHc7JUX01eU6QTPojtIbGMF2jCOP/EQw77/byahNbxeLoAIQx10F0IR3Rw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/private-theming': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@mui/styled-engine': 6.0.0-alpha.8(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/types@7.2.14(@types/react@18.2.66): + resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.66 + dev: false + + /@mui/utils@5.15.14(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/utils@6.0.0-alpha.8(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-X5lg0bh8B6uYt/0HXV+t82HXLTOVFEKcIBmIbJ5El1h9ykXaRTenr8mORxt5UC5w9DHFhkRoI8XiM5qyDuSJVw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-date-pickers-pro@6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-lXbO8xCKRvIKgu2R8EAGhYwN1BkMS9GxTeinKZg5lCGvxTrd5pErQ4xpOxYVQq7wNLphDiY7I/Xf88VekKTNLQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 || ^3.2.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@mui/x-date-pickers': 6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-license-pro': 6.10.2(@types/react@18.2.66)(react@18.2.0) + clsx: 2.1.1 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-date-pickers@6.20.0(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-q/x3rNmPYMXnx75+3s9pQb1YDtws9y5bwxpxeB3EW88oCp33eS7bvJpeuoCA1LzW/PpVfIRhi5RCyAvrEeTL7Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 || ^3.2.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.11.4(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.11.5(@emotion/react@11.11.4)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 6.0.0-alpha.2(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.11.4)(@emotion/styled@11.11.5)(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react-transition-group': 4.4.10 + clsx: 2.1.1 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-license-pro@6.10.2(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-Baw3shilU+eHgU+QYKNPFUKvfS5rSyNJ98pQx02E0gKA22hWp/XAt88K1qUfUMPlkPpvg/uci6gviQSSLZkuKw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: true + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + /@reduxjs/toolkit@2.0.0(react-redux@8.1.3)(react@18.2.0): + resolution: {integrity: sha512-Kq/a+aO28adYdPoNEu9p800MYPKoUc0tlkYfv035Ief9J7MPq8JvmT7UdpYhvXsoMtOdt567KwZjc9H3Rf8yjg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.1.1 + react: 18.2.0 + react-redux: 8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.0 + dev: false + + /@remix-run/router@1.16.1: + resolution: {integrity: sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==} + engines: {node: '>=14.0.0'} + dev: false + + /@restart/hooks@0.4.16(react@18.2.0): + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + dependencies: + dequal: 2.0.3 + react: 18.2.0 + dev: false + + /@rollup/plugin-strip@3.0.4: + resolution: {integrity: sha512-LDRV49ZaavxUo2YoKKMQjCxzCxugu1rCPQa0lDYBOWLj6vtzBMr8DcoJjsmg+s450RbKbe3qI9ZLaSO+O1oNbg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0 + estree-walker: 2.0.2 + magic-string: 0.30.8 + dev: true + + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.13.2: + resolution: {integrity: sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-android-arm64@4.13.2: + resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-arm64@4.13.2: + resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-x64@4.13.2: + resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.13.2: + resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.13.2: + resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.13.2: + resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-powerpc64le-gnu@4.13.2: + resolution: {integrity: sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==} + cpu: [ppc64le] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.2: + resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.13.2: + resolution: {integrity: sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.2: + resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.13.2: + resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.13.2: + resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.13.2: + resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.13.2: + resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + + /@slate-yjs/core@1.0.2(slate@0.101.5)(yjs@13.6.15): + resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.101.5 + y-protocols: 1.0.6(yjs@13.6.15) + yjs: 13.6.15 + dev: false + + /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.24.3): + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-preset@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.24.3) + dev: true + + /@svgr/babel-preset@8.1.0(@babel/core@7.24.3): + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.24.3) + dev: true + + /@svgr/core@7.0.0(typescript@4.9.5): + resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 7.0.0(@babel/core@7.24.3) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@4.9.5) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@svgr/core@8.1.0(typescript@4.9.5): + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 8.1.0(@babel/core@7.24.3) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@4.9.5) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@svgr/hast-util-to-babel-ast@7.0.0: + resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==} + engines: {node: '>=14'} + dependencies: + '@babel/types': 7.24.0 + entities: 4.5.0 + dev: true + + /@svgr/plugin-jsx@7.0.0: + resolution: {integrity: sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 7.0.0(@babel/core@7.24.3) + '@svgr/hast-util-to-babel-ast': 7.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@svgr/plugin-svgo@8.0.1(@svgr/core@8.1.0)(typescript@4.9.5): + resolution: {integrity: sha512-29OJ1QmJgnohQHDAgAuY2h21xWD6TZiXji+hnx+W635RiXTAlHTbjrZDktfqzkN0bOeQEtNe+xgq73/XeWFfSg==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + dependencies: + '@svgr/core': 8.1.0(typescript@4.9.5) + cosmiconfig: 8.3.6(typescript@4.9.5) + deepmerge: 4.3.1 + svgo: 3.2.0 + transitivePeerDependencies: + - typescript + dev: true + + /@tauri-apps/api@1.5.6: + resolution: {integrity: sha512-LH5ToovAHnDVe5Qa9f/+jW28I6DeMhos8bNDtBOmmnaDpPmJmYLyHdeDblAWWWYc7KKRDg9/66vMuKyq0WIeFA==} + engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + dev: false + + /@tauri-apps/cli-darwin-arm64@1.5.11: + resolution: {integrity: sha512-2NLSglDb5VfvTbMtmOKWyD+oaL/e8Z/ZZGovHtUFyUSFRabdXc6cZOlcD1BhFvYkHqm+TqGaz5qtPR5UbqDs8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-darwin-x64@1.5.11: + resolution: {integrity: sha512-/RQllHiJRH2fJOCudtZlaUIjofkHzP3zZgxi71ZUm7Fy80smU5TDfwpwOvB0wSVh0g/ciDjMArCSTo0MRvL+ag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm-gnueabihf@1.5.11: + resolution: {integrity: sha512-IlBuBPKmMm+a5LLUEK6a21UGr9ZYd6zKuKLq6IGM4tVweQa8Sf2kP2Nqs74dMGIUrLmMs0vuqdURpykQg+z4NQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-gnu@1.5.11: + resolution: {integrity: sha512-w+k1bNHCU/GbmXshtAhyTwqosThUDmCEFLU4Zkin1vl2fuAtQry2RN7thfcJFepblUGL/J7yh3Q/0+BCjtspKQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-musl@1.5.11: + resolution: {integrity: sha512-PN6/dl+OfYQ/qrAy4HRAfksJ2AyWQYn2IA/2Wwpaa7SDRz2+hzwTQkvajuvy0sQ5L2WCG7ymFYRYMbpC6Hk9Pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-gnu@1.5.11: + resolution: {integrity: sha512-MTVXLi89Nj7Apcvjezw92m7ZqIDKT5SFKZtVPCg6RoLUBTzko/BQoXYIRWmdoz2pgkHDUHgO2OMJ8oKzzddXbw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-musl@1.5.11: + resolution: {integrity: sha512-kwzAjqFpz7rvTs7WGZLy/a5nS5t15QKr3E9FG95MNF0exTl3d29YoAUAe1Mn0mOSrTJ9Z+vYYAcI/QdcsGBP+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-arm64-msvc@1.5.11: + resolution: {integrity: sha512-L+5NZ/rHrSUrMxjj6YpFYCXp6wHnq8c8SfDTBOX8dO8x+5283/vftb4vvuGIsLS4UwUFXFnLt3XQr44n84E67Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-ia32-msvc@1.5.11: + resolution: {integrity: sha512-oVlD9IVewrY0lZzTdb71kNXkjdgMqFq+ohb67YsJb4Rf7o8A9DTlFds1XLCe3joqLMm4M+gvBKD7YnGIdxQ9vA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-x64-msvc@1.5.11: + resolution: {integrity: sha512-1CexcqUFCis5ypUIMOKllxUBrna09McbftWENgvVXMfA+SP+yPDPAVb8fIvUcdTIwR/yHJwcIucmTB4anww4vg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli@1.5.11: + resolution: {integrity: sha512-B475D7phZrq5sZ3kDABH4g2mEoUIHtnIO+r4ZGAAfsjMbZCwXxR/jlMGTEL+VO3YzjpF7gQe38IzB4vLBbVppw==} + engines: {node: '>= 10'} + hasBin: true + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 1.5.11 + '@tauri-apps/cli-darwin-x64': 1.5.11 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.5.11 + '@tauri-apps/cli-linux-arm64-gnu': 1.5.11 + '@tauri-apps/cli-linux-arm64-musl': 1.5.11 + '@tauri-apps/cli-linux-x64-gnu': 1.5.11 + '@tauri-apps/cli-linux-x64-musl': 1.5.11 + '@tauri-apps/cli-win32-arm64-msvc': 1.5.11 + '@tauri-apps/cli-win32-ia32-msvc': 1.5.11 + '@tauri-apps/cli-win32-x64-msvc': 1.5.11 + dev: true + + /@testing-library/dom@10.1.0: + resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} + engines: {node: '>=18'} + dependencies: + '@babel/code-frame': 7.24.7 + '@babel/runtime': 7.24.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + dev: true + + /@testing-library/react@16.0.0(@testing-library/dom@10.1.0)(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.6 + '@testing-library/dom': 10.1.0 + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: true + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/aria-query@5.0.4: + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.5 + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.24.0 + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.24.0 + + /@types/date-arithmetic@4.1.4: + resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} + dev: true + + /@types/eslint-scope@3.7.7: + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + dependencies: + '@types/eslint': 8.56.10 + '@types/estree': 1.0.5 + dev: true + + /@types/eslint@8.56.10: + resolution: {integrity: sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==} + dependencies: + '@types/estree': 1.0.5 + '@types/json-schema': 7.0.15 + dev: true + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + /@types/google-protobuf@3.15.12: + resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.11.30 + + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + dev: false + + /@types/is-hotkey@0.1.10: + resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==} + dev: false + + /@types/is-hotkey@0.1.7: + resolution: {integrity: sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + /@types/jest@29.5.3: + resolution: {integrity: sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 20.11.30 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/katex@0.16.0: + resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} + dev: true + + /@types/lodash-es@4.17.11: + resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==} + dependencies: + '@types/lodash': 4.17.0 + dev: true + + /@types/lodash@4.17.0: + resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + + /@types/node@20.11.30: + resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + dependencies: + undici-types: 5.26.5 + + /@types/numeral@2.0.5: + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + dev: true + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: false + + /@types/prismjs@1.26.0: + resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} + dev: true + + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + /@types/quill@2.0.10: + resolution: {integrity: sha512-L6OHONEj2v4NRbWQOsn7j1N0SyzhRR3M4g1M6j/uuIwIsIW2ShWHhwbqNvH8hSmVktzqu0lITfdnqVOQ4qkrhA==} + dependencies: + parchment: 1.1.4 + quill-delta: 4.2.2 + dev: true + + /@types/react-beautiful-dnd@13.1.3: + resolution: {integrity: sha512-BNdmvONKtsrZq3AGrujECQrIn8cDT+fZsxBLXuX3YWY/nHfZinUFx4W88eS0rkcXzuLbXpKOsu/1WCMPMLEpPg==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-big-calendar@1.8.9: + resolution: {integrity: sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==} + dependencies: + '@types/date-arithmetic': 4.1.4 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + dev: true + + /@types/react-color@3.0.6: + resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==} + dependencies: + '@types/react': 18.2.66 + '@types/reactcss': 1.2.12 + dev: true + + /@types/react-custom-scrollbars@4.0.13: + resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} + dependencies: + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + date-fns: 2.30.0 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react + - react-dom + dev: true + + /@types/react-dom@18.2.22: + resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} + dependencies: + '@types/react': 18.2.66 + + /@types/react-katex@3.0.0: + resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-measure@2.0.12: + resolution: {integrity: sha512-Y6V11CH6bU7RhqrIdENPwEUZlPXhfXNGylMNnGwq5TAEs2wDoBA3kSVVM/EQ8u72sz5r9ja+7W8M8PIVcS841Q==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-redux@7.1.33: + resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + + /@types/react-swipeable-views@0.13.5: + resolution: {integrity: sha512-ni6WjO7gBq2xB2Y/ZiRdQOgjGOxIik5ow2s7xKieDq8DxsXTdV46jJslSBVK2yoIJHf6mG3uqNTwxwgzbXRRzg==} + dependencies: + '@types/react': 18.2.66 + dev: false + + /@types/react-transition-group@4.4.10: + resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} + dependencies: + '@types/react': 18.2.66 + dev: false + + /@types/react-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-window@1.8.8: + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + dependencies: + '@types/react': 18.2.66 + + /@types/react@18.2.66: + resolution: {integrity: sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==} + dependencies: + '@types/prop-types': 15.7.12 + '@types/scheduler': 0.23.0 + csstype: 3.1.3 + + /@types/reactcss@1.2.12: + resolution: {integrity: sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/scheduler@0.23.0: + resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} + + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + + /@types/sinonjs__fake-timers@8.1.1: + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + dev: true + + /@types/sizzle@2.3.8: + resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} + dev: true + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: true + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: true + + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + + /@types/utf8@3.0.1: + resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==} + dev: true + + /@types/uuid@9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + + /@types/validator@13.11.9: + resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} + dev: true + + /@types/warning@3.0.3: + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + dev: false + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + + /@types/yauzl@2.10.3: + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + requiresBuild: true + dependencies: + '@types/node': 20.11.30 + dev: true + optional: true + + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.57.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@7.2.0: + resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + dev: true + + /@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + debug: 4.3.4(supports-color@8.1.1) + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@7.2.0: + resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@7.2.0(typescript@4.9.5): + resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + eslint: 8.57.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@7.2.0: + resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.2.1(vite@5.2.0): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.3) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + transitivePeerDependencies: + - supports-color + dev: true + + /@webassemblyjs/ast@1.12.1: + resolution: {integrity: sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==} + dependencies: + '@webassemblyjs/helper-numbers': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + dev: true + + /@webassemblyjs/floating-point-hex-parser@1.11.6: + resolution: {integrity: sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==} + dev: true + + /@webassemblyjs/helper-api-error@1.11.6: + resolution: {integrity: sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==} + dev: true + + /@webassemblyjs/helper-buffer@1.12.1: + resolution: {integrity: sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==} + dev: true + + /@webassemblyjs/helper-numbers@1.11.6: + resolution: {integrity: sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==} + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.11.6 + '@webassemblyjs/helper-api-error': 1.11.6 + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/helper-wasm-bytecode@1.11.6: + resolution: {integrity: sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==} + dev: true + + /@webassemblyjs/helper-wasm-section@1.12.1: + resolution: {integrity: sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/wasm-gen': 1.12.1 + dev: true + + /@webassemblyjs/ieee754@1.11.6: + resolution: {integrity: sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==} + dependencies: + '@xtuc/ieee754': 1.2.0 + dev: true + + /@webassemblyjs/leb128@1.11.6: + resolution: {integrity: sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==} + dependencies: + '@xtuc/long': 4.2.2 + dev: true + + /@webassemblyjs/utf8@1.11.6: + resolution: {integrity: sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==} + dev: true + + /@webassemblyjs/wasm-edit@1.12.1: + resolution: {integrity: sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/helper-wasm-section': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-opt': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + '@webassemblyjs/wast-printer': 1.12.1 + dev: true + + /@webassemblyjs/wasm-gen@1.12.1: + resolution: {integrity: sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wasm-opt@1.12.1: + resolution: {integrity: sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-buffer': 1.12.1 + '@webassemblyjs/wasm-gen': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + dev: true + + /@webassemblyjs/wasm-parser@1.12.1: + resolution: {integrity: sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/helper-api-error': 1.11.6 + '@webassemblyjs/helper-wasm-bytecode': 1.11.6 + '@webassemblyjs/ieee754': 1.11.6 + '@webassemblyjs/leb128': 1.11.6 + '@webassemblyjs/utf8': 1.11.6 + dev: true + + /@webassemblyjs/wast-printer@1.12.1: + resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + dependencies: + '@webassemblyjs/ast': 1.12.1 + '@xtuc/long': 4.2.2 + dev: true + + /@xtuc/ieee754@1.2.0: + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + dev: true + + /@xtuc/long@4.2.2: + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + dev: true + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + dev: true + + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.11.3 + acorn-walk: 8.3.2 + dev: true + + /acorn-import-assertions@1.9.0(acorn@8.11.3): + resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + peerDependencies: + acorn: ^8 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-node@1.8.2: + resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + xtend: 4.0.2 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true + + /aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /ajv-formats@2.1.1(ajv@8.14.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.14.0 + dev: true + + /ajv-keywords@3.5.2(ajv@6.12.6): + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + dependencies: + ajv: 6.12.6 + dev: true + + /ajv-keywords@5.1.0(ajv@8.14.0): + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + dependencies: + ajv: 8.14.0 + fast-deep-equal: 3.1.3 + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + /ajv@8.14.0: + resolution: {integrity: sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /append-transform@2.0.0: + resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} + engines: {node: '>=8'} + dependencies: + default-require-extensions: 3.0.1 + dev: true + + /arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + dev: true + + /archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + dev: true + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + dependencies: + dequal: 2.0.3 + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.tosorted@1.1.3: + resolution: {integrity: sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + + /assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: false + + /async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /autoprefixer@10.4.13(postcss@8.4.21): + resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001603 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + /aws4@1.12.0: + resolution: {integrity: sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==} + + /axios@1.7.2: + resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /b4a@1.6.6: + resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + dev: true + + /babel-jest@29.6.2(@babel/core@7.24.3): + resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.24.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-jest@29.7.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.24.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + /babel-loader@9.1.3(@babel/core@7.24.3)(webpack@5.91.0): + resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} + engines: {node: '>= 14.15.0'} + peerDependencies: + '@babel/core': ^7.12.0 + webpack: '>=5' + dependencies: + '@babel/core': 7.24.3 + find-cache-dir: 4.0.0 + schema-utils: 4.2.0 + webpack: 5.91.0 + dev: true + + /babel-plugin-import@1.13.8: + resolution: {integrity: sha512-36babpjra5m3gca44V6tSTomeBlPA7cHUynrE2WiQIm3rEGD9xy28MKsx5IdO45EbnpJY7Jrgd00C6Dwt/l/2Q==} + dependencies: + '@babel/helper-module-imports': 7.24.3 + dev: true + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.24.0 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.5 + + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.24.6 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + dev: false + + /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.3): + resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.24.7 + '@babel/core': 7.24.3 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.3) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.3) + core-js-compat: 3.37.1 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.6.2(@babel/core@7.24.3): + resolution: {integrity: sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-define-polyfill-provider': 0.6.2(@babel/core@7.24.3) + transitivePeerDependencies: + - supports-color + dev: true + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.3): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) + + /babel-preset-jest@29.6.3(@babel/core@7.24.3): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.3) + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /bare-events@2.2.2: + resolution: {integrity: sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==} + requiresBuild: true + dev: true + optional: true + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + /bind-event-listener@3.0.0: + resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} + dev: false + + /blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + dev: true + + /bluebird@3.7.1: + resolution: {integrity: sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==} + dev: true + + /bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + dev: true + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browserify-zlib@0.1.4: + resolution: {integrity: sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==} + dependencies: + pako: 0.2.9 + dev: true + + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001603 + electron-to-chromium: 1.4.722 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + + /buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + dev: true + + /caching-transform@4.0.0: + resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} + engines: {node: '>=8'} + dependencies: + hasha: 5.2.2 + make-dir: 3.1.0 + package-hash: 4.0.0 + write-file-atomic: 3.0.3 + dev: true + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.2 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-lite@1.0.30001603: + resolution: {integrity: sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==} + + /capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + dev: true + + /caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: true + + /change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + /check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + dev: true + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + dev: true + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + + /clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + dependencies: + restore-cursor: 3.1.0 + dev: true + + /cli-table3@0.6.4: + resolution: {integrity: sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + dev: true + + /cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + dev: true + + /cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + dev: true + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + dev: true + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: true + + /colorthief@2.4.0: + resolution: {integrity: sha512-0U48RGNRo5fVO+yusBwgp+d3augWSorXabnqXUu9SabEhCpCgZJEUjUTTI41OOBBYuMMxawa3177POT6qLfLeQ==} + dependencies: + '@lokesh.dhakar/quantize': 1.3.0 + get-pixels: 3.3.3 + dev: false + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: true + + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + dev: true + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + /common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + dev: true + + /common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + dev: true + + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: true + + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case: 2.0.2 + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + /core-js-compat@3.37.1: + resolution: {integrity: sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==} + dependencies: + browserslist: 4.23.0 + dev: true + + /core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + /core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@8.3.6(typescript@4.9.5): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 4.9.5 + dev: true + + /create-jest@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.11.30) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.3 + dev: false + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.0 + dev: true + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.0 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + /cwise-compiler@1.1.3: + resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} + dependencies: + uniq: 1.0.1 + dev: false + + /cypress@13.7.2: + resolution: {integrity: sha512-FF5hFI5wlRIHY8urLZjJjj/YvfCBrRpglbZCLr/cYcL9MdDe0+5usa8kTIrDHthlEc9lwihbkb5dmwqBDNS2yw==} + engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0} + hasBin: true + requiresBuild: true + dependencies: + '@cypress/request': 3.0.1 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.8 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + check-more-types: 2.24.0 + cli-cursor: 3.1.0 + cli-table3: 0.6.4 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.9 + debug: 4.3.4(supports-color@8.1.1) + enquirer: 2.4.1 + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + extract-zip: 2.0.1(supports-color@8.1.1) + figures: 3.2.0 + fs-extra: 9.1.0 + getos: 3.2.1 + is-ci: 3.0.1 + is-installed-globally: 0.4.0 + lazy-ass: 1.6.0 + listr2: 3.14.0(enquirer@2.4.1) + lodash: 4.17.21 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + semver: 7.6.0 + supports-color: 8.1.1 + tmp: 0.2.3 + untildify: 4.0.0 + yauzl: 2.10.0 + dev: true + + /dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + + /data-uri-to-buffer@0.0.3: + resolution: {integrity: sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==} + dev: false + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /date-arithmetic@4.1.0: + resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==} + dev: false + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.24.1 + + /dayjs@1.11.10: + resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} + dev: true + + /dayjs@1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + + /debug@3.2.7(supports-color@8.1.1): + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /debug@4.3.4(supports-color@8.1.1): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + + /decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + /deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.2 + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /default-require-extensions@3.0.1: + resolution: {integrity: sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==} + engines: {node: '>=8'} + dependencies: + strip-bom: 4.0.0 + dev: true + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + /define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + /defined@1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + /detective@5.2.1: + resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + acorn-node: 1.8.2 + defined: 1.0.1 + minimist: 1.2.8 + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /direction@1.0.4: + resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==} + hasBin: true + dev: false + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true + + /dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + dev: false + + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.24.1 + csstype: 3.1.3 + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + dev: true + + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + /electron-to-chromium@1.4.722: + resolution: {integrity: sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==} + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + /emoji-mart@5.6.0: + resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} + dev: false + + /emoji-regex@10.3.0: + resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: true + + /enhanced-resolve@5.16.1: + resolution: {integrity: sha512-4U5pNsuDl0EhuZpq46M5xPslstkviJuhrdobaRDBk2Jy2KO37FDAJl4lb2KlNabxT0m4MTK2UHNrsAcphE8nyw==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-module-lexer@1.5.3: + resolution: {integrity: sha512-i1gCgmR9dCl6Vil6UKPI/trA69s08g/syhiDK9TG0Nf1RJjjFI+AzoWW7sPufzkgYAn861skuCwJa0pIIHYxvg==} + dev: true + + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + dev: true + + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-plugin-react-refresh@0.4.6(eslint@8.57.0): + resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-plugin-react@7.32.2(eslint@8.57.0): + resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.3 + doctrine: 2.1.0 + eslint: 8.57.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.hasown: 1.1.4 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + dev: true + + /eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint-visitor-keys@4.0.0: + resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@10.0.1: + resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 4.0.0 + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + dev: true + + /eventemitter3@2.0.3: + resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + /execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + dev: true + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + /executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + dependencies: + pify: 2.3.0 + dev: true + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + /extract-zip@2.0.1(supports-color@8.1.1): + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + dev: true + + /extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + /fast-diff@1.1.2: + resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} + dev: false + + /fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: false + + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: true + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + + /fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + dependencies: + pend: 1.2.0 + dev: true + + /figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + dependencies: + escape-string-regexp: 1.0.5 + dev: true + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + dev: true + + /find-cache-dir@3.3.2: + resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} + engines: {node: '>=8'} + dependencies: + commondir: 1.0.1 + make-dir: 3.1.0 + pkg-dir: 4.2.0 + dev: true + + /find-cache-dir@4.0.0: + resolution: {integrity: sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==} + engines: {node: '>=14.16'} + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 7.0.0 + dev: true + + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + + /find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: true + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /find-up@6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + dev: true + + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@2.0.0: + resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} + engines: {node: '>=8.0.0'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 3.0.7 + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + /form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + + /fromentries@1.3.2: + resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==} + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + /get-node-dimensions@1.2.1: + resolution: {integrity: sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==} + dev: false + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + /get-pixels@3.3.3: + resolution: {integrity: sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg==} + dependencies: + data-uri-to-buffer: 0.0.3 + jpeg-js: 0.4.4 + mime-types: 2.1.35 + ndarray: 1.0.19 + ndarray-pack: 1.2.1 + node-bitmap: 0.0.1 + omggif: 1.0.10 + parse-data-uri: 0.2.0 + pngjs: 3.4.0 + request: 2.88.2 + through: 2.3.8 + dev: false + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + + /getos@3.2.1: + resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==} + dependencies: + async: 3.2.5 + dev: true + + /getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + dev: true + + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.0.4 + path-scurry: 1.10.2 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + dependencies: + ini: 2.0.0 + dev: true + + /globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + dev: false + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /goober@2.1.14(csstype@3.1.3): + resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.3 + dev: false + + /google-protobuf@3.21.2: + resolution: {integrity: sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==} + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /gunzip-maybe@1.4.2: + resolution: {integrity: sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==} + hasBin: true + dependencies: + browserify-zlib: 0.1.4 + is-deflate: 1.0.0 + is-gzip: 1.0.0 + peek-stream: 1.1.3 + pumpify: 1.5.1 + through2: 2.0.5 + dev: true + + /har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: false + + /har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: false + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + dev: true + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + dependencies: + capital-case: 1.0.4 + tslib: 2.6.2 + dev: true + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true + + /http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + dev: false + + /http-signature@1.3.6: + resolution: {integrity: sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + /i18next-browser-languagedetector@7.2.1: + resolution: {integrity: sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /i18next-resources-to-backend@1.2.1: + resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /i18next@22.5.1: + resolution: {integrity: sha512-8TGPgM3pAD+VRsMtUMNknRz3kzqwp/gPALrWMsDnmC1mKqJwpWyooQRLMcbTwq8z8YwSmuj+ZYvc+xCuEpkssA==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true + + /immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + dev: false + + /immutable@4.3.6: + resolution: {integrity: sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + dev: true + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + dev: true + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /iota-array@1.0.0: + resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: false + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + dev: false + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + dependencies: + ci-info: 3.9.0 + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.2 + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + + /is-deflate@1.0.0: + resolution: {integrity: sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==} + dev: true + + /is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-gzip@1.0.0: + resolution: {integrity: sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + dev: false + + /is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + dev: true + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + dependencies: + isobject: 3.0.1 + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + + /is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: true + + /is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + dependencies: + is-docker: 2.2.1 + dev: true + + /isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + dev: true + + /isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + + /isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + /istanbul-lib-hook@3.0.0: + resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} + engines: {node: '>=8'} + dependencies: + append-transform: 2.0.0 + dev: true + + /istanbul-lib-instrument@4.0.3: + resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.24.3 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.24.3 + '@babel/parser': 7.24.1 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-instrument@6.0.2: + resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.24.3 + '@babel/parser': 7.24.1 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-processinfo@2.0.3: + resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} + engines: {node: '>=8'} + dependencies: + archy: 1.0.0 + cross-spawn: 7.0.3 + istanbul-lib-coverage: 3.2.2 + p-map: 3.0.0 + rimraf: 3.0.2 + uuid: 8.3.2 + dev: true + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4(supports-color@8.1.1) + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + /jest-cli@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.11.30) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.11.30) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /jest-config@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.24.3 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + babel-jest: 29.7.0(@babel/core@7.24.3) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + /jest-environment-jsdom@29.6.2: + resolution: {integrity: sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.11.30 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.24.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-util: 29.7.0 + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.3 + '@babel/generator': 7.24.1 + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/types': 7.24.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.3) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + /jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/node': 20.11.30 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.11.30 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /jest@29.5.0(@types/node@20.11.30): + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.11.30) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + dev: false + + /js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.11.3 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.16.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsesc@0.5.0: + resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} + hasBin: true + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: false + + /jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + dev: true + + /katex@0.16.10: + resolution: {integrity: sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /keycode@2.2.1: + resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + + /lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lib0@0.2.94: + resolution: {integrity: sha512-hZ3p54jL4Wpu7IOg26uC7dnEWiMyNlUrb9KoG7+xYs45WkQwpVvKFndVq2+pqLYKe1u8Fp3+zAfZHVvTK34PvQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /listr2@3.14.0(enquirer@2.4.1): + resolution: {integrity: sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==} + engines: {node: '>=10.0.0'} + peerDependencies: + enquirer: '>= 2.3.0 < 3' + peerDependenciesMeta: + enquirer: + optional: true + dependencies: + cli-truncate: 2.1.0 + colorette: 2.0.20 + enquirer: 2.4.1 + log-update: 4.0.0 + p-map: 4.0.0 + rfdc: 1.3.1 + rxjs: 7.8.0 + through: 2.3.8 + wrap-ansi: 7.0.0 + dev: true + + /loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + dev: true + + /locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: true + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: true + + /lodash.flattendeep@4.4.0: + resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /log-update@4.0.0: + resolution: {integrity: sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==} + engines: {node: '>=10'} + dependencies: + ansi-escapes: 4.3.2 + cli-cursor: 3.1.0 + slice-ansi: 4.0.0 + wrap-ansi: 6.2.0 + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.6.2 + dev: true + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + + /lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + dev: true + + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + dependencies: + pify: 4.0.1 + semver: 5.7.2 + dev: true + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.1 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + + /material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + dev: false + + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + + /memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /moment-timezone@0.5.45: + resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} + dependencies: + moment: 2.30.1 + dev: false + + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + dev: false + + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /ndarray-pack@1.2.1: + resolution: {integrity: sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==} + dependencies: + cwise-compiler: 1.1.3 + ndarray: 1.0.19 + dev: false + + /ndarray@1.0.19: + resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + dependencies: + iota-array: 1.0.0 + is-buffer: 1.1.6 + dev: false + + /neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.6.2 + dev: true + + /node-bitmap@0.0.1: + resolution: {integrity: sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==} + engines: {node: '>=v0.6.5'} + dev: false + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + /node-preload@0.2.1: + resolution: {integrity: sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==} + engines: {node: '>=8'} + dependencies: + process-on-spawn: 1.0.0 + dev: true + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + dev: false + + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: true + + /nyc@15.1.0: + resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==} + engines: {node: '>=8.9'} + hasBin: true + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + caching-transform: 4.0.0 + convert-source-map: 1.9.0 + decamelize: 1.2.0 + find-cache-dir: 3.3.2 + find-up: 4.1.0 + foreground-child: 2.0.0 + get-package-type: 0.1.0 + glob: 7.2.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-hook: 3.0.0 + istanbul-lib-instrument: 4.0.3 + istanbul-lib-processinfo: 2.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + make-dir: 3.1.0 + node-preload: 0.2.1 + p-map: 3.0.0 + process-on-spawn: 1.0.0 + resolve-from: 5.0.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + spawn-wrap: 2.0.0 + test-exclude: 6.0.0 + yargs: 15.4.1 + transitivePeerDependencies: + - supports-color + dev: true + + /oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /object.hasown@1.1.4: + resolution: {integrity: sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + dev: false + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + dev: true + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: true + + /p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: true + + /p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + /package-hash@4.0.0: + resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} + engines: {node: '>=8'} + dependencies: + graceful-fs: 4.2.11 + hasha: 5.2.2 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + dev: true + + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: true + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /parchment@1.1.4: + resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-data-uri@0.2.0: + resolution: {integrity: sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==} + dependencies: + data-uri-to-buffer: 0.0.3 + dev: false + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.24.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /peek-stream@1.1.3: + resolution: {integrity: sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==} + dependencies: + buffer-from: 1.1.2 + duplexify: 3.7.1 + through2: 2.0.5 + dev: true + + /pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + dev: true + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + /pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + dev: true + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /pkg-dir@7.0.0: + resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} + engines: {node: '>=14.16'} + dependencies: + find-up: 6.3.0 + dev: true + + /pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + dev: false + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss-import@14.1.0(postcss@8.4.21): + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.21): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.21 + dev: true + + /postcss-load-config@3.1.4(postcss@8.4.21): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.21 + yaml: 1.10.2 + dev: true + + /postcss-nested@6.0.0(postcss@8.4.21): + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.16 + dev: true + + /postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + + /prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-plugin-tailwindcss@0.2.2(prettier@2.8.4): + resolution: {integrity: sha512-5RjUbWRe305pUpc48MosoIp6uxZvZxrM6GyOgsbGLTce+ehePKNm7ziW2dLG2air9aXbGuXlHVSQQw4Lbosq3w==} + engines: {node: '>=12.17.0'} + peerDependencies: + '@prettier/plugin-php': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@shufo/prettier-plugin-blade': '*' + '@trivago/prettier-plugin-sort-imports': '*' + prettier: '>=2.2.0' + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + prettier-plugin-twig-melody: '*' + peerDependenciesMeta: + '@prettier/plugin-php': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@shufo/prettier-plugin-blade': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier-plugin-twig-melody: + optional: true + dependencies: + prettier: 2.8.4 + dev: true + + /prettier@2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + dev: true + + /pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + + /process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /process-on-spawn@1.0.0: + resolution: {integrity: sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==} + engines: {node: '>=8'} + dependencies: + fromentries: 1.3.2 + dev: true + + /process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + dev: true + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /protoc-gen-ts@0.8.7: + resolution: {integrity: sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==} + hasBin: true + dev: false + + /proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + dev: true + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + + /pump@2.0.1: + resolution: {integrity: sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: true + + /pumpify@1.5.1: + resolution: {integrity: sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==} + dependencies: + duplexify: 3.7.1 + inherits: 2.0.4 + pump: 2.0.1 + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + /qs@6.10.4: + resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.6 + dev: true + + /qs@6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /quill-delta@3.6.3: + resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} + engines: {node: '>=0.10'} + dependencies: + deep-equal: 1.1.2 + extend: 3.0.2 + fast-diff: 1.1.2 + dev: false + + /quill-delta@4.2.2: + resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==} + dependencies: + fast-diff: 1.2.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: true + + /quill-delta@5.1.0: + resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==} + engines: {node: '>= 12.0.0'} + dependencies: + fast-diff: 1.3.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: false + + /quill@1.3.7: + resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==} + dependencies: + clone: 2.1.2 + deep-equal: 1.1.2 + eventemitter3: 2.0.3 + extend: 3.0.2 + parchment: 1.1.4 + quill-delta: 3.6.3 + dev: false + + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + + /react-big-calendar@1.12.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cPVcwH5V1YiC6QKaV4afvpuZ2DtP8+TocnZY98nGodqq8bfjVDiP3Ch+TewBZzj9mg7JbewHdufDZXZBqQl1lw==} + peerDependencies: + react: ^16.14.0 || ^17 || ^18 + react-dom: ^16.14.0 || ^17 || ^18 + dependencies: + '@babel/runtime': 7.24.1 + clsx: 1.2.1 + date-arithmetic: 4.1.0 + dayjs: 1.11.9 + dom-helpers: 5.2.1 + globalize: 0.1.1 + invariant: 2.2.4 + lodash: 4.17.21 + lodash-es: 4.17.21 + luxon: 3.4.4 + memoize-one: 6.0.0 + moment: 2.30.1 + moment-timezone: 0.5.45 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-overlays: 5.2.1(react-dom@18.2.0)(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + dev: false + + /react-color@2.19.3(react@18.2.0): + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + dependencies: + '@icons/material': 0.2.4(react@18.2.0) + lodash: 4.17.21 + lodash-es: 4.17.21 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 18.2.0 + reactcss: 1.2.3(react@18.2.0) + tinycolor2: 1.6.0 + dev: false + + /react-custom-scrollbars-2@4.5.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/z0nWAeXfMDr4+OXReTpYd1Atq9kkn4oI3qxq3iMXGQx1EEfwETSqB8HTAvg1X7dEqcCachbny1DRNGlqX5bDQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-datepicker@4.25.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.5.1 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.1(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-error-boundary@4.0.13(react@18.2.0): + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.24.1 + react: 18.2.0 + dev: false + + /react-event-listener@0.6.6(react@18.2.0): + resolution: {integrity: sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==} + peerDependencies: + react: ^16.3.0 + dependencies: + '@babel/runtime': 7.24.6 + prop-types: 15.8.1 + react: 18.2.0 + warning: 4.0.3 + dev: false + + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.14(csstype@3.1.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + + /react-i18next@14.1.2(i18next@22.5.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + html-parse-stringify: 3.0.1 + i18next: 22.5.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + /react-katex@3.0.1(prop-types@15.8.1)(react@18.2.0): + resolution: {integrity: sha512-wIUW1fU5dHlkKvq4POfDkHruQsYp3fM8xNb/jnc8dnQ+nNCnaj0sx5pw7E6UyuEdLRyFKK0HZjmXBo+AtXXy0A==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=15.3.2 <=18' + dependencies: + katex: 0.16.10 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-measure@2.5.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==} + peerDependencies: + react: '>0.13.0' + react-dom: '>0.13.0' + dependencies: + '@babel/runtime': 7.24.1 + get-node-dimensions: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + dev: false + + /react-onclickoutside@6.13.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-LdrrxK/Yh9zbBQdFbMTXPp3dTSN9B+9YJQucdDu3JNKRrbdU+H+/TVONJoWtOwy4II8Sqf1y/DTI6w/vGPYW0w==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-overlays@5.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + dependencies: + '@babel/runtime': 7.24.1 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.16(react@18.2.0) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + + /react-redux@8.1.3(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): + resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==} + peerDependencies: + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 || ^5.0.0-beta.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + redux: 4.2.1 + use-sync-external-store: 1.2.2(react@18.2.0) + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-router-dom@6.23.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.16.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.23.1(react@18.2.0) + dev: false + + /react-router@6.23.1(react@18.2.0): + resolution: {integrity: sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.16.1 + react: 18.2.0 + dev: false + + /react-swipeable-views-core@0.14.0: + resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + warning: 4.0.3 + dev: false + + /react-swipeable-views-utils@0.14.0(react@18.2.0): + resolution: {integrity: sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + keycode: 2.2.1 + prop-types: 15.8.1 + react-event-listener: 0.6.6(react@18.2.0) + react-swipeable-views-core: 0.14.0 + shallow-equal: 1.2.1 + transitivePeerDependencies: + - react + dev: false + + /react-swipeable-views@0.14.0(react@18.2.0): + resolution: {integrity: sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-swipeable-views-core: 0.14.0 + react-swipeable-views-utils: 0.14.0(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.24.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-virtualized-auto-sizer@1.0.24(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-vtree@2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0): + resolution: {integrity: sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==} + peerDependencies: + '@types/react-window': ^1.8.2 + react: ^16.13.1 + react-dom: ^16.13.1 + react-window: ^1.8.5 + dependencies: + '@babel/runtime': 7.24.1 + '@types/react-window': 1.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react18-input-otp@1.1.4(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-35xvmTeuPWIxd0Z0Opx4z3OoMaTmKN4ubirQCx1YMZiNoe+2h1hsOSUco4aKPlGXWZCtXrfOFieAh46vqiK9mA==} + peerDependencies: + react: 16.2.0 - 18 + react-dom: 16.2.0 - 18 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /reactcss@1.2.3(react@18.2.0): + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + dependencies: + lodash: 4.17.21 + react: 18.2.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /redux-thunk@3.1.0(redux@5.0.1): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.1 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + dev: false + + /regenerate-unicode-properties@10.1.1: + resolution: {integrity: sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==} + engines: {node: '>=4'} + dependencies: + regenerate: 1.4.2 + dev: true + + /regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + dev: true + + /regenerator-runtime@0.12.1: + resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} + dev: false + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + /regenerator-transform@0.15.2: + resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} + dependencies: + '@babel/runtime': 7.24.6 + dev: true + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + /regexpu-core@5.3.2: + resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} + engines: {node: '>=4'} + dependencies: + '@babel/regjsgen': 0.8.0 + regenerate: 1.4.2 + regenerate-unicode-properties: 10.1.1 + regjsparser: 0.9.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.1.0 + dev: true + + /regjsparser@0.9.1: + resolution: {integrity: sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==} + hasBin: true + dependencies: + jsesc: 0.5.0 + dev: true + + /release-zalgo@1.0.0: + resolution: {integrity: sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==} + engines: {node: '>=4'} + dependencies: + es6-error: 4.1.1 + dev: true + + /request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + dependencies: + throttleit: 1.0.1 + dev: true + + /request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.12.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.1.2 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /reselect@5.1.0: + resolution: {integrity: sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==} + dev: false + + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: true + + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rfdc@1.3.1: + resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup-plugin-visualizer@5.12.0: + resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x || 4.x + peerDependenciesMeta: + rollup: + optional: true + dependencies: + open: 8.4.2 + picomatch: 2.3.1 + source-map: 0.7.4 + yargs: 17.7.2 + dev: true + + /rollup@4.13.2: + resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.13.2 + '@rollup/rollup-android-arm64': 4.13.2 + '@rollup/rollup-darwin-arm64': 4.13.2 + '@rollup/rollup-darwin-x64': 4.13.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.2 + '@rollup/rollup-linux-arm64-gnu': 4.13.2 + '@rollup/rollup-linux-arm64-musl': 4.13.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.13.2 + '@rollup/rollup-linux-riscv64-gnu': 4.13.2 + '@rollup/rollup-linux-s390x-gnu': 4.13.2 + '@rollup/rollup-linux-x64-gnu': 4.13.2 + '@rollup/rollup-linux-x64-musl': 4.13.2 + '@rollup/rollup-win32-arm64-msvc': 4.13.2 + '@rollup/rollup-win32-ia32-msvc': 4.13.2 + '@rollup/rollup-win32-x64-msvc': 4.13.2 + fsevents: 2.3.3 + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + dependencies: + tslib: 2.6.2 + + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + /sass@1.77.2: + resolution: {integrity: sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.6.0 + immutable: 4.3.6 + source-map-js: 1.2.0 + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + + /schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + dev: true + + /schema-utils@4.2.0: + resolution: {integrity: sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==} + engines: {node: '>= 12.13.0'} + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.14.0 + ajv-formats: 2.1.1(ajv@8.14.0) + ajv-keywords: 5.1.0(ajv@8.14.0) + dev: true + + /scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + dependencies: + compute-scroll-into-view: 3.1.0 + dev: false + + /semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + dev: true + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + dev: true + + /serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + /shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + dependencies: + kind-of: 6.0.3 + dev: true + + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: true + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slate-history@0.100.0(slate@0.101.5): + resolution: {integrity: sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==} + peerDependencies: + slate: '>=0.65.3' + dependencies: + is-plain-object: 5.0.0 + slate: 0.101.5 + dev: false + + /slate-react@0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5): + resolution: {integrity: sha512-aMtp9FY127hKWTkCcTBonfKIwKJC2ESPqFdw2o/RuOk3RMQRwsWay8XTOHx8OBGOHanI2fsKaTAPF5zxOLA1Qg==} + peerDependencies: + react: '>=18.2.0' + react-dom: '>=18.2.0' + slate: '>=0.99.0' + dependencies: + '@juggle/resize-observer': 3.4.0 + '@types/is-hotkey': 0.1.10 + '@types/lodash': 4.17.0 + direction: 1.0.4 + is-hotkey: 0.2.0 + is-plain-object: 5.0.0 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + slate: 0.101.5 + tiny-invariant: 1.3.1 + dev: false + + /slate@0.101.5: + resolution: {integrity: sha512-ZZt1ia8ayRqxtpILRMi2a4MfdvwdTu64CorxTVq9vNSd0GQ/t3YDkze6wKjdeUtENmBlq5wNIDInZbx38Hfu5Q==} + dependencies: + immer: 10.1.1 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + dev: false + + /slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /smooth-scroll-into-view-if-needed@2.0.2: + resolution: {integrity: sha512-z54WzUSlM+xHHvJu3lMIsh+1d1kA4vaakcAtQvqzeGJ5Ffau7EKjpRrMHh1/OBo5zyU2h30ZYEt77vWmPHqg7Q==} + dependencies: + scroll-into-view-if-needed: 3.1.0 + dev: false + + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + dev: true + + /spawn-wrap@2.0.0: + resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} + engines: {node: '>=8'} + dependencies: + foreground-child: 2.0.0 + is-windows: 1.0.2 + make-dir: 3.1.0 + rimraf: 3.0.2 + signal-exit: 3.0.7 + which: 2.0.2 + dev: true + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + /sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + dev: true + + /streamx@2.16.1: + resolution: {integrity: sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + optionalDependencies: + bare-events: 2.2.2 + dev: true + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: true + + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /style-dictionary@3.9.2: + resolution: {integrity: sha512-M2pcQ6hyRtqHOh+NyT6T05R3pD/gwNpuhREBKvxC1En0vyywx+9Wy9nXWT1SZ9ePzv1vAo65ItnpA16tT9ZUCg==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + change-case: 4.1.2 + commander: 8.3.0 + fs-extra: 10.1.0 + glob: 10.3.12 + json5: 2.2.3 + jsonc-parser: 3.2.1 + lodash: 4.17.21 + tinycolor2: 1.6.0 + dev: true + + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: true + + /svgo@3.2.0: + resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.0.0 + dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tailwindcss@3.2.7(postcss@8.4.21): + resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==} + engines: {node: '>=12.13.0'} + hasBin: true + peerDependencies: + postcss: ^8.0.9 + dependencies: + arg: 5.0.2 + chokidar: 3.6.0 + color-name: 1.1.4 + detective: 5.2.1 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-import: 14.1.0(postcss@8.4.21) + postcss-js: 4.0.1(postcss@8.4.21) + postcss-load-config: 3.1.4(postcss@8.4.21) + postcss-nested: 6.0.0(postcss@8.4.21) + postcss-selector-parser: 6.0.16 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.8 + transitivePeerDependencies: + - ts-node + dev: true + + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + + /tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + dependencies: + b4a: 1.6.6 + fast-fifo: 1.3.2 + streamx: 2.16.1 + dev: true + + /terser-webpack-plugin@5.3.10(webpack@5.91.0): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.0 + webpack: 5.91.0 + dev: true + + /terser@5.31.0: + resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.11.3 + commander: 2.20.3 + source-map-support: 0.5.21 + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + dev: true + + /through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + dependencies: + readable-stream: 2.3.8 + xtend: 4.0.2 + dev: true + + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + /tmp@0.2.3: + resolution: {integrity: sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==} + engines: {node: '>=14.14'} + dev: true + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + /to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + dependencies: + to-space-case: 1.0.0 + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + + /tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + dev: false + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-api-utils@1.3.0(typescript@4.9.5): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 4.9.5 + dev: true + + /ts-jest@29.1.1(@babel/core@7.24.3)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.24.3 + babel-jest: 29.6.2(@babel/core@7.24.3) + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@20.11.30) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.0 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + + /ts-node-dev@2.0.0(@types/node@20.11.30)(typescript@4.9.5): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.8 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.11.30)(typescript@4.9.5) + tsconfig: 7.0.0 + typescript: 4.9.5 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: true + + /ts-node@10.9.2(@types/node@20.11.30)(typescript@4.9.5): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.11.30 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /ts-results@3.3.0: + resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} + dev: false + + /tsconfig-paths-jest@0.0.1: + resolution: {integrity: sha512-YKhUKqbteklNppC2NqL7dv1cWF8eEobgHVD5kjF1y9Q4ocqpBiaDlYslQ9eMhtbqIPRrA68RIEXqknEjlxdwxw==} + dev: true + + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.1.2 + + /tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + /type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: true + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + + /typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + dependencies: + is-typedarray: 1.0.0 + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uncontrollable@7.2.1(react@18.2.0): + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + dependencies: + '@babel/runtime': 7.24.1 + '@types/react': 18.2.66 + invariant: 2.2.4 + react: 18.2.0 + react-lifecycles-compat: 3.0.4 + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /unicode-canonical-property-names-ecmascript@2.0.0: + resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} + engines: {node: '>=4'} + dev: true + + /unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.0 + unicode-property-aliases-ecmascript: 2.1.0 + dev: true + + /unicode-match-property-value-ecmascript@2.1.0: + resolution: {integrity: sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==} + engines: {node: '>=4'} + dev: true + + /unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} + engines: {node: '>=4'} + dev: true + + /uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + dev: false + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unsplash-js@7.0.19: + resolution: {integrity: sha512-j6qT2floy5Q2g2d939FJpwey1yw/GpQecFiSouyJtsHQPj3oqmqq3K4rI+GF8vU1zwGCT7ZwIGQd2dtCQLjYJw==} + engines: {node: '>=10'} + dev: false + + /untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + dev: true + + /update-browserslist-db@1.0.13(browserslist@4.23.0): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.0 + + /upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + dependencies: + tslib: 2.6.2 + dev: true + + /upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + dependencies: + tslib: 2.6.2 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /use-sync-external-store@1.2.2(react@18.2.0): + resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: true + + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + /validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + dev: false + + /verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + /vite-plugin-compression2@1.0.0: + resolution: {integrity: sha512-42XNp6FjxE0JIecxj1fdi770pLhYm3MJhBUAod9EszTgDg9C4LDOgBzWcj/0K52KfJrpRXwUsWV6kqTDuoCfLA==} + dependencies: + '@rollup/pluginutils': 5.1.0 + gunzip-maybe: 1.4.2 + tar-stream: 3.1.7 + transitivePeerDependencies: + - rollup + dev: true + + /vite-plugin-importer@0.2.5: + resolution: {integrity: sha512-6OtqJmVwnfw8+B4OIh7pIdXs+jLkN7g5PIqmZdpgrMYjIFMiZrcMB1zlyUQSTokKGC90KwXviO/lq1hcUBUG3Q==} + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) + babel-plugin-import: 1.13.8 + transitivePeerDependencies: + - supports-color + dev: true + + /vite-plugin-istanbul@6.0.2(vite@5.2.0): + resolution: {integrity: sha512-0/sKwjEEIwbEyl43xX7onX3dIbMJAsigNsKyyVPalG1oRFo5jn3qkJbS2PUfp9wrr3piy1eT6qRoeeum2p4B2A==} + peerDependencies: + vite: '>=4 <=6' + dependencies: + '@istanbuljs/load-nyc-config': 1.1.0 + espree: 10.0.1 + istanbul-lib-instrument: 6.0.2 + picocolors: 1.0.0 + source-map: 0.7.4 + test-exclude: 6.0.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + transitivePeerDependencies: + - supports-color + dev: true + + /vite-plugin-svgr@3.2.0(typescript@4.9.5)(vite@5.2.0): + resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==} + peerDependencies: + vite: ^2.6.0 || 3 || 4 + dependencies: + '@rollup/pluginutils': 5.1.0 + '@svgr/core': 7.0.0(typescript@4.9.5) + '@svgr/plugin-jsx': 7.0.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + dev: true + + /vite-plugin-terminal@1.2.0(vite@5.2.0): + resolution: {integrity: sha512-IIw1V+IySth8xlrGmH4U7YmfTp681vTzYpa7b8A3KNCJ2oW1BGPPwW8tSz6BQTvSgbRmrP/9NsBLsfXkN4e8sA==} + engines: {node: '>=14'} + peerDependencies: + vite: ^2.0.0||^3.0.0||^4.0.0||^5.0.0 + dependencies: + '@rollup/plugin-strip': 3.0.4 + debug: 4.3.4(supports-color@8.1.1) + kolorist: 1.8.0 + sirv: 2.0.4 + ufo: 1.5.3 + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /vite-plugin-total-bundle-size@1.0.7(vite@5.2.0): + resolution: {integrity: sha512-ritAi5hRcuNonHP1wquvzqkZHGpOqRpWiMoEQQDJ3DLYuuVAS3THKyIGv7QSGig5nT+xuMYTLUamBu3Legaipg==} + peerDependencies: + vite: '>=5.0.0' + dependencies: + chalk: 5.3.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + dev: true + + /vite-plugin-wasm@3.3.0(vite@5.2.0): + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + dependencies: + vite: 5.2.0(@types/node@20.11.30)(sass@1.77.2) + dev: false + + /vite@5.2.0(@types/node@20.11.30)(sass@1.77.2): + resolution: {integrity: sha512-xMSLJNEjNk/3DJRgWlPADDwaU9AgYRodDH2t6oENhJnIlmU9Hx1Q6VpjyXua/JdMw1WJRbnAgHJ9xgET9gnIAg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.30 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.13.2 + sass: 1.77.2 + optionalDependencies: + fsevents: 2.3.3 + + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + + /watchpack@2.4.1: + resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} + engines: {node: '>=10.13.0'} + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + dev: true + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: true + + /webpack@5.91.0: + resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) + browserslist: 4.23.0 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.16.1 + es-module-lexer: 1.5.3 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.91.0) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + dependencies: + imurmurhash: 0.1.4 + is-typedarray: 1.0.0 + signal-exit: 3.0.7 + typedarray-to-buffer: 3.1.5 + dev: true + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y-indexeddb@9.0.12(yjs@13.6.15): + resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.94 + yjs: 13.6.15 + dev: false + + /y-protocols@1.0.6(yjs@13.6.15): + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.94 + yjs: 13.6.15 + dev: false + + /y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + /yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + dependencies: + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 4.2.3 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 + dev: true + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + dev: true + + /yjs@13.6.15: + resolution: {integrity: sha512-moFv4uNYhp8BFxIk3AkpoAnnjts7gwdpiG8RtyFiKbMtxKCS0zVZ5wPaaGpwC3V2N/K8TK8MwtSI3+WO9CHWjQ==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.94 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + /yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/frontend/appflowy_web_app/postcss.config.cjs b/frontend/appflowy_web_app/postcss.config.cjs new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/frontend/appflowy_web_app/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/appflowy_web_app/public/appflowy.svg b/frontend/appflowy_web_app/public/appflowy.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_web_app/public/appflowy.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/public/launch_splash.jpg b/frontend/appflowy_web_app/public/launch_splash.jpg new file mode 100644 index 0000000000..7e3bb9cee6 Binary files /dev/null and b/frontend/appflowy_web_app/public/launch_splash.jpg differ diff --git a/frontend/appflowy_web_app/scripts/create-symlink.cjs b/frontend/appflowy_web_app/scripts/create-symlink.cjs new file mode 100644 index 0000000000..472f511f27 --- /dev/null +++ b/frontend/appflowy_web_app/scripts/create-symlink.cjs @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +const sourcePath = process.argv[2]; +const targetPath = process.argv[3]; + +// ensure source and target paths are provided +if (!sourcePath || !targetPath) { + console.error(chalk.red('source and target paths are required')); + process.exit(1); +} + +const fullSourcePath = path.resolve(sourcePath); +const fullTargetPath = path.resolve(targetPath); +// ensure source path exists +if (!fs.existsSync(fullSourcePath)) { + console.error(chalk.red(`source path does not exist: ${fullSourcePath}`)); + process.exit(1); +} + +// ensure target path exists +if (!fs.existsSync(fullTargetPath)) { + console.error(chalk.red(`target path does not exist: ${fullTargetPath}`)); + process.exit(1); +} + + +if (fs.existsSync(fullTargetPath)) { + // unlink existing symlink + console.log(chalk.yellow(`unlinking existing symlink: `) + chalk.blue(`${fullTargetPath}`)); + fs.unlinkSync(fullTargetPath); +} + +// create symlink +fs.symlink(fullSourcePath, fullTargetPath, 'junction', (err) => { + if (err) { + console.error(chalk.red(`error creating symlink: ${err.message}`)); + process.exit(1); + } + console.log(chalk.green(`symlink created: `) + chalk.blue(`${fullSourcePath}`) + ' -> ' + chalk.blue(`${fullTargetPath}`)); + +}); diff --git a/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs b/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs new file mode 100644 index 0000000000..83f5bb25d5 --- /dev/null +++ b/frontend/appflowy_web_app/scripts/generateTailwindColors.cjs @@ -0,0 +1,61 @@ +const fs = require('fs'); +const path = require('path'); + +// Read CSS file +const cssFilePath = path.join(__dirname, '../src/styles/variables/light.variables.css'); +const cssContent = fs.readFileSync(cssFilePath, 'utf-8'); + +// Extract color variables +const shadowVariables = cssContent.match(/--shadow:\s.*;/g); +const colorVariables = cssContent.match(/--[\w-]+:\s*#[0-9a-fA-F]{6}/g); + +if (!colorVariables) { + console.error('No color variables found in CSS file.'); + process.exit(1); +} + +const shadows = shadowVariables.reduce((shadows, variable) => { + const [name, value] = variable.split(':').map(str => str.trim()); + const formattedName = name.replace('--', '').replace(/-/g, '_'); + const key = 'md'; + + shadows[key] = `var(${name})`; + return shadows; +}, {}); +// Generate Tailwind CSS colors configuration +// Replace -- with _ and - with _ in color variable names +const tailwindColors = colorVariables.reduce((colors, variable) => { + const [name, value] = variable.split(':').map(str => str.trim()); + const formattedName = name.replace('--', '').replace(/-/g, '_'); + const category = formattedName.split('_')[0]; + const key = formattedName.replace(`${category}_`, ''); + + if (!colors[category]) { + colors[category] = {}; + } + colors[category][key] = `var(${name})`; + return colors; +}, {}); + +const tailwindColorsFormatted = JSON.stringify(tailwindColors, null, 2) + .replace(/_/g, '-'); +const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + +// Write Tailwind CSS colors configuration to file +const tailwindColorTemplate = ` +${header} +module.exports = ${tailwindColorsFormatted}; +`; + +const tailwindShadowTemplate = ` +${header} +module.exports = ${JSON.stringify(shadows, null, 2).replace(/_/g, '-')}; +`; + +const tailwindConfigFilePath = path.join(__dirname, '../tailwind/colors.cjs'); +fs.writeFileSync(tailwindConfigFilePath, tailwindColorTemplate, 'utf-8'); + +const tailwindShadowFilePath = path.join(__dirname, '../tailwind/box-shadow.cjs'); +fs.writeFileSync(tailwindShadowFilePath, tailwindShadowTemplate, 'utf-8'); + +console.log('Tailwind CSS colors configuration generated successfully.'); diff --git a/frontend/appflowy_web_app/scripts/i18n.cjs b/frontend/appflowy_web_app/scripts/i18n.cjs new file mode 100644 index 0000000000..407a03694a --- /dev/null +++ b/frontend/appflowy_web_app/scripts/i18n.cjs @@ -0,0 +1,63 @@ +const languages = [ + 'ar-SA', + 'ca-ES', + 'de-DE', + 'en', + 'es-VE', + 'eu-ES', + 'fr-FR', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'th-TH', + 'tr-TR', + 'zh-CN', + 'zh-TW', +]; + +const fs = require('fs'); +languages.forEach(language => { + const json = require(`../../resources/translations/${language}.json`); + const outputJSON = flattenJSON(json); + const output = JSON.stringify(outputJSON); + const isExistDir = fs.existsSync('./src/@types/translations'); + if (!isExistDir) { + fs.mkdirSync('./src/@types/translations'); + } + fs.writeFile(`./src/@types/translations/${language}.json`, new Uint8Array(Buffer.from(output)), (res) => { + if (res) { + console.error(res); + } + }) +}); + + +function flattenJSON(obj, prefix = '') { + let result = {}; + const pluralsKey = ["one", "other", "few", "many", "two", "zero"]; + + for (let key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + + const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`); + result = { ...result, ...nestedKeys }; + } else { + let newKey = `${prefix}${key}`; + let replaceChar = '{' + if (pluralsKey.includes(key)) { + newKey = `${prefix.slice(0, -1)}_${key}`; + } + result[newKey] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}'); + } + } + + return result; +} + diff --git a/frontend/appflowy_web_app/server.cjs b/frontend/appflowy_web_app/server.cjs new file mode 100644 index 0000000000..e13c337faa --- /dev/null +++ b/frontend/appflowy_web_app/server.cjs @@ -0,0 +1,114 @@ +const path = require('path'); +const fs = require('fs'); +const pino = require('pino'); +const cheerio = require('cheerio'); +const axios = require('axios'); + +const distDir = path.join(__dirname, 'dist'); +const indexPath = path.join(distDir, 'index.html'); + +const setOrUpdateMetaTag = ($, selector, attribute, content) => { + if ($(selector).length === 0) { + $('head').append(``); + } else { + $(selector).attr('content', content); + } +}; +// Create a new logger instance +const logger = pino({ + transport: { + target: 'pino-pretty', + level: 'info', + options: { + colorize: true, + translateTime: 'SYS:standard', + destination: `${__dirname}/pino-logger.log`, + }, + }, +}); + +const logRequestTimer = (req) => { + const start = Date.now(); + const pathname = new URL(req.url).pathname; + logger.info(`Incoming request: ${pathname}`); + return () => { + const duration = Date.now() - start; + logger.info(`Request for ${pathname} took ${duration}ms`); + }; +}; + +const fetchMetaData = async (url) => { + try { + const response = await axios.get(url); + return response.data; + } catch (error) { + logger.error('Error fetching meta data', error); + return null; + } +}; + +const createServer = async (req) => { + const timer = logRequestTimer(req); + + if (req.method === 'GET') { + const pageId = req.url.split('/').pop(); + let htmlData = fs.readFileSync(indexPath, 'utf8'); + const $ = cheerio.load(htmlData); + if (!pageId) { + timer(); + return new Response($.html(), { + headers: { 'Content-Type': 'text/html' }, + }); + } + + const description = 'Write, share, comment, react, and publish docs quickly and securely on AppFlowy.'; + let title = 'AppFlowy'; + const url = 'https://appflowy.com'; + let image = 'https://d3uafhn8yrvdfn.cloudfront.net/website/production/_next/static/media/og-image.e347bfb5.png'; + // Inject meta data into the HTML to support SEO and social sharing + // if (metaData) { + // title = metaData.title; + // image = metaData.image; + // } + + $('title').text(title); + setOrUpdateMetaTag($, 'meta[name="description"]', 'name', description); + setOrUpdateMetaTag($, 'meta[property="og:title"]', 'property', title); + setOrUpdateMetaTag($, 'meta[property="og:description"]', 'property', description); + setOrUpdateMetaTag($, 'meta[property="og:image"]', 'property', image); + setOrUpdateMetaTag($, 'meta[property="og:url"]', 'property', url); + setOrUpdateMetaTag($, 'meta[property="og:type"]', 'property', 'article'); + setOrUpdateMetaTag($, 'meta[name="twitter:card"]', 'name', 'summary_large_image'); + setOrUpdateMetaTag($, 'meta[name="twitter:title"]', 'name', title); + setOrUpdateMetaTag($, 'meta[name="twitter:description"]', 'name', description); + setOrUpdateMetaTag($, 'meta[name="twitter:image"]', 'name', image); + + timer(); + return new Response($.html(), { + headers: { 'Content-Type': 'text/html' }, + }); + } else { + timer(); + logger.error({ message: 'Method not allowed', method: req.method }); + return new Response('Method not allowed', { status: 405 }); + } +}; + +const start = () => { + try { + Bun.serve({ + port: 3000, + fetch: createServer, + error: (err) => { + logger.error(`Internal Server Error: ${err}`); + return new Response('Internal Server Error', { status: 500 }); + }, + }); + logger.info(`Server is running on port 3000`); + } catch (err) { + logger.error(err); + process.exit(1); + } +}; + +start(); diff --git a/frontend/appflowy_web_app/src-tauri/.gitignore b/frontend/appflowy_web_app/src-tauri/.gitignore new file mode 100644 index 0000000000..9e4914893d --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +.env \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock new file mode 100644 index 0000000000..e3589d8718 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -0,0 +1,8467 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "accessory" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "log", + "rand 0.7.3", + "wasm-timer", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.12", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allo-isolate" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509" +dependencies = [ + "atomic", + "pin-project", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "getrandom 0.2.12", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tsify", + "url", + "uuid", + "wasm-bindgen", +] + +[[package]] +name = "appflowy-ai-client" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bytes", + "futures", + "serde", + "serde_json", + "serde_repr", + "thiserror", +] + +[[package]] +name = "appflowy_tauri" +version = "0.0.0" +dependencies = [ + "bytes", + "dotenv", + "flowy-chat", + "flowy-config", + "flowy-core", + "flowy-date", + "flowy-document", + "flowy-error", + "flowy-notification", + "flowy-user", + "lib-dispatch", + "semver", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-utils", + "tracing", + "uuid", +] + +[[package]] +name = "arboard" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" +dependencies = [ + "clipboard-win", + "core-graphics 0.23.1", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot 0.12.1", + "thiserror", + "windows-sys 0.48.0", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.55", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.55", + "syn_derive", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "census" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.2", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.2", + "phf_codegen 0.11.2", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "client-api" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "again", + "anyhow", + "app-error", + "async-trait", + "bincode", + "brotli", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", + "database-entity", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "gotrue", + "gotrue-entity", + "mime", + "parking_lot 0.12.1", + "prost", + "reqwest", + "scraper 0.17.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "serde_urlencoded", + "shared-entity", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "wasm-bindgen-futures", + "yrs", +] + +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "clipboard-win" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmd_lib" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f4cbdcab51ca635c5b19c85ece4072ea42e0d2360242826a6fc96fb11f0d40" +dependencies = [ + "cmd_lib_macros", + "env_logger", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae881960f7e2a409f91ef0b1c09558cf293031a1d6e8b45f908311f2a43f5fdf" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "collab" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "chrono", + "js-sys", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-database" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "collab", + "collab-entity", + "collab-plugins", + "dashmap", + "getrandom 0.2.12", + "js-sys", + "lazy_static", + "nanoid", + "parking_lot 0.12.1", + "rayon", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "collab-document" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "collab-entity" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "bytes", + "collab", + "getrandom 0.2.12", + "serde", + "serde_json", + "serde_repr", + "uuid", +] + +[[package]] +name = "collab-folder" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-integrate" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collab", + "collab-entity", + "collab-plugins", + "futures", + "lib-infra", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "collab-plugins" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bincode", + "bytes", + "chrono", + "collab", + "collab-entity", + "futures", + "futures-util", + "getrandom 0.2.12", + "indexed_db_futures", + "js-sys", + "lazy_static", + "parking_lot 0.12.1", + "rand 0.8.5", + "rocksdb", + "serde", + "serde_json", + "similar 2.4.0", + "smallvec", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tracing", + "tracing-wasm", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "collab", + "collab-entity", + "serde", + "thiserror", + "tokio", + "tracing", + "yrs", +] + +[[package]] +name = "collab-user" +version = "0.2.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 1.0.10", + "phf 0.11.2", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.55", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +dependencies = [ + "quote", + "syn 2.0.55", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.55", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "database-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "bincode", + "chrono", + "collab-entity", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "date_time_parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0521d96e513670773ac503e5f5239178c3aef16cffda1e77a3cdbdbe993fb5a" +dependencies = [ + "chrono", + "regex", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "deunicode" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" + +[[package]] +name = "diesel" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" +dependencies = [ + "chrono", + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "serde_json", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.55", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "embed-resource" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.12", + "vswhom", + "winreg 0.52.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + +[[package]] +name = "fancy-regex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "fastdivide" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59668941c55e5c186b8b58c391629af56774ec768f73c08bbcd56f09348eb00b" + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flowy-ast" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "flowy-chat" +version = "0.1.0" +dependencies = [ + "allo-isolate", + "bytes", + "dashmap", + "flowy-chat-pub", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-sqlite", + "futures", + "lib-dispatch", + "lib-infra", + "log", + "protobuf", + "strum_macros 0.21.1", + "tokio", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-chat-pub" +version = "0.1.0" +dependencies = [ + "bytes", + "client-api", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-codegen" +version = "0.1.0" +dependencies = [ + "cmd_lib", + "console", + "fancy-regex 0.10.0", + "flowy-ast", + "itertools 0.10.5", + "lazy_static", + "log", + "phf 0.8.0", + "protoc-bin-vendored", + "protoc-rust", + "quote", + "serde", + "serde_json", + "similar 1.3.0", + "syn 1.0.109", + "tera", + "toml 0.5.11", + "walkdir", +] + +[[package]] +name = "flowy-config" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", +] + +[[package]] +name = "flowy-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "client-api", + "collab", + "collab-entity", + "collab-integrate", + "collab-plugins", + "diesel", + "flowy-chat", + "flowy-chat-pub", + "flowy-config", + "flowy-database-pub", + "flowy-database2", + "flowy-date", + "flowy-document", + "flowy-document-pub", + "flowy-error", + "flowy-folder", + "flowy-folder-pub", + "flowy-search", + "flowy-search-pub", + "flowy-server", + "flowy-server-pub", + "flowy-sqlite", + "flowy-storage", + "flowy-user", + "flowy-user-pub", + "futures", + "futures-core", + "lib-dispatch", + "lib-infra", + "lib-log", + "parking_lot 0.12.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "sysinfo", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "walkdir", +] + +[[package]] +name = "flowy-database-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "client-api", + "collab", + "collab-entity", + "lib-infra", +] + +[[package]] +name = "flowy-database2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bytes", + "chrono", + "chrono-tz", + "collab", + "collab-database", + "collab-entity", + "collab-integrate", + "collab-plugins", + "csv", + "dashmap", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-database-pub", + "flowy-derive", + "flowy-error", + "flowy-notification", + "futures", + "indexmap 2.2.6", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "rayon", + "rust_decimal", + "rusty-money", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "tokio", + "tracing", + "url", + "validator", +] + +[[package]] +name = "flowy-date" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "date_time_parser", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", + "tracing", +] + +[[package]] +name = "flowy-derive" +version = "0.1.0" +dependencies = [ + "dashmap", + "flowy-ast", + "flowy-codegen", + "lazy_static", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "flowy-document" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "collab", + "collab-document", + "collab-entity", + "collab-integrate", + "collab-plugins", + "dashmap", + "flowy-codegen", + "flowy-derive", + "flowy-document-pub", + "flowy-error", + "flowy-notification", + "flowy-storage", + "futures", + "getrandom 0.2.12", + "indexmap 2.2.6", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "scraper 0.18.1", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-document-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-document", + "flowy-error", + "lib-infra", +] + +[[package]] +name = "flowy-encrypt" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.21.7", + "getrandom 0.2.12", + "hmac", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2", +] + +[[package]] +name = "flowy-error" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "client-api", + "collab-database", + "collab-document", + "collab-folder", + "collab-plugins", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "r2d2", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "tantivy", + "thiserror", + "tokio", + "url", + "validator", +] + +[[package]] +name = "flowy-folder" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-folder-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "collab-folder", + "lib-infra", + "uuid", +] + +[[package]] +name = "flowy-notification" +version = "0.1.0" +dependencies = [ + "bytes", + "dashmap", + "flowy-codegen", + "flowy-derive", + "lazy_static", + "lib-dispatch", + "protobuf", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "flowy-search" +version = "0.1.0" +dependencies = [ + "async-stream", + "bytes", + "collab", + "collab-folder", + "diesel", + "diesel_derives", + "diesel_migrations", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder", + "flowy-notification", + "flowy-search-pub", + "flowy-sqlite", + "flowy-user", + "futures", + "lib-dispatch", + "lib-infra", + "protobuf", + "serde", + "serde_json", + "strsim 0.11.1", + "strum_macros 0.26.2", + "tantivy", + "tempfile", + "tokio", + "tracing", + "validator", +] + +[[package]] +name = "flowy-search-pub" +version = "0.1.0" +dependencies = [ + "client-api", + "collab", + "collab-folder", + "flowy-error", + "futures", + "lib-infra", +] + +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "client-api", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-plugins", + "flowy-chat-pub", + "flowy-database-pub", + "flowy-document-pub", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-search-pub", + "flowy-server-pub", + "flowy-storage", + "flowy-user-pub", + "futures", + "futures-util", + "hex", + "hyper", + "lazy_static", + "lib-dispatch", + "lib-infra", + "mime_guess", + "parking_lot 0.12.1", + "postgrest", + "rand 0.8.5", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "yrs", +] + +[[package]] +name = "flowy-server-pub" +version = "0.1.0" +dependencies = [ + "flowy-error", + "serde", + "serde_repr", +] + +[[package]] +name = "flowy-sqlite" +version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel_derives", + "diesel_migrations", + "libsqlite3-sys", + "parking_lot 0.12.1", + "r2d2", + "scheduled-thread-pool", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "flowy-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "flowy-error", + "fxhash", + "lib-infra", + "mime", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "flowy-user" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "chrono", + "collab", + "collab-database", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "collab-user", + "diesel", + "diesel_derives", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-server-pub", + "flowy-sqlite", + "flowy-user-pub", + "lazy_static", + "lib-dispatch", + "lib-infra", + "once_cell", + "parking_lot 0.12.1", + "protobuf", + "semver", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-user-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "chrono", + "collab", + "collab-entity", + "flowy-error", + "flowy-folder-pub", + "lib-infra", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.2", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gotrue" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "futures-util", + "getrandom 0.2.12", + "gotrue-entity", + "infra", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "gotrue-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "chrono", + "jsonwebtoken", + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.6", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexed_db_futures" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "infra" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser 0.27.2", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors 0.22.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + +[[package]] +name = "lib-dispatch" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "derivative", + "dyn-clone", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "thread-id", + "tokio", + "tracing", + "validator", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "lib-infra" +version = "0.1.0" +dependencies = [ + "allo-isolate", + "anyhow", + "async-trait", + "atomic_refcell", + "bytes", + "chrono", + "futures", + "futures-core", + "md5", + "pin-project", + "tempfile", + "tokio", + "tracing", + "validator", + "walkdir", + "zip", +] + +[[package]] +name = "lib-log" +version = "0.1.0" +dependencies = [ + "chrono", + "lazy_static", + "lib-infra", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-bunyan-formatter", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.4", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" +dependencies = [ + "hashbrown 0.14.3", +] + +[[package]] +name = "lz4_flex" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memmap2" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "murmurhash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2195bf6aa996a481483b29d62a7663eed3fe39600c460e323f8ff41e90bdd89b" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oneshot" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "ownedbytes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap 2.2.6", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" +dependencies = [ + "reqwest", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +dependencies = [ + "proc-macro2", + "syn 2.0.55", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools 0.11.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.55", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "protobuf-codegen" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protoc" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0218039c514f9e14a5060742ecd50427f8ac4f85a6dc58f2ddb806e318c55ee" +dependencies = [ + "log", + "which", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + +[[package]] +name = "protoc-rust" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f8a182bb17c485f20bdc4274a8c39000a61024cfe461c799b50fec77267838" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc", + "tempfile", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.12", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.50.0", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.12", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "rust_decimal" +version = "1.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e418701588729bef95e7a655f2b483ad64bb97c46e8e79fde83efd92aaab6d82" +dependencies = [ + "quote", + "rust_decimal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a930e03325234c18c7071fd2b60118307e025d6fff3e12745ffbf63a3d29c" +dependencies = [ + "ahash 0.8.11", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors 0.25.0", + "smallvec", + "tendril", +] + +[[package]] +name = "scraper" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" +dependencies = [ + "ahash 0.8.11", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors 0.25.0", + "tendril", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.27.2", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.1.1", + "smallvec", + "thin-slice", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.5.0", + "cssparser 0.31.2", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc 0.3.0", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "indexmap 2.2.6", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" +dependencies = [ + "anyhow", + "app-error", + "appflowy-ai-client", + "bytes", + "chrono", + "collab-entity", + "database-entity", + "futures", + "gotrue-entity", + "log", + "pin-project", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "sketches-ddsketch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.55", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.55", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.30.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr 0.15.7", + "heck 0.5.0", + "pkg-config", + "toml 0.8.12", + "version-compare 0.2.0", +] + +[[package]] +name = "tantivy" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" +dependencies = [ + "aho-corasick", + "arc-swap", + "async-trait", + "base64 0.21.7", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fastdivide", + "fs4", + "htmlescape", + "itertools 0.11.0", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" +dependencies = [ + "fastdivide", + "fnv", + "itertools 0.11.0", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" +dependencies = [ + "nom", +] + +[[package]] +name = "tantivy-sstable" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.4", +] + +[[package]] +name = "tantivy-stacker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" +dependencies = [ + "serde", +] + +[[package]] +name = "tao" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22205b267a679ca1c590b9f178488d50981fc3e48a1b91641ae31593db875ce" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics 0.22.3", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tauri" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed" +dependencies = [ + "anyhow", + "cocoa", + "dirs-next", + "dunce", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http", + "ignore", + "objc", + "once_cell", + "open", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "regex", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-build" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" +dependencies = [ + "base64 0.21.7", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "regex", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" +dependencies = [ + "gtk", + "http", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067c56fc153b3caf406d7cd6de4486c80d1d66c0f414f39e94cb2f5543f6445f" +dependencies = [ + "arboard", + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa 1.0.10", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.9", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.6", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash 0.8.11", + "gethostname 0.2.3", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-serde", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tree_magic_mini" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + +[[package]] +name = "treediff" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.55", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.12", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna 0.4.0", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "wry" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "gethostname 0.4.3", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "yrs" +version = "0.18.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" +dependencies = [ + "arc-swap", + "atomic_refcell", + "fastrand", + "serde", + "serde_json", + "smallstr", + "smallvec", + "thiserror", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" +dependencies = [ + "zstd-safe 6.0.6", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "6.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml new file mode 100644 index 0000000000..597dfb2fb4 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -0,0 +1,116 @@ +[package] +name = "appflowy_tauri" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.57" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[workspace.dependencies] +anyhow = "1.0" +tracing = "0.1.40" +bytes = "1.5.0" +serde = "1.0" +serde_json = "1.0.108" +protobuf = { version = "2.28.0" } +diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] } +uuid = { version = "1.5.0", features = ["serde", "v4"] } +serde_repr = "0.1" +parking_lot = "0.12" +futures = "0.3.29" +tokio = "1.34.0" +tokio-stream = "0.1.14" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } +yrs = "0.18.8" +# Please use the following script to update collab. +# Working directory: frontend +# +# To update the commit ID, run: +# scripts/tool/update_collab_rev.sh new_rev_id +# +# To switch to the local path, run: +# scripts/tool/update_collab_source.sh +# ⚠️⚠️⚠️️ +collab = { version = "0.2" } +collab-entity = { version = "0.2" } +collab-folder = { version = "0.2" } +collab-document = { version = "0.2" } +collab-database = { version = "0.2" } +collab-plugins = { version = "0.2" } +collab-user = { version = "0.2" } + +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" } + +[dependencies] +serde_json.workspace = true +serde.workspace = true +tauri = { version = "1.5", features = [ + "dialog-all", + "clipboard-all", + "fs-all", + "shell-open", +] } +tauri-utils = "1.5.2" +bytes.workspace = true +tracing.workspace = true +lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = [ + "use_serde", +] } +flowy-core = { path = "../../rust-lib/flowy-core", features = [ + "ts", +] } +flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] } +flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] } +flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] } +flowy-error = { path = "../../rust-lib/flowy-error", features = [ + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_appflowy_cloud", + "impl_from_reqwest", + "impl_from_serde", + "tauri_ts", +] } +flowy-document = { path = "../../rust-lib/flowy-document", features = [ + "tauri_ts", +] } +flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ + "tauri_ts", +] } +flowy-chat = { path = "../../rust-lib/flowy-chat", features = [ + "tauri_ts", +] } + +uuid = "1.5.0" +tauri-plugin-deep-link = "0.1.2" +dotenv = "0.15.0" +semver = "1.0.23" + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL +default = ["custom-protocol"] +# this feature is used used for production builds where `devPath` points to the filesystem +# DO NOT remove this +custom-protocol = ["tauri/custom-protocol"] + +[patch.crates-io] +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } diff --git a/frontend/appflowy_web_app/src-tauri/Info.plist b/frontend/appflowy_web_app/src-tauri/Info.plist new file mode 100644 index 0000000000..25b430c049 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Info.plist @@ -0,0 +1,19 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + appflowy-flutter + CFBundleURLSchemes + + appflowy-flutter + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/build.rs b/frontend/appflowy_web_app/src-tauri/build.rs new file mode 100644 index 0000000000..795b9b7c83 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/frontend/appflowy_web_app/src-tauri/env.development b/frontend/appflowy_web_app/src-tauri/env.development new file mode 100644 index 0000000000..188835e3d0 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/env.development @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_web_app/src-tauri/env.production b/frontend/appflowy_web_app/src-tauri/env.production new file mode 100644 index 0000000000..b03c328b84 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/env.production @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_web_app/src-tauri/icons/128x128.png b/frontend/appflowy_web_app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000..3a51041313 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/128x128.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png b/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000..9076de3a4b Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/32x32.png b/frontend/appflowy_web_app/src-tauri/icons/32x32.png new file mode 100644 index 0000000000..6ae6683fef Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/32x32.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000..b08dcf7d21 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000..f3e437b76e Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000..6a1dc04864 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000..2f2d9d6fe6 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000..46e3802c0b Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000..230b1abe58 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000..ad188037a3 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000..ceae9ad1bb Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000..123dcea650 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png b/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000..d7906c3c03 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.icns b/frontend/appflowy_web_app/src-tauri/icons/icon.icns new file mode 100644 index 0000000000..74b585f25d Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.icns differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.ico b/frontend/appflowy_web_app/src-tauri/icons/icon.ico new file mode 100644 index 0000000000..cd9ad402d1 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.ico differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.png b/frontend/appflowy_web_app/src-tauri/icons/icon.png new file mode 100644 index 0000000000..7cc3853d67 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.png differ diff --git a/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml b/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml new file mode 100644 index 0000000000..6f14058b2e --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.77.2" diff --git a/frontend/appflowy_web_app/src-tauri/rustfmt.toml b/frontend/appflowy_web_app/src-tauri/rustfmt.toml new file mode 100644 index 0000000000..5cb0d67ee5 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/rustfmt.toml @@ -0,0 +1,12 @@ +# https://rust-lang.github.io/rustfmt/?version=master&search= +max_width = 100 +tab_spaces = 2 +newline_style = "Auto" +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +merge_derives = true +edition = "2021" \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs new file mode 100644 index 0000000000..42c857abdf --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -0,0 +1,61 @@ +use flowy_core::config::AppFlowyCoreConfig; +use flowy_core::{AppFlowyCore, DEFAULT_NAME}; +use lib_dispatch::runtime::AFPluginRuntime; +use std::sync::Arc; + +use dotenv::dotenv; + +pub fn read_env() { + dotenv().ok(); + + let env = if cfg!(debug_assertions) { + include_str!("../env.development") + } else { + include_str!("../env.production") + }; + + for line in env.lines() { + if let Some((key, value)) = line.split_once('=') { + // Check if the environment variable is not already set in the system + let current_value = std::env::var(key).unwrap_or_default(); + if current_value.is_empty() { + std::env::set_var(key, value); + } + } + } +} + +pub fn init_flowy_core() -> AppFlowyCore { + let config_json = include_str!("../tauri.conf.json"); + let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); + + let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); + let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); + if cfg!(debug_assertions) { + data_path.push("data_dev"); + } else { + data_path.push("data"); + } + + let custom_application_path = data_path.to_str().unwrap().to_string(); + let application_path = data_path.to_str().unwrap().to_string(); + let device_id = uuid::Uuid::new_v4().to_string(); + + read_env(); + std::env::set_var("RUST_LOG", "trace"); + + let config = AppFlowyCoreConfig::new( + app_version, + custom_application_path, + application_path, + device_id, + "web".to_string(), + DEFAULT_NAME.to_string(), + ) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); + + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(async move { AppFlowyCore::new(config, cloned_runtime, None).await }) +} diff --git a/frontend/appflowy_web_app/src-tauri/src/main.rs b/frontend/appflowy_web_app/src-tauri/src/main.rs new file mode 100644 index 0000000000..6a69de07fd --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/main.rs @@ -0,0 +1,71 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +#[allow(dead_code)] +pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter"; +pub const OPEN_DEEP_LINK: &str = "open_deep_link"; + +mod init; +mod notification; +mod request; + +use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; +use init::*; +use notification::*; +use request::*; +use tauri::Manager; +extern crate dotenv; + +fn main() { + tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); + + let flowy_core = init_flowy_core(); + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![invoke_request]) + .manage(flowy_core) + .on_window_event(|_window_event| {}) + .on_menu_event(|_menu| {}) + .on_page_load(|window, _payload| { + let app_handler = window.app_handle(); + // Make sure hot reload won't register the notification sender twice + unregister_all_notification_sender(); + register_notification_sender(TSNotificationSender::new(app_handler.clone())); + // tauri::async_runtime::spawn(async move {}); + + window.listen_global(AF_EVENT, move |event| { + on_event(app_handler.clone(), event); + }); + }) + .setup(|_app| { + let splashscreen_window = _app.get_window("splashscreen").unwrap(); + let window = _app.get_window("main").unwrap(); + let handle = _app.handle(); + + // we perform the initialization code on a new task so the app doesn't freeze + tauri::async_runtime::spawn(async move { + // initialize your app here instead of sleeping :) + std::thread::sleep(std::time::Duration::from_secs(2)); + + // After it's done, close the splashscreen and display the main window + splashscreen_window.close().unwrap(); + window.show().unwrap(); + // If you need macOS support this must be called in .setup() ! + // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + tauri_plugin_deep_link::register( + DEEP_LINK_SCHEME, + move |request| { + dbg!(&request); + handle.emit_all(OPEN_DEEP_LINK, request).unwrap(); + }, + ) + .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */); + }); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/frontend/appflowy_web_app/src-tauri/src/notification.rs b/frontend/appflowy_web_app/src-tauri/src/notification.rs new file mode 100644 index 0000000000..b42541edec --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/notification.rs @@ -0,0 +1,35 @@ +use flowy_notification::entities::SubscribeObject; +use flowy_notification::NotificationSender; +use serde::Serialize; +use tauri::{AppHandle, Event, Manager, Wry}; + +#[allow(dead_code)] +pub const AF_EVENT: &str = "af-event"; +pub const AF_NOTIFICATION: &str = "af-notification"; + +#[tracing::instrument(level = "trace")] +pub fn on_event(app_handler: AppHandle, event: Event) {} + +#[allow(dead_code)] +pub fn send_notification(app_handler: AppHandle, payload: P) { + app_handler.emit_all(AF_NOTIFICATION, payload).unwrap(); +} + +pub struct TSNotificationSender { + handler: AppHandle, +} + +impl TSNotificationSender { + pub fn new(handler: AppHandle) -> Self { + Self { handler } + } +} + +impl NotificationSender for TSNotificationSender { + fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { + self + .handler + .emit_all(AF_NOTIFICATION, subject) + .map_err(|e| format!("{:?}", e)) + } +} diff --git a/frontend/appflowy_web_app/src-tauri/src/request.rs b/frontend/appflowy_web_app/src-tauri/src/request.rs new file mode 100644 index 0000000000..029e71c18c --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/request.rs @@ -0,0 +1,45 @@ +use flowy_core::AppFlowyCore; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, +}; +use tauri::{AppHandle, Manager, State, Wry}; + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct AFTauriRequest { + ty: String, + payload: Vec, +} + +impl std::convert::From for AFPluginRequest { + fn from(event: AFTauriRequest) -> Self { + AFPluginRequest::new(event.ty).payload(event.payload) + } +} + +#[derive(Clone, serde::Serialize)] +pub struct AFTauriResponse { + code: StatusCode, + payload: Vec, +} + +impl std::convert::From for AFTauriResponse { + fn from(response: AFPluginEventResponse) -> Self { + Self { + code: response.status_code, + payload: response.payload.to_vec(), + } + } +} + +// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +#[tauri::command] +pub async fn invoke_request( + request: AFTauriRequest, + app_handler: AppHandle, +) -> AFTauriResponse { + let request: AFPluginRequest = request.into(); + let state: State = app_handler.state(); + let dispatcher = state.inner().dispatcher(); + let response = AFPluginDispatcher::async_send(dispatcher.as_ref(), request).await; + response.into() +} diff --git a/frontend/appflowy_web_app/src-tauri/tauri.conf.json b/frontend/appflowy_web_app/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..ea11f47def --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/tauri.conf.json @@ -0,0 +1,113 @@ +{ + "build": { + "beforeBuildCommand": "npm run build:tauri", + "beforeDevCommand": "npm run dev:tauri", + "devPath": "http://localhost:5173", + "distDir": "../dist", + "withGlobalTauri": false + }, + "package": { + "productName": "AppFlowy", + "version": "0.0.1" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + }, + "fs": { + "all": true, + "scope": [ + "$APPLOCALDATA/**" + ], + "readFile": true, + "writeFile": true, + "readDir": true, + "copyFile": true, + "createDir": true, + "removeDir": true, + "removeFile": true, + "renameFile": true, + "exists": true + }, + "clipboard": { + "all": true, + "writeText": true, + "readText": true + }, + "dialog": { + "all": true, + "ask": true, + "confirm": true, + "message": true, + "open": true, + "save": true + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": [], + "identifier": "com.appflowy.tauri", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null, + "minimumSystemVersion": "10.15.0" + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fileDropEnabled": false, + "fullscreen": false, + "height": 800, + "resizable": true, + "title": "AppFlowy", + "width": 1200, + "minWidth": 800, + "minHeight": 600, + "visible": false, + "label": "main" + }, + { + "height": 300, + "width": 549, + "decorations": false, + "url": "launch_splash.jpg", + "label": "splashscreen", + "center": true, + "visible": true + } + ] + } +} diff --git a/frontend/appflowy_web_app/src/@types/i18next.d.ts b/frontend/appflowy_web_app/src/@types/i18next.d.ts new file mode 100644 index 0000000000..6adbb4a512 --- /dev/null +++ b/frontend/appflowy_web_app/src/@types/i18next.d.ts @@ -0,0 +1,8 @@ +import resources from './resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation'; + resources: typeof resources; + } +} diff --git a/frontend/appflowy_web_app/src/@types/resources.ts b/frontend/appflowy_web_app/src/@types/resources.ts new file mode 100644 index 0000000000..6bd90364e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/@types/resources.ts @@ -0,0 +1,7 @@ +import translation from './translations/en.json'; + +const resources = { + translation, +} as const; + +export default resources; diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts new file mode 100644 index 0000000000..567be3b4ed --- /dev/null +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -0,0 +1,638 @@ +import * as Y from 'yjs'; + +export type BlockId = string; + +export type ExternalId = string; + +export type ChildrenId = string; + +export type ViewId = string; + +export type RowId = string; + +export type CellId = string; + +export enum BlockType { + Paragraph = 'paragraph', + Page = 'page', + HeadingBlock = 'heading', + TodoListBlock = 'todo_list', + BulletedListBlock = 'bulleted_list', + NumberedListBlock = 'numbered_list', + ToggleListBlock = 'toggle_list', + CodeBlock = 'code', + EquationBlock = 'math_equation', + QuoteBlock = 'quote', + CalloutBlock = 'callout', + DividerBlock = 'divider', + ImageBlock = 'image', + GridBlock = 'grid', + BoardBlock = 'board', + CalendarBlock = 'calendar', + OutlineBlock = 'outline', + TableBlock = 'table', + TableCell = 'table/cell', +} + +export enum InlineBlockType { + Formula = 'formula', + Mention = 'mention', +} + +export enum AlignType { + Left = 'left', + Center = 'center', + Right = 'right', +} + +export interface BlockData { + bg_color?: string; + font_color?: string; + align?: AlignType; +} + +export interface HeadingBlockData extends BlockData { + level: number; +} + +export interface NumberedListBlockData extends BlockData { + number: number; +} + +export interface TodoListBlockData extends BlockData { + checked: boolean; +} + +export interface ToggleListBlockData extends BlockData { + collapsed: boolean; +} + +export interface CodeBlockData extends BlockData { + language: string; +} + +export interface CalloutBlockData extends BlockData { + icon: string; +} + +export interface MathEquationBlockData extends BlockData { + formula?: string; +} + +export enum ImageType { + Local = 0, + Internal = 1, + External = 2, +} + +export interface ImageBlockData extends BlockData { + url?: string; + width?: number; + align?: AlignType; + image_type?: ImageType; + height?: number; +} + +export interface OutlineBlockData extends BlockData { + depth?: number; +} + +export interface TableBlockData extends BlockData { + colDefaultWidth: number; + colMinimumWidth: number; + colsHeight: number; + colsLen: number; + rowDefaultHeight: number; + rowsLen: number; +} + +export interface TableCellBlockData extends BlockData { + colPosition: number; + height: number; + rowPosition: number; + width: number; +} + +export interface DatabaseNodeData extends BlockData { + view_id: ViewId; +} + +export enum MentionType { + PageRef = 'page', + Date = 'date', +} + +export interface Mention { + // inline page ref id + page_id?: string; + // reminder date ref id + date?: string; + reminder_id?: string; + reminder_option?: string; + + type: MentionType; +} + +export interface FolderMeta { + current_view: ViewId; + current_workspace: string; +} + +export enum DocCoverType { + Color = 'CoverType.color', + Image = 'CoverType.file', + Asset = 'CoverType.asset', +} + +export type DocCover = { + image_type?: ImageType; + cover_selection_type?: DocCoverType; + cover_selection?: string; +} | null; + +export enum ViewLayout { + Document = 0, + Grid = 1, + Board = 2, + Calendar = 3, +} + +export enum YjsEditorKey { + data_section = 'data', + document = 'document', + database = 'database', + workspace_database = 'databases', + folder = 'folder', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + database_row = 'data', + user_awareness = 'user_awareness', + empty = 'empty', + + // document + blocks = 'blocks', + page_id = 'page_id', + meta = 'meta', + children_map = 'children_map', + text_map = 'text_map', + text = 'text', + delta = 'delta', + block_id = 'id', + block_type = 'ty', + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + block_data = 'data', + block_parent = 'parent', + block_children = 'children', + block_external_id = 'external_id', + block_external_type = 'external_type', +} + +export enum YjsFolderKey { + views = 'views', + relation = 'relation', + section = 'section', + private = 'private', + favorite = 'favorite', + recent = 'recent', + trash = 'trash', + meta = 'meta', + current_view = 'current_view', + current_workspace = 'current_workspace', + id = 'id', + name = 'name', + icon = 'icon', + extra = 'extra', + cover = 'cover', + line_height_layout = 'line_height_layout', + font_layout = 'font_layout', + type = 'ty', + value = 'value', + layout = 'layout', + bid = 'bid', +} + +export enum YjsDatabaseKey { + views = 'views', + id = 'id', + metas = 'metas', + fields = 'fields', + is_primary = 'is_primary', + last_modified = 'last_modified', + created_at = 'created_at', + name = 'name', + type = 'ty', + type_option = 'type_option', + content = 'content', + data = 'data', + iid = 'iid', + database_id = 'database_id', + field_orders = 'field_orders', + field_settings = 'field_settings', + visibility = 'visibility', + wrap = 'wrap', + width = 'width', + filters = 'filters', + groups = 'groups', + layout = 'layout', + layout_settings = 'layout_settings', + modified_at = 'modified_at', + row_orders = 'row_orders', + sorts = 'sorts', + height = 'height', + cells = 'cells', + field_type = 'field_type', + end_timestamp = 'end_timestamp', + include_time = 'include_time', + is_range = 'is_range', + reminder_id = 'reminder_id', + time_format = 'time_format', + date_format = 'date_format', + calculations = 'calculations', + field_id = 'field_id', + calculation_value = 'calculation_value', + condition = 'condition', + format = 'format', + filter_type = 'filter_type', + visible = 'visible', + hide_ungrouped_column = 'hide_ungrouped_column', + collapse_hidden_groups = 'collapse_hidden_groups', + first_day_of_week = 'first_day_of_week', + show_week_numbers = 'show_week_numbers', + show_weekends = 'show_weekends', + layout_ty = 'layout_ty', +} + +export interface YDoc extends Y.Doc { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getMap(key: YjsEditorKey.data_section): YSharedRoot | any; +} + +export interface YDatabaseRow extends Y.Map { + get(key: YjsDatabaseKey.id): RowId; + + get(key: YjsDatabaseKey.height): string; + + get(key: YjsDatabaseKey.visibility): boolean; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.cells): YDatabaseCells; +} + +export interface YDatabaseCells extends Y.Map { + get(key: FieldId): YDatabaseCell; +} + +export type EndTimestamp = string; +export type ReminderId = string; + +export interface YDatabaseCell extends Y.Map { + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.field_type): string; + + get(key: YjsDatabaseKey.data): object | string | boolean | number; + + get(key: YjsDatabaseKey.end_timestamp): EndTimestamp; + + get(key: YjsDatabaseKey.include_time): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.is_range): boolean; + + get(key: YjsDatabaseKey.reminder_id): ReminderId; +} + +export interface YSharedRoot extends Y.Map { + get(key: YjsEditorKey.document): YDocument; + + get(key: YjsEditorKey.folder): YFolder; + + get(key: YjsEditorKey.database): YDatabase; + + get(key: YjsEditorKey.database_row): YDatabaseRow; +} + +export interface YFolder extends Y.Map { + get(key: YjsFolderKey.views): YViews; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.meta): YFolderMeta; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.relation): YFolderRelation; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.section): YFolderSection; +} + +export interface YViews extends Y.Map { + get(key: ViewId): YView; +} + +export interface YView extends Y.Map { + get(key: YjsFolderKey.id): ViewId; + + get(key: YjsFolderKey.bid): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.name): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.icon | YjsFolderKey.extra): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsFolderKey.layout): string; +} + +export interface YFolderRelation extends Y.Map { + get(key: ViewId): Y.Array; +} + +export interface YFolderMeta extends Y.Map { + get(key: YjsFolderKey.current_view | YjsFolderKey.current_workspace): string; +} + +export interface YFolderSection extends Y.Map { + get(key: YjsFolderKey.favorite | YjsFolderKey.private | YjsFolderKey.recent | YjsFolderKey.trash): YFolderSectionItem; +} + +export interface YFolderSectionItem extends Y.Map { + get(key: string): Y.Array; +} + +export interface YDocument extends Y.Map { + get(key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; +} + +export interface YBlocks extends Y.Map { + get(key: BlockId): YBlock; +} + +export interface YBlock extends Y.Map { + get(key: YjsEditorKey.block_id | YjsEditorKey.block_parent): BlockId; + + get(key: YjsEditorKey.block_type): BlockType; + + get(key: YjsEditorKey.block_data): string; + + get(key: YjsEditorKey.block_children): ChildrenId; + + get(key: YjsEditorKey.block_external_id): ExternalId; +} + +export interface YMeta extends Y.Map { + get(key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; +} + +export interface YChildrenMap extends Y.Map { + get(key: ChildrenId): Y.Array; +} + +export interface YTextMap extends Y.Map { + get(key: ExternalId): Y.Text; +} + +export interface YDatabase extends Y.Map { + get(key: YjsDatabaseKey.views): YDatabaseViews; + + get(key: YjsDatabaseKey.metas): YDatabaseMetas; + + get(key: YjsDatabaseKey.fields): YDatabaseFields; + + get(key: YjsDatabaseKey.id): string; +} + +export interface YDatabaseViews extends Y.Map { + get(key: ViewId): YDatabaseView; +} + +export type DatabaseId = string; +export type CreatedAt = string; +export type LastModified = string; +export type ModifiedAt = string; +export type FieldId = string; + +export enum DatabaseViewLayout { + Grid = 0, + Board = 1, + Calendar = 2, +} + +export interface YDatabaseView extends Y.Map { + get(key: YjsDatabaseKey.database_id): DatabaseId; + + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.modified_at): ModifiedAt; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.layout): string; + + get(key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; + + get(key: YjsDatabaseKey.filters): YDatabaseFilters; + + get(key: YjsDatabaseKey.groups): YDatabaseGroups; + + get(key: YjsDatabaseKey.sorts): YDatabaseSorts; + + get(key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; + + get(key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; + + get(key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; + + get(key: YjsDatabaseKey.calculations): YDatabaseCalculations; +} + +export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] + +export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] + +export type YDatabaseGroups = Y.Array; + +export type YDatabaseFilters = Y.Array; + +export type YDatabaseSorts = Y.Array; + +export type YDatabaseCalculations = Y.Array; + +export type SortId = string; + +export type GroupId = string; + +export interface YDatabaseLayoutSettings extends Y.Map { + // DatabaseViewLayout.Board + get(key: '1'): YDatabaseBoardLayoutSetting; + + // DatabaseViewLayout.Calendar + get(key: '2'): YDatabaseCalendarLayoutSetting; +} + +export interface YDatabaseBoardLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; +} + +export interface YDatabaseCalendarLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string; + + get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean; +} + +export interface YDatabaseGroup extends Y.Map { + get(key: YjsDatabaseKey.id): GroupId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.content): string; + + get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns; +} + +export type YDatabaseGroupColumns = Y.Array; + +export interface YDatabaseGroupColumn extends Y.Map { + get(key: YjsDatabaseKey.id): string; + + get(key: YjsDatabaseKey.visible): boolean; +} + +export interface YDatabaseRowOrder extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.height): number; +} + +export interface YDatabaseSort extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.condition): string; +} + +export type FilterId = string; + +export interface YDatabaseFilter extends Y.Map { + get(key: YjsDatabaseKey.id): FilterId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; +} + +export interface YDatabaseCalculation extends Y.Map { + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; +} + +export interface YDatabaseFieldSettings extends Y.Map { + get(key: FieldId): YDatabaseFieldSetting; +} + +export interface YDatabaseFieldSetting extends Y.Map { + get(key: YjsDatabaseKey.visibility): string; + + get(key: YjsDatabaseKey.wrap): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.width): string; +} + +export interface YDatabaseMetas extends Y.Map { + get(key: YjsDatabaseKey.iid): string; +} + +export interface YDatabaseFields extends Y.Map { + get(key: FieldId): YDatabaseField; +} + +export interface YDatabaseField extends Y.Map { + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.type): string; + + get(key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; + + get(key: YjsDatabaseKey.is_primary): boolean; + + get(key: YjsDatabaseKey.last_modified): LastModified; +} + +export interface YDatabaseFieldTypeOption extends Y.Map { + // key is the field type + get(key: string): YMapFieldTypeOption; +} + +export interface YMapFieldTypeOption extends Y.Map { + get(key: YjsDatabaseKey.content): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.data): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.time_format): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.date_format): string; + + get(key: YjsDatabaseKey.database_id): DatabaseId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.format): string; +} + +export enum CollabType { + Document = 0, + Database = 1, + WorkspaceDatabase = 2, + Folder = 3, + DatabaseRow = 4, + UserAwareness = 5, + Empty = 6, +} + +export enum CollabOrigin { + // from local changes and never sync to remote. used for read-only mode + Local = 'local', + // from remote changes and never sync to remote. + Remote = 'remote', + // from local changes and sync to remote. used for collaborative mode + LocalSync = 'local_sync', +} + +export const layoutMap = { + [ViewLayout.Document]: 'document', + [ViewLayout.Grid]: 'grid', + [ViewLayout.Board]: 'board', + [ViewLayout.Calendar]: 'calendar', +}; + +export const databaseLayoutMap = { + [DatabaseViewLayout.Grid]: 'grid', + [DatabaseViewLayout.Board]: 'board', + [DatabaseViewLayout.Calendar]: 'calendar', +}; + +export enum FontLayout { + small = 'small', + normal = 'normal', + large = 'large', +} + +export enum LineHeightLayout { + small = 'small', + normal = 'normal', + large = 'large', +} diff --git a/frontend/appflowy_web_app/src/application/constants.ts b/frontend/appflowy_web_app/src/application/constants.ts new file mode 100644 index 0000000000..36e31606ff --- /dev/null +++ b/frontend/appflowy_web_app/src/application/constants.ts @@ -0,0 +1,2 @@ +export const databasePrefix = 'af_database'; + diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/filter.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/filter.test.ts new file mode 100644 index 0000000000..979105a982 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/filter.test.ts @@ -0,0 +1,671 @@ +import { + NumberFilterCondition, + TextFilterCondition, + CheckboxFilterCondition, + ChecklistFilterCondition, + SelectOptionFilterCondition, + Row, +} from '@/application/database-yjs'; +import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData'; +import { + withCheckboxFilter, + withChecklistFilter, + withDateTimeFilter, + withMultiSelectOptionFilter, + withNumberFilter, + withRichTextFilter, + withSingleSelectOptionFilter, + withUrlFilter, +} from '@/application/database-yjs/__tests__/withTestingFilters'; +import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows'; +import { + textFilterCheck, + numberFilterCheck, + checkboxFilterCheck, + checklistFilterCheck, + selectOptionFilterCheck, + filterBy, +} from '../filter'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +describe('Text filter check', () => { + const text = 'Hello, world!'; + it('should return true for TextIs condition', () => { + const condition = TextFilterCondition.TextIs; + const content = 'Hello, world!'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for TextIs condition', () => { + const condition = TextFilterCondition.TextIs; + const content = 'Hello, world'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for TextIsNot condition', () => { + const condition = TextFilterCondition.TextIsNot; + const content = 'Hello, world'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for TextIsNot condition', () => { + const condition = TextFilterCondition.TextIsNot; + const content = 'Hello, world!'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for TextContains condition', () => { + const condition = TextFilterCondition.TextContains; + const content = 'world'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for TextContains condition', () => { + const condition = TextFilterCondition.TextContains; + const content = 'planet'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for TextDoesNotContain condition', () => { + const condition = TextFilterCondition.TextDoesNotContain; + const content = 'planet'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for TextDoesNotContain condition', () => { + const condition = TextFilterCondition.TextDoesNotContain; + const content = 'world'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for TextIsEmpty condition', () => { + const condition = TextFilterCondition.TextIsEmpty; + const text = ''; + + const result = textFilterCheck(text, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for TextIsEmpty condition', () => { + const condition = TextFilterCondition.TextIsEmpty; + const text = 'Hello, world!'; + + const result = textFilterCheck(text, '', condition); + + expect(result).toBe(false); + }); + + it('should return true for TextIsNotEmpty condition', () => { + const condition = TextFilterCondition.TextIsNotEmpty; + const text = 'Hello, world!'; + + const result = textFilterCheck(text, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for TextIsNotEmpty condition', () => { + const condition = TextFilterCondition.TextIsNotEmpty; + const text = ''; + + const result = textFilterCheck(text, '', condition); + + expect(result).toBe(false); + }); + + it('should return false for unknown condition', () => { + const condition = 42; + const content = 'Hello, world!'; + + const result = textFilterCheck(text, content, condition); + + expect(result).toBe(false); + }); +}); + +describe('Number filter check', () => { + const num = '42'; + it('should return true for Equal condition', () => { + const condition = NumberFilterCondition.Equal; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for Equal condition', () => { + const condition = NumberFilterCondition.Equal; + const content = '43'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for NotEqual condition', () => { + const condition = NumberFilterCondition.NotEqual; + const content = '43'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for NotEqual condition', () => { + const condition = NumberFilterCondition.NotEqual; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for GreaterThan condition', () => { + const condition = NumberFilterCondition.GreaterThan; + const content = '41'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for GreaterThan condition', () => { + const condition = NumberFilterCondition.GreaterThan; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for GreaterThanOrEqualTo condition', () => { + const condition = NumberFilterCondition.GreaterThanOrEqualTo; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for GreaterThanOrEqualTo condition', () => { + const condition = NumberFilterCondition.GreaterThanOrEqualTo; + const content = '43'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for LessThan condition', () => { + const condition = NumberFilterCondition.LessThan; + const content = '43'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for LessThan condition', () => { + const condition = NumberFilterCondition.LessThan; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for LessThanOrEqualTo condition', () => { + const condition = NumberFilterCondition.LessThanOrEqualTo; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for LessThanOrEqualTo condition', () => { + const condition = NumberFilterCondition.LessThanOrEqualTo; + const content = '41'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for NumberIsEmpty condition', () => { + const condition = NumberFilterCondition.NumberIsEmpty; + + const result = numberFilterCheck('', '', condition); + + expect(result).toBe(true); + }); + + it('should return false for NumberIsEmpty condition', () => { + const condition = NumberFilterCondition.NumberIsEmpty; + const num = '42'; + + const result = numberFilterCheck(num, '', condition); + + expect(result).toBe(false); + }); + + it('should return true for NumberIsNotEmpty condition', () => { + const condition = NumberFilterCondition.NumberIsNotEmpty; + const num = '42'; + + const result = numberFilterCheck(num, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for NumberIsNotEmpty condition', () => { + const condition = NumberFilterCondition.NumberIsNotEmpty; + const num = ''; + + const result = numberFilterCheck(num, '', condition); + + expect(result).toBe(false); + }); + + it('should return false for unknown condition', () => { + const condition = 42; + const content = '42'; + + const result = numberFilterCheck(num, content, condition); + + expect(result).toBe(false); + }); +}); + +describe('Checkbox filter check', () => { + it('should return true for IsChecked condition', () => { + const condition = CheckboxFilterCondition.IsChecked; + const data = 'Yes'; + + const result = checkboxFilterCheck(data, condition); + + expect(result).toBe(true); + }); + + it('should return false for IsChecked condition', () => { + const condition = CheckboxFilterCondition.IsChecked; + const data = 'No'; + + const result = checkboxFilterCheck(data, condition); + + expect(result).toBe(false); + }); + + it('should return true for IsUnChecked condition', () => { + const condition = CheckboxFilterCondition.IsUnChecked; + const data = 'No'; + + const result = checkboxFilterCheck(data, condition); + + expect(result).toBe(true); + }); + + it('should return false for IsUnChecked condition', () => { + const condition = CheckboxFilterCondition.IsUnChecked; + const data = 'Yes'; + + const result = checkboxFilterCheck(data, condition); + + expect(result).toBe(false); + }); + + it('should return false for unknown condition', () => { + const condition = 42; + const data = 'Yes'; + + const result = checkboxFilterCheck(data, condition); + + expect(result).toBe(false); + }); +}); + +describe('Checklist filter check', () => { + it('should return true for IsComplete condition', () => { + const condition = ChecklistFilterCondition.IsComplete; + const data = JSON.stringify({ + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' }, + ], + selected_option_ids: ['1', '2'], + }); + + const result = checklistFilterCheck(data, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for IsComplete condition', () => { + const condition = ChecklistFilterCondition.IsComplete; + const data = JSON.stringify({ + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' }, + ], + selected_option_ids: ['1'], + }); + + const result = checklistFilterCheck(data, '', condition); + + expect(result).toBe(false); + }); + + it('should return false for unknown condition', () => { + const condition = 42; + const data = JSON.stringify({ + options: [ + { id: '1', name: 'Option 1' }, + { id: '2', name: 'Option 2' }, + ], + selected_option_ids: ['1', '2'], + }); + + const result = checklistFilterCheck(data, '', condition); + + expect(result).toBe(false); + }); +}); + +describe('SelectOption filter check', () => { + it('should return true for OptionIs condition', () => { + const condition = SelectOptionFilterCondition.OptionIs; + const content = '1'; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionIs condition', () => { + const condition = SelectOptionFilterCondition.OptionIs; + const content = '3'; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for OptionIsNot condition', () => { + const condition = SelectOptionFilterCondition.OptionIsNot; + const content = '3'; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionIsNot condition', () => { + const condition = SelectOptionFilterCondition.OptionIsNot; + const content = '1'; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for OptionContains condition', () => { + const condition = SelectOptionFilterCondition.OptionContains; + const content = '1,3'; + const data = '1,2,3'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionContains condition', () => { + const condition = SelectOptionFilterCondition.OptionContains; + const content = '4'; + const data = '1,2,3'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for OptionDoesNotContain condition', () => { + const condition = SelectOptionFilterCondition.OptionDoesNotContain; + const content = '4,5'; + const data = '1,2,3'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionDoesNotContain condition', () => { + const condition = SelectOptionFilterCondition.OptionDoesNotContain; + const content = '1,3'; + const data = '1,2,3'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(false); + }); + + it('should return true for OptionIsEmpty condition', () => { + const condition = SelectOptionFilterCondition.OptionIsEmpty; + const data = ''; + + const result = selectOptionFilterCheck(data, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionIsEmpty condition', () => { + const condition = SelectOptionFilterCondition.OptionIsEmpty; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, '', condition); + + expect(result).toBe(false); + }); + + it('should return true for OptionIsNotEmpty condition', () => { + const condition = SelectOptionFilterCondition.OptionIsNotEmpty; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, '', condition); + + expect(result).toBe(true); + }); + + it('should return false for OptionIsNotEmpty condition', () => { + const condition = SelectOptionFilterCondition.OptionIsNotEmpty; + const data = ''; + + const result = selectOptionFilterCheck(data, '', condition); + + expect(result).toBe(false); + }); + + it('should return false for unknown condition', () => { + const condition = 42; + const content = '1'; + const data = '1,2'; + + const result = selectOptionFilterCheck(data, content, condition); + + expect(result).toBe(false); + }); +}); + +describe('Database filterBy', () => { + let rows: Row[]; + + beforeEach(() => { + rows = withTestingRows(); + }); + + it('should return all rows for empty filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should return all rows for empty rowMap', () => { + const { filters, fields } = withTestingData(); + const rowMap = new Y.Map() as Y.Map; + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should return rows that match text filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withRichTextFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,5'); + }); + + it('should return rows that match number filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withNumberFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('4,5,6,7,8,9,10'); + }); + + it('should return rows that match checkbox filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withCheckboxFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('2,4,6,8,10'); + }); + + it('should return rows that match checklist filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withChecklistFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,2,4,5,6,7,8,10'); + }); + + it('should return rows that match multiple filters', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter1 = withRichTextFilter(); + const filter2 = withNumberFilter(); + filters.push([filter1, filter2]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('5'); + }); + + it('should return rows that match url filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withUrlFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('4'); + }); + + it('should return rows that match date filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withDateTimeFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should return rows that match select option filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withSingleSelectOptionFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('2,5,8'); + }); + + it('should return rows that match multi select option filter', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter = withMultiSelectOptionFilter(); + filters.push([filter]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('1,2,3,5,6,7,8,9'); + }); + + it('should return rows that match multiple filters', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter1 = withNumberFilter(); + const filter2 = withChecklistFilter(); + filters.push([filter1, filter2]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe('4,5,6,7,8,10'); + }); + + it('should return empty array for all filters', () => { + const { filters, fields, rowMap } = withTestingData(); + const filter1 = withNumberFilter(); + const filter2 = withChecklistFilter(); + const filter3 = withRichTextFilter(); + const filter4 = withCheckboxFilter(); + const filter5 = withSingleSelectOptionFilter(); + const filter6 = withMultiSelectOptionFilter(); + const filter7 = withUrlFilter(); + const filter8 = withDateTimeFilter(); + filters.push([filter1, filter2, filter3, filter4, filter5, filter6, filter7, filter8]); + const result = filterBy(rows, filters, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(result).toBe(''); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/filters.json b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/filters.json new file mode 100644 index 0000000000..eb0688a5de --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/filters.json @@ -0,0 +1,40 @@ +{ + "filter_text_field": { + "field_id": "text_field", + "condition": 2, + "content": "w" + }, + "filter_number_field": { + "field_id": "number_field", + "condition": 2, + "content": 1000 + }, + "filter_date_field": { + "field_id": "date_field", + "condition": 1, + "content": 1685798400000 + }, + "filter_checkbox_field": { + "field_id": "checkbox_field", + "condition": 1 + }, + "filter_checklist_field": { + "field_id": "checklist_field", + "condition": 1 + }, + "filter_url_field": { + "field_id": "url_field", + "condition": 0, + "content": "https://example.com/4" + }, + "filter_single_select_field": { + "field_id": "single_select_field", + "condition": 0, + "content": "2" + }, + "filter_multi_select_field": { + "field_id": "multi_select_field", + "condition": 2, + "content": "1,3" + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/rows.json b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/rows.json new file mode 100644 index 0000000000..989a335527 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/rows.json @@ -0,0 +1,412 @@ +[ + { + "id": "1", + "cells": { + "text_field": { + "id": "text_field", + "data": "Hello world" + }, + "number_field": { + "id": "number_field", + "data": 123 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "Yes" + }, + "date_field": { + "id": "date_field", + "data": 1685539200000, + "end_timestamp": 1685625600000, + "include_time": true, + "is_range": false, + "reminder_id": "rem1" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/1" + }, + "single_select_field": { + "id": "single_select_field", + "data": "1" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,2" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}" + } + } + }, + { + "id": "2", + "cells": { + "text_field": { + "id": "text_field", + "data": "Good morning" + }, + "number_field": { + "id": "number_field", + "data": 456 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "No" + }, + "date_field": { + "id": "date_field", + "data": 1685625600000, + "end_timestamp": 1685712000000, + "include_time": false, + "is_range": true, + "reminder_id": "rem2" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/2" + }, + "single_select_field": { + "id": "single_select_field", + "data": "2" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "2,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}" + } + } + }, + { + "id": "3", + "cells": { + "text_field": { + "id": "text_field", + "data": "Good night" + }, + "number_field": { + "id": "number_field", + "data": 789 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "Yes" + }, + "date_field": { + "id": "date_field", + "data": 1685712000000, + "end_timestamp": 1685798400000, + "include_time": true, + "is_range": false, + "reminder_id": "rem3" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/3" + }, + "single_select_field": { + "id": "single_select_field", + "data": "3" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\",\"2\"]}" + } + } + }, + { + "id": "4", + "cells": { + "text_field": { + "id": "text_field", + "data": "Happy day" + }, + "number_field": { + "id": "number_field", + "data": 1011 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "No" + }, + "date_field": { + "id": "date_field", + "data": 1685798400000, + "end_timestamp": 1685884800000, + "include_time": false, + "is_range": true, + "reminder_id": "rem4" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/4" + }, + "single_select_field": { + "id": "single_select_field", + "data": "1" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "2" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[]}" + } + } + }, + { + "id": "5", + "cells": { + "text_field": { + "id": "text_field", + "data": "Sunny weather" + }, + "number_field": { + "id": "number_field", + "data": 1213 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "Yes" + }, + "date_field": { + "id": "date_field", + "data": 1685884800000, + "end_timestamp": 1685971200000, + "include_time": true, + "is_range": false, + "reminder_id": "rem5" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/5" + }, + "single_select_field": { + "id": "single_select_field", + "data": "2" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,2,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}" + } + } + }, + { + "id": "6", + "cells": { + "text_field": { + "id": "text_field", + "data": "Rainy day" + }, + "number_field": { + "id": "number_field", + "data": 1415 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "No" + }, + "date_field": { + "id": "date_field", + "data": 1685971200000, + "end_timestamp": 1686057600000, + "include_time": false, + "is_range": true, + "reminder_id": "rem6" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/6" + }, + "single_select_field": { + "id": "single_select_field", + "data": "3" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}" + } + } + }, + { + "id": "7", + "cells": { + "text_field": { + "id": "text_field", + "data": "Winter is coming" + }, + "number_field": { + "id": "number_field", + "data": 1617 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "Yes" + }, + "date_field": { + "id": "date_field", + "data": 1686057600000, + "end_timestamp": 1686144000000, + "include_time": true, + "is_range": false, + "reminder_id": "rem7" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/7" + }, + "single_select_field": { + "id": "single_select_field", + "data": "1" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,2" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\"]}" + } + } + }, + { + "id": "8", + "cells": { + "text_field": { + "id": "text_field", + "data": "Summer vibes" + }, + "number_field": { + "id": "number_field", + "data": 1819 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "No" + }, + "date_field": { + "id": "date_field", + "data": 1686144000000, + "end_timestamp": 1686230400000, + "include_time": false, + "is_range": true, + "reminder_id": "rem8" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/8" + }, + "single_select_field": { + "id": "single_select_field", + "data": "2" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "2,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"2\"]}" + } + } + }, + { + "id": "9", + "cells": { + "text_field": { + "id": "text_field", + "data": "Autumn leaves" + }, + "number_field": { + "id": "number_field", + "data": 2021 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "Yes" + }, + "date_field": { + "id": "date_field", + "data": 1686230400000, + "end_timestamp": 1686316800000, + "include_time": true, + "is_range": false, + "reminder_id": "rem9" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/9" + }, + "single_select_field": { + "id": "single_select_field", + "data": "3" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "1,3" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[\"1\",\"2\"]}" + } + } + }, + { + "id": "10", + "cells": { + "text_field": { + "id": "text_field", + "data": "Spring blossoms" + }, + "number_field": { + "id": "number_field", + "data": 2223 + }, + "checkbox_field": { + "id": "checkbox_field", + "data": "No" + }, + "date_field": { + "id": "date_field", + "data": 1686316800000, + "end_timestamp": 1686403200000, + "include_time": false, + "is_range": true, + "reminder_id": "rem10" + }, + "url_field": { + "id": "url_field", + "data": "https://example.com/10" + }, + "single_select_field": { + "id": "single_select_field", + "data": "1" + }, + "multi_select_field": { + "id": "multi_select_field", + "data": "2" + }, + "checklist_field": { + "id": "checklist_field", + "data": "{\"options\":[{\"id\":\"1\",\"name\":\"Task 1\"},{\"id\":\"2\",\"name\":\"Task 2\"}],\"selected_option_ids\":[]}" + } + } + } +] diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/sorts.json b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/sorts.json new file mode 100644 index 0000000000..11ae36cf60 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/fixtures/sorts.json @@ -0,0 +1,102 @@ +{ + "sort_asc_text_field": { + "id": "sort_asc_text_field", + "field_id": "text_field", + "condition": "asc" + }, + "sort_desc_text_field": { + "field_id": "text_field", + "condition": "desc", + "id": "sort_desc_text_field" + }, + "sort_asc_number_field": { + "field_id": "number_field", + "condition": "asc", + "id": "sort_asc_number_field" + }, + "sort_desc_number_field": { + "field_id": "number_field", + "condition": "desc", + "id": "sort_desc_number_field" + }, + "sort_asc_date_field": { + "field_id": "date_field", + "condition": "asc", + "id": "sort_asc_date_field" + }, + "sort_desc_date_field": { + "field_id": "date_field", + "condition": "desc", + "id": "sort_desc_date_field" + }, + "sort_asc_checkbox_field": { + "field_id": "checkbox_field", + "condition": "asc", + "id": "sort_asc_checkbox_field" + }, + "sort_desc_checkbox_field": { + "field_id": "checkbox_field", + "condition": "desc", + "id": "sort_desc_checkbox_field" + }, + "sort_asc_checklist_field": { + "field_id": "checklist_field", + "condition": "asc", + "id": "sort_asc_checklist_field" + }, + "sort_desc_checklist_field": { + "field_id": "checklist_field", + "condition": "desc", + "id": "sort_desc_checklist_field" + }, + "sort_asc_single_select_field": { + "field_id": "single_select_field", + "condition": "asc", + "id": "sort_asc_single_select_field" + }, + "sort_desc_single_select_field": { + "field_id": "single_select_field", + "condition": "desc", + "id": "sort_desc_single_select_field" + }, + "sort_asc_multi_select_field": { + "field_id": "multi_select_field", + "condition": "asc", + "id": "sort_asc_multi_select_field" + }, + "sort_desc_multi_select_field": { + "field_id": "multi_select_field", + "condition": "desc", + "id": "sort_desc_multi_select_field" + }, + "sort_asc_url_field": { + "field_id": "url_field", + "condition": "asc", + "id": "sort_asc_url_field" + }, + "sort_desc_url_field": { + "field_id": "url_field", + "condition": "desc", + "id": "sort_desc_url_field" + }, + "sort_asc_created_at": { + "field_id": "created_at_field", + "condition": "asc", + "id": "sort_asc_created_at" + }, + "sort_desc_created_at": { + "field_id": "created_at_field", + "condition": "desc", + "id": "sort_desc_created_at" + }, + "sort_asc_updated_at": { + "field_id": "last_modified_field", + "condition": "asc", + "id": "sort_asc_updated_at" + }, + "sort_desc_updated_at": { + "field_id": "last_modified_field", + "condition": "desc", + "id": "sort_desc_updated_at" + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts new file mode 100644 index 0000000000..adbe80aaa3 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/group.test.ts @@ -0,0 +1,172 @@ +import { FieldType, Row } from '@/application/database-yjs'; +import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData'; +import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows'; +import { expect } from '@jest/globals'; +import { groupByField } from '../group'; +import * as Y from 'yjs'; +import { + YDatabaseField, + YDatabaseFieldTypeOption, + YjsDatabaseKey, + YjsEditorKey, + YMapFieldTypeOption, +} from '@/application/collab.type'; +import { YjsEditor } from '@/application/slate-yjs'; + +describe('Database group', () => { + let rows: Row[]; + + beforeEach(() => { + rows = withTestingRows(); + }); + + it('should return undefined if field is not select option', () => { + const { fields, rowMap } = withTestingData(); + expect(groupByField(rows, rowMap, fields.get('text_field'))).toBeUndefined(); + expect(groupByField(rows, rowMap, fields.get('number_field'))).toBeUndefined(); + expect(groupByField(rows, rowMap, fields.get('checkbox_field'))).toBeUndefined(); + expect(groupByField(rows, rowMap, fields.get('checklist_field'))).toBeUndefined(); + }); + + it('should group by select option field', () => { + const { fields, rowMap } = withTestingData(); + const field = fields.get('single_select_field'); + const result = groupByField(rows, rowMap, field); + const expectRes = new Map([ + [ + '1', + [ + { id: '1', height: 37 }, + { id: '4', height: 37 }, + { id: '7', height: 37 }, + { id: '10', height: 37 }, + ], + ], + [ + '2', + [ + { id: '2', height: 37 }, + { id: '5', height: 37 }, + { id: '8', height: 37 }, + ], + ], + [ + '3', + [ + { id: '3', height: 37 }, + { id: '6', height: 37 }, + { id: '9', height: 37 }, + ], + ], + ]); + expect(result).toEqual(expectRes); + }); + + it('should group by multi select option field', () => { + const { fields, rowMap } = withTestingData(); + const field = fields.get('multi_select_field'); + const result = groupByField(rows, rowMap, field); + const expectRes = new Map([ + [ + '1', + [ + { id: '1', height: 37 }, + { id: '3', height: 37 }, + { id: '5', height: 37 }, + { id: '6', height: 37 }, + { id: '7', height: 37 }, + { id: '9', height: 37 }, + ], + ], + [ + '2', + [ + { id: '1', height: 37 }, + { id: '2', height: 37 }, + { id: '4', height: 37 }, + { id: '5', height: 37 }, + { id: '7', height: 37 }, + { id: '8', height: 37 }, + { id: '10', height: 37 }, + ], + ], + [ + '3', + [ + { id: '2', height: 37 }, + { id: '3', height: 37 }, + { id: '5', height: 37 }, + { id: '6', height: 37 }, + { id: '8', height: 37 }, + { id: '9', height: 37 }, + ], + ], + ]); + expect(result).toEqual(expectRes); + }); + + it('should not group if no options', () => { + const { fields, rowMap } = withTestingData(); + const field = new Y.Map() as YDatabaseField; + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Single Select Field'); + field.set(YjsDatabaseKey.id, 'another_single_select_field'); + field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + field.set(YjsDatabaseKey.type_option, typeOption); + fields.set('another_single_select_field', field); + expect(groupByField(rows, rowMap, field)).toBeUndefined(); + + const selectTypeOption = new Y.Map() as YMapFieldTypeOption; + + typeOption.set(String(FieldType.SingleSelect), selectTypeOption); + selectTypeOption.set(YjsDatabaseKey.content, JSON.stringify({ disable_color: false, options: [] })); + const expectRes = new Map([['another_single_select_field', rows]]); + expect(groupByField(rows, rowMap, field)).toEqual(expectRes); + }); + + it('should handle empty selected ids', () => { + const { fields, rowMap } = withTestingData(); + const cell = rowMap + .get('1') + ?.getMap(YjsEditorKey.data_section) + ?.get(YjsEditorKey.database_row) + ?.get(YjsDatabaseKey.cells) + ?.get('single_select_field'); + cell?.set(YjsDatabaseKey.data, null); + + const field = fields.get('single_select_field'); + const result = groupByField(rows, rowMap, field); + expect(result).toEqual( + new Map([ + ['single_select_field', [{ id: '1', height: 37 }]], + [ + '2', + [ + { id: '2', height: 37 }, + { id: '5', height: 37 }, + { id: '8', height: 37 }, + ], + ], + [ + '3', + [ + { id: '3', height: 37 }, + { id: '6', height: 37 }, + { id: '9', height: 37 }, + ], + ], + [ + '1', + [ + { id: '4', height: 37 }, + { id: '7', height: 37 }, + { id: '10', height: 37 }, + ], + ], + ]) + ); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/parse.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/parse.test.ts new file mode 100644 index 0000000000..190a4846a1 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/parse.test.ts @@ -0,0 +1,72 @@ +import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; +import { expect } from '@jest/globals'; +import { withTestingCheckboxCell, withTestingDateCell } from '@/application/database-yjs/__tests__/withTestingCell'; +import * as Y from 'yjs'; +import { + FieldType, + parseSelectOptionTypeOptions, + parseRelationTypeOption, + parseNumberTypeOptions, +} from '@/application/database-yjs'; +import { YDatabaseField, YDatabaseFieldTypeOption, YjsDatabaseKey } from '@/application/collab.type'; +import { withNumberTestingField, withRelationTestingField } from '@/application/database-yjs/__tests__/withTestingField'; + +describe('parseYDatabaseCellToCell', () => { + it('should parse a DateTime cell', () => { + const doc = new Y.Doc(); + const cell = withTestingDateCell(); + doc.getMap('cells').set('date_field', cell); + const parsedCell = parseYDatabaseCellToCell(cell); + expect(parsedCell.data).not.toBe(undefined); + expect(parsedCell.createdAt).not.toBe(undefined); + expect(parsedCell.lastModified).not.toBe(undefined); + expect(parsedCell.fieldType).toBe(Number(FieldType.DateTime)); + }); + it('should parse a Checkbox cell', () => { + const doc = new Y.Doc(); + const cell = withTestingCheckboxCell(); + doc.getMap('cells').set('checkbox_field', cell); + const parsedCell = parseYDatabaseCellToCell(cell); + expect(parsedCell.data).toBe(true); + expect(parsedCell.createdAt).not.toBe(undefined); + expect(parsedCell.lastModified).not.toBe(undefined); + expect(parsedCell.fieldType).toBe(Number(FieldType.Checkbox)); + }); +}); + +describe('Select option field parse', () => { + it('should parse select option type options', () => { + const doc = new Y.Doc(); + const field = new Y.Map() as YDatabaseField; + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Single Select Field'); + field.set(YjsDatabaseKey.id, 'single_select_field'); + field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + field.set(YjsDatabaseKey.type_option, typeOption); + doc.getMap('fields').set('single_select_field', field); + expect(parseSelectOptionTypeOptions(field)).toEqual(null); + }); +}); + +describe('number field parse', () => { + it('should parse number field', () => { + const doc = new Y.Doc(); + const field = withNumberTestingField(); + doc.getMap('fields').set('number_field', field); + expect(parseNumberTypeOptions(field)).toEqual({ + format: 0, + }); + }); +}); + +describe('relation field parse', () => { + it('should parse relation field', () => { + const doc = new Y.Doc(); + const field = withRelationTestingField(); + doc.getMap('fields').set('relation_field', field); + expect(parseRelationTypeOption(field)).toEqual(undefined); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx new file mode 100644 index 0000000000..23c8bc8221 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/selector.test.tsx @@ -0,0 +1,283 @@ +import { renderHook } from '@testing-library/react'; +import { + useCalendarEventsSelector, + useCellSelector, + useFieldSelector, + useFieldsSelector, + useFilterSelector, + useFiltersSelector, + useGroup, + useGroupsSelector, + usePrimaryFieldId, + useRowDataSelector, + useRowDocMapSelector, + useRowMetaSelector, + useRowOrdersSelector, + useRowsByGroup, + useSortSelector, + useSortsSelector, +} from '../selector'; +import { useDatabaseViewId } from '../context'; +import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { withTestingDatabase } from '@/application/database-yjs/__tests__/withTestingData'; +import { expect } from '@jest/globals'; +import { YDoc, YjsDatabaseKey, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; +import * as Y from 'yjs'; +import { withNumberTestingField, withTestingFields } from '@/application/database-yjs/__tests__/withTestingField'; +import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows'; + +const wrapperCreator = + (viewId: string, doc: YDoc, rowDocMap: Y.Map) => + ({ children }: { children: React.ReactNode }) => { + return ( + + + {children} + + + ); + }; + +describe('Database selector', () => { + let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; + let rowDocMap: Y.Map; + let doc: YDoc; + + beforeEach(() => { + const data = withTestingDatabase('1'); + + doc = data.doc; + rowDocMap = data.rowDocMap; + wrapper = wrapperCreator('1', doc, rowDocMap); + }); + + it('should select a field', () => { + const { result } = renderHook(() => useFieldSelector('number_field'), { wrapper }); + + const tempDoc = new Y.Doc(); + const field = withNumberTestingField(); + + tempDoc.getMap().set('number_field', field); + + expect(result.current.field?.toJSON()).toEqual(field.toJSON()); + }); + + it('should select all fields', () => { + const { result } = renderHook(() => useFieldsSelector(), { wrapper }); + + expect(result.current.map((item) => item.fieldId)).toEqual(Array.from(withTestingFields().keys())); + }); + + it('should select all filters', () => { + const { result } = renderHook(() => useFiltersSelector(), { wrapper }); + + expect(result.current).toEqual(['filter_multi_select_field']); + }); + + it('should select a filter', () => { + const { result } = renderHook(() => useFilterSelector('filter_multi_select_field'), { wrapper }); + + expect(result.current).toEqual({ + content: '1,3', + condition: 2, + fieldId: 'multi_select_field', + id: 'filter_multi_select_field', + filterType: NaN, + optionIds: ['1', '3'], + }); + }); + + it('should select all sorts', () => { + const { result } = renderHook(() => useSortsSelector(), { wrapper }); + + expect(result.current).toEqual(['sort_asc_text_field']); + }); + + it('should select a sort', () => { + const { result } = renderHook(() => useSortSelector('sort_asc_text_field'), { wrapper }); + + expect(result.current).toEqual({ + fieldId: 'text_field', + id: 'sort_asc_text_field', + condition: 0, + }); + }); + + it('should select all groups', () => { + const { result } = renderHook(() => useGroupsSelector(), { wrapper }); + + expect(result.current).toEqual(['g:single_select_field']); + }); + + it('should select a group', () => { + const { result } = renderHook(() => useGroup('g:single_select_field'), { wrapper }); + + expect(result.current).toEqual({ + fieldId: 'single_select_field', + columns: [ + { + id: '1', + visible: true, + }, + { + id: 'single_select_field', + visible: true, + }, + ], + }); + }); + + it('should select rows by group', () => { + const { result } = renderHook(() => useRowsByGroup('g:single_select_field'), { wrapper }); + + const { fieldId, columns, notFound, groupResult } = result.current; + + expect(fieldId).toEqual('single_select_field'); + expect(columns).toEqual([ + { + id: '1', + visible: true, + }, + { + id: 'single_select_field', + visible: true, + }, + ]); + expect(notFound).toBeFalsy(); + + expect(groupResult).toEqual( + new Map([ + [ + '1', + [ + { id: '1', height: 37 }, + { id: '7', height: 37 }, + ], + ], + [ + '2', + [ + { id: '2', height: 37 }, + { id: '8', height: 37 }, + { id: '5', height: 37 }, + ], + ], + [ + '3', + [ + { id: '9', height: 37 }, + { id: '3', height: 37 }, + { id: '6', height: 37 }, + ], + ], + ]) + ); + }); + + it('should select all row orders', () => { + const { result } = renderHook(() => useRowOrdersSelector(), { wrapper }); + + expect(result.current?.map((item) => item.id).join(',')).toEqual('9,2,3,1,6,8,5,7'); + }); + + it('should select all row doc map', () => { + const { result } = renderHook(() => useRowDocMapSelector(), { wrapper }); + + expect(result.current.rows).toEqual(rowDocMap); + }); + + it('should select a row data', () => { + const rows = withTestingRows(); + const { result } = renderHook(() => useRowDataSelector(rows[0].id), { wrapper }); + + expect(result.current.row.toJSON()).toEqual( + rowDocMap.get(rows[0].id)?.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database_row)?.toJSON() + ); + }); + + it('should select a cell', () => { + const rows = withTestingRows(); + const { result } = renderHook( + () => + useCellSelector({ + rowId: rows[0].id, + fieldId: 'number_field', + }), + { wrapper } + ); + + expect(result.current).toEqual({ + createdAt: NaN, + data: 123, + fieldType: 1, + lastModified: NaN, + }); + }); + + it('should select a primary field id', () => { + const { result } = renderHook(() => usePrimaryFieldId(), { wrapper }); + + expect(result.current).toEqual('text_field'); + }); + + it('should select a row meta', () => { + const rows = withTestingRows(); + const { result } = renderHook(() => useRowMetaSelector(rows[0].id), { wrapper }); + + expect(result.current?.documentId).not.toBeNull(); + }); + + it('should select all calendar events', () => { + const { result } = renderHook(() => useCalendarEventsSelector(), { wrapper }); + + expect(result.current.events.length).toEqual(8); + expect(result.current.emptyEvents.length).toEqual(0); + }); + + it('should select view id', () => { + const { result } = renderHook(() => useDatabaseViewId(), { wrapper }); + + expect(result.current).toEqual('1'); + }); + + it('should select all rows if filter is not found', () => { + const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.views) + .get('1'); + + view.set(YjsDatabaseKey.filters, new Y.Array()); + + const { result } = renderHook(() => useRowOrdersSelector(), { wrapper }); + + expect(result.current?.map((item) => item.id).join(',')).toEqual('9,2,3,4,1,6,10,8,5,7'); + }); + + it('should select original row orders if sorts is not found', () => { + const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.views) + .get('1'); + + view.set(YjsDatabaseKey.sorts, new Y.Array()); + + const { result } = renderHook(() => useRowOrdersSelector(), { wrapper }); + + expect(result.current?.map((item) => item.id).join(',')).toEqual('1,2,3,5,6,7,8,9'); + }); + + it('should select all rows if filters and sorts are not found', () => { + const view = (doc.get(YjsEditorKey.data_section) as YSharedRoot) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.views) + .get('1'); + + view.set(YjsDatabaseKey.filters, new Y.Array()); + view.set(YjsDatabaseKey.sorts, new Y.Array()); + + const { result } = renderHook(() => useRowOrdersSelector(), { wrapper }); + + expect(result.current?.map((item) => item.id).join(',')).toEqual('1,2,3,4,5,6,7,8,9,10'); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/sort.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/sort.test.ts new file mode 100644 index 0000000000..e790b05fdd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/sort.test.ts @@ -0,0 +1,397 @@ +import { Row } from '@/application/database-yjs'; +import { withTestingData } from '@/application/database-yjs/__tests__/withTestingData'; +import { withTestingRows } from '@/application/database-yjs/__tests__/withTestingRows'; +import { + withCheckboxSort, + withChecklistSort, + withCreatedAtSort, + withDateTimeSort, + withLastModifiedSort, + withMultiSelectOptionSort, + withNumberSort, + withRichTextSort, + withSingleSelectOptionSort, + withUrlSort, +} from '@/application/database-yjs/__tests__/withTestingSorts'; +import { + withCheckboxTestingField, + withDateTimeTestingField, + withNumberTestingField, + withRichTextTestingField, + withSelectOptionTestingField, + withURLTestingField, + withChecklistTestingField, + withRelationTestingField, +} from './withTestingField'; +import { sortBy, parseCellDataForSort } from '../sort'; +import * as Y from 'yjs'; +import { expect } from '@jest/globals'; +import { YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; + +describe('parseCellDataForSort', () => { + it('should parse data correctly based on field type', () => { + const doc = new Y.Doc(); + const field = withNumberTestingField(); + doc.getMap().set('field', field); + const data = 42; + + const result = parseCellDataForSort(field, data); + + expect(result).toEqual(data); + }); + + it('should return default value for empty rich text', () => { + const doc = new Y.Doc(); + const field = withRichTextTestingField(); + doc.getMap().set('field', field); + const data = ''; + + const result = parseCellDataForSort(field, data); + + expect(result).toEqual('\uFFFF'); + }); + + it('should return default value for empty URL', () => { + const doc = new Y.Doc(); + const field = withURLTestingField(); + doc.getMap().set('field', field); + const data = ''; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe('\uFFFF'); + }); + + it('should return data for non-empty rich text', () => { + const doc = new Y.Doc(); + const field = withRichTextTestingField(); + doc.getMap().set('field', field); + const data = 'Hello, world!'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe(data); + }); + + it('should parse checkbox data correctly', () => { + const doc = new Y.Doc(); + const field = withCheckboxTestingField(); + doc.getMap().set('field', field); + const data = 'Yes'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe(true); + + const noData = 'No'; + const noResult = parseCellDataForSort(field, noData); + expect(noResult).toBe(false); + }); + + it('should parse DateTime data correctly', () => { + const doc = new Y.Doc(); + const field = withDateTimeTestingField(); + doc.getMap().set('field', field); + const data = '1633046400000'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe(Number(data)); + }); + + it('should parse SingleSelect data correctly', () => { + const doc = new Y.Doc(); + const field = withSelectOptionTestingField(); + doc.getMap().set('field', field); + const data = '1'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe('Option 1'); + }); + + it('should parse MultiSelect data correctly', () => { + const doc = new Y.Doc(); + const field = withSelectOptionTestingField(); + doc.getMap().set('field', field); + const data = '1,2'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe('Option 1, Option 2'); + }); + + it('should parse Checklist data correctly', () => { + const doc = new Y.Doc(); + const field = withChecklistTestingField(); + doc.getMap().set('field', field); + const data = '[]'; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe(0); + }); + + it('should return empty string for Relation field', () => { + const doc = new Y.Doc(); + const field = withRelationTestingField(); + doc.getMap().set('field', field); + const data = ''; + + const result = parseCellDataForSort(field, data); + + expect(result).toBe(''); + }); +}); + +describe('Database sortBy', () => { + let rows: Row[]; + + beforeEach(() => { + rows = withTestingRows(); + }); + + it('should not sort rows if no sort is provided', () => { + const { sorts, fields, rowMap } = withTestingData(); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should not sort rows if no rows are provided', () => { + const { sorts, fields } = withTestingData(); + const rowMap = new Y.Map() as Y.Map; + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should return default data if rowMeta is not found', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withNumberSort(); + sorts.push([sort]); + rowMap.delete('1'); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should return default data if cell is not found', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withNumberSort(); + sorts.push([sort]); + const rowDoc = rowMap.get('1'); + rowDoc + ?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database_row) + ?.get(YjsDatabaseKey.cells) + .delete('number_field'); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should sort by number field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withNumberSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should sort by number field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withNumberSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('10,9,8,7,6,5,4,3,2,1'); + }); + + it('should sort by rich text field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withRichTextSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('9,2,3,4,1,6,10,8,5,7'); + }); + + it('should sort by rich text field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withRichTextSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('7,5,8,10,6,1,4,3,2,9'); + }); + + it('should sort by url field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withUrlSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,10,2,3,4,5,6,7,8,9'); + }); + + it('should sort by url field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withUrlSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('9,8,7,6,5,4,3,2,10,1'); + }); + + it('should sort by checkbox field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withCheckboxSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('2,4,6,8,10,1,3,5,7,9'); + }); + + it('should sort by checkbox field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withCheckboxSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,3,5,7,9,2,4,6,8,10'); + }); + + it('should sort by DateTime field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withDateTimeSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should sort by DateTime field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withDateTimeSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('10,9,8,7,6,5,4,3,2,1'); + }); + + it('should sort by SingleSelect field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withSingleSelectOptionSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,4,7,10,2,5,8,3,6,9'); + }); + + it('should sort by SingleSelect field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withSingleSelectOptionSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('3,6,9,2,5,8,1,4,7,10'); + }); + + it('should sort by MultiSelect field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withMultiSelectOptionSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,7,5,3,6,9,4,10,2,8'); + }); + + it('should sort by MultiSelect field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withMultiSelectOptionSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('2,8,4,10,3,6,9,5,1,7'); + }); + + it('should sort by Checklist field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withChecklistSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('4,10,1,2,5,6,7,8,3,9'); + }); + + it('should sort by Checklist field in descending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withChecklistSort(false); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('3,9,1,2,5,6,7,8,4,10'); + }); + + it('should sort by CreatedAt field in ascending order', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withCreatedAtSort(); + sorts.push([sort]); + + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); + + it('should sort by LastEditedTime field', () => { + const { sorts, fields, rowMap } = withTestingData(); + const sort = withLastModifiedSort(); + sorts.push([sort]); + const sortedRows = sortBy(rows, sorts, fields, rowMap) + .map((row) => row.id) + .join(','); + expect(sortedRows).toBe('1,2,3,4,5,6,7,8,9,10'); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingCell.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingCell.ts new file mode 100644 index 0000000000..4021903b36 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingCell.ts @@ -0,0 +1,43 @@ +import * as Y from 'yjs'; +import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; + +export function withTestingDateCell() { + const cell = new Y.Map() as YDatabaseCell; + + cell.set(YjsDatabaseKey.id, 'date_field'); + cell.set(YjsDatabaseKey.data, Date.now()); + cell.set(YjsDatabaseKey.field_type, Number(FieldType.DateTime)); + cell.set(YjsDatabaseKey.created_at, Date.now()); + cell.set(YjsDatabaseKey.last_modified, Date.now()); + cell.set(YjsDatabaseKey.end_timestamp, Date.now() + 1000); + cell.set(YjsDatabaseKey.include_time, true); + cell.set(YjsDatabaseKey.is_range, true); + cell.set(YjsDatabaseKey.reminder_id, 'reminderId'); + + return cell; +} + +export function withTestingCheckboxCell() { + const cell = new Y.Map() as YDatabaseCell; + + cell.set(YjsDatabaseKey.id, 'checkbox_field'); + cell.set(YjsDatabaseKey.data, 'Yes'); + cell.set(YjsDatabaseKey.field_type, Number(FieldType.Checkbox)); + cell.set(YjsDatabaseKey.created_at, Date.now()); + cell.set(YjsDatabaseKey.last_modified, Date.now()); + + return cell; +} + +export function withTestingSingleOptionCell() { + const cell = new Y.Map() as YDatabaseCell; + + cell.set(YjsDatabaseKey.id, 'single_select_field'); + cell.set(YjsDatabaseKey.data, 'optionId'); + cell.set(YjsDatabaseKey.field_type, Number(FieldType.SingleSelect)); + cell.set(YjsDatabaseKey.created_at, Date.now()); + cell.set(YjsDatabaseKey.last_modified, Date.now()); + + return cell; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts new file mode 100644 index 0000000000..3ff4a32b12 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingData.ts @@ -0,0 +1,181 @@ +import { + YDatabase, + YDatabaseField, + YDatabaseFields, + YDatabaseFilters, + YDatabaseGroup, + YDatabaseGroupColumn, + YDatabaseGroupColumns, + YDatabaseLayoutSettings, + YDatabaseSorts, + YDatabaseView, + YDatabaseViews, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { withTestingFields } from '@/application/database-yjs/__tests__/withTestingField'; +import { + withTestingRowData, + withTestingRowDataMap, + withTestingRows, +} from '@/application/database-yjs/__tests__/withTestingRows'; +import * as Y from 'yjs'; +import { withMultiSelectOptionFilter } from '@/application/database-yjs/__tests__/withTestingFilters'; +import { withRichTextSort } from '@/application/database-yjs/__tests__/withTestingSorts'; +import { metaIdFromRowId, RowMetaKey } from '@/application/database-yjs'; + +export function withTestingData() { + const doc = new Y.Doc(); + const sharedRoot = doc.getMap(); + const fields = withTestingFields() as YDatabaseFields; + + sharedRoot.set('fields', fields); + + const rowMap = withTestingRowDataMap(); + + sharedRoot.set('rows', rowMap); + + const sorts = new Y.Array() as YDatabaseSorts; + + sharedRoot.set('sorts', sorts); + + const filters = new Y.Array() as YDatabaseFilters; + + sharedRoot.set('filters', filters); + + return { + fields, + rowMap, + sorts, + filters, + doc, + }; +} + +export function withTestingDatabase(viewId: string) { + const doc = new Y.Doc(); + const sharedRoot = doc.getMap(YjsEditorKey.data_section); + const database = new Y.Map() as YDatabase; + + sharedRoot.set(YjsEditorKey.database, database); + + const fields = withTestingFields() as YDatabaseFields; + + database.set(YjsDatabaseKey.fields, fields); + database.set(YjsDatabaseKey.id, viewId); + + const metas = new Y.Map(); + + database.set(YjsDatabaseKey.metas, metas); + metas.set(YjsDatabaseKey.iid, viewId); + + const views = new Y.Map() as YDatabaseViews; + + database.set(YjsDatabaseKey.views, views); + + const view = new Y.Map() as YDatabaseView; + + views.set('1', view); + view.set(YjsDatabaseKey.id, viewId); + view.set(YjsDatabaseKey.layout, 0); + view.set(YjsDatabaseKey.name, 'View 1'); + view.set(YjsDatabaseKey.database_id, viewId); + + const layoutSetting = new Y.Map() as YDatabaseLayoutSettings; + + const calendarSetting = new Y.Map(); + + calendarSetting.set(YjsDatabaseKey.field_id, 'date_field'); + layoutSetting.set('2', calendarSetting); + + view.set(YjsDatabaseKey.layout_settings, layoutSetting); + + const filters = new Y.Array() as YDatabaseFilters; + const filter = withMultiSelectOptionFilter(); + + filters.push([filter]); + + const sorts = new Y.Array() as YDatabaseSorts; + const sort = withRichTextSort(); + + sorts.push([sort]); + + const groups = new Y.Array(); + const group = new Y.Map() as YDatabaseGroup; + + groups.push([group]); + group.set(YjsDatabaseKey.id, 'g:single_select_field'); + group.set(YjsDatabaseKey.field_id, 'single_select_field'); + group.set(YjsDatabaseKey.type, '3'); + group.set(YjsDatabaseKey.content, ''); + + const groupColumns = new Y.Array() as YDatabaseGroupColumns; + + group.set(YjsDatabaseKey.groups, groupColumns); + + const column1 = new Y.Map() as YDatabaseGroupColumn; + const column2 = new Y.Map() as YDatabaseGroupColumn; + + column1.set(YjsDatabaseKey.id, '1'); + column1.set(YjsDatabaseKey.visible, true); + column2.set(YjsDatabaseKey.id, 'single_select_field'); + column2.set(YjsDatabaseKey.visible, true); + + groupColumns.push([column1]); + groupColumns.push([column2]); + + view.set(YjsDatabaseKey.filters, filters); + view.set(YjsDatabaseKey.sorts, sorts); + view.set(YjsDatabaseKey.groups, groups); + + const fieldSettings = new Y.Map(); + const fieldOrder = new Y.Array(); + const rowOrders = new Y.Array(); + + Array.from(fields).forEach(([fieldId, field]) => { + const setting = new Y.Map(); + + if (fieldId === 'text_field') { + (field as YDatabaseField).set(YjsDatabaseKey.is_primary, true); + } + + fieldOrder.push([fieldId]); + fieldSettings.set(fieldId, setting); + setting.set(YjsDatabaseKey.visibility, 0); + }); + const rows = withTestingRows(); + + rows.forEach(({ id, height }) => { + const row = new Y.Map(); + + row.set(YjsDatabaseKey.id, id); + row.set(YjsDatabaseKey.height, height); + rowOrders.push([row]); + }); + + view.set(YjsDatabaseKey.field_settings, fieldSettings); + view.set(YjsDatabaseKey.field_orders, fieldOrder); + view.set(YjsDatabaseKey.row_orders, rowOrders); + + const rowMapDoc = new Y.Doc(); + + const rowMapFolder = rowMapDoc.getMap(); + + rows.forEach((row, index) => { + const rowDoc = new Y.Doc(); + const rowData = withTestingRowData(row.id, index); + const rowMeta = new Y.Map(); + const parser = metaIdFromRowId('281e76fb-712e-59e2-8370-678bf0788355'); + + rowMeta.set(parser(RowMetaKey.IconId), '😊'); + rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.meta, rowMeta); + rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.database_row, rowData); + rowMapFolder.set(row.id, rowDoc); + }); + + return { + rowDocMap: rowMapFolder as Y.Map, + doc: doc as YDoc, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingField.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingField.ts new file mode 100644 index 0000000000..869acfe55e --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingField.ts @@ -0,0 +1,204 @@ +import { + YDatabaseField, + YDatabaseFieldTypeOption, + YjsDatabaseKey, + YMapFieldTypeOption, +} from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; +import { SelectOptionColor } from '@/application/database-yjs/fields/select-option'; +import * as Y from 'yjs'; + +export function withTestingFields() { + const fields = new Y.Map(); + const textField = withRichTextTestingField(); + + fields.set('text_field', textField); + const numberField = withNumberTestingField(); + + fields.set('number_field', numberField); + + const checkboxField = withCheckboxTestingField(); + + fields.set('checkbox_field', checkboxField); + + const dateTimeField = withDateTimeTestingField(); + + fields.set('date_field', dateTimeField); + + const singleSelectField = withSelectOptionTestingField(); + + fields.set('single_select_field', singleSelectField); + const multipleSelectField = withSelectOptionTestingField(true); + + fields.set('multi_select_field', multipleSelectField); + + const urlField = withURLTestingField(); + + fields.set('url_field', urlField); + + const checklistField = withChecklistTestingField(); + + fields.set('checklist_field', checklistField); + + const createdAtField = withCreatedAtTestingField(); + + fields.set('created_at_field', createdAtField); + + const lastModifiedField = withLastModifiedTestingField(); + + fields.set('last_modified_field', lastModifiedField); + + return fields; +} + +export function withRichTextTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Rich Text Field'); + field.set(YjsDatabaseKey.id, 'text_field'); + field.set(YjsDatabaseKey.type, String(FieldType.RichText)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} + +export function withNumberTestingField() { + const field = new Y.Map() as YDatabaseField; + + field.set(YjsDatabaseKey.name, 'Number Field'); + field.set(YjsDatabaseKey.id, 'number_field'); + field.set(YjsDatabaseKey.type, String(FieldType.Number)); + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + + const numberTypeOption = new Y.Map() as YMapFieldTypeOption; + + typeOption.set(String(FieldType.Number), numberTypeOption); + numberTypeOption.set(YjsDatabaseKey.format, '0'); + field.set(YjsDatabaseKey.type_option, typeOption); + + return field; +} + +export function withRelationTestingField() { + const field = new Y.Map() as YDatabaseField; + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Relation Field'); + field.set(YjsDatabaseKey.id, 'relation_field'); + field.set(YjsDatabaseKey.type, String(FieldType.Relation)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + field.set(YjsDatabaseKey.type_option, typeOption); + + return field; +} + +export function withCheckboxTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Checkbox Field'); + field.set(YjsDatabaseKey.id, 'checkbox_field'); + field.set(YjsDatabaseKey.type, String(FieldType.Checkbox)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} + +export function withDateTimeTestingField() { + const field = new Y.Map() as YDatabaseField; + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'DateTime Field'); + field.set(YjsDatabaseKey.id, 'date_field'); + field.set(YjsDatabaseKey.type, String(FieldType.DateTime)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + field.set(YjsDatabaseKey.type_option, typeOption); + + const dateTypeOption = new Y.Map() as YMapFieldTypeOption; + + typeOption.set(String(FieldType.DateTime), dateTypeOption); + + dateTypeOption.set(YjsDatabaseKey.time_format, '0'); + dateTypeOption.set(YjsDatabaseKey.date_format, '0'); + return field; +} + +export function withURLTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'URL Field'); + field.set(YjsDatabaseKey.id, 'url_field'); + field.set(YjsDatabaseKey.type, String(FieldType.URL)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} + +export function withSelectOptionTestingField(isMultiple = false) { + const field = new Y.Map() as YDatabaseField; + const typeOption = new Y.Map() as YDatabaseFieldTypeOption; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Single Select Field'); + field.set(YjsDatabaseKey.id, isMultiple ? 'multi_select_field' : 'single_select_field'); + field.set(YjsDatabaseKey.type, String(FieldType.SingleSelect)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + field.set(YjsDatabaseKey.type_option, typeOption); + + const selectTypeOption = new Y.Map() as YMapFieldTypeOption; + + typeOption.set(String(FieldType.SingleSelect), selectTypeOption); + + selectTypeOption.set( + YjsDatabaseKey.content, + JSON.stringify({ + disable_color: false, + options: [ + { id: '1', name: 'Option 1', color: SelectOptionColor.Purple }, + { id: '2', name: 'Option 2', color: SelectOptionColor.Pink }, + { id: '3', name: 'Option 3', color: SelectOptionColor.LightPink }, + ], + }) + ); + return field; +} + +export function withChecklistTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Checklist Field'); + field.set(YjsDatabaseKey.id, 'checklist_field'); + field.set(YjsDatabaseKey.type, String(FieldType.Checklist)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} + +export function withCreatedAtTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Created At Field'); + field.set(YjsDatabaseKey.id, 'created_at_field'); + field.set(YjsDatabaseKey.type, String(FieldType.CreatedTime)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} + +export function withLastModifiedTestingField() { + const field = new Y.Map() as YDatabaseField; + const now = Date.now().toString(); + + field.set(YjsDatabaseKey.name, 'Last Modified Field'); + field.set(YjsDatabaseKey.id, 'last_modified_field'); + field.set(YjsDatabaseKey.type, String(FieldType.LastEditedTime)); + field.set(YjsDatabaseKey.last_modified, now.valueOf()); + + return field; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingFilters.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingFilters.ts new file mode 100644 index 0000000000..540c298abf --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingFilters.ts @@ -0,0 +1,83 @@ +import { YDatabaseFilter, YjsDatabaseKey } from '@/application/collab.type'; +import * as Y from 'yjs'; +import * as filtersJson from './fixtures/filters.json'; + +export function withRichTextFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_text_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_text_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_text_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_text_field.content); + return filter; +} + +export function withUrlFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_url_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_url_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_url_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_url_field.content); + return filter; +} + +export function withNumberFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_number_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_number_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_number_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_number_field.content); + return filter; +} + +export function withCheckboxFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_checkbox_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_checkbox_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_checkbox_field.condition); + filter.set(YjsDatabaseKey.content, ''); + return filter; +} + +export function withChecklistFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_checklist_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_checklist_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_checklist_field.condition); + filter.set(YjsDatabaseKey.content, ''); + return filter; +} + +export function withSingleSelectOptionFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_single_select_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_single_select_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_single_select_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_single_select_field.content); + return filter; +} + +export function withMultiSelectOptionFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_multi_select_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_multi_select_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_multi_select_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_multi_select_field.content); + return filter; +} + +export function withDateTimeFilter() { + const filter = new Y.Map() as YDatabaseFilter; + + filter.set(YjsDatabaseKey.id, 'filter_date_field'); + filter.set(YjsDatabaseKey.field_id, filtersJson.filter_date_field.field_id); + filter.set(YjsDatabaseKey.condition, filtersJson.filter_date_field.condition); + filter.set(YjsDatabaseKey.content, filtersJson.filter_date_field.content); + return filter; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingRows.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingRows.ts new file mode 100644 index 0000000000..3ed75b409c --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingRows.ts @@ -0,0 +1,96 @@ +import { + YDatabaseCell, + YDatabaseCells, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType, Row } from '@/application/database-yjs'; +import * as Y from 'yjs'; +import * as rowsJson from './fixtures/rows.json'; + +export function withTestingRows(): Row[] { + return rowsJson.map((row) => { + return { + id: row.id, + height: 37, + }; + }); +} + +export function withTestingRowDataMap(): Y.Map { + const folder = new Y.Map(); + const rows = withTestingRows(); + + rows.forEach((row, index) => { + const rowDoc = new Y.Doc(); + const rowData = withTestingRowData(row.id, index); + + rowDoc.getMap(YjsEditorKey.data_section).set(YjsEditorKey.database_row, rowData); + folder.set(row.id, rowDoc); + }); + + return folder as Y.Map; +} + +export function withTestingRowData(id: string, index: number) { + const rowData = new Y.Map() as YDatabaseRow; + + rowData.set(YjsDatabaseKey.id, id); + rowData.set(YjsDatabaseKey.height, 37); + rowData.set(YjsDatabaseKey.last_modified, Date.now() + index * 1000); + rowData.set(YjsDatabaseKey.created_at, Date.now() + index * 1000); + + const cells = new Y.Map() as YDatabaseCells; + + const textFieldCell = withTestingCell(rowsJson[index].cells.text_field.data); + + textFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.RichText)); + cells.set('text_field', textFieldCell); + + const numberFieldCell = withTestingCell(rowsJson[index].cells.number_field.data); + + numberFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Number)); + cells.set('number_field', numberFieldCell); + + const checkboxFieldCell = withTestingCell(rowsJson[index].cells.checkbox_field.data); + + checkboxFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Checkbox)); + cells.set('checkbox_field', checkboxFieldCell); + + const dateTimeFieldCell = withTestingCell(rowsJson[index].cells.date_field.data); + + dateTimeFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.DateTime)); + cells.set('date_field', dateTimeFieldCell); + + const urlFieldCell = withTestingCell(rowsJson[index].cells.url_field.data); + + urlFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.URL)); + cells.set('url_field', urlFieldCell); + + const singleSelectFieldCell = withTestingCell(rowsJson[index].cells.single_select_field.data); + + singleSelectFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.SingleSelect)); + cells.set('single_select_field', singleSelectFieldCell); + + const multiSelectFieldCell = withTestingCell(rowsJson[index].cells.multi_select_field.data); + + multiSelectFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.MultiSelect)); + cells.set('multi_select_field', multiSelectFieldCell); + + const checlistFieldCell = withTestingCell(rowsJson[index].cells.checklist_field.data); + + checlistFieldCell.set(YjsDatabaseKey.field_type, Number(FieldType.Checklist)); + cells.set('checklist_field', checlistFieldCell); + + rowData.set(YjsDatabaseKey.cells, cells); + return rowData; +} + +export function withTestingCell(cellData: string | number) { + const cell = new Y.Map() as YDatabaseCell; + + cell.set(YjsDatabaseKey.data, cellData); + return cell; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingSorts.ts b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingSorts.ts new file mode 100644 index 0000000000..d97c6f4f71 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/__tests__/withTestingSorts.ts @@ -0,0 +1,113 @@ +import { YDatabaseSort, YjsDatabaseKey } from '@/application/collab.type'; +import * as Y from 'yjs'; +import * as sortsJson from './fixtures/sorts.json'; + +export function withRichTextSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_text_field : sortsJson.sort_desc_text_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withUrlSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_url_field : sortsJson.sort_desc_url_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withNumberSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_number_field : sortsJson.sort_desc_number_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withCheckboxSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_checkbox_field : sortsJson.sort_desc_checkbox_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withDateTimeSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_date_field : sortsJson.sort_desc_date_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withSingleSelectOptionSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_single_select_field : sortsJson.sort_desc_single_select_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withMultiSelectOptionSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_multi_select_field : sortsJson.sort_desc_multi_select_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withChecklistSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_checklist_field : sortsJson.sort_desc_checklist_field; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withCreatedAtSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_created_at : sortsJson.sort_desc_created_at; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} + +export function withLastModifiedSort(isAscending: boolean = true) { + const sort = new Y.Map() as YDatabaseSort; + const sortJSON = isAscending ? sortsJson.sort_asc_updated_at : sortsJson.sort_desc_updated_at; + + sort.set(YjsDatabaseKey.id, sortJSON.id); + sort.set(YjsDatabaseKey.field_id, sortJSON.field_id); + sort.set(YjsDatabaseKey.condition, sortJSON.condition === 'asc' ? '0' : '1'); + + return sort; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/cell.parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/cell.parse.ts new file mode 100644 index 0000000000..4124381c06 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/cell.parse.ts @@ -0,0 +1,46 @@ +import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Cell, CheckboxCell, DateTimeCell } from './cell.type'; + +export function parseYDatabaseCommonCellToCell(cell: YDatabaseCell): Cell { + return { + createdAt: Number(cell.get(YjsDatabaseKey.created_at)), + lastModified: Number(cell.get(YjsDatabaseKey.last_modified)), + fieldType: parseInt(cell.get(YjsDatabaseKey.field_type)) as FieldType, + data: cell.get(YjsDatabaseKey.data), + }; +} + +export function parseYDatabaseCellToCell(cell: YDatabaseCell): Cell { + const fieldType = parseInt(cell.get(YjsDatabaseKey.field_type)); + + if (fieldType === FieldType.DateTime) { + return parseYDatabaseDateTimeCellToCell(cell); + } + + if (fieldType === FieldType.Checkbox) { + return parseYDatabaseCheckboxCellToCell(cell); + } + + return parseYDatabaseCommonCellToCell(cell); +} + +export function parseYDatabaseDateTimeCellToCell(cell: YDatabaseCell): DateTimeCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) as string, + fieldType: FieldType.DateTime, + endTimestamp: cell.get(YjsDatabaseKey.end_timestamp), + includeTime: cell.get(YjsDatabaseKey.include_time), + isRange: cell.get(YjsDatabaseKey.is_range), + reminderId: cell.get(YjsDatabaseKey.reminder_id), + }; +} + +export function parseYDatabaseCheckboxCellToCell(cell: YDatabaseCell): CheckboxCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) === 'Yes', + fieldType: FieldType.Checkbox, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/cell.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/cell.type.ts new file mode 100644 index 0000000000..9e4bf77737 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/cell.type.ts @@ -0,0 +1,86 @@ +import { FieldId, RowId } from '@/application/collab.type'; +import { DateFormat, TimeFormat } from '@/application/database-yjs/index'; +import { FieldType } from '@/application/database-yjs/database.type'; +import React from 'react'; +import { YArray } from 'yjs/dist/src/types/YArray'; + +export interface Cell { + createdAt: number; + lastModified: number; + fieldType: FieldType; + data: unknown; +} + +export interface TextCell extends Cell { + fieldType: FieldType.RichText; + data: string; +} + +export interface NumberCell extends Cell { + fieldType: FieldType.Number; + data: string; +} + +export interface CheckboxCell extends Cell { + fieldType: FieldType.Checkbox; + data: boolean; +} + +export interface UrlCell extends Cell { + fieldType: FieldType.URL; + data: string; +} + +export type SelectionId = string; + +export interface SelectOptionCell extends Cell { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectionId; +} + +export interface DataTimeTypeOption { + timeFormat: TimeFormat; + dateFormat: DateFormat; +} + +export interface DateTimeCell extends Cell { + fieldType: FieldType.DateTime; + data: string; + endTimestamp?: string; + includeTime?: boolean; + isRange?: boolean; + reminderId?: string; +} + +export interface DateTimeCellData { + date?: string; + time?: string; + timestamp?: number; + includeTime?: boolean; + endDate?: string; + endTime?: string; + endTimestamp?: number; + isRange?: boolean; +} + +export interface ChecklistCell extends Cell { + fieldType: FieldType.Checklist; + data: string; +} + +export interface RelationCell extends Cell { + fieldType: FieldType.Relation; + data: YArray; +} + +export type RelationCellData = RowId[]; + +export interface CellProps { + cell?: T; + rowId: string; + fieldId: FieldId; + style?: React.CSSProperties; + readOnly?: boolean; + placeholder?: string; + className?: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts new file mode 100644 index 0000000000..436f28ef91 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -0,0 +1,32 @@ +import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { RowMetaKey } from '@/application/database-yjs/database.type'; +import * as Y from 'yjs'; +import { v5 as uuidv5, parse as uuidParse } from 'uuid'; + +export const DEFAULT_ROW_HEIGHT = 36; +export const MIN_COLUMN_WIDTH = 100; + +export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + const rowMeta = rowMetas.get(rowId); + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return meta?.get(YjsDatabaseKey.cells)?.get(fieldId); +}; + +export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data); +}; + +export const metaIdFromRowId = (rowId: string) => { + let namespace: Uint8Array; + + try { + namespace = uuidParse(rowId); + } catch (e) { + namespace = uuidParse(generateUUID()); + } + + return (key: RowMetaKey) => uuidv5(key, namespace).toString(); +}; + +export const generateUUID = () => uuidv5(Date.now().toString(), uuidv5.URL); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts new file mode 100644 index 0000000000..5d51001976 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -0,0 +1,73 @@ +import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { createContext, useContext } from 'react'; +import * as Y from 'yjs'; + +export interface DatabaseContextState { + readOnly: boolean; + databaseDoc: YDoc; + viewId: string; + rowDocMap: Y.Map; + isDatabaseRowPage?: boolean; + navigateToRow?: (rowId: string) => void; +} + +export const DatabaseContext = createContext(null); + +export const useDatabase = () => { + const database = useContext(DatabaseContext) + ?.databaseDoc?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) as YDatabase; + + return database; +}; + +export function useDatabaseViewId() { + return useContext(DatabaseContext)?.viewId; +} + +export const useNavigateToRow = () => { + return useContext(DatabaseContext)?.navigateToRow; +}; + +export const useRowDocMap = () => { + return useContext(DatabaseContext)?.rowDocMap; +}; + +export const useIsDatabaseRowPage = () => { + return useContext(DatabaseContext)?.isDatabaseRowPage; +}; + +export const useRow = (rowId: string) => { + const rows = useRowDocMap(); + + return rows?.get(rowId)?.getMap(YjsEditorKey.data_section); +}; + +export const useRowData = (rowId: string) => { + return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow; +}; + +export const useViewId = () => { + const context = useContext(DatabaseContext); + + return context?.viewId; +}; + +export const useReadOnly = () => { + const context = useContext(DatabaseContext); + + return context?.readOnly; +}; + +export const useDatabaseView = () => { + const database = useDatabase(); + const viewId = useViewId(); + + return viewId ? database?.get(YjsDatabaseKey.views)?.get(viewId) : undefined; +}; + +export function useDatabaseFields() { + const database = useDatabase(); + + return database.get(YjsDatabaseKey.fields); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts new file mode 100644 index 0000000000..c8ac7da5b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -0,0 +1,72 @@ +import { FieldId } from '@/application/collab.type'; + +export enum FieldVisibility { + AlwaysShown = 0, + HideWhenEmpty = 1, + AlwaysHidden = 2, +} + +export enum FieldType { + RichText = 0, + Number = 1, + DateTime = 2, + SingleSelect = 3, + MultiSelect = 4, + Checkbox = 5, + URL = 6, + Checklist = 7, + LastEditedTime = 8, + CreatedTime = 9, + Relation = 10, +} + +export enum CalculationType { + Average = 0, + Max = 1, + Median = 2, + Min = 3, + Sum = 4, + Count = 5, + CountEmpty = 6, + CountNonEmpty = 7, +} + +export enum SortCondition { + Ascending = 0, + Descending = 1, +} + +export enum FilterType { + Data = 0, + And = 1, + Or = 2, +} + +export interface Filter { + fieldId: FieldId; + filterType: FilterType; + condition: number; + id: string; + content: string; +} + +export enum CalendarLayout { + MonthLayout = 0, + WeekLayout = 1, + DayLayout = 2, +} + +export interface CalendarLayoutSetting { + fieldId: string; + firstDayOfWeek: number; + showWeekNumbers: boolean; + showWeekends: boolean; + layout: CalendarLayout; +} + +export enum RowMetaKey { + DocumentId = 'document_id', + IconId = 'icon_id', + CoverId = 'cover_id', + IsDocumentEmpty = 'is_document_empty', +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts new file mode 100644 index 0000000000..b9da4341f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum CheckboxFilterCondition { + IsChecked = 0, + IsUnChecked = 1, +} + +export interface CheckboxFilter extends Filter { + condition: CheckboxFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts new file mode 100644 index 0000000000..9ccd409dc8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts @@ -0,0 +1 @@ +export * from './checkbox.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts new file mode 100644 index 0000000000..2b504ded8a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum ChecklistFilterCondition { + IsComplete = 0, + IsIncomplete = 1, +} + +export interface ChecklistFilter extends Filter { + condition: ChecklistFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts new file mode 100644 index 0000000000..15d37f912b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts @@ -0,0 +1,2 @@ +export * from './checklist.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts new file mode 100644 index 0000000000..c93fee7a38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts @@ -0,0 +1,22 @@ +import { SelectOption } from '../select-option'; + +export interface ChecklistCellData { + selectedOptionIds?: string[]; + options?: SelectOption[]; + percentage: number; +} + +export function parseChecklistData(data: string): ChecklistCellData | null { + try { + const { options, selected_option_ids } = JSON.parse(data); + const percentage = selected_option_ids.length / options.length; + + return { + percentage, + options, + selectedOptionIds: selected_option_ids, + }; + } catch (e) { + return null; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts new file mode 100644 index 0000000000..0db15f21eb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts @@ -0,0 +1,32 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +export enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} + +export enum DateFilterCondition { + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, +} + +export interface DateFilter extends Filter { + condition: DateFilterCondition; + start?: number; + end?: number; + timestamp?: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts new file mode 100644 index 0000000000..106279c949 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts @@ -0,0 +1,2 @@ +export * from './date.type'; +export * from './utils'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.test.ts new file mode 100644 index 0000000000..9d3821ba1c --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.test.ts @@ -0,0 +1,21 @@ +import { getTimeFormat, getDateFormat } from './utils'; +import { expect } from '@jest/globals'; +import { DateFormat, TimeFormat } from '@/application/database-yjs'; + +describe('DateFormat', () => { + it('should return time format', () => { + expect(getTimeFormat(TimeFormat.TwelveHour)).toEqual('h:mm A'); + expect(getTimeFormat(TimeFormat.TwentyFourHour)).toEqual('HH:mm'); + expect(getTimeFormat(56)).toEqual('HH:mm'); + }); + + it('should return date format', () => { + expect(getDateFormat(DateFormat.US)).toEqual('YYYY/MM/DD'); + expect(getDateFormat(DateFormat.ISO)).toEqual('YYYY-MM-DD'); + expect(getDateFormat(DateFormat.Friendly)).toEqual('MMM DD, YYYY'); + expect(getDateFormat(DateFormat.Local)).toEqual('MM/DD/YYYY'); + expect(getDateFormat(DateFormat.DayMonthYear)).toEqual('DD/MM/YYYY'); + + expect(getDateFormat(56)).toEqual('YYYY-MM-DD'); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts new file mode 100644 index 0000000000..985402768b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts @@ -0,0 +1,29 @@ +import { TimeFormat, DateFormat } from '@/application/database-yjs'; + +export function getTimeFormat(timeFormat?: TimeFormat) { + switch (timeFormat) { + case TimeFormat.TwelveHour: + return 'h:mm A'; + case TimeFormat.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormat) { + switch (dateFormat) { + case DateFormat.Friendly: + return 'MMM DD, YYYY'; + case DateFormat.ISO: + return 'YYYY-MM-DD'; + case DateFormat.US: + return 'YYYY/MM/DD'; + case DateFormat.Local: + return 'MM/DD/YYYY'; + case DateFormat.DayMonthYear: + return 'DD/MM/YYYY'; + default: + return 'YYYY-MM-DD'; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts new file mode 100644 index 0000000000..5505f0e4ed --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts @@ -0,0 +1,8 @@ +export * from './type_option'; +export * from './date'; +export * from './number'; +export * from './select-option'; +export * from './text'; +export * from './checkbox'; +export * from './checklist'; +export * from './relation'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts new file mode 100644 index 0000000000..e165752348 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts @@ -0,0 +1,628 @@ +import { currencyFormaterMap } from '../format'; +import { NumberFormat } from '../number.type'; +import { expect } from '@jest/globals'; + +const testCases = [0, 1, 0.5, 0.5666, 1000, 10000, 1000000, 10000000, 1000000.0]; +describe('currencyFormaterMap', () => { + test('should return the correct formatter for Num', () => { + const formater = currencyFormaterMap[NumberFormat.Num]; + const result = ['0', '1', '0.5', '0.5666', '1,000', '10,000', '1,000,000', '10,000,000', '1,000,000']; + testCases.forEach((testCase) => { + expect(formater(testCase)).toBe(result[testCases.indexOf(testCase)]); + }); + }); + + test('should return the correct formatter for Percent', () => { + const formater = currencyFormaterMap[NumberFormat.Percent]; + const result = ['0%', '1%', '0.5%', '0.57%', '1,000%', '10,000%', '1,000,000%', '10,000,000%', '1,000,000%']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for USD', () => { + const formater = currencyFormaterMap[NumberFormat.USD]; + const result = ['$0', '$1', '$0.5', '$0.57', '$1,000', '$10,000', '$1,000,000', '$10,000,000', '$1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for CanadianDollar', () => { + const formater = currencyFormaterMap[NumberFormat.CanadianDollar]; + const result = [ + 'CA$0', + 'CA$1', + 'CA$0.5', + 'CA$0.57', + 'CA$1,000', + 'CA$10,000', + 'CA$1,000,000', + 'CA$10,000,000', + 'CA$1,000,000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for EUR', () => { + const formater = currencyFormaterMap[NumberFormat.EUR]; + + const result = ['€0', '€1', '€0.5', '€0.57', '€1,000', '€10,000', '€1,000,000', '€10,000,000', '€1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Pound', () => { + const formater = currencyFormaterMap[NumberFormat.Pound]; + + const result = ['£0', '£1', '£0.5', '£0.57', '£1,000', '£10,000', '£1,000,000', '£10,000,000', '£1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yen', () => { + const formater = currencyFormaterMap[NumberFormat.Yen]; + + const result = [ + '¥0', + '¥1', + '¥0.5', + '¥0.57', + '¥1,000', + '¥10,000', + '¥1,000,000', + '¥10,000,000', + '¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ruble', () => { + const formater = currencyFormaterMap[NumberFormat.Ruble]; + + const result = [ + '0 RUB', + '1 RUB', + '0,5 RUB', + '0,57 RUB', + '1 000 RUB', + '10 000 RUB', + '1 000 000 RUB', + '10 000 000 RUB', + '1 000 000 RUB', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupee', () => { + const formater = currencyFormaterMap[NumberFormat.Rupee]; + + const result = ['₹0', '₹1', '₹0.5', '₹0.57', '₹1,000', '₹10,000', '₹10,00,000', '₹1,00,00,000', '₹10,00,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Won', () => { + const formater = currencyFormaterMap[NumberFormat.Won]; + + const result = ['₩0', '₩1', '₩0.5', '₩0.57', '₩1,000', '₩10,000', '₩1,000,000', '₩10,000,000', '₩1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yuan', () => { + const formater = currencyFormaterMap[NumberFormat.Yuan]; + + const result = [ + 'CN¥0', + 'CN¥1', + 'CN¥0.5', + 'CN¥0.57', + 'CN¥1,000', + 'CN¥10,000', + 'CN¥1,000,000', + 'CN¥10,000,000', + 'CN¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Real', () => { + const formater = currencyFormaterMap[NumberFormat.Real]; + + const result = [ + 'R$ 0', + 'R$ 1', + 'R$ 0,5', + 'R$ 0,57', + 'R$ 1.000', + 'R$ 10.000', + 'R$ 1.000.000', + 'R$ 10.000.000', + 'R$ 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Lira', () => { + const formater = currencyFormaterMap[NumberFormat.Lira]; + + const result = [ + 'TRY 0', + 'TRY 1', + 'TRY 0,5', + 'TRY 0,57', + 'TRY 1.000', + 'TRY 10.000', + 'TRY 1.000.000', + 'TRY 10.000.000', + 'TRY 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupiah', () => { + const formater = currencyFormaterMap[NumberFormat.Rupiah]; + + const result = [ + 'IDR 0', + 'IDR 1', + 'IDR 0,5', + 'IDR 0,57', + 'IDR 1.000', + 'IDR 10.000', + 'IDR 1.000.000', + 'IDR 10.000.000', + 'IDR 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Franc', () => { + const formater = currencyFormaterMap[NumberFormat.Franc]; + + const result = [ + 'CHF 0', + 'CHF 1', + 'CHF 0.5', + 'CHF 0.57', + `CHF 1’000`, + `CHF 10’000`, + `CHF 1’000’000`, + `CHF 10’000’000`, + `CHF 1’000’000`, + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for HongKongDollar', () => { + const formater = currencyFormaterMap[NumberFormat.HongKongDollar]; + + const result = [ + 'HK$0', + 'HK$1', + 'HK$0.5', + 'HK$0.57', + 'HK$1,000', + 'HK$10,000', + 'HK$1,000,000', + 'HK$10,000,000', + 'HK$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewZealandDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewZealandDollar]; + + const result = [ + 'NZ$0', + 'NZ$1', + 'NZ$0.5', + 'NZ$0.57', + 'NZ$1,000', + 'NZ$10,000', + 'NZ$1,000,000', + 'NZ$10,000,000', + 'NZ$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Krona', () => { + const formater = currencyFormaterMap[NumberFormat.Krona]; + + const result = [ + '0 SEK', + '1 SEK', + '0,5 SEK', + '0,57 SEK', + '1 000 SEK', + '10 000 SEK', + '1 000 000 SEK', + '10 000 000 SEK', + '1 000 000 SEK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for NorwegianKrone', () => { + const formater = currencyFormaterMap[NumberFormat.NorwegianKrone]; + + const result = [ + 'NOK 0', + 'NOK 1', + 'NOK 0,5', + 'NOK 0,57', + 'NOK 1 000', + 'NOK 10 000', + 'NOK 1 000 000', + 'NOK 10 000 000', + 'NOK 1 000 000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for MexicanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.MexicanPeso]; + + const result = [ + 'MX$0', + 'MX$1', + 'MX$0.5', + 'MX$0.57', + 'MX$1,000', + 'MX$10,000', + 'MX$1,000,000', + 'MX$10,000,000', + 'MX$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rand', () => { + const formater = currencyFormaterMap[NumberFormat.Rand]; + + const result = [ + 'ZAR 0', + 'ZAR 1', + 'ZAR 0,5', + 'ZAR 0,57', + 'ZAR 1 000', + 'ZAR 10 000', + 'ZAR 1 000 000', + 'ZAR 10 000 000', + 'ZAR 1 000 000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewTaiwanDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewTaiwanDollar]; + + const result = [ + 'NT$0', + 'NT$1', + 'NT$0.5', + 'NT$0.57', + 'NT$1,000', + 'NT$10,000', + 'NT$1,000,000', + 'NT$10,000,000', + 'NT$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for DanishKrone', () => { + const formater = currencyFormaterMap[NumberFormat.DanishKrone]; + + const result = [ + '0 DKK', + '1 DKK', + '0,5 DKK', + '0,57 DKK', + '1.000 DKK', + '10.000 DKK', + '1.000.000 DKK', + '10.000.000 DKK', + '1.000.000 DKK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Baht', () => { + const formater = currencyFormaterMap[NumberFormat.Baht]; + + const result = [ + 'THB 0', + 'THB 1', + 'THB 0.5', + 'THB 0.57', + 'THB 1,000', + 'THB 10,000', + 'THB 1,000,000', + 'THB 10,000,000', + 'THB 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Forint', () => { + const formater = currencyFormaterMap[NumberFormat.Forint]; + + const result = [ + '0 HUF', + '1 HUF', + '0,5 HUF', + '0,57 HUF', + '1 000 HUF', + '10 000 HUF', + '1 000 000 HUF', + '10 000 000 HUF', + '1 000 000 HUF', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Koruna', () => { + const formater = currencyFormaterMap[NumberFormat.Koruna]; + + const result = [ + '0 CZK', + '1 CZK', + '0,5 CZK', + '0,57 CZK', + '1 000 CZK', + '10 000 CZK', + '1 000 000 CZK', + '10 000 000 CZK', + '1 000 000 CZK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Shekel', () => { + const formater = currencyFormaterMap[NumberFormat.Shekel]; + + const result = [ + '‏0 ‏₪', + '‏1 ‏₪', + '‏0.5 ‏₪', + '‏0.57 ‏₪', + '‏1,000 ‏₪', + '‏10,000 ‏₪', + '‏1,000,000 ‏₪', + '‏10,000,000 ‏₪', + '‏1,000,000 ‏₪', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ChileanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ChileanPeso]; + + const result = [ + 'CLP 0', + 'CLP 1', + 'CLP 0,5', + 'CLP 0,57', + 'CLP 1.000', + 'CLP 10.000', + 'CLP 1.000.000', + 'CLP 10.000.000', + 'CLP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for PhilippinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.PhilippinePeso]; + + const result = ['₱0', '₱1', '₱0.5', '₱0.57', '₱1,000', '₱10,000', '₱1,000,000', '₱10,000,000', '₱1,000,000']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Dirham', () => { + const formater = currencyFormaterMap[NumberFormat.Dirham]; + + const result = [ + '‏0 AED', + '‏1 AED', + '‏0.5 AED', + '‏0.57 AED', + '‏1,000 AED', + '‏10,000 AED', + '‏1,000,000 AED', + '‏10,000,000 AED', + '‏1,000,000 AED', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ColombianPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ColombianPeso]; + + const result = [ + 'COP 0', + 'COP 1', + 'COP 0,5', + 'COP 0,57', + 'COP 1.000', + 'COP 10.000', + 'COP 1.000.000', + 'COP 10.000.000', + 'COP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Riyal', () => { + const formater = currencyFormaterMap[NumberFormat.Riyal]; + + const result = [ + 'SAR 0', + 'SAR 1', + 'SAR 0.5', + 'SAR 0.57', + 'SAR 1,000', + 'SAR 10,000', + 'SAR 1,000,000', + 'SAR 10,000,000', + 'SAR 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ringgit', () => { + const formater = currencyFormaterMap[NumberFormat.Ringgit]; + + const result = [ + 'RM 0', + 'RM 1', + 'RM 0.5', + 'RM 0.57', + 'RM 1,000', + 'RM 10,000', + 'RM 1,000,000', + 'RM 10,000,000', + 'RM 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Leu', () => { + const formater = currencyFormaterMap[NumberFormat.Leu]; + + const result = [ + '0 RON', + '1 RON', + '0,5 RON', + '0,57 RON', + '1.000 RON', + '10.000 RON', + '1.000.000 RON', + '10.000.000 RON', + '1.000.000 RON', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for ArgentinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.ArgentinePeso]; + + const result = [ + 'ARS 0', + 'ARS 1', + 'ARS 0,5', + 'ARS 0,57', + 'ARS 1.000', + 'ARS 10.000', + 'ARS 1.000.000', + 'ARS 10.000.000', + 'ARS 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for UruguayanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.UruguayanPeso]; + + const result = [ + 'UYU 0', + 'UYU 1', + 'UYU 0,5', + 'UYU 0,57', + 'UYU 1.000', + 'UYU 10.000', + 'UYU 1.000.000', + 'UYU 10.000.000', + 'UYU 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts new file mode 100644 index 0000000000..589f6ac3ec --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts @@ -0,0 +1,229 @@ +import { NumberFormat } from './number.type'; + +const commonProps = { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + style: 'currency', + currencyDisplay: 'symbol', + useGrouping: true, +}; + +export const currencyFormaterMap: Record string> = { + [NumberFormat.Num]: (n: number) => + new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }).format(n), + [NumberFormat.Percent]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + style: 'decimal', + }).format(n) + '%', + [NumberFormat.USD]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'USD', + }).format(n), + [NumberFormat.CanadianDollar]: (n: number) => + new Intl.NumberFormat('en-CA', { + ...commonProps, + currency: 'CAD', + }) + .format(n) + .replace('$', 'CA$'), + [NumberFormat.EUR]: (n: number) => + new Intl.NumberFormat('en-IE', { + ...commonProps, + currency: 'EUR', + }).format(n), + [NumberFormat.Pound]: (n: number) => + new Intl.NumberFormat('en-GB', { + ...commonProps, + currency: 'GBP', + }).format(n), + [NumberFormat.Yen]: (n: number) => + new Intl.NumberFormat('ja-JP', { + ...commonProps, + currency: 'JPY', + }).format(n), + [NumberFormat.Ruble]: (n: number) => + new Intl.NumberFormat('ru-RU', { + ...commonProps, + currency: 'RUB', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupee]: (n: number) => + new Intl.NumberFormat('hi-IN', { + ...commonProps, + currency: 'INR', + }).format(n), + [NumberFormat.Won]: (n: number) => + new Intl.NumberFormat('ko-KR', { + ...commonProps, + currency: 'KRW', + }).format(n), + [NumberFormat.Yuan]: (n: number) => + new Intl.NumberFormat('zh-CN', { + ...commonProps, + currency: 'CNY', + }) + .format(n) + .replace('¥', 'CN¥'), + [NumberFormat.Real]: (n: number) => + new Intl.NumberFormat('pt-BR', { + ...commonProps, + currency: 'BRL', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Lira]: (n: number) => + new Intl.NumberFormat('tr-TR', { + ...commonProps, + currency: 'TRY', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupiah]: (n: number) => + new Intl.NumberFormat('id-ID', { + ...commonProps, + currency: 'IDR', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Franc]: (n: number) => + new Intl.NumberFormat('de-CH', { + ...commonProps, + currency: 'CHF', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.HongKongDollar]: (n: number) => + new Intl.NumberFormat('zh-HK', { + ...commonProps, + currency: 'HKD', + }).format(n), + [NumberFormat.NewZealandDollar]: (n: number) => + new Intl.NumberFormat('en-NZ', { + ...commonProps, + currency: 'NZD', + }) + .format(n) + .replace('$', 'NZ$'), + [NumberFormat.Krona]: (n: number) => + new Intl.NumberFormat('sv-SE', { + ...commonProps, + currency: 'SEK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NorwegianKrone]: (n: number) => + new Intl.NumberFormat('nb-NO', { + ...commonProps, + currency: 'NOK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.MexicanPeso]: (n: number) => + new Intl.NumberFormat('es-MX', { + ...commonProps, + currency: 'MXN', + }) + .format(n) + .replace('$', 'MX$'), + [NumberFormat.Rand]: (n: number) => + new Intl.NumberFormat('en-ZA', { + ...commonProps, + currency: 'ZAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NewTaiwanDollar]: (n: number) => + new Intl.NumberFormat('zh-TW', { + ...commonProps, + currency: 'TWD', + }) + .format(n) + .replace('$', 'NT$'), + [NumberFormat.DanishKrone]: (n: number) => + new Intl.NumberFormat('da-DK', { + ...commonProps, + currency: 'DKK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Baht]: (n: number) => + new Intl.NumberFormat('th-TH', { + ...commonProps, + currency: 'THB', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Forint]: (n: number) => + new Intl.NumberFormat('hu-HU', { + ...commonProps, + currency: 'HUF', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Koruna]: (n: number) => + new Intl.NumberFormat('cs-CZ', { + ...commonProps, + currency: 'CZK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Shekel]: (n: number) => + new Intl.NumberFormat('he-IL', { + ...commonProps, + currency: 'ILS', + }).format(n), + [NumberFormat.ChileanPeso]: (n: number) => + new Intl.NumberFormat('es-CL', { + ...commonProps, + currency: 'CLP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.PhilippinePeso]: (n: number) => + new Intl.NumberFormat('fil-PH', { + ...commonProps, + currency: 'PHP', + }).format(n), + [NumberFormat.Dirham]: (n: number) => + new Intl.NumberFormat('ar-AE', { + ...commonProps, + currency: 'AED', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.ColombianPeso]: (n: number) => + new Intl.NumberFormat('es-CO', { + ...commonProps, + currency: 'COP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Riyal]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'SAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Ringgit]: (n: number) => + new Intl.NumberFormat('ms-MY', { + ...commonProps, + currency: 'MYR', + }).format(n), + [NumberFormat.Leu]: (n: number) => + new Intl.NumberFormat('ro-RO', { + ...commonProps, + currency: 'RON', + }).format(n), + [NumberFormat.ArgentinePeso]: (n: number) => + new Intl.NumberFormat('es-AR', { + ...commonProps, + currency: 'ARS', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.UruguayanPeso]: (n: number) => + new Intl.NumberFormat('es-UY', { + ...commonProps, + currency: 'UYU', + currencyDisplay: 'code', + }).format(n), +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts new file mode 100644 index 0000000000..27ca7cd8d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './number.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts new file mode 100644 index 0000000000..9140531325 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts @@ -0,0 +1,56 @@ +import { Filter } from '@/application/database-yjs'; + +export enum NumberFormat { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +export enum NumberFilterCondition { + Equal = 0, + NotEqual = 1, + GreaterThan = 2, + LessThan = 3, + GreaterThanOrEqualTo = 4, + LessThanOrEqualTo = 5, + NumberIsEmpty = 6, + NumberIsNotEmpty = 7, +} + +export interface NumberFilter extends Filter { + condition: NumberFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts new file mode 100644 index 0000000000..9abac198b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts @@ -0,0 +1,11 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { NumberFormat } from './number.type'; + +export function parseNumberTypeOptions(field: YDatabaseField) { + const numberTypeOption = getTypeOptions(field)?.toJSON(); + + return { + format: parseInt(numberTypeOption.format) as NumberFormat, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts new file mode 100644 index 0000000000..4b94064b52 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts @@ -0,0 +1,2 @@ +export * from './parse'; +export * from './relation.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts new file mode 100644 index 0000000000..c5820576cd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts @@ -0,0 +1,9 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { RelationTypeOption } from './relation.type'; +import { getTypeOptions } from '../type_option'; + +export function parseRelationTypeOption(field: YDatabaseField) { + const relationTypeOption = getTypeOptions(field)?.toJSON(); + + return relationTypeOption as RelationTypeOption; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts new file mode 100644 index 0000000000..31021afc38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts @@ -0,0 +1,9 @@ +import { Filter } from '@/application/database-yjs'; + +export interface RelationTypeOption { + database_id: string; +} + +export interface RelationFilter extends Filter { + condition: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts new file mode 100644 index 0000000000..a569b2ca47 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts @@ -0,0 +1,2 @@ +export * from './select_option.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts new file mode 100644 index 0000000000..7840278a34 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts @@ -0,0 +1,28 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { SelectTypeOption } from './select_option.type'; + +export function parseSelectOptionTypeOptions(field: YDatabaseField) { + const content = getTypeOptions(field)?.get(YjsDatabaseKey.content); + + if (!content) return null; + + try { + return JSON.parse(content) as SelectTypeOption; + } catch (e) { + return null; + } +} + +export function parseSelectOptionCellData(field: YDatabaseField, data: string) { + const typeOption = parseSelectOptionTypeOptions(field); + const selectedIds = typeof data === 'string' ? data.split(',') : []; + + return selectedIds + .map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + return option?.name ?? ''; + }) + .join(', '); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts new file mode 100644 index 0000000000..343941d588 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts @@ -0,0 +1,38 @@ +import { Filter } from '@/application/database-yjs'; + +export enum SelectOptionColor { + Purple = 'Purple', + Pink = 'Pink', + LightPink = 'LightPink', + Orange = 'Orange', + Yellow = 'Yellow', + Lime = 'Lime', + Green = 'Green', + Aqua = 'Aqua', + Blue = 'Blue', +} + +export enum SelectOptionFilterCondition { + OptionIs = 0, + OptionIsNot = 1, + OptionContains = 2, + OptionDoesNotContain = 3, + OptionIsEmpty = 4, + OptionIsNotEmpty = 5, +} + +export interface SelectOptionFilter extends Filter { + condition: SelectOptionFilterCondition; + optionIds: string[]; +} + +export interface SelectOption { + id: string; + name: string; + color: SelectOptionColor; +} + +export interface SelectTypeOption { + disable_color: boolean; + options: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts new file mode 100644 index 0000000000..7d0a52cd9d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts @@ -0,0 +1 @@ +export * from './text.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts new file mode 100644 index 0000000000..c2f230c738 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts @@ -0,0 +1,17 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TextFilterCondition { + TextIs = 0, + TextIsNot = 1, + TextContains = 2, + TextDoesNotContain = 3, + TextStartsWith = 4, + TextEndsWith = 5, + TextIsEmpty = 6, + TextIsNotEmpty = 7, +} + +export interface TextFilter extends Filter { + condition: TextFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts new file mode 100644 index 0000000000..bf9c80706f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts @@ -0,0 +1,8 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; + +export function getTypeOptions(field: YDatabaseField) { + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts new file mode 100644 index 0000000000..0bf25e4ca8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -0,0 +1,234 @@ +import { + YDatabaseFields, + YDatabaseFilter, + YDatabaseFilters, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { + CheckboxFilter, + CheckboxFilterCondition, + ChecklistFilter, + ChecklistFilterCondition, + DateFilter, + NumberFilter, + NumberFilterCondition, + parseChecklistData, + SelectOptionFilter, + SelectOptionFilterCondition, + TextFilter, + TextFilterCondition, +} from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import Decimal from 'decimal.js'; +import * as Y from 'yjs'; +import { every, filter, some } from 'lodash-es'; + +export function parseFilter(fieldType: FieldType, filter: YDatabaseFilter) { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const filterType = Number(filter.get(YjsDatabaseKey.filter_type)); + const id = filter.get(YjsDatabaseKey.id); + const content = filter.get(YjsDatabaseKey.content); + const condition = Number(filter.get(YjsDatabaseKey.condition)); + + const value = { + fieldId, + filterType, + condition, + id, + content, + }; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return value as TextFilter; + case FieldType.Number: + return value as NumberFilter; + case FieldType.Checklist: + return value as ChecklistFilter; + case FieldType.Checkbox: + return value as CheckboxFilter; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + // eslint-disable-next-line no-case-declarations + const options = content.split(','); + + return { + ...value, + optionIds: options, + } as SelectOptionFilter; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return value as DateFilter; + } + + return value; +} + +function createPredicate(conditions: ((row: Row) => boolean)[]) { + return function (item: Row) { + return every(conditions, (condition) => condition(item)); + }; +} + +export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map) { + const filterArray = filters.toArray(); + + if (filterArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows; + + const conditions = filterArray.map((filter) => { + return (row: { id: string }) => { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + if (!rowMeta) return false; + const filterValue = parseFilter(fieldType, filter); + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return false; + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return false; + const { condition, content } = filterValue; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return textFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Number: + return numberFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checkbox: + return checkboxFilterCheck(cell.get(YjsDatabaseKey.data) as string, condition); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return selectOptionFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checklist: + return checklistFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + default: + return true; + } + }; + }); + const predicate = createPredicate(conditions); + + return filter(rows, predicate); +} + +export function textFilterCheck(data: string, content: string, condition: TextFilterCondition) { + switch (condition) { + case TextFilterCondition.TextContains: + return data.includes(content); + case TextFilterCondition.TextDoesNotContain: + return !data.includes(content); + case TextFilterCondition.TextIs: + return data === content; + case TextFilterCondition.TextIsNot: + return data !== content; + case TextFilterCondition.TextIsEmpty: + return data === ''; + case TextFilterCondition.TextIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function numberFilterCheck(data: string, content: string, condition: number) { + if (isNaN(Number(data)) || isNaN(Number(content)) || data === '' || content === '') { + if (condition === NumberFilterCondition.NumberIsEmpty) { + return data === ''; + } + + if (condition === NumberFilterCondition.NumberIsNotEmpty) { + return data !== ''; + } + + return false; + } + + const decimal = new Decimal(data).toNumber(); + const filterDecimal = new Decimal(content).toNumber(); + + switch (condition) { + case NumberFilterCondition.Equal: + return decimal === filterDecimal; + case NumberFilterCondition.NotEqual: + return decimal !== filterDecimal; + case NumberFilterCondition.GreaterThan: + return decimal > filterDecimal; + case NumberFilterCondition.GreaterThanOrEqualTo: + return decimal >= filterDecimal; + case NumberFilterCondition.LessThan: + return decimal < filterDecimal; + case NumberFilterCondition.LessThanOrEqualTo: + return decimal <= filterDecimal; + default: + return false; + } +} + +export function checkboxFilterCheck(data: string, condition: number) { + switch (condition) { + case CheckboxFilterCondition.IsChecked: + return data === 'Yes'; + case CheckboxFilterCondition.IsUnChecked: + return data !== 'Yes'; + default: + return false; + } +} + +export function checklistFilterCheck(data: string, content: string, condition: number) { + const percentage = parseChecklistData(data)?.percentage ?? 0; + + if (condition === ChecklistFilterCondition.IsComplete) { + return percentage === 1; + } + + return percentage !== 1; +} + +export function selectOptionFilterCheck(data: string, content: string, condition: number) { + if (SelectOptionFilterCondition.OptionIsEmpty === condition) { + return data === ''; + } + + if (SelectOptionFilterCondition.OptionIsNotEmpty === condition) { + return data !== ''; + } + + const selectedOptionIds = data.split(','); + const filterOptionIds = content.split(','); + + switch (condition) { + // Ensure all filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIs: + return every(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure none of the filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIsNot: + return every(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is included in selectedOptionIds + case SelectOptionFilterCondition.OptionContains: + return some(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is not included in selectedOptionIds + case SelectOptionFilterCondition.OptionDoesNotContain: + return some(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Default case, if no conditions match + default: + return false; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts new file mode 100644 index 0000000000..709053aa32 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -0,0 +1,54 @@ +import { YDatabaseField, YDoc, YjsDatabaseKey } from '@/application/collab.type'; +import { getCellData } from '@/application/database-yjs/const'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import * as Y from 'yjs'; + +export function groupByField(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); + + if (!isSelectOptionField) return; + return groupBySelectOption(rows, rowMetas, field); +} + +export function groupBySelectOption(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldId = field.get(YjsDatabaseKey.id); + const result = new Map(); + const typeOption = parseSelectOptionTypeOptions(field); + + if (!typeOption) { + return; + } + + if (typeOption.options.length === 0) { + result.set(fieldId, rows); + return result; + } + + rows.forEach((row) => { + const cellData = getCellData(row.id, fieldId, rowMetas); + + const selectedIds = (cellData as string)?.split(',') ?? []; + + if (selectedIds.length === 0) { + const group = result.get(fieldId) ?? []; + + group.push(row); + result.set(fieldId, group); + return; + } + + selectedIds.forEach((id) => { + const option = typeOption.options.find((option) => option.id === id); + const groupName = option?.id ?? fieldId; + const group = result.get(groupName) ?? []; + + group.push(row); + result.set(groupName, group); + }); + }); + + return result; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/index.ts new file mode 100644 index 0000000000..1d5aa0ce3d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -0,0 +1,6 @@ +export * from './context'; +export * from './fields'; +export * from './context'; +export * from './selector'; +export * from './database.type'; +export * from './const'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts new file mode 100644 index 0000000000..884c58516d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -0,0 +1,769 @@ +import { + FieldId, + SortId, + YDatabaseField, + YDoc, + YjsDatabaseKey, + YjsEditorKey, + YjsFolderKey, +} from '@/application/collab.type'; +import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { + useDatabase, + useDatabaseFields, + useDatabaseView, + useIsDatabaseRowPage, + useRowDocMap, + useViewId, +} from '@/application/database-yjs/context'; +import { filterBy, parseFilter } from '@/application/database-yjs/filter'; +import { groupByField } from '@/application/database-yjs/group'; +import { sortBy } from '@/application/database-yjs/sort'; +import { useViewsIdSelector } from '@/application/folder-yjs'; +import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; +import { DateTimeCell } from '@/application/database-yjs/cell.type'; +import * as dayjs from 'dayjs'; +import { throttle } from 'lodash-es'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import Y from 'yjs'; +import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type'; + +export interface Column { + fieldId: string; + width: number; + visibility: FieldVisibility; + wrap?: boolean; +} + +export interface Row { + id: string; + height: number; +} + +const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export function useDatabaseViewsSelector(iidIndex: string) { + const database = useDatabase(); + const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector(); + + const views = database?.get(YjsDatabaseKey.views); + const [viewIds, setViewIds] = useState([]); + const childViews = useMemo(() => { + return viewIds.map((viewId) => views?.get(viewId)); + }, [viewIds, views]); + + useEffect(() => { + if (!views) return; + + const observerEvent = () => { + const viewsObj = views.toJSON(); + + const viewsSorted = Object.entries(viewsObj).sort((a, b) => { + const [, viewA] = a; + const [, viewB] = b; + + return Number(viewB.created_at) - Number(viewA.created_at); + }); + + const viewsId = []; + + for (const viewItem of viewsSorted) { + const [key] = viewItem; + const view = folderViews?.get(key); + + if ( + visibleViewsId.includes(key) && + view && + (view.get(YjsFolderKey.bid) === iidIndex || view.get(YjsFolderKey.id) === iidIndex) + ) { + viewsId.push(key); + } + } + + setViewIds(viewsId); + }; + + observerEvent(); + views.observe(observerEvent); + + return () => { + views.unobserve(observerEvent); + }; + }, [visibleViewsId, views, folderViews, iidIndex]); + + return { + childViews, + viewIds, + }; +} + +export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisible) { + const viewId = useViewId(); + const database = useDatabase(); + const [columns, setColumns] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const fields = database?.get(YjsDatabaseKey.fields); + const fieldsOrder = view?.get(YjsDatabaseKey.field_orders); + const fieldSettings = view?.get(YjsDatabaseKey.field_settings); + const getColumns = () => { + if (!fields || !fieldsOrder || !fieldSettings) return []; + const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[]; + + return fieldIds + .map((fieldId) => { + const setting = fieldSettings.get(fieldId); + + return { + fieldId, + width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, + visibility: Number( + setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown + ) as FieldVisibility, + wrap: setting?.get(YjsDatabaseKey.wrap) ?? true, + }; + }) + .filter((column) => { + return visibilitys.includes(column.visibility); + }); + }; + + const observerEvent = () => setColumns(getColumns()); + + setColumns(getColumns()); + + fieldsOrder?.observe(observerEvent); + fieldSettings?.observe(observerEvent); + + return () => { + fieldsOrder?.unobserve(observerEvent); + fieldSettings?.unobserve(observerEvent); + }; + }, [database, viewId, visibilitys]); + + return columns; +} + +export function useFieldSelector(fieldId: string) { + const database = useDatabase(); + const [field, setField] = useState(null); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!database) return; + + const field = database.get(YjsDatabaseKey.fields)?.get(fieldId); + + setField(field || null); + const observerEvent = () => setClock((prev) => prev + 1); + + field?.observe(observerEvent); + + return () => { + field?.unobserve(observerEvent); + }; + }, [database, fieldId]); + + return { + field, + clock, + }; +} + +export function useFiltersSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [filters, setFilters] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filterOrders = view?.get(YjsDatabaseKey.filters); + + if (!filterOrders) return; + + const getFilters = () => { + return filterOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setFilters(getFilters()); + + setFilters(getFilters()); + + filterOrders.observe(observerEvent); + + return () => { + filterOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return filters; +} + +export function useFilterSelector(filterId: string) { + const database = useDatabase(); + const viewId = useViewId(); + const fields = database?.get(YjsDatabaseKey.fields); + const [filterValue, setFilterValue] = useState(null); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filter = view + ?.get(YjsDatabaseKey.filters) + .toArray() + .find((filter) => filter.get(YjsDatabaseKey.id) === filterId); + const field = fields?.get(filter?.get(YjsDatabaseKey.field_id) as FieldId); + + const observerEvent = () => { + if (!filter || !field) return; + const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType; + + setFilterValue(parseFilter(fieldType, filter)); + }; + + observerEvent(); + field?.observe(observerEvent); + filter?.observe(observerEvent); + return () => { + field?.unobserve(observerEvent); + filter?.unobserve(observerEvent); + }; + }, [fields, viewId, filterId, database]); + return filterValue; +} + +export function useSortsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [sorts, setSorts] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const sortOrders = view?.get(YjsDatabaseKey.sorts); + + if (!sortOrders) return; + + const getSorts = () => { + return sortOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setSorts(getSorts()); + + setSorts(getSorts()); + + sortOrders.observe(observerEvent); + + return () => { + sortOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return sorts; +} + +export interface Sort { + fieldId: FieldId; + condition: SortCondition; + id: SortId; +} + +export function useSortSelector(sortId: SortId) { + const database = useDatabase(); + const viewId = useViewId(); + const [sortValue, setSortValue] = useState(null); + const views = database?.get(YjsDatabaseKey.views); + + useEffect(() => { + if (!viewId) return; + const view = views?.get(viewId); + const sort = view + ?.get(YjsDatabaseKey.sorts) + .toArray() + .find((sort) => sort.get(YjsDatabaseKey.id) === sortId); + + const observerEvent = () => { + setSortValue({ + fieldId: sort?.get(YjsDatabaseKey.field_id) as FieldId, + condition: Number(sort?.get(YjsDatabaseKey.condition)), + id: sort?.get(YjsDatabaseKey.id) as SortId, + }); + }; + + observerEvent(); + sort?.observe(observerEvent); + + return () => { + sort?.unobserve(observerEvent); + }; + }, [viewId, sortId, views]); + + return sortValue; +} + +export function useGroupsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [groups, setGroups] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const groupOrders = view?.get(YjsDatabaseKey.groups); + + if (!groupOrders) return; + + const getGroups = () => { + return groupOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setGroups(getGroups()); + + setGroups(getGroups()); + + groupOrders.observe(observerEvent); + + return () => { + groupOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return groups; +} + +export interface GroupColumn { + id: string; + visible: boolean; +} + +export function useGroup(groupId: string) { + const database = useDatabase(); + const viewId = useViewId() as string; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const group = view + ?.get(YjsDatabaseKey.groups) + ?.toArray() + .find((group) => group.get(YjsDatabaseKey.id) === groupId); + const groupColumns = group?.get(YjsDatabaseKey.groups); + const [fieldId, setFieldId] = useState(null); + const [columns, setColumns] = useState([]); + + useEffect(() => { + if (!viewId) return; + + const observerEvent = () => { + setFieldId(group?.get(YjsDatabaseKey.field_id) as string); + }; + + observerEvent(); + group?.observe(observerEvent); + + const observerColumns = () => { + if (!groupColumns) return; + setColumns(groupColumns.toJSON()); + }; + + observerColumns(); + groupColumns?.observe(observerColumns); + + return () => { + group?.unobserve(observerEvent); + groupColumns?.unobserve(observerColumns); + }; + }, [database, viewId, groupId, group, groupColumns]); + + return { + columns, + fieldId, + }; +} + +export function useRowsByGroup(groupId: string) { + const { columns, fieldId } = useGroup(groupId); + const rows = useRowDocMap(); + const rowOrders = useRowOrdersSelector(); + + const fields = useDatabaseFields(); + const [notFound, setNotFound] = useState(false); + const [groupResult, setGroupResult] = useState>(new Map()); + + useEffect(() => { + if (!fieldId || !rowOrders || !rows) return; + + const onConditionsChange = () => { + if (rows.size < rowOrders?.length) return; + + const newResult = new Map(); + + const field = fields.get(fieldId); + + if (!field) { + setNotFound(true); + setGroupResult(newResult); + return; + } + + const groupResult = groupByField(rowOrders, rows, field); + + if (!groupResult) { + setGroupResult(newResult); + return; + } + + setGroupResult(groupResult); + }; + + onConditionsChange(); + + fields.observeDeep(onConditionsChange); + return () => { + fields.unobserveDeep(onConditionsChange); + }; + }, [fieldId, fields, rowOrders, rows]); + + const visibleColumns = columns.filter((column) => column.visible); + + return { + fieldId, + groupResult, + columns: visibleColumns, + notFound, + }; +} + +export function useRowOrdersSelector() { + const isDatabaseRowPage = useIsDatabaseRowPage(); + const { rows, clock } = useRowDocMapSelector(); + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + const onConditionsChange = useCallback(() => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + if (originalRowOrders.length > rows.size && !isDatabaseRowPage) return; + if (sorts?.length === 0 && filters?.length === 0) { + setRowOrders(originalRowOrders); + return; + } + + let rowOrders: Row[] | undefined; + + if (sorts?.length) { + rowOrders = sortBy(originalRowOrders, sorts, fields, rows); + } + + if (filters?.length) { + rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); + } + + if (rowOrders) { + setRowOrders(rowOrders); + } else { + setRowOrders(originalRowOrders); + } + }, [fields, filters, rows, sorts, view, isDatabaseRowPage]); + + useEffect(() => { + onConditionsChange(); + }, [onConditionsChange, clock]); + + useEffect(() => { + const throttleChange = throttle(onConditionsChange, 200); + + sorts?.observeDeep(throttleChange); + filters?.observeDeep(throttleChange); + fields?.observeDeep(throttleChange); + + return () => { + sorts?.unobserveDeep(throttleChange); + filters?.unobserveDeep(throttleChange); + fields?.unobserveDeep(throttleChange); + }; + }, [onConditionsChange, fields, filters, sorts]); + + return rowOrders; +} + +export function useRowDocMapSelector() { + const rowMap = useRowDocMap(); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!rowMap) return; + const observerEvent = () => setClock((prev) => prev + 1); + + const rowIds = Array.from(rowMap?.keys() || []); + + rowMap.observe(observerEvent); + + const observers = rowIds.map((rowId) => { + return observeDeepRow(rowId, rowMap, observerEvent); + }); + + return () => { + rowMap.unobserve(observerEvent); + observers.forEach((observer) => observer()); + }; + }, [rowMap]); + + return { + rows: rowMap, + clock, + }; +} + +export function observeDeepRow( + rowId: string, + rowMap: Y.Map, + observerEvent: () => void, + key: YjsEditorKey.meta | YjsEditorKey.database_row = YjsEditorKey.database_row +) { + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const row = rowSharedRoot?.get(key); + + rowSharedRoot?.observe(observerEvent); + row?.observeDeep(observerEvent); + return () => { + rowSharedRoot?.unobserve(observerEvent); + row?.unobserveDeep(observerEvent); + }; +} + +export function useRowDataSelector(rowId: string) { + const rowMap = useRowDocMap(); + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const row = rowSharedRoot?.get(YjsEditorKey.database_row); + + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!rowMap) return; + const onChange = () => { + setClock((prev) => prev + 1); + }; + + const observer = observeDeepRow(rowId, rowMap, onChange); + + rowMap.observe(onChange); + + return () => { + rowMap.unobserve(onChange); + observer(); + }; + }, [rowId, rowMap]); + + return { + row, + clock, + }; +} + +export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { + const { row } = useRowDataSelector(rowId); + + const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); + + useEffect(() => { + if (!cell) return; + setCellValue(parseYDatabaseCellToCell(cell)); + const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); + + cell.observe(observerEvent); + + return () => { + cell.unobserve(observerEvent); + }; + }, [cell]); + + return cellValue; +} + +export interface CalendarEvent { + start?: Date; + end?: Date; + id: string; +} + +export function useCalendarEventsSelector() { + const setting = useCalendarLayoutSetting(); + const filedId = setting.fieldId; + const { field } = useFieldSelector(filedId); + const rowOrders = useRowOrdersSelector(); + const rows = useRowDocMap(); + const [events, setEvents] = useState([]); + const [emptyEvents, setEmptyEvents] = useState([]); + + useEffect(() => { + if (!field || !rowOrders || !rows) return; + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (fieldType !== FieldType.DateTime) return; + const newEvents: CalendarEvent[] = []; + const emptyEvents: CalendarEvent[] = []; + + rowOrders?.forEach((row) => { + const cell = getCell(row.id, filedId, rows); + + if (!cell) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const value = parseYDatabaseCellToCell(cell) as DateTimeCell; + + if (!value || !value.data) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const getDate = (timestamp: string) => { + const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp); + + return dayjsResult.toDate(); + }; + + newEvents.push({ + id: `${row.id}:${filedId}`, + start: getDate(value.data), + end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : getDate(value.data), + }); + }); + + setEvents(newEvents); + setEmptyEvents(emptyEvents); + }, [field, rowOrders, rows, filedId]); + + return { events, emptyEvents }; +} + +export function useCalendarLayoutSetting() { + const view = useDatabaseView(); + const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2'); + const [setting, setSetting] = useState({ + fieldId: '', + firstDayOfWeek: 0, + showWeekNumbers: true, + showWeekends: true, + layout: 0, + }); + + useEffect(() => { + const observerHandler = () => { + setSetting({ + fieldId: layoutSetting?.get(YjsDatabaseKey.field_id) as string, + firstDayOfWeek: Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week)), + showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)), + showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)), + layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)), + }); + }; + + observerHandler(); + layoutSetting?.observe(observerHandler); + return () => { + layoutSetting?.unobserve(observerHandler); + }; + }, [layoutSetting]); + + return setting; +} + +export function usePrimaryFieldId() { + const database = useDatabase(); + const [primaryFieldId, setPrimaryFieldId] = useState(null); + + useEffect(() => { + const fields = database?.get(YjsDatabaseKey.fields); + const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { + return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); + }); + + setPrimaryFieldId(primaryFieldId || null); + }, [database]); + + return primaryFieldId; +} + +export interface RowMeta { + documentId: string; + cover: string; + icon: string; + isEmptyDocument: boolean; +} + +const metaIdMapFromRowIdMap = new Map>(); + +function getMetaIdMap(rowId: string) { + const hasMetaIdMap = metaIdMapFromRowIdMap.has(rowId); + + if (!hasMetaIdMap) { + const parser = metaIdFromRowId(rowId); + const map = new Map(); + + map.set(RowMetaKey.IconId, parser(RowMetaKey.IconId)); + map.set(RowMetaKey.CoverId, parser(RowMetaKey.CoverId)); + map.set(RowMetaKey.DocumentId, parser(RowMetaKey.DocumentId)); + map.set(RowMetaKey.IsDocumentEmpty, parser(RowMetaKey.IsDocumentEmpty)); + metaIdMapFromRowIdMap.set(rowId, map); + return map; + } + + return metaIdMapFromRowIdMap.get(rowId) as Map; +} + +export const useRowMetaSelector = (rowId: string) => { + const [meta, setMeta] = useState(); + const rowMap = useRowDocMap(); + + const updateMeta = useCallback(() => { + const metaKeyMap = getMetaIdMap(rowId); + + const iconKey = metaKeyMap.get(RowMetaKey.IconId) ?? ''; + const coverKey = metaKeyMap.get(RowMetaKey.CoverId) ?? ''; + const documentId = metaKeyMap.get(RowMetaKey.DocumentId) ?? ''; + const isEmptyDocumentKey = metaKeyMap.get(RowMetaKey.IsDocumentEmpty) ?? ''; + const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section); + const yMeta = rowSharedRoot?.get(YjsEditorKey.meta); + + if (!yMeta) return; + const metaJson = yMeta.toJSON(); + + const icon = metaJson[iconKey]; + const cover = metaJson[coverKey]; + const isEmptyDocument = metaJson[isEmptyDocumentKey]; + + setMeta({ + icon, + cover, + documentId, + isEmptyDocument, + }); + }, [rowId, rowMap]); + + useEffect(() => { + if (!rowMap) return; + updateMeta(); + const observer = observeDeepRow(rowId, rowMap, updateMeta, YjsEditorKey.meta); + + rowMap.observe(updateMeta); + + return () => { + rowMap.unobserve(updateMeta); + observer(); + }; + }, [rowId, rowMap, updateMeta]); + + return meta; +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts new file mode 100644 index 0000000000..cead275830 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -0,0 +1,81 @@ +import { + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDatabaseSorts, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType, SortCondition } from '@/application/database-yjs/database.type'; +import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import { orderBy } from 'lodash-es'; +import * as Y from 'yjs'; + +export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map) { + const sortArray = sorts.toArray(); + + if (sortArray.length === 0 || rowMetas.size === 0 || fields.size === 0) return rows; + const iteratees = sortArray.map((sort) => { + return (row: { id: string }) => { + const fieldId = sort.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + const defaultData = parseCellDataForSort(field, ''); + + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return defaultData; + if (fieldType === FieldType.LastEditedTime) { + return meta.get(YjsDatabaseKey.last_modified); + } + + if (fieldType === FieldType.CreatedTime) { + return meta.get(YjsDatabaseKey.created_at); + } + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return defaultData; + + return parseCellDataForSort(field, cell.get(YjsDatabaseKey.data) ?? ''); + }; + }); + const orders = sortArray.map((sort) => { + const condition = Number(sort.get(YjsDatabaseKey.condition)); + + if (condition === SortCondition.Descending) return 'desc'; + return 'asc'; + }); + + return orderBy(rows, iteratees, orders); +} + +export function parseCellDataForSort(field: YDatabaseField, data: string | boolean | number | object) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return data ? data : '\uFFFF'; + case FieldType.Number: + return data; + case FieldType.Checkbox: + return data === 'Yes'; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return parseSelectOptionCellData(field, data as string); + case FieldType.Checklist: + return parseChecklistData(data as string)?.percentage ?? 0; + case FieldType.DateTime: + return Number(data); + case FieldType.Relation: + return ''; + } +} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/context.ts b/frontend/appflowy_web_app/src/application/folder-yjs/context.ts new file mode 100644 index 0000000000..cb0e1f63ff --- /dev/null +++ b/frontend/appflowy_web_app/src/application/folder-yjs/context.ts @@ -0,0 +1,38 @@ +import { ViewLayout, YFolder, YjsFolderKey } from '@/application/collab.type'; +import { createContext, useContext } from 'react'; +import { useParams } from 'react-router-dom'; + +export interface Crumb { + viewId: string; + rowId?: string; + name: string; + icon: string; +} + +export const FolderContext = createContext<{ + folder: YFolder | null; + onNavigateToView?: (viewId: string) => void; + crumbs?: Crumb[]; + setCrumbs?: React.Dispatch>; +} | null>(null); + +export const useFolderContext = () => { + return useContext(FolderContext)?.folder; +}; + +export const useViewLayout = () => { + const folder = useFolderContext(); + const { objectId } = useParams(); + const views = folder?.get(YjsFolderKey.views); + const view = objectId ? views?.get(objectId) : null; + + return Number(view?.get(YjsFolderKey.layout)) as ViewLayout; +}; + +export const useNavigateToView = () => { + return useContext(FolderContext)?.onNavigateToView; +}; + +export const useCrumbs = () => { + return useContext(FolderContext)?.crumbs; +}; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts new file mode 100644 index 0000000000..ce9bebcefb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/folder-yjs/folder.type.ts @@ -0,0 +1,9 @@ +export enum CoverType { + NormalColor = 'color', + GradientColor = 'gradient', + BuildInImage = 'built_in', + CustomImage = 'custom', + LocalImage = 'local', + UpsplashImage = 'unsplash', + None = 'none', +} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/index.ts b/frontend/appflowy_web_app/src/application/folder-yjs/index.ts new file mode 100644 index 0000000000..f94cc509da --- /dev/null +++ b/frontend/appflowy_web_app/src/application/folder-yjs/index.ts @@ -0,0 +1,2 @@ +export * from './selector'; +export * from './context'; diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts new file mode 100644 index 0000000000..8e43efbf6a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts @@ -0,0 +1,70 @@ +import { YjsFolderKey, YView } from '@/application/collab.type'; +import { useFolderContext } from '@/application/folder-yjs/context'; +import { useEffect, useState } from 'react'; + +export function useViewsIdSelector() { + const folder = useFolderContext(); + const [viewsId, setViewsId] = useState([]); + const views = folder?.get(YjsFolderKey.views); + const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); + const meta = folder?.get(YjsFolderKey.meta); + + useEffect(() => { + if (!views) { + return; + } + + const trashUid = trash ? Array.from(trash.keys())[0] : null; + const userTrash = trashUid ? trash?.get(trashUid) : null; + + const collectIds = () => { + const trashIds = userTrash?.toJSON()?.map((item) => item.id) || []; + + return Array.from(views.keys()).filter((id) => { + return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace); + }); + }; + + setViewsId(collectIds()); + const observerEvent = () => setViewsId(collectIds()); + + views.observe(observerEvent); + userTrash?.observe(observerEvent); + + return () => { + views.unobserve(observerEvent); + userTrash?.unobserve(observerEvent); + }; + }, [views, trash, meta]); + + return { + viewsId, + views, + }; +} + +export function useViewSelector(viewId: string) { + const folder = useFolderContext(); + const [clock, setClock] = useState(0); + const [view, setView] = useState(null); + + useEffect(() => { + if (!folder) return; + + const view = folder.get(YjsFolderKey.views)?.get(viewId); + + setView(view || null); + const observerEvent = () => setClock((prev) => prev + 1); + + view?.observe(observerEvent); + + return () => { + view?.unobserve(observerEvent); + }; + }, [folder, viewId]); + + return { + clock, + view, + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/index.ts b/frontend/appflowy_web_app/src/application/services/index.ts new file mode 100644 index 0000000000..21d401d0da --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/index.ts @@ -0,0 +1,11 @@ +import { AFService, AFServiceConfig } from '@/application/services/services.type'; +import { AFClientService } from '$client-services'; + +let service: AFService; + +export async function getService (config: AFServiceConfig) { + if (service) return service; + + service = new AFClientService(config); + return service; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts new file mode 100644 index 0000000000..9a7e3fcb9d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/cache.test.ts @@ -0,0 +1,310 @@ +import { CollabType } from '@/application/collab.type'; +import * as Y from 'yjs'; +import { withTestingYDoc } from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; +import { expect } from '@jest/globals'; +import { getCollab, batchCollab, collabTypeToDBType } from '../cache'; +import { applyYDoc } from '@/application/ydoc/apply'; +import { getCollabDBName, openCollabDB } from '../cache/db'; +import { StrategyType } from '../cache/types'; + +jest.mock('@/application/ydoc/apply', () => ({ + applyYDoc: jest.fn(), +})); +jest.mock('../cache/db', () => ({ + openCollabDB: jest.fn(), + getCollabDBName: jest.fn(), +})); + +const emptyDoc = new Y.Doc(); +const normalDoc = withTestingYDoc('1'); +const mockFetcher = jest.fn(); +const mockBatchFetcher = jest.fn(); + +describe('Cache functions', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getCollab', () => { + describe('with CACHE_ONLY strategy', () => { + it('should throw error when no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + await expect( + getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_ONLY + ) + ).rejects.toThrow('No cache found'); + }); + it('should fetch collab with CACHE_ONLY strategy and existing cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_ONLY + ); + + expect(result).toBe(normalDoc); + expect(mockFetcher).not.toHaveBeenCalled(); + expect(applyYDoc).not.toHaveBeenCalled(); + }); + }); + + describe('with CACHE_FIRST strategy', () => { + it('should fetch collab with CACHE_FIRST strategy and existing cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + mockFetcher.mockResolvedValue({ state: new Uint8Array() }); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_FIRST + ); + + expect(result).toBe(normalDoc); + expect(mockFetcher).not.toHaveBeenCalled(); + expect(applyYDoc).not.toHaveBeenCalled(); + }); + + it('should fetch collab with CACHE_FIRST strategy and no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + mockFetcher.mockResolvedValue({ state: new Uint8Array() }); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_FIRST + ); + + expect(result).toBe(emptyDoc); + expect(mockFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + }); + + describe('with CACHE_AND_NETWORK strategy', () => { + it('should fetch collab with CACHE_AND_NETWORK strategy and existing cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + mockFetcher.mockResolvedValue({ state: new Uint8Array() }); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_AND_NETWORK + ); + + expect(result).toBe(normalDoc); + expect(mockFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + + it('should fetch collab with CACHE_AND_NETWORK strategy and no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + mockFetcher.mockResolvedValue({ state: new Uint8Array() }); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.CACHE_AND_NETWORK + ); + + expect(result).toBe(emptyDoc); + expect(mockFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + }); + + describe('with default strategy', () => { + it('should fetch collab with default strategy', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + mockFetcher.mockResolvedValue({ state: new Uint8Array() }); + + const result = await getCollab( + mockFetcher, + { + collabId: 'id1', + collabType: CollabType.Document, + }, + StrategyType.NETWORK_ONLY + ); + + expect(result).toBe(normalDoc); + expect(mockFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + }); + }); + + describe('batchCollab', () => { + describe('with CACHE_ONLY strategy', () => { + it('should batch fetch collabs with CACHE_ONLY strategy and no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + await expect( + batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_ONLY + ) + ).rejects.toThrow('No cache found'); + }); + + it('should batch fetch collabs with CACHE_ONLY strategy and existing cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + await batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_ONLY + ); + + expect(mockBatchFetcher).not.toHaveBeenCalled(); + }); + }); + + describe('with CACHE_FIRST strategy', () => { + it('should batch fetch collabs with CACHE_FIRST strategy and existing cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + + await batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_FIRST + ); + + expect(mockBatchFetcher).not.toHaveBeenCalled(); + }); + + it('should batch fetch collabs with CACHE_FIRST strategy and no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); + + await batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_FIRST + ); + + expect(mockBatchFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + }); + + describe('with CACHE_AND_NETWORK strategy', () => { + it('should batch fetch collabs with CACHE_AND_NETWORK strategy', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(normalDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); + + await batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_AND_NETWORK + ); + + expect(mockBatchFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + + it('should batch fetch collabs with CACHE_AND_NETWORK strategy and no cache', async () => { + (openCollabDB as jest.Mock).mockResolvedValue(emptyDoc); + + (getCollabDBName as jest.Mock).mockReturnValue('testDB'); + mockBatchFetcher.mockResolvedValue({ id1: [1, 2, 3] }); + + await batchCollab( + mockBatchFetcher, + [ + { + collabId: 'id1', + collabType: CollabType.Document, + }, + ], + StrategyType.CACHE_AND_NETWORK + ); + + expect(mockBatchFetcher).toHaveBeenCalled(); + expect(applyYDoc).toHaveBeenCalled(); + }); + }); + }); +}); + +describe('collabTypeToDBType', () => { + it('should return correct DB type', () => { + expect(collabTypeToDBType(CollabType.Document)).toBe('document'); + expect(collabTypeToDBType(CollabType.Folder)).toBe('folder'); + expect(collabTypeToDBType(CollabType.Database)).toBe('database'); + expect(collabTypeToDBType(CollabType.WorkspaceDatabase)).toBe('databases'); + expect(collabTypeToDBType(CollabType.DatabaseRow)).toBe('database_row'); + expect(collabTypeToDBType(CollabType.UserAwareness)).toBe('user_awareness'); + expect(collabTypeToDBType(CollabType.Empty)).toBe(''); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts new file mode 100644 index 0000000000..afdc418c2a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/__tests__/fetch.test.ts @@ -0,0 +1,57 @@ +import { expect } from '@jest/globals'; +import { fetchCollab, batchFetchCollab } from '../fetch'; +import { CollabType } from '@/application/collab.type'; +import { APIService } from '@/application/services/js-services/wasm'; + +jest.mock('@/application/services/js-services/wasm', () => { + return { + APIService: { + getCollab: jest.fn(), + batchGetCollab: jest.fn(), + }, + }; +}); + +describe('Collab fetch functions with deduplication', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchCollab', () => { + it('should fetch collab without duplicating requests', async () => { + const workspaceId = 'workspace1'; + const id = 'id1'; + const type = CollabType.Document; + const mockResponse = { data: 'mockData' }; + + (APIService.getCollab as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = fetchCollab(workspaceId, id, type); + const result2 = fetchCollab(workspaceId, id, type); + + expect(result1).toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + expect(APIService.getCollab).toHaveBeenCalledTimes(1); + }); + }); + + describe('batchFetchCollab', () => { + it('should batch fetch collabs without duplicating requests', async () => { + const workspaceId = 'workspace1'; + const params = [ + { collabId: 'id1', collabType: CollabType.Document }, + { collabId: 'id2', collabType: CollabType.Folder }, + ]; + const mockResponse = { data: 'mockData' }; + + (APIService.batchGetCollab as jest.Mock).mockResolvedValue(mockResponse); + + const result1 = batchFetchCollab(workspaceId, params); + const result2 = batchFetchCollab(workspaceId, params); + + expect(result1).toBe(result2); + await expect(result1).resolves.toEqual(mockResponse); + expect(APIService.batchGetCollab).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts new file mode 100644 index 0000000000..7f80c9f871 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts @@ -0,0 +1,39 @@ +import { AuthService } from '@/application/services/services.type'; +import { ProviderType, SignUpWithEmailPasswordParams } from '@/application/user.type'; +import { APIService } from 'src/application/services/js-services/wasm'; +import { signInSuccess } from '@/application/services/js-services/session/auth'; +import { invalidToken } from 'src/application/services/js-services/session'; +import { afterSignInDecorator } from '@/application/services/js-services/decorator'; + +export class JSAuthService implements AuthService { + constructor() { + // Do nothing + } + + getOAuthURL = async (_provider: ProviderType): Promise => { + return Promise.reject('Not implemented'); + }; + + @afterSignInDecorator(signInSuccess) + async signInWithOAuth(_: { uri: string }): Promise { + return Promise.reject('Not implemented'); + } + + signupWithEmailPassword = async (_params: SignUpWithEmailPasswordParams): Promise => { + return Promise.reject('Not implemented'); + }; + + @afterSignInDecorator(signInSuccess) + async signinWithEmailPassword(email: string, password: string): Promise { + try { + return APIService.signIn(email, password); + } catch (e) { + return Promise.reject(e); + } + } + + signOut = async (): Promise => { + invalidToken(); + return APIService.logout(); + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts new file mode 100644 index 0000000000..a4d888498b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/db.ts @@ -0,0 +1,41 @@ +import { YDoc } from '@/application/collab.type'; +import { databasePrefix } from '@/application/constants'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import * as Y from 'yjs'; + +const openedSet = new Set(); + +/** + * Open the collaboration database, and return a function to close it + */ +export async function openCollabDB(docName: string): Promise { + const name = `${databasePrefix}_${docName}`; + const doc = new Y.Doc(); + + const provider = new IndexeddbPersistence(name, doc); + + let resolve: (value: unknown) => void; + const promise = new Promise((resolveFn) => { + resolve = resolveFn; + }); + + provider.on('synced', () => { + if (!openedSet.has(name)) { + openedSet.add(name); + } + + resolve(true); + }); + + await promise; + + return doc as YDoc; +} + +export function getCollabDBName(id: string, type: string, uuid?: string) { + if (!uuid) { + return `${type}_${id}`; + } + + return `${uuid}_${type}_${id}`; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts new file mode 100644 index 0000000000..1f7fe670f1 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/index.ts @@ -0,0 +1,165 @@ +import { CollabType, YDoc, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; +import { applyYDoc } from '@/application/ydoc/apply'; +import { getCollabDBName, openCollabDB } from './db'; +import { Fetcher, StrategyType } from './types'; + +export function collabTypeToDBType(type: CollabType) { + switch (type) { + case CollabType.Folder: + return 'folder'; + case CollabType.Document: + return 'document'; + case CollabType.Database: + return 'database'; + case CollabType.WorkspaceDatabase: + return 'databases'; + case CollabType.DatabaseRow: + return 'database_row'; + case CollabType.UserAwareness: + return 'user_awareness'; + default: + return ''; + } +} + +const collabSharedRootKeyMap = { + [CollabType.Folder]: YjsEditorKey.folder, + [CollabType.Document]: YjsEditorKey.document, + [CollabType.Database]: YjsEditorKey.database, + [CollabType.WorkspaceDatabase]: YjsEditorKey.workspace_database, + [CollabType.DatabaseRow]: YjsEditorKey.database_row, + [CollabType.UserAwareness]: YjsEditorKey.user_awareness, + [CollabType.Empty]: YjsEditorKey.empty, +}; + +export function hasCache(doc: YDoc, type: CollabType) { + const data = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + return data.has(collabSharedRootKeyMap[type] as string); +} + +export async function getCollab( + fetcher: Fetcher<{ + state: Uint8Array; + }>, + { + collabId, + collabType, + uuid, + }: { + uuid?: string; + collabId: string; + collabType: CollabType; + }, + strategy: StrategyType = StrategyType.CACHE_AND_NETWORK +) { + const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid); + const collab = await openCollabDB(name); + const exist = hasCache(collab, collabType); + + switch (strategy) { + case StrategyType.CACHE_ONLY: { + if (!exist) { + throw new Error('No cache found'); + } + + return collab; + } + + case StrategyType.CACHE_FIRST: { + if (!exist) { + await revalidateCollab(fetcher, collab); + } + + return collab; + } + + case StrategyType.CACHE_AND_NETWORK: { + if (!exist) { + await revalidateCollab(fetcher, collab); + } else { + void revalidateCollab(fetcher, collab); + } + + return collab; + } + + default: { + await revalidateCollab(fetcher, collab); + + return collab; + } + } +} + +async function revalidateCollab( + fetcher: Fetcher<{ + state: Uint8Array; + }>, + collab: YDoc +) { + const { state } = await fetcher(); + + applyYDoc(collab, state); +} + +export async function batchCollab( + batchFetcher: Fetcher>, + collabs: { + collabId: string; + collabType: CollabType; + uuid?: string; + }[], + strategy: StrategyType = StrategyType.CACHE_AND_NETWORK, + itemCallback?: (id: string, doc: YDoc) => void +) { + const collabMap = new Map(); + + for (const { collabId, collabType, uuid } of collabs) { + const name = getCollabDBName(collabId, collabTypeToDBType(collabType), uuid); + const collab = await openCollabDB(name); + const exist = hasCache(collab, collabType); + + collabMap.set(collabId, collab); + if (exist) { + itemCallback?.(collabId, collab); + } + } + + const notCacheIds = collabs.filter(({ collabId, collabType }) => { + const id = collabMap.get(collabId); + + if (!id) return false; + + return !hasCache(id, collabType); + }); + + if (strategy === StrategyType.CACHE_ONLY) { + if (notCacheIds.length > 0) { + throw new Error('No cache found'); + } + + return; + } + + if (strategy === StrategyType.CACHE_FIRST && notCacheIds.length === 0) { + return; + } + + const states = await batchFetcher(); + + for (const [collabId, data] of Object.entries(states)) { + const info = collabs.find((item) => item.collabId === collabId); + const collab = collabMap.get(collabId); + + if (!info || !collab) { + continue; + } + + const state = new Uint8Array(data); + + applyYDoc(collab, state); + + itemCallback?.(collabId, collab); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/cache/types.ts b/frontend/appflowy_web_app/src/application/services/js-services/cache/types.ts new file mode 100644 index 0000000000..1c1a949723 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/cache/types.ts @@ -0,0 +1,12 @@ +export enum StrategyType { + // Cache only: return the cache if it exists, otherwise throw an error + CACHE_ONLY = 'CACHE_ONLY', + // Cache first: return the cache if it exists, otherwise fetch from the network + CACHE_FIRST = 'CACHE_FIRST', + // Cache and network: return the cache if it exists, otherwise fetch from the network and update the cache + CACHE_AND_NETWORK = 'CACHE_AND_NETWORK', + // Network only: fetch from the network and update the cache + NETWORK_ONLY = 'NETWORK_ONLY', +} + +export type Fetcher = () => Promise; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts new file mode 100644 index 0000000000..cf29b221dd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts @@ -0,0 +1,157 @@ +import { CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { batchCollab, getCollab } from '@/application/services/js-services/cache'; +import { StrategyType } from '@/application/services/js-services/cache/types'; +import { batchFetchCollab, fetchCollab } from '@/application/services/js-services/fetch'; +import { getCurrentWorkspace } from 'src/application/services/js-services/session'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class JSDatabaseService implements DatabaseService { + private loadedDatabaseId: Set = new Set(); + + private loadedWorkspaceId: Set = new Set(); + + private cacheDatabaseRowDocMap: Map = new Map(); + + constructor() { + // + } + + currentWorkspace() { + return getCurrentWorkspace(); + } + + async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> { + const workspace = await this.currentWorkspace(); + + if (!workspace) { + throw new Error('Workspace database not found'); + } + + const isLoaded = this.loadedWorkspaceId.has(workspace.id); + + const workspaceDatabase = await getCollab( + () => { + return fetchCollab(workspace.id, workspace.workspaceDatabaseId, CollabType.WorkspaceDatabase); + }, + { + collabId: workspace.workspaceDatabaseId, + collabType: CollabType.WorkspaceDatabase, + }, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + ); + + if (!isLoaded) { + this.loadedWorkspaceId.add(workspace.id); + } + + return workspaceDatabase.getMap(YjsEditorKey.data_section).get(YjsEditorKey.workspace_database).toJSON() as { + views: string[]; + database_id: string; + }[]; + } + + async openDatabase(databaseId: string): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const workspace = await this.currentWorkspace(); + + if (!workspace) { + throw new Error('Workspace database not found'); + } + + const workspaceId = workspace.id; + const isLoaded = this.loadedDatabaseId.has(databaseId); + + const rootRowsDoc = + this.cacheDatabaseRowDocMap.get(databaseId) ?? + new Y.Doc({ + guid: databaseId, + }); + + if (!this.cacheDatabaseRowDocMap.has(databaseId)) { + this.cacheDatabaseRowDocMap.set(databaseId, rootRowsDoc); + } + + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + + const databaseDoc = await getCollab( + () => { + return fetchCollab(workspaceId, databaseId, CollabType.Database); + }, + { + collabId: databaseId, + collabType: CollabType.Database, + }, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + ); + + if (!isLoaded) this.loadedDatabaseId.add(databaseId); + + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const rowOrdersIds = rowOrders.toJSON() as { + id: string; + }[]; + + if (!rowOrdersIds) { + throw new Error('Database rows not found'); + } + + const rowsParams = rowOrdersIds.map((item) => ({ + collabId: item.id, + collabType: CollabType.DatabaseRow, + })); + + void batchCollab( + () => { + return batchFetchCollab(workspaceId, rowsParams); + }, + rowsParams, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK, + (id: string, doc: YDoc) => { + if (!rowsFolder.has(id)) { + rowsFolder.set(id, doc); + } + } + ); + + // Update rows if there are new rows added after the database has been loaded + rowOrders?.observe((event) => { + if (event.changes.added.size > 0) { + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + const params = rowIds.map((item) => ({ + collabId: item.id, + collabType: CollabType.DatabaseRow, + })); + + void batchCollab( + () => { + return batchFetchCollab(workspaceId, params); + }, + params, + StrategyType.CACHE_AND_NETWORK, + (id: string, doc: YDoc) => { + if (!rowsFolder.has(id)) { + rowsFolder.set(id, doc); + } + } + ); + } + }); + + return { + databaseDoc, + rows: rowsFolder, + }; + } + + async closeDatabase(databaseId: string) { + this.cacheDatabaseRowDocMap.delete(databaseId); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts b/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts new file mode 100644 index 0000000000..a6f9cf9ee4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/decorator.ts @@ -0,0 +1,60 @@ +/** + * @description: + * * This is a decorator that can be used to read data from storage and fetch data from the server. + * * If the data is already in storage, it will return the data from storage and fetch the data from the server in the background. + * + * @param getStorage A function that returns the data from storage. eg. `() => Promise` + * + * @param setStorage A function that saves the data to storage. eg. `(data: T) => Promise` + * + * @param fetchFunction A function that fetches the data from the server. eg. `(params: P) => Promise` + * + * @returns: A function that returns the data from storage and fetches the data from the server in the background. + */ +export function asyncDataDecorator( + getStorage: () => Promise, + setStorage: (data: T) => Promise, + fetchFunction: (params: P) => Promise +) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + async function fetchData(params: P) { + const data = await fetchFunction(params); + + if (!data) return; + await setStorage(data); + return data; + } + + const originalMethod = descriptor.value; + + descriptor.value = async function (params: P) { + const data = await getStorage(); + + await originalMethod.apply(this, [params]); + if (data) { + void fetchData(params); + return data; + } else { + return fetchData(params); + } + }; + + return descriptor; + }; +} + +export function afterSignInDecorator(successCallback: () => Promise) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + descriptor.value = async function (...args: any[]) { + await originalMethod.apply(this, args); + await successCallback(); + }; + + return descriptor; + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts new file mode 100644 index 0000000000..bcf73ae550 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts @@ -0,0 +1,47 @@ +import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; +import { getCollab } from '@/application/services/js-services/cache'; +import { StrategyType } from '@/application/services/js-services/cache/types'; +import { fetchCollab } from '@/application/services/js-services/fetch'; +import { getCurrentWorkspace } from 'src/application/services/js-services/session'; +import { DocumentService } from '@/application/services/services.type'; + +export class JSDocumentService implements DocumentService { + private loaded: Set = new Set(); + + constructor() { + // + } + + async openDocument(docId: string): Promise { + const workspace = await getCurrentWorkspace(); + + if (!workspace) { + throw new Error('Workspace database not found'); + } + + const isLoaded = this.loaded.has(docId); + + const doc = await getCollab( + () => { + return fetchCollab(workspace.id, docId, CollabType.Document); + }, + { + collabId: docId, + collabType: CollabType.Document, + }, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + ); + + if (!isLoaded) this.loaded.add(docId); + const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); + } + }; + + doc.on('update', handleUpdate); + + return doc; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts b/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts new file mode 100644 index 0000000000..7ae4dc8902 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/fetch.ts @@ -0,0 +1,66 @@ +import { CollabType } from '@/application/collab.type'; +import { APIService } from '@/application/services/js-services/wasm'; + +const pendingRequests = new Map(); + +function generateRequestKey(url: string, params: T) { + if (!params) return url; + + try { + return `${url}_${JSON.stringify(params)}`; + } catch (_e) { + return `${url}_${params}`; + } +} + +// Deduplication fetch requests +// When multiple requests are made to the same URL with the same params, only one request is made +// and the result is shared with all the requests +function fetchWithDeduplication(url: string, params: Req, fetchFunction: () => Promise): Promise { + const requestKey = generateRequestKey(url, params); + + if (pendingRequests.has(requestKey)) { + return pendingRequests.get(requestKey); + } + + const fetchPromise = fetchFunction().finally(() => { + pendingRequests.delete(requestKey); + }); + + pendingRequests.set(requestKey, fetchPromise); + return fetchPromise; +} + +/** + * Fetch collab + * @param workspaceId + * @param id + * @param type [CollabType] + */ +export function fetchCollab(workspaceId: string, id: string, type: CollabType) { + const fetchFunction = () => APIService.getCollab(workspaceId, id, type); + + return fetchWithDeduplication(`fetchCollab_${workspaceId}`, { id, type }, fetchFunction); +} + +/** + * Batch fetch collab + * Usage: + * // load database rows + * const rows = await batchFetchCollab(workspaceId, databaseRows.map((row) => ({ collabId: row.id, collabType: CollabType.DatabaseRow }))); + * + * @param workspaceId + * @param params [{ collabId: string; collabType: CollabType }] + */ +export function batchFetchCollab(workspaceId: string, params: { collabId: string; collabType: CollabType }[]) { + const fetchFunction = () => + APIService.batchGetCollab( + workspaceId, + params.map(({ collabId, collabType }) => ({ + object_id: collabId, + collab_type: collabType, + })) + ); + + return fetchWithDeduplication(`batchFetchCollab_${workspaceId}`, params, fetchFunction); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts new file mode 100644 index 0000000000..f145480c18 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts @@ -0,0 +1,39 @@ +import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; +import { getCollab } from '@/application/services/js-services/cache'; +import { StrategyType } from '@/application/services/js-services/cache/types'; +import { fetchCollab } from '@/application/services/js-services/fetch'; +import { FolderService } from '@/application/services/services.type'; + +export class JSFolderService implements FolderService { + private loaded: Set = new Set(); + + constructor() { + // + } + + async openWorkspace(workspaceId: string): Promise { + const isLoaded = this.loaded.has(workspaceId); + const doc = await getCollab( + () => { + return fetchCollab(workspaceId, workspaceId, CollabType.Folder); + }, + { + collabId: workspaceId, + collabType: CollabType.Folder, + }, + isLoaded ? StrategyType.CACHE_FIRST : StrategyType.CACHE_AND_NETWORK + ); + + if (!isLoaded) this.loaded.add(workspaceId); + const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); + } + }; + + doc.on('update', handleUpdate); + + return doc; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts new file mode 100644 index 0000000000..d31b7f117a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -0,0 +1,54 @@ +import { JSDatabaseService } from '@/application/services/js-services/database.service'; +import { + AFService, + AFServiceConfig, + AuthService, + DatabaseService, + DocumentService, + FolderService, + UserService, +} from '@/application/services/services.type'; +import { JSUserService } from '@/application/services/js-services/user.service'; +import { JSAuthService } from '@/application/services/js-services/auth.service'; +import { JSFolderService } from '@/application/services/js-services/folder.service'; +import { JSDocumentService } from '@/application/services/js-services/document.service'; +import { nanoid } from 'nanoid'; +import { initAPIService } from '@/application/services/js-services/wasm/client_api'; + +export class AFClientService implements AFService { + authService: AuthService; + + userService: UserService; + + documentService: DocumentService; + + folderService: FolderService; + + databaseService: DatabaseService; + + private deviceId: string = nanoid(8); + + private clientId: string = 'web'; + + getDeviceID = (): string => { + return this.deviceId; + }; + + getClientID = (): string => { + return this.clientId; + }; + + constructor(config: AFServiceConfig) { + initAPIService({ + ...config.cloudConfig, + deviceId: this.deviceId, + clientId: this.clientId, + }); + + this.authService = new JSAuthService(); + this.userService = new JSUserService(); + this.documentService = new JSDocumentService(); + this.folderService = new JSFolderService(); + this.databaseService = new JSDatabaseService(); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts new file mode 100644 index 0000000000..dd8d3d1d99 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/session/auth.ts @@ -0,0 +1,3 @@ +export async function signInSuccess() { + // Do nothing +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts new file mode 100644 index 0000000000..c618a85cfd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/session/index.ts @@ -0,0 +1,3 @@ +export * from './token'; +export * from './user'; +export * from './auth'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts new file mode 100644 index 0000000000..e22f980423 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/session/token.ts @@ -0,0 +1,37 @@ +import { notify } from '@/components/_shared/notify'; + +const tokenKey = 'token'; + +export function readTokenStr() { + return sessionStorage.getItem(tokenKey); +} + +export function getAuthInfo() { + const token = readTokenStr() || ''; + + try { + const info = JSON.parse(token); + + return { + uuid: info.user.id, + access_token: info.access_token, + email: info.user.email, + }; + } catch (e) { + return; + } +} + +export function writeToken(token: string) { + if (!token) { + invalidToken(); + return; + } + + sessionStorage.setItem(tokenKey, token); +} + +export function invalidToken() { + sessionStorage.removeItem(tokenKey); + notify.error('Invalid token, please login again'); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts b/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts new file mode 100644 index 0000000000..6fbab3f390 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/session/user.ts @@ -0,0 +1,43 @@ +import { UserProfile, UserWorkspace, Workspace } from '@/application/user.type'; + +const userKey = 'user'; +const workspaceKey = 'workspace'; + +export async function getSignInUser(): Promise { + const userStr = localStorage.getItem(userKey); + + try { + return userStr ? JSON.parse(userStr) : undefined; + } catch (e) { + return undefined; + } +} + +export async function setSignInUser(profile: UserProfile) { + const userStr = JSON.stringify(profile); + + localStorage.setItem(userKey, userStr); +} + +export async function getUserWorkspace(): Promise { + const str = localStorage.getItem(workspaceKey); + + try { + return str ? JSON.parse(str) : undefined; + } catch (e) { + return undefined; + } +} + +export async function setUserWorkspace(workspace: UserWorkspace) { + const str = JSON.stringify(workspace); + + localStorage.setItem(workspaceKey, str); +} + +export async function getCurrentWorkspace(): Promise { + const userProfile = await getSignInUser(); + const userWorkspace = await getUserWorkspace(); + + return userWorkspace?.workspaces.find((workspace) => workspace.id === userProfile?.workspaceId); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts new file mode 100644 index 0000000000..ce912bd50f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts @@ -0,0 +1,45 @@ +import { UserService } from '@/application/services/services.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; +import { APIService } from 'src/application/services/js-services/wasm'; +import { + getAuthInfo, + getSignInUser, + getUserWorkspace, + invalidToken, + setSignInUser, + setUserWorkspace, +} from 'src/application/services/js-services/session'; +import { asyncDataDecorator } from '@/application/services/js-services/decorator'; + +async function getUser() { + try { + const user = await APIService.getUser(); + + return user; + } catch (e) { + console.error(e); + invalidToken(); + } +} + +export class JSUserService implements UserService { + @asyncDataDecorator(getSignInUser, setSignInUser, getUser) + async getUserProfile(): Promise { + if (!getAuthInfo()) { + return Promise.reject('Not authenticated'); + } + + await this.getUserWorkspace(); + + return null!; + } + + async checkUser(): Promise { + return (await getSignInUser()) !== undefined; + } + + @asyncDataDecorator(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace) + async getUserWorkspace(): Promise { + return null!; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts new file mode 100644 index 0000000000..cd09cb74d1 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -0,0 +1,121 @@ +import { CollabType } from '@/application/collab.type'; +import { ClientAPI } from '@appflowyinc/client-api-wasm'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; +import { AFCloudConfig } from '@/application/services/services.type'; +import { invalidToken, readTokenStr, writeToken } from 'src/application/services/js-services/session'; + +let client: ClientAPI; + +export function initAPIService( + config: AFCloudConfig & { + deviceId: string; + clientId: string; + } +) { + window.refresh_token = writeToken; + window.invalid_token = invalidToken; + client = ClientAPI.new({ + base_url: config.baseURL, + ws_addr: config.wsURL, + gotrue_url: config.gotrueURL, + device_id: config.deviceId, + client_id: config.clientId, + configuration: { + compression_quality: 8, + compression_buffer_size: 10240, + }, + }); + + const token = readTokenStr(); + + if (token) { + client.restore_token(token); + } + + client.subscribe(); +} + +export function signIn(email: string, password: string) { + return client.login(email, password); +} + +export function logout() { + return client.logout(); +} + +export async function getUser(): Promise { + try { + const user = await client.get_user(); + + if (!user) { + throw new Error('No user found'); + } + + return { + uid: parseInt(user.uid), + uuid: user.uuid || undefined, + email: user.email || undefined, + name: user.name || undefined, + workspaceId: user.latest_workspace_id, + iconUrl: user.icon_url || undefined, + }; + } catch (e) { + return Promise.reject(e); + } +} + +export async function getCollab(workspaceId: string, object_id: string, collabType: CollabType) { + const res = await client.get_collab({ + workspace_id: workspaceId, + object_id: object_id, + collab_type: Number(collabType) as 0 | 1 | 2 | 3 | 4 | 5, + }); + + const state = new Uint8Array(res.doc_state); + + return { + state, + }; +} + +export async function batchGetCollab( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[] +) { + const res = (await client.batch_get_collab( + workspaceId, + params.map((param) => ({ + object_id: param.object_id, + collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5, + })) + )) as unknown as Map; + + const result: Record = {}; + + res.forEach((value, key) => { + result[key] = value.doc_state; + }); + return result; +} + +export async function getUserWorkspace(): Promise { + const res = await client.get_user_workspace(); + + return { + visitingWorkspaceId: res.visiting_workspace_id, + workspaces: res.workspaces.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.workspace_name, + icon: workspace.icon, + owner: { + id: Number(workspace.owner_uid), + name: workspace.owner_name, + }, + type: workspace.workspace_type, + workspaceDatabaseId: workspace.database_storage_id, + })), + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/index.ts new file mode 100644 index 0000000000..b4f0b4f4cc --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/index.ts @@ -0,0 +1 @@ +export * as APIService from './client_api'; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts new file mode 100644 index 0000000000..1e837f1576 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -0,0 +1,56 @@ +import { YDoc } from '@/application/collab.type'; +import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; +import * as Y from 'yjs'; + +export interface AFService { + getDeviceID: () => string; + getClientID: () => string; + authService: AuthService; + userService: UserService; + documentService: DocumentService; + folderService: FolderService; + databaseService: DatabaseService; +} + +export interface AFServiceConfig { + cloudConfig: AFCloudConfig; +} + +export interface AFCloudConfig { + baseURL: string; + gotrueURL: string; + wsURL: string; +} + +export interface AuthService { + getOAuthURL: (provider: ProviderType) => Promise; + signInWithOAuth: (params: { uri: string }) => Promise; + signupWithEmailPassword: (params: SignUpWithEmailPasswordParams) => Promise; + signinWithEmailPassword: (email: string, password: string) => Promise; + signOut: () => Promise; +} + +export interface DocumentService { + openDocument: (docId: string) => Promise; +} + +export interface DatabaseService { + getWorkspaceDatabases: () => Promise<{ views: string[]; database_id: string }[]>; + openDatabase: ( + databaseId: string, + rowIds?: string[] + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; + closeDatabase: (databaseId: string) => Promise; +} + +export interface UserService { + getUserProfile: () => Promise; + checkUser: () => Promise; +} + +export interface FolderService { + openWorkspace: (workspaceId: string) => Promise; +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts new file mode 100644 index 0000000000..f039782058 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts @@ -0,0 +1,114 @@ +import { AFCloudConfig, AuthService } from '@/application/services/services.type'; +import { + AuthenticatorPB, + OauthProviderPB, + OauthSignInPB, + SignInPayloadPB, + SignUpPayloadPB, + UserEventGetOauthURLWithProvider, + UserEventOauthSignIn, + UserEventSignInWithEmailPassword, + UserEventSignOut, + UserEventSignUp, + UserProfilePB, +} from './backend/events/flowy-user'; +import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; + +export class TauriAuthService implements AuthService { + + constructor (private cloudConfig: AFCloudConfig, private clientConfig: { + deviceId: string; + clientId: string; + + }) {} + + getDeviceID = (): string => { + return this.clientConfig.deviceId; + }; + getOAuthURL = async (provider: ProviderType): Promise => { + const providerDataRes = await UserEventGetOauthURLWithProvider( + OauthProviderPB.fromObject({ + provider: provider as number, + }), + ); + + if (!providerDataRes.ok) { + throw new Error(providerDataRes.val.msg); + } + + const providerData = providerDataRes.val; + + return providerData.oauth_url; + }; + + signInWithOAuth = async ({ uri }: { uri: string }): Promise => { + const payload = OauthSignInPB.fromObject({ + authenticator: AuthenticatorPB.AppFlowyCloud, + map: { + sign_in_url: uri, + device_id: this.getDeviceID(), + }, + }); + + const res = await UserEventOauthSignIn(payload); + + if (!res.ok) { + throw new Error(res.val.msg); + } + + return; + }; + signinWithEmailPassword = async (email: string, password: string): Promise => { + const payload = SignInPayloadPB.fromObject({ + email, + password, + }); + + const res = await UserEventSignInWithEmailPassword(payload); + + if (!res.ok) { + return Promise.reject(res.val.msg); + } + + return; + }; + + signupWithEmailPassword = async (params: SignUpWithEmailPasswordParams): Promise => { + const payload = SignUpPayloadPB.fromObject({ + name: params.name, + email: params.email, + password: params.password, + device_id: this.getDeviceID(), + }); + + const res = await UserEventSignUp(payload); + + if (!res.ok) { + return Promise.reject(res.val.msg); + } + + return; + }; + + signOut = async () => { + const res = await UserEventSignOut(); + + if (!res.ok) { + return Promise.reject(res.val.msg); + } + + return; + }; +} + +export function parseUserProfileFrom (userPB: UserProfilePB): UserProfile { + const user = userPB.toObject(); + + return { + uid: user.id as number, + email: user.email, + name: user.name, + iconUrl: user.icon_url, + workspaceId: user.workspace_id, + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts new file mode 100644 index 0000000000..38a126a402 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts @@ -0,0 +1,7 @@ +export * from "./models/flowy-user"; +export * from "./models/flowy-database2"; +export * from "./models/flowy-folder"; +export * from "./models/flowy-document"; +export * from "./models/flowy-error"; +export * from "./models/flowy-config"; +export * from "./models/flowy-date"; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts new file mode 100644 index 0000000000..d7909679fb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts @@ -0,0 +1,24 @@ +import { YDoc } from '@/application/collab.type'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class TauriDatabaseService implements DatabaseService { + constructor() { + // + } + + async getWorkspaceDatabases(): Promise<{ views: string[]; database_id: string }[]> { + return Promise.reject('Not implemented'); + } + + async closeDatabase(_databaseId: string) { + return Promise.reject('Not implemented'); + } + + async openDatabase(_viewId: string): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts new file mode 100644 index 0000000000..9ae2987350 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts @@ -0,0 +1,8 @@ +import { DocumentService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class TauriDocumentService implements DocumentService { + async openDocument(_id: string): Promise { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts new file mode 100644 index 0000000000..868e6f1391 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/folder.service.ts @@ -0,0 +1,12 @@ +import { YDoc } from '@/application/collab.type'; +import { FolderService } from '@/application/services/services.type'; + +export class TauriFolderService implements FolderService { + constructor() { + // + } + + async openWorkspace(_workspaceId: string): Promise { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts new file mode 100644 index 0000000000..8908c002ee --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -0,0 +1,50 @@ +import { + AFService, + AFServiceConfig, + AuthService, + DatabaseService, + DocumentService, + FolderService, + UserService, +} from '@/application/services/services.type'; +import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; +import { TauriDatabaseService } from '@/application/services/tauri-services/database.service'; +import { TauriFolderService } from '@/application/services/tauri-services/folder.service'; +import { TauriUserService } from '@/application/services/tauri-services/user.service'; +import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; +import { nanoid } from 'nanoid'; + +export class AFClientService implements AFService { + authService: AuthService; + + userService: UserService; + + documentService: DocumentService; + + folderService: FolderService; + + databaseService: DatabaseService; + + private deviceId: string = nanoid(8); + + private clientId: string = 'web'; + + getDeviceID = (): string => { + return this.deviceId; + }; + + getClientID = (): string => { + return this.clientId; + }; + + constructor(config: AFServiceConfig) { + this.authService = new TauriAuthService(config.cloudConfig, { + deviceId: this.deviceId, + clientId: this.clientId, + }); + this.userService = new TauriUserService(); + this.documentService = new TauriDocumentService(); + this.folderService = new TauriFolderService(); + this.databaseService = new TauriDatabaseService(); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts new file mode 100644 index 0000000000..383e648052 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts @@ -0,0 +1,20 @@ +import { UserService } from '@/application/services/services.type'; +import { UserProfile } from '@/application/user.type'; +import { UserEventGetUserProfile } from './backend/events/flowy-user'; +import { parseUserProfileFrom } from '@/application/services/tauri-services/auth.service'; + +export class TauriUserService implements UserService { + async getUserProfile(): Promise { + const res = await UserEventGetUserProfile(); + + if (res.ok) { + return parseUserProfileFrom(res.val); + } + + return null; + } + + async checkUser(): Promise { + return Promise.resolve(false); + } +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/applyRemoteEvents.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/applyRemoteEvents.ts new file mode 100644 index 0000000000..62c24c12b8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/applyRemoteEvents.ts @@ -0,0 +1,49 @@ +import { CollabOrigin } from '@/application/collab.type'; +import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert'; +import { generateId, insertBlock, withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor'; +import { createEditor } from 'slate'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +export async function runApplyRemoteEventsTest() { + const pageId = generateId(); + const remoteDoc = withTestingYDoc(pageId); + const remote = withTestingYjsEditor(createEditor(), remoteDoc); + + const localDoc = new Y.Doc(); + + Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc)); + const editor = withTestingYjsEditor(createEditor(), localDoc); + + editor.connect(); + expect(editor.children).toEqual(remote.children); + + // update remote doc + const id = generateId(); + + const { applyDelta } = insertBlock({ + doc: remoteDoc, + blockObject: { + id, + ty: 'paragraph', + relation_id: id, + text_id: id, + data: JSON.stringify({ level: 1 }), + }, + }); + + applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]); + + remote.children = yDocToSlateContent(remoteDoc)?.children ?? []; + + // apply remote changes to local doc + Y.transact( + localDoc, + () => { + Y.applyUpdateV2(localDoc, Y.encodeStateAsUpdateV2(remoteDoc)); + }, + CollabOrigin.Remote + ); + + expect(editor.children).toEqual(remote.children); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts new file mode 100644 index 0000000000..0e473517d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.test.ts @@ -0,0 +1,268 @@ +import { generateId, getTestingDocData, insertBlock, withTestingYDoc } from './withTestingYjsEditor'; +import { yDocToSlateContent, deltaInsertToSlateNode, yDataToSlateContent } from '@/application/slate-yjs/utils/convert'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +describe('convert yjs data to slate content', () => { + it('should return undefined if root block is not exist', () => { + const doc = new Y.Doc(); + + expect(() => yDocToSlateContent(doc)).toThrowError(); + + const doc2 = withTestingYDoc('1'); + const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc2); + expect(yDataToSlateContent({ blocks, rootId: '2', childrenMap, textMap })).toBeUndefined(); + + blocks.delete(pageId); + + expect(yDataToSlateContent({ blocks, rootId: pageId, childrenMap, textMap })).toBeUndefined(); + }); + it('should match empty array', () => { + const doc = withTestingYDoc('1'); + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toMatchObject([]); + }); + it('should match single paragraph', () => { + const doc = withTestingYDoc('1'); + const id = generateId(); + + const { applyDelta } = insertBlock({ + doc, + blockObject: { + id, + ty: 'paragraph', + relation_id: id, + text_id: id, + data: JSON.stringify({ level: 1 }), + }, + }); + + applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]); + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toEqual([ + { + blockId: id, + relationId: id, + type: 'paragraph', + data: { level: 1 }, + children: [ + { + textId: id, + type: 'text', + children: [{ text: 'Hello ' }, { text: 'World', bold: true }], + }, + ], + }, + ]); + }); + it('should match nesting paragraphs', () => { + const doc = withTestingYDoc('1'); + const id1 = generateId(); + const id2 = generateId(); + + const { applyDelta, appendChild } = insertBlock({ + doc, + blockObject: { + id: id1, + ty: 'paragraph', + relation_id: id1, + text_id: id1, + data: '', + }, + }); + + applyDelta([{ insert: 'Hello ' }, { insert: 'World', attributes: { bold: true } }]); + appendChild({ + id: id2, + ty: 'paragraph', + relation_id: id2, + text_id: id2, + data: '', + }).applyDelta([{ insert: 'I am nested' }]); + + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toEqual([ + { + blockId: id1, + relationId: id1, + type: 'paragraph', + data: {}, + children: [ + { + textId: id1, + type: 'text', + children: [{ text: 'Hello ' }, { text: 'World', bold: true }], + }, + { + blockId: id2, + relationId: id2, + type: 'paragraph', + data: {}, + children: [{ textId: id2, type: 'text', children: [{ text: 'I am nested' }] }], + }, + ], + }, + ]); + }); + it('should compatible with delta in data', () => { + const doc = withTestingYDoc('1'); + const id = generateId(); + + insertBlock({ + doc, + blockObject: { + id, + ty: 'paragraph', + relation_id: id, + text_id: id, + data: JSON.stringify({ + delta: [ + { insert: 'Hello ' }, + { insert: 'World', attributes: { bold: true } }, + { insert: ' ', attributes: { code: true } }, + ], + }), + }, + }); + + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toEqual([ + { + blockId: id, + relationId: id, + type: 'paragraph', + data: { + delta: [ + { insert: 'Hello ' }, + { insert: 'World', attributes: { bold: true } }, + { + insert: ' ', + attributes: { code: true }, + }, + ], + }, + children: [ + { + textId: id, + type: 'text', + children: [{ text: 'Hello ' }, { text: 'World', bold: true }, { text: ' ', code: true }], + }, + { + text: '', + }, + ], + }, + ]); + }); + it('should return undefined if data is invalid', () => { + const doc = withTestingYDoc('1'); + const id = generateId(); + + insertBlock({ + doc, + blockObject: { + id, + ty: 'paragraph', + relation_id: id, + text_id: id, + data: 'invalid', + }, + }); + + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toEqual([undefined]); + }); + it('should return a normalize node if the delta is not exist', () => { + const doc = withTestingYDoc('1'); + const id = generateId(); + + insertBlock({ + doc, + blockObject: { + id, + ty: 'paragraph', + relation_id: id, + text_id: id, + data: JSON.stringify({}), + }, + }); + + const slateContent = yDocToSlateContent(doc)!; + + expect(slateContent).not.toBeUndefined(); + expect(slateContent.children).toEqual([ + { + blockId: id, + relationId: id, + type: 'paragraph', + data: {}, + children: [{ text: '' }], + }, + ]); + }); +}); + +describe('test deltaInsertToSlateNode', () => { + it('should match text node', () => { + const node = deltaInsertToSlateNode({ insert: 'Hello' }); + + expect(node).toEqual({ text: 'Hello' }); + }); + + it('should match text node with attributes', () => { + const node = deltaInsertToSlateNode({ insert: 'Hello', attributes: { bold: true } }); + + expect(node).toEqual({ text: 'Hello', bold: true }); + }); + + it('should delete empty string attributes', () => { + const node = deltaInsertToSlateNode({ insert: 'Hello', attributes: { bold: false, font_color: '' } }); + + expect(node).toEqual({ text: 'Hello' }); + }); + + it('should generate formula inline node', () => { + const node = deltaInsertToSlateNode({ + insert: '$$', + attributes: { formula: 'world' }, + }); + + expect(node).toEqual([ + { + type: 'formula', + data: 'world', + children: [{ text: '$' }], + }, + { + type: 'formula', + data: 'world', + children: [{ text: '$' }], + }, + ]); + }); + + it('should generate mention inline node', () => { + const node = deltaInsertToSlateNode({ + insert: '@', + attributes: { mention: 'world' }, + }); + + expect(node).toEqual([ + { + type: 'mention', + data: 'world', + children: [{ text: '@' }], + }, + ]); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.ts new file mode 100644 index 0000000000..6aa830b6b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/convert.ts @@ -0,0 +1,72 @@ +import { withTestingYDoc, withTestingYjsEditor } from './withTestingYjsEditor'; +import { yDocToSlateContent } from '../utils/convert'; +import { createEditor, Editor } from 'slate'; +import { expect } from '@jest/globals'; +import * as Y from 'yjs'; + +function normalizedSlateDoc(doc: Y.Doc) { + const editor = createEditor(); + + const yjsEditor = withTestingYjsEditor(editor, doc); + + editor.children = yDocToSlateContent(doc)?.children ?? []; + return yjsEditor.children; +} + +export async function runCollaborationTest() { + const doc = withTestingYDoc('1'); + const editor = createEditor(); + const yjsEditor = withTestingYjsEditor(editor, doc); + + // Keep the 'local' editor state before applying run. + const baseState = Y.encodeStateAsUpdateV2(doc); + + Editor.normalize(editor, { force: true }); + + expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children); + + // Setup remote editor with input base state + const remoteDoc = new Y.Doc(); + + Y.applyUpdateV2(remoteDoc, baseState); + const remote = withTestingYjsEditor(createEditor(), remoteDoc); + + // Apply changes from 'run' + Y.applyUpdateV2(remoteDoc, Y.encodeStateAsUpdateV2(yjsEditor.sharedRoot.doc!)); + + // Verify remote and editor state are equal + expect(normalizedSlateDoc(remoteDoc)).toEqual(remote.children); + expect(yjsEditor.children).toEqual(remote.children); + expect(normalizedSlateDoc(doc)).toEqual(yjsEditor.children); +} + +export function runLocalChangeTest() { + const doc = withTestingYDoc('1'); + const editor = withTestingYjsEditor(createEditor(), doc); + + editor.connect(); + + editor.insertNode( + { + type: 'paragraph', + blockId: '1', + children: [ + { + textId: '1', + type: 'text', + children: [{ text: 'Hello' }], + }, + ], + }, + { + at: [0], + } + ); + + editor.apply({ + type: 'set_selection', + properties: {}, + newProperties: { anchor: { path: [0, 0], offset: 5 }, focus: { path: [0, 0], offset: 5 } }, + }); + // expect(editor.children).toEqual(yDocToSlateContent(doc)?.children); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/index.test.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/index.test.ts new file mode 100644 index 0000000000..f261b2244f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/index.test.ts @@ -0,0 +1,67 @@ +import { runCollaborationTest, runLocalChangeTest } from './convert'; +import { runApplyRemoteEventsTest } from './applyRemoteEvents'; +import { + getTestingDocData, + withTestingYDoc, + withTestingYjsEditor, +} from '@/application/slate-yjs/__tests__/withTestingYjsEditor'; +import { createEditor } from 'slate'; +import Y from 'yjs'; +import { expect } from '@jest/globals'; +import { YjsEditor } from '@/application/slate-yjs'; + +describe('slate-yjs adapter', () => { + it('should pass the collaboration test', async () => { + await runCollaborationTest(); + }); + + it('should pass the apply remote events test', async () => { + await runApplyRemoteEventsTest(); + }); + + it('should store local changes', () => { + runLocalChangeTest(); + }); + + it('should throw error when already connected', () => { + const doc = withTestingYDoc('1'); + const editor = withTestingYjsEditor(createEditor(), doc); + editor.connect(); + expect(() => editor.connect()).toThrowError(); + }); + + it('should re connect after disconnect', () => { + const doc = withTestingYDoc('1'); + const editor = withTestingYjsEditor(createEditor(), doc); + editor.connect(); + editor.disconnect(); + expect(() => editor.connect()).not.toThrowError(); + }); + + it('should ensure the editor is connected before disconnecting', () => { + const doc = withTestingYDoc('1'); + const editor = withTestingYjsEditor(createEditor(), doc); + expect(() => editor.disconnect()).toThrowError(); + }); + + it('should have been called', () => { + const doc = withTestingYDoc('1'); + const editor = withTestingYjsEditor(createEditor(), doc); + editor.connect = jest.fn(); + YjsEditor.connect(editor); + expect(editor.connect).toHaveBeenCalled(); + + editor.disconnect = jest.fn(); + YjsEditor.disconnect(editor); + expect(editor.disconnect).toHaveBeenCalled(); + }); + + it('should can not be converted to slate content', () => { + const doc = withTestingYDoc('1'); + const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc); + blocks.delete(pageId); + const editor = withTestingYjsEditor(createEditor(), doc); + YjsEditor.connect(editor); + expect(editor.children).toEqual([]); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/withTestingYjsEditor.ts b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/withTestingYjsEditor.ts new file mode 100644 index 0000000000..81ffce4f9e --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/__tests__/withTestingYjsEditor.ts @@ -0,0 +1,135 @@ +import { + CollabOrigin, + YBlocks, + YChildrenMap, + YjsEditorKey, + YMeta, + YSharedRoot, + YTextMap, +} from '@/application/collab.type'; +import { withYjs } from '@/application/slate-yjs'; +import { YDelta } from '@/application/slate-yjs/utils/convert'; +import { Editor } from 'slate'; +import * as Y from 'yjs'; +import { v4 as uuidv4 } from 'uuid'; + +export function generateId() { + return uuidv4(); +} + +export function withTestingYjsEditor(editor: Editor, doc: Y.Doc) { + const yjdEditor = withYjs(editor, doc, { + localOrigin: CollabOrigin.LocalSync, + }); + + return yjdEditor; +} + +export function getTestingDocData(doc: Y.Doc) { + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + const document = sharedRoot.get(YjsEditorKey.document); + const blocks = document.get(YjsEditorKey.blocks) as YBlocks; + const meta = document.get(YjsEditorKey.meta) as YMeta; + const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap; + const textMap = meta.get(YjsEditorKey.text_map) as YTextMap; + const pageId = document.get(YjsEditorKey.page_id) as string; + + return { + sharedRoot, + document, + blocks, + meta, + childrenMap, + textMap, + pageId, + }; +} + +export function withTestingYDoc(docId: string) { + const doc = new Y.Doc(); + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + const document = new Y.Map(); + const blocks = new Y.Map(); + const meta = new Y.Map(); + const children_map = new Y.Map(); + const text_map = new Y.Map(); + const rootBlock = new Y.Map(); + const blockOrders = new Y.Array(); + const pageId = docId; + + sharedRoot.set(YjsEditorKey.document, document); + document.set(YjsEditorKey.page_id, pageId); + document.set(YjsEditorKey.blocks, blocks); + document.set(YjsEditorKey.meta, meta); + meta.set(YjsEditorKey.children_map, children_map); + meta.set(YjsEditorKey.text_map, text_map); + children_map.set(pageId, blockOrders); + blocks.set(pageId, rootBlock); + rootBlock.set(YjsEditorKey.block_id, pageId); + rootBlock.set(YjsEditorKey.block_children, pageId); + rootBlock.set(YjsEditorKey.block_type, 'page'); + rootBlock.set(YjsEditorKey.block_data, '{}'); + rootBlock.set(YjsEditorKey.block_external_id, ''); + return doc; +} + +export interface BlockObject { + id: string; + ty: string; + relation_id: string; + text_id: string; + data: string; +} + +export function insertBlock({ + doc, + parentBlockId, + prevBlockId, + blockObject, +}: { + doc: Y.Doc; + parentBlockId?: string; + prevBlockId?: string; + blockObject: BlockObject; +}) { + const { blocks, childrenMap, textMap, pageId } = getTestingDocData(doc); + const block = new Y.Map(); + const { id, ty, relation_id, text_id, data } = blockObject; + + block.set(YjsEditorKey.block_id, id); + block.set(YjsEditorKey.block_type, ty); + block.set(YjsEditorKey.block_children, relation_id); + block.set(YjsEditorKey.block_external_id, text_id); + block.set(YjsEditorKey.block_data, data); + blocks.set(id, block); + + const blockParentId = parentBlockId || pageId; + const blockParentChildren = childrenMap.get(blockParentId); + const index = prevBlockId ? blockParentChildren.toArray().indexOf(prevBlockId) + 1 : 0; + + blockParentChildren.insert(index, [id]); + + return { + applyDelta: (delta: YDelta[]) => { + let text = textMap.get(text_id); + + if (!text) { + text = new Y.Text(); + textMap.set(text_id, text); + } + + text.applyDelta(delta); + }, + appendChild: (childBlock: BlockObject) => { + if (!childrenMap.has(relation_id)) { + childrenMap.set(relation_id, new Y.Array()); + } + + return insertBlock({ + doc, + parentBlockId: id, + blockObject: childBlock, + }); + }, + }; +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/index.ts b/frontend/appflowy_web_app/src/application/slate-yjs/index.ts new file mode 100644 index 0000000000..715957a727 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/index.ts @@ -0,0 +1 @@ +export * from './plugins/withYjs'; diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts new file mode 100644 index 0000000000..837063fd1d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -0,0 +1,161 @@ +import { CollabOrigin, YjsEditorKey, YSharedRoot } from '@/application/collab.type'; +import { applyToYjs } from '@/application/slate-yjs/utils/applyToYjs'; +import { Editor, Operation, Descendant } from 'slate'; +import Y, { YEvent, Transaction } from 'yjs'; +import { yDocToSlateContent } from '@/application/slate-yjs/utils/convert'; + +type LocalChange = { + op: Operation; + slateContent: Descendant[]; +}; + +export interface YjsEditor extends Editor { + connect: () => void; + disconnect: () => void; + sharedRoot: YSharedRoot; + applyRemoteEvents: (events: Array>, transaction: Transaction) => void; + flushLocalChanges: () => void; + storeLocalChange: (op: Operation) => void; +} + +const connectSet = new WeakSet(); + +const localChanges = new WeakMap(); + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const YjsEditor = { + connected(editor: YjsEditor): boolean { + return connectSet.has(editor); + }, + + connect(editor: YjsEditor): void { + editor.connect(); + }, + + disconnect(editor: YjsEditor): void { + editor.disconnect(); + }, + + applyRemoteEvents(editor: YjsEditor, events: Array>, transaction: Transaction): void { + editor.applyRemoteEvents(events, transaction); + }, + + localChanges(editor: YjsEditor): LocalChange[] { + return localChanges.get(editor) ?? []; + }, + + storeLocalChange(editor: YjsEditor, op: Operation): void { + editor.storeLocalChange(op); + }, + + flushLocalChanges(editor: YjsEditor): void { + editor.flushLocalChanges(); + }, +}; + +export function withYjs( + editor: T, + doc: Y.Doc, + opts?: { + localOrigin: CollabOrigin; + } +): T & YjsEditor { + const { localOrigin = CollabOrigin.Local } = opts ?? {}; + const e = editor as T & YjsEditor; + const { apply, onChange } = e; + + e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + const initializeDocumentContent = () => { + const content = yDocToSlateContent(doc); + + if (!content) { + return; + } + + e.children = content.children; + + console.log('initializeDocumentContent', doc.getMap(YjsEditorKey.data_section).toJSON(), e.children); + Editor.normalize(editor, { force: true }); + }; + + const applyIntercept = (op: Operation) => { + if (YjsEditor.connected(e)) { + YjsEditor.storeLocalChange(e, op); + } + + apply(op); + }; + + const applyRemoteIntercept = (op: Operation) => { + apply(op); + }; + + e.applyRemoteEvents = (_events: Array>, _: Transaction) => { + // Flush local changes to ensure all local changes are applied before processing remote events + YjsEditor.flushLocalChanges(e); + // Replace the apply function to avoid storing remote changes as local changes + e.apply = applyRemoteIntercept; + + // Initialize or update the document content to ensure it is in the correct state before applying remote events + initializeDocumentContent(); + + // Restore the apply function to store local changes after applying remote changes + e.apply = applyIntercept; + }; + + const handleYEvents = (events: Array>, transaction: Transaction) => { + if (transaction.origin === CollabOrigin.Remote) { + YjsEditor.applyRemoteEvents(e, events, transaction); + } + }; + + e.connect = () => { + if (YjsEditor.connected(e)) { + throw new Error('Already connected'); + } + + initializeDocumentContent(); + e.sharedRoot.observeDeep(handleYEvents); + connectSet.add(e); + }; + + e.disconnect = () => { + if (!YjsEditor.connected(e)) { + throw new Error('Not connected'); + } + + e.sharedRoot.unobserveDeep(handleYEvents); + connectSet.delete(e); + }; + + e.storeLocalChange = (op) => { + const changes = localChanges.get(e) ?? []; + + localChanges.set(e, [...changes, { op, slateContent: e.children }]); + }; + + e.flushLocalChanges = () => { + const changes = YjsEditor.localChanges(e); + + localChanges.delete(e); + // parse changes and apply to ydoc + doc.transact(() => { + changes.forEach((change) => { + applyToYjs(doc, { children: change.slateContent }, change.op); + }); + }, localOrigin); + }; + + e.apply = applyIntercept; + + e.onChange = () => { + if (YjsEditor.connected(e)) { + YjsEditor.flushLocalChanges(e); + } + + onChange(); + }; + + return e; +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts new file mode 100644 index 0000000000..98daed4817 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applyToYjs.ts @@ -0,0 +1,8 @@ +import { Operation, Node } from 'slate'; +import * as Y from 'yjs'; + +// transform slate op to yjs op and apply it to ydoc +export function applyToYjs(_ydoc: Y.Doc, _slateRoot: Node, op: Operation) { + if (op.type === 'set_selection') return; + console.log('applySlateOp', op); +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts new file mode 100644 index 0000000000..67defd6acc --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/convert.ts @@ -0,0 +1,208 @@ +import { + InlineBlockType, + YBlocks, + YChildrenMap, + YSharedRoot, + YDoc, + YjsEditorKey, + YMeta, + YTextMap, + BlockData, + BlockType, +} from '@/application/collab.type'; +import { BlockJson } from '@/application/slate-yjs/utils/types'; +import { Element, Text } from 'slate'; + +export function yDataToSlateContent({ + blocks, + rootId, + childrenMap, + textMap, +}: { + blocks: YBlocks; + childrenMap: YChildrenMap; + textMap: YTextMap; + rootId: string; +}): Element | undefined { + function traverse(id: string) { + const block = blocks.get(id).toJSON() as BlockJson; + const childrenId = block.children as string; + + const children = (childrenMap.get(childrenId)?.toJSON() ?? []).map(traverse) as (Element | Text)[]; + + const slateNode = blockToSlateNode(block); + + slateNode.children = children; + + if (slateNode.type === BlockType.Page) { + return slateNode; + } + + let textId = block.external_id as string; + + let delta; + + const yText = textId ? textMap.get(textId) : undefined; + + if (!yText) { + if (children.length === 0) { + children.push({ + text: '', + }); + } + + // Compatible data + // The old version of delta data is fully covered through the data field + if (slateNode.data) { + const data = slateNode.data as BlockData; + + if (YjsEditorKey.delta in data) { + textId = block.id; + delta = data.delta; + } else { + return slateNode; + } + } + } else { + delta = yText.toDelta(); + } + + try { + const slateDelta = delta.flatMap(deltaInsertToSlateNode); + + const textNode: Element = { + textId, + type: YjsEditorKey.text, + children: slateDelta, + }; + + children.unshift(textNode); + return slateNode; + } catch (e) { + return; + } + } + + const root = blocks.get(rootId); + + if (!root) return; + + const result = traverse(rootId); + + if (!result) return; + + return result; +} + +export function yDocToSlateContent(doc: YDoc): Element | undefined { + const sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + const document = sharedRoot.get(YjsEditorKey.document); + const pageId = document.get(YjsEditorKey.page_id) as string; + const blocks = document.get(YjsEditorKey.blocks) as YBlocks; + const meta = document.get(YjsEditorKey.meta) as YMeta; + const childrenMap = meta.get(YjsEditorKey.children_map) as YChildrenMap; + const textMap = meta.get(YjsEditorKey.text_map) as YTextMap; + + return yDataToSlateContent({ + blocks, + rootId: pageId, + childrenMap, + textMap, + }); +} + +export function blockToSlateNode(block: BlockJson): Element { + const data = block.data; + let blockData; + + try { + blockData = data ? JSON.parse(data) : {}; + } catch (e) { + // do nothing + } + + return { + blockId: block.id, + relationId: block.children, + data: blockData, + type: block.ty, + children: [], + }; +} + +export interface YDelta { + insert: string; + attributes?: Record; +} + +export function deltaInsertToSlateNode({ attributes, insert }: YDelta): Element | Text | Element[] { + const matchInlines = transformToInlineElement({ + insert, + attributes, + }); + + if (matchInlines.length > 0) { + return matchInlines; + } + + if (attributes) { + dealWithEmptyAttribute(attributes); + } + + return { + ...attributes, + text: insert, + }; +} + +function dealWithEmptyAttribute(attributes: Record) { + for (const key in attributes) { + if (!attributes[key]) { + delete attributes[key]; + } + } +} + +export function transformToInlineElement(op: YDelta): Element[] { + const attributes = op.attributes; + + if (!attributes) return []; + const { formula, mention, ...attrs } = attributes; + + if (formula) { + const texts = op.insert.split(''); + + return texts.map((text) => { + return { + type: InlineBlockType.Formula, + data: formula, + children: [ + { + text, + ...attrs, + }, + ], + }; + }); + } + + if (mention) { + const texts = op.insert.split(''); + + return texts.map((text) => { + return { + type: InlineBlockType.Mention, + data: mention, + children: [ + { + text, + ...attrs, + }, + ], + }; + }); + } + + return []; +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts new file mode 100644 index 0000000000..0dbe81a970 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/types.ts @@ -0,0 +1,29 @@ +import { Node as SlateNode } from 'slate'; + +export interface BlockJson { + id: string; + ty: string; + data?: string; + children?: string; + external_id?: string; +} + +export interface Operation { + type: OperationType; +} + +export enum OperationType { + InsertNode = 'insert_node', + InsertChildren = 'insert_children', +} + +export interface InsertNodeOperation extends Operation { + type: OperationType.InsertNode; + node: SlateNode; +} + +export interface InsertChildrenOperation extends Operation { + type: OperationType.InsertChildren; + blockId: string; + children: string[]; +} diff --git a/frontend/appflowy_web_app/src/application/user.type.ts b/frontend/appflowy_web_app/src/application/user.type.ts new file mode 100644 index 0000000000..e2c3bcdb43 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/user.type.ts @@ -0,0 +1,75 @@ +export enum Authenticator { + Local = 0, + Supabase = 1, + AppFlowyCloud = 2, +} + +export enum EncryptionType { + NoEncryption = 0, + Symmetric = 1, +} + +export interface UserProfile { + uid: number; + uuid?: string; + email?: string; + name?: string; + iconUrl?: string; + workspaceId?: string; +} + +export interface UserWorkspace { + visitingWorkspaceId: string; + workspaces: Workspace[]; +} + +export interface Workspace { + id: string; + name: string; + icon: string; + owner: { + id: number; + name: string; + }; + type: number; + workspaceDatabaseId: string; +} + +export interface SignUpWithEmailPasswordParams { + name: string; + email: string; + password: string; +} + +export enum ProviderType { + Apple = 0, + Azure = 1, + Bitbucket = 2, + Discord = 3, + Facebook = 4, + Figma = 5, + Github = 6, + Gitlab = 7, + Google = 8, + Keycloak = 9, + Kakao = 10, + Linkedin = 11, + Notion = 12, + Spotify = 13, + Slack = 14, + Workos = 15, + Twitch = 16, + Twitter = 17, + Email = 18, + Phone = 19, + Zoom = 20, +} + +export interface UserSetting { + workspaceId: string; + latestView?: { + id: string; + name: string; + }; + hasLatestView: boolean; +} diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts new file mode 100644 index 0000000000..8a332f4b60 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts @@ -0,0 +1,18 @@ +import { YjsEditorKey } from '@/application/collab.type'; +import { applyYDoc } from '@/application/ydoc/apply'; +import * as Y from 'yjs'; +import * as docJson from '../../../../../cypress/fixtures/simple_doc.json'; + +describe('apply document', () => { + it('should apply document', () => { + const collab = new Y.Doc(); + const data = collab.getMap(YjsEditorKey.data_section); + const document = new Y.Map(); + data.set(YjsEditorKey.document, document); + + const state = new Uint8Array(docJson.data.doc_state); + applyYDoc(collab, state); + }); +}); + +export {}; diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts new file mode 100644 index 0000000000..b19cb43328 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts @@ -0,0 +1,18 @@ +import { CollabOrigin } from '@/application/collab.type'; +import * as Y from 'yjs'; + +/** + * Apply doc state from server to client + * Note: origin is always remote + * @param doc local Y.Doc + * @param state state from server + */ +export function applyYDoc(doc: Y.Doc, state: Uint8Array) { + Y.transact( + doc, + () => { + Y.applyUpdate(doc, state); + }, + CollabOrigin.Remote + ); +} diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png new file mode 100644 index 0000000000..fb72022287 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_1.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png new file mode 100644 index 0000000000..9ecf02d253 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_2.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png new file mode 100644 index 0000000000..97072b04f4 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_3.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png new file mode 100644 index 0000000000..00d26a0500 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_4.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png new file mode 100644 index 0000000000..3ecc9546c1 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_5.png differ diff --git a/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png new file mode 100644 index 0000000000..0abd2700e8 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/cover/m_cover_image_6.png differ diff --git a/frontend/appflowy_web_app/src/assets/information.svg b/frontend/appflowy_web_app/src/assets/information.svg new file mode 100644 index 0000000000..37ca4d5837 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/information.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/logo.svg b/frontend/appflowy_web_app/src/assets/logo.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/settings/discord.png b/frontend/appflowy_web_app/src/assets/settings/discord.png new file mode 100644 index 0000000000..f71e68c6ed Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/discord.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/github.png b/frontend/appflowy_web_app/src/assets/settings/github.png new file mode 100644 index 0000000000..597883b7a3 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/github.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/google.png b/frontend/appflowy_web_app/src/assets/settings/google.png new file mode 100644 index 0000000000..60032628a8 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/google.png differ diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx new file mode 100644 index 0000000000..37bb03533b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/context-provider/FolderProvider.tsx @@ -0,0 +1,23 @@ +import { YFolder } from '@/application/collab.type'; +import { Crumb, FolderContext } from '@/application/folder-yjs'; + +export const FolderProvider: React.FC<{ + folder: YFolder | null; + children?: React.ReactNode; + onNavigateToView?: (viewId: string) => void; + crumbs?: Crumb[]; + setCrumbs?: React.Dispatch>; +}> = ({ folder, children, onNavigateToView, crumbs, setCrumbs }) => { + return ( + + {children} + + ); +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx new file mode 100644 index 0000000000..666554ff73 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx @@ -0,0 +1,17 @@ +import { useContext, createContext } from 'react'; + +export const IdContext = createContext(null); + +interface IdProviderProps { + objectId: string; +} + +export const IdProvider = ({ children, ...props }: IdProviderProps & { children: React.ReactNode }) => { + return {children}; +}; + +const defaultIdValue = {} as IdProviderProps; + +export function useId() { + return useContext(IdContext) || defaultIdValue; +} diff --git a/frontend/appflowy_web_app/src/components/_shared/katex-math/KatexMath.tsx b/frontend/appflowy_web_app/src/components/_shared/katex-math/KatexMath.tsx new file mode 100644 index 0000000000..e6c7cac5ed --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/katex-math/KatexMath.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import 'katex/dist/katex.min.css'; +import { BlockMath, InlineMath } from 'react-katex'; +import './index.css'; + +function KatexMath({ latex, isInline = false }: { latex: string; isInline?: boolean }) { + return isInline ? ( + + ) : ( + { + return ( +
+ {error.name}: {error.message} +
+ ); + }} + > + {latex} +
+ ); +} + +export default KatexMath; diff --git a/frontend/appflowy_web_app/src/components/_shared/katex-math/index.css b/frontend/appflowy_web_app/src/components/_shared/katex-math/index.css new file mode 100644 index 0000000000..d127dc343b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/katex-math/index.css @@ -0,0 +1,4 @@ + +.katex-html { + white-space: normal; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx new file mode 100644 index 0000000000..a7ef1d2684 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx @@ -0,0 +1,33 @@ +import { getCurrentWorkspace } from 'src/application/services/js-services/session'; +import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@mui/material'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +export function RecordNotFound({ open, title }: { open: boolean; title?: string }) { + const navigate = useNavigate(); + + return ( + + Oops.. something went wrong + + + {title ? title : 'The record you are looking for does not exist.'} + + + + + + + ); +} + +export default RecordNotFound; diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts b/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts new file mode 100644 index 0000000000..e4f431167c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/not-found/index.ts @@ -0,0 +1 @@ +export * from './RecordNotFound'; diff --git a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts new file mode 100644 index 0000000000..1086cabdfd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts @@ -0,0 +1,27 @@ +import toast from 'react-hot-toast'; + +const commonOptions = { + style: { + background: 'var(--bg-base)', + color: 'var(--text-title)', + shadows: 'var(--shadow)', + }, +}; + +export const notify = { + success: (message: string) => { + toast.success(message, commonOptions); + }, + error: (message: string) => { + toast.error(message, commonOptions); + }, + loading: (message: string) => { + toast.loading(message, commonOptions); + }, + info: (message: string) => { + toast(message, commonOptions); + }, + clear: () => { + toast.dismiss(); + }, +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx b/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx new file mode 100644 index 0000000000..090c15d3b2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/page/Page.tsx @@ -0,0 +1,30 @@ +import { YView } from '@/application/collab.type'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +export function Page({ + id, + onClick, + ...props +}: { + id: string; + onClick?: (view: YView) => void; + style?: React.CSSProperties; +}) { + const { view, icon, name } = usePageInfo(id); + + return ( +
{ + onClick && view && onClick(view); + }} + className={'flex items-center justify-center gap-2 overflow-hidden'} + {...props} + > +
{icon}
+
{name}
+
+ ); +} + +export default Page; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/index.ts b/frontend/appflowy_web_app/src/components/_shared/page/index.ts new file mode 100644 index 0000000000..d9925d7520 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/page/index.ts @@ -0,0 +1 @@ +export * from './Page'; diff --git a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx new file mode 100644 index 0000000000..2418d669b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx @@ -0,0 +1,92 @@ +import { FontLayout, LineHeightLayout, ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; +import { useViewSelector } from '@/application/folder-yjs'; +import { CoverType } from '@/application/folder-yjs/folder.type'; +import React, { useEffect, useMemo, useState } from 'react'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { useTranslation } from 'react-i18next'; + +export interface PageCover { + type: CoverType; + value: string; +} + +export interface PageExtra { + cover: PageCover | null; + fontLayout: FontLayout; + lineHeightLayout: LineHeightLayout; + font?: string; +} + +function parseExtra(extra: string): PageExtra { + let extraObj; + + try { + extraObj = JSON.parse(extra); + } catch (e) { + extraObj = {}; + } + + return { + cover: extraObj.cover + ? { + type: extraObj.cover.type, + value: extraObj.cover.value, + } + : null, + fontLayout: extraObj.font_layout || FontLayout.normal, + lineHeightLayout: extraObj.line_height_layout || LineHeightLayout.normal, + font: extraObj.font, + }; +} + +export function usePageInfo(id: string) { + const { view } = useViewSelector(id); + + const [loading, setLoading] = useState(true); + const layout = view?.get(YjsFolderKey.layout); + const icon = view?.get(YjsFolderKey.icon); + const extra = view?.get(YjsFolderKey.extra); + const name = view?.get(YjsFolderKey.name) || ''; + const iconObj = useMemo(() => { + try { + return JSON.parse(icon || ''); + } catch (e) { + return null; + } + }, [icon]); + + const extraObj = useMemo(() => { + return parseExtra(extra || ''); + }, [extra]); + + const defaultIcon = useMemo(() => { + switch (parseInt(layout ?? '0')) { + case ViewLayout.Document: + return ; + case ViewLayout.Grid: + return ; + case ViewLayout.Board: + return ; + case ViewLayout.Calendar: + return ; + default: + return ; + } + }, [layout]); + + const { t } = useTranslation(); + + useEffect(() => { + setLoading(!view); + }, [view]); + return { + icon: iconObj?.value || defaultIcon, + name: name || t('menuAppHeader.defaultNewPageName'), + view: view as YView, + loading, + extra: extraObj, + }; +} diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx new file mode 100644 index 0000000000..f91ac8284e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Popover as PopoverComponent, PopoverProps as PopoverComponentProps } from '@mui/material'; + +const defaultProps: Partial = { + keepMounted: false, + disableRestoreFocus: true, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, +}; + +export function Popover({ children, ...props }: PopoverComponentProps) { + return ( + + {children} + + ); +} diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx new file mode 100644 index 0000000000..437b08eaf5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx @@ -0,0 +1,63 @@ +import { Box, ClickAwayListener, Fade, Paper, Popper, PopperPlacementType } from '@mui/material'; +import React, { ReactElement, useEffect } from 'react'; + +interface Props { + content: ReactElement; + children: ReactElement; + open: boolean; + onClose: () => void; + placement?: PopperPlacementType; +} + +export const RichTooltip = ({ placement = 'top', open, onClose, content, children }: Props) => { + const [childNode, setChildNode] = React.useState(null); + const [, setTransitioning] = React.useState(false); + + useEffect(() => { + if (open) { + setTransitioning(true); + } + }, [open]); + return ( + <> + {React.cloneElement(children, { ...children.props, ref: setChildNode })} + + {({ TransitionProps }) => ( + { + setTransitioning(false); + }} + > + + + + {content} + + + + + )} + + + ); +}; + +export default RichTooltip; diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts new file mode 100644 index 0000000000..f1c61c79c4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts @@ -0,0 +1,2 @@ +export * from './Popover'; +export * from './RichTooltip'; diff --git a/frontend/appflowy_web_app/src/components/_shared/progress/ComponentLoading.tsx b/frontend/appflowy_web_app/src/components/_shared/progress/ComponentLoading.tsx new file mode 100644 index 0000000000..9c96a1890c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/progress/ComponentLoading.tsx @@ -0,0 +1,12 @@ +import CircularProgress from '@mui/material/CircularProgress'; +import React from 'react'; + +function ComponentLoading() { + return ( +
+ +
+ ); +} + +export default ComponentLoading; diff --git a/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..0a4ba3414c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; + +function LinearProgressWithLabel({ + value, + count, + selectedCount, +}: { + value: number; + count: number; + selectedCount: number; +}) { + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + const options = useMemo(() => { + return Array.from({ length: count }, (_, i) => ({ + id: i, + checked: i < selectedCount, + })); + }, [count, selectedCount]); + + const isSplit = count < 6; + + return ( +
+
+ {options.map((option) => ( + + ))} +
+
{result}
+
+ ); +} + +export default LinearProgressWithLabel; diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx new file mode 100644 index 0000000000..911ccff808 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx @@ -0,0 +1,72 @@ +import { Scrollbars } from 'react-custom-scrollbars-2'; +import React from 'react'; + +export interface AFScrollerProps { + children: React.ReactNode; + overflowXHidden?: boolean; + overflowYHidden?: boolean; + className?: string; + style?: React.CSSProperties; + onScroll?: (e: React.UIEvent) => void; +} + +export const AFScroller = React.forwardRef( + ({ onScroll, style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps, ref) => { + return ( + { + if (!el) return; + + const scrollEl = el.container?.firstChild as HTMLElement; + + if (!scrollEl) return; + if (typeof ref === 'function') { + ref(scrollEl); + } else if (ref) { + ref.current = scrollEl; + } + }} + renderThumbHorizontal={(props) =>
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => ( +
+ )} + > + {children} + + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/index.ts b/frontend/appflowy_web_app/src/components/_shared/scroller/index.ts new file mode 100644 index 0000000000..7a740a5bb0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/index.ts @@ -0,0 +1 @@ +export * from './AFScroller'; diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx new file mode 100644 index 0000000000..6e379e6458 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx @@ -0,0 +1,29 @@ +import { FC, useMemo } from 'react'; + +export interface TagProps { + color?: string; + label?: string; + size?: 'small' | 'medium'; +} + +export const Tag: FC = ({ color, size = 'small', label }) => { + const className = useMemo(() => { + const classList = ['rounded-md', 'font-medium', 'leading-[18px]']; + + if (color) classList.push(`text-text-title`); + if (size === 'small') classList.push('px-2', 'py-[2px]'); + if (size === 'medium') classList.push('px-3', 'py-1'); + return classList.join(' '); + }, [color, size]); + + return ( +
+ {label} +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/index.ts b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts new file mode 100644 index 0000000000..9790fcbf11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts @@ -0,0 +1 @@ +export * from './Tag'; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx new file mode 100644 index 0000000000..d7e9037ad5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -0,0 +1,28 @@ +import FolderPage from '@/pages/FolderPage'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import ProtectedRoutes from '@/components/auth/ProtectedRoutes'; +import LoginPage from '@/pages/LoginPage'; +import ProductPage from '@/pages/ProductPage'; +import withAppWrapper from '@/components/app/withAppWrapper'; + +const AppMain = withAppWrapper(() => { + return ( + + }> + } /> + } /> + + } /> + + ); +}); + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/frontend/appflowy_web_app/src/components/app/AppConfig.tsx b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx new file mode 100644 index 0000000000..fe817e7004 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/AppConfig.tsx @@ -0,0 +1,37 @@ +import { useAppLanguage } from '@/components/app/useAppLanguage'; +import React, { createContext, useEffect, useMemo, useState } from 'react'; +import { AFService } from '@/application/services/services.type'; +import { getService } from '@/application/services'; +import { useAppSelector } from '@/stores/store'; + +export const AFConfigContext = createContext< + | { + service: AFService | undefined; + } + | undefined +>(undefined); + +function AppConfig({ children }: { children: React.ReactNode }) { + const appConfig = useAppSelector((state) => state.app.appConfig); + const [service, setService] = useState(); + + useAppLanguage(); + + useEffect(() => { + void (async () => { + if (!appConfig) return; + setService(await getService(appConfig)); + })(); + }, [appConfig]); + + const config = useMemo( + () => ({ + service, + }), + [service] + ); + + return {children}; +} + +export default AppConfig; diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx new file mode 100644 index 0000000000..8ae3d12616 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -0,0 +1,167 @@ +import { useAppThemeMode } from '@/components/app/useAppThemeMode'; +import React, { useMemo } from 'react'; +import createTheme from '@mui/material/styles/createTheme'; +import ThemeProvider from '@mui/material/styles/ThemeProvider'; +import '@/i18n/config'; + +import 'src/styles/tailwind.css'; +import 'src/styles/template.css'; + +function AppTheme({ children }: { children: React.ReactNode }) { + const { isDark } = useAppThemeMode(); + const theme = useMemo( + () => + createTheme({ + typography: { + fontFamily: ['inherit'].join(','), + fontSize: 12, + button: { + textTransform: 'none', + }, + }, + components: { + MuiMenuItem: { + defaultProps: { + sx: { + '&.Mui-selected.Mui-focusVisible': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&.Mui-focusVisible': { + backgroundColor: 'unset', + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + borderRadius: '4px', + padding: '2px', + }, + }, + }, + MuiButton: { + styleOverrides: { + contained: { + color: 'var(--content-on-fill)', + boxShadow: 'var(--shadow)', + }, + }, + }, + MuiButtonBase: { + styleOverrides: { + root: { + '&:not(.MuiButton-contained)': { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&:active': { + backgroundColor: 'var(--fill-list-hover)', + }, + }, + + borderRadius: '4px', + padding: '2px', + boxShadow: 'none', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + boxShadow: 'var(--shadow)', + }, + }, + }, + MuiDialog: { + defaultProps: { + sx: { + '& .MuiBackdrop-root': { + backgroundColor: 'var(--bg-mask)', + }, + }, + }, + }, + + MuiTooltip: { + styleOverrides: { + arrow: { + color: 'var(--bg-tips)', + }, + tooltip: { + backgroundColor: 'var(--bg-tips)', + color: 'var(--text-title)', + fontSize: '0.85rem', + borderRadius: '8px', + fontWeight: 400, + }, + }, + }, + MuiInputBase: { + defaultProps: { + sx: { + '&.Mui-disabled, .Mui-disabled': { + color: 'var(--text-caption)', + WebkitTextFillColor: 'var(--text-caption) !important', + }, + }, + }, + styleOverrides: { + input: { + backgroundColor: 'transparent !important', + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'var(--line-divider)', + }, + }, + }, + }, + palette: { + mode: isDark ? 'dark' : 'light', + primary: { + main: '#00BCF0', + dark: '#00BCF0', + }, + error: { + main: '#FB006D', + dark: '#D32772', + }, + warning: { + main: '#FFC107', + dark: '#E9B320', + }, + info: { + main: '#00BCF0', + dark: '#2E9DBB', + }, + success: { + main: '#66CF80', + dark: '#3BA856', + }, + text: { + primary: isDark ? '#E2E9F2' : '#333333', + secondary: isDark ? '#7B8A9D' : '#828282', + disabled: isDark ? '#363D49' : '#F2F2F2', + }, + divider: isDark ? '#59647A' : '#BDBDBD', + background: { + default: isDark ? '#1A202C' : '#FFFFFF', + paper: isDark ? '#1A202C' : '#FFFFFF', + }, + }, + }), + [isDark] + ); + + return {children}; +} + +export default AppTheme; diff --git a/frontend/appflowy_web_app/src/components/app/useAppLanguage.ts b/frontend/appflowy_web_app/src/components/app/useAppLanguage.ts new file mode 100644 index 0000000000..421c44c350 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/useAppLanguage.ts @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function useAppLanguage() { + const { i18n } = useTranslation(); + + useEffect(() => { + const detectLanguageChange = () => { + const language = window.navigator.language; + + void i18n.changeLanguage(language); + }; + + detectLanguageChange(); + + window.addEventListener('languagechange', detectLanguageChange); + return () => { + window.removeEventListener('languagechange', detectLanguageChange); + }; + }, [i18n]); +} diff --git a/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts b/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts new file mode 100644 index 0000000000..8250d999ec --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/useAppThemeMode.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; + +export function useAppThemeMode() { + const [isDark, setIsDark] = useState(false); + + useEffect(() => { + function detectColorScheme() { + const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + setIsDark(darkModeMediaQuery.matches); + document.documentElement.setAttribute('data-dark-mode', darkModeMediaQuery.matches ? 'true' : 'false'); + } + + detectColorScheme(); + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', detectColorScheme); + return () => { + window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', detectColorScheme); + }; + }, []); + + return { + isDark, + }; +} diff --git a/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx new file mode 100644 index 0000000000..ca5bdcd100 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/app/withAppWrapper.tsx @@ -0,0 +1,27 @@ +import { Provider } from 'react-redux'; +import { store } from 'src/stores/store'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorHandlerPage } from 'src/components/error/ErrorHandlerPage'; +import AppTheme from '@/components/app/AppTheme'; +import { Toaster } from 'react-hot-toast'; +import AppConfig from '@/components/app/AppConfig'; +import { Suspense } from 'react'; + +export default function withAppWrapper (Component: React.FC): React.FC { + return function AppWrapper (): JSX.Element { + return ( + + + + + + + + + + + + + ); + }; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx b/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx new file mode 100644 index 0000000000..5e437bd0f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx @@ -0,0 +1,70 @@ +import Button from '@mui/material/Button'; +import GoogleIcon from '@/assets/settings/google.png'; +import GithubIcon from '@/assets/settings/github.png'; +import DiscordIcon from '@/assets/settings/discord.png'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from './auth.hooks'; +import { ProviderType } from '@/application/user.type'; +import { useState } from 'react'; +import EmailOutlined from '@mui/icons-material/EmailOutlined'; +import SignInWithEmail from './SignInWithEmail'; + +export const LoginButtonGroup = () => { + const { t } = useTranslation(); + const [openSignInWithEmail, setOpenSignInWithEmail] = useState(false); + const { signInWithProvider } = useAuth(); + + return ( +
+ + + + + setOpenSignInWithEmail(false)} /> +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx new file mode 100644 index 0000000000..4d6825c1cb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx @@ -0,0 +1,97 @@ +import React, { lazy, useCallback, useEffect, useMemo, useState } from 'react'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { currentUserActions, LoginState } from '@/stores/currentUser/slice'; +import { useAppDispatch } from '@/stores/store'; +import { getPlatform } from '@/utils/platform'; +import SplashScreen from '@/components/auth/SplashScreen'; +import CircularProgress from '@mui/material/CircularProgress'; +import Portal from '@mui/material/Portal'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import { useNavigate } from 'react-router-dom'; + +const TauriAuth = lazy(() => import('@/components/tauri/TauriAuth')); + +function ProtectedRoutes() { + const { currentUser, checkUser, isReady } = useAuth(); + + const isLoading = currentUser?.loginState === LoginState.LOADING; + const [checked, setChecked] = useState(false); + + const checkUserStatus = useCallback(async () => { + if (!isReady) return; + setChecked(false); + try { + if (!currentUser.isAuthenticated) { + await checkUser(); + } + } finally { + setChecked(true); + } + }, [checkUser, isReady, currentUser.isAuthenticated]); + + useEffect(() => { + void checkUserStatus(); + }, [checkUserStatus]); + + const platform = useMemo(() => getPlatform(), []); + + const navigate = useNavigate(); + + if (checked && !currentUser.isAuthenticated && window.location.pathname !== '/login') { + navigate(`/login?redirect=${encodeURIComponent(window.location.pathname)}`); + return null; + } + + if (currentUser.user?.workspaceId && (window.location.pathname === '/' || window.location.pathname === '')) { + navigate(`/view/${currentUser.user.workspaceId}`); + return null; + } + + return ( +
+ {checked ? ( + + ) : ( +
+ +
+ )} + + {isLoading && } + {platform.isTauri && } +
+ ); +} + +export default ProtectedRoutes; + +const StartLoading = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + const preventDefault = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + dispatch(currentUserActions.resetLoginState()); + } + }; + + document.addEventListener('keydown', preventDefault, true); + + return () => { + document.removeEventListener('keydown', preventDefault, true); + }; + }, [dispatch]); + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx b/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx new file mode 100644 index 0000000000..06d36c2594 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx @@ -0,0 +1,83 @@ +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { useTranslation } from 'react-i18next'; + +function SignInWithEmail({ open, onClose }: { open: boolean; onClose: () => void }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const { signInWithEmailPassword } = useAuth(); + + const handleSignIn = async () => { + setLoading(true); + try { + await signInWithEmailPassword(email, password); + onClose(); + } catch (e) { + // Handle error + } + + setLoading(false); + }; + + return ( + { + if (e.key === 'Enter') { + e.preventDefault(); + void handleSignIn(); + } + }} + > + + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + + + + + ); +} + +export default SignInWithEmail; diff --git a/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx b/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx new file mode 100644 index 0000000000..bf5a5a854d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import Layout from '@/components/layout/Layout'; + +function SplashScreen () { + + return ( + + + + ); +} + +export default SplashScreen; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx new file mode 100644 index 0000000000..768cf3587b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import Welcome from './Welcome'; +import withAppWrapper from '@/components/app/withAppWrapper'; + +describe('', () => { + beforeEach(() => { + cy.mockAPI(); + }); + + it('renders', () => { + const AppWrapper = withAppWrapper(Welcome); + + cy.mount(); + }); + + it('should handle login success', () => { + const AppWrapper = withAppWrapper(Welcome); + + cy.mount(); + + cy.get('[data-cy=signInWithEmail]').click(); + + cy.wait(100); + + cy.get('[data-cy=signInWithEmailDialog]').as('dialog').should('be.visible'); + cy.get('[data-cy=email]').type('fakeEmail123'); + cy.get('[data-cy=password]').type('fakePassword123'); + cy.get('[data-cy=submit]').click(); + cy.wait('@loginSuccess'); + cy.wait('@verifyToken'); + cy.wait('@getUserProfile'); + cy.wait('@getUserWorkspace'); + cy.get('@dialog').should('not.exist'); + }); +}); diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.tsx new file mode 100644 index 0000000000..1281c3336f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.tsx @@ -0,0 +1,39 @@ +import { ReactComponent as AppflowyLogo } from '@/assets/logo.svg'; +import { Stack } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { LoginButtonGroup } from './LoginButtonGroup'; +import { getPlatform } from '@/utils/platform'; +import { lazy } from 'react'; + +const SignInAsAnonymous = lazy(() => import('@/components/tauri/SignInAsAnonymous')); + +export const Welcome = () => { + const { t } = useTranslation(); + + return ( + <> +
e.preventDefault()} method='POST'> + +
+ +
+ +
+ + {t('welcomeTo')} {t('appName')} + +
+ +
+ {getPlatform().isTauri && } +
+ +
+
+
+
+ + ); +}; + +export default Welcome; diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts new file mode 100644 index 0000000000..affe339c81 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts @@ -0,0 +1,192 @@ +import { useAppDispatch, useAppSelector } from '@/stores/store'; +import { useCallback, useContext } from 'react'; +import { nanoid } from 'nanoid'; +import { open } from '@tauri-apps/api/shell'; +import { ProviderType, UserProfile } from '@/application/user.type'; +import { currentUserActions } from '@/stores/currentUser/slice'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { notify } from '@/components/_shared/notify'; + +export const useAuth = () => { + const dispatch = useAppDispatch(); + const AFConfig = useContext(AFConfigContext); + const currentUser = useAppSelector((state) => state.currentUser); + const isReady = !!AFConfig?.service; + + const handleSuccess = useCallback(() => { + notify.clear(); + dispatch(currentUserActions.loginSuccess()); + }, [dispatch]); + + const setUser = useCallback( + async (userProfile: UserProfile) => { + handleSuccess(); + dispatch(currentUserActions.updateUser(userProfile)); + }, + [dispatch, handleSuccess] + ); + + const handleStart = useCallback(() => { + notify.clear(); + notify.loading('Loading...'); + dispatch(currentUserActions.loginStart()); + }, [dispatch]); + + const handleError = useCallback( + ({ message }: { message: string }) => { + notify.clear(); + notify.error(message); + dispatch(currentUserActions.loginError()); + }, + [dispatch] + ); + + // Check if the user is authenticated + const checkUser = useCallback(async () => { + try { + const userHasSignIn = await AFConfig?.service?.userService.checkUser(); + + if (!userHasSignIn) { + throw new Error('Failed to check user'); + } + + const userProfile = await AFConfig?.service?.userService.getUserProfile(); + + if (!userProfile) { + throw new Error('Failed to check user'); + } + + console.log('userProfile', userProfile); + await setUser(userProfile); + + return userProfile; + } catch (e) { + return Promise.reject('Failed to check user'); + } + }, [AFConfig?.service?.userService, setUser]); + + const register = useCallback( + async (email: string, password: string, name: string): Promise => { + handleStart(); + try { + const userProfile = await AFConfig?.service?.authService.signupWithEmailPassword({ + email, + password, + name, + }); + + if (!userProfile) { + throw new Error('Failed to register'); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to register', + }); + return null; + } + }, + [handleStart, AFConfig?.service?.authService, setUser, handleError] + ); + + const logout = useCallback(async () => { + try { + await AFConfig?.service?.authService.signOut(); + dispatch(currentUserActions.logout()); + } catch (e) { + handleError({ + message: 'Failed to logout', + }); + } + }, [AFConfig?.service?.authService, dispatch, handleError]); + + const signInAsAnonymous = useCallback(async () => { + const fakeEmail = nanoid(8) + '@appflowy.io'; + const fakePassword = 'AppFlowy123@'; + const fakeName = 'Me'; + + await register(fakeEmail, fakePassword, fakeName); + }, [register]); + + const signInWithProvider = useCallback( + async (provider: ProviderType) => { + handleStart(); + try { + const url = await AFConfig?.service?.authService.getOAuthURL(provider); + + if (!url) { + throw new Error(); + } + + await open(url); + } catch { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, handleError, handleStart] + ); + + const signInWithOAuth = useCallback( + async (uri: string) => { + handleStart(); + try { + await AFConfig?.service?.authService.signInWithOAuth({ uri }); + const userProfile = await AFConfig?.service?.userService.getUserProfile(); + + if (!userProfile) { + throw new Error(); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser] + ); + + const signInWithEmailPassword = useCallback( + async (email: string, password: string) => { + handleStart(); + try { + await AFConfig?.service?.authService.signinWithEmailPassword(email, password); + + const userProfile = await AFConfig?.service?.userService.getUserProfile(); + + if (!userProfile) { + throw new Error(); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser] + ); + + return { + isReady, + currentUser, + checkUser, + register, + logout, + signInWithProvider, + signInAsAnonymous, + signInWithOAuth, + signInWithEmailPassword, + }; +}; diff --git a/frontend/appflowy_web_app/src/components/database/Database.hooks.ts b/frontend/appflowy_web_app/src/components/database/Database.hooks.ts new file mode 100644 index 0000000000..a8945cc6ba --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/Database.hooks.ts @@ -0,0 +1,89 @@ +import { YDoc, YjsEditorKey } from '@/application/collab.type'; +import { DatabaseContextState } from '@/application/database-yjs'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Log } from '@/utils/log'; +import { useCallback, useContext, useEffect, useState } from 'react'; + +export function useGetDatabaseId(iidIndex: string) { + const [databaseId, setDatabaseId] = useState(); + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + + const loadDatabaseId = useCallback(async () => { + if (!databaseService) return; + const databases = await databaseService.getWorkspaceDatabases(); + + console.log('databses', databases); + const id = databases.find((item) => item.views.includes(iidIndex))?.database_id; + + if (!id) return; + setDatabaseId(id); + }, [iidIndex, databaseService]); + + useEffect(() => { + void loadDatabaseId(); + }, [loadDatabaseId]); + return databaseId; +} + +export function useGetDatabaseDispatch() { + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + const onOpenDatabase = useCallback( + async ({ databaseId, rowIds }: { databaseId: string; rowIds?: string[] }) => { + if (!databaseService) return Promise.reject(); + return databaseService.openDatabase(databaseId, rowIds); + }, + [databaseService] + ); + + const onCloseDatabase = useCallback( + (databaseId: string) => { + if (!databaseService) return; + void databaseService.closeDatabase(databaseId); + }, + [databaseService] + ); + + return { + onOpenDatabase, + onCloseDatabase, + }; +} + +export function useLoadDatabase({ databaseId, rowIds }: { databaseId?: string; rowIds?: string[] }) { + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState(null); // Map(false); + const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch(); + + const handleOpenDatabase = useCallback( + async (databaseId: string, rowIds?: string[]) => { + try { + setDoc(null); + const { databaseDoc, rows } = await onOpenDatabase({ + databaseId, + rowIds, + }); + + console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + console.log('rows', rows); + + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, + [onOpenDatabase] + ); + + useEffect(() => { + if (!databaseId) return; + void handleOpenDatabase(databaseId, rowIds); + return () => { + onCloseDatabase(databaseId); + }; + }, [handleOpenDatabase, databaseId, rowIds, onCloseDatabase]); + + return { doc, rows, notFound }; +} diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx new file mode 100644 index 0000000000..0c590462f9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -0,0 +1,23 @@ +import DatabaseViews from '@/components/database/DatabaseViews'; + +import React, { memo } from 'react'; + +export const Database = memo( + ({ + viewId, + onNavigateToView, + iidIndex, + }: { + iidIndex: string; + viewId: string; + onNavigateToView: (viewId: string) => void; + }) => { + return ( +
+ +
+ ); + } +); + +export default Database; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx new file mode 100644 index 0000000000..8adc87d4e6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx @@ -0,0 +1,10 @@ +import { DatabaseContext, DatabaseContextState } from '@/application/database-yjs'; + +export const DatabaseContextProvider = ({ + children, + ...props +}: DatabaseContextState & { + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx new file mode 100644 index 0000000000..43549ba81a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx @@ -0,0 +1,29 @@ +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row'; +import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader'; +import { Divider } from '@mui/material'; +import React, { Suspense } from 'react'; + +export function DatabaseRow({ rowId }: { rowId: string }) { + return ( +
+
+
+ + +
+ + + + + }> + + +
+
+
+
+ ); +} + +export default DatabaseRow; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx new file mode 100644 index 0000000000..fb996978ff --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx @@ -0,0 +1,19 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +function DatabaseTitle({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ( +
+
+
+
{icon}
+
{name}
+
+
+
+ ); +} + +export default DatabaseTitle; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx new file mode 100644 index 0000000000..66730ed897 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -0,0 +1,75 @@ +import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseViewsSelector } from '@/application/database-yjs'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { Board } from '@/components/database/board'; +import { Calendar } from '@/components/database/calendar'; +import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; +import { DatabaseTabs } from '@/components/database/components/tabs'; +import { Grid } from '@/components/database/grid'; +import { ElementFallbackRender } from '@/components/error/ElementFallbackRender'; +import React, { Suspense, useCallback, useMemo, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; + +function DatabaseViews({ + onChangeView, + viewId, + iidIndex, +}: { + onChangeView: (viewId: string) => void; + viewId: string; + iidIndex: string; +}) { + const { childViews, viewIds } = useDatabaseViewsSelector(iidIndex); + + const value = useMemo(() => { + return Math.max( + 0, + viewIds.findIndex((id) => id === viewId) + ); + }, [viewId, viewIds]); + + const [conditionsExpanded, setConditionsExpanded] = useState(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + const activeView = useMemo(() => { + return childViews[value]; + }, [childViews, value]); + + const view = useMemo(() => { + if (!activeView) return null; + const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + + switch (layout) { + case DatabaseViewLayout.Grid: + return ; + case DatabaseViewLayout.Board: + return ; + case DatabaseViewLayout.Calendar: + return ; + } + }, [activeView]); + + return ( + <> + + + + +
+ }> + {view} + +
+ + ); +} + +export default DatabaseViews; diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx new file mode 100644 index 0000000000..23793e9227 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/__tests__/Database.cy.tsx @@ -0,0 +1,40 @@ +import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase'; +import '@/components/layout/layout.scss'; + +describe('', () => { + beforeEach(() => { + cy.viewport(1280, 720); + Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); + cy.mockDatabase(); + }); + + it('renders with a database', () => { + const onNavigateToView = cy.stub(); + + renderDatabase( + { + databaseId: '4c658817-20db-4f56-b7f9-0637a22dfeb6', + viewId: '7d2148fc-cace-4452-9c5c-96e52e6bf8b5', + onNavigateToView, + }, + () => { + cy.get('[data-testid^=view-tab-]').should('have.length', 4); + cy.get('.database-grid').should('exist'); + + cy.get('[data-testid=view-tab-e410747b-5f2f-45a0-b2f7-890ad3001355]').click(); + cy.get('.database-board').should('exist'); + cy.wrap(onNavigateToView).should('have.been.calledOnceWith', 'e410747b-5f2f-45a0-b2f7-890ad3001355'); + + cy.wait(800); + cy.get('[data-testid=view-tab-7d2148fc-cace-4452-9c5c-96e52e6bf8b5]').click(); + cy.get('.database-grid').should('exist'); + cy.wrap(onNavigateToView).should('have.been.calledWith', '7d2148fc-cace-4452-9c5c-96e52e6bf8b5'); + + cy.wait(800); + cy.get('[data-testid=view-tab-2143e95d-5dcb-4e0f-bb2c-50944e6e019f]').click(); + cy.get('.database-calendar').should('exist'); + cy.wrap(onNavigateToView).should('have.been.calledWith', '2143e95d-5dcb-4e0f-bb2c-50944e6e019f'); + } + ); + }); +}); diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx new file mode 100644 index 0000000000..00255c4ed8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseRow.cy.tsx @@ -0,0 +1,97 @@ +import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { applyYDoc } from '@/application/ydoc/apply'; +import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; +import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import withAppWrapper from '@/components/app/withAppWrapper'; +import { DatabaseRow } from 'src/components/database/DatabaseRow'; +import { DatabaseContextProvider } from 'src/components/database/DatabaseContext'; +import * as Y from 'yjs'; +import '@/components/layout/layout.scss'; + +describe('', () => { + beforeEach(() => { + cy.viewport(1280, 720); + Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); + Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] }); + cy.mockDatabase(); + cy.mockDocument('f56bdf0f-90c8-53fb-97d9-ad5860d2b7a0'); + }); + + it('renders with a row', () => { + cy.wait(1000); + cy.fixture('folder').then((folderJson) => { + const doc = new Y.Doc(); + const state = new Uint8Array(folderJson.data.doc_state); + + applyYDoc(doc, state); + const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; + + cy.fixture('database/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((database) => { + const doc = new Y.Doc(); + const databaseState = new Uint8Array(database.data.doc_state); + + applyYDoc(doc, databaseState); + + cy.fixture('database/rows/4c658817-20db-4f56-b7f9-0637a22dfeb6').then((rows) => { + const rootRowsDoc = new Y.Doc(); + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const data = rows['2f944220-9f45-40d9-96b5-e8c0888daf7c']; + const rowDoc = new Y.Doc(); + + applyYDoc(rowDoc, new Uint8Array(data)); + rowsFolder.set('2f944220-9f45-40d9-96b5-e8c0888daf7c', rowDoc); + + const AppWrapper = withAppWrapper(() => { + return ( +
+ +
+ ); + }); + + cy.mount(); + + cy.wait(1000); + + cy.get('[role="textbox"]').should('exist'); + }); + }); + }); + }); +}); + +function TestDatabaseRow({ + rowId, + databaseDoc, + rows, + folder, + viewId, +}: { + rowId: string; + databaseDoc: YDoc; + rows: Y.Map; + folder: YFolder; + viewId: string; +}) { + return ( + + + + + + + + ); +} diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx new file mode 100644 index 0000000000..4c63443ad9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/__tests__/DatabaseWithFilter.cy.tsx @@ -0,0 +1,99 @@ +import { renderDatabase } from '@/components/database/__tests__/withTestingDatabase'; +import '@/components/layout/layout.scss'; + +describe(' with filters and sorts', () => { + beforeEach(() => { + cy.viewport(1280, 720); + Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); + cy.mockDatabase(); + }); + + it('render a database with filters and sorts', () => { + const onNavigateToView = cy.stub(); + + renderDatabase( + { + onNavigateToView, + databaseId: '87bc006e-c1eb-47fd-9ac6-e39b17956369', + viewId: '7f233be4-1b4d-46b2-bcfc-f341b8d75267', + }, + () => { + cy.wait(1000); + cy.getTestingSelector('database-actions-filter').click(); + + cy.get('.database-conditions').then(($el) => { + cy.wait(500); + const height = $el.height(); + + expect(height).to.be.greaterThan(0); + }); + + cy.getTestingSelector('database-sort-condition').click(); + cy.wait(500); + cy.getTestingSelector('sort-condition').as('sortConditions').should('have.length', 2); + cy.get('@sortConditions').eq(0).contains('number'); + cy.get('@sortConditions').eq(0).contains('Ascending'); + cy.get('@sortConditions').eq(1).contains('Name'); + cy.get('@sortConditions').eq(1).contains('Descending'); + cy.clickOutside(); + cy.getTestingSelector('sort-condition-list').should('not.exist'); + + // the length of filters should be 6 + cy.getTestingSelector('database-filter-condition').as('filterConditions'); + cy.get('@filterConditions').should('have.length', 6); + // the first filter should be 'Name', the value should be 'contains', and the input should be 123 + cy.get('@filterConditions').eq(0).as('filterCondition'); + cy.get('@filterCondition').contains('Name'); + cy.get('@filterCondition').contains('123'); + cy.get('@filterCondition').click(); + cy.getTestingSelector('filter-menu-popover').should('be.visible'); + cy.getTestingSelector('filter-condition-type').contains('Contains'); + cy.get(`[data-testid="text-filter-input"] input`).should('have.value', '123'); + cy.clickOutside(); + // the second filter should be 'Type', the value should be 'is not empty' + cy.get('@filterConditions').eq(1).as('filterCondition'); + cy.get('@filterCondition').contains('Type'); + cy.get('@filterCondition').contains('is not empty'); + cy.get('@filterCondition').click(); + cy.clickOutside(); + // the third filter should be 'Done', the value should be 'is Checked' + cy.get('@filterConditions').eq(2).as('filterCondition'); + cy.get('@filterCondition').contains('Done'); + cy.get('@filterCondition').contains('is Checked'); + cy.get('@filterCondition').click(); + cy.clickOutside(); + // the fourth filter should be 'Number', the value should be 'is greater than', and the input should be 600 + cy.get('@filterConditions').eq(3).as('filterCondition'); + cy.get('@filterCondition').contains('number'); + cy.get('@filterCondition').contains('> 600'); + cy.get('@filterCondition').click(); + cy.getTestingSelector('filter-menu-popover').should('be.visible'); + cy.getTestingSelector('filter-condition-type').contains('Is greater than'); + cy.get(`[data-testid="number-filter-input"] input`).should('have.value', '600'); + cy.clickOutside(); + // the fifth filter should be 'multi type', the value should be 'Does not contain' + cy.get('@filterConditions').eq(4).as('filterCondition'); + cy.get('@filterCondition').contains('multi type'); + cy.get('@filterCondition').click(); + cy.getTestingSelector('filter-menu-popover').should('be.visible'); + cy.getTestingSelector('filter-condition-type').contains('Does not contain'); + cy.getTestingSelector('select-option-list').as('selectOptionList'); + cy.get('@selectOptionList').should('have.length', 2); + cy.get('@selectOptionList').eq(0).contains('option-2'); + cy.get('@selectOptionList').eq(1).contains('option-1'); + cy.get('@selectOptionList').eq(1).should('have.data', 'checked', true); + cy.clickOutside(); + // the sixth filter should be 'Checklist', the value should be 'is completed' + cy.get('@filterConditions').eq(5).as('filterCondition'); + cy.get('@filterCondition').contains('Checklist'); + cy.get('@filterCondition').contains('is complete'); + cy.get('@filterCondition').click(); + cy.clickOutside(); + + cy.getTestingSelector('view-tab-a734a068-e73d-4b4b-853c-4daffea389c0').click(); + cy.wait(800); + cy.getTestingSelector('view-tab-7f233be4-1b4d-46b2-bcfc-f341b8d75267').click(); + } + ); + }); +}); diff --git a/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx b/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx new file mode 100644 index 0000000000..d9e79811d1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/__tests__/withTestingDatabase.tsx @@ -0,0 +1,106 @@ +import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { applyYDoc } from '@/application/ydoc/apply'; +import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; +import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import withAppWrapper from '@/components/app/withAppWrapper'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { useState } from 'react'; +import * as Y from 'yjs'; +import { Database } from 'src/components/database/Database'; + +export function renderDatabase( + { + databaseId, + viewId, + onNavigateToView, + }: { + databaseId: string; + viewId: string; + onNavigateToView: (viewId: string) => void; + }, + onAfterRender?: () => void +) { + cy.fixture('folder').then((folderJson) => { + const doc = new Y.Doc(); + const state = new Uint8Array(folderJson.data.doc_state); + + applyYDoc(doc, state); + + const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; + + cy.fixture(`database/${databaseId}`).then((database) => { + cy.fixture(`database/rows/${databaseId}`).then((rows) => { + const doc = new Y.Doc(); + const rootRowsDoc = new Y.Doc(); + const rowsFolder: Y.Map = rootRowsDoc.getMap(); + const databaseState = new Uint8Array(database.data.doc_state); + + applyYDoc(doc, databaseState); + + Object.keys(rows).forEach((key) => { + const data = rows[key]; + const rowDoc = new Y.Doc(); + + applyYDoc(rowDoc, new Uint8Array(data)); + rowsFolder.set(key, rowDoc); + }); + + const AppWrapper = withAppWrapper(() => { + return ( +
+ +
+ ); + }); + + cy.mount(); + onAfterRender?.(); + }); + }); + }); +} + +export function TestDatabase({ + databaseDoc, + rows, + folder, + iidIndex, + initialViewId, + onNavigateToView, +}: { + databaseDoc: YDoc; + rows: Y.Map; + folder: YFolder; + iidIndex: string; + initialViewId: string; + onNavigateToView: (viewId: string) => void; +}) { + const [activeViewId, setActiveViewId] = useState(initialViewId); + + const handleNavigateToView = (viewId: string) => { + setActiveViewId(viewId); + onNavigateToView(viewId); + }; + + return ( + + + + + + + + ); +} diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx new file mode 100644 index 0000000000..73c0f95398 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -0,0 +1,27 @@ +import { useDatabase, useGroupsSelector } from '@/application/database-yjs'; +import { Group } from '@/components/database/components/board'; +import { CircularProgress } from '@mui/material'; +import React from 'react'; + +export function Board() { + const database = useDatabase(); + const groups = useGroupsSelector(); + + if (!database) { + return ( +
+ +
+ ); + } + + return ( +
+ {groups.map((groupId) => ( + + ))} +
+ ); +} + +export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/board/index.ts b/frontend/appflowy_web_app/src/components/database/board/index.ts new file mode 100644 index 0000000000..0b5afd6588 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Board = lazy(() => import('./Board')); diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts new file mode 100644 index 0000000000..b3ec014505 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts @@ -0,0 +1,41 @@ +import { useCalendarEventsSelector, useCalendarLayoutSetting } from '@/application/database-yjs'; +import { useCallback, useEffect, useMemo } from 'react'; +import { dayjsLocalizer } from 'react-big-calendar'; +import dayjs from 'dayjs'; +import en from 'dayjs/locale/en'; + +export function useCalendarSetup() { + const layoutSetting = useCalendarLayoutSetting(); + const { events, emptyEvents } = useCalendarEventsSelector(); + + const dayPropGetter = useCallback((date: Date) => { + const day = date.getDay(); + + return { + className: `day-${day}`, + }; + }, []); + + useEffect(() => { + dayjs.locale({ + ...en, + weekStart: layoutSetting.firstDayOfWeek, + }); + }, [layoutSetting]); + + const localizer = useMemo(() => dayjsLocalizer(dayjs), []); + + const formats = useMemo(() => { + return { + weekdayFormat: 'ddd', + }; + }, []); + + return { + localizer, + formats, + dayPropGetter, + events, + emptyEvents, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx new file mode 100644 index 0000000000..5a25cd6e49 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -0,0 +1,33 @@ +import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks'; +import { Toolbar, Event } from '@/components/database/components/calendar'; +import React from 'react'; +import { Calendar as BigCalendar } from 'react-big-calendar'; +import './calendar.scss'; + +export function Calendar() { + const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); + + return ( +
+ , + eventWrapper: Event, + }} + style={{ + marginBottom: '24px', + }} + events={events} + views={['month']} + localizer={localizer} + formats={formats} + dayPropGetter={dayPropGetter} + showMultiDayTimes={true} + step={1} + showAllEvents={true} + /> +
+ ); +} + +export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss new file mode 100644 index 0000000000..5829d4e1cc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -0,0 +1,81 @@ +@use "src/styles/mixin.scss"; + +$today-highlight-bg: transparent; +@import 'react-big-calendar/lib/sass/styles'; +@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD + +.rbc-calendar { + font-size: 12px; +} + +.rbc-button-link { + @apply rounded-full w-[20px] h-[20px] my-1.5; +} + + +.rbc-date-cell, .rbc-header { + min-width: 97px; +} + +.rbc-date-cell.rbc-now { + + color: var(--content-on-fill); + + .rbc-button-link { + background-color: var(--function-error); + } +} + +.rbc-month-view { + border: none; + @apply h-full overflow-auto; + + .rbc-month-row { + border: 1px solid var(--line-divider); + border-top: none; + + } + + @include mixin.scrollbar-style; + +} + + +.rbc-month-header { + height: 40px; + position: sticky; + top: 0; + background: var(--bg-body); + z-index: 50; + + .rbc-header { + border: none; + border-bottom: 1px solid var(--line-divider); + @apply flex items-end py-2 justify-center font-normal text-text-caption bg-bg-body; + + } +} + +.rbc-month-row .rbc-row-bg { + .rbc-off-range-bg { + background-color: transparent; + color: var(--text-caption); + } + + .rbc-day-bg.day-0, .rbc-day-bg.day-6 { + background-color: var(--fill-list-active); + } +} + +.rbc-month-row { + display: inline-table !important; + flex: 0 0 0 !important; + min-height: 97px !important; + height: fit-content; +} + +.event-properties { + .property-label { + @apply text-text-caption; + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/calendar/index.ts new file mode 100644 index 0000000000..59e83476ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Calendar = lazy(() => import('./Calendar')); diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx new file mode 100644 index 0000000000..b189bd59dd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -0,0 +1,55 @@ +import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; +import CardField from '@/components/database/components/field/CardField'; +import React, { memo, useEffect, useMemo } from 'react'; + +export interface CardProps { + groupFieldId: string; + rowId: string; + onResize?: (height: number) => void; + isDragging?: boolean; +} + +export const Card = memo(({ groupFieldId, rowId, onResize, isDragging }: CardProps) => { + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]); + + const ref = React.useRef(null); + + useEffect(() => { + if (isDragging) return; + const el = ref.current; + + if (!el) return; + + const observer = new ResizeObserver(() => { + onResize?.(el.offsetHeight); + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [onResize, isDragging]); + + const navigateToRow = useNavigateToRow(); + + return ( +
{ + navigateToRow?.(rowId); + }} + ref={ref} + style={{ + minHeight: '38px', + }} + className='relative flex cursor-pointer flex-col rounded-lg border border-line-border p-3 text-xs shadow-sm hover:bg-fill-list-active hover:shadow' + > + {showFields.map((field, index) => { + return ; + })} +
+ ); +}); + +export default Card; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts new file mode 100644 index 0000000000..ca0b060473 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx new file mode 100644 index 0000000000..28982b3bfc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx @@ -0,0 +1,102 @@ +import { Row } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { Tag } from '@/components/_shared/tag'; +import ListItem from '@/components/database/components/board/column/ListItem'; +import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn'; +import { useMeasureHeight } from '@/components/database/components/cell/useMeasure'; +import React, { memo, useCallback, useEffect, useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { VariableSizeList } from 'react-window'; + +export interface ColumnProps { + id: string; + rows?: Row[]; + fieldId: string; +} + +export const Column = memo( + ({ id, rows, fieldId }: ColumnProps) => { + const { header } = useRenderColumn(id, fieldId); + const ref = React.useRef(null); + const forceUpdate = useCallback((index: number) => { + ref.current?.resetAfterIndex(index, true); + }, []); + + useEffect(() => { + forceUpdate(0); + }, [rows, forceUpdate]); + + const measureRows = useMemo( + () => + rows?.map((row) => { + return { + rowId: row.id, + }; + }) || [], + [rows] + ); + const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows }); + + const Row = useCallback( + ({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => { + const item = data[index]; + + // We are rendering an extra item for the placeholder + if (!item) { + return null; + } + + const onResizeCallback = (height: number) => { + onResize(index, 0, { + width: 0, + height: height + 8, + }); + }; + + return ; + }, + [fieldId, onResize] + ); + + const getItemSize = useCallback( + (index: number) => { + if (!rows || index >= rows.length) return 0; + const row = rows[index]; + + if (!row) return 0; + return rowHeight(index); + }, + [rowHeight, rows] + ); + const rowCount = rows?.length || 0; + + return ( +
+
+ +
+ +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + + {Row} + + ); + }} + +
+
+ ); + }, + (prev, next) => JSON.stringify(prev) === JSON.stringify(next) +); diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx new file mode 100644 index 0000000000..c14e2eaa7d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx @@ -0,0 +1,33 @@ +import { Row } from '@/application/database-yjs'; +import React, { memo } from 'react'; +import { areEqual } from 'react-window'; +import Card from 'src/components/database/components/board/card/Card'; + +export const ListItem = memo( + ({ + item, + style, + onResize, + fieldId, + }: { + item?: Row; + style?: React.CSSProperties; + fieldId: string; + onResize?: (height: number) => void; + }) => { + return ( +
+ {item?.id ? : null} +
+ ); + }, + areEqual +); + +export default ListItem; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts new file mode 100644 index 0000000000..f59b699c20 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts @@ -0,0 +1 @@ +export * from './Column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts new file mode 100644 index 0000000000..c845d4b5a3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts @@ -0,0 +1,31 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { useMemo } from 'react'; + +export function useRenderColumn(id: string, fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const fieldName = field?.get(YjsDatabaseKey.name) || ''; + const header = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: { + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + + return { + name: option?.name || `No ${fieldName}`, + color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent', + }; + } + + default: + return null; + } + }, [field, fieldName, fieldType, id]); + + return { + header, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx new file mode 100644 index 0000000000..5331f4f8c2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -0,0 +1,37 @@ +import { useRowsByGroup } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Column } from '../column'; + +export interface GroupProps { + groupId: string; +} + +export const Group = ({ groupId }: GroupProps) => { + const { columns, groupResult, fieldId, notFound } = useRowsByGroup(groupId); + + const { t } = useTranslation(); + + if (notFound) { + return ( +
+
{t('board.noGroup')}
+
{t('board.noGroupDesc')}
+
+ ); + } + + if (columns.length === 0 || !fieldId) return null; + return ( + +
+ {columns.map((data) => ( + + ))} +
+
+ ); +}; + +export default Group; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts new file mode 100644 index 0000000000..8401278d65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts @@ -0,0 +1 @@ +export * from './Group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/index.ts new file mode 100644 index 0000000000..8a78f59377 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/index.ts @@ -0,0 +1 @@ +export * from './group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx new file mode 100644 index 0000000000..ee96745b61 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx @@ -0,0 +1,51 @@ +import { CalendarEvent, useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import EventPaper from '@/components/database/components/calendar/event/EventPaper'; +import CardField from '@/components/database/components/field/CardField'; +import React, { useMemo } from 'react'; +import { EventWrapperProps } from 'react-big-calendar'; + +export function Event({ event }: EventWrapperProps) { + const { id } = event; + const [rowId, fieldId] = id.split(':'); + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]); + + const navigateToRow = useNavigateToRow(); + const [open, setOpen] = React.useState(false); + + return ( +
+ } open={open} placement='right' onClose={() => setOpen(false)}> +
{ + if (window.innerWidth < 768) { + navigateToRow?.(rowId); + } else { + setOpen((prev) => !prev); + } + }} + className={ + 'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 text-xs shadow-sm hover:bg-fill-list-active hover:shadow' + } + > + {showFields.map((field) => { + return ( +
+ +
+ ); + })} +
+
+
+ ); +} + +export default Event; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx new file mode 100644 index 0000000000..f84619fe8f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -0,0 +1,29 @@ +import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs'; +import EventPaperTitle from '@/components/database/components/calendar/event/EventPaperTitle'; +import OpenAction from '@/components/database/components/database-row/OpenAction'; +import { Property } from '@/components/database/components/property'; +import React from 'react'; + +function EventPaper({ rowId }: { rowId: string }) { + const primaryFieldId = usePrimaryFieldId(); + + const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId); + + return ( +
+
+
+ +
+
+ {primaryFieldId && } + {fields.map((field) => { + return ; + })} +
+
+
+ ); +} + +export default EventPaper; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaperTitle.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaperTitle.tsx new file mode 100644 index 0000000000..0ab976c13e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaperTitle.tsx @@ -0,0 +1,15 @@ +import { useCellSelector } from '@/application/database-yjs'; +import { TextCell } from '@/application/database-yjs/cell.type'; +import { TextProperty } from '@/components/database/components/property/text'; +import React from 'react'; + +function EventPaperTitle({ fieldId, rowId }: { fieldId: string; rowId: string }) { + const cell = useCellSelector({ + fieldId, + rowId, + }); + + return ; +} + +export default EventPaperTitle; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts new file mode 100644 index 0000000000..e59a119814 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts @@ -0,0 +1 @@ +export * from './Event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts new file mode 100644 index 0000000000..7b631093dc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts @@ -0,0 +1,2 @@ +export * from './toolbar'; +export * from './event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx new file mode 100644 index 0000000000..b6238185eb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx @@ -0,0 +1,46 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow'; +import Button from '@mui/material/Button'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) { + const [open, setOpen] = React.useState(false); + const { t } = useTranslation(); + const content = useMemo(() => { + return ( +
+
{t('calendar.settings.clickToOpen')}
+ {emptyEvents.map((event) => { + const rowId = event.id.split(':')[0]; + + return ; + })} +
+ ); + }, [emptyEvents, t]); + + return ( + { + setOpen(false); + }} + > + + + ); +} + +export default NoDate; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx new file mode 100644 index 0000000000..5e2eaa61d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx @@ -0,0 +1,39 @@ +import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs'; +import { Cell } from '@/components/database/components/cell'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +function NoDateRow({ rowId }: { rowId: string }) { + const navigateToRow = useNavigateToRow(); + const primaryFieldId = usePrimaryFieldId(); + const cell = useCellSelector({ + rowId, + fieldId: primaryFieldId || '', + }); + const { t } = useTranslation(); + + if (!primaryFieldId || !cell?.data) { + return
{t('grid.row.titlePlaceholder')}
; + } + + return ( +
{ + navigateToRow?.(rowId); + }} + className={'w-full hover:text-fill-default'} + > + +
+ ); +} + +export default NoDateRow; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx new file mode 100644 index 0000000000..fd2861e4e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx @@ -0,0 +1,59 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import NoDate from '@/components/database/components/calendar/toolbar/NoDate'; +import { IconButton } from '@mui/material'; +import Button from '@mui/material/Button'; +import dayjs from 'dayjs'; +import React, { useMemo } from 'react'; +import { ToolbarProps } from 'react-big-calendar'; +import { ReactComponent as LeftArrow } from '$icons/16x/arrow_left.svg'; +import { ReactComponent as RightArrow } from '$icons/16x/arrow_right.svg'; +import { ReactComponent as DownArrow } from '$icons/16x/arrow_down.svg'; + +import { useTranslation } from 'react-i18next'; + +export function Toolbar({ + onNavigate, + date, + emptyEvents, +}: ToolbarProps & { + emptyEvents: CalendarEvent[]; +}) { + const dateStr = useMemo(() => dayjs(date).format('MMM YYYY'), [date]); + const { t } = useTranslation(); + + return ( +
+
{dateStr}
+
+ onNavigate('PREV')}> + + + + onNavigate('NEXT')}> + + + + +
+
+ ); +} + +export default Toolbar; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts new file mode 100644 index 0000000000..7c6430332b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts @@ -0,0 +1 @@ +export * from './Toolbar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts new file mode 100644 index 0000000000..2e752f8f6e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -0,0 +1,47 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { DateFormat, TimeFormat, getDateFormat, getTimeFormat } from '@/application/database-yjs'; +import { renderDate } from '@/utils/time'; +import { useCallback, useMemo } from 'react'; + +export function useCellTypeOption(fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return useMemo(() => { + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); + }, [fieldType, field]); +} + +export function useDateTypeCellDispatcher(fieldId: string) { + const typeOption = useCellTypeOption(fieldId); + const typeOptionValue = useMemo(() => { + if (!typeOption) return null; + return { + timeFormat: parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat, + dateFormat: parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat, + }; + }, [typeOption]); + + const getDateTimeStr = useCallback( + (timeStamp: string, includeTime?: boolean) => { + if (!typeOptionValue || !timeStamp) return null; + const timeFormat = getTimeFormat(typeOptionValue.timeFormat); + const dateFormat = getDateFormat(typeOptionValue.dateFormat); + const format = [dateFormat]; + + if (includeTime) { + format.push(timeFormat); + } + + return renderDate(timeStamp, format.join(' '), true); + }, + [typeOptionValue] + ); + + return { + getDateTimeStr, + typeOptionValue, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx new file mode 100644 index 0000000000..3835db12ff --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -0,0 +1,58 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; +import React, { FC, useMemo } from 'react'; +import { TextCell } from '@/components/database/components/cell/text'; +import { UrlCell } from '@/components/database/components/cell/url'; +import { NumberCell } from '@/components/database/components/cell/number'; +import { CheckboxCell } from '@/components/database/components/cell/checkbox'; +import { SelectOptionCell } from '@/components/database/components/cell/select-option'; +import { DateTimeCell } from '@/components/database/components/cell/date'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import { CellProps, Cell as CellType } from '@/application/database-yjs/cell.type'; +import { RelationCell } from '@/components/database/components/cell/relation'; + +export function Cell(props: CellProps) { + const { cell, rowId, fieldId, style } = props; + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + return TextCell; + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistCell; + case FieldType.Relation: + return RelationCell; + default: + return TextCell; + } + }, [fieldType]) as FC>; + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ; + } + + if (cell && cell.fieldType !== fieldType) { + return null; + } + + return ; +} + +export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts new file mode 100644 index 0000000000..b358ed6e49 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts @@ -0,0 +1,13 @@ +import { SelectOptionColor } from '@/application/database-yjs'; + +export const SelectOptionColorMap = { + [SelectOptionColor.Purple]: '--tint-purple', + [SelectOptionColor.Pink]: '--tint-pink', + [SelectOptionColor.LightPink]: '--tint-red', + [SelectOptionColor.Orange]: '--tint-orange', + [SelectOptionColor.Yellow]: '--tint-yellow', + [SelectOptionColor.Lime]: '--tint-lime', + [SelectOptionColor.Green]: '--tint-green', + [SelectOptionColor.Aqua]: '--tint-aqua', + [SelectOptionColor.Blue]: '--tint-blue', +}; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx new file mode 100644 index 0000000000..3b480f946a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -0,0 +1,16 @@ +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { FieldType } from '@/application/database-yjs'; +import { CellProps, CheckboxCell as CheckboxCellType } from '@/application/database-yjs/cell.type'; + +export function CheckboxCell({ cell, style }: CellProps) { + const checked = cell?.data; + + if (cell?.fieldType !== FieldType.Checkbox) return null; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts new file mode 100644 index 0000000000..f1cb1ac4bf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts @@ -0,0 +1 @@ +export * from './CheckboxCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx new file mode 100644 index 0000000000..e3c927a607 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -0,0 +1,27 @@ +import { FieldType, parseChecklistData } from '@/application/database-yjs'; +import { CellProps, ChecklistCell as ChecklistCellType } from '@/application/database-yjs/cell.type'; +import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; +import React, { useMemo } from 'react'; + +export function ChecklistCell({ cell, style, placeholder }: CellProps) { + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + if (cell?.fieldType !== FieldType.Checklist) return null; + + if (!data || !options || !selectedOptions) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts new file mode 100644 index 0000000000..b12d47b6c5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx new file mode 100644 index 0000000000..56e7027567 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -0,0 +1,48 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useRowDataSelector } from '@/application/database-yjs'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import React, { useEffect, useMemo, useState } from 'react'; + +export function RowCreateModifiedTime({ + rowId, + fieldId, + attrName, + style, +}: { + rowId: string; + fieldId: string; + style?: React.CSSProperties; + attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; +}) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + const { row: rowData } = useRowDataSelector(rowId); + const [value, setValue] = useState(null); + + useEffect(() => { + if (!rowData) return; + const observeHandler = () => { + setValue(rowData.get(attrName)); + }; + + observeHandler(); + + rowData.observe(observeHandler); + return () => { + rowData.unobserve(observeHandler); + }; + }, [rowData, attrName]); + + const time = useMemo(() => { + if (!value) return null; + return getDateTimeStr(value, false); + }, [value, getDateTimeStr]); + + if (!time) return null; + return ( +
+ {time} +
+ ); +} + +export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts new file mode 100644 index 0000000000..ed951f3521 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts @@ -0,0 +1 @@ +export * from './RowCreateModifiedTime'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx new file mode 100644 index 0000000000..ca3ab41957 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -0,0 +1,42 @@ +import { FieldType } from '@/application/database-yjs'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import { CellProps, DateTimeCell as DateTimeCellType } from '@/application/database-yjs/cell.type'; +import React, { useMemo } from 'react'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; + +export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + + const startDateTime = useMemo(() => { + return getDateTimeStr(cell?.data || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const endDateTime = useMemo(() => { + if (!cell) return null; + const { endTimestamp, isRange } = cell; + + if (!isRange) return null; + + return getDateTimeStr(endTimestamp || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const dateStr = useMemo(() => { + return [startDateTime, endDateTime].filter(Boolean).join(' - '); + }, [startDateTime, endDateTime]); + + const hasReminder = !!cell?.reminderId; + + if (cell?.fieldType !== FieldType.DateTime) return null; + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ {hasReminder && } + {dateStr} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts new file mode 100644 index 0000000000..e05bb1674a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts @@ -0,0 +1 @@ +export * from './DateTimeCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts new file mode 100644 index 0000000000..2440976340 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts @@ -0,0 +1 @@ +export * from './Cell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx new file mode 100644 index 0000000000..4217c880c4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -0,0 +1,42 @@ +import { + currencyFormaterMap, + NumberFormat, + useFieldSelector, + parseNumberTypeOptions, + FieldType, +} from '@/application/database-yjs'; +import { CellProps, NumberCell as NumberCellType } from '@/application/database-yjs/cell.type'; +import React, { useMemo } from 'react'; +import Decimal from 'decimal.js'; + +export function NumberCell({ cell, fieldId, style, placeholder }: CellProps) { + const { field } = useFieldSelector(fieldId); + + const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); + + const className = useMemo(() => { + const classList = ['select-text', 'cursor-text']; + + return classList.join(' '); + }, []); + + const value = useMemo(() => { + if (!cell || cell.fieldType !== FieldType.Number) return ''; + const numberFormater = currencyFormaterMap[format]; + + if (!numberFormater) return cell.data; + return numberFormater(new Decimal(cell.data).toNumber()); + }, [cell, format]); + + if (value === undefined) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ {value} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts new file mode 100644 index 0000000000..3e1686c783 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts @@ -0,0 +1 @@ +export * from './NumberCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx new file mode 100644 index 0000000000..7c2eb6c648 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -0,0 +1,73 @@ +import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs'; +import { TextCell as CellType, CellProps } from '@/application/database-yjs/cell.type'; +import { TextCell } from '@/components/database/components/cell/text'; +import OpenAction from '@/components/database/components/database-row/OpenAction'; +import { getPlatform } from '@/utils/platform'; +import React, { useEffect, useMemo, useState } from 'react'; + +export function PrimaryCell(props: CellProps) { + const { rowId } = props; + const meta = useRowMetaSelector(rowId); + const icon = meta?.icon; + + const [hover, setHover] = useState(false); + + useEffect(() => { + const table = document.querySelector('.grid-table'); + + if (!table) { + return; + } + + const onMouseMove = (e: Event) => { + const target = e.target as HTMLElement; + + if (target.closest('.grid-row-cell')?.getAttribute('data-row-id') === rowId) { + setHover(true); + } else { + setHover(false); + } + }; + + const onMouseLeave = () => { + setHover(false); + }; + + table.addEventListener('mousemove', onMouseMove); + table.addEventListener('mouseleave', onMouseLeave); + return () => { + table.removeEventListener('mousemove', onMouseMove); + table.removeEventListener('mouseleave', onMouseLeave); + }; + }, [rowId]); + + const isMobile = useMemo(() => { + return getPlatform().isMobile; + }, []); + + const navigateToRow = useNavigateToRow(); + + return ( +
{ + if (isMobile) { + navigateToRow?.(rowId); + } + }} + className={'primary-cell relative flex min-h-full w-full items-center gap-2'} + > + {icon &&
{icon}
} +
+ +
+ + {hover && ( +
+ +
+ )} +
+ ); +} + +export default PrimaryCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts new file mode 100644 index 0000000000..c854b4e336 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts @@ -0,0 +1 @@ +export * from './PrimaryCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx new file mode 100644 index 0000000000..90beed4732 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -0,0 +1,17 @@ +import { FieldType } from '@/application/database-yjs'; +import { CellProps, RelationCell as RelationCellType } from '@/application/database-yjs/cell.type'; +import RelationItems from '@/components/database/components/cell/relation/RelationItems'; +import React from 'react'; + +export function RelationCell({ cell, fieldId, style, placeholder }: CellProps) { + if (cell?.fieldType !== FieldType.Relation) return null; + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + + return ; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx new file mode 100644 index 0000000000..259447e0ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -0,0 +1,77 @@ +import { YDatabaseField, YDatabaseFields, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + DatabaseContextState, + parseRelationTypeOption, + useDatabase, + useFieldSelector, + useNavigateToRow, +} from '@/application/database-yjs'; +import { RelationCell, RelationCellData } from '@/application/database-yjs/cell.type'; +import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue'; +import { useGetDatabaseDispatch } from '@/components/database/Database.hooks'; +import React, { useEffect, useMemo, useState } from 'react'; + +function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { + const { field } = useFieldSelector(fieldId); + const currentDatabaseId = useDatabase()?.get(YjsDatabaseKey.id); + const { onOpenDatabase, onCloseDatabase } = useGetDatabaseDispatch(); + const rowIds = useMemo(() => { + return (cell.data?.toJSON() as RelationCellData) ?? []; + }, [cell.data]); + const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; + const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); + const [rows, setRows] = useState(); + + const navigateToRow = useNavigateToRow(); + + useEffect(() => { + if (!databaseId || !rowIds.length) return; + void onOpenDatabase({ databaseId, rowIds }).then(({ databaseDoc: doc, rows }) => { + const fields = doc + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.fields) as YDatabaseFields; + + fields.forEach((field, fieldId) => { + if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) { + setDatabasePrimaryFieldId(fieldId); + } + }); + + setRows(rows); + }); + }, [onOpenDatabase, databaseId, rowIds, onCloseDatabase]); + + useEffect(() => { + return () => { + if (currentDatabaseId !== databaseId && databaseId) { + onCloseDatabase(databaseId); + } + }; + }, [databaseId, currentDatabaseId, onCloseDatabase]); + + return ( +
+ {rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId); + + return ( +
{ + e.stopPropagation(); + navigateToRow?.(rowId); + }} + className={'w-full cursor-pointer underline'} + > + {rowDoc && databasePrimaryFieldId && ( + + )} +
+ ); + })} +
+ ); +} + +export default RelationItems; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx new file mode 100644 index 0000000000..a6ae613dd5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -0,0 +1,42 @@ +import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { parseYDatabaseCellToCell } from '@/application/database-yjs/cell.parse'; +import React, { useEffect, useState } from 'react'; + +export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { + const [text, setText] = useState(null); + const [row, setRow] = useState(null); + + useEffect(() => { + const data = rowDoc.getMap(YjsEditorKey.data_section); + + const onRowChange = () => { + setRow(data?.get(YjsEditorKey.database_row) as YDatabaseRow); + }; + + onRowChange(); + data?.observe(onRowChange); + return () => { + data?.unobserve(onRowChange); + }; + }, [rowDoc]); + + useEffect(() => { + if (!row) return; + const cells = row.get(YjsDatabaseKey.cells); + const primaryCell = cells.get(fieldId); + + if (!primaryCell) return; + const observeHandler = () => { + setText(parseYDatabaseCellToCell(primaryCell).data as string); + }; + + observeHandler(); + + primaryCell.observe(observeHandler); + return () => { + primaryCell.unobserve(observeHandler); + }; + }, [row, fieldId]); + + return
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts new file mode 100644 index 0000000000..95a0aa3668 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts @@ -0,0 +1 @@ +export * from './RelationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx new file mode 100644 index 0000000000..9538582f11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -0,0 +1,41 @@ +import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/application/database-yjs/cell.type'; +import React, { useCallback, useMemo } from 'react'; + +export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps) { + const selectOptionIds = useMemo(() => (!cell?.data ? [] : cell?.data.split(',')), [cell]); + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected.map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + if (!option) return null; + return ; + }), + [typeOption] + ); + + if (!typeOption || !selectOptionIds?.length) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + + return ( +
+ {renderSelectedOptions(selectOptionIds)} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts new file mode 100644 index 0000000000..40df2f3d7d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx new file mode 100644 index 0000000000..4e78093c08 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -0,0 +1,19 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { CellProps, TextCell as TextCellType } from '@/application/database-yjs/cell.type'; +import React from 'react'; + +export function TextCell({ cell, style, placeholder }: CellProps) { + const readOnly = useReadOnly(); + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + return ( +
+ {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts new file mode 100644 index 0000000000..64bcb41a7f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts @@ -0,0 +1 @@ +export * from './TextCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx new file mode 100644 index 0000000000..6a30c4c9d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -0,0 +1,45 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { CellProps, UrlCell as UrlCellType } from '@/application/database-yjs/cell.type'; +import { openUrl, processUrl } from '@/utils/url'; +import React, { useMemo } from 'react'; + +export function UrlCell({ cell, style, placeholder }: CellProps) { + const readOnly = useReadOnly(); + + const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); + + const className = useMemo(() => { + const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center']; + + if (isUrl) { + classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); + } else { + classList.push('cursor-text'); + } + + return classList.join(' '); + }, [isUrl]); + + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; + + return ( +
{ + if (!isUrl || !cell) return; + if (readOnly) { + e.stopPropagation(); + void openUrl(cell.data, '_blank'); + } + }} + className={className} + > + {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts new file mode 100644 index 0000000000..9f45924c97 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts @@ -0,0 +1 @@ +export * from './UrlCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts new file mode 100644 index 0000000000..d4d7020523 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts @@ -0,0 +1,53 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs'; +import { useCallback, useRef } from 'react'; + +export function useMeasureHeight({ + forceUpdate, + rows, +}: { + forceUpdate: (index: number) => void; + rows: { + rowId?: string; + }[]; +}) { + const heightRef = useRef<{ [rowId: string]: number }>({}); + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return heightRef.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = heightRef.current[rowId]; + + heightRef.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + + if (oldHeight !== height) { + forceUpdate(index); + } + }, + [forceUpdate, rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + return { + rowHeight, + onResize, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx new file mode 100644 index 0000000000..38c96edf10 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx @@ -0,0 +1,38 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import { TextButton } from '@/components/database/components/tabs/TextButton'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function DatabaseActions() { + const { t } = useTranslation(); + + const sorts = useSortsSelector(); + const filter = useFiltersSelector(); + const conditionsContext = useConditionsContext(); + + return ( +
+ { + conditionsContext?.toggleExpanded(); + }} + data-testid={'database-actions-filter'} + color={filter.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.filter')} + + { + conditionsContext?.toggleExpanded(); + }} + color={sorts.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.sort')} + +
+ ); +} + +export default DatabaseActions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx new file mode 100644 index 0000000000..7c74e0fb8a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -0,0 +1,33 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import React from 'react'; +import Filters from 'src/components/database/components/filters/Filters'; +import Sorts from 'src/components/database/components/sorts/Sorts'; + +export function DatabaseConditions() { + const conditionsContext = useConditionsContext(); + const expanded = conditionsContext?.expanded ?? false; + const sorts = useSortsSelector(); + const filters = useFiltersSelector(); + + return ( +
+ + + {sorts.length > 0 && filters.length > 0 &&
} + + +
+ ); +} + +export default DatabaseConditions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts new file mode 100644 index 0000000000..aadb5007af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +interface DatabaseConditionsContextType { + expanded: boolean; + toggleExpanded: () => void; +} + +export function useConditionsContext() { + return useContext(DatabaseConditionsContext); +} + +export const DatabaseConditionsContext = createContext(undefined); diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts new file mode 100644 index 0000000000..7b30286c5c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseActions'; +export * from './DatabaseConditions'; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx new file mode 100644 index 0000000000..59405d6c15 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx @@ -0,0 +1,18 @@ +import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs'; +import { Property } from '@/components/database/components/property'; +import React from 'react'; + +export function DatabaseRowProperties({ rowId }: { rowId: string }) { + const primaryFieldId = usePrimaryFieldId(); + const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId); + + return ( +
+ {fields.map((field) => { + return ; + })} +
+ ); +} + +export default DatabaseRowProperties; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx new file mode 100644 index 0000000000..09339b530e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx @@ -0,0 +1,49 @@ +import { YDoc } from '@/application/collab.type'; +import { useRowMetaSelector } from '@/application/database-yjs'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Editor } from '@/components/editor'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; + +export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { + const meta = useRowMetaSelector(rowId); + const documentId = meta?.documentId; + + const [loading, setLoading] = useState(true); + const [doc, setDoc] = useState(null); + + const documentService = useContext(AFConfigContext)?.service?.documentService; + + const handleOpenDocument = useCallback(async () => { + if (!documentService || !documentId) return; + try { + setDoc(null); + const doc = await documentService.openDocument(documentId); + + console.log('doc', doc); + setDoc(doc); + } catch (e) { + console.error(e); + // haven't created by client, ignore error and show empty + } + }, [documentService, documentId]); + + useEffect(() => { + setLoading(true); + void handleOpenDocument().then(() => setLoading(false)); + }, [handleOpenDocument]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (!doc) return null; + + return ; +} + +export default DatabaseRowSubDocument; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx new file mode 100644 index 0000000000..94b6977bca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/OpenAction.tsx @@ -0,0 +1,27 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useTranslation } from 'react-i18next'; +import { useNavigateToRow } from '@/application/database-yjs'; +import { Tooltip } from '@mui/material'; +import React from 'react'; + +function OpenAction({ rowId }: { rowId: string }) { + const navigateToRow = useNavigateToRow(); + + const { t } = useTranslation(); + + return ( + + + + ); +} + +export default OpenAction; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts new file mode 100644 index 0000000000..a5f4ddd8aa --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseRowProperties'; +export * from './DatabaseRowSubDocument'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx new file mode 100644 index 0000000000..2575aa47ca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/CardField.tsx @@ -0,0 +1,46 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import Cell from '@/components/database/components/cell/Cell'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) { + const { t } = useTranslation(); + const { field } = useFieldSelector(fieldId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const style = useMemo(() => { + const styleProperties = {}; + + if (isPrimary) { + Object.assign(styleProperties, { + fontSize: '1.25em', + fontWeight: 500, + }); + } + + if (index !== 0) { + Object.assign(styleProperties, { + marginTop: '8px', + }); + } + + return styleProperties; + }, [index, isPrimary]); + + if (isPrimary && !cell?.data) { + return ( +
+ {t('grid.row.titlePlaceholder')} +
+ ); + } + + return ; +} + +export default CardField; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx new file mode 100644 index 0000000000..3ff135e8f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx @@ -0,0 +1,20 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useFieldSelector } from '@/application/database-yjs'; +import { FieldTypeIcon } from '@/components/database/components/field/FieldTypeIcon'; +import React from 'react'; + +export function FieldDisplay({ fieldId }: { fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (!field) return null; + + return ( +
+ + {field?.get(YjsDatabaseKey.name)} +
+ ); +} + +export default FieldDisplay; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx new file mode 100644 index 0000000000..3749e21afd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx @@ -0,0 +1,33 @@ +import { FieldType } from '@/application/database-yjs/database.type'; +import { FC, memo } from 'react'; +import { ReactComponent as TextSvg } from '$icons/16x/text.svg'; +import { ReactComponent as NumberSvg } from '$icons/16x/number.svg'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg'; +import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg'; +import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg'; +import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg'; +import { ReactComponent as URLSvg } from '$icons/16x/url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg'; +import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg'; +import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg'; + +export const FieldTypeSvgMap: Record>> = { + [FieldType.RichText]: TextSvg, + [FieldType.Number]: NumberSvg, + [FieldType.DateTime]: DateSvg, + [FieldType.SingleSelect]: SingleSelectSvg, + [FieldType.MultiSelect]: MultiSelectSvg, + [FieldType.Checkbox]: CheckboxSvg, + [FieldType.URL]: URLSvg, + [FieldType.Checklist]: ChecklistSvg, + [FieldType.LastEditedTime]: LastEditedTimeSvg, + [FieldType.CreatedTime]: CreatedSvg, + [FieldType.Relation]: RelationSvg, +}; + +export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { + const Svg = FieldTypeSvgMap[type]; + + return ; +}); diff --git a/frontend/appflowy_web_app/src/components/database/components/field/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/index.ts new file mode 100644 index 0000000000..85ff96da07 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/index.ts @@ -0,0 +1,2 @@ +export * from './FieldTypeIcon'; +export * from './FieldDisplay'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx new file mode 100644 index 0000000000..4a1c6fff33 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx @@ -0,0 +1,35 @@ +import { parseSelectOptionTypeOptions, SelectOption, useFieldSelector } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as CheckIcon } from '$icons/16x/check.svg'; + +export function SelectOptionList({ fieldId, selectedIds }: { fieldId: string; selectedIds: string[] }) { + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderOption = useCallback( + (option: SelectOption) => { + const isSelected = selectedIds.includes(option.id); + + return ( +
+ + {isSelected && } +
+ ); + }, + [selectedIds] + ); + + if (!field || !typeOption) return null; + return
{typeOption.options.map(renderOption)}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts new file mode 100644 index 0000000000..20465070b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionList'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx new file mode 100644 index 0000000000..844303d49b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx @@ -0,0 +1,59 @@ +import { useFilterSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import { FilterContentOverview } from './overview'; +import React, { useState } from 'react'; +import { FieldDisplay } from '@/components/database/components/field'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FilterMenu } from './filter-menu'; + +function Filter({ filterId }: { filterId: string }) { + const filter = useFilterSelector(filterId); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + if (!filter) return null; + + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + data-testid={'database-filter-condition'} + className={ + 'flex cursor-pointer flex-nowrap items-center gap-1 rounded-full border border-line-divider py-1 px-2 hover:border-fill-default hover:text-fill-default hover:shadow-sm' + } + > +
+ +
+ +
+ +
+ +
+ {open && ( + { + setAnchorEl(null); + }} + data-testid={'filter-menu-popover'} + slotProps={{ + paper: { + style: { + maxHeight: '260px', + }, + }, + }} + > + + + )} + + ); +} + +export default Filter; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx new file mode 100644 index 0000000000..41f54f8cac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx @@ -0,0 +1,32 @@ +import { useFiltersSelector, useReadOnly } from '@/application/database-yjs'; +import Filter from '@/components/database/components/filters/Filter'; +import Button from '@mui/material/Button'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddFilterSvg } from '$icons/16x/add.svg'; + +export function Filters() { + const filters = useFiltersSelector(); + const { t } = useTranslation(); + const readOnly = useReadOnly(); + + return ( + <> + {filters.map((filterId) => ( + + ))} + + + ); +} + +export default Filters; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx new file mode 100644 index 0000000000..851e811499 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx @@ -0,0 +1,33 @@ +import { CheckboxFilter, CheckboxFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CheckboxFilterMenu({ filter }: { filter: CheckboxFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: CheckboxFilterCondition.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterCondition.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default CheckboxFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx new file mode 100644 index 0000000000..5d6398b242 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx @@ -0,0 +1,33 @@ +import { ChecklistFilter, ChecklistFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function ChecklistFilterMenu({ filter }: { filter: ChecklistFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: ChecklistFilterCondition.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterCondition.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default ChecklistFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx new file mode 100644 index 0000000000..da7d9dd817 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx @@ -0,0 +1,26 @@ +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function FieldMenuTitle({ fieldId, selectedConditionText }: { fieldId: string; selectedConditionText: string }) { + return ( +
+
+ +
+
+
+
+ {selectedConditionText} +
+ +
+
+
+ ); +} + +export default FieldMenuTitle; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx new file mode 100644 index 0000000000..720dac3d3d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx @@ -0,0 +1,39 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, Filter, SelectOptionFilter, useFieldSelector } from '@/application/database-yjs'; +import CheckboxFilterMenu from './CheckboxFilterMenu'; +import ChecklistFilterMenu from './ChecklistFilterMenu'; +import MultiSelectOptionFilterMenu from './MultiSelectOptionFilterMenu'; +import NumberFilterMenu from './NumberFilterMenu'; +import SingleSelectOptionFilterMenu from './SingleSelectOptionFilterMenu'; +import TextFilterMenu from './TextFilterMenu'; +import React, { useMemo } from 'react'; + +export function FilterMenu({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const menu = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Checkbox: + return ; + case FieldType.Checklist: + return ; + case FieldType.Number: + return ; + case FieldType.MultiSelect: + return ; + case FieldType.SingleSelect: + return ; + default: + return null; + } + }, [field, fieldType, filter]); + + return menu; +} + +export default FilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..68def09bb8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx @@ -0,0 +1,56 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from './FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MultiSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionContains, + text: t('grid.selectOptionFilter.contains'), + }, + { + value: SelectOptionFilterCondition.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default MultiSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx new file mode 100644 index 0000000000..6ed24cc05e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx @@ -0,0 +1,75 @@ +import { NumberFilter, NumberFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterMenu({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: NumberFilterCondition.Equal, + text: t('grid.numberFilter.equal'), + }, + { + value: NumberFilterCondition.NotEqual, + text: t('grid.numberFilter.notEqual'), + }, + { + value: NumberFilterCondition.GreaterThan, + text: t('grid.numberFilter.greaterThan'), + }, + { + value: NumberFilterCondition.LessThan, + text: t('grid.numberFilter.lessThan'), + }, + { + value: NumberFilterCondition.GreaterThanOrEqualTo, + text: t('grid.numberFilter.greaterThanOrEqualTo'), + }, + { + value: NumberFilterCondition.LessThanOrEqualTo, + text: t('grid.numberFilter.lessThanOrEqualTo'), + }, + { + value: NumberFilterCondition.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterCondition.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![NumberFilterCondition.NumberIsEmpty, NumberFilterCondition.NumberIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default NumberFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..217ad8d1ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx @@ -0,0 +1,48 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function SingleSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default SingleSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx new file mode 100644 index 0000000000..e11f834352 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx @@ -0,0 +1,75 @@ +import { TextFilter, TextFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterMenu({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: TextFilterCondition.TextContains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterCondition.TextDoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterCondition.TextStartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterCondition.TextEndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterCondition.TextIs, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterCondition.TextIsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterCondition.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterCondition.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![TextFilterCondition.TextIsEmpty, TextFilterCondition.TextIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default TextFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts new file mode 100644 index 0000000000..fc54ea0f3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts @@ -0,0 +1 @@ +export * from './FilterMenu'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts new file mode 100644 index 0000000000..c7b59bcd2f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts @@ -0,0 +1 @@ +export * from './Filters'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx new file mode 100644 index 0000000000..d3a30e1844 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx @@ -0,0 +1,51 @@ +import { DateFilter, DateFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; + +function DateFilterContentOverview({ filter }: { filter: DateFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (filter.start) { + const end = filter.end ?? filter.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(filter.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(filter.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(filter.timestamp).format('MMM D'); + + switch (filter.condition) { + case DateFilterCondition.DateIs: + return `: ${timestamp}`; + case DateFilterCondition.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterCondition.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterCondition.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterCondition.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterCondition.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterCondition.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterCondition.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter, t]); + + return <>{value}; +} + +export default DateFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx new file mode 100644 index 0000000000..9f6d1ea188 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx @@ -0,0 +1,59 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { + CheckboxFilterCondition, + ChecklistFilterCondition, + FieldType, + Filter, + SelectOptionFilter, + useFieldSelector, +} from '@/application/database-yjs'; +import DateFilterContentOverview from '@/components/database/components/filters/overview/DateFilterContentOverview'; +import NumberFilterContentOverview from '@/components/database/components/filters/overview/NumberFilterContentOverview'; +import SelectFilterContentOverview from '@/components/database/components/filters/overview/SelectFilterContentOverview'; +import TextFilterContentOverview from '@/components/database/components/filters/overview/TextFilterContentOverview'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function FilterContentOverview({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const { t } = useTranslation(); + + return useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Checkbox: + return ( + <> + : {t('grid.checkboxFilter.choicechipPrefix.is')}{' '} + {filter.condition === CheckboxFilterCondition.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked')} + + ); + case FieldType.Checklist: + return ( + <> + :{' '} + {filter.condition === ChecklistFilterCondition.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted')} + + ); + default: + return null; + } + }, [field, fieldType, filter, t]); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx new file mode 100644 index 0000000000..64864541e7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx @@ -0,0 +1,38 @@ +import { NumberFilter, NumberFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterContentOverview({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) { + return ''; + } + + const content = parseInt(filter.content); + + switch (filter.condition) { + case NumberFilterCondition.Equal: + return `= ${content}`; + case NumberFilterCondition.NotEqual: + return `!= ${content}`; + case NumberFilterCondition.GreaterThan: + return `> ${content}`; + case NumberFilterCondition.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterCondition.LessThan: + return `< ${content}`; + case NumberFilterCondition.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterCondition.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterCondition.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [filter.condition, filter.content, t]); + + return <>{value}; +} + +export default NumberFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx new file mode 100644 index 0000000000..64e8ddc00c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx @@ -0,0 +1,42 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { + parseSelectOptionTypeOptions, + SelectOptionFilter, + SelectOptionFilterCondition, +} from '@/application/database-yjs'; +import React, { useMemo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +function SelectFilterContentOverview({ filter, field }: { filter: SelectOptionFilter; field: YDatabaseField }) { + const typeOption = parseSelectOptionTypeOptions(field); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!filter.optionIds?.length) return ''; + + const options = filter.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (filter.condition) { + case SelectOptionFilterCondition.OptionIs: + return `: ${options}`; + case SelectOptionFilterCondition.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionFilterCondition.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionFilterCondition.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter.condition, filter.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx new file mode 100644 index 0000000000..fc03b39c96 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx @@ -0,0 +1,33 @@ +import { TextFilter, TextFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterContentOverview({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) return ''; + switch (filter.condition) { + case TextFilterCondition.TextContains: + case TextFilterCondition.TextIs: + return `: ${filter.content}`; + case TextFilterCondition.TextDoesNotContain: + case TextFilterCondition.TextIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${filter.content}`; + case TextFilterCondition.TextStartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${filter.content}`; + case TextFilterCondition.TextEndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${filter.content}`; + case TextFilterCondition.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterCondition.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, filter]); + + return <>{value}; +} + +export default TextFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts new file mode 100644 index 0000000000..47e041409e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts @@ -0,0 +1 @@ +export * from './FilterContentOverview'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/package.json b/frontend/appflowy_web_app/src/components/database/components/filters/package.json new file mode 100644 index 0000000000..e56f3198c9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/package.json @@ -0,0 +1,14 @@ +{ + "name": "filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/qinluhe/AppFlowy.git" + }, + "private": true +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx new file mode 100644 index 0000000000..1ddb4e2d32 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -0,0 +1,50 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export interface ICalculationCell { + value: string; + fieldId: string; + id: string; + type: CalculationType; +} + +export interface CalculationCellProps { + cell?: ICalculationCell; +} + +export function CalculationCell({ cell }: CalculationCellProps) { + const { t } = useTranslation(); + + const prefix = useMemo(() => { + if (!cell) return ''; + + switch (cell.type) { + case CalculationType.Average: + return t('grid.calculationTypeLabel.average'); + case CalculationType.Max: + return t('grid.calculationTypeLabel.max'); + case CalculationType.Count: + return t('grid.calculationTypeLabel.count'); + case CalculationType.Min: + return t('grid.calculationTypeLabel.min'); + case CalculationType.Sum: + return t('grid.calculationTypeLabel.sum'); + case CalculationType.CountEmpty: + return t('grid.calculationTypeLabel.countEmptyShort'); + case CalculationType.CountNonEmpty: + return t('grid.calculationTypeLabel.countNonEmptyShort'); + default: + return ''; + } + }, [cell, t]); + + return ( +
+ {prefix} + {cell?.value ?? ''} +
+ ); +} + +export default CalculationCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts new file mode 100644 index 0000000000..9bf73af548 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts @@ -0,0 +1 @@ +export * from './CalculationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx new file mode 100644 index 0000000000..5840109be7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx @@ -0,0 +1,62 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector } from '@/application/database-yjs'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { Cell } from '@/components/database/components/cell'; +import { CellProps, Cell as CellType } from '@/application/database-yjs/cell.type'; +import { PrimaryCell } from '@/components/database/components/cell/primary'; +import React, { useEffect, useMemo, useRef } from 'react'; + +export interface GridCellProps { + rowId: string; + fieldId: FieldId; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { + const ref = useRef(null); + const { field } = useFieldSelector(fieldId); + const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + useEffect(() => { + const el = ref.current; + + if (!el || !cell) return; + + const observer = new ResizeObserver(() => { + onResize?.(rowIndex, columnIndex, { + width: el.offsetWidth, + height: el.offsetHeight, + }); + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [columnIndex, onResize, rowIndex, cell]); + + const Component = useMemo(() => { + if (isPrimary) { + return PrimaryCell; + } + + return Cell; + }, [isPrimary]) as React.FC>; + + if (!field) return null; + + return ( +
+ +
+ ); +} + +export default GridCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts new file mode 100644 index 0000000000..2b6d663ef5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts @@ -0,0 +1 @@ +export * from './GridCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx new file mode 100644 index 0000000000..56845e8425 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx @@ -0,0 +1,38 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Column, useFieldSelector } from '@/application/database-yjs/selector'; +import { FieldTypeIcon } from '@/components/database/components/field'; +import { Tooltip } from '@mui/material'; +import React, { useMemo } from 'react'; + +export function GridColumn({ column, index }: { column: Column; index: number }) { + const { field } = useFieldSelector(column.fieldId); + const name = field?.get(YjsDatabaseKey.name); + const type = useMemo(() => { + const type = field?.get(YjsDatabaseKey.type); + + if (!type) return FieldType.RichText; + + return parseInt(type) as FieldType; + }, [field]); + + return ( + +
+
+ +
+
{name}
+
+
+ ); +} + +export default GridColumn; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts new file mode 100644 index 0000000000..3c71a6b899 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from 'src/components/database/components/grid/grid-column/useRenderFields'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx new file mode 100644 index 0000000000..7bacc7b882 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -0,0 +1,70 @@ +import { FieldId } from '@/application/collab.type'; +import { FieldVisibility } from '@/application/database-yjs/database.type'; +import { useFieldsSelector } from '@/application/database-yjs/selector'; +import { useCallback, useMemo } from 'react'; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +export type RenderColumn = { + type: GridColumnType; + visibility?: FieldVisibility; + fieldId?: FieldId; + width: number; + wrap?: boolean; +}; + +export function useRenderFields() { + const fields = useFieldsSelector(); + + const renderColumns = useMemo(() => { + const data = fields.map((column) => ({ + ...column, + type: GridColumnType.Field, + })); + + return [ + { + type: GridColumnType.Action, + width: 64, + }, + ...data, + { + type: GridColumnType.NewProperty, + width: 150, + }, + { + type: GridColumnType.Action, + width: 64, + }, + ].filter(Boolean) as RenderColumn[]; + }, [fields]); + + const columnWidth = useCallback( + (index: number, containerWidth: number) => { + const { type, width } = renderColumns[index]; + + if (type === GridColumnType.NewProperty) { + const totalWidth = renderColumns.reduce((acc, column) => acc + column.width, 0); + const remainingWidth = containerWidth - totalWidth; + + return remainingWidth > 0 ? remainingWidth + width : width; + } + + if (type === GridColumnType.Action && containerWidth < 800) { + return 16; + } + + return width; + }, + [renderColumns] + ); + + return { + fields: renderColumns, + columnWidth, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx new file mode 100644 index 0000000000..30dd0ffe9e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx @@ -0,0 +1,74 @@ +import React, { memo, useEffect, useRef } from 'react'; +import { areEqual, GridChildComponentProps, VariableSizeGrid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridColumnType, RenderColumn, GridColumn } from '../grid-column'; + +export interface GridHeaderProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + columns: RenderColumn[]; + scrollLeft?: number; +} + +const Cell = memo(({ columnIndex, style, data }: GridChildComponentProps) => { + const column = data[columnIndex]; + + // Placeholder for Action toolbar + if (!column || column.type === GridColumnType.Action) return
; + + if (column.type === GridColumnType.Field) { + return ( +
+ +
+ ); + } + + return
; +}, areEqual); + +export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => { + const ref = useRef(null); + + useEffect(() => { + if (ref.current) { + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + return ( +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + ref={ref} + onScroll={(props) => { + onScrollLeft(props.scrollLeft); + }} + itemData={columns} + style={{ overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + +
+ ); +}; + +export default GridHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts new file mode 100644 index 0000000000..44d8082bd7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts @@ -0,0 +1 @@ +export * from './GridHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx new file mode 100644 index 0000000000..4d7abb7a2c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -0,0 +1,40 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalculationCell, ICalculationCell } from '../grid-calculation-cell'; +import React, { useEffect, useState } from 'react'; + +export interface GridCalculateRowCellProps { + fieldId: string; +} + +export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { + const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); + const [calculation, setCalculation] = useState(); + + useEffect(() => { + if (!calculations) return; + const observerHandle = () => { + calculations.forEach((calculation) => { + if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { + setCalculation({ + id: calculation.get(YjsDatabaseKey.id), + fieldId: calculation.get(YjsDatabaseKey.field_id), + value: calculation.get(YjsDatabaseKey.calculation_value), + type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, + }); + } + }); + }; + + observerHandle(); + calculations.observeDeep(observerHandle); + + return () => { + calculations.unobserveDeep(observerHandle); + }; + }, [calculations, fieldId]); + return ; +} + +export default GridCalculateRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx new file mode 100644 index 0000000000..51b7781008 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx @@ -0,0 +1,29 @@ +import { areEqual } from 'react-window'; +import { GridColumnType } from '../grid-column'; +import React, { memo } from 'react'; +import GridCell from '../grid-cell/GridCell'; + +export interface GridRowCellProps { + rowId: string; + fieldId?: string; + type: GridColumnType; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export const GridRowCell = memo(({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) => { + if (type === GridColumnType.Field && fieldId) { + return ( + + ); + } + + if (type === GridColumnType.Action) { + return null; + } + + return null; +}, areEqual); + +export default GridRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts new file mode 100644 index 0000000000..365c3f467e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts @@ -0,0 +1,3 @@ +export * from './GridCalculateRowCell'; +export * from './GridRowCell'; +export * from './useRenderRows'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx new file mode 100644 index 0000000000..b1de9ea0de --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx @@ -0,0 +1,46 @@ +import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowOrdersSelector } from '@/application/database-yjs'; + +import { useMemo } from 'react'; + +export enum RenderRowType { + Row = 'row', + NewRow = 'new-row', + CalculateRow = 'calculate-row', +} + +export type RenderRow = { + type: RenderRowType; + rowId?: string; + height?: number; +}; + +export function useRenderRows() { + const rows = useRowOrdersSelector(); + const readOnly = useReadOnly(); + + const renderRows = useMemo(() => { + const rowItems = + rows?.map((row) => ({ + type: RenderRowType.Row, + rowId: row.id, + height: row.height, + })) ?? []; + + return [ + ...rowItems, + + !readOnly && { + type: RenderRowType.NewRow, + height: DEFAULT_ROW_HEIGHT, + }, + { + type: RenderRowType.CalculateRow, + height: DEFAULT_ROW_HEIGHT, + }, + ].filter(Boolean) as RenderRow[]; + }, [readOnly, rows]); + + return { + rows: renderRows, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx new file mode 100644 index 0000000000..41e52c5086 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -0,0 +1,143 @@ +import { AFScroller } from '@/components/_shared/scroller'; +import { useMeasureHeight } from '@/components/database/components/cell/useMeasure'; +import { GridColumnType, RenderColumn } from '../grid-column'; +import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row'; +import React, { useCallback, useEffect, useRef } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; + +export interface GridTableProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + + columns: RenderColumn[]; + scrollLeft?: number; + viewId: string; +} + +export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { + const ref = useRef(null); + const { rows } = useRenderRows(); + const forceUpdate = useCallback((index: number) => { + ref.current?.resetAfterRowIndex(index, true); + }, []); + + const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows }); + + useEffect(() => { + if (ref.current) { + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = rows[rowIndex]; + const column = columns[columnIndex]; + const fieldId = column.fieldId; + + if (row.type === RenderRowType.Row) { + if (fieldId) { + return `${row.rowId}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + } + + if (fieldId) { + return `${row.type}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + }, + [columns, rows] + ); + const Cell = useCallback( + ({ columnIndex, rowIndex, style, data }: GridChildComponentProps) => { + const row = data.rows[rowIndex]; + const column = data.columns[columnIndex] as RenderColumn; + + const classList = ['flex', 'items-center', 'overflow-hidden', 'grid-row-cell']; + + if (column.wrap) { + classList.push('wrap-cell'); + } else { + classList.push('whitespace-nowrap'); + } + + if (column.type === GridColumnType.Field) { + classList.push('border-b', 'border-l', 'border-line-divider', 'px-2'); + } + + if (column.type === GridColumnType.NewProperty) { + classList.push('border-b', 'border-line-divider', 'px-2'); + } + + if (row.type === RenderRowType.Row) { + return ( +
+ +
+ ); + } + + if (row.type === RenderRowType.CalculateRow && column.fieldId) { + return ( +
+ +
+ ); + } + + return
; + }, + [onResize] + ); + + return ( + + {({ height, width }: { height: number; width: number }) => ( + onScrollLeft(scrollLeft)} + rowCount={rows.length} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + rowHeight={rowHeight} + className={'grid-table'} + overscanRowCount={5} + overscanColumnCount={5} + style={{ + overscrollBehavior: 'none', + }} + itemKey={getItemKey} + itemData={{ columns, rows }} + outerElementType={AFScroller} + > + {Cell} + + )} + + ); +}; + +export default GridTable; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts new file mode 100644 index 0000000000..49518fa391 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts @@ -0,0 +1 @@ +export * from './GridTable'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts new file mode 100644 index 0000000000..2e9a6988f4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts @@ -0,0 +1,3 @@ +export * from './grid-table'; +export * from './grid-header'; +export * from './grid-column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx new file mode 100644 index 0000000000..f84da67aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -0,0 +1,11 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import Title from './Title'; +import React from 'react'; + +export function DatabaseHeader({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ; +} + +export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx new file mode 100644 index 0000000000..330e5ba1cf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx @@ -0,0 +1,36 @@ +import { useCellSelector, useDatabaseViewId, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; +import { FolderContext } from '@/application/folder-yjs'; +import Title from '@/components/database/components/header/Title'; +import React, { useContext, useEffect } from 'react'; + +function DatabaseRowHeader({ rowId }: { rowId: string }) { + const fieldId = usePrimaryFieldId() || ''; + const setCrumbs = useContext(FolderContext)?.setCrumbs; + const viewId = useDatabaseViewId(); + + const meta = useRowMetaSelector(rowId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + useEffect(() => { + if (!viewId) return; + setCrumbs?.((prev) => { + const lastCrumb = prev[prev.length - 1]; + const crumb = { + viewId, + rowId, + name: cell?.data as string, + icon: meta?.icon || '', + }; + + if (lastCrumb?.rowId === rowId) return [...prev.slice(0, -1), crumb]; + return [...prev, crumb]; + }); + }, [cell, meta, rowId, setCrumbs, viewId]); + + return <Title icon={meta?.icon} name={cell?.data as string} />; +} + +export default DatabaseRowHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx new file mode 100644 index 0000000000..8bb1979196 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function Title({ icon, name }: { icon?: string; name?: string }) { + const { t } = useTranslation(); + + return ( + <div className={'flex w-full flex-col py-4'}> + <div className={'flex w-full items-center px-16 max-md:px-4'}> + <div className={'flex items-center gap-2 text-3xl'}> + <div>{icon}</div> + <div className={'font-bold'}>{name || t('document.title.placeholder')}</div> + </div> + </div> + </div> + ); +} + +export default Title; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/index.ts b/frontend/appflowy_web_app/src/components/database/components/header/index.ts new file mode 100644 index 0000000000..452eceafe1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseHeader'; +export * from './DatabaseRowHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx new file mode 100644 index 0000000000..c8e5f34a43 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx @@ -0,0 +1,77 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import { Cell as CellType, CellProps } from '@/application/database-yjs/cell.type'; +import { CheckboxCell } from '@/components/database/components/cell/checkbox'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; +import { DateTimeCell } from '@/components/database/components/cell/date'; +import { NumberCell } from '@/components/database/components/cell/number'; +import { RelationCell } from '@/components/database/components/cell/relation'; +import { SelectOptionCell } from '@/components/database/components/cell/select-option'; +import { TextCell } from '@/components/database/components/cell/text'; +import { UrlCell } from '@/components/database/components/cell/url'; +import PropertyWrapper from '@/components/database/components/property/PropertyWrapper'; +import { TextProperty } from '@/components/database/components/property/text'; +import { ChecklistProperty } from 'src/components/database/components/property/cheklist'; + +import React, { FC, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function Property({ fieldId, rowId }: { fieldId: string; rowId: string }) { + const cell = useCellSelector({ + fieldId, + rowId, + }); + + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const { t } = useTranslation(); + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistProperty; + case FieldType.Relation: + return RelationCell; + case FieldType.RichText: + return TextCell; + default: + return TextProperty; + } + }, [fieldType]) as FC<CellProps<CellType>>; + + const style = useMemo( + () => ({ + fontSize: '12px', + }), + [] + ); + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ( + <PropertyWrapper fieldId={fieldId}> + <RowCreateModifiedTime style={style} rowId={rowId} fieldId={fieldId} attrName={attrName} /> + </PropertyWrapper> + ); + } + + return ( + <PropertyWrapper fieldId={fieldId}> + <Component cell={cell} style={style} placeholder={t('grid.row.textPlaceholder')} fieldId={fieldId} rowId={rowId} /> + </PropertyWrapper> + ); +} + +export default Property; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx new file mode 100644 index 0000000000..c930365b37 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx @@ -0,0 +1,15 @@ +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { + return ( + <div className={'flex min-h-[28px] w-full gap-2'}> + <div className={'property-label flex h-[28px] w-[30%] items-center'}> + <FieldDisplay fieldId={fieldId} /> + </div> + <div className={'flex flex-1 flex-wrap items-center overflow-x-hidden pr-1'}>{children}</div> + </div> + ); +} + +export default PropertyWrapper; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx new file mode 100644 index 0000000000..a8ed3ae2e7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx @@ -0,0 +1,34 @@ +import { parseChecklistData } from '@/application/database-yjs'; +import { CellProps, ChecklistCell as CellType } from '@/application/database-yjs/cell.type'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import React, { useMemo } from 'react'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; + +export function ChecklistProperty(props: CellProps<CellType>) { + const { cell } = props; + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + return ( + <div className={'flex w-full flex-col gap-2 py-2'}> + <ChecklistCell {...props} /> + {options?.map((option) => { + const isSelected = selectedOptions?.includes(option.id); + + return ( + <div key={option.id} className={'flex items-center gap-2 text-xs font-medium'}> + {isSelected ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />} + <div>{option.name}</div> + </div> + ); + })} + </div> + ); +} + +export default ChecklistProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts new file mode 100644 index 0000000000..413d3c884b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/index.ts new file mode 100644 index 0000000000..1a4ad04e85 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/index.ts @@ -0,0 +1 @@ +export * from './Property'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx new file mode 100644 index 0000000000..99b54b08a7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx @@ -0,0 +1,29 @@ +import { CellProps, TextCell } from '@/application/database-yjs/cell.type'; +import { TextField } from '@mui/material'; +import React from 'react'; + +export function TextProperty({ cell }: CellProps<TextCell>) { + return ( + <TextField + value={cell?.data} + inputProps={{ + readOnly: true, + }} + fullWidth + size={'small'} + sx={{ + '& .MuiInputBase-root': { + fontSize: '0.875rem', + borderRadius: '8px', + }, + + '& .MuiInputBase-input': { + padding: '4px 8px', + fontWeight: 500, + }, + }} + /> + ); +} + +export default TextProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts new file mode 100644 index 0000000000..c10e4ed3d0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts @@ -0,0 +1 @@ +export * from './TextProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx new file mode 100644 index 0000000000..bc4f5b60c1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx @@ -0,0 +1,20 @@ +import { useSortSelector } from '@/application/database-yjs'; +import SortCondition from '@/components/database/components/sorts/SortCondition'; +import React from 'react'; +import { FieldDisplay } from 'src/components/database/components/field'; + +function Sort({ sortId }: { sortId: string }) { + const sort = useSortSelector(sortId); + + if (!sort) return null; + return ( + <div data-testid={'sort-condition'} className={'flex items-center gap-1.5'}> + <div className={'w-[120px] max-w-[250px] overflow-hidden rounded-full border border-line-divider py-1 px-2 '}> + <FieldDisplay fieldId={sort.fieldId} /> + </div> + <SortCondition sort={sort} /> + </div> + ); +} + +export default Sort; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx new file mode 100644 index 0000000000..78457da1ca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx @@ -0,0 +1,30 @@ +import { Sort } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +function SortCondition({ sort }: { sort: Sort }) { + const condition = sort.condition; + const { t } = useTranslation(); + const conditionText = useMemo(() => { + switch (condition) { + case 0: + return t('grid.sort.ascending'); + case 1: + return t('grid.sort.descending'); + } + }, [condition, t]); + + return ( + <div + className={ + 'flex w-[120px] max-w-[250px] items-center justify-between gap-1.5 rounded-full border border-line-divider py-1 px-2 font-medium ' + } + > + <span className={'text-xs'}>{conditionText}</span> + <ArrowDownSvg className={'text-text-caption'} /> + </div> + ); +} + +export default SortCondition; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx new file mode 100644 index 0000000000..a657b4a0b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx @@ -0,0 +1,17 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import Sort from '@/components/database/components/sorts/Sort'; +import React from 'react'; + +function SortList() { + const sorts = useSortsSelector(); + + return ( + <div className={'flex w-fit flex-col gap-2 p-2'}> + {sorts.map((sortId) => ( + <Sort sortId={sortId} key={sortId} /> + ))} + </div> + ); +} + +export default SortList; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx new file mode 100644 index 0000000000..4ae872f8ec --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx @@ -0,0 +1,44 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import SortList from '@/components/database/components/sorts/SortList'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as SortSvg } from '$icons/16x/sort_ascending.svg'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +export function Sorts() { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); + const open = Boolean(anchorEl); + const sorts = useSortsSelector(); + + if (sorts.length === 0) return null; + return ( + <> + <div + onClick={(e) => { + setAnchorEl(e.currentTarget); + }} + data-testid={'database-sort-condition'} + className='flex cursor-pointer items-center gap-1 rounded-full border border-line-divider px-2 py-1 text-xs hover:border-fill-default hover:text-fill-default hover:shadow-sm' + > + <SortSvg /> + {t('grid.settings.sort')} + <ArrowDownSvg /> + </div> + {open && ( + <Popover + open={open} + anchorEl={anchorEl} + onClose={() => { + setAnchorEl(null); + }} + > + <SortList /> + </Popover> + )} + </> + ); +} + +export default Sorts; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts new file mode 100644 index 0000000000..467acd9081 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts @@ -0,0 +1 @@ +export * from './Sorts'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx new file mode 100644 index 0000000000..881f3d91df --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -0,0 +1,107 @@ +import { DatabaseViewLayout, ViewLayout, YjsDatabaseKey, YjsFolderKey, YView } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; +import { useFolderContext } from '@/application/folder-yjs'; +import { DatabaseActions } from '@/components/database/components/conditions'; +import { Tooltip } from '@mui/material'; +import { forwardRef, FunctionComponent, SVGProps, useCallback, useMemo } from 'react'; +import { ViewTabs, ViewTab } from './ViewTabs'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; + +export interface DatabaseTabBarProps { + viewIds: string[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +const DatabaseIcons: { + [key in ViewLayout]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>; +} = { + [ViewLayout.Document]: DocumentSvg, + [ViewLayout.Grid]: GridSvg, + [ViewLayout.Board]: BoardSvg, + [ViewLayout.Calendar]: CalendarSvg, +}; + +export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( + ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { + const { t } = useTranslation(); + const folder = useFolderContext(); + const view = useDatabaseView(); + const layout = Number(view?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); + }; + + const getFolderView = useCallback( + (viewId: string) => { + if (!folder) return null; + return folder.get(YjsFolderKey.views)?.get(viewId) as YView | null; + }, + [folder] + ); + + const className = useMemo(() => { + const classList = [ + 'mx-16 -mb-[0.5px] flex items-center overflow-hidden border-line-divider text-text-title max-md:mx-4', + ]; + + if (layout === DatabaseViewLayout.Calendar) { + classList.push('border-b'); + } + + return classList.join(' '); + }, [layout]); + + if (viewIds.length === 0) return null; + return ( + <div ref={ref} className={className}> + <div + style={{ + width: 'calc(100% - 120px)', + }} + className='flex items-center ' + > + <ViewTabs + scrollButtons={false} + variant='scrollable' + allowScrollButtonsMobile + value={selectedViewId} + onChange={handleChange} + > + {viewIds.map((viewId) => { + const view = getFolderView(viewId); + + if (!view) return null; + const layout = Number(view.get(YjsFolderKey.layout)) as ViewLayout; + const Icon = DatabaseIcons[layout]; + const name = view.get(YjsFolderKey.name); + + return ( + <ViewTab + key={viewId} + data-testid={`view-tab-${viewId}`} + icon={<Icon className={'h-4 w-4'} />} + iconPosition='start' + color='inherit' + label={ + <Tooltip title={name} enterDelay={1000} enterNextDelay={1000} placement={'right'}> + <span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span> + </Tooltip> + } + value={viewId} + /> + ); + })} + </ViewTabs> + </div> + {layout !== DatabaseViewLayout.Calendar ? <DatabaseActions /> : null} + </div> + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx new file mode 100644 index 0000000000..7bbf91cf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps, styled } from '@mui/material'; + +export const TextButton = styled((props: ButtonProps) => ( + <Button + {...props} + sx={{ + '&.MuiButton-colorInherit': { + color: 'var(--text-caption)', + }, + }} + /> +))<ButtonProps>(() => ({ + padding: '4px 6px', + fontSize: '0.75rem', + lineHeight: '1rem', + fontWeight: 400, + minWidth: 'unset', +})); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx new file mode 100644 index 0000000000..a9c58e42c7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx @@ -0,0 +1,52 @@ +import { styled, Tab, TabProps, Tabs, TabsProps } from '@mui/material'; +import { HTMLAttributes } from 'react'; + +export const ViewTabs = styled((props: TabsProps) => <Tabs {...props} />)({ + minHeight: '28px', + + '& .MuiTabs-scroller': { + paddingBottom: '2px', + }, +}); + +export const ViewTab = styled((props: TabProps) => <Tab disableRipple {...props} />)({ + padding: '0 12px', + minHeight: '24px', + fontSize: '12px', + minWidth: 'unset', + margin: '4px 0', + borderRadius: 0, + '&:hover': { + backgroundColor: 'transparent !important', + color: 'inherit', + }, + '&.Mui-selected': { + color: 'inherit', + backgroundColor: 'transparent', + }, +}); + +interface TabPanelProps extends HTMLAttributes<HTMLDivElement> { + children?: React.ReactNode; + index: number; + value: number; +} + +export function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + const isActivated = value === index; + + return ( + <div + role='tabpanel' + hidden={!isActivated} + id={`full-width-tabpanel-${index}`} + aria-labelledby={`full-width-tab-${index}`} + dir={'ltr'} + {...other} + > + {isActivated ? children : null} + </div> + ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts b/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts new file mode 100644 index 0000000000..8d2722a633 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseTabs'; +export * from './ViewTabs'; diff --git a/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx new file mode 100644 index 0000000000..15aaa23e51 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/grid/Grid.tsx @@ -0,0 +1,41 @@ +import { useDatabase, useViewId } from '@/application/database-yjs'; +import { useRenderFields, GridHeader, GridTable } from '@/components/database/components/grid'; +import { CircularProgress } from '@mui/material'; +import React, { useEffect, useState } from 'react'; + +export function Grid() { + const database = useDatabase(); + const viewId = useViewId() || ''; + const [scrollLeft, setScrollLeft] = useState(0); + + const { fields, columnWidth } = useRenderFields(); + + useEffect(() => { + setScrollLeft(0); + }, [viewId]); + + if (!database) { + return ( + <div className={'flex w-full flex-1 flex-col items-center justify-center'}> + <CircularProgress /> + </div> + ); + } + + return ( + <div className={'database-grid flex w-full flex-1 flex-col'}> + <GridHeader scrollLeft={scrollLeft} columnWidth={columnWidth} columns={fields} onScrollLeft={setScrollLeft} /> + <div className={'grid-scroll-table w-full flex-1'}> + <GridTable + viewId={viewId} + scrollLeft={scrollLeft} + columnWidth={columnWidth} + columns={fields} + onScrollLeft={setScrollLeft} + /> + </div> + </div> + ); +} + +export default Grid; diff --git a/frontend/appflowy_web_app/src/components/database/grid/index.ts b/frontend/appflowy_web_app/src/components/database/grid/index.ts new file mode 100644 index 0000000000..762542e7cb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/grid/index.ts @@ -0,0 +1 @@ +export * from './Grid'; diff --git a/frontend/appflowy_web_app/src/components/database/index.ts b/frontend/appflowy_web_app/src/components/database/index.ts new file mode 100644 index 0000000000..8ef9c34dc1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Database = lazy(() => import('./Database')); diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx new file mode 100644 index 0000000000..8809cffee3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -0,0 +1,113 @@ +import { YDoc } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import ComponentLoading from '@/components/_shared/progress/ComponentLoading'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { DocumentHeader } from '@/components/document/document_header'; +import { Editor } from '@/components/editor'; +import { EditorLayoutStyle } from '@/components/editor/EditorContext'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { Suspense, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; + +export const Document = () => { + const { objectId: documentId } = useId() || {}; + const [doc, setDoc] = useState<YDoc | null>(null); + const [notFound, setNotFound] = useState<boolean>(false); + const extra = usePageInfo(documentId).extra; + + const layoutStyle: EditorLayoutStyle = useMemo(() => { + return { + font: extra?.font || '', + fontLayout: extra?.fontLayout, + lineHeightLayout: extra?.lineHeightLayout, + }; + }, [extra]); + const documentService = useContext(AFConfigContext)?.service?.documentService; + + const handleOpenDocument = useCallback(async () => { + if (!documentService || !documentId) return; + try { + setDoc(null); + const doc = await documentService.openDocument(documentId); + + setDoc(doc); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [documentService, documentId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDocument(); + }, [handleOpenDocument]); + + const style = useMemo(() => { + const fontSizeMap = { + small: '14px', + normal: '16px', + large: '20px', + }; + + return { + fontFamily: layoutStyle.font, + fontSize: fontSizeMap[layoutStyle.fontLayout], + }; + }, [layoutStyle]); + + const layoutClassName = useMemo(() => { + const classList = []; + + if (layoutStyle.fontLayout === 'large') { + classList.push('font-large'); + } else if (layoutStyle.fontLayout === 'small') { + classList.push('font-small'); + } + + if (layoutStyle.lineHeightLayout === 'large') { + classList.push('line-height-large'); + } else if (layoutStyle.lineHeightLayout === 'small') { + classList.push('line-height-small'); + } + + return classList.join(' '); + }, [layoutStyle]); + + useEffect(() => { + if (!layoutStyle.font) return; + void window.WebFont?.load({ + google: { + families: [layoutStyle.font], + }, + }); + }, [layoutStyle.font]); + + if (!documentId) return null; + + return ( + <> + {doc ? ( + <div style={style} className={`relative w-full ${layoutClassName}`}> + <DocumentHeader doc={doc} viewId={documentId} /> + <div className={'flex w-full justify-center'}> + <Suspense fallback={<ComponentLoading />}> + <div className={'max-w-screen w-[964px] min-w-0'}> + <Editor doc={doc} readOnly={true} layoutStyle={layoutStyle} /> + </div> + </Suspense> + </div> + </div> + ) : ( + <div className={'flex h-full w-full items-center justify-center'}> + <CircularProgress /> + </div> + )} + + <RecordNotFound open={notFound} /> + </> + ); +}; + +export default Document; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx new file mode 100644 index 0000000000..1d01474622 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentCover.tsx @@ -0,0 +1,56 @@ +import { showColorsForImage } from '@/components/document/document_header/utils'; +import { renderColor } from '@/utils/color'; +import React, { useCallback } from 'react'; + +function DocumentCover({ + coverValue, + coverType, + onTextColor, +}: { + coverValue?: string; + coverType?: string; + onTextColor: (color: string) => void; +}) { + const renderCoverColor = useCallback((color: string) => { + return ( + <div + style={{ + background: renderColor(color), + }} + className={`h-full w-full`} + /> + ); + }, []); + + const renderCoverImage = useCallback( + (url: string) => { + return ( + <img + onLoad={(e) => { + void showColorsForImage(e.currentTarget).then((res) => { + onTextColor(res); + }); + }} + draggable={false} + src={url} + alt={''} + className={'h-full w-full object-cover'} + /> + ); + }, + [onTextColor] + ); + + if (!coverType || !coverValue) { + return null; + } + + return ( + <div className={'relative flex h-[255px] w-full max-sm:h-[180px]'}> + {coverType === 'color' && renderCoverColor(coverValue)} + {(coverType === 'custom' || coverType === 'built_in') && renderCoverImage(coverValue)} + </div> + ); +} + +export default DocumentCover; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx new file mode 100644 index 0000000000..04201f5ce5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/DocumentHeader.tsx @@ -0,0 +1,100 @@ +import { DocCoverType, YDoc, YjsFolderKey } from '@/application/collab.type'; +import { useViewSelector } from '@/application/folder-yjs'; +import { CoverType } from '@/application/folder-yjs/folder.type'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import DocumentCover from '@/components/document/document_header/DocumentCover'; +import { useBlockCover } from '@/components/document/document_header/useBlockCover'; +import React, { memo, useMemo, useRef, useState } from 'react'; +import BuiltInImage1 from '@/assets/cover/m_cover_image_1.png'; +import BuiltInImage2 from '@/assets/cover/m_cover_image_2.png'; +import BuiltInImage3 from '@/assets/cover/m_cover_image_3.png'; +import BuiltInImage4 from '@/assets/cover/m_cover_image_4.png'; +import BuiltInImage5 from '@/assets/cover/m_cover_image_5.png'; +import BuiltInImage6 from '@/assets/cover/m_cover_image_6.png'; + +export function DocumentHeader({ viewId, doc }: { viewId: string; doc: YDoc }) { + const ref = useRef<HTMLDivElement>(null); + const { view } = useViewSelector(viewId); + const [textColor, setTextColor] = useState<string>('var(--text-title)'); + const icon = view?.get(YjsFolderKey.icon); + const iconObject = useMemo(() => { + try { + return JSON.parse(icon || ''); + } catch (e) { + return null; + } + }, [icon]); + + const { extra } = usePageInfo(viewId); + + const pageCover = extra.cover; + const { cover } = useBlockCover(doc); + + const coverType = useMemo(() => { + if ( + (pageCover && [CoverType.NormalColor, CoverType.GradientColor].includes(pageCover.type)) || + cover?.cover_selection_type === DocCoverType.Color + ) { + return 'color'; + } + + if (CoverType.BuildInImage === pageCover?.type || cover?.cover_selection_type === DocCoverType.Asset) { + return 'built_in'; + } + + if ( + (pageCover && [CoverType.CustomImage, CoverType.UpsplashImage].includes(pageCover.type)) || + cover?.cover_selection_type === DocCoverType.Image + ) { + return 'custom'; + } + }, [cover?.cover_selection_type, pageCover]); + + const coverValue = useMemo(() => { + if (coverType === 'built_in') { + return { + 1: BuiltInImage1, + 2: BuiltInImage2, + 3: BuiltInImage3, + 4: BuiltInImage4, + 5: BuiltInImage5, + 6: BuiltInImage6, + }[pageCover?.value as string]; + } + + return pageCover?.value || cover?.cover_selection; + }, [coverType, cover?.cover_selection, pageCover]); + + return ( + <div ref={ref} className={'document-header mb-[10px] select-none'}> + <div className={'view-banner relative flex w-full flex-col overflow-hidden'}> + <DocumentCover onTextColor={setTextColor} coverType={coverType} coverValue={coverValue} /> + + <div className={`relative mx-16 w-[964px] min-w-0 max-w-full overflow-visible max-md:mx-4`}> + <div + style={{ + position: coverValue ? 'absolute' : 'relative', + bottom: '100%', + width: '100%', + }} + className={'flex items-center gap-2 px-14 py-8 text-4xl max-md:px-2 max-sm:text-[7vw]'} + > + <div className={`view-icon`}>{iconObject?.value}</div> + <div className={'flex flex-1 items-center gap-2 overflow-hidden'}> + <div + style={{ + color: textColor, + }} + className={'font-bold leading-[1.5em]'} + > + {view?.get(YjsFolderKey.name)} + </div> + </div> + </div> + </div> + </div> + </div> + ); +} + +export default memo(DocumentHeader); diff --git a/frontend/appflowy_web_app/src/components/document/document_header/index.ts b/frontend/appflowy_web_app/src/components/document/document_header/index.ts new file mode 100644 index 0000000000..00f48716bf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/index.ts @@ -0,0 +1 @@ +export * from './DocumentHeader'; diff --git a/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts b/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts new file mode 100644 index 0000000000..ba6226a6e8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/useBlockCover.ts @@ -0,0 +1,36 @@ +import { DocCover, YBlocks, YDoc, YDocument, YjsEditorKey } from '@/application/collab.type'; +import { useEffect, useMemo, useState } from 'react'; + +export function useBlockCover(doc: YDoc) { + const [cover, setCover] = useState<string | null>(null); + + useEffect(() => { + if (!doc) return; + + const document = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.document) as YDocument; + const pageId = document.get(YjsEditorKey.page_id) as string; + const blocks = document.get(YjsEditorKey.blocks) as YBlocks; + const root = blocks.get(pageId); + + setCover(root.toJSON().data || null); + const observerEvent = () => setCover(root.toJSON().data || null); + + root.observe(observerEvent); + + return () => { + root.unobserve(observerEvent); + }; + }, [doc]); + + const coverObj: DocCover = useMemo(() => { + try { + return JSON.parse(cover || ''); + } catch (e) { + return null; + } + }, [cover]); + + return { + cover: coverObj, + }; +} diff --git a/frontend/appflowy_web_app/src/components/document/document_header/utils.ts b/frontend/appflowy_web_app/src/components/document/document_header/utils.ts new file mode 100644 index 0000000000..fe2c0acbe0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/document_header/utils.ts @@ -0,0 +1,28 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +import ColorThief from 'colorthief'; + +const colorThief = new ColorThief(); + +export function calculateTextColor(rgb: [number, number, number]): string { + const [r, g, b] = rgb; + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + + return brightness > 125 ? 'black' : 'white'; +} + +export async function showColorsForImage(image: HTMLImageElement) { + const img = new Image(); + + img.crossOrigin = 'Anonymous'; // Handle CORS + img.src = image.src; + + await new Promise((resolve, reject) => { + img.onload = resolve; + img.onerror = reject; + }); + + const dominantColor = colorThief.getColor(img); + + return calculateTextColor(dominantColor); +} diff --git a/frontend/appflowy_web_app/src/components/document/index.ts b/frontend/appflowy_web_app/src/components/document/index.ts new file mode 100644 index 0000000000..a844aa51ad --- /dev/null +++ b/frontend/appflowy_web_app/src/components/document/index.ts @@ -0,0 +1 @@ +export * from './Document'; diff --git a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx new file mode 100644 index 0000000000..02a27f17ed --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx @@ -0,0 +1,49 @@ +import { CollabOrigin } from '@/application/collab.type'; +import { withYjs, YjsEditor } from '@/application/slate-yjs/plugins/withYjs'; +import EditorEditable from '@/components/editor/Editable'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import { withPlugins } from '@/components/editor/plugins'; +import React, { useEffect, useMemo, useState } from 'react'; +import { createEditor, Descendant } from 'slate'; +import { Slate, withReact } from 'slate-react'; +import * as Y from 'yjs'; + +const defaultInitialValue: Descendant[] = []; + +function CollaborativeEditor({ doc }: { doc: Y.Doc }) { + const context = useEditorContext(); + // if readOnly, collabOrigin is Local, otherwise RemoteSync + const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; + const editor = useMemo( + () => + doc && + (withPlugins( + withReact( + withYjs(createEditor(), doc, { + localOrigin, + }) + ) + ) as YjsEditor), + [doc, localOrigin] + ); + const [, setIsConnected] = useState(false); + + useEffect(() => { + if (!editor) return; + + editor.connect(); + setIsConnected(true); + + return () => { + editor.disconnect(); + }; + }, [editor]); + + return ( + <Slate editor={editor} initialValue={defaultInitialValue}> + <EditorEditable editor={editor} /> + </Slate> + ); +} + +export default CollaborativeEditor; diff --git a/frontend/appflowy_web_app/src/components/editor/Editable.tsx b/frontend/appflowy_web_app/src/components/editor/Editable.tsx new file mode 100644 index 0000000000..2ba4e83e3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/Editable.tsx @@ -0,0 +1,37 @@ +import { useDecorate } from '@/components/editor/components/blocks/code/useDecorate'; +import { Leaf } from '@/components/editor/components/leaf'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import React, { useCallback } from 'react'; +import { NodeEntry } from 'slate'; +import { Editable, ReactEditor, RenderElementProps } from 'slate-react'; +import { Element } from './components/element'; + +const EditorEditable = ({ editor }: { editor: ReactEditor }) => { + const { readOnly } = useEditorContext(); + const codeDecorate = useDecorate(editor); + + const decorate = useCallback( + (entry: NodeEntry) => { + return [...codeDecorate(entry)]; + }, + [codeDecorate] + ); + + const renderElement = useCallback((props: RenderElementProps) => <Element {...props} />, []); + + return ( + <Editable + role={'textbox'} + decorate={decorate} + className={'px-16 outline-none focus:outline-none max-md:px-4'} + renderLeaf={Leaf} + renderElement={renderElement} + readOnly={readOnly} + spellCheck={false} + autoCorrect={'off'} + autoComplete={'off'} + /> + ); +}; + +export default EditorEditable; diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx new file mode 100644 index 0000000000..7c6ec9a55e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx @@ -0,0 +1,61 @@ +import { YDoc, YFolder, YjsEditorKey } from '@/application/collab.type'; +import { DocumentTest } from '@/../cypress/support/document'; +import { applyYDoc } from '@/application/ydoc/apply'; +import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; +import React from 'react'; +import * as Y from 'yjs'; +import { Editor } from './Editor'; +import withAppWrapper from '@/components/app/withAppWrapper'; + +describe('<Editor />', () => { + beforeEach(() => { + cy.viewport(1280, 720); + }); + it('renders with a paragraph', () => { + const documentTest = new DocumentTest(); + + documentTest.insertParagraph('Hello, world!'); + renderEditor(documentTest.doc); + cy.get('[role="textbox"]').should('contain', 'Hello, world!'); + }); + + it('renders with a full document', () => { + cy.mockDatabase(); + Object.defineProperty(window.navigator, 'language', { value: 'en-US' }); + Object.defineProperty(window.navigator, 'languages', { value: ['en-US'] }); + cy.fixture('folder').then((folderJson) => { + const doc = new Y.Doc(); + const state = new Uint8Array(folderJson.data.doc_state); + + applyYDoc(doc, state); + + const folder = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.folder) as YFolder; + + cy.fixture('full_doc').then((docJson) => { + const doc = new Y.Doc(); + const state = new Uint8Array(docJson.data.doc_state); + + applyYDoc(doc, state); + renderEditor(doc, folder); + }); + }); + }); +}); + +function renderEditor(doc: YDoc, folder?: YFolder) { + const AppWrapper = withAppWrapper(() => { + return ( + <div className={'h-screen w-screen overflow-y-auto'}> + {folder ? ( + <FolderProvider folder={folder}> + <Editor doc={doc} readOnly /> + </FolderProvider> + ) : ( + <Editor doc={doc} readOnly /> + )} + </div> + ); + }); + + cy.mount(<AppWrapper />); +} diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.tsx new file mode 100644 index 0000000000..183aed8918 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -0,0 +1,21 @@ +import { YDoc } from '@/application/collab.type'; +import CollaborativeEditor from '@/components/editor/CollaborativeEditor'; +import { defaultLayoutStyle, EditorContextProvider, EditorLayoutStyle } from '@/components/editor/EditorContext'; +import React, { memo } from 'react'; +import './editor.scss'; + +export interface EditorProps { + readOnly: boolean; + doc: YDoc; + layoutStyle?: EditorLayoutStyle; +} + +export const Editor = memo(({ readOnly, doc, layoutStyle = defaultLayoutStyle }: EditorProps) => { + return ( + <EditorContextProvider layoutStyle={layoutStyle} readOnly={readOnly}> + <CollaborativeEditor doc={doc} /> + </EditorContextProvider> + ); +}); + +export default Editor; diff --git a/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx new file mode 100644 index 0000000000..c360c969dd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/EditorContext.tsx @@ -0,0 +1,32 @@ +import { FontLayout, LineHeightLayout } from '@/application/collab.type'; +import { createContext, useContext } from 'react'; + +export interface EditorLayoutStyle { + fontLayout: FontLayout; + font: string; + lineHeightLayout: LineHeightLayout; +} + +export const defaultLayoutStyle: EditorLayoutStyle = { + fontLayout: FontLayout.normal, + font: '', + lineHeightLayout: LineHeightLayout.normal, +}; + +interface EditorContextState { + readOnly: boolean; + layoutStyle: EditorLayoutStyle; +} + +export const EditorContext = createContext<EditorContextState>({ + readOnly: true, + layoutStyle: defaultLayoutStyle, +}); + +export const EditorContextProvider = ({ children, ...props }: EditorContextState & { children: React.ReactNode }) => { + return <EditorContext.Provider value={props}>{children}</EditorContext.Provider>; +}; + +export function useEditorContext() { + return useContext(EditorContext); +} diff --git a/frontend/appflowy_web_app/src/components/editor/command/index.ts b/frontend/appflowy_web_app/src/components/editor/command/index.ts new file mode 100644 index 0000000000..d4ee1a6e28 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/command/index.ts @@ -0,0 +1,40 @@ +import { InlineBlockType, Mention, MentionType } from '@/application/collab.type'; +import { FormulaNode } from '@/components/editor/editor.type'; +import { renderDate } from '@/utils/time'; +import { Editor, Transforms, Element, Text, Node } from 'slate'; +import { ReactEditor } from 'slate-react'; + +export const CustomEditor = { + setDocumentTitle: (editor: ReactEditor, title: string) => { + const length = Editor.string(editor, [0, 0]).length; + + Transforms.insertText(editor, title, { + at: { + anchor: { path: [0, 0, 0], offset: 0 }, + focus: { path: [0, 0, 0], offset: length }, + }, + }); + }, + + // Get the text content of a block node, including the text content of its children and formula nodes + getBlockTextContent(node: Node): string { + if (Element.isElement(node)) { + if (node.type === InlineBlockType.Formula) { + return (node as FormulaNode).data || ''; + } + + if (node.type === InlineBlockType.Mention && (node.data as Mention)?.type === MentionType.Date) { + const date = (node.data as Mention).date || ''; + const isUnix = date?.length === 10; + + return renderDate(date, 'MMM DD, YYYY', isUnix); + } + } + + if (Text.isText(node)) { + return node.text || ''; + } + + return node.children.map((n) => CustomEditor.getBlockTextContent(n)).join(''); + }, +}; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx new file mode 100644 index 0000000000..53552336d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx @@ -0,0 +1,16 @@ +import { BulletedListNode, EditorElementProps } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; + +export const BulletedList = memo( + forwardRef<HTMLDivElement, EditorElementProps<BulletedListNode>>( + ({ node: _, children, className, ...attributes }, ref) => { + return ( + <div ref={ref} {...attributes} className={`${className}`}> + {children} + </div> + ); + } + ) +); + +export default BulletedList; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx new file mode 100644 index 0000000000..62e06b6ba9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx @@ -0,0 +1,49 @@ +import { BulletedListNode } from '@/components/editor/editor.type'; +import { getListLevel } from '@/components/editor/utils/list'; +import React, { useMemo } from 'react'; +import { ReactEditor, useSlateStatic } from 'slate-react'; + +enum Letter { + Disc, + Circle, + Square, +} + +export function BulletedListIcon({ block, className }: { block: BulletedListNode; className: string }) { + const staticEditor = useSlateStatic(); + const path = ReactEditor.findPath(staticEditor, block); + + const letter = useMemo(() => { + const level = getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Disc; + } else if (level % 3 === 1) { + return Letter.Circle; + } else { + return Letter.Square; + } + }, [block.type, staticEditor, path]); + + const dataLetter = useMemo(() => { + switch (letter) { + case Letter.Disc: + return '•'; + case Letter.Circle: + return '◦'; + case Letter.Square: + return '▪'; + } + }, [letter]); + + return ( + <span + onMouseDown={(e) => { + e.preventDefault(); + }} + data-letter={dataLetter} + contentEditable={false} + className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`} + /> + ); +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts new file mode 100644 index 0000000000..393f4a03aa --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts @@ -0,0 +1,2 @@ +export * from './BulletedList'; +export * from './BulletedListIcon'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx new file mode 100644 index 0000000000..4b4f02ea6b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx @@ -0,0 +1,25 @@ +import { EditorElementProps, CalloutNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; +import CalloutIcon from './CalloutIcon'; + +export const Callout = memo( + forwardRef<HTMLDivElement, EditorElementProps<CalloutNode>>(({ node, children, ...attributes }, ref) => { + return ( + <> + <div contentEditable={false} className={'absolute w-full select-none px-2 pt-[15px]'}> + <CalloutIcon node={node} /> + </div> + <div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}> + <div + {...attributes} + className={`flex w-full flex-col rounded border border-line-divider bg-fill-list-active py-2 pl-10`} + > + {children} + </div> + </div> + </> + ); + }) +); + +export default Callout; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx new file mode 100644 index 0000000000..0c72c971d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -0,0 +1,16 @@ +import { CalloutNode } from '@/components/editor/editor.type'; +import React, { useRef } from 'react'; + +function CalloutIcon({ node }: { node: CalloutNode }) { + const ref = useRef<HTMLButtonElement>(null); + + return ( + <> + <span contentEditable={false} ref={ref} className={`flex h-8 w-8 items-center p-1`}> + {node.data.icon} + </span> + </> + ); +} + +export default React.memo(CalloutIcon); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/index.ts new file mode 100644 index 0000000000..4ca74e4be8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/index.ts @@ -0,0 +1 @@ +export * from './Callout'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts new file mode 100644 index 0000000000..b7bb3500af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.hooks.ts @@ -0,0 +1,27 @@ +import { CodeNode } from '@/components/editor/editor.type'; +import { useCallback } from 'react'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Element as SlateElement, Transforms } from 'slate'; + +export function useCodeBlock(node: CodeNode) { + const language = node.data.language; + const editor = useSlateStatic() as ReactEditor; + const handleChangeLanguage = useCallback( + (newLang: string) => { + const path = ReactEditor.findPath(editor, node); + const newProperties = { + data: { + language: newLang, + }, + } as Partial<SlateElement>; + + Transforms.setNodes(editor, newProperties, { at: path }); + }, + [editor, node] + ); + + return { + language, + handleChangeLanguage, + }; +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx new file mode 100644 index 0000000000..5ef2279da3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx @@ -0,0 +1,27 @@ +import { useCodeBlock } from '@/components/editor/components/blocks/code/Code.hooks'; +import { CodeNode, EditorElementProps } from '@/components/editor/editor.type'; +import { forwardRef, memo } from 'react'; +import LanguageSelect from './SelectLanguage'; + +export const CodeBlock = memo( + forwardRef<HTMLDivElement, EditorElementProps<CodeNode>>(({ node, children, ...attributes }, ref) => { + const { language, handleChangeLanguage } = useCodeBlock(node); + + return ( + <> + <div contentEditable={false} className={'absolute mt-2 flex h-20 w-full select-none items-center px-6'}> + <LanguageSelect readOnly language={language} onChangeLanguage={handleChangeLanguage} /> + </div> + <div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}> + <pre + spellCheck={false} + className={`flex w-full rounded border border-line-divider bg-fill-list-active p-5 pt-20`} + > + <code>{children}</code> + </pre> + </div> + </> + ); + }), + (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) +); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx new file mode 100644 index 0000000000..f249a19951 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/SelectLanguage.tsx @@ -0,0 +1,43 @@ +import React, { useRef } from 'react'; +import { TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +function SelectLanguage({ + readOnly, + language = 'json', +}: { + readOnly?: boolean; + language: string; + onChangeLanguage: (language: string) => void; + onBlur?: () => void; +}) { + const { t } = useTranslation(); + const ref = useRef<HTMLDivElement>(null); + + return ( + <> + <TextField + ref={ref} + size={'small'} + variant={'standard'} + sx={{ + '& .MuiInputBase-root, & .MuiInputBase-input': { + userSelect: 'none', + }, + }} + className={'w-[150px]'} + value={language} + onClick={() => { + if (readOnly) return; + }} + InputProps={{ + readOnly: true, + }} + placeholder={t('document.codeBlock.language.placeholder')} + label={t('document.codeBlock.language.label')} + /> + </> + ); +} + +export default SelectLanguage; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/constants.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/constants.ts new file mode 100644 index 0000000000..dee71624db --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/constants.ts @@ -0,0 +1,154 @@ +export const supportLanguage = [ + { + id: 'bash', + title: 'Bash', + }, + { + id: 'basic', + title: 'Basic', + }, + { + id: 'c', + title: 'C', + }, + { + id: 'clojure', + title: 'Clojure', + }, + { + id: 'cpp', + title: 'C++', + }, + { + id: 'cs', + title: 'CS', + }, + { + id: 'css', + title: 'CSS', + }, + { + id: 'dart', + title: 'Dart', + }, + { + id: 'elixir', + title: 'Elixir', + }, + { + id: 'elm', + title: 'Elm', + }, + { + id: 'erlang', + title: 'Erlang', + }, + { + id: 'fortran', + title: 'Fortran', + }, + { + id: 'go', + title: 'Go', + }, + { + id: 'graphql', + title: 'GraphQL', + }, + { + id: 'haskell', + title: 'Haskell', + }, + { + id: 'java', + title: 'Java', + }, + { + id: 'javascript', + title: 'JavaScript', + }, + { + id: 'json', + title: 'JSON', + }, + { + id: 'kotlin', + title: 'Kotlin', + }, + { + id: 'lisp', + title: 'Lisp', + }, + { + id: 'lua', + title: 'Lua', + }, + { + id: 'markdown', + title: 'Markdown', + }, + { + id: 'matlab', + title: 'Matlab', + }, + { + id: 'ocaml', + title: 'OCaml', + }, + { + id: 'perl', + title: 'Perl', + }, + { + id: 'php', + title: 'PHP', + }, + { + id: 'powershell', + title: 'Powershell', + }, + { + id: 'python', + title: 'Python', + }, + { + id: 'r', + title: 'R', + }, + { + id: 'ruby', + title: 'Ruby', + }, + { + id: 'rust', + title: 'Rust', + }, + { + id: 'scala', + title: 'Scala', + }, + { + id: 'shell', + title: 'Shell', + }, + { + id: 'sql', + title: 'SQL', + }, + { + id: 'swift', + title: 'Swift', + }, + { + id: 'typescript', + title: 'TypeScript', + }, + { + id: 'xml', + title: 'XML', + }, + { + id: 'yaml', + title: 'YAML', + }, +]; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/index.ts new file mode 100644 index 0000000000..c3aa9443d1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/index.ts @@ -0,0 +1 @@ +export * from './Code'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts new file mode 100644 index 0000000000..1ec1a2e980 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/useDecorate.ts @@ -0,0 +1,33 @@ +import { BlockType } from '@/application/collab.type'; +import { decorateCode } from '@/components/editor/components/blocks/code/utils'; +import { CodeNode } from '@/components/editor/editor.type'; +import { useCallback } from 'react'; +import { BaseRange, Editor, NodeEntry, Element } from 'slate'; +import { ReactEditor } from 'slate-react'; + +export function useDecorate(editor: ReactEditor) { + return useCallback( + (entry: NodeEntry): BaseRange[] => { + const path = entry[1]; + + const blockEntry = editor.above({ + at: path, + match: (n) => + !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined && n.type === BlockType.CodeBlock, + }); + + if (!blockEntry) return []; + + const block = blockEntry[0] as CodeNode; + + if (block.type === BlockType.CodeBlock) { + const language = block.data.language; + + return decorateCode(entry, language, false); + } + + return []; + }, + [editor] + ); +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts new file mode 100644 index 0000000000..458d9e8d7b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/utils.ts @@ -0,0 +1,137 @@ +import Prism from 'prismjs'; + +import 'prismjs/components/prism-bash'; +import 'prismjs/components/prism-basic'; +import 'prismjs/components/prism-c'; +import 'prismjs/components/prism-clojure'; +import 'prismjs/components/prism-cpp'; +import 'prismjs/components/prism-csp'; +import 'prismjs/components/prism-css'; +import 'prismjs/components/prism-dart'; +import 'prismjs/components/prism-elixir'; +import 'prismjs/components/prism-elm'; +import 'prismjs/components/prism-erlang'; +import 'prismjs/components/prism-fortran'; +import 'prismjs/components/prism-go'; +import 'prismjs/components/prism-graphql'; +import 'prismjs/components/prism-haskell'; +import 'prismjs/components/prism-java'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-json'; +import 'prismjs/components/prism-kotlin'; +import 'prismjs/components/prism-lisp'; +import 'prismjs/components/prism-lua'; +import 'prismjs/components/prism-markdown'; +import 'prismjs/components/prism-matlab'; +import 'prismjs/components/prism-ocaml'; +import 'prismjs/components/prism-perl'; +import 'prismjs/components/prism-php'; +import 'prismjs/components/prism-powershell'; +import 'prismjs/components/prism-python'; +import 'prismjs/components/prism-r'; +import 'prismjs/components/prism-ruby'; +import 'prismjs/components/prism-rust'; +import 'prismjs/components/prism-scala'; +import 'prismjs/components/prism-shell-session'; +import 'prismjs/components/prism-sql'; +import 'prismjs/components/prism-swift'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-xml-doc'; +import 'prismjs/components/prism-yaml'; + +import { BaseRange, NodeEntry, Text, Path } from 'slate'; + +const push_string = ( + token: string | Prism.Token, + path: Path, + start: number, + ranges: BaseRange[], + token_type = 'text' +) => { + let newStart = start; + + ranges.push({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prism_token: token_type, + anchor: { path, offset: newStart }, + focus: { path, offset: newStart + token.length }, + }); + newStart += token.length; + return newStart; +}; + +// This recurses through the Prism.tokenizes result and creates stylized ranges based on the token type +const recurseTokenize = ( + token: string | Prism.Token, + path: Path, + ranges: BaseRange[], + start: number, + parent_tag?: string +) => { + // Uses the parent's token type if a Token only has a string as its content + if (typeof token === 'string') { + return push_string(token, path, start, ranges, parent_tag); + } + + if ('content' in token) { + if (token.content instanceof Array) { + // Calls recurseTokenize on nested Tokens in content + let newStart = start; + + for (const subToken of token.content) { + newStart = recurseTokenize(subToken, path, ranges, newStart, token.type) || 0; + } + + return newStart; + } + + return push_string(token.content, path, start, ranges, token.type); + } +}; + +function switchCodeTheme(isDark: boolean) { + const link = document.getElementById('prism-css'); + + if (link && link.classList.contains('dark') === isDark) { + return; + } + + if (link) { + document.head.removeChild(link); + } + + const newLink = document.createElement('link'); + + newLink.rel = 'stylesheet'; + newLink.href = isDark + ? 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-dark.min.css' + : 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism.min.css'; + newLink.id = 'prism-css'; + newLink.classList.add(isDark ? 'dark' : 'light'); + document.head.appendChild(newLink); +} + +export const decorateCode = ([node, path]: NodeEntry, language: string, isDark: boolean) => { + switchCodeTheme(isDark); + + const ranges: BaseRange[] = []; + + if (!Text.isText(node)) { + return ranges; + } + + try { + const tokens = Prism.tokenize(node.text, Prism.languages[language.toLowerCase()]); + + let start = 0; + + for (const token of tokens) { + start = recurseTokenize(token, path, ranges, start) || 0; + } + + return ranges; + } catch { + return ranges; + } +}; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx new file mode 100644 index 0000000000..1e6bbb151a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -0,0 +1,121 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useNavigateToView } from '@/application/folder-yjs'; +import { getCurrentWorkspace } from 'src/application/services/js-services/session'; +import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import { Database } from '@/components/database'; +import { useGetDatabaseId, useLoadDatabase } from '@/components/database/Database.hooks'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; +import { Tooltip } from '@mui/material'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { forwardRef, memo, useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BlockType } from '@/application/collab.type'; + +export const DatabaseBlock = memo( + forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => { + const { t } = useTranslation(); + const viewId = node.data.view_id; + const type = node.type; + const navigateToView = useNavigateToView(); + const [isHovering, setIsHovering] = useState(false); + const [databaseViewId, setDatabaseViewId] = useState<string | undefined>(viewId); + const style = useMemo(() => { + const style = {}; + + switch (type) { + case BlockType.GridBlock: + Object.assign(style, { + height: 360, + }); + break; + case BlockType.CalendarBlock: + case BlockType.BoardBlock: + Object.assign(style, { + height: 560, + }); + } + + return style; + }, [type]); + + const handleNavigateToRow = useCallback( + async (rowId: string) => { + const workspace = await getCurrentWorkspace(); + + if (!workspace) return; + + const url = `/view/${workspace.id}/${databaseViewId}?r=${rowId}`; + + window.open(url, '_blank'); + }, + [databaseViewId] + ); + const databaseId = useGetDatabaseId(viewId); + + const { doc, rows, notFound } = useLoadDatabase({ + databaseId, + }); + + return ( + <> + <div + {...attributes} + className={`relative w-full cursor-pointer py-2`} + onMouseEnter={() => setIsHovering(true)} + onMouseLeave={() => setIsHovering(false)} + > + <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> + {children} + </div> + <div contentEditable={false} style={style} className={`container-bg relative flex w-full flex-col px-3`}> + {viewId && doc && rows ? ( + <IdProvider objectId={viewId}> + <DatabaseContextProvider + navigateToRow={handleNavigateToRow} + viewId={databaseViewId || viewId} + databaseDoc={doc} + rowDocMap={rows} + readOnly={true} + > + <Database iidIndex={viewId} viewId={databaseViewId || viewId} onNavigateToView={setDatabaseViewId} /> + </DatabaseContextProvider> + {isHovering && ( + <div className={'absolute right-4 top-1'}> + <Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}> + <button + color={'primary'} + className={'rounded border border-line-divider bg-bg-body p-1 hover:bg-fill-list-hover'} + onClick={() => { + navigateToView?.(viewId); + }} + > + <ExpandMoreIcon /> + </button> + </Tooltip> + </div> + )} + </IdProvider> + ) : ( + <div + className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'} + > + {notFound ? ( + <> + <div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div> + <div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div> + </> + ) : ( + <CircularProgress /> + )} + </div> + )} + </div> + </div> + </> + ); + }), + (prevProps, nextProps) => prevProps.node.data.view_id === nextProps.node.data.view_id +); + +export default DatabaseBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts new file mode 100644 index 0000000000..8eaf478025 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts @@ -0,0 +1 @@ +export * from './DatabaseBlock'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx new file mode 100644 index 0000000000..450f865b79 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/DividerNode.tsx @@ -0,0 +1,30 @@ +import { EditorElementProps, DividerNode as DividerBlock } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; +import { useSelected } from 'slate-react'; + +export const DividerNode = memo( + forwardRef<HTMLDivElement, EditorElementProps<DividerBlock>>( + ({ node: _node, children: children, ...attributes }, ref) => { + const selected = useSelected(); + + const className = useMemo(() => { + return `${attributes.className ?? ''} divider-node relative w-full rounded ${ + selected ? 'bg-content-blue-100' : '' + }`; + }, [attributes.className, selected]); + + return ( + <div {...attributes} className={className}> + <div contentEditable={false} className={'w-full px-1 py-2'}> + <hr className={'border-line-border'} /> + </div> + <div ref={ref} className={`absolute h-full w-full caret-transparent`}> + {children} + </div> + </div> + ); + } + ) +); + +export default DividerNode; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/index.ts new file mode 100644 index 0000000000..8f6141749a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/divider/index.ts @@ -0,0 +1 @@ +export * from './DividerNode'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx new file mode 100644 index 0000000000..8d4351a2d0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/Heading.tsx @@ -0,0 +1,20 @@ +import { getHeadingCssProperty } from './utils'; +import { EditorElementProps, HeadingNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; + +export const Heading = memo( + forwardRef<HTMLDivElement, EditorElementProps<HeadingNode>>(({ node, children, ...attributes }, ref) => { + const level = node.data.level; + const fontSizeCssProperty = getHeadingCssProperty(level); + + const className = `${attributes.className ?? ''} ${fontSizeCssProperty} level-${level}`; + + return ( + <div {...attributes} ref={ref} id={`heading-${node.blockId}`} className={className}> + {children} + </div> + ); + }) +); + +export default Heading; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/index.ts new file mode 100644 index 0000000000..a202e12acd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/index.ts @@ -0,0 +1,2 @@ +export * from './utils'; +export * from './Heading'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts new file mode 100644 index 0000000000..22fe53980a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/heading/utils.ts @@ -0,0 +1,18 @@ +export function getHeadingCssProperty(level: number) { + switch (level) { + case 1: + return 'text-3xl pt-[10px] max-md:pt-[1.5vw] pb-[4px] max-md:pb-[1vw] font-bold max-sm:text-[6vw]'; + case 2: + return 'text-2xl pt-[8px] max-md:pt-[1vw] pb-[2px] max-md:pb-[0.5vw] font-bold max-sm:text-[5vw]'; + case 3: + return 'text-xl pt-[4px] font-bold max-sm:text-[4vw]'; + case 4: + return 'text-lg pt-[4px] font-bold'; + case 5: + return 'pt-[4px] font-bold'; + case 6: + return 'pt-[4px] font-bold'; + default: + return ''; + } +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx new file mode 100644 index 0000000000..50de92cc66 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageBlock.tsx @@ -0,0 +1,56 @@ +import { AlignType } from '@/application/collab.type'; +import { EditorElementProps, ImageBlockNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useCallback, useMemo, useRef } from 'react'; +import { ReactEditor, useSelected, useSlateStatic } from 'slate-react'; +import ImageEmpty from './ImageEmpty'; +import ImageRender from './ImageRender'; + +export const ImageBlock = memo( + forwardRef<HTMLDivElement, EditorElementProps<ImageBlockNode>>(({ node, children, className, ...attributes }, ref) => { + const selected = useSelected(); + const { url, align } = useMemo(() => node.data || {}, [node.data]); + const containerRef = useRef<HTMLDivElement>(null); + const editor = useSlateStatic(); + const onFocusNode = useCallback(() => { + ReactEditor.focus(editor); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); + }, [editor, node]); + + const alignCss = useMemo(() => { + if (!align) return ''; + + return align === AlignType.Center ? 'justify-center' : align === AlignType.Right ? 'justify-end' : 'justify-start'; + }, [align]); + + return ( + <div + {...attributes} + ref={containerRef} + onClick={() => { + if (!selected) onFocusNode(); + }} + className={`${className || ''} image-block relative w-full cursor-pointer py-1`} + > + <div ref={ref} className={'absolute left-0 top-0 h-full w-full select-none caret-transparent'}> + {children} + </div> + <div + contentEditable={false} + className={`flex w-full select-none ${url ? '' : 'rounded border'} ${ + selected ? 'border-fill-list-hover' : 'border-line-divider' + } ${alignCss}`} + > + {url ? ( + <ImageRender selected={selected} node={node} /> + ) : ( + <ImageEmpty node={node} onEscape={onFocusNode} containerRef={containerRef} /> + )} + </div> + </div> + ); + }) +); + +export default ImageBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx new file mode 100644 index 0000000000..32ea2881f9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageEmpty.tsx @@ -0,0 +1,23 @@ +import { ImageBlockNode } from '@/components/editor/editor.type'; +import React from 'react'; +import { ReactComponent as ImageIcon } from '$icons/16x/image.svg'; +import { useTranslation } from 'react-i18next'; + +function ImageEmpty(_: { containerRef: React.RefObject<HTMLDivElement>; onEscape: () => void; node: ImageBlockNode }) { + const { t } = useTranslation(); + + return ( + <> + <div + className={ + 'container-bg flex h-[48px] w-full cursor-pointer select-none items-center gap-[10px] bg-content-blue-50 px-4 text-text-caption' + } + > + <ImageIcon /> + {t('document.plugins.image.addAnImage')} + </div> + </> + ); +} + +export default ImageEmpty; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx new file mode 100644 index 0000000000..55677506b3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/ImageRender.tsx @@ -0,0 +1,77 @@ +import { ImageBlockNode } from '@/components/editor/editor.type'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { CircularProgress } from '@mui/material'; +import { ErrorOutline } from '@mui/icons-material'; + +const MIN_WIDTH = 100; + +function ImageRender({ selected, node }: { selected: boolean; node: ImageBlockNode }) { + const [loading, setLoading] = useState(true); + const [hasError, setHasError] = useState(false); + + const imgRef = useRef<HTMLImageElement>(null); + const { url = '', width: imageWidth } = useMemo(() => node.data || {}, [node.data]); + const { t } = useTranslation(); + const blockId = node.blockId; + const [initialWidth, setInitialWidth] = useState<number | null>(null); + const [newWidth] = useState<number | null>(imageWidth ?? null); + + useEffect(() => { + if (!loading && !hasError && initialWidth === null && imgRef.current) { + setInitialWidth(imgRef.current.offsetWidth); + } + }, [hasError, initialWidth, loading]); + const imageProps: React.ImgHTMLAttributes<HTMLImageElement> = useMemo(() => { + return { + style: { width: loading || hasError ? '0' : newWidth ?? '100%', opacity: selected ? 0.8 : 1 }, + className: 'object-cover', + ref: imgRef, + src: url, + draggable: false, + onLoad: () => { + setHasError(false); + setLoading(false); + }, + onError: () => { + setHasError(true); + setLoading(false); + }, + }; + }, [url, newWidth, loading, hasError, selected]); + + const renderErrorNode = useCallback(() => { + return ( + <div + className={'flex h-full w-full items-center justify-center gap-2 rounded border border-function-error bg-red-50'} + > + <ErrorOutline className={'text-function-error'} /> + <div className={'text-function-error'}>{t('editor.imageLoadFailed')}</div> + </div> + ); + }, [t]); + + if (!url) return null; + + return ( + <div + style={{ + minWidth: MIN_WIDTH, + width: 'fit-content', + }} + className={`image-render relative min-h-[48px] ${hasError ? 'w-full' : ''}`} + > + <img loading={'lazy'} {...imageProps} alt={`image-${blockId}`} /> + {hasError ? ( + renderErrorNode() + ) : loading ? ( + <div className={'flex h-full w-full items-center justify-center gap-2 rounded bg-gray-100'}> + <CircularProgress size={24} /> + <div className={'text-text-caption'}>{t('editor.loading')}</div> + </div> + ) : null} + </div> + ); +} + +export default ImageRender; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/image/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/index.ts new file mode 100644 index 0000000000..73c3003a92 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/image/index.ts @@ -0,0 +1 @@ +export * from './ImageBlock'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx new file mode 100644 index 0000000000..6f6ba420e1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx @@ -0,0 +1,45 @@ +import KatexMath from '@/components/_shared/katex-math/KatexMath'; +import { EditorElementProps, MathEquationNode } from '@/components/editor/editor.type'; +import { FunctionsOutlined } from '@mui/icons-material'; +import { forwardRef, memo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +export const MathEquation = memo( + forwardRef<HTMLDivElement, EditorElementProps<MathEquationNode>>( + ({ node, children, className, ...attributes }, ref) => { + const formula = node.data.formula; + const { t } = useTranslation(); + const containerRef = useRef<HTMLDivElement>(null); + + return ( + <> + <div + {...attributes} + ref={containerRef} + className={`${className} math-equation-block relative w-full cursor-pointer py-2`} + > + <div + contentEditable={false} + className={`container-bg w-full select-none rounded border border-line-divider bg-fill-list-active px-3`} + > + {formula ? ( + <KatexMath latex={formula} /> + ) : ( + <div className={'flex h-[48px] w-full items-center gap-[10px] text-text-caption'}> + <FunctionsOutlined /> + {t('document.plugins.mathEquation.addMathEquation')} + </div> + )} + </div> + <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> + {children} + </div> + </div> + </> + ); + } + ), + (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) +); + +export default MathEquation; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts new file mode 100644 index 0000000000..d10b172020 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const MathEquation = lazy(() => import('./MathEquation')); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx new file mode 100644 index 0000000000..12b4c5d0e2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx @@ -0,0 +1,84 @@ +import { NumberedListNode } from '@/components/editor/editor.type'; +import { getListLevel, letterize, romanize } from '@/components/editor/utils/list'; +import React, { useMemo } from 'react'; +import { ReactEditor, useSlate, useSlateStatic } from 'slate-react'; +import { Element, Path } from 'slate'; + +enum Letter { + Number = 'number', + Letter = 'letter', + Roman = 'roman', +} + +function getLetterNumber(index: number, letter: Letter) { + if (letter === Letter.Number) { + return index; + } else if (letter === Letter.Letter) { + return letterize(index); + } else { + return romanize(index); + } +} + +export function NumberListIcon({ block, className }: { block: NumberedListNode; className: string }) { + const editor = useSlate(); + const staticEditor = useSlateStatic(); + + const path = ReactEditor.findPath(editor, block); + const index = useMemo(() => { + let index = 1; + + let topNode; + let prevPath = Path.previous(path); + + while (prevPath) { + const prev = editor.node(prevPath); + + const prevNode = prev[0] as Element; + + if (prevNode.type === block.type) { + index += 1; + topNode = prevNode; + } else { + break; + } + + prevPath = Path.previous(prevPath); + } + + if (!topNode) { + return Number(block.data?.number ?? 1); + } + + const startIndex = (topNode as NumberedListNode).data?.number ?? 1; + + return index + Number(startIndex) - 1; + }, [editor, block, path]); + + const letter = useMemo(() => { + const level = getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Number; + } else if (level % 3 === 1) { + return Letter.Letter; + } else { + return Letter.Roman; + } + }, [block.type, staticEditor, path]); + + const dataNumber = useMemo(() => { + return getLetterNumber(index, letter); + }, [index, letter]); + + return ( + <span + onMouseDown={(e) => { + e.preventDefault(); + }} + contentEditable={false} + data-number={dataNumber} + className={`${className} numbered-icon flex w-[24px] min-w-[24px] justify-center pr-1 font-medium`} + /> + ); +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx new file mode 100644 index 0000000000..9b10389944 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx @@ -0,0 +1,16 @@ +import { EditorElementProps, NumberedListNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; + +export const NumberedList = memo( + forwardRef<HTMLDivElement, EditorElementProps<NumberedListNode>>( + ({ node: _, children, className, ...attributes }, ref) => { + return ( + <div ref={ref} {...attributes} className={`${className}`}> + {children} + </div> + ); + } + ) +); + +export default NumberedList; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts new file mode 100644 index 0000000000..df030e8e83 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts @@ -0,0 +1,2 @@ +export * from './NumberedList'; +export * from './NumberListIcon'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx new file mode 100644 index 0000000000..e26c066a71 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/Outline.tsx @@ -0,0 +1,70 @@ +import { extractHeadings, nestHeadings } from '@/components/editor/components/blocks/outline/utils'; +import { EditorElementProps, HeadingNode, OutlineNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSlate } from 'slate-react'; +import smoothScrollIntoViewIfNeeded from 'smooth-scroll-into-view-if-needed'; + +export const Outline = memo( + forwardRef<HTMLDivElement, EditorElementProps<OutlineNode>>(({ node, children, className, ...attributes }, ref) => { + const editor = useSlate(); + const [root, setRoot] = useState<HeadingNode[]>([]); + const { t } = useTranslation(); + + useEffect(() => { + const root = nestHeadings(extractHeadings(editor, node.data.depth || 6)); + + setRoot(root); + }, [editor, node.data.depth]); + + const jumpToHeading = useCallback((heading: HeadingNode) => { + const id = `heading-${heading.blockId}`; + + const element = document.getElementById(id); + + if (element) { + void smoothScrollIntoViewIfNeeded(element, { + behavior: 'smooth', + block: 'center', + }); + } + }, []); + + const renderHeading = useCallback( + (heading: HeadingNode, index: number) => { + const children = (heading.children as HeadingNode[]).map(renderHeading); + const { text, level } = heading.data as { text: string; level: number }; + + return ( + <div + onClick={(e) => { + e.stopPropagation(); + jumpToHeading(heading); + }} + className={`my-1 ml-4 `} + key={`${level}-${index}`} + > + <div className={'cursor-pointer rounded px-2 underline hover:text-content-blue-400'}>{text}</div> + + <div className={'ml-2'}>{children}</div> + </div> + ); + }, + [jumpToHeading] + ); + + return ( + <div {...attributes} className={`outline-block relative my-2 px-1 ${className || ''}`}> + <div ref={ref} className={'absolute left-0 top-0 select-none caret-transparent'}> + {children} + </div> + <div contentEditable={false} className={`flex w-full select-none flex-col`}> + <div className={'text-md my-2 font-bold'}>{t('document.outlineBlock.placeholder')}</div> + {root.map(renderHeading)} + </div> + </div> + ); + }) +); + +export default Outline; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/index.ts new file mode 100644 index 0000000000..739fdf04f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/index.ts @@ -0,0 +1 @@ +export * from './Outline'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/utils.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/utils.ts new file mode 100644 index 0000000000..57105fef5a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/outline/utils.ts @@ -0,0 +1,55 @@ +import { BlockType } from '@/application/collab.type'; +import { CustomEditor } from '@/components/editor/command'; +import { HeadingNode } from '@/components/editor/editor.type'; +import { Element, Text } from 'slate'; +import { ReactEditor } from 'slate-react'; + +export function extractHeadings(editor: ReactEditor, maxDepth: number): HeadingNode[] { + const headings: HeadingNode[] = []; + const blocks = editor.children; + + function traverse(children: (Element | Text)[]) { + for (const block of children) { + if (Text.isText(block)) continue; + if (block.type === BlockType.HeadingBlock && (block as HeadingNode).data?.level <= maxDepth) { + headings.push({ + ...block, + data: { + level: (block as HeadingNode).data.level, + text: CustomEditor.getBlockTextContent(block), + }, + children: [], + } as HeadingNode); + } else { + traverse(block.children); + } + } + + return headings; + } + + return traverse(blocks); +} + +export function nestHeadings(headings: HeadingNode[]): HeadingNode[] { + const root: HeadingNode[] = []; + const stack: HeadingNode[] = []; + + headings.forEach((heading) => { + const node = { ...heading, children: [] }; + + while (stack.length > 0 && stack[stack.length - 1].data.level >= node.data.level) { + stack.pop(); + } + + if (stack.length === 0) { + root.push(node); + } else { + stack[stack.length - 1].children.push(node); + } + + stack.push(node); + }); + + return root; +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/page/Page.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/page/Page.tsx new file mode 100644 index 0000000000..e0d3b4e293 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/page/Page.tsx @@ -0,0 +1,18 @@ +import { EditorElementProps, PageNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; + +export const Page = memo( + forwardRef<HTMLDivElement, EditorElementProps<PageNode>>(({ node: _, children, ...attributes }, ref) => { + const className = useMemo(() => { + return `${attributes.className ?? ''} document-title pb-3 text-5xl font-bold`; + }, [attributes.className]); + + return ( + <div ref={ref} {...attributes} className={className}> + {children} + </div> + ); + }) +); + +export default Page; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/page/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/page/index.ts new file mode 100644 index 0000000000..d9925d7520 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/page/index.ts @@ -0,0 +1 @@ +export * from './Page'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/Paragraph.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/Paragraph.tsx new file mode 100644 index 0000000000..ad6737510e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/Paragraph.tsx @@ -0,0 +1,14 @@ +import { EditorElementProps, ParagraphNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; + +export const Paragraph = memo( + forwardRef<HTMLDivElement, EditorElementProps<ParagraphNode>>(({ node: _, children, ...attributes }, ref) => { + { + return ( + <div ref={ref} {...attributes} className={`${attributes.className ?? ''}`}> + {children} + </div> + ); + } + }) +); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/index.ts new file mode 100644 index 0000000000..01752c914c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/paragraph/index.ts @@ -0,0 +1 @@ +export * from './Paragraph'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx new file mode 100644 index 0000000000..0ddc0af985 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/Quote.tsx @@ -0,0 +1,18 @@ +import { EditorElementProps, QuoteNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; + +export const Quote = memo( + forwardRef<HTMLDivElement, EditorElementProps<QuoteNode>>(({ node: _, children, ...attributes }, ref) => { + const className = useMemo(() => { + return `flex w-full flex-col ml-3 border-l-[4px] border-fill-default pl-2 ${attributes.className ?? ''}`; + }, [attributes.className]); + + return ( + <div {...attributes} ref={ref} className={className}> + {children} + </div> + ); + }) +); + +export default Quote; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/index.ts new file mode 100644 index 0000000000..c88e677a53 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/quote/index.ts @@ -0,0 +1 @@ +export * from './Quote'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx new file mode 100644 index 0000000000..ae0522a1da --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/Table.tsx @@ -0,0 +1,58 @@ +import { EditorElementProps, TableCellNode, TableNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; +import { Grid } from '@atlaskit/primitives'; +import './table.scss'; +import isEqual from 'lodash-es/isEqual'; + +const Table = memo( + forwardRef<HTMLDivElement, EditorElementProps<TableNode>>(({ node, children, className, ...attributes }, ref) => { + const { rowsLen, colsLen, rowDefaultHeight, colsHeight } = node.data; + const cells = node.children as TableCellNode[]; + + const columnGroup = useMemo(() => { + return Array.from({ length: colsLen }, (_, index) => { + return cells.filter((cell) => cell.data.colPosition === index); + }); + }, [cells, colsLen]); + + const rowGroup = useMemo(() => { + return Array.from({ length: rowsLen }, (_, index) => { + return cells.filter((cell) => cell.data.rowPosition === index); + }); + }, [cells, rowsLen]); + + const templateColumns = useMemo(() => { + return columnGroup + .map((group) => { + return `${group[0].data.width || colsHeight}px`; + }) + .join(' '); + }, [colsHeight, columnGroup]); + + const templateRows = useMemo(() => { + return rowGroup + .map((group) => { + return `${group[0].data.height || rowDefaultHeight}px`; + }) + .join(' '); + }, [rowGroup, rowDefaultHeight]); + + return ( + <div ref={ref} {...attributes} className={`table-block relative my-2 w-full px-1 ${className || ''}`}> + <Grid + id={`table-${node.blockId}`} + rowGap='space.0' + autoFlow='column' + columnGap='space.0' + templateRows={templateRows} + templateColumns={templateColumns} + > + {children} + </Grid> + </div> + ); + }), + (prevProps, nextProps) => isEqual(prevProps.node, nextProps.node) +); + +export default Table; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx new file mode 100644 index 0000000000..b2a01d5c8c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/TableCell.tsx @@ -0,0 +1,16 @@ +import { EditorElementProps, TableCellNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; + +const TableCell = memo( + forwardRef<HTMLDivElement, EditorElementProps<TableCellNode>>( + ({ node: _, children, className, ...attributes }, ref) => { + return ( + <div ref={ref} {...attributes} className={`relative table-cell text-left ${className || ''}`}> + {children} + </div> + ); + } + ) +); + +export default TableCell; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/index.ts new file mode 100644 index 0000000000..99a5478645 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/index.ts @@ -0,0 +1,3 @@ +export { default as TableBlock } from './Table'; + +export { default as TableCellBlock } from './TableCell'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss new file mode 100644 index 0000000000..1aa812f6c5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/table/table.scss @@ -0,0 +1,10 @@ +.table-block { + [id^=table-] { + width: fit-content; + @apply border-t border-l border-line-border; + } + + .table-cell { + @apply border-b border-r border-line-border; + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx new file mode 100644 index 0000000000..08a66de9d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Placeholder.tsx @@ -0,0 +1,134 @@ +import { BlockType } from '@/application/collab.type'; +import { HeadingNode } from '@/components/editor/editor.type'; +import { useEditorContext } from '@/components/editor/EditorContext'; +import React, { CSSProperties, useEffect, useMemo, useState } from 'react'; +import { ReactEditor, useSelected, useSlate } from 'slate-react'; +import { Editor, Element, Range } from 'slate'; +import { useTranslation } from 'react-i18next'; + +function Placeholder({ node, ...attributes }: { node: Element; className?: string; style?: CSSProperties }) { + const { t } = useTranslation(); + const { readOnly } = useEditorContext(); + const editor = useSlate(); + + const selected = useSelected() && !readOnly && !!editor.selection && Range.isCollapsed(editor.selection); + const [isComposing, setIsComposing] = useState(false); + const block = useMemo(() => { + const path = ReactEditor.findPath(editor, node); + const match = Editor.above(editor, { + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined && n.type !== undefined; + }, + at: path, + }); + + if (!match) return null; + + return match[0] as Element; + }, [editor, node]); + + const className = useMemo(() => { + return `text-placeholder select-none ${attributes.className ?? ''}`; + }, [attributes.className]); + + const unSelectedPlaceholder = useMemo(() => { + switch (block?.type) { + case BlockType.Paragraph: { + if (editor.children.length === 1 && !readOnly) { + return t('editor.slashPlaceHolder'); + } + + return ''; + } + + case BlockType.ToggleListBlock: + return t('blockPlaceholders.bulletList'); + case BlockType.QuoteBlock: + return t('blockPlaceholders.quote'); + case BlockType.TodoListBlock: + return t('blockPlaceholders.todoList'); + case BlockType.NumberedListBlock: + return t('blockPlaceholders.numberList'); + case BlockType.BulletedListBlock: + return t('blockPlaceholders.bulletList'); + case BlockType.HeadingBlock: { + const level = (block as HeadingNode).data.level; + + switch (level) { + case 1: + return t('editor.mobileHeading1'); + case 2: + return t('editor.mobileHeading2'); + case 3: + return t('editor.mobileHeading3'); + default: + return ''; + } + } + + case BlockType.Page: + return t('document.title.placeholder'); + case BlockType.CalloutBlock: + case BlockType.CodeBlock: + return t('editor.typeSomething'); + default: + return ''; + } + }, [readOnly, block, t, editor.children.length]); + + const selectedPlaceholder = useMemo(() => { + switch (block?.type) { + case BlockType.HeadingBlock: + return unSelectedPlaceholder; + case BlockType.Page: + return t('document.title.placeholder'); + case BlockType.GridBlock: + case BlockType.EquationBlock: + case BlockType.CodeBlock: + case BlockType.DividerBlock: + return ''; + + default: + return t('editor.slashPlaceHolder'); + } + }, [block?.type, t, unSelectedPlaceholder]); + + useEffect(() => { + if (!selected) return; + + const handleCompositionStart = () => { + setIsComposing(true); + }; + + const handleCompositionEnd = () => { + setIsComposing(false); + }; + + const editorDom = ReactEditor.toDOMNode(editor, editor); + + // placeholder should be hidden when composing + editorDom.addEventListener('compositionstart', handleCompositionStart); + editorDom.addEventListener('compositionend', handleCompositionEnd); + editorDom.addEventListener('compositionupdate', handleCompositionStart); + return () => { + editorDom.removeEventListener('compositionstart', handleCompositionStart); + editorDom.removeEventListener('compositionend', handleCompositionEnd); + editorDom.removeEventListener('compositionupdate', handleCompositionStart); + }; + }, [editor, selected]); + + if (isComposing) { + return null; + } + + return ( + <span + data-placeholder={selected && !readOnly ? selectedPlaceholder : unSelectedPlaceholder} + contentEditable={false} + {...attributes} + className={className} + /> + ); +} + +export default Placeholder; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx new file mode 100644 index 0000000000..2ab5996b09 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -0,0 +1,47 @@ +import { BlockType } from '@/application/collab.type'; +import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted-list'; +import { NumberListIcon } from '@/components/editor/components/blocks/numbered-list'; +import ToggleIcon from '@/components/editor/components/blocks/toggle-list/ToggleIcon'; +import { TextNode } from '@/components/editor/editor.type'; +import React, { FC, useCallback, useMemo } from 'react'; +import { ReactEditor, useSlate } from 'slate-react'; +import { Editor, Element } from 'slate'; +import CheckboxIcon from '@/components/editor/components/blocks/todo-list/CheckboxIcon'; + +export function useStartIcon(node: TextNode) { + const editor = useSlate(); + const path = ReactEditor.findPath(editor, node); + const block = Editor.parent(editor, path)?.[0] as Element | null; + + const Component = useMemo(() => { + if (!Element.isElement(block)) { + return null; + } + + switch (block.type) { + case BlockType.TodoListBlock: + return CheckboxIcon; + case BlockType.ToggleListBlock: + return ToggleIcon; + case BlockType.NumberedListBlock: + return NumberListIcon; + case BlockType.BulletedListBlock: + return BulletedListIcon; + default: + return null; + } + }, [block]) as FC<{ block: Element; className: string }> | null; + + const renderIcon = useCallback(() => { + if (!Component || !block) { + return null; + } + + return <Component className={`text-block-icon relative h-[24px] w-[24px]`} block={block} />; + }, [Component, block]); + + return { + hasStartIcon: !!Component, + renderIcon, + }; +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx new file mode 100644 index 0000000000..a9843570ea --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/Text.tsx @@ -0,0 +1,31 @@ +import Placeholder from '@/components/editor/components/blocks/text/Placeholder'; +import { useSlateStatic } from 'slate-react'; +import { useStartIcon } from './StartIcon.hooks'; +import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; + +export const Text = memo( + forwardRef<HTMLSpanElement, EditorElementProps<TextNode>>( + ({ node, children, className: classNameProp, ...attributes }, ref) => { + const { hasStartIcon, renderIcon } = useStartIcon(node); + const editor = useSlateStatic(); + const isEmpty = editor.isEmpty(node); + const className = useMemo(() => { + const classList = ['text-element', 'relative', 'flex', 'w-full', 'whitespace-pre-wrap', 'break-all', 'px-1']; + + if (classNameProp) classList.push(classNameProp); + if (hasStartIcon) classList.push('has-start-icon'); + return classList.join(' '); + }, [classNameProp, hasStartIcon]); + + return ( + <span {...attributes} ref={ref} className={className}> + {renderIcon()} + {isEmpty && <Placeholder node={node} />} + <span className={`text-content ${isEmpty ? 'empty-text' : ''}`}>{children}</span> + </span> + ); + } + ), + (prevProps, nextProps) => JSON.stringify(prevProps.node) === JSON.stringify(nextProps.node) +); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/index.ts new file mode 100644 index 0000000000..b0c76af0b0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/index.ts @@ -0,0 +1 @@ +export * from './Text'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx new file mode 100644 index 0000000000..007903b89a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx @@ -0,0 +1,24 @@ +import { TodoListNode } from '@/components/editor/editor.type'; +import React from 'react'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; + +function CheckboxIcon({ block, className }: { block: TodoListNode; className: string }) { + const { checked } = block.data; + + return ( + <span + data-playwright-selected={false} + contentEditable={false} + draggable={false} + onMouseDown={(e) => { + e.preventDefault(); + }} + className={`${className} cursor-pointer pr-1 text-xl text-fill-default`} + > + {checked ? <CheckboxCheckSvg /> : <CheckboxUncheckSvg />} + </span> + ); +} + +export default CheckboxIcon; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx new file mode 100644 index 0000000000..f0356d4cbf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx @@ -0,0 +1,17 @@ +import { EditorElementProps, TodoListNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; + +export const TodoList = memo( + forwardRef<HTMLDivElement, EditorElementProps<TodoListNode>>(({ node, children, ...attributes }, ref) => { + const { checked = false } = useMemo(() => node.data || {}, [node.data]); + const className = useMemo(() => { + return `flex w-full flex-col ${checked ? 'checked' : ''} ${attributes.className ?? ''}`; + }, [attributes.className, checked]); + + return ( + <div {...attributes} ref={ref} className={className}> + {children} + </div> + ); + }) +); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts new file mode 100644 index 0000000000..f239f43459 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts @@ -0,0 +1 @@ +export * from './TodoList'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx new file mode 100644 index 0000000000..51ed844e82 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx @@ -0,0 +1,22 @@ +import { ToggleListNode } from '@/components/editor/editor.type'; +import React from 'react'; +import { ReactComponent as ExpandSvg } from '$icons/16x/drop_menu_show.svg'; + +function ToggleIcon({ block, className }: { block: ToggleListNode; className: string }) { + const { collapsed } = block.data; + + return ( + <span + data-playwright-selected={false} + contentEditable={false} + onMouseDown={(e) => { + e.preventDefault(); + }} + className={`${className} cursor-pointer pr-1 text-xl hover:text-fill-default`} + > + {collapsed ? <ExpandSvg className={'-rotate-90 transform'} /> : <ExpandSvg />} + </span> + ); +} + +export default ToggleIcon; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx new file mode 100644 index 0000000000..6ccc38f757 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx @@ -0,0 +1,19 @@ +import React, { forwardRef, memo, useMemo } from 'react'; +import { EditorElementProps, ToggleListNode } from '@/components/editor/editor.type'; + +export const ToggleList = memo( + forwardRef<HTMLDivElement, EditorElementProps<ToggleListNode>>(({ node, children, ...attributes }, ref) => { + const { collapsed } = useMemo(() => node.data || {}, [node.data]); + const className = `${attributes.className ?? ''} flex w-full flex-col ${collapsed ? 'collapsed' : ''}`; + + return ( + <> + <div {...attributes} ref={ref} className={className}> + {children} + </div> + </> + ); + }) +); + +export default ToggleList; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts new file mode 100644 index 0000000000..833bdb5210 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts @@ -0,0 +1 @@ +export * from './ToggleList'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx new file mode 100644 index 0000000000..d117a14221 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -0,0 +1,138 @@ +import { BlockData, BlockType, InlineBlockType, YjsEditorKey } from '@/application/collab.type'; +import { BulletedList } from '@/components/editor/components/blocks/bulleted-list'; +import { Callout } from '@/components/editor/components/blocks/callout'; +import { CodeBlock } from '@/components/editor/components/blocks/code'; +import { DividerNode } from '@/components/editor/components/blocks/divider'; +import { Heading } from '@/components/editor/components/blocks/heading'; +import { ImageBlock } from '@/components/editor/components/blocks/image'; +import { MathEquation } from '@/components/editor/components/blocks/math-equation'; +import { NumberedList } from '@/components/editor/components/blocks/numbered-list'; +import { Outline } from '@/components/editor/components/blocks/outline'; +import { Page } from '@/components/editor/components/blocks/page'; +import { Paragraph } from '@/components/editor/components/blocks/paragraph'; +import { Quote } from '@/components/editor/components/blocks/quote'; +import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table'; +import { Text } from '@/components/editor/components/blocks/text'; +import { ElementFallbackRender } from '@/components/error/ElementFallbackRender'; +import { Skeleton } from '@mui/material'; +import { ErrorBoundary } from 'react-error-boundary'; +import { TodoList } from 'src/components/editor/components/blocks/todo-list'; +import { ToggleList } from 'src/components/editor/components/blocks/toggle-list'; +import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock'; +import { Formula } from '@/components/editor/components/leaf/formula'; +import { Mention } from '@/components/editor/components/leaf/mention'; +import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; +import { renderColor } from '@/utils/color'; +import React, { FC, memo, Suspense, useMemo } from 'react'; +import { RenderElementProps } from 'slate-react'; +import { DatabaseBlock } from 'src/components/editor/components/blocks/database'; +import isEqual from 'lodash-es/isEqual'; + +export const Element = memo( + ({ + element: node, + attributes, + children, + }: RenderElementProps & { + element: EditorElementProps['node']; + }) => { + const Component = useMemo(() => { + switch (node.type) { + case BlockType.HeadingBlock: + return Heading; + case BlockType.TodoListBlock: + return TodoList; + case BlockType.ToggleListBlock: + return ToggleList; + case BlockType.Paragraph: + return Paragraph; + case BlockType.DividerBlock: + return DividerNode; + case BlockType.Page: + return Page; + case BlockType.QuoteBlock: + return Quote; + case BlockType.BulletedListBlock: + return BulletedList; + case BlockType.NumberedListBlock: + return NumberedList; + case BlockType.CodeBlock: + return CodeBlock; + case BlockType.CalloutBlock: + return Callout; + case BlockType.EquationBlock: + return MathEquation; + case BlockType.ImageBlock: + return ImageBlock; + case BlockType.OutlineBlock: + return Outline; + case BlockType.TableBlock: + return TableBlock; + case BlockType.TableCell: + return TableCellBlock; + case BlockType.GridBlock: + case BlockType.BoardBlock: + case BlockType.CalendarBlock: + return DatabaseBlock; + default: + return UnSupportedBlock; + } + }, [node.type]) as FC<EditorElementProps>; + + const InlineComponent = useMemo(() => { + switch (node.type) { + case InlineBlockType.Formula: + return Formula; + case InlineBlockType.Mention: + return Mention; + default: + return null; + } + }, [node.type]) as FC<EditorElementProps>; + + const className = useMemo(() => { + const data = (node.data as BlockData) || {}; + const align = data.align; + + return `block-element flex rounded ${align ? `block-align-${align}` : ''}`; + }, [node.data]); + + const style = useMemo(() => { + const data = (node.data as BlockData) || {}; + + return { + backgroundColor: data.bg_color ? renderColor(data.bg_color) : undefined, + color: data.font_color ? renderColor(data.font_color) : undefined, + }; + }, [node.data]); + + if (InlineComponent) { + return ( + <InlineComponent {...attributes} node={node}> + {children} + </InlineComponent> + ); + } + + if (node.type === YjsEditorKey.text) { + return ( + <Text {...attributes} node={node as TextNode}> + {children} + </Text> + ); + } + + return ( + <Suspense fallback={<Skeleton width={'100%'} height={24} />}> + <ErrorBoundary fallbackRender={ElementFallbackRender}> + <div {...attributes} data-block-type={node.type} className={className}> + <Component style={style} className={`flex w-full flex-col`} node={node}> + {children} + </Component> + </div> + </ErrorBoundary> + </Suspense> + ); + }, + (prevProps, nextProps) => isEqual(prevProps.element, nextProps.element) +); diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/UnSupportedBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/UnSupportedBlock.tsx new file mode 100644 index 0000000000..99c299bc64 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/element/UnSupportedBlock.tsx @@ -0,0 +1,13 @@ +import { EditorElementProps } from '@/components/editor/editor.type'; +import React, { forwardRef } from 'react'; +import { Alert } from '@mui/material'; + +export const UnSupportedBlock = forwardRef<HTMLDivElement, EditorElementProps>(({ node }, ref) => { + return ( + <div className={'w-full'} ref={ref}> + <Alert className={'h-[48px] w-full'} severity={'error'}> + {`Unsupported block: ${node.type}`} + </Alert> + </div> + ); +}); diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/index.ts b/frontend/appflowy_web_app/src/components/editor/components/element/index.ts new file mode 100644 index 0000000000..b5347c4fe9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/element/index.ts @@ -0,0 +1 @@ +export * from './Element'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx new file mode 100644 index 0000000000..38ba65e3f2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx @@ -0,0 +1,55 @@ +import { Href } from '@/components/editor/components/leaf/href'; +import { getFontFamily } from '@/utils/font'; +import React, { CSSProperties } from 'react'; +import { RenderLeafProps } from 'slate-react'; +import { renderColor } from '@/utils/color'; + +export function Leaf({ attributes, children, leaf }: RenderLeafProps) { + let newChildren = children; + + const classList = [leaf.prism_token, leaf.prism_token && 'token', leaf.class_name].filter(Boolean); + + if (leaf.code) { + newChildren = <span className={'bg-line-divider font-medium text-[#EB5757]'}>{newChildren}</span>; + } + + if (leaf.underline) { + newChildren = <u>{newChildren}</u>; + } + + if (leaf.strikethrough) { + newChildren = <s>{newChildren}</s>; + } + + if (leaf.italic) { + newChildren = <em>{newChildren}</em>; + } + + if (leaf.bold) { + newChildren = <strong>{newChildren}</strong>; + } + + const style: CSSProperties = {}; + + if (leaf.font_color) { + style['color'] = renderColor(leaf.font_color); + } + + if (leaf.bg_color) { + style['backgroundColor'] = renderColor(leaf.bg_color); + } + + if (leaf.href) { + newChildren = <Href leaf={leaf}>{newChildren}</Href>; + } + + if (leaf.font_family) { + style['fontFamily'] = getFontFamily(leaf.font_family); + } + + return ( + <span {...attributes} style={style} className={`${classList.join(' ')}`}> + {newChildren} + </span> + ); +} diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx new file mode 100644 index 0000000000..3731fc7216 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/Formula.tsx @@ -0,0 +1,30 @@ +import KatexMath from '@/components/_shared/katex-math/KatexMath'; +import { EditorElementProps, FormulaNode } from '@/components/editor/editor.type'; +import React, { memo, forwardRef } from 'react'; +import { useSelected } from 'slate-react'; + +export const Formula = memo( + forwardRef<HTMLSpanElement, EditorElementProps<FormulaNode>>(({ node, children, ...attributes }, ref) => { + const formula = node.data; + const selected = useSelected(); + + return ( + <span + ref={ref} + {...attributes} + contentEditable={false} + className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded px-1 py-0.5 ${ + selected ? 'selected' : '' + }`} + > + <span className={'select-none'} contentEditable={false}> + <KatexMath latex={formula || ''} isInline /> + </span> + + <span className={'absolute left-0 right-0 h-0 w-0 opacity-0'}>{children}</span> + </span> + ); + }) +); + +export default Formula; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/index.ts b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/index.ts new file mode 100644 index 0000000000..1c01fca07e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/formula/index.ts @@ -0,0 +1,5 @@ +import { lazy } from 'react'; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-expect-error +export const Formula = lazy(() => import('./Formula?chunkName=formula')); diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/href/Href.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/href/Href.tsx new file mode 100644 index 0000000000..83c0ed2297 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/href/Href.tsx @@ -0,0 +1,21 @@ +import { useEditorContext } from '@/components/editor/EditorContext'; +import { openUrl } from '@/utils/url'; +import React, { memo } from 'react'; +import { Text } from 'slate'; + +export const Href = memo(({ children, leaf }: { leaf: Text; children: React.ReactNode }) => { + const readonly = useEditorContext().readOnly; + + return ( + <span + onClick={() => { + if (readonly && leaf.href) { + void openUrl(leaf.href, '_blank'); + } + }} + className={`cursor-pointer select-auto px-1 py-0.5 text-fill-default underline`} + > + {children} + </span> + ); +}); diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/href/index.ts b/frontend/appflowy_web_app/src/components/editor/components/leaf/href/index.ts new file mode 100644 index 0000000000..758b3b39d3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/href/index.ts @@ -0,0 +1 @@ +export * from './Href'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/index.ts b/frontend/appflowy_web_app/src/components/editor/components/leaf/index.ts new file mode 100644 index 0000000000..711768ed28 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/index.ts @@ -0,0 +1 @@ +export * from './Leaf'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx new file mode 100644 index 0000000000..eba846b9c1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/Mention.tsx @@ -0,0 +1,25 @@ +import { EditorElementProps, MentionNode } from '@/components/editor/editor.type'; +import React, { forwardRef, memo } from 'react'; +import { useSelected } from 'slate-react'; + +import MentionLeaf from './MentionLeaf'; + +export const Mention = memo( + forwardRef<HTMLSpanElement, EditorElementProps<MentionNode>>(({ node, children, ...attributes }, ref) => { + const selected = useSelected(); + + return ( + <span + {...attributes} + contentEditable={false} + className={`mention relative cursor-pointer ${selected ? 'selected' : ''}`} + ref={ref} + > + <span className={'absolute right-0 top-0 h-full w-0 opacity-0'}>{children}</span> + <MentionLeaf mention={node.data} /> + </span> + ); + }) +); + +export default Mention; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx new file mode 100644 index 0000000000..c430968b52 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionDate.tsx @@ -0,0 +1,20 @@ +import { renderDate } from '@/utils/time'; +import React, { useMemo } from 'react'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; + +function MentionDate({ date, reminder }: { date: string; reminder?: { id: string; option: string } }) { + const dateFormat = useMemo(() => { + return renderDate(date, 'MMM D, YYYY'); + }, [date]); + + return ( + <span className={'mention-inline'}> + {reminder ? <ReminderSvg className={'mention-icon'} /> : <DateSvg className={'mention-icon'} />} + + <span className={'mention-content'}>{dateFormat}</span> + </span> + ); +} + +export default MentionDate; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionLeaf.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionLeaf.tsx new file mode 100644 index 0000000000..360dc2a678 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionLeaf.tsx @@ -0,0 +1,24 @@ +import { Mention, MentionType } from '@/application/collab.type'; +import MentionDate from '@/components/editor/components/leaf/mention/MentionDate'; +import MentionPage from '@/components/editor/components/leaf/mention/MentionPage'; +import { useMemo } from 'react'; + +export function MentionLeaf({ mention }: { mention: Mention }) { + const { type, date, page_id, reminder_id, reminder_option } = mention; + + const reminder = useMemo(() => { + return reminder_id ? { id: reminder_id ?? '', option: reminder_option ?? '' } : undefined; + }, [reminder_id, reminder_option]); + + if (type === MentionType.PageRef && page_id) { + return <MentionPage pageId={page_id} />; + } + + if (type === MentionType.Date && date) { + return <MentionDate date={date} reminder={reminder} />; + } + + return null; +} + +export default MentionLeaf; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx new file mode 100644 index 0000000000..40e6d31e23 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/MentionPage.tsx @@ -0,0 +1,24 @@ +import { useNavigateToView } from '@/application/folder-yjs'; +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +function MentionPage({ pageId }: { pageId: string }) { + const onNavigateToView = useNavigateToView(); + const { icon, name } = usePageInfo(pageId); + + return ( + <span + onClick={() => { + onNavigateToView?.(pageId); + }} + className={`mention-inline px-1 underline`} + contentEditable={false} + > + <span className={'mention-icon'}>{icon}</span> + + <span className={'mention-content'}>{name}</span> + </span> + ); +} + +export default MentionPage; diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/index.ts b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/index.ts new file mode 100644 index 0000000000..d3ee18034d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/mention/index.ts @@ -0,0 +1 @@ +export * from './Mention'; diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss new file mode 100644 index 0000000000..71832ec330 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -0,0 +1,303 @@ + +.block-element:not([data-block-type="table/cell"]) { + @apply my-[4px]; + +} + +.block-element .block-element:not([data-block-type="table/cell"]) { + @apply mb-0; + margin-left: 24px; +} + +.block-element[data-block-type="table/cell"] { + margin-left: 0 !important; + + .block-element { + margin-left: 0 !important; + height: 100%; + } +} + +.block-element.block-align-left { + > div > .text-element { + text-align: left; + justify-content: flex-start; + } +} + +.block-element.block-align-right { + > div > .text-element { + text-align: right; + justify-content: flex-end; + } +} + +.block-element.block-align-center { + > div > .text-element { + text-align: center; + justify-content: center; + } + +} + + +.block-element[data-block-type="todo_list"] .checked > .text-element .text-content { + text-decoration: line-through; + color: var(--text-caption); +} + +.block-element .collapsed .block-element { + display: none !important; +} + +[role=textbox] { + .text-element { + @apply my-1; + &::selection { + @apply bg-transparent; + } + } +} + + +span[data-slate-placeholder="true"]:not(.inline-block-content) { + @apply text-text-placeholder; + opacity: 1 !important; +} + + +[role="textbox"] { + ::selection { + @apply bg-content-blue-100; + } + + .text-content { + &::selection { + @apply bg-transparent; + } + + &.selected { + @apply bg-content-blue-100; + } + + span { + &::selection { + @apply bg-content-blue-100; + } + } + } +} + + +[data-dark-mode="true"] [role="textbox"] { + ::selection { + background-color: #1e79a2; + } + + .text-content { + &::selection { + @apply bg-transparent; + } + + &.selected { + background-color: #1e79a2; + } + + span { + &::selection { + background-color: #1e79a2; + } + } + } +} + + +.text-content, [data-dark-mode="true"] .text-content { + @apply min-w-[1px]; + &.empty-text { + span { + &::selection { + @apply bg-transparent; + } + } + } +} + +.text-element:has(.text-placeholder), .divider-node, [data-dark-mode="true"] .text-element:has(.text-placeholder), [data-dark-mode="true"] .divider-node { + ::selection { + @apply bg-transparent; + } +} + +.text-placeholder { + @apply absolute left-[5px] transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap; + &:after { + @apply text-text-placeholder absolute top-0; + content: (attr(data-placeholder)); + } +} + +.text-placeholder { + &:after { + @apply left-0; + } +} + +.has-start-icon .text-placeholder { + &:after { + @apply left-[24px]; + } +} + +.block-align-center { + .text-placeholder { + @apply left-[calc(50%+1px)]; + &:after { + @apply left-0; + } + } + + .has-start-icon .text-placeholder { + @apply left-[calc(50%+13px)]; + &:after { + @apply left-0; + } + } + +} + + +.block-align-right { + + .text-placeholder { + + @apply relative w-fit h-0 order-2; + &:after { + @apply relative w-fit top-1/2 left-[-6px]; + } + } + + .text-content { + @apply order-1; + } + + .has-start-icon .text-placeholder { + &:after { + @apply left-[-6px]; + } + } +} + + +.formula-inline, .mention { + &.selected { + @apply rounded bg-content-blue-100; + } +} + +.bulleted-icon { + &:after { + content: attr(data-letter); + font-weight: 500; + } +} + +.numbered-icon { + &:after { + content: attr(data-number) "."; + font-weight: 500; + } +} + + +.image-render { + .image-resizer { + @apply absolute w-[10px] top-0 z-10 flex h-full cursor-col-resize items-center justify-end; + .resize-handle { + @apply h-1/4 w-1/2 transform transition-all duration-500 select-none rounded-full border border-white opacity-0; + background: var(--fill-toolbar); + } + } + + &:hover { + .image-resizer { + .resize-handle { + @apply opacity-90; + } + } + } +} + + +.image-block, .math-equation-block, [data-dark-mode="true"] .image-block, [data-dark-mode="true"] .math-equation-block { + ::selection { + @apply bg-transparent; + } + + &:hover { + .container-bg { + background: var(--fill-list-hover) !important; + } + } +} + +[data-block-type='heading'] { + .mention-inline .mention-content { + @apply ml-6; + } + + .level-1, .level-2 { + .mention-inline .mention-content { + @apply ml-8; + } + } + +} + +.mention-inline { + height: inherit; + overflow: hidden; + @apply inline-flex select-none gap-1 relative; + + .mention-icon { + @apply absolute top-1/2 transform -translate-y-1/2; + font-size: 1.25em; + } + + .mention-content { + @apply ml-6; + } + +} + +.text-block-icon { + @apply flex items-center justify-center; +} + + +.font-small { + .text-element { + line-height: 1.7; + } +} + + +.font-large { + .text-element { + line-height: 1.2; + } +} + +.line-height-large { + .text-element { + margin-top: 6px; + margin-bottom: 6px; + } +} + +.line-height-small { + .text-element { + margin-top: 0px; + margin-bottom: 0px; + } +} diff --git a/frontend/appflowy_web_app/src/components/editor/editor.type.ts b/frontend/appflowy_web_app/src/components/editor/editor.type.ts new file mode 100644 index 0000000000..d21f75cd3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/editor.type.ts @@ -0,0 +1,144 @@ +import { + BlockType, + CalloutBlockData, + CodeBlockData, + HeadingBlockData, + ImageBlockData, + MathEquationBlockData, + NumberedListBlockData, + TodoListBlockData, + ToggleListBlockData, + YjsEditorKey, + InlineBlockType, + Mention, + OutlineBlockData, + TableBlockData, + TableCellBlockData, + BlockId, + BlockData, + DatabaseNodeData, +} from '@/application/collab.type'; +import { HTMLAttributes } from 'react'; +import { Element } from 'slate'; + +export interface BlockNode extends Element { + blockId: BlockId; + type: BlockType; + data?: BlockData; +} + +export interface TextNode extends Element { + type: YjsEditorKey.text; + textId: string; +} + +export interface PageNode extends BlockNode { + type: BlockType.Page; +} + +export interface ParagraphNode extends BlockNode { + type: BlockType.Paragraph; +} + +export interface HeadingNode extends BlockNode { + blockId: string; + type: BlockType.HeadingBlock; + data: HeadingBlockData; +} + +export interface DividerNode extends BlockNode { + type: BlockType.DividerBlock; + blockId: string; +} + +export interface TodoListNode extends BlockNode { + type: BlockType.TodoListBlock; + blockId: string; + data: TodoListBlockData; +} + +export interface ToggleListNode extends BlockNode { + type: BlockType.ToggleListBlock; + blockId: string; + data: ToggleListBlockData; +} + +export interface BulletedListNode extends BlockNode { + type: BlockType.BulletedListBlock; + blockId: string; +} + +export interface NumberedListNode extends BlockNode { + type: BlockType.NumberedListBlock; + blockId: string; + data: NumberedListBlockData; +} + +export interface QuoteNode extends BlockNode { + type: BlockType.QuoteBlock; + blockId: string; +} + +export interface CodeNode extends BlockNode { + type: BlockType.CodeBlock; + blockId: string; + data: CodeBlockData; +} + +export interface CalloutNode extends BlockNode { + type: BlockType.CalloutBlock; + blockId: string; + data: CalloutBlockData; +} + +export interface MathEquationNode extends BlockNode { + type: BlockType.EquationBlock; + blockId: string; + data: MathEquationBlockData; +} + +export interface ImageBlockNode extends BlockNode { + type: BlockType.ImageBlock; + blockId: string; + data: ImageBlockData; +} + +export interface OutlineNode extends BlockNode { + type: BlockType.OutlineBlock; + blockId: string; + data: OutlineBlockData; +} + +export interface TableNode extends BlockNode { + type: BlockType.TableBlock; + blockId: string; + data: TableBlockData; +} + +export interface TableCellNode extends BlockNode { + type: BlockType.TableCell; + blockId: string; + data: TableCellBlockData; +} + +export interface DatabaseNode extends BlockNode { + type: BlockType.GridBlock | BlockType.BoardBlock | BlockType.CalendarBlock; + blockId: string; + data: DatabaseNodeData; +} + +export interface EditorElementProps<T = Element> extends HTMLAttributes<HTMLDivElement> { + node: T; +} + +type FormulaValue = string; + +export interface FormulaNode extends Element { + type: InlineBlockType.Formula; + data: FormulaValue; +} + +export interface MentionNode extends Element { + type: InlineBlockType.Mention; + data: Mention; +} diff --git a/frontend/appflowy_web_app/src/components/editor/index.ts b/frontend/appflowy_web_app/src/components/editor/index.ts new file mode 100644 index 0000000000..59a50adf9b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/index.ts @@ -0,0 +1,3 @@ +import { lazy } from 'react'; + +export const Editor = lazy(() => import('./Editor')); diff --git a/frontend/appflowy_web_app/src/components/editor/plugins/index.ts b/frontend/appflowy_web_app/src/components/editor/plugins/index.ts new file mode 100644 index 0000000000..4a87275330 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/plugins/index.ts @@ -0,0 +1,6 @@ +import { withInlines } from '@/components/editor/plugins/withInlineElement'; +import { ReactEditor } from 'slate-react'; + +export function withPlugins(editor: ReactEditor) { + return withInlines(editor); +} diff --git a/frontend/appflowy_web_app/src/components/editor/plugins/withInlineElement.ts b/frontend/appflowy_web_app/src/components/editor/plugins/withInlineElement.ts new file mode 100644 index 0000000000..fbbd238f5f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/plugins/withInlineElement.ts @@ -0,0 +1,31 @@ +import { InlineBlockType } from '@/application/collab.type'; +import { ReactEditor } from 'slate-react'; +import { Element } from 'slate'; + +export function withInlines(editor: ReactEditor) { + const { isInline, isElementReadOnly, isSelectable, isVoid, markableVoid } = editor; + + const matchInlineType = (element: Element) => { + return Object.values(InlineBlockType).includes(element.type as InlineBlockType); + }; + + editor.isInline = (element) => { + return matchInlineType(element) || isInline(element); + }; + + editor.isVoid = (element) => { + return matchInlineType(element) || isVoid(element); + }; + + editor.markableVoid = (element) => { + return matchInlineType(element) || markableVoid(element); + }; + + editor.isElementReadOnly = (element) => + Object.values(InlineBlockType).includes(element.type as InlineBlockType) || isElementReadOnly(element); + + editor.isSelectable = (element) => + !Object.values(InlineBlockType).includes(element.type as InlineBlockType) && isSelectable(element); + + return editor; +} diff --git a/frontend/appflowy_web_app/src/components/editor/utils/list.ts b/frontend/appflowy_web_app/src/components/editor/utils/list.ts new file mode 100644 index 0000000000..b5e34184b7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/utils/list.ts @@ -0,0 +1,73 @@ +import { BlockType } from '@/application/collab.type'; +import { Element, NodeEntry, Path } from 'slate'; +import { ReactEditor } from 'slate-react'; + +const romanMap: [number, string][] = [ + [1000, 'M'], + [900, 'CM'], + [500, 'D'], + [400, 'CD'], + [100, 'C'], + [90, 'XC'], + [50, 'L'], + [40, 'XL'], + [10, 'X'], + [9, 'IX'], + [5, 'V'], + [4, 'IV'], + [1, 'I'], +]; + +export function romanize(num: number): string { + let result = ''; + let nextNum = num; + + for (const [value, symbol] of romanMap) { + const count = Math.floor(nextNum / value); + + nextNum -= value * count; + result += symbol.repeat(count); + if (nextNum === 0) break; + } + + return result; +} + +export function letterize(num: number): string { + let nextNum = num; + let letters = ''; + + while (nextNum > 0) { + nextNum--; + const letter = String.fromCharCode((nextNum % 26) + 'a'.charCodeAt(0)); + + letters = letter + letters; + nextNum = Math.floor(nextNum / 26); + } + + return letters; +} + +export function getListLevel(editor: ReactEditor, type: BlockType, path: Path) { + let level = 0; + let currentPath = path; + + while (currentPath.length > 0) { + const parent = editor.parent(currentPath); + + if (!parent) { + break; + } + + const [parentNode, parentPath] = parent as NodeEntry<Element>; + + if (parentNode.type !== type) { + break; + } + + level += 1; + currentPath = parentPath; + } + + return level; +} diff --git a/frontend/appflowy_web_app/src/components/error/ElementFallbackRender.tsx b/frontend/appflowy_web_app/src/components/error/ElementFallbackRender.tsx new file mode 100644 index 0000000000..ddfdac39f1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ElementFallbackRender.tsx @@ -0,0 +1,11 @@ +import { Alert } from '@mui/material'; +import { FallbackProps } from 'react-error-boundary'; + +export function ElementFallbackRender({ error }: FallbackProps) { + return ( + <Alert severity={'error'} variant={'standard'} className={'my-2'}> + <p>Something went wrong:</p> + <pre>{error.message}</pre> + </Alert> + ); +} diff --git a/frontend/appflowy_web_app/src/components/error/Error.hooks.ts b/frontend/appflowy_web_app/src/components/error/Error.hooks.ts new file mode 100644 index 0000000000..a9da4ed829 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/Error.hooks.ts @@ -0,0 +1,39 @@ +import { useAppDispatch, useAppSelector } from '@/stores/store'; +import { useCallback, useEffect, useState } from 'react'; +import {errorActions} from "@/stores/error/slice"; + +export const useError = (e: Error) => { + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.error); + const [errorMessage, setErrorMessage] = useState(''); + const [displayError, setDisplayError] = useState(false); + + useEffect(() => { + setDisplayError(error.display); + setErrorMessage(error.message); + }, [error]); + + const showError = useCallback( + (msg: string) => { + dispatch(errorActions.showError(msg)); + }, + [dispatch] + ); + + useEffect(() => { + if (e) { + showError(e.message); + } + }, [e, showError]); + + const hideError = () => { + dispatch(errorActions.hideError()); + }; + + return { + showError, + hideError, + errorMessage, + displayError, + }; +}; diff --git a/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx new file mode 100644 index 0000000000..1bb15f2ca3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx @@ -0,0 +1,8 @@ +import { useError } from './Error.hooks'; +import { ErrorModal } from './ErrorModal'; + +export const ErrorHandlerPage = ({ error }: { error: Error }) => { + const { hideError, errorMessage, displayError } = useError(error); + + return displayError ? <ErrorModal message={errorMessage} onClose={hideError}></ErrorModal> : <></>; +}; diff --git a/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx new file mode 100644 index 0000000000..dc8d664a01 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx @@ -0,0 +1,35 @@ +import { ReactComponent as InformationSvg } from '@/assets/information.svg'; +import { ReactComponent as CloseSvg } from '$icons/16x/close.svg'; +import { Button } from '@mui/material'; + +export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { + return ( + <div className={'fixed inset-0 z-10 flex items-center justify-center bg-white/30 backdrop-blur-sm'}> + <div + className={ + 'border-shade-5 relative flex flex-col items-center gap-8 rounded-xl border bg-white px-16 py-8 shadow-md' + } + > + <button + onClick={() => onClose()} + className={'absolute right-0 top-0 z-10 px-2 py-2 text-text-caption hover:text-text-title'} + > + <CloseSvg className={'h-8 w-8'} /> + </button> + <div className={'text-main-alert'}> + <InformationSvg className={'h-24 w-24'} /> + </div> + <h1 className={'text-xl'}>Oops.. something went wrong</h1> + <h2>{message}</h2> + + <Button + onClick={() => { + window.location.reload(); + }} + > + Reload + </Button> + </div> + </div> + ); +}; diff --git a/frontend/appflowy_web_app/src/components/folder/Folder.tsx b/frontend/appflowy_web_app/src/components/folder/Folder.tsx new file mode 100644 index 0000000000..f3b9641723 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/folder/Folder.tsx @@ -0,0 +1,17 @@ +import { useViewsIdSelector } from '@/application/folder-yjs'; +import ViewItem from '@/components/folder/ViewItem'; +import React from 'react'; + +export function Folder() { + const { viewsId } = useViewsIdSelector(); + + return ( + <div className={'m-10 p-10'}> + {viewsId.map((viewId) => { + return <ViewItem key={viewId} id={viewId} />; + })} + </div> + ); +} + +export default Folder; diff --git a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx new file mode 100644 index 0000000000..49feb382e2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx @@ -0,0 +1,20 @@ +import { useNavigateToView } from '@/application/folder-yjs'; +import React from 'react'; +import Page from '@/components/_shared/page/Page'; + +function ViewItem({ id }: { id: string }) { + const onNavigateToView = useNavigateToView(); + + return ( + <div className={'cursor-pointer border-b border-line-border py-4 px-2'}> + <Page + onClick={() => { + onNavigateToView?.(id); + }} + id={id} + /> + </div> + ); +} + +export default ViewItem; diff --git a/frontend/appflowy_web_app/src/components/folder/index.ts b/frontend/appflowy_web_app/src/components/folder/index.ts new file mode 100644 index 0000000000..569707cd4f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/folder/index.ts @@ -0,0 +1 @@ +export * from './Folder'; diff --git a/frontend/appflowy_web_app/src/components/layout/Header.tsx b/frontend/appflowy_web_app/src/components/layout/Header.tsx new file mode 100644 index 0000000000..df87892b42 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/Header.tsx @@ -0,0 +1,74 @@ +import { downloadPage, openAppFlowySchema, openUrl } from '@/utils/url'; +import { Button } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; +import Popover, { PopoverOrigin } from '@mui/material/Popover'; +import Breadcrumb from 'src/components/layout/breadcrumb/Breadcrumb'; + +const popoverOrigin: { + anchorOrigin: PopoverOrigin; + transformOrigin: PopoverOrigin; +} = { + anchorOrigin: { + vertical: 'bottom', + horizontal: 'right', + }, + transformOrigin: { + vertical: -10, + horizontal: 'right', + }, +}; + +function Header() { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null); + + return ( + <div className={'appflowy-top-bar flex h-[64px] p-4'}> + <div className={'flex w-full items-center justify-between overflow-hidden'}> + <Breadcrumb /> + + <Button + className={'border-line-border'} + onClick={(e) => { + setAnchorEl(e.currentTarget); + }} + variant={'outlined'} + color={'inherit'} + endIcon={<Logo />} + > + Built with + </Button> + </div> + <Popover open={Boolean(anchorEl)} anchorEl={anchorEl} {...popoverOrigin} onClose={() => setAnchorEl(null)}> + <div className={'flex w-fit flex-col gap-2 p-4'}> + <Button + onClick={() => { + void openUrl(openAppFlowySchema); + }} + className={'w-full'} + variant={'outlined'} + > + {`🥳 Open AppFlowy`} + </Button> + <div className={'flex w-full items-center justify-center gap-2 text-xs text-text-caption'}> + <div className={'h-px flex-1 bg-line-divider'} /> + {t('signIn.or')} + <div className={'h-px flex-1 bg-line-divider'} /> + </div> + <Button + onClick={() => { + void openUrl(downloadPage, '_blank'); + }} + variant={'contained'} + > + {`Download AppFlowy`} + </Button> + </div> + </Popover> + </div> + ); +} + +export default Header; diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts b/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts new file mode 100644 index 0000000000..3ab11d17e9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/Layout.hooks.ts @@ -0,0 +1,96 @@ +import { YFolder, YjsEditorKey, YjsFolderKey } from '@/application/collab.type'; +import { Crumb } from '@/application/folder-yjs'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { useCallback, useContext, useEffect, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; + +export function useLayout() { + const { workspaceId, objectId } = useParams(); + const [search] = useSearchParams(); + const folderService = useContext(AFConfigContext)?.service?.folderService; + const [folder, setFolder] = useState<YFolder | null>(null); + const views = folder?.get(YjsFolderKey.views); + const view = objectId ? views?.get(objectId) : null; + const [crumbs, setCrumbs] = useState<Crumb[]>([]); + + const getFolder = useCallback( + async (workspaceId: string) => { + const folder = (await folderService?.openWorkspace(workspaceId)) + ?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.folder); + + if (!folder) return; + + console.log(folder.toJSON()); + setFolder(folder); + }, + [folderService] + ); + + useEffect(() => { + if (!workspaceId) return; + + void getFolder(workspaceId); + }, [getFolder, workspaceId]); + + const navigate = useNavigate(); + + const handleNavigateToView = useCallback( + (viewId: string) => { + const view = folder?.get(YjsFolderKey.views)?.get(viewId); + + if (!view) return; + navigate(`/view/${workspaceId}/${viewId}`); + }, + [folder, navigate, workspaceId] + ); + + const onChangeBreadcrumb = useCallback(() => { + if (!view) return; + const queue = [view]; + let parentId = view.get(YjsFolderKey.bid); + + while (parentId) { + const parent = views?.get(parentId); + + if (!parent) break; + + queue.unshift(parent); + parentId = parent?.get(YjsFolderKey.bid); + } + + setCrumbs( + queue + .map((view) => { + let icon = view.get(YjsFolderKey.icon); + + try { + icon = JSON.parse(icon || '')?.value; + } catch (e) { + // do nothing + } + + return { + viewId: view.get(YjsFolderKey.id), + name: view.get(YjsFolderKey.name), + icon: icon || view.get(YjsFolderKey.layout), + }; + }) + .slice(1) + ); + }, [view, views]); + + useEffect(() => { + onChangeBreadcrumb(); + + view?.observe(onChangeBreadcrumb); + views?.observe(onChangeBreadcrumb); + + return () => { + view?.unobserve(onChangeBreadcrumb); + views?.unobserve(onChangeBreadcrumb); + }; + }, [search, onChangeBreadcrumb, view, views]); + + return { folder, handleNavigateToView, crumbs, setCrumbs }; +} diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx new file mode 100644 index 0000000000..dc3f075f69 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/Layout.tsx @@ -0,0 +1,35 @@ +import { FolderProvider } from '@/components/_shared/context-provider/FolderProvider'; +import Header from '@/components/layout/Header'; +import { AFScroller } from '@/components/_shared/scroller'; +import { useLayout } from '@/components/layout/Layout.hooks'; +import React from 'react'; +import './layout.scss'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; + +function Layout({ children }: { children: React.ReactNode }) { + const { folder, handleNavigateToView, crumbs, setCrumbs } = useLayout(); + + if (!folder) + return ( + <div className={'flex h-screen w-screen items-center justify-center'}> + <Logo className={'h-20 w-20'} /> + </div> + ); + + return ( + <FolderProvider setCrumbs={setCrumbs} crumbs={crumbs} onNavigateToView={handleNavigateToView} folder={folder}> + <Header /> + <AFScroller + overflowXHidden + style={{ + height: 'calc(100vh - 64px)', + }} + className={'appflowy-layout appflowy-scroll-container'} + > + {children} + </AFScroller> + </FolderProvider> + ); +} + +export default Layout; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000..02e682514e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,25 @@ +import { useCrumbs } from '@/application/folder-yjs'; +import Item from '@/components/layout/breadcrumb/Item'; +import React, { useMemo } from 'react'; + +export function Breadcrumb() { + const crumbs = useCrumbs(); + + const renderCrumb = useMemo(() => { + return crumbs?.map((crumb, index) => { + const isLast = index === crumbs.length - 1; + const key = crumb.rowId ? `${crumb.viewId}-${crumb.rowId}` : `${crumb.viewId}`; + + return ( + <React.Fragment key={key}> + <Item crumb={crumb} disableClick={isLast} /> + {!isLast && <span>/</span>} + </React.Fragment> + ); + }); + }, [crumbs]); + + return <div className={'flex flex-1 items-center gap-2 overflow-hidden'}>{renderCrumb}</div>; +} + +export default Breadcrumb; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx new file mode 100644 index 0000000000..7d857ac893 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/Item.tsx @@ -0,0 +1,54 @@ +import { ViewLayout } from '@/application/collab.type'; +import { Crumb, useNavigateToView } from '@/application/folder-yjs'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; + +const renderCrumbIcon = (icon: string) => { + if (Number(icon) === ViewLayout.Grid) { + return <GridSvg className={'h-4 w-4'} />; + } + + if (Number(icon) === ViewLayout.Board) { + return <BoardSvg className={'h-4 w-4'} />; + } + + if (Number(icon) === ViewLayout.Calendar) { + return <CalendarSvg className={'h-4 w-4'} />; + } + + if (Number(icon) === ViewLayout.Document) { + return <DocumentSvg className={'h-4 w-4'} />; + } + + return icon; +}; + +function Item({ crumb, disableClick = false }: { crumb: Crumb; disableClick?: boolean }) { + const { viewId, icon, name } = crumb; + + const { t } = useTranslation(); + const onNavigateToView = useNavigateToView(); + + return ( + <div + className={`flex items-center gap-1 ${!disableClick ? 'cursor-pointer' : 'flex-1 overflow-hidden'}`} + onClick={() => { + if (disableClick) return; + onNavigateToView?.(viewId); + }} + > + {renderCrumbIcon(icon)} + <span + className={!disableClick ? 'max-w-[250px] truncate hover:text-fill-default hover:underline' : 'flex-1 truncate'} + > + {name || t('menuAppHeader.defaultNewPageName')} + </span> + </div> + ); +} + +export default Item; diff --git a/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts b/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts new file mode 100644 index 0000000000..116446358b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/breadcrumb/index.ts @@ -0,0 +1 @@ +export * from './Breadcrumb'; diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss new file mode 100644 index 0000000000..b51a842e43 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -0,0 +1,106 @@ +@use "src/styles/mixin.scss"; + +.sketch-picker { + background-color: var(--bg-body) !important; + border-color: transparent !important; + box-shadow: none !important; +} + +.sketch-picker .flexbox-fix { + border-color: var(--line-divider) !important; +} + +.sketch-picker [id^='rc-editable-input'] { + background-color: var(--bg-body) !important; + border-color: var(--line-divider) !important; + color: var(--text-title) !important; + box-shadow: var(--line-border) 0px 0px 0px 1px inset !important; +} + + +body { + + &[data-os="windows"]:not([data-browser="firefox"]) { + .appflowy-custom-scroller { + @include mixin.hidden-scrollbar + } + + .MuiBox-root { + @include mixin.scrollbar-style; + } + } + + .grid-sticky-header { + @include mixin.hidden-scrollbar + } +} + + +.appflowy-date-picker-calendar { + width: 100%; +} + + +.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical { + background-color: var(--scrollbar-thumb); + border-radius: 4px; + opacity: 60%; +} + + +.view-icon { + @apply flex w-fit leading-[1.5em] cursor-pointer rounded-lg py-2 text-[1.5em]; + font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; + line-height: 1em; + white-space: nowrap; +} + +.theme-mode-item { + @apply relative flex h-[72px] w-[88px] cursor-pointer items-end justify-end rounded border hover:shadow; + background: linear-gradient(150.74deg, rgba(231, 231, 231, 0) 17.95%, #C5C5C5 95.51%); +} + +[data-dark-mode="true"] { + .theme-mode-item { + background: linear-gradient(150.74deg, rgba(128, 125, 125, 0) 17.95%, #4d4d4d 95.51%); + } +} + +.document-header { + .view-banner { + @apply items-center; + } +} + +.tooltip-arrow { + overflow: hidden; + position: absolute; + width: 1em; + height: 0.71em; + color: var(--bg-body); + + &:before { + content: '""'; + margin: auto; + display: block; + width: 100%; + height: 100%; + box-shadow: var(--shadow); + background-color: var(--bg-body); + transform: rotate(45deg); + } +} + +.grid-row-cell.wrap-cell { + .text-cell { + @apply py-2 break-words whitespace-pre-wrap; + } + + .relation-cell { + @apply py-2 break-words whitespace-pre-wrap flex-wrap; + } + + .select-option-cell { + @apply flex-wrap py-2; + } +} diff --git a/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx b/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx new file mode 100644 index 0000000000..ff241f728b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/tauri/SignInAsAnonymous.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from '@mui/material'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { useTranslation } from 'react-i18next'; + +function SignInAsAnonymous() { + const { signInAsAnonymous } = useAuth(); + const { t } = useTranslation(); + + return ( + <> + <Button + size={'large'} + color={'inherit'} + className={'border-transparent bg-line-divider py-3'} + variant={'outlined'} + onClick={signInAsAnonymous} + > + {t('signIn.loginStartWithAnonymous')} + </Button> + <div className={'flex w-full items-center justify-center gap-2 text-sm'}> + <div className={'h-px flex-1 bg-line-divider'} /> + {t('signIn.or')} + <div className={'h-px flex-1 bg-line-divider'} /> + </div> + </> + ); +} + +export default SignInAsAnonymous; diff --git a/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx b/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx new file mode 100644 index 0000000000..5ee605463e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react'; +import { useDeepLink } from '@/components/tauri/tauri.hooks'; + +function TauriAuth() { + const { + onDeepLink, + } = useDeepLink(); + + useEffect(() => { + void onDeepLink(); + }, [onDeepLink]); + + return null; +} + +export default TauriAuth; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts new file mode 100644 index 0000000000..f95c2ca696 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; +import { notify } from '@/components/_shared/notify'; +import { useAuth } from '@/components/auth/auth.hooks'; + +export function useDeepLink() { + const { + signInWithOAuth, + } = useAuth(); + const onDeepLink = useCallback(async () => { + const { event } = await import('@tauri-apps/api'); + + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + return await event.listen('open_deep_link', async (e) => { + const payload = e.payload as string; + + const [, hash] = payload.split('//#'); + const obj = parseHash(hash); + + if (!obj.access_token) { + notify.error('Failed to sign in, the access token is missing'); + // update login state to error + return; + } + + await signInWithOAuth(payload); + }); + }, [signInWithOAuth]); + + return { + onDeepLink, + }; + +} + +function parseHash(hash: string) { + const hashParams = new URLSearchParams(hash); + const hashObject: Record<string, string> = {}; + + for (const [key, value] of hashParams) { + hashObject[key] = value; + } + + return hashObject; +} diff --git a/frontend/appflowy_web_app/src/i18n/config.ts b/frontend/appflowy_web_app/src/i18n/config.ts new file mode 100644 index 0000000000..b2a116e0b6 --- /dev/null +++ b/frontend/appflowy_web_app/src/i18n/config.ts @@ -0,0 +1,15 @@ +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +void i18next + .use(resourcesToBackend((language: string) => import(`../@types/translations/${language}.json`))) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + lng: 'en', + defaultNS: 'translation', + debug: false, + fallbackLng: 'en', + }); diff --git a/frontend/appflowy_web_app/src/main.tsx b/frontend/appflowy_web_app/src/main.tsx new file mode 100644 index 0000000000..d964a40e3d --- /dev/null +++ b/frontend/appflowy_web_app/src/main.tsx @@ -0,0 +1,4 @@ +import ReactDOM from 'react-dom/client'; +import App from 'src/components/app/App'; + +ReactDOM.createRoot(document.getElementById('root')!).render(<App />); diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx new file mode 100644 index 0000000000..10e9e5c015 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -0,0 +1,72 @@ +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseHeader } from '@/components/database/components/header'; +import { useGetDatabaseId, useLoadDatabase } from '@/components/database/Database.hooks'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import DatabaseRow from '@/components/database/DatabaseRow'; +import Database from '@/components/database/Database'; +import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; + +function DatabasePage() { + const { objectId } = useId() || {}; + const [search, setSearch] = useSearchParams(); + const rowId = search.get('r'); + + const viewId = search.get('v') || undefined; + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + const handleNavigateToRow = useCallback( + (rowId: string) => { + setSearch({ r: rowId }); + }, + [setSearch] + ); + + const databaseId = useGetDatabaseId(objectId); + const rowIds = useMemo(() => (rowId ? [rowId] : undefined), [rowId]); + + const { doc, rows, notFound } = useLoadDatabase({ + databaseId, + rowIds, + }); + + if (notFound || !objectId) { + return <RecordNotFound open={notFound} />; + } + + if (!rows || !doc) { + return ( + <div className={'flex h-full w-full items-center justify-center'}> + <CircularProgress /> + </div> + ); + } + + return ( + <DatabaseContextProvider + isDatabaseRowPage={!!rowId} + navigateToRow={handleNavigateToRow} + viewId={viewId || objectId} + databaseDoc={doc} + rowDocMap={rows} + readOnly={true} + > + {rowId ? ( + <DatabaseRow rowId={rowId} /> + ) : ( + <div className={'relative flex h-full w-full flex-col'}> + <DatabaseHeader viewId={objectId} /> + <Database iidIndex={objectId} viewId={viewId || objectId} onNavigateToView={handleChangeView} /> + </div> + )} + </DatabaseContextProvider> + ); +} + +export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/pages/DocumentPage.tsx b/frontend/appflowy_web_app/src/pages/DocumentPage.tsx new file mode 100644 index 0000000000..0a9a359afc --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/DocumentPage.tsx @@ -0,0 +1,8 @@ +import { Document } from '@/components/document'; +import React from 'react'; + +function DocumentPage() { + return <Document />; +} + +export default DocumentPage; diff --git a/frontend/appflowy_web_app/src/pages/FolderPage.tsx b/frontend/appflowy_web_app/src/pages/FolderPage.tsx new file mode 100644 index 0000000000..6381fe4ace --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/FolderPage.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { Folder } from 'src/components/folder'; + +function FolderPage() { + return <Folder />; +} + +export default FolderPage; diff --git a/frontend/appflowy_web_app/src/pages/LoginPage.tsx b/frontend/appflowy_web_app/src/pages/LoginPage.tsx new file mode 100644 index 0000000000..a4ded1d5e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/LoginPage.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from 'react'; +import Welcome from '@/components/auth/Welcome'; +import { useNavigate } from 'react-router-dom'; +import { useAppSelector } from '@/stores/store'; + +function LoginPage() { + const currentUser = useAppSelector((state) => state.currentUser); + const navigate = useNavigate(); + + useEffect(() => { + if (currentUser.isAuthenticated) { + const redirect = new URLSearchParams(window.location.search).get('redirect'); + const workspaceId = currentUser.user?.workspaceId; + + if (!redirect || redirect === '/') { + return navigate(`/view/${workspaceId}`); + } + + navigate(`${redirect}`); + } + }, [currentUser, navigate]); + return <Welcome />; +} + +export default LoginPage; diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx new file mode 100644 index 0000000000..1df649b077 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -0,0 +1,32 @@ +import { ViewLayout } from '@/application/collab.type'; +import { useViewLayout } from '@/application/folder-yjs'; +import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import React, { lazy, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import DocumentPage from '@/pages/DocumentPage'; + +const DatabasePage = lazy(() => import('./DatabasePage')); + +function ProductPage() { + const { workspaceId, objectId } = useParams(); + const type = useViewLayout(); + + const PageComponent = useMemo(() => { + switch (type) { + case ViewLayout.Document: + return DocumentPage; + case ViewLayout.Grid: + case ViewLayout.Board: + case ViewLayout.Calendar: + return DatabasePage; + default: + return null; + } + }, [type]); + + if (!workspaceId || !objectId) return null; + + return <IdProvider objectId={objectId}>{PageComponent && <PageComponent />}</IdProvider>; +} + +export default ProductPage; diff --git a/frontend/appflowy_web_app/src/slate-editor.d.ts b/frontend/appflowy_web_app/src/slate-editor.d.ts new file mode 100644 index 0000000000..58b7c8502e --- /dev/null +++ b/frontend/appflowy_web_app/src/slate-editor.d.ts @@ -0,0 +1,46 @@ +import { ReactEditor } from 'slate-react'; + +interface EditorInlineAttributes { + bold?: boolean; + italic?: boolean; + underline?: boolean; + strikethrough?: boolean; + font_color?: string; + bg_color?: string; + href?: string; + code?: boolean; + font_family?: string; + formula?: string; + prism_token?: string; + class_name?: string; + mention?: { + type: string; + // inline page ref id + page?: string; + // reminder date ref id + date?: string; + }; +} + +type CustomElement = { + children: (CustomText | CustomElement)[]; + type?: string; + data?: unknown; + blockId?: string; + textId?: string; + relationId?: string; +}; + +type CustomText = { text: string } & EditorInlineAttributes; + +declare module 'slate' { + interface CustomTypes { + Editor: BaseEditor & ReactEditor; + Element: CustomElement; + Text: CustomText; + } + + interface BaseEditor { + isEmbed: (element: CustomElement) => boolean; + } +} diff --git a/frontend/appflowy_web_app/src/stores/app/slice.ts b/frontend/appflowy_web_app/src/stores/app/slice.ts new file mode 100644 index 0000000000..dee62a6fc7 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/app/slice.ts @@ -0,0 +1,42 @@ +import { AFServiceConfig } from '@/application/services/services.type'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +const defaultConfig: AFServiceConfig = { + cloudConfig: { + baseURL: import.meta.env.AF_BASE_URL + ? import.meta.env.AF_BASE_URL + : import.meta.env.DEV + ? 'https://test.appflowy.cloud' + : 'https://beta.appflowy.cloud', + gotrueURL: import.meta.env.AF_GOTRUE_URL + ? import.meta.env.AF_GOTRUE_URL + : import.meta.env.DEV + ? 'https://test.appflowy.cloud/gotrue' + : 'https://beta.appflowy.cloud/gotrue', + wsURL: import.meta.env.AF_WS_URL + ? import.meta.env.AF_WS_URL + : import.meta.env.DEV + ? 'wss://test.appflowy.cloud/ws/v1' + : 'wss://beta.appflowy.cloud/ws/v1', + }, +}; + +export interface AppState { + appConfig: AFServiceConfig; +} + +const initialState: AppState = { + appConfig: defaultConfig, +}; + +export const slice = createSlice({ + name: 'app', + initialState, + reducers: { + setAppConfig: (state, action: PayloadAction<AFServiceConfig>) => { + state.appConfig = action.payload; + }, + }, +}); + +export const { setAppConfig } = slice.actions; diff --git a/frontend/appflowy_web_app/src/stores/currentUser/slice.ts b/frontend/appflowy_web_app/src/stores/currentUser/slice.ts new file mode 100644 index 0000000000..ecd40a433e --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/currentUser/slice.ts @@ -0,0 +1,53 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { UserProfile, UserSetting } from '@/application/user.type'; + +export enum LoginState { + IDLE = 'idle', + LOADING = 'loading', + SUCCESS = 'success', + ERROR = 'error', +} + +export interface InitialState { + user?: UserProfile; + isAuthenticated: boolean; + userSetting?: UserSetting; + loginState?: LoginState; +} + +const initialState: InitialState = { + isAuthenticated: false, +}; + +export const currentUserSlice = createSlice({ + name: 'currentUser', + initialState: initialState, + reducers: { + updateUser: (state, action: PayloadAction<UserProfile>) => { + state.user = action.payload; + state.isAuthenticated = true; + }, + logout: (state) => { + state.user = undefined; + state.isAuthenticated = false; + }, + setUserSetting: (state, action: PayloadAction<UserSetting>) => { + state.userSetting = action.payload; + }, + loginStart: (state) => { + state.loginState = LoginState.LOADING; + }, + loginSuccess: (state) => { + state.loginState = LoginState.SUCCESS; + }, + loginError: (state) => { + state.loginState = LoginState.ERROR; + }, + resetLoginState: (state) => { + state.loginState = LoginState.IDLE; + }, + + }, +}); + +export const currentUserActions = currentUserSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/error/slice.ts b/frontend/appflowy_web_app/src/stores/error/slice.ts new file mode 100644 index 0000000000..9b47df7777 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/error/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface IErrorOptions { + display: boolean; + message: string; +} + +const initialState: IErrorOptions = { + display: false, + message: '', +}; + +export const errorSlice = createSlice({ + name: 'error', + initialState: initialState, + reducers: { + showError(state, action: PayloadAction<string>) { + return { + display: true, + message: action.payload, + }; + }, + hideError() { + return { + display: false, + message: '', + }; + }, + }, +}); + +export const errorActions = errorSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/store.ts b/frontend/appflowy_web_app/src/stores/store.ts new file mode 100644 index 0000000000..b75363e911 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/store.ts @@ -0,0 +1,45 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { + configureStore, + createListenerMiddleware, + TypedStartListening, + TypedAddListener, + ListenerEffectAPI, + addListener, +} from '@reduxjs/toolkit'; +import { errorSlice } from '@/stores/error/slice'; +import { currentUserSlice } from '@/stores/currentUser/slice'; +import { slice as appSlice } from '@/stores/app/slice'; + +const listenerMiddlewareInstance = createListenerMiddleware({ + onError: () => console.error, +}); + +const store = configureStore({ + reducer: { + [appSlice.name]: appSlice.reducer, + [errorSlice.name]: errorSlice.reducer, + [currentUserSlice.name]: currentUserSlice.reducer, + }, + middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware), +}); + +export { store }; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType<typeof store.getState>; +// @see https://redux-toolkit.js.org/usage/usage-with-typescript#getting-the-dispatch-type +export type AppDispatch = typeof store.dispatch; + +export type AppListenerEffectAPI = ListenerEffectAPI<RootState, AppDispatch>; + +// @see https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +export type AppStartListening = TypedStartListening<RootState, AppDispatch>; +export type AppAddListener = TypedAddListener<RootState, AppDispatch>; + +export const startAppListening = listenerMiddlewareInstance.startListening as AppStartListening; +export const addAppListener = addListener as AppAddListener; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch<AppDispatch>(); +export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector; diff --git a/frontend/appflowy_web_app/src/styles/mixin.scss b/frontend/appflowy_web_app/src/styles/mixin.scss new file mode 100644 index 0000000000..d571ef9cbf --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/mixin.scss @@ -0,0 +1,23 @@ +@mixin hidden-scrollbar { + &::-webkit-scrollbar { + display: none; + } + + -ms-overflow-style: none; + scrollbar-width: none; // For Firefox +} + +@mixin scrollbar-style { + ::-webkit-scrollbar, &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + &:hover { + + &::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: var(--scrollbar-thumb); + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/tailwind.css b/frontend/appflowy_web_app/src/styles/tailwind.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/appflowy_web_app/src/styles/template.css b/frontend/appflowy_web_app/src/styles/template.css new file mode 100644 index 0000000000..82a597519f --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/template.css @@ -0,0 +1,59 @@ +@import "./variables/light.variables.css"; +@import "./variables/dark.variables.css"; + + +:root { + /* resize popover shadow */ + --shadow-resize-popover: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +* { + margin: 0; + padding: 0; +} + +/* stop body from scrolling */ +html, +body { + margin: 0; + height: 100%; + overflow: hidden; +} + +[contenteditable] { + -webkit-tap-highlight-color: transparent; +} + +input, +textarea { + outline: 0; + background: transparent; +} + + +.btn { + @apply rounded-xl border border-line-divider px-4 py-3; +} + +.btn-primary { + @apply bg-fill-default text-text-title hover:bg-fill-list-hover; +} + +.input { + @apply rounded-xl border border-line-divider px-[18px] py-[14px] text-sm; +} + + +th { + @apply text-left font-normal; +} + +html { + font-size: 16px; +} + +@media (max-width: 600px) { + html { + font-size: 14px; + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css new file mode 100644 index 0000000000..de8fcf9824 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -0,0 +1,58 @@ + +:root[data-dark-mode=true] { + --text-title: #e2e9f2; + --text-caption: #87A0BF; + --text-placeholder: #3c4557; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #005174; + --icon-primary: #e2e9f2; + --icon-secondary: #59647a; + --icon-disabled: #525A69; + --icon-on-toolbar: white; + --line-border: #59647a; + --line-divider: #252F41; + --line-on-toolbar: #99a6b8; + --fill-default: #00bcf0; + --fill-hover: #005174; + --fill-toolbar: #0F111C; + --fill-selector: #232b38; + --fill-list-active: #3c4557; + --fill-list-hover: #005174; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #005174; + --content-on-fill: #1a202c; + --content-on-tag: #99a6b8; + --content-blue-50: #232b38; + --bg-body: #1a202c; + --bg-base: #232b38; + --bg-mask: rgba(0, 0, 0, 0.7); + --bg-tips: #005174; + --bg-brand: #2c144b; + --function-error: #d32772; + --function-warning: #e9b320; + --function-success: #3ba856; + --function-info: #2e9dbb; + --tint-red: #56363F; + --tint-green: #3C5133; + --tint-purple: #4D4078; + --tint-blue: #2C3B58; + --tint-yellow: #695E3E; + --tint-pink: #5E3C5E; + --tint-lime: #394027; + --tint-aqua: #1B3849; + --tint-orange: #5E3C3C; + --shadow: 0px 0px 25px 0px rgba(0, 0, 0, 0.3); + --scrollbar-track: #252F41; + --scrollbar-thumb: #3c4557; + --gradient1: linear-gradient(233deg, #34BDAF 0%, #B682D5 100%); + --gradient2: linear-gradient(180deg, #4CC2CC 0%, #E17570 100%); + --gradient3: linear-gradient(180deg, #AF70E1 0%, #ED7196 100%); + --gradient4: linear-gradient(180deg, #A348D6 0%, #45A7DF 100%); + --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); + --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); + --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css new file mode 100644 index 0000000000..cd4ffee0f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -0,0 +1,61 @@ + +:root { + --text-title: #333333; + --text-caption: #828282; + --text-placeholder: #bdbdbd; + --text-disabled: #e0e0e0; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #e0f8ff; + --icon-primary: #333333; + --icon-secondary: #59647a; + --icon-disabled: #e0e0e0; + --icon-on-toolbar: #ffffff; + --line-border: #bdbdbd; + --line-divider: #e5e5e5; + --line-on-toolbar: #4f4f4f; + --fill-toolbar: #333333; + --fill-default: #00bcf0; + --fill-hover: #52d1f4; + --fill-pressed: #009fd1; + --fill-active: #e0f8ff; + --fill-list-hover: #e0f8ff; + --fill-list-active: #f9fafd; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #e0f8ff; + --content-blue-50: #f2fcff; + --content-on-fill-hover: #00bcf0; + --content-on-fill: #ffffff; + --content-on-tag: #4f4f4f; + --bg-body: #ffffff; + --bg-base: #f9fafd; + --bg-mask: rgba(0, 0, 0, 0.55); + --bg-tips: #e0f8ff; + --bg-brand: #2c144b; + --function-error: #fb006d; + --function-waring: #ffd667; + --function-success: #66cf80; + --function-info: #00bcf0; + --tint-purple: #e8e0ff; + --tint-pink: #ffe7ee; + --tint-red: #ffdddd; + --tint-lime: #f5ffdc; + --tint-green: #ddffd6; + --tint-aqua: #defff1; + --tint-blue: #e1fbff; + --tint-orange: #ffefe3; + --tint-yellow: #fff2cd; + --shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); + --scrollbar-thumb: #bdbdbd; + --scrollbar-track: #e5e5e5; + --gradient1: linear-gradient(233deg, #34BDAF 0%, #B682D5 100%); + --gradient2: linear-gradient(180deg, #4CC2CC 0%, #E17570 100%); + --gradient3: linear-gradient(180deg, #AF70E1 0%, #ED7196 100%); + --gradient4: linear-gradient(180deg, #A348D6 0%, #45A7DF 100%); + --gradient5: linear-gradient(56.2deg, #5749CA 0%, #BB4A97 100%); + --gradient6: linear-gradient(180deg, #036FFA 0%, #00B8E5 100%); + --gradient7: linear-gradient(38.2deg, #F0C6CF 0%, #DECCE2 40.4754%, #CAD3F9 100%); +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/color.ts b/frontend/appflowy_web_app/src/utils/color.ts new file mode 100644 index 0000000000..9de9da1dca --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/color.ts @@ -0,0 +1,74 @@ +export enum ColorEnum { + Purple = 'appflowy_them_color_tint1', + Pink = 'appflowy_them_color_tint2', + LightPink = 'appflowy_them_color_tint3', + Orange = 'appflowy_them_color_tint4', + Yellow = 'appflowy_them_color_tint5', + Lime = 'appflowy_them_color_tint6', + Green = 'appflowy_them_color_tint7', + Aqua = 'appflowy_them_color_tint8', + Blue = 'appflowy_them_color_tint9', +} + +export enum GradientEnum { + gradient1 = 'appflowy_them_color_gradient1', + gradient2 = 'appflowy_them_color_gradient2', + gradient3 = 'appflowy_them_color_gradient3', + gradient4 = 'appflowy_them_color_gradient4', + gradient5 = 'appflowy_them_color_gradient5', + gradient6 = 'appflowy_them_color_gradient6', + gradient7 = 'appflowy_them_color_gradient7', +} + +export const colorMap = { + [ColorEnum.Purple]: 'var(--tint-purple)', + [ColorEnum.Pink]: 'var(--tint-pink)', + [ColorEnum.LightPink]: 'var(--tint-red)', + [ColorEnum.Orange]: 'var(--tint-orange)', + [ColorEnum.Yellow]: 'var(--tint-yellow)', + [ColorEnum.Lime]: 'var(--tint-lime)', + [ColorEnum.Green]: 'var(--tint-green)', + [ColorEnum.Aqua]: 'var(--tint-aqua)', + [ColorEnum.Blue]: 'var(--tint-blue)', +}; + +export const gradientMap = { + [GradientEnum.gradient1]: 'var(--gradient1)', + [GradientEnum.gradient2]: 'var(--gradient2)', + [GradientEnum.gradient3]: 'var(--gradient3)', + [GradientEnum.gradient4]: 'var(--gradient4)', + [GradientEnum.gradient5]: 'var(--gradient5)', + [GradientEnum.gradient6]: 'var(--gradient6)', + [GradientEnum.gradient7]: 'var(--gradient7)', +}; + +// Convert ARGB to RGBA +// Flutter uses ARGB, but CSS uses RGBA +function argbToRgba(color: string): string { + const hex = color.replace(/^#|0x/, ''); + + const hasAlpha = hex.length === 8; + + if (!hasAlpha) { + return color.replace('0x', '#'); + } + + const r = parseInt(hex.slice(2, 4), 16); + const g = parseInt(hex.slice(4, 6), 16); + const b = parseInt(hex.slice(6, 8), 16); + const a = hasAlpha ? parseInt(hex.slice(0, 2), 16) / 255 : 1; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + +export function renderColor(color: string) { + if (colorMap[color as ColorEnum]) { + return colorMap[color as ColorEnum]; + } + + if (gradientMap[color as GradientEnum]) { + return gradientMap[color as GradientEnum]; + } + + return argbToRgba(color); +} diff --git a/frontend/appflowy_web_app/src/utils/font.ts b/frontend/appflowy_web_app/src/utils/font.ts new file mode 100644 index 0000000000..645340d958 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/font.ts @@ -0,0 +1,16 @@ +const hasLoadedFonts: Set<string> = new Set(); + +export function getFontFamily(attribute: string) { + const fontFamily = attribute.split('_')[0]; + + if (hasLoadedFonts.has(fontFamily)) { + return fontFamily; + } + + window.WebFont?.load({ + google: { + families: [fontFamily], + }, + }); + return fontFamily; +} diff --git a/frontend/appflowy_web_app/src/utils/log.ts b/frontend/appflowy_web_app/src/utils/log.ts new file mode 100644 index 0000000000..daccf21d0a --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/log.ts @@ -0,0 +1,20 @@ +export class Log { + static error(...msg: unknown[]) { + console.error(...msg); + } + static info(...msg: unknown[]) { + console.info(...msg); + } + + static debug(...msg: unknown[]) { + console.debug(...msg); + } + + static trace(...msg: unknown[]) { + console.trace(...msg); + } + + static warn(...msg: unknown[]) { + console.warn(...msg); + } +} diff --git a/frontend/appflowy_web_app/src/utils/platform.ts b/frontend/appflowy_web_app/src/utils/platform.ts new file mode 100644 index 0000000000..fc3a538750 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/platform.ts @@ -0,0 +1,6 @@ +export function getPlatform() { + return { + isTauri: !!import.meta.env.TAURI_PLATFORM, + isMobile: window.navigator.userAgent.match(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i), + }; +} diff --git a/frontend/appflowy_web_app/src/utils/time.ts b/frontend/appflowy_web_app/src/utils/time.ts new file mode 100644 index 0000000000..792b72ee61 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/time.ts @@ -0,0 +1,6 @@ +import dayjs from 'dayjs'; + +export function renderDate(date: string, format: string, isUnix?: boolean): string { + if (isUnix) return dayjs.unix(Number(date)).format(format); + return dayjs(date).format(format); +} diff --git a/frontend/appflowy_web_app/src/utils/url.ts b/frontend/appflowy_web_app/src/utils/url.ts new file mode 100644 index 0000000000..a10cf9ca85 --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/url.ts @@ -0,0 +1,49 @@ +import { getPlatform } from '@/utils/platform'; +import isURL from 'validator/lib/isURL'; +import isIP from 'validator/lib/isIP'; +import isFQDN from 'validator/lib/isFQDN'; + +export const downloadPage = 'https://appflowy.io/download'; + +export const openAppFlowySchema = 'appflowy-flutter://'; + +export function isValidUrl(input: string) { + return isURL(input, { require_protocol: true, require_host: false }); +} + +// Process the URL to make sure it's a valid URL +// If it's not a valid URL(eg: 'appflowy.io' or '192.168.1.2'), we'll add 'https://' to the URL +export function processUrl(input: string) { + let processedUrl = input; + + if (isValidUrl(input)) { + return processedUrl; + } + + const domain = input.split('/')[0]; + + if (isIP(domain) || isFQDN(domain)) { + processedUrl = `https://${input}`; + if (isValidUrl(processedUrl)) { + return processedUrl; + } + } + + return; +} + +export async function openUrl(url: string, target: string = '_current') { + const platform = getPlatform(); + + const newUrl = processUrl(url); + + if (!newUrl) return; + if (platform.isTauri) { + const { open } = await import('@tauri-apps/api/shell'); + + await open(newUrl); + return; + } + + window.open(newUrl, target); +} diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts new file mode 100644 index 0000000000..5748ee1aed --- /dev/null +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -0,0 +1,12 @@ +/// <reference types="vite/client" /> +/// <reference types="vite-plugin-svgr/client" /> +/// <reference types="vite-plugin-terminal/client" /> +/// <reference types="cypress" /> +/// <reference types="cypress-plugin-tab" /> +interface Window { + refresh_token: (token: string) => void; + invalid_token: () => void; + WebFont?: { + load: (options: { google: { families: string[] } }) => void; + }; +} diff --git a/frontend/appflowy_web_app/start.sh b/frontend/appflowy_web_app/start.sh new file mode 100644 index 0000000000..b4691baa1a --- /dev/null +++ b/frontend/appflowy_web_app/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Start the frontend server +bun run server.cjs & + +# Start the nginx server +service nginx start + +tail -f /dev/null + diff --git a/frontend/appflowy_web_app/tailwind.config.cjs b/frontend/appflowy_web_app/tailwind.config.cjs new file mode 100644 index 0000000000..8589a0f4b6 --- /dev/null +++ b/frontend/appflowy_web_app/tailwind.config.cjs @@ -0,0 +1,20 @@ +const colors = require('./tailwind/colors.cjs'); +const boxShadow = require('./tailwind/box-shadow.cjs'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + './node_modules/react-tailwindcss-datepicker/dist/index.esm.js', + ], + important: '#body', + darkMode: 'class', + theme: { + extend: { + colors, + boxShadow, + }, + }, + plugins: [], +}; diff --git a/frontend/appflowy_web_app/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/tailwind/box-shadow.cjs new file mode 100644 index 0000000000..d72c255227 --- /dev/null +++ b/frontend/appflowy_web_app/tailwind/box-shadow.cjs @@ -0,0 +1,11 @@ + +/** +* Do not edit directly +* Generated on Mon, 27 May 2024 06:26:20 GMT +* Generated from $pnpm css:variables +*/ + + +module.exports = { + "md": "var(--shadow)" +}; diff --git a/frontend/appflowy_web_app/tailwind/colors.cjs b/frontend/appflowy_web_app/tailwind/colors.cjs new file mode 100644 index 0000000000..27a3b07c30 --- /dev/null +++ b/frontend/appflowy_web_app/tailwind/colors.cjs @@ -0,0 +1,77 @@ + +/** +* Do not edit directly +* Generated on Mon, 27 May 2024 06:26:20 GMT +* Generated from $pnpm css:variables +*/ + + +module.exports = { + "text": { + "title": "var(--text-title)", + "caption": "var(--text-caption)", + "placeholder": "var(--text-placeholder)", + "disabled": "var(--text-disabled)", + "link-default": "var(--text-link-default)", + "link-hover": "var(--text-link-hover)", + "link-pressed": "var(--text-link-pressed)", + "link-disabled": "var(--text-link-disabled)" + }, + "icon": { + "primary": "var(--icon-primary)", + "secondary": "var(--icon-secondary)", + "disabled": "var(--icon-disabled)", + "on-toolbar": "var(--icon-on-toolbar)" + }, + "line": { + "border": "var(--line-border)", + "divider": "var(--line-divider)", + "on-toolbar": "var(--line-on-toolbar)" + }, + "fill": { + "toolbar": "var(--fill-toolbar)", + "default": "var(--fill-default)", + "hover": "var(--fill-hover)", + "pressed": "var(--fill-pressed)", + "active": "var(--fill-active)", + "list-hover": "var(--fill-list-hover)", + "list-active": "var(--fill-list-active)" + }, + "content": { + "blue-400": "var(--content-blue-400)", + "blue-300": "var(--content-blue-300)", + "blue-600": "var(--content-blue-600)", + "blue-100": "var(--content-blue-100)", + "blue-50": "var(--content-blue-50)", + "on-fill-hover": "var(--content-on-fill-hover)", + "on-fill": "var(--content-on-fill)", + "on-tag": "var(--content-on-tag)" + }, + "bg": { + "body": "var(--bg-body)", + "base": "var(--bg-base)", + "tips": "var(--bg-tips)", + "brand": "var(--bg-brand)" + }, + "function": { + "error": "var(--function-error)", + "waring": "var(--function-waring)", + "success": "var(--function-success)", + "info": "var(--function-info)" + }, + "tint": { + "purple": "var(--tint-purple)", + "pink": "var(--tint-pink)", + "red": "var(--tint-red)", + "lime": "var(--tint-lime)", + "green": "var(--tint-green)", + "aqua": "var(--tint-aqua)", + "blue": "var(--tint-blue)", + "orange": "var(--tint-orange)", + "yellow": "var(--tint-yellow)" + }, + "scrollbar": { + "thumb": "var(--scrollbar-thumb)", + "track": "var(--scrollbar-track)" + } +}; diff --git a/frontend/appflowy_web_app/test.env b/frontend/appflowy_web_app/test.env new file mode 100644 index 0000000000..89e2936fab --- /dev/null +++ b/frontend/appflowy_web_app/test.env @@ -0,0 +1,3 @@ +AF_WS_URL=wss://test.appflowy.cloud/ws/v1 +AF_BASE_URL=https://test.appflowy.cloud +AF_GOTRUE_URL=https://test.appflowy.cloud/gotrue \ No newline at end of file diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json new file mode 100644 index 0000000000..875f06f3e5 --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "preserveValueImports": false, + "noEmit": true, + "jsx": "react-jsx", + "types": [ + "cypress", + "node", + "jest" + ], + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ], + "src/*": [ + "src/*" + ], + "$client-services": [ + "src/application/services/js-services" + ], + "$icons/*": [ + "../resources/flowy-flowy_icons/*" + ] + } + }, + "include": [ + "src", + "vite.config.ts", + "cypress.config.ts", + "cypress" + ], + "exclude": [ + "node_modules", + "dist", + "coverage" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/appflowy_web_app/tsconfig.node.json b/frontend/appflowy_web_app/tsconfig.node.json new file mode 100644 index 0000000000..20f2adefab --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.node.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ], + "exclude": [ + "node_modules", + "dist", + "coverage" + ] +} diff --git a/frontend/appflowy_web_app/tsconfig.web.json b/frontend/appflowy_web_app/tsconfig.web.json new file mode 100644 index 0000000000..90f704a38f --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.web.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "src/application/services/tauri-services", + "**/*.cy.tsx", + "dist", + "coverage" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts new file mode 100644 index 0000000000..451fe002fb --- /dev/null +++ b/frontend/appflowy_web_app/vite.config.ts @@ -0,0 +1,152 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; +import wasm from 'vite-plugin-wasm'; +import { visualizer } from 'rollup-plugin-visualizer'; +import usePluginImport from 'vite-plugin-importer'; +import { totalBundleSize } from 'vite-plugin-total-bundle-size'; +import path from 'path'; +import istanbul from 'vite-plugin-istanbul'; + +const resourcesPath = path.resolve(__dirname, '../resources'); +const isDev = process.env.NODE_ENV === 'development'; +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + wasm(), + svgr({ + svgrOptions: { + prettier: false, + plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], + icon: true, + svgoConfig: { + multipass: true, + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, + svgProps: { + role: 'img', + }, + replaceAttrValues: { + '#333': 'currentColor', + }, + }, + }), + istanbul({ + cypress: true, + requireEnv: false, + include: ['src/**/*'], + exclude: [ + '**/__tests__/**/*', + 'cypress/**/*', + 'node_modules/**/*', + 'src/application/services/tauri-services/**/*', + ], + }), + usePluginImport({ + libraryName: '@mui/icons-material', + libraryDirectory: '', + camel2DashComponentName: false, + style: false, + }), + process.env.ANALYZE_MODE + ? visualizer({ + emitFile: true, + }) + : undefined, + process.env.ANALYZE_MODE + ? totalBundleSize({ + fileNameRegex: /\.(js|css)$/, + calculateGzip: false, + }) + : undefined, + ], + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + port: !!process.env.TAURI_PLATFORM ? 5173 : process.env.PORT ? parseInt(process.env.PORT) : 3000, + strictPort: true, + watch: { + ignored: ['node_modules'], + }, + cors: false, + }, + envPrefix: ['AF', 'TAURI_'], + esbuild: { + pure: !isDev ? ['console.log', 'console.debug', 'console.info', 'console.trace'] : [], + }, + build: !!process.env.TAURI_PLATFORM + ? { + // Tauri supports es2021 + target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG, + } + : { + target: `esnext`, + reportCompressedSize: true, + sourcemap: isDev, + rollupOptions: !isDev + ? { + output: { + chunkFileNames: 'static/js/[name]-[hash].js', + entryFileNames: 'static/js/[name]-[hash].js', + assetFileNames: 'static/[ext]/[name]-[hash].[ext]', + manualChunks(id) { + if ( + id.includes('/react@') || + id.includes('/react-dom@') || + id.includes('/react-is@') || + id.includes('/yjs@') || + id.includes('/y-indexeddb@') || + id.includes('/redux') || + id.includes('/react-custom-scrollbars') + ) { + return 'common'; + } + }, + }, + } + : {}, + }, + resolve: { + alias: [ + { find: 'src/', replacement: `${__dirname}/src/` }, + { find: '@/', replacement: `${__dirname}/src/` }, + { + find: '$client-services', + replacement: !!process.env.TAURI_PLATFORM + ? `${__dirname}/src/application/services/tauri-services` + : `${__dirname}/src/application/services/js-services`, + }, + { find: '$icons', replacement: `${resourcesPath}/flowy_icons/` }, + ], + }, + + optimizeDeps: { + include: [ + 'react', + 'react-dom', + '@mui/icons-material/ErrorOutline', + '@mui/icons-material/CheckCircleOutline', + '@mui/icons-material/FunctionsOutlined', + 'react-katex', + // 'react-custom-scrollbars-2', + // 'react-window', + // 'react-virtualized-auto-sizer', + ], + }, +}); diff --git a/frontend/resources/flowy_icons/16x/add_less_padding.svg b/frontend/resources/flowy_icons/16x/add_less_padding.svg deleted file mode 100644 index 4c56779b38..0000000000 --- a/frontend/resources/flowy_icons/16x/add_less_padding.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3 8H13" stroke="#000000" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 13V3" stroke="#000000" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/add_thin.svg b/frontend/resources/flowy_icons/16x/add_thin.svg deleted file mode 100644 index 56520cf3cf..0000000000 --- a/frontend/resources/flowy_icons/16x/add_thin.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="#000000" id="Plus-Light--Streamline-Phosphor.svg" height="16" width="16"><desc>Plus Light Streamline Icon: https://streamlinehq.com</desc><path d="M14.85 7.5c0 0.2591015625 -0.210046875 0.4691484375 -0.4691484375 0.4691484375H7.9691484374999995v6.411703125c0 0.3611484375 -0.39095507812499997 0.5868691406250001 -0.70372265625 0.406294921875 -0.145154296875 -0.083806640625 -0.23457421875 -0.2386875 -0.23457421875 -0.406294921875V7.9691484374999995H0.6191484375c-0.3611484375 0 -0.5868691406250001 -0.39095507812499997 -0.406294921875 -0.70372265625 0.083806640625 -0.145154296875 0.238681640625 -0.23457421875 0.406294921875 -0.23457421875h6.411703125V0.6191484375c0 -0.3611484375 0.39095507812499997 -0.5868691406250001 0.70372265625 -0.406294921875 0.145154296875 0.083806640625 0.23457421875 0.238681640625 0.23457421875 0.406294921875v6.411703125h6.411703125c0.259095703125 0.00001171875 0.4691484375 0.21005273437500002 0.4691484375 0.4691484375Z" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/ai_add_to_page.svg b/frontend/resources/flowy_icons/16x/ai_add_to_page.svg deleted file mode 100644 index 8895d9953d..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_add_to_page.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.02353 11.94L8.28027 10.66L7.02353 9.38L8.28027 10.66H4.28027" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.24 7.24V10.66C14.24 13.98 13.056 15.32 9.72 15.32H5.72C2.384 15.32 1.2 13.98 1.2 10.66V6.62C1.2 3.3 2.384 1.96 5.72 1.96H8.72" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.24 7.24H11.72C9.72 7.24 8.72 6.62 8.72 4.52V1.96L14.24 7.24Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_at.svg b/frontend/resources/flowy_icons/16x/ai_at.svg deleted file mode 100644 index 72128f6c9c..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_at.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.5 8C5.5 8.66304 5.76339 9.29893 6.23223 9.76777C6.70107 10.2366 7.33696 10.5 8 10.5C8.66304 10.5 9.29893 10.2366 9.76777 9.76777C10.2366 9.29893 10.5 8.66304 10.5 8C10.5 7.33696 10.2366 6.70107 9.76777 6.23223C9.29893 5.76339 8.66304 5.5 8 5.5C7.33696 5.5 6.70107 5.76339 6.23223 6.23223C5.76339 6.70107 5.5 7.33696 5.5 8Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.5 5.50001V8.62501C10.5 9.12229 10.6976 9.5992 11.0492 9.95083C11.4008 10.3025 11.8777 10.5 12.375 10.5C12.8723 10.5 13.3492 10.3025 13.7008 9.95083C14.0525 9.5992 14.25 9.12229 14.25 8.62501V8.00001C14.25 6.59207 13.7746 5.22538 12.9009 4.12136C12.0271 3.01734 10.8062 2.24068 9.43596 1.9172C8.06569 1.59372 6.62634 1.74238 5.35111 2.3391C4.07588 2.93582 3.03949 3.94563 2.40984 5.20492C1.78019 6.46422 1.59418 7.89922 1.88195 9.27743C2.16971 10.6556 2.91439 11.8963 3.99534 12.7985C5.07628 13.7006 6.43016 14.2113 7.83762 14.2479C9.24508 14.2845 10.6237 13.8448 11.75 13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_attachment.svg b/frontend/resources/flowy_icons/16x/ai_attachment.svg deleted file mode 100644 index d998bbd621..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_attachment.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.20789 11.9705L10.6031 6.80618C11.2508 6.18618 11.2508 5.18087 10.6031 4.56087C9.95533 3.94087 8.90514 3.9408 8.25739 4.56087L2.90127 9.68781C1.67058 10.8658 1.67058 12.7759 2.90127 13.9539C4.13202 15.1321 6.12739 15.1321 7.35814 13.9539L12.7924 8.75218C14.6061 7.01605 14.6061 4.2013 12.7924 2.46518C10.9787 0.729055 8.03808 0.729055 6.22439 2.46518L1.8457 6.65649" stroke="black" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_chat_outlined.svg b/frontend/resources/flowy_icons/16x/ai_chat_outlined.svg deleted file mode 100644 index 3be82d4bf2..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_chat_outlined.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.5 6.06201H11.1667C11.5 6.06201 11.8333 6.32764 11.8333 6.72868C11.8333 7.06201 11.5755 7.39535 11.1667 7.39534H6.51302C6.09865 7.39535 5.83333 7.06201 5.83333 6.72868C5.83333 6.32764 6.16667 6.06201 6.5 6.06201Z" fill="black"/> -<path d="M6.5 8.72868H9.15885C9.57031 8.72868 9.83333 9.06201 9.83333 9.39535C9.83333 9.72868 9.57031 10.062 9.16667 10.062H6.5C6.16667 10.062 5.83333 9.79653 5.83333 9.39535C5.83333 8.99416 6.16667 8.72868 6.5 8.72868Z" fill="black"/> -<path d="M2.76088 13.8836L3.46384 12.8777C2.32857 11.711 1.60305 10.1638 1.51107 8.45342C1.50372 8.32386 1.5 8.19335 1.5 8.06201C1.5 4.19602 4.72439 1.06201 8.70189 1.06201C8.72413 1.06201 8.74635 1.06211 8.76854 1.06231C8.79012 1.06211 8.81171 1.06201 8.83333 1.06201C10.574 1.06201 12.1663 1.69737 13.391 2.74883C14.3416 3.54217 15.0769 4.5724 15.5 5.74532C15.5145 5.78564 15.8333 7.10668 15.8333 8.06201C15.8333 11.645 13.1413 14.5993 9.66957 15.0126L9.66791 15.0128C9.39425 15.0453 9.11574 15.062 8.83333 15.062L8.69835 15.0617C8.56267 15.0614 8.38169 15.0609 8.3441 15.0617C8.33953 15.0618 8.33708 15.062 8.33708 15.062H3.40299C2.77997 15.062 2.41082 14.3845 2.76088 13.8836ZM8.3408 13.7283C8.35765 13.7281 8.37817 13.728 8.39845 13.728C8.44025 13.7279 8.49549 13.728 8.55174 13.7281L8.83333 13.7287C11.9629 13.7287 14.5 11.1916 14.5 8.06201C14.5 7.70427 14.4372 7.22782 14.3597 6.79581C14.3225 6.58852 14.285 6.40744 14.2563 6.27777C14.2435 6.21991 14.2328 6.17374 14.2251 6.14142C13.8818 5.22135 13.2962 4.40641 12.5367 3.77253L12.5295 3.76654L12.5224 3.76045C11.5304 2.90871 10.2435 2.39535 8.83333 2.39535C8.81574 2.39535 8.79817 2.39542 8.78063 2.39558L8.76871 2.39569L8.75679 2.39559C8.73852 2.39543 8.72021 2.39535 8.70189 2.39535C5.42478 2.39535 2.83333 4.96788 2.83333 8.06201C2.83333 8.16816 2.83634 8.27349 2.84226 8.37793L2.84248 8.38181C2.91624 9.7534 3.49725 11.0002 4.41943 11.9479L5.18819 12.7379L4.49581 13.7287H8.3143C8.32535 13.7284 8.33506 13.7283 8.3408 13.7283ZM14.2152 6.10117L14.216 6.10405C14.2116 6.08819 14.2109 6.08427 14.2152 6.10117Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_check_filled.svg b/frontend/resources/flowy_icons/16x/ai_check_filled.svg deleted file mode 100644 index 79efe0bd0a..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_check_filled.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clip-path="url(#clip0_3494_21449)"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M15.35 8A7.35 7.35 0 1 1 0.65 8a7.35 7.35 0 0 1 14.7 0M11.002 5.742a0.56 0.56 0 0 1 0 0.79L7.278 10.256a0.56 0.56 0 0 1 -0.79 0L5 8.768a0.558 0.558 0 1 1 0.79 -0.79l1.094 1.094 1.664 -1.665 1.665 -1.665a0.56 0.56 0 0 1 0.79 0" fill="black"/> - </g> - <defs> - <clipPath id="clip0_3494_21449"> - <path width="16" height="16" fill="white" d="M0 0H16V16H0V0z"/> - </clipPath> - </defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_close_filled.svg b/frontend/resources/flowy_icons/16x/ai_close_filled.svg deleted file mode 100644 index 9bf64c2bd8..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_close_filled.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clip-path="url(#clip0_3544_30046)"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M15.35 8A7.35 7.35 0 1 1 0.65 8a7.35 7.35 0 0 1 14.7 0M5.773 5.773a0.55 0.55 0 0 1 0.78 0L8 7.221l1.448 -1.448a0.551 0.551 0 0 1 0.78 0.78L8.78 8l1.448 1.448a0.55 0.55 0 1 1 -0.78 0.78L8 8.78l-1.447 1.448a0.551 0.551 0 1 1 -0.78 -0.78L7.221 8 5.773 6.553a0.55 0.55 0 0 1 0 -0.78" fill="black"/> - </g> - <defs> - <clipPath id="clip0_3544_30046"> - <path width="16" height="16" fill="white" d="M0 0H16V16H0V0z"/> - </clipPath> - </defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_dislike.svg b/frontend/resources/flowy_icons/16x/ai_dislike.svg deleted file mode 100644 index 5754775e88..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_dislike.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.84673 9.2077L2.35761 9.25189C2.33398 9.52552 2.09898 9.73183 1.82467 9.72002C1.5503 9.7082 1.33398 9.48227 1.33398 9.2077H1.84673ZM13.6314 7.96114L13.149 5.1717L14.1597 4.99695L14.642 7.78639L13.6314 7.96114ZM8.85148 1.67595H5.67311V0.650391H8.85148V1.67595ZM5.04998 2.24864L4.49467 8.67058L3.47292 8.5822L4.02817 2.16027L5.04998 2.24864ZM13.149 5.1717C12.8025 3.16802 10.9958 1.67595 8.85148 1.67595V0.650391C11.4675 0.650391 13.7235 2.4752 14.1597 4.99695L13.149 5.1717ZM8.85817 12.7179L8.40505 9.95258L9.41717 9.78683L9.8703 12.5521L8.85817 12.7179ZM4.70998 9.19989L5.69367 10.0476L5.02417 10.8245L4.04048 9.97683L4.70998 9.19989ZM7.48336 12.8055L7.80861 14.0594L6.81586 14.3169L6.49061 13.0631L7.48336 12.8055ZM8.29986 14.3054L8.39898 14.2735L8.71267 15.25L8.61355 15.2818L8.29986 14.3054ZM6.99073 11.5448C7.20398 11.9441 7.36961 12.367 7.48336 12.8055L6.49061 13.0631C6.39742 12.7036 6.26155 12.3565 6.08611 12.028L6.99073 11.5448ZM8.39898 14.2735C8.60848 14.2063 8.75724 14.0444 8.80611 13.8559L9.79892 14.1134C9.65836 14.6549 9.24392 15.0793 8.71267 15.25L8.39898 14.2735ZM7.80861 14.0594C7.83261 14.1516 7.89923 14.2365 8.00011 14.2851L7.55486 15.209C7.19111 15.0338 6.91786 14.7103 6.81586 14.3169L7.80861 14.0594ZM8.00011 14.2851C8.09217 14.3295 8.20061 14.3372 8.29986 14.3054L8.61355 15.2818C8.26505 15.3938 7.8848 15.368 7.55486 15.209L8.00011 14.2851ZM9.47273 8.69489H13.0152V9.72045H9.47273V8.69489ZM3.02192 1.5692L2.35761 9.25189L1.33586 9.16352L2.00017 1.48089L3.02192 1.5692ZM2.35955 1.49633V9.2077H1.33398V1.49633H2.35955ZM2.00017 1.48089C1.99117 1.5852 2.07336 1.67595 2.17955 1.67595V0.650391C2.67586 0.650391 3.06461 1.0757 3.02192 1.5692L2.00017 1.48089ZM9.8703 12.5521C9.95542 13.0718 9.93111 13.6036 9.79892 14.1134L8.80611 13.8559C8.90255 13.4844 8.92023 13.0968 8.85817 12.7179L9.8703 12.5521ZM5.67311 1.67595C5.34905 1.67595 5.07805 1.9242 5.04998 2.24864L4.02817 2.16027C4.10198 1.30658 4.81592 0.650391 5.67311 0.650391V1.67595ZM5.69367 10.0476C6.15848 10.4481 6.65961 10.9248 6.99073 11.5448L6.08611 12.028C5.8493 11.5846 5.47217 11.2105 5.02417 10.8245L5.69367 10.0476ZM14.642 7.78639C14.8166 8.79589 14.0403 9.72045 13.0152 9.72045V8.69489C13.4025 8.69489 13.6978 8.34502 13.6314 7.96114L14.642 7.78639ZM2.17955 1.67595C2.27948 1.67595 2.35955 1.59502 2.35955 1.49633H1.33398C1.33398 1.0297 1.71198 0.650391 2.17955 0.650391V1.67595ZM8.40505 9.95258C8.29723 9.29427 8.80467 8.69489 9.47273 8.69489V9.72045C9.43867 9.72045 9.41136 9.75139 9.41717 9.78683L8.40505 9.95258ZM4.49467 8.67058C4.4773 8.87158 4.55755 9.06852 4.70998 9.19989L4.04048 9.97683C3.63823 9.63014 3.42717 9.1112 3.47292 8.5822L4.49467 8.67058Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_expand.svg b/frontend/resources/flowy_icons/16x/ai_expand.svg deleted file mode 100644 index 83df4e9234..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_expand.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3622_33848)"> -<path d="M5.94811 10.0508L1.16211 14.8368M1.16211 14.8368H5.16673M1.16211 14.8368V10.8322" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.0508 5.94909L14.8368 1.16309M14.8368 1.16309H10.8322M14.8368 1.16309V5.16771" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_3622_33848"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg b/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg deleted file mode 100644 index 2f7df6c78a..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_fix_spelling_grammar.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.3327 4L5.99935 11.3333L2.66602 8" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_image.svg b/frontend/resources/flowy_icons/16x/ai_image.svg deleted file mode 100644 index 6f33715496..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_image.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_4912_80381)"> -<path d="M2.44453 7.99973C2.44453 5.38094 2.44453 4.0716 3.25805 3.25803C4.07162 2.44452 5.38096 2.44452 7.99975 2.44452C10.6185 2.44452 11.9279 2.44452 12.7414 3.25803C13.555 4.0716 13.555 5.38094 13.555 7.99973C13.555 10.6185 13.555 11.9279 12.7414 12.7414C11.9279 13.5549 10.6185 13.5549 7.99975 13.5549C5.38096 13.5549 4.07162 13.5549 3.25805 12.7414C2.44453 11.9279 2.44453 10.6185 2.44453 7.99973Z" stroke="black"/> -<path d="M9.11211 5.77811C9.11028 6.63337 10.035 7.16992 10.7766 6.74387C11.1223 6.54526 11.3351 6.17674 11.3342 5.77811C11.336 4.9228 10.4113 4.3863 9.66969 4.81235C9.32402 5.01091 9.11125 5.37948 9.11211 5.77811Z" fill="black" stroke="black" stroke-width="0.2"/> -<path d="M2.44453 8.27756L3.41755 7.42616C3.92379 6.98325 4.68678 7.00864 5.1624 7.48425L7.54546 9.86732C7.92718 10.2491 8.52818 10.3011 8.96993 9.99066L9.13557 9.87427C9.7712 9.42755 10.6312 9.4793 11.2087 9.99904L12.9995 11.6107" stroke="black" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_4912_80381"> -<rect width="13" height="13" fill="white" transform="translate(1.5 1.5)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_improve_writing.svg b/frontend/resources/flowy_icons/16x/ai_improve_writing.svg deleted file mode 100644 index 9cff9e9875..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_improve_writing.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.27221 14.1865C6.40487 14.1865 6.53208 14.1338 6.62586 14.04L14.5247 6.13678C14.7198 5.94154 14.7198 5.62512 14.5247 5.42987L12.4109 3.3148C12.2156 3.1194 11.8989 3.1194 11.7036 3.31479L3.80504 11.2177C3.71133 11.3115 3.65869 11.4386 3.65869 11.5712V13.6865C3.65869 13.9627 3.88255 14.1865 4.15869 14.1865H6.27221Z" stroke="#1F2329"/> -<path d="M10.0376 4.97949L12.8583 7.80188" stroke="#1F2329"/> -<path d="M6.44835 1.52132C6.48728 1.42647 6.62028 1.42649 6.65918 1.52136L6.70994 1.64515C6.78714 1.83341 6.93517 1.983 7.1215 2.06101L7.24319 2.11196C7.3371 2.15129 7.33708 2.2857 7.24315 2.32499L7.12064 2.37624C6.93411 2.45426 6.78593 2.60399 6.70873 2.79245L6.65864 2.91471C6.61975 3.00967 6.48662 3.00964 6.44775 2.91468L6.39732 2.79145C6.32013 2.60284 6.17185 2.45299 5.98519 2.37494L5.86382 2.3242C5.76986 2.28492 5.76984 2.15047 5.86378 2.11115L5.98633 2.05987C6.17263 1.9819 6.32067 1.83239 6.39791 1.6442L6.44835 1.52132Z" fill="#1F2329"/> -<path d="M4.18739 2.01949C4.28856 1.77289 4.63432 1.77295 4.73541 2.01958L4.93384 2.5037C5.18456 3.1154 5.66544 3.60147 6.27078 3.85504L6.75028 4.05591C6.99433 4.15814 6.99427 4.50742 6.75019 4.60957L6.27096 4.81012C5.665 5.06371 5.18365 5.55024 4.93291 6.16257L4.73526 6.64525C4.63418 6.8921 4.28808 6.89204 4.18708 6.64516L3.98871 6.16024C3.73801 5.54741 3.25636 5.06048 2.64994 4.80683L2.1724 4.60708C1.92826 4.50496 1.9282 4.15559 2.17231 4.05338L2.65258 3.85229C3.25783 3.59888 3.73873 3.11305 3.9896 2.50158L4.18739 2.01949Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_indicator.svg b/frontend/resources/flowy_icons/16x/ai_indicator.svg index 390571cb8c..690c01ac0b 100644 --- a/frontend/resources/flowy_icons/16x/ai_indicator.svg +++ b/frontend/resources/flowy_icons/16x/ai_indicator.svg @@ -1,11 +1,23 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect width="18" height="18" rx="6" fill="url(#paint0_linear_3154_1297)" fill-opacity="0.08"/> -<path d="M9.06641 10.3512L9.92804 12.6047C10.0221 12.8506 10.2976 12.9737 10.5435 12.8797C10.7894 12.7857 10.9125 12.5101 10.8185 12.2643L8.2445 5.53232C8.01837 4.9409 7.18168 4.9409 6.95555 5.53232L4.38157 12.2643C4.28756 12.5101 4.41067 12.7857 4.65656 12.8797C4.90245 12.9737 5.17799 12.8506 5.27201 12.6047L6.13364 10.3512H9.06641ZM6.49813 9.39792L7.60002 6.51606L8.70192 9.39792H6.49813Z" fill="#9D46F3" stroke="#9D46F3" stroke-width="0.1"/> -<rect x="12.3" y="5.10001" width="1" height="7.8" rx="0.5" fill="#9D46F3"/> +<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g filter="url(#filter0_d_2389_7357)"> +<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="url(#paint0_linear_2389_7357)" shape-rendering="crispEdges"/> +<rect x="3.66663" y="2" width="16" height="16" rx="6" fill="#806989" shape-rendering="crispEdges"/> +<path d="M11.1576 11.884H8.79963L8.42163 13H6.81063L9.09663 6.682H10.8786L13.1646 13H11.5356L11.1576 11.884ZM10.7616 10.696L9.97863 8.383L9.20463 10.696H10.7616ZM14.6794 7.456C14.4094 7.456 14.1874 7.378 14.0134 7.222C13.8454 7.06 13.7614 6.862 13.7614 6.628C13.7614 6.388 13.8454 6.19 14.0134 6.034C14.1874 5.872 14.4094 5.791 14.6794 5.791C14.9434 5.791 15.1594 5.872 15.3274 6.034C15.5014 6.19 15.5884 6.388 15.5884 6.628C15.5884 6.862 15.5014 7.06 15.3274 7.222C15.1594 7.378 14.9434 7.456 14.6794 7.456ZM15.4444 7.978V13H13.9054V7.978H15.4444Z" fill="white"/> +</g> <defs> -<linearGradient id="paint0_linear_3154_1297" x1="-5.28572" y1="16.9984" x2="16.2306" y2="3.99301" gradientUnits="userSpaceOnUse"> -<stop stop-color="#665CE5"/> -<stop offset="1" stop-color="#F03EFF"/> +<filter id="filter0_d_2389_7357" x="0.666626" y="0" width="22" height="22" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> +<feFlood flood-opacity="0" result="BackgroundImageFix"/> +<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/> +<feOffset dy="1"/> +<feGaussianBlur stdDeviation="1.5"/> +<feComposite in2="hardAlpha" operator="out"/> +<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/> +<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2389_7357"/> +<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2389_7357" result="shape"/> +</filter> +<linearGradient id="paint0_linear_2389_7357" x1="15.6666" y1="2.4" x2="6.86663" y2="17.2" gradientUnits="userSpaceOnUse"> +<stop stop-color="#726084" stop-opacity="0.8"/> +<stop offset="1" stop-color="#5D5862"/> </linearGradient> </defs> </svg> diff --git a/frontend/resources/flowy_icons/16x/ai_like.svg b/frontend/resources/flowy_icons/16x/ai_like.svg deleted file mode 100644 index b7a92725aa..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_like.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.8468 6.79307L2.35767 6.74888C2.33405 6.47532 2.09905 6.26888 1.82467 6.28076C1.55036 6.29257 1.33398 6.51844 1.33398 6.79307H1.8468ZM13.6315 8.03963L13.1491 10.829L14.1597 11.0038L14.6421 8.21438L13.6315 8.03963ZM8.85155 14.3248H5.67317V15.3504H8.85155V14.3248ZM5.05005 13.7522L4.49473 7.33019L3.47298 7.41851L4.02823 13.8405L5.05005 13.7522ZM13.1491 10.829C12.8026 12.8327 10.9959 14.3248 8.85155 14.3248V15.3504C11.4676 15.3504 13.7236 13.5255 14.1597 11.0038L13.1491 10.829ZM8.85823 3.28282L8.40511 6.04819L9.41723 6.21401L9.87036 3.44863L8.85823 3.28282ZM4.71005 6.80088L5.69373 5.95319L5.02423 5.17632L4.04055 6.02394L4.71005 6.80088ZM7.48342 3.19526L7.80867 1.94138L6.81598 1.68388L6.49067 2.93769L7.48342 3.19526ZM8.29992 1.69538L8.39905 1.72726L8.71273 0.750819L8.61361 0.718944L8.29992 1.69538ZM6.9908 4.45594C7.20405 4.05669 7.36973 3.63369 7.48342 3.19526L6.49067 2.93769C6.39748 3.29713 6.26161 3.64432 6.08617 3.97276L6.9908 4.45594ZM8.39905 1.72726C8.60855 1.79451 8.75724 1.95632 8.80617 2.14488L9.79898 1.88738C9.65848 1.34588 9.24398 0.921444 8.71273 0.750819L8.39905 1.72726ZM7.80867 1.94138C7.83267 1.84907 7.8993 1.76426 8.00017 1.71563L7.55492 0.791757C7.19117 0.967007 6.91798 1.29051 6.81598 1.68388L7.80867 1.94138ZM8.00017 1.71563C8.0923 1.67126 8.20073 1.66351 8.29992 1.69538L8.61361 0.718944C8.26511 0.607007 7.88492 0.632757 7.55492 0.791757L8.00017 1.71563ZM9.47286 7.30588H13.0152V6.28026H9.47286V7.30588ZM3.02198 14.4315L2.35767 6.74888L1.33592 6.83726L2.00023 14.5199L3.02198 14.4315ZM2.35961 14.5044V6.79307H1.33398L1.33405 14.5044H2.35961ZM2.00023 14.5199C1.99123 14.4156 2.07342 14.3248 2.17961 14.3248V15.3504C2.67592 15.3504 3.06467 14.925 3.02198 14.4315L2.00023 14.5199ZM9.87036 3.44863C9.95548 2.92894 9.93117 2.39713 9.79898 1.88738L8.80617 2.14488C8.90261 2.51644 8.9203 2.90401 8.85823 3.28282L9.87036 3.44863ZM5.67317 14.3248C5.34911 14.3248 5.07811 14.0766 5.05005 13.7522L4.02823 13.8405C4.10205 14.6941 4.81598 15.3504 5.67317 15.3504V14.3248ZM5.69373 5.95319C6.15855 5.55263 6.65967 5.07588 6.9908 4.45594L6.08617 3.97276C5.84936 4.41607 5.47223 4.79026 5.02423 5.17632L5.69373 5.95319ZM14.6421 8.21438C14.8167 7.20488 14.0404 6.28026 13.0152 6.28026V7.30588C13.4027 7.30588 13.6979 7.65569 13.6315 8.03963L14.6421 8.21438ZM2.17961 14.3248C2.27955 14.3248 2.35961 14.4058 2.35961 14.5044H1.33405C1.33405 14.9711 1.71205 15.3504 2.17961 15.3504V14.3248ZM8.40511 6.04819C8.2973 6.70657 8.8048 7.30588 9.47286 7.30588V6.28026C9.43873 6.28026 9.41142 6.24938 9.41723 6.21401L8.40511 6.04819ZM4.49473 7.33019C4.47736 7.12919 4.55761 6.93226 4.71005 6.80088L4.04055 6.02394C3.6383 6.37057 3.42723 6.88951 3.47298 7.41851L4.49473 7.33019Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_list.svg b/frontend/resources/flowy_icons/16x/ai_list.svg deleted file mode 100644 index ee31345b13..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_list.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.5 4.25H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.5 8H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.5 11.75H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.375 4.25H2.38125" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.375 8H2.38125" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.375 11.75H2.38125" stroke="black" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_make_longer.svg b/frontend/resources/flowy_icons/16x/ai_make_longer.svg deleted file mode 100644 index 9f61441f0f..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_make_longer.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.00504 11.7381C3.87137 11.7381 3.75931 11.6939 3.66887 11.6054C3.57843 11.517 3.5332 11.4074 3.5332 11.2766C3.5332 11.1458 3.57843 11.0338 3.66887 10.9406C3.75931 10.8475 3.87137 10.8009 4.00504 10.8009H8.0332C8.15598 10.8009 8.25887 10.8427 8.34187 10.9263C8.42498 11.0099 8.46654 11.1135 8.46654 11.2371C8.46654 11.3589 8.42504 11.4621 8.34204 11.5469C8.25915 11.6316 8.15831 11.6739 8.03954 11.6739L4.00504 11.7381ZM3.97137 9.37659C3.8487 9.37659 3.74504 9.33537 3.66037 9.25292C3.57559 9.17048 3.5332 9.06837 3.5332 8.94659C3.5332 8.8247 3.5747 8.72142 3.6577 8.63675C3.7407 8.5522 3.84354 8.50992 3.9662 8.50992H12.0284C12.151 8.50992 12.2547 8.55114 12.3394 8.63359C12.4241 8.71603 12.4665 8.81814 12.4665 8.93992C12.4665 9.06181 12.425 9.16503 12.342 9.24959C12.259 9.33426 12.1562 9.37659 12.0335 9.37659H3.97137ZM3.97137 7.09192C3.8487 7.09192 3.74504 7.0507 3.66037 6.96825C3.57559 6.88581 3.5332 6.7837 3.5332 6.66192C3.5332 6.54003 3.5747 6.43681 3.6577 6.35226C3.7407 6.26759 3.84354 6.22526 3.9662 6.22526H12.0284C12.151 6.22526 12.2547 6.26648 12.3394 6.34892C12.4241 6.43137 12.4665 6.53348 12.4665 6.65526C12.4665 6.77714 12.425 6.88042 12.342 6.96509C12.259 7.04964 12.1562 7.09192 12.0335 7.09192H3.97137ZM4.00987 4.80092C3.87631 4.80092 3.76348 4.75603 3.67137 4.66625C3.57926 4.57648 3.5332 4.46526 3.5332 4.33259C3.5332 4.19992 3.57837 4.08753 3.6687 3.99542C3.75915 3.90342 3.87115 3.85742 4.0047 3.85742H11.9899C12.1234 3.85742 12.2363 3.90226 12.3284 3.99192C12.4205 4.0817 12.4665 4.19292 12.4665 4.32559C12.4665 4.45825 12.4214 4.57064 12.331 4.66276C12.2406 4.75487 12.1286 4.80092 11.995 4.80092H4.00987Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_make_shorter.svg b/frontend/resources/flowy_icons/16x/ai_make_shorter.svg deleted file mode 100644 index 5f07c58fcc..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_make_shorter.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.96654 9.83268C3.84376 9.83268 3.74087 9.79146 3.65787 9.70902C3.57476 9.62657 3.5332 9.52446 3.5332 9.40268C3.5332 9.28079 3.57476 9.17757 3.65787 9.09302C3.74087 9.00835 3.84376 8.96602 3.96654 8.96602H8.8332C8.95598 8.96602 9.05887 9.00724 9.14187 9.08968C9.22498 9.17213 9.26654 9.27424 9.26654 9.39602C9.26654 9.5179 9.22498 9.62113 9.14187 9.70568C9.05887 9.79035 8.95598 9.83268 8.8332 9.83268H3.96654ZM3.97137 7.03268C3.8487 7.03268 3.74504 6.99146 3.66037 6.90902C3.57559 6.82657 3.5332 6.72446 3.5332 6.60268C3.5332 6.48079 3.5747 6.37757 3.6577 6.29302C3.7407 6.20835 3.84354 6.16602 3.9662 6.16602H12.0284C12.151 6.16602 12.2547 6.20724 12.3394 6.28968C12.4241 6.37213 12.4665 6.47424 12.4665 6.59602C12.4665 6.7179 12.425 6.82113 12.342 6.90568C12.259 6.99035 12.1562 7.03268 12.0335 7.03268H3.97137Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_number_list.svg b/frontend/resources/flowy_icons/16x/ai_number_list.svg deleted file mode 100644 index c226e339f7..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_number_list.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.75 4.25H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.75 8H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.75 11.75H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3 4.25H3.625V6.75" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3 6.75H4.25" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.25 11.7495H3C3 11.1245 4.25 10.4995 4.25 9.87455C4.25 9.24955 3.625 8.93705 3 9.24955" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_page.svg b/frontend/resources/flowy_icons/16x/ai_page.svg deleted file mode 100644 index 09598791b9..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_page.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.6673 6.74503V10.0183C13.6673 13.2916 12.534 14.6009 9.70065 14.6009H6.30065C3.46732 14.6009 2.33398 13.2916 2.33398 10.0183V6.09038C2.33398 2.81712 3.46732 1.50781 6.30065 1.50781H9.13398" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.6173 6.66608H11.3507C9.65065 6.66608 9.08398 6.00787 9.08398 4.03323V1.68666C9.08398 1.58813 9.20647 1.54266 9.27075 1.61733L13.6173 6.66608Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_paragraph.svg b/frontend/resources/flowy_icons/16x/ai_paragraph.svg deleted file mode 100644 index fc184ace91..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_paragraph.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.75586 2.3252V14.4311" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.7832 2.3252V14.4311" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.2969 2.3252H6.10908C3.48807 2.3252 1.84996 5.16255 3.16044 7.43241C3.76862 8.4858 4.89272 9.13472 6.10908 9.13477H8.75721" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_reset.svg b/frontend/resources/flowy_icons/16x/ai_reset.svg deleted file mode 100644 index a589eda1fe..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_reset.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.0051 4.57673L12.449 4.02061C9.99206 1.56373 6.00856 1.56373 3.55169 4.02061C1.09475 6.47748 1.09475 10.461 3.55169 12.9179C6.00856 15.3749 9.99206 15.3749 12.449 12.9179C13.8778 11.4891 14.4756 9.54411 14.2426 7.68323M13.0051 4.57673H9.6685M13.0051 4.57673V1.24023" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_retry.svg b/frontend/resources/flowy_icons/16x/ai_retry.svg deleted file mode 100644 index cfc50452af..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_retry.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.38672 3.43945H11.6134C12.7201 3.43945 13.6134 4.33279 13.6134 5.43945V7.65279" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.49339 1.33301L2.38672 3.43966L4.49339 5.54635" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.6134 12.56H4.38672C3.28005 12.56 2.38672 11.6667 2.38672 10.56V8.34668" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.5059 14.6665L13.6125 12.5598L11.5059 10.4531" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_retry_filled.svg b/frontend/resources/flowy_icons/16x/ai_retry_filled.svg deleted file mode 100644 index 37fcdaea43..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_retry_filled.svg +++ /dev/null @@ -1,50 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - width="16" - height="16" - viewBox="0 0 16 16" - fill="none" - version="1.1" - id="svg3" - sodipodi:docname="ai_retry_filled.svg" - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns="http://www.w3.org/2000/svg" - xmlns:svg="http://www.w3.org/2000/svg"> - <sodipodi:namedview - id="namedview3" - pagecolor="#ffffff" - bordercolor="#000000" - borderopacity="0.25" - inkscape:showpageshadow="2" - inkscape:pageopacity="0.0" - inkscape:pagecheckerboard="0" - inkscape:deskcolor="#d1d1d1" - inkscape:zoom="24.32251" - inkscape:cx="-1.8090238" - inkscape:cy="8.6545345" - inkscape:window-width="1920" - inkscape:window-height="1008" - inkscape:window-x="0" - inkscape:window-y="0" - inkscape:window-maximized="1" - inkscape:current-layer="g2" /> - <g - clip-path="url(#clip0_3532_93841)" - id="g2"> - <path - d="M 7.9960937,0.6171875 C 3.8984912,0.61784666 0.57687774,3.9578212 0.57617187,8.078125 0.57580518,12.199191 3.8977327,15.540356 7.9960937,15.541016 12.095218,15.541441 15.418335,12.199958 15.417969,8.078125 15.417263,3.9570543 12.094459,0.61676218 7.9960937,0.6171875 Z m 3.1855473,2.3964844 c 0.308943,7.756e-4 0.558896,0.2516023 0.558593,0.5605469 v 2.1210937 c -0.0053,0.01456 -0.01113,0.028895 -0.01758,0.042969 -0.0053,0.131134 -0.05642,0.2562536 -0.144531,0.3535157 0,0 -0.002,0 -0.002,0 -0.104658,0.1046817 -0.246506,0.1636679 -0.394531,0.1640625 H 9.0605469 C 8.7508401,6.256162 8.4996974,6.0050192 8.5,5.6953125 8.5007757,5.3863688 8.7516023,5.1364161 9.0605469,5.1367187 h 0.515625 C 8.3534414,4.4547337 7.0207494,4.6410706 6,5.3710937 4.7359739,6.2751043 4.0326162,7.8888416 4.9101562,9.6816406 5.7876964,11.47444 7.495039,11.910503 8.984375,11.466797 c 1.489336,-0.443706 2.677355,-1.7416934 2.427734,-3.7265626 -0.03828,-0.3068806 0.179452,-0.5866929 0.486329,-0.625 0.30688,-0.038278 0.586692,0.1794512 0.625,0.4863281 C 12.838147,10.103997 11.240268,11.96241 9.3046875,12.539063 7.3691071,13.115715 5.0114879,12.4358 3.9042969,10.173828 2.7971059,7.9118565 3.7068477,5.6338604 5.3496094,4.4589844 6.8201788,3.4072574 8.9142819,3.2819034 10.621094,4.5292969 V 3.5742188 c -3.03e-4,-0.3097068 0.25084,-0.5608495 0.560547,-0.5605469 z" - style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#000000;enable-background:accumulate;stop-color:#000000;stop-opacity:1" - id="path13" /> - </g> - <defs - id="defs3"> - <clipPath - id="clip0_3532_93841"> - <path - d="M0 0H16V16H0V0z" - id="path3" /> - </clipPath> - </defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_retry_font.svg b/frontend/resources/flowy_icons/16x/ai_retry_font.svg deleted file mode 100644 index 1c622f4040..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_retry_font.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.93913 2.16989C6.3343 2.16989 4.96504 2.73674 3.83136 3.87042C2.69767 5.00411 2.13083 6.37336 2.13083 7.97819C2.13083 8.98158 2.36243 9.90038 2.82562 10.7346C3.28881 11.5687 3.91116 12.2337 4.69267 12.7297V11.7547C4.69267 11.592 4.74729 11.4556 4.85654 11.3455C4.96578 11.2355 5.10109 11.1805 5.26245 11.1805C5.42397 11.1805 5.56075 11.2355 5.67279 11.3455C5.78498 11.4556 5.84107 11.592 5.84107 11.7547V14.2688C5.84107 14.4701 5.77305 14.6387 5.63701 14.7748C5.50082 14.911 5.33217 14.9791 5.13105 14.9791H2.6167C2.454 14.9791 2.31767 14.9244 2.20769 14.8152C2.09756 14.706 2.04249 14.5706 2.04249 14.4093C2.04249 14.2478 2.09756 14.111 2.20769 13.9989C2.31767 13.8868 2.454 13.8307 2.6167 13.8307H4.23397C3.25782 13.227 2.47182 12.4136 1.87597 11.3905C1.28027 10.3675 0.982422 9.23747 0.982422 8.00028C0.982422 7.04621 1.16624 6.14449 1.53388 5.29511C1.90152 4.44558 2.40144 3.70479 3.03366 3.07272C3.66572 2.44051 4.40659 1.94058 5.25627 1.57294C6.10595 1.2053 7.00885 1.02148 7.96497 1.02148C9.43537 1.02148 10.7524 1.42902 11.9162 2.2441C13.08 3.05903 13.9181 4.10629 14.4303 5.38588C14.496 5.53355 14.4963 5.6785 14.4312 5.82073C14.366 5.96296 14.2614 6.06727 14.1176 6.13367C13.9737 6.20022 13.8305 6.19926 13.6878 6.1308C13.5453 6.06219 13.4411 5.95596 13.3753 5.81212C12.9541 4.74645 12.2553 3.873 11.279 3.19176C10.3027 2.51051 9.18942 2.16989 7.93913 2.16989Z" fill="black"/> -<path d="M8.65266 14.3423L11.2145 7.60975C11.2591 7.50867 11.3193 7.43207 11.3949 7.37995C11.4705 7.32782 11.5567 7.30176 11.6533 7.30176H12.0177C12.1204 7.30176 12.2098 7.32913 12.2859 7.38389C12.3619 7.43864 12.4223 7.51655 12.4669 7.61763L14.9796 14.3309C15.0412 14.4879 15.0306 14.635 14.9476 14.7725C14.8647 14.9099 14.7339 14.9786 14.5554 14.9786C14.4458 14.9786 14.35 14.9513 14.268 14.8967C14.1861 14.842 14.126 14.764 14.0876 14.6628L13.4639 12.9301H10.1671L9.53286 14.6756C9.50203 14.7629 9.44686 14.8352 9.36736 14.8926C9.28786 14.9499 9.19591 14.9786 9.09152 14.9786C8.91468 14.9786 8.78263 14.9106 8.69539 14.7745C8.60814 14.6384 8.5939 14.4943 8.65266 14.3423ZM10.4678 12.0719H13.1609L11.8817 8.49181H11.8095L10.4678 12.0719Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_save.svg b/frontend/resources/flowy_icons/16x/ai_save.svg deleted file mode 100644 index ad228863b2..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_save.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3480_64260)"> -<path d="M1.21484 10.2617C1.21484 12.3939 1.21484 13.46 1.87722 14.1223C2.53966 14.7848 3.60572 14.7848 5.73791 14.7848H10.261C12.3932 14.7848 13.4593 14.7848 14.1217 14.1223C14.7841 13.46 14.7841 12.3938 14.7841 10.2617" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.99975 1.21582V11.0158M7.99975 11.0158L11.0151 7.71776M7.99975 11.0158L4.98438 7.71776" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_3480_64260"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_scroll_to_bottom.svg b/frontend/resources/flowy_icons/16x/ai_scroll_to_bottom.svg deleted file mode 100644 index e91acbda42..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_scroll_to_bottom.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 3.75V16.25M10 16.25L14.6875 11.5625M10 16.25L5.3125 11.5625" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_send_filled.svg b/frontend/resources/flowy_icons/16x/ai_send_filled.svg deleted file mode 100644 index 5f86dd3528..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_send_filled.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clip-path="url(#clip0)"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M15.35 8A7.35 7.35 0 1 1 0.65 8a7.35 7.35 0 0 1 14.7 0M8.472 3.906q1.054 1.053 2.107 2.107l0.723 0.723a0.666 0.666 0 0 1 -0.938 0.947l-1.696 -1.696v5.219a0.666 0.666 0 0 1 -1.334 0V5.986L5.643 7.678a0.666 0.666 0 0 1 -0.944 -0.942L7.53 3.904a0.666 0.666 0 0 1 0.942 0" fill="black"/> - </g> - <defs> - <clipPath id="clip0"> - <path width="16" height="16" d="M0 0H16V16H0V0z"/> - </clipPath> - </defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_source_drop_down.svg b/frontend/resources/flowy_icons/16x/ai_source_drop_down.svg deleted file mode 100644 index 5fe1dc47eb..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_source_drop_down.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.78805 4.37204C1.95077 4.20932 2.21459 4.20932 2.37731 4.37204L4.99935 6.99408L7.62139 4.37204C7.78411 4.20932 8.04792 4.20932 8.21064 4.37204C8.37336 4.53476 8.37336 4.79858 8.21064 4.96129L5.29398 7.87796C5.21584 7.9561 5.10986 8 4.99935 8C4.88884 8 4.78286 7.9561 4.70472 7.87796L1.78805 4.96129C1.62534 4.79858 1.62534 4.53476 1.78805 4.37204Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_sparks.svg b/frontend/resources/flowy_icons/16x/ai_sparks.svg deleted file mode 100644 index a419454f8e..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_sparks.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.5491 4.17206C13.3043 3.65812 12.892 3.24292 12.3798 2.99709C12.8922 2.75182 13.3049 2.33713 13.5502 1.82359C13.7949 2.33773 14.2073 2.7531 14.7196 2.99902C14.2072 3.24416 13.7945 3.6587 13.5491 4.17206ZM7.8553 2.67116C7.94884 2.44317 8.26264 2.44322 8.35611 2.67125L8.87245 3.93101C9.57472 5.64439 10.9224 7.00747 12.6208 7.71895L13.8686 8.24163C14.0945 8.33625 14.0944 8.66539 13.8685 8.75993L12.6215 9.28181C10.9213 9.99333 9.57229 11.3577 8.86997 13.0729L8.35566 14.3289C8.2622 14.5571 7.94808 14.557 7.8547 14.3288L7.33851 13.0669C6.6363 11.3504 5.28646 9.9849 3.58499 9.2732L2.34236 8.75342L2.14941 9.2147L2.34236 8.75342C2.1164 8.65891 2.11633 8.32969 2.34227 8.23509L3.59203 7.71183C5.29021 7.00081 6.63792 5.6384 7.34062 3.92565L7.8553 2.67116Z" stroke="#1F2329"/> -<circle cx="13.5825" cy="2.9375" r="0.5" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_star.svg b/frontend/resources/flowy_icons/16x/ai_star.svg index 336e160f6f..b98634fda1 100644 --- a/frontend/resources/flowy_icons/16x/ai_star.svg +++ b/frontend/resources/flowy_icons/16x/ai_star.svg @@ -1 +1,3 @@ -<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16" id="Ai-Chip-Spark--Streamline-Core-Remix.svg" height="16" width="16"><desc>Ai Chip Spark Streamline Icon: https://streamlinehq.com</desc><g id="Free Remix/Artificial Intelligence/ai-chip-spark--chip-processor-artificial-intelligence-ai"><path id="Union" fill="#000000" fill-rule="evenodd" d="M5.730194285714285 3.2857142857142856H3.80952c-0.2892914285714286 0 -0.5238057142857143 0.2345142857142857 -0.5238057142857143 0.5238057142857143v8.380994285714285c0 0.2892571428571428 0.2345142857142857 0.5237714285714286 0.5238057142857143 0.5237714285714286h8.380994285714285c0.2892571428571428 0 0.5237714285714286 -0.2345142857142857 0.5237714285714286 -0.5237714285714286V3.80952c0 -0.2892914285714286 -0.2345142857142857 -0.5238057142857143 -0.5237714285714286 -0.5238057142857143H5.730194285714285Zm5.253897142857142 -1.4285714285714284v-1.1428571428571428c0 -0.3944891428571428 -0.3197942857142857 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142 -0.39447999999999994 0 -0.7142857142857142 0.31979657142857143 -0.7142857142857142 0.7142857142857142v1.1428571428571428H6.4444799999999995v-1.1428571428571428c0 -0.3944891428571428 -0.3198057142857143 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142 -0.39449142857142855 0 -0.7142857142857142 0.31979657142857143 -0.7142857142857142 0.7142857142857142v1.1428571428571428H3.80952c-1.078262857142857 0 -1.9523771428571426 0.8741142857142857 -1.9523771428571426 1.9523771428571426v1.2063885714285714h-1.1428571428571428c-0.3944891428571428 0 -0.7142857142857142 0.3197942857142857 -0.7142857142857142 0.7142857142857142 0 0.39447999999999994 0.31979657142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142h1.1428571428571428v3.11104h-1.1428571428571428c-0.3944891428571428 0 -0.7142857142857142 0.3198057142857143 -0.7142857142857142 0.7142857142857142 0 0.39449142857142855 0.31979657142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142h1.1428571428571428v1.206422857142857c0 1.0782857142857143 0.8741142857142857 1.952342857142857 1.9523771428571426 1.952342857142857h1.2063885714285714v1.1428571428571428c0 0.3945142857142857 0.3197942857142857 0.7142857142857142 0.7142857142857142 0.7142857142857142 0.39447999999999994 0 0.7142857142857142 -0.31977142857142854 0.7142857142857142 -0.7142857142857142v-1.1428571428571428h3.11104v1.1428571428571428c0 0.3945142857142857 0.3198057142857143 0.7142857142857142 0.7142857142857142 0.7142857142857142 0.39449142857142855 0 0.7142857142857142 -0.31977142857142854 0.7142857142857142 -0.7142857142857142v-1.1428571428571428h1.206422857142857c1.0782857142857143 0 1.952342857142857 -0.8740571428571429 1.952342857142857 -1.952342857142857V10.984091428571427h1.1428571428571428c0.3945142857142857 0 0.7142857142857142 -0.3197942857142857 0.7142857142857142 -0.7142857142857142 0 -0.39447999999999994 -0.31977142857142854 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142h-1.1428571428571428V6.4444799999999995h1.1428571428571428c0.3945142857142857 0 0.7142857142857142 -0.3198057142857143 0.7142857142857142 -0.7142857142857142 0 -0.39449142857142855 -0.31977142857142854 -0.7142857142857142 -0.7142857142857142 -0.7142857142857142h-1.1428571428571428V3.80952c0 -1.078262857142857 -0.8740571428571429 -1.9523771428571426 -1.952342857142857 -1.9523771428571426H10.984091428571427ZM8.843817142857143 5.2543999999999995c-0.20528 -0.9151542857142857 -1.503062857142857 -0.9085371428571427 -1.70024 0.007394285714285714l-0.01904 0.08846857142857144c-0.20056 0.9316228571428571 -0.9331085714285714 1.6393714285714285 -1.8442742857142855 1.80056 -0.94512 0.16720000000000002 -0.9451085714285714 1.5311542857142857 0 1.6983542857142855 0.9111657142857142 0.16118857142857143 1.6437142857142857 0.8689371428571429 1.8442742857142855 1.8005714285714283l0.01904 0.08845714285714285c0.19717714285714283 0.9159657142857143 1.4949599999999998 0.9225942857142857 1.70024 0.007394285714285714l0.02312 -0.10308571428571428c0.20821714285714282 -0.9282742857142856 0.9417599999999999 -1.630925714285714 1.8515885714285711 -1.7918742857142855 0.9467314285714284 -0.1674857142857143 0.9467314285714284 -1.5337942857142857 0 -1.70128 -0.9098285714285714 -0.16094857142857144 -1.6433714285714285 -0.8636 -1.8515885714285711 -1.7918742857142855l-0.02312 -0.10308571428571428Z" clip-rule="evenodd" stroke-width="1"></path></g></svg> \ No newline at end of file +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.76392 5.78125L13.8889 7.5L9.76392 9.21875L7.88892 13L6.01392 9.21875L1.88892 7.5L6.01392 5.78125L7.88892 2L9.76392 5.78125Z" fill="#750D7E"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_stop_filled.svg b/frontend/resources/flowy_icons/16x/ai_stop_filled.svg deleted file mode 100644 index 33c0fe090c..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_stop_filled.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <g clip-path="url(#clip0_3584_82155)"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M15.35 8A7.35 7.35 0 1 1 0.65 8a7.35 7.35 0 0 1 14.7 0M5.628 5.47c-0.429 0.43 -0.429 1.12 -0.429 2.503 0 1.382 0 2.074 0.43 2.503s1.12 0.43 2.503 0.43c1.382 0 2.074 0 2.504 -0.43 0.429 -0.43 0.429 -1.121 0.429 -2.504 0 -1.382 0 -2.073 -0.43 -2.502s-1.12 -0.43 -2.503 -0.43c-1.382 0 -2.074 0 -2.504 0.43" fill="black"/> - </g> - <defs> - <clipPath id="clip0_3584_82155"> - <path width="16" height="16" fill="white" d="M0 0H16V16H0V0z"/> - </clipPath> - </defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_stream_stop.svg b/frontend/resources/flowy_icons/16x/ai_stream_stop.svg new file mode 100644 index 0000000000..55c7355ab7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/ai_stream_stop.svg @@ -0,0 +1 @@ +<svg width="64px" height="64px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.00024000000000000003" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: #e8e6e3;"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9 8C8.44772 8 8 8.44772 8 9V15C8 15.5523 8.44772 16 9 16H15C15.5523 16 16 15.5523 16 15V9C16 8.44772 15.5523 8 15 8H9ZM6 9C6 7.34315 7.34315 6 9 6H15C16.6569 6 18 7.34315 18 9V15C18 16.6569 16.6569 18 15 18H9C7.34315 18 6 16.6569 6 15V9Z" fill="#333333" style="--darkreader-inline-fill: #262a2b;" data-darkreader-inline-fill=""></path> </g></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/ai_summarize.svg b/frontend/resources/flowy_icons/16x/ai_summarize.svg deleted file mode 100644 index 761dae9dc0..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_summarize.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.39559 5.93492C5.54326 5.93492 5.67009 5.88314 5.77609 5.77959C5.88198 5.67592 5.93492 5.55026 5.93492 5.40259C5.93492 5.25492 5.88314 5.12809 5.77959 5.02209C5.67592 4.9162 5.55026 4.86326 5.40259 4.86326C5.25492 4.86326 5.12809 4.91503 5.02209 5.01859C4.9162 5.12226 4.86326 5.24792 4.86326 5.39559C4.86326 5.54326 4.91503 5.67009 5.01859 5.77609C5.12226 5.88198 5.24792 5.93492 5.39559 5.93492ZM5.39559 8.53492C5.54326 8.53492 5.67009 8.48314 5.77609 8.37959C5.88198 8.27592 5.93492 8.15026 5.93492 8.00259C5.93492 7.85492 5.88314 7.72809 5.77959 7.62209C5.67592 7.5162 5.55026 7.46325 5.40259 7.46325C5.25492 7.46325 5.12809 7.51503 5.02209 7.61859C4.9162 7.72226 4.86326 7.84792 4.86326 7.99559C4.86326 8.14326 4.91503 8.27009 5.01859 8.37609C5.12226 8.48198 5.24792 8.53492 5.39559 8.53492ZM5.39559 11.1349C5.54326 11.1349 5.67009 11.0831 5.77609 10.9796C5.88198 10.8759 5.93492 10.7503 5.93492 10.6026C5.93492 10.4549 5.88314 10.3281 5.77959 10.2221C5.67592 10.1162 5.55026 10.0633 5.40259 10.0633C5.25492 10.0633 5.12809 10.115 5.02209 10.2186C4.9162 10.3223 4.86326 10.4479 4.86326 10.5956C4.86326 10.7433 4.91503 10.8701 5.01859 10.9761C5.12226 11.082 5.24792 11.1349 5.39559 11.1349ZM3.80426 13.2658C3.50414 13.2658 3.25048 13.1621 3.04326 12.9549C2.83603 12.7477 2.73242 12.494 2.73242 12.1939V3.80426C2.73242 3.50414 2.83603 3.25048 3.04326 3.04326C3.25048 2.83603 3.50414 2.73242 3.80426 2.73242H9.80926C9.95026 2.73242 10.0859 2.75809 10.2163 2.80942C10.3467 2.86064 10.4652 2.93959 10.5718 3.04626L12.9519 5.42642C13.0586 5.53298 13.1375 5.65148 13.1888 5.78192C13.2401 5.91225 13.2658 6.04792 13.2658 6.18892V12.1939C13.2658 12.494 13.1621 12.7477 12.9549 12.9549C12.7477 13.1621 12.494 13.2658 12.1939 13.2658H3.80426ZM3.80426 12.3991H12.1939C12.2538 12.3991 12.303 12.3799 12.3414 12.3414C12.3799 12.303 12.3991 12.2538 12.3991 12.1939V6.39909H10.1349C9.98203 6.39909 9.85453 6.34798 9.75242 6.24576C9.6502 6.14364 9.59909 6.01614 9.59909 5.86326V3.59909H3.80426C3.74437 3.59909 3.6952 3.61831 3.65676 3.65676C3.61831 3.6952 3.59909 3.74437 3.59909 3.80426V12.1939C3.59909 12.2538 3.61831 12.303 3.65676 12.3414C3.6952 12.3799 3.74437 12.3991 3.80426 12.3991Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_summary.svg b/frontend/resources/flowy_icons/16x/ai_summary.svg index f3bfa1d9f3..2455874bc5 100644 --- a/frontend/resources/flowy_icons/16x/ai_summary.svg +++ b/frontend/resources/flowy_icons/16x/ai_summary.svg @@ -1,7 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.33334 1.33334V3.33334" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.6667 1.33334V3.33334" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.66666 8.66669H9.99999" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.66666 11.3333H7.99999" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.6667 2.33334C12.8867 2.45334 14 3.30001 14 6.43334V10.5533C14 13.3 13.3333 14.6733 10 14.6733H6C2.66667 14.6733 2 13.3 2 10.5533V6.43334C2 3.30001 3.11333 2.46001 5.33333 2.33334H10.6667Z" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9.44667 3.33333L12.6667 6.55333V12.6667H3.33333V3.33333H9.44667ZM9.44667 2H3.33333C2.6 2 2 2.6 2 3.33333V12.6667C2 13.4 2.6 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V6.55333C14 6.2 13.86 5.86 13.6067 5.61333L10.3867 2.39333C10.14 2.14 9.8 2 9.44667 2ZM4.66667 10H11.3333V11.3333H4.66667V10ZM4.66667 7.33333H11.3333V8.66667H4.66667V7.33333ZM4.66667 4.66667H9.33333V6H4.66667V4.66667Z" fill="#8F959E"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/ai_table.svg b/frontend/resources/flowy_icons/16x/ai_table.svg deleted file mode 100644 index 993e1d5764..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_table.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3551_49501)"> -<path d="M3.49719 2.21191H12.5009C12.5009 2.21191 13.7872 2.21191 13.7872 3.49816V12.5019C13.7872 12.5019 13.7872 13.7882 12.5009 13.7882H3.49719C3.49719 13.7882 2.21094 13.7882 2.21094 12.5019V3.49816C2.21094 3.49816 2.21094 2.21191 3.49719 2.21191Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.21094 8H13.7872" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 2.21191V13.7882" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_3551_49501"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_text.svg b/frontend/resources/flowy_icons/16x/ai_text.svg deleted file mode 100644 index 0198b267b8..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_text.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.375 4.25H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.375 8H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.375 11.75H13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_text_auto.svg b/frontend/resources/flowy_icons/16x/ai_text_auto.svg deleted file mode 100644 index b09fd45305..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_text_auto.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.0938 7.37305H3.09375" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -<path d="M10.0938 12.373H3.09375" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -<path d="M8.09375 17.373H3.09375" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -<path d="M11.41 17.0652C11.24 17.443 11.4084 17.887 11.7861 18.0569C12.1639 18.2269 12.6079 18.0585 12.7778 17.6808L11.41 17.0652ZM16.5939 7.37305L17.2778 7.06528C17.1567 6.79614 16.889 6.62305 16.5939 6.62305C16.2988 6.62305 16.0311 6.79614 15.91 7.06528L16.5939 7.37305ZM20.41 17.6808C20.5799 18.0585 21.0239 18.2269 21.4017 18.0569C21.7794 17.887 21.9478 17.443 21.7778 17.0652L20.41 17.6808ZM13.7303 12.9866C13.3161 12.9866 12.9803 13.3224 12.9803 13.7366C12.9803 14.1509 13.3161 14.4866 13.7303 14.4866V12.9866ZM12.7778 17.6808L17.2778 7.68082L15.91 7.06528L11.41 17.0652L12.7778 17.6808ZM21.7778 17.0652L20.1415 13.4289L18.7736 14.0444L20.41 17.6808L21.7778 17.0652ZM20.1415 13.4289L17.2778 7.06528L15.91 7.68082L18.7736 14.0444L20.1415 13.4289ZM19.4575 12.9866H13.7303V14.4866H19.4575V12.9866Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_text_image.svg b/frontend/resources/flowy_icons/16x/ai_text_image.svg deleted file mode 100644 index 320aad6bab..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_text_image.svg +++ /dev/null @@ -1,15 +0,0 @@ -<svg width="22" height="16" viewBox="0 0 22 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_4912_80370)"> -<path d="M1.49336 7.99973C1.49336 5.38094 1.49336 4.0716 2.30688 3.25803C3.12044 2.44452 4.42979 2.44452 7.04858 2.44452C9.66731 2.44452 10.9767 2.44452 11.7902 3.25803C12.6038 4.0716 12.6038 5.38094 12.6038 7.99973C12.6038 10.6185 12.6038 11.9279 11.7902 12.7414C10.9768 13.5549 9.66731 13.5549 7.04858 13.5549C4.42979 13.5549 3.12044 13.5549 2.30688 12.7414C1.49336 11.9279 1.49336 10.6185 1.49336 7.99973Z" stroke="black"/> -<path d="M8.16094 5.77811C8.15912 6.63337 9.08384 7.16992 9.82545 6.74387C10.1711 6.54526 10.3839 6.17674 10.383 5.77811C10.3849 4.9228 9.46013 4.3863 8.71852 4.81235C8.37285 5.01091 8.16008 5.37948 8.16094 5.77811Z" fill="black" stroke="black" stroke-width="0.2"/> -<path d="M1.49336 8.27756L2.46638 7.42616C2.97262 6.98325 3.73561 7.00864 4.21123 7.48425L6.59429 9.86732C6.97601 10.2491 7.57701 10.3011 8.01875 9.99066L8.1844 9.87427C8.82003 9.42755 9.68006 9.4793 10.2575 9.99904L12.0483 11.6107" stroke="black" stroke-linecap="round"/> -</g> -<path d="M21.4512 3.51453H14.5488" stroke="black" stroke-linecap="round"/> -<path d="M19.3805 8.00012H14.5488" stroke="black" stroke-linecap="round"/> -<path d="M18 12.4857H14.5488" stroke="black" stroke-linecap="round"/> -<defs> -<clipPath id="clip0_4912_80370"> -<rect width="13" height="13" fill="white" transform="translate(0.548828 1.5)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_translate.svg b/frontend/resources/flowy_icons/16x/ai_translate.svg index a32d7cd908..7e9706c4af 100644 --- a/frontend/resources/flowy_icons/16x/ai_translate.svg +++ b/frontend/resources/flowy_icons/16x/ai_translate.svg @@ -1,12 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.7067 12.4467L11.28 9.60004L9.85336 12.4467" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.1134 11.94H12.46" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.28 14.6667C9.41331 14.6667 7.89331 13.1533 7.89331 11.28C7.89331 9.41334 9.40665 7.89337 11.28 7.89337C13.1466 7.89337 14.6666 9.40668 14.6666 11.28C14.6666 13.1533 13.1533 14.6667 11.28 14.6667Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.34668 1.33334H5.96001C7.34001 1.33334 8.00668 2.00002 7.97335 3.34669V5.96001C8.00668 7.34001 7.34001 8.00669 5.96001 7.97336H3.34668C2.00001 8.00003 1.33334 7.33334 1.33334 5.95334V3.34002C1.33334 2.00002 2.00001 1.33334 3.34668 1.33334Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.00659 3.89999H3.29993" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.64661 3.44666V3.89998" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.32661 3.89334C5.32661 5.06001 4.41327 6.00666 3.29327 6.00666" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.00658 6.00668C5.51991 6.00668 5.07992 5.74668 4.77325 5.33334" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.33331 10C1.33331 12.58 3.41998 14.6667 5.99998 14.6667L5.29998 13.5" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.6666 6.00001C14.6666 3.42001 12.58 1.33334 9.99997 1.33334L10.7 2.50001" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.58008 10.0468L6.88675 8.3735L6.90675 8.3535C8.06675 7.06016 8.89341 5.5735 9.38008 4.00016H11.3334V2.66683H6.66675V1.3335H5.33341V2.66683H0.666748V3.9935H8.11341C7.66675 5.28016 6.96008 6.50016 6.00008 7.56683C5.38008 6.88016 4.86675 6.12683 4.46008 5.3335H3.12675C3.61341 6.42016 4.28008 7.44683 5.11341 8.3735L1.72008 11.7202L2.66675 12.6668L6.00008 9.3335L8.07341 11.4068L8.58008 10.0468ZM12.3334 6.66683H11.0001L8.00008 14.6668H9.33341L10.0801 12.6668H13.2467L14.0001 14.6668H15.3334L12.3334 6.66683ZM10.5867 11.3335L11.6667 8.44683L12.7467 11.3335H10.5867Z" fill="#750D7E"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/ai_try_again.svg b/frontend/resources/flowy_icons/16x/ai_try_again.svg deleted file mode 100644 index c3f51de34d..0000000000 --- a/frontend/resources/flowy_icons/16x/ai_try_again.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.2793 5.06004H10.5193C12.8389 5.06004 14.7193 6.94041 14.7193 9.26004C14.7193 11.5796 12.8389 13.46 10.5193 13.46H4.6393M1.2793 5.06004L3.7993 2.54004M1.2793 5.06004L3.7993 7.58004" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/app_logo.svg b/frontend/resources/flowy_icons/16x/app_logo.svg deleted file mode 100644 index 96af87f8ff..0000000000 --- a/frontend/resources/flowy_icons/16x/app_logo.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M28.992 17.8574C28.9896 17.8574 28.9875 17.8592 28.987 17.8615C28.1826 22.2013 24.675 26.0049 20.6109 28.137C20.1001 28.4039 19.9667 28.5512 19.3938 28.5972H27.566C27.7515 28.604 27.9364 28.5733 28.1098 28.507C28.2832 28.4407 28.4414 28.3401 28.575 28.2112C28.7086 28.0823 28.8148 27.9278 28.8873 27.757C28.9598 27.5861 28.9971 27.4024 28.997 27.2168V17.8624C28.997 17.8597 28.9948 17.8574 28.992 17.8574V17.8574Z" fill="#F7931E"/> -<path d="M20.0057 28.3418C19.8387 28.3533 19.6711 28.3533 19.5042 28.3418H20.0057Z" fill="#FFCE00"/> -<path d="M11.797 9.47252C11.6728 9.57605 11.5485 9.67038 11.422 9.76011C9.33523 11.228 2.96683 15.9859 1.52888 13.9382C0.11624 11.9274 1.60481 6.2239 5.27216 3.47684C5.34118 3.42163 5.4102 3.37101 5.48152 3.32269C9.45947 0.525018 12.4366 0.911539 13.8838 2.96148C15.2274 4.88488 13.7158 7.90343 11.797 9.47252Z" fill="#8427E0"/> -<path d="M27.3635 13.8847C25.394 15.2651 22.2858 13.6546 20.7443 11.6737C20.6822 11.5931 20.6224 11.5126 20.5648 11.4298C19.097 9.33614 14.3437 2.96774 16.3867 1.52749C18.4298 0.0872391 24.3196 1.65863 27.0022 5.48013C27.0621 5.56525 27.1196 5.64808 27.1748 5.73091C29.7838 9.58691 29.3674 12.4674 27.3635 13.8847Z" fill="#00B5FF"/> -<path d="M24.7268 26.4382C24.6578 26.4919 24.5888 26.5425 24.5198 26.59C20.5395 29.3877 17.5624 28.9989 16.1221 26.9512C14.7716 25.0278 16.2809 22.0162 18.1928 20.4402C18.317 20.3367 18.4436 20.24 18.5701 20.1503C20.6569 18.6848 27.0253 13.9384 28.4632 15.9745C29.8851 17.983 28.3965 23.6911 24.7268 26.4382Z" fill="#FFBD00"/> -<path d="M13.6143 28.3827C11.5644 29.8207 5.68145 28.247 2.99651 24.4278L2.83776 24.1977C0.214941 20.3302 0.629071 17.452 2.6376 16.0417C4.60472 14.6612 7.71299 16.2717 9.25447 18.2504C9.31659 18.3309 9.37641 18.4114 9.43623 18.4942C10.9041 20.5833 15.662 26.9517 13.6143 28.3827Z" fill="#E3006D"/> -<path d="M13.9434 6.64539C13.2413 8.20966 11.9657 9.44491 10.3796 10.0965C7.30357 11.3918 2.78725 13.0368 2.00961 11.2468C1.24807 9.48678 2.65611 5.90225 5.26973 3.47729L5.32495 3.43128C9.38342 0.513965 12.4181 0.886682 13.8836 2.96193C14.6084 4.00416 14.5002 5.37309 13.9434 6.64539Z" fill="#9327FF"/> -<path d="M27.3632 13.8805C26.2956 14.6305 24.8945 14.4994 23.59 13.9081C22.0248 13.1796 20.7922 11.8871 20.1389 10.2891C18.8666 7.2084 17.3136 2.83703 19.0668 2.0801C20.9074 1.28405 24.7565 2.86924 27.1745 5.72674C29.7835 9.58275 29.3671 12.4633 27.3632 13.8805Z" fill="#00C8FF"/> -<path d="M24.7266 26.4401L24.6805 26.4769C20.6198 29.3988 17.5805 29.0284 16.1219 26.9532C15.3902 25.911 15.4984 24.5489 16.0551 23.272C16.7565 21.7072 18.0324 20.4717 19.619 19.8209C22.6927 18.5233 27.2113 16.8806 27.989 18.6706C28.7666 20.4605 27.3425 24.006 24.7266 26.4401Z" fill="#FFCE00"/> -<path d="M10.934 27.8297C9.09342 28.6235 5.25812 27.0475 2.83776 24.1992C0.214941 20.3248 0.629071 17.4466 2.6376 16.0362C3.70283 15.2862 5.10397 15.415 6.40848 16.0063C7.97135 16.7389 9.19984 18.0348 9.84806 19.6345C11.1227 22.6922 12.6871 27.0682 10.934 27.8297Z" fill="#FB006D"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/calendar_layout.svg b/frontend/resources/flowy_icons/16x/calendar_layout.svg deleted file mode 100644 index eee5fca964..0000000000 --- a/frontend/resources/flowy_icons/16x/calendar_layout.svg +++ /dev/null @@ -1,19 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_45_2)"> -<path d="M1.16284 7.82906C1.16284 5.25056 1.16284 3.96137 1.96384 3.16031C2.7649 2.35931 4.05409 2.35931 6.63259 2.35931H9.36747C11.9459 2.35931 13.2352 2.35931 14.0362 3.16031C14.8372 3.96137 14.8372 5.25056 14.8372 7.82906V9.1965C14.8372 11.7749 14.8372 13.0642 14.0362 13.8652C13.2352 14.6662 11.9459 14.6663 9.36747 14.6663H6.63259C4.05409 14.6663 2.7649 14.6662 1.96384 13.8652C1.16284 13.0642 1.16284 11.7749 1.16284 9.1965V7.82906Z" stroke="black"/> -<path d="M4.58136 2.35931V1.33369" stroke="black" stroke-linecap="round"/> -<path d="M11.4186 2.35931V1.33369" stroke="black" stroke-linecap="round"/> -<path d="M1.50464 5.77787H14.4954" stroke="black" stroke-linecap="round"/> -<path d="M12.1023 11.2477C12.1023 11.6253 11.7962 11.9314 11.4186 11.9314C11.041 11.9314 10.7349 11.6253 10.7349 11.2477C10.7349 10.8701 11.041 10.5639 11.4186 10.5639C11.7962 10.5639 12.1023 10.8701 12.1023 11.2477Z" fill="black"/> -<path d="M12.1023 8.51281C12.1023 8.89044 11.7962 9.1965 11.4186 9.1965C11.041 9.1965 10.7349 8.89044 10.7349 8.51281C10.7349 8.13519 11.041 7.82906 11.4186 7.82906C11.7962 7.82906 12.1023 8.13519 12.1023 8.51281Z" fill="black"/> -<path d="M8.68372 11.2477C8.68372 11.6253 8.3776 11.9314 7.99997 11.9314C7.62235 11.9314 7.31628 11.6253 7.31628 11.2477C7.31628 10.8701 7.62235 10.5639 7.99997 10.5639C8.3776 10.5639 8.68372 10.8701 8.68372 11.2477Z" fill="black"/> -<path d="M8.68372 8.51281C8.68372 8.89044 8.3776 9.1965 7.99997 9.1965C7.62235 9.1965 7.31628 8.89044 7.31628 8.51281C7.31628 8.13519 7.62235 7.82906 7.99997 7.82906C8.3776 7.82906 8.68372 8.13519 8.68372 8.51281Z" fill="black"/> -<path d="M5.26514 11.2477C5.26514 11.6253 4.95902 11.9314 4.58139 11.9314C4.20377 11.9314 3.89771 11.6253 3.89771 11.2477C3.89771 10.8701 4.20383 10.5639 4.58139 10.5639C4.95896 10.5639 5.26514 10.8701 5.26514 11.2477Z" fill="black"/> -<path d="M5.26514 8.51281C5.26514 8.89044 4.95902 9.1965 4.58139 9.1965C4.20377 9.1965 3.89771 8.89044 3.89771 8.51281C3.89771 8.13519 4.20383 7.82906 4.58139 7.82906C4.95896 7.82906 5.26514 8.13519 5.26514 8.51281Z" fill="black"/> -</g> -<defs> -<clipPath id="clip0_45_2"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/camera.svg b/frontend/resources/flowy_icons/16x/camera.svg deleted file mode 100644 index ce3eda4fb9..0000000000 --- a/frontend/resources/flowy_icons/16x/camera.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Camera--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Camera Streamline Icon: https://streamlinehq.com</desc><path stroke="#000000" d="M5.625 8.125a1.875 1.875 0 1 0 3.75 0 1.875 1.875 0 1 0 -3.75 0" stroke-width="1"></path><path d="M6.1111125 13.125h2.7777625c1.9506875000000001 0 2.926 0 3.6266249999999998 -0.45962500000000006 0.30325 -0.199 0.5636875 -0.4546875 0.766375 -0.7524375 0.468125 -0.687875 0.468125 -1.6455 0.468125 -3.5606875 0 -1.915125 0 -2.87274375 -0.468125 -3.560625 -0.2026875 -0.2977875 -0.463125 -0.553475 -0.766375 -0.7524500000000001 -0.45025000000000004 -0.29534375 -1.0138125 -0.40090624999999996 -1.8767500000000001 -0.4386375 -0.4118125 0 -0.7663125 -0.3063625 -0.8470624999999999 -0.7028125000000001C9.6705 2.30305625 9.1386875 1.875 8.5210625 1.875h-2.042125c-0.61765625 0 -1.14946875 0.42805625 -1.2706062500000002 1.022725 -0.08075625 0.39644999999999997 -0.4353 0.7028125000000001 -0.84708125 0.7028125000000001 -0.8629187500000001 0.03773125 -1.42653125 0.14329375 -1.876725 0.4386375 -0.30330625 0.19897499999999999 -0.563725 0.45466249999999997 -0.7663875 0.7524500000000001C1.25 5.47950625 1.25 6.437125 1.25 8.35225c0 1.9151874999999998 0 2.8728124999999998 0.4681375 3.5606875 0.2026625 0.29775 0.46308125 0.5534375 0.7663875 0.7524375C3.18515 13.125 4.16046875 13.125 6.1111125 13.125Z" stroke="#000000" stroke-width="1"></path><path d="M11.875 6.25h-0.625" stroke="#000000" stroke-linecap="round" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/chat_at.svg b/frontend/resources/flowy_icons/16x/chat_at.svg deleted file mode 100644 index 2d4c8d507b..0000000000 --- a/frontend/resources/flowy_icons/16x/chat_at.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" id="At-Sign--Streamline-Lucide.svg" height="16" width="16"><desc>At Sign Streamline Icon: https://streamlinehq.com</desc><path d="M5 7.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0 -5 0" stroke-width="1"></path><path d="M10 5v3.125a1.875 1.875 0 0 0 3.75 0v-0.625a6.25 6.25 0 1 0 -2.5 5" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/check_circle_outlined.svg b/frontend/resources/flowy_icons/16x/check_circle_outlined.svg deleted file mode 100644 index 3677adcc96..0000000000 --- a/frontend/resources/flowy_icons/16x/check_circle_outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.31665 11.5668L12.0166 6.86683L11.0833 5.9335L7.31665 9.70016L5.41665 7.80016L4.48331 8.7335L7.31665 11.5668ZM8.24998 15.1668C7.32776 15.1668 6.46109 14.9918 5.64998 14.6418C4.83887 14.2918 4.13331 13.8168 3.53331 13.2168C2.93331 12.6168 2.45831 11.9113 2.10831 11.1002C1.75831 10.2891 1.58331 9.42238 1.58331 8.50016C1.58331 7.57794 1.75831 6.71127 2.10831 5.90016C2.45831 5.08905 2.93331 4.3835 3.53331 3.7835C4.13331 3.1835 4.83887 2.7085 5.64998 2.3585C6.46109 2.0085 7.32776 1.8335 8.24998 1.8335C9.1722 1.8335 10.0389 2.0085 10.85 2.3585C11.6611 2.7085 12.3666 3.1835 12.9666 3.7835C13.5666 4.3835 14.0416 5.08905 14.3916 5.90016C14.7416 6.71127 14.9166 7.57794 14.9166 8.50016C14.9166 9.42238 14.7416 10.2891 14.3916 11.1002C14.0416 11.9113 13.5666 12.6168 12.9666 13.2168C12.3666 13.8168 11.6611 14.2918 10.85 14.6418C10.0389 14.9918 9.1722 15.1668 8.24998 15.1668ZM8.24998 13.8335C9.73887 13.8335 11 13.3168 12.0333 12.2835C13.0666 11.2502 13.5833 9.98905 13.5833 8.50016C13.5833 7.01127 13.0666 5.75016 12.0333 4.71683C11 3.6835 9.73887 3.16683 8.24998 3.16683C6.76109 3.16683 5.49998 3.6835 4.46665 4.71683C3.43331 5.75016 2.91665 7.01127 2.91665 8.50016C2.91665 9.98905 3.43331 11.2502 4.46665 12.2835C5.49998 13.3168 6.76109 13.8335 8.24998 13.8335Z" fill="#653E8C"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/check_partial.svg b/frontend/resources/flowy_icons/16x/check_partial.svg deleted file mode 100644 index 528d630790..0000000000 --- a/frontend/resources/flowy_icons/16x/check_partial.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> - <rect x="2.5" y="2.5" width="11" height="11" rx="3.5" stroke="#BDBDBD" stroke-width="1"/> - <path d="M5.5 8H10.5" stroke="#00BCF0" stroke-width="1.5" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/checkbox.svg b/frontend/resources/flowy_icons/16x/checkbox.svg index 944381cd5f..37f52c47ed 100644 --- a/frontend/resources/flowy_icons/16x/checkbox.svg +++ b/frontend/resources/flowy_icons/16x/checkbox.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10.1601 14.6H5.84006C4.09924 14.6 3.03675 14.2481 2.39433 13.6057C1.7519 12.9633 1.40006 11.9008 1.40006 10.16V5.83999C1.40006 4.09917 1.7519 3.03668 2.39433 2.39425C3.03675 1.75183 4.09924 1.39999 5.84006 1.39999H10.1601C11.9009 1.39999 12.9634 1.75183 13.6058 2.39425C14.2482 3.03668 14.6001 4.09917 14.6001 5.83999V10.16C14.6001 11.9008 14.2482 12.9633 13.6058 13.6057C12.9634 14.2481 11.9009 14.6 10.1601 14.6Z" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.95006 8.14046L6.9452 10.1356L11.0501 6.20001" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/checkbox_ai_empty.svg b/frontend/resources/flowy_icons/16x/checkbox_ai_empty.svg deleted file mode 100644 index 9b6bb59b26..0000000000 --- a/frontend/resources/flowy_icons/16x/checkbox_ai_empty.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="2.8125" y="3.73499" width="12.375" height="12.375" rx="3.9375" fill="white" stroke="#BDBDBD" stroke-width="1.125"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/checkbox_ai_minus.svg b/frontend/resources/flowy_icons/16x/checkbox_ai_minus.svg deleted file mode 100644 index 4adc9445c1..0000000000 --- a/frontend/resources/flowy_icons/16x/checkbox_ai_minus.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="2.8125" y="3.64246" width="12.375" height="12.375" rx="3.9375" fill="white" stroke="#BDBDBD" stroke-width="1.125"/> -<path d="M6 9.82996H12" stroke="#00BCF0" stroke-width="2" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/checkbox_ai_selected.svg b/frontend/resources/flowy_icons/16x/checkbox_ai_selected.svg deleted file mode 100644 index 8f04722a89..0000000000 --- a/frontend/resources/flowy_icons/16x/checkbox_ai_selected.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="2.815" y="2.815" width="12.37" height="12.37" rx="3.935" fill="white" stroke="#BDBDBD" stroke-width="1.13"/> -<path d="M6.75 9L8.56731 10.6875L11.8125 7.3125" stroke="#00BCF0" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/checklist.svg b/frontend/resources/flowy_icons/16x/checklist.svg index 006825d3e2..3a88d236a1 100644 --- a/frontend/resources/flowy_icons/16x/checklist.svg +++ b/frontend/resources/flowy_icons/16x/checklist.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.16671 8C2.16671 4.77834 4.77838 2.16667 8.00004 2.16667C8.86971 2.16667 9.69343 2.3566 10.4333 2.69669C10.726 2.83124 11.0724 2.70302 11.207 2.41029C11.3415 2.11757 11.2133 1.77119 10.9206 1.63664C10.0309 1.22773 9.04129 1 8.00004 1C4.13405 1 1.00005 4.13401 1.00005 8C1.00005 11.866 4.13405 15 8.00004 15C11.866 15 15 11.866 15 8C15 7.67783 14.7389 7.41667 14.4167 7.41667C14.0945 7.41667 13.8334 7.67783 13.8334 8C13.8334 11.2217 11.2217 13.8333 8.00004 13.8333C4.77838 13.8333 2.16671 11.2217 2.16671 8Z" fill="#171717"/> -<path d="M14.8136 4.34413C15.0497 4.12491 15.0634 3.75582 14.8442 3.51974C14.625 3.28366 14.2559 3.26999 14.0198 3.4892L8.13466 8.95396L6.64697 7.57254C6.41089 7.35332 6.0418 7.36699 5.82258 7.60307C5.60336 7.83915 5.61703 8.20824 5.85311 8.42746L7.73773 10.1775C7.96154 10.3853 8.30778 10.3853 8.53159 10.1775L14.8136 4.34413Z" fill="#171717"/> +<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C8.81896 2.5 9.59612 2.679 10.2945 3" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/child_page.svg b/frontend/resources/flowy_icons/16x/child_page.svg deleted file mode 100644 index 40861287d4..0000000000 --- a/frontend/resources/flowy_icons/16x/child_page.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.6668 6.74406V10.0173C13.6668 13.2906 12.5335 14.5999 9.70016 14.5999H6.30016C3.46683 14.5999 2.3335 13.2906 2.3335 10.0173V6.0894C2.3335 2.81614 3.46683 1.50684 6.30016 1.50684H9.1335" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.6168 6.66559H11.3502C9.65016 6.66559 9.0835 6.00738 9.0835 4.03274V1.68617C9.0835 1.58764 9.20598 1.54218 9.27027 1.61685L13.6168 6.66559Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/close_error.svg b/frontend/resources/flowy_icons/16x/close_error.svg deleted file mode 100644 index 6e1321a6ec..0000000000 --- a/frontend/resources/flowy_icons/16x/close_error.svg +++ /dev/null @@ -1,9 +0,0 @@ - -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<mask id="mask0_4030_10552" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"> -<rect width="16" height="16" fill="#D9D9D9"/> -</mask> -<g mask="url(#mask0_4030_10552)"> -<path d="M4.26659 12.7942L3.20572 11.7333L6.93905 8.00001L3.20572 4.26668L4.26659 3.20581L7.99992 6.93914L11.7333 3.20581L12.7941 4.26668L9.06078 8.00001L12.7941 11.7333L11.7333 12.7942L7.99992 9.06088L4.26659 12.7942Z" fill="#900000"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/close_viewer.svg b/frontend/resources/flowy_icons/16x/close_viewer.svg deleted file mode 100644 index 4fa8061bae..0000000000 --- a/frontend/resources/flowy_icons/16x/close_viewer.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="11.6765" y="3.27295" width="1.48562" height="11.8849" rx="0.742808" transform="rotate(45 11.6765 3.27295)" fill="#333333"/> -<rect x="12.7271" y="11.6766" width="1.48562" height="11.8849" rx="0.742808" transform="rotate(135 12.7271 11.6766)" fill="#333333"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/copy.svg b/frontend/resources/flowy_icons/16x/copy.svg index 8830822589..f11048fd2f 100644 --- a/frontend/resources/flowy_icons/16x/copy.svg +++ b/frontend/resources/flowy_icons/16x/copy.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10.6654 8.60065V11.4007C10.6654 13.734 9.73203 14.6673 7.3987 14.6673H4.5987C2.26536 14.6673 1.33203 13.734 1.33203 11.4007V8.60065C1.33203 6.26732 2.26536 5.33398 4.5987 5.33398H7.3987C9.73203 5.33398 10.6654 6.26732 10.6654 8.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.6654 4.60065V7.40065C14.6654 9.73398 13.732 10.6673 11.3987 10.6673H10.6654V8.60065C10.6654 6.26732 9.73203 5.33398 7.3987 5.33398H5.33203V4.60065C5.33203 2.26732 6.26536 1.33398 8.5987 1.33398H11.3987C13.732 1.33398 14.6654 2.26732 14.6654 4.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.9743 6.33301H7.35889C6.79245 6.33301 6.33325 6.7922 6.33325 7.35865V11.974C6.33325 12.5405 6.79245 12.9997 7.35889 12.9997H11.9743C12.5407 12.9997 12.9999 12.5405 12.9999 11.974V7.35865C12.9999 6.7922 12.5407 6.33301 11.9743 6.33301Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/cover.svg b/frontend/resources/flowy_icons/16x/cover.svg deleted file mode 100644 index 84749fbc30..0000000000 --- a/frontend/resources/flowy_icons/16x/cover.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3228_21314)"> -<path d="M1.16284 7.99979C1.16284 4.77666 1.16284 3.16516 2.16409 2.16385C3.1654 1.1626 4.7769 1.1626 8.00003 1.1626C11.2231 1.1626 12.8347 1.1626 13.8359 2.16385C14.8372 3.16516 14.8372 4.77666 14.8372 7.99979C14.8372 11.2228 14.8372 12.8344 13.8359 13.8357C12.8347 14.837 11.2231 14.837 8.00003 14.837C4.7769 14.837 3.1654 14.837 2.16409 13.8357C1.16284 12.8345 1.16284 11.2228 1.16284 7.99979Z" stroke="#171717"/> -<path d="M9.36743 5.26499C9.36518 6.31762 10.5033 6.97799 11.4161 6.45362C11.8415 6.20918 12.1034 5.75562 12.1023 5.26499C12.1046 4.21231 10.9664 3.55199 10.0537 4.07637C9.62825 4.32074 9.36637 4.77437 9.36743 5.26499Z" stroke="#171717"/> -<path d="M1.16284 8.34172L2.3604 7.29385C2.98347 6.74872 3.92253 6.77997 4.5079 7.36535L7.4409 10.2983C7.91072 10.7682 8.6504 10.8323 9.19409 10.4502L9.39797 10.3069C10.1803 9.7571 11.2388 9.82079 11.9495 10.4605L14.1535 12.444" stroke="#171717" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_3228_21314"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/created_at.svg b/frontend/resources/flowy_icons/16x/created_at.svg new file mode 100644 index 0000000000..df94328071 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/created_at.svg @@ -0,0 +1,7 @@ +<svg width="35" height="34" viewBox="0 0 35 34" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M25.5 21.834C25.5 21.2817 25.9477 20.834 26.5 20.834C27.0523 20.834 27.5 21.2817 27.5 21.834V22.834H28.5C29.0523 22.834 29.5 23.2817 29.5 23.834C29.5 24.3863 29.0523 24.834 28.5 24.834H26.5C25.9477 24.834 25.5 24.3863 25.5 23.834V21.834Z" fill="#2B2F36"/> +<path d="M26.5 29.834C29.8137 29.834 32.5 27.1477 32.5 23.834C32.5 20.5203 29.8137 17.834 26.5 17.834C23.1863 17.834 20.5 20.5203 20.5 23.834C20.5 27.1477 23.1863 29.834 26.5 29.834ZM26.5 27.834C24.2909 27.834 22.5 26.0431 22.5 23.834C22.5 21.6248 24.2909 19.834 26.5 19.834C28.7091 19.834 30.5 21.6248 30.5 23.834C30.5 26.0431 28.7091 27.834 26.5 27.834Z" fill="#2B2F36"/> +<path d="M25.5 21.834C25.5 21.2817 25.9477 20.834 26.5 20.834C27.0523 20.834 27.5 21.2817 27.5 21.834V22.834H28.5C29.0523 22.834 29.5 23.2817 29.5 23.834C29.5 24.3863 29.0523 24.834 28.5 24.834H26.5C25.9477 24.834 25.5 24.3863 25.5 23.834V21.834Z" stroke="#FDEDA7" stroke-width="0.342857"/> +<path d="M26.5 29.834C29.8137 29.834 32.5 27.1477 32.5 23.834C32.5 20.5203 29.8137 17.834 26.5 17.834C23.1863 17.834 20.5 20.5203 20.5 23.834C20.5 27.1477 23.1863 29.834 26.5 29.834ZM26.5 27.834C24.2909 27.834 22.5 26.0431 22.5 23.834C22.5 21.6248 24.2909 19.834 26.5 19.834C28.7091 19.834 30.5 21.6248 30.5 23.834C30.5 26.0431 28.7091 27.834 26.5 27.834Z" stroke="#FDEDA7" stroke-width="0.342857"/> +<path d="M12.734 6.50065V6.60065H12.834H22.1673H22.2673V6.50065C22.2673 5.91155 22.7449 5.43398 23.334 5.43398C23.9231 5.43398 24.4007 5.91155 24.4007 6.50065V6.60065H24.5007H26.834C28.0675 6.60065 29.0673 7.60055 29.0673 8.83398V15.734H8.16732H8.06732V15.834V26.334V26.434H8.16732H18.734V28.5673H8.16732C6.93388 28.5673 5.93398 27.5674 5.93398 26.334V8.83399C5.93398 7.60055 6.93384 6.60065 8.16728 6.60065H10.5007H10.6007V6.50065C10.6007 5.91155 11.0782 5.43398 11.6673 5.43398C12.2564 5.43398 12.734 5.91155 12.734 6.50065ZM26.834 13.6007H26.934V13.5007V8.83398V8.73398H26.834H24.5007H24.4007V8.83398C24.4007 9.42309 23.9231 9.90065 23.334 9.90065C22.7449 9.90065 22.2673 9.42309 22.2673 8.83398V8.73398H22.1673H12.834H12.734V8.83398C12.734 9.42309 12.2564 9.90065 11.6673 9.90065C11.0782 9.90065 10.6007 9.42309 10.6007 8.83398V8.73398H10.5007H8.16732H8.06732V8.83398V13.5007V13.6007H8.16732H26.834Z" fill="#2B2F36" stroke="#FDEDA7" stroke-width="0.2"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/database_filter.svg b/frontend/resources/flowy_icons/16x/database_filter.svg deleted file mode 100644 index 8ca4fa3e51..0000000000 --- a/frontend/resources/flowy_icons/16x/database_filter.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.75 4.875H2.25" stroke="#666D76" stroke-linecap="round"/> -<path d="M12.875 8H4.125" stroke="#666D76" stroke-linecap="round"/> -<path d="M11 11.125H6" stroke="#666D76" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/database_fullscreen.svg b/frontend/resources/flowy_icons/16x/database_fullscreen.svg deleted file mode 100644 index 7e8794cbc5..0000000000 --- a/frontend/resources/flowy_icons/16x/database_fullscreen.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.13636 9.36328L3.5 12.9996M3.5 12.9996H6.54267M3.5 12.9996V9.957" stroke="#666D76" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.86328 6.63636L13.4996 3M13.4996 3H10.457M13.4996 3V6.04267" stroke="#666D76" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/database_layout.svg b/frontend/resources/flowy_icons/16x/database_layout.svg deleted file mode 100644 index 7b1fb9a846..0000000000 --- a/frontend/resources/flowy_icons/16x/database_layout.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_45_18)"> -<path d="M5 1V9H1V1H5ZM1 0C0.734784 0 0.48043 0.105357 0.292893 0.292893C0.105357 0.48043 0 0.734784 0 1L0 9C0 9.26522 0.105357 9.51957 0.292893 9.70711C0.48043 9.89464 0.734784 10 1 10H5C5.26522 10 5.51957 9.89464 5.70711 9.70711C5.89464 9.51957 6 9.26522 6 9V1C6 0.734784 5.89464 0.48043 5.70711 0.292893C5.51957 0.105357 5.26522 0 5 0L1 0ZM14 2V7H9V2H14ZM9 1C8.73478 1 8.48043 1.10536 8.29289 1.29289C8.10536 1.48043 8 1.73478 8 2V7C8 7.26522 8.10536 7.51957 8.29289 7.70711C8.48043 7.89464 8.73478 8 9 8H14C14.2652 8 14.5196 7.89464 14.7071 7.70711C14.8946 7.51957 15 7.26522 15 7V2C15 1.73478 14.8946 1.48043 14.7071 1.29289C14.5196 1.10536 14.2652 1 14 1H9ZM5 13V15H3V13H5ZM3 12C2.73478 12 2.48043 12.1054 2.29289 12.2929C2.10536 12.4804 2 12.7348 2 13V15C2 15.2652 2.10536 15.5196 2.29289 15.7071C2.48043 15.8946 2.73478 16 3 16H5C5.26522 16 5.51957 15.8946 5.70711 15.7071C5.89464 15.5196 6 15.2652 6 15V13C6 12.7348 5.89464 12.4804 5.70711 12.2929C5.51957 12.1054 5.26522 12 5 12H3ZM15 11V13H9V11H15ZM9 10C8.73478 10 8.48043 10.1054 8.29289 10.2929C8.10536 10.4804 8 10.7348 8 11V13C8 13.2652 8.10536 13.5196 8.29289 13.7071C8.48043 13.8946 8.73478 14 9 14H15C15.2652 14 15.5196 13.8946 15.7071 13.7071C15.8946 13.5196 16 13.2652 16 13V11C16 10.7348 15.8946 10.4804 15.7071 10.2929C15.5196 10.1054 15.2652 10 15 10H9Z" fill="black"/> -</g> -<defs> -<clipPath id="clip0_45_18"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> - diff --git a/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg b/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg deleted file mode 100644 index 00fe13b7fc..0000000000 --- a/frontend/resources/flowy_icons/16x/database_settings_arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z" fill="#8F959E"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/database_sort.svg b/frontend/resources/flowy_icons/16x/database_sort.svg deleted file mode 100644 index 3df5682f3a..0000000000 --- a/frontend/resources/flowy_icons/16x/database_sort.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.5 3.5V12.5M5.5 12.5L8.5 9.40625M5.5 12.5L2.5 9.40625" stroke="#666D76" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.5 12.5V3.5M11.5 3.5L14.5 6.59375M11.5 3.5L8.5 6.59375" stroke="#666D76" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/date.svg b/frontend/resources/flowy_icons/16x/date.svg index 9c19568379..78243f1e75 100644 --- a/frontend/resources/flowy_icons/16x/date.svg +++ b/frontend/resources/flowy_icons/16x/date.svg @@ -1,9 +1,6 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.00006 0.799988V2.89999" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.5999 0.799988V2.89999" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.8501 5.76297H13.7501" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.1 5.34995V11.2999C14.1 13.3999 13.05 14.7999 10.6 14.7999H5C2.55 14.7999 1.5 13.3999 1.5 11.2999V5.34995C1.5 3.24995 2.55 1.84995 5 1.84995H10.6C13.05 1.84995 14.1 3.24995 14.1 5.34995Z" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.0946 8.91612H5.10359" stroke="#171717" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.0946 11.0839H5.10359" stroke="#171717" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.69659 8.89999H7.70558" stroke="#171717" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.8889 3.5H4.11111C3.49746 3.5 3 3.94772 3 4.5V11.5C3 12.0523 3.49746 12.5 4.11111 12.5H11.8889C12.5025 12.5 13 12.0523 13 11.5V4.5C13 3.94772 12.5025 3.5 11.8889 3.5Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3 6.5H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/debug.svg b/frontend/resources/flowy_icons/16x/debug.svg deleted file mode 100644 index 643edfbd23..0000000000 --- a/frontend/resources/flowy_icons/16x/debug.svg +++ /dev/null @@ -1,20 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_51_8)"> -<path d="M12.7861 10.0512V7.95725C12.7861 6.47044 11.5807 5.26513 10.0939 5.26513H5.90618C4.4193 5.26513 3.21399 6.47044 3.21399 7.95725V10.0512C3.21399 12.6944 5.3568 14.8372 8.00005 14.8372C10.6433 14.8372 12.7861 12.6944 12.7861 10.0512Z" stroke="black"/> -<path d="M11.0767 5.607V4.92325C11.0767 3.224 9.69922 1.8465 7.99997 1.8465C6.30072 1.8465 4.92322 3.224 4.92322 4.92325V5.607" stroke="black"/> -<path d="M12.786 9.36744H14.8371" stroke="black" stroke-linecap="round"/> -<path d="M3.21397 9.36744H1.16284" stroke="black" stroke-linecap="round"/> -<path d="M9.70935 2.18837L11.4187 1.16281" stroke="black" stroke-linecap="round"/> -<path d="M6.29073 2.18837L4.58142 1.16281" stroke="black" stroke-linecap="round"/> -<path d="M13.8117 13.4699L12.4442 12.9229" stroke="black" stroke-linecap="round"/> -<path d="M13.8117 5.265L12.4442 5.81194" stroke="black" stroke-linecap="round"/> -<path d="M2.18835 13.4699L3.55579 12.9229" stroke="black" stroke-linecap="round"/> -<path d="M2.18835 5.265L3.55579 5.81194" stroke="black" stroke-linecap="round"/> -<path d="M8 14.4954V10.0512" stroke="black" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_51_8"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/download.svg b/frontend/resources/flowy_icons/16x/download.svg deleted file mode 100644 index c753fba274..0000000000 --- a/frontend/resources/flowy_icons/16x/download.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" id="Download--Streamline-Lucide.svg" height="16" width="16"><desc>Download Streamline Icon: https://streamlinehq.com</desc><path d="M13.125 9.375v2.5a1.25 1.25 0 0 1 -1.25 1.25H3.125a1.25 1.25 0 0 1 -1.25 -1.25v-2.5" stroke-width="1"></path><path d="m4.375 6.25 3.125 3.125 3.125 -3.125" stroke-width="1"></path><path d="m7.5 9.375 0 -7.5" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/download_success.svg b/frontend/resources/flowy_icons/16x/download_success.svg deleted file mode 100644 index d7cbd38d82..0000000000 --- a/frontend/resources/flowy_icons/16x/download_success.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="23" height="22" viewBox="0 0 23 22" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M15.7072 6.94837L9.66634 12.9892L6.37551 9.70754L5.08301 11L9.66634 15.5834L16.9997 8.25004L15.7072 6.94837ZM11.4997 1.83337C6.43967 1.83337 2.33301 5.94004 2.33301 11C2.33301 16.06 6.43967 20.1667 11.4997 20.1667C16.5597 20.1667 20.6663 16.06 20.6663 11C20.6663 5.94004 16.5597 1.83337 11.4997 1.83337ZM11.4997 18.3334C7.44801 18.3334 4.16634 15.0517 4.16634 11C4.16634 6.94837 7.44801 3.66671 11.4997 3.66671C15.5513 3.66671 18.833 6.94837 18.833 11C18.833 15.0517 15.5513 18.3334 11.4997 18.3334Z" fill="#2E7D32"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/download_warn.svg b/frontend/resources/flowy_icons/16x/download_warn.svg deleted file mode 100644 index 8f4f3810af..0000000000 --- a/frontend/resources/flowy_icons/16x/download_warn.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<mask id="mask0_2118_4891" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="17"> -<rect y="0.5" width="16" height="16" fill="#D9D9D9"/> -</mask> -<g mask="url(#mask0_2118_4891)"> -<path d="M0.666504 14.5L7.99984 1.83337L15.3332 14.5H0.666504ZM2.9665 13.1667H13.0332L7.99984 4.50004L2.9665 13.1667ZM7.99984 12.5C8.18873 12.5 8.34706 12.4362 8.47484 12.3084C8.60262 12.1806 8.6665 12.0223 8.6665 11.8334C8.6665 11.6445 8.60262 11.4862 8.47484 11.3584C8.34706 11.2306 8.18873 11.1667 7.99984 11.1667C7.81095 11.1667 7.65262 11.2306 7.52484 11.3584C7.39706 11.4862 7.33317 11.6445 7.33317 11.8334C7.33317 12.0223 7.39706 12.1806 7.52484 12.3084C7.65262 12.4362 7.81095 12.5 7.99984 12.5ZM7.33317 10.5H8.6665V7.16671H7.33317V10.5Z" fill="#1C1B1F"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/edit_layout.svg b/frontend/resources/flowy_icons/16x/edit_layout.svg deleted file mode 100644 index a2f875742e..0000000000 --- a/frontend/resources/flowy_icons/16x/edit_layout.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M215.74-528Q186-528 165-549.5 144-571 144-600v-144q0-29.7 21.18-50.85Q186.35-816 216.09-816h144.17Q390-816 411-794.85q21 21.15 21 50.85v144q0 29-21.18 50.5-21.17 21.5-50.91 21.5H215.74Zm0 384Q186-144 165-165.18q-21-21.17-21-50.91v-144.17Q144-390 165.18-411q21.17-21 50.91-21h144.17Q390-432 411-410.82q21 21.17 21 50.91v144.17Q432-186 410.82-165q-21.17 21-50.91 21H215.74ZM600-528q-29 0-50.5-21.5T528-600v-144q0-29.7 21.5-50.85Q571-816 600-816h144q29.7 0 50.85 21.15Q816-773.7 816-744v144q0 29-21.15 50.5T744-528H600Zm0 384q-29 0-50.5-21.18-21.5-21.17-21.5-50.91v-144.17Q528-390 549.5-411q21.5-21 50.5-21h144q29.7 0 50.85 21.18Q816-389.65 816-359.91v144.17Q816-186 794.85-165 773.7-144 744-144H600ZM216-600h144v-144H216v144Zm384 0h144v-144H600v144Zm0 384h144v-144H600v144Zm-384 0h144v-144H216v144Zm384-384Zm0 240Zm-240 0Zm0-240Z"/></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/export_html.svg b/frontend/resources/flowy_icons/16x/export_html.svg deleted file mode 100644 index 02ae35a2b4..0000000000 --- a/frontend/resources/flowy_icons/16x/export_html.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.73332 1.33337H13.2667C13.6667 1.33337 14 1.66671 13.9333 2.06671L12.7333 12.8667C12.7333 13.1334 12.5333 13.3334 12.2667 13.4667L8.19998 14.6C8.06665 14.6667 7.93332 14.6667 7.86665 14.6L3.79998 13.4667C3.53332 13.4 3.33332 13.2 3.33332 12.8667L2.06665 2.06671C2.06665 1.66671 2.33332 1.33337 2.73332 1.33337Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.8 4.53333H5.19995L5.46662 7.46666H10.5333L10.1333 10.8L7.86662 11.4667L5.46662 10.8V9.46666" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/export_markdown.svg b/frontend/resources/flowy_icons/16x/export_markdown.svg deleted file mode 100644 index a40e5baa26..0000000000 --- a/frontend/resources/flowy_icons/16x/export_markdown.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.4071 4.26416L10.593 1.45009C10.0304 0.887482 9.26736 0.571411 8.47171 0.571411H3.71436C2.60979 0.571411 1.71436 1.46684 1.71436 2.57141V13.0938C1.71436 14.1984 2.60978 15.0938 3.71435 15.0938H12.2858C13.3904 15.0938 14.2858 14.1984 14.2858 13.0938V6.38548C14.2858 5.58983 13.9697 4.82677 13.4071 4.26416Z" stroke="#171717" stroke-width="0.995556" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.19995 12V7.70621C4.19995 7.55289 4.32425 7.42859 4.47757 7.42859C4.58683 7.42859 4.68593 7.49267 4.73075 7.5923L5.59171 9.50585C5.64879 9.63271 5.77497 9.7143 5.91409 9.7143C6.05319 9.7143 6.17936 9.63273 6.23644 9.50588L7.09768 7.59228C7.14252 7.49266 7.24161 7.42859 7.35085 7.42859C7.50419 7.42859 7.6285 7.55289 7.6285 7.70622V12" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.8572 7.42859V11.9999" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.5 10.8572L10.8 12.1572L12.1 10.8572" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_pin.svg b/frontend/resources/flowy_icons/16x/favorite_pin.svg index 49ec94354a..fa4065cd0a 100644 --- a/frontend/resources/flowy_icons/16x/favorite_pin.svg +++ b/frontend/resources/flowy_icons/16x/favorite_pin.svg @@ -1,3 +1,3 @@ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.64912 1.71521C2.64912 1.43942 2.84766 1.2002 3.16 1.2002H8.855C9.125 1.2002 9.36707 1.40199 9.36707 1.71021C9.36707 1.89847 9.20994 2.20021 8.855 2.20021L8.01019 2.20277L8.00984 4.46598C8.00984 4.53315 8.05692 4.59785 8.10288 4.64382L10.1752 7.02856C10.223 7.07629 10.25 7.14117 10.25 7.20834L10.2495 7.61027C10.25 7.89027 10.025 8.11628 9.75 8.11628L6.51011 8.11753L6.51 11.2002C6.50999 11.4763 6.28614 11.7002 6.01 11.7002H5.99949C5.72355 11.7002 5.49977 11.4761 5.49949 11.2002L5.49613 8.11681L2.255 8.11631C1.96831 8.11631 1.75 7.89027 1.75 7.61027L1.75046 7.20898C1.75048 7.14179 1.77659 7.07678 1.82409 7.02928L3.90196 4.64452C3.94951 4.59697 4.00575 4.53192 4.00575 4.46474L4.00593 2.20171L3.16 2.20021C2.89258 2.20171 2.64912 1.9824 2.64912 1.71521Z" fill="#F65B6E"/> +<path d="M2.64912 1.71521C2.64912 1.43942 2.84766 1.2002 3.16 1.2002H8.855C9.125 1.2002 9.36707 1.40199 9.36707 1.71021C9.36707 1.89847 9.20994 2.20021 8.855 2.20021L8.01019 2.20277L8.00984 4.46598C8.00984 4.53315 8.05692 4.59785 8.10288 4.64382L10.1752 7.02856C10.223 7.07629 10.25 7.14117 10.25 7.20834L10.2495 7.61027C10.25 7.89027 10.025 8.11628 9.75 8.11628L6.51011 8.11753L6.51 11.2002C6.50999 11.4763 6.28614 11.7002 6.01 11.7002H5.99949C5.72355 11.7002 5.49977 11.4761 5.49949 11.2002L5.49613 8.11681L2.255 8.11631C1.96831 8.11631 1.75 7.89027 1.75 7.61027L1.75046 7.20898C1.75048 7.14179 1.77659 7.07678 1.82409 7.02928L3.90196 4.64452C3.94951 4.59697 4.00575 4.53192 4.00575 4.46474L4.00593 2.20171L3.16 2.20021C2.89258 2.20171 2.64912 1.9824 2.64912 1.71521Z" fill="#4DA594"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_section_pin.svg b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg index 32b2466de4..0402120e41 100644 --- a/frontend/resources/flowy_icons/16x/favorite_section_pin.svg +++ b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg @@ -1,5 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g opacity="0.5"> -<path d="M4.21594 1C3.79948 1 3.53477 1.31897 3.53477 1.68668C3.53477 2.04293 3.85938 2.33534 4.21594 2.33333L5.34384 2.33534L5.3436 5.35268C5.3436 5.44225 5.26862 5.52899 5.20522 5.59239L2.43472 8.77204C2.37139 8.83537 2.33657 8.92204 2.33655 9.01163L2.33594 9.54668C2.33594 9.92001 2.62702 10.2214 3.00927 10.2214L7.33078 10.2221L7.33526 14.4C7.33563 14.7679 7.634 15 8.00193 15H8.01594C8.38412 15 8.68259 14.7682 8.6826 14.4L8.68275 10.223L13.0026 10.2213C13.3693 10.2213 13.6693 9.92001 13.6686 9.54668L13.6693 9.01078C13.6693 8.92122 13.6332 8.83471 13.5696 8.77107L10.8064 5.59145C10.7452 5.53017 10.6824 5.4439 10.6824 5.35433L10.6829 2.33676L11.8093 2.33334C12.2825 2.33334 12.492 1.93102 12.492 1.68001C12.492 1.26906 12.1693 1.00001 11.8093 1H4.21594ZM6.70711 2.33483L9.37687 2.33333L9.38048 5.94579L11.9314 8.89026L4.13249 8.88794L6.70923 5.93339L6.70711 2.33483Z" fill="#2B2F36"/> +<path d="M8.38274 2.1912C8.61919 1.95467 8.99451 1.91977 9.2623 2.18766L14.145 7.07205C14.3765 7.30363 14.411 7.68431 14.1468 7.94865C13.9854 8.11011 13.5919 8.23414 13.2876 7.92972L12.5611 7.20735L10.6204 9.1481C10.5628 9.20571 10.5477 9.30158 10.5477 9.38042L10.2799 13.2031C10.2799 13.285 10.2475 13.3638 10.1899 13.4214L9.84485 13.7657C9.60524 14.0063 9.21857 14.0071 8.98279 13.7713L6.20394 10.9936L3.37596 13.8224C3.1392 14.0592 2.75536 14.0592 2.51861 13.8224L2.5096 13.8134C2.27301 13.5767 2.27282 13.193 2.50916 12.9561L5.33522 10.1233L2.5568 7.34311C2.31101 7.09723 2.31763 6.71613 2.55769 6.47598L2.90214 6.13221C2.95976 6.07459 3.03787 6.04124 3.11932 6.04124L6.94541 5.77806C7.02695 5.77806 7.13094 5.77049 7.18854 5.71288L9.12891 3.77213L8.40493 3.04532C8.17437 2.81725 8.15367 2.42036 8.38274 2.1912Z" fill="#171717"/> </g> </svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg index 999adfa03b..3e72f90f4b 100644 --- a/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg +++ b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg @@ -1,7 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <g opacity="0.5"> -<path d="M2.42691 8.77204L4.58617 6.2939L5.54809 7.25582L4.12467 8.88794L7.18111 8.88885L8.67493 10.3827L8.67479 14.6208C8.67478 14.989 8.37631 15.2875 8.00813 15.2875H7.99411C7.62619 15.2875 7.32782 14.9894 7.32745 14.6215L7.32297 10.2221L3.00146 10.2214C2.61921 10.2214 2.32812 9.92001 2.32812 9.54668L2.32874 9.01163C2.32876 8.92204 2.36357 8.83537 2.42691 8.77204Z" fill="#171717"/> -<path d="M11.9236 8.89026L11.9012 8.89025L13.1997 10.1888C13.4669 10.1012 13.6613 9.8473 13.6608 9.54668L13.6615 9.01078C13.6615 8.92122 13.6254 8.83471 13.5618 8.77107L10.7986 5.59145C10.7374 5.53017 10.6746 5.4439 10.6746 5.35433L10.675 2.33675L11.8015 2.33334C12.2747 2.33334 12.4842 1.93102 12.4842 1.68001C12.4842 1.26906 12.1615 1.00001 11.8015 1H4.20813C4.14572 1 4.08673 1.00716 4.03152 1.02058L6.70009 3.68915L6.6993 2.33483L9.36906 2.33333L9.37267 5.94579L11.9236 8.89026Z" fill="#171717"/> +<path d="M9.2623 2.18766C8.99451 1.91977 8.61919 1.95467 8.38274 2.1912C8.15367 2.42036 8.17437 2.81725 8.40493 3.04532L9.12891 3.77213L7.18854 5.71288C7.13094 5.77049 7.02695 5.77806 6.94541 5.77806L3.11932 6.04124C3.03787 6.04124 2.95976 6.07459 2.90214 6.13221L2.55769 6.47598C2.31763 6.71613 2.31101 7.09723 2.5568 7.34311L5.33522 10.1233L2.50916 12.9561C2.27282 13.193 2.27301 13.5767 2.5096 13.8134L2.51861 13.8224C2.75536 14.0592 3.1392 14.0592 3.37596 13.8224L6.20394 10.9936L8.98279 13.7713C9.21857 14.0071 9.60524 14.0063 9.84485 13.7657L10.1899 13.4214C10.2475 13.3638 10.2799 13.285 10.2799 13.2031L10.5477 9.38042C10.5477 9.30159 10.5628 9.20571 10.6204 9.1481L12.5611 7.20735L13.2876 7.92972C13.5919 8.23414 13.9854 8.11011 14.1468 7.94865C14.411 7.68431 14.3765 7.30363 14.145 7.07205L9.2623 2.18766ZM10.0059 4.64872L11.7235 6.36508L9.40296 8.69111L9.14988 12.226L4.1365 7.20788L7.69325 6.96485L10.0059 4.64872Z" fill="#171717"/> </g> -<path opacity="0.5" d="M1.79688 1.2002L14.1712 13.5746" stroke="#171717" stroke-width="1.2" stroke-linecap="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/file.svg b/frontend/resources/flowy_icons/16x/file.svg deleted file mode 100644 index acf1094ee8..0000000000 --- a/frontend/resources/flowy_icons/16x/file.svg +++ /dev/null @@ -1,2 +0,0 @@ - -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Folder-2--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Folder 2 Streamline Icon: https://streamlinehq.com</desc><path d="M13.75 6.875 1.25 6.875" stroke="#000000" stroke-linecap="round" stroke-width="1"></path><path d="M1.25 4.34359375c0 -0.55158125 0 -0.8273750000000001 0.04334375 -1.0571 0.19080625 -1.0112999999999999 0.9818499999999999 -1.80234375 1.99315 -1.99315C3.5162187499999997 1.25 3.7920125 1.25 4.34359375 1.25c0.24166875000000002 0 0.36250625000000003 0 0.47863749999999994 0.0108625 0.500675 0.046818750000000006 0.97559375 0.24353750000000002 1.36273125 0.56445625 0.0897875 0.0744375 0.175225 0.1598875 0.3461 0.33077500000000004L6.875 2.5c0.509875 0.5098625 0.7648125 0.76479375 1.0700625 0.93464375 0.1676875 0.0933 0.345625 0.16698125000000003 0.5301875 0.21959375C8.811187499999999 3.75 9.171687499999999 3.75 9.89275 3.75h0.23356249999999998c1.64525 0 2.4678125 0 3.0025625 0.4809125 0.0491875 0.0442375 0.09599999999999999 0.09105 0.1401875 0.14023125C13.75 4.90584375 13.75 5.7284625 13.75 7.3736875V8.75c0 2.3569999999999998 0 3.5355625 -0.73225 4.26775C12.285562500000001 13.75 11.107 13.75 8.75 13.75h-2.5c-2.357025 0 -3.53553125 0 -4.26776875 -0.73225C1.25 12.285562500000001 1.25 11.107 1.25 8.75V4.34359375Z" stroke="#000000" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/file_upload.svg b/frontend/resources/flowy_icons/16x/file_upload.svg deleted file mode 100644 index 7df55f8984..0000000000 --- a/frontend/resources/flowy_icons/16x/file_upload.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_54_39)"> -<path d="M9.35439 14.2642H6.64556V15.28H9.35439V14.2642ZM1.73577 9.35441V6.64558H0.719971V9.35441H1.73577ZM14.2641 9.05844V9.35441H15.2799V9.05844H14.2641ZM9.95789 2.99623L12.6388 5.40903L13.3183 4.65398L10.6375 2.24118L9.95789 2.99623ZM15.2799 9.05844C15.2799 7.915 15.2901 7.19109 15.0016 6.54332L14.0738 6.95659C14.2539 7.36108 14.2641 7.8253 14.2641 9.05844H15.2799ZM12.6388 5.40903C13.5554 6.23392 13.8936 6.55211 14.0738 6.95659L15.0016 6.54332C14.7132 5.89554 14.1682 5.41888 13.3183 4.65398L12.6388 5.40903ZM6.66574 1.73585C7.73694 1.73585 8.14118 1.74372 8.5014 1.88195L8.86534 0.933568C8.28851 0.712197 7.65999 0.720059 6.66574 0.720059V1.73585ZM10.6375 2.24118C9.90199 1.57923 9.44223 1.15488 8.86534 0.933568L8.5014 1.88195C8.86181 2.02024 9.16576 2.28334 9.95789 2.99623L10.6375 2.24118ZM6.64556 14.2642C5.35422 14.2642 4.43686 14.2631 3.74086 14.1695C3.05954 14.0779 2.667 13.9061 2.38044 13.6195L1.6621 14.3379C2.16891 14.8446 2.81155 15.0695 3.60554 15.1763C4.38492 15.2811 5.38295 15.28 6.64556 15.28V14.2642ZM0.719971 9.35441C0.719971 10.617 0.718919 11.615 0.823662 12.3944C0.930447 13.1884 1.15529 13.8311 1.6621 14.3379L2.38044 13.6195C2.09382 13.333 1.92204 12.9404 1.83042 12.2591C1.73688 11.5631 1.73577 10.6457 1.73577 9.35441H0.719971ZM9.35439 15.28C10.6169 15.28 11.615 15.2811 12.3943 15.1763C13.1883 15.0695 13.831 14.8446 14.3378 14.3379L13.6195 13.6195C13.3329 13.9061 12.9403 14.0779 12.259 14.1695C11.5631 14.2631 10.6457 14.2642 9.35439 14.2642V15.28ZM14.2641 9.35441C14.2641 10.6457 14.2631 11.5631 14.1695 12.2591C14.0779 12.9404 13.9061 13.333 13.6195 13.6195L14.3378 14.3379C14.8445 13.8311 15.0694 13.1884 15.1762 12.3944C15.281 11.615 15.2799 10.617 15.2799 9.35441H14.2641ZM1.73577 6.64558C1.73577 5.35431 1.73688 4.43688 1.83042 3.74095C1.92204 3.05963 2.09382 2.66709 2.38044 2.38047L1.6621 1.66219C1.15535 2.169 0.930447 2.81164 0.823662 3.60563C0.718919 4.38494 0.719971 5.38297 0.719971 6.64558H1.73577ZM6.66574 0.720059C5.39632 0.720059 4.3934 0.718944 3.61086 0.823687C2.81409 0.930349 2.16929 1.155 1.6621 1.66219L2.38044 2.38047C2.66663 2.09428 3.06034 1.92225 3.74563 1.83051C4.44509 1.73691 5.36772 1.73585 6.66574 1.73585V0.720059Z" fill="black"/> -<path d="M8.67712 1.56655V3.25958C8.67712 4.85574 8.67712 5.65387 9.17298 6.14973C9.6689 6.64559 10.467 6.64559 12.0632 6.64559H14.772" stroke="black"/> -<path d="M5.62968 12.4019V9.01585M5.62968 9.01585L4.27527 10.2856M5.62968 9.01585L6.9841 10.2856" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_54_39"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ai_chat_logo.svg b/frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/ai_chat_logo.svg rename to frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg diff --git a/frontend/resources/flowy_icons/16x/ft_archive.svg b/frontend/resources/flowy_icons/16x/ft_archive.svg deleted file mode 100644 index 64f1b3b039..0000000000 --- a/frontend/resources/flowy_icons/16x/ft_archive.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3449_43627)"> -<path d="M15.1394 7.30103H1.16113" stroke="#666D76" stroke-linecap="round"/> -<path d="M1.16113 4.47032C1.16113 3.85347 1.16113 3.54508 1.20956 3.28818C1.42295 2.15729 2.30755 1.27268 3.43845 1.05929C3.69535 1.01086 4.00374 1.01086 4.62059 1.01086C4.89084 1.01086 5.02596 1.01086 5.15579 1.023C5.71571 1.07533 6.24675 1.2953 6.67966 1.65423C6.7801 1.73741 6.87561 1.83299 7.0667 2.02408L7.45131 2.40869C8.02152 2.97883 8.30659 3.26391 8.64795 3.45385C8.83547 3.55818 9.03448 3.64059 9.24084 3.69944C9.61651 3.80651 10.0196 3.80651 10.826 3.80651H11.0872C12.927 3.80651 13.8468 3.80651 14.4448 4.34427C14.4998 4.39378 14.5522 4.44611 14.6016 4.50111C15.1394 5.09905 15.1394 6.01892 15.1394 7.85873V9.39781C15.1394 12.0336 15.1394 13.3515 14.3206 14.1703C13.5018 14.9891 12.1838 14.9891 9.54808 14.9891H6.75243C4.11663 14.9891 2.7988 14.9891 1.97993 14.1703C1.16113 13.3515 1.16113 12.0336 1.16113 9.39781V4.47032Z" stroke="#666D76"/> -</g> -<defs> -<clipPath id="clip0_3449_43627"> -<rect width="16" height="16" fill="white" transform="translate(0.150391)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ft_audio.svg b/frontend/resources/flowy_icons/16x/ft_audio.svg deleted file mode 100644 index a7d541d7d0..0000000000 --- a/frontend/resources/flowy_icons/16x/ft_audio.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.20011 11.9042C8.20011 13.5068 6.90092 14.8059 5.29836 14.8059C3.69579 14.8059 2.39661 13.5068 2.39661 11.9042C2.39661 10.3016 3.69579 9.00244 5.29836 9.00244C6.90092 9.00244 8.20011 10.3016 8.20011 11.9042Z" stroke="#666D76"/> -<path d="M8.20007 11.904V3.19873" stroke="#666D76"/> -<path d="M11.1864 6.14349L9.27614 5.1883C9.01139 5.05599 8.87901 4.9898 8.7697 4.90618C8.48657 4.68968 8.29295 4.37649 8.22595 4.02637C8.20007 3.89124 8.20007 3.74324 8.20007 3.4473C8.20007 2.7428 8.20007 2.39055 8.28682 2.15112C8.51607 1.51824 9.15045 1.12618 9.81901 1.20418C10.072 1.23368 10.3871 1.39118 11.0171 1.70624L12.9275 2.66143C13.1923 2.7938 13.3246 2.85999 13.4339 2.94355C13.7171 3.16005 13.9107 3.47324 13.9776 3.82337C14.0036 3.95855 14.0036 4.10649 14.0036 4.40249C14.0036 5.10693 14.0036 5.45918 13.9168 5.69862C13.6876 6.33149 13.0531 6.72355 12.3846 6.64562C12.1316 6.61605 11.8166 6.45855 11.1864 6.14349Z" stroke="#666D76" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ft_link.svg b/frontend/resources/flowy_icons/16x/ft_link.svg deleted file mode 100644 index f8dd8460b9..0000000000 --- a/frontend/resources/flowy_icons/16x/ft_link.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3449_39572)"> -<path d="M9.86652 12.9993L9.31107 13.5548C7.4703 15.3956 4.48593 15.3956 2.64522 13.5548C0.804457 11.7142 0.804457 8.72971 2.64522 6.88901L3.20067 6.3335" stroke="#666D76" stroke-linecap="round"/> -<path d="M6.53357 9.66645L9.86652 6.3335" stroke="#666D76" stroke-linecap="round"/> -<path d="M6.53357 3.00048L7.08908 2.44503C8.92979 0.604261 11.9142 0.604261 13.7549 2.44503C15.5956 4.28573 15.5956 7.27011 13.7549 9.11088L13.1994 9.66633" stroke="#666D76" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_3449_39572"> -<rect width="16" height="16" fill="white" transform="translate(0.200073)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/ft_text.svg b/frontend/resources/flowy_icons/16x/ft_text.svg deleted file mode 100644 index 0b82109141..0000000000 --- a/frontend/resources/flowy_icons/16x/ft_text.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Document-Text--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Document Text Streamline Icon: https://streamlinehq.com</desc><path d="M1.875 6.25c0 -2.357025 0 -3.53553125 0.73223125 -4.26776875C3.33946875 1.25 4.517975 1.25 6.875 1.25h1.25c2.3569999999999998 0 3.5355625 0 4.26775 0.73223125C13.125 2.71446875 13.125 3.8929750000000003 13.125 6.25v2.5c0 2.3569999999999998 0 3.5355625 -0.73225 4.26775C11.660562500000001 13.75 10.482 13.75 8.125 13.75h-1.25c-2.357025 0 -3.53553125 0 -4.26776875 -0.73225C1.875 12.285562500000001 1.875 11.107 1.875 8.75v-2.5Z" stroke="#000000" stroke-width="1"></path><path d="M5 7.5h5" stroke="#000000" stroke-linecap="round" stroke-width="1"></path><path d="M5 5h5" stroke="#000000" stroke-linecap="round" stroke-width="1"></path><path d="M5 10h3.125" stroke="#000000" stroke-linecap="round" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/ft_video.svg b/frontend/resources/flowy_icons/16x/ft_video.svg deleted file mode 100644 index 1065c0ea08..0000000000 --- a/frontend/resources/flowy_icons/16x/ft_video.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.16284 7.65815C1.16284 5.41046 1.16284 4.28659 1.78359 3.53015C1.89728 3.39165 2.02422 3.26471 2.16272 3.15102C2.91915 2.53027 4.04303 2.53027 6.29072 2.53027C8.53847 2.53027 9.66228 2.53027 10.4188 3.15102C10.5572 3.26471 10.6842 3.39165 10.7978 3.53015C11.4187 4.28659 11.4187 5.41046 11.4187 7.65815V8.3419C11.4187 10.5896 11.4187 11.7135 10.7978 12.4699C10.6842 12.6084 10.5572 12.7353 10.4188 12.849C9.66228 13.4698 8.53847 13.4698 6.29072 13.4698C4.04303 13.4698 2.91915 13.4698 2.16272 12.849C2.02422 12.7353 1.89728 12.6084 1.78359 12.4699C1.16284 11.7135 1.16284 10.5896 1.16284 8.3419V7.65815Z" stroke="#666D76"/> -<path d="M11.4186 6.29102L11.8688 6.06595C13.1991 5.40077 13.8645 5.06814 14.3508 5.36877C14.8372 5.66933 14.8372 6.41308 14.8372 7.90058V8.10008C14.8372 9.58758 14.8372 10.3313 14.3508 10.6319C13.8645 10.9325 13.1991 10.5999 11.8688 9.9347L11.4186 9.70964V6.29102Z" stroke="#666D76"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/group.svg b/frontend/resources/flowy_icons/16x/group.svg index eaa2b9c862..f0a6dff4f9 100644 --- a/frontend/resources/flowy_icons/16x/group.svg +++ b/frontend/resources/flowy_icons/16x/group.svg @@ -1,15 +1,7 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_45_20)"> -<path d="M1.38501 4.325V2.855C1.38501 2.0465 2.04651 1.385 2.85501 1.385H4.32501" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.675 1.385H13.145C13.9535 1.385 14.615 2.0465 14.615 2.855V4.325" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.615 11.675V13.145C14.615 13.9535 13.9535 14.615 13.145 14.615H11.675" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.32501 14.615H2.85501C2.04651 14.615 1.38501 13.9535 1.38501 13.145V11.675" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.06001 4.325H8.73501C8.73501 4.325 9.47001 4.325 9.47001 5.06V7.265C9.47001 7.265 9.47001 8 8.73501 8H5.06001C5.06001 8 4.32501 8 4.32501 7.265V5.06C4.32501 5.06 4.32501 4.325 5.06001 4.325Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.26503 8H10.94C10.94 8 11.675 8 11.675 8.735V10.94C11.675 10.94 11.675 11.675 10.94 11.675H7.26503C7.26503 11.675 6.53003 11.675 6.53003 10.94V8.735C6.53003 8.735 6.53003 8 7.26503 8Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_45_20"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> +<path d="M10 2H13C13.5523 2 14 2.44772 14 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6 2H3C2.44772 2 2 2.44772 2 3V6" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6 14H3C2.44772 14 2 13.5523 2 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10 14H13C13.5523 14 14 13.5523 14 13V10" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<rect x="6" y="6" width="4" height="4" rx="1" stroke="#333333"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/help_and_documentation.svg b/frontend/resources/flowy_icons/16x/help_and_documentation.svg deleted file mode 100644 index e4c68c2583..0000000000 --- a/frontend/resources/flowy_icons/16x/help_and_documentation.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M18 8.8V10C18 13.7712 18 15.6569 16.8284 16.8284C15.6569 18 13.7712 18 10 18C6.22876 18 4.34314 18 3.17158 16.8284C2 15.6569 2 13.7712 2 10C2 6.22876 2 4.34314 3.17158 3.17158C4.34314 2 6.22876 2 10 2H11.2M6 11H13M6 14H10M13 4.5C13 5.16304 13.2634 5.79893 13.7322 6.26777C14.2011 6.73661 14.837 7 15.5 7C16.163 7 16.7989 6.73661 17.2678 6.26777C17.7366 5.79893 18 5.16304 18 4.5C18 3.83696 17.7366 3.20107 17.2678 2.73223C16.7989 2.26339 16.163 2 15.5 2C14.837 2 14.2011 2.26339 13.7322 2.73223C13.2634 3.20107 13 3.83696 13 4.5Z" stroke="#21232A" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/help_center.svg b/frontend/resources/flowy_icons/16x/help_center.svg index 7840906683..52766922e9 100644 --- a/frontend/resources/flowy_icons/16x/help_center.svg +++ b/frontend/resources/flowy_icons/16x/help_center.svg @@ -1,6 +1,11 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.55676 4.62532C4.55676 0.243068 11.4431 0.24313 11.4431 4.62532C11.4431 7.75538 8.31295 7.12932 8.31295 10.8854" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.32104 14.6614L8.33023 14.6512" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g clip-path="url(#clip0_859_40861)"> +<path d="M8.00207 15.9668C3.84348 15.9668 0.472656 12.596 0.472656 8.43737C0.472656 4.27878 3.84348 0.907959 8.00207 0.907959C12.1607 0.907959 15.5315 4.27878 15.5315 8.43737C15.5315 12.596 12.1607 15.9668 8.00207 15.9668ZM8.00207 15.0256C11.6407 15.0256 14.5903 12.076 14.5903 8.43737C14.5903 4.79878 11.6407 1.84914 8.00207 1.84914C4.36348 1.84914 1.41383 4.79878 1.41383 8.43737C1.41383 12.076 4.36348 15.0256 8.00207 15.0256Z" fill="#171717" stroke="black" stroke-width="0.133333"/> +<path d="M7.96863 12.6016C7.78728 12.6016 7.61337 12.5295 7.48513 12.4013C7.3569 12.2731 7.28486 12.0992 7.28486 11.9178C7.28486 11.7365 7.3569 11.5626 7.48513 11.4343C7.61337 11.3061 7.78728 11.2341 7.96863 11.2341C8.14997 11.2341 8.32389 11.3061 8.45212 11.4343C8.58035 11.5626 8.65239 11.7365 8.65239 11.9178C8.65239 12.0992 8.58035 12.2731 8.45212 12.4013C8.32389 12.5295 8.14997 12.6016 7.96863 12.6016ZM6.12016 7.2157C6.05257 7.21522 5.98585 7.20044 5.92438 7.17233C5.86292 7.14423 5.80809 7.10344 5.76351 7.05263C5.71893 7.00183 5.68561 6.94217 5.66572 6.87757C5.64584 6.81297 5.63986 6.7449 5.64816 6.67782C5.67922 6.41194 5.72863 6.19453 5.79639 6.02559C5.91127 5.74558 6.08998 5.49629 6.31828 5.29759C6.55006 5.08809 6.82166 4.92741 7.11686 4.82512C7.41609 4.72254 7.73043 4.671 8.04675 4.67265C8.70557 4.67265 9.25381 4.86653 9.69239 5.25476C10.131 5.64159 10.3507 6.15829 10.3507 6.803C10.3507 7.09288 10.2943 7.35547 10.1813 7.59076C10.0693 7.82653 9.83028 8.12017 9.46463 8.47123C9.09804 8.82276 8.85616 9.07265 8.73616 9.22135C8.61183 9.3793 8.51973 9.56014 8.4651 9.75359C8.43216 9.86229 8.40863 9.99076 8.39592 10.1385C8.37334 10.3879 8.17286 10.5856 7.92251 10.5856C7.85592 10.5852 7.79014 10.5709 7.72937 10.5436C7.66861 10.5164 7.6142 10.4768 7.5696 10.4273C7.52501 10.3778 7.49122 10.3196 7.47039 10.2564C7.44956 10.1931 7.44215 10.1262 7.44863 10.0599C7.4651 9.88865 7.49004 9.73947 7.52486 9.61241C7.58933 9.37006 7.68722 9.15829 7.81804 8.97712C7.94839 8.79547 8.18369 8.52912 8.52392 8.17759C8.86463 7.82653 9.08392 7.57053 9.18275 7.41194C9.27969 7.25241 9.38181 6.99312 9.38181 6.67782C9.38181 6.36253 9.21569 6.10606 8.99216 5.86323C8.76722 5.62041 8.4411 5.49853 8.01333 5.49853C7.17851 5.49853 6.7051 5.92817 6.59404 6.78747C6.56251 7.02935 6.3658 7.2157 6.12204 7.2157H6.12016Z" fill="#171717" stroke="black" stroke-width="0.133333"/> +</g> +<defs> +<clipPath id="clip0_859_40861"> +<rect width="16" height="16" fill="white" transform="translate(0 0.4375)"/> +</clipPath> +</defs> </svg> - - diff --git a/frontend/resources/flowy_icons/16x/hide.svg b/frontend/resources/flowy_icons/16x/hide.svg index d76e4576df..45e81d8748 100644 --- a/frontend/resources/flowy_icons/16x/hide.svg +++ b/frontend/resources/flowy_icons/16x/hide.svg @@ -1,6 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.67502 6.675C6.4908 6.84666 6.34305 7.05366 6.24057 7.28366C6.13809 7.51366 6.08298 7.76194 6.07854 8.0137C6.0741 8.26545 6.12041 8.51553 6.21471 8.749C6.30901 8.98247 6.44937 9.19455 6.62742 9.3726C6.80547 9.55065 7.01755 9.69101 7.25102 9.78531C7.48449 9.87961 7.73457 9.92592 7.98632 9.92148C8.23808 9.91704 8.48637 9.86194 8.71636 9.75946C8.94636 9.65698 9.15336 9.50922 9.32502 9.325" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.2063 3.675C7.46962 3.64219 7.73469 3.6255 8.00005 3.625C12.375 3.625 14.25 8 14.25 8C13.9706 8.5982 13.6202 9.16058 13.2063 9.675" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.63125 4.63125C3.38828 5.47789 2.39367 6.64079 1.75 8C1.75 8 3.625 12.375 8 12.375C9.19744 12.3782 10.3692 12.0282 11.3687 11.3687" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.75 1.75L14.25 14.25" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M5.12265 11.5847C5.92255 12.1165 6.88538 12.5 8.00024 12.5C10.4842 12.5 12.2135 10.596 13.0675 9.39083C13.6624 8.55146 13.6624 7.44854 13.0675 6.60917C12.7341 6.13867 12.2673 5.56168 11.6743 5.03305L10.9661 5.74127C11.4908 6.20089 11.9225 6.72296 12.2516 7.18736C12.601 7.68035 12.601 8.31965 12.2516 8.81264C11.4276 9.97552 9.9599 11.5 8.00024 11.5C7.19618 11.5 6.47495 11.2434 5.84702 10.8603L5.12265 11.5847ZM5.03441 10.2587L4.32618 10.967C3.73316 10.4383 3.26636 9.86133 2.93294 9.39083C2.33811 8.55146 2.33811 7.44854 2.93294 6.60917C3.78701 5.40397 5.51627 3.5 8.00024 3.5C9.1151 3.5 10.0779 3.88354 10.8778 4.4153L10.1535 5.13966C9.52554 4.75665 8.80431 4.5 8.00024 4.5C6.04059 4.5 4.57293 6.02448 3.74884 7.18736C3.39948 7.68035 3.39948 8.31965 3.74884 8.81264C4.07794 9.27704 4.50968 9.79911 5.03441 10.2587ZM6.99269 9.71466C7.28548 9.8954 7.62952 10 8.00036 10C9.09422 10 9.95491 9.08996 9.95491 8C9.95491 7.64165 9.86187 7.30275 9.69811 7.00924L8.93118 7.77618C8.94668 7.84779 8.95491 7.92265 8.95491 8C8.95491 8.5669 8.51315 9 8.00036 9C7.91225 9 7.82623 8.98721 7.7442 8.96316L6.99269 9.71466ZM7.06951 8.22363L6.30253 8.99061C6.13882 8.69713 6.04582 8.35829 6.04582 8C6.04582 6.91005 6.9065 6 8.00036 6C8.37114 6 8.71513 6.10456 9.00789 6.28525L8.25635 7.03679C8.17436 7.01277 8.08841 7 8.00036 7C7.48757 7 7.04582 7.4331 7.04582 8C7.04582 8.07728 7.05403 8.15208 7.06951 8.22363Z" fill="#333333"/> +<path d="M11.667 3.33325L3.33366 11.6666" stroke="#333333" stroke-linecap="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/icon_code_block.svg b/frontend/resources/flowy_icons/16x/icon_code_block.svg deleted file mode 100644 index 1d36321b6a..0000000000 --- a/frontend/resources/flowy_icons/16x/icon_code_block.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_22_30)"> -<path d="M10.0643 6.23059L10.1655 6.33179C10.9519 7.11818 11.3451 7.51143 11.3451 8C11.3451 8.48857 10.9519 8.88182 10.1655 9.66816L10.0643 9.76941" stroke="black" stroke-linecap="round"/> -<path d="M8.76325 5.15155L7.99999 8L7.23672 10.8485" stroke="black" stroke-linecap="round"/> -<path d="M5.93572 6.23059L5.83453 6.33179C5.04813 7.11818 4.65494 7.51143 4.65494 8C4.65494 8.48857 5.04813 8.88182 5.83453 9.66816L5.93572 9.76941" stroke="black" stroke-linecap="round"/> -<path d="M2.10205 8C2.10205 5.21965 2.10205 3.82953 2.96576 2.96577C3.82951 2.10207 5.21964 2.10207 7.99999 2.10207C10.7803 2.10207 12.1705 2.10207 13.0342 2.96577C13.8979 3.82953 13.8979 5.21965 13.8979 8C13.8979 10.7803 13.8979 12.1705 13.0342 13.0342C12.1705 13.8979 10.7803 13.8979 7.99999 13.8979C5.21964 13.8979 3.82951 13.8979 2.96576 13.0342C2.10205 12.1705 2.10205 10.7803 2.10205 8Z" stroke="black"/> -</g> -<defs> -<clipPath id="clip0_22_30"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/icon_delete.svg b/frontend/resources/flowy_icons/16x/icon_delete.svg deleted file mode 100644 index de7c3a7fc1..0000000000 --- a/frontend/resources/flowy_icons/16x/icon_delete.svg +++ /dev/null @@ -1,14 +0,0 @@ -<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.6" clip-path="url(#clip0_632_5742)"> -<path d="M12.3746 4.48999C10.7096 4.32499 9.03454 4.23999 7.36453 4.23999C6.37453 4.23999 5.38452 4.28999 4.39452 4.38999L3.37451 4.48999" stroke="#171717" stroke-width="0.750004" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.12427 3.98501L6.23427 3.33C6.31427 2.855 6.37427 2.5 7.21927 2.5H8.52928C9.37429 2.5 9.43929 2.875 9.51429 3.335L9.62429 3.98501" stroke="#171717" stroke-width="0.750004" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.2995 6.07031L10.9745 11.1053C10.9195 11.8903 10.8745 12.5003 9.47949 12.5003H6.26947C4.87447 12.5003 4.82947 11.8903 4.77446 11.1053L4.44946 6.07031" stroke="#171717" stroke-width="0.750004" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.03955 9.75H8.70456" stroke="#171717" stroke-width="0.750004" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.62451 7.75H9.12453" stroke="#171717" stroke-width="0.750004" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_632_5742"> -<rect width="14" height="14" fill="white" transform="translate(0.874512 0.5)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/icon_math_eq.svg b/frontend/resources/flowy_icons/16x/icon_math_eq.svg deleted file mode 100644 index 8f4a6fc4f4..0000000000 --- a/frontend/resources/flowy_icons/16x/icon_math_eq.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.93463 13.4183H2.15028C3.28248 13.4183 4.25293 12.6097 4.46859 11.4775L5.87036 4.5225C6.08602 3.39031 7.05645 2.5816 8.18865 2.5816H8.40431" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.61996 13.4184C8.0269 12.8253 7.64951 11.747 7.64951 10.4531C7.64951 9.15916 8.0269 8.08088 8.61996 7.48782" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.0949 13.4184C13.6879 12.8253 14.0653 11.747 14.0653 10.4531C14.0653 9.15916 13.6879 8.08088 13.0949 7.48782" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.59045 8.40425L12.0705 12.4478" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.0705 8.40425L9.59045 12.4478" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.60596 6.89473H6.94863" stroke="black" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/icon_template.svg b/frontend/resources/flowy_icons/16x/icon_template.svg deleted file mode 100644 index 094050cfb4..0000000000 --- a/frontend/resources/flowy_icons/16x/icon_template.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.6"> -<path d="M6.65532 5.36433C6.1857 5.19336 5.67874 5.1001 5.15 5.1001C2.71995 5.1001 0.75 7.07004 0.75 9.5001C0.75 11.9302 2.71995 13.9001 5.15 13.9001C5.28928 13.9001 5.42705 13.8936 5.56302 13.881C5.58146 13.8352 5.60362 13.7899 5.62967 13.7453L6.22523 12.7266C5.88729 12.8391 5.52577 12.9001 5.15 12.9001C3.27223 12.9001 1.75 11.3779 1.75 9.5001C1.75 7.62233 3.27223 6.1001 5.15 6.1001C5.58951 6.1001 6.00955 6.18349 6.39515 6.33532L6.65532 5.36433Z" fill="#171717"/> -<path d="M8.75223 2.17655L14.1614 3.62593C14.2681 3.65452 14.3314 3.76419 14.3028 3.87088L12.8534 9.28007C12.8368 9.34216 12.7927 9.38956 12.7377 9.4127L13.2431 10.2772C13.5173 10.1248 13.7318 9.8658 13.8194 9.53889L15.2688 4.1297C15.4403 3.48954 15.0604 2.83154 14.4202 2.66001L9.01105 1.21062C8.37089 1.03909 7.71289 1.41899 7.54136 2.05915L6.09197 7.46833C5.92044 8.10849 6.30034 8.7665 6.9405 8.93803L8.23738 9.28553L8.76065 8.39046L7.19932 7.9721C7.09262 7.94351 7.02931 7.83385 7.05789 7.72715L8.50728 2.31797C8.53587 2.21127 8.64554 2.14796 8.75223 2.17655Z" fill="#171717"/> - <path d="M10.8816 7.22904L14.8387 13.9977C15.0336 14.331 14.7931 14.75 14.407 14.75H6.49297C6.10686 14.75 5.86645 14.331 6.06132 13.9977L10.0184 7.22904C10.2114 6.89884 10.6886 6.89884 10.8816 7.22904Z" stroke="#171717"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/image_rounded.svg b/frontend/resources/flowy_icons/16x/image_rounded.svg deleted file mode 100644 index d360e7de69..0000000000 --- a/frontend/resources/flowy_icons/16x/image_rounded.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Gallery--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Gallery Streamline Icon: https://streamlinehq.com</desc><path d="M0.6628125 7.5C0.6628125 4.276875 0.6628125 2.665375 1.6640625 1.6640625C2.665375 0.6628125 4.276875 0.6628125 7.5 0.6628125C10.723062500000001 0.6628125 12.334624999999999 0.6628125 13.335875 1.6640625C14.337187499999999 2.665375 14.337187499999999 4.276875 14.337187499999999 7.5C14.337187499999999 10.723062500000001 14.337187499999999 12.334624999999999 13.335875 13.335875C12.3346875 14.337187499999999 10.723062500000001 14.337187499999999 7.5 14.337187499999999C4.276875 14.337187499999999 2.665375 14.337187499999999 1.6640625 13.335875C0.6628125 12.3346875 0.6628125 10.723062500000001 0.6628125 7.5Z" stroke="#000000" stroke-width="1"></path><path stroke="#000000" d="M8.867437500000001 4.765125C8.865187500000001 5.81775 10.0033125 6.478125 10.916062499999999 5.953749999999999C11.3415 5.7093125 11.603375 5.25575 11.6023125 4.765125C11.6045625 3.7124375 10.466437500000001 3.052125 9.5536875 3.5765000000000002C9.12825 3.820875 8.866375 4.2745 8.867437500000001 4.765125" stroke-width="1"></path><path d="M0.6628125 7.8419375L1.860375 6.7940625C2.4834375 6.2489375 3.4225 6.280187499999999 4.007875 6.865562499999999L6.940875 9.7985625C7.410687500000001 10.268437500000001 8.150375 10.3325 8.694062500000001 9.950375000000001L8.897937500000001 9.807125C9.680250000000001 9.2573125 10.73875 9.321 11.449499999999999 9.960687499999999L13.653500000000001 11.94425" stroke="#000000" stroke-linecap="round" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/insert_document.svg b/frontend/resources/flowy_icons/16x/insert_document.svg deleted file mode 100644 index 712203a2be..0000000000 --- a/frontend/resources/flowy_icons/16x/insert_document.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Document-Medicine--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Document Medicine Streamline Icon: https://streamlinehq.com</desc><path d="M1.3464999999999998 6.1325625C1.3464999999999998 3.5540624999999997 1.3464999999999998 2.264875 2.1475625000000003 1.4638125C2.9485625 0.6628125 4.2378125 0.6628125 6.81625 0.6628125H8.18375C10.7621875 0.6628125 12.051437499999999 0.6628125 12.8524375 1.4638125C13.653500000000001 2.264875 13.653500000000001 3.5540624999999997 13.653500000000001 6.1325625V8.867437500000001C13.653500000000001 11.445875000000001 13.653500000000001 12.7351875 12.8524375 13.536125000000002C12.051437499999999 14.337187499999999 10.7621875 14.337187499999999 8.18375 14.337187499999999H6.81625C4.2378125 14.337187499999999 2.9485625 14.337187499999999 2.1475625000000003 13.5361875C1.3464999999999998 12.7351875 1.3464999999999998 11.445875000000001 1.3464999999999998 8.867437500000001V6.1325625Z" stroke="#000000" stroke-width="1"></path><path d="M7.5 3.3976875V4.765125M7.5 4.765125V6.1325625M7.5 4.765125H6.1325625M7.5 4.765125H8.867437500000001" stroke="#000000" stroke-linecap="round" stroke-width="1"></path><path d="M4.765125 8.867437500000001H10.234875" stroke="#000000" stroke-linecap="round" stroke-width="1"></path><path d="M5.4488125 11.6023125H9.551187500000001" stroke="#000000" stroke-linecap="round" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/keyboard.svg b/frontend/resources/flowy_icons/16x/keyboard.svg deleted file mode 100644 index 40cb2d42a8..0000000000 --- a/frontend/resources/flowy_icons/16x/keyboard.svg +++ /dev/null @@ -1,14 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.58143 5.94881C4.58143 6.32644 4.2753 6.63256 3.89774 6.63256C3.52018 6.63256 3.21399 6.32644 3.21399 5.94881C3.21399 5.57119 3.52011 5.26513 3.89774 5.26513C4.27536 5.26513 4.58143 5.57125 4.58143 5.94881Z" fill="black"/> -<path d="M4.58143 8C4.58143 8.37762 4.2753 8.68369 3.89774 8.68369C3.52018 8.68369 3.21399 8.37762 3.21399 8C3.21399 7.62238 3.52011 7.31625 3.89774 7.31625C4.27536 7.31625 4.58143 7.62238 4.58143 8Z" fill="black"/> -<path d="M6.63257 8C6.63257 8.37762 6.32645 8.68369 5.94882 8.68369C5.5712 8.68369 5.26514 8.37762 5.26514 8C5.26514 7.62238 5.57126 7.31625 5.94882 7.31625C6.32639 7.31625 6.63257 7.62238 6.63257 8Z" fill="black"/> -<path d="M6.63257 5.94881C6.63257 6.32644 6.32645 6.63256 5.94882 6.63256C5.5712 6.63256 5.26514 6.32644 5.26514 5.94881C5.26514 5.57119 5.57126 5.26513 5.94882 5.26513C6.32639 5.26513 6.63257 5.57125 6.63257 5.94881Z" fill="black"/> -<path d="M8.68372 5.94881C8.68372 6.32644 8.3776 6.63256 7.99997 6.63256C7.62235 6.63256 7.31628 6.32644 7.31628 5.94881C7.31628 5.57119 7.62235 5.26513 7.99997 5.26513C8.3776 5.26513 8.68372 5.57125 8.68372 5.94881Z" fill="black"/> -<path d="M8.68372 8C8.68372 8.37762 8.3776 8.68369 7.99997 8.68369C7.62235 8.68369 7.31628 8.37762 7.31628 8C7.31628 7.62238 7.62235 7.31625 7.99997 7.31625C8.3776 7.31625 8.68372 7.62238 8.68372 8Z" fill="black"/> -<path d="M10.7349 5.94881C10.7349 6.32644 10.4288 6.63256 10.0512 6.63256C9.67356 6.63256 9.36743 6.32644 9.36743 5.94881C9.36743 5.57119 9.67356 5.26513 10.0512 5.26513C10.4288 5.26513 10.7349 5.57125 10.7349 5.94881Z" fill="black"/> -<path d="M10.7349 8C10.7349 8.37762 10.4288 8.68369 10.0512 8.68369C9.67356 8.68369 9.36743 8.37762 9.36743 8C9.36743 7.62238 9.67356 7.31625 10.0512 7.31625C10.4288 7.31625 10.7349 7.62238 10.7349 8Z" fill="black"/> -<path d="M12.786 5.94881C12.786 6.32644 12.4799 6.63256 12.1023 6.63256C11.7246 6.63256 11.4186 6.32644 11.4186 5.94881C11.4186 5.57119 11.7246 5.26513 12.1023 5.26513C12.4799 5.26513 12.786 5.57125 12.786 5.94881Z" fill="black"/> -<path d="M12.786 8C12.786 8.37762 12.4799 8.68369 12.1023 8.68369C11.7246 8.68369 11.4186 8.37762 11.4186 8C11.4186 7.62238 11.7246 7.31625 12.1023 7.31625C12.4799 7.31625 12.786 7.62238 12.786 8Z" fill="black"/> -<path d="M1.16284 7.31625C1.16284 5.38244 1.16284 4.4155 1.76359 3.81475C2.36434 3.21394 3.33128 3.21394 5.26515 3.21394H10.7349C12.6688 3.21394 13.6357 3.21394 14.2365 3.81475C14.8372 4.4155 14.8372 5.38244 14.8372 7.31625V8.68375C14.8372 10.6176 14.8372 11.5845 14.2365 12.1853C13.6357 12.7861 12.6687 12.7861 10.7349 12.7861H5.26515C3.33128 12.7861 2.36434 12.7861 1.76359 12.1853C1.16284 11.5845 1.16284 10.6176 1.16284 8.68375V7.31625Z" stroke="black"/> -<path d="M4.58142 10.7349H11.4187" stroke="black" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/last_modified.svg b/frontend/resources/flowy_icons/16x/last_modified.svg new file mode 100644 index 0000000000..5ba3d7494e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/last_modified.svg @@ -0,0 +1,3 @@ +<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.734 6.50065V6.60065H12.834H22.1673H22.2673V6.50065C22.2673 5.91155 22.7449 5.43398 23.334 5.43398C23.9231 5.43398 24.4007 5.91155 24.4007 6.50065V6.60065H24.5007H26.834C28.0675 6.60065 29.0673 7.60055 29.0673 8.83398V15.734H8.16732H8.06732V15.834V26.334V26.434H8.16732H19.734V28.5673H8.16732C6.93388 28.5673 5.93398 27.5674 5.93398 26.334V8.83399C5.93398 7.60055 6.93384 6.60065 8.16728 6.60065H10.5007H10.6007V6.50065C10.6007 5.91155 11.0782 5.43398 11.6673 5.43398C12.2564 5.43398 12.734 5.91155 12.734 6.50065ZM26.834 13.6007H26.934V13.5007V8.83398V8.73398H26.834H24.5007H24.4007V8.83398C24.4007 9.42309 23.9231 9.90065 23.334 9.90065C22.7449 9.90065 22.2673 9.42309 22.2673 8.83398V8.73398H22.1673H12.834H12.734V8.83398C12.734 9.42309 12.2564 9.90065 11.6673 9.90065C11.0782 9.90065 10.6007 9.42309 10.6007 8.83398V8.73398H10.5007H8.16732H8.06732V8.83398V13.5007V13.6007H8.16732H26.834ZM27.6602 18.999C27.7937 18.7678 28.0893 18.6886 28.3205 18.8221L29.3308 19.4054C29.562 19.5389 29.6412 19.8345 29.5077 20.0657L25.1827 27.5568L23.3352 26.4901L27.6602 18.999ZM22.7054 27.7046L24.4459 28.7094L22.5708 29.9475L22.7054 27.7046Z" fill="#2B2F36" stroke="#FDEDA7" stroke-width="0.2"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/leave_workspace.svg b/frontend/resources/flowy_icons/16x/leave_workspace.svg deleted file mode 100644 index 5ce4842e80..0000000000 --- a/frontend/resources/flowy_icons/16x/leave_workspace.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.29688 5.66993C6.52938 2.96993 7.91688 1.86743 10.9544 1.86743H11.0519C14.4044 1.86743 15.7469 3.20993 15.7469 6.56243V11.4524C15.7469 14.8049 14.4044 16.1474 11.0519 16.1474H10.9544C7.93938 16.1474 6.55188 15.0599 6.30438 12.4049" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.5 9H11.16" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.48438 6.4873L11.9969 8.9998L9.48438 11.5123" stroke="#FB006D" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/link_to_page.svg b/frontend/resources/flowy_icons/16x/link_to_page.svg deleted file mode 100644 index 7968a5b367..0000000000 --- a/frontend/resources/flowy_icons/16x/link_to_page.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_315_1360)"> -<path d="M1 10.3867L3.31601 8.07071C3.35506 8.03166 3.35506 7.96834 3.31601 7.92929L1 5.61328" stroke="#171717" stroke-linecap="round"/> -<path d="M15.2441 6.92362V9.72927C15.2441 12.5349 14.2726 13.6572 11.8441 13.6572H8.92978C6.50121 13.6572 5.52979 12.5349 5.52979 9.72927V6.36249C5.52979 3.55683 6.50121 2.43457 8.92978 2.43457H11.3584" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M15.2011 6.85622H13.2583C11.8011 6.85622 11.3154 6.29204 11.3154 4.5995V2.62904C11.3154 2.53051 11.4379 2.48505 11.5022 2.55972L15.2011 6.85622Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_315_1360"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/load_more.svg b/frontend/resources/flowy_icons/16x/load_more.svg deleted file mode 100644 index 078b8668b3..0000000000 --- a/frontend/resources/flowy_icons/16x/load_more.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Arrow-Down--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Arrow Down Streamline Icon: https://streamlinehq.com</desc><path d="m7.5 2.5 0 10m0 0 3.75 -3.75m-3.75 3.75 -3.75 -3.75" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/local_model_download.svg b/frontend/resources/flowy_icons/16x/local_model_download.svg deleted file mode 100644 index 270bcb25d1..0000000000 --- a/frontend/resources/flowy_icons/16x/local_model_download.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="12" height="11" viewBox="0 0 12 11" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11 7.5L11 9.5C11 9.76522 10.8829 10.0196 10.6746 10.2071C10.4662 10.3946 10.1836 10.5 9.88889 10.5L2.11111 10.5C1.81643 10.5 1.53381 10.3946 1.32544 10.2071C1.11706 10.0196 1 9.76522 1 9.5L1 7.5" stroke="#005483" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3 3.5L6 6.5L9 3.5" stroke="#005483" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6 0.5L6 6.5" stroke="#005483" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/lock_page.svg b/frontend/resources/flowy_icons/16x/lock_page.svg deleted file mode 100644 index b68b7ab42f..0000000000 --- a/frontend/resources/flowy_icons/16x/lock_page.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.75 10.5C1.75 8.73225 1.75 7.84838 2.29918 7.29919C2.84835 6.75 3.73223 6.75 5.5 6.75H10.5C12.2678 6.75 13.1516 6.75 13.7008 7.29919C14.25 7.84838 14.25 8.73225 14.25 10.5C14.25 12.2678 14.25 13.1516 13.7008 13.7008C13.1516 14.25 12.2678 14.25 10.5 14.25H5.5C3.73223 14.25 2.84835 14.25 2.29918 13.7008C1.75 13.1516 1.75 12.2678 1.75 10.5Z" stroke="#171711"/> -<path d="M4.25 6.75V5.5C4.25 3.42893 5.92893 1.75 8 1.75C10.0711 1.75 11.75 3.42893 11.75 5.5V6.75" stroke="#171711" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/lock_page_fill.svg b/frontend/resources/flowy_icons/16x/lock_page_fill.svg deleted file mode 100644 index b2ed846e69..0000000000 --- a/frontend/resources/flowy_icons/16x/lock_page_fill.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.75 10.5C1.75 8.73225 1.75 7.84838 2.29918 7.29919C2.84835 6.75 3.73223 6.75 5.5 6.75H10.5C12.2677 6.75 13.1516 6.75 13.7008 7.29919C14.25 7.84838 14.25 8.73225 14.25 10.5C14.25 12.2677 14.25 13.1516 13.7008 13.7008C13.1516 14.25 12.2677 14.25 10.5 14.25H5.5C3.73223 14.25 2.84835 14.25 2.29918 13.7008C1.75 13.1516 1.75 12.2677 1.75 10.5Z" fill="#ED8835" stroke="#ED8835"/> -<path d="M4.25 6.75V5.5C4.25 3.42893 5.92893 1.75 8 1.75C10.0711 1.75 11.75 3.42893 11.75 5.5V6.75" stroke="#ED8835" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_add_block_photo_gallery.svg b/frontend/resources/flowy_icons/16x/m_add_block_photo_gallery.svg deleted file mode 100644 index ccbfa80599..0000000000 --- a/frontend/resources/flowy_icons/16x/m_add_block_photo_gallery.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M360-384h384L618-552l-90 120-66-88-102 136Zm-48 144q-29.7 0-50.85-21.15Q240-282.3 240-312v-480q0-29.7 21.15-50.85Q282.3-864 312-864h480q29.7 0 50.85 21.15Q864-821.7 864-792v480q0 29.7-21.15 50.85Q821.7-240 792-240H312Zm0-72h480v-480H312v480ZM168-96q-29.7 0-50.85-21.15Q96-138.3 96-168v-552h72v552h552v72H168Zm144-696v480-480Z"/></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/m_bottom_sheet_back.svg b/frontend/resources/flowy_icons/16x/m_bottom_sheet_back.svg deleted file mode 100644 index e67a6361ba..0000000000 --- a/frontend/resources/flowy_icons/16x/m_bottom_sheet_back.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.5149 2.41107C14.8403 2.73651 14.8403 3.26414 14.5149 3.58958L7.60417 10.5003L14.5149 17.4111C14.8403 17.7365 14.8403 18.2641 14.5149 18.5896C14.1895 18.915 13.6618 18.915 13.3364 18.5896L6.42566 11.6788C5.77478 11.028 5.77478 9.97269 6.42566 9.32182L13.3364 2.41107C13.6618 2.08563 14.1895 2.08563 14.5149 2.41107Z" fill="#2B2F36"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_copy_link.svg b/frontend/resources/flowy_icons/16x/m_copy_link.svg deleted file mode 100644 index 89b5ba6c07..0000000000 --- a/frontend/resources/flowy_icons/16x/m_copy_link.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.37265 11.6283L11.6277 6.37322M4.40235 8.34366L3.08857 9.65744C1.63742 11.1086 1.637 13.4615 3.08815 14.9127C4.5393 16.3638 6.89279 16.3634 8.34394 14.9123L9.65631 13.5986M8.3431 4.40201L9.65687 3.08823C11.108 1.63708 13.4605 1.63734 14.9117 3.08849C16.3628 4.53964 16.3628 6.89245 14.9116 8.3436L13.5985 9.65733" stroke="#171717" stroke-width="1.23529" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg b/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg deleted file mode 100644 index ada869aebf..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_action_archive.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.25 8.5166V15.8333C16.25 17.4999 15.8333 18.3333 13.75 18.3333H6.25C4.16667 18.3333 3.75 17.4999 3.75 15.8333V8.5166" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.16675 1.6665H15.8334C17.5001 1.6665 18.3334 2.49984 18.3334 4.1665V5.83317C18.3334 7.49984 17.5001 8.33317 15.8334 8.33317H4.16675C2.50008 8.33317 1.66675 7.49984 1.66675 5.83317V4.1665C1.66675 2.49984 2.50008 1.6665 4.16675 1.6665Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.48315 11.6665H11.5165" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg b/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg deleted file mode 100644 index 68d249cf74..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_action_mark_as_read.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.225 21.251H14.775C19.4 21.251 21.25 19.401 21.25 14.776V9.22598C21.25 4.60098 19.4 2.75098 14.775 2.75098H9.225C4.6 2.75098 2.75 4.60098 2.75 9.22598V14.776C2.75 19.401 4.6 21.251 9.225 21.251Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.06909 12.2014L10.6868 14.8192L16.1998 9.40137" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg b/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg deleted file mode 100644 index 5c06dd12c3..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_action_multiple_choice.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.80005 3.25L3.02359 4.43061C3.13543 4.53852 3.31311 4.53694 3.42301 4.42704L5.60005 2.25" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -<path d="M1.80005 9.625L3.02359 10.8056C3.13543 10.9135 3.31311 10.9119 3.42301 10.802L5.60005 8.625" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -<path d="M1.80005 16L3.02359 17.1806C3.13543 17.2885 3.31311 17.2869 3.42301 17.177L5.60005 15" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -<path d="M8.19995 3.5H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -<path d="M8.19995 10H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -<path d="M8.19995 16.5H17.7" stroke="#1E2022" stroke-width="1.4" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_archived.svg b/frontend/resources/flowy_icons/16x/m_notification_archived.svg deleted file mode 100644 index 37c769880e..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_archived.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.25 8.5166V15.8333C16.25 17.4999 15.8333 18.3333 13.75 18.3333H6.25C4.16667 18.3333 3.75 17.4999 3.75 15.8333V8.5166" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.1665 1.66675H15.8332C17.4998 1.66675 18.3332 2.50008 18.3332 4.16675V5.83341C18.3332 7.50008 17.4998 8.33342 15.8332 8.33342H4.1665C2.49984 8.33342 1.6665 7.50008 1.6665 5.83341V4.16675C1.6665 2.50008 2.49984 1.66675 4.1665 1.66675Z" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.48291 11.6667H11.5162" stroke="#171717" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg b/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg deleted file mode 100644 index 5292de6310..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_mark_as_read.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.68734 17.7084H12.3123C16.1665 17.7084 17.7082 16.1667 17.7082 12.3126V7.68758C17.7082 3.83341 16.1665 2.29175 12.3123 2.29175H7.68734C3.83317 2.29175 2.2915 3.83341 2.2915 7.68758V12.3126C2.2915 16.1667 3.83317 17.7084 7.68734 17.7084Z" stroke="#1E2022" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.72412 10.1669L8.90558 12.3484L13.4997 7.8335" stroke="#1E2022" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_multi_select.svg b/frontend/resources/flowy_icons/16x/m_notification_multi_select.svg deleted file mode 100644 index 249d716cc1..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_multi_select.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect width="20" height="20" rx="10" fill="#00BCF0"/> -<path d="M6.25 10.3125L9.27885 13.125L14.6875 7.5" stroke="white" stroke-width="1.6875" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_multi_unselect.svg b/frontend/resources/flowy_icons/16x/m_notification_multi_unselect.svg deleted file mode 100644 index a0ae39fe15..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_multi_unselect.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="0.5" width="19" height="19" rx="9.5" stroke="#1F2329" stroke-opacity="0.3"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_reminder.svg b/frontend/resources/flowy_icons/16x/m_notification_reminder.svg deleted file mode 100644 index 8369a8c9a8..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_reminder.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect width="36" height="36" rx="18" fill="#F1E2FF"/> -<g clip-path="url(#clip0_756_7938)"> -<path d="M10.889 18.9758C10.889 16.467 12.2534 14.148 14.4676 12.8929C15.5594 12.276 16.7922 11.9519 18.0463 11.9519C19.3004 11.9519 20.5332 12.276 21.6251 12.8929C23.8397 14.148 25.2036 16.4666 25.2036 18.9758C25.204 22.8556 21.9997 26 18.0465 26C14.0934 26 10.889 22.8552 10.889 18.9758ZM14.5662 10.2628C14.6346 10.3376 14.6871 10.4256 14.7205 10.5213C14.7539 10.617 14.7674 10.7185 14.7604 10.8196C14.7533 10.9208 14.7258 11.0194 14.6794 11.1096C14.633 11.1997 14.5688 11.2795 14.4907 11.3441L11.3166 14.0258C11.1563 14.1599 10.9506 14.2272 10.7421 14.2137C10.5336 14.2002 10.3383 14.107 10.1966 13.9533C10.1282 13.8785 10.0757 13.7906 10.0423 13.6949C10.0089 13.5993 9.99528 13.4978 10.0023 13.3967C10.0092 13.2956 10.0367 13.1969 10.0829 13.1067C10.1292 13.0166 10.1933 12.9367 10.2713 12.872L13.4458 10.1904C13.6062 10.0558 13.8125 9.98844 14.0214 10.0024C14.2316 10.0157 14.4276 10.1099 14.5662 10.2628ZM17.9363 14.9813C17.4981 14.9813 17.143 15.3244 17.143 15.7475V18.8122C17.143 19.0687 17.2755 19.3078 17.4959 19.45L19.8767 20.9824C20.2411 21.217 20.7344 21.1224 20.9775 20.7699C21.2206 20.418 21.122 19.9424 20.7576 19.7078L18.7301 18.4025V15.7475C18.7301 15.3244 18.3745 14.9813 17.9363 14.9813ZM21.4237 10.2677C21.5641 10.111 21.7602 10.0155 21.9702 10.0016C22.1801 9.98776 22.3871 10.0566 22.5468 10.1935L25.7276 12.9254C26.0582 13.2089 26.092 13.7022 25.8031 14.0267C25.5138 14.3511 25.0112 14.3844 24.6805 14.1004L21.4993 11.3694C21.4208 11.3029 21.3565 11.2213 21.3101 11.1294C21.2636 11.0376 21.2361 10.9374 21.2291 10.8347C21.222 10.7321 21.2356 10.629 21.2691 10.5317C21.3025 10.4344 21.3551 10.3448 21.4237 10.2681V10.2677Z" fill="#B369FE"/> -</g> -<defs> -<clipPath id="clip0_756_7938"> -<rect width="16" height="16" fill="white" transform="translate(10 10)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_notification_settings.svg b/frontend/resources/flowy_icons/16x/m_notification_settings.svg deleted file mode 100644 index 987f547415..0000000000 --- a/frontend/resources/flowy_icons/16x/m_notification_settings.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 12.5C11.3807 12.5 12.5 11.3807 12.5 10C12.5 8.61929 11.3807 7.5 10 7.5C8.61929 7.5 7.5 8.61929 7.5 10C7.5 11.3807 8.61929 12.5 10 12.5Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.6665 10.7334V9.26669C1.6665 8.40003 2.37484 7.68336 3.24984 7.68336C4.75817 7.68336 5.37484 6.6167 4.6165 5.30836C4.18317 4.55836 4.4415 3.58336 5.19984 3.15003L6.6415 2.32503C7.29984 1.93336 8.14984 2.1667 8.5415 2.82503L8.63317 2.98336C9.38317 4.2917 10.6165 4.2917 11.3748 2.98336L11.4665 2.82503C11.8582 2.1667 12.7082 1.93336 13.3665 2.32503L14.8082 3.15003C15.5665 3.58336 15.8248 4.55836 15.3915 5.30836C14.6332 6.6167 15.2498 7.68336 16.7582 7.68336C17.6248 7.68336 18.3415 8.39169 18.3415 9.26669V10.7334C18.3415 11.6 17.6332 12.3167 16.7582 12.3167C15.2498 12.3167 14.6332 13.3834 15.3915 14.6917C15.8248 15.45 15.5665 16.4167 14.8082 16.85L13.3665 17.675C12.7082 18.0667 11.8582 17.8334 11.4665 17.175L11.3748 17.0167C10.6248 15.7084 9.3915 15.7084 8.63317 17.0167L8.5415 17.175C8.14984 17.8334 7.29984 18.0667 6.6415 17.675L5.19984 16.85C4.4415 16.4167 4.18317 15.4417 4.6165 14.6917C5.37484 13.3834 4.75817 12.3167 3.24984 12.3167C2.37484 12.3167 1.6665 11.6 1.6665 10.7334Z" stroke="#171717" stroke-width="1.25" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_publish.svg b/frontend/resources/flowy_icons/16x/m_publish.svg deleted file mode 100644 index 136dfaed6a..0000000000 --- a/frontend/resources/flowy_icons/16x/m_publish.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_324_837)"> -<path d="M14.7847 5.73828C14.7847 3.60609 14.7847 2.54003 14.1223 1.87766C13.4599 1.21522 12.3938 1.21522 10.2616 1.21522H5.73848C3.60636 1.21522 2.54023 1.21522 1.87786 1.87766C1.21542 2.54003 1.21542 3.60616 1.21542 5.73828" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.99975 14.7849V4.98486M7.99975 4.98486L11.0151 8.28293M7.99975 4.98486L4.98438 8.28293" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_324_837"> -<rect width="16" height="16" fill="white" transform="matrix(-1 0 0 -1 16 16)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_rename.svg b/frontend/resources/flowy_icons/16x/m_rename.svg index 8f7620a806..98200fd061 100644 --- a/frontend/resources/flowy_icons/16x/m_rename.svg +++ b/frontend/resources/flowy_icons/16x/m_rename.svg @@ -1,13 +1,4 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_65_16)"> -<path d="M8.33777 14.5813H15.2568" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.7972 1.89644C12.6849 1.00875 14.2007 1.41488 14.5256 2.6275C14.6764 3.19031 14.5155 3.79075 14.1035 4.20275L4.49383 13.8124L1.4187 14.5813L2.18751 11.5061L11.7972 1.89644Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.644 3.04956L12.9504 5.35594" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_65_16"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14.7339 2C14.0997 2 13.4915 2.25192 13.0431 2.70034L3.00447 12.739C2.91636 12.8271 2.85386 12.9375 2.82363 13.0584L2.02054 16.2708C1.96197 16.505 2.03062 16.7529 2.20138 16.9236C2.37214 17.0944 2.61998 17.163 2.85426 17.1045L6.06663 16.3014C6.18751 16.2712 6.29791 16.2087 6.38602 16.1205L16.4247 6.08189C16.6467 5.85985 16.8228 5.59626 16.943 5.30616C17.0632 5.01605 17.125 4.70512 17.125 4.39112C17.125 4.07711 17.0632 3.76618 16.943 3.47608C16.8228 3.18597 16.6467 2.92238 16.4247 2.70034C16.2026 2.47831 15.939 2.30218 15.6489 2.18201C15.3588 2.06185 15.0479 2 14.7339 2ZM14.0154 3.67261C14.206 3.48205 14.4644 3.375 14.7339 3.375C14.8673 3.375 14.9995 3.40128 15.1227 3.45235C15.246 3.50341 15.358 3.57826 15.4524 3.67261C15.5468 3.76697 15.6216 3.87898 15.6727 4.00226C15.7237 4.12555 15.75 4.25768 15.75 4.39112C15.75 4.52455 15.7237 4.65669 15.6727 4.77997C15.6216 4.90325 15.5468 5.01526 15.4524 5.10962L5.5484 15.0136L3.63239 15.4926L4.11139 13.5766L14.0154 3.67261Z" fill="#333333"/> +<path d="M9.56251 15.75C9.18282 15.75 8.87501 16.0578 8.87501 16.4375C8.87501 16.8172 9.18282 17.125 9.56251 17.125H17.8125C18.1922 17.125 18.5 16.8172 18.5 16.4375C18.5 16.0578 18.1922 15.75 17.8125 15.75H9.56251Z" fill="#333333"/> </svg> - diff --git a/frontend/resources/flowy_icons/16x/m_settings_member.svg b/frontend/resources/flowy_icons/16x/m_settings_member.svg deleted file mode 100644 index edd1e3fe83..0000000000 --- a/frontend/resources/flowy_icons/16x/m_settings_member.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10.5 13.625V12.375C10.5 11.712 10.2366 11.0761 9.76777 10.6072C9.29893 10.1384 8.66304 9.875 8 9.875H4.25C3.58696 9.875 2.95107 10.1384 2.48223 10.6072C2.01339 11.0761 1.75 11.712 1.75 12.375V13.625" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.625 4.875C3.625 5.53804 3.88839 6.17393 4.35723 6.64277C4.82607 7.11161 5.46196 7.375 6.125 7.375C6.78804 7.375 7.42393 7.11161 7.89277 6.64277C8.36161 6.17393 8.625 5.53804 8.625 4.875C8.625 4.21196 8.36161 3.57607 7.89277 3.10723C7.42393 2.63839 6.78804 2.375 6.125 2.375C5.46196 2.375 4.82607 2.63839 4.35723 3.10723C3.88839 3.57607 3.625 4.21196 3.625 4.875Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.25 13.625V12.375C14.2496 11.8211 14.0652 11.283 13.7259 10.8452C13.3865 10.4074 12.9113 10.0947 12.375 9.95624" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.5 2.45624C11.0378 2.59393 11.5144 2.90668 11.8548 3.34518C12.1952 3.78369 12.3799 4.32301 12.3799 4.87811C12.3799 5.43322 12.1952 5.97254 11.8548 6.41104C11.5144 6.84955 11.0378 7.1623 10.5 7.29999" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_settings_more.svg b/frontend/resources/flowy_icons/16x/m_settings_more.svg deleted file mode 100644 index bfd339eb61..0000000000 --- a/frontend/resources/flowy_icons/16x/m_settings_more.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10.964 20.1331C16.0057 20.1331 20.1307 16.0081 20.1307 10.9665C20.1307 5.9248 16.0057 1.7998 10.964 1.7998C5.92236 1.7998 1.79736 5.9248 1.79736 10.9665C1.79736 16.0081 5.92236 20.1331 10.964 20.1331Z" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.7606 11.4167H6.77707" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.9906 11.4167H11.007" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M15.2201 11.4167H15.2365" stroke="#171717" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_table_duplicate.svg b/frontend/resources/flowy_icons/16x/m_table_duplicate.svg deleted file mode 100644 index 1a0b6902e4..0000000000 --- a/frontend/resources/flowy_icons/16x/m_table_duplicate.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.66783 9.66783C3.0898 10.2459 2.75 11.224 2.75 12.9V17.1C2.75 18.776 3.0898 19.7541 3.66783 20.3322C4.24586 20.9102 5.22397 21.25 6.9 21.25H11.1C12.776 21.25 13.7541 20.9102 14.3322 20.3322C14.9102 19.7541 15.25 18.776 15.25 17.1V12.9C15.25 11.224 14.9102 10.2459 14.3322 9.66783C13.7541 9.0898 12.776 8.75 11.1 8.75H6.9C5.22397 8.75 4.24586 9.0898 3.66783 9.66783ZM2.60717 8.60717C3.60414 7.6102 5.07603 7.25 6.9 7.25H11.1C12.924 7.25 14.3959 7.6102 15.3928 8.60717C16.3898 9.60414 16.75 11.076 16.75 12.9V17.1C16.75 18.924 16.3898 20.3959 15.3928 21.3928C14.3959 22.3898 12.924 22.75 11.1 22.75H6.9C5.07603 22.75 3.60414 22.3898 2.60717 21.3928C1.6102 20.3959 1.25 18.924 1.25 17.1V12.9C1.25 11.076 1.6102 9.60414 2.60717 8.60717Z" fill="#1F2329"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9.66783 3.66783C9.0898 4.24586 8.75 5.22397 8.75 6.9V7.25H11.1C12.924 7.25 14.3959 7.6102 15.3928 8.60717C16.3898 9.60414 16.75 11.076 16.75 12.9V15.25H17.1C18.776 15.25 19.7541 14.9102 20.3322 14.3322C20.9102 13.7541 21.25 12.776 21.25 11.1V6.9C21.25 5.22397 20.9102 4.24586 20.3322 3.66783C19.7541 3.0898 18.776 2.75 17.1 2.75H12.9C11.224 2.75 10.2459 3.0898 9.66783 3.66783ZM8.60717 2.60717C9.60414 1.6102 11.076 1.25 12.9 1.25H17.1C18.924 1.25 20.3959 1.6102 21.3928 2.60717C22.3898 3.60414 22.75 5.07603 22.75 6.9V11.1C22.75 12.924 22.3898 14.3959 21.3928 15.3928C20.3959 16.3898 18.924 16.75 17.1 16.75H16C15.5858 16.75 15.25 16.4142 15.25 16V12.9C15.25 11.224 14.9102 10.2459 14.3322 9.66783C13.7541 9.0898 12.776 8.75 11.1 8.75H8C7.58579 8.75 7.25 8.41421 7.25 8V6.9C7.25 5.07603 7.6102 3.60414 8.60717 2.60717Z" fill="#1F2329"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.25 15C5.25 14.5858 5.58579 14.25 6 14.25H12C12.4142 14.25 12.75 14.5858 12.75 15C12.75 15.4142 12.4142 15.75 12 15.75H6C5.58579 15.75 5.25 15.4142 5.25 15Z" fill="#1F2329"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9 11.25C9.41421 11.25 9.75 11.5858 9.75 12V18C9.75 18.4142 9.41421 18.75 9 18.75C8.58579 18.75 8.25 18.4142 8.25 18L8.25 12C8.25 11.5858 8.58579 11.25 9 11.25Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_table_quick_action_copy.svg b/frontend/resources/flowy_icons/16x/m_table_quick_action_copy.svg deleted file mode 100644 index 5e69041afe..0000000000 --- a/frontend/resources/flowy_icons/16x/m_table_quick_action_copy.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.2969 12.9V17.1C16.2969 20.6 14.8969 22 11.3969 22H7.19688C3.69688 22 2.29688 20.6 2.29688 17.1V12.9C2.29688 9.4 3.69688 8 7.19688 8H11.3969C14.8969 8 16.2969 9.4 16.2969 12.9Z" stroke="#666D76" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M22.2969 6.9V11.1C22.2969 14.6 20.8969 16 17.3969 16H16.2969V12.9C16.2969 9.4 14.8969 8 11.3969 8H8.29688V6.9C8.29688 3.4 9.69687 2 13.1969 2H17.3969C20.8969 2 22.2969 3.4 22.2969 6.9Z" stroke="#666D76" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_table_quick_action_cut.svg b/frontend/resources/flowy_icons/16x/m_table_quick_action_cut.svg deleted file mode 100644 index 60660f7891..0000000000 --- a/frontend/resources/flowy_icons/16x/m_table_quick_action_cut.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.4482 20.5L6.04688 2M22.0469 19C22.0469 20.6569 20.7038 22 19.0469 22C17.39 22 16.0469 20.6569 16.0469 19C16.0469 17.3431 17.39 16 19.0469 16C20.7038 16 22.0469 17.3431 22.0469 19Z" stroke="#666D76" stroke-width="1.5" stroke-linecap="round"/> -<path d="M7.64552 20.5L18.0469 2M2.04688 19C2.04688 20.6569 3.39003 22 5.04688 22C6.70372 22 8.04688 20.6569 8.04688 19C8.04688 17.3431 6.70372 16 5.04688 16C3.39003 16 2.04688 17.3431 2.04688 19Z" stroke="#666D76" stroke-width="1.5" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_table_quick_action_delete.svg b/frontend/resources/flowy_icons/16x/m_table_quick_action_delete.svg deleted file mode 100644 index e11d4fa185..0000000000 --- a/frontend/resources/flowy_icons/16x/m_table_quick_action_delete.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M21.7969 5.97998C18.4669 5.64998 15.1169 5.47998 11.7769 5.47998C9.79687 5.47998 7.81687 5.57998 5.83687 5.77998L3.79688 5.97998" stroke="#FB006D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.29688 4.97L9.51688 3.66C9.67688 2.71 9.79687 2 11.4869 2H14.1069C15.7969 2 15.9269 2.75 16.0769 3.67L16.2969 4.97" stroke="#FB006D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M19.6473 9.14014L18.9973 19.2101C18.8873 20.7801 18.7973 22.0001 16.0073 22.0001H9.58727C6.79727 22.0001 6.70727 20.7801 6.59727 19.2101L5.94727 9.14014" stroke="#FB006D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.127 16.5H14.457" stroke="#FB006D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.2969 12.5H15.2969" stroke="#FB006D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_table_quick_action_paste.svg b/frontend/resources/flowy_icons/16x/m_table_quick_action_paste.svg deleted file mode 100644 index 6172fc1d40..0000000000 --- a/frontend/resources/flowy_icons/16x/m_table_quick_action_paste.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.5469 4.00195C18.7219 4.01406 19.8998 4.11051 20.6682 4.87889C21.5469 5.75757 21.5469 7.17179 21.5469 10.0002V16.0002C21.5469 18.8286 21.5469 20.2429 20.6682 21.1215C19.7895 22.0002 18.3753 22.0002 15.5469 22.0002H9.54688C6.71845 22.0002 5.30423 22.0002 4.42555 21.1215C3.54688 20.2429 3.54688 18.8286 3.54688 16.0002V10.0002C3.54688 7.17179 3.54688 5.75757 4.42555 4.87889C5.19393 4.11051 6.37185 4.01406 8.54688 4.00195" stroke="#666D76" stroke-width="1.5"/> -<path d="M7.54688 14.5H15.5469" stroke="#666D76" stroke-width="1.5" stroke-linecap="round"/> -<path d="M7.54688 18H13.0469" stroke="#666D76" stroke-width="1.5" stroke-linecap="round"/> -<path d="M8.54688 3.5C8.54688 2.67157 9.21845 2 10.0469 2H15.0469C15.8753 2 16.5469 2.67157 16.5469 3.5V4.5C16.5469 5.32843 15.8753 6 15.0469 6H10.0469C9.21845 6 8.54688 5.32843 8.54688 4.5V3.5Z" stroke="#666D76" stroke-width="1.5"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_unpublish.svg b/frontend/resources/flowy_icons/16x/m_unpublish.svg deleted file mode 100644 index 49976703f8..0000000000 --- a/frontend/resources/flowy_icons/16x/m_unpublish.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_325_863)"> -<path d="M0.55957 1.04443L15.2068 15.3953" stroke="black" stroke-linecap="round"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.36877 4.64728C8.27405 4.54368 8.14014 4.48467 7.99976 4.48467C7.85938 4.48467 7.72547 4.54368 7.63074 4.64728L7.04482 5.28814L8.49976 6.71365V6.27254L10.6461 8.62012C10.8325 8.82392 11.1487 8.83808 11.3525 8.65174C11.5563 8.46541 11.5705 8.14915 11.3841 7.94535L8.36877 4.64728ZM8.49976 8.11362V14.7847C8.49976 15.0608 8.2759 15.2847 7.99976 15.2847C7.72362 15.2847 7.49976 15.0608 7.49976 14.7847L7.49976 7.13386L8.49976 8.11362ZM7.08437 6.72687L6.36964 6.02661L4.61537 7.94535C4.42904 8.14915 4.4432 8.46541 4.647 8.65174C4.8508 8.83808 5.16706 8.82392 5.3534 8.62012L7.08437 6.72687Z" fill="black"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M1.55985 1.48933C1.54792 1.50074 1.53608 1.51232 1.52432 1.52409L1.52428 1.52412C1.08469 1.96369 0.89294 2.51891 0.802682 3.19018C0.715396 3.83935 0.715405 4.66661 0.715418 5.70119V5.70122V5.73828C0.715418 6.01442 0.939275 6.23828 1.21542 6.23828C1.49156 6.23828 1.71542 6.01442 1.71542 5.73828C1.71542 4.65808 1.71648 3.89822 1.79376 3.32344C1.86911 2.76305 2.00857 2.45403 2.23139 2.23123L2.23142 2.23119C2.24554 2.21708 2.25999 2.2033 2.27484 2.18985L1.63575 1.5637L1.55985 1.48933ZM2.73041 0.886241L3.62347 1.76123C4.15755 1.71596 4.83629 1.71522 5.73848 1.71522H10.2616C11.3418 1.71522 12.1017 1.71628 12.6765 1.79356C13.2369 1.86891 13.5459 2.00837 13.7687 2.23121C13.9915 2.45401 14.131 2.76302 14.2063 3.32341C14.2836 3.89819 14.2847 4.65805 14.2847 5.73828C14.2847 5.89472 14.3565 6.03438 14.469 6.12606C14.5551 6.19622 14.665 6.23828 14.7847 6.23828C15.0608 6.23828 15.2847 6.01442 15.2847 5.73828L15.2847 5.70123C15.2847 4.66659 15.2847 3.83933 15.1974 3.19017C15.1072 2.51888 14.9154 1.96367 14.4758 1.5241C14.0362 1.08451 13.481 0.892744 12.8097 0.802483C12.1606 0.715196 11.3333 0.715206 10.2987 0.715219H10.2987H10.2616H5.73848H5.70143C4.66682 0.715206 3.83956 0.715196 3.19038 0.802483C3.03048 0.823983 2.87715 0.851243 2.73041 0.886241Z" fill="black"/> -</g> -<defs> -<clipPath id="clip0_325_863"> -<rect x="16" y="16" width="16" height="16" transform="rotate(-180 16 16)" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/m_visit_site.svg b/frontend/resources/flowy_icons/16x/m_visit_site.svg deleted file mode 100644 index cc0d41f282..0000000000 --- a/frontend/resources/flowy_icons/16x/m_visit_site.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.61963 7.38039L14.1959 1.80414M14.1959 1.80414H10.8849M14.1959 1.80414V5.11504" stroke="black" stroke-width="1.13" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.1959 7.99997C14.1959 10.9207 14.1959 12.3811 13.2885 13.2884C12.3812 14.1958 10.9207 14.1958 8.00003 14.1958C5.07929 14.1958 3.61892 14.1958 2.71156 13.2884C1.8042 12.3811 1.8042 10.9207 1.8042 7.99997C1.8042 5.07922 1.8042 3.61885 2.71156 2.7115C3.61892 1.80414 5.07929 1.80414 8.00003 1.80414" stroke="black" stroke-width="1.13" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/magnifier.svg b/frontend/resources/flowy_icons/16x/magnifier.svg deleted file mode 100644 index 60c7ae8d5f..0000000000 --- a/frontend/resources/flowy_icons/16x/magnifier.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.6"> -<path d="M7.66536 14.0007C11.1632 14.0007 13.9987 11.1651 13.9987 7.66732C13.9987 4.16951 11.1632 1.33398 7.66536 1.33398C4.16756 1.33398 1.33203 4.16951 1.33203 7.66732C1.33203 11.1651 4.16756 14.0007 7.66536 14.0007Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.6654 14.6673L13.332 13.334" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/media.svg b/frontend/resources/flowy_icons/16x/media.svg deleted file mode 100644 index a56903e5da..0000000000 --- a/frontend/resources/flowy_icons/16x/media.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.44844 11.6292L10.3802 6.90844C10.9724 6.34167 10.9724 5.42273 10.3802 4.85594C9.78812 4.28915 8.82813 4.28915 8.236 4.85594L3.33992 9.54256C2.21489 10.6194 2.21489 12.3654 3.33992 13.4423C4.46494 14.5192 6.28898 14.5192 7.414 13.4423L12.3816 8.68731C14.0395 7.10031 14.0395 4.52726 12.3816 2.94025C10.7236 1.35325 8.03556 1.35325 6.37762 2.94025L2.375 6.77162" stroke="#333333" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/menu_item_ai_writer.svg b/frontend/resources/flowy_icons/16x/menu_item_ai_writer.svg deleted file mode 100644 index 744f4b9b05..0000000000 --- a/frontend/resources/flowy_icons/16x/menu_item_ai_writer.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" fill="none" stroke="#000000" stroke-linecap="round" stroke-linejoin="round" id="Wand-Sparkles--Streamline-Lucide.svg" height="16" width="16"><desc>Wand Sparkles Streamline Icon: https://streamlinehq.com</desc><path d="m13.525 2.275 -0.8 -0.8a0.75625 0.75625 0 0 0 -1.075 0L1.4749999999999999 11.65a0.75625 0.75625 0 0 0 0 1.075l0.8 0.8a0.75 0.75 0 0 0 1.075 0L13.525 3.35a0.75 0.75 0 0 0 0 -1.075" stroke-width="1"></path><path d="m8.75 4.375 1.875 1.875" stroke-width="1"></path><path d="M3.125 3.75v2.5" stroke-width="1"></path><path d="M11.875 8.75v2.5" stroke-width="1"></path><path d="M6.25 1.25v1.25" stroke-width="1"></path><path d="M4.375 5H1.875" stroke-width="1"></path><path d="M13.125 10h-2.5" stroke-width="1"></path><path d="M6.875 1.875H5.625" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/message_support.svg b/frontend/resources/flowy_icons/16x/message_support.svg deleted file mode 100644 index 03e72384d7..0000000000 --- a/frontend/resources/flowy_icons/16x/message_support.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_51_4)"> -<path d="M5.47817 13.2508C9.79161 15.4635 14.8829 12.177 14.6424 7.33506C14.4019 2.49313 9.01011 -0.272812 4.93711 2.35644C2.21798 4.11169 1.27198 7.64219 2.74917 10.5218L1.34973 14.6503L5.47817 13.2508Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.31091 5.55369C6.84716 4.02931 8.83254 3.65706 9.88454 4.88369C10.2117 5.265 10.3911 5.75106 10.3904 6.25344C10.3904 7.65294 8.29116 8.35262 8.29116 8.35262" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.34705 11.1516H8.35405" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_51_4"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/minus.svg b/frontend/resources/flowy_icons/16x/minus.svg deleted file mode 100644 index 8be3fe893d..0000000000 --- a/frontend/resources/flowy_icons/16x/minus.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="12" y="7.5" width="1" height="8" rx="0.5" transform="rotate(90 12 7.5)" fill="#333333"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/multiselect.svg b/frontend/resources/flowy_icons/16x/multiselect.svg index e1682cebb5..97a2e9c434 100644 --- a/frontend/resources/flowy_icons/16x/multiselect.svg +++ b/frontend/resources/flowy_icons/16x/multiselect.svg @@ -1,11 +1,8 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.8174 2.67738C5.06386 2.43092 5.06386 2.03132 4.8174 1.78485C4.57093 1.53839 4.17134 1.53839 3.92487 1.78485L2.4778 3.23192L1.9774 2.73152C1.73093 2.48506 1.33134 2.48506 1.08487 2.73152C0.838408 2.97799 0.838408 3.37758 1.08487 3.62405L2.03154 4.57071C2.278 4.81718 2.6776 4.81718 2.92407 4.57071L4.8174 2.67738Z" fill="#171717"/> -<path d="M6.89558 2.23112C6.54703 2.23112 6.26447 2.51368 6.26447 2.86223C6.26447 3.21078 6.54703 3.49334 6.89558 3.49334H14.4689C14.8175 3.49334 15.1 3.21078 15.1 2.86223C15.1 2.51368 14.8175 2.23112 14.4689 2.23112H6.89558Z" fill="#171717"/> -<path d="M6.89558 7.28001C6.54703 7.28001 6.26447 7.56256 6.26447 7.91112C6.26447 8.25967 6.54703 8.54223 6.89558 8.54223H14.4689C14.8175 8.54223 15.1 8.25967 15.1 7.91112C15.1 7.56256 14.8175 7.28001 14.4689 7.28001H6.89558Z" fill="#171717"/> -<path d="M6.89558 12.3289C6.54703 12.3289 6.26447 12.6115 6.26447 12.96C6.26447 13.3086 6.54703 13.5911 6.89558 13.5911H14.4689C14.8175 13.5911 15.1 13.3086 15.1 12.96C15.1 12.6115 14.8175 12.3289 14.4689 12.3289H6.89558Z" fill="#171717"/> -<path d="M4.8174 12.144C5.06386 11.8976 5.06386 11.498 4.8174 11.2515C4.57093 11.0051 4.17134 11.0051 3.92487 11.2515L2.4778 12.6986L1.9774 12.1982C1.73093 11.9517 1.33134 11.9517 1.08487 12.1982C0.838408 12.4447 0.838408 12.8442 1.08487 13.0907L2.03154 14.0374C2.278 14.2838 2.6776 14.2838 2.92407 14.0374L4.8174 12.144Z" fill="#171717"/> -<path d="M4.8174 6.51819C5.06386 6.76465 5.06386 7.16425 4.8174 7.41071L2.92407 9.30405C2.6776 9.55051 2.278 9.55051 2.03154 9.30405L1.08487 8.35738C0.838408 8.11092 0.838408 7.71132 1.08487 7.46485C1.33134 7.21839 1.73093 7.21839 1.9774 7.46485L2.4778 7.96526L3.92487 6.51819C4.17134 6.27172 4.57093 6.27172 4.8174 6.51819Z" fill="#171717"/> -<path d="M1.5 12.5L2.5 13.5L4.5 11.5" stroke="#171717" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.5 8L2.5 9L4.5 7" stroke="#171717" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.5 3L2.5 4L4.5 2" stroke="#171717" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<circle cx="4" cy="4" r="0.5" fill="#333333"/> +<circle cx="4" cy="8" r="0.5" fill="#333333"/> +<circle cx="4" cy="12" r="0.5" fill="#333333"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/number.svg b/frontend/resources/flowy_icons/16x/number.svg index e0dcb04637..9d8b98d10d 100644 --- a/frontend/resources/flowy_icons/16x/number.svg +++ b/frontend/resources/flowy_icons/16x/number.svg @@ -1,6 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1 10.6298H13.7647" stroke="#171717" stroke-width="1.23529" stroke-linecap="round"/> -<path d="M2.02948 4.45334H15.0001" stroke="#171717" stroke-width="1.23529" stroke-linecap="round"/> -<path d="M3.98532 14.2327L5.70452 2" stroke="#171717" stroke-width="1.23529" stroke-linecap="round"/> -<path d="M10.1618 14.2327L11.881 2" stroke="#171717" stroke-width="1.23529" stroke-linecap="round"/> +<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/open_in_browser.svg b/frontend/resources/flowy_icons/16x/open_in_browser.svg deleted file mode 100644 index 8c2964aa95..0000000000 --- a/frontend/resources/flowy_icons/16x/open_in_browser.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3228_23979)"> -<path d="M8.68378 7.31604L14.8372 1.1626M14.8372 1.1626H11.1836M14.8372 1.1626V4.81622" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.8372 7.99979C14.8372 11.2228 14.8372 12.8344 13.8359 13.8357C12.8347 14.837 11.2231 14.837 8.00003 14.837C4.7769 14.837 3.1654 14.837 2.16409 13.8357C1.16284 12.8345 1.16284 11.2228 1.16284 7.99979C1.16284 4.77666 1.16284 3.16516 2.16409 2.16385C3.1654 1.1626 4.7769 1.1626 8.00003 1.1626" stroke="#171717" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_3228_23979"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/paragraph_mark.svg b/frontend/resources/flowy_icons/16x/paragraph_mark.svg deleted file mode 100644 index be4b14bffd..0000000000 --- a/frontend/resources/flowy_icons/16x/paragraph_mark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="9" height="10" viewBox="0 0 9 10" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.05969 10C3.92616 10 3.81509 9.95506 3.72656 9.86516C3.63803 9.77537 3.59375 9.66406 3.59375 9.53125V5.9375H2.96875C2.14741 5.9375 1.44725 5.64788 0.868281 5.06859C0.289428 4.48944 0 3.78891 0 2.96703C0 2.14525 0.289428 1.44531 0.868281 0.867188C1.44725 0.289062 2.14741 0 2.96875 0H8.28125C8.41406 0 8.52541 0.0452093 8.61531 0.135625C8.70509 0.225937 8.75 0.337916 8.75 0.471562C8.75 0.605103 8.70509 0.716156 8.61531 0.804688C8.52541 0.893219 8.41406 0.9375 8.28125 0.9375H7.03125V9.53125C7.03125 9.66406 6.98603 9.77537 6.89563 9.86516C6.80531 9.95506 6.69334 10 6.55969 10C6.42616 10 6.31509 9.95506 6.22656 9.86516C6.13803 9.77537 6.09375 9.66406 6.09375 9.53125V0.9375H4.53125V9.53125C4.53125 9.66406 4.48603 9.77537 4.39563 9.86516C4.30531 9.95506 4.19334 10 4.05969 10Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/photo_layout_browser.svg b/frontend/resources/flowy_icons/16x/photo_layout_browser.svg deleted file mode 100644 index 9f9240da5f..0000000000 --- a/frontend/resources/flowy_icons/16x/photo_layout_browser.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M96-384v-192q0-29.7 21.21-50.85 21.21-21.15 51-21.15T219-626.85q21 21.15 21 50.85v192q0 29.7-21.21 50.85-21.21 21.15-51 21.15T117-333.15Q96-354.3 96-384Zm264 144q-33 0-52.5-19.5T288-312v-336q0-33 19.5-52.5T360-720h240q33 0 52.5 19.5T672-648v336q0 33-19.5 52.5T600-240H360Zm360-144v-192q0-29.7 21.21-50.85 21.21-21.15 51-21.15T843-626.85q21 21.15 21 50.85v192q0 29.7-21.21 50.85-21.21 21.15-51 21.15T741-333.15Q720-354.3 720-384Zm-360 72h240v-336H360v336Zm120-168Z"/></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/photo_layout_grid.svg b/frontend/resources/flowy_icons/16x/photo_layout_grid.svg deleted file mode 100644 index dfc3cfd1e2..0000000000 --- a/frontend/resources/flowy_icons/16x/photo_layout_grid.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#e8eaed"><path d="M216-144q-29 0-50.5-21.5T144-216v-528q0-29.7 21.5-50.85Q187-816 216-816h528q29.7 0 50.85 21.15Q816-773.7 816-744v528q0 29-21.15 50.5T744-144H216Zm0-72h228v-528H216v528Zm300 0h228v-264H516v264Zm0-336h228v-192H516v192Z"/></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/pinned.svg b/frontend/resources/flowy_icons/16x/pinned.svg deleted file mode 100644 index 55f9f30ddf..0000000000 --- a/frontend/resources/flowy_icons/16x/pinned.svg +++ /dev/null @@ -1,2 +0,0 @@ - -<svg viewBox="-0.5 -0.5 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="Pin--Streamline-Solar-Ar.svg" height="16" width="16"><desc>Pin Streamline Icon: https://streamlinehq.com</desc><path d="m9.993375 3.0938749999999997 0.33162499999999995 -0.33128749999999996 -0.33162499999999995 0.33128749999999996Zm1.926375 1.9283875 -0.33162499999999995 0.33128749999999996 0.33162499999999995 -0.33128749999999996ZM5.46148125 12.143125l-0.33162499999999995 0.3313125 0.33162499999999995 -0.3313125Zm-2.5729249999999997 -2.5755624999999998 0.33162499999999995 -0.33125000000000004 -0.33162499999999995 0.33125000000000004Zm8.09075625 -0.19612500000000002 -0.1648125 -0.43881249999999994 0.1648125 0.43881249999999994Zm-1.1981875 0.4501875 0.164875 0.43881249999999994 -0.164875 -0.43881249999999994ZM5.207625 5.23860625l-0.44013749999999996 -0.16126250000000003 0.44013749999999996 0.16126250000000003Zm0.43403125 -1.18463125 0.44013749999999996 0.16126250000000003 -0.44013749999999996 -0.16126250000000003ZM3.65315 6.670937500000001l0.12458749999999999 0.45187499999999997 -0.12458749999999999 -0.45187499999999997Zm0.9103937500000001 -0.3365 -0.27388124999999997 -0.38039999999999996 0.27388124999999997 0.38039999999999996Zm0.23384375 -0.21545000000000003 0.35676874999999997 0.3040125 -0.35676874999999997 -0.3040125ZM8.906875 10.2378125l0.306875 0.354375 -0.306875 -0.354375Zm-0.5439999999999999 1.1379375 -0.45199999999999996 -0.12437500000000001 0.45199999999999996 0.12437500000000001Zm0.33518749999999997 -0.909125 -0.380875 -0.27325 0.380875 0.27325ZM1.6990375 7.972l-0.4687375 0.0029999999999999996 0.4687375 -0.0029999999999999996Zm0.13249375000000002 -0.5019375 -0.4062 -0.23393750000000002 0.4062 0.23393750000000002Zm5.23421875 5.869375 0.0007499999999999999 -0.46875 -0.0007499999999999999 0.46875Zm0.4925625 -0.13025 -0.232375 -0.40712499999999996 0.232375 0.40712499999999996Zm-0.2475 -11.93665 0.1009375 0.45774375 -0.1009375 -0.45774375ZM0.918375 13.418687499999999c-0.1829625 0.1831875 -0.1828125 0.48 0.0003375 0.6629375 0.18315 0.1829375 0.47995000000000004 0.1828125 0.6629125 -0.0003125l-0.6632499999999999 -0.662625Zm3.57151875 -2.248625c0.1829625 -0.18312499999999998 0.1828125 -0.4799375 -0.0003375 -0.6629375 -0.18315625 -0.1829375 -0.47995000000000004 -0.1828125 -0.6629125 0.00037499999999999995l0.6632499999999999 0.6625625ZM9.66175 3.4251625000000003l1.926375 1.9283875 0.6632499999999999 -0.6625749999999999L10.325 2.7625875l-0.6632499999999999 0.6625749999999999ZM5.793106249999999 11.8118125l-2.5729249999999997 -2.5755 -0.6632499999999999 0.6625625 2.5729249999999997 2.5755624999999998 0.6632499999999999 -0.662625Zm5.0213937500000005 -2.8791875 -1.19825 0.4501875 0.32975 0.8776249999999999 1.1981875 -0.4501875 -0.32968749999999997 -0.8776249999999999ZM5.6477625 5.3998687499999996l0.43403125 -1.18463125 -0.8802749999999999 -0.32252500000000006 -0.43403125 1.18463125 0.8802749999999999 0.32252500000000006ZM3.7777375 7.1228125c0.44465625000000003 -0.12262500000000001 0.78300625 -0.20875000000000002 1.05968125 -0.40793749999999995l-0.5477562500000001 -0.7608375c-0.10808125 0.07781874999999999 -0.24840625000000002 0.1236375 -0.7611 0.26498750000000004l0.24917499999999998 0.9037875Zm0.9897499999999999 -2.04546875c-0.18309999999999998 0.49973125 -0.240375 0.6361125 -0.32686875 0.7376l0.7135374999999999 0.60805625c0.22110000000000002 -0.25940625 0.33478125 -0.58964375 0.49360624999999997 -1.02313125l-0.8802749999999999 -0.32252500000000006Zm0.06993125 1.6375312499999999c0.11693125 -0.0841875 0.223275 -0.1821875 0.3167375 -0.291875l-0.7135374999999999 -0.60805625c-0.044556250000000006 0.0522875 -0.09524375 0.09898749999999999 -0.15095625 0.13909375l0.5477562500000001 0.7608375Zm4.7788312500000005 2.6679375c-0.430625 0.16181250000000003 -0.7589999999999999 0.277875 -1.0162499999999999 0.500625l0.61375 0.70875c0.1005 -0.0870625 0.23575 -0.1451875 0.73225 -0.33175000000000004l-0.32975 -0.8776249999999999Zm-0.8014375 2.11725c0.1408125 -0.5117499999999999 0.1865 -0.6519999999999999 0.264125 -0.7601875l-0.76175 -0.5465c-0.1983125 0.2764375 -0.284125 0.6140625000000001 -0.4063125 1.058l0.9039375 0.24868749999999998Zm-0.21481250000000002 -1.616625c-0.10606249999999999 0.091875 -0.201 0.19587500000000002 -0.2828125 0.3099375l0.76175 0.5465c0.039 -0.05437499999999999 0.08425 -0.1039375 0.1348125 -0.1476875l-0.61375 -0.70875Zm-5.37981875 -0.6471250000000001c-0.4038375 -0.40425 -0.6776875 -0.6794374999999999 -0.85539375 -0.9026875 -0.17831875 -0.2240625 -0.19673125 -0.32106250000000003 -0.19700625 -0.3645625l-0.93748125 0.0059375c0.00229375 0.36387500000000006 0.18299375 0.6685625000000001 0.4009625 0.9424375 0.2185875 0.274625 0.53820625 0.5935625 0.92566875 0.9814375l0.6632499999999999 -0.6625625Zm0.30838125 -3.0172875c-0.5282625 0.1456625 -0.9635875 0.2649125 -1.2892124999999999 0.3956625 -0.32471874999999994 0.1303125 -0.632375 0.3060625 -0.81401875 0.6214375l0.81240625 0.46787500000000004c0.021637499999999997 -0.0375625 0.08534375 -0.11275 0.3508875 -0.2193125 0.26464374999999996 -0.10625000000000001 0.6384812500000001 -0.2100625 1.1891125 -0.36187499999999995L3.5285624999999996 6.219025ZM2.16778125 7.969062500000001c-0.0005875 -0.0930625 0.02356875 -0.1845 0.06995625 -0.2650625l-0.81240625 -0.46787500000000004c-0.12936875 0.224625 -0.19666875 0.47962499999999997 -0.19503125 0.738875l0.93748125 -0.0059375Zm2.962075 4.505375c0.38991875000000004 0.39031250000000006 0.71054375 0.7122499999999999 0.9866874999999999 0.9321875000000001 0.27533124999999997 0.2193125 0.58214375 0.40099999999999997 0.9485187500000001 0.4015625l0.0014375 -0.9375c-0.043437500000000004 -0.0000625 -0.140625 -0.0179375 -0.365875 -0.19737500000000002 -0.2245 -0.1788125 -0.50110625 -0.45462500000000006 -0.9075187499999999 -0.8615l-0.6632499999999999 0.662625Zm2.78101875 -1.2230625000000002c-0.15256250000000002 0.5546875 -0.257 0.931375 -0.3639375 1.197875 -0.107375 0.2674375 -0.18312499999999998 0.3311875 -0.22100000000000003 0.3528125l0.46468750000000003 0.8142499999999999c0.318125 -0.1815625 0.4951875 -0.49106249999999996 0.6263125 -0.817875 0.1315625 -0.3276875 0.2514375 -0.7661875 0.39787500000000003 -1.298375l-0.9039375 -0.24868749999999998Zm-0.8458125 2.5568125000000004c0.2544375 0.00037499999999999995 0.5045625 -0.06575 0.7255625 -0.191875l-0.46468750000000003 -0.8142499999999999c-0.07906250000000001 0.045125 -0.1685 0.06875 -0.2594375 0.06862499999999999l-0.0014375 0.9375ZM11.588125 5.35355c0.6646874999999999 0.6653375 1.1245625 1.1273875 1.4066875 1.5082 0.27725 0.37412500000000004 0.309125 0.5731875 0.27525 0.72975l0.9163125 0.198125c0.1186875 -0.54875 -0.1 -1.0295 -0.4383125 -1.4860625 -0.333375 -0.4498625 -0.8540000000000001 -0.969275 -1.4966875 -1.6125875L11.588125 5.35355Zm-0.44393750000000004 4.4567c0.8508749999999999 -0.31968749999999996 1.5395625 -0.577125 2.029 -0.8487500000000001 0.49668749999999995 -0.2756875 0.8945000000000001 -0.623 1.0131875 -1.171875l-0.9163125 -0.198125c-0.0338125 0.1565 -0.14500000000000002 0.3244375 -0.551875 0.55025 -0.41412499999999997 0.229875 -1.0236874999999999 0.46025000000000005 -1.9036875 0.790875l0.32968749999999997 0.8776249999999999ZM10.325 2.7625875c-0.6474375 -0.6480874999999999 -1.1699374999999999 -1.1728812499999999 -1.622125 -1.5086 -0.458625 -0.34045625 -0.9419375000000001 -0.5607875 -1.4930625 -0.43919374999999994l0.2019375 0.9154875c0.15612499999999999 -0.0344375 0.3555625 -0.0032187500000000003 0.73225 0.2764375 0.3830625 0.2844 0.84825 0.7482875 1.51775 1.41844375L10.325 2.7625875ZM6.081793749999999 4.2152375c0.32601875 -0.88976875 0.55326875 -1.5064687500000002 0.78145625 -1.92576875 0.224375 -0.41235625 0.39237500000000003 -0.5247375 0.5485 -0.5591875000000001l-0.2019375 -0.9154875c-0.5510625 0.121575 -0.897 0.52478125 -1.17005 1.02656875 -0.269275 0.49484374999999997 -0.5229625 1.19085 -0.8382437500000001 2.0513500000000002l0.8802749999999999 0.32252500000000006ZM1.581625 14.081312500000001l2.90826875 -2.9112500000000003 -0.6632499999999999 -0.6625625 -2.90826875 2.9111874999999996 0.6632499999999999 0.662625Z" fill="#000000" stroke-width="1"></path></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/published_checkmark.svg b/frontend/resources/flowy_icons/16x/published_checkmark.svg deleted file mode 100644 index 097ace0524..0000000000 --- a/frontend/resources/flowy_icons/16x/published_checkmark.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_391_9245)"> -<circle cx="6.99957" cy="6.99981" r="6.22222" fill="#00BCF0"/> -<path d="M4.27734 7.38884L6.37136 9.33329L10.1107 5.4444" stroke="white" stroke-width="1.2963" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_391_9245"> -<rect width="14" height="14" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/referenced_page.svg b/frontend/resources/flowy_icons/16x/referenced_page.svg deleted file mode 100644 index 802212ac98..0000000000 --- a/frontend/resources/flowy_icons/16x/referenced_page.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="9" height="7" viewBox="0 0 9 7" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.14645 1.14645C5.34171 0.951184 5.65829 0.951184 5.85355 1.14645L7.85355 3.14645C8.04882 3.34171 8.04882 3.65829 7.85355 3.85355L5.85355 5.85355C5.65829 6.04882 5.34171 6.04882 5.14645 5.85355C4.95118 5.65829 4.95118 5.34171 5.14645 5.14645L6.29289 4H3.5C3.24668 4 2.85557 4.08011 2.54215 4.30577C2.25423 4.51307 2 4.86296 2 5.5C2 5.77614 1.77614 6 1.5 6C1.22386 6 1 5.77614 1 5.5C1 4.53704 1.41243 3.88693 1.95785 3.49423C2.47777 3.11989 3.08665 3 3.5 3H6.29289L5.14645 1.85355C4.95118 1.65829 4.95118 1.34171 5.14645 1.14645Z" fill="#1F2329"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.14664 1.85375C4.95138 1.65849 4.95138 1.3419 5.14664 1.14664C5.3419 0.95138 5.65849 0.95138 5.85375 1.14664L7.85375 3.14664C8.04901 3.3419 8.04901 3.65849 7.85375 3.85375L5.85375 5.85375C5.65849 6.04901 5.3419 6.04901 5.14664 5.85375C4.95138 5.65849 4.95138 5.3419 5.14664 5.14664L6.29309 4.0002H3.5002C3.24687 4.0002 2.85576 4.08031 2.54235 4.30596C2.25443 4.51326 2.0002 4.86316 2.0002 5.5002C2.0002 5.77634 1.77634 6.0002 1.5002 6.0002C1.22405 6.0002 1.0002 5.77634 1.0002 5.5002C1.0002 4.53724 1.41263 3.88713 1.95804 3.49443C2.47796 3.12009 3.08685 3.0002 3.5002 3.0002H6.29309L5.14664 1.85375ZM4.40442 2.2002H3.5002C2.95908 2.2002 2.17588 2.35179 1.4906 2.8452C0.739241 3.38618 0.200195 4.27643 0.200195 5.5002C0.200195 6.21816 0.782225 6.8002 1.5002 6.8002C2.21817 6.8002 2.8002 6.21816 2.8002 5.5002C2.8002 5.29081 2.84115 5.17293 2.87431 5.10863C2.90721 5.04483 2.95123 4.99735 3.00979 4.95519C3.07701 4.90679 3.16492 4.8659 3.26382 4.83761C3.36373 4.80903 3.45084 4.8002 3.5002 4.8002H4.40442C4.08237 5.30328 4.14121 5.97969 4.58096 6.41943C5.08864 6.92712 5.91175 6.92712 6.41943 6.41943L8.41943 4.41943C8.92711 3.91175 8.92712 3.08864 8.41943 2.58096L6.41943 0.580957C5.91175 0.0732746 5.08864 0.0732751 4.58096 0.580956C4.14121 1.0207 4.08237 1.69711 4.40442 2.2002Z" fill="white"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/relation.svg b/frontend/resources/flowy_icons/16x/relation.svg index 98d43aba76..f82a41d226 100644 --- a/frontend/resources/flowy_icons/16x/relation.svg +++ b/frontend/resources/flowy_icons/16x/relation.svg @@ -1,3 +1,8 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.27096 5.40009C4.12679 5.40009 4.84192 4.69557 4.84192 3.80004C4.84192 2.90452 4.12679 2.2 3.27096 2.2C2.41512 2.2 1.7 2.90452 1.7 3.80004C1.7 4.69557 2.41512 5.40009 3.27096 5.40009ZM4.92464 9.63685L3.97786 6.50815C5.02313 6.23029 5.82697 5.35088 6.00498 4.25835L9.96163 4.81659C9.95931 4.86353 9.95814 4.91076 9.95814 4.95828C9.95814 5.65895 10.2128 6.29951 10.6337 6.79059L7.67829 9.9392C7.21987 9.59992 6.65442 9.39959 6.04266 9.39959C5.64484 9.39959 5.2666 9.48431 4.92464 9.63685ZM8.47034 10.8487C8.68912 11.2492 8.81362 11.7097 8.81362 12.1996C8.81362 13.7461 7.57302 14.9997 6.04266 14.9997C4.5123 14.9997 3.2717 13.7461 3.2717 12.1996C3.2717 11.5195 3.51168 10.896 3.91076 10.4108L2.74216 6.54916C1.46473 6.29973 0.5 5.16373 0.5 3.80004C0.5 2.25362 1.7406 1 3.27096 1C4.53731 1 5.60525 1.8584 5.93604 3.03078L10.2818 3.64392C10.7477 2.75997 11.6688 2.15823 12.7291 2.15823C14.2595 2.15823 15.5001 3.41186 15.5001 4.95828C15.5001 6.5047 14.2595 7.75832 12.7291 7.75832C12.3266 7.75832 11.9441 7.67159 11.5989 7.51561L8.47034 10.8487ZM14.3001 4.95828C14.3001 5.8538 13.5849 6.55832 12.7291 6.55832C11.8733 6.55832 11.1581 5.8538 11.1581 4.95828C11.1581 4.06276 11.8733 3.35823 12.7291 3.35823C13.5849 3.35823 14.3001 4.06276 14.3001 4.95828ZM7.61362 12.1996C7.61362 13.0952 6.89849 13.7997 6.04266 13.7997C5.18682 13.7997 4.4717 13.0952 4.4717 12.1996C4.4717 11.3041 5.18682 10.5996 6.04266 10.5996C6.89849 10.5996 7.61362 11.3041 7.61362 12.1996Z" fill="#171717"/> +<circle cx="11.8274" cy="5.82739" r="1.5" stroke="#333333"/> +<path d="M10.5008 5.38471L6.24097 4.78992" stroke="#333333"/> +<path d="M4.86475 6.24121L6.02777 10.1009" stroke="#333333"/> +<circle cx="7" cy="11" r="1.5" stroke="#333333"/> +<circle cx="5" cy="5" r="1.5" stroke="#333333"/> +<path d="M10.9011 7.14258L8.1484 10.0447" stroke="#333333"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/reminder_clock.svg b/frontend/resources/flowy_icons/16x/reminder_clock.svg deleted file mode 100644 index c383974855..0000000000 --- a/frontend/resources/flowy_icons/16x/reminder_clock.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.8332 8.83333C13.8332 12.0533 11.2198 14.6667 7.99984 14.6667C4.77984 14.6667 2.1665 12.0533 2.1665 8.83333C2.1665 5.61333 4.77984 3 7.99984 3C11.2198 3 13.8332 5.61333 13.8332 8.83333Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 5.33325V8.66659" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6 1.33325H10" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/rename.svg b/frontend/resources/flowy_icons/16x/rename.svg deleted file mode 100644 index 6a820cf5fc..0000000000 --- a/frontend/resources/flowy_icons/16x/rename.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="14" height="16" viewBox="0 0 14 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.83958 2.40031L2.36624 8.19364C2.15958 8.41364 1.95958 8.84697 1.91958 9.14697L1.67291 11.307C1.58624 12.087 2.14624 12.6203 2.91958 12.487L5.06624 12.1203C5.36624 12.067 5.78624 11.847 5.99291 11.6203L11.4662 5.82697C12.4129 4.82697 12.8396 3.68697 11.3662 2.29364C9.89958 0.913641 8.78624 1.40031 7.83958 2.40031Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.92706 3.3667C7.21373 5.2067 8.70706 6.61337 10.5604 6.80003" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1 14.6665H13" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/save_as.svg b/frontend/resources/flowy_icons/16x/save_as.svg deleted file mode 100644 index f82938f6db..0000000000 --- a/frontend/resources/flowy_icons/16x/save_as.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3228_20949)"> -<path d="M1.21539 10.2617C1.21539 12.3939 1.21539 13.46 1.87777 14.1223C2.54021 14.7848 3.60627 14.7848 5.73846 14.7848H10.2616C12.3937 14.7848 13.4598 14.7848 14.1222 14.1223C14.7846 13.46 14.7846 12.3938 14.7846 10.2617" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.99999 1.21533V11.0153M7.99999 11.0153L11.0154 7.71727M7.99999 11.0153L4.98462 7.71727" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_3228_20949"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/send.svg b/frontend/resources/flowy_icons/16x/send.svg index b141b255b0..a5f933a8ca 100644 --- a/frontend/resources/flowy_icons/16x/send.svg +++ b/frontend/resources/flowy_icons/16x/send.svg @@ -1 +1,3 @@ -<svg viewBox="-0.5 -0.5 13.2 13.2" fill="none" xmlns="http://www.w3.org/2000/svg" id="Arrow-Up--Streamline-Radix.svg" height="13.2" width="13.2"><desc>Arrow Up Streamline Icon: https://streamlinehq.com</desc><path fill-rule="evenodd" clip-rule="evenodd" d="M5.7157 0.2811693333333333C5.92798 0.06897066666666667 6.27202 0.06897066666666667 6.4843 0.2811693333333333L10.831973333333334 4.628842666666667C11.044090666666666 4.841041333333333 11.044090666666666 5.185162666666667 10.831973333333334 5.3973613333333335C10.619693333333334 5.609641333333333 10.275572 5.609641333333333 10.063292 5.3973613333333335L6.643469333333334 1.9774573333333332V11.534530666666667C6.643469333333334 11.834650666666667 6.40012 12.078 6.1000000000000005 12.078S5.556530666666667 11.834650666666667 5.556530666666667 11.534530666666667V1.9774573333333332L2.1366266666666665 5.3973613333333335C1.924428 5.609641333333333 1.5803066666666667 5.609641333333333 1.3681079999999999 5.3973613333333335C1.155828 5.185162666666667 1.155828 4.841041333333333 1.3681079999999999 4.628842666666667L5.7157 0.2811693333333333Z" stroke="#333333" fill="#333333" stroke-width="1"></path></svg> \ No newline at end of file +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.19048 8.71739L8.77817 12.3415C8.96008 12.7567 9.55634 12.735 9.70752 12.3076L12.6399 4.01804C12.7819 3.61644 12.3889 3.2325 11.9907 3.38397L4.1181 6.37898C3.7032 6.53683 3.68442 7.1168 4.08824 7.30115L7.19048 8.71739ZM7.19048 8.71739L8.89286 7.16304" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/share_feedback.svg b/frontend/resources/flowy_icons/16x/share_feedback.svg deleted file mode 100644 index 9dc9b7f404..0000000000 --- a/frontend/resources/flowy_icons/16x/share_feedback.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_51_33)"> -<path d="M7.99998 1.16281V6.63256M7.99998 6.63256L10.0511 4.58137M7.99998 6.63256L5.94885 4.58137" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M1.16284 8.68369H3.32359C3.94247 8.68369 4.2519 8.68369 4.5239 8.80881C4.7959 8.93387 4.99728 9.16887 5.40003 9.63869L5.81397 10.1217C6.21672 10.5915 6.41815 10.8265 6.69015 10.9516C6.96215 11.0767 7.27159 11.0767 7.8904 11.0767H8.10965C8.72847 11.0767 9.0379 11.0767 9.3099 10.9516C9.58197 10.8265 9.78328 10.5915 10.1861 10.1217L10.6 9.63869C11.0028 9.16887 11.2042 8.93387 11.4762 8.80881C11.7482 8.68369 12.0576 8.68369 12.6765 8.68369H14.8372" stroke="black" stroke-linecap="round"/> -<path d="M11.4186 1.24956C12.5297 1.35887 13.2777 1.60587 13.8359 2.16412C14.8372 3.16544 14.8372 4.77694 14.8372 8.00006C14.8372 11.2231 14.8372 12.8347 13.8359 13.8359C12.8347 14.8372 11.2231 14.8372 8.00003 14.8372C4.77697 14.8372 3.1654 14.8372 2.16415 13.8359C1.16284 12.8347 1.16284 11.2231 1.16284 8.00006C1.16284 4.77694 1.16284 3.16544 2.16415 2.16412C2.72234 1.60587 3.47028 1.35887 4.58147 1.24956" stroke="black" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_51_33"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/share_publish.svg b/frontend/resources/flowy_icons/16x/share_publish.svg deleted file mode 100644 index 345208ab90..0000000000 --- a/frontend/resources/flowy_icons/16x/share_publish.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9 1.6875C13.0387 1.6875 16.3125 4.96125 16.3125 9C16.3125 13.0387 13.0387 16.3125 9 16.3125C4.96125 16.3125 1.6875 13.0387 1.6875 9C1.6875 4.96125 4.96125 1.6875 9 1.6875ZM8.4375 9.5625L6.1995 9.56288C6.21525 9.92288 6.24563 10.2731 6.2895 10.6117L6.32438 10.863L6.36787 11.1311C6.71512 13.1156 7.52325 14.607 8.43787 15.051L8.4375 9.5625ZM11.8005 9.56288L9.5625 9.5625V15.051C10.4565 14.6164 11.2489 13.1813 11.6081 11.2628L11.6321 11.1311L11.6756 10.8634C11.7402 10.4324 11.7819 9.99828 11.8005 9.56288ZM5.07337 9.56288H2.83763C3.04238 11.8316 4.47225 13.7479 6.4605 14.6441C6.07163 14.0415 5.751 13.3009 5.5155 12.4639L5.46225 12.2689L5.40038 12.0199L5.34337 11.7656C5.19117 11.0403 5.10084 10.3034 5.07337 9.56288ZM15.1624 9.56288H12.9262C12.9014 10.2245 12.827 10.8834 12.7035 11.5339L12.6562 11.7656L12.5992 12.0199L12.5374 12.2692C12.2974 13.1858 11.958 13.9954 11.5391 14.6441C13.5274 13.7479 14.9573 11.8316 15.1616 9.56288H15.1624ZM6.4605 3.3555L6.41025 3.37838C4.4475 4.28437 3.0405 6.1875 2.83763 8.4375H5.07337C5.10037 7.75125 5.1765 7.08938 5.29613 6.46613L5.34337 6.23438L5.40038 5.98012L5.46225 5.73075C5.70225 4.81425 6.04163 4.00463 6.4605 3.35588V3.3555ZM8.4375 2.949C7.54612 3.38175 6.75562 4.8105 6.39488 6.72113L6.36787 6.86888L6.32438 7.13662C6.25977 7.56774 6.21809 8.00197 6.1995 8.4375H8.4375V2.949ZM11.5395 3.35588L11.5845 3.42712C11.952 4.01475 12.2565 4.72762 12.4826 5.52863L12.5378 5.73112L12.5996 5.98012L12.6566 6.23438C12.8036 6.92438 12.8963 7.66538 12.9266 8.4375H15.1624C14.958 6.16838 13.5281 4.25213 11.5395 3.35625V3.35588ZM9.5625 2.949V8.4375H11.8005C11.7849 8.07668 11.7537 7.71672 11.7067 7.35862L11.6756 7.13662L11.6321 6.86888C11.2849 4.88475 10.4771 3.39338 9.56288 2.949H9.5625Z" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/share_tab_copy.svg b/frontend/resources/flowy_icons/16x/share_tab_copy.svg deleted file mode 100644 index 6e5b0429ad..0000000000 --- a/frontend/resources/flowy_icons/16x/share_tab_copy.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.07617 4.30773L8.42791 2.95589C9.70243 1.68137 11.7688 1.68137 13.0434 2.95589C14.3179 4.23042 14.3179 6.29682 13.0434 7.57135L11.6916 8.92318" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.92308 11.6924L7.57135 13.0442C6.29682 14.3187 4.23042 14.3187 2.9559 13.0442C1.68137 11.7697 1.68137 9.70326 2.9559 8.42873L4.30762 7.0769" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.84666 6.15381L6.1543 9.84617" stroke="white" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/share_tab_icon.svg b/frontend/resources/flowy_icons/16x/share_tab_icon.svg deleted file mode 100644 index a4d019edd5..0000000000 --- a/frontend/resources/flowy_icons/16x/share_tab_icon.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.10573 7.24659C6.03906 7.23992 5.95906 7.23992 5.88573 7.24659C4.29906 7.19325 3.03906 5.89325 3.03906 4.29325C3.03906 2.65992 4.35906 1.33325 5.99906 1.33325C7.6324 1.33325 8.95906 2.65992 8.95906 4.29325C8.9524 5.89325 7.6924 7.19325 6.10573 7.24659Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.9402 2.66675C12.2335 2.66675 13.2735 3.71341 13.2735 5.00008C13.2735 6.26008 12.2735 7.28675 11.0268 7.33341C10.9735 7.32675 10.9135 7.32675 10.8535 7.33341" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.7725 9.70675C1.15917 10.7867 1.15917 12.5467 2.7725 13.6201C4.60583 14.8467 7.6125 14.8467 9.44583 13.6201C11.0592 12.5401 11.0592 10.7801 9.44583 9.70675C7.61917 8.48675 4.6125 8.48675 2.7725 9.70675Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.2266 13.3333C12.7066 13.2333 13.1599 13.0399 13.5332 12.7533C14.5732 11.9733 14.5732 10.6866 13.5332 9.90659C13.1666 9.62659 12.7199 9.43992 12.2466 9.33325" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/show.svg b/frontend/resources/flowy_icons/16x/show.svg deleted file mode 100644 index 1124aee221..0000000000 --- a/frontend/resources/flowy_icons/16x/show.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.03447 10.2533C1.45334 9.49838 1.16284 9.12088 1.16284 8C1.16284 6.87913 1.4534 6.50163 2.03447 5.74669C3.19478 4.23925 5.14078 2.53025 8.00003 2.53025C10.8593 2.53025 12.8052 4.23925 13.9655 5.74669C14.5467 6.50163 14.8372 6.87913 14.8372 8C14.8372 9.12088 14.5467 9.49838 13.9656 10.2533C12.8052 11.7608 10.8593 13.4698 8.00003 13.4698C5.14078 13.4698 3.19478 11.7608 2.03447 10.2533Z" stroke="black"/> -<path d="M10.0512 8C10.0512 9.13288 9.13292 10.0512 8.00004 10.0512C6.86717 10.0512 5.94885 9.13288 5.94885 8C5.94885 6.86713 6.86717 5.94881 8.00004 5.94881C9.13292 5.94881 10.0512 6.86713 10.0512 8Z" stroke="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/sidebar_upgrade_version.svg b/frontend/resources/flowy_icons/16x/sidebar_upgrade_version.svg deleted file mode 100644 index 3f23cbd709..0000000000 --- a/frontend/resources/flowy_icons/16x/sidebar_upgrade_version.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M10.5066 13.892L10.6734 13.6784C11.6487 12.4302 11.9926 11.2916 11.7791 9.94711C14.7314 6.68151 14.9121 3.29101 14.0748 1.20029L14.027 1.07792L13.8959 1.06053C11.6655 0.754485 8.40056 1.73528 5.96573 5.40397C4.62845 5.53682 3.60683 6.14599 2.65933 7.35874L2.49249 7.57229L5.32961 9.01353C5.058 9.6909 5.37947 10.6032 6.12887 11.1887C6.87748 11.7736 7.83985 11.8656 8.41785 11.4555L10.5066 13.892ZM4.1851 13.6929C4.1851 13.6929 5.38377 13.3362 5.78764 12.8193C6.09424 12.4269 6.09502 11.911 5.77351 11.6598C5.452 11.4086 4.95241 11.5348 4.64581 11.9272C4.28381 12.3906 4.1851 13.6929 4.1851 13.6929ZM11.1536 6.2089C11.9263 6.18191 12.5309 5.53362 12.5039 4.76089C12.4769 3.98816 11.8286 3.38362 11.0559 3.4106C10.2832 3.43759 9.67863 4.08588 9.70561 4.85861C9.7326 5.63134 10.3809 6.23588 11.1536 6.2089Z" fill="url(#paint0_linear_8313_6635)"/> -<defs> -<linearGradient id="paint0_linear_8313_6635" x1="4.20975" y1="13.6559" x2="14.9687" y2="11.254" gradientUnits="userSpaceOnUse"> -<stop stop-color="#8032FF"/> -<stop offset="1" stop-color="#FB3DFF"/> -</linearGradient> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/single_select.svg b/frontend/resources/flowy_icons/16x/single_select.svg index 431c228fcd..8ccbc9a2e3 100644 --- a/frontend/resources/flowy_icons/16x/single_select.svg +++ b/frontend/resources/flowy_icons/16x/single_select.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.5238 9.49081L5.6523 7.61931C5.37505 7.34206 5.57141 6.86801 5.9635 6.86801H9.70651C10.0986 6.86801 10.295 7.34206 10.0177 7.61931L8.1462 9.49081C7.97433 9.66268 7.69567 9.66268 7.5238 9.49081Z" fill="#171717"/> -<path d="M14.57 8.335C14.57 12.0546 11.5546 15.07 7.835 15.07C4.11536 15.07 1.1 12.0546 1.1 8.335C1.1 4.61536 4.11536 1.6 7.835 1.6C11.5546 1.6 14.57 4.61536 14.57 8.335Z" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.78787 8.78787L6.51213 7.51213C6.32314 7.32314 6.45699 7 6.72426 7H9.27574C9.54301 7 9.67686 7.32314 9.48787 7.51213L8.21213 8.78787C8.09497 8.90503 7.90503 8.90503 7.78787 8.78787Z" fill="#333333"/> +<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_ai_writer.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_ai_writer.svg deleted file mode 100644 index 7f72551486..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_ai_writer.svg +++ /dev/null @@ -1,16 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_25_68)"> -<path d="M13.4459 8.09759C13.9034 7.64013 14.0371 6.95191 13.6556 6.42895C13.4234 6.11243 13.167 5.81439 12.8887 5.53747C12.6118 5.25921 12.3137 5.0028 11.9972 4.77055C11.4743 4.3891 10.786 4.52254 10.3286 4.98027L4.82179 10.4868C4.6622 10.6464 4.55707 10.8513 4.53739 11.0758C4.501 11.4934 4.46299 12.2835 4.54063 13.4009C4.55842 13.6604 4.76545 13.8672 5.02504 13.8853C6.14268 13.9629 6.93253 13.9249 7.35009 13.8885C7.57464 13.8688 7.77952 13.7637 7.93883 13.6041L13.4459 8.09759Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.3641 8.9171C12.3641 8.9171 12.2005 8.18845 11.219 7.20694C10.2377 6.2257 9.50906 6.06207 9.50906 6.06207" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.64283 13.6381C7.64283 13.6381 7.47921 12.9094 6.49797 11.9282C5.51646 10.9467 4.78781 10.7831 4.78781 10.7831" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.87563 6.2036C6.84113 6.66672 6.49931 7.04304 6.03781 7.09858C5.63588 7.14683 5.10078 7.19131 4.49559 7.19131C3.89041 7.19131 3.35558 7.14683 2.95338 7.09858C2.49188 7.04304 2.15006 6.66672 2.11555 6.2036C2.09129 5.87365 2.06946 5.49193 2.06946 5.16953C2.06946 4.03787 2.97926 3.16123 4.11092 3.14963C4.36736 3.14706 4.62383 3.14706 4.88027 3.14963C6.01193 3.16096 6.92173 4.03787 6.92173 5.16953C6.92173 5.49166 6.90016 5.87365 6.87563 6.2036Z" stroke="black"/> -<path d="M4.49561 2.06946V3.14774" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.68689 5.03473V5.3043" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.30432 5.03473V5.3043" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_25_68"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_bulleted_list.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_bulleted_list.svg deleted file mode 100644 index 16af99d0c7..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_bulleted_list.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.5238 4.28572H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.5238 8H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.5238 11.7143H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.42856 4.28572H2.43397" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.42856 8H2.43397" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.42856 11.7143H2.43397" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar-1.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar-1.svg deleted file mode 100644 index bdabe8f5da..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar-1.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_25_52)"> -<path d="M2.10205 7.85254C2.10205 5.62826 2.10205 4.51617 2.79301 3.82516C3.48403 3.13419 4.59612 3.13419 6.8204 3.13419H9.17958C11.4038 3.13419 12.516 3.13419 13.2069 3.82516C13.8979 4.51617 13.8979 5.62826 13.8979 7.85254V9.03213C13.8979 11.2564 13.8979 12.3686 13.207 13.0595C12.516 13.7505 11.4038 13.7505 9.17958 13.7505H6.8204C4.59612 13.7505 3.48403 13.7505 2.79301 13.0595C2.10205 12.3686 2.10205 11.2564 2.10205 9.03213V7.85254Z" stroke="black"/> -<path d="M5.05099 3.13419V2.24946" stroke="black" stroke-linecap="round"/> -<path d="M10.949 3.13419V2.24946" stroke="black" stroke-linecap="round"/> -<path d="M2.39691 6.08314H13.6031" stroke="black" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_25_52"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar.svg deleted file mode 100644 index d0bf3100bc..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_calendar.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_25_51)"> -<path d="M2.10205 7.85254C2.10205 5.62826 2.10205 4.51617 2.79301 3.82516C3.48403 3.13419 4.59612 3.13419 6.8204 3.13419H9.17958C11.4038 3.13419 12.516 3.13419 13.2069 3.82516C13.8979 4.51617 13.8979 5.62826 13.8979 7.85254V9.03213C13.8979 11.2564 13.8979 12.3686 13.207 13.0595C12.516 13.7505 11.4038 13.7505 9.17958 13.7505H6.8204C4.59612 13.7505 3.48403 13.7505 2.79301 13.0595C2.10205 12.3686 2.10205 11.2564 2.10205 9.03213V7.85254Z" stroke="black"/> -<path d="M5.05099 3.1342V2.24947" stroke="black" stroke-linecap="round"/> -<path d="M10.949 3.1342V2.24947" stroke="black" stroke-linecap="round"/> -<path d="M2.39691 6.08314H13.6031" stroke="black" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_25_51"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_callout.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_callout.svg deleted file mode 100644 index 9539972a37..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_callout.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_25_49)"> -<path d="M2.25056 8H13.8744M2.25056 10.6418H13.8744M2.25056 13.2836H13.8744M3.57146 2.71642H12.5536C13.5704 2.71642 14.2059 3.81718 13.6975 4.69776C13.4615 5.10643 13.0255 5.35821 12.5536 5.35821H3.57146C2.55464 5.35826 1.91915 4.25755 2.42751 3.37692C2.66343 2.96819 3.09955 2.71642 3.57146 2.71642Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_25_49"> -<rect width="14" height="14" fill="white" transform="translate(1.0625 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_checkbox.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_checkbox.svg deleted file mode 100644 index abcf733ccb..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_checkbox.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_125)"> -<path d="M2.07443 7.99999C2.07443 5.20662 2.07443 3.80999 2.94218 2.94218C3.80999 2.07443 5.20662 2.07443 7.99999 2.07443C10.7933 2.07443 12.19 2.07443 13.0578 2.94218C13.9256 3.80999 13.9256 5.20662 13.9256 7.99999C13.9256 10.7933 13.9256 12.19 13.0578 13.0578C12.1901 13.9256 10.7933 13.9256 7.99999 13.9256C5.20662 13.9256 3.80999 13.9256 2.94218 13.0578C2.07443 12.1901 2.07443 10.7933 2.07443 7.99999Z" stroke="black"/> -<path d="M5.92606 8.2963L7.11117 9.48141L10.0739 6.5186" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_125"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_code block.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_code block.svg deleted file mode 100644 index 1d36321b6a..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_code block.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_22_30)"> -<path d="M10.0643 6.23059L10.1655 6.33179C10.9519 7.11818 11.3451 7.51143 11.3451 8C11.3451 8.48857 10.9519 8.88182 10.1655 9.66816L10.0643 9.76941" stroke="black" stroke-linecap="round"/> -<path d="M8.76325 5.15155L7.99999 8L7.23672 10.8485" stroke="black" stroke-linecap="round"/> -<path d="M5.93572 6.23059L5.83453 6.33179C5.04813 7.11818 4.65494 7.51143 4.65494 8C4.65494 8.48857 5.04813 8.88182 5.83453 9.66816L5.93572 9.76941" stroke="black" stroke-linecap="round"/> -<path d="M2.10205 8C2.10205 5.21965 2.10205 3.82953 2.96576 2.96577C3.82951 2.10207 5.21964 2.10207 7.99999 2.10207C10.7803 2.10207 12.1705 2.10207 13.0342 2.96577C13.8979 3.82953 13.8979 5.21965 13.8979 8C13.8979 10.7803 13.8979 12.1705 13.0342 13.0342C12.1705 13.8979 10.7803 13.8979 7.99999 13.8979C5.21964 13.8979 3.82951 13.8979 2.96576 13.0342C2.10205 12.1705 2.10205 10.7803 2.10205 8Z" stroke="black"/> -</g> -<defs> -<clipPath id="clip0_22_30"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_date_or_reminder.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_date_or_reminder.svg deleted file mode 100644 index 71d0aa18b9..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_date_or_reminder.svg +++ /dev/null @@ -1,15 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_31_8)"> -<path d="M13.1326 4.9808V4.07505C13.1326 3.40808 12.5919 2.86737 11.925 2.86737H3.47123C2.80425 2.86737 2.26355 3.40808 2.26355 4.07505V12.5288C2.26355 13.1958 2.8042 13.7365 3.47123 13.7365H5.58466" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.1134 1.6597V4.07505" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.28271 1.6597V4.07505" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.26355 6.49042H5.28274" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.0192 11.0192L10.1134 10.2946V8.90576" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.49042 10.1134C6.49042 12.9025 9.50961 14.6456 11.925 13.2511C13.0459 12.6039 13.7365 11.4078 13.7365 10.1134C13.7365 7.32439 10.7173 5.58129 8.30193 6.97578C7.18095 7.62297 6.49042 8.81905 6.49042 10.1134Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_31_8"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_divider.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_divider.svg deleted file mode 100644 index fd53b92748..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_divider.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M1.6597 8C1.6597 7.68169 1.9178 7.42359 2.23611 7.42359H13.7639C14.0822 7.42359 14.3403 7.68169 14.3403 8C14.3403 8.31831 14.0822 8.57641 13.7639 8.57641H2.23611C1.9178 8.57641 1.6597 8.31831 1.6597 8Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_doc.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_doc.svg deleted file mode 100644 index 71b9659027..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_doc.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_123)"> -<path d="M3.541 2.267H12.459C12.459 2.267 13.733 2.267 13.733 3.541V12.459C13.733 12.459 13.733 13.733 12.459 13.733H3.541C3.541 13.733 2.267 13.733 2.267 12.459V3.541C2.267 3.541 2.267 2.267 3.541 2.267Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.815 5.452H11.185" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.815 8H11.185" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.815 10.548H11.185" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_123"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_emoji_picker.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_emoji_picker.svg deleted file mode 100644 index f525e8cbfc..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_emoji_picker.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_25_66)"> -<path d="M7.99999 13.8979C4.74266 13.8979 2.10205 11.2573 2.10205 8C2.10205 4.74267 4.74266 2.10207 7.99999 2.10207C11.2573 2.10207 13.8979 4.74267 13.8979 8C13.8979 11.2573 11.2573 13.8979 7.99999 13.8979Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.6541 9.4745C10.6541 9.4745 9.76941 10.6541 8 10.6541C6.23059 10.6541 5.34592 9.4745 5.34592 9.4745" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.0643 6.23059C9.90144 6.23059 9.76941 6.09855 9.76941 5.93573C9.76941 5.77291 9.90144 5.64082 10.0643 5.64082C10.2271 5.64082 10.3592 5.77286 10.3592 5.93573C10.3592 6.09861 10.2271 6.23059 10.0643 6.23059Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.93575 6.23059C5.77287 6.23059 5.64084 6.09855 5.64084 5.93573C5.64084 5.77291 5.77287 5.64082 5.93575 5.64082C6.09862 5.64082 6.23061 5.77286 6.23061 5.93573C6.23061 6.09861 6.09857 6.23059 5.93575 6.23059Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_25_66"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_file.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_file.svg deleted file mode 100644 index 32f4057a05..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_file.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_26_76)"> -<path d="M9.18512 13.4812H6.81489V14.37H9.18512V13.4812ZM2.51883 9.18512V6.8149H1.63001V9.18512H2.51883ZM13.4811 8.92615V9.18512H14.37V8.92615H13.4811ZM9.71319 3.62172L12.059 5.73292L12.6536 5.07225L10.3078 2.96105L9.71319 3.62172ZM14.37 8.92615C14.37 7.92564 14.3789 7.29222 14.1265 6.72542L13.3146 7.08703C13.4722 7.44096 13.4811 7.84715 13.4811 8.92615H14.37ZM12.059 5.73292C12.861 6.45469 13.1569 6.73311 13.3146 7.08703L14.1265 6.72542C13.8741 6.15862 13.3972 5.74153 12.6536 5.07225L12.059 5.73292ZM6.83255 2.51889C7.76985 2.51889 8.12356 2.52577 8.43876 2.64672L8.7572 1.81689C8.25248 1.62319 7.70252 1.63007 6.83255 1.63007V2.51889ZM10.3078 2.96105C9.66428 2.38184 9.26198 2.01053 8.7572 1.81689L8.43876 2.64672C8.75411 2.76773 9.02007 2.99794 9.71319 3.62172L10.3078 2.96105ZM6.81489 13.4812C5.68498 13.4812 4.88228 13.4802 4.27328 13.3983C3.67713 13.3182 3.33366 13.1679 3.08292 12.9171L2.45437 13.5456C2.89783 13.9891 3.46013 14.1858 4.15488 14.2793C4.83683 14.3709 5.71011 14.37 6.81489 14.37V13.4812ZM1.63001 9.18512C1.63001 10.2899 1.62908 11.1632 1.72073 11.8451C1.81417 12.5398 2.01091 13.1022 2.45437 13.5456L3.08292 12.9171C2.83213 12.6664 2.68181 12.3228 2.60165 11.7267C2.5198 11.1177 2.51883 10.315 2.51883 9.18512H1.63001ZM9.18512 14.37C10.2898 14.37 11.1632 14.3709 11.8451 14.2793C12.5398 14.1858 13.1021 13.9891 13.5456 13.5456L12.9171 12.9171C12.6663 13.1679 12.3228 13.3182 11.7267 13.3983C11.1177 13.4802 10.315 13.4812 9.18512 13.4812V14.37ZM13.4811 9.18512C13.4811 10.315 13.4802 11.1177 13.3983 11.7267C13.3182 12.3228 13.1678 12.6664 12.9171 12.9171L13.5456 13.5456C13.989 13.1022 14.1858 12.5398 14.2792 11.8451C14.3709 11.1632 14.37 10.2899 14.37 9.18512H13.4811ZM2.51883 6.8149C2.51883 5.68504 2.5198 4.88229 2.60165 4.27334C2.68181 3.67719 2.83213 3.33372 3.08292 3.08292L2.45437 2.45443C2.01096 2.89789 1.81417 3.46019 1.72073 4.15494C1.62908 4.83684 1.63001 5.71012 1.63001 6.8149H2.51883ZM6.83255 1.63007C5.72181 1.63007 4.84426 1.62909 4.15953 1.72074C3.46236 1.81407 2.89816 2.01064 2.45437 2.45443L3.08292 3.08292C3.33333 2.83251 3.67783 2.68198 4.27746 2.60171C4.88948 2.51981 5.69678 2.51889 6.83255 2.51889V1.63007Z" fill="black"/> -<path d="M8.59253 2.37073V3.85213C8.59253 5.24876 8.59253 5.94714 9.0264 6.38101C9.46033 6.81489 10.1587 6.81489 11.5553 6.81489H13.9256" stroke="black"/> -<path d="M5.92602 11.8516V8.88889M5.92602 8.88889L4.74091 9.9999M5.92602 8.88889L7.11113 9.9999" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_26_76"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg deleted file mode 100644 index aa4dbff160..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_four_columns.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6 17.5V2.5M10 17.5V2.5M14 17.5V2.5M2 9.16667C2 6.02397 2 4.45263 2.93726 3.47631C3.87452 2.5 5.38301 2.5 8.4 2.5H11.6C14.617 2.5 16.1255 2.5 17.0627 3.47631C18 4.45263 18 6.02397 18 9.16667V10.8333C18 13.976 18 15.5474 17.0627 16.5237C16.1255 17.5 14.617 17.5 11.6 17.5H8.4C5.38301 17.5 3.87452 17.5 2.93726 16.5237C2 15.5474 2 13.976 2 10.8333V9.16667Z" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_grid.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_grid.svg deleted file mode 100644 index 9db2280443..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_grid.svg +++ /dev/null @@ -1,14 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_103)"> -<path d="M3.541 2.267H12.459C12.459 2.267 13.733 2.267 13.733 3.541V12.459C13.733 12.459 13.733 13.733 12.459 13.733H3.541C3.541 13.733 2.267 13.733 2.267 12.459V3.541C2.267 3.541 2.267 2.267 3.541 2.267Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.267 6.089H13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.267 9.911H13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.08899 6.089V13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.91101 6.089V13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_103"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_h1.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_h1.svg deleted file mode 100644 index a94b3fa12d..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_h1.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.3378 7.64612H8" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.3378 11.8928V3.39948" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 11.8928V3.39948" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.5389 7.64612L13.6622 6.23053V11.8928" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_h2.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_h2.svg deleted file mode 100644 index a0580717b8..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_h2.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.30054 7.66476H7.66477" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.30054 11.6879V3.6416" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.66476 11.6879V3.6416" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.6995 11.6879H11.0174C11.0174 9.00577 13.6995 9.67629 13.6995 7.66476C13.6995 6.65894 12.3584 5.98841 11.0174 6.99423" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_h3.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_h3.svg deleted file mode 100644 index 485caefb15..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_h3.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.30054 7.60458H7.66477" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.30054 11.6278V3.58142" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.66476 11.6278V3.58142" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.3527 6.59877C12.4925 5.9283 13.6995 6.59877 13.6995 7.60459C13.6995 8.34521 13.0991 8.94565 12.3584 8.94565" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.0174 11.2925C12.3584 12.2983 13.6995 11.4936 13.6995 10.2867C13.6995 9.54603 13.0991 8.94565 12.3584 8.94565" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_image.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_image.svg deleted file mode 100644 index 28e628ec8d..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_image.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_152)"> -<path d="M3.541 2.267H12.459C12.459 2.267 13.733 2.267 13.733 3.541V12.459C13.733 12.459 13.733 13.733 12.459 13.733H3.541C3.541 13.733 2.267 13.733 2.267 12.459V3.541C2.267 3.541 2.267 2.267 3.541 2.267Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.815 6.08901C4.815 7.06975 5.87667 7.6827 6.726 7.19233C7.12017 6.96472 7.363 6.54417 7.363 6.08901C7.363 5.10826 6.30134 4.49531 5.452 4.98568C5.05783 5.21329 4.815 5.63384 4.815 6.08901Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.733 9.911L11.7672 7.94524C11.2697 7.44788 10.4633 7.44788 9.96577 7.94524L4.17801 13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_152"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_kanban.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_kanban.svg deleted file mode 100644 index 0ee6e28ae7..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_kanban.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_95)"> -<path d="M3.541 2.267H12.459C12.459 2.267 13.733 2.267 13.733 3.541V12.459C13.733 12.459 13.733 13.733 12.459 13.733H3.541C3.541 13.733 2.267 13.733 2.267 12.459V3.541C2.267 3.541 2.267 2.267 3.541 2.267Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M5.452 4.815V9.274" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 4.815V7.363" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.548 4.815V10.548" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_95"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_math_equation.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_math_equation.svg deleted file mode 100644 index 602b211850..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_math_equation.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.3378 12.954L8 7.29182" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 12.954L2.3378 7.29182" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.6622 7.99962H10.8311C10.8311 6.93796 11.1439 6.58403 11.8928 6.23016C12.6416 5.87629 13.6622 5.40488 13.6622 4.46216C13.6622 4.12806 13.5419 3.80393 13.3196 3.54913C12.8529 3.02111 12.0801 2.89236 11.4674 3.24049C11.1701 3.40965 10.945 3.67506 10.8311 3.99075" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_numbered_list.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_numbered_list.svg deleted file mode 100644 index ffc957d59f..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_numbered_list.svg +++ /dev/null @@ -1,8 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.7619 4.28572H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.7619 8H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M6.7619 11.7143H13.5714" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.04761 4.28572H3.66665V6.76191" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.04761 6.7619H4.2857" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.2857 11.7143H3.04761C3.04761 11.0952 4.2857 10.4762 4.2857 9.85714C4.2857 9.2381 3.66666 8.92857 3.04761 9.2381" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_outline.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_outline.svg deleted file mode 100644 index e6645d2168..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_outline.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.45224 3.24477H13.5478M2.45224 6.41493H10.3776M2.45224 9.58508H13.5478M2.45224 12.7552H7.20747" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_photo_gallery.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_photo_gallery.svg deleted file mode 100644 index 581c8c009e..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_photo_gallery.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.91919 4.09786C4.91919 3.66203 5.09232 3.24405 5.4005 2.93587C5.70868 2.62769 6.12666 2.45456 6.56249 2.45456H11.9021C12.338 2.45456 12.756 2.62769 13.0641 2.93587C13.3723 3.24405 13.5455 3.66203 13.5455 4.09786V9.43752C13.5455 9.87335 13.3723 10.2913 13.0641 10.5995C12.756 10.9077 12.338 11.0808 11.9021 11.0808H6.56249C6.12666 11.0808 5.70868 10.9077 5.4005 10.5995C5.09232 10.2913 4.91919 9.87335 4.91919 9.43752V4.09786Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.07811 5.07941C2.88916 5.18712 2.732 5.34282 2.62251 5.53075C2.51302 5.71868 2.45508 5.93218 2.45456 6.14968V12.3113C2.45456 12.9891 3.0091 13.5436 3.68688 13.5436H9.8485C10.3106 13.5436 10.562 13.3064 10.7727 12.9275" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.0808 4.91919H11.0859" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.91919 8.61616L7.16448 6.37087C7.23372 6.30156 7.31594 6.24658 7.40644 6.20907C7.49694 6.17156 7.59395 6.15225 7.69192 6.15225C7.78988 6.15225 7.88689 6.17156 7.97739 6.20907C8.06789 6.24658 8.15011 6.30156 8.21935 6.37087L10.4646 8.61616" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.84848 7.99999L10.8614 6.98702C10.9307 6.91771 11.0129 6.86273 11.1034 6.82522C11.1939 6.78771 11.2909 6.7684 11.3889 6.7684C11.4869 6.7684 11.5839 6.78771 11.6744 6.82522C11.7649 6.86273 11.8471 6.91771 11.9163 6.98702L13.5455 8.61615" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_quote.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_quote.svg deleted file mode 100644 index 9028b658cb..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_quote.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.6879 3.97683H2.30054" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.37 8H5.65317" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.37 12.0232H5.65317" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.30054 8V12.0232" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_simple_table.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_simple_table.svg deleted file mode 100644 index 1666d7aad7..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_simple_table.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_27_108)"> -<path d="M3.541 2.267H12.459C12.459 2.267 13.733 2.267 13.733 3.541V12.459C13.733 12.459 13.733 13.733 12.459 13.733H3.541C3.541 13.733 2.267 13.733 2.267 12.459V3.541C2.267 3.541 2.267 2.267 3.541 2.267Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.267 8H13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 2.267V13.733" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_27_108"> -<rect width="14" height="14" fill="white" transform="translate(1 1)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_text.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_text.svg deleted file mode 100644 index 134e755dfd..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_text.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 2.12H5.38667C4.1547 2.12 3.53878 2.12 3.15603 2.50274C2.77335 2.88542 2.77335 3.50141 2.77335 4.73332V5.35402M8 2.12H10.6133C11.8452 2.12 12.4612 2.12 12.844 2.50274C13.2266 2.88542 13.2266 3.50141 13.2266 4.73332V5.35402M8 2.12V13.88" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.73331 13.88H11.2667" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg deleted file mode 100644 index 6eb3aeab2b..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_three_columns.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7 17.5V2.5M13 17.5V2.5M2 9.16667C2 6.02397 2 4.45263 2.93726 3.47631C3.87452 2.5 5.38301 2.5 8.4 2.5H11.6C14.617 2.5 16.1255 2.5 17.0627 3.47631C18 4.45263 18 6.02397 18 9.16667V10.8333C18 13.976 18 15.5474 17.0627 16.5237C16.1255 17.5 14.617 17.5 11.6 17.5H8.4C5.38301 17.5 3.87452 17.5 2.93726 16.5237C2 15.5474 2 13.976 2 10.8333V9.16667Z" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_toggle.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_toggle.svg deleted file mode 100644 index ee995b4d8e..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_toggle.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.84344 11.7334V4.18547L11.774 7.95946L5.84344 11.7334Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg deleted file mode 100644 index b3b3e55452..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_two_columns.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 17.5V2.5M2 9.16667C2 6.02397 2 4.45263 2.93726 3.47631C3.87452 2.5 5.38301 2.5 8.4 2.5H11.6C14.617 2.5 16.1255 2.5 17.0627 3.47631C18 4.45263 18 6.02397 18 9.16667V10.8333C18 13.976 18 15.5474 17.0627 16.5237C16.1255 17.5 14.617 17.5 11.6 17.5H8.4C5.38301 17.5 3.87452 17.5 2.93726 16.5237C2 15.5474 2 13.976 2 10.8333V9.16667Z" stroke="#1F2329" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/slash_menu_icon_visuals.svg b/frontend/resources/flowy_icons/16x/slash_menu_icon_visuals.svg deleted file mode 100644 index 65434c3316..0000000000 --- a/frontend/resources/flowy_icons/16x/slash_menu_icon_visuals.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.47554 4.28512C7.99226 3.64256 8.66796 3.21419 9.5424 3V3.97357C9.00581 4.12934 8.58847 4.42141 8.27049 4.84979C7.97238 5.21974 7.83327 5.64811 7.83327 6.11543C7.91276 6.07649 8.07175 6.05701 8.27049 6.05701C8.60834 6.05701 8.90644 6.15437 9.14493 6.38803C9.36354 6.62169 9.48278 6.89429 9.48278 7.24477C9.48278 7.59526 9.34366 7.88733 9.10518 8.12099C8.8667 8.33517 8.56859 8.452 8.19099 8.452C7.7339 8.452 7.37617 8.25729 7.09794 7.9068C6.83959 7.55631 6.72034 7.089 6.72034 6.5438C6.72034 5.66759 6.95883 4.9082 7.47554 4.28512Z" fill="#2B2F36"/> -<path d="M3.77507 4.28512C4.27191 3.64256 4.94762 3.21419 5.82206 3V3.97357C5.28547 4.12934 4.86812 4.42141 4.57002 4.83031C4.25204 5.21974 4.11292 5.64811 4.11292 6.11543C4.19242 6.07648 4.35141 6.05701 4.57002 6.05701C4.888 6.05701 5.16623 6.15437 5.40471 6.38803C5.64319 6.62169 5.76244 6.89429 5.76244 7.24477C5.76244 7.59526 5.64319 7.88733 5.42458 8.12099C5.1861 8.33517 4.86812 8.452 4.47065 8.452C4.01356 8.452 3.65583 8.25729 3.39747 7.9068C3.11924 7.55631 3 7.10847 3 6.5438C3 5.66759 3.25836 4.9082 3.77507 4.28512Z" fill="#2B2F36"/> -<path d="M10.6328 6.75582C10.6328 6.48817 10.8498 6.2712 11.1174 6.2712H16.6906C16.9582 6.2712 17.1752 6.48817 17.1752 6.75582V7.42218C17.1752 7.68983 16.9582 7.9068 16.6906 7.9068H11.1174C10.8498 7.9068 10.6328 7.68983 10.6328 7.42218V6.75582Z" fill="#2B2F36"/> -<path d="M3.5452 11.1174C3.5452 10.8498 3.76217 10.6328 4.02982 10.6328H16.6906C16.9582 10.6328 17.1752 10.8498 17.1752 11.1174V11.7838C17.1752 12.0514 16.9582 12.2684 16.6906 12.2684H4.02982C3.76217 12.2684 3.5452 12.0514 3.5452 11.7838V11.1174Z" fill="#2B2F36"/> -<path d="M3.5452 15.479C3.5452 15.2114 3.76217 14.9944 4.02982 14.9944H13.4194C13.687 14.9944 13.904 15.2114 13.904 15.479V16.1454C13.904 16.413 13.687 16.63 13.4194 16.63H4.02982C3.76217 16.63 3.5452 16.413 3.5452 16.1454V15.479Z" fill="#2B2F36"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/space_lock.svg b/frontend/resources/flowy_icons/16x/space_lock.svg index e5103e7de8..a048076b50 100644 --- a/frontend/resources/flowy_icons/16x/space_lock.svg +++ b/frontend/resources/flowy_icons/16x/space_lock.svg @@ -1,3 +1,3 @@ -<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M10.2465 5.5418V4.85902C10.2465 4.00116 10.0281 1.45801 6.99367 1.45801C3.84479 1.45801 3.74089 4.00116 3.74089 4.85902V5.5418L3.94766 5.52418L3.74201 5.54277C2.68377 5.83152 2.32812 6.67639 2.32812 8.50516V9.57461C2.32812 11.9274 2.91797 12.8326 4.82627 12.8326H9.16331C11.0716 12.8326 11.6615 11.9274 11.6615 9.57461V8.50516C11.6615 6.67639 11.3058 5.83152 10.2476 5.54277L10.0787 5.5275L10.2465 5.5418ZM5.042 4.85902V5.42523L8.94534 5.42513V4.85902C8.94534 3.51227 8.51146 2.68324 6.99367 2.68324C5.41979 2.68324 5.042 3.51227 5.042 4.85902ZM6.99115 10.4423C7.76434 10.4423 8.39115 9.81545 8.39115 9.04225C8.39115 8.26905 7.76434 7.64225 6.99115 7.64225C6.21795 7.64225 5.59115 8.26905 5.59115 9.04225C5.59115 9.81545 6.21795 10.4423 6.99115 10.4423Z" fill="#171717"/> -</svg> \ No newline at end of file +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M8.78714 4.75039V4.16515C8.78714 3.42984 8.6 1.25 5.99904 1.25C3.3 1.25 3.21094 3.42984 3.21094 4.16515V4.75039L3.38817 4.73529L3.2119 4.75123C2.30484 4.99873 2 5.7229 2 7.29041V8.20709C2 10.2238 2.50558 10.9996 4.14127 10.9996H7.85873C9.49442 10.9996 10 10.2238 10 8.20709V7.29041C10 5.7229 9.69516 4.99873 8.7881 4.75123L8.64331 4.73813L8.78714 4.75039ZM4.32618 4.16515V4.65048L7.6719 4.65039V4.16515C7.6719 3.0108 7.3 2.3002 5.99904 2.3002C4.65 2.3002 4.32618 3.0108 4.32618 4.16515ZM6.00078 8.95078C6.66352 8.95078 7.20078 8.41352 7.20078 7.75078C7.20078 7.08804 6.66352 6.55078 6.00078 6.55078C5.33804 6.55078 4.80078 7.08804 4.80078 7.75078C4.80078 8.41352 5.33804 8.95078 6.00078 8.95078Z" fill="#171717"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/star.svg b/frontend/resources/flowy_icons/16x/star.svg deleted file mode 100644 index f4d6f7ae3c..0000000000 --- a/frontend/resources/flowy_icons/16x/star.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_51_2)"> -<path d="M6.0536 3.49319C6.9196 1.93962 7.3526 1.16281 8.00004 1.16281C8.64748 1.16281 9.08048 1.93956 9.94648 3.49319L10.1705 3.89512C10.4167 4.33656 10.5397 4.55731 10.7315 4.703C10.9234 4.84862 11.1623 4.90269 11.6402 5.01081L12.0754 5.10925C13.757 5.48975 14.5979 5.68 14.798 6.32331C14.998 6.96662 14.4248 7.637 13.2783 8.97762L12.9817 9.3245C12.6559 9.7055 12.493 9.89594 12.4197 10.1316C12.3464 10.3673 12.371 10.6214 12.4203 11.1298L12.4651 11.5926C12.6385 13.3813 12.7252 14.2757 12.2014 14.6732C11.6776 15.0708 10.8903 14.7083 9.31573 13.9834L8.90835 13.7957C8.46092 13.5897 8.23723 13.4867 8.00004 13.4867C7.76285 13.4867 7.5391 13.5897 7.09173 13.7957L6.68435 13.9834C5.10973 14.7083 4.32242 15.0708 3.79867 14.6732C3.27492 14.2757 3.3616 13.3813 3.53492 11.5926L3.57973 11.1298C3.62904 10.6214 3.65367 10.3673 3.58035 10.1316C3.5071 9.89594 3.34417 9.7055 3.01835 9.3245L2.72179 8.97762C1.57529 7.637 1.00204 6.96662 1.2021 6.32331C1.4021 5.68 2.24298 5.48975 3.92473 5.10925L4.35979 5.01081C4.83773 4.90269 5.07667 4.84862 5.26854 4.703C5.46042 4.55737 5.58342 4.33656 5.82954 3.89512L6.0536 3.49319Z" stroke="black"/> -</g> -<defs> -<clipPath id="clip0_51_2"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg b/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg deleted file mode 100644 index aa071665c3..0000000000 --- a/frontend/resources/flowy_icons/16x/suggestion_insert_below.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3 4H14.25M3 6.5H14.25M9.25 9H14.25M9.25 11.5H14.25M7.625 10.25L6.29167 11.5833M7.625 10.25L6.29167 8.91666M7.625 10.25L4.625 10.2503C3.125 10.2503 3.125 9.81634 3.125 8.74967" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_align_center.svg b/frontend/resources/flowy_icons/16x/table_align_center.svg deleted file mode 100644 index a3e462df06..0000000000 --- a/frontend/resources/flowy_icons/16x/table_align_center.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.625 4.25H2.375" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.125 8H4.875" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M12.375 11.75H3.625" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_align_left.svg b/frontend/resources/flowy_icons/16x/table_align_left.svg deleted file mode 100644 index 0f71e7fc3c..0000000000 --- a/frontend/resources/flowy_icons/16x/table_align_left.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.625 4.25H2.375" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.875 8H2.375" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M11.125 11.75H2.375" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_align_right.svg b/frontend/resources/flowy_icons/16x/table_align_right.svg deleted file mode 100644 index de3efa902a..0000000000 --- a/frontend/resources/flowy_icons/16x/table_align_right.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M13.625 4.25H2.375" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.625 8H6.125" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.625 11.75H4.875" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_clear_content.svg b/frontend/resources/flowy_icons/16x/table_clear_content.svg deleted file mode 100644 index de1d42cabe..0000000000 --- a/frontend/resources/flowy_icons/16x/table_clear_content.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_6135_6661)"> -<path d="M8.92911 12.376C9.14841 12.5953 9.50398 12.5953 9.72328 12.376C9.94257 12.1567 9.94257 11.8011 9.72328 11.5818L8.92911 12.376ZM4.4183 6.27689C4.19901 6.05759 3.84346 6.05759 3.62417 6.27689C3.40487 6.49619 3.40487 6.85174 3.62417 7.07104L4.4183 6.27689ZM12.4658 8.04519L8.04495 12.466L8.83911 13.2602L13.2599 8.83936L12.4658 8.04519ZM3.53414 7.9552L7.95495 3.53438L7.16079 2.74025L2.74001 7.16103L3.53414 7.9552ZM3.53414 12.466C2.89772 11.8296 2.4612 11.3914 2.17682 11.0187C1.90233 10.6589 1.82303 10.4269 1.82303 10.2106H0.699951C0.699951 10.7712 0.933244 11.2402 1.28395 11.6999C1.62477 12.1466 2.12603 12.6461 2.74001 13.2602L3.53414 12.466ZM2.74001 7.16103C2.12603 7.77498 1.62477 8.2746 1.28395 8.72129C0.933244 9.18092 0.699951 9.65 0.699951 10.2106H1.82303C1.82303 9.99426 1.90233 9.7623 2.17682 9.40254C2.4612 9.02983 2.89772 8.59161 3.53414 7.9552L2.74001 7.16103ZM8.04495 12.466C7.40854 13.1024 6.97031 13.539 6.5976 13.8233C6.23785 14.0978 6.00585 14.1771 5.78956 14.1771V15.3002C6.35014 15.3002 6.81922 15.0669 7.27886 14.7162C7.72555 14.3754 8.22517 13.8741 8.83911 13.2602L8.04495 12.466ZM2.74001 13.2602C3.35398 13.8741 3.85356 14.3754 4.30026 14.7162C4.75992 15.0669 5.22896 15.3002 5.78956 15.3002V14.1771C5.57325 14.1771 5.34125 14.0978 4.98149 13.8233C4.60878 13.539 4.17057 13.1024 3.53414 12.466L2.74001 13.2602ZM12.4658 3.53438C13.1022 4.17081 13.5387 4.60902 13.8231 4.98174C14.0976 5.3415 14.1769 5.5735 14.1769 5.7898H15.3C15.3 5.22921 15.0667 4.76016 14.716 4.3005C14.3751 3.8538 13.8739 3.35422 13.2599 2.74025L12.4658 3.53438ZM13.2599 8.83936C13.8739 8.22541 14.3751 7.72579 14.716 7.27911C15.0667 6.81947 15.3 6.35039 15.3 5.7898H14.1769C14.1769 6.0061 14.0976 6.23809 13.8231 6.59785C13.5387 6.97056 13.1022 7.40878 12.4658 8.04519L13.2599 8.83936ZM13.2599 2.74025C12.6459 2.12628 12.1464 1.62502 11.6997 1.2842C11.24 0.933488 10.771 0.700195 10.2103 0.700195V1.82327C10.4266 1.82327 10.6586 1.90258 11.0184 2.17706C11.3911 2.46144 11.8293 2.89797 12.4658 3.53438L13.2599 2.74025ZM7.95495 3.53438C8.59136 2.89797 9.02959 2.46144 9.4023 2.17706C9.76206 1.90258 9.99401 1.82327 10.2103 1.82327V0.700195C9.64975 0.700195 9.18068 0.933488 8.72104 1.2842C8.27436 1.62502 7.77474 2.12628 7.16079 2.74025L7.95495 3.53438ZM9.72328 11.5818L4.4183 6.27689L3.62417 7.07104L8.92911 12.376L9.72328 11.5818Z" fill="#171717"/> -<path d="M5.7677 14.7979H14.8092" stroke="#171717" stroke-linecap="round"/> -</g> -<defs> -<clipPath id="clip0_6135_6661"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_distribute_columns_evenly.svg b/frontend/resources/flowy_icons/16x/table_distribute_columns_evenly.svg deleted file mode 100644 index fe7acf8c98..0000000000 --- a/frontend/resources/flowy_icons/16x/table_distribute_columns_evenly.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_6139_6635)"> -<rect width="16" height="16"/> -<path d="M1.33325 1.33301L1.33325 14.6663" stroke="#1F2329" stroke-linecap="round"/> -<path d="M14.6667 1.33301L14.6667 14.6663" stroke="#1F2329" stroke-linecap="round"/> -<rect x="4.33569" y="3.66699" width="3.66423" height="8.66667" stroke="#1F2329" stroke-linejoin="round"/> -<rect x="8" y="3.66699" width="3.66667" height="8.66667" stroke="#1F2329" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_header_column.svg b/frontend/resources/flowy_icons/16x/table_header_column.svg deleted file mode 100644 index 3ba5640bec..0000000000 --- a/frontend/resources/flowy_icons/16x/table_header_column.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.5 12.6667L2.5 3.33333C2.5 3.33333 2.5 2 3.83333 2H13.1667C13.1667 2 14.5 2 14.5 3.33333V12.6667C14.5 12.6667 14.5 14 13.1667 14H3.83333C3.83333 14 2.5 14 2.5 12.6667Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.35715 14V2" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M4.21428 14L4.21428 2" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.5 8H14.4996" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<rect x="2.50027" y="14" width="12" height="1.71429" rx="0.857143" transform="rotate(-90 2.50027 14)" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_header_row.svg b/frontend/resources/flowy_icons/16x/table_header_row.svg deleted file mode 100644 index a4d5c77606..0000000000 --- a/frontend/resources/flowy_icons/16x/table_header_row.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.33333 2H12.6667C12.6667 2 14 2 14 3.33333V12.6667C14 12.6667 14 14 12.6667 14H3.33333C3.33333 14 2 14 2 12.6667V3.33333C2 3.33333 2 2 3.33333 2Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2 8.85742H14" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2 3.71387H14" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8 2V13.9996" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -<rect x="2" y="2" width="12" height="1.71429" rx="0.857143" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_insert_above.svg b/frontend/resources/flowy_icons/16x/table_insert_above.svg deleted file mode 100644 index e3bcfe4ca6..0000000000 --- a/frontend/resources/flowy_icons/16x/table_insert_above.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8.375C1.68934 8.375 1.4375 8.62684 1.4375 8.9375L1.4375 12.6875C1.4375 14.2408 2.6967 15.5 4.25 15.5H11.75C13.3033 15.5 14.5625 14.2408 14.5625 12.6875V8.9375C14.5625 8.62684 14.3107 8.375 14 8.375H2ZM2.5625 9.5H13.4375V12.6875C13.4375 13.6195 12.682 14.375 11.75 14.375H4.25C3.31802 14.375 2.5625 13.6195 2.5625 12.6875L2.5625 9.5Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9.1925 8.9375C9.1925 8.62684 8.94066 8.375 8.63 8.375H2.5625L2.5625 4.46603C2.5625 3.9325 2.995 3.5 3.52853 3.5C3.83919 3.5 4.09103 3.24816 4.09103 2.9375C4.09103 2.62684 3.83919 2.375 3.52853 2.375C2.37368 2.375 1.4375 3.31118 1.4375 4.46603L1.4375 8.9375C1.4375 9.24816 1.68934 9.5 2 9.5H8.63C8.94066 9.5 9.1925 9.24816 9.1925 8.9375Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M6.8075 8.9375C6.8075 9.24816 7.05934 9.5 7.37 9.5H14C14.3107 9.5 14.5625 9.24816 14.5625 8.9375V4.46603C14.5625 3.31118 13.6263 2.375 12.4715 2.375C12.1608 2.375 11.909 2.62684 11.909 2.9375C11.909 3.24816 12.1608 3.5 12.4715 3.5C13.005 3.5 13.4375 3.9325 13.4375 4.46603V8.375H7.37C7.05934 8.375 6.8075 8.62684 6.8075 8.9375Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5625 2.9375C5.5625 3.24816 5.81434 3.5 6.125 3.5H7.4375V4.8125C7.4375 5.12316 7.68934 5.375 8 5.375C8.31066 5.375 8.5625 5.12316 8.5625 4.8125V3.5H9.875C10.1857 3.5 10.4375 3.24816 10.4375 2.9375C10.4375 2.62684 10.1857 2.375 9.875 2.375H8.5625V1.0625C8.5625 0.75184 8.31066 0.5 8 0.5C7.68934 0.5 7.4375 0.75184 7.4375 1.0625V2.375L6.125 2.375C5.81434 2.375 5.5625 2.62684 5.5625 2.9375Z" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_insert_below.svg b/frontend/resources/flowy_icons/16x/table_insert_below.svg deleted file mode 100644 index de1719edeb..0000000000 --- a/frontend/resources/flowy_icons/16x/table_insert_below.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M2 7.625C1.68934 7.625 1.4375 7.37316 1.4375 7.0625L1.4375 3.3125C1.4375 1.7592 2.6967 0.5 4.25 0.5H11.75C13.3033 0.5 14.5625 1.7592 14.5625 3.3125V7.0625C14.5625 7.37316 14.3107 7.625 14 7.625L2 7.625ZM2.5625 6.5H13.4375V3.3125C13.4375 2.38052 12.682 1.625 11.75 1.625H4.25C3.31802 1.625 2.5625 2.38052 2.5625 3.3125L2.5625 6.5Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9.1925 7.0625C9.1925 7.37316 8.94066 7.625 8.63 7.625H2.5625L2.5625 11.534C2.5625 12.0675 2.995 12.5 3.52853 12.5C3.83919 12.5 4.09103 12.7518 4.09103 13.0625C4.09103 13.3732 3.83919 13.625 3.52853 13.625C2.37368 13.625 1.4375 12.6888 1.4375 11.534L1.4375 7.0625C1.4375 6.75184 1.68934 6.5 2 6.5H8.63C8.94066 6.5 9.1925 6.75184 9.1925 7.0625Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M6.8075 7.0625C6.8075 6.75184 7.05934 6.5 7.37 6.5H14C14.3107 6.5 14.5625 6.75184 14.5625 7.0625V11.534C14.5625 12.6888 13.6263 13.625 12.4715 13.625C12.1608 13.625 11.909 13.3732 11.909 13.0625C11.909 12.7518 12.1608 12.5 12.4715 12.5C13.005 12.5 13.4375 12.0675 13.4375 11.534V7.625L7.37 7.625C7.05934 7.625 6.8075 7.37316 6.8075 7.0625Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M5.5625 13.0625C5.5625 12.7518 5.81434 12.5 6.125 12.5H7.4375V11.1875C7.4375 10.8768 7.68934 10.625 8 10.625C8.31066 10.625 8.5625 10.8768 8.5625 11.1875V12.5H9.875C10.1857 12.5 10.4375 12.7518 10.4375 13.0625C10.4375 13.3732 10.1857 13.625 9.875 13.625H8.5625V14.9375C8.5625 15.2482 8.31066 15.5 8 15.5C7.68934 15.5 7.4375 15.2482 7.4375 14.9375V13.625H6.125C5.81434 13.625 5.5625 13.3732 5.5625 13.0625Z" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_insert_left.svg b/frontend/resources/flowy_icons/16x/table_insert_left.svg deleted file mode 100644 index 47bbb0820c..0000000000 --- a/frontend/resources/flowy_icons/16x/table_insert_left.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.875 2C8.875 1.68934 9.12684 1.4375 9.4375 1.4375H13.1875C14.7408 1.4375 16 2.6967 16 4.25V11.75C16 13.3033 14.7408 14.5625 13.1875 14.5625H9.4375C9.12684 14.5625 8.875 14.3107 8.875 14V2ZM10 2.5625V13.4375H13.1875C14.1195 13.4375 14.875 12.682 14.875 11.75V4.25C14.875 3.31802 14.1195 2.5625 13.1875 2.5625H10Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9.4375 9.1925C9.12684 9.1925 8.875 8.94066 8.875 8.63V2.5625H4.96603C4.4325 2.5625 4 2.995 4 3.52853C4 3.83919 3.74816 4.09103 3.4375 4.09103C3.12684 4.09103 2.875 3.83919 2.875 3.52853C2.875 2.37368 3.81118 1.4375 4.96603 1.4375H9.4375C9.74816 1.4375 10 1.68934 10 2V8.63C10 8.94066 9.74816 9.1925 9.4375 9.1925Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M9.4375 6.8075C9.74816 6.8075 10 7.05934 10 7.37V14C10 14.3107 9.74816 14.5625 9.4375 14.5625H4.96603C3.81118 14.5625 2.875 13.6263 2.875 12.4715C2.875 12.1608 3.12684 11.909 3.4375 11.909C3.74816 11.909 4 12.1608 4 12.4715C4 13.005 4.4325 13.4375 4.96603 13.4375H8.875V7.37C8.875 7.05934 9.12684 6.8075 9.4375 6.8075Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.4375 5.5625C3.74816 5.5625 4 5.81434 4 6.125V7.4375H5.3125C5.62316 7.4375 5.875 7.68934 5.875 8C5.875 8.31066 5.62316 8.5625 5.3125 8.5625H4V9.875C4 10.1857 3.74816 10.4375 3.4375 10.4375C3.12684 10.4375 2.875 10.1857 2.875 9.875V8.5625H1.5625C1.25184 8.5625 1 8.31066 1 8C1 7.68934 1.25184 7.4375 1.5625 7.4375H2.875V6.125C2.875 5.81434 3.12684 5.5625 3.4375 5.5625Z" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_insert_right.svg b/frontend/resources/flowy_icons/16x/table_insert_right.svg deleted file mode 100644 index f754f516d0..0000000000 --- a/frontend/resources/flowy_icons/16x/table_insert_right.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.125 2C8.125 1.68934 7.87316 1.4375 7.5625 1.4375H3.8125C2.2592 1.4375 1 2.6967 1 4.25V11.75C1 13.3033 2.2592 14.5625 3.8125 14.5625H7.5625C7.87316 14.5625 8.125 14.3107 8.125 14V2ZM7 2.5625V13.4375H3.8125C2.88052 13.4375 2.125 12.682 2.125 11.75V4.25C2.125 3.31802 2.88052 2.5625 3.8125 2.5625H7Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5625 9.1925C7.87316 9.1925 8.125 8.94066 8.125 8.63V2.5625H12.034C12.5675 2.5625 13 2.995 13 3.52853C13 3.83919 13.2518 4.09103 13.5625 4.09103C13.8732 4.09103 14.125 3.83919 14.125 3.52853C14.125 2.37368 13.1888 1.4375 12.034 1.4375H7.5625C7.25184 1.4375 7 1.68934 7 2V8.63C7 8.94066 7.25184 9.1925 7.5625 9.1925Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5625 6.8075C7.25184 6.8075 7 7.05934 7 7.37V14C7 14.3107 7.25184 14.5625 7.5625 14.5625H12.034C13.1888 14.5625 14.125 13.6263 14.125 12.4715C14.125 12.1608 13.8732 11.909 13.5625 11.909C13.2518 11.909 13 12.1608 13 12.4715C13 13.005 12.5675 13.4375 12.034 13.4375H8.125V7.37C8.125 7.05934 7.87316 6.8075 7.5625 6.8075Z" fill="#171717"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5625 5.5625C13.2518 5.5625 13 5.81434 13 6.125V7.4375H11.6875C11.3768 7.4375 11.125 7.68934 11.125 8C11.125 8.31066 11.3768 8.5625 11.6875 8.5625H13V9.875C13 10.1857 13.2518 10.4375 13.5625 10.4375C13.8732 10.4375 14.125 10.1857 14.125 9.875V8.5625H15.4375C15.7482 8.5625 16 8.31066 16 8C16 7.68934 15.7482 7.4375 15.4375 7.4375H14.125V6.125C14.125 5.81434 13.8732 5.5625 13.5625 5.5625Z" fill="#171717"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_reorder_column.svg b/frontend/resources/flowy_icons/16x/table_reorder_column.svg deleted file mode 100644 index 96142c1ecc..0000000000 --- a/frontend/resources/flowy_icons/16x/table_reorder_column.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.5 4.5V11.5" stroke="#8F959E" stroke-linecap="round"/> -<path d="M9.5 4.5V11.5" stroke="#8F959E" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_reorder_row.svg b/frontend/resources/flowy_icons/16x/table_reorder_row.svg deleted file mode 100644 index 9bd9dc2c14..0000000000 --- a/frontend/resources/flowy_icons/16x/table_reorder_row.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.5 9.5H11.5" stroke="white" stroke-linecap="round"/> -<path d="M4.5 6.5L11.5 6.5" stroke="white" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/table_set_to_page_width.svg b/frontend/resources/flowy_icons/16x/table_set_to_page_width.svg deleted file mode 100644 index 0787942325..0000000000 --- a/frontend/resources/flowy_icons/16x/table_set_to_page_width.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2 8.00033L5 5.33366M2 8.00033L5 10.667M2 8.00033H14M14 8.00033L11 5.33366M14 8.00033L11 10.667" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/text.svg b/frontend/resources/flowy_icons/16x/text.svg index 1862ea1801..7befa5080f 100644 --- a/frontend/resources/flowy_icons/16x/text.svg +++ b/frontend/resources/flowy_icons/16x/text.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M15.2 13.292V10.8157M15.2 10.8157V8.33939M15.2 10.8157C15.2 12.1833 14.0913 13.292 12.7236 13.292C11.356 13.292 10.2473 12.1833 10.2473 10.8157C10.2473 9.44807 11.356 8.33939 12.7236 8.33939C14.0913 8.33939 15.2 9.44807 15.2 10.8157Z" stroke="#171717" stroke-width="1.15" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M8.25275 13.464L6.99032 9.94589L6.97947 9.94599H2.66533L2.65451 9.94589L1.39209 13.464C1.27052 13.8028 0.914247 13.9724 0.596318 13.8429C0.278388 13.7133 0.119201 13.3337 0.240762 12.9949L3.95892 2.63325C4.2619 1.78892 5.38293 1.78892 5.68591 2.63325L9.40407 12.9949C9.52563 13.3337 9.36644 13.7133 9.04851 13.8429C8.73058 13.9724 8.37431 13.8028 8.25275 13.464ZM4.82242 3.90443L3.12577 8.6326H6.51906L4.82242 3.90443Z" fill="#171717"/> +<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/> +<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/time.svg b/frontend/resources/flowy_icons/16x/time.svg index cb3f6ca112..634af3e361 100644 --- a/frontend/resources/flowy_icons/16x/time.svg +++ b/frontend/resources/flowy_icons/16x/time.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.8 8.00001C14.8 11.7536 11.7536 14.8 7.99995 14.8C4.24635 14.8 1.19995 11.7536 1.19995 8.00001C1.19995 4.24641 4.24635 1.20001 7.99995 1.20001C11.7536 1.20001 14.8 4.24641 14.8 8.00001Z" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.5227 10.1624L8.41466 8.90441C8.04746 8.68681 7.74826 8.16321 7.74826 7.73481V4.94681" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8 5V8L10 9" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/toast_checked_filled.svg b/frontend/resources/flowy_icons/16x/toast_checked_filled.svg deleted file mode 100644 index 6d43cf16c3..0000000000 --- a/frontend/resources/flowy_icons/16x/toast_checked_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10.6 13.8L8.45 11.65C8.26667 11.4667 8.03333 11.375 7.75 11.375C7.46667 11.375 7.23333 11.4667 7.05 11.65C6.86667 11.8333 6.775 12.0667 6.775 12.35C6.775 12.6333 6.86667 12.8667 7.05 13.05L9.9 15.9C10.1 16.1 10.3333 16.2 10.6 16.2C10.8667 16.2 11.1 16.1 11.3 15.9L16.95 10.25C17.1333 10.0667 17.225 9.83333 17.225 9.55C17.225 9.26667 17.1333 9.03333 16.95 8.85C16.7667 8.66667 16.5333 8.575 16.25 8.575C15.9667 8.575 15.7333 8.66667 15.55 8.85L10.6 13.8ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#3AC25C"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toast_close.svg b/frontend/resources/flowy_icons/16x/toast_close.svg deleted file mode 100644 index 9941c80abc..0000000000 --- a/frontend/resources/flowy_icons/16x/toast_close.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.10968 3.35641C3.9014 3.14813 3.56371 3.14813 3.35543 3.35641C3.14715 3.56468 3.14715 3.90237 3.35543 4.11065L7.24497 8.0002L3.35543 11.8897C3.14715 12.098 3.14715 12.4357 3.35543 12.644C3.56371 12.8523 3.9014 12.8523 4.10968 12.644L7.99922 8.75444L11.8888 12.644C12.097 12.8523 12.4347 12.8523 12.643 12.644C12.8513 12.4357 12.8513 12.098 12.643 11.8897L8.75347 8.0002L12.643 4.11065C12.8513 3.90237 12.8513 3.56468 12.643 3.35641C12.4347 3.14813 12.097 3.14813 11.8888 3.35641L7.99922 7.24595L4.10968 3.35641Z" fill="black" stroke="black" stroke-width="0.15" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toast_error_filled.svg b/frontend/resources/flowy_icons/16x/toast_error_filled.svg deleted file mode 100644 index bdf63223e2..0000000000 --- a/frontend/resources/flowy_icons/16x/toast_error_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16C13 15.7167 12.9042 15.4792 12.7125 15.2875C12.5208 15.0958 12.2833 15 12 15C11.7167 15 11.4792 15.0958 11.2875 15.2875C11.0958 15.4792 11 15.7167 11 16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 13C12.2833 13 12.5208 12.9042 12.7125 12.7125C12.9042 12.5208 13 12.2833 13 12V8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8V12C11 12.2833 11.0958 12.5208 11.2875 12.7125C11.4792 12.9042 11.7167 13 12 13ZM12 22C10.6167 22 9.31667 21.7375 8.1 21.2125C6.88333 20.6875 5.825 19.975 4.925 19.075C4.025 18.175 3.3125 17.1167 2.7875 15.9C2.2625 14.6833 2 13.3833 2 12C2 10.6167 2.2625 9.31667 2.7875 8.1C3.3125 6.88333 4.025 5.825 4.925 4.925C5.825 4.025 6.88333 3.3125 8.1 2.7875C9.31667 2.2625 10.6167 2 12 2C13.3833 2 14.6833 2.2625 15.9 2.7875C17.1167 3.3125 18.175 4.025 19.075 4.925C19.975 5.825 20.6875 6.88333 21.2125 8.1C21.7375 9.31667 22 10.6167 22 12C22 13.3833 21.7375 14.6833 21.2125 15.9C20.6875 17.1167 19.975 18.175 19.075 19.075C18.175 19.975 17.1167 20.6875 15.9 21.2125C14.6833 21.7375 13.3833 22 12 22Z" fill="#FB006D"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toast_warning_filled.svg b/frontend/resources/flowy_icons/16x/toast_warning_filled.svg deleted file mode 100644 index 5c60f1e009..0000000000 --- a/frontend/resources/flowy_icons/16x/toast_warning_filled.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.72503 21C2.5417 21 2.37503 20.9542 2.22503 20.8625C2.07503 20.7708 1.95837 20.65 1.87503 20.5C1.7917 20.35 1.74587 20.1875 1.73753 20.0125C1.7292 19.8375 1.77503 19.6667 1.87503 19.5L11.125 3.5C11.225 3.33333 11.3542 3.20833 11.5125 3.125C11.6709 3.04167 11.8334 3 12 3C12.1667 3 12.3292 3.04167 12.4875 3.125C12.6459 3.20833 12.775 3.33333 12.875 3.5L22.125 19.5C22.225 19.6667 22.2709 19.8375 22.2625 20.0125C22.2542 20.1875 22.2084 20.35 22.125 20.5C22.0417 20.65 21.925 20.7708 21.775 20.8625C21.625 20.9542 21.4584 21 21.275 21H2.72503ZM12 18C12.2834 18 12.5209 17.9042 12.7125 17.7125C12.9042 17.5208 13 17.2833 13 17C13 16.7167 12.9042 16.4792 12.7125 16.2875C12.5209 16.0958 12.2834 16 12 16C11.7167 16 11.4792 16.0958 11.2875 16.2875C11.0959 16.4792 11 16.7167 11 17C11 17.2833 11.0959 17.5208 11.2875 17.7125C11.4792 17.9042 11.7167 18 12 18ZM12 15C12.2834 15 12.5209 14.9042 12.7125 14.7125C12.9042 14.5208 13 14.2833 13 14V11C13 10.7167 12.9042 10.4792 12.7125 10.2875C12.5209 10.0958 12.2834 10 12 10C11.7167 10 11.4792 10.0958 11.2875 10.2875C11.0959 10.4792 11 10.7167 11 11V14C11 14.2833 11.0959 14.5208 11.2875 14.7125C11.4792 14.9042 11.7167 15 12 15Z" fill="#FF7E1E"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toggle_heading1.svg b/frontend/resources/flowy_icons/16x/toggle_heading1.svg deleted file mode 100644 index 8392acb665..0000000000 --- a/frontend/resources/flowy_icons/16x/toggle_heading1.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.18516 8.3457L0.738083 10.3849C0.444986 10.6292 0 10.4208 0 10.0392L0 5.96077C0 5.57924 0.444985 5.37082 0.738082 5.61507L3.18516 7.6543C3.40105 7.83421 3.40105 8.16579 3.18516 8.3457Z" fill="#333333"/> -<path d="M5.3999 4V7.99982M5.3999 7.99982V11.9996M5.3999 7.99982L10.7844 8.0002M10.7844 8.0002V4.00039M10.7844 8.0002V12M13.0923 7.39954L14.2461 6.5996V11.9994M14.2461 11.9994H13.0923M14.2461 11.9994H15.3999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toggle_heading2.svg b/frontend/resources/flowy_icons/16x/toggle_heading2.svg deleted file mode 100644 index 1b0721777e..0000000000 --- a/frontend/resources/flowy_icons/16x/toggle_heading2.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.18516 8.3457L0.738083 10.3849C0.444986 10.6292 0 10.4208 0 10.0392L0 5.96077C0 5.57924 0.444985 5.37082 0.738082 5.61507L3.18516 7.6543C3.40105 7.83421 3.40105 8.16579 3.18516 8.3457Z" fill="#333333"/> -<path d="M15.3999 11.8468H12.7086V11.0218C12.7086 10.5848 12.9554 10.1855 13.3462 9.99008L14.8275 9.24942C15.1615 9.08241 15.3999 8.75986 15.3999 8.38645C15.3999 8.13105 15.3791 7.88048 15.3393 7.63636C15.2702 7.21369 14.9114 6.90752 14.4845 6.87333C14.2792 6.85688 14.0716 6.84849 13.862 6.84849C13.47 6.84849 13.0848 6.87783 12.7086 6.93442M5.3999 4.15332V7.99849M5.3999 7.99849V11.8436M5.3999 7.99849L10.7827 7.99884M10.7827 7.99884V4.15369M10.7827 7.99884V11.844" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toggle_heading3.svg b/frontend/resources/flowy_icons/16x/toggle_heading3.svg deleted file mode 100644 index 0939a5e997..0000000000 --- a/frontend/resources/flowy_icons/16x/toggle_heading3.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.18516 8.3457L0.738083 10.3849C0.444986 10.6292 0 10.4208 0 10.0392L0 5.96077C0 5.57924 0.444985 5.37082 0.738082 5.61507L3.18516 7.6543C3.40105 7.83421 3.40105 8.16579 3.18516 8.3457Z" fill="#333333"/> -<path d="M14.967 9.34754C15.2394 9.72633 15.3999 10.191 15.3999 10.6932C15.3999 10.8654 15.381 11.0332 15.3453 11.1945C15.2663 11.551 14.9378 11.7807 14.5742 11.8142C14.3397 11.8357 14.1021 11.8467 13.862 11.8467C13.47 11.8467 13.0848 11.8173 12.7086 11.7607M14.967 9.34754C15.2394 8.96881 15.3999 8.50407 15.3999 8.0019C15.3999 7.8297 15.381 7.66197 15.3453 7.5006C15.2663 7.14406 14.9378 6.91438 14.5742 6.88098C14.3397 6.85945 14.1021 6.84844 13.862 6.84844C13.47 6.84844 13.0848 6.87777 12.7086 6.93437M14.967 9.34754H13.4775M5.3999 4.15332V7.99841M5.3999 7.99841V11.8435M5.3999 7.99841L10.7826 7.99877M10.7826 7.99877V4.15369M10.7826 7.99877V11.8438" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/toolbar_item_ai.svg b/frontend/resources/flowy_icons/16x/toolbar_item_ai.svg deleted file mode 100644 index e3828bddbd..0000000000 --- a/frontend/resources/flowy_icons/16x/toolbar_item_ai.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.9"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.03559 3.91001C8.24508 3.76431 8.49414 3.68622 8.74931 3.68622C9.00448 3.68622 9.25353 3.76431 9.46302 3.91001C9.67251 4.0557 9.83239 4.26202 9.92118 4.50124L9.92169 4.50261L11.4217 8.57292L11.4235 8.57578L11.4263 8.57758L15.498 10.0781C15.7373 10.1669 15.9436 10.3268 16.0893 10.5363C16.235 10.7458 16.3131 10.9948 16.3131 11.25C16.3131 11.5051 16.235 11.7542 16.0893 11.9637C15.9436 12.1732 15.7373 12.3331 15.498 12.4219L15.4967 12.4224L11.4264 13.9224L11.4235 13.9242L11.4217 13.927L9.92169 17.9973L9.92118 17.9987C9.83239 18.2379 9.6725 18.4443 9.46302 18.59C9.25353 18.7356 9.00448 18.8137 8.74931 18.8137C8.49414 18.8137 8.24508 18.7356 8.03559 18.59C7.82611 18.4443 7.66623 18.2379 7.57743 17.9987L7.57692 17.9973L6.07692 13.927L6.07511 13.9242L6.07383 13.9231L6.07228 13.9224L2.00194 12.4224L2.00057 12.4219C1.76134 12.3331 1.55503 12.1732 1.40933 11.9637C1.26364 11.7542 1.18555 11.5051 1.18555 11.25C1.18555 10.9948 1.26364 10.7458 1.40933 10.5363C1.55503 10.3268 1.76134 10.1669 2.00057 10.0781L2.00194 10.0776L6.07225 8.5776L6.07511 8.57578L6.07691 8.57295L7.57692 4.50261L7.57743 4.50124C7.66623 4.26202 7.82611 4.0557 8.03559 3.91001ZM10.2488 13.4948C10.3117 13.324 10.4109 13.169 10.5396 13.0403C10.6683 12.9116 10.8234 12.8124 10.9941 12.7495L15.0631 11.25L10.9942 9.7505C10.8234 9.68758 10.6683 9.58834 10.5396 9.45966C10.4109 9.33098 10.3117 9.17592 10.2488 9.00516L8.74931 4.93622L7.24983 9.00513C7.1869 9.17588 7.08767 9.33098 6.95899 9.45966C6.83031 9.58834 6.67524 9.68756 6.50449 9.75049L2.43555 11.25L6.50446 12.7495C6.67521 12.8124 6.83031 12.9116 6.95899 13.0403C7.08767 13.169 7.18689 13.324 7.24981 13.4948L8.74931 17.5637L10.2488 13.4948Z" fill="#8427E0"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7493 0.625C14.0945 0.625 14.3743 0.904822 14.3743 1.25V5C14.3743 5.34518 14.0945 5.625 13.7493 5.625C13.4042 5.625 13.1243 5.34518 13.1243 5V1.25C13.1243 0.904822 13.4042 0.625 13.7493 0.625Z" fill="#8427E0"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2493 3.125C11.2493 2.77982 11.5292 2.5 11.8743 2.5H15.6243C15.9695 2.5 16.2493 2.77982 16.2493 3.125C16.2493 3.47018 15.9695 3.75 15.6243 3.75H11.8743C11.5292 3.75 11.2493 3.47018 11.2493 3.125Z" fill="#8427E0"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4993 5C17.8445 5 18.1243 5.27982 18.1243 5.625V8.125C18.1243 8.47018 17.8445 8.75 17.4993 8.75C17.1542 8.75 16.8743 8.47018 16.8743 8.125V5.625C16.8743 5.27982 17.1542 5 17.4993 5Z" fill="#8427E0"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M15.6243 6.875C15.6243 6.52982 15.9042 6.25 16.2493 6.25H18.7493C19.0945 6.25 19.3743 6.52982 19.3743 6.875C19.3743 7.22018 19.0945 7.5 18.7493 7.5H16.2493C15.9042 7.5 15.6243 7.22018 15.6243 6.875Z" fill="#8427E0"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/16x/turninto.svg b/frontend/resources/flowy_icons/16x/turninto.svg deleted file mode 100644 index 3d1b62b697..0000000000 --- a/frontend/resources/flowy_icons/16x/turninto.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="-0.5 -0.5 16 16" id="Transfer-Line--Streamline-Mingcute.svg" height="16" width="16"><desc>Transfer Line Streamline Icon: https://streamlinehq.com</desc><g fill="none" fill-rule="nonzero"><path d="M15 0v15H0V0h15ZM7.870625 14.536249999999999l-0.006874999999999999 0.00125 -0.044375 0.021875000000000002 -0.0125 0.0025 -0.00875 -0.0025 -0.044375 -0.021875000000000002c-0.00625 -0.0025 -0.011875 -0.000625 -0.015 0.003125l-0.0025 0.00625 -0.010625 0.2675 0.003125 0.0125 0.00625 0.008125 0.065 0.04625 0.009375 0.0025 0.0075 -0.0025 0.065 -0.04625 0.0075 -0.01 0.0025 -0.010625 -0.010625 -0.266875c-0.00125 -0.00625 -0.005625 -0.010625 -0.010625 -0.01125Zm0.16562500000000002 -0.07062500000000001 -0.008125 0.00125 -0.115625 0.058124999999999996 -0.00625 0.00625 -0.001875 0.006874999999999999 0.01125 0.26875 0.003125 0.0075 0.005 0.004375 0.12562500000000001 0.058124999999999996c0.0075 0.0025 0.014374999999999999 0 0.018125000000000002 -0.005l0.0025 -0.00875 -0.02125 -0.38375c-0.001875 -0.0075 -0.00625 -0.0125 -0.0125 -0.013749999999999998Zm-0.44687499999999997 0.00125a0.014374999999999999 0.014374999999999999 0 0 0 -0.016875 0.00375l-0.00375 0.00875 -0.02125 0.38375c0 0.0075 0.004375 0.0125 0.010625 0.015l0.009375 -0.00125 0.12562500000000001 -0.058124999999999996 0.00625 -0.005 0.0025 -0.006874999999999999 0.010625 -0.26875 -0.001875 -0.0075 -0.00625 -0.00625 -0.11499999999999999 -0.057499999999999996Z" stroke-width="1"></path><path fill="#000000" d="M12.5 8.75a0.625 0.625 0 0 1 0.07312500000000001 1.245625L12.5 10H4.00875l1.433125 1.433125a0.625 0.625 0 0 1 -0.8250000000000001 0.935625l-0.05875 -0.051875000000000004 -2.39375 -2.39375c-0.415625 -0.41500000000000004 -0.14937499999999998 -1.114375 0.41437500000000005 -1.169375L2.650625 8.75H12.5Zm-2.941875 -6.0668750000000005a0.625 0.625 0 0 1 0.8250000000000001 -0.051875000000000004l0.05875 0.051875000000000004 2.39375 2.39375c0.415625 0.41500000000000004 0.14937499999999998 1.114375 -0.41437500000000005 1.169375l-0.07187500000000001 0.00375H2.5a0.625 0.625 0 0 1 -0.07312500000000001 -1.245625L2.5 5h8.49125l-1.433125 -1.433125a0.625 0.625 0 0 1 0 -0.8837499999999999Z" stroke-width="1"></path></g></svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/unable_select.svg b/frontend/resources/flowy_icons/16x/unable_select.svg deleted file mode 100644 index 0c661c79a4..0000000000 --- a/frontend/resources/flowy_icons/16x/unable_select.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="2.25" y="2.25" width="13.5" height="13.5" rx="4.5" fill="#171717" fill-opacity="0.2"/> -<path d="M6.75 9L8.56731 10.6875L11.8125 7.3125" stroke="white" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/unlock_page.svg b/frontend/resources/flowy_icons/16x/unlock_page.svg deleted file mode 100644 index 38f60dedb8..0000000000 --- a/frontend/resources/flowy_icons/16x/unlock_page.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M1.75 10.5C1.75 8.73225 1.75 7.84838 2.29918 7.29919C2.84835 6.75 3.73223 6.75 5.5 6.75H10.5C12.2677 6.75 13.1516 6.75 13.7008 7.29919C14.25 7.84838 14.25 8.73225 14.25 10.5C14.25 12.2677 14.25 13.1516 13.7008 13.7008C13.1516 14.25 12.2677 14.25 10.5 14.25H5.5C3.73223 14.25 2.84835 14.25 2.29918 13.7008C1.75 13.1516 1.75 12.2677 1.75 10.5Z" fill="#8F959E" stroke="#8F959E"/> -<path d="M4.25 6.75V5.5C4.25 3.42893 5.92893 1.75 8 1.75C9.11064 1.75 10.1085 2.23281 10.7951 3" stroke="#8F959E" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/upgrade_storage.svg b/frontend/resources/flowy_icons/16x/upgrade_storage.svg deleted file mode 100644 index ec0ff3a41b..0000000000 --- a/frontend/resources/flowy_icons/16x/upgrade_storage.svg +++ /dev/null @@ -1,15 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="#E8EAED"/> -<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="url(#paint0_linear_3646_112419)"/> -<path d="M6 16V14.5H14V16H6ZM9.25 13V5.875L7.0625 8.0625L6 7L10 3L14 7L12.9375 8.0625L10.75 5.875V13H9.25Z" fill="url(#paint1_linear_3646_112419)"/> -<defs> -<linearGradient id="paint0_linear_3646_112419" x1="10" y1="3" x2="10" y2="16" gradientUnits="userSpaceOnUse"> -<stop stop-color="#8132FF" stop-opacity="0"/> -<stop offset="1" stop-color="#8132FF"/> -</linearGradient> -<linearGradient id="paint1_linear_3646_112419" x1="7.54546" y1="14.8182" x2="15.0845" y2="11.9942" gradientUnits="userSpaceOnUse"> -<stop stop-color="#8032FF"/> -<stop offset="1" stop-color="#EF35FF"/> -</linearGradient> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/16x/url.svg b/frontend/resources/flowy_icons/16x/url.svg index 0b0d678074..f00f5c7aa2 100644 --- a/frontend/resources/flowy_icons/16x/url.svg +++ b/frontend/resources/flowy_icons/16x/url.svg @@ -1,10 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3140_1253)"> -<path d="M5.56032 10.4406L10.44 5.56085M3.73075 7.39055L2.51082 8.61048C1.16332 9.95798 1.16293 12.1429 2.51043 13.4904C3.85793 14.8379 6.0433 14.8375 7.3908 13.49L8.60943 12.2702M7.39002 3.73043L8.60995 2.5105C9.95745 1.16301 12.1419 1.16325 13.4894 2.51074C14.8369 3.85824 14.8368 6.04299 13.4893 7.39049L12.2701 8.61038" stroke="#171717" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/> -</g> -<defs> -<clipPath id="clip0_3140_1253"> -<rect width="16" height="16" fill="white"/> -</clipPath> -</defs> +<path d="M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632" stroke="#333333" stroke-width="0.9989" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/warning.svg b/frontend/resources/flowy_icons/16x/warning.svg deleted file mode 100644 index 7ac605dffc..0000000000 --- a/frontend/resources/flowy_icons/16x/warning.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<circle cx="8.00033" cy="7.99984" r="7.33333" fill="#FF811A"/> -<path d="M7.99967 4.6665C7.63148 4.6665 7.33301 4.96498 7.33301 5.33317V8.6665C7.33301 9.03469 7.63148 9.33317 7.99967 9.33317C8.36786 9.33317 8.66634 9.03469 8.66634 8.6665V5.33317C8.66634 4.96498 8.36786 4.6665 7.99967 4.6665Z" fill="white"/> -<path d="M7.99967 11.3332C8.36786 11.3332 8.66634 11.0347 8.66634 10.6665C8.66634 10.2983 8.36786 9.99984 7.99967 9.99984C7.63148 9.99984 7.33301 10.2983 7.33301 10.6665C7.33301 11.0347 7.63148 11.3332 7.99967 11.3332Z" fill="white"/> -</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_add_member.svg b/frontend/resources/flowy_icons/16x/workspace_add_member.svg deleted file mode 100644 index bd65361f8d..0000000000 --- a/frontend/resources/flowy_icons/16x/workspace_add_member.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9 9C11.0711 9 12.75 7.32107 12.75 5.25C12.75 3.17893 11.0711 1.5 9 1.5C6.92893 1.5 5.25 3.17893 5.25 5.25C5.25 7.32107 6.92893 9 9 9Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M2.55859 16.5C2.55859 13.5975 5.44609 11.25 9.00109 11.25C9.72109 11.25 10.4186 11.3475 11.0711 11.5275" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M16.5 13.5C16.5 13.74 16.47 13.9725 16.41 14.1975C16.3425 14.4975 16.2225 14.79 16.065 15.045C15.5475 15.915 14.595 16.5 13.5 16.5C12.7275 16.5 12.03 16.2075 11.505 15.7275C11.28 15.5325 11.085 15.3 10.935 15.045C10.6575 14.595 10.5 14.0625 10.5 13.5C10.5 12.69 10.8225 11.9475 11.3475 11.4075C11.895 10.845 12.66 10.5 13.5 10.5C14.385 10.5 15.1875 10.8825 15.7275 11.4975C16.2075 12.03 16.5 12.735 16.5 13.5Z" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M14.6178 13.4849H12.3828" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M13.5 12.3899V14.6324" stroke="#171717" stroke-width="1.125" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/ai_explain.svg b/frontend/resources/flowy_icons/20x/ai_explain.svg deleted file mode 100644 index f490472688..0000000000 --- a/frontend/resources/flowy_icons/20x/ai_explain.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.97294 13.8714C9.80435 13.8714 9.64266 13.8045 9.52344 13.6853C9.40423 13.566 9.33725 13.4043 9.33725 13.2358C9.33725 13.0672 9.40423 12.9055 9.52344 12.7863C9.64266 12.667 9.80435 12.6001 9.97294 12.6001C10.1415 12.6001 10.3032 12.667 10.4224 12.7863C10.5417 12.9055 10.6086 13.0672 10.6086 13.2358C10.6086 13.4043 10.5417 13.566 10.4224 13.6853C10.3032 13.8045 10.1415 13.8714 9.97294 13.8714ZM8.25444 8.86425C8.19161 8.8638 8.12958 8.85006 8.07243 8.82393C8.01529 8.79781 7.96431 8.75988 7.92287 8.71265C7.88142 8.66542 7.85044 8.60995 7.83196 8.5499C7.81348 8.48984 7.80791 8.42655 7.81563 8.36419C7.8445 8.117 7.89044 7.91488 7.95344 7.75782C8.06024 7.4975 8.22638 7.26573 8.43863 7.081C8.65412 6.88624 8.90661 6.73686 9.18107 6.64176C9.45925 6.54639 9.75149 6.49847 10.0456 6.50001C10.6581 6.50001 11.1678 6.68025 11.5755 7.04119C11.9833 7.40082 12.1876 7.88119 12.1876 8.48057C12.1876 8.75007 12.1351 8.99419 12.0301 9.21294C11.9259 9.43213 11.7037 9.70513 11.3638 10.0315C11.0229 10.3583 10.7981 10.5906 10.6865 10.7289C10.5709 10.8757 10.4853 11.0438 10.4345 11.2237C10.4039 11.3248 10.382 11.4442 10.3702 11.5816C10.3492 11.8134 10.1628 11.9972 9.93007 11.9972C9.86815 11.9968 9.807 11.9835 9.75051 11.9582C9.69402 11.9329 9.64343 11.896 9.60197 11.85C9.56052 11.8041 9.5291 11.7499 9.50974 11.6911C9.49037 11.6323 9.48348 11.5701 9.4895 11.5085C9.50481 11.3493 9.528 11.2106 9.56038 11.0924C9.62031 10.8671 9.71132 10.6703 9.83294 10.5018C9.95413 10.3329 10.1729 10.0853 10.4892 9.7585C10.8059 9.43213 11.0098 9.19413 11.1017 9.04669C11.1918 8.89838 11.2868 8.65732 11.2868 8.36419C11.2868 8.07107 11.1323 7.83263 10.9245 7.60688C10.7154 7.38113 10.4122 7.26782 10.0145 7.26782C9.23838 7.26782 8.79825 7.66725 8.695 8.46613C8.66569 8.691 8.48282 8.86425 8.25619 8.86425H8.25444Z" fill="#1F2329"/> -<circle cx="10" cy="10" r="6.5" stroke="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/anonymous_mode.svg b/frontend/resources/flowy_icons/20x/anonymous_mode.svg deleted file mode 100644 index bee519e54a..0000000000 --- a/frontend/resources/flowy_icons/20x/anonymous_mode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.6 14.0002C11.6 15.5466 12.8536 16.8002 14.4 16.8002C15.9464 16.8002 17.2 15.5466 17.2 14.0002C17.2 12.4538 15.9464 11.2002 14.4 11.2002C12.8536 11.2002 11.6 12.4538 11.6 14.0002ZM11.6 14.0002L11.0733 13.7368C10.3977 13.399 9.60232 13.399 8.92672 13.7368L8.4 14.0002M2 8.8002H18M3.6 8.8002L4.09104 6.83603C4.52758 5.08988 4.74585 4.21681 5.39687 3.7085C6.0479 3.2002 6.94784 3.2002 8.74776 3.2002H11.2522C13.0522 3.2002 13.9521 3.2002 14.6031 3.7085C15.2542 4.21681 15.4724 5.08988 15.909 6.83603L16.4 8.8002M8.4 14.0002C8.4 15.5466 7.1464 16.8002 5.6 16.8002C4.0536 16.8002 2.8 15.5466 2.8 14.0002C2.8 12.4538 4.0536 11.2002 5.6 11.2002C7.1464 11.2002 8.4 12.4538 8.4 14.0002Z" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/cloud_mode.svg b/frontend/resources/flowy_icons/20x/cloud_mode.svg deleted file mode 100644 index 5aaf68e3db..0000000000 --- a/frontend/resources/flowy_icons/20x/cloud_mode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7 6.2605C7.24603 3.23529 8.71429 2 11.9286 2H12.0317C15.5794 2 17 3.5042 17 7.2605V12.7395C17 16.4958 15.5794 18 12.0317 18H11.9286C8.73809 18 7.26984 16.7815 7.00794 13.8067M2 10H12M10 7L13.0033 9.99673L10 12.9935" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/embed_fullscreen.svg b/frontend/resources/flowy_icons/20x/embed_fullscreen.svg deleted file mode 100644 index b8b197fb13..0000000000 --- a/frontend/resources/flowy_icons/20x/embed_fullscreen.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.36379 11.6362L4 16M4 16H7.65133M4 16V12.3487M11.6362 8.36379L16 4M16 4H12.3487M16 4V7.65133" stroke="#B5BBD3" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/hide_password.svg b/frontend/resources/flowy_icons/20x/hide_password.svg deleted file mode 100644 index 2ebd274866..0000000000 --- a/frontend/resources/flowy_icons/20x/hide_password.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g id="Icon / Hide / M"> -<path id="Vector" d="M11.6674 11.7265C11.2147 12.1636 10.6085 12.4055 9.97928 12.4001C9.35004 12.3946 8.74812 12.1422 8.30317 11.6973C7.85821 11.2523 7.60582 10.6504 7.60035 10.0211C7.59488 9.3919 7.83677 8.78568 8.27393 8.33306M8.9867 4.46126C10.8501 4.23919 12.735 4.6331 14.3535 5.58284C15.972 6.53257 17.2353 7.98594 17.9502 9.72099C18.0169 9.90059 18.0169 10.0982 17.9502 10.2778C17.6563 10.9905 17.2677 11.6605 16.7951 12.2697M14.3832 14.3992C13.3221 15.0277 12.1381 15.4207 10.9117 15.5514C9.68527 15.6821 8.44508 15.5475 7.27529 15.1566C6.10549 14.7658 5.03345 14.1279 4.13191 13.2862C3.23037 12.4445 2.52042 11.4188 2.05025 10.2786C1.98358 10.099 1.98358 9.90139 2.05025 9.72179C2.75952 8.00176 4.00749 6.55815 5.60687 5.6076M2.00065 2.00058L17.9998 17.9998" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/20x/password_close.svg b/frontend/resources/flowy_icons/20x/password_close.svg deleted file mode 100644 index 52a44e1a8e..0000000000 --- a/frontend/resources/flowy_icons/20x/password_close.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g id="Icon"> -<path id="Vector" d="M16 4L4 16M4 4L16 16" stroke="#21232A" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_ai.svg b/frontend/resources/flowy_icons/20x/settings_page_ai.svg deleted file mode 100644 index d98a0c90fd..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_ai.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.03338 4.65784C8.37931 3.78072 9.62067 3.78072 9.9666 4.65785L11.0386 7.37598C11.1442 7.64377 11.3562 7.85575 11.624 7.96136L14.3422 9.03338C15.2193 9.37931 15.2193 10.6207 14.3422 10.9666L11.624 12.0386C11.3562 12.1442 11.1442 12.3562 11.0386 12.624L9.9666 15.3422C9.62066 16.2193 8.37931 16.2193 8.03338 15.3422L6.96136 12.624C6.85574 12.3562 6.64377 12.1442 6.37598 12.0386L3.65784 10.9666C2.78072 10.6207 2.78072 9.37931 3.65785 9.03338L6.37598 7.96136C6.64377 7.85574 6.85575 7.64377 6.96136 7.37598L8.03338 4.65784Z" stroke="#21232A"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2788 2.18536C15.1765 1.93826 14.8264 1.9382 14.7239 2.18526L14.5912 2.5053C14.3879 2.99548 13.9983 3.3849 13.5079 3.58798L13.1854 3.72156C12.9382 3.82396 12.9382 4.17415 13.1855 4.27647L13.5049 4.40864C13.9962 4.61192 14.3865 5.00222 14.5896 5.49348L14.7224 5.81445C14.8247 6.0618 15.1751 6.06186 15.2774 5.81455L15.4093 5.49609C15.6125 5.00523 16.0025 4.61524 16.4934 4.41202L16.8158 4.27854C17.063 4.1762 17.0631 3.8261 16.8159 3.72367L16.4957 3.59096C16.0053 3.38775 15.6156 2.99814 15.4124 2.50778L15.2788 2.18536ZM15.2788 14.1854C15.1765 13.9383 14.8264 13.9382 14.7239 14.1853L14.5912 14.5053C14.3879 14.9955 13.9983 15.3849 13.5079 15.588L13.1854 15.7216C12.9382 15.824 12.9382 16.1742 13.1855 16.2765L13.5049 16.4086C13.9962 16.6119 14.3865 17.0022 14.5896 17.4935L14.7224 17.8145C14.8247 18.0618 15.1751 18.0619 15.2774 17.8145L15.4093 17.4961C15.6125 17.0052 16.0025 16.6152 16.4934 16.412L16.8158 16.2785C17.063 16.1762 17.0631 15.8261 16.8159 15.7237L16.4957 15.591C16.0053 15.3878 15.6156 14.9981 15.4124 14.5078L15.2788 14.1854Z" fill="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_bell.svg b/frontend/resources/flowy_icons/20x/settings_page_bell.svg deleted file mode 100644 index 57031d1f90..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_bell.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.7246 8.3967V7.90348C14.7246 5.19536 12.6094 3 10.0002 3C7.39099 3 5.2758 5.19536 5.2758 7.90348V8.3967C5.2758 8.98864 5.107 9.56726 4.79066 10.0598L4.01545 11.2666C3.30737 12.369 3.84793 13.8674 5.07945 14.216C8.30111 15.128 11.6993 15.128 14.9209 14.216C16.1525 13.8674 16.693 12.369 15.9849 11.2666L15.2097 10.0598C14.8934 9.56726 14.7246 8.98864 14.7246 8.3967Z" stroke="#21232A"/> -<path d="M6.85059 14.9001C7.30911 16.1235 8.54631 17.0001 10.0006 17.0001C11.4548 17.0001 12.6921 16.1235 13.1506 14.9001" stroke="#21232A" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_cloud.svg b/frontend/resources/flowy_icons/20x/settings_page_cloud.svg deleted file mode 100644 index 44c20bb51b..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_cloud.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.6667 7.95621C12.0837 7.81314 12.5325 7.73529 13 7.73529C13.4583 7.73529 13.8985 7.81009 14.3086 7.9478M14.3086 7.9478C15.8751 8.47391 17 9.91826 17 11.6176C17 13.7618 15.2091 15.5 13 15.5H6C4.34315 15.5 3 14.1964 3 12.5882C3 10.9801 4.34315 9.67646 6 9.67646C6.19888 9.67646 6.39325 9.69523 6.58131 9.73112M14.3086 7.9478C14.086 6.00818 12.3911 4.5 10.3333 4.5C8.1242 4.5 6.33333 6.23819 6.33333 8.38235C6.33333 8.85662 6.42094 9.31099 6.58131 9.73112M6.58131 9.73112C6.97641 9.8064 7.3437 9.95696 7.66667 10.1668" stroke="#21232A" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg b/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg deleted file mode 100644 index e1c64ee509..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_credit_card.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 13H6M3 8.70001H17M3 10C3 7.40727 3 6.11092 3.8201 5.30545C4.64021 4.5 5.96013 4.5 8.6 4.5H11.4C14.0398 4.5 15.3598 4.5 16.1799 5.30545C17 6.11092 17 7.40727 17 10C17 12.5927 17 13.8891 16.1799 14.6945C15.3598 15.5 14.0398 15.5 11.4 15.5H8.6C5.96013 15.5 4.64021 15.5 3.8201 14.6945C3 13.8891 3 12.5927 3 10Z" stroke="#21232A" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_database.svg b/frontend/resources/flowy_icons/20x/settings_page_database.svg deleted file mode 100644 index bfbae5f8fe..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_database.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.5 5.8V14.2C4.5 15.7464 6.96242 17 9.99998 17C13.0375 17 15.5 15.7464 15.5 14.2V5.8M4.5 5.8C4.5 4.2536 6.96242 3 9.99998 3C13.0375 3 15.5 4.2536 15.5 5.8M4.5 5.8C4.5 7.34639 6.96242 8.6 9.99998 8.6C13.0375 8.6 15.5 7.34639 15.5 5.8M15.5 10C15.5 11.5464 13.0375 12.8 9.99998 12.8C6.96242 12.8 4.5 11.5464 4.5 10" stroke="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_earth.svg b/frontend/resources/flowy_icons/20x/settings_page_earth.svg deleted file mode 100644 index 0a205592b4..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_earth.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3M10 17C11.6569 17 13 13.866 13 10C13 6.13401 11.6569 3 10 3M10 17C8.34315 17 7 13.866 7 10C7 6.13401 8.34315 3 10 3M3 10.025H17" stroke="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg b/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg deleted file mode 100644 index 92efc30142..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_keyboard.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7 7.25C7 7.66421 6.66421 8 6.25 8C5.83579 8 5.5 7.66421 5.5 7.25C5.5 6.83579 5.83579 6.5 6.25 6.5C6.66421 6.5 7 6.83579 7 7.25Z" fill="#21232A"/> -<path d="M9.5 7.25C9.5 7.66421 9.16422 8 8.75 8C8.33577 8 8 7.66421 8 7.25C8 6.83579 8.33577 6.5 8.75 6.5C9.16422 6.5 9.5 6.83579 9.5 7.25Z" fill="#21232A"/> -<path d="M12 7.25C12 7.66421 11.6642 8 11.25 8C10.8358 8 10.5 7.66421 10.5 7.25C10.5 6.83579 10.8358 6.5 11.25 6.5C11.6642 6.5 12 6.83579 12 7.25Z" fill="#21232A"/> -<path d="M14.5 7.25C14.5 7.66421 14.1642 8 13.75 8C13.3358 8 13 7.66421 13 7.25C13 6.83579 13.3358 6.5 13.75 6.5C14.1642 6.5 14.5 6.83579 14.5 7.25Z" fill="#21232A"/> -<path d="M7 9.75C7 10.1642 6.66421 10.5 6.25 10.5C5.83579 10.5 5.5 10.1642 5.5 9.75C5.5 9.33579 5.83579 9 6.25 9C6.66421 9 7 9.33579 7 9.75Z" fill="#21232A"/> -<path d="M9.5 9.75C9.5 10.1642 9.16422 10.5 8.75 10.5C8.33577 10.5 8 10.1642 8 9.75C8 9.33579 8.33577 9 8.75 9C9.16422 9 9.5 9.33579 9.5 9.75Z" fill="#21232A"/> -<path d="M12 9.75C12 10.1642 11.6642 10.5 11.25 10.5C10.8358 10.5 10.5 10.1642 10.5 9.75C10.5 9.33579 10.8358 9 11.25 9C11.6642 9 12 9.33579 12 9.75Z" fill="#21232A"/> -<path d="M14.5 9.75C14.5 10.1642 14.1642 10.5 13.75 10.5C13.3358 10.5 13 10.1642 13 9.75C13 9.33579 13.3358 9 13.75 9C14.1642 9 14.5 9.33579 14.5 9.75Z" fill="#21232A"/> -<path d="M7 13H13M3 9.21429C3 6.99195 3 5.88078 3.61508 5.19039C4.23015 4.5 5.2201 4.5 7.2 4.5H12.8C14.7799 4.5 15.7698 4.5 16.3849 5.19039C17 5.88078 17 6.99195 17 9.21429V10.7857C17 13.008 17 14.1192 16.3849 14.8096C15.7698 15.5 14.7799 15.5 12.8 15.5H7.2C5.2201 15.5 4.23015 15.5 3.61508 14.8096C3 14.1192 3 13.008 3 10.7857V9.21429Z" stroke="#21232A" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_plan.svg b/frontend/resources/flowy_icons/20x/settings_page_plan.svg deleted file mode 100644 index 9792bd41c4..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_plan.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7 11.5C6.72386 11.5 6.5 11.7239 6.5 12C6.5 12.2761 6.72386 12.5 7 12.5V11.5ZM13 12.5C13.2761 12.5 13.5 12.2761 13.5 12C13.5 11.7239 13.2761 11.5 13 11.5V12.5ZM7 13.5C6.72386 13.5 6.5 13.7239 6.5 14C6.5 14.2761 6.72386 14.5 7 14.5V13.5ZM10 14.5C10.2761 14.5 10.5 14.2761 10.5 14C10.5 13.7239 10.2761 13.5 10 13.5V14.5ZM15.4142 5.58468L15.7678 5.23114L15.7678 5.23114L15.4142 5.58468ZM15.4142 16.4141L15.0607 16.0606L15.0606 16.0606L15.4142 16.4141ZM4.58579 16.4141L4.93936 16.0606L4.93934 16.0606L4.58579 16.4141ZM4.58579 5.58468L4.23222 5.23115L4.23222 5.23115L4.58579 5.58468ZM7 12.5H13V11.5H7V12.5ZM7 14.5H10V13.5H7V14.5ZM12.6639 5.49999C13.3929 5.50405 13.9096 5.52299 14.3017 5.59503C14.6796 5.66444 14.8983 5.7759 15.0606 5.93822L15.7678 5.23114C15.4178 4.88116 14.9878 4.70432 14.4824 4.61148C13.9914 4.52128 13.3905 4.50402 12.6695 4.50001L12.6639 5.49999ZM15.0606 5.93822C15.2452 6.12276 15.3655 6.38185 15.4312 6.87109C15.4989 7.37471 15.5 8.04219 15.5 8.99922H16.5C16.5 8.07046 16.5011 7.32359 16.4223 6.73785C16.3416 6.13774 16.169 5.63245 15.7678 5.23114L15.0606 5.93822ZM15.5 8.99922V12.9996H16.5V8.99922H15.5ZM15.5 12.9996C15.5 13.9566 15.4989 14.6241 15.4312 15.1278C15.3655 15.617 15.2452 15.8761 15.0607 16.0606L15.7678 16.7677C16.169 16.3664 16.3416 15.8611 16.4223 15.261C16.5011 14.6752 16.5 13.9284 16.5 12.9996H15.5ZM15.0606 16.0606C14.8761 16.2451 14.6171 16.3655 14.1279 16.4312C13.6243 16.4989 12.9569 16.5 12 16.5V17.5C12.9287 17.5 13.6755 17.5011 14.2612 17.4223C14.8612 17.3416 15.3665 17.169 15.7678 16.7677L15.0606 16.0606ZM12 16.5H8V17.5H12V16.5ZM8 16.5C7.04305 16.5 6.37565 16.4989 5.87209 16.4312C5.38292 16.3655 5.12387 16.2451 4.93936 16.0606L4.23221 16.7677C4.63349 17.169 5.13874 17.3416 5.73882 17.4223C6.32452 17.5011 7.07133 17.5 8 17.5V16.5ZM4.93934 16.0606C4.75484 15.8761 4.63454 15.617 4.56877 15.1278C4.50106 14.6241 4.5 13.9566 4.5 12.9996H3.5C3.5 13.9284 3.49894 14.6752 3.57768 15.261C3.65836 15.8611 3.83094 16.3664 4.23223 16.7677L4.93934 16.0606ZM4.5 12.9996V8.99922H3.5V12.9996H4.5ZM4.5 8.99922C4.5 8.04219 4.50106 7.37471 4.56877 6.87109C4.63454 6.38184 4.75484 6.12275 4.93936 5.93822L4.23222 5.23115C3.83095 5.63246 3.65836 6.13774 3.57768 6.73786C3.49894 7.32359 3.5 8.07046 3.5 8.99922H4.5ZM4.93936 5.93822C5.10166 5.7759 5.32037 5.66444 5.69824 5.59503C6.0904 5.52299 6.60712 5.50405 7.33612 5.49999L7.33055 4.50001C6.60953 4.50402 6.0086 4.52128 5.51756 4.61148C5.01222 4.70432 4.58217 4.88116 4.23222 5.23115L4.93936 5.93822ZM7.5 4.125C7.5 3.77982 7.77982 3.5 8.125 3.5V2.5C7.22754 2.5 6.5 3.22754 6.5 4.125H7.5ZM8.125 3.5H11.875V2.5H8.125V3.5ZM11.875 3.5C12.2202 3.5 12.5 3.77983 12.5 4.125H13.5C13.5 3.22753 12.7724 2.5 11.875 2.5V3.5ZM12.5 4.125V4.875H13.5V4.125H12.5ZM12.5 4.875C12.5 5.22017 12.2202 5.5 11.875 5.5V6.5C12.7724 6.5 13.5 5.77247 13.5 4.875H12.5ZM11.875 5.5H8.125V6.5H11.875V5.5ZM8.125 5.5C7.77982 5.5 7.5 5.22018 7.5 4.875H6.5C6.5 5.77246 7.22754 6.5 8.125 6.5V5.5ZM7.5 4.875V4.125H6.5V4.875H7.5Z" fill="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_user.svg b/frontend/resources/flowy_icons/20x/settings_page_user.svg deleted file mode 100644 index 94968ff06b..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_user.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.2 5.8C7.2 6.54261 7.495 7.2548 8.0201 7.7799C8.5452 8.305 9.25739 8.6 10 8.6C10.7426 8.6 11.4548 8.305 11.9799 7.7799C12.505 7.2548 12.8 6.54261 12.8 5.8C12.8 5.05739 12.505 4.3452 11.9799 3.8201C11.4548 3.295 10.7426 3 10 3C9.25739 3 8.5452 3.295 8.0201 3.8201C7.495 4.3452 7.2 5.05739 7.2 5.8Z" stroke="#21232A"/> -<path d="M16 14C16 15.6569 16 17 10 17C4 17 4 15.6569 4 14C4 12.3431 6.68629 11 10 11C13.3137 11 16 12.3431 16 14Z" stroke="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_users.svg b/frontend/resources/flowy_icons/20x/settings_page_users.svg deleted file mode 100644 index eb65bf7192..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_users.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.5789 8.15789C13.7998 8.15789 14.7895 7.16821 14.7895 5.94737C14.7895 4.72653 13.7998 3.73684 12.5789 3.73684M14.7895 11.8421C16.082 12.1256 17 12.8434 17 13.6842C17 14.4426 16.2531 15.1011 15.1579 15.4308M5.21053 5.94737C5.21053 6.72906 5.52105 7.47873 6.07379 8.03147C6.62653 8.58421 7.3762 8.89474 8.15789 8.89474C8.93959 8.89474 9.68926 8.58421 10.242 8.03147C10.7947 7.47873 11.1053 6.72906 11.1053 5.94737C11.1053 5.16568 10.7947 4.416 10.242 3.86326C9.68926 3.31053 8.93959 3 8.15789 3C7.3762 3 6.62653 3.31053 6.07379 3.86326C5.52105 4.416 5.21053 5.16568 5.21053 5.94737ZM3 14.0526C3 14.8343 3.54342 15.584 4.51071 16.1367C5.47801 16.6895 6.78994 17 8.15789 17C9.52585 17 10.8378 16.6895 11.8051 16.1367C12.7724 15.584 13.3158 14.8343 13.3158 14.0526C13.3158 13.2709 12.7724 12.5213 11.8051 11.9685C10.8378 11.4158 9.52585 11.1053 8.15789 11.1053C6.78994 11.1053 5.47801 11.4158 4.51071 11.9685C3.54342 12.5213 3 13.2709 3 14.0526Z" stroke="#21232A" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/settings_page_workspace.svg b/frontend/resources/flowy_icons/20x/settings_page_workspace.svg deleted file mode 100644 index e9a6eb9a10..0000000000 --- a/frontend/resources/flowy_icons/20x/settings_page_workspace.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3 9.875H17M7.375 10V3.5M12.375 16.5V10M3 10C3 7.40727 3 5.61092 3.8201 4.80545C4.64021 4 5.96013 4 8.6 4H11.4C14.0398 4 15.3598 4 16.1799 4.80545C17 5.61092 17 7.40727 17 10C17 13.0927 17 14.3891 16.1799 15.1945C15.3598 16 14.0398 16 11.4 16H8.6C5.96013 16 4.64021 16 3.8201 15.1945C3 14.3891 3 13.0927 3 10Z" stroke="#21232A"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/show_password.svg b/frontend/resources/flowy_icons/20x/show_password.svg deleted file mode 100644 index ac8d092b37..0000000000 --- a/frontend/resources/flowy_icons/20x/show_password.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.05025 10.2786C1.98358 10.099 1.98358 9.90141 2.05025 9.72181C2.69957 8.14737 3.80177 6.80119 5.2171 5.85392C6.63243 4.90666 8.29717 4.40097 10.0002 4.40097C11.7033 4.40097 13.3681 4.90666 14.7834 5.85392C16.1987 6.80119 17.3009 8.14737 17.9502 9.72181C18.0169 9.90141 18.0169 10.099 17.9502 10.2786C17.3009 11.853 16.1987 13.1992 14.7834 14.1465C13.3681 15.0937 11.7033 15.5994 10.0002 15.5994C8.29717 15.5994 6.63243 15.0937 5.2171 14.1465C3.80177 13.1992 2.69957 11.853 2.05025 10.2786Z" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M10.0002 12.4001C11.3257 12.4001 12.4001 11.3256 12.4001 10.0002C12.4001 8.67478 11.3257 7.60032 10.0002 7.60032C8.67483 7.60032 7.60036 8.67478 7.60036 10.0002C7.60036 11.3256 8.67483 12.4001 10.0002 12.4001Z" stroke="#6F748C" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/sign_in_settings.svg b/frontend/resources/flowy_icons/20x/sign_in_settings.svg deleted file mode 100644 index 5d88d23086..0000000000 --- a/frontend/resources/flowy_icons/20x/sign_in_settings.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.40014 10C8.40014 10.557 8.62139 11.0911 9.01522 11.4849C9.40905 11.8788 9.94319 12.1 10.5001 12.1C11.0571 12.1 11.5912 11.8788 11.9851 11.4849C12.3789 11.0911 12.6001 10.557 12.6001 10C12.6001 9.44305 12.3789 8.9089 11.9851 8.51508C11.5912 8.12125 11.0571 7.9 10.5001 7.9C9.94319 7.9 9.40905 8.12125 9.01522 8.51508C8.62139 8.9089 8.40014 9.44305 8.40014 10Z" stroke="#6F748C"/> -<path d="M11.7359 3.10657C11.4786 3 11.1525 3 10.5001 3C9.84781 3 9.52169 3 9.26437 3.10657C8.92134 3.24866 8.6488 3.52121 8.50671 3.86424C8.44184 4.02084 8.41645 4.20295 8.40652 4.46859C8.39193 4.85898 8.19173 5.22032 7.8534 5.41565C7.51509 5.61097 7.10206 5.60368 6.75668 5.42113C6.52166 5.29691 6.35125 5.22783 6.1832 5.20571C5.81507 5.15725 5.44277 5.257 5.1482 5.48304C4.92726 5.65257 4.76418 5.93503 4.43803 6.49995C4.11187 7.06488 3.94879 7.34734 3.91244 7.62344C3.86398 7.99156 3.96373 8.36386 4.18977 8.65845C4.29294 8.79292 4.43793 8.9059 4.66298 9.0473C4.9938 9.2552 5.20667 9.60933 5.20665 10C5.20663 10.3907 4.99377 10.7447 4.66297 10.9526C4.4379 11.094 4.29288 11.2071 4.1897 11.3416C3.96366 11.6361 3.86391 12.0084 3.91237 12.3765C3.94872 12.6526 4.1118 12.9351 4.43796 13.5C4.76412 14.0649 4.9272 14.3474 5.14813 14.5169C5.4427 14.7429 5.815 14.8427 6.18313 14.7942C6.35117 14.7721 6.52157 14.703 6.75657 14.5788C7.10197 14.3963 7.51504 14.389 7.85337 14.5843C8.19171 14.7797 8.39193 15.141 8.40652 15.5315C8.41646 15.7971 8.44184 15.9792 8.50671 16.1358C8.6488 16.4788 8.92134 16.7514 9.26437 16.8935C9.52169 17 9.84781 17 10.5001 17C11.1525 17 11.4786 17 11.7359 16.8935C12.0789 16.7514 12.3515 16.4788 12.4935 16.1358C12.5584 15.9792 12.5838 15.7971 12.5938 15.5314C12.6083 15.141 12.8085 14.7797 13.1468 14.5843C13.4852 14.3889 13.8982 14.3963 14.2437 14.5788C14.4787 14.703 14.649 14.772 14.817 14.7942C15.1852 14.8427 15.5575 14.7429 15.8521 14.5169C16.073 14.3474 16.2361 14.0649 16.5622 13.4999C16.8884 12.935 17.0515 12.6526 17.0878 12.3765C17.1363 12.0084 17.0365 11.636 16.8105 11.3415C16.7073 11.207 16.5623 11.094 16.3372 10.9526C16.0065 10.7447 15.7936 10.3906 15.7936 9.99993C15.7936 9.60926 16.0065 9.25527 16.3372 9.04744C16.5624 8.90597 16.7074 8.79299 16.8106 8.65845C17.0366 8.36391 17.1364 7.99161 17.0879 7.62348C17.0516 7.34739 16.8885 7.06492 16.5623 6.5C16.2362 5.93508 16.0731 5.65262 15.8521 5.48309C15.5576 5.25705 15.1852 5.1573 14.8171 5.20576C14.6491 5.22788 14.4787 5.29695 14.2437 5.42116C13.8983 5.60371 13.4852 5.61101 13.1469 5.41567C12.8085 5.22034 12.6083 4.85896 12.5938 4.46856C12.5838 4.20294 12.5584 4.02083 12.4935 3.86424C12.3515 3.52121 12.0789 3.24866 11.7359 3.10657Z" stroke="#6F748C"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/slash_menu_image.svg b/frontend/resources/flowy_icons/20x/slash_menu_image.svg deleted file mode 100644 index f5b7917ad3..0000000000 --- a/frontend/resources/flowy_icons/20x/slash_menu_image.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.1875 10C2.1875 6.31715 2.1875 4.47573 3.33162 3.33162C4.47573 2.1875 6.31715 2.1875 10 2.1875C13.6828 2.1875 15.5243 2.1875 16.6684 3.33162C17.8125 4.47573 17.8125 6.31715 17.8125 10C17.8125 13.6828 17.8125 15.5243 16.6684 16.6684C15.5243 17.8125 13.6828 17.8125 10 17.8125C6.31715 17.8125 4.47573 17.8125 3.33162 16.6684C2.1875 15.5243 2.1875 13.6828 2.1875 10Z" stroke="black" stroke-width="1.25"/> -<path d="M11.5625 6.875C11.5625 7.2894 11.7271 7.68683 12.0201 7.97985C12.3132 8.27288 12.7106 8.4375 13.125 8.4375C13.5394 8.4375 13.9368 8.27288 14.2299 7.97985C14.5229 7.68683 14.6875 7.2894 14.6875 6.875C14.6875 6.4606 14.5229 6.06317 14.2299 5.77015C13.9368 5.47712 13.5394 5.3125 13.125 5.3125C12.7106 5.3125 12.3132 5.47712 12.0201 5.77015C11.7271 6.06317 11.5625 6.4606 11.5625 6.875Z" stroke="black" stroke-width="1.25"/> -<path d="M2.1875 10.3907L3.55593 9.19334C4.26786 8.57045 5.34084 8.60616 6.00976 9.27506L9.36109 12.6264C9.89797 13.1633 10.7431 13.2365 11.3644 12.7999L11.5973 12.6362C12.4912 12.008 13.7007 12.0808 14.5129 12.8117L17.0312 15.0782" stroke="black" stroke-width="1.25" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg deleted file mode 100644 index dd0390d2d5..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_ai_improve_writing.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.55364 2.82613L5.01065 2.63676L4.55364 2.82613C4.85837 3.56156 5.44271 4.14587 6.17817 4.45062L6.30047 4.5013L6.17547 4.55305C5.43925 4.85782 4.85435 5.44268 4.5496 6.17887L5.01158 6.37011L4.5496 6.17887L4.49997 6.29877L4.4491 6.17577C4.1444 5.43898 3.55912 4.85364 2.82235 4.54879L2.70125 4.49869L2.82624 4.44692C3.56159 4.14236 4.14595 3.55834 4.45086 2.82318L4.50166 2.70069L4.55364 2.82613Z" fill="#1F2329" stroke="#1F2329"/> -<path d="M11.0234 5.71096C11.2876 7.34532 12.6641 8.59477 14.3725 8.76058M11.8628 4.85217L4.64726 12.1683C4.45676 12.3637 4.2724 12.7487 4.23553 13.0151L4.00815 14.9337C3.92827 15.6265 4.44447 16.1003 5.15731 15.9818L7.13608 15.6562C7.41262 15.6088 7.79977 15.4134 7.99027 15.212L15.2058 7.89587C16.0784 7.00763 16.4717 5.99504 15.1136 4.75742C13.7617 3.53165 12.7354 3.96393 11.8628 4.85217Z" stroke="#1F2329" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg b/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg deleted file mode 100644 index a8c8657135..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_ai_writer.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.03338 4.65784C8.37931 3.78072 9.62067 3.78072 9.9666 4.65785L11.0386 7.37598C11.1442 7.64377 11.3562 7.85575 11.624 7.96136L14.3422 9.03338C15.2193 9.37931 15.2193 10.6207 14.3422 10.9666L11.624 12.0386C11.3562 12.1442 11.1442 12.3562 11.0386 12.624L9.9666 15.3422C9.62066 16.2193 8.37931 16.2193 8.03338 15.3422L6.96136 12.624C6.85574 12.3562 6.64377 12.1442 6.37598 12.0386L3.65784 10.9666C2.78072 10.6207 2.78072 9.37931 3.65785 9.03338L6.37598 7.96136C6.64377 7.85574 6.85575 7.64377 6.96136 7.37598L8.03338 4.65784Z" stroke="#1F2329"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2788 2.18536C15.1765 1.93826 14.8264 1.9382 14.7239 2.18526L14.5912 2.5053C14.3879 2.99548 13.9983 3.3849 13.5079 3.58798L13.1854 3.72156C12.9382 3.82396 12.9382 4.17415 13.1855 4.27647L13.5049 4.40864C13.9962 4.61192 14.3865 5.00222 14.5896 5.49348L14.7224 5.81445C14.8247 6.0618 15.1751 6.06186 15.2774 5.81455L15.4093 5.49609C15.6125 5.00523 16.0025 4.61524 16.4934 4.41202L16.8158 4.27854C17.063 4.1762 17.0631 3.8261 16.8159 3.72367L16.4957 3.59096C16.0053 3.38775 15.6156 2.99814 15.4124 2.50778L15.2788 2.18536ZM15.2788 14.1854C15.1765 13.9383 14.8264 13.9382 14.7239 14.1853L14.5912 14.5053C14.3879 14.9955 13.9983 15.3849 13.5079 15.588L13.1854 15.7216C12.9382 15.824 12.9382 16.1742 13.1855 16.2765L13.5049 16.4086C13.9962 16.6119 14.3865 17.0022 14.5896 17.4935L14.7224 17.8145C14.8247 18.0618 15.1751 18.0619 15.2774 17.8145L15.4093 17.4961C15.6125 17.0052 16.0025 16.6152 16.4934 16.412L16.8158 16.2785C17.063 16.1762 17.0631 15.8261 16.8159 15.7237L16.4957 15.591C16.0053 15.3878 15.6156 14.9981 15.4124 14.5078L15.2788 14.1854Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_alignment.svg b/frontend/resources/flowy_icons/20x/toolbar_alignment.svg deleted file mode 100644 index 638ff3ece8..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_alignment.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3 4.5H17M3 10H13.2667M3 15.5H11.4" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg deleted file mode 100644 index e6ef664403..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_arrow_down.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="20" viewBox="0 0 12 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9 8.5L6 11.5L3 8.5" stroke="#99A1A8" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg b/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg deleted file mode 100644 index 2e39539ab0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_arrow_right.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 5L13 10L8 15" stroke="#99A1A8" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_bold.svg b/frontend/resources/flowy_icons/20x/toolbar_bold.svg deleted file mode 100644 index a131c6aa3e..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_bold.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.21423 10.0001H10.497C12.073 10.0001 13.3542 8.6548 13.3542 7.00004C13.3542 5.34528 12.073 4 10.497 4H7.41726C6.75559 4 6.21423 4.56843 6.21423 5.26317V10.0001ZM6.21423 10.0001L11.9286 9.99992C13.5046 9.99992 14.7858 11.3452 14.7858 13C14.7858 14.6547 13.5046 16 11.9286 16H7.41726C6.75559 16 6.21423 15.4316 6.21423 14.7368V10.0001Z" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_check.svg b/frontend/resources/flowy_icons/20x/toolbar_check.svg deleted file mode 100644 index e59186292c..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_check.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M15.8182 6L7.81819 14L4.18182 10.3636" stroke="#99A1A8" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg deleted file mode 100644 index c263b0c66b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_inline_code.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.375 14.5L3 10L7.375 5.5M12.625 5.5L17 10L12.625 14.5" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg b/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg deleted file mode 100644 index bc17a7b05b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_inline_italic.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.50469 4H14.7M5.29999 15.9999H11.4953M11.9301 4L8.40106 16" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_link.svg b/frontend/resources/flowy_icons/20x/toolbar_link.svg deleted file mode 100644 index 8564d243d0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M11.6818 15.0459L11.1212 15.6065C9.2633 17.4645 6.2512 17.4645 4.3934 15.6065C2.53553 13.7487 2.53553 10.7365 4.3934 8.87858L4.95401 8.31788M8.31822 11.6819L11.6821 8.31788M8.31822 4.95409L8.8789 4.39345C10.7367 2.53552 13.7489 2.53552 15.6066 4.39345C17.4645 6.25133 17.4645 9.26355 15.6066 11.1215L15.046 11.6821" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg b/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg deleted file mode 100644 index 57cb67da9a..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_earth.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 17C13.866 17 17 13.866 17 10C17 6.13401 13.866 3 10 3M10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3M10 17C11.6569 17 13 13.866 13 10C13 6.13401 11.6569 3 10 3M10 17C8.34315 17 7 13.866 7 10C7 6.13401 8.34315 3 10 3M3 10.025H17" stroke="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg b/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg deleted file mode 100644 index fc8765fa5b..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_edit.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.91797 5.7627C10.2674 7.92417 12.0879 9.57661 14.3473 9.79589M11.0285 4.62701L4.35601 11.4325C4.10407 11.691 3.86025 12.2 3.81149 12.5524L3.51078 15.0898C3.40513 16.0061 4.08782 16.6326 5.03057 16.476L7.64754 16.0453C8.01326 15.9826 8.52527 15.7242 8.77722 15.4579L15.4497 8.65237C16.6037 7.47766 17.1239 6.13848 15.3278 4.50171C13.5398 2.8806 12.1825 3.45229 11.0285 4.62701Z" stroke="#1F2329" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg b/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg deleted file mode 100644 index e1061b914a..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_link_unlink.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M15.1609 13.2392L17.0001 13.2666M13.2393 15.1608L13.2668 17M4.83926 6.76085L3.00009 6.73337M6.76086 4.83921L6.73339 3M8.60003 6.73337L10.2262 5.10712C11.5149 3.81844 13.6042 3.81844 14.8929 5.10712C16.1815 6.3958 16.1815 8.48516 14.8929 9.77383L13.2667 11.4001M11.4 13.2668L9.7738 14.893C8.48515 16.1817 6.39584 16.1817 5.10718 14.893C3.81853 13.6043 3.81853 11.515 5.10718 10.2263L6.73339 8.60006" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_more.svg b/frontend/resources/flowy_icons/20x/toolbar_more.svg deleted file mode 100644 index d156f313a1..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_more.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.78409 10C5.78409 10.7732 5.16085 11.4 4.39205 11.4C3.62324 11.4 3 10.7732 3 10C3 9.22681 3.62324 8.60001 4.39205 8.60001C5.16085 8.60001 5.78409 9.22681 5.78409 10Z" fill="#1F2329"/> -<path d="M11.3725 10C11.3725 10.7732 10.7492 11.4 9.98042 11.4C9.21161 11.4 8.58837 10.7732 8.58837 10C8.58837 9.22681 9.21161 8.60001 9.98042 8.60001C10.7492 8.60001 11.3725 9.22681 11.3725 10Z" fill="#1F2329"/> -<path d="M17 10C17 10.7732 16.3768 11.4 15.608 11.4C14.8391 11.4 14.2159 10.7732 14.2159 10C14.2159 9.22681 14.8391 8.60001 15.608 8.60001C16.3768 8.60001 17 9.22681 17 10Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg deleted file mode 100644 index 87c67115fb..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_center.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4 4H17M7 10H14M4 16H17" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg deleted file mode 100644 index bcaebfe5d0..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_left.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4 4H17M4 10H11M4 16H17" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg b/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg deleted file mode 100644 index 68069290ce..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_align_right.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M17 16L4 16M17 10L10 10M17 4L4 4" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg b/frontend/resources/flowy_icons/20x/toolbar_text_color.svg deleted file mode 100644 index e96b00ac35..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_color.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.55279 12.7764C4.42929 13.0234 4.5294 13.3237 4.77639 13.4472C5.02338 13.5707 5.32372 13.4706 5.44721 13.2236L4.55279 12.7764ZM10 3L10.4472 2.77639C10.3625 2.607 10.1894 2.5 10 2.5C9.81061 2.5 9.63748 2.607 9.55279 2.77639L10 3ZM14.5528 13.2236C14.6763 13.4706 14.9766 13.5707 15.2236 13.4472C15.4706 13.3237 15.5707 13.0234 15.4472 12.7764L14.5528 13.2236ZM5.44721 13.2236L7.11388 9.89027L6.21945 9.44306L4.55279 12.7764L5.44721 13.2236ZM7.11388 9.89027L10.4472 3.22361L9.55279 2.77639L6.21945 9.44306L7.11388 9.89027ZM9.55279 3.22361L12.8861 9.89027L13.7805 9.44306L10.4472 2.77639L9.55279 3.22361ZM12.8861 9.89027L14.5528 13.2236L15.4472 12.7764L13.7805 9.44306L12.8861 9.89027ZM6.66667 10.1667H13.3333V9.16667H6.66667V10.1667Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_format.svg b/frontend/resources/flowy_icons/20x/toolbar_text_format.svg deleted file mode 100644 index 0f3cd07a01..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_format.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M17.5 16C17.5 16.2761 17.7239 16.5 18 16.5C18.2761 16.5 18.5 16.2761 18.5 16H17.5ZM18.5 10C18.5 9.72386 18.2761 9.5 18 9.5C17.7239 9.5 17.5 9.72386 17.5 10H18.5ZM1.53184 15.8244C1.43488 16.083 1.56588 16.3712 1.82444 16.4682C2.083 16.5651 2.3712 16.4341 2.46816 16.1756L1.53184 15.8244ZM10.5318 16.1756C10.6288 16.4341 10.917 16.5651 11.1756 16.4682C11.4341 16.3712 11.5651 16.083 11.4682 15.8244L10.5318 16.1756ZM5.9382 5.49813L5.47004 5.32257L5.9382 5.49813ZM18.5 16C18.5 15.9973 18.5 15.9947 18.5 15.992C18.5 15.9893 18.5 15.9866 18.5 15.984C18.5 15.9813 18.5 15.9786 18.5 15.9759C18.5 15.9733 18.5 15.9706 18.5 15.9679C18.5 15.9652 18.5 15.9626 18.5 15.9599C18.5 15.9572 18.5 15.9545 18.5 15.9518C18.5 15.9492 18.5 15.9465 18.5 15.9438C18.5 15.9411 18.5 15.9384 18.5 15.9357C18.5 15.9331 18.5 15.9304 18.5 15.9277C18.5 15.925 18.5 15.9223 18.5 15.9196C18.5 15.9169 18.5 15.9142 18.5 15.9116C18.5 15.9089 18.5 15.9062 18.5 15.9035C18.5 15.9008 18.5 15.8981 18.5 15.8954C18.5 15.8927 18.5 15.89 18.5 15.8873C18.5 15.8846 18.5 15.8819 18.5 15.8792C18.5 15.8765 18.5 15.8738 18.5 15.8711C18.5 15.8684 18.5 15.8657 18.5 15.863C18.5 15.8603 18.5 15.8576 18.5 15.8549C18.5 15.8522 18.5 15.8495 18.5 15.8468C18.5 15.8441 18.5 15.8414 18.5 15.8387C18.5 15.836 18.5 15.8333 18.5 15.8306C18.5 15.8279 18.5 15.8252 18.5 15.8225C18.5 15.8198 18.5 15.8171 18.5 15.8144C18.5 15.8116 18.5 15.8089 18.5 15.8062C18.5 15.8035 18.5 15.8008 18.5 15.7981C18.5 15.7954 18.5 15.7927 18.5 15.79C18.5 15.7872 18.5 15.7845 18.5 15.7818C18.5 15.7791 18.5 15.7764 18.5 15.7737C18.5 15.771 18.5 15.7682 18.5 15.7655C18.5 15.7628 18.5 15.7601 18.5 15.7574C18.5 15.7547 18.5 15.7519 18.5 15.7492C18.5 15.7465 18.5 15.7438 18.5 15.7411C18.5 15.7383 18.5 15.7356 18.5 15.7329C18.5 15.7302 18.5 15.7275 18.5 15.7247C18.5 15.722 18.5 15.7193 18.5 15.7166C18.5 15.7138 18.5 15.7111 18.5 15.7084C18.5 15.7057 18.5 15.7029 18.5 15.7002C18.5 15.6975 18.5 15.6948 18.5 15.692C18.5 15.6893 18.5 15.6866 18.5 15.6839C18.5 15.6811 18.5 15.6784 18.5 15.6757C18.5 15.6729 18.5 15.6702 18.5 15.6675C18.5 15.6648 18.5 15.662 18.5 15.6593C18.5 15.6566 18.5 15.6538 18.5 15.6511C18.5 15.6484 18.5 15.6456 18.5 15.6429C18.5 15.6402 18.5 15.6375 18.5 15.6347C18.5 15.632 18.5 15.6293 18.5 15.6265C18.5 15.6238 18.5 15.6211 18.5 15.6183C18.5 15.6156 18.5 15.6129 18.5 15.6101C18.5 15.6074 18.5 15.6047 18.5 15.6019C18.5 15.5992 18.5 15.5964 18.5 15.5937C18.5 15.591 18.5 15.5882 18.5 15.5855C18.5 15.5828 18.5 15.58 18.5 15.5773C18.5 15.5746 18.5 15.5718 18.5 15.5691C18.5 15.5663 18.5 15.5636 18.5 15.5609C18.5 15.5581 18.5 15.5554 18.5 15.5527C18.5 15.5499 18.5 15.5472 18.5 15.5444C18.5 15.5417 18.5 15.539 18.5 15.5362C18.5 15.5335 18.5 15.5307 18.5 15.528C18.5 15.5253 18.5 15.5225 18.5 15.5198C18.5 15.517 18.5 15.5143 18.5 15.5116C18.5 15.5088 18.5 15.5061 18.5 15.5033C18.5 15.5006 18.5 15.4979 18.5 15.4951C18.5 15.4924 18.5 15.4896 18.5 15.4869C18.5 15.4841 18.5 15.4814 18.5 15.4787C18.5 15.4759 18.5 15.4732 18.5 15.4704C18.5 15.4677 18.5 15.465 18.5 15.4622C18.5 15.4595 18.5 15.4567 18.5 15.454C18.5 15.4512 18.5 15.4485 18.5 15.4458C18.5 15.443 18.5 15.4403 18.5 15.4375C18.5 15.4348 18.5 15.432 18.5 15.4293C18.5 15.4266 18.5 15.4238 18.5 15.4211C18.5 15.4183 18.5 15.4156 18.5 15.4128C18.5 15.4101 18.5 15.4074 18.5 15.4046C18.5 15.4019 18.5 15.3991 18.5 15.3964C18.5 15.3937 18.5 15.3909 18.5 15.3882C18.5 15.3854 18.5 15.3827 18.5 15.3799C18.5 15.3772 18.5 15.3745 18.5 15.3717C18.5 15.369 18.5 15.3662 18.5 15.3635C18.5 15.3607 18.5 15.358 18.5 15.3553C18.5 15.3525 18.5 15.3498 18.5 15.347C18.5 15.3443 18.5 15.3416 18.5 15.3388C18.5 15.3361 18.5 15.3333 18.5 15.3306C18.5 15.3278 18.5 15.3251 18.5 15.3224C18.5 15.3196 18.5 15.3169 18.5 15.3141C18.5 15.3114 18.5 15.3087 18.5 15.3059C18.5 15.3032 18.5 15.3004 18.5 15.2977C18.5 15.295 18.5 15.2922 18.5 15.2895C18.5 15.2867 18.5 15.284 18.5 15.2813C18.5 15.2785 18.5 15.2758 18.5 15.2731C18.5 15.2703 18.5 15.2676 18.5 15.2648C18.5 15.2621 18.5 15.2594 18.5 15.2566C18.5 15.2539 18.5 15.2512 18.5 15.2484C18.5 15.2457 18.5 15.2429 18.5 15.2402C18.5 15.2375 18.5 15.2347 18.5 15.232C18.5 15.2293 18.5 15.2265 18.5 15.2238C18.5 15.2211 18.5 15.2183 18.5 15.2156C18.5 15.2129 18.5 15.2101 18.5 15.2074C18.5 15.2047 18.5 15.2019 18.5 15.1992C18.5 15.1965 18.5 15.1937 18.5 15.191C18.5 15.1883 18.5 15.1855 18.5 15.1828C18.5 15.1801 18.5 15.1773 18.5 15.1746C18.5 15.1719 18.5 15.1691 18.5 15.1664C18.5 15.1637 18.5 15.161 18.5 15.1582C18.5 15.1555 18.5 15.1528 18.5 15.15C18.5 15.1473 18.5 15.1446 18.5 15.1419C18.5 15.1391 18.5 15.1364 18.5 15.1337C18.5 15.131 18.5 15.1282 18.5 15.1255C18.5 15.1228 18.5 15.1201 18.5 15.1173C18.5 15.1146 18.5 15.1119 18.5 15.1092C18.5 15.1064 18.5 15.1037 18.5 15.101C18.5 15.0983 18.5 15.0955 18.5 15.0928C18.5 15.0901 18.5 15.0874 18.5 15.0847C18.5 15.0819 18.5 15.0792 18.5 15.0765C18.5 15.0738 18.5 15.0711 18.5 15.0684C18.5 15.0656 18.5 15.0629 18.5 15.0602C18.5 15.0575 18.5 15.0548 18.5 15.0521C18.5 15.0493 18.5 15.0466 18.5 15.0439C18.5 15.0412 18.5 15.0385 18.5 15.0358C18.5 15.0331 18.5 15.0304 18.5 15.0276C18.5 15.0249 18.5 15.0222 18.5 15.0195C18.5 15.0168 18.5 15.0141 18.5 15.0114C18.5 15.0087 18.5 15.006 18.5 15.0033C18.5 15.0006 18.5 14.9978 18.5 14.9951C18.5 14.9924 18.5 14.9897 18.5 14.987C18.5 14.9843 18.5 14.9816 18.5 14.9789C18.5 14.9762 18.5 14.9735 18.5 14.9708C18.5 14.9681 18.5 14.9654 18.5 14.9627C18.5 14.96 18.5 14.9573 18.5 14.9546C18.5 14.9519 18.5 14.9492 18.5 14.9465C18.5 14.9438 18.5 14.9411 18.5 14.9384C18.5 14.9357 18.5 14.9331 18.5 14.9304C18.5 14.9277 18.5 14.925 18.5 14.9223C18.5 14.9196 18.5 14.9169 18.5 14.9142C18.5 14.9115 18.5 14.9088 18.5 14.9061C18.5 14.9035 18.5 14.9008 18.5 14.8981C18.5 14.8954 18.5 14.8927 18.5 14.89C18.5 14.8873 18.5 14.8847 18.5 14.882C18.5 14.8793 18.5 14.8766 18.5 14.8739C18.5 14.8713 18.5 14.8686 18.5 14.8659C18.5 14.8632 18.5 14.8605 18.5 14.8579C18.5 14.8552 18.5 14.8525 18.5 14.8498C18.5 14.8472 18.5 14.8445 18.5 14.8418C18.5 14.8391 18.5 14.8365 18.5 14.8338C18.5 14.8311 18.5 14.8285 18.5 14.8258C18.5 14.8231 18.5 14.8205 18.5 14.8178C18.5 14.8151 18.5 14.8125 18.5 14.8098C18.5 14.8071 18.5 14.8045 18.5 14.8018C18.5 14.7991 18.5 14.7965 18.5 14.7938C18.5 14.7912 18.5 14.7885 18.5 14.7858C18.5 14.7832 18.5 14.7805 18.5 14.7779C18.5 14.7752 18.5 14.7726 18.5 14.7699C18.5 14.7672 18.5 14.7646 18.5 14.7619C18.5 14.7593 18.5 14.7566 18.5 14.754C18.5 14.7513 18.5 14.7487 18.5 14.746C18.5 14.7434 18.5 14.7408 18.5 14.7381C18.5 14.7355 18.5 14.7328 18.5 14.7302C18.5 14.7275 18.5 14.7249 18.5 14.7223C18.5 14.7196 18.5 14.717 18.5 14.7143C18.5 14.7117 18.5 14.7091 18.5 14.7064C18.5 14.7038 18.5 14.7012 18.5 14.6985C18.5 14.6959 18.5 14.6933 18.5 14.6906C18.5 14.688 18.5 14.6854 18.5 14.6828C18.5 14.6801 18.5 14.6775 18.5 14.6749C18.5 14.6723 18.5 14.6696 18.5 14.667C18.5 14.6644 18.5 14.6618 18.5 14.6591C18.5 14.6565 18.5 14.6539 18.5 14.6513C18.5 14.6487 18.5 14.6461 18.5 14.6434C18.5 14.6408 18.5 14.6382 18.5 14.6356C18.5 14.633 18.5 14.6304 18.5 14.6278C18.5 14.6252 18.5 14.6226 18.5 14.62C18.5 14.6173 18.5 14.6147 18.5 14.6121C18.5 14.6095 18.5 14.6069 18.5 14.6043C18.5 14.6017 18.5 14.5991 18.5 14.5965C18.5 14.5939 18.5 14.5913 18.5 14.5887C18.5 14.5862 18.5 14.5836 18.5 14.581C18.5 14.5784 18.5 14.5758 18.5 14.5732C18.5 14.5706 18.5 14.568 18.5 14.5654C18.5 14.5628 18.5 14.5603 18.5 14.5577C18.5 14.5551 18.5 14.5525 18.5 14.5499C18.5 14.5474 18.5 14.5448 18.5 14.5422C18.5 14.5396 18.5 14.537 18.5 14.5345C18.5 14.5319 18.5 14.5293 18.5 14.5268C18.5 14.5242 18.5 14.5216 18.5 14.5191C18.5 14.5165 18.5 14.5139 18.5 14.5114C18.5 14.5088 18.5 14.5062 18.5 14.5037C18.5 14.5011 18.5 14.4985 18.5 14.496C18.5 14.4934 18.5 14.4909 18.5 14.4883C18.5 14.4858 18.5 14.4832 18.5 14.4807C18.5 14.4781 18.5 14.4756 18.5 14.473C18.5 14.4705 18.5 14.4679 18.5 14.4654C18.5 14.4628 18.5 14.4603 18.5 14.4577C18.5 14.4552 18.5 14.4527 18.5 14.4501C18.5 14.4476 18.5 14.445 18.5 14.4425C18.5 14.44 18.5 14.4374 18.5 14.4349C18.5 14.4324 18.5 14.4299 18.5 14.4273C18.5 14.4248 18.5 14.4223 18.5 14.4197C18.5 14.4172 18.5 14.4147 18.5 14.4122C18.5 14.4097 18.5 14.4071 18.5 14.4046C18.5 14.4021 18.5 14.3996 18.5 14.3971C18.5 14.3946 18.5 14.3921 18.5 14.3895C18.5 14.387 18.5 14.3845 18.5 14.382C18.5 14.3795 18.5 14.377 18.5 14.3745C18.5 14.372 18.5 14.3695 18.5 14.367C18.5 14.3645 18.5 14.362 18.5 14.3595C18.5 14.357 18.5 14.3545 18.5 14.352C18.5 14.3495 18.5 14.3471 18.5 14.3446C18.5 14.3421 18.5 14.3396 18.5 14.3371C18.5 14.3346 18.5 14.3321 18.5 14.3297C18.5 14.3272 18.5 14.3247 18.5 14.3222C18.5 14.3198 18.5 14.3173 18.5 14.3148C18.5 14.3123 18.5 14.3099 18.5 14.3074C18.5 14.3049 18.5 14.3025 18.5 14.3C18.5 14.2975 18.5 14.2951 18.5 14.2926C18.5 14.2901 18.5 14.2877 18.5 14.2852C18.5 14.2828 18.5 14.2803 18.5 14.2779C18.5 14.2754 18.5 14.273 18.5 14.2705C18.5 14.2681 18.5 14.2656 18.5 14.2632C18.5 14.2607 18.5 14.2583 18.5 14.2559C18.5 14.2534 18.5 14.251 18.5 14.2485C18.5 14.2461 18.5 14.2437 18.5 14.2412C18.5 14.2388 18.5 14.2364 18.5 14.234C18.5 14.2315 18.5 14.2291 18.5 14.2267C18.5 14.2243 18.5 14.2218 18.5 14.2194C18.5 14.217 18.5 14.2146 18.5 14.2122C18.5 14.2097 18.5 14.2073 18.5 14.2049C18.5 14.2025 18.5 14.2001 18.5 14.1977C18.5 14.1953 18.5 14.1929 18.5 14.1905C18.5 14.1881 18.5 14.1857 18.5 14.1833C18.5 14.1809 18.5 14.1785 18.5 14.1761C18.5 14.1737 18.5 14.1713 18.5 14.1689C18.5 14.1665 18.5 14.1642 18.5 14.1618C18.5 14.1594 18.5 14.157 18.5 14.1546C18.5 14.1523 18.5 14.1499 18.5 14.1475C18.5 14.1451 18.5 14.1428 18.5 14.1404C18.5 14.138 18.5 14.1356 18.5 14.1333C18.5 14.1309 18.5 14.1286 18.5 14.1262C18.5 14.1238 18.5 14.1215 18.5 14.1191C18.5 14.1168 18.5 14.1144 18.5 14.1121C18.5 14.1097 18.5 14.1074 18.5 14.105C18.5 14.1027 18.5 14.1003 18.5 14.098C18.5 14.0956 18.5 14.0933 18.5 14.091C18.5 14.0886 18.5 14.0863 18.5 14.0839C18.5 14.0816 18.5 14.0793 18.5 14.077C18.5 14.0746 18.5 14.0723 18.5 14.07C18.5 14.0677 18.5 14.0653 18.5 14.063C18.5 14.0607 18.5 14.0584 18.5 14.0561C18.5 14.0538 18.5 14.0514 18.5 14.0491C18.5 14.0468 18.5 14.0445 18.5 14.0422C18.5 14.0399 18.5 14.0376 18.5 14.0353C18.5 14.033 18.5 14.0307 18.5 14.0284C18.5 14.0261 18.5 14.0239 18.5 14.0216C18.5 14.0193 18.5 14.017 18.5 14.0147C18.5 14.0124 18.5 14.0101 18.5 14.0079C18.5 14.0056 18.5 14.0033 18.5 14.001C18.5 13.9988 18.5 13.9965 18.5 13.9942C18.5 13.992 18.5 13.9897 18.5 13.9874C18.5 13.9852 18.5 13.9829 18.5 13.9807C18.5 13.9784 18.5 13.9761 18.5 13.9739C18.5 13.9716 18.5 13.9694 18.5 13.9671C18.5 13.9649 18.5 13.9627 18.5 13.9604C18.5 13.9582 18.5 13.9559 18.5 13.9537C18.5 13.9515 18.5 13.9492 18.5 13.947C18.5 13.9448 18.5 13.9425 18.5 13.9403C18.5 13.9381 18.5 13.9359 18.5 13.9337C18.5 13.9314 18.5 13.9292 18.5 13.927C18.5 13.9248 18.5 13.9226 18.5 13.9204C18.5 13.9182 18.5 13.9159 18.5 13.9137C18.5 13.9115 18.5 13.9093 18.5 13.9071C18.5 13.9049 18.5 13.9028 18.5 13.9006C18.5 13.8984 18.5 13.8962 18.5 13.894C18.5 13.8918 18.5 13.8896 18.5 13.8874C18.5 13.8853 18.5 13.8831 18.5 13.8809C18.5 13.8787 18.5 13.8766 18.5 13.8744C18.5 13.8722 18.5 13.8701 18.5 13.8679C18.5 13.8657 18.5 13.8636 18.5 13.8614C18.5 13.8592 18.5 13.8571 18.5 13.8549C18.5 13.8528 18.5 13.8506 18.5 13.8485C18.5 13.8463 18.5 13.8442 18.5 13.8421C18.5 13.8399 18.5 13.8378 18.5 13.8356C18.5 13.8335 18.5 13.8314 18.5 13.8293C18.5 13.8271 18.5 13.825 18.5 13.8229C18.5 13.8208 18.5 13.8186 18.5 13.8165C18.5 13.8144 18.5 13.8123 18.5 13.8102C18.5 13.8081 18.5 13.806 18.5 13.8038C18.5 13.8017 18.5 13.7996 18.5 13.7975C18.5 13.7954 18.5 13.7933 18.5 13.7913C18.5 13.7892 18.5 13.7871 18.5 13.785C18.5 13.7829 18.5 13.7808 18.5 13.7787C18.5 13.7766 18.5 13.7746 18.5 13.7725C18.5 13.7704 18.5 13.7683 18.5 13.7663C18.5 13.7642 18.5 13.7621 18.5 13.7601C18.5 13.758 18.5 13.756 18.5 13.7539C18.5 13.7519 18.5 13.7498 18.5 13.7477C18.5 13.7457 18.5 13.7437 18.5 13.7416C18.5 13.7396 18.5 13.7375 18.5 13.7355C18.5 13.7334 18.5 13.7314 18.5 13.7294C18.5 13.7274 18.5 13.7253 18.5 13.7233C18.5 13.7213 18.5 13.7193 18.5 13.7172C18.5 13.7152 18.5 13.7132 18.5 13.7112C18.5 13.7092 18.5 13.7072 18.5 13.7052C18.5 13.7032 18.5 13.7012 18.5 13.6992C18.5 13.6972 18.5 13.6952 18.5 13.6932C18.5 13.6912 18.5 13.6892 18.5 13.6872C18.5 13.6852 18.5 13.6832 18.5 13.6813C18.5 13.6793 18.5 13.6773 18.5 13.6753C18.5 13.6734 18.5 13.6714 18.5 13.6694C18.5 13.6674 18.5 13.6655 18.5 13.6635C18.5 13.6616 18.5 13.6596 18.5 13.6577C18.5 13.6557 18.5 13.6538 18.5 13.6518C18.5 13.6499 18.5 13.6479 18.5 13.646C18.5 13.644 18.5 13.6421 18.5 13.6402C18.5 13.6382 18.5 13.6363 18.5 13.6344C18.5 13.6325 18.5 13.6305 18.5 13.6286C18.5 13.6267 18.5 13.6248 18.5 13.6229C18.5 13.621 18.5 13.619 18.5 13.6171C18.5 13.6152 18.5 13.6133 18.5 13.6114C18.5 13.6095 18.5 13.6076 18.5 13.6058C18.5 13.6039 18.5 13.602 18.5 13.6001C18.5 13.5982 18.5 13.5963 18.5 13.5944C18.5 13.5926 18.5 13.5907 18.5 13.5888C18.5 13.587 18.5 13.5851 18.5 13.5832C18.5 13.5814 18.5 13.5795 18.5 13.5776C18.5 13.5758 18.5 13.5739 18.5 13.5721C18.5 13.5702 18.5 13.5684 18.5 13.5665C18.5 13.5647 18.5 13.5629 18.5 13.561C18.5 13.5592 18.5 13.5574 18.5 13.5555C18.5 13.5537 18.5 13.5519 18.5 13.5501C18.5 13.5482 18.5 13.5464 18.5 13.5446C18.5 13.5428 18.5 13.541 18.5 13.5392C18.5 13.5374 18.5 13.5356 18.5 13.5338C18.5 13.532 18.5 13.5302 18.5 13.5284C18.5 13.5266 18.5 13.5248 18.5 13.523C18.5 13.5212 18.5 13.5195 18.5 13.5177C18.5 13.5159 18.5 13.5141 18.5 13.5124C18.5 13.5106 18.5 13.5088 18.5 13.5071C18.5 13.5053 18.5 13.5035 18.5 13.5018C18.5 13.5 18.5 13.4983 18.5 13.4965C18.5 13.4948 18.5 13.493 18.5 13.4913C18.5 13.4896 18.5 13.4878 18.5 13.4861C18.5 13.4844 18.5 13.4826 18.5 13.4809C18.5 13.4792 18.5 13.4775 18.5 13.4758C18.5 13.474 18.5 13.4723 18.5 13.4706C18.5 13.4689 18.5 13.4672 18.5 13.4655C18.5 13.4638 18.5 13.4621 18.5 13.4604C18.5 13.4587 18.5 13.457 18.5 13.4553C18.5 13.4536 18.5 13.452 18.5 13.4503C18.5 13.4486 18.5 13.4469 18.5 13.4453C18.5 13.4436 18.5 13.4419 18.5 13.4403C18.5 13.4386 18.5 13.4369 18.5 13.4353C18.5 13.4336 18.5 13.432 18.5 13.4303C18.5 13.4287 18.5 13.427 18.5 13.4254C18.5 13.4238 18.5 13.4221 18.5 13.4205C18.5 13.4189 18.5 13.4172 18.5 13.4156C18.5 13.414 18.5 13.4124 18.5 13.4108C18.5 13.4091 18.5 13.4075 18.5 13.4059C18.5 13.4043 18.5 13.4027 18.5 13.4011C18.5 13.3995 18.5 13.3979 18.5 13.3963C18.5 13.3947 18.5 13.3931 18.5 13.3916C18.5 13.39 18.5 13.3884 18.5 13.3868C18.5 13.3853 18.5 13.3837 18.5 13.3821C18.5 13.3805 18.5 13.379 18.5 13.3774C18.5 13.3759 18.5 13.3743 18.5 13.3728C18.5 13.3712 18.5 13.3697 18.5 13.3681C18.5 13.3666 18.5 13.365 18.5 13.3635C18.5 13.362 18.5 13.3604 18.5 13.3589C18.5 13.3574 18.5 13.3559 18.5 13.3544C18.5 13.3528 18.5 13.3513 18.5 13.3498C18.5 13.3483 18.5 13.3468 18.5 13.3453C18.5 13.3438 18.5 13.3423 18.5 13.3408C18.5 13.3393 18.5 13.3378 18.5 13.3363C18.5 13.3349 18.5 13.3334 18.5 13.3319C18.5 13.3304 18.5 13.329 18.5 13.3275C18.5 13.326 18.5 13.3246 18.5 13.3231C18.5 13.3216 18.5 13.3202 18.5 13.3187C18.5 13.3173 18.5 13.3158 18.5 13.3144C18.5 13.313 18.5 13.3115 18.5 13.3101C18.5 13.3087 18.5 13.3072 18.5 13.3058C18.5 13.3044 18.5 13.303 18.5 13.3015C18.5 13.3001 18.5 13.2987 18.5 13.2973C18.5 13.2959 18.5 13.2945 18.5 13.2931C18.5 13.2917 18.5 13.2903 18.5 13.2889C18.5 13.2875 18.5 13.2862 18.5 13.2848C18.5 13.2834 18.5 13.282 18.5 13.2806C18.5 13.2793 18.5 13.2779 18.5 13.2765C18.5 13.2752 18.5 13.2738 18.5 13.2725C18.5 13.2711 18.5 13.2698 18.5 13.2684C18.5 13.2671 18.5 13.2657 18.5 13.2644C18.5 13.2631 18.5 13.2617 18.5 13.2604C18.5 13.2591 18.5 13.2577 18.5 13.2564C18.5 13.2551 18.5 13.2538 18.5 13.2525C18.5 13.2512 18.5 13.2499 18.5 13.2486C18.5 13.2473 18.5 13.246 18.5 13.2447C18.5 13.2434 18.5 13.2421 18.5 13.2408C18.5 13.2395 18.5 13.2383 18.5 13.237C18.5 13.2357 18.5 13.2344 18.5 13.2332C18.5 13.2319 18.5 13.2307 18.5 13.2294C18.5 13.2281 18.5 13.2269 18.5 13.2257C18.5 13.2244 18.5 13.2232 18.5 13.2219C18.5 13.2207 18.5 13.2195 18.5 13.2182C18.5 13.217 18.5 13.2158 18.5 13.2146C18.5 13.2133 18.5 13.2121 18.5 13.2109C18.5 13.2097 18.5 13.2085 18.5 13.2073C18.5 13.2061 18.5 13.2049 18.5 13.2037C18.5 13.2025 18.5 13.2014 18.5 13.2002C18.5 13.199 18.5 13.1978 18.5 13.1966C18.5 13.1955 18.5 13.1943 18.5 13.1931C18.5 13.192 18.5 13.1908 18.5 13.1897C18.5 13.1885 18.5 13.1874 18.5 13.1862C18.5 13.1851 18.5 13.184 18.5 13.1828C18.5 13.1817 18.5 13.1806 18.5 13.1794C18.5 13.1783 18.5 13.1772 18.5 13.1761C18.5 13.175 18.5 13.1739 18.5 13.1727C18.5 13.1716 18.5 13.1705 18.5 13.1694C18.5 13.1684 18.5 13.1673 18.5 13.1662C18.5 13.1651 18.5 13.164 18.5 13.1629C18.5 13.1619 18.5 13.1608 18.5 13.1597C18.5 13.1587 18.5 13.1576 18.5 13.1565C18.5 13.1555 18.5 13.1544 18.5 13.1534C18.5 13.1523 18.5 13.1513 18.5 13.1503C18.5 13.1492 18.5 13.1482 18.5 13.1472C18.5 13.1461 18.5 13.1451 18.5 13.1441C18.5 13.1431 18.5 13.1421 18.5 13.1411C18.5 13.1401 18.5 13.1391 18.5 13.1381C18.5 13.1371 18.5 13.1361 18.5 13.1351C18.5 13.1341 18.5 13.1331 18.5 13.1321C18.5 13.1312 18.5 13.1302 18.5 13.1292C18.5 13.1283 18.5 13.1273 18.5 13.1263C18.5 13.1254 18.5 13.1244 18.5 13.1235C18.5 13.1225 18.5 13.1216 18.5 13.1207C18.5 13.1197 18.5 13.1188 18.5 13.1179C18.5 13.1169 18.5 13.116 18.5 13.1151C18.5 13.1142 18.5 13.1133 18.5 13.1124C18.5 13.1115 18.5 13.1106 18.5 13.1097C18.5 13.1088 18.5 13.1079 18.5 13.107C18.5 13.1061 18.5 13.1052 18.5 13.1044C18.5 13.1035 18.5 13.1026 18.5 13.1017C18.5 13.1009 18.5 13.1 18.5 13.0992C18.5 13.0983 18.5 13.0975 18.5 13.0966C18.5 13.0958 18.5 13.0949 18.5 13.0941C18.5 13.0933 18.5 13.0924 18.5 13.0916C18.5 13.0908 18.5 13.09 18.5 13.0892C18.5 13.0883 18.5 13.0875 18.5 13.0867C18.5 13.0859 18.5 13.0851 18.5 13.0843C18.5 13.0835 18.5 13.0828 18.5 13.082C18.5 13.0812 18.5 13.0804 18.5 13.0796C18.5 13.0789 18.5 13.0781 18.5 13.0773C18.5 13.0766 18.5 13.0758 18.5 13.0751C18.5 13.0743 18.5 13.0736 18.5 13.0728C18.5 13.0721 18.5 13.0714 18.5 13.0706C18.5 13.0699 18.5 13.0692 18.5 13.0685C18.5 13.0678 18.5 13.067 18.5 13.0663C18.5 13.0656 18.5 13.0649 18.5 13.0642C18.5 13.0635 18.5 13.0628 18.5 13.0622C18.5 13.0615 18.5 13.0608 18.5 13.0601C18.5 13.0594 18.5 13.0588 18.5 13.0581C18.5 13.0574 18.5 13.0568 18.5 13.0561C18.5 13.0555 18.5 13.0548 18.5 13.0542C18.5 13.0535 18.5 13.0529 18.5 13.0523C18.5 13.0516 18.5 13.051 18.5 13.0504C18.5 13.0498 18.5 13.0492 18.5 13.0485C18.5 13.0479 18.5 13.0473 18.5 13.0467C18.5 13.0461 18.5 13.0455 18.5 13.045C18.5 13.0444 18.5 13.0438 18.5 13.0432C18.5 13.0426 18.5 13.0421 18.5 13.0415C18.5 13.0409 18.5 13.0404 18.5 13.0398C18.5 13.0393 18.5 13.0387 18.5 13.0382C18.5 13.0376 18.5 13.0371 18.5 13.0366C18.5 13.036 18.5 13.0355 18.5 13.035C18.5 13.0345 18.5 13.0339 18.5 13.0334C18.5 13.0329 18.5 13.0324 18.5 13.0319C18.5 13.0314 18.5 13.0309 18.5 13.0304C18.5 13.03 18.5 13.0295 18.5 13.029C18.5 13.0285 18.5 13.0281 18.5 13.0276C18.5 13.0271 18.5 13.0267 18.5 13.0262C18.5 13.0258 18.5 13.0253 18.5 13.0249C18.5 13.0244 18.5 13.024 18.5 13.0236C18.5 13.0231 18.5 13.0227 18.5 13.0223C18.5 13.0219 18.5 13.0215 18.5 13.021C18.5 13.0206 18.5 13.0202 18.5 13.0198C18.5 13.0194 18.5 13.0191 18.5 13.0187C18.5 13.0183 18.5 13.0179 18.5 13.0175C18.5 13.0172 18.5 13.0168 18.5 13.0164C18.5 13.0161 18.5 13.0157 18.5 13.0154C18.5 13.015 18.5 13.0147 18.5 13.0143C18.5 13.014 18.5 13.0137 18.5 13.0133C18.5 13.013 18.5 13.0127 18.5 13.0124C18.5 13.0121 18.5 13.0118 18.5 13.0115C18.5 13.0112 18.5 13.0109 18.5 13.0106C18.5 13.0103 18.5 13.01 18.5 13.0097C18.5 13.0094 18.5 13.0092 18.5 13.0089C18.5 13.0086 18.5 13.0084 18.5 13.0081C18.5 13.0079 18.5 13.0076 18.5 13.0074C18.5 13.0071 18.5 13.0069 18.5 13.0066C18.5 13.0064 18.5 13.0062 18.5 13.006C18.5 13.0058 18.5 13.0055 18.5 13.0053C18.5 13.0051 18.5 13.0049 18.5 13.0047C18.5 13.0045 18.5 13.0043 18.5 13.0042C18.5 13.004 18.5 13.0038 18.5 13.0036C18.5 13.0035 18.5 13.0033 18.5 13.0031C18.5 13.003 18.5 13.0028 18.5 13.0027C18.5 13.0025 18.5 13.0024 18.5 13.0022C18.5 13.0021 18.5 13.002 18.5 13.0019C18.5 13.0017 18.5 13.0016 18.5 13.0015C18.5 13.0014 18.5 13.0013 18.5 13.0012C18.5 13.0011 18.5 13.001 18.5 13.0009C18.5 13.0008 18.5 13.0007 18.5 13.0007C18.5 13.0006 18.5 13.0005 18.5 13.0005C18.5 13.0004 18.5 13.0003 18.5 13.0003C18.5 13.0002 18.5 13.0002 18.5 13.0002C18.5 13.0001 18.5 13.0001 18.5 13.0001C18.5 13 18.5 13 18.5 13C18.5 13 18.5 13 18 13C17.5 13 17.5 13 17.5 13C17.5 13 17.5 13 17.5 13.0001C17.5 13.0001 17.5 13.0001 17.5 13.0002C17.5 13.0002 17.5 13.0002 17.5 13.0003C17.5 13.0003 17.5 13.0004 17.5 13.0005C17.5 13.0005 17.5 13.0006 17.5 13.0007C17.5 13.0007 17.5 13.0008 17.5 13.0009C17.5 13.001 17.5 13.0011 17.5 13.0012C17.5 13.0013 17.5 13.0014 17.5 13.0015C17.5 13.0016 17.5 13.0017 17.5 13.0019C17.5 13.002 17.5 13.0021 17.5 13.0022C17.5 13.0024 17.5 13.0025 17.5 13.0027C17.5 13.0028 17.5 13.003 17.5 13.0031C17.5 13.0033 17.5 13.0035 17.5 13.0036C17.5 13.0038 17.5 13.004 17.5 13.0042C17.5 13.0043 17.5 13.0045 17.5 13.0047C17.5 13.0049 17.5 13.0051 17.5 13.0053C17.5 13.0055 17.5 13.0058 17.5 13.006C17.5 13.0062 17.5 13.0064 17.5 13.0066C17.5 13.0069 17.5 13.0071 17.5 13.0074C17.5 13.0076 17.5 13.0079 17.5 13.0081C17.5 13.0084 17.5 13.0086 17.5 13.0089C17.5 13.0092 17.5 13.0094 17.5 13.0097C17.5 13.01 17.5 13.0103 17.5 13.0106C17.5 13.0109 17.5 13.0112 17.5 13.0115C17.5 13.0118 17.5 13.0121 17.5 13.0124C17.5 13.0127 17.5 13.013 17.5 13.0133C17.5 13.0137 17.5 13.014 17.5 13.0143C17.5 13.0147 17.5 13.015 17.5 13.0154C17.5 13.0157 17.5 13.0161 17.5 13.0164C17.5 13.0168 17.5 13.0172 17.5 13.0175C17.5 13.0179 17.5 13.0183 17.5 13.0187C17.5 13.0191 17.5 13.0194 17.5 13.0198C17.5 13.0202 17.5 13.0206 17.5 13.021C17.5 13.0215 17.5 13.0219 17.5 13.0223C17.5 13.0227 17.5 13.0231 17.5 13.0236C17.5 13.024 17.5 13.0244 17.5 13.0249C17.5 13.0253 17.5 13.0258 17.5 13.0262C17.5 13.0267 17.5 13.0271 17.5 13.0276C17.5 13.0281 17.5 13.0285 17.5 13.029C17.5 13.0295 17.5 13.03 17.5 13.0304C17.5 13.0309 17.5 13.0314 17.5 13.0319C17.5 13.0324 17.5 13.0329 17.5 13.0334C17.5 13.0339 17.5 13.0345 17.5 13.035C17.5 13.0355 17.5 13.036 17.5 13.0366C17.5 13.0371 17.5 13.0376 17.5 13.0382C17.5 13.0387 17.5 13.0393 17.5 13.0398C17.5 13.0404 17.5 13.0409 17.5 13.0415C17.5 13.0421 17.5 13.0426 17.5 13.0432C17.5 13.0438 17.5 13.0444 17.5 13.045C17.5 13.0455 17.5 13.0461 17.5 13.0467C17.5 13.0473 17.5 13.0479 17.5 13.0485C17.5 13.0492 17.5 13.0498 17.5 13.0504C17.5 13.051 17.5 13.0516 17.5 13.0523C17.5 13.0529 17.5 13.0535 17.5 13.0542C17.5 13.0548 17.5 13.0555 17.5 13.0561C17.5 13.0568 17.5 13.0574 17.5 13.0581C17.5 13.0588 17.5 13.0594 17.5 13.0601C17.5 13.0608 17.5 13.0615 17.5 13.0622C17.5 13.0628 17.5 13.0635 17.5 13.0642C17.5 13.0649 17.5 13.0656 17.5 13.0663C17.5 13.067 17.5 13.0678 17.5 13.0685C17.5 13.0692 17.5 13.0699 17.5 13.0706C17.5 13.0714 17.5 13.0721 17.5 13.0728C17.5 13.0736 17.5 13.0743 17.5 13.0751C17.5 13.0758 17.5 13.0766 17.5 13.0773C17.5 13.0781 17.5 13.0789 17.5 13.0796C17.5 13.0804 17.5 13.0812 17.5 13.082C17.5 13.0828 17.5 13.0835 17.5 13.0843C17.5 13.0851 17.5 13.0859 17.5 13.0867C17.5 13.0875 17.5 13.0883 17.5 13.0892C17.5 13.09 17.5 13.0908 17.5 13.0916C17.5 13.0924 17.5 13.0933 17.5 13.0941C17.5 13.0949 17.5 13.0958 17.5 13.0966C17.5 13.0975 17.5 13.0983 17.5 13.0992C17.5 13.1 17.5 13.1009 17.5 13.1017C17.5 13.1026 17.5 13.1035 17.5 13.1044C17.5 13.1052 17.5 13.1061 17.5 13.107C17.5 13.1079 17.5 13.1088 17.5 13.1097C17.5 13.1106 17.5 13.1115 17.5 13.1124C17.5 13.1133 17.5 13.1142 17.5 13.1151C17.5 13.116 17.5 13.1169 17.5 13.1179C17.5 13.1188 17.5 13.1197 17.5 13.1207C17.5 13.1216 17.5 13.1225 17.5 13.1235C17.5 13.1244 17.5 13.1254 17.5 13.1263C17.5 13.1273 17.5 13.1283 17.5 13.1292C17.5 13.1302 17.5 13.1312 17.5 13.1321C17.5 13.1331 17.5 13.1341 17.5 13.1351C17.5 13.1361 17.5 13.1371 17.5 13.1381C17.5 13.1391 17.5 13.1401 17.5 13.1411C17.5 13.1421 17.5 13.1431 17.5 13.1441C17.5 13.1451 17.5 13.1461 17.5 13.1472C17.5 13.1482 17.5 13.1492 17.5 13.1503C17.5 13.1513 17.5 13.1523 17.5 13.1534C17.5 13.1544 17.5 13.1555 17.5 13.1565C17.5 13.1576 17.5 13.1587 17.5 13.1597C17.5 13.1608 17.5 13.1619 17.5 13.1629C17.5 13.164 17.5 13.1651 17.5 13.1662C17.5 13.1673 17.5 13.1684 17.5 13.1694C17.5 13.1705 17.5 13.1716 17.5 13.1727C17.5 13.1739 17.5 13.175 17.5 13.1761C17.5 13.1772 17.5 13.1783 17.5 13.1794C17.5 13.1806 17.5 13.1817 17.5 13.1828C17.5 13.184 17.5 13.1851 17.5 13.1862C17.5 13.1874 17.5 13.1885 17.5 13.1897C17.5 13.1908 17.5 13.192 17.5 13.1931C17.5 13.1943 17.5 13.1955 17.5 13.1966C17.5 13.1978 17.5 13.199 17.5 13.2002C17.5 13.2014 17.5 13.2025 17.5 13.2037C17.5 13.2049 17.5 13.2061 17.5 13.2073C17.5 13.2085 17.5 13.2097 17.5 13.2109C17.5 13.2121 17.5 13.2133 17.5 13.2146C17.5 13.2158 17.5 13.217 17.5 13.2182C17.5 13.2195 17.5 13.2207 17.5 13.2219C17.5 13.2232 17.5 13.2244 17.5 13.2257C17.5 13.2269 17.5 13.2281 17.5 13.2294C17.5 13.2307 17.5 13.2319 17.5 13.2332C17.5 13.2344 17.5 13.2357 17.5 13.237C17.5 13.2383 17.5 13.2395 17.5 13.2408C17.5 13.2421 17.5 13.2434 17.5 13.2447C17.5 13.246 17.5 13.2473 17.5 13.2486C17.5 13.2499 17.5 13.2512 17.5 13.2525C17.5 13.2538 17.5 13.2551 17.5 13.2564C17.5 13.2577 17.5 13.2591 17.5 13.2604C17.5 13.2617 17.5 13.2631 17.5 13.2644C17.5 13.2657 17.5 13.2671 17.5 13.2684C17.5 13.2698 17.5 13.2711 17.5 13.2725C17.5 13.2738 17.5 13.2752 17.5 13.2765C17.5 13.2779 17.5 13.2793 17.5 13.2806C17.5 13.282 17.5 13.2834 17.5 13.2848C17.5 13.2862 17.5 13.2875 17.5 13.2889C17.5 13.2903 17.5 13.2917 17.5 13.2931C17.5 13.2945 17.5 13.2959 17.5 13.2973C17.5 13.2987 17.5 13.3001 17.5 13.3015C17.5 13.303 17.5 13.3044 17.5 13.3058C17.5 13.3072 17.5 13.3087 17.5 13.3101C17.5 13.3115 17.5 13.313 17.5 13.3144C17.5 13.3158 17.5 13.3173 17.5 13.3187C17.5 13.3202 17.5 13.3216 17.5 13.3231C17.5 13.3246 17.5 13.326 17.5 13.3275C17.5 13.329 17.5 13.3304 17.5 13.3319C17.5 13.3334 17.5 13.3349 17.5 13.3363C17.5 13.3378 17.5 13.3393 17.5 13.3408C17.5 13.3423 17.5 13.3438 17.5 13.3453C17.5 13.3468 17.5 13.3483 17.5 13.3498C17.5 13.3513 17.5 13.3528 17.5 13.3544C17.5 13.3559 17.5 13.3574 17.5 13.3589C17.5 13.3604 17.5 13.362 17.5 13.3635C17.5 13.365 17.5 13.3666 17.5 13.3681C17.5 13.3697 17.5 13.3712 17.5 13.3728C17.5 13.3743 17.5 13.3759 17.5 13.3774C17.5 13.379 17.5 13.3805 17.5 13.3821C17.5 13.3837 17.5 13.3853 17.5 13.3868C17.5 13.3884 17.5 13.39 17.5 13.3916C17.5 13.3931 17.5 13.3947 17.5 13.3963C17.5 13.3979 17.5 13.3995 17.5 13.4011C17.5 13.4027 17.5 13.4043 17.5 13.4059C17.5 13.4075 17.5 13.4091 17.5 13.4108C17.5 13.4124 17.5 13.414 17.5 13.4156C17.5 13.4172 17.5 13.4189 17.5 13.4205C17.5 13.4221 17.5 13.4238 17.5 13.4254C17.5 13.427 17.5 13.4287 17.5 13.4303C17.5 13.432 17.5 13.4336 17.5 13.4353C17.5 13.4369 17.5 13.4386 17.5 13.4403C17.5 13.4419 17.5 13.4436 17.5 13.4453C17.5 13.4469 17.5 13.4486 17.5 13.4503C17.5 13.452 17.5 13.4536 17.5 13.4553C17.5 13.457 17.5 13.4587 17.5 13.4604C17.5 13.4621 17.5 13.4638 17.5 13.4655C17.5 13.4672 17.5 13.4689 17.5 13.4706C17.5 13.4723 17.5 13.474 17.5 13.4758C17.5 13.4775 17.5 13.4792 17.5 13.4809C17.5 13.4826 17.5 13.4844 17.5 13.4861C17.5 13.4878 17.5 13.4896 17.5 13.4913C17.5 13.493 17.5 13.4948 17.5 13.4965C17.5 13.4983 17.5 13.5 17.5 13.5018C17.5 13.5035 17.5 13.5053 17.5 13.5071C17.5 13.5088 17.5 13.5106 17.5 13.5124C17.5 13.5141 17.5 13.5159 17.5 13.5177C17.5 13.5195 17.5 13.5212 17.5 13.523C17.5 13.5248 17.5 13.5266 17.5 13.5284C17.5 13.5302 17.5 13.532 17.5 13.5338C17.5 13.5356 17.5 13.5374 17.5 13.5392C17.5 13.541 17.5 13.5428 17.5 13.5446C17.5 13.5464 17.5 13.5482 17.5 13.5501C17.5 13.5519 17.5 13.5537 17.5 13.5555C17.5 13.5574 17.5 13.5592 17.5 13.561C17.5 13.5629 17.5 13.5647 17.5 13.5665C17.5 13.5684 17.5 13.5702 17.5 13.5721C17.5 13.5739 17.5 13.5758 17.5 13.5776C17.5 13.5795 17.5 13.5814 17.5 13.5832C17.5 13.5851 17.5 13.587 17.5 13.5888C17.5 13.5907 17.5 13.5926 17.5 13.5944C17.5 13.5963 17.5 13.5982 17.5 13.6001C17.5 13.602 17.5 13.6039 17.5 13.6058C17.5 13.6076 17.5 13.6095 17.5 13.6114C17.5 13.6133 17.5 13.6152 17.5 13.6171C17.5 13.619 17.5 13.621 17.5 13.6229C17.5 13.6248 17.5 13.6267 17.5 13.6286C17.5 13.6305 17.5 13.6325 17.5 13.6344C17.5 13.6363 17.5 13.6382 17.5 13.6402C17.5 13.6421 17.5 13.644 17.5 13.646C17.5 13.6479 17.5 13.6499 17.5 13.6518C17.5 13.6538 17.5 13.6557 17.5 13.6577C17.5 13.6596 17.5 13.6616 17.5 13.6635C17.5 13.6655 17.5 13.6674 17.5 13.6694C17.5 13.6714 17.5 13.6734 17.5 13.6753C17.5 13.6773 17.5 13.6793 17.5 13.6813C17.5 13.6832 17.5 13.6852 17.5 13.6872C17.5 13.6892 17.5 13.6912 17.5 13.6932C17.5 13.6952 17.5 13.6972 17.5 13.6992C17.5 13.7012 17.5 13.7032 17.5 13.7052C17.5 13.7072 17.5 13.7092 17.5 13.7112C17.5 13.7132 17.5 13.7152 17.5 13.7172C17.5 13.7193 17.5 13.7213 17.5 13.7233C17.5 13.7253 17.5 13.7274 17.5 13.7294C17.5 13.7314 17.5 13.7334 17.5 13.7355C17.5 13.7375 17.5 13.7396 17.5 13.7416C17.5 13.7437 17.5 13.7457 17.5 13.7477C17.5 13.7498 17.5 13.7519 17.5 13.7539C17.5 13.756 17.5 13.758 17.5 13.7601C17.5 13.7621 17.5 13.7642 17.5 13.7663C17.5 13.7683 17.5 13.7704 17.5 13.7725C17.5 13.7746 17.5 13.7766 17.5 13.7787C17.5 13.7808 17.5 13.7829 17.5 13.785C17.5 13.7871 17.5 13.7892 17.5 13.7913C17.5 13.7933 17.5 13.7954 17.5 13.7975C17.5 13.7996 17.5 13.8017 17.5 13.8038C17.5 13.806 17.5 13.8081 17.5 13.8102C17.5 13.8123 17.5 13.8144 17.5 13.8165C17.5 13.8186 17.5 13.8208 17.5 13.8229C17.5 13.825 17.5 13.8271 17.5 13.8293C17.5 13.8314 17.5 13.8335 17.5 13.8356C17.5 13.8378 17.5 13.8399 17.5 13.8421C17.5 13.8442 17.5 13.8463 17.5 13.8485C17.5 13.8506 17.5 13.8528 17.5 13.8549C17.5 13.8571 17.5 13.8592 17.5 13.8614C17.5 13.8636 17.5 13.8657 17.5 13.8679C17.5 13.8701 17.5 13.8722 17.5 13.8744C17.5 13.8766 17.5 13.8787 17.5 13.8809C17.5 13.8831 17.5 13.8853 17.5 13.8874C17.5 13.8896 17.5 13.8918 17.5 13.894C17.5 13.8962 17.5 13.8984 17.5 13.9006C17.5 13.9028 17.5 13.9049 17.5 13.9071C17.5 13.9093 17.5 13.9115 17.5 13.9137C17.5 13.9159 17.5 13.9182 17.5 13.9204C17.5 13.9226 17.5 13.9248 17.5 13.927C17.5 13.9292 17.5 13.9314 17.5 13.9337C17.5 13.9359 17.5 13.9381 17.5 13.9403C17.5 13.9425 17.5 13.9448 17.5 13.947C17.5 13.9492 17.5 13.9515 17.5 13.9537C17.5 13.9559 17.5 13.9582 17.5 13.9604C17.5 13.9627 17.5 13.9649 17.5 13.9671C17.5 13.9694 17.5 13.9716 17.5 13.9739C17.5 13.9761 17.5 13.9784 17.5 13.9807C17.5 13.9829 17.5 13.9852 17.5 13.9874C17.5 13.9897 17.5 13.992 17.5 13.9942C17.5 13.9965 17.5 13.9988 17.5 14.001C17.5 14.0033 17.5 14.0056 17.5 14.0079C17.5 14.0101 17.5 14.0124 17.5 14.0147C17.5 14.017 17.5 14.0193 17.5 14.0216C17.5 14.0239 17.5 14.0261 17.5 14.0284C17.5 14.0307 17.5 14.033 17.5 14.0353C17.5 14.0376 17.5 14.0399 17.5 14.0422C17.5 14.0445 17.5 14.0468 17.5 14.0491C17.5 14.0514 17.5 14.0538 17.5 14.0561C17.5 14.0584 17.5 14.0607 17.5 14.063C17.5 14.0653 17.5 14.0677 17.5 14.07C17.5 14.0723 17.5 14.0746 17.5 14.077C17.5 14.0793 17.5 14.0816 17.5 14.0839C17.5 14.0863 17.5 14.0886 17.5 14.091C17.5 14.0933 17.5 14.0956 17.5 14.098C17.5 14.1003 17.5 14.1027 17.5 14.105C17.5 14.1074 17.5 14.1097 17.5 14.1121C17.5 14.1144 17.5 14.1168 17.5 14.1191C17.5 14.1215 17.5 14.1238 17.5 14.1262C17.5 14.1286 17.5 14.1309 17.5 14.1333C17.5 14.1356 17.5 14.138 17.5 14.1404C17.5 14.1428 17.5 14.1451 17.5 14.1475C17.5 14.1499 17.5 14.1523 17.5 14.1546C17.5 14.157 17.5 14.1594 17.5 14.1618C17.5 14.1642 17.5 14.1665 17.5 14.1689C17.5 14.1713 17.5 14.1737 17.5 14.1761C17.5 14.1785 17.5 14.1809 17.5 14.1833C17.5 14.1857 17.5 14.1881 17.5 14.1905C17.5 14.1929 17.5 14.1953 17.5 14.1977C17.5 14.2001 17.5 14.2025 17.5 14.2049C17.5 14.2073 17.5 14.2097 17.5 14.2122C17.5 14.2146 17.5 14.217 17.5 14.2194C17.5 14.2218 17.5 14.2243 17.5 14.2267C17.5 14.2291 17.5 14.2315 17.5 14.234C17.5 14.2364 17.5 14.2388 17.5 14.2412C17.5 14.2437 17.5 14.2461 17.5 14.2485C17.5 14.251 17.5 14.2534 17.5 14.2559C17.5 14.2583 17.5 14.2607 17.5 14.2632C17.5 14.2656 17.5 14.2681 17.5 14.2705C17.5 14.273 17.5 14.2754 17.5 14.2779C17.5 14.2803 17.5 14.2828 17.5 14.2852C17.5 14.2877 17.5 14.2901 17.5 14.2926C17.5 14.2951 17.5 14.2975 17.5 14.3C17.5 14.3025 17.5 14.3049 17.5 14.3074C17.5 14.3099 17.5 14.3123 17.5 14.3148C17.5 14.3173 17.5 14.3198 17.5 14.3222C17.5 14.3247 17.5 14.3272 17.5 14.3297C17.5 14.3321 17.5 14.3346 17.5 14.3371C17.5 14.3396 17.5 14.3421 17.5 14.3446C17.5 14.3471 17.5 14.3495 17.5 14.352C17.5 14.3545 17.5 14.357 17.5 14.3595C17.5 14.362 17.5 14.3645 17.5 14.367C17.5 14.3695 17.5 14.372 17.5 14.3745C17.5 14.377 17.5 14.3795 17.5 14.382C17.5 14.3845 17.5 14.387 17.5 14.3895C17.5 14.3921 17.5 14.3946 17.5 14.3971C17.5 14.3996 17.5 14.4021 17.5 14.4046C17.5 14.4071 17.5 14.4097 17.5 14.4122C17.5 14.4147 17.5 14.4172 17.5 14.4197C17.5 14.4223 17.5 14.4248 17.5 14.4273C17.5 14.4299 17.5 14.4324 17.5 14.4349C17.5 14.4374 17.5 14.44 17.5 14.4425C17.5 14.445 17.5 14.4476 17.5 14.4501C17.5 14.4527 17.5 14.4552 17.5 14.4577C17.5 14.4603 17.5 14.4628 17.5 14.4654C17.5 14.4679 17.5 14.4705 17.5 14.473C17.5 14.4756 17.5 14.4781 17.5 14.4807C17.5 14.4832 17.5 14.4858 17.5 14.4883C17.5 14.4909 17.5 14.4934 17.5 14.496C17.5 14.4985 17.5 14.5011 17.5 14.5037C17.5 14.5062 17.5 14.5088 17.5 14.5114C17.5 14.5139 17.5 14.5165 17.5 14.5191C17.5 14.5216 17.5 14.5242 17.5 14.5268C17.5 14.5293 17.5 14.5319 17.5 14.5345C17.5 14.537 17.5 14.5396 17.5 14.5422C17.5 14.5448 17.5 14.5474 17.5 14.5499C17.5 14.5525 17.5 14.5551 17.5 14.5577C17.5 14.5603 17.5 14.5628 17.5 14.5654C17.5 14.568 17.5 14.5706 17.5 14.5732C17.5 14.5758 17.5 14.5784 17.5 14.581C17.5 14.5836 17.5 14.5862 17.5 14.5887C17.5 14.5913 17.5 14.5939 17.5 14.5965C17.5 14.5991 17.5 14.6017 17.5 14.6043C17.5 14.6069 17.5 14.6095 17.5 14.6121C17.5 14.6147 17.5 14.6173 17.5 14.62C17.5 14.6226 17.5 14.6252 17.5 14.6278C17.5 14.6304 17.5 14.633 17.5 14.6356C17.5 14.6382 17.5 14.6408 17.5 14.6434C17.5 14.6461 17.5 14.6487 17.5 14.6513C17.5 14.6539 17.5 14.6565 17.5 14.6591C17.5 14.6618 17.5 14.6644 17.5 14.667C17.5 14.6696 17.5 14.6723 17.5 14.6749C17.5 14.6775 17.5 14.6801 17.5 14.6828C17.5 14.6854 17.5 14.688 17.5 14.6906C17.5 14.6933 17.5 14.6959 17.5 14.6985C17.5 14.7012 17.5 14.7038 17.5 14.7064C17.5 14.7091 17.5 14.7117 17.5 14.7143C17.5 14.717 17.5 14.7196 17.5 14.7223C17.5 14.7249 17.5 14.7275 17.5 14.7302C17.5 14.7328 17.5 14.7355 17.5 14.7381C17.5 14.7408 17.5 14.7434 17.5 14.746C17.5 14.7487 17.5 14.7513 17.5 14.754C17.5 14.7566 17.5 14.7593 17.5 14.7619C17.5 14.7646 17.5 14.7672 17.5 14.7699C17.5 14.7726 17.5 14.7752 17.5 14.7779C17.5 14.7805 17.5 14.7832 17.5 14.7858C17.5 14.7885 17.5 14.7912 17.5 14.7938C17.5 14.7965 17.5 14.7991 17.5 14.8018C17.5 14.8045 17.5 14.8071 17.5 14.8098C17.5 14.8125 17.5 14.8151 17.5 14.8178C17.5 14.8205 17.5 14.8231 17.5 14.8258C17.5 14.8285 17.5 14.8311 17.5 14.8338C17.5 14.8365 17.5 14.8391 17.5 14.8418C17.5 14.8445 17.5 14.8472 17.5 14.8498C17.5 14.8525 17.5 14.8552 17.5 14.8579C17.5 14.8605 17.5 14.8632 17.5 14.8659C17.5 14.8686 17.5 14.8713 17.5 14.8739C17.5 14.8766 17.5 14.8793 17.5 14.882C17.5 14.8847 17.5 14.8873 17.5 14.89C17.5 14.8927 17.5 14.8954 17.5 14.8981C17.5 14.9008 17.5 14.9035 17.5 14.9061C17.5 14.9088 17.5 14.9115 17.5 14.9142C17.5 14.9169 17.5 14.9196 17.5 14.9223C17.5 14.925 17.5 14.9277 17.5 14.9304C17.5 14.9331 17.5 14.9357 17.5 14.9384C17.5 14.9411 17.5 14.9438 17.5 14.9465C17.5 14.9492 17.5 14.9519 17.5 14.9546C17.5 14.9573 17.5 14.96 17.5 14.9627C17.5 14.9654 17.5 14.9681 17.5 14.9708C17.5 14.9735 17.5 14.9762 17.5 14.9789C17.5 14.9816 17.5 14.9843 17.5 14.987C17.5 14.9897 17.5 14.9924 17.5 14.9951C17.5 14.9978 17.5 15.0006 17.5 15.0033C17.5 15.006 17.5 15.0087 17.5 15.0114C17.5 15.0141 17.5 15.0168 17.5 15.0195C17.5 15.0222 17.5 15.0249 17.5 15.0276C17.5 15.0304 17.5 15.0331 17.5 15.0358C17.5 15.0385 17.5 15.0412 17.5 15.0439C17.5 15.0466 17.5 15.0493 17.5 15.0521C17.5 15.0548 17.5 15.0575 17.5 15.0602C17.5 15.0629 17.5 15.0656 17.5 15.0684C17.5 15.0711 17.5 15.0738 17.5 15.0765C17.5 15.0792 17.5 15.0819 17.5 15.0847C17.5 15.0874 17.5 15.0901 17.5 15.0928C17.5 15.0955 17.5 15.0983 17.5 15.101C17.5 15.1037 17.5 15.1064 17.5 15.1092C17.5 15.1119 17.5 15.1146 17.5 15.1173C17.5 15.1201 17.5 15.1228 17.5 15.1255C17.5 15.1282 17.5 15.131 17.5 15.1337C17.5 15.1364 17.5 15.1391 17.5 15.1419C17.5 15.1446 17.5 15.1473 17.5 15.15C17.5 15.1528 17.5 15.1555 17.5 15.1582C17.5 15.161 17.5 15.1637 17.5 15.1664C17.5 15.1691 17.5 15.1719 17.5 15.1746C17.5 15.1773 17.5 15.1801 17.5 15.1828C17.5 15.1855 17.5 15.1883 17.5 15.191C17.5 15.1937 17.5 15.1965 17.5 15.1992C17.5 15.2019 17.5 15.2047 17.5 15.2074C17.5 15.2101 17.5 15.2129 17.5 15.2156C17.5 15.2183 17.5 15.2211 17.5 15.2238C17.5 15.2265 17.5 15.2293 17.5 15.232C17.5 15.2347 17.5 15.2375 17.5 15.2402C17.5 15.2429 17.5 15.2457 17.5 15.2484C17.5 15.2512 17.5 15.2539 17.5 15.2566C17.5 15.2594 17.5 15.2621 17.5 15.2648C17.5 15.2676 17.5 15.2703 17.5 15.2731C17.5 15.2758 17.5 15.2785 17.5 15.2813C17.5 15.284 17.5 15.2867 17.5 15.2895C17.5 15.2922 17.5 15.295 17.5 15.2977C17.5 15.3004 17.5 15.3032 17.5 15.3059C17.5 15.3087 17.5 15.3114 17.5 15.3141C17.5 15.3169 17.5 15.3196 17.5 15.3224C17.5 15.3251 17.5 15.3278 17.5 15.3306C17.5 15.3333 17.5 15.3361 17.5 15.3388C17.5 15.3416 17.5 15.3443 17.5 15.347C17.5 15.3498 17.5 15.3525 17.5 15.3553C17.5 15.358 17.5 15.3607 17.5 15.3635C17.5 15.3662 17.5 15.369 17.5 15.3717C17.5 15.3745 17.5 15.3772 17.5 15.3799C17.5 15.3827 17.5 15.3854 17.5 15.3882C17.5 15.3909 17.5 15.3937 17.5 15.3964C17.5 15.3991 17.5 15.4019 17.5 15.4046C17.5 15.4074 17.5 15.4101 17.5 15.4128C17.5 15.4156 17.5 15.4183 17.5 15.4211C17.5 15.4238 17.5 15.4266 17.5 15.4293C17.5 15.432 17.5 15.4348 17.5 15.4375C17.5 15.4403 17.5 15.443 17.5 15.4458C17.5 15.4485 17.5 15.4512 17.5 15.454C17.5 15.4567 17.5 15.4595 17.5 15.4622C17.5 15.465 17.5 15.4677 17.5 15.4704C17.5 15.4732 17.5 15.4759 17.5 15.4787C17.5 15.4814 17.5 15.4841 17.5 15.4869C17.5 15.4896 17.5 15.4924 17.5 15.4951C17.5 15.4979 17.5 15.5006 17.5 15.5033C17.5 15.5061 17.5 15.5088 17.5 15.5116C17.5 15.5143 17.5 15.517 17.5 15.5198C17.5 15.5225 17.5 15.5253 17.5 15.528C17.5 15.5307 17.5 15.5335 17.5 15.5362C17.5 15.539 17.5 15.5417 17.5 15.5444C17.5 15.5472 17.5 15.5499 17.5 15.5527C17.5 15.5554 17.5 15.5581 17.5 15.5609C17.5 15.5636 17.5 15.5663 17.5 15.5691C17.5 15.5718 17.5 15.5746 17.5 15.5773C17.5 15.58 17.5 15.5828 17.5 15.5855C17.5 15.5882 17.5 15.591 17.5 15.5937C17.5 15.5964 17.5 15.5992 17.5 15.6019C17.5 15.6047 17.5 15.6074 17.5 15.6101C17.5 15.6129 17.5 15.6156 17.5 15.6183C17.5 15.6211 17.5 15.6238 17.5 15.6265C17.5 15.6293 17.5 15.632 17.5 15.6347C17.5 15.6375 17.5 15.6402 17.5 15.6429C17.5 15.6456 17.5 15.6484 17.5 15.6511C17.5 15.6538 17.5 15.6566 17.5 15.6593C17.5 15.662 17.5 15.6648 17.5 15.6675C17.5 15.6702 17.5 15.6729 17.5 15.6757C17.5 15.6784 17.5 15.6811 17.5 15.6839C17.5 15.6866 17.5 15.6893 17.5 15.692C17.5 15.6948 17.5 15.6975 17.5 15.7002C17.5 15.7029 17.5 15.7057 17.5 15.7084C17.5 15.7111 17.5 15.7138 17.5 15.7166C17.5 15.7193 17.5 15.722 17.5 15.7247C17.5 15.7275 17.5 15.7302 17.5 15.7329C17.5 15.7356 17.5 15.7383 17.5 15.7411C17.5 15.7438 17.5 15.7465 17.5 15.7492C17.5 15.7519 17.5 15.7547 17.5 15.7574C17.5 15.7601 17.5 15.7628 17.5 15.7655C17.5 15.7682 17.5 15.771 17.5 15.7737C17.5 15.7764 17.5 15.7791 17.5 15.7818C17.5 15.7845 17.5 15.7872 17.5 15.79C17.5 15.7927 17.5 15.7954 17.5 15.7981C17.5 15.8008 17.5 15.8035 17.5 15.8062C17.5 15.8089 17.5 15.8116 17.5 15.8144C17.5 15.8171 17.5 15.8198 17.5 15.8225C17.5 15.8252 17.5 15.8279 17.5 15.8306C17.5 15.8333 17.5 15.836 17.5 15.8387C17.5 15.8414 17.5 15.8441 17.5 15.8468C17.5 15.8495 17.5 15.8522 17.5 15.8549C17.5 15.8576 17.5 15.8603 17.5 15.863C17.5 15.8657 17.5 15.8684 17.5 15.8711C17.5 15.8738 17.5 15.8765 17.5 15.8792C17.5 15.8819 17.5 15.8846 17.5 15.8873C17.5 15.89 17.5 15.8927 17.5 15.8954C17.5 15.8981 17.5 15.9008 17.5 15.9035C17.5 15.9062 17.5 15.9089 17.5 15.9116C17.5 15.9142 17.5 15.9169 17.5 15.9196C17.5 15.9223 17.5 15.925 17.5 15.9277C17.5 15.9304 17.5 15.9331 17.5 15.9357C17.5 15.9384 17.5 15.9411 17.5 15.9438C17.5 15.9465 17.5 15.9492 17.5 15.9518C17.5 15.9545 17.5 15.9572 17.5 15.9599C17.5 15.9626 17.5 15.9652 17.5 15.9679C17.5 15.9706 17.5 15.9733 17.5 15.9759C17.5 15.9786 17.5 15.9813 17.5 15.984C17.5 15.9866 17.5 15.9893 17.5 15.992C17.5 15.9947 17.5 15.9973 17.5 16H18.5ZM18 13C18.5 13 18.5 13 18.5 13C18.5 13 18.5 13 18.5 12.9999C18.5 12.9999 18.5 12.9999 18.5 12.9998C18.5 12.9998 18.5 12.9998 18.5 12.9997C18.5 12.9997 18.5 12.9996 18.5 12.9995C18.5 12.9995 18.5 12.9994 18.5 12.9993C18.5 12.9993 18.5 12.9992 18.5 12.9991C18.5 12.999 18.5 12.9989 18.5 12.9988C18.5 12.9987 18.5 12.9986 18.5 12.9985C18.5 12.9984 18.5 12.9983 18.5 12.9981C18.5 12.998 18.5 12.9979 18.5 12.9978C18.5 12.9976 18.5 12.9975 18.5 12.9973C18.5 12.9972 18.5 12.997 18.5 12.9969C18.5 12.9967 18.5 12.9965 18.5 12.9964C18.5 12.9962 18.5 12.996 18.5 12.9958C18.5 12.9957 18.5 12.9955 18.5 12.9953C18.5 12.9951 18.5 12.9949 18.5 12.9947C18.5 12.9945 18.5 12.9942 18.5 12.994C18.5 12.9938 18.5 12.9936 18.5 12.9934C18.5 12.9931 18.5 12.9929 18.5 12.9926C18.5 12.9924 18.5 12.9921 18.5 12.9919C18.5 12.9916 18.5 12.9914 18.5 12.9911C18.5 12.9908 18.5 12.9906 18.5 12.9903C18.5 12.99 18.5 12.9897 18.5 12.9894C18.5 12.9891 18.5 12.9888 18.5 12.9885C18.5 12.9882 18.5 12.9879 18.5 12.9876C18.5 12.9873 18.5 12.987 18.5 12.9867C18.5 12.9863 18.5 12.986 18.5 12.9857C18.5 12.9853 18.5 12.985 18.5 12.9846C18.5 12.9843 18.5 12.9839 18.5 12.9836C18.5 12.9832 18.5 12.9828 18.5 12.9825C18.5 12.9821 18.5 12.9817 18.5 12.9813C18.5 12.9809 18.5 12.9806 18.5 12.9802C18.5 12.9798 18.5 12.9794 18.5 12.979C18.5 12.9785 18.5 12.9781 18.5 12.9777C18.5 12.9773 18.5 12.9769 18.5 12.9764C18.5 12.976 18.5 12.9756 18.5 12.9751C18.5 12.9747 18.5 12.9742 18.5 12.9738C18.5 12.9733 18.5 12.9729 18.5 12.9724C18.5 12.9719 18.5 12.9715 18.5 12.971C18.5 12.9705 18.5 12.97 18.5 12.9696C18.5 12.9691 18.5 12.9686 18.5 12.9681C18.5 12.9676 18.5 12.9671 18.5 12.9666C18.5 12.9661 18.5 12.9655 18.5 12.965C18.5 12.9645 18.5 12.964 18.5 12.9634C18.5 12.9629 18.5 12.9624 18.5 12.9618C18.5 12.9613 18.5 12.9607 18.5 12.9602C18.5 12.9596 18.5 12.9591 18.5 12.9585C18.5 12.9579 18.5 12.9574 18.5 12.9568C18.5 12.9562 18.5 12.9556 18.5 12.955C18.5 12.9545 18.5 12.9539 18.5 12.9533C18.5 12.9527 18.5 12.9521 18.5 12.9515C18.5 12.9508 18.5 12.9502 18.5 12.9496C18.5 12.949 18.5 12.9484 18.5 12.9477C18.5 12.9471 18.5 12.9465 18.5 12.9458C18.5 12.9452 18.5 12.9445 18.5 12.9439C18.5 12.9432 18.5 12.9426 18.5 12.9419C18.5 12.9412 18.5 12.9406 18.5 12.9399C18.5 12.9392 18.5 12.9385 18.5 12.9378C18.5 12.9372 18.5 12.9365 18.5 12.9358C18.5 12.9351 18.5 12.9344 18.5 12.9337C18.5 12.933 18.5 12.9322 18.5 12.9315C18.5 12.9308 18.5 12.9301 18.5 12.9294C18.5 12.9286 18.5 12.9279 18.5 12.9272C18.5 12.9264 18.5 12.9257 18.5 12.9249C18.5 12.9242 18.5 12.9234 18.5 12.9227C18.5 12.9219 18.5 12.9211 18.5 12.9204C18.5 12.9196 18.5 12.9188 18.5 12.918C18.5 12.9172 18.5 12.9165 18.5 12.9157C18.5 12.9149 18.5 12.9141 18.5 12.9133C18.5 12.9125 18.5 12.9117 18.5 12.9108C18.5 12.91 18.5 12.9092 18.5 12.9084C18.5 12.9076 18.5 12.9067 18.5 12.9059C18.5 12.9051 18.5 12.9042 18.5 12.9034C18.5 12.9025 18.5 12.9017 18.5 12.9008C18.5 12.9 18.5 12.8991 18.5 12.8983C18.5 12.8974 18.5 12.8965 18.5 12.8956C18.5 12.8948 18.5 12.8939 18.5 12.893C18.5 12.8921 18.5 12.8912 18.5 12.8903C18.5 12.8894 18.5 12.8885 18.5 12.8876C18.5 12.8867 18.5 12.8858 18.5 12.8849C18.5 12.884 18.5 12.8831 18.5 12.8821C18.5 12.8812 18.5 12.8803 18.5 12.8793C18.5 12.8784 18.5 12.8775 18.5 12.8765C18.5 12.8756 18.5 12.8746 18.5 12.8737C18.5 12.8727 18.5 12.8717 18.5 12.8708C18.5 12.8698 18.5 12.8688 18.5 12.8679C18.5 12.8669 18.5 12.8659 18.5 12.8649C18.5 12.8639 18.5 12.8629 18.5 12.8619C18.5 12.8609 18.5 12.8599 18.5 12.8589C18.5 12.8579 18.5 12.8569 18.5 12.8559C18.5 12.8549 18.5 12.8539 18.5 12.8528C18.5 12.8518 18.5 12.8508 18.5 12.8497C18.5 12.8487 18.5 12.8477 18.5 12.8466C18.5 12.8456 18.5 12.8445 18.5 12.8435C18.5 12.8424 18.5 12.8413 18.5 12.8403C18.5 12.8392 18.5 12.8381 18.5 12.8371C18.5 12.836 18.5 12.8349 18.5 12.8338C18.5 12.8327 18.5 12.8316 18.5 12.8306C18.5 12.8295 18.5 12.8284 18.5 12.8273C18.5 12.8261 18.5 12.825 18.5 12.8239C18.5 12.8228 18.5 12.8217 18.5 12.8206C18.5 12.8194 18.5 12.8183 18.5 12.8172C18.5 12.816 18.5 12.8149 18.5 12.8138C18.5 12.8126 18.5 12.8115 18.5 12.8103C18.5 12.8092 18.5 12.808 18.5 12.8069C18.5 12.8057 18.5 12.8045 18.5 12.8034C18.5 12.8022 18.5 12.801 18.5 12.7998C18.5 12.7986 18.5 12.7975 18.5 12.7963C18.5 12.7951 18.5 12.7939 18.5 12.7927C18.5 12.7915 18.5 12.7903 18.5 12.7891C18.5 12.7879 18.5 12.7867 18.5 12.7854C18.5 12.7842 18.5 12.783 18.5 12.7818C18.5 12.7805 18.5 12.7793 18.5 12.7781C18.5 12.7768 18.5 12.7756 18.5 12.7743C18.5 12.7731 18.5 12.7719 18.5 12.7706C18.5 12.7693 18.5 12.7681 18.5 12.7668C18.5 12.7656 18.5 12.7643 18.5 12.763C18.5 12.7617 18.5 12.7605 18.5 12.7592C18.5 12.7579 18.5 12.7566 18.5 12.7553C18.5 12.754 18.5 12.7527 18.5 12.7514C18.5 12.7501 18.5 12.7488 18.5 12.7475C18.5 12.7462 18.5 12.7449 18.5 12.7436C18.5 12.7423 18.5 12.7409 18.5 12.7396C18.5 12.7383 18.5 12.7369 18.5 12.7356C18.5 12.7343 18.5 12.7329 18.5 12.7316C18.5 12.7302 18.5 12.7289 18.5 12.7275C18.5 12.7262 18.5 12.7248 18.5 12.7235C18.5 12.7221 18.5 12.7207 18.5 12.7194C18.5 12.718 18.5 12.7166 18.5 12.7152C18.5 12.7138 18.5 12.7125 18.5 12.7111C18.5 12.7097 18.5 12.7083 18.5 12.7069C18.5 12.7055 18.5 12.7041 18.5 12.7027C18.5 12.7013 18.5 12.6999 18.5 12.6985C18.5 12.697 18.5 12.6956 18.5 12.6942C18.5 12.6928 18.5 12.6913 18.5 12.6899C18.5 12.6885 18.5 12.687 18.5 12.6856C18.5 12.6842 18.5 12.6827 18.5 12.6813C18.5 12.6798 18.5 12.6784 18.5 12.6769C18.5 12.6754 18.5 12.674 18.5 12.6725C18.5 12.671 18.5 12.6696 18.5 12.6681C18.5 12.6666 18.5 12.6651 18.5 12.6637C18.5 12.6622 18.5 12.6607 18.5 12.6592C18.5 12.6577 18.5 12.6562 18.5 12.6547C18.5 12.6532 18.5 12.6517 18.5 12.6502C18.5 12.6487 18.5 12.6472 18.5 12.6456C18.5 12.6441 18.5 12.6426 18.5 12.6411C18.5 12.6396 18.5 12.638 18.5 12.6365C18.5 12.635 18.5 12.6334 18.5 12.6319C18.5 12.6303 18.5 12.6288 18.5 12.6272C18.5 12.6257 18.5 12.6241 18.5 12.6226C18.5 12.621 18.5 12.6195 18.5 12.6179C18.5 12.6163 18.5 12.6147 18.5 12.6132C18.5 12.6116 18.5 12.61 18.5 12.6084C18.5 12.6069 18.5 12.6053 18.5 12.6037C18.5 12.6021 18.5 12.6005 18.5 12.5989C18.5 12.5973 18.5 12.5957 18.5 12.5941C18.5 12.5925 18.5 12.5909 18.5 12.5892C18.5 12.5876 18.5 12.586 18.5 12.5844C18.5 12.5828 18.5 12.5811 18.5 12.5795C18.5 12.5779 18.5 12.5762 18.5 12.5746C18.5 12.573 18.5 12.5713 18.5 12.5697C18.5 12.568 18.5 12.5664 18.5 12.5647C18.5 12.5631 18.5 12.5614 18.5 12.5597C18.5 12.5581 18.5 12.5564 18.5 12.5547C18.5 12.5531 18.5 12.5514 18.5 12.5497C18.5 12.548 18.5 12.5464 18.5 12.5447C18.5 12.543 18.5 12.5413 18.5 12.5396C18.5 12.5379 18.5 12.5362 18.5 12.5345C18.5 12.5328 18.5 12.5311 18.5 12.5294C18.5 12.5277 18.5 12.526 18.5 12.5242C18.5 12.5225 18.5 12.5208 18.5 12.5191C18.5 12.5174 18.5 12.5156 18.5 12.5139C18.5 12.5122 18.5 12.5104 18.5 12.5087C18.5 12.507 18.5 12.5052 18.5 12.5035C18.5 12.5017 18.5 12.5 18.5 12.4982C18.5 12.4965 18.5 12.4947 18.5 12.4929C18.5 12.4912 18.5 12.4894 18.5 12.4876C18.5 12.4859 18.5 12.4841 18.5 12.4823C18.5 12.4805 18.5 12.4788 18.5 12.477C18.5 12.4752 18.5 12.4734 18.5 12.4716C18.5 12.4698 18.5 12.468 18.5 12.4662C18.5 12.4644 18.5 12.4626 18.5 12.4608C18.5 12.459 18.5 12.4572 18.5 12.4554C18.5 12.4536 18.5 12.4518 18.5 12.4499C18.5 12.4481 18.5 12.4463 18.5 12.4445C18.5 12.4426 18.5 12.4408 18.5 12.439C18.5 12.4371 18.5 12.4353 18.5 12.4335C18.5 12.4316 18.5 12.4298 18.5 12.4279C18.5 12.4261 18.5 12.4242 18.5 12.4224C18.5 12.4205 18.5 12.4186 18.5 12.4168C18.5 12.4149 18.5 12.413 18.5 12.4112C18.5 12.4093 18.5 12.4074 18.5 12.4056C18.5 12.4037 18.5 12.4018 18.5 12.3999C18.5 12.398 18.5 12.3961 18.5 12.3942C18.5 12.3924 18.5 12.3905 18.5 12.3886C18.5 12.3867 18.5 12.3848 18.5 12.3829C18.5 12.381 18.5 12.379 18.5 12.3771C18.5 12.3752 18.5 12.3733 18.5 12.3714C18.5 12.3695 18.5 12.3675 18.5 12.3656C18.5 12.3637 18.5 12.3618 18.5 12.3598C18.5 12.3579 18.5 12.356 18.5 12.354C18.5 12.3521 18.5 12.3501 18.5 12.3482C18.5 12.3462 18.5 12.3443 18.5 12.3423C18.5 12.3404 18.5 12.3384 18.5 12.3365C18.5 12.3345 18.5 12.3326 18.5 12.3306C18.5 12.3286 18.5 12.3266 18.5 12.3247C18.5 12.3227 18.5 12.3207 18.5 12.3187C18.5 12.3168 18.5 12.3148 18.5 12.3128C18.5 12.3108 18.5 12.3088 18.5 12.3068C18.5 12.3048 18.5 12.3028 18.5 12.3008C18.5 12.2988 18.5 12.2968 18.5 12.2948C18.5 12.2928 18.5 12.2908 18.5 12.2888C18.5 12.2868 18.5 12.2848 18.5 12.2828C18.5 12.2807 18.5 12.2787 18.5 12.2767C18.5 12.2747 18.5 12.2726 18.5 12.2706C18.5 12.2686 18.5 12.2666 18.5 12.2645C18.5 12.2625 18.5 12.2604 18.5 12.2584C18.5 12.2563 18.5 12.2543 18.5 12.2523C18.5 12.2502 18.5 12.2481 18.5 12.2461C18.5 12.244 18.5 12.242 18.5 12.2399C18.5 12.2379 18.5 12.2358 18.5 12.2337C18.5 12.2317 18.5 12.2296 18.5 12.2275C18.5 12.2254 18.5 12.2234 18.5 12.2213C18.5 12.2192 18.5 12.2171 18.5 12.215C18.5 12.2129 18.5 12.2108 18.5 12.2087C18.5 12.2067 18.5 12.2046 18.5 12.2025C18.5 12.2004 18.5 12.1983 18.5 12.1962C18.5 12.194 18.5 12.1919 18.5 12.1898C18.5 12.1877 18.5 12.1856 18.5 12.1835C18.5 12.1814 18.5 12.1792 18.5 12.1771C18.5 12.175 18.5 12.1729 18.5 12.1707C18.5 12.1686 18.5 12.1665 18.5 12.1644C18.5 12.1622 18.5 12.1601 18.5 12.1579C18.5 12.1558 18.5 12.1537 18.5 12.1515C18.5 12.1494 18.5 12.1472 18.5 12.1451C18.5 12.1429 18.5 12.1408 18.5 12.1386C18.5 12.1364 18.5 12.1343 18.5 12.1321C18.5 12.1299 18.5 12.1278 18.5 12.1256C18.5 12.1234 18.5 12.1213 18.5 12.1191C18.5 12.1169 18.5 12.1147 18.5 12.1126C18.5 12.1104 18.5 12.1082 18.5 12.106C18.5 12.1038 18.5 12.1016 18.5 12.0994C18.5 12.0972 18.5 12.0951 18.5 12.0929C18.5 12.0907 18.5 12.0885 18.5 12.0863C18.5 12.0841 18.5 12.0818 18.5 12.0796C18.5 12.0774 18.5 12.0752 18.5 12.073C18.5 12.0708 18.5 12.0686 18.5 12.0663C18.5 12.0641 18.5 12.0619 18.5 12.0597C18.5 12.0575 18.5 12.0552 18.5 12.053C18.5 12.0508 18.5 12.0485 18.5 12.0463C18.5 12.0441 18.5 12.0418 18.5 12.0396C18.5 12.0373 18.5 12.0351 18.5 12.0329C18.5 12.0306 18.5 12.0284 18.5 12.0261C18.5 12.0239 18.5 12.0216 18.5 12.0193C18.5 12.0171 18.5 12.0148 18.5 12.0126C18.5 12.0103 18.5 12.008 18.5 12.0058C18.5 12.0035 18.5 12.0012 18.5 11.999C18.5 11.9967 18.5 11.9944 18.5 11.9921C18.5 11.9899 18.5 11.9876 18.5 11.9853C18.5 11.983 18.5 11.9807 18.5 11.9784C18.5 11.9761 18.5 11.9739 18.5 11.9716C18.5 11.9693 18.5 11.967 18.5 11.9647C18.5 11.9624 18.5 11.9601 18.5 11.9578C18.5 11.9555 18.5 11.9532 18.5 11.9509C18.5 11.9486 18.5 11.9462 18.5 11.9439C18.5 11.9416 18.5 11.9393 18.5 11.937C18.5 11.9347 18.5 11.9323 18.5 11.93C18.5 11.9277 18.5 11.9254 18.5 11.923C18.5 11.9207 18.5 11.9184 18.5 11.9161C18.5 11.9137 18.5 11.9114 18.5 11.909C18.5 11.9067 18.5 11.9044 18.5 11.902C18.5 11.8997 18.5 11.8973 18.5 11.895C18.5 11.8926 18.5 11.8903 18.5 11.8879C18.5 11.8856 18.5 11.8832 18.5 11.8809C18.5 11.8785 18.5 11.8762 18.5 11.8738C18.5 11.8714 18.5 11.8691 18.5 11.8667C18.5 11.8644 18.5 11.862 18.5 11.8596C18.5 11.8572 18.5 11.8549 18.5 11.8525C18.5 11.8501 18.5 11.8477 18.5 11.8454C18.5 11.843 18.5 11.8406 18.5 11.8382C18.5 11.8358 18.5 11.8335 18.5 11.8311C18.5 11.8287 18.5 11.8263 18.5 11.8239C18.5 11.8215 18.5 11.8191 18.5 11.8167C18.5 11.8143 18.5 11.8119 18.5 11.8095C18.5 11.8071 18.5 11.8047 18.5 11.8023C18.5 11.7999 18.5 11.7975 18.5 11.7951C18.5 11.7927 18.5 11.7903 18.5 11.7878C18.5 11.7854 18.5 11.783 18.5 11.7806C18.5 11.7782 18.5 11.7757 18.5 11.7733C18.5 11.7709 18.5 11.7685 18.5 11.766C18.5 11.7636 18.5 11.7612 18.5 11.7588C18.5 11.7563 18.5 11.7539 18.5 11.7515C18.5 11.749 18.5 11.7466 18.5 11.7441C18.5 11.7417 18.5 11.7393 18.5 11.7368C18.5 11.7344 18.5 11.7319 18.5 11.7295C18.5 11.727 18.5 11.7246 18.5 11.7221C18.5 11.7197 18.5 11.7172 18.5 11.7148C18.5 11.7123 18.5 11.7099 18.5 11.7074C18.5 11.7049 18.5 11.7025 18.5 11.7C18.5 11.6975 18.5 11.6951 18.5 11.6926C18.5 11.6901 18.5 11.6877 18.5 11.6852C18.5 11.6827 18.5 11.6802 18.5 11.6778C18.5 11.6753 18.5 11.6728 18.5 11.6703C18.5 11.6679 18.5 11.6654 18.5 11.6629C18.5 11.6604 18.5 11.6579 18.5 11.6554C18.5 11.6529 18.5 11.6505 18.5 11.648C18.5 11.6455 18.5 11.643 18.5 11.6405C18.5 11.638 18.5 11.6355 18.5 11.633C18.5 11.6305 18.5 11.628 18.5 11.6255C18.5 11.623 18.5 11.6205 18.5 11.618C18.5 11.6155 18.5 11.613 18.5 11.6105C18.5 11.6079 18.5 11.6054 18.5 11.6029C18.5 11.6004 18.5 11.5979 18.5 11.5954C18.5 11.5929 18.5 11.5903 18.5 11.5878C18.5 11.5853 18.5 11.5828 18.5 11.5803C18.5 11.5777 18.5 11.5752 18.5 11.5727C18.5 11.5701 18.5 11.5676 18.5 11.5651C18.5 11.5626 18.5 11.56 18.5 11.5575C18.5 11.555 18.5 11.5524 18.5 11.5499C18.5 11.5473 18.5 11.5448 18.5 11.5423C18.5 11.5397 18.5 11.5372 18.5 11.5346C18.5 11.5321 18.5 11.5295 18.5 11.527C18.5 11.5244 18.5 11.5219 18.5 11.5193C18.5 11.5168 18.5 11.5142 18.5 11.5117C18.5 11.5091 18.5 11.5066 18.5 11.504C18.5 11.5015 18.5 11.4989 18.5 11.4963C18.5 11.4938 18.5 11.4912 18.5 11.4886C18.5 11.4861 18.5 11.4835 18.5 11.4809C18.5 11.4784 18.5 11.4758 18.5 11.4732C18.5 11.4707 18.5 11.4681 18.5 11.4655C18.5 11.463 18.5 11.4604 18.5 11.4578C18.5 11.4552 18.5 11.4526 18.5 11.4501C18.5 11.4475 18.5 11.4449 18.5 11.4423C18.5 11.4397 18.5 11.4372 18.5 11.4346C18.5 11.432 18.5 11.4294 18.5 11.4268C18.5 11.4242 18.5 11.4216 18.5 11.419C18.5 11.4164 18.5 11.4138 18.5 11.4113C18.5 11.4087 18.5 11.4061 18.5 11.4035C18.5 11.4009 18.5 11.3983 18.5 11.3957C18.5 11.3931 18.5 11.3905 18.5 11.3879C18.5 11.3853 18.5 11.3827 18.5 11.38C18.5 11.3774 18.5 11.3748 18.5 11.3722C18.5 11.3696 18.5 11.367 18.5 11.3644C18.5 11.3618 18.5 11.3592 18.5 11.3566C18.5 11.3539 18.5 11.3513 18.5 11.3487C18.5 11.3461 18.5 11.3435 18.5 11.3409C18.5 11.3382 18.5 11.3356 18.5 11.333C18.5 11.3304 18.5 11.3277 18.5 11.3251C18.5 11.3225 18.5 11.3199 18.5 11.3172C18.5 11.3146 18.5 11.312 18.5 11.3094C18.5 11.3067 18.5 11.3041 18.5 11.3015C18.5 11.2988 18.5 11.2962 18.5 11.2936C18.5 11.2909 18.5 11.2883 18.5 11.2857C18.5 11.283 18.5 11.2804 18.5 11.2777C18.5 11.2751 18.5 11.2725 18.5 11.2698C18.5 11.2672 18.5 11.2645 18.5 11.2619C18.5 11.2592 18.5 11.2566 18.5 11.254C18.5 11.2513 18.5 11.2487 18.5 11.246C18.5 11.2434 18.5 11.2407 18.5 11.2381C18.5 11.2354 18.5 11.2328 18.5 11.2301C18.5 11.2274 18.5 11.2248 18.5 11.2221C18.5 11.2195 18.5 11.2168 18.5 11.2142C18.5 11.2115 18.5 11.2088 18.5 11.2062C18.5 11.2035 18.5 11.2009 18.5 11.1982C18.5 11.1955 18.5 11.1929 18.5 11.1902C18.5 11.1875 18.5 11.1849 18.5 11.1822C18.5 11.1795 18.5 11.1769 18.5 11.1742C18.5 11.1715 18.5 11.1689 18.5 11.1662C18.5 11.1635 18.5 11.1609 18.5 11.1582C18.5 11.1555 18.5 11.1528 18.5 11.1502C18.5 11.1475 18.5 11.1448 18.5 11.1421C18.5 11.1395 18.5 11.1368 18.5 11.1341C18.5 11.1314 18.5 11.1287 18.5 11.1261C18.5 11.1234 18.5 11.1207 18.5 11.118C18.5 11.1153 18.5 11.1127 18.5 11.11C18.5 11.1073 18.5 11.1046 18.5 11.1019C18.5 11.0992 18.5 11.0965 18.5 11.0939C18.5 11.0912 18.5 11.0885 18.5 11.0858C18.5 11.0831 18.5 11.0804 18.5 11.0777C18.5 11.075 18.5 11.0723 18.5 11.0696C18.5 11.0669 18.5 11.0643 18.5 11.0616C18.5 11.0589 18.5 11.0562 18.5 11.0535C18.5 11.0508 18.5 11.0481 18.5 11.0454C18.5 11.0427 18.5 11.04 18.5 11.0373C18.5 11.0346 18.5 11.0319 18.5 11.0292C18.5 11.0265 18.5 11.0238 18.5 11.0211C18.5 11.0184 18.5 11.0157 18.5 11.013C18.5 11.0103 18.5 11.0076 18.5 11.0049C18.5 11.0022 18.5 10.9994 18.5 10.9967C18.5 10.994 18.5 10.9913 18.5 10.9886C18.5 10.9859 18.5 10.9832 18.5 10.9805C18.5 10.9778 18.5 10.9751 18.5 10.9724C18.5 10.9696 18.5 10.9669 18.5 10.9642C18.5 10.9615 18.5 10.9588 18.5 10.9561C18.5 10.9534 18.5 10.9507 18.5 10.9479C18.5 10.9452 18.5 10.9425 18.5 10.9398C18.5 10.9371 18.5 10.9344 18.5 10.9316C18.5 10.9289 18.5 10.9262 18.5 10.9235C18.5 10.9208 18.5 10.9181 18.5 10.9153C18.5 10.9126 18.5 10.9099 18.5 10.9072C18.5 10.9045 18.5 10.9017 18.5 10.899C18.5 10.8963 18.5 10.8936 18.5 10.8908C18.5 10.8881 18.5 10.8854 18.5 10.8827C18.5 10.8799 18.5 10.8772 18.5 10.8745C18.5 10.8718 18.5 10.869 18.5 10.8663C18.5 10.8636 18.5 10.8609 18.5 10.8581C18.5 10.8554 18.5 10.8527 18.5 10.85C18.5 10.8472 18.5 10.8445 18.5 10.8418C18.5 10.839 18.5 10.8363 18.5 10.8336C18.5 10.8309 18.5 10.8281 18.5 10.8254C18.5 10.8227 18.5 10.8199 18.5 10.8172C18.5 10.8145 18.5 10.8117 18.5 10.809C18.5 10.8063 18.5 10.8035 18.5 10.8008C18.5 10.7981 18.5 10.7953 18.5 10.7926C18.5 10.7899 18.5 10.7871 18.5 10.7844C18.5 10.7817 18.5 10.7789 18.5 10.7762C18.5 10.7735 18.5 10.7707 18.5 10.768C18.5 10.7653 18.5 10.7625 18.5 10.7598C18.5 10.7571 18.5 10.7543 18.5 10.7516C18.5 10.7488 18.5 10.7461 18.5 10.7434C18.5 10.7406 18.5 10.7379 18.5 10.7352C18.5 10.7324 18.5 10.7297 18.5 10.7269C18.5 10.7242 18.5 10.7215 18.5 10.7187C18.5 10.716 18.5 10.7133 18.5 10.7105C18.5 10.7078 18.5 10.705 18.5 10.7023C18.5 10.6996 18.5 10.6968 18.5 10.6941C18.5 10.6913 18.5 10.6886 18.5 10.6859C18.5 10.6831 18.5 10.6804 18.5 10.6776C18.5 10.6749 18.5 10.6722 18.5 10.6694C18.5 10.6667 18.5 10.6639 18.5 10.6612C18.5 10.6584 18.5 10.6557 18.5 10.653C18.5 10.6502 18.5 10.6475 18.5 10.6447C18.5 10.642 18.5 10.6393 18.5 10.6365C18.5 10.6338 18.5 10.631 18.5 10.6283C18.5 10.6255 18.5 10.6228 18.5 10.6201C18.5 10.6173 18.5 10.6146 18.5 10.6118C18.5 10.6091 18.5 10.6063 18.5 10.6036C18.5 10.6009 18.5 10.5981 18.5 10.5954C18.5 10.5926 18.5 10.5899 18.5 10.5872C18.5 10.5844 18.5 10.5817 18.5 10.5789C18.5 10.5762 18.5 10.5734 18.5 10.5707C18.5 10.568 18.5 10.5652 18.5 10.5625C18.5 10.5597 18.5 10.557 18.5 10.5542C18.5 10.5515 18.5 10.5488 18.5 10.546C18.5 10.5433 18.5 10.5405 18.5 10.5378C18.5 10.535 18.5 10.5323 18.5 10.5296C18.5 10.5268 18.5 10.5241 18.5 10.5213C18.5 10.5186 18.5 10.5159 18.5 10.5131C18.5 10.5104 18.5 10.5076 18.5 10.5049C18.5 10.5021 18.5 10.4994 18.5 10.4967C18.5 10.4939 18.5 10.4912 18.5 10.4884C18.5 10.4857 18.5 10.483 18.5 10.4802C18.5 10.4775 18.5 10.4747 18.5 10.472C18.5 10.4693 18.5 10.4665 18.5 10.4638C18.5 10.461 18.5 10.4583 18.5 10.4556C18.5 10.4528 18.5 10.4501 18.5 10.4473C18.5 10.4446 18.5 10.4419 18.5 10.4391C18.5 10.4364 18.5 10.4337 18.5 10.4309C18.5 10.4282 18.5 10.4254 18.5 10.4227C18.5 10.42 18.5 10.4172 18.5 10.4145C18.5 10.4118 18.5 10.409 18.5 10.4063C18.5 10.4036 18.5 10.4008 18.5 10.3981C18.5 10.3953 18.5 10.3926 18.5 10.3899C18.5 10.3871 18.5 10.3844 18.5 10.3817C18.5 10.3789 18.5 10.3762 18.5 10.3735C18.5 10.3707 18.5 10.368 18.5 10.3653C18.5 10.3625 18.5 10.3598 18.5 10.3571C18.5 10.3544 18.5 10.3516 18.5 10.3489C18.5 10.3462 18.5 10.3434 18.5 10.3407C18.5 10.338 18.5 10.3352 18.5 10.3325C18.5 10.3298 18.5 10.3271 18.5 10.3243C18.5 10.3216 18.5 10.3189 18.5 10.3161C18.5 10.3134 18.5 10.3107 18.5 10.308C18.5 10.3052 18.5 10.3025 18.5 10.2998C18.5 10.2971 18.5 10.2943 18.5 10.2916C18.5 10.2889 18.5 10.2862 18.5 10.2834C18.5 10.2807 18.5 10.278 18.5 10.2753C18.5 10.2725 18.5 10.2698 18.5 10.2671C18.5 10.2644 18.5 10.2617 18.5 10.2589C18.5 10.2562 18.5 10.2535 18.5 10.2508C18.5 10.2481 18.5 10.2453 18.5 10.2426C18.5 10.2399 18.5 10.2372 18.5 10.2345C18.5 10.2318 18.5 10.229 18.5 10.2263C18.5 10.2236 18.5 10.2209 18.5 10.2182C18.5 10.2155 18.5 10.2128 18.5 10.21C18.5 10.2073 18.5 10.2046 18.5 10.2019C18.5 10.1992 18.5 10.1965 18.5 10.1938C18.5 10.1911 18.5 10.1884 18.5 10.1856C18.5 10.1829 18.5 10.1802 18.5 10.1775C18.5 10.1748 18.5 10.1721 18.5 10.1694C18.5 10.1667 18.5 10.164 18.5 10.1613C18.5 10.1586 18.5 10.1559 18.5 10.1532C18.5 10.1505 18.5 10.1478 18.5 10.1451C18.5 10.1424 18.5 10.1397 18.5 10.137C18.5 10.1343 18.5 10.1316 18.5 10.1289C18.5 10.1262 18.5 10.1235 18.5 10.1208C18.5 10.1181 18.5 10.1154 18.5 10.1127C18.5 10.11 18.5 10.1073 18.5 10.1046C18.5 10.1019 18.5 10.0992 18.5 10.0965C18.5 10.0938 18.5 10.0911 18.5 10.0884C18.5 10.0858 18.5 10.0831 18.5 10.0804C18.5 10.0777 18.5 10.075 18.5 10.0723C18.5 10.0696 18.5 10.0669 18.5 10.0643C18.5 10.0616 18.5 10.0589 18.5 10.0562C18.5 10.0535 18.5 10.0508 18.5 10.0482C18.5 10.0455 18.5 10.0428 18.5 10.0401C18.5 10.0374 18.5 10.0348 18.5 10.0321C18.5 10.0294 18.5 10.0267 18.5 10.0241C18.5 10.0214 18.5 10.0187 18.5 10.016C18.5 10.0134 18.5 10.0107 18.5 10.008C18.5 10.0053 18.5 10.0027 18.5 10H17.5C17.5 10.0027 17.5 10.0053 17.5 10.008C17.5 10.0107 17.5 10.0134 17.5 10.016C17.5 10.0187 17.5 10.0214 17.5 10.0241C17.5 10.0267 17.5 10.0294 17.5 10.0321C17.5 10.0348 17.5 10.0374 17.5 10.0401C17.5 10.0428 17.5 10.0455 17.5 10.0482C17.5 10.0508 17.5 10.0535 17.5 10.0562C17.5 10.0589 17.5 10.0616 17.5 10.0643C17.5 10.0669 17.5 10.0696 17.5 10.0723C17.5 10.075 17.5 10.0777 17.5 10.0804C17.5 10.0831 17.5 10.0858 17.5 10.0884C17.5 10.0911 17.5 10.0938 17.5 10.0965C17.5 10.0992 17.5 10.1019 17.5 10.1046C17.5 10.1073 17.5 10.11 17.5 10.1127C17.5 10.1154 17.5 10.1181 17.5 10.1208C17.5 10.1235 17.5 10.1262 17.5 10.1289C17.5 10.1316 17.5 10.1343 17.5 10.137C17.5 10.1397 17.5 10.1424 17.5 10.1451C17.5 10.1478 17.5 10.1505 17.5 10.1532C17.5 10.1559 17.5 10.1586 17.5 10.1613C17.5 10.164 17.5 10.1667 17.5 10.1694C17.5 10.1721 17.5 10.1748 17.5 10.1775C17.5 10.1802 17.5 10.1829 17.5 10.1856C17.5 10.1884 17.5 10.1911 17.5 10.1938C17.5 10.1965 17.5 10.1992 17.5 10.2019C17.5 10.2046 17.5 10.2073 17.5 10.21C17.5 10.2128 17.5 10.2155 17.5 10.2182C17.5 10.2209 17.5 10.2236 17.5 10.2263C17.5 10.229 17.5 10.2318 17.5 10.2345C17.5 10.2372 17.5 10.2399 17.5 10.2426C17.5 10.2453 17.5 10.2481 17.5 10.2508C17.5 10.2535 17.5 10.2562 17.5 10.2589C17.5 10.2617 17.5 10.2644 17.5 10.2671C17.5 10.2698 17.5 10.2725 17.5 10.2753C17.5 10.278 17.5 10.2807 17.5 10.2834C17.5 10.2862 17.5 10.2889 17.5 10.2916C17.5 10.2943 17.5 10.2971 17.5 10.2998C17.5 10.3025 17.5 10.3052 17.5 10.308C17.5 10.3107 17.5 10.3134 17.5 10.3161C17.5 10.3189 17.5 10.3216 17.5 10.3243C17.5 10.3271 17.5 10.3298 17.5 10.3325C17.5 10.3352 17.5 10.338 17.5 10.3407C17.5 10.3434 17.5 10.3462 17.5 10.3489C17.5 10.3516 17.5 10.3544 17.5 10.3571C17.5 10.3598 17.5 10.3625 17.5 10.3653C17.5 10.368 17.5 10.3707 17.5 10.3735C17.5 10.3762 17.5 10.3789 17.5 10.3817C17.5 10.3844 17.5 10.3871 17.5 10.3899C17.5 10.3926 17.5 10.3953 17.5 10.3981C17.5 10.4008 17.5 10.4036 17.5 10.4063C17.5 10.409 17.5 10.4118 17.5 10.4145C17.5 10.4172 17.5 10.42 17.5 10.4227C17.5 10.4254 17.5 10.4282 17.5 10.4309C17.5 10.4337 17.5 10.4364 17.5 10.4391C17.5 10.4419 17.5 10.4446 17.5 10.4473C17.5 10.4501 17.5 10.4528 17.5 10.4556C17.5 10.4583 17.5 10.461 17.5 10.4638C17.5 10.4665 17.5 10.4693 17.5 10.472C17.5 10.4747 17.5 10.4775 17.5 10.4802C17.5 10.483 17.5 10.4857 17.5 10.4884C17.5 10.4912 17.5 10.4939 17.5 10.4967C17.5 10.4994 17.5 10.5021 17.5 10.5049C17.5 10.5076 17.5 10.5104 17.5 10.5131C17.5 10.5159 17.5 10.5186 17.5 10.5213C17.5 10.5241 17.5 10.5268 17.5 10.5296C17.5 10.5323 17.5 10.535 17.5 10.5378C17.5 10.5405 17.5 10.5433 17.5 10.546C17.5 10.5488 17.5 10.5515 17.5 10.5542C17.5 10.557 17.5 10.5597 17.5 10.5625C17.5 10.5652 17.5 10.568 17.5 10.5707C17.5 10.5734 17.5 10.5762 17.5 10.5789C17.5 10.5817 17.5 10.5844 17.5 10.5872C17.5 10.5899 17.5 10.5926 17.5 10.5954C17.5 10.5981 17.5 10.6009 17.5 10.6036C17.5 10.6063 17.5 10.6091 17.5 10.6118C17.5 10.6146 17.5 10.6173 17.5 10.6201C17.5 10.6228 17.5 10.6255 17.5 10.6283C17.5 10.631 17.5 10.6338 17.5 10.6365C17.5 10.6393 17.5 10.642 17.5 10.6447C17.5 10.6475 17.5 10.6502 17.5 10.653C17.5 10.6557 17.5 10.6584 17.5 10.6612C17.5 10.6639 17.5 10.6667 17.5 10.6694C17.5 10.6722 17.5 10.6749 17.5 10.6776C17.5 10.6804 17.5 10.6831 17.5 10.6859C17.5 10.6886 17.5 10.6913 17.5 10.6941C17.5 10.6968 17.5 10.6996 17.5 10.7023C17.5 10.705 17.5 10.7078 17.5 10.7105C17.5 10.7133 17.5 10.716 17.5 10.7187C17.5 10.7215 17.5 10.7242 17.5 10.7269C17.5 10.7297 17.5 10.7324 17.5 10.7352C17.5 10.7379 17.5 10.7406 17.5 10.7434C17.5 10.7461 17.5 10.7488 17.5 10.7516C17.5 10.7543 17.5 10.7571 17.5 10.7598C17.5 10.7625 17.5 10.7653 17.5 10.768C17.5 10.7707 17.5 10.7735 17.5 10.7762C17.5 10.7789 17.5 10.7817 17.5 10.7844C17.5 10.7871 17.5 10.7899 17.5 10.7926C17.5 10.7953 17.5 10.7981 17.5 10.8008C17.5 10.8035 17.5 10.8063 17.5 10.809C17.5 10.8117 17.5 10.8145 17.5 10.8172C17.5 10.8199 17.5 10.8227 17.5 10.8254C17.5 10.8281 17.5 10.8309 17.5 10.8336C17.5 10.8363 17.5 10.839 17.5 10.8418C17.5 10.8445 17.5 10.8472 17.5 10.85C17.5 10.8527 17.5 10.8554 17.5 10.8581C17.5 10.8609 17.5 10.8636 17.5 10.8663C17.5 10.869 17.5 10.8718 17.5 10.8745C17.5 10.8772 17.5 10.8799 17.5 10.8827C17.5 10.8854 17.5 10.8881 17.5 10.8908C17.5 10.8936 17.5 10.8963 17.5 10.899C17.5 10.9017 17.5 10.9045 17.5 10.9072C17.5 10.9099 17.5 10.9126 17.5 10.9153C17.5 10.9181 17.5 10.9208 17.5 10.9235C17.5 10.9262 17.5 10.9289 17.5 10.9316C17.5 10.9344 17.5 10.9371 17.5 10.9398C17.5 10.9425 17.5 10.9452 17.5 10.9479C17.5 10.9507 17.5 10.9534 17.5 10.9561C17.5 10.9588 17.5 10.9615 17.5 10.9642C17.5 10.9669 17.5 10.9696 17.5 10.9724C17.5 10.9751 17.5 10.9778 17.5 10.9805C17.5 10.9832 17.5 10.9859 17.5 10.9886C17.5 10.9913 17.5 10.994 17.5 10.9967C17.5 10.9994 17.5 11.0022 17.5 11.0049C17.5 11.0076 17.5 11.0103 17.5 11.013C17.5 11.0157 17.5 11.0184 17.5 11.0211C17.5 11.0238 17.5 11.0265 17.5 11.0292C17.5 11.0319 17.5 11.0346 17.5 11.0373C17.5 11.04 17.5 11.0427 17.5 11.0454C17.5 11.0481 17.5 11.0508 17.5 11.0535C17.5 11.0562 17.5 11.0589 17.5 11.0616C17.5 11.0643 17.5 11.0669 17.5 11.0696C17.5 11.0723 17.5 11.075 17.5 11.0777C17.5 11.0804 17.5 11.0831 17.5 11.0858C17.5 11.0885 17.5 11.0912 17.5 11.0939C17.5 11.0965 17.5 11.0992 17.5 11.1019C17.5 11.1046 17.5 11.1073 17.5 11.11C17.5 11.1127 17.5 11.1153 17.5 11.118C17.5 11.1207 17.5 11.1234 17.5 11.1261C17.5 11.1287 17.5 11.1314 17.5 11.1341C17.5 11.1368 17.5 11.1395 17.5 11.1421C17.5 11.1448 17.5 11.1475 17.5 11.1502C17.5 11.1528 17.5 11.1555 17.5 11.1582C17.5 11.1609 17.5 11.1635 17.5 11.1662C17.5 11.1689 17.5 11.1715 17.5 11.1742C17.5 11.1769 17.5 11.1795 17.5 11.1822C17.5 11.1849 17.5 11.1875 17.5 11.1902C17.5 11.1929 17.5 11.1955 17.5 11.1982C17.5 11.2009 17.5 11.2035 17.5 11.2062C17.5 11.2088 17.5 11.2115 17.5 11.2142C17.5 11.2168 17.5 11.2195 17.5 11.2221C17.5 11.2248 17.5 11.2274 17.5 11.2301C17.5 11.2328 17.5 11.2354 17.5 11.2381C17.5 11.2407 17.5 11.2434 17.5 11.246C17.5 11.2487 17.5 11.2513 17.5 11.254C17.5 11.2566 17.5 11.2592 17.5 11.2619C17.5 11.2645 17.5 11.2672 17.5 11.2698C17.5 11.2725 17.5 11.2751 17.5 11.2777C17.5 11.2804 17.5 11.283 17.5 11.2857C17.5 11.2883 17.5 11.2909 17.5 11.2936C17.5 11.2962 17.5 11.2988 17.5 11.3015C17.5 11.3041 17.5 11.3067 17.5 11.3094C17.5 11.312 17.5 11.3146 17.5 11.3172C17.5 11.3199 17.5 11.3225 17.5 11.3251C17.5 11.3277 17.5 11.3304 17.5 11.333C17.5 11.3356 17.5 11.3382 17.5 11.3409C17.5 11.3435 17.5 11.3461 17.5 11.3487C17.5 11.3513 17.5 11.3539 17.5 11.3566C17.5 11.3592 17.5 11.3618 17.5 11.3644C17.5 11.367 17.5 11.3696 17.5 11.3722C17.5 11.3748 17.5 11.3774 17.5 11.38C17.5 11.3827 17.5 11.3853 17.5 11.3879C17.5 11.3905 17.5 11.3931 17.5 11.3957C17.5 11.3983 17.5 11.4009 17.5 11.4035C17.5 11.4061 17.5 11.4087 17.5 11.4113C17.5 11.4138 17.5 11.4164 17.5 11.419C17.5 11.4216 17.5 11.4242 17.5 11.4268C17.5 11.4294 17.5 11.432 17.5 11.4346C17.5 11.4372 17.5 11.4397 17.5 11.4423C17.5 11.4449 17.5 11.4475 17.5 11.4501C17.5 11.4526 17.5 11.4552 17.5 11.4578C17.5 11.4604 17.5 11.463 17.5 11.4655C17.5 11.4681 17.5 11.4707 17.5 11.4732C17.5 11.4758 17.5 11.4784 17.5 11.4809C17.5 11.4835 17.5 11.4861 17.5 11.4886C17.5 11.4912 17.5 11.4938 17.5 11.4963C17.5 11.4989 17.5 11.5015 17.5 11.504C17.5 11.5066 17.5 11.5091 17.5 11.5117C17.5 11.5142 17.5 11.5168 17.5 11.5193C17.5 11.5219 17.5 11.5244 17.5 11.527C17.5 11.5295 17.5 11.5321 17.5 11.5346C17.5 11.5372 17.5 11.5397 17.5 11.5423C17.5 11.5448 17.5 11.5473 17.5 11.5499C17.5 11.5524 17.5 11.555 17.5 11.5575C17.5 11.56 17.5 11.5626 17.5 11.5651C17.5 11.5676 17.5 11.5701 17.5 11.5727C17.5 11.5752 17.5 11.5777 17.5 11.5803C17.5 11.5828 17.5 11.5853 17.5 11.5878C17.5 11.5903 17.5 11.5929 17.5 11.5954C17.5 11.5979 17.5 11.6004 17.5 11.6029C17.5 11.6054 17.5 11.6079 17.5 11.6105C17.5 11.613 17.5 11.6155 17.5 11.618C17.5 11.6205 17.5 11.623 17.5 11.6255C17.5 11.628 17.5 11.6305 17.5 11.633C17.5 11.6355 17.5 11.638 17.5 11.6405C17.5 11.643 17.5 11.6455 17.5 11.648C17.5 11.6505 17.5 11.6529 17.5 11.6554C17.5 11.6579 17.5 11.6604 17.5 11.6629C17.5 11.6654 17.5 11.6679 17.5 11.6703C17.5 11.6728 17.5 11.6753 17.5 11.6778C17.5 11.6802 17.5 11.6827 17.5 11.6852C17.5 11.6877 17.5 11.6901 17.5 11.6926C17.5 11.6951 17.5 11.6975 17.5 11.7C17.5 11.7025 17.5 11.7049 17.5 11.7074C17.5 11.7099 17.5 11.7123 17.5 11.7148C17.5 11.7172 17.5 11.7197 17.5 11.7221C17.5 11.7246 17.5 11.727 17.5 11.7295C17.5 11.7319 17.5 11.7344 17.5 11.7368C17.5 11.7393 17.5 11.7417 17.5 11.7441C17.5 11.7466 17.5 11.749 17.5 11.7515C17.5 11.7539 17.5 11.7563 17.5 11.7588C17.5 11.7612 17.5 11.7636 17.5 11.766C17.5 11.7685 17.5 11.7709 17.5 11.7733C17.5 11.7757 17.5 11.7782 17.5 11.7806C17.5 11.783 17.5 11.7854 17.5 11.7878C17.5 11.7903 17.5 11.7927 17.5 11.7951C17.5 11.7975 17.5 11.7999 17.5 11.8023C17.5 11.8047 17.5 11.8071 17.5 11.8095C17.5 11.8119 17.5 11.8143 17.5 11.8167C17.5 11.8191 17.5 11.8215 17.5 11.8239C17.5 11.8263 17.5 11.8287 17.5 11.8311C17.5 11.8335 17.5 11.8358 17.5 11.8382C17.5 11.8406 17.5 11.843 17.5 11.8454C17.5 11.8477 17.5 11.8501 17.5 11.8525C17.5 11.8549 17.5 11.8572 17.5 11.8596C17.5 11.862 17.5 11.8644 17.5 11.8667C17.5 11.8691 17.5 11.8714 17.5 11.8738C17.5 11.8762 17.5 11.8785 17.5 11.8809C17.5 11.8832 17.5 11.8856 17.5 11.8879C17.5 11.8903 17.5 11.8926 17.5 11.895C17.5 11.8973 17.5 11.8997 17.5 11.902C17.5 11.9044 17.5 11.9067 17.5 11.909C17.5 11.9114 17.5 11.9137 17.5 11.9161C17.5 11.9184 17.5 11.9207 17.5 11.923C17.5 11.9254 17.5 11.9277 17.5 11.93C17.5 11.9323 17.5 11.9347 17.5 11.937C17.5 11.9393 17.5 11.9416 17.5 11.9439C17.5 11.9462 17.5 11.9486 17.5 11.9509C17.5 11.9532 17.5 11.9555 17.5 11.9578C17.5 11.9601 17.5 11.9624 17.5 11.9647C17.5 11.967 17.5 11.9693 17.5 11.9716C17.5 11.9739 17.5 11.9761 17.5 11.9784C17.5 11.9807 17.5 11.983 17.5 11.9853C17.5 11.9876 17.5 11.9899 17.5 11.9921C17.5 11.9944 17.5 11.9967 17.5 11.999C17.5 12.0012 17.5 12.0035 17.5 12.0058C17.5 12.008 17.5 12.0103 17.5 12.0126C17.5 12.0148 17.5 12.0171 17.5 12.0193C17.5 12.0216 17.5 12.0239 17.5 12.0261C17.5 12.0284 17.5 12.0306 17.5 12.0329C17.5 12.0351 17.5 12.0373 17.5 12.0396C17.5 12.0418 17.5 12.0441 17.5 12.0463C17.5 12.0485 17.5 12.0508 17.5 12.053C17.5 12.0552 17.5 12.0575 17.5 12.0597C17.5 12.0619 17.5 12.0641 17.5 12.0663C17.5 12.0686 17.5 12.0708 17.5 12.073C17.5 12.0752 17.5 12.0774 17.5 12.0796C17.5 12.0818 17.5 12.0841 17.5 12.0863C17.5 12.0885 17.5 12.0907 17.5 12.0929C17.5 12.0951 17.5 12.0972 17.5 12.0994C17.5 12.1016 17.5 12.1038 17.5 12.106C17.5 12.1082 17.5 12.1104 17.5 12.1126C17.5 12.1147 17.5 12.1169 17.5 12.1191C17.5 12.1213 17.5 12.1234 17.5 12.1256C17.5 12.1278 17.5 12.1299 17.5 12.1321C17.5 12.1343 17.5 12.1364 17.5 12.1386C17.5 12.1408 17.5 12.1429 17.5 12.1451C17.5 12.1472 17.5 12.1494 17.5 12.1515C17.5 12.1537 17.5 12.1558 17.5 12.1579C17.5 12.1601 17.5 12.1622 17.5 12.1644C17.5 12.1665 17.5 12.1686 17.5 12.1707C17.5 12.1729 17.5 12.175 17.5 12.1771C17.5 12.1792 17.5 12.1814 17.5 12.1835C17.5 12.1856 17.5 12.1877 17.5 12.1898C17.5 12.1919 17.5 12.194 17.5 12.1962C17.5 12.1983 17.5 12.2004 17.5 12.2025C17.5 12.2046 17.5 12.2067 17.5 12.2087C17.5 12.2108 17.5 12.2129 17.5 12.215C17.5 12.2171 17.5 12.2192 17.5 12.2213C17.5 12.2234 17.5 12.2254 17.5 12.2275C17.5 12.2296 17.5 12.2317 17.5 12.2337C17.5 12.2358 17.5 12.2379 17.5 12.2399C17.5 12.242 17.5 12.244 17.5 12.2461C17.5 12.2481 17.5 12.2502 17.5 12.2523C17.5 12.2543 17.5 12.2563 17.5 12.2584C17.5 12.2604 17.5 12.2625 17.5 12.2645C17.5 12.2666 17.5 12.2686 17.5 12.2706C17.5 12.2726 17.5 12.2747 17.5 12.2767C17.5 12.2787 17.5 12.2807 17.5 12.2828C17.5 12.2848 17.5 12.2868 17.5 12.2888C17.5 12.2908 17.5 12.2928 17.5 12.2948C17.5 12.2968 17.5 12.2988 17.5 12.3008C17.5 12.3028 17.5 12.3048 17.5 12.3068C17.5 12.3088 17.5 12.3108 17.5 12.3128C17.5 12.3148 17.5 12.3168 17.5 12.3187C17.5 12.3207 17.5 12.3227 17.5 12.3247C17.5 12.3266 17.5 12.3286 17.5 12.3306C17.5 12.3326 17.5 12.3345 17.5 12.3365C17.5 12.3384 17.5 12.3404 17.5 12.3423C17.5 12.3443 17.5 12.3462 17.5 12.3482C17.5 12.3501 17.5 12.3521 17.5 12.354C17.5 12.356 17.5 12.3579 17.5 12.3598C17.5 12.3618 17.5 12.3637 17.5 12.3656C17.5 12.3675 17.5 12.3695 17.5 12.3714C17.5 12.3733 17.5 12.3752 17.5 12.3771C17.5 12.379 17.5 12.381 17.5 12.3829C17.5 12.3848 17.5 12.3867 17.5 12.3886C17.5 12.3905 17.5 12.3924 17.5 12.3942C17.5 12.3961 17.5 12.398 17.5 12.3999C17.5 12.4018 17.5 12.4037 17.5 12.4056C17.5 12.4074 17.5 12.4093 17.5 12.4112C17.5 12.413 17.5 12.4149 17.5 12.4168C17.5 12.4186 17.5 12.4205 17.5 12.4224C17.5 12.4242 17.5 12.4261 17.5 12.4279C17.5 12.4298 17.5 12.4316 17.5 12.4335C17.5 12.4353 17.5 12.4371 17.5 12.439C17.5 12.4408 17.5 12.4426 17.5 12.4445C17.5 12.4463 17.5 12.4481 17.5 12.4499C17.5 12.4518 17.5 12.4536 17.5 12.4554C17.5 12.4572 17.5 12.459 17.5 12.4608C17.5 12.4626 17.5 12.4644 17.5 12.4662C17.5 12.468 17.5 12.4698 17.5 12.4716C17.5 12.4734 17.5 12.4752 17.5 12.477C17.5 12.4788 17.5 12.4805 17.5 12.4823C17.5 12.4841 17.5 12.4859 17.5 12.4876C17.5 12.4894 17.5 12.4912 17.5 12.4929C17.5 12.4947 17.5 12.4965 17.5 12.4982C17.5 12.5 17.5 12.5017 17.5 12.5035C17.5 12.5052 17.5 12.507 17.5 12.5087C17.5 12.5104 17.5 12.5122 17.5 12.5139C17.5 12.5156 17.5 12.5174 17.5 12.5191C17.5 12.5208 17.5 12.5225 17.5 12.5242C17.5 12.526 17.5 12.5277 17.5 12.5294C17.5 12.5311 17.5 12.5328 17.5 12.5345C17.5 12.5362 17.5 12.5379 17.5 12.5396C17.5 12.5413 17.5 12.543 17.5 12.5447C17.5 12.5464 17.5 12.548 17.5 12.5497C17.5 12.5514 17.5 12.5531 17.5 12.5547C17.5 12.5564 17.5 12.5581 17.5 12.5597C17.5 12.5614 17.5 12.5631 17.5 12.5647C17.5 12.5664 17.5 12.568 17.5 12.5697C17.5 12.5713 17.5 12.573 17.5 12.5746C17.5 12.5762 17.5 12.5779 17.5 12.5795C17.5 12.5811 17.5 12.5828 17.5 12.5844C17.5 12.586 17.5 12.5876 17.5 12.5892C17.5 12.5909 17.5 12.5925 17.5 12.5941C17.5 12.5957 17.5 12.5973 17.5 12.5989C17.5 12.6005 17.5 12.6021 17.5 12.6037C17.5 12.6053 17.5 12.6069 17.5 12.6084C17.5 12.61 17.5 12.6116 17.5 12.6132C17.5 12.6147 17.5 12.6163 17.5 12.6179C17.5 12.6195 17.5 12.621 17.5 12.6226C17.5 12.6241 17.5 12.6257 17.5 12.6272C17.5 12.6288 17.5 12.6303 17.5 12.6319C17.5 12.6334 17.5 12.635 17.5 12.6365C17.5 12.638 17.5 12.6396 17.5 12.6411C17.5 12.6426 17.5 12.6441 17.5 12.6456C17.5 12.6472 17.5 12.6487 17.5 12.6502C17.5 12.6517 17.5 12.6532 17.5 12.6547C17.5 12.6562 17.5 12.6577 17.5 12.6592C17.5 12.6607 17.5 12.6622 17.5 12.6637C17.5 12.6651 17.5 12.6666 17.5 12.6681C17.5 12.6696 17.5 12.671 17.5 12.6725C17.5 12.674 17.5 12.6754 17.5 12.6769C17.5 12.6784 17.5 12.6798 17.5 12.6813C17.5 12.6827 17.5 12.6842 17.5 12.6856C17.5 12.687 17.5 12.6885 17.5 12.6899C17.5 12.6913 17.5 12.6928 17.5 12.6942C17.5 12.6956 17.5 12.697 17.5 12.6985C17.5 12.6999 17.5 12.7013 17.5 12.7027C17.5 12.7041 17.5 12.7055 17.5 12.7069C17.5 12.7083 17.5 12.7097 17.5 12.7111C17.5 12.7125 17.5 12.7138 17.5 12.7152C17.5 12.7166 17.5 12.718 17.5 12.7194C17.5 12.7207 17.5 12.7221 17.5 12.7235C17.5 12.7248 17.5 12.7262 17.5 12.7275C17.5 12.7289 17.5 12.7302 17.5 12.7316C17.5 12.7329 17.5 12.7343 17.5 12.7356C17.5 12.7369 17.5 12.7383 17.5 12.7396C17.5 12.7409 17.5 12.7423 17.5 12.7436C17.5 12.7449 17.5 12.7462 17.5 12.7475C17.5 12.7488 17.5 12.7501 17.5 12.7514C17.5 12.7527 17.5 12.754 17.5 12.7553C17.5 12.7566 17.5 12.7579 17.5 12.7592C17.5 12.7605 17.5 12.7617 17.5 12.763C17.5 12.7643 17.5 12.7656 17.5 12.7668C17.5 12.7681 17.5 12.7693 17.5 12.7706C17.5 12.7719 17.5 12.7731 17.5 12.7743C17.5 12.7756 17.5 12.7768 17.5 12.7781C17.5 12.7793 17.5 12.7805 17.5 12.7818C17.5 12.783 17.5 12.7842 17.5 12.7854C17.5 12.7867 17.5 12.7879 17.5 12.7891C17.5 12.7903 17.5 12.7915 17.5 12.7927C17.5 12.7939 17.5 12.7951 17.5 12.7963C17.5 12.7975 17.5 12.7986 17.5 12.7998C17.5 12.801 17.5 12.8022 17.5 12.8034C17.5 12.8045 17.5 12.8057 17.5 12.8069C17.5 12.808 17.5 12.8092 17.5 12.8103C17.5 12.8115 17.5 12.8126 17.5 12.8138C17.5 12.8149 17.5 12.816 17.5 12.8172C17.5 12.8183 17.5 12.8194 17.5 12.8206C17.5 12.8217 17.5 12.8228 17.5 12.8239C17.5 12.825 17.5 12.8261 17.5 12.8273C17.5 12.8284 17.5 12.8295 17.5 12.8306C17.5 12.8316 17.5 12.8327 17.5 12.8338C17.5 12.8349 17.5 12.836 17.5 12.8371C17.5 12.8381 17.5 12.8392 17.5 12.8403C17.5 12.8413 17.5 12.8424 17.5 12.8435C17.5 12.8445 17.5 12.8456 17.5 12.8466C17.5 12.8477 17.5 12.8487 17.5 12.8497C17.5 12.8508 17.5 12.8518 17.5 12.8528C17.5 12.8539 17.5 12.8549 17.5 12.8559C17.5 12.8569 17.5 12.8579 17.5 12.8589C17.5 12.8599 17.5 12.8609 17.5 12.8619C17.5 12.8629 17.5 12.8639 17.5 12.8649C17.5 12.8659 17.5 12.8669 17.5 12.8679C17.5 12.8688 17.5 12.8698 17.5 12.8708C17.5 12.8717 17.5 12.8727 17.5 12.8737C17.5 12.8746 17.5 12.8756 17.5 12.8765C17.5 12.8775 17.5 12.8784 17.5 12.8793C17.5 12.8803 17.5 12.8812 17.5 12.8821C17.5 12.8831 17.5 12.884 17.5 12.8849C17.5 12.8858 17.5 12.8867 17.5 12.8876C17.5 12.8885 17.5 12.8894 17.5 12.8903C17.5 12.8912 17.5 12.8921 17.5 12.893C17.5 12.8939 17.5 12.8948 17.5 12.8956C17.5 12.8965 17.5 12.8974 17.5 12.8983C17.5 12.8991 17.5 12.9 17.5 12.9008C17.5 12.9017 17.5 12.9025 17.5 12.9034C17.5 12.9042 17.5 12.9051 17.5 12.9059C17.5 12.9067 17.5 12.9076 17.5 12.9084C17.5 12.9092 17.5 12.91 17.5 12.9108C17.5 12.9117 17.5 12.9125 17.5 12.9133C17.5 12.9141 17.5 12.9149 17.5 12.9157C17.5 12.9165 17.5 12.9172 17.5 12.918C17.5 12.9188 17.5 12.9196 17.5 12.9204C17.5 12.9211 17.5 12.9219 17.5 12.9227C17.5 12.9234 17.5 12.9242 17.5 12.9249C17.5 12.9257 17.5 12.9264 17.5 12.9272C17.5 12.9279 17.5 12.9286 17.5 12.9294C17.5 12.9301 17.5 12.9308 17.5 12.9315C17.5 12.9322 17.5 12.933 17.5 12.9337C17.5 12.9344 17.5 12.9351 17.5 12.9358C17.5 12.9365 17.5 12.9372 17.5 12.9378C17.5 12.9385 17.5 12.9392 17.5 12.9399C17.5 12.9406 17.5 12.9412 17.5 12.9419C17.5 12.9426 17.5 12.9432 17.5 12.9439C17.5 12.9445 17.5 12.9452 17.5 12.9458C17.5 12.9465 17.5 12.9471 17.5 12.9477C17.5 12.9484 17.5 12.949 17.5 12.9496C17.5 12.9502 17.5 12.9508 17.5 12.9515C17.5 12.9521 17.5 12.9527 17.5 12.9533C17.5 12.9539 17.5 12.9545 17.5 12.955C17.5 12.9556 17.5 12.9562 17.5 12.9568C17.5 12.9574 17.5 12.9579 17.5 12.9585C17.5 12.9591 17.5 12.9596 17.5 12.9602C17.5 12.9607 17.5 12.9613 17.5 12.9618C17.5 12.9624 17.5 12.9629 17.5 12.9634C17.5 12.964 17.5 12.9645 17.5 12.965C17.5 12.9655 17.5 12.9661 17.5 12.9666C17.5 12.9671 17.5 12.9676 17.5 12.9681C17.5 12.9686 17.5 12.9691 17.5 12.9696C17.5 12.97 17.5 12.9705 17.5 12.971C17.5 12.9715 17.5 12.9719 17.5 12.9724C17.5 12.9729 17.5 12.9733 17.5 12.9738C17.5 12.9742 17.5 12.9747 17.5 12.9751C17.5 12.9756 17.5 12.976 17.5 12.9764C17.5 12.9769 17.5 12.9773 17.5 12.9777C17.5 12.9781 17.5 12.9785 17.5 12.979C17.5 12.9794 17.5 12.9798 17.5 12.9802C17.5 12.9806 17.5 12.9809 17.5 12.9813C17.5 12.9817 17.5 12.9821 17.5 12.9825C17.5 12.9828 17.5 12.9832 17.5 12.9836C17.5 12.9839 17.5 12.9843 17.5 12.9846C17.5 12.985 17.5 12.9853 17.5 12.9857C17.5 12.986 17.5 12.9863 17.5 12.9867C17.5 12.987 17.5 12.9873 17.5 12.9876C17.5 12.9879 17.5 12.9882 17.5 12.9885C17.5 12.9888 17.5 12.9891 17.5 12.9894C17.5 12.9897 17.5 12.99 17.5 12.9903C17.5 12.9906 17.5 12.9908 17.5 12.9911C17.5 12.9914 17.5 12.9916 17.5 12.9919C17.5 12.9921 17.5 12.9924 17.5 12.9926C17.5 12.9929 17.5 12.9931 17.5 12.9934C17.5 12.9936 17.5 12.9938 17.5 12.994C17.5 12.9942 17.5 12.9945 17.5 12.9947C17.5 12.9949 17.5 12.9951 17.5 12.9953C17.5 12.9955 17.5 12.9957 17.5 12.9958C17.5 12.996 17.5 12.9962 17.5 12.9964C17.5 12.9965 17.5 12.9967 17.5 12.9969C17.5 12.997 17.5 12.9972 17.5 12.9973C17.5 12.9975 17.5 12.9976 17.5 12.9978C17.5 12.9979 17.5 12.998 17.5 12.9981C17.5 12.9983 17.5 12.9984 17.5 12.9985C17.5 12.9986 17.5 12.9987 17.5 12.9988C17.5 12.9989 17.5 12.999 17.5 12.9991C17.5 12.9992 17.5 12.9993 17.5 12.9993C17.5 12.9994 17.5 12.9995 17.5 12.9995C17.5 12.9996 17.5 12.9997 17.5 12.9997C17.5 12.9998 17.5 12.9998 17.5 12.9998C17.5 12.9999 17.5 12.9999 17.5 12.9999C17.5 13 17.5 13 17.5 13C17.5 13 17.5 13 18 13ZM17.5 13C17.5 14.3807 16.3807 15.5 15 15.5V16.5C16.933 16.5 18.5 14.933 18.5 13H17.5ZM15 15.5C13.6193 15.5 12.5 14.3807 12.5 13H11.5C11.5 14.933 13.067 16.5 15 16.5V15.5ZM12.5 13C12.5 11.6193 13.6193 10.5 15 10.5V9.5C13.067 9.5 11.5 11.067 11.5 13H12.5ZM15 10.5C16.3807 10.5 17.5 11.6193 17.5 13H18.5C18.5 11.067 16.933 9.5 15 9.5V10.5ZM2.46816 16.1756L6.40637 5.67369L5.47004 5.32257L1.53184 15.8244L2.46816 16.1756ZM6.59363 5.67369L8.93087 11.9063L9.8672 11.5552L7.52996 5.32257L6.59363 5.67369ZM8.93087 11.9063L10.5318 16.1756L11.4682 15.8244L9.8672 11.5552L8.93087 11.9063ZM3.60096 12.2308C3.60508 12.2308 3.6092 12.2308 3.61333 12.2308C3.61746 12.2308 3.6216 12.2308 3.62574 12.2308C3.62988 12.2308 3.63403 12.2308 3.63819 12.2308C3.64234 12.2308 3.6465 12.2308 3.65067 12.2308C3.65483 12.2308 3.659 12.2308 3.66318 12.2308C3.66736 12.2308 3.67154 12.2308 3.67573 12.2308C3.67992 12.2308 3.68411 12.2308 3.68831 12.2308C3.69252 12.2308 3.69672 12.2308 3.70093 12.2308C3.70515 12.2308 3.70936 12.2308 3.71359 12.2308C3.71781 12.2308 3.72204 12.2308 3.72627 12.2308C3.73051 12.2308 3.73475 12.2308 3.739 12.2308C3.74324 12.2308 3.74749 12.2308 3.75175 12.2308C3.75601 12.2308 3.76027 12.2308 3.76454 12.2308C3.76881 12.2308 3.77308 12.2308 3.77736 12.2308C3.78164 12.2308 3.78592 12.2308 3.79021 12.2308C3.7945 12.2308 3.7988 12.2308 3.8031 12.2308C3.8074 12.2308 3.81171 12.2308 3.81602 12.2308C3.82033 12.2308 3.82465 12.2308 3.82897 12.2308C3.83329 12.2308 3.83762 12.2308 3.84195 12.2308C3.84629 12.2308 3.85063 12.2308 3.85497 12.2308C3.85931 12.2308 3.86366 12.2308 3.86802 12.2308C3.87237 12.2308 3.87673 12.2308 3.88109 12.2308C3.88546 12.2308 3.88983 12.2308 3.8942 12.2308C3.89858 12.2308 3.90296 12.2308 3.90734 12.2308C3.91173 12.2308 3.91612 12.2308 3.92051 12.2308C3.92491 12.2308 3.92931 12.2308 3.93371 12.2308C3.93812 12.2308 3.94253 12.2308 3.94694 12.2308C3.95136 12.2308 3.95578 12.2308 3.9602 12.2308C3.96463 12.2308 3.96906 12.2308 3.97349 12.2308C3.97793 12.2308 3.98237 12.2308 3.98681 12.2308C3.99126 12.2308 3.99571 12.2308 4.00016 12.2308C4.00462 12.2308 4.00908 12.2308 4.01354 12.2308C4.018 12.2308 4.02247 12.2308 4.02695 12.2308C4.03142 12.2308 4.0359 12.2308 4.04038 12.2308C4.04486 12.2308 4.04935 12.2308 4.05384 12.2308C4.05833 12.2308 4.06283 12.2308 4.06733 12.2308C4.07183 12.2308 4.07634 12.2308 4.08085 12.2308C4.08536 12.2308 4.08988 12.2308 4.0944 12.2308C4.09892 12.2308 4.10344 12.2308 4.10797 12.2308C4.1125 12.2308 4.11703 12.2308 4.12157 12.2308C4.12611 12.2308 4.13065 12.2308 4.1352 12.2308C4.13974 12.2308 4.14429 12.2308 4.14885 12.2308C4.1534 12.2308 4.15796 12.2308 4.16253 12.2308C4.16709 12.2308 4.17166 12.2308 4.17623 12.2308C4.18081 12.2308 4.18538 12.2308 4.18997 12.2308C4.19455 12.2308 4.19913 12.2308 4.20372 12.2308C4.20831 12.2308 4.21291 12.2308 4.21751 12.2308C4.2221 12.2308 4.22671 12.2308 4.23131 12.2308C4.23592 12.2308 4.24053 12.2308 4.24514 12.2308C4.24976 12.2308 4.25438 12.2308 4.259 12.2308C4.26362 12.2308 4.26825 12.2308 4.27288 12.2308C4.27751 12.2308 4.28215 12.2308 4.28679 12.2308C4.29143 12.2308 4.29607 12.2308 4.30072 12.2308C4.30537 12.2308 4.31002 12.2308 4.31467 12.2308C4.31933 12.2308 4.32399 12.2308 4.32865 12.2308C4.33331 12.2308 4.33798 12.2308 4.34265 12.2308C4.34732 12.2308 4.35199 12.2308 4.35667 12.2308C4.36135 12.2308 4.36603 12.2308 4.37072 12.2308C4.3754 12.2308 4.38009 12.2308 4.38478 12.2308C4.38948 12.2308 4.39417 12.2308 4.39887 12.2308C4.40358 12.2308 4.40828 12.2308 4.41299 12.2308C4.4177 12.2308 4.42241 12.2308 4.42712 12.2308C4.43184 12.2308 4.43656 12.2308 4.44128 12.2308C4.446 12.2308 4.45073 12.2308 4.45546 12.2308C4.46018 12.2308 4.46492 12.2308 4.46965 12.2308C4.47439 12.2308 4.47913 12.2308 4.48387 12.2308C4.48862 12.2308 4.49336 12.2308 4.49811 12.2308C4.50286 12.2308 4.50762 12.2308 4.51237 12.2308C4.51713 12.2308 4.52189 12.2308 4.52665 12.2308C4.53142 12.2308 4.53618 12.2308 4.54095 12.2308C4.54572 12.2308 4.5505 12.2308 4.55527 12.2308C4.56005 12.2308 4.56483 12.2308 4.56961 12.2308C4.5744 12.2308 4.57918 12.2308 4.58397 12.2308C4.58876 12.2308 4.59355 12.2308 4.59835 12.2308C4.60315 12.2308 4.60794 12.2308 4.61275 12.2308C4.61755 12.2308 4.62235 12.2308 4.62716 12.2308C4.63197 12.2308 4.63678 12.2308 4.64159 12.2308C4.64641 12.2308 4.65123 12.2308 4.65605 12.2308C4.66087 12.2308 4.66569 12.2308 4.67052 12.2308C4.67534 12.2308 4.68017 12.2308 4.685 12.2308C4.68983 12.2308 4.69467 12.2308 4.69951 12.2308C4.70434 12.2308 4.70918 12.2308 4.71403 12.2308C4.71887 12.2308 4.72372 12.2308 4.72856 12.2308C4.73341 12.2308 4.73827 12.2308 4.74312 12.2308C4.74797 12.2308 4.75283 12.2308 4.75769 12.2308C4.76255 12.2308 4.76741 12.2308 4.77228 12.2308C4.77714 12.2308 4.78201 12.2308 4.78688 12.2308C4.79175 12.2308 4.79662 12.2308 4.8015 12.2308C4.80638 12.2308 4.81125 12.2308 4.81613 12.2308C4.82101 12.2308 4.8259 12.2308 4.83078 12.2308C4.83567 12.2308 4.84056 12.2308 4.84545 12.2308C4.85034 12.2308 4.85523 12.2308 4.86013 12.2308C4.86502 12.2308 4.86992 12.2308 4.87482 12.2308C4.87972 12.2308 4.88462 12.2308 4.88953 12.2308C4.89443 12.2308 4.89934 12.2308 4.90425 12.2308C4.90916 12.2308 4.91407 12.2308 4.91899 12.2308C4.9239 12.2308 4.92882 12.2308 4.93374 12.2308C4.93866 12.2308 4.94358 12.2308 4.9485 12.2308C4.95342 12.2308 4.95835 12.2308 4.96328 12.2308C4.9682 12.2308 4.97313 12.2308 4.97806 12.2308C4.983 12.2308 4.98793 12.2308 4.99287 12.2308C4.9978 12.2308 5.00274 12.2308 5.00768 12.2308C5.01262 12.2308 5.01756 12.2308 5.02251 12.2308C5.02745 12.2308 5.0324 12.2308 5.03734 12.2308C5.04229 12.2308 5.04724 12.2308 5.05219 12.2308C5.05714 12.2308 5.0621 12.2308 5.06705 12.2308C5.07201 12.2308 5.07697 12.2308 5.08193 12.2308C5.08689 12.2308 5.09185 12.2308 5.09681 12.2308C5.10177 12.2308 5.10674 12.2308 5.1117 12.2308C5.11667 12.2308 5.12164 12.2308 5.12661 12.2308C5.13158 12.2308 5.13655 12.2308 5.14152 12.2308C5.1465 12.2308 5.15147 12.2308 5.15645 12.2308C5.16142 12.2308 5.1664 12.2308 5.17138 12.2308C5.17636 12.2308 5.18134 12.2308 5.18633 12.2308C5.19131 12.2308 5.19629 12.2308 5.20128 12.2308C5.20627 12.2308 5.21125 12.2308 5.21624 12.2308C5.22123 12.2308 5.22622 12.2308 5.23121 12.2308C5.23621 12.2308 5.2412 12.2308 5.24619 12.2308C5.25119 12.2308 5.25618 12.2308 5.26118 12.2308C5.26618 12.2308 5.27118 12.2308 5.27618 12.2308C5.28118 12.2308 5.28618 12.2308 5.29118 12.2308C5.29619 12.2308 5.30119 12.2308 5.30619 12.2308C5.3112 12.2308 5.31621 12.2308 5.32121 12.2308C5.32622 12.2308 5.33123 12.2308 5.33624 12.2308C5.34125 12.2308 5.34626 12.2308 5.35127 12.2308C5.35629 12.2308 5.3613 12.2308 5.36632 12.2308C5.37133 12.2308 5.37635 12.2308 5.38136 12.2308C5.38638 12.2308 5.3914 12.2308 5.39642 12.2308C5.40143 12.2308 5.40645 12.2308 5.41148 12.2308C5.4165 12.2308 5.42152 12.2308 5.42654 12.2308C5.43156 12.2308 5.43659 12.2308 5.44161 12.2308C5.44664 12.2308 5.45166 12.2308 5.45669 12.2308C5.46171 12.2308 5.46674 12.2308 5.47177 12.2308C5.4768 12.2308 5.48182 12.2308 5.48685 12.2308C5.49188 12.2308 5.49691 12.2308 5.50194 12.2308C5.50698 12.2308 5.51201 12.2308 5.51704 12.2308C5.52207 12.2308 5.5271 12.2308 5.53214 12.2308C5.53717 12.2308 5.54221 12.2308 5.54724 12.2308C5.55228 12.2308 5.55731 12.2308 5.56235 12.2308C5.56738 12.2308 5.57242 12.2308 5.57746 12.2308C5.5825 12.2308 5.58753 12.2308 5.59257 12.2308C5.59761 12.2308 5.60265 12.2308 5.60769 12.2308C5.61273 12.2308 5.61777 12.2308 5.62281 12.2308C5.62785 12.2308 5.63289 12.2308 5.63793 12.2308C5.64297 12.2308 5.64801 12.2308 5.65305 12.2308C5.65809 12.2308 5.66314 12.2308 5.66818 12.2308C5.67322 12.2308 5.67826 12.2308 5.68331 12.2308C5.68835 12.2308 5.69339 12.2308 5.69843 12.2308C5.70348 12.2308 5.70852 12.2308 5.71356 12.2308C5.71861 12.2308 5.72365 12.2308 5.7287 12.2308C5.73374 12.2308 5.73878 12.2308 5.74383 12.2308C5.74887 12.2308 5.75392 12.2308 5.75896 12.2308C5.76401 12.2308 5.76905 12.2308 5.7741 12.2308C5.77914 12.2308 5.78418 12.2308 5.78923 12.2308C5.79427 12.2308 5.79932 12.2308 5.80436 12.2308C5.80941 12.2308 5.81445 12.2308 5.81949 12.2308C5.82454 12.2308 5.82958 12.2308 5.83463 12.2308C5.83967 12.2308 5.84472 12.2308 5.84976 12.2308C5.8548 12.2308 5.85985 12.2308 5.86489 12.2308C5.86993 12.2308 5.87498 12.2308 5.88002 12.2308C5.88506 12.2308 5.8901 12.2308 5.89515 12.2308C5.90019 12.2308 5.90523 12.2308 5.91027 12.2308C5.91531 12.2308 5.92036 12.2308 5.9254 12.2308C5.93044 12.2308 5.93548 12.2308 5.94052 12.2308C5.94556 12.2308 5.9506 12.2308 5.95564 12.2308C5.96068 12.2308 5.96572 12.2308 5.97076 12.2308C5.97579 12.2308 5.98083 12.2308 5.98587 12.2308C5.99091 12.2308 5.99594 12.2308 6.00098 12.2308C6.00602 12.2308 6.01105 12.2308 6.01609 12.2308C6.02112 12.2308 6.02616 12.2308 6.03119 12.2308C6.03622 12.2308 6.04126 12.2308 6.04629 12.2308C6.05132 12.2308 6.05636 12.2308 6.06139 12.2308C6.06642 12.2308 6.07145 12.2308 6.07648 12.2308C6.08151 12.2308 6.08654 12.2308 6.09156 12.2308C6.09659 12.2308 6.10162 12.2308 6.10665 12.2308C6.11167 12.2308 6.1167 12.2308 6.12172 12.2308C6.12675 12.2308 6.13177 12.2308 6.1368 12.2308C6.14182 12.2308 6.14684 12.2308 6.15186 12.2308C6.15688 12.2308 6.1619 12.2308 6.16692 12.2308C6.17194 12.2308 6.17696 12.2308 6.18198 12.2308C6.18699 12.2308 6.19201 12.2308 6.19702 12.2308C6.20204 12.2308 6.20705 12.2308 6.21207 12.2308C6.21708 12.2308 6.22209 12.2308 6.2271 12.2308C6.23211 12.2308 6.23712 12.2308 6.24213 12.2308C6.24714 12.2308 6.25214 12.2308 6.25715 12.2308C6.26216 12.2308 6.26716 12.2308 6.27216 12.2308C6.27717 12.2308 6.28217 12.2308 6.28717 12.2308C6.29217 12.2308 6.29717 12.2308 6.30217 12.2308C6.30717 12.2308 6.31216 12.2308 6.31716 12.2308C6.32215 12.2308 6.32715 12.2308 6.33214 12.2308C6.33713 12.2308 6.34212 12.2308 6.34711 12.2308C6.3521 12.2308 6.35709 12.2308 6.36208 12.2308C6.36706 12.2308 6.37205 12.2308 6.37703 12.2308C6.38202 12.2308 6.387 12.2308 6.39198 12.2308C6.39696 12.2308 6.40194 12.2308 6.40691 12.2308C6.41189 12.2308 6.41687 12.2308 6.42184 12.2308C6.42681 12.2308 6.43179 12.2308 6.43676 12.2308C6.44173 12.2308 6.4467 12.2308 6.45166 12.2308C6.45663 12.2308 6.4616 12.2308 6.46656 12.2308C6.47152 12.2308 6.47648 12.2308 6.48144 12.2308C6.4864 12.2308 6.49136 12.2308 6.49632 12.2308C6.50128 12.2308 6.50623 12.2308 6.51118 12.2308C6.51613 12.2308 6.52109 12.2308 6.52603 12.2308C6.53098 12.2308 6.53593 12.2308 6.54087 12.2308C6.54582 12.2308 6.55076 12.2308 6.5557 12.2308C6.56064 12.2308 6.56558 12.2308 6.57052 12.2308C6.57545 12.2308 6.58039 12.2308 6.58532 12.2308C6.59025 12.2308 6.59518 12.2308 6.60011 12.2308C6.60504 12.2308 6.60997 12.2308 6.61489 12.2308C6.61982 12.2308 6.62474 12.2308 6.62966 12.2308C6.63458 12.2308 6.63949 12.2308 6.64441 12.2308C6.64932 12.2308 6.65424 12.2308 6.65915 12.2308C6.66406 12.2308 6.66897 12.2308 6.67387 12.2308C6.67878 12.2308 6.68368 12.2308 6.68858 12.2308C6.69348 12.2308 6.69838 12.2308 6.70328 12.2308C6.70818 12.2308 6.71307 12.2308 6.71796 12.2308C6.72285 12.2308 6.72774 12.2308 6.73263 12.2308C6.73751 12.2308 6.7424 12.2308 6.74728 12.2308C6.75216 12.2308 6.75704 12.2308 6.76192 12.2308C6.76679 12.2308 6.77167 12.2308 6.77654 12.2308C6.78141 12.2308 6.78628 12.2308 6.79115 12.2308C6.79601 12.2308 6.80088 12.2308 6.80574 12.2308C6.8106 12.2308 6.81545 12.2308 6.82031 12.2308C6.82517 12.2308 6.83002 12.2308 6.83487 12.2308C6.83972 12.2308 6.84456 12.2308 6.84941 12.2308C6.85425 12.2308 6.85909 12.2308 6.86393 12.2308C6.86877 12.2308 6.87361 12.2308 6.87844 12.2308C6.88327 12.2308 6.8881 12.2308 6.89293 12.2308C6.89776 12.2308 6.90258 12.2308 6.9074 12.2308C6.91222 12.2308 6.91704 12.2308 6.92186 12.2308C6.92667 12.2308 6.93148 12.2308 6.93629 12.2308C6.9411 12.2308 6.94591 12.2308 6.95071 12.2308C6.95552 12.2308 6.96032 12.2308 6.96511 12.2308C6.96991 12.2308 6.9747 12.2308 6.97949 12.2308C6.98428 12.2308 6.98907 12.2308 6.99386 12.2308C6.99864 12.2308 7.00342 12.2308 7.0082 12.2308C7.01298 12.2308 7.01775 12.2308 7.02252 12.2308C7.02729 12.2308 7.03206 12.2308 7.03683 12.2308C7.04159 12.2308 7.04635 12.2308 7.05111 12.2308C7.05587 12.2308 7.06062 12.2308 7.06538 12.2308C7.07013 12.2308 7.07487 12.2308 7.07962 12.2308C7.08436 12.2308 7.0891 12.2308 7.09384 12.2308C7.09858 12.2308 7.10331 12.2308 7.10804 12.2308C7.11277 12.2308 7.1175 12.2308 7.12223 12.2308C7.12695 12.2308 7.13167 12.2308 7.13639 12.2308C7.1411 12.2308 7.14581 12.2308 7.15052 12.2308C7.15523 12.2308 7.15994 12.2308 7.16464 12.2308C7.16934 12.2308 7.17404 12.2308 7.17873 12.2308C7.18343 12.2308 7.18812 12.2308 7.19281 12.2308C7.19749 12.2308 7.20218 12.2308 7.20686 12.2308C7.21154 12.2308 7.21621 12.2308 7.22088 12.2308C7.22556 12.2308 7.23022 12.2308 7.23489 12.2308C7.23955 12.2308 7.24421 12.2308 7.24887 12.2308C7.25353 12.2308 7.25818 12.2308 7.26283 12.2308C7.26748 12.2308 7.27212 12.2308 7.27676 12.2308C7.2814 12.2308 7.28604 12.2308 7.29067 12.2308C7.2953 12.2308 7.29993 12.2308 7.30456 12.2308C7.30918 12.2308 7.3138 12.2308 7.31842 12.2308C7.32303 12.2308 7.32765 12.2308 7.33226 12.2308C7.33686 12.2308 7.34147 12.2308 7.34607 12.2308C7.35067 12.2308 7.35526 12.2308 7.35985 12.2308C7.36445 12.2308 7.36903 12.2308 7.37362 12.2308C7.3782 12.2308 7.38278 12.2308 7.38735 12.2308C7.39193 12.2308 7.3965 12.2308 7.40106 12.2308C7.40563 12.2308 7.41019 12.2308 7.41475 12.2308C7.4193 12.2308 7.42386 12.2308 7.4284 12.2308C7.43295 12.2308 7.4375 12.2308 7.44204 12.2308C7.44658 12.2308 7.45111 12.2308 7.45564 12.2308C7.46017 12.2308 7.4647 12.2308 7.46922 12.2308C7.47374 12.2308 7.47826 12.2308 7.48277 12.2308C7.48728 12.2308 7.49179 12.2308 7.49629 12.2308C7.5008 12.2308 7.5053 12.2308 7.50979 12.2308C7.51428 12.2308 7.51877 12.2308 7.52326 12.2308C7.52774 12.2308 7.53222 12.2308 7.5367 12.2308C7.54117 12.2308 7.54564 12.2308 7.55011 12.2308C7.55457 12.2308 7.55903 12.2308 7.56349 12.2308C7.56795 12.2308 7.5724 12.2308 7.57684 12.2308C7.58129 12.2308 7.58573 12.2308 7.59017 12.2308C7.59461 12.2308 7.59904 12.2308 7.60346 12.2308C7.60789 12.2308 7.61231 12.2308 7.61673 12.2308C7.62115 12.2308 7.62556 12.2308 7.62997 12.2308C7.63437 12.2308 7.63878 12.2308 7.64317 12.2308C7.64757 12.2308 7.65196 12.2308 7.65635 12.2308C7.66074 12.2308 7.66512 12.2308 7.66949 12.2308C7.67387 12.2308 7.67824 12.2308 7.68261 12.2308C7.68698 12.2308 7.69134 12.2308 7.69569 12.2308C7.70005 12.2308 7.7044 12.2308 7.70874 12.2308C7.71309 12.2308 7.71743 12.2308 7.72177 12.2308C7.7261 12.2308 7.73043 12.2308 7.73476 12.2308C7.73908 12.2308 7.7434 12.2308 7.74771 12.2308C7.75203 12.2308 7.75633 12.2308 7.76064 12.2308C7.76494 12.2308 7.76924 12.2308 7.77353 12.2308C7.77782 12.2308 7.78211 12.2308 7.78639 12.2308C7.79067 12.2308 7.79495 12.2308 7.79922 12.2308C7.80349 12.2308 7.80775 12.2308 7.81201 12.2308C7.81627 12.2308 7.82053 12.2308 7.82477 12.2308C7.82902 12.2308 7.83326 12.2308 7.8375 12.2308C7.84174 12.2308 7.84597 12.2308 7.85019 12.2308C7.85442 12.2308 7.85864 12.2308 7.86285 12.2308C7.86707 12.2308 7.87128 12.2308 7.87548 12.2308C7.87968 12.2308 7.88388 12.2308 7.88807 12.2308C7.89226 12.2308 7.89645 12.2308 7.90063 12.2308C7.90481 12.2308 7.90898 12.2308 7.91315 12.2308C7.91732 12.2308 7.92148 12.2308 7.92563 12.2308C7.92979 12.2308 7.93394 12.2308 7.93809 12.2308C7.94223 12.2308 7.94637 12.2308 7.9505 12.2308C7.95463 12.2308 7.95876 12.2308 7.96288 12.2308C7.967 12.2308 7.97111 12.2308 7.97522 12.2308C7.97933 12.2308 7.98343 12.2308 7.98753 12.2308C7.99162 12.2308 7.99571 12.2308 7.9998 12.2308C8.00388 12.2308 8.00796 12.2308 8.01203 12.2308C8.0161 12.2308 8.02017 12.2308 8.02423 12.2308C8.02829 12.2308 8.03234 12.2308 8.03639 12.2308C8.04043 12.2308 8.04447 12.2308 8.04851 12.2308C8.05254 12.2308 8.05657 12.2308 8.06059 12.2308C8.06461 12.2308 8.06863 12.2308 8.07264 12.2308C8.07664 12.2308 8.08065 12.2308 8.08464 12.2308C8.08864 12.2308 8.09263 12.2308 8.09661 12.2308C8.10059 12.2308 8.10457 12.2308 8.10854 12.2308C8.11251 12.2308 8.11647 12.2308 8.12043 12.2308C8.12439 12.2308 8.12834 12.2308 8.13228 12.2308C8.13623 12.2308 8.14016 12.2308 8.14409 12.2308C8.14803 12.2308 8.15195 12.2308 8.15587 12.2308C8.15978 12.2308 8.1637 12.2308 8.1676 12.2308C8.1715 12.2308 8.1754 12.2308 8.17929 12.2308C8.18318 12.2308 8.18707 12.2308 8.19094 12.2308C8.19482 12.2308 8.19869 12.2308 8.20256 12.2308C8.20642 12.2308 8.21028 12.2308 8.21413 12.2308C8.21798 12.2308 8.22182 12.2308 8.22566 12.2308C8.22949 12.2308 8.23332 12.2308 8.23714 12.2308C8.24097 12.2308 8.24478 12.2308 8.24859 12.2308C8.2524 12.2308 8.2562 12.2308 8.25999 12.2308C8.26379 12.2308 8.26758 12.2308 8.27136 12.2308C8.27514 12.2308 8.27891 12.2308 8.28268 12.2308C8.28644 12.2308 8.2902 12.2308 8.29395 12.2308C8.29771 12.2308 8.30145 12.2308 8.30519 12.2308C8.30893 12.2308 8.31266 12.2308 8.31638 12.2308C8.32011 12.2308 8.32382 12.2308 8.32753 12.2308C8.33124 12.2308 8.33494 12.2308 8.33864 12.2308C8.34233 12.2308 8.34602 12.2308 8.3497 12.2308C8.35338 12.2308 8.35705 12.2308 8.36072 12.2308C8.36438 12.2308 8.36804 12.2308 8.37169 12.2308C8.37534 12.2308 8.37898 12.2308 8.38262 12.2308C8.38625 12.2308 8.38988 12.2308 8.3935 12.2308C8.39712 12.2308 8.40074 12.2308 8.40434 12.2308C8.40795 12.2308 8.41155 12.2308 8.41514 12.2308C8.41873 12.2308 8.42231 12.2308 8.42589 12.2308C8.42946 12.2308 8.43303 12.2308 8.43659 12.2308C8.44015 12.2308 8.4437 12.2308 8.44725 12.2308C8.45079 12.2308 8.45433 12.2308 8.45786 12.2308C8.46139 12.2308 8.46491 12.2308 8.46843 12.2308C8.47194 12.2308 8.47545 12.2308 8.47895 12.2308C8.48244 12.2308 8.48594 12.2308 8.48942 12.2308C8.4929 12.2308 8.49638 12.2308 8.49984 12.2308C8.50331 12.2308 8.50677 12.2308 8.51022 12.2308C8.51367 12.2308 8.51712 12.2308 8.52055 12.2308C8.52399 12.2308 8.52742 12.2308 8.53084 12.2308C8.53426 12.2308 8.53767 12.2308 8.54107 12.2308C8.54448 12.2308 8.54787 12.2308 8.55126 12.2308C8.55465 12.2308 8.55803 12.2308 8.5614 12.2308C8.56477 12.2308 8.56813 12.2308 8.57149 12.2308C8.57484 12.2308 8.57819 12.2308 8.58153 12.2308C8.58487 12.2308 8.5882 12.2308 8.59152 12.2308C8.59484 12.2308 8.59816 12.2308 8.60146 12.2308C8.60477 12.2308 8.60807 12.2308 8.61136 12.2308C8.61465 12.2308 8.61793 12.2308 8.6212 12.2308C8.62447 12.2308 8.62774 12.2308 8.63099 12.2308C8.63425 12.2308 8.6375 12.2308 8.64073 12.2308C8.64397 12.2308 8.6472 12.2308 8.65043 12.2308C8.65365 12.2308 8.65686 12.2308 8.66007 12.2308C8.66327 12.2308 8.66647 12.2308 8.66966 12.2308C8.67285 12.2308 8.67603 12.2308 8.6792 12.2308C8.68237 12.2308 8.68553 12.2308 8.68869 12.2308C8.69184 12.2308 8.69499 12.2308 8.69812 12.2308C8.70126 12.2308 8.70439 12.2308 8.70751 12.2308C8.71063 12.2308 8.71374 12.2308 8.71684 12.2308C8.71994 12.2308 8.72303 12.2308 8.72612 12.2308C8.7292 12.2308 8.73228 12.2308 8.73535 12.2308C8.73841 12.2308 8.74147 12.2308 8.74452 12.2308C8.74757 12.2308 8.75061 12.2308 8.75364 12.2308C8.75667 12.2308 8.7597 12.2308 8.76271 12.2308C8.76572 12.2308 8.76873 12.2308 8.77172 12.2308C8.77472 12.2308 8.77771 12.2308 8.78068 12.2308C8.78366 12.2308 8.78663 12.2308 8.78959 12.2308C8.79255 12.2308 8.7955 12.2308 8.79844 12.2308C8.80138 12.2308 8.80432 12.2308 8.80724 12.2308C8.81016 12.2308 8.81308 12.2308 8.81598 12.2308C8.81889 12.2308 8.82178 12.2308 8.82467 12.2308C8.82756 12.2308 8.83043 12.2308 8.8333 12.2308C8.83617 12.2308 8.83903 12.2308 8.84188 12.2308C8.84473 12.2308 8.84757 12.2308 8.8504 12.2308C8.85323 12.2308 8.85605 12.2308 8.85887 12.2308C8.86168 12.2308 8.86448 12.2308 8.86728 12.2308C8.87007 12.2308 8.87285 12.2308 8.87563 12.2308C8.8784 12.2308 8.88117 12.2308 8.88392 12.2308C8.88668 12.2308 8.88943 12.2308 8.89216 12.2308C8.8949 12.2308 8.89763 12.2308 8.90035 12.2308C8.90306 12.2308 8.90577 12.2308 8.90847 12.2308C8.91117 12.2308 8.91386 12.2308 8.91654 12.2308C8.91922 12.2308 8.92189 12.2308 8.92455 12.2308C8.92721 12.2308 8.92986 12.2308 8.9325 12.2308C8.93514 12.2308 8.93777 12.2308 8.94039 12.2308C8.94301 12.2308 8.94563 12.2308 8.94823 12.2308C8.95083 12.2308 8.95342 12.2308 8.956 12.2308C8.95858 12.2308 8.96116 12.2308 8.96372 12.2308C8.96628 12.2308 8.96883 12.2308 8.97138 12.2308C8.97392 12.2308 8.97645 12.2308 8.97898 12.2308C8.9815 12.2308 8.98401 12.2308 8.98651 12.2308C8.98902 12.2308 8.99151 12.2308 8.99399 12.2308C8.99647 12.2308 8.99895 12.2308 9.00141 12.2308C9.00387 12.2308 9.00633 12.2308 9.00877 12.2308C9.01121 12.2308 9.01364 12.2308 9.01607 12.2308C9.01849 12.2308 9.0209 12.2308 9.0233 12.2308C9.0257 12.2308 9.0281 12.2308 9.03048 12.2308C9.03286 12.2308 9.03523 12.2308 9.03759 12.2308C9.03995 12.2308 9.0423 12.2308 9.04464 12.2308C9.04698 12.2308 9.04931 12.2308 9.05163 12.2308C9.05395 12.2308 9.05626 12.2308 9.05856 12.2308C9.06086 12.2308 9.06315 12.2308 9.06543 12.2308C9.06771 12.2308 9.06997 12.2308 9.07223 12.2308C9.07449 12.2308 9.07674 12.2308 9.07897 12.2308C9.08121 12.2308 9.08343 12.2308 9.08565 12.2308C9.08786 12.2308 9.09007 12.2308 9.09226 12.2308C9.09446 12.2308 9.09664 12.2308 9.09881 12.2308C9.10099 12.2308 9.10315 12.2308 9.1053 12.2308C9.10745 12.2308 9.10959 12.2308 9.11173 12.2308C9.11386 12.2308 9.11598 12.2308 9.11808 12.2308C9.12019 12.2308 9.12229 12.2308 9.12438 12.2308C9.12647 12.2308 9.12854 12.2308 9.13061 12.2308C9.13267 12.2308 9.13473 12.2308 9.13677 12.2308C9.13882 12.2308 9.14085 12.2308 9.14287 12.2308C9.1449 12.2308 9.14691 12.2308 9.14891 12.2308C9.15091 12.2308 9.1529 12.2308 9.15488 12.2308C9.15686 12.2308 9.15882 12.2308 9.16078 12.2308C9.16274 12.2308 9.16468 12.2308 9.16662 12.2308C9.16855 12.2308 9.17048 12.2308 9.17239 12.2308C9.1743 12.2308 9.1762 12.2308 9.17809 12.2308C9.17998 12.2308 9.18186 12.2308 9.18373 12.2308C9.1856 12.2308 9.18746 12.2308 9.1893 12.2308C9.19115 12.2308 9.19298 12.2308 9.1948 12.2308C9.19663 12.2308 9.19844 12.2308 9.20024 12.2308C9.20204 12.2308 9.20383 12.2308 9.20561 12.2308C9.20739 12.2308 9.20915 12.2308 9.21091 12.2308C9.21266 12.2308 9.21441 12.2308 9.21614 12.2308C9.21787 12.2308 9.21959 12.2308 9.2213 12.2308C9.22301 12.2308 9.22471 12.2308 9.2264 12.2308C9.22809 12.2308 9.22976 12.2308 9.23142 12.2308C9.23309 12.2308 9.23474 12.2308 9.23638 12.2308C9.23802 12.2308 9.23965 12.2308 9.24127 12.2308C9.24289 12.2308 9.24449 12.2308 9.24609 12.2308C9.24768 12.2308 9.24926 12.2308 9.25083 12.2308C9.25241 12.2308 9.25397 12.2308 9.25551 12.2308C9.25706 12.2308 9.2586 12.2308 9.26012 12.2308C9.26164 12.2308 9.26316 12.2308 9.26466 12.2308C9.26616 12.2308 9.26765 12.2308 9.26912 12.2308C9.2706 12.2308 9.27207 12.2308 9.27352 12.2308C9.27497 12.2308 9.27641 12.2308 9.27784 12.2308C9.27927 12.2308 9.28069 12.2308 9.2821 12.2308C9.2835 12.2308 9.28489 12.2308 9.28628 12.2308C9.28766 12.2308 9.28903 12.2308 9.29039 12.2308C9.29174 12.2308 9.29309 12.2308 9.29442 12.2308C9.29576 12.2308 9.29708 12.2308 9.29839 12.2308C9.2997 12.2308 9.30099 12.2308 9.30228 12.2308C9.30356 12.2308 9.30484 12.2308 9.3061 12.2308C9.30736 12.2308 9.30861 12.2308 9.30984 12.2308C9.31108 12.2308 9.3123 12.2308 9.31351 12.2308C9.31473 12.2308 9.31593 12.2308 9.31711 12.2308C9.3183 12.2308 9.31947 12.2308 9.32064 12.2308C9.3218 12.2308 9.32295 12.2308 9.32409 12.2308C9.32523 12.2308 9.32635 12.2308 9.32746 12.2308C9.32858 12.2308 9.32968 12.2308 9.33077 12.2308C9.33185 12.2308 9.33293 12.2308 9.33399 12.2308C9.33506 12.2308 9.33611 12.2308 9.33714 12.2308C9.33818 12.2308 9.33921 12.2308 9.34022 12.2308C9.34123 12.2308 9.34223 12.2308 9.34322 12.2308C9.34421 12.2308 9.34519 12.2308 9.34615 12.2308C9.34711 12.2308 9.34806 12.2308 9.349 12.2308C9.34993 12.2308 9.35086 12.2308 9.35177 12.2308C9.35268 12.2308 9.35358 12.2308 9.35447 12.2308C9.35535 12.2308 9.35623 12.2308 9.35709 12.2308C9.35795 12.2308 9.3588 12.2308 9.35963 12.2308C9.36047 12.2308 9.36129 12.2308 9.3621 12.2308C9.36291 12.2308 9.3637 12.2308 9.36449 12.2308C9.36527 12.2308 9.36604 12.2308 9.3668 12.2308C9.36755 12.2308 9.3683 12.2308 9.36903 12.2308C9.36976 12.2308 9.37048 12.2308 9.37118 12.2308C9.37189 12.2308 9.37258 12.2308 9.37326 12.2308C9.37394 12.2308 9.37461 12.2308 9.37526 12.2308C9.37591 12.2308 9.37655 12.2308 9.37718 12.2308C9.37781 12.2308 9.37842 12.2308 9.37902 12.2308C9.37962 12.2308 9.38021 12.2308 9.38078 12.2308C9.38135 12.2308 9.38192 12.2308 9.38246 12.2308C9.38301 12.2308 9.38354 12.2308 9.38406 12.2308C9.38459 12.2308 9.38509 12.2308 9.38559 12.2308C9.38608 12.2308 9.38656 12.2308 9.38703 12.2308C9.3875 12.2308 9.38795 12.2308 9.38839 12.2308C9.38883 12.2308 9.38926 12.2308 9.38967 12.2308C9.39008 12.2308 9.39048 12.2308 9.39087 12.2308C9.39126 12.2308 9.39163 12.2308 9.39199 12.2308C9.39235 12.2308 9.39269 12.2308 9.39303 12.2308C9.39336 12.2308 9.39368 12.2308 9.39398 12.2308C9.39429 12.2308 9.39458 12.2308 9.39486 12.2308C9.39513 12.2308 9.3954 12.2308 9.39565 12.2308C9.3959 12.2308 9.39613 12.2308 9.39636 12.2308C9.39658 12.2308 9.39679 12.2308 9.39698 12.2308C9.39718 12.2308 9.39736 12.2308 9.39753 12.2308C9.39769 12.2308 9.39785 12.2308 9.39799 12.2308C9.39813 12.2308 9.39825 12.2308 9.39837 12.2308C9.39848 12.2308 9.39858 12.2308 9.39866 12.2308C9.39874 12.2308 9.39881 12.2308 9.39887 12.2308C9.39893 12.2308 9.39897 12.2308 9.399 12.2308C9.39902 12.2308 9.39904 12.2308 9.39904 11.7308C9.39904 11.2308 9.39902 11.2308 9.399 11.2308C9.39897 11.2308 9.39893 11.2308 9.39887 11.2308C9.39881 11.2308 9.39874 11.2308 9.39866 11.2308C9.39858 11.2308 9.39848 11.2308 9.39837 11.2308C9.39825 11.2308 9.39813 11.2308 9.39799 11.2308C9.39785 11.2308 9.39769 11.2308 9.39753 11.2308C9.39736 11.2308 9.39718 11.2308 9.39698 11.2308C9.39679 11.2308 9.39658 11.2308 9.39636 11.2308C9.39613 11.2308 9.3959 11.2308 9.39565 11.2308C9.3954 11.2308 9.39513 11.2308 9.39486 11.2308C9.39458 11.2308 9.39429 11.2308 9.39398 11.2308C9.39368 11.2308 9.39336 11.2308 9.39303 11.2308C9.39269 11.2308 9.39235 11.2308 9.39199 11.2308C9.39163 11.2308 9.39126 11.2308 9.39087 11.2308C9.39048 11.2308 9.39008 11.2308 9.38967 11.2308C9.38926 11.2308 9.38883 11.2308 9.38839 11.2308C9.38795 11.2308 9.3875 11.2308 9.38703 11.2308C9.38656 11.2308 9.38608 11.2308 9.38559 11.2308C9.38509 11.2308 9.38459 11.2308 9.38406 11.2308C9.38354 11.2308 9.38301 11.2308 9.38246 11.2308C9.38192 11.2308 9.38135 11.2308 9.38078 11.2308C9.38021 11.2308 9.37962 11.2308 9.37902 11.2308C9.37842 11.2308 9.37781 11.2308 9.37718 11.2308C9.37655 11.2308 9.37591 11.2308 9.37526 11.2308C9.37461 11.2308 9.37394 11.2308 9.37326 11.2308C9.37258 11.2308 9.37189 11.2308 9.37118 11.2308C9.37048 11.2308 9.36976 11.2308 9.36903 11.2308C9.3683 11.2308 9.36755 11.2308 9.3668 11.2308C9.36604 11.2308 9.36527 11.2308 9.36449 11.2308C9.3637 11.2308 9.36291 11.2308 9.3621 11.2308C9.36129 11.2308 9.36047 11.2308 9.35963 11.2308C9.3588 11.2308 9.35795 11.2308 9.35709 11.2308C9.35623 11.2308 9.35535 11.2308 9.35447 11.2308C9.35358 11.2308 9.35268 11.2308 9.35177 11.2308C9.35086 11.2308 9.34993 11.2308 9.349 11.2308C9.34806 11.2308 9.34711 11.2308 9.34615 11.2308C9.34519 11.2308 9.34421 11.2308 9.34322 11.2308C9.34223 11.2308 9.34123 11.2308 9.34022 11.2308C9.33921 11.2308 9.33818 11.2308 9.33714 11.2308C9.33611 11.2308 9.33506 11.2308 9.33399 11.2308C9.33293 11.2308 9.33185 11.2308 9.33077 11.2308C9.32968 11.2308 9.32858 11.2308 9.32746 11.2308C9.32635 11.2308 9.32523 11.2308 9.32409 11.2308C9.32295 11.2308 9.3218 11.2308 9.32064 11.2308C9.31947 11.2308 9.3183 11.2308 9.31711 11.2308C9.31593 11.2308 9.31473 11.2308 9.31351 11.2308C9.3123 11.2308 9.31108 11.2308 9.30984 11.2308C9.30861 11.2308 9.30736 11.2308 9.3061 11.2308C9.30484 11.2308 9.30356 11.2308 9.30228 11.2308C9.30099 11.2308 9.2997 11.2308 9.29839 11.2308C9.29708 11.2308 9.29576 11.2308 9.29442 11.2308C9.29309 11.2308 9.29174 11.2308 9.29039 11.2308C9.28903 11.2308 9.28766 11.2308 9.28628 11.2308C9.28489 11.2308 9.2835 11.2308 9.2821 11.2308C9.28069 11.2308 9.27927 11.2308 9.27784 11.2308C9.27641 11.2308 9.27497 11.2308 9.27352 11.2308C9.27207 11.2308 9.2706 11.2308 9.26912 11.2308C9.26765 11.2308 9.26616 11.2308 9.26466 11.2308C9.26316 11.2308 9.26164 11.2308 9.26012 11.2308C9.2586 11.2308 9.25706 11.2308 9.25551 11.2308C9.25397 11.2308 9.25241 11.2308 9.25083 11.2308C9.24926 11.2308 9.24768 11.2308 9.24609 11.2308C9.24449 11.2308 9.24289 11.2308 9.24127 11.2308C9.23965 11.2308 9.23802 11.2308 9.23638 11.2308C9.23474 11.2308 9.23309 11.2308 9.23142 11.2308C9.22976 11.2308 9.22809 11.2308 9.2264 11.2308C9.22471 11.2308 9.22301 11.2308 9.2213 11.2308C9.21959 11.2308 9.21787 11.2308 9.21614 11.2308C9.21441 11.2308 9.21266 11.2308 9.21091 11.2308C9.20915 11.2308 9.20739 11.2308 9.20561 11.2308C9.20383 11.2308 9.20204 11.2308 9.20024 11.2308C9.19844 11.2308 9.19663 11.2308 9.1948 11.2308C9.19298 11.2308 9.19115 11.2308 9.1893 11.2308C9.18746 11.2308 9.1856 11.2308 9.18373 11.2308C9.18186 11.2308 9.17998 11.2308 9.17809 11.2308C9.1762 11.2308 9.1743 11.2308 9.17239 11.2308C9.17048 11.2308 9.16855 11.2308 9.16662 11.2308C9.16468 11.2308 9.16274 11.2308 9.16078 11.2308C9.15882 11.2308 9.15686 11.2308 9.15488 11.2308C9.1529 11.2308 9.15091 11.2308 9.14891 11.2308C9.14691 11.2308 9.1449 11.2308 9.14287 11.2308C9.14085 11.2308 9.13882 11.2308 9.13677 11.2308C9.13473 11.2308 9.13267 11.2308 9.13061 11.2308C9.12854 11.2308 9.12647 11.2308 9.12438 11.2308C9.12229 11.2308 9.12019 11.2308 9.11808 11.2308C9.11598 11.2308 9.11386 11.2308 9.11173 11.2308C9.10959 11.2308 9.10745 11.2308 9.1053 11.2308C9.10315 11.2308 9.10099 11.2308 9.09881 11.2308C9.09664 11.2308 9.09446 11.2308 9.09226 11.2308C9.09007 11.2308 9.08786 11.2308 9.08565 11.2308C9.08343 11.2308 9.08121 11.2308 9.07897 11.2308C9.07674 11.2308 9.07449 11.2308 9.07223 11.2308C9.06997 11.2308 9.06771 11.2308 9.06543 11.2308C9.06315 11.2308 9.06086 11.2308 9.05856 11.2308C9.05626 11.2308 9.05395 11.2308 9.05163 11.2308C9.04931 11.2308 9.04698 11.2308 9.04464 11.2308C9.0423 11.2308 9.03995 11.2308 9.03759 11.2308C9.03523 11.2308 9.03286 11.2308 9.03048 11.2308C9.0281 11.2308 9.0257 11.2308 9.0233 11.2308C9.0209 11.2308 9.01849 11.2308 9.01607 11.2308C9.01364 11.2308 9.01121 11.2308 9.00877 11.2308C9.00633 11.2308 9.00387 11.2308 9.00141 11.2308C8.99895 11.2308 8.99647 11.2308 8.99399 11.2308C8.99151 11.2308 8.98902 11.2308 8.98651 11.2308C8.98401 11.2308 8.9815 11.2308 8.97898 11.2308C8.97645 11.2308 8.97392 11.2308 8.97138 11.2308C8.96883 11.2308 8.96628 11.2308 8.96372 11.2308C8.96116 11.2308 8.95858 11.2308 8.956 11.2308C8.95342 11.2308 8.95083 11.2308 8.94823 11.2308C8.94563 11.2308 8.94301 11.2308 8.94039 11.2308C8.93777 11.2308 8.93514 11.2308 8.9325 11.2308C8.92986 11.2308 8.92721 11.2308 8.92455 11.2308C8.92189 11.2308 8.91922 11.2308 8.91654 11.2308C8.91386 11.2308 8.91117 11.2308 8.90847 11.2308C8.90577 11.2308 8.90306 11.2308 8.90035 11.2308C8.89763 11.2308 8.8949 11.2308 8.89216 11.2308C8.88943 11.2308 8.88668 11.2308 8.88392 11.2308C8.88117 11.2308 8.8784 11.2308 8.87563 11.2308C8.87285 11.2308 8.87007 11.2308 8.86728 11.2308C8.86448 11.2308 8.86168 11.2308 8.85887 11.2308C8.85605 11.2308 8.85323 11.2308 8.8504 11.2308C8.84757 11.2308 8.84473 11.2308 8.84188 11.2308C8.83903 11.2308 8.83617 11.2308 8.8333 11.2308C8.83043 11.2308 8.82756 11.2308 8.82467 11.2308C8.82178 11.2308 8.81889 11.2308 8.81598 11.2308C8.81308 11.2308 8.81016 11.2308 8.80724 11.2308C8.80432 11.2308 8.80138 11.2308 8.79844 11.2308C8.7955 11.2308 8.79255 11.2308 8.78959 11.2308C8.78663 11.2308 8.78366 11.2308 8.78068 11.2308C8.77771 11.2308 8.77472 11.2308 8.77172 11.2308C8.76873 11.2308 8.76572 11.2308 8.76271 11.2308C8.7597 11.2308 8.75667 11.2308 8.75364 11.2308C8.75061 11.2308 8.74757 11.2308 8.74452 11.2308C8.74147 11.2308 8.73841 11.2308 8.73535 11.2308C8.73228 11.2308 8.7292 11.2308 8.72612 11.2308C8.72303 11.2308 8.71994 11.2308 8.71684 11.2308C8.71374 11.2308 8.71063 11.2308 8.70751 11.2308C8.70439 11.2308 8.70126 11.2308 8.69812 11.2308C8.69499 11.2308 8.69184 11.2308 8.68869 11.2308C8.68553 11.2308 8.68237 11.2308 8.6792 11.2308C8.67603 11.2308 8.67285 11.2308 8.66966 11.2308C8.66647 11.2308 8.66327 11.2308 8.66007 11.2308C8.65686 11.2308 8.65365 11.2308 8.65043 11.2308C8.6472 11.2308 8.64397 11.2308 8.64073 11.2308C8.6375 11.2308 8.63425 11.2308 8.63099 11.2308C8.62774 11.2308 8.62447 11.2308 8.6212 11.2308C8.61793 11.2308 8.61465 11.2308 8.61136 11.2308C8.60807 11.2308 8.60477 11.2308 8.60146 11.2308C8.59816 11.2308 8.59484 11.2308 8.59152 11.2308C8.5882 11.2308 8.58487 11.2308 8.58153 11.2308C8.57819 11.2308 8.57484 11.2308 8.57149 11.2308C8.56813 11.2308 8.56477 11.2308 8.5614 11.2308C8.55803 11.2308 8.55465 11.2308 8.55126 11.2308C8.54787 11.2308 8.54448 11.2308 8.54107 11.2308C8.53767 11.2308 8.53426 11.2308 8.53084 11.2308C8.52742 11.2308 8.52399 11.2308 8.52055 11.2308C8.51712 11.2308 8.51367 11.2308 8.51022 11.2308C8.50677 11.2308 8.50331 11.2308 8.49984 11.2308C8.49638 11.2308 8.4929 11.2308 8.48942 11.2308C8.48594 11.2308 8.48244 11.2308 8.47895 11.2308C8.47545 11.2308 8.47194 11.2308 8.46843 11.2308C8.46491 11.2308 8.46139 11.2308 8.45786 11.2308C8.45433 11.2308 8.45079 11.2308 8.44725 11.2308C8.4437 11.2308 8.44015 11.2308 8.43659 11.2308C8.43303 11.2308 8.42946 11.2308 8.42589 11.2308C8.42231 11.2308 8.41873 11.2308 8.41514 11.2308C8.41155 11.2308 8.40795 11.2308 8.40434 11.2308C8.40074 11.2308 8.39712 11.2308 8.3935 11.2308C8.38988 11.2308 8.38625 11.2308 8.38262 11.2308C8.37898 11.2308 8.37534 11.2308 8.37169 11.2308C8.36804 11.2308 8.36438 11.2308 8.36072 11.2308C8.35705 11.2308 8.35338 11.2308 8.3497 11.2308C8.34602 11.2308 8.34233 11.2308 8.33864 11.2308C8.33494 11.2308 8.33124 11.2308 8.32753 11.2308C8.32382 11.2308 8.32011 11.2308 8.31638 11.2308C8.31266 11.2308 8.30893 11.2308 8.30519 11.2308C8.30145 11.2308 8.29771 11.2308 8.29395 11.2308C8.2902 11.2308 8.28644 11.2308 8.28268 11.2308C8.27891 11.2308 8.27514 11.2308 8.27136 11.2308C8.26758 11.2308 8.26379 11.2308 8.25999 11.2308C8.2562 11.2308 8.2524 11.2308 8.24859 11.2308C8.24478 11.2308 8.24097 11.2308 8.23714 11.2308C8.23332 11.2308 8.22949 11.2308 8.22566 11.2308C8.22182 11.2308 8.21798 11.2308 8.21413 11.2308C8.21028 11.2308 8.20642 11.2308 8.20256 11.2308C8.19869 11.2308 8.19482 11.2308 8.19094 11.2308C8.18707 11.2308 8.18318 11.2308 8.17929 11.2308C8.1754 11.2308 8.1715 11.2308 8.1676 11.2308C8.1637 11.2308 8.15978 11.2308 8.15587 11.2308C8.15195 11.2308 8.14803 11.2308 8.14409 11.2308C8.14016 11.2308 8.13623 11.2308 8.13228 11.2308C8.12834 11.2308 8.12439 11.2308 8.12043 11.2308C8.11647 11.2308 8.11251 11.2308 8.10854 11.2308C8.10457 11.2308 8.10059 11.2308 8.09661 11.2308C8.09263 11.2308 8.08864 11.2308 8.08464 11.2308C8.08065 11.2308 8.07664 11.2308 8.07264 11.2308C8.06863 11.2308 8.06461 11.2308 8.06059 11.2308C8.05657 11.2308 8.05254 11.2308 8.04851 11.2308C8.04447 11.2308 8.04043 11.2308 8.03639 11.2308C8.03234 11.2308 8.02829 11.2308 8.02423 11.2308C8.02017 11.2308 8.0161 11.2308 8.01203 11.2308C8.00796 11.2308 8.00388 11.2308 7.9998 11.2308C7.99571 11.2308 7.99162 11.2308 7.98753 11.2308C7.98343 11.2308 7.97933 11.2308 7.97522 11.2308C7.97111 11.2308 7.967 11.2308 7.96288 11.2308C7.95876 11.2308 7.95463 11.2308 7.9505 11.2308C7.94637 11.2308 7.94223 11.2308 7.93809 11.2308C7.93394 11.2308 7.92979 11.2308 7.92563 11.2308C7.92148 11.2308 7.91732 11.2308 7.91315 11.2308C7.90898 11.2308 7.90481 11.2308 7.90063 11.2308C7.89645 11.2308 7.89226 11.2308 7.88807 11.2308C7.88388 11.2308 7.87968 11.2308 7.87548 11.2308C7.87128 11.2308 7.86707 11.2308 7.86285 11.2308C7.85864 11.2308 7.85442 11.2308 7.85019 11.2308C7.84597 11.2308 7.84174 11.2308 7.8375 11.2308C7.83326 11.2308 7.82902 11.2308 7.82477 11.2308C7.82053 11.2308 7.81627 11.2308 7.81201 11.2308C7.80775 11.2308 7.80349 11.2308 7.79922 11.2308C7.79495 11.2308 7.79067 11.2308 7.78639 11.2308C7.78211 11.2308 7.77782 11.2308 7.77353 11.2308C7.76924 11.2308 7.76494 11.2308 7.76064 11.2308C7.75633 11.2308 7.75203 11.2308 7.74771 11.2308C7.7434 11.2308 7.73908 11.2308 7.73476 11.2308C7.73043 11.2308 7.7261 11.2308 7.72177 11.2308C7.71743 11.2308 7.71309 11.2308 7.70874 11.2308C7.7044 11.2308 7.70005 11.2308 7.69569 11.2308C7.69134 11.2308 7.68698 11.2308 7.68261 11.2308C7.67824 11.2308 7.67387 11.2308 7.66949 11.2308C7.66512 11.2308 7.66074 11.2308 7.65635 11.2308C7.65196 11.2308 7.64757 11.2308 7.64317 11.2308C7.63878 11.2308 7.63437 11.2308 7.62997 11.2308C7.62556 11.2308 7.62115 11.2308 7.61673 11.2308C7.61231 11.2308 7.60789 11.2308 7.60346 11.2308C7.59904 11.2308 7.59461 11.2308 7.59017 11.2308C7.58573 11.2308 7.58129 11.2308 7.57684 11.2308C7.5724 11.2308 7.56795 11.2308 7.56349 11.2308C7.55903 11.2308 7.55457 11.2308 7.55011 11.2308C7.54564 11.2308 7.54117 11.2308 7.5367 11.2308C7.53222 11.2308 7.52774 11.2308 7.52326 11.2308C7.51877 11.2308 7.51428 11.2308 7.50979 11.2308C7.5053 11.2308 7.5008 11.2308 7.49629 11.2308C7.49179 11.2308 7.48728 11.2308 7.48277 11.2308C7.47826 11.2308 7.47374 11.2308 7.46922 11.2308C7.4647 11.2308 7.46017 11.2308 7.45564 11.2308C7.45111 11.2308 7.44658 11.2308 7.44204 11.2308C7.4375 11.2308 7.43295 11.2308 7.4284 11.2308C7.42386 11.2308 7.4193 11.2308 7.41475 11.2308C7.41019 11.2308 7.40563 11.2308 7.40106 11.2308C7.3965 11.2308 7.39193 11.2308 7.38735 11.2308C7.38278 11.2308 7.3782 11.2308 7.37362 11.2308C7.36903 11.2308 7.36445 11.2308 7.35985 11.2308C7.35526 11.2308 7.35067 11.2308 7.34607 11.2308C7.34147 11.2308 7.33686 11.2308 7.33226 11.2308C7.32765 11.2308 7.32303 11.2308 7.31842 11.2308C7.3138 11.2308 7.30918 11.2308 7.30456 11.2308C7.29993 11.2308 7.2953 11.2308 7.29067 11.2308C7.28604 11.2308 7.2814 11.2308 7.27676 11.2308C7.27212 11.2308 7.26748 11.2308 7.26283 11.2308C7.25818 11.2308 7.25353 11.2308 7.24887 11.2308C7.24421 11.2308 7.23955 11.2308 7.23489 11.2308C7.23022 11.2308 7.22556 11.2308 7.22088 11.2308C7.21621 11.2308 7.21154 11.2308 7.20686 11.2308C7.20218 11.2308 7.19749 11.2308 7.19281 11.2308C7.18812 11.2308 7.18343 11.2308 7.17873 11.2308C7.17404 11.2308 7.16934 11.2308 7.16464 11.2308C7.15994 11.2308 7.15523 11.2308 7.15052 11.2308C7.14581 11.2308 7.1411 11.2308 7.13639 11.2308C7.13167 11.2308 7.12695 11.2308 7.12223 11.2308C7.1175 11.2308 7.11277 11.2308 7.10804 11.2308C7.10331 11.2308 7.09858 11.2308 7.09384 11.2308C7.0891 11.2308 7.08436 11.2308 7.07962 11.2308C7.07487 11.2308 7.07013 11.2308 7.06538 11.2308C7.06062 11.2308 7.05587 11.2308 7.05111 11.2308C7.04635 11.2308 7.04159 11.2308 7.03683 11.2308C7.03206 11.2308 7.02729 11.2308 7.02252 11.2308C7.01775 11.2308 7.01298 11.2308 7.0082 11.2308C7.00342 11.2308 6.99864 11.2308 6.99386 11.2308C6.98907 11.2308 6.98428 11.2308 6.97949 11.2308C6.9747 11.2308 6.96991 11.2308 6.96511 11.2308C6.96032 11.2308 6.95552 11.2308 6.95071 11.2308C6.94591 11.2308 6.9411 11.2308 6.93629 11.2308C6.93148 11.2308 6.92667 11.2308 6.92186 11.2308C6.91704 11.2308 6.91222 11.2308 6.9074 11.2308C6.90258 11.2308 6.89776 11.2308 6.89293 11.2308C6.8881 11.2308 6.88327 11.2308 6.87844 11.2308C6.87361 11.2308 6.86877 11.2308 6.86393 11.2308C6.85909 11.2308 6.85425 11.2308 6.84941 11.2308C6.84456 11.2308 6.83972 11.2308 6.83487 11.2308C6.83002 11.2308 6.82517 11.2308 6.82031 11.2308C6.81545 11.2308 6.8106 11.2308 6.80574 11.2308C6.80088 11.2308 6.79601 11.2308 6.79115 11.2308C6.78628 11.2308 6.78141 11.2308 6.77654 11.2308C6.77167 11.2308 6.76679 11.2308 6.76192 11.2308C6.75704 11.2308 6.75216 11.2308 6.74728 11.2308C6.7424 11.2308 6.73751 11.2308 6.73263 11.2308C6.72774 11.2308 6.72285 11.2308 6.71796 11.2308C6.71307 11.2308 6.70818 11.2308 6.70328 11.2308C6.69838 11.2308 6.69348 11.2308 6.68858 11.2308C6.68368 11.2308 6.67878 11.2308 6.67387 11.2308C6.66897 11.2308 6.66406 11.2308 6.65915 11.2308C6.65424 11.2308 6.64932 11.2308 6.64441 11.2308C6.63949 11.2308 6.63458 11.2308 6.62966 11.2308C6.62474 11.2308 6.61982 11.2308 6.61489 11.2308C6.60997 11.2308 6.60504 11.2308 6.60011 11.2308C6.59518 11.2308 6.59025 11.2308 6.58532 11.2308C6.58039 11.2308 6.57545 11.2308 6.57052 11.2308C6.56558 11.2308 6.56064 11.2308 6.5557 11.2308C6.55076 11.2308 6.54582 11.2308 6.54087 11.2308C6.53593 11.2308 6.53098 11.2308 6.52603 11.2308C6.52109 11.2308 6.51613 11.2308 6.51118 11.2308C6.50623 11.2308 6.50128 11.2308 6.49632 11.2308C6.49136 11.2308 6.4864 11.2308 6.48144 11.2308C6.47648 11.2308 6.47152 11.2308 6.46656 11.2308C6.4616 11.2308 6.45663 11.2308 6.45166 11.2308C6.4467 11.2308 6.44173 11.2308 6.43676 11.2308C6.43179 11.2308 6.42681 11.2308 6.42184 11.2308C6.41687 11.2308 6.41189 11.2308 6.40691 11.2308C6.40194 11.2308 6.39696 11.2308 6.39198 11.2308C6.387 11.2308 6.38202 11.2308 6.37703 11.2308C6.37205 11.2308 6.36706 11.2308 6.36208 11.2308C6.35709 11.2308 6.3521 11.2308 6.34711 11.2308C6.34212 11.2308 6.33713 11.2308 6.33214 11.2308C6.32715 11.2308 6.32215 11.2308 6.31716 11.2308C6.31216 11.2308 6.30717 11.2308 6.30217 11.2308C6.29717 11.2308 6.29217 11.2308 6.28717 11.2308C6.28217 11.2308 6.27717 11.2308 6.27216 11.2308C6.26716 11.2308 6.26216 11.2308 6.25715 11.2308C6.25214 11.2308 6.24714 11.2308 6.24213 11.2308C6.23712 11.2308 6.23211 11.2308 6.2271 11.2308C6.22209 11.2308 6.21708 11.2308 6.21207 11.2308C6.20705 11.2308 6.20204 11.2308 6.19702 11.2308C6.19201 11.2308 6.18699 11.2308 6.18198 11.2308C6.17696 11.2308 6.17194 11.2308 6.16692 11.2308C6.1619 11.2308 6.15688 11.2308 6.15186 11.2308C6.14684 11.2308 6.14182 11.2308 6.1368 11.2308C6.13177 11.2308 6.12675 11.2308 6.12172 11.2308C6.1167 11.2308 6.11167 11.2308 6.10665 11.2308C6.10162 11.2308 6.09659 11.2308 6.09156 11.2308C6.08654 11.2308 6.08151 11.2308 6.07648 11.2308C6.07145 11.2308 6.06642 11.2308 6.06139 11.2308C6.05636 11.2308 6.05132 11.2308 6.04629 11.2308C6.04126 11.2308 6.03622 11.2308 6.03119 11.2308C6.02616 11.2308 6.02112 11.2308 6.01609 11.2308C6.01105 11.2308 6.00602 11.2308 6.00098 11.2308C5.99594 11.2308 5.99091 11.2308 5.98587 11.2308C5.98083 11.2308 5.97579 11.2308 5.97076 11.2308C5.96572 11.2308 5.96068 11.2308 5.95564 11.2308C5.9506 11.2308 5.94556 11.2308 5.94052 11.2308C5.93548 11.2308 5.93044 11.2308 5.9254 11.2308C5.92036 11.2308 5.91531 11.2308 5.91027 11.2308C5.90523 11.2308 5.90019 11.2308 5.89515 11.2308C5.8901 11.2308 5.88506 11.2308 5.88002 11.2308C5.87498 11.2308 5.86993 11.2308 5.86489 11.2308C5.85985 11.2308 5.8548 11.2308 5.84976 11.2308C5.84472 11.2308 5.83967 11.2308 5.83463 11.2308C5.82958 11.2308 5.82454 11.2308 5.81949 11.2308C5.81445 11.2308 5.80941 11.2308 5.80436 11.2308C5.79932 11.2308 5.79427 11.2308 5.78923 11.2308C5.78418 11.2308 5.77914 11.2308 5.7741 11.2308C5.76905 11.2308 5.76401 11.2308 5.75896 11.2308C5.75392 11.2308 5.74887 11.2308 5.74383 11.2308C5.73878 11.2308 5.73374 11.2308 5.7287 11.2308C5.72365 11.2308 5.71861 11.2308 5.71356 11.2308C5.70852 11.2308 5.70348 11.2308 5.69843 11.2308C5.69339 11.2308 5.68835 11.2308 5.68331 11.2308C5.67826 11.2308 5.67322 11.2308 5.66818 11.2308C5.66314 11.2308 5.65809 11.2308 5.65305 11.2308C5.64801 11.2308 5.64297 11.2308 5.63793 11.2308C5.63289 11.2308 5.62785 11.2308 5.62281 11.2308C5.61777 11.2308 5.61273 11.2308 5.60769 11.2308C5.60265 11.2308 5.59761 11.2308 5.59257 11.2308C5.58753 11.2308 5.5825 11.2308 5.57746 11.2308C5.57242 11.2308 5.56738 11.2308 5.56235 11.2308C5.55731 11.2308 5.55228 11.2308 5.54724 11.2308C5.54221 11.2308 5.53717 11.2308 5.53214 11.2308C5.5271 11.2308 5.52207 11.2308 5.51704 11.2308C5.51201 11.2308 5.50698 11.2308 5.50194 11.2308C5.49691 11.2308 5.49188 11.2308 5.48685 11.2308C5.48182 11.2308 5.4768 11.2308 5.47177 11.2308C5.46674 11.2308 5.46171 11.2308 5.45669 11.2308C5.45166 11.2308 5.44664 11.2308 5.44161 11.2308C5.43659 11.2308 5.43156 11.2308 5.42654 11.2308C5.42152 11.2308 5.4165 11.2308 5.41148 11.2308C5.40645 11.2308 5.40143 11.2308 5.39642 11.2308C5.3914 11.2308 5.38638 11.2308 5.38136 11.2308C5.37635 11.2308 5.37133 11.2308 5.36632 11.2308C5.3613 11.2308 5.35629 11.2308 5.35127 11.2308C5.34626 11.2308 5.34125 11.2308 5.33624 11.2308C5.33123 11.2308 5.32622 11.2308 5.32121 11.2308C5.31621 11.2308 5.3112 11.2308 5.30619 11.2308C5.30119 11.2308 5.29619 11.2308 5.29118 11.2308C5.28618 11.2308 5.28118 11.2308 5.27618 11.2308C5.27118 11.2308 5.26618 11.2308 5.26118 11.2308C5.25618 11.2308 5.25119 11.2308 5.24619 11.2308C5.2412 11.2308 5.23621 11.2308 5.23121 11.2308C5.22622 11.2308 5.22123 11.2308 5.21624 11.2308C5.21125 11.2308 5.20627 11.2308 5.20128 11.2308C5.19629 11.2308 5.19131 11.2308 5.18633 11.2308C5.18134 11.2308 5.17636 11.2308 5.17138 11.2308C5.1664 11.2308 5.16142 11.2308 5.15645 11.2308C5.15147 11.2308 5.1465 11.2308 5.14152 11.2308C5.13655 11.2308 5.13158 11.2308 5.12661 11.2308C5.12164 11.2308 5.11667 11.2308 5.1117 11.2308C5.10674 11.2308 5.10177 11.2308 5.09681 11.2308C5.09185 11.2308 5.08689 11.2308 5.08193 11.2308C5.07697 11.2308 5.07201 11.2308 5.06705 11.2308C5.0621 11.2308 5.05714 11.2308 5.05219 11.2308C5.04724 11.2308 5.04229 11.2308 5.03734 11.2308C5.0324 11.2308 5.02745 11.2308 5.02251 11.2308C5.01756 11.2308 5.01262 11.2308 5.00768 11.2308C5.00274 11.2308 4.9978 11.2308 4.99287 11.2308C4.98793 11.2308 4.983 11.2308 4.97806 11.2308C4.97313 11.2308 4.9682 11.2308 4.96328 11.2308C4.95835 11.2308 4.95342 11.2308 4.9485 11.2308C4.94358 11.2308 4.93866 11.2308 4.93374 11.2308C4.92882 11.2308 4.9239 11.2308 4.91899 11.2308C4.91407 11.2308 4.90916 11.2308 4.90425 11.2308C4.89934 11.2308 4.89443 11.2308 4.88953 11.2308C4.88462 11.2308 4.87972 11.2308 4.87482 11.2308C4.86992 11.2308 4.86502 11.2308 4.86013 11.2308C4.85523 11.2308 4.85034 11.2308 4.84545 11.2308C4.84056 11.2308 4.83567 11.2308 4.83078 11.2308C4.8259 11.2308 4.82101 11.2308 4.81613 11.2308C4.81125 11.2308 4.80638 11.2308 4.8015 11.2308C4.79662 11.2308 4.79175 11.2308 4.78688 11.2308C4.78201 11.2308 4.77714 11.2308 4.77228 11.2308C4.76741 11.2308 4.76255 11.2308 4.75769 11.2308C4.75283 11.2308 4.74797 11.2308 4.74312 11.2308C4.73827 11.2308 4.73341 11.2308 4.72856 11.2308C4.72372 11.2308 4.71887 11.2308 4.71403 11.2308C4.70918 11.2308 4.70434 11.2308 4.69951 11.2308C4.69467 11.2308 4.68983 11.2308 4.685 11.2308C4.68017 11.2308 4.67534 11.2308 4.67052 11.2308C4.66569 11.2308 4.66087 11.2308 4.65605 11.2308C4.65123 11.2308 4.64641 11.2308 4.64159 11.2308C4.63678 11.2308 4.63197 11.2308 4.62716 11.2308C4.62235 11.2308 4.61755 11.2308 4.61275 11.2308C4.60794 11.2308 4.60315 11.2308 4.59835 11.2308C4.59355 11.2308 4.58876 11.2308 4.58397 11.2308C4.57918 11.2308 4.5744 11.2308 4.56961 11.2308C4.56483 11.2308 4.56005 11.2308 4.55527 11.2308C4.5505 11.2308 4.54572 11.2308 4.54095 11.2308C4.53618 11.2308 4.53142 11.2308 4.52665 11.2308C4.52189 11.2308 4.51713 11.2308 4.51237 11.2308C4.50762 11.2308 4.50286 11.2308 4.49811 11.2308C4.49336 11.2308 4.48862 11.2308 4.48387 11.2308C4.47913 11.2308 4.47439 11.2308 4.46965 11.2308C4.46492 11.2308 4.46018 11.2308 4.45546 11.2308C4.45073 11.2308 4.446 11.2308 4.44128 11.2308C4.43656 11.2308 4.43184 11.2308 4.42712 11.2308C4.42241 11.2308 4.4177 11.2308 4.41299 11.2308C4.40828 11.2308 4.40358 11.2308 4.39887 11.2308C4.39417 11.2308 4.38948 11.2308 4.38478 11.2308C4.38009 11.2308 4.3754 11.2308 4.37072 11.2308C4.36603 11.2308 4.36135 11.2308 4.35667 11.2308C4.35199 11.2308 4.34732 11.2308 4.34265 11.2308C4.33798 11.2308 4.33331 11.2308 4.32865 11.2308C4.32399 11.2308 4.31933 11.2308 4.31467 11.2308C4.31002 11.2308 4.30537 11.2308 4.30072 11.2308C4.29607 11.2308 4.29143 11.2308 4.28679 11.2308C4.28215 11.2308 4.27751 11.2308 4.27288 11.2308C4.26825 11.2308 4.26362 11.2308 4.259 11.2308C4.25438 11.2308 4.24976 11.2308 4.24514 11.2308C4.24053 11.2308 4.23592 11.2308 4.23131 11.2308C4.22671 11.2308 4.2221 11.2308 4.21751 11.2308C4.21291 11.2308 4.20831 11.2308 4.20372 11.2308C4.19913 11.2308 4.19455 11.2308 4.18997 11.2308C4.18538 11.2308 4.18081 11.2308 4.17623 11.2308C4.17166 11.2308 4.16709 11.2308 4.16253 11.2308C4.15796 11.2308 4.1534 11.2308 4.14885 11.2308C4.14429 11.2308 4.13974 11.2308 4.1352 11.2308C4.13065 11.2308 4.12611 11.2308 4.12157 11.2308C4.11703 11.2308 4.1125 11.2308 4.10797 11.2308C4.10344 11.2308 4.09892 11.2308 4.0944 11.2308C4.08988 11.2308 4.08536 11.2308 4.08085 11.2308C4.07634 11.2308 4.07183 11.2308 4.06733 11.2308C4.06283 11.2308 4.05833 11.2308 4.05384 11.2308C4.04935 11.2308 4.04486 11.2308 4.04038 11.2308C4.0359 11.2308 4.03142 11.2308 4.02695 11.2308C4.02247 11.2308 4.018 11.2308 4.01354 11.2308C4.00908 11.2308 4.00462 11.2308 4.00016 11.2308C3.99571 11.2308 3.99126 11.2308 3.98681 11.2308C3.98237 11.2308 3.97793 11.2308 3.97349 11.2308C3.96906 11.2308 3.96463 11.2308 3.9602 11.2308C3.95578 11.2308 3.95136 11.2308 3.94694 11.2308C3.94253 11.2308 3.93812 11.2308 3.93371 11.2308C3.92931 11.2308 3.92491 11.2308 3.92051 11.2308C3.91612 11.2308 3.91173 11.2308 3.90734 11.2308C3.90296 11.2308 3.89858 11.2308 3.8942 11.2308C3.88983 11.2308 3.88546 11.2308 3.88109 11.2308C3.87673 11.2308 3.87237 11.2308 3.86802 11.2308C3.86366 11.2308 3.85931 11.2308 3.85497 11.2308C3.85063 11.2308 3.84629 11.2308 3.84195 11.2308C3.83762 11.2308 3.83329 11.2308 3.82897 11.2308C3.82465 11.2308 3.82033 11.2308 3.81602 11.2308C3.81171 11.2308 3.8074 11.2308 3.8031 11.2308C3.7988 11.2308 3.7945 11.2308 3.79021 11.2308C3.78592 11.2308 3.78164 11.2308 3.77736 11.2308C3.77308 11.2308 3.76881 11.2308 3.76454 11.2308C3.76027 11.2308 3.75601 11.2308 3.75175 11.2308C3.74749 11.2308 3.74324 11.2308 3.739 11.2308C3.73475 11.2308 3.73051 11.2308 3.72627 11.2308C3.72204 11.2308 3.71781 11.2308 3.71359 11.2308C3.70936 11.2308 3.70515 11.2308 3.70093 11.2308C3.69672 11.2308 3.69252 11.2308 3.68831 11.2308C3.68411 11.2308 3.67992 11.2308 3.67573 11.2308C3.67154 11.2308 3.66736 11.2308 3.66318 11.2308C3.659 11.2308 3.65483 11.2308 3.65067 11.2308C3.6465 11.2308 3.64234 11.2308 3.63819 11.2308C3.63403 11.2308 3.62988 11.2308 3.62574 11.2308C3.6216 11.2308 3.61746 11.2308 3.61333 11.2308C3.6092 11.2308 3.60508 11.2308 3.60096 11.2308V12.2308ZM6.40637 5.67369C6.41733 5.64445 6.43108 5.63184 6.44211 5.62463C6.45582 5.61567 6.47592 5.6088 6.5 5.6088C6.52408 5.6088 6.54418 5.61567 6.55789 5.62463C6.56892 5.63184 6.58267 5.64445 6.59363 5.67369L7.52996 5.32257C7.17308 4.37088 5.82692 4.37088 5.47004 5.32257L6.40637 5.67369Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg b/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg deleted file mode 100644 index ab53a64b38..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_text_highlight.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 4.66667C10.2498 6.36375 11.5514 7.66117 13.1667 7.83333M10.7911 3.86693L5.65847 9.10195C5.46467 9.30075 5.27712 9.69232 5.23961 9.96341L5.0083 11.9152C4.92702 12.6201 5.45217 13.102 6.17736 12.9815L8.19041 12.6502C8.47174 12.602 8.8656 12.4032 9.0594 12.1984L14.192 6.96336C15.0798 6.05974 15.4799 5.0296 14.0983 3.77054C12.7229 2.52354 11.6789 2.9633 10.7911 3.86693Z" stroke="#1F2329" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/toolbar_underline.svg b/frontend/resources/flowy_icons/20x/toolbar_underline.svg deleted file mode 100644 index ea467a45d6..0000000000 --- a/frontend/resources/flowy_icons/20x/toolbar_underline.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4 16H16M5.5 4V8.5C5.5 10.9879 7.51214 13 10 13C12.4879 13 14.5 10.9879 14.5 8.5V4" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/turninto.svg b/frontend/resources/flowy_icons/20x/turninto.svg deleted file mode 100644 index 598b870ec7..0000000000 --- a/frontend/resources/flowy_icons/20x/turninto.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4 12H16L13 15M16 8L4 8L7 5" stroke="#21232A" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_bulleted_list.svg b/frontend/resources/flowy_icons/20x/type_bulleted_list.svg deleted file mode 100644 index bc726f59ec..0000000000 --- a/frontend/resources/flowy_icons/20x/type_bulleted_list.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7 15H17M7 5H17M7 10H17" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<circle cx="3.75" cy="5" r="0.75" fill="#1F2329"/> -<circle cx="3.75" cy="10" r="0.75" fill="#1F2329"/> -<circle cx="3.75" cy="15" r="0.75" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_callout.svg b/frontend/resources/flowy_icons/20x/type_callout.svg deleted file mode 100644 index a933b4bbb3..0000000000 --- a/frontend/resources/flowy_icons/20x/type_callout.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4 10H16M4 13H16M4 16H16M5 7H15C16.3807 7 17 6.5 17 5.5C17 4.5 16.3807 4 15 4H5C3.61929 4 3 4.5 3 5.5C3 6.5 3.61929 7 5 7Z" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_font.svg b/frontend/resources/flowy_icons/20x/type_font.svg deleted file mode 100644 index d0b33b0277..0000000000 --- a/frontend/resources/flowy_icons/20x/type_font.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.35082 5.90976L6.70217 6.2655L6.70217 6.2655L6.35082 5.90976ZM5.5625 8.03125C5.5625 8.30739 5.78636 8.53125 6.0625 8.53125C6.33864 8.53125 6.5625 8.30739 6.5625 8.03125H5.5625ZM13.6492 5.90976L13.2978 6.2655L13.2978 6.26551L13.6492 5.90976ZM13.4375 8.03125C13.4375 8.30739 13.6614 8.53125 13.9375 8.53125C14.2136 8.53125 14.4375 8.30739 14.4375 8.03125H13.4375ZM7.75 13.875C7.47386 13.875 7.25 14.0989 7.25 14.375C7.25 14.6511 7.47386 14.875 7.75 14.875V13.875ZM12.25 14.875C12.5261 14.875 12.75 14.6511 12.75 14.375C12.75 14.0989 12.5261 13.875 12.25 13.875V14.875ZM10 5.125H8.03125V6.125H10V5.125ZM8.03125 5.125C7.58118 5.125 7.19393 5.12396 6.88531 5.16495C6.56293 5.20775 6.25224 5.30436 5.99946 5.55402L6.70217 6.2655C6.73771 6.2304 6.8032 6.18463 7.01694 6.15624C7.24443 6.12604 7.55325 6.125 8.03125 6.125V5.125ZM5.99947 5.55401C5.74583 5.80452 5.64685 6.11387 5.60311 6.43521C5.56141 6.74152 5.5625 7.12544 5.5625 7.56944H6.5625C6.5625 7.09683 6.56359 6.79328 6.59397 6.57009C6.62231 6.36193 6.66749 6.29975 6.70217 6.2655L5.99947 5.55401ZM5.5625 7.56944V8.03125H6.5625V7.56944H5.5625ZM10 6.125H11.9688V5.125H10V6.125ZM11.9688 6.125C12.4467 6.125 12.7556 6.12604 12.9831 6.15624C13.1968 6.18463 13.2623 6.2304 13.2978 6.2655L14.0005 5.55401C13.7477 5.30436 13.4371 5.20775 13.1147 5.16495C12.8061 5.12396 12.4188 5.125 11.9688 5.125V6.125ZM13.2978 6.26551C13.3325 6.29976 13.3777 6.36193 13.406 6.57009C13.4364 6.79328 13.4375 7.09683 13.4375 7.56944H14.4375C14.4375 7.12544 14.4386 6.74152 14.3969 6.43521C14.3531 6.11387 14.2542 5.80451 14.0005 5.55401L13.2978 6.26551ZM13.4375 7.56944V8.03125H14.4375V7.56944H13.4375ZM9.5 5.625V14.375H10.5V5.625H9.5ZM7.75 14.875H12.25V13.875H7.75V14.875ZM4.75 3.5H15.25V2.5H4.75V3.5ZM16.5 4.75V15.25H17.5V4.75H16.5ZM15.25 16.5H4.75V17.5H15.25V16.5ZM3.5 15.25V4.75H2.5V15.25H3.5ZM4.75 16.5C4.05964 16.5 3.5 15.9404 3.5 15.25H2.5C2.5 16.4926 3.50736 17.5 4.75 17.5V16.5ZM16.5 15.25C16.5 15.9404 15.9404 16.5 15.25 16.5V17.5C16.4926 17.5 17.5 16.4926 17.5 15.25H16.5ZM15.25 3.5C15.9404 3.5 16.5 4.05964 16.5 4.75H17.5C17.5 3.50736 16.4926 2.5 15.25 2.5V3.5ZM4.75 2.5C3.50736 2.5 2.5 3.50736 2.5 4.75H3.5C3.5 4.05964 4.05964 3.5 4.75 3.5V2.5Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_formula.svg b/frontend/resources/flowy_icons/20x/type_formula.svg deleted file mode 100644 index 316c225b79..0000000000 --- a/frontend/resources/flowy_icons/20x/type_formula.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16 5.625C16 4.17525 14.7688 3 13.25 3C11.7312 3 10.5 4.17525 10.5 5.625V14.375C10.5 15.8247 9.26878 17 7.75 17C6.23122 17 5 15.8247 5 14.375M14.5104 10H7.40625" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_h1.svg b/frontend/resources/flowy_icons/20x/type_h1.svg deleted file mode 100644 index a6a7f561cf..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h1.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.61539 4V9.99974M2.61539 9.99974V15.9994M2.61539 9.99974L10.5679 10.0003M10.5679 10.0003V4.00058M10.5679 10.0003V16M13.9765 9.0993L15.6805 7.8994V15.999M15.6805 15.999H13.9765M15.6805 15.999H17.3846" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_h2.svg b/frontend/resources/flowy_icons/20x/type_h2.svg deleted file mode 100644 index 9bba1b7d33..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h2.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M17.3846 16H13.4097V14.7131C13.4097 14.0316 13.7743 13.4087 14.3514 13.104L16.5392 11.9487C17.0325 11.6882 17.3846 11.1851 17.3846 10.6027C17.3846 10.2043 17.354 9.81347 17.2951 9.4327C17.193 8.77344 16.6632 8.2959 16.0326 8.24256C15.7294 8.2169 15.4227 8.20382 15.1132 8.20382C14.5343 8.20382 13.9654 8.24958 13.4097 8.33785M2.61539 4V9.99754M2.61539 9.99754V15.995M2.61539 9.99754L10.5654 9.9981M10.5654 9.9981V4.00058M10.5654 9.9981V15.9956" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_h3.svg b/frontend/resources/flowy_icons/20x/type_h3.svg deleted file mode 100644 index 3b231df67d..0000000000 --- a/frontend/resources/flowy_icons/20x/type_h3.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16.7452 12.1019C17.1476 12.6927 17.3846 13.4176 17.3846 14.2009C17.3846 14.4694 17.3568 14.7311 17.304 14.9828C17.1873 15.5389 16.7022 15.8972 16.1651 15.9493C15.8188 15.9829 15.4679 16 15.1133 16C14.5343 16 13.9654 15.9543 13.4098 15.866M16.7452 12.1019C17.1476 11.5112 17.3846 10.7863 17.3846 10.003C17.3846 9.73439 17.3568 9.47276 17.304 9.22105C17.1873 8.66494 16.7022 8.30667 16.1651 8.25459C15.8188 8.22099 15.4679 8.20382 15.1133 8.20382C14.5343 8.20382 13.9654 8.24958 13.4098 8.33785M16.7452 12.1019H14.5454M2.61539 4V9.99754M2.61539 9.99754V15.995M2.61539 9.99754L10.5652 9.9981M10.5652 9.9981V4.00058M10.5652 9.9981V15.9956" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_numbered_list.svg b/frontend/resources/flowy_icons/20x/type_numbered_list.svg deleted file mode 100644 index 23046f9b34..0000000000 --- a/frontend/resources/flowy_icons/20x/type_numbered_list.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.25 15.25H17M8.25 4.75H17M8.25 10H17" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3 8.125H5.625M3 3.75H4.3125V8.125M5.625 16.25H3C3 14.4999 5.625 14.4999 5.625 13.1221C5.625 11.4374 3 11.4374 3 12.75" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_page.svg b/frontend/resources/flowy_icons/20x/type_page.svg deleted file mode 100644 index 405548fcf7..0000000000 --- a/frontend/resources/flowy_icons/20x/type_page.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M16 9.06203H12.9689C11.3451 9.06203 10.8039 8.41252 10.8039 6.46397V2.99988M16 12.1V9.33984C16 8.86242 15.8292 8.40074 15.5185 8.03826L11.7986 3.69841C11.4187 3.25511 10.864 2.99999 10.2801 2.99999H8.2C5.2 2.99999 4 4.39999 4 7.89999V12.1C4 15.6 5.2 17 8.2 17H11.8C14.8 17 16 15.6 16 12.1Z" stroke="#1F2329" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_quote.svg b/frontend/resources/flowy_icons/20x/type_quote.svg deleted file mode 100644 index 3564d92ff8..0000000000 --- a/frontend/resources/flowy_icons/20x/type_quote.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.5 4H10.5M6.5 8H16.5M6.5 12H16.5M6.5 16H16.5M3.5 8L3.5 16" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_strikethrough.svg b/frontend/resources/flowy_icons/20x/type_strikethrough.svg deleted file mode 100644 index dbf4e86116..0000000000 --- a/frontend/resources/flowy_icons/20x/type_strikethrough.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.6667 10C13.5333 10.4333 14.4 11.3 14.4 13.0333C14.4 15.6333 11.8 16.5 10.0667 16.5C8.33333 16.5 6.6 16.0667 5.3 13.9M13.9667 6.53333C13.9667 6.1 13.5333 3.5 10.0666 3.5C6.16661 3.5 6.16667 6.53333 6.59998 7.4M4 10H16.1333" stroke="#1F2329" stroke-linecap="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_text.svg b/frontend/resources/flowy_icons/20x/type_text.svg deleted file mode 100644 index 40335aa89b..0000000000 --- a/frontend/resources/flowy_icons/20x/type_text.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M10 3H6.9375C5.49382 3 4.77198 3 4.3235 3.45561C3.875 3.91122 3.875 4.64452 3.875 6.11111V6.85M10 3H13.0625C14.5062 3 15.228 3 15.6765 3.45561C16.125 3.91122 16.125 4.64452 16.125 6.11111V6.85M10 3V17M6.5 17H13.5" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_todo.svg b/frontend/resources/flowy_icons/20x/type_todo.svg deleted file mode 100644 index 3d4f38ae9f..0000000000 --- a/frontend/resources/flowy_icons/20x/type_todo.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.02513 4.02513L4.37868 4.37868L4.37868 4.37868L4.02513 4.02513ZM15.9749 4.02513L15.6213 4.37867L15.6213 4.37869L15.9749 4.02513ZM15.9749 15.9749L15.6213 15.6213L15.6213 15.6213L15.9749 15.9749ZM4.02513 15.9749L4.37869 15.6213L4.37867 15.6213L4.02513 15.9749ZM7.90355 9.99645C7.70829 9.80118 7.39171 9.80118 7.19645 9.99645C7.00118 10.1917 7.00118 10.5083 7.19645 10.7036L7.90355 9.99645ZM8.95 11.75L8.59645 12.1036C8.79171 12.2988 9.10829 12.2988 9.30355 12.1036L8.95 11.75ZM12.8036 8.60355C12.9988 8.40829 12.9988 8.09171 12.8036 7.89645C12.6083 7.70118 12.2917 7.70118 12.0964 7.89645L12.8036 8.60355ZM3.5 10C3.5 8.33595 3.50106 7.13821 3.62368 6.22617C3.74437 5.32852 3.9745 4.78286 4.37868 4.37868L3.67158 3.67157C3.05063 4.29252 2.7682 5.08438 2.6326 6.09292C2.49894 7.08708 2.5 8.36422 2.5 10H3.5ZM4.37868 4.37868C4.78286 3.9745 5.32852 3.74437 6.22617 3.62368C7.13821 3.50106 8.33595 3.5 10 3.5V2.5C8.36422 2.5 7.08708 2.49894 6.09292 2.6326C5.08438 2.7682 4.29252 3.05063 3.67157 3.67158L4.37868 4.37868ZM10 3.5C11.664 3.5 12.8618 3.50106 13.7738 3.62368C14.6715 3.74437 15.2171 3.9745 15.6213 4.37867L16.3284 3.67158C15.7075 3.05063 14.9156 2.76819 13.9071 2.6326C12.9129 2.49894 11.6358 2.5 10 2.5V3.5ZM15.6213 4.37869C16.0255 4.78286 16.2556 5.32852 16.3763 6.22617C16.4989 7.13821 16.5 8.33595 16.5 10H17.5C17.5 8.36422 17.5011 7.08708 17.3674 6.09292C17.2318 5.08438 16.9494 4.29252 16.3284 3.67157L15.6213 4.37869ZM16.5 10C16.5 11.664 16.4989 12.8618 16.3763 13.7738C16.2556 14.6715 16.0255 15.2171 15.6213 15.6213L16.3284 16.3284C16.9494 15.7075 17.2318 14.9156 17.3674 13.9071C17.5011 12.9129 17.5 11.6358 17.5 10H16.5ZM15.6213 15.6213C15.2171 16.0255 14.6715 16.2556 13.7738 16.3763C12.8618 16.4989 11.664 16.5 10 16.5V17.5C11.6358 17.5 12.9129 17.5011 13.9071 17.3674C14.9156 17.2318 15.7075 16.9494 16.3284 16.3284L15.6213 15.6213ZM10 16.5C8.33595 16.5 7.13821 16.4989 6.22617 16.3763C5.32852 16.2556 4.78286 16.0255 4.37869 15.6213L3.67157 16.3284C4.29252 16.9494 5.08438 17.2318 6.09292 17.3674C7.08708 17.5011 8.36422 17.5 10 17.5V16.5ZM4.37867 15.6213C3.9745 15.2171 3.74437 14.6715 3.62368 13.7738C3.50106 12.8618 3.5 11.664 3.5 10H2.5C2.5 11.6358 2.49894 12.9129 2.6326 13.9071C2.76819 14.9156 3.05063 15.7075 3.67158 16.3284L4.37867 15.6213ZM7.19645 10.7036L8.59645 12.1036L9.30355 11.3964L7.90355 9.99645L7.19645 10.7036ZM9.30355 12.1036L12.8036 8.60355L12.0964 7.89645L8.59645 11.3964L9.30355 12.1036Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h1.svg b/frontend/resources/flowy_icons/20x/type_toggle_h1.svg deleted file mode 100644 index 45cc7d3859..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h1.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.29961 5V9.99978M7.29961 9.99978V14.9995M7.29961 9.99978L13.761 10.0002M13.761 10.0002V5.00049M13.761 10.0002V15M16.5305 9.24942L17.9151 8.2495V14.9992M17.9151 14.9992H16.5305M17.9151 14.9992H19.2996M1 12.5L4 10L1 7.5V12.5Z" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h2.svg b/frontend/resources/flowy_icons/20x/type_toggle_h2.svg deleted file mode 100644 index 3dce8523e8..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h2.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.29961 5V9.99978M7.29961 9.99978V14.9995M7.29961 9.99978L13.761 10.0002M13.761 10.0002V5.00049M13.761 10.0002V15M19 15H16V14.0096C16 13.4851 16.2751 13.0057 16.7107 12.7712L18.3619 11.8821C18.7342 11.6816 19 11.2944 19 10.8462C19 10.5396 18.9769 10.2388 18.9324 9.94576C18.8554 9.43839 18.4555 9.07087 17.9796 9.02981C17.7507 9.01007 17.5193 9 17.2857 9C16.8487 9 16.4194 9.03522 16 9.10315M1 12.5L4 10L1 7.5V12.5Z" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_toggle_h3.svg b/frontend/resources/flowy_icons/20x/type_toggle_h3.svg deleted file mode 100644 index e619a5f250..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_h3.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.29961 5V9.99978M7.29961 9.99978V14.9995M7.29961 9.99978L13.761 10.0002M13.761 10.0002V5.00049M13.761 10.0002V15M18.5174 12C18.8211 12.4547 19 13.0125 19 13.6154C19 13.8221 18.979 14.0235 18.9391 14.2172C18.8511 14.6451 18.4849 14.9209 18.0796 14.961C17.8182 14.9868 17.5534 15 17.2857 15C16.8487 15 16.4194 14.9648 16 14.8969M18.5174 12C18.8211 11.5454 19 10.9875 19 10.3846C19 10.1779 18.979 9.97659 18.9391 9.78287C18.8511 9.35488 18.4849 9.07916 18.0796 9.03907C17.8182 9.01322 17.5534 9 17.2857 9C16.8487 9 16.4194 9.03522 16 9.10315M18.5174 12H16.8571M1 12.5L4 10L1 7.5V12.5Z" stroke="#1F2329" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/20x/type_toggle_list.svg b/frontend/resources/flowy_icons/20x/type_toggle_list.svg deleted file mode 100644 index 2cb1e83599..0000000000 --- a/frontend/resources/flowy_icons/20x/type_toggle_list.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12.4384 9.62558C12.7055 9.8037 12.7055 10.1963 12.4384 10.3744L7.69961 13.5336C7.40057 13.733 7 13.5186 7 13.1592V6.84083C7 6.48142 7.40057 6.26704 7.69962 6.46641L12.4384 9.62558Z" fill="#1F2329"/> -</svg> diff --git a/frontend/resources/flowy_icons/24x/calendar_layout.svg b/frontend/resources/flowy_icons/24x/calendar_layout.svg new file mode 100644 index 0000000000..52e06e9111 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/calendar_layout.svg @@ -0,0 +1,4 @@ +<svg width="800" height="800" viewBox="-.02 0 122.88 122.88" xmlns="http://www.w3.org/2000/svg"> + <path + d="M81.54 4.71c0-2.62 2.58-4.71 5.77-4.71 3.2 0 5.77 2.13 5.77 4.71V25.4c0 2.62-2.58 4.71-5.77 4.71-3.2 0-5.77-2.13-5.77-4.71V4.71zM20.4 89.87l7.55-.47c.16 1.22.5 2.16 1 2.79.82 1.04 1.99 1.56 3.51 1.56 1.13 0 2.01-.26 2.62-.8.61-.54.92-1.15.92-1.85 0-.66-.29-1.26-.87-1.79-.58-.53-1.93-1.02-4.06-1.49-3.49-.78-5.96-1.82-7.45-3.12-1.5-1.29-2.25-2.95-2.25-4.96 0-1.32.38-2.56 1.15-3.74.77-1.18 1.92-2.1 3.46-2.77 1.54-.67 3.65-1.01 6.32-1.01 3.29 0 5.79.61 7.52 1.84 1.72 1.22 2.75 3.17 3.08 5.84l-7.47.44c-.2-1.17-.62-2.02-1.25-2.55-.64-.53-1.52-.8-2.64-.8-.92 0-1.62.2-2.09.59-.47.39-.7.87-.7 1.43 0 .41.19.77.57 1.1.37.34 1.25.65 2.65.95 3.47.75 5.96 1.51 7.46 2.28 1.5.77 2.6 1.71 3.28 2.85.68 1.13 1.02 2.4 1.02 3.81 0 1.65-.46 3.17-1.37 4.56-.92 1.39-2.19 2.45-3.83 3.17-1.64.72-3.7 1.08-6.19 1.08-4.37 0-7.4-.84-9.09-2.53-1.67-1.68-2.63-3.82-2.85-6.41zm44.13-17.22h7.94v15.33c0 1.52-.24 2.95-.71 4.3a9.377 9.377 0 01-2.23 3.55c-1.01 1.01-2.07 1.72-3.18 2.13-1.54.57-3.4.86-5.56.86-1.25 0-2.62-.09-4.1-.26-1.48-.17-2.72-.52-3.71-1.04-.99-.52-1.9-1.26-2.72-2.22-.83-.96-1.39-1.95-1.69-2.96-.49-1.64-.74-3.08-.74-4.35V72.65h7.94v15.69c0 1.4.39 2.5 1.16 3.28.78.79 1.86 1.19 3.23 1.19 1.36 0 2.43-.39 3.21-1.17.77-.77 1.16-1.87 1.16-3.3V72.65zm13.32 0h7.42l9.65 14.21V72.65h7.51v25.73h-7.51l-9.59-14.13v14.13h-7.47V72.65h-.01zM29.53 4.71C29.53 2.09 32.11 0 35.3 0c3.2 0 5.77 2.13 5.77 4.71V25.4c0 2.62-2.58 4.71-5.77 4.71-3.2 0-5.77-2.13-5.77-4.71V4.71zM7.56 44.09h107.62V22.66c0-.8-.31-1.55-.84-2.04-.53-.53-1.24-.84-2.04-.84h-9.31c-1.78 0-3.2-2.63-3.2-4.41 0-1.78 1.42-3.2 3.2-3.2h10.53c2.58 0 4.88 1.07 6.57 2.75 1.69 1.69 2.75 4.04 2.75 6.57v92.06c0 2.58-1.07 4.88-2.75 6.57-1.69 1.69-4.04 2.75-6.57 2.75H9.33c-2.58 0-4.88-1.07-6.57-2.75-1.69-1.68-2.76-4.04-2.76-6.57V21.49c0-2.58 1.07-4.88 2.75-6.57 1.69-1.69 4.04-2.75 6.57-2.75H20.6c1.78 0 3.2 1.42 3.2 3.2s-1.42 4.41-3.2 4.41H10.54c-.8 0-1.55.31-2.09.84-.53.53-.84 1.24-.84 2.09v21.43l-.05-.05zm107.63 8.81H7.56v59.4c0 .8.31 1.55.84 2.09.53.53 1.24.84 2.09.84h101.76c.8 0 1.55-.31 2.09-.84.53-.53.84-1.24.84-2.09V52.9h.01zM50.36 19.73c-1.78 0-3.2-2.63-3.2-4.41 0-1.78 1.42-3.2 3.2-3.2h21.49c1.78 0 3.2 1.42 3.2 3.2 0 1.78-1.42 4.41-3.2 4.41H50.36z" /> +</svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/16x/close_filled.svg b/frontend/resources/flowy_icons/24x/close_filled.svg similarity index 100% rename from frontend/resources/flowy_icons/16x/close_filled.svg rename to frontend/resources/flowy_icons/24x/close_filled.svg diff --git a/frontend/resources/flowy_icons/24x/database_layout.svg b/frontend/resources/flowy_icons/24x/database_layout.svg new file mode 100644 index 0000000000..240a5065d8 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/database_layout.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path + d="M3 6C3 3.79086 4.79086 2 7 2H17C19.2091 2 21 3.79086 21 6C21 8.20914 19.2091 10 17 10H7C4.79086 10 3 8.20914 3 6Z" + stroke="#000000" stroke-width="2" /> + <path + d="M3 16C3 14.8954 3.89543 14 5 14H8C9.10457 14 10 14.8954 10 16V19C10 20.1046 9.10457 21 8 21H5C3.89543 21 3 20.1046 3 19V16Z" + stroke="#000000" stroke-width="2" /> + <path + d="M14 17.5C14 15.567 15.567 14 17.5 14C19.433 14 21 15.567 21 17.5C21 19.433 19.433 21 17.5 21C15.567 21 14 19.433 14 17.5Z" + stroke="#000000" stroke-width="2" /> +</svg> \ No newline at end of file diff --git a/frontend/resources/flowy_icons/24x/m_home_active_notification.svg b/frontend/resources/flowy_icons/24x/m_home_active_notification.svg deleted file mode 100644 index 17cd501184..0000000000 --- a/frontend/resources/flowy_icons/24x/m_home_active_notification.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M19.3399 14.49L18.3399 12.83C18.1299 12.46 17.9399 11.76 17.9399 11.35V8.82C17.9399 6.47 16.5599 4.44 14.5699 3.49C14.0499 2.57 13.0899 2 11.9899 2C10.8999 2 9.91994 2.59 9.39994 3.52C7.44994 4.49 6.09994 6.5 6.09994 8.82V11.35C6.09994 11.76 5.90994 12.46 5.69994 12.82L4.68994 14.49C4.28994 15.16 4.19994 15.9 4.44994 16.58C4.68994 17.25 5.25994 17.77 5.99994 18.02C7.93994 18.68 9.97994 19 12.0199 19C14.0599 19 16.0999 18.68 18.0399 18.03C18.7399 17.8 19.2799 17.27 19.5399 16.58C19.7999 15.89 19.7299 15.13 19.3399 14.49Z" fill="#00BDF1"/> -<path d="M14.8301 20.01C14.4101 21.17 13.3001 22 12.0001 22C11.2101 22 10.4301 21.68 9.88005 21.11C9.56005 20.81 9.32005 20.41 9.18005 20C9.31005 20.02 9.44005 20.03 9.58005 20.05C9.81005 20.08 10.0501 20.11 10.2901 20.13C10.8601 20.18 11.4401 20.21 12.0201 20.21C12.5901 20.21 13.1601 20.18 13.7201 20.13C13.9301 20.11 14.1401 20.1 14.3401 20.07C14.5001 20.05 14.6601 20.03 14.8301 20.01Z" fill="#00BDF1"/> -</svg> diff --git a/frontend/resources/flowy_icons/24x/m_publish.svg b/frontend/resources/flowy_icons/24x/m_publish.svg deleted file mode 100644 index de50eea53a..0000000000 --- a/frontend/resources/flowy_icons/24x/m_publish.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.7846 6.23846C14.7846 4.10628 14.7846 3.04021 14.1222 2.37784C13.4598 1.7154 12.3937 1.7154 10.2615 1.7154H5.73842C3.60629 1.7154 2.54017 1.7154 1.87779 2.37784C1.21536 3.04021 1.21536 4.10634 1.21536 6.23846" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M7.99975 15.2847V5.48468M7.99975 5.48468L11.0151 8.78274M7.99975 5.48468L4.98438 8.78274" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -</svg> diff --git a/frontend/resources/flowy_icons/24x/m_table_text_color.svg b/frontend/resources/flowy_icons/24x/m_table_text_color.svg deleted file mode 100644 index bd8253ea0d..0000000000 --- a/frontend/resources/flowy_icons/24x/m_table_text_color.svg +++ /dev/null @@ -1,4 +0,0 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M14.8724 11.5318L14.8561 11.4938L14.8155 11.5016C14.7642 11.5114 14.7113 11.5166 14.6571 11.5166H9.34296C9.28863 11.5166 9.23556 11.5114 9.1842 11.5015L9.14353 11.4937L9.12722 11.5318L7.45253 15.4423C7.27102 15.8662 6.78056 16.0625 6.35709 15.8809C5.93359 15.6992 5.73737 15.2084 5.9189 14.7845L10.826 3.32604C11.2683 2.29319 12.7313 2.29319 13.1737 3.32604L18.0807 14.7845C18.2622 15.2084 18.066 15.6992 17.6425 15.8809C17.219 16.0625 16.7286 15.8662 16.5471 15.4423L14.8724 11.5318ZM12.0471 4.93434L11.9998 4.824L11.9526 4.93434L9.87952 9.77508L9.84884 9.84672H9.92677H14.0728H14.1508L14.1201 9.77508L12.0471 4.93434Z" fill="#1E2022" stroke="white" stroke-width="0.102805"/> -<rect x="3" y="18.5" width="18" height="3" rx="1.5" fill="#1E2022"/> -</svg> diff --git a/frontend/resources/flowy_icons/24x/m_unpublish.svg b/frontend/resources/flowy_icons/24x/m_unpublish.svg deleted file mode 100644 index b7ed4a5902..0000000000 --- a/frontend/resources/flowy_icons/24x/m_unpublish.svg +++ /dev/null @@ -1,12 +0,0 @@ -<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_361_3208)"> -<path d="M0.559387 1.54443L15.2066 15.8953" stroke="black" stroke-linecap="round"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.36877 5.14741C8.27405 5.0438 8.14014 4.98479 7.99976 4.98479C7.85938 4.98479 7.72547 5.0438 7.63074 5.14741L7.04482 5.78826L8.49976 7.21377V6.77266L10.6461 9.12024C10.8325 9.32404 11.1487 9.3382 11.3525 9.15187C11.5563 8.96553 11.5705 8.64927 11.3841 8.44547L8.36877 5.14741ZM8.49976 8.61375V15.2848C8.49976 15.5609 8.2759 15.7848 7.99976 15.7848C7.72362 15.7848 7.49976 15.5609 7.49976 15.2848L7.49976 7.63398L8.49976 8.61375ZM7.08437 7.22699L6.36964 6.52673L4.61537 8.44547C4.42904 8.64927 4.4432 8.96553 4.647 9.15187C4.8508 9.3382 5.16706 9.32404 5.3534 9.12024L7.08437 7.22699Z" fill="black"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M1.55997 1.98933C1.54804 2.00074 1.5362 2.01232 1.52444 2.02409L1.52441 2.02412C1.08482 2.46369 0.893062 3.01891 0.802803 3.69018C0.715517 4.33935 0.715527 5.16661 0.715539 6.20119V6.20122V6.23828C0.715539 6.51442 0.939397 6.73828 1.21554 6.73828C1.49168 6.73828 1.71554 6.51442 1.71554 6.23828C1.71554 5.15808 1.7166 4.39822 1.79388 3.82344C1.86923 3.26305 2.00869 2.95403 2.23151 2.73123L2.23155 2.73119C2.24566 2.71708 2.26011 2.7033 2.27496 2.68985L1.63588 2.0637L1.55997 1.98933ZM2.73054 1.38624L3.6236 2.26123C4.15767 2.21596 4.83641 2.21522 5.7386 2.21522H10.2617C11.342 2.21522 12.1018 2.21628 12.6766 2.29356C13.237 2.36891 13.546 2.50837 13.7689 2.73121C13.9917 2.95401 14.1311 3.26302 14.2065 3.82341C14.2837 4.39819 14.2848 5.15805 14.2848 6.23828C14.2848 6.39472 14.3566 6.53438 14.4691 6.62606C14.5552 6.69622 14.6651 6.73828 14.7848 6.73828C15.0609 6.73828 15.2848 6.51442 15.2848 6.23828V6.20123V6.20122C15.2848 5.16659 15.2848 4.33933 15.1975 3.69017C15.1073 3.01888 14.9155 2.46367 14.476 2.0241C14.0364 1.58451 13.4812 1.39274 12.8099 1.30248C12.1607 1.2152 11.3334 1.21521 10.2988 1.21522H10.2988H10.2617H5.7386H5.70155C4.66695 1.21521 3.83968 1.2152 3.1905 1.30248C3.0306 1.32398 2.87728 1.35124 2.73054 1.38624Z" fill="black"/> -</g> -<defs> -<clipPath id="clip0_361_3208"> -<rect width="16" height="16" fill="white" transform="matrix(-1 0 0 -1 16 16.5)"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/flowy_icons/24x/show.svg b/frontend/resources/flowy_icons/24x/show.svg index 0d411e5cc4..3550115093 100644 --- a/frontend/resources/flowy_icons/24x/show.svg +++ b/frontend/resources/flowy_icons/24x/show.svg @@ -1,5 +1,4 @@ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M2.41663 12C2.41663 12 5.29163 5.29167 12 5.29167C18.7083 5.29167 21.5833 12 21.5833 12C21.5833 12 18.7083 18.7083 12 18.7083C5.29163 18.7083 2.41663 12 2.41663 12Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M9.125 12C9.125 12.7625 9.4279 13.4938 9.96707 14.0329C10.5062 14.5721 11.2375 14.875 12 14.875C12.7625 14.875 13.4938 14.5721 14.0329 14.0329C14.5721 13.4938 14.875 12.7625 14.875 12C14.875 11.2375 14.5721 10.5062 14.0329 9.96707C13.4938 9.4279 12.7625 9.125 12 9.125C11.2375 9.125 10.5062 9.4279 9.96707 9.96707C9.4279 10.5062 9.125 11.2375 9.125 12Z" stroke="black" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.01097 13.6526C4.30282 12.6533 4.30282 11.3467 5.01097 10.3474C6.26959 8.57133 8.66728 6 12 6C15.3327 6 17.7304 8.57133 18.989 10.3474C19.6972 11.3467 19.6972 12.6533 18.989 13.6526C17.7304 15.4287 15.3327 18 12 18C8.66728 18 6.26959 15.4287 5.01097 13.6526Z" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.9999 14.25C13.2049 14.25 14.1818 13.2426 14.1818 12C14.1818 10.7574 13.2049 9.75 11.9999 9.75C10.7949 9.75 9.81812 10.7574 9.81812 12C9.81812 13.2426 10.7949 14.25 11.9999 14.25Z" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> - diff --git a/frontend/resources/flowy_icons/40x/embed_error.svg b/frontend/resources/flowy_icons/40x/embed_error.svg deleted file mode 100644 index 68196c7b7e..0000000000 --- a/frontend/resources/flowy_icons/40x/embed_error.svg +++ /dev/null @@ -1,7 +0,0 @@ -<svg width="64" height="64" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path fill-rule="evenodd" clip-rule="evenodd" d="M18 8a4 4 0 0 0-4 4v40a4 4 0 0 0 4 4h28a4 4 0 0 0 4-4V20L38 8H18Z" fill="#fff"/> - <rect x="24" y="24" width="3" height="6" rx="1.5" fill="#D3D8E1"/> - <rect x="37" y="24" width="3" height="6" rx="1.5" fill="#D3D8E1"/> - <path d="m38 8 12 12H40a2 2 0 0 1-2-2V8Z" fill="#D3D8E1"/> - <path fill-rule="evenodd" clip-rule="evenodd" d="M38 40a6 6 0 0 0-12 0h12Z" fill="#D3D8E1"/> -</svg> diff --git a/frontend/resources/flowy_icons/40x/app_logo.svg b/frontend/resources/flowy_icons/40x/flowy_logo.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo.svg rename to frontend/resources/flowy_icons/40x/flowy_logo.svg diff --git a/frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg b/frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo_with_text_dark.svg rename to frontend/resources/flowy_icons/40x/flowy_logo_dark_mode.svg diff --git a/frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg b/frontend/resources/flowy_icons/40x/flowy_logo_text.svg similarity index 100% rename from frontend/resources/flowy_icons/40x/app_logo_with_text_light.svg rename to frontend/resources/flowy_icons/40x/flowy_logo_text.svg diff --git a/frontend/resources/flowy_icons/40x/icon_warning.svg b/frontend/resources/flowy_icons/40x/icon_warning.svg deleted file mode 100644 index abdf5fb20d..0000000000 --- a/frontend/resources/flowy_icons/40x/icon_warning.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> -<ellipse cx="21.9997" cy="21.6966" rx="20.1667" ry="19.8885" fill="#FF811A"/> -<path d="M22.0003 12.6562C20.9878 12.6562 20.167 13.4657 20.167 14.4643V23.5045C20.167 24.5031 20.9878 25.3126 22.0003 25.3126C23.0128 25.3126 23.8337 24.5031 23.8337 23.5045V14.4643C23.8337 13.4657 23.0128 12.6562 22.0003 12.6562Z" fill="white"/> -<path d="M22.0003 30.7367C23.0128 30.7367 23.8337 29.9272 23.8337 28.9287C23.8337 27.9301 23.0128 27.1206 22.0003 27.1206C20.9878 27.1206 20.167 27.9301 20.167 28.9287C20.167 29.9272 20.9878 30.7367 22.0003 30.7367Z" fill="white"/> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_apple_icon.svg b/frontend/resources/flowy_icons/40x/m_apple_icon.svg deleted file mode 100644 index 5ec0b2627c..0000000000 --- a/frontend/resources/flowy_icons/40x/m_apple_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M36.2874 31.1721C35.677 32.5696 34.9544 33.856 34.1173 35.0387C32.9761 36.651 32.0418 37.767 31.3217 38.3867C30.2055 39.404 29.0095 39.925 27.7289 39.9546C26.8095 39.9546 25.7007 39.6953 24.4101 39.1694C23.1152 38.646 21.9252 38.3867 20.8372 38.3867C19.696 38.3867 18.4722 38.646 17.1631 39.1694C15.852 39.6953 14.7959 39.9694 13.9883 39.9966C12.7602 40.0484 11.5361 39.5126 10.3143 38.3867C9.5344 37.7127 8.55895 36.5572 7.3904 34.9202C6.13664 33.1721 5.10588 31.145 4.29836 28.8339C3.43353 26.3377 3 23.9205 3 21.5803C3 18.8997 3.58452 16.5877 4.75531 14.6502C5.67545 13.0939 6.89956 11.8663 8.43163 10.9651C9.9637 10.0639 11.6191 9.60465 13.4018 9.57527C14.3773 9.57527 15.6564 9.87427 17.2461 10.4619C18.8312 11.0515 19.849 11.3505 20.2953 11.3505C20.6289 11.3505 21.7595 11.0009 23.6763 10.3039C25.4889 9.65749 27.0188 9.38984 28.272 9.49527C31.668 9.76687 34.2194 11.0935 35.9162 13.4835C32.8789 15.3072 31.3765 17.8614 31.4064 21.1381C31.4338 23.6904 32.3682 25.8143 34.2045 27.5007C35.0366 28.2833 35.966 28.8883 37 29.3179C36.7758 29.9623 36.5391 30.5796 36.2874 31.1721ZM28.4988 0.800228C28.4988 2.80068 27.7612 4.6685 26.2912 6.39734C24.5172 8.45259 22.3715 9.64021 20.0446 9.4528C20.0149 9.21281 19.9978 8.96023 19.9978 8.6948C19.9978 6.77437 20.8414 4.71912 22.3396 3.03868C23.0876 2.18784 24.0388 1.48038 25.1924 0.916026C26.3435 0.360092 27.4324 0.0526484 28.4564 0C28.4863 0.26743 28.4988 0.534877 28.4988 0.800202V0.800228Z" fill="black"/> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_discord_icon.svg b/frontend/resources/flowy_icons/40x/m_discord_icon.svg deleted file mode 100644 index 12842e2100..0000000000 --- a/frontend/resources/flowy_icons/40x/m_discord_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M33.7359 8.64277C31.2096 7.48358 28.5005 6.62955 25.6679 6.1404C25.6163 6.13096 25.5648 6.15455 25.5382 6.20174C25.1898 6.82142 24.8039 7.62985 24.5336 8.26527C21.4871 7.80916 18.4561 7.80916 15.472 8.26527C15.2017 7.61572 14.8018 6.82142 14.4518 6.20174C14.4252 6.15613 14.3737 6.13254 14.3221 6.1404C11.4911 6.62798 8.78201 7.48202 6.25412 8.64277C6.23224 8.6522 6.21348 8.66795 6.20103 8.68838C1.06243 16.3653 -0.345244 23.8536 0.345315 31.249C0.348439 31.2852 0.36875 31.3198 0.396872 31.3418C3.78717 33.8316 7.07126 35.3431 10.2944 36.3449C10.3459 36.3607 10.4006 36.3418 10.4334 36.2993C11.1958 35.2582 11.8755 34.1603 12.4582 33.0058C12.4926 32.9382 12.4598 32.858 12.3895 32.8313C11.3115 32.4223 10.285 31.9237 9.29757 31.3575C9.21947 31.3119 9.21322 31.2002 9.28507 31.1467C9.49285 30.991 9.7007 30.829 9.8991 30.6655C9.935 30.6356 9.98502 30.6293 10.0272 30.6482C16.5141 33.6098 23.5368 33.6098 29.9471 30.6482C29.9894 30.6277 30.0394 30.634 30.0768 30.6639C30.2753 30.8275 30.4831 30.991 30.6924 31.1467C30.7643 31.2002 30.7596 31.3119 30.6815 31.3575C29.6941 31.9347 28.6676 32.4223 27.588 32.8297C27.5177 32.8564 27.4865 32.9382 27.5209 33.0058C28.1161 34.1587 28.7957 35.2565 29.5441 36.2978C29.5753 36.3418 29.6316 36.3607 29.6831 36.3449C32.9219 35.3431 36.2059 33.8316 39.5962 31.3418C39.6259 31.3198 39.6447 31.2868 39.6478 31.2506C40.4743 22.7007 38.2635 15.2738 33.7874 8.68994C33.7765 8.66795 33.7578 8.6522 33.7359 8.64277ZM13.4269 26.746C11.4739 26.746 9.86471 24.953 9.86471 22.751C9.86471 20.549 11.4427 18.7561 13.4269 18.7561C15.4267 18.7561 17.0203 20.5648 16.989 22.751C16.989 24.953 15.411 26.746 13.4269 26.746ZM26.5975 26.746C24.6446 26.746 23.0354 24.953 23.0354 22.751C23.0354 20.549 24.6133 18.7561 26.5975 18.7561C28.5973 18.7561 30.1909 20.5648 30.1597 22.751C30.1597 24.953 28.5973 26.746 26.5975 26.746Z" fill="#5865F2"/> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_empty_notification.svg b/frontend/resources/flowy_icons/40x/m_empty_notification.svg deleted file mode 100644 index f1ec21a65c..0000000000 --- a/frontend/resources/flowy_icons/40x/m_empty_notification.svg +++ /dev/null @@ -1,6 +0,0 @@ -<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.4"> -<path d="M16.5001 40.3334H27.5001C36.6667 40.3334 40.3334 36.6667 40.3334 27.5001V16.5001C40.3334 7.33341 36.6667 3.66675 27.5001 3.66675H16.5001C7.33341 3.66675 3.66675 7.33341 3.66675 16.5001V27.5001C3.66675 36.6667 7.33341 40.3334 16.5001 40.3334Z" stroke="#171717" stroke-width="1.83333" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M3.66675 23.8338H10.5601C11.9534 23.8338 13.2184 24.6221 13.8417 25.8688L15.4734 29.1504C16.5001 31.1671 18.3334 31.1671 18.7734 31.1671H25.2451C26.6384 31.1671 27.9034 30.3788 28.5267 29.1321L30.1584 25.8504C30.7817 24.6038 32.0467 23.8154 33.4401 23.8154H40.2967" stroke="#171717" stroke-width="1.83333" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_empty_trash.svg b/frontend/resources/flowy_icons/40x/m_empty_trash.svg deleted file mode 100644 index 1cb0d4ba15..0000000000 --- a/frontend/resources/flowy_icons/40x/m_empty_trash.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g opacity="0.6"> -<path d="M49 10.5517C40.1201 9.7597 31.1865 9.35171 22.2799 9.35171C16.9999 9.35171 11.7199 9.5917 6.44 10.0717L1 10.5517" stroke="#171717" stroke-width="1.83" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M15.6652 8.12783L16.2518 4.98388C16.6785 2.70395 16.9985 1 21.5051 1H28.4918C32.9984 1 33.3451 2.79995 33.7451 5.00788L34.3317 8.12783" stroke="#171717" stroke-width="1.83" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M43.2662 18.137L41.5329 42.3042C41.2396 46.0721 40.9996 49 33.5596 49H16.4397C8.99973 49 8.75973 46.0721 8.46635 42.3042L6.73303 18.137" stroke="#171717" stroke-width="1.83" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M20.5466 35.799H29.4266" stroke="#171717" stroke-width="1.83" stroke-linecap="round" stroke-linejoin="round"/> -<path d="M18.3333 26.1992H31.6666" stroke="#171717" stroke-width="1.83" stroke-linecap="round" stroke-linejoin="round"/> -</g> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_github_icon.svg b/frontend/resources/flowy_icons/40x/m_github_icon.svg deleted file mode 100644 index 000c5f9213..0000000000 --- a/frontend/resources/flowy_icons/40x/m_github_icon.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M19.993 1C15.2746 1.00245 10.7109 2.6736 7.11804 5.71464C3.52513 8.75568 1.13726 12.9683 0.381404 17.5993C-0.374455 22.2302 0.550977 26.9775 2.99224 30.9922C5.43351 35.007 9.23141 38.0274 13.7068 39.5135C14.6942 39.6967 15.0661 39.0848 15.0661 38.5645C15.0661 38.0441 15.0463 36.5355 15.0397 34.8862C9.51058 36.0807 8.34223 32.553 8.34223 32.553C7.44044 30.2623 6.13714 29.6601 6.13714 29.6601C4.33357 28.4362 6.27209 28.4591 6.27209 28.4591C8.26983 28.5999 9.31971 30.4979 9.31971 30.4979C11.0904 33.5183 13.9701 32.6446 15.1023 32.1341C15.28 30.8546 15.7967 29.9841 16.3661 29.49C11.9493 28.9925 7.30879 27.2974 7.30879 19.725C7.28142 17.7611 8.01434 15.8619 9.35591 14.4203C9.15186 13.9229 8.47057 11.9136 9.55008 9.1844C9.55008 9.1844 11.2187 8.65427 15.0167 11.2101C18.2744 10.3242 21.7115 10.3242 24.9692 11.2101C28.7639 8.65427 30.4293 9.1844 30.4293 9.1844C31.5121 11.9071 30.8308 13.9164 30.6267 14.4203C31.9726 15.8621 32.707 17.7646 32.6771 19.7315C32.6771 27.3203 28.0267 28.9925 23.6034 29.4801C24.3143 30.0954 24.9495 31.2964 24.9495 33.142C24.9495 35.7862 24.9264 37.9132 24.9264 38.5645C24.9264 39.0913 25.2852 39.7066 26.2923 39.5135C30.7682 38.0273 34.5665 35.0063 37.0077 30.9908C39.4489 26.9754 40.3739 22.2274 39.6172 17.596C38.8604 12.9647 36.4714 8.75199 32.8773 5.71147C29.2832 2.67095 24.7185 1.0009 19.9995 1H19.993Z" fill="#191717"/> -<path d="M7.64403 29.3754C7.60125 29.4736 7.44327 29.503 7.31491 29.4343C7.18656 29.3656 7.09112 29.238 7.1372 29.1365C7.18327 29.0351 7.33796 29.0089 7.46631 29.0776C7.59467 29.1463 7.6934 29.2772 7.64403 29.3754Z" fill="#191717"/> -<path d="M8.45041 30.2688C8.38226 30.3029 8.30428 30.3125 8.22983 30.2957C8.15538 30.279 8.0891 30.2371 8.04231 30.1772C7.91396 30.0397 7.88761 29.8499 7.98635 29.7648C8.08508 29.6797 8.26282 29.719 8.39118 29.8565C8.51953 29.9939 8.54915 30.1837 8.45041 30.2688Z" fill="#191717"/> -<path d="M9.23364 31.4043C9.11186 31.4894 8.90451 31.4043 8.78932 31.2341C8.75747 31.2036 8.73214 31.167 8.71483 31.1265C8.69753 31.0861 8.6886 31.0425 8.6886 30.9985C8.6886 30.9545 8.69753 30.911 8.71483 30.8705C8.73214 30.83 8.75747 30.7934 8.78932 30.7629C8.91109 30.6811 9.11845 30.7629 9.23364 30.9298C9.34883 31.0967 9.35212 31.3192 9.23364 31.4043Z" fill="#191717"/> -<path d="M10.2968 32.5039C10.1881 32.625 9.96764 32.5922 9.78662 32.4286C9.60561 32.265 9.56281 32.0425 9.67142 31.9246C9.78003 31.8068 10.0005 31.8396 10.1881 31.9999C10.3757 32.1603 10.4119 32.3861 10.2968 32.5039Z" fill="#191717"/> -<path d="M11.7876 33.1453C11.7382 33.2991 11.5144 33.3678 11.2906 33.3023C11.0668 33.2369 10.9187 33.0536 10.9615 32.8965C11.0043 32.7395 11.2314 32.6675 11.4585 32.7395C11.6856 32.8115 11.8304 32.9849 11.7876 33.1453Z" fill="#191717"/> -<path d="M13.4136 33.2565C13.4136 33.4169 13.2293 33.5543 12.9924 33.5576C12.7554 33.5609 12.5612 33.43 12.5612 33.2696C12.5612 33.1093 12.7455 32.9718 12.9825 32.9686C13.2194 32.9653 13.4136 33.0929 13.4136 33.2565Z" fill="#191717"/> -<path d="M14.9274 33.0046C14.957 33.1649 14.7924 33.3318 14.5555 33.3711C14.3185 33.4104 14.1111 33.3155 14.0815 33.1584C14.0519 33.0013 14.223 32.8311 14.4534 32.7886C14.6838 32.7461 14.8977 32.8442 14.9274 33.0046Z" fill="#191717"/> -</svg> diff --git a/frontend/resources/flowy_icons/40x/m_google_icon.svg b/frontend/resources/flowy_icons/40x/m_google_icon.svg deleted file mode 100644 index 3f1aaeafe8..0000000000 --- a/frontend/resources/flowy_icons/40x/m_google_icon.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_10_316)"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M39.2 20.4546C39.2 19.0364 39.0727 17.6728 38.8364 16.3637H20V24.1H30.7636C30.3 26.6 28.8909 28.7182 26.7727 30.1364V35.1546H33.2364C37.0182 31.6728 39.2 26.5455 39.2 20.4546Z" fill="#4285F4"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M20 40C25.4 40 29.9273 38.2091 33.2364 35.1545L26.7727 30.1364C24.9818 31.3364 22.6909 32.0454 20 32.0454C14.7909 32.0454 10.3818 28.5273 8.80909 23.8H2.12727V28.9818C5.41818 35.5182 12.1818 40 20 40Z" fill="#34A853"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.80909 23.8C8.40909 22.6 8.18182 21.3182 8.18182 20C8.18182 18.6818 8.40909 17.4 8.80909 16.2V11.0182H2.12727C0.772727 13.7182 0 16.7727 0 20C0 23.2273 0.772727 26.2818 2.12727 28.9818L8.80909 23.8Z" fill="#FBBC05"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M20 7.95455C22.9364 7.95455 25.5727 8.96364 27.6455 10.9455L33.3818 5.20909C29.9182 1.98182 25.3909 0 20 0C12.1818 0 5.41818 4.48182 2.12727 11.0182L8.80909 16.2C10.3818 11.4727 14.7909 7.95455 20 7.95455Z" fill="#EA4335"/> -</g> -<defs> -<clipPath id="clip0_10_316"> -<rect width="40" height="40" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/frontend/resources/translations/am-ET.json b/frontend/resources/translations/am-ET.json index 9cebbf8241..b07649f0e4 100644 --- a/frontend/resources/translations/am-ET.json +++ b/frontend/resources/translations/am-ET.json @@ -358,7 +358,7 @@ "email": "ኢሜል", "tooltipSelectIcon": "አዶን ይምረጡ", "selectAnIcon": "አዶን ይምረጡ", - "pleaseInputYourOpenAIKey": "እባክዎን AI ቁልፍዎን ያስገቡ", + "pleaseInputYourOpenAIKey": "እባክዎን OpenAI ቁልፍዎን ያስገቡ", "pleaseInputYourStabilityAIKey": "እባክዎ Stability AI ቁልፍን ያስገቡ", "clickToLogout": "የአሁኑን ተጠቃሚ ለመግባት ጠቅ ያድርጉ" }, @@ -556,12 +556,12 @@ "referencedBoard": "ማጣቀሻ ቦርድ", "referencedGrid": "ማጣቀሻ ፍርግርግ", "referencedCalendar": "የቀን ቀን መቁጠሪያ", - "autoGeneratorMenuItemName": "AI ጸሐፊ", - "autoGeneratorTitleName": "AI ማንኛውንም ነገር እንዲጽፉ ይጠይቁ ...", + "autoGeneratorMenuItemName": "OpenAI ጸሐፊ", + "autoGeneratorTitleName": "OpenAI ማንኛውንም ነገር እንዲጽፉ ይጠይቁ ...", "autoGeneratorLearnMore": "ተጨማሪ እወቅ", "autoGeneratorGenerate": "ማመንጨት", - "autoGeneratorHintText": "AI ይጠይቁ ...", - "autoGeneratorCantGetOpenAIKey": "የ AI ቁልፍ ማግኘት አልተቻለም", + "autoGeneratorHintText": "OpenAI ይጠይቁ ...", + "autoGeneratorCantGetOpenAIKey": "የ OpenAI ቁልፍ ማግኘት አልተቻለም", "autoGeneratorRewrite": "እንደገና ይፃፉ", "smartEdit": "ረዳቶች", "openAI": "ኦፔና", @@ -572,7 +572,7 @@ "smartEditMakeLonger": "ረዘም ላለ ጊዜ ያድርጉ", "smartEditCouldNotFetchResult": "ከOpenAI ውጤት ማምለጥ አልተቻለም", "smartEditCouldNotFetchKey": "ኦፕናይ ቁልፍን ማጣት አልተቻለም", - "smartEditDisabled": "በቅንብሮች ውስጥ AI ያገናኙ", + "smartEditDisabled": "በቅንብሮች ውስጥ OpenAI ያገናኙ", "discardResponse": "የ AI ምላሾችን መጣል ይፈልጋሉ?", "createInlineMathEquation": "እኩልነት ይፍጠሩ", "toggleList": "የተስተካከለ ዝርዝር", @@ -657,8 +657,8 @@ "placeholder": "የምስል ዩአርኤል ያስገቡ" }, "ai": { - "label": "ምስል AI ውስጥ ምስልን ማመንጨት", - "placeholder": "ምስልን ለማመንጨት እባክዎን ለ AI ይጠይቁ" + "label": "ምስል OpenAI ውስጥ ምስልን ማመንጨት", + "placeholder": "ምስልን ለማመንጨት እባክዎን ለ OpenAI ይጠይቁ" }, "stability_ai": { "label": "ምስልን Stability AI ያመነጫል", diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index e8ca8c4ceb..136bfb3fa1 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "أنا", "welcomeText": "مرحبًا بك في @: appName", - "welcomeTo": "مرحبا بكم في", "githubStarText": "نجمة على GitHub", "subscribeNewsletterText": "اشترك في النشرة الإخبارية", "letsGoButtonText": "بداية سريعة", "title": "عنوان", "youCanAlso": "بامكانك ايضا", "and": "و", - "failedToOpenUrl": "فشل في فتح الرابط: {}", "blockActions": { "addBelowTooltip": "انقر للإضافة أدناه", "addAboveCmd": "Alt + انقر", @@ -36,39 +34,16 @@ "loginButtonText": "تسجيل الدخول", "loginStartWithAnonymous": "ابدأ بجلسة خفية", "continueAnonymousUser": "استمر بجلسة خفية", - "anonymous": "مجهول", "buttonText": "تسجيل الدخول", - "signingInText": "جاري تسجيل الدخول...", "forgotPassword": "هل نسيت كلمة السر؟", "emailHint": "بريد إلكتروني", "passwordHint": "كلمة المرور", "dontHaveAnAccount": "ليس لديك حساب؟", - "createAccount": "إنشاء حساب", "repeatPasswordEmptyError": "إعادة كلمة المرور لا يمكن أن تكون فارغة", "unmatchedPasswordError": "تكرار كلمة المرور ليس هو نفسه كلمة المرور", "syncPromptMessage": "قد تستغرق مزامنة البيانات بعض الوقت. من فضلك لا تغلق هذه الصفحة", "or": "أو", - "signInWithGoogle": "استكمال باستخدام Google", - "signInWithGithub": "استكمال باستخدام Github", - "signInWithDiscord": "استكمال باستخدام Discord", - "signInWithApple": "استكمال باستخدام Apple", - "continueAnotherWay": "استكمال بطريقة أخرى", - "signUpWithGoogle": "سجل باستخدام Google", - "signUpWithGithub": "سجل باستخدام Github", - "signUpWithDiscord": "سجل باستخدام Discord", "signInWith": "تسجيل الدخول ب:", - "signInWithEmail": "متابعة باستخدام البريد الإلكتروني", - "signInWithMagicLink": "استكمال", - "signUpWithMagicLink": "سجل باستخدام Magic Link", - "pleaseInputYourEmail": "الرجاء إدخال عنوان بريدك الإلكتروني", - "settings": "إعدادات", - "magicLinkSent": "تم إرسال Magic Link!", - "invalidEmail": "يرجى إدخال عنوان بريد إلكتروني صالح", - "alreadyHaveAnAccount": "هل لديك حساب؟", - "logIn": "تسجيل الدخول", - "generalError": "حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقًا", - "limitRateError": "لأسباب أمنية، يمكنك طلب Magic Link كل 60 ثانية فقط", - "magicLinkSentDescription": "تم إرسال Magic Link إلى بريدك الإلكتروني. انقر على الرابط لإكمال تسجيل الدخول. ستنتهي صلاحية الرابط بعد 5 دقائق.", "LogInWithGoogle": "تسجيل الدخول عبر جوجل", "LogInWithGithub": "تسجيل الدخول مع جيثب", "LogInWithDiscord": "تسجيل الدخول مع ديسكورد", @@ -76,68 +51,23 @@ }, "workspace": { "chooseWorkspace": "اختر مساحة العمل الخاصة بك", - "defaultName": "مساحة العمل الخاصة بي", "create": "قم بإنشاء مساحة عمل", - "new": "مساحة عمل جديدة", - "importFromNotion": "الاستيراد من Notion", - "learnMore": "تعلم المزيد", "reset": "إعادة تعيين مساحة العمل", - "renameWorkspace": "إعادة تسمية مساحة العمل", - "workspaceNameCannotBeEmpty": "لا يمكن أن يكون اسم مساحة العمل فارغًا", "resetWorkspacePrompt": "ستؤدي إعادة تعيين مساحة العمل إلى حذف جميع الصفحات والبيانات الموجودة بداخلها. هل أنت متأكد أنك تريد إعادة تعيين مساحة العمل؟ وبدلاً من ذلك، يمكنك الاتصال بفريق الدعم لاستعادة مساحة العمل", "hint": "مساحة العمل", "notFoundError": "مساحة العمل غير موجودة", "failedToLoad": "هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ @:appName وحاول مرة أخرى.", "errorActions": { "reportIssue": "بلغ عن خطأ", - "reportIssueOnGithub": "الإبلاغ عن مشكلة على Github", - "exportLogFiles": "تصدير ملفات السجل", "reachOut": "تواصل مع ديسكورد" - }, - "menuTitle": "مساحات العمل", - "deleteWorkspaceHintText": " هل أنت متأكد من أنك تريد حذف مساحة العمل؟ لا يمكن التراجع عن هذا الإجراء، وستختفي أي صفحات قمت بنشرها سابقاً.", - "createSuccess": "تم إنشاء مساحة العمل بنجاح", - "createFailed": "فشل في إنشاء مساحة العمل", - "createLimitExceeded": "لقد وصلت إلى الحد الأقصى لمساحة العمل المسموح بها لحسابك. إذا كنت بحاجة إلى مساحات عمل إضافية لمواصلة عملك، فيرجى تقديم طلب على Github", - "deleteSuccess": "تم حذف مساحة العمل بنجاح", - "deleteFailed": "فشل في حذف مساحة العمل", - "openSuccess": "تم فتح مساحة العمل بنجاح", - "openFailed": "فشل في فتح مساحة العمل", - "renameSuccess": "تم إعادة تسمية مساحة العمل بنجاح", - "renameFailed": "فشل في إعادة تسمية مساحة العمل", - "updateIconSuccess": "تم تحديث أيقونة مساحة العمل بنجاح", - "updateIconFailed": "فشل تحديث أيقونة مساحة العمل", - "cannotDeleteTheOnlyWorkspace": "لا يمكن حذف مساحة العمل الوحيدة", - "fetchWorkspacesFailed": "فشل في الوصول لمساحات العمل", - "leaveCurrentWorkspace": "مغادرة مساحة العمل", - "leaveCurrentWorkspacePrompt": "هل أنت متأكد أنك تريد مغادرة مساحة العمل الحالية؟" + } }, "shareAction": { "buttonText": "مشاركه", "workInProgress": "قريباً", "markdown": "Markdown", - "html": "HTML", - "clipboard": "نسخ إلى الحافظة", "csv": "CSV", - "copyLink": "نسخ الرابط", - "publishToTheWeb": "نشر على الويب", - "publishToTheWebHint": "إنشاء موقع ويب مع AppFlowy", - "publish": "نشر", - "unPublish": "التراجع عن النشر", - "visitSite": "زيارة الموقع", - "exportAsTab": "تصدير كـ", - "publishTab": "نشر", - "shareTab": "مشاركة", - "publishOnAppFlowy": "نشر على AppFlowy", - "shareTabTitle": "دعوة للتعاون", - "shareTabDescription": "من أجل التعاون السهل مع أي شخص", - "copyLinkSuccess": "تم نسخ الرابط إلى الحافظة", - "copyShareLink": "نسخ رابط المشاركة", - "copyLinkFailed": "فشل في نسخ الرابط إلى الحافظة", - "copyLinkToBlockSuccess": "تم نسخ رابط الكتلة إلى الحافظة", - "copyLinkToBlockFailed": "فشل في نسخ رابط الكتلة إلى الحافظة", - "manageAllSites": "إدارة كافة المواقع", - "updatePathName": "تحديث اسم المسار" + "copyLink": "نسخ الرابط" }, "moreAction": { "small": "صغير", @@ -145,36 +75,15 @@ "large": "كبير", "fontSize": "حجم الخط", "import": "استيراد", - "moreOptions": "المزيد من الخيارات", - "wordCount": "عدد الكلمات: {}", - "charCount": "عدد الأحرف: {}", - "createdAt": "منشأ: {}", - "deleteView": "يمسح", - "duplicateView": "تكرار", - "wordCountLabel": "عدد الكلمات: ", - "charCountLabel": "عدد الأحرف: ", - "createdAtLabel": "تم إنشاؤه: ", - "syncedAtLabel": "تم المزامنة: ", - "saveAsNewPage": "حفظ الرسائل في الصفحة", - "saveAsNewPageDisabled": "لا توجد رسائل متاحة" + "moreOptions": "المزيد من الخيارات" }, "importPanel": { "textAndMarkdown": "نص و Markdown", "documentFromV010": "مستند من الإصدار 0.1.0", "databaseFromV010": "قاعدة بيانات من الإصدار 0.1.0", - "notionZip": "ملف Zip المُصدَّر من Notion", "csv": "CSV", "database": "قاعدة البيانات" }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "اسحب وأفلِت ملفًا، وانقر فوقه ", - "placeholderUpload": "رفع", - "placeholderRight": "أو قم بلصق رابط الصورة.", - "dropToUpload": "إفلات ملف لتحميله", - "change": "تغير" - } - }, "disclosureAction": { "rename": "إعادة تسمية", "delete": "يمسح", @@ -184,12 +93,7 @@ "openNewTab": "افتح في علامة تبويب جديدة", "moveTo": "نقل إلى", "addToFavorites": "اضافة الى المفضلة", - "copyLink": "نسخ الرابط", - "changeIcon": "تغيير الأيقونة", - "collapseAllPages": "طي جميع الصفحات الفرعية", - "movePageTo": "تحريك الصفحة إلى", - "move": "تحريك", - "lockPage": "إلغاء تأمين الصفحة" + "copyLink": "نسخ الرابط" }, "blankPageTitle": "صفحة فارغة", "newPageText": "صفحة جديدة", @@ -197,83 +101,9 @@ "newGridText": "شبكة جديدة", "newCalendarText": "تقويم جديد", "newBoardText": "سبورة جديدة", - "chat": { - "newChat": "الدردشة بالذكاء الاصطناعي", - "inputMessageHint": "اسأل @:appName AI", - "inputLocalAIMessageHint": "اسأل @:appName Local AI", - "unsupportedCloudPrompt": "هذه الميزة متوفرة فقط عند استخدام @:appName Cloud", - "relatedQuestion": "ذات صلة", - "serverUnavailable": "الخدمة غير متاحة مؤقتًا. يرجى المحاولة مرة أخرى لاحقًا.", - "aiServerUnavailable": "تم فقد الاتصال. يرجى التحقق من اتصالك بالإنترنت", - "retry": "إعادة المحاولة", - "clickToRetry": "انقر لإعادة المحاولة", - "regenerateAnswer": "إعادة توليد", - "question1": "كيفية استخدام كانبان لإدارة المهام", - "question2": "شرح طريقة GTD", - "question3": "لماذا تستخدم Rust", - "question4": "وصفة بما في مطبخي", - "question5": "إنشاء رسم توضيحي لصفحتي", - "question6": "قم بإعداد قائمة بالمهام التي يجب القيام بها خلال الأسبوع القادم", - "aiMistakePrompt": "الذكاء الاصطناعي قد يرتكب أخطاء. تحقق من المعلومات المهمة.", - "chatWithFilePrompt": "هل تريد الدردشة مع الملف؟", - "indexFileSuccess": "تم فهرسة الملف بنجاح", - "inputActionNoPages": "لا توجد نتائج للصفحة", - "referenceSource": { - "zero": "تم العثور على 0 من مصادر", - "one": "تم العثور على {count} مصدر", - "other": "تم العثور على {count} مصادر" - }, - "clickToMention": "اذكر صفحة", - "uploadFile": "إرفاق ملفات PDF أو ملفات نصية أو ملفات Markdown", - "questionDetail": "مرحبًا {}! كيف يمكنني مساعدتك اليوم؟", - "indexingFile": "الفهرسة {}", - "generatingResponse": "توليد الاستجابة", - "selectSources": "اختر المصادر", - "currentPage": "الصفحة الحالية", - "sourcesLimitReached": "يمكنك فقط تحديد ما يصل إلى 3 مستندات من المستوى العلوي ومستنداتها الفرعية", - "sourceUnsupported": "نحن لا ندعم الدردشة مع قواعد البيانات في الوقت الحالي", - "regenerate": "حاول ثانية", - "addToPageButton": "أضف إلى الصفحة", - "addToPageTitle": "أضف رسالة إلى...", - "addToNewPage": "أضف إلى صفحة جديدة", - "addToNewPageName": "الرسائل المستخرجة من \"{}\"", - "addToNewPageSuccessToast": "تمت إضافة الرسالة إلى", - "openPagePreviewFailedToast": "فشل في فتح الصفحة", - "changeFormat": { - "actionButton": "تغيير التنسيق", - "confirmButton": "إعادة توليد مع هذا التنسيق", - "textOnly": "النص", - "imageOnly": "الصورة فقط", - "textAndImage": "النص والصورة", - "text": "الفقرة", - "bullet": "قائمة نقطية", - "number": "قائمة مرقمة", - "table": "الجدول", - "blankDescription": "تنسيق الاستجابة", - "defaultDescription": "الوضع التلقائي", - "textWithImageDescription": "@:chat.changeFormat.text مع الصورة", - "numberWithImageDescription": "@:chat.changeFormat.number مع الصورة", - "bulletWithImageDescription": "@:chat.changeFormat.bullet مع الصورة", - "tableWithImageDescription": "@:chat.changeFormat.table مع الصورة" - }, - "switchModel": { - "label": "تبديل النموذج", - "localModel": "النموذج المحلي", - "cloudModel": "نموذج السحابة", - "autoModel": "آلي" - }, - "selectBanner": { - "saveButton": "أضف إلى...", - "selectMessages": "حدد الرسائل", - "nSelected": "{} تم التحديد", - "allSelected": "جميعها محددة" - }, - "stopTooltip": "توقف عن التوليد" - }, "trash": { "text": "المهملات", "restoreAll": "استعادة الكل", - "restore": "إسترجاع", "deleteAll": "حذف الكل", "pageHeader": { "fileName": "اسم الملف", @@ -288,46 +118,37 @@ "title": "هل أنت متأكد من استعادة جميع الصفحات في المهملات؟", "caption": "لا يمكن التراجع عن هذا الإجراء." }, - "restorePage": { - "title": "إسترجاع: {}", - "caption": "هل أنت متأكد أنك تريد استعادة هذه الصفحة؟" - }, "mobile": { "actions": "إجراءات سلة المهملات", "empty": "سلة المهملات فارغة", "emptyDescription": "ليس لديك أي ملفات محذوفة", "isDeleted": "محذوف", "isRestored": "تمت استعادته" - }, - "confirmDeleteTitle": "هل أنت متأكد أنك تريد حذف هذه الصفحة نهائياً؟" + } }, "deletePagePrompt": { "text": "هذه الصفحة في المهملات", "restore": "استعادة الصفحة", - "deletePermanent": "الحذف بشكل نهائي", - "deletePermanentDescription": "هل أنت متأكد أنك تريد حذف هذه الصفحة بشكل دائم؟ هذا لا يمكن التراجع عنه." + "deletePermanent": "الحذف بشكل نهائي" }, "dialogCreatePageNameHint": "اسم الصفحة", "questionBubble": { "shortcuts": "الاختصارات", "whatsNew": "ما هو الجديد؟", - "helpAndDocumentation": "المساعدة والتوثيق", - "getSupport": "احصل على الدعم", + "help": "المساعدة والدعم", "markdown": "Markdown", "debug": { "name": "معلومات التصحيح", "success": "تم نسخ معلومات التصحيح إلى الحافظة!", "fail": "تعذر نسخ معلومات التصحيح إلى الحافظة" }, - "feedback": "تعليق", - "help": "المساعدة والدعم" + "feedback": "تعليق" }, "menuAppHeader": { "moreButtonToolTip": "إزالة وإعادة تسمية والمزيد...", "addPageTooltip": "أضف صفحة في الداخل بسرعة", "defaultNewPageName": "بدون عنوان", - "renameDialog": "إعادة تسمية", - "pageNameSuffix": "نسخ" + "renameDialog": "إعادة تسمية" }, "noPagesInside": "لا توجد صفحات في الداخل", "toolbar": { @@ -357,61 +178,17 @@ "dragRow": "اضغط مطولاً لإعادة ترتيب الصف", "viewDataBase": "عرض قاعدة البيانات", "referencePage": "تمت الإشارة إلى هذا {name}", - "addBlockBelow": "إضافة كتلة أدناه", - "aiGenerate": "توليد" + "addBlockBelow": "إضافة كتلة أدناه" }, "sideBar": { "closeSidebar": "إغلاق الشريط الجانبي", "openSidebar": "فتح الشريط الجانبي", - "expandSidebar": "توسيع كصفحة كاملة", "personal": "شخصي", - "private": "خاص", - "workspace": "مساحة العمل", "favorites": "المفضلة", - "clickToHidePrivate": "انقر لإخفاء المساحة الخاصة\nالصفحات التي قمت بإنشائها هنا مرئية لك فقط", - "clickToHideWorkspace": "انقر لإخفاء مساحة العمل\nالصفحات التي قمت بإنشائها هنا تكون مرئية لكل الأعضاء", "clickToHidePersonal": "انقر لإخفاء القسم الشخصي", "clickToHideFavorites": "انقر لإخفاء القسم المفضل", "addAPage": "أضف صفحة", - "addAPageToPrivate": "أضف صفحة إلى مساحة خاصة", - "addAPageToWorkspace": "إضافة صفحة إلى مساحة العمل", - "recent": "مؤخرًا", - "today": "اليوم", - "thisWeek": "هذا الأسبوع", - "others": "المفضلات السابقة", - "earlier": "سابقًا", - "justNow": "الآن", - "minutesAgo": "منذ {count} دقيقة", - "lastViewed": "آخر مشاهدة", - "favoriteAt": "المفضلة", - "emptyRecent": "لا توجد صفحات حديثة", - "emptyRecentDescription": "عند عرض الصفحات، ستظهر هنا لسهولة استرجاعها.", - "emptyFavorite": "لا توجد صفحات مفضلة", - "emptyFavoriteDescription": "قم بوضع علامة على الصفحات كمفضلة - سيتم إدراجها هنا للوصول السريع!", - "removePageFromRecent": "هل تريد إزالة هذه الصفحة من القائمة الأخيرة؟", - "removeSuccess": "تمت الإزالة بنجاح", - "favoriteSpace": "المفضلة", - "RecentSpace": "مؤخرًا", - "Spaces": "المساحات", - "upgradeToPro": "الترقية إلى الإصدار الاحترافي", - "upgradeToAIMax": "إلغاء تأمين الذكاء الاصطناعي غير المحدود", - "storageLimitDialogTitle": "لقد نفدت مساحة التخزين المجانية لديك. قم بالترقية لإلغاء تأمين مساحة تخزين غير محدودة", - "storageLimitDialogTitleIOS": "لقد نفدت مساحة التخزين المجانية.", - "aiResponseLimitTitle": "لقد نفدت منك استجابات الذكاء الاصطناعي المجانية. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "aiResponseLimitDialogTitle": "تم الوصول إلى الحد الأقصى لاستجابات الذكاء الاصطناعي", - "aiResponseLimit": "لقد نفدت استجابات الذكاء الاصطناعي المجانية.<inlang-LineFeed>\nانتقل إلى الإعدادات -> الخطة -> انقر فوق AI Max أو Pro Plan للحصول على المزيد من استجابات الذكاء الاصطناعي", - "askOwnerToUpgradeToPro": "مساحة العمل الخاصة بك نفدت من مساحة التخزين المجانية. يرجى مطالبة مالك مساحة العمل الخاصة بك بالترقية إلى الخطة الاحترافية", - "askOwnerToUpgradeToProIOS": "مساحة العمل الخاصة بك على وشك النفاد من مساحة التخزين المجانية.", - "askOwnerToUpgradeToAIMax": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بترقية الخطة أو شراء إضافات الذكاء الاصطناعي", - "askOwnerToUpgradeToAIMaxIOS": "مساحة العمل الخاصة بك تفتقر إلى الاستجابات المجانية للذكاء الاصطناعي.", - "purchaseAIMax": "لقد نفدت استجابات الصور بالذكاء الاصطناعي من مساحة العمل الخاصة بك. يرجى مطالبة مالك مساحة العمل الخاصة بك بشراء AI Max", - "aiImageResponseLimit": "لقد نفدت استجابات الصور الخاصة بالذكاء الاصطناعي.<inlang-LineFeed>\nانتقل إلى الإعدادات -> الخطة -> انقر فوق AI Max للحصول على المزيد من استجابات صور AI", - "purchaseStorageSpace": "شراء مساحة تخزين", - "singleFileProPlanLimitationDescription": "لقد تجاوزت الحد الأقصى لحجم تحميل الملف المسموح به في الخطة المجانية. يرجى الترقية إلى الخطة الاحترافية لتحميل ملفات أكبر حجمًا", - "purchaseAIResponse": "شراء", - "askOwnerToUpgradeToLocalAI": "اطلب من مالك مساحة العمل تمكين الذكاء الاصطناعي على الجهاز", - "upgradeToAILocal": "قم بتشغيل النماذج المحلية على جهازك لتحقيق أقصى قدر من الخصوصية", - "upgradeToAILocalDesc": "الدردشة باستخدام ملفات PDF، وتحسين كتابتك، وملء الجداول تلقائيًا باستخدام الذكاء الاصطناعي المحلي" + "recent": "مؤخرًا" }, "notifications": { "export": { @@ -427,7 +204,6 @@ }, "button": { "ok": "حسنا", - "confirm": "تأكيد", "done": "منتهي", "cancel": "الغاء", "signIn": "تسجيل الدخول", @@ -445,48 +221,17 @@ "upload": "رفع", "edit": "يحرر", "delete": "يمسح", - "copy": "النسخ", "duplicate": "ينسخ", "putback": "ضعها بالخلف", "update": "تحديث", "share": "مشاركة", "removeFromFavorites": "إزالة من المفضلة", - "removeFromRecent": "إزالة من الأحدث", "addToFavorites": "اضافة الى المفضلة", - "favoriteSuccessfully": "النجاح المفضل", - "unfavoriteSuccessfully": "النجاح غير المفضل", - "duplicateSuccessfully": "تم التكرار بنجاح", "rename": "إعادة تسمية", "helpCenter": "مركز المساعدة", "add": "اضافة", "yes": "نعم", - "no": "لا", - "clear": "مسح", - "remove": "حذف", - "dontRemove": "لا تقم بالإزالة", - "copyLink": "نسخ الرابط", - "align": "المحاذاة", - "login": "تسجيل الدخول", - "logout": "تسجيل الخروج", - "deleteAccount": "حذف الحساب", "back": "خلف", - "signInGoogle": "متابعة مع جوجل", - "signInGithub": "متابعة مع GitHub", - "signInDiscord": "متابعة مع Discord", - "more": "أكثر", - "create": "إنشاء", - "close": "إغلاق", - "next": "التالي", - "previous": "السابق", - "submit": "إرسال", - "download": "تحميل", - "backToHome": "العودة إلى الصفحة الرئيسية", - "viewing": "عرض", - "editing": "تحرير", - "gotIt": "فهمتها", - "retry": "إعادة المحاولة", - "uploadFailed": "فشل الرفع.", - "copyLinkOriginal": "نسخ الرابط إلى الأصل", "tryAGain": "حاول ثانية" }, "label": { @@ -511,613 +256,6 @@ }, "settings": { "title": "إعدادات", - "popupMenuItem": { - "settings": "إعدادات", - "members": "الأعضاء", - "trash": "سلة المحذوفات", - "helpAndDocumentation": "المساعدة والتوثيق", - "getSupport": "احصل على الدعم", - "helpAndSupport": "المساعدة والدعم" - }, - "sites": { - "title": "المواقع", - "namespaceTitle": "مساحة الاسم", - "namespaceDescription": "إدارة مساحة الاسم والصفحة الرئيسية الخاصة بك", - "namespaceHeader": "مساحة الاسم", - "homepageHeader": "الصفحة الرئيسية", - "updateNamespace": "تحديث مساحة الاسم", - "removeHomepage": "إزالة الصفحة الرئيسية", - "selectHomePage": "حدد الصفحة", - "clearHomePage": "مسح الصفحة الرئيسية لهذه المساحة الاسمية", - "customUrl": "عنوان URL مخصص", - "namespace": { - "description": "سيتم تطبيق هذا التغيير على جميع الصفحات المنشورة الموجودة على مساحة الاسم هذه بشكل فوري", - "tooltip": "نحن نحتفظ بالحق في إزالة أي مساحات أسماء غير مناسبة", - "updateExistingNamespace": "تحديث مساحة الاسم الحالية", - "upgradeToPro": "قم بالترقية إلى الخطة الاحترافية لتعيين الصفحة الرئيسية", - "redirectToPayment": "إعادة التوجيه إلى صفحة الدفع...", - "onlyWorkspaceOwnerCanSetHomePage": "يمكن فقط لمالك مساحة العمل تعيين الصفحة الرئيسية", - "pleaseAskOwnerToSetHomePage": "يرجى طلب الترقية إلى الخطة الاحترافية من مالك مساحة العمل" - }, - "publishedPage": { - "title": "جميع الصفحات المنشورة", - "description": "إدارة صفحاتك المنشورة", - "page": "صفحة", - "pathName": "اسم المسار", - "date": "تاريخ النشر", - "emptyHinText": "ليس لديك صفحات منشورة في مساحة العمل هذه", - "noPublishedPages": "لا توجد صفحات منشورة", - "settings": "نشر الإعدادات", - "clickToOpenPageInApp": "فتح الصفحة في التطبيق", - "clickToOpenPageInBrowser": "فتح الصفحة في المتصفح" - }, - "error": { - "failedToGeneratePaymentLink": "فشل في إنشاء رابط الدفع لخطة الاحترافية", - "failedToUpdateNamespace": "فشل في تحديث مساحة الاسم", - "proPlanLimitation": "يجب عليك الترقية إلى الخطة الاحترافية لتحديث مساحة الاسم", - "namespaceAlreadyInUse": "تم أخذ مساحة الاسم بالفعل، يرجى تجربة مساحة اسم أخرى", - "invalidNamespace": "مساحة اسم غير صالحة، يرجى تجربة مساحة اسم أخرى", - "namespaceLengthAtLeast2Characters": "يجب أن تكون مساحة الاسم بطول حرفين على الأقل", - "onlyWorkspaceOwnerCanUpdateNamespace": "يمكن فقط لمالك مساحة العمل تحديث مساحة الاسم", - "onlyWorkspaceOwnerCanRemoveHomepage": "لا يمكن إلا لمالك مساحة العمل إزالة الصفحة الرئيسية", - "setHomepageFailed": "فشل في تعيين الصفحة الرئيسية", - "namespaceTooLong": "مساحة الاسم طويلة جدًا، يرجى تجربة مساحة اسم أخرى", - "namespaceTooShort": "مساحة الاسم قصيرة جدًا، يرجى تجربة مساحة اسم أخرى", - "namespaceIsReserved": "تم حجز مساحة الاسم، يرجى تجربة مساحة اسم أخرى", - "updatePathNameFailed": "فشل في تحديث اسم المسار", - "removeHomePageFailed": "فشل في إزالة الصفحة الرئيسية", - "publishNameContainsInvalidCharacters": "يحتوي اسم المسار على أحرف غير صالحة، يرجى تجربة اسم مسار آخر", - "publishNameTooShort": "اسم المسار قصير جدًا، يرجى تجربة اسم مسار آخر", - "publishNameTooLong": "اسم المسار طويل جدًا، يرجى تجربة اسم مسار آخر", - "publishNameAlreadyInUse": "اسم المسار قيد الاستخدام بالفعل، يرجى تجربة اسم مسار آخر", - "namespaceContainsInvalidCharacters": "تحتوي مساحة الاسم على أحرف غير صالحة، يرجى تجربة مساحة اسم أخرى", - "publishPermissionDenied": "يمكن فقط لمالك مساحة العمل أو ناشر الصفحة إدارة إعدادات النشر", - "publishNameCannotBeEmpty": "لا يمكن أن يكون اسم المسار فارغًا، يرجى تجربة اسم مسار آخر" - }, - "success": { - "namespaceUpdated": "تم تحديث مساحة الاسم بنجاح", - "setHomepageSuccess": "تم تعيين الصفحة الرئيسية بنجاح", - "updatePathNameSuccess": "تم تحديث اسم المسار بنجاح", - "removeHomePageSuccess": "تم إزالة الصفحة الرئيسية بنجاح" - } - }, - "accountPage": { - "menuLabel": "حسابي", - "title": "حسابي", - "general": { - "title": "اسم الحساب وصورة الملف الشخصي", - "changeProfilePicture": "تغيير صورة الملف الشخصي" - }, - "email": { - "title": "بريد إلكتروني", - "actions": { - "change": "تغيير البريد الإلكتروني" - } - }, - "login": { - "title": "تسجيل الدخول إلى الحساب", - "loginLabel": "تسجيل الدخول", - "logoutLabel": "تسجيل الخروج" - }, - "isUpToDate": "تم تحديث @:appName !", - "officialVersion": "الإصدار {version} (الإصدار الرسمي)" - }, - "workspacePage": { - "menuLabel": "مساحة العمل", - "title": "مساحة العمل", - "description": "قم بتخصيص مظهر مساحة العمل الخاصة بك، والسمة، والخط، وتخطيط النص، وتنسيق التاريخ/الوقت، واللغة.", - "workspaceName": { - "title": "اسم مساحة العمل" - }, - "workspaceIcon": { - "title": "أيقونة مساحة العمل", - "description": "قم بتحميل صورة أو استخدم رمزًا تعبيريًا لمساحة عملك. ستظهر الأيقونة في الشريط الجانبي والإشعارات." - }, - "appearance": { - "title": "المظهر", - "description": "قم بتخصيص مظهر مساحة العمل الخاصة بك والموضوع والخط وتخطيط النص والتاريخ والوقت واللغة.", - "options": { - "system": "آلي", - "light": "فاتح", - "dark": "داكن" - } - }, - "resetCursorColor": { - "title": "إعادة تعيين لون مؤشر المستند", - "description": "هل أنت متأكد أنك تريد إعادة تعيين لون المؤشر؟" - }, - "resetSelectionColor": { - "title": "إعادة تعيين لون اختيار المستند", - "description": "هل أنت متأكد أنك تريد إعادة تعيين لون الإختيار؟" - }, - "resetWidth": { - "resetSuccess": "تم إعادة تعيين عرض المستند بنجاح" - }, - "theme": { - "title": "السمة", - "description": "حدد السمة المحددة مسبقًا، أو قم برفع موضوعك المخصص.", - "uploadCustomThemeTooltip": "رفع السمة المخصصة" - }, - "workspaceFont": { - "title": "خط مساحة العمل", - "noFontHint": "لم يتم العثور على الخط، حاول مصطلحًا آخر." - }, - "textDirection": { - "title": "اتجاه النص", - "leftToRight": "من اليسار إلى اليمين", - "rightToLeft": "من اليمين إلى اليسار", - "auto": "آلي", - "enableRTLItems": "تمكين عناصر شريط أدوات RTL" - }, - "layoutDirection": { - "title": "اتجاه التخطيط", - "leftToRight": "من اليسار إلى اليمين", - "rightToLeft": "من اليمين إلى اليسار" - }, - "dateTime": { - "title": "التاريخ والوقت", - "example": "{} في {} ({})", - "24HourTime": "الوقت بنظام 24 ساعة", - "dateFormat": { - "label": "تنسيق التاريخ", - "local": "محلي", - "us": "US أميركي", - "iso": "ايزو", - "friendly": "ودي", - "dmy": "يوم/شهر/سنة" - } - }, - "language": { - "title": "اللغة" - }, - "deleteWorkspacePrompt": { - "title": "حذف مساحة العمل", - "content": "هل أنت متأكد من أنك تريد حذف مساحة العمل هذه؟ لا يمكن التراجع عن هذا الإجراء، وسيتم إلغاء نشر أي صفحات قمت بنشرها." - }, - "leaveWorkspacePrompt": { - "title": "مغادرة مساحة العمل", - "content": "هل أنت متأكد من أنك تريد مغادرة مساحة العمل هذه؟ ستفقد إمكانية الوصول إلى جميع الصفحات والبيانات الموجودة فيها.", - "success": "لقد غادرت مساحة العمل بنجاح.", - "fail": "فشل في مغادرة مساحة العمل." - }, - "manageWorkspace": { - "title": "إدارة مساحة العمل", - "leaveWorkspace": "مغادرة مساحة العمل", - "deleteWorkspace": "حذف مساحة العمل" - } - }, - "manageDataPage": { - "menuLabel": "إدارة البيانات", - "title": "إدارة البيانات", - "description": "إدارة تخزين البيانات محليًا أو استيراد البيانات الموجودة لديك إلى @:appName .", - "dataStorage": { - "title": "موقع تخزين الملفات", - "tooltip": "الموقع الذي يتم تخزين ملفاتك فيه", - "actions": { - "change": "تغيير المسار", - "open": "افتح المجلد", - "openTooltip": "فتح موقع مجلد البيانات الحالي", - "copy": "نسخ المسار", - "copiedHint": "تم نسخ المسار!", - "resetTooltip": "إعادة التعيين إلى الموقع الافتراضي" - }, - "resetDialog": { - "title": "هل أنت متأكد؟", - "description": "لن يؤدي إعادة تعيين المسار إلى موقع البيانات الافتراضي إلى حذف بياناتك. إذا كنت تريد إعادة استيراد بياناتك الحالية، فيجب عليك نسخ مسار موقعك الحالي أولاً." - } - }, - "importData": { - "title": "استيراد البيانات", - "tooltip": "استيراد البيانات من مجلدات النسخ الاحتياطية/البيانات @:appName", - "description": "نسخ البيانات من مجلد بيانات خارجي @:appName", - "action": "تصفح الملف" - }, - "encryption": { - "title": "التشفير", - "tooltip": "إدارة كيفية تخزين بياناتك وتشفيرها", - "descriptionNoEncryption": "سيؤدي تشغيل التشفير إلى تشفير كافة البيانات. ولا يمكن التراجع عن هذا الإجراء.", - "descriptionEncrypted": "بياناتك مشفرة.", - "action": "تشفير البيانات", - "dialog": { - "title": "هل تريد تشفير كافة بياناتك؟", - "description": "سيؤدي تشفير جميع بياناتك إلى الحفاظ على بياناتك آمنة. لا يمكن التراجع عن هذا الإجراء. هل أنت متأكد من أنك تريد المتابعة؟" - } - }, - "cache": { - "title": "مسح ذاكرة التخزين المؤقت", - "description": "ساعد في حل مشكلات مثل عدم تحميل الصور، أو عدم ظهور الصفحات في مساحة، أو عدم تحميل الخطوط. لن يؤثر هذا على بياناتك.", - "dialog": { - "title": "مسح ذاكرة التخزين المؤقت", - "description": "ساعد في حل مشكلات مثل عدم تحميل الصور، أو عدم ظهور الصفحات في مساحة، أو عدم تحميل الخطوط. لن يؤثر هذا على بياناتك.", - "successHint": "تم مسح ذاكرة التخزين المؤقت!" - } - }, - "data": { - "fixYourData": "إصلاح بياناتك", - "fixButton": "إصلاح", - "fixYourDataDescription": "إذا كنت تواجه مشكلات مع بياناتك، فيمكنك محاولة إصلاحها هنا." - } - }, - "shortcutsPage": { - "menuLabel": "اختصارات", - "title": "اختصارات", - "editBindingHint": "إدخال ربط جديد", - "searchHint": "بحث", - "actions": { - "resetDefault": "إعادة ضبط الافتراضي" - }, - "errorPage": { - "message": "فشل تحميل الاختصارات: {}", - "howToFix": "يرجى المحاولة مرة أخرى، إذا استمرت المشكلة، فيرجى التواصل معنا عبر GitHub." - }, - "resetDialog": { - "title": "إعادة ضبط الاختصارات", - "description": "سيؤدي هذا إلى إعادة ضبط كافة ارتباطات المفاتيح الخاصة بك إلى الوضع الافتراضي، ولا يمكنك التراجع عن هذا لاحقًا، هل أنت متأكد من أنك تريد المتابعة؟", - "buttonLabel": "إعادة ضبط" - }, - "conflictDialog": { - "title": "{} قيد الاستخدام حاليًا", - "descriptionPrefix": "يتم استخدام ارتباطات المفاتيح هذه حاليًا بواسطة ", - "descriptionSuffix": "إذا قمت باستبدال ارتباط المفتاح هذا، فسيتم إزالته من {}.", - "confirmLabel": "متابعة" - }, - "editTooltip": "اضغط لبدء تحرير ربط المفتاح", - "keybindings": { - "toggleToDoList": "التبديل إلى قائمة المهام", - "insertNewParagraphInCodeblock": "إدراج فقرة جديدة", - "pasteInCodeblock": "لصق في كتلة الكود", - "selectAllCodeblock": "حدد الكل", - "indentLineCodeblock": "أدخل فراغين فاصلين في بداية السطر", - "outdentLineCodeblock": "حذف فراغين فاصلين في بداية السطر", - "twoSpacesCursorCodeblock": "إدراج فراغين فاصلين عند المؤشر", - "copy": "اختيار النسخ", - "paste": "لصق في المحتوى", - "cut": "اختيار القص", - "alignLeft": "محاذاة النص إلى اليسار", - "alignCenter": "محاذاة النص إلى الوسط", - "alignRight": "محاذاة النص إلى اليمين", - "insertInlineMathEquation": "إدراج معادلة رياضية مضمنة", - "undo": "التراجع", - "redo": "الإعادة", - "convertToParagraph": "تحويل الكتلة إلى فقرة", - "backspace": "الحذف", - "deleteLeftWord": "حذف الكلمة اليسرى", - "deleteLeftSentence": "حذف الجملة اليسرى", - "delete": "حذف الحرف الأيمن", - "deleteMacOS": "حذف الحرف الأيسر", - "deleteRightWord": "حذف الكلمة اليمنى", - "moveCursorLeft": "حرك المؤشر إلى اليسار", - "moveCursorBeginning": "حرك المؤشر إلى البداية", - "moveCursorLeftWord": "حرك المؤشر إلى اليسار كلمة واحدة", - "moveCursorLeftSelect": "حدد وحرك المؤشر إلى اليسار", - "moveCursorBeginSelect": "حدد وحرك المؤشر إلى البداية", - "moveCursorLeftWordSelect": "حدد وحرك المؤشر إلى اليسار كلمة واحدة", - "moveCursorRight": "حرك المؤشر إلى اليمين", - "moveCursorEnd": "حرك المؤشر إلى النهاية", - "moveCursorRightWord": "حرك المؤشر إلى اليمين كلمة واحدة", - "moveCursorRightSelect": "حدد المؤشر وحركه إلى اليمين", - "moveCursorEndSelect": "حدد وحرك المؤشر إلى النهاية", - "moveCursorRightWordSelect": "حدد وحرك المؤشر إلى اليمين كلمة واحدة", - "moveCursorUp": "حرك المؤشر لأعلى", - "moveCursorTopSelect": "حدد وحرك المؤشر إلى الأعلى", - "moveCursorTop": "حرك المؤشر إلى الأعلى", - "moveCursorUpSelect": "حدد وحرك المؤشر لأعلى", - "moveCursorBottomSelect": "حدد وحرك المؤشر إلى الأسفل", - "moveCursorBottom": "حرك المؤشر إلى الأسفل", - "moveCursorDown": "حرك المؤشر إلى الأسفل", - "moveCursorDownSelect": "حدد وحرك المؤشر لأسفل", - "home": "تمرير إلى الأعلى", - "end": "تمرير إلى الأسفل", - "toggleBold": "تبديل الخط الغامق", - "toggleItalic": "تبديل الخط المائل", - "toggleUnderline": "تبديل التسطير", - "toggleStrikethrough": "تبديل الشطب", - "toggleCode": "تبديل الكود المضمن", - "toggleHighlight": "تبديل التمييز", - "showLinkMenu": "عرض قائمة الروابط", - "openInlineLink": "افتح الرابط المضمن", - "openLinks": "فتح جميع الروابط المحددة", - "indent": "المسافة البادئة", - "outdent": "مسافة بادئة سلبية", - "exit": "الخروج من التحرير", - "pageUp": "قم بالتمرير صفحة واحدة لأعلى", - "pageDown": "قم بالتمرير صفحة واحدة لأسفل", - "selectAll": "حدد الكل", - "pasteWithoutFormatting": "لصق المحتوى بدون تنسيق", - "showEmojiPicker": "عرض أداة اختيار الرموز التعبيرية", - "enterInTableCell": "إضافة فاصل الأسطر في الجدول", - "leftInTableCell": "حرك خلية واحدة إلى اليسار في الجدول", - "rightInTableCell": "حرك خلية واحدة إلى اليمين في الجدول", - "upInTableCell": "التحرك لأعلى خلية واحدة في الجدول", - "downInTableCell": "التحرك لأسفل خلية واحدة في الجدول", - "tabInTableCell": "انتقل إلى الخلية التالية المتوفرة في الجدول", - "shiftTabInTableCell": "انتقل إلى الخلية المتوفرة سابقًا في الجدول", - "backSpaceInTableCell": "توقف في بداية الخلية" - }, - "commands": { - "codeBlockNewParagraph": "إدراج فقرة جديدة بجوار كتلة الكود", - "codeBlockIndentLines": "أدخل مسافتين في بداية السطر في كتلة الكود", - "codeBlockOutdentLines": "حذف مسافتين في بداية السطر في كتلة الكود", - "codeBlockAddTwoSpaces": "أدخل مسافتين في موضع المؤشر في كتلة الكود", - "codeBlockSelectAll": "تحديد كل المحتوى داخل كتلة الكود", - "codeBlockPasteText": "لصق النص في كتلة الكود", - "textAlignLeft": "محاذاة النص إلى اليسار", - "textAlignCenter": "محاذاة النص إلى الوسط", - "textAlignRight": "محاذاة النص إلى اليمين" - }, - "couldNotLoadErrorMsg": "لم نتمكن من تحميل الاختصارات، حاول مرة أخرى", - "couldNotSaveErrorMsg": "لم نتمكن من حفظ الاختصارات، حاول مرة أخرى" - }, - "aiPage": { - "title": "إعدادات الذكاء الاصطناعي", - "menuLabel": "إعدادات الذكاء الاصطناعي", - "keys": { - "enableAISearchTitle": "بحث الذكاء الاصطناعي", - "aiSettingsDescription": "اختر النموذج المفضل لديك لتشغيل AppFlowy AI. يتضمن الآن GPT 4-o وClaude 3,5 وLlama 3.1 وMistral 7B", - "loginToEnableAIFeature": "لا يتم تمكين ميزات الذكاء الاصطناعي إلا بعد تسجيل الدخول باستخدام @:appName Cloud. إذا لم يكن لديك حساب @:appName ، فانتقل إلى \"حسابي\" للتسجيل", - "llmModel": "نموذج اللغة", - "llmModelType": "نوع نموذج اللغة", - "downloadLLMPrompt": "تنزيل {}", - "downloadAppFlowyOfflineAI": "سيؤدي تنزيل حزمة AI دون اتصال بالإنترنت إلى تفعيل تشغيل AI على جهازك. هل تريد الاستمرار؟", - "downloadLLMPromptDetail": " تنزيل النموذج المحلي {} سيستخدم ما يصل إلى {} من مساحة التخزين. هل تريد الاستمرار؟", - "downloadBigFilePrompt": "قد يستغرق الأمر حوالي 10 دقائق لإكمال التنزيل", - "downloadAIModelButton": "تنزيل", - "downloadingModel": "جاري التنزيل", - "localAILoaded": "تمت إضافة نموذج الذكاء الاصطناعي المحلي بنجاح وهو جاهز للاستخدام", - "localAIStart": "بدأت الدردشة المحلية بالذكاء الاصطناعي...", - "localAILoading": "جاري تحميل نموذج الدردشة المحلية للذكاء الاصطناعي...", - "localAIStopped": "تم إيقاف الذكاء الاصطناعي المحلي", - "localAIRunning": "الذكاء الاصطناعي المحلي قيد التشغيل", - "localAINotReadyRetryLater": "جاري تهيئة الذكاء الاصطناعي المحلي، يرجى المحاولة مرة أخرى لاحقًا", - "localAIDisabled": "أنت تستخدم الذكاء الاصطناعي المحلي، ولكنه مُعطّل. يُرجى الانتقال إلى الإعدادات لتفعيله أو تجربة نموذج آخر.", - "localAIInitializing": "يتم تهيئة الذكاء الاصطناعي المحلي وقد يستغرق الأمر بضع دقائق، حسب جهازك", - "localAINotReadyTextFieldPrompt": "لا يمكنك التحرير أثناء تحميل الذكاء الاصطناعي المحلي", - "failToLoadLocalAI": "فشل في بدء تشغيل الذكاء الاصطناعي المحلي", - "restartLocalAI": "إعادة تشغيل الذكاء الاصطناعي المحلي", - "disableLocalAITitle": "تعطيل الذكاء الاصطناعي المحلي", - "disableLocalAIDescription": "هل تريد تعطيل الذكاء الاصطناعي المحلي؟", - "localAIToggleTitle": "التبديل لتفعيل أو تعطيل الذكاء الاصطناعي المحلي", - "localAIToggleSubTitle": "قم بتشغيل نماذج الذكاء الاصطناعي المحلية الأكثر تقدمًا داخل AppFlowy للحصول على أقصى درجات الخصوصية والأمان", - "offlineAIInstruction1": "اتبع", - "offlineAIInstruction2": "تعليمات", - "offlineAIInstruction3": "لتفعيل الذكاء الاصطناعي دون اتصال بالإنترنت.", - "offlineAIDownload1": "إذا لم تقم بتنزيل AppFlowy AI، فيرجى", - "offlineAIDownload2": "التنزيل", - "offlineAIDownload3": "إنه أولا", - "activeOfflineAI": "نشط", - "downloadOfflineAI": "التنزيل", - "openModelDirectory": "افتح المجلد", - "laiNotReady": "لم يتم تثبيت تطبيق الذكاء الاصطناعي المحلي بشكل صحيح.", - "ollamaNotReady": "خادم Ollama غير جاهز.", - "pleaseFollowThese": "اتبع هؤلاء", - "instructions": "التعليمات", - "installOllamaLai": "لإعداد Ollama وAppFlowy Local AI. تخطَّ هذه الخطوة إذا كنت قد قمت بإعدادها بالفعل", - "modelsMissing": "لم يتم العثور على النماذج المطلوبة.", - "downloadModel": "لتنزيلها.", - "startLocalAI": "قد يستغرق الأمر بضع ثوانٍ لبدء تشغيل الذكاء الاصطناعي المحلي" - } - }, - "planPage": { - "menuLabel": "الخطة", - "title": "خطة التسعير", - "planUsage": { - "title": "ملخص استخدام الخطة", - "storageLabel": "تخزين", - "storageUsage": "{} من {} جيجا بايت", - "unlimitedStorageLabel": "تخزين غير محدود", - "collaboratorsLabel": "أعضاء", - "collaboratorsUsage": "{} ل {}", - "aiResponseLabel": "استجابات الذكاء الاصطناعي", - "aiResponseUsage": "{} ل {}", - "unlimitedAILabel": "استجابات غير محدودة", - "proBadge": "احترافية", - "aiMaxBadge": "الذكاء الاصطناعي ماكس", - "aiOnDeviceBadge": "الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac", - "memberProToggle": "مزيد من الأعضاء وذكاء اصطناعي غير محدود", - "aiMaxToggle": "ذكاء اصطناعي غير محدود وإمكانية الوصول إلى نماذج متقدمة", - "aiOnDeviceToggle": "الذكاء الاصطناعي المحلي لتحقيق الخصوصية القصوى", - "aiCredit": { - "title": "أضف رصيد الذكاء الاصطناعي @:appName", - "price": "{}", - "priceDescription": "مقابل 1000 نقطة", - "purchase": "شراء الذكاء الاصطناعي", - "info": "أضف 1000 أرصدة الذكاء الاصطناعي لكل مساحة عمل وقم بدمج الذكاء الاصطناعي القابل للتخصيص بسلاسة في سير عملك للحصول على نتائج أذكى وأسرع مع ما يصل إلى:", - "infoItemOne": "10000 استجابة لكل قاعدة بيانات", - "infoItemTwo": "1000 استجابة لكل مساحة عمل" - }, - "currentPlan": { - "bannerLabel": "الخطة الحالية", - "freeTitle": "مجاني", - "proTitle": "محترف", - "teamTitle": "فريق", - "freeInfo": "مثالي للأفراد حتى عضوين لتنظيم كل شيء", - "proInfo": "مثالي للفرق الصغيرة والمتوسطة التي يصل عددها إلى 10 أعضاء.", - "teamInfo": "مثالي لجميع الفرق المنتجة والمنظمة جيدًا.", - "upgrade": "تغيير الخطة", - "canceledInfo": "لقد تم إلغاء خطتك، وسيتم تخفيض مستوى خطتك إلى الخطة المجانية في {}." - }, - "addons": { - "title": "مَرافِق", - "addLabel": "إضافة", - "activeLabel": "تمت الإضافة", - "aiMax": { - "title": "الحد الأقصى للذكاء الاصطناعي", - "description": "استجابات الذكاء الاصطناعي غير المحدودة مدعومة بـ GPT-4o وClaude 3.5 Sonnet والمزيد", - "price": "{}", - "priceInfo": "لكل مستخدم شهريًا يتم تحصيل الرسوم سنويًا" - }, - "aiOnDevice": { - "title": "الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac", - "description": "قم بتشغيل Mistral 7B وLLAMA 3 والمزيد من النماذج المحلية على جهازك", - "price": "{}", - "priceInfo": "لكل مستخدم شهريًا يتم تحصيل الرسوم سنويًا", - "recommend": "يوصى باستخدام M1 أو أحدث" - } - }, - "deal": { - "bannerLabel": "صفقة العام الجديد!", - "title": "تنمية فريقك!", - "info": "قم بالترقية واحصل على خصم 10% على خطط Pro وTeam! عزز إنتاجية مساحة العمل لديك من خلال ميزات جديدة قوية بما في ذلك الذكاء الاصطناعي @:appName .", - "viewPlans": "عرض الخطط" - } - } - }, - "billingPage": { - "menuLabel": "الفوترة", - "title": "الفوترة", - "plan": { - "title": "الخطة", - "freeLabel": "مجاني", - "proLabel": "مجاني", - "planButtonLabel": "تغيير الخطة", - "billingPeriod": "فترة الفوترة", - "periodButtonLabel": "تعديل الفترة" - }, - "paymentDetails": { - "title": "تفاصيل الدفع", - "methodLabel": "طريقة الدفع", - "methodButtonLabel": "طريقة التعديل" - }, - "addons": { - "title": "مَرافِق", - "addLabel": "إضافة", - "removeLabel": "إزالة", - "renewLabel": "تجديد", - "aiMax": { - "label": "الحد الأقصى للذكاء الاصطناعي", - "description": "إلغاء تأمين عدد غير محدود من الذكاء الاصطناعي والنماذج المتقدمة", - "activeDescription": "الفاتورة القادمة مستحقة في {}", - "canceledDescription": "سيكون AI Max متوفرا حتى {}" - }, - "aiOnDevice": { - "label": "الذكاء الاصطناعي على الجهاز لنظام التشغيل Mac", - "description": "إلغاء تأمين الذكاء الاصطناعي غير المحدود على جهازك", - "activeDescription": "الفاتورة القادمة مستحقة في {}", - "canceledDescription": "ستتوفر ميزة AI On-device for Mac حتى {}" - }, - "removeDialog": { - "title": "إزالة {}", - "description": "هل أنت متأكد من أنك تريد إزالة {plan}؟ ستفقد الوصول إلى ميزات وفوائد {plan} على الفور." - } - }, - "currentPeriodBadge": "الحالي", - "changePeriod": "فترة التغيير", - "planPeriod": "{} فترة", - "monthlyInterval": "شهريا", - "monthlyPriceInfo": "لكل مقعد يتم دفع الفاتورة شهريًا", - "annualInterval": "سنويا", - "annualPriceInfo": "لكل مقعد يتم دفع الفاتورة سنويًا" - }, - "comparePlanDialog": { - "title": "قارن واختر الخطة", - "planFeatures": " الخطة\nميزات", - "current": "الحالي", - "actions": { - "upgrade": "ترقية", - "downgrade": "يرجع إلى إصدار أقدم", - "current": "الحالي" - }, - "freePlan": { - "title": "مجاني", - "description": "للأفراد حتى عضوين لتنظيم كل شيء", - "price": "{}", - "priceInfo": "مجاني للأبد" - }, - "proPlan": { - "title": "احترافي", - "description": "للفرق الصغيرة لإدارة المشاريع ومعرفة الفريق", - "price": "{}", - "priceInfo": "لكل مستخدم شهريا\nيتم فوترتها سنويا\n\n{} يتم الفوترتها شهريًا" - }, - "planLabels": { - "itemOne": "مساحات العمل", - "itemTwo": "الأعضاء", - "itemThree": "التخزين", - "itemFour": "التعاون في الزمن الحقيقي", - "itemFive": "تطبيق الجوال", - "itemSix": "استجابات الذكاء الاصطناعي", - "itemSeven": "صور الذكاء الاصطناعي", - "itemFileUpload": "رفع الملفات", - "customNamespace": "مساحة اسم مخصصة", - "tooltipSix": "تعني مدة الحياة أن عدد الاستجابات لا يتم إعادة ضبطه أبدًا", - "intelligentSearch": "البحث الذكي", - "tooltipSeven": "يتيح لك تخصيص جزء من عنوان URL لمساحة العمل الخاصة بك", - "customNamespaceTooltip": "عنوان URL للموقع المنشور المخصص" - }, - "freeLabels": { - "itemOne": "يتم فرض رسوم على كل مساحة عمل", - "itemTwo": "حتى 2", - "itemThree": "5 جيجا بايت", - "itemFour": "نعم", - "itemFive": "نعم", - "itemSix": "10 مدى الحياة", - "itemSeven": "2 مدى الحياة", - "itemFileUpload": "حتى 7 ميجا بايت", - "intelligentSearch": "البحث الذكي" - }, - "proLabels": { - "itemOne": "تتم الفوترة على كل مساحة عمل", - "itemTwo": "حتى 10", - "itemThree": "غير محدود", - "itemFour": "نعم", - "itemFive": "نعم", - "itemSix": "غير محدود", - "itemSeven": "10 صور شهريا", - "itemFileUpload": "غير محدود", - "intelligentSearch": "البحث الذكي" - }, - "paymentSuccess": { - "title": "أنت الآن على خطة {}!", - "description": "لقد تمت معالجة الدفع بنجاح وتم ترقية خطتك إلى @:appName {}. يمكنك عرض تفاصيل خطتك على صفحة الخطة" - }, - "downgradeDialog": { - "title": "هل أنت متأكد أنك تريد تخفيض خطتك؟", - "description": "سيؤدي تخفيض مستوى خطتك إلى إعادتك إلى الخطة المجانية. قد يفقد الأعضاء إمكانية الوصول إلى مساحة العمل هذه وقد تحتاج إلى تحرير مساحة لتلبية حدود التخزين للخطة المجانية.", - "downgradeLabel": " تخفيض الخطة" - } - }, - "cancelSurveyDialog": { - "title": "نحن نأسف لرحيلك", - "description": "نحن نأسف لرحيلك. يسعدنا سماع تعليقاتك لمساعدتنا على تحسين @:appName . يُرجى تخصيص بعض الوقت للإجابة على بعض الأسئلة.", - "commonOther": "آخر", - "otherHint": "اكتب إجابتك هنا", - "questionOne": { - "question": "ما الذي دفعك إلى إلغاء اشتراكك @:appName Pro؟", - "answerOne": "التكلفة مرتفعة للغاية", - "answerTwo": "الميزات لم تلبي التوقعات", - "answerThree": "وجدت بديلا أفضل", - "answerFour": "لم أستخدمه بشكل كافي لتبرير التكلفة", - "answerFive": "مشكلة في الخدمة أو صعوبات فنية" - }, - "questionTwo": { - "question": "ما مدى احتمالية أن تفكر في إعادة الاشتراك في @:appName Pro في المستقبل؟", - "answerOne": "من المرجح جدًا", - "answerTwo": "من المحتمل إلى حد ما", - "answerThree": "غير متأكد", - "answerFour": "من غير المحتمل", - "answerFive": "من غير المحتمل جدًا" - }, - "questionThree": { - "question": "ما هي الميزة الاحترافية التي تقدرها أكثر أثناء اشتراكك؟", - "answerOne": "التعاون بين المستخدمين المتعددين", - "answerTwo": "تاريخ الإصدار الأطول", - "answerThree": "استجابات الذكاء الاصطناعي غير المحدودة", - "answerFour": "الوصول إلى نماذج الذكاء الاصطناعي المحلية" - }, - "questionFour": { - "question": "كيف تصف تجربتك العامة مع @:appName ؟", - "answerOne": "عظيم", - "answerTwo": "جيد", - "answerThree": "متوسط", - "answerFour": "أقل من المتوسط", - "answerFive": "غير راضٍ" - } - }, - "common": { - "uploadingFile": "جاري رفع الملف. الرجاء عدم الخروج من التطبيق", - "uploadNotionSuccess": "تم تحميل ملف Notion zip الخاص بك بنجاح. بمجرد اكتمال عملية الاستيراد، ستتلقى رسالة تأكيد عبر البريد الإلكتروني", - "reset": "إعادة ضبط" - }, "menu": { "appearance": "مظهر", "language": "لغة", @@ -1131,31 +269,26 @@ "syncSetting": "إعدادات المزامنة", "cloudSettings": "إعدادات السحابة", "enableSync": "تفعيل المزامنة", - "enableSyncLog": "تفعيل تسجيل المزامنة", - "enableSyncLogWarning": "شكرًا لك على مساعدتك في تشخيص مشكلات المزامنة. سيؤدي هذا إلى تسجيل تعديلات المستندات الخاصة بك في ملف محلي. يرجى الخروج من التطبيق وإعادة فتحه بعد تفعيله", "enableEncrypt": "تشفير البيانات", "cloudURL": "الرابط الأساسي", - "webURL": "عنوان الويب", "invalidCloudURLScheme": "مخطط غير صالح", "cloudServerType": "خادم سحابي", "cloudServerTypeTip": "يرجى ملاحظة أنه قد يقوم بتسجيل الخروج من حسابك الحالي بعد تبديل الخادم السحابي", "cloudLocal": "محلي", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "رابط Supabase", + "cloudSupabaseAnonKey": "مفتاح Supabase الخفي", + "cloudSupabaseAnonKeyCanNotBeEmpty": "لا يمكن أن يكون المفتاح المجهول فارغًا إذا لم يكن عنوان URL الخاص بـ Supabase فارغًا", "cloudAppFlowy": "سحابة @:appName", - "cloudAppFlowySelfHost": "@:appName استضافة ذاتية على السحابة", - "appFlowyCloudUrlCanNotBeEmpty": "لا يمكن أن يكون عنوان URL السحابي فارغًا", "clickToCopy": "انقر للنسخ", "selfHostStart": "إذا لم يكن لديك خادم، يرجى الرجوع إلى", "selfHostContent": "مستند", "selfHostEnd": "للحصول على إرشادات حول كيفية استضافة الخادم الخاص بك ذاتيًا", - "pleaseInputValidURL": "الرجاء إدخال عنوان URL صالح", - "changeUrl": "تغيير عنوان URL المستضاف ذاتيًا إلى {}", "cloudURLHint": "أدخل الرابط الأساسي لخادمك", - "webURLHint": "أدخل عنوان URL الأساسي لخادم الويب الخاص بك", "cloudWSURL": "عنوان Websockey", "cloudWSURLHint": "أدخل عنوان websocket لخادمك", "restartApp": "إعادة تشغيل", "restartAppTip": "أعد تشغيل التطبيق لتصبح التغييرات سارية المفعول. يرجى ملاحظة أن هذا قد يؤدي إلى تسجيل الخروج من حسابك الحالي", - "changeServerTip": "بعد تغيير الخادم يجب عليك الضغط على زر إعادة التشغيل حتى تسري التغييرات", "enableEncryptPrompt": "قم بتنشيط التشفير لتأمين بياناتك بهذا السر. قم بتخزينها بأمان؛ بمجرد تمكينه، لا يمكن إيقاف تشغيله. في حالة فقدانها، تصبح بياناتك غير قابلة للاسترداد. انقر للنسخ", "inputEncryptPrompt": "الرجاء إدخال سر التشفير الخاص بك ل", "clickToCopySecret": "انقر لنسخ السر", @@ -1166,96 +299,27 @@ "historicalUserListTooltip": "تعرض هذه القائمة حساباتك المجهولة. يمكنك النقر على الحساب لعرض تفاصيله. يتم إنشاء الحسابات المجهولة بالنقر فوق الزر \"البدء\".", "openHistoricalUser": "انقر لفتح الحساب الخفي", "customPathPrompt": "قد يؤدي تخزين مجلد بيانات @:appName في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات", - "importAppFlowyData": "استيراد البيانات من مجلد خارجي @:appName", - "importingAppFlowyDataTip": "جاري استيراد البيانات. يرجى عدم إغلاق التطبيق", - "importAppFlowyDataDescription": "انسخ البيانات من مجلد بيانات خارجي @:appName واستوردها إلى مجلد بيانات AppFlowy الحالي", - "importSuccess": "تم استيراد مجلد البيانات @:appName بنجاح", - "importFailed": "فشل استيراد مجلد البيانات @:appName", - "importGuide": "لمزيد من التفاصيل، يرجى مراجعة الوثيقة المشار إليها", + "supabaseSetting": "إعداد Supabase", "cloudSetting": "إعداد السحابة" }, "notifications": { "enableNotifications": { "label": "تمكين الإشعارات", "hint": "قم بإيقاف تشغيله لمنع ظهور الإشعارات المحلية." - }, - "showNotificationsIcon": { - "label": "إظهار أيقونة الإشعارات", - "hint": "قم بإيقاف تشغيله لإخفاء أيقونة الإشعار في الشريط الجانبي." - }, - "archiveNotifications": { - "allSuccess": "تم أرشفة جميع الإشعارات بنجاح", - "success": "تم أرشفة الإشعار بنجاح" - }, - "markAsReadNotifications": { - "allSuccess": "تم وضع علامة على كل شيء كمقروء بنجاح", - "success": "تم وضع علامة على القراءة بنجاح" - }, - "action": { - "markAsRead": "وضع علامة كمقروءة", - "multipleChoice": "إختر المزيد", - "archive": "أرشيف" - }, - "settings": { - "settings": "إعدادات", - "markAllAsRead": "وضع علامة على الكل كمقروء", - "archiveAll": "أرشفة الكل" - }, - "emptyInbox": { - "title": "صندوق الوارد صفر!", - "description": "قم بتعيين التذكيرات لتلقي الإشعارات هنا." - }, - "emptyUnread": { - "title": "لا توجد إشعارات غير مقروءة", - "description": "لقد تم قراءة جميع الرسائل." - }, - "emptyArchived": { - "title": "غير مؤرشفة", - "description": "ستظهر الإشعارات المؤرشفة هنا." - }, - "tabs": { - "inbox": "صندوق الوارد", - "unread": "غير مقروء", - "archived": "مؤرشفة" - }, - "refreshSuccess": "تم تحديث الإشعارات بنجاح", - "titles": { - "notifications": "إشعارات", - "reminder": "تذكير" } }, "appearance": { "resetSetting": "إعادة ضبط هذا الإعداد", "fontFamily": { "label": "الخط", - "search": "يبحث", - "defaultFont": "نظام" + "search": "يبحث" }, "themeMode": { - "label": "مظهر السمة", - "light": " المظهر الفاتح", - "dark": "المظهر الداكن", + "label": "وضع السمة", + "light": "وضع الضوء", + "dark": "الوضع الداكن", "system": "التكيف مع النظام" }, - "fontScaleFactor": "عامل مقياس الخط", - "displaySize": "حجم العرض", - "documentSettings": { - "cursorColor": "لون مؤشر المستند", - "selectionColor": "لون اختيار المستند", - "width": "عرض المستند", - "changeWidth": "تغير", - "pickColor": "حدد اللون", - "colorShade": "ظل اللون", - "opacity": "الشفافية", - "hexEmptyError": "لا يمكن أن يكون اللون السادسة عشرية فارغًا", - "hexLengthError": "يجب أن تكون القيمة السداسية عشرية مكونة من 6 أرقام", - "hexInvalidError": "القيمة السادسة العشرية غير صالحة", - "opacityEmptyError": "لا يمكن أن تكون الشفافية فارغة", - "opacityRangeError": "يجب أن تكون الشفافية بين 1 و 100", - "app": "App", - "flowy": "Flowy", - "apply": "تطبيق" - }, "layoutDirection": { "label": "اتجاه التخطيط", "hint": "التحكم في تدفق المحتوى على شاشتك، من اليسار إلى اليمين أو من اليمين إلى اليسار.", @@ -1272,10 +336,10 @@ }, "themeUpload": { "button": "رفع", - "uploadTheme": "رفع السمة", - "description": "قم برفع السمة @:appName الخاص بك باستخدام الزر أدناه.", - "loading": "يرجى الانتظار بينما نقوم بالتحقق من صحة السمة الخاصة بك وترفعها ...", - "uploadSuccess": "تم رفع سمتك بنجاح", + "uploadTheme": "تحميل الموضوع", + "description": "قم بتحميل قالب @:appName الخاص بك باستخدام الزر أدناه.", + "loading": "يرجى الانتظار بينما نقوم بالتحقق من صحة السمة الخاصة بك وتحميلها ...", + "uploadSuccess": "تم تحميل موضوعك بنجاح", "deletionFailure": "فشل حذف الموضوع. حاول حذفه يدويًا.", "filePickerDialogTitle": "اختر ملف .flowy_plugin", "urlUploadFailure": "فشل فتح عنوان url: {}", @@ -1298,48 +362,6 @@ "twentyFourHour": "نظام أربع وعشرون ساعة" }, "showNamingDialogWhenCreatingPage": "إظهار مربع حوار التسمية عند إنشاء الصفحة", - "enableRTLToolbarItems": "تمكين عناصر شريط أدوات RTL", - "members": { - "title": "إعدادات الأعضاء", - "inviteMembers": "دعوة الأعضاء", - "inviteHint": "دعوة عبر البريد الإلكتروني", - "sendInvite": "إرسال دعوة", - "copyInviteLink": "نسخ رابط الدعوة", - "label": "الأعضاء", - "user": "المستخدم", - "role": "الدور", - "removeFromWorkspace": "الإزالة من مساحة العمل", - "removeFromWorkspaceSuccess": "تمت الإزالة من مساحة العمل بنجاح", - "removeFromWorkspaceFailed": "فشلت عملية الإزالة من مساحة العمل", - "owner": "المالك", - "guest": "الضيف", - "member": "العضو", - "memberHintText": "يمكن للعضو قراءة الصفحات وتحريرها", - "guestHintText": "يمكن للضيف القراءة والرد والتعليق وتحرير صفحات معينة بإذن.", - "emailInvalidError": "بريد إلكتروني غير صالح، يرجى التحقق والمحاولة مرة أخرى", - "emailSent": "تم إرسال البريد الإلكتروني، يرجى التحقق من صندوق الوارد", - "members": "الأعضاء", - "membersCount": { - "zero": "{} الأعضاء", - "one": "{} العضو", - "other": "{} الأعضاء" - }, - "inviteFailedDialogTitle": "فشل في إرسال الدعوة", - "inviteFailedMemberLimit": "لقد تم الوصول إلى الحد الأقصى للأعضاء، يرجى الترقية لدعوة المزيد من الأعضاء.", - "inviteFailedMemberLimitMobile": "لقد وصلت مساحة العمل الخاصة بك إلى الحد الأقصى للأعضاء.", - "memberLimitExceeded": "تم الوصول إلى الحد الأقصى للأعضاء، لدعوة المزيد من الأعضاء، يرجى ", - "memberLimitExceededUpgrade": "ترقية", - "memberLimitExceededPro": "تم الوصول إلى الحد الأقصى للأعضاء، إذا كنت بحاجة إلى المزيد من الأعضاء، فاتصل بنا ", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "فشل في إضافة العضو", - "addMemberSuccess": "تمت إضافة العضو بنجاح", - "removeMember": "إزالة العضو", - "areYouSureToRemoveMember": "هل أنت متأكد أنك تريد إزالة هذا العضو؟", - "inviteMemberSuccess": "لقد تم ارسال الدعوة بنجاح", - "failedToInviteMember": "فشل في دعوة العضو", - "workspaceMembersError": "عفواً، حدث خطأ ما", - "workspaceMembersErrorDescription": "لم نتمكن من تحميل قائمة الأعضاء في هذا الوقت. يرجى المحاولة مرة أخرى لاحقًا" - }, "lightLabel": "فاتح", "darkLabel": "غامق" }, @@ -1377,37 +399,16 @@ "recoverLocationTooltips": "إعادة التعيين إلى دليل البيانات الافتراضي لـ @:appName", "exportFileSuccess": "تم تصدير الملف بنجاح!", "exportFileFail": "فشل تصدير الملف!", - "export": "يصدّر", - "clearCache": "مسح ذاكرة التخزين المؤقت", - "clearCacheDesc": "إذا واجهت مشكلات تتعلق بعدم تحميل الصور أو عدم عرض الخطوط بشكل صحيح، فحاول مسح ذاكرة التخزين المؤقت. لن يؤدي هذا الإجراء إلى إزالة بيانات المستخدم الخاصة بك.", - "areYouSureToClearCache": "هل أنت متأكد من مسح ذاكرة التخزين المؤقت؟", - "clearCacheSuccess": "تم مسح ذاكرة التخزين المؤقت بنجاح!" + "export": "يصدّر" }, "user": { "name": "اسم", "email": "بريد إلكتروني", "tooltipSelectIcon": "حدد أيقونة", "selectAnIcon": "حدد أيقونة", - "pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح AI الخاص بك", - "clickToLogout": "انقر لتسجيل خروج المستخدم الحالي", - "pleaseInputYourStabilityAIKey": "يرجى إدخال رمز Stability AI الخاص بك" - }, - "mobile": { - "personalInfo": "معلومات شخصية", - "username": "اسم المستخدم", - "usernameEmptyError": "لا يمكن أن يكون اسم المستخدم فارغا", - "about": "حول", - "pushNotifications": "اشعارات لحظية", - "support": "دعم", - "joinDiscord": "انضم إلينا على ديسكورد", - "privacyPolicy": "سياسة الخصوصية", - "userAgreement": "اتفاقية المستخدم", - "termsAndConditions": "الشروط والأحكام", - "userprofileError": "فشل تحميل ملف تعريف المستخدم", - "userprofileErrorDescription": "يرجى محاولة تسجيل الخروج وتسجيل الدخول مرة أخرى للتحقق مما إذا كانت المشكلة لا تزال قائمة.", - "selectLayout": "حدد الشكل", - "selectStartingDay": "اختر يوم البدء", - "version": "النسخة" + "pleaseInputYourOpenAIKey": "الرجاء إدخال مفتاح OpenAI الخاص بك", + "pleaseInputYourStabilityAIKey": "يرجى إدخال رمز Stability AI الخاص بك", + "clickToLogout": "انقر لتسجيل خروج المستخدم الحالي" }, "shortcuts": { "shortcutsLabel": "الاختصارات", @@ -1419,6 +420,21 @@ "resetToDefault": "إعادة التعيين إلى روابط المفاتيح الافتراضية", "couldNotLoadErrorMsg": "تعذر تحميل الاختصارات، حاول مرة أخرى", "couldNotSaveErrorMsg": "تعذر حفظ الاختصارات، حاول مرة أخرى" + }, + "mobile": { + "personalInfo": "معلومات شخصية", + "username": "اسم المستخدم", + "usernameEmptyError": "لا يمكن أن يكون اسم المستخدم فارغا", + "about": "حول", + "pushNotifications": "اشعارات لحظية", + "support": "دعم", + "joinDiscord": "انضم إلينا على ديسكورد", + "privacyPolicy": "سياسة الخصوصية", + "userAgreement": "اتفاقية المستخدم", + "userprofileError": "فشل تحميل ملف تعريف المستخدم", + "userprofileErrorDescription": "يرجى محاولة تسجيل الخروج وتسجيل الدخول مرة أخرى للتحقق مما إذا كانت المشكلة لا تزال قائمة.", + "selectLayout": "حدد الشكل", + "selectStartingDay": "اختر يوم البدء" } }, "grid": { @@ -1439,29 +455,9 @@ "filterBy": "مصنف بواسطة...", "typeAValue": "اكتب قيمة ...", "layout": "تَخطِيط", - "compactMode": "الوضع المضغوط", "databaseLayout": "تَخطِيط", - "viewList": { - "zero": "0 مشاهدات", - "one": "{count} مشاهدة", - "other": "{count} المشاهدات" - }, - "editView": "تعديل العرض", - "boardSettings": "إعدادات اللوحة", - "calendarSettings": "إعدادات التقويم", - "createView": "عرض جديد", - "duplicateView": "عرض مكرر", - "deleteView": "حذف العرض", - "numberOfVisibleFields": "{} تم عرضه", "Properties": "ملكيات" }, - "filter": { - "empty": "لا توجد عوامل تصفية نشطة", - "addFilter": "أضف عامل التصفية", - "cannotFindCreatableField": "لا يمكن العثور على حقل مناسب للتصفية حسبه", - "conditon": "حالة", - "where": "أين" - }, "textFilter": { "contains": "يتضمن", "doesNotContain": "لا يحتوي", @@ -1507,40 +503,15 @@ "onOrAfter": "يكون في او بعد", "between": "يتراوح ما بين", "empty": "فارغ", - "notEmpty": "ليس فارغا", - "startDate": "تاريخ البدء", - "endDate": "تاريخ النهاية", - "choicechipPrefix": { - "before": "قبل", - "after": "بعد", - "between": "بين", - "onOrBefore": "في أو قبل", - "onOrAfter": "في أو بعد", - "isEmpty": "هو فارغ", - "isNotEmpty": "ليس فارغا" - } - }, - "numberFilter": { - "equal": "يساوي", - "notEqual": "لا يساوي", - "lessThan": "أقل من", - "greaterThan": "أكبر من", - "lessThanOrEqualTo": "أقل من أو يساوي", - "greaterThanOrEqualTo": "أكبر من أو يساوي", - "isEmpty": "هو فارغ", - "isNotEmpty": "ليس فارغا" + "notEmpty": "ليس فارغا" }, "field": { - "label": "خاصية", "hide": "يخفي", "show": "عرض", "insertLeft": "أدخل اليسار", "insertRight": "أدخل اليمين", "duplicate": "ينسخ", "delete": "يمسح", - "wrapCellContent": "لف النص", - "clear": "مسح الخلايا", - "switchPrimaryFieldTooltip": "لا يمكن تغيير نوع الحقل للحقل الأساسي", "textFieldName": "نص", "checkboxFieldName": "خانة اختيار", "dateFieldName": "تاريخ", @@ -1551,12 +522,6 @@ "multiSelectFieldName": "تحديد متعدد", "urlFieldName": "URL", "checklistFieldName": "قائمة تدقيق", - "relationFieldName": "العلاقة", - "summaryFieldName": "ملخص الذكاء الاصطناعي", - "timeFieldName": "وقت", - "mediaFieldName": "الملفات والوسائط", - "translateFieldName": "الترجمة بالذكاء الاصطناعي", - "translateTo": "ترجم إلى", "numberFormat": "تنسيق الأرقام", "dateFormat": "صيغة التاريخ", "includeTime": "أضف الوقت", @@ -1585,13 +550,9 @@ "addOption": "إضافة خيار", "editProperty": "تحرير الملكية", "newProperty": "خاصية جديدة", - "openRowDocument": "افتح كصفحة", "deleteFieldPromptMessage": "هل أنت متأكد؟ سيتم حذف هذه الخاصية", - "clearFieldPromptMessage": "هل أنت متأكد؟ سيتم إفراغ جميع الخلايا في هذا العمود", "newColumn": "عمود جديد", - "format": "شكل", - "reminderOnDateTooltip": "تحتوي هذه الخلية على تذكير مجدول", - "optionAlreadyExist": "الخيار موجود بالفعل" + "format": "شكل" }, "rowPage": { "newField": "إضافة حقل جديد", @@ -1605,25 +566,16 @@ "one": "إخفاء {count} الحقل المخفي", "many": "إخفاء {count} الحقول المخفية", "other": "إخفاء {count} الحقول المخفية" - }, - "openAsFullPage": "افتح كصفحة كاملة", - "moreRowActions": "مزيد من إجراءات الصف" + } }, "sort": { "ascending": "تصاعدي", "descending": "تنازلي", - "by": "بواسطة", - "empty": "لا توجد تصنيفات نشطة", - "cannotFindCreatableField": "لا يمكن العثور على حقل مناسب للتصنيف حسبه", "deleteAllSorts": "حذف جميع التراتيب", "addSort": "أضف نوعًا", - "sortsActive": "لا يمكن {intention} أثناء التصنيف", - "removeSorting": "هل ترغب في إزالة كافة التصنيفات في هذا العرض والمتابعة؟", - "fieldInUse": "أنت تقوم بالفعل بالتصنيف حسب هذا الحقل", "deleteSort": "حذف الفرز" }, "row": { - "label": "صف", "duplicate": "مكرره", "delete": "يمسح", "titlePlaceholder": "بدون عنوان", @@ -1631,19 +583,12 @@ "copyProperty": "نسخ الممتلكات إلى الحافظة", "count": "عدد", "newRow": "صف جديد", - "loadMore": "تحميل المزيد", "action": "فعل", "add": "انقر فوق إضافة إلى أدناه", "drag": "اسحب للتحريك", - "deleteRowPrompt": "هل أنت متأكد من أنك تريد حذف هذا الصف؟ لا يمكن التراجع عن هذا الإجراء.", - "deleteCardPrompt": "هل أنت متأكد من أنك تريد حذف هذه البطاقة؟ لا يمكن التراجع عن هذا الإجراء.", "dragAndClick": "اسحب للتحريك، انقر لفتح القائمة", "insertRecordAbove": "أدخل السجل أعلاه", - "insertRecordBelow": "أدخل السجل أدناه", - "noContent": "لا يوجد محتوى", - "reorderRowDescription": "إعادة ترتيب الصف", - "createRowAboveDescription": "إنشاء صف أعلى", - "createRowBelowDescription": "أدخل صفًا أدناه" + "insertRecordBelow": "أدخل السجل أدناه" }, "selectOption": { "create": "يخلق", @@ -1664,7 +609,9 @@ "createNew": "إنشاء جديد", "orSelectOne": "أو حدد خيارًا", "typeANewOption": "اكتب خيارًا جديدًا", - "tagName": "اسم العلامة" + "tagName": "اسم العلامة", + "colorPannelTitle": "لوحة الالوان", + "pannelTitle": "لوحة الخيارات" }, "checklist": { "taskHint": "وصف المهمة", @@ -1675,53 +622,10 @@ }, "url": { "launch": "فتح في المتصفح", - "copy": "إنسخ الرابط", - "textFieldHint": "أدخل عنوان URL" - }, - "relation": { - "relatedDatabasePlaceLabel": "قاعدة البيانات ذات الصلة", - "relatedDatabasePlaceholder": "لا أحد", - "inRelatedDatabase": "في", - "rowSearchTextFieldPlaceholder": "بحث", - "noDatabaseSelected": "لم يتم تحديد قاعدة بيانات، الرجاء تحديد قاعدة بيانات واحدة أولاً من القائمة أدناه:", - "emptySearchResult": "لم يتم العثور على أي سجلات", - "linkedRowListLabel": "{count} صفوف مرتبطة", - "unlinkedRowListLabel": "ربط صف آخر" + "copy": "إنسخ الرابط" }, "menuName": "شبكة", - "referencedGridPrefix": "نظرا ل", - "calculate": "احسب", - "calculationTypeLabel": { - "none": "لا أحد", - "average": "المعدل", - "max": "الحد الأقصى", - "median": "المتوسط", - "min": "الحد الأدنى", - "sum": "المجموع", - "count": "العدد", - "countEmpty": "عدد فارغ", - "countEmptyShort": "فارغ", - "countNonEmpty": "العدد ليس فارغا", - "countNonEmptyShort": "مملوء" - }, - "media": { - "rename": "إعادة تسمية", - "download": "التنزيل", - "expand": "توسيع", - "delete": "الحذف", - "moreFilesHint": "+{}", - "addFileOrImage": "إضافة ملف أو رابط", - "attachmentsHint": "{}", - "addFileMobile": "إضافة ملف", - "extraCount": "+{}", - "deleteFileDescription": "هل أنت متأكد من أنك تريد حذف هذا الملف؟ هذا الإجراء لا رجعة فيه.", - "showFileNames": "إظهار اسم الملف", - "downloadSuccess": "تم تنزيل الملف", - "downloadFailedToken": "فشل تنزيل الملف، الرمز المميز للمستخدم غير متوفر", - "setAsCover": "تعيين كغلاف", - "openInBrowser": "فتح في المتصفح", - "embedLink": "تضمين رابط الملف" - } + "referencedGridPrefix": "نظرا ل" }, "document": { "menuName": "وثيقة", @@ -1729,7 +633,6 @@ "timeHintTextInTwelveHour": "01:00 مساءً", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "جارٍ الإنشاء...", "slashMenu": { "board": { "selectABoardToLinkTo": "حدد لوحة للارتباط بها", @@ -1745,144 +648,43 @@ }, "document": { "selectADocumentToLinkTo": "حدد مستندًا للارتباط به" - }, - "name": { - "textStyle": "نمط النص", - "list": "قائمة", - "toggle": "تبديل", - "fileAndMedia": "الملفات والوسائط", - "simpleTable": "جدول بسيط", - "visuals": "المرئيات", - "document": "وثيقة", - "advanced": "متقدم", - "text": "نص", - "heading1": "العنوان 1", - "heading2": "العنوان 2", - "heading3": "العنوان 3", - "image": "صورة", - "bulletedList": "قائمة نقطية", - "numberedList": "قائمة مرقمة", - "todoList": "قائمة المهام", - "doc": "وثيقة", - "linkedDoc": "رابط الصفحة", - "grid": "شبكة", - "linkedGrid": "الشبكة المرتبطة", - "kanban": "كانبان", - "linkedKanban": "كانبان مرتبط", - "calendar": "تقويم", - "linkedCalendar": "التقويم المرتبط", - "quote": "اقتباس", - "divider": "فاصل", - "table": "الجدول", - "callout": "وسيلة الشرح", - "outline": "مخطط تفصيلي", - "mathEquation": "معادلة الرياضيات", - "code": "الكود", - "toggleList": "قائمة التبديل", - "toggleHeading1": "تبديل العنوان 1", - "toggleHeading2": "تبديل العنوان 2", - "toggleHeading3": "تبديل العنوان 3", - "emoji": "الرموز التعبيرية", - "aiWriter": "كاتب الذكاء الاصطناعي", - "dateOrReminder": "التاريخ أو التذكير", - "photoGallery": "معرض الصور", - "file": "الملف", - "twoColumns": "عمودين", - "threeColumns": "3 أعمدة", - "fourColumns": "4 أعمدة" - }, - "subPage": { - "name": "المستند", - "keyword1": "الصفحة الفرعية", - "keyword2": "الصفحة", - "keyword3": "الصفحة الطفل", - "keyword4": "ادراج الصفحة", - "keyword5": "تضمين الصفحة", - "keyword6": "صفحة جديدة", - "keyword7": "إنشاء صفحة", - "keyword8": "المستند" } }, "selectionMenu": { "outline": "الخطوط العريضة", - "codeBlock": "كتلة الكود" + "codeBlock": "حقل الكود" }, "plugins": { "referencedBoard": "المجلس المشار إليه", "referencedGrid": "الشبكة المشار إليها", "referencedCalendar": "التقويم المشار إليه", "referencedDocument": "الوثيقة المشار إليها", - "aiWriter": { - "userQuestion": "اسأل الذكاء الاصطناعي عن أي شيء", - "continueWriting": "استمر في الكتابة", - "fixSpelling": "تصحيح الأخطاء الإملائية والنحوية", - "improveWriting": "تحسين الكتابة", - "summarize": "تلخيص", - "explain": "اشرح", - "makeShorter": "اجعلها أقصر", - "makeLonger": "اجعلها أطول" - }, - "autoGeneratorMenuItemName": "كاتب AI", - "autoGeneratorTitleName": "AI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...", + "autoGeneratorMenuItemName": "كاتب OpenAI", + "autoGeneratorTitleName": "OpenAI: اطلب من الذكاء الاصطناعي كتابة أي شيء ...", "autoGeneratorLearnMore": "يتعلم أكثر", "autoGeneratorGenerate": "يولد", - "autoGeneratorHintText": "اسأل AI ...", - "autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح AI", + "autoGeneratorHintText": "اسأل OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "لا يمكن الحصول على مفتاح OpenAI", "autoGeneratorRewrite": "اعادة كتابة", "smartEdit": "مساعدي الذكاء الاصطناعي", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "أصلح التهجئة", "warning": "⚠️ يمكن أن تكون استجابات الذكاء الاصطناعي غير دقيقة أو مضللة.", "smartEditSummarize": "لخص", "smartEditImproveWriting": "تحسين الكتابة", "smartEditMakeLonger": "اجعله أطول", - "smartEditCouldNotFetchResult": "تعذر جلب النتيجة من AI", - "smartEditCouldNotFetchKey": "تعذر جلب مفتاح AI", - "smartEditDisabled": "قم بتوصيل AI في الإعدادات", - "appflowyAIEditDisabled": "تسجيل الدخول لتمكين ميزات الذكاء الاصطناعي", + "smartEditCouldNotFetchResult": "تعذر جلب النتيجة من OpenAI", + "smartEditCouldNotFetchKey": "تعذر جلب مفتاح OpenAI", + "smartEditDisabled": "قم بتوصيل OpenAI في الإعدادات", "discardResponse": "هل تريد تجاهل استجابات الذكاء الاصطناعي؟", "createInlineMathEquation": "اصنع معادلة", "fonts": "الخطوط", - "insertDate": "أدخل التاريخ", - "emoji": "الرموز التعبيرية", "toggleList": "تبديل القائمة", - "emptyToggleHeading": "تبديل فارغ h{}. انقر لإضافة محتوى.", - "emptyToggleList": "قائمة تبديل فارغة. انقر لإضافة المحتوى.", - "emptyToggleHeadingWeb": "تبديل فارغ h{level}. انقر لإضافة محتوى", "quoteList": "قائمة الاقتباس", "numberedList": "قائمة مرقمة", "bulletedList": "قائمة نقطية", "todoList": "قائمة مهام", "callout": "استدعاء", - "simpleTable": { - "moreActions": { - "color": "اللون", - "align": "المحاذاة", - "delete": "حذف", - "duplicate": "تكرار", - "insertLeft": "أدخل اليسار", - "insertRight": "إدراج إلى اليمين", - "insertAbove": "إدراج أعلاه", - "insertBelow": "إدراج أدناه", - "headerColumn": "عمود رأس الصفحة", - "headerRow": "صف رأس الصفحة", - "clearContents": "مسح المحتويات", - "setToPageWidth": "اضبط على عرض الصفحة", - "distributeColumnsWidth": "توزيع الأعمدة بالتساوي", - "duplicateRow": "صف مكرر", - "duplicateColumn": "عمود مكرر", - "textColor": "لون النص", - "cellBackgroundColor": "لون خلفية الخلية", - "duplicateTable": "جدول مكرر" - }, - "clickToAddNewRow": "انقر لإضافة صف جديد", - "clickToAddNewColumn": "انقر لإضافة عمود جديد", - "clickToAddNewRowAndColumn": "انقر لإضافة صف وعمود جديدين", - "headerName": { - "table": "الجدول", - "alignText": "محاذاة النص" - } - }, "cover": { "changeCover": "تبديل الغطاء", "colors": "الألوان", @@ -1898,7 +700,6 @@ "back": "خلف", "saveToGallery": "حفظ في المعرض", "removeIcon": "إزالة الرمز", - "removeCover": "إزالة الغلاف", "pasteImageUrl": "لصق عنوان URL للصورة", "or": "أو", "pickFromFiles": "اختر من الملفات", @@ -1917,8 +718,6 @@ "optionAction": { "click": "انقر", "toOpenMenu": " لفتح القائمة", - "drag": "سحب", - "toMove": " حرك", "delete": "يمسح", "duplicate": "ينسخ", "turnInto": "تحول إلى", @@ -1929,44 +728,14 @@ "left": "غادر", "center": "مركز", "right": "يمين", - "defaultColor": "تقصير", - "depth": "عمق", - "copyLinkToBlock": "نسخ الرابط إلى الكتلة" + "defaultColor": "تقصير" }, "image": { - "addAnImage": "أضف صورة", "copiedToPasteBoard": "تم نسخ رابط الصورة إلى الحافظة", - "addAnImageDesktop": "أضف صورة", - "addAnImageMobile": "انقر لإضافة صورة واحدة أو أكثر", - "dropImageToInsert": "إسقاط الصور لإدراجها", - "imageUploadFailed": "فشل رفع الصورة", - "imageDownloadFailed": "فشل تنزيل الصورة، يرجى المحاولة مرة أخرى", - "imageDownloadFailedToken": "فشل تنزيل الصورة بسبب عدم وجود رمز المستخدم، يرجى المحاولة مرة أخرى", - "errorCode": "كود الخطأ" - }, - "photoGallery": { - "name": "معرض الصور", - "imageKeyword": "صورة", - "imageGalleryKeyword": "معرض الصور", - "photoKeyword": "صورة", - "photoBrowserKeyword": "متصفح الصور", - "galleryKeyword": "معرض الصور", - "addImageTooltip": "أضف صورة", - "changeLayoutTooltip": "تغيير التخطيط", - "browserLayout": "المتصفح", - "gridLayout": "شبكة", - "deleteBlockTooltip": "حذف المعرض بأكمله" - }, - "math": { - "copiedToPasteBoard": "تم نسخ المعادلة الرياضية إلى الحافظة" - }, - "urlPreview": { - "copiedToPasteBoard": "تم نسخ الرابط إلى الحافظة", - "convertToLink": "تحويل إلى رابط التضمين" + "addAnImage": "أضف صورة" }, "outline": { - "addHeadingToCreateOutline": "أضف عناوين لإنشاء جدول محتويات.", - "noMatchHeadings": "لم يتم العثور على عناوين مطابقة." + "addHeadingToCreateOutline": "أضف عناوين لإنشاء جدول محتويات." }, "table": { "addAfter": "أضف بعد", @@ -1979,69 +748,11 @@ "contextMenu": { "copy": "نسخ", "cut": "قطع", - "paste": "لصق", - "pasteAsPlainText": "لصق كنص عادي" + "paste": "لصق" }, "action": "أجراءات", - "database": { - "selectDataSource": "حدد مصدر البيانات", - "noDataSource": "لا يوجد مصدر للبيانات", - "selectADataSource": "حدد مصدر البيانات", - "toContinue": "للمتابعة", - "newDatabase": "قاعدة بيانات جديدة", - "linkToDatabase": "رابط لقاعدة البيانات" - }, - "date": "تاريخ", - "video": { - "label": "فيديو", - "emptyLabel": "أضف فيديو", - "placeholder": "ألصق رابط الفيديو", - "copiedToPasteBoard": "تم نسخ رابط الفيديو إلى الحافظة", - "insertVideo": "إضافة الفيديو", - "invalidVideoUrl": "لم يتم دعم عنوان URL المصدر بعد.", - "invalidVideoUrlYouTube": "لم يتم دعم YouTube بعد.", - "supportedFormats": "التنسيقات المدعومة: MP4، WebM، MOV، AVI، FLV، MPEG/M4V، H.264" - }, - "file": { - "name": "ملف", - "uploadTab": "رفع", - "uploadMobile": "اختر ملف", - "uploadMobileGallery": "من معرض الصور", - "networkTab": "تضمين الرابط", - "placeholderText": "رفع أو تضمين ملف", - "placeholderDragging": "إفلات الملف للرفع", - "dropFileToUpload": "إفلات ملف لتحميله", - "fileUploadHint": "اسحب وأفلِت ملفًا أو انقر فوقه ", - "fileUploadHintSuffix": "تصفح", - "networkHint": "لصق رابط الملف", - "networkUrlInvalid": "عنوان URL غير صالح. تحقق من عنوان URL وحاول مرة أخرى.", - "networkAction": "تضمين", - "fileTooBigError": "حجم الملف كبير جدًا، يرجى رفع ملف بحجم أقل من 10 ميجا بايت", - "renameFile": { - "title": "إعادة تسمية الملف", - "description": "أدخل الاسم الجديد لهذا الملف", - "nameEmptyError": "لا يمكن ترك اسم الملف فارغًا." - }, - "uploadedAt": "تم الرفع في {}", - "linkedAt": "تمت إضافة الرابط في {}", - "failedToOpenMsg": "فشل في الفتح، لم يتم العثور على الملف" - }, - "subPage": { - "handlingPasteHint": " - (التعامل مع اللصق)", - "errors": { - "failedDeletePage": "فشل في حذف الصفحة", - "failedCreatePage": "فشل في إنشاء الصفحة", - "failedMovePage": "فشل نقل الصفحة إلى هذا المستند", - "failedDuplicatePage": "فشل في تكرار الصفحة", - "failedDuplicateFindView": "فشل في تكرار الصفحة - لم يتم العثور على العرض الأصلي" - } - }, - "cannotMoveToItsChildren": "لا يمكن الانتقال إلى أطفاله", "autoCompletionMenuItemName": "عنصر قائمة الإكمال التلقائي" }, - "outlineBlock": { - "placeholder": "جدول المحتويات" - }, "textBlock": { "placeholder": "اكتب \"/\" للأوامر" }, @@ -2059,8 +770,8 @@ "placeholder": "أدخل عنوان URL للصورة" }, "ai": { - "label": "إنشاء صورة من AI", - "placeholder": "يرجى إدخال الامر الواصف لـ AI لإنشاء الصورة" + "label": "إنشاء صورة من OpenAI", + "placeholder": "يرجى إدخال الامر الواصف لـ OpenAI لإنشاء الصورة" }, "stability_ai": { "label": "إنشاء صورة من Stability AI", @@ -2071,9 +782,7 @@ "invalidImage": "صورة غير صالحة", "invalidImageSize": "يجب أن يكون حجم الصورة أقل من 5 ميغا بايت", "invalidImageFormat": "تنسيق الصورة غير مدعوم. التنسيقات المدعومة: JPEG ، PNG ، GIF ، SVG", - "invalidImageUrl": "عنوان URL للصورة غير صالح", - "noImage": "لا يوجد مثل هذا الملف أو الدليل", - "multipleImagesFailed": "فشلت عملية رفع صورة واحدة أو أكثر، يرجى المحاولة مرة أخرى" + "invalidImageUrl": "عنوان URL للصورة غير صالح" }, "embedLink": { "label": "رابط متضمن", @@ -2083,40 +792,18 @@ "label": "Unsplash" }, "searchForAnImage": "ابحث عن صورة", - "pleaseInputYourOpenAIKey": "يرجى إدخال مفتاح AI الخاص بك في صفحة الإعدادات", + "pleaseInputYourOpenAIKey": "يرجى إدخال مفتاح OpenAI الخاص بك في صفحة الإعدادات", + "pleaseInputYourStabilityAIKey": "يرجى إدخال مفتاح Stability AI الخاص بك في صفحة الإعدادات", "saveImageToGallery": "احفظ الصورة", "failedToAddImageToGallery": "فشلت إضافة الصورة إلى المعرض", "successToAddImageToGallery": "تمت إضافة الصورة إلى المعرض بنجاح", - "unableToLoadImage": "غير قادر على تحميل الصورة", - "maximumImageSize": "الحد الأقصى لحجم الصورة المسموح برفعها هو 10 ميجا بايت", - "uploadImageErrorImageSizeTooBig": "يجب أن يكون حجم الصورة أقل من 10 ميجا بايت", - "imageIsUploading": "جاري رفع الصورة", - "openFullScreen": "افتح في الشاشة الكاملة", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "الصورة السابقة", - "nextImageTooltip": "الصورة التالية", - "zoomOutTooltip": "تصغير", - "zoomInTooltip": "تكبير", - "changeZoomLevelTooltip": "تغيير مستوى التكبير", - "openLocalImage": "افتح الصورة", - "downloadImage": "تنزيل الصورة", - "closeViewer": "إغلاق العارض التفاعلي", - "scalePercentage": "{}%", - "deleteImageTooltip": "حذف الصورة" - } - }, - "pleaseInputYourStabilityAIKey": "يرجى إدخال مفتاح Stability AI الخاص بك في صفحة الإعدادات" + "unableToLoadImage": "غير قادر على تحميل الصورة" }, "codeBlock": { "language": { "label": "لغة", - "placeholder": "اختار اللغة", - "auto": "آلي" - }, - "copyTooltip": "نسخ", - "searchLanguageHint": "ابحث عن لغة", - "codeCopiedSnackbar": "تم نسخ الكود إلى الحافظة!" + "placeholder": "اختار اللغة" + } }, "inlineLink": { "placeholder": "الصق أو اكتب ارتباطًا", @@ -2137,54 +824,14 @@ "page": { "label": "رابط إلى الصفحة", "tooltip": "انقر لفتح الصفحة" - }, - "deleted": "تم الحذف", - "deletedContent": "هذا المحتوى غير موجود أو تم حذفه", - "noAccess": "لا يوجد وصول", - "deletedPage": "الصفحة المحذوفة", - "trashHint": " - في سلة المهملات", - "morePages": "المزيد من الصفحات" + } }, "toolbar": { - "resetToDefaultFont": "إعادة تعيين إلى الافتراضي", - "textSize": "حجم النص", - "textColor": "لون النص", - "h1": "العنوان 1", - "h2": "العنوان 2", - "h3": "العنوان 3", - "alignLeft": "محاذاة إلى اليسار", - "alignRight": "محاذاة إلى اليمين", - "alignCenter": "محاذاة إلى الوسط", - "link": "وصلة", - "textAlign": "محاذاة النص", - "moreOptions": "المزيد من الخيارات", - "font": "الخط", - "inlineCode": "الكود المضمن", - "suggestions": "اقتراحات", - "turnInto": "تحول إلى", - "equation": "معادلة", - "insert": "إدراج", - "linkInputHint": "لصق الرابط أو البحث عن الصفحات", - "pageOrURL": "الصفحة أو عنوان URL", - "linkName": "اسم الرابط", - "linkNameHint": "اسم رابط الإدخال" + "resetToDefaultFont": "إعادة تعيين إلى الافتراضي" }, "errorBlock": { "theBlockIsNotSupported": "الإصدار الحالي لا يدعم هذا الحقل.", - "clickToCopyTheBlockContent": "انقر هنا لنسخ محتوى الكتلة", - "blockContentHasBeenCopied": "تم نسخ محتوى الحقل.", - "parseError": "حدث خطأ أثناء تحليل الكتلة {}.", - "copyBlockContent": "نسخ محتوى الكتلة" - }, - "mobilePageSelector": { - "title": "حدد الصفحة", - "failedToLoad": "فشل تحميل قائمة الصفحات", - "noPagesFound": "لم يتم العثور على صفحات" - }, - "attachmentMenu": { - "choosePhoto": "اختر الصورة", - "takePicture": "التقط صورة", - "chooseFile": "اختر الملف" + "blockContentHasBeenCopied": "تم نسخ محتوى الحقل." }, "data": { "timeHintTextInTwelveHour": "اثنا عشر ساعة", @@ -2193,7 +840,6 @@ }, "board": { "column": { - "label": "عمود", "createNewCard": "جديد", "renameGroupTooltip": "اضغط لإعادة تسمية المجموعة", "createNewColumn": "أضف مجموعة جديدة", @@ -2224,7 +870,6 @@ "ungroupedButtonTooltip": "تحتوي على بطاقات لا تنتمي إلى أي مجموعة", "ungroupedItemsTitle": "انقر للإضافة إلى السبورة", "groupBy": "مجموعة من", - "groupCondition": "حالة المجموعة", "referencedBoardPrefix": "نظرا ل", "notesTooltip": "ملاحظات بالداخل", "mobile": { @@ -2232,22 +877,6 @@ "showGroup": "إظهار المجموعة", "showGroupContent": "هل أنت متأكد من إظهار هذه المجموعة على السبورة؟", "failedToLoad": "فشل تحميل عرض السبورة" - }, - "dateCondition": { - "weekOf": "أسبوع {} - {}", - "today": "اليوم", - "yesterday": "أمس", - "tomorrow": "غداً", - "lastSevenDays": "آخر 7 أيام", - "nextSevenDays": "الأيام 7 القادمة", - "lastThirtyDays": "آخر 30 يوما", - "nextThirtyDays": "30 يوما القادم" - }, - "noGroup": "لا توجد مجموعة حسب الخاصية", - "noGroupDesc": "تتطلب وجهات نظر اللوحة خاصية للتجميع من أجل العرض", - "media": { - "cardText": "{} {}", - "fallbackName": "الملفات" } }, "calendar": { @@ -2258,45 +887,29 @@ "today": "اليوم", "jumpToday": "انتقل إلى اليوم", "previousMonth": "الشهر الماضى", - "nextMonth": "الشهر القادم", - "views": { - "day": "يوم", - "week": "أسبوع", - "month": "شهر", - "year": "سنة" - } - }, - "mobileEventScreen": { - "emptyTitle": "لا يوجد أحداث حتى الآن", - "emptyBody": "اضغط على زر الإضافة \"+\" لإنشاء حدث في هذا اليوم." + "nextMonth": "الشهر القادم" }, "settings": { "showWeekNumbers": "إظهار أرقام الأسبوع", "showWeekends": "عرض عطلات نهاية الأسبوع", "firstDayOfWeek": "اليوم الأول من الأسبوع", "layoutDateField": "تقويم التخطيط بواسطة", - "changeLayoutDateField": "تغيير حقل التخطيط", "noDateTitle": "بدون تاريخ", - "noDateHint": "ستظهر الأحداث غير المجدولة هنا", "unscheduledEventsTitle": "الأحداث غير المجدولة", "clickToAdd": "انقر للإضافة إلى التقويم", "name": "تخطيط التقويم", - "clickToOpen": "انقر لفتح السجل" + "noDateHint": "ستظهر الأحداث غير المجدولة هنا" }, "referencedCalendarPrefix": "نظرا ل", - "quickJumpYear": "انتقل إلى", - "duplicateEvent": "حدث مكرر" + "quickJumpYear": "انتقل إلى" }, "errorDialog": { "title": "خطأ @:appName", "howToFixFallback": "نأسف للإزعاج! قم بإرسال مشكلة على صفحة GitHub الخاصة بنا والتي تصف الخطأ الخاص بك.", - "howToFixFallbackHint1": "نحن نأسف للإزعاج! أرسل مشكلة على ", - "howToFixFallbackHint2": " الصفحة التي تصف الخطأ الخاص بك.", "github": "عرض على جيثب" }, "search": { "label": "يبحث", - "sidebarSearchIcon": "ابحث وانتقل بسرعة إلى الصفحة", "placeholder": { "actions": "إجراءات البحث ..." } @@ -2354,48 +967,19 @@ "medium": "متوسط", "mediumDark": "متوسطة الظلمة", "dark": "مظلم" - }, - "openSourceIconsFrom": "أيقونات مفتوحة المصدر من" + } }, "inlineActions": { "noResults": "لا نتائج", - "recentPages": "الصفحات الأخيرة", "pageReference": "مرجع الصفحة", - "docReference": "مرجع المستند", - "boardReference": "مرجع اللوحة", - "calReference": "مرجع التقويم", - "gridReference": "مرجع الشبكة", "date": "تاريخ", "reminder": { "groupTitle": "تذكير", "shortKeyword": "يذكر" - }, - "createPage": "إنشاء صفحة فرعية \"{}\"" + } }, "datePicker": { - "dateTimeFormatTooltip": "تغيير تنسيق التاريخ والوقت في الإعدادات", - "dateFormat": "تنسيق التاريخ", - "includeTime": "تضمين الوقت", - "isRange": "تاريخ النهاية", - "timeFormat": "تنسيق الوقت", - "clearDate": "مسح التاريخ", - "reminderLabel": "تذكير", - "selectReminder": "حدد التذكير", - "reminderOptions": { - "none": "لا شيء", - "atTimeOfEvent": "وقت الحدث", - "fiveMinsBefore": "قبل 5 دقائق", - "tenMinsBefore": "قبل 10 دقائق", - "fifteenMinsBefore": "قبل 15 دقيقة", - "thirtyMinsBefore": "قبل 30 دقيقة", - "oneHourBefore": "قبل ساعة واحدة", - "twoHoursBefore": "قبل ساعتين", - "onDayOfEvent": "في يوم الحدث", - "oneDayBefore": "قبل يوم واحد", - "twoDaysBefore": "قبل يومين", - "oneWeekBefore": "قبل اسبوع واحد", - "custom": "مخصص" - } + "dateTimeFormatTooltip": "تغيير تنسيق التاريخ والوقت في الإعدادات" }, "relativeDates": { "yesterday": "أمس", @@ -2443,20 +1027,15 @@ "replace": "استبدل", "replaceAll": "استبدال الكل", "noResult": "لا نتائج", - "caseSensitive": "دقة الحروف", - "searchMore": "ابحث للعثور على المزيد من النتائج" + "caseSensitive": "دقة الحروف" }, "error": { "weAreSorry": "آسفون", - "loadingViewError": "نواجه مشكلة في تحميل هذا العرض. يرجى التحقق من اتصالك بالإنترنت، وتحديث التطبيق، ولا تتردد في التواصل مع الفريق إذا استمرت المشكلة.", - "syncError": "لم تتم مزامنة البيانات من جهاز آخر", - "syncErrorHint": "يرجى إعادة فتح هذه الصفحة على الجهاز الذي تم تحريرها عليه آخر مرة، ثم فتحها مرة أخرى على الجهاز الحالي.", - "clickToCopy": "انقر هنا لنسخ كود الخطأ" + "loadingViewError": "نواجه مشكلة في تحميل هذا العرض. يرجى التحقق من اتصالك بالإنترنت، وتحديث التطبيق، ولا تتردد في التواصل مع الفريق إذا استمرت المشكلة." }, "editor": { "bold": "عريض", "bulletedList": "قائمة نقطية", - "bulletedListShortForm": "نقطية", "checkbox": "خانة الاختيار", "embedCode": "كود متضمن", "heading1": "رأسية اولى", @@ -2465,15 +1044,9 @@ "highlight": "ابراز", "color": "لون", "image": "صورة", - "date": "تاريخ", - "page": "صفحة", "italic": "مائل", "link": "رابط", "numberedList": "قائمة مرقمة", - "numberedListShortForm": "مرقمة", - "toggleHeading1ShortForm": "تبديل h1", - "toggleHeading2ShortForm": "تبديل h2", - "toggleHeading3ShortForm": "تبديل h3", "quote": "اقتباس", "strikethrough": "يتوسطه خط", "text": "نص", @@ -2498,8 +1071,6 @@ "backgroundColorPurple": "خلفية بنفسجية", "backgroundColorPink": "خلفية وردية", "backgroundColorRed": "خلفية حمراء", - "backgroundColorLime": "خلفية ليمونية", - "backgroundColorAqua": "خلفية مائية", "done": "تم", "cancel": "الغاء", "tint1": "صبغة 1", @@ -2524,9 +1095,6 @@ "mobileHeading1": "عنوان 1", "mobileHeading2": "العنوان 2", "mobileHeading3": "العنوان 3", - "mobileHeading4": "العنوان 4", - "mobileHeading5": "العنوان 5", - "mobileHeading6": "العنوان 6", "textColor": "لون الخط", "backgroundColor": "لون الخلفية", "addYourLink": "أضف الرابط الخاص بك", @@ -2550,8 +1118,6 @@ "copy": "نسخ", "paste": "لصق", "find": "البحث", - "select": "حدد", - "selectAll": "حدد الكل", "previousMatch": "نتيجة البحث السابقة", "nextMatch": "نتيجة البحث التالية", "closeFind": "اغلاق", @@ -2578,18 +1144,11 @@ "rowDuplicate": "نسخة طبق الاصل", "colClear": "مسح المحتوى", "rowClear": "مسح المحتوى", - "slashPlaceHolder": "اكتب \"/\" لإدراج كتلة، أو ابدأ الكتابة", - "typeSomething": "اكتب شيئا ما...", - "toggleListShortForm": "تبديل", - "quoteListShortForm": "إقتباس", - "mathEquationShortForm": "صيغة", - "codeBlockShortForm": "الكود" + "slashPlaceHolder": "اكتب \"/\" لإدراج كتلة، أو ابدأ الكتابة" }, "favorite": { "noFavorite": "لا توجد صفحة مفضلة", - "noFavoriteHintText": "اسحب الصفحة إلى اليسار لإضافتها إلى المفضلة لديك", - "removeFromSidebar": "إزالة من الشريط الجانبي", - "addToSidebar": "تثبيت على الشريط الجانبي" + "noFavoriteHintText": "اسحب الصفحة إلى اليسار لإضافتها إلى المفضلة لديك" }, "cardDetails": { "notesPlaceholder": "أدخل / لإدراج كتلة، أو ابدأ في الكتابة" @@ -2609,635 +1168,5 @@ "date": "تاريخ", "addField": "إضافة حقل", "userIcon": "رمز المستخدم" - }, - "noLogFiles": "لا توجد ملفات السجل", - "newSettings": { - "myAccount": { - "title": "حسابي", - "subtitle": "قم بتخصيص ملفك الشخصي، وإدارة أمان الحساب، وفتح مفاتيح الذكاء الاصطناعي، أو تسجيل الدخول إلى حسابك.", - "profileLabel": "اسم الحساب وصورة الملف الشخصي", - "profileNamePlaceholder": "أدخل اسمك", - "accountSecurity": "أمان الحساب", - "2FA": "المصادقة بخطوتين", - "aiKeys": "مفاتيح الذكاء الاصطناعي", - "accountLogin": "تسجيل الدخول إلى الحساب", - "updateNameError": "فشل في تحديث الاسم", - "updateIconError": "فشل في تحديث الأيقونة", - "aboutAppFlowy": "حول appName", - "deleteAccount": { - "title": "حذف الحساب", - "subtitle": "احذف حسابك وجميع بياناتك بشكل دائم.", - "description": "احذف حسابك نهائيًا وأزل إمكانية الوصول إلى جميع مساحات العمل.", - "deleteMyAccount": "حذف حسابي", - "dialogTitle": "حذف الحساب", - "dialogContent1": "هل أنت متأكد أنك تريد حذف حسابك نهائياً؟", - "dialogContent2": "لا يمكن التراجع عن هذا الإجراء، وسوف يؤدي إلى إزالة الوصول من جميع مساحات العمل، ومسح حسابك بالكامل، بما في ذلك مساحات العمل الخاصة، وإزالتك من جميع مساحات العمل المشتركة.", - "confirmHint1": "من فضلك اكتب \"@:newSettings.myAccount.deleteAccount.confirmHint3\" للتأكيد.", - "confirmHint2": "أفهم أن هذا الإجراء لا رجعة فيه وسيؤدي إلى حذف حسابي وجميع البيانات المرتبطة به بشكل دائم.", - "confirmHint3": "حذف حسابي", - "checkToConfirmError": "يجب عليك تحديد المربع لتأكيد الحذف", - "failedToGetCurrentUser": "فشل في الحصول على البريد الإلكتروني الحالي للمستخدم", - "confirmTextValidationFailed": "نص التأكيد الخاص بك لا يتطابق مع \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", - "deleteAccountSuccess": "تم حذف الحساب بنجاح" - } - }, - "workplace": { - "name": "مكان العمل", - "title": "إعدادات مكان العمل", - "subtitle": "قم بتخصيص مظهر مساحة العمل الخاصة بك والسمة والخط وتخطيط النص والتاريخ والوقت واللغة.", - "workplaceName": "اسم مكان العمل", - "workplaceNamePlaceholder": "أدخل اسم مكان العمل", - "workplaceIcon": "أيقونة مكان العمل", - "workplaceIconSubtitle": "قم بتحميل صورة أو استخدم رمزًا تعبيريًا لمساحة عملك. سيظهر الرمز في الشريط الجانبي والإشعارات.", - "renameError": "فشل في إعادة تسمية مكان العمل", - "updateIconError": "فشل في تحديث الأيقونة", - "chooseAnIcon": "اختر أيقونة", - "appearance": { - "name": "المظهر", - "themeMode": { - "auto": "آلي", - "light": "فاتح", - "dark": "داكن" - }, - "language": "لغة" - } - }, - "syncState": { - "syncing": "المزامنة", - "synced": "متزامنة", - "noNetworkConnected": "لا يوجد شبكة متصلة" - } - }, - "pageStyle": { - "title": "نمط الصفحة", - "layout": "تَخطِيط", - "coverImage": "صورة الغلاف", - "pageIcon": "أيقونة الصفحة", - "colors": "الألوان", - "gradient": "متدرج", - "backgroundImage": "صورة الخلفية", - "presets": "الإعدادات المسبقة", - "photo": "صورة", - "unsplash": "Unsplash", - "pageCover": "غلاف الصفحة", - "none": "لا شيء", - "openSettings": "افتح الإعدادات", - "photoPermissionTitle": "يرغب @:appName في الوصول إلى مكتبة الصور الخاصة بك", - "photoPermissionDescription": "يحتاج @:appName إلى الوصول إلى صورك للسماح لك بإضافة صور إلى مستنداتك", - "cameraPermissionTitle": "يريد @:appName الوصول إلى الكاميرا الخاصة بك", - "cameraPermissionDescription": "يحتاج @:appName إلى الوصول إلى الكاميرا للسماح لك بإضافة صور إلى مستنداتك من الكاميرا", - "doNotAllow": "لا تسمح", - "image": "صورة" - }, - "commandPalette": { - "placeholder": "ابحث أو اطرح سؤالا...", - "bestMatches": "أفضل المطابقات", - "recentHistory": "التاريخ الحديث", - "navigateHint": "للتنقل", - "loadingTooltip": "نحن نبحث عن نتائج...", - "betaLabel": "النسخة التجريبية", - "betaTooltip": "نحن ندعم حاليًا البحث عن الصفحات والمحتوى في المستندات فقط", - "fromTrashHint": "من سلة المحذوفات", - "noResultsHint": "لم نعثر على ما تبحث عنه، حاول البحث عن مصطلح آخر.", - "clearSearchTooltip": "مسح حقل البحث" - }, - "space": { - "delete": "حذف", - "deleteConfirmation": "حذف: ", - "deleteConfirmationDescription": "سيتم حذف جميع الصفحات الموجودة ضمن هذه المساحة ونقلها إلى سلة المحذوفات، وسيتم إلغاء نشر أي صفحات منشورة.", - "rename": "إعادة تسمية المساحة", - "changeIcon": "تغيير الأيقونة", - "manage": "إدارة المساحة", - "addNewSpace": "إنشاء مساحة", - "collapseAllSubPages": "طي كل الصفحات الفرعية", - "createNewSpace": "إنشاء مساحة جديدة", - "createSpaceDescription": "إنشاء مساحات عامة وخاصة متعددة لتنظيم عملك بشكل أفضل.", - "spaceName": "اسم المساحة", - "spaceNamePlaceholder": "على سبيل المثال التسويق والهندسة والموارد البشرية", - "permission": "إذن المساحة", - "publicPermission": "عام", - "publicPermissionDescription": "جميع أعضاء مساحة العمل لديهم إمكانية الوصول الكامل", - "privatePermission": "خاص", - "privatePermissionDescription": "أنت فقط من يمكنه الوصول إلى هذه المساحة", - "spaceIconBackground": "لون الخلفية", - "spaceIcon": "أيقونة", - "dangerZone": "منطقة الخطر", - "unableToDeleteLastSpace": "غير قادر على حذف المساحة الأخيرة", - "unableToDeleteSpaceNotCreatedByYou": "غير قادر على حذف المساحات التي أنشأها الآخرون", - "enableSpacesForYourWorkspace": "تمكين المساحات لمساحة العمل الخاصة بك", - "title": "المساحات", - "defaultSpaceName": "عام", - "upgradeSpaceTitle": "تمكين المساحات", - "upgradeSpaceDescription": "قم بإنشاء مساحات عامة وخاصة متعددة لتنظيم مساحة عملك بشكل أفضل.", - "upgrade": "تحديث", - "upgradeYourSpace": "إنشاء مساحات متعددة", - "quicklySwitch": "انتقل بسرعة إلى المساحة التالية", - "duplicate": "مساحة مكررة", - "movePageToSpace": "نقل الصفحة إلى المساحة", - "cannotMovePageToDatabase": "لا يمكن نقل الصفحة إلى قاعدة البيانات", - "switchSpace": "تبديل المساحة", - "spaceNameCannotBeEmpty": "لا يمكن أن يكون اسم المساحة فارغًا", - "success": { - "deleteSpace": "تم حذف المساحة بنجاح", - "renameSpace": "تم إعادة تسمية المساحة بنجاح", - "duplicateSpace": "تم تكرار المساحة بنجاح", - "updateSpace": "تم تحديث المساحة بنجاح" - }, - "error": { - "deleteSpace": "فشل في حذف المساحة", - "renameSpace": "فشل في إعادة تسمية المساحة", - "duplicateSpace": "فشل في تكرار المساحة", - "updateSpace": "فشل في تحديث المساحة" - }, - "createSpace": "إنشاء مساحة", - "manageSpace": "إدارة المساحة", - "renameSpace": "إعادة تسمية المساحة", - "mSpaceIconColor": "لون أيقونة المساحة", - "mSpaceIcon": "أيقونة المساحة" - }, - "publish": { - "hasNotBeenPublished": "لم يتم نشر هذه الصفحة بعد", - "spaceHasNotBeenPublished": "لم يتم دعم نشر المساحة بعد", - "reportPage": "صفحة التقرير", - "databaseHasNotBeenPublished": "لم يتم دعم نشر قاعدة البيانات بعد.", - "createdWith": "تم إنشاؤه باستخدام", - "downloadApp": "تنزيل AppFlowy", - "copy": { - "codeBlock": "تم نسخ محتوى كتلة الكود إلى الحافظة", - "imageBlock": "تم نسخ رابط الصورة إلى الحافظة", - "mathBlock": "تم نسخ المعادلة الرياضية إلى الحافظة", - "fileBlock": "تم نسخ رابط الملف إلى الحافظة" - }, - "containsPublishedPage": "تحتوي هذه الصفحة على صفحة واحدة أو أكثر منشورة. إذا تابعت، فسيتم إلغاء نشرها. هل تريد متابعة الحذف؟", - "publishSuccessfully": "تم النشر بنجاح", - "unpublishSuccessfully": "تم إلغاء النشر بنجاح", - "publishFailed": "فشل في النشر", - "unpublishFailed": "فشل في إلغاء النشر", - "noAccessToVisit": "لا يمكن الوصول إلى هذه الصفحة...", - "createWithAppFlowy": "إنشاء موقع ويب مع AppFlowy", - "fastWithAI": "سريع وسهل مع الذكاء الاصطناعي.", - "tryItNow": "جربها الآن", - "onlyGridViewCanBePublished": "لا يمكن نشر سوى عرض الشبكة", - "database": { - "zero": "نشر {} عرض محدد", - "one": "نشر {} عروض محددة", - "many": "نشر {} عروض محددة", - "other": "نشر {} عروض محددة" - }, - "mustSelectPrimaryDatabase": "يجب تحديد العرض الأساسي", - "noDatabaseSelected": "لم يتم تحديد أي قاعدة بيانات، يرجى تحديد قاعدة بيانات واحدة على الأقل.", - "unableToDeselectPrimaryDatabase": "غير قادر على إلغاء تحديد قاعدة البيانات الأساسية", - "saveThisPage": "ابدأ بهذا القالب", - "duplicateTitle": "أين تريد أن تضيف", - "selectWorkspace": "حدد مساحة العمل", - "addTo": "أضف إلى", - "duplicateSuccessfully": "تمت إضافته إلى مساحة العمل الخاصة بك", - "duplicateSuccessfullyDescription": "لم تقم بتثبيت AppFlowy؟ سيبدأ التنزيل تلقائيًا بعد النقر فوق \"تنزيل\".", - "downloadIt": "تنزيل", - "openApp": "افتح في التطبيق", - "duplicateFailed": "مكررة فشلت", - "membersCount": { - "zero": "لا يوجد أعضاء", - "one": "1 عضو", - "many": "{count} عضوا", - "other": "{count} عضوا" - }, - "useThisTemplate": "استخدم القالب" - }, - "web": { - "continue": "متابعة", - "or": "أو", - "continueWithGoogle": "متابعة مع جوجل", - "continueWithGithub": "متابعة مع GitHub", - "continueWithDiscord": "متابعة مع Discord", - "continueWithApple": "متابعة مع Apple ", - "moreOptions": "المزيد من الخيارات", - "collapse": "طي", - "signInAgreement": "بالنقر فوق \"متابعة\" أعلاه، فإنك توافق على شروط استخدام AppFlowy", - "signInLocalAgreement": "من خلال النقر على \"البدء\" أعلاه، فإنك توافق على شروط وأحكام AppFlowy", - "and": "و", - "termOfUse": "شروط", - "privacyPolicy": "سياسة الخصوصية", - "signInError": "خطأ في تسجيل الدخول", - "login": "سجل أو قم بتسجيل الدخول", - "fileBlock": { - "uploadedAt": "تم الرفع في {time}", - "linkedAt": "تمت إضافة الرابط في {time}", - "empty": "رفع أو تضمين ملف", - "uploadFailed": "فشل الرفع، يرجى المحاولة مرة أخرى", - "retry": "إعادة المحاولة" - }, - "importNotion": "الاستيراد من Notion", - "import": "الاستيراد", - "importSuccess": "تم الرفع بنجاح", - "importSuccessMessage": "سنخطرك عند اكتمال عملية الاستيراد. بعد ذلك، يمكنك عرض الصفحات المستوردة في الشريط الجانبي.", - "importFailed": "فشل الاستيراد، يرجى التحقق من تنسيق الملف", - "dropNotionFile": "قم بإسقاط ملف Notion zip الخاص بك هنا للرفع، أو انقر للاستعراض", - "error": { - "pageNameIsEmpty": "اسم الصفحة فارغ، الرجاء تجربة صفحة أخرى" - } - }, - "globalComment": { - "comments": "تعليقات", - "addComment": "أضف تعليق", - "reactedBy": "تفاعل من قبل", - "addReaction": "إضافة تفاعل", - "reactedByMore": "و {count} آخرين", - "showSeconds": { - "one": "منذ ثانية واحدة", - "other": "منذ {count} ثانية", - "zero": "الآن", - "many": "منذ {count} ثانية" - }, - "showMinutes": { - "one": "منذ دقيقة واحدة", - "other": "منذ {count} دقيقة", - "many": "منذ {count} دقيقة" - }, - "showHours": { - "one": "منذ ساعة واحدة", - "other": "منذ {count} ساعة", - "many": "منذ {count} ساعة" - }, - "showDays": { - "one": "منذ يوم واحد", - "other": "منذ {count} يوم", - "many": "منذ {count} يوم" - }, - "showMonths": { - "one": "منذ شهر واحد", - "other": "منذ {count} شهر", - "many": "منذ {count} شهر" - }, - "showYears": { - "one": "منذ سنة واحدة", - "other": "منذ {count} سنة", - "many": "منذ {count} سنة" - }, - "reply": "رد", - "deleteComment": "حذف التعليق", - "youAreNotOwner": "أنت لست صاحب هذا التعليق", - "confirmDeleteDescription": "هل أنت متأكد أنك تريد حذف هذا التعليق؟", - "hasBeenDeleted": "تم الحذف", - "replyingTo": "الرد على", - "noAccessDeleteComment": "لا يجوز لك حذف هذا التعليق", - "collapse": "طي", - "readMore": "اقرأ المزيد", - "failedToAddComment": "فشل في إضافة التعليق", - "commentAddedSuccessfully": "تمت إضافة التعليق بنجاح.", - "commentAddedSuccessTip": "لقد قمت للتو بإضافة تعليق أو الرد عليه. هل ترغب في الانتقال إلى الأعلى لمشاهدة أحدث التعليقات؟" - }, - "template": { - "asTemplate": "حفظ كقالب", - "name": "اسم القالب", - "description": "وصف القالب", - "about": "قالب حول", - "deleteFromTemplate": "حذف من القوالب", - "preview": "معاينة القالب", - "categories": "فئات القوالب", - "isNewTemplate": "PIN إلى قالب جديد", - "featured": "PIN إلى المميز", - "relatedTemplates": "القوالب ذات الصلة", - "requiredField": "{field} مطلوب", - "addCategory": "أضف \"{category}\"", - "addNewCategory": "إضافة فئة جديدة", - "addNewCreator": "إضافة مبدع جديد", - "deleteCategory": "حذف الفئة", - "editCategory": "تعديل الفئة", - "editCreator": "مبدع التعديل", - "category": { - "name": "اسم الفئة", - "icon": "أيقونة الفئة", - "bgColor": "لون خلفية الفئة", - "priority": "أولوية الفئة", - "desc": "وصف الفئة", - "type": "نوع الفئة", - "icons": "أيقونات الفئة", - "colors": "فئة الألوان", - "byUseCase": "حسب حالة الاستخدام", - "byFeature": "حسب الميزة", - "deleteCategory": "حذف الفئة", - "deleteCategoryDescription": "هل أنت متأكد أنك تريد حذف هذه الفئة؟", - "typeToSearch": "اكتب للبحث عن الفئات..." - }, - "creator": { - "label": "مبدع القالب", - "name": "اسم المنشئ", - "avatar": "الصورة الرمزية للمبدع", - "accountLinks": "روابط حسابات المبدع", - "uploadAvatar": "انقر هنا لتحميل الصورة الرمزية", - "deleteCreator": "حذف المبدع", - "deleteCreatorDescription": "هل أنت متأكد أنك تريد حذف هذا المبدع؟", - "typeToSearch": "اكتب للبحث عن المبدعين..." - }, - "uploadSuccess": "تم تحميل القالب بنجاح", - "uploadSuccessDescription": "لقد تم رفع القالب الخاص بك بنجاح. يمكنك الآن عرضه في معرض القوالب.", - "viewTemplate": "عرض القالب", - "deleteTemplate": "حذف القالب", - "deleteSuccess": "تم حذف القالب بنجاح", - "deleteTemplateDescription": "لن يؤثر هذا على الصفحة الحالية أو حالة النشر. هل أنت متأكد من أنك تريد حذف هذا القالب؟", - "addRelatedTemplate": "إضافة قالب ذو صلة", - "removeRelatedTemplate": "إزالة القالب ذو الصلة", - "uploadAvatar": "رفع الصورة الرمزية", - "searchInCategory": "البحث في {category}", - "label": "القوالب" - }, - "fileDropzone": { - "dropFile": "انقر أو اسحب الملف إلى هذه المنطقة لتحميله", - "uploading": "جاري الرفع...", - "uploadFailed": "فشل الرفع", - "uploadSuccess": "تم الرفع بنجاح", - "uploadSuccessDescription": "تم رفع الملف بنجاح", - "uploadFailedDescription": "فشل رفع الملف", - "uploadingDescription": "جاري رفع الملف" - }, - "gallery": { - "preview": "افتح في شاشة كاملة", - "copy": "نسخ", - "download": "تنزيل", - "prev": "السابق", - "next": "التالي", - "resetZoom": "إعادة ضبط التكبير", - "zoomIn": "التكبير ", - "zoomOut": "التصغير" - }, - "invitation": { - "join": "الانضمام", - "on": "على", - "invitedBy": "بدعوة من", - "membersCount": { - "zero": "{count} عضوا", - "one": "{count} عضو", - "many": "{count} عضوا", - "other": "{count} عضوا" - }, - "tip": "لقد تمت دعوتك للانضمام إلى مساحة العمل هذه باستخدام معلومات الاتصال أدناه. إذا كانت هذه المعلومات غير صحيحة، فاتصل بالمسؤول لإعادة إرسال الدعوة.", - "joinWorkspace": "انضم إلى مساحة العمل", - "success": "لقد انضممت بنجاح إلى مساحة العمل", - "successMessage": "يمكنك الآن الوصول إلى كافة الصفحات ومساحات العمل الموجودة بداخله.", - "openWorkspace": "افتح AppFlowy", - "alreadyAccepted": "لقد قبلت الدعوة بالفعل", - "errorModal": { - "title": "لقد حدث خطأ ما", - "description": "قد لا يكون لحسابك الحالي {email} حق الوصول إلى مساحة العمل هذه. يرجى تسجيل الدخول بالحساب الصحيح أو الاتصال بمالك مساحة العمل للحصول على المساعدة.", - "contactOwner": "اتصل بالمالك", - "close": "العودة إلى الرئيسية", - "changeAccount": "تغيير الحساب" - } - }, - "requestAccess": { - "title": "لا يمكن الوصول إلى هذه الصفحة", - "subtitle": "يمكنك طلب الوصول من مالك هذه الصفحة. بمجرد الموافقة، يمكنك عرض الصفحة.", - "requestAccess": "طلب الوصول", - "backToHome": "العودة إلى الرئيسية", - "tip": "لقد قمت بتسجيل الدخول حاليًا باسم<link/> .", - "mightBe": "قد تحتاج إلى<login/> مع حساب مختلف.", - "successful": "تم إرسال الطلب بنجاح", - "successfulMessage": "سيتم إعلامك بمجرد موافقة المالك على طلبك.", - "requestError": "فشل في طلب الوصول", - "repeatRequestError": "لقد طلبت بالفعل الوصول إلى هذه الصفحة" - }, - "approveAccess": { - "title": "الموافقة على طلب الانضمام إلى مساحة العمل", - "requestSummary": "<user/>طلبات الانضمام<workspace/> والوصول<page/>", - "upgrade": "ترقية", - "downloadApp": "تنزيل AppFlowy", - "approveButton": "موافقة", - "approveSuccess": "تمت الموافقة بنجاح", - "approveError": "فشل في الموافقة، تأكد من عدم تجاوز حد خطة مساحة العمل", - "getRequestInfoError": "فشل في الحصول على معلومات الطلب", - "memberCount": { - "zero": "لا يوجد أعضاء", - "one": "عضو واحد", - "many": "{count} عضوا", - "other": "{count} عضوا" - }, - "alreadyProTitle": "لقد وصلت إلى الحد الأقصى لخطة مساحة العمل", - "alreadyProMessage": "اطلب منهم الاتصال<email/> لإلغاء تأمين المزيد من الأعضاء", - "repeatApproveError": "لقد وافقت بالفعل على هذا الطلب", - "ensurePlanLimit": "تأكد من عدم تجاوز حد خطة مساحة العمل. إذا تم تجاوز الحد، ففكر في<upgrade/> خطة مساحة العمل أو<download/> .", - "requestToJoin": "طلب الانضمام", - "asMember": "كعضو" - }, - "upgradePlanModal": { - "title": "الترقية إلى الإصدار الاحترافي", - "message": "وصل {name} إلى الحد الأقصى للعضوية المجانية. قم بالترقية إلى الخطة الاحترافية لدعوة المزيد من الأعضاء.", - "upgradeSteps": "كيفية ترقية خطتك على AppFlowy:", - "step1": "1. انتقل إلى الإعدادات", - "step2": "2. انقر فوق \"الخطة\"", - "step3": "3. حدد \"تغيير الخطة\"", - "appNote": "ملحوظة: ", - "actionButton": "ترقية", - "downloadLink": "تنزيل التطبيق", - "laterButton": "لاحقاً", - "refreshNote": "بعد الترقية الناجحة، انقر فوق<refresh/> لتفعيل ميزاتك الجديدة.", - "refresh": "هنا" - }, - "breadcrumbs": { - "label": "مسارات التنقل" - }, - "time": { - "justNow": "الآن", - "seconds": { - "one": "ثانية واحدة", - "other": "{count} من الثواني" - }, - "minutes": { - "one": "دقيقة واحدة", - "other": "{count} من الدقائق" - }, - "hours": { - "one": "ساعة واحدة", - "other": "{count} من الساعات" - }, - "days": { - "one": "يوم واحد", - "other": "{count} من الأيام" - }, - "weeks": { - "one": "اسبوع واحد", - "other": "{count} من الأسابيع" - }, - "months": { - "one": "شهر واحد", - "other": "{count} من الاشهر" - }, - "years": { - "one": "سنة واحدة", - "other": "{count} من السنوات" - }, - "ago": "منذ", - "yesterday": "أمس", - "today": "اليوم" - }, - "members": { - "zero": "لا يوجد أعضاء", - "one": "عضو واحد", - "many": "{count} من الأعضاء", - "other": "{count} من الأعضاء" - }, - "tabMenu": { - "close": "إغلاق", - "closeDisabledHint": "لا يمكن إغلاق علامة تبويب مثبتة، يرجى إلغاء التثبيت أولاً", - "closeOthers": "إغلاق علامات التبويب الأخرى", - "closeOthersHint": "سيؤدي هذا إلى إغلاق جميع علامات التبويب غير المثبتة باستثناء هذه العلامة", - "closeOthersDisabledHint": "تم تثبيت جميع علامات التبويب، ولا يمكن العثور على أي علامات تبويب لإغلاقها", - "favorite": "مفضل", - "unfavorite": "غير مفضل", - "favoriteDisabledHint": "لا يمكن إضافة هذا العرض إلى المفضلة", - "pinTab": "تثبيت", - "unpinTab": "إزالة التثبيت" - }, - "openFileMessage": { - "success": "تم فتح الملف بنجاح", - "fileNotFound": "لم يتم العثور على الملف", - "noAppToOpenFile": "لا يوجد تطبيق لفتح هذا الملف", - "permissionDenied": "لا يوجد إذن لفتح هذا الملف", - "unknownError": "فشل فتح الملف" - }, - "inviteMember": { - "requestInviteMembers": "دعوة إلى مكان مساحة عملك", - "inviteFailedMemberLimit": "لقد تم الوصول إلى الحد الأقصى للأعضاء، يرجى ", - "upgrade": "ترقية", - "addEmail": "email@example.com، email2@example.com...", - "requestInvites": "إرسال الدعوات", - "inviteAlready": "لقد قمت بالفعل بدعوة هذا البريد الإلكتروني: {email}", - "inviteSuccess": "تم إرسال الدعوة بنجاح", - "description": "أدخل رسائل البريد الإلكتروني أدناه مع وضع فاصلة بينها. تعتمد الرسوم على عدد الأعضاء.", - "emails": "بريد إلكتروني" - }, - "quickNote": { - "label": "ملاحظة سريعة", - "quickNotes": "ملاحظات سريعة", - "search": "بحث ملاحظات سريعة", - "collapseFullView": "طي العرض الكامل", - "expandFullView": "توسيع العرض الكامل", - "createFailed": "فشل في إنشاء ملاحظة سريعة", - "quickNotesEmpty": "لا توجد ملاحظات سريعة", - "emptyNote": "ملاحظة فارغة", - "deleteNotePrompt": "سيتم حذف الملاحظة المحددة بشكل دائم. هل أنت متأكد من أنك تريد حذفها؟", - "addNote": "ملاحظة جديدة", - "noAdditionalText": "لا يوجد نص إضافي" - }, - "subscribe": { - "upgradePlanTitle": "قارن واختر الخطة", - "yearly": "سنوي", - "save": "وفر {discount}%", - "monthly": "شهريا", - "priceIn": "السعر في ", - "free": "مجاني", - "pro": "احترافي", - "freeDescription": "للأفراد حتى عضوين لتنظيم كل شيء", - "proDescription": "للفرق الصغيرة لإدارة المشاريع ومعرفة الفريق", - "proDuration": { - "monthly": "لكل عضو شهريا\nيتم دفع الفاتورة شهريا", - "yearly": "لكل عضو شهريا\nيتم دفعها سنويا" - }, - "cancel": "يرجع إلى إصدار أقدم", - "changePlan": "الترقية إلى الخطة الاحترافية", - "everythingInFree": "كل شيء مجاني +", - "currentPlan": "الحالي", - "freeDuration": "للأبد", - "freePoints": { - "first": "مساحة عمل تعاونية واحدة تتسع لعضوين كحد أقصى", - "second": "صفحات وكتل غير محدودة", - "three": "سعة تخزين 5 جيجا بايت", - "four": "البحث الذكي", - "five": "20 استجابة الذكاء الاصطناعي", - "six": "تطبيق الجوال", - "seven": "التعاون في الوقت الحقيقي" - }, - "proPoints": { - "first": "تخزين غير محدود", - "second": "ما يصل إلى 10 أعضاء في مساحة العمل", - "three": "استجابات الذكاء الاصطناعي غير المحدودة", - "four": "رفع ملفات غير محدودة", - "five": "مساحة اسم مخصصة" - }, - "cancelPlan": { - "title": "نأسف على مغادرتك", - "success": "لقد تم إلغاء اشتراكك بنجاح", - "description": "يؤسفنا رحيلك. يسعدنا سماع تعليقاتك لمساعدتنا على تحسين AppFlowy. يُرجى تخصيص بعض الوقت للإجابة على بعض الأسئلة.", - "commonOther": "آخر", - "otherHint": "اكتب إجابتك هنا", - "questionOne": { - "question": "ما الذي دفعك إلى إلغاء اشتراكك في AppFlowy Pro؟", - "answerOne": "التكلفة مرتفعة للغاية", - "answerTwo": "الميزات لم ترق إلى مستوى التوقعات", - "answerThree": "وجدت بديلا أفضل", - "answerFour": "لم أستخدمه بشكل كافي لتبرير التكلفة", - "answerFive": "مشكلة في الخدمة أو صعوبات فنية" - }, - "questionTwo": { - "question": "ما مدى احتمالية تفكيرك في إعادة الاشتراك في AppFlowy Pro في المستقبل؟", - "answerOne": "من المرجح جدًا", - "answerTwo": "من المحتمل إلى حد ما", - "answerThree": "غير متأكد", - "answerFour": "من غير المحتمل", - "answerFive": "من غير المحتمل جدًا" - }, - "questionThree": { - "question": "ما هي الميزة الاحترافية التي تقدرها أكثر أثناء اشتراكك؟", - "answerOne": "التعاون بين المستخدمين المتعددين", - "answerTwo": "سجل تاريخ الإصدارات لفترة أطول", - "answerThree": "استجابات الذكاء الاصطناعي غير المحدودة", - "answerFour": "الوصول إلى نماذج الذكاء الاصطناعي المحلية" - }, - "questionFour": { - "question": "كيف تصف تجربتك الشاملة مع AppFlowy؟", - "answerOne": "عظيمة", - "answerTwo": "جيدة", - "answerThree": "متوسطة", - "answerFour": "أقل من المتوسط", - "answerFive": "غير راضٍ" - } - } - }, - "ai": { - "contentPolicyViolation": "فشل إنشاء الصورة بسبب محتوى حساس. يرجى إعادة صياغة إدخالك والمحاولة مرة أخرى", - "textLimitReachedDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. قم بالترقية إلى الخطة الاحترافية أو قم بشراء إضافة للذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "imageLimitReachedDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يُرجى الترقية إلى الخطة الاحترافية أو شراء إضافة الذكاء الاصطناعي لفتح عدد غير محدود من الاستجابات", - "limitReachedAction": { - "textDescription": "لقد نفدت الاستجابات المجانية للذكاء الاصطناعي من مساحة عملك. للحصول على المزيد من الاستجابات، يرجى", - "imageDescription": "لقد استنفدت حصتك المجانية من صور الذكاء الاصطناعي. يرجى", - "upgrade": "ترقية", - "toThe": "الى", - "proPlan": "الخطة الاحترافية", - "orPurchaseAn": "أو شراء", - "aiAddon": "مَرافِق الذكاء الاصطناعي" - }, - "editing": "تحرير", - "analyzing": "تحليل", - "continueWritingEmptyDocumentTitle": "استمر في كتابة الخطأ", - "continueWritingEmptyDocumentDescription": "نواجه مشكلة في توسيع نطاق المحتوى في مستندك. اكتب مقدمة قصيرة وسنتولى الأمر من هناك!", - "more": "أكثر" - }, - "autoUpdate": { - "criticalUpdateTitle": "التحديث ضروري للمتابعة", - "criticalUpdateDescription": "لقد أجرينا تحسينات لتحسين تجربتك! يُرجى التحديث من {currentVersion} إلى {newVersion} لمواصلة استخدام التطبيق.", - "criticalUpdateButton": "تحديث", - "bannerUpdateTitle": "النسخة الجديدة متاحة!", - "bannerUpdateDescription": "احصل على أحدث الميزات والإصلاحات. انقر على \"تحديث\" للتثبيت الآن.", - "bannerUpdateButton": "تحديث", - "settingsUpdateTitle": "الإصدار الجديد ({newVersion}) متاح!", - "settingsUpdateDescription": "الإصدار الحالي: {currentVersion} (الإصدار الرسمي) → {newVersion}", - "settingsUpdateButton": "تحديث", - "settingsUpdateWhatsNew": "ما الجديد" - }, - "lockPage": { - "lockPage": "مقفل", - "reLockPage": "إعادة القفل", - "lockTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود. انقر لفتح القفل.", - "pageLockedToast": "الصفحة مقفلة. التعديل معطل حتى يفتحها أحد.", - "lockedOperationTooltip": "تم قفل الصفحة لمنع التعديل غير المقصود." - }, - "suggestion": { - "accept": "يقبل", - "keep": "يحفظ", - "discard": "تجاهل", - "close": "يغلق", - "tryAgain": "حاول ثانية", - "rewrite": "إعادة كتابة", - "insertBelow": "أدخل أدناه" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 8d98cb5cbc..d00c2c4114 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -133,14 +133,14 @@ "questionBubble": { "shortcuts": "Dreceres", "whatsNew": "Què hi ha de nou?", + "help": "Ajuda i Suport", "markdown": "Reducció", "debug": { "name": "Informació de depuració", "success": "S'ha copiat la informació de depuració!", "fail": "No es pot copiar la informació de depuració" }, - "feedback": "Feedback", - "help": "Ajuda i Suport" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Suprimeix, canvia el nom i més...", @@ -278,7 +278,8 @@ "inputEncryptPrompt": "Introduïu el vostre secret de xifratge per a", "clickToCopySecret": "Feu clic per copiar el secret", "inputTextFieldHint": "El teu secret", - "importSuccess": "S'ha importat correctament la carpeta de dades d'@:appName" + "importSuccess": "S'ha importat correctament la carpeta de dades d'@:appName", + "supabaseSetting": "Configuració Supabase" }, "notifications": { "enableNotifications": { @@ -382,7 +383,11 @@ "email": "Correu electrònic", "tooltipSelectIcon": "Seleccioneu la icona", "selectAnIcon": "Seleccioneu una icona", - "pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau AI" + "pleaseInputYourOpenAIKey": "si us plau, introduïu la vostra clau OpenAI" + }, + "shortcuts": { + "command": "Comandament", + "addNewCommand": "Afegeix una comanda nova" }, "mobile": { "personalInfo": "Informació personal", @@ -397,10 +402,6 @@ "userprofileError": "No s'ha pogut carregar el perfil d'usuari", "selectStartingDay": "Seleccioneu el dia d'inici", "version": "Versió" - }, - "shortcuts": { - "command": "Comandament", - "addNewCommand": "Afegeix una comanda nova" } }, "grid": { @@ -601,23 +602,23 @@ "referencedBoard": "Junta de referència", "referencedGrid": "Quadrícula de referència", "referencedCalendar": "Calendari de referència", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Demana a AI que escrigui qualsevol cosa...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Demana a AI que escrigui qualsevol cosa...", "autoGeneratorLearnMore": "Aprèn més", "autoGeneratorGenerate": "Generar", - "autoGeneratorHintText": "Pregunta a AI...", - "autoGeneratorCantGetOpenAIKey": "No es pot obtenir la clau AI", + "autoGeneratorHintText": "Pregunta a OpenAI...", + "autoGeneratorCantGetOpenAIKey": "No es pot obtenir la clau OpenAI", "autoGeneratorRewrite": "Reescriure", "smartEdit": "Assistents d'IA", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Corregir l'ortografia", "warning": "⚠️ Les respostes de la IA poden ser inexactes o enganyoses.", "smartEditSummarize": "Resumir", "smartEditImproveWriting": "Millorar l'escriptura", "smartEditMakeLonger": "Fer més llarg", - "smartEditCouldNotFetchResult": "No s'ha pogut obtenir el resultat d'AI", - "smartEditCouldNotFetchKey": "No s'ha pogut obtenir la clau AI", - "smartEditDisabled": "Connecteu AI a Configuració", + "smartEditCouldNotFetchResult": "No s'ha pogut obtenir el resultat d'OpenAI", + "smartEditCouldNotFetchKey": "No s'ha pogut obtenir la clau OpenAI", + "smartEditDisabled": "Connecteu OpenAI a Configuració", "discardResponse": "Voleu descartar les respostes d'IA?", "createInlineMathEquation": "Crea una equació", "fonts": "Fonts", @@ -670,8 +671,8 @@ "defaultColor": "Per defecte" }, "image": { - "addAnImage": "Afegeix una imatge", - "copiedToPasteBoard": "L'enllaç de la imatge s'ha copiat al porta-retalls" + "copiedToPasteBoard": "L'enllaç de la imatge s'ha copiat al porta-retalls", + "addAnImage": "Afegeix una imatge" }, "outline": { "addHeadingToCreateOutline": "Afegiu títols per crear una taula de continguts." @@ -713,7 +714,7 @@ "placeholder": "Introduïu l'URL de la imatge" }, "ai": { - "label": "Generar imatge des d'AI" + "label": "Generar imatge des d'OpenAI" }, "support": "El límit de mida de la imatge és de 5 MB. Formats admesos: JPEG, PNG, GIF, SVG", "error": { @@ -811,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index acfc571536..cb8befcd8a 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -170,14 +170,14 @@ "questionBubble": { "shortcuts": "کورتە ڕێگاکان", "whatsNew": "نوێترین", + "help": "پشتیوانی و یارمەتی", "markdown": "Markdown", "debug": { "name": "زانیاری دیباگ", "success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!", "fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد" }, - "feedback": "فیدباک", - "help": "پشتیوانی و یارمەتی" + "feedback": "فیدباک" }, "menuAppHeader": { "moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...", @@ -331,6 +331,11 @@ "cloudServerType": "ڕاژەکاری کڵاود", "cloudServerTypeTip": "تکایە ئاگاداربە کە لەوانەیە دوای گۆڕینی ڕاژەکاری کڵاودکە لە ئەکاونتی ئێستات دەربچێت", "cloudLocal": "خۆماڵی", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "url ی supabase ناتوانێت بەتاڵ بێت", + "cloudSupabaseAnonKey": "کلیلی شاراوەی Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "کلیلی anon ناتوانێت بەتاڵ بێت", "cloudAppFlowy": "ئەپفلۆوی کلاود بێتا", "cloudAppFlowySelfHost": "ئەپفلۆوی کلاود بە هۆستی خۆیی", "appFlowyCloudUrlCanNotBeEmpty": "url ی هەور ناتوانێت بەتاڵ بێت", @@ -475,9 +480,20 @@ "email": "ئیمەیڵ", "tooltipSelectIcon": "هەڵبژاەدنی وێنۆچكه‌", "selectAnIcon": "هەڵبژاردنی وێنۆچكه‌", - "pleaseInputYourOpenAIKey": "تکایە کلیلی AI ـەکەت بنووسە", - "clickToLogout": "بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە", - "pleaseInputYourStabilityAIKey": "تکایە جێگیری کلیلی AI ـەکەت بنووسە" + "pleaseInputYourOpenAIKey": "تکایە کلیلی OpenAI ـەکەت بنووسە", + "pleaseInputYourStabilityAIKey": "تکایە جێگیری کلیلی AI ـەکەت بنووسە", + "clickToLogout": "بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە" + }, + "shortcuts": { + "shortcutsLabel": "کورتە ڕێگاکان", + "command": "فەرمان", + "keyBinding": "کورتکراوەکانی تەختەکلیل", + "addNewCommand": "زیاد کردنی فەرمانێکی نوێ", + "updateShortcutStep": "تێکەڵەی کلیلی دڵخواز داگرە و ENTER داگرە", + "shortcutIsAlreadyUsed": "ئەم کورتە ڕێگایە پێشتر بۆ: {conflict} بەکارهاتووە.", + "resetToDefault": "گەڕاندنەوە بۆ کلیلەکانی بنه‌ڕه‌ت", + "couldNotLoadErrorMsg": "کورتە ڕێگاکان نەتوانرا باربکرێن، تکایە دووبارە هەوڵبدەرەوە", + "couldNotSaveErrorMsg": "کورتە ڕێگاکان نەتوانرا پاشەکەوت بکرێن، تکایە دووبارە هەوڵبدەرەوە" }, "mobile": { "personalInfo": "زانیاری کەسی", @@ -495,17 +511,6 @@ "selectLayout": "نەخشە هەڵبژێرە", "selectStartingDay": "ڕۆژی دەستپێکردنەکەت هەڵبژێرە", "version": "وەشان" - }, - "shortcuts": { - "shortcutsLabel": "کورتە ڕێگاکان", - "command": "فەرمان", - "keyBinding": "کورتکراوەکانی تەختەکلیل", - "addNewCommand": "زیاد کردنی فەرمانێکی نوێ", - "updateShortcutStep": "تێکەڵەی کلیلی دڵخواز داگرە و ENTER داگرە", - "shortcutIsAlreadyUsed": "ئەم کورتە ڕێگایە پێشتر بۆ: {conflict} بەکارهاتووە.", - "resetToDefault": "گەڕاندنەوە بۆ کلیلەکانی بنه‌ڕه‌ت", - "couldNotLoadErrorMsg": "کورتە ڕێگاکان نەتوانرا باربکرێن، تکایە دووبارە هەوڵبدەرەوە", - "couldNotSaveErrorMsg": "کورتە ڕێگاکان نەتوانرا پاشەکەوت بکرێن، تکایە دووبارە هەوڵبدەرەوە" } }, "grid": { @@ -729,22 +734,23 @@ "referencedGrid": "تۆڕی ئاماژەپێکراو", "referencedCalendar": "ساڵنامەی ئاماژەپێکراو", "referencedDocument": "بەڵگەنامەی ئاماژەپێکراو", - "autoGeneratorMenuItemName": "AI نووسەری", + "autoGeneratorMenuItemName": "OpenAI نووسەری", "autoGeneratorTitleName": "داوا لە AI بکە هەر شتێک بنووسێت...", "autoGeneratorLearnMore": "زیاتر زانین", "autoGeneratorGenerate": "بنووسە", - "autoGeneratorHintText": "لە AI پرسیار بکە...", - "autoGeneratorCantGetOpenAIKey": "نەتوانرا کلیلی AI بەدەست بهێنرێت", + "autoGeneratorHintText": "لە OpenAI پرسیار بکە...", + "autoGeneratorCantGetOpenAIKey": "نەتوانرا کلیلی OpenAI بەدەست بهێنرێت", "autoGeneratorRewrite": "دووبارە نووسینەوە", "smartEdit": "یاریدەدەری زیرەک", + "openAI": "OpenAI ژیری دەستکرد", "smartEditFixSpelling": "ڕاستکردنەوەی نووسین", "warning": "⚠️ وەڵامەکانی AI دەتوانن هەڵە یان چەواشەکارانە بن", "smartEditSummarize": "کورتەنووسی", "smartEditImproveWriting": "پێشخستن نوووسین", "smartEditMakeLonger": "درێژتری بکەرەوە", - "smartEditCouldNotFetchResult": "هیچ ئەنجامێک لە AI وەرنەگیرا", - "smartEditCouldNotFetchKey": "نەتوانرا کلیلی AI بهێنێتە ئاراوە", - "smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە AI بکە", + "smartEditCouldNotFetchResult": "هیچ ئەنجامێک لە OpenAI وەرنەگیرا", + "smartEditCouldNotFetchKey": "نەتوانرا کلیلی OpenAI بهێنێتە ئاراوە", + "smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە OpenAI بکە", "discardResponse": "ئایا دەتەوێت وەڵامەکانی AI بسڕیتەوە؟", "createInlineMathEquation": "درووست کردنی هاوکێشە", "fonts": "فۆنتەکان", @@ -799,8 +805,7 @@ }, "outline": { "addHeadingToCreateOutline": "بۆ دروستکردنی خشتەی ناوەڕۆک سەردێڕەکان داخڵ بکە" - }, - "openAI": "AI ژیری دەستکرد" + } }, "textBlock": { "placeholder": "بۆ فەرمانەکان '/' بنووسە" @@ -941,4 +946,4 @@ "frequentlyUsed": "زۆرجار بەکارت هێناوە" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 28750dd542..dab1d101e2 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -134,14 +134,14 @@ "questionBubble": { "shortcuts": "Klávesové zkratky", "whatsNew": "Co je nového?", + "help": "Pomoc a podpora", "markdown": "Markdown", "debug": { "name": "Debug informace", "success": "Debug informace zkopírovány do schránky!", "fail": "Nepodařilo se zkopáí" }, - "feedback": "Zpětná vazba", - "help": "Pomoc a podpora" + "feedback": "Zpětná vazba" }, "menuAppHeader": { "moreButtonToolTip": "Smazat, přejmenovat, a další...", @@ -378,9 +378,20 @@ "email": "E-mail", "tooltipSelectIcon": "Vyberte ikonu", "selectAnIcon": "Vyberte ikonu", - "pleaseInputYourOpenAIKey": "Prosím vložte svůj AI klíč", - "clickToLogout": "Klin", - "pleaseInputYourStabilityAIKey": "Prosím vložte svůj Stability AI klíč" + "pleaseInputYourOpenAIKey": "Prosím vložte svůj OpenAI klíč", + "pleaseInputYourStabilityAIKey": "Prosím vložte svůj Stability AI klíč", + "clickToLogout": "Klin" + }, + "shortcuts": { + "shortcutsLabel": "Klávesové zkratky", + "command": "Příkaz", + "keyBinding": "Přiřazená klávesa", + "addNewCommand": "Přidat nový příkaz", + "updateShortcutStep": "Stiskněte požadovanou kombinaci kláves a stiskněte ENTER", + "shortcutIsAlreadyUsed": "Tato zkratka je již použita pro: @@", + "resetToDefault": "Obnovit výchozí klávesové zkratky", + "couldNotLoadErrorMsg": "Nepodařilo se načíst klávesové zkratky, zkuste to znovu", + "couldNotSaveErrorMsg": "Nepodařilo seuložit klávesové zkta" }, "mobile": { "personalInfo": "Osobní informace", @@ -394,17 +405,6 @@ "userAgreement": "Uživatels", "userprofileError": "Nepodařilo se načíst uživatelský profil", "userprofileErrorDescription": "Prosím zkuste se odhlásit a znovu přihlásit a zkontrolujte, zda problém přetrvává" - }, - "shortcuts": { - "shortcutsLabel": "Klávesové zkratky", - "command": "Příkaz", - "keyBinding": "Přiřazená klávesa", - "addNewCommand": "Přidat nový příkaz", - "updateShortcutStep": "Stiskněte požadovanou kombinaci kláves a stiskněte ENTER", - "shortcutIsAlreadyUsed": "Tato zkratka je již použita pro: @@", - "resetToDefault": "Obnovit výchozí klávesové zkratky", - "couldNotLoadErrorMsg": "Nepodařilo se načíst klávesové zkratky, zkuste to znovu", - "couldNotSaveErrorMsg": "Nepodařilo seuložit klávesové zkta" } }, "grid": { @@ -606,23 +606,23 @@ "referencedGrid": "Odkazovaná mřížka", "referencedCalendar": "Odkazovaný kalendář", "referencedDocument": "Odkazovaný dokument", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Zeptej se AI na cokoliv...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Zeptej se AI na cokoliv...", "autoGeneratorLearnMore": "Zjistit více", "autoGeneratorGenerate": "Vygenerovat", - "autoGeneratorHintText": "Zeptat se AI...", - "autoGeneratorCantGetOpenAIKey": "Nepodařilo se získat klíč AI", + "autoGeneratorHintText": "Zeptat se OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Nepodařilo se získat klíč OpenAI", "autoGeneratorRewrite": "Přepsat", "smartEdit": "AI asistenti", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Opravit pravopis", "warning": "⚠️ odpovědi AI mohou být nepřesné nebo zavádějící.", "smartEditSummarize": "Shrnout", "smartEditImproveWriting": "Vylepšit styl psaní", "smartEditMakeLonger": "Prodloužit", - "smartEditCouldNotFetchResult": "Nepodařilo se stáhnout výsledek z AI", - "smartEditCouldNotFetchKey": "Nepodařilo se stáhnout klíč AI", - "smartEditDisabled": "Propojit s AI v Nastavení", + "smartEditCouldNotFetchResult": "Nepodařilo se stáhnout výsledek z OpenAI", + "smartEditCouldNotFetchKey": "Nepodařilo se stáhnout klíč OpenAI", + "smartEditDisabled": "Propojit s OpenAI v Nastavení", "discardResponse": "Opravdu chcete zahodit odpovědi od AI?", "createInlineMathEquation": "Vytvořit rovnici", "fonts": "Písma", @@ -678,8 +678,8 @@ "defaultColor": "Výchozí" }, "image": { - "addAnImage": "Přidat obrázek", - "copiedToPasteBoard": "Odkaz na obrázek byl zkopírován do schránky" + "copiedToPasteBoard": "Odkaz na obrázek byl zkopírován do schránky", + "addAnImage": "Přidat obrázek" }, "outline": { "addHeadingToCreateOutline": "Přidáním nadpisů vytvoříte obsah dokumentu" @@ -716,7 +716,7 @@ "placeholder": "Vlože URL adresu obrázku" }, "ai": { - "label": "Vygenerujte obrázek pomocí AI", + "label": "Vygenerujte obrázek pomocí OpenAI", "placeholder": "Prosím vlo" }, "stability_ai": { @@ -735,12 +735,12 @@ "placeholder": "Vložte nebo napište odkaz na obrázek" }, "searchForAnImage": "Hledat obrázek", - "pleaseInputYourOpenAIKey": "zadejte prosím svůj AI klíč v Nastavení", + "pleaseInputYourOpenAIKey": "zadejte prosím svůj OpenAI klíč v Nastavení", + "pleaseInputYourStabilityAIKey": "prosím vložte svůjStability AI klíč v Nastavení", "saveImageToGallery": "Uložit obrázek", "failedToAddImageToGallery": "Nepodařilo se přidat obrázek do galerie", "successToAddImageToGallery": "Obrázek byl úspěšně přidán do galerie", - "unableToLoadImage": "Nepodařilo se nahrát obrázek", - "pleaseInputYourStabilityAIKey": "prosím vložte svůjStability AI klíč v Nastavení" + "unableToLoadImage": "Nepodařilo se nahrát obrázek" }, "codeBlock": { "language": { @@ -1094,4 +1094,4 @@ "font": "Písmo", "actions": "Příkazy" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 65a7fbea05..892e6ae9a5 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -36,7 +36,6 @@ "loginButtonText": "Anmelden", "loginStartWithAnonymous": "Anonyme Sitzung starten", "continueAnonymousUser": "in anonymer Sitzung fortfahren", - "anonymous": "Anonym", "buttonText": "Anmelden", "signingInText": "Anmelden...", "forgotPassword": "Passwort vergessen?", @@ -51,8 +50,6 @@ "signInWithGoogle": "Mit Google anmelden", "signInWithGithub": "Mit Github anmelden", "signInWithDiscord": "Mit Discord anmelden", - "signInWithApple": "Weiter mit Apple", - "continueAnotherWay": "Anders fortfahren", "signUpWithGoogle": "Mit Google registrieren", "signUpWithGithub": "Mit Github registrieren", "signUpWithDiscord": "Mit Discord registrieren", @@ -67,18 +64,13 @@ "alreadyHaveAnAccount": "Du hast bereits ein Konto?", "logIn": "Anmeldung", "generalError": "Etwas ist schiefgelaufen. Bitte versuche es später noch einmal", - "limitRateError": "Aus Sicherheitsgründen kannst du nur alle 60 Sekunden einen Authentifizierungslink anfordern", - "magicLinkSentDescription": "Ein Magic Link wurde an deine E-Mail-Adresse gesendet. Klicke auf den Link, um deine Anmeldung abzuschließen. Der Link läuft nach 5 Minuten ab." + "limitRateError": "Aus Sicherheitsgründen kannst du nur alle 60 Sekunden einen Authentifizierungslink anfordern" }, "workspace": { "chooseWorkspace": "Arbeitsbereich wählen", - "defaultName": "Mein Arbeitsbereich", "create": "Arbeitsbereich erstellen", - "importFromNotion": "Von Notion importieren", - "learnMore": "Mehr erfahren", "reset": "Arbeitsbereich zurücksetzen", "renameWorkspace": "Arbeitsbereich umbenennen", - "workspaceNameCannotBeEmpty": "Arbeitsbereichname darf nicht leer sein", "resetWorkspacePrompt": "Das Zurücksetzen des Arbeitsbereiches löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchtest? ", "hint": "Arbeitsbereich", "notFoundError": "Arbeitsbereich nicht gefunden", @@ -114,25 +106,7 @@ "html": "HTML", "clipboard": "In die Zwischenablage kopieren", "csv": "CSV", - "copyLink": "Link kopieren", - "publishToTheWeb": "Im Web veröffentlichen", - "publishToTheWebHint": "Erstelle eine Website mit AppFlowy", - "publish": "Veröffentlichen", - "unPublish": "Veröffentlichung aufheben", - "visitSite": "Seite aufrufen", - "exportAsTab": "Exportieren als", - "publishTab": "Veröffentlichen", - "shareTab": "Teilen", - "publishOnAppFlowy": "Auf AppFlowy veröffentlichen", - "shareTabTitle": "Zum Mitmachen einladen", - "shareTabDescription": "Für eine einfache Zusammenarbeit mit allen", - "copyLinkSuccess": "Link in die Zwischenablage kopiert", - "copyShareLink": "Link zum Teilen kopieren", - "copyLinkFailed": "Link konnte nicht in die Zwischenablage kopiert werden", - "copyLinkToBlockSuccess": "Blocklink in die Zwischenablage kopiert", - "copyLinkToBlockFailed": "Blocklink konnte nicht in die Zwischenablage kopiert werden", - "manageAllSites": "Alle Seiten verwalten", - "updatePathName": "Pfadnamen aktualisieren" + "copyLink": "Link kopieren" }, "moreAction": { "small": "klein", @@ -151,7 +125,6 @@ "textAndMarkdown": "Text & Markdown", "documentFromV010": "Dokument ab v0.1.0", "databaseFromV010": "Datenbank ab v0.1.0", - "notionZip": "von Notion exportierte Zip-Datei", "csv": "CSV", "database": "Datenbank" }, @@ -171,50 +144,27 @@ "blankPageTitle": "Leere Seite", "newPageText": "Neue Seite", "newDocumentText": "Neues Dokument", - "newGridText": "Neue Datentabelle", + "newGridText": "Neues Raster", "newCalendarText": "Neuer Kalender", "newBoardText": "Neues Board", "chat": { "newChat": "Neuer Chat", - "inputMessageHint": "Nachricht an @:appName AI", - "inputLocalAIMessageHint": "Nachricht an @:appName Lokale KI", - "unsupportedCloudPrompt": "Diese Funktion ist nur bei Verwendung der @:appName Cloud verfügbar", + "inputMessageHint": "Nachricht an AppFlowy AI", + "unsupportedCloudPrompt": "Diese Funktion ist nur bei Verwendung der AppFlowy Cloud verfügbar", "relatedQuestion": "Verwandt", "serverUnavailable": "Dienst vorübergehend nicht verfügbar. Bitte versuche es später erneut.", "aiServerUnavailable": "Beim Generieren einer Antwort ist ein Fehler aufgetreten.", - "retry": "Wiederholen", "clickToRetry": "Erneut versuchen", "regenerateAnswer": "Regenerieren", "question1": "Wie verwendet man Kanban zur Aufgabenverwaltung?", "question2": "Erkläre mir die GTD-Methode", "question3": "Warum sollte ich Rust verwenden?", "question4": "Gebe mir ein Rezept mit dem, was in meiner Küche ist", - "question5": "Eine Illustration für meine Seite erstellen", - "question6": "Erstelle eine To-Do-Liste für meine kommende Woche", - "aiMistakePrompt": "KI kann Fehler machen. Überprüfe wichtige Informationen.", - "chatWithFilePrompt": "Möchtest du mit der Datei chatten?", - "indexFileSuccess": "Datei erfolgreich indiziert", - "inputActionNoPages": "Keine Seitenergebnisse", - "referenceSource": { - "zero": "0 Quellen gefunden", - "one": "{count} Quelle gefunden", - "other": "{count} Quellen gefunden" - }, - "clickToMention": "Klicke hier, um eine Seite zu erwähnen", - "uploadFile": "Lade PDF-, md- oder txt-Dateien in den Chat hoch", - "questionDetail": "Hallo {}! Wie kann ich dir heute helfen?", - "indexingFile": "Indizierung {}", - "generatingResponse": "Antwort generieren", - "selectSources": "Quellen auswählen", - "regenerate": "Versuchen Sie es erneut", - "addToPageButton": "Zur Seite hinzufügen", - "addToPageTitle": "Nachricht hinzufügen an...", - "addToNewPage": "Zu einer neuen Seite hinzufügen" + "aiMistakePrompt": "KI kann Fehler machen. Überprüfe wichtige Informationen." }, "trash": { "text": "Papierkorb", "restoreAll": "Alles wiederherstellen", - "restore": "Wiederherstellen", "deleteAll": "Alles löschen", "pageHeader": { "fileName": "Dateiname", @@ -229,10 +179,6 @@ "title": "Möchtest du wirklich alle Seiten aus dem Papierkorb wiederherstellen?", "caption": "Diese Aktion kann nicht rückgängig gemacht werden." }, - "restorePage": { - "title": "Wiederherstellen: {}", - "caption": "Möchten Sie diese Seite wirklich wiederherstellen?" - }, "mobile": { "actions": "Papierkorb-Einstellungen", "empty": "Der Papierkorb ist leer.", @@ -245,21 +191,20 @@ "deletePagePrompt": { "text": "Diese Seite befindet sich im Papierkorb", "restore": "Seite wiederherstellen", - "deletePermanent": "Dauerhaft löschen", - "deletePermanentDescription": "Möchten Sie diese Seite wirklich dauerhaft löschen? Dies kann nicht rückgängig gemacht werden." + "deletePermanent": "Dauerhaft löschen" }, "dialogCreatePageNameHint": "Seitenname", "questionBubble": { "shortcuts": "Tastenkürzel", "whatsNew": "Was gibt es Neues?", + "help": "Hilfe & Support", "markdown": "Markdown", "debug": { "name": "Debug-Informationen", "success": "Debug-Informationen in die Zwischenablage kopiert!", "fail": "Debug-Informationen konnten nicht in die Zwischenablage kopiert werden" }, - "feedback": "Feedback", - "help": "Hilfe & Support" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Entfernen, umbenennen und mehr...", @@ -296,10 +241,9 @@ "viewDataBase": "Datenbank ansehen", "referencePage": "Auf diesen {Name} wird verwiesen", "addBlockBelow": "Einen Block hinzufügen", - "aiGenerate": "Erzeugen", + "genSummary": "Zusammenfassung generieren", "urlLaunchAccessory": "Im Browser öffnen", - "urlCopyAccessory": "Webadresse kopieren.", - "genSummary": "Zusammenfassung generieren" + "urlCopyAccessory": "Webadresse kopieren." }, "sideBar": { "closeSidebar": "Seitenleiste schließen", @@ -315,11 +259,10 @@ "addAPage": "Seite hinzufügen", "addAPageToPrivate": "Eine Seite zum privaten Bereich hinzufügen.", "addAPageToWorkspace": "Eine Seite zum Arbeitsbereich hinzufügen", - "recent": "Kürzlich", + "recent": "Zuletzt", "today": "Heute", "thisWeek": "Diese Woche", "others": "Andere", - "earlier": "Früher", "justNow": "soeben", "minutesAgo": "vor {count} Minuten", "lastViewed": "Zuletzt angesehen", @@ -328,31 +271,14 @@ "emptyRecentDescription": "Kürzlich aufgerufene Dokumente werden hier, zur vereinfachten Auffindbarkeit, angezeigt.", "emptyFavorite": "Keine favorisierten Dokumente", "emptyFavoriteDescription": "Beginne mit der Erkundung und markiere Dokumente als Favoriten. Diese werden hier für den schnellen Zugriff aufgelistet!", - "removePageFromRecent": "Diese Seite aus „Kürzlich“ entfernen?", + "removePageFromRecent": "Diese Seite aus „Zuletzt angesehen“ entfernen?", "removeSuccess": "Erfolgreich entfernt", "favoriteSpace": "Favoriten", - "RecentSpace": "Kürzlich", - "Spaces": "Bereiche", - "upgradeToPro": "Upgrade auf Pro", - "upgradeToAIMax": "Schalte unbegrenzte KI frei", - "storageLimitDialogTitle": "Dein freier Speicherplatz ist aufgebraucht. Upgrade deinen Plan, um unbegrenzten Speicherplatz freizuschalten.", - "storageLimitDialogTitleIOS": "Ihr freier Speicherplatz ist aufgebraucht.", - "aiResponseLimitTitle": "Du hast keine kostenlosen KI-Antworten mehr. Upgrade auf den Pro-Plan oder kaufe ein KI-Add-on, um unbegrenzte Antworten freizuschalten", - "aiResponseLimitDialogTitle": "Limit für KI-Antworten erreicht", - "aiResponseLimit": "Du hast keine kostenlosen KI-Antworten mehr zur Verfügung.\n\nGehe zu Einstellungen -> Plan -> Klicke auf KI Max oder Pro Plan, um mehr KI-Antworten zu erhalten", - "askOwnerToUpgradeToPro": "Dein Arbeitsbereich hat nicht mehr genügend freien Speicherplatz. Bitte den Eigentümer deines Arbeitsbereichs, auf den Pro-Plan hochzustufen.", - "askOwnerToUpgradeToProIOS": "In Ihrem Arbeitsbereich ist nicht mehr genügend freier Speicherplatz verfügbar.", - "askOwnerToUpgradeToAIMax": "In deinem Arbeitsbereich sind die kostenlosen KI-Antworten aufgebraucht. Bitte den Eigentümer deines Arbeitsbereichs, den Plan zu wechseln oder KI-Add-ons zu erwerben.", - "askOwnerToUpgradeToAIMaxIOS": "In Ihrem Arbeitsbereich gehen die kostenlosen KI-Antworten aus.", - "purchaseStorageSpace": "Speicherplatz kaufen", - "singleFileProPlanLimitationDescription": "Sie haben die maximal zulässige Datei-Uploadgröße im kostenlosen Plan überschritten. Bitte aktualisieren Sie auf den Pro-Plan, um größere Dateien hochzuladen", - "purchaseAIResponse": "Kaufen ", - "askOwnerToUpgradeToLocalAI": "Bitte den Arbeitsbereichsbesitzer, KI auf dem Gerät zu aktivieren.", - "upgradeToAILocal": "KI offline auf Ihrem Gerät", - "upgradeToAILocalDesc": "Chatte mit PDFs, verbessere deine Schreibfähigkeiten und fülle Tabellen automatisch mithilfe lokaler KI aus.", + "RecentSpace": "Zuletzt angesehen", + "Spaces": "Gemeinsam genutzte Bereiche", "public": "Öffentlich", "clickToHidePublic": "Hier klicken, um den öffentlichen Bereich auszublenden.\nHier erstellte Seiten sind für jedes Mitglied sichtbar.", - "addAPageToPublic": "Eine Seite zur öffentlichen Domäne hinzufügen." + "addAPageToPublic": "Eine Seite zum öffentlichen Bereich hinzufügen." }, "notifications": { "export": { @@ -386,22 +312,17 @@ "upload": "Hochladen", "edit": "Bearbeiten", "delete": "Löschen", - "copy": "kopieren", "duplicate": "Duplikat", "putback": "wieder zurückgeben", "update": "Update", "share": "Teilen", "removeFromFavorites": "Aus den Favoriten entfernen", - "removeFromRecent": "Aus „Kürzlich“ entfernen", + "removeFromRecent": "Aus „Zuletzt angesehen“ entfernen", "addToFavorites": "Zu den Favoriten hinzufügen", - "favoriteSuccessfully": "Erfolgreich favorisiert", - "unfavoriteSuccessfully": "Erfolgreich entfavorisiert", - "duplicateSuccessfully": "Erfolgreich dupliziert", "rename": "Umbenennen", "helpCenter": "Hilfe Center", "add": "Hinzufügen", "yes": "Ja", - "no": "Nein", "clear": "Leeren", "remove": "Entfernen", "dontRemove": "Nicht entfernen", @@ -414,17 +335,7 @@ "signInGoogle": "Mit einem Google Benutzerkonto anmelden", "signInGithub": "Mit einem Github Benutzerkonto anmelden", "signInDiscord": "Mit einem Discord Benutzerkonto anmelden", - "more": "Mehr", - "create": "Erstellen", - "close": "Schließen", - "next": "Weiter", - "previous": "Zurück", - "submit": "Einreichen", - "download": "Herunterladen", - "backToHome": "Zurück zur Startseite", - "viewing": "anschauen", - "editing": "Bearbeiten", - "gotIt": "Verstanden" + "more": "Mehr" }, "label": { "welcome": "Willkommen!", @@ -448,77 +359,10 @@ }, "settings": { "title": "Einstellungen", - "popupMenuItem": { - "settings": "Einstellungen", - "members": "Mitglieder", - "trash": "Müll", - "helpAndSupport": "Hilfe & Unterstützung" - }, - "sites": { - "title": "Seiten", - "namespaceTitle": "Namensraum", - "namespaceDescription": "Verwalten Sie Ihren Namespace und Ihre Startseite", - "namespaceHeader": "Namensraum", - "homepageHeader": "Startseite", - "updateNamespace": "Namespace aktualisieren", - "removeHomepage": "Startseite entfernen", - "selectHomePage": "Seite auswählen", - "clearHomePage": "Löschen Sie die Startseite für diesen Namensraum", - "customUrl": "Benutzerdefinierte URL", - "namespace": { - "description": "Diese Änderung gilt für alle veröffentlichten Seiten in diesem Namespace.", - "tooltip": "Wir behalten uns das Recht vor, unangemessene Namespaces zu entfernen", - "updateExistingNamespace": "Vorhandenen Namensraum aktualisieren", - "upgradeToPro": "Aktualisieren Sie auf den Pro-Plan, um eine Startseite einzurichten", - "redirectToPayment": "Weiterleitung zur Zahlungsseite ...", - "onlyWorkspaceOwnerCanSetHomePage": "Nur der Arbeitsbereichsbesitzer kann eine Startseite festlegen", - "pleaseAskOwnerToSetHomePage": "Bitten Sie den Arbeitsbereichsbesitzer, auf den Pro-Plan zu aktualisieren" - }, - "publishedPage": { - "title": "Alle veröffentlichten Seiten", - "description": "Verwalten Sie Ihre veröffentlichten Seiten", - "page": "Seite", - "pathName": "Pfadname", - "date": "Veröffentlichungsdatum", - "emptyHinText": "Sie haben keine veröffentlichten Seiten in diesem Arbeitsbereich", - "noPublishedPages": "Keine veröffentlichten Seiten", - "settings": "Veröffentlichungseinstellungen", - "clickToOpenPageInApp": "Seite in App öffnen", - "clickToOpenPageInBrowser": "Seite im Browser öffnen" - }, - "error": { - "failedToGeneratePaymentLink": "Zahlungslink für Pro Plan konnte nicht generiert werden", - "failedToUpdateNamespace": "Namensraum konnte nicht aktualisiert werden", - "proPlanLimitation": "Sie müssen auf den Pro-Plan upgraden, um den Namespace zu aktualisieren", - "namespaceAlreadyInUse": "Der Namespace ist bereits vergeben, bitte versuchen Sie es mit einem anderen", - "invalidNamespace": "Ungültiger Namensraum, bitte versuchen Sie einen anderen", - "namespaceLengthAtLeast2Characters": "Der Namensraum muss mindestens 2 Zeichen lang sein", - "onlyWorkspaceOwnerCanUpdateNamespace": "Nur der Arbeitsbereichsbesitzer kann den Namespace aktualisieren", - "onlyWorkspaceOwnerCanRemoveHomepage": "Nur der Arbeitsbereichsbesitzer kann die Homepage entfernen", - "setHomepageFailed": "Startseite konnte nicht eingerichtet werden", - "namespaceTooLong": "Der Namensraum ist zu lang. Bitte versuchen Sie es mit einem anderen.", - "namespaceTooShort": "Der Namensraum ist zu kurz, bitte versuchen Sie es mit einem anderen", - "namespaceIsReserved": "Der Namensraum ist reserviert, bitte versuchen Sie es mit einem anderen", - "updatePathNameFailed": "Pfadname konnte nicht aktualisiert werden", - "removeHomePageFailed": "Startseite konnte nicht entfernt werden", - "publishNameContainsInvalidCharacters": "Der Pfadname enthält ungültige Zeichen. Bitte versuchen Sie es mit einem anderen.", - "publishNameTooShort": "Der Pfadname ist zu kurz, bitte versuchen Sie es mit einem anderen", - "publishNameTooLong": "Der Pfadname ist zu lang, bitte versuchen Sie es mit einem anderen", - "publishNameAlreadyInUse": "Der Pfadname wird bereits verwendet. Bitte versuchen Sie einen anderen.", - "namespaceContainsInvalidCharacters": "Der Namespace enthält ungültige Zeichen. Bitte versuchen Sie es mit einem anderen.", - "publishPermissionDenied": "Nur der Arbeitsbereichsbesitzer oder Seitenherausgeber kann die Veröffentlichungseinstellungen verwalten", - "publishNameCannotBeEmpty": "Der Pfadname darf nicht leer sein. Bitte versuchen Sie es mit einem anderen." - }, - "success": { - "namespaceUpdated": "Namensraum erfolgreich aktualisiert", - "setHomepageSuccess": "Startseite erfolgreich eingerichtet", - "updatePathNameSuccess": "Pfadname erfolgreich aktualisiert", - "removeHomePageSuccess": "Startseite erfolgreich entfernt" - } - }, "accountPage": { "menuLabel": "Mein Konto", "title": "Mein Konto", + "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an.", "general": { "title": "Kontoname und Profilbild", "changeProfilePicture": "Profilbild ändern" @@ -529,12 +373,20 @@ "change": "E-Mail ändern" } }, + "keys": { + "title": "KI API-Schlüssel", + "openAILabel": "OpenAI API-Schlüssel", + "openAITooltip": "Der für die KI-Modelle zu verwendende OpenAI-API-Schlüssel", + "openAIHint": "OpenAI API-Schlüssel eingeben", + "stabilityAILabel": "Stability API-Schlüssel", + "stabilityAITooltip": "Der für die KI-Modelle zu verwendende Stability API-Schlüssel", + "stabilityAIHint": "Stability API-Schlüssel eingeben" + }, "login": { "title": "Kontoanmeldung", "loginLabel": "Anmeldung", "logoutLabel": "Ausloggen" - }, - "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an." + } }, "workspacePage": { "menuLabel": "Arbeitsbereich", @@ -558,25 +410,12 @@ "dark": "Dunkel" } }, - "resetCursorColor": { - "title": "Farbe des Dokumentcursors zurücksetzen", - "description": "Möchtest du die Cursorfarbe wirklich zurücksetzen?" - }, - "resetSelectionColor": { - "title": "Dokumentauswahlfarbe zurücksetzen", - "description": "Möchtest du die Auswahlfarbe wirklich zurücksetzen?" - }, - "resetWidth": { - "resetSuccess": "Dokumentbreite erfolgreich zurückgesetzt" - }, "theme": { "title": "Design", - "description": "Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch.", - "uploadCustomThemeTooltip": "Ein benutzerdefiniertes Theme hochladen" + "description": "Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch." }, "workspaceFont": { - "title": "Schriftart", - "noFontHint": "Keine Schriftart gefunden, versuchen Sie einen anderen Begriff." + "title": "Schriftart" }, "textDirection": { "title": "Textrichtung", @@ -612,9 +451,7 @@ }, "leaveWorkspacePrompt": { "title": "Arbeitsbereich verlassen", - "content": "Möchtest du diesen Arbeitsbereich wirklich verlassen? Du verlierst den Zugriff auf alle darin enthaltenen Seiten und Daten.", - "success": "Sie haben den Arbeitsbereich erfolgreich verlassen.", - "fail": "Das Verlassen des Arbeitsbereichs ist fehlgeschlagen." + "content": "Möchtest du diesen Arbeitsbereich wirklich verlassen? Du verlierst den Zugriff auf alle darin enthaltenen Seiten und Daten." }, "manageWorkspace": { "title": "Arbeitsbereich verwalten", @@ -667,404 +504,9 @@ "description": "Durch das Leeren des Caches werden Bilder und Schriftarten beim Laden erneut heruntergeladen. Deine Daten werden durch diese Aktion weder entfernt noch geändert.", "successHint": "Cache geleert!" } - }, - "data": { - "fixYourData": "Deine Daten korrigieren", - "fixButton": "Korrigieren", - "fixYourDataDescription": "Wenn du Probleme mit deinen Daten hast, kannst du hier versuchen, diese zu beheben." - } - }, - "shortcutsPage": { - "menuLabel": "Tastenkombinationen", - "title": "Shortcuts", - "editBindingHint": "Neue Verknüpfung eingeben", - "searchHint": "Suchen", - "actions": { - "resetDefault": "Standardeinstellung zurücksetzen" - }, - "errorPage": { - "message": "Shortcuts konnten nicht geladen werden: {}", - "howToFix": "Bitte versuche es erneut. Wenn das Problem weiterhin besteht, melde es bitte auf GitHub." - }, - "resetDialog": { - "title": "Shortcuts zurücksetzen", - "description": "Dies wird alle deine Shortcuts auf die Standardeinstellungen zurücksetzen, dies kann nicht rückgängig gemacht werden. Bist du dir sicher, dass du fortfahren möchtest?", - "buttonLabel": "Zurücksetzen" - }, - "conflictDialog": { - "title": "{} ist derzeit in Verwendung", - "descriptionPrefix": "Diese Tastenkombination wird derzeit verwendet von ", - "descriptionSuffix": ". Wenn du diese Tastaturbelegung ersetzt, wird sie aus {} entfernt.", - "confirmLabel": "Weiter" - }, - "editTooltip": "Zum Starten der Bearbeitung der Tastaturbelegung drücken.", - "keybindings": { - "toggleToDoList": "Aufgabenliste ein-/ausblenden", - "insertNewParagraphInCodeblock": "Neuen Absatz einfügen", - "pasteInCodeblock": "In Codeblock einfügen", - "selectAllCodeblock": "Alles auswählen", - "indentLineCodeblock": "Zwei Leerzeichen am Zeilenanfang einfügen", - "outdentLineCodeblock": "Zwei Leerzeichen am Zeilenanfang löschen", - "twoSpacesCursorCodeblock": "Zwei Leerzeichen am Cursor einfügen", - "copy": "Auswahl kopieren", - "paste": "Inhalt einfügen", - "cut": "Auswahl ausschneiden", - "alignLeft": "Text links ausrichten", - "alignCenter": "Text zentriert ausrichten", - "alignRight": "Text rechts ausrichten", - "undo": "Undo", - "redo": "Redo", - "convertToParagraph": "Block in Absatz umwandeln", - "backspace": "Löschen", - "deleteLeftWord": "Linkes Wort löschen", - "deleteLeftSentence": "Linken Satz löschen", - "delete": "Rechtes Zeichen löschen", - "deleteMacOS": "Linkes Zeichen löschen", - "deleteRightWord": "Rechtes Wort löschen", - "moveCursorLeft": "Cursor nach links bewegen", - "moveCursorBeginning": "Cursor an den Zeilenanfang bewegen", - "moveCursorLeftWord": "Cursor ein Wort nach links bewegen", - "moveCursorLeftSelect": "Auswählen und Cursor nach links bewegen", - "moveCursorBeginSelect": "Auswählen und Cursor an den Zeilenanfang bewegen", - "moveCursorLeftWordSelect": "Auswählen und Cursor ein Wort nach links bewegen", - "moveCursorRight": "Cursor nach rechts bewegen", - "moveCursorEnd": "Cursor an das Zeilenende bewegen", - "moveCursorRightWord": "Cursor ein Wort nach rechts bewegen", - "moveCursorRightSelect": "Auswählen und Cursor nach rechts bewegen", - "moveCursorEndSelect": "Auswählen und Cursor an das Zeilenende bewegen", - "moveCursorRightWordSelect": "Markiere das Wort und bewege den Cursor ein Wort nach rechts", - "moveCursorUp": "Cursor nach oben bewegen", - "moveCursorTopSelect": "Auswählen und Cursor zum Anfang bewegen", - "moveCursorTop": "Cursor zum Anfang bewegen", - "moveCursorUpSelect": "Auswählen und Cursor nach oben bewegen", - "moveCursorBottomSelect": "Auswählen und Cursor ans Ende bewegen", - "moveCursorBottom": "Cursor ans Ende bewegen", - "moveCursorDown": "Cursor nach unten bewegen", - "moveCursorDownSelect": "Auswählen und Cursor nach unten bewegen", - "home": "Zum Anfang scrollen", - "end": "Zum Ende scrollen", - "toggleBold": "Fett ein-/ausschalten", - "toggleItalic": "Kursivschrift ein-/ausschalten", - "toggleUnderline": "Unterstreichung ein-/ausschalten", - "toggleStrikethrough": "Durchgestrichen ein-/ausschalten", - "toggleCode": "Inline-Code ein-/ausschalten", - "toggleHighlight": "Hervorhebung ein-/ausschalten", - "showLinkMenu": "Linkmenü anzeigen", - "openInlineLink": "Inline-Link öffnen", - "openLinks": "Alle ausgewählten Links öffnen", - "indent": "Einzug", - "outdent": "Ausrücken", - "exit": "Bearbeitung beenden", - "pageUp": "Eine Seite nach oben scrollen", - "pageDown": "Eine Seite nach unten scrollen", - "selectAll": "Alles auswählen", - "pasteWithoutFormatting": "Inhalt ohne Formatierung einfügen", - "showEmojiPicker": "Emoji-Auswahl anzeigen", - "enterInTableCell": "Zeilenumbruch in Tabelle hinzufügen", - "leftInTableCell": "In der Tabelle eine Zelle nach links verschieben", - "rightInTableCell": "In der Tabelle eine Zelle nach rechts verschieben", - "upInTableCell": "In der Tabelle eine Zelle nach oben verschieben", - "downInTableCell": "In der Tabelle eine Zelle nach unten verschieben", - "tabInTableCell": "Zur nächsten verfügbaren Zelle in der Tabelle gehen", - "shiftTabInTableCell": "Zur zuvor verfügbaren Zelle in der Tabelle gehen", - "backSpaceInTableCell": "Am Anfang der Zelle anhalten" - }, - "commands": { - "codeBlockNewParagraph": "Füge einen neuen Absatz neben dem Codeblock ein", - "codeBlockIndentLines": "Füge am Zeilenanfang im Codeblock zwei Leerzeichen ein", - "codeBlockOutdentLines": "Lösche zwei Leerzeichen am Zeilenanfang im Codeblock", - "codeBlockAddTwoSpaces": "Einfügen von zwei Leerzeichen an der Cursorposition im Codeblock", - "codeBlockSelectAll": "Wähle den gesamten Inhalt innerhalb eines Codeblocks aus", - "codeBlockPasteText": "Text in Codeblock einfügen", - "textAlignLeft": "Text nach links ausrichten", - "textAlignCenter": "Text nach rechts ausrichten", - "textAlignRight": "Text rechtsbündig ausrichten" - }, - "couldNotLoadErrorMsg": "Konnte keine Shortcuts laden, versuche es erneut", - "couldNotSaveErrorMsg": "Shortcuts konnten nicht gespeichert werden, versuche es erneut" - }, - "aiPage": { - "title": "KI-Einstellungen", - "menuLabel": "KI-Einstellungen", - "keys": { - "enableAISearchTitle": "KI-Suche", - "aiSettingsDescription": "Wähle oder konfiguriere KI-Modelle, die in @:appName verwendet werden. Für eine optimale Leistung empfehlen wir die Verwendung der Standardmodelloptionen", - "loginToEnableAIFeature": "KI-Funktionen werden erst nach der Anmeldung bei @:appName Cloud aktiviert. Wenn du kein @:appName-Konto hast, gehe zu „Mein Konto“, um dich zu registrieren", - "llmModel": "Sprachmodell", - "llmModelType": "Sprachmodelltyp", - "downloadLLMPrompt": "Herunterladen {}", - "downloadAppFlowyOfflineAI": "Durch das Herunterladen des KI-Offlinepakets kann KI auf deinem Gerät ausgeführt werden. Möchtest du fortfahren?", - "downloadLLMPromptDetail": "Das Herunterladen des lokalen Modells {} beansprucht bis zu {} Speicherplatz. Möchtest du fortfahren?", - "downloadBigFilePrompt": "Der Download kann etwa 10 Minuten dauern", - "downloadAIModelButton": "KI-Modell herunterladen", - "downloadingModel": "wird heruntergeladen", - "localAILoaded": "Lokales KI-Modell erfolgreich hinzugefügt und einsatzbereit", - "localAIStart": "Der lokale KI-Chat beginnt …", - "localAILoading": "Das lokale KI-Chat-Modell wird geladen …", - "localAIStopped": "Lokale KI wurde gestoppt", - "failToLoadLocalAI": "Lokale KI konnte nicht gestartet werden", - "restartLocalAI": "Lokale KI neustarten", - "disableLocalAITitle": "Lokale KI deaktivieren", - "disableLocalAIDescription": "Möchtest du die lokale KI deaktivieren?", - "localAIToggleTitle": "Umschalten zum Aktivieren oder Deaktivieren der lokalen KI", - "offlineAIInstruction1": "Folge der", - "offlineAIInstruction2": "Anweisung", - "offlineAIInstruction3": "um Offline-KI zu aktivieren.", - "offlineAIDownload1": "Wenn du die AppFlowy KI noch nicht heruntergeladen hast,", - "offlineAIDownload2": "lade", - "offlineAIDownload3": "sie zuerst herunter", - "activeOfflineAI": "Aktiv", - "downloadOfflineAI": "Herunterladen", - "openModelDirectory": "Ordner öffnen", - "title": "KI-API-Schlüssel" - } - }, - "planPage": { - "menuLabel": "Plan", - "title": "Tarifplan", - "planUsage": { - "title": "Zusammenfassung der Plannutzung", - "storageLabel": "Speicher", - "storageUsage": "{} von {} GB", - "unlimitedStorageLabel": "Unbegrenzter Speicherplatz", - "collaboratorsLabel": "Gastmitarbeiter", - "collaboratorsUsage": "{} von {}", - "aiResponseLabel": "KI-Antworten", - "aiResponseUsage": "{} von {}", - "unlimitedAILabel": "Unbegrenzte Antworten", - "proBadge": "Pro", - "aiMaxBadge": "KI Max", - "aiOnDeviceBadge": "KI On-Device", - "memberProToggle": "Unbegrenzte Mitgliederzahl", - "aiMaxToggle": "Unbegrenzte KI-Antworten", - "aiOnDeviceToggle": "KI auf dem Gerät für ultimative Privatsphäre", - "aiCredit": { - "title": "@:appName KI-Guthaben hinzufügen", - "price": "{}", - "priceDescription": "für 1.000 Credits", - "purchase": "Kauf von KI", - "info": "Füge 1.000 KI-Credits pro Arbeitsbereich hinzu und integriere anpassbare KI nahtlos in deinen Arbeitsablauf für intelligentere, schnellere Ergebnisse mit bis zu:", - "infoItemOne": "10.000 Antworten pro Datenbank", - "infoItemTwo": "1.000 Antworten pro Arbeitsbereich" - }, - "currentPlan": { - "bannerLabel": "Derzeitiger Plan", - "freeTitle": "Kostenfrei", - "proTitle": "Pro", - "teamTitle": "Team", - "freeInfo": "Perfekt für Einzelpersonen oder kleine Teams mit bis zu 3 Mitgliedern.", - "proInfo": "Perfekt für kleine und mittlere Teams mit bis zu 10 Mitgliedern.", - "teamInfo": "Perfekt für alle produktiven und gut organisierten Teams.", - "upgrade": "Vergleichen &\n Upgraden", - "canceledInfo": "Dein Plan wurde gekündigt und du wirst am {} auf den kostenlosen Plan herabgestuft.", - "freeProOne": "Gemeinsamer Arbeitsbereich", - "freeProTwo": "Bis zu 3 Mitglieder (inkl. Eigentümer)", - "freeProThree": "Unbegrenzte Anzahl an Gästen (nur anzeigen)", - "freeProFour": "Speicher 5 GB", - "freeProFive": "30 Tage Änderungshistorie", - "freeConOne": "Gastmitarbeiter (Bearbeitungszugriff)", - "freeConTwo": "Unbegrenzter Speicherplatz", - "freeConThree": "6 Monate Änderungshistorie", - "professionalProOne": "Gemeinsamer Arbeitsbereich", - "professionalProTwo": "Unbegrenzte Mitgliederzahl", - "professionalProThree": "Unbegrenzte Anzahl an Gästen (nur anzeigen)", - "professionalProFour": "Unbegrenzter Speicherplatz", - "professionalProFive": "6 Monate Änderungshistorie", - "professionalConOne": "Unbegrenzte Anzahl an Gastmitarbeitern (Bearbeitungszugriff)", - "professionalConTwo": "Unbegrenzte KI-Antworten", - "professionalConThree": "1 Jahr Änderungshistorie" - }, - "addons": { - "title": "Add-ons", - "addLabel": "Hinzufügen", - "activeLabel": "Hinzugefügt", - "aiMax": { - "title": "KI Max", - "description": "Schalte unbegrenzte KI frei", - "price": "{}", - "priceInfo": "pro Benutzer und Monat", - "billingInfo": "jährliche Abrechnung oder {} bei monatlicher Abrechnung" - }, - "aiOnDevice": { - "title": "KI On-Device", - "description": "KI offline auf deinem Gerät", - "price": "{}", - "priceInfo": "pro Benutzer und Monat", - "recommend": "Empfohlen wird M1 oder neuer", - "billingInfo": "jährliche Abrechnung oder {} bei monatlicher Abrechnung" - } - }, - "deal": { - "bannerLabel": "Neujahrsangebot!", - "title": "Erweiter dein Team!", - "info": "Upgraden und 10 % auf Pro- und Team-Pläne sparen! Steiger die Produktivität deines Arbeitsplatzes mit leistungsstarken neuen Funktionen, einschließlich @:appName KI.", - "viewPlans": "Pläne anzeigen" - }, - "guestCollabToggle": "10 Gastmitarbeiter", - "storageUnlimited": "Unbegrenzter Speicherplatz mit deinem Pro-Plan" - } - }, - "billingPage": { - "menuLabel": "Abrechnung", - "title": "Abrechnung", - "plan": { - "title": "Plan", - "freeLabel": "Kostenfrei", - "proLabel": "Pro", - "planButtonLabel": "Plan ändern", - "billingPeriod": "Abrechnungszeitraum", - "periodButtonLabel": "Zeitraum bearbeiten" - }, - "paymentDetails": { - "title": "Zahlungsdetails", - "methodLabel": "Zahlungsmethode", - "methodButtonLabel": "Zahlungsmethode bearbeiten" - }, - "addons": { - "title": "Add-ons", - "addLabel": "Hinzufügen", - "removeLabel": "Entfernen", - "renewLabel": "Erneuern", - "aiMax": { - "label": "KI Max", - "description": "Schalte unbegrenzte KI Antworten und erweiterte Modelle frei", - "activeDescription": "Nächste Rechnung fällig am {}", - "canceledDescription": "KI Max ist verfügbar bis {}" - }, - "aiOnDevice": { - "label": "KI On-Device", - "description": "Schalte unbegrenzte KI Antworten offline auf deinem Gerät frei", - "activeDescription": "Nächste Rechnung fällig am {}", - "canceledDescription": "KI On-Device ist verfügbar bis {}" - }, - "removeDialog": { - "title": "Entfernen {}", - "description": "Möchtest du den {plan} wirklich entfernen? Du verlierst dann sofort den Zugriff auf die Funktionen und Vorteile des {plan}." - } - }, - "currentPeriodBadge": "AKTUELL", - "changePeriod": "Zeitraum ändern", - "planPeriod": "{} Zeitraum", - "monthlyInterval": "Monatlich", - "monthlyPriceInfo": "pro Sitzplatz, monatliche Abrechnung", - "annualInterval": "Jährlich", - "annualPriceInfo": "pro Sitzplatz, jährliche Abrechnung" - }, - "comparePlanDialog": { - "title": "Plan vergleichen & auswählen", - "planFeatures": "Plan\nFeatures", - "current": "Aktuell", - "actions": { - "upgrade": "Upgrade", - "downgrade": "Downgrade", - "current": "Aktuell", - "downgradeDisabledTooltip": "Du wirst am Ende des Abrechnungszeitraums automatisch herabgestuft" - }, - "freePlan": { - "title": "Kostenlos", - "description": "Für die Organisation jeder Ecke Ihres Lebens und Ihrer Arbeit.", - "price": "0€", - "priceInfo": "free forever" - }, - "proPlan": { - "title": "Professionell", - "description": "Ein Ort für kleine Gruppen zum Planen und Organisieren.", - "price": "{} /Monat", - "priceInfo": "jährlich abgerechnet" - }, - "planLabels": { - "itemOne": "Arbeitsbereiche", - "itemTwo": "Mitglieder", - "itemThree": "Gäste", - "itemFour": "Gäste", - "itemFive": "Speicher", - "itemSix": "Zusammenarbeit in Echtzeit", - "itemSeven": "Mobile App", - "itemFileUpload": "Datei-Uploads", - "customNamespace": "Benutzerdefinierter Namensraum", - "tooltipSix": "Lebenslang bedeutet, dass die Anzahl der Antworten nie zurückgesetzt wird", - "intelligentSearch": "Intelligente Suche", - "tooltipSeven": "Ermöglicht dir, einen Teil der URL für deinen Arbeitsbereich anzupassen", - "customNamespaceTooltip": "Benutzerdefinierte veröffentlichte Seiten-URL", - "tooltipThree": "Gäste haben nur Leserechte für die speziell freigegebenen Inhalte", - "tooltipFour": "Gäste werden als ein Sitzplatz abgerechnet", - "itemEight": "AI-Antworten", - "tooltipEight": "Lebenslang bedeutet, dass die Anzahl der Antworten nie zurückgesetzt wird." - }, - "freeLabels": { - "itemOne": "pro Arbeitsbereich berechnet", - "itemTwo": "3", - "itemThree": "5 GB", - "itemFour": "0", - "itemFive": "5 GB", - "itemSix": "10 Lebenszeiten", - "itemSeven": "ja", - "itemFileUpload": "Bis zu 7 MB", - "intelligentSearch": "Intelligente Suche", - "itemEight": "1.000 lebenslang" - }, - "proLabels": { - "itemOne": "pro Arbeitsbereich berechnet", - "itemTwo": "bis zu 10", - "itemThree": "unbegrenzt", - "itemFour": "10 Gäste werden als ein Sitzplatz berechnet", - "itemFive": "unbegrenzt", - "itemSix": "ja", - "itemSeven": "ja", - "itemFileUpload": "Unbegrenzt", - "intelligentSearch": "Intelligente Suche", - "itemEight": "10.000 monatlich" - }, - "paymentSuccess": { - "title": "Du bist jetzt im {} Plan!", - "description": "Deine Zahlung wurde erfolgreich verarbeitet und dein Plan wurde auf @:appName {} aktualisiert. Du kannst die Details deines Plans auf der Seite \"Plan\" einsehen" - }, - "downgradeDialog": { - "title": "Bist du sicher, dass du deinen Plan herabstufen willst?", - "description": "Wenn du deinen Plan herabstufst, kehrst du zum kostenfreien Plan zurück. Mitglieder können den Zugang zu Arbeitsbereichen verlieren und du musst möglicherweise Speicherplatz freigeben, um die Speichergrenzen des kostenfreien Tarifs einzuhalten.", - "downgradeLabel": "Downgrade Plan" - } - }, - "cancelSurveyDialog": { - "title": "Schade dich gehen zu sehen", - "description": "Wir bedauern, dass du gehst. Wir würden uns über dein Feedback freuen, das uns hilft @:appName zu verbessern. Bitte nehme dir einen Moment Zeit, um ein paar Fragen zu beantworten.", - "commonOther": "Andere", - "otherHint": "Schreibe deine Antwort hier", - "questionOne": { - "question": "Was hat dich dazu veranlasst, dein @:appName Pro-Abonnement zu kündigen?", - "answerOne": "Kosten zu hoch", - "answerTwo": "Die Funktionen entsprachen nicht den Erwartungen", - "answerThree": "Habe eine bessere Alternative gefunden", - "answerFour": "Habe es nicht oft genug genutzt, um die Kosten zu rechtfertigen", - "answerFive": "Serviceproblem oder technische Schwierigkeiten" - }, - "questionTwo": { - "question": "Wie wahrscheinlich ist es, dass du in Zukunft ein erneutes Abonnement von @:appName Pro in Betracht ziehst?", - "answerOne": "Sehr wahrscheinlich", - "answerTwo": "Ziemlich wahrscheinlich", - "answerThree": "Nicht sicher", - "answerFour": "Unwahrscheinlich", - "answerFive": "Sehr unwahrscheinlich" - }, - "questionThree": { - "question": "Welche Pro-Funktion hast du während deines Abonnements am meisten geschätzt?", - "answerOne": "Zusammenarbeit mehrerer Benutzer", - "answerTwo": "Längerer Versionsverlauf", - "answerThree": "Unbegrenzte KI-Antworten", - "answerFour": "Zugriff auf lokale KI-Modelle" - }, - "questionFour": { - "question": "Wie würdest du deine allgemeine Erfahrung mit @:appName beschreiben?", - "answerOne": "Großartig", - "answerTwo": "Gut", - "answerThree": "Durchschnitt", - "answerFour": "Unterdurchschnittlich", - "answerFive": "Nicht zufrieden" } }, "common": { - "uploadingFile": "Datei wird hochgeladen. Bitte beenden Sie die App nicht.", - "uploadNotionSuccess": "Ihre Notion-ZIP-Datei wurde erfolgreich hochgeladen. Sobald der Import abgeschlossen ist, erhalten Sie eine Bestätigungs-E-Mail", "reset": "Zurücksetzen" }, "menu": { @@ -1075,19 +517,22 @@ "notifications": "Benachrichtigungen", "open": "Einstellungen öffnen", "logout": "Abmelden", - "logoutPrompt": "Willst du dich wirklich abmelden?", + "logoutPrompt": "Wollen sie sich wirklich Abmelden?", "selfEncryptionLogoutPrompt": "Willst du dich wirklich Abmelden? Bitte stelle sicher, dass der Encryption Secret Code kopiert wurde.", - "syncSetting": "Synchronisations-Einstellung", + "syncSetting": "Sync Einstellung", "cloudSettings": "Cloud Einstellungen", - "enableSync": "Synchronisation aktivieren", - "enableSyncLog": "Synchronisation der Protokolldateien aktivieren", - "enableSyncLogWarning": "Vielen Dank für Ihre Hilfe bei der Diagnose von Synchronisierungsproblemen. Dadurch werden Ihre Dokumentänderungen in einer lokalen Datei protokolliert. Bitte beenden Sie die App und öffnen Sie sie erneut, nachdem Sie sie aktiviert haben.", + "enableSync": "Sync aktivieren", "enableEncrypt": "Daten verschlüsseln", "cloudURL": "Basis URL", "invalidCloudURLScheme": "Ungültiges Format", "cloudServerType": "Cloud Server", "cloudServerTypeTip": "Bitte beachte, dass der aktuelle Benutzer ausgeloggt wird beim wechsel des Cloud-Servers", "cloudLocal": "Lokal", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Die Supabase-URL darf nicht leer sein", + "cloudSupabaseAnonKey": "Supabase anonymer Schlüssel", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Der anonyme Schlüssel darf nicht leer sein", "cloudAppFlowy": "@:appName Cloud Beta", "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "Die Cloud-URL darf nicht leer sein", @@ -1095,8 +540,6 @@ "selfHostStart": "Falls du keinen Server hast, konsultiere bitte", "selfHostContent": "Dokument", "selfHostEnd": "um einen einen eigenen Server aufzusetzen", - "pleaseInputValidURL": "Bitte geben Sie eine gültige URL ein", - "changeUrl": "Ändern Sie die selbst gehostete URL in {}", "cloudURLHint": "Eingabe der Basis- URL Ihres Servers", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Eingbe der Websocket Adresse Ihres Servers", @@ -1128,46 +571,6 @@ "showNotificationsIcon": { "label": "Benachrichtigungssymbol anzeigen", "hint": "Deaktiviere diese Option, um das Benachrichtigungssymbol in der Seitenleiste auszublenden." - }, - "archiveNotifications": { - "allSuccess": "Alle Benachrichtigungen erfolgreich archiviert", - "success": "Benachrichtigung erfolgreich archiviert" - }, - "markAsReadNotifications": { - "allSuccess": "Alle erfolgreich als gelesen markiert", - "success": "Erfolgreich als gelesen markiert" - }, - "action": { - "markAsRead": "Als gelesen markieren", - "multipleChoice": "Mehrfachauswahl", - "archive": "Archiv" - }, - "settings": { - "settings": "Einstellungen", - "markAllAsRead": "Alles als gelesen markieren", - "archiveAll": "Alles archivieren" - }, - "emptyInbox": { - "title": "Noch keine Benachrichtigungen", - "description": "Du wirst hier über @Erwähnungen benachrichtigt" - }, - "emptyUnread": { - "title": "Keine ungelesenen Benachrichtigungen", - "description": "Du bist auf dem Laufenden!" - }, - "emptyArchived": { - "title": "Keine archivierten Benachrichtigungen", - "description": "Du hast noch keine Benachrichtigungen archiviert" - }, - "tabs": { - "inbox": "Posteingang", - "unread": "Ungelesen", - "archived": "Archiviert" - }, - "refreshSuccess": "Benachrichtigungen erfolgreich aktualisiert", - "titles": { - "notifications": "Benachrichtigungen", - "reminder": "Erinnerung" } }, "appearance": { @@ -1187,11 +590,6 @@ "documentSettings": { "cursorColor": "Cursor-Farbe", "selectionColor": "Auswahl-Farbe", - "width": "Dokumentbreite", - "changeWidth": "Ändern", - "pickColor": "Wähle eine Farbe", - "colorShade": "Farbschattierung", - "opacity": "Opazität", "hexEmptyError": "Hex-Farbe darf nicht leer sein", "hexLengthError": "Hex-Wert muss 6 Zeichen lang sein", "hexInvalidError": "Ungültiger Hex-Wert", @@ -1247,15 +645,12 @@ "members": { "title": "Mitglieder-Einstellungen", "inviteMembers": "Mitglieder einladen", - "inviteHint": "Per E-Mail einladen", "sendInvite": "Einladung senden", "copyInviteLink": "Kopiere Einladungslink", "label": "Mitglieder", "user": "Nutzer", "role": "Rolle", "removeFromWorkspace": "Vom Arbeitsbereich entfernen", - "removeFromWorkspaceSuccess": "Erfolgreich aus dem Arbeitsbereich entfernt", - "removeFromWorkspaceFailed": "Entfernen aus Arbeitsbereich fehlgeschlagen", "owner": "Besitzer", "guest": "Gast", "member": "Mitglied", @@ -1269,21 +664,13 @@ "one": "{} Mitglied", "other": "{} Mitglieder" }, - "inviteFailedDialogTitle": "Einladung konnte nicht gesendet werden", - "inviteFailedMemberLimit": "Das Mitgliederlimit wurde erreicht. Bitte führe ein Upgrade durch, um weitere Mitglieder einzuladen.", - "inviteFailedMemberLimitMobile": "Ihr Arbeitsbereich hat das Mitgliederlimit erreicht.", "memberLimitExceeded": "Du hast die maximal zulässige Mitgliederzahl für dein Benutzerkonto erreicht. Benötigst du weitere Mitglieder, um deine Arbeit fortsetzen zu können, erstelle auf Github bitte eine entsprechende Anfrage.", - "memberLimitExceededUpgrade": "upgrade", - "memberLimitExceededPro": "Mitgliederlimit erreicht. Wenn du mehr Mitglieder benötigst, kontaktiere", - "memberLimitExceededProContact": "support@appflowy.io", "failedToAddMember": "Mitglied konnte nicht hinzugefügt werden!", "addMemberSuccess": "Mitglied erfolgreich hinzugefügt", "removeMember": "Mitglied entfernen", - "areYouSureToRemoveMember": "Möchtest du dieses Mitglied wirklich entfernen?", + "areYouSureToRemoveMember": "Möchten Sie dieses Mitglied wirklich entfernen?", "inviteMemberSuccess": "Die Einladung wurde erfolgreich versendet", - "failedToInviteMember": "Das Einladen des Mitglieds ist fehlgeschlagen", - "workspaceMembersError": "Hoppla, da ist etwas schiefgelaufen", - "workspaceMembersErrorDescription": "Wir konnten die Mitgliederliste derzeit nicht laden. Bitte versuchen Sie es später noch einmal." + "failedToInviteMember": "Das Einladen des Mitglieds ist fehlgeschlagen" } }, "files": { @@ -1293,7 +680,7 @@ "doubleTapToCopy": "Zweimal tippen, um den Pfad zu kopieren", "restoreLocation": "@:appName-Standardpfad wiederherstellen", "customizeLocation": "Einen anderen Ordner öffnen", - "restartApp": "Bitte starte die App neu, damit die Änderungen wirksam werden.", + "restartApp": "Bitte starten Sie die App neu, damit die Änderungen wirksam werden.", "exportDatabase": "Datenbank exportieren", "selectFiles": "Dateien auswählen, die exportiert werden sollen", "selectAll": "Alle auswählen", @@ -1331,26 +718,9 @@ "email": "E-Mail", "tooltipSelectIcon": "Symbol auswählen", "selectAnIcon": "Ein Symbol auswählen", - "pleaseInputYourOpenAIKey": "Bitte gebe den AI-Schlüssel ein", - "clickToLogout": "Klicken, um den aktuellen Nutzer auszulogen", - "pleaseInputYourStabilityAIKey": "Bitte gebe den Stability AI Schlüssel ein" - }, - "mobile": { - "personalInfo": "Persönliche Informationen", - "username": "Nutzername", - "usernameEmptyError": "Der Nutzername darf nicht leer sein", - "about": "Über", - "pushNotifications": "Push Benachrichtigungen", - "support": "Support", - "joinDiscord": "Komm zu uns auf Discord", - "privacyPolicy": "Datenschutz", - "userAgreement": "Nutzungsbedingungen", - "termsAndConditions": "Geschäftsbedingungen", - "userprofileError": "Das Nutzerprofil konnte nicht geladen werden", - "userprofileErrorDescription": "Bitte abmelden und wieder anmelden, um zu prüfen ob das Problem weiterhin bestehen bleibt.", - "selectLayout": "Layout auswählen", - "selectStartingDay": "Ersten Tag auswählen", - "version": "Version" + "pleaseInputYourOpenAIKey": "Bitte gebe den OpenAI-Schlüssel ein", + "pleaseInputYourStabilityAIKey": "Bitte gebe den Stability AI Schlüssel ein", + "clickToLogout": "Klicken, um den aktuellen Nutzer auszulogen" }, "shortcuts": { "shortcutsLabel": "Tastenkürzel", @@ -1373,10 +743,27 @@ "textAlignCenter": "Text zentriert ausrichten", "textAlignRight": "Text rechtsbündig ausrichten" } + }, + "mobile": { + "personalInfo": "Persönliche Informationen", + "username": "Nutzername", + "usernameEmptyError": "Der Nutzername darf nicht leer sein", + "about": "Über", + "pushNotifications": "Push Benachrichtigungen", + "support": "Support", + "joinDiscord": "Komm zu uns auf Discord", + "privacyPolicy": "Datenschutz", + "userAgreement": "Nutzungsbedingungen", + "termsAndConditions": "Geschäftsbedingungen", + "userprofileError": "Das Nutzerprofil konnte nicht geladen werden", + "userprofileErrorDescription": "Bitte abmelden und wieder anmelden, um zu prüfen ob das Problem weiterhin bestehen bleibt.", + "selectLayout": "Layout auswählen", + "selectStartingDay": "Ersten Tag auswählen", + "version": "Version" } }, "grid": { - "deleteView": "Möchtest du diese Ansicht wirklich löschen?", + "deleteView": "Möchten Sie diese Ansicht wirklich löschen?", "createView": "Neu", "title": { "placeholder": "Unbenannt" @@ -1407,13 +794,6 @@ "deleteView": "Anslicht löschen", "numberOfVisibleFields": "{} angezeigt" }, - "filter": { - "empty": "Keine aktiven Filter", - "addFilter": "Filter hinzufügen", - "cannotFindCreatableField": "Es wurde kein geeignetes Feld zum Filtern gefunden.", - "conditon": "Bedingung", - "where": "Wo" - }, "textFilter": { "contains": "Enthält", "doesNotContain": "Beinhaltet nicht", @@ -1459,12 +839,9 @@ "between": "Ist zwischen", "empty": "Ist leer", "notEmpty": "Ist nicht leer", - "startDate": "Startdatum", - "endDate": "Enddatum", "choicechipPrefix": { "before": "Vorher", "after": "Danach", - "between": "Zwischen", "onOrBefore": "Am oder davor", "onOrAfter": "Während oder danach", "isEmpty": "leer", @@ -1482,7 +859,6 @@ "isNotEmpty": "nicht leer" }, "field": { - "label": "Eigenschaft", "hide": "Verstecken", "show": "Anzeigen", "insertLeft": "Links einfügen", @@ -1491,12 +867,11 @@ "delete": "Löschen", "wrapCellContent": "Zeilenumbruch", "clear": " Zelleninhalte löschen", - "switchPrimaryFieldTooltip": "Feldtyp des Primärfelds kann nicht geändert werden", "textFieldName": "Text", "checkboxFieldName": "Kontrollkästchen", "dateFieldName": "Datum", "updatedAtFieldName": "Letzte Änderungszeit", - "createdAtFieldName": "Erstellungsdatum", + "createdAtFieldName": "Zeit geschaffen", "numberFieldName": "Zahlen", "singleSelectFieldName": "Wählen", "multiSelectFieldName": "Mehrfachauswahl", @@ -1504,10 +879,6 @@ "checklistFieldName": "Checkliste", "relationFieldName": "Beziehung", "summaryFieldName": "KI-Zusammenfassung", - "timeFieldName": "Zeit", - "mediaFieldName": "Dateien und Medien", - "translateFieldName": "AI-Übersetzen", - "translateTo": "Übersetzen in", "numberFormat": "Zahlenformat", "dateFormat": "Datumsformat", "includeTime": "Zeitangabe", @@ -1536,7 +907,6 @@ "addOption": "Option hinzufügen", "editProperty": "Eigenschaft bearbeiten", "newProperty": "Neue Eigenschaft", - "openRowDocument": "Als Seite öffnen", "deleteFieldPromptMessage": "Sicher? Diese Eigenschaft wird gelöscht", "clearFieldPromptMessage": "Bist du dir sicher? Alle Zelleninhalte in dieser Spalte werden gelöscht!", "newColumn": "Neue Spalte", @@ -1568,11 +938,10 @@ "cannotFindCreatableField": "Es konnte kein geeignetes Feld zum Sortieren gefunden werden", "deleteAllSorts": "Alle Sortierungen entfernen", "addSort": "Sortierung hinzufügen", - "removeSorting": "Möchtest du die Sortierung entfernen?", - "fieldInUse": "Du sortierst bereits nach diesem Feld" + "removeSorting": "Möchten Sie die Sortierung entfernen?", + "fieldInUse": "Sie sortieren bereits nach diesem Feld" }, "row": { - "label": "Reihe", "duplicate": "Duplikat", "delete": "Löschen", "titlePlaceholder": "Unbenannt", @@ -1580,19 +949,13 @@ "copyProperty": "Eigenschaft in die Zwischenablage kopiert", "count": "Zählen", "newRow": "Neue Zeile", - "loadMore": "Mehr laden", "action": "Aktion", "add": "Klicken, um unten hinzuzufügen", "drag": "Ziehen, um zu verschieben", - "deleteRowPrompt": "Bist du sicher, dass du diese Zeile löschen willst? Diese Aktion kann nicht rückgängig gemacht werden", - "deleteCardPrompt": "Bist du sicher, dass du diese Karte löschen willst? Diese Aktion kann nicht rückgängig gemacht werden", "dragAndClick": "Ziehen, um zu verschieben. Klicke, um das Menü zu öffnen", "insertRecordAbove": "Füge Datensatz oben ein", "insertRecordBelow": "Füge Datensatz unten ein", - "noContent": "Kein Inhalt", - "reorderRowDescription": "Zeile neu anordnen", - "createRowAboveDescription": "Erstelle oben eine Zeile", - "createRowBelowDescription": "Unten eine Zeile einfügen" + "noContent": "Kein Inhalt" }, "selectOption": { "create": "Erstellen", @@ -1616,8 +979,8 @@ "tagName": "Tag-Name" }, "checklist": { - "taskHint": "Aufgabenbeschreibung", - "addNew": "Füge eine Aufgabe hinzu", + "taskHint": "Aufgbenbeschreibbung", + "addNew": "Fügen Sie einen Artikel hinzu", "submitNewTask": "Erstellen", "hideComplete": "Blende abgeschlossene Aufgaben aus", "showComplete": "Zeige alle Aufgaben" @@ -1625,7 +988,8 @@ "url": { "launch": "Im Browser öffnen", "copy": "Webadresse kopieren", - "textFieldHint": "Gebe eine URL ein" + "textFieldHint": "Geben Sie eine URL ein", + "copiedNotification": "In die Zwischenablage kopiert!" }, "relation": { "relatedDatabasePlaceLabel": "Verwandte Datenbank", @@ -1637,7 +1001,7 @@ "linkedRowListLabel": "{count} verknüpfte Zeilen", "unlinkedRowListLabel": "Eine weitere Zeile verknüpfen" }, - "menuName": "Datentabelle", + "menuName": "Raster", "referencedGridPrefix": "Sicht von", "calculate": "berechnet", "calculationTypeLabel": { @@ -1652,24 +1016,6 @@ "countEmptyShort": "leer", "countNonEmpty": "Zahl nicht leer", "countNonEmptyShort": "nicht leer" - }, - "media": { - "rename": "Umbenennen", - "download": "Herunterladen", - "expand": "Erweitern", - "delete": "Löschen", - "moreFilesHint": "+{}", - "addFileOrImage": "Datei oder Link hinzufügen", - "attachmentsHint": "{}", - "addFileMobile": "Datei hinzufügen", - "extraCount": "+{}", - "deleteFileDescription": "Möchten Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", - "showFileNames": "Dateinamen anzeigen", - "downloadSuccess": "Datei heruntergeladen", - "downloadFailedToken": "Datei konnte nicht heruntergeladen werden, Benutzertoken nicht verfügbar", - "setAsCover": "Als Cover festlegen", - "openInBrowser": "Im Browser öffnen", - "embedLink": "Dateilink einbetten" } }, "document": { @@ -1684,55 +1030,15 @@ "createANewBoard": "Ein neues Board erstellen" }, "grid": { - "selectAGridToLinkTo": "Eine Datentabelle zum Verknüpfen auswählen", - "createANewGrid": "Eine neue Datentabelle erstellen" + "selectAGridToLinkTo": "Ein Raster zum Verknüpfen auswählen", + "createANewGrid": "Ein neues Raster erstellen" }, "calendar": { "selectACalendarToLinkTo": "Einen Kalender zum Verknüpfen auswählen", "createANewCalendar": "Einen neuen Kalender erstellen" }, "document": { - "selectADocumentToLinkTo": "Eine Datentabelle zum Verknüpfen auswählen" - }, - "name": { - "text": "Text", - "heading1": "Überschrift 1", - "heading2": "Überschrift 2", - "heading3": "Überschrift 3", - "image": "Bild", - "bulletedList": "Aufzählungsliste", - "numberedList": "Nummerierte Liste", - "todoList": "Aufgabenliste", - "doc": "Dokument", - "linkedDoc": "Link zur Seite", - "grid": "Raster", - "linkedGrid": "Verknüpftes Raster", - "kanban": "Kanban", - "linkedKanban": "Verknüpftes Kanban", - "calendar": "Kalender", - "linkedCalendar": "Verknüpfter Kalender", - "quote": "Zitat", - "divider": "Trenner", - "table": "Tabelle", - "outline": "Gliederung", - "mathEquation": "Mathematische Gleichung", - "code": "Code", - "toggleList": "Liste ein-/ausblenden", - "emoji": "Emoji", - "aiWriter": "KI-Autor", - "dateOrReminder": "Datum oder Erinnerung", - "photoGallery": "Fotogalerie", - "file": "Datei" - }, - "subPage": { - "name": "Dokument", - "keyword1": "Unterseite", - "keyword2": "Seite", - "keyword3": "untergeordnete Seite", - "keyword4": "Seite einfügen", - "keyword6": "neue Seite", - "keyword7": "Seite erstellen", - "keyword8": "Dokument" + "selectADocumentToLinkTo": "Ein Raster zum Verknüpfen auswählen" } }, "selectionMenu": { @@ -1741,28 +1047,27 @@ }, "plugins": { "referencedBoard": "Referenziertes Board", - "referencedGrid": "Referenzierte Datentabelle", + "referencedGrid": "Referenziertes Raster", "referencedCalendar": "Referenzierter Kalender", "referencedDocument": "Referenziertes Dokument", - "autoGeneratorMenuItemName": "AI-Autor", - "autoGeneratorTitleName": "AI: Die KI bitten, etwas zu schreiben ...", + "autoGeneratorMenuItemName": "OpenAI-Autor", + "autoGeneratorTitleName": "OpenAI: Die KI bitten, etwas zu schreiben ...", "autoGeneratorLearnMore": "Mehr erfahren", "autoGeneratorGenerate": "Erstellen", - "autoGeneratorHintText": "AI fragen ...", - "autoGeneratorCantGetOpenAIKey": "Der AI-Schlüssel kann nicht abgerufen werden", + "autoGeneratorHintText": "OpenAI fragen ...", + "autoGeneratorCantGetOpenAIKey": "Der OpenAI-Schlüssel kann nicht abgerufen werden", "autoGeneratorRewrite": "Umschreiben", "smartEdit": "KI-Assistenten", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Korrigiere Rechtschreibung", "warning": "⚠️ KI-Antworten können ungenau oder irreführend sein.", "smartEditSummarize": "Zusammenfassen", "smartEditImproveWriting": "Das Geschriebene verbessern", "smartEditMakeLonger": "Länger machen", - "smartEditCouldNotFetchResult": "Das Ergebnis konnte nicht von AI abgerufen werden", - "smartEditCouldNotFetchKey": "Der AI-Schlüssel konnte nicht abgerufen werden", - "smartEditDisabled": "AI in den Einstellungen verbinden", - "appflowyAIEditDisabled": "Melde dich an, um KI-Funktionen zu aktivieren", - "discardResponse": "Möchtest du die KI-Antworten verwerfen?", + "smartEditCouldNotFetchResult": "Das Ergebnis konnte nicht von OpenAI abgerufen werden", + "smartEditCouldNotFetchKey": "Der OpenAI-Schlüssel konnte nicht abgerufen werden", + "smartEditDisabled": "OpenAI in den Einstellungen verbinden", + "discardResponse": "Möchten Sie die KI-Antworten verwerfen?", "createInlineMathEquation": "Formel erstellen", "fonts": "Schriftarten", "insertDate": "Datum einfügen", @@ -1773,35 +1078,6 @@ "bulletedList": "Stichpunktliste", "todoList": "Offene Aufgaben Liste", "callout": "Hervorhebung", - "simpleTable": { - "moreActions": { - "color": "Farbe", - "align": "Ausrichten", - "delete": "Löschen", - "duplicate": "duplizieren", - "insertLeft": "Links einfügen", - "insertRight": "Rechts einfügen", - "insertAbove": "Oben einfügen", - "insertBelow": "Unten einfügen", - "headerColumn": "Kopfspalte", - "headerRow": "Kopfzeile", - "clearContents": "Klarer Inhalt", - "setToPageWidth": "Auf Seitenbreite einstellen", - "distributeColumnsWidth": "Spalten gleichmäßig verteilen", - "duplicateRow": "Zeile duplizieren", - "duplicateColumn": "Spalte duplizieren", - "textColor": "Textfarbe", - "cellBackgroundColor": "Zellen-Hintergrundfarbe", - "duplicateTable": "Tabelle duplizieren" - }, - "clickToAddNewRow": "Klicken Sie hier, um eine neue Zeile hinzuzufügen", - "clickToAddNewColumn": "Klicken Sie hier, um eine neue Spalte hinzuzufügen", - "clickToAddNewRowAndColumn": "Klicken Sie hier, um eine neue Zeile und Spalte hinzuzufügen", - "headerName": { - "table": "Tabelle", - "alignText": "Text ausrichten" - } - }, "cover": { "changeCover": "Titelbild wechseln", "colors": "Farben", @@ -1817,7 +1093,6 @@ "back": "Zurück", "saveToGallery": "In der Galerie speichern", "removeIcon": "Symbol entfernen", - "removeCover": "Cover entfernen", "pasteImageUrl": "Bild-URL einfügen", "or": "ODER", "pickFromFiles": "Dateien auswählen", @@ -1826,7 +1101,7 @@ "addIcon": "Symbol hinzufügen", "changeIcon": "Symbol wechseln", "coverRemoveAlert": "Nach dem Löschen wird es aus dem Titelbild entfernt.", - "alertDialogConfirmation": "Sicher, dass du weitermachen willst?" + "alertDialogConfirmation": "Sicher, dass Sie weitermachen wollen?" }, "mathEquation": { "name": "Mathematische Formel", @@ -1836,11 +1111,9 @@ "optionAction": { "click": "Klicken", "toOpenMenu": " um das Menü zu öffnen", - "drag": "Ziehen", - "toMove": " bewegen", "delete": "Löschen", "duplicate": "Duplikat", - "turnInto": "Umwandeln in", + "turnInto": "Einbiegen in", "moveUp": "Nach oben verschieben", "moveDown": "Nach unten verschieben", "color": "Farbe", @@ -1849,42 +1122,20 @@ "center": "Zentriert", "right": "Rechts", "defaultColor": "Standard", - "depth": "Tiefe", - "copyLinkToBlock": "Link zum Block kopieren" + "depth": "Tiefe" }, "image": { - "addAnImage": "Ein Bild hinzufügen", "copiedToPasteBoard": "Der Bildlink wurde in die Zwischenablage kopiert", - "addAnImageDesktop": "Bild(er) hier ablegen oder klicken, um Bild(er) hinzuzufügen", - "addAnImageMobile": "Klicke, um ein oder mehrere Bilder hinzuzufügen", - "dropImageToInsert": "Zum Einfügen Bilder hier ablegen", + "addAnImage": "Ein Bild hinzufügen", "imageUploadFailed": "Bild hochladen gescheitert", - "imageDownloadFailed": "Das Hochladen des Bilds ist fehlgeschlagen. Bitte versuche es erneut.", - "imageDownloadFailedToken": "Das Hochladen des Bilds ist aufgrund eines fehlenden Benutzertokens fehlgeschlagen. Bitte versuche es erneut.", "errorCode": "Fehlercode" }, - "photoGallery": { - "name": "Fotogallerie", - "imageKeyword": "Bild", - "imageGalleryKeyword": "Bildergalerie", - "photoKeyword": "Foto", - "photoBrowserKeyword": "Fotobrowser", - "galleryKeyword": "Galerie", - "addImageTooltip": "Bild hinzufügen", - "changeLayoutTooltip": "Layout ändern", - "browserLayout": "Browser", - "gridLayout": "Datentabelle", - "deleteBlockTooltip": "Ganze Galerie löschen" - }, - "math": { - "copiedToPasteBoard": "Die mathematische Gleichung wurde in die Zwischenablage kopiert" - }, "urlPreview": { "copiedToPasteBoard": "Der Link wurde in die Zwischenablage kopiert", "convertToLink": "Konvertieren zum eingebetteten Link" }, "outline": { - "addHeadingToCreateOutline": "Füge Überschriften hinzu, um ein Inhaltsverzeichnis zu erstellen.", + "addHeadingToCreateOutline": "Fügen Sie Überschriften hinzu, um ein Inhaltsverzeichnis zu erstellen.", "noMatchHeadings": "Keine passenden Überschriften gefunden." }, "table": { @@ -1898,8 +1149,7 @@ "contextMenu": { "copy": "Kopieren", "cut": "Ausschneiden", - "paste": "Einfügen", - "pasteAsPlainText": "Als einfachen Text einfügen" + "paste": "Einfügen" }, "action": "Aktionen", "database": { @@ -1920,46 +1170,13 @@ "invalidVideoUrl": "Die Quell-URL wird noch nicht unterstützt.", "invalidVideoUrlYouTube": "YouTube wird noch nicht unterstützt.", "supportedFormats": "Unterstützte Formate: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "Datei", - "uploadTab": "Hochladen", - "uploadMobile": "Wählen Sie eine Datei", - "uploadMobileGallery": "Aus der Fotogalerie", - "networkTab": "Link integrieren", - "placeholderText": "Klicke oder ziehe eine Datei hoch, um sie hochzuladen", - "placeholderDragging": "Lege die hochzuladende Datei hier ab", - "dropFileToUpload": "Lege die hochzuladende Datei hier ab", - "fileUploadHint": "Lege die hochzuladende Datei hier ab\noder klicke, um eine Datei auszuwählen.", - "fileUploadHintSuffix": "Durchsuchen", - "networkHint": "Gebe einen Link zu einer Datei ein", - "networkUrlInvalid": "Ungültige URL. Bitte korrigiere die URL und versuche es erneut.", - "networkAction": "Dateilink einbetten", - "fileTooBigError": "Die Dateigröße ist zu groß. Bitte lade eine Datei mit einer Größe von weniger als 10 MB hoch.", - "renameFile": { - "title": "Datei umbenennen", - "description": "Gebe den neuen Namen für diese Datei ein", - "nameEmptyError": "Der Dateiname darf nicht leer bleiben." - }, - "uploadedAt": "Hochgeladen am {}", - "linkedAt": "Link hinzugefügt am {}", - "failedToOpenMsg": "Öffnen fehlgeschlagen, Datei nicht gefunden" - }, - "subPage": { - "errors": { - "failedDeletePage": "Seite konnte nicht gelöscht werden", - "failedCreatePage": "Seite konnte nicht erstellt werden", - "failedMovePage": "Seite konnte nicht in dieses Dokument verschoben werden", - "failedDuplicatePage": "Seite konnte nicht dupliziert werden", - "failedDuplicateFindView": "Seite konnte nicht dupliziert werden - Originalansicht nicht gefunden" - } } }, "outlineBlock": { "placeholder": "Inhaltsverzeichnis" }, "textBlock": { - "placeholder": "Gebe „/“ für Inhaltsblöcke ein" + "placeholder": "Geben Sie „/“ für Inhaltsblöcke ein" }, "title": { "placeholder": "Ohne Titel" @@ -1975,8 +1192,8 @@ "placeholder": "Bild-URL eingeben" }, "ai": { - "label": "Bild mit AI erstellen", - "placeholder": "Bitte den Prompt für AI eingeben, um ein Bild zu erstellen" + "label": "Bild mit OpenAI erstellen", + "placeholder": "Bitte den Prompt für OpenAI eingeben, um ein Bild zu erstellen" }, "stability_ai": { "label": "Bild mit Stability AI erstellen", @@ -1988,8 +1205,7 @@ "invalidImageSize": "Die Bildgröße muss kleiner als 5 MB sein", "invalidImageFormat": "Das Bildformat wird nicht unterstützt. Unterstützte Formate: JPEG, PNG, GIF, SVG", "invalidImageUrl": "Ungültige Bild-URL", - "noImage": "Keine Datei oder Verzeichnis", - "multipleImagesFailed": "Ein oder mehrere Bilder konnten nicht hochgeladen werden. Bitte versuche es erneut." + "noImage": "Keine Datei oder Verzeichnis" }, "embedLink": { "label": "Eingebetteter Link", @@ -1999,30 +1215,15 @@ "label": "Unsplash" }, "searchForAnImage": "Nach einem Bild suchen", - "pleaseInputYourOpenAIKey": "biitte den AI Schlüssel in der Einstellungsseite eingeben", + "pleaseInputYourOpenAIKey": "biitte den OpenAI Schlüssel in der Einstellungsseite eingeben", + "pleaseInputYourStabilityAIKey": "biitte den Stability AI Schlüssel in der Einstellungsseite eingeben", "saveImageToGallery": "Bild speichern", "failedToAddImageToGallery": "Das Bild konnte nicht zur Galerie hinzugefügt werden", "successToAddImageToGallery": "Das Bild wurde zur Galerie hinzugefügt werden", "unableToLoadImage": "Das Bild konnte nicht geladen werden", "maximumImageSize": "Die maximal unterstützte Upload-Bildgröße beträgt 10 MB", "uploadImageErrorImageSizeTooBig": "Die Bildgröße muss weniger als 10 MB betragen", - "imageIsUploading": "Bild wird hochgeladen", - "openFullScreen": "Im Vollbildmodus öffnen", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Vorheriges Bild", - "nextImageTooltip": "Nächstes Bild", - "zoomOutTooltip": "Rauszoomen", - "zoomInTooltip": "Hineinzoomen", - "changeZoomLevelTooltip": "Zoomstufe ändern", - "openLocalImage": "Bild öffnen", - "downloadImage": "Bild herunterladen", - "closeViewer": "Interaktive Anzeige schließen", - "scalePercentage": "{}%", - "deleteImageTooltip": "Bild löschen" - } - }, - "pleaseInputYourStabilityAIKey": "biitte den Stability AI Schlüssel in der Einstellungsseite eingeben" + "imageIsUploading": "Bild wird hochgeladen" }, "codeBlock": { "language": { @@ -2055,10 +1256,7 @@ "tooltip": "Klicken, um die Seite zu öffnen" }, "deleted": "gelöscht", - "deletedContent": "Dieser Inhalt existiert nicht oder wurde gelöscht", - "noAccess": "Kein Zugriff", - "deletedPage": "Gelöschte Seite", - "trashHint": " - im Papierkorb" + "deletedContent": "Dieser Inhalt existiert nicht oder wurde gelöscht" }, "toolbar": { "resetToDefaultFont": "Auf den Standard zurücksetzen" @@ -2066,23 +1264,16 @@ "errorBlock": { "theBlockIsNotSupported": "Die aktuelle Version unterstützt diesen Block nicht.", "clickToCopyTheBlockContent": "Hier klicken, um den Blockinhalt zu kopieren", - "blockContentHasBeenCopied": "Der Blockinhalt wurde kopiert.", - "copyBlockContent": "Blockinhalt kopieren" + "blockContentHasBeenCopied": "Der Blockinhalt wurde kopiert." }, "mobilePageSelector": { "title": "Seite auswählen", "failedToLoad": "Seitenliste konnte nicht geladen werden", "noPagesFound": "Keine Seiten gefunden" - }, - "attachmentMenu": { - "choosePhoto": "Foto auswählen", - "takePicture": "Ein Foto machen", - "chooseFile": "Datei auswählen" } }, "board": { "column": { - "label": "Spalte", "createNewCard": "Neu", "renameGroupTooltip": "Drücken, um die Gruppe umzubenennen", "createNewColumn": "Eine neue Gruppe hinzufügen", @@ -2113,7 +1304,6 @@ "ungroupedButtonTooltip": "Enthält Karten, die keiner Gruppe zugeordnet sind", "ungroupedItemsTitle": "Klicke, um dem Board hinzuzufügen", "groupBy": "Gruppiert nach", - "groupCondition": "Gruppenbedingung", "referencedBoardPrefix": "Sicht von", "notesTooltip": "Notizen vorhanden", "mobile": { @@ -2122,22 +1312,8 @@ "showGroupContent": "Sicher, dass diese Gruppe auf dem Board angezeigt werden soll?", "failedToLoad": "Boardansicht konnte nicht geladen werden" }, - "dateCondition": { - "weekOf": "Woche von {} - {}", - "today": "Heute", - "yesterday": "Gestern", - "tomorrow": "Morgen", - "lastSevenDays": "Letzte 7 Tage", - "nextSevenDays": "Nächste 7 Tage", - "lastThirtyDays": "Letzte 30 Tage", - "nextThirtyDays": "Nächste 30 Tage" - }, "noGroup": "Keine Gruppierung nach Eigenschaft", - "noGroupDesc": "Board-Ansichten benötigen eine Eigenschaft zum Gruppieren, um angezeigt zu werden", - "media": { - "cardText": "{} {}", - "fallbackName": "Dateien" - } + "noGroupDesc": "Board-Ansichten benötigen eine Eigenschaft zum Gruppieren, um angezeigt zu werden" }, "calendar": { "menuName": "Kalender", @@ -2172,7 +1348,7 @@ "other": "{count} Ereignisse ohne Datum" }, "unscheduledEventsTitle": "Ungeplante Events", - "clickToAdd": "Klicken zum hinzufügen im Kalender", + "clickToAdd": "Klicken Sie, um es zum Kalender hinzuzufügen", "name": "Kalendereinstellungen", "clickToOpen": "Hier klicken, um den Eintrag zu öffnen" }, @@ -2182,14 +1358,11 @@ }, "errorDialog": { "title": "@:appName-Fehler", - "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das deinen Fehler beschreibt.", - "howToFixFallbackHint1": "Wir entschuldigen uns für die Unannehmlichkeiten! Melden Sie ein Problem auf unserer ", - "howToFixFallbackHint2": " Seite, die Ihren Fehler beschreibt.", + "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das Ihren Fehler beschreibt.", "github": "Auf GitHub ansehen" }, "search": { "label": "Suchen", - "sidebarSearchIcon": "Suchen und schnell zu einer Seite springen", "placeholder": { "actions": "Suchaktionen..." } @@ -2202,8 +1375,8 @@ }, "unSupportBlock": "Die aktuelle Version unterstützt diesen Block nicht.", "views": { - "deleteContentTitle": "Möchtest du den {pageType} wirklich löschen?", - "deleteContentCaption": "Wenn du diesen {pageType} löschst, kannst du ihn aus dem Papierkorb wiederherstellen." + "deleteContentTitle": "Möchten Sie den {pageType} wirklich löschen?", + "deleteContentCaption": "Wenn Sie diesen {pageType} löschen, können Sie ihn aus dem Papierkorb wiederherstellen." }, "colors": { "custom": "Individuell", @@ -2247,12 +1420,11 @@ "medium": "Mittel", "mediumDark": "Mitteldunkel", "dark": "Dunkel" - }, - "openSourceIconsFrom": "Open-Source-Icons von" + } }, "inlineActions": { "noResults": "Keine Ergebnisse", - "recentPages": "Kürzliche Seiten", + "recentPages": "Letzte Seiten", "pageReference": "Seitenreferenz", "docReference": "Dokumentverweis", "boardReference": "Board-Referenz", @@ -2339,10 +1511,7 @@ }, "error": { "weAreSorry": "Das tut uns leid", - "loadingViewError": "Wir haben Schwierigkeiten diese Ansicht zu laden. Bitte prüfe die Internetverbindung, lade die App neu und zögere nicht, das Team zu kontaktieren, falls das Problem weiterhin besteht.", - "syncError": "Daten werden nicht von einem anderen Gerät synchronisiert", - "syncErrorHint": "Bitte öffnen Sie diese Seite erneut auf dem Gerät, auf dem sie zuletzt bearbeitet wurde, und öffnen Sie die Seite dann erneut auf dem aktuellen Gerät.", - "clickToCopy": "Klicken Sie hier, um den Fehlercode zu kopieren" + "loadingViewError": "Wir haben Schwierigkeiten diese Ansicht zu laden. Bitte prüfe die Internetverbindung, lade die App neu und zögere Sie nicht, dass Team zu kontaktieren, falls das Problem weiterhin besteht." }, "editor": { "bold": "Fett", @@ -2511,17 +1680,10 @@ "deleteAccount": { "title": "Benutzerkonto löschen", "subtitle": "Benutzerkonto inkl. deiner persönlicher Daten unwiderruflich löschen.", - "description": "Löschen Sie Ihr Konto dauerhaft und entfernen Sie den Zugriff auf alle Arbeitsbereiche.", "deleteMyAccount": "Mein Benutzerkonto löschen", "dialogTitle": "Benutzerkonto löschen", "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", - "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst.", - "confirmHint1": "Geben Sie zur Bestätigung bitte „@:newSettings.myAccount.deleteAccount.confirmHint3“ ein.", - "confirmHint3": "MEIN KONTO LÖSCHEN", - "checkToConfirmError": "Sie müssen das Kontrollkästchen aktivieren, um das Löschen zu bestätigen", - "failedToGetCurrentUser": "Aktuelle Benutzer-E-Mail konnte nicht abgerufen werden.", - "confirmTextValidationFailed": "Ihr Bestätigungstext stimmt nicht mit „@:newSettings.myAccount.deleteAccount.confirmHint3“ überein.", - "deleteAccountSuccess": "Konto erfolgreich gelöscht" + "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst." } }, "workplace": { @@ -2564,11 +1726,9 @@ "unsplash": "Unsplash", "pageCover": "Deckblatt", "none": "Keines", + "photoPermissionDescription": "Erlaube den Zugriff auf die Fotobibliothek zum Hochladen von Bildern.", "openSettings": "Einstellungen öffnen", "photoPermissionTitle": "@:appName möchte auf deine Fotobibliothek zugreifen", - "photoPermissionDescription": "Erlaube den Zugriff auf die Fotobibliothek zum Hochladen von Bildern.", - "cameraPermissionTitle": "@:appName möchte auf Ihre Kamera zugreifen", - "cameraPermissionDescription": "@:appName benötigt Zugriff auf Ihre Kamera, damit Sie Bilder von der Kamera zu Ihren Dokumenten hinzufügen können.", "doNotAllow": "Nicht zulassen", "image": "Bild" }, @@ -2580,288 +1740,6 @@ "loadingTooltip": "Wir suchen nach Ergebnissen ...", "betaLabel": "BETA", "betaTooltip": "Wir unterstützen derzeit nur die Suche nach Seiten", - "fromTrashHint": "Aus dem Mülleimer", - "noResultsHint": "Wir haben nicht gefunden, wonach du gesucht hast. Versuche nach einem anderen Begriff zu suchen.", - "clearSearchTooltip": "Suchfeld löschen" - }, - "space": { - "delete": "Löschen", - "deleteConfirmation": "Löschen:", - "deleteConfirmationDescription": "Alle Seiten innerhalb dieses Space werden gelöscht und in den Papierkorb verschoben.", - "rename": "Domäne umbennen", - "changeIcon": "Symbol ändern", - "manage": "Domäne verwalten", - "addNewSpace": "Domäne erstellen", - "collapseAllSubPages": "Alle Unterseiten einklappen", - "createNewSpace": "Eine neue Domäne erstellen", - "createSpaceDescription": "Erstellen mehrere öffentliche und private Domänen, um deine Arbeit besser zu organisieren.", - "spaceName": "Name der Domäne", - "spaceNamePlaceholder": "z.B. Marketing, Entwicklung, Personalabteilung", - "permission": "Berechtigung", - "publicPermission": "Öffentlich", - "publicPermissionDescription": "Alle Mitglieder des Arbeitsbereichs mit Vollzugriff", - "privatePermission": "Privat", - "privatePermissionDescription": "Nur du hast Zugang zu dieser Domäne", - "spaceIconBackground": "Hintergrundfarbe", - "spaceIcon": "Symbol", - "dangerZone": "Gefahrenzone", - "unableToDeleteLastSpace": "Die letzte Domäne kann nicht gelöscht werden", - "unableToDeleteSpaceNotCreatedByYou": "Von anderen erstellte Domänen können nicht gelöscht werden", - "enableSpacesForYourWorkspace": "Domänen für deinen Arbeitsbereich aktivieren", - "title": "Domänen", - "defaultSpaceName": "Allgemein", - "upgradeSpaceTitle": "Domänen aktivieren", - "upgradeSpaceDescription": "Erstelle mehrere öffentliche und private Domänen, um deinen Arbeitsbereich besser zu organisieren.", - "upgrade": "Update", - "upgradeYourSpace": "Mehrere Domänen erstellen", - "quicklySwitch": "Schnell zur nächsten Domäne wechseln", - "duplicate": "Domäne duplizieren", - "movePageToSpace": "Seite in die Domäne verschieben", - "cannotMovePageToDatabase": "Seite kann nicht in die Datenbank verschoben werden", - "switchSpace": "Domäne wechseln", - "spaceNameCannotBeEmpty": "Der Space-Name darf nicht leer sein", - "success": { - "deleteSpace": "Domäne erfolgreich gelöscht", - "renameSpace": "Domäne erfolgreich umbenannt", - "duplicateSpace": "Domäne erfolgreich dupliziert", - "updateSpace": "Domäne erfolgreich angepasst" - }, - "error": { - "deleteSpace": "Löschung der Domäne fehlgeschlagen", - "renameSpace": "Umbenennung der Domäne fehlgeschlagen", - "duplicateSpace": "Duplizierung der Domäne fehlgeschlagen", - "updateSpace": "Anpassung der Domäne fehlgeschlagen" - }, - "createSpace": "Erstelle Domäne", - "manageSpace": "Verwalte Domäne", - "renameSpace": "Domäne umbenennen", - "mSpaceIconColor": "Domänen-Iconfarbe", - "mSpaceIcon": "Domänen-Icon" - }, - "publish": { - "hasNotBeenPublished": "Diese Seite wurde noch nicht veröffentlicht", - "reportPage": "Berichtsseite", - "databaseHasNotBeenPublished": "Das Veröffentlichen einer Datenbank wird noch nicht unterstützt.", - "createdWith": "Erstellt mit", - "downloadApp": "AppFlowy herunterladen", - "copy": { - "codeBlock": "Der Inhalt des Codeblocks wurde in die Zwischenablage kopiert", - "imageBlock": "Der Bildlink wurde in die Zwischenablage kopiert", - "mathBlock": "Die mathematische Gleichung wurde in die Zwischenablage kopiert" - }, - "containsPublishedPage": "Diese Seite enthält eine oder mehrere veröffentlichte Seiten. Wenn du fortfährst, werden sie nicht mehr veröffentlicht. Möchtest du mit dem Löschen fortfahren?", - "publishSuccessfully": "Erfolgreich veröffentlicht", - "unpublishSuccessfully": "Veröffentlichung erfolgreich aufgehoben", - "publishFailed": "Veröffentlichung fehlgeschlagen", - "unpublishFailed": "Die Veröffentlichung konnte nicht rückgängig gemacht werden.", - "noAccessToVisit": "Kein Zugriff auf diese Seite...", - "createWithAppFlowy": "Erstelle eine Website mit AppFlowy", - "fastWithAI": "Schnell und einfach mit KI.", - "tryItNow": "Probiere es jetzt", - "onlyGridViewCanBePublished": "Nur die Datentabellenansicht kann veröffentlicht werden", - "database": { - "zero": "{} ausgewählte Ansichten veröffentlichen", - "one": "{} ausgewählte Ansicht veröffentlichen", - "many": "{} ausgewählte Ansichten veröffentlichen", - "other": "{} ausgewählte Ansichten veröffentlichen" - }, - "mustSelectPrimaryDatabase": "Die primäre Ansicht muss ausgewählt sein", - "noDatabaseSelected": "Keine Datenbank ausgewählt, bitte wähle mindestens eine Datenbank aus.", - "unableToDeselectPrimaryDatabase": "Die Auswahl der primären Datenbank kann nicht aufgehoben werden.", - "saveThisPage": "Diese Seite speichern", - "duplicateTitle": "Wo möchten Sie hinzufügen", - "selectWorkspace": "Einen Arbeitsbereich auswählen", - "addTo": "Hinzufügen zu", - "duplicateSuccessfully": "Duplizierung erfolgreich. Möchtest du die Dokumente anzeigen?", - "duplicateSuccessfullyDescription": "Du hast die App nicht? Dein Download beginnt automatisch, nachdem du auf „Herunterladen“ geklickt hast.", - "downloadIt": "Herunterladen", - "openApp": "In App öffnen", - "duplicateFailed": "Duplizierung fehlgeschlagen", - "membersCount": { - "zero": "Keine Mitglieder", - "one": "1 Mitglied", - "many": "{count} Mitglieder", - "other": "{count} Mitglieder" - }, - "useThisTemplate": "Verwenden Sie die Vorlage" - }, - "web": { - "continue": "Weiter", - "or": "oder", - "continueWithGoogle": "Weiter mit Google", - "continueWithGithub": "Weiter mit GitHub", - "continueWithDiscord": "Weiter mit Discord", - "continueWithApple": "Weiter mit Apple ", - "moreOptions": "Weitere Optionen", - "signInAgreement": "Wenn du oben auf \"Weiter\" klickst, bestätigst du, dass\ndu folgende Dokumente gelesen, verstanden und akzeptiert hast:\nAppFlowys", - "and": "und", - "termOfUse": "Bedingungen", - "privacyPolicy": "Datenschutzrichtlinie", - "signInError": "Anmeldefehler", - "login": "Registrieren oder anmelden", - "fileBlock": { - "uploadedAt": "Hochgeladen am {Zeit}", - "linkedAt": "Link hinzugefügt am {Zeit}", - "empty": "Hochladen oder Einbetten einer Datei" - }, - "importNotion": "Importieren aus Notion", - "import": "Import", - "importSuccess": "Erfolgreich hochgeladen", - "importFailed": "Import fehlgeschlagen, bitte überprüfen Sie das Dateiformat" - }, - "globalComment": { - "comments": "Kommentare", - "addComment": "Einen Kommentar hinzufügen", - "reactedBy": "Reaktion von", - "addReaction": "Reaktion hinzufügen", - "reactedByMore": "und {count} andere", - "showSeconds": { - "one": "vor 1 Sekunde", - "other": "vor {count} Sekunden", - "zero": "Soeben", - "many": "vor {count} Sekunden" - }, - "showMinutes": { - "one": "vor 1 Minute", - "other": "vor {count} Minuten", - "many": "vor {count} Minuten" - }, - "showHours": { - "one": "vor 1 Stunde", - "other": "vor {count} Stunden", - "many": "vor {count} Stunden" - }, - "showDays": { - "one": "vor 1 Tag", - "other": "vor {count} Tagen", - "many": "vor {count} Tagen" - }, - "showMonths": { - "one": "vor 1 Monat", - "other": "vor {count} Monaten", - "many": "vor {count} Monaten" - }, - "showYears": { - "one": "vor 1 Jahr", - "other": "vor {count} Jahren", - "many": "vor {count} Jahren" - }, - "reply": "Antworten", - "deleteComment": "Kommentar löschen", - "youAreNotOwner": "Du bist nicht der Verfasser dieses Kommentars", - "confirmDeleteDescription": "Möchtest du diesen Kommentar wirklich löschen?", - "hasBeenDeleted": "Gelöscht", - "replyingTo": "Antwort auf", - "noAccessDeleteComment": "Du bist nicht berechtigt, diesen Kommentar zu löschen", - "collapse": "Zusammenklappen", - "readMore": "Mehr lesen", - "failedToAddComment": "Kommentar konnte nicht hinzugefügt werden", - "commentAddedSuccessfully": "Kommentar erfolgreich hinzugefügt.", - "commentAddedSuccessTip": "Du hast gerade einen Kommentar hinzugefügt oder darauf geantwortet. Möchtest du nach oben springen, um die neuesten Kommentare zu sehen?" - }, - "template": { - "asTemplate": "Als Vorlage speichern", - "name": "Vorlagenname", - "description": "Vorlagenbeschreibung", - "requiredField": "{field} ist erforderlich", - "addCategory": "\"{category}\" hinzufügen", - "addNewCategory": "Neue Kategorie hinzufügen", - "deleteCategory": "Lösche Kategorie", - "editCategory": "Bearbeite Kategorie", - "category": { - "name": "Kategoriename", - "icon": "Kategorie-Icon", - "bgColor": "Kategorie-Hintergrundfarbe", - "priority": "Kategoriepriorität", - "desc": "Kategoriebeschreibung", - "type": "Kategorie-Typ", - "icons": "Kategorie-Icons", - "colors": "Kategoriefarbe", - "deleteCategory": "Lösche Kategorie", - "deleteCategoryDescription": "Möchten Sie diese Kategorie wirklich löschen?", - "typeToSearch": "Tippen, um nach Kategorien zu suchen..." - } - }, - "fileDropzone": { - "dropFile": "Klicken oder ziehen Sie die Datei zum Hochladen in diesen Bereich", - "uploading": "Hochladen...", - "uploadFailed": "Hochladen fehlgeschlagen", - "uploadSuccess": "Hochladen erfolgreich", - "uploadSuccessDescription": "Die Datei wurde erfolgreich hochgeladen", - "uploadFailedDescription": "Das Hochladen der Datei ist fehlgeschlagen", - "uploadingDescription": "Die Datei wird hochgeladen" - }, - "gallery": { - "preview": "Im Vollbildmodus öffnen", - "copy": "Kopiere", - "prev": "Vorherige", - "next": "Nächste", - "resetZoom": "Setze Zoom zurück", - "zoomIn": "Vergrößern", - "zoomOut": "Verkleinern" - }, - "invitation": { - "joinWorkspace": "Arbeitsbereich beitreten", - "success": "Sie sind dem Arbeitsbereich erfolgreich beigetreten", - "successMessage": "Sie können nun auf alle darin enthaltenen Seiten und Arbeitsbereiche zugreifen.", - "openWorkspace": "Öffne AppFlowy", - "errorModal": { - "description": "Ihr aktuelles Konto {email} hat möglicherweise keinen Zugriff auf diesen Arbeitsbereich. Bitte melden Sie sich mit dem richtigen Konto an oder wenden Sie sich an den Eigentümer des Arbeitsbereichs, um Hilfe zu erhalten." - } - }, - "approveAccess": { - "title": "Anfrage zum Beitritt zum Arbeitsbereich genehmigen", - "requestSummary": "<user/> fragt den Beitritt zu <workspace/> und den Zugriff auf <page/> an." - }, - "time": { - "justNow": "Soeben", - "seconds": { - "one": "1 Sekunde", - "other": "{count} Sekunden" - }, - "minutes": { - "one": "1 Minute", - "other": "{count} Minuten" - }, - "hours": { - "one": "1 Stunde", - "other": "{count} Stunden" - }, - "days": { - "one": "1 Tag", - "other": "{count} Tage" - }, - "weeks": { - "one": "1 Woche", - "other": "{count} Wochen" - }, - "months": { - "one": "1 Monat", - "other": "{count} Monate" - }, - "years": { - "one": "1 Jahr", - "other": "{count} Jahre" - }, - "ago": "vor", - "yesterday": "Gestern", - "today": "Heute" - }, - "members": { - "zero": "Keine Mitglieder", - "one": "1 Mitglied", - "many": "{count} Mitglieder", - "other": "{count} Mitglieder" - }, - "tabMenu": { - "close": "Schließen", - "closeOthers": "Schließe andere Tabs", - "favoriteDisabledHint": "Diese Ansicht kann nicht als Favorit markiert werden" - }, - "openFileMessage": { - "success": "Datei erfolgreich geöffnet", - "fileNotFound": "Datei nicht gefunden", - "permissionDenied": "Keine Berechtigung zum Öffnen dieser Datei", - "unknownError": "Öffnen der Datei fehlgeschlagen" + "fromTrashHint": "Aus dem Mülleimer" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json index a329a8998c..633a4adf65 100644 --- a/frontend/resources/translations/el-GR.json +++ b/frontend/resources/translations/el-GR.json @@ -179,7 +179,7 @@ "underline": "Υπογράμμιση", "strike": "Διακριτή διαγραφή", "numList": "Αριθμημένη λίστα", - "bulletList": "Bulleted list", + "bulletList": "Bulleted List", "checkList": "Check List", "inlineCode": "Inline Code", "quote": "Quote Block", @@ -305,7 +305,12 @@ "cloudServerType": "Cloud server", "cloudServerTypeTip": "Please note that it might log out your current account after switching the cloud server", "cloudLocal": "Local", - "cloudAppFlowy": "AppFlowy Cloud", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "The supabase url can't be empty", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty", + "cloudAppFlowy": "AppFlowy Cloud Beta", "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "The cloud url can't be empty", "clickToCopy": "Click to copy", @@ -472,7 +477,7 @@ "email": "Email", "tooltipSelectIcon": "Select icon", "selectAnIcon": "Select an icon", - "pleaseInputYourOpenAIKey": "παρακαλώ εισάγετε το AI κλειδί σας", + "pleaseInputYourOpenAIKey": "παρακαλώ εισάγετε το OpenAI κλειδί σας", "pleaseInputYourStabilityAIKey": "παρακαλώ εισάγετε το Stability AI κλειδί σας", "clickToLogout": "Κάντε κλικ για αποσύνδεση του τρέχοντος χρήστη" }, @@ -724,7 +729,8 @@ "url": { "launch": "Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης", "copy": "Copy link to clipboard", - "textFieldHint": "Enter a URL" + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", @@ -783,23 +789,23 @@ "referencedGrid": "Referenced Grid", "referencedCalendar": "Referenced Calendar", "referencedDocument": "Referenced Document", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Ask AI to write anything...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", "autoGeneratorLearnMore": "Μάθετε περισσότερα", "autoGeneratorGenerate": "Generate", - "autoGeneratorHintText": "Ρωτήστε Το AI ...", - "autoGeneratorCantGetOpenAIKey": "Αδυναμία λήψης κλειδιού AI", + "autoGeneratorHintText": "Ρωτήστε Το OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Αδυναμία λήψης κλειδιού OpenAI", "autoGeneratorRewrite": "Rewrite", "smartEdit": "AI Assistants", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Διόρθωση ορθογραφίας", "warning": "⚠️ Οι απαντήσεις AI μπορεί να είναι ανακριβείς ή παραπλανητικές.", "smartEditSummarize": "Summarize", "smartEditImproveWriting": "Improve writing", "smartEditMakeLonger": "Make longer", - "smartEditCouldNotFetchResult": "Could not fetch result from AI", - "smartEditCouldNotFetchKey": "Could not fetch AI key", - "smartEditDisabled": "Connect AI in Settings", + "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", + "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", + "smartEditDisabled": "Connect OpenAI in Settings", "discardResponse": "Do you want to discard the AI responses?", "createInlineMathEquation": "Create equation", "fonts": "Γραμματοσειρές", @@ -807,7 +813,7 @@ "quoteList": "Quote list", "numberedList": "Αριθμημένη λίστα", "bulletedList": "Bulleted list", - "todoList": "Todo list", + "todoList": "Todo List", "callout": "Callout", "cover": { "changeCover": "Change Cover", @@ -913,8 +919,8 @@ "placeholder": "Enter image URL" }, "ai": { - "label": "Generate image from AI", - "placeholder": "Please input the prompt for AI to generate image" + "label": "Generate image from OpenAI", + "placeholder": "Please input the prompt for OpenAI to generate image" }, "stability_ai": { "label": "Generate image from Stability AI", @@ -936,7 +942,7 @@ "label": "Unsplash" }, "searchForAnImage": "Search for an image", - "pleaseInputYourOpenAIKey": "please input your AI key in Settings page", + "pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page", "saveImageToGallery": "Save image", "failedToAddImageToGallery": "Failed to add image to gallery", @@ -1215,7 +1221,7 @@ }, "editor": { "bold": "Bold", - "bulletedList": "Bulleted list", + "bulletedList": "Bulleted List", "bulletedListShortForm": "Bulleted", "checkbox": "Checkbox", "embedCode": "Embed Code", @@ -1228,7 +1234,7 @@ "date": "Date", "italic": "Italic", "link": "Link", - "numberedList": "Numbered list", + "numberedList": "Numbered List", "numberedListShortForm": "Numbered", "quote": "Quote", "strikethrough": "Strikethrough", @@ -1402,4 +1408,4 @@ } } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 746833fd1f..50067fbf6c 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -34,11 +34,8 @@ "signIn": { "loginTitle": "Login to @:appName", "loginButtonText": "Login", - "loginStartWithAnonymous": "Continue with an anonymous session", + "loginStartWithAnonymous": "Start with an anonymous session", "continueAnonymousUser": "Continue with an anonymous session", - "continueWithLocalModel": "Continue with local model", - "switchToAppFlowyCloud": "AppFlowy Cloud", - "anonymousMode": "Anonymous mode", "buttonText": "Sign In", "signingInText": "Signing in...", "forgotPassword": "Forgot Password?", @@ -49,54 +46,31 @@ "repeatPasswordEmptyError": "Repeat password can't be empty", "unmatchedPasswordError": "Repeat password is not the same as password", "syncPromptMessage": "Syncing the data might take a while. Please don't close this page", - "or": "or", - "signInWithGoogle": "Continue with Google", - "signInWithGithub": "Continue with GitHub", - "signInWithDiscord": "Continue with Discord", - "signInWithApple": "Continue with Apple", - "continueAnotherWay": "Continue another way", + "or": "OR", + "signInWithGoogle": "Log in with Google", + "signInWithGithub": "Log in with Github", + "signInWithDiscord": "Log in with Discord", "signUpWithGoogle": "Sign up with Google", "signUpWithGithub": "Sign up with Github", "signUpWithDiscord": "Sign up with Discord", - "signInWith": "Continue with:", - "signInWithEmail": "Continue with Email", - "signInWithMagicLink": "Continue", + "signInWith": "Sign in with:", + "signInWithEmail": "Sign in with Email", + "signInWithMagicLink": "Log in with Magic Link", "signUpWithMagicLink": "Sign up with Magic Link", "pleaseInputYourEmail": "Please enter your email address", "settings": "Settings", - "magicLinkSent": "Magic Link sent!", + "magicLinkSent": "We emailed a magic link. Click the link to log in.", "invalidEmail": "Please enter a valid email address", "alreadyHaveAnAccount": "Already have an account?", "logIn": "Log in", "generalError": "Something went wrong. Please try again later", - "limitRateError": "For security reasons, you can only request a magic link every 60 seconds", - "magicLinkSentDescription": "A Magic Link was sent to your email. Click the link to complete your login. The link will expire after 5 minutes.", - "tokenHasExpiredOrInvalid": "The code has expired or is invalid. Please try again.", - "signingIn": "Signing in...", - "checkYourEmail": "Check your email", - "temporaryVerificationLinkSent": "A temporary verification link has been sent.\nPlease check your inbox at", - "temporaryVerificationCodeSent": "A temporary verification code has been sent.\nPlease check your inbox at", - "continueToSignIn": "Continue to sign in", - "backToLogin": "Back to login", - "enterCode": "Enter code", - "enterCodeManually": "Enter code manually", - "continueWithEmail": "Continue with email", - "enterPassword": "Enter password", - "loginAs": "Login as", - "invalidVerificationCode": "Please enter a valid verification code", - "tooFrequentVerificationCodeRequest": "You have made too many requests. Please try again later.", - "invalidLoginCredentials": "Your password is incorrect, please try again" + "limitRateError": "For security reasons, you can only request a magic link every 60 seconds" }, "workspace": { "chooseWorkspace": "Choose your workspace", - "defaultName": "My Workspace", "create": "Create workspace", - "new": "New workspace", - "importFromNotion": "Import from Notion", - "learnMore": "Learn more", "reset": "Reset workspace", "renameWorkspace": "Rename workspace", - "workspaceNameCannotBeEmpty": "Workspace name cannot be empty", "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found", @@ -108,7 +82,7 @@ "reachOut": "Reach out on Discord" }, "menuTitle": "Workspaces", - "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone, and any pages you have published will be unpublished.", + "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", "createSuccess": "Workspace created successfully", "createFailed": "Failed to create workspace", "createLimitExceeded": "You've reached the maximum workspace limit allowed for your account. If you need additional workspaces to continue your work, please request on Github", @@ -132,25 +106,7 @@ "html": "HTML", "clipboard": "Copy to clipboard", "csv": "CSV", - "copyLink": "Copy link", - "publishToTheWeb": "Publish to Web", - "publishToTheWebHint": "Create a website with AppFlowy", - "publish": "Publish", - "unPublish": "Unpublish", - "visitSite": "Visit site", - "exportAsTab": "Export as", - "publishTab": "Publish", - "shareTab": "Share", - "publishOnAppFlowy": "Publish on AppFlowy", - "shareTabTitle": "Invite to collaborate", - "shareTabDescription": "For easy collaboration with anyone", - "copyLinkSuccess": "Copied link to clipboard", - "copyShareLink": "Copy share link", - "copyLinkFailed": "Failed to copy link to clipboard", - "copyLinkToBlockSuccess": "Copied block link to clipboard", - "copyLinkToBlockFailed": "Failed to copy block link to clipboard", - "manageAllSites": "Manage all sites", - "updatePathName": "Update path name" + "copyLink": "Copy Link" }, "moreAction": { "small": "small", @@ -163,46 +119,27 @@ "charCount": "Character count: {}", "createdAt": "Created: {}", "deleteView": "Delete", - "duplicateView": "Duplicate", - "wordCountLabel": "Word count: ", - "charCountLabel": "Character count: ", - "createdAtLabel": "Created: ", - "syncedAtLabel": "Synced: ", - "saveAsNewPage": "Add messages to page", - "saveAsNewPageDisabled": "No messages available" + "duplicateView": "Duplicate" }, "importPanel": { "textAndMarkdown": "Text & Markdown", "documentFromV010": "Document from v0.1.0", "databaseFromV010": "Database from v0.1.0", - "notionZip": "Notion Exported Zip File", "csv": "CSV", "database": "Database" }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "Drag & drop a file, click to ", - "placeholderUpload": "Upload", - "placeholderRight": ", or paste an image link.", - "dropToUpload": "Drop a file to upload", - "change": "Change" - } - }, "disclosureAction": { "rename": "Rename", "delete": "Delete", "duplicate": "Duplicate", - "unfavorite": "Remove from Favorites", - "favorite": "Add to Favorites", + "unfavorite": "Remove from favorites", + "favorite": "Add to favorites", "openNewTab": "Open in a new tab", "moveTo": "Move to", "addToFavorites": "Add to Favorites", - "copyLink": "Copy link", + "copyLink": "Copy Link", "changeIcon": "Change icon", - "collapseAllPages": "Collapse all subpages", - "movePageTo": "Move page to", - "move": "Move", - "lockPage": "Lock page" + "collapseAllPages": "Collapse all subpages" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -212,81 +149,22 @@ "newBoardText": "New board", "chat": { "newChat": "AI Chat", - "inputMessageHint": "Ask @:appName AI", - "inputLocalAIMessageHint": "Ask @:appName Local AI", - "unsupportedCloudPrompt": "This feature is only available when using @:appName Cloud", - "relatedQuestion": "Suggested", - "serverUnavailable": "Connection lost. Please check your internet and", - "aiServerUnavailable": "The AI service is temporarily unavailable. Please try again later.", - "retry": "Retry", + "inputMessageHint": "Message AppFlowy AI", + "unsupportedCloudPrompt": "This feature is only available when using AppFlowy Cloud", + "relatedQuestion": "Related", + "serverUnavailable": "Service Temporarily Unavailable. Please try again later.", + "aiServerUnavailable": "🌈 Uh-oh! 🌈. A unicorn ate our response. Please retry!", "clickToRetry": "Click to retry", "regenerateAnswer": "Regenerate", "question1": "How to use Kanban to manage tasks", "question2": "Explain the GTD method", "question3": "Why use Rust", "question4": "Recipe with what's in my kitchen", - "question5": "Create an illustration for my page", - "question6": "Draw up a to-do list for my upcoming week", - "aiMistakePrompt": "AI can make mistakes. Check important info.", - "chatWithFilePrompt": "Do you want to chat with the file?", - "indexFileSuccess": "Indexing file successfully", - "inputActionNoPages": "No page results", - "referenceSource": { - "zero": "0 sources found", - "one": "{count} source found", - "other": "{count} sources found" - }, - "clickToMention": "Mention a page", - "uploadFile": "Attach PDFs, text or markdown files", - "questionDetail": "Hi {}! How can I help you today?", - "indexingFile": "Indexing {}", - "generatingResponse": "Generating response", - "selectSources": "Select Sources", - "currentPage": "Current page", - "sourcesLimitReached": "You can only select up to 3 top-level documents and its children", - "sourceUnsupported": "We don't support chatting with databases at this time", - "regenerate": "Try again", - "addToPageButton": "Add message to page", - "addToPageTitle": "Add message to...", - "addToNewPage": "Create new page", - "addToNewPageName": "Messages extracted from \"{}\"", - "addToNewPageSuccessToast": "Message added to", - "openPagePreviewFailedToast": "Failed to open page", - "changeFormat": { - "actionButton": "Change format", - "confirmButton": "Regenerate with this format", - "textOnly": "Text", - "imageOnly": "Image only", - "textAndImage": "Text and Image", - "text": "Paragraph", - "bullet": "Bullet list", - "number": "Numbered list", - "table": "Table", - "blankDescription": "Format response", - "defaultDescription": "Auto response format", - "textWithImageDescription": "@:chat.changeFormat.text with image", - "numberWithImageDescription": "@:chat.changeFormat.number with image", - "bulletWithImageDescription": "@:chat.changeFormat.bullet with image", - "tableWithImageDescription": "@:chat.changeFormat.table with image" - }, - "switchModel": { - "label": "Switch model", - "localModel": "Local Model", - "cloudModel": "Cloud Model", - "autoModel": "Auto" - }, - "selectBanner": { - "saveButton": "Add to …", - "selectMessages": "Select messages", - "nSelected": "{} selected", - "allSelected": "All selected" - }, - "stopTooltip": "Stop generating" + "aiMistakePrompt": "AI can make mistakes. Check important info." }, "trash": { "text": "Trash", "restoreAll": "Restore All", - "restore": "Restore", "deleteAll": "Delete All", "pageHeader": { "fileName": "File name", @@ -294,21 +172,17 @@ "created": "Created" }, "confirmDeleteAll": { - "title": "All pages in trash", - "caption": "Are you sure you want to delete everything in Trash? This action cannot be undone." - }, - "confirmRestoreAll": { - "title": "Restore all pages in trash", + "title": "Are you sure to delete all pages in Trash?", "caption": "This action cannot be undone." }, - "restorePage": { - "title": "Restore: {}", - "caption": "Are you sure you want to restore this page?" + "confirmRestoreAll": { + "title": "Are you sure to restore all pages in Trash?", + "caption": "This action cannot be undone." }, "mobile": { "actions": "Trash Actions", - "empty": "No pages or spaces in Trash", - "emptyDescription": "Move things you don't need to the Trash.", + "empty": "Trash Bin is Empty", + "emptyDescription": "You don't have any deleted file", "isDeleted": "is deleted", "isRestored": "is restored" }, @@ -317,15 +191,13 @@ "deletePagePrompt": { "text": "This page is in Trash", "restore": "Restore page", - "deletePermanent": "Delete permanently", - "deletePermanentDescription": "Are you sure you want to delete this page permanently? This is irreversible." + "deletePermanent": "Delete permanently" }, "dialogCreatePageNameHint": "Page name", "questionBubble": { "shortcuts": "Shortcuts", "whatsNew": "What's new?", - "helpAndDocumentation": "Help & documentation", - "getSupport": "Get support", + "help": "Help & Support", "markdown": "Markdown", "debug": { "name": "Debug Info", @@ -338,8 +210,7 @@ "moreButtonToolTip": "Remove, rename, and more...", "addPageTooltip": "Quickly add a page inside", "defaultNewPageName": "Untitled", - "renameDialog": "Rename", - "pageNameSuffix": "Copy" + "renameDialog": "Rename" }, "noPagesInside": "No pages inside", "toolbar": { @@ -349,15 +220,16 @@ "italic": "Italic", "underline": "Underline", "strike": "Strikethrough", - "numList": "Numbered list", - "bulletList": "Bulleted list", + "numList": "Numbered List", + "bulletList": "Bulleted List", "checkList": "Check List", "inlineCode": "Inline Code", "quote": "Quote Block", "header": "Header", "highlight": "Highlight", "color": "Color", - "addLink": "Add Link" + "addLink": "Add Link", + "link": "Link" }, "tooltip": { "lightMode": "Switch to Light mode", @@ -365,7 +237,7 @@ "openAsPage": "Open as a Page", "addNewRow": "Add a new row", "openMenu": "Click to open menu", - "dragRow": "Drag to reorder the row", + "dragRow": "Long press to reorder the row", "viewDataBase": "View database", "referencePage": "This {name} is referenced", "addBlockBelow": "Add a block below", @@ -374,7 +246,6 @@ "sideBar": { "closeSidebar": "Close sidebar", "openSidebar": "Open sidebar", - "expandSidebar": "Expand as full page", "personal": "Personal", "private": "Private", "workspace": "Workspace", @@ -389,40 +260,20 @@ "recent": "Recent", "today": "Today", "thisWeek": "This week", - "others": "Earlier favorites", - "earlier": "Earlier", + "others": "Other favorites", "justNow": "just now", "minutesAgo": "{count} minutes ago", "lastViewed": "Last viewed", - "favoriteAt": "Favorited", - "emptyRecent": "No Recent Pages", - "emptyRecentDescription": "As you view pages, they will appear here for easy retrieval.", - "emptyFavorite": "No Favorite Pages", - "emptyFavoriteDescription": "Mark pages as favorites—they'll be listed here for quick access!", + "favoriteAt": "Favorited at", + "emptyRecent": "No Recent Documents", + "emptyRecentDescription": "As you view documents, they will appear here for easy retrieval", + "emptyFavorite": "No Favorite Documents", + "emptyFavoriteDescription": "Start exploring and mark documents as favorites. They’ll be listed here for quick access!", "removePageFromRecent": "Remove this page from the Recent?", "removeSuccess": "Removed successfully", "favoriteSpace": "Favorites", "RecentSpace": "Recent", - "Spaces": "Spaces", - "upgradeToPro": "Upgrade to Pro", - "upgradeToAIMax": "Unlock unlimited AI", - "storageLimitDialogTitle": "You have run out of free storage. Upgrade to unlock unlimited storage", - "storageLimitDialogTitleIOS": "You have run out of free storage.", - "aiResponseLimitTitle": "You have run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", - "aiResponseLimitDialogTitle": "AI Responses limit reached", - "aiResponseLimit": "You have run out of free AI responses.\n\nGo to Settings -> Plan -> Click AI Max or Pro Plan to get more AI responses", - "askOwnerToUpgradeToPro": "Your workspace is running out of free storage. Please ask your workspace owner to upgrade to the Pro Plan", - "askOwnerToUpgradeToProIOS": "Your workspace is running out of free storage.", - "askOwnerToUpgradeToAIMax": "Your workspace has ran out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons", - "askOwnerToUpgradeToAIMaxIOS": "Your workspace is running out of free AI responses.", - "purchaseAIMax": "Your workspace has ran out of AI Image responses. Please ask your workspace owner to purchase AI Max", - "aiImageResponseLimit": "You have run out of AI image responses.\n\nGo to Settings -> Plan -> Click AI Max to get more AI image responses", - "purchaseStorageSpace": "Purchase Storage Space", - "singleFileProPlanLimitationDescription": "You have exceeded the maximum file upload size allowed in the free plan. Please upgrade to the Pro Plan to upload larger files", - "purchaseAIResponse": "Purchase ", - "askOwnerToUpgradeToLocalAI": "Ask workspace owner to enable AI On-device", - "upgradeToAILocal": "Run local models on your device for ultimate privacy", - "upgradeToAILocalDesc": "Chat with PDFs, improve your writing, and auto-fill tables using local AI" + "Spaces": "Spaces" }, "notifications": { "export": { @@ -456,22 +307,17 @@ "upload": "Upload", "edit": "Edit", "delete": "Delete", - "copy": "Copy", "duplicate": "Duplicate", "putback": "Put Back", "update": "Update", "share": "Share", - "removeFromFavorites": "Remove from Favorites", - "removeFromRecent": "Remove from Recent", - "addToFavorites": "Add to Favorites", - "favoriteSuccessfully": "Favorited success", - "unfavoriteSuccessfully": "Unfavorited success", - "duplicateSuccessfully": "Duplicated successfully", + "removeFromFavorites": "Remove from favorites", + "removeFromRecent": "Remove from recent", + "addToFavorites": "Add to favorites", "rename": "Rename", "helpCenter": "Help Center", "add": "Add", "yes": "Yes", - "no": "No", "clear": "Clear", "remove": "Remove", "dontRemove": "Don't remove", @@ -481,23 +327,12 @@ "logout": "Log out", "deleteAccount": "Delete account", "back": "Back", - "signInGoogle": "Continue with Google", - "signInGithub": "Continue with GitHub", - "signInDiscord": "Continue with Discord", + "signInGoogle": "Sign in with Google", + "signInGithub": "Sign in with Github", + "signInDiscord": "Sign in with Discord", "more": "More", "create": "Create", - "close": "Close", - "next": "Next", - "previous": "Previous", - "submit": "Submit", - "download": "Download", - "backToHome": "Back to Home", - "viewing": "Viewing", - "editing": "Editing", - "gotIt": "Got it", - "retry": "Retry", - "uploadFailed": "Upload failed.", - "copyLinkOriginal": "Copy link to original" + "close": "Close" }, "label": { "welcome": "Welcome!", @@ -521,77 +356,8 @@ }, "settings": { "title": "Settings", - "popupMenuItem": { - "settings": "Settings", - "members": "Members", - "trash": "Trash", - "helpAndDocumentation": "Help & documentation", - "getSupport": "Get Support" - }, - "sites": { - "title": "Sites", - "namespaceTitle": "Namespace", - "namespaceDescription": "Manage your namespace and homepage", - "namespaceHeader": "Namespace", - "homepageHeader": "Homepage", - "updateNamespace": "Update namespace", - "removeHomepage": "Remove homepage", - "selectHomePage": "Select a page", - "clearHomePage": "Clear the home page for this namespace", - "customUrl": "Custom URL", - "namespace": { - "description": "This change will apply to all the published pages live on this namespace", - "tooltip": "We reserve the rights to remove any inappropriate namespaces", - "updateExistingNamespace": "Update existing namespace", - "upgradeToPro": "Upgrade to Pro Plan to set a homepage", - "redirectToPayment": "Redirecting to payment page...", - "onlyWorkspaceOwnerCanSetHomePage": "Only the workspace owner can set a homepage", - "pleaseAskOwnerToSetHomePage": "Please ask the workspace owner to upgrade to Pro Plan" - }, - "publishedPage": { - "title": "All published pages", - "description": "Manage your published pages", - "page": "Page", - "pathName": "Path name", - "date": "Published date", - "emptyHinText": "You have no published pages in this workspace", - "noPublishedPages": "No published pages", - "settings": "Publish settings", - "clickToOpenPageInApp": "Open page in app", - "clickToOpenPageInBrowser": "Open page in browser" - }, - "error": { - "failedToGeneratePaymentLink": "Failed to generate payment link for Pro Plan", - "failedToUpdateNamespace": "Failed to update namespace", - "proPlanLimitation": "You need to upgrade to Pro Plan to update the namespace", - "namespaceAlreadyInUse": "The namespace is already taken, please try another one", - "invalidNamespace": "Invalid namespace, please try another one", - "namespaceLengthAtLeast2Characters": "The namespace must be at least 2 characters long", - "onlyWorkspaceOwnerCanUpdateNamespace": "Only workspace owner can update the namespace", - "onlyWorkspaceOwnerCanRemoveHomepage": "Only workspace owner can remove the homepage", - "setHomepageFailed": "Failed to set homepage", - "namespaceTooLong": "The namespace is too long, please try another one", - "namespaceTooShort": "The namespace is too short, please try another one", - "namespaceIsReserved": "The namespace is reserved, please try another one", - "updatePathNameFailed": "Failed to update path name", - "removeHomePageFailed": "Failed to remove homepage", - "publishNameContainsInvalidCharacters": "The path name contains invalid character(s), please try another one", - "publishNameTooShort": "The path name is too short, please try another one", - "publishNameTooLong": "The path name is too long, please try another one", - "publishNameAlreadyInUse": "The path name is already in use, please try another one", - "namespaceContainsInvalidCharacters": "The namespace contains invalid character(s), please try another one", - "publishPermissionDenied": "Only the workspace owner or page publisher can manage the publish settings", - "publishNameCannotBeEmpty": "The path name cannot be empty, please try another one" - }, - "success": { - "namespaceUpdated": "Updated namespace successfully", - "setHomepageSuccess": "Set homepage successfully", - "updatePathNameSuccess": "Updated path name successfully", - "removeHomePageSuccess": "Remove homepage successfully" - } - }, "accountPage": { - "menuLabel": "Account & App", + "menuLabel": "My account", "title": "My account", "general": { "title": "Account name & profile image", @@ -603,13 +369,20 @@ "change": "Change email" } }, + "keys": { + "title": "AI API Keys", + "openAILabel": "OpenAI API key", + "openAITooltip": "You can find your Secret API key on the API key page", + "openAIHint": "Input your OpenAI API Key", + "stabilityAILabel": "Stability API key", + "stabilityAITooltip": "Your Stability API key, used to authenticate your requests", + "stabilityAIHint": "Input your Stability API Key" + }, "login": { "title": "Account login", "loginLabel": "Log in", "logoutLabel": "Log out" - }, - "isUpToDate": "@:appName is up to date!", - "officialVersion": "Version {version} (Official build)" + } }, "workspacePage": { "menuLabel": "Workspace", @@ -631,22 +404,10 @@ "dark": "Dark" } }, - "resetCursorColor": { - "title": "Reset document cursor color", - "description": "Are you sure you want to reset the cursor color?" - }, - "resetSelectionColor": { - "title": "Reset document selection color", - "description": "Are you sure you want to reset the selection color?" - }, - "resetWidth": { - "resetSuccess": "Reset document width successfully" - }, "theme": { "title": "Theme", "description": "Select a preset theme, or upload your own custom theme.", - "uploadCustomThemeTooltip": "Upload a custom theme", - "failedToLoadThemes": "Failed to load themes, please check your permission settings in System Settings > Privacy and Security > Files and Folders > @:appName" + "uploadCustomThemeTooltip": "Upload a custom theme" }, "workspaceFont": { "title": "Workspace font", @@ -682,13 +443,11 @@ }, "deleteWorkspacePrompt": { "title": "Delete workspace", - "content": "Are you sure you want to delete this workspace? This action cannot be undone, and any pages you have published will be unpublished." + "content": "Are you sure you want to delete this workspace? This action cannot be undone." }, "leaveWorkspacePrompt": { "title": "Leave workspace", - "content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it.", - "success": "You have left the workspace successfully.", - "fail": "Failed to leave the workspace." + "content": "Are you sure you want to leave this workspace? You will lose access to all pages and data within it." }, "manageWorkspace": { "title": "Manage workspace", @@ -719,8 +478,8 @@ "importData": { "title": "Import data", "tooltip": "Import data from @:appName backups/data folders", - "description": "Copy data from an external @:appName data folder", - "action": "Browse file" + "description": "Copy data from an external @:appName data folder and import it into the current @:appName data folder", + "action": "Browse folder" }, "encryption": { "title": "Encryption", @@ -735,17 +494,12 @@ }, "cache": { "title": "Clear cache", - "description": "Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.", + "description": "If you encounter issues with images not loading or fonts not displaying correctly, try clearing your cache. This action will not remove your user data.", "dialog": { - "title": "Clear cache", - "description": "Help resolve issues like image not loading, missing pages in a space, and fonts not loading. This will not affect your data.", + "title": "Are you sure?", + "description": "Clearing the cache will cause images and fonts to be re-downloaded on load. This action will not remove or modify your data.", "successHint": "Cache cleared!" } - }, - "data": { - "fixYourData": "Fix your data", - "fixButton": "Fix", - "fixYourDataDescription": "If you're experiencing issues with your data, you can try to fix it here." } }, "shortcutsPage": { @@ -786,7 +540,6 @@ "alignLeft": "Align text left", "alignCenter": "Align text center", "alignRight": "Align text right", - "insertInlineMathEquation": "Insert inline math eqaution", "undo": "Undo", "redo": "Redo", "convertToParagraph": "Convert block to paragraph", @@ -830,7 +583,7 @@ "indent": "Indent", "outdent": "Outdent", "exit": "Exit editing", - "pageUp": "Scroll one page up", + "pageUp": "Scroll on page up", "pageDown": "Scroll one page down", "selectAll": "Select all", "pasteWithoutFormatting": "Paste content without formatting", @@ -858,54 +611,6 @@ "couldNotLoadErrorMsg": "Could not load shortcuts, Try again", "couldNotSaveErrorMsg": "Could not save shortcuts, Try again" }, - "aiPage": { - "title": "AI Settings", - "menuLabel": "AI Settings", - "keys": { - "enableAISearchTitle": "AI Search", - "aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet, and models available in Ollama", - "loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up", - "llmModel": "Language Model", - "llmModelType": "Language Model Type", - "downloadLLMPrompt": "Download {}", - "downloadAppFlowyOfflineAI": "Downloading AI offline package will enable AI to run on your device. Do you want to continue?", - "downloadLLMPromptDetail": "Downloading {} local model will take up to {} of storage. Do you want to continue?", - "downloadBigFilePrompt": "It may take around 10 minutes to complete the download", - "downloadAIModelButton": "Download", - "downloadingModel": "Downloading", - "localAILoaded": "Local AI Model successfully added and ready to use", - "localAIStart": "Local AI is starting. If it's slow, try toggling it off and on", - "localAILoading": "Local AI Chat Model is loading...", - "localAIStopped": "Local AI stopped", - "localAIRunning": "Local AI is running", - "localAINotReadyRetryLater": "Local AI is initializing, please retry later", - "localAIDisabled": "You are using local AI, but it is disabled. Please go to settings to enable it or try different model", - "localAIInitializing": "Local AI is loading. This may take a few seconds depending on your device", - "localAINotReadyTextFieldPrompt": "You can not edit while Local AI is loading", - "failToLoadLocalAI": "Failed to start local AI.", - "restartLocalAI": "Restart", - "disableLocalAITitle": "Disable local AI", - "disableLocalAIDescription": "Do you want to disable local AI?", - "localAIToggleTitle": "AppFlowy Local AI (LAI)", - "localAIToggleSubTitle": "Run the most advanced local AI models within AppFlowy for ultimate privacy and security", - "offlineAIInstruction1": "Follow the", - "offlineAIInstruction2": "instruction", - "offlineAIInstruction3": "to enable offline AI.", - "offlineAIDownload1": "If you have not downloaded the AppFlowy AI, please", - "offlineAIDownload2": "download", - "offlineAIDownload3": "it first", - "activeOfflineAI": "Active", - "downloadOfflineAI": "Download", - "openModelDirectory": "Open folder", - "laiNotReady": "The Local AI app was not installed correctly.", - "ollamaNotReady": "The Ollama server is not ready.", - "pleaseFollowThese": "Please follow these", - "instructions": "instructions", - "installOllamaLai": "to set up Ollama and AppFlowy Local AI.", - "modelsMissing": "Cannot find the required models: ", - "downloadModel": "to download them." - } - }, "planPage": { "menuLabel": "Plan", "title": "Pricing plan", @@ -913,21 +618,16 @@ "title": "Plan usage summary", "storageLabel": "Storage", "storageUsage": "{} of {} GB", - "unlimitedStorageLabel": "Unlimited storage", - "collaboratorsLabel": "Members", + "collaboratorsLabel": "Collaborators", "collaboratorsUsage": "{} of {}", "aiResponseLabel": "AI Responses", "aiResponseUsage": "{} of {}", - "unlimitedAILabel": "Unlimited responses", "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "AI On-device for Mac", - "memberProToggle": "More members & unlimited AI", - "aiMaxToggle": "Unlimited AI and access to advanced models", - "aiOnDeviceToggle": "Local AI for ultimate privacy", + "memberProToggle": "Unlimited members", + "guestCollabToggle": "10 guest collaborators", "aiCredit": { - "title": "Add @:appName AI Credit", - "price": "{}", + "title": "Add AppFlowy AI Credit", + "price": "5$", "priceDescription": "for 1,000 credits", "purchase": "Purchase AI", "info": "Add 1,000 Ai credits per workspace and seamlessly integrate customizable AI into your workflow for smarter, faster results with up to:", @@ -939,34 +639,32 @@ "freeTitle": "Free", "proTitle": "Pro", "teamTitle": "Team", - "freeInfo": "Perfect for individuals up to 2 members to organize everything", + "freeInfo": "Perfect for individuals or small teams up to 3 members.", "proInfo": "Perfect for small and medium teams up to 10 members.", "teamInfo": "Perfect for all productive and well-organized teams..", - "upgrade": "Change plan", + "upgrade": "Compare &\n Upgrade", + "freeProOne": "Collaborative workspace", + "freeProTwo": "Up to 3 members (incl. owner)", + "freeProThree": "Unlimited guests (view-only)", + "freeProFour": "Storage 5gb", + "freeProFive": "30 day revision history", + "freeConOne": "Guest collaborators (edit access)", + "freeConTwo": "Unlimited storage", + "freeConThree": "6 month revision history", + "professionalProOne": "Collaborative workspace", + "professionalProTwo": "Unlimited members", + "professionalProThree": "Unlimited guests (view-only)", + "professionalProFour": "Unlimited storage", + "professionalProFive": "6 month revision history", + "professionalConOne": "Unlimited guest collaborators (edit access)", + "professionalConTwo": "Unlimited AI responses", + "professionalConThree": "1 year revision history", "canceledInfo": "Your plan is cancelled, you will be downgraded to the Free plan on {}." }, - "addons": { - "title": "Add-ons", - "addLabel": "Add", - "activeLabel": "Added", - "aiMax": { - "title": "AI Max", - "description": "Unlimited AI responses powered by advanced AI models, and 50 AI images per month", - "price": "{}", - "priceInfo": "Per user per month billed annually" - }, - "aiOnDevice": { - "title": "AI On-device for Mac", - "description": "Run Mistral 7B, LLAMA 3, and more local models on your machine", - "price": "{}", - "priceInfo": "Per user per month billed annually", - "recommend": "Recommend M1 or newer" - } - }, "deal": { "bannerLabel": "New year deal!", "title": "Grow your team!", - "info": "Upgrade and save 10% off Pro and Team plans! Boost your workspace productivity with powerful new features including @:appName AI.", + "info": "Upgrade and save 10% off Pro and Team plans! Boost your workspace productivity with powerful new features including Appflowy Ai.", "viewPlans": "View plans" } } @@ -986,36 +684,7 @@ "title": "Payment details", "methodLabel": "Payment method", "methodButtonLabel": "Edit method" - }, - "addons": { - "title": "Add-ons", - "addLabel": "Add", - "removeLabel": "Remove", - "renewLabel": "Renew", - "aiMax": { - "label": "AI Max", - "description": "Unlock unlimited AI and advanced models", - "activeDescription": "Next invoice due on {}", - "canceledDescription": "AI Max will be available until {}" - }, - "aiOnDevice": { - "label": "AI On-device for Mac", - "description": "Unlock unlimited AI On-device on your device", - "activeDescription": "Next invoice due on {}", - "canceledDescription": "AI On-device for Mac will be available until {}" - }, - "removeDialog": { - "title": "Remove {}", - "description": "Are you sure you want to remove {plan}? You will lose access to the features and benefits of {plan} immediately." - } - }, - "currentPeriodBadge": "CURRENT", - "changePeriod": "Change period", - "planPeriod": "{} period", - "monthlyInterval": "Monthly", - "monthlyPriceInfo": "per seat billed monthly", - "annualInterval": "Annually", - "annualPriceInfo": "per seat billed annually" + } }, "comparePlanDialog": { "title": "Compare & select plan", @@ -1024,107 +693,65 @@ "actions": { "upgrade": "Upgrade", "downgrade": "Downgrade", + "downgradeDisabledTooltip": "You will automatically downgrade at the end of the billing cycle", "current": "Current" }, "freePlan": { "title": "Free", - "description": "For individuals up to 2 members to organize everything", - "price": "{}", - "priceInfo": "Free forever" + "description": "For organizing every corner of your work & life.", + "price": "$0", + "priceInfo": "free forever" }, "proPlan": { - "title": "Pro", - "description": "For small teams to manage projects and team knowledge", - "price": "{}", - "priceInfo": "Per user per month \nbilled annually\n\n{} billed monthly" + "title": "Professional", + "description": "A place for small groups to plan & get organized.", + "price": "$10 /month", + "priceInfo": "billed annually" }, "planLabels": { "itemOne": "Workspaces", "itemTwo": "Members", - "itemThree": "Storage", - "itemFour": "Real-time collaboration", - "itemFive": "Mobile app", - "itemSix": "AI Responses", - "itemSeven": "AI Images", - "itemFileUpload": "File uploads", - "customNamespace": "Custom namespace", - "tooltipSix": "Lifetime means the number of responses never reset", - "intelligentSearch": "Intelligent search", - "tooltipSeven": "Allows you to customize part of the URL for your workspace", - "customNamespaceTooltip": "Custom published site URL" + "itemThree": "Guests", + "tooltipThree": "Guests have read-only permission to the specifically shared content", + "itemFour": "Guest collaborators", + "tooltipFour": "Guest collaborators are billed as one seat", + "itemFive": "Storage", + "itemSix": "Real-time collaboration", + "itemSeven": "Mobile app", + "itemEight": "AI Responses", + "tooltipEight": "Lifetime means the number of responses never reset" }, "freeLabels": { - "itemOne": "Charged per workspace", - "itemTwo": "Up to 2", - "itemThree": "5 GB", - "itemFour": "yes", - "itemFive": "yes", - "itemSix": "10 lifetime", - "itemSeven": "2 lifetime", - "itemFileUpload": "Up to 7 MB", - "intelligentSearch": "Intelligent search" + "itemOne": "charged per workspace", + "itemTwo": "3", + "itemThree": "", + "itemFour": "0", + "itemFive": "5 GB", + "itemSix": "yes", + "itemSeven": "yes", + "itemEight": "1,000 lifetime" }, "proLabels": { - "itemOne": "Charged per workspace", - "itemTwo": "Up to 10", - "itemThree": "Unlimited", - "itemFour": "yes", - "itemFive": "yes", - "itemSix": "Unlimited", - "itemSeven": "10 images per month", - "itemFileUpload": "Unlimited", - "intelligentSearch": "Intelligent search" + "itemOne": "charged per workspace", + "itemTwo": "up to 10", + "itemThree": "", + "itemFour": "10 guests billed as one seat", + "itemFive": "unlimited", + "itemSix": "yes", + "itemSeven": "yes", + "itemEight": "10,000 monthly" }, "paymentSuccess": { "title": "You are now on the {} plan!", - "description": "Your payment has been successfully processed and your plan is upgraded to @:appName {}. You can view your plan details on the Plan page" + "description": "Your payment has been successfully processed and your plan is upgraded to AppFlowy {}. You can view your plan details on the Plan page" }, "downgradeDialog": { "title": "Are you sure you want to downgrade your plan?", - "description": "Downgrading your plan will revert you back to the Free plan. Members may lose access to this workspace and you may need to free up space to meet the storage limits of the Free plan.", + "description": "Downgrading your plan will revert you back to the Free plan. Members may lose access to workspaces and you may need to free up space to meet the storage limits of the Free plan.", "downgradeLabel": "Downgrade plan" } }, - "cancelSurveyDialog": { - "title": "Sorry to see you go", - "description": "We're sorry to see you go. We'd love to hear your feedback to help us improve @:appName. Please take a moment to answer a few questions.", - "commonOther": "Other", - "otherHint": "Write your answer here", - "questionOne": { - "question": "What prompted you to cancel your @:appName Pro subscription?", - "answerOne": "Cost too high", - "answerTwo": "Features did not meet expectations", - "answerThree": "Found a better alternative", - "answerFour": "Did not use it enough to justify the expense", - "answerFive": "Service issue or technical difficulties" - }, - "questionTwo": { - "question": "How likely are you to consider re-subscribing to @:appName Pro in the future?", - "answerOne": "Very likely", - "answerTwo": "Somewhat likely", - "answerThree": "Not sure", - "answerFour": "Unlikely", - "answerFive": "Very unlikely" - }, - "questionThree": { - "question": "Which Pro feature did you value the most during your subscription?", - "answerOne": "Multi-user collaboration", - "answerTwo": "Longer time version history", - "answerThree": "Unlimited AI responses", - "answerFour": "Access to local AI models" - }, - "questionFour": { - "question": "How would you describe your overall experience with @:appName?", - "answerOne": "Great", - "answerTwo": "Good", - "answerThree": "Average", - "answerFour": "Below average", - "answerFive": "Unsatisfied" - } - }, "common": { - "uploadingFile": "File is uploading. Please do not quit the app", - "uploadNotionSuccess": "Your Notion zip file has been uploaded successfully. Once the import is complete, you will receive a confirmation email", "reset": "Reset" }, "menu": { @@ -1135,31 +762,30 @@ "notifications": "Notifications", "open": "Open Settings", "logout": "Logout", - "logoutPrompt": "Are you sure you want to logout?", + "logoutPrompt": "Are you sure to logout?", "selfEncryptionLogoutPrompt": "Are you sure you want to log out? Please ensure you have copied the encryption secret", "syncSetting": "Sync Setting", "cloudSettings": "Cloud Settings", "enableSync": "Enable sync", - "enableSyncLog": "Enable sync logging", - "enableSyncLogWarning": "Thank you for helping diagnose sync issues. This will log your document edits to a local file. Please quit and reopen the app after enabling", "enableEncrypt": "Encrypt data", "cloudURL": "Base URL", - "webURL": "Web URL", "invalidCloudURLScheme": "Invalid Scheme", "cloudServerType": "Cloud server", "cloudServerTypeTip": "Please note that it might log out your current account after switching the cloud server", "cloudLocal": "Local", - "cloudAppFlowy": "@:appName Cloud", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "The supabase url can't be empty", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty", + "cloudAppFlowy": "@:appName Cloud Beta", "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "The cloud url can't be empty", - "clickToCopy": "Copy to clipboard", + "clickToCopy": "Click to copy", "selfHostStart": "If you don't have a server, please refer to the", "selfHostContent": "document", "selfHostEnd": "for guidance on how to self-host your own server", - "pleaseInputValidURL": "Please input a valid URL", - "changeUrl": "Change self-hosted url to {}", "cloudURLHint": "Input the base URL of your server", - "webURLHint": "Input the base URL of your web server", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Input the websocket address of your server", "restartApp": "Restart", @@ -1190,46 +816,6 @@ "showNotificationsIcon": { "label": "Show notifications icon", "hint": "Toggle off to hide the notification icon in the sidebar." - }, - "archiveNotifications": { - "allSuccess": "Archived all notifications successfully", - "success": "Archived notification successfully" - }, - "markAsReadNotifications": { - "allSuccess": "Marked all as read successfully", - "success": "Marked as read successfully" - }, - "action": { - "markAsRead": "Mark as read", - "multipleChoice": "Select more", - "archive": "Archive" - }, - "settings": { - "settings": "Settings", - "markAllAsRead": "Mark all as read", - "archiveAll": "Archive all" - }, - "emptyInbox": { - "title": "Inbox Zero!", - "description": "Set reminders to receive notifications here." - }, - "emptyUnread": { - "title": "No unread notifications", - "description": "You're all caught up!" - }, - "emptyArchived": { - "title": "No archived", - "description": "Archived notifications will appear here." - }, - "tabs": { - "inbox": "Inbox", - "unread": "Unread", - "archived": "Archived" - }, - "refreshSuccess": "Notifications refreshed successfully", - "titles": { - "notifications": "Notifications", - "reminder": "Reminder" } }, "appearance": { @@ -1246,15 +832,9 @@ "system": "Adapt to System" }, "fontScaleFactor": "Font Scale Factor", - "displaySize": "Display Size", "documentSettings": { "cursorColor": "Document cursor color", "selectionColor": "Document selection color", - "width": "Document width", - "changeWidth": "Change", - "pickColor": "Select a color", - "colorShade": "Color shade", - "opacity": "Opacity", "hexEmptyError": "Hex color cannot be empty", "hexLengthError": "Hex value must be 6 digits long", "hexInvalidError": "Invalid hex value", @@ -1307,17 +887,15 @@ "showNamingDialogWhenCreatingPage": "Show naming dialog when creating a page", "enableRTLToolbarItems": "Enable RTL toolbar items", "members": { - "title": "Members", + "title": "Members settings", "inviteMembers": "Invite members", "inviteHint": "Invite by email", - "sendInvite": "Invite", + "sendInvite": "Send invite", "copyInviteLink": "Copy invite link", "label": "Members", "user": "User", "role": "Role", "removeFromWorkspace": "Remove from Workspace", - "removeFromWorkspaceSuccess": "Remove from workspace successfully", - "removeFromWorkspaceFailed": "Remove from workspace failed", "owner": "Owner", "guest": "Guest", "member": "Member", @@ -1331,36 +909,13 @@ "one": "{} member", "other": "{} members" }, - "inviteFailedDialogTitle": "Failed to send invite", - "inviteFailedMemberLimit": "Member limit has been reached, please upgrade to invite more members.", - "inviteFailedMemberLimitMobile": "Your workspace has reached the member limit.", - "memberLimitExceeded": "Member limit reached, to invite more members, please ", - "memberLimitExceededUpgrade": "upgrade", - "memberLimitExceededPro": "Member limit reached, if you require more members contact ", - "memberLimitExceededProContact": "support@appflowy.io", + "memberLimitExceeded": "You've reached the maximum member limit allowed for your account. If you want to add more additional members to continue your work, please request on Github", "failedToAddMember": "Failed to add member", "addMemberSuccess": "Member added successfully", "removeMember": "Remove Member", "areYouSureToRemoveMember": "Are you sure you want to remove this member?", "inviteMemberSuccess": "The invitation has been sent successfully", - "failedToInviteMember": "Failed to invite member", - "workspaceMembersError": "Oops, something went wrong", - "workspaceMembersErrorDescription": "We couldn't load the member list at this time. Please try again later", - "inviteLinkToAddMember": "Invite link to add member", - "clickToCopyLink": "Click to copy link", - "or": "or", - "generateANewLink": "generate a new link", - "inviteMemberByEmail": "Invite member by email", - "inviteMemberHintText": "Invite by email", - "resetInviteLink": "Reset the invite link", - "resetInviteLinkDescription": "Resetting will deactivate the current link for all space members and generate a new one. The previous link can only be managed through the", - "adminPanel": "Admin Panel", - "reset": "Reset", - "resetInviteLinkSuccess": "Invite link reset successfully", - "resetInviteLinkFailed": "Failed to reset the invite link", - "resetInviteLinkFailedDescription": "Please try again later", - "memberPageDescription1": "Access the", - "memberPageDescription2": "for guest and advanced user management." + "failedToInviteMember": "Failed to invite member" } }, "files": { @@ -1408,7 +963,8 @@ "email": "Email", "tooltipSelectIcon": "Select icon", "selectAnIcon": "Select an icon", - "pleaseInputYourOpenAIKey": "please input your AI key", + "pleaseInputYourOpenAIKey": "please input your OpenAI key", + "pleaseInputYourStabilityAIKey": "please input your Stability AI key", "clickToLogout": "Click to logout the current user" }, "mobile": { @@ -1444,10 +1000,9 @@ "group": "Group", "addFilter": "Add Filter", "deleteFilter": "Delete filter", - "filterBy": "Filter by", + "filterBy": "Filter by...", "typeAValue": "Type a value...", "layout": "Layout", - "compactMode": "Compact mode", "databaseLayout": "Layout", "viewList": { "zero": "0 views", @@ -1462,13 +1017,6 @@ "deleteView": "Delete view", "numberOfVisibleFields": "{} shown" }, - "filter": { - "empty": "No active filters", - "addFilter": "Add filter", - "cannotFindCreatableField": "Cannot find a suitable field to filter by", - "conditon": "Condition", - "where": "Where" - }, "textFilter": { "contains": "Contains", "doesNotContain": "Does not contain", @@ -1494,8 +1042,8 @@ } }, "checklistFilter": { - "isComplete": "Is complete", - "isIncomplted": "Is incomplete" + "isComplete": "is complete", + "isIncomplted": "is incomplete" }, "selectOptionFilter": { "is": "Is", @@ -1506,7 +1054,7 @@ "isNotEmpty": "Is not empty" }, "dateFilter": { - "is": "Is on", + "is": "Is", "before": "Is before", "after": "Is after", "onOrBefore": "Is on or before", @@ -1514,12 +1062,9 @@ "between": "Is between", "empty": "Is empty", "notEmpty": "Is not empty", - "startDate": "Start date", - "endDate": "End date", "choicechipPrefix": { "before": "Before", "after": "After", - "between": "Between", "onOrBefore": "On or before", "onOrAfter": "On or after", "isEmpty": "Is empty", @@ -1537,16 +1082,14 @@ "isNotEmpty": "Is not empty" }, "field": { - "label": "Property", - "hide": "Hide property", - "show": "Show property", - "insertLeft": "Insert left", - "insertRight": "Insert right", + "hide": "Hide", + "show": "Show", + "insertLeft": "Insert Left", + "insertRight": "Insert Right", "duplicate": "Duplicate", "delete": "Delete", "wrapCellContent": "Wrap text", "clear": "Clear cells", - "switchPrimaryFieldTooltip": "Cannot change field type of primary field", "textFieldName": "Text", "checkboxFieldName": "Checkbox", "dateFieldName": "Date", @@ -1560,7 +1103,6 @@ "relationFieldName": "Relation", "summaryFieldName": "AI Summary", "timeFieldName": "Time", - "mediaFieldName": "Files & media", "translateFieldName": "AI Translate", "translateTo": "Translate to", "numberFormat": "Number format", @@ -1591,10 +1133,9 @@ "addOption": "Add option", "editProperty": "Edit property", "newProperty": "New property", - "openRowDocument": "Open as a page", - "deleteFieldPromptMessage": "Are you sure? This property and all its data will be deleted", + "deleteFieldPromptMessage": "Are you sure? This property will be deleted", "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", - "newColumn": "New column", + "newColumn": "New Column", "format": "Format", "reminderOnDateTooltip": "This cell has a scheduled reminder", "optionAlreadyExist": "Option already exists" @@ -1622,13 +1163,11 @@ "empty": "No active sorts", "cannotFindCreatableField": "Cannot find a suitable field to sort by", "deleteAllSorts": "Delete all sorts", - "addSort": "Add sort", - "sortsActive": "Cannot {intention} while sorting", - "removeSorting": "Would you like to remove all the sorts in this view and continue?", + "addSort": "Add new sort", + "removeSorting": "Would you like to remove sorting?", "fieldInUse": "You are already sorting by this field" }, "row": { - "label": "Row", "duplicate": "Duplicate", "delete": "Delete", "titlePlaceholder": "Untitled", @@ -1636,19 +1175,15 @@ "copyProperty": "Copied property to clipboard", "count": "Count", "newRow": "New row", - "loadMore": "Load more", "action": "Action", "add": "Click add to below", "drag": "Drag to move", - "deleteRowPrompt": "Are you sure you want to delete this row? This action cannot be undone.", - "deleteCardPrompt": "Are you sure you want to delete this card? This action cannot be undone.", + "deleteRowPrompt": "Are you sure you want to delete this row? This action cannot be undone", + "deleteCardPrompt": "Are you sure you want to delete this card? This action cannot be undone", "dragAndClick": "Drag to move, click to open menu", "insertRecordAbove": "Insert record above", "insertRecordBelow": "Insert record below", - "noContent": "No content", - "reorderRowDescription": "reorder row", - "createRowAboveDescription": "create a row above", - "createRowBelowDescription": "insert a row below" + "noContent": "No content" }, "selectOption": { "create": "Create", @@ -1681,7 +1216,8 @@ "url": { "launch": "Open link in browser", "copy": "Copy link to clipboard", - "textFieldHint": "Enter a URL" + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" }, "relation": { "relatedDatabasePlaceLabel": "Related Database", @@ -1708,24 +1244,6 @@ "countEmptyShort": "EMPTY", "countNonEmpty": "Count not empty", "countNonEmptyShort": "FILLED" - }, - "media": { - "rename": "Rename", - "download": "Download", - "expand": "Expand", - "delete": "Delete", - "moreFilesHint": "+{}", - "addFileOrImage": "Add file or link", - "attachmentsHint": "{}", - "addFileMobile": "Add file", - "extraCount": "+{}", - "deleteFileDescription": "Are you sure you want to delete this file? This action is irreversible.", - "showFileNames": "Show file name", - "downloadSuccess": "File downloaded", - "downloadFailedToken": "Failed to download file, user token unavailable", - "setAsCover": "Set as cover", - "openInBrowser": "Open in browser", - "embedLink": "Embed file link" } }, "document": { @@ -1734,7 +1252,6 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "Creating...", "slashMenu": { "board": { "selectABoardToLinkTo": "Select a Board to link to", @@ -1750,62 +1267,6 @@ }, "document": { "selectADocumentToLinkTo": "Select a Document to link to" - }, - "name": { - "textStyle": "Text Style", - "list": "List", - "toggle": "Toggle", - "fileAndMedia": "File & Media", - "simpleTable": "Simple Table", - "visuals": "Visuals", - "document": "Document", - "advanced": "Advanced", - "text": "Text", - "heading1": "Heading 1", - "heading2": "Heading 2", - "heading3": "Heading 3", - "image": "Image", - "bulletedList": "Bulleted list", - "numberedList": "Numbered list", - "todoList": "To-do list", - "doc": "Doc", - "linkedDoc": "Link to page", - "grid": "Grid", - "linkedGrid": "Linked Grid", - "kanban": "Kanban", - "linkedKanban": "Linked Kanban", - "calendar": "Calendar", - "linkedCalendar": "Linked Calendar", - "quote": "Quote", - "divider": "Divider", - "table": "Table", - "callout": "Callout", - "outline": "Outline", - "mathEquation": "Math Equation", - "code": "Code", - "toggleList": "Toggle list", - "toggleHeading1": "Toggle heading 1", - "toggleHeading2": "Toggle heading 2", - "toggleHeading3": "Toggle heading 3", - "emoji": "Emoji", - "aiWriter": "Ask AI Anything", - "dateOrReminder": "Date or Reminder", - "photoGallery": "Photo Gallery", - "file": "File", - "twoColumns": "2 Columns", - "threeColumns": "3 Columns", - "fourColumns": "4 Columns" - }, - "subPage": { - "name": "Document", - "keyword1": "sub page", - "keyword2": "page", - "keyword3": "child page", - "keyword4": "insert page", - "keyword5": "embed page", - "keyword6": "new page", - "keyword7": "create page", - "keyword8": "document" } }, "selectionMenu": { @@ -1817,77 +1278,34 @@ "referencedGrid": "Referenced Grid", "referencedCalendar": "Referenced Calendar", "referencedDocument": "Referenced Document", - "aiWriter": { - "userQuestion": "Ask AI anything", - "continueWriting": "Continue writing", - "fixSpelling": "Fix spelling & grammar", - "improveWriting": "Improve writing", - "summarize": "Summarize", - "explain": "Explain", - "makeShorter": "Make shorter", - "makeLonger": "Make longer" - }, - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Ask AI to write anything...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", "autoGeneratorLearnMore": "Learn more", "autoGeneratorGenerate": "Generate", - "autoGeneratorHintText": "Ask AI ...", - "autoGeneratorCantGetOpenAIKey": "Can't get AI key", + "autoGeneratorHintText": "Ask OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Can't get OpenAI key", "autoGeneratorRewrite": "Rewrite", - "smartEdit": "Ask AI", - "aI": "AI", - "smartEditFixSpelling": "Fix spelling & grammar", + "smartEdit": "AI Assistants", + "openAI": "OpenAI", + "smartEditFixSpelling": "Fix spelling", "warning": "⚠️ AI responses can be inaccurate or misleading.", "smartEditSummarize": "Summarize", "smartEditImproveWriting": "Improve writing", "smartEditMakeLonger": "Make longer", - "smartEditCouldNotFetchResult": "Could not fetch result from AI", - "smartEditCouldNotFetchKey": "Could not fetch AI key", - "smartEditDisabled": "Connect AI in Settings", - "appflowyAIEditDisabled": "Sign in to enable AI features", - "discardResponse": "Are you sure you want to discard the AI response?", + "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", + "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", + "smartEditDisabled": "Connect OpenAI in Settings", + "discardResponse": "Do you want to discard the AI responses?", "createInlineMathEquation": "Create equation", "fonts": "Fonts", "insertDate": "Insert date", "emoji": "Emoji", "toggleList": "Toggle list", - "emptyToggleHeading": "Empty toggle h{}. Click to add content.", - "emptyToggleList": "Empty toggle list. Click to add content.", - "emptyToggleHeadingWeb": "Empty toggle h{level}. Click to add content", "quoteList": "Quote list", "numberedList": "Numbered list", "bulletedList": "Bulleted list", - "todoList": "Todo list", + "todoList": "Todo List", "callout": "Callout", - "simpleTable": { - "moreActions": { - "color": "Color", - "align": "Align", - "delete": "Delete", - "duplicate": "Duplicate", - "insertLeft": "Insert left", - "insertRight": "Insert right", - "insertAbove": "Insert above", - "insertBelow": "Insert below", - "headerColumn": "Header column", - "headerRow": "Header row", - "clearContents": "Clear contents", - "setToPageWidth": "Set to page width", - "distributeColumnsWidth": "Distribute columns evenly", - "duplicateRow": "Duplicate row", - "duplicateColumn": "Duplicate column", - "textColor": "Text color", - "cellBackgroundColor": "Cell background color", - "duplicateTable": "Duplicate table" - }, - "clickToAddNewRow": "Click to add a new row", - "clickToAddNewColumn": "Click to add a new column", - "clickToAddNewRowAndColumn": "Click to add a new row and column", - "headerName": { - "table": "Table", - "alignText": "Align text" - } - }, "cover": { "changeCover": "Change Cover", "colors": "Colors", @@ -1903,7 +1321,6 @@ "back": "Back", "saveToGallery": "Save to gallery", "removeIcon": "Remove icon", - "removeCover": "Remove cover", "pasteImageUrl": "Paste image URL", "or": "OR", "pickFromFiles": "Pick from files", @@ -1922,8 +1339,6 @@ "optionAction": { "click": "Click", "toOpenMenu": " to open menu", - "drag": "Drag", - "toMove": " to move", "delete": "Delete", "duplicate": "Duplicate", "turnInto": "Turn into", @@ -1935,36 +1350,14 @@ "center": "Center", "right": "Right", "defaultColor": "Default", - "depth": "Depth", - "copyLinkToBlock": "Copy link to block" + "depth": "Depth" }, "image": { - "addAnImage": "Add images", "copiedToPasteBoard": "The image link has been copied to the clipboard", - "addAnImageDesktop": "Add an image", - "addAnImageMobile": "Click to add one or more images", - "dropImageToInsert": "Drop images to insert", + "addAnImage": "Add an image", "imageUploadFailed": "Image upload failed", - "imageDownloadFailed": "Image download failed, please try again", - "imageDownloadFailedToken": "Image download failed due to missing user token, please try again", "errorCode": "Error code" }, - "photoGallery": { - "name": "Photo gallery", - "imageKeyword": "image", - "imageGalleryKeyword": "image gallery", - "photoKeyword": "photo", - "photoBrowserKeyword": "photo browser", - "galleryKeyword": "gallery", - "addImageTooltip": "Add image", - "changeLayoutTooltip": "Change layout", - "browserLayout": "Browser", - "gridLayout": "Grid", - "deleteBlockTooltip": "Delete whole gallery" - }, - "math": { - "copiedToPasteBoard": "The math equation has been copied to the clipboard" - }, "urlPreview": { "copiedToPasteBoard": "The link has been copied to the clipboard", "convertToLink": "Convert to embed link" @@ -1984,8 +1377,7 @@ "contextMenu": { "copy": "Copy", "cut": "Cut", - "paste": "Paste", - "pasteAsPlainText": "Paste as plain text" + "paste": "Paste" }, "action": "Actions", "database": { @@ -2006,62 +1398,6 @@ "invalidVideoUrl": "The source URL is not supported yet.", "invalidVideoUrlYouTube": "YouTube is not supported yet.", "supportedFormats": "Supported formats: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "File", - "uploadTab": "Upload", - "uploadMobile": "Choose a file", - "uploadMobileGallery": "From Photo Gallery", - "networkTab": "Embed link", - "placeholderText": "Upload or embed a file", - "placeholderDragging": "Drop the file to upload", - "dropFileToUpload": "Drop a file to upload", - "fileUploadHint": "Drag & drop a file or click to ", - "fileUploadHintSuffix": "Browse", - "networkHint": "Paste a file link", - "networkUrlInvalid": "Invalid URL. Check the URL and try again.", - "networkAction": "Embed", - "fileTooBigError": "File size is too big, please upload a file with size less than 10MB", - "renameFile": { - "title": "Rename file", - "description": "Enter the new name for this file", - "nameEmptyError": "File name cannot be left empty." - }, - "uploadedAt": "Uploaded on {}", - "linkedAt": "Link added on {}", - "failedToOpenMsg": "Failed to open, file not found" - }, - "subPage": { - "handlingPasteHint": " - (handling paste)", - "errors": { - "failedDeletePage": "Failed to delete page", - "failedCreatePage": "Failed to create page", - "failedMovePage": "Failed to move page to this document", - "failedDuplicatePage": "Failed to duplicate page", - "failedDuplicateFindView": "Failed to duplicate page - original view not found" - } - }, - "cannotMoveToItsChildren": "Cannot move to its children", - "linkPreview": { - "typeSelection": { - "pasteAs": "Paste as", - "mention": "Mention", - "URL": "URL", - "bookmark": "Bookmark", - "embed": "Embed" - }, - "linkPreviewMenu": { - "toMetion": "Convert to Mention", - "toUrl": "Convert to URL", - "toEmbed": "Convert to Embed", - "toBookmark": "Convert to Bookmark", - "copyLink": "Copy Link", - "replace": "Replace", - "reload": "Reload", - "removeLink": "Remove Link", - "pasteHint": "Paste in https://...", - "unableToDisplay": "unable to display" - } } }, "outlineBlock": { @@ -2074,7 +1410,7 @@ "placeholder": "Untitled" }, "imageBlock": { - "placeholder": "Click to add image(s)", + "placeholder": "Click to add image", "upload": { "label": "Upload", "placeholder": "Click to upload image" @@ -2084,8 +1420,8 @@ "placeholder": "Enter image URL" }, "ai": { - "label": "Generate image from AI", - "placeholder": "Please input the prompt for AI to generate image" + "label": "Generate image from OpenAI", + "placeholder": "Please input the prompt for OpenAI to generate image" }, "stability_ai": { "label": "Generate image from Stability AI", @@ -2097,8 +1433,7 @@ "invalidImageSize": "Image size must be less than 5MB", "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP", "invalidImageUrl": "Invalid image URL", - "noImage": "No such file or directory", - "multipleImagesFailed": "One or more images failed to upload, please try again" + "noImage": "No such file or directory" }, "embedLink": { "label": "Embed link", @@ -2108,29 +1443,15 @@ "label": "Unsplash" }, "searchForAnImage": "Search for an image", - "pleaseInputYourOpenAIKey": "please input your AI key in Settings page", + "pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", + "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page", "saveImageToGallery": "Save image", - "failedToAddImageToGallery": "Failed to save image", - "successToAddImageToGallery": "Saved image to Photos", + "failedToAddImageToGallery": "Failed to add image to gallery", + "successToAddImageToGallery": "Image added to gallery successfully", "unableToLoadImage": "Unable to load image", "maximumImageSize": "Maximum supported upload image size is 10MB", "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB", - "imageIsUploading": "Image is uploading", - "openFullScreen": "Open in full screen", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Previous image", - "nextImageTooltip": "Next image", - "zoomOutTooltip": "Zoom out", - "zoomInTooltip": "Zoom in", - "changeZoomLevelTooltip": "Change zoom level", - "openLocalImage": "Open image", - "downloadImage": "Download image", - "closeViewer": "Close interactive viewer", - "scalePercentage": "{}%", - "deleteImageTooltip": "Delete image" - } - } + "imageIsUploading": "Image is uploading" }, "codeBlock": { "language": { @@ -2138,7 +1459,7 @@ "placeholder": "Select language", "auto": "Auto" }, - "copyTooltip": "Copy", + "copyTooltip": "Copy contents of the code block", "searchLanguageHint": "Search for a language", "codeCopiedSnackbar": "Code copied to clipboard!" }, @@ -2163,57 +1484,24 @@ "tooltip": "Click to open page" }, "deleted": "Deleted", - "deletedContent": "This content does not exist or has been deleted", - "noAccess": "No Access", - "deletedPage": "Deleted page", - "trashHint": " - in trash", - "morePages": "more pages" + "deletedContent": "This content does not exist or has been deleted" }, "toolbar": { - "resetToDefaultFont": "Reset to default", - "textSize": "Text size", - "textColor": "Text color", - "h1": "Heading 1", - "h2": "Heading 2", - "h3": "Heading 3", - "alignLeft": "Align left", - "alignRight": "Align right", - "alignCenter": "Align center", - "link": "Link", - "textAlign": "Text align", - "moreOptions": "More options", - "font": "Font", - "inlineCode": "Inline code", - "suggestions": "Suggestions", - "turnInto": "Turn into", - "equation": "Equation", - "insert": "Insert", - "linkInputHint": "Paste link or search pages", - "pageOrURL": "Page or URL", - "linkName": "Link Name", - "linkNameHint": "Input link name" + "resetToDefaultFont": "Reset to default" }, "errorBlock": { "theBlockIsNotSupported": "Unable to parse the block content", "clickToCopyTheBlockContent": "Click to copy the block content", - "blockContentHasBeenCopied": "The block content has been copied.", - "parseError": "An error occurred while parsing the {} block.", - "copyBlockContent": "Copy block content" + "blockContentHasBeenCopied": "The block content has been copied." }, "mobilePageSelector": { "title": "Select page", "failedToLoad": "Failed to load page list", "noPagesFound": "No pages found" - }, - "attachmentMenu": { - "choosePhoto": "Choose photo", - "takePicture": "Take a picture", - "chooseFile": "Choose file" } }, "board": { "column": { - "label": "Column", "createNewCard": "New", "renameGroupTooltip": "Press to rename group", "createNewColumn": "Add a new group", @@ -2223,7 +1511,7 @@ "hideColumn": "Hide", "newGroup": "New group", "deleteColumn": "Delete", - "deleteColumnConfirmation": "This will delete this group and all the cards in it. Are you sure you want to continue?" + "deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?" }, "hiddenGroupSection": { "sectionTitle": "Hidden Groups", @@ -2263,11 +1551,7 @@ "nextThirtyDays": "Next 30 days" }, "noGroup": "No group by property", - "noGroupDesc": "Board views require a property to group by in order to display", - "media": { - "cardText": "{} {}", - "fallbackName": "files" - } + "noGroupDesc": "Board views require a property to group by in order to display" }, "calendar": { "menuName": "Calendar", @@ -2313,20 +1597,17 @@ "errorDialog": { "title": "@:appName Error", "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", - "howToFixFallbackHint1": "We're sorry for the inconvenience! Submit an issue on our ", - "howToFixFallbackHint2": " page that describes your error.", "github": "View on GitHub" }, "search": { "label": "Search", - "sidebarSearchIcon": "Search and quickly jump to a page", "placeholder": { "actions": "Search actions..." } }, "message": { "copy": { - "success": "Copied to clipboard", + "success": "Copied!", "fail": "Unable to copy" } }, @@ -2359,16 +1640,16 @@ "remove": "Remove emoji", "categories": { "smileys": "Smileys & Emotion", - "people": "people", - "animals": "nature", - "food": "foods", - "activities": "activities", - "places": "places", - "objects": "objects", - "symbols": "symbols", - "flags": "flags", - "nature": "nature", - "frequentlyUsed": "frequently Used" + "people": "People & Body", + "animals": "Animals & Nature", + "food": "Food & Drink", + "activities": "Activities", + "places": "Travel & Places", + "objects": "Objects", + "symbols": "Symbols", + "flags": "Flags", + "nature": "Nature", + "frequentlyUsed": "Frequently Used" }, "skinTone": { "default": "Default", @@ -2377,8 +1658,7 @@ "medium": "Medium", "mediumDark": "Medium-Dark", "dark": "Dark" - }, - "openSourceIconsFrom": "Open source icons from" + } }, "inlineActions": { "noResults": "No results", @@ -2392,8 +1672,7 @@ "reminder": { "groupTitle": "Reminder", "shortKeyword": "remind" - }, - "createPage": "Create \"{}\" sub-page" + } }, "datePicker": { "dateTimeFormatTooltip": "Change the date and time format in settings", @@ -2470,14 +1749,11 @@ }, "error": { "weAreSorry": "We're sorry", - "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues.", - "syncError": "Data is not synced from another device", - "syncErrorHint": "Please reopen this page on the device where it was last edited, then open it again on the current device.", - "clickToCopy": "Click to copy error code" + "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." }, "editor": { "bold": "Bold", - "bulletedList": "Bulleted list", + "bulletedList": "Bulleted List", "bulletedListShortForm": "Bulleted", "checkbox": "Checkbox", "embedCode": "Embed Code", @@ -2491,11 +1767,8 @@ "page": "Page", "italic": "Italic", "link": "Link", - "numberedList": "Numbered list", + "numberedList": "Numbered List", "numberedListShortForm": "Numbered", - "toggleHeading1ShortForm": "Toggle H1", - "toggleHeading2ShortForm": "Toggle H2", - "toggleHeading3ShortForm": "Toggle H3", "quote": "Quote", "strikethrough": "Strikethrough", "text": "Text", @@ -2546,9 +1819,6 @@ "mobileHeading1": "Heading 1", "mobileHeading2": "Heading 2", "mobileHeading3": "Heading 3", - "mobileHeading4": "Heading 4", - "mobileHeading5": "Heading 5", - "mobileHeading6": "Heading 6", "textColor": "Text Color", "backgroundColor": "Background Color", "addYourLink": "Add your link", @@ -2556,7 +1826,6 @@ "copyLink": "Copy link", "removeLink": "Remove link", "editLink": "Edit link", - "convertTo": "Convert to", "linkText": "Text", "linkTextHint": "Please enter text", "linkAddressHint": "Please enter URL", @@ -2636,7 +1905,7 @@ "noLogFiles": "There're no log files", "newSettings": { "myAccount": { - "title": "Account & App", + "title": "My account", "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", "profileLabel": "Account name & Profile image", "profileNamePlaceholder": "Enter your name", @@ -2646,50 +1915,14 @@ "accountLogin": "Account Login", "updateNameError": "Failed to update name", "updateIconError": "Failed to update icon", - "aboutAppFlowy": "About @:appName", "deleteAccount": { "title": "Delete Account", "subtitle": "Permanently delete your account and all of your data.", - "description": "Permanently delete your account and remove access from all workspaces.", "deleteMyAccount": "Delete my account", "dialogTitle": "Delete account", "dialogContent1": "Are you sure you want to permanently delete your account?", - "dialogContent2": "This action cannot be undone, and will remove access from all workspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces.", - "confirmHint1": "Please type \"@:newSettings.myAccount.deleteAccount.confirmHint3\" to confirm.", - "confirmHint2": "I understand that this action is irreversible and will permanently delete my account and all associated data.", - "confirmHint3": "DELETE MY ACCOUNT", - "checkToConfirmError": "You must check the box to confirm deletion", - "failedToGetCurrentUser": "Failed to get current user email", - "confirmTextValidationFailed": "Your confirmation text does not match \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", - "deleteAccountSuccess": "Account deleted successfully" - }, - "password": { - "title": "Password", - "changePassword": "Change password", - "currentPassword": "Current password", - "newPassword": "New password", - "confirmNewPassword": "Confirm new password", - "setupPassword": "Setup password", - "error": { - "newPasswordIsRequired": "New password is required", - "confirmPasswordIsRequired": "Confirm password is required", - "passwordsDoNotMatch": "Passwords do not match", - "newPasswordIsSameAsCurrent": "New password is same as current password" - }, - "toast": { - "passwordUpdatedSuccessfully": "Password updated successfully", - "passwordUpdatedFailed": "Failed to update password", - "passwordSetupSuccessfully": "Password setup successfully", - "passwordSetupFailed": "Failed to setup password" - }, - "hint": { - "enterYourCurrentPassword": "Enter your current password", - "enterYourNewPassword": "Enter your new password", - "confirmYourNewPassword": "Confirm your new password" - } - }, - "myAccount": "My Account", - "myProfile": "My Profile" + "dialogContent2": "This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces." + } }, "workplace": { "name": "Workplace", @@ -2731,22 +1964,15 @@ "unsplash": "Unsplash", "pageCover": "Page cover", "none": "None", + "photoPermissionDescription": "Allow access to the photo library for uploading images.", "openSettings": "Open Settings", "photoPermissionTitle": "@:appName would like to access your photo library", - "photoPermissionDescription": "@:appName needs access to your photos to let you add images to your documents", - "cameraPermissionTitle": "@:appName would like to access your camera", - "cameraPermissionDescription": "@:appName needs access to your camera to let you add images to your documents from the camera", "doNotAllow": "Don't Allow", "image": "Image" }, "commandPalette": { - "placeholder": "Search or ask a question...", + "placeholder": "Type to search...", "bestMatches": "Best matches", - "aiOverview": "AI overview", - "aiOverviewSource": "Reference sources", - "aiOverviewMoreDetails": "More details", - "pagePreview": "Content preview", - "clickToOpenPage": "Click to open page", "recentHistory": "Recent history", "navigateHint": "to navigate", "loadingTooltip": "We are looking for results...", @@ -2759,7 +1985,7 @@ "space": { "delete": "Delete", "deleteConfirmation": "Delete: ", - "deleteConfirmationDescription": "All pages within this Space will be deleted and moved to the Trash, and any published pages will be unpublished.", + "deleteConfirmationDescription": "All pages within this Space will be deleted and moved to Trash.", "rename": "Rename Space", "changeIcon": "Change icon", "manage": "Manage Space", @@ -2768,8 +1994,7 @@ "createNewSpace": "Create a new space", "createSpaceDescription": "Create multiple public and private spaces to better organize your work.", "spaceName": "Space name", - "spaceNamePlaceholder": "e.g. Marketing, Engineering, HR", - "permission": "Space permission", + "permission": "Permission", "publicPermission": "Public", "publicPermissionDescription": "All workspace members with full access", "privatePermission": "Private", @@ -2778,7 +2003,7 @@ "spaceIcon": "Icon", "dangerZone": "Danger Zone", "unableToDeleteLastSpace": "Unable to delete the last Space", - "unableToDeleteSpaceNotCreatedByYou": "Unable to delete spaces created by others", + "unableToDeleteSpaceNotCreatedByYou": "Unable to delete Spaces created by others", "enableSpacesForYourWorkspace": "Enable Spaces for your workspace", "title": "Spaces", "defaultSpaceName": "General", @@ -2786,513 +2011,6 @@ "upgradeSpaceDescription": "Create multiple public and private Spaces to better organize your workspace.", "upgrade": "Update", "upgradeYourSpace": "Create multiple Spaces", - "quicklySwitch": "Quickly switch to the next space", - "duplicate": "Duplicate Space", - "movePageToSpace": "Move page to space", - "cannotMovePageToDatabase": "Cannot move page to database", - "switchSpace": "Switch space", - "spaceNameCannotBeEmpty": "Space name cannot be empty", - "success": { - "deleteSpace": "Space deleted successfully", - "renameSpace": "Space renamed successfully", - "duplicateSpace": "Space duplicated successfully", - "updateSpace": "Space updated successfully" - }, - "error": { - "deleteSpace": "Failed to delete space", - "renameSpace": "Failed to rename space", - "duplicateSpace": "Failed to duplicate space", - "updateSpace": "Failed to update space" - }, - "createSpace": "Create space", - "manageSpace": "Manage space", - "renameSpace": "Rename space", - "mSpaceIconColor": "Space icon color", - "mSpaceIcon": "Space icon" - }, - "publish": { - "hasNotBeenPublished": "This page hasn't been published yet", - "spaceHasNotBeenPublished": "Haven't supported publishing a space yet", - "reportPage": "Report page", - "databaseHasNotBeenPublished": "Publishing a database is not supported yet.", - "createdWith": "Created with", - "downloadApp": "Download AppFlowy", - "copy": { - "codeBlock": "The content of code block has been copied to the clipboard", - "imageBlock": "The image link has been copied to the clipboard", - "mathBlock": "The math equation has been copied to the clipboard", - "fileBlock": "The file link has been copied to the clipboard" - }, - "containsPublishedPage": "This page contains one or more published pages. If you continue, they will be unpublished. Do you want to proceed with deletion?", - "publishSuccessfully": "Published successfully", - "unpublishSuccessfully": "Unpublished successfully", - "publishFailed": "Failed to publish", - "unpublishFailed": "Failed to unpublish", - "noAccessToVisit": "No access to this page...", - "createWithAppFlowy": "Create a website with AppFlowy", - "fastWithAI": "Fast and easy with AI.", - "tryItNow": "Try it now", - "onlyGridViewCanBePublished": "Only Grid view can be published", - "database": { - "zero": "Publish {} selected view", - "one": "Publish {} selected views", - "many": "Publish {} selected views", - "other": "Publish {} selected views" - }, - "mustSelectPrimaryDatabase": "The primary view must be selected", - "noDatabaseSelected": "No database selected, please select at least one database.", - "unableToDeselectPrimaryDatabase": "Unable to deselect primary database", - "saveThisPage": "Start with this template", - "duplicateTitle": "Where would you like to add", - "selectWorkspace": "Select a workspace", - "addTo": "Add to", - "duplicateSuccessfully": "Added to your workspace", - "duplicateSuccessfullyDescription": "Don't have AppFlowy installed? The download will start automatically after you click 'Download'.", - "downloadIt": "Download", - "openApp": "Open in app", - "duplicateFailed": "Duplicated failed", - "membersCount": { - "zero": "No members", - "one": "1 member", - "many": "{count} members", - "other": "{count} members" - }, - "useThisTemplate": "Use the template" - }, - "web": { - "continue": "Continue", - "or": "or", - "continueWithGoogle": "Continue with Google", - "continueWithGithub": "Continue with GitHub", - "continueWithDiscord": "Continue with Discord", - "continueWithApple": "Continue with Apple ", - "moreOptions": "More options", - "collapse": "Collapse", - "signInAgreement": "By clicking \"Continue\" above, you agreed to \nAppFlowy's ", - "signInLocalAgreement": "By clicking \"Get Started\" above, you agreed to \nAppFlowy's ", - "and": "and", - "termOfUse": "Terms", - "privacyPolicy": "Privacy Policy", - "signInError": "Sign in error", - "login": "Sign up or log in", - "fileBlock": { - "uploadedAt": "Uploaded on {time}", - "linkedAt": "Link added on {time}", - "empty": "Upload or embed a file", - "uploadFailed": "Upload failed, please try again", - "retry": "Retry" - }, - "importNotion": "Import from Notion", - "import": "Import", - "importSuccess": "Uploaded successfully", - "importSuccessMessage": "We'll notify you when the import is complete. After that, you can view your imported pages in the sidebar.", - "importFailed": "Import failed, please check the file format", - "dropNotionFile": "Drop your Notion zip file here to upload, or click to browse", - "error": { - "pageNameIsEmpty": "The page name is empty, please try another one" - } - }, - "globalComment": { - "comments": "Comments", - "addComment": "Add a comment", - "reactedBy": "reacted by", - "addReaction": "Add reaction", - "reactedByMore": "and {count} others", - "showSeconds": { - "one": "1 second ago", - "other": "{count} seconds ago", - "zero": "Just now", - "many": "{count} seconds ago" - }, - "showMinutes": { - "one": "1 minute ago", - "other": "{count} minutes ago", - "many": "{count} minutes ago" - }, - "showHours": { - "one": "1 hour ago", - "other": "{count} hours ago", - "many": "{count} hours ago" - }, - "showDays": { - "one": "1 day ago", - "other": "{count} days ago", - "many": "{count} days ago" - }, - "showMonths": { - "one": "1 month ago", - "other": "{count} months ago", - "many": "{count} months ago" - }, - "showYears": { - "one": "1 year ago", - "other": "{count} years ago", - "many": "{count} years ago" - }, - "reply": "Reply", - "deleteComment": "Delete comment", - "youAreNotOwner": "You are not the owner of this comment", - "confirmDeleteDescription": "Are you sure you want to delete this comment?", - "hasBeenDeleted": "Deleted", - "replyingTo": "Replying to", - "noAccessDeleteComment": "You're not allowed to delete this comment", - "collapse": "Collapse", - "readMore": "Read more", - "failedToAddComment": "Failed to add comment", - "commentAddedSuccessfully": "Comment added successfully.", - "commentAddedSuccessTip": "You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?" - }, - "template": { - "asTemplate": "Save as template", - "name": "Template name", - "description": "Template Description", - "about": "Template About", - "deleteFromTemplate": "Delete from templates", - "preview": "Template Preview", - "categories": "Template Categories", - "isNewTemplate": "PIN to New template", - "featured": "PIN to Featured", - "relatedTemplates": "Related Templates", - "requiredField": "{field} is required", - "addCategory": "Add \"{category}\"", - "addNewCategory": "Add new category", - "addNewCreator": "Add new creator", - "deleteCategory": "Delete category", - "editCategory": "Edit category", - "editCreator": "Edit creator", - "category": { - "name": "Category name", - "icon": "Category icon", - "bgColor": "Category background color", - "priority": "Category priority", - "desc": "Category description", - "type": "Category type", - "icons": "Category Icons", - "colors": "Category Colors", - "byUseCase": "By Use Case", - "byFeature": "By Feature", - "deleteCategory": "Delete category", - "deleteCategoryDescription": "Are you sure you want to delete this category?", - "typeToSearch": "Type to search categories..." - }, - "creator": { - "label": "Template Creator", - "name": "Creator name", - "avatar": "Creator avatar", - "accountLinks": "Creator account links", - "uploadAvatar": "Click to upload avatar", - "deleteCreator": "Delete creator", - "deleteCreatorDescription": "Are you sure you want to delete this creator?", - "typeToSearch": "Type to search creators..." - }, - "uploadSuccess": "Template uploaded successfully", - "uploadSuccessDescription": "Your template has been uploaded successfully. You can now view it in the template gallery.", - "viewTemplate": "View template", - "deleteTemplate": "Delete template", - "deleteSuccess": "Template deleted successfully", - "deleteTemplateDescription": "This won't affect the current page or published status. Are you sure you want to delete this template?", - "addRelatedTemplate": "Add related template", - "removeRelatedTemplate": "Remove related template", - "uploadAvatar": "Upload avatar", - "searchInCategory": "Search in {category}", - "label": "Templates" - }, - "fileDropzone": { - "dropFile": "Click or drag file to this area to upload", - "uploading": "Uploading...", - "uploadFailed": "Upload failed", - "uploadSuccess": "Upload success", - "uploadSuccessDescription": "The file has been uploaded successfully", - "uploadFailedDescription": "The file upload failed", - "uploadingDescription": "The file is being uploaded" - }, - "gallery": { - "preview": "Open in full screen", - "copy": "Copy", - "download": "Download", - "prev": "Previous", - "next": "Next", - "resetZoom": "Reset zoom", - "zoomIn": "Zoom in", - "zoomOut": "Zoom out" - }, - "invitation": { - "join": "Join", - "on": "on", - "invitedBy": "Invited by", - "membersCount": { - "zero": "{count} members", - "one": "{count} member", - "many": "{count} members", - "other": "{count} members" - }, - "tip": "You’ve been invited to Join this workspace with the contact information below. If this is incorrect, contact your administrator to resend the invite.", - "joinWorkspace": "Join workspace", - "success": "You've successfully joined the workspace", - "successMessage": "You can now access all the pages and workspaces within it.", - "openWorkspace": "Open AppFlowy", - "alreadyAccepted": "You've already accepted the invitation", - "errorModal": { - "title": "Something went wrong", - "description": "Your current account {email} may not have access to this workspace. Please log in with the correct account or contact the workspace owner for help.", - "contactOwner": "Contact owner", - "close": "Back to home", - "changeAccount": "Change account" - } - }, - "requestAccess": { - "title": "No access to this page", - "subtitle": "You can request access from the owner of this page. Once approved, you can view the page.", - "requestAccess": "Request access", - "backToHome": "Back to home", - "tip": "You're currently logged in as <link/>.", - "mightBe": "You might need to <login/> with a different account.", - "successful": "Request sent successfully", - "successfulMessage": "You will be notified once the owner approves your request.", - "requestError": "Failed to request access", - "repeatRequestError": "You've already requested access to this page" - }, - "approveAccess": { - "title": "Approve Workspace Join Request", - "requestSummary": "<user/> requests to join <workspace/> and access <page/>", - "upgrade": "upgrade", - "downloadApp": "Download AppFlowy", - "approveButton": "Approve", - "approveSuccess": "Approved successfully", - "approveError": "Failed to approve, ensure the workspace plan limit is not exceeded", - "getRequestInfoError": "Failed to get request info", - "memberCount": { - "zero": "No members", - "one": "1 member", - "many": "{count} members", - "other": "{count} members" - }, - "alreadyProTitle": "You've reached the workspace plan limit", - "alreadyProMessage": "Ask them to contact <email/> to unlock more members", - "repeatApproveError": "You've already approved this request", - "ensurePlanLimit": "Ensure the workspace plan limit is not exceeded. If the limit is exceeded, consider <upgrade/> the workspace plan or <download/>.", - "requestToJoin": "requested to join", - "asMember": "as a member" - }, - "upgradePlanModal": { - "title": "Upgrade to Pro", - "message": "{name} has reached the free member limit. Upgrade to the Pro Plan to invite more members.", - "upgradeSteps": "How to upgrade your plan on AppFlowy:", - "step1": "1. Go to Settings", - "step2": "2. Click on 'Plan'", - "step3": "3. Select 'Change Plan'", - "appNote": "Note: ", - "actionButton": "Upgrade", - "downloadLink": "Download App", - "laterButton": "Later", - "refreshNote": "After successful upgrade, click <refresh/> to activate your new features.", - "refresh": "here" - }, - "breadcrumbs": { - "label": "Breadcrumbs" - }, - "time": { - "justNow": "Just now", - "seconds": { - "one": "1 second", - "other": "{count} seconds" - }, - "minutes": { - "one": "1 minute", - "other": "{count} minutes" - }, - "hours": { - "one": "1 hour", - "other": "{count} hours" - }, - "days": { - "one": "1 day", - "other": "{count} days" - }, - "weeks": { - "one": "1 week", - "other": "{count} weeks" - }, - "months": { - "one": "1 month", - "other": "{count} months" - }, - "years": { - "one": "1 year", - "other": "{count} years" - }, - "ago": "ago", - "yesterday": "Yesterday", - "today": "Today" - }, - "members": { - "zero": "No members", - "one": "1 member", - "many": "{count} members", - "other": "{count} members" - }, - "tabMenu": { - "close": "Close", - "closeDisabledHint": "Cannot close a pinned tab, please unpin first", - "closeOthers": "Close other tabs", - "closeOthersHint": "This will close all unpinned tabs except this one", - "closeOthersDisabledHint": "All tabs are pinned, cannot find any tabs to close", - "favorite": "Favorite", - "unfavorite": "Unfavorite", - "favoriteDisabledHint": "Cannot favorite this view", - "pinTab": "Pin", - "unpinTab": "Unpin" - }, - "openFileMessage": { - "success": "File opened successfully", - "fileNotFound": "File not found", - "noAppToOpenFile": "No app to open this file", - "permissionDenied": "No permission to open this file", - "unknownError": "File open failed" - }, - "inviteMember": { - "requestInviteMembers": "Invite to your workspace", - "inviteFailedMemberLimit": "Member limit has been reached, please ", - "upgrade": "upgrade", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "Send invites", - "inviteAlready": "You've already invited this email: {email}", - "inviteSuccess": "Invitation sent successfully", - "description": "Input emails below with commas between them. Charges are based on member count.", - "emails": "Email" - }, - "quickNote": { - "label": "Quick Note", - "quickNotes": "Quick Notes", - "search": "Search Quick Notes", - "collapseFullView": "Collapse full view", - "expandFullView": "Expand full view", - "createFailed": "Failed to create Quick Note", - "quickNotesEmpty": "No Quick Notes", - "emptyNote": "Empty note", - "deleteNotePrompt": "The selected note will be deleted permanently. Are you sure you want to delete it?", - "addNote": "New Note", - "noAdditionalText": "No additional text" - }, - "subscribe": { - "upgradePlanTitle": "Compare & select plan", - "yearly": "Yearly", - "save": "Save {discount}%", - "monthly": "Monthly", - "priceIn": "Price in ", - "free": "Free", - "pro": "Pro", - "freeDescription": "For individuals up to 2 members to organize everything", - "proDescription": "For small teams to manage projects and team knowledge", - "proDuration": { - "monthly": "per member per month\nbilled monthly", - "yearly": "per member per month\nbilled annually" - }, - "cancel": "Downgrade", - "changePlan": "Upgrade to Pro Plan", - "everythingInFree": "Everything in Free +", - "currentPlan": "Current", - "freeDuration": "forever", - "freePoints": { - "first": "1 collaborative workspace up to 2 members", - "second": "Unlimited pages & blocks", - "three": "5 GB storage", - "four": "Intelligent search", - "five": "20 AI responses", - "six": "Mobile app", - "seven": "Real-time collaboration" - }, - "proPoints": { - "first": "Unlimited storage", - "second": "Up to 10 workspace members", - "three": "Unlimited AI responses", - "four": "Unlimited file uploads", - "five": "Custom namespace" - }, - "cancelPlan": { - "title": "Sorry to see you go", - "success": "Your subscription has been canceled successfully", - "description": "We're sorry to see you go. We'd love to hear your feedback to help us improve AppFlowy. Please take a moment to answer a few questions.", - "commonOther": "Other", - "otherHint": "Write your answer here", - "questionOne": { - "question": "What prompted you to cancel your AppFlowy Pro subscription?", - "answerOne": "Cost too high", - "answerTwo": "Features did not meet expectations", - "answerThree": "Found a better alternative", - "answerFour": "Did not use it enough to justify the expense", - "answerFive": "Service issue or technical difficulties" - }, - "questionTwo": { - "question": "How likely are you to consider re-subscribing to AppFlowy Pro in the future?", - "answerOne": "Very likely", - "answerTwo": "Somewhat likely", - "answerThree": "Not sure", - "answerFour": "Unlikely", - "answerFive": "Very unlikely" - }, - "questionThree": { - "question": "Which Pro feature did you value the most during your subscription?", - "answerOne": "Multi-user collaboration", - "answerTwo": "Longer time version history", - "answerThree": "Unlimited AI responses", - "answerFour": "Access to local AI models" - }, - "questionFour": { - "question": "How would you describe your overall experience with AppFlowy?", - "answerOne": "Great", - "answerTwo": "Good", - "answerThree": "Average", - "answerFour": "Below average", - "answerFive": "Unsatisfied" - } - } - }, - "ai": { - "contentPolicyViolation": "Image generation failed due to sensitive content. Please rephrase your input and try again", - "textLimitReachedDescription": "Your workspace has run out of free AI responses. Upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", - "imageLimitReachedDescription": "You've used up your free AI image quota. Please upgrade to the Pro Plan or purchase an AI add-on to unlock unlimited responses", - "limitReachedAction": { - "textDescription": "Your workspace has run out of free AI responses. To get more responses, please", - "imageDescription": "You've used up your free AI image quota. Please", - "upgrade": "upgrade", - "toThe": "to the", - "proPlan": "Pro Plan", - "orPurchaseAn": "or purchase an", - "aiAddon": "AI add-on" - }, - "editing": "Editing", - "analyzing": "Analyzing", - "continueWritingEmptyDocumentTitle": "Continue writing error", - "continueWritingEmptyDocumentDescription": "We are having trouble expanding on the content in your document. Write a small intro and we can take it from there!", - "more": "More" - }, - "autoUpdate": { - "criticalUpdateTitle": "Update required to continue", - "criticalUpdateDescription": "We've made improvements to enhance your experience! Please update from {currentVersion} to {newVersion} to keep using the app.", - "criticalUpdateButton": "Update", - "bannerUpdateTitle": "New Version Available!", - "bannerUpdateDescription": "Get the latest features and fixes. Click \"Update\" to install now", - "bannerUpdateButton": "Update", - "settingsUpdateTitle": "New Version ({newVersion}) Available!", - "settingsUpdateDescription": "Current version: {currentVersion} (Official build) → {newVersion}", - "settingsUpdateButton": "Update", - "settingsUpdateWhatsNew": "What's new" - }, - "lockPage": { - "lockPage": "Locked", - "reLockPage": "Re-lock", - "lockTooltip": "Page locked to prevent accidental editing. Click to unlock.", - "pageLockedToast": "Page locked. Editing is disabled until someone unlocks it.", - "lockedOperationTooltip": "Page locked to prevent accidental editing." - }, - "suggestion": { - "accept": "Accept", - "keep": "Keep", - "discard": "Discard", - "close": "Close", - "tryAgain": "Try again", - "rewrite": "Rewrite", - "insertBelow": "Insert below" + "quicklySwitch": "Quickly switch to the next space" } } \ No newline at end of file diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 5f947ea015..6177cd5377 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -5,7 +5,7 @@ "welcomeTo": "Bienvenido a", "githubStarText": "Favorito en GitHub", "subscribeNewsletterText": "Suscribir al boletín", - "letsGoButtonText": "Inicio rápido", + "letsGoButtonText": "Vamos", "title": "Título", "youCanAlso": "Tú también puedes", "and": "y", @@ -19,24 +19,23 @@ "openMenuTooltip": "Haga clic para abrir el menú" }, "signUp": { - "buttonText": "Registro", - "title": "Registro en @:appName", + "buttonText": "Registrar", + "title": "Registrar en @:appName", "getStartedText": "Empezar", "emptyPasswordError": "La contraseña no puede estar en blanco", - "repeatPasswordEmptyError": "La contraseña repetida no puede estar vacía", + "repeatPasswordEmptyError": "La contraseña no puede estar en blanco", "unmatchedPasswordError": "Las contraseñas no coinciden", - "alreadyHaveAnAccount": "¿Ya posee una cuenta?", - "emailHint": "Correo electrónico", + "alreadyHaveAnAccount": "¿Posee credenciales?", + "emailHint": "Correo", "passwordHint": "Contraseña", "repeatPasswordHint": "Repetir contraseña", - "signUpWith": "Registro con:" + "signUpWith": "Registrarte con:" }, "signIn": { "loginTitle": "Ingresa a @:appName", "loginButtonText": "Ingresar", "loginStartWithAnonymous": "Comience una sesión anónima", "continueAnonymousUser": "Continuar con una sesión anónima", - "anonymous": "Anónimo", "buttonText": "Ingresar", "signingInText": "Iniciando sesión...", "forgotPassword": "¿Olvidó su contraseña?", @@ -51,8 +50,6 @@ "signInWithGoogle": "Iniciar sesión con Google", "signInWithGithub": "Iniciar sesión con Github", "signInWithDiscord": "Iniciar sesión con Discord", - "signInWithApple": "Continuar con Apple", - "continueAnotherWay": "Continuar por otro camino", "signUpWithGoogle": "Registrarse con Google", "signUpWithGithub": "Registrarse con Github", "signUpWithDiscord": "Registrarse con Discord", @@ -71,12 +68,8 @@ }, "workspace": { "chooseWorkspace": "Elige tu espacio de trabajo", - "defaultName": "Mi espacio de trabajo", "create": "Crear espacio de trabajo", - "new": "Nuevo espacio de trabajo", - "learnMore": "Más información", "reset": "Restablecer espacio de trabajo", - "renameWorkspace": "Cambiar el nombre del espacio de trabajo", "resetWorkspacePrompt": "Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.", "hint": "Espacio de trabajo", "notFoundError": "Espacio de trabajo no encontrado", @@ -112,9 +105,7 @@ "html": "HTML", "clipboard": "Copiar al portapapeles", "csv": "CSV", - "copyLink": "Copiar enlace", - "publish": "Publicar", - "publishTab": "Publicar" + "copyLink": "Copiar enlace" }, "moreAction": { "small": "pequeño", @@ -122,17 +113,15 @@ "large": "grande", "fontSize": "Tamaño de fuente", "import": "Importar", - "moreOptions": "Más opciones", + "moreOptions": "Mas opciones", "wordCount": "El recuento de palabras: {}", "charCount": "Número de caracteres : {}", "createdAt": "Creado: {}", "deleteView": "Borrar", - "duplicateView": "Duplicar", - "createdAtLabel": "Creado: ", - "syncedAtLabel": "Sincronizado: " + "duplicateView": "Duplicar" }, "importPanel": { - "textAndMarkdown": "Texto y Markdown", + "textAndMarkdown": "Texto y descuento", "documentFromV010": "Documento de v0.1.0", "databaseFromV010": "Base de datos desde v0.1.0", "csv": "CSV", @@ -147,8 +136,7 @@ "openNewTab": "Abrir en una pestaña nueva", "moveTo": "Mover a", "addToFavorites": "Añadira los favoritos", - "copyLink": "Copiar Enlace", - "move": "Mover" + "copyLink": "Copiar Enlace" }, "blankPageTitle": "Página en blanco", "newPageText": "Nueva página", @@ -156,36 +144,9 @@ "newGridText": "Nueva patrón", "newCalendarText": "Nuevo calendario", "newBoardText": "Nuevo tablero", - "chat": { - "newChat": "Chat de IA", - "relatedQuestion": "Relacionado", - "serverUnavailable": "Servicio temporalmente no disponible. Por favor, inténtelo de nuevo más tarde.", - "aiServerUnavailable": "🌈 ¡Uh-oh! 🌈. Un unicornio se comió nuestra respuesta. ¡Por favor, intenta de nuevo!", - "retry": "Rever", - "clickToRetry": "Haga clic para volver a intentarlo", - "regenerateAnswer": "Regenerar", - "question1": "Cómo utilizar Kanban para gestionar tareas", - "question2": "Explica el método GTD", - "question3": "¿Por qué usar Rust?", - "aiMistakePrompt": "La IA puede cometer errores. Consulta información importante.", - "referenceSource": { - "one": "Se encontró {count} fuente", - "other": "Se encontraron {count} fuentes" - }, - "regenerate": "Intentar otra vez", - "addToNewPage": "Crear nueva página", - "changeFormat": { - "textOnly": "Texto", - "text": "Párrafo" - }, - "selectBanner": { - "saveButton": "Añadir …" - } - }, "trash": { "text": "Papelera", "restoreAll": "Recuperar todo", - "restore": "Restaurar", "deleteAll": "Eliminar todo", "pageHeader": { "fileName": "Nombre de archivo", @@ -218,21 +179,20 @@ "questionBubble": { "shortcuts": "Atajos", "whatsNew": "¿Qué hay de nuevo?", + "help": "Ayuda y Soporte", "markdown": "Reducción", "debug": { "name": "Información de depuración", "success": "¡Información copiada!", "fail": "No fue posible copiar la información" }, - "feedback": "Comentario", - "help": "Ayuda y Soporte" + "feedback": "Comentario" }, "menuAppHeader": { "moreButtonToolTip": "Eliminar, renombrar y más...", "addPageTooltip": "Inserta una página", "defaultNewPageName": "Sin Título", - "renameDialog": "Renombrar", - "pageNameSuffix": "Copiar" + "renameDialog": "Renombrar" }, "noPagesInside": "No hay páginas dentro", "toolbar": { @@ -262,8 +222,7 @@ "dragRow": "Pulsación larga para reordenar la fila", "viewDataBase": "Ver base de datos", "referencePage": "Se hace referencia a este {nombre}", - "addBlockBelow": "Añadir un bloque a continuación", - "aiGenerate": "Generar" + "addBlockBelow": "Añadir un bloque a continuación" }, "sideBar": { "closeSidebar": "Cerrar panel lateral", @@ -279,19 +238,7 @@ "addAPage": "Añadir una página", "addAPageToPrivate": "Agregar una página al espacio privado", "addAPageToWorkspace": "Agregar una página al espacio de trabajo", - "recent": "Reciente", - "today": "Hoy", - "thisWeek": "Esta semana", - "others": "Otros favoritos", - "justNow": "En este momento", - "lastViewed": "Visto por última vez", - "emptyRecent": "Sin documentos recientes", - "favoriteSpace": "Favoritos", - "RecentSpace": "Reciente", - "Spaces": "Espacios", - "aiImageResponseLimit": "Se ha quedado sin respuestas de imágenes de IA.\n\nVaya a Configuración -> Plan -> Haga clic en AI Max para obtener más respuestas de imágenes de IA", - "purchaseStorageSpace": "Comprar espacio de almacenamiento", - "purchaseAIResponse": "Compra " + "recent": "Reciente" }, "notifications": { "export": { @@ -325,19 +272,16 @@ "upload": "Subir", "edit": "Editar", "delete": "Borrar", - "copy": "Copiar", "duplicate": "Duplicar", "putback": "Volver", "update": "Actualizar", "share": "Compartir", "removeFromFavorites": "Quitar de favoritos", - "removeFromRecent": "Eliminar de los recientes", "addToFavorites": "Añadir a favoritos", "rename": "Renombrar", "helpCenter": "Centro de ayuda", "add": "Añadir", "yes": "Si", - "no": "No", "clear": "Limpiar", "remove": "Eliminar", "dontRemove": "no quitar", @@ -349,15 +293,7 @@ "back": "Atrás", "signInGoogle": "Inicia sesión con Google", "signInGithub": "Iniciar sesión con Github", - "signInDiscord": "Iniciar sesión con discordia", - "more": "Más", - "create": "Crear", - "close": "Cerca", - "next": "Próximo", - "previous": "Anterior", - "submit": "Entregar", - "download": "Descargar", - "backToHome": "Volver a Inicio" + "signInDiscord": "Iniciar sesión con discordia" }, "label": { "welcome": "¡Bienvenido!", @@ -381,12 +317,10 @@ }, "settings": { "title": "Ajustes", - "popupMenuItem": { - "settings": "Ajustes" - }, "accountPage": { "menuLabel": "Mi cuenta", "title": "Mi cuenta", + "description": "Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta.", "general": { "title": "Nombre de cuenta e imagen de perfil", "changeProfilePicture": "Cambiar" @@ -397,12 +331,20 @@ "change": "Cambiar email" } }, + "keys": { + "title": "Claves API de IA", + "openAILabel": "Clave API de OpenAI", + "openAITooltip": "La clave API de OpenAI para usar en los modelos de IA", + "openAIHint": "Ingresa tu clave API de OpenAI", + "stabilityAILabel": "Clave API de Stability", + "stabilityAITooltip": "La clave API de Stability que se utilizará en los modelos de IA", + "stabilityAIHint": "Ingresa tu clave API de Stability" + }, "login": { "title": "Inicio de sesión en la cuenta", "loginLabel": "Inicio de sesión", "logoutLabel": "Cerrar sesión" - }, - "description": "Personaliza tu perfil, administra la seguridad de la cuenta y las claves API de IA, o inicia sesión en tu cuenta." + } }, "menu": { "appearance": "Apariencia", @@ -423,6 +365,11 @@ "cloudServerType": "servidor en la nube", "cloudServerTypeTip": "Tenga en cuenta que es posible que se cierre la sesión de su cuenta actual después de cambiar el servidor en la nube.", "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL de la base de datos", + "cloudSupabaseUrlCanNotBeEmpty": "La URL de supabase no puede estar vacía.", + "cloudSupabaseAnonKey": "Supabase clave anon", + "cloudSupabaseAnonKeyCanNotBeEmpty": "La clave anon no puede estar vacía si la URL de supabase no está vacía", "cloudAppFlowy": "Nube @:appName", "cloudAppFlowySelfHost": "@:appName Cloud autohospedado", "appFlowyCloudUrlCanNotBeEmpty": "La URL de la nube no puede estar vacía", @@ -451,7 +398,8 @@ "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de @:appName e impórtalos a la carpeta de datos actual de @:appName", "importSuccess": "Importó exitosamente la carpeta de datos de @:appName", "importFailed": "Error al importar la carpeta de datos de @:appName", - "importGuide": "Para obtener más detalles, consulte el documento de referencia." + "importGuide": "Para obtener más detalles, consulte el documento de referencia.", + "supabaseSetting": "Ajuste de base superior" }, "notifications": { "enableNotifications": { @@ -603,26 +551,9 @@ "email": "Correo electrónico", "tooltipSelectIcon": "Seleccionar icono", "selectAnIcon": "Seleccione un icono", - "pleaseInputYourOpenAIKey": "por favor ingrese su clave AI", - "clickToLogout": "Haga clic para cerrar la sesión del usuario actual.", - "pleaseInputYourStabilityAIKey": "por favor ingrese su clave de estabilidad AI" - }, - "mobile": { - "personalInfo": "Informacion personal", - "username": "Nombre de usuario", - "usernameEmptyError": "El nombre de usuario no puede estar vacío", - "about": "Acerca de", - "pushNotifications": "Notificaciones ", - "support": "Soporte", - "joinDiscord": "Únete a nosotros en Discord", - "privacyPolicy": "política de privacidad", - "userAgreement": "Acuerdo del Usuario", - "termsAndConditions": "Términos y condiciones", - "userprofileError": "No se pudo cargar el perfil de usuario", - "userprofileErrorDescription": "Intente cerrar sesión y volver a iniciarla para comprobar si el problema persiste.", - "selectLayout": "Seleccionar diseño", - "selectStartingDay": "Seleccione el día de inicio", - "version": "Versión" + "pleaseInputYourOpenAIKey": "por favor ingrese su clave OpenAI", + "pleaseInputYourStabilityAIKey": "por favor ingrese su clave de estabilidad AI", + "clickToLogout": "Haga clic para cerrar la sesión del usuario actual." }, "shortcuts": { "shortcutsLabel": "Atajos", @@ -644,6 +575,23 @@ "textAlignCenter": "Alinear el texto al centro", "textAlignRight": "Alinear el texto a la derecha" } + }, + "mobile": { + "personalInfo": "Informacion personal", + "username": "Nombre de usuario", + "usernameEmptyError": "El nombre de usuario no puede estar vacío", + "about": "Acerca de", + "pushNotifications": "Notificaciones ", + "support": "Soporte", + "joinDiscord": "Únete a nosotros en Discord", + "privacyPolicy": "política de privacidad", + "userAgreement": "Acuerdo del Usuario", + "termsAndConditions": "Términos y condiciones", + "userprofileError": "No se pudo cargar el perfil de usuario", + "userprofileErrorDescription": "Intente cerrar sesión y volver a iniciarla para comprobar si el problema persiste.", + "selectLayout": "Seleccionar diseño", + "selectStartingDay": "Seleccione el día de inicio", + "version": "Versión" } }, "grid": { @@ -866,7 +814,8 @@ "url": { "launch": "Abrir en el navegador", "copy": "Copiar URL", - "textFieldHint": "Introduce una URL" + "textFieldHint": "Introduce una URL", + "copiedNotification": "¡Copiado al portapapeles!" }, "relation": { "relatedDatabasePlaceLabel": "Base de datos relacionada", @@ -927,22 +876,23 @@ "referencedGrid": "Cuadrícula referenciada", "referencedCalendar": "Calendario referenciado", "referencedDocument": "Documento referenciado", - "autoGeneratorMenuItemName": "Escritor de AI", - "autoGeneratorTitleName": "AI: Pídele a AI que escriba cualquier cosa...", + "autoGeneratorMenuItemName": "Escritor de OpenAI", + "autoGeneratorTitleName": "OpenAI: Pídele a AI que escriba cualquier cosa...", "autoGeneratorLearnMore": "Aprende más", "autoGeneratorGenerate": "Generar", - "autoGeneratorHintText": "Pregúntale a AI...", - "autoGeneratorCantGetOpenAIKey": "No puedo obtener la clave de AI", + "autoGeneratorHintText": "Pregúntale a OpenAI...", + "autoGeneratorCantGetOpenAIKey": "No puedo obtener la clave de OpenAI", "autoGeneratorRewrite": "Volver a escribir", "smartEdit": "Asistentes de IA", + "openAI": "IA abierta", "smartEditFixSpelling": "Corregir ortografía", "warning": "⚠️ Las respuestas de la IA pueden ser inexactas o engañosas.", "smartEditSummarize": "Resumir", "smartEditImproveWriting": "Mejorar la escritura", "smartEditMakeLonger": "hacer más largo", - "smartEditCouldNotFetchResult": "No se pudo obtener el resultado de AI", - "smartEditCouldNotFetchKey": "No se pudo obtener la clave de AI", - "smartEditDisabled": "Conectar AI en Configuración", + "smartEditCouldNotFetchResult": "No se pudo obtener el resultado de OpenAI", + "smartEditCouldNotFetchKey": "No se pudo obtener la clave de OpenAI", + "smartEditDisabled": "Conectar OpenAI en Configuración", "discardResponse": "¿Quieres descartar las respuestas de IA?", "createInlineMathEquation": "Crear ecuación", "fonts": "Tipo de letra", @@ -1001,8 +951,8 @@ "depth": "Profundidad" }, "image": { - "addAnImage": "Añadir una imagen", "copiedToPasteBoard": "El enlace de la imagen se ha copiado en el portapapeles.", + "addAnImage": "Añadir una imagen", "imageUploadFailed": "Error al subir la imagen", "errorCode": "Código de error" }, @@ -1036,8 +986,7 @@ "newDatabase": "Nueva base de datos", "linkToDatabase": "Enlace a la base de datos" }, - "date": "Fecha", - "openAI": "IA abierta" + "date": "Fecha" }, "outlineBlock": { "placeholder": "Tabla de contenidos" @@ -1059,8 +1008,8 @@ "placeholder": "Introduce la URL de la imagen" }, "ai": { - "label": "Generar imagen desde AI", - "placeholder": "Ingrese el prompt para que AI genere una imagen" + "label": "Generar imagen desde OpenAI", + "placeholder": "Ingrese el prompt para que OpenAI genere una imagen" }, "stability_ai": { "label": "Generar imagen desde Stability AI", @@ -1082,15 +1031,15 @@ "label": "Desempaquetar" }, "searchForAnImage": "Buscar una imagen", - "pleaseInputYourOpenAIKey": "ingresa tu clave AI en la página de Configuración", + "pleaseInputYourOpenAIKey": "ingresa tu clave OpenAI en la página de Configuración", + "pleaseInputYourStabilityAIKey": "ingresa tu clave de Stability AI en la página de configuración", "saveImageToGallery": "Guardar imagen", "failedToAddImageToGallery": "No se pudo agregar la imagen a la galería", "successToAddImageToGallery": "Imagen agregada a la galería con éxito", "unableToLoadImage": "No se puede cargar la imagen", "maximumImageSize": "El tamaño máximo de imagen es de 10 MB", "uploadImageErrorImageSizeTooBig": "El tamaño de la imagen debe ser inferior a 10 MB.", - "imageIsUploading": "La imagen se está subiendo", - "pleaseInputYourStabilityAIKey": "ingresa tu clave de Stability AI en la página de configuración" + "imageIsUploading": "La imagen se está subiendo" }, "codeBlock": { "language": { @@ -1587,4 +1536,4 @@ "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", "fromTrashHint": "De la papelera" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 2e52231f7c..e7029a0d38 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -99,14 +99,14 @@ "questionBubble": { "shortcuts": "Lasterbideak", "whatsNew": "Ze berri?", + "help": "Laguntza", "markdown": "Markdown", "debug": { "name": "Debug informazioa", "success": "Debug informazioa kopiatu da!", "fail": "Ezin izan da debug informazioa kopiatu" }, - "feedback": "Iritzia", - "help": "Laguntza" + "feedback": "Iritzia" }, "menuAppHeader": { "addPageTooltip": "Gehitu orri bat", @@ -206,7 +206,8 @@ "language": "Hizkuntza", "user": "Erabiltzailea", "files": "Fitxategiak", - "open": "Ezarpenak ireki" + "open": "Ezarpenak ireki", + "supabaseSetting": "Supabase ezarpena" }, "appearance": { "fontFamily": { @@ -272,7 +273,7 @@ "user": { "name": "Izena", "selectAnIcon": "Hautatu ikono bat", - "pleaseInputYourOpenAIKey": "mesedez sartu zure AI gakoa" + "pleaseInputYourOpenAIKey": "mesedez sartu zure OpenAI gakoa" } }, "grid": { @@ -429,23 +430,23 @@ "referencedBoard": "Erreferentziazko Batzordea", "referencedGrid": "Erreferentziazko Sarea", "referencedCalendar": "Erreferentziazko Egutegia", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Eskatu AIri edozer idazteko...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Eskatu AIri edozer idazteko...", "autoGeneratorLearnMore": "Gehiago ikasi", "autoGeneratorGenerate": "Sortu", - "autoGeneratorHintText": "Galdetu AI...", - "autoGeneratorCantGetOpenAIKey": "Ezin da lortu AI gakoa", + "autoGeneratorHintText": "Galdetu OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Ezin da lortu OpenAI gakoa", "autoGeneratorRewrite": "Berridatzi", "smartEdit": "AI Laguntzaileak", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Ortografia konpondu", "warning": "⚠️ AI erantzunak okerrak edo engainagarriak izan daitezke.", "smartEditSummarize": "Laburtu", "smartEditImproveWriting": "Hobetu idazkera", "smartEditMakeLonger": "Luzatu", - "smartEditCouldNotFetchResult": "Ezin izan da emaitzarik eskuratu AI-tik", - "smartEditCouldNotFetchKey": "Ezin izan da AI gakoa eskuratu", - "smartEditDisabled": "Konektatu AI Ezarpenetan", + "smartEditCouldNotFetchResult": "Ezin izan da emaitzarik eskuratu OpenAI-tik", + "smartEditCouldNotFetchKey": "Ezin izan da OpenAI gakoa eskuratu", + "smartEditDisabled": "Konektatu OpenAI Ezarpenetan", "discardResponse": "AI erantzunak baztertu nahi dituzu?", "createInlineMathEquation": "Sortu ekuazioa", "toggleList": "Aldatu zerrenda", @@ -600,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index cc93c17d64..147bca02b6 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "من", "welcomeText": "به @:appName خوش آمدید", - "welcomeTo": "خوش آمدید به", "githubStarText": "به گیت‌هاب ما ستاره دهید", "subscribeNewsletterText": "اشتراک در خبرنامه", "letsGoButtonText": "شروع کنید", "title": "عنوان", "youCanAlso": "همچنین می‌توانید", "and": "و", - "failedToOpenUrl": "خطا در بازکردن نشانی وب: {}", "blockActions": { "addBelowTooltip": "برای افزودن در زیر کلیک کنید", "addAboveCmd": "Alt+click", @@ -34,47 +32,19 @@ "signIn": { "loginTitle": "ورود به @:appName", "loginButtonText": "ورود", - "loginStartWithAnonymous": "ادامه دادن با یک جلسه ناشناس", "continueAnonymousUser": "ادامه دادن به صورت کاربر مهمان", - "anonymous": "ناشناس", "buttonText": "ورود", - "signingInText": "در حال ورود...", "forgotPassword": "رمز عبور را فراموش کرده اید؟", "emailHint": "ایمیل", "passwordHint": "رمز عبور", "dontHaveAnAccount": "آیا حساب کاربری ندارید؟", - "createAccount": "ساخت حساب کاربری", "repeatPasswordEmptyError": "تکرار رمز عبور نمی‌تواند خالی باشد", "unmatchedPasswordError": "تکرار رمز عبور مشابه رمز عبور نیست", - "syncPromptMessage": "همگام سازی داده ها ممکن است کمی طول بکشد. لطفا این صفحه را نبندید", - "or": "یا", - "signInWithGoogle": "ادامه دادن با گوگل", - "signInWithGithub": "ادامه دادن با گیتهاب", - "signInWithDiscord": "ادامه دادن با دیسکورد", - "signInWithApple": "ادامه دادن با اپل", - "continueAnotherWay": "ادامه دادن از طریق دیگر", - "signUpWithGoogle": "ثبت نام با گوگل", - "signUpWithGithub": "ثبت نام با گیتهاب", - "signUpWithDiscord": "ثبت نام با دیسکورد", "signInWith": "ثبت نام با:", - "signInWithEmail": "ادامه دادن با ایمیل", - "signInWithMagicLink": "ادامه", - "pleaseInputYourEmail": "لطفا آدرس ایمیل خود را وارد کنید", - "settings": "تنظیمات", - "invalidEmail": "لطفا یک آدرس ایمیل معتبر وارد کنید", - "alreadyHaveAnAccount": "حساب کاربری دارید؟", - "logIn": "ورود", - "generalError": "مشکلی پیش آمد. لطفاً بعداً دوباره امتحان کنید", "loginAsGuestButtonText": "شروع کنید" }, "workspace": { - "chooseWorkspace": "فضای کار خود را انتخاب کنید", - "defaultName": "فضای کار من", "create": "ایجاد فضای کار", - "new": "فضای کار جدید", - "learnMore": "بیشتر بدانید", - "renameWorkspace": "حذف فضای کار", - "workspaceNameCannotBeEmpty": "اسم فضای کار نمی‌تواند خالی باشد", "hint": "فضای کار", "notFoundError": "فضای کاری پیدا نشد" }, @@ -139,14 +109,14 @@ "questionBubble": { "shortcuts": "میانبرها", "whatsNew": "تازه‌ترین‌ها", + "help": "پشتیبانی و مستندات", "markdown": "Markdown", "debug": { "name": "اطلاعات اشکال‌زدایی", "success": "طلاعات اشکال زدایی در کلیپ بورد کپی شد!", "fail": "نمی توان اطلاعات اشکال زدایی را در کلیپ بورد کپی کرد" }, - "feedback": "بازخورد", - "help": "پشتیبانی و مستندات" + "feedback": "بازخورد" }, "menuAppHeader": { "moreButtonToolTip": "حذف، تغییر نام، و موارد دیگر...", @@ -326,7 +296,7 @@ "user": { "name": "نام", "selectAnIcon": "انتخاب یک آیکون", - "pleaseInputYourOpenAIKey": "لطفا کلید AI خود را وارد کنید", + "pleaseInputYourOpenAIKey": "لطفا کلید OpenAI خود را وارد کنید", "clickToLogout": "برای خروج از کاربر فعلی کلیک کنید" }, "shortcuts": { @@ -495,22 +465,23 @@ "referencedBoard": "بورد مرجع", "referencedGrid": "شبکه‌نمایش مرجع", "referencedCalendar": "تقویم مرجع", - "autoGeneratorMenuItemName": "AI نویسنده", + "autoGeneratorMenuItemName": "OpenAI نویسنده", "autoGeneratorTitleName": "از هوش مصنوعی بخواهید هر چیزی بنویسد...", "autoGeneratorLearnMore": "بیشتر بدانید", "autoGeneratorGenerate": "بنویس", - "autoGeneratorHintText": "از AI بپرسید ...", - "autoGeneratorCantGetOpenAIKey": "کلید AI را نمی توان دریافت کرد", + "autoGeneratorHintText": "از OpenAI بپرسید ...", + "autoGeneratorCantGetOpenAIKey": "کلید OpenAI را نمی توان دریافت کرد", "autoGeneratorRewrite": "بازنویس", "smartEdit": "دستیاران هوشمند", + "openAI": "OpenAI", "smartEditFixSpelling": "اصلاح نگارش", "warning": "⚠️ پاسخ‌های هوش مصنوعی می‌توانند نادرست یا گمراه‌کننده باشند", "smartEditSummarize": "خلاصه‌نویسی", "smartEditImproveWriting": "بهبود نگارش", "smartEditMakeLonger": "به نوشته اضافه کن", - "smartEditCouldNotFetchResult": "نتیجه‌ای از AI گرفته نشد", - "smartEditCouldNotFetchKey": "کلید AI واکشی نشد", - "smartEditDisabled": "به AI در تنظیمات وصل شوید", + "smartEditCouldNotFetchResult": "نتیجه‌ای از OpenAI گرفته نشد", + "smartEditCouldNotFetchKey": "کلید OpenAI واکشی نشد", + "smartEditDisabled": "به OpenAI در تنظیمات وصل شوید", "discardResponse": "آیا می خواهید پاسخ های هوش مصنوعی را حذف کنید؟", "createInlineMathEquation": "ایجاد معادله", "toggleList": "Toggle لیست", @@ -562,8 +533,7 @@ }, "outline": { "addHeadingToCreateOutline": "برای ایجاد فهرست مطالب سر‌فصل‌ها را وارد کنید" - }, - "openAI": "AI" + } }, "textBlock": { "placeholder": "برای دستورات '/' را تایپ کنید" @@ -704,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 589d2dfe18..f5bbb4b2db 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -9,7 +9,6 @@ "title": "Titre", "youCanAlso": "Vous pouvez aussi", "and": "et", - "failedToOpenUrl": "Échec de l'ouverture de l'URL : {}", "blockActions": { "addBelowTooltip": "Cliquez pour ajouter ci-dessous", "addAboveCmd": "Alt+clic", @@ -36,39 +35,17 @@ "loginButtonText": "Connexion", "loginStartWithAnonymous": "Lancer avec une session anonyme", "continueAnonymousUser": "Continuer avec une session anonyme", - "anonymous": "Anonyme", "buttonText": "Se connecter", "signingInText": "Connexion en cours...", "forgotPassword": "Mot de passe oublié?", "emailHint": "Courriel", "passwordHint": "Mot de passe", "dontHaveAnAccount": "Vous n'avez pas encore de compte?", - "createAccount": "Créer un compte", "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", "or": "OU", - "signInWithGoogle": "Continuer avec Google", - "signInWithGithub": "Continuer avec Github", - "signInWithDiscord": "Continuer avec Discord", - "signInWithApple": "Se connecter avec Apple", - "continueAnotherWay": "Continuer avec une autre méthode", - "signUpWithGoogle": "S'inscrire avec Google", - "signUpWithGithub": "S'inscrire avec Github", - "signUpWithDiscord": "S'inscrire avec Discord", "signInWith": "Se connecter avec:", - "signInWithEmail": "Se connecter avec e-mail", - "signInWithMagicLink": "Continuer", - "signUpWithMagicLink": "S'inscrire avec un lien spécial", - "pleaseInputYourEmail": "Veuillez entrer votre adresse e-mail", - "settings": "Paramètres", - "magicLinkSent": "Lien spécial envoyé à votre email, veuillez vérifier votre boîte de réception", - "invalidEmail": "S'il vous plaît, mettez une adresse email valide", - "alreadyHaveAnAccount": "Déjà un compte ?", - "logIn": "Connexion", - "generalError": "Une erreur s'est produite. Veuillez réessayer plus tard", - "limitRateError": "Pour des raisons de sécurité, vous ne pouvez demander un lien spécial que toutes les 60 secondes", - "magicLinkSentDescription": "Un lien spécial vous a été envoyé par e-mail. Cliquez sur le lien pour vous connecter. Le lien expirera dans 5 minutes.", "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", @@ -76,14 +53,8 @@ }, "workspace": { "chooseWorkspace": "Choisissez votre espace de travail", - "defaultName": "Mon espace de travail", "create": "Créer un espace de travail", - "new": "Nouveau espace de travail", - "importFromNotion": "Importer depuis Notion", - "learnMore": "En savoir plus", "reset": "Réinitialiser l'espace de travail", - "renameWorkspace": "Renommer l'espace de travail", - "workspaceNameCannotBeEmpty": "Le nom de l'espace de travail ne peut être vide", "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", @@ -93,42 +64,14 @@ "reportIssueOnGithub": "Signaler un bug sur Github", "exportLogFiles": "Exporter les logs", "reachOut": "Contactez-nous sur Discord" - }, - "menuTitle": "Espaces de travail", - "deleteWorkspaceHintText": "Êtes-vous sûr de vouloir supprimer l'espace de travail ? Cette action ne peut pas être annulée.", - "createSuccess": "Espace de travail créé avec succès", - "createFailed": "Échec de la création de l'espace de travail", - "createLimitExceeded": "Vous avez atteint la limite maximale d'espace de travail autorisée pour votre compte. Si vous avez besoin d'espaces de travail supplémentaires pour continuer votre travail, veuillez en faire la demande sur Github.", - "deleteSuccess": "Espace de travail supprimé avec succès", - "deleteFailed": "Échec de la suppression de l'espace de travail", - "openSuccess": "Ouverture de l'espace de travail réussie", - "openFailed": "Échec de l'ouverture de l'espace de travail", - "renameSuccess": "Espace de travail renommé avec succès", - "renameFailed": "Échec du renommage de l'espace de travail", - "updateIconSuccess": "L'icône de l'espace de travail a été mise à jour avec succès", - "updateIconFailed": "La mise a jour de l'icône de l'espace de travail a échoué", - "cannotDeleteTheOnlyWorkspace": "Impossible de supprimer le seul espace de travail", - "fetchWorkspacesFailed": "Échec de la récupération des espaces de travail", - "leaveCurrentWorkspace": "Quitter l'espace de travail", - "leaveCurrentWorkspacePrompt": "Êtes-vous sûr de vouloir quitter l'espace de travail actuel ?" + } }, "shareAction": { "buttonText": "Partager", "workInProgress": "Bientôt disponible", "markdown": "Markdown", - "html": "HTML", - "clipboard": "Copier dans le presse-papier", "csv": "CSV", - "copyLink": "Copier le lien", - "publishToTheWeb": "Publier sur le Web", - "publishToTheWebHint": "Créer un site Internet avec AppFlowy", - "publish": "Partager", - "unPublish": "Annuler la publication", - "visitSite": "Visitez le site", - "exportAsTab": "Exporter en tant que", - "publishTab": "Partager", - "shareTab": "Partager", - "publishOnAppFlowy": "Partager sur AppFlowy" + "copyLink": "Copier le lien" }, "moreAction": { "small": "petit", @@ -196,14 +139,14 @@ "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", + "help": "Aide et Support Technique", "markdown": "Réduction", "debug": { "name": "Infos du système", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour", - "help": "Aide et Support Technique" + "feedback": "Retour" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", @@ -335,6 +278,11 @@ "cloudServerType": "Serveur cloud", "cloudServerTypeTip": "Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.", "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL de Supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", + "cloudSupabaseAnonKey": "Clé anonyme Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", "cloudAppFlowy": "@:appName Cloud Bêta", "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", @@ -363,7 +311,8 @@ "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", "importSuccess": "Importation réussie du dossier de données @:appName", "importFailed": "L'importation du dossier de données @:appName a échoué", - "importGuide": "Pour plus de détails, veuillez consulter le document référencé" + "importGuide": "Pour plus de détails, veuillez consulter le document référencé", + "supabaseSetting": "Paramètre Supabase" }, "notifications": { "enableNotifications": { @@ -479,9 +428,20 @@ "email": "Courriel", "tooltipSelectIcon": "Sélectionner l'icône", "selectAnIcon": "Sélectionnez une icône", - "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé AI", - "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel", - "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI" + "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI", + "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI", + "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel" + }, + "shortcuts": { + "shortcutsLabel": "Raccourcis", + "command": "Commande", + "keyBinding": "Racourcis clavier", + "addNewCommand": "Ajouter une Nouvelle Commande", + "updateShortcutStep": "Appuyez sur la combinaison de touches souhaitée et appuyez sur ENTER", + "shortcutIsAlreadyUsed": "Ce raccourci est déjà utilisé pour : {conflict}", + "resetToDefault": "Réinitialiser les raccourcis clavier par défaut", + "couldNotLoadErrorMsg": "Impossible de charger les raccourcis, réessayez", + "couldNotSaveErrorMsg": "Impossible d'enregistrer les raccourcis. Réessayez" }, "mobile": { "personalInfo": "Informations personnelles", @@ -499,17 +459,6 @@ "selectLayout": "Sélectionner la mise en page", "selectStartingDay": "Sélectionnez le jour de début", "version": "Version" - }, - "shortcuts": { - "shortcutsLabel": "Raccourcis", - "command": "Commande", - "keyBinding": "Racourcis clavier", - "addNewCommand": "Ajouter une Nouvelle Commande", - "updateShortcutStep": "Appuyez sur la combinaison de touches souhaitée et appuyez sur ENTER", - "shortcutIsAlreadyUsed": "Ce raccourci est déjà utilisé pour : {conflict}", - "resetToDefault": "Réinitialiser les raccourcis clavier par défaut", - "couldNotLoadErrorMsg": "Impossible de charger les raccourcis, réessayez", - "couldNotSaveErrorMsg": "Impossible d'enregistrer les raccourcis. Réessayez" } }, "grid": { @@ -752,23 +701,23 @@ "referencedGrid": "Grille référencée", "referencedCalendar": "Calendrier référencé", "referencedDocument": "Document référencé", - "autoGeneratorMenuItemName": "Rédacteur AI", - "autoGeneratorTitleName": "AI : Demandez à l'IA d'écrire quelque chose...", + "autoGeneratorMenuItemName": "Rédacteur OpenAI", + "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", "autoGeneratorLearnMore": "Apprendre encore plus", "autoGeneratorGenerate": "Générer", - "autoGeneratorHintText": "Demandez à AI...", - "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé AI", + "autoGeneratorHintText": "Demandez à OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI", "autoGeneratorRewrite": "Réécrire", "smartEdit": "Assistants IA", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Corriger l'orthographe", "warning": "⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.", "smartEditSummarize": "Résumer", "smartEditImproveWriting": "Améliorer l'écriture", "smartEditMakeLonger": "Rallonger", - "smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'AI", - "smartEditCouldNotFetchKey": "Impossible de récupérer la clé AI", - "smartEditDisabled": "Connectez AI dans les paramètres", + "smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'OpenAI", + "smartEditCouldNotFetchKey": "Impossible de récupérer la clé OpenAI", + "smartEditDisabled": "Connectez OpenAI dans les paramètres", "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", "fonts": "Polices", @@ -825,8 +774,8 @@ "defaultColor": "Défaut" }, "image": { - "addAnImage": "Ajouter une image", - "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers" + "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers", + "addAnImage": "Ajouter une image" }, "urlPreview": { "copiedToPasteBoard": "Le lien a été copié dans le presse-papier" @@ -875,8 +824,8 @@ "placeholder": "Entrez l'URL de l'image" }, "ai": { - "label": "Générer une image à partir d'AI", - "placeholder": "Veuillez saisir l'invite pour qu'AI génère l'image" + "label": "Générer une image à partir d'OpenAI", + "placeholder": "Veuillez saisir l'invite pour qu'OpenAI génère l'image" }, "stability_ai": { "label": "Générer une image à partir de Stability AI", @@ -897,14 +846,14 @@ "label": "Unsplash" }, "searchForAnImage": "Rechercher une image", - "pleaseInputYourOpenAIKey": "veuillez saisir votre clé AI dans la page Paramètres", + "pleaseInputYourOpenAIKey": "veuillez saisir votre clé OpenAI dans la page Paramètres", + "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres", "saveImageToGallery": "Enregistrer l'image", "failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie", "successToAddImageToGallery": "Image ajoutée à la galerie avec succès", "unableToLoadImage": "Impossible de charger l'image", "maximumImageSize": "La taille d'image maximale est 10Mo", - "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo", - "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres" + "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo" }, "codeBlock": { "language": { @@ -1313,4 +1262,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 989e21f349..e258e9099b 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -36,7 +36,6 @@ "loginButtonText": "Connexion", "loginStartWithAnonymous": "Lancer avec une session anonyme", "continueAnonymousUser": "Continuer avec une session anonyme", - "anonymous": "Anonyme", "buttonText": "Se connecter", "signingInText": "Connexion en cours...", "forgotPassword": "Mot de passe oublié ?", @@ -48,27 +47,12 @@ "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", "or": "OU", - "signInWithGoogle": "Continuer avec Google", - "signInWithGithub": "Continuer avec Github", - "signInWithDiscord": "Continuer avec Discord", - "signInWithApple": "Se connecter via Apple", - "continueAnotherWay": "Continuer via une autre méthode", - "signUpWithGoogle": "S'inscrire avec Google", - "signUpWithGithub": "S'inscrire avec Github", - "signUpWithDiscord": "S'inscrire avec Discord", "signInWith": "Se connecter avec :", "signInWithEmail": "Se connecter via e-mail", - "signInWithMagicLink": "Continuer", - "signUpWithMagicLink": "S'inscrire avec un lien magique", "pleaseInputYourEmail": "Veuillez entrer votre adresse e-mail", - "settings": "Paramètres", "magicLinkSent": "Lien magique envoyé à votre email, veuillez vérifier votre boîte de réception", "invalidEmail": "S'il vous plaît, mettez une adresse email valide", - "alreadyHaveAnAccount": "Déjà un compte ?", "logIn": "Connexion", - "generalError": "Une erreur s'est produite. Veuillez réessayer plus tard", - "limitRateError": "Pour des raisons de sécurité, vous ne pouvez demander un lien magique que toutes les 60 secondes", - "magicLinkSentDescription": "Un lien magique vous a été envoyé par e-mail. Cliquez sur le lien pour vous connecter. Le lien expirera dans 5 minutes.", "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", @@ -77,14 +61,8 @@ }, "workspace": { "chooseWorkspace": "Choisissez votre espace de travail", - "defaultName": "Mon espace de travail", "create": "Créer un espace de travail", - "new": "Nouveau espace de travail", - "importFromNotion": "Importer depuis Notion", - "learnMore": "En savoir plus", "reset": "Réinitialiser l'espace de travail", - "renameWorkspace": "Renommer l'espace de travail", - "workspaceNameCannotBeEmpty": "Le nom de l'espace de travail ne peut être vide", "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", @@ -120,25 +98,7 @@ "html": "HTML", "clipboard": "Copier dans le presse-papier", "csv": "CSV", - "copyLink": "Copier le lien", - "publishToTheWeb": "Publier sur le Web", - "publishToTheWebHint": "Créer un site Web avec AppFlowy", - "publish": "Publier", - "unPublish": "Annuler la publication", - "visitSite": "Visitez le site", - "exportAsTab": "Exporter en tant que", - "publishTab": "Publier", - "shareTab": "Partager", - "publishOnAppFlowy": "Publier sur AppFlowy", - "shareTabTitle": "Inviter à collaborer", - "shareTabDescription": "Pour faciliter la collaboration avec n'importe qui", - "copyLinkSuccess": "Lien copié", - "copyShareLink": "Copier le lien de partage", - "copyLinkFailed": "Impossible de copier le lien dans le presse-papiers", - "copyLinkToBlockSuccess": "Lien de bloc copié dans le presse-papiers", - "copyLinkToBlockFailed": "Impossible de copier le lien du bloc dans le presse-papiers", - "manageAllSites": "Gérer tous les sites", - "updatePathName": "Mettre à jour le nom du chemin" + "copyLink": "Copier le lien" }, "moreAction": { "small": "petit", @@ -151,17 +111,12 @@ "charCount": "Compteur de caractère: {}", "createdAt": "Créé à: {}", "deleteView": "Supprimer", - "duplicateView": "Dupliquer", - "wordCountLabel": "Mots:", - "charCountLabel": "Charactères: ", - "createdAtLabel": "Créé:", - "syncedAtLabel": "Synchronisé" + "duplicateView": "Dupliquer" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", "documentFromV010": "Document de la v0.1.0", "databaseFromV010": "Base de données à partir de la v0.1.0", - "notionZip": "Fichier ZIP exporté depuis Notion", "csv": "CSV", "database": "Base de données" }, @@ -174,11 +129,7 @@ "openNewTab": "Ouvrir dans un nouvel onglet", "moveTo": "Déplacer vers", "addToFavorites": "Ajouter aux Favoris", - "copyLink": "Copier le lien", - "changeIcon": "Changer d'icône", - "collapseAllPages": "Réduire toutes les sous-pages", - "movePageTo": "Déplacer vers", - "move": "Déplacer" + "copyLink": "Copier le lien" }, "blankPageTitle": "Page vierge", "newPageText": "Nouvelle page", @@ -186,52 +137,9 @@ "newGridText": "Nouvelle grille", "newCalendarText": "Nouveau calendrier", "newBoardText": "Nouveau tableau", - "chat": { - "newChat": "Chat IA", - "inputMessageHint": "Demandez à l'IA @:appName", - "inputLocalAIMessageHint": "Demander l'IA locale @:appName", - "unsupportedCloudPrompt": "Cette fonctionnalité n'est disponible que lors de l'utilisation du cloud @:appName", - "relatedQuestion": "Questions Associées", - "serverUnavailable": "Service temporairement indisponible. Veuillez réessayer ultérieurement.", - "aiServerUnavailable": "🌈 Oh-oh ! 🌈. Une licorne a mangé notre réponse. Veuillez réessayer !", - "retry": "Réessayer", - "clickToRetry": "Cliquez pour réessayer", - "regenerateAnswer": "Régénérer", - "question1": "Comment utiliser Kanban pour gérer les tâches", - "question2": "Expliquez la méthode GTD", - "question3": "Pourquoi utiliser Rust", - "question4": "Recette avec ce qu'il y a dans ma cuisine", - "question5": "Créer une illustration pour ma page", - "question6": "Dresser une liste de choses à faire pour ma semaine à venir", - "aiMistakePrompt": "L'IA peut faire des erreurs. Vérifiez les informations importantes.", - "chatWithFilePrompt": "Voulez-vous discuter avec le fichier ?", - "indexFileSuccess": "Indexation du fichier réussie", - "inputActionNoPages": "Aucun résultat de page", - "referenceSource": { - "zero": "0 sources trouvées", - "one": "{count} source trouvée", - "other": "{count} sources trouvées" - }, - "clickToMention": "Cliquez pour mentionner une page", - "uploadFile": "Téléchargez des fichiers PDF, MD ou TXT pour discuter avec", - "questionDetail": "Bonjour {}! Comment puis-je vous aider aujourd'hui?", - "indexingFile": "Indexation {}", - "generatingResponse": "Générer une réponse", - "selectSources": "Sélectionner Sources", - "sourcesLimitReached": "Vous ne pouvez sélectionner que jusqu'à 3 documents de niveau supérieur et leurs enfants", - "sourceUnsupported": "Nous ne prenons pas en charge le chat avec des bases de données pour le moment", - "regenerate": "Réessayer", - "addToPageButton": "Ajouter à la page", - "addToPageTitle": "Ajouter un message à...", - "addToNewPage": "Ajouter à une nouvelle page", - "addToNewPageName": "Messages extraits de \"{}\"", - "addToNewPageSuccessToast": "Message ajouté à", - "openPagePreviewFailedToast": "Échec de l'ouverture de la page" - }, "trash": { "text": "Corbeille", "restoreAll": "Tout restaurer", - "restore": "Restaurer", "deleteAll": "Tout supprimer", "pageHeader": { "fileName": "Nom de fichier", @@ -246,10 +154,6 @@ "title": "Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?", "caption": "Cette action ne peut pas être annulée." }, - "restorePage": { - "title": "Restaurer: {}", - "caption": "Etes-vous sûr de vouloir restaurer cette page ?" - }, "mobile": { "actions": "Actions de la corbeille", "empty": "La corbeille est vide", @@ -262,28 +166,26 @@ "deletePagePrompt": { "text": "Cette page se trouve dans la corbeille", "restore": "Restaurer la page", - "deletePermanent": "Supprimer définitivement", - "deletePermanentDescription": "Etes-vous sûr de vouloir supprimer définitivement cette page ? Cette action est irréversible." + "deletePermanent": "Supprimer définitivement" }, "dialogCreatePageNameHint": "Nom de la page", "questionBubble": { "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", + "help": "Aide et Support", "markdown": "Rédaction", "debug": { "name": "Informations de Débogage", "success": "Informations de débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, - "feedback": "Retour", - "help": "Aide et Support" + "feedback": "Retour" }, "menuAppHeader": { "moreButtonToolTip": "Supprimer, renommer et plus...", "addPageTooltip": "Ajoutez rapidement une page à l'intérieur", "defaultNewPageName": "Sans-titre", - "renameDialog": "Renommer", - "pageNameSuffix": "Copier" + "renameDialog": "Renommer" }, "noPagesInside": "Aucune page à l'intérieur", "toolbar": { @@ -295,7 +197,7 @@ "strike": "Barré", "numList": "Liste numérotée", "bulletList": "Liste à puces", - "checkList": "To-Do list", + "checkList": "To-Do List", "inlineCode": "Code en ligne", "quote": "Citation", "header": "En-tête", @@ -313,13 +215,11 @@ "dragRow": "Appuyez longuement pour réorganiser la ligne", "viewDataBase": "Voir la base de données", "referencePage": "Ce {nom} est référencé", - "addBlockBelow": "Ajouter un bloc ci-dessous", - "aiGenerate": "Générer" + "addBlockBelow": "Ajouter un bloc ci-dessous" }, "sideBar": { "closeSidebar": "Fermer le menu latéral", "openSidebar": "Ouvrir le menu latéral", - "expandSidebar": "Agrandir la page", "personal": "Personnel", "private": "Privé", "workspace": "Espace de travail", @@ -332,40 +232,6 @@ "addAPageToPrivate": "Ajouter une page à l'espace privé", "addAPageToWorkspace": "Ajouter une page à l'espace de travail", "recent": "Récent", - "today": "Aujourd'hui", - "thisWeek": "Cette semaine", - "others": "Favoris précédents", - "earlier": "Plus tôt", - "justNow": "tout à l' heure", - "minutesAgo": "Il y a {count} minutes", - "lastViewed": "Dernière consultation", - "favoriteAt": "Favoris", - "emptyRecent": "Aucun document récent", - "emptyRecentDescription": "Quand vous consultez des documents, ils apparaîtront ici pour les retrouver facilement", - "emptyFavorite": "Aucun document favori", - "emptyFavoriteDescription": "Commencez à explorer et marquez les documents comme favoris. Ils seront répertoriés ici pour un accès rapide !", - "removePageFromRecent": "Supprimer cette page des Récents ?", - "removeSuccess": "Supprimé avec succès", - "favoriteSpace": "Favoris", - "RecentSpace": "Récent", - "Spaces": "Espaces", - "upgradeToPro": "Passer à Pro", - "upgradeToAIMax": "Débloquez une l'IA illimitée", - "storageLimitDialogTitle": "Vous n'avez plus d'espace de stockage gratuit. Effectuez une mise à niveau pour débloquer un espace de stockage illimité", - "storageLimitDialogTitleIOS": "Vous n'avez plus d'espace de stockage gratuit.", - "aiResponseLimitTitle": "Vous n'avez plus de réponses d'IA gratuites. Passez au plan Pro ou achetez un module complémentaire d'IA pour débloquer des réponses illimitées", - "aiResponseLimitDialogTitle": "La limite des réponses de l'IA a été atteinte", - "aiResponseLimit": "Vous n'avez plus de réponses IA gratuites.\n\nAccédez à Paramètres -> Plans -> Cliquez sur AI Max ou Pro Plan pour obtenir plus de réponses AI", - "askOwnerToUpgradeToPro": "Votre espace de stockage gratuit est presque plein. Demandez au propriétaire de votre espace de travail de passer au plan Pro", - "askOwnerToUpgradeToProIOS": "Votre espace de travail manque d’espace de stockage gratuit.", - "askOwnerToUpgradeToAIMax": "Votre espace de travail est à court de réponses d'IA gratuites. Demandez au propriétaire de votre espace de travail de mettre à niveau le plan ou d'acheter des modules complémentaires d'IA", - "askOwnerToUpgradeToAIMaxIOS": "Votre espace de travail est à court de réponses IA gratuites.", - "purchaseStorageSpace": "Acheter un espace de stockage", - "singleFileProPlanLimitationDescription": "Vous avez dépassé la taille maximale de téléchargement de fichiers autorisée dans le plan gratuit. Veuillez passer au plan Pro pour télécharger des fichiers plus volumineux", - "purchaseAIResponse": "Acheter", - "askOwnerToUpgradeToLocalAI": "Demander au propriétaire de l'espace de travail d'activer l'IA locale", - "upgradeToAILocal": "Exécutez des modèles locaux sur votre appareil pour une confidentialité optimale", - "upgradeToAILocalDesc": "Discutez avec des PDF, améliorez votre écriture et remplissez automatiquement des tableaux à l'aide de l'IA locale", "public": "Publique", "clickToHidePublic": "Cliquez pour masquer l'espace public\nLes pages que vous avez créées ici sont visibles par tous les membres", "addAPageToPublic": "Ajouter une page à l'espace public" @@ -384,7 +250,6 @@ }, "button": { "ok": "OK", - "confirm": "Confirmer", "done": "Fait", "cancel": "Annuler", "signIn": "Se connecter", @@ -402,22 +267,16 @@ "upload": "Télécharger", "edit": "Modifier", "delete": "Supprimer", - "copy": "Copier", "duplicate": "Dupliquer", "putback": "Remettre", "update": "Mettre à jour", "share": "Partager", "removeFromFavorites": "Retirer des favoris", - "removeFromRecent": "Supprimer des récents", "addToFavorites": "Ajouter aux favoris", - "favoriteSuccessfully": "Succès en favoris", - "unfavoriteSuccessfully": "Succès retiré des favoris", - "duplicateSuccessfully": "Dupliqué avec succès", "rename": "Renommer", "helpCenter": "Centre d'aide", "add": "Ajouter", "yes": "Oui", - "no": "Non", "clear": "Nettoyer", "remove": "Retirer", "dontRemove": "Ne pas retirer", @@ -430,20 +289,6 @@ "signInGoogle": "Se connecter avec Google", "signInGithub": "Se connecter avec Github", "signInDiscord": "Se connecter avec Discord", - "more": "Plus", - "create": "Créer", - "close": "Fermer", - "next": "Suivant", - "previous": "Précédent", - "submit": "Soumettre", - "download": "Télécharger", - "backToHome": "Retour à l'accueil", - "viewing": "Affichage", - "editing": "Édition", - "gotIt": "Compris", - "retry": "Réessayer ", - "uploadFailed": "Échec du téléchargement.", - "copyLinkOriginal": "Copier le lien vers l'original", "tryAGain": "Réessayer" }, "label": { @@ -468,81 +313,9 @@ }, "settings": { "title": "Paramètres", - "popupMenuItem": { - "settings": "Paramètres", - "members": "Membres", - "trash": "Corbeille", - "helpAndSupport": "Aide & Support" - }, - "sites": { - "title": "Sites", - "namespaceTitle": "Espace", - "namespaceDescription": "Gérez votre espace et votre page d'accueil", - "namespaceHeader": "Espace de nom", - "homepageHeader": "Page d'accueil", - "updateNamespace": "Mettre à jour l'espace", - "removeHomepage": "Supprimer la page d'accueil", - "selectHomePage": "Sélectionnez une page", - "clearHomePage": "Effacer la page d'accueil pour cet espace", - "customUrl": "URL personnalisée", - "namespace": { - "description": "Ce changement s'appliquera à toutes les pages publiées en direct sur cet espace", - "tooltip": "Nous nous réservons le droit de supprimer tout espace inapproprié", - "updateExistingNamespace": "Mettre à jour l'espace existant", - "upgradeToPro": "Passez au plan Pro pour définir une page d'accueil", - "redirectToPayment": "Redirection vers la page de paiement...", - "onlyWorkspaceOwnerCanSetHomePage": "Seul le propriétaire de l'espace de travail peut définir une page d'accueil", - "pleaseAskOwnerToSetHomePage": "Veuillez demander au propriétaire de l'espace de travail de passer au plan Pro" - }, - "publishedPage": { - "title": "Toutes les pages publiées", - "description": "Gérez vos pages publiées", - "page": "Page", - "pathName": "Nom du chemin", - "date": "Date de publication", - "emptyHinText": "Vous n'avez aucune page publiée dans cet espace de travail", - "noPublishedPages": "Aucune page publiée", - "settings": "Paramètres de publication", - "clickToOpenPageInApp": "Ouvrir la page dans l'application", - "clickToOpenPageInBrowser": "Ouvrir la page dans le navigateur" - }, - "error": { - "failedToGeneratePaymentLink": "Impossible de générer le lien de paiement pour le plan Pro", - "failedToUpdateNamespace": "Échec de la mise à jour de l'espace", - "proPlanLimitation": "Vous devez effectuer une mise à niveau vers le plan Pro pour mettre à jour l'espace", - "namespaceAlreadyInUse": "Ce nom d'espace déjà pris, veuillez en essayer un autre", - "invalidNamespace": "Nom d'espace invalide, veuillez en essayer un autre", - "namespaceLengthAtLeast2Characters": "Le nom de l'espace doit comporter au moins 2 caractères", - "onlyWorkspaceOwnerCanUpdateNamespace": "Seul le propriétaire de l'espace de travail peut mettre à jour l'espace", - "onlyWorkspaceOwnerCanRemoveHomepage": "Seul le propriétaire de l'espace de travail peut supprimer la page d'accueil", - "setHomepageFailed": "Impossible de définir la page d'accueil", - "namespaceTooLong": "Le nom de l'espace est trop long, veuillez en essayer un autre", - "namespaceTooShort": "Le nom de l'espace est trop court, veuillez en essayer un autre", - "namespaceIsReserved": "Ce nom d'espace est réservé, veuillez en essayer un autre", - "updatePathNameFailed": "Échec de la mise à jour du nom du chemin", - "removeHomePageFailed": "Impossible de supprimer la page d'accueil", - "publishNameContainsInvalidCharacters": "Le nom du chemin contient des caractères non valides, veuillez en essayer un autre", - "publishNameTooShort": "Le nom du chemin est trop court, veuillez en essayer un autre", - "publishNameTooLong": "Le nom du chemin est trop long, veuillez en essayer un autre", - "publishNameAlreadyInUse": "Le nom du chemin est déjà utilisé, veuillez en essayer un autre", - "namespaceContainsInvalidCharacters": "Le nom d'espace contient des caractères non valides, veuillez en essayer un autre", - "publishPermissionDenied": "Seul le propriétaire de l'espace de travail ou l'éditeur de la page peut gérer les paramètres de publication", - "publishNameCannotBeEmpty": "Le nom du chemin ne peut pas être vide, veuillez en essayer un autre" - }, - "success": { - "namespaceUpdated": "Espace mis à jour avec succès", - "setHomepageSuccess": "Définir la page d'accueil avec succès", - "updatePathNameSuccess": "Nom du chemin mis à jour avec succès", - "removeHomePageSuccess": "Supprimer la page d'accueil avec succès" - } - }, "accountPage": { "menuLabel": "Mon compte", "title": "Mon compte", - "general": { - "title": "Nom du compte et image de profil", - "changeProfilePicture": "Changer la photo de profil" - }, "email": { "title": "Email", "actions": { @@ -550,7 +323,6 @@ } }, "login": { - "title": "Connexion au compte", "loginLabel": "Connexion", "logoutLabel": "Déconnexion" } @@ -576,48 +348,31 @@ "dark": "Foncé" } }, - "resetCursorColor": { - "title": "Réinitialiser la couleur du curseur du document", - "description": "Êtes-vous sûr de vouloir réinitialiser la couleur du curseur ?" - }, - "resetSelectionColor": { - "title": "Réinitialiser la couleur de sélection du document", - "description": "Êtes-vous sûr de vouloir réinitialiser la couleur de sélection ?" - }, - "resetWidth": { - "resetSuccess": "Réinitialisation réussie de la largeur du document" - }, "theme": { "title": "Thème", - "description": "Sélectionnez un thème prédéfini ou téléchargez votre propre thème personnalisé.", - "uploadCustomThemeTooltip": "Télécharger un thème personnalisé" + "description": "Sélectionnez un thème prédéfini ou téléchargez votre propre thème personnalisé." }, "workspaceFont": { - "title": "Police de caractère de l'espace de travail", - "noFontHint": "Aucune police trouvée, essayez un autre terme." + "title": "Police de caractère de l'espace de travail" }, "textDirection": { "title": "Sens du texte", "leftToRight": "De gauche à droite", "rightToLeft": "De droite à gauche", - "auto": "Auto", - "enableRTLItems": "Activer les éléments de la barre d'outils RTL" + "auto": "Auto" }, "layoutDirection": { - "title": "Sens de mise en page", "leftToRight": "De gauche à droite", "rightToLeft": "De droite à gauche" }, "dateTime": { "title": "Date et heure", - "example": "{} à {} ({})", "24HourTime": "Heure sur 24 heures", "dateFormat": { "label": "Format de date", "local": "Locale", "us": "US", "iso": "ISO", - "friendly": "Facile à lire", "dmy": "J/M/A" } }, @@ -630,9 +385,7 @@ }, "leaveWorkspacePrompt": { "title": "Quitter l'espace de travail", - "content": "Êtes-vous sûr de vouloir quitter cet espace de travail ? Vous allez perdre l’accès à toutes les pages et données qu’il contient.", - "success": "Vous avez quitté l'espace de travail avec succès.", - "fail": "Impossible de quitter l'espace de travail." + "content": "Êtes-vous sûr de vouloir quitter cet espace de travail ? Vous allez perdre l’accès à toutes les pages et données qu’il contient." }, "manageWorkspace": { "title": "Gérer l'espace de travail", @@ -643,417 +396,25 @@ "manageDataPage": { "menuLabel": "Gérer les données", "title": "Gérer les données", - "description": "Gérez le stockage local des données ou importez vos données existantes dans @:appName .", "dataStorage": { - "title": "Emplacement de stockage des fichiers", - "tooltip": "L'emplacement où vos fichiers sont stockés", "actions": { "change": "Changer de chemin", "open": "Ouvrir le répertoire", - "openTooltip": "Ouvrir l’emplacement actuel du dossier de données", "copy": "Copier le chemin", - "copiedHint": "Lien copié !", - "resetTooltip": "Réinitialiser à l'emplacement par défaut" + "copiedHint": "Lien copié !" }, "resetDialog": { - "title": "Êtes-vous sûr ?", - "description": "La réinitialisation du chemin d'accès à l'emplacement de données par défaut ne supprimera pas vos données. Si vous souhaitez réimporter vos données actuelles, vous devriez sauvegarder le chemin d'accès actuel." + "title": "Êtes-vous sûr ?" } }, "importData": { "title": "Importer des données", - "tooltip": "Importer des données depuis les dossiers de sauvegarde/données @:appName", - "description": "Copier les données à partir d'un dossier de données externe @:appName", "action": "Parcourir le dossier" }, "encryption": { - "title": "Chiffrement", - "tooltip": "Gérez la manière dont vos données sont stockées et cryptées", - "descriptionNoEncryption": "L'activation du cryptage crypte toutes les données. Cette opération ne peut pas être annulée.", - "descriptionEncrypted": "Vos données sont cryptées.", - "action": "Crypter les données", - "dialog": { - "title": "Crypter toutes vos données ?", - "description": "Le cryptage de toutes vos données permettra de les protéger et de les sécuriser. Cette action NE PEUT PAS être annulée. Êtes-vous sûr de vouloir continuer ?" - } - }, - "cache": { - "title": "Vider le cache", - "description": "Aide à résoudre des problèmes tels que des image qui ne se chargent pas, des pages manquantes dans un espace ou les polices qui ne se chargent pas. Cela n'affectera pas vos données.", - "dialog": { - "title": "Vider le cache", - "description": "Aide à résoudre des problèmes tels que des image qui ne se chargent pas, des pages manquantes dans un espace ou les polices qui ne se chargent pas. Cela n'affectera pas vos données.", - "successHint": "Cache vidé !" - } - }, - "data": { - "fixYourData": "Corrigez vos données", - "fixButton": "Réparer", - "fixYourDataDescription": "Si vous rencontrez des problèmes avec vos données, vous pouvez essayer de les résoudre ici." + "title": "Chiffrement" } }, - "shortcutsPage": { - "menuLabel": "Raccourcis", - "title": "Raccourcis", - "editBindingHint": "Saisir une nouvelle liaison", - "searchHint": "Rechercher", - "actions": { - "resetDefault": "Réinitialiser les paramètres par défaut" - }, - "errorPage": { - "message": "Échec du chargement des raccourcis : {}", - "howToFix": "Veuillez réessayer. Si le problème persiste, veuillez nous contacter sur GitHub." - }, - "resetDialog": { - "title": "Réinitialiser les raccourcis", - "description": "Cela réinitialisera tous vos raccourcis clavier aux valeurs par défaut, vous ne pourrez pas annuler cette opération plus tard, êtes-vous sûr de vouloir continuer ?", - "buttonLabel": "Réinitialiser" - }, - "conflictDialog": { - "title": "{} est actuellement utilisé", - "descriptionPrefix": "Ce raccourci clavier est actuellement utilisé par ", - "descriptionSuffix": ". Si vous remplacez ce raccourci clavier, il sera supprimé de {}.", - "confirmLabel": "Continuer" - }, - "editTooltip": "Appuyez pour commencer à modifier le raccourci clavier", - "keybindings": { - "toggleToDoList": "Basculer vers la liste des tâches", - "insertNewParagraphInCodeblock": "Insérer un nouveau paragraphe", - "pasteInCodeblock": "Coller dans le bloc de code", - "selectAllCodeblock": "Sélectionner tout", - "indentLineCodeblock": "Insérer deux espaces au début de la ligne", - "outdentLineCodeblock": "Supprimer deux espaces au début de la ligne", - "twoSpacesCursorCodeblock": "Insérer deux espaces au niveau du curseur", - "copy": "Copier la sélection", - "paste": "Coller le contenu", - "cut": "Couper la sélection", - "alignLeft": "Aligner le texte à gauche", - "alignCenter": "Aligner le texte au centre", - "alignRight": "Aligner le texte à droite", - "undo": "Annuler", - "redo": "Rétablir", - "convertToParagraph": "Convertir un bloc en paragraphe", - "backspace": "Supprimer", - "deleteLeftWord": "Supprimer le mot de gauche", - "deleteLeftSentence": "Supprimer la phrase de gauche", - "delete": "Supprimer le caractère de droite", - "deleteMacOS": "Supprimer le caractère de gauche", - "deleteRightWord": "Supprimer le mot de droite", - "moveCursorLeft": "Déplacer le curseur vers la gauche", - "moveCursorBeginning": "Déplacer le curseur au début", - "moveCursorLeftWord": "Déplacer le curseur d'un mot vers la gauche", - "moveCursorLeftSelect": "Sélectionnez et déplacez le curseur vers la gauche", - "moveCursorBeginSelect": "Sélectionnez et déplacez le curseur au début", - "moveCursorLeftWordSelect": "Sélectionnez et déplacez le curseur d'un mot vers la gauche", - "moveCursorRight": "Déplacer le curseur vers la droite", - "moveCursorEnd": "Déplacer le curseur jusqu'à la fin", - "moveCursorRightWord": "Déplacer le curseur d'un mot vers la droite", - "moveCursorRightSelect": "Sélectionnez et déplacez le curseur vers la droite", - "moveCursorEndSelect": "Sélectionnez et déplacez le curseur jusqu'à la fin", - "moveCursorRightWordSelect": "Sélectionnez et déplacez le curseur vers la droite d'un mot", - "moveCursorUp": "Déplacer le curseur vers le haut", - "moveCursorTopSelect": "Sélectionnez et déplacez le curseur vers le haut", - "moveCursorTop": "Déplacer le curseur vers le haut", - "moveCursorUpSelect": "Sélectionnez et déplacez le curseur vers le haut", - "moveCursorBottomSelect": "Sélectionnez et déplacez le curseur vers le bas", - "moveCursorBottom": "Déplacer le curseur vers le bas", - "moveCursorDown": "Déplacer le curseur vers le bas", - "moveCursorDownSelect": "Sélectionnez et déplacez le curseur vers le bas", - "home": "Faites défiler vers le haut", - "end": "Faites défiler vers le bas", - "toggleBold": "Inverser le gras", - "toggleItalic": "Inverser l'italique", - "toggleUnderline": "Inverser le soulignement", - "toggleStrikethrough": "Inverser le barré", - "toggleCode": "Inverser la mise en forme code", - "toggleHighlight": "Inverser la surbrillance", - "showLinkMenu": "Afficher le menu des liens", - "openInlineLink": "Ouvrir le lien en ligne", - "openLinks": "Ouvrir tous les liens sélectionnés", - "indent": "Augmenter le retrait", - "outdent": "Diminuer le retrait", - "exit": "Quitter l'édition", - "pageUp": "Faites défiler une page vers le haut", - "pageDown": "Faites défiler une page vers le bas", - "selectAll": "Sélectionner tout", - "pasteWithoutFormatting": "Coller le contenu sans formatage", - "showEmojiPicker": "Afficher le sélecteur d'emoji", - "enterInTableCell": "Ajouter un saut de ligne dans le tableau", - "leftInTableCell": "Déplacer d'une cellule vers la gauche dans le tableau", - "rightInTableCell": "Déplacer d'une cellule vers la droite dans le tableau", - "upInTableCell": "Déplacer d'une cellule vers le haut dans le tableau", - "downInTableCell": "Déplacer d'une cellule vers le bas dans le tableau", - "tabInTableCell": "Aller à la prochaine cellule vide dans le tableau", - "shiftTabInTableCell": "Aller à la précédente cellule vide dans le tableau", - "backSpaceInTableCell": "S'arrêter au début de la cellule" - }, - "commands": { - "codeBlockNewParagraph": "Insérer un nouveau paragraphe à côté du bloc de code", - "codeBlockIndentLines": "Insérer deux retraits au début du bloc de code", - "codeBlockOutdentLines": "Supprimer deux retraits au début du bloc de code", - "codeBlockAddTwoSpaces": "Insérer deux retraits au niveau du curseur dans le bloc de code", - "codeBlockSelectAll": "Sélectionner tout le contenu d'un bloc de code", - "codeBlockPasteText": "Coller du texte dans le bloc de code", - "textAlignLeft": "Aligner le texte à gauche", - "textAlignCenter": "Aligner le texte au centre", - "textAlignRight": "Aligner le texte à droite" - }, - "couldNotLoadErrorMsg": "Impossible de charger les raccourcis, réessayez", - "couldNotSaveErrorMsg": "Impossible d'enregistrer les raccourcis, réessayez" - }, - "aiPage": { - "title": "Paramètres de l'IA", - "menuLabel": "Paramètres IA", - "keys": { - "enableAISearchTitle": "Recherche IA", - "aiSettingsDescription": "Choisissez votre modèle préféré pour alimenter AppFlowy AI. Inclut désormais GPT 4-o, Claude 3,5, Llama 3.1 et Mistral 7B", - "loginToEnableAIFeature": "Les fonctionnalités d'IA sont accessibles uniquement après s'être connecté avec @:appName Cloud. Pour créer un compte @:appName, voir dans la rubrique 'Mon Compte'.", - "llmModel": "Modèle de langage", - "llmModelType": "Type de modèle de langue", - "downloadLLMPrompt": "Télécharger {}", - "downloadAppFlowyOfflineAI": "Le téléchargement du package hors ligne AI permettra à AI de fonctionner sur votre appareil. Voulez-vous continuer ?", - "downloadLLMPromptDetail": "Le téléchargement du modèle local {} prendra jusqu'à {} d'espace de stockage. Voulez-vous continuer ?", - "downloadBigFilePrompt": "Le téléchargement peut prendre environ 10 minutes.", - "downloadAIModelButton": "Télécharger", - "downloadingModel": "Téléchargement", - "localAILoaded": "Modèle d'IA local ajouté avec succès et prêt à être utilisé", - "localAIStart": "Démarrage du chat avec l'IA locale...", - "localAILoading": "Chargement du modèle d'IA locale...", - "localAIStopped": "IA locale arrêtée", - "failToLoadLocalAI": "Impossible de démarrer l'IA locale", - "restartLocalAI": "Redémarrer l'IA locale", - "disableLocalAITitle": "Désactiver l'IA locale", - "disableLocalAIDescription": "Voulez-vous désactiver l'IA locale ?", - "localAIToggleTitle": "Basculer pour activer ou désactiver l'IA locale", - "offlineAIInstruction1": "Suivre les", - "offlineAIInstruction2": "instructions", - "offlineAIInstruction3": "pour activer l'IA hors ligne.", - "offlineAIDownload1": "Si vous n'avez pas téléchargé l'IA AppFlowy, veuillez", - "offlineAIDownload2": "télécharger", - "offlineAIDownload3": "d'abord", - "activeOfflineAI": "Activer", - "downloadOfflineAI": "Télécharger", - "openModelDirectory": "Ouvrir le dossier" - } - }, - "planPage": { - "menuLabel": "Offre", - "title": "Tarif de l'offre", - "planUsage": { - "title": "Résumé de l'utilisation du plan", - "storageLabel": "Stockage", - "storageUsage": "{} sur {} GB", - "unlimitedStorageLabel": "Stockage illimité", - "collaboratorsLabel": "Membres", - "collaboratorsUsage": "{} sur {}", - "aiResponseLabel": "Réponses de l'IA", - "aiResponseUsage": "{} sur {}", - "unlimitedAILabel": "Réponses illimitées", - "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "IA sur appareil pour Mac", - "memberProToggle": "Plus de membres et une IA illimitée", - "aiMaxToggle": "IA illimitée et accès à des modèles avancés", - "aiOnDeviceToggle": "IA locale pour une confidentialité ultime", - "aiCredit": { - "title": "Ajoutez des crédit IA @:appName ", - "price": "{}", - "priceDescription": "pour 1 000 crédits", - "purchase": "Acheter l'IA", - "info": "Ajoutez 1 000 crédits d'IA par espace de travail et intégrez de manière transparente une IA personnalisable dans votre flux de travail pour des résultats plus intelligents et plus rapides avec jusqu'à :", - "infoItemOne": "10 000 réponses par base de données", - "infoItemTwo": "1 000 réponses par espace de travail" - }, - "currentPlan": { - "bannerLabel": "Plan actuel", - "freeTitle": "Gratuit", - "proTitle": "Pro", - "teamTitle": "Équipe", - "freeInfo": "Idéal pour les particuliers jusqu'à 2 membres pour tout organiser", - "proInfo": "Idéal pour les petites et moyennes équipes jusqu'à 10 membres.", - "teamInfo": "Parfait pour toutes les équipes productives et bien organisées.", - "upgrade": "Changer de plan", - "canceledInfo": "Votre forfait est annulé, vous serez rétrogradé au plan gratuit le {}." - }, - "addons": { - "title": "Compléments", - "addLabel": "Ajouter", - "activeLabel": "Ajouté", - "aiMax": { - "title": "AI Max", - "description": "Réponses IA illimitées alimentées par GPT-4o, Claude 3.5 Sonnet et plus", - "price": "{}", - "priceInfo": "par utilisateur et par mois, facturé annuellement" - }, - "aiOnDevice": { - "title": "IA sur appareil pour Mac", - "description": "Exécutez Mistral 7B, LLAMA 3 et d'autres modèles locaux sur votre machine", - "price": "{}", - "priceInfo": "par utilisateur et par mois, facturé annuellement", - "recommend": "Recommand2 M1 ou plus récent" - } - }, - "deal": { - "bannerLabel": "Offre de nouvelle année !", - "title": "Développez votre équipe !", - "info": "Effectuez une mise à niveau et économisez 10 % sur les forfaits Pro et Team ! Boostez la productivité de votre espace de travail avec de nouvelles fonctionnalités puissantes, notamment l'IA @:appName .", - "viewPlans": "Voir les plans" - } - } - }, - "billingPage": { - "menuLabel": "Facturation", - "title": "Facturation", - "plan": { - "title": "Plan", - "freeLabel": "Gratuit", - "proLabel": "Pro", - "planButtonLabel": "Changer de plan", - "billingPeriod": "Période de facturation", - "periodButtonLabel": "Changer la période " - }, - "paymentDetails": { - "title": "Détails de paiement", - "methodLabel": "Mode de paiement", - "methodButtonLabel": "Changer de mode de paiement" - }, - "addons": { - "title": "Compléments", - "addLabel": "Ajouter", - "removeLabel": "Retirer", - "renewLabel": "Renouveler", - "aiMax": { - "label": "IA Max", - "description": "Débloquez une IA illimitée et des modèles avancés", - "activeDescription": "Prochaine facture due le {}", - "canceledDescription": "IA Max sera disponible jusqu'au {}" - }, - "aiOnDevice": { - "label": "IA local pour Mac", - "description": "Débloquez une IA illimitée locale sur votre appareil", - "activeDescription": "Prochaine facture due le {}", - "canceledDescription": "IA locale pour Mac sera disponible jusqu'au {}" - }, - "removeDialog": { - "title": "Supprimer", - "description": "Êtes-vous sûr de vouloir supprimer {plan}? Vous perdrez l'accès aux fonctionnalités et bénéfices de {plan} de manière immédiate." - } - }, - "currentPeriodBadge": "ACTUEL", - "changePeriod": "Changer de période", - "planPeriod": "{} période", - "monthlyInterval": "Mensuel", - "monthlyPriceInfo": "par personne, facturé mensuellement", - "annualInterval": "Annuellement", - "annualPriceInfo": "par personne, facturé annuellement" - }, - "comparePlanDialog": { - "title": "Comparer et sélectionner un plan", - "planFeatures": "Plan\nCaractéristiques", - "current": "Actuel", - "actions": { - "upgrade": "Améliorer", - "downgrade": "Rétrograder", - "current": "Actuel" - }, - "freePlan": { - "title": "Gratuit", - "description": "Pour les particuliers jusqu'à 2 membres pour tout organiser", - "price": "{}", - "priceInfo": "gratuit pour toujours" - }, - "proPlan": { - "title": "Pro", - "description": "Pour les petites équipes pour gérer les projets et les bases de connaissance", - "price": "{}", - "priceInfo": "par utilisateur et par mois\nfacturé annuellement\n\n{} facturé mensuellement" - }, - "planLabels": { - "itemOne": "Espaces de travail", - "itemTwo": "Membres", - "itemThree": "Stockage", - "itemFour": "Collaboration en temps réel", - "itemFive": "Application mobile", - "itemSix": "Réponses de l'IA", - "itemFileUpload": "Téléchargements de fichiers", - "customNamespace": "Nom d'espace personnalisé", - "tooltipSix": "La durée de vie signifie que le nombre de réponses n'est jamais réinitialisé", - "intelligentSearch": "Recherche intelligente", - "tooltipSeven": "Vous permet de personnaliser une partie de l'URL de votre espace de travail", - "customNamespaceTooltip": "URL de site publiée personnalisée" - }, - "freeLabels": { - "itemOne": "facturé par espace de travail", - "itemTwo": "jusqu'à 2", - "itemThree": "5 Go", - "itemFour": "Oui", - "itemFive": "Oui", - "itemSix": "10 à vie", - "itemFileUpload": "Jusqu'à 7 Mo", - "intelligentSearch": "Recherche intelligente" - }, - "proLabels": { - "itemOne": "facturé par espace de travail", - "itemTwo": "jusqu'à 10", - "itemThree": "illimité", - "itemFour": "Oui", - "itemFive": "Oui", - "itemSix": "illimité", - "itemFileUpload": "Illimité", - "intelligentSearch": "Recherche intelligente" - }, - "paymentSuccess": { - "title": "Vous êtes maintenant sur le plan {} !", - "description": "Votre paiement a été traité avec succès et votre forfait est mis à niveau vers @:appName {}. Vous pouvez consulter les détails de votre forfait sur la page Forfait" - }, - "downgradeDialog": { - "title": "Êtes-vous sûr de vouloir rétrograder votre forfait ?", - "description": "La baisse de votre offre vous ramènera au forfait gratuit. Les membres peuvent perdre l'accès à cet espace de travail et vous devrez peut-être libérer de l'espace pour respecter les limites de stockage du forfait gratuit.", - "downgradeLabel": "Baisser l'offre" - } - }, - "cancelSurveyDialog": { - "title": "Désolé de vous voir partir", - "description": "Nous sommes désolés de vous voir partir. Nous aimerions connaître votre avis pour nous aider à améliorer @:appName . Veuillez prendre un moment pour répondre à quelques questions.", - "commonOther": "Autre", - "otherHint": "Écrivez votre réponse ici", - "questionOne": { - "question": "Qu'est-ce qui vous a poussé à annuler votre @:appName Pro ?", - "answerOne": "Coût trop élevé", - "answerTwo": "Les fonctionnalités ne répondent pas à mes attentes", - "answerThree": "J'ai trouvé une meilleure alternative", - "answerFour": "Je ne l'ai pas suffisamment utilisé pour justifier la dépense", - "answerFive": "Problème de service ou difficultés techniques" - }, - "questionTwo": { - "question": "Quelle est la probabilité que vous envisagiez de vous réabonner à @:appName Pro à l'avenir ?", - "answerOne": "Très probablement", - "answerTwo": "Assez probable", - "answerThree": "Pas sûr", - "answerFour": "Peu probable", - "answerFive": "Très peu probable" - }, - "questionThree": { - "question": "Quelle fonctionnalité Pro avez-vous le plus appréciée lors de votre abonnement ?", - "answerOne": "Collaboration multi-utilisateurs", - "answerTwo": "Historique des versions plus long", - "answerThree": "Réponses IA illimitées", - "answerFour": "Accès aux modèles d'IA locaux" - }, - "questionFour": { - "question": "Comment décririez-vous votre expérience globale avec @:appName ?", - "answerOne": "Super", - "answerTwo": "Bien", - "answerThree": "Moyenne", - "answerFour": "En dessous de la moyenne", - "answerFive": "Insatisfait" - } - }, - "common": { - "uploadingFile": "Le fichier est en cours de téléchargement. Veuillez ne pas quitter l'application", - "uploadNotionSuccess": "Votre fichier zip Notion a été téléchargé avec succès. Une fois l'importation terminée, vous recevrez un e-mail de confirmation", - "reset": "Réinitialiser" - }, "menu": { "appearance": "Apparence", "language": "Langue", @@ -1067,15 +428,17 @@ "syncSetting": "Paramètres de synchronisation", "cloudSettings": "Paramètres cloud", "enableSync": "Activer la synchronisation", - "enableSyncLog": "Activer la journalisation de synchronisation", - "enableSyncLogWarning": "Merci de nous aider à diagnostiquer les problèmes de synchronisation. Cela enregistrera les modifications de votre document dans un fichier local. Veuillez quitter et rouvrir l'application après l'avoir activée", "enableEncrypt": "Chiffrer les données", "cloudURL": "URL de base", - "webURL": "Web URL", "invalidCloudURLScheme": "Schéma invalide", "cloudServerType": "Serveur cloud", "cloudServerTypeTip": "Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.", "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL de Supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", + "cloudSupabaseAnonKey": "Clé anonyme Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", "cloudAppFlowy": "@:appName Cloud Bêta", "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", @@ -1083,13 +446,10 @@ "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", "selfHostContent": "document", "selfHostEnd": "pour obtenir des conseils sur la façon d'auto-héberger votre propre serveur", - "pleaseInputValidURL": "Veuillez saisir une URL valide", - "changeUrl": "Changer l'URL auto-hébergée en {}", "cloudURLHint": "Saisissez l'URL de base de votre serveur", - "webURLHint": "Saisissez l'URL de base de votre serveur web", "cloudWSURL": "URL du websocket", "cloudWSURLHint": "Saisissez l'adresse websocket de votre serveur", - "restartApp": "Redémarrer", + "restartApp": "Redémarer", "restartAppTip": "Redémarrez l'application pour que les modifications prennent effet. Veuillez noter que cela pourrait déconnecter votre compte actuel.", "changeServerTip": "Après avoir changé de serveur, vous devez cliquer sur le bouton de redémarrer pour que les modifications prennent effet", "enableEncryptPrompt": "Activez le chiffrement pour sécuriser vos données avec cette clé. Rangez-la en toute sécurité ; une fois activé, il ne peut pas être désactivé. En cas de perte, vos données deviennent irrécupérables. Cliquez pour copier", @@ -1107,64 +467,20 @@ "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", "importSuccess": "Importation réussie du dossier de données @:appName", "importFailed": "L'importation du dossier de données @:appName a échoué", - "importGuide": "Pour plus de détails, veuillez consulter le document référencé" + "importGuide": "Pour plus de détails, veuillez consulter le document référencé", + "supabaseSetting": "Paramètre Supabase" }, "notifications": { "enableNotifications": { "label": "Activer les notifications", "hint": "Désactivez-la pour empêcher l'affichage des notifications locales." - }, - "showNotificationsIcon": { - "label": "Afficher l'icône des notifications", - "hint": "Désactiver pour masquer l'icône de notification dans la barre latérale." - }, - "archiveNotifications": { - "allSuccess": "Toutes les notifications ont été archivées avec succès", - "success": "Notification archivée avec succès" - }, - "markAsReadNotifications": { - "allSuccess": "Tout a été marqué comme lu avec succès", - "success": "Marqué comme lu avec succès" - }, - "action": { - "markAsRead": "Marquer comme lu", - "multipleChoice": "Sélectionnez plus", - "archive": "Archiver" - }, - "settings": { - "settings": "Paramètres", - "markAllAsRead": "Marquer tout comme lu", - "archiveAll": "Archiver tout" - }, - "emptyInbox": { - "title": "Aucune notification pour le moment", - "description": "Vous serez averti ici des @mentions" - }, - "emptyUnread": { - "title": "Aucune notification non lue", - "description": "Vous êtes à jour !" - }, - "emptyArchived": { - "title": "Aucune notification archivée", - "description": "Vous n'avez pas encore archivé de notifications" - }, - "tabs": { - "inbox": "Boîte de réception", - "unread": "Non lu", - "archived": "Archivé" - }, - "refreshSuccess": "Les notifications ont été actualisées avec succès", - "titles": { - "notifications": "Notifications", - "reminder": "Rappel" } }, "appearance": { "resetSetting": "Réinitialiser ce paramètre", "fontFamily": { "label": "Famille de polices", - "search": "Recherche", - "defaultFont": "Système" + "search": "Recherche" }, "themeMode": { "label": " Mode du Thème", @@ -1176,11 +492,6 @@ "documentSettings": { "cursorColor": "Couleur du curseur du document", "selectionColor": "Couleur de sélection du document", - "width": "Largeur du document", - "changeWidth": "Changement", - "pickColor": "Sélectionnez une couleur", - "colorShade": "Nuance de couleur", - "opacity": "Opacité", "hexEmptyError": "La couleur hexadécimale ne peut pas être vide", "hexLengthError": "La valeur hexadécimale doit comporter 6 chiffres", "hexInvalidError": "Valeur hexadécimale invalide", @@ -1236,15 +547,12 @@ "members": { "title": "Paramètres des membres", "inviteMembers": "Inviter des membres", - "inviteHint": "Invitation par email", "sendInvite": "Envoyer une invitation", "copyInviteLink": "Copier le lien d'invitation", "label": "Membres", "user": "Utilisateur", "role": "Rôle", "removeFromWorkspace": "Retirer de l'espace de travail", - "removeFromWorkspaceSuccess": "Retiré de l'espace de travail avec succès", - "removeFromWorkspaceFailed": "Suppression du membre échouée ", "owner": "Propriétaire", "guest": "Invité", "member": "Membre", @@ -1258,21 +566,11 @@ "one": "{} membre", "other": "{} membres" }, - "inviteFailedDialogTitle": "Échec de l'envoi de l'invitation", - "inviteFailedMemberLimit": "La limite de membres a été atteinte, veuillez effectuer une mise à niveau pour inviter plus de membres.", - "inviteFailedMemberLimitMobile": "Votre espace de travail a atteint la limite de membres. Utilisez l'application sur PC pour effectuez une mise à niveau et débloquer plus de fonctionnalités.", "memberLimitExceeded": "Vous avez atteint la limite maximale de membres autorisée pour votre compte. Si vous souhaitez ajouter d'autres membres pour continuer votre travail, veuillez en faire la demande sur Github.", - "memberLimitExceededUpgrade": "mise à niveau", - "memberLimitExceededPro": "Limite de membres atteinte, si vous avez besoin de plus de membres, contactez ", - "memberLimitExceededProContact": "support@appflowy.io", "failedToAddMember": "Échec de l'ajout d'un membre", "addMemberSuccess": "Membre ajouté avec succès", "removeMember": "Supprimer un membre", - "areYouSureToRemoveMember": "Êtes-vous sûr de vouloir supprimer ce membre ?", - "inviteMemberSuccess": "L'invitation a été envoyée avec succès", - "failedToInviteMember": "Impossible d'inviter un membre", - "workspaceMembersError": "Une erreur s'est produite", - "workspaceMembersErrorDescription": "Nous n'avons pas pu charger la liste des membres. Veuillez essayer plus tard s'il vous plait" + "areYouSureToRemoveMember": "Êtes-vous sûr de vouloir supprimer ce membre ?" } }, "files": { @@ -1320,26 +618,9 @@ "email": "Courriel", "tooltipSelectIcon": "Sélectionner l'icône", "selectAnIcon": "Sélectionnez une icône", - "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé AI", - "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel", - "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI" - }, - "mobile": { - "personalInfo": "Informations personnelles", - "username": "Nom d'utilisateur", - "usernameEmptyError": "Le nom d'utilisateur ne peut pas être vide", - "about": "À propos", - "pushNotifications": "Notifications push", - "support": "Support", - "joinDiscord": "Rejoignez-nous sur Discord", - "privacyPolicy": "Politique de Confidentialité", - "userAgreement": "Accord de l'utilisateur", - "termsAndConditions": "Termes et conditions", - "userprofileError": "Échec du chargement du profil utilisateur", - "userprofileErrorDescription": "Veuillez essayer de vous déconnecter et de vous reconnecter pour vérifier si le problème persiste.", - "selectLayout": "Sélectionner la mise en page", - "selectStartingDay": "Sélectionnez le jour de début", - "version": "Version" + "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI", + "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI", + "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel" }, "shortcuts": { "shortcutsLabel": "Raccourcis", @@ -1361,6 +642,23 @@ "textAlignRight": "Aligner le texte à droite", "codeBlockDeleteTwoSpaces": "Supprimez deux espaces au début de la ligne dans le bloc de code" } + }, + "mobile": { + "personalInfo": "Informations personnelles", + "username": "Nom d'utilisateur", + "usernameEmptyError": "Le nom d'utilisateur ne peut pas être vide", + "about": "À propos", + "pushNotifications": "Notifications push", + "support": "Support", + "joinDiscord": "Rejoignez-nous sur Discord", + "privacyPolicy": "Politique de Confidentialité", + "userAgreement": "Accord de l'utilisateur", + "termsAndConditions": "Termes et conditions", + "userprofileError": "Échec du chargement du profil utilisateur", + "userprofileErrorDescription": "Veuillez essayer de vous déconnecter et de vous reconnecter pour vérifier si le problème persiste.", + "selectLayout": "Sélectionner la mise en page", + "selectStartingDay": "Sélectionnez le jour de début", + "version": "Version" } }, "grid": { @@ -1392,13 +690,6 @@ "Properties": "Propriétés", "viewList": "Vues de base de données" }, - "filter": { - "empty": "Aucun filtre actif", - "addFilter": "Ajouter un filtre", - "cannotFindCreatableField": "Impossible de trouver un champ approprié pour filtrer", - "conditon": "Condition", - "where": "Où" - }, "textFilter": { "contains": "Contient", "doesNotContain": "Ne contient pas", @@ -1444,12 +735,9 @@ "between": "Est entre", "empty": "Est vide", "notEmpty": "N'est pas vide", - "startDate": "Date de début", - "endDate": "Date de fin", "choicechipPrefix": { "before": "Avant", "after": "Après", - "between": "Entre", "onOrBefore": "Pendant ou avant", "onOrAfter": "Pendant ou après", "isEmpty": "Est vide", @@ -1467,16 +755,13 @@ "isNotEmpty": "N'est pas vide" }, "field": { - "label": "Propriété", "hide": "Cacher", "show": "Afficher", "insertLeft": "Insérer à gauche", "insertRight": "Insérer à droite", "duplicate": "Dupliquer", "delete": "Supprimer", - "wrapCellContent": "Envelopper le texte", "clear": "Effacer les cellules", - "switchPrimaryFieldTooltip": "Impossible de modifier le type de champ du champ principal", "textFieldName": "Texte", "checkboxFieldName": "Case à cocher", "dateFieldName": "Date", @@ -1488,11 +773,6 @@ "urlFieldName": "URL", "checklistFieldName": "Check-list", "relationFieldName": "Relation", - "summaryFieldName": "Résume IA", - "timeFieldName": "Horaire", - "mediaFieldName": "Fichiers et médias", - "translateFieldName": "Traduction IA", - "translateTo": "Traduire en", "numberFormat": "Format du nombre", "dateFormat": "Format de la date", "includeTime": "Inclure l'heure", @@ -1521,7 +801,6 @@ "addOption": "Ajouter une option", "editProperty": "Modifier la propriété", "newProperty": "Nouvelle colonne", - "openRowDocument": "Ouvrir en tant que page", "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?", "clearFieldPromptMessage": "Es-tu sûr? Toutes les cellules de cette colonne seront vidées", "newColumn": "Nouvelle colonne", @@ -1541,9 +820,7 @@ "one": "Cacher {count} champ caché", "many": "Cacher {count} champs masqués", "other": "Cacher {count} champs masqués" - }, - "openAsFullPage": "Ouvrir en pleine page", - "moreRowActions": "Plus d'actions de ligne" + } }, "sort": { "ascending": "Ascendant", @@ -1553,13 +830,11 @@ "cannotFindCreatableField": "Impossible de trouver un champ approprié pour trier", "deleteAllSorts": "Supprimer tous les tris", "addSort": "Ajouter un tri", - "sortsActive": "Impossible {intention} lors du tri", "removeSorting": "Voulez-vous supprimer le tri ?", "fieldInUse": "Vous êtes déjà en train de trier par ce champ", "deleteSort": "Supprimer le tri" }, "row": { - "label": "Ligne", "duplicate": "Dupliquer", "delete": "Supprimer", "titlePlaceholder": "Sans titre", @@ -1567,19 +842,12 @@ "copyProperty": "Copie de la propriété dans le presse-papiers", "count": "Compte", "newRow": "Nouvelle ligne", - "loadMore": "Charger plus", "action": "Action", "add": "Cliquez sur ajouter ci-dessous", "drag": "Glisser pour déplacer", - "deleteRowPrompt": "Etes-vous sûr de vouloir supprimer cette ligne ? Cette action ne peut pas être annulée", - "deleteCardPrompt": "Etes-vous sûr de vouloir supprimer cette carte ? Cette action ne peut pas être annulée", "dragAndClick": "Faites glisser pour déplacer, cliquez pour ouvrir le menu", "insertRecordAbove": "Insérer l'enregistrement ci-dessus", - "insertRecordBelow": "Insérer l'enregistrement ci-dessous", - "noContent": "Aucun contenu", - "reorderRowDescription": "réorganiser la ligne", - "createRowAboveDescription": "créer une ligne au dessus", - "createRowBelowDescription": "insérer une ligne ci-dessous" + "insertRecordBelow": "Insérer l'enregistrement ci-dessous" }, "selectOption": { "create": "Créer", @@ -1612,7 +880,8 @@ "url": { "launch": "Ouvrir dans le navigateur", "copy": "Copier l'URL", - "textFieldHint": "Entrez une URL" + "textFieldHint": "Entrez une URL", + "copiedNotification": "Copié dans le presse-papier!" }, "relation": { "relatedDatabasePlaceLabel": "Base de données associée", @@ -1639,24 +908,6 @@ "countEmptyShort": "VIDE", "countNonEmpty": "Compter les cellules non vides", "countNonEmptyShort": "REMPLI" - }, - "media": { - "rename": "Rebaptiser", - "download": "Télécharger", - "expand": "Développer", - "delete": "Supprimer", - "moreFilesHint": "+{}", - "addFileOrImage": "Ajouter un fichier ou un lien", - "attachmentsHint": "{}", - "addFileMobile": "Ajouter un fichier", - "extraCount": "+{}", - "deleteFileDescription": "Etes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible.", - "showFileNames": "Afficher le nom du fichier", - "downloadSuccess": "Fichier téléchargé", - "downloadFailedToken": "Échec du téléchargement du fichier, jeton utilisateur indisponible", - "setAsCover": "Définir comme couverture", - "openInBrowser": "Ouvrir dans le navigateur", - "embedLink": "Intégrer le lien du fichier" } }, "document": { @@ -1665,7 +916,6 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "Création...", "slashMenu": { "board": { "selectABoardToLinkTo": "Sélectionnez un tableau à lier", @@ -1681,51 +931,6 @@ }, "document": { "selectADocumentToLinkTo": "Sélectionnez un Document vers lequel créer un lien" - }, - "name": { - "text": "Texte", - "heading1": "Titre 1", - "heading2": "Titre 2", - "heading3": "Titre 3", - "image": "Image", - "bulletedList": "Liste à puces", - "numberedList": "Liste numérotée", - "todoList": "Liste de choses à faire", - "doc": "Doc", - "linkedDoc": "Lien vers la page", - "grid": "Grille", - "linkedGrid": "Grille liée", - "kanban": "Kanban", - "linkedKanban": "Kanban lié", - "calendar": "Calendrier", - "linkedCalendar": "Calendrier lié", - "quote": "Citation", - "divider": "Diviseur", - "table": "Tableau", - "outline": "Table des matières", - "mathEquation": "Équation mathématique", - "code": "Code", - "toggleList": "Menu dépliant", - "toggleHeading1": "Basculer en titre 1", - "toggleHeading2": "Basculer en titre 2", - "toggleHeading3": "Basculer en titre 3", - "emoji": "Émoji", - "aiWriter": "Rédacteur IA", - "dateOrReminder": "Date ou rappel", - "photoGallery": "Galerie de photos", - "file": "Fichier", - "checkbox": "Case à cocher" - }, - "subPage": { - "name": "Document", - "keyword1": "sous-page", - "keyword2": "page", - "keyword3": "page enfant", - "keyword4": "insérer une page", - "keyword5": "page intégrée", - "keyword6": "nouvelle page", - "keyword7": "créer une page", - "keyword8": "document" } }, "selectionMenu": { @@ -1737,67 +942,34 @@ "referencedGrid": "Grille référencée", "referencedCalendar": "Calendrier référencé", "referencedDocument": "Document référencé", - "autoGeneratorMenuItemName": "Rédacteur AI", - "autoGeneratorTitleName": "AI : Demandez à l'IA d'écrire quelque chose...", + "autoGeneratorMenuItemName": "Rédacteur OpenAI", + "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", "autoGeneratorLearnMore": "Apprendre encore plus", "autoGeneratorGenerate": "Générer", - "autoGeneratorHintText": "Demandez à AI...", - "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé AI", + "autoGeneratorHintText": "Demandez à OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI", "autoGeneratorRewrite": "Réécrire", "smartEdit": "Assistants IA", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Corriger l'orthographe", "warning": "⚠️ Les réponses de l'IA peuvent être inexactes ou trompeuses.", "smartEditSummarize": "Résumer", "smartEditImproveWriting": "Améliorer l'écriture", "smartEditMakeLonger": "Rallonger", - "smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'AI", - "smartEditCouldNotFetchKey": "Impossible de récupérer la clé AI", - "smartEditDisabled": "Connectez AI dans les paramètres", - "appflowyAIEditDisabled": "Connectez-vous pour activer les fonctionnalités de l'IA", + "smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'OpenAI", + "smartEditCouldNotFetchKey": "Impossible de récupérer la clé OpenAI", + "smartEditDisabled": "Connectez OpenAI dans les paramètres", "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", "fonts": "Polices", "insertDate": "Insérer la date", "emoji": "Emoji", "toggleList": "Liste pliable", - "emptyToggleHeading": "Bouton vide h{}. Cliquez pour ajouter du contenu.", - "emptyToggleList": "Liste vide. Cliquez pour ajouter du contenu.", - "emptyToggleHeadingWeb": "Bascule h{level} vide. Cliquer pour ajouter du contenu ", "quoteList": "Liste de citations", "numberedList": "Liste numérotée", "bulletedList": "Liste à puces", "todoList": "Liste de tâches", "callout": "Encadré", - "simpleTable": { - "moreActions": { - "color": "Couleur", - "align": "Aligner", - "delete": "Supprimer", - "duplicate": "Dupliquer", - "insertLeft": "Insérer à gauche", - "insertRight": "Insérer à droite", - "insertAbove": "Insérer ci-dessus", - "insertBelow": "Insérer ci-dessous", - "headerColumn": "Colonne d'en-tête", - "headerRow": "Ligne d'en-tête", - "clearContents": "Supprimer le contenu", - "setToPageWidth": "Définir sur la largeur de la page", - "distributeColumnsWidth": "Répartir les colonnes uniformément", - "duplicateRow": "Ligne dupliquée", - "duplicateColumn": "Colonne dupliquée", - "textColor": "Couleur du texte", - "cellBackgroundColor": "Couleur d'arrière-plan de la cellule", - "duplicateTable": "Tableau dupliqué" - }, - "clickToAddNewRow": "Cliquez pour ajouter une nouvelle ligne", - "clickToAddNewColumn": "Cliquez pour ajouter une nouvelle colonne", - "clickToAddNewRowAndColumn": "Cliquez pour ajouter une nouvelle ligne et une nouvelle colonne", - "headerName": { - "table": "Tableau", - "alignText": "Aligner le texte" - } - }, "cover": { "changeCover": "Changer la couverture", "colors": "Couleurs", @@ -1813,7 +985,6 @@ "back": "Dos", "saveToGallery": "Sauvegarder dans la gallerie", "removeIcon": "Supprimer l'icône", - "removeCover": "Supprimer la couverture", "pasteImageUrl": "Coller l'URL de l'image", "or": "OU", "pickFromFiles": "Choisissez parmi les fichiers", @@ -1832,8 +1003,6 @@ "optionAction": { "click": "Cliquez sur", "toOpenMenu": " pour ouvrir le menu", - "drag": "Glisser", - "toMove": " à déplacer", "delete": "Supprimer", "duplicate": "Dupliquer", "turnInto": "Changer en", @@ -1845,35 +1014,12 @@ "center": "Centre", "right": "Droite", "defaultColor": "Défaut", - "depth": "Profond", - "copyLinkToBlock": "Copier le lien pour bloquer" + "depth": "Profond" }, "image": { - "addAnImage": "Ajouter une image", "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers", - "addAnImageDesktop": "Ajouter une image", - "addAnImageMobile": "Cliquez pour ajouter une ou plusieurs images", - "dropImageToInsert": "Déposez les images à insérer", - "imageUploadFailed": "Téléchargement de l'image échoué", - "imageDownloadFailed": "Le téléchargement de l'image a échoué, veuillez réessayer", - "imageDownloadFailedToken": "Le téléchargement de l'image a échoué en raison d'un jeton d'utilisateur manquant, veuillez réessayer", - "errorCode": "Code erreur" - }, - "photoGallery": { - "name": "Galerie de photos", - "imageKeyword": "image", - "imageGalleryKeyword": "Galerie d'images", - "photoKeyword": "photo", - "photoBrowserKeyword": "navigateur de photos", - "galleryKeyword": "galerie", - "addImageTooltip": "Ajouter une image", - "changeLayoutTooltip": "Changer la mise en page", - "browserLayout": "Navigateur", - "gridLayout": "Grille", - "deleteBlockTooltip": "Supprimer toute la galerie" - }, - "math": { - "copiedToPasteBoard": "L'équation mathématique a été copiée dans le presse-papiers" + "addAnImage": "Ajouter une image", + "imageUploadFailed": "Téléchargement de l'image échoué" }, "urlPreview": { "copiedToPasteBoard": "Le lien a été copié dans le presse-papier", @@ -1894,8 +1040,7 @@ "contextMenu": { "copy": "Copier", "cut": "Couper", - "paste": "Coller", - "pasteAsPlainText": "Coller en tant que texte brut" + "paste": "Coller" }, "action": "Actions", "database": { @@ -1906,51 +1051,7 @@ "newDatabase": "Nouvelle Base de données", "linkToDatabase": "Lien vers la Base de données" }, - "date": "Date", - "video": { - "label": "Vidéo", - "emptyLabel": "Ajouter une vidéo", - "placeholder": "Collez le lien vidéo", - "copiedToPasteBoard": "Le lien vidéo a été copié dans le presse-papiers", - "insertVideo": "Ajouter une vidéo", - "invalidVideoUrl": "L'URL source n'est pas encore prise en charge.", - "invalidVideoUrlYouTube": "YouTube n'est pas encore pris en charge.", - "supportedFormats": "Formats pris en charge : MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "Fichier", - "uploadTab": "Télécharger", - "uploadMobile": "Choisissez un fichier", - "uploadMobileGallery": "Depuis la galerie photo", - "networkTab": "Intégrer un lien", - "placeholderText": "Télécharger ou intégrer un fichier", - "placeholderDragging": "Glisser le fichier à télécharger", - "dropFileToUpload": "Glisser le fichier à télécharger", - "fileUploadHint": "Glisser un fichier ici pour le télécharger\nou cliquez pour parcourir", - "fileUploadHintSuffix": "Parcourir", - "networkHint": "Coller un lien de fichier", - "networkUrlInvalid": "URL non valide, veuillez corriger l'URL et réessayer", - "networkAction": "Intégrer", - "fileTooBigError": "La taille du fichier est trop grande, veuillez télécharger un fichier d'une taille inférieure à 10 Mo", - "renameFile": { - "title": "Renommer le fichier", - "description": "Entrez le nouveau nom pour ce fichier", - "nameEmptyError": "Le nom du fichier ne peut pas être laissé vide." - }, - "uploadedAt": "Mis en ligne le {}", - "linkedAt": "Lien ajouté le {}", - "failedToOpenMsg": "Impossible d'ouvrir, fichier non trouvé" - }, - "subPage": { - "errors": { - "failedDeletePage": "Impossible de supprimer la page", - "failedCreatePage": "Échec de la création de la page", - "failedMovePage": "Impossible de déplacer la page vers ce document", - "failedDuplicatePage": "Impossible de dupliquer la page", - "failedDuplicateFindView": "Impossible de dupliquer la page - vue d'origine non trouvée" - } - }, - "cannotMoveToItsChildren": "Ne peut pas se déplacer vers ses enfants" + "date": "Date" }, "outlineBlock": { "placeholder": "Table de contenu" @@ -1972,8 +1073,8 @@ "placeholder": "Entrez l'URL de l'image" }, "ai": { - "label": "Générer une image à partir d'AI", - "placeholder": "Veuillez saisir l'invite pour qu'AI génère l'image" + "label": "Générer une image à partir d'OpenAI", + "placeholder": "Veuillez saisir l'invite pour qu'OpenAI génère l'image" }, "stability_ai": { "label": "Générer une image à partir de Stability AI", @@ -1985,8 +1086,7 @@ "invalidImageSize": "La taille de l'image doit être inférieure à 5 Mo", "invalidImageFormat": "Le format d'image n'est pas pris en charge. Formats pris en charge : JPEG, PNG, GIF, SVG", "invalidImageUrl": "URL d'image non valide", - "noImage": "Aucun fichier ou répertoire de ce nom", - "multipleImagesFailed": "Une ou plusieurs images n'ont pas pu être téléchargées, veuillez réessayer" + "noImage": "Aucun fichier ou répertoire de ce nom" }, "embedLink": { "label": "Lien intégré", @@ -1996,36 +1096,20 @@ "label": "Unsplash" }, "searchForAnImage": "Rechercher une image", - "pleaseInputYourOpenAIKey": "veuillez saisir votre clé AI dans la page Paramètres", + "pleaseInputYourOpenAIKey": "veuillez saisir votre clé OpenAI dans la page Paramètres", + "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres", "saveImageToGallery": "Enregistrer l'image", "failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie", "successToAddImageToGallery": "Image ajoutée à la galerie avec succès", "unableToLoadImage": "Impossible de charger l'image", "maximumImageSize": "La taille d'image maximale est 10Mo", "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo", - "imageIsUploading": "L'image est en cours de téléchargement", - "openFullScreen": "Ouvrir en plein écran", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Image précédente", - "nextImageTooltip": "Image suivante", - "zoomOutTooltip": "Zoom arrière", - "zoomInTooltip": "Agrandir", - "changeZoomLevelTooltip": "Changer le niveau de zoom", - "openLocalImage": "Ouvrir l'image", - "downloadImage": "Télécharger l'image", - "closeViewer": "Fermer la visionneuse", - "scalePercentage": "{}%", - "deleteImageTooltip": "Supprimer l'image" - } - }, - "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres" + "imageIsUploading": "L'image est en cours de téléchargement" }, "codeBlock": { "language": { "label": "Langue", - "placeholder": "Choisir la langue", - "auto": "Auto" + "placeholder": "Choisir la langue" }, "copyTooltip": "Copier le contenu du bloc de code", "searchLanguageHint": "Rechercher une langue", @@ -2052,36 +1136,18 @@ "tooltip": "Cliquez pour ouvrir la page" }, "deleted": "Supprimer", - "deletedContent": "Ce document n'existe pas ou a été supprimé", - "noAccess": "Pas d'accès", - "deletedPage": "Page supprimée", - "trashHint": " - à la corbeille", - "morePages": "plus de pages" + "deletedContent": "Ce document n'existe pas ou a été supprimé" }, "toolbar": { "resetToDefaultFont": "Réinitialiser aux valeurs par défaut" }, "errorBlock": { "theBlockIsNotSupported": "La version actuelle ne prend pas en charge ce bloc.", - "clickToCopyTheBlockContent": "Cliquez pour copier le contenu du bloc", - "blockContentHasBeenCopied": "Le contenu du bloc a été copié.", - "parseError": "Une erreur s'est produite lors de l'analyse du bloc {}.", - "copyBlockContent": "Copier le contenu du bloc" - }, - "mobilePageSelector": { - "title": "Sélectionner une page", - "failedToLoad": "Impossible de charger la liste des pages", - "noPagesFound": "Aucune page trouvée" - }, - "attachmentMenu": { - "choosePhoto": "Choisir une photo", - "takePicture": "Prendre une photo", - "chooseFile": "Choisir le fichier" + "blockContentHasBeenCopied": "Le contenu du bloc a été copié." } }, "board": { "column": { - "label": "Colonne", "createNewCard": "Nouveau", "renameGroupTooltip": "Appuyez pour renommer le groupe", "createNewColumn": "Ajouter un nouveau groupe", @@ -2112,7 +1178,6 @@ "ungroupedButtonTooltip": "Contient des cartes qui n'appartiennent à aucun groupe", "ungroupedItemsTitle": "Cliquez pour ajouter au tableau", "groupBy": "Regrouper par", - "groupCondition": "Condition de groupe", "referencedBoardPrefix": "Vue", "notesTooltip": "Notes à l'intérieur", "mobile": { @@ -2120,22 +1185,6 @@ "showGroup": "Afficher le groupe", "showGroupContent": "Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?", "failedToLoad": "Échec du chargement de la vue du tableau" - }, - "dateCondition": { - "weekOf": "Semaine de {} - {}", - "today": "Aujourd'hui", - "yesterday": "Hier", - "tomorrow": "Demain", - "lastSevenDays": "7 derniers jours", - "nextSevenDays": "7 prochains jours", - "lastThirtyDays": "30 derniers jours", - "nextThirtyDays": "30 prochains jours" - }, - "noGroup": "Pas de groupe par propriété", - "noGroupDesc": "Les vues du tableau nécessitent une propriété de regroupement pour pouvoir s'afficher", - "media": { - "cardText": "{} {}", - "fallbackName": "fichiers" } }, "calendar": { @@ -2146,13 +1195,7 @@ "today": "Aujourd'hui", "jumpToday": "Aller à aujourd'hui", "previousMonth": "Mois précédent", - "nextMonth": "Mois prochain", - "views": { - "day": "Jour", - "week": "Semaine", - "month": "Mois", - "year": "Année" - } + "nextMonth": "Mois prochain" }, "mobileEventScreen": { "emptyTitle": "Pas d'événements", @@ -2168,7 +1211,6 @@ "unscheduledEventsTitle": "Événements non planifiés", "clickToAdd": "Cliquez pour ajouter au calendrier", "name": "Disposition du calendrier", - "clickToOpen": "Cliquez pour ouvrir l'évènement", "noDateHint": "Les événements non planifiés s'afficheront ici" }, "referencedCalendarPrefix": "Vue", @@ -2178,13 +1220,10 @@ "errorDialog": { "title": "Erreur @:appName", "howToFixFallback": "Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.", - "howToFixFallbackHint1": "Nous sommes désolés pour la gêne occasionnée ! Soumettez un problème sur notre ", - "howToFixFallbackHint2": " page qui décrit votre erreur.", "github": "Afficher sur GitHub" }, "search": { "label": "Recherche", - "sidebarSearchIcon": "Rechercher et accéder rapidement à une page", "placeholder": { "actions": "Actions de recherche..." } @@ -2242,12 +1281,10 @@ "medium": "Moyen", "mediumDark": "Moyennement foncé", "dark": "Foncé" - }, - "openSourceIconsFrom": "Icônes open source de" + } }, "inlineActions": { "noResults": "Aucun résultat", - "recentPages": "Pages récentes", "pageReference": "Référence de page", "docReference": "Référence de document", "boardReference": "Référence du tableau", @@ -2257,8 +1294,7 @@ "reminder": { "groupTitle": "Rappel", "shortKeyword": "rappeler" - }, - "createPage": "Créer une sous-page « {} »" + } }, "datePicker": { "dateTimeFormatTooltip": "Modifier le format de la date et de l'heure dans les paramètres", @@ -2335,10 +1371,7 @@ }, "error": { "weAreSorry": "Nous sommes désolés", - "loadingViewError": "Nous rencontrons des difficultés pour charger cette vue. Veuillez vérifier votre connexion Internet, actualiser l'application et n'hésitez pas à contacter l'équipe si le problème persiste.", - "syncError": "Les données ne sont pas synchronisées depuis un autre appareil", - "syncErrorHint": "Veuillez rouvrir cette page sur l'appareil sur lequel elle a été modifiée pour la dernière fois, puis l'ouvrir à nouveau sur l'appareil actuel.", - "clickToCopy": "Cliquez pour copier le code d'erreur" + "loadingViewError": "Nous rencontrons des difficultés pour charger cette vue. Veuillez vérifier votre connexion Internet, actualiser l'application et n'hésitez pas à contacter l'équipe si le problème persiste." }, "editor": { "bold": "Gras", @@ -2353,14 +1386,10 @@ "color": "Couleur", "image": "Image", "date": "Date", - "page": "Page", "italic": "Italique", "link": "Lien", "numberedList": "Liste numérotée", "numberedListShortForm": "Numéroté", - "toggleHeading1ShortForm": "Bascule h1", - "toggleHeading2ShortForm": "Bascule h2", - "toggleHeading3ShortForm": "Bascule h3", "quote": "Citation", "strikethrough": "Barré", "text": "Texte", @@ -2411,9 +1440,6 @@ "mobileHeading1": "Titre 1", "mobileHeading2": "Titre 2", "mobileHeading3": "Titre 3", - "mobileHeading4": "Titre 4", - "mobileHeading5": "Titre 5 ", - "mobileHeading6": "Titre 6", "textColor": "Couleur du texte", "backgroundColor": "Couleur du fond", "addYourLink": "Ajoutez votre lien", @@ -2435,7 +1461,7 @@ "auto": "Auto", "cut": "Couper", "copy": "Copier", - "paste": "Coller", + "paste": "Color", "find": "Chercher", "select": "Sélectionner", "selectAll": "Tout sélectionner", @@ -2474,9 +1500,7 @@ }, "favorite": { "noFavorite": "Aucune page favorite", - "noFavoriteHintText": "Faites glisser la page vers la gauche pour l'ajouter à vos favoris", - "removeFromSidebar": "Supprimer de la barre latérale", - "addToSidebar": "Épingler sur la barre latérale" + "noFavoriteHintText": "Faites glisser la page vers la gauche pour l'ajouter à vos favoris" }, "cardDetails": { "notesPlaceholder": "Entrez un / pour insérer un bloc ou commencez à taper" @@ -2513,18 +1537,10 @@ "deleteAccount": { "title": "Supprimer le compte", "subtitle": "Supprimez définitivement votre compte et toutes vos données.", - "description": "Supprimez définitivement votre compte et supprimez l'accès à tous les espaces de travail.", "deleteMyAccount": "Supprimer mon compte", "dialogTitle": "Supprimer le compte", "dialogContent1": "Êtes-vous sûr de vouloir supprimer définitivement votre compte ?", - "dialogContent2": "Cette action ne peut pas être annulée et supprimera l'accès à tous les espaces d'équipe, effaçant l'intégralité de votre compte, y compris les espaces de travail privés, et vous supprimant de tous les espaces de travail partagés.", - "confirmHint1": "Veuillez taper « @:newSettings.myAccount.deleteAccount.confirmHint3 » pour confirmer.", - "confirmHint2": "Je comprends que cette action est irréversible et supprimera définitivement mon compte et toutes les données associées.", - "confirmHint3": "SUPPRIMER MON COMPTE", - "checkToConfirmError": "Vous devez cocher la case pour confirmer la suppression", - "failedToGetCurrentUser": "Impossible d'obtenir l'e-mail de l'utilisateur actuel", - "confirmTextValidationFailed": "Votre texte de confirmation ne correspond pas à « @:newSettings.myAccount.deleteAccount.confirmHint3 »", - "deleteAccountSuccess": "Compte supprimé avec succès" + "dialogContent2": "Cette action ne peut pas être annulée et supprimera l'accès à tous les espaces d'équipe, effaçant l'intégralité de votre compte, y compris les espaces de travail privés, et vous supprimant de tous les espaces de travail partagés." } }, "workplace": { @@ -2537,13 +1553,10 @@ "workplaceIconSubtitle": "Téléchargez une image ou utilisez un emoji pour votre espace de travail. L'icône s'affichera dans votre barre latérale et dans vos notifications", "renameError": "Échec du changement de nom du lieu de travail", "updateIconError": "Échec de la mise à jour de l'icône", - "chooseAnIcon": "Choisissez une icône", "appearance": { "name": "Apparence", "themeMode": { - "auto": "Auto", - "light": "Claire", - "dark": "Sombre" + "auto": "Auto" }, "language": "Langue" } @@ -2554,451 +1567,10 @@ "noNetworkConnected": "Aucun réseau connecté" } }, - "pageStyle": { - "title": "Style de page", - "layout": "Mise en page", - "coverImage": "Image de couverture", - "pageIcon": "Icône de page", - "colors": "Couleurs", - "gradient": "Dégradé", - "backgroundImage": "Image d'arrière-plan", - "presets": "Préréglages", - "photo": "Photo", - "unsplash": "Unsplash", - "pageCover": "Couverture de page", - "none": "Aucun", - "openSettings": "Ouvrir les paramètres", - "photoPermissionTitle": "@:appName souhaite accéder à votre photothèque", - "photoPermissionDescription": "Autoriser l'accès à la photothèque pour le téléchargement d'images.", - "cameraPermissionTitle": "@:appName souhaite accéder à votre caméra", - "cameraPermissionDescription": "@:appName a besoin d'accéder à votre appareil photo pour vous permettre d'ajouter des images à vos documents à partir de l'appareil photo", - "doNotAllow": "Ne pas autoriser", - "image": "Image" - }, "commandPalette": { "placeholder": "Tapez pour rechercher des vues...", - "bestMatches": "Meilleurs résultats", - "recentHistory": "Historique récent", "navigateHint": "naviguer", "loadingTooltip": "Nous recherchons des résultats...", - "betaLabel": "BÊTA", - "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages", - "fromTrashHint": "Depuis la poubelle", - "noResultsHint": "Nous n'avons pas trouvé ce que vous cherchez, essayez avec un autre terme.", - "clearSearchTooltip": "Effacer le champ de recherche" - }, - "space": { - "delete": "Supprimer", - "deleteConfirmation": "Supprimer: ", - "deleteConfirmationDescription": "Toutes les pages de cet espace seront supprimées et déplacées vers la corbeille, et toutes les pages publiées seront dépubliées.", - "rename": "Renommer l'espace", - "changeIcon": "Changer d'icône", - "manage": "Gérer l'espace", - "addNewSpace": "Créer un espace", - "collapseAllSubPages": "Réduire toutes les sous-pages", - "createNewSpace": "Créer un nouvel espace", - "createSpaceDescription": "Créez plusieurs espaces publics et privés pour mieux organiser votre travail.", - "spaceName": "Nom de l'espace", - "spaceNamePlaceholder": "par exemple, marketing, ingénierie, ressources humaines", - "permission": "Autorisation", - "publicPermission": "Publique", - "publicPermissionDescription": "Tous les membres de l'espace de travail avec un accès complet", - "privatePermission": "Privé", - "privatePermissionDescription": "Vous seul pouvez accéder à cet espace", - "spaceIconBackground": "Couleur d'arrière-plan", - "spaceIcon": "Icône", - "dangerZone": "Zone de danger", - "unableToDeleteLastSpace": "Impossible de supprimer le dernier espace", - "unableToDeleteSpaceNotCreatedByYou": "Impossible de supprimer les espaces créés par d'autres", - "enableSpacesForYourWorkspace": "Activer les espaces pour votre espace de travail", - "title": "Espaces", - "defaultSpaceName": "Général", - "upgradeSpaceTitle": "Activer les espaces", - "upgradeSpaceDescription": "Créez plusieurs espaces publics et privés pour mieux organiser votre espace de travail.", - "upgrade": "Mettre à jour", - "upgradeYourSpace": "Créer plusieurs espaces", - "quicklySwitch": "Passer rapidement à l’espace suivant", - "duplicate": "dupliquer l'espace ", - "movePageToSpace": "Déplacer la page vers l'espace", - "cannotMovePageToDatabase": "Impossible de déplacer la page vers la base de données", - "switchSpace": "Changer d'espace", - "spaceNameCannotBeEmpty": "Le nom de l'espace ne peut pas être vide", - "success": { - "deleteSpace": "Espace supprimé avec succès", - "renameSpace": "Espace renommé avec succès", - "duplicateSpace": "Espace dupliqué avec succès", - "updateSpace": "Espace mis à jour avec succès" - }, - "error": { - "deleteSpace": "Impossible de supprimer l'espace", - "renameSpace": "Impossible de renommer l'espace", - "duplicateSpace": "Impossible de dupliquer l'espace", - "updateSpace": "Échec de la mise à jour de l'espace" - }, - "createSpace": "Créer de l'espace", - "manageSpace": "Gérer l'espace", - "renameSpace": "Renommer l'espace", - "mSpaceIconColor": "Couleur de l'icône de l'espace", - "mSpaceIcon": "Icône de l'espace" - }, - "publish": { - "hasNotBeenPublished": "Cette page n'a pas encore été publiée", - "spaceHasNotBeenPublished": "Je n'ai pas encore pris en charge la publication d'un espace", - "reportPage": "Page de rapport", - "databaseHasNotBeenPublished": "La publication d'une base de données n'est pas encore prise en charge.", - "createdWith": "Créé avec", - "downloadApp": "Télécharger AppFlowy", - "copy": { - "codeBlock": "Le contenu du bloc de code a été copié dans le presse-papiers", - "imageBlock": "Le lien de l'image a été copié dans le presse-papiers", - "mathBlock": "L'équation mathématique a été copiée dans le presse-papiers", - "fileBlock": "Le lien du fichier a été copié dans le presse-papiers" - }, - "containsPublishedPage": "Cette page contient une ou plusieurs pages publiées. Si vous continuez, elles ne seront plus publiées. Voulez-vous procéder à la suppression ?", - "publishSuccessfully": "Publié avec succès", - "unpublishSuccessfully": "Dépublié avec succès", - "publishFailed": "Impossible de publier", - "unpublishFailed": "Impossible de dépublier", - "noAccessToVisit": "Pas d'accès à cette page...", - "createWithAppFlowy": "Créer un site Web avec AppFlowy", - "fastWithAI": "Rapide et facile avec l'IA.", - "tryItNow": "Essayez maintenant", - "onlyGridViewCanBePublished": "Seule la vue Grille peut être publiée", - "database": { - "zero": "Publier {} vue sélectionné", - "one": "Publier {} vues sélectionnées", - "many": "Publier {} vues sélectionnées", - "other": "Publier {} vues sélectionnées" - }, - "mustSelectPrimaryDatabase": "La vue principale doit être sélectionnée", - "noDatabaseSelected": "Aucune base de données sélectionnée, veuillez sélectionner au moins une base de données.", - "unableToDeselectPrimaryDatabase": "Impossible de désélectionner la base de données principale", - "saveThisPage": "Sauvegarder cette page", - "duplicateTitle": "Où souhaitez-vous ajouter", - "selectWorkspace": "Sélectionnez un espace de travail", - "addTo": "Ajouter à", - "duplicateSuccessfully": "Dupliqué avec succès. Vous souhaitez consulter les documents ?", - "duplicateSuccessfullyDescription": "Vous n'avez pas l'application ? Le téléchargement commencera automatiquement après avoir cliqué sur « Télécharger ».", - "downloadIt": "Télécharger", - "openApp": "Ouvrir dans l'application", - "duplicateFailed": "Duplication échouée", - "membersCount": { - "zero": "Aucun membre", - "one": "1 membre", - "many": "{count} membres", - "other": "{count} membres" - }, - "useThisTemplate": "Utiliser le modèle" - }, - "web": { - "continue": "Continuer", - "or": "ou", - "continueWithGoogle": "Continuer avec Google", - "continueWithGithub": "Continuer avec GitHub", - "continueWithDiscord": "Continuer avec Discord", - "continueWithApple": "Continuer avec Apple ", - "moreOptions": "Plus d'options", - "collapse": "Réduire", - "signInAgreement": "En cliquant sur « Continuer » ci-dessus, vous avez accepté les conditions d'utilisation d'AppFlowy.", - "and": "et", - "termOfUse": "Termes", - "privacyPolicy": "politique de confidentialité", - "signInError": "Erreur de connexion", - "login": "Inscrivez-vous ou connectez-vous", - "fileBlock": { - "uploadedAt": "Mis en ligne le {time}", - "linkedAt": "Lien ajouté le {time}", - "empty": "Envoyer ou intégrer un fichier", - "uploadFailed": "Échec du téléchargement, veuillez réessayer", - "retry": "Réessayer" - }, - "importNotion": "Importer depuis Notion", - "import": "Importer", - "importSuccess": "Téléchargé avec succès", - "importSuccessMessage": "Nous vous informerons lorsque l'importation sera terminée. Vous pourrez ensuite visualiser vos pages importées dans la barre latérale.", - "importFailed": "L'importation a échoué, veuillez vérifier le format du fichier", - "dropNotionFile": "Déposez votre fichier zip Notion ici pour le télécharger, ou cliquez pour parcourir", - "error": { - "pageNameIsEmpty": "Le nom de la page est vide, veuillez réessayer" - } - }, - "globalComment": { - "comments": "Commentaires", - "addComment": "Ajouter un commentaire", - "reactedBy": "réagi par", - "addReaction": "Ajouter une réaction", - "reactedByMore": "et {count} autres", - "showSeconds": { - "one": "Il y a 1 seconde", - "other": "Il y a {count} secondes", - "zero": "Tout à l' heure", - "many": "Il y a {count} secondes" - }, - "showMinutes": { - "one": "Il y a 1 minute", - "other": "Il y a {count} minutes", - "many": "Il y a {count} minutes" - }, - "showHours": { - "one": "il y a 1 heure", - "other": "Il y a {count} heures", - "many": "Il y a {count} heures" - }, - "showDays": { - "one": "Il y a 1 jour", - "other": "Il y a {count} jours", - "many": "Il y a {count} jours" - }, - "showMonths": { - "one": "Il y a 1 mois", - "other": "Il y a {count} mois", - "many": "Il y a {count} mois" - }, - "showYears": { - "one": "Il y a 1 an", - "other": "Il y a {count} ans", - "many": "Il y a {count} ans" - }, - "reply": "Répondre", - "deleteComment": "Supprimer le commentaire", - "youAreNotOwner": "Vous n'êtes pas le propriétaire de ce commentaire", - "confirmDeleteDescription": "Etes-vous sûr de vouloir supprimer ce commentaire ?", - "hasBeenDeleted": "Supprimé", - "replyingTo": "En réponse à", - "noAccessDeleteComment": "Vous n'êtes pas autorisé à supprimer ce commentaire", - "collapse": "Réduire", - "readMore": "En savoir plus", - "failedToAddComment": "Problème lors de l'ajout du commentaire", - "commentAddedSuccessfully": "Commentaire ajouté avec succès.", - "commentAddedSuccessTip": "Vous venez d'ajouter ou de répondre à un commentaire. Souhaitez-vous passer en haut de la page pour voir les derniers commentaires ?" - }, - "template": { - "asTemplate": "En tant que modèle", - "name": "Nom du modèle", - "description": "Description du modèle", - "about": "À propos du modèle", - "deleteFromTemplate": "Supprimer des modèles", - "preview": "Aperçu du modèle", - "categories": "Catégories de modèles", - "relatedTemplates": "Modèles associés", - "requiredField": "{field} est requis", - "addCategory": "Ajouter \"{category}\"", - "addNewCategory": "Ajouter une nouvelle catégorie", - "addNewCreator": "Ajouter un nouvel auteur", - "deleteCategory": "Supprimer la catégorie", - "editCategory": "Modifier la catégorie", - "editCreator": "Modifier le créateur", - "category": { - "name": "Nom de la catégorie", - "icon": "Icône de la catégorie", - "bgColor": "Couleur d'arrière-plan de la catégorie", - "priority": "Priorité de la catégorie", - "desc": "Description de la catégorie", - "type": "Type de catégorie", - "icons": "Icônes de catégorie", - "colors": "Couleurs de catégorie ", - "byUseCase": "Par cas d'utilisation", - "byFeature": "Par fonctionnalité", - "deleteCategory": "Supprimer la catégorie", - "deleteCategoryDescription": "Êtes-vous sûr de vouloir supprimer cette catégorie ?", - "typeToSearch": "Tapez pour rechercher des catégories..." - }, - "creator": { - "label": "Auteur du modèle", - "name": "Nom de l'auteur", - "avatar": "Avatar de l'auteur", - "accountLinks": "Liens vers le compte de l'auteur", - "uploadAvatar": "Cliquez pour ajouter un avatar", - "deleteCreator": "Supprimer l'auteur", - "deleteCreatorDescription": "Êtes-vous sûr de vouloir supprimer cet auteur ?", - "typeToSearch": "Tapez pour rechercher des auteurs..." - }, - "uploadSuccess": "Modèle envoyé avec succès", - "uploadSuccessDescription": "Votre modèle a été envoyé avec succès. Vous pouvez maintenant le visualiser dans la galerie de modèles.", - "viewTemplate": "Voir le modèle", - "deleteTemplate": "Supprimer le modèle", - "deleteSuccess": "Modèle supprimé avec succès", - "deleteTemplateDescription": "Êtes-vous sûr de vouloir supprimer ce modèle ?", - "addRelatedTemplate": "Ajouter un modèle associé", - "removeRelatedTemplate": "Supprimer le modèle associé", - "uploadAvatar": "Envoyer l'avatar", - "searchInCategory": "Rechercher dans {category}", - "label": "Modèle" - }, - "fileDropzone": { - "dropFile": "Cliquez ou faites glisser le fichier vers cette zone pour l'envoyer", - "uploading": "Envoi en cours...", - "uploadFailed": "Envoi échoué", - "uploadSuccess": "Envoi réussi", - "uploadSuccessDescription": "Le fichier a été envoyé avec succès", - "uploadFailedDescription": "L'envoi du fichier a échoué", - "uploadingDescription": "Le fichier est en cours d'envoi" - }, - "gallery": { - "preview": "Ouvrir en plein écran", - "copy": "Copier", - "download": "Télécharger", - "prev": "Précédent", - "next": "Suivant", - "resetZoom": "Réinitialiser le zoom", - "zoomIn": "Agrandir", - "zoomOut": "Rétrécir" - }, - "invitation": { - "join": "Rejoindre", - "on": "sur", - "invitedBy": "Invité par", - "membersCount": { - "zero": "{count} membres", - "one": "{count} membre", - "many": "{count} membres", - "other": "{count} membres" - }, - "tip": "Vous avez été invité à rejoindre cet espace de travail avec les coordonnées ci-dessous. Si celles-ci sont incorrectes, contactez votre administrateur pour renvoyer l'invitation.", - "joinWorkspace": "Rejoindre l'espace de travail", - "success": "Vous avez rejoint avec succès l'espace de travail", - "successMessage": "Vous pouvez désormais accéder à toutes les pages et espaces de travail qu'il contient.", - "openWorkspace": "Ouvrir AppFlowy", - "alreadyAccepted": "Vous avez déjà accepté l'invitation", - "errorModal": { - "title": "Quelque chose s'est mal passé", - "description": "Il est possible que votre compte actuel {email} n'ait pas accès à cet espace de travail. Veuillez vous connecter avec le compte approprié ou contacter le propriétaire de l'espace de travail pour obtenir de l'aide.", - "contactOwner": "Contacter le propriétaire", - "close": "Retour à l'accueil", - "changeAccount": "Changer de compte" - } - }, - "requestAccess": { - "title": "Pas d'accès à cette page", - "subtitle": "Vous pouvez demander l'accès au propriétaire de cette page. Une fois approuvé, vous pourrez consulter la page.", - "requestAccess": "Demande d'accès", - "backToHome": "Retour à l'accueil", - "tip": "Vous êtes actuellement connecté en tant que<link/> .", - "mightBe": "Vous pourriez avoir besoin de<login/> avec un compte différent.", - "successful": "Demande envoyée avec succès", - "successfulMessage": "Vous serez averti une fois que le propriétaire aura approuvé votre demande.", - "requestError": "Échec de la demande d'accès", - "repeatRequestError": "Vous avez déjà demandé l'accès à cette page" - }, - "approveAccess": { - "title": "Approuver la demande d'adhésion à l'espace de travail", - "requestSummary": "<user/>demandes d'adhésion<workspace/> et accès<page/>", - "upgrade": "mise à niveau", - "downloadApp": "Télécharger AppFlowy", - "approveButton": "Approuver", - "approveSuccess": "Approuvé avec succès", - "approveError": "Échec de l'approbation, assurez-vous que la limite du plan d'espace de travail n'est pas dépassée", - "getRequestInfoError": "Impossible d'obtenir les informations de la demande", - "memberCount": { - "zero": "Aucun membre", - "one": "1 membre", - "many": "{count} membres", - "other": "{count} membres" - }, - "alreadyProTitle": "Vous avez atteint la limite du plan d'espace de travail", - "alreadyProMessage": "Demandez-leur de contacter<email/> pour débloquer plus de membres", - "repeatApproveError": "Vous avez déjà approuvé cette demande", - "ensurePlanLimit": "Assurez-vous que la limite du plan d'espace de travail n'est pas dépassée. Si la limite est dépassée, envisagez<upgrade/> le plan de l'espace de travail ou<download/> .", - "requestToJoin": "demandé à rejoindre", - "asMember": "en tant que membre" - }, - "upgradePlanModal": { - "title": "Passer à Pro", - "message": "{name} a atteint la limite de membres gratuits. Passez au plan Pro pour inviter plus de membres.", - "upgradeSteps": "Comment mettre à niveau votre plan sur AppFlowy :", - "step1": "1. Accédez aux paramètres", - "step2": "2. Cliquez sur « Offre »", - "step3": "3. Sélectionnez « Changer d'offre »", - "appNote": "Note: ", - "actionButton": "Mettre à niveau", - "downloadLink": "Télécharger l'application", - "laterButton": "Plus tard", - "refreshNote": "Après une mise à niveau réussie, cliquez sur<refresh/> pour activer vos nouvelles fonctionnalités.", - "refresh": "ici" - }, - "breadcrumbs": { - "label": "Fil d'Ariane" - }, - "time": { - "justNow": "A l'instant", - "seconds": { - "one": "1 seconde", - "other": "{count} secondes" - }, - "minutes": { - "one": "1 minute", - "other": "{compter} minutes" - }, - "hours": { - "one": "1 heure", - "other": "{count} heures" - }, - "days": { - "one": "1 jour", - "other": "{count} jours" - }, - "weeks": { - "one": "1 semaine", - "other": "{count} semaines" - }, - "months": { - "one": "1 mois", - "other": "{count} mois" - }, - "years": { - "one": "1 an", - "other": "{count} années" - }, - "ago": "il y a", - "yesterday": "Hier", - "today": "Aujourd'hui" - }, - "members": { - "zero": "Aucun membre", - "one": "1 membre", - "many": "{count} membres", - "other": "{count} membres" - }, - "tabMenu": { - "close": "Fermer", - "closeDisabledHint": "Impossible de fermer un onglet épinglé, veuillez d'abord le désépingler", - "closeOthers": "Fermer les autres onglets", - "closeOthersHint": "Cela fermera tous les onglets non épinglés sauf celui-ci", - "closeOthersDisabledHint": "Tous les onglets sont épinglés, je ne trouve aucun onglet à fermer", - "favorite": "Favori", - "unfavorite": "Non favori", - "favoriteDisabledHint": "Impossible de mettre en favori cette vue", - "pinTab": "Épingler", - "unpinTab": "Désépingler" - }, - "openFileMessage": { - "success": "Fichier ouvert avec succès", - "fileNotFound": "Fichier introuvable", - "noAppToOpenFile": "Aucune application pour ouvrir ce fichier", - "permissionDenied": "Aucune autorisation d'ouvrir ce fichier", - "unknownError": "Échec de l'ouverture du fichier" - }, - "inviteMember": { - "requestInviteMembers": "Inviter à votre espace de travail", - "inviteFailedMemberLimit": "La limite de membres a été atteinte, veuillez ", - "upgrade": "mettre à niveau", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "Envoyer des invitations", - "inviteAlready": "Vous avez déjà inviter cet email: {email}", - "inviteSuccess": "Invitation envoyée avec succès", - "description": "Saisissez les adresses emails en les séparant par une virgule. Les frais sont basés sur le nombre de membres.", - "emails": "Email" - }, - "quickNote": { - "label": "Note Rapide", - "quickNotes": "Notes Rapides", - "search": "Chercher les Notes Rapides", - "collapseFullView": "Fermer la vue d'ensemble", - "expandFullView": "Ouvrir la vue d'ensemble", - "createFailed": "Échec de la création de la Note Rapide", - "quickNotesEmpty": "Aucune Notes Rapides", - "emptyNote": "Note vide", - "deleteNotePrompt": "La note sélectionnée sera supprimée définitivement. Êtes-vous sûr de vouloir la supprimer ?", - "addNote": "Nouvelle Note" + "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ga-IE.json b/frontend/resources/translations/ga-IE.json deleted file mode 100644 index 1520e46fea..0000000000 --- a/frontend/resources/translations/ga-IE.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "appName": "Appflowy", - "defaultUsername": "Liom", - "welcomeText": "Fáilte go @:appName", - "welcomeTo": "Fáilte chuig", - "githubStarText": "Réalta ar GitHub", - "subscribeNewsletterText": "Liostáil le Nuachtlitir", - "letsGoButtonText": "Tús Tapa", - "title": "Teideal", - "youCanAlso": "Is féidir leat freisin", - "and": "agus", - "failedToOpenUrl": "Theip ar oscailt an url: {}", - "blockActions": { - "addBelowTooltip": "Cliceáil chun cur leis thíos", - "addAboveCmd": "Alt+cliceáil", - "addAboveMacCmd": "Option+cliceáil", - "addAboveTooltip": "a chur thuas", - "dragTooltip": "Tarraing chun bogadh", - "openMenuTooltip": "Cliceáil chun an roghchlár a oscailt" - }, - "signUp": { - "buttonText": "Cláraigh", - "title": "Cláraigh le @:appName", - "getStartedText": "Faigh Tosaigh", - "emptyPasswordError": "Ní féidir le pasfhocal a bheith folamh", - "repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh", - "unmatchedPasswordError": "Ní ionann pasfhocal athdhéanta agus pasfhocal", - "alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?", - "emailHint": "Ríomhphost", - "passwordHint": "Pasfhocal", - "repeatPasswordHint": "Déan pasfhocal arís", - "signUpWith": "Cláraigh le:" - }, - "signIn": { - "loginTitle": "Logáil isteach ar @:appName", - "loginButtonText": "Logáil isteach", - "loginStartWithAnonymous": "Lean ar aghaidh le seisiún gan ainm", - "continueAnonymousUser": "Lean ar aghaidh le seisiún gan ainm", - "anonymous": "Gan ainm", - "buttonText": "Sínigh Isteach", - "signingInText": "Ag síniú isteach...", - "forgotPassword": "Pasfhocal Dearmadta?", - "emailHint": "Ríomhphost", - "passwordHint": "Pasfhocal", - "dontHaveAnAccount": "Nach bhfuil cuntas agat?", - "createAccount": "Cruthaigh cuntas", - "repeatPasswordEmptyError": "Ní féidir an pasfhocal athdhéanta a bheith folamh", - "unmatchedPasswordError": "Ní hionann pasfhocal athdhéanta agus pasfhocal", - "syncPromptMessage": "Seans go dtógfaidh sé tamall na sonraí a shioncronú. Ná dún an leathanach seo, le do thoil", - "or": "NÓ", - "signInWithGoogle": "Lean ar aghaidh le Google", - "signInWithGithub": "Lean ar aghaidh le GitHub", - "signInWithDiscord": "Lean ar aghaidh le Discord", - "signInWithApple": "Lean ar aghaidh le Apple", - "continueAnotherWay": "Lean ar aghaidh ar bhealach eile", - "signUpWithGoogle": "Cláraigh le Google", - "signUpWithGithub": "Cláraigh le Github", - "signUpWithDiscord": "Cláraigh le Discord", - "signInWith": "Lean ar aghaidh le:", - "signInWithEmail": "Lean ar aghaidh le Ríomhphost", - "signInWithMagicLink": "Lean ort", - "signUpWithMagicLink": "Cláraigh le Magic Link", - "pleaseInputYourEmail": "Cuir isteach do sheoladh ríomhphoist", - "settings": "Socruithe", - "magicLinkSent": "Magic Link seolta!", - "invalidEmail": "Cuir isteach seoladh ríomhphoist bailí", - "alreadyHaveAnAccount": "An bhfuil cuntas agat cheana féin?", - "logIn": "Logáil isteach", - "generalError": "Chuaigh rud éigin mícheart. Bain triail eile as ar ball", - "limitRateError": "Ar chúiseanna slándála, ní féidir leat nasc draíochta a iarraidh ach gach 60 soicind" - }, - "workspace": { - "chooseWorkspace": "Roghnaigh do spás oibre", - "defaultName": "Mo Spás Oibre", - "create": "Cruthaigh spás oibre", - "new": "Spás oibre nua", - "importFromNotion": "Iompórtáil ó Notion", - "learnMore": "Foghlaim níos mó", - "reset": "Athshocraigh spás oibre", - "renameWorkspace": "Athainmnigh spás oibre", - "workspaceNameCannotBeEmpty": "Ní féidir leis an ainm spás oibre a bheith folamh", - "hint": "spás oibre", - "notFoundError": "Spás oibre gan aimsiú", - "errorActions": { - "reportIssue": "Tuairiscigh saincheist", - "reportIssueOnGithub": "Tuairiscigh ceist faoi Github", - "exportLogFiles": "Easpórtáil comhaid logáil", - "reachOut": "Bhaint amach le Discord" - }, - "menuTitle": "Spásanna oibre", - "createSuccess": "Cruthaíodh spás oibre go rathúil", - "leaveCurrentWorkspace": "Fág spás óibre" - }, - "shareAction": { - "buttonText": "Comhroinn", - "workInProgress": "Ag teacht go luath", - "markdown": "Markdown", - "html": "HTML", - "clipboard": "Cóipeáil chuig an ngearrthaisce", - "csv": "CSV", - "copyLink": "Cóipeáil nasc", - "publishToTheWeb": "Foilsigh don Ghréasán", - "publishToTheWebHint": "Cruthaigh suíomh Gréasáin le AppFlowy", - "publish": "Foilsigh", - "unPublish": "Dífhoilsiú", - "visitSite": "Tabhair cuairt ar an suíomh", - "publishTab": "Foilsigh", - "shareTab": "Comhroinn" - }, - "moreAction": { - "small": "beag", - "medium": "meánach", - "large": "mór", - "fontSize": "Clómhéid", - "import": "Iompórtáil", - "createdAt": "Cruthaithe: {}", - "deleteView": "Scrios" - } -} diff --git a/frontend/resources/translations/he.json b/frontend/resources/translations/he.json deleted file mode 100644 index 6c40f88947..0000000000 --- a/frontend/resources/translations/he.json +++ /dev/null @@ -1,2083 +0,0 @@ -{ - "appName": "AppFlowy", - "defaultUsername": "אני", - "welcomeText": "ברוך בואך אל @:appName", - "welcomeTo": "ברוך בואך אל", - "githubStarText": "כוכב ב־GitHub", - "subscribeNewsletterText": "הרשמה לרשימת הדיוור", - "letsGoButtonText": "התחלה זריזה", - "title": "כותרת", - "youCanAlso": "אפשר גם", - "and": "וגם", - "failedToOpenUrl": "פתיחת הכתובת נכשלה: {}", - "blockActions": { - "addBelowTooltip": "יש ללחוץ כדי להוסיף להלן", - "addAboveCmd": "Alt+לחיצה", - "addAboveMacCmd": "Option+לחיצה", - "addAboveTooltip": "כדי להוסיף מעל", - "dragTooltip": "יש לגרור כדי להזיז", - "openMenuTooltip": "יש ללחוץ כדי לפתוח תפריט" - }, - "signUp": { - "buttonText": "הרשמה", - "title": "הרשמה ל־@:appName", - "getStartedText": "מאיפה מתחילים", - "emptyPasswordError": "הסיסמה לא יכולה להיות ריקה", - "repeatPasswordEmptyError": "הסיסמה החוזרת לא יכולה להיות ריקה", - "unmatchedPasswordError": "הסיסמה החוזרת לא זהה לסיסמה", - "alreadyHaveAnAccount": "כבר יש לך חשבון?", - "emailHint": "דוא״ל", - "passwordHint": "סיסמה", - "repeatPasswordHint": "סיסמה חוזרת", - "signUpWith": "הרשמה עם:" - }, - "signIn": { - "loginTitle": "כניסה אל @:appName", - "loginButtonText": "כניסה", - "loginStartWithAnonymous": "התחלה אלמונית", - "continueAnonymousUser": "המשך התהליך בצורה אלמונית", - "buttonText": "כניסה", - "signingInText": "מתבצעת כניסה…", - "forgotPassword": "שכחת סיסמה?", - "emailHint": "דוא״ל", - "passwordHint": "סיסמה", - "dontHaveAnAccount": "אין לך חשבון?", - "createAccount": "ליצור חשבון", - "repeatPasswordEmptyError": "הסיסמה החוזרת לא יכולה להיות ריקה", - "unmatchedPasswordError": "הסיסמה החוזרת לא זהה לסיסמה", - "syncPromptMessage": "סנכרון הנתונים עלול לארוך זמן מה. נא לא לסגור את העמוד הזה", - "or": "או", - "signInWithGoogle": "להיכנס עם Google", - "signInWithGithub": "להיכנס עם Github", - "signInWithDiscord": "להיכנס עם Discord", - "signUpWithGoogle": "להירשם עם Google", - "signUpWithGithub": "להירשם עם Github", - "signUpWithDiscord": "להירשם עם Discord", - "signInWith": "להיכנס עם:", - "signInWithEmail": "להיכנס עם דוא״ל", - "signInWithMagicLink": "כניסה עם קישור קסם", - "signUpWithMagicLink": "הרשמה עם קישור קסם", - "pleaseInputYourEmail": "נא למלא את כתובת הדוא״ל שלך", - "settings": "הגדרות", - "magicLinkSent": "קישור קסם נשלח!", - "invalidEmail": "נא למלא כתובת דוא״ל תקפה", - "alreadyHaveAnAccount": "כבר יש לך חשבון?", - "logIn": "להיכנס", - "generalError": "משהו השתבש. נא לנסות שוב מאוחר יותר", - "limitRateError": "מטעמי אבטחת מידע, אפשר לבקש קישור קסם כל 60 שניות", - "magicLinkSentDescription": "קישור קסם נשלח לדוא״ל שלך. יש ללחוץ על הקישור כדי להשלים את הכניסה שלך למערכת.הקישור יפוג תוך 5 דקות." - }, - "workspace": { - "chooseWorkspace": "נא לבחור את מרחב העבודה שלך", - "create": "יצירת מרחב עבודה", - "reset": "איפוס מרחב עבודה", - "renameWorkspace": "שינוי שם מרחב עבודה", - "resetWorkspacePrompt": "איפוס מרחב העבודה ימחק את כל הדפים והנתונים שבו. לאפס את מרחב העבודה? לחלופין, אפשר ליצור קשר עם צוות התמיכה לשחזור מרחב העבודה", - "hint": "מרחב עבודה", - "notFoundError": "לא נמצא מרחב עבודה", - "failedToLoad": "משהו השתבש! טעינת מרחב העבודה נכשלה. כדאי לנסות לסגור את כל העותקים הפתוחים של @:appName ולנסות שוב.", - "errorActions": { - "reportIssue": "דיווח על תקלה", - "reportIssueOnGithub": "דיווח על תקלה דרך GitHub", - "exportLogFiles": "ייצוא קובצי יומנים", - "reachOut": "פנייה אלינו דרך Discord" - }, - "menuTitle": "מרחבי עבודה", - "deleteWorkspaceHintText": "למחוק את מרחב העבודה? הפעולה הזאת היא בלתי הפיכה, ועמודים שפרסמת יוסתרו.", - "createSuccess": "מרחב העבודה נוצר בהצלחה", - "createFailed": "יצירת מרחב העבודה נכשלה", - "createLimitExceeded": "הגעת לכמות מרחבי העבודה המרבית המותרת לחשבון שלך. כדי להוסיף מרחבי עבודה נוספים ולהמשיך בעבודה שלך, נא לבקש ב־GitHub", - "deleteSuccess": "מרחב העבודה נמחק בהצלחה", - "deleteFailed": "מחיקת מרחב העבודה נכשלה", - "openSuccess": "פתיחת מרחב העבודה הצליחה", - "openFailed": "פתיחת מרחב העבודה נכשלה", - "renameSuccess": "שם מרחב העבודה השתנה בהצלחה", - "renameFailed": "שינוי שם מרחב העבודה נכשל", - "updateIconSuccess": "סמל מרחב העבודה עודכן בהצלחה", - "updateIconFailed": "עדכון סמל מרחב העבודה נכשל", - "cannotDeleteTheOnlyWorkspace": "לא ניתן למחוק את מרחב העבודה היחיד", - "fetchWorkspacesFailed": "משיכת מרחבי העבודה נכשלה", - "leaveCurrentWorkspace": "יציאה ממרחב העבודה", - "leaveCurrentWorkspacePrompt": "לעזוב את סביבת העבודה הנוכחית?" - }, - "shareAction": { - "buttonText": "שיתוף", - "workInProgress": "בקרוב", - "markdown": "Markdown", - "html": "HTML", - "clipboard": "העתקה ללוח הגזירים", - "csv": "CSV", - "copyLink": "העתקת קישור", - "publishToTheWeb": "פרסום לאינטרנט", - "publishToTheWebHint": "יצירת אתר עם", - "publish": "פרסום", - "unPublish": "הסתרה", - "visitSite": "ביקור באתר", - "exportAsTab": "ייצוא בתור", - "publishTab": "פרסום", - "shareTab": "שיתוף" - }, - "moreAction": { - "small": "קטן", - "medium": "בינוני", - "large": "גדול", - "fontSize": "גודל גופן", - "import": "ייבוא", - "moreOptions": "אפשרויות נוספות", - "wordCount": "כמות מילים: {}", - "charCount": "כמות תווים: {}", - "createdAt": "מועד יצירה: {}", - "deleteView": "מחיקה", - "duplicateView": "שכפול" - }, - "importPanel": { - "textAndMarkdown": "טקסט ו־Markdown", - "documentFromV010": "מסמך מגרסה 0.1.0", - "databaseFromV010": "מסד נתונים מגרסה 0.1.0", - "csv": "CSV", - "database": "מסד נתונים" - }, - "disclosureAction": { - "rename": "שינוי שם", - "delete": "מחיקה", - "duplicate": "שכפול", - "unfavorite": "הסרה מהמועדפים", - "favorite": "הוספה למועדפים", - "openNewTab": "פתיחה בלשונית חדשה", - "moveTo": "העברה אל", - "addToFavorites": "הוספה למועדפים", - "copyLink": "העתקת קישור", - "changeIcon": "החלפת סמל", - "collapseAllPages": "צמצום כל תת־העמודים" - }, - "blankPageTitle": "עמוד ריק", - "newPageText": "עמוד חדש", - "newDocumentText": "מסמך חדש", - "newGridText": "טבלה חדשה", - "newCalendarText": "לוח שנה חדש", - "newBoardText": "לוח חדש", - "chat": { - "newChat": "שיחה עם בינה מלאכותית", - "inputMessageHint": "שליחת הודעה לבינה המלאכותית של @:appName", - "unsupportedCloudPrompt": "היכולת הזאת זמינה רק עם @:appName בענן", - "relatedQuestion": "קשורים", - "serverUnavailable": "השירות אינו זמין באופן זמני. נא לנסות שוב מאוחר יותר.", - "aiServerUnavailable": "🌈 אוי לא! 🌈. חד־קרן אכל לנו את התגובה. נא לנסות שוב!", - "clickToRetry": "נא ללחוץ לניסיון חוזר", - "regenerateAnswer": "יצירה מחדש", - "question1": "איך להשתמש בקנבן לניהול משימות", - "question2": "הסבר על שיטת החתימה למטרה", - "question3": "למה להשתמש ב־Rust", - "question4": "מתכון ממה שיש לי במטבח", - "aiMistakePrompt": "בינה מלאכותית יכולה לטעות. כדאי לאשש את המידע." - }, - "trash": { - "text": "אשפה", - "restoreAll": "שחזור של הכול", - "deleteAll": "מחיקה של הכול", - "pageHeader": { - "fileName": "שם קובץ", - "lastModified": "שינוי אחרון", - "created": "נוצר" - }, - "confirmDeleteAll": { - "title": "למחוק את כל העמודים שבאשפה?", - "caption": "זאת פעולה בלתי הפיכה." - }, - "confirmRestoreAll": { - "title": "לשחזר את כל העמודים מהאשפה?", - "caption": "זאת פעולה בלתי הפיכה." - }, - "mobile": { - "actions": "פעולות אשפה", - "empty": "סל האשפה ריק", - "emptyDescription": "אין לך קבצים שנמחקו", - "isDeleted": "נמחק", - "isRestored": "משוחזר" - }, - "confirmDeleteTitle": "למחוק את העמוד הזה לצמיתות?" - }, - "deletePagePrompt": { - "text": "העמוד הזה הוא באשפה", - "restore": "שחזור עמוד", - "deletePermanent": "למחוק לצמיתות" - }, - "dialogCreatePageNameHint": "שם העמוד", - "questionBubble": { - "shortcuts": "מקשי קיצור", - "whatsNew": "מה חדש?", - "markdown": "Markdown", - "debug": { - "name": "פרטי ניפוי שגיאות", - "success": "פרטי ניפוי השגיאות הועתקו ללוח הגזירים!", - "fail": "לא ניתן להעתיק את פרטי ניפוי השגיאות ללוח הגזירים" - }, - "feedback": "משוב", - "help": "עזרה ותמיכה" - }, - "menuAppHeader": { - "moreButtonToolTip": "הסרה, שינוי שם ועוד…", - "addPageTooltip": "הוספת עמוד בפנים במהירות", - "defaultNewPageName": "ללא שם", - "renameDialog": "שינוי שם" - }, - "noPagesInside": "אין עמודים בפנים", - "toolbar": { - "undo": "הסגה", - "redo": "ביצוע מחדש", - "bold": "מודגש", - "italic": "נטוי", - "underline": "קו תחתי", - "strike": "קו חוצה", - "numList": "רשימה ממוספרת", - "bulletList": "רשימת תבליטים", - "checkList": "רשימת סימונים", - "inlineCode": "קוד מוטבע", - "quote": "מקטע ציטוט", - "header": "כותרת עליונה", - "highlight": "הדגשה", - "color": "צבע", - "addLink": "הוספת קישור", - "link": "קישור" - }, - "tooltip": { - "lightMode": "מעבר למצב בהיר", - "darkMode": "מעבר למצב כהה", - "openAsPage": "פתיחה כעמוד", - "addNewRow": "הוספת שורה חדשה", - "openMenu": "לחיצה תפתח את התפריט", - "dragRow": "לחיצה ארוכה תסדר את השורה מחדש", - "viewDataBase": "הצגת מסד הנתונים", - "referencePage": "זאת הפנייה אל {name}", - "addBlockBelow": "הוספת מקטע למטה", - "aiGenerate": "יצירה" - }, - "sideBar": { - "closeSidebar": "סגירת סרגל צד", - "openSidebar": "פתיחת סרגל צד", - "personal": "אישי", - "private": "פרטי", - "workspace": "מרחב עבודה", - "favorites": "מועדפים", - "clickToHidePrivate": "לחיצה תסתיר את המרחב הפרטי\nעמודים שיצרת כאן הם לעיניך בלבד", - "clickToHideWorkspace": "לחיצה תסתיר את מרחב העבודה\nעמודים שיצרת כאן יהיו גלויים בפני כל החברים", - "clickToHidePersonal": "לחיצה תסתיר את המרחב האישי", - "clickToHideFavorites": "לחיצה תסתיר מרחב מועדף", - "addAPage": "הוספת עמוד חדש", - "addAPageToPrivate": "הוספת עמוד למרחב פרטי", - "addAPageToWorkspace": "הוספת עמוד למרחב עבודה פרטי", - "recent": "אחרונים", - "today": "היום", - "thisWeek": "השבוע", - "others": "מועדפים אחרים", - "justNow": "כרגע", - "minutesAgo": "לפני {count} דקות", - "lastViewed": "צפייה אחרונה", - "favoriteAt": "הוספה למועדפים ב־", - "emptyRecent": "אין מסמכים אחרונים", - "emptyRecentDescription": "בעת צפייה במסמכים הם יופיעו כאן לאיתור פשוט יותר", - "emptyFavorite": "אין מסמכים מועדפים", - "emptyFavoriteDescription": "אפשר להתחיל לעיין ולסמן מסמכים כמועדפים. הם יופיעו כאן כדי להקל על הגישה אליהם!", - "removePageFromRecent": "להסיר את העמוד הזה מהאחרונים?", - "removeSuccess": "הוסר בהצלחה", - "favoriteSpace": "מועדפים", - "RecentSpace": "אחרונים", - "Spaces": "מרחבים" - }, - "notifications": { - "export": { - "markdown": "פתקית יוצאה ל־Markdown", - "path": "מסמכיםflowy" - } - }, - "contactsPage": { - "title": "אנשי קשר", - "whatsHappening": "מה קורה השבוע?", - "addContact": "הוספת איש קשר", - "editContact": "עריכת איש קשר" - }, - "button": { - "ok": "אישור", - "confirm": "אישור", - "done": "בוצע", - "cancel": "ביטול", - "signIn": "כניסה", - "signOut": "יציאה", - "complete": "השלמה", - "save": "שמירה", - "generate": "יצירה", - "esc": "ESC", - "keep": "להשאיר", - "tryAgain": "לנסות שוב", - "discard": "התעלמות", - "replace": "החלפה", - "insertBelow": "הוספה מתחת", - "insertAbove": "הוספה מעל", - "upload": "העלאה", - "edit": "עריכה", - "delete": "מחיקה", - "duplicate": "שכפול", - "putback": "החזרה למקום", - "update": "עדכון", - "share": "שיתוף", - "removeFromFavorites": "הסרה מהמועדפים", - "removeFromRecent": "הסרה מהאחרונים", - "addToFavorites": "הוספה למועדפים", - "rename": "שינוי שם", - "helpCenter": "מרכז העזרה", - "add": "הוספה", - "yes": "כן", - "clear": "פינוי", - "remove": "להסיר", - "dontRemove": "לא להסיר", - "copyLink": "העתקת קישור", - "align": "יישור", - "login": "כניסה", - "logout": "יציאה", - "deleteAccount": "מחיקת חשבון", - "back": "חזרה", - "signInGoogle": "המשך עם Google", - "signInGithub": "המשך עם GitHub", - "signInDiscord": "המשך עם Discord", - "more": "עוד", - "create": "יצירה", - "close": "סגירה" - }, - "label": { - "welcome": "ברוך בואך!", - "firstName": "שם פרטי", - "middleName": "שם אמצעי", - "lastName": "שם משפחה", - "stepX": "שלב {X}" - }, - "oAuth": { - "err": { - "failedTitle": "לא ניתן להתחבר לחשבון שלך.", - "failedMsg": "נא לוודא שהשלמת את תהליך הכניסה בדפדפן שלך." - }, - "google": { - "title": "כניסה עם GOOGLE", - "instruction1": "כדי לייבא את אנשי הקשר שלך מ־Google, צריך לאמת את היישום הזה בעזרת הדפדפן שלך.", - "instruction2": "יש להעתיק את הקוד ללוח הגזירים שלך בלחיצה על הסמל או על ידי בחירת הטקסט:", - "instruction3": "יש לנווט לקישור הבא בדפדפן שלך ולמלא את הקוד הבא:", - "instruction4": "יש ללחוץ על הכפתור שלהלן לאחר השלמת ההרשמה:" - } - }, - "settings": { - "title": "הגדרות", - "accountPage": { - "menuLabel": "החשבון שלי", - "title": "החשבון שלי", - "general": { - "title": "שם חשבון ותמונת פרופיל", - "changeProfilePicture": "החלפת תמונת פרופיל" - }, - "email": { - "title": "דוא״ל", - "actions": { - "change": "החלפת כתובת דוא״ל" - } - }, - "login": { - "title": "כניסה לחשבון", - "loginLabel": "כניסה", - "logoutLabel": "יציאה" - } - }, - "workspacePage": { - "menuLabel": "מרחב עבודה", - "title": "מרחב עבודה", - "description": "התאמת מראה, ערכת העיצוב, הגופן, תבנית התאריך והשעה והשפה של מרחב העבודה שלך.", - "workspaceName": { - "title": "שם מרחב העבודה" - }, - "workspaceIcon": { - "title": "סמל מרחב העבודה", - "description": "אפשר להעלות תמונה או להשתמש באמוג׳י למרחב העבודה שלך. הסמל יופיע בסרגל הצד ובהתראות שלך." - }, - "appearance": { - "title": "מראה", - "description": "התאמת המראה, ערכת העיצוב, גופן, פריסת הטקסט, התאריך, השעה והשפה של מרחב העבודה שלך.", - "options": { - "system": "אוטו׳", - "light": "בהיר", - "dark": "כהה" - } - }, - "theme": { - "title": "ערכת עיצוב", - "description": "נא לבחור ערכת עיצוב מוגדרת מראש או להעלות ערכת עיצוב משופרת משלך.", - "uploadCustomThemeTooltip": "העלאת ערכת עיצוב משופרת" - }, - "workspaceFont": { - "title": "גופן מרחב עבודה", - "noFontHint": "הגופן לא נמצא, נא לנסות ביטוי אחר." - }, - "textDirection": { - "title": "כיוון טקסט", - "leftToRight": "משמאל לימין", - "rightToLeft": "מימין לשמאל", - "auto": "אוטו׳", - "enableRTLItems": "הפעלת פריטי סרגל כלים לכתיבה מימין לשמאל" - }, - "layoutDirection": { - "title": "כיוון פריסה", - "leftToRight": "משמאל לימין", - "rightToLeft": "מימין לשמאל" - }, - "dateTime": { - "title": "תאריך ושעה", - "example": "{} ב־{} ({})", - "24HourTime": "שעון 24 שעות", - "dateFormat": { - "label": "תבנית תאריך", - "local": "מקומית", - "us": "אמריקאית", - "iso": "ISO", - "friendly": "ידידותית", - "dmy": "D/M/Y" - } - }, - "language": { - "title": "שפה" - }, - "deleteWorkspacePrompt": { - "title": "מחיקת מרחב עבודה", - "content": "למחוק את מרחב העבודה הזה? זאת פעולה בלתי הפיכה וכל הדפים שפרסמת יוסתרו." - }, - "leaveWorkspacePrompt": { - "title": "עזיבת מרחב העבודה", - "content": "לעזוב את מרחב העבודה הזה? הגישה שלך לכל העמודים והנתונים שבתוכו תלך לאיבוד." - }, - "manageWorkspace": { - "title": "ניהול מרחב עבודה", - "leaveWorkspace": "עזיבת מרחב עבודה", - "deleteWorkspace": "מחיקת מרחב עבודה" - } - }, - "manageDataPage": { - "menuLabel": "ניהול נתונים", - "title": "ניהול נתונים", - "description": "ניהול אחסון הנתונים מקומית או ייבוא הנתונים הקיימים שלך אל @:appName.", - "dataStorage": { - "title": "מקום אחסון קבצים", - "tooltip": "המקום בו יאוחסנו הקבצים שלך", - "actions": { - "change": "החלפת נתיב", - "open": "פתיחת תיקייה", - "openTooltip": "פתיחת מקום תיקיית הנתונים הנוכחית", - "copy": "העתקת נתיב", - "copiedHint": "הנתיב הועתק!", - "resetTooltip": "איפוס למקום ברירת המחדל" - }, - "resetDialog": { - "title": "להמשיך?", - "description": "איפוס הנתיב למקום ברירת המחדל לאחסון הנתונים שלך. כדי לייבא את הנתונים הנוכחיים שלך מחדש, יש להעתיק את נתיב המקום הנוכחי שלך תחילה." - } - }, - "importData": { - "title": "ייבוא נתונים", - "tooltip": "ייבוא נתונים מתיקיות גיבויים/נתונים של @:appName", - "description": "העתקת נתונים מתיקיית נתונים חיצונית של @:appName", - "action": "עיון בקובץ" - }, - "encryption": { - "title": "הצפנה", - "tooltip": "ניהול אופן האחסון וההצפנה של הנתוים שלך", - "descriptionNoEncryption": "הפעלת הצפנה תצפין את כל הנתונים. זה תהליך בלתי הפיך.", - "descriptionEncrypted": "הנתונים שלך מוצפנים.", - "action": "הצפנת נתונים", - "dialog": { - "title": "להצפין את כל הנתונים שלך?", - "description": "להצפין את כל הנתונים שלך ישמור עליהם בבטחה ובצורה מאובטחת. זאת פעולה בלתי הפיכה. להמשיך?" - } - }, - "cache": { - "title": "ביטול זיכרון המטמון", - "description": "פינוי זיכרון המטמון של היישום, הפינוי יכול לסייע בפתרון תקלות כמו תמונות או גופנים שלא נטענים. אין לזה השפעה על הנתונים שלך.", - "dialog": { - "title": "להמשיך?", - "description": "פינוי זיכרון המטמון יגרום להורדת כל התמונות מחדש עם הטעינה. הפעולה הזאת לא תסיר או תשנה את הנתונים שלך.", - "successHint": "זיכרון המטמון התפנה!" - } - }, - "data": { - "fixYourData": "תיקון הנתונים שלך", - "fixButton": "תיקון", - "fixYourDataDescription": "אם נתקלת בבעיות עם הנתונים שלך, אפשר לנסות לתקן אותן כאן." - } - }, - "shortcutsPage": { - "menuLabel": "מקשי קיצור", - "title": "מקשי קיצור", - "editBindingHint": "נא להקליד צירוף חדש", - "searchHint": "חיפוש", - "actions": { - "resetDefault": "איפוס ברירת מחדל" - }, - "errorPage": { - "message": "טעינת מקשי הקיצור נכשלה: {}", - "howToFix": "נא לנסות שוב, אם הבעיה נמשכת נא לפנות אלינו דרך GitHub." - }, - "resetDialog": { - "title": "איפוס מקשי קיצור", - "description": "הפעולה הזאת תאפס את כל צירופי המקשים שלך לברירת המחדל, היא בלתי הפיכה, להמשיך?", - "buttonLabel": "איפוס" - }, - "conflictDialog": { - "title": "{} נמצא בשימוש כרגע", - "descriptionPrefix": "צירוף המקשים הזה כבר משמש לטובת ", - "descriptionSuffix": ". החלפת צירוף המקשים הזה יסיר אותו מהשליטה ב־{}.", - "confirmLabel": "המשך" - }, - "editTooltip": "יש ללחוץ כדי להתחיל לערוך את צירוף המקשים", - "keybindings": { - "toggleToDoList": "הצגת/הסתרת רשימת מטלות", - "insertNewParagraphInCodeblock": "הוספת פסקה חדשה", - "pasteInCodeblock": "הדבקה במקטע קוד", - "selectAllCodeblock": "בחירה בהכול", - "indentLineCodeblock": "הוספת שני רווחים בתחילת השורה", - "outdentLineCodeblock": "מחיקת שני רווחים בתחילת השורה", - "twoSpacesCursorCodeblock": "הוספת שני רווחים איפה שהסמן", - "copy": "העתקת בחירה", - "paste": "הדבקה בתוכן", - "cut": "גזירת הבחירה", - "alignLeft": "יישור הטקסט לשמאל", - "alignCenter": "מירכוז הטקסט", - "alignRight": "יישור הטקסט לימין", - "undo": "הסגה", - "redo": "ביצוע מחדש", - "convertToParagraph": "המרת מקטע לפסקה", - "backspace": "מחיקה", - "deleteLeftWord": "מחיקת המילה משמאל", - "deleteLeftSentence": "מחיקת המשפט משמאל", - "delete": "מחיקת התו מימין", - "deleteMacOS": "מחיקת השתו משמאל", - "deleteRightWord": "מחיקת המילה מימין", - "moveCursorLeft": "הזזת הסמל שמאלה", - "moveCursorBeginning": "הזזת הסמן להתחלה", - "moveCursorLeftWord": "הזזת הסמן מילה שמאלה", - "moveCursorLeftSelect": "בחירה והזזת הסמן שמאלה", - "moveCursorBeginSelect": "בחירה והזזת הסמן להתחלה", - "moveCursorLeftWordSelect": "בחירה והזזת הסמן שמאלה במילה", - "moveCursorRight": "הזזת הסמן ימינה", - "moveCursorEnd": "הזזת הסמן לסוף", - "moveCursorRightWord": "הזזת הסמן ימינה במילה", - "moveCursorRightSelect": "בחירה והזזת הסמן אחד ימינה", - "moveCursorEndSelect": "בחירה והזזת הסמן לסוף", - "moveCursorRightWordSelect": "בחירה והזזת הסמן ימינה במילה", - "moveCursorUp": "הזזת הסמן למעלה", - "moveCursorTopSelect": "בחירה והזזת הסמן לראש", - "moveCursorTop": "הזזת הסמן לראש", - "moveCursorUpSelect": "בחירה והזזת הסמן למעלה", - "moveCursorBottomSelect": "בחירה והזזת הסמן לתחתית", - "moveCursorBottom": "הזזת הסמן לתחתית", - "moveCursorDown": "הזזת הסמן למטה", - "moveCursorDownSelect": "בחירה והזזת הסמן למטה", - "home": "גלילה למעלה", - "end": "גלילה לתחתית", - "toggleBold": "החלפת מצב מודגש", - "toggleItalic": "החלפת מצב הטייה", - "toggleUnderline": "החלפת מצב קו תחתי", - "toggleStrikethrough": "החלפת קו חוצה", - "toggleCode": "החלפת קוד כחלק מהשורה", - "toggleHighlight": "החלפת מצב הדגשה", - "showLinkMenu": "הצגת תפריט קישורים", - "openInlineLink": "פתיחת קישור מובנה", - "openLinks": "פתיחת כל הקישורים הנבחרים", - "indent": "הגדלת הזחה", - "outdent": "הקטנת הזחה", - "exit": "יציאה מעריכה", - "pageUp": "גלילה עמוד למעלה", - "pageDown": "גלילה עמוד למטה", - "selectAll": "בחירה בהכול", - "pasteWithoutFormatting": "הדבקת תוכן בלי עיצוב", - "showEmojiPicker": "הצגת בורר אמוג׳י", - "enterInTableCell": "הוספת מעבר שורה בטבלה", - "leftInTableCell": "מעבר תא שמאלה בטבלה", - "rightInTableCell": "מעבר תא ימינה בטבלה", - "upInTableCell": "מעבר תא למעלה בטבלה", - "downInTableCell": "מעבר תא למטה בטבלה", - "tabInTableCell": "מעבר לתא הזמין הבא בטבלה", - "shiftTabInTableCell": "מעבר לתא הזמין הקודם בטבלה", - "backSpaceInTableCell": "עצירה בתחילת התא" - }, - "commands": { - "codeBlockNewParagraph": "הוספת פסקה חדשה ליד מקטע הקוד", - "codeBlockIndentLines": "הוספת שני רווחים בתחילת השורה במקטע קוד", - "codeBlockOutdentLines": "מחיקת שני רווחים מתחילת השורה במקטע קוד", - "codeBlockAddTwoSpaces": "הוספת שני רווחים במקום של הסמן במקטע קוד", - "codeBlockSelectAll": "בחירת כל התוכן בתוך מקטע קוד", - "codeBlockPasteText": "הדבקת טקסט במקטע קוד", - "textAlignLeft": "יישור טקסט לשמאל", - "textAlignCenter": "מירכוז טקסט", - "textAlignRight": "יישור טקסט לימין" - }, - "couldNotLoadErrorMsg": "לא ניתן לטעון את קיצורי המקשים, נא לנסות שוב", - "couldNotSaveErrorMsg": "לא ניתן לשמור את קיצורי המקשים, נא לנסות שוב" - }, - "aiPage": { - "title": "הגדרת בינה מלאכותית", - "menuLabel": "הגדרות בינה מלאכותית", - "keys": { - "enableAISearchTitle": "חיפוש בינה מלאכותית", - "aiSettingsDescription": "בחירת או הגדרת מודלים של בינה מלאכותית לשימוש עם @:appName. לביצועים מיטביים אנו ממליצים להשתמש באפשרויות ברירת המחדל של המודל", - "loginToEnableAIFeature": "יכולות בינה מלאכותית מופעלות רק לאחר כניסה עם @:appName בענן. אם אין לך חשבון של @:appName, יש לגשת אל ‚החשבון שלי’ כדי להירשם", - "llmModel": "מודל שפה", - "llmModelType": "סוג מודל שפה", - "downloadLLMPrompt": "הורדת {}", - "downloadLLMPromptDetail": "הורדת המודל המקומי {} תתפוס {} מהאחסון. להמשיך?", - "downloadAIModelButton": "הורדת מודל בינה מלאכותית", - "downloadingModel": "מתבצעת הורדה", - "localAILoaded": "מודל הבינה המלאכותית המקומי נוסף והוא מוכן לשימוש", - "localAILoading": "מודל הבינה המלאכותית המקומי נטען…", - "localAIStopped": "מודל הבינה המלאכותית המקומי נעצר", - "title": "מפתחות API לבינה מלאכותית", - "openAILabel": "מפתח API ל־OpenAI", - "openAITooltip": "אפשר למצוא את מפתח ה־API הסודי שלך בעמוד מפתח ה־API", - "openAIHint": "מילוי מפתח ה־API שלך ב־OpenAI", - "stabilityAILabel": "מפתח API של Stability", - "stabilityAITooltip": "מפתח ה־API שלך ב־Stability, משמש לאימות הבקשות שלך", - "stabilityAIHint": "נא למלא את מפתח ה־API שלך ב־Stability" - } - }, - "planPage": { - "menuLabel": "תוכנית", - "title": "תוכנית חיוב", - "planUsage": { - "title": "תקציר שימוש בתוכנית", - "storageLabel": "אחסון", - "storageUsage": "{} מתוך {} ג״ב", - "collaboratorsLabel": "שותפים", - "collaboratorsUsage": "{} מתוך {}", - "aiResponseLabel": "תגובות בינה מלאכותית", - "aiResponseUsage": "{} מתוך {}", - "proBadge": "פרו", - "memberProToggle": "אין הגבלה על כמות חברים", - "aiCredit": { - "title": "הוספת קרדיט בינה מלאכותית ל־@:appName", - "price": "{}", - "priceDescription": "ל־1,000 קרדיטים", - "purchase": "רכישת בינה מלאכותית", - "info": "הוספת 1,000 קרדיטים של בינה מלאכותית לכל סביבת עבודה ושילוב בינה מלאכותית מותאמת למרחב העבודה שלך לתוצאות חכמות ומהירות יותר עם עד:", - "infoItemOne": "10,000 תגובות למסד נתונים", - "infoItemTwo": "1,000 תגובות לסביבת עבודה" - }, - "currentPlan": { - "bannerLabel": "תוכנית נוכחית", - "freeTitle": "חינם", - "proTitle": "פרו", - "teamTitle": "צוות", - "freeInfo": "מעולה לעצמאיים או צוותים קטנים של עד 3 חברים.", - "proInfo": "מעולה לצוות קטנים ובינוניים עד 10 חברים.", - "teamInfo": "מעולה לכל סוג של צוות משימתי שמאורגן היטב.", - "upgrade": "השוואה\n ושדרוג", - "canceledInfo": "התוכנית שלך בוטלה, יבוצע שנמוך לתוכנית החופשית ב־{}.", - "freeProOne": "מרחב עבודה שיתופי", - "freeProTwo": "עד 3 חברים (כולל הבעלים)", - "freeProThree": "כמות אורחים בלתי מוגבלת (צפייה בלבד)", - "freeProFour": "אחסון של 5 ג״ב", - "freeProFive": "30 ימי היסטוריית מהדורות", - "freeConOne": "משתתפי אורח (גישת עריכה)", - "freeConTwo": "ללא הגבלת אחסון", - "freeConThree": "6 חודשי היסטוריית מהדורות", - "professionalProOne": "מרחב עבודה שיתופי", - "professionalProTwo": "כמות חברים בלתי מוגבלת", - "professionalProThree": "כמות אורחים בלתי מוגבלת (צפייה בלבד)", - "professionalProFour": "אחסון ללא הגבלה", - "professionalProFive": "6 חודשי היסטוריית מהדורות", - "professionalConOne": "משתתפי אורח ללא הגבלה (גישת עריכה)", - "professionalConTwo": "תגובות מבינה מלאכותית ללא הגבלה", - "professionalConThree": "שנה של היסטוריית מהדורות" - }, - "deal": { - "bannerLabel": "מבצע לשנה החדשה!", - "title": "הגדלת הצוות שלך!", - "info": "שדרוג כעת יזכה אותך ב־10% הנחה מתוכניות פרו ולצוותים! אפשר לחזק את יעילות סביבת העבודה שלך עם יכולות חדשות ורבות עוצמה כולל הבינה המלאכותית של @:appName.", - "viewPlans": "הצגת תוכניות" - }, - "guestCollabToggle": "10 משתתפי אורח", - "storageUnlimited": "אחסון ללא הגבלה עם תוכנית הפרו שלך" - } - }, - "billingPage": { - "menuLabel": "חיוב", - "title": "חיוב", - "plan": { - "title": "תוכנית", - "freeLabel": "חינם", - "proLabel": "פרו", - "planButtonLabel": "החלפת תוכנית", - "billingPeriod": "תקופת חיוב", - "periodButtonLabel": "עריכת תקופה" - }, - "paymentDetails": { - "title": "פרטי תשלום", - "methodLabel": "שיטת תשלום", - "methodButtonLabel": "עריכת שיטה" - } - }, - "comparePlanDialog": { - "title": "השוואה ובחירת תוכנית", - "planFeatures": "יכולות\nהתוכנית", - "current": "נוכחית", - "actions": { - "upgrade": "שדרוג", - "downgrade": "שנמוך", - "current": "נוכחית", - "downgradeDisabledTooltip": "השנמוך יתבצע בסוף מחזור החיוב" - }, - "freePlan": { - "title": "חינם", - "description": "לארגון כל פינה בעבודה ובחיים שלך.", - "price": "{}", - "priceInfo": "חינם לנצח" - }, - "proPlan": { - "title": "מקצועי", - "description": "מקום קטן לתכנות והתארגנות של קבוצות קטנות.", - "price": "{} לחודש", - "priceInfo": "בחיוב שנתי" - }, - "planLabels": { - "itemOne": "מרחבי עבודה", - "itemTwo": "חברים", - "itemThree": "אורחים", - "itemFour": "משתתפי אורח", - "itemFive": "אחסון", - "itemSix": "שיתוף בזמן אמת", - "itemSeven": "יישומון לניידים", - "tooltipThree": "לאורחים יש הרשאות לקריאה בלבד לתוכן המסוים ששותף", - "tooltipFour": "משתתפי אורח מחויבים כמושב אחד", - "itemEight": "תגובות בינה מלאכותית", - "tooltipEight": "הכוונה בביטוי „לכל החיים” כלומר שמספר התגובות לעולם לא יתאפס" - }, - "freeLabels": { - "itemOne": "חיוב לפני מרחבי עבודה", - "itemTwo": "3", - "itemFour": "0", - "itemFive": "5 ג״ב", - "itemSix": "כן", - "itemSeven": "כן", - "itemEight": "1,000 לכל החיים" - }, - "proLabels": { - "itemOne": "מחויב לפי מרחב עבודה", - "itemTwo": "עד 10", - "itemFour": "10 אורחים חויבו כמושב אחד", - "itemFive": "ללא הגבלה", - "itemSix": "כן", - "itemSeven": "כן", - "itemEight": "10,000 בחודש" - }, - "paymentSuccess": { - "title": "עברת לתוכנית {}!", - "description": "התשלום שלך עבר עיבוד והתוכנית שלך שודרגה ל־@:appName {}. אפשר לצפות בפרטי התוכנית שלך בעמוד התוכנית" - }, - "downgradeDialog": { - "title": "לשנמך את התוכנית שלך?", - "description": "שנמוך התוכנית שלך יחזיר אותך לתוכנית החינמית. חברים עלולים לאבד גישה למרחבי העבודה ויהיה עליך לפנות מקום אחסון כדי לעמוד במגבלות האחסון של התוכנית החינמית.", - "downgradeLabel": "שנמוך תוכנית" - } - }, - "common": { - "reset": "איפוס" - }, - "menu": { - "appearance": "מראה", - "language": "שפה", - "user": "משתמש", - "files": "קבצים", - "notifications": "התראות", - "open": "פתיחת ההגדרות", - "logout": "יציאה", - "logoutPrompt": "לצאת?", - "selfEncryptionLogoutPrompt": "לצאת מהמערכת? נא לוודא שהעתקת את סוד ההצפנה", - "syncSetting": "סנכרון הגדרות", - "cloudSettings": "הגדרות ענן", - "enableSync": "הפעלת סנכרון", - "enableEncrypt": "הצפנת נתונים", - "cloudURL": "כתובת בסיס", - "invalidCloudURLScheme": "סכמה שגויה", - "cloudServerType": "שרת ענן", - "cloudServerTypeTip": "נא לשים לב שהפעולה הזאת עלולה להוציא אותך מהחשבון הנוכחי שלך לאחר מעבר לשרת הענן", - "cloudLocal": "מקומי", - "cloudAppFlowy": "@:appName בענן בטא", - "cloudAppFlowySelfHost": "@:appName בענן באירוח עצמי", - "appFlowyCloudUrlCanNotBeEmpty": "כתובת הענן לא יכולה להישאר ריקה", - "clickToCopy": "לחיצה להעתקה", - "selfHostStart": "אם אין לך שרת, נא לפנות אל", - "selfHostContent": "המסמך", - "selfHostEnd": "להנחיה בנוגע לאירוח בשרת משלך", - "cloudURLHint": "נא למלא את כתובת הבסיס של השרת שלך", - "cloudWSURL": "כתובת Websocket", - "cloudWSURLHint": "נא למלא את כתובת השקע המקוון (websocket) של השרת שלך", - "restartApp": "הפעלה מחדש", - "restartAppTip": "יש להפעיל את היישום מחדש כדי להחיל את השינויים. נא לשים לב שיכול להיות שתתבצע יציאה מהחשבון הנוכחי שלך.", - "changeServerTip": "לאחר החלפת השרת, יש ללחוץ על כפתור ההפעלה מחדש כדי שהשינויים ייכנסו לתוקף", - "enableEncryptPrompt": "אפשר להפעיל הצפנה כדי להגן על הנתונים שלך עם הסוד הזה. יש לאחסן אותו בצורה בטוחה, לאחר שהופעלה, אי אפשר לכבות אותה. אם הסוד אבד, לא תהיה עוד גישה לנתונים שלך. לחיצה להעתקה", - "inputEncryptPrompt": "נא למלא את סוד ההצפנה שלך עבור", - "clickToCopySecret": "לחיצה להעתקת סוד", - "configServerSetting": "הגדרת השרת שלך", - "configServerGuide": "לאחר בחירה ב`התחלה מהירה`, יש לנווט אל `הגדרות` ואז ל„הגדרות ענן” כדי להגדיר שרת באירוח עצמי.", - "inputTextFieldHint": "הסוד שלך", - "historicalUserList": "היסטוריית כניסת משתמשים", - "historicalUserListTooltip": "הרשימה הזאת מציגה את החשבונות האלמוניים שלך. אפשר ללחוץ על חשבון כדי לצפות בפרטים שלו. חשבונות אלמוניים נוצרים בלחיצה על הכפתור ‚איך מתחילים’", - "openHistoricalUser": "נא ללחוץ כדי לפתוח את החשבון האלמוני", - "customPathPrompt": "אחסון תיקיית הנתונים של @:appName בתיקייה שמסונכרנת עם הענן כגון Google Drive יכול להוות סיכון. אם למסד הנתונים בתיקייה הזאת ניגשים או עורכים ממגוון מקומות בו־זמנית, יכולות לקרות סתירות סנכרון והנתונים יכולים להינזק", - "importAppFlowyData": "ייבוא נתונים מתיקיית @:appName חיצונית", - "importingAppFlowyDataTip": "מתבצע ייבוא נתונים. נא לא לסגור את היישום", - "importAppFlowyDataDescription": "העתקת נתונים מתיקיית נתונים חיצונית של @:appName ולייבא אותה לתיקיית הנתונים הנוכחית של AppFlowy", - "importSuccess": "תיקיית הנתונים של @:appName יובאה בהצלחה", - "importFailed": "ייבוא תיקיית הנתונים של @:appName נכשל", - "importGuide": "לפרטים נוספים, נא לעיין במסמך המוזכר" - }, - "notifications": { - "enableNotifications": { - "label": "הפעלת התראות", - "hint": "יש לכבות כדי לעצור הצגת התראות מקומיות." - }, - "showNotificationsIcon": { - "label": "הצגת סמל התראות", - "hint": "יש לכבות כדי להסתיר את סמל ההתראות בסרגל הצד." - } - }, - "appearance": { - "resetSetting": "איפוס", - "fontFamily": { - "label": "משפחת גופנים", - "search": "חיפוש", - "defaultFont": "מערכת" - }, - "themeMode": { - "label": "מצב ערכת עיצוב", - "light": "מצב בהיר", - "dark": "מצב כהה", - "system": "התאמה למערכת" - }, - "fontScaleFactor": "מקדם קנה מידה לגופנים", - "documentSettings": { - "cursorColor": "צבע סמן מסמך", - "selectionColor": "צבע בחירת מסמך", - "pickColor": "נא לבחור צבע", - "colorShade": "צללית צבע", - "opacity": "שקיפות", - "hexEmptyError": "צבע הקס׳ לא יכול להיות ריק", - "hexLengthError": "ערך הקס׳ חייב להיות באורך 6 תווים לפחות", - "hexInvalidError": "ערך הקס׳ שגוי", - "opacityEmptyError": "האטימות לא יכולה להיות ריקה", - "opacityRangeError": "האטימות חייבת להיות בין 1 ל־100", - "app": "יישום", - "flowy": "Flowy", - "apply": "החלה" - }, - "layoutDirection": { - "label": "כיוון פריסה", - "hint": "שליטה בזרימת התוכן על המסך שלך, משמאל לימין או מימין לשמאל.", - "ltr": "משמאל לימין", - "rtl": "מימין לשמאל" - }, - "textDirection": { - "label": "כיוון ברירת המחדל של טקסט", - "hint": "נא לציין האם טקסט אמור להתחיל משמאל או מימין כברירת מחדל.", - "ltr": "משמאל לימין", - "rtl": "מימין לשמאל", - "auto": "אוטו׳", - "fallback": "כמו כיוון הפריסה" - }, - "themeUpload": { - "button": "העלאה", - "uploadTheme": "העלאת ערכת עיצוב", - "description": "אפשר להעלות ערכת עיצוב משלך ל־@:appName בעזרת הכפתור שלהלן.", - "loading": "נא להמתין בזמן תיקוף והעלאת ערכת העיצוב שלך…", - "uploadSuccess": "ערכת העיצוב שלך הועלתה בהצלחה", - "deletionFailure": "מחיקת ערכת העיצוב נכשלה. נא לנסות למחוק אותה ידנית.", - "filePickerDialogTitle": "נא לבחור קובץ ‎.flowy_plugin", - "urlUploadFailure": "פתיחת הכתובת נכשלה: {}" - }, - "theme": "ערכת עיצוב", - "builtInsLabel": "ערכות עיצוב מובנות", - "pluginsLabel": "תוספים", - "dateFormat": { - "label": "תבנית תאריך", - "local": "מקומית", - "us": "אמריקאית", - "iso": "ISO", - "friendly": "ידידותית", - "dmy": "D/M/Y" - }, - "timeFormat": { - "label": "תבנית שעה", - "twelveHour": "12 שעות", - "twentyFourHour": "24 שעות" - }, - "showNamingDialogWhenCreatingPage": "הצגת חלונית מתן שם בעת יצירת עמוד", - "enableRTLToolbarItems": "הפעלת פריטי ימין לשמאל בסרגל הכלים", - "members": { - "title": "הגדרות חברים", - "inviteMembers": "הזמנת חברים", - "inviteHint": "הזמנה בדוא״ל", - "sendInvite": "שליחת הזמנה", - "copyInviteLink": "העתקת קישור הזמנה", - "label": "חברים", - "user": "משתמש", - "role": "תפקיד", - "removeFromWorkspace": "הסרה ממרחב העבודה", - "owner": "בעלים", - "guest": "אורח", - "member": "חבר", - "memberHintText": "חברים יכולים לקרוא ולערוך עמודים", - "guestHintText": "אורחים יכולים לקרוא, להגיב עם סמל, לכתוב הערות ולערוך עמודים מסוימים לפי ההרשאה.", - "emailInvalidError": "כתובת דוא״ל שגויה, נא לבדוק ולנסות שוב", - "emailSent": "ההודעה נשלחה בדוא״ל, נא לבדוק את תיבת הדוא״ל הנכנס", - "members": "חברים", - "membersCount": { - "zero": "{} חברים", - "one": "חבר", - "other": "{} חברים" - }, - "memberLimitExceeded": "הגעת למגבלת החברים המרבית לחשבון שלך. כדי להוסיף חברים נוספים ולהמשיך בעבודתך, נא להגיש בקשה ב־GitHub", - "failedToAddMember": "הוספת החבר נכשלה", - "addMemberSuccess": "החבר נוסף בהצלחה", - "removeMember": "הסרת חבר", - "areYouSureToRemoveMember": "להסיר את החבר הזה?", - "inviteMemberSuccess": "ההזמנה נשלחה בהצלחה", - "failedToInviteMember": "הזמנת החבר נכשלה" - } - }, - "files": { - "copy": "העתקה", - "defaultLocation": "קריאת הקבצים ומקום אחסון הנתונים", - "exportData": "ייצוא הנתונים שלך", - "doubleTapToCopy": "הנתיב יועתק בלחיצה כפולה", - "restoreLocation": "שחזור לנתיב ברירת המחדל של @:appName", - "customizeLocation": "פתיחת תיקייה אחרת", - "restartApp": "נא להפעיל את היישום מחדש כדי שהשינויים ייכנסו לתוקף.", - "exportDatabase": "ייצוא מסד נתונים", - "selectFiles": "נא לבחור את הקבצים לייצור", - "selectAll": "בחירה בהכול", - "deselectAll": "ביטול בחירה", - "createNewFolder": "יצירת תיקייה חדשה", - "createNewFolderDesc": "נא הגדיר לנו איפה לאחסן את הנתונים שלך", - "defineWhereYourDataIsStored": "הגדרת מקום אחסון הנתונים שלך", - "open": "פתיחה", - "openFolder": "פתיחת תיקייה קיימת", - "openFolderDesc": "קריאה וכתיבה אל תיקיית ה־@:appName הקיימת שלך", - "folderHintText": "שם התיקייה", - "location": "יצירת תיקייה חדשה", - "locationDesc": "נא לבחור שם לתיקיית הנתונים של @:appName", - "browser": "עיון", - "create": "יצירה", - "set": "הגדרה", - "folderPath": "נתיב לאחסון התיקייה שלך", - "locationCannotBeEmpty": "הנתיב לא יכול להיות ריק", - "pathCopiedSnackbar": "נתיב אחסון הקבצים הועתק ללוח הגזירים!", - "changeLocationTooltips": "החלפת תיקיית הנתונים", - "change": "שינוי", - "openLocationTooltips": "פתיחת תיקיית נתונים אחרת", - "openCurrentDataFolder": "פתיחת תיקיית הנתונים הנוכחית", - "recoverLocationTooltips": "איפוס לתיקיית הנתונים כברירת המחדל של @:appName", - "exportFileSuccess": "ייצוא הקובץ הצליח!", - "exportFileFail": "ייצוא הקובץ נכשל!", - "export": "ייצוא", - "clearCache": "פינוי זיכרון המטמון", - "clearCacheDesc": "אם נתקלת בבעיות עם תמונות שלא נטענות או גופנים שלא מופיעים כראוי, כדאי לנסות לפנות את זיכרון המטמון. הפעולה הזאת לא תסיר את נתוני המשתמש שלך.", - "areYouSureToClearCache": "לפנות את זיכרון המטמון?", - "clearCacheSuccess": "זיכרון המטמון התפנה בהצלחה!" - }, - "user": { - "name": "שם", - "email": "דוא״ל", - "tooltipSelectIcon": "בחירת סמל", - "selectAnIcon": "נא לבחור סמל", - "pleaseInputYourOpenAIKey": "נא למלא את המפתח שלך ב־OpenAI", - "clickToLogout": "לחיצה תוציא את המשתמש הנוכחי", - "pleaseInputYourStabilityAIKey": "נא למלא את המפתח שלך ב־Stability AI" - }, - "mobile": { - "personalInfo": "פרטים אישיים", - "username": "שם משתמש", - "usernameEmptyError": "שם שמשתמש לא יכול להישאר ריק", - "about": "על אודות", - "pushNotifications": "התראות בדחיפה", - "support": "תמיכה", - "joinDiscord": "להצטרף אלינו ב־Discord", - "privacyPolicy": "מדיניות פרטיות", - "userAgreement": "הסכם שימוש", - "termsAndConditions": "תנאים והתניות", - "userprofileError": "טעינת פרופיל המשתמש נכשלה", - "userprofileErrorDescription": "נא לנסות לצאת ולהיכנס בחזרה אם הבעיה נמשכת.", - "selectLayout": "בחירת פריסה", - "selectStartingDay": "בחירת יום ההתחלה", - "version": "גרסה" - } - }, - "grid": { - "deleteView": "למחוק את התצוגה הזאת?", - "createView": "חדש", - "title": { - "placeholder": "ללא שם" - }, - "settings": { - "filter": "מסנן", - "sort": "מיון", - "sortBy": "מיון לפי", - "properties": "מאפיינים", - "reorderPropertiesTooltip": "אפשר לשנות את סדר המאפיינים בגרירה", - "group": "קבוצה", - "addFilter": "הוספת מסנן", - "deleteFilter": "מחיקת מסנן", - "filterBy": "סינון לפי…", - "typeAValue": "נא להקליד ערך…", - "layout": "פריסה", - "databaseLayout": "פריסה", - "viewList": { - "zero": "0 צפיות", - "one": "צפייה", - "other": "{count} צפיות" - }, - "editView": "עריכת תצוגה", - "boardSettings": "הגדרות לוח", - "calendarSettings": "הגדרות לוח שנה", - "createView": "תצוגה חדשה", - "duplicateView": "שכפול תצוגה", - "deleteView": "מחיקת תצוגה", - "numberOfVisibleFields": "{} מופיע" - }, - "textFilter": { - "contains": "מכיל", - "doesNotContain": "לא מכיל", - "endsWith": "מסתיים ב־", - "startWith": "נפתח ב־", - "is": "הוא", - "isNot": "אינו", - "isEmpty": "ריק", - "isNotEmpty": "לא ריק", - "choicechipPrefix": { - "isNot": "לא", - "startWith": "נפתח ב־", - "endWith": "מסתיים ב־", - "isEmpty": "ריק", - "isNotEmpty": "לא ריק" - } - }, - "checkboxFilter": { - "isChecked": "מסומן", - "isUnchecked": "לא מסומן", - "choicechipPrefix": { - "is": "הוא" - } - }, - "checklistFilter": { - "isComplete": "הושלם", - "isIncomplted": "טרם הושלם" - }, - "selectOptionFilter": { - "is": "הוא", - "isNot": "אינו", - "contains": "מכיל", - "doesNotContain": "לא מכיל", - "isEmpty": "ריק", - "isNotEmpty": "לא ריק" - }, - "dateFilter": { - "is": "הוא", - "before": "הוא לפני", - "after": "הוא אחרי", - "onOrBefore": "בתאריך או לפני", - "onOrAfter": "בתאריך או אחרי", - "between": "בין", - "empty": "ריק", - "notEmpty": "לא ריק", - "choicechipPrefix": { - "before": "לפני", - "after": "אחרי", - "onOrBefore": "בתאריך או לפני", - "onOrAfter": "בתאריך או אחרי", - "isEmpty": "ריק", - "isNotEmpty": "לא ריק" - } - }, - "numberFilter": { - "equal": "שווה ל־", - "notEqual": "לא שווה ל־", - "lessThan": "קטן מ־", - "greaterThan": "גדול מ־", - "lessThanOrEqualTo": "קטן או שווה ל־", - "greaterThanOrEqualTo": "גדול או שווה ל־", - "isEmpty": "ריק", - "isNotEmpty": "לא ריק" - }, - "field": { - "hide": "הסתרה", - "show": "הצגה", - "insertLeft": "הוספה משמאל", - "insertRight": "הוספה מימין", - "duplicate": "שכפול", - "delete": "מחיקה", - "wrapCellContent": "גלישת טקסט", - "clear": "פינוי תאים", - "textFieldName": "טקסט", - "checkboxFieldName": "תיבת סימון", - "dateFieldName": "תאריך", - "updatedAtFieldName": "שינוי אחרון", - "createdAtFieldName": "יצירה", - "numberFieldName": "מספרים", - "singleSelectFieldName": "בחירה", - "multiSelectFieldName": "ריבוי בחירות", - "urlFieldName": "כתובת", - "checklistFieldName": "רשימת סימונים", - "relationFieldName": "יחס", - "summaryFieldName": "תקציר בינה מלאכותית", - "timeFieldName": "שעה", - "translateFieldName": "תרגום בינה מלאכותית", - "translateTo": "תרגום ל־", - "numberFormat": "תבנית מספר", - "dateFormat": "תבנית תאריך", - "includeTime": "כולל השעה", - "isRange": "תאריך סיום", - "dateFormatFriendly": "חודש יום, שנה", - "dateFormatISO": "שנה-חודש-יום", - "dateFormatLocal": "חודש/יום/שנה", - "dateFormatUS": "שנה/חודש/יום", - "dateFormatDayMonthYear": "יום/חודש/שנה", - "timeFormat": "תבנית שעה", - "invalidTimeFormat": "תבנית שגויה", - "timeFormatTwelveHour": "12 שעות", - "timeFormatTwentyFourHour": "24 שעות", - "clearDate": "פינוי התאריך", - "dateTime": "תאריך שעה", - "startDateTime": "תאריך ושעת התחלה", - "endDateTime": "תאריך ושעת סיום", - "failedToLoadDate": "טעינת ערך התאריך נכשלה", - "selectTime": "נא לבחור שעה", - "selectDate": "נא לבחור תאריך", - "visibility": "חשיפה", - "propertyType": "סוג המאפיין", - "addSelectOption": "הוספת אפשרות", - "typeANewOption": "נא להקליד אפשרות חדשה", - "optionTitle": "אפשרויות", - "addOption": "הוספת אפשרות", - "editProperty": "עריכת מאפיין", - "newProperty": "מאפיין חדש", - "deleteFieldPromptMessage": "להמשיך? המאפיין יימחק", - "clearFieldPromptMessage": "להמשיך? כל התאים בעמודה הזאת יתרוקנו", - "newColumn": "עמודה חדשה", - "format": "תבנית", - "reminderOnDateTooltip": "בתא הזה יש תזכורת מתוזמנת", - "optionAlreadyExist": "האפשרות כבר קיימת" - }, - "rowPage": { - "newField": "הוספת שדה חדש", - "fieldDragElementTooltip": "נא ללחוץ לפתיחת התפריט", - "showHiddenFields": { - "one": "הצגת שדה מוסתר", - "many": "הצגת {count} שדות מוסתרים", - "other": "הצגת {count} שדות מוסתרים" - }, - "hideHiddenFields": { - "one": "הסתרת שדה מוסתר", - "many": "הסתרת {count} שדות מוסתרים", - "other": "הסתרת {count} שדות מוסתרים" - }, - "openAsFullPage": "Open as full page", - "moreRowActions": "פעולות שורה נוספות" - }, - "sort": { - "ascending": "עולה", - "descending": "יורד", - "by": "לפי", - "empty": "אין מיונים פעילים", - "cannotFindCreatableField": "לא ניתן למצוא שדה מתאים למיין לפיו", - "deleteAllSorts": "מחיקת כל המיונים", - "addSort": "הוספת מיון חדש", - "removeSorting": "להסיר מיון?", - "fieldInUse": "כבר בחרת למיין לפי השדה הזה" - }, - "row": { - "duplicate": "שכפול", - "delete": "מחיקה", - "titlePlaceholder": "ללא שם", - "textPlaceholder": "ריק", - "copyProperty": "המאפיין הועתק ללוח הגזירים", - "count": "ספירה", - "newRow": "שורה חדשה", - "action": "פעולה", - "add": "לחיצה תוסיף להלן", - "drag": "גרירה להזזה", - "deleteRowPrompt": "למחוק את השורה הזאת? זאת פעולה בלתי הפיכה", - "deleteCardPrompt": "למחוק את הכרטיס הזה? זאת פעולה בלתי הפיכה", - "dragAndClick": "גרירה להזזה, לחיצה לפתיחת התפריט", - "insertRecordAbove": "הוספת רשומה למעלה", - "insertRecordBelow": "הוספת רשומה מתחת", - "noContent": "אין תוכן" - }, - "selectOption": { - "create": "יצירה", - "purpleColor": "סגול", - "pinkColor": "ורוד", - "lightPinkColor": "ורוד בהיר", - "orangeColor": "כתום", - "yellowColor": "צהוב", - "limeColor": "ליים", - "greenColor": "ירוק", - "aquaColor": "אוקיינוס", - "blueColor": "כחול", - "deleteTag": "מחיקת תגית", - "colorPanelTitle": "צבע", - "panelTitle": "נא לבחור אפשרות או ליצור אחת", - "searchOption": "חיפוש אפשרות", - "searchOrCreateOption": "חיפוש אפשרות או ליצור אחת", - "createNew": "ליצור חדשה", - "orSelectOne": "או לבחור אפשרות", - "typeANewOption": "נא למלא אפשרות חדשה", - "tagName": "שם תגית" - }, - "checklist": { - "taskHint": "תיאור משימה", - "addNew": "הוספת משימה חדשה", - "submitNewTask": "יצירה", - "hideComplete": "הסתרת משימות שהושלמו", - "showComplete": "הצגת כל המשימות" - }, - "url": { - "launch": "פתיחת קישור בדפדפן", - "copy": "העתקת קישור ללוח הגזירים", - "textFieldHint": "נא למלא כתובת" - }, - "relation": { - "relatedDatabasePlaceLabel": "מסד נתונים קשור", - "relatedDatabasePlaceholder": "אין", - "inRelatedDatabase": "בתוך", - "rowSearchTextFieldPlaceholder": "חיפוש", - "noDatabaseSelected": "לא נבחר מסד נתונים, נא לבחור אחד מהרשימה להלן תחילה:", - "emptySearchResult": "לא נמצאו רשומות", - "linkedRowListLabel": "{count} שורות מקושרות", - "unlinkedRowListLabel": "קישור שורה נוספת" - }, - "menuName": "רשת", - "referencedGridPrefix": "View of", - "calculate": "חישוב", - "calculationTypeLabel": { - "none": "אין", - "average": "ממוצע", - "max": "מרבי", - "median": "חציוני", - "min": "מזערי", - "sum": "סכום", - "count": "ספירה", - "countEmpty": "ספירת הריקים", - "countEmptyShort": "ריק", - "countNonEmpty": "ספירת הלא ריקים", - "countNonEmptyShort": "מלאים" - } - }, - "document": { - "menuName": "מסמך", - "date": { - "timeHintTextInTwelveHour": "‎01:00 PM", - "timeHintTextInTwentyFourHour": "13:00" - }, - "slashMenu": { - "board": { - "selectABoardToLinkTo": "נא לבחור לוח לקשר אליו", - "createANewBoard": "יצירת לוח חדש" - }, - "grid": { - "selectAGridToLinkTo": "נא לבחור רשת לקשר אליה", - "createANewGrid": "יצירת רשת חדשה" - }, - "calendar": { - "selectACalendarToLinkTo": "נא לבחור לוח שנה לקשר אליו", - "createANewCalendar": "יצירת לוח שנה חדש" - }, - "document": { - "selectADocumentToLinkTo": "נא לבחור מסמך לקשר אליו" - } - }, - "selectionMenu": { - "outline": "קו מתאר", - "codeBlock": "מקטע קוד" - }, - "plugins": { - "referencedBoard": "לוח מופנה", - "referencedGrid": "טבלה מופנית", - "referencedCalendar": "לוח שנה מופנה", - "referencedDocument": "מסמך מופנה", - "autoGeneratorMenuItemName": "כותב OpenAI", - "autoGeneratorTitleName": "OpenAI: לבקש מהבינה המלאכותית לכתוב כל דבר שהוא…", - "autoGeneratorLearnMore": "מידע נוסף", - "autoGeneratorGenerate": "יצירה", - "autoGeneratorHintText": "לבקש מ־OpenAI…", - "autoGeneratorCantGetOpenAIKey": "לא ניתן למשוך את המפתח של OpenAI", - "autoGeneratorRewrite": "שכתוב", - "smartEdit": "סייעני בינה מלאכותית", - "smartEditFixSpelling": "תיקון איות", - "warning": "⚠️ תגובות הבינה המלאכותית יכולות להיות מסולפות או מטעות.", - "smartEditSummarize": "סיכום", - "smartEditImproveWriting": "שיפור הכתיבה", - "smartEditMakeLonger": "הארכה", - "smartEditCouldNotFetchResult": "לא ניתן למשוך את התוצאה מ־OpenAI", - "smartEditCouldNotFetchKey": "לא ניתן למשוך מפתח OpenAI", - "smartEditDisabled": "חיבור OpenAI בהגדרות", - "appflowyAIEditDisabled": "יש להיכנס כדי להפעיל יכולות בינה מלאכותית", - "discardResponse": "להתעלם מתגובות הבינה המלאכותית?", - "createInlineMathEquation": "יצירת משוואה", - "fonts": "גופנים", - "insertDate": "הוספת תאריך", - "emoji": "אמוג׳י", - "toggleList": "רשימת מתגים", - "quoteList": "רשימת ציטוטים", - "numberedList": "רשימה ממוספרת", - "bulletedList": "רשימת תבליטים", - "todoList": "רשימת מטלות", - "callout": "מסר", - "cover": { - "changeCover": "החלפת עטיפה", - "colors": "צבעים", - "images": "תמונות", - "clearAll": "פינוי של הכול", - "abstract": "מופשט", - "addCover": "הוספת עטיפה", - "addLocalImage": "הוספת תמונה מקומית", - "invalidImageUrl": "כתובת תמונה שגויה", - "failedToAddImageToGallery": "הוספת תמונה לגלריה נכשלה", - "enterImageUrl": "נא למלא כתובת תמונה", - "add": "הוספה", - "back": "חזרה", - "saveToGallery": "שמירה לגלריה", - "removeIcon": "הסרת סמל", - "pasteImageUrl": "הדבקת כתובת תמונה", - "or": "או", - "pickFromFiles": "בחירה מהקבצים", - "couldNotFetchImage": "לא ניתן למשוך תמונה", - "imageSavingFailed": "שמירת התמונה נכשלה", - "addIcon": "הוספת סמל", - "changeIcon": "החלפת סמל", - "coverRemoveAlert": "הוא יוסר מהעטיפה לאחר מחיקתו.", - "alertDialogConfirmation": "להמשיך?" - }, - "mathEquation": { - "name": "משוואה מתמטית", - "addMathEquation": "הוספת משוואת TeX", - "editMathEquation": "עריכת משוואה מתמטית" - }, - "optionAction": { - "click": "יש ללחוץ על", - "toOpenMenu": " כדי לפתוח תפריט", - "delete": "מחיקה", - "duplicate": "שכפול", - "turnInto": "הפיכה ל־", - "moveUp": "העלאה למעלה", - "moveDown": "הורדה למטה", - "color": "צבע", - "align": "יישור", - "left": "שמאל", - "center": "מרכז", - "right": "ימין", - "defaultColor": "ברירת מחדל", - "depth": "עומק" - }, - "image": { - "addAnImage": "הוספת תמונה", - "copiedToPasteBoard": "קישור התמונה הועתק ללוח הגזירים", - "imageUploadFailed": "העלאת התמונה נכשלה", - "errorCode": "קוד שגיאה" - }, - "math": { - "copiedToPasteBoard": "המשוואה המתמטית הועתקה ללוח הגזירים" - }, - "urlPreview": { - "copiedToPasteBoard": "הקישור הועתק ללוח הגזירים", - "convertToLink": "המרה לקישור להטמעה" - }, - "outline": { - "addHeadingToCreateOutline": "יש להוסיף כותרות ראשיות כדי ליצור תוכן עניינים.", - "noMatchHeadings": "לא נמצאו כותרות ראשויות תואמות." - }, - "table": { - "addAfter": "הוספה לאחר", - "addBefore": "הוספה לפני", - "delete": "מחיקה", - "clear": "פינוי התוכן", - "duplicate": "שכפול", - "bgColor": "צבע רקע" - }, - "contextMenu": { - "copy": "העתקה", - "cut": "גזירה", - "paste": "הדבקה" - }, - "action": "פעולות", - "database": { - "selectDataSource": "בחירת מקור נתונים", - "noDataSource": "אין מקור נתונים", - "selectADataSource": "נא לבחור מקור נתונים", - "toContinue": "כדי להמשיך", - "newDatabase": "מסד נתונים חדש", - "linkToDatabase": "קישור למסד נתונים" - }, - "date": "תאריך", - "video": { - "label": "סרטון", - "emptyLabel": "הוספת סרטון", - "placeholder": "הדבקת הקישור לסרטון", - "copiedToPasteBoard": "הקישור לסרטון הועתק ללוח הגזירים", - "insertVideo": "הוספת סרטון", - "invalidVideoUrl": "כתובת המקור לא נתמכת עדיין.", - "invalidVideoUrlYouTube": "עדיין אין תמיכה ב־YouTube.", - "supportedFormats": "סוגים נתמכים: MP4,‏ WebM,‏ MOV,‏ AVI,‏ FLV,‏ MPEG/M4V,‏ H.264" - }, - "openAI": "OpenAI" - }, - "outlineBlock": { - "placeholder": "תוכן עניינים" - }, - "textBlock": { - "placeholder": "נא להקליד ‚/’ לקבלת פקודות" - }, - "title": { - "placeholder": "ללא שם" - }, - "imageBlock": { - "placeholder": "נא ללחוץ כדי להוסיף תמונה", - "upload": { - "label": "העלאה", - "placeholder": "נא ללחוץ כדי להעלות תמונה" - }, - "url": { - "label": "כתובת תמונה", - "placeholder": "נא למלא כתובת תמונה" - }, - "ai": { - "label": "יצירת תמונה מ־OpenAI", - "placeholder": "נא למלא את הקלט ל־OpenAI כדי לייצר תמונה" - }, - "stability_ai": { - "label": "יצירת תמונה מ־Stability AI", - "placeholder": "נא למלא את הבקשה ל־Stability AI כדי לייצר תמונה" - }, - "support": "מגבלת גודל התמונה היא 5 מ״ב. הסוגים הנתמכים הם: JPEG,‏ PNG,‏ GIF,‏ SVG", - "error": { - "invalidImage": "תמונה שגויה", - "invalidImageSize": "גודל התמונה חייב להיות קטן מ־5 מ״ב", - "invalidImageFormat": "סוג התמונה לא נתמך. הסוגים הנתמכים: JPEG,‏ PNG,‏ JPG,‏ GIF,‏ SVG,‏ WEBP", - "invalidImageUrl": "כתובת תמונה שגויה", - "noImage": "אין קובץ או תיקייה כאלה" - }, - "embedLink": { - "label": "קישור להטמעה", - "placeholder": "נא להדביק או להקליד קישור לתמונה" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "חיפוש אחר תמונה", - "pleaseInputYourOpenAIKey": "נא למלא את מפתח ה־OpenAI שלך בעמוד ההגדרות", - "saveImageToGallery": "שמירת תמונה", - "failedToAddImageToGallery": "הוספת התמונה לגלריה נכשלה", - "successToAddImageToGallery": "התמונה נוספה לגלריה בהצלחה", - "unableToLoadImage": "לא ניתן לטעון את התמונה", - "maximumImageSize": "גודל ההעלאה המרבי הנתמך הוא 10 מ״ב", - "uploadImageErrorImageSizeTooBig": "גודל התמונה חייב להיות גדול מ־10 מ״ב", - "imageIsUploading": "התמונה נשלחת", - "pleaseInputYourStabilityAIKey": "נא למלא את המפתח ל־Stability AI בעמוד ההגדרות" - }, - "codeBlock": { - "language": { - "label": "שפה", - "placeholder": "בחירת שפה", - "auto": "אוטו׳" - }, - "copyTooltip": "העתקת תוכן למקטע קוד", - "searchLanguageHint": "חיפוש אחר שפה", - "codeCopiedSnackbar": "הקוד הועתק ללוח הגזירים!" - }, - "inlineLink": { - "placeholder": "נא להדביק או להקליד קישור", - "openInNewTab": "פתיחה בלשונית חדשה", - "copyLink": "העתקת קישור", - "removeLink": "הסרת קישור", - "url": { - "label": "כתובת קישור", - "placeholder": "נא למלא כתובת לקישור" - }, - "title": { - "label": "כותרת קישור", - "placeholder": "נא למלא כותרת לקישור" - } - }, - "mention": { - "placeholder": "אזכור משתמשים או עמוד או תאריך…", - "page": { - "label": "קישור לעמוד", - "tooltip": "לחיצה תפתח את העמוד" - }, - "deleted": "נמחק", - "deletedContent": "התוכן הזה לא קיים או שנמחק" - }, - "toolbar": { - "resetToDefaultFont": "איפוס לברירת מחדל" - }, - "errorBlock": { - "theBlockIsNotSupported": "לא ניתן לפענח את תוכן המקטע", - "clickToCopyTheBlockContent": "לחיצה תעתיק את תוכן המקטע", - "blockContentHasBeenCopied": "תוכן המקטע הועתק." - }, - "mobilePageSelector": { - "title": "נא לבחור עמוד", - "failedToLoad": "טעינת רשימת העמודים נכשלה", - "noPagesFound": "לא נמצאו עמודים" - } - }, - "board": { - "column": { - "createNewCard": "חדש", - "renameGroupTooltip": "נא ללחוץ לשינוי שם הקבוצה", - "createNewColumn": "הוספת קבוצה חדשה", - "addToColumnTopTooltip": "הוספת כרטיס חדש בראש", - "addToColumnBottomTooltip": "הוספת כרטיס חדש בתחתית", - "renameColumn": "שינוי שם", - "hideColumn": "הסתרה", - "newGroup": "קבוצה חדשה", - "deleteColumn": "מחיקה", - "deleteColumnConfirmation": "הפעולה הזאת תמחק את הקבוצה הזאת ואת כל הכרטיסים שבה.\nלהמשיך?" - }, - "hiddenGroupSection": { - "sectionTitle": "קבוצות מוסתרות", - "collapseTooltip": "הסתרת הקבוצות המוסתרות", - "expandTooltip": "הצגת הקבוצות המוסתרות" - }, - "cardDetail": "פרטי הכרטיס", - "cardActions": "פעולות על הכרטיס", - "cardDuplicated": "הכרטיס שוכפל", - "cardDeleted": "הכרטיס נמחק", - "showOnCard": "הצגה על פרט הכרטיס", - "setting": "הגדרה", - "propertyName": "שם מאפיין", - "menuName": "לוח", - "showUngrouped": "הצגת פריטים מחוץ לקבוצות", - "ungroupedButtonText": "מחוץ לקבוצה", - "ungroupedButtonTooltip": "מכיל כרטיסים שלא שייכים לאף קבוצה", - "ungroupedItemsTitle": "נא ללחוץ כדי להוסיף ללוח", - "groupBy": "קיבוץ לפי", - "groupCondition": "תנאי קיבוץ", - "referencedBoardPrefix": "View of", - "notesTooltip": "הערות בפנים", - "mobile": { - "editURL": "עריכת כתובת", - "showGroup": "הצגת קבוצה", - "showGroupContent": "להציג את הקבוצה הזאת בלוח?", - "failedToLoad": "טעינת תצוגת הלוח נכשלה" - }, - "dateCondition": { - "weekOf": "שבוע {} - {}", - "today": "היום", - "yesterday": "אתמול", - "tomorrow": "מחר", - "lastSevenDays": "7 הימים האחרונים", - "nextSevenDays": "7 הימים הבאים", - "lastThirtyDays": "30 הימים האחרונים", - "nextThirtyDays": "30 הימים הבאים" - }, - "noGroup": "אין קבוצה לפי מאפיין", - "noGroupDesc": "תצוגות הלוח דורשות מאפיין לקיבוץ כדי שתוצגנה" - }, - "calendar": { - "menuName": "לוח שנה", - "defaultNewCalendarTitle": "ללא שם", - "newEventButtonTooltip": "הוספת אירוע חדש", - "navigation": { - "today": "היום", - "jumpToday": "דילוג להיום", - "previousMonth": "החודש הקודם", - "nextMonth": "החודש הבא", - "views": { - "day": "יום", - "week": "שבוע", - "month": "חודש", - "year": "שנה" - } - }, - "mobileEventScreen": { - "emptyTitle": "אין אירועים עדיין", - "emptyBody": "יש ללחוץ על כפתור הפלוס כדי להוסיף אירוע ביום הזה." - }, - "settings": { - "showWeekNumbers": "הצגת מספרי השבועות", - "showWeekends": "הצגת סופי שבוע", - "firstDayOfWeek": "השבוע מתחיל ב־", - "layoutDateField": "פריסת לוח השנה לפי", - "changeLayoutDateField": "החלפת שדה פריסה", - "noDateTitle": "אין תאריך", - "noDateHint": { - "zero": "אירועים לא מתוזמנים יופיעו כאן", - "one": "אירוע לא מתוזמן", - "other": "{count} אירועים לא מתוזמנים" - }, - "unscheduledEventsTitle": "אירועים לא מתוזמנים", - "clickToAdd": "נא ללחוץ כדי להוסיף ללוח השנה", - "name": "הגדרות לוח שנה", - "clickToOpen": "לחיצה תפתח את הרשומה" - }, - "referencedCalendarPrefix": "תצוגה של", - "quickJumpYear": "דילוג אל", - "duplicateEvent": "שכפול אירוע" - }, - "errorDialog": { - "title": "שגיאה ב־@:appName", - "howToFixFallback": "סליחה על חוסר הנעימות! נא להגיש תיעוד תקלה בעמוד ה־GitHub שלנו שמתאר את השגיאה שלך.", - "github": "הצגה ב־GitHub" - }, - "search": { - "label": "חיפוש", - "placeholder": { - "actions": "חיפוש פעולות…" - } - }, - "message": { - "copy": { - "success": "הועתק!", - "fail": "לא ניתן להעתיק" - } - }, - "unSupportBlock": "הגרסה הנוכחית לא תומכת במקטע הזה.", - "views": { - "deleteContentTitle": "למחוק את {pageType}?", - "deleteContentCaption": "ניתן יהיה לשחזר את ה{pageType} הזה מהאשפה אם בחרת למחוק." - }, - "colors": { - "custom": "משלך", - "default": "ברירת מחדל", - "red": "אדום", - "orange": "כתום", - "yellow": "צהוב", - "green": "ירוק", - "blue": "כחול", - "purple": "סגול", - "pink": "ורוד", - "brown": "חום", - "gray": "אפור" - }, - "emoji": { - "emojiTab": "אמוג׳י", - "search": "חיפוש אמוג׳י", - "noRecent": "אין אמוג׳י אחרונים", - "noEmojiFound": "לא נמצא אמוג׳י", - "filter": "מסנן", - "random": "אקראי", - "selectSkinTone": "בחירת גוון עור", - "remove": "הסרת אמוג׳י", - "categories": { - "smileys": "חייכנים וסמלי רגש", - "people": "אנשים וגוף", - "animals": "חיות וטבע", - "food": "מזון ומשקאות", - "activities": "פעילויות", - "places": "טיול ומקומות", - "objects": "חפצים", - "symbols": "סמלים", - "flags": "דגלים", - "nature": "טבע", - "frequentlyUsed": "בשימוש תדיר" - }, - "skinTone": { - "default": "ברירת מחדל", - "light": "בהיר", - "mediumLight": "בהיר ממוצע", - "medium": "ממוצע", - "mediumDark": "כהה ממוצע", - "dark": "כהה" - } - }, - "inlineActions": { - "noResults": "אין תוצאות", - "recentPages": "עמודים אחרונים", - "pageReference": "הפנייה לעמוד", - "docReference": "הפנייה למסמך", - "boardReference": "הפנייה ללוח", - "calReference": "הפנייה ללוח שנה", - "gridReference": "הפנייה לטבלה", - "date": "תאריך", - "reminder": { - "groupTitle": "תזכורת", - "shortKeyword": "להזכיר" - } - }, - "datePicker": { - "dateTimeFormatTooltip": "אפשר להחליף את התאריך והשעה בהגדרות", - "dateFormat": "תבנית תאריך", - "includeTime": "כולל השעה", - "isRange": "תאריך סיום", - "timeFormat": "תבנית שעה", - "clearDate": "פינוי התאריך", - "reminderLabel": "תזכורת", - "selectReminder": "בחירת תזכורת", - "reminderOptions": { - "none": "בלי", - "atTimeOfEvent": "בזמן האירוע", - "fiveMinsBefore": "5 דק׳ לפני", - "tenMinsBefore": "10 דק׳ לפני", - "fifteenMinsBefore": "15 דק׳ לפני", - "thirtyMinsBefore": "30 דק׳ לפני", - "oneHourBefore": "שעה לפני", - "twoHoursBefore": "שעתיים לפני", - "onDayOfEvent": "ביום האירוע", - "oneDayBefore": "יום לפני", - "twoDaysBefore": "יומיים לפני", - "oneWeekBefore": "שבוע לפני", - "custom": "אחר" - } - }, - "relativeDates": { - "yesterday": "אתמול", - "today": "היום", - "tomorrow": "מחר", - "oneWeek": "שבוע" - }, - "notificationHub": { - "title": "התראות", - "mobile": { - "title": "עדכונים" - }, - "emptyTitle": "התעדכנת בהכול!", - "emptyBody": "אין התראות ממתינות לפעולות. אפשר פשוט להירגע.", - "tabs": { - "inbox": "הודעות נכנסות", - "upcoming": "בקרוב" - }, - "actions": { - "markAllRead": "סימון של כולם כנקראו", - "showAll": "הכול", - "showUnreads": "לא נקראו" - }, - "filters": { - "ascending": "עולה", - "descending": "יורד", - "groupByDate": "קיבוץ לפי תאריך", - "showUnreadsOnly": "להציג כאלו שלא נקראו בלבד", - "resetToDefault": "איפוס לברירת המחדל" - } - }, - "reminderNotification": { - "title": "תזכורת", - "message": "חשוב לסמן את זה לפני ששוכחים!", - "tooltipDelete": "מחיקה", - "tooltipMarkRead": "סימון כנקראה", - "tooltipMarkUnread": "סימון כלא נקראה" - }, - "findAndReplace": { - "find": "איתור", - "previousMatch": "התוצאה הקודמת", - "nextMatch": "התוצאה הבאה", - "close": "סגירה", - "replace": "החלפה", - "replaceAll": "החלפה של הכול", - "noResult": "אין תוצאות", - "caseSensitive": "תלוי רישיות", - "searchMore": "אפשר לחפש לאיתור תוצאות נוספות" - }, - "error": { - "weAreSorry": "אנו מתנצלים", - "loadingViewError": "טעינת התצוגה הזאת נתקלת בקשיים. נא לבדוק שהחיבור שלך לאינטרנט תקין, לאחר מכן לרענן את היישום ולא להסס לפנות לצוות אם המצב הזה נמשך." - }, - "editor": { - "bold": "מודגש", - "bulletedList": "רשימת תבליטים", - "bulletedListShortForm": "תבליטים", - "checkbox": "תיבת סימון", - "embedCode": "הטמעת קוד", - "heading1": "כ1", - "heading2": "כ2", - "heading3": "כ3", - "highlight": "הדגשה", - "color": "צבע", - "image": "תמונה", - "date": "תאריך", - "page": "עמוד", - "italic": "נטוי", - "link": "קישור", - "numberedList": "רשימה ממוספרת", - "numberedListShortForm": "ממוספר", - "quote": "ציטוט", - "strikethrough": "קו חוצה", - "text": "טקסט", - "underline": "קו תחתי", - "fontColorDefault": "ברירת מחדל", - "fontColorGray": "אפור", - "fontColorBrown": "חום", - "fontColorOrange": "כתום", - "fontColorYellow": "צהוב", - "fontColorGreen": "ירוק", - "fontColorBlue": "כחול", - "fontColorPurple": "סגול", - "fontColorPink": "ורוד", - "fontColorRed": "אדום", - "backgroundColorDefault": "רקע ברירת מחדל", - "backgroundColorGray": "רקע אפור", - "backgroundColorBrown": "רקע חום", - "backgroundColorOrange": "רקע כתום", - "backgroundColorYellow": "רקע צהוב", - "backgroundColorGreen": "רקע ירוק", - "backgroundColorBlue": "רקע כחול", - "backgroundColorPurple": "רקע סגול", - "backgroundColorPink": "רקע ורוד", - "backgroundColorRed": "רקע אדום", - "backgroundColorLime": "רקע ליים", - "backgroundColorAqua": "רקע אוקיינוס", - "done": "בוצע", - "cancel": "ביטול", - "tint1": "גוון 1", - "tint2": "גוון 2", - "tint3": "גוון 3", - "tint4": "גוון 4", - "tint5": "גוון 5", - "tint6": "גוון 6", - "tint7": "גוון 7", - "tint8": "גוון 8", - "tint9": "גוון 9", - "lightLightTint1": "סגול", - "lightLightTint2": "ורוד", - "lightLightTint3": "ורוד בהיר", - "lightLightTint4": "כתום", - "lightLightTint5": "צהוב", - "lightLightTint6": "ליים", - "lightLightTint7": "ירוק", - "lightLightTint8": "אוקיינוס", - "lightLightTint9": "כחול", - "urlHint": "כתובת", - "mobileHeading1": "כותרת 1", - "mobileHeading2": "כותרת 2", - "mobileHeading3": "כותרת 3", - "textColor": "צבע טקסט", - "backgroundColor": "צבע רקע", - "addYourLink": "הוספת הקישור שלך", - "openLink": "פתיחת קישור", - "copyLink": "העתקת קישור", - "removeLink": "הסרת קישור", - "editLink": "עריכת קישור", - "linkText": "טקסט", - "linkTextHint": "נא למלא טקסט", - "linkAddressHint": "נא למלא כתובת", - "highlightColor": "צבע הדגשה", - "clearHighlightColor": "איפוס צבע הדגשה", - "customColor": "צבע משלך", - "hexValue": "ערך הקס׳", - "opacity": "אטימות", - "resetToDefaultColor": "איפוס לצבע ברירת המחדל", - "ltr": "משמאל לימין", - "rtl": "מימין לשמאל", - "auto": "אוטו׳", - "cut": "גזירה", - "copy": "העתקה", - "paste": "הדבקה", - "find": "איתור", - "select": "בחירה", - "selectAll": "בחירה בהכול", - "previousMatch": "התוצאה הקודמת", - "nextMatch": "התוצאה הבאה", - "closeFind": "סגירה", - "replace": "החלפה", - "replaceAll": "החלפה של הכול", - "regex": "ביטוי רגולרי", - "caseSensitive": "תלוי רישיות", - "uploadImage": "העלאת תמונה", - "urlImage": "קישור לתמונה", - "incorrectLink": "קישור שגוי", - "upload": "העלאה", - "chooseImage": "בחירת תמונה", - "loading": "בטעינה", - "imageLoadFailed": "טעינת התמונה נכשלה", - "divider": "מפריד", - "table": "טבלה", - "colAddBefore": "הוספה לפני", - "rowAddBefore": "הוספה לפני", - "colAddAfter": "הוספה אחרי", - "rowAddAfter": "הוספה אחרי", - "colRemove": "הסרה", - "rowRemove": "הסרה", - "colDuplicate": "שכפול", - "rowDuplicate": "שכפול", - "colClear": "פינוי תוכן", - "rowClear": "פינוי תוכן", - "slashPlaceHolder": "נא להקליד ‚/’ כדי להוסיף מקטע או להתחיל להקליד", - "typeSomething": "נא להקליד משהו…", - "toggleListShortForm": "מתג", - "quoteListShortForm": "ציטוט", - "mathEquationShortForm": "נוסחה", - "codeBlockShortForm": "קוד" - }, - "favorite": { - "noFavorite": "לא עמוד מועדף", - "noFavoriteHintText": "החלקת העמוד שמאלה תוסיף אותו למועדפים שלך", - "removeFromSidebar": "הסרה מסרגל הצד", - "addToSidebar": "הצמדה לסרגל צד" - }, - "cardDetails": { - "notesPlaceholder": "יש להקליד / כדי להוסיף מקטע, או להתחיל להקליד" - }, - "blockPlaceholders": { - "todoList": "מטלה", - "bulletList": "רשימה", - "numberList": "רשימה", - "quote": "ציטוט", - "heading": "כותרת {}" - }, - "titleBar": { - "pageIcon": "סמל עמוד", - "language": "שפה", - "font": "גופן", - "actions": "פעולות", - "date": "תאריך", - "addField": "הוספת שדה", - "userIcon": "סמל משתמש" - }, - "noLogFiles": "אין קובצי יומנים", - "newSettings": { - "myAccount": { - "title": "החשבון שלי", - "subtitle": "התאמת הפרופיל שלך, ניהול אבטחת החשבון, מפתחות בינה מלאכותית או כניסה לחשבון שלך.", - "profileLabel": "שם חשבון ותמונת פרופיל", - "profileNamePlaceholder": "נא למלא את השם שלך", - "accountSecurity": "אבטחת חשבון", - "2FA": "אימות דו־שלבי", - "aiKeys": "מפתחות בינה מלאכותית", - "accountLogin": "כניסה לחשבון", - "updateNameError": "עדכון השם נכשל", - "updateIconError": "עדכון הסמל נכשל", - "deleteAccount": { - "title": "מחיקת חשבון", - "subtitle": "מחיקת החשבון וכל הנתונים שלך לצמיתות.", - "deleteMyAccount": "מחיקת החשבון שלי", - "dialogTitle": "מחיקת חשבון", - "dialogContent1": "למחוק את החשבון שלך לצמיתות?", - "dialogContent2": "זאת פעולה בלתי הפיכה והיא תסיר את הגישה מכל מרחבי הצוותים תוך מחיקת החשבון כולו לרבות מרחבי עבודה פרטיים והסרתך מכל מרחבי העבודה המשותפים." - } - }, - "workplace": { - "name": "מקום עבודה", - "title": "הגדרות מקום עבודה", - "subtitle": "התאמת המראה, ערכת העיצוב, גופן, פריסת הטקסט, התאריך, השעה והשפה של מרחב העבודה שלך.", - "workplaceName": "שם מקום העבודה", - "workplaceNamePlaceholder": "נא למלא שם למקום העבודה", - "workplaceIcon": "סמל מקום עבודה", - "workplaceIconSubtitle": "אפשר להעלות תמונה או להשתמש באמוג׳י למרחב העבודה שלך. הסמל יופיע בסרגל הצד ובהתראות שלך.", - "renameError": "שינוי שם מקום העבודה נכשל", - "updateIconError": "עדכון הסמל נכשל", - "chooseAnIcon": "נא לבחור סמל", - "appearance": { - "name": "מראה", - "themeMode": { - "auto": "אוטו׳", - "light": "בהיר", - "dark": "כהה" - }, - "language": "שפה" - } - }, - "syncState": { - "syncing": "סנכרון", - "synced": "מסונכרן", - "noNetworkConnected": "אין רשת מחוברת" - } - }, - "pageStyle": { - "title": "סגנון עמוד", - "layout": "פריסה", - "coverImage": "תמונת כריכה", - "pageIcon": "סמל עמוד", - "colors": "צבעים", - "gradient": "מדרג", - "backgroundImage": "תמונת רקע", - "presets": "ערכות מוגדרות", - "photo": "צילום", - "unsplash": "Unsplash", - "pageCover": "כריכת עמוד", - "none": "אין", - "openSettings": "פתיחת הגדרות", - "photoPermissionTitle": "@:appName רוצה לגשת לספריית התמונות שלך", - "photoPermissionDescription": "לאפשר גישה לספריית התמונות כדי להעלות תמונות.", - "doNotAllow": "לא להרשות", - "image": "תמונה" - }, - "commandPalette": { - "placeholder": "נא להקליד כדי לחפש…", - "bestMatches": "התוצאות הטובות ביותר", - "recentHistory": "היסטוריה עדכנית", - "navigateHint": "לניווט", - "loadingTooltip": "אנו מחפשים תוצאות…", - "betaLabel": "בטא", - "betaTooltip": "אנו תומכים רק בחיפוש אחר עמודים ותוכן במסמכים", - "fromTrashHint": "מהאשפה", - "noResultsHint": "לא מצאנו מה שחיפשת, כדאי לנסות לחפש ביטוי אחר.", - "clearSearchTooltip": "פינוי שדה חיפוש" - }, - "space": { - "delete": "מחיקה", - "deleteConfirmation": "מחיקה: ", - "deleteConfirmationDescription": "כל העמודים במרחב הזה יימחקו ויעברו לאשפה, ועמודים שפורסמו יוסתרו.", - "rename": "שינוי שם מרחב", - "changeIcon": "החלפת סמל", - "manage": "ניהול מרחב", - "addNewSpace": "יצירת מרחב", - "collapseAllSubPages": "צמצום כל תת־העמודים", - "createNewSpace": "יצירת מרחב חדש", - "createSpaceDescription": "אפשר ליצור מגוון מרחבים ציבוריים ופרטיים כדי לארגן את העבודה שלך בצורה טובה יותר.", - "spaceName": "שם המרחב", - "permission": "הרשאה", - "publicPermission": "ציבורי", - "publicPermissionDescription": "כל החברים במרחב העבודה שיש להם גישה מלאה", - "privatePermission": "פרטי", - "privatePermissionDescription": "רק לך יש גישה למרחב הזה", - "spaceIconBackground": "צבע הרקע", - "spaceIcon": "סמל", - "dangerZone": "אזור הסכנה", - "unableToDeleteLastSpace": "לא ניתן למחוק את המרחב האחרון", - "unableToDeleteSpaceNotCreatedByYou": "לא ניתן למחוק מרחבים שנוצרו על ידי אחרים", - "enableSpacesForYourWorkspace": "הפעלת מרחבים למרחב העבודה שלך", - "title": "מרחבים", - "defaultSpaceName": "כללי", - "upgradeSpaceTitle": "הפעלת מרחבים", - "upgradeSpaceDescription": "אפשר ליצור מגוון מרחבים ציבוריים ופרטיים כדי לארגן את מרחב העבודה שלך בצורה טובה יותר.", - "upgrade": "עדכון", - "upgradeYourSpace": "יצירת מרחבים במרוכז", - "quicklySwitch": "מעבר למרחב הבא במהירות", - "duplicate": "שכפול מרחב", - "movePageToSpace": "העבר עמוד למרחב", - "switchSpace": "החלפת מרחב" - }, - "publish": { - "hasNotBeenPublished": "העמוד הזה לא פורסם עדיין", - "reportPage": "דיווח על עמוד", - "databaseHasNotBeenPublished": "עדיין אין תמיכה בפרסום למסדי נתונים.", - "createdWith": "נוצר עם", - "downloadApp": "הורדת AppFlowy", - "copy": { - "codeBlock": "תוכן מקטע הקוד הועתק ללוח הגזירים", - "imageBlock": "קישור התמונה הועתק ללוח הגזירים", - "mathBlock": "הנוסחה המתמטית הועתקה ללוח הגזירים" - }, - "containsPublishedPage": "העמוד הזה מכיל עמוד או יותר שפורסמו. המשך בתהליך יסתיר אותם. להמשיך במחיקה?", - "publishSuccessfully": "הפרסום הצליח", - "unpublishSuccessfully": "ההסתרה הצליחה", - "publishFailed": "הפרסום נכשל", - "unpublishFailed": "ההסתרה נכשלה", - "noAccessToVisit": "אין גישה לעמוד הזה…", - "createWithAppFlowy": "יצירת אתר עם AppFlowy", - "fastWithAI": "מהיר וקל עם בינה מלאכותית.", - "tryItNow": "לנסות כעת" - }, - "web": { - "continue": "המשך", - "or": "או", - "continueWithGoogle": "להמשיך עם Google", - "continueWithGithub": "להמשיך עם GitHub", - "continueWithDiscord": "להמשיך עם Discord", - "signInAgreement": "לחיצה על „המשך” להלן, מהווה את אישורך\nלכך שקראת, הבנת והסכמת לתנאים של AppFlowy", - "and": "וגם", - "termOfUse": "תנאים", - "privacyPolicy": "מדיניות פרטיות", - "signInError": "שגיאת כניסה", - "login": "הרשמה או כניסה" - } -} diff --git a/frontend/resources/translations/hin.json b/frontend/resources/translations/hin.json index 7351d119c2..8ce86ed96b 100644 --- a/frontend/resources/translations/hin.json +++ b/frontend/resources/translations/hin.json @@ -333,7 +333,7 @@ "email": "ईमेल", "tooltipSelectIcon": "आइकन चुनें", "selectAnIcon": "एक आइकन चुनें", - "pleaseInputYourOpenAIKey": "कृपया अपनी AI key इनपुट करें", + "pleaseInputYourOpenAIKey": "कृपया अपनी OpenAI key इनपुट करें", "clickToLogout": "वर्तमान उपयोगकर्ता को लॉगआउट करने के लिए क्लिक करें" }, "shortcuts": { @@ -515,23 +515,23 @@ "referencedBoard": "रेफेरेंस बोर्ड", "referencedGrid": "रेफेरेंस ग्रिड", "referencedCalendar": "रेफेरेंस कैलेंडर", - "autoGeneratorMenuItemName": "AI लेखक", - "autoGeneratorTitleName": "AI: AI को कुछ भी लिखने के लिए कहें...", + "autoGeneratorMenuItemName": "OpenAI लेखक", + "autoGeneratorTitleName": "OpenAI: AI को कुछ भी लिखने के लिए कहें...", "autoGeneratorLearnMore": "और जानें", "autoGeneratorGenerate": "उत्पन्न करें", - "autoGeneratorHintText": "AI से पूछें...", - "autoGeneratorCantGetOpenAIKey": "AI key नहीं मिल सकी", + "autoGeneratorHintText": "OpenAI से पूछें...", + "autoGeneratorCantGetOpenAIKey": "OpenAI key नहीं मिल सकी", "autoGeneratorRewrite": "पुनः लिखें", "smartEdit": "AI सहायक", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "वर्तनी ठीक करें", "warning": "⚠️ AI प्रतिक्रियाएँ गलत या भ्रामक हो सकती हैं।", "smartEditSummarize": "सारांश", "smartEditImproveWriting": "लेख में सुधार करें", "smartEditMakeLonger": "लंबा बनाएं", - "smartEditCouldNotFetchResult": "AI से परिणाम प्राप्त नहीं किया जा सका", - "smartEditCouldNotFetchKey": "AI key नहीं लायी जा सकी", - "smartEditDisabled": "सेटिंग्स में AI कनेक्ट करें", + "smartEditCouldNotFetchResult": "OpenAI से परिणाम प्राप्त नहीं किया जा सका", + "smartEditCouldNotFetchKey": "OpenAI key नहीं लायी जा सकी", + "smartEditDisabled": "सेटिंग्स में OpenAI कनेक्ट करें", "discardResponse": "क्या आप AI प्रतिक्रियाओं को छोड़ना चाहते हैं?", "createInlineMathEquation": "समीकरण बनाएं", "toggleList": "सूची टॉगल करें", diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 1c10e40da4..1a60a1c6f5 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -103,14 +103,14 @@ "questionBubble": { "shortcuts": "Parancsikonok", "whatsNew": "Újdonságok", + "help": "Segítség & Támogatás", "markdown": "Markdown", "debug": { "name": "Debug Információ", "success": "Debug információ a vágólapra másolva", "fail": "A Debug információ nem másolható a vágólapra" }, - "feedback": "Visszacsatolás", - "help": "Segítség & Támogatás" + "feedback": "Visszacsatolás" }, "menuAppHeader": { "addPageTooltip": "Belső oldal hozzáadása", @@ -210,7 +210,8 @@ "language": "Nyelv", "user": "Felhasználó", "files": "Fájlok", - "open": "Beállítások megnyitása" + "open": "Beállítások megnyitása", + "supabaseSetting": "Supabase beállítás" }, "appearance": { "fontFamily": { @@ -276,7 +277,7 @@ "user": { "name": "Név", "selectAnIcon": "Válasszon ki egy ikont", - "pleaseInputYourOpenAIKey": "kérjük, adja meg AI kulcsát" + "pleaseInputYourOpenAIKey": "kérjük, adja meg OpenAI kulcsát" } }, "grid": { @@ -431,23 +432,23 @@ "referencedBoard": "Hivatkozott feladat tábla", "referencedGrid": "Hivatkozott táblázat", "referencedCalendar": "Hivatkozott naptár", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Kérd meg az AI-t, hogy írjon bármit...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Kérd meg az AI-t, hogy írjon bármit...", "autoGeneratorLearnMore": "Tudj meg többet", "autoGeneratorGenerate": "generál", - "autoGeneratorHintText": "Kérdezd meg az AI-t...", - "autoGeneratorCantGetOpenAIKey": "Nem lehet beszerezni az AI kulcsot", + "autoGeneratorHintText": "Kérdezd meg az OpenAI-t...", + "autoGeneratorCantGetOpenAIKey": "Nem lehet beszerezni az OpenAI kulcsot", "autoGeneratorRewrite": "Újraírni", "smartEdit": "AI asszisztensek", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Helyesírás javítása", "warning": "⚠️ Az AI-válaszok pontatlanok vagy félrevezetőek lehetnek.", "smartEditSummarize": "Összesít", "smartEditImproveWriting": "Az írás javítása", "smartEditMakeLonger": "Hosszabb legyen", - "smartEditCouldNotFetchResult": "Nem sikerült lekérni az eredményt az AI-ból", - "smartEditCouldNotFetchKey": "Nem sikerült lekérni az AI kulcsot", - "smartEditDisabled": "Csatlakoztassa az AI-t a Beállításokban", + "smartEditCouldNotFetchResult": "Nem sikerült lekérni az eredményt az OpenAI-ból", + "smartEditCouldNotFetchKey": "Nem sikerült lekérni az OpenAI kulcsot", + "smartEditDisabled": "Csatlakoztassa az OpenAI-t a Beállításokban", "discardResponse": "El szeretné vetni az AI-válaszokat?", "createInlineMathEquation": "Hozzon létre egyenletet", "toggleList": "Lista váltása", @@ -598,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index b900929966..286f5a3e06 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -9,7 +9,6 @@ "title": "Judul", "youCanAlso": "Anda juga bisa", "and": "Dan", - "failedToOpenUrl": "Gagal membuka url: {}", "blockActions": { "addBelowTooltip": "Klik untuk menambahkan di bawah", "addAboveCmd": "Alt+klik", @@ -36,39 +35,17 @@ "loginButtonText": "Masuk", "loginStartWithAnonymous": "Mulai dengan sesi anonim", "continueAnonymousUser": "Lanjutkan dengan sesi anonim", - "anonymous": "Anonim", "buttonText": "Masuk", "signingInText": "Sedang masuk", "forgotPassword": "Lupa kata sandi?", "emailHint": "Surat elektronik", "passwordHint": "Kata sandi", "dontHaveAnAccount": "Belum punya akun?", - "createAccount": "Membuat akun", "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", "unmatchedPasswordError": "Kata sandi konfirmasi tidak sesuai dengan kata sandi awal", "syncPromptMessage": "Menyinkronkan data mungkin memerlukan waktu beberapa saat. Mohon jangan tutup halaman ini", "or": "ATAU", - "signInWithGoogle": "Lanjutkan dengan Google", - "signInWithGithub": "Lanjutkan dengan GitHub", - "signInWithDiscord": "Lanjutkan dengan Discord", - "signInWithApple": "Lanjutkan dengan Apple", - "continueAnotherWay": "Lanjutkan dengan cara lain", - "signUpWithGoogle": "Mendaftar dengan Google", - "signUpWithGithub": "Mendaftar dengan GitHub", - "signUpWithDiscord": "Mendaftar dengan Discord", "signInWith": "Masuk dengan:", - "signInWithEmail": "Lanjutkan dengan Email", - "signInWithMagicLink": "Lanjut", - "signUpWithMagicLink": "Mendaftar dengan Magic Link", - "pleaseInputYourEmail": "Masukkan alamat email Anda", - "settings": "Pengaturan", - "magicLinkSent": "Magic Link terkirim!", - "invalidEmail": "Masukkan alamat email yang valid", - "alreadyHaveAnAccount": "Sudah memiliki akun?", - "logIn": "Masuk", - "generalError": "Ada yang salah. Mohon coba kembali nanti", - "limitRateError": "Demi keamanan, Anda hanya dapat meminta magic link setiap 60 detik", - "magicLinkSentDescription": "Magic Link sudah terkirim ke email Anda. Klik pada tautan untuk menyelesaikan proses masuk Anda. Tautan akan kedaluwarsa setelah 5 menit.", "LogInWithGoogle": "Masuk dengan Google", "LogInWithGithub": "Masuk dengan Github", "LogInWithDiscord": "Masuk dengan Discord", @@ -160,14 +137,14 @@ "questionBubble": { "shortcuts": "Pintasan", "whatsNew": "Apa yang baru?", + "help": "Bantuan & Dukungan", "markdown": "Penurunan harga", "debug": { "name": "Info debug", "success": "Info debug disalin ke papan klip!", "fail": "Tidak dapat menyalin info debug ke papan klip" }, - "feedback": "Masukan", - "help": "Bantuan & Dukungan" + "feedback": "Masukan" }, "menuAppHeader": { "moreButtonToolTip": "Menghapus, merubah nama, dan banyak lagi...", @@ -206,13 +183,13 @@ "addBlockBelow": "Tambahkan blok di bawah ini" }, "sideBar": { - "closeSidebar": "Tutup sidebar", - "openSidebar": "Buka sidebar", + "closeSidebar": "Close sidebar", + "openSidebar": "Open sidebar", "personal": "Pribadi", "favorites": "Favorit", - "clickToHidePersonal": "Klik untuk menutup Pribadi", - "clickToHideFavorites": "Klik untuk menutup Favorit", - "addAPage": "Tambah halaman baru" + "clickToHidePersonal": "Klik untuk menutup seksi pribadi", + "clickToHideFavorites": "Klik untuk menutup seksi favorit", + "addAPage": "Tambah sebuah page" }, "notifications": { "export": { @@ -294,7 +271,8 @@ "inputTextFieldHint": "Rahasia anda", "historicalUserList": "History masuk user", "historicalUserListTooltip": "Daftar ini menampilkan akun anonim Anda. Anda dapat mengeklik salah satu akun untuk melihat detailnya. Akun anonim dibuat dengan mengeklik tombol 'Memulai'", - "openHistoricalUser": "Klik untuk membuka akun anonim" + "openHistoricalUser": "Klik untuk membuka akun anonim", + "supabaseSetting": "Pengaturan Supabase" }, "notifications": { "enableNotifications": { @@ -305,28 +283,28 @@ "appearance": { "resetSetting": "Mengatur ulang pengaturan ini", "fontFamily": { - "label": "Jenis Font", - "search": "Cari" + "label": "Keluarga Fon", + "search": "Mencari" }, "themeMode": { - "label": "Tema", - "light": "Terang", - "dark": "Gelap", - "system": "Sesuai Sistem" + "label": "Theme Mode", + "light": "Mode Terang", + "dark": "Mode Gelap", + "system": "Adapt to System" }, "layoutDirection": { - "label": "Arah Tampilan", - "hint": "Mengatur arah tampilan konten, apakah dari kiri ke kanan atau kanan ke kiri.", - "ltr": "Kiri ke Kanan", - "rtl": "Kanan ke Kiri" + "label": "Arah Layout", + "hint": "Mengontrol aliran konten pada layar Anda, dari kiri ke kanan atau kanan ke kiri.", + "ltr": "LTR", + "rtl": "RTL" }, "textDirection": { - "label": "Arah Teks Bawaan", - "hint": "Atur arah teks bawaan, apakah dari kiri ke kanan atau kanan ke kiri.", - "ltr": "Kiri ke Kanan", - "rtl": "Kanan ke Kiri", - "auto": "Otomatis", - "fallback": "Sesuai Arah Tampilan" + "label": "Arah teks default", + "hint": "Menentukan apakah teks harus dimulai dari kiri atau kanan sebagai default.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "AUTO", + "fallback": "Sama seperti arah layout" }, "themeUpload": { "button": "Mengunggah", @@ -398,20 +376,9 @@ "email": "Surel", "tooltipSelectIcon": "Pilih ikon", "selectAnIcon": "Pilih ikon", - "pleaseInputYourOpenAIKey": "silakan masukkan kunci AI Anda", - "clickToLogout": "Klik untuk keluar dari pengguna saat ini", - "pleaseInputYourStabilityAIKey": "Masukkan kunci Stability AI anda" - }, - "mobile": { - "personalInfo": "Informasi pribadi", - "username": "Nama Pengguna", - "usernameEmptyError": "Nama pengguna tidak boleh kosong", - "about": "Tentang", - "pushNotifications": "Pemberitahuan Dorong", - "support": "Dukungan", - "joinDiscord": "Bergabunglah dengan kami di Discord", - "privacyPolicy": "Kebijakan Privasi", - "userAgreement": "Perjanjian Pengguna" + "pleaseInputYourOpenAIKey": "silakan masukkan kunci OpenAI Anda", + "pleaseInputYourStabilityAIKey": "Masukkan kunci Stability AI anda", + "clickToLogout": "Klik untuk keluar dari pengguna saat ini" }, "shortcuts": { "shortcutsLabel": "Pintasan", @@ -423,6 +390,17 @@ "resetToDefault": "Mengatur ulang ke keybinding default", "couldNotLoadErrorMsg": "Tidak dapat memuat pintasan, Coba lagi", "couldNotSaveErrorMsg": "Tidak dapat menyimpan pintasan, Coba lagi" + }, + "mobile": { + "personalInfo": "Informasi pribadi", + "username": "Nama Pengguna", + "usernameEmptyError": "Nama pengguna tidak boleh kosong", + "about": "Tentang", + "pushNotifications": "Pemberitahuan Dorong", + "support": "Dukungan", + "joinDiscord": "Bergabunglah dengan kami di Discord", + "privacyPolicy": "Kebijakan Privasi", + "userAgreement": "Perjanjian Pengguna" } }, "grid": { @@ -611,23 +589,23 @@ "referencedBoard": "Papan Referensi", "referencedGrid": "Kisi yang Direferensikan", "referencedCalendar": "Kalender Referensi", - "autoGeneratorMenuItemName": "Penulis AI", - "autoGeneratorTitleName": "AI: Minta AI untuk menulis apa saja...", + "autoGeneratorMenuItemName": "Penulis OpenAI", + "autoGeneratorTitleName": "OpenAI: Minta AI untuk menulis apa saja...", "autoGeneratorLearnMore": "Belajarlah lagi", "autoGeneratorGenerate": "Menghasilkan", - "autoGeneratorHintText": "Tanya AI...", - "autoGeneratorCantGetOpenAIKey": "Tidak bisa mendapatkan kunci AI", + "autoGeneratorHintText": "Tanya OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Tidak bisa mendapatkan kunci OpenAI", "autoGeneratorRewrite": "Menulis kembali", "smartEdit": "Asisten AI", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Perbaiki ejaan", "warning": "⚠️ Respons AI bisa jadi tidak akurat atau menyesatkan.", "smartEditSummarize": "Meringkaskan", "smartEditImproveWriting": "Perbaiki tulisan", "smartEditMakeLonger": "Buat lebih lama", - "smartEditCouldNotFetchResult": "Tidak dapat mengambil hasil dari AI", - "smartEditCouldNotFetchKey": "Tidak dapat mengambil kunci AI", - "smartEditDisabled": "Hubungkan AI di Pengaturan", + "smartEditCouldNotFetchResult": "Tidak dapat mengambil hasil dari OpenAI", + "smartEditCouldNotFetchKey": "Tidak dapat mengambil kunci OpenAI", + "smartEditDisabled": "Hubungkan OpenAI di Pengaturan", "discardResponse": "Apakah Anda ingin membuang respons AI?", "createInlineMathEquation": "Buat persamaan", "toggleList": "Beralih Daftar", @@ -675,8 +653,8 @@ "defaultColor": "Bawaan" }, "image": { - "addAnImage": "Tambah gambar", - "copiedToPasteBoard": "Tautan gambar telah disalin ke papan klip" + "copiedToPasteBoard": "Tautan gambar telah disalin ke papan klip", + "addAnImage": "Tambah gambar" }, "outline": { "addHeadingToCreateOutline": "Tambahkan judul untuk membuat daftar isi." @@ -712,8 +690,8 @@ "placeholder": "Masukkan URL gambar" }, "ai": { - "label": "Buat gambar dari AI", - "placeholder": "Masukkan perintah agar AI menghasilkan gambar" + "label": "Buat gambar dari OpenAI", + "placeholder": "Masukkan perintah agar OpenAI menghasilkan gambar" }, "stability_ai": { "label": "Buat gambar dari Stability AI", @@ -731,7 +709,7 @@ "placeholder": "Tempel atau ketik tautan gambar" }, "searchForAnImage": "Mencari gambar", - "pleaseInputYourOpenAIKey": "masukkan kunci AI Anda di halaman Pengaturan", + "pleaseInputYourOpenAIKey": "masukkan kunci OpenAI Anda di halaman Pengaturan", "pleaseInputYourStabilityAIKey": "masukkan kunci AI Stabilitas Anda di halaman Pengaturan" }, "codeBlock": { @@ -1044,4 +1022,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 7fb463da20..74282485d2 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "Me", "welcomeText": "Benvenuto in @:appName", - "welcomeTo": "Benvenuto a", "githubStarText": "Vota su GitHub", "subscribeNewsletterText": "Sottoscrivi la Newsletter", "letsGoButtonText": "Andiamo", "title": "Titolo", "youCanAlso": "Puoi anche", "and": "E", - "failedToOpenUrl": "Apertura URL fallita: {}", "blockActions": { "addBelowTooltip": "Fare clic per aggiungere di seguito", "addAboveCmd": "Alt+clic", @@ -37,35 +35,15 @@ "loginStartWithAnonymous": "Inizia con una sessione anonima", "continueAnonymousUser": "Continua con una sessione anonima", "buttonText": "Accedi", - "signingInText": "Accesso in corso...", "forgotPassword": "Password Dimentica?", "emailHint": "Email", "passwordHint": "Password", "dontHaveAnAccount": "Non hai un account?", - "createAccount": "Crea account", "repeatPasswordEmptyError": "La password ripetuta non può essere vuota", "unmatchedPasswordError": "La password ripetuta non è uguale alla password", "syncPromptMessage": "La sincronizzazione dei dati potrebbe richiedere del tempo. Per favore, non chiudere questa pagina", "or": "O", - "signInWithGoogle": "Continua con Google", - "signInWithGithub": "Continua con Github", - "signInWithDiscord": "Continua con Discord", - "signUpWithGoogle": "Registrati con Google", - "signUpWithGithub": "Registrati con Github", - "signUpWithDiscord": "Registrati con Discord", "signInWith": "Loggati con:", - "signInWithEmail": "Continua con Email", - "signInWithMagicLink": "Continua", - "signUpWithMagicLink": "Registrati con un Link Magico", - "pleaseInputYourEmail": "Per favore, inserisci il tuo indirizzo email", - "settings": "Impostazioni", - "magicLinkSent": "Link Magico inviato!", - "invalidEmail": "Per favore, inserisci un indirizzo email valido", - "alreadyHaveAnAccount": "Hai già un account?", - "logIn": "Accedi", - "generalError": "Qualcosa è andato storto. Per favore, riprova più tardi", - "limitRateError": "Per ragioni di sicurezza, puoi richiedere un link magico ogni 60 secondi", - "magicLinkSentDescription": "Un Link Magico è stato inviato alla tua email. Clicca il link per completare il tuo accesso. Il link scadrà dopo 5 minuti.", "LogInWithGoogle": "Accedi con Google", "LogInWithGithub": "Accedi con Github", "LogInWithDiscord": "Accedi con Discord", @@ -75,7 +53,6 @@ "chooseWorkspace": "Scegli il tuo spazio di lavoro", "create": "Crea spazio di lavoro", "reset": "Ripristina lo spazio di lavoro", - "renameWorkspace": "Rinomina workspace", "resetWorkspacePrompt": "Il ripristino dello spazio di lavoro eliminerà tutte le pagine e i dati al suo interno. Sei sicuro di voler ripristinare lo spazio di lavoro? In alternativa, puoi contattare il team di supporto per ristabilire lo spazio di lavoro", "hint": "spazio di lavoro", "notFoundError": "Spazio di lavoro non trovato", @@ -85,39 +62,14 @@ "reportIssueOnGithub": "Segnalate un problema su Github", "exportLogFiles": "Esporta i file di log", "reachOut": "Contattaci su Discord" - }, - "deleteWorkspaceHintText": "Sei sicuro di voler cancellare la workspace? Questa azione non è reversibile, e ogni pagina che hai pubblicato sarà rimossa.", - "createSuccess": "Workspace creata con successo", - "createFailed": "Creazione workspace fallita", - "createLimitExceeded": "Hai raggiunto il numero massimo di workspace permesse per il tuo account. Se hai bisogno di ulteriori workspace per continuare il tuo lavoro, per favore fai richiesta su Github", - "deleteSuccess": "Workspace cancellata con successo", - "deleteFailed": "Cancellazione workspace fallita", - "openSuccess": "Workspace aperta con successo", - "openFailed": "Apertura workspace fallita", - "renameSuccess": "Workspace rinominata con successo", - "renameFailed": "Rinomina workspace fallita", - "updateIconSuccess": "Icona della workspace aggiornata con successo", - "updateIconFailed": "Aggiornamento icona della workspace fallito", - "cannotDeleteTheOnlyWorkspace": "Impossibile cancellare l'unica workspace", - "fetchWorkspacesFailed": "Recupero workspaces fallito", - "leaveCurrentWorkspace": "Lascia workspace", - "leaveCurrentWorkspacePrompt": "Sei sicuro di voler lasciare la workspace corrente?" + } }, "shareAction": { "buttonText": "Condividi", "workInProgress": "Prossimamente", "markdown": "Markdown", - "clipboard": "Copia", "csv": "CSV", - "copyLink": "Copia Link", - "publishToTheWeb": "Pubblica sul Web", - "publishToTheWebHint": "Crea un sito con AppFlowy", - "publish": "Pubblica", - "unPublish": "Annulla la pubblicazione", - "visitSite": "Visita sito", - "exportAsTab": "Esporta come", - "publishTab": "Pubblica", - "shareTab": "Condividi" + "copyLink": "Copia Link" }, "moreAction": { "small": "piccolo", @@ -128,7 +80,6 @@ "moreOptions": "Più opzioni", "wordCount": "Conteggio parole: {}", "charCount": "Numero di caratteri: {}", - "createdAt": "Creata: {}", "deleteView": "Cancella", "duplicateView": "Duplica" }, @@ -148,9 +99,7 @@ "openNewTab": "Apri in una nuova scheda", "moveTo": "Sposta in", "addToFavorites": "Aggiungi ai preferiti", - "copyLink": "Copia link", - "changeIcon": "Cambia icona", - "collapseAllPages": "Comprimi le sottopagine" + "copyLink": "Copia link" }, "blankPageTitle": "Pagina vuota", "newPageText": "Nuova pagina", @@ -158,34 +107,6 @@ "newGridText": "Nuova griglia", "newCalendarText": "Nuovo calendario", "newBoardText": "Nuova bacheca", - "chat": { - "newChat": "Chat AI", - "inputMessageHint": "Chiedi a @:appName AI", - "inputLocalAIMessageHint": "Chiedi a @:appName Local AI", - "unsupportedCloudPrompt": "Questa funzione è disponibile solo quando si usa @:appName Cloud", - "relatedQuestion": "Correlato", - "serverUnavailable": "Servizio temporaneamente non disponibile. Per favore, riprova più tardi.", - "aiServerUnavailable": "🌈 Uh-oh! 🌈. Un unicorno ha mangiato la nostra risposta. Per favore, riprova!", - "clickToRetry": "Clicca per riprovare", - "regenerateAnswer": "Rigenera", - "question1": "Come usare Kanban per organizzare le attività", - "question2": "Spiega il metodo GTD", - "question3": "Perché usare Rust", - "question4": "Ricetta con cos'è presente nella mia cucina", - "aiMistakePrompt": "Le IA possono fare errori. Controlla le informazioni importanti.", - "chatWithFilePrompt": "Vuoi chattare col file?", - "indexFileSuccess": "Indicizzazione file completata con successo", - "inputActionNoPages": "Nessuna pagina risultante", - "referenceSource": { - "zero": "0 fonti trovate", - "one": "{count} fonte trovata", - "other": "{count} fonti trovate" - }, - "clickToMention": "Clicca per menzionare una pagina", - "uploadFile": "Carica file PDF, MD o TXT con la quale chattare", - "questionDetail": "Salve {}! Come posso aiutarti oggi?", - "indexingFile": "Indicizzazione {}" - }, "trash": { "text": "Cestino", "restoreAll": "Ripristina Tutto", @@ -204,13 +125,11 @@ "caption": "Questa azione non può essere annullata." }, "mobile": { - "actions": "Azioni del cestino", "empty": "Il cestino è vuoto", "emptyDescription": "Non hai alcun file eliminato", "isDeleted": "è stato cancellato", "isRestored": "è stato ripristinato" - }, - "confirmDeleteTitle": "Sei sicuro di voler eliminare questa pagina permanentemente?" + } }, "deletePagePrompt": { "text": "Questa pagina è nel Cestino", @@ -221,14 +140,14 @@ "questionBubble": { "shortcuts": "Scorciatoie", "whatsNew": "Cosa c'è di nuovo?", + "help": "Aiuto & Supporto", "markdown": "Markdown", "debug": { "name": "Informazioni di debug", "success": "Informazioni di debug copiate negli appunti!", "fail": "Impossibile copiare le informazioni di debug negli appunti" }, - "feedback": "Feedback", - "help": "Aiuto & Supporto" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Rimuovi, rinomina e altro...", @@ -264,44 +183,17 @@ "dragRow": "Premere a lungo per riordinare la riga", "viewDataBase": "Visualizza banca dati", "referencePage": "Questo {nome} è referenziato", - "addBlockBelow": "Aggiungi un blocco qui sotto", - "aiGenerate": "Genera" + "addBlockBelow": "Aggiungi un blocco qui sotto" }, "sideBar": { "closeSidebar": "Close sidebar", "openSidebar": "Open sidebar", "personal": "Personale", - "private": "Privato", "favorites": "Preferiti", - "clickToHidePrivate": "Clicca per nascondere l'area privata\nLe pagine create qui sono visibili solo a te", - "clickToHideWorkspace": "Clicca per nascondere la workspace\nLe pagine che crei qui sono visibili a ogni membro", "clickToHidePersonal": "Fare clic per nascondere la sezione personale", "clickToHideFavorites": "Fare clic per nascondere la sezione dei preferiti", "addAPage": "Aggiungi una pagina", - "addAPageToPrivate": "Aggiungi pagina all'area privata", - "addAPageToWorkspace": "Aggiungi pagina alla workspace", - "recent": "Recente", - "today": "Oggi", - "thisWeek": "Questa settimana", - "others": "Preferiti precedenti", - "justNow": "poco fa", - "minutesAgo": "{count} minuti fa", - "lastViewed": "Visto per ultimo", - "favoriteAt": "Aggiunto tra ai preferiti", - "emptyRecent": "Nessun documento recente", - "emptyRecentDescription": "Quando vedrai documenti, appariranno qui per accesso facilitato", - "emptyFavorite": "Nessun documento preferito", - "emptyFavoriteDescription": "Comincia a esplorare e marchia documenti come preferiti. Verranno elencati qui per accesso rapido!", - "removePageFromRecent": "Rimuovere questa pagina da Recenti?", - "removeSuccess": "Rimozione effettuata con successo", - "favoriteSpace": "Preferiti", - "RecentSpace": "Recenti", - "Spaces": "Aree", - "upgradeToPro": "Aggiorna a Pro", - "upgradeToAIMax": "Sblocca AI illimitata", - "storageLimitDialogTitle": "Hai esaurito lo spazio d'archiviazione gratuito. Aggiorna per avere spazio d'archiviazione illimitato!", - "aiResponseLimitTitle": "Hai esaurito le risposte AI gratuite. Aggiorna al Piano Pro per acquistare un add-on AI per avere risposte illimitate", - "aiResponseLimitDialogTitle": "Numero di riposte AI raggiunto" + "recent": "Recente" }, "notifications": { "export": { @@ -388,6 +280,9 @@ "cloudServerType": "Server cloud", "cloudServerTypeTip": "Tieni presente che dopo aver cambiato il server cloud il tuo account corrente potrebbe disconnettersi", "cloudLocal": "Locale", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL di Supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'url di supabase non può essere vuoto", "cloudAppFlowy": "@:appName Cloud Beta", "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted (autogestito)", "appFlowyCloudUrlCanNotBeEmpty": "L'url del cloud non può essere vuoto", @@ -416,7 +311,8 @@ "importAppFlowyDataDescription": "Copia i dati da una cartella dati @:appName esterna e importali nella cartella dati @:appName corrente", "importSuccess": "Importazione della cartella dati @:appName riuscita", "importFailed": "L'importazione della cartella dati di @:appName non è riuscita", - "importGuide": "Per ulteriori dettagli si prega di consultare il documento di riferimento" + "importGuide": "Per ulteriori dettagli si prega di consultare il documento di riferimento", + "supabaseSetting": "Impostazione Supabase" }, "notifications": { "enableNotifications": { @@ -531,9 +427,20 @@ "email": "E-mail", "tooltipSelectIcon": "Seleziona l'icona", "selectAnIcon": "Seleziona un'icona", - "pleaseInputYourOpenAIKey": "inserisci la tua chiave AI", - "clickToLogout": "Fare clic per disconnettere l'utente corrente", - "pleaseInputYourStabilityAIKey": "per favore inserisci la tua chiave Stability AI" + "pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI", + "pleaseInputYourStabilityAIKey": "per favore inserisci la tua chiave Stability AI", + "clickToLogout": "Fare clic per disconnettere l'utente corrente" + }, + "shortcuts": { + "shortcutsLabel": "Scorciatoie", + "command": "Comando", + "keyBinding": "Combinazione", + "addNewCommand": "Aggiungi un nuovo comando", + "updateShortcutStep": "Premi la combinazione di tasti e poi premi INVIO", + "shortcutIsAlreadyUsed": "Questa scorciatoia è già usata per: {conflict}", + "resetToDefault": "Ripristina le combinazioni di default", + "couldNotLoadErrorMsg": "Impossibile caricare le scorciatoie, Riprova.", + "couldNotSaveErrorMsg": "Impossibile salvare le scorciatoie, Riprova." }, "mobile": { "personalInfo": "Informazione personale", @@ -551,17 +458,6 @@ "selectLayout": "Seleziona disposizione", "selectStartingDay": "Seleziona il giorno di inizio", "version": "Versione" - }, - "shortcuts": { - "shortcutsLabel": "Scorciatoie", - "command": "Comando", - "keyBinding": "Combinazione", - "addNewCommand": "Aggiungi un nuovo comando", - "updateShortcutStep": "Premi la combinazione di tasti e poi premi INVIO", - "shortcutIsAlreadyUsed": "Questa scorciatoia è già usata per: {conflict}", - "resetToDefault": "Ripristina le combinazioni di default", - "couldNotLoadErrorMsg": "Impossibile caricare le scorciatoie, Riprova.", - "couldNotSaveErrorMsg": "Impossibile salvare le scorciatoie, Riprova." } }, "grid": { @@ -810,23 +706,23 @@ "referencedGrid": "Griglia di riferimento", "referencedCalendar": "Calendario referenziato", "referencedDocument": "Documento riferito", - "autoGeneratorMenuItemName": "Scrittore AI", - "autoGeneratorTitleName": "AI: chiedi all'AI di scrivere qualsiasi cosa...", + "autoGeneratorMenuItemName": "Scrittore OpenAI", + "autoGeneratorTitleName": "OpenAI: chiedi all'AI di scrivere qualsiasi cosa...", "autoGeneratorLearnMore": "Saperne di più", "autoGeneratorGenerate": "creare", - "autoGeneratorHintText": "Chiedi a AI...", - "autoGeneratorCantGetOpenAIKey": "Impossibile ottenere la chiave AI", + "autoGeneratorHintText": "Chiedi a OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Impossibile ottenere la chiave OpenAI", "autoGeneratorRewrite": "Riscrivere", "smartEdit": "Assistenti AI", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Correggi l'ortografia", "warning": "⚠️ Le risposte AI possono essere imprecise o fuorvianti.", "smartEditSummarize": "Riassumere", "smartEditImproveWriting": "Migliora la scrittura", "smartEditMakeLonger": "Rendi più lungo", - "smartEditCouldNotFetchResult": "Impossibile recuperare il risultato da AI", - "smartEditCouldNotFetchKey": "Impossibile recuperare la chiave AI", - "smartEditDisabled": "Connetti AI in Impostazioni", + "smartEditCouldNotFetchResult": "Impossibile recuperare il risultato da OpenAI", + "smartEditCouldNotFetchKey": "Impossibile recuperare la chiave OpenAI", + "smartEditDisabled": "Connetti OpenAI in Impostazioni", "discardResponse": "Vuoi scartare le risposte AI?", "createInlineMathEquation": "Crea un'equazione", "fonts": "Caratteri", @@ -882,8 +778,8 @@ "defaultColor": "Predefinito" }, "image": { - "addAnImage": "Aggiungi un'immagine", "copiedToPasteBoard": "Il link dell'immagine è stato copiato negli appunti", + "addAnImage": "Aggiungi un'immagine", "imageUploadFailed": "Caricamento dell'immagine non riuscito" }, "urlPreview": { @@ -935,8 +831,8 @@ "placeholder": "Inserisci l'URL dell'immagine" }, "ai": { - "label": "Genera immagine da AI", - "placeholder": "Inserisci la richiesta affinché AI generi l'immagine" + "label": "Genera immagine da OpenAI", + "placeholder": "Inserisci la richiesta affinché OpenAI generi l'immagine" }, "stability_ai": { "label": "Genera immagine da Stability AI", @@ -957,15 +853,15 @@ "label": "Unsplash" }, "searchForAnImage": "Cerca un'immagine", - "pleaseInputYourOpenAIKey": "inserisci la tua chiave AI nella pagina Impostazioni", + "pleaseInputYourOpenAIKey": "inserisci la tua chiave OpenAI nella pagina Impostazioni", + "pleaseInputYourStabilityAIKey": "inserisci la chiave Stability AI nella pagina Impostazioni", "saveImageToGallery": "Salva immagine", "failedToAddImageToGallery": "Impossibile aggiungere l'immagine alla galleria", "successToAddImageToGallery": "Immagine aggiunta alla galleria con successo", "unableToLoadImage": "Impossibile caricare l'immagine", "maximumImageSize": "La dimensione massima supportata per il caricamento delle immagini è di 10 MB", "uploadImageErrorImageSizeTooBig": "Le dimensioni dell'immagine devono essere inferiori a 10 MB", - "imageIsUploading": "L'immagine si sta caricando", - "pleaseInputYourStabilityAIKey": "inserisci la chiave Stability AI nella pagina Impostazioni" + "imageIsUploading": "L'immagine si sta caricando" }, "codeBlock": { "language": { @@ -1366,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index ebe679ad84..00738dde42 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "ユーザー", "welcomeText": "Welcome to @:appName", - "welcomeTo": "ようこそ", "githubStarText": "Star on GitHub", "subscribeNewsletterText": "新着情報を受け取る", "letsGoButtonText": "Let's Go", "title": "タイトル", "youCanAlso": "他にもこんなことができます", "and": "と", - "failedToOpenUrl": "URLを開けませんでした: {}", "blockActions": { "addBelowTooltip": "クリックして下に追加してください", "addAboveCmd": "Alt+クリック", @@ -28,258 +26,118 @@ "alreadyHaveAnAccount": "すでにアカウントを登録済ですか?", "emailHint": "メールアドレス", "passwordHint": "パスワード", - "repeatPasswordHint": "パスワード(確認用)", - "signUpWith": "サインアップ:" + "repeatPasswordHint": "パスワード(確認用)" }, "signIn": { - "loginTitle": "@:appNameにログイン", + "loginTitle": "@:appName にログイン", "loginButtonText": "ログイン", - "loginStartWithAnonymous": "匿名で続行", - "continueAnonymousUser": "匿名で続行", - "anonymous": "匿名", "buttonText": "サインイン", "signingInText": "サインイン中...", - "forgotPassword": "パスワードを忘れましたか?", + "forgotPassword": "パスワードをお忘れですか?", "emailHint": "メールアドレス", "passwordHint": "パスワード", - "dontHaveAnAccount": "アカウントをお持ちでないですか?", - "createAccount": "アカウントを作成", - "repeatPasswordEmptyError": "パスワードを再入力してください", - "unmatchedPasswordError": "パスワードが一致しません", - "syncPromptMessage": "データの同期には時間がかかる場合があります。このページを閉じないでください", - "or": "または", - "signInWithGoogle": "Googleで続行", - "signInWithGithub": "GitHubで続行", - "signInWithDiscord": "Discordで続行", - "signInWithApple": "Appleで続行", - "continueAnotherWay": "別の方法で続行", - "signUpWithGoogle": "Googleで登録", - "signUpWithGithub": "GitHubで登録", - "signUpWithDiscord": "Discordで登録", - "signInWith": "続行方法:", - "signInWithEmail": "メールで続行", - "signInWithMagicLink": "続行", - "signUpWithMagicLink": "Magic Linkで登録", - "pleaseInputYourEmail": "メールアドレスを入力してください", - "settings": "設定", - "magicLinkSent": "Magic Linkが送信されました!", - "invalidEmail": "有効なメールアドレスを入力してください", - "alreadyHaveAnAccount": "すでにアカウントをお持ちですか?", - "logIn": "ログイン", - "generalError": "エラーが発生しました。後でもう一度お試しください", - "limitRateError": "セキュリティのため、Magic Linkは60秒に1回しかリクエストできません", - "magicLinkSentDescription": "Magic Linkがあなたのメールアドレスに送信されました。リンクをクリックしてログインを完了してください。リンクは5分後に無効になります。" + "dontHaveAnAccount": "まだアカウントをお持ちではないですか?", + "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません", + "unmatchedPasswordError": "パスワード(確認用)が一致しません", + "LogInWithGoogle": "Googleでログイン", + "LogInWithGithub": "GitHubでログイン", + "LogInWithDiscord": "Discordでログイン", + "loginAsGuestButtonText": "始めましょう" }, "workspace": { - "chooseWorkspace": "ワークスペースを選択", - "defaultName": "私のワークスペース", - "create": "ワークスペースを作成", - "new": "新しいワークスペース", - "importFromNotion": "Notionからインポート", - "learnMore": "もっと詳しく知る", - "reset": "ワークスペースをリセット", - "renameWorkspace": "ワークスペースの名前を変更", - "workspaceNameCannotBeEmpty": "ワークスペース名は空にできません", - "resetWorkspacePrompt": "ワークスペースをリセットすると、すべてのページとデータが削除されます。本当にリセットしますか?代わりに、サポートチームに連絡してワークスペースを復元することもできます。", + "chooseWorkspace": "ワークスベースを選択", + "create": "ワークスペースを作成する", "hint": "ワークスペース", - "notFoundError": "ワークスペースが見つかりません", - "failedToLoad": "問題が発生しました!ワークスペースの読み込みに失敗しました。@:appNameの開いているインスタンスをすべて閉じて、再試行してください。", + "notFoundError": "ワークスペースがみつかりません", "errorActions": { "reportIssue": "問題を報告", - "reportIssueOnGithub": "Githubで問題を報告", - "exportLogFiles": "ログファイルをエクスポート", - "reachOut": "Discordで連絡" - }, - "menuTitle": "ワークスペース", - "deleteWorkspaceHintText": "ワークスペースを削除してもよろしいですか?この操作は元に戻せません。公開したページはすべて非公開になります。", - "createSuccess": "ワークスペースが正常に作成されました", - "createFailed": "ワークスペースの作成に失敗しました", - "createLimitExceeded": "アカウントで許可されている最大数のワークスペースに達しました。追加のワークスペースが必要な場合は、Githubでリクエストしてください。", - "deleteSuccess": "ワークスペースが正常に削除されました", - "deleteFailed": "ワークスペースの削除に失敗しました", - "openSuccess": "ワークスペースを正常に開きました", - "openFailed": "ワークスペースを開くことができませんでした", - "renameSuccess": "ワークスペース名が正常に変更されました", - "renameFailed": "ワークスペース名の変更に失敗しました", - "updateIconSuccess": "ワークスペースアイコンを正常に更新しました", - "updateIconFailed": "ワークスペースアイコンの更新に失敗しました", - "cannotDeleteTheOnlyWorkspace": "ワークスペースは削除できません", - "fetchWorkspacesFailed": "ワークスペースの取得に失敗しました", - "leaveCurrentWorkspace": "ワークスペースを離れる", - "leaveCurrentWorkspacePrompt": "現在のワークスペースから退出してもよろしいですか?" + "reportIssueOnGithub": "GitHubで問題を報告", + "exportLogFiles": "ログファイルを出力" + } }, "shareAction": { - "buttonText": "共有", - "workInProgress": "近日公開", + "buttonText": "共有する", + "workInProgress": "Coming soon", "markdown": "Markdown", - "html": "HTML", - "clipboard": "クリップボードにコピー", "csv": "CSV", - "copyLink": "リンクをコピー", - "publishToTheWeb": "Webに公開", - "publishToTheWebHint": "AppFlowyでウェブサイトを作成", - "publish": "公開", - "unPublish": "非公開", - "visitSite": "サイトを訪問", - "exportAsTab": "エクスポート形式", - "publishTab": "公開", - "shareTab": "共有", - "publishOnAppFlowy": "AppFlowyで公開", - "shareTabTitle": "コラボレーションに招待", - "shareTabDescription": "誰とでも簡単にコラボレーション", - "copyLinkSuccess": "リンクをクリップボードにコピーしました", - "copyShareLink": "共有リンクをコピー", - "copyLinkFailed": "リンクをクリップボードにコピーできませんでした", - "copyLinkToBlockSuccess": "ブロックリンクをクリップボードにコピーしました", - "copyLinkToBlockFailed": "ブロックリンクをクリップボードにコピーできませんでした", - "manageAllSites": "すべてのサイトを管理する", - "updatePathName": "パス名を更新" + "copyLink": "リンクをコピー" }, "moreAction": { - "small": "小", - "medium": "中", - "large": "大", + "small": "小さい", + "medium": "中くらい", + "large": "大きい", "fontSize": "フォントサイズ", - "import": "インポート", - "moreOptions": "その他のオプション", - "wordCount": "単語数: {}", - "charCount": "文字数: {}", - "createdAt": "作成日: {}", - "deleteView": "削除", - "duplicateView": "複製", - "wordCountLabel": "単語数:", - "charCountLabel": "文字数:", - "createdAtLabel": "作成日:", - "syncedAtLabel": "同期済み:" + "import": "取り込む", + "moreOptions": "より多くのオプション" }, "importPanel": { "textAndMarkdown": "テキストとマークダウン", - "documentFromV010": "v0.1.0 以降のドキュメント", + "documentFromV010": "v0.1.0 のドキュメント", "databaseFromV010": "v0.1.0 以降のデータベース", - "notionZip": "Notion エクスポートされた Zip ファイル", "csv": "CSV", "database": "データベース" }, "disclosureAction": { "rename": "名前を変更", "delete": "削除", - "duplicate": "複製", + "duplicate": "コピーを作成", "unfavorite": "お気に入りから削除", "favorite": "お気に入りに追加", "openNewTab": "新しいタブで開く", - "moveTo": "移動", "addToFavorites": "お気に入りに追加", - "copyLink": "リンクをコピー", - "changeIcon": "アイコンを変更", - "collapseAllPages": "すべてのサブページを折りたたむ", - "movePageTo": "ページを移動", - "move": "動く" + "copyLink": "リンクをコピー" }, - "blankPageTitle": "空白のページ", + "blankPageTitle": "空のページ", "newPageText": "新しいページ", "newDocumentText": "新しいドキュメント", - "newGridText": "新しいグリッド", "newCalendarText": "新しいカレンダー", "newBoardText": "新しいボード", - "chat": { - "newChat": "AIチャット", - "inputMessageHint": "@:appName AIに質問", - "inputLocalAIMessageHint": "@:appName ローカルAIに質問", - "unsupportedCloudPrompt": "この機能は@:appName Cloudでのみ利用可能です", - "relatedQuestion": "関連質問", - "serverUnavailable": "サービスが一時的に利用できません。後でもう一度お試しください。", - "aiServerUnavailable": "🌈 おっと! 🌈 ユニコーンが返答を食べちゃいました。再試行してください!", - "retry": "リトライ", - "clickToRetry": "再試行をクリック", - "regenerateAnswer": "回答を再生成", - "question1": "タスク管理にKanbanを使用する方法", - "question2": "GTDメソッドを説明してください", - "question3": "なぜRustを使うのか", - "question4": "キッチンにある材料のレシピ", - "question5": "マイページ用のイラストを作成する", - "question6": "来週のやることリストを作成する", - "aiMistakePrompt": "AIは間違いを犯すことがあります。重要な情報は確認してください。", - "chatWithFilePrompt": "ファイルとチャットしますか?", - "indexFileSuccess": "ファイルのインデックス作成が成功しました", - "inputActionNoPages": "ページ結果がありません", - "referenceSource": { - "zero": "0 件のソースが見つかりました", - "one": "{count} 件のソースが見つかりました", - "other": "{count} 件のソースが見つかりました" - }, - "clickToMention": "ページをメンションするにはクリック", - "uploadFile": "PDF、md、txtファイルをアップロードしてチャット", - "questionDetail": "こんにちは、{}!今日はどんなお手伝いをしましょうか?", - "indexingFile": "{}をインデックス作成中", - "generatingResponse": "レスポンスの作成", - "selectSources": "ソースを選択", - "sourcesLimitReached": "選択できるのは最上位のドキュメントとその子ドキュメントの3つまでです。", - "sourceUnsupported": "現時点ではデータベースとのチャットはサポートされていません", - "regenerate": "もう一度やり直してください", - "addToPageButton": "ページにメッセージを追加", - "addToPageTitle": "メッセージを追加...", - "addToNewPageName": "\"{}\"から抽出されたメッセージ", - "addToNewPageSuccessToast": "メッセージが追加されました", - "openPagePreviewFailedToast": "ページを開けませんでした" - }, "trash": { - "text": "ゴミ箱", - "restoreAll": "すべて復元", - "restore": "復元する", - "deleteAll": "すべて削除", + "text": "ごみ箱", + "restoreAll": "全て復元", + "deleteAll": "全て削除", "pageHeader": { "fileName": "ファイル名", - "lastModified": "最終更新日", - "created": "作成日" + "lastModified": "最終更新日時", + "created": "作成日時" }, "confirmDeleteAll": { - "title": "ゴミ箱内のすべてのページを削除してもよろしいですか?", - "caption": "この操作は元に戻せません。" + "title": "ゴミ箱内のすべてのページを削除してもよろしいですか?", + "caption": "この操作は元に戻すことができません。" }, "confirmRestoreAll": { - "title": "ゴミ箱内のすべてのページを復元してもよろしいですか?", - "caption": "この操作は元に戻せません。" - }, - "restorePage": { - "title": "復元する: {}", - "caption": "このページを復元してもよろしいですか?" + "title": "ゴミ箱内のすべてのページを復元してもよろしいですか?", + "caption": "この操作は元に戻すことができません。" }, "mobile": { - "actions": "ゴミ箱の操作", - "empty": "ゴミ箱にページやスペースはありません", - "emptyDescription": "不要なものをゴミ箱に移動します。", - "isDeleted": "が削除されました", - "isRestored": "が復元されました" - }, - "confirmDeleteTitle": "このページを完全に削除してもよろしいですか?" + "empty": "ゴミ箱を空にする", + "emptyDescription": "削除されたファイルはありません", + "isDeleted": "削除済み" + } }, "deletePagePrompt": { "text": "このページはごみ箱にあります", "restore": "ページを元に戻す", - "deletePermanent": "削除する", - "deletePermanentDescription": "このページを完全に削除してもよろしいですか? 削除すると元に戻せません。" + "deletePermanent": "削除する" }, "dialogCreatePageNameHint": "ページ名", "questionBubble": { "shortcuts": "ショートカット", - "whatsNew": "新着情報", - "markdown": "Markdown", + "whatsNew": "What's new?", + "help": "ヘルプとサポート", + "markdown": "マークダウン", "debug": { "name": "デバッグ情報", "success": "デバッグ情報をクリップボードにコピーしました!", "fail": "デバッグ情報をクリップボードにコピーできませんでした" }, - "feedback": "フィードバック", - "help": "ヘルプ & サポート" + "feedback": "フィードバック" }, "menuAppHeader": { - "moreButtonToolTip": "削除、名前の変更、その他...", - "addPageTooltip": "ページを追加", - "defaultNewPageName": "無題", - "renameDialog": "名前を変更", - "pageNameSuffix": "コピー" + "addPageTooltip": "内部ページを追加", + "defaultNewPageName": "Untitled", + "renameDialog": "名前を変更" }, - "noPagesInside": "中にページがありません", "toolbar": { "undo": "元に戻す", "redo": "やり直し", @@ -288,13 +146,13 @@ "underline": "下線", "strike": "取り消し線", "numList": "番号付きリスト", - "bulletList": "箇条書きリスト", - "checkList": "チェックリスト", + "bulletList": "箇条書き", + "checkList": "チェックボックス", "inlineCode": "インラインコード", - "quote": "引用ブロック", - "header": "ヘッダー", - "highlight": "ハイライト", - "color": "カラー", + "quote": "引用文", + "header": "見出し", + "highlight": "文字の背景色", + "color": "色", "addLink": "リンクを追加", "link": "リンク" }, @@ -303,79 +161,35 @@ "darkMode": "ダークモードに切り替える", "openAsPage": "ページとして開く", "addNewRow": "新しい行を追加", - "openMenu": "クリックしてメニューを開く", - "dragRow": "長押しして行を並べ替える", - "viewDataBase": "データベースを表示", + "openMenu": "クリックしてメニューを開きます", + "dragRow": "長押しして行を並べ替えます", + "viewDataBase": "データベースを見る", "referencePage": "この {name} は参照されています", - "addBlockBelow": "下にブロックを追加", - "aiGenerate": "生成する" + "addBlockBelow": "以下にブロックを追加します" }, "sideBar": { - "closeSidebar": "サイドバーを閉じる", - "openSidebar": "サイドバーを開く", - "expandSidebar": "全ページ展開", - "personal": "個人", - "private": "プライベート", - "workspace": "ワークスペース", + "closeSidebar": "Close sidebar", + "openSidebar": "Open sidebar", "favorites": "お気に入り", - "clickToHidePrivate": "クリックしてプライベートスペースを非表示にする\nここで作成したページはあなただけが見えます", - "clickToHideWorkspace": "クリックしてワークスペースを非表示にする\nここで作成したページはすべてのメンバーが閲覧できます", - "clickToHidePersonal": "クリックして個人スペースを非表示にする", - "clickToHideFavorites": "クリックしてお気に入りスペースを非表示にする", - "addAPage": "新しいページを追加", - "addAPageToPrivate": "プライベートスペースにページを追加", - "addAPageToWorkspace": "ワークスペースにページを追加", - "recent": "最近", - "today": "今日", - "thisWeek": "今週", - "others": "以前のお気に入り", - "earlier": "以前", - "justNow": "たった今", - "minutesAgo": "{count}分前", - "lastViewed": "最後に表示したページ", - "favoriteAt": "お気に入り登録", - "emptyRecent": "最近表示したページはありません", - "emptyRecentDescription": "ページを閲覧すると、ここに簡単にアクセスできるよう表示されます。", - "emptyFavorite": "お気に入りのページはありません", - "emptyFavoriteDescription": "ページをお気に入りに追加すると、ここに表示されて素早くアクセスできます!", - "removePageFromRecent": "最近表示したページから削除しますか?", - "removeSuccess": "削除に成功しました", - "favoriteSpace": "お気に入り", - "RecentSpace": "最近", - "Spaces": "スペース", - "upgradeToPro": "Proプランにアップグレード", - "upgradeToAIMax": "無制限のAIを解放", - "storageLimitDialogTitle": "無料のストレージが不足しています。無制限のストレージを解放するにはアップグレードしてください", - "aiResponseLimitTitle": "無料のAIレスポンスが不足しています。Proプランにアップグレードするか、AIアドオンを購入して無制限のレスポンスを解放してください", - "aiResponseLimitDialogTitle": "AIレスポンスの制限に達しました", - "aiResponseLimit": "無料のAIレスポンスが不足しています。\n\n設定 -> プラン -> AI MaxまたはProプランをクリックして、さらにAIレスポンスを取得してください", - "askOwnerToUpgradeToPro": "ワークスペースの無料ストレージが不足しています。ワークスペースのオーナーにProプランへのアップグレードを依頼してください", - "askOwnerToUpgradeToProIOS": "ワークスペースの無料ストレージが不足しています。", - "askOwnerToUpgradeToAIMax": "ワークスペースのAIレスポンスが不足しています。ワークスペースのオーナーにプランのアップグレードかAIアドオンの購入を依頼してください", - "askOwnerToUpgradeToAIMaxIOS": "ワークスペースの無料AIレスポンスが不足しています。", - "purchaseStorageSpace": "ストレージスペースを購入", - "singleFileProPlanLimitationDescription": "無料プランで許可されている最大ファイルアップロードサイズを超えました。より大きなファイルをアップロードするには、プロプランにアップグレードしてください。", - "purchaseAIResponse": "AIレスポンスを購入", - "askOwnerToUpgradeToLocalAI": "ワークスペースのオーナーにオンデバイスAIを有効にするよう依頼してください", - "upgradeToAILocal": "デバイス上でローカルモデルを実行して究極のプライバシーを実現", - "upgradeToAILocalDesc": "PDFとチャットしたり、文章を改善したり、ローカルAIでテーブルを自動入力したりできます" + "clickToHideFavorites": "クリックしてお気に入りを隠す", + "addAPage": "ページを追加", + "recent": "最近" }, "notifications": { "export": { - "markdown": "ノートをMarkdownにエクスポート", + "markdown": "マークダウン形式のノート", "path": "Documents/flowy" } }, "contactsPage": { "title": "連絡先", - "whatsHappening": "今週は何が起こっている?", - "addContact": "連絡先を追加", - "editContact": "連絡先を編集" + "whatsHappening": "今週はどんなことがありましたか?", + "addContact": "連絡先を追加する", + "editContact": "連絡先を編集する" }, "button": { "ok": "OK", - "confirm": "確認", - "done": "完了", + "done": "終わり", "cancel": "キャンセル", "signIn": "サインイン", "signOut": "サインアウト", @@ -383,64 +197,31 @@ "save": "保存", "generate": "生成", "esc": "ESC", - "keep": "保持", - "tryAgain": "再試行", + "keep": "保つ", + "tryAgain": "再試行する", "discard": "破棄", - "replace": "置き換え", + "replace": "交換", "insertBelow": "下に挿入", "insertAbove": "上に挿入", "upload": "アップロード", "edit": "編集", - "delete": "削除", - "copy": "コピー", + "delete": "消去", "duplicate": "複製", - "putback": "元に戻す", + "putback": "戻す", "update": "更新", "share": "共有", "removeFromFavorites": "お気に入りから削除", - "removeFromRecent": "最近の項目から削除", - "addToFavorites": "お気に入りに追加", - "favoriteSuccessfully": "お気に入りに追加しました", - "unfavoriteSuccessfully": "お気に入りから削除しました", - "duplicateSuccessfully": "複製が成功しました", + "addToFavorites": "お気に入りへ追加", "rename": "名前を変更", "helpCenter": "ヘルプセンター", - "add": "追加", - "yes": "はい", - "no": "いいえ", - "clear": "クリア", - "remove": "削除", - "dontRemove": "削除しない", - "copyLink": "リンクをコピー", - "align": "整列", - "login": "ログイン", - "logout": "ログアウト", - "deleteAccount": "アカウントを削除", - "back": "戻る", - "signInGoogle": "Googleで続行", - "signInGithub": "GitHubで続行", - "signInDiscord": "Discordで続行", - "more": "もっと見る", - "create": "作成", - "close": "閉じる", - "next": "次へ", - "previous": "前へ", - "submit": "送信", - "download": "ダウンロード", - "backToHome": "Homeに戻る", - "viewing": "閲覧", - "editing": "編集", - "gotIt": "わかった", - "retry": "リトライ", - "uploadFailed": "アップロードに失敗しました。", - "copyLinkOriginal": "元のリンクをコピー" + "add": "追加" }, "label": { "welcome": "ようこそ!", "firstName": "名", "middleName": "ミドルネーム", "lastName": "姓", - "stepX": "ステップ {X}" + "stepX": "Step {X}" }, "oAuth": { "err": { @@ -449,944 +230,182 @@ }, "google": { "title": "Googleでサインイン", - "instruction1": "Googleでのサインインを有効にするためには、Webブラウザーを使用してこのアプリケーションを認証する必要があります。", - "instruction2": "アイコンをクリックするかテキストを選択して、このコードをクリップボードにコピーします。", + "instruction1": "Googleでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。", + "instruction2": "アイコンをクリックするか、以下のテキストを選択して、このコードをクリップボードにコピーします。", "instruction3": "以下のリンク先をブラウザーで開いて、次のコードを入力します。", "instruction4": "登録が完了したら以下のボタンを押してください。" } }, "settings": { "title": "設定", - "popupMenuItem": { - "settings": "設定", - "members": "メンバー", - "trash": "ゴミ箱", - "helpAndSupport": "ヘルプ & サポート" - }, - "sites": { - "title": "サイト", - "namespaceTitle": "名前空間", - "namespaceDescription": "名前空間とホームページを管理する", - "namespaceHeader": "名前空間", - "homepageHeader": "ホームページ", - "updateNamespace": "名前空間を更新する", - "removeHomepage": "ホームページを削除する", - "selectHomePage": "ページを選択", - "clearHomePage": "この名前空間のホームページをクリアする", - "customUrl": "カスタム URL", - "namespace": { - "description": "この変更は、この名前空間で公開されているすべてのページに適用されます。", - "tooltip": "不適切な名前空間を削除する権利を留保します", - "updateExistingNamespace": "既存の名前空間を更新する", - "upgradeToPro": "ホームページを設定するにはプロプランにアップグレードしてください", - "redirectToPayment": "支払いページにリダイレクトしています...", - "onlyWorkspaceOwnerCanSetHomePage": "ワークスペースの所有者のみがホームページを設定できます", - "pleaseAskOwnerToSetHomePage": "ワークスペースのオーナーにプロプランへのアップグレードを依頼してください" - }, - "publishedPage": { - "title": "公開されたすべてのページ", - "description": "公開したページを管理する", - "page": "ページ", - "pathName": "パス名", - "date": "公開日", - "emptyHinText": "このワークスペースには公開されたページはありません", - "noPublishedPages": "公開ページはありません", - "settings": "公開設定", - "clickToOpenPageInApp": "アプリでページを開く", - "clickToOpenPageInBrowser": "ブラウザでページを開く" - }, - "error": { - "failedToGeneratePaymentLink": "プロプランの支払いリンクを生成できませんでした", - "failedToUpdateNamespace": "名前空間の更新に失敗しました", - "proPlanLimitation": "名前空間を更新するには、Proプランにアップグレードする必要があります", - "namespaceAlreadyInUse": "名前空間はすでに使用されています。別の名前空間を試してください", - "invalidNamespace": "名前空間が無効です。別の名前空間を試してください", - "namespaceLengthAtLeast2Characters": "名前空間は少なくとも 2 文字の長さである必要があります", - "onlyWorkspaceOwnerCanUpdateNamespace": "ワークスペースの所有者のみが名前空間を更新できます", - "onlyWorkspaceOwnerCanRemoveHomepage": "ワークスペースの所有者のみがホームページを削除できます", - "setHomepageFailed": "ホームページの設定に失敗しました", - "namespaceTooLong": "名前空間が長すぎます。別の名前空間を試してください", - "namespaceTooShort": "名前空間が短すぎます。別の名前空間を試してください。", - "namespaceIsReserved": "名前空間は予約されています。別の名前空間を試してください", - "updatePathNameFailed": "パス名の更新に失敗しました", - "removeHomePageFailed": "ホームページを削除できませんでした", - "publishNameContainsInvalidCharacters": "パス名に無効な文字が含まれています。別のパス名を試してください。", - "publishNameTooShort": "パス名が短すぎます。別のパス名を試してください。", - "publishNameTooLong": "パス名が長すぎます。別のパス名を試してください。", - "publishNameAlreadyInUse": "パス名はすでに使用されています。別のパス名を試してください", - "namespaceContainsInvalidCharacters": "名前空間に無効な文字が含まれています。別の名前空間を試してください。", - "publishPermissionDenied": "公開設定を管理できるのはワークスペースの所有者またはページ発行者のみです。", - "publishNameCannotBeEmpty": "パス名は空にできません。別のパス名を試してください。" - }, - "success": { - "namespaceUpdated": "名前空間が正常に更新されました", - "setHomepageSuccess": "ホームページの設定に成功しました", - "updatePathNameSuccess": "パス名が正常に更新されました", - "removeHomePageSuccess": "ホームページを削除しました" - } - }, - "accountPage": { - "menuLabel": "マイアカウント", - "title": "マイアカウント", - "general": { - "title": "アカウント名 & プロフィール画像", - "changeProfilePicture": "プロフィール画像を変更" - }, - "email": { - "title": "メールアドレス", - "actions": { - "change": "メールアドレスを変更" - } - }, - "login": { - "title": "アカウントログイン", - "loginLabel": "ログイン", - "logoutLabel": "ログアウト" - } - }, - "workspacePage": { - "menuLabel": "ワークスペース", - "title": "ワークスペース", - "description": "ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付/時間形式、言語をカスタマイズします。", - "workspaceName": { - "title": "ワークスペース名" - }, - "workspaceIcon": { - "title": "ワークスペースアイコン", - "description": "画像をアップロードするか、絵文字を使用してワークスペースにアイコンを設定します。アイコンはサイドバーや通知に表示されます。" - }, - "appearance": { - "title": "外観", - "description": "ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付、時間、言語をカスタマイズします。", - "options": { - "system": "自動", - "light": "ライト", - "dark": "ダーク" - } - }, - "resetCursorColor": { - "title": "ドキュメントのカーソル色をリセット", - "description": "カーソルの色をリセットしてもよろしいですか?" - }, - "resetSelectionColor": { - "title": "ドキュメントの選択色をリセット", - "description": "選択色をリセットしてもよろしいですか?" - }, - "resetWidth": { - "resetSuccess": "ドキュメントの幅を正常にリセットしました" - }, - "theme": { - "title": "テーマ", - "description": "プリセットテーマを選択するか、独自のカスタムテーマをアップロードします。", - "uploadCustomThemeTooltip": "カスタムテーマをアップロード" - }, - "workspaceFont": { - "title": "ワークスペースのフォント", - "noFontHint": "フォントが見つかりません。別のキーワードをお試しください。" - }, - "textDirection": { - "title": "テキストの方向", - "leftToRight": "左から右", - "rightToLeft": "右から左", - "auto": "自動", - "enableRTLItems": "RTLツールバー項目を有効にする" - }, - "layoutDirection": { - "title": "レイアウトの方向", - "leftToRight": "左から右", - "rightToLeft": "右から左" - }, - "dateTime": { - "title": "日付と時間", - "example": "{} at {} ({})", - "24HourTime": "24時間表記", - "dateFormat": { - "label": "日付形式", - "local": "ローカル", - "us": "米国", - "iso": "ISO", - "friendly": "読み易さ", - "dmy": "日/月/年" - } - }, - "language": { - "title": "言語" - }, - "deleteWorkspacePrompt": { - "title": "ワークスペースの削除", - "content": "このワークスペースを削除してもよろしいですか?この操作は元に戻せません。公開しているページはすべて非公開になります。" - }, - "leaveWorkspacePrompt": { - "title": "ワークスペースを退出", - "content": "このワークスペースを退出してもよろしいですか?ワークスペース内のすべてのページやデータへのアクセスを失います。" - }, - "manageWorkspace": { - "title": "ワークスペースを管理", - "leaveWorkspace": "ワークスペースを退出", - "deleteWorkspace": "ワークスペースを削除" - } - }, - "manageDataPage": { - "menuLabel": "データ管理", - "title": "データ管理", - "description": "ローカルストレージデータの管理または既存のデータを@:appNameにインポートします。", - "dataStorage": { - "title": "ファイル保存場所", - "tooltip": "ファイルが保存されている場所", - "actions": { - "change": "パスを変更", - "open": "フォルダを開く", - "openTooltip": "現在のデータフォルダの場所を開く", - "copy": "パスをコピー", - "copiedHint": "パスをコピーしました!", - "resetTooltip": "デフォルトの場所にリセット" - }, - "resetDialog": { - "title": "本当にリセットしますか?", - "description": "パスをデフォルトのデータ保存場所にリセットしてもデータは削除されません。現在のデータを再インポートする場合、最初に現在の場所のパスをコピーしてください。" - } - }, - "importData": { - "title": "データのインポート", - "tooltip": "@:appNameのバックアップ/データフォルダからデータをインポート", - "description": "外部の@:appNameデータフォルダからデータをコピー", - "action": "ファイルを参照" - }, - "encryption": { - "title": "暗号化", - "tooltip": "データの保存と暗号化方法を管理", - "descriptionNoEncryption": "暗号化をオンにするとすべてのデータが暗号化されます。この操作は元に戻せません。", - "descriptionEncrypted": "データは暗号化されています。", - "action": "データを暗号化", - "dialog": { - "title": "すべてのデータを暗号化しますか?", - "description": "すべてのデータを暗号化すると、データは安全かつ保護されます。この操作は元に戻せません。本当に続行しますか?" - } - }, - "cache": { - "title": "キャッシュをクリア", - "description": "画像が読み込まれない、スペース内のページが見つからない、フォントが読み込まれないなどの問題を解決します。データには影響しません。", - "dialog": { - "title": "キャッシュをクリア", - "description": "画像が読み込まれない、スペース内のページが見つからない、フォントが読み込まれないなどの問題を解決します。データには影響しません。", - "successHint": "キャッシュをクリアしました!" - } - }, - "data": { - "fixYourData": "データを修復", - "fixButton": "修復", - "fixYourDataDescription": "データに問題がある場合、ここで修復を試みることができます。" - } - }, - "shortcutsPage": { - "menuLabel": "ショートカット", - "title": "ショートカット", - "editBindingHint": "新しいバインディングを入力", - "searchHint": "検索", - "actions": { - "resetDefault": "デフォルトにリセット" - }, - "errorPage": { - "message": "ショートカットの読み込みに失敗しました: {}", - "howToFix": "もう一度お試しください。問題が続く場合はGitHubでお問い合わせください。" - }, - "resetDialog": { - "title": "ショートカットをリセット", - "description": "これにより、すべてのキー設定がデフォルトにリセットされます。この操作は元に戻せません。続行してもよろしいですか?", - "buttonLabel": "リセット" - }, - "conflictDialog": { - "title": "{}は現在使用されています", - "descriptionPrefix": "このキー設定は現在 ", - "descriptionSuffix": " によって使用されています。このキー設定を置き換えると、{} から削除されます。", - "confirmLabel": "続行" - }, - "editTooltip": "キー設定の編集を開始するには押してください", - "keybindings": { - "toggleToDoList": "To-Doリストを切り替え", - "insertNewParagraphInCodeblock": "新しい段落を挿入", - "pasteInCodeblock": "コードブロックに貼り付け", - "selectAllCodeblock": "すべて選択", - "indentLineCodeblock": "行の先頭にスペースを2つ挿入", - "outdentLineCodeblock": "行の先頭のスペースを2つ削除", - "twoSpacesCursorCodeblock": "カーソル位置にスペースを2つ挿入", - "copy": "選択をコピー", - "paste": "コンテンツに貼り付け", - "cut": "選択をカット", - "alignLeft": "テキストを左揃え", - "alignCenter": "テキストを中央揃え", - "alignRight": "テキストを右揃え", - "undo": "元に戻す", - "redo": "やり直し", - "convertToParagraph": "ブロックを段落に変換", - "backspace": "削除", - "deleteLeftWord": "左の単語を削除", - "deleteLeftSentence": "左の文を削除", - "delete": "右の文字を削除", - "deleteMacOS": "左の文字を削除", - "deleteRightWord": "右の単語を削除", - "moveCursorLeft": "カーソルを左に移動", - "moveCursorBeginning": "カーソルを先頭に移動", - "moveCursorLeftWord": "カーソルを左に1単語移動", - "moveCursorLeftSelect": "選択してカーソルを左に移動", - "moveCursorBeginSelect": "選択してカーソルを先頭に移動", - "moveCursorLeftWordSelect": "選択してカーソルを左に1単語移動", - "moveCursorRight": "カーソルを右に移動", - "moveCursorEnd": "カーソルを末尾に移動", - "moveCursorRightWord": "カーソルを右に1単語移動", - "moveCursorRightSelect": "選択してカーソルを右に1つ移動", - "moveCursorEndSelect": "選択してカーソルを末尾に移動", - "moveCursorRightWordSelect": "選択してカーソルを右に1単語移動", - "moveCursorUp": "カーソルを上に移動", - "moveCursorTopSelect": "選択してカーソルを最上部に移動", - "moveCursorTop": "カーソルを最上部に移動", - "moveCursorUpSelect": "選択してカーソルを上に移動", - "moveCursorBottomSelect": "選択してカーソルを最下部に移動", - "moveCursorBottom": "カーソルを最下部に移動", - "moveCursorDown": "カーソルを下に移動", - "moveCursorDownSelect": "選択してカーソルを下に移動", - "home": "最上部にスクロール", - "end": "最下部にスクロール", - "toggleBold": "太字を切り替え", - "toggleItalic": "斜体を切り替え", - "toggleUnderline": "下線を切り替え", - "toggleStrikethrough": "取り消し線を切り替え", - "toggleCode": "インラインコードを切り替え", - "toggleHighlight": "ハイライトを切り替え", - "showLinkMenu": "リンクメニューを表示", - "openInlineLink": "インラインリンクを開く", - "openLinks": "選択されたすべてのリンクを開く", - "indent": "インデント", - "outdent": "アウトデント", - "exit": "編集を終了", - "pageUp": "1ページ上にスクロール", - "pageDown": "1ページ下にスクロール", - "selectAll": "すべて選択", - "pasteWithoutFormatting": "フォーマットなしで貼り付け", - "showEmojiPicker": "絵文字ピッカーを表示", - "enterInTableCell": "表に改行を追加", - "leftInTableCell": "表の左隣のセルに移動", - "rightInTableCell": "表の右隣のセルに移動", - "upInTableCell": "表の上隣のセルに移動", - "downInTableCell": "表の下隣のセルに移動", - "tabInTableCell": "表の次の利用可能なセルに移動", - "shiftTabInTableCell": "表の前の利用可能なセルに移動", - "backSpaceInTableCell": "セルの先頭で停止" - }, - "commands": { - "codeBlockNewParagraph": "コードブロックの隣に新しい段落を挿入", - "codeBlockIndentLines": "コードブロック内の行の先頭にスペースを2つ挿入", - "codeBlockOutdentLines": "コードブロック内の行の先頭のスペースを2つ削除", - "codeBlockAddTwoSpaces": "コードブロック内のカーソル位置にスペースを2つ挿入", - "codeBlockSelectAll": "コードブロック内のすべてのコンテンツを選択", - "codeBlockPasteText": "コードブロック内にテキストを貼り付け", - "textAlignLeft": "テキストを左揃え", - "textAlignCenter": "テキストを中央揃え", - "textAlignRight": "テキストを右揃え" - }, - "couldNotLoadErrorMsg": "ショートカットを読み込めませんでした。もう一度お試しください。", - "couldNotSaveErrorMsg": "ショートカットを保存できませんでした。もう一度お試しください。" - }, - "aiPage": { - "title": "AI設定", - "menuLabel": "AI設定", - "keys": { - "enableAISearchTitle": "AI検索", - "aiSettingsDescription": "AppFlowy AIを駆動するモデルを選択します。GPT 4-o、Claude 3.5、Llama 3.1、Mistral 7Bが含まれます。", - "loginToEnableAIFeature": "AI機能は@:appName Cloudにログインした後に有効になります。@:appNameアカウントがない場合は、「マイアカウント」でサインアップしてください。", - "llmModel": "言語モデル", - "llmModelType": "言語モデルのタイプ", - "downloadLLMPrompt": "{}をダウンロード", - "downloadAppFlowyOfflineAI": "AIオフラインパッケージをダウンロードすると、デバイス上でAIが動作します。続行しますか?", - "downloadLLMPromptDetail": "{}のローカルモデルをダウンロードすると、{}のストレージを使用します。続行しますか?", - "downloadBigFilePrompt": "ダウンロードが完了するまで約10分かかることがあります", - "downloadAIModelButton": "ダウンロード", - "downloadingModel": "ダウンロード中", - "localAILoaded": "ローカルAIモデルが正常に追加され、使用可能です", - "localAIStart": "ローカルAIチャットが開始しています...", - "localAILoading": "ローカルAIチャットモデルを読み込み中...", - "localAIStopped": "ローカルAIが停止しました", - "failToLoadLocalAI": "ローカルAIの起動に失敗しました", - "restartLocalAI": "ローカルAIを再起動", - "disableLocalAITitle": "ローカルAIを無効化", - "disableLocalAIDescription": "ローカルAIを無効にしますか?", - "localAIToggleTitle": "ローカルAIを有効化または無効化", - "offlineAIInstruction1": "次の手順に従って", - "offlineAIInstruction2": "指示", - "offlineAIInstruction3": "でオフラインAIを有効にします。", - "offlineAIDownload1": "AppFlowy AIをまだダウンロードしていない場合は、", - "offlineAIDownload2": "ダウンロード", - "offlineAIDownload3": "してください", - "activeOfflineAI": "アクティブ", - "downloadOfflineAI": "ダウンロード", - "openModelDirectory": "フォルダを開く" - } - }, - "planPage": { - "menuLabel": "プラン", - "title": "料金プラン", - "planUsage": { - "title": "プラン使用状況の概要", - "storageLabel": "ストレージ", - "storageUsage": "{} / {} GB", - "unlimitedStorageLabel": "無制限のストレージ", - "collaboratorsLabel": "メンバー", - "collaboratorsUsage": "{} / {}", - "aiResponseLabel": "AIレスポンス", - "aiResponseUsage": "{} / {}", - "unlimitedAILabel": "無制限のAIレスポンス", - "proBadge": "プロ", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "Mac用AIオンデバイス", - "memberProToggle": "メンバーを追加 & 無制限のAI", - "aiMaxToggle": "無制限のAI & 高度なモデルにアクセス", - "aiOnDeviceToggle": "究極のプライバシーのためのローカルAI", - "aiCredit": { - "title": "@:appName AIクレジットを追加", - "price": "{}", - "priceDescription": "1,000クレジットの価格", - "purchase": "AIを購入", - "info": "ワークスペースごとに1,000 AIクレジットを追加し、より賢く迅速な結果を得るためにAIをワークフローにシームレスに統合します。最大で:", - "infoItemOne": "データベースごとに10,000レスポンス", - "infoItemTwo": "ワークスペースごとに1,000レスポンス" - }, - "currentPlan": { - "bannerLabel": "現在のプラン", - "freeTitle": "無料", - "proTitle": "プロ", - "teamTitle": "チーム", - "freeInfo": "個人や2名までのメンバーに最適", - "proInfo": "小規模・中規模チームに最適(最大10名)", - "teamInfo": "生産的かつ整理されたチームに最適", - "upgrade": "プランを変更", - "canceledInfo": "プランはキャンセルされました。{}に無料プランにダウングレードされます。" - }, - "addons": { - "title": "アドオン", - "addLabel": "追加", - "activeLabel": "追加済み", - "aiMax": { - "title": "AI Max", - "description": "GPT-4o、Claude 3.5 Sonnetなどによる無制限のAIレスポンス", - "price": "{}", - "priceInfo": "1ユーザーあたり月額、年間請求" - }, - "aiOnDevice": { - "title": "Mac用AIオンデバイス", - "description": "Mistral 7B、LLAMA 3、その他のローカルモデルをマシンで実行", - "price": "{}", - "priceInfo": "1ユーザーあたり月額、年間請求", - "recommend": "M1以上を推奨" - } - }, - "deal": { - "bannerLabel": "新年キャンペーン!", - "title": "チームを成長させましょう!", - "info": "Proプランとチームプランを10%オフでアップグレード!@:appName AIを含む強力な新機能でワークスペースの生産性を向上させましょう。", - "viewPlans": "プランを表示" - } - } - }, - "billingPage": { - "menuLabel": "請求", - "title": "請求", - "plan": { - "title": "プラン", - "freeLabel": "無料", - "proLabel": "プロ", - "planButtonLabel": "プランを変更", - "billingPeriod": "請求期間", - "periodButtonLabel": "期間を編集" - }, - "paymentDetails": { - "title": "支払い詳細", - "methodLabel": "支払い方法", - "methodButtonLabel": "方法を編集" - }, - "addons": { - "title": "アドオン", - "addLabel": "追加", - "removeLabel": "削除", - "renewLabel": "更新", - "aiMax": { - "label": "AI Max", - "description": "無制限のAIと高度なモデルを解放", - "activeDescription": "次の請求日は{}", - "canceledDescription": "AI Maxは{}まで利用可能です" - }, - "aiOnDevice": { - "label": "Mac用AIオンデバイス", - "description": "デバイス上で無制限のAIを解放", - "activeDescription": "次の請求日は{}", - "canceledDescription": "Mac用AIオンデバイスは{}まで利用可能です" - }, - "removeDialog": { - "title": "{}を削除", - "description": "{plan}を削除してもよろしいですか?{plan}の機能と特典へのアクセスをすぐに失います。" - } - }, - "currentPeriodBadge": "現在の期間", - "changePeriod": "期間を変更", - "planPeriod": "{}期間", - "monthlyInterval": "毎月", - "monthlyPriceInfo": "毎月請求される座席あたりの料金", - "annualInterval": "年次", - "annualPriceInfo": "年間請求される座席あたりの料金" - }, - "comparePlanDialog": { - "title": "プランの比較と選択", - "planFeatures": "プラン\n機能", - "current": "現在のプラン", - "actions": { - "upgrade": "アップグレード", - "downgrade": "ダウングレード", - "current": "現在のプラン" - }, - "freePlan": { - "title": "無料", - "description": "個人や2名までのメンバーに最適", - "price": "{}", - "priceInfo": "永久に無料" - }, - "proPlan": { - "title": "プロ", - "description": "小規模チームのプロジェクト管理とチーム知識の管理に最適", - "price": "{}", - "priceInfo": "1ユーザーあたり月額 \n年間請求\n\n{} 毎月請求" - }, - "planLabels": { - "itemOne": "ワークスペース", - "itemTwo": "メンバー", - "itemThree": "ストレージ", - "itemFour": "リアルタイムコラボレーション", - "itemFive": "モバイルアプリ", - "itemSix": "AIレスポンス", - "itemFileUpload": "ファイルアップロード", - "customNamespace": "カスタム名前空間", - "tooltipSix": "ライフタイムはレスポンスの数がリセットされないことを意味します", - "intelligentSearch": "インテリジェント検索", - "tooltipSeven": "ワークスペースのURLの一部をカスタマイズできます", - "customNamespaceTooltip": "カスタム公開サイト URL" - }, - "freeLabels": { - "itemOne": "ワークスペースごとの料金", - "itemTwo": "最大2名", - "itemThree": "5 GB", - "itemFour": "はい", - "itemFive": "はい", - "itemSix": "10回のライフタイムレスポンス", - "itemFileUpload": "最大7 MB", - "intelligentSearch": "インテリジェント検索" - }, - "proLabels": { - "itemOne": "ワークスペースごとの料金", - "itemTwo": "最大10名", - "itemThree": "無制限", - "itemFour": "はい", - "itemFive": "はい", - "itemSix": "無制限", - "itemFileUpload": "無制限", - "intelligentSearch": "インテリジェント検索" - }, - "paymentSuccess": { - "title": "あなたは現在{}プランに加入しています!", - "description": "お支払いが正常に処理され、プランが@:appName {}にアップグレードされました。プランの詳細はプランページで確認できます。" - }, - "downgradeDialog": { - "title": "本当にプランをダウングレードしますか?", - "description": "プランをダウングレードすると、無料プランに戻ります。メンバーがこのワークスペースにアクセスできなくなり、無料プランのストレージ制限を満たすためにスペースを空ける必要がある場合があります。", - "downgradeLabel": "プランをダウングレード" - } - }, - "cancelSurveyDialog": { - "title": "ご利用いただきありがとうございました", - "description": "ご解約されるのは残念です。@:appNameを改善するために、お客様のフィードバックをお聞かせください。いくつかの質問にお答えいただけますと幸いです。", - "commonOther": "その他", - "otherHint": "ここに回答を入力してください", - "questionOne": { - "question": "@:appName Proサブスクリプションをキャンセルした理由は何ですか?", - "answerOne": "料金が高すぎる", - "answerTwo": "機能が期待に応えなかった", - "answerThree": "より良い代替案を見つけた", - "answerFour": "コストに見合うほど使用しなかった", - "answerFive": "サービスや技術的な問題があった" - }, - "questionTwo": { - "question": "将来的に@:appName Proに再加入する可能性はどのくらいですか?", - "answerOne": "非常に高い", - "answerTwo": "やや高い", - "answerThree": "わからない", - "answerFour": "低い", - "answerFive": "非常に低い" - }, - "questionThree": { - "question": "サブスクリプション中に最も価値を感じたPro機能は何ですか?", - "answerOne": "複数ユーザーのコラボレーション", - "answerTwo": "長期間のバージョン履歴", - "answerThree": "無制限のAIレスポンス", - "answerFour": "ローカルAIモデルへのアクセス" - }, - "questionFour": { - "question": "全体的な@:appNameの体験はどのようなものでしたか?", - "answerOne": "素晴らしい", - "answerTwo": "良い", - "answerThree": "普通", - "answerFour": "平均以下", - "answerFive": "満足していない" - } - }, - "common": { - "uploadingFile": "ファイルをアップロード中です。アプリを終了しないでください", - "uploadNotionSuccess": "Notion zipファイルが正常にアップロードされました。インポートが完了すると、確認メールが届きます。", - "reset": "リセット" - }, "menu": { "appearance": "外観", "language": "言語", "user": "ユーザー", "files": "ファイル", "notifications": "通知", - "open": "設定を開く", + "open": "設定", "logout": "ログアウト", "logoutPrompt": "本当にログアウトしますか?", - "selfEncryptionLogoutPrompt": "ログアウトしますか?暗号化キーをコピーしていることを確認してください。", - "syncSetting": "同期設定", - "cloudSettings": "クラウド設定", - "enableSync": "同期を有効化", - "enableSyncLog": "同期ログを有効にする", - "enableSyncLogWarning": "同期の問題の診断にご協力いただきありがとうございます。これにより、ドキュメントの編集内容がローカルファイルに記録されます。有効にした後、アプリを終了して再度開いてください。", - "enableEncrypt": "データを暗号化", - "cloudURL": "基本URL", - "webURL": "ウェブURL", - "invalidCloudURLScheme": "無効なスキーム", - "cloudServerType": "クラウドサーバー", - "cloudServerTypeTip": "クラウドサーバーを変更すると現在のアカウントがログアウトされる可能性があります。", - "cloudLocal": "ローカル", - "cloudAppFlowy": "@:appName Cloud", - "cloudAppFlowySelfHost": "@:appName クラウドセルフホスト", - "appFlowyCloudUrlCanNotBeEmpty": "クラウドのURLを空にすることはできません", + "syncSetting": "設定を同期", + "enableSync": "同期を有効可", + "enableEncrypt": "暗号化されたデータ", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Supabase URLは空白にはできません", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Supabase anon keyは空白にはできません", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud セルフホスト", + "appFlowyCloudUrlCanNotBeEmpty": "クラウドURLは空白にはできません", "clickToCopy": "クリックしてコピー", - "selfHostStart": "サーバーをお持ちでない場合は、", + "selfHostStart": "サーバーが準備できていない場合、", "selfHostContent": "ドキュメント", - "selfHostEnd": "を参照してセルフホストサーバーの設定方法をご確認ください", - "pleaseInputValidURL": "有効なURLを入力してください", - "changeUrl": "セルフホスト URL を {} に変更します", - "cloudURLHint": "サーバーの基本URLを入力してください", - "webURLHint": "ウェブサーバーのベースURLを入力してください", + "selfHostEnd": "を参照してセルフホストサーバーのセットアップ手順を確認してください", + "cloudURLHint": "サーバーのURLを入力", "cloudWSURL": "Websocket URL", - "cloudWSURLHint": "サーバーのWebsocketアドレスを入力してください", + "cloudWSURLHint": "Websocketサーバーのアドレスを入力", "restartApp": "再起動", - "restartAppTip": "変更を反映するにはアプリケーションを再起動してください。再起動すると現在のアカウントがログアウトされる可能性があります。", - "changeServerTip": "サーバーを変更後、変更を反映するには再起動ボタンをクリックしてください。", - "enableEncryptPrompt": "この秘密キーでデータを暗号化します。安全に保管してください。有効にすると解除できません。キーを紛失するとデータが回復不可能になります。コピーするにはクリックしてください。", - "inputEncryptPrompt": "暗号化キーを入力してください", - "clickToCopySecret": "クリックして秘密キーをコピー", - "configServerSetting": "サーバー設定を構成", - "configServerGuide": "「クイックスタート」を選択後、「設定」→「クラウド設定」に進み、セルフホストサーバーを構成します。", - "inputTextFieldHint": "秘密キー", - "historicalUserList": "ユーザーログイン履歴", - "historicalUserListTooltip": "このリストには匿名アカウントが表示されます。アカウントをクリックすると詳細が表示されます。匿名アカウントは「始める」ボタンをクリックすることで作成されます。", - "openHistoricalUser": "匿名アカウントを開くにはクリック", - "customPathPrompt": "Google Driveなどのクラウド同期フォルダに@:appNameデータフォルダを保存することはリスクを伴います。このフォルダ内のデータベースが複数の場所から同時にアクセスまたは変更されると、同期の競合やデータ破損が発生する可能性があります。", - "importAppFlowyData": "外部@:appNameフォルダからデータをインポート", - "importingAppFlowyDataTip": "データインポート中です。アプリを閉じないでください。", - "importAppFlowyDataDescription": "外部@:appNameデータフォルダからデータをコピーし、現在のAppFlowyデータフォルダにインポートします。", - "importSuccess": "@:appNameデータフォルダのインポートに成功しました", - "importFailed": "@:appNameデータフォルダのインポートに失敗しました", - "importGuide": "詳細については、参照されたドキュメントをご確認ください。" - }, - "notifications": { - "enableNotifications": { - "label": "通知を有効化", - "hint": "ローカル通知の表示を停止するにはオフにします。" - }, - "showNotificationsIcon": { - "label": "通知アイコンを表示", - "hint": "サイドバーに通知アイコンを表示するにはオンにします。" - }, - "archiveNotifications": { - "allSuccess": "すべての通知を正常にアーカイブしました", - "success": "通知を正常にアーカイブしました" - }, - "markAsReadNotifications": { - "allSuccess": "すべて既読にしました", - "success": "正常に既読にしました" - }, - "action": { - "markAsRead": "既読にする", - "multipleChoice": "複数選択", - "archive": "アーカイブ" - }, - "settings": { - "settings": "設定", - "markAllAsRead": "すべて既読にする", - "archiveAll": "すべてアーカイブ" - }, - "emptyInbox": { - "title": "インボックスゼロ!", - "description": "リマインダーを設定してここで通知を受け取ります。" - }, - "emptyUnread": { - "title": "未読の通知はありません", - "description": "すべてが完了しました!" - }, - "emptyArchived": { - "title": "アーカイブなし", - "description": "アーカイブされた通知がここに表示されます。" - }, - "tabs": { - "inbox": "受信箱", - "unread": "未読", - "archived": "アーカイブ" - }, - "refreshSuccess": "通知が正常に更新されました", - "titles": { - "notifications": "通知", - "reminder": "リマインダー" - } + "clickToCopySecret": "クリックしてシークレットをコピー", + "historicalUserList": "ログイン履歴", + "supabaseSetting": "Supabaseの設定" }, "appearance": { - "resetSetting": "リセット", "fontFamily": { "label": "フォントファミリー", - "search": "検索", - "defaultFont": "システム" + "search": "検索" }, "themeMode": { - "label": "テーマモード", + "label": "外観テーマ", "light": "ライトモード", "dark": "ダークモード", - "system": "システムに適応" - }, - "fontScaleFactor": "フォントの拡大率", - "documentSettings": { - "cursorColor": "ドキュメントのカーソル色", - "selectionColor": "ドキュメントの選択色", - "width": "文書の幅", - "changeWidth": "変更", - "pickColor": "色を選択", - "colorShade": "色の濃淡", - "opacity": "不透明度", - "hexEmptyError": "Hexカラーコードは空にできません", - "hexLengthError": "Hex値は6桁である必要があります", - "hexInvalidError": "無効なHex値です", - "opacityEmptyError": "不透明度は空にできません", - "opacityRangeError": "不透明度は1から100の間である必要があります", - "app": "アプリ", - "flowy": "Flowy", - "apply": "適用" - }, - "layoutDirection": { - "label": "レイアウト方向", - "hint": "画面上のコンテンツの流れを、左から右、または右から左に制御します。", - "ltr": "LTR", - "rtl": "RTL" - }, - "textDirection": { - "label": "デフォルトのテキスト方向", - "hint": "テキストがデフォルトで左から始まるか右から始まるかを指定します。", - "ltr": "LTR", - "rtl": "RTL", - "auto": "自動", - "fallback": "レイアウト方向と同じ" + "system": "システムと同期" }, "themeUpload": { "button": "アップロード", "uploadTheme": "テーマをアップロード", - "description": "下のボタンを使用して独自の@:appNameテーマをアップロードします。", - "loading": "テーマの検証とアップロード中です。しばらくお待ちください...", - "uploadSuccess": "テーマが正常にアップロードされました", - "deletionFailure": "テーマの削除に失敗しました。手動で削除をお試しください。", - "filePickerDialogTitle": ".flowy_pluginファイルを選択", - "urlUploadFailure": "URLのオープンに失敗しました: {}" + "description": "下のボタンを使用して、独自の@:appNameテーマをアップロードします。", + "loading": "テーマを検証してアップロードするまでお待ちください...", + "uploadSuccess": "テーマは正常にアップロードされました", + "deletionFailure": "テーマの削除に失敗しました。手動で削除してみてください。", + "filePickerDialogTitle": ".flowy_plugin ファイルを選択します", + "urlUploadFailure": "URLを開けませんでした: {}", + "failure": "アップロードされたテーマの形式が無効でした。" }, "theme": "テーマ", "builtInsLabel": "組み込みテーマ", "pluginsLabel": "プラグイン", "dateFormat": { - "label": "日付形式", - "local": "ローカル", - "us": "US", - "iso": "ISO", - "friendly": "読み易さ", - "dmy": "日/月/年" + "label": "日付フォーマット" }, "timeFormat": { - "label": "時間形式", - "twelveHour": "12時間制", - "twentyFourHour": "24時間制" - }, - "showNamingDialogWhenCreatingPage": "ページ作成時に名前付けダイアログを表示", - "enableRTLToolbarItems": "RTLツールバー項目を有効にする", - "members": { - "title": "メンバー設定", - "inviteMembers": "メンバーを招待", - "inviteHint": "メールで招待", - "sendInvite": "招待を送信", - "copyInviteLink": "招待リンクをコピー", - "label": "メンバー", - "user": "ユーザー", - "role": "役割", - "removeFromWorkspace": "ワークスペースから削除", - "removeFromWorkspaceSuccess": "ワークスペースから正常に削除されました", - "removeFromWorkspaceFailed": "ワークスペースからの削除に失敗しました", - "owner": "オーナー", - "guest": "ゲスト", - "member": "メンバー", - "memberHintText": "メンバーはページを閲覧および編集できます", - "guestHintText": "ゲストはページを閲覧、反応、コメントし、許可があれば一部のページを編集できます。", - "emailInvalidError": "無効なメールアドレスです。確認してもう一度お試しください。", - "emailSent": "メールが送信されました。受信箱を確認してください。", - "members": "メンバー", - "membersCount": { - "zero": "{} 人のメンバー", - "one": "{} 人のメンバー", - "other": "{} 人のメンバー" - }, - "inviteFailedDialogTitle": "招待の送信に失敗しました", - "inviteFailedMemberLimit": "メンバー制限に達しました。メンバーを追加するにはアップグレードしてください。", - "inviteFailedMemberLimitMobile": "ワークスペースのメンバー制限に達しました。", - "memberLimitExceeded": "メンバー制限に達しました。さらにメンバーを招待するには ", - "memberLimitExceededUpgrade": "アップグレード", - "memberLimitExceededPro": "メンバー制限に達しました。さらにメンバーが必要な場合は ", - "memberLimitExceededProContact": "support@appflowy.io にご連絡ください", - "failedToAddMember": "メンバーの追加に失敗しました", - "addMemberSuccess": "メンバーが正常に追加されました", - "removeMember": "メンバーを削除", - "areYouSureToRemoveMember": "本当にこのメンバーを削除しますか?", - "inviteMemberSuccess": "招待が正常に送信されました", - "failedToInviteMember": "メンバーの招待に失敗しました", - "workspaceMembersError": "問題が発生しました", - "workspaceMembersErrorDescription": "現在、メンバーリストを読み込むことができません。後でもう一度お試しください" + "label": "時刻フォーマット", + "twelveHour": "12時間表記", + "twentyFourHour": "24時間表記" } }, "files": { "copy": "コピー", - "defaultLocation": "ファイルとデータ保存場所を読み取る", - "exportData": "データをエクスポート", - "doubleTapToCopy": "パスをコピーするにはダブルタップ", - "restoreLocation": "@:appName のデフォルトパスに復元", - "customizeLocation": "別のフォルダを開く", - "restartApp": "変更を反映するにはアプリを再起動してください。", - "exportDatabase": "データベースをエクスポート", - "selectFiles": "エクスポートするファイルを選択", + "defaultLocation": "ファイルの読み取りとデータの保存場所", + "exportData": "データをエクスポートする", + "doubleTapToCopy": "ダブルタップしてパスをコピーします", + "restoreLocation": "@:appNameのデフォルトパスに戻す", + "customizeLocation": "別のフォルダーを開く", + "restartApp": "変更を有効にするには、アプリを再起動してください。", + "exportDatabase": "データベースのエクスポート", + "selectFiles": "エクスポートする必要があるファイルを選択します", "selectAll": "すべて選択", - "deselectAll": "すべて選択解除", - "createNewFolder": "新しいフォルダを作成", - "createNewFolderDesc": "データを保存する場所を指定してください", - "defineWhereYourDataIsStored": "データが保存される場所を設定", - "open": "開く", - "openFolder": "既存のフォルダを開く", - "openFolderDesc": "既存の@:appName フォルダに読み書き", + "deselectAll": "すべての選択を解除", + "createNewFolder": "新しいフォルダーを作成する", + "createNewFolderDesc": "データの保存場所を教えてください", + "defineWhereYourDataIsStored": "データの保存場所を定義する", + "open": "開ける", + "openFolder": "既存のフォルダーを開く", + "openFolderDesc": "既存の@:appNameフォルダに読み書きします", "folderHintText": "フォルダ名", - "location": "新しいフォルダを作成", - "locationDesc": "@:appName データフォルダの名前を指定", - "browser": "参照", + "location": "新しいフォルダーの作成", + "locationDesc": "@:appNameデータフォルダーの名前を選択します", + "browser": "ブラウズ", "create": "作成", "set": "設定", - "folderPath": "フォルダを保存するパス", + "folderPath": "フォルダーを保存するパス", "locationCannotBeEmpty": "パスを空にすることはできません", - "pathCopiedSnackbar": "ファイル保存パスがクリップボードにコピーされました!", - "changeLocationTooltips": "データディレクトリを変更", - "change": "変更", - "openLocationTooltips": "別のデータディレクトリを開く", + "pathCopiedSnackbar": "ファイルの保存パスがクリップボードにコピーされました。", + "changeLocationTooltips": "データディレクトリを変更する", + "change": "変化", + "openLocationTooltips": "別のデータ ディレクトリを開く", "openCurrentDataFolder": "現在のデータディレクトリを開く", - "recoverLocationTooltips": "@:appName のデフォルトデータディレクトリにリセット", - "exportFileSuccess": "ファイルのエクスポートに成功しました!", - "exportFileFail": "ファイルのエクスポートに失敗しました!", - "export": "エクスポート", - "clearCache": "キャッシュをクリア", - "clearCacheDesc": "画像が読み込まれない、フォントが表示されないなどの問題がある場合は、キャッシュをクリアしてください。この操作はユーザーデータを削除しません。", - "areYouSureToClearCache": "キャッシュをクリアしますか?", - "clearCacheSuccess": "キャッシュが正常にクリアされました!" + "recoverLocationTooltips": "@:appNameのデフォルトのデータディレクトリにリセットします", + "exportFileSuccess": "ファイルのエクスポートに成功しました。", + "exportFileFail": "ファイルのエクスポートに失敗しました!", + "export": "書き出す" }, "user": { "name": "名前", - "email": "メールアドレス", - "tooltipSelectIcon": "アイコンを選択", - "selectAnIcon": "アイコンを選択", - "pleaseInputYourOpenAIKey": "AIキーを入力してください", - "clickToLogout": "現在のユーザーをログアウトするにはクリック" + "selectAnIcon": "アイコンを選択してください", + "pleaseInputYourOpenAIKey": "OpenAI キーを入力してください" + }, + "shortcuts": { + "shortcutsLabel": "ショートカット", + "command": "コマンド", + "keyBinding": "キーバインディング", + "addNewCommand": "新しいコマンドを追加", + "resetToDefault": "キーバインディングをデフォルトに戻す" }, "mobile": { - "personalInfo": "個人情報", "username": "ユーザー名", - "usernameEmptyError": "ユーザー名は空にできません", - "about": "概要", + "usernameEmptyError": "ユーザー名は空白にはできません", "pushNotifications": "プッシュ通知", "support": "サポート", - "joinDiscord": "Discordに参加", "privacyPolicy": "プライバシーポリシー", - "userAgreement": "利用規約", - "termsAndConditions": "利用条件", - "userprofileError": "ユーザープロフィールの読み込みに失敗しました", - "userprofileErrorDescription": "ログアウトして再度ログインして、問題が解決するか確認してください。", - "selectLayout": "レイアウトを選択", - "selectStartingDay": "開始日を選択", + "userAgreement": "ユーザー同意", + "termsAndConditions": "利用規約", "version": "バージョン" } }, "grid": { - "deleteView": "このビューを削除してもよろしいですか?", - "createView": "新規", + "deleteView": "このビューを削除してもよろしいですか?", + "createView": "新しい", "title": { - "placeholder": "無題" + "placeholder": "Untitled" }, "settings": { - "filter": "フィルター", - "sort": "並べ替え", - "sortBy": "並べ替え基準", + "filter": "絞り込み", + "sort": "選別", + "sortBy": "並び替え", "properties": "プロパティ", - "reorderPropertiesTooltip": "プロパティをドラッグして並べ替え", + "reorderPropertiesTooltip": "ドラッグしてプロパティを並べ替えます", "group": "グループ", - "addFilter": "フィルターを追加", - "deleteFilter": "フィルターを削除", + "addFilter": "フィルターの追加", + "deleteFilter": "フィルタの削除", "filterBy": "フィルター条件...", - "typeAValue": "値を入力...", + "typeAValue": "値を入力してください...", "layout": "レイアウト", "databaseLayout": "レイアウト", - "viewList": { - "zero": "0 ビュー", - "one": "{count} ビュー", - "other": "{count} ビュー" - }, - "editView": "ビューを編集", - "boardSettings": "ボード設定", - "calendarSettings": "カレンダー設定", - "createView": "新しいビュー", - "duplicateView": "ビューを複製", - "deleteView": "ビューを削除", - "numberOfVisibleFields": "{} 表示中" - }, - "filter": { - "empty": "アクティブなフィルターはありません", - "addFilter": "フィルターを追加", - "cannotFindCreatableField": "フィルタリングに適したフィールドが見つかりません", - "conditon": "状態", - "where": "どこ" + "Properties": "プロパティ" }, "textFilter": { - "contains": "含む", - "doesNotContain": "含まない", + "contains": "含まれています", + "doesNotContain": "含まれていない", "endsWith": "で終わる", "startWith": "で始まる", - "is": "である", - "isNot": "でない", - "isEmpty": "空", - "isNotEmpty": "空でない", + "is": "は", + "isNot": "ではありません", + "isEmpty": "空である", + "isNotEmpty": "空ではない", "choicechipPrefix": { - "isNot": "ではない", + "isNot": "いいえ", "startWith": "で始まる", "endWith": "で終わる", - "isEmpty": "空", - "isNotEmpty": "空でない" + "isEmpty": "空である", + "isNotEmpty": "空ではない" } }, "checkboxFilter": { "isChecked": "チェック済み", "isUnchecked": "未チェック", "choicechipPrefix": { - "is": "である" + "is": "は" } }, "checklistFilter": { @@ -1394,537 +413,184 @@ "isIncomplted": "未完了" }, "selectOptionFilter": { - "is": "である", - "isNot": "ではない", - "contains": "含む", - "doesNotContain": "含まない", - "isEmpty": "空", - "isNotEmpty": "空でない" - }, - "dateFilter": { - "is": "である", - "before": "以前", - "after": "以後", - "onOrBefore": "以前またはその日", - "onOrAfter": "以後またはその日", - "between": "の間", - "empty": "空", - "notEmpty": "空でない", - "startDate": "開始日", - "endDate": "終了日", - "choicechipPrefix": { - "before": "以前", - "after": "以後", - "between": "間", - "onOrBefore": "以前またはその日", - "onOrAfter": "以後またはその日", - "isEmpty": "空", - "isNotEmpty": "空でない" - } - }, - "numberFilter": { - "equal": "等しい", - "notEqual": "等しくない", - "lessThan": "より小さい", - "greaterThan": "より大きい", - "lessThanOrEqualTo": "以下", - "greaterThanOrEqualTo": "以上", - "isEmpty": "空", - "isNotEmpty": "空でない" + "is": "等しい", + "isNot": "等しくない", + "contains": "を含む", + "doesNotContain": "を含まない", + "isEmpty": "空である", + "isNotEmpty": "空ではない" }, "field": { - "label": "プロパティ", - "hide": "非表示", - "show": "表示", + "hide": "隠す", "insertLeft": "左に挿入", "insertRight": "右に挿入", - "duplicate": "複製", + "duplicate": "コピーを作成", "delete": "削除", - "wrapCellContent": "テキストを折り返し", - "clear": "セルをクリア", - "switchPrimaryFieldTooltip": "プライマリフィールドのフィールドタイプを変更できません", "textFieldName": "テキスト", "checkboxFieldName": "チェックボックス", "dateFieldName": "日付", - "updatedAtFieldName": "最終更新日", - "createdAtFieldName": "作成日", - "numberFieldName": "数字", - "singleSelectFieldName": "選択", - "multiSelectFieldName": "マルチセレクト", + "updatedAtFieldName": "最終変更時刻", + "createdAtFieldName": "作成時間", + "numberFieldName": "数値", + "singleSelectFieldName": "単一選択", + "multiSelectFieldName": "複数選択", "urlFieldName": "URL", "checklistFieldName": "チェックリスト", - "relationFieldName": "リレーション", - "summaryFieldName": "AIサマリー", - "timeFieldName": "時間", - "mediaFieldName": "ファイルとメディア", - "translateFieldName": "AI翻訳", - "translateTo": "翻訳先", - "numberFormat": "数字の形式", - "dateFormat": "日付の形式", - "includeTime": "時間を含む", - "isRange": "終了日", + "numberFormat": "数値書式", + "dateFormat": "日付書式", + "includeTime": "時刻を含める", "dateFormatFriendly": "月 日, 年", "dateFormatISO": "年-月-日", "dateFormatLocal": "月/日/年", "dateFormatUS": "年/月/日", - "dateFormatDayMonthYear": "日/月/年", - "timeFormat": "時間形式", + "dateFormatDayMonthYear": "日月年", + "timeFormat": "時刻書式", "invalidTimeFormat": "無効な形式", - "timeFormatTwelveHour": "12時間制", - "timeFormatTwentyFourHour": "24時間制", - "clearDate": "日付をクリア", - "dateTime": "日時", - "startDateTime": "開始日時", - "endDateTime": "終了日時", - "failedToLoadDate": "日付の読み込みに失敗しました", - "selectTime": "時間を選択", - "selectDate": "日付を選択", - "visibility": "表示", - "propertyType": "プロパティの種類", - "addSelectOption": "オプションを追加", - "typeANewOption": "新しいオプションを入力", - "optionTitle": "オプション", - "addOption": "オプションを追加", - "editProperty": "プロパティを編集", + "timeFormatTwelveHour": "12 時間表記", + "timeFormatTwentyFourHour": "24 時間表記", + "addSelectOption": "選択候補追加", + "optionTitle": "選択候補", + "addOption": "選択候補追加", + "editProperty": "プロパティの編集", "newProperty": "新しいプロパティ", - "openRowDocument": "ページとして開く", - "deleteFieldPromptMessage": "本当によろしいですか?このプロパティとそのすべてのデータが削除されます", - "clearFieldPromptMessage": "本当に空にしますか?この列のすべてのセルが空になります", - "newColumn": "新しい列", - "format": "形式", - "reminderOnDateTooltip": "このセルにはリマインダーが設定されています", - "optionAlreadyExist": "オプションはすでに存在します" - }, - "rowPage": { - "newField": "新しいフィールドを追加", - "fieldDragElementTooltip": "メニューを開くにはクリック", - "showHiddenFields": { - "one": "{count} 非表示フィールドを表示", - "many": "{count} 非表示フィールドを表示", - "other": "{count} 非表示フィールドを表示" - }, - "hideHiddenFields": { - "one": "{count} 非表示フィールドを隠す", - "many": "{count} 非表示フィールドを隠す", - "other": "{count} 非表示フィールドを隠す" - }, - "openAsFullPage": "全画面で開く", - "moreRowActions": "その他の行アクション" + "deleteFieldPromptMessage": "本当にこのプロパティを削除してもよろしいですか?" }, "sort": { "ascending": "昇順", "descending": "降順", - "by": "基準", - "empty": "アクティブな並べ替えがありません", - "cannotFindCreatableField": "並べ替え可能なフィールドが見つかりません", - "deleteAllSorts": "すべての並べ替えを削除", - "addSort": "新しい並べ替えを追加", - "sortsActive": "並べ替え中に{intention}できません", - "removeSorting": "並べ替えを削除しますか?", - "fieldInUse": "このフィールドですでに並べ替えが行われています" + "addSort": "並べ替えの追加", + "deleteSort": "ソートの削除" }, "row": { - "label": "行", - "duplicate": "複製", + "duplicate": "コピーを作成", "delete": "削除", - "titlePlaceholder": "無題", - "textPlaceholder": "空", - "copyProperty": "プロパティをクリップボードにコピー", + "textPlaceholder": "空白", + "copyProperty": "プロパティをクリップボードにコピーしました", "count": "カウント", "newRow": "新しい行", - "loadMore": "さらに読み込む", - "action": "アクション", - "add": "下に追加をクリック", - "drag": "ドラッグして移動", - "deleteRowPrompt": "この行を削除してもよろしいですか?この操作は元に戻せません", - "deleteCardPrompt": "このカードを削除してもよろしいですか?この操作は元に戻せません", - "dragAndClick": "ドラッグして移動、クリックしてメニューを開く", - "insertRecordAbove": "上にレコードを挿入", - "insertRecordBelow": "下にレコードを挿入", - "noContent": "コンテンツなし", - "reorderRowDescription": "行を並べ替える", - "createRowAboveDescription": "上に行を作成", - "createRowBelowDescription": "下に行を挿入" + "action": "アクション" }, "selectOption": { "create": "作成", - "purpleColor": "パープル", + "purpleColor": "紫", "pinkColor": "ピンク", "lightPinkColor": "ライトピンク", "orangeColor": "オレンジ", - "yellowColor": "イエロー", + "yellowColor": "黄色", "limeColor": "ライム", - "greenColor": "グリーン", - "aquaColor": "アクア", - "blueColor": "ブルー", - "deleteTag": "タグを削除", - "colorPanelTitle": "カラー", - "panelTitle": "オプションを選択するか作成", - "searchOption": "オプションを検索", - "searchOrCreateOption": "オプションを検索するか作成", - "createNew": "新しいものを作成", - "orSelectOne": "またはオプションを選択", - "typeANewOption": "新しいオプションを入力", - "tagName": "タグ名" + "greenColor": "緑", + "aquaColor": "水色", + "blueColor": "青", + "deleteTag": "選択候補を削除", + "colorPanelTitle": "色", + "panelTitle": "選択候補を検索 または 作成する", + "searchOption": "選択候補を検索" }, "checklist": { - "taskHint": "タスクの説明", - "addNew": "新しいタスクを追加", - "submitNewTask": "作成", - "hideComplete": "完了したタスクを非表示", - "showComplete": "すべてのタスクを表示" - }, - "url": { - "launch": "リンクをブラウザで開く", - "copy": "リンクをクリップボードにコピー", - "textFieldHint": "URLを入力" - }, - "relation": { - "relatedDatabasePlaceLabel": "関連データベース", - "relatedDatabasePlaceholder": "なし", - "inRelatedDatabase": "に", - "rowSearchTextFieldPlaceholder": "検索", - "noDatabaseSelected": "データベースが選択されていません。まず以下のリストから1つ選択してください:", - "emptySearchResult": "レコードが見つかりません", - "linkedRowListLabel": "{count} リンクされた行", - "unlinkedRowListLabel": "他の行をリンク" + "addNew": "アイテムを追加する" }, "menuName": "グリッド", - "referencedGridPrefix": "ビュー", - "calculate": "計算", - "calculationTypeLabel": { - "none": "なし", - "average": "平均", - "max": "最大", - "median": "中央値", - "min": "最小", - "sum": "合計", - "count": "カウント", - "countEmpty": "空のカウント", - "countEmptyShort": "空", - "countNonEmpty": "空でないカウント", - "countNonEmptyShort": "入力済み" - }, - "media": { - "rename": "名前を変更", - "download": "ダウンロード", - "expand": "拡大", - "delete": "削除", - "moreFilesHint": "+{} ファイル", - "addFileOrImage": "ファイル、画像、またはリンクを追加", - "attachmentsHint": "{} 添付ファイル", - "addFileMobile": "ファイルを追加", - "extraCount": "+{}", - "deleteFileDescription": "このファイルを削除してもよろしいですか? この操作は元に戻せません。", - "showFileNames": "ファイル名を表示", - "downloadSuccess": "ファイルをダウンロードしました", - "downloadFailedToken": "ファイルのダウンロードに失敗しました。ユーザー トークンが利用できません", - "setAsCover": "カバーとして設定", - "openInBrowser": "ブラウザで開く", - "embedLink": "ファイルリンクを埋め込む", - "open": "開く", - "showMore": "{} ファイルがさらにあります。クリックして表示" - } + "referencedGridPrefix": "のビュー" }, "document": { - "menuName": "ドキュメント", + "menuName": "書類", "date": { - "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwelveHour": "午後1時", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "作成...", "slashMenu": { "board": { - "selectABoardToLinkTo": "リンクするボードを選択", - "createANewBoard": "新しいボードを作成" + "selectABoardToLinkTo": "リンク先のボードを選択してください", + "createANewBoard": "新しいボードを作成する" }, "grid": { - "selectAGridToLinkTo": "リンクするグリッドを選択", - "createANewGrid": "新しいグリッドを作成" + "selectAGridToLinkTo": "リンク先のグリッドを選択してください", + "createANewGrid": "新しいグリッドを作成する" }, "calendar": { - "selectACalendarToLinkTo": "リンクするカレンダーを選択", - "createANewCalendar": "新しいカレンダーを作成" - }, - "document": { - "selectADocumentToLinkTo": "リンクするドキュメントを選択" - }, - "name": { - "text": "テキスト", - "heading1": "見出し1", - "heading2": "見出し2", - "heading3": "見出し3", - "image": "画像", - "bulletedList": "箇条書きリスト", - "numberedList": "番号付きリスト", - "todoList": "To-do リスト", - "doc": "ドキュメント", - "linkedDoc": "ページへのリンク", - "grid": "グリッド", - "linkedGrid": "リンクされたグリッド", - "kanban": "カンバン", - "linkedKanban": "リンクされたカンバン", - "calendar": "カレンダー", - "linkedCalendar": "リンクされたカレンダー", - "quote": "引用", - "divider": "区切り線", - "table": "テーブル", - "callout": "呼び出し", - "outline": "アウトライン", - "mathEquation": "数式", - "code": "コード", - "toggleList": "折りたたみリスト", - "toggleHeading1": "見出し 1 を切り替える", - "toggleHeading2": "見出し 2 を切り替える", - "toggleHeading3": "見出し 3 を切り替える", - "emoji": "絵文字", - "aiWriter": "AIライター", - "dateOrReminder": "日付またはリマインダー", - "photoGallery": "フォトギャラリー", - "file": "ファイル" - }, - "subPage": { - "name": "書類", - "keyword1": "サブページ", - "keyword2": "ページ", - "keyword3": "子ページ", - "keyword4": "ページを挿入", - "keyword5": "埋め込みページ", - "keyword6": "新しいページ", - "keyword7": "ページを作成", - "keyword8": "書類" + "selectACalendarToLinkTo": "リンク先のカレンダーを選択してください", + "createANewCalendar": "新しいカレンダーを作成する" } }, "selectionMenu": { - "outline": "アウトライン", - "codeBlock": "コードブロック" + "outline": "概要" }, "plugins": { - "referencedBoard": "参照されたボード", - "referencedGrid": "参照されたグリッド", - "referencedCalendar": "参照されたカレンダー", - "referencedDocument": "参照されたドキュメント", - "autoGeneratorMenuItemName": "AIライター", - "autoGeneratorTitleName": "AI: 任意の文章をAIに依頼...", - "autoGeneratorLearnMore": "詳細を読む", + "referencedBoard": "参照ボード", + "referencedGrid": "参照されるグリッド", + "referencedCalendar": "参照カレンダー", + "autoGeneratorMenuItemName": "OpenAI ライター", + "autoGeneratorTitleName": "OpenAI: AI に何でも書いてもらいます...", + "autoGeneratorLearnMore": "もっと詳しく知る", "autoGeneratorGenerate": "生成", - "autoGeneratorHintText": "AIに質問 ...", - "autoGeneratorCantGetOpenAIKey": "AIキーを取得できません", - "autoGeneratorRewrite": "書き直し", - "smartEdit": "AIに依頼", - "aI": "AI", - "smartEditFixSpelling": "スペルと文法を修正", - "warning": "⚠️ AIの回答は不正確または誤解を招く可能性があります。", - "smartEditSummarize": "要約", - "smartEditImproveWriting": "文章を改善", - "smartEditMakeLonger": "長くする", - "smartEditCouldNotFetchResult": "AIから結果を取得できませんでした", - "smartEditCouldNotFetchKey": "AIキーを取得できませんでした", - "smartEditDisabled": "設定でAIを接続してください", - "appflowyAIEditDisabled": "AI機能を有効にするにはサインインしてください", - "discardResponse": "AIの回答を破棄しますか?", - "createInlineMathEquation": "数式を作成", - "fonts": "フォント", - "insertDate": "日付を挿入", - "emoji": "絵文字", - "toggleList": "折りたたみリスト", - "emptyToggleHeading": "トグル h{} が空です。クリックしてコンテンツを追加します。", - "emptyToggleList": "トグル リストが空です。クリックしてコンテンツを追加します。", - "emptyToggleHeadingWeb": "トグル h{level} が空です。クリックしてコンテンツを追加してください", - "quoteList": "引用リスト", - "numberedList": "番号付きリスト", - "bulletedList": "箇条書きリスト", - "todoList": "To-do リスト", - "callout": "呼び出し", - "simpleTable": { - "moreActions": { - "color": "色", - "align": "整列", - "delete": "消去", - "duplicate": "重複", - "insertLeft": "左に挿入", - "insertRight": "右に挿入", - "insertAbove": "上に挿入", - "insertBelow": "以下に挿入", - "headerColumn": "ヘッダー列", - "headerRow": "ヘッダー行", - "clearContents": "内容をクリア", - "setToPageWidth": "ページ幅に設定", - "distributeColumnsWidth": "列を均等に分散する", - "duplicateRow": "重複行", - "duplicateColumn": "重複した列", - "textColor": "テキストの色", - "cellBackgroundColor": "セルの背景色", - "duplicateTable": "重複テーブル" - }, - "clickToAddNewRow": "クリックして新しい行を追加します", - "clickToAddNewColumn": "クリックして新しい列を追加します", - "clickToAddNewRowAndColumn": "クリックして新しい行と列を追加します", - "headerName": { - "table": "テーブル", - "alignText": "テキストを揃える" - } - }, + "autoGeneratorHintText": "OpenAIに質問してください...", + "autoGeneratorCantGetOpenAIKey": "OpenAI キーを取得できません", + "autoGeneratorRewrite": "リライト", + "smartEdit": "AIアシスタント", + "openAI": "OpenAI", + "smartEditFixSpelling": "スペルを修正", + "warning": "⚠️ AI の応答は不正確または誤解を招く可能性があります。", + "smartEditSummarize": "要約する", + "smartEditImproveWriting": "ライティングを改善する", + "smartEditMakeLonger": "もっと長くする", + "smartEditCouldNotFetchResult": "OpenAIから結果を取得できませんでした", + "smartEditCouldNotFetchKey": "OpenAI キーを取得できませんでした", + "smartEditDisabled": "設定で OpenAI に接続する", + "discardResponse": "AI の応答を破棄しますか?", + "createInlineMathEquation": "方程式の作成", + "toggleList": "リストの切り替え", "cover": { - "changeCover": "カバーを変更", + "changeCover": "表紙を変える", "colors": "色", "images": "画像", "clearAll": "すべてクリア", - "abstract": "抽象的", - "addCover": "カバーを追加", - "addLocalImage": "ローカル画像を追加", - "invalidImageUrl": "無効な画像URL", - "failedToAddImageToGallery": "ギャラリーに画像を追加できませんでした", - "enterImageUrl": "画像URLを入力", + "abstract": "概要", + "addCover": "カバーを追加する", + "addLocalImage": "ローカルイメージを追加", + "invalidImageUrl": "無効な画像 URL", + "failedToAddImageToGallery": "画像をギャラリーに追加できませんでした", + "enterImageUrl": "画像のURLを入力してください", "add": "追加", "back": "戻る", "saveToGallery": "ギャラリーに保存", "removeIcon": "アイコンを削除", - "removeCover": "カバーを削除", - "pasteImageUrl": "画像URLを貼り付け", - "or": "または", + "pasteImageUrl": "画像のURLを貼り付けます", + "or": "また", "pickFromFiles": "ファイルから選択", "couldNotFetchImage": "画像を取得できませんでした", "imageSavingFailed": "画像の保存に失敗しました", "addIcon": "アイコンを追加", - "changeIcon": "アイコンを変更", - "coverRemoveAlert": "削除するとカバーからも削除されます。", - "alertDialogConfirmation": "本当に続けますか?" + "coverRemoveAlert": "削除後は表紙からも外されます。", + "alertDialogConfirmation": "続行しますか?" }, "mathEquation": { - "name": "数式", - "addMathEquation": "TeX数式を追加", - "editMathEquation": "数式を編集" + "addMathEquation": "数式を追加", + "editMathEquation": "数式の編集" }, "optionAction": { "click": "クリック", - "toOpenMenu": " でメニューを開く", - "drag": "ドラッグ", - "toMove": " 移動する", - "delete": "削除", + "toOpenMenu": " メニューを開く", + "delete": "消去", "duplicate": "複製", - "turnInto": "変換", + "turnInto": "へ変更", "moveUp": "上に移動", "moveDown": "下に移動", "color": "色", "align": "整列", "left": "左", - "center": "中央", + "center": "中心", "right": "右", - "defaultColor": "デフォルト", - "depth": "深さ", - "copyLinkToBlock": "リンクをブロックにコピーする" + "defaultColor": "デフォルト" }, "image": { - "addAnImage": "画像を追加", - "copiedToPasteBoard": "画像リンクがクリップボードにコピーされました", - "addAnImageDesktop": "画像を追加", - "addAnImageMobile": "クリックして画像を追加", - "dropImageToInsert": "挿入する画像をドロップ", - "imageUploadFailed": "画像のアップロードに失敗しました", - "imageDownloadFailed": "画像のダウンロードに失敗しました。もう一度お試しください", - "imageDownloadFailedToken": "ユーザートークンがないため、画像のアップロードに失敗しました。もう一度お試しください", - "errorCode": "エラーコード" - }, - "photoGallery": { - "name": "フォトギャラリー", - "imageKeyword": "画像", - "imageGalleryKeyword": "画像ギャラリー", - "photoKeyword": "写真", - "photoBrowserKeyword": "フォトブラウザ", - "galleryKeyword": "ギャラリー", - "addImageTooltip": "画像を追加", - "changeLayoutTooltip": "レイアウトを変更", - "browserLayout": "ブラウザ", - "gridLayout": "グリッド", - "deleteBlockTooltip": "ギャラリー全体を削除" - }, - "math": { - "copiedToPasteBoard": "数式がクリップボードにコピーされました" - }, - "urlPreview": { - "copiedToPasteBoard": "リンクがクリップボードにコピーされました", - "convertToLink": "埋め込みリンクに変換" + "copiedToPasteBoard": "画像リンクがクリップボードにコピーされました" }, "outline": { - "addHeadingToCreateOutline": "目次を作成するには見出しを追加してください。", - "noMatchHeadings": "一致する見出しが見つかりませんでした。" - }, - "table": { - "addAfter": "後に追加", - "addBefore": "前に追加", - "delete": "削除", - "clear": "内容をクリア", - "duplicate": "複製", - "bgColor": "背景色" - }, - "contextMenu": { - "copy": "コピー", - "cut": "切り取り", - "paste": "貼り付け", - "pasteAsPlainText": "プレーンテキストとして貼り付け" - }, - "action": "アクション", - "database": { - "selectDataSource": "データソースを選択", - "noDataSource": "データソースなし", - "selectADataSource": "データソースを選択", - "toContinue": "続行する", - "newDatabase": "新しいデータベース", - "linkToDatabase": "データベースにリンク" - }, - "date": "日付", - "video": { - "label": "ビデオ", - "emptyLabel": "ビデオを追加", - "placeholder": "ビデオリンクを貼り付け", - "copiedToPasteBoard": "ビデオリンクがクリップボードにコピーされました", - "insertVideo": "ビデオを追加", - "invalidVideoUrl": "ソースURLはサポートされていません。", - "invalidVideoUrlYouTube": "YouTubeはまだサポートされていません。", - "supportedFormats": "サポートされている形式: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "ファイル", - "uploadTab": "アップロード", - "uploadMobile": "ファイルを選択", - "uploadMobileGallery": "フォトギャラリーより", - "networkTab": "リンクを埋め込む", - "placeholderText": "ファイルをアップロードまたは埋め込む", - "placeholderDragging": "アップロードするファイルをドロップ", - "dropFileToUpload": "アップロードするファイルをドロップ", - "fileUploadHint": "ファイルをここにドロップ\nまたはクリックして参照", - "fileUploadHintSuffix": "ブラウズ", - "networkHint": "ファイルリンクを貼り付け", - "networkUrlInvalid": "無効なURLです。URLを修正してもう一度お試しください", - "networkAction": "ファイルリンクを埋め込む", - "fileTooBigError": "ファイルサイズが大きすぎます。10MB未満のファイルをアップロードしてください", - "renameFile": { - "title": "ファイルの名前を変更", - "description": "このファイルの新しい名前を入力してください", - "nameEmptyError": "ファイル名を空にすることはできません。" - }, - "uploadedAt": "アップロード日: {}", - "linkedAt": "リンク追加日: {}", - "failedToOpenMsg": "開けませんでした。ファイルが見つかりません" - }, - "subPage": { - "handlingPasteHint": " - (ペーストの取り扱い)", - "errors": { - "failedDeletePage": "ページの削除に失敗しました", - "failedCreatePage": "ページの作成に失敗しました", - "failedMovePage": "このドキュメントにページを移動できませんでした", - "failedDuplicatePage": "ページの複製に失敗しました", - "failedDuplicateFindView": "ページの複製に失敗しました - 元のビューが見つかりません" - } - }, - "cannotMoveToItsChildren": "子に移動できません" - }, - "outlineBlock": { - "placeholder": "目次" + "addHeadingToCreateOutline": "見出しを追加して目次を作成します。" + } }, "textBlock": { - "placeholder": "'/' を入力してコマンドを使用" + "placeholder": "コマンドには「/」を入力します" }, "title": { "placeholder": "無題" @@ -1937,1114 +603,87 @@ }, "url": { "label": "画像URL", - "placeholder": "画像URLを入力" + "placeholder": "画像のURLを入力してください" }, - "ai": { - "label": "AIから画像を生成", - "placeholder": "AIに画像を生成させるプロンプトを入力してください" - }, - "stability_ai": { - "label": "Stability AIから画像を生成", - "placeholder": "Stability AIに画像を生成させるプロンプトを入力してください" - }, - "support": "画像サイズの上限は5MBです。サポートされている形式: JPEG, PNG, GIF, SVG", + "support": "画像サイズ制限は5MBです。サポートされている形式: JPEG、PNG、GIF、SVG", "error": { "invalidImage": "無効な画像です", "invalidImageSize": "画像サイズは5MB未満である必要があります", - "invalidImageFormat": "サポートされていない画像形式です。サポートされている形式: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "無効な画像URLです", - "noImage": "ファイルまたはディレクトリが見つかりません", - "multipleImagesFailed": "1つ以上の画像のアップロードに失敗しました。再試行してください" - }, - "embedLink": { - "label": "リンクを埋め込む", - "placeholder": "画像リンクを貼り付けるか入力" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "画像を検索", - "pleaseInputYourOpenAIKey": "設定ページでAIキーを入力してください", - "saveImageToGallery": "画像をギャラリーに保存", - "failedToAddImageToGallery": "ギャラリーに画像を追加できませんでした", - "successToAddImageToGallery": "画像がギャラリーに正常に追加されました", - "unableToLoadImage": "画像を読み込めませんでした", - "maximumImageSize": "サポートされている画像の最大サイズは10MBです", - "uploadImageErrorImageSizeTooBig": "画像サイズは10MB未満である必要があります", - "imageIsUploading": "画像をアップロード中", - "openFullScreen": "全画面で開く", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "前の画像", - "nextImageTooltip": "次の画像", - "zoomOutTooltip": "ズームアウト", - "zoomInTooltip": "ズームイン", - "changeZoomLevelTooltip": "ズームレベルを変更", - "openLocalImage": "画像を開く", - "downloadImage": "画像をダウンロード", - "closeViewer": "インタラクティブビューアを閉じる", - "scalePercentage": "{}%", - "deleteImageTooltip": "画像を削除" - } + "invalidImageFormat": "画像形式はサポートされていません。サポートされている形式: JPEG、PNG、GIF、SVG", + "invalidImageUrl": "無効な画像 URL" } }, "codeBlock": { "language": { "label": "言語", - "placeholder": "言語を選択", - "auto": "自動" - }, - "copyTooltip": "コピー", - "searchLanguageHint": "言語を検索", - "codeCopiedSnackbar": "コードがクリップボードにコピーされました!" + "placeholder": "言語を選択する" + } }, "inlineLink": { - "placeholder": "リンクを貼り付けるか入力", - "openInNewTab": "新しいタブで開く", - "copyLink": "リンクをコピー", - "removeLink": "リンクを削除", + "placeholder": "リンクを貼り付けるか入力します", "url": { "label": "リンクURL", - "placeholder": "リンクURLを入力" + "placeholder": "リンクURLを入力してください" }, "title": { "label": "リンクタイトル", - "placeholder": "リンクタイトルを入力" + "placeholder": "リンクタイトルを入力してください" } - }, - "mention": { - "placeholder": "人物、ページ、日付をメンション...", - "page": { - "label": "ページへのリンク", - "tooltip": "クリックしてページを開く" - }, - "deleted": "削除済み", - "deletedContent": "このコンテンツは存在しないか、削除されました", - "noAccess": "アクセス権がありません", - "deletedPage": "削除されたページ", - "trashHint": " - ゴミ箱に入れる", - "morePages": "その他のページ" - }, - "toolbar": { - "resetToDefaultFont": "デフォルトに戻す" - }, - "errorBlock": { - "theBlockIsNotSupported": "ブロックコンテンツを解析できません", - "clickToCopyTheBlockContent": "クリックしてブロックコンテンツをコピー", - "blockContentHasBeenCopied": "ブロックコンテンツがコピーされました。", - "parseError": "{}ブロックの解析中にエラーが発生しました。", - "copyBlockContent": "ブロックコンテンツをコピー" - }, - "mobilePageSelector": { - "title": "ページを選択", - "failedToLoad": "ページリストの読み込みに失敗しました", - "noPagesFound": "ページが見つかりません" - }, - "attachmentMenu": { - "choosePhoto": "写真を選択", - "takePicture": "写真を撮る", - "chooseFile": "ファイルを選択" } }, "board": { "column": { - "label": "カラム", - "createNewCard": "新規", - "renameGroupTooltip": "押してグループ名を変更", - "createNewColumn": "新しいグループを追加", - "addToColumnTopTooltip": "上に新しいカードを追加", - "addToColumnBottomTooltip": "下に新しいカードを追加", - "renameColumn": "名前を変更", - "hideColumn": "非表示", - "newGroup": "新しいグループ", - "deleteColumn": "削除", - "deleteColumnConfirmation": "このグループとその中のすべてのカードが削除されます。\n続行してもよろしいですか?" + "createNewCard": "新しい" }, - "hiddenGroupSection": { - "sectionTitle": "非表示のグループ", - "collapseTooltip": "非表示グループを隠す", - "expandTooltip": "非表示グループを表示" - }, - "cardDetail": "カードの詳細", - "cardActions": "カードアクション", - "cardDuplicated": "カードが複製されました", - "cardDeleted": "カードが削除されました", - "showOnCard": "カードの詳細に表示", - "setting": "設定", - "propertyName": "プロパティ名", "menuName": "ボード", - "showUngrouped": "グループ化されていない項目を表示", - "ungroupedButtonText": "グループ化されていない", - "ungroupedButtonTooltip": "どのグループにも属していないカードが含まれています", - "ungroupedItemsTitle": "クリックしてボードに追加", - "groupBy": "グループ化", - "groupCondition": "グループ条件", - "referencedBoardPrefix": "表示元", - "notesTooltip": "内部のメモ", + "referencedBoardPrefix": "のビュー", "mobile": { - "editURL": "URLを編集", "showGroup": "グループを表示", - "showGroupContent": "このグループをボード上に表示してもよろしいですか?", + "showGroupContent": "このグループをボードに表示してもよろしいですか?", "failedToLoad": "ボードビューの読み込みに失敗しました" - }, - "dateCondition": { - "weekOf": "{}週 - {}", - "today": "今日", - "yesterday": "昨日", - "tomorrow": "明日", - "lastSevenDays": "過去7日間", - "nextSevenDays": "次の7日間", - "lastThirtyDays": "過去30日間", - "nextThirtyDays": "次の30日間" - }, - "noGroup": "プロパティによるグループ化なし", - "noGroupDesc": "ボードビューを表示するには、グループ化するプロパティが必要です", - "media": { - "cardText": "{} {}", - "fallbackName": "ファイル" } }, "calendar": { "menuName": "カレンダー", "defaultNewCalendarTitle": "無題", - "newEventButtonTooltip": "新しいイベントを追加", "navigation": { "today": "今日", "jumpToday": "今日にジャンプ", - "previousMonth": "前の月", - "nextMonth": "次の月", - "views": { - "day": "日", - "week": "週", - "month": "月", - "year": "年" - } - }, - "mobileEventScreen": { - "emptyTitle": "まだイベントがありません", - "emptyBody": "プラスボタンを押してこの日にイベントを作成してください。" + "previousMonth": "前月", + "nextMonth": "来月" }, "settings": { - "showWeekNumbers": "週番号を表示", - "showWeekends": "週末を表示", + "showWeekNumbers": "週番号を表示する", + "showWeekends": "週末を表示する", "firstDayOfWeek": "週の開始日", - "layoutDateField": "カレンダーのレイアウト", - "changeLayoutDateField": "レイアウトフィールドを変更", + "layoutDateField": "レイアウトカレンダー", "noDateTitle": "日付なし", - "noDateHint": { - "zero": "予定されていないイベントがここに表示されます", - "one": "{count} 件の予定されていないイベント", - "other": "{count} 件の予定されていないイベント" - }, - "unscheduledEventsTitle": "予定されていないイベント", - "clickToAdd": "クリックしてカレンダーに追加", - "name": "カレンダー設定", - "clickToOpen": "クリックしてレコードを開く" + "clickToAdd": "クリックしてカレンダーに追加します", + "name": "カレンダーのレイアウト", + "noDateHint": "予定外のイベントがここに表示されます" }, - "referencedCalendarPrefix": "表示元", - "quickJumpYear": "ジャンプ", - "duplicateEvent": "イベントを複製" + "referencedCalendarPrefix": "のビュー" }, "errorDialog": { - "title": "@:appName エラー", - "howToFixFallback": "ご不便をおかけして申し訳ありません!エラー内容をGitHubページに報告してください。", - "howToFixFallbackHint1": "ご不便をおかけして申し訳ありません!エラー内容を報告するには、", - "howToFixFallbackHint2": "ページにアクセスしてください。", - "github": "GitHubで表示" + "title": "@:appNameエラー", + "howToFixFallback": "ご不便をおかけして申し訳ございません。エラーを説明した issue を GitHub ページに送信してください。", + "github": "GitHub で見る" }, "search": { "label": "検索", - "sidebarSearchIcon": "検索してページに素早く移動", "placeholder": { - "actions": "アクションを検索..." + "actions": "検索アクション..." } }, "message": { "copy": { - "success": "コピー完了!", - "fail": "コピーに失敗しました" + "success": "コピーしました!", + "fail": "コピーできません" } }, - "unSupportBlock": "このバージョンではこのブロックはサポートされていません。", + "unSupportBlock": "現在のバージョンではこのブロックはサポートされていません。", "views": { - "deleteContentTitle": "{pageType}を削除してもよろしいですか?", - "deleteContentCaption": "この{pageType}を削除すると、ゴミ箱から復元できます。" - }, - "colors": { - "custom": "カスタム", - "default": "デフォルト", - "red": "赤", - "orange": "オレンジ", - "yellow": "黄色", - "green": "緑", - "blue": "青", - "purple": "紫", - "pink": "ピンク", - "brown": "茶色", - "gray": "灰色" - }, - "emoji": { - "emojiTab": "絵文字", - "search": "絵文字を検索", - "noRecent": "最近使用された絵文字なし", - "noEmojiFound": "絵文字が見つかりません", - "filter": "フィルター", - "random": "ランダム", - "selectSkinTone": "スキントーンを選択", - "remove": "絵文字を削除", - "categories": { - "smileys": "スマイリー&感情", - "people": "人々", - "animals": "自然", - "food": "食べ物", - "activities": "アクティビティ", - "places": "場所", - "objects": "オブジェクト", - "symbols": "記号", - "flags": "旗", - "nature": "自然", - "frequentlyUsed": "よく使われる" - }, - "skinTone": { - "default": "デフォルト", - "light": "明るい", - "mediumLight": "やや明るい", - "medium": "普通", - "mediumDark": "やや暗い", - "dark": "暗い" - }, - "openSourceIconsFrom": "オープンソースのアイコン提供元" - }, - "inlineActions": { - "noResults": "結果なし", - "recentPages": "最近のページ", - "pageReference": "ページ参照", - "docReference": "ドキュメント参照", - "boardReference": "ボード参照", - "calReference": "カレンダー参照", - "gridReference": "グリッド参照", - "date": "日付", - "reminder": { - "groupTitle": "リマインダー", - "shortKeyword": "リマインド" - }, - "createPage": "\"{}\" サブページを作成" - }, - "datePicker": { - "dateTimeFormatTooltip": "設定で日付と時刻の形式を変更", - "dateFormat": "日付形式", - "includeTime": "時刻を含む", - "isRange": "終了日", - "timeFormat": "時刻形式", - "clearDate": "日付をクリア", - "reminderLabel": "リマインダー", - "selectReminder": "リマインダーを選択", - "reminderOptions": { - "none": "なし", - "atTimeOfEvent": "イベント時", - "fiveMinsBefore": "5分前", - "tenMinsBefore": "10分前", - "fifteenMinsBefore": "15分前", - "thirtyMinsBefore": "30分前", - "oneHourBefore": "1時間前", - "twoHoursBefore": "2時間前", - "onDayOfEvent": "イベント当日", - "oneDayBefore": "1日前", - "twoDaysBefore": "2日前", - "oneWeekBefore": "1週間前", - "custom": "カスタム" - } - }, - "relativeDates": { - "yesterday": "昨日", - "today": "今日", - "tomorrow": "明日", - "oneWeek": "1週間" - }, - "notificationHub": { - "title": "通知", - "mobile": { - "title": "更新" - }, - "emptyTitle": "すべて完了!", - "emptyBody": "保留中の通知やアクションはありません。", - "tabs": { - "inbox": "受信箱", - "upcoming": "今後" - }, - "actions": { - "markAllRead": "すべて既読にする", - "showAll": "すべて表示", - "showUnreads": "未読のみ表示" - }, - "filters": { - "ascending": "昇順", - "descending": "降順", - "groupByDate": "日付でグループ化", - "showUnreadsOnly": "未読のみ表示", - "resetToDefault": "デフォルトにリセット" - } - }, - "reminderNotification": { - "title": "リマインダー", - "message": "忘れないうちに確認してください!", - "tooltipDelete": "削除", - "tooltipMarkRead": "既読にする", - "tooltipMarkUnread": "未読にする" - }, - "findAndReplace": { - "find": "検索", - "previousMatch": "前の一致", - "nextMatch": "次の一致", - "close": "閉じる", - "replace": "置換", - "replaceAll": "すべて置換", - "noResult": "結果なし", - "caseSensitive": "大文字と小文字を区別", - "searchMore": "さらに検索して結果を探す" - }, - "error": { - "weAreSorry": "申し訳ありません", - "loadingViewError": "このビューの読み込みに問題があります。インターネット接続を確認し、アプリを更新してください。問題が続く場合は、チームにお問い合わせください。", - "syncError": "別のデバイスからデータが同期されていません", - "syncErrorHint": "最後に編集したデバイスでこのページを再度開き、次に現在のデバイスで再度開いてください。", - "clickToCopy": "クリックしてエラーコードをコピー" - }, - "editor": { - "bold": "太字", - "bulletedList": "箇条書き", - "bulletedListShortForm": "箇条書き", - "checkbox": "チェックボックス", - "embedCode": "コードを埋め込む", - "heading1": "見出し1", - "heading2": "見出し2", - "heading3": "見出し3", - "highlight": "ハイライト", - "color": "色", - "image": "画像", - "date": "日付", - "page": "ページ", - "italic": "斜体", - "link": "リンク", - "numberedList": "番号付きリスト", - "numberedListShortForm": "番号付き", - "toggleHeading1ShortForm": "h1 トグル", - "toggleHeading2ShortForm": "h2 トグル", - "toggleHeading3ShortForm": "h3 トグル", - "quote": "引用", - "strikethrough": "取り消し線", - "text": "テキスト", - "underline": "下線", - "fontColorDefault": "デフォルト", - "fontColorGray": "灰色", - "fontColorBrown": "茶色", - "fontColorOrange": "オレンジ", - "fontColorYellow": "黄色", - "fontColorGreen": "緑", - "fontColorBlue": "青", - "fontColorPurple": "紫", - "fontColorPink": "ピンク", - "fontColorRed": "赤", - "backgroundColorDefault": "デフォルト背景", - "backgroundColorGray": "灰色背景", - "backgroundColorBrown": "茶色背景", - "backgroundColorOrange": "オレンジ背景", - "backgroundColorYellow": "黄色背景", - "backgroundColorGreen": "緑背景", - "backgroundColorBlue": "青背景", - "backgroundColorPurple": "紫背景", - "backgroundColorPink": "ピンク背景", - "backgroundColorRed": "赤背景", - "backgroundColorLime": "ライム背景", - "backgroundColorAqua": "アクア背景", - "done": "完了", - "cancel": "キャンセル", - "tint1": "ティント1", - "tint2": "ティント2", - "tint3": "ティント3", - "tint4": "ティント4", - "tint5": "ティント5", - "tint6": "ティント6", - "tint7": "ティント7", - "tint8": "ティント8", - "tint9": "ティント9", - "lightLightTint1": "紫", - "lightLightTint2": "ピンク", - "lightLightTint3": "ライトピンク", - "lightLightTint4": "オレンジ", - "lightLightTint5": "黄色", - "lightLightTint6": "ライム", - "lightLightTint7": "緑", - "lightLightTint8": "アクア", - "lightLightTint9": "青", - "urlHint": "URL", - "mobileHeading1": "見出し1", - "mobileHeading2": "見出し2", - "mobileHeading3": "見出し3", - "mobileHeading4": "見出し4", - "mobileHeading5": "見出し5", - "mobileHeading6": "見出し6", - "textColor": "テキスト色", - "backgroundColor": "背景色", - "addYourLink": "リンクを追加", - "openLink": "リンクを開く", - "copyLink": "リンクをコピー", - "removeLink": "リンクを削除", - "editLink": "リンクを編集", - "linkText": "テキスト", - "linkTextHint": "テキストを入力してください", - "linkAddressHint": "URLを入力してください", - "highlightColor": "ハイライト色", - "clearHighlightColor": "ハイライト色をクリア", - "customColor": "カスタムカラー", - "hexValue": "16進数値", - "opacity": "不透明度", - "resetToDefaultColor": "デフォルト色にリセット", - "ltr": "左から右", - "rtl": "右から左", - "auto": "自動", - "cut": "切り取り", - "copy": "コピー", - "paste": "貼り付け", - "find": "検索", - "select": "選択", - "selectAll": "すべて選択", - "previousMatch": "前の一致", - "nextMatch": "次の一致", - "closeFind": "閉じる", - "replace": "置換", - "replaceAll": "すべて置換", - "regex": "正規表現", - "caseSensitive": "大文字小文字を区別", - "uploadImage": "画像をアップロード", - "urlImage": "URL画像", - "incorrectLink": "不正なリンク", - "upload": "アップロード", - "chooseImage": "画像を選択", - "loading": "読み込み中", - "imageLoadFailed": "画像の読み込みに失敗しました", - "divider": "区切り線", - "table": "テーブル", - "colAddBefore": "前に追加", - "rowAddBefore": "前に追加", - "colAddAfter": "後に追加", - "rowAddAfter": "後に追加", - "colRemove": "削除", - "rowRemove": "削除", - "colDuplicate": "複製", - "rowDuplicate": "複製", - "colClear": "内容をクリア", - "rowClear": "内容をクリア", - "slashPlaceHolder": "'/'を入力してブロックを挿入、または入力を開始", - "typeSomething": "何かを入力...", - "toggleListShortForm": "トグル", - "quoteListShortForm": "引用", - "mathEquationShortForm": "数式", - "codeBlockShortForm": "コード" - }, - "favorite": { - "noFavorite": "お気に入りページがありません", - "noFavoriteHintText": "ページを左にスワイプして、お気に入りに追加します", - "removeFromSidebar": "サイドバーから削除", - "addToSidebar": "サイドバーにピン留め" - }, - "cardDetails": { - "notesPlaceholder": "ブロックを挿入するには「/」を入力、または入力を開始" - }, - "blockPlaceholders": { - "todoList": "To-do", - "bulletList": "リスト", - "numberList": "リスト", - "quote": "引用", - "heading": "見出し {}" - }, - "titleBar": { - "pageIcon": "ページアイコン", - "language": "言語", - "font": "フォント", - "actions": "アクション", - "date": "日付", - "addField": "フィールドを追加", - "userIcon": "ユーザーアイコン" - }, - "noLogFiles": "ログファイルがありません", - "newSettings": { - "myAccount": { - "title": "アカウント", - "subtitle": "プロフィールのカスタマイズ、アカウントセキュリティ管理、AIキーの設定、アカウントへのログイン管理。", - "profileLabel": "アカウント名とプロフィール画像", - "profileNamePlaceholder": "名前を入力", - "accountSecurity": "アカウントセキュリティ", - "2FA": "2段階認証", - "aiKeys": "AIキー", - "accountLogin": "アカウントログイン", - "updateNameError": "名前の更新に失敗しました", - "updateIconError": "アイコンの更新に失敗しました", - "deleteAccount": { - "title": "アカウント削除", - "subtitle": "アカウントとすべてのデータを完全に削除します。", - "description": "アカウントを削除し、すべてのワークスペースへのアクセスを削除します。", - "deleteMyAccount": "アカウントを削除", - "dialogTitle": "アカウント削除", - "dialogContent1": "アカウントを完全に削除してよろしいですか?", - "dialogContent2": "この操作は元に戻せません。すべてのワークスペースへのアクセスが削除され、アカウント全体とプライベートワークスペースを含むすべてが削除されます。", - "confirmHint1": "確認のために「DELETE MY ACCOUNT」と入力してください。", - "confirmHint2": "この操作が元に戻せないことを理解しました。", - "confirmHint3": "DELETE MY ACCOUNT", - "checkToConfirmError": "削除の確認チェックボックスを選択してください", - "failedToGetCurrentUser": "現在のユーザーの取得に失敗しました", - "confirmTextValidationFailed": "確認テキストが「DELETE MY ACCOUNT」と一致しません", - "deleteAccountSuccess": "アカウントが正常に削除されました" - } - }, - "workplace": { - "name": "ワークプレース", - "title": "ワークプレース設定", - "subtitle": "ワークスペースの外観、テーマ、フォント、テキストレイアウト、日付、時刻、言語をカスタマイズします。", - "workplaceName": "ワークプレース名", - "workplaceNamePlaceholder": "ワークプレース名を入力", - "workplaceIcon": "ワークプレースアイコン", - "workplaceIconSubtitle": "画像をアップロードするか、絵文字を使用してワークスペースを表現します。アイコンはサイドバーや通知で表示されます。", - "renameError": "ワークプレースの名前変更に失敗しました", - "updateIconError": "アイコンの更新に失敗しました", - "chooseAnIcon": "アイコンを選択", - "appearance": { - "name": "外観", - "themeMode": { - "auto": "自動", - "light": "ライト", - "dark": "ダーク" - }, - "language": "言語" - } - }, - "syncState": { - "syncing": "同期中", - "synced": "同期済み", - "noNetworkConnected": "ネットワークに接続されていません" - } - }, - "pageStyle": { - "title": "ページスタイル", - "layout": "レイアウト", - "coverImage": "カバー画像", - "pageIcon": "ページアイコン", - "colors": "色", - "gradient": "グラデーション", - "backgroundImage": "背景画像", - "presets": "プリセット", - "photo": "写真", - "unsplash": "Unsplash", - "pageCover": "ページカバー", - "none": "なし", - "openSettings": "設定を開く", - "photoPermissionTitle": "@:appName はフォトライブラリへのアクセスを希望します", - "photoPermissionDescription": "@:appName に写真へのアクセス権が必要です。写真をドキュメントに追加するには許可が必要です。", - "cameraPermissionTitle": "@:appNameカメラにアクセスしようとしています", - "cameraPermissionDescription": "カメラからドキュメントに画像を追加するには、 @:appNameにカメラへのアクセスを許可する必要があります", - "doNotAllow": "許可しない", - "image": "画像" - }, - "commandPalette": { - "placeholder": "検索ワードを入力...", - "bestMatches": "最適な候補", - "recentHistory": "最近の履歴", - "navigateHint": "移動するには", - "loadingTooltip": "結果を検索中...", - "betaLabel": "ベータ", - "betaTooltip": "現在はページやドキュメントの内容のみの検索をサポートしています", - "fromTrashHint": "ゴミ箱から", - "noResultsHint": "目的の結果が見つかりませんでした。別のキーワードで検索してください。", - "clearSearchTooltip": "検索フィールドをクリア" - }, - "space": { - "delete": "削除", - "deleteConfirmation": "削除: ", - "deleteConfirmationDescription": "このスペース内のすべてのページが削除され、ゴミ箱に移動されます。公開されたページは公開が取り消されます。", - "rename": "スペースの名前変更", - "changeIcon": "アイコンを変更", - "manage": "スペースの管理", - "addNewSpace": "スペースを作成", - "collapseAllSubPages": "すべてのサブページを折りたたむ", - "createNewSpace": "新しいスペースを作成", - "createSpaceDescription": "複数のパブリックおよびプライベートスペースを作成し、作業を整理しましょう。", - "spaceName": "スペース名", - "spaceNamePlaceholder": "例: マーケティング、エンジニアリング、HR", - "permission": "権限", - "publicPermission": "パブリック", - "publicPermissionDescription": "フルアクセスを持つすべてのワークスペースメンバー", - "privatePermission": "プライベート", - "privatePermissionDescription": "あなたのみがこのスペースにアクセス可能", - "spaceIconBackground": "アイコンの背景色", - "spaceIcon": "アイコン", - "dangerZone": "危険ゾーン", - "unableToDeleteLastSpace": "最後のスペースは削除できません", - "unableToDeleteSpaceNotCreatedByYou": "他のユーザーが作成したスペースは削除できません", - "enableSpacesForYourWorkspace": "ワークスペースでスペースを有効にする", - "title": "スペース", - "defaultSpaceName": "全般", - "upgradeSpaceTitle": "スペースを有効にする", - "upgradeSpaceDescription": "複数のパブリックおよびプライベートスペースを作成し、ワークスペースを整理しましょう。", - "upgrade": "アップグレード", - "upgradeYourSpace": "複数のスペースを作成", - "quicklySwitch": "次のスペースに素早く切り替える", - "duplicate": "スペースを複製", - "movePageToSpace": "ページをスペースに移動", - "cannotMovePageToDatabase": "ページをデータベースに移動できません", - "switchSpace": "スペースを切り替え", - "spaceNameCannotBeEmpty": "スペース名は空にできません", - "success": { - "deleteSpace": "スペースが正常に削除されました", - "renameSpace": "スペース名の変更に成功しました", - "duplicateSpace": "スペースが正常に複製されました", - "updateSpace": "スペースが正常に更新されました" - }, - "error": { - "deleteSpace": "スペースを削除できませんでした", - "renameSpace": "スペースの名前を変更できませんでした", - "duplicateSpace": "スペースの複製に失敗しました", - "updateSpace": "スペースの更新に失敗しました" - }, - "createSpace": "スペースを作る", - "manageSpace": "スペースを管理する", - "renameSpace": "スペースの名前を変更する", - "mSpaceIconColor": "スペースアイコンの色", - "mSpaceIcon": "スペースアイコン" - }, - "publish": { - "hasNotBeenPublished": "このページはまだ公開されていません", - "spaceHasNotBeenPublished": "スペースの公開はまだサポートされていません", - "reportPage": "ページを報告", - "databaseHasNotBeenPublished": "データベースの公開はまだサポートされていません。", - "createdWith": "作成元", - "downloadApp": "AppFlowy をダウンロード", - "copy": { - "codeBlock": "コードブロックの内容がクリップボードにコピーされました", - "imageBlock": "画像リンクがクリップボードにコピーされました", - "mathBlock": "数式がクリップボードにコピーされました", - "fileBlock": "ファイルリンクがクリップボードにコピーされました" - }, - "containsPublishedPage": "このページには公開済みのページが含まれています。続行すると公開が解除されます。削除してもよろしいですか?", - "publishSuccessfully": "正常に公開されました", - "unpublishSuccessfully": "正常に非公開にされました", - "publishFailed": "公開に失敗しました", - "unpublishFailed": "非公開に失敗しました", - "noAccessToVisit": "このページへのアクセス権がありません...", - "createWithAppFlowy": "AppFlowy でウェブサイトを作成", - "fastWithAI": "AIを使って迅速かつ簡単に。", - "tryItNow": "今すぐ試してみる", - "onlyGridViewCanBePublished": "グリッドビューのみが公開可能です", - "database": { - "zero": "{} 選択したビューを公開", - "one": "{} 選択したビューを公開", - "many": "{} 選択したビューを公開", - "other": "{} 選択したビューを公開" - }, - "mustSelectPrimaryDatabase": "プライマリビューを選択する必要があります", - "noDatabaseSelected": "少なくとも1つのデータベースを選択してください。", - "unableToDeselectPrimaryDatabase": "プライマリデータベースの選択を解除できません", - "saveThisPage": "このテンプレートを使用", - "duplicateTitle": "どこに追加しますか?", - "selectWorkspace": "ワークスペースを選択", - "addTo": "追加", - "duplicateSuccessfully": "ワークスペースに追加されました", - "duplicateSuccessfullyDescription": "AppFlowy がインストールされていませんか? 「ダウンロード」をクリックすると、自動的にダウンロードが開始されます。", - "downloadIt": "ダウンロード", - "openApp": "アプリで開く", - "duplicateFailed": "複製に失敗しました", - "membersCount": { - "zero": "メンバーなし", - "one": "1人のメンバー", - "many": "{count}人のメンバー", - "other": "{count}人のメンバー" - }, - "useThisTemplate": "このテンプレートを使用" - }, - "web": { - "continue": "続ける", - "or": "または", - "continueWithGoogle": "Googleで続ける", - "continueWithGithub": "GitHubで続ける", - "continueWithDiscord": "Discordで続ける", - "continueWithApple": "Appleで続ける", - "moreOptions": "他のオプション", - "collapse": "折りたたむ", - "signInAgreement": "「続ける」をクリックすると、AppFlowyの", - "and": "および", - "termOfUse": "利用規約", - "privacyPolicy": "プライバシーポリシー", - "signInError": "サインインエラー", - "login": "サインアップまたはログイン", - "fileBlock": { - "uploadedAt": "{time}にアップロード", - "linkedAt": "{time}にリンク追加", - "empty": "ファイルをアップロードまたは埋め込み", - "uploadFailed": "アップロードに失敗しました。もう一度お試しください。", - "retry": "リトライ" - }, - "importNotion": "Notionからインポート", - "import": "輸入", - "importSuccess": "アップロードに成功しました", - "importSuccessMessage": "インポートが完了すると通知されます。その後、インポートしたページをサイドバーで表示できます。", - "importFailed": "インポートに失敗しました。ファイル形式を確認してください", - "dropNotionFile": "Notionのzipファイルをここにドロップしてアップロードするか、クリックして参照してください", - "error": { - "pageNameIsEmpty": "ページ名が空です。別の名前を試してください" - } - }, - "globalComment": { - "comments": "コメント", - "addComment": "コメントを追加", - "reactedBy": "リアクションした人", - "addReaction": "リアクションを追加", - "reactedByMore": "他 {count} 人", - "showSeconds": { - "one": "1秒前", - "other": "{count}秒前", - "zero": "たった今", - "many": "{count}秒前" - }, - "showMinutes": { - "one": "1分前", - "other": "{count}分前", - "many": "{count}分前" - }, - "showHours": { - "one": "1時間前", - "other": "{count}時間前", - "many": "{count}時間前" - }, - "showDays": { - "one": "1日前", - "other": "{count}日前", - "many": "{count}日前" - }, - "showMonths": { - "one": "1ヶ月前", - "other": "{count}ヶ月前", - "many": "{count}ヶ月前" - }, - "showYears": { - "one": "1年前", - "other": "{count}年前", - "many": "{count}年前" - }, - "reply": "返信", - "deleteComment": "コメントを削除", - "youAreNotOwner": "このコメントの所有者ではありません", - "confirmDeleteDescription": "このコメントを削除してもよろしいですか?", - "hasBeenDeleted": "削除済み", - "replyingTo": "返信先", - "noAccessDeleteComment": "このコメントを削除する権限がありません", - "collapse": "折りたたむ", - "readMore": "続きを読む", - "failedToAddComment": "コメントの追加に失敗しました...", - "commentAddedSuccessfully": "コメントが正常に追加されました。", - "commentAddedSuccessTip": "コメントまたは返信を追加しました。最新のコメントを確認するためにトップに移動しますか?" - }, - "template": { - "asTemplate": "テンプレートとして保存", - "name": "テンプレート名", - "description": "テンプレートの説明", - "about": "テンプレートについて", - "preview": "テンプレートのプレビュー", - "categories": "テンプレートのカテゴリー", - "isNewTemplate": "新しいテンプレートとしてピン留め", - "featured": "おすすめとしてピン留め", - "relatedTemplates": "関連テンプレート", - "requiredField": "{field}は必須です", - "addCategory": "「{category}」を追加", - "addNewCategory": "新しいカテゴリーを追加", - "addNewCreator": "新しい作成者を追加", - "deleteCategory": "カテゴリーを削除", - "editCategory": "カテゴリーを編集", - "editCreator": "作成者を編集", - "category": { - "name": "カテゴリー名", - "icon": "カテゴリーアイコン", - "bgColor": "カテゴリー背景色", - "priority": "カテゴリーの優先度", - "desc": "カテゴリーの説明", - "type": "カテゴリーの種類", - "icons": "カテゴリーアイコン", - "colors": "カテゴリーの色", - "byUseCase": "ユースケース別", - "byFeature": "機能別", - "deleteCategory": "カテゴリーを削除", - "deleteCategoryDescription": "このカテゴリーを削除してもよろしいですか?", - "typeToSearch": "カテゴリーを検索..." - }, - "creator": { - "label": "テンプレート作成者", - "name": "作成者名", - "avatar": "作成者のアバター", - "accountLinks": "作成者のアカウントリンク", - "uploadAvatar": "クリックしてアバターをアップロード", - "deleteCreator": "作成者を削除", - "deleteCreatorDescription": "この作成者を削除してもよろしいですか?", - "typeToSearch": "作成者を検索..." - }, - "uploadSuccess": "テンプレートが正常にアップロードされました。", - "uploadSuccessDescription": "テンプレートが正常にアップロードされました。テンプレートギャラリーで確認できます。", - "viewTemplate": "テンプレートを表示", - "deleteTemplate": "テンプレートを削除", - "deleteSuccess": "テンプレートが正常に削除されました", - "deleteTemplateDescription": "このテンプレートを削除してもよろしいですか?", - "addRelatedTemplate": "関連テンプレートを追加", - "removeRelatedTemplate": "関連テンプレートを削除", - "uploadAvatar": "アバターをアップロード", - "searchInCategory": "{category}で検索", - "label": "テンプレート" - }, - "fileDropzone": { - "dropFile": "クリックまたはドラッグしてファイルをアップロード", - "uploading": "アップロード中...", - "uploadFailed": "アップロードに失敗しました", - "uploadSuccess": "アップロード成功", - "uploadSuccessDescription": "ファイルが正常にアップロードされました", - "uploadFailedDescription": "ファイルのアップロードに失敗しました", - "uploadingDescription": "ファイルをアップロード中" - }, - "gallery": { - "preview": "全画面表示で開く", - "copy": "コピー", - "download": "ダウンロード", - "prev": "前へ", - "next": "次へ", - "resetZoom": "ズームをリセット", - "zoomIn": "ズームイン", - "zoomOut": "ズームアウト" - }, - "invitation": { - "join": "参加する", - "on": "の上", - "invitedBy": "招待者", - "membersCount": { - "zero": "{count} 人のメンバー", - "one": "{count} 人のメンバー", - "many": "{count} 人のメンバー", - "other": "{count} 人のメンバー" - }, - "tip": "以下の連絡先情報を使用して、このワークスペースに参加するよう招待されました。これが間違っている場合は、管理者に連絡して招待を再送信してください。", - "joinWorkspace": "ワークスペースに参加", - "success": "ワークスペースへの参加が完了しました", - "successMessage": "これで、その中のすべてのページとワークスペースにアクセスできるようになります。", - "openWorkspace": "AppFlowyを開く", - "alreadyAccepted": "すでに招待を承諾しています", - "errorModal": { - "title": "問題が発生しました", - "description": "現在のアカウント {email} ではこのワークスペースにアクセスできない可能性があります。正しいアカウントでログインするか、ワークスペースの所有者に問い合わせてください。", - "contactOwner": "所有者に連絡する", - "close": "家に戻る", - "changeAccount": "アカウントを変更する" - } - }, - "requestAccess": { - "title": "このページにアクセスできません", - "subtitle": "このページの所有者にアクセスをリクエストできます。承認されると、ページを表示できるようになります。", - "requestAccess": "アクセスをリクエスト", - "backToHome": "homeに戻る", - "tip": "現在ログインしているのは<link/>。", - "mightBe": "別のアカウントでのログイン<login/>が必要になるかもしれません。", - "successful": "リクエストは正常に送信されました", - "successfulMessage": "所有者がリクエストを承認すると通知されます。", - "requestError": "アクセスをリクエストできませんでした", - "repeatRequestError": "このページへのアクセスはすでにリクエストされています" - }, - "approveAccess": { - "title": "ワークスペース参加リクエストを承認", - "requestSummary": "<user/>参加リクエスト<workspace/>アクセス<page/>", - "upgrade": "アップグレード", - "downloadApp": "AppFlowyをダウンロード", - "approveButton": "承認する", - "approveSuccess": "承認されました", - "approveError": "承認に失敗しました。ワークスペース プランの制限を超えていないことを確認してください。", - "getRequestInfoError": "リクエスト情報を取得できませんでした", - "memberCount": { - "zero": "メンバーなし", - "one": "メンバー 1 人", - "many": "{count} 人のメンバー", - "other": "{count} 人のメンバー" - }, - "alreadyProTitle": "ワークスペースプランの制限に達しました", - "alreadyProMessage": "連絡を取るよう依頼する<email/>より多くのメンバーのロックを解除する", - "repeatApproveError": "このリクエストはすでに承認されています", - "ensurePlanLimit": "ワークスペースプランの制限を超えていないことを確認してください。制限を超えた場合は、<upgrade/>ワークスペースプランまたは<download/>。", - "requestToJoin": "参加をリクエストされた", - "asMember": "メンバーとして" - }, - "upgradePlanModal": { - "title": "プロにアップグレード", - "message": "{name} は無料メンバーの上限に達しました。より多くのメンバーを招待するには、Pro プランにアップグレードしてください。", - "upgradeSteps": "AppFlowyでプランをアップグレードする方法:", - "step1": "1.設定へ移動", - "step2": "2. 「プラン」をクリック", - "step3": "3. 「プランの変更」を選択します", - "appNote": "注記: ", - "actionButton": "アップグレード", - "downloadLink": "アプリをダウンロード", - "laterButton": "後で", - "refreshNote": "アップグレードが成功したら、<refresh/>新しい機能を有効にします。", - "refresh": "ここ" - }, - "breadcrumbs": { - "label": "パンくず" - }, - "time": { - "justNow": "たった今", - "seconds": { - "one": "1秒", - "other": "{count} 秒" - }, - "minutes": { - "one": "1分", - "other": "{count} 分" - }, - "hours": { - "one": "1時間", - "other": "{count} 時間" - }, - "days": { - "one": "1日", - "other": "{count} 日" - }, - "weeks": { - "one": "1週間", - "other": "{count} 週間" - }, - "months": { - "one": "1ヶ月", - "other": "{count} ヶ月" - }, - "years": { - "one": "1年", - "other": "{count} 年" - }, - "ago": "以前", - "yesterday": "昨日", - "today": "今日" - }, - "members": { - "zero": "メンバーなし", - "one": "メンバー 1 人", - "many": "{count} 人のメンバー", - "other": "{count} 人のメンバー" - }, - "tabMenu": { - "close": "閉じる", - "closeDisabledHint": "固定されたタブを閉じることはできません。まず固定を解除してください", - "closeOthers": "他のタブを閉じる", - "closeOthersHint": "これにより、このタブを除くすべての固定されていないタブが閉じられます", - "closeOthersDisabledHint": "すべてのタブが固定されており、閉じるタブが見つかりません", - "favorite": "お気に入り", - "unfavorite": "お気に入りを解除", - "favoriteDisabledHint": "このビューをお気に入りに登録できません", - "pinTab": "ピン", - "unpinTab": "ピンを外す" - }, - "openFileMessage": { - "success": "ファイルは正常に開かれました", - "fileNotFound": "ファイルが見つかりません", - "noAppToOpenFile": "このファイルを開くアプリはありません", - "permissionDenied": "このファイルを開く権限がありません", - "unknownError": "ファイルのオープンに失敗しました" - }, - "inviteMember": { - "requestInviteMembers": "ワークスペースに招待する", - "inviteFailedMemberLimit": "メンバーの上限に達しました。 ", - "upgrade": "アップグレード", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "招待状を送る", - "inviteAlready": "このメールアドレスはすでに招待されています: {email}", - "inviteSuccess": "招待状は正常に送信されました", - "description": "以下にカンマで区切ってメールアドレスを入力してください。料金はメンバー数に基づいて決まります。", - "emails": "メール" - }, - "quickNote": { - "label": "クイックノート", - "quickNotes": "クイックノート", - "search": "クイックノートを検索", - "collapseFullView": "全画面表示を折りたたむ", - "expandFullView": "全画面表示を拡大", - "createFailed": "クイックノートの作成に失敗しました", - "quickNotesEmpty": "クイックノートなし", - "emptyNote": "空のメモ", - "deleteNotePrompt": "選択したメモは完全に削除されます。削除してもよろしいですか?", - "addNote": "新しいメモ", - "noAdditionalText": "追加テキストなし" - }, - "subscribe": { - "upgradePlanTitle": "プランを比較して選択", - "yearly": "年間", - "save": "{discount}% 節約", - "monthly": "月次", - "priceIn": "価格", - "free": "無料", - "pro": "プロ", - "freeDescription": "個人で最大2人までがすべてを管理", - "proDescription": "小規模なチームがプロジェクトとチームの知識を管理する", - "proDuration": { - "monthly": "会員あたり月額\n毎月請求", - "yearly": "メンバーあたり月額\n毎年請求" - }, - "cancel": "ダウングレード", - "changePlan": "プロプランにアップグレード", - "everythingInFree": "すべて無料+", - "currentPlan": "現在", - "freeDuration": "永遠", - "freePoints": { - "first": "最大 2 人のメンバーが参加できる 1 つの共同ワークスペース", - "second": "ページとブロック無制限", - "three": "5 GBのストレージ", - "four": "インテリジェント検索", - "five": "20件のAI回答", - "six": "モバイルアプリ", - "seven": "リアルタイムコラボレーション" - }, - "proPoints": { - "first": "無制限のストレージ", - "second": "ワークスペースメンバー最大10人", - "three": "無制限のAI応答", - "four": "無制限のファイルアップロード", - "five": "カスタム名前空間" - }, - "cancelPlan": { - "title": "去ってしまうのは残念です", - "success": "サブスクリプションは正常にキャンセルされました", - "description": "ご利用いただけなくなるのは残念です。AppFlowy の改善に役立てていただけるよう、皆様のフィードバックをお待ちしています。お時間をいただき、いくつかの質問にお答えください。", - "commonOther": "他", - "otherHint": "ここに答えを書いてください", - "questionOne": { - "question": "AppFlowy Pro サブスクリプションをキャンセルした理由は何ですか?", - "answerOne": "コストが高すぎる", - "answerTwo": "機能が期待に応えられなかった", - "answerThree": "より良い代替案を見つけた", - "answerFour": "費用に見合うほど使用しなかった", - "answerFive": "サービスの問題または技術的な問題" - }, - "questionTwo": { - "question": "将来的にAppFlowy Proへの再加入を検討する可能性はどのくらいありますか?", - "answerOne": "非常に可能性が高い", - "answerTwo": "可能性が高い", - "answerThree": "わからない", - "answerFour": "可能性が低い", - "answerFive": "非常に可能性が低い" - }, - "questionThree": { - "question": "サブスクリプション期間中に最も価値を感じた Pro 機能はどれですか?", - "answerOne": "複数ユーザーコラボレーション", - "answerTwo": "長期間のバージョン履歴", - "answerThree": "無制限のAI応答", - "answerFour": "ローカルAIモデルへのアクセス" - }, - "questionFour": { - "question": "AppFlowy の全体的な使用感と満足感をどう思いますか?", - "answerOne": "秀", - "answerTwo": "優", - "answerThree": "良", - "answerFour": "可", - "answerFive": "不可" - } - } + "deleteContentTitle": "{pageType} を削除してもよろしいですか?", + "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 1246b65f30..b0b7a472b4 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -1,1525 +1,356 @@ { "appName": "AppFlowy", - "defaultUsername": "나", - "welcomeText": "@:appName에 오신 것을 환영합니다", - "welcomeTo": "환영합니다", - "githubStarText": "GitHub에서 별표", + "defaultUsername": "Me", + "welcomeText": "@:appName 에 오신것을 환영합니다", + "githubStarText": "Star on GitHub", "subscribeNewsletterText": "뉴스레터 구독", - "letsGoButtonText": "빠른 시작", + "letsGoButtonText": "Let's Go", "title": "제목", - "youCanAlso": "또한 할 수 있습니다", + "youCanAlso": "당신은 또한 수", "and": "그리고", - "failedToOpenUrl": "URL을 열지 못했습니다: {}", "blockActions": { "addBelowTooltip": "아래에 추가하려면 클릭", "addAboveCmd": "Alt+클릭", "addAboveMacCmd": "Option+클릭", - "addAboveTooltip": "위에 추가하려면", - "dragTooltip": "이동하려면 드래그", - "openMenuTooltip": "메뉴를 열려면 클릭" + "addAboveTooltip": "위에 추가" }, "signUp": { - "buttonText": "가입하기", - "title": "@:appName에 가입하기", + "buttonText": "회원가입", + "title": "@:appName 에 회원가입", "getStartedText": "시작하기", - "emptyPasswordError": "비밀번호는 비워둘 수 없습니다", - "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", - "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", + "emptyPasswordError": "비밀번호는 공백일 수 없습니다", + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", "alreadyHaveAnAccount": "이미 계정이 있으신가요?", "emailHint": "이메일", "passwordHint": "비밀번호", - "repeatPasswordHint": "비밀번호 확인", - "signUpWith": "다음으로 가입:" + "repeatPasswordHint": "비밀번호 재입력" }, "signIn": { - "loginTitle": "@:appName에 로그인", + "loginTitle": "@:appName 에 로그인", "loginButtonText": "로그인", - "loginStartWithAnonymous": "익명 세션으로 계속", - "continueAnonymousUser": "익명 세션으로 계속", - "anonymous": "익명", "buttonText": "로그인", - "signingInText": "로그인 중...", "forgotPassword": "비밀번호를 잊으셨나요?", "emailHint": "이메일", "passwordHint": "비밀번호", "dontHaveAnAccount": "계정이 없으신가요?", - "createAccount": "계정 만들기", - "repeatPasswordEmptyError": "비밀번호 확인은 비워둘 수 없습니다", - "unmatchedPasswordError": "비밀번호 확인이 비밀번호와 일치하지 않습니다", - "syncPromptMessage": "데이터 동기화에는 시간이 걸릴 수 있습니다. 이 페이지를 닫지 마세요", - "or": "또는", - "signInWithGoogle": "Google로 계속", - "signInWithGithub": "GitHub로 계속", - "signInWithDiscord": "Discord로 계속", - "signInWithApple": "Apple로 계속", - "continueAnotherWay": "다른 방법으로 계속", - "signUpWithGoogle": "Google로 가입", - "signUpWithGithub": "GitHub로 가입", - "signUpWithDiscord": "Discord로 가입", - "signInWith": "다음으로 계속:", - "signInWithEmail": "이메일로 계속", - "signInWithMagicLink": "계속", - "signUpWithMagicLink": "Magic Link로 가입", - "pleaseInputYourEmail": "이메일 주소를 입력하세요", - "settings": "설정", - "magicLinkSent": "Magic Link가 전송되었습니다!", - "invalidEmail": "유효한 이메일 주소를 입력하세요", - "alreadyHaveAnAccount": "이미 계정이 있으신가요?", - "logIn": "로그인", - "generalError": "문제가 발생했습니다. 나중에 다시 시도하세요", - "limitRateError": "보안상의 이유로, 매 60초마다 한 번씩만 Magic Link를 요청할 수 있습니다", - "magicLinkSentDescription": "Magic Link가 이메일로 전송되었습니다. 링크를 클릭하여 로그인을 완료하세요. 링크는 5분 후에 만료됩니다." + "repeatPasswordEmptyError": "비밀번호 재입력란은 공백일 수 없습니다", + "unmatchedPasswordError": "재입력 하신 비밀번호가 같지 않습니다", + "loginAsGuestButtonText": "시작하다" }, "workspace": { - "chooseWorkspace": "작업 공간 선택", - "defaultName": "내 작업 공간", - "create": "작업 공간 생성", - "new": "새 작업 공간", - "importFromNotion": "Notion에서 가져오기", - "learnMore": "자세히 알아보기", - "reset": "작업 공간 재설정", - "renameWorkspace": "작업 공간 이름 변경", - "workspaceNameCannotBeEmpty": "작업 공간 이름은 비워둘 수 없습니다", - "resetWorkspacePrompt": "작업 공간을 재설정하면 모든 페이지와 데이터가 삭제됩니다. 작업 공간을 재설정하시겠습니까? 또는 지원 팀에 문의하여 작업 공간을 복원할 수 있습니다", - "hint": "작업 공간", - "notFoundError": "작업 공간을 찾을 수 없습니다", - "failedToLoad": "문제가 발생했습니다! 작업 공간을 로드하지 못했습니다. @:appName의 모든 열린 인스턴스를 닫고 다시 시도하세요.", - "errorActions": { - "reportIssue": "문제 보고", - "reportIssueOnGithub": "GitHub에서 문제 보고", - "exportLogFiles": "로그 파일 내보내기", - "reachOut": "Discord에서 문의" - }, - "menuTitle": "작업 공간", - "deleteWorkspaceHintText": "작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다.", - "createSuccess": "작업 공간이 성공적으로 생성되었습니다", - "createFailed": "작업 공간 생성 실패", - "createLimitExceeded": "계정에 허용된 최대 작업 공간 수에 도달했습니다. 추가 작업 공간이 필요하면 GitHub에 요청하세요", - "deleteSuccess": "작업 공간이 성공적으로 삭제되었습니다", - "deleteFailed": "작업 공간 삭제 실패", - "openSuccess": "작업 공간이 성공적으로 열렸습니다", - "openFailed": "작업 공간 열기 실패", - "renameSuccess": "작업 공간 이름이 성공적으로 변경되었습니다", - "renameFailed": "작업 공간 이름 변경 실패", - "updateIconSuccess": "작업 공간 아이콘이 성공적으로 업데이트되었습니다", - "updateIconFailed": "작업 공간 아이콘 업데이트 실패", - "cannotDeleteTheOnlyWorkspace": "유일한 작업 공간을 삭제할 수 없습니다", - "fetchWorkspacesFailed": "작업 공간을 가져오지 못했습니다", - "leaveCurrentWorkspace": "작업 공간 나가기", - "leaveCurrentWorkspacePrompt": "현재 작업 공간을 나가시겠습니까?" + "create": "워크스페이스 생성", + "hint": "워크스페이스", + "notFoundError": "워크스페이스를 찾을 수 없습니다" }, "shareAction": { "buttonText": "공유", - "workInProgress": "곧 출시 예정", - "markdown": "Markdown", - "html": "HTML", - "clipboard": "클립보드에 복사", - "csv": "CSV", - "copyLink": "링크 복사", - "publishToTheWeb": "웹에 게시", - "publishToTheWebHint": "AppFlowy로 웹사이트 만들기", - "publish": "게시", - "unPublish": "게시 취소", - "visitSite": "사이트 방문", - "exportAsTab": "다음으로 내보내기", - "publishTab": "게시", - "shareTab": "공유", - "publishOnAppFlowy": "AppFlowy에 게시", - "shareTabTitle": "협업 초대", - "shareTabDescription": "누구와도 쉽게 협업할 수 있습니다", - "copyLinkSuccess": "링크가 클립보드에 복사되었습니다", - "copyShareLink": "공유 링크 복사", - "copyLinkFailed": "링크를 클립보드에 복사하지 못했습니다", - "copyLinkToBlockSuccess": "블록 링크가 클립보드에 복사되었습니다", - "copyLinkToBlockFailed": "블록 링크를 클립보드에 복사하지 못했습니다", - "manageAllSites": "모든 사이트 관리", - "updatePathName": "경로 이름 업데이트" + "workInProgress": "Coming soon", + "markdown": "마크다운", + "copyLink": "링크 복사" }, "moreAction": { - "small": "작게", + "small": "작은", "medium": "중간", - "large": "크게", + "large": "크기가 큰", "fontSize": "글꼴 크기", - "import": "가져오기", - "moreOptions": "더 많은 옵션", - "wordCount": "단어 수: {}", - "charCount": "문자 수: {}", - "createdAt": "생성일: {}", - "deleteView": "삭제", - "duplicateView": "복제", - "wordCountLabel": "단어 수: ", - "charCountLabel": "문자 수: ", - "createdAtLabel": "생성일: ", - "syncedAtLabel": "동기화됨: ", - "saveAsNewPage": "페이지에 메시지 추가", - "saveAsNewPageDisabled": "사용 가능한 메시지가 없습니다" + "import": "수입", + "moreOptions": "추가 옵션" }, "importPanel": { - "textAndMarkdown": "텍스트 & Markdown", - "documentFromV010": "v0.1.0에서 문서 가져오기", - "databaseFromV010": "v0.1.0에서 데이터베이스 가져오기", - "notionZip": "Notion 내보낸 Zip 파일", + "textAndMarkdown": "텍스트 및 마크다운", + "documentFromV010": "v0.1.0의 문서", + "databaseFromV010": "v0.1.0의 데이터베이스", "csv": "CSV", - "database": "데이터베이스" - }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "파일을 드래그 앤 드롭하거나 클릭하여 ", - "placeholderUpload": "업로드", - "placeholderRight": "하거나 이미지 링크를 붙여넣으세요.", - "dropToUpload": "업로드할 파일을 드롭하세요", - "change": "변경" - } + "database": "데이터 베이스" }, "disclosureAction": { - "rename": "이름 변경", + "rename": "이름변경", "delete": "삭제", "duplicate": "복제", - "unfavorite": "즐겨찾기에서 제거", - "favorite": "즐겨찾기에 추가", - "openNewTab": "새 탭에서 열기", - "moveTo": "이동", - "addToFavorites": "즐겨찾기에 추가", - "copyLink": "링크 복사", - "changeIcon": "아이콘 변경", - "collapseAllPages": "모든 하위 페이지 접기", - "movePageTo": "페이지 이동", - "move": "이동", - "lockPage": "페이지 잠금" + "openNewTab": "새 탭에서 열기" }, "blankPageTitle": "빈 페이지", - "newPageText": "새 페이지", - "newDocumentText": "새 문서", - "newGridText": "새 그리드", - "newCalendarText": "새 캘린더", - "newBoardText": "새 보드", - "chat": { - "newChat": "AI 채팅", - "inputMessageHint": "@:appName AI에게 물어보세요", - "inputLocalAIMessageHint": "@:appName 로컬 AI에게 물어보세요", - "unsupportedCloudPrompt": "이 기능은 @:appName Cloud를 사용할 때만 사용할 수 있습니다", - "relatedQuestion": "추천 질문", - "serverUnavailable": "연결이 끊어졌습니다. 인터넷을 확인하고", - "aiServerUnavailable": "AI 서비스가 일시적으로 사용할 수 없습니다. 나중에 다시 시도하세요.", - "retry": "다시 시도", - "clickToRetry": "다시 시도하려면 클릭", - "regenerateAnswer": "다시 생성", - "question1": "Kanban을 사용하여 작업 관리하는 방법", - "question2": "GTD 방법 설명", - "question3": "Rust를 사용하는 이유", - "question4": "내 주방에 있는 재료로 요리법 만들기", - "question5": "내 페이지에 대한 일러스트레이션 만들기", - "question6": "다가오는 주의 할 일 목록 작성", - "aiMistakePrompt": "AI는 실수를 할 수 있습니다. 중요한 정보를 확인하세요.", - "chatWithFilePrompt": "파일과 채팅하시겠습니까?", - "indexFileSuccess": "파일 색인화 성공", - "inputActionNoPages": "페이지 결과 없음", - "referenceSource": { - "zero": "0개의 출처 발견", - "one": "{count}개의 출처 발견", - "other": "{count}개의 출처 발견" - }, - "clickToMention": "페이지 언급", - "uploadFile": "PDF, 텍스트 또는 마크다운 파일 첨부", - "questionDetail": "안녕하세요 {}! 오늘 어떻게 도와드릴까요?", - "indexingFile": "{} 색인화 중", - "generatingResponse": "응답 생성 중", - "selectSources": "출처 선택", - "currentPage": "현재 페이지", - "sourcesLimitReached": "최대 3개의 최상위 문서와 그 하위 문서만 선택할 수 있습니다", - "sourceUnsupported": "현재 데이터베이스와의 채팅을 지원하지 않습니다", - "regenerate": "다시 시도", - "addToPageButton": "페이지에 메시지 추가", - "addToPageTitle": "메시지 추가...", - "addToNewPage": "새 페이지 만들기", - "addToNewPageName": "\"{}\"에서 추출한 메시지", - "addToNewPageSuccessToast": "메시지가 추가되었습니다", - "openPagePreviewFailedToast": "페이지를 열지 못했습니다", - "changeFormat": { - "actionButton": "형식 변경", - "confirmButton": "이 형식으로 다시 생성", - "textOnly": "텍스트", - "imageOnly": "이미지 전용", - "textAndImage": "텍스트와 이미지", - "text": "단락", - "bullet": "글머리 기호 목록", - "number": "번호 매기기 목록", - "table": "표", - "blankDescription": "응답 형식", - "defaultDescription": "자동 모드", - "textWithImageDescription": "@:chat.changeFormat.text와 이미지", - "numberWithImageDescription": "@:chat.changeFormat.number와 이미지", - "bulletWithImageDescription": "@:chat.changeFormat.bullet와 이미지", - "tableWithImageDescription": "@:chat.changeFormat.table와 이미지" - }, - "selectBanner": { - "saveButton": "추가 ...", - "selectMessages": "메시지 선택", - "nSelected": "{}개 선택됨", - "allSelected": "모두 선택됨" - }, - "stopTooltip": "생성 중지" - }, + "newPageText": "새로운 페이지", "trash": { "text": "휴지통", - "restoreAll": "모두 복원", - "restore": "복원", + "restoreAll": "모두 복구", "deleteAll": "모두 삭제", "pageHeader": { "fileName": "파일 이름", - "lastModified": "마지막 수정", - "created": "생성됨" + "lastModified": "수정날짜", + "created": "생성날짜" }, "confirmDeleteAll": { - "title": "휴지통의 모든 페이지", - "caption": "휴지통의 모든 항목을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "title": "휴지통의 모든 페이지를 삭제하시겠습니까?", + "caption": "이 작업은 취소할 수 없습니다." }, "confirmRestoreAll": { - "title": "휴지통의 모든 페이지 복원", - "caption": "이 작업은 되돌릴 수 없습니다." - }, - "restorePage": { - "title": "복원: {}", - "caption": "이 페이지를 복원하시겠습니까?" - }, - "mobile": { - "actions": "휴지통 작업", - "empty": "휴지통에 페이지나 공간이 없습니다", - "emptyDescription": "필요 없는 항목을 휴지통으로 이동하세요.", - "isDeleted": "삭제됨", - "isRestored": "복원됨" - }, - "confirmDeleteTitle": "이 페이지를 영구적으로 삭제하시겠습니까?" + "title": "휴지통의 모든 페이지를 복원하시겠습니까?", + "caption": "이 작업은 취소할 수 없습니다." + } }, "deletePagePrompt": { - "text": "이 페이지는 휴지통에 있습니다", - "restore": "페이지 복원", - "deletePermanent": "영구적으로 삭제", - "deletePermanentDescription": "이 페이지를 영구적으로 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." + "text": "현재 페이지는 휴지통에 있습니다", + "restore": "페이지 복구", + "deletePermanent": "영구 삭제" }, "dialogCreatePageNameHint": "페이지 이름", "questionBubble": { - "shortcuts": "단축키", - "whatsNew": "새로운 기능", - "markdown": "Markdown", + "shortcuts": "바로 가기", + "whatsNew": "새로운 소식", + "help": "도움 및 지원", + "markdown": "가격 인하", "debug": { "name": "디버그 정보", - "success": "디버그 정보를 클립보드에 복사했습니다!", - "fail": "디버그 정보를 클립보드에 복사할 수 없습니다" + "success": "디버그 정보를 클립보드로 복사했습니다.", + "fail": "디버그 정보를 클립보드로 복사할 수 없습니다." }, - "feedback": "피드백", - "help": "도움말 및 지원" + "feedback": "피드백" }, "menuAppHeader": { - "moreButtonToolTip": "제거, 이름 변경 등...", - "addPageTooltip": "빠르게 페이지 추가", - "defaultNewPageName": "제목 없음", - "renameDialog": "이름 변경", - "pageNameSuffix": "복사본" + "addPageTooltip": "하위에 페이지 추가", + "defaultNewPageName": "제목없음", + "renameDialog": "이름변경" }, - "noPagesInside": "내부에 페이지가 없습니다", "toolbar": { - "undo": "실행 취소", - "redo": "다시 실행", + "undo": "실행취소", + "redo": "재실행", "bold": "굵게", "italic": "기울임꼴", "underline": "밑줄", "strike": "취소선", "numList": "번호 매기기 목록", "bulletList": "글머리 기호 목록", - "checkList": "체크리스트", + "checkList": "작업 목록", "inlineCode": "인라인 코드", - "quote": "인용 블록", + "quote": "인용구 블록", "header": "헤더", - "highlight": "강조", + "highlight": "하이라이트", "color": "색상", - "addLink": "링크 추가" + "addLink": "링크 추가", + "link": "링크" }, "tooltip": { - "lightMode": "라이트 모드로 전환", - "darkMode": "다크 모드로 전환", + "lightMode": "라이트 모드로 변경", + "darkMode": "다크 모드로 변경", "openAsPage": "페이지로 열기", - "addNewRow": "새 행 추가", - "openMenu": "메뉴 열기", - "dragRow": "행 순서 변경", + "addNewRow": "열 추가", + "openMenu": "메뉴를 여시려면 클릭하세요", + "dragRow": "행을 재정렬하려면 길게 누르세요.", "viewDataBase": "데이터베이스 보기", - "referencePage": "이 {name}이 참조됨", - "addBlockBelow": "아래에 블록 추가", - "aiGenerate": "생성" + "referencePage": "이 {name}은(는) 참조됩니다", + "addBlockBelow": "아래에 블록 추가" }, "sideBar": { "closeSidebar": "사이드바 닫기", - "openSidebar": "사이드바 열기", - "expandSidebar": "전체 페이지로 확장", - "personal": "개인", - "private": "비공개", - "workspace": "작업 공간", - "favorites": "즐겨찾기", - "clickToHidePrivate": "비공개 공간 숨기기\n여기에서 만든 페이지는 본인만 볼 수 있습니다", - "clickToHideWorkspace": "작업 공간 숨기기\n여기에서 만든 페이지는 모든 멤버가 볼 수 있습니다", - "clickToHidePersonal": "개인 공간 숨기기", - "clickToHideFavorites": "즐겨찾기 공간 숨기기", - "addAPage": "새 페이지 추가", - "addAPageToPrivate": "비공개 공간에 페이지 추가", - "addAPageToWorkspace": "작업 공간에 페이지 추가", - "recent": "최근", - "today": "오늘", - "thisWeek": "이번 주", - "others": "이전 즐겨찾기", - "earlier": "이전", - "justNow": "방금", - "minutesAgo": "{count}분 전", - "lastViewed": "마지막으로 본", - "favoriteAt": "즐겨찾기한", - "emptyRecent": "최근 페이지 없음", - "emptyRecentDescription": "페이지를 보면 여기에서 쉽게 찾을 수 있습니다.", - "emptyFavorite": "즐겨찾기 페이지 없음", - "emptyFavoriteDescription": "페이지를 즐겨찾기에 추가하면 여기에서 빠르게 접근할 수 있습니다!", - "removePageFromRecent": "최근 항목에서 이 페이지를 제거하시겠습니까?", - "removeSuccess": "성공적으로 제거되었습니다", - "favoriteSpace": "즐겨찾기", - "RecentSpace": "최근", - "Spaces": "공간", - "upgradeToPro": "Pro로 업그레이드", - "upgradeToAIMax": "무제한 AI 잠금 해제", - "storageLimitDialogTitle": "무료 저장 공간이 부족합니다. 무제한 저장 공간을 잠금 해제하려면 업그레이드하세요", - "storageLimitDialogTitleIOS": "무료 저장 공간이 부족합니다.", - "aiResponseLimitTitle": "무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "aiResponseLimitDialogTitle": "AI 응답 한도에 도달했습니다", - "aiResponseLimit": "무료 AI 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max 또는 Pro 플랜을 클릭하여 더 많은 AI 응답을 받으세요", - "askOwnerToUpgradeToPro": "작업 공간의 무료 저장 공간이 부족합니다. 작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요", - "askOwnerToUpgradeToProIOS": "작업 공간의 무료 저장 공간이 부족합니다.", - "askOwnerToUpgradeToAIMax": "작업 공간의 무료 AI 응답이 부족합니다. 작업 공간 소유자에게 플랜을 업그레이드하거나 AI 애드온을 구매하도록 요청하세요", - "askOwnerToUpgradeToAIMaxIOS": "작업 공간의 무료 AI 응답이 부족합니다.", - "purchaseAIMax": "작업 공간의 AI 이미지 응답이 부족합니다. 작업 공간 소유자에게 AI Max를 구매하도록 요청하세요", - "aiImageResponseLimit": "AI 이미지 응답이 부족합니다.\n\n설정 -> 플랜 -> AI Max를 클릭하여 더 많은 AI 이미지 응답을 받으세요", - "purchaseStorageSpace": "저장 공간 구매", - "singleFileProPlanLimitationDescription": "무료 플랜에서 허용되는 최대 파일 업로드 크기를 초과했습니다. 더 큰 파일을 업로드하려면 Pro 플랜으로 업그레이드하세요", - "purchaseAIResponse": "구매 ", - "askOwnerToUpgradeToLocalAI": "작업 공간 소유자에게 AI On-device를 활성화하도록 요청하세요", - "upgradeToAILocal": "최고의 프라이버시를 위해 로컬 모델을 장치에서 실행", - "upgradeToAILocalDesc": "PDF와 채팅하고, 글쓰기를 개선하고, 로컬 AI를 사용하여 테이블을 자동으로 채우세요" + "openSidebar": "사이드바 열기" }, "notifications": { "export": { - "markdown": "노트를 Markdown으로 내보냈습니다", + "markdown": "마크다운으로 노트를 내보냄", "path": "Documents/flowy" } }, "contactsPage": { "title": "연락처", - "whatsHappening": "이번 주에 무슨 일이 있나요?", + "whatsHappening": "이번주에는 무슨 일이 있나요?", "addContact": "연락처 추가", - "editContact": "연락처 수정" + "editContact": "연락처 편집" }, "button": { "ok": "확인", - "confirm": "확인", "done": "완료", "cancel": "취소", "signIn": "로그인", "signOut": "로그아웃", "complete": "완료", "save": "저장", - "generate": "생성", + "generate": "생성하다", "esc": "ESC", - "keep": "유지", - "tryAgain": "다시 시도", - "discard": "버리기", - "replace": "교체", + "keep": "유지하다", + "tryAgain": "다시 시도하십시오", + "discard": "버리다", + "replace": "바꾸다", "insertBelow": "아래에 삽입", - "insertAbove": "위에 삽입", "upload": "업로드", - "edit": "편집", + "edit": "편집하다", "delete": "삭제", - "copy": "복사", - "duplicate": "복제", - "putback": "되돌리기", - "update": "업데이트", - "share": "공유", - "removeFromFavorites": "즐겨찾기에서 제거", - "removeFromRecent": "최근 항목에서 제거", - "addToFavorites": "즐겨찾기에 추가", - "favoriteSuccessfully": "즐겨찾기에 성공적으로 추가되었습니다", - "unfavoriteSuccessfully": "즐겨찾기에서 성공적으로 제거되었습니다", - "duplicateSuccessfully": "성공적으로 복제되었습니다", - "rename": "이름 변경", - "helpCenter": "도움말 센터", - "add": "추가", - "yes": "예", - "no": "아니요", - "clear": "지우기", - "remove": "제거", - "dontRemove": "제거하지 않음", - "copyLink": "링크 복사", - "align": "정렬", - "login": "로그인", - "logout": "로그아웃", - "deleteAccount": "계정 삭제", - "back": "뒤로", - "signInGoogle": "Google로 계속", - "signInGithub": "GitHub로 계속", - "signInDiscord": "Discord로 계속", - "more": "더 보기", - "create": "생성", - "close": "닫기", - "next": "다음", - "previous": "이전", - "submit": "제출", - "download": "다운로드", - "backToHome": "홈으로 돌아가기", - "viewing": "보기", - "editing": "편집 중", - "gotIt": "알겠습니다", - "retry": "다시 시도", - "uploadFailed": "업로드 실패.", - "copyLinkOriginal": "원본 링크 복사" + "duplicate": "복제하다", + "putback": "다시 집어 넣어" }, "label": { "welcome": "환영합니다!", "firstName": "이름", "middleName": "중간 이름", "lastName": "성", - "stepX": "단계 {X}" + "stepX": "{X} 단계" }, "oAuth": { "err": { - "failedTitle": "계정에 연결할 수 없습니다.", - "failedMsg": "브라우저에서 로그인 프로세스를 완료했는지 확인하세요." + "failedTitle": "계정에 연결을 할 수 없습니다.", + "failedMsg": "브라우저에서 회원가입이 완료되었는지 확인해주세요." }, "google": { - "title": "GOOGLE 로그인", - "instruction1": "Google 연락처를 가져오려면 웹 브라우저를 사용하여 이 애플리케이션을 인증해야 합니다.", - "instruction2": "아이콘을 클릭하거나 텍스트를 선택하여 이 코드를 클립보드에 복사하세요:", - "instruction3": "웹 브라우저에서 다음 링크로 이동하고 위의 코드를 입력하세요:", - "instruction4": "가입을 완료했으면 아래 버튼을 누르세요:" + "title": "GOOGLE SIGN-IN", + "instruction1": "구글 연락처를 가져오기 위해서 웹브라우저로 앱을 승인 해야 합니다.", + "instruction2": "아이콘을 클릭 또는 텍스트를 선택해서 이 코드를 클립보드로 복사하세요:", + "instruction3": "웹브라우저로 다음 링크로 가셔서 위 코드를 입력해주세요:", + "instruction4": "가입 완료 후 아래 버튼을 눌러주세요:" } }, "settings": { "title": "설정", - "popupMenuItem": { - "settings": "설정", - "members": "멤버", - "trash": "휴지통", - "helpAndSupport": "도움말 및 지원" - }, - "sites": { - "title": "사이트", - "namespaceTitle": "네임스페이스", - "namespaceDescription": "네임스페이스 및 홈페이지 관리", - "namespaceHeader": "네임스페이스", - "homepageHeader": "홈페이지", - "updateNamespace": "네임스페이스 업데이트", - "removeHomepage": "홈페이지 제거", - "selectHomePage": "페이지 선택", - "clearHomePage": "이 네임스페이스의 홈페이지를 지웁니다", - "customUrl": "맞춤 URL", - "namespace": { - "description": "이 변경 사항은 이 네임스페이스에 라이브로 게시된 모든 페이지에 적용됩니다", - "tooltip": "부적절한 네임스페이스를 제거할 권리를 보유합니다", - "updateExistingNamespace": "기존 네임스페이스 업데이트", - "upgradeToPro": "Pro 플랜으로 업그레이드하여 홈페이지 설정", - "redirectToPayment": "결제 페이지로 리디렉션 중...", - "onlyWorkspaceOwnerCanSetHomePage": "작업 공간 소유자만 홈페이지를 설정할 수 있습니다", - "pleaseAskOwnerToSetHomePage": "작업 공간 소유자에게 Pro 플랜으로 업그레이드하도록 요청하세요" - }, - "publishedPage": { - "title": "모든 게시된 페이지", - "description": "게시된 페이지 관리", - "page": "페이지", - "pathName": "경로 이름", - "date": "게시 날짜", - "emptyHinText": "이 작업 공간에 게시된 페이지가 없습니다", - "noPublishedPages": "게시된 페이지 없음", - "settings": "게시 설정", - "clickToOpenPageInApp": "앱에서 페이지 열기", - "clickToOpenPageInBrowser": "브라우저에서 페이지 열기" - }, - "error": { - "failedToGeneratePaymentLink": "Pro 플랜 결제 링크 생성 실패", - "failedToUpdateNamespace": "네임스페이스 업데이트 실패", - "proPlanLimitation": "네임스페이스를 업데이트하려면 Pro 플랜으로 업그레이드해야 합니다", - "namespaceAlreadyInUse": "네임스페이스가 이미 사용 중입니다. 다른 네임스페이스를 시도하세요", - "invalidNamespace": "잘못된 네임스페이스입니다. 다른 네임스페이스를 시도하세요", - "namespaceLengthAtLeast2Characters": "네임스페이스는 최소 2자 이상이어야 합니다", - "onlyWorkspaceOwnerCanUpdateNamespace": "작업 공간 소유자만 네임스페이스를 업데이트할 수 있습니다", - "onlyWorkspaceOwnerCanRemoveHomepage": "작업 공간 소유자만 홈페이지를 제거할 수 있습니다", - "setHomepageFailed": "홈페이지 설정 실패", - "namespaceTooLong": "네임스페이스가 너무 깁니다. 다른 네임스페이스를 시도하세요", - "namespaceTooShort": "네임스페이스가 너무 짧습니다. 다른 네임스페이스를 시도하세요", - "namespaceIsReserved": "네임스페이스가 예약되어 있습니다. 다른 네임스페이스를 시도하세요", - "updatePathNameFailed": "경로 이름 업데이트 실패", - "removeHomePageFailed": "홈페이지 제거 실패", - "publishNameContainsInvalidCharacters": "경로 이름에 잘못된 문자가 포함되어 있습니다. 다른 이름을 시도하세요", - "publishNameTooShort": "경로 이름이 너무 짧습니다. 다른 이름을 시도하세요", - "publishNameTooLong": "경로 이름이 너무 깁니다. 다른 이름을 시도하세요", - "publishNameAlreadyInUse": "경로 이름이 이미 사용 중입니다. 다른 이름을 시도하세요", - "namespaceContainsInvalidCharacters": "네임스페이스에 잘못된 문자가 포함되어 있습니다. 다른 네임스페이스를 시도하세요", - "publishPermissionDenied": "작업 공간 소유자 또는 페이지 게시자만 게시 설정을 관리할 수 있습니다", - "publishNameCannotBeEmpty": "경로 이름은 비워둘 수 없습니다. 다른 이름을 시도하세요" - }, - "success": { - "namespaceUpdated": "네임스페이스가 성공적으로 업데이트되었습니다", - "setHomepageSuccess": "홈페이지가 성공적으로 설정되었습니다", - "updatePathNameSuccess": "경로 이름이 성공적으로 업데이트되었습니다", - "removeHomePageSuccess": "홈페이지가 성공적으로 제거되었습니다" - } - }, - "accountPage": { - "menuLabel": "계정 및 앱", - "title": "내 계정", - "general": { - "title": "계정 이름 및 프로필 이미지", - "changeProfilePicture": "프로필 사진 변경" - }, - "email": { - "title": "이메일", - "actions": { - "change": "이메일 변경" - } - }, - "login": { - "title": "계정 로그인", - "loginLabel": "로그인", - "logoutLabel": "로그아웃" - }, - "isUpToDate": "@:appName이 최신 상태입니다!", - "officialVersion": "버전 {version} (공식 빌드)" - }, - "workspacePage": { - "menuLabel": "작업 공간", - "title": "작업 공간", - "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜/시간 형식 및 언어를 사용자 정의합니다.", - "workspaceName": { - "title": "작업 공간 이름" - }, - "workspaceIcon": { - "title": "작업 공간 아이콘", - "description": "작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다." - }, - "appearance": { - "title": "외관", - "description": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.", - "options": { - "system": "자동", - "light": "라이트", - "dark": "다크" - } - }, - "resetCursorColor": { - "title": "문서 커서 색상 재설정", - "description": "커서 색상을 재설정하시겠습니까?" - }, - "resetSelectionColor": { - "title": "문서 선택 색상 재설정", - "description": "선택 색상을 재설정하시겠습니까?" - }, - "resetWidth": { - "resetSuccess": "문서 너비가 성공적으로 재설정되었습니다" - }, - "theme": { - "title": "테마", - "description": "미리 설정된 테마를 선택하거나 사용자 정의 테마를 업로드하세요.", - "uploadCustomThemeTooltip": "사용자 정의 테마 업로드" - }, - "workspaceFont": { - "title": "작업 공간 글꼴", - "noFontHint": "글꼴을 찾을 수 없습니다. 다른 용어를 시도하세요." - }, - "textDirection": { - "title": "텍스트 방향", - "leftToRight": "왼쪽에서 오른쪽으로", - "rightToLeft": "오른쪽에서 왼쪽으로", - "auto": "자동", - "enableRTLItems": "RTL 도구 모음 항목 활성화" - }, - "layoutDirection": { - "title": "레이아웃 방향", - "leftToRight": "왼쪽에서 오른쪽으로", - "rightToLeft": "오른쪽에서 왼쪽으로" - }, - "dateTime": { - "title": "날짜 및 시간", - "example": "{}에 {} ({})", - "24HourTime": "24시간 형식", - "dateFormat": { - "label": "날짜 형식", - "local": "로컬", - "us": "미국", - "iso": "ISO", - "friendly": "친숙한", - "dmy": "일/월/년" - } - }, - "language": { - "title": "언어" - }, - "deleteWorkspacePrompt": { - "title": "작업 공간 삭제", - "content": "이 작업 공간을 삭제하시겠습니까? 이 작업은 되돌릴 수 없으며, 게시한 모든 페이지가 게시 취소됩니다." - }, - "leaveWorkspacePrompt": { - "title": "작업 공간 나가기", - "content": "이 작업 공간을 나가시겠습니까? 모든 페이지와 데이터에 대한 액세스를 잃게 됩니다.", - "success": "작업 공간을 성공적으로 나갔습니다.", - "fail": "작업 공간 나가기 실패." - }, - "manageWorkspace": { - "title": "작업 공간 관리", - "leaveWorkspace": "작업 공간 나가기", - "deleteWorkspace": "작업 공간 삭제" - } - }, - "manageDataPage": { - "menuLabel": "데이터 관리", - "title": "데이터 관리", - "description": "로컬 저장소 데이터를 관리하거나 기존 데이터를 @:appName에 가져옵니다.", - "dataStorage": { - "title": "파일 저장 위치", - "tooltip": "파일이 저장되는 위치", - "actions": { - "change": "경로 변경", - "open": "폴더 열기", - "openTooltip": "현재 데이터 폴더 위치 열기", - "copy": "경로 복사", - "copiedHint": "경로가 복사되었습니다!", - "resetTooltip": "기본 위치로 재설정" - }, - "resetDialog": { - "title": "확실합니까?", - "description": "경로를 기본 데이터 위치로 재설정해도 데이터가 삭제되지 않습니다. 현재 데이터를 다시 가져오려면 현재 위치의 경로를 먼저 복사해야 합니다." - } - }, - "importData": { - "title": "데이터 가져오기", - "tooltip": "@:appName 백업/데이터 폴더에서 데이터 가져오기", - "description": "외부 @:appName 데이터 폴더에서 데이터 복사", - "action": "파일 찾아보기" - }, - "encryption": { - "title": "암호화", - "tooltip": "데이터 저장 및 암호화 방법 관리", - "descriptionNoEncryption": "암호화를 켜면 모든 데이터가 암호화됩니다. 이 작업은 되돌릴 수 없습니다.", - "descriptionEncrypted": "데이터가 암호화되었습니다.", - "action": "데이터 암호화", - "dialog": { - "title": "모든 데이터를 암호화하시겠습니까?", - "description": "모든 데이터를 암호화하면 데이터가 안전하고 보호됩니다. 이 작업은 되돌릴 수 없습니다. 계속하시겠습니까?" - } - }, - "cache": { - "title": "캐시 지우기", - "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", - "dialog": { - "title": "캐시 지우기", - "description": "이미지가 로드되지 않거나 공간에 페이지가 누락되거나 글꼴이 로드되지 않는 등의 문제를 해결하는 데 도움이 됩니다. 데이터에는 영향을 미치지 않습니다.", - "successHint": "캐시가 지워졌습니다!" - } - }, - "data": { - "fixYourData": "데이터 수정", - "fixButton": "수정", - "fixYourDataDescription": "데이터에 문제가 있는 경우 여기에서 수정할 수 있습니다." - } - }, - "shortcutsPage": { - "menuLabel": "단축키", - "title": "단축키", - "editBindingHint": "새 바인딩 입력", - "searchHint": "검색", - "actions": { - "resetDefault": "기본값으로 재설정" - }, - "errorPage": { - "message": "단축키를 로드하지 못했습니다: {}", - "howToFix": "다시 시도해 보세요. 문제가 계속되면 GitHub에 문의하세요." - }, - "resetDialog": { - "title": "단축키 재설정", - "description": "모든 키 바인딩을 기본값으로 재설정합니다. 나중에 되돌릴 수 없습니다. 계속하시겠습니까?", - "buttonLabel": "재설정" - }, - "conflictDialog": { - "title": "{}가 이미 사용 중입니다", - "descriptionPrefix": "이 키 바인딩은 현재 ", - "descriptionSuffix": "에서 사용 중입니다. 이 키 바인딩을 교체하면 {}에서 제거됩니다.", - "confirmLabel": "계속" - }, - "editTooltip": "키 바인딩 편집을 시작하려면 누르세요", - "keybindings": { - "toggleToDoList": "할 일 목록 전환", - "insertNewParagraphInCodeblock": "새 단락 삽입", - "pasteInCodeblock": "코드 블록에 붙여넣기", - "selectAllCodeblock": "모두 선택", - "indentLineCodeblock": "줄 시작에 두 칸 삽입", - "outdentLineCodeblock": "줄 시작에서 두 칸 삭제", - "twoSpacesCursorCodeblock": "커서 위치에 두 칸 삽입", - "copy": "선택 항목 복사", - "paste": "내용 붙여넣기", - "cut": "선택 항목 잘라내기", - "alignLeft": "텍스트 왼쪽 정렬", - "alignCenter": "텍스트 가운데 정렬", - "alignRight": "텍스트 오른쪽 정렬", - "insertInlineMathEquation": "인라인 수학 방정식 삽입", - "undo": "실행 취소", - "redo": "다시 실행", - "convertToParagraph": "블록을 단락으로 변환", - "backspace": "삭제", - "deleteLeftWord": "왼쪽 단어 삭제", - "deleteLeftSentence": "왼쪽 문장 삭제", - "delete": "오른쪽 문자 삭제", - "deleteMacOS": "왼쪽 문자 삭제", - "deleteRightWord": "오른쪽 단어 삭제", - "moveCursorLeft": "커서를 왼쪽으로 이동", - "moveCursorBeginning": "커서를 시작으로 이동", - "moveCursorLeftWord": "커서를 왼쪽 단어로 이동", - "moveCursorLeftSelect": "선택하고 커서를 왼쪽으로 이동", - "moveCursorBeginSelect": "선택하고 커서를 시작으로 이동", - "moveCursorLeftWordSelect": "선택하고 커서를 왼쪽 단어로 이동", - "moveCursorRight": "커서를 오른쪽으로 이동", - "moveCursorEnd": "커서를 끝으로 이동", - "moveCursorRightWord": "커서를 오른쪽 단어로 이동", - "moveCursorRightSelect": "선택하고 커서를 오른쪽으로 이동", - "moveCursorEndSelect": "선택하고 커서를 끝으로 이동", - "moveCursorRightWordSelect": "선택하고 커서를 오른쪽 단어로 이동", - "moveCursorUp": "커서를 위로 이동", - "moveCursorTopSelect": "선택하고 커서를 위로 이동", - "moveCursorTop": "커서를 위로 이동", - "moveCursorUpSelect": "선택하고 커서를 위로 이동", - "moveCursorBottomSelect": "선택하고 커서를 아래로 이동", - "moveCursorBottom": "커서를 아래로 이동", - "moveCursorDown": "커서를 아래로 이동", - "moveCursorDownSelect": "선택하고 커서를 아래로 이동", - "home": "맨 위로 스크롤", - "end": "맨 아래로 스크롤", - "toggleBold": "굵게 전환", - "toggleItalic": "기울임꼴 전환", - "toggleUnderline": "밑줄 전환", - "toggleStrikethrough": "취소선 전환", - "toggleCode": "인라인 코드 전환", - "toggleHighlight": "강조 전환", - "showLinkMenu": "링크 메뉴 표시", - "openInlineLink": "인라인 링크 열기", - "openLinks": "선택한 모든 링크 열기", - "indent": "들여쓰기", - "outdent": "내어쓰기", - "exit": "편집 종료", - "pageUp": "한 페이지 위로 스크롤", - "pageDown": "한 페이지 아래로 스크롤", - "selectAll": "모두 선택", - "pasteWithoutFormatting": "서식 없이 붙여넣기", - "showEmojiPicker": "이모지 선택기 표시", - "enterInTableCell": "테이블에 줄 바꿈 추가", - "leftInTableCell": "테이블에서 왼쪽 셀로 이동", - "rightInTableCell": "테이블에서 오른쪽 셀로 이동", - "upInTableCell": "테이블에서 위쪽 셀로 이동", - "downInTableCell": "테이블에서 아래쪽 셀로 이동", - "tabInTableCell": "테이블에서 다음 사용 가능한 셀로 이동", - "shiftTabInTableCell": "테이블에서 이전 사용 가능한 셀로 이동", - "backSpaceInTableCell": "셀의 시작 부분에서 멈춤" - }, - "commands": { - "codeBlockNewParagraph": "코드 블록 옆에 새 단락 삽입", - "codeBlockIndentLines": "코드 블록에서 줄 시작에 두 칸 삽입", - "codeBlockOutdentLines": "코드 블록에서 줄 시작에서 두 칸 삭제", - "codeBlockAddTwoSpaces": "코드 블록에서 커서 위치에 두 칸 삽입", - "codeBlockSelectAll": "코드 블록 내의 모든 내용 선택", - "codeBlockPasteText": "코드 블록에 텍스트 붙여넣기", - "textAlignLeft": "텍스트를 왼쪽으로 정렬", - "textAlignCenter": "텍스트를 가운데로 정렬", - "textAlignRight": "텍스트를 오른쪽으로 정렬" - }, - "couldNotLoadErrorMsg": "단축키를 로드할 수 없습니다. 다시 시도하세요", - "couldNotSaveErrorMsg": "단축키를 저장할 수 없습니다. 다시 시도하세요" - }, - "aiPage": { - "title": "AI 설정", - "menuLabel": "AI 설정", - "keys": { - "enableAISearchTitle": "AI 검색", - "aiSettingsDescription": "선호하는 모델을 선택하여 AppFlowy AI를 구동하세요. 이제 GPT 4-o, Claude 3,5, Llama 3.1 및 Mistral 7B를 포함합니다", - "loginToEnableAIFeature": "AI 기능은 @:appName Cloud에 로그인한 후에만 활성화됩니다. @:appName 계정이 없는 경우 '내 계정'에서 가입하세요", - "llmModel": "언어 모델", - "llmModelType": "언어 모델 유형", - "downloadLLMPrompt": "{} 다운로드", - "downloadAppFlowyOfflineAI": "AI 오프라인 패키지를 다운로드하면 AI가 장치에서 실행됩니다. 계속하시겠습니까?", - "downloadLLMPromptDetail": "{} 로컬 모델을 다운로드하면 최대 {}의 저장 공간이 필요합니다. 계속하시겠습니까?", - "downloadBigFilePrompt": "다운로드 완료까지 약 10분이 소요될 수 있습니다", - "downloadAIModelButton": "다운로드", - "downloadingModel": "다운로드 중", - "localAILoaded": "로컬 AI 모델이 성공적으로 추가되어 사용할 준비가 되었습니다", - "localAIStart": "로컬 AI가 시작 중입니다. 느리다면 껐다가 다시 켜보세요", - "localAILoading": "로컬 AI 채팅 모델이 로드 중입니다...", - "localAIStopped": "로컬 AI가 중지되었습니다", - "localAIRunning": "로컬 AI가 실행 중입니다", - "localAIInitializing": "로컬 AI가 로드 중이며 장치에 따라 몇 분이 소요될 수 있습니다", - "localAINotReadyTextFieldPrompt": "로컬 AI가 로드되는 동안 편집할 수 없습니다", - "failToLoadLocalAI": "로컬 AI를 시작하지 못했습니다", - "restartLocalAI": "로컬 AI 다시 시작", - "disableLocalAITitle": "로컬 AI 비활성화", - "disableLocalAIDescription": "로컬 AI를 비활성화하시겠습니까?", - "localAIToggleTitle": "로컬 AI를 활성화 또는 비활성화하려면 전환", - "offlineAIInstruction1": "다음을 따르세요", - "offlineAIInstruction2": "지침", - "offlineAIInstruction3": "오프라인 AI를 활성화하려면", - "offlineAIDownload1": "AppFlowy AI를 다운로드하지 않은 경우 먼저", - "offlineAIDownload2": "다운로드", - "offlineAIDownload3": "하세요", - "activeOfflineAI": "활성화됨", - "downloadOfflineAI": "다운로드", - "openModelDirectory": "폴더 열기", - "pleaseFollowThese": "지침", - "instructions": "이 지침을 따르세요", - "installOllamaLai": "Ollama 및 AppFlowy 로컬 AI를 설정합니다. 이미 설정한 경우 건너뛰세요", - "startLocalAI": "로컬 AI를 시작하는 데 몇 초가 소요될 수 있습니다" - } - }, - "planPage": { - "menuLabel": "플랜", - "title": "가격 플랜", - "planUsage": { - "title": "플랜 사용 요약", - "storageLabel": "저장 공간", - "storageUsage": "{} / {} GB", - "unlimitedStorageLabel": "무제한 저장 공간", - "collaboratorsLabel": "멤버", - "collaboratorsUsage": "{} / {}", - "aiResponseLabel": "AI 응답", - "aiResponseUsage": "{} / {}", - "unlimitedAILabel": "무제한 응답", - "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "Mac용 AI On-device", - "memberProToggle": "더 많은 멤버 및 무제한 AI", - "aiMaxToggle": "무제한 AI 및 고급 모델 액세스", - "aiOnDeviceToggle": "최고의 프라이버시를 위한 로컬 AI", - "aiCredit": { - "title": "@:appName AI 크레딧 추가", - "price": "{}", - "priceDescription": "1,000 크레딧당", - "purchase": "AI 구매", - "info": "작업 공간당 1,000개의 AI 크레딧을 추가하여 더 스마트하고 빠른 결과를 위해 AI를 워크플로에 원활하게 통합하세요:", - "infoItemOne": "데이터베이스당 최대 10,000개의 응답", - "infoItemTwo": "작업 공간당 최대 1,000개의 응답" - }, - "currentPlan": { - "bannerLabel": "현재 플랜", - "freeTitle": "무료", - "proTitle": "Pro", - "teamTitle": "팀", - "freeInfo": "모든 것을 정리하기 위한 최대 2명의 개인용", - "proInfo": "최대 10명의 소규모 팀을 위한 완벽한 솔루션.", - "teamInfo": "모든 생산적이고 잘 조직된 팀을 위한 완벽한 솔루션.", - "upgrade": "플랜 변경", - "canceledInfo": "플랜이 취소되었습니다. {}에 무료 플랜으로 다운그레이드됩니다." - }, - "addons": { - "title": "애드온", - "addLabel": "추가", - "activeLabel": "추가됨", - "aiMax": { - "title": "AI Max", - "description": "무제한 AI 응답 및 고급 AI 모델로 구동되는 50개의 AI 이미지를 매월 제공합니다", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별" - }, - "aiOnDevice": { - "title": "Mac용 AI On-device", - "description": "장치에서 Mistral 7B, LLAMA 3 및 기타 로컬 모델 실행", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별", - "recommend": "M1 이상 권장" - } - }, - "deal": { - "bannerLabel": "새해 할인!", - "title": "팀을 성장시키세요!", - "info": "Pro 및 팀 플랜을 업그레이드하고 10% 할인 혜택을 받으세요! @:appName AI를 포함한 강력한 새로운 기능으로 작업 공간 생산성을 높이세요.", - "viewPlans": "플랜 보기" - } - } - }, - "billingPage": { - "menuLabel": "청구", - "title": "청구", - "plan": { - "title": "플랜", - "freeLabel": "무료", - "proLabel": "Pro", - "planButtonLabel": "플랜 변경", - "billingPeriod": "청구 기간", - "periodButtonLabel": "기간 수정" - }, - "paymentDetails": { - "title": "결제 세부 정보", - "methodLabel": "결제 방법", - "methodButtonLabel": "방법 수정" - }, - "addons": { - "title": "애드온", - "addLabel": "추가", - "removeLabel": "제거", - "renewLabel": "갱신", - "aiMax": { - "label": "AI Max", - "description": "무제한 AI 및 고급 모델 잠금 해제", - "activeDescription": "다음 청구서가 {}에 만료됩니다", - "canceledDescription": "AI Max는 {}까지 사용할 수 있습니다" - }, - "aiOnDevice": { - "label": "Mac용 AI On-device", - "description": "장치에서 무제한 AI 잠금 해제", - "activeDescription": "다음 청구서가 {}에 만료됩니다", - "canceledDescription": "Mac용 AI On-device는 {}까지 사용할 수 있습니다" - }, - "removeDialog": { - "title": "{} 제거", - "description": "{plan}을 제거하시겠습니까? {plan}의 기능과 혜택에 대한 액세스를 즉시 잃게 됩니다." - } - }, - "currentPeriodBadge": "현재", - "changePeriod": "기간 변경", - "planPeriod": "{} 기간", - "monthlyInterval": "월별", - "monthlyPriceInfo": "월별 청구되는 좌석당", - "annualInterval": "연간", - "annualPriceInfo": "연간 청구되는 좌석당" - }, - "comparePlanDialog": { - "title": "플랜 비교 및 선택", - "planFeatures": "플랜\n기능", - "current": "현재", - "actions": { - "upgrade": "업그레이드", - "downgrade": "다운그레이드", - "current": "현재" - }, - "freePlan": { - "title": "무료", - "description": "모든 것을 정리하기 위한 최대 2명의 개인용", - "price": "{}", - "priceInfo": "영원히 무료" - }, - "proPlan": { - "title": "Pro", - "description": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", - "price": "{}", - "priceInfo": "연간 청구되는 사용자당 월별\n\n{} 월별 청구" - }, - "planLabels": { - "itemOne": "작업 공간", - "itemTwo": "멤버", - "itemThree": "저장 공간", - "itemFour": "실시간 협업", - "itemFive": "모바일 앱", - "itemSix": "AI 응답", - "itemSeven": "AI 이미지", - "itemFileUpload": "파일 업로드", - "customNamespace": "맞춤 네임스페이스", - "tooltipSix": "평생 동안 응답 수는 재설정되지 않습니다", - "intelligentSearch": "지능형 검색", - "tooltipSeven": "작업 공간의 URL 일부를 사용자 정의할 수 있습니다", - "customNamespaceTooltip": "맞춤 게시 사이트 URL" - }, - "freeLabels": { - "itemOne": "작업 공간당 청구", - "itemTwo": "최대 2명", - "itemThree": "5 GB", - "itemFour": "예", - "itemFive": "예", - "itemSix": "평생 10회", - "itemSeven": "평생 2회", - "itemFileUpload": "최대 7 MB", - "intelligentSearch": "지능형 검색" - }, - "proLabels": { - "itemOne": "작업 공간당 청구", - "itemTwo": "최대 10명", - "itemThree": "무제한", - "itemFour": "예", - "itemFive": "예", - "itemSix": "무제한", - "itemSeven": "월별 10개 이미지", - "itemFileUpload": "무제한", - "intelligentSearch": "지능형 검색" - }, - "paymentSuccess": { - "title": "이제 {} 플랜을 사용 중입니다!", - "description": "결제가 성공적으로 처리되었으며 플랜이 @:appName {}로 업그레이드되었습니다. 플랜 세부 정보를 플랜 페이지에서 확인할 수 있습니다" - }, - "downgradeDialog": { - "title": "플랜을 다운그레이드하시겠습니까?", - "description": "플랜을 다운그레이드하면 무료 플랜으로 돌아갑니다. 멤버는 이 작업 공간에 대한 액세스를 잃을 수 있으며 저장 공간 제한을 충족하기 위해 공간을 확보해야 할 수 있습니다.", - "downgradeLabel": "플랜 다운그레이드" - } - }, - "cancelSurveyDialog": { - "title": "떠나셔서 아쉽습니다", - "description": "@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.", - "commonOther": "기타", - "otherHint": "여기에 답변을 작성하세요", - "questionOne": { - "question": "@:appName Pro 구독을 취소한 이유는 무엇입니까?", - "answerOne": "비용이 너무 높음", - "answerTwo": "기능이 기대에 미치지 못함", - "answerThree": "더 나은 대안을 찾음", - "answerFour": "비용을 정당화할 만큼 충분히 사용하지 않음", - "answerFive": "서비스 문제 또는 기술적 어려움" - }, - "questionTwo": { - "question": "미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?", - "answerOne": "매우 가능성이 높음", - "answerTwo": "어느 정도 가능성이 있음", - "answerThree": "잘 모르겠음", - "answerFour": "가능성이 낮음", - "answerFive": "매우 가능성이 낮음" - }, - "questionThree": { - "question": "구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?", - "answerOne": "다중 사용자 협업", - "answerTwo": "더 긴 시간 버전 기록", - "answerThree": "무제한 AI 응답", - "answerFour": "로컬 AI 모델 액세스" - }, - "questionFour": { - "question": "@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?", - "answerOne": "훌륭함", - "answerTwo": "좋음", - "answerThree": "보통", - "answerFour": "평균 이하", - "answerFive": "불만족" - } - }, - "common": { - "uploadingFile": "파일 업로드 중입니다. 앱을 종료하지 마세요", - "uploadNotionSuccess": "Notion zip 파일이 성공적으로 업로드되었습니다. 가져오기가 완료되면 확인 이메일을 받게 됩니다", - "reset": "재설정" - }, "menu": { - "appearance": "외관", + "appearance": "화면", "language": "언어", "user": "사용자", "files": "파일", - "notifications": "알림", "open": "설정 열기", - "logout": "로그아웃", - "logoutPrompt": "로그아웃하시겠습니까?", - "selfEncryptionLogoutPrompt": "로그아웃하시겠습니까? 암호화 비밀을 복사했는지 확인하세요", - "syncSetting": "동기화 설정", - "cloudSettings": "클라우드 설정", - "enableSync": "동기화 활성화", - "enableSyncLog": "동기화 로그 활성화", - "enableSyncLogWarning": "동기화 문제를 진단하는 데 도움을 주셔서 감사합니다. 이 작업은 문서 편집 내용을 로컬 파일에 기록합니다. 활성화 후 앱을 종료하고 다시 열어야 합니다", - "enableEncrypt": "데이터 암호화", - "cloudURL": "기본 URL", - "webURL": "웹 URL", - "invalidCloudURLScheme": "잘못된 스키마", - "cloudServerType": "클라우드 서버", - "cloudServerTypeTip": "클라우드 서버를 변경한 후 현재 계정에서 로그아웃될 수 있습니다", - "cloudLocal": "로컬", - "cloudAppFlowy": "@:appName Cloud", - "cloudAppFlowySelfHost": "@:appName Cloud 셀프 호스팅", - "appFlowyCloudUrlCanNotBeEmpty": "클라우드 URL은 비워둘 수 없습니다", - "clickToCopy": "클립보드에 복사", - "selfHostStart": "서버가 없는 경우", - "selfHostContent": "문서", - "selfHostEnd": "를 참조하여 셀프 호스팅 서버를 설정하는 방법을 확인하세요", - "pleaseInputValidURL": "유효한 URL을 입력하세요", - "changeUrl": "셀프 호스팅 URL을 {}로 변경", - "cloudURLHint": "서버의 기본 URL을 입력하세요", - "webURLHint": "웹 서버의 기본 URL을 입력하세요", - "cloudWSURL": "웹소켓 URL", - "cloudWSURLHint": "서버의 웹소켓 주소를 입력하세요", - "restartApp": "재시작", - "restartAppTip": "변경 사항을 적용하려면 애플리케이션을 재시작하세요. 현재 계정에서 로그아웃될 수 있습니다.", - "changeServerTip": "서버를 변경한 후 변경 사항을 적용하려면 재시작 버튼을 클릭해야 합니다", - "enableEncryptPrompt": "이 비밀로 데이터를 보호하려면 암호화를 활성화하세요. 안전하게 보관하세요. 활성화 후에는 비활성화할 수 없습니다. 비밀을 잃어버리면 데이터를 복구할 수 없습니다. 복사하려면 클릭하세요", - "inputEncryptPrompt": "암호화 비밀을 입력하세요", - "clickToCopySecret": "비밀을 복사하려면 클릭", - "configServerSetting": "서버 설정 구성", - "configServerGuide": "`빠른 시작`을 선택한 후 `설정`으로 이동하여 \"클라우드 설정\"을 구성하세요.", - "inputTextFieldHint": "비밀", - "historicalUserList": "사용자 로그인 기록", - "historicalUserListTooltip": "이 목록에는 익명 계정이 표시됩니다. 계정을 클릭하여 세부 정보를 확인할 수 있습니다. 익명 계정은 '시작하기' 버튼을 클릭하여 생성됩니다", - "openHistoricalUser": "익명 계정을 열려면 클릭", - "customPathPrompt": "Google Drive와 같은 클라우드 동기화 폴더에 @:appName 데이터 폴더를 저장하면 위험이 발생할 수 있습니다. 이 폴더 내의 데이터베이스에 여러 위치에서 동시에 액세스하거나 수정하면 동기화 충돌 및 데이터 손상이 발생할 수 있습니다", - "importAppFlowyData": "외부 @:appName 폴더에서 데이터 가져오기", - "importingAppFlowyDataTip": "데이터 가져오는 중입니다. 앱을 종료하지 마세요", - "importAppFlowyDataDescription": "외부 @:appName 데이터 폴더에서 데이터를 복사하여 현재 AppFlowy 데이터 폴더에 가져옵니다", - "importSuccess": "성공적으로 @:appName 데이터 폴더를 가져왔습니다", - "importFailed": "@:appName 데이터 폴더 가져오기 실패", - "importGuide": "자세한 내용은 참조 문서를 확인하세요" - }, - "notifications": { - "enableNotifications": { - "label": "알림 활성화", - "hint": "로컬 알림이 나타나지 않도록 하려면 끄세요." - }, - "showNotificationsIcon": { - "label": "알림 아이콘 표시", - "hint": "사이드바에서 알림 아이콘을 숨기려면 끄세요." - }, - "archiveNotifications": { - "allSuccess": "모든 알림이 성공적으로 보관되었습니다", - "success": "알림이 성공적으로 보관되었습니다" - }, - "markAsReadNotifications": { - "allSuccess": "모두 읽음으로 표시되었습니다", - "success": "읽음으로 표시되었습니다" - }, - "action": { - "markAsRead": "읽음으로 표시", - "multipleChoice": "더 선택", - "archive": "보관" - }, - "settings": { - "settings": "설정", - "markAllAsRead": "모두 읽음으로 표시", - "archiveAll": "모두 보관" - }, - "emptyInbox": { - "title": "받은 편지함 비어 있음!", - "description": "알림을 받으려면 알림을 설정하세요." - }, - "emptyUnread": { - "title": "읽지 않은 알림 없음", - "description": "모든 알림을 확인했습니다!" - }, - "emptyArchived": { - "title": "보관된 항목 없음", - "description": "보관된 알림이 여기에 표시됩니다." - }, - "tabs": { - "inbox": "받은 편지함", - "unread": "읽지 않음", - "archived": "보관됨" - }, - "refreshSuccess": "알림이 성공적으로 새로고침되었습니다", - "titles": { - "notifications": "알림", - "reminder": "알림" - } + "supabaseSetting": "수파베이스 설정" }, "appearance": { - "resetSetting": "재설정", "fontFamily": { - "label": "글꼴", - "search": "검색", - "defaultFont": "시스템" + "label": "글꼴 패밀리", + "search": "찾다" }, "themeMode": { "label": "테마 모드", "light": "라이트 모드", "dark": "다크 모드", - "system": "시스템에 맞춤" - }, - "fontScaleFactor": "글꼴 크기 비율", - "displaySize": "디스플레이 크기", - "documentSettings": { - "cursorColor": "문서 커서 색상", - "selectionColor": "문서 선택 색상", - "width": "문서 너비", - "changeWidth": "변경", - "pickColor": "색상 선택", - "colorShade": "색상 음영", - "opacity": "불투명도", - "hexEmptyError": "16진수 색상은 비워둘 수 없습니다", - "hexLengthError": "16진수 값은 6자리여야 합니다", - "hexInvalidError": "잘못된 16진수 값", - "opacityEmptyError": "불투명도는 비워둘 수 없습니다", - "opacityRangeError": "불투명도는 1에서 100 사이여야 합니다", - "app": "앱", - "flowy": "Flowy", - "apply": "적용" - }, - "layoutDirection": { - "label": "레이아웃 방향", - "hint": "화면의 콘텐츠 흐름을 왼쪽에서 오른쪽 또는 오른쪽에서 왼쪽으로 제어합니다.", - "ltr": "LTR", - "rtl": "RTL" - }, - "textDirection": { - "label": "기본 텍스트 방향", - "hint": "텍스트가 기본적으로 왼쪽에서 시작할지 오른쪽에서 시작할지 지정합니다.", - "ltr": "LTR", - "rtl": "RTL", - "auto": "자동", - "fallback": "레이아웃 방향과 동일" + "system": "시스템에 적응" }, "themeUpload": { "button": "업로드", - "uploadTheme": "테마 업로드", - "description": "아래 버튼을 사용하여 사용자 정의 @:appName 테마를 업로드하세요.", - "loading": "테마를 검증하고 업로드하는 동안 기다려주세요...", - "uploadSuccess": "테마가 성공적으로 업로드되었습니다", - "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보세요.", + "description": "아래 버튼을 사용하여 나만의 @:appName 테마를 업로드하세요.", + "loading": "테마를 확인하고 업로드하는 동안 잠시 기다려 주십시오...", + "uploadSuccess": "테마가 성공적으로 업로드되었습니다.", + "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보십시오.", "filePickerDialogTitle": ".flowy_plugin 파일 선택", - "urlUploadFailure": "URL을 열지 못했습니다: {}" + "urlUploadFailure": "URL을 열지 못했습니다: {}", + "failure": "업로드된 테마의 형식이 잘못되었습니다." }, - "theme": "테마", + "theme": "주제", "builtInsLabel": "내장 테마", "pluginsLabel": "플러그인", - "dateFormat": { - "label": "날짜 형식", - "local": "로컬", - "us": "미국", - "iso": "ISO", - "friendly": "친숙한", - "dmy": "일/월/년" - }, - "timeFormat": { - "label": "시간 형식", - "twelveHour": "12시간 형식", - "twentyFourHour": "24시간 형식" - }, - "showNamingDialogWhenCreatingPage": "페이지 생성 시 이름 지정 대화 상자 표시", - "enableRTLToolbarItems": "RTL 도구 모음 항목 활성화", - "members": { - "title": "멤버 설정", - "inviteMembers": "멤버 초대", - "inviteHint": "이메일로 초대", - "sendInvite": "초대 보내기", - "copyInviteLink": "초대 링크 복사", - "label": "멤버", - "user": "사용자", - "role": "역할", - "removeFromWorkspace": "작업 공간에서 제거", - "removeFromWorkspaceSuccess": "작업 공간에서 성공적으로 제거되었습니다", - "removeFromWorkspaceFailed": "작업 공간에서 제거 실패", - "owner": "소유자", - "guest": "게스트", - "member": "멤버", - "memberHintText": "멤버는 페이지를 읽고 편집할 수 있습니다", - "guestHintText": "게스트는 페이지를 읽고, 반응하고, 댓글을 달 수 있으며, 권한이 있는 특정 페이지를 편집할 수 있습니다.", - "emailInvalidError": "잘못된 이메일입니다. 확인하고 다시 시도하세요", - "emailSent": "이메일이 전송되었습니다. 받은 편지함을 확인하세요", - "members": "멤버", - "membersCount": { - "zero": "{}명의 멤버", - "one": "{}명의 멤버", - "other": "{}명의 멤버" - }, - "inviteFailedDialogTitle": "초대 전송 실패", - "inviteFailedMemberLimit": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 업그레이드하세요.", - "inviteFailedMemberLimitMobile": "작업 공간의 멤버 한도에 도달했습니다.", - "memberLimitExceeded": "멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 ", - "memberLimitExceededUpgrade": "업그레이드", - "memberLimitExceededPro": "멤버 한도에 도달했습니다. 더 많은 멤버가 필요하면 ", - "memberLimitExceededProContact": "support@appflowy.io에 문의하세요", - "failedToAddMember": "멤버 추가 실패", - "addMemberSuccess": "멤버가 성공적으로 추가되었습니다", - "removeMember": "멤버 제거", - "areYouSureToRemoveMember": "이 멤버를 제거하시겠습니까?", - "inviteMemberSuccess": "초대가 성공적으로 전송되었습니다", - "failedToInviteMember": "멤버 초대 실패", - "workspaceMembersError": "문제가 발생했습니다", - "workspaceMembersErrorDescription": "현재 멤버 목록을 로드할 수 없습니다. 나중에 다시 시도하세요" - } + "lightLabel": "라이트 모드", + "darkLabel": "다크 모드" }, "files": { "copy": "복사", "defaultLocation": "파일 및 데이터 저장 위치 읽기", "exportData": "데이터 내보내기", - "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요", + "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요.", "restoreLocation": "@:appName 기본 경로로 복원", "customizeLocation": "다른 폴더 열기", - "restartApp": "변경 사항을 적용하려면 앱을 재시작하세요.", + "restartApp": "변경 사항을 적용하려면 앱을 다시 시작하십시오.", "exportDatabase": "데이터베이스 내보내기", - "selectFiles": "내보낼 파일 선택", + "selectFiles": "내보낼 파일을 선택하십시오", "selectAll": "모두 선택", - "deselectAll": "모두 선택 해제", + "deselectAll": "모두 선택 취소", "createNewFolder": "새 폴더 만들기", - "createNewFolderDesc": "데이터를 저장할 위치를 알려주세요", + "createNewFolderDesc": "데이터를 저장할 위치를 알려주십시오.", "defineWhereYourDataIsStored": "데이터가 저장되는 위치 정의", - "open": "열기", + "open": "열려 있는", "openFolder": "기존 폴더 열기", - "openFolderDesc": "기존 @:appName 폴더를 읽고 쓰기", + "openFolderDesc": "기존 @:appName 폴더에서 읽고 쓰기", "folderHintText": "폴더 이름", "location": "새 폴더 만들기", - "locationDesc": "@:appName 데이터 폴더의 이름을 지정하세요", - "browser": "찾아보기", - "create": "생성", - "set": "설정", + "locationDesc": "@:appName 데이터 폴더의 이름을 선택하세요", + "browser": "검색", + "create": "만들다", + "set": "세트", "folderPath": "폴더를 저장할 경로", - "locationCannotBeEmpty": "경로는 비워둘 수 없습니다", + "locationCannotBeEmpty": "경로는 비워둘 수 없습니다.", "pathCopiedSnackbar": "파일 저장 경로가 클립보드에 복사되었습니다!", "changeLocationTooltips": "데이터 디렉토리 변경", - "change": "변경", + "change": "변화", "openLocationTooltips": "다른 데이터 디렉토리 열기", "openCurrentDataFolder": "현재 데이터 디렉토리 열기", - "recoverLocationTooltips": "@:appName의 기본 데이터 디렉토리로 재설정", - "exportFileSuccess": "파일이 성공적으로 내보내졌습니다!", + "recoverLocationTooltips": "@:appName의 기본 데이터 디렉터리로 재설정", + "exportFileSuccess": "파일을 성공적으로 내보냈습니다!", "exportFileFail": "파일 내보내기 실패!", - "export": "내보내기", - "clearCache": "캐시 지우기", - "clearCacheDesc": "이미지가 로드되지 않거나 글꼴이 제대로 표시되지 않는 등의 문제가 발생하면 캐시를 지워보세요. 이 작업은 사용자 데이터에는 영향을 미치지 않습니다.", - "areYouSureToClearCache": "캐시를 지우시겠습니까?", - "clearCacheSuccess": "캐시가 성공적으로 지워졌습니다!" + "export": "내보내다" }, "user": { "name": "이름", - "email": "이메일", - "tooltipSelectIcon": "아이콘 선택", - "selectAnIcon": "아이콘 선택", - "pleaseInputYourOpenAIKey": "AI 키를 입력하세요", - "clickToLogout": "현재 사용자 로그아웃" - }, - "mobile": { - "personalInfo": "개인 정보", - "username": "사용자 이름", - "usernameEmptyError": "사용자 이름은 비워둘 수 없습니다", - "about": "정보", - "pushNotifications": "푸시 알림", - "support": "지원", - "joinDiscord": "Discord에 참여", - "privacyPolicy": "개인정보 보호정책", - "userAgreement": "사용자 계약", - "termsAndConditions": "이용 약관", - "userprofileError": "사용자 프로필을 로드하지 못했습니다", - "userprofileErrorDescription": "로그아웃 후 다시 로그인하여 문제가 계속되는지 확인하세요.", - "selectLayout": "레이아웃 선택", - "selectStartingDay": "시작일 선택", - "version": "버전" + "selectAnIcon": "아이콘을 선택하세요", + "pleaseInputYourOpenAIKey": "OpenAI 키를 입력하십시오" } }, "grid": { "deleteView": "이 보기를 삭제하시겠습니까?", - "createView": "새로 만들기", - "title": { - "placeholder": "제목 없음" - }, + "createView": "새로운", "settings": { "filter": "필터", - "sort": "정렬", + "sort": "종류", "sortBy": "정렬 기준", "properties": "속성", - "reorderPropertiesTooltip": "속성 순서 변경", + "reorderPropertiesTooltip": "드래그하여 속성 재정렬", "group": "그룹", "addFilter": "필터 추가", "deleteFilter": "필터 삭제", - "filterBy": "필터 기준", - "typeAValue": "값 입력...", - "layout": "레이아웃", - "compactMode": "압축 모드", - "databaseLayout": "레이아웃", - "viewList": { - "zero": "0개의 보기", - "one": "{count}개의 보기", - "other": "{count}개의 보기" - }, - "editView": "보기 편집", - "boardSettings": "보드 설정", - "calendarSettings": "캘린더 설정", - "createView": "새 보기", - "duplicateView": "보기 복제", - "deleteView": "보기 삭제", - "numberOfVisibleFields": "{}개 표시됨" - }, - "filter": { - "empty": "활성 필터 없음", - "addFilter": "필터 추가", - "cannotFindCreatableField": "필터링할 적절한 필드를 찾을 수 없습니다", - "conditon": "조건", - "where": "조건" + "filterBy": "필터링 기준...", + "typeAValue": "값을 입력하세요...", + "layout": "공들여 나열한 것", + "databaseLayout": "공들여 나열한 것", + "Properties": "속성" }, "textFilter": { "contains": "포함", - "doesNotContain": "포함하지 않음", - "endsWith": "끝남", + "doesNotContain": "포함되어 있지 않다", + "endsWith": "로 끝나다", "startWith": "시작", - "is": "일치", - "isNot": "일치하지 않음", - "isEmpty": "비어 있음", + "is": "~이다", + "isNot": "아니다", + "isEmpty": "비었다", "isNotEmpty": "비어 있지 않음", "choicechipPrefix": { - "isNot": "일치하지 않음", + "isNot": "아니다", "startWith": "시작", - "endWith": "끝남", - "isEmpty": "비어 있음", - "isNotEmpty": "비어 있지 않음" + "endWith": "로 끝나다", + "isEmpty": "비었다", + "isNotEmpty": "비어있지 않다" } }, "checkboxFilter": { - "isChecked": "체크됨", - "isUnchecked": "체크되지 않음", + "isChecked": "체크", + "isUnchecked": "체크 해제", "choicechipPrefix": { - "is": "체크됨" + "is": "~이다" } }, "checklistFilter": { - "isComplete": "완료됨", - "isIncomplted": "미완료" + "isComplete": "완료되었습니다", + "isIncomplted": "불완전하다" }, "selectOptionFilter": { - "is": "일치", - "isNot": "일치하지 않음", + "is": "~이다", + "isNot": "아니다", "contains": "포함", - "doesNotContain": "포함하지 않음", - "isEmpty": "비어 있음", - "isNotEmpty": "비어 있지 않음" - }, - "dateFilter": { - "is": "일치", - "before": "이전", - "after": "이후", - "onOrBefore": "이전 또는 일치", - "onOrAfter": "이후 또는 일치", - "between": "사이", - "empty": "비어 있음", - "notEmpty": "비어 있지 않음", - "startDate": "시작 날짜", - "endDate": "종료 날짜", - "choicechipPrefix": { - "before": "이전", - "after": "이후", - "between": "사이", - "onOrBefore": "이전 또는 일치", - "onOrAfter": "이후 또는 일치", - "isEmpty": "비어 있음", - "isNotEmpty": "비어 있지 않음" - } - }, - "numberFilter": { - "equal": "일치", - "notEqual": "일치하지 않음", - "lessThan": "미만", - "greaterThan": "초과", - "lessThanOrEqualTo": "이하", - "greaterThanOrEqualTo": "이상", - "isEmpty": "비어 있음", + "doesNotContain": "포함되어 있지 않다", + "isEmpty": "비었다", "isNotEmpty": "비어 있지 않음" }, "field": { - "label": "속성", - "hide": "속성 숨기기", - "show": "속성 표시", - "insertLeft": "왼쪽에 삽입", - "insertRight": "오른쪽에 삽입", + "hide": "숨기기", + "insertLeft": "왼쪽 삽입", + "insertRight": "오른쪽 삽입", "duplicate": "복제", "delete": "삭제", - "wrapCellContent": "텍스트 줄 바꿈", - "clear": "셀 지우기", - "switchPrimaryFieldTooltip": "기본 필드의 필드 유형을 변경할 수 없습니다", "textFieldName": "텍스트", "checkboxFieldName": "체크박스", "dateFieldName": "날짜", - "updatedAtFieldName": "마지막 수정", - "createdAtFieldName": "생성일", + "updatedAtFieldName": "마지막 수정 시간", + "createdAtFieldName": "만든 시간", "numberFieldName": "숫자", "singleSelectFieldName": "선택", - "multiSelectFieldName": "다중 선택", - "urlFieldName": "URL", + "multiSelectFieldName": "다중선택", + "urlFieldName": "링크", "checklistFieldName": "체크리스트", - "relationFieldName": "관계", - "summaryFieldName": "AI 요약", - "timeFieldName": "시간", - "mediaFieldName": "파일 및 미디어", - "translateFieldName": "AI 번역", - "translateTo": "번역 대상", "numberFormat": "숫자 형식", "dateFormat": "날짜 형식", - "includeTime": "시간 포함", - "isRange": "종료 날짜", + "includeTime": "시간 표시", "dateFormatFriendly": "월 일, 년", "dateFormatISO": "년-월-일", "dateFormatLocal": "월/일/년", @@ -1527,558 +358,181 @@ "dateFormatDayMonthYear": "일/월/년", "timeFormat": "시간 형식", "invalidTimeFormat": "잘못된 형식", - "timeFormatTwelveHour": "12시간", - "timeFormatTwentyFourHour": "24시간", - "clearDate": "날짜 지우기", - "dateTime": "날짜 시간", - "startDateTime": "시작 날짜 시간", - "endDateTime": "종료 날짜 시간", - "failedToLoadDate": "날짜 값을 로드하지 못했습니다", - "selectTime": "시간 선택", - "selectDate": "날짜 선택", - "visibility": "가시성", - "propertyType": "속성 유형", + "timeFormatTwelveHour": "12 시간", + "timeFormatTwentyFourHour": "24 시간", "addSelectOption": "옵션 추가", - "typeANewOption": "새 옵션 입력", "optionTitle": "옵션", "addOption": "옵션 추가", "editProperty": "속성 편집", - "newProperty": "새 속성", - "openRowDocument": "페이지로 열기", - "deleteFieldPromptMessage": "확실합니까? 이 속성과 모든 데이터가 삭제됩니다", - "clearFieldPromptMessage": "확실합니까? 이 열의 모든 셀이 비워집니다", - "newColumn": "새 열", - "format": "형식", - "reminderOnDateTooltip": "이 셀에는 예약된 알림이 있습니다", - "optionAlreadyExist": "옵션이 이미 존재합니다" - }, - "rowPage": { - "newField": "새 필드 추가", - "fieldDragElementTooltip": "메뉴 열기", - "showHiddenFields": { - "one": "숨겨진 {count}개의 필드 표시", - "many": "숨겨진 {count}개의 필드 표시", - "other": "숨겨진 {count}개의 필드 표시" - }, - "hideHiddenFields": { - "one": "숨겨진 {count}개의 필드 숨기기", - "many": "숨겨진 {count}개의 필드 숨기기", - "other": "숨겨진 {count}개의 필드 숨기기" - }, - "openAsFullPage": "전체 페이지로 열기", - "moreRowActions": "더 많은 행 작업" + "newProperty": "열 추가", + "deleteFieldPromptMessage": "해당 속성을 삭제 하시겠습니까?" }, "sort": { "ascending": "오름차순", "descending": "내림차순", - "by": "기준", - "empty": "활성 정렬 없음", - "cannotFindCreatableField": "정렬할 적절한 필드를 찾을 수 없습니다", - "deleteAllSorts": "모든 정렬 삭제", "addSort": "정렬 추가", - "sortsActive": "정렬 중에는 {intention}할 수 없습니다", - "removeSorting": "이 보기의 모든 정렬을 제거하고 계속하시겠습니까?", - "fieldInUse": "이미 이 필드로 정렬 중입니다" + "deleteSort": "정렬 삭제" }, "row": { - "label": "행", "duplicate": "복제", "delete": "삭제", - "titlePlaceholder": "제목 없음", - "textPlaceholder": "비어 있음", - "copyProperty": "속성이 클립보드에 복사되었습니다", + "textPlaceholder": "비어있음", + "copyProperty": "속성이 클립보드로 복사됨", "count": "개수", - "newRow": "새 행", - "loadMore": "더 로드", - "action": "작업", - "add": "아래에 추가하려면 클릭", - "drag": "이동하려면 드래그", - "deleteRowPrompt": "이 행을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "deleteCardPrompt": "이 카드를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "dragAndClick": "이동하려면 드래그, 메뉴를 열려면 클릭", - "insertRecordAbove": "위에 레코드 삽입", - "insertRecordBelow": "아래에 레코드 삽입", - "noContent": "내용 없음", - "reorderRowDescription": "행 순서 변경", - "createRowAboveDescription": "위에 행 생성", - "createRowBelowDescription": "아래에 행 삽입" + "newRow": "행 추가", + "action": "행동" }, "selectOption": { "create": "생성", "purpleColor": "보라색", - "pinkColor": "분홍색", - "lightPinkColor": "연분홍색", - "orangeColor": "주황색", - "yellowColor": "노란색", + "pinkColor": "핑크색", + "lightPinkColor": "연한 핑크색", + "orangeColor": "오렌지색", + "yellowColor": "노랑색", "limeColor": "라임색", - "greenColor": "녹색", - "aquaColor": "청록색", - "blueColor": "파란색", + "greenColor": "초록색", + "aquaColor": "아쿠아색", + "blueColor": "파랑색", "deleteTag": "태그 삭제", "colorPanelTitle": "색상", "panelTitle": "옵션 선택 또는 생성", - "searchOption": "옵션 검색", - "searchOrCreateOption": "옵션 검색 또는 생성", - "createNew": "새로 생성", - "orSelectOne": "또는 옵션 선택", - "typeANewOption": "새 옵션 입력", - "tagName": "태그 이름" + "searchOption": "옵션 검색" }, "checklist": { - "taskHint": "작업 설명", - "addNew": "새 작업 추가", - "submitNewTask": "생성", - "hideComplete": "완료된 작업 숨기기", - "showComplete": "모든 작업 표시" - }, - "url": { - "launch": "브라우저에서 링크 열기", - "copy": "링크를 클립보드에 복사", - "textFieldHint": "URL 입력" - }, - "relation": { - "relatedDatabasePlaceLabel": "관련 데이터베이스", - "relatedDatabasePlaceholder": "없음", - "inRelatedDatabase": "에", - "rowSearchTextFieldPlaceholder": "검색", - "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 아래 목록에서 하나를 먼저 선택하세요:", - "emptySearchResult": "레코드를 찾을 수 없습니다", - "linkedRowListLabel": "{count}개의 연결된 행", - "unlinkedRowListLabel": "다른 행 연결" + "addNew": "항목 추가" }, "menuName": "그리드", - "referencedGridPrefix": "보기", - "calculate": "계산", - "calculationTypeLabel": { - "none": "없음", - "average": "평균", - "max": "최대", - "median": "중앙값", - "min": "최소", - "sum": "합계", - "count": "개수", - "countEmpty": "비어 있는 개수", - "countEmptyShort": "비어 있음", - "countNonEmpty": "비어 있지 않은 개수", - "countNonEmptyShort": "채워짐" - }, - "media": { - "rename": "이름 변경", - "download": "다운로드", - "expand": "확장", - "delete": "삭제", - "moreFilesHint": "+{}", - "addFileOrImage": "파일 또는 링크 추가", - "attachmentsHint": "{}", - "addFileMobile": "파일 추가", - "extraCount": "+{}", - "deleteFileDescription": "이 파일을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", - "showFileNames": "파일 이름 표시", - "downloadSuccess": "파일이 다운로드되었습니다", - "downloadFailedToken": "파일을 다운로드하지 못했습니다. 사용자 토큰이 없습니다", - "setAsCover": "표지로 설정", - "openInBrowser": "브라우저에서 열기", - "embedLink": "파일 링크 삽입" - } + "referencedGridPrefix": "관점" }, "document": { - "menuName": "문서", + "menuName": "도큐먼트", "date": { - "timeHintTextInTwelveHour": "오후 01:00", + "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "생성 중...", "slashMenu": { "board": { "selectABoardToLinkTo": "연결할 보드 선택", - "createANewBoard": "새 보드 생성" + "createANewBoard": "새 보드 만들기" }, "grid": { "selectAGridToLinkTo": "연결할 그리드 선택", - "createANewGrid": "새 그리드 생성" + "createANewGrid": "새 그리드 만들기" }, "calendar": { "selectACalendarToLinkTo": "연결할 캘린더 선택", - "createANewCalendar": "새 캘린더 생성" - }, - "document": { - "selectADocumentToLinkTo": "연결할 문서 선택" - }, - "name": { - "textStyle": "텍스트 스타일", - "list": "목록", - "toggle": "토글", - "fileAndMedia": "파일 및 미디어", - "simpleTable": "간단한 테이블", - "visuals": "시각 자료", - "document": "문서", - "advanced": "고급", - "text": "텍스트", - "heading1": "헤딩 1", - "heading2": "헤딩 2", - "heading3": "헤딩 3", - "image": "이미지", - "bulletedList": "글머리 기호 목록", - "numberedList": "번호 매기기 목록", - "todoList": "할 일 목록", - "doc": "문서", - "linkedDoc": "페이지로 연결", - "grid": "그리드", - "linkedGrid": "연결된 그리드", - "kanban": "칸반", - "linkedKanban": "연결된 칸반", - "calendar": "캘린더", - "linkedCalendar": "연결된 캘린더", - "quote": "인용", - "divider": "구분선", - "table": "테이블", - "callout": "콜아웃", - "outline": "개요", - "mathEquation": "수학 방정식", - "code": "코드", - "toggleList": "토글 목록", - "toggleHeading1": "토글 헤딩 1", - "toggleHeading2": "토글 헤딩 2", - "toggleHeading3": "토글 헤딩 3", - "emoji": "이모지", - "aiWriter": "AI에게 물어보기", - "dateOrReminder": "날짜 또는 알림", - "photoGallery": "사진 갤러리", - "file": "파일", - "twoColumns": "2열", - "threeColumns": "3열", - "fourColumns": "4열" - }, - "subPage": { - "name": "문서", - "keyword1": "하위 페이지", - "keyword2": "페이지", - "keyword3": "자식 페이지", - "keyword4": "페이지 삽입", - "keyword5": "페이지 포함", - "keyword6": "새 페이지", - "keyword7": "페이지 생성", - "keyword8": "문서" + "createANewCalendar": "새 캘린더 만들기" } }, "selectionMenu": { - "outline": "개요", - "codeBlock": "코드 블록" + "outline": "개요" }, "plugins": { - "referencedBoard": "참조된 보드", + "referencedBoard": "참조 보드", "referencedGrid": "참조된 그리드", - "referencedCalendar": "참조된 캘린더", - "referencedDocument": "참조된 문서", - "aiWriter": { - "userQuestion": "AI에게 물어보기", - "continueWriting": "계속 작성", - "fixSpelling": "맞춤법 및 문법 수정", - "improveWriting": "글쓰기 개선", - "summarize": "요약", - "explain": "설명", - "makeShorter": "짧게 만들기", - "makeLonger": "길게 만들기" - }, - "autoGeneratorMenuItemName": "AI 작성기", - "autoGeneratorTitleName": "AI: AI에게 무엇이든 물어보세요...", - "autoGeneratorLearnMore": "자세히 알아보기", - "autoGeneratorGenerate": "생성", - "autoGeneratorHintText": "AI에게 물어보기 ...", - "autoGeneratorCantGetOpenAIKey": "AI 키를 가져올 수 없습니다", - "autoGeneratorRewrite": "다시 작성", - "smartEdit": "AI에게 물어보기", - "aI": "AI", - "smartEditFixSpelling": "맞춤법 및 문법 수정", + "referencedCalendar": "참조된 달력", + "autoGeneratorMenuItemName": "OpenAI 작성자", + "autoGeneratorTitleName": "OpenAI: AI에게 무엇이든 쓰라고 요청하세요...", + "autoGeneratorLearnMore": "더 알아보기", + "autoGeneratorGenerate": "생성하다", + "autoGeneratorHintText": "OpenAI에게 물어보세요 ...", + "autoGeneratorCantGetOpenAIKey": "OpenAI 키를 가져올 수 없습니다.", + "autoGeneratorRewrite": "고쳐 쓰기", + "smartEdit": "AI 어시스턴트", + "openAI": "OpenAI", + "smartEditFixSpelling": "맞춤법 수정", "warning": "⚠️ AI 응답은 부정확하거나 오해의 소지가 있을 수 있습니다.", - "smartEditSummarize": "요약", - "smartEditImproveWriting": "글쓰기 개선", - "smartEditMakeLonger": "길게 만들기", - "smartEditCouldNotFetchResult": "AI에서 결과를 가져올 수 없습니다", - "smartEditCouldNotFetchKey": "AI 키를 가져올 수 없습니다", - "smartEditDisabled": "설정에서 AI 연결", - "appflowyAIEditDisabled": "AI 기능을 활성화하려면 로그인하세요", - "discardResponse": "AI 응답을 버리시겠습니까?", - "createInlineMathEquation": "방정식 생성", - "fonts": "글꼴", - "insertDate": "날짜 삽입", - "emoji": "이모지", + "smartEditSummarize": "요약하다", + "smartEditImproveWriting": "쓰기 향상", + "smartEditMakeLonger": "더 길게", + "smartEditCouldNotFetchResult": "OpenAI에서 결과를 가져올 수 없습니다.", + "smartEditCouldNotFetchKey": "OpenAI 키를 가져올 수 없습니다.", + "smartEditDisabled": "설정에서 OpenAI 연결", + "discardResponse": "AI 응답을 삭제하시겠습니까?", + "createInlineMathEquation": "방정식 만들기", "toggleList": "토글 목록", - "emptyToggleHeading": "빈 토글 h{}. 내용을 추가하려면 클릭하세요.", - "emptyToggleList": "빈 토글 목록. 내용을 추가하려면 클릭하세요.", - "emptyToggleHeadingWeb": "빈 토글 h{level}. 내용을 추가하려면 클릭하세요", - "quoteList": "인용 목록", - "numberedList": "번호 매기기 목록", - "bulletedList": "글머리 기호 목록", - "todoList": "할 일 목록", - "callout": "콜아웃", - "simpleTable": { - "moreActions": { - "color": "색상", - "align": "정렬", - "delete": "삭제", - "duplicate": "복제", - "insertLeft": "왼쪽에 삽입", - "insertRight": "오른쪽에 삽입", - "insertAbove": "위에 삽입", - "insertBelow": "아래에 삽입", - "headerColumn": "헤더 열", - "headerRow": "헤더 행", - "clearContents": "내용 지우기", - "setToPageWidth": "페이지 너비로 설정", - "distributeColumnsWidth": "열 너비 균등 분배", - "duplicateRow": "행 복제", - "duplicateColumn": "열 복제", - "textColor": "텍스트 색상", - "cellBackgroundColor": "셀 배경 색상", - "duplicateTable": "테이블 복제" - }, - "clickToAddNewRow": "새 행을 추가하려면 클릭", - "clickToAddNewColumn": "새 열을 추가하려면 클릭", - "clickToAddNewRowAndColumn": "새 행과 열을 추가하려면 클릭", - "headerName": { - "table": "테이블", - "alignText": "텍스트 정렬" - } - }, "cover": { - "changeCover": "표지 변경", - "colors": "색상", + "changeCover": "커버를 바꾸다", + "colors": "그림 물감", "images": "이미지", "clearAll": "모두 지우기", - "abstract": "추상", + "abstract": "추상적인", "addCover": "표지 추가", "addLocalImage": "로컬 이미지 추가", "invalidImageUrl": "잘못된 이미지 URL", - "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다", + "failedToAddImageToGallery": "갤러리에 이미지를 추가하지 못했습니다.", "enterImageUrl": "이미지 URL 입력", - "add": "추가", - "back": "뒤로", + "add": "추가하다", + "back": "뒤쪽에", "saveToGallery": "갤러리에 저장", "removeIcon": "아이콘 제거", - "removeCover": "표지 제거", "pasteImageUrl": "이미지 URL 붙여넣기", "or": "또는", "pickFromFiles": "파일에서 선택", - "couldNotFetchImage": "이미지를 가져올 수 없습니다", + "couldNotFetchImage": "이미지를 가져올 수 없습니다.", "imageSavingFailed": "이미지 저장 실패", "addIcon": "아이콘 추가", - "changeIcon": "아이콘 변경", "coverRemoveAlert": "삭제 후 표지에서 제거됩니다.", - "alertDialogConfirmation": "계속하시겠습니까?" + "alertDialogConfirmation": "너 정말 계속하고 싶니?" }, "mathEquation": { - "name": "수학 방정식", - "addMathEquation": "TeX 방정식 추가", + "addMathEquation": "수학 방정식 추가", "editMathEquation": "수학 방정식 편집" }, "optionAction": { - "click": "클릭", + "click": "딸깍 하는 소리", "toOpenMenu": " 메뉴 열기", - "drag": "드래그", - "toMove": " 이동", "delete": "삭제", - "duplicate": "복제", - "turnInto": "변환", - "moveUp": "위로 이동", + "duplicate": "복제하다", + "turnInto": "로 변하다", + "moveUp": "이동", "moveDown": "아래로 이동", "color": "색상", - "align": "정렬", + "align": "맞추다", "left": "왼쪽", - "center": "가운데", + "center": "센터", "right": "오른쪽", - "defaultColor": "기본", - "depth": "깊이", - "copyLinkToBlock": "블록 링크 복사" + "defaultColor": "기본" }, "image": { - "addAnImage": "이미지 추가", - "copiedToPasteBoard": "이미지 링크가 클립보드에 복사되었습니다", - "addAnImageDesktop": "이미지 추가", - "addAnImageMobile": "하나 이상의 이미지를 추가하려면 클릭", - "dropImageToInsert": "이미지를 드롭하여 삽입", - "imageUploadFailed": "이미지 업로드 실패", - "imageDownloadFailed": "이미지 다운로드 실패, 다시 시도하세요", - "imageDownloadFailedToken": "사용자 토큰이 없어 이미지 다운로드 실패, 다시 시도하세요", - "errorCode": "오류 코드" - }, - "photoGallery": { - "name": "사진 갤러리", - "imageKeyword": "이미지", - "imageGalleryKeyword": "이미지 갤러리", - "photoKeyword": "사진", - "photoBrowserKeyword": "사진 브라우저", - "galleryKeyword": "갤러리", - "addImageTooltip": "이미지 추가", - "changeLayoutTooltip": "레이아웃 변경", - "browserLayout": "브라우저", - "gridLayout": "그리드", - "deleteBlockTooltip": "전체 갤러리 삭제" - }, - "math": { - "copiedToPasteBoard": "수학 방정식이 클립보드에 복사되었습니다" - }, - "urlPreview": { - "copiedToPasteBoard": "링크가 클립보드에 복사되었습니다", - "convertToLink": "링크로 변환" + "copiedToPasteBoard": "이미지 링크가 클립보드에 복사되었습니다." }, "outline": { - "addHeadingToCreateOutline": "목차를 만들려면 헤딩을 추가하세요.", - "noMatchHeadings": "일치하는 헤딩이 없습니다." - }, - "table": { - "addAfter": "뒤에 추가", - "addBefore": "앞에 추가", - "delete": "삭제", - "clear": "내용 지우기", - "duplicate": "복제", - "bgColor": "배경 색상" - }, - "contextMenu": { - "copy": "복사", - "cut": "잘라내기", - "paste": "붙여넣기", - "pasteAsPlainText": "서식 없이 붙여넣기" - }, - "action": "작업", - "database": { - "selectDataSource": "데이터 소스 선택", - "noDataSource": "데이터 소스 없음", - "selectADataSource": "데이터 소스 선택", - "toContinue": "계속하려면", - "newDatabase": "새 데이터베이스", - "linkToDatabase": "데이터베이스로 연결" - }, - "date": "날짜", - "video": { - "label": "비디오", - "emptyLabel": "비디오 추가", - "placeholder": "비디오 링크 붙여넣기", - "copiedToPasteBoard": "비디오 링크가 클립보드에 복사되었습니다", - "insertVideo": "비디오 추가", - "invalidVideoUrl": "지원되지 않는 소스 URL입니다.", - "invalidVideoUrlYouTube": "YouTube는 아직 지원되지 않습니다.", - "supportedFormats": "지원되는 형식: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "파일", - "uploadTab": "업로드", - "uploadMobile": "파일 선택", - "uploadMobileGallery": "사진 갤러리에서", - "networkTab": "링크 삽입", - "placeholderText": "파일 업로드 또는 삽입", - "placeholderDragging": "업로드할 파일을 드롭하세요", - "dropFileToUpload": "업로드할 파일을 드롭하세요", - "fileUploadHint": "파일을 드래그 앤 드롭하거나 클릭하여 ", - "fileUploadHintSuffix": "찾아보기", - "networkHint": "파일 링크 붙여넣기", - "networkUrlInvalid": "잘못된 URL입니다. URL을 확인하고 다시 시도하세요.", - "networkAction": "삽입", - "fileTooBigError": "파일 크기가 너무 큽니다. 10MB 미만의 파일을 업로드하세요", - "renameFile": { - "title": "파일 이름 변경", - "description": "이 파일의 새 이름을 입력하세요", - "nameEmptyError": "파일 이름은 비워둘 수 없습니다." - }, - "uploadedAt": "{}에 업로드됨", - "linkedAt": "{}에 링크 추가됨", - "failedToOpenMsg": "열지 못했습니다. 파일을 찾을 수 없습니다" - }, - "subPage": { - "handlingPasteHint": " - (붙여넣기 처리 중)", - "errors": { - "failedDeletePage": "페이지 삭제 실패", - "failedCreatePage": "페이지 생성 실패", - "failedMovePage": "이 문서로 페이지 이동 실패", - "failedDuplicatePage": "페이지 복제 실패", - "failedDuplicateFindView": "페이지 복제 실패 - 원본 보기를 찾을 수 없습니다" - } - }, - "cannotMoveToItsChildren": "자식으로 이동할 수 없습니다" - }, - "outlineBlock": { - "placeholder": "목차" + "addHeadingToCreateOutline": "제목을 추가하여 목차를 만듭니다." + } }, "textBlock": { - "placeholder": "명령어를 입력하려면 '/'를 입력하세요" + "placeholder": "명령에 '/' 입력" }, "title": { - "placeholder": "제목 없음" + "placeholder": "무제" }, "imageBlock": { - "placeholder": "이미지 추가하려면 클릭", + "placeholder": "이미지를 추가하려면 클릭하세요.", "upload": { "label": "업로드", - "placeholder": "이미지 업로드하려면 클릭" + "placeholder": "이미지를 업로드하려면 클릭하세요." }, "url": { "label": "이미지 URL", "placeholder": "이미지 URL 입력" }, - "ai": { - "label": "AI로 이미지 생성", - "placeholder": "AI가 이미지를 생성할 프롬프트를 입력하세요" - }, - "stability_ai": { - "label": "Stability AI로 이미지 생성", - "placeholder": "Stability AI가 이미지를 생성할 프롬프트를 입력하세요" - }, "support": "이미지 크기 제한은 5MB입니다. 지원되는 형식: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "잘못된 이미지", - "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다", - "invalidImageFormat": "지원되지 않는 이미지 형식입니다. 지원되는 형식: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "잘못된 이미지 URL", - "noImage": "파일 또는 디렉토리가 없습니다", - "multipleImagesFailed": "하나 이상의 이미지 업로드 실패, 다시 시도하세요" - }, - "embedLink": { - "label": "링크 삽입", - "placeholder": "이미지 링크를 붙여넣거나 입력하세요" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "이미지 검색", - "pleaseInputYourOpenAIKey": "설정 페이지에서 AI 키를 입력하세요", - "saveImageToGallery": "이미지 저장", - "failedToAddImageToGallery": "이미지 저장 실패", - "successToAddImageToGallery": "이미지가 사진에 저장되었습니다", - "unableToLoadImage": "이미지를 로드할 수 없습니다", - "maximumImageSize": "최대 지원 업로드 이미지 크기는 10MB입니다", - "uploadImageErrorImageSizeTooBig": "이미지 크기는 10MB 미만이어야 합니다", - "imageIsUploading": "이미지 업로드 중", - "openFullScreen": "전체 화면으로 열기", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "이전 이미지", - "nextImageTooltip": "다음 이미지", - "zoomOutTooltip": "축소", - "zoomInTooltip": "확대", - "changeZoomLevelTooltip": "확대/축소 수준 변경", - "openLocalImage": "이미지 열기", - "downloadImage": "이미지 다운로드", - "closeViewer": "인터랙티브 뷰어 닫기", - "scalePercentage": "{}%", - "deleteImageTooltip": "이미지 삭제" - } + "invalidImageSize": "이미지 크기는 5MB 미만이어야 합니다.", + "invalidImageFormat": "이미지 형식은 지원되지 않습니다. 지원되는 형식: JPEG, PNG, GIF, SVG", + "invalidImageUrl": "잘못된 이미지 URL" } }, "codeBlock": { "language": { "label": "언어", - "placeholder": "언어 선택", - "auto": "자동" - }, - "copyTooltip": "복사", - "searchLanguageHint": "언어 검색", - "codeCopiedSnackbar": "코드가 클립보드에 복사되었습니다!" + "placeholder": "언어 선택" + } }, "inlineLink": { - "placeholder": "링크를 붙여넣거나 입력하세요", - "openInNewTab": "새 탭에서 열기", - "copyLink": "링크 복사", - "removeLink": "링크 제거", + "placeholder": "링크 붙여넣기 또는 입력", "url": { "label": "링크 URL", "placeholder": "링크 URL 입력" @@ -2087,1101 +541,61 @@ "label": "링크 제목", "placeholder": "링크 제목 입력" } - }, - "mention": { - "placeholder": "사람, 페이지 또는 날짜 언급...", - "page": { - "label": "페이지로 연결", - "tooltip": "페이지 열기" - }, - "deleted": "삭제됨", - "deletedContent": "이 콘텐츠는 존재하지 않거나 삭제되었습니다", - "noAccess": "액세스 불가", - "deletedPage": "삭제된 페이지", - "trashHint": " - 휴지통에 있음", - "morePages": "더 많은 페이지" - }, - "toolbar": { - "resetToDefaultFont": "기본값으로 재설정", - "textSize": "텍스트 크기", - "h1": "헤딩 1", - "h2": "헤딩 2", - "h3": "헤딩 3", - "alignLeft": "왼쪽 정렬", - "alignRight": "오른쪽 정렬", - "alignCenter": "가운데 정렬", - "link": "링크", - "textAlign": "텍스트 정렬", - "moreOptions": "더 많은 옵션", - "font": "글꼴", - "suggestions": "제안", - "turnInto": "변환" - }, - "errorBlock": { - "theBlockIsNotSupported": "블록 콘텐츠를 구문 분석할 수 없습니다", - "clickToCopyTheBlockContent": "블록 콘텐츠를 복사하려면 클릭", - "blockContentHasBeenCopied": "블록 콘텐츠가 복사되었습니다.", - "parseError": "{} 블록을 구문 분석하는 동안 오류가 발생했습니다.", - "copyBlockContent": "블록 콘텐츠 복사" - }, - "mobilePageSelector": { - "title": "페이지 선택", - "failedToLoad": "페이지 목록을 로드하지 못했습니다", - "noPagesFound": "페이지를 찾을 수 없습니다" - }, - "attachmentMenu": { - "choosePhoto": "사진 선택", - "takePicture": "사진 찍기", - "chooseFile": "파일 선택" } }, "board": { "column": { - "label": "열", - "createNewCard": "새로 만들기", - "renameGroupTooltip": "그룹 이름 변경", - "createNewColumn": "새 그룹 추가", - "addToColumnTopTooltip": "맨 위에 새 카드 추가", - "addToColumnBottomTooltip": "맨 아래에 새 카드 추가", - "renameColumn": "이름 변경", - "hideColumn": "숨기기", - "newGroup": "새 그룹", - "deleteColumn": "삭제", - "deleteColumnConfirmation": "이 그룹과 그룹 내 모든 카드를 삭제합니다. 계속하시겠습니까?" + "createNewCard": "추가" }, - "hiddenGroupSection": { - "sectionTitle": "숨겨진 그룹", - "collapseTooltip": "숨겨진 그룹 숨기기", - "expandTooltip": "숨겨진 그룹 보기" - }, - "cardDetail": "카드 세부 정보", - "cardActions": "카드 작업", - "cardDuplicated": "카드가 복제되었습니다", - "cardDeleted": "카드가 삭제되었습니다", - "showOnCard": "카드 세부 정보에 표시", - "setting": "설정", - "propertyName": "속성 이름", - "menuName": "보드", - "showUngrouped": "그룹화되지 않은 항목 표시", - "ungroupedButtonText": "그룹화되지 않음", - "ungroupedButtonTooltip": "어떤 그룹에도 속하지 않는 카드가 포함되어 있습니다", - "ungroupedItemsTitle": "보드에 추가하려면 클릭", - "groupBy": "그룹 기준", - "groupCondition": "그룹 조건", - "referencedBoardPrefix": "보기", - "notesTooltip": "내부에 노트 있음", + "menuName": "판자", + "referencedBoardPrefix": "관점", "mobile": { - "editURL": "URL 편집", "showGroup": "그룹 표시", "showGroupContent": "이 그룹을 보드에 표시하시겠습니까?", - "failedToLoad": "보드 보기를 로드하지 못했습니다" - }, - "dateCondition": { - "weekOf": "{} - {} 주", - "today": "오늘", - "yesterday": "어제", - "tomorrow": "내일", - "lastSevenDays": "지난 7일", - "nextSevenDays": "다음 7일", - "lastThirtyDays": "지난 30일", - "nextThirtyDays": "다음 30일" - }, - "noGroup": "그룹화할 속성 없음", - "noGroupDesc": "보드 보기를 표시하려면 그룹화할 속성이 필요합니다", - "media": { - "cardText": "{} {}", - "fallbackName": "파일" + "failedToLoad": "보드 보기를 로드하지 못했습니다." } }, "calendar": { - "menuName": "캘린더", - "defaultNewCalendarTitle": "제목 없음", - "newEventButtonTooltip": "새 이벤트 추가", + "menuName": "달력", + "defaultNewCalendarTitle": "무제", "navigation": { "today": "오늘", "jumpToday": "오늘로 이동", - "previousMonth": "이전 달", - "nextMonth": "다음 달", - "views": { - "day": "일", - "week": "주", - "month": "월", - "year": "년" - } - }, - "mobileEventScreen": { - "emptyTitle": "이벤트 없음", - "emptyBody": "이 날에 이벤트를 생성하려면 더하기 버튼을 누르세요." + "previousMonth": "지난달", + "nextMonth": "다음 달" }, "settings": { "showWeekNumbers": "주 번호 표시", - "showWeekends": "주말 표시", - "firstDayOfWeek": "주 시작일", - "layoutDateField": "캘린더 레이아웃 기준", - "changeLayoutDateField": "레이아웃 필드 변경", + "showWeekends": "주말 보기", + "firstDayOfWeek": "주 시작", + "layoutDateField": "레이아웃 캘린더", "noDateTitle": "날짜 없음", - "noDateHint": { - "zero": "일정이 없는 이벤트가 여기에 표시됩니다", - "one": "{count}개의 일정이 없는 이벤트", - "other": "{count}개의 일정이 없는 이벤트" - }, - "unscheduledEventsTitle": "일정이 없는 이벤트", - "clickToAdd": "캘린더에 추가하려면 클릭", - "name": "캘린더 설정", - "clickToOpen": "레코드를 열려면 클릭" + "clickToAdd": "캘린더에 추가하려면 클릭하세요.", + "name": "달력 레이아웃", + "noDateHint": "예약되지 않은 일정이 여기에 표시됩니다." }, - "referencedCalendarPrefix": "보기", - "quickJumpYear": "이동", - "duplicateEvent": "이벤트 복제" + "referencedCalendarPrefix": "관점" }, "errorDialog": { "title": "@:appName 오류", - "howToFixFallback": "불편을 드려 죄송합니다! GitHub 페이지에 오류를 설명하는 문제를 제출하세요.", - "howToFixFallbackHint1": "불편을 드려 죄송합니다! ", - "howToFixFallbackHint2": " 페이지에 오류를 설명하는 문제를 제출하세요.", + "howToFixFallback": "불편을 끼쳐드려 죄송합니다! 오류를 설명하는 문제를 GitHub 페이지에 제출하세요.", "github": "GitHub에서 보기" }, "search": { - "label": "검색", - "sidebarSearchIcon": "검색하고 페이지로 빠르게 이동", + "label": "찾다", "placeholder": { - "actions": "작업 검색..." + "actions": "검색 작업..." } }, "message": { "copy": { - "success": "복사됨!", + "success": "복사했습니다!", "fail": "복사할 수 없음" } }, - "unSupportBlock": "현재 버전에서는 이 블록을 지원하지 않습니다.", + "unSupportBlock": "현재 버전은 이 블록을 지원하지 않습니다.", "views": { "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." - }, - "colors": { - "custom": "사용자 정의", - "default": "기본", - "red": "빨간색", - "orange": "주황색", - "yellow": "노란색", - "green": "녹색", - "blue": "파란색", - "purple": "보라색", - "pink": "분홍색", - "brown": "갈색", - "gray": "회색" - }, - "emoji": { - "emojiTab": "이모지", - "search": "이모지 검색", - "noRecent": "최근 사용한 이모지 없음", - "noEmojiFound": "이모지를 찾을 수 없음", - "filter": "필터", - "random": "무작위", - "selectSkinTone": "피부 톤 선택", - "remove": "이모지 제거", - "categories": { - "smileys": "스마일리 및 감정", - "people": "사람", - "animals": "자연", - "food": "음식", - "activities": "활동", - "places": "장소", - "objects": "사물", - "symbols": "기호", - "flags": "깃발", - "nature": "자연", - "frequentlyUsed": "자주 사용됨" - }, - "skinTone": { - "default": "기본", - "light": "밝은", - "mediumLight": "중간 밝은", - "medium": "중간", - "mediumDark": "중간 어두운", - "dark": "어두운" - }, - "openSourceIconsFrom": "오픈 소스 아이콘 제공" - }, - "inlineActions": { - "noResults": "결과 없음", - "recentPages": "최근 페이지", - "pageReference": "페이지 참조", - "docReference": "문서 참조", - "boardReference": "보드 참조", - "calReference": "캘린더 참조", - "gridReference": "그리드 참조", - "date": "날짜", - "reminder": { - "groupTitle": "알림", - "shortKeyword": "알림" - }, - "createPage": "\"{}\" 하위 페이지 생성" - }, - "datePicker": { - "dateTimeFormatTooltip": "설정에서 날짜 및 시간 형식 변경", - "dateFormat": "날짜 형식", - "includeTime": "시간 포함", - "isRange": "종료 날짜", - "timeFormat": "시간 형식", - "clearDate": "날짜 지우기", - "reminderLabel": "알림", - "selectReminder": "알림 선택", - "reminderOptions": { - "none": "없음", - "atTimeOfEvent": "이벤트 시간", - "fiveMinsBefore": "5분 전", - "tenMinsBefore": "10분 전", - "fifteenMinsBefore": "15분 전", - "thirtyMinsBefore": "30분 전", - "oneHourBefore": "1시간 전", - "twoHoursBefore": "2시간 전", - "onDayOfEvent": "이벤트 당일", - "oneDayBefore": "1일 전", - "twoDaysBefore": "2일 전", - "oneWeekBefore": "1주일 전", - "custom": "사용자 정의" - } - }, - "relativeDates": { - "yesterday": "어제", - "today": "오늘", - "tomorrow": "내일", - "oneWeek": "1주일" - }, - "notificationHub": { - "title": "알림", - "mobile": { - "title": "업데이트" - }, - "emptyTitle": "모두 확인했습니다!", - "emptyBody": "대기 중인 알림이나 작업이 없습니다. 평온을 즐기세요.", - "tabs": { - "inbox": "받은 편지함", - "upcoming": "다가오는" - }, - "actions": { - "markAllRead": "모두 읽음으로 표시", - "showAll": "모두", - "showUnreads": "읽지 않음" - }, - "filters": { - "ascending": "오름차순", - "descending": "내림차순", - "groupByDate": "날짜별 그룹", - "showUnreadsOnly": "읽지 않은 항목만 표시", - "resetToDefault": "기본값으로 재설정" - } - }, - "reminderNotification": { - "title": "알림", - "message": "잊기 전에 확인하세요!", - "tooltipDelete": "삭제", - "tooltipMarkRead": "읽음으로 표시", - "tooltipMarkUnread": "읽지 않음으로 표시" - }, - "findAndReplace": { - "find": "찾기", - "previousMatch": "이전 일치 항목", - "nextMatch": "다음 일치 항목", - "close": "닫기", - "replace": "교체", - "replaceAll": "모두 교체", - "noResult": "결과 없음", - "caseSensitive": "대소문자 구분", - "searchMore": "더 많은 결과를 찾으려면 검색" - }, - "error": { - "weAreSorry": "죄송합니다", - "loadingViewError": "이 보기를 로드하는 데 문제가 있습니다. 인터넷 연결을 확인하고 앱을 새로 고침하세요. 문제가 계속되면 팀에 문의하세요.", - "syncError": "다른 장치에서 데이터가 동기화되지 않음", - "syncErrorHint": "마지막으로 편집한 장치에서 이 페이지를 다시 열고 현재 장치에서 다시 열어보세요.", - "clickToCopy": "오류 코드를 복사하려면 클릭" - }, - "editor": { - "bold": "굵게", - "bulletedList": "글머리 기호 목록", - "bulletedListShortForm": "글머리 기호", - "checkbox": "체크박스", - "embedCode": "코드 삽입", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "강조", - "color": "색상", - "image": "이미지", - "date": "날짜", - "page": "페이지", - "italic": "기울임꼴", - "link": "링크", - "numberedList": "번호 매기기 목록", - "numberedListShortForm": "번호 매기기", - "toggleHeading1ShortForm": "토글 h1", - "toggleHeading2ShortForm": "토글 h2", - "toggleHeading3ShortForm": "토글 h3", - "quote": "인용", - "strikethrough": "취소선", - "text": "텍스트", - "underline": "밑줄", - "fontColorDefault": "기본", - "fontColorGray": "회색", - "fontColorBrown": "갈색", - "fontColorOrange": "주황색", - "fontColorYellow": "노란색", - "fontColorGreen": "녹색", - "fontColorBlue": "파란색", - "fontColorPurple": "보라색", - "fontColorPink": "분홍색", - "fontColorRed": "빨간색", - "backgroundColorDefault": "기본 배경", - "backgroundColorGray": "회색 배경", - "backgroundColorBrown": "갈색 배경", - "backgroundColorOrange": "주황색 배경", - "backgroundColorYellow": "노란색 배경", - "backgroundColorGreen": "녹색 배경", - "backgroundColorBlue": "파란색 배경", - "backgroundColorPurple": "보라색 배경", - "backgroundColorPink": "분홍색 배경", - "backgroundColorRed": "빨간색 배경", - "backgroundColorLime": "라임색 배경", - "backgroundColorAqua": "청록색 배경", - "done": "완료", - "cancel": "취소", - "tint1": "색조 1", - "tint2": "색조 2", - "tint3": "색조 3", - "tint4": "색조 4", - "tint5": "색조 5", - "tint6": "색조 6", - "tint7": "색조 7", - "tint8": "색조 8", - "tint9": "색조 9", - "lightLightTint1": "보라색", - "lightLightTint2": "분홍색", - "lightLightTint3": "연분홍색", - "lightLightTint4": "주황색", - "lightLightTint5": "노란색", - "lightLightTint6": "라임색", - "lightLightTint7": "녹색", - "lightLightTint8": "청록색", - "lightLightTint9": "파란색", - "urlHint": "URL", - "mobileHeading1": "헤딩 1", - "mobileHeading2": "헤딩 2", - "mobileHeading3": "헤딩 3", - "mobileHeading4": "헤딩 4", - "mobileHeading5": "헤딩 5", - "mobileHeading6": "헤딩 6", - "textColor": "텍스트 색상", - "backgroundColor": "배경 색상", - "addYourLink": "링크 추가", - "openLink": "링크 열기", - "copyLink": "링크 복사", - "removeLink": "링크 제거", - "editLink": "링크 편집", - "linkText": "텍스트", - "linkTextHint": "텍스트를 입력하세요", - "linkAddressHint": "URL을 입력하세요", - "highlightColor": "강조 색상", - "clearHighlightColor": "강조 색상 지우기", - "customColor": "사용자 정의 색상", - "hexValue": "16진수 값", - "opacity": "불투명도", - "resetToDefaultColor": "기본 색상으로 재설정", - "ltr": "LTR", - "rtl": "RTL", - "auto": "자동", - "cut": "잘라내기", - "copy": "복사", - "paste": "붙여넣기", - "find": "찾기", - "select": "선택", - "selectAll": "모두 선택", - "previousMatch": "이전 일치 항목", - "nextMatch": "다음 일치 항목", - "closeFind": "닫기", - "replace": "교체", - "replaceAll": "모두 교체", - "regex": "정규식", - "caseSensitive": "대소문자 구분", - "uploadImage": "이미지 업로드", - "urlImage": "URL 이미지", - "incorrectLink": "잘못된 링크", - "upload": "업로드", - "chooseImage": "이미지 선택", - "loading": "로드 중", - "imageLoadFailed": "이미지 로드 실패", - "divider": "구분선", - "table": "테이블", - "colAddBefore": "앞에 추가", - "rowAddBefore": "앞에 추가", - "colAddAfter": "뒤에 추가", - "rowAddAfter": "뒤에 추가", - "colRemove": "제거", - "rowRemove": "제거", - "colDuplicate": "복제", - "rowDuplicate": "복제", - "colClear": "내용 지우기", - "rowClear": "내용 지우기", - "slashPlaceHolder": "'/'를 입력하여 블록을 삽입하거나 입력 시작", - "typeSomething": "무언가 입력...", - "toggleListShortForm": "토글", - "quoteListShortForm": "인용", - "mathEquationShortForm": "수식", - "codeBlockShortForm": "코드" - }, - "favorite": { - "noFavorite": "즐겨찾기 페이지 없음", - "noFavoriteHintText": "페이지를 왼쪽으로 스와이프하여 즐겨찾기에 추가하세요", - "removeFromSidebar": "사이드바에서 제거", - "addToSidebar": "사이드바에 고정" - }, - "cardDetails": { - "notesPlaceholder": "/를 입력하여 블록을 삽입하거나 입력 시작" - }, - "blockPlaceholders": { - "todoList": "할 일", - "bulletList": "목록", - "numberList": "목록", - "quote": "인용", - "heading": "헤딩 {}" - }, - "titleBar": { - "pageIcon": "페이지 아이콘", - "language": "언어", - "font": "글꼴", - "actions": "작업", - "date": "날짜", - "addField": "필드 추가", - "userIcon": "사용자 아이콘" - }, - "noLogFiles": "로그 파일이 없습니다", - "newSettings": { - "myAccount": { - "title": "내 계정", - "subtitle": "프로필을 사용자 정의하고, 계정 보안을 관리하고, AI 키를 열거나 계정에 로그인하세요.", - "profileLabel": "계정 이름 및 프로필 이미지", - "profileNamePlaceholder": "이름 입력", - "accountSecurity": "계정 보안", - "2FA": "2단계 인증", - "aiKeys": "AI 키", - "accountLogin": "계정 로그인", - "updateNameError": "이름 업데이트 실패", - "updateIconError": "아이콘 업데이트 실패", - "aboutAppFlowy": "@:appName 정보", - "deleteAccount": { - "title": "계정 삭제", - "subtitle": "계정과 모든 데이터를 영구적으로 삭제합니다.", - "description": "계정을 영구적으로 삭제하고 모든 작업 공간에서 액세스를 제거합니다.", - "deleteMyAccount": "내 계정 삭제", - "dialogTitle": "계정 삭제", - "dialogContent1": "계정을 영구적으로 삭제하시겠습니까?", - "dialogContent2": "이 작업은 되돌릴 수 없으며, 모든 작업 공간에서 액세스를 제거하고, 개인 작업 공간을 포함한 전체 계정을 삭제하며, 모든 공유 작업 공간에서 제거됩니다.", - "confirmHint1": "\"@:newSettings.myAccount.deleteAccount.confirmHint3\"를 입력하여 확인하세요.", - "confirmHint2": "이 작업은 되돌릴 수 없으며, 계정과 모든 관련 데이터를 영구적으로 삭제합니다.", - "confirmHint3": "내 계정 삭제", - "checkToConfirmError": "삭제를 확인하려면 확인란을 선택해야 합니다", - "failedToGetCurrentUser": "현재 사용자 이메일을 가져오지 못했습니다", - "confirmTextValidationFailed": "확인 텍스트가 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"와 일치하지 않습니다", - "deleteAccountSuccess": "계정이 성공적으로 삭제되었습니다" - } - }, - "workplace": { - "name": "작업 공간", - "title": "작업 공간 설정", - "subtitle": "작업 공간의 외관, 테마, 글꼴, 텍스트 레이아웃, 날짜, 시간 및 언어를 사용자 정의합니다.", - "workplaceName": "작업 공간 이름", - "workplaceNamePlaceholder": "작업 공간 이름 입력", - "workplaceIcon": "작업 공간 아이콘", - "workplaceIconSubtitle": "작업 공간에 대한 이미지를 업로드하거나 이모지를 사용하세요. 아이콘은 사이드바와 알림에 표시됩니다.", - "renameError": "작업 공간 이름 변경 실패", - "updateIconError": "아이콘 업데이트 실패", - "chooseAnIcon": "아이콘 선택", - "appearance": { - "name": "외관", - "themeMode": { - "auto": "자동", - "light": "라이트", - "dark": "다크" - }, - "language": "언어" - } - }, - "syncState": { - "syncing": "동기화 중", - "synced": "동기화됨", - "noNetworkConnected": "네트워크에 연결되지 않음" - } - }, - "pageStyle": { - "title": "페이지 스타일", - "layout": "레이아웃", - "coverImage": "표지 이미지", - "pageIcon": "페이지 아이콘", - "colors": "색상", - "gradient": "그라데이션", - "backgroundImage": "배경 이미지", - "presets": "프리셋", - "photo": "사진", - "unsplash": "Unsplash", - "pageCover": "페이지 표지", - "none": "없음", - "openSettings": "설정 열기", - "photoPermissionTitle": "@:appName가 사진 라이브러리에 접근하려고 합니다", - "photoPermissionDescription": "@:appName가 문서에 이미지를 추가할 수 있도록 사진에 접근해야 합니다", - "cameraPermissionTitle": "@:appName가 카메라에 접근하려고 합니다", - "cameraPermissionDescription": "카메라에서 문서에 이미지를 추가하려면 @:appName가 카메라에 액세스할 수 있어야 합니다.", - "doNotAllow": "허용하지 않음", - "image": "이미지" - }, - "commandPalette": { - "placeholder": "검색하거나 질문하세요...", - "bestMatches": "최고의 일치 항목", - "recentHistory": "최근 기록", - "navigateHint": "탐색하려면", - "loadingTooltip": "결과를 찾고 있습니다...", - "betaLabel": "베타", - "betaTooltip": "현재 문서의 페이지 및 콘텐츠 검색만 지원합니다", - "fromTrashHint": "휴지통에서", - "noResultsHint": "찾고 있는 항목을 찾지 못했습니다. 다른 용어로 검색해 보세요.", - "clearSearchTooltip": "검색 필드 지우기" - }, - "space": { - "delete": "삭제", - "deleteConfirmation": "삭제: ", - "deleteConfirmationDescription": "이 공간 내의 모든 페이지가 삭제되어 휴지통으로 이동되며, 게시한 모든 페이지가 게시 취소됩니다.", - "rename": "공간 이름 변경", - "changeIcon": "아이콘 변경", - "manage": "공간 관리", - "addNewSpace": "새 공간 생성", - "collapseAllSubPages": "모든 하위 페이지 접기", - "createNewSpace": "새 공간 생성", - "createSpaceDescription": "작업을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.", - "spaceName": "공간 이름", - "spaceNamePlaceholder": "예: 마케팅, 엔지니어링, 인사", - "permission": "공간 권한", - "publicPermission": "공용", - "publicPermissionDescription": "전체 액세스 권한이 있는 모든 작업 공간 멤버", - "privatePermission": "비공개", - "privatePermissionDescription": "이 공간에 접근할 수 있는 사람은 본인뿐입니다", - "spaceIconBackground": "배경 색상", - "spaceIcon": "아이콘", - "dangerZone": "위험 구역", - "unableToDeleteLastSpace": "마지막 공간을 삭제할 수 없습니다", - "unableToDeleteSpaceNotCreatedByYou": "다른 사람이 생성한 공간을 삭제할 수 없습니다", - "enableSpacesForYourWorkspace": "작업 공간에 공간을 활성화하세요", - "title": "공간", - "defaultSpaceName": "일반", - "upgradeSpaceTitle": "공간 활성화", - "upgradeSpaceDescription": "작업 공간을 더 잘 조직하기 위해 여러 공용 및 비공개 공간을 생성하세요.", - "upgrade": "업데이트", - "upgradeYourSpace": "여러 공간 생성", - "quicklySwitch": "다음 공간으로 빠르게 전환", - "duplicate": "공간 복제", - "movePageToSpace": "페이지를 공간으로 이동", - "cannotMovePageToDatabase": "페이지를 데이터베이스로 이동할 수 없습니다", - "switchSpace": "공간 전환", - "spaceNameCannotBeEmpty": "공간 이름은 비워둘 수 없습니다", - "success": { - "deleteSpace": "공간이 성공적으로 삭제되었습니다", - "renameSpace": "공간 이름이 성공적으로 변경되었습니다", - "duplicateSpace": "공간이 성공적으로 복제되었습니다", - "updateSpace": "공간이 성공적으로 업데이트되었습니다" - }, - "error": { - "deleteSpace": "공간 삭제 실패", - "renameSpace": "공간 이름 변경 실패", - "duplicateSpace": "공간 복제 실패", - "updateSpace": "공간 업데이트 실패" - }, - "createSpace": "공간 생성", - "manageSpace": "공간 관리", - "renameSpace": "공간 이름 변경", - "mSpaceIconColor": "공간 아이콘 색상", - "mSpaceIcon": "공간 아이콘" - }, - "publish": { - "hasNotBeenPublished": "이 페이지는 아직 게시되지 않았습니다", - "spaceHasNotBeenPublished": "아직 공간 게시를 지원하지 않습니다", - "reportPage": "페이지 신고", - "databaseHasNotBeenPublished": "데이터베이스 게시를 아직 지원하지 않습니다.", - "createdWith": "제작", - "downloadApp": "AppFlowy 다운로드", - "copy": { - "codeBlock": "코드 블록의 내용이 클립보드에 복사되었습니다", - "imageBlock": "이미지 링크가 클립보드에 복사되었습니다", - "mathBlock": "수학 방정식이 클립보드에 복사되었습니다", - "fileBlock": "파일 링크가 클립보드에 복사되었습니다" - }, - "containsPublishedPage": "이 페이지에는 하나 이상의 게시된 페이지가 포함되어 있습니다. 계속하면 게시가 취소됩니다. 삭제를 진행하시겠습니까?", - "publishSuccessfully": "성공적으로 게시되었습니다", - "unpublishSuccessfully": "성공적으로 게시 취소되었습니다", - "publishFailed": "게시 실패", - "unpublishFailed": "게시 취소 실패", - "noAccessToVisit": "이 페이지에 접근할 수 없습니다...", - "createWithAppFlowy": "AppFlowy로 웹사이트 만들기", - "fastWithAI": "AI로 빠르고 쉽게.", - "tryItNow": "지금 시도해보세요", - "onlyGridViewCanBePublished": "그리드 보기만 게시할 수 있습니다", - "database": { - "zero": "선택한 {} 보기 게시", - "one": "선택한 {} 보기 게시", - "many": "선택한 {} 보기 게시", - "other": "선택한 {} 보기 게시" - }, - "mustSelectPrimaryDatabase": "기본 보기를 선택해야 합니다", - "noDatabaseSelected": "선택된 데이터베이스가 없습니다. 최소 하나의 데이터베이스를 선택하세요.", - "unableToDeselectPrimaryDatabase": "기본 보기를 선택 해제할 수 없습니다", - "saveThisPage": "이 템플릿으로 시작", - "duplicateTitle": "추가할 위치 선택", - "selectWorkspace": "작업 공간 선택", - "addTo": "추가", - "duplicateSuccessfully": "작업 공간에 추가되었습니다", - "duplicateSuccessfullyDescription": "AppFlowy가 설치되어 있지 않습니까? '다운로드'를 클릭하면 다운로드가 자동으로 시작됩니다.", - "downloadIt": "다운로드", - "openApp": "앱에서 열기", - "duplicateFailed": "복제 실패", - "membersCount": { - "zero": "멤버 없음", - "one": "1명의 멤버", - "many": "{count}명의 멤버", - "other": "{count}명의 멤버" - }, - "useThisTemplate": "템플릿 사용" - }, - "web": { - "continue": "계속", - "or": "또는", - "continueWithGoogle": "Google로 계속", - "continueWithGithub": "GitHub로 계속", - "continueWithDiscord": "Discord로 계속", - "continueWithApple": "Apple로 계속", - "moreOptions": "더 많은 옵션", - "collapse": "접기", - "signInAgreement": "\"계속\"을 클릭하면 AppFlowy의", - "and": "및", - "termOfUse": "이용 약관", - "privacyPolicy": "개인정보 보호정책", - "signInError": "로그인 오류", - "login": "가입 또는 로그인", - "fileBlock": { - "uploadedAt": "{time}에 업로드됨", - "linkedAt": "{time}에 링크 추가됨", - "empty": "파일 업로드 또는 삽입", - "uploadFailed": "업로드 실패, 다시 시도하세요", - "retry": "다시 시도" - }, - "importNotion": "Notion에서 가져오기", - "import": "가져오기", - "importSuccess": "성공적으로 업로드되었습니다", - "importSuccessMessage": "가져오기가 완료되면 알림을 받게 됩니다. 이후 사이드바에서 가져온 페이지를 확인할 수 있습니다.", - "importFailed": "가져오기 실패, 파일 형식을 확인하세요", - "dropNotionFile": "Notion zip 파일을 여기에 드롭하여 업로드하거나 클릭하여 찾아보기", - "error": { - "pageNameIsEmpty": "페이지 이름이 비어 있습니다. 다른 이름을 시도하세요" - } - }, - "globalComment": { - "comments": "댓글", - "addComment": "댓글 추가", - "reactedBy": "반응한 사람", - "addReaction": "반응 추가", - "reactedByMore": "및 {count}명", - "showSeconds": { - "one": "1초 전", - "other": "{count}초 전", - "zero": "방금", - "many": "{count}초 전" - }, - "showMinutes": { - "one": "1분 전", - "other": "{count}분 전", - "many": "{count}분 전" - }, - "showHours": { - "one": "1시간 전", - "other": "{count}시간 전", - "many": "{count}시간 전" - }, - "showDays": { - "one": "1일 전", - "other": "{count}일 전", - "many": "{count}일 전" - }, - "showMonths": { - "one": "1개월 전", - "other": "{count}개월 전", - "many": "{count}개월 전" - }, - "showYears": { - "one": "1년 전", - "other": "{count}년 전", - "many": "{count}년 전" - }, - "reply": "답글", - "deleteComment": "댓글 삭제", - "youAreNotOwner": "이 댓글의 소유자가 아닙니다", - "confirmDeleteDescription": "이 댓글을 삭제하시겠습니까?", - "hasBeenDeleted": "삭제됨", - "replyingTo": "답글 대상", - "noAccessDeleteComment": "이 댓글을 삭제할 수 없습니다", - "collapse": "접기", - "readMore": "더 읽기", - "failedToAddComment": "댓글 추가 실패", - "commentAddedSuccessfully": "댓글이 성공적으로 추가되었습니다.", - "commentAddedSuccessTip": "댓글을 추가하거나 답글을 달았습니다. 최신 댓글을 보려면 상단으로 이동하시겠습니까?" - }, - "template": { - "asTemplate": "템플릿으로 저장", - "name": "템플릿 이름", - "description": "템플릿 설명", - "about": "템플릿 정보", - "deleteFromTemplate": "템플릿에서 삭제", - "preview": "템플릿 미리보기", - "categories": "템플릿 카테고리", - "isNewTemplate": "새 템플릿에 고정", - "featured": "추천에 고정", - "relatedTemplates": "관련 템플릿", - "requiredField": "{field}은(는) 필수 항목입니다", - "addCategory": "\"{category}\" 추가", - "addNewCategory": "새 카테고리 추가", - "addNewCreator": "새 제작자 추가", - "deleteCategory": "카테고리 삭제", - "editCategory": "카테고리 편집", - "editCreator": "제작자 편집", - "category": { - "name": "카테고리 이름", - "icon": "카테고리 아이콘", - "bgColor": "카테고리 배경 색상", - "priority": "카테고리 우선순위", - "desc": "카테고리 설명", - "type": "카테고리 유형", - "icons": "카테고리 아이콘", - "colors": "카테고리 색상", - "byUseCase": "사용 사례별", - "byFeature": "기능별", - "deleteCategory": "카테고리 삭제", - "deleteCategoryDescription": "이 카테고리를 삭제하시겠습니까?", - "typeToSearch": "카테고리 검색..." - }, - "creator": { - "label": "템플릿 제작자", - "name": "제작자 이름", - "avatar": "제작자 아바타", - "accountLinks": "제작자 계정 링크", - "uploadAvatar": "아바타 업로드", - "deleteCreator": "제작자 삭제", - "deleteCreatorDescription": "이 제작자를 삭제하시겠습니까?", - "typeToSearch": "제작자 검색..." - }, - "uploadSuccess": "템플릿이 성공적으로 업로드되었습니다", - "uploadSuccessDescription": "템플릿이 성공적으로 업로드되었습니다. 이제 템플릿 갤러리에서 확인할 수 있습니다.", - "viewTemplate": "템플릿 보기", - "deleteTemplate": "템플릿 삭제", - "deleteSuccess": "템플릿이 성공적으로 삭제되었습니다", - "deleteTemplateDescription": "현재 페이지나 게시 상태에는 영향을 미치지 않습니다. 이 템플릿을 삭제하시겠습니까?", - "addRelatedTemplate": "관련 템플릿 추가", - "removeRelatedTemplate": "관련 템플릿 제거", - "uploadAvatar": "아바타 업로드", - "searchInCategory": "{category}에서 검색", - "label": "템플릿" - }, - "fileDropzone": { - "dropFile": "업로드하려면 파일을 클릭하거나 드래그하세요", - "uploading": "업로드 중...", - "uploadFailed": "업로드 실패", - "uploadSuccess": "업로드 성공", - "uploadSuccessDescription": "파일이 성공적으로 업로드되었습니다", - "uploadFailedDescription": "파일 업로드 실패", - "uploadingDescription": "파일이 업로드 중입니다" - }, - "gallery": { - "preview": "전체 화면으로 열기", - "copy": "복사", - "download": "다운로드", - "prev": "이전", - "next": "다음", - "resetZoom": "확대/축소 재설정", - "zoomIn": "확대", - "zoomOut": "축소" - }, - "invitation": { - "join": "참여", - "on": "에", - "invitedBy": "초대자", - "membersCount": { - "zero": "{count}명의 멤버", - "one": "{count}명의 멤버", - "many": "{count}명의 멤버", - "other": "{count}명의 멤버" - }, - "tip": "아래 연락처 정보로 이 작업 공간에 참여하도록 초대되었습니다. 정보가 잘못된 경우 관리자에게 연락하여 초대를 다시 보내달라고 요청하세요.", - "joinWorkspace": "작업 공간 참여", - "success": "작업 공간에 성공적으로 참여했습니다", - "successMessage": "이제 작업 공간 내의 모든 페이지와 작업 공간에 접근할 수 있습니다.", - "openWorkspace": "AppFlowy 열기", - "alreadyAccepted": "이미 초대를 수락했습니다", - "errorModal": { - "title": "문제가 발생했습니다", - "description": "현재 계정 {email}이(가) 이 작업 공간에 접근할 수 없을 수 있습니다. 올바른 계정으로 로그인하거나 작업 공간 소유자에게 도움을 요청하세요.", - "contactOwner": "소유자에게 연락", - "close": "홈으로 돌아가기", - "changeAccount": "계정 변경" - } - }, - "requestAccess": { - "title": "이 페이지에 접근할 수 없습니다", - "subtitle": "이 페이지의 소유자에게 접근을 요청할 수 있습니다. 승인되면 페이지를 볼 수 있습니다.", - "requestAccess": "접근 요청", - "backToHome": "홈으로 돌아가기", - "tip": "현재 <link/>로 로그인 중입니다.", - "mightBe": "다른 계정으로 <login/>해야 할 수 있습니다.", - "successful": "요청이 성공적으로 전송되었습니다", - "successfulMessage": "소유자가 요청을 승인하면 알림을 받게 됩니다.", - "requestError": "접근 요청 실패", - "repeatRequestError": "이미 이 페이지에 접근을 요청했습니다" - }, - "approveAccess": { - "title": "작업 공간 참여 요청 승인", - "requestSummary": "<user/>이(가) <workspace/>에 참여하고 <page/>에 접근하려고 요청합니다", - "upgrade": "업그레이드", - "downloadApp": "AppFlowy 다운로드", - "approveButton": "승인", - "approveSuccess": "성공적으로 승인되었습니다", - "approveError": "승인 실패, 작업 공간 플랜 한도를 초과하지 않았는지 확인하세요", - "getRequestInfoError": "요청 정보를 가져오지 못했습니다", - "memberCount": { - "zero": "멤버 없음", - "one": "1명의 멤버", - "many": "{count}명의 멤버", - "other": "{count}명의 멤버" - }, - "alreadyProTitle": "작업 공간 플랜 한도에 도달했습니다", - "alreadyProMessage": "<email/>에 연락하여 더 많은 멤버를 잠금 해제하도록 요청하세요", - "repeatApproveError": "이미 이 요청을 승인했습니다", - "ensurePlanLimit": "작업 공간 플랜 한도를 초과하지 않았는지 확인하세요. 한도를 초과한 경우 작업 공간 플랜을 <upgrade/>하거나 <download/>를 고려하세요.", - "requestToJoin": "참여 요청", - "asMember": "멤버로" - }, - "upgradePlanModal": { - "title": "Pro로 업그레이드", - "message": "{name}이(가) 무료 멤버 한도에 도달했습니다. 더 많은 멤버를 초대하려면 Pro 플랜으로 업그레이드하세요.", - "upgradeSteps": "AppFlowy에서 플랜을 업그레이드하는 방법:", - "step1": "1. 설정으로 이동", - "step2": "2. '플랜' 클릭", - "step3": "3. '플랜 변경' 선택", - "appNote": "참고: ", - "actionButton": "업그레이드", - "downloadLink": "앱 다운로드", - "laterButton": "나중에", - "refreshNote": "성공적으로 업그레이드한 후 새 기능을 활성화하려면 <refresh/>를 클릭하세요.", - "refresh": "여기" - }, - "breadcrumbs": { - "label": "탐색 경로" - }, - "time": { - "justNow": "방금", - "seconds": { - "one": "1초", - "other": "{count}초" - }, - "minutes": { - "one": "1분", - "other": "{count}분" - }, - "hours": { - "one": "1시간", - "other": "{count}시간" - }, - "days": { - "one": "1일", - "other": "{count}일" - }, - "weeks": { - "one": "1주일", - "other": "{count}주일" - }, - "months": { - "one": "1개월", - "other": "{count}개월" - }, - "years": { - "one": "1년", - "other": "{count}년" - }, - "ago": "전", - "yesterday": "어제", - "today": "오늘" - }, - "members": { - "zero": "멤버 없음", - "one": "1명의 멤버", - "many": "{count}명의 멤버", - "other": "{count}명의 멤버" - }, - "tabMenu": { - "close": "닫기", - "closeDisabledHint": "고정된 탭은 닫을 수 없습니다. 먼저 고정을 해제하세요", - "closeOthers": "다른 탭 닫기", - "closeOthersHint": "이 탭을 제외한 모든 고정되지 않은 탭을 닫습니다", - "closeOthersDisabledHint": "모든 탭이 고정되어 있어 닫을 탭을 찾을 수 없습니다", - "favorite": "즐겨찾기", - "unfavorite": "즐겨찾기 해제", - "favoriteDisabledHint": "이 보기를 즐겨찾기에 추가할 수 없습니다", - "pinTab": "고정", - "unpinTab": "고정 해제" - }, - "openFileMessage": { - "success": "파일이 성공적으로 열렸습니다", - "fileNotFound": "파일을 찾을 수 없습니다", - "noAppToOpenFile": "이 파일을 열 수 있는 앱이 없습니다", - "permissionDenied": "이 파일을 열 수 있는 권한이 없습니다", - "unknownError": "파일 열기 실패" - }, - "inviteMember": { - "requestInviteMembers": "작업 공간에 초대", - "inviteFailedMemberLimit": "멤버 한도에 도달했습니다. ", - "upgrade": "업그레이드", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "초대 보내기", - "inviteAlready": "이미 이 이메일을 초대했습니다: {email}", - "inviteSuccess": "초대가 성공적으로 전송되었습니다", - "description": "아래에 이메일을 쉼표로 구분하여 입력하세요. 멤버 수에 따라 요금이 부과됩니다.", - "emails": "이메일" - }, - "quickNote": { - "label": "빠른 노트", - "quickNotes": "빠른 노트", - "search": "빠른 노트 검색", - "collapseFullView": "전체 보기 축소", - "expandFullView": "전체 보기 확장", - "createFailed": "빠른 노트 생성 실패", - "quickNotesEmpty": "빠른 노트 없음", - "emptyNote": "빈 노트", - "deleteNotePrompt": "선택한 노트가 영구적으로 삭제됩니다. 삭제하시겠습니까?", - "addNote": "새 노트", - "noAdditionalText": "추가 텍스트 없음" - }, - "subscribe": { - "upgradePlanTitle": "플랜 비교 및 선택", - "yearly": "연간", - "save": "{discount}% 절약", - "monthly": "월별", - "priceIn": "가격 ", - "free": "무료", - "pro": "Pro", - "freeDescription": "모든 것을 정리하기 위한 최대 2명의 개인용", - "proDescription": "프로젝트 및 팀 지식을 관리하기 위한 소규모 팀용", - "proDuration": { - "monthly": "월별 청구되는 멤버당 월별", - "yearly": "연간 청구되는 멤버당 월별" - }, - "cancel": "다운그레이드", - "changePlan": "Pro 플랜으로 업그레이드", - "everythingInFree": "무료 플랜의 모든 기능 +", - "currentPlan": "현재", - "freeDuration": "영원히", - "freePoints": { - "first": "최대 2명의 협업 작업 공간", - "second": "무제한 페이지 및 블록", - "three": "5 GB 저장 공간", - "four": "지능형 검색", - "five": "20 AI 응답", - "six": "모바일 앱", - "seven": "실시간 협업" - }, - "proPoints": { - "first": "무제한 저장 공간", - "second": "최대 10명의 작업 공간 멤버", - "three": "무제한 AI 응답", - "four": "무제한 파일 업로드", - "five": "맞춤 네임스페이스" - }, - "cancelPlan": { - "title": "떠나셔서 아쉽습니다", - "success": "구독이 성공적으로 취소되었습니다", - "description": "@:appName을 개선하는 데 도움이 되도록 피드백을 듣고 싶습니다. 몇 가지 질문에 답변해 주세요.", - "commonOther": "기타", - "otherHint": "여기에 답변을 작성하세요", - "questionOne": { - "question": "@:appName Pro 구독을 취소한 이유는 무엇입니까?", - "answerOne": "비용이 너무 높음", - "answerTwo": "기능이 기대에 미치지 못함", - "answerThree": "더 나은 대안을 찾음", - "answerFour": "비용을 정당화할 만큼 충분히 사용하지 않음", - "answerFive": "서비스 문제 또는 기술적 어려움" - }, - "questionTwo": { - "question": "미래에 @:appName Pro를 다시 구독할 가능성은 얼마나 됩니까?", - "answerOne": "매우 가능성이 높음", - "answerTwo": "어느 정도 가능성이 있음", - "answerThree": "잘 모르겠음", - "answerFour": "가능성이 낮음", - "answerFive": "매우 가능성이 낮음" - }, - "questionThree": { - "question": "구독 기간 동안 가장 가치 있게 여긴 Pro 기능은 무엇입니까?", - "answerOne": "다중 사용자 협업", - "answerTwo": "더 긴 시간 버전 기록", - "answerThree": "무제한 AI 응답", - "answerFour": "로컬 AI 모델 액세스" - }, - "questionFour": { - "question": "@:appName에 대한 전반적인 경험을 어떻게 설명하시겠습니까?", - "answerOne": "훌륭함", - "answerTwo": "좋음", - "answerThree": "보통", - "answerFour": "평균 이하", - "answerFive": "불만족" - } - } - }, - "ai": { - "contentPolicyViolation": "민감한 콘텐츠로 인해 이미지 생성에 실패했습니다. 입력을 다시 작성하고 다시 시도하세요", - "textLimitReachedDescription": "작업 공간의 무료 AI 응답이 부족합니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "imageLimitReachedDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. Pro 플랜으로 업그레이드하거나 AI 애드온을 구매하여 무제한 응답을 잠금 해제하세요", - "limitReachedAction": { - "textDescription": "작업 공간의 무료 AI 응답이 부족합니다. 더 많은 응답을 받으려면 ", - "imageDescription": "무료 AI 이미지 할당량을 모두 사용했습니다. ", - "upgrade": "업그레이드", - "toThe": " ", - "proPlan": "Pro 플랜", - "orPurchaseAn": " 또는 ", - "aiAddon": "AI 애드온을 구매하세요" - }, - "editing": "편집 중", - "analyzing": "분석 중", - "continueWritingEmptyDocumentTitle": "계속 작성 오류", - "continueWritingEmptyDocumentDescription": "문서의 내용을 확장하는 데 문제가 있습니다. 간단한 소개를 작성하면 나머지는 우리가 처리할 수 있습니다!" - }, - "autoUpdate": { - "criticalUpdateTitle": "계속하려면 업데이트가 필요합니다", - "criticalUpdateDescription": "경험을 향상시키기 위해 개선 사항을 추가했습니다! 앱을 계속 사용하려면 {currentVersion}에서 {newVersion}으로 업데이트하세요.", - "criticalUpdateButton": "업데이트", - "bannerUpdateTitle": "새 버전 사용 가능!", - "bannerUpdateDescription": "최신 기능 및 수정 사항을 받으세요. 지금 설치하려면 \"업데이트\"를 클릭하세요", - "bannerUpdateButton": "업데이트", - "settingsUpdateTitle": "새 버전 ({newVersion}) 사용 가능!", - "settingsUpdateDescription": "현재 버전: {currentVersion} (공식 빌드) → {newVersion}", - "settingsUpdateButton": "업데이트", - "settingsUpdateWhatsNew": "새로운 기능" - }, - "lockPage": { - "lockPage": "잠금", - "reLockPage": "다시 잠금", - "lockTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다. 잠금 해제하려면 클릭하세요.", - "pageLockedToast": "페이지가 잠겼습니다. 누군가 잠금을 해제할 때까지 편집이 비활성화됩니다.", - "lockedOperationTooltip": "실수로 편집하지 않도록 페이지가 잠겨 있습니다." - }, - "suggestion": { - "accept": "수락", - "keep": "유지", - "discard": "버리기", - "close": "닫기", - "tryAgain": "다시 시도", - "rewrite": "다시 작성", - "insertBelow": "아래에 삽입" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/mr-IN.json b/frontend/resources/translations/mr-IN.json deleted file mode 100644 index f86a1e0081..0000000000 --- a/frontend/resources/translations/mr-IN.json +++ /dev/null @@ -1,3210 +0,0 @@ -{ - "appName": "AppFlowy", - "defaultUsername": "मी", - "welcomeText": "@:appName मध्ये आ पले स्वागत आहे.", - "welcomeTo": "मध्ये आ पले स्वागत आ हे", - "githubStarText": "GitHub वर स्टार करा", - "subscribeNewsletterText": "वृत्तपत्राची सदस्यता घ्या", - "letsGoButtonText": "क्विक स्टार्ट", - "title": "Title", - "youCanAlso": "तुम्ही देखील", - "and": "आ णि", - "failedToOpenUrl": "URL उघडण्यात अयशस्वी: {}", - "blockActions": { - "addBelowTooltip": "खाली जोडण्यासाठी क्लिक करा", - "addAboveCmd": "Alt+click", - "addAboveMacCmd": "Option+click", - "addAboveTooltip": "वर जोडण्यासाठी", - "dragTooltip": "Drag to move", - "openMenuTooltip": "मेनू उघडण्यासाठी क्लिक करा" - }, - "signUp": { - "buttonText": "साइन अप", - "title": "साइन अप to @:appName", - "getStartedText": "सुरुवात करा", - "emptyPasswordError": "पासवर्ड रिकामा असू शकत नाही", - "repeatPasswordEmptyError": "Repeat पासवर्ड रिकामा असू शकत नाही", - "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", - "alreadyHaveAnAccount": "आधीच खाते आहे?", - "emailHint": "Email", - "passwordHint": "Password", - "repeatPasswordHint": "पासवर्ड पुन्हा लिहा", - "signUpWith": "यामध्ये साइन अप करा:" - }, - "signIn": { - "loginTitle": "@:appName मध्ये लॉगिन करा", - "loginButtonText": "लॉगिन", - "loginStartWithAnonymous": "अनामिक सत्रासह पुढे जा", - "continueAnonymousUser": "अनामिक सत्रासह पुढे जा", - "anonymous": "अनामिक", - "buttonText": "साइन इन", - "signingInText": "साइन इन होत आहे...", - "forgotPassword": "पासवर्ड विसरलात?", - "emailHint": "ईमेल", - "passwordHint": "पासवर्ड", - "dontHaveAnAccount": "तुमचं खाते नाही?", - "createAccount": "खाते तयार करा", - "repeatPasswordEmptyError": "पुन्हा पासवर्ड रिकामा असू शकत नाही", - "unmatchedPasswordError": "पुन्हा लिहिलेला पासवर्ड मूळ पासवर्डशी जुळत नाही", - "syncPromptMessage": "डेटा सिंक होण्यास थोडा वेळ लागू शकतो. कृपया हे पृष्ठ बंद करू नका", - "or": "किंवा", - "signInWithGoogle": "Google सह पुढे जा", - "signInWithGithub": "GitHub सह पुढे जा", - "signInWithDiscord": "Discord सह पुढे जा", - "signInWithApple": "Apple सह पुढे जा", - "continueAnotherWay": "इतर पर्यायांनी पुढे जा", - "signUpWithGoogle": "Google सह साइन अप करा", - "signUpWithGithub": "GitHub सह साइन अप करा", - "signUpWithDiscord": "Discord सह साइन अप करा", - "signInWith": "यासह पुढे जा:", - "signInWithEmail": "ईमेलसह पुढे जा", - "signInWithMagicLink": "पुढे जा", - "signUpWithMagicLink": "Magic Link सह साइन अप करा", - "pleaseInputYourEmail": "कृपया तुमचा ईमेल पत्ता टाका", - "settings": "सेटिंग्ज", - "magicLinkSent": "Magic Link पाठवण्यात आली आहे!", - "invalidEmail": "कृपया वैध ईमेल पत्ता टाका", - "alreadyHaveAnAccount": "आधीच खाते आहे?", - "logIn": "लॉगिन", - "generalError": "काहीतरी चुकलं. कृपया नंतर प्रयत्न करा", - "limitRateError": "सुरक्षेच्या कारणास्तव, तुम्ही दर ६० सेकंदांतून एकदाच Magic Link मागवू शकता", - "magicLinkSentDescription": "तुमच्या ईमेलवर Magic Link पाठवण्यात आली आहे. लॉगिन पूर्ण करण्यासाठी लिंकवर क्लिक करा. ही लिंक ५ मिनिटांत कालबाह्य होईल." - }, - "workspace": { - "chooseWorkspace": "तुमचे workspace निवडा", - "defaultName": "माझे Workspace", - "create": "नवीन workspace तयार करा", - "new": "नवीन workspace", - "importFromNotion": "Notion मधून आयात करा", - "learnMore": "अधिक जाणून घ्या", - "reset": "workspace रीसेट करा", - "renameWorkspace": "workspace चे नाव बदला", - "workspaceNameCannotBeEmpty": "workspace चे नाव रिकामे असू शकत नाही", - "resetWorkspacePrompt": "workspace रीसेट केल्यास त्यातील सर्व पृष्ठे आणि डेटा हटवले जातील. तुम्हाला workspace रीसेट करायचे आहे का? पर्यायी म्हणून तुम्ही सपोर्ट टीमशी संपर्क करू शकता.", - "hint": "workspace", - "notFoundError": "workspace सापडले नाही", - "failedToLoad": "काहीतरी चूक झाली! workspace लोड होण्यात अयशस्वी. कृपया @:appName चे कोणतेही उघडे instance बंद करा आणि पुन्हा प्रयत्न करा.", - "errorActions": { - "reportIssue": "समस्या नोंदवा", - "reportIssueOnGithub": "Github वर समस्या नोंदवा", - "exportLogFiles": "लॉग फाइल्स निर्यात करा", - "reachOut": "Discord वर संपर्क करा" - }, - "menuTitle": "कार्यक्षेत्रे", - "deleteWorkspaceHintText": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील.", - "createSuccess": "कार्यक्षेत्र यशस्वीरित्या तयार झाले", - "createFailed": "कार्यक्षेत्र तयार करण्यात अयशस्वी", - "createLimitExceeded": "तुम्ही तुमच्या खात्यासाठी परवानगी दिलेल्या कार्यक्षेत्र मर्यादेपर्यंत पोहोचलात. अधिक कार्यक्षेत्रासाठी GitHub वर विनंती करा.", - "deleteSuccess": "कार्यक्षेत्र यशस्वीरित्या हटवले गेले", - "deleteFailed": "कार्यक्षेत्र हटवण्यात अयशस्वी", - "openSuccess": "कार्यक्षेत्र यशस्वीरित्या उघडले", - "openFailed": "कार्यक्षेत्र उघडण्यात अयशस्वी", - "renameSuccess": "कार्यक्षेत्राचे नाव यशस्वीरित्या बदलले", - "renameFailed": "कार्यक्षेत्राचे नाव बदलण्यात अयशस्वी", - "updateIconSuccess": "कार्यक्षेत्राचे चिन्ह यशस्वीरित्या अद्यतनित केले", - "updateIconFailed": "कार्यक्षेत्राचे चिन्ह अद्यतनित करण्यात अयशस्वी", - "cannotDeleteTheOnlyWorkspace": "फक्त एकच कार्यक्षेत्र असल्यास ते हटवता येत नाही", - "fetchWorkspacesFailed": "कार्यक्षेत्रे मिळवण्यात अयशस्वी", - "leaveCurrentWorkspace": "कार्यक्षेत्र सोडा", - "leaveCurrentWorkspacePrompt": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का?" - }, - "shareAction": { - "buttonText": "शेअर करा", - "workInProgress": "लवकरच येत आहे", - "markdown": "Markdown", - "html": "HTML", - "clipboard": "क्लिपबोर्डवर कॉपी करा", - "csv": "CSV", - "copyLink": "लिंक कॉपी करा", - "publishToTheWeb": "वेबवर प्रकाशित करा", - "publishToTheWebHint": "AppFlowy सह वेबसाइट तयार करा", - "publish": "प्रकाशित करा", - "unPublish": "अप्रकाशित करा", - "visitSite": "साइटला भेट द्या", - "exportAsTab": "या स्वरूपात निर्यात करा", - "publishTab": "प्रकाशित करा", - "shareTab": "शेअर करा", - "publishOnAppFlowy": "AppFlowy वर प्रकाशित करा", - "shareTabTitle": "सहकार्य करण्यासाठी आमंत्रित करा", - "shareTabDescription": "कोणासही सहज सहकार्य करण्यासाठी", - "copyLinkSuccess": "लिंक क्लिपबोर्डवर कॉपी केली", - "copyShareLink": "शेअर लिंक कॉपी करा", - "copyLinkFailed": "लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", - "copyLinkToBlockSuccess": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी केली", - "copyLinkToBlockFailed": "ब्लॉकची लिंक क्लिपबोर्डवर कॉपी करण्यात अयशस्वी", - "manageAllSites": "सर्व साइट्स व्यवस्थापित करा", - "updatePathName": "पथाचे नाव अपडेट करा" - }, - "moreAction": { - "small": "लहान", - "medium": "मध्यम", - "large": "मोठा", - "fontSize": "फॉन्ट आकार", - "import": "Import", - "moreOptions": "अधिक पर्याय", - "wordCount": "शब्द संख्या: {}", - "charCount": "अक्षर संख्या: {}", - "createdAt": "निर्मिती: {}", - "deleteView": "हटवा", - "duplicateView": "प्रत बनवा", - "wordCountLabel": "शब्द संख्या: ", - "charCountLabel": "अक्षर संख्या: ", - "createdAtLabel": "निर्मिती: ", - "syncedAtLabel": "सिंक केले: ", - "saveAsNewPage": "संदेश पृष्ठात जोडा", - "saveAsNewPageDisabled": "उपलब्ध संदेश नाहीत" - }, - "importPanel": { - "textAndMarkdown": "मजकूर आणि Markdown", - "documentFromV010": "v0.1.0 पासून दस्तऐवज", - "databaseFromV010": "v0.1.0 पासून डेटाबेस", - "notionZip": "Notion निर्यात केलेली Zip फाईल", - "csv": "CSV", - "database": "डेटाबेस" - }, - "emojiIconPicker": { - "iconUploader": { - "placeholderLeft": "फाईल ड्रॅग आणि ड्रॉप करा, क्लिक करा ", - "placeholderUpload": "अपलोड", - "placeholderRight": ", किंवा इमेज लिंक पेस्ट करा.", - "dropToUpload": "अपलोडसाठी फाईल ड्रॉप करा", - "change": "बदला" - } - }, - "disclosureAction": { - "rename": "नाव बदला", - "delete": "हटवा", - "duplicate": "प्रत बनवा", - "unfavorite": "आवडतीतून काढा", - "favorite": "आवडतीत जोडा", - "openNewTab": "नवीन टॅबमध्ये उघडा", - "moveTo": "या ठिकाणी हलवा", - "addToFavorites": "आवडतीत जोडा", - "copyLink": "लिंक कॉपी करा", - "changeIcon": "आयकॉन बदला", - "collapseAllPages": "सर्व उपपृष्ठे संकुचित करा", - "movePageTo": "पृष्ठ हलवा", - "move": "हलवा", - "lockPage": "पृष्ठ लॉक करा" - }, - "blankPageTitle": "रिक्त पृष्ठ", - "newPageText": "नवीन पृष्ठ", - "newDocumentText": "नवीन दस्तऐवज", - "newGridText": "नवीन ग्रिड", - "newCalendarText": "नवीन कॅलेंडर", - "newBoardText": "नवीन बोर्ड", - "chat": { - "newChat": "AI गप्पा", - "inputMessageHint": "@:appName AI ला विचार करा", - "inputLocalAIMessageHint": "@:appName लोकल AI ला विचार करा", - "unsupportedCloudPrompt": "ही सुविधा फक्त @:appName Cloud वापरताना उपलब्ध आहे", - "relatedQuestion": "सूचवलेले", - "serverUnavailable": "कनेक्शन गमावले. कृपया तुमचे इंटरनेट तपासा आणि पुन्हा प्रयत्न करा", - "aiServerUnavailable": "AI सेवा सध्या अनुपलब्ध आहे. कृपया नंतर पुन्हा प्रयत्न करा.", - "retry": "पुन्हा प्रयत्न करा", - "clickToRetry": "पुन्हा प्रयत्न करण्यासाठी क्लिक करा", - "regenerateAnswer": "उत्तर पुन्हा तयार करा", - "question1": "Kanban वापरून कामे कशी व्यवस्थापित करायची", - "question2": "GTD पद्धत समजावून सांगा", - "question3": "Rust का वापरावा", - "question4": "माझ्या स्वयंपाकघरात असलेल्या वस्तूंनी रेसिपी", - "question5": "माझ्या पृष्ठासाठी एक चित्र तयार करा", - "question6": "या आठवड्याची माझी कामांची यादी तयार करा", - "aiMistakePrompt": "AI चुकू शकतो. महत्त्वाची माहिती तपासा.", - "chatWithFilePrompt": "तुम्हाला फाइलसोबत गप्पा मारायच्या आहेत का?", - "indexFileSuccess": "फाईल यशस्वीरित्या अनुक्रमित केली गेली", - "inputActionNoPages": "काहीही पृष्ठे सापडली नाहीत", - "referenceSource": { - "zero": "0 स्रोत सापडले", - "one": "{count} स्रोत सापडला", - "other": "{count} स्रोत सापडले" - } - }, - "clickToMention": "पृष्ठाचा उल्लेख करा", - "uploadFile": "PDFs, मजकूर किंवा markdown फाइल्स जोडा", - "questionDetail": "नमस्कार {}! मी तुम्हाला कशी मदत करू शकतो?", - "indexingFile": "{} अनुक्रमित करत आहे", - "generatingResponse": "उत्तर तयार होत आहे", - "selectSources": "स्रोत निवडा", - "currentPage": "सध्याचे पृष्ठ", - "sourcesLimitReached": "तुम्ही फक्त ३ मुख्य दस्तऐवज आणि त्यांची उपदस्तऐवज निवडू शकता", - "sourceUnsupported": "सध्या डेटाबेससह चॅटिंगसाठी आम्ही समर्थन करत नाही", - "regenerate": "पुन्हा प्रयत्न करा", - "addToPageButton": "संदेश पृष्ठावर जोडा", - "addToPageTitle": "या पृष्ठात संदेश जोडा...", - "addToNewPage": "नवीन पृष्ठ तयार करा", - "addToNewPageName": "\"{}\" मधून काढलेले संदेश", - "addToNewPageSuccessToast": "संदेश जोडण्यात आला", - "openPagePreviewFailedToast": "पृष्ठ उघडण्यात अयशस्वी", - "changeFormat": { - "actionButton": "फॉरमॅट बदला", - "confirmButton": "या फॉरमॅटसह पुन्हा तयार करा", - "textOnly": "मजकूर", - "imageOnly": "फक्त प्रतिमा", - "textAndImage": "मजकूर आणि प्रतिमा", - "text": "परिच्छेद", - "bullet": "बुलेट यादी", - "number": "क्रमांकित यादी", - "table": "सारणी", - "blankDescription": "उत्तराचे फॉरमॅट ठरवा", - "defaultDescription": "स्वयंचलित उत्तर फॉरमॅट", - "textWithImageDescription": "@:chat.changeFormat.text प्रतिमेसह", - "numberWithImageDescription": "@:chat.changeFormat.number प्रतिमेसह", - "bulletWithImageDescription": "@:chat.changeFormat.bullet प्रतिमेसह", - " tableWithImageDescription": "@:chat.changeFormat.table प्रतिमेसह" - }, - "switchModel": { - "label": "मॉडेल बदला", - "localModel": "स्थानिक मॉडेल", - "cloudModel": "क्लाऊड मॉडेल", - "autoModel": "स्वयंचलित" - }, - "selectBanner": { - "saveButton": "… मध्ये जोडा", - "selectMessages": "संदेश निवडा", - "nSelected": "{} निवडले गेले", - "allSelected": "सर्व निवडले गेले" - }, - "stopTooltip": "उत्पन्न करणे थांबवा", - "trash": { - "text": "कचरा", - "restoreAll": "सर्व पुनर्संचयित करा", - "restore": "पुनर्संचयित करा", - "deleteAll": "सर्व हटवा", - "pageHeader": { - "fileName": "फाईलचे नाव", - "lastModified": "शेवटचा बदल", - "created": "निर्मिती" - } - }, - "confirmDeleteAll": { - "title": "कचरापेटीतील सर्व पृष्ठे", - "caption": "तुम्हाला कचरापेटीतील सर्व काही हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "confirmRestoreAll": { - "title": "कचरापेटीतील सर्व पृष्ठे पुनर्संचयित करा", - "caption": "ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "restorePage": { - "title": "पुनर्संचयित करा: {}", - "caption": "तुम्हाला हे पृष्ठ पुनर्संचयित करायचे आहे का?" - }, - "mobile": { - "actions": "कचरा क्रिया", - "empty": "कचरापेटीत कोणतीही पृष्ठे किंवा जागा नाहीत", - "emptyDescription": "जे आवश्यक नाही ते कचरापेटीत हलवा.", - "isDeleted": "हटवले गेले आहे", - "isRestored": "पुनर्संचयित केले गेले आहे" - }, - "confirmDeleteTitle": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का?", - "deletePagePrompt": { - "text": "हे पृष्ठ कचरापेटीत आहे", - "restore": "पृष्ठ पुनर्संचयित करा", - "deletePermanent": "कायमचे हटवा", - "deletePermanentDescription": "तुम्हाला हे पृष्ठ कायमचे हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही." - }, - "dialogCreatePageNameHint": "पृष्ठाचे नाव", - "questionBubble": { - "shortcuts": "शॉर्टकट्स", - "whatsNew": "नवीन काय आहे?", - "help": "मदत आणि समर्थन", - "markdown": "Markdown", - "debug": { - "name": "डीबग माहिती", - "success": "डीबग माहिती क्लिपबोर्डवर कॉपी केली!", - "fail": "डीबग माहिती कॉपी करता आली नाही" - }, - "feedback": "अभिप्राय" - }, - "menuAppHeader": { - "moreButtonToolTip": "हटवा, नाव बदला आणि अधिक...", - "addPageTooltip": "तत्काळ एक पृष्ठ जोडा", - "defaultNewPageName": "शीर्षक नसलेले", - "renameDialog": "नाव बदला", - "pageNameSuffix": "प्रत" - }, - "noPagesInside": "अंदर कोणतीही पृष्ठे नाहीत", - "toolbar": { - "undo": "पूर्ववत करा", - "redo": "पुन्हा करा", - "bold": "ठळक", - "italic": "तिरकस", - "underline": "अधोरेखित", - "strike": "मागे ओढलेले", - "numList": "क्रमांकित यादी", - "bulletList": "बुलेट यादी", - "checkList": "चेक यादी", - "inlineCode": "इनलाइन कोड", - "quote": "उद्धरण ब्लॉक", - "header": "शीर्षक", - "highlight": "हायलाइट", - "color": "रंग", - "addLink": "लिंक जोडा" - }, - "tooltip": { - "lightMode": "लाइट मोडमध्ये स्विच करा", - "darkMode": "डार्क मोडमध्ये स्विच करा", - "openAsPage": "पृष्ठ म्हणून उघडा", - "addNewRow": "नवीन पंक्ती जोडा", - "openMenu": "मेनू उघडण्यासाठी क्लिक करा", - "dragRow": "पंक्तीचे स्थान बदलण्यासाठी ड्रॅग करा", - "viewDataBase": "डेटाबेस पहा", - "referencePage": "हे {name} संदर्भित आहे", - "addBlockBelow": "खाली एक ब्लॉक जोडा", - "aiGenerate": "निर्मिती करा" - }, - "sideBar": { - "closeSidebar": "साइडबार बंद करा", - "openSidebar": "साइडबार उघडा", - "expandSidebar": "पूर्ण पृष्ठावर विस्तारित करा", - "personal": "वैयक्तिक", - "private": "खाजगी", - "workspace": "कार्यक्षेत्र", - "favorites": "आवडती", - "clickToHidePrivate": "खाजगी जागा लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने फक्त तुम्हाला दिसतील", - "clickToHideWorkspace": "कार्यक्षेत्र लपवण्यासाठी क्लिक करा\nयेथे तयार केलेली पाने सर्व सदस्यांना दिसतील", - "clickToHidePersonal": "वैयक्तिक जागा लपवण्यासाठी क्लिक करा", - "clickToHideFavorites": "आवडती जागा लपवण्यासाठी क्लिक करा", - "addAPage": "नवीन पृष्ठ जोडा", - "addAPageToPrivate": "खाजगी जागेत पृष्ठ जोडा", - "addAPageToWorkspace": "कार्यक्षेत्रात पृष्ठ जोडा", - "recent": "अलीकडील", - "today": "आज", - "thisWeek": "या आठवड्यात", - "others": "पूर्वीच्या आवडती", - "earlier": "पूर्वीचे", - "justNow": "आत्ताच", - "minutesAgo": "{count} मिनिटांपूर्वी", - "lastViewed": "शेवटी पाहिलेले", - "favoriteAt": "आवडते म्हणून चिन्हांकित", - "emptyRecent": "अलीकडील पृष्ठे नाहीत", - "emptyRecentDescription": "तुम्ही पाहिलेली पृष्ठे येथे सहज पुन्हा मिळवण्यासाठी दिसतील.", - "emptyFavorite": "आवडती पृष्ठे नाहीत", - "emptyFavoriteDescription": "पृष्ठांना आवडते म्हणून चिन्हांकित करा—ते येथे झपाट्याने प्रवेशासाठी दिसतील!", - "removePageFromRecent": "हे पृष्ठ अलीकडील यादीतून काढायचे?", - "removeSuccess": "यशस्वीरित्या काढले गेले", - "favoriteSpace": "आवडती", - "RecentSpace": "अलीकडील", - "Spaces": "जागा", - "upgradeToPro": "Pro मध्ये अपग्रेड करा", - "upgradeToAIMax": "अमर्यादित AI अनलॉक करा", - "storageLimitDialogTitle": "तुमचा मोफत स्टोरेज संपला आहे. अमर्यादित स्टोरेजसाठी अपग्रेड करा", - "storageLimitDialogTitleIOS": "तुमचा मोफत स्टोरेज संपला आहे.", - "aiResponseLimitTitle": "तुमचे मोफत AI प्रतिसाद संपले आहेत. कृपया Pro प्लॅनला अपग्रेड करा किंवा AI add-on खरेदी करा", - "aiResponseLimitDialogTitle": "AI प्रतिसाद मर्यादा गाठली आहे", - "aiResponseLimit": "तुमचे मोफत AI प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max किंवा Pro प्लॅन क्लिक करा", - "askOwnerToUpgradeToPro": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे. कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा", - "askOwnerToUpgradeToProIOS": "तुमच्या कार्यक्षेत्राचे मोफत स्टोरेज संपत चालले आहे.", - "askOwnerToUpgradeToAIMax": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला प्लॅन अपग्रेड किंवा AI add-ons खरेदी करण्यास सांगा", - "askOwnerToUpgradeToAIMaxIOS": "तुमच्या कार्यक्षेत्राचे मोफत AI प्रतिसाद संपले आहेत.", - "purchaseAIMax": "तुमच्या कार्यक्षेत्राचे AI प्रतिमा प्रतिसाद संपले आहेत. कृपया कार्यक्षेत्र मालकाला AI Max खरेदी करण्यास सांगा", - "aiImageResponseLimit": "तुमचे AI प्रतिमा प्रतिसाद संपले आहेत.\n\nसेटिंग्ज -> प्लॅन -> AI Max क्लिक करा", - "purchaseStorageSpace": "स्टोरेज स्पेस खरेदी करा", - "singleFileProPlanLimitationDescription": "तुम्ही मोफत प्लॅनमध्ये परवानगी दिलेल्या फाइल अपलोड आकाराची मर्यादा ओलांडली आहे. कृपया Pro प्लॅनमध्ये अपग्रेड करा", - "purchaseAIResponse": "AI प्रतिसाद खरेदी करा", - "askOwnerToUpgradeToLocalAI": "कार्यक्षेत्र मालकाला ऑन-डिव्हाइस AI सक्षम करण्यास सांगा", - "upgradeToAILocal": "अत्याधिक गोपनीयतेसाठी तुमच्या डिव्हाइसवर लोकल मॉडेल चालवा", - "upgradeToAILocalDesc": "PDFs सोबत गप्पा मारा, तुमचे लेखन सुधारा आणि लोकल AI वापरून टेबल्स आपोआप भरा" -}, - "notifications": { - "export": { - "markdown": "टीप Markdown मध्ये निर्यात केली", - "path": "Documents/flowy" - } - }, - "contactsPage": { - "title": "संपर्क", - "whatsHappening": "या आठवड्यात काय घडत आहे?", - "addContact": "संपर्क जोडा", - "editContact": "संपर्क संपादित करा" - }, - "button": { - "ok": "ठीक आहे", - "confirm": "खात्री करा", - "done": "पूर्ण", - "cancel": "रद्द करा", - "signIn": "साइन इन", - "signOut": "साइन आउट", - "complete": "पूर्ण करा", - "save": "जतन करा", - "generate": "निर्माण करा", - "esc": "ESC", - "keep": "ठेवा", - "tryAgain": "पुन्हा प्रयत्न करा", - "discard": "टाका", - "replace": "बदला", - "insertBelow": "खाली घाला", - "insertAbove": "वर घाला", - "upload": "अपलोड करा", - "edit": "संपादित करा", - "delete": "हटवा", - "copy": "कॉपी करा", - "duplicate": "प्रत बनवा", - "putback": "परत ठेवा", - "update": "अद्यतनित करा", - "share": "शेअर करा", - "removeFromFavorites": "आवडतीतून काढा", - "removeFromRecent": "अलीकडील यादीतून काढा", - "addToFavorites": "आवडतीत जोडा", - "favoriteSuccessfully": "आवडतीत यशस्वीरित्या जोडले", - "unfavoriteSuccessfully": "आवडतीतून यशस्वीरित्या काढले", - "duplicateSuccessfully": "प्रत यशस्वीरित्या तयार झाली", - "rename": "नाव बदला", - "helpCenter": "मदत केंद्र", - "add": "जोड़ा", - "yes": "होय", - "no": "नाही", - "clear": "साफ करा", - "remove": "काढा", - "dontRemove": "काढू नका", - "copyLink": "लिंक कॉपी करा", - "align": "जुळवा", - "login": "लॉगिन", - "logout": "लॉगआउट", - "deleteAccount": "खाते हटवा", - "back": "मागे", - "signInGoogle": "Google सह पुढे जा", - "signInGithub": "GitHub सह पुढे जा", - "signInDiscord": "Discord सह पुढे जा", - "more": "अधिक", - "create": "तयार करा", - "close": "बंद करा", - "next": "पुढे", - "previous": "मागील", - "submit": "सबमिट करा", - "download": "डाउनलोड करा", - "backToHome": "मुख्यपृष्ठावर परत जा", - "viewing": "पाहत आहात", - "editing": "संपादन करत आहात", - "gotIt": "समजले", - "retry": "पुन्हा प्रयत्न करा", - "uploadFailed": "अपलोड अयशस्वी.", - "copyLinkOriginal": "मूळ दुव्याची कॉपी करा" - }, - "label": { - "welcome": "स्वागत आहे!", - "firstName": "पहिले नाव", - "middleName": "मधले नाव", - "lastName": "आडनाव", - "stepX": "पायरी {X}" - }, - "oAuth": { - "err": { - "failedTitle": "तुमच्या खात्याशी कनेक्ट होता आले नाही.", - "failedMsg": "कृपया खात्री करा की तुम्ही ब्राउझरमध्ये साइन-इन प्रक्रिया पूर्ण केली आहे." - }, - "google": { - "title": "GOOGLE साइन-इन", - "instruction1": "तुमचे Google Contacts आयात करण्यासाठी, तुम्हाला तुमच्या वेब ब्राउझरचा वापर करून या अ‍ॅप्लिकेशनला अधिकृत करणे आवश्यक आहे.", - "instruction2": "ही कोड आयकॉनवर क्लिक करून किंवा मजकूर निवडून क्लिपबोर्डवर कॉपी करा:", - "instruction3": "तुमच्या वेब ब्राउझरमध्ये खालील दुव्यावर जा आणि वरील कोड टाका:", - "instruction4": "साइनअप पूर्ण झाल्यावर खालील बटण क्लिक करा:" - } - }, - "settings": { - "title": "सेटिंग्ज", - "popupMenuItem": { - "settings": "सेटिंग्ज", - "members": "सदस्य", - "trash": "कचरा", - "helpAndSupport": "मदत आणि समर्थन" - }, - "sites": { - "title": "साइट्स", - "namespaceTitle": "नेमस्पेस", - "namespaceDescription": "तुमचा नेमस्पेस आणि मुख्यपृष्ठ व्यवस्थापित करा", - "namespaceHeader": "नेमस्पेस", - "homepageHeader": "मुख्यपृष्ठ", - "updateNamespace": "नेमस्पेस अद्यतनित करा", - "removeHomepage": "मुख्यपृष्ठ हटवा", - "selectHomePage": "एक पृष्ठ निवडा", - "clearHomePage": "या नेमस्पेससाठी मुख्यपृष्ठ साफ करा", - "customUrl": "स्वतःची URL", - "namespace": { - "description": "हे बदल सर्व प्रकाशित पृष्ठांवर लागू होतील जे या नेमस्पेसवर चालू आहेत", - "tooltip": "कोणताही अनुचित नेमस्पेस आम्ही काढून टाकण्याचा अधिकार राखून ठेवतो", - "updateExistingNamespace": "विद्यमान नेमस्पेस अद्यतनित करा", - "upgradeToPro": "मुख्यपृष्ठ सेट करण्यासाठी Pro प्लॅनमध्ये अपग्रेड करा", - "redirectToPayment": "पेमेंट पृष्ठावर वळवत आहे...", - "onlyWorkspaceOwnerCanSetHomePage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ सेट करू शकतो", - "pleaseAskOwnerToSetHomePage": "कृपया कार्यक्षेत्र मालकाला Pro प्लॅनमध्ये अपग्रेड करण्यास सांगा" - }, - "publishedPage": { - "title": "सर्व प्रकाशित पृष्ठे", - "description": "तुमची प्रकाशित पृष्ठे व्यवस्थापित करा", - "page": "पृष्ठ", - "pathName": "पथाचे नाव", - "date": "प्रकाशन तारीख", - "emptyHinText": "या कार्यक्षेत्रात तुमच्याकडे कोणतीही प्रकाशित पृष्ठे नाहीत", - "noPublishedPages": "प्रकाशित पृष्ठे नाहीत", - "settings": "प्रकाशन सेटिंग्ज", - "clickToOpenPageInApp": "पृष्ठ अ‍ॅपमध्ये उघडा", - "clickToOpenPageInBrowser": "पृष्ठ ब्राउझरमध्ये उघडा" - } - } - }, - "error": { - "failedToGeneratePaymentLink": "Pro प्लॅनसाठी पेमेंट लिंक तयार करण्यात अयशस्वी", - "failedToUpdateNamespace": "नेमस्पेस अद्यतनित करण्यात अयशस्वी", - "proPlanLimitation": "नेमस्पेस अद्यतनित करण्यासाठी तुम्हाला Pro प्लॅनमध्ये अपग्रेड करणे आवश्यक आहे", - "namespaceAlreadyInUse": "नेमस्पेस आधीच वापरात आहे, कृपया दुसरे प्रयत्न करा", - "invalidNamespace": "अवैध नेमस्पेस, कृपया दुसरे प्रयत्न करा", - "namespaceLengthAtLeast2Characters": "नेमस्पेस किमान २ अक्षरे लांब असावे", - "onlyWorkspaceOwnerCanUpdateNamespace": "फक्त कार्यक्षेत्र मालकच नेमस्पेस अद्यतनित करू शकतो", - "onlyWorkspaceOwnerCanRemoveHomepage": "फक्त कार्यक्षेत्र मालकच मुख्यपृष्ठ हटवू शकतो", - "setHomepageFailed": "मुख्यपृष्ठ सेट करण्यात अयशस्वी", - "namespaceTooLong": "नेमस्पेस खूप लांब आहे, कृपया दुसरे प्रयत्न करा", - "namespaceTooShort": "नेमस्पेस खूप लहान आहे, कृपया दुसरे प्रयत्न करा", - "namespaceIsReserved": "हा नेमस्पेस राखीव आहे, कृपया दुसरे प्रयत्न करा", - "updatePathNameFailed": "पथाचे नाव अद्यतनित करण्यात अयशस्वी", - "removeHomePageFailed": "मुख्यपृष्ठ हटवण्यात अयशस्वी", - "publishNameContainsInvalidCharacters": "पथाच्या नावामध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", - "publishNameTooShort": "पथाचे नाव खूप लहान आहे, कृपया दुसरे प्रयत्न करा", - "publishNameTooLong": "पथाचे नाव खूप लांब आहे, कृपया दुसरे प्रयत्न करा", - "publishNameAlreadyInUse": "हे पथाचे नाव आधीच वापरले गेले आहे, कृपया दुसरे प्रयत्न करा", - "namespaceContainsInvalidCharacters": "नेमस्पेसमध्ये अवैध अक्षरे आहेत, कृपया दुसरे प्रयत्न करा", - "publishPermissionDenied": "फक्त कार्यक्षेत्र मालक किंवा पृष्ठ प्रकाशकच प्रकाशन सेटिंग्ज व्यवस्थापित करू शकतो", - "publishNameCannotBeEmpty": "पथाचे नाव रिकामे असू शकत नाही, कृपया दुसरे प्रयत्न करा" - }, - "success": { - "namespaceUpdated": "नेमस्पेस यशस्वीरित्या अद्यतनित केला", - "setHomepageSuccess": "मुख्यपृष्ठ यशस्वीरित्या सेट केले", - "updatePathNameSuccess": "पथाचे नाव यशस्वीरित्या अद्यतनित केले", - "removeHomePageSuccess": "मुख्यपृष्ठ यशस्वीरित्या हटवले" - }, - "accountPage": { - "menuLabel": "खाते आणि अ‍ॅप", - "title": "माझे खाते", - "general": { - "title": "खात्याचे नाव आणि प्रोफाइल प्रतिमा", - "changeProfilePicture": "प्रोफाइल प्रतिमा बदला" - }, - "email": { - "title": "ईमेल", - "actions": { - "change": "ईमेल बदला" - } - }, - "login": { - "title": "खाते लॉगिन", - "loginLabel": "लॉगिन", - "logoutLabel": "लॉगआउट" - }, - "isUpToDate": "@:appName अद्ययावत आहे!", - "officialVersion": "आवृत्ती {version} (अधिकृत बिल्ड)" -}, - "workspacePage": { - "menuLabel": "कार्यक्षेत्र", - "title": "कार्यक्षेत्र", - "description": "तुमचे कार्यक्षेत्र स्वरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक/वेळ फॉरमॅट आणि भाषा सानुकूलित करा.", - "workspaceName": { - "title": "कार्यक्षेत्राचे नाव" - }, - "workspaceIcon": { - "title": "कार्यक्षेत्राचे चिन्ह", - "description": "तुमच्या कार्यक्षेत्रासाठी प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे चिन्ह साइडबार आणि सूचना मध्ये दिसेल." - }, - "appearance": { - "title": "दृश्यरूप", - "description": "कार्यक्षेत्राचे दृश्यरूप, थीम, फॉन्ट, मजकूर रचना, दिनांक, वेळ आणि भाषा सानुकूलित करा.", - "options": { - "system": "स्वयंचलित", - "light": "लाइट", - "dark": "डार्क" - } - } - }, - "resetCursorColor": { - "title": "दस्तऐवज कर्सरचा रंग रीसेट करा", - "description": "तुम्हाला कर्सरचा रंग रीसेट करायचा आहे का?" - }, - "resetSelectionColor": { - "title": "दस्तऐवज निवडीचा रंग रीसेट करा", - "description": "तुम्हाला निवडीचा रंग रीसेट करायचा आहे का?" - }, - "resetWidth": { - "resetSuccess": "दस्तऐवजाची रुंदी यशस्वीरित्या रीसेट केली" - }, - "theme": { - "title": "थीम", - "description": "पूर्व-निर्धारित थीम निवडा किंवा तुमची स्वतःची थीम अपलोड करा.", - "uploadCustomThemeTooltip": "स्वतःची थीम अपलोड करा" - }, - "workspaceFont": { - "title": "कार्यक्षेत्र फॉन्ट", - "noFontHint": "कोणताही फॉन्ट सापडला नाही, कृपया दुसरा शब्द वापरून पहा." - }, - "textDirection": { - "title": "मजकूर दिशा", - "leftToRight": "डावीकडून उजवीकडे", - "rightToLeft": "उजवीकडून डावीकडे", - "auto": "स्वयंचलित", - "enableRTLItems": "RTL टूलबार घटक सक्षम करा" - }, - "layoutDirection": { - "title": "लेआउट दिशा", - "leftToRight": "डावीकडून उजवीकडे", - "rightToLeft": "उजवीकडून डावीकडे" - }, - "dateTime": { - "title": "दिनांक आणि वेळ", - "example": "{} वाजता {} ({})", - "24HourTime": "२४-तास वेळ", - "dateFormat": { - "label": "दिनांक फॉरमॅट", - "local": "स्थानिक", - "us": "US", - "iso": "ISO", - "friendly": "सुलभ", - "dmy": "D/M/Y" - } - }, - "language": { - "title": "भाषा" - }, - "deleteWorkspacePrompt": { - "title": "कार्यक्षेत्र हटवा", - "content": "तुम्हाला हे कार्यक्षेत्र हटवायचे आहे का? ही कृती पूर्ववत केली जाऊ शकत नाही, आणि तुम्ही प्रकाशित केलेली कोणतीही पृष्ठे अप्रकाशित होतील." - }, - "leaveWorkspacePrompt": { - "title": "कार्यक्षेत्र सोडा", - "content": "तुम्हाला हे कार्यक्षेत्र सोडायचे आहे का? यानंतर तुम्हाला यामधील सर्व पृष्ठे आणि डेटावर प्रवेश मिळणार नाही.", - "success": "तुम्ही कार्यक्षेत्र यशस्वीरित्या सोडले.", - "fail": "कार्यक्षेत्र सोडण्यात अयशस्वी." - }, - "manageWorkspace": { - "title": "कार्यक्षेत्र व्यवस्थापित करा", - "leaveWorkspace": "कार्यक्षेत्र सोडा", - "deleteWorkspace": "कार्यक्षेत्र हटवा" - }, - "manageDataPage": { - "menuLabel": "डेटा व्यवस्थापित करा", - "title": "डेटा व्यवस्थापन", - "description": "@:appName मध्ये स्थानिक डेटा संचयन व्यवस्थापित करा किंवा तुमचा विद्यमान डेटा आयात करा.", - "dataStorage": { - "title": "फाइल संचयन स्थान", - "tooltip": "जिथे तुमच्या फाइल्स संग्रहित आहेत ते स्थान", - "actions": { - "change": "मार्ग बदला", - "open": "फोल्डर उघडा", - "openTooltip": "सध्याच्या डेटा फोल्डरचे स्थान उघडा", - "copy": "मार्ग कॉपी करा", - "copiedHint": "मार्ग कॉपी केला!", - "resetTooltip": "मूलभूत स्थानावर रीसेट करा" - }, - "resetDialog": { - "title": "तुम्हाला खात्री आहे का?", - "description": "पथ मूलभूत डेटा स्थानावर रीसेट केल्याने तुमचा डेटा हटवला जाणार नाही. तुम्हाला सध्याचा डेटा पुन्हा आयात करायचा असल्यास, कृपया आधी त्याचा पथ कॉपी करा." - } - }, - "importData": { - "title": "डेटा आयात करा", - "tooltip": "@:appName बॅकअप/डेटा फोल्डरमधून डेटा आयात करा", - "description": "बाह्य @:appName डेटा फोल्डरमधून डेटा कॉपी करा", - "action": "फाइल निवडा" - }, - "encryption": { - "title": "एनक्रिप्शन", - "tooltip": "तुमचा डेटा कसा संग्रहित आणि एनक्रिप्ट केला जातो ते व्यवस्थापित करा", - "descriptionNoEncryption": "एनक्रिप्शन चालू केल्याने सर्व डेटा एनक्रिप्ट केला जाईल. ही क्रिया पूर्ववत केली जाऊ शकत नाही.", - "descriptionEncrypted": "तुमचा डेटा एनक्रिप्टेड आहे.", - "action": "डेटा एनक्रिप्ट करा", - "dialog": { - "title": "संपूर्ण डेटा एनक्रिप्ट करायचा?", - "description": "तुमचा संपूर्ण डेटा एनक्रिप्ट केल्याने तो सुरक्षित राहील. ही क्रिया पूर्ववत केली जाऊ शकत नाही. तुम्हाला पुढे जायचे आहे का?" - } - }, - "cache": { - "title": "कॅशे साफ करा", - "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", - "dialog": { - "title": "कॅशे साफ करा", - "description": "प्रतिमा न दिसणे, पृष्ठे हरवणे, फॉन्ट लोड न होणे अशा समस्यांचे निराकरण करण्यासाठी मदत करा. याचा तुमच्या डेटावर परिणाम होणार नाही.", - "successHint": "कॅशे साफ झाली!" - } - }, - "data": { - "fixYourData": "तुमचा डेटा सुधारा", - "fixButton": "सुधारा", - "fixYourDataDescription": "तुमच्या डेटामध्ये काही अडचण येत असल्यास, तुम्ही येथे ती सुधारू शकता." - } - }, - "shortcutsPage": { - "menuLabel": "शॉर्टकट्स", - "title": "शॉर्टकट्स", - "editBindingHint": "नवीन बाइंडिंग टाका", - "searchHint": "शोधा", - "actions": { - "resetDefault": "मूलभूत रीसेट करा" - }, - "errorPage": { - "message": "शॉर्टकट्स लोड करण्यात अयशस्वी: {}", - "howToFix": "कृपया पुन्हा प्रयत्न करा. समस्या कायम राहिल्यास GitHub वर संपर्क साधा." - }, - "resetDialog": { - "title": "शॉर्टकट्स रीसेट करा", - "description": "हे सर्व कीबाइंडिंग्जना मूळ स्थितीत रीसेट करेल. ही क्रिया पूर्ववत करता येणार नाही. तुम्हाला खात्री आहे का?", - "buttonLabel": "रीसेट करा" - }, - "conflictDialog": { - "title": "{} आधीच वापरले जात आहे", - "descriptionPrefix": "हे कीबाइंडिंग सध्या ", - "descriptionSuffix": " यामध्ये वापरले जात आहे. तुम्ही हे कीबाइंडिंग बदलल्यास, ते {} मधून काढले जाईल.", - "confirmLabel": "पुढे जा" - }, - "editTooltip": "कीबाइंडिंग संपादित करण्यासाठी क्लिक करा", - "keybindings": { - "toggleToDoList": "टू-डू सूची चालू/बंद करा", - "insertNewParagraphInCodeblock": "नवीन परिच्छेद टाका", - "pasteInCodeblock": "कोडब्लॉकमध्ये पेस्ट करा", - "selectAllCodeblock": "सर्व निवडा", - "indentLineCodeblock": "ओळीच्या सुरुवातीला दोन स्पेस टाका", - "outdentLineCodeblock": "ओळीच्या सुरुवातीची दोन स्पेस काढा", - "twoSpacesCursorCodeblock": "कर्सरवर दोन स्पेस टाका", - "copy": "निवड कॉपी करा", - "paste": "मजकुरात पेस्ट करा", - "cut": "निवड कट करा", - "alignLeft": "मजकूर डावीकडे संरेखित करा", - "alignCenter": "मजकूर मधोमध संरेखित करा", - "alignRight": "मजकूर उजवीकडे संरेखित करा", - "insertInlineMathEquation": "इनलाइन गणितीय सूत्र टाका", - "undo": "पूर्ववत करा", - "redo": "पुन्हा करा", - "convertToParagraph": "ब्लॉक परिच्छेदात रूपांतरित करा", - "backspace": "हटवा", - "deleteLeftWord": "डावीकडील शब्द हटवा", - "deleteLeftSentence": "डावीकडील वाक्य हटवा", - "delete": "उजवीकडील अक्षर हटवा", - "deleteMacOS": "डावीकडील अक्षर हटवा", - "deleteRightWord": "उजवीकडील शब्द हटवा", - "moveCursorLeft": "कर्सर डावीकडे हलवा", - "moveCursorBeginning": "कर्सर सुरुवातीला हलवा", - "moveCursorLeftWord": "कर्सर एक शब्द डावीकडे हलवा", - "moveCursorLeftSelect": "निवडा आणि कर्सर डावीकडे हलवा", - "moveCursorBeginSelect": "निवडा आणि कर्सर सुरुवातीला हलवा", - "moveCursorLeftWordSelect": "निवडा आणि कर्सर एक शब्द डावीकडे हलवा", - "moveCursorRight": "कर्सर उजवीकडे हलवा", - "moveCursorEnd": "कर्सर शेवटी हलवा", - "moveCursorRightWord": "कर्सर एक शब्द उजवीकडे हलवा", - "moveCursorRightSelect": "निवडा आणि कर्सर उजवीकडे हलवा", - "moveCursorEndSelect": "निवडा आणि कर्सर शेवटी हलवा", - "moveCursorRightWordSelect": "निवडा आणि कर्सर एक शब्द उजवीकडे हलवा", - "moveCursorUp": "कर्सर वर हलवा", - "moveCursorTopSelect": "निवडा आणि कर्सर वर हलवा", - "moveCursorTop": "कर्सर वर हलवा", - "moveCursorUpSelect": "निवडा आणि कर्सर वर हलवा", - "moveCursorBottomSelect": "निवडा आणि कर्सर खाली हलवा", - "moveCursorBottom": "कर्सर खाली हलवा", - "moveCursorDown": "कर्सर खाली हलवा", - "moveCursorDownSelect": "निवडा आणि कर्सर खाली हलवा", - "home": "वर स्क्रोल करा", - "end": "खाली स्क्रोल करा", - "toggleBold": "बोल्ड चालू/बंद करा", - "toggleItalic": "इटालिक चालू/बंद करा", - "toggleUnderline": "अधोरेखित चालू/बंद करा", - "toggleStrikethrough": "स्ट्राईकथ्रू चालू/बंद करा", - "toggleCode": "इनलाइन कोड चालू/बंद करा", - "toggleHighlight": "हायलाईट चालू/बंद करा", - "showLinkMenu": "लिंक मेनू दाखवा", - "openInlineLink": "इनलाइन लिंक उघडा", - "openLinks": "सर्व निवडलेले लिंक उघडा", - "indent": "इंडेंट", - "outdent": "आउटडेंट", - "exit": "संपादनातून बाहेर पडा", - "pageUp": "एक पृष्ठ वर स्क्रोल करा", - "pageDown": "एक पृष्ठ खाली स्क्रोल करा", - "selectAll": "सर्व निवडा", - "pasteWithoutFormatting": "फॉरमॅटिंगशिवाय पेस्ट करा", - "showEmojiPicker": "इमोजी निवडकर्ता दाखवा", - "enterInTableCell": "टेबलमध्ये नवीन ओळ जोडा", - "leftInTableCell": "टेबलमध्ये डावीकडील सेलमध्ये जा", - "rightInTableCell": "टेबलमध्ये उजवीकडील सेलमध्ये जा", - "upInTableCell": "टेबलमध्ये वरील सेलमध्ये जा", - "downInTableCell": "टेबलमध्ये खालील सेलमध्ये जा", - "tabInTableCell": "टेबलमध्ये पुढील सेलमध्ये जा", - "shiftTabInTableCell": "टेबलमध्ये मागील सेलमध्ये जा", - "backSpaceInTableCell": "सेलच्या सुरुवातीला थांबा" - }, - "commands": { - "codeBlockNewParagraph": "कोडब्लॉकच्या शेजारी नवीन परिच्छेद टाका", - "codeBlockIndentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीला दोन स्पेस टाका", - "codeBlockOutdentLines": "कोडब्लॉकमध्ये ओळीच्या सुरुवातीची दोन स्पेस काढा", - "codeBlockAddTwoSpaces": "कोडब्लॉकमध्ये कर्सरच्या ठिकाणी दोन स्पेस टाका", - "codeBlockSelectAll": "कोडब्लॉकमधील सर्व मजकूर निवडा", - "codeBlockPasteText": "कोडब्लॉकमध्ये मजकूर पेस्ट करा", - "textAlignLeft": "मजकूर डावीकडे संरेखित करा", - "textAlignCenter": "मजकूर मधोमध संरेखित करा", - "textAlignRight": "मजकूर उजवीकडे संरेखित करा" - }, - "couldNotLoadErrorMsg": "शॉर्टकट लोड करता आले नाहीत. पुन्हा प्रयत्न करा", - "couldNotSaveErrorMsg": "शॉर्टकट सेव्ह करता आले नाहीत. पुन्हा प्रयत्न करा" -}, - "aiPage": { - "title": "AI सेटिंग्ज", - "menuLabel": "AI सेटिंग्ज", - "keys": { - "enableAISearchTitle": "AI शोध", - "aiSettingsDescription": "AppFlowy AI चालवण्यासाठी तुमचा पसंतीचा मॉडेल निवडा. आता GPT-4o, GPT-o3-mini, DeepSeek R1, Claude 3.5 Sonnet आणि Ollama मधील मॉडेल्सचा समावेश आहे.", - "loginToEnableAIFeature": "AI वैशिष्ट्ये फक्त @:appName Cloud मध्ये लॉगिन केल्यानंतरच सक्षम होतात. तुमच्याकडे @:appName खाते नसेल तर 'माय अकाउंट' मध्ये जाऊन साइन अप करा.", - "llmModel": "भाषा मॉडेल", - "llmModelType": "भाषा मॉडेल प्रकार", - "downloadLLMPrompt": "{} डाउनलोड करा", - "downloadAppFlowyOfflineAI": "AI ऑफलाइन पॅकेज डाउनलोड केल्याने तुमच्या डिव्हाइसवर AI चालवता येईल. तुम्हाला पुढे जायचे आहे का?", - "downloadLLMPromptDetail": "{} स्थानिक मॉडेल डाउनलोड करण्यासाठी सुमारे {} संचयन लागेल. तुम्हाला पुढे जायचे आहे का?", - "downloadBigFilePrompt": "डाउनलोड पूर्ण होण्यासाठी सुमारे १० मिनिटे लागू शकतात", - "downloadAIModelButton": "डाउनलोड करा", - "downloadingModel": "डाउनलोड करत आहे", - "localAILoaded": "स्थानिक AI मॉडेल यशस्वीरित्या जोडले गेले आणि वापरण्यास सज्ज आहे", - "localAIStart": "स्थानिक AI सुरू होत आहे. जर हळू वाटत असेल, तर ते बंद करून पुन्हा सुरू करून पहा", - "localAILoading": "स्थानिक AI Chat मॉडेल लोड होत आहे...", - "localAIStopped": "स्थानिक AI थांबले आहे", - "localAIRunning": "स्थानिक AI चालू आहे", - "localAINotReadyRetryLater": "स्थानिक AI सुरू होत आहे, कृपया नंतर पुन्हा प्रयत्न करा", - "localAIDisabled": "तुम्ही स्थानिक AI वापरत आहात, पण ते अकार्यक्षम आहे. कृपया सेटिंग्जमध्ये जाऊन ते सक्षम करा किंवा दुसरे मॉडेल वापरून पहा", - "localAIInitializing": "स्थानिक AI लोड होत आहे. तुमच्या डिव्हाइसवर अवलंबून याला काही मिनिटे लागू शकतात", - "localAINotReadyTextFieldPrompt": "स्थानिक AI लोड होत असताना संपादन करता येणार नाही", - "failToLoadLocalAI": "स्थानिक AI सुरू करण्यात अयशस्वी.", - "restartLocalAI": "पुन्हा सुरू करा", - "disableLocalAITitle": "स्थानिक AI अकार्यक्षम करा", - "disableLocalAIDescription": "तुम्हाला स्थानिक AI अकार्यक्षम करायचा आहे का?", - "localAIToggleTitle": "AppFlowy स्थानिक AI (LAI)", - "localAIToggleSubTitle": "AppFlowy मध्ये अत्याधुनिक स्थानिक AI मॉडेल्स वापरून गोपनीयता आणि सुरक्षा सुनिश्चित करा", - "offlineAIInstruction1": "हे अनुसरा", - "offlineAIInstruction2": "सूचना", - "offlineAIInstruction3": "ऑफलाइन AI सक्षम करण्यासाठी.", - "offlineAIDownload1": "जर तुम्ही AppFlowy AI डाउनलोड केले नसेल, तर कृपया", - "offlineAIDownload2": "डाउनलोड", - "offlineAIDownload3": "करा", - "activeOfflineAI": "सक्रिय", - "downloadOfflineAI": "डाउनलोड करा", - "openModelDirectory": "फोल्डर उघडा", - "laiNotReady": "स्थानिक AI अ‍ॅप योग्य प्रकारे इन्स्टॉल झालेले नाही.", - "ollamaNotReady": "Ollama सर्व्हर तयार नाही.", - "pleaseFollowThese": "कृपया हे अनुसरा", - "instructions": "सूचना", - "installOllamaLai": "Ollama आणि AppFlowy स्थानिक AI सेटअप करण्यासाठी.", - "modelsMissing": "आवश्यक मॉडेल्स सापडत नाहीत.", - "downloadModel": "त्यांना डाउनलोड करण्यासाठी." - } -}, - "planPage": { - "menuLabel": "योजना", - "title": "दर योजना", - "planUsage": { - "title": "योजनेचा वापर सारांश", - "storageLabel": "स्टोरेज", - "storageUsage": "{} पैकी {} GB", - "unlimitedStorageLabel": "अमर्यादित स्टोरेज", - "collaboratorsLabel": "सदस्य", - "collaboratorsUsage": "{} पैकी {}", - "aiResponseLabel": "AI प्रतिसाद", - "aiResponseUsage": "{} पैकी {}", - "unlimitedAILabel": "अमर्यादित AI प्रतिसाद", - "proBadge": "प्रो", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "मॅकसाठी ऑन-डिव्हाइस AI", - "memberProToggle": "अधिक सदस्य आणि अमर्यादित AI", - "aiMaxToggle": "अमर्यादित AI आणि प्रगत मॉडेल्सचा प्रवेश", - "aiOnDeviceToggle": "जास्त गोपनीयतेसाठी स्थानिक AI", - "aiCredit": { - "title": "@:appName AI क्रेडिट जोडा", - "price": "{}", - "priceDescription": "1,000 क्रेडिट्ससाठी", - "purchase": "AI खरेदी करा", - "info": "प्रत्येक कार्यक्षेत्रासाठी 1,000 AI क्रेडिट्स जोडा आणि आपल्या कामाच्या प्रवाहात सानुकूलनीय AI सहजपणे एकत्र करा — अधिक स्मार्ट आणि जलद निकालांसाठी:", - "infoItemOne": "प्रत्येक डेटाबेससाठी 10,000 प्रतिसाद", - "infoItemTwo": "प्रत्येक कार्यक्षेत्रासाठी 1,000 प्रतिसाद" - }, - "currentPlan": { - "bannerLabel": "सद्य योजना", - "freeTitle": "फ्री", - "proTitle": "प्रो", - "teamTitle": "टीम", - "freeInfo": "2 सदस्यांपर्यंत वैयक्तिक वापरासाठी उत्तम", - "proInfo": "10 सदस्यांपर्यंत लहान आणि मध्यम टीमसाठी योग्य", - "teamInfo": "सर्व उत्पादनक्षम आणि सुव्यवस्थित टीमसाठी योग्य", - "upgrade": "योजना बदला", - "canceledInfo": "तुमची योजना रद्द करण्यात आली आहे, {} रोजी तुम्हाला फ्री योजनेवर डाऊनग्रेड केले जाईल." - }, - "addons": { - "title": "ऍड-ऑन्स", - "addLabel": "जोडा", - "activeLabel": "जोडले गेले", - "aiMax": { - "title": "AI Max", - "description": "प्रगत AI मॉडेल्ससह अमर्यादित AI प्रतिसाद आणि दरमहा 50 AI प्रतिमा", - "price": "{}", - "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)" - }, - "aiOnDevice": { - "title": "मॅकसाठी ऑन-डिव्हाइस AI", - "description": "तुमच्या डिव्हाइसवर Mistral 7B, LLAMA 3 आणि अधिक स्थानिक मॉडेल्स चालवा", - "price": "{}", - "priceInfo": "प्रति वापरकर्ता प्रति महिना (वार्षिक बिलिंग)", - "recommend": "M1 किंवा नवीनतम शिफारस केली जाते" - } - }, - "deal": { - "bannerLabel": "नववर्षाचे विशेष ऑफर!", - "title": "तुमची टीम वाढवा!", - "info": "Pro आणि Team योजनांवर 10% सूट मिळवा! @:appName AI सह शक्तिशाली नवीन वैशिष्ट्यांसह तुमचे कार्यक्षेत्र अधिक कार्यक्षम बनवा.", - "viewPlans": "योजना पहा" - } - } -}, - "billingPage": { - "menuLabel": "बिलिंग", - "title": "बिलिंग", - "plan": { - "title": "योजना", - "freeLabel": "फ्री", - "proLabel": "प्रो", - "planButtonLabel": "योजना बदला", - "billingPeriod": "बिलिंग कालावधी", - "periodButtonLabel": "कालावधी संपादित करा" - }, - "paymentDetails": { - "title": "पेमेंट तपशील", - "methodLabel": "पेमेंट पद्धत", - "methodButtonLabel": "पद्धत संपादित करा" - }, - "addons": { - "title": "ऍड-ऑन्स", - "addLabel": "जोडा", - "removeLabel": "काढा", - "renewLabel": "नवीन करा", - "aiMax": { - "label": "AI Max", - "description": "अमर्यादित AI आणि प्रगत मॉडेल्स अनलॉक करा", - "activeDescription": "पुढील बिलिंग तारीख {} आहे", - "canceledDescription": "AI Max {} पर्यंत उपलब्ध असेल" - }, - "aiOnDevice": { - "label": "मॅकसाठी ऑन-डिव्हाइस AI", - "description": "तुमच्या डिव्हाइसवर अमर्यादित ऑन-डिव्हाइस AI अनलॉक करा", - "activeDescription": "पुढील बिलिंग तारीख {} आहे", - "canceledDescription": "मॅकसाठी ऑन-डिव्हाइस AI {} पर्यंत उपलब्ध असेल" - }, - "removeDialog": { - "title": "{} काढा", - "description": "तुम्हाला {plan} काढायचे आहे का? तुम्हाला तत्काळ {plan} चे सर्व फिचर्स आणि फायदे वापरण्याचा अधिकार गमावावा लागेल." - } - }, - "currentPeriodBadge": "सद्य कालावधी", - "changePeriod": "कालावधी बदला", - "planPeriod": "{} कालावधी", - "monthlyInterval": "मासिक", - "monthlyPriceInfo": "प्रति सदस्य, मासिक बिलिंग", - "annualInterval": "वार्षिक", - "annualPriceInfo": "प्रति सदस्य, वार्षिक बिलिंग" -}, - "comparePlanDialog": { - "title": "योजना तुलना आणि निवड", - "planFeatures": "योजनेची\nवैशिष्ट्ये", - "current": "सध्याची", - "actions": { - "upgrade": "अपग्रेड करा", - "downgrade": "डाऊनग्रेड करा", - "current": "सध्याची" - }, - "freePlan": { - "title": "फ्री", - "description": "२ सदस्यांपर्यंत व्यक्तींसाठी सर्व काही व्यवस्थित करण्यासाठी", - "price": "{}", - "priceInfo": "सदैव फ्री" - }, - "proPlan": { - "title": "प्रो", - "description": "लहान टीम्ससाठी प्रोजेक्ट्स व ज्ञान व्यवस्थापनासाठी", - "price": "{}", - "priceInfo": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग\n\n{} मासिक बिलिंगसाठी" - }, - "planLabels": { - "itemOne": "वर्कस्पेसेस", - "itemTwo": "सदस्य", - "itemThree": "स्टोरेज", - "itemFour": "रिअल-टाइम सहकार्य", - "itemFive": "मोबाईल अ‍ॅप", - "itemSix": "AI प्रतिसाद", - "itemSeven": "AI प्रतिमा", - "itemFileUpload": "फाइल अपलोड", - "customNamespace": "सानुकूल नेमस्पेस", - "tooltipSix": "‘Lifetime’ म्हणजे या प्रतिसादांची मर्यादा कधीही रीसेट केली जात नाही", - "intelligentSearch": "स्मार्ट शोध", - "tooltipSeven": "तुमच्या कार्यक्षेत्रासाठी URL चा भाग सानुकूलित करू देते", - "customNamespaceTooltip": "सानुकूल प्रकाशित साइट URL" - }, - "freeLabels": { - "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", - "itemTwo": "२ पर्यंत", - "itemThree": "५ GB", - "itemFour": "होय", - "itemFive": "होय", - "itemSix": "१० कायमस्वरूपी", - "itemSeven": "२ कायमस्वरूपी", - "itemFileUpload": "७ MB पर्यंत", - "intelligentSearch": "स्मार्ट शोध" - }, - "proLabels": { - "itemOne": "प्रत्येक वर्कस्पेसवर शुल्क", - "itemTwo": "१० पर्यंत", - "itemThree": "अमर्यादित", - "itemFour": "होय", - "itemFive": "होय", - "itemSix": "अमर्यादित", - "itemSeven": "दर महिन्याला १० प्रतिमा", - "itemFileUpload": "अमर्यादित", - "intelligentSearch": "स्मार्ट शोध" - }, - "paymentSuccess": { - "title": "तुम्ही आता {} योजनेवर आहात!", - "description": "तुमचे पेमेंट यशस्वीरित्या पूर्ण झाले असून तुमची योजना @:appName {} मध्ये अपग्रेड झाली आहे. तुम्ही 'योजना' पृष्ठावर तपशील पाहू शकता." - }, - "downgradeDialog": { - "title": "तुम्हाला योजना डाऊनग्रेड करायची आहे का?", - "description": "तुमची योजना डाऊनग्रेड केल्यास तुम्ही फ्री योजनेवर परत जाल. काही सदस्यांना या कार्यक्षेत्राचा प्रवेश मिळणार नाही आणि तुम्हाला स्टोरेज मर्यादेप्रमाणे जागा रिकामी करावी लागू शकते.", - "downgradeLabel": "योजना डाऊनग्रेड करा" - } -}, - "cancelSurveyDialog": { - "title": "तुम्ही जात आहात याचे दुःख आहे", - "description": "तुम्ही जात आहात याचे आम्हाला खरोखरच दुःख वाटते. @:appName सुधारण्यासाठी तुमचा अभिप्राय आम्हाला महत्त्वाचा वाटतो. कृपया काही क्षण घेऊन काही प्रश्नांची उत्तरे द्या.", - "commonOther": "इतर", - "otherHint": "तुमचे उत्तर येथे लिहा", - "questionOne": { - "question": "तुम्ही तुमची @:appName Pro सदस्यता का रद्द केली?", - "answerOne": "खर्च खूप जास्त आहे", - "answerTwo": "वैशिष्ट्ये अपेक्षांनुसार नव्हती", - "answerThree": "यापेक्षा चांगला पर्याय सापडला", - "answerFour": "वापर फारसा केला नाही, त्यामुळे खर्च योग्य वाटला नाही", - "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" - }, - "questionTwo": { - "question": "भविष्यात @:appName Pro सदस्यता पुन्हा घेण्याची शक्यता किती आहे?", - "answerOne": "खूप शक्यता आहे", - "answerTwo": "काहीशी शक्यता आहे", - "answerThree": "निश्चित नाही", - "answerFour": "अल्प शक्यता", - "answerFive": "एकदम कमी शक्यता" - }, - "questionThree": { - "question": "तुमच्या सदस्यत्वादरम्यान कोणते Pro वैशिष्ट्य सर्वात उपयुक्त वाटले?", - "answerOne": "अनेक वापरकर्त्यांशी सहकार्य", - "answerTwo": "लांब कालावधीची आवृत्ती इतिहास", - "answerThree": "अमर्यादित AI प्रतिसाद", - "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" - }, - "questionFour": { - "question": "@:appName वापरण्याचा तुमचा एकूण अनुभव कसा होता?", - "answerOne": "खूप छान", - "answerTwo": "चांगला", - "answerThree": "सरासरी", - "answerFour": "सरासरीपेक्षा कमी", - "answerFive": "असंतोषजनक" - } -}, - "common": { - "uploadingFile": "फाईल अपलोड होत आहे. कृपया अ‍ॅप बंद करू नका", - "uploadNotionSuccess": "तुमची Notion zip फाईल यशस्वीरित्या अपलोड झाली आहे. आयात पूर्ण झाल्यानंतर तुम्हाला पुष्टीकरण ईमेल मिळेल", - "reset": "रीसेट करा" -}, - "menu": { - "appearance": "दृश्यरूप", - "language": "भाषा", - "user": "वापरकर्ता", - "files": "फाईल्स", - "notifications": "सूचना", - "open": "सेटिंग्ज उघडा", - "logout": "लॉगआउट", - "logoutPrompt": "तुम्हाला नक्की लॉगआउट करायचे आहे का?", - "selfEncryptionLogoutPrompt": "तुम्हाला खात्रीने लॉगआउट करायचे आहे का? कृपया खात्री करा की तुम्ही एनक्रिप्शन गुप्तकी कॉपी केली आहे", - "syncSetting": "सिंक्रोनायझेशन सेटिंग", - "cloudSettings": "क्लाऊड सेटिंग्ज", - "enableSync": "सिंक्रोनायझेशन सक्षम करा", - "enableSyncLog": "सिंक लॉगिंग सक्षम करा", - "enableSyncLogWarning": "सिंक समस्यांचे निदान करण्यात मदतीसाठी धन्यवाद. हे तुमचे डॉक्युमेंट संपादन स्थानिक फाईलमध्ये लॉग करेल. कृपया सक्षम केल्यानंतर अ‍ॅप बंद करून पुन्हा उघडा", - "enableEncrypt": "डेटा एन्क्रिप्ट करा", - "cloudURL": "बेस URL", - "webURL": "वेब URL", - "invalidCloudURLScheme": "अवैध स्कीम", - "cloudServerType": "क्लाऊड सर्व्हर", - "cloudServerTypeTip": "कृपया लक्षात घ्या की क्लाऊड सर्व्हर बदलल्यास तुम्ही सध्या लॉगिन केलेले खाते लॉगआउट होऊ शकते", - "cloudLocal": "स्थानिक", - "cloudAppFlowy": "@:appName Cloud", - "cloudAppFlowySelfHost": "@:appName क्लाऊड सेल्फ-होस्टेड", - "appFlowyCloudUrlCanNotBeEmpty": "क्लाऊड URL रिकामा असू शकत नाही", - "clickToCopy": "क्लिपबोर्डवर कॉपी करा", - "selfHostStart": "जर तुमच्याकडे सर्व्हर नसेल, तर कृपया हे पाहा", - "selfHostContent": "दस्तऐवज", - "selfHostEnd": "तुमचा स्वतःचा सर्व्हर कसा होस्ट करावा यासाठी मार्गदर्शनासाठी", - "pleaseInputValidURL": "कृपया वैध URL टाका", - "changeUrl": "सेल्फ-होस्टेड URL {} मध्ये बदला", - "cloudURLHint": "तुमच्या सर्व्हरचा बेस URL टाका", - "webURLHint": "तुमच्या वेब सर्व्हरचा बेस URL टाका", - "cloudWSURL": "वेबसॉकेट URL", - "cloudWSURLHint": "तुमच्या सर्व्हरचा वेबसॉकेट पत्ता टाका", - "restartApp": "अ‍ॅप रीस्टार्ट करा", - "restartAppTip": "बदल प्रभावी होण्यासाठी अ‍ॅप रीस्टार्ट करा. कृपया लक्षात घ्या की यामुळे सध्याचे खाते लॉगआउट होऊ शकते.", - "changeServerTip": "सर्व्हर बदलल्यानंतर, बदल लागू करण्यासाठी तुम्हाला 'रीस्टार्ट' बटणावर क्लिक करणे आवश्यक आहे", - "enableEncryptPrompt": "तुमचा डेटा सुरक्षित करण्यासाठी एन्क्रिप्शन सक्रिय करा. ही गुप्तकी सुरक्षित ठेवा; एकदा सक्षम केल्यावर ती बंद करता येणार नाही. जर हरवली तर तुमचा डेटा पुन्हा मिळवता येणार नाही. कॉपी करण्यासाठी क्लिक करा", - "inputEncryptPrompt": "कृपया खालीलसाठी तुमची एनक्रिप्शन गुप्तकी टाका:", - "clickToCopySecret": "गुप्तकी कॉपी करण्यासाठी क्लिक करा", - "configServerSetting": "तुमच्या सर्व्हर सेटिंग्ज कॉन्फिगर करा", - "configServerGuide": "`Quick Start` निवडल्यानंतर, `Settings` → \"Cloud Settings\" मध्ये जा आणि तुमचा सेल्फ-होस्टेड सर्व्हर कॉन्फिगर करा.", - "inputTextFieldHint": "तुमची गुप्तकी", - "historicalUserList": "वापरकर्ता लॉगिन इतिहास", - "historicalUserListTooltip": "ही यादी तुमची अनामिक खाती दर्शवते. तपशील पाहण्यासाठी खात्यावर क्लिक करा. अनामिक खाती 'सुरुवात करा' बटणावर क्लिक करून तयार केली जातात", - "openHistoricalUser": "अनामिक खाते उघडण्यासाठी क्लिक करा", - "customPathPrompt": "@:appName डेटा फोल्डर Google Drive सारख्या क्लाऊड-सिंक फोल्डरमध्ये साठवणे धोकादायक ठरू शकते. फोल्डरमधील डेटाबेस अनेक ठिकाणांवरून एकाच वेळी ऍक्सेस/संपादित केल्यास डेटा सिंक समस्या किंवा भ्रष्ट होण्याची शक्यता असते.", - "importAppFlowyData": "बाह्य @:appName फोल्डरमधून डेटा आयात करा", - "importingAppFlowyDataTip": "डेटा आयात सुरू आहे. कृपया अ‍ॅप बंद करू नका", - "importAppFlowyDataDescription": "बाह्य @:appName फोल्डरमधून डेटा कॉपी करून सध्याच्या AppFlowy डेटा फोल्डरमध्ये आयात करा", - "importSuccess": "@:appName डेटा फोल्डर यशस्वीरित्या आयात झाला", - "importFailed": "@:appName डेटा फोल्डर आयात करण्यात अयशस्वी", - "importGuide": "अधिक माहितीसाठी, कृपया संदर्भित दस्तऐवज पहा" -}, - "notifications": { - "enableNotifications": { - "label": "सूचना सक्षम करा", - "hint": "स्थानिक सूचना दिसू नयेत यासाठी बंद करा." - }, - "showNotificationsIcon": { - "label": "सूचना चिन्ह दाखवा", - "hint": "साइडबारमध्ये सूचना चिन्ह लपवण्यासाठी बंद करा." - }, - "archiveNotifications": { - "allSuccess": "सर्व सूचना यशस्वीरित्या संग्रहित केल्या", - "success": "सूचना यशस्वीरित्या संग्रहित केली" - }, - "markAsReadNotifications": { - "allSuccess": "सर्व वाचलेल्या म्हणून चिन्हांकित केल्या", - "success": "वाचलेले म्हणून चिन्हांकित केले" - }, - "action": { - "markAsRead": "वाचलेले म्हणून चिन्हांकित करा", - "multipleChoice": "अधिक निवडा", - "archive": "संग्रहित करा" - }, - "settings": { - "settings": "सेटिंग्ज", - "markAllAsRead": "सर्व वाचलेले म्हणून चिन्हांकित करा", - "archiveAll": "सर्व संग्रहित करा" - }, - "emptyInbox": { - "title": "इनबॉक्स झिरो!", - "description": "इथे सूचना मिळवण्यासाठी रिमाइंडर सेट करा." - }, - "emptyUnread": { - "title": "कोणतीही न वाचलेली सूचना नाही", - "description": "तुम्ही सर्व वाचले आहे!" - }, - "emptyArchived": { - "title": "कोणतीही संग्रहित सूचना नाही", - "description": "संग्रहित सूचना इथे दिसतील." - }, - "tabs": { - "inbox": "इनबॉक्स", - "unread": "न वाचलेले", - "archived": "संग्रहित" - }, - "refreshSuccess": "सूचना यशस्वीरित्या रीफ्रेश केल्या", - "titles": { - "notifications": "सूचना", - "reminder": "रिमाइंडर" - } -}, - "appearance": { - "resetSetting": "रीसेट", - "fontFamily": { - "label": "फॉन्ट फॅमिली", - "search": "शोध", - "defaultFont": "सिस्टम" - }, - "themeMode": { - "label": "थीम मोड", - "light": "लाइट मोड", - "dark": "डार्क मोड", - "system": "सिस्टमशी जुळवा" - }, - "fontScaleFactor": "फॉन्ट स्केल घटक", - "displaySize": "डिस्प्ले आकार", - "documentSettings": { - "cursorColor": "डॉक्युमेंट कर्सरचा रंग", - "selectionColor": "डॉक्युमेंट निवडीचा रंग", - "width": "डॉक्युमेंटची रुंदी", - "changeWidth": "बदला", - "pickColor": "रंग निवडा", - "colorShade": "रंगाची छटा", - "opacity": "अपारदर्शकता", - "hexEmptyError": "Hex रंग रिकामा असू शकत नाही", - "hexLengthError": "Hex व्हॅल्यू 6 अंकांची असावी", - "hexInvalidError": "अवैध Hex व्हॅल्यू", - "opacityEmptyError": "अपारदर्शकता रिकामी असू शकत नाही", - "opacityRangeError": "अपारदर्शकता 1 ते 100 दरम्यान असावी", - "app": "अ‍ॅप", - "flowy": "Flowy", - "apply": "लागू करा" - }, - "layoutDirection": { - "label": "लेआउट दिशा", - "hint": "तुमच्या स्क्रीनवरील कंटेंटचा प्रवाह नियंत्रित करा — डावीकडून उजवीकडे किंवा उजवीकडून डावीकडे.", - "ltr": "LTR", - "rtl": "RTL" - }, - "textDirection": { - "label": "मूलभूत मजकूर दिशा", - "hint": "मजकूर डावीकडून सुरू व्हावा की उजवीकडून याचे पूर्वनिर्धारित निर्धारण करा.", - "ltr": "LTR", - "rtl": "RTL", - "auto": "स्वयं", - "fallback": "लेआउट दिशेशी जुळवा" - }, - "themeUpload": { - "button": "अपलोड", - "uploadTheme": "थीम अपलोड करा", - "description": "खालील बटण वापरून तुमची स्वतःची @:appName थीम अपलोड करा.", - "loading": "कृपया प्रतीक्षा करा. आम्ही तुमची थीम पडताळत आहोत आणि अपलोड करत आहोत...", - "uploadSuccess": "तुमची थीम यशस्वीरित्या अपलोड झाली आहे", - "deletionFailure": "थीम हटवण्यात अयशस्वी. कृपया ती मॅन्युअली हटवून पाहा.", - "filePickerDialogTitle": ".flowy_plugin फाईल निवडा", - "urlUploadFailure": "URL उघडण्यात अयशस्वी: {}" - }, - "theme": "थीम", - "builtInsLabel": "अंतर्गत थीम्स", - "pluginsLabel": "प्लगइन्स", - "dateFormat": { - "label": "दिनांक फॉरमॅट", - "local": "स्थानिक", - "us": "US", - "iso": "ISO", - "friendly": "अनौपचारिक", - "dmy": "D/M/Y" - }, - "timeFormat": { - "label": "वेळ फॉरमॅट", - "twelveHour": "१२ तास", - "twentyFourHour": "२४ तास" - }, - "showNamingDialogWhenCreatingPage": "पृष्ठ तयार करताना नाव विचारणारा डायलॉग दाखवा", - "enableRTLToolbarItems": "RTL टूलबार आयटम्स सक्षम करा", - "members": { - "title": "सदस्य सेटिंग्ज", - "inviteMembers": "सदस्यांना आमंत्रण द्या", - "inviteHint": "ईमेलद्वारे आमंत्रण द्या", - "sendInvite": "आमंत्रण पाठवा", - "copyInviteLink": "आमंत्रण दुवा कॉपी करा", - "label": "सदस्य", - "user": "वापरकर्ता", - "role": "भूमिका", - "removeFromWorkspace": "वर्कस्पेसमधून काढा", - "removeFromWorkspaceSuccess": "वर्कस्पेसमधून यशस्वीरित्या काढले", - "removeFromWorkspaceFailed": "वर्कस्पेसमधून काढण्यात अयशस्वी", - "owner": "मालक", - "guest": "अतिथी", - "member": "सदस्य", - "memberHintText": "सदस्य पृष्ठे वाचू व संपादित करू शकतो", - "guestHintText": "अतिथी पृष्ठे वाचू शकतो, प्रतिक्रिया देऊ शकतो, टिप्पणी करू शकतो, आणि परवानगी असल्यास काही पृष्ठे संपादित करू शकतो.", - "emailInvalidError": "अवैध ईमेल, कृपया तपासा व पुन्हा प्रयत्न करा", - "emailSent": "ईमेल पाठवला गेला आहे, कृपया इनबॉक्स तपासा", - "members": "सदस्य", - "membersCount": { - "zero": "{} सदस्य", - "one": "{} सदस्य", - "other": "{} सदस्य" - }, - "inviteFailedDialogTitle": "आमंत्रण पाठवण्यात अयशस्वी", - "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, अधिक सदस्यांसाठी कृपया अपग्रेड करा.", - "inviteFailedMemberLimitMobile": "तुमच्या वर्कस्पेसने सदस्य मर्यादा गाठली आहे.", - "memberLimitExceeded": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आमंत्रित करण्यासाठी कृपया ", - "memberLimitExceededUpgrade": "अपग्रेड करा", - "memberLimitExceededPro": "सदस्य मर्यादा गाठली आहे, अधिक सदस्य आवश्यक असल्यास संपर्क करा", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "सदस्य जोडण्यात अयशस्वी", - "addMemberSuccess": "सदस्य यशस्वीरित्या जोडला गेला", - "removeMember": "सदस्य काढा", - "areYouSureToRemoveMember": "तुम्हाला हा सदस्य काढायचा आहे का?", - "inviteMemberSuccess": "आमंत्रण यशस्वीरित्या पाठवले गेले", - "failedToInviteMember": "सदस्य आमंत्रित करण्यात अयशस्वी", - "workspaceMembersError": "अरे! काहीतरी चूक झाली आहे", - "workspaceMembersErrorDescription": "आम्ही सध्या सदस्यांची यादी लोड करू शकत नाही. कृपया नंतर पुन्हा प्रयत्न करा" - } -}, - "files": { - "copy": "कॉपी करा", - "defaultLocation": "फाईल्स आणि डेटाचा संचय स्थान", - "exportData": "तुमचा डेटा निर्यात करा", - "doubleTapToCopy": "पथ कॉपी करण्यासाठी दोनदा टॅप करा", - "restoreLocation": "@:appName चे मूळ स्थान पुनर्संचयित करा", - "customizeLocation": "इतर फोल्डर उघडा", - "restartApp": "बदल लागू करण्यासाठी कृपया अ‍ॅप रीस्टार्ट करा.", - "exportDatabase": "डेटाबेस निर्यात करा", - "selectFiles": "निर्यात करण्यासाठी फाईल्स निवडा", - "selectAll": "सर्व निवडा", - "deselectAll": "सर्व निवड रद्द करा", - "createNewFolder": "नवीन फोल्डर तयार करा", - "createNewFolderDesc": "तुमचा डेटा कुठे साठवायचा हे सांगा", - "defineWhereYourDataIsStored": "तुमचा डेटा कुठे साठवला जातो हे ठरवा", - "open": "उघडा", - "openFolder": "आधीक फोल्डर उघडा", - "openFolderDesc": "तुमच्या विद्यमान @:appName फोल्डरमध्ये वाचन व लेखन करा", - "folderHintText": "फोल्डरचे नाव", - "location": "नवीन फोल्डर तयार करत आहे", - "locationDesc": "तुमच्या @:appName डेटासाठी नाव निवडा", - "browser": "ब्राउझ करा", - "create": "तयार करा", - "set": "सेट करा", - "folderPath": "फोल्डर साठवण्याचा मार्ग", - "locationCannotBeEmpty": "मार्ग रिकामा असू शकत नाही", - "pathCopiedSnackbar": "फाईल संचय मार्ग क्लिपबोर्डवर कॉपी केला!", - "changeLocationTooltips": "डेटा डिरेक्टरी बदला", - "change": "बदला", - "openLocationTooltips": "इतर डेटा डिरेक्टरी उघडा", - "openCurrentDataFolder": "सध्याची डेटा डिरेक्टरी उघडा", - "recoverLocationTooltips": "@:appName च्या मूळ डेटा डिरेक्टरीवर रीसेट करा", - "exportFileSuccess": "फाईल यशस्वीरित्या निर्यात झाली!", - "exportFileFail": "फाईल निर्यात करण्यात अयशस्वी!", - "export": "निर्यात करा", - "clearCache": "कॅशे साफ करा", - "clearCacheDesc": "प्रतिमा लोड होत नाहीत किंवा फॉन्ट अयोग्यरित्या दिसत असल्यास, कॅशे साफ करून पाहा. ही क्रिया तुमचा वापरकर्ता डेटा हटवणार नाही.", - "areYouSureToClearCache": "तुम्हाला नक्की कॅशे साफ करायची आहे का?", - "clearCacheSuccess": "कॅशे यशस्वीरित्या साफ झाली!" -}, - "user": { - "name": "नाव", - "email": "ईमेल", - "tooltipSelectIcon": "चिन्ह निवडा", - "selectAnIcon": "चिन्ह निवडा", - "pleaseInputYourOpenAIKey": "कृपया तुमची AI की टाका", - "clickToLogout": "सध्याचा वापरकर्ता लॉगआउट करण्यासाठी क्लिक करा" -}, - "mobile": { - "personalInfo": "वैयक्तिक माहिती", - "username": "वापरकर्तानाव", - "usernameEmptyError": "वापरकर्तानाव रिकामे असू शकत नाही", - "about": "विषयी", - "pushNotifications": "पुश सूचना", - "support": "सपोर्ट", - "joinDiscord": "Discord मध्ये सहभागी व्हा", - "privacyPolicy": "गोपनीयता धोरण", - "userAgreement": "वापरकर्ता करार", - "termsAndConditions": "अटी व शर्ती", - "userprofileError": "वापरकर्ता प्रोफाइल लोड करण्यात अयशस्वी", - "userprofileErrorDescription": "कृपया लॉगआउट करून पुन्हा लॉगिन करा आणि त्रुटी अजूनही येते का ते पहा.", - "selectLayout": "लेआउट निवडा", - "selectStartingDay": "सप्ताहाचा प्रारंभ दिवस निवडा", - "version": "आवृत्ती" -}, - "grid": { - "deleteView": "तुम्हाला हे दृश्य हटवायचे आहे का?", - "createView": "नवीन", - "title": { - "placeholder": "नाव नाही" - }, - "settings": { - "filter": "फिल्टर", - "sort": "क्रमवारी", - "sortBy": "यावरून क्रमवारी लावा", - "properties": "गुणधर्म", - "reorderPropertiesTooltip": "गुणधर्मांचे स्थान बदला", - "group": "समूह", - "addFilter": "फिल्टर जोडा", - "deleteFilter": "फिल्टर हटवा", - "filterBy": "यावरून फिल्टर करा", - "typeAValue": "मूल्य लिहा...", - "layout": "लेआउट", - "compactMode": "कॉम्पॅक्ट मोड", - "databaseLayout": "लेआउट", - "viewList": { - "zero": "० दृश्ये", - "one": "{count} दृश्य", - "other": "{count} दृश्ये" - }, - "editView": "दृश्य संपादित करा", - "boardSettings": "बोर्ड सेटिंग", - "calendarSettings": "कॅलेंडर सेटिंग", - "createView": "नवीन दृश्य", - "duplicateView": "दृश्याची प्रत बनवा", - "deleteView": "दृश्य हटवा", - "numberOfVisibleFields": "{} दर्शविले" - }, - "filter": { - "empty": "कोणतेही सक्रिय फिल्टर नाहीत", - "addFilter": "फिल्टर जोडा", - "cannotFindCreatableField": "फिल्टर करण्यासाठी योग्य फील्ड सापडले नाही", - "conditon": "अट", - "where": "जिथे" - }, - "textFilter": { - "contains": "अंतर्भूत आहे", - "doesNotContain": "अंतर्भूत नाही", - "endsWith": "याने समाप्त होते", - "startWith": "याने सुरू होते", - "is": "आहे", - "isNot": "नाही", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही", - "choicechipPrefix": { - "isNot": "नाही", - "startWith": "याने सुरू होते", - "endWith": "याने समाप्त होते", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" - } - }, - "checkboxFilter": { - "isChecked": "निवडलेले आहे", - "isUnchecked": "निवडलेले नाही", - "choicechipPrefix": { - "is": "आहे" - } - }, - "checklistFilter": { - "isComplete": "पूर्ण झाले आहे", - "isIncomplted": "अपूर्ण आहे" - }, - "selectOptionFilter": { - "is": "आहे", - "isNot": "नाही", - "contains": "अंतर्भूत आहे", - "doesNotContain": "अंतर्भूत नाही", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" -}, -"dateFilter": { - "is": "या दिवशी आहे", - "before": "पूर्वी आहे", - "after": "नंतर आहे", - "onOrBefore": "या दिवशी किंवा त्याआधी आहे", - "onOrAfter": "या दिवशी किंवा त्यानंतर आहे", - "between": "दरम्यान आहे", - "empty": "रिकामे आहे", - "notEmpty": "रिकामे नाही", - "startDate": "सुरुवातीची तारीख", - "endDate": "शेवटची तारीख", - "choicechipPrefix": { - "before": "पूर्वी", - "after": "नंतर", - "between": "दरम्यान", - "onOrBefore": "या दिवशी किंवा त्याआधी", - "onOrAfter": "या दिवशी किंवा त्यानंतर", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" - } -}, -"numberFilter": { - "equal": "बरोबर आहे", - "notEqual": "बरोबर नाही", - "lessThan": "पेक्षा कमी आहे", - "greaterThan": "पेक्षा जास्त आहे", - "lessThanOrEqualTo": "किंवा कमी आहे", - "greaterThanOrEqualTo": "किंवा जास्त आहे", - "isEmpty": "रिकामे आहे", - "isNotEmpty": "रिकामे नाही" -}, -"field": { - "label": "गुणधर्म", - "hide": "गुणधर्म लपवा", - "show": "गुणधर्म दर्शवा", - "insertLeft": "डावीकडे जोडा", - "insertRight": "उजवीकडे जोडा", - "duplicate": "प्रत बनवा", - "delete": "हटवा", - "wrapCellContent": "पाठ लपेटा", - "clear": "सेल्स रिकामे करा", - "switchPrimaryFieldTooltip": "प्राथमिक फील्डचा प्रकार बदलू शकत नाही", - "textFieldName": "मजकूर", - "checkboxFieldName": "चेकबॉक्स", - "dateFieldName": "तारीख", - "updatedAtFieldName": "शेवटचे अपडेट", - "createdAtFieldName": "तयार झाले", - "numberFieldName": "संख्या", - "singleSelectFieldName": "सिंगल सिलेक्ट", - "multiSelectFieldName": "मल्टीसिलेक्ट", - "urlFieldName": "URL", - "checklistFieldName": "चेकलिस्ट", - "relationFieldName": "संबंध", - "summaryFieldName": "AI सारांश", - "timeFieldName": "वेळ", - "mediaFieldName": "फाईल्स आणि मीडिया", - "translateFieldName": "AI भाषांतर", - "translateTo": "मध्ये भाषांतर करा", - "numberFormat": "संख्या स्वरूप", - "dateFormat": "तारीख स्वरूप", - "includeTime": "वेळ जोडा", - "isRange": "शेवटची तारीख", - "dateFormatFriendly": "महिना दिवस, वर्ष", - "dateFormatISO": "वर्ष-महिना-दिनांक", - "dateFormatLocal": "महिना/दिवस/वर्ष", - "dateFormatUS": "वर्ष/महिना/दिवस", - "dateFormatDayMonthYear": "दिवस/महिना/वर्ष", - "timeFormat": "वेळ स्वरूप", - "invalidTimeFormat": "अवैध स्वरूप", - "timeFormatTwelveHour": "१२ तास", - "timeFormatTwentyFourHour": "२४ तास", - "clearDate": "तारीख हटवा", - "dateTime": "तारीख व वेळ", - "startDateTime": "सुरुवातीची तारीख व वेळ", - "endDateTime": "शेवटची तारीख व वेळ", - "failedToLoadDate": "तारीख मूल्य लोड करण्यात अयशस्वी", - "selectTime": "वेळ निवडा", - "selectDate": "तारीख निवडा", - "visibility": "दृश्यता", - "propertyType": "गुणधर्माचा प्रकार", - "addSelectOption": "पर्याय जोडा", - "typeANewOption": "नवीन पर्याय लिहा", - "optionTitle": "पर्याय", - "addOption": "पर्याय जोडा", - "editProperty": "गुणधर्म संपादित करा", - "newProperty": "नवीन गुणधर्म", - "openRowDocument": "पृष्ठ म्हणून उघडा", - "deleteFieldPromptMessage": "तुम्हाला खात्री आहे का? हा गुणधर्म आणि त्याचा डेटा हटवला जाईल", - "clearFieldPromptMessage": "तुम्हाला खात्री आहे का? या कॉलममधील सर्व सेल्स रिकामे होतील", - "newColumn": "नवीन कॉलम", - "format": "स्वरूप", - "reminderOnDateTooltip": "या सेलमध्ये अनुस्मारक शेड्यूल केले आहे", - "optionAlreadyExist": "पर्याय आधीच अस्तित्वात आहे" -}, - "rowPage": { - "newField": "नवीन फील्ड जोडा", - "fieldDragElementTooltip": "मेनू उघडण्यासाठी क्लिक करा", - "showHiddenFields": { - "one": "{count} लपलेले फील्ड दाखवा", - "many": "{count} लपलेली फील्ड दाखवा", - "other": "{count} लपलेली फील्ड दाखवा" - }, - "hideHiddenFields": { - "one": "{count} लपलेले फील्ड लपवा", - "many": "{count} लपलेली फील्ड लपवा", - "other": "{count} लपलेली फील्ड लपवा" - }, - "openAsFullPage": "पूर्ण पृष्ठ म्हणून उघडा", - "moreRowActions": "अधिक पंक्ती क्रिया" -}, -"sort": { - "ascending": "चढत्या क्रमाने", - "descending": "उतरत्या क्रमाने", - "by": "द्वारे", - "empty": "सक्रिय सॉर्ट्स नाहीत", - "cannotFindCreatableField": "सॉर्टसाठी योग्य फील्ड सापडले नाही", - "deleteAllSorts": "सर्व सॉर्ट्स हटवा", - "addSort": "सॉर्ट जोडा", - "sortsActive": "सॉर्टिंग करत असताना {intention} करू शकत नाही", - "removeSorting": "या दृश्यातील सर्व सॉर्ट्स हटवायचे आहेत का?", - "fieldInUse": "या फील्डवर आधीच सॉर्टिंग चालू आहे" -}, -"row": { - "label": "पंक्ती", - "duplicate": "प्रत बनवा", - "delete": "हटवा", - "titlePlaceholder": "शीर्षक नाही", - "textPlaceholder": "रिक्त", - "copyProperty": "गुणधर्म क्लिपबोर्डवर कॉपी केला", - "count": "संख्या", - "newRow": "नवीन पंक्ती", - "loadMore": "अधिक लोड करा", - "action": "क्रिया", - "add": "खाली जोडा वर क्लिक करा", - "drag": "हलवण्यासाठी ड्रॅग करा", - "deleteRowPrompt": "तुम्हाला खात्री आहे का की ही पंक्ती हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "deleteCardPrompt": "तुम्हाला खात्री आहे का की हे कार्ड हटवायचे आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "dragAndClick": "हलवण्यासाठी ड्रॅग करा, मेनू उघडण्यासाठी क्लिक करा", - "insertRecordAbove": "वर रेकॉर्ड जोडा", - "insertRecordBelow": "खाली रेकॉर्ड जोडा", - "noContent": "माहिती नाही", - "reorderRowDescription": "पंक्तीचे पुन्हा क्रमांकन", - "createRowAboveDescription": "वर पंक्ती तयार करा", - "createRowBelowDescription": "खाली पंक्ती जोडा" -}, -"selectOption": { - "create": "तयार करा", - "purpleColor": "जांभळा", - "pinkColor": "गुलाबी", - "lightPinkColor": "फिकट गुलाबी", - "orangeColor": "नारंगी", - "yellowColor": "पिवळा", - "limeColor": "लिंबू", - "greenColor": "हिरवा", - "aquaColor": "आक्वा", - "blueColor": "निळा", - "deleteTag": "टॅग हटवा", - "colorPanelTitle": "रंग", - "panelTitle": "पर्याय निवडा किंवा नवीन तयार करा", - "searchOption": "पर्याय शोधा", - "searchOrCreateOption": "पर्याय शोधा किंवा तयार करा", - "createNew": "नवीन तयार करा", - "orSelectOne": "किंवा पर्याय निवडा", - "typeANewOption": "नवीन पर्याय टाइप करा", - "tagName": "टॅग नाव" -}, -"checklist": { - "taskHint": "कार्याचे वर्णन", - "addNew": "नवीन कार्य जोडा", - "submitNewTask": "तयार करा", - "hideComplete": "पूर्ण कार्ये लपवा", - "showComplete": "सर्व कार्ये दाखवा" -}, -"url": { - "launch": "बाह्य ब्राउझरमध्ये लिंक उघडा", - "copy": "लिंक क्लिपबोर्डवर कॉपी करा", - "textFieldHint": "URL टाका", - "copiedNotification": "क्लिपबोर्डवर कॉपी केले!" -}, -"relation": { - "relatedDatabasePlaceLabel": "संबंधित डेटाबेस", - "relatedDatabasePlaceholder": "काही नाही", - "inRelatedDatabase": "या मध्ये", - "rowSearchTextFieldPlaceholder": "शोध", - "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया खालील यादीतून एक निवडा:", - "emptySearchResult": "कोणतीही नोंद सापडली नाही", - "linkedRowListLabel": "{count} लिंक केलेल्या पंक्ती", - "unlinkedRowListLabel": "आणखी एक पंक्ती लिंक करा" -}, -"menuName": "ग्रिड", -"referencedGridPrefix": "दृश्य", -"calculate": "गणना करा", -"calculationTypeLabel": { - "none": "काही नाही", - "average": "सरासरी", - "max": "कमाल", - "median": "मध्यम", - "min": "किमान", - "sum": "बेरीज", - "count": "मोजणी", - "countEmpty": "रिकाम्यांची मोजणी", - "countEmptyShort": "रिक्त", - "countNonEmpty": "रिक्त नसलेल्यांची मोजणी", - "countNonEmptyShort": "भरलेले" -}, -"media": { - "rename": "पुन्हा नाव द्या", - "download": "डाउनलोड करा", - "expand": "मोठे करा", - "delete": "हटवा", - "moreFilesHint": "+{}", - "addFileOrImage": "फाईल किंवा लिंक जोडा", - "attachmentsHint": "{}", - "addFileMobile": "फाईल जोडा", - "extraCount": "+{}", - "deleteFileDescription": "तुम्हाला खात्री आहे का की ही फाईल हटवायची आहे? ही क्रिया पूर्ववत करता येणार नाही.", - "showFileNames": "फाईलचे नाव दाखवा", - "downloadSuccess": "फाईल डाउनलोड झाली", - "downloadFailedToken": "फाईल डाउनलोड अयशस्वी, वापरकर्ता टोकन अनुपलब्ध", - "setAsCover": "कव्हर म्हणून सेट करा", - "openInBrowser": "ब्राउझरमध्ये उघडा", - "embedLink": "फाईल लिंक एम्बेड करा" - } -}, - "document": { - "menuName": "दस्तऐवज", - "date": { - "timeHintTextInTwelveHour": "01:00 PM", - "timeHintTextInTwentyFourHour": "13:00" - }, - "creating": "तयार करत आहे...", - "slashMenu": { - "board": { - "selectABoardToLinkTo": "लिंक करण्यासाठी बोर्ड निवडा", - "createANewBoard": "नवीन बोर्ड तयार करा" - }, - "grid": { - "selectAGridToLinkTo": "लिंक करण्यासाठी ग्रिड निवडा", - "createANewGrid": "नवीन ग्रिड तयार करा" - }, - "calendar": { - "selectACalendarToLinkTo": "लिंक करण्यासाठी दिनदर्शिका निवडा", - "createANewCalendar": "नवीन दिनदर्शिका तयार करा" - }, - "document": { - "selectADocumentToLinkTo": "लिंक करण्यासाठी दस्तऐवज निवडा" - }, - "name": { - "textStyle": "मजकुराची शैली", - "list": "यादी", - "toggle": "टॉगल", - "fileAndMedia": "फाईल व मीडिया", - "simpleTable": "सोपे टेबल", - "visuals": "दृश्य घटक", - "document": "दस्तऐवज", - "advanced": "प्रगत", - "text": "मजकूर", - "heading1": "शीर्षक 1", - "heading2": "शीर्षक 2", - "heading3": "शीर्षक 3", - "image": "प्रतिमा", - "bulletedList": "बुलेट यादी", - "numberedList": "क्रमांकित यादी", - "todoList": "करण्याची यादी", - "doc": "दस्तऐवज", - "linkedDoc": "पृष्ठाशी लिंक करा", - "grid": "ग्रिड", - "linkedGrid": "लिंक केलेला ग्रिड", - "kanban": "कानबन", - "linkedKanban": "लिंक केलेला कानबन", - "calendar": "दिनदर्शिका", - "linkedCalendar": "लिंक केलेली दिनदर्शिका", - "quote": "उद्धरण", - "divider": "विभाजक", - "table": "टेबल", - "callout": "महत्त्वाचा मजकूर", - "outline": "रूपरेषा", - "mathEquation": "गणिती समीकरण", - "code": "कोड", - "toggleList": "टॉगल यादी", - "toggleHeading1": "टॉगल शीर्षक 1", - "toggleHeading2": "टॉगल शीर्षक 2", - "toggleHeading3": "टॉगल शीर्षक 3", - "emoji": "इमोजी", - "aiWriter": "AI ला काहीही विचारा", - "dateOrReminder": "दिनांक किंवा स्मरणपत्र", - "photoGallery": "फोटो गॅलरी", - "file": "फाईल", - "twoColumns": "२ स्तंभ", - "threeColumns": "३ स्तंभ", - "fourColumns": "४ स्तंभ" - }, - "subPage": { - "name": "दस्तऐवज", - "keyword1": "उपपृष्ठ", - "keyword2": "पृष्ठ", - "keyword3": "चाइल्ड पृष्ठ", - "keyword4": "पृष्ठ जोडा", - "keyword5": "एम्बेड पृष्ठ", - "keyword6": "नवीन पृष्ठ", - "keyword7": "पृष्ठ तयार करा", - "keyword8": "दस्तऐवज" - } - }, - "selectionMenu": { - "outline": "रूपरेषा", - "codeBlock": "कोड ब्लॉक" - }, - "plugins": { - "referencedBoard": "संदर्भित बोर्ड", - "referencedGrid": "संदर्भित ग्रिड", - "referencedCalendar": "संदर्भित दिनदर्शिका", - "referencedDocument": "संदर्भित दस्तऐवज", - "aiWriter": { - "userQuestion": "AI ला काहीही विचारा", - "continueWriting": "लेखन सुरू ठेवा", - "fixSpelling": "स्पेलिंग व व्याकरण सुधारणा", - "improveWriting": "लेखन सुधारित करा", - "summarize": "सारांश द्या", - "explain": "स्पष्टीकरण द्या", - "makeShorter": "लहान करा", - "makeLonger": "मोठे करा" - }, - "autoGeneratorMenuItemName": "AI लेखक", -"autoGeneratorTitleName": "AI: काहीही लिहिण्यासाठी AI ला विचारा...", -"autoGeneratorLearnMore": "अधिक जाणून घ्या", -"autoGeneratorGenerate": "उत्पन्न करा", -"autoGeneratorHintText": "AI ला विचारा...", -"autoGeneratorCantGetOpenAIKey": "AI की मिळवू शकलो नाही", -"autoGeneratorRewrite": "पुन्हा लिहा", -"smartEdit": "AI ला विचारा", -"aI": "AI", -"smartEditFixSpelling": "स्पेलिंग आणि व्याकरण सुधारा", -"warning": "⚠️ AI उत्तरं चुकीची किंवा दिशाभूल करणारी असू शकतात.", -"smartEditSummarize": "सारांश द्या", -"smartEditImproveWriting": "लेखन सुधारित करा", -"smartEditMakeLonger": "लांब करा", -"smartEditCouldNotFetchResult": "AI कडून उत्तर मिळवता आले नाही", -"smartEditCouldNotFetchKey": "AI की मिळवता आली नाही", -"smartEditDisabled": "सेटिंग्जमध्ये AI कनेक्ट करा", -"appflowyAIEditDisabled": "AI वैशिष्ट्ये सक्षम करण्यासाठी साइन इन करा", -"discardResponse": "AI उत्तर फेकून द्यायचं आहे का?", -"createInlineMathEquation": "समीकरण तयार करा", -"fonts": "फॉन्ट्स", -"insertDate": "तारीख जोडा", -"emoji": "इमोजी", -"toggleList": "टॉगल यादी", -"emptyToggleHeading": "रिकामे टॉगल h{}. मजकूर जोडण्यासाठी क्लिक करा.", -"emptyToggleList": "रिकामी टॉगल यादी. मजकूर जोडण्यासाठी क्लिक करा.", -"emptyToggleHeadingWeb": "रिकामे टॉगल h{level}. मजकूर जोडण्यासाठी क्लिक करा", -"quoteList": "उद्धरण यादी", -"numberedList": "क्रमांकित यादी", -"bulletedList": "बुलेट यादी", -"todoList": "करण्याची यादी", -"callout": "ठळक मजकूर", -"simpleTable": { - "moreActions": { - "color": "रंग", - "align": "पंक्तिबद्ध करा", - "delete": "हटा", - "duplicate": "डुप्लिकेट करा", - "insertLeft": "डावीकडे घाला", - "insertRight": "उजवीकडे घाला", - "insertAbove": "वर घाला", - "insertBelow": "खाली घाला", - "headerColumn": "हेडर स्तंभ", - "headerRow": "हेडर ओळ", - "clearContents": "सामग्री साफ करा", - "setToPageWidth": "पृष्ठाच्या रुंदीप्रमाणे सेट करा", - "distributeColumnsWidth": "स्तंभ समान करा", - "duplicateRow": "ओळ डुप्लिकेट करा", - "duplicateColumn": "स्तंभ डुप्लिकेट करा", - "textColor": "मजकूराचा रंग", - "cellBackgroundColor": "सेलचा पार्श्वभूमी रंग", - "duplicateTable": "टेबल डुप्लिकेट करा" - }, - "clickToAddNewRow": "नवीन ओळ जोडण्यासाठी क्लिक करा", - "clickToAddNewColumn": "नवीन स्तंभ जोडण्यासाठी क्लिक करा", - "clickToAddNewRowAndColumn": "नवीन ओळ आणि स्तंभ जोडण्यासाठी क्लिक करा", - "headerName": { - "table": "टेबल", - "alignText": "मजकूर पंक्तिबद्ध करा" - } -}, -"cover": { - "changeCover": "कव्हर बदला", - "colors": "रंग", - "images": "प्रतिमा", - "clearAll": "सर्व साफ करा", - "abstract": "ऍबस्ट्रॅक्ट", - "addCover": "कव्हर जोडा", - "addLocalImage": "स्थानिक प्रतिमा जोडा", - "invalidImageUrl": "अवैध प्रतिमा URL", - "failedToAddImageToGallery": "प्रतिमा गॅलरीत जोडता आली नाही", - "enterImageUrl": "प्रतिमा URL लिहा", - "add": "जोडा", - "back": "मागे", - "saveToGallery": "गॅलरीत जतन करा", - "removeIcon": "आयकॉन काढा", - "removeCover": "कव्हर काढा", - "pasteImageUrl": "प्रतिमा URL पेस्ट करा", - "or": "किंवा", - "pickFromFiles": "फाईल्समधून निवडा", - "couldNotFetchImage": "प्रतिमा मिळवता आली नाही", - "imageSavingFailed": "प्रतिमा जतन करणे अयशस्वी", - "addIcon": "आयकॉन जोडा", - "changeIcon": "आयकॉन बदला", - "coverRemoveAlert": "हे हटवल्यानंतर कव्हरमधून काढले जाईल.", - "alertDialogConfirmation": "तुम्हाला खात्री आहे का? तुम्हाला पुढे जायचे आहे?" -}, -"mathEquation": { - "name": "गणिती समीकरण", - "addMathEquation": "TeX समीकरण जोडा", - "editMathEquation": "गणिती समीकरण संपादित करा" -}, -"optionAction": { - "click": "क्लिक", - "toOpenMenu": "मेनू उघडण्यासाठी", - "drag": "ओढा", - "toMove": "हलवण्यासाठी", - "delete": "हटा", - "duplicate": "डुप्लिकेट करा", - "turnInto": "मध्ये बदला", - "moveUp": "वर हलवा", - "moveDown": "खाली हलवा", - "color": "रंग", - "align": "पंक्तिबद्ध करा", - "left": "डावीकडे", - "center": "मध्यभागी", - "right": "उजवीकडे", - "defaultColor": "डिफॉल्ट", - "depth": "खोली", - "copyLinkToBlock": "ब्लॉकसाठी लिंक कॉपी करा" -}, - "image": { - "addAnImage": "प्रतिमा जोडा", - "copiedToPasteBoard": "प्रतिमेची लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "addAnImageDesktop": "प्रतिमा जोडा", - "addAnImageMobile": "एक किंवा अधिक प्रतिमा जोडण्यासाठी क्लिक करा", - "dropImageToInsert": "प्रतिमा ड्रॉप करून जोडा", - "imageUploadFailed": "प्रतिमा अपलोड करण्यात अयशस्वी", - "imageDownloadFailed": "प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "imageDownloadFailedToken": "युजर टोकन नसल्यामुळे प्रतिमा डाउनलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "errorCode": "त्रुटी कोड" -}, -"photoGallery": { - "name": "फोटो गॅलरी", - "imageKeyword": "प्रतिमा", - "imageGalleryKeyword": "प्रतिमा गॅलरी", - "photoKeyword": "फोटो", - "photoBrowserKeyword": "फोटो ब्राउझर", - "galleryKeyword": "गॅलरी", - "addImageTooltip": "प्रतिमा जोडा", - "changeLayoutTooltip": "लेआउट बदला", - "browserLayout": "ब्राउझर", - "gridLayout": "ग्रिड", - "deleteBlockTooltip": "संपूर्ण गॅलरी हटवा" -}, -"math": { - "copiedToPasteBoard": "समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे" -}, -"urlPreview": { - "copiedToPasteBoard": "लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "convertToLink": "एंबेड लिंकमध्ये रूपांतर करा" -}, -"outline": { - "addHeadingToCreateOutline": "सामग्री यादी तयार करण्यासाठी शीर्षके जोडा.", - "noMatchHeadings": "जुळणारी शीर्षके आढळली नाहीत." -}, -"table": { - "addAfter": "नंतर जोडा", - "addBefore": "आधी जोडा", - "delete": "हटा", - "clear": "सामग्री साफ करा", - "duplicate": "डुप्लिकेट करा", - "bgColor": "पार्श्वभूमीचा रंग" -}, -"contextMenu": { - "copy": "कॉपी करा", - "cut": "कापा", - "paste": "पेस्ट करा", - "pasteAsPlainText": "साध्या मजकूराच्या स्वरूपात पेस्ट करा" -}, -"action": "कृती", -"database": { - "selectDataSource": "डेटा स्रोत निवडा", - "noDataSource": "डेटा स्रोत नाही", - "selectADataSource": "डेटा स्रोत निवडा", - "toContinue": "पुढे जाण्यासाठी", - "newDatabase": "नवीन डेटाबेस", - "linkToDatabase": "डेटाबेसशी लिंक करा" -}, -"date": "तारीख", -"video": { - "label": "व्हिडिओ", - "emptyLabel": "व्हिडिओ जोडा", - "placeholder": "व्हिडिओ लिंक पेस्ट करा", - "copiedToPasteBoard": "व्हिडिओ लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "insertVideo": "व्हिडिओ जोडा", - "invalidVideoUrl": "ही URL सध्या समर्थित नाही.", - "invalidVideoUrlYouTube": "YouTube सध्या समर्थित नाही.", - "supportedFormats": "समर्थित स्वरूप: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" -}, -"file": { - "name": "फाईल", - "uploadTab": "अपलोड", - "uploadMobile": "फाईल निवडा", - "uploadMobileGallery": "फोटो गॅलरीमधून", - "networkTab": "लिंक एम्बेड करा", - "placeholderText": "फाईल अपलोड किंवा एम्बेड करा", - "placeholderDragging": "फाईल ड्रॉप करून अपलोड करा", - "dropFileToUpload": "फाईल ड्रॉप करून अपलोड करा", - "fileUploadHint": "फाईल ड्रॅग आणि ड्रॉप करा किंवा क्लिक करा ", - "fileUploadHintSuffix": "ब्राउझ करा", - "networkHint": "फाईल लिंक पेस्ट करा", - "networkUrlInvalid": "अवैध URL. कृपया तपासा आणि पुन्हा प्रयत्न करा.", - "networkAction": "एम्बेड", - "fileTooBigError": "फाईल खूप मोठी आहे, कृपया 10MB पेक्षा कमी फाईल अपलोड करा", - "renameFile": { - "title": "फाईलचे नाव बदला", - "description": "या फाईलसाठी नवीन नाव लिहा", - "nameEmptyError": "फाईलचे नाव रिकामे असू शकत नाही." - }, - "uploadedAt": "{} रोजी अपलोड केले", - "linkedAt": "{} रोजी लिंक जोडली", - "failedToOpenMsg": "उघडण्यात अयशस्वी, फाईल सापडली नाही" -}, -"subPage": { - "handlingPasteHint": " - (पेस्ट प्रक्रिया करत आहे)", - "errors": { - "failedDeletePage": "पृष्ठ हटवण्यात अयशस्वी", - "failedCreatePage": "पृष्ठ तयार करण्यात अयशस्वी", - "failedMovePage": "हे पृष्ठ हलवण्यात अयशस्वी", - "failedDuplicatePage": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी", - "failedDuplicateFindView": "पृष्ठ डुप्लिकेट करण्यात अयशस्वी - मूळ दृश्य सापडले नाही" - } -}, - "cannotMoveToItsChildren": "मुलांमध्ये हलवू शकत नाही" -}, -"outlineBlock": { - "placeholder": "सामग्री सूची" -}, -"textBlock": { - "placeholder": "कमांडसाठी '/' टाइप करा" -}, -"title": { - "placeholder": "शीर्षक नाही" -}, -"imageBlock": { - "placeholder": "प्रतिमा जोडण्यासाठी क्लिक करा", - "upload": { - "label": "अपलोड", - "placeholder": "प्रतिमा अपलोड करण्यासाठी क्लिक करा" - }, - "url": { - "label": "प्रतिमेची URL", - "placeholder": "प्रतिमेची URL टाका" - }, - "ai": { - "label": "AI द्वारे प्रतिमा तयार करा", - "placeholder": "AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" - }, - "stability_ai": { - "label": "Stability AI द्वारे प्रतिमा तयार करा", - "placeholder": "Stability AI द्वारे प्रतिमा तयार करण्यासाठी कृपया संकेत द्या" - }, - "support": "प्रतिमेचा कमाल आकार 5MB आहे. समर्थित स्वरूप: JPEG, PNG, GIF, SVG", - "error": { - "invalidImage": "अवैध प्रतिमा", - "invalidImageSize": "प्रतिमेचा आकार 5MB पेक्षा कमी असावा", - "invalidImageFormat": "प्रतिमेचे स्वरूप समर्थित नाही. समर्थित स्वरूप: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "अवैध प्रतिमेची URL", - "noImage": "अशी फाईल किंवा निर्देशिका नाही", - "multipleImagesFailed": "एक किंवा अधिक प्रतिमा अपलोड करण्यात अयशस्वी, कृपया पुन्हा प्रयत्न करा" - }, - "embedLink": { - "label": "लिंक एम्बेड करा", - "placeholder": "प्रतिमेची लिंक पेस्ट करा किंवा टाका" - }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "प्रतिमा शोधा", - "pleaseInputYourOpenAIKey": "कृपया सेटिंग्ज पृष्ठात आपला AI की प्रविष्ट करा", - "saveImageToGallery": "प्रतिमा जतन करा", - "failedToAddImageToGallery": "प्रतिमा जतन करण्यात अयशस्वी", - "successToAddImageToGallery": "प्रतिमा 'Photos' मध्ये जतन केली", - "unableToLoadImage": "प्रतिमा लोड करण्यात अयशस्वी", - "maximumImageSize": "कमाल प्रतिमा अपलोड आकार 10MB आहे", - "uploadImageErrorImageSizeTooBig": "प्रतिमेचा आकार 10MB पेक्षा कमी असावा", - "imageIsUploading": "प्रतिमा अपलोड होत आहे", - "openFullScreen": "पूर्ण स्क्रीनमध्ये उघडा", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "मागील प्रतिमा", - "nextImageTooltip": "पुढील प्रतिमा", - "zoomOutTooltip": "लहान करा", - "zoomInTooltip": "मोठी करा", - "changeZoomLevelTooltip": "झूम पातळी बदला", - "openLocalImage": "प्रतिमा उघडा", - "downloadImage": "प्रतिमा डाउनलोड करा", - "closeViewer": "इंटरअॅक्टिव्ह व्ह्युअर बंद करा", - "scalePercentage": "{}%", - "deleteImageTooltip": "प्रतिमा हटवा" - } - } -}, - "codeBlock": { - "language": { - "label": "भाषा", - "placeholder": "भाषा निवडा", - "auto": "स्वयंचलित" - }, - "copyTooltip": "कॉपी करा", - "searchLanguageHint": "भाषा शोधा", - "codeCopiedSnackbar": "कोड क्लिपबोर्डवर कॉपी झाला!" -}, -"inlineLink": { - "placeholder": "लिंक पेस्ट करा किंवा टाका", - "openInNewTab": "नवीन टॅबमध्ये उघडा", - "copyLink": "लिंक कॉपी करा", - "removeLink": "लिंक काढा", - "url": { - "label": "लिंक URL", - "placeholder": "लिंक URL टाका" - }, - "title": { - "label": "लिंक शीर्षक", - "placeholder": "लिंक शीर्षक टाका" - } -}, -"mention": { - "placeholder": "कोणीतरी, पृष्ठ किंवा तारीख नमूद करा...", - "page": { - "label": "पृष्ठाला लिंक करा", - "tooltip": "पृष्ठ उघडण्यासाठी क्लिक करा" - }, - "deleted": "हटवले गेले", - "deletedContent": "ही सामग्री अस्तित्वात नाही किंवा हटवण्यात आली आहे", - "noAccess": "प्रवेश नाही", - "deletedPage": "हटवलेले पृष्ठ", - "trashHint": " - ट्रॅशमध्ये", - "morePages": "अजून पृष्ठे" -}, -"toolbar": { - "resetToDefaultFont": "डीफॉल्ट फॉन्टवर परत जा", - "textSize": "मजकूराचा आकार", - "textColor": "मजकूराचा रंग", - "h1": "मथळा 1", - "h2": "मथळा 2", - "h3": "मथळा 3", - "alignLeft": "डावीकडे संरेखित करा", - "alignRight": "उजवीकडे संरेखित करा", - "alignCenter": "मध्यभागी संरेखित करा", - "link": "लिंक", - "textAlign": "मजकूर संरेखन", - "moreOptions": "अधिक पर्याय", - "font": "फॉन्ट", - "inlineCode": "इनलाइन कोड", - "suggestions": "सूचना", - "turnInto": "मध्ये रूपांतरित करा", - "equation": "समीकरण", - "insert": "घाला", - "linkInputHint": "लिंक पेस्ट करा किंवा पृष्ठे शोधा", - "pageOrURL": "पृष्ठ किंवा URL", - "linkName": "लिंकचे नाव", - "linkNameHint": "लिंकचे नाव प्रविष्ट करा" -}, -"errorBlock": { - "theBlockIsNotSupported": "ब्लॉक सामग्री पार्स करण्यात अक्षम", - "clickToCopyTheBlockContent": "ब्लॉक सामग्री कॉपी करण्यासाठी क्लिक करा", - "blockContentHasBeenCopied": "ब्लॉक सामग्री कॉपी केली आहे.", - "parseError": "{} ब्लॉक पार्स करताना त्रुटी आली.", - "copyBlockContent": "ब्लॉक सामग्री कॉपी करा" -}, -"mobilePageSelector": { - "title": "पृष्ठ निवडा", - "failedToLoad": "पृष्ठ यादी लोड करण्यात अयशस्वी", - "noPagesFound": "कोणतीही पृष्ठे सापडली नाहीत" -}, -"attachmentMenu": { - "choosePhoto": "फोटो निवडा", - "takePicture": "फोटो काढा", - "chooseFile": "फाईल निवडा" - } - }, - "board": { - "column": { - "label": "स्तंभ", - "createNewCard": "नवीन", - "renameGroupTooltip": "गटाचे नाव बदलण्यासाठी क्लिक करा", - "createNewColumn": "नवीन गट जोडा", - "addToColumnTopTooltip": "वर नवीन कार्ड जोडा", - "addToColumnBottomTooltip": "खाली नवीन कार्ड जोडा", - "renameColumn": "स्तंभाचे नाव बदला", - "hideColumn": "लपवा", - "newGroup": "नवीन गट", - "deleteColumn": "हटवा", - "deleteColumnConfirmation": "हा गट आणि त्यामधील सर्व कार्ड्स हटवले जातील. तुम्हाला खात्री आहे का?" - }, - "hiddenGroupSection": { - "sectionTitle": "लपवलेले गट", - "collapseTooltip": "लपवलेले गट लपवा", - "expandTooltip": "लपवलेले गट पाहा" - }, - "cardDetail": "कार्ड तपशील", - "cardActions": "कार्ड क्रिया", - "cardDuplicated": "कार्डची प्रत तयार झाली", - "cardDeleted": "कार्ड हटवले गेले", - "showOnCard": "कार्ड तपशिलावर दाखवा", - "setting": "सेटिंग", - "propertyName": "गुणधर्माचे नाव", - "menuName": "बोर्ड", - "showUngrouped": "गटात नसलेली कार्ड्स दाखवा", - "ungroupedButtonText": "गट नसलेली", - "ungroupedButtonTooltip": "ज्या कार्ड्स कोणत्याही गटात नाहीत", - "ungroupedItemsTitle": "बोर्डमध्ये जोडण्यासाठी क्लिक करा", - "groupBy": "या आधारावर गट करा", - "groupCondition": "गट स्थिती", - "referencedBoardPrefix": "याचे दृश्य", - "notesTooltip": "नोट्स आहेत", - "mobile": { - "editURL": "URL संपादित करा", - "showGroup": "गट दाखवा", - "showGroupContent": "हा गट बोर्डवर दाखवायचा आहे का?", - "failedToLoad": "बोर्ड दृश्य लोड होण्यात अयशस्वी" - }, - "dateCondition": { - "weekOf": "{} - {} ची आठवडा", - "today": "आज", - "yesterday": "काल", - "tomorrow": "उद्या", - "lastSevenDays": "शेवटचे ७ दिवस", - "nextSevenDays": "पुढील ७ दिवस", - "lastThirtyDays": "शेवटचे ३० दिवस", - "nextThirtyDays": "पुढील ३० दिवस" - }, - "noGroup": "गटासाठी कोणताही गुणधर्म निवडलेला नाही", - "noGroupDesc": "बोर्ड दृश्य दाखवण्यासाठी गट करण्याचा एक गुणधर्म आवश्यक आहे", - "media": { - "cardText": "{} {}", - "fallbackName": "फायली" - } -}, - "calendar": { - "menuName": "कॅलेंडर", - "defaultNewCalendarTitle": "नाव नाही", - "newEventButtonTooltip": "नवीन इव्हेंट जोडा", - "navigation": { - "today": "आज", - "jumpToday": "आजवर जा", - "previousMonth": "मागील महिना", - "nextMonth": "पुढील महिना", - "views": { - "day": "दिवस", - "week": "आठवडा", - "month": "महिना", - "year": "वर्ष" - } - }, - "mobileEventScreen": { - "emptyTitle": "सध्या कोणतेही इव्हेंट नाहीत", - "emptyBody": "या दिवशी इव्हेंट तयार करण्यासाठी प्लस बटणावर क्लिक करा." - }, - "settings": { - "showWeekNumbers": "आठवड्याचे क्रमांक दाखवा", - "showWeekends": "सप्ताहांत दाखवा", - "firstDayOfWeek": "आठवड्याची सुरुवात", - "layoutDateField": "कॅलेंडर मांडणी दिनांकानुसार", - "changeLayoutDateField": "मांडणी फील्ड बदला", - "noDateTitle": "तारीख नाही", - "noDateHint": { - "zero": "नियोजित नसलेली इव्हेंट्स येथे दिसतील", - "one": "{count} नियोजित नसलेली इव्हेंट", - "other": "{count} नियोजित नसलेल्या इव्हेंट्स" - }, - "unscheduledEventsTitle": "नियोजित नसलेल्या इव्हेंट्स", - "clickToAdd": "कॅलेंडरमध्ये जोडण्यासाठी क्लिक करा", - "name": "कॅलेंडर सेटिंग्ज", - "clickToOpen": "रेकॉर्ड उघडण्यासाठी क्लिक करा" - }, - "referencedCalendarPrefix": "याचे दृश्य", - "quickJumpYear": "या वर्षावर जा", - "duplicateEvent": "इव्हेंट डुप्लिकेट करा" -}, - "errorDialog": { - "title": "@:appName त्रुटी", - "howToFixFallback": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया GitHub पेजवर त्रुटीबद्दल माहिती देणारे एक issue सबमिट करा.", - "howToFixFallbackHint1": "या गैरसोयीबद्दल आम्ही दिलगीर आहोत! कृपया ", - "howToFixFallbackHint2": " पेजवर त्रुटीचे वर्णन करणारे issue सबमिट करा.", - "github": "GitHub वर पहा" -}, -"search": { - "label": "शोध", - "sidebarSearchIcon": "पृष्ठ शोधा आणि पटकन जा", - "placeholder": { - "actions": "कृती शोधा..." - } -}, -"message": { - "copy": { - "success": "कॉपी झाले!", - "fail": "कॉपी करू शकत नाही" - } -}, -"unSupportBlock": "सध्याचा व्हर्जन या ब्लॉकला समर्थन देत नाही.", -"views": { - "deleteContentTitle": "तुम्हाला हे {pageType} हटवायचे आहे का?", - "deleteContentCaption": "हे {pageType} हटवल्यास, तुम्ही ते trash मधून पुनर्संचयित करू शकता." -}, - "colors": { - "custom": "सानुकूल", - "default": "डीफॉल्ट", - "red": "लाल", - "orange": "संत्रा", - "yellow": "पिवळा", - "green": "हिरवा", - "blue": "निळा", - "purple": "जांभळा", - "pink": "गुलाबी", - "brown": "तपकिरी", - "gray": "करड्या रंगाचा" -}, - "emoji": { - "emojiTab": "इमोजी", - "search": "इमोजी शोधा", - "noRecent": "अलीकडील कोणतेही इमोजी नाहीत", - "noEmojiFound": "कोणतेही इमोजी सापडले नाहीत", - "filter": "फिल्टर", - "random": "योगायोगाने", - "selectSkinTone": "त्वचेचा टोन निवडा", - "remove": "इमोजी काढा", - "categories": { - "smileys": "स्मायली आणि भावना", - "people": "लोक", - "animals": "प्राणी आणि निसर्ग", - "food": "अन्न", - "activities": "क्रिया", - "places": "स्थळे", - "objects": "वस्तू", - "symbols": "चिन्हे", - "flags": "ध्वज", - "nature": "निसर्ग", - "frequentlyUsed": "नेहमी वापरलेले" - }, - "skinTone": { - "default": "डीफॉल्ट", - "light": "हलका", - "mediumLight": "मध्यम-हलका", - "medium": "मध्यम", - "mediumDark": "मध्यम-गडद", - "dark": "गडद" - }, - "openSourceIconsFrom": "खुल्या स्रोताचे आयकॉन्स" -}, - "inlineActions": { - "noResults": "निकाल नाही", - "recentPages": "अलीकडील पृष्ठे", - "pageReference": "पृष्ठ संदर्भ", - "docReference": "दस्तऐवज संदर्भ", - "boardReference": "बोर्ड संदर्भ", - "calReference": "कॅलेंडर संदर्भ", - "gridReference": "ग्रिड संदर्भ", - "date": "तारीख", - "reminder": { - "groupTitle": "स्मरणपत्र", - "shortKeyword": "remind" - }, - "createPage": "\"{}\" उप-पृष्ठ तयार करा" -}, - "datePicker": { - "dateTimeFormatTooltip": "सेटिंग्जमध्ये तारीख आणि वेळ फॉरमॅट बदला", - "dateFormat": "तारीख फॉरमॅट", - "includeTime": "वेळ समाविष्ट करा", - "isRange": "शेवटची तारीख", - "timeFormat": "वेळ फॉरमॅट", - "clearDate": "तारीख साफ करा", - "reminderLabel": "स्मरणपत्र", - "selectReminder": "स्मरणपत्र निवडा", - "reminderOptions": { - "none": "काहीही नाही", - "atTimeOfEvent": "इव्हेंटच्या वेळी", - "fiveMinsBefore": "५ मिनिटे आधी", - "tenMinsBefore": "१० मिनिटे आधी", - "fifteenMinsBefore": "१५ मिनिटे आधी", - "thirtyMinsBefore": "३० मिनिटे आधी", - "oneHourBefore": "१ तास आधी", - "twoHoursBefore": "२ तास आधी", - "onDayOfEvent": "इव्हेंटच्या दिवशी", - "oneDayBefore": "१ दिवस आधी", - "twoDaysBefore": "२ दिवस आधी", - "oneWeekBefore": "१ आठवडा आधी", - "custom": "सानुकूल" - } -}, - "relativeDates": { - "yesterday": "काल", - "today": "आज", - "tomorrow": "उद्या", - "oneWeek": "१ आठवडा" -}, - "notificationHub": { - "title": "सूचना", - "mobile": { - "title": "अपडेट्स" - }, - "emptyTitle": "सर्व पूर्ण झाले!", - "emptyBody": "कोणतीही प्रलंबित सूचना किंवा कृती नाहीत. शांततेचा आनंद घ्या.", - "tabs": { - "inbox": "इनबॉक्स", - "upcoming": "आगामी" - }, - "actions": { - "markAllRead": "सर्व वाचलेल्या म्हणून चिन्हित करा", - "showAll": "सर्व", - "showUnreads": "न वाचलेल्या" - }, - "filters": { - "ascending": "आरोही", - "descending": "अवरोही", - "groupByDate": "तारीखेनुसार गटबद्ध करा", - "showUnreadsOnly": "फक्त न वाचलेल्या दाखवा", - "resetToDefault": "डीफॉल्टवर रीसेट करा" - } -}, - "reminderNotification": { - "title": "स्मरणपत्र", - "message": "तुम्ही विसरण्याआधी हे तपासण्याचे लक्षात ठेवा!", - "tooltipDelete": "हटवा", - "tooltipMarkRead": "वाचले म्हणून चिन्हित करा", - "tooltipMarkUnread": "न वाचले म्हणून चिन्हित करा" -}, - "findAndReplace": { - "find": "शोधा", - "previousMatch": "मागील जुळणारे", - "nextMatch": "पुढील जुळणारे", - "close": "बंद करा", - "replace": "बदला", - "replaceAll": "सर्व बदला", - "noResult": "कोणतेही निकाल नाहीत", - "caseSensitive": "केस सेंसिटिव्ह", - "searchMore": "अधिक निकालांसाठी शोधा" -}, - "error": { - "weAreSorry": "आम्ही क्षमस्व आहोत", - "loadingViewError": "हे दृश्य लोड करण्यात अडचण येत आहे. कृपया तुमचे इंटरनेट कनेक्शन तपासा, अ‍ॅप रीफ्रेश करा, आणि समस्या कायम असल्यास आमच्याशी संपर्क साधा.", - "syncError": "इतर डिव्हाइसमधून डेटा सिंक झाला नाही", - "syncErrorHint": "कृपया हे पृष्ठ शेवटचे जिथे संपादित केले होते त्या डिव्हाइसवर उघडा, आणि मग सध्याच्या डिव्हाइसवर पुन्हा उघडा.", - "clickToCopy": "एरर कोड कॉपी करण्यासाठी क्लिक करा" -}, - "editor": { - "bold": "जाड", - "bulletedList": "बुलेट यादी", - "bulletedListShortForm": "बुलेट", - "checkbox": "चेकबॉक्स", - "embedCode": "कोड एम्बेड करा", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "हायलाइट", - "color": "रंग", - "image": "प्रतिमा", - "date": "तारीख", - "page": "पृष्ठ", - "italic": "तिरका", - "link": "लिंक", - "numberedList": "क्रमांकित यादी", - "numberedListShortForm": "क्रमांकित", - "toggleHeading1ShortForm": "Toggle H1", - "toggleHeading2ShortForm": "Toggle H2", - "toggleHeading3ShortForm": "Toggle H3", - "quote": "कोट", - "strikethrough": "ओढून टाका", - "text": "मजकूर", - "underline": "अधोरेखित", - "fontColorDefault": "डीफॉल्ट", - "fontColorGray": "धूसर", - "fontColorBrown": "तपकिरी", - "fontColorOrange": "केशरी", - "fontColorYellow": "पिवळा", - "fontColorGreen": "हिरवा", - "fontColorBlue": "निळा", - "fontColorPurple": "जांभळा", - "fontColorPink": "पिंग", - "fontColorRed": "लाल", - "backgroundColorDefault": "डीफॉल्ट पार्श्वभूमी", - "backgroundColorGray": "धूसर पार्श्वभूमी", - "backgroundColorBrown": "तपकिरी पार्श्वभूमी", - "backgroundColorOrange": "केशरी पार्श्वभूमी", - "backgroundColorYellow": "पिवळी पार्श्वभूमी", - "backgroundColorGreen": "हिरवी पार्श्वभूमी", - "backgroundColorBlue": "निळी पार्श्वभूमी", - "backgroundColorPurple": "जांभळी पार्श्वभूमी", - "backgroundColorPink": "पिंग पार्श्वभूमी", - "backgroundColorRed": "लाल पार्श्वभूमी", - "backgroundColorLime": "लिंबू पार्श्वभूमी", - "backgroundColorAqua": "पाण्याचा पार्श्वभूमी", - "done": "पूर्ण", - "cancel": "रद्द करा", - "tint1": "टिंट 1", - "tint2": "टिंट 2", - "tint3": "टिंट 3", - "tint4": "टिंट 4", - "tint5": "टिंट 5", - "tint6": "टिंट 6", - "tint7": "टिंट 7", - "tint8": "टिंट 8", - "tint9": "टिंट 9", - "lightLightTint1": "जांभळा", - "lightLightTint2": "पिंग", - "lightLightTint3": "फिकट पिंग", - "lightLightTint4": "केशरी", - "lightLightTint5": "पिवळा", - "lightLightTint6": "लिंबू", - "lightLightTint7": "हिरवा", - "lightLightTint8": "पाणी", - "lightLightTint9": "निळा", - "urlHint": "URL", - "mobileHeading1": "Heading 1", - "mobileHeading2": "Heading 2", - "mobileHeading3": "Heading 3", - "mobileHeading4": "Heading 4", - "mobileHeading5": "Heading 5", - "mobileHeading6": "Heading 6", - "textColor": "मजकूराचा रंग", - "backgroundColor": "पार्श्वभूमीचा रंग", - "addYourLink": "तुमची लिंक जोडा", - "openLink": "लिंक उघडा", - "copyLink": "लिंक कॉपी करा", - "removeLink": "लिंक काढा", - "editLink": "लिंक संपादित करा", - "linkText": "मजकूर", - "linkTextHint": "कृपया मजकूर प्रविष्ट करा", - "linkAddressHint": "कृपया URL प्रविष्ट करा", - "highlightColor": "हायलाइट रंग", - "clearHighlightColor": "हायलाइट काढा", - "customColor": "स्वतःचा रंग", - "hexValue": "Hex मूल्य", - "opacity": "अपारदर्शकता", - "resetToDefaultColor": "डीफॉल्ट रंगावर रीसेट करा", - "ltr": "LTR", - "rtl": "RTL", - "auto": "स्वयंचलित", - "cut": "कट", - "copy": "कॉपी", - "paste": "पेस्ट", - "find": "शोधा", - "select": "निवडा", - "selectAll": "सर्व निवडा", - "previousMatch": "मागील जुळणारे", - "nextMatch": "पुढील जुळणारे", - "closeFind": "बंद करा", - "replace": "बदला", - "replaceAll": "सर्व बदला", - "regex": "Regex", - "caseSensitive": "केस सेंसिटिव्ह", - "uploadImage": "प्रतिमा अपलोड करा", - "urlImage": "URL प्रतिमा", - "incorrectLink": "चुकीची लिंक", - "upload": "अपलोड", - "chooseImage": "प्रतिमा निवडा", - "loading": "लोड करत आहे", - "imageLoadFailed": "प्रतिमा लोड करण्यात अयशस्वी", - "divider": "विभाजक", - "table": "तक्त्याचे स्वरूप", - "colAddBefore": "यापूर्वी स्तंभ जोडा", - "rowAddBefore": "यापूर्वी पंक्ती जोडा", - "colAddAfter": "यानंतर स्तंभ जोडा", - "rowAddAfter": "यानंतर पंक्ती जोडा", - "colRemove": "स्तंभ काढा", - "rowRemove": "पंक्ती काढा", - "colDuplicate": "स्तंभ डुप्लिकेट", - "rowDuplicate": "पंक्ती डुप्लिकेट", - "colClear": "सामग्री साफ करा", - "rowClear": "सामग्री साफ करा", - "slashPlaceHolder": "'/' टाइप करा आणि घटक जोडा किंवा टाइप सुरू करा", - "typeSomething": "काहीतरी लिहा...", - "toggleListShortForm": "टॉगल", - "quoteListShortForm": "कोट", - "mathEquationShortForm": "सूत्र", - "codeBlockShortForm": "कोड" -}, - "favorite": { - "noFavorite": "कोणतेही आवडते पृष्ठ नाही", - "noFavoriteHintText": "पृष्ठाला डावीकडे स्वाइप करा आणि ते आवडत्या यादीत जोडा", - "removeFromSidebar": "साइडबारमधून काढा", - "addToSidebar": "साइडबारमध्ये पिन करा" -}, -"cardDetails": { - "notesPlaceholder": "/ टाइप करा ब्लॉक घालण्यासाठी, किंवा टाइप करायला सुरुवात करा" -}, -"blockPlaceholders": { - "todoList": "करण्याची यादी", - "bulletList": "यादी", - "numberList": "क्रमांकित यादी", - "quote": "कोट", - "heading": "मथळा {}" -}, -"titleBar": { - "pageIcon": "पृष्ठ चिन्ह", - "language": "भाषा", - "font": "फॉन्ट", - "actions": "क्रिया", - "date": "तारीख", - "addField": "फील्ड जोडा", - "userIcon": "वापरकर्त्याचे चिन्ह" -}, -"noLogFiles": "कोणतीही लॉग फाइल्स नाहीत", -"newSettings": { - "myAccount": { - "title": "माझे खाते", - "subtitle": "तुमचा प्रोफाइल सानुकूल करा, खाते सुरक्षा व्यवस्थापित करा, AI कीज पहा किंवा लॉगिन करा.", - "profileLabel": "खाते नाव आणि प्रोफाइल चित्र", - "profileNamePlaceholder": "तुमचे नाव प्रविष्ट करा", - "accountSecurity": "खाते सुरक्षा", - "2FA": "2-स्टेप प्रमाणीकरण", - "aiKeys": "AI कीज", - "accountLogin": "खाते लॉगिन", - "updateNameError": "नाव अपडेट करण्यात अयशस्वी", - "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", - "aboutAppFlowy": "@:appName विषयी", - "deleteAccount": { - "title": "खाते हटवा", - "subtitle": "तुमचे खाते आणि सर्व डेटा कायमचे हटवा.", - "description": "तुमचे खाते कायमचे हटवले जाईल आणि सर्व वर्कस्पेसमधून प्रवेश काढून टाकला जाईल.", - "deleteMyAccount": "माझे खाते हटवा", - "dialogTitle": "खाते हटवा", - "dialogContent1": "तुम्हाला खात्री आहे की तुम्ही तुमचे खाते कायमचे हटवू इच्छिता?", - "dialogContent2": "ही क्रिया पूर्ववत केली जाऊ शकत नाही. हे सर्व वर्कस्पेसमधून प्रवेश हटवेल, खाजगी वर्कस्पेस मिटवेल, आणि सर्व शेअर्ड वर्कस्पेसमधून काढून टाकेल.", - "confirmHint1": "कृपया पुष्टीसाठी \"@:newSettings.myAccount.deleteAccount.confirmHint3\" टाइप करा.", - "confirmHint2": "मला समजले आहे की ही क्रिया अपरिवर्तनीय आहे आणि माझे खाते व सर्व संबंधित डेटा कायमचा हटवला जाईल.", - "confirmHint3": "DELETE MY ACCOUNT", - "checkToConfirmError": "हटवण्यासाठी पुष्टी बॉक्स निवडणे आवश्यक आहे", - "failedToGetCurrentUser": "वर्तमान वापरकर्त्याचा ईमेल मिळवण्यात अयशस्वी", - "confirmTextValidationFailed": "तुमचा पुष्टी मजकूर \"@:newSettings.myAccount.deleteAccount.confirmHint3\" शी जुळत नाही", - "deleteAccountSuccess": "खाते यशस्वीरित्या हटवले गेले" - } - }, - "workplace": { - "name": "वर्कस्पेस", - "title": "वर्कस्पेस सेटिंग्स", - "subtitle": "तुमचा वर्कस्पेस लुक, थीम, फॉन्ट, मजकूर लेआउट, तारीख, वेळ आणि भाषा सानुकूल करा.", - "workplaceName": "वर्कस्पेसचे नाव", - "workplaceNamePlaceholder": "वर्कस्पेसचे नाव टाका", - "workplaceIcon": "वर्कस्पेस चिन्ह", - "workplaceIconSubtitle": "एक प्रतिमा अपलोड करा किंवा इमोजी वापरा. हे साइडबार आणि सूचना मध्ये दर्शवले जाईल.", - "renameError": "वर्कस्पेसचे नाव बदलण्यात अयशस्वी", - "updateIconError": "चिन्ह अपडेट करण्यात अयशस्वी", - "chooseAnIcon": "चिन्ह निवडा", - "appearance": { - "name": "दृश्यरूप", - "themeMode": { - "auto": "स्वयंचलित", - "light": "प्रकाश मोड", - "dark": "गडद मोड" - }, - "language": "भाषा" - } - }, - "syncState": { - "syncing": "सिंक्रोनायझ करत आहे", - "synced": "सिंक्रोनायझ झाले", - "noNetworkConnected": "नेटवर्क कनेक्ट केलेले नाही" - } -}, - "pageStyle": { - "title": "पृष्ठ शैली", - "layout": "लेआउट", - "coverImage": "मुखपृष्ठ प्रतिमा", - "pageIcon": "पृष्ठ चिन्ह", - "colors": "रंग", - "gradient": "ग्रेडियंट", - "backgroundImage": "पार्श्वभूमी प्रतिमा", - "presets": "पूर्वनियोजित", - "photo": "फोटो", - "unsplash": "Unsplash", - "pageCover": "पृष्ठ कव्हर", - "none": "काही नाही", - "openSettings": "सेटिंग्स उघडा", - "photoPermissionTitle": "@:appName तुमच्या फोटो लायब्ररीमध्ये प्रवेश करू इच्छित आहे", - "photoPermissionDescription": "तुमच्या कागदपत्रांमध्ये प्रतिमा जोडण्यासाठी @:appName ला तुमच्या फोटोंमध्ये प्रवेश आवश्यक आहे", - "cameraPermissionTitle": "@:appName तुमच्या कॅमेऱ्याला प्रवेश करू इच्छित आहे", - "cameraPermissionDescription": "कॅमेऱ्यातून प्रतिमा जोडण्यासाठी @:appName ला तुमच्या कॅमेऱ्याचा प्रवेश आवश्यक आहे", - "doNotAllow": "परवानगी देऊ नका", - "image": "प्रतिमा" -}, -"commandPalette": { - "placeholder": "शोधा किंवा प्रश्न विचारा...", - "bestMatches": "सर्वोत्तम जुळवणी", - "recentHistory": "अलीकडील इतिहास", - "navigateHint": "नेव्हिगेट करण्यासाठी", - "loadingTooltip": "आम्ही निकाल शोधत आहोत...", - "betaLabel": "बेटा", - "betaTooltip": "सध्या आम्ही फक्त दस्तऐवज आणि पृष्ठ शोध समर्थन करतो", - "fromTrashHint": "कचरापेटीतून", - "noResultsHint": "आपण जे शोधत आहात ते सापडले नाही, कृपया दुसरा शब्द वापरून शोधा.", - "clearSearchTooltip": "शोध फील्ड साफ करा" -}, -"space": { - "delete": "हटवा", - "deleteConfirmation": "हटवा: ", - "deleteConfirmationDescription": "या स्पेसमधील सर्व पृष्ठे हटवली जातील आणि कचरापेटीत टाकली जातील, आणि प्रकाशित पृष्ठे अनपब्लिश केली जातील.", - "rename": "स्पेसचे नाव बदला", - "changeIcon": "चिन्ह बदला", - "manage": "स्पेस व्यवस्थापित करा", - "addNewSpace": "स्पेस तयार करा", - "collapseAllSubPages": "सर्व उपपृष्ठे संकुचित करा", - "createNewSpace": "नवीन स्पेस तयार करा", - "createSpaceDescription": "तुमचे कार्य अधिक चांगल्या प्रकारे आयोजित करण्यासाठी अनेक सार्वजनिक व खाजगी स्पेस तयार करा.", - "spaceName": "स्पेसचे नाव", - "spaceNamePlaceholder": "उदा. मार्केटिंग, अभियांत्रिकी, HR", - "permission": "स्पेस परवानगी", - "publicPermission": "सार्वजनिक", - "publicPermissionDescription": "पूर्ण प्रवेशासह सर्व वर्कस्पेस सदस्य", - "privatePermission": "खाजगी", - "privatePermissionDescription": "फक्त तुम्हाला या स्पेसमध्ये प्रवेश आहे", - "spaceIconBackground": "पार्श्वभूमीचा रंग", - "spaceIcon": "चिन्ह", - "dangerZone": "धोकादायक क्षेत्र", - "unableToDeleteLastSpace": "शेवटची स्पेस हटवता येणार नाही", - "unableToDeleteSpaceNotCreatedByYou": "तुमच्याद्वारे तयार न केलेली स्पेस हटवता येणार नाही", - "enableSpacesForYourWorkspace": "तुमच्या वर्कस्पेससाठी स्पेस सक्षम करा", - "title": "स्पेसेस", - "defaultSpaceName": "सामान्य", - "upgradeSpaceTitle": "स्पेस सक्षम करा", - "upgradeSpaceDescription": "तुमच्या वर्कस्पेसचे अधिक चांगल्या प्रकारे व्यवस्थापन करण्यासाठी अनेक सार्वजनिक आणि खाजगी स्पेस तयार करा.", - "upgrade": "अपग्रेड", - "upgradeYourSpace": "अनेक स्पेस तयार करा", - "quicklySwitch": "पुढील स्पेसवर पटकन स्विच करा", - "duplicate": "स्पेस डुप्लिकेट करा", - "movePageToSpace": "पृष्ठ स्पेसमध्ये हलवा", - "cannotMovePageToDatabase": "पृष्ठ डेटाबेसमध्ये हलवता येणार नाही", - "switchSpace": "स्पेस स्विच करा", - "spaceNameCannotBeEmpty": "स्पेसचे नाव रिकामे असू शकत नाही", - "success": { - "deleteSpace": "स्पेस यशस्वीरित्या हटवली", - "renameSpace": "स्पेसचे नाव यशस्वीरित्या बदलले", - "duplicateSpace": "स्पेस यशस्वीरित्या डुप्लिकेट केली", - "updateSpace": "स्पेस यशस्वीरित्या अपडेट केली" - }, - "error": { - "deleteSpace": "स्पेस हटवण्यात अयशस्वी", - "renameSpace": "स्पेसचे नाव बदलण्यात अयशस्वी", - "duplicateSpace": "स्पेस डुप्लिकेट करण्यात अयशस्वी", - "updateSpace": "स्पेस अपडेट करण्यात अयशस्वी" - }, - "createSpace": "स्पेस तयार करा", - "manageSpace": "स्पेस व्यवस्थापित करा", - "renameSpace": "स्पेसचे नाव बदला", - "mSpaceIconColor": "स्पेस चिन्हाचा रंग", - "mSpaceIcon": "स्पेस चिन्ह" -}, - "publish": { - "hasNotBeenPublished": "हे पृष्ठ अजून प्रकाशित केलेले नाही", - "spaceHasNotBeenPublished": "स्पेस प्रकाशित करण्यासाठी समर्थन नाही", - "reportPage": "पृष्ठाची तक्रार करा", - "databaseHasNotBeenPublished": "डेटाबेस प्रकाशित करण्यास समर्थन नाही.", - "createdWith": "यांनी तयार केले", - "downloadApp": "AppFlowy डाउनलोड करा", - "copy": { - "codeBlock": "कोड ब्लॉकची सामग्री क्लिपबोर्डवर कॉपी केली गेली आहे", - "imageBlock": "प्रतिमा लिंक क्लिपबोर्डवर कॉपी केली गेली आहे", - "mathBlock": "गणितीय समीकरण क्लिपबोर्डवर कॉपी केले गेले आहे", - "fileBlock": "फाइल लिंक क्लिपबोर्डवर कॉपी केली गेली आहे" - }, - "containsPublishedPage": "या पृष्ठात एक किंवा अधिक प्रकाशित पृष्ठे आहेत. तुम्ही पुढे गेल्यास ती अनपब्लिश होतील. तुम्हाला हटवणे चालू ठेवायचे आहे का?", - "publishSuccessfully": "यशस्वीरित्या प्रकाशित झाले", - "unpublishSuccessfully": "यशस्वीरित्या अनपब्लिश झाले", - "publishFailed": "प्रकाशित करण्यात अयशस्वी", - "unpublishFailed": "अनपब्लिश करण्यात अयशस्वी", - "noAccessToVisit": "या पृष्ठावर प्रवेश नाही...", - "createWithAppFlowy": "AppFlowy ने वेबसाइट तयार करा", - "fastWithAI": "AI सह जलद आणि सोपे.", - "tryItNow": "आत्ताच वापरून पहा", - "onlyGridViewCanBePublished": "फक्त Grid view प्रकाशित केला जाऊ शकतो", - "database": { - "zero": "{} निवडलेले दृश्य प्रकाशित करा", - "one": "{} निवडलेली दृश्ये प्रकाशित करा", - "many": "{} निवडलेली दृश्ये प्रकाशित करा", - "other": "{} निवडलेली दृश्ये प्रकाशित करा" - }, - "mustSelectPrimaryDatabase": "प्राथमिक दृश्य निवडणे आवश्यक आहे", - "noDatabaseSelected": "कोणताही डेटाबेस निवडलेला नाही, कृपया किमान एक डेटाबेस निवडा.", - "unableToDeselectPrimaryDatabase": "प्राथमिक डेटाबेस अननिवड करता येणार नाही", - "saveThisPage": "या टेम्पलेटपासून सुरू करा", - "duplicateTitle": "तुम्हाला हे कुठे जोडायचे आहे", - "selectWorkspace": "वर्कस्पेस निवडा", - "addTo": "मध्ये जोडा", - "duplicateSuccessfully": "तुमच्या वर्कस्पेसमध्ये जोडले गेले", - "duplicateSuccessfullyDescription": "AppFlowy स्थापित केले नाही? तुम्ही 'डाउनलोड' वर क्लिक केल्यावर डाउनलोड आपोआप सुरू होईल.", - "downloadIt": "डाउनलोड करा", - "openApp": "अ‍ॅपमध्ये उघडा", - "duplicateFailed": "डुप्लिकेट करण्यात अयशस्वी", - "membersCount": { - "zero": "सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "useThisTemplate": "हा टेम्पलेट वापरा" -}, -"web": { - "continue": "पुढे जा", - "or": "किंवा", - "continueWithGoogle": "Google सह पुढे जा", - "continueWithGithub": "GitHub सह पुढे जा", - "continueWithDiscord": "Discord सह पुढे जा", - "continueWithApple": "Apple सह पुढे जा", - "moreOptions": "अधिक पर्याय", - "collapse": "आकुंचन", - "signInAgreement": "\"पुढे जा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", - "signInLocalAgreement": "\"सुरुवात करा\" क्लिक करून, तुम्ही AppFlowy च्या अटींना सहमती दिली आहे", - "and": "आणि", - "termOfUse": "वापर अटी", - "privacyPolicy": "गोपनीयता धोरण", - "signInError": "साइन इन त्रुटी", - "login": "साइन अप किंवा लॉग इन करा", - "fileBlock": { - "uploadedAt": "{time} रोजी अपलोड केले", - "linkedAt": "{time} रोजी लिंक जोडली", - "empty": "फाईल अपलोड करा किंवा एम्बेड करा", - "uploadFailed": "अपलोड अयशस्वी, कृपया पुन्हा प्रयत्न करा", - "retry": "पुन्हा प्रयत्न करा" - }, - "importNotion": "Notion वरून आयात करा", - "import": "आयात करा", - "importSuccess": "यशस्वीरित्या अपलोड केले", - "importSuccessMessage": "आम्ही तुम्हाला आयात पूर्ण झाल्यावर सूचित करू. त्यानंतर, तुम्ही साइडबारमध्ये तुमची आयात केलेली पृष्ठे पाहू शकता.", - "importFailed": "आयात अयशस्वी, कृपया फाईल फॉरमॅट तपासा", - "dropNotionFile": "तुमची Notion zip फाईल येथे ड्रॉप करा किंवा ब्राउझ करा", - "error": { - "pageNameIsEmpty": "पृष्ठाचे नाव रिकामे आहे, कृपया दुसरे नाव वापरून पहा" - } -}, - "globalComment": { - "comments": "टिप्पण्या", - "addComment": "टिप्पणी जोडा", - "reactedBy": "यांनी प्रतिक्रिया दिली", - "addReaction": "प्रतिक्रिया जोडा", - "reactedByMore": "आणि {count} इतर", - "showSeconds": { - "one": "1 सेकंदापूर्वी", - "other": "{count} सेकंदांपूर्वी", - "zero": "आत्ताच", - "many": "{count} सेकंदांपूर्वी" - }, - "showMinutes": { - "one": "1 मिनिटापूर्वी", - "other": "{count} मिनिटांपूर्वी", - "many": "{count} मिनिटांपूर्वी" - }, - "showHours": { - "one": "1 तासापूर्वी", - "other": "{count} तासांपूर्वी", - "many": "{count} तासांपूर्वी" - }, - "showDays": { - "one": "1 दिवसापूर्वी", - "other": "{count} दिवसांपूर्वी", - "many": "{count} दिवसांपूर्वी" - }, - "showMonths": { - "one": "1 महिन्यापूर्वी", - "other": "{count} महिन्यांपूर्वी", - "many": "{count} महिन्यांपूर्वी" - }, - "showYears": { - "one": "1 वर्षापूर्वी", - "other": "{count} वर्षांपूर्वी", - "many": "{count} वर्षांपूर्वी" - }, - "reply": "उत्तर द्या", - "deleteComment": "टिप्पणी हटवा", - "youAreNotOwner": "तुम्ही या टिप्पणीचे मालक नाही", - "confirmDeleteDescription": "तुम्हाला ही टिप्पणी हटवायची आहे याची खात्री आहे का?", - "hasBeenDeleted": "हटवले गेले", - "replyingTo": "याला उत्तर देत आहे", - "noAccessDeleteComment": "तुम्हाला ही टिप्पणी हटवण्याची परवानगी नाही", - "collapse": "संकुचित करा", - "readMore": "अधिक वाचा", - "failedToAddComment": "टिप्पणी जोडण्यात अयशस्वी", - "commentAddedSuccessfully": "टिप्पणी यशस्वीरित्या जोडली गेली.", - "commentAddedSuccessTip": "तुम्ही नुकतीच एक टिप्पणी जोडली किंवा उत्तर दिले आहे. वर जाऊन ताजी टिप्पण्या पाहायच्या का?" -}, - "template": { - "asTemplate": "टेम्पलेट म्हणून जतन करा", - "name": "टेम्पलेट नाव", - "description": "टेम्पलेट वर्णन", - "about": "टेम्पलेट माहिती", - "deleteFromTemplate": "टेम्पलेटमधून हटवा", - "preview": "टेम्पलेट पूर्वदृश्य", - "categories": "टेम्पलेट श्रेणी", - "isNewTemplate": "नवीन टेम्पलेटमध्ये पिन करा", - "featured": "वैशिष्ट्यीकृतमध्ये पिन करा", - "relatedTemplates": "संबंधित टेम्पलेट्स", - "requiredField": "{field} आवश्यक आहे", - "addCategory": "\"{category}\" जोडा", - "addNewCategory": "नवीन श्रेणी जोडा", - "addNewCreator": "नवीन निर्माता जोडा", - "deleteCategory": "श्रेणी हटवा", - "editCategory": "श्रेणी संपादित करा", - "editCreator": "निर्माता संपादित करा", - "category": { - "name": "श्रेणीचे नाव", - "icon": "श्रेणी चिन्ह", - "bgColor": "श्रेणी पार्श्वभूमीचा रंग", - "priority": "श्रेणी प्राधान्य", - "desc": "श्रेणीचे वर्णन", - "type": "श्रेणी प्रकार", - "icons": "श्रेणी चिन्हे", - "colors": "श्रेणी रंग", - "byUseCase": "वापराच्या आधारे", - "byFeature": "वैशिष्ट्यांनुसार", - "deleteCategory": "श्रेणी हटवा", - "deleteCategoryDescription": "तुम्हाला ही श्रेणी हटवायची आहे का?", - "typeToSearch": "श्रेणी शोधण्यासाठी टाइप करा..." - }, - "creator": { - "label": "टेम्पलेट निर्माता", - "name": "निर्मात्याचे नाव", - "avatar": "निर्मात्याचा अवतार", - "accountLinks": "निर्मात्याचे खाते दुवे", - "uploadAvatar": "अवतार अपलोड करण्यासाठी क्लिक करा", - "deleteCreator": "निर्माता हटवा", - "deleteCreatorDescription": "तुम्हाला हा निर्माता हटवायचा आहे का?", - "typeToSearch": "निर्माते शोधण्यासाठी टाइप करा..." - }, - "uploadSuccess": "टेम्पलेट यशस्वीरित्या अपलोड झाले", - "uploadSuccessDescription": "तुमचे टेम्पलेट यशस्वीरित्या अपलोड झाले आहे. आता तुम्ही ते टेम्पलेट गॅलरीमध्ये पाहू शकता.", - "viewTemplate": "टेम्पलेट पहा", - "deleteTemplate": "टेम्पलेट हटवा", - "deleteSuccess": "टेम्पलेट यशस्वीरित्या हटवले गेले", - "deleteTemplateDescription": "याचा वर्तमान पृष्ठ किंवा प्रकाशित स्थितीवर परिणाम होणार नाही. तुम्हाला हे टेम्पलेट हटवायचे आहे का?", - "addRelatedTemplate": "संबंधित टेम्पलेट जोडा", - "removeRelatedTemplate": "संबंधित टेम्पलेट हटवा", - "uploadAvatar": "अवतार अपलोड करा", - "searchInCategory": "{category} मध्ये शोधा", - "label": "टेम्पलेट्स" -}, - "fileDropzone": { - "dropFile": "फाइल अपलोड करण्यासाठी येथे क्लिक करा किंवा ड्रॅग करा", - "uploading": "अपलोड करत आहे...", - "uploadFailed": "अपलोड अयशस्वी", - "uploadSuccess": "अपलोड यशस्वी", - "uploadSuccessDescription": "फाइल यशस्वीरित्या अपलोड झाली आहे", - "uploadFailedDescription": "फाइल अपलोड अयशस्वी झाली आहे", - "uploadingDescription": "फाइल अपलोड होत आहे" -}, - "gallery": { - "preview": "पूर्ण स्क्रीनमध्ये उघडा", - "copy": "कॉपी करा", - "download": "डाउनलोड", - "prev": "मागील", - "next": "पुढील", - "resetZoom": "झूम रिसेट करा", - "zoomIn": "झूम इन", - "zoomOut": "झूम आउट" -}, - "invitation": { - "join": "सामील व्हा", - "on": "वर", - "invitedBy": "यांनी आमंत्रित केले", - "membersCount": { - "zero": "{count} सदस्य", - "one": "{count} सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "tip": "तुम्हाला खालील माहितीच्या आधारे या कार्यक्षेत्रात सामील होण्यासाठी आमंत्रित करण्यात आले आहे. ही माहिती चुकीची असल्यास, कृपया प्रशासकाशी संपर्क साधा.", - "joinWorkspace": "वर्कस्पेसमध्ये सामील व्हा", - "success": "तुम्ही यशस्वीरित्या वर्कस्पेसमध्ये सामील झाला आहात", - "successMessage": "आता तुम्ही सर्व पृष्ठे आणि कार्यक्षेत्रे वापरू शकता.", - "openWorkspace": "AppFlowy उघडा", - "alreadyAccepted": "तुम्ही आधीच आमंत्रण स्वीकारले आहे", - "errorModal": { - "title": "काहीतरी चुकले आहे", - "description": "तुमचे सध्याचे खाते {email} कदाचित या वर्कस्पेसमध्ये प्रवेशासाठी पात्र नाही. कृपया योग्य खात्याने लॉग इन करा किंवा वर्कस्पेस मालकाशी संपर्क साधा.", - "contactOwner": "मालकाशी संपर्क करा", - "close": "मुख्यपृष्ठावर परत जा", - "changeAccount": "खाते बदला" - } -}, - "requestAccess": { - "title": "या पृष्ठासाठी प्रवेश नाही", - "subtitle": "तुम्ही या पृष्ठाच्या मालकाकडून प्रवेशासाठी विनंती करू शकता. मंजुरीनंतर तुम्हाला हे पृष्ठ पाहता येईल.", - "requestAccess": "प्रवेशाची विनंती करा", - "backToHome": "मुख्यपृष्ठावर परत जा", - "tip": "तुम्ही सध्या <link/> म्हणून लॉग इन आहात.", - "mightBe": "कदाचित तुम्हाला <login/> दुसऱ्या खात्याने लॉग इन करणे आवश्यक आहे.", - "successful": "विनंती यशस्वीपणे पाठवली गेली", - "successfulMessage": "मालकाने मंजुरी दिल्यावर तुम्हाला सूचित केले जाईल.", - "requestError": "प्रवेशाची विनंती अयशस्वी", - "repeatRequestError": "तुम्ही यासाठी आधीच विनंती केली आहे" -}, - "approveAccess": { - "title": "वर्कस्पेसमध्ये सामील होण्यासाठी विनंती मंजूर करा", - "requestSummary": "<user/> यांनी <workspace/> मध्ये सामील होण्यासाठी आणि <page/> पाहण्यासाठी विनंती केली आहे", - "upgrade": "अपग्रेड", - "downloadApp": "AppFlowy डाउनलोड करा", - "approveButton": "मंजूर करा", - "approveSuccess": "मंजूर यशस्वी", - "approveError": "मंजुरी अयशस्वी. कृपया वर्कस्पेस मर्यादा ओलांडलेली नाही याची खात्री करा", - "getRequestInfoError": "विनंतीची माहिती मिळवण्यात अयशस्वी", - "memberCount": { - "zero": "कोणतेही सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" - }, - "alreadyProTitle": "वर्कस्पेस योजना मर्यादा गाठली आहे", - "alreadyProMessage": "अधिक सदस्यांसाठी <email/> शी संपर्क साधा", - "repeatApproveError": "तुम्ही ही विनंती आधीच मंजूर केली आहे", - "ensurePlanLimit": "कृपया खात्री करा की योजना मर्यादा ओलांडलेली नाही. जर ओलांडली असेल तर वर्कस्पेस योजना <upgrade/> करा किंवा <download/> करा.", - "requestToJoin": "मध्ये सामील होण्यासाठी विनंती केली", - "asMember": "सदस्य म्हणून" -}, - "upgradePlanModal": { - "title": "Pro प्लॅनवर अपग्रेड करा", - "message": "{name} ने फ्री सदस्य मर्यादा गाठली आहे. अधिक सदस्य आमंत्रित करण्यासाठी Pro प्लॅनवर अपग्रेड करा.", - "upgradeSteps": "AppFlowy वर तुमची योजना कशी अपग्रेड करावी:", - "step1": "1. सेटिंग्जमध्ये जा", - "step2": "2. 'योजना' वर क्लिक करा", - "step3": "3. 'योजना बदला' निवडा", - "appNote": "नोंद:", - "actionButton": "अपग्रेड करा", - "downloadLink": "अ‍ॅप डाउनलोड करा", - "laterButton": "नंतर", - "refreshNote": "यशस्वी अपग्रेडनंतर, तुमची नवीन वैशिष्ट्ये सक्रिय करण्यासाठी <refresh/> वर क्लिक करा.", - "refresh": "येथे" -}, - "breadcrumbs": { - "label": "ब्रेडक्रम्स" -}, - "time": { - "justNow": "आत्ताच", - "seconds": { - "one": "1 सेकंद", - "other": "{count} सेकंद" - }, - "minutes": { - "one": "1 मिनिट", - "other": "{count} मिनिटे" - }, - "hours": { - "one": "1 तास", - "other": "{count} तास" - }, - "days": { - "one": "1 दिवस", - "other": "{count} दिवस" - }, - "weeks": { - "one": "1 आठवडा", - "other": "{count} आठवडे" - }, - "months": { - "one": "1 महिना", - "other": "{count} महिने" - }, - "years": { - "one": "1 वर्ष", - "other": "{count} वर्षे" - }, - "ago": "पूर्वी", - "yesterday": "काल", - "today": "आज" -}, - "members": { - "zero": "सदस्य नाहीत", - "one": "1 सदस्य", - "many": "{count} सदस्य", - "other": "{count} सदस्य" -}, - "tabMenu": { - "close": "बंद करा", - "closeDisabledHint": "पिन केलेले टॅब बंद करता येत नाही, कृपया आधी अनपिन करा", - "closeOthers": "इतर टॅब बंद करा", - "closeOthersHint": "हे सर्व अनपिन केलेले टॅब्स बंद करेल या टॅब वगळता", - "closeOthersDisabledHint": "सर्व टॅब पिन केलेले आहेत, बंद करण्यासाठी कोणतेही टॅब सापडले नाहीत", - "favorite": "आवडते", - "unfavorite": "आवडते काढा", - "favoriteDisabledHint": "हे दृश्य आवडते म्हणून जतन करता येत नाही", - "pinTab": "पिन करा", - "unpinTab": "अनपिन करा" -}, - "openFileMessage": { - "success": "फाइल यशस्वीरित्या उघडली", - "fileNotFound": "फाइल सापडली नाही", - "noAppToOpenFile": "ही फाइल उघडण्यासाठी कोणतेही अ‍ॅप उपलब्ध नाही", - "permissionDenied": "ही फाइल उघडण्यासाठी परवानगी नाही", - "unknownError": "फाइल उघडण्यात अयशस्वी" -}, - "inviteMember": { - "requestInviteMembers": "तुमच्या वर्कस्पेसमध्ये आमंत्रित करा", - "inviteFailedMemberLimit": "सदस्य मर्यादा गाठली आहे, कृपया ", - "upgrade": "अपग्रेड करा", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "आमंत्रण पाठवा", - "inviteAlready": "तुम्ही या ईमेलला आधीच आमंत्रित केले आहे: {email}", - "inviteSuccess": "आमंत्रण यशस्वीपणे पाठवले", - "description": "खाली ईमेल कॉमा वापरून टाका. शुल्क सदस्य संख्येवर आधारित असते.", - "emails": "ईमेल" -}, - "quickNote": { - "label": "झटपट नोंद", - "quickNotes": "झटपट नोंदी", - "search": "झटपट नोंदी शोधा", - "collapseFullView": "पूर्ण दृश्य लपवा", - "expandFullView": "पूर्ण दृश्य उघडा", - "createFailed": "झटपट नोंद तयार करण्यात अयशस्वी", - "quickNotesEmpty": "कोणत्याही झटपट नोंदी नाहीत", - "emptyNote": "रिकामी नोंद", - "deleteNotePrompt": "निवडलेली नोंद कायमची हटवली जाईल. तुम्हाला नक्की ती हटवायची आहे का?", - "addNote": "नवीन नोंद", - "noAdditionalText": "अधिक माहिती नाही" -}, - "subscribe": { - "upgradePlanTitle": "योजना तुलना करा आणि निवडा", - "yearly": "वार्षिक", - "save": "{discount}% बचत", - "monthly": "मासिक", - "priceIn": "किंमत येथे: ", - "free": "फ्री", - "pro": "प्रो", - "freeDescription": "2 सदस्यांपर्यंत वैयक्तिक वापरकर्त्यांसाठी सर्व काही आयोजित करण्यासाठी", - "proDescription": "प्रकल्प आणि टीम नॉलेज व्यवस्थापित करण्यासाठी लहान टीमसाठी", - "proDuration": { - "monthly": "प्रति सदस्य प्रति महिना\nमासिक बिलिंग", - "yearly": "प्रति सदस्य प्रति महिना\nवार्षिक बिलिंग" - }, - "cancel": "खालच्या योजनेवर जा", - "changePlan": "प्रो योजनेवर अपग्रेड करा", - "everythingInFree": "फ्री योजनेतील सर्व काही +", - "currentPlan": "सध्याची योजना", - "freeDuration": "कायम", - "freePoints": { - "first": "1 सहकार्यात्मक वर्कस्पेस (2 सदस्यांपर्यंत)", - "second": "अमर्यादित पृष्ठे आणि ब्लॉक्स", - "three": "5 GB संचयन", - "four": "बुद्धिमान शोध", - "five": "20 AI प्रतिसाद", - "six": "मोबाईल अ‍ॅप", - "seven": "रिअल-टाइम सहकार्य" - }, - "proPoints": { - "first": "अमर्यादित संचयन", - "second": "10 वर्कस्पेस सदस्यांपर्यंत", - "three": "अमर्यादित AI प्रतिसाद", - "four": "अमर्यादित फाइल अपलोड्स", - "five": "कस्टम नेमस्पेस" - }, - "cancelPlan": { - "title": "आपल्याला जाताना पाहून वाईट वाटते", - "success": "आपली सदस्यता यशस्वीरित्या रद्द झाली आहे", - "description": "आपल्याला जाताना वाईट वाटते. कृपया आम्हाला सुधारण्यासाठी तुमचे अभिप्राय कळवा. खालील काही प्रश्नांना उत्तर द्या.", - "commonOther": "इतर", - "otherHint": "आपले उत्तर येथे लिहा", - "questionOne": { - "question": "तुम्ही AppFlowy Pro सदस्यता का रद्द केली?", - "answerOne": "खर्च खूप जास्त आहे", - "answerTwo": "वैशिष्ट्ये अपेक्षेनुसार नव्हती", - "answerThree": "चांगला पर्याय सापडला", - "answerFour": "खर्च योग्य ठरण्यासाठी वापर पुरेसा नव्हता", - "answerFive": "सेवा समस्या किंवा तांत्रिक अडचणी" - }, - "questionTwo": { - "question": "तुम्ही भविष्यात AppFlowy Pro पुन्हा घेण्याची शक्यता किती आहे?", - "answerOne": "खूप शक्यता आहे", - "answerTwo": "काहीशी शक्यता आहे", - "answerThree": "निश्चित नाही", - "answerFour": "अल्प शक्यता आहे", - "answerFive": "शक्यता नाही" - }, - "questionThree": { - "question": "सदस्यतेदरम्यान कोणते Pro फिचर तुम्हाला सर्वात जास्त उपयोगी वाटले?", - "answerOne": "मल्टी-यूजर सहकार्य", - "answerTwo": "लांब कालावधीसाठी आवृत्ती इतिहास", - "answerThree": "अमर्यादित AI प्रतिसाद", - "answerFour": "स्थानिक AI मॉडेल्सचा प्रवेश" - }, - "questionFour": { - "question": "AppFlowy बाबत तुमचा एकूण अनुभव कसा होता?", - "answerOne": "छान", - "answerTwo": "चांगला", - "answerThree": "सामान्य", - "answerFour": "थोडासा वाईट", - "answerFive": "असंतोषजनक" - } - } -}, - "ai": { - "contentPolicyViolation": "संवेदनशील सामग्रीमुळे प्रतिमा निर्मिती अयशस्वी झाली. कृपया तुमचे इनपुट पुन्हा लिहा आणि पुन्हा प्रयत्न करा.", - "textLimitReachedDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अनलिमिटेड प्रतिसादांसाठी प्रो योजना घेण्याचा किंवा AI अ‍ॅड-ऑन खरेदी करण्याचा विचार करा.", - "imageLimitReachedDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया प्रो योजना घ्या किंवा AI अ‍ॅड-ऑन खरेदी करा.", - "limitReachedAction": { - "textDescription": "तुमच्या वर्कस्पेसमध्ये मोफत AI प्रतिसाद संपले आहेत. अधिक प्रतिसाद मिळवण्यासाठी कृपया", - "imageDescription": "तुमचे मोफत AI प्रतिमा कोटा संपले आहे. कृपया", - "upgrade": "अपग्रेड करा", - "toThe": "या योजनेवर", - "proPlan": "प्रो योजना", - "orPurchaseAn": "किंवा खरेदी करा", - "aiAddon": "AI अ‍ॅड-ऑन" - }, - "editing": "संपादन करत आहे", - "analyzing": "विश्लेषण करत आहे", - "continueWritingEmptyDocumentTitle": "लेखन सुरू ठेवता आले नाही", - "continueWritingEmptyDocumentDescription": "तुमच्या दस्तऐवजातील मजकूर वाढवण्यात अडचण येत आहे. कृपया एक छोटं परिचय लिहा, मग आम्ही पुढे नेऊ!", - "more": "अधिक" -}, - "autoUpdate": { - "criticalUpdateTitle": "अद्यतन आवश्यक आहे", - "criticalUpdateDescription": "तुमचा अनुभव सुधारण्यासाठी आम्ही सुधारणा केल्या आहेत! कृपया {currentVersion} वरून {newVersion} वर अद्यतन करा.", - "criticalUpdateButton": "अद्यतन करा", - "bannerUpdateTitle": "नवीन आवृत्ती उपलब्ध!", - "bannerUpdateDescription": "नवीन वैशिष्ट्ये आणि सुधारणांसाठी. अद्यतनासाठी 'Update' क्लिक करा.", - "bannerUpdateButton": "अद्यतन करा", - "settingsUpdateTitle": "नवीन आवृत्ती ({newVersion}) उपलब्ध!", - "settingsUpdateDescription": "सध्याची आवृत्ती: {currentVersion} (अधिकृत बिल्ड) → {newVersion}", - "settingsUpdateButton": "अद्यतन करा", - "settingsUpdateWhatsNew": "काय नवीन आहे" -}, - "lockPage": { - "lockPage": "लॉक केलेले", - "reLockPage": "पुन्हा लॉक करा", - "lockTooltip": "अनवधानाने संपादन टाळण्यासाठी पृष्ठ लॉक केले आहे. अनलॉक करण्यासाठी क्लिक करा.", - "pageLockedToast": "पृष्ठ लॉक केले आहे. कोणी अनलॉक करेपर्यंत संपादन अक्षम आहे.", - "lockedOperationTooltip": "पृष्ठ अनवधानाने संपादनापासून वाचवण्यासाठी लॉक केले आहे." -}, - "suggestion": { - "accept": "स्वीकारा", - "keep": "जसे आहे तसे ठेवा", - "discard": "रद्द करा", - "close": "बंद करा", - "tryAgain": "पुन्हा प्रयत्न करा", - "rewrite": "पुन्हा लिहा", - "insertBelow": "खाली टाका" -} -} diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 9473d7e2f0..389eaa068d 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "Ja", "welcomeText": "Witaj w @:appName", - "welcomeTo": "Witamy w", "githubStarText": "Gwiazdka na GitHub-ie", "subscribeNewsletterText": "Zapisz się do naszego Newslettera", "letsGoButtonText": "Start!", "title": "Tytuł", "youCanAlso": "Możesz również", "and": "i", - "failedToOpenUrl": "Nie udało się otworzyć adresu URL: {}", "blockActions": { "addBelowTooltip": "Kliknij, aby dodać poniżej", "addAboveCmd": "Alt+kliknięcie", @@ -37,27 +35,15 @@ "loginStartWithAnonymous": "Rozpocznij sesję anonimową", "continueAnonymousUser": "Kontynuuj sesję anonimową", "buttonText": "Zaloguj", - "signingInText": "Logowanie się...", "forgotPassword": "Zapomniałeś hasła?", "emailHint": "Email", "passwordHint": "Hasło", "dontHaveAnAccount": "Nie masz konta?", - "createAccount": "Utwórz konto", "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", "unmatchedPasswordError": "Hasła nie są takie same", "syncPromptMessage": "Synchronizacja danych może chwilę potrwać. Proszę nie zamykać tej strony", "or": "ALBO", - "signInWithGoogle": "Zaloguj się za pomocą Google", - "signInWithGithub": "Zaloguj się za pomocą Githuba", - "signInWithDiscord": "Zaloguj się za pomocą Discorda", - "signUpWithGoogle": "Zarejestruj się za pomocą Google", - "signUpWithGithub": "Zarejestruj się za pomocą Githuba", - "signUpWithDiscord": "Zarejestruj się za pomocą Discorda", "signInWith": "Zaloguj się korzystając z:", - "pleaseInputYourEmail": "Podaj swój adres e-mail", - "settings": "Ustawienia", - "logIn": "Zaloguj się", - "generalError": "Coś poszło nie tak. Spróbuj ponownie później", "LogInWithGoogle": "Zaloguj się za pomocą Google", "LogInWithGithub": "Zaloguj się za pomocą Githuba", "LogInWithDiscord": "Zaloguj się za pomocą Discorda", @@ -67,29 +53,19 @@ "chooseWorkspace": "Wybierz swoją przestrzeń do pracy", "create": "Utwórz przestrzeń", "reset": "Zresetuj przestrzeń roboczą", - "renameWorkspace": "Zmień nazwę obszaru roboczego", "resetWorkspacePrompt": "Zresetowanie przestrzeni roboczej spowoduje usunięcie wszystkich znajdujących się w niej stron i danych. Czy na pewno chcesz zresetować przestrzeń roboczą? Alternatywnie możesz skontaktować się z zespołem pomocy technicznej, aby przywrócić przestrzeń roboczą", "hint": "przestrzeń robocza", "notFoundError": "Przestrzeni nie znaleziono", "failedToLoad": "Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje @:appName i spróbuj ponownie.", "errorActions": { "reportIssue": "Zgłoś problem", - "reportIssueOnGithub": "Zgłoś problem na Githubie", "reachOut": "Skontaktuj się na Discord" - }, - "menuTitle": "Obszary robocze", - "deleteWorkspaceHintText": "Czy na pewno chcesz usunąć obszar roboczy? Tej akcji nie można cofnąć.", - "createSuccess": "Obszar roboczy został utworzony pomyślnie", - "createFailed": "Nie udało się utworzyć obszaru roboczego", - "deleteSuccess": "Obszar roboczy został pomyślnie usunięty", - "deleteFailed": "Nie udało się usunąć obszaru roboczego" + } }, "shareAction": { "buttonText": "Udostępnij", "workInProgress": "Dostępne wkrótce", "markdown": "Markdown", - "html": "HTML", - "clipboard": "Skopiuj do schowka", "csv": "CSV", "copyLink": "Skopiuj link" }, @@ -99,11 +75,7 @@ "large": "duży", "fontSize": "Rozmiar czcionki", "import": "Import", - "moreOptions": "Więcej opcji", - "wordCount": "Liczba słów: {}", - "charCount": "Liczba znaków: {}", - "createdAt": "Utworzony: {}", - "deleteView": "Usuń" + "moreOptions": "Więcej opcji" }, "importPanel": { "textAndMarkdown": "Tekst i Markdown", @@ -121,8 +93,7 @@ "openNewTab": "Otwórz w nowej karcie", "moveTo": "Przenieś do", "addToFavorites": "Dodaj do ulubionych", - "copyLink": "Skopiuj link", - "changeIcon": "Zmień ikonę" + "copyLink": "Skopiuj link" }, "blankPageTitle": "Pusta strona", "newPageText": "Nowa strona", @@ -164,14 +135,14 @@ "questionBubble": { "shortcuts": "Skróty", "whatsNew": "Co nowego?", + "help": "Pomoc & Wsparcie", "markdown": "Markdown", "debug": { "name": "Informacje Debugowania", "success": "Skopiowano informacje debugowania do schowka!", "fail": "Nie mozna skopiować informacji debugowania do schowka" }, - "feedback": "Feedback", - "help": "Pomoc & Wsparcie" + "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Usuń, zmień nazwę i więcej...", @@ -217,10 +188,7 @@ "clickToHidePersonal": "Kliknij, aby ukryć sekcję osobistą", "clickToHideFavorites": "Kliknij, aby ukryć ulubioną sekcję", "addAPage": "Dodaj stronę", - "recent": "Najnowsze", - "today": "Dziś", - "thisWeek": "Ten tydzień", - "favoriteSpace": "Ulubione" + "recent": "Najnowsze" }, "notifications": { "export": { @@ -236,7 +204,6 @@ }, "button": { "ok": "OK", - "confirm": "Potwierdź", "done": "Zrobione", "cancel": "Anuluj", "signIn": "Zaloguj", @@ -261,21 +228,7 @@ "removeFromFavorites": "Usuń z ulubionych", "addToFavorites": "Dodaj do ulubionych", "rename": "Zmień nazwę", - "helpCenter": "Centrum Pomocy", - "add": "Dodaj", - "yes": "Tak", - "clear": "Wyczyść", - "remove": "Usuń", - "login": "Zaloguj się", - "logout": "Wyloguj", - "deleteAccount": "Usuń konto", - "back": "Wstecz", - "signInGoogle": "Zaloguj się za pomocą Google", - "signInGithub": "Zaloguj się za pomocą Githuba", - "signInDiscord": "Zaloguj się za pomocą Discorda", - "more": "Więcej", - "create": "Utwórz", - "close": "Zamknij" + "helpCenter": "Centrum Pomocy" }, "label": { "welcome": "Witaj!", @@ -299,28 +252,6 @@ }, "settings": { "title": "Ustawienia", - "accountPage": { - "menuLabel": "Moje konto", - "title": "Moje konto", - "general": { - "title": "Nazwa konta i zdjęcie profilowe", - "changeProfilePicture": "Zmień zdjęcie profilowe" - }, - "email": { - "title": "E-mail", - "actions": { - "change": "Zmień adres e-mail" - } - }, - "login": { - "loginLabel": "Zaloguj się", - "logoutLabel": "Wyloguj" - } - }, - "workspacePage": { - "menuLabel": "Obszar roboczy", - "title": "Obszar roboczy" - }, "menu": { "appearance": "Wygląd", "language": "Język", @@ -342,6 +273,7 @@ "historicalUserList": "Historia logowania użytkownika", "historicalUserListTooltip": "Na tej liście wyświetlane są Twoje anonimowe konta. Możesz kliknąć konto, aby wyświetlić jego szczegóły. Konta anonimowe tworzy się poprzez kliknięcie przycisku „Rozpocznij”.", "openHistoricalUser": "Kliknij, aby otworzyć anonimowe konto", + "supabaseSetting": "Ustawienie Supabase", "cloudSetting": "Ustawienia Chmury" }, "notifications": { @@ -445,9 +377,20 @@ "email": "E-mail", "tooltipSelectIcon": "Wybierz ikonę", "selectAnIcon": "Wybierz ikonę", - "pleaseInputYourOpenAIKey": "wprowadź swój klucz AI", - "clickToLogout": "Kliknij, aby wylogować bieżącego użytkownika", - "pleaseInputYourStabilityAIKey": "wprowadź swój klucz Stability AI" + "pleaseInputYourOpenAIKey": "wprowadź swój klucz OpenAI", + "pleaseInputYourStabilityAIKey": "wprowadź swój klucz Stability AI", + "clickToLogout": "Kliknij, aby wylogować bieżącego użytkownika" + }, + "shortcuts": { + "shortcutsLabel": "Skróty", + "command": "Polecenie", + "keyBinding": "Skróty klawiszowe", + "addNewCommand": "Dodaj nowe polecenie", + "updateShortcutStep": "Naciśnij żądaną kombinację klawiszy i naciśnij ENTER", + "shortcutIsAlreadyUsed": "Ten skrót jest już używany w przypadku: {conflict}", + "resetToDefault": "Przywróć domyślne skróty klawiszowe", + "couldNotLoadErrorMsg": "Nie udało się wczytać skrótów. Spróbuj ponownie", + "couldNotSaveErrorMsg": "Nie udało się zapisać skrótów. Spróbuj ponownie" }, "mobile": { "personalInfo": "Informacje Osobiste", @@ -461,17 +404,6 @@ "userAgreement": "Regulamin Użytkowania", "userprofileError": "Nie udało się wczytać profilu użytkownika", "userprofileErrorDescription": "Spróbuj wylogować się i zalogować ponownie, aby zobaczyć czy problem nadal występuje." - }, - "shortcuts": { - "shortcutsLabel": "Skróty", - "command": "Polecenie", - "keyBinding": "Skróty klawiszowe", - "addNewCommand": "Dodaj nowe polecenie", - "updateShortcutStep": "Naciśnij żądaną kombinację klawiszy i naciśnij ENTER", - "shortcutIsAlreadyUsed": "Ten skrót jest już używany w przypadku: {conflict}", - "resetToDefault": "Przywróć domyślne skróty klawiszowe", - "couldNotLoadErrorMsg": "Nie udało się wczytać skrótów. Spróbuj ponownie", - "couldNotSaveErrorMsg": "Nie udało się zapisać skrótów. Spróbuj ponownie" } }, "grid": { @@ -666,23 +598,23 @@ "referencedGrid": "Siatka referencyjna", "referencedCalendar": "Kalendarz referencyjny", "referencedDocument": "Dokument referencyjny", - "autoGeneratorMenuItemName": "Pisarz AI", - "autoGeneratorTitleName": "AI: Poproś AI o napisanie czegokolwiek...", + "autoGeneratorMenuItemName": "Pisarz OpenAI", + "autoGeneratorTitleName": "OpenAI: Poproś AI o napisanie czegokolwiek...", "autoGeneratorLearnMore": "Dowiedz się więcej", "autoGeneratorGenerate": "Generuj", - "autoGeneratorHintText": "Zapytaj AI...", - "autoGeneratorCantGetOpenAIKey": "Nie można uzyskać klucza AI", + "autoGeneratorHintText": "Zapytaj OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Nie można uzyskać klucza OpenAI", "autoGeneratorRewrite": "Przepisz", "smartEdit": "Asystenci AI", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Popraw pisownię", "warning": "⚠️ Odpowiedzi AI mogą być niedokładne lub mylące.", "smartEditSummarize": "Podsumuj", "smartEditImproveWriting": "Popraw pisanie", "smartEditMakeLonger": "Dłużej", - "smartEditCouldNotFetchResult": "Nie można pobrać wyniku z AI", - "smartEditCouldNotFetchKey": "Nie można pobrać klucza AI", - "smartEditDisabled": "Połącz AI w Ustawieniach", + "smartEditCouldNotFetchResult": "Nie można pobrać wyniku z OpenAI", + "smartEditCouldNotFetchKey": "Nie można pobrać klucza OpenAI", + "smartEditDisabled": "Połącz OpenAI w Ustawieniach", "discardResponse": "Czy chcesz odrzucić odpowiedzi AI?", "createInlineMathEquation": "Utwórz równanie", "toggleList": "Przełącz listę", @@ -737,8 +669,8 @@ "defaultColor": "Domyślny" }, "image": { - "addAnImage": "Dodaj obraz", - "copiedToPasteBoard": "Link do obrazu został skopiowany do schowka" + "copiedToPasteBoard": "Link do obrazu został skopiowany do schowka", + "addAnImage": "Dodaj obraz" }, "outline": { "addHeadingToCreateOutline": "Dodaj nagłówki, aby utworzyć spis treści." @@ -775,8 +707,8 @@ "placeholder": "Wprowadź adres URL obrazu" }, "ai": { - "label": "Wygeneruj obraz z AI", - "placeholder": "Wpisz treść podpowiedzi dla AI, aby wygenerować obraz" + "label": "Wygeneruj obraz z OpenAI", + "placeholder": "Wpisz treść podpowiedzi dla OpenAI, aby wygenerować obraz" }, "stability_ai": { "label": "Wygeneruj obraz z Stability AI", @@ -794,12 +726,12 @@ "placeholder": "Wklej lub wpisz link obrazu" }, "searchForAnImage": "Szukaj obrazu", - "pleaseInputYourOpenAIKey": "wpisz swój klucz AI w ustawieniach", + "pleaseInputYourOpenAIKey": "wpisz swój klucz OpenAI w ustawieniach", + "pleaseInputYourStabilityAIKey": "wpisz swój klucz Stability AI w ustawieniach", "saveImageToGallery": "Zapisz obraz", "failedToAddImageToGallery": "Nie udało się dodać obrazu do galerii", "successToAddImageToGallery": "Pomyślnie dodano obraz do galerii", - "unableToLoadImage": "Nie udało się wczytać obrazu", - "pleaseInputYourStabilityAIKey": "wpisz swój klucz Stability AI w ustawieniach" + "unableToLoadImage": "Nie udało się wczytać obrazu" }, "codeBlock": { "language": { @@ -1145,4 +1077,4 @@ "language": "Język", "font": "Czcionka" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 864d225095..dc1340d3b8 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -9,7 +9,6 @@ "title": "Título", "youCanAlso": "Você também pode", "and": "e", - "failedToOpenUrl": "Falha ao abrir url: {}", "blockActions": { "addBelowTooltip": "Clique para adicionar abaixo", "addAboveCmd": "Alt+clique", @@ -36,39 +35,17 @@ "loginButtonText": "Conectar-se", "loginStartWithAnonymous": "Iniciar com uma sessão anônima", "continueAnonymousUser": "Continuar em uma sessão anônima", - "anonymous": "Anônimo", "buttonText": "Entre", "signingInText": "Entrando...", "forgotPassword": "Esqueceu sua senha?", "emailHint": "E-mail", "passwordHint": "Senha", "dontHaveAnAccount": "Não possui uma conta?", - "createAccount": "Criar uma conta", "repeatPasswordEmptyError": "Senha não pode estar em branco.", "unmatchedPasswordError": "As senhas não conferem.", "syncPromptMessage": "A sincronização dos dados pode demorar um pouco. Por favor não feche esta página", "or": "OU", - "signInWithGoogle": "Continuar com o Google", - "signInWithGithub": "Continuar com o Github", - "signInWithDiscord": "Continuar com o Discord", - "signInWithApple": "Continuar com a Apple", - "continueAnotherWay": "Continuar de outra forma", - "signUpWithGoogle": "Cadastro com o Google", - "signUpWithGithub": "Cadastro com o Github", - "signUpWithDiscord": "Cadastro com o Discord", "signInWith": "Entrar com:", - "signInWithEmail": "Continuar com e-mail", - "signInWithMagicLink": "Continuar", - "signUpWithMagicLink": "Cadastro com um Link Mágico", - "pleaseInputYourEmail": "Por favor, insira seu endereço de e-mail", - "settings": "Configurações", - "magicLinkSent": "Link Mágico enviado!", - "invalidEmail": "Por favor, insira um endereço de e-mail válido", - "alreadyHaveAnAccount": "Já tem uma conta?", - "logIn": "Entrar", - "generalError": "Algo deu errado. Tente novamente mais tarde.", - "limitRateError": "Por razões de segurança, você só pode solicitar um link mágico a cada 60 segundos", - "magicLinkSentDescription": "Um Link Mágico foi enviado para seu e-mail. Clique no link para concluir seu login. O link expirará após 5 minutos.", "LogInWithGoogle": "Entrar com o Google", "LogInWithGithub": "Entrar com o Github", "LogInWithDiscord": "Entrar com o Discord", @@ -78,50 +55,21 @@ "chooseWorkspace": "Escolha seu espaço de trabalho", "create": "Crie um espaço de trabalho", "reset": "Redefinir espaço de trabalho", - "renameWorkspace": "Renomear espaço de trabalho", "resetWorkspacePrompt": "A redefinição do espaço de trabalho excluirá todas as páginas e dados contidos nele. Tem certeza de que deseja redefinir o espaço de trabalho? Alternativamente, você pode entrar em contato com a equipe de suporte para restaurar o espaço de trabalho", "hint": "Espaço de trabalho", "notFoundError": "Espaço de trabalho não encontrado", "failedToLoad": "Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.", "errorActions": { "reportIssue": "Reporte um problema", - "reportIssueOnGithub": "Reportar um problema no Github", - "exportLogFiles": "Exportar arquivos de log", "reachOut": "Entre em contato no Discord" - }, - "menuTitle": "Espaços de trabalho", - "deleteWorkspaceHintText": "Tem certeza de que deseja excluir o espaço de trabalho? Esta ação não pode ser desfeita, e quaisquer páginas que você tenha publicado deixarão de estar publicadas.", - "createSuccess": "Espaço de trabalho criado com sucesso", - "createFailed": "Falha ao criar espaço de trabalho", - "createLimitExceeded": "Você atingiu o limite máximo de espaços de trabalho permitido para sua conta. Se precisar de espaços de trabalho adicionais para continuar seu trabalho, solicite no Github", - "deleteSuccess": "Espaço de trabalho excluído com sucesso", - "deleteFailed": "Falha ao excluir o espaço de trabalho", - "openSuccess": "Espaço de trabalho aberto com sucesso", - "openFailed": "Falha ao abrir o espaço de trabalho", - "renameSuccess": "Espaço de trabalho renomeado com sucesso", - "renameFailed": "Falha ao renomear o espaço de trabalho", - "updateIconSuccess": "Ícone do espaço de trabalho atualizado com sucesso", - "updateIconFailed": "Falha ao atualizar ícone do espaço de trabalho", - "cannotDeleteTheOnlyWorkspace": "Não é possível excluir o único espaço de trabalho", - "fetchWorkspacesFailed": "Falha ao buscar espaços de trabalho", - "leaveCurrentWorkspace": "Sair do espaço de trabalho", - "leaveCurrentWorkspacePrompt": "Tem certeza de que deseja sair do espaço de trabalho atual?" + } }, "shareAction": { "buttonText": "Compartilhar", "workInProgress": "Em breve", "markdown": "Marcador", - "clipboard": "Copiar para área de transferência", "csv": "CSV", - "copyLink": "Copiar link", - "publishToTheWeb": "Publicar na Web", - "publishToTheWebHint": "Crie um site com AppFlowy", - "publish": "Publicar", - "unPublish": "Remover publicação", - "visitSite": "Visitar site", - "exportAsTab": "Exportar como", - "publishTab": "Publicar", - "shareTab": "Compartilhar" + "copyLink": "Copiar link" }, "moreAction": { "small": "pequeno", @@ -129,12 +77,7 @@ "large": "grande", "fontSize": "Tamanho da fonte", "import": "Importar", - "moreOptions": "Mais opções", - "wordCount": "Contagem de palavras: {}", - "charCount": "Contagem de caracteres: {}", - "createdAt": "Criado: {}", - "deleteView": "Excluir", - "duplicateView": "Duplicar" + "moreOptions": "Mais opções" }, "importPanel": { "textAndMarkdown": "Texto e Remarcação", @@ -152,9 +95,7 @@ "openNewTab": "Abrir em uma nova guia", "moveTo": "Mover para", "addToFavorites": "Adicionar aos favoritos", - "copyLink": "Copiar link", - "changeIcon": "Alterar ícone", - "collapseAllPages": "Recolher todas as subpáginas" + "copyLink": "Copiar link" }, "blankPageTitle": "Página em branco", "newPageText": "Nova página", @@ -162,34 +103,6 @@ "newGridText": "Nova grelha", "newCalendarText": "Novo calendário", "newBoardText": "Novo quadro", - "chat": { - "newChat": "Bate-papo com IA", - "inputMessageHint": "Pergunte a IA @:appName", - "inputLocalAIMessageHint": "Pergunte a IA local @:appName", - "unsupportedCloudPrompt": "Este recurso só está disponível ao usar a nuvem @:appName", - "relatedQuestion": "Relacionado", - "serverUnavailable": "Serviço Temporariamente Indisponível. Tente novamente mais tarde.", - "aiServerUnavailable": "🌈 Uh-oh! 🌈. Um unicórnio comeu nossa resposta. Por favor, tente novamente!", - "clickToRetry": "Clique para tentar novamente", - "regenerateAnswer": "Gerar novamente", - "question1": "Como usar Kanban para gerenciar tarefas", - "question2": "Explique o método GTD", - "question3": "Por que usar Rust", - "question4": "Receita com o que tenho na cozinha", - "aiMistakePrompt": "A IA pode cometer erros. Verifique informações importantes.", - "chatWithFilePrompt": "Você quer conversar com o arquivo?", - "indexFileSuccess": "Arquivo indexado com sucesso", - "inputActionNoPages": "Nenhum resultado", - "referenceSource": { - "zero": "0 fontes encontradas", - "one": "{count} fonte encontrada", - "other": "{count} fontes encontradas" - }, - "clickToMention": "Clique para mencionar uma página", - "uploadFile": "Carregue arquivos PDFs, md ou txt para conversar", - "questionDetail": "Olá {}! Como posso te ajudar hoje?", - "indexingFile": "Indexando {}" - }, "trash": { "text": "Lixeira", "restoreAll": "Restaurar tudo", @@ -213,8 +126,7 @@ "emptyDescription": "Você não tem nenhum arquivo excluído", "isDeleted": "foi deletado", "isRestored": "foi restaurado" - }, - "confirmDeleteTitle": "Tem certeza de que deseja excluir esta página permanentemente?" + } }, "deletePagePrompt": { "text": "Está página está na lixeira", @@ -225,14 +137,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", + "help": "Ajuda e Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Informação de depuração copiada para a área de transferência!", "fail": "Falha ao copiar a informação de depuração para a área de transferência" }, - "feedback": "Opinião", - "help": "Ajuda e Suporte" + "feedback": "Opinião" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", @@ -268,53 +180,17 @@ "dragRow": "Pressione e segure para reordenar a linha", "viewDataBase": "Visualizar banco de dados", "referencePage": "Esta {name} é uma referência", - "addBlockBelow": "Adicione um bloco abaixo", - "aiGenerate": "Gerar" + "addBlockBelow": "Adicione um bloco abaixo" }, "sideBar": { "closeSidebar": "Fechar barra lateral", "openSidebar": "Abrir barra lateral", "personal": "Pessoal", - "private": "Privado", - "workspace": "Espaço de trabalho", "favorites": "Favoritos", - "clickToHidePrivate": "Clique para ocultar o espaço privado\nAs páginas que você criou aqui são visíveis apenas para você", - "clickToHideWorkspace": "Clique para ocultar o espaço de trabalho\nAs páginas que você criou aqui são visíveis para todos os membros", "clickToHidePersonal": "Clique para ocultar a seção pessoal", "clickToHideFavorites": "Clique para ocultar a seção favorita", "addAPage": "Adicionar uma página", - "addAPageToPrivate": "Adicionar uma página ao espaço privado", - "addAPageToWorkspace": "Adicionar uma página ao espaço de trabalho", - "recent": "Recentes", - "today": "Hoje", - "thisWeek": "Essa semana", - "others": "Favoritos anteriores", - "justNow": "agora mesmo", - "minutesAgo": "{count} minutos atrás", - "lastViewed": "Última visualização", - "favoriteAt": "Favorito", - "emptyRecent": "Nenhum documento recente", - "emptyRecentDescription": "Conforme você visualiza os documentos, eles aparecerão aqui para fácil recuperação", - "emptyFavorite": "Nenhum documento favorito", - "emptyFavoriteDescription": "Comece a explorar e marque os documentos como favoritos. Eles serão listados aqui para acesso rápido!", - "removePageFromRecent": "Remover esta página dos Recentes?", - "removeSuccess": "Removido com sucesso", - "favoriteSpace": "Favoritos", - "RecentSpace": "Recente", - "Spaces": "Espaços", - "upgradeToPro": "Atualizar para Pro", - "upgradeToAIMax": "Desbloqueie IA ilimitada", - "storageLimitDialogTitle": "Você ficou sem armazenamento gratuito. Atualize para desbloquear armazenamento ilimitado", - "aiResponseLimitTitle": "Você ficou sem respostas de IA gratuitas. Atualize para o Plano Pro ou adquira um complemento de IA para desbloquear respostas ilimitadas", - "aiResponseLimitDialogTitle": "Limite de respostas de IA atingido", - "aiResponseLimit": "Você ficou sem respostas de IA gratuitas.\n\nVá para Configurações -> Plano -> Clique em AI Max ou Plano Pro para obter mais respostas de IA", - "askOwnerToUpgradeToPro": "Seu espaço de trabalho está ficando sem armazenamento gratuito. Peça ao proprietário do seu espaço de trabalho para atualizar para o Plano Pro", - "askOwnerToUpgradeToAIMax": "Seu espaço de trabalho está ficando sem respostas de IA gratuitas. Peça ao proprietário do seu espaço de trabalho para atualizar o plano ou adquirir complementos de IA", - "purchaseStorageSpace": "Adquirir espaço de armazenamento", - "purchaseAIResponse": "Adquirir ", - "askOwnerToUpgradeToLocalAI": "Peça ao proprietário do espaço de trabalho para habilitar a IA no dispositivo", - "upgradeToAILocal": "Execute modelos locais no seu dispositivo para máxima privacidade", - "upgradeToAILocalDesc": "Converse com PDFs, melhore sua escrita e preencha tabelas automaticamente usando IA local" + "recent": "Recentes" }, "notifications": { "export": { @@ -330,7 +206,6 @@ }, "button": { "ok": "OK", - "confirm": "Confirmar", "done": "Feito", "cancel": "Cancelar", "signIn": "Conectar", @@ -353,34 +228,11 @@ "update": "Atualizar", "share": "Compartilhar", "removeFromFavorites": "Remover dos favoritos", - "removeFromRecent": "Remover dos recentes", "addToFavorites": "Adicionar aos favoritos", - "favoriteSuccessfully": "Adicionado aos favoritos", - "unfavoriteSuccessfully": "Removido dos favoritos", - "duplicateSuccessfully": "Duplicado com sucesso", "rename": "Renomear", "helpCenter": "Central de Ajuda", "add": "Adicionar", "yes": "Sim", - "no": "Não", - "clear": "Limpar", - "remove": "Remover", - "dontRemove": "Não remova", - "copyLink": "Copiar Link", - "align": "Alinhar", - "login": "Entrar", - "logout": "Sair", - "deleteAccount": "Deletar conta", - "back": "Voltar", - "signInGoogle": "Continuar com o Google", - "signInGithub": "Continuar com o Github", - "signInDiscord": "Continuar com o Discord", - "more": "Mais", - "create": "Criar", - "close": "Fechar", - "next": "Próximo", - "previous": "Anterior", - "submit": "Enviar", "tryAGain": "Tentar novamente" }, "label": { @@ -405,190 +257,6 @@ }, "settings": { "title": "Configurações", - "popupMenuItem": { - "settings": "Configurações", - "members": "Membros", - "trash": "Lixo", - "helpAndSupport": "Ajuda e Suporte" - }, - "accountPage": { - "menuLabel": "Minha conta", - "title": "Minha conta", - "general": { - "title": "Nome da conta e foto de perfil", - "changeProfilePicture": "Alterar foto do perfil" - }, - "email": { - "title": "E-mail", - "actions": { - "change": "Alterar e-mail" - } - }, - "login": { - "title": "Entrar com uma conta", - "loginLabel": "Entrar", - "logoutLabel": "Sair" - } - }, - "workspacePage": { - "menuLabel": "Espaço de trabalho", - "title": "Espaço de trabalho", - "description": "Personalize a aparência do seu espaço de trabalho, tema, fonte, layout de texto, formato de data/hora e idioma.", - "workspaceName": { - "title": "Nome do espaço de trabalho" - }, - "workspaceIcon": { - "title": "Ícone do espaço de trabalho", - "description": "Carregue uma imagem ou use um emoji para seu espaço de trabalho. O ícone será exibido na sua barra lateral e notificações." - }, - "appearance": { - "title": "Aparência", - "description": "Personalize a aparência do seu espaço de trabalho, tema, fonte, layout de texto, data, hora e idioma.", - "options": { - "system": "Automático", - "light": "Claro", - "dark": "Escuro" - } - }, - "resetCursorColor": { - "title": "Redefinir a cor do cursor do documento", - "description": "Tem certeza de que deseja redefinir a cor do cursor?" - }, - "resetSelectionColor": { - "title": "Redefinir cor de seleção de documento", - "description": "Tem certeza de que deseja redefinir a cor de seleção?" - }, - "theme": { - "title": "Tema", - "description": "Selecione um tema predefinido ou carregue seu próprio tema personalizado.", - "uploadCustomThemeTooltip": "Carregar um tema personalizado" - }, - "workspaceFont": { - "title": "Fonte do espaço de trabalho", - "noFontHint": "Nenhuma fonte encontrada, tente outro termo." - }, - "textDirection": { - "title": "Direção do texto", - "leftToRight": "Da esquerda para a direita", - "rightToLeft": "Da direita para a esquerda", - "auto": "Automático", - "enableRTLItems": "Habilitar items da barra de ferramenta da direita para a esquerda" - }, - "layoutDirection": { - "title": "Direção do layout", - "leftToRight": "Da esquerda para a direita", - "rightToLeft": "Da direita para a esquerda" - }, - "dateTime": { - "title": "Data e hora", - "example": "{} as {} ({})", - "24HourTime": "Tempo de 24 horas", - "dateFormat": { - "label": "Formato de data", - "us": "EUA", - "friendly": "Amigável" - } - }, - "language": { - "title": "Língua" - }, - "deleteWorkspacePrompt": { - "title": "Excluir espaço de trabalho", - "content": "Tem certeza de que deseja excluir este espaço de trabalho? Esta ação não pode ser desfeita, e quaisquer páginas que você tenha publicado deixarão de estar publicadas." - }, - "leaveWorkspacePrompt": { - "title": "Sair do espaço de trabalho", - "content": "Tem certeza de que deseja sair deste espaço de trabalho? Você perderá o acesso a todas as páginas e dados dentro dele." - }, - "manageWorkspace": { - "title": "Gerenciar espaço de trabalho", - "leaveWorkspace": "Sair do espaço de trabalho", - "deleteWorkspace": "Excluir espaço de trabalho" - } - }, - "manageDataPage": { - "menuLabel": "Gerenciar dados", - "title": "Gerenciar dados", - "description": "Gerencie o armazenamento local de dados ou importe seus dados existentes para @:appName .", - "dataStorage": { - "title": "Local de armazenamento de arquivos", - "tooltip": "O local onde seus arquivos são armazenados", - "actions": { - "change": "Mudar caminho", - "open": "Abrir pasta", - "openTooltip": "Abrir local da pasta de dados atual", - "copy": "Copiar caminho", - "copiedHint": "Caminho copiado!", - "resetTooltip": "Redefinir para o local padrão" - }, - "resetDialog": { - "title": "Tem certeza?", - "description": "Redefinir o caminho para o local de dados padrão não excluirá seus dados. Se você quiser reimportar seus dados atuais, você deve copiar o caminho do seu local atual primeiro." - } - }, - "importData": { - "title": "Importar dados", - "tooltip": "Importar dados das pastas de backups/dados de @:appName", - "description": "Copiar dados de uma pasta de dados externa ao @:appName", - "action": "Selecionar arquivo" - }, - "encryption": { - "title": "Criptografia", - "tooltip": "Gerencie como seus dados são armazenados e criptografados", - "descriptionNoEncryption": "Ativar a criptografia criptografará todos os dados. Isso não pode ser desfeito.", - "descriptionEncrypted": "Seus dados estão criptografados.", - "action": "Criptografar dados", - "dialog": { - "title": "Criptografar todos os seus dados?", - "description": "Criptografar todos os seus dados manterá seus dados seguros e protegidos. Esta ação NÃO pode ser desfeita. Tem certeza de que deseja continuar?" - } - }, - "cache": { - "title": "Limpar cache", - "description": "Ajude a resolver problemas como imagem não carregando, páginas faltando em um espaço e fontes não carregando. Isso não afetará seus dados.", - "dialog": { - "title": "Limpar cache", - "description": "Ajude a resolver problemas como imagem não carregando, páginas faltando em um espaço e fontes não carregando. Isso não afetará seus dados.", - "successHint": "Cache limpo!" - } - }, - "data": { - "fixYourData": "Corrija seus dados", - "fixButton": "Corrigir", - "fixYourDataDescription": "Se estiver com problemas com seus dados, você pode tentar corrigi-los aqui." - } - }, - "shortcutsPage": { - "menuLabel": "Atalhos", - "title": "Atalhos", - "editBindingHint": "Insira uma nova combinação", - "searchHint": "Pesquisar", - "actions": { - "resetDefault": "Redefinir padrão" - }, - "errorPage": { - "message": "Falha ao carregar atalhos: {}", - "howToFix": "Por favor, tente novamente. Se o problema persistir, entre em contato pelo GitHub." - }, - "resetDialog": { - "title": "Redefinir atalhos", - "description": "Isso redefinirá todas as suas combinações de teclas para o padrão. Você não poderá desfazer isso depois. Tem certeza de que deseja continuar?", - "buttonLabel": "Redefinir" - }, - "conflictDialog": { - "title": "{} já está em uso", - "descriptionPrefix": "Esta combinação de teclas está sendo usada atualmente por ", - "descriptionSuffix": ". Se você substituir esta combinação de teclas, ela será removida de {}.", - "confirmLabel": "Continuar" - }, - "editTooltip": "Pressione para começar a editar a combinação de teclas", - "keybindings": { - "toggleToDoList": "Alternar para a lista de tarefas", - "insertNewParagraphInCodeblock": "Inserir novo parágrafo", - "pasteInCodeblock": "Colar bloco de código", - "selectAllCodeblock": "Selecionar tudo" - } - }, "menu": { "appearance": "Aparência", "language": "Idioma", @@ -608,6 +276,10 @@ "cloudServerType": "Servidor em nuvem", "cloudServerTypeTip": "Observe que ele pode desconectar sua conta atual após mudar o servidor em nuvem", "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL da Supabase", + "cloudSupabaseAnonKey": "Chave anônima Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "A chave anon não pode estar vazia se o URL da supabase não estiver vazio", "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Clique para copiar", "selfHostStart": "Se você não possui um servidor, consulte o", @@ -632,7 +304,8 @@ "importAppFlowyDataDescription": "Copie dados de uma pasta de dados externa do @:appName e importe-os para a pasta de dados atual do @:appName", "importSuccess": "Importou com sucesso a pasta de dados do @:appName", "importFailed": "Falha ao importar a pasta de dados do @:appName", - "importGuide": "Para mais detalhes, consulte o documento referenciado" + "importGuide": "Para mais detalhes, consulte o documento referenciado", + "supabaseSetting": "Configuração de Supabase" }, "notifications": { "enableNotifications": { @@ -748,26 +421,9 @@ "email": "E-mail", "tooltipSelectIcon": "Selecionar ícone", "selectAnIcon": "Escolha um ícone", - "pleaseInputYourOpenAIKey": "por favor insira sua chave AI", - "clickToLogout": "Clique para sair do usuário atual", - "pleaseInputYourStabilityAIKey": "insira sua chave Stability AI" - }, - "mobile": { - "personalInfo": "Informações pessoais", - "username": "Nome de usuário", - "usernameEmptyError": "O nome de usuário não pode ficar vazio", - "about": "Sobre", - "pushNotifications": "Notificações via push", - "support": "Apoiar", - "joinDiscord": "Junte-se a nós no Discord", - "privacyPolicy": "Política de Privacidade", - "userAgreement": "Termo de Acordo do Usuário", - "termsAndConditions": "Termos e Condições", - "userprofileError": "Falha ao carregar o perfil do usuário", - "userprofileErrorDescription": "Tente sair e fazer login novamente para verificar se o problema ainda persiste.", - "selectLayout": "Selecione o layout", - "selectStartingDay": "Selecione o dia de início", - "version": "Versão" + "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI", + "pleaseInputYourStabilityAIKey": "insira sua chave Stability AI", + "clickToLogout": "Clique para sair do usuário atual" }, "shortcuts": { "shortcutsLabel": "Atalhos", @@ -789,6 +445,23 @@ "textAlignRight": "Alinhar texto à direita", "codeBlockDeleteTwoSpaces": "Bloco de código: excluir dois espaços no início da linha" } + }, + "mobile": { + "personalInfo": "Informações pessoais", + "username": "Nome de usuário", + "usernameEmptyError": "O nome de usuário não pode ficar vazio", + "about": "Sobre", + "pushNotifications": "Notificações via push", + "support": "Apoiar", + "joinDiscord": "Junte-se a nós no Discord", + "privacyPolicy": "Política de Privacidade", + "userAgreement": "Termo de Acordo do Usuário", + "termsAndConditions": "Termos e Condições", + "userprofileError": "Falha ao carregar o perfil do usuário", + "userprofileErrorDescription": "Tente sair e fazer login novamente para verificar se o problema ainda persiste.", + "selectLayout": "Selecione o layout", + "selectStartingDay": "Selecione o dia de início", + "version": "Versão" } }, "grid": { @@ -968,7 +641,9 @@ "createNew": "Crie um novo", "orSelectOne": "Ou selecione uma opção", "typeANewOption": "Digite uma nova opção", - "tagName": "Nome da etiqueta" + "tagName": "Nome da etiqueta", + "colorPannelTitle": "Cores", + "pannelTitle": "Escolha uma opção ou crie uma" }, "checklist": { "taskHint": "Descrição da tarefa", @@ -1021,18 +696,18 @@ "autoGeneratorLearnMore": "Saiba mais", "autoGeneratorGenerate": "Gerar", "autoGeneratorHintText": "Diga-nos o que você deseja gerar por IA ...", - "autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da AI", + "autoGeneratorCantGetOpenAIKey": "Não foi possível obter a chave da OpenAI", "autoGeneratorRewrite": "Reescrever", "smartEdit": "Assistentes de IA", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Corrigir ortografia", "warning": "⚠️ As respostas da IA podem ser imprecisas ou enganosas.", "smartEditSummarize": "Resumir", "smartEditImproveWriting": "melhorar a escrita", "smartEditMakeLonger": "Faça mais", - "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do AI", - "smartEditCouldNotFetchKey": "Não foi possível obter a chave AI", - "smartEditDisabled": "Conecte AI em Configurações", + "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI", + "smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI", + "smartEditDisabled": "Conecte OpenAI em Configurações", "discardResponse": "Deseja descartar as respostas de IA?", "createInlineMathEquation": "Criar equação", "fonts": "Fontes", @@ -1090,8 +765,8 @@ "defaultColor": "Padrão" }, "image": { - "addAnImage": "Adicione uma imagem", - "copiedToPasteBoard": "O link da imagem foi copiado para a área de transferência" + "copiedToPasteBoard": "O link da imagem foi copiado para a área de transferência", + "addAnImage": "Adicione uma imagem" }, "urlPreview": { "copiedToPasteBoard": "O link foi copiado para a área de transferência" @@ -1140,8 +815,8 @@ "placeholder": "Insira o URL da imagem" }, "ai": { - "label": "Gerar imagem da AI", - "placeholder": "Insira o prompt para AI gerar imagem" + "label": "Gerar imagem da OpenAI", + "placeholder": "Insira o prompt para OpenAI gerar imagem" }, "stability_ai": { "label": "Gerar imagem da Stability AI", @@ -1162,12 +837,12 @@ "label": "Remover respingo" }, "searchForAnImage": "Procurar uma imagem", - "pleaseInputYourOpenAIKey": "insira sua chave AI na página configurações", + "pleaseInputYourOpenAIKey": "insira sua chave OpenAI na página configurações", + "pleaseInputYourStabilityAIKey": "insira sua chave Stability AI na página Configurações", "saveImageToGallery": "Salvar imagem", "failedToAddImageToGallery": "Falha ao adicionar imagem à galeria", "successToAddImageToGallery": "Imagem adicionada à galeria com sucesso", - "unableToLoadImage": "Não foi possível carregar a imagem", - "pleaseInputYourStabilityAIKey": "insira sua chave Stability AI na página Configurações" + "unableToLoadImage": "Não foi possível carregar a imagem" }, "codeBlock": { "language": { @@ -1544,4 +1219,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index c9892bf9df..1b5ee1fcd1 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -128,14 +128,14 @@ "questionBubble": { "shortcuts": "Atalhos", "whatsNew": "O que há de novo?", + "help": "Ajuda & Suporte", "markdown": "Remarcação", "debug": { "name": "Informação de depuração", "success": "Copiar informação de depuração para o clipboard!", "fail": "Falha em copiar a informação de depuração para o clipboard" }, - "feedback": "Opinião", - "help": "Ajuda & Suporte" + "feedback": "Opinião" }, "menuAppHeader": { "moreButtonToolTip": "Remover, renomear e muito mais...", @@ -255,7 +255,8 @@ "inputTextFieldHint": "A sua palavra-chave", "historicalUserList": "Histórico de login do utilizador", "historicalUserListTooltip": "Esta lista mostrs suas contas anónimas. Pode clicar numa conta para ver os detalhes. Contas anónimas são criadas clicando no botão ‘Começar’", - "openHistoricalUser": "Clique para abrir a conta anónima" + "openHistoricalUser": "Clique para abrir a conta anónima", + "supabaseSetting": "Configuração de Supabase" }, "notifications": { "enableNotifications": { @@ -361,9 +362,9 @@ "email": "E-mail", "tooltipSelectIcon": "Selecione o ícone", "selectAnIcon": "Selecione um ícone", - "pleaseInputYourOpenAIKey": "por favor insira sua chave AI", - "clickToLogout": "Clique para fazer logout", - "pleaseInputYourStabilityAIKey": "por favor, insira a sua chave Stability AI" + "pleaseInputYourOpenAIKey": "por favor insira sua chave OpenAI", + "pleaseInputYourStabilityAIKey": "por favor, insira a sua chave Stability AI", + "clickToLogout": "Clique para fazer logout" }, "shortcuts": { "shortcutsLabel": "Atalhos", @@ -560,23 +561,23 @@ "referencedBoard": "Conselho Referenciado", "referencedGrid": "grade referenciada", "referencedCalendar": "calendário referenciado", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Peça à IA para escrever qualquer coisa...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Peça à IA para escrever qualquer coisa...", "autoGeneratorLearnMore": "Saber mais", "autoGeneratorGenerate": "Gerar", - "autoGeneratorHintText": "Pergunte ao AI...", - "autoGeneratorCantGetOpenAIKey": "Não é possível obter a chave AI", + "autoGeneratorHintText": "Pergunte ao OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Não é possível obter a chave OpenAI", "autoGeneratorRewrite": "Reescrever", "smartEdit": "Assistentes de IA", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "corrigir ortografia", "warning": "⚠️ As respostas da IA podem ser imprecisas ou enganosas.", "smartEditSummarize": "Resumir", "smartEditImproveWriting": "melhorar a escrita", "smartEditMakeLonger": "Faça mais", - "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do AI", - "smartEditCouldNotFetchKey": "Não foi possível obter a chave AI", - "smartEditDisabled": "Conecte AI em Configurações", + "smartEditCouldNotFetchResult": "Não foi possível obter o resultado do OpenAI", + "smartEditCouldNotFetchKey": "Não foi possível obter a chave OpenAI", + "smartEditDisabled": "Conecte OpenAI em Configurações", "discardResponse": "Deseja descartar as respostas de IA?", "createInlineMathEquation": "Criar equação", "toggleList": "Alternar lista", @@ -624,8 +625,8 @@ "defaultColor": "Padrão" }, "image": { - "addAnImage": "Adicione uma imagem", - "copiedToPasteBoard": "O link da imagem foi copiado para a área de transferência" + "copiedToPasteBoard": "O link da imagem foi copiado para a área de transferência", + "addAnImage": "Adicione uma imagem" }, "outline": { "addHeadingToCreateOutline": "Adicione títulos para criar um sumário." @@ -661,8 +662,8 @@ "placeholder": "Insira o URL da imagem" }, "ai": { - "label": "Gerar imagem da AI", - "placeholder": "Por favor, insira o comando para a AI gerar a imagem" + "label": "Gerar imagem da OpenAI", + "placeholder": "Por favor, insira o comando para a OpenAI gerar a imagem" }, "stability_ai": { "label": "Gerar imagem da Stability AI", @@ -680,7 +681,7 @@ "placeholder": "Cole ou digite uma hiperligação de imagem" }, "searchForAnImage": "Procure uma imagem", - "pleaseInputYourOpenAIKey": "por favor, insira a sua chave AI na página Configurações", + "pleaseInputYourOpenAIKey": "por favor, insira a sua chave OpenAI na página Configurações", "pleaseInputYourStabilityAIKey": "por favor, insira a sua chave Stability AI na página Configurações" }, "codeBlock": { @@ -856,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index c45010b8fc..89b81271dc 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -9,7 +9,6 @@ "title": "Заголовок", "youCanAlso": "Вы также можете", "and": "и", - "failedToOpenUrl": "Не удалось открыть URL: {}", "blockActions": { "addBelowTooltip": "Нажмите, чтобы добавить ниже", "addAboveCmd": "Alt+клик", @@ -25,7 +24,7 @@ "emptyPasswordError": "Пароль не может быть пустым", "repeatPasswordEmptyError": "Повтор пароля не может быть пустым", "unmatchedPasswordError": "Пароли не совпадают", - "alreadyHaveAnAccount": "У Вас уже есть аккаунт?", + "alreadyHaveAnAccount": "Уже есть аккаунт?", "emailHint": "Электронная почта", "passwordHint": "Пароль", "repeatPasswordHint": "Повторите пароль", @@ -36,39 +35,17 @@ "loginButtonText": "Войти", "loginStartWithAnonymous": "Начать анонимную сессию", "continueAnonymousUser": "Продолжить анонимную сессию", - "anonymous": "Анонимно", "buttonText": "Авторизация", "signingInText": "Вход…", "forgotPassword": "Забыли пароль?", "emailHint": "Электронная почта", "passwordHint": "Пароль", - "dontHaveAnAccount": "У Вас ещё нет аккаунта?", - "createAccount": "Зарегистрироваться", + "dontHaveAnAccount": "Нет аккаунта?", "repeatPasswordEmptyError": "Повтор пароля не может быть пустым", "unmatchedPasswordError": "Пароли не совпадают", "syncPromptMessage": "Синхронизация данных может занять некоторое время. Пожалуйста, не закрывайте эту страницу", "or": "или", - "signInWithGoogle": "Продолжить с Google", - "signInWithGithub": "Продолжить с Github", - "signInWithDiscord": "Продолжить с Discord", - "signInWithApple": "Вход с Apple", - "continueAnotherWay": "Другие способы входа", - "signUpWithGoogle": "Зарегистрироваться с Google", - "signUpWithGithub": "Зарегистрироваться с Github", - "signUpWithDiscord": "Зарегистрироваться с Discord", "signInWith": "Войти с помощью:", - "signInWithEmail": "Продолжить с эл. почтой", - "signInWithMagicLink": "Продолжить", - "signUpWithMagicLink": "Зарегистрироваться с волшебной ссылкой", - "pleaseInputYourEmail": "Пожалуйста, введите ваш адрес эл. почты", - "settings": "Настройки", - "magicLinkSent": "Волшебная ссылка отправлена!", - "invalidEmail": "Пожалуйста, введите действующий адрес эл. почты", - "alreadyHaveAnAccount": "У Вас уже есть аккаунт?", - "logIn": "Войти", - "generalError": "Что-то пошло не так. Пожалуйста, повторите попытку позже.", - "limitRateError": "Из соображений безопасности вы можете запрашивать волшебную ссылку только каждые 60 секунд.", - "magicLinkSentDescription": "Волшебная ссылка была отправлена на ваш адрес эл. почты. Нажмите на ссылку, чтобы завершить вход. Срок действия ссылки истекает через 5 минут.", "LogInWithGoogle": "Войти через Google", "LogInWithGithub": "Войти через GitHub", "LogInWithDiscord": "Войти через Discord", @@ -76,60 +53,25 @@ }, "workspace": { "chooseWorkspace": "Выберите рабочее пространство", - "defaultName": "Моё рабочее пространство", "create": "Создать рабочее пространство", - "new": "Новое рабочее пространство", - "importFromNotion": "Импортировать с Notion", - "learnMore": "Узнать больше", "reset": "Сбросить рабочее пространство", - "renameWorkspace": "Переименовать рабочее пространство", - "workspaceNameCannotBeEmpty": "Название рабочего пространства не может быть пустым", "resetWorkspacePrompt": "Сброс рабочего пространства приведет к удалению всех страниц и данных внутри него. Вы уверены, что хотите сбросить рабочее пространство? В качестве альтернативы вы можете обратиться в службу поддержки для восстановления рабочего пространства", "hint": "рабочее пространство", - "notFoundError": "Рабочее пространство не найдено.", + "notFoundError": "Рабочее пространство не найдено", "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры @:appName и повторите попытку.", "errorActions": { "reportIssue": "Сообщить о проблеме", "reportIssueOnGithub": "Сообщить о проблеме на Github", "exportLogFiles": "Экспорт логов", "reachOut": "Написать в Discord" - }, - "menuTitle": "Рабочие пространства", - "deleteWorkspaceHintText": "Вы уверены, что хотите удалить рабочее пространство? Это действие нельзя отменить, и все опубликованные страницы будут отменены.", - "createSuccess": "Рабочее пространство успешно создано.", - "createFailed": "Не удалось создать рабочее пространство.", - "createLimitExceeded": "Вы достигли максимального количества рабочих пространств для вашего аккаунта. Если вам нужны дополнительные рабочие пространства для продолжения работы, отправьте запрос на Github.", - "deleteSuccess": "Рабочее пространство успешно удалено.", - "deleteFailed": "Не удалось удалить рабочее пространство.", - "openSuccess": "Рабочее пространство успешно открыто.", - "openFailed": "Не удалось открыть рабочее пространство.", - "renameSuccess": "Рабочее пространство успешно переименовано.", - "renameFailed": "Не удалось переименовать рабочее пространство.", - "updateIconSuccess": "Иконка рабочего пространства успешно обновлена.", - "updateIconFailed": "Не удалось обновить иконку рабочего пространства.", - "cannotDeleteTheOnlyWorkspace": "Нельзя удалить единственное рабочее пространство.", - "fetchWorkspacesFailed": "Не удалось получить рабочие пространства.", - "leaveCurrentWorkspace": "Покинуть рабочее пространство", - "leaveCurrentWorkspacePrompt": "Вы уверены, что хотите покинуть текущее рабочее пространство?" + } }, "shareAction": { "buttonText": "Поделиться", "workInProgress": "В разработке", "markdown": "Markdown", - "html": "HTML", - "clipboard": "Скопировать в буфер обмена", "csv": "CSV", - "copyLink": "Скопировать ссылку", - "publishToTheWeb": "Опубликовать в интернете", - "publishToTheWebHint": "Создать веб-сайт с AppFlowy", - "publish": "Опубликовать", - "unPublish": "Отменить", - "visitSite": "Посетить сайт", - "exportAsTab": "Экспортировать как", - "publishTab": "Опубликовать", - "shareTab": "Поделиться", - "publishOnAppFlowy": "Выложить на AppFlowy", - "shareTabTitle": "Пригласить к сотрудничеству" + "copyLink": "Скопировать ссылку" }, "moreAction": { "small": "маленький", @@ -151,11 +93,6 @@ "csv": "CSV", "database": "База данных" }, - "emojiIconPicker": { - "iconUploader": { - "change": "Изменить" - } - }, "disclosureAction": { "rename": "Переименовать", "delete": "Удалить", @@ -165,10 +102,7 @@ "openNewTab": "Открыть в новой вкладке", "moveTo": "Переместить в", "addToFavorites": "Добавить в избранное", - "copyLink": "Скопировать ссылку", - "changeIcon": "Изменить иконку", - "collapseAllPages": "Свернуть все подстраницы", - "lockPage": "Заблокировать страницу" + "copyLink": "Скопировать ссылку" }, "blankPageTitle": "Пустая страница", "newPageText": "Новая страница", @@ -176,46 +110,9 @@ "newGridText": "Новая таблица", "newCalendarText": "Новый календарь", "newBoardText": "Новая доска", - "chat": { - "newChat": "Чат с ИИ", - "inputMessageHint": "Спросите ИИ @:appName", - "unsupportedCloudPrompt": "Эта функция доступна только при использовании облака @:appName", - "relatedQuestion": "Связано", - "serverUnavailable": "Сервис временно недоступен. Пожалуйста, повторите попытку позже.", - "aiServerUnavailable": "🌈 Ой-ой! 🌈. Единорог съел наш ответ. Пожалуйста, повторите попытку!", - "retry": "Повторить", - "clickToRetry": "Нажмите, чтобы повторить попытку", - "regenerateAnswer": "Повторно сгенерировать", - "question1": "Как использовать канбан для управления задачами.", - "question2": "Объясните метод GTD.", - "question3": "Зачем использовать Rust.", - "question4": "Рецепт из того, что есть у меня на кухне.", - "aiMistakePrompt": "ИИ может ошибаться. Проверяйте важную информацию.", - "inputActionNoPages": "Нет результатов на странице", - "currentPage": "Текущая страница", - "regenerate": "Попробуйте ещё раз", - "addToNewPage": "Создать новую страницу", - "openPagePreviewFailedToast": "Не удалось открыть страницу", - "changeFormat": { - "actionButton": "Изменить формат", - "textOnly": "Текст", - "imageOnly": "Только изображение", - "textAndImage": "Текст и изображение", - "text": "Параграф", - "bullet": "Список маркеров", - "number": "Нумерованный список", - "defaultDescription": "Автоматический режим" - }, - "selectBanner": { - "selectMessages": "Выбрать сообщения", - "allSelected": "Все выбрано" - }, - "stopTooltip": "Остановить генерацию" - }, "trash": { "text": "Корзина", "restoreAll": "Восстановить всё", - "restore": "Восстановить", "deleteAll": "Удалить всё", "pageHeader": { "fileName": "Имя файла", @@ -230,17 +127,13 @@ "title": "Вы уверены, что хотите восстановить все страницы в корзине?", "caption": "Это действие не может быть отменено." }, - "restorePage": { - "caption": "Вы уверены, что хотите восстановить эту страницу?" - }, "mobile": { "actions": "Действия с корзиной", "empty": "Корзина пуста", - "emptyDescription": "У Вас нет удалённых файлов", + "emptyDescription": "У вас нет удалённых файлов", "isDeleted": "удалён", "isRestored": "восстановлен" - }, - "confirmDeleteTitle": "Вы уверены, что хотите удалить эту страницу навсегда?" + } }, "deletePagePrompt": { "text": "Эта страница находится в корзине", @@ -251,14 +144,14 @@ "questionBubble": { "shortcuts": "Горячие клавиши", "whatsNew": "Что нового?", + "help": "Помощь и поддержка", "markdown": "Markdown", "debug": { "name": "Отладочная информация", "success": "Отладочная информация скопирована в буфер обмена!", "fail": "Не удалось скопировать отладочную информацию в буфер обмена" }, - "feedback": "Обратная связь", - "help": "Помощь и поддержка" + "feedback": "Обратная связь" }, "menuAppHeader": { "moreButtonToolTip": "Удалить, переименовать и другие действия...", @@ -294,43 +187,17 @@ "dragRow": "Перетащите для изменения порядка строк", "viewDataBase": "Просмотр базы данных", "referencePage": "Ссылки на {name}", - "addBlockBelow": "Добавьте блок ниже", - "aiGenerate": "Сгенерировать" + "addBlockBelow": "Добавьте блок ниже" }, "sideBar": { "closeSidebar": "Закрыть боковое меню", "openSidebar": "Открыть боковое меню", "personal": "Личное", - "private": "Личное", - "workspace": "Рабочее пространство", "favorites": "Избранное", - "clickToHidePrivate": "Нажмите, чтобы скрыть личное пространство\nСтраницы, созданные вами здесь, видны только вам.", - "clickToHideWorkspace": "Нажмите, чтобы скрыть рабочее пространство\nСтраницы, созданные вами здесь, видны каждому участнику.", "clickToHidePersonal": "Нажмите, чтобы скрыть личный раздел", "clickToHideFavorites": "Нажмите, чтобы скрыть раздел избранного", "addAPage": "Добавить страницу", - "addAPageToPrivate": "Добавить страницу в личное пространство", - "addAPageToWorkspace": "Добавить страницу в рабочее пространство", - "recent": "Недавние", - "today": "Сегодня", - "thisWeek": "На этой неделе", - "others": "Другие избранные", - "justNow": "только что", - "minutesAgo": "{count} минут назад", - "lastViewed": "Последний раз просмотрено", - "favoriteAt": "Добавлено в избранное", - "emptyRecent": "Нет последних документов", - "emptyRecentDescription": "Когда вы просматриваете документы, они будут появляться здесь для облегчения поиска.", - "emptyFavorite": "Нет избранных документов", - "emptyFavoriteDescription": "Начните изучать и отмечайте документы как избранные. Они будут перечислены здесь для быстрого доступа!", - "removePageFromRecent": "Удалить эту страницу из списка недавних?", - "removeSuccess": "Удалено успешно", - "favoriteSpace": "Избранное", - "RecentSpace": "Недавнее", - "Spaces": "Пространства", - "upgradeToPro": "Обновление до Pro", - "upgradeToAIMax": "Разблокируйте неограниченный ИИ", - "purchaseAIResponse": "Покупка " + "recent": "Недавние" }, "notifications": { "export": { @@ -346,7 +213,6 @@ }, "button": { "ok": "Ок", - "confirm": "Подтвердить", "done": "Готово", "cancel": "Отмена", "signIn": "Войти", @@ -364,48 +230,21 @@ "upload": "Загрузить", "edit": "Редактировать", "delete": "Удалить", - "copy": "Копировать", "duplicate": "Дублировать", "putback": "Вернуть", "update": "Обновить", "share": "Поделиться", "removeFromFavorites": "Удалить из избранного", - "removeFromRecent": "Удалить из недавних", "addToFavorites": "Добавить в избранное", "rename": "Переименовать", "helpCenter": "Центр помощи", "add": "Добавить", "yes": "Да", - "no": "Нет", "clear": "Очистить", "remove": "Удалить", "dontRemove": "Не удалять", "copyLink": "Скопировать ссылку", - "align": "Выровнять", - "login": "Войти", - "logout": "Выйти", - "deleteAccount": "Удалить аккаунт", - "back": "Назад", - "signInGoogle": "Продолжить с Google", - "signInGithub": "Продолжить с Github", - "signInDiscord": "Продолжить с Discord", - "more": "Больше", - "create": "Создать", - "close": "Закрыть", - "next": "Следующий", - "previous": "Предыдущий", - "submit": "Представить", - "download": "Скачать", - "backToHome": "Вернуться на главную", - "viewing": "Просмотр", - "editing": "Редактирование", - "gotIt": "Понятно", - "retry": "Повторить попытку", - "uploadFailed": "Загрузка не удалась.", - "tryAGain": "Попробовать ещё раз", - "Done": "Готово", - "Cancel": "Отмена", - "OK": "Хорошо" + "align": "Выровнять" }, "label": { "welcome": "Добро пожаловать!", @@ -429,483 +268,6 @@ }, "settings": { "title": "Настройки", - "popupMenuItem": { - "settings": "Настройки", - "members": "Участники", - "helpAndSupport": "Помощь и поддержка" - }, - "sites": { - "namespaceTitle": "Пространство имен", - "namespaceDescription": "Управляйте своим пространством имен и домашней страницей", - "namespaceHeader": "Пространство имен", - "homepageHeader": "Домашняя страница", - "updateNamespace": "Обновить пространство имен", - "removeHomepage": "Удалить домашнюю страницу", - "selectHomePage": "Выберите страницу", - "clearHomePage": "Очистить домашнюю страницу для этого пространства имен", - "customUrl": "Пользовательский URL-адрес", - "namespace": { - "description": "Это изменение будет применено ко всем опубликованным страницам, размещенным в этом пространстве имен." - }, - "publishedPage": { - "page": "Страница" - } - }, - "accountPage": { - "menuLabel": "Мой аккаунт", - "title": "Мой аккаунт", - "general": { - "title": "Имя аккаунта и изображение профиля", - "changeProfilePicture": "Изменить изображение профиля" - }, - "email": { - "title": "Адрес эл. почты", - "actions": { - "change": "Изменить адрес эл. почты" - } - }, - "login": { - "title": "Войти в аккаунт", - "loginLabel": "Войти", - "logoutLabel": "Выйти" - } - }, - "workspacePage": { - "menuLabel": "Рабочее пространство", - "title": "Рабочее пространство", - "description": "Настройте внешний вид рабочего пространства, тему, шрифт, оформление текста, формат даты и времени и язык.", - "workspaceName": { - "title": "Имя рабочего пространства" - }, - "workspaceIcon": { - "title": "Иконка рабочего пространства", - "description": "Загрузите изображение или используйте эмодзи для своего рабочего пространства. Иконка будет отображаться на боковой панели и в уведомлениях." - }, - "appearance": { - "title": "Внешний вид", - "description": "Настройте внешний вид рабочего пространства, тему, шрифт, оформление текста, дату, время и язык.", - "options": { - "system": "Авто", - "light": "Светлая", - "dark": "Темная" - } - }, - "theme": { - "title": "Тема", - "description": "Выберите предустановленную тему или загрузите собственную тему.", - "uploadCustomThemeTooltip": "Загрузить собственную тему" - }, - "workspaceFont": { - "title": "Шрифт рабочего пространства", - "noFontHint": "Шрифт не найден, попробуйте другой термин." - }, - "textDirection": { - "title": "Направление текста", - "leftToRight": "Слева направо", - "rightToLeft": "Справа налево", - "auto": "Авто", - "enableRTLItems": "Вкл. элементы пан. инстр. с письмом спр. нал." - }, - "layoutDirection": { - "title": "Направление оформления", - "leftToRight": "Слева направо", - "rightToLeft": "Справа налево" - }, - "dateTime": { - "title": "Дата и время", - "example": "{} в {} ({})", - "24HourTime": "24-часовое время", - "dateFormat": { - "label": "Формат даты", - "local": "Локальный", - "us": "США", - "iso": "ИСО", - "friendly": "Дружественный", - "dmy": "Д/М/Г" - } - }, - "language": { - "title": "Язык" - }, - "deleteWorkspacePrompt": { - "title": "Удалить рабочее пространство", - "content": "Вы уверены, что хотите удалить это рабочее пространство? Это действие нельзя отменить, и все опубликованные вами страницы будут отменены." - }, - "leaveWorkspacePrompt": { - "title": "Покинуть рабочее пространство", - "content": "Вы уверены, что хотите покинуть это рабочее пространство? Вы потеряете доступ ко всем страницам и данным на нем." - }, - "manageWorkspace": { - "title": "Управление рабочим пространством", - "leaveWorkspace": "Покинуть рабочее пространство", - "deleteWorkspace": "Удалить рабочее пространство" - } - }, - "manageDataPage": { - "menuLabel": "Управление данными", - "title": "Управление данными", - "description": "Управляйте локальным хранилищем данных или импортируйте существующие данные в @:appName.", - "dataStorage": { - "title": "Место хранения файлов", - "tooltip": "Место, где хранятся ваши файлы", - "actions": { - "change": "Изменить путь", - "open": "Открыть папку", - "openTooltip": "Открыть текущее местоположение папки с данными", - "copy": "Скопировать путь", - "copiedHint": "Путь скопирован!", - "resetTooltip": "Сброс к стандартному местоположению" - }, - "resetDialog": { - "title": "Вы уверены?", - "description": "Сброс пути к стандартному местоположению данных не приведёт к удалению ваших данных. Если вы хотите повторно импортировать текущие данные, вам следует сначала скопировать путь к вашему текущему местоположению." - } - }, - "importData": { - "title": "Импортировать данные", - "tooltip": "Импортировать данные из папок резервных копий или данных @:appName", - "description": "Копировать данные из внешней папки данных @:appName", - "action": "Смотреть файл" - }, - "encryption": { - "title": "Шифрование", - "tooltip": "Управлять тем, как ваши данные хранятся и шифруются", - "descriptionNoEncryption": "Включение шифрования зашифрует все данные. Это не может быть отменено.", - "descriptionEncrypted": "Ваши данные зашифрованы.", - "action": "Зашифровать данные", - "dialog": { - "title": "Зашифровать все данные?", - "description": "Шифрование всех ваших данных обеспечит их безопасность и надежность. Это действие НЕЛЬЗЯ отменить. Вы уверены, что хотите продолжить?" - } - }, - "cache": { - "title": "Очистить кеш", - "description": "Очистите кеш приложения. Это может помочь решить такие проблемы, как незагружаемые изображения или шрифты. Это не повлияет на ваши данные.", - "dialog": { - "title": "Вы уверены?", - "description": "Очистка кеша приведёт к повторной загрузке изображений и шрифтов при загрузке. Это действие не приведет к удалению или изменению ваших данных.", - "successHint": "Кэш очищен!" - } - }, - "data": { - "fixYourData": "Исправить данные", - "fixButton": "Исправить", - "fixYourDataDescription": "Если у вас возникли проблемы с данными, вы можете попробовать исправить их здесь." - } - }, - "shortcutsPage": { - "menuLabel": "Горячие клавиши", - "title": "Горячие клавиши", - "editBindingHint": "Введите новое сочетание клавиш", - "searchHint": "Поиск", - "actions": { - "resetDefault": "Сбросить все" - }, - "errorPage": { - "message": "Не удалось загрузить горячие клавиши: {}", - "howToFix": "Пожалуйста, повторите попытку. Если проблема не исчезнет, обратитесь на GitHub." - }, - "resetDialog": { - "title": "Сбросить горячие клавиши", - "description": "Это сбросит все горячие клавиши к значениям по умолчанию. Вы не сможете отменить это позже. Вы уверены, что хотите продолжить?", - "buttonLabel": "Сбросить" - }, - "conflictDialog": { - "title": "{} сейчас используется", - "descriptionPrefix": "Эта комбинация клавиш сейчас используется ", - "descriptionSuffix": ". Если вы замените это сочетание, оно будет удалено из {}.", - "confirmLabel": "Продолжить" - }, - "editTooltip": "Нажмите, чтобы редактировать сочетание", - "keybindings": { - "toggleToDoList": "Переключить список дел", - "insertNewParagraphInCodeblock": "Вставить новый абзац", - "pasteInCodeblock": "Вставить в кодовый блок", - "selectAllCodeblock": "Выбрать всё", - "indentLineCodeblock": "Вставить два пробела в начале строки", - "outdentLineCodeblock": "Удалить два пробела в начале строки", - "twoSpacesCursorCodeblock": "Вставить два пробела возле курсора", - "copy": "Скопировать выделение", - "paste": "Вставить в контент", - "cut": "Вырезать выделение", - "alignLeft": "Выровнять текст по левому краю", - "alignCenter": "Выровнять текст по центру", - "alignRight": "Выровнять текст по правому краю", - "undo": "Отменить", - "redo": "Вернуться", - "convertToParagraph": "Преобразовать блок в абзац", - "backspace": "Удалить", - "deleteLeftWord": "Удалить левое слово", - "deleteLeftSentence": "Удалить левое предложение", - "delete": "Удалить правый символ", - "deleteMacOS": "Удалить левый символ", - "deleteRightWord": "Удалить правое слово", - "moveCursorLeft": "Переместить курсор влево", - "moveCursorBeginning": "Переместить курсор в начало", - "moveCursorLeftWord": "Переместить курсор на одно слово влево", - "moveCursorLeftSelect": "Выделить и переместить курсор влево", - "moveCursorBeginSelect": "Выделить и переместить курсор в начало", - "moveCursorLeftWordSelect": "Выделить и переместить курсор на одно слово влево", - "moveCursorRight": "Переместить курсор вправо", - "moveCursorEnd": "Переместить курсор в конец", - "moveCursorRightWord": "Переместить курсор на одно слово вправо", - "moveCursorRightSelect": "Выделить и переместить курсор вправо", - "moveCursorEndSelect": "Выделить и переместить курсор в конец", - "moveCursorRightWordSelect": "Выделитб и переместить курсор на одно слово вправо", - "moveCursorUp": "Переместить курсор вверх", - "moveCursorTopSelect": "Выделить и переместить курсор на самый верх", - "moveCursorTop": "Переместить курсор на самый верх", - "moveCursorUpSelect": "Выделить и переместить курсор вверх", - "moveCursorBottomSelect": "Выделить и переместить курсор в самый низ", - "moveCursorBottom": "Переместить курсор в самый низ", - "moveCursorDown": "Переместить курсор вниз", - "moveCursorDownSelect": "Выбрать и переместить курсор вниз", - "home": "Прокрутить на самый верх", - "end": "Прокрутить в самый низ", - "toggleBold": "Переключить жирный шрифт", - "toggleItalic": "Переключить курсив", - "toggleUnderline": "Переключить подчёркивание", - "toggleStrikethrough": "Переключить зачёркивание", - "toggleCode": "Переключить встроенный код", - "toggleHighlight": "Переключить выделение", - "showLinkMenu": "Показать меню ссылок", - "openInlineLink": "Открыть встроенную ссылку", - "openLinks": "Открыть все выбранные ссылки", - "indent": "Отступ", - "outdent": "Обратный отступ", - "exit": "Покинуть редактирование", - "pageUp": "Прокрутить одну страницу вверх", - "pageDown": "Прокрутить одну страницу вниз", - "selectAll": "Выбрать всё", - "pasteWithoutFormatting": "Вставить контент без форматирования", - "showEmojiPicker": "Показать средство выбора эмодзи", - "enterInTableCell": "Добавить перенос строки в таблицу", - "leftInTableCell": "Переместить одну ячейку таблицы влево", - "rightInTableCell": "Переместить одну ячейку таблицы вправо", - "upInTableCell": "Переместить одну ячейку таблицы вверх", - "downInTableCell": "Переместить одну ячейку таблицы вниз", - "tabInTableCell": "Перейти к следующей доступной ячейке таблицы", - "shiftTabInTableCell": "Перейти к ранее доступной ячейке в таблице", - "backSpaceInTableCell": "Остановиться в начале ячейки" - }, - "commands": { - "codeBlockNewParagraph": "Вставить новый абзац рядом с блоком кода", - "codeBlockIndentLines": "Вставить два пробела в начале строки в блоке кода", - "codeBlockOutdentLines": "Удалить два пробела в начале строки в блоке кода", - "codeBlockAddTwoSpaces": "Вставить два пробела в позицию курсора в блоке кода", - "codeBlockSelectAll": "Выбрать всё содержимое внутри блока кода", - "codeBlockPasteText": "Вставить текст в кодовый блок", - "textAlignLeft": "Выровнять текст по левому краю", - "textAlignCenter": "Выровнять текст по центру", - "textAlignRight": "Выровнять текст по правому краю" - }, - "couldNotLoadErrorMsg": "Не удалось загрузить горячие клавиши. Попробуйте ещё раз.", - "couldNotSaveErrorMsg": "Не удалось сохранить горячие клавиши. Попробуйте ещё раз." - }, - "aiPage": { - "title": "Настройки ИИ", - "menuLabel": "Настройки ИИ", - "keys": { - "enableAISearchTitle": "Поиск с ИИ", - "aiSettingsDescription": "Выберите или настройте модели ИИ, используемые в @:appName. Для достижения наилучшей производительности мы рекомендуем использовать параметры модели по умолчанию.", - "loginToEnableAIFeature": "Функции ИИ активируются только после входа в систему с помощью @:appName Cloud. Если у Вас нет аккаунта @:appName, перейдите в раздел «Мой аккаунт», чтобы зарегистрироваться.", - "llmModel": "Языковая модель", - "title": "Ключи API ИИ", - "openAILabel": "Ключ API OpenAI", - "openAITooltip": "Вы можете найти свой секретный ключ API на странице ключа API.", - "openAIHint": "Введите свой ключ API OpenAI", - "stabilityAILabel": "Ключ API Stability", - "stabilityAITooltip": "Ваш ключ API Stability, используемый для аутентификации ваших запросов", - "stabilityAIHint": "Введите свой ключ API Stability" - } - }, - "planPage": { - "menuLabel": "План", - "title": "Тарифный план", - "planUsage": { - "title": "Сводка использования плана", - "storageLabel": "Хранилище", - "storageUsage": "{} из {} ГБ", - "collaboratorsLabel": "Сотрудники", - "collaboratorsUsage": "{} из {}", - "aiResponseLabel": "Ответы ИИ", - "aiResponseUsage": "{} из {}", - "proBadge": "Pro", - "memberProToggle": "Неогр. кол-во участников", - "aiCredit": { - "title": "Добавить кредитов @:appName AI", - "price": "{}", - "priceDescription": "за 1000 кредитов", - "purchase": "Купить ИИ", - "info": "Добавьте 1000 кредитов ИИ на каждое рабочее пространство и легко интегрируйте настраиваемый ИИ в свой рабочий процесс, чтобы получить более разумные и быстрые результаты:", - "infoItemOne": "10 000 ответов на базу данных", - "infoItemTwo": "1000 ответов на рабочее пространство" - }, - "currentPlan": { - "bannerLabel": "Текущий план", - "freeTitle": "Бесплатно", - "proTitle": "Профессиональный", - "teamTitle": "Командный", - "freeInfo": "Идеально подходит для отдельных лиц или небольших команд до 3 человек.", - "proInfo": "Идеально подходит для небольших и средних команд до 10 человек.", - "teamInfo": "Идеально подходит для всех продуктивных и хорошо организованных команд..", - "upgrade": "Сравнить и\n Обновить", - "canceledInfo": "Ваш план отменён, ваш уровень будет понижен до бесплатного плана {}.", - "freeProOne": "Совместное рабочее пространство", - "freeProTwo": "До 3 участников (вкл. владельца)", - "freeProThree": "Неогр. кол-во гостей (только просмотр)", - "freeProFour": "5 ГБ хранилища", - "freeProFive": "История изменений за 30 дней", - "freeConOne": "Гостевые сотрудники (доступ редакт.)", - "freeConTwo": "Неогр. хранилище", - "freeConThree": "История изменений за 6 месяцев", - "professionalProOne": "Совместное рабочее пространство", - "professionalProTwo": "Неогр. кол-во участников", - "professionalProThree": "Неогр. кол-во гостей (только просмотр)", - "professionalProFour": "Неогр. хранилище", - "professionalProFive": "История изменений за 6 месяцев", - "professionalConOne": "Неогр. кол-во приглашенных соавторов (доступ для редактирования)", - "professionalConTwo": "Неогр. кол-во ответов ИИ", - "professionalConThree": "История изменений за 1 год" - }, - "addons": { - "aiMax": { - "price": "{}" - }, - "aiOnDevice": { - "price": "{}" - } - }, - "deal": { - "bannerLabel": "Новогодняя акция!", - "title": "Развивайте свою команду!", - "info": "Обновитесь и сэкономьте 10% на профессиональном и командном планах! Повысьте производительность своего рабочего пространства с помощью новых мощных функций, включая ИИ @:appName.", - "viewPlans": "Посмотреть планы" - }, - "guestCollabToggle": "10 гостевых сотрудников", - "storageUnlimited": "Неогр. хранилище с профессиональным планом" - } - }, - "billingPage": { - "menuLabel": "Выставление счетов", - "title": "Выставление счетов", - "plan": { - "title": "План", - "freeLabel": "Бесплатный", - "proLabel": "Профессиональный", - "planButtonLabel": "Изменить план", - "billingPeriod": "Расчётный период", - "periodButtonLabel": "Изменить период" - }, - "paymentDetails": { - "title": "Детали оплаты", - "methodLabel": "Способ оплаты", - "methodButtonLabel": "Изменить способ" - } - }, - "comparePlanDialog": { - "title": "Сравнить и выбрать план", - "planFeatures": "План\nФункции", - "current": "Текущий", - "actions": { - "upgrade": "Обновиться", - "downgrade": "Понизиться", - "current": "Текущий", - "downgradeDisabledTooltip": "Вы автоматически понизите план в конце платежного цикла." - }, - "freePlan": { - "title": "Бесплатный", - "description": "Для организации каждого уголка вашей работы и жизни.", - "price": "{}", - "priceInfo": "бесплатно навсегда" - }, - "proPlan": { - "title": "Профессиональный", - "description": "Место, где небольшие группы могут планировать и организовываться.", - "price": "{}/месяц", - "priceInfo": "снимается ежегодно" - }, - "planLabels": { - "itemOne": "Рабочие пространства", - "itemTwo": "Участники", - "itemThree": "Гости", - "itemFour": "Гостевые сотрудники", - "itemFive": "Хранилище", - "itemSix": "Сотрудничество в реальном времени", - "itemSeven": "Мобильное приложение", - "tooltipThree": "Гости имеют разрешение только на чтение опубликованного контента.", - "tooltipFour": "Гостевые сотрудники оплачиваются как одно рабочее место.", - "itemEight": "Ответы ИИ", - "tooltipEight": "Навсегда означает, что количество ответов никогда не сбрасывается." - }, - "freeLabels": { - "itemOne": "снимается за рабочее пространство", - "itemTwo": "3", - "itemFour": "0", - "itemFive": "5 ГБ", - "itemSix": "10 пожизненных", - "itemSeven": "да", - "itemEight": "1000 навсегда" - }, - "proLabels": { - "itemOne": "оплата за рабочее пространство", - "itemTwo": "до 10", - "itemFour": "10 гостей оплачиваются как одно место", - "itemFive": "неограничено", - "itemSix": "да", - "itemSeven": "да", - "itemEight": "10 000 ежемесячно" - }, - "paymentSuccess": { - "title": "Теперь Вы пользуетесь планом {}!", - "description": "Ваш платёж успешно обработан, и ваш план повышен до @:appName {}. Вы можете посмотреть информацию о своём плане на странице «План»." - }, - "downgradeDialog": { - "title": "Вы уверены, что хотите понизить свой план?", - "description": "Понижение Вашего плана вернёт Вас обратно к бесплатному плану. Участники могут потерять доступ к рабочим пространствам, и Вам может потребоваться освободить место, чтобы соответствовать ограничениям на хранилище бесплатного плана.", - "downgradeLabel": "Понизить план" - } - }, - "cancelSurveyDialog": { - "title": "Жаль, что вы уходите", - "description": "Нам жаль, что вы уходите. Нам будет приятно, если вы оставите отзыв и поможете улучшить @:appName. Ответьте, пожалуйста, на несколько вопросов.", - "commonOther": "Другое", - "otherHint": "Дайте ответ здесь", - "questionOne": { - "question": "Почему вы решили отменить подписку на @:appName Pro?", - "answerOne": "Высокая стоимость", - "answerTwo": "Функционал не оправдал ожиданий", - "answerThree": "Нашлась алтернатива лучше", - "answerFour": "Не использовал(а) его достаточно, чтобы оправдать стоимость", - "answerFive": "Технические проблемы" - }, - "questionTwo": { - "question": "Насколько вероятно, что вы купите подписку на @:appName Pro в будущем?", - "answerOne": "Очень вероятно", - "answerTwo": "Довольно вероятно", - "answerThree": "Не уверен(а)", - "answerFour": "Маловероятно", - "answerFive": "Очень маловероятно" - }, - "questionThree": { - "question": "Какой функционал Pro версии понравился вам больше всего?", - "answerOne": "Работа с другими пользователями", - "answerTwo": "Более длинная история версий", - "answerThree": "Неограниченный ИИ", - "answerFour": "Доступ к локальным ИИ-моделям" - }, - "questionFour": { - "question": "Как бы вы оценили ваши впечатления от @:appName в целом?", - "answerOne": "Отлично", - "answerTwo": "Хорошо", - "answerThree": "Средне", - "answerFour": "Ниже среднего", - "answerFive": "Неудовлетворительно" - } - }, - "common": { - "reset": "Сбросить" - }, "menu": { "appearance": "Внешний вид", "language": "Язык", @@ -923,22 +285,27 @@ "cloudURL": "Базовый URL", "invalidCloudURLScheme": "Неверный формат URL", "cloudServerType": "Тип облачного сервера", - "cloudServerTypeTip": "Обратите внимание, что после смены облачного сервера может произойти выход из текущего аккаунта.", + "cloudServerTypeTip": "Обратите внимание, что после смены облачного сервера может произойти выход из текущего аккаунта", "cloudLocal": "Локально", - "cloudAppFlowy": "@:appName Cloud Бета", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "URL-адрес Supabase не может быть пустым.", + "cloudSupabaseAnonKey": "Анонимный ключ Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Анонимный ключ не может быть пустым, если URL Supabase не пуст", + "cloudAppFlowy": "@:appName Cloud Beta", "cloudAppFlowySelfHost": "@:appName Cloud на своём сервере", "appFlowyCloudUrlCanNotBeEmpty": "URL облака не может быть пустым.", "clickToCopy": "Нажмите, чтобы скопировать", - "selfHostStart": "Если у Вас нет сервера, пожалуйста, обратитесь к", + "selfHostStart": "Если у вас нет сервера, пожалуйста, обратитесь к", "selfHostContent": "документации", "selfHostEnd": "для получения инструкций по самостоятельному размещению собственного сервера", "cloudURLHint": "Введите базовый URL вашего сервера", "cloudWSURL": "URL вебсокета", "cloudWSURLHint": "Введите адрес вебсокета вашего сервера", "restartApp": "Перезапуск", - "restartAppTip": "Перезапустите приложение, чтобы изменения вступили в силу. Обратите внимание, что это может привести к выходу из текущего аккаунта.", + "restartAppTip": "Перезапустите приложение, чтобы изменения вступили в силу. Обратите внимание, что это может привести к выходу из текущего аккаунта", "changeServerTip": "После смены сервера необходимо нажать кнопку перезагрузки, чтобы изменения вступили в силу.", - "enableEncryptPrompt": "Активировать шифрование для защиты ваших данных с этим секретом. Храните его безопасно; после включения, он не может быть отключён. В случае потери секрета ваши данные будут также потеряны. Нажмите, чтобы скопировать.", + "enableEncryptPrompt": "Активируйте шифрование для защиты ваших данных с этим секретом. Храните его безопасно; после включения, он не может быть отключён. В случае потери секрета ваши данные будут также потеряны. Нажмите, чтобы скопировать", "inputEncryptPrompt": "Пожалуйста, введите ваш секрет шифрования для", "clickToCopySecret": "Нажмите, чтобы скопировать секрет", "configServerSetting": "Настройте параметры вашего сервера", @@ -946,70 +313,27 @@ "inputTextFieldHint": "Ваш секрет", "historicalUserList": "История входа пользователя", "historicalUserListTooltip": "В этом списке отображаются ваши анонимные аккаунты. Вы можете нажать на аккаунт, чтобы посмотреть данные. Анонимный аккаунт создаётся нажатием кнопки «Начать».", - "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт.", + "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт", "customPathPrompt": "Хранение папки данных @:appName в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", "importAppFlowyData": "Импортировать данные из внешней папки @:appName", - "importingAppFlowyDataTip": "Выполняется импорт данных. Пожалуйста, не закрывайте приложение.", + "importingAppFlowyDataTip": "Выполняется импорт данных. Пожалуйста, не закрывайте приложение", "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных @:appName и импортируйте их в текущую папку данных @:appName.", "importSuccess": "Папка данных @:appName успешно импортирована", "importFailed": "Не удалось импортировать папку данных @:appName", - "importGuide": "Для получения более подробной информации, пожалуйста, проверьте указанный документ." + "importGuide": "Для получения более подробной информации, пожалуйста, проверьте указанный документ.", + "supabaseSetting": "Настройка Supabase" }, "notifications": { "enableNotifications": { "label": "Включить уведомления", "hint": "Отключите, чтобы прекратить появление локальных уведомлений." - }, - "showNotificationsIcon": { - "label": "Показать иконку уведомлений", - "hint": "Выключите, чтобы скрыть иконку уведомления на боковой панели." - }, - "archiveNotifications": { - "allSuccess": "Все уведомления были добавлены в архив", - "success": "Уведомление было добавлено в архив" - }, - "markAsReadNotifications": { - "allSuccess": "Отмечено как прочитанное: все", - "success": "Отмечено как прочитанное" - }, - "action": { - "markAsRead": "Отметить как прочитанное", - "archive": "Архивировать" - }, - "settings": { - "settings": "Настройки", - "markAllAsRead": "Отметить все как прочитанные", - "archiveAll": "Архивировать все" - }, - "emptyInbox": { - "title": "Пусто!", - "description": "Ставьте напоминания и получайте уведомления." - }, - "emptyUnread": { - "title": "Нет непрочитанных напоминаний", - "description": "Все напоминания прочитаны!" - }, - "emptyArchived": { - "title": "Архив напоминаний пуст", - "description": "Архивированные напоминания будут здесь. " - }, - "tabs": { - "inbox": "Входящие", - "unread": "Непрочитано", - "archived": "Архивировано" - }, - "refreshSuccess": "Уведомления были обновлены", - "titles": { - "notifications": "Уведомления", - "reminder": "Напоминание" } }, "appearance": { "resetSetting": "Сбросить", "fontFamily": { "label": "Семейство шрифтов", - "search": "Поиск", - "defaultFont": "Системный" + "search": "Поиск" }, "themeMode": { "label": "Тема приложения", @@ -1021,9 +345,6 @@ "documentSettings": { "cursorColor": "Цвет курсора в документе", "selectionColor": "Цвет выделения в документе", - "pickColor": "Выбрать цвет", - "colorShade": "Цветовой оттенок", - "opacity": "Непрозрачность", "hexEmptyError": "HEX-код цвета не может быть пустым", "hexLengthError": "HEX-код цвета должен быть шестизначным", "hexInvalidError": "Неверный HEX-код", @@ -1075,40 +396,7 @@ "twentyFourHour": "24Ч" }, "showNamingDialogWhenCreatingPage": "Показывать диалоговое окно именования при создании страницы", - "enableRTLToolbarItems": "Включить режим панели слева-направо", - "members": { - "title": "Настройки участников", - "inviteMembers": "Пригласить участников", - "inviteHint": "Пригласить по эл. почте", - "sendInvite": "Отправить приглашение", - "copyInviteLink": "Скопировать ссылку-приглашение", - "label": "Участники", - "user": "Пользователь", - "role": "Роль", - "removeFromWorkspace": "Удалить из рабочего пространства", - "owner": "Владелец", - "guest": "Гость", - "member": "Участник", - "memberHintText": "Участник может читать и редактировать страницы.", - "guestHintText": "Гость может читать, реагировать, комментировать и редактировать определенные страницы с разрешением.", - "emailInvalidError": "Неверный адрес эл. почты, пожалуйста, проверьте и повторите попытку.", - "emailSent": "Письмо отправлено, пожалуйста, проверьте входящие.", - "members": "участники", - "membersCount": { - "zero": "{} участников", - "one": "{} участник", - "other": "{} участников" - }, - "memberLimitExceeded": "Вы достигли максимального количества участников, разрешенного для вашей учетной записи. Если вы хотите добавить дополнительных участников для продолжения вашей работы, отправьте запрос на Github.", - "failedToAddMember": "Не удалось добавить участника.", - "addMemberSuccess": "Участник успешно добавлен.", - "removeMember": "Удалить участника", - "areYouSureToRemoveMember": "Вы уверены, что хотите удалить этого участника?", - "inviteMemberSuccess": "Приглашение успешно отправлено.", - "failedToInviteMember": "Не удалось пригласить участника." - }, - "lightLabel": "Светлая", - "darkLabel": "Темная" + "enableRTLToolbarItems": "Включить режим панели слева-направо" }, "files": { "copy": "Копировать", @@ -1137,27 +425,34 @@ "folderPath": "Путь к вашей папке", "locationCannotBeEmpty": "Путь не может быть пустым", "pathCopiedSnackbar": "Путь скопирован в буфер обмена!", - "changeLocationTooltips": "Сменить каталог", + "changeLocationTooltips": "Сменить местоположение", "change": "Изменить", "openLocationTooltips": "Открыть другой каталог", "openCurrentDataFolder": "Открыть текущий каталог данных", - "recoverLocationTooltips": "Сбросить к каталогу по умолчанию", + "recoverLocationTooltips": "Сбросить к местоположению по умолчанию", "exportFileSuccess": "Экспорт завершён!", "exportFileFail": "Не удалось экспортировать!", - "export": "Экспорт", - "clearCache": "Очистить кэш", - "clearCacheDesc": "Если у вас возникли проблемы с загрузкой изображений или некорректным отображением шрифтов, попробуйте очистить кеш. Это действие не приведет к удалению ваших пользовательских данных.", - "areYouSureToClearCache": "Вы уверены, что хотите очистить кеш?", - "clearCacheSuccess": "Кэш успешно очищен!" + "export": "Экспорт" }, "user": { "name": "Имя", "email": "Электронная почта", "tooltipSelectIcon": "Выберите иконку", "selectAnIcon": "Выбрать иконку", - "pleaseInputYourOpenAIKey": "Пожалуйста, введите токен AI", - "clickToLogout": "Нажмите, чтобы выйти из текущего аккаунта", - "pleaseInputYourStabilityAIKey": "Пожалуйста, введите свой токен Stability AI" + "pleaseInputYourOpenAIKey": "Пожалуйста, введите токен OpenAI", + "pleaseInputYourStabilityAIKey": "Пожалуйста, введите свой токен Stability AI", + "clickToLogout": "Нажмите, чтобы выйти из текущего аккаунта" + }, + "shortcuts": { + "shortcutsLabel": "Горячие клавиши", + "command": "Команда", + "keyBinding": "Привязка клавиш", + "addNewCommand": "Добавить новую команду", + "updateShortcutStep": "Нажмите нужную комбинацию клавиш и нажмите Enter.", + "shortcutIsAlreadyUsed": "Это сочетание клавиш уже используется для: {conflict}", + "resetToDefault": "Сбросить к стандартным сочетаниям клавиш", + "couldNotLoadErrorMsg": "Не удалось загрузить горячие клавиши, попробуйте снова", + "couldNotSaveErrorMsg": "Не удалось сохранить горячие клавиши, попробуйте снова" }, "mobile": { "personalInfo": "Личная информация", @@ -1175,22 +470,11 @@ "selectLayout": "Выбрать раскладку", "selectStartingDay": "Выбрать день начала", "version": "Версия" - }, - "shortcuts": { - "shortcutsLabel": "Горячие клавиши", - "command": "Команда", - "keyBinding": "Привязка клавиш", - "addNewCommand": "Добавить новую команду", - "updateShortcutStep": "Нажмите нужную комбинацию клавиш и нажмите Enter.", - "shortcutIsAlreadyUsed": "Это сочетание клавиш уже используется для: {conflict}", - "resetToDefault": "Сбросить к стандартным сочетаниям клавиш", - "couldNotLoadErrorMsg": "Не удалось загрузить горячие клавиши, попробуйте снова", - "couldNotSaveErrorMsg": "Не удалось сохранить горячие клавиши, попробуйте снова" } }, "grid": { - "deleteView": "Вы уверены, что хотите удалить этот вид?", - "createView": "Новый", + "deleteView": "Вы уверены, что хотите удалить это представление?", + "createView": "Новое представление", "title": { "placeholder": "Без названия" }, @@ -1207,19 +491,14 @@ "typeAValue": "Введите значение...", "layout": "Вид", "databaseLayout": "Вид базы данных", - "viewList": { - "zero": "0 просмотров", - "one": "{count} просмотр", - "other": "{count} просмотров" - }, - "editView": "Редактировать вид", + "editView": "Редактировать представление", "boardSettings": "Настройки доски", "calendarSettings": "Настройки календаря", - "createView": "Новый вид", - "duplicateView": "Дублировать вид", - "deleteView": "Удалить вид", + "createView": "Новое представление", + "duplicateView": "Дублировать представление", + "deleteView": "Удалить представление", "numberOfVisibleFields": "{} показано", - "Properties": "Свойства" + "viewList": "Представление базы данных" }, "textFilter": { "contains": "Содержит", @@ -1292,10 +571,8 @@ "insertRight": "Вставить справа", "duplicate": "Дублировать", "delete": "Удалить", - "wrapCellContent": "Обернуть текст", - "clear": "Очистить ячейки", "textFieldName": "Текст", - "checkboxFieldName": "Флажок", + "checkboxFieldName": "Чекбокс", "dateFieldName": "Дата", "updatedAtFieldName": "Последнее изменение", "createdAtFieldName": "Дата создания", @@ -1305,10 +582,6 @@ "urlFieldName": "URL", "checklistFieldName": "To-Do лист", "relationFieldName": "Связь", - "summaryFieldName": "Обзор ИИ", - "timeFieldName": "Время", - "translateFieldName": "ИИ-переводчик", - "translateTo": "Перевести на", "numberFormat": "Формат числа", "dateFormat": "Формат даты", "includeTime": "Время", @@ -1338,7 +611,6 @@ "editProperty": "Редактировать свойство", "newProperty": "Новое свойство", "deleteFieldPromptMessage": "Вы уверены, что хотите удалить?", - "clearFieldPromptMessage": "Вы уверены? Все ячейки в этом столбце будут очищены.", "newColumn": "Новый столбец", "format": "Формат", "reminderOnDateTooltip": "В этой ячейке есть запланированное напоминание", @@ -1356,20 +628,17 @@ "one": "Скрыть {} скрытое поле", "many": "Скрыто {} скрытых полей", "other": "Скрыто {} скрытых поля" - }, - "openAsFullPage": "Открыть на всю страницу", - "moreRowActions": "Доп. действия со строками" + } }, "sort": { "ascending": "По возрастанию", "descending": "По убыванию", "by": "По", "empty": "Нет активных сортировок", - "cannotFindCreatableField": "Не могу найти подходящее поле для сортировки.", + "cannotFindCreatableField": "Не могу найти подходящее поле для сортировки", "deleteAllSorts": "Удалить все сортировки", "addSort": "Добавить сортировку", - "removeSorting": "Убрать сортировку?", - "fieldInUse": "Вы уже сортируете по этому полю" + "removeSorting": "Убрать сортировку?" }, "row": { "duplicate": "Дублировать", @@ -1382,12 +651,9 @@ "action": "Действия", "add": "Нажмите, чтобы добавить ниже", "drag": "Перетащите для перемещения", - "deleteRowPrompt": "Вы уверены, что хотите удалить эту строку? Это действие нельзя отменить.", - "deleteCardPrompt": "Вы уверены, что хотите удалить эту карту? Это действие нельзя отменить.", "dragAndClick": "Перетащите, чтобы переместить; нажмите, чтобы открыть меню", "insertRecordAbove": "Вставить запись выше", - "insertRecordBelow": "Вставить запись ниже", - "noContent": "Без содержания" + "insertRecordBelow": "Вставить запись ниже" }, "selectOption": { "create": "Создать", @@ -1419,21 +685,16 @@ }, "url": { "launch": "Открыть в браузере", - "copy": "Скопировать URL", - "textFieldHint": "Введите URL-адрес" + "copy": "Скопировать URL" }, "relation": { "relatedDatabasePlaceLabel": "Связанная база данных", "relatedDatabasePlaceholder": "Пусто", "inRelatedDatabase": "В", - "rowSearchTextFieldPlaceholder": "Поиск", - "noDatabaseSelected": "База данных не выбрана. Сначала выберите одну из списка ниже:", - "emptySearchResult": "записей не найдено", - "linkedRowListLabel": "{count} связанных строк", - "unlinkedRowListLabel": "Связать ещё одну строку" + "emptySearchResult": "записей не найдено" }, "menuName": "Сетка", - "referencedGridPrefix": "Вид", + "referencedGridPrefix": "Просмотр", "calculate": "Рассчитать", "calculationTypeLabel": { "none": "Пусто", @@ -1441,18 +702,13 @@ "max": "Максимум", "median": "Медиана", "min": "Минимум", - "sum": "Сумма", - "count": "Кол-во", - "countEmpty": "Кол-во пустое", - "countEmptyShort": "ПУСТО", - "countNonEmpty": "Кол-во не пустое", - "countNonEmptyShort": "ЗАПОЛНЕНО" + "sum": "Сумма" } }, "document": { "menuName": "Документ", "date": { - "timeHintTextInTwelveHour": "01:00 ппд.", + "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, "slashMenu": { @@ -1461,7 +717,7 @@ "createANewBoard": "Создать доску" }, "grid": { - "selectAGridToLinkTo": "Выбрать таблицу", + "selectAGridToLinkTo": "Выбрать сетку", "createANewGrid": "Создать сетку" }, "calendar": { @@ -1481,15 +737,15 @@ "referencedGrid": "Связанные сетки", "referencedCalendar": "Связанные календари", "referencedDocument": "Связанные документы", - "autoGeneratorMenuItemName": "AI Генератор", - "autoGeneratorTitleName": "AI: попросить ИИ написать что угодно...", + "autoGeneratorMenuItemName": "OpenAI Генератор", + "autoGeneratorTitleName": "OpenAI: попросить ИИ написать что угодно...", "autoGeneratorLearnMore": "Узнать больше", "autoGeneratorGenerate": "Генерировать", - "autoGeneratorHintText": "Спросить AI ...", - "autoGeneratorCantGetOpenAIKey": "Не могу получить токен AI", + "autoGeneratorHintText": "Спросить OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Не могу получить токен OpenAI", "autoGeneratorRewrite": "Переписать", "smartEdit": "ИИ-ассистенты", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Исправить правописание", "warning": "⚠️ Ответы ИИ могут быть неправильными или неточными.", "smartEditSummarize": "Обобщить", @@ -1498,11 +754,9 @@ "smartEditCouldNotFetchResult": "Не могу получить ответ от OpenAI", "smartEditCouldNotFetchKey": "Не могу получить токен OpenAI", "smartEditDisabled": "OpenAI", - "appflowyAIEditDisabled": "Войдите, чтобы включить функции ИИ.", "discardResponse": "Хотите убрать ответы ИИ?", "createInlineMathEquation": "Создать уравнение", "fonts": "Шрифты", - "insertDate": "Вставить дату", "emoji": "Эмодзи", "toggleList": "Выпадающий список", "quoteList": "Список цитат", @@ -1531,7 +785,7 @@ "couldNotFetchImage": "Не удалось получить изображение", "imageSavingFailed": "Не удалось сохранить изображение", "addIcon": "Добавить иконку", - "changeIcon": "Изменить иконку", + "changeIcon": "Изменить значок", "coverRemoveAlert": "Изображение будет удалено с обложки", "alertDialogConfirmation": "Вы хотите продолжить?" }, @@ -1557,13 +811,9 @@ "depth": "Глубина" }, "image": { - "addAnImage": "Добавить изображение", "copiedToPasteBoard": "Ссылка на изображение скопирована в буфер обмена", - "imageUploadFailed": "Не удалось загрузить изображение.", - "errorCode": "Код ошибки" - }, - "math": { - "copiedToPasteBoard": "Математическое выражение скопировано в буфер обмена." + "addAnImage": "Добавить изображение", + "imageUploadFailed": "Не удалось загрузить изображение." }, "urlPreview": { "copiedToPasteBoard": "Ссылка скопирована в буфер обмена", @@ -1595,17 +845,7 @@ "newDatabase": "Новая база данных", "linkToDatabase": "Связать базу данных" }, - "date": "Дата", - "video": { - "label": "Видео", - "emptyLabel": "Добавить видео", - "placeholder": "Вставьте ссылку на видео", - "copiedToPasteBoard": "Ссылка на видео скопирована в буфер обмена.", - "insertVideo": "Добавить видео", - "invalidVideoUrl": "Исходный URL-адрес пока не поддерживается.", - "invalidVideoUrlYouTube": "YouTube пока не поддерживается.", - "supportedFormats": "Поддерживаемые форматы: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264." - } + "date": "Дата" }, "outlineBlock": { "placeholder": "Оглавление" @@ -1627,8 +867,8 @@ "placeholder": "Введите URL-адрес изображения" }, "ai": { - "label": "Сгенерировать изображение через AI", - "placeholder": "Пожалуйста, введите запрос для AI чтобы сгенерировать изображение" + "label": "Сгенерировать изображение через OpenAI", + "placeholder": "Пожалуйста, введите запрос для OpenAI чтобы сгенерировать изображение" }, "stability_ai": { "label": "Сгенерировать изображение через Stability AI", @@ -1638,9 +878,8 @@ "error": { "invalidImage": "Недопустимое изображение", "invalidImageSize": "Размер изображения должен быть менее 5 МБ.", - "invalidImageFormat": "Формат изображения не поддерживается. Поддерживаемые форматы: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "Недопустимый URL-адрес изображения", - "noImage": "Данный файл или каталог отсутствует" + "invalidImageFormat": "Формат изображения не поддерживается. Поддерживаемые форматы: JPEG, PNG, GIF, SVG", + "invalidImageUrl": "Недопустимый URL-адрес изображения" }, "embedLink": { "label": "Вставить ссылку", @@ -1650,25 +889,21 @@ "label": "Unsplash" }, "searchForAnImage": "Поиск изображения", - "pleaseInputYourOpenAIKey": "пожалуйста, введите свой токен AI на странице настроек", + "pleaseInputYourOpenAIKey": "пожалуйста, введите свой токен OpenAI на странице настроек", + "pleaseInputYourStabilityAIKey": "пожалуйста, введите свой токен Stability AI на странице настроек", "saveImageToGallery": "Сохранить изображение", "failedToAddImageToGallery": "Ошибка добавления изображения в галерею", "successToAddImageToGallery": "Изображение успешно добавлено", "unableToLoadImage": "Ошибка загрузки изображения", "maximumImageSize": "Максимальный поддерживаемый размер загружаемого изображения — 10 МБ.", "uploadImageErrorImageSizeTooBig": "Размер изображения должен быть меньше 10 МБ.", - "imageIsUploading": "Изображение загружается", - "pleaseInputYourStabilityAIKey": "пожалуйста, введите свой токен Stability AI на странице настроек" + "imageIsUploading": "Изображение загружается" }, "codeBlock": { "language": { "label": "Язык", - "placeholder": "Выберите язык", - "auto": "Авто" - }, - "copyTooltip": "Скопировать содержимое блока кода", - "searchLanguageHint": "Поиск языка", - "codeCopiedSnackbar": "Код скопирован в буфер обмена!" + "placeholder": "Выберите язык" + } }, "inlineLink": { "placeholder": "Вставьте или введите ссылку", @@ -1691,20 +926,14 @@ "tooltip": "Нажмите, чтобы открыть страницу" }, "deleted": "Удалено", - "deletedContent": "Этот контент не существует или был удалён." + "deletedContent": "Этот контент не существует или был удален" }, "toolbar": { "resetToDefaultFont": "Восстановить по умолчанию" }, "errorBlock": { "theBlockIsNotSupported": "Текущая версия не поддерживает этот блок.", - "clickToCopyTheBlockContent": "Нажмите, чтобы скопировать содержимое блока.", "blockContentHasBeenCopied": "Содержимое блока скопировано." - }, - "mobilePageSelector": { - "title": "Выбрать страницу", - "failedToLoad": "Не удалось загрузить список страниц.", - "noPagesFound": "Страницы не найдены." } }, "board": { @@ -1713,7 +942,7 @@ "renameGroupTooltip": "Нажмите, чтобы переименовать группу", "createNewColumn": "Создать новую группу", "addToColumnTopTooltip": "Добавить новую карту сверху", - "addToColumnBottomTooltip": "Добавить новую карту в самом низу", + "addToColumnBottomTooltip": "Add a new card at the bottom", "renameColumn": "Переименовать", "hideColumn": "Скрыть", "newGroup": "Новая группа", @@ -1739,27 +968,14 @@ "ungroupedButtonTooltip": "Содержит карточки, которые не принадлежат ни к одной группе.", "ungroupedItemsTitle": "Нажмите, чтобы добавить на доску", "groupBy": "Сгруппировать по", - "groupCondition": "Групповое состояние", - "referencedBoardPrefix": "Вид", + "referencedBoardPrefix": "Просмотр", "notesTooltip": "Заметки", "mobile": { "editURL": "Редактировать URL", "showGroup": "Показать группу", "showGroupContent": "Точно показать эту группу на доске?", "failedToLoad": "Ошибка загрузки доски" - }, - "dateCondition": { - "weekOf": "Неделя {} - {}", - "today": "Сегодня", - "yesterday": "Вчера", - "tomorrow": "Завтра", - "lastSevenDays": "Последние 7 дней", - "nextSevenDays": "Следующие 7 дней", - "lastThirtyDays": "Последние 30 дней", - "nextThirtyDays": "Следующие 30 дней" - }, - "noGroup": "Нет группировки по свойствам", - "noGroupDesc": "Для просмотра видов досок требуется свойство для группировки." + } }, "calendar": { "menuName": "Календарь", @@ -1769,16 +985,10 @@ "today": "Сегодня", "jumpToday": "Перейти к сегодняшнему дню", "previousMonth": "Предыдущий месяц", - "nextMonth": "Следующий месяц", - "views": { - "day": "День", - "week": "Неделя", - "month": "Месяц", - "year": "Год" - } + "nextMonth": "Следующий месяц" }, "mobileEventScreen": { - "emptyTitle": "Событий пока нет", + "emptyTitle": "Мероприятий пока нет", "emptyBody": "Нажмите кнопку «плюс», чтобы создать событие в этот день." }, "settings": { @@ -1789,18 +999,16 @@ "changeLayoutDateField": "Изменить отображение поля", "noDateTitle": "Без даты", "noDateHint": { - "zero": "Здесь будут отображаться незапланированные события.", + "zero": "Здесь будут отображаться незапланированные мероприятия.", "one": "{count} незапланированное событие", - "other": "{count} незапланированных событий" + "other": "{count} незапланированных события" }, - "unscheduledEventsTitle": "Незапланированные мероприятия", + "unscheduledEventsTitle": "Unscheduled events", "clickToAdd": "Нажмите, чтобы добавить в календарь", - "name": "Макет календаря", - "clickToOpen": "Нажмите, чтобы открыть запись." + "name": "Макет календаря" }, "referencedCalendarPrefix": "Вид", - "quickJumpYear": "Перейти к", - "duplicateEvent": "Дублировать событие" + "quickJumpYear": "Перейти к" }, "errorDialog": { "title": "Ошибка приложения", @@ -1870,7 +1078,6 @@ }, "inlineActions": { "noResults": "Нет результатов", - "recentPages": "Последние страницы", "pageReference": "Ссылка на страницу", "docReference": "Ссылка на документ", "boardReference": "Ссылка на доску", @@ -1900,7 +1107,7 @@ "thirtyMinsBefore": "за 30 минут до", "oneHourBefore": "за 1 час до", "twoHoursBefore": "за 2 часа до", - "onDayOfEvent": "В день события", + "onDayOfEvent": "В день мероприятия", "oneDayBefore": "за 1 день до", "twoDaysBefore": "за 2 дня до", "oneWeekBefore": "за 1 неделю до", @@ -1957,22 +1164,21 @@ }, "error": { "weAreSorry": "Мы сожалеем", - "loadingViewError": "У нас возникли проблемы при загрузке этого вида. Пожалуйста, проверьте ваше интернет-соединение, обновите приложение, и не стесняйтесь обратиться к команде, если проблема не исчезнет." + "loadingViewError": "У нас возникли проблемы при загрузке этого представления. Пожалуйста, проверьте ваше интернет-соединение, обновите приложение, и не стесняйтесь обратиться к команде, если проблема не исчезнет." }, "editor": { "bold": "Жирный", "bulletedList": "Маркированный список", "bulletedListShortForm": "Маркированный", - "checkbox": "Флажок", + "checkbox": "Чекбокс", "embedCode": "Встроенный код", - "heading1": "Заголовок 1 уровня", - "heading2": "Заголовок 2 уровня", - "heading3": "Заголовок 3 уровня", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", "highlight": "Выделить", "color": "Цвет", "image": "Изображение", "date": "Дата", - "page": "Страница", "italic": "Курсив", "link": "Ссылка", "numberedList": "Нумерованный список", @@ -2001,8 +1207,6 @@ "backgroundColorPurple": "Фиолетовый фон", "backgroundColorPink": "Розовый фон", "backgroundColorRed": "Красный фон", - "backgroundColorLime": "Лаймовый фон", - "backgroundColorAqua": "Аква фон", "done": "Готово", "cancel": "Отмена", "tint1": "Оттенок 1", @@ -2023,7 +1227,7 @@ "lightLightTint7": "Зелёный", "lightLightTint8": "Бирюзовый", "lightLightTint9": "Синий", - "urlHint": "Ссылка", + "urlHint": "URL", "mobileHeading1": "Заголовок 1", "mobileHeading2": "Заголовок 2", "mobileHeading3": "Заголовок 3", @@ -2050,8 +1254,6 @@ "copy": "Копировать", "paste": "Вставить", "find": "Найти", - "select": "Выбрать", - "selectAll": "Выбрать всё", "previousMatch": "Предыдущее совпадение", "nextMatch": "Следующее совпадение", "closeFind": "Закрыть", @@ -2087,9 +1289,7 @@ }, "favorite": { "noFavorite": "Нет избранных страниц", - "noFavoriteHintText": "Проведите по странице влево, чтобы добавить ее в избранное.", - "removeFromSidebar": "Удалить из боковой панели", - "addToSidebar": "Закрепить на боковой панели" + "noFavoriteHintText": "Проведите по странице влево, чтобы добавить ее в избранное." }, "cardDetails": { "notesPlaceholder": "Введите '/', чтобы вставить блок, или начните писать" @@ -2110,162 +1310,5 @@ "addField": "Добавить поле", "userIcon": "Пользовательская иконка" }, - "noLogFiles": "Нет файлов журналов", - "newSettings": { - "myAccount": { - "title": "Мой аккаунт", - "subtitle": "Настройте свой профиль, управляйте безопасностью аккаунта, открывайте ключи ИИ или войдите в свой аккаунт.", - "profileLabel": "Имя аккаунта и изображение профиля", - "profileNamePlaceholder": "Введите ваше имя", - "accountSecurity": "Безопасность аккаунта", - "2FA": "Двухэтапная аутентификация", - "aiKeys": "Ключи ИИ", - "accountLogin": "Войти в аккаунт", - "updateNameError": "Не удалось обновить имя", - "updateIconError": "Не удалось обновить иконку", - "deleteAccount": { - "title": "Удалить аккаунт", - "subtitle": "Удалить навсегда ваш аккаунт и все ваши данные.", - "deleteMyAccount": "Удалить мой аккаунт", - "dialogTitle": "Удалить аккаунт", - "dialogContent1": "Вы уверены, что хотите навсегда удалить свой аккаунт?", - "dialogContent2": "Это действие невозможно отменить. Оно приведет к закрытию доступа ко всем рабочим пространствам, удалению вашего аккаунта, включая личные рабочие пространства, а также к удалению вас из всех общих рабочих пространств." - } - }, - "workplace": { - "name": "Рабочее место", - "title": "Настройки рабочего места", - "subtitle": "Настройте внешний вид рабочего пространства, тему, шрифт, макет текста, дату, время и язык.", - "workplaceName": "Название рабочего места", - "workplaceNamePlaceholder": "Введите название рабочего места", - "workplaceIcon": "Иконка рабочего места", - "workplaceIconSubtitle": "Загрузите изображение или используйте эмодзи для своего рабочего пространства. Иконка будет отображаться на боковой панели и в уведомлениях.", - "renameError": "Не удалось переименовать рабочее место.", - "updateIconError": "Не удалось обновить иконку", - "chooseAnIcon": "Выберите иконку", - "appearance": { - "name": "Внешний вид", - "themeMode": { - "auto": "Авто", - "light": "Светлый", - "dark": "Тёмный" - }, - "language": "Язык" - } - }, - "syncState": { - "syncing": "Синхронизация", - "synced": "Синхронизировано", - "noNetworkConnected": "Нет подключения к сети" - } - }, - "pageStyle": { - "title": "Стиль страницы", - "layout": "Оформление", - "coverImage": "Обложка", - "pageIcon": "Иконка страницы", - "colors": "Цвета", - "gradient": "Градиент", - "backgroundImage": "Изображение на заднем плане", - "presets": "Пресеты", - "photo": "Фото", - "unsplash": "Unsplash", - "pageCover": "Обложка страницы", - "none": "Ничего", - "openSettings": "Открыть настройки", - "photoPermissionTitle": "@:appName хочет получить доступ к вашей библиотеке фотографий", - "photoPermissionDescription": "Разрешите доступ к библиотеке фотографий для загрузки изображений.", - "doNotAllow": "Не разрешать", - "image": "Изображение" - }, - "commandPalette": { - "placeholder": "Нажмите для поиска...", - "bestMatches": "Лучшие соответствия", - "recentHistory": "Недавняя история", - "navigateHint": "для навигации", - "loadingTooltip": "Мы ищем результаты...", - "betaLabel": "БЕТА", - "betaTooltip": "В настоящее время мы поддерживаем только поиск страниц и содержимого в документах.", - "fromTrashHint": "Из корзины", - "noResultsHint": "Мы не нашли то, что Вы ищете, попробуйте поискать по другому термину.", - "clearSearchTooltip": "Очистить поле поиска" - }, - "space": { - "delete": "Удалить", - "deleteConfirmation": "Удалить: ", - "deleteConfirmationDescription": "Все страницы в этом пространстве будут удалены и перемещены в корзину, а все опубликованные страницы будут отменены.", - "rename": "Переименовать пространство", - "changeIcon": "Изменить иконку", - "manage": "Управление пространством", - "addNewSpace": "Создать пространство", - "collapseAllSubPages": "Свернуть все подстраницы", - "createNewSpace": "Создать новое пространство", - "createSpaceDescription": "Создайте несколько общедоступных и личных пространств, чтобы лучше организовать свою работу.", - "spaceName": "Название пространства", - "permission": "Разрешение", - "publicPermission": "Общедоступное", - "publicPermissionDescription": "Все участники рабочего пространства с полным доступом", - "privatePermission": "Личное", - "privatePermissionDescription": "Только Вы можете получить доступ к этому пространству", - "spaceIconBackground": "Фоновый цвет", - "spaceIcon": "Иконка", - "dangerZone": "Опасная зона", - "unableToDeleteLastSpace": "Невозможно удалить последнее пространство.", - "unableToDeleteSpaceNotCreatedByYou": "Невозможно удалить пространства, созданные другими.", - "enableSpacesForYourWorkspace": "Включить пространства для вашего рабочего пространства", - "title": "Пространства", - "defaultSpaceName": "Общее", - "upgradeSpaceTitle": "Включить пространства", - "upgradeSpaceDescription": "Создайте несколько общедоступных и личных пространств, чтобы лучше организовать свое рабочее пространство.", - "upgrade": "Обновление", - "upgradeYourSpace": "Создать несколько пространств", - "quicklySwitch": "Быстро переключиться на следующее пространство", - "duplicate": "Дублировать пространство", - "movePageToSpace": "Переместить страницу в пространство", - "switchSpace": "Переключить пространство" - }, - "publish": { - "hasNotBeenPublished": "Эта страница ещё не опубликована.", - "reportPage": "Донести на страницу", - "databaseHasNotBeenPublished": "Публикация базы данных пока не поддерживается.", - "createdWith": "Создано с", - "downloadApp": "Скачать AppFlowy", - "copy": { - "codeBlock": "Содержимое блока кода скопировано в буфер обмена.", - "imageBlock": "Ссылка на изображение скопирована в буфер обмена.", - "mathBlock": "Математическое выражение скопировано в буфер обмена." - }, - "containsPublishedPage": "Эта страница содержит одну или несколько опубликованных страниц. Если вы продолжите, они будут отменены из публикации. Вы хотите продолжить удаление?", - "publishSuccessfully": "Опубликовано успешно.", - "unpublishSuccessfully": "Публикация успешно отменена.", - "publishFailed": "Не удалось опубликовать.", - "unpublishFailed": "Не удалось отменить публикацию.", - "noAccessToVisit": "Нет доступа к этой странице...", - "createWithAppFlowy": "Создать веб-сайт с AppFlowy", - "fastWithAI": "Быстро и легко с ИИ.", - "tryItNow": "Попробуй сейчас" - }, - "web": { - "continue": "Продолжить", - "or": "или", - "continueWithGoogle": "Продолжить с Google", - "continueWithGithub": "Продолжить с GitHub", - "continueWithDiscord": "Продолжить с Discord", - "signInAgreement": "Нажимая «Продолжить» выше, вы подтверждаете, что\nвы прочитали, поняли и согласились с\nAppFlowy", - "and": "и", - "termOfUse": "Условия", - "privacyPolicy": "Политика конфиденциальности", - "signInError": "Ошибка входа", - "login": "Зарегистрироваться или войти" - }, - "ai": { - "limitReachedAction": { - "upgrade": "улучшить", - "proPlan": "план Pro", - "aiAddon": "Дополнение ИИ" - }, - "editing": "Редактирование", - "analyzing": "Анализ", - "more": "Более" - } -} + "noLogFiles": "Нет файлов журналов" +} \ No newline at end of file diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 3210aa1f15..50be68350d 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -107,14 +107,14 @@ "questionBubble": { "shortcuts": "Genvägar", "whatsNew": "Vad nytt?", + "help": "Hjälp & Support", "markdown": "Prissänkning", "debug": { "name": "Felsökningsinfo", "success": "Kopierade felsökningsinfo till urklipp!", "fail": "Kunde inte kopiera felsökningsinfo till urklipp" }, - "feedback": "Återkoppling", - "help": "Hjälp & Support" + "feedback": "Återkoppling" }, "menuAppHeader": { "addPageTooltip": "Lägg till en underliggande sida", @@ -247,6 +247,11 @@ "cloudServerType": "Molnserver", "cloudServerTypeTip": "Observera att det kan logga ut ditt nuvarande konto efter att ha bytt molnserver", "cloudLocal": "Lokal", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Webbadressen för supabase kan inte vara tom", + "cloudSupabaseAnonKey": "Supabase anonym nyckel", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Anon-nyckeln kan inte vara tom", "appFlowyCloudUrlCanNotBeEmpty": "Molnets webbadress får inte vara tom", "clickToCopy": "Klicka för att kopiera", "selfHostStart": "Om du inte har en server, vänligen se", @@ -273,7 +278,8 @@ "importAppFlowyDataDescription": "Kopiera data från en extern @:appName-datamapp och importera den till den aktuella @:appName-datamappen", "importSuccess": "@:appName-datamappen har importerats", "importFailed": "Det gick inte att importera @:appName-datamappen", - "importGuide": "För ytterligare information, snälla se det refererade dokumentet" + "importGuide": "För ytterligare information, snälla se det refererade dokumentet", + "supabaseSetting": "Supabase-inställning" }, "appearance": { "fontFamily": { @@ -339,7 +345,7 @@ "user": { "name": "namn", "selectAnIcon": "Välj en ikon", - "pleaseInputYourOpenAIKey": "vänligen ange din AI-nyckel" + "pleaseInputYourOpenAIKey": "vänligen ange din OpenAI-nyckel" } }, "grid": { @@ -495,23 +501,23 @@ "referencedBoard": "Refererad tavla", "referencedGrid": "Refererade tabell", "referencedCalendar": "Refererad kalender", - "autoGeneratorMenuItemName": "AI Writer", - "autoGeneratorTitleName": "AI: Be AI skriva vad som helst...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Be AI skriva vad som helst...", "autoGeneratorLearnMore": "Läs mer", "autoGeneratorGenerate": "Generera", - "autoGeneratorHintText": "Fråga AI...", - "autoGeneratorCantGetOpenAIKey": "Kan inte hämta AI-nyckeln", + "autoGeneratorHintText": "Fråga OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Kan inte hämta OpenAI-nyckeln", "autoGeneratorRewrite": "Skriva om", "smartEdit": "AI-assistenter", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "Fixa stavningen", "warning": "⚠️ AI-svar kan vara felaktiga eller vilseledande.", "smartEditSummarize": "Sammanfatta", "smartEditImproveWriting": "Förbättra skrivandet", "smartEditMakeLonger": "Gör längre", - "smartEditCouldNotFetchResult": "Det gick inte att hämta resultatet från AI", - "smartEditCouldNotFetchKey": "Det gick inte att hämta AI-nyckeln", - "smartEditDisabled": "Anslut AI i Inställningar", + "smartEditCouldNotFetchResult": "Det gick inte att hämta resultatet från OpenAI", + "smartEditCouldNotFetchKey": "Det gick inte att hämta OpenAI-nyckeln", + "smartEditDisabled": "Anslut OpenAI i Inställningar", "discardResponse": "Vill du kassera AI-svaren?", "createInlineMathEquation": "Skapa ekvation", "toggleList": "Växla lista", @@ -662,4 +668,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 78e5462d7f..2986fa6264 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "ฉัน", "welcomeText": "ยินดีต้อนรับเข้าสู่ @:appName", - "welcomeTo": "ยินดีต้อนรับเข้าสู่", "githubStarText": "กดดาวบน GitHub", "subscribeNewsletterText": "สมัครรับจดหมายข่าว", "letsGoButtonText": "เริ่มต้นอย่างรวดเร็ว", "title": "ชื่อ", "youCanAlso": "นอกจากนี้คุณยังสามารถ", "and": "และ", - "failedToOpenUrl": "ไม่สามารถเปิด url ได้: {}", "blockActions": { "addBelowTooltip": "คลิกเพื่อเพิ่มด้านล่าง", "addAboveCmd": "Alt+click", @@ -23,12 +21,12 @@ "title": "ลงทะเบียนเพื่อใช้ @:appName", "getStartedText": "เริ่มต้น", "emptyPasswordError": "รหัสผ่านต้องไม่เว้นว่าง", - "repeatPasswordEmptyError": "ช่องยืนยันรหัสผ่านต้องไม่เว้นว่าง", - "unmatchedPasswordError": "รหัสผ่านที่ยืนยันไม่ตรงกับรหัสผ่าน", + "repeatPasswordEmptyError": "รหัสผ่านซ้ำต้องไม่เว้นว่าง", + "unmatchedPasswordError": "รหัสผ่านซ้ำไม่เท่ากับรหัสผ่าน", "alreadyHaveAnAccount": "มีบัญชีอยู่แล้วหรือไม่?", "emailHint": "อีเมล", "passwordHint": "รหัสผ่าน", - "repeatPasswordHint": "ยืนยันรหัสผ่าน", + "repeatPasswordHint": "รหัสผ่านซ้ำ", "signUpWith": "ลงทะเบียนกับ:" }, "signIn": { @@ -36,106 +34,39 @@ "loginButtonText": "เข้าสู่ระบบ", "loginStartWithAnonymous": "เริ่มต้นด้วยเซสชั่นแบบไม่ระบุตัวตน", "continueAnonymousUser": "ดำเนินการต่อด้วยเซสชันแบบไม่ระบุตัวตน", - "anonymous": "ไม่ระบุตัวตน", "buttonText": "เข้าสู่ระบบ", - "signingInText": "กำลังลงชื่อเข้าใช้...", "forgotPassword": "ลืมรหัสผ่านหรือไม่?", "emailHint": "อีเมล", "passwordHint": "รหัสผ่าน", "dontHaveAnAccount": "ยังไม่มีบัญชีใช่หรือไม่?", - "createAccount": "สร้างบัญชี", - "repeatPasswordEmptyError": "ช่องยืนยันรหัสผ่านต้องไม่เว้นว่าง", - "unmatchedPasswordError": "รหัสผ่านที่ยืนยันไม่ตรงกับรหัสผ่าน", - "syncPromptMessage": "กำลังซิงค์ข้อมูล ซึ่งอาจใช้เวลาสักครู่ กรุณาอย่าปิดหน้านี้", + "repeatPasswordEmptyError": "รหัสผ่านซ้ำต้องไม่เว้นว่าง", + "unmatchedPasswordError": "รหัสผ่านซ้ำไม่เท่ากับรหัสผ่าน", + "syncPromptMessage": "การซิงค์ข้อมูลอาจใช้เวลาสักครู่ กรุณาอย่าปิดหน้านี้", "or": "หรือ", - "signInWithGoogle": "ดำเนินการต่อด้วย Google", - "signInWithGithub": "ดำเนินการต่อด้วย GitHub", - "signInWithDiscord": "ดำเนินการต่อด้วย Discord", - "signInWithApple": "ดำเนินการต่อด้วย Apple", - "continueAnotherWay": "ลองวิธีอื่นเพื่อดำเนินการต่อ", - "signUpWithGoogle": "ลงทะเบียนด้วย Google", - "signUpWithGithub": "ลงทะเบียนด้วย Github", - "signUpWithDiscord": "ลงทะเบียนด้วย Discord", - "signInWith": "ลงชื่อเข้าใช้ด้วย:", - "signInWithEmail": "ดำเนินการต่อด้วยอีเมล์", - "signInWithMagicLink": "ดำเนินการต่อ", - "signUpWithMagicLink": "ลงทะเบียนด้วย Magic Link", - "pleaseInputYourEmail": "กรุณากรอกที่อยู่อีเมล์ของคุณ", - "settings": "การตั้งค่า", - "magicLinkSent": "ส่ง Magic Link แล้ว!", - "invalidEmail": "กรุณากรอกอีเมลที่ถูกต้อง", - "alreadyHaveAnAccount": "มีบัญชีอยู่แล้วใช่ไหม?", - "logIn": "เข้าสู่ระบบ", - "generalError": "มีบางอย่างผิดพลาด โปรดลองอีกครั้งในภายหลัง", - "limitRateError": "ด้วยเหตุผลด้านความปลอดภัย คุณสามารถขอ Magic Link ได้ทุกๆ 60 วินาทีเท่านั้น", - "magicLinkSentDescription": "Magic Link ถูกส่งไปยังอีเมลของคุณแล้ว กรุณาคลิกลิงก์เพื่อเข้าสู่ระบบ ลิงก์จะหมดอายุภายใน 5 นาที", "LogInWithGoogle": "เข้าสู่ระบบด้วย Google", "LogInWithGithub": "เข้าสู่ระบบด้วย Github", - "LogInWithDiscord": "เข้าสู่ระบบด้วย Discord" + "LogInWithDiscord": "เข้าสู่ระบบด้วย Discord", + "signInWith": "ลงชื่อเข้าใช้ด้วย:" }, "workspace": { "chooseWorkspace": "เลือกพื้นที่ทำงานของคุณ", - "defaultName": "พื้นที่ทำงานของฉัน", "create": "สร้างพื้นที่ทำงาน", - "importFromNotion": "นำเข้าจาก Notion", - "learnMore": "เรียนรู้เพิ่มเติม", "reset": "รีเซ็ตพื้นที่ทำงาน", - "renameWorkspace": "เปลี่ยนชื่อพื้นที่ทำงาน", - "workspaceNameCannotBeEmpty": "ชื่อพื้นที่ทำงานไม่สามารถเว้นว่างได้", - "resetWorkspacePrompt": "การรีเซ็ตพื้นที่ทำงานจะลบทุกหน้าและข้อมูลภายในนั้น คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตพื้นที่ทำงาน? หรือคุณสามารถติดต่อทีมสนับสนุนเพื่อกู้คืนพื้นที่ทำงานได้", + "resetWorkspacePrompt": "การรีเซ็ตพื้นที่ทำงานจะลบหน้าและข้อมูลทั้งหมดภายในนั้น คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตพื้นที่ทำงาน หรือคุณสามารถติดต่อทีมสนับสนุนเพื่อกู้คืนพื้นที่ทำงาน", "hint": "พื้นที่ทำงาน", "notFoundError": "ไม่พบพื้นที่ทำงาน", - "failedToLoad": "เกิดข้อผิดพลาด! ไม่สามารถโหลดพื้นที่ทำงานได้ โปรดปิดแอปพลิเคชัน @:appName ที่เปิดอยู่ทั้งหมดแล้วลองอีกครั้ง", + "failedToLoad": "เกิดข้อผิดพลาด! ไม่สามารถโหลดพื้นที่ทำงานได้ โปรดปิดแอปพลิเคชัน AppFlowy ใดๆ ที่เปิดอยู่แล้วลองอีกครั้ง", "errorActions": { "reportIssue": "รายงานปัญหา", - "reportIssueOnGithub": "รายงานปัญหาบน Github", - "exportLogFiles": "ส่งออกไฟล์บันทึก", "reachOut": "ติดต่อได้ที่ Discord" - }, - "menuTitle": "พื้นที่ทำงาน", - "deleteWorkspaceHintText": "คุณแน่ใจหรือไม่ว่าต้องการลบพื้นที่ทำงาน? การกระทำนี้ไม่สามารถย้อนกลับได้ และทุกหน้าที่คุณเผยแพร่จะถูกยกเลิกการเผยแพร่", - "createSuccess": "สร้างพื้นที่ทำงานสำเร็จแล้ว", - "createFailed": "ไม่สามารถสร้างพื้นที่ทำงานได้", - "createLimitExceeded": "คุณถึงขีดจำกัดสูงสุดของพื้นที่ทำงานที่บัญชีของคุณสามารถสร้างได้แล้ว หากคุณต้องการพื้นที่ทำงานเพิ่มเติมเพื่อดำเนินการต่อ โปรดส่งคำขอบน Github", - "deleteSuccess": "ลบพื้นที่ทำงานสำเร็จแล้ว", - "deleteFailed": "ไม่สามารถลบพื้นที่ทำงานได้", - "openSuccess": "เปิดพื้นที่ทำงานได้สำเร็จ", - "openFailed": "ไม่สามารถเปิดพื้นที่ทำงานได้", - "renameSuccess": "เปลี่ยนชื่อพื้นที่ทำงานสำเร็จแล้ว", - "renameFailed": "ไม่สามารถเปลี่ยนชื่อพื้นที่ทำงานได้", - "updateIconSuccess": "อัปเดตไอคอนพื้นที่ทำงานสำเร็จแล้ว", - "updateIconFailed": "ไม่สามารถอัปเดตไอคอนพื้นที่ทำงานได้", - "cannotDeleteTheOnlyWorkspace": "ไม่สามารถลบพื้นที่ทำงานเพียงอย่างเดียวได้", - "fetchWorkspacesFailed": "ไม่สามารถดึงข้อมูลพื้นที่ทำงานได้", - "leaveCurrentWorkspace": "ออกจากพื้นที่ทำงาน", - "leaveCurrentWorkspacePrompt": "คุณแน่ใจหรือไม่ว่าต้องการออกจากพื้นที่ทำงานปัจจุบัน?" + } }, "shareAction": { "buttonText": "แชร์", "workInProgress": "เร็วๆ นี้", "markdown": "Markdown", - "html": "HTML", - "clipboard": "คัดลอกไปยังคลิปบอร์ด", "csv": "CSV", - "copyLink": "คัดลอกลิงค์", - "publishToTheWeb": "เผยแพร่สู่เว็บ", - "publishToTheWebHint": "สร้างเว็บไซต์ด้วย AppFlowy", - "publish": "เผยแพร่", - "unPublish": "ยกเลิกการเผยแพร่", - "visitSite": "เยี่ยมชมเว็บไซต์", - "exportAsTab": "ส่งออกเป็น", - "publishTab": "เผยแพร่", - "shareTab": "แชร์", - "publishOnAppFlowy": "เผยแพร่บน AppFlowy", - "shareTabTitle": "เชิญเพื่อการทำงานร่วมกัน", - "shareTabDescription": "เพื่อการทำงานร่วมกันอย่างง่ายดายกับใครก็ได้", - "copyLinkSuccess": "คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว", - "copyShareLink": "คัดลอกลิงก์แชร์", - "copyLinkFailed": "ไม่สามารถคัดลอกลิงก์ไปยังคลิปบอร์ดได้", - "copyLinkToBlockSuccess": "คัดลอกลิงก์บล็อกไปยังคลิปบอร์ดแล้ว", - "copyLinkToBlockFailed": "ไม่สามารถคัดลอกลิงก์บล็อกไปยังคลิปบอร์ด", - "manageAllSites": "จัดการไซต์ทั้งหมด", - "updatePathName": "อัปเดตชื่อเส้นทาง" + "copyLink": "คัดลอกลิงค์" }, "moreAction": { "small": "เล็ก", @@ -143,18 +74,12 @@ "large": "ใหญ่", "fontSize": "ขนาดตัวอักษร", "import": "นำเข้า", - "moreOptions": "ตัวเลือกเพิ่มเติม", - "wordCount": "จำนวนคำ: {}", - "charCount": "จำนวนตัวอักษร: {}", - "createdAt": "สร้างแล้ว: {}", - "deleteView": "ลบ", - "duplicateView": "ทำสำเนา" + "moreOptions": "ตัวเลือกเพิ่มเติม" }, "importPanel": { "textAndMarkdown": "ข้อความ & Markdown", "documentFromV010": "เอกสารจาก v0.1.0", "databaseFromV010": "ฐานข้อมูลจาก v0.1.0", - "notionZip": "ไฟล์ Zip ที่ส่งออกจาก Notion", "csv": "CSV", "database": "ฐานข้อมูล" }, @@ -167,9 +92,7 @@ "openNewTab": "เปิดในแท็บใหม่", "moveTo": "ย้ายไปยัง", "addToFavorites": "เพิ่มในรายการโปรด", - "copyLink": "คัดลอกลิงค์", - "changeIcon": "เปลี่ยนไอคอน", - "collapseAllPages": "ยุบหน้าย่อยทั้งหมด" + "copyLink": "คัดลอกลิงค์" }, "blankPageTitle": "หน้าเปล่า", "newPageText": "หน้าใหม่", @@ -177,42 +100,9 @@ "newGridText": "ตารางใหม่", "newCalendarText": "ปฏิทินใหม่", "newBoardText": "กระดานใหม่", - "chat": { - "newChat": "แชท AI", - "inputMessageHint": "ถาม @:appName AI", - "inputLocalAIMessageHint": "ถาม @:appName Local AI", - "unsupportedCloudPrompt": "คุณสมบัตินี้ใช้ได้เมื่อใช้ @:appName Cloud เท่านั้น", - "relatedQuestion": "ข้อเสนอแนะ", - "serverUnavailable": "บริการไม่สามารถใช้งานได้ชั่วคราว กรุณาลองใหม่อีกครั้งในภายหลัง", - "aiServerUnavailable": "การเชื่อมต่อขาดหาย กรุณาตรวจสอบอินเทอร์เน็ตของคุณ", - "retry": "ลองใหม่อีกครั้ง", - "clickToRetry": "คลิกเพื่อลองใหม่อีกครั้ง", - "regenerateAnswer": "สร้างใหม่", - "question1": "วิธีการใช้ Kanban เพื่อจัดการงาน", - "question2": "อธิบายวิธี GTD", - "question3": "เหตุใดจึงใช้ Rust", - "question4": "สูตรอาหารจากสิ่งที่มีในครัวของฉัน", - "question5": "สร้างภาพประกอบให้กับหน้าของฉัน", - "question6": "จัดทำรายการสิ่งที่ต้องทำสำหรับสัปดาห์หน้าของฉัน", - "aiMistakePrompt": "AI อาจทำผิดพลาดได้ กรุณาตรวจสอบข้อมูลที่สำคัญ", - "chatWithFilePrompt": "คุณต้องการแชทกับไฟล์หรือไม่?", - "indexFileSuccess": "สร้างดัชนีไฟล์สำเร็จแล้ว", - "inputActionNoPages": "ไม่พบผลลัพธ์จากหน้า", - "referenceSource": { - "zero": "ไม่พบแหล่งข้อมูล 0 รายการ", - "one": "พบแหล่งข้อมูล {count} รายการ", - "other": "พบแหล่งข้อมูล {count} รายการ" - }, - "clickToMention": "อ้างอิงถึงหน้า", - "uploadFile": "แนบไฟล์ PDF, ข้อความ หรือไฟล์ Markdown", - "questionDetail": "สวัสดี {}! วันนี้ฉันสามารถช่วยอะไรคุณได้บ้าง?", - "indexingFile": "การจัดทำดัชนี {}", - "generatingResponse": "กำลังสร้างการตอบกลับ" - }, "trash": { "text": "ขยะ", "restoreAll": "กู้คืนทั้งหมด", - "restore": "กู้คืน", "deleteAll": "ลบทั้งหมด", "pageHeader": { "fileName": "ชื่อไฟล์", @@ -227,37 +117,31 @@ "title": "คุณแน่ใจหรือว่าจะกู้คืนทุกหน้าในถังขยะ", "caption": "การดำเนินการนี้ไม่สามารถยกเลิกได้" }, - "restorePage": { - "title": "กู้คืน: {}", - "caption": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนหน้านี้?" - }, "mobile": { "actions": "การดำเนินการถังขยะ", "empty": "ถังขยะว่างเปล่า", "emptyDescription": "คุณไม่มีไฟล์ที่ถูกลบ", "isDeleted": "ถูกลบแล้ว", "isRestored": "ถูกกู้คืนแล้ว" - }, - "confirmDeleteTitle": "คุณแน่ใจหรือไม่ว่าต้องการลบหน้านี้อย่างถาวร?" + } }, "deletePagePrompt": { "text": "หน้านี้อยู่ในถังขยะ", "restore": "กู้คืนหน้า", - "deletePermanent": "ลบถาวร", - "deletePermanentDescription": "คุณแน่ใจหรือไม่ว่าต้องการลบหน้านี้อย่างถาวร? การกระทำนี้ไม่สามารถย้อนกลับได้" + "deletePermanent": "ลบถาวร" }, "dialogCreatePageNameHint": "ชื่อหน้า", "questionBubble": { "shortcuts": "ทางลัด", "whatsNew": "มีอะไรใหม่?", + "help": "ช่วยเหลือและสนับสนุน", "markdown": "Markdown", "debug": { - "name": "ข้อมูลดีบัก", - "success": "คัดลอกข้อมูลดีบักไปยังคลิปบอร์ดแล้ว!", - "fail": "ไม่สามารถคัดลอกข้อมูลดีบักไปยังคลิปบอร์ด" + "name": "ข้อมูล debug", + "success": "คัดลอกข้อมูล debug ไปยังคลิปบอร์ดแล้ว!", + "fail": "คัดลอกข้อมูล debug ไปยังคลิปบอร์ดไม่ได้" }, - "feedback": "ข้อเสนอแนะ", - "help": "ช่วยเหลือและสนับสนุน" + "feedback": "ข้อเสนอแนะ" }, "menuAppHeader": { "moreButtonToolTip": "ลบ เปลี่ยนชื่อ และอื่นๆ...", @@ -277,7 +161,7 @@ "bulletList": "รายการลำดับหัวข้อย่อย", "checkList": "รายการลำดับ Check", "inlineCode": "อินไลน์โค้ด", - "quote": "บล็อกคำกล่าว", + "quote": "บล็อกอ้างอิง", "header": "หัวข้อ", "highlight": "ไฮไลท์", "color": "สี", @@ -293,58 +177,17 @@ "dragRow": "กดค้างเพื่อเรียงลำดับแถวใหม่", "viewDataBase": "ดูฐานข้อมูล", "referencePage": "{name} ถูกอ้างอิงถึง", - "addBlockBelow": "เพิ่มบล็อกด้านล่าง", - "aiGenerate": "สร้าง" + "addBlockBelow": "เพิ่มบล็อกด้านล่าง" }, "sideBar": { "closeSidebar": "ปิดแถบด้านข้าง", "openSidebar": "เปิดแถบด้านข้าง", "personal": "ส่วนบุคคล", - "private": "ส่วนตัว", - "workspace": "พื้นที่ทำงาน", "favorites": "รายการโปรด", - "clickToHidePrivate": "คลิกเพื่อซ่อนพื้นที่ส่วนตัว\nหน้าที่คุณสร้างที่นี่จะแสดงให้เห็นเฉพาะคุณเท่านั้น", - "clickToHideWorkspace": "คลิกเพื่อซ่อนพื้นที่ทำงาน\nหน้าที่คุณสร้างที่นี่จะแสดงให้สมาชิกทุกคนเห็น", "clickToHidePersonal": "คลิกเพื่อซ่อนส่วนส่วนบุคคล", "clickToHideFavorites": "คลิกเพื่อซ่อนส่วนรายการโปรด", "addAPage": "เพิ่มหน้า", - "addAPageToPrivate": "เพิ่มหน้าไปยังพื้นที่ส่วนตัว", - "addAPageToWorkspace": "เพิ่มหน้าไปยังพื้นที่ทำงาน", - "recent": "ล่าสุด", - "today": "วันนี้", - "thisWeek": "สัปดาห์นี้", - "others": "รายการโปรดก่อนหน้านี้", - "earlier": "ก่อนหน้านี้", - "justNow": "เมื่อสักครู่", - "minutesAgo": "{count} นาทีที่ผ่านมา", - "lastViewed": "ดูล่าสุด", - "favoriteAt": "รายการโปรด", - "emptyRecent": "ไม่มีหน้าล่าสุด", - "emptyRecentDescription": "เมื่อคุณดูหน้าเพจ หน้าเหล่านั้นจะปรากฏที่นี่เพื่อให้คุณสามารถเรียกดูได้ง่าย", - "emptyFavorite": "ไม่มีหน้าโปรด", - "emptyFavoriteDescription": "ทำเครื่องหมายหน้าเป็นรายการโปรด—หน้าเหล่านี้จะแสดงไว้ที่นี่เพื่อการเข้าถึงอย่างรวดเร็ว!", - "removePageFromRecent": "ลบหน้านี้ออกจากรายการล่าสุดหรือไม่?", - "removeSuccess": "ลบสำเร็จ", - "favoriteSpace": "รายการโปรด", - "RecentSpace": "ล่าสุด", - "Spaces": "พื้นที่", - "upgradeToPro": "อัพเกรดเป็น Pro", - "upgradeToAIMax": "ปลดล็อค AI แบบไม่จำกัด", - "storageLimitDialogTitle": "คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว กรุณาอัปเกรดเพื่อปลดล็อกพื้นที่จัดเก็บแบบไม่จำกัด", - "storageLimitDialogTitleIOS": "คุณใช้พื้นที่จัดเก็บฟรีหมดแล้ว", - "aiResponseLimitTitle": "คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว กรุณาอัปเกรดเป็นแผน Pro หรือซื้อส่วนเสริม AI เพื่อปลดล็อกการตอบกลับไม่จำกัด", - "aiResponseLimitDialogTitle": "ถึงขีดจำกัดการตอบกลับ AI แล้ว", - "aiResponseLimit": "คุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว\nไปที่ การตั้งค่า -> แผน -> คลิก AI Max หรือแผน Pro เพื่อรับการตอบกลับ AI เพิ่มเติม", - "askOwnerToUpgradeToPro": "พื้นที่จัดเก็บฟรีในพื้นที่ทำงานของคุณกำลังจะหมด กรุณาขอให้เจ้าของพื้นที่ทำงานของคุณอัปเกรดเป็นแผน Pro", - "askOwnerToUpgradeToProIOS": "พื้นที่จัดเก็บฟรีในพื้นที่ทำงานของคุณกำลังจะหมด", - "askOwnerToUpgradeToAIMax": "พื้นที่ทำงานของคุณใช้จำนวนการตอบกลับ AI ฟรีหมดแล้ว กรุณาขอให้เจ้าของพื้นที่ทำงานของคุณอัปเกรดแผนหรือซื้อส่วนเสริม AI", - "askOwnerToUpgradeToAIMaxIOS": "จำนวนการตอบกลับ AI ฟรี ในพื้นที่ทำงานของคุณใกล้หมดแล้ว", - "purchaseStorageSpace": "ซื้อพื้นที่จัดเก็บข้อมูล", - "singleFileProPlanLimitationDescription": "คุณอัปโหลดไฟล์เกินขนาดสูงสุดที่อนุญาตในแผนฟรี โปรดอัปเกรดเป็นแผน Pro เพื่ออัปโหลดไฟล์ขนาดใหญ่ขึ้น", - "purchaseAIResponse": "ซื้อ ", - "askOwnerToUpgradeToLocalAI": "กรุณาขอให้เจ้าของพื้นที่ทำงานเปิดใช้งาน AI บนอุปกรณ์", - "upgradeToAILocal": "เรียกใช้โมเดลบนอุปกรณ์ของคุณเพื่อความเป็นส่วนตัวสูงสุด", - "upgradeToAILocalDesc": "สนทนากับ PDFs ปรับปรุงการเขียนของคุณ และเติมข้อมูลในตารางอัตโนมัติด้วย AI ภายในเครื่อง" + "recent": "ล่าสุด" }, "notifications": { "export": { @@ -360,7 +203,6 @@ }, "button": { "ok": "ตกลง", - "confirm": "ยืนยัน", "done": "เสร็จแล้ว", "cancel": "ยกเลิก", "signIn": "ลงชื่อเข้าใช้", @@ -378,45 +220,16 @@ "upload": "อัปโหลด", "edit": "แก้ไข", "delete": "ลบ", - "copy": "คัดลอก", "duplicate": "ทำสำเนา", "putback": "นำกลับมา", "update": "อัปโหลด", "share": "แชร์", "removeFromFavorites": "ลบออกจากรายการโปรด", - "removeFromRecent": "ลบออกจากรายการล่าสุด", "addToFavorites": "เพิ่มในรายการโปรด", - "favoriteSuccessfully": "เพิ่มในรายการที่ชื่นชอบสำเร็จ", - "unfavoriteSuccessfully": "นำออกจากรายการที่ชื่นชอบสำเร็จ", - "duplicateSuccessfully": "ทำสำเนาสำเร็จแล้ว", "rename": "เปลี่ยนชื่อ", "helpCenter": "ศูนย์ช่วยเหลือ", "add": "เพิ่ม", - "yes": "ใช่", - "no": "ไม่", - "clear": "ล้าง", - "remove": "ลบ", - "dontRemove": "ไม่ลบ", - "copyLink": "คัดลอกลิงค์", - "align": "จัดตำแหน่ง", - "login": "เข้าสู่ระบบ", - "logout": "ออกจากระบบ", - "deleteAccount": "ลบบัญชี", - "back": "กลับ", - "signInGoogle": "ดำเนินการต่อด้วย Google", - "signInGithub": "ดำเนินการต่อด้วย GitHub", - "signInDiscord": "ดำเนินการต่อด้วย Discord", - "more": "เพิ่มเติม", - "create": "สร้าง", - "close": "ปิด", - "next": "ถัดไป", - "previous": "ก่อนหน้า", - "submit": "ส่ง", - "download": "ดาวน์โหลด", - "backToHome": "กลับสู่หน้าแรก", - "viewing": "การดู", - "editing": "การแก้ไข", - "gotIt": "เข้าใจแล้ว" + "yes": "ใช่" }, "label": { "welcome": "ยินดีต้อนรับ!", @@ -440,591 +253,6 @@ }, "settings": { "title": "การตั้งค่า", - "popupMenuItem": { - "settings": "การตั้งค่า", - "members": "สมาชิก", - "trash": "ถังขยะ", - "helpAndSupport": "ความช่วยเหลือ & การสนับสนุน" - }, - "sites": { - "title": "ไซต์", - "namespaceTitle": "เนมสเปซ", - "namespaceDescription": "จัดการเนมสเปซและโฮมเพจของคุณ", - "namespaceHeader": "เนมสเปซ", - "homepageHeader": "หน้าแรก", - "updateNamespace": "อัปเดตเนมสเปซ", - "removeHomepage": "ลบหน้าแรก", - "selectHomePage": "เลือกหน้า", - "clearHomePage": "ล้างโฮมเพจสำหรับเนมสเปซนี้", - "customUrl": "URL แบบกำหนดเอง", - "namespace": { - "description": "การเปลี่ยนแปลงนี้จะมีผลกับหน้าที่เผยแพร่ทั้งหมดที่เปิดใช้งานในพื้นที่นี้", - "tooltip": "เราขอสงวนสิทธิ์ในการลบเนมสเปซที่ไม่เหมาะสม", - "updateExistingNamespace": "อัปเดตเนมสเปซที่มีอยู่", - "upgradeToPro": "อัปเกรดเป็นแผน Pro เพื่อกำหนดหน้าโฮมเพจ", - "redirectToPayment": "กำลังเปลี่ยนเส้นทางไปยังหน้าชำระเงิน...", - "onlyWorkspaceOwnerCanSetHomePage": "เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถกำหนดหน้าโฮมเพจได้", - "pleaseAskOwnerToSetHomePage": "กรุณาขอให้เจ้าของพื้นที่ทำงานอัปเกรดเป็นแผน Pro" - }, - "publishedPage": { - "title": "หน้าที่เผยแพร่ทั้งหมด", - "description": "จัดการหน้าที่เผยแพร่ของคุณ", - "page": "หน้า", - "pathName": "ชื่อเส้นทาง", - "date": "วันที่เผยแพร่", - "emptyHinText": "คุณไม่มีหน้าที่เผยแพร่ในพื้นที่ทำงานนี้", - "noPublishedPages": "ยังไม่มีหน้าที่เผยแพร่", - "settings": "การตั้งค่าการเผยแพร่", - "clickToOpenPageInApp": "เปิดหน้าในแอป", - "clickToOpenPageInBrowser": "เปิดหน้าในเบราว์เซอร์" - }, - "error": { - "failedToGeneratePaymentLink": "ไม่สามารถสร้างลิงก์การชำระเงินสำหรับแผน Pro ได้", - "failedToUpdateNamespace": "ไม่สามารถอัปเดตเนมสเปซได้", - "proPlanLimitation": "คุณต้องอัปเกรดเป็นแผน Pro เพื่ออัปเดตเนมสเปซ", - "namespaceAlreadyInUse": "เนมสเปซนี้ถูกใช้ไปแล้ว กรุณาลองชื่ออื่น", - "invalidNamespace": "เนมสเปซไม่ถูกต้อง กรุณาลองชื่ออื่น", - "namespaceLengthAtLeast2Characters": "เนมสเปซต้องมีความยาวอย่างน้อย 2 ตัวอักษร", - "onlyWorkspaceOwnerCanUpdateNamespace": "เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถอัปเดตเนมสเปซได้", - "onlyWorkspaceOwnerCanRemoveHomepage": "เฉพาะเจ้าของพื้นที่ทำงานเท่านั้นที่สามารถลบหน้าโฮมเพจได้", - "setHomepageFailed": "ไม่สามารถตั้งค่าหน้าแรกได้", - "namespaceTooLong": "เนมสเปซยาวเกินไป กรุณาลองชื่ออื่น", - "namespaceTooShort": "เนมสเปซสั้นเกินไป กรุณาลองชื่ออื่น", - "namespaceIsReserved": "เนมสเปซนี้ถูกจองไว้แล้ว กรุณาลองชื่ออื่น", - "updatePathNameFailed": "ไม่สามารถอัปเดตชื่อเส้นทางได้", - "removeHomePageFailed": "ไม่สามารถลบหน้าแรกได้", - "publishNameContainsInvalidCharacters": "ชื่อเส้นทางมีอักขระที่ไม่ถูกต้อง โปรดลองอักขระอื่น", - "publishNameTooShort": "ชื่อเส้นทางสั้นเกินไป กรุณาลองชื่ออื่น", - "publishNameTooLong": "ชื่อเส้นทางยาวเกินไป กรุณาลองชื่ออื่น", - "publishNameAlreadyInUse": "ชื่อเส้นทางนี้ถูกใช้ไปแล้ว กรุณาลองชื่ออื่น", - "namespaceContainsInvalidCharacters": "เนมสเปซมีอักขระที่ไม่ถูกต้อง โปรดลองอักขระอื่น", - "publishPermissionDenied": "เฉพาะเจ้าของพื้นที่ทำงานหรือผู้เผยแพร่เพจเท่านั้นที่สามารถจัดการการตั้งค่าการเผยแพร่ได้", - "publishNameCannotBeEmpty": "ชื่อเส้นทางไม่สามารถเว้นว่างได้ กรุณาลองชื่ออื่น" - }, - "success": { - "namespaceUpdated": "อัปเดตเนมสเปซสำเร็จ", - "setHomepageSuccess": "ตั้งค่าหน้าแรกสำเร็จ", - "updatePathNameSuccess": "อัปเดตชื่อเส้นทางสำเร็จ", - "removeHomePageSuccess": "ลบหน้าแรกสำเร็จ" - } - }, - "accountPage": { - "menuLabel": "บัญชีของฉัน", - "title": "บัญชีของฉัน", - "general": { - "title": "ชื่อบัญชีและรูปโปรไฟล์", - "changeProfilePicture": "เปลี่ยนรูปโปรไฟล์" - }, - "email": { - "title": "อีเมล", - "actions": { - "change": "เปลี่ยนอีเมล" - } - }, - "login": { - "title": "การเข้าสู่ระบบบัญชี", - "loginLabel": "เข้าสู่ระบบ", - "logoutLabel": "ออกจากระบบ" - } - }, - "workspacePage": { - "menuLabel": "พื้นที่ทำงาน", - "title": "พื้นที่ทำงาน", - "description": "ปรับแต่งรูปลักษณ์ของพื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ รูปแบบวันที่/เวลา และภาษา", - "workspaceName": { - "title": "ชื่อพื้นที่ทำงาน" - }, - "workspaceIcon": { - "title": "ไอคอนพื้นที่ทำงาน", - "description": "อัปโหลดรูปภาพ หรือใช้อีโมจิสำหรับพื้นที่ทำงานของคุณ ไอคอนจะแสดงในแถบด้านข้าง และการแจ้งเตือนของคุณ" - }, - "appearance": { - "title": "ลักษณะการแสดงผล", - "description": "ปรับแต่งรูปลักษณ์พื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ วันที่ เวลา และภาษาของคุณ", - "options": { - "system": "อัตโนมัติ", - "light": "สว่าง", - "dark": "มืด" - } - }, - "resetCursorColor": { - "title": "รีเซ็ตสีของเคอร์เซอร์ในเอกสาร", - "description": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตสีของเคอร์เซอร์?" - }, - "resetSelectionColor": { - "title": "รีเซ็ตสีการเลือกในเอกสาร", - "description": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตสีการเลือก?" - }, - "resetWidth": { - "resetSuccess": "รีเซ็ตความกว้างของเอกสารสำเร็จ" - }, - "theme": { - "title": "ธีม", - "description": "เลือกธีมที่ตั้งไว้ล่วงหน้าหรืออัปโหลดธีมที่คุณกำหนดเอง", - "uploadCustomThemeTooltip": "อัพโหลดธีมที่กำหนดเอง" - }, - "workspaceFont": { - "title": "ฟอนต์ของพื้นที่ทำงาน", - "noFontHint": "ไม่พบฟอนต์ กรุณาลองคำค้นหาอื่น" - }, - "textDirection": { - "title": "ทิศทางข้อความ", - "leftToRight": "จากซ้ายไปขวา", - "rightToLeft": "จากขวาไปซ้าย", - "auto": "อัตโนมัติ", - "enableRTLItems": "เปิดใช้งานแถบเครื่องมือ RTL (จากขวาไปซ้าย)" - }, - "layoutDirection": { - "title": "ทิศทางการจัดวาง", - "leftToRight": "จากซ้ายไปขวา", - "rightToLeft": "จากขวาไปซ้าย" - }, - "dateTime": { - "title": "วันที่และเวลา", - "example": "{} ที่ {} ({})", - "24HourTime": "เวลาแบบ 24 ชั่วโมง", - "dateFormat": { - "label": "รูปแบบวันที่", - "local": "ท้องถิ่น", - "us": "US", - "iso": "ISO", - "friendly": "ที่เข้าใจง่าย", - "dmy": "D/M/Y" - } - }, - "language": { - "title": "ภาษา" - }, - "deleteWorkspacePrompt": { - "title": "ลบพื้นที่ทำงาน", - "content": "คุณแน่ใจหรือไม่ว่าต้องการลบพื้นที่ทำงานนี้? การดำเนินการนี้ไม่สามารถย้อนกลับได้ และหน้าใดๆ ที่คุณเผยแพร่ไปแล้วจะถูกยกเลิกการเผยแพร่" - }, - "leaveWorkspacePrompt": { - "title": "ออกจากพื้นที่ทำงาน", - "content": "คุณแน่ใจหรือไม่ว่าต้องการออกจากพื้นที่ทำงานนี้? คุณจะสูญเสียการเข้าถึงหน้าและข้อมูลทั้งหมดภายในพื้นที่นี้", - "success": "คุณได้ออกจากพื้นที่ทำงานเรียบร้อยแล้ว", - "fail": "ไม่สามารถออกจากพื้นที่ทำงานได้" - }, - "manageWorkspace": { - "title": "จัดการพื้นที่ทำงาน", - "leaveWorkspace": "ออกจากพื้นที่ทำงาน", - "deleteWorkspace": "ลบพื้นที่ทำงาน" - } - }, - "manageDataPage": { - "menuLabel": "การจัดการข้อมูล", - "title": "การจัดการข้อมูล", - "description": "จัดการพื้นที่จัดเก็บข้อมูลในเครื่องหรือนำเข้าข้อมูลที่มีอยู่ของคุณลงใน @:appName", - "dataStorage": { - "title": "ตำแหน่งจัดเก็บไฟล์", - "tooltip": "ตำแหน่งที่จัดเก็บไฟล์ของคุณ", - "actions": { - "change": "เปลี่ยนเส้นทาง", - "open": "เปิดโฟลเดอร์", - "openTooltip": "เปิดตำแหน่งโฟลเดอร์ข้อมูลปัจจุบัน", - "copy": "คัดลอกเส้นทาง", - "copiedHint": "คัดลอกเส้นทางแล้ว!", - "resetTooltip": "รีเซ็ตเป็นตำแหน่งเริ่มต้น" - }, - "resetDialog": { - "title": "คุณแน่ใจหรือไม่?", - "description": "การรีเซ็ตเส้นทางไปยังตำแหน่งข้อมูลเริ่มต้นจะไม่ลบข้อมูลของคุณ หากคุณต้องการนำเข้าข้อมูลปัจจุบันอีกครั้ง ควรคัดลอกเส้นทางของตำแหน่งปัจจุบันก่อน" - } - }, - "importData": { - "title": "นำเข้าข้อมูล", - "tooltip": "นำเข้าข้อมูลจากการสำรองข้อมูล/โฟลเดอร์ข้อมูล @:appName", - "description": "คัดลอกข้อมูลจากโฟลเดอร์ข้อมูลภายนอกของ @:appName", - "action": "เรียกดูไฟล์" - }, - "encryption": { - "title": "การเข้ารหัส", - "tooltip": "จัดการวิธีการจัดเก็บและเข้ารหัสข้อมูลของคุณ", - "descriptionNoEncryption": "การเปิดใช้งานการเข้ารหัสจะทำให้ข้อมูลทั้งหมดถูกเข้ารหัส การกระทำนี้ไม่สามารถย้อนกลับได้", - "descriptionEncrypted": "ข้อมูลของคุณได้รับการเข้ารหัสแล้ว", - "action": "เข้ารหัสข้อมูล", - "dialog": { - "title": "ต้องการเข้ารหัสข้อมูลทั้งหมดของคุณหรือไม่?", - "description": "การเข้ารหัสข้อมูลทั้งหมดของคุณจะช่วยให้ข้อมูลของคุณปลอดภัยและมั่นคง การดำเนินการนี้ไม่สามารถย้อนกลับได้ คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?" - } - }, - "cache": { - "title": "ล้างแคช", - "description": "ช่วยแก้ไขปัญหาต่างๆ เช่น รูปภาพไม่โหลด หน้าหายไปในช่องว่าง และฟอนต์ไม่โหลด ซึ่งจะไม่กระทบกับข้อมูลของคุณ", - "dialog": { - "title": "ล้างแคช", - "description": "ช่วยแก้ไขปัญหาต่างๆ เช่น รูปภาพไม่โหลด หน้าหายไปในช่องว่าง และฟอนต์ไม่โหลด ซึ่งจะไม่กระทบกับข้อมูลของคุณ", - "successHint": "ล้างแคชแล้ว!" - } - }, - "data": { - "fixYourData": "ซ่อมแซมข้อมูลของคุณ", - "fixButton": "ซ่อมแซม", - "fixYourDataDescription": "หากคุณพบปัญหากับข้อมูลของคุณ คุณสามารถลองแก้ไขได้ที่นี่" - } - }, - "shortcutsPage": { - "menuLabel": "ทางลัด", - "title": "ทางลัด", - "editBindingHint": "ป้อนการผูกปุ่มลัดใหม่", - "searchHint": "ค้นหา", - "actions": { - "resetDefault": "รีเซ็ตค่าเริ่มต้น" - }, - "errorPage": { - "message": "ไม่สามารถโหลดทางลัดได้: {}", - "howToFix": "โปรดลองอีกครั้ง หากปัญหายังคงอยู่ โปรดติดต่อบน GitHub" - }, - "resetDialog": { - "title": "รีเซ็ตทางลัด", - "description": "การดำเนินการนี้จะรีเซ็ตการตั้งค่าปุ่มทั้งหมดของคุณเป็นค่าเริ่มต้น คุณไม่สามารถย้อนกลับได้ในภายหลัง คุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", - "buttonLabel": "รีเซ็ต" - }, - "conflictDialog": { - "title": "{} กำลังใช้งานอยู่ในขณะนี้", - "descriptionPrefix": "การกำหนดปุ่มลัดนี้กำลังถูกใช้งานโดย", - "descriptionSuffix": ". หากคุณแทนที่ปุ่มลัดนี้ ปุ่มลัดนี้จะถูกลบออกจาก {}", - "confirmLabel": "ดำเนินการต่อ" - }, - "editTooltip": "กดเพื่อเริ่มแก้ไขปุ่มลัด", - "keybindings": { - "toggleToDoList": "เปิดปิดรายการสิ่งที่ต้องทำ", - "insertNewParagraphInCodeblock": "แทรกย่อหน้าใหม่", - "pasteInCodeblock": "วางในโค้ดบล็อค", - "selectAllCodeblock": "เลือกทั้งหมด", - "indentLineCodeblock": "แทรกช่องว่างสองช่องที่จุดเริ่มต้นของบรรทัด", - "outdentLineCodeblock": "ลบช่องว่างสองช่องที่จุดเริ่มต้นของบรรทัด", - "twoSpacesCursorCodeblock": "แทรกช่องว่างสองช่องที่เคอร์เซอร์", - "copy": "คัดลอกการเลือก", - "paste": "วางในเนื้อหา", - "cut": "ตัดการเลือก", - "alignLeft": "จัดข้อความให้ชิดซ้าย", - "alignCenter": "จัดตำแหน่งข้อความให้อยู่กึ่งกลาง", - "alignRight": "จัดข้อความให้ชิดขวา", - "undo": "เลิกทำ", - "redo": "ทำซ้ำ", - "convertToParagraph": "แปลงบล็อกเป็นย่อหน้า", - "backspace": "ลบ", - "deleteLeftWord": "ลบคำซ้าย", - "deleteLeftSentence": "ลบประโยคซ้าย", - "delete": "ลบตัวอักษรขวา", - "deleteMacOS": "ลบตัวอักษรซ้าย", - "deleteRightWord": "ลบคำขวา", - "moveCursorLeft": "เลื่อนเคอร์เซอร์ไปทางซ้าย", - "moveCursorBeginning": "เลื่อนเคอร์เซอร์ไปที่จุดเริ่มต้น", - "moveCursorLeftWord": "เลื่อนเคอร์เซอร์ไปทางซ้ายหนึ่งคำ", - "moveCursorLeftSelect": "เลือกและเลื่อนเคอร์เซอร์ไปทางซ้าย", - "moveCursorBeginSelect": "เลือกและเลื่อนเคอร์เซอร์ไปที่จุดเริ่มต้น", - "moveCursorLeftWordSelect": "เลือกและเลื่อนเคอร์เซอร์ไปทางซ้ายหนึ่งคำ", - "moveCursorRight": "เลื่อนเคอร์เซอร์ไปทางขวา", - "moveCursorEnd": "เลื่อนเคอร์เซอร์ไปที่ท้ายสุด", - "moveCursorRightWord": "เลื่อนเคอร์เซอร์ไปทางขวาหนึ่งคำ", - "moveCursorRightSelect": "เลือกและเลื่อนเคอร์เซอร์ไปทางขวาหนึ่งช่อง", - "moveCursorEndSelect": "เลือกและเลื่อนเคอร์เซอร์ไปที่จุดสิ้นสุด", - "moveCursorRightWordSelect": "เลือกและเลื่อนเคอร์เซอร์ไปทางขวาหนึ่งคำ", - "moveCursorUp": "เลื่อนเคอร์เซอร์ขึ้น", - "moveCursorTopSelect": "เลือกและเลื่อนเคอร์เซอร์ไปด้านบน", - "moveCursorTop": "เลื่อนเคอร์เซอร์ไปด้านบน", - "moveCursorUpSelect": "เลือกและเลื่อนเคอร์เซอร์ขึ้น", - "moveCursorBottomSelect": "เลือกและเลื่อนเคอร์เซอร์ไปที่ด้านล่าง", - "moveCursorBottom": "เลื่อนเคอร์เซอร์ไปที่ด้านล่าง", - "moveCursorDown": "เลื่อนเคอร์เซอร์ลง", - "moveCursorDownSelect": "เลือกและเลื่อนเคอร์เซอร์ลง", - "home": "เลื่อนไปด้านบน", - "end": "เลื่อนไปด้านล่าง", - "toggleBold": "เปิดปิดตัวหนา", - "toggleItalic": "เปิดปิดตัวเอียง", - "toggleUnderline": "เปิดปิดขีดเส้นใต้", - "toggleStrikethrough": "เปิดปิดการขีดฆ่า", - "toggleCode": "เปิดปิดโค้ดในบรรทัด", - "toggleHighlight": "เปิดปิดไฮไลท์", - "showLinkMenu": "แสดงลิงค์เมนู", - "openInlineLink": "เปิดลิงก์ในบรรทัด", - "openLinks": "เปิดลิงก์ที่เลือกทั้งหมด", - "indent": "เยื้อง", - "outdent": "เยื้องออก", - "exit": "ออกจากการแก้ไข", - "pageUp": "เลื่อนขึ้นไปหนึ่งหน้า", - "pageDown": "เลื่อนลงมาหนึ่งหน้า", - "selectAll": "เลือกทั้งหมด", - "pasteWithoutFormatting": "วางเนื้อหาที่ไม่มีการจัดรูปแบบ", - "showEmojiPicker": "แสดงตัวเลือกอีโมจิ", - "enterInTableCell": "เพิ่มการแบ่งบรรทัดในตาราง", - "leftInTableCell": "เลื่อนไปทางซ้ายหนึ่งเซลล์ในตาราง", - "rightInTableCell": "เลื่อนไปทางขวาหนึ่งเซลล์ในตาราง", - "upInTableCell": "เลื่อนขึ้นหนึ่งเซลล์ในตาราง", - "downInTableCell": "เลื่อนลงหนึ่งเซลล์ในตาราง", - "tabInTableCell": "ไปยังเซลล์ถัดไปที่ว่างในตาราง", - "shiftTabInTableCell": "ไปยังเซลล์ก่อนหน้าที่ว่างในตาราง", - "backSpaceInTableCell": "หยุดที่จุดเริ่มต้นของเซลล์" - }, - "commands": { - "codeBlockNewParagraph": "แทรกย่อหน้าใหม่ถัดจากโค้ดบล็อก", - "codeBlockIndentLines": "แทรกช่องว่างสองช่องที่จุดเริ่มต้นบรรทัดในโค้ดบล็อก", - "codeBlockOutdentLines": "ลบช่องว่างสองช่องที่จุดเริ่มต้นบรรทัดในโค้ดบล็อค", - "codeBlockAddTwoSpaces": "แทรกช่องว่างสองช่องที่ตำแหน่งเคอร์เซอร์ในโค้ดบล็อก", - "codeBlockSelectAll": "เลือกเนื้อหาทั้งหมดภายในโค้ดบล็อก", - "codeBlockPasteText": "วางข้อความลงในโค้ดบล็อค", - "textAlignLeft": "จัดตำแหน่งข้อความให้ชิดซ้าย", - "textAlignCenter": "จัดตำแหน่งข้อความให้อยู่กึ่งกลาง", - "textAlignRight": "จัดตำแหน่งข้อความให้ชิดขวา" - }, - "couldNotLoadErrorMsg": "ไม่สามารถโหลดทางลัดได้ ลองอีกครั้ง", - "couldNotSaveErrorMsg": "ไม่สามารถบันทึกทางลัดได้ โปรดลองอีกครั้ง" - }, - "aiPage": { - "title": "การตั้งค่า AI", - "menuLabel": "การตั้งค่า AI", - "keys": { - "enableAISearchTitle": "การค้นหาด้วย AI", - "aiSettingsDescription": "เลือกโมเดลที่คุณต้องการใช้เพื่อขับเคลื่อน AppFlowy AI ขณะนี้มี GPT-4, Claude 3.5, Llama 3.1, และ Mistral 7B", - "loginToEnableAIFeature": "ฟีเจอร์ AI จะเปิดใช้งานได้หลังจากเข้าสู่ระบบด้วย @:appName Cloud เท่านั้น หากคุณไม่มีบัญชี @:appName ให้ไปที่ 'บัญชีของฉัน' เพื่อลงทะเบียน", - "llmModel": "โมเดลภาษา", - "llmModelType": "ประเภทของโมเดลภาษา", - "downloadLLMPrompt": "ดาวน์โหลด {}", - "downloadAppFlowyOfflineAI": "การดาวน์โหลดแพ็กเกจ AI แบบออฟไลน์จะทำให้ AI สามารถทำงานบนอุปกรณ์ของคุณได้ คุณต้องการดำเนินการต่อหรือไม่?", - "downloadLLMPromptDetail": "การดาวน์โหลดโมเดล {} ในเครื่องของคุณ จะใช้พื้นที่เก็บข้อมูลสูงสุด {} คุณต้องการดำเนินการต่อหรือไม่", - "downloadBigFilePrompt": "การดาวน์โหลดอาจใช้เวลาประมาณ 10 นาทีให้เสร็จสิ้น", - "downloadAIModelButton": "ดาวน์โหลด", - "downloadingModel": "กำลังดาวน์โหลด", - "localAILoaded": "เพิ่มโมเดล AI ในเครื่องสำเร็จ และพร้อมใช้งานแล้ว", - "localAIStart": "การแชท Local AI กำลังเริ่มต้น...", - "localAILoading": "กำลังโหลดโมเดลแชท Local AI...", - "localAIStopped": "Local AI หยุดทำงานแล้ว", - "failToLoadLocalAI": "ไม่สามารถเริ่มต้น Local AI ได้", - "restartLocalAI": "เริ่มต้น Local AI ใหม่", - "disableLocalAITitle": "ปิดการใช้งาน Local AI", - "disableLocalAIDescription": "คุณต้องการปิดการใช้งาน Local AI หรือไม่?", - "localAIToggleTitle": "สลับเพื่อเปิดหรือปิดใช้งาน Local AI", - "offlineAIInstruction1": "ติดตาม", - "offlineAIInstruction2": "คำแนะนำ", - "offlineAIInstruction3": "เพื่อเปิดใช้งาน AI แบบออฟไลน์", - "offlineAIDownload1": "หากคุณยังไม่ได้ดาวน์โหลด AppFlowy AI, กรุณา", - "offlineAIDownload2": "ดาวน์โหลด", - "offlineAIDownload3": "เป็นอันดับแรก", - "activeOfflineAI": "ใช้งานอยู่", - "downloadOfflineAI": "ดาวน์โหลด", - "openModelDirectory": "เปิดโฟลเดอร์" - } - }, - "planPage": { - "menuLabel": "แผน", - "title": "แผนราคา", - "planUsage": { - "title": "สรุปการใช้งานแผน", - "storageLabel": "พื้นที่จัดเก็บ", - "storageUsage": "{} จาก {} GB", - "unlimitedStorageLabel": "พื้นที่เก็บข้อมูลไม่จำกัด", - "collaboratorsLabel": "สมาชิก", - "collaboratorsUsage": "{} จาก {}", - "aiResponseLabel": "การตอบกลับของ AI", - "aiResponseUsage": "{} จาก {}", - "unlimitedAILabel": "การตอบกลับแบบไม่จำกัด", - "proBadge": "Pro", - "aiMaxBadge": "AI Max", - "aiOnDeviceBadge": "AI บนอุปกรณ์สำหรับ Mac", - "memberProToggle": "สมาชิกมากขึ้น และ AI ไม่จำกัด", - "aiMaxToggle": "AI ไม่จำกัด และการเข้าถึงโมเดลขั้นสูง", - "aiOnDeviceToggle": "Local AI เพื่อความเป็นส่วนตัวสูงสุด", - "aiCredit": { - "title": "เพิ่ม @:appName AI เครดิต", - "price": "{}", - "priceDescription": "สำหรับ 1,000 เครดิต", - "purchase": "ซื้อ AI", - "info": "เพิ่มเครดิต AI 1,000 เครดิตต่อพื้นที่ทำงาน และผสานรวม AI ที่สามารถปรับแต่งได้ เข้าไปในกระบวนการทำงานของคุณได้อย่างราบรื่น เพื่อผลลัพธ์ที่ชาญฉลาด และรวดเร็วยิ่งขึ้นด้วย สูงสุดถึง:", - "infoItemOne": "10,000 การตอบกลับต่อฐานข้อมูล", - "infoItemTwo": "1,000 การตอบกลับต่อพื้นที่ทำงาน" - }, - "currentPlan": { - "bannerLabel": "แผนปัจจุบัน", - "freeTitle": "Free", - "proTitle": "Pro", - "teamTitle": "Team", - "freeInfo": "เหมาะสำหรับบุคคลที่มีสมาชิกสูงสุด 2 คน เพื่อจัดระเบียบทุกอย่าง", - "proInfo": "เหมาะสำหรับทีมขนาดเล็ก และขนาดกลางที่มีสมาชิกไม่เกิน 10 คน", - "teamInfo": "เหมาะสำหรับทีมที่มีประสิทธิภาพ และการจัดระเบียบที่ดี", - "upgrade": "เปลี่ยนแผน", - "canceledInfo": "แผนของคุณถูกยกเลิก คุณจะถูกปรับลดเป็นแผน Free ในวันที่ {}" - }, - "addons": { - "title": "ส่วนเสริม", - "addLabel": "เพิ่ม", - "activeLabel": "เพิ่มแล้ว", - "aiMax": { - "title": "AI Max", - "description": "การตอบกลับ AI แบบไม่จำกัด ที่ขับเคลื่อนโดย GPT-4o, Claude 3.5 Sonnet และอื่นๆ", - "price": "{}", - "priceInfo": "ต่อผู้ใช้ ต่อเดือน เก็บค่าบริการเป็นรายปี" - }, - "aiOnDevice": { - "title": "AI บนอุปกรณ์สำหรับ Mac", - "description": "เรียกใช้ Mistral 7B, LLAMA 3 และโมเดล local อื่น ๆ บนเครื่องของคุณ", - "price": "{}", - "priceInfo": "ต่อผู้ใช้ ต่อเดือน เก็บค่าบริการเป็นรายปี", - "recommend": "แนะนำ M1 หรือใหม่กว่า" - } - }, - "deal": { - "bannerLabel": "ดีลปีใหม่!", - "title": "ขยายทีมของคุณ!", - "info": "อัปเกรด และรับส่วนลด 10% สำหรับแผน Pro และ Team! เพิ่มประสิทธิภาพการทำงานในพื้นที่ทำงานของคุณด้วยฟีเจอร์ใหม่อันทรงพลัง รวมถึง @:appName AI", - "viewPlans": "ดูแผน" - } - } - }, - "billingPage": { - "menuLabel": "การเรียกเก็บเงิน", - "title": "การเรียกเก็บเงิน", - "plan": { - "title": "แผน", - "freeLabel": "Free", - "proLabel": "Pro", - "planButtonLabel": "เปลี่ยนแผน", - "billingPeriod": "รอบบิล", - "periodButtonLabel": "แก้ไขช่วงเวลา" - }, - "paymentDetails": { - "title": "รายละเอียดการชำระเงิน", - "methodLabel": "วิธีการชำระเงิน", - "methodButtonLabel": "แก้ไขวิธีการ" - }, - "addons": { - "title": "ส่วนเสริม", - "addLabel": "เพิ่ม", - "removeLabel": "ลบ", - "renewLabel": "ต่ออายุ", - "aiMax": { - "label": "AI Max", - "description": "ปลดล็อค AI และโมเดลขั้นสูงแบบไม่จำกัด", - "activeDescription": "ใบแจ้งหนี้ถัดไปจะครบกำหนดในวันที่ {}", - "canceledDescription": "AI Max จะพร้อมใช้งานจนถึงวันที่ {}" - }, - "aiOnDevice": { - "label": "AI บนอุปกรณ์สำหรับ Mac", - "description": "ปลดล็อค AI ไม่จำกัดบนอุปกรณ์ของคุณ", - "activeDescription": "ใบแจ้งหนี้ถัดไปจะครบกำหนดในวันที่ {}", - "canceledDescription": "AI บนอุปกรณ์สำหรับ Mac จะพร้อมใช้งานจนถึงวันที่ {}" - }, - "removeDialog": { - "title": "ลบ {}", - "description": "คุณแน่ใจหรือไม่ว่าต้องการลบ {plan}? คุณจะสูญเสียการเข้าถึงฟีเจอร์ และสิทธิประโยชน์ของ {plan} ทันที" - } - }, - "currentPeriodBadge": "ปัจจุบัน", - "changePeriod": "การเปลี่ยนแปลงช่วงเวลา", - "planPeriod": "{} รอบ", - "monthlyInterval": "รายเดือน", - "monthlyPriceInfo": "คิดค่าบริการต่อที่นั่งแบบรายเดือน", - "annualInterval": "รายปี", - "annualPriceInfo": "คิดค่าบริการต่อที่นั่งแบบรายปี" - }, - "comparePlanDialog": { - "title": "เปรียบเทียบและเลือกแผน", - "planFeatures": "แผน\nฟีเจอร์", - "current": "ปัจจุบัน", - "actions": { - "upgrade": "อัพเกรด", - "downgrade": "ลดระดับ", - "current": "ปัจจุบัน" - }, - "freePlan": { - "title": "Free", - "description": "สำหรับบุคคลตั้งแต่ 2 คนขึ้นไป เพื่อจัดระเบียบทุกอย่าง", - "price": "{}", - "priceInfo": "ฟรีตลอดไป" - }, - "proPlan": { - "title": "Pro", - "description": "สำหรับทีมขนาดเล็กเพื่อจัดการโครงการและความรู้ของทีม", - "price": "{}", - "priceInfo": "ต่อผู้ใช้ ต่อเดือน\nเรียกเก็บค่าบริการรายปี\n{} เรียกเก็บค่าบริการรายเดือน" - }, - "planLabels": { - "itemOne": "พื้นที่ทำงาน", - "itemTwo": "สมาชิก", - "itemThree": "พื้นที่จัดเก็บ", - "itemFour": "การทำงานร่วมกันแบบเรียลไทม์", - "itemFive": "แอพมือถือ", - "itemSix": "การตอบกลับของ AI", - "itemFileUpload": "การอัพโหลดไฟล์", - "customNamespace": "เนมสเปซที่กำหนดเอง", - "tooltipSix": "ตลอดอายุการใช้งาน หมายถึง จำนวนการตอบกลับที่ไม่รีเซ็ต", - "intelligentSearch": "การค้นหาอัจฉริยะ", - "tooltipSeven": "อนุญาตให้คุณปรับแต่งส่วนหนึ่งของ URL สำหรับพื้นที่ทำงานของคุณ", - "customNamespaceTooltip": "URL ของไซต์ที่เผยแพร่แบบกำหนดเอง" - }, - "freeLabels": { - "itemOne": "คิดค่าบริการตามพื้นที่ทำงาน", - "itemTwo": "สูงสุด 2", - "itemThree": "5 GB", - "itemFour": "ใช่", - "itemFive": "ใช่", - "itemSix": "10 ตลอดอายุการใช้งาน", - "itemFileUpload": "ไม่เกิน 7 MB", - "intelligentSearch": "การค้นหาอัจฉริยะ" - }, - "proLabels": { - "itemOne": "คิดค่าบริการตามพื้นที่ทำงาน", - "itemTwo": "สูงถึง 10", - "itemThree": "ไม่จำกัด", - "itemFour": "ใช่", - "itemFive": "ใช่", - "itemSix": "ไม่จำกัด", - "itemFileUpload": "ไม่จำกัด", - "intelligentSearch": "การค้นหาอัจฉริยะ" - }, - "paymentSuccess": { - "title": "ตอนนี้คุณอยู่ในแผน {} แล้ว!", - "description": "การชำระเงินของคุณได้รับการดำเนินการเรียบร้อย แล้วและแผนของคุณได้รับการอัปเกรดเป็น @:appName {} คุณสามารถดูรายละเอียดแผนของคุณได้ในหน้าแผน" - }, - "downgradeDialog": { - "title": "คุณแน่ใจหรือไม่ว่าต้องการลดระดับแผนของคุณ?", - "description": "การลดระดับแผนของคุณจะทำให้คุณกลับไปใช้แผนฟรี สมาชิกอาจสูญเสียสิทธิ์การเข้าถึงพื้นที่ทำงานนี้ และคุณอาจต้องเพิ่มพื้นที่ว่างเพื่อให้ตรงตามขีดจำกัดพื้นที่เก็บข้อมูลของแผนฟรี", - "downgradeLabel": "การลดระดับแผน" - } - }, - "cancelSurveyDialog": { - "title": "เสียใจที่เห็นคุณไป", - "description": "เราเสียใจที่เห็นคุณไป เรายินดีรับฟังคำติชมของคุณเพื่อช่วยเราปรับปรุง @:appName โปรดใช้เวลาสักครู่เพื่อตอบคำถามสองสามข้อ", - "commonOther": "อื่นๆ", - "otherHint": "เขียนคำตอบของคุณที่นี่", - "questionOne": { - "question": "อะไรเป็นเหตุผลที่ทำให้คุณยกเลิกการสมัครสมาชิก @:appName Pro?", - "answerOne": "ราคาสูงเกินไป", - "answerTwo": "ฟีเจอร์ไม่เป็นไปตามที่คาดไว้", - "answerThree": "พบทางเลือกที่ดีกว่า", - "answerFour": "ใช้ไม่มากพอที่จะคุ้มกับค่าใช้จ่าย", - "answerFive": "ปัญหาด้านการบริการหรือความยุ่งยากทางเทคนิค" - }, - "questionTwo": { - "question": "คุณมีความเป็นไปได้มากแค่ไหนที่จะพิจารณากลับไปสมัครสมาชิก @:appName Pro ในอนาคต?", - "answerOne": "เป็นไปได้มาก", - "answerTwo": "ค่อนข้างจะเป็นไปได้", - "answerThree": "ไม่แน่ใจ", - "answerFour": "ไม่น่าจะเป็นไปได้", - "answerFive": "ไม่น่าจะเป็นไปได้มาก" - }, - "questionThree": { - "question": "ฟีเจอร์ Pro อะไรที่คุณคิดว่าคุ้มค่าที่สุดในระหว่างที่ใช้บริการ?", - "answerOne": "การทำงานร่วมกันระหว่างผู้ใช้หลายราย", - "answerTwo": "ประวัติเวอร์ชันที่ยาวนานกว่า", - "answerThree": "การตอบกลับของ AI ไม่จำกัด", - "answerFour": "การเข้าถึงโมเดล AI ในเครื่อง" - }, - "questionFour": { - "question": "คุณคิดว่าประสบการณ์โดยรวมกับ @:appName เป็นอย่างไร?", - "answerOne": "ยอดเยี่ยม", - "answerTwo": "ดี", - "answerThree": "ปานกลาง", - "answerFour": "ต่ำกว่าค่าเฉลี่ย", - "answerFive": "ไม่พึงพอใจ" - } - }, - "common": { - "uploadingFile": "กำลังอัพโหลดไฟล์ กรุณาอย่าออกจากแอป", - "uploadNotionSuccess": "ไฟล์ zip ของ Notion ของคุณได้รับการอัปโหลดเรียบร้อยแล้ว เมื่อการนำเข้าเสร็จสมบูรณ์ คุณจะได้รับอีเมลยืนยัน", - "reset": "รีเซ็ต" - }, "menu": { "appearance": "รูปลักษณ์", "language": "ภาษา", @@ -1044,21 +272,20 @@ "cloudServerType": "เซิร์ฟเวอร์คลาวด์", "cloudServerTypeTip": "โปรดทราบว่าอาจมีการออกจากระบบบัญชีปัจจุบันของคุณหลังจากเปลี่ยนเซิร์ฟเวอร์คลาวด์", "cloudLocal": "ในเครื่อง", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL ของ Supabase", + "cloudSupabaseAnonKey": "คีย์ไม่ระบุชื่อของ Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "หากมีการระบุ URL ของ Supabase คีย์ไม่ระบุชื่อจะต้องไม่ว่างเปล่า", "cloudAppFlowy": "คลาวด์ของ AppFlowy", - "cloudAppFlowySelfHost": "@:appName คลาวด์ที่โฮสต์เอง", - "appFlowyCloudUrlCanNotBeEmpty": "URL ของคลาวด์ไม่สามารถเว้นว่างได้", "clickToCopy": "คลิกเพื่อคัดลอก", "selfHostStart": "หากคุณไม่มีเซิร์ฟเวอร์ โปรดดูที่", "selfHostContent": "เอกสาร", "selfHostEnd": "สำหรับคำแนะนำเกี่ยวกับวิธีการโฮสต์เซิร์ฟเวอร์ของคุณเอง", - "pleaseInputValidURL": "กรุณาใส่ URL ที่ถูกต้อง", - "changeUrl": "เปลี่ยน URL ที่โฮสต์เองเป็น {}", "cloudURLHint": "ป้อน URL หลักของเซิร์ฟเวอร์ของคุณ", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "ป้อนที่อยู่ websocket ของเซิร์ฟเวอร์ของคุณ", "restartApp": "รีสตาร์ท", "restartAppTip": "รีสตาร์ทแอปพลิเคชันเพื่อให้มีการเปลี่ยนแปลง โปรดทราบไว้ว่าการดำเนินการนี้อาจจะออกจากระบบบัญชีปัจจุบันของคุณ", - "changeServerTip": "หลังจากเปลี่ยนเซิร์ฟเวอร์แล้ว คุณต้องคลิกปุ่มรีสตาร์ทเพื่อให้การเปลี่ยนแปลงมีผล", "enableEncryptPrompt": "เปิดใช้งานการเข้ารหัสเพื่อรักษาความปลอดภัยข้อมูลของคุณแบบเป็นความลับ จะเก็บไว้อย่างปลอดภัย เมื่อเปิดใช้งานแล้วจะไม่สามารถปิดได้ หากสูญหาย ข้อมูลของคุณจะไม่สามารถเรียกคืนได้ คลิกเพื่อคัดลอก", "inputEncryptPrompt": "กรุณาระบุการเข้ารหัสลับของคุณสำหรับ", "clickToCopySecret": "คลิกเพื่อคัดลอกรหัสลับ", @@ -1068,70 +295,19 @@ "historicalUserList": "ประวัติการเข้าสู่ระบบของผู้ใช้", "historicalUserListTooltip": "รายการนี้จะแสดงบัญชีที่ไม่ระบุตัวตนของคุณ คุณสามารถคลิกที่บัญชีเพื่อดูรายละเอียดได้ บัญชีที่ไม่เปิดเผยตัวตนถูกสร้างขึ้นโดยการคลิกปุ่ม 'เริ่มต้น'", "openHistoricalUser": "คลิกเพื่อเปิดบัญชีที่ไม่ระบุตัวตน", - "customPathPrompt": "การจัดเก็บโฟลเดอร์ข้อมูลของ AppFlowy ไว้ในโฟลเดอร์ที่ซิงค์บนคลาวด์ เช่น Google Drive อาจทำให้เกิดความเสี่ยงได้ หากมีการเข้าถึงหรือแก้ไขฐานข้อมูลภายในโฟลเดอร์นี้จากหลายตำแหน่งพร้อมกัน อาจส่งผลให้เกิดความขัดแย้งในการซิงโครไนซ์และข้อมูลอาจเสียหายได้", - "importAppFlowyData": "นำเข้าข้อมูลจากโฟลเดอร์ @:appName ภายนอก", - "importingAppFlowyDataTip": "กำลังดำเนินการนำเข้าข้อมูล โปรดอย่าปิดแอป", - "importAppFlowyDataDescription": "คัดลอกข้อมูลจากโฟลเดอร์ข้อมูลภายนอกของ @:appName และนำเข้าไปยังโฟลเดอร์ข้อมูล AppFlowy ปัจจุบัน", - "importSuccess": "นำเข้าโฟลเดอร์ข้อมูล @:appName สำเร็จแล้ว", - "importFailed": "การนำเข้าโฟลเดอร์ข้อมูล @:appName ล้มเหลว", - "importGuide": "สำหรับรายละเอียดเพิ่มเติม โปรดตรวจสอบเอกสารอ้างอิง" + "customPathPrompt": "การจัดเก็บโฟลเดอร์ข้อมูลของ AppFlowy ไว้ในโฟลเดอร์ที่ซิงค์บนคลาวด์ เช่น Google Drive อาจทำให้เกิดความเสี่ยงได้ หากมีการเข้าถึงหรือแก้ไขฐานข้อมูลภายในโฟลเดอร์นี้จากหลายตำแหน่งพร้อมกัน อาจส่งผลให้เกิดความขัดแย้งในการซิงโครไนซ์และข้อมูลอาจเสียหายได้" }, "notifications": { "enableNotifications": { "label": "เปิดใช้งานการแจ้งเตือน", "hint": "ปิดเพื่อหยุดไม่ให้แสดงการแจ้งเตือนในเครื่องของคุณ" - }, - "showNotificationsIcon": { - "label": "แสดงไอคอนการแจ้งเตือน", - "hint": "ปิดเพื่อซ่อนไอคอนการแจ้งเตือนในแถบด้านข้าง" - }, - "archiveNotifications": { - "allSuccess": "จัดเก็บการแจ้งเตือนทั้งหมดเรียบร้อยแล้ว", - "success": "จัดเก็บการแจ้งเตือนเรียบร้อยแล้ว" - }, - "markAsReadNotifications": { - "allSuccess": "ทำเครื่องหมายทั้งหมดว่าอ่านแล้วสำเร็จแล้ว", - "success": "ทำเครื่องหมายว่าอ่านแล้วสำเร็จ" - }, - "action": { - "markAsRead": "ทำเครื่องหมายว่าอ่านแล้ว", - "multipleChoice": "เลือกเพิ่มเติม", - "archive": "คลังเก็บเอกสารสำคัญ" - }, - "settings": { - "settings": "การตั้งค่า", - "markAllAsRead": "ทำเครื่องหมายทั้งหมดว่าอ่านแล้ว", - "archiveAll": "เก็บถาวรทั้งหมด" - }, - "emptyInbox": { - "title": "กล่องข้อความว่างเปล่า", - "description": "ตั้งค่าการเตือนเพื่อรับการแจ้งเตือนที่นี่" - }, - "emptyUnread": { - "title": "ไม่มีการแจ้งเตือนที่ยังไม่ได้อ่าน", - "description": "คุณตามทันทั้งหมดแล้ว!" - }, - "emptyArchived": { - "title": "ไม่มีการเก็บถาวร", - "description": "การแจ้งเตือนที่เก็บถาวรจะปรากฏที่นี่" - }, - "tabs": { - "inbox": "กล่องข้อความ", - "unread": "ยังไม่ได้อ่าน", - "archived": "เก็บถาวร" - }, - "refreshSuccess": "รีเฟรชการแจ้งเตือนสำเร็จ", - "titles": { - "notifications": "การแจ้งเตือน", - "reminder": "การเตือน" } }, "appearance": { "resetSetting": "รีเซ็ตการตั้งค่านี้", "fontFamily": { "label": "แบบตัวอักษร", - "search": "ค้นหา", - "defaultFont": "ระบบ" + "search": "ค้นหา" }, "themeMode": { "label": "โหมดธีม", @@ -1139,24 +315,6 @@ "dark": "โหมดมืด", "system": "ใช้ตามระบบ" }, - "fontScaleFactor": "ตัวปรับขนาดฟอนต์", - "documentSettings": { - "cursorColor": "สีเคอร์เซอร์เอกสาร", - "selectionColor": "สีการเลือกเอกสาร", - "width": "ความกว้างของเอกสาร", - "changeWidth": "เปลี่ยน", - "pickColor": "เลือกสี", - "colorShade": "เฉดสี", - "opacity": "ความทึบแสง", - "hexEmptyError": "รหัสสีเลขฐานสิบหกไม่สามารถเว้นว่างได้", - "hexLengthError": "ค่าเลขฐานสิบหกต้องมีความยาว 6 หลัก", - "hexInvalidError": "ค่าเลขฐานสิบหกไม่ถูกต้อง", - "opacityEmptyError": "ความทึบแสงไม่สามารถเว้นว่างได้", - "opacityRangeError": "ความทึบแสงต้องอยู่ระหว่าง 1 และ 100", - "app": "App", - "flowy": "Flowy", - "apply": "นำไปใช้" - }, "layoutDirection": { "label": "ทิศทางของเค้าโครง", "hint": "ควบคุมการไหลของเนื้อหาบนหน้าจอของคุณ จากซ้ายไปขวาหรือจากขวาไปซ้าย", @@ -1175,12 +333,12 @@ "button": "อัปโหลด", "uploadTheme": "อัปโหลดธีม", "description": "อัปโหลดธีม AppFlowy ของคุณเองโดยใช้ปุ่มด้านล่าง", + "failure": "ธีมที่อัปโหลดมีรูปแบบที่ไม่ถูกต้อง", "loading": "โปรดรอสักครู่ในขณะที่เราตรวจสอบและอัปโหลดธีมของคุณ...", "uploadSuccess": "อัปโหลดธีมของคุณสำเร็จแล้ว", "deletionFailure": "ไม่สามารถลบธีมได้ ลองลบมันด้วยตนเอง", "filePickerDialogTitle": "เลือกไฟล์ .flowy_plugin", - "urlUploadFailure": "ไม่สามารถเปิด URL: {} ได้", - "failure": "ธีมที่อัปโหลดมีรูปแบบที่ไม่ถูกต้อง" + "urlUploadFailure": "ไม่สามารถเปิด URL: {} ได้" }, "theme": "ธีม", "builtInsLabel": "ธีมในตัวแอป", @@ -1198,49 +356,7 @@ "twelveHour": "สิบสองชั่วโมง", "twentyFourHour": "ยี่สิบสี่ชั่วโมง" }, - "showNamingDialogWhenCreatingPage": "แสดงกล่องโต้ตอบการตั้งชื่อเมื่อสร้างหน้า", - "enableRTLToolbarItems": "เปิดใช้งานแถบเครื่องมือ RTL (จากขวาไปซ้าย)", - "members": { - "title": "การตั้งค่าสมาชิก", - "inviteMembers": "ส่งคำเชิญสมาชิก", - "inviteHint": "ส่งคำเชิญทางอีเมล", - "sendInvite": "ส่งคำเชิญ", - "copyInviteLink": "คัดลอกลิงค์คำเชิญ", - "label": "สมาชิก", - "user": "ผู้ใช้", - "role": "บทบาท", - "removeFromWorkspace": "ลบออกจากพื้นที่ทำงาน", - "removeFromWorkspaceSuccess": "ลบออกจากพื้นที่ทำงานสำเร็จ", - "removeFromWorkspaceFailed": "ลบออกจากพื้นที่ทำงานไม่สำเร็จ", - "owner": "เจ้าของ", - "guest": "ผู้เยี่ยมชม", - "member": "สมาชิก", - "memberHintText": "สมาชิกสามารถอ่านและแก้ไขหน้าได้", - "guestHintText": "ผู้เยี่ยมชมสามารถอ่าน ตอบสนอง แสดงความคิดเห็น และแก้ไขหน้าบางหน้าได้ด้วยการอนุญาต", - "emailInvalidError": "อีเมล์ไม่ถูกต้อง กรุณาตรวจสอบและลองอีกครั้ง", - "emailSent": "ส่งอีเมลแล้ว กรุณาตรวจสอบกล่องจดหมาย", - "members": "สมาชิก", - "membersCount": { - "zero": "{} สมาชิก", - "one": "{} สมาชิก", - "other": "{} สมาชิก" - }, - "inviteFailedDialogTitle": "ไม่สามารถส่งคำเชิญได้", - "inviteFailedMemberLimit": "จำนวนสมาชิกถึงขีดจำกัดแล้ว กรุณาอัปเกรดเพื่อเชิญสมาชิกเพิ่มเติม", - "inviteFailedMemberLimitMobile": "พื้นที่ทำงานของคุณถึงขีดจำกัดจำนวนสมาชิกแล้ว", - "memberLimitExceeded": "จำนวนสมาชิกถึงขีดจำกัดแล้ว เพื่อเชิญสมาชิกเพิ่มเติม กรุณา...", - "memberLimitExceededUpgrade": "อัพเกรด", - "memberLimitExceededPro": "จำนวนสมาชิกถึงขีดจำกัดแล้ว หากต้องการสมาชิกเพิ่มเติม กรุณาติดต่อ ", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "เพิ่มสมาชิกไม่สำเร็จ", - "addMemberSuccess": "เพิ่มสมาชิกสำเร็จ", - "removeMember": "ลบสมาชิก", - "areYouSureToRemoveMember": "คุณแน่ใจหรือไม่ว่าต้องการลบสมาชิกคนนี้ออก?", - "inviteMemberSuccess": "ส่งคำเชิญเรียบร้อยแล้ว", - "failedToInviteMember": "เชิญสมาชิกไม่สำเร็จ", - "workspaceMembersError": "อุ๊ปส์, มีบางอย่างผิดปกติ", - "workspaceMembersErrorDescription": "เราไม่สามารถโหลดรายชื่อสมาชิกได้ในขณะนี้ กรุณาลองอีกครั้งในภายหลัง" - } + "showNamingDialogWhenCreatingPage": "แสดงกล่องโต้ตอบการตั้งชื่อเมื่อสร้างหน้า" }, "files": { "copy": "คัดลอก", @@ -1276,37 +392,16 @@ "recoverLocationTooltips": "รีเซ็ตเป็นไดเร็กทอรีข้อมูลเริ่มต้นของ AppFlowy", "exportFileSuccess": "ส่งออกไฟล์สำเร็จ!", "exportFileFail": "ส่งออกไฟล์ล้มเหลว!", - "export": "ส่งออก", - "clearCache": "ล้างแคช", - "clearCacheDesc": "หากคุณพบปัญหาเกี่ยวกับรูปภาพไม่โหลด หรือฟอนต์ไม่แสดงอย่างถูกต้อง ให้ลองล้างแคช การดำเนินการนี้จะไม่ลบข้อมูลผู้ใช้ของคุณ", - "areYouSureToClearCache": "คุณแน่ใจหรือไม่ที่จะล้างแคช?", - "clearCacheSuccess": "ล้างแคชสำเร็จ!" + "export": "ส่งออก" }, "user": { "name": "ชื่อ", "email": "อีเมล", "tooltipSelectIcon": "เลือกไอคอน", "selectAnIcon": "เลือกไอคอน", - "pleaseInputYourOpenAIKey": "โปรดระบุคีย์ AI ของคุณ", - "clickToLogout": "คลิกเพื่อออกจากระบบผู้ใช้ปัจจุบัน", - "pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณ" - }, - "mobile": { - "personalInfo": "ข้อมูลส่วนตัว", - "username": "ชื่อผู้ใช้", - "usernameEmptyError": "โปรดกรอกชื่อผู้ใช้", - "about": "เกี่ยวกับ", - "pushNotifications": "การแจ้งเตือนแบบพุช", - "support": "การสนับสนุน", - "joinDiscord": "เข้าร่วมกับเราบน Discord", - "privacyPolicy": "นโยบายความเป็นส่วนตัว", - "userAgreement": "ข้อตกลงผู้ใช้", - "termsAndConditions": "ข้อกำหนดและเงื่อนไข", - "userprofileError": "ไม่สามารถโหลดโปรไฟล์ผู้ใช้ได้", - "userprofileErrorDescription": "โปรดลองออกจากระบบแล้วเข้าสู่ระบบอีกครั้งเพื่อตรวจสอบว่าปัญหายังคงอยู่หรือไม่", - "selectLayout": "เลือกเค้าโครง", - "selectStartingDay": "เลือกวันเริ่มต้น", - "version": "เวอร์ชัน" + "pleaseInputYourOpenAIKey": "โปรดระบุคีย์ OpenAI ของคุณ", + "pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณ", + "clickToLogout": "คลิกเพื่อออกจากระบบผู้ใช้ปัจจุบัน" }, "shortcuts": { "shortcutsLabel": "ทางลัด", @@ -1318,6 +413,21 @@ "resetToDefault": "รีเซ็ตการกำหนดแป้นพิมพ์เป็นค่าเริ่มต้น", "couldNotLoadErrorMsg": "ไม่สามารถโหลดทางลัดได้ ลองอีกครั้ง", "couldNotSaveErrorMsg": "ไม่สามารถบันทึกทางลัดได้ ลองอีกครั้ง" + }, + "mobile": { + "personalInfo": "ข้อมูลส่วนตัว", + "username": "ชื่อผู้ใช้", + "usernameEmptyError": "โปรดกรอกชื่อผู้ใช้", + "about": "เกี่ยวกับ", + "pushNotifications": "การแจ้งเตือนแบบพุช", + "support": "การสนับสนุน", + "joinDiscord": "เข้าร่วมกับเราบน Discord", + "privacyPolicy": "นโยบายความเป็นส่วนตัว", + "userAgreement": "ข้อตกลงผู้ใช้", + "userprofileError": "ไม่สามารถโหลดโปรไฟล์ผู้ใช้ได้", + "userprofileErrorDescription": "โปรดลองออกจากระบบแล้วเข้าสู่ระบบอีกครั้งเพื่อตรวจสอบว่าปัญหายังคงอยู่หรือไม่", + "selectLayout": "เลือกเค้าโครง", + "selectStartingDay": "เลือกวันเริ่มต้น" } }, "grid": { @@ -1338,26 +448,7 @@ "filterBy": "กรองตาม...", "typeAValue": "พิมพ์ค่า...", "layout": "เค้าโครง", - "databaseLayout": "เค้าโครงฐานข้อมูล", - "viewList": { - "zero": "0 มุมมอง", - "one": "{count} มุมมอง", - "other": "{count} มุมมอง" - }, - "editView": "แก้ไขมุมมอง", - "boardSettings": "การตั้งค่าบอร์ด", - "calendarSettings": "การตั้งค่าปฏิทิน", - "createView": "มุมมองใหม่", - "duplicateView": "ทำสำเนามุมมอง", - "deleteView": "ลบมุมมอง", - "numberOfVisibleFields": "{} ที่แสดงอยู่" - }, - "filter": { - "empty": "ไม่มีตัวกรองที่ใช้งานอยู่", - "addFilter": "เพิ่มตัวกรอง", - "cannotFindCreatableField": "ไม่พบฟิลด์ที่เหมาะสมในการกรอง", - "conditon": "เงื่อนไข", - "where": "โดยที่" + "databaseLayout": "เค้าโครงฐานข้อมูล" }, "textFilter": { "contains": "ประกอบด้วย", @@ -1403,40 +494,15 @@ "onOrAfter": "อยู่ในหรือหลังจาก", "between": "อยู่ระหว่าง", "empty": "มันว่างเปล่า", - "notEmpty": "มันไม่ว่างเปล่า", - "startDate": "วันที่เริ่มต้น", - "endDate": "วันที่สิ้นสุด", - "choicechipPrefix": { - "before": "ก่อน", - "after": "หลังจาก", - "between": "ระหว่าง", - "onOrBefore": "ภายในหรือก่อน", - "onOrAfter": "ภายในหรือหลัง", - "isEmpty": "ว่างเปล่า", - "isNotEmpty": "ไม่ว่างเปล่า" - } - }, - "numberFilter": { - "equal": "เท่ากับ", - "notEqual": "ไม่เท่ากับ", - "lessThan": "น้อยกว่า", - "greaterThan": "มากกว่า", - "lessThanOrEqualTo": "น้อยกว่าหรือเท่ากับ", - "greaterThanOrEqualTo": "มากกว่าหรือเท่ากับ", - "isEmpty": "ว่างเปล่า", - "isNotEmpty": "ไม่ว่างเปล่า" + "notEmpty": "มันไม่ว่างเปล่า" }, "field": { - "label": "คุณสมบัติ", "hide": "ซ่อน", "show": "แสดง", "insertLeft": "แทรกทางซ้าย", "insertRight": "แทรกทางขวา", "duplicate": "ทำสำเนา", "delete": "ลบ", - "wrapCellContent": "ตัดข้อความ", - "clear": "ล้างเซลล์", - "switchPrimaryFieldTooltip": "ไม่สามารถเปลี่ยนประเภทฟิลด์ของฟิลด์หลักได้", "textFieldName": "ข้อความ", "checkboxFieldName": "กล่องกาเครื่องหมาย", "dateFieldName": "วันที่", @@ -1447,12 +513,6 @@ "multiSelectFieldName": "การเลือกหลายรายการ", "urlFieldName": "URL", "checklistFieldName": "รายการตรวจสอบ", - "relationFieldName": "ความสัมพันธ์", - "summaryFieldName": "AI สรุป", - "timeFieldName": "เวลา", - "mediaFieldName": "ไฟล์และสื่อ", - "translateFieldName": "AI แปล", - "translateTo": "แปลเป็น", "numberFormat": "รูปแบบตัวเลข", "dateFormat": "รูปแบบวันที่", "includeTime": "รวมเวลา", @@ -1481,13 +541,9 @@ "addOption": "เพิ่มตัวเลือก", "editProperty": "แก้ไขคุณสมบัติ", "newProperty": "คุณสมบัติใหม่", - "openRowDocument": "เปิดเป็นหน้า", "deleteFieldPromptMessage": "แน่ใจหรือไม่? คุณสมบัติเหล่านี้จะถูกลบ", - "clearFieldPromptMessage": "คุณแน่ใจหรือไม่ฦ เซลล์ทั้งหมดในคอลัมน์นี้จะถูกล้างข้อมูล", "newColumn": "คอลัมน์ใหม่", - "format": "รูปแบบ", - "reminderOnDateTooltip": "เซลล์นี้มีการตั้งค่าการเตือนในวันที่กำหนด", - "optionAlreadyExist": "ตัวเลือกมีอยู่แล้ว" + "format": "รูปแบบ" }, "rowPage": { "newField": "เพิ่มฟิลด์ใหม่", @@ -1501,24 +557,15 @@ "one": "ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์", "many": "ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์", "other": "ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์" - }, - "openAsFullPage": "เปิดแบบเต็มหน้า", - "moreRowActions": "การดำเนินการแถวเพิ่มเติม" + } }, "sort": { "ascending": "เรียงลำดับจากน้อยไปมาก", "descending": "เรียงลำดับจากมากไปน้อย", - "by": "โดย", - "empty": "ไม่มีการเรียงลำดับที่ใช้งานอยู่", - "cannotFindCreatableField": "ไม่พบฟิลด์ที่เหมาะสมในการเรียงลำดับ", "deleteAllSorts": "ลบการเรียงลำดับทั้งหมด", - "addSort": "เพิ่มการเรียงลำดับ", - "sortsActive": "ไม่สามารถ {intention} ขณะทำการจัดเรียง", - "removeSorting": "คุณต้องการลบการจัดเรียงทั้งหมดในมุมมองนี้ และดำเนินการต่อหรือไม่?", - "fieldInUse": "คุณกำลังเรียงลำดับตามฟิลด์นี้อยู่แล้ว" + "addSort": "เพิ่มการเรียงลำดับ" }, "row": { - "label": "แถว", "duplicate": "ทำสำเนา", "delete": "ลบ", "titlePlaceholder": "ไม่มีชื่อ", @@ -1529,15 +576,9 @@ "action": "การดำเนินการ", "add": "คลิกเพิ่มด้านล่าง", "drag": "ลากเพื่อย้าย", - "deleteRowPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบแถวนี้? การกระทำนี้ไม่สามารถย้อนกลับได้", - "deleteCardPrompt": "คุณแน่ใจหรือไม่ว่าต้องการลบการ์ดนี้? การกระทำนี้ไม่สามารถย้อนกลับได้", "dragAndClick": "ลากเพื่อย้ายคลิกเพื่อเปิดเมนู", "insertRecordAbove": "แทรกเวิ่นระเบียนด้านบน", - "insertRecordBelow": "แทรกเวิ่นระเบียนด้านล่าง", - "noContent": "ไม่มีเนื้อหา", - "reorderRowDescription": "เรียงลำดับแถวใหม่", - "createRowAboveDescription": "สร้างแถวด้านบน", - "createRowBelowDescription": "สร้างแถวด้านล่าง" + "insertRecordBelow": "แทรกเวิ่นระเบียนด้านล่าง" }, "selectOption": { "create": "สร้าง", @@ -1569,53 +610,10 @@ }, "url": { "launch": "เปิดในเบราว์เซอร์", - "copy": "คัดลอก URL", - "textFieldHint": "ป้อน URL" - }, - "relation": { - "relatedDatabasePlaceLabel": "ฐานข้อมูลที่เกี่ยวข้อง", - "relatedDatabasePlaceholder": "ไม่มี", - "inRelatedDatabase": "ใน", - "rowSearchTextFieldPlaceholder": "ค้นหา", - "noDatabaseSelected": "ยังไม่ได้เลือกฐานข้อมูล กรุณาเลือกฐานข้อมูลหนึ่งรายการจากรายการด้านล่างก่อน", - "emptySearchResult": "ไม่พบข้อมูล", - "linkedRowListLabel": "{count} แถวที่เชื่อมโยง", - "unlinkedRowListLabel": "เชื่อมโยงแถวอื่น" + "copy": "คัดลอก URL" }, "menuName": "ตาราง", - "referencedGridPrefix": "มุมมองของ", - "calculate": "คำนวณ", - "calculationTypeLabel": { - "none": "ไม่มี", - "average": "เฉลี่ย", - "max": "สูงสุด", - "median": "มัธยฐาน", - "min": "ต่ำสุด", - "sum": "ผลรวม", - "count": "นับ", - "countEmpty": "นับที่ว่าง", - "countEmptyShort": "ว่างเปล่า", - "countNonEmpty": "นับที่ไม่ว่าง", - "countNonEmptyShort": "เติมแล้ว" - }, - "media": { - "rename": "เปลี่ยนชื่อ", - "download": "ดาวน์โหลด", - "expand": "ขยาย", - "delete": "ลบ", - "moreFilesHint": "+{}", - "addFileOrImage": "เพิ่มไฟล์หรือลิงค์", - "attachmentsHint": "{}", - "addFileMobile": "เพิ่มไฟล์", - "extraCount": "+{}", - "deleteFileDescription": "คุณแน่ใจหรือไม่ว่าต้องการลบไฟล์นี้? การกระทำนี้ไม่สามารถย้อนกลับได้", - "showFileNames": "แสดงชื่อไฟล์", - "downloadSuccess": "ดาวน์โหลดไฟล์แล้ว", - "downloadFailedToken": "ไม่สามารถดาวน์โหลดไฟล์ได้ โทเค็นผู้ใช้ไม่พร้อมใช้งาน", - "setAsCover": "ตั้งเป็นปก", - "openInBrowser": "เปิดในเบราว์เซอร์", - "embedLink": "ฝังลิงค์ไฟล์" - } + "referencedGridPrefix": "มุมมองของ" }, "document": { "menuName": "เอกสาร", @@ -1638,51 +636,6 @@ }, "document": { "selectADocumentToLinkTo": "เลือกเอกสารเพื่อเชื่อมโยง" - }, - "name": { - "text": "ข้อความ", - "heading1": "หัวข้อที่ 1", - "heading2": "หัวข้อที่ 2", - "heading3": "หัวข้อที่ 3", - "image": "รูปภาพ", - "bulletedList": "รายการลำดับหัวข้อย่อย", - "numberedList": "รายการลำดับตัวเลข", - "todoList": "รายการสิ่งที่ต้องทำ", - "doc": "เอกสาร", - "linkedDoc": "ลิงค์ไปยังหน้า", - "grid": "กริด", - "linkedGrid": "กริดที่เชื่อมโยง", - "kanban": "คันบัน", - "linkedKanban": "คันบันที่เชื่อมโยง", - "calendar": "ปฏิทิน", - "linkedCalendar": "ปฏิทินที่เชื่อมโยง", - "quote": "คำกล่าว", - "divider": "ตัวคั่น", - "table": "ตาราง", - "callout": "การเน้นข้อความ", - "outline": "โครงร่าง", - "mathEquation": "สมการคณิตศาสตร์", - "code": "โค้ด", - "toggleList": "ตัวเปิดปิดรายการ", - "toggleHeading1": "ตัวเปิดปิดหัวข้อ 1", - "toggleHeading2": "ตัวเปิดปิดหัวข้อ 2", - "toggleHeading3": "ตัวเปิดปิดหัวข้อ 3", - "emoji": "อิโมจิ", - "aiWriter": "นักเขียน AI", - "dateOrReminder": "วันที่หรือการเตือน", - "photoGallery": "แกลอรี่รูปภาพ", - "file": "ไฟล์" - }, - "subPage": { - "name": "เอกสาร", - "keyword1": "หน้าย่อย", - "keyword2": "หน้า", - "keyword3": "หน้าย่อย", - "keyword4": "แทรกหน้า", - "keyword5": "ฝังหน้า", - "keyword6": "หน้าใหม่", - "keyword7": "สร้างหน้า", - "keyword8": "เอกสาร" } }, "selectionMenu": { @@ -1694,55 +647,32 @@ "referencedGrid": "ตารางอ้างอิง", "referencedCalendar": "ปฏิทินที่อ้างอิง", "referencedDocument": "เอกสารอ้างอิง", - "autoGeneratorMenuItemName": "นักเขียน AI", - "autoGeneratorTitleName": "AI: สอบถาม AI เพื่อให้เขียนอะไรก็ได้...", + "autoGeneratorMenuItemName": "นักเขียน OpenAI", + "autoGeneratorTitleName": "OpenAI: สอบถาม AI เพื่อให้เขียนอะไรก็ได้...", "autoGeneratorLearnMore": "เรียนรู้เพิ่มเติม", "autoGeneratorGenerate": "สร้าง", - "autoGeneratorHintText": "ถาม AI ...", - "autoGeneratorCantGetOpenAIKey": "ไม่สามารถรับคีย์ AI ได้", + "autoGeneratorHintText": "ถาม OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "ไม่สามารถรับคีย์ OpenAI ได้", "autoGeneratorRewrite": "เขียนใหม่", "smartEdit": "ผู้ช่วย AI", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "แก้ไขการสะกด", "warning": "⚠️ คำตอบของ AI อาจจะไม่ถูกต้องหรืออาจจะเข้าใจผิดได้", "smartEditSummarize": "สรุป", "smartEditImproveWriting": "ปรับปรุงการเขียน", "smartEditMakeLonger": "ทำให้ยาวขึ้น", - "smartEditCouldNotFetchResult": "ไม่สามารถดึงผลลัพธ์จาก AI ได้", - "smartEditCouldNotFetchKey": "ไม่สามารถดึงคีย์ AI ได้", - "smartEditDisabled": "เชื่อมต่อ AI ในการตั้งค่า", - "appflowyAIEditDisabled": "ลงชื่อเข้าใช้เพื่อเปิดใช้งานฟีเจอร์ AI", + "smartEditCouldNotFetchResult": "ไม่สามารถดึงผลลัพธ์จาก OpenAI ได้", + "smartEditCouldNotFetchKey": "ไม่สามารถดึงคีย์ OpenAI ได้", + "smartEditDisabled": "เชื่อมต่อ OpenAI ในการตั้งค่า", "discardResponse": "คุณต้องการทิ้งการตอบกลับของ AI หรือไม่", "createInlineMathEquation": "สร้างสมการ", "fonts": "แบบอักษร", - "insertDate": "ใส่วันที่", - "emoji": "อิโมจิ", - "toggleList": "ตัวเปิดปิดรายการ", - "emptyToggleHeading": "ตัวเปิดปิด h{} ว่างเปล่า คลิกเพื่อเพิ่มเนื้อหา", - "emptyToggleList": "ตัวเปิดปิดรายการว่างเปล่า คลิกเพื่อเพิ่มเนื้อหา", - "quoteList": "รายการคำกล่าว", - "numberedList": "รายการลำดับตัวเลข", - "bulletedList": "รายการลำดับหัวข้อย่อย", + "toggleList": "สลับรายการ", + "quoteList": "รายการอ้างอิง", + "numberedList": "รายการแบบมีหมายเลข", + "bulletedList": "รายการแบบมีจุด", "todoList": "รายการสิ่งที่ต้องทำ", "callout": "คำอธิบายประกอบ", - "simpleTable": { - "moreActions": { - "color": "สี", - "align": "จัดตำแหน่ง", - "delete": "ลบ", - "duplicate": "ทำสำเนา", - "insertLeft": "แทรกซ้าย", - "insertRight": "แทรกขวา", - "insertAbove": "แทรกด้านบน", - "insertBelow": "แทรกด้านล่าง", - "headerColumn": "ส่วนหัวของคอลัมน์", - "headerRow": "ส่วนหัวของแถว", - "clearContents": "ล้างเนื้อหา" - }, - "clickToAddNewRow": "คลิกเพื่อเพิ่มแถวใหม่", - "clickToAddNewColumn": "คลิกเพื่อเพิ่มคอลัมน์ใหม่", - "clickToAddNewRowAndColumn": "คลิกเพื่อเพิ่มแถว และคอลัมน์ใหม่" - }, "cover": { "changeCover": "เปลี่ยนปก", "colors": "สี", @@ -1758,7 +688,6 @@ "back": "ย้อนกลับ", "saveToGallery": "บันทึกลงในแกลเลอรี่", "removeIcon": "ลบไอคอน", - "removeCover": "ลบปก", "pasteImageUrl": "วาง URL รูปภาพ", "or": "หรือ", "pickFromFiles": "เลือกจากไฟล์", @@ -1777,8 +706,6 @@ "optionAction": { "click": "คลิก", "toOpenMenu": " เพื่อเปิดเมนู", - "drag": "ลาก", - "toMove": " เพื่อย้าย", "delete": "ลบ", "duplicate": "ทำสำเนา", "turnInto": "แปลงเป็น", @@ -1789,44 +716,14 @@ "left": "ซ้าย", "center": "กึ่งกลาง", "right": "ขวา", - "defaultColor": "สีเริ่มต้น", - "depth": "ความลึก", - "copyLinkToBlock": "คัดลอกลิงก์ไปยังบล็อก" + "defaultColor": "สีเริ่มต้น" }, "image": { - "addAnImage": "เพิ่มรูปภาพ", "copiedToPasteBoard": "ลิงก์รูปภาพถูกคัดลอกไปที่คลิปบอร์ดแล้ว", - "addAnImageDesktop": "เพิ่มรูปภาพ", - "addAnImageMobile": "คลิกเพื่อเพิ่มรูปภาพหนึ่งรูป หรือมากกว่า", - "dropImageToInsert": "วางภาพเพื่อแทรก", - "imageUploadFailed": "อัพโหลดรูปภาพไม่สำเร็จ", - "imageDownloadFailed": "ดาวน์โหลดรูปภาพล้มเหลว กรุณาลองอีกครั้ง", - "imageDownloadFailedToken": "การดาวน์โหลดภาพล้มเหลวเนื่องจากขาดโทเค็นผู้ใช้ โปรดลองอีกครั้ง", - "errorCode": "รหัสข้อผิดพลาด" - }, - "photoGallery": { - "name": "แกลอรี่รูปภาพ", - "imageKeyword": "รูปภาพ", - "imageGalleryKeyword": "แกลอรี่รูปภาพ", - "photoKeyword": "ภาพถ่าย", - "photoBrowserKeyword": "เบราว์เซอร์รูปภาพ", - "galleryKeyword": "แกลเลอรี่", - "addImageTooltip": "เพิ่มรูปภาพ", - "changeLayoutTooltip": "เปลี่ยนเค้าโครง", - "browserLayout": "เบราว์เซอร์", - "gridLayout": "กริด", - "deleteBlockTooltip": "ลบทั้งแกลอรี่" - }, - "math": { - "copiedToPasteBoard": "คัดลอกสมการคณิตศาสตร์ไปยังคลิปบอร์ดแล้ว" - }, - "urlPreview": { - "copiedToPasteBoard": "คัดลอกลิงก์ไปยังคลิปบอร์ดแล้ว", - "convertToLink": "แปลงเป็นลิงค์ฝัง" + "addAnImage": "เพิ่มรูปภาพ" }, "outline": { - "addHeadingToCreateOutline": "เพิ่มหัวข้อเพื่อสร้างสารบัญ", - "noMatchHeadings": "ไม่พบหัวข้อที่ตรงกัน" + "addHeadingToCreateOutline": "เพิ่มหัวข้อเพื่อสร้างสารบัญ" }, "table": { "addAfter": "เพิ่มหลัง", @@ -1841,64 +738,7 @@ "cut": "ตัด", "paste": "วาง" }, - "action": "การดำเนินการ", - "database": { - "selectDataSource": "เลือกแหล่งข้อมูล", - "noDataSource": "ไม่มีแหล่งข้อมูล", - "selectADataSource": "เลือกแหล่งข้อมูล", - "toContinue": "เพื่อดำเนินการต่อ", - "newDatabase": "ฐานข้อมูลใหม่", - "linkToDatabase": "ลิงค์ไปยังฐานข้อมูล" - }, - "date": "วันที่", - "video": { - "label": "วีดีโอ", - "emptyLabel": "เพิ่มวิดีโอ", - "placeholder": "วางลิงค์วิดีโอ", - "copiedToPasteBoard": "คัดลอกลิงก์วิดีโอไปยังคลิปบอร์ดแล้ว", - "insertVideo": "เพิ่มวีดีโอ", - "invalidVideoUrl": "URL แหล่งที่มายังไม่รองรับในขณะนี้", - "invalidVideoUrlYouTube": "YouTube ยังไม่รองรับ", - "supportedFormats": "รูปแบบที่รองรับ: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "ไฟล์", - "uploadTab": "อัพโหลด", - "uploadMobile": "เลือกไฟล์", - "uploadMobileGallery": "จากแกลเลอรี่รูปภาพ", - "networkTab": "ฝังลิงค์", - "placeholderText": "อัพโหลดหรือฝังไฟล์", - "placeholderDragging": "วางไฟล์เพื่ออัพโหลด", - "dropFileToUpload": "วางไฟล์เพื่ออัพโหลด", - "fileUploadHint": "ลากและวางไฟล์หรือคลิกเพื่อ ", - "fileUploadHintSuffix": "เรียกดู", - "networkHint": "วางลิงก์ไฟล์", - "networkUrlInvalid": "URL ไม่ถูกต้อง ตรวจสอบ URL และลองอีกครั้ง", - "networkAction": "ฝัง", - "fileTooBigError": "ขนาดไฟล์ใหญ่เกินไป กรุณาอัพโหลดไฟล์ที่มีขนาดน้อยกว่า 10MB", - "renameFile": { - "title": "เปลี่ยนชื่อไฟล์", - "description": "ป้อนชื่อใหม่สำหรับไฟล์นี้", - "nameEmptyError": "ชื่อไฟล์ไม่สามารถเว้นว่างได้" - }, - "uploadedAt": "อัพโหลดเมื่อ {}", - "linkedAt": "ลิงก์ถูกเพิ่มเมื่อ {}", - "failedToOpenMsg": "ไม่สามารถเปิดได้ ไม่พบไฟล์" - }, - "subPage": { - "handlingPasteHint": " - (การจัดการการวาง)", - "errors": { - "failedDeletePage": "ลบหน้าไม่สำเร็จ", - "failedCreatePage": "สร้างหน้าไม่สำเร็จ", - "failedMovePage": "ย้ายหน้ามายังเอกสารนี้ไม่สำเร็จ", - "failedDuplicatePage": "ทำสำเนาหน้าไม่สำเร็จ", - "failedDuplicateFindView": "ทำสำเนาหน้าไม่สำเร็จ - ไม่พบมุมมองต้นฉบับ" - } - }, - "cannotMoveToItsChildren": "ไม่สามารถย้ายไปยังหน้าย่อยได้" - }, - "outlineBlock": { - "placeholder": "สารบัญ" + "action": "การดำเนินการ" }, "textBlock": { "placeholder": "พิมพ์ '/' เพื่อดูคำสั่ง" @@ -1917,8 +757,8 @@ "placeholder": "ป้อน URL รูปภาพ" }, "ai": { - "label": "สร้างรูปภาพจาก AI", - "placeholder": "โปรดระบุคำขอให้ AI สร้างรูปภาพ" + "label": "สร้างรูปภาพจาก OpenAI", + "placeholder": "โปรดระบุคำขอให้ OpenAI สร้างรูปภาพ" }, "stability_ai": { "label": "สร้างรูปภาพจาก Stability AI", @@ -1929,9 +769,7 @@ "invalidImage": "รูปภาพไม่ถูกต้อง", "invalidImageSize": "ขนาดรูปภาพต้องไม่เกิน 5MB", "invalidImageFormat": "ไม่รองรับรูปแบบรูปภาพนี้ รูปแบบที่รองรับ: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "URL รูปภาพไม่ถูกต้อง", - "noImage": "ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว", - "multipleImagesFailed": "มีภาพหนึ่งหรือมากกว่าที่ไม่สามารถอัปโหลดได้ กรุณาลองใหม่อีกครั้ง" + "invalidImageUrl": "URL รูปภาพไม่ถูกต้อง" }, "embedLink": { "label": "ฝังลิงก์", @@ -1941,40 +779,18 @@ "label": "Unsplash" }, "searchForAnImage": "ค้นหารูปภาพ", - "pleaseInputYourOpenAIKey": "โปรดระบุคีย์ AI ของคุณในหน้าการตั้งค่า", + "pleaseInputYourOpenAIKey": "โปรดระบุคีย์ OpenAI ของคุณในหน้าการตั้งค่า", + "pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณในหน้าการตั้งค่า", "saveImageToGallery": "บันทึกภาพ", "failedToAddImageToGallery": "ไม่สามารถเพิ่มรูปภาพลงในแกลเลอรี่ได้", "successToAddImageToGallery": "เพิ่มรูปภาพลงในแกลเลอรี่เรียบร้อยแล้ว", - "unableToLoadImage": "ไม่สามารถโหลดรูปภาพได้", - "maximumImageSize": "ขนาดรูปภาพอัปโหลดที่รองรับสูงสุดคือ 10MB", - "uploadImageErrorImageSizeTooBig": "ขนาดรูปภาพต้องน้อยกว่า 10MB", - "imageIsUploading": "กำลังอัพโหลดรูปภาพ", - "openFullScreen": "เปิดแบบเต็มจอ", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "ภาพก่อนหน้า", - "nextImageTooltip": "ภาพถัดไป", - "zoomOutTooltip": "ซูมออก", - "zoomInTooltip": "ซูมเข้า", - "changeZoomLevelTooltip": "เปลี่ยนระดับการซูม", - "openLocalImage": "เปิดภาพ", - "downloadImage": "ดาวน์โหลดภาพ", - "closeViewer": "ปิดโปรแกรมดูแบบโต้ตอบ", - "scalePercentage": "{}%", - "deleteImageTooltip": "ลบรูปภาพ" - } - }, - "pleaseInputYourStabilityAIKey": "โปรดระบุคีย์ Stability AI ของคุณในหน้าการตั้งค่า" + "unableToLoadImage": "ไม่สามารถโหลดรูปภาพได้" }, "codeBlock": { "language": { "label": "ภาษา", - "placeholder": "เลือกภาษา", - "auto": "อัตโนมัติ" - }, - "copyTooltip": "สำเนา", - "searchLanguageHint": "ค้นหาภาษา", - "codeCopiedSnackbar": "คัดลอกโค้ดไปยังคลิปบอร์ดแล้ว!" + "placeholder": "เลือกภาษา" + } }, "inlineLink": { "placeholder": "วางหรือพิมพ์ลิงก์", @@ -1995,37 +811,18 @@ "page": { "label": "ลิงก์ไปยังหน้า", "tooltip": "คลิกเพื่อเปิดหน้า" - }, - "deleted": "ลบแล้ว", - "deletedContent": "เนื้อหานี้ไม่มีอยู่หรือถูกลบไปแล้ว", - "noAccess": "ไม่มีการเข้าถึง", - "deletedPage": "หน้าที่ถูกลบ", - "trashHint": " - ในถังขยะ" + } }, "toolbar": { "resetToDefaultFont": "รีเซ็ตเป็นค่าเริ่มต้น" }, "errorBlock": { "theBlockIsNotSupported": "เวอร์ชันปัจจุบันไม่รองรับบล็อกนี้", - "clickToCopyTheBlockContent": "คลิกเพื่อคัดลอกเนื้อหาบล็อค", - "blockContentHasBeenCopied": "เนื้อหาบล็อกได้รับการคัดลอกแล้ว", - "parseError": "เกิดข้อผิดพลาดขณะทำการแยกข้อมูลบล็อก {}", - "copyBlockContent": "คัดลอกเนื้อหาบล็อค" - }, - "mobilePageSelector": { - "title": "เลือกหน้า", - "failedToLoad": "โหลดรายการหน้าไม่สำเร็จ", - "noPagesFound": "ไม่พบหน้าใดๆ" - }, - "attachmentMenu": { - "choosePhoto": "เลือกภาพถ่าย", - "takePicture": "ถ่ายรูป", - "chooseFile": "เลือกไฟล์" + "blockContentHasBeenCopied": "เนื้อหาบล็อกได้รับการคัดลอกแล้ว" } }, "board": { "column": { - "label": "คอลัมน์", "createNewCard": "สร้างใหม่", "renameGroupTooltip": "กดเพื่อเปลี่ยนชื่อกลุ่ม", "createNewColumn": "เพิ่มกลุ่มใหม่", @@ -2033,10 +830,10 @@ "addToColumnBottomTooltip": "เพิ่มการ์ดใหม่ที่ด้านล่างสุด", "renameColumn": "เปลี่ยนชื่อ", "hideColumn": "ซ่อน", + "groupActions": "กลุ่มการดำเนินการ", "newGroup": "กลุ่มใหม่", "deleteColumn": "ลบ", - "deleteColumnConfirmation": "การดำเนินการนี้จะลบกลุ่มนี้และการ์ดทั้งหมดในกลุ่ม\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", - "groupActions": "กลุ่มการดำเนินการ" + "deleteColumnConfirmation": "การดำเนินการนี้จะลบกลุ่มนี้และการ์ดทั้งหมดในกลุ่ม\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?" }, "hiddenGroupSection": { "sectionTitle": "กลุ่มที่ซ่อนไว้", @@ -2056,7 +853,6 @@ "ungroupedButtonTooltip": "ประกอบด้วยการ์ดที่ไม่อยู่ในกลุ่มใดๆ", "ungroupedItemsTitle": "คลิกเพื่อเพิ่มไปยังกระดาน", "groupBy": "จัดกลุ่มตาม", - "groupCondition": "เงื่อนไขการจัดกลุ่ม", "referencedBoardPrefix": "มุมมองของ", "notesTooltip": "บันทึกย่อข้างใน", "mobile": { @@ -2064,22 +860,6 @@ "showGroup": "เลิกซ่อนกลุ่ม", "showGroupContent": "คุณแน่ใจหรือไม่ว่าต้องการแสดงกลุ่มนี้บนกระดาน?", "failedToLoad": "โหลดมุมมองกระดานไม่สำเร็จ" - }, - "dateCondition": { - "weekOf": "สัปดาห์ที่ {} - {}", - "today": "วันนี้", - "yesterday": "เมื่อวาน", - "tomorrow": "พรุ่งนี้", - "lastSevenDays": "7 วันที่ผ่านมา", - "nextSevenDays": "7 วันถัดไป", - "lastThirtyDays": "30 วันที่ผ่านมา", - "nextThirtyDays": "30 วันถัดไป" - }, - "noGroup": "ไม่มีการจัดกลุ่มตามคุณสมบัติ", - "noGroupDesc": "มุมมองบอร์ดต้องการคุณสมบัติสำหรับการจัดกลุ่มเพื่อแสดงผล", - "media": { - "cardText": "{} {}", - "fallbackName": "ไฟล์" } }, "calendar": { @@ -2090,24 +870,13 @@ "today": "วันนี้", "jumpToday": "ข้ามไปยังวันนี้", "previousMonth": "เดือนก่อนหน้า", - "nextMonth": "เดือนถัดไป", - "views": { - "day": "วัน", - "week": "สัปดาห์", - "month": "เดือน", - "year": "ปี" - } - }, - "mobileEventScreen": { - "emptyTitle": "ยังไม่มีกิจกรรม", - "emptyBody": "กดปุ่มบวกเพื่อสร้างกิจกรรมในวันนี้" + "nextMonth": "เดือนถัดไป" }, "settings": { "showWeekNumbers": "แสดงหมายเลขสัปดาห์", "showWeekends": "แสดงวันหยุดสุดสัปดาห์", "firstDayOfWeek": "เริ่มต้นสัปดาห์ในวัน", "layoutDateField": "จัดรูปแบบปฏิทินตาม", - "changeLayoutDateField": "เปลี่ยนเค้าโครงฟิลด์", "noDateTitle": "ไม่มีวันที่", "noDateHint": { "zero": "กิจกรรมที่ไม่ได้กำหนดวันจะแสดงที่นี่", @@ -2116,23 +885,18 @@ }, "unscheduledEventsTitle": "เหตุการณ์ที่ไม่ได้กำหนดไว้", "clickToAdd": "คลิกเพื่อเพิ่มไปยังปฏิทิน", - "name": "การตั้งค่าปฏิทิน", - "clickToOpen": "คลิกเพื่อเปิดบันทึก" + "name": "การตั้งค่าปฏิทิน" }, "referencedCalendarPrefix": "มุมมองของ", - "quickJumpYear": "ข้ามไปที่", - "duplicateEvent": "ทำสำเนาเหตุการณ์" + "quickJumpYear": "ข้ามไปที่" }, "errorDialog": { "title": "ข้อผิดพลาด AppFlowy", "howToFixFallback": "ขออภัยในความไม่สะดวก! ส่งปัญหาบนหน้า GitHub ของเราอธิบายถึงข้อผิดพลาดของคุณ", - "howToFixFallbackHint1": "ขออภัยในความไม่สะดวก! ส่งปัญหาของคุณมาที่ ", - "howToFixFallbackHint2": " หน้าที่อธิบายข้อผิดพลาดของคุณ", "github": "ดูบน GitHub" }, "search": { "label": "ค้นหา", - "sidebarSearchIcon": "ค้นหาและไปยังหน้านั้นอย่างรวดเร็ว", "placeholder": { "actions": "ค้นหาการการดำเนินการ..." } @@ -2190,48 +954,19 @@ "medium": "ปานกลาง", "mediumDark": "ปานกลางเข้ม", "dark": "เข้ม" - }, - "openSourceIconsFrom": "ไอคอนโอเพ่นซอร์สจาก" + } }, "inlineActions": { "noResults": "ไม่พบผลลัพธ์", - "recentPages": "หน้าล่าสุด", "pageReference": "อ้างอิงหน้า", - "docReference": "การอ้างอิงเอกสาร", - "boardReference": "การอ้างอิงบอร์ด", - "calReference": "การอ้างอิงปฏิทิน", - "gridReference": "การอ้างอิงตาราง", "date": "วันที่", "reminder": { "groupTitle": "ตัวเตือน", "shortKeyword": "ตัวเตือน" - }, - "createPage": "สร้าง \"{}\" หน้าย่อย" + } }, "datePicker": { - "dateTimeFormatTooltip": "เปลี่ยนรูปแบบวันที่และเวลาในการตั้งค่า", - "dateFormat": "รูปแบบวันที่", - "includeTime": "รวมถึงเวลา", - "isRange": "วันที่สิ้นสุด", - "timeFormat": "รูปแบบเวลา", - "clearDate": "ล้างวันที่", - "reminderLabel": "การเตือน", - "selectReminder": "เลือกการเตือน", - "reminderOptions": { - "none": "ไม่มี", - "atTimeOfEvent": "เวลาของงาน", - "fiveMinsBefore": "5 นาทีก่อน", - "tenMinsBefore": "10 นาทีก่อน", - "fifteenMinsBefore": "15 นาทีก่อน", - "thirtyMinsBefore": "30 นาทีก่อน", - "oneHourBefore": "1 ชั่วโมงก่อน", - "twoHoursBefore": "2 ชั่วโมงก่อน", - "onDayOfEvent": "ในวันที่มีงาน", - "oneDayBefore": "1 วันก่อน", - "twoDaysBefore": "2 วันก่อน", - "oneWeekBefore": "1 สัปดาห์ก่อน", - "custom": "กำหนดเอง" - } + "dateTimeFormatTooltip": "เปลี่ยนรูปแบบวันที่และเวลาในการตั้งค่า" }, "relativeDates": { "yesterday": "เมื่อวานนี้", @@ -2278,20 +1013,15 @@ "replace": "แทนที่", "replaceAll": "แทนที่ทั้งหมด", "noResult": "ไม่มีผลลัพธ์", - "caseSensitive": "แบบเจาะจง", - "searchMore": "ค้นหาเพื่อหาผลลัพธ์เพิ่มเติม" + "caseSensitive": "แบบเจาะจง" }, "error": { "weAreSorry": "ขออภัย", - "loadingViewError": "เรากำลังพบปัญหาในการโหลดมุมมองนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ โหลดแอปซ้ำ และอย่าลังเลที่จะติดต่อทีมงานหากปัญหายังคงไม่หาย", - "syncError": "ข้อมูลไม่ได้รับการซิงค์จากอุปกรณ์อื่น", - "syncErrorHint": "โปรดเปิดหน้านี้อีกครั้งบนอุปกรณ์ที่แก้ไขล่าสุด จากนั้นเปิดอีกครั้งบนอุปกรณ์ปัจจุบัน", - "clickToCopy": "คลิกเพื่อคัดลอกรหัสข้อผิดพลาด" + "loadingViewError": "เรากำลังพบปัญหาในการโหลดมุมมองนี้ โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ โหลดแอปซ้ำ และอย่าลังเลที่จะติดต่อทีมงานหากปัญหายังคงไม่หาย" }, "editor": { "bold": "ตัวหนา", "bulletedList": "รายการลำดับหัวข้อย่อย", - "bulletedListShortForm": "รายการสัญลักษณ์จุด", "checkbox": "กล่องกาเครื่องหมาย", "embedCode": "ฝังโค้ด", "heading1": "H1", @@ -2300,15 +1030,9 @@ "highlight": "ไฮไลท์", "color": "สี", "image": "รูปภาพ", - "date": "วันที่", - "page": "หน้า", "italic": "ตัวเอียง", "link": "ลิงก์", "numberedList": "รายการลำดับตัวเลข", - "numberedListShortForm": "แบบระบุหมายเลข", - "toggleHeading1ShortForm": "ตัวเปิดปิดหัวข้อ h1", - "toggleHeading2ShortForm": "ตัวเปิดปิดหัวข้อ h2", - "toggleHeading3ShortForm": "ตัวเปิดปิดหัวข้อ h3", "quote": "คำกล่าว", "strikethrough": "ขีดฆ่า", "text": "ข้อความ", @@ -2333,8 +1057,6 @@ "backgroundColorPurple": "พื้นหลังสีม่วง", "backgroundColorPink": "พื้นหลังสีชมพู", "backgroundColorRed": "พื้นหลังสีแดง", - "backgroundColorLime": "พื้นหลังสีเขียวมะนาว", - "backgroundColorAqua": "พื้นหลังสีฟ้าน้ำทะเล", "done": "เสร็จสิ้น", "cancel": "ยกเลิก", "tint1": "สีจาง 1", @@ -2382,8 +1104,6 @@ "copy": "คัดลอก", "paste": "วาง", "find": "ค้นหา", - "select": "เลือก", - "selectAll": "เลือกทั้งหมด", "previousMatch": "จับคู่ก่อนหน้า", "nextMatch": "จับคู่ถัดไป", "closeFind": "ปิด", @@ -2410,18 +1130,11 @@ "rowDuplicate": "ทำซ้ำ", "colClear": "ล้างเนื้อหา", "rowClear": "ล้างเนื้อหา", - "slashPlaceHolder": "พิมพ์ / เพื่อแทรกบล็อก หรือเริ่มพิมพ์", - "typeSomething": "พิมพ์อะไรบางอย่าง...", - "toggleListShortForm": "สลับ", - "quoteListShortForm": "คำกล่าว", - "mathEquationShortForm": "สูตร", - "codeBlockShortForm": "โค้ด" + "slashPlaceHolder": "พิมพ์ / เพื่อแทรกบล็อก หรือเริ่มพิมพ์" }, "favorite": { "noFavorite": "ไม่มีหน้ารายการโปรด", - "noFavoriteHintText": "ปัดหน้าไปทางซ้ายเพื่อเพิ่มลงในรายการโปรด", - "removeFromSidebar": "ลบออกจากแถบด้านข้าง", - "addToSidebar": "ปักหมุดไปที่แถบด้านข้าง" + "noFavoriteHintText": "ปัดหน้าไปทางซ้ายเพื่อเพิ่มลงในรายการโปรด" }, "cardDetails": { "notesPlaceholder": "ป้อน / เพื่อแทรกบล็อก หรือเริ่มพิมพ์" @@ -2441,473 +1154,5 @@ "date": "วันที่", "addField": "เพิ่มฟิลด์", "userIcon": "ไอคอนผู้ใช้งาน" - }, - "noLogFiles": "ไม่มีไฟล์บันทึก", - "newSettings": { - "myAccount": { - "title": "บัญชีของฉัน", - "subtitle": "ปรับแต่งโปรไฟล์ของคุณ จัดการความปลอดภัยบัญชี เปิดคีย์ AI หรือเข้าสู่ระบบบัญชีของคุณ", - "profileLabel": "ชื่อบัญชีและรูปโปรไฟล์", - "profileNamePlaceholder": "กรุณากรอกชื่อของคุณ", - "accountSecurity": "ความปลอดภัยของบัญชี", - "2FA": "การยืนยันตัวตน 2 ขั้นตอน", - "aiKeys": "คีย์ AI", - "accountLogin": "การเข้าสู่ระบบบัญชี", - "updateNameError": "อัปเดตชื่อไม่สำเร็จ", - "updateIconError": "อัปเดตไอคอนไม่สำเร็จ", - "deleteAccount": { - "title": "ลบบัญชี", - "subtitle": "ลบบัญชี และข้อมูลทั้งหมดของคุณอย่างถาวร", - "description": "ลบบัญชีของคุณอย่างถาวร และลบสิทธิ์การเข้าถึงจากพื้นที่ทำงานทั้งหมด", - "deleteMyAccount": "ลบบัญชีของฉัน", - "dialogTitle": "ลบบัญชี", - "dialogContent1": "คุณแน่ใจหรือไม่ว่าต้องการลบบัญชีของคุณอย่างถาวร?", - "dialogContent2": "การดำเนินการนี้ไม่สามารถย้อนกลับได้ และจะลบสิทธิ์การเข้าถึงจากพื้นที่ทำงานทั้งหมด ลบบัญชีของคุณทั้งหมด รวมถึงพื้นที่ทำงานส่วนตัว และลบคุณออกจากพื้นที่ทำงานที่แชร์ทั้งหมด", - "confirmHint1": "กรุณาพิมพ์ \"DELETE MY ACCOUNT\" เพื่อยืนยัน", - "confirmHint2": "ฉันเข้าใจดีว่าการดำเนินการนี้ไม่สามารถย้อนกลับได้ และจะลบบัญชีของฉันรวมถึงข้อมูลที่เกี่ยวข้องทั้งหมดอย่างถาวร", - "confirmHint3": "DELETE MY ACCOUNT", - "checkToConfirmError": "คุณต้องทำเครื่องหมายในช่องเพื่อยืนยันการลบ", - "failedToGetCurrentUser": "ไม่สามารถดึงอีเมลของผู้ใช้ปัจจุบันได้", - "confirmTextValidationFailed": "ข้อความยืนยันของคุณไม่ตรงกับ \"DELETE MY ACCOUNT\"", - "deleteAccountSuccess": "ลบบัญชีสำเร็จแล้ว" - } - }, - "workplace": { - "name": "พื้นไที่ทำงาน", - "title": "การตั้งค่าพื้นไที่ทำงาน", - "subtitle": "ปรับแต่งรูปลักษณ์พื้นที่ทำงาน ธีม แบบอักษร เค้าโครงข้อความ วันที่ เวลา และภาษาของคุณ", - "workplaceName": "ชื่อพื้นไที่ทำงาน", - "workplaceNamePlaceholder": "กรอกชื่อพื้นที่ทำงาน", - "workplaceIcon": "ไอคอนพื้นที่ทำงาน", - "workplaceIconSubtitle": "อัปโหลดรูปภาพ หรือใช้อีโมจิสำหรับพื้นที่ทำงานของคุณ ไอคอนจะแสดงในแถบด้านข้าง และการแจ้งเตือนของคุณ", - "renameError": "ไม่สามารถเปลี่ยนชื่อสถานที่ทำงานได้", - "updateIconError": "ไม่สามารถอัปเดตไอคอนได้", - "chooseAnIcon": "เลือกไอคอน", - "appearance": { - "name": "ลักษณะการแสดงผล", - "themeMode": { - "auto": "อัตโนมัติ", - "light": "สว่าง", - "dark": "มืด" - }, - "language": "ภาษา" - } - }, - "syncState": { - "syncing": "กำลังซิงค์", - "synced": "ซิงค์แล้ว", - "noNetworkConnected": "ไม่มีการเชื่อมต่อเครือข่าย" - } - }, - "pageStyle": { - "title": "รูปแบบหน้า", - "layout": "เค้าโครง", - "coverImage": "ภาพปก", - "pageIcon": "ไอคอนหน้าเพจ", - "colors": "สีสัน", - "gradient": "การไล่ระดับ", - "backgroundImage": "ภาพพื้นหลัง", - "presets": "พรีเซ็ต", - "photo": "รูปถ่าย", - "unsplash": "อันสแปลช", - "pageCover": "ปกหน้า", - "none": "ไม่มี", - "openSettings": "เปิดการตั้งค่า", - "photoPermissionTitle": "@:appName ต้องการเข้าถึงคลังรูปภาพของคุณ", - "photoPermissionDescription": "@:appName ต้องการเข้าถึงรูปภาพของคุณเพื่อให้คุณสามารถเพิ่มภาพลงในเอกสารของคุณได้", - "cameraPermissionTitle": "@:appName ต้องการเข้าถึงกล้องของคุณ", - "cameraPermissionDescription": "@:appName ต้องการเข้าถึงกล้องของคุณเพื่อให้คุณสามารถเพิ่มรูปภาพลงในเอกสารจากกล้องได้", - "doNotAllow": "ไม่อนุญาต", - "image": "รูปภาพ" - }, - "commandPalette": { - "placeholder": "พิมพ์เพื่อค้นหา...", - "bestMatches": "การจับคู่ที่ดีที่สุด", - "recentHistory": "ประวัติล่าสุด", - "navigateHint": "เพื่อนำทาง", - "loadingTooltip": "เรากำลังมองหาผลลัพธ์...", - "betaLabel": "BETA", - "betaTooltip": "ปัจจุบันเรารองรับเฉพาะการค้นหาหน้า หรือเนื้อหาภายในเอกสารเท่านั้น", - "fromTrashHint": "จากถังขยะ", - "noResultsHint": "เราไม่พบสิ่งที่คุณกำลังมองหา ลองค้นหาด้วยคำอื่นดู", - "clearSearchTooltip": "ล้างช่องค้นหา" - }, - "space": { - "delete": "ลบ", - "deleteConfirmation": "ลบ: ", - "deleteConfirmationDescription": "หน้าทั้งหมดในพื้นที่นี้จะถูกลบออก และย้ายไปยังถังขยะ และหน้าที่เผยแพร่ทั้งหมดจะถูกยกเลิกการเผยแพร่", - "rename": "เปลี่ยนชื่อพื้นที่", - "changeIcon": "เปลี่ยนไอคอน", - "manage": "จัดการพื้นที่", - "addNewSpace": "สร้างพื้นที่", - "collapseAllSubPages": "ยุบหน้าย่อยทั้งหมด", - "createNewSpace": "สร้างพื้นที่ใหม่", - "createSpaceDescription": "สร้างพื้นที่สาธารณะ และส่วนตัวหลายแห่ง เพื่อจัดระเบียบงานของคุณได้ดีขึ้น", - "spaceName": "ชื่อพื้นที่", - "spaceNamePlaceholder": "เช่น การตลาด วิศวกรรม ทรัพยากรบุคคล", - "permission": "การอนุญาตใช้พื้นที่", - "publicPermission": "สาธารณะ", - "publicPermissionDescription": "สมาชิกพื้นที่ทำงานทั้งหมดที่มีสิทธิ์เข้าถึงเต็มรูปแบบ", - "privatePermission": "ส่วนตัว", - "privatePermissionDescription": "เฉพาะคุณเท่านั้นที่สามารถเข้าถึงพื้นที่นี้ได้", - "spaceIconBackground": "สีพื้นหลัง", - "spaceIcon": "ไอคอน", - "dangerZone": "โซนอันตราย", - "unableToDeleteLastSpace": "ไม่สามารถลบพื้นที่สุดท้ายได้", - "unableToDeleteSpaceNotCreatedByYou": "ไม่สามารถลบพื้นที่ที่สร้างโดยผู้อื่นได้", - "enableSpacesForYourWorkspace": "เปิดใช้งานพื้นที่สำหรับพื้นที่ทำงานของคุณ", - "title": "พื้นที่", - "defaultSpaceName": "ทั่วไป", - "upgradeSpaceTitle": "เปิดใช้งานพื้นที่", - "upgradeSpaceDescription": "สร้างพื้นที่สาธารณะ และส่วนตัวหลายแห่ง เพื่อจัดระเบียบพื้นที่ทำงานของคุณได้ดีขึ้น", - "upgrade": "อัปเดต", - "upgradeYourSpace": "สร้างหลายพื้นที่", - "quicklySwitch": "สลับไปยังพื้นที่ถัดไปอย่างรวดเร็ว", - "duplicate": "ทำสำเนาพื้นที่", - "movePageToSpace": "ย้ายหน้าไปยังพื้นที่", - "cannotMovePageToDatabase": "ไม่สามารถย้ายหน้าไปยังฐานข้อมูลได้", - "switchSpace": "สลับพื้นที่", - "spaceNameCannotBeEmpty": "ชื่อพื้นที่ไม่สามารถปล่อยว่างได้", - "success": { - "deleteSpace": "ลบพื้นที่สำเร็จ", - "renameSpace": "เปลี่ยนชื่อพื้นที่สำเร็จ", - "duplicateSpace": "คัดลอกพื้นที่สำเร็จ", - "updateSpace": "อัปเดตพื้นที่สำเร็จ" - }, - "error": { - "deleteSpace": "ลบพื้นที่ไม่สำเร็จ", - "renameSpace": "เปลี่ยนชื่อพื้นที่ไม่สำเร็จ", - "duplicateSpace": "ทำสำเนาพื้นที่ไม่สำเร็จ", - "updateSpace": "อัปเดตพื้นที่ไม่สำเร็จ" - }, - "createSpace": "สร้างพื้นที่", - "manageSpace": "จัดการพื้นที่", - "renameSpace": "เปลี่ยนชื่อพื้นที่", - "mSpaceIconColor": "สีไอคอนพื้นที่", - "mSpaceIcon": "ไอคอนพื้นที่" - }, - "publish": { - "hasNotBeenPublished": "หน้านี้ยังไม่ได้เผยแพร่", - "spaceHasNotBeenPublished": "ยังไม่รองรับการเผยแพร่พื้นที่", - "reportPage": "รายงานหน้า", - "databaseHasNotBeenPublished": "ยังไม่รองรับการเผยแพร่ฐานข้อมูล", - "createdWith": "สร้างด้วย", - "downloadApp": "ดาวน์โหลด AppFlowy", - "copy": { - "codeBlock": "คัดลอกเนื้อหาของโค้ดบล็อคไปยังคลิปบอร์ดแล้ว", - "imageBlock": "คัดลอกลิงก์รูปภาพไปยังคลิปบอร์ดแล้ว", - "mathBlock": "คัดลอกสมการคณิตศาสตร์ไปยังคลิปบอร์ดแล้ว", - "fileBlock": "คัดลอกลิงก์ไฟล์ไปยังคลิปบอร์ดแล้ว" - }, - "containsPublishedPage": "หน้านี้มีหน้าที่เผยแพร่แล้วหนึ่งหน้าขึ้นไป หากคุณดำเนินการต่อ หน้าเหล่านั้นจะถูกยกเลิกการเผยแพร่ คุณต้องการดำเนินการลบหรือไม่", - "publishSuccessfully": "เผยแพร่สำเร็จ", - "unpublishSuccessfully": "ยกเลิกการเผยแพร่สำเร็จ", - "publishFailed": "เผยแพร่ไม่สำเร็จ", - "unpublishFailed": "ยกเลิกการเผยแพร่ไม่สำเร็จ", - "noAccessToVisit": "ไม่มีการเข้าถึงหน้านี้...", - "createWithAppFlowy": "สร้างเว็บไซต์ด้วย AppFlowy", - "fastWithAI": "รวดเร็วและง่ายดายด้วย AI", - "tryItNow": "ลองเลยตอนนี้", - "onlyGridViewCanBePublished": "สามารถเผยแพร่ได้เฉพาะมุมมองแบบกริดเท่านั้น", - "database": { - "zero": "เผยแพร่ {} มุมมองที่เลือก", - "one": "เผยแพร่ {} มุมมองที่เลือก", - "many": "เผยแพร่ {} มุมมองที่เลือก", - "other": "เผยแพร่ {} มุมมองที่เลือก" - }, - "mustSelectPrimaryDatabase": "ต้องเลือกมุมมองหลัก", - "noDatabaseSelected": "ยังไม่ได้เลือกฐานข้อมูล กรุณาเลือกอย่างน้อยหนึ่งฐานข้อมูล", - "unableToDeselectPrimaryDatabase": "ไม่สามารถยกเลิกการเลือกฐานข้อมูลหลักได้", - "saveThisPage": "เริ่มต้นด้วยเทมเพลตนี้", - "duplicateTitle": "คุณต้องการเพิ่มที่ไหน", - "selectWorkspace": "เลือกพื้นที่ทำงาน", - "addTo": "เพิ่มไปยัง", - "duplicateSuccessfully": "เพิ่มไปที่พื้นที่ทำงานของคุณแล้ว", - "duplicateSuccessfullyDescription": "ยังไม่ได้ติดตั้ง AppFlowy ใช่ไหม? การดาวน์โหลดจะเริ่มต้นโดยอัตโนมัติหลังจากที่คุณคลิก 'ดาวน์โหลด'", - "downloadIt": "ดาวน์โหลด", - "openApp": "เปิดในแอป", - "duplicateFailed": "ทำสำเนาไม่สำเร็จ", - "membersCount": { - "zero": "ไม่มีสมาชิก", - "one": "1 สมาชิก", - "many": "{count} สมาชิก", - "other": "{count} สมาชิก" - }, - "useThisTemplate": "ใช้เทมเพลต" - }, - "web": { - "continue": "ดำเนินการต่อ", - "or": "หรือ", - "continueWithGoogle": "ดำเนินการต่อด้วย Google", - "continueWithGithub": "ดำเนินการต่อด้วย GitHub", - "continueWithDiscord": "ดำเนินการต่อด้วย Discord", - "continueWithApple": "ดำเนินการต่อด้วย Apple ", - "moreOptions": "ตัวเลือกเพิ่มเติม", - "collapse": "ยุบ", - "signInAgreement": "โดยการคลิก \"ดำเนินการต่อ\" ด้านบน, คุณตกลงตามข้อกำหนดของ AppFlowy", - "and": "และ", - "termOfUse": "เงื่อนไข", - "privacyPolicy": "นโยบายความเป็นส่วนตัว", - "signInError": "เกิดข้อผิดพลาดในการเข้าสู่ระบบ", - "login": "ลงทะเบียนหรือเข้าสู่ระบบ", - "fileBlock": { - "uploadedAt": "อัพโหลดเมื่อ {เวลา}", - "linkedAt": "เพิ่มลิงก์เมื่อ {เวลา}", - "empty": "อัพโหลดหรือฝังไฟล์" - }, - "importNotion": "นำเข้าจาก Notion", - "import": "นำเข้า", - "importSuccess": "อัพโหลดสำเร็จ", - "importSuccessMessage": "เราจะแจ้งให้คุณทราบเมื่อการนำเข้าเสร็จสมบูรณ์ หลังจากนั้นคุณสามารถดูหน้าที่นำเข้าได้ในแถบด้านข้าง", - "importFailed": "การนำเข้าไม่สำเร็จ กรุณาตรวจสอบรูปแบบไฟล์", - "dropNotionFile": "วางไฟล์ zip ของ Notion ไว้ที่นี่เพื่ออัพโหลด หรือคลิกเพื่อเรียกดู" - }, - "globalComment": { - "comments": "ความคิดเห็น", - "addComment": "เพิ่มความคิดเห็น", - "reactedBy": "ตอบสนองโดย", - "addReaction": "เพิ่มความรู้สึก", - "reactedByMore": "และอีก {count}", - "showSeconds": { - "one": "1 วินาทีที่แล้ว", - "other": "{count} วินาทีที่ผ่านมา", - "zero": "เมื่อสักครู่", - "many": "{count} วินาทีที่ผ่านมา" - }, - "showMinutes": { - "one": "1 นาทีที่แล้ว", - "other": "{count} นาทีที่ผ่านมา", - "many": "{count} นาทีที่ผ่านมา" - }, - "showHours": { - "one": "1 ชั่วโมงที่แล้ว", - "other": "{count} ชั่วโมงที่ผ่านมา", - "many": "{count} ชั่วโมงที่ผ่านมา" - }, - "showDays": { - "one": "1 วันที่ผ่านมา", - "other": "{count} วันที่ผ่านมา", - "many": "{count} วันที่ผ่านมา" - }, - "showMonths": { - "one": "1 เดือนที่ผ่านมา", - "other": "{count} เดือนที่ผ่านมา", - "many": "{count} เดือนที่ผ่านมา" - }, - "showYears": { - "one": "1 ปีที่ผ่านมา", - "other": "{count} ปีที่ผ่านมา", - "many": "{count} ปีที่ผ่านมา" - }, - "reply": "ตอบกลับ", - "deleteComment": "ลบความคิดเห็น", - "youAreNotOwner": "คุณไม่ใช่เจ้าของความคิดเห็นนี้", - "confirmDeleteDescription": "คุณแน่ใจหรือไม่ว่าต้องการลบความคิดเห็นนี้?", - "hasBeenDeleted": "ลบแล้ว", - "replyingTo": "กำลังตอบกลับถึง", - "noAccessDeleteComment": "คุณไม่มีสิทธิ์ลบความคิดเห็นนี้", - "collapse": "ยุบ", - "readMore": "อ่านเพิ่มเติม", - "failedToAddComment": "เพิ่มความคิดเห็นไม่สำเร็จ", - "commentAddedSuccessfully": "เพิ่มความคิดเห็นสำเร็จ", - "commentAddedSuccessTip": "คุณเพิ่งเพิ่ม หรือตอบกลับความคิดเห็น คุณต้องการข้ามไปที่ด้านบนเพื่อดูความคิดเห็นล่าสุดหรือไม่" - }, - "template": { - "asTemplate": "บันทึกเป็นเทมเพลต", - "name": "ชื่อเทมเพลต", - "description": "คำอธิบายเทมเพลต", - "about": "เทมเพลตเกี่ยวกับ", - "deleteFromTemplate": "ลบออกจากเทมเพลต", - "preview": "ตัวอย่างเทมเพลต", - "categories": "หมวดหมู่เทมเพลต", - "isNewTemplate": "PIN ไปยังเทมเพลตใหม่", - "featured": "PIN ไปที่ฟีเจอร์", - "relatedTemplates": "เทมเพลตที่เกี่ยวข้อง", - "requiredField": "{field} จำเป็นต้องกรอก", - "addCategory": "เพิ่ม \"{category}\"", - "addNewCategory": "เพิ่มหมวดหมู่ใหม่", - "addNewCreator": "เพิ่มผู้สร้างใหม่", - "deleteCategory": "ลบหมวดหมู่", - "editCategory": "แก้ไขหมวดหมู่", - "editCreator": "แก้ไขผู้สร้าง", - "category": { - "name": "ชื่อหมวดหมู่", - "icon": "ไอคอนหมวดหมู่", - "bgColor": "สีพื้นหลังหมวดหมู่", - "priority": "ลำดับความสำคัญของหมวดหมู่", - "desc": "คำอธิบายหมวดหมู่", - "type": "ประเภทหมวดหมู่", - "icons": "ไอคอนหมวดหมู่", - "colors": "สีหมวดหมู่", - "byUseCase": "ตามลักษณะการใช้งาน", - "byFeature": "ตามคุณสมบัติ", - "deleteCategory": "ลบหมวดหมู่", - "deleteCategoryDescription": "คุณแน่ใจหรือไม่ว่าต้องการลบหมวดหมู่นี้?", - "typeToSearch": "พิมพ์เพื่อค้นหาหมวดหมู่..." - }, - "creator": { - "label": "ผู้สร้างเทมเพลต", - "name": "ชื่อผู้สร้าง", - "avatar": "อวตาร์ของผู้สร้าง", - "accountLinks": "ลิงค์บัญชีผู้สร้าง", - "uploadAvatar": "คลิกเพื่ออัพโหลดอวาตาร์", - "deleteCreator": "ลบผู้สร้าง", - "deleteCreatorDescription": "คุณแน่ใจหรือไม่ว่าต้องการลบผู้สร้างรายนี้?", - "typeToSearch": "พิมพ์เพื่อค้นหาผู้สร้าง..." - }, - "uploadSuccess": "อัพโหลดเทมเพลตสำเร็จ", - "uploadSuccessDescription": "เทมเพลตของคุณได้รับการอัปโหลดสำเร็จแล้ว ขณะนี้คุณสามารถดูเทมเพลตได้ในแกลเลอรีเทมเพลต", - "viewTemplate": "ดูเทมเพลต", - "deleteTemplate": "ลบเทมเพลต", - "deleteSuccess": "ลบเทมเพลตสำเร็จ", - "deleteTemplateDescription": "การกระทำนี้จะไม่ส่งผลกระทบต่อหน้าปัจจุบัน หรือสถานะการเผยแพร่ คุณแน่ใจหรือไม่ว่าต้องการลบเทมเพลตนี้?", - "addRelatedTemplate": "เพิ่มเทมเพลตที่เกี่ยวข้อง", - "removeRelatedTemplate": "ลบเทมเพลตที่เกี่ยวข้อง", - "uploadAvatar": "อัพโหลดอวาตาร์", - "searchInCategory": "ค้นหาใน {category}", - "label": "เทมเพลต" - }, - "fileDropzone": { - "dropFile": "คลิก หรือลากไฟล์ ไปยังพื้นที่นี้เพื่ออัพโหลด", - "uploading": "กำลังอัพโหลด...", - "uploadFailed": "อัพโหลดไม่สำเร็จ", - "uploadSuccess": "อัพโหลดสำเร็จ", - "uploadSuccessDescription": "ไฟล์ได้รับการอัพโหลดเรียบร้อยแล้ว", - "uploadFailedDescription": "อัพโหลดไฟล์ไม่สำเร็จ", - "uploadingDescription": "ไฟล์กำลังถูกอัพโหลด" - }, - "gallery": { - "preview": "เปิดแบบเต็มจอ", - "copy": "สำเนา", - "download": "ดาวน์โหลด", - "prev": "ก่อนหน้า", - "next": "ถัดไป", - "resetZoom": "รีเซ็ตการซูม", - "zoomIn": "ซูมเข้า", - "zoomOut": "ซูมออก" - }, - "invitation": { - "join": "เข้าร่วม", - "on": "บน", - "invitedBy": "ได้รับเชิญโดย", - "membersCount": { - "zero": "{count} สมาชิก", - "one": "{count} สมาชิก", - "many": "{count} สมาชิก", - "other": "{count} สมาชิก" - }, - "tip": "คุณได้รับคำเชิญให้เข้าร่วมพื้นที่ทำงานนี้โดยใช้ข้อมูลติดต่อด้านล่าง หากข้อมูลนี้ไม่ถูกต้อง กรุณาติดต่อผู้ดูแลเพื่อขอให้ส่งคำเชิญใหม่", - "joinWorkspace": "เข้าร่วมพื้นที่ทำงาน", - "success": "คุณได้เข้าร่วมพื้นที่ทำงานเรียบร้อยแล้ว", - "successMessage": "ตอนนี้คุณสามารถเข้าถึงหน้า และพื้นที่ทำงานทั้งหมดภายในนั้นได้แล้ว", - "openWorkspace": "เปิด AppFlowy", - "alreadyAccepted": "คุณได้ยอมรับคำเชิญแล้ว", - "errorModal": { - "title": "มีบางอย่างผิดพลาด", - "description": "บัญชีปัจจุบันของคุณ {email} อาจไม่มีสิทธิ์เข้าถึงพื้นที่ทำงานนี้ กรุณาล็อกอินด้วยบัญชีที่ถูกต้องหรือ ติดต่อเจ้าของพื้นที่ทำงานเพื่อขอความช่วยเหลือ", - "contactOwner": "ติดต่อเจ้าของ", - "close": "กลับสู่หน้าแรก", - "changeAccount": "เปลี่ยนบัญชี" - } - }, - "requestAccess": { - "title": "ไม่มีการเข้าถึงหน้านี้", - "subtitle": "คุณสามารถขอสิทธิ์การเข้าถึงจากเจ้าขอหน้านี้ได้ เมื่อได้รับการอนุมัติแล้วคุณจะสามารถดูหน้าได้", - "requestAccess": "ขอสิทธิ์การเข้าถึง", - "backToHome": "กลับสู่หน้าแรก", - "tip": "ขณะนี้คุณเข้าสู่ระบบเป็น <link/>", - "mightBe": "คุณอาจต้อง <login/> ด้วยบัญชีอื่น", - "successful": "ส่งคำขอเรียบร้อยแล้ว", - "successfulMessage": "คุณจะได้รับการแจ้งเตือนเมื่อเจ้าของอนุมัติคำขอของคุณ", - "requestError": "ไม่สามารถขอการเข้าถึงได้", - "repeatRequestError": "คุณได้ขอการเข้าถึงหน้านี้แล้ว" - }, - "approveAccess": { - "title": "อนุมัติคำขอเข้าร่วมพื้นที่ทำงาน", - "requestSummary": "<user/> ขอเข้าร่วม <workspace/> และเข้าถึง <page/>", - "upgrade": "อัพเกรด", - "downloadApp": "ดาวน์โหลด AppFlowy", - "approveButton": "อนุมัติ", - "approveSuccess": "ได้รับการอนุมัติเรียบร้อยแล้ว", - "approveError": "ไม่สามารถอนุมัติได้ โปรดตรวจสอบให้แน่ใจว่าไม่เกินขีดจำกัดของแผนพื้นที่ทำงาน", - "getRequestInfoError": "ไม่สามารถรับข้อมูลคำขอได้", - "memberCount": { - "zero": "ไม่มีสมาชิก", - "one": "1 สมาชิก", - "many": "{count} สมาชิก", - "other": "{count} สมาชิก" - }, - "alreadyProTitle": "คุณได้ถึงขีดจำกัดของแผนพื้นที่ทำงานแล้ว", - "alreadyProMessage": "ขอให้พวกเขาติดต่อ <email/> เพื่อปลดล็อกสมาชิกเพิ่มเติม", - "repeatApproveError": "คุณได้อนุมัติคำขอนี้แล้ว", - "ensurePlanLimit": "โปรดตรวจสอบให้แน่ใจว่าไม่เกินขีดจำกัดของแผนพื้นที่ทำงาน หากเกินขีดจำกัดแล้ว ให้พิจารณา <upgrade/> แผนพื้นที่ทำงานหรือ <download/>", - "requestToJoin": "ขอเข้าร่วม", - "asMember": "ในฐานะสมาชิก" - }, - "upgradePlanModal": { - "title": "อัพเกรดเป็น Pro", - "message": "{name} ได้ถึงขีดจำกัดของสมาชิกฟรีแล้ว อัปเกรดเป็นแผน Pro เพื่อเชิญสมาชิกเพิ่ม", - "upgradeSteps": "วิธีอัพเกรดแผนของคุณบน AppFlowy:", - "step1": "1. ไปที่การตั้งค่า", - "step2": "2. คลิกที่ 'แผน'", - "step3": "3. เลือก 'เปลี่ยนแผน'", - "appNote": "บันทึก: ", - "actionButton": "อัพเกรด", - "downloadLink": "ดาวน์โหลดแอป", - "laterButton": "ภายหลัง", - "refreshNote": "หลังจากอัปเกรดสำเร็จแล้ว คลิก <refresh/> เพื่อเปิดใช้งานฟีเจอร์ใหม่ของคุณ", - "refresh": "ที่นี่" - }, - "breadcrumbs": { - "label": "เส้นทางนำทาง" - }, - "time": { - "justNow": "เมื่อสักครู่", - "seconds": { - "one": "1 วินาที", - "other": "{count} วินาที" - }, - "minutes": { - "one": "1 นาที", - "other": "{count} นาที" - }, - "hours": { - "one": "1 ชั่วโมง", - "other": "{count} ชั่วโมง" - }, - "days": { - "one": "1 วัน", - "other": "{จำนวน} วัน" - }, - "weeks": { - "one": "1 สัปดาห์", - "other": "{count} สัปดาห์" - }, - "months": { - "one": "1 เดือน", - "other": "{count} เดือน" - }, - "years": { - "one": "1 ปี", - "other": "{count} ปี" - }, - "ago": "ที่ผ่านมา", - "yesterday": "เมื่อวาน", - "today": "วันนี้" - }, - "members": { - "zero": "ไม่มีสมาชิก", - "one": "1 สมาชิก", - "many": "{count} สมาชิก", - "other": "{count} สมาชิก" - }, - "tabMenu": { - "close": "ปิด", - "closeOthers": "ปิดแท็บอื่น ๆ", - "favorite": "รายการโปรด", - "unfavorite": "ยกเลิกรายการโปรด", - "favoriteDisabledHint": "ไม่สามารถเพิ่มมุมมองนี้เป็นรายการโปรดได้", - "pinTab": "ปักหมุด", - "unpinTab": "ยกเลิกการปักหมุด" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 0eeac684c6..735234169d 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1,100 +1,80 @@ { "appName": "AppFlowy", - "defaultUsername": "Kullanıcı", - "welcomeText": "@:appName'ye Hoş Geldiniz", + "defaultUsername": "Ben", + "welcomeText": "@:appName'a Hoş Geldiniz", "welcomeTo": "Hoş Geldiniz", "githubStarText": "GitHub'da Yıldız Ver", - "subscribeNewsletterText": "Bültenimize Abone Ol", - "letsGoButtonText": "Hemen Başla", + "subscribeNewsletterText": "Bültene Abone Ol", + "letsGoButtonText": "Hızlı Başlangıç", "title": "Başlık", - "youCanAlso": "Ayrıca", + "youCanAlso": "Ayrıca yapabilirsiniz", "and": "ve", "failedToOpenUrl": "URL açılamadı: {}", "blockActions": { - "addBelowTooltip": "Altına eklemek için tıklayın", - "addAboveCmd": "Alt+tıklama", - "addAboveMacCmd": "Option+tıklama", - "addAboveTooltip": "Üstüne eklemek için", - "dragTooltip": "Sürükleyerek taşıyın", - "openMenuTooltip": "Menüyü aç" + "addBelowTooltip": "Alta eklemek için tıklayın", + "addAboveCmd": "Alt+tıkla", + "addAboveMacCmd": "Option+tıkla", + "addAboveTooltip": "üste eklemek için", + "dragTooltip": "Taşımak için sürükleyin", + "openMenuTooltip": "Menüyü açmak için tıklayın" }, "signUp": { "buttonText": "Kayıt Ol", - "title": "@:appName'e Kayıt Ol", + "title": "@:appName'a Kayıt Ol", "getStartedText": "Başlayın", - "emptyPasswordError": "Parola boş olamaz", - "repeatPasswordEmptyError": "Parola tekrarı alanı boş olamaz", - "unmatchedPasswordError": "Parola tekrarı, parola ile aynı değil", - "alreadyHaveAnAccount": "Hesabınız zaten var mı?", - "emailHint": "E-posta adresi", + "emptyPasswordError": "Parola boş bırakılamaz", + "repeatPasswordEmptyError": "Parola tekrarı boş bırakılamaz", + "unmatchedPasswordError": "Parola tekrarı girmiş olduğunuz parola ile aynı değil", + "alreadyHaveAnAccount": "Zaten bir hesabınız var mı?", + "emailHint": "E-posta", "passwordHint": "Parola", - "repeatPasswordHint": "Parolayı tekrarla", - "signUpWith": "Kayıt ol:" + "repeatPasswordHint": "Parolayı tekrar girin", + "signUpWith": "Şununla kayıt olun:" }, "signIn": { - "loginTitle": "@:appName'e Oturum Aç", - "loginButtonText": "Oturum Aç", + "loginTitle": "@:appName'a Giriş Yap", + "loginButtonText": "Giriş Yap", "loginStartWithAnonymous": "Anonim oturumla başla", "continueAnonymousUser": "Anonim oturumla devam et", - "anonymous": "Anonim", - "buttonText": "Oturum Aç", - "signingInText": "Oturum açılıyor...", - "forgotPassword": "Parolamı Unuttum?", - "emailHint": "E-posta adresi", + "buttonText": "Giriş Yap", + "signingInText": "Giriş yapılıyor...", + "forgotPassword": "Parolanızı mı unuttunuz?", + "emailHint": "E-posta", "passwordHint": "Parola", "dontHaveAnAccount": "Hesabınız yok mu?", - "createAccount": "Hesap oluştur", - "repeatPasswordEmptyError": "Parola tekrarı alanı boş bırakılamaz", - "unmatchedPasswordError": "Parola tekrarı parolayla eşleşmiyor", - "syncPromptMessage": "Verilerin senkronize edilmesi biraz zaman alabilir. Lütfen bu sayfayı kapatmayın", + "repeatPasswordEmptyError": "Parola tekrarı boş bırakılamaz", + "unmatchedPasswordError": "Parola tekrarı girmiş olduğunuz parola ile aynı değil", + "syncPromptMessage": "Veriler senkronize ediliyor, lütfen bu sayfayı kapatmayın", "or": "VEYA", - "signInWithGoogle": "Google ile devam et", - "signInWithGithub": "GitHub ile devam et", - "signInWithDiscord": "Discord ile devam et", - "signInWithApple": "Apple ile devam et", - "continueAnotherWay": "Başka yöntemle devam et", - "signUpWithGoogle": "Google ile kaydol", - "signUpWithGithub": "GitHub ile kaydol", - "signUpWithDiscord": "Discord ile kaydol", - "signInWith": "Devam et:", - "signInWithEmail": "E-posta ile devam et", - "signInWithMagicLink": "Devam et", - "signUpWithMagicLink": "Sihirli Bağlantı ile kaydol", + "signInWith": "Şununla giriş yapın:", + "signInWithEmail": "E-posta ile giriş yap", "pleaseInputYourEmail": "Lütfen e-posta adresinizi girin", - "settings": "Ayarlar", - "magicLinkSent": "Sihirli Bağlantı gönderildi!", - "invalidEmail": "Geçerli bir e-posta adresi girin", - "alreadyHaveAnAccount": "Zaten hesabınız var mı?", - "logIn": "Oturum Aç", - "generalError": "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.", - "limitRateError": "Güvenlik önlemi olarak, sihirli bağlantı talepleri 60 saniyede bir ile sınırlandırılmıştır.", - "magicLinkSentDescription": "E-posta adresinize sihirli bir bağlantı gönderdik. Giriş yapmak için bu bağlantıya tıklayın. Bağlantı 5 dakika içinde geçersiz hale gelecektir." + "magicLinkSent": "Sihirli bağlantı e-postanıza gönderildi, lütfen gelen kutunuzu kontrol edin", + "invalidEmail": "Lütfen geçerli bir e-posta adresi girin", + "LogInWithGoogle": "Google ile Giriş Yap", + "LogInWithGithub": "Github ile Giriş Yap", + "LogInWithDiscord": "Discord ile Giriş Yap", + "logInWithMagicLink": "Sihirli Bağlantı ile Giriş Yap" }, "workspace": { - "chooseWorkspace": "Çalışma alanınızı seçin", - "defaultName": "Çalışma Alanım", - "create": "Çalışma alanı oluştur", - "new": "Yeni çalışma alanı", - "importFromNotion": "Notion'dan içe aktar", - "learnMore": "Daha fazla bilgi", - "reset": "Çalışma alanını sıfırla", - "renameWorkspace": "Çalışma alanını yeniden adlandır", - "workspaceNameCannotBeEmpty": "Çalışma alanı adı boş olamaz", - "resetWorkspacePrompt": "Çalışma alanını sıfırlamak tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Geri yüklemek için destek ekibine ulaşabilirsiniz.", + "chooseWorkspace": "Çalışma Alanınızı Seçin", + "create": "Çalışma Alanı Oluştur", + "reset": "Çalışma Alanını Sıfırla", + "resetWorkspacePrompt": "Çalışma alanını sıfırlamak, içindeki tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Alternatif olarak, çalışma alanını geri yüklemek için destek ekibiyle iletişime geçebilirsiniz", "hint": "çalışma alanı", "notFoundError": "Çalışma alanı bulunamadı", - "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. @:appName'in açık olan tüm örneklerini kapatıp tekrar deneyin.", + "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm @:appName örneklerini kapatıp tekrar deneyin.", "errorActions": { - "reportIssue": "Hata bildir", - "reportIssueOnGithub": "GitHub'da hata bildir", + "reportIssue": "Sorun bildir", + "reportIssueOnGithub": "GitHub'da sorun bildir", "exportLogFiles": "Günlük dosyalarını dışa aktar", - "reachOut": "Discord'da iletişime geç" + "reachOut": "Discord'da ulaşın" }, "menuTitle": "Çalışma Alanları", - "deleteWorkspaceHintText": "Çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır.", + "deleteWorkspaceHintText": "Çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "createSuccess": "Çalışma alanı başarıyla oluşturuldu", "createFailed": "Çalışma alanı oluşturulamadı", - "createLimitExceeded": "Hesabınız için izin verilen maksimum çalışma alanı limitine ulaştınız. Çalışmanıza devam etmek için ek çalışma alanlarına ihtiyacınız varsa, lütfen GitHub'da talepte bulunun", + "createLimitExceeded": "Hesabınız için izin verilen maksimum çalışma alanı sınırına ulaştınız. Çalışmanıza devam etmek için ek çalışma alanlarına ihtiyacınız varsa, lütfen GitHub'da talepte bulunun", "deleteSuccess": "Çalışma alanı başarıyla silindi", "deleteFailed": "Çalışma alanı silinemedi", "openSuccess": "Çalışma alanı başarıyla açıldı", @@ -105,215 +85,119 @@ "updateIconFailed": "Çalışma alanı simgesi güncellenemedi", "cannotDeleteTheOnlyWorkspace": "Tek çalışma alanı silinemez", "fetchWorkspacesFailed": "Çalışma alanları getirilemedi", - "leaveCurrentWorkspace": "Çalışma alanından ayrıl", - "leaveCurrentWorkspacePrompt": "Mevcut çalışma alanından ayrılmak istediğinizden emin misiniz?" + "leaveCurrentWorkspace": "Çalışma alanından çık", + "leaveCurrentWorkspacePrompt": "Geçerli çalışma alanından çıkmak istediğinizden emin misiniz?" }, "shareAction": { "buttonText": "Paylaş", - "workInProgress": "Yakında", + "workInProgress": "Yakında geliyor", "markdown": "Markdown", "html": "HTML", "clipboard": "Panoya kopyala", "csv": "CSV", - "copyLink": "Bağlantıyı kopyala", - "publishToTheWeb": "Web'de Yayınla", - "publishToTheWebHint": "AppFlowy ile bir web sitesi oluşturun", - "publish": "Yayınla", - "unPublish": "Yayından kaldır", - "visitSite": "Siteyi ziyaret et", - "exportAsTab": "Farklı dışa aktar", - "publishTab": "Yayınla", - "shareTab": "Paylaş", - "publishOnAppFlowy": "AppFlowy'de Yayınla", - "shareTabTitle": "İşbirliği için davet et", - "shareTabDescription": "Herhangi biriyle kolay işbirliği için", - "copyLinkSuccess": "Bağlantı panoya kopyalandı", - "copyShareLink": "Paylaşım bağlantısını kopyala", - "copyLinkFailed": "Bağlantı panoya kopyalanamadı", - "copyLinkToBlockSuccess": "Blok bağlantısı panoya kopyalandı", - "copyLinkToBlockFailed": "Blok bağlantısı panoya kopyalanamadı", - "manageAllSites": "Tüm siteleri yönet", - "updatePathName": "Yol adını güncelle" + "copyLink": "Bağlantıyı Kopyala" }, "moreAction": { "small": "küçük", "medium": "orta", "large": "büyük", - "fontSize": "Yazı tipi boyutu", - "import": "İçe aktar", + "fontSize": "Yazı boyutu", + "import": "İçe Aktar", "moreOptions": "Daha fazla seçenek", "wordCount": "Kelime sayısı: {}", "charCount": "Karakter sayısı: {}", - "createdAt": "Oluşturulma: {}", + "createdAt": "Oluşturulma tarihi: {}", "deleteView": "Sil", - "duplicateView": "Çoğalt", - "wordCountLabel": "Kelime sayısı: ", - "charCountLabel": "Karakter sayısı: ", - "createdAtLabel": "Oluşturulma: ", - "syncedAtLabel": "Senkronize edilme: ", - "saveAsNewPage": "Mesajları sayfaya ekle" + "duplicateView": "Kopyala" }, "importPanel": { - "textAndMarkdown": "Metin ve Markdown", - "documentFromV010": "v0.1.0'dan belge", - "databaseFromV010": "v0.1.0'dan veritabanı", - "notionZip": "Notion Dışa Aktarılmış Zip Dosyası", + "textAndMarkdown": "Metin & Markdown", + "documentFromV010": "v0.1.0 Belgesi", + "databaseFromV010": "v0.1.0 Veritabanı", "csv": "CSV", "database": "Veritabanı" }, "disclosureAction": { - "rename": "Yeniden adlandır", + "rename": "Yeniden Adlandır", "delete": "Sil", - "duplicate": "Çoğalt", - "unfavorite": "Favorilerden kaldır", + "duplicate": "Kopyala", + "unfavorite": "Favorilerden çıkar", "favorite": "Favorilere ekle", "openNewTab": "Yeni sekmede aç", - "moveTo": "Şuraya taşı", - "addToFavorites": "Favorilere ekle", - "copyLink": "Bağlantıyı kopyala", - "changeIcon": "Simgeyi değiştir", - "collapseAllPages": "Tüm alt sayfaları daralt", - "movePageTo": "Sayfayı şuraya taşı", - "move": "Taşı" + "moveTo": "Taşı", + "addToFavorites": "Favorilere Ekle", + "copyLink": "Bağlantıyı Kopyala" }, "blankPageTitle": "Boş sayfa", "newPageText": "Yeni sayfa", "newDocumentText": "Yeni belge", - "newGridText": "Yeni ızgara", + "newGridText": "Yeni tablo", "newCalendarText": "Yeni takvim", "newBoardText": "Yeni pano", "chat": { - "newChat": "Yapay Zeka Sohbeti", - "inputMessageHint": "@:appName Yapay Zekasına sorun", - "inputLocalAIMessageHint": "@:appName Yerel Yapay Zekasına sorun", - "unsupportedCloudPrompt": "Bu özellik yalnızca @:appName Cloud kullanırken kullanılabilir", - "relatedQuestion": "Önerilen", - "serverUnavailable": "Bağlantı kesildi. Lütfen internet bağlantınızı kontrol edin ve", - "aiServerUnavailable": "Yapay zeka hizmeti geçici olarak kullanılamıyor. Lütfen daha sonra tekrar deneyin.", - "retry": "Tekrar dene", - "clickToRetry": "Tekrar denemek için tıklayın", - "regenerateAnswer": "Yeniden oluştur", - "question1": "Görevleri yönetmek için Kanban nasıl kullanılır", - "question2": "GTD yöntemini açıkla", - "question3": "Neden Rust kullanmalı", - "question4": "Mutfağımdaki malzemelerle tarif", - "question5": "Sayfam için bir illüstrasyon oluştur", - "question6": "Önümüzdeki hafta için yapılacaklar listesi hazırla", - "aiMistakePrompt": "Yapay zeka hata yapabilir. Önemli bilgileri kontrol edin.", - "chatWithFilePrompt": "Dosya ile sohbet etmek ister misiniz?", - "indexFileSuccess": "Dosya başarıyla indekslendi", - "inputActionNoPages": "Sayfa sonucu yok", - "referenceSource": { - "zero": "0 kaynak bulundu", - "one": "{count} kaynak bulundu", - "other": "{count} kaynak bulundu" - }, - "clickToMention": "Bir sayfadan bahset", - "uploadFile": "PDF, metin veya markdown dosyaları ekle", - "questionDetail": "Merhaba {}! Size bugün nasıl yardımcı olabilirim?", - "indexingFile": "{} indeksleniyor", - "generatingResponse": "Yanıt oluşturuluyor", - "selectSources": "Kaynakları Seç", - "sourcesLimitReached": "En fazla 3 üst düzey belge ve alt öğelerini seçebilirsiniz", - "sourceUnsupported": "Şu anda veritabanlarıyla sohbet etmeyi desteklemiyoruz", - "regenerate": "Tekrar dene", - "addToPageButton": "Mesajı sayfaya ekle", - "addToPageTitle": "Mesajı şuraya ekle...", - "addToNewPage": "Yeni sayfa oluştur", - "addToNewPageName": "\"{}\" kaynağından çıkarılan mesajlar", - "addToNewPageSuccessToast": "Mesaj şuraya eklendi:", - "openPagePreviewFailedToast": "Sayfa açılamadı", - "changeFormat": { - "actionButton": "Biçimi değiştir", - "confirmButton": "Bu biçimle yeniden oluştur", - "textOnly": "Metin", - "imageOnly": "Sadece görsel", - "textAndImage": "Metin ve Görsel", - "text": "Paragraf", - "bullet": "Madde işaretli liste", - "number": "Numaralı liste", - "table": "Tablo", - "blankDescription": "Yanıt biçimi", - "defaultDescription": "Otomatik mod", - "textWithImageDescription": "@:chat.changeFormat.text ve görsel", - "numberWithImageDescription": "@:chat.changeFormat.number ve görsel", - "bulletWithImageDescription": "@:chat.changeFormat.bullet ve görsel", - "tableWithImageDescription": "@:chat.changeFormat.table ve görsel" - }, - "selectBanner": { - "saveButton": "Şuraya ekle …", - "selectMessages": "Mesajları seç", - "nSelected": "{} seçildi", - "allSelected": "Tümü seçildi" - } + "relatedQuestion": "İlgili", + "serverUnavailable": "Hizmet Geçici Olarak Kullanılamıyor. Lütfen daha sonra tekrar deneyiniz." }, "trash": { "text": "Çöp Kutusu", "restoreAll": "Tümünü Geri Yükle", - "restore": "Geri Yükle", "deleteAll": "Tümünü Sil", "pageHeader": { "fileName": "Dosya adı", - "lastModified": "Son Değiştirilme", - "created": "Oluşturulma" + "lastModified": "Son Değiştirilme Tarihi", + "created": "Oluşturulma Tarihi" }, "confirmDeleteAll": { - "title": "Çöp kutusundaki tüm sayfalar", - "caption": "Çöp kutusundaki her şeyi silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." - }, - "confirmRestoreAll": { - "title": "Çöp kutusundaki tüm sayfaları geri yükle", + "title": "Çöp Kutusu'ndaki tüm sayfaları silmek istediğinizden emin misiniz?", "caption": "Bu işlem geri alınamaz." }, - "restorePage": { - "title": "Geri Yükle: {}", - "caption": "Bu sayfayı geri yüklemek istediğinizden emin misiniz?" + "confirmRestoreAll": { + "title": "Çöp Kutusu'ndaki tüm sayfaları geri yüklemek istediğinizden emin misiniz?", + "caption": "Bu işlem geri alınamaz." }, "mobile": { "actions": "Çöp Kutusu İşlemleri", - "empty": "Çöp kutusunda sayfa veya alan yok", - "emptyDescription": "İhtiyacınız olmayan şeyleri Çöp Kutusuna taşıyın.", + "empty": "Çöp Kutusu Boş", + "emptyDescription": "Silinmiş dosyanız yok", "isDeleted": "silindi", "isRestored": "geri yüklendi" }, "confirmDeleteTitle": "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz?" }, "deletePagePrompt": { - "text": "Bu sayfa Çöp Kutusunda", + "text": "Bu sayfa Çöp Kutusu'nda", "restore": "Sayfayı geri yükle", - "deletePermanent": "Kalıcı olarak sil", - "deletePermanentDescription": "Bu sayfayı kalıcı olarak silmek istediğinizden emin misiniz? Bu işlem geri alınamaz." + "deletePermanent": "Kalıcı olarak sil" }, "dialogCreatePageNameHint": "Sayfa adı", "questionBubble": { "shortcuts": "Kısayollar", - "whatsNew": "Yenilikler", + "whatsNew": "Yenilikler?", + "help": "Yardım & Destek", "markdown": "Markdown", "debug": { "name": "Hata Ayıklama Bilgisi", "success": "Hata ayıklama bilgisi panoya kopyalandı!", "fail": "Hata ayıklama bilgisi panoya kopyalanamadı" }, - "feedback": "Geri Bildirim", - "help": "Yardım ve Destek" + "feedback": "Geri Bildirim" }, "menuAppHeader": { "moreButtonToolTip": "Kaldır, yeniden adlandır ve daha fazlası...", - "addPageTooltip": "Hızlıca içeri sayfa ekle", - "defaultNewPageName": "Başlıksız", - "renameDialog": "Yeniden adlandır", - "pageNameSuffix": "Kopya" + "addPageTooltip": "İçeriye hızlıca bir sayfa ekle", + "defaultNewPageName": "İsimsiz", + "renameDialog": "Yeniden Adlandır" }, - "noPagesInside": "İçeride sayfa yok", + "noPagesInside": "İçinde sayfa yok", "toolbar": { - "undo": "Geri al", + "undo": "Geri Al", "redo": "Yinele", "bold": "Kalın", "italic": "İtalik", - "underline": "Altı çizili", - "strike": "Üstü çizili", - "numList": "Numaralı liste", - "bulletList": "Madde işaretli liste", + "underline": "Altı Çizili", + "strike": "Üstü Çizili", + "numList": "Numaralı Liste", + "bulletList": "Madde İşaretli Liste", "checkList": "Kontrol Listesi", "inlineCode": "Satır İçi Kod", "quote": "Alıntı Bloğu", @@ -324,21 +208,19 @@ "link": "Bağlantı" }, "tooltip": { - "lightMode": "Aydınlık moda geç", - "darkMode": "Karanlık moda geç", + "lightMode": "Açık moda geç", + "darkMode": "Koyu moda geç", "openAsPage": "Sayfa olarak aç", - "addNewRow": "Yeni satır ekle", + "addNewRow": "Yeni bir satır ekle", "openMenu": "Menüyü açmak için tıklayın", - "dragRow": "Satırı yeniden sıralamak için sürükleyin", + "dragRow": "Satırı yeniden sıralamak için uzun basın", "viewDataBase": "Veritabanını görüntüle", - "referencePage": "Bu {name} referans alındı", - "addBlockBelow": "Alta blok ekle", - "aiGenerate": "Oluştur" + "referencePage": "Bu {name} referans gösteriliyor", + "addBlockBelow": "Alta bir blok ekle" }, "sideBar": { "closeSidebar": "Kenar çubuğunu kapat", "openSidebar": "Kenar çubuğunu aç", - "expandSidebar": "Tam sayfa olarak genişlet", "personal": "Kişisel", "private": "Özel", "workspace": "Çalışma Alanı", @@ -346,52 +228,26 @@ "clickToHidePrivate": "Özel alanı gizlemek için tıklayın\nBurada oluşturduğunuz sayfalar yalnızca size görünür", "clickToHideWorkspace": "Çalışma alanını gizlemek için tıklayın\nBurada oluşturduğunuz sayfalar tüm üyelere görünür", "clickToHidePersonal": "Kişisel alanı gizlemek için tıklayın", - "clickToHideFavorites": "Favoriler alanını gizlemek için tıklayın", - "addAPage": "Yeni sayfa ekle", + "clickToHideFavorites": "Favori alanı gizlemek için tıklayın", + "addAPage": "Sayfa ekle", "addAPageToPrivate": "Özel alana sayfa ekle", "addAPageToWorkspace": "Çalışma alanına sayfa ekle", "recent": "Son", "today": "Bugün", - "thisWeek": "Bu hafta", - "others": "Önceki favoriler", - "earlier": "Daha önce", - "justNow": "az önce", + "thisWeek": "Bu Hafta", + "others": "Diğerleri", + "justNow": "Şu anda", "minutesAgo": "{count} dakika önce", - "lastViewed": "Son görüntüleme", - "favoriteAt": "Favorilere eklendi", - "emptyRecent": "Son Sayfa Yok", - "emptyRecentDescription": "Sayfaları görüntüledikçe, kolay erişim için burada listelenecekler.", - "emptyFavorite": "Favori Sayfa Yok", - "emptyFavoriteDescription": "Sayfaları favori olarak işaretleyin—hızlı erişim için burada listelenecekler!", - "removePageFromRecent": "Bu sayfayı Son'dan kaldır?", - "removeSuccess": "Başarıyla kaldırıldı", - "favoriteSpace": "Favoriler", - "RecentSpace": "Son", - "Spaces": "Alanlar", - "upgradeToPro": "Pro'ya yükselt", - "upgradeToAIMax": "Sınırsız yapay zekayı aç", - "storageLimitDialogTitle": "Ücretsiz depolama alanınız bitti. Sınırsız depolama için yükseltin", - "storageLimitDialogTitleIOS": "Ücretsiz depolama alanınız bitti.", - "aiResponseLimitTitle": "Ücretsiz yapay zeka yanıtlarınız bitti. Sınırsız yanıt için Pro Plana yükseltin veya bir yapay zeka eklentisi satın alın", - "aiResponseLimitDialogTitle": "Yapay zeka yanıt limitine ulaşıldı", - "aiResponseLimit": "Ücretsiz yapay zeka yanıtlarınız bitti.\n\nDaha fazla yapay zeka yanıtı almak için Ayarlar -> Plan -> AI Max veya Pro Plan'a tıklayın", - "askOwnerToUpgradeToPro": "Çalışma alanınızın ücretsiz depolama alanı bitiyor. Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin", - "askOwnerToUpgradeToProIOS": "Çalışma alanınızın ücretsiz depolama alanı bitiyor.", - "askOwnerToUpgradeToAIMax": "Çalışma alanınızın ücretsiz yapay zeka yanıtları bitti. Lütfen çalışma alanı sahibinden planı yükseltmesini veya yapay zeka eklentileri satın almasını isteyin", - "askOwnerToUpgradeToAIMaxIOS": "Çalışma alanınızın ücretsiz yapay zeka yanıtları bitiyor.", - "purchaseAIMax": "Çalışma alanınızın yapay zeka görsel yanıtları bitti. Lütfen çalışma alanı sahibinden AI Max satın almasını isteyin", - "aiImageResponseLimit": "Yapay zeka görsel yanıtlarınız bitti.\n\nDaha fazla yapay zeka görsel yanıtı almak için Ayarlar -> Plan -> AI Max'a tıklayın", - "purchaseStorageSpace": "Depolama Alanı Satın Al", - "singleFileProPlanLimitationDescription": "Ücretsiz planda izin verilen maksimum dosya yükleme boyutunu aştınız. Daha büyük dosyalar yüklemek için lütfen Pro Plana yükseltin", - "purchaseAIResponse": "Satın Al ", - "askOwnerToUpgradeToLocalAI": "Çalışma alanı sahibinden Cihaz Üzerinde Yapay Zekayı etkinleştirmesini isteyin", - "upgradeToAILocal": "En üst düzey gizlilik için yerel modelleri cihazınızda çalıştırın", - "upgradeToAILocalDesc": "Yerel yapay zeka kullanarak PDF'lerle sohbet edin, yazılarınızı geliştirin ve tabloları otomatik doldurun" + "lastViewed": "Son Görüntülenen", + "favoriteAt": "Favorilere Eklendi", + "emptyFavorite": "Favori Belge Yok", + "removeSuccess": "Başarıyla Kaldırıldı", + "favoriteSpace": "Favoriler" }, "notifications": { "export": { "markdown": "Not Markdown Olarak Dışa Aktarıldı", - "path": "Documents/flowy" + "path": "Belgeler/flowy" } }, "contactsPage": { @@ -411,7 +267,7 @@ "save": "Kaydet", "generate": "Oluştur", "esc": "ESC", - "keep": "Sakla", + "keep": "Tut", "tryAgain": "Tekrar dene", "discard": "Vazgeç", "replace": "Değiştir", @@ -420,22 +276,16 @@ "upload": "Yükle", "edit": "Düzenle", "delete": "Sil", - "copy": "Kopyala", - "duplicate": "Çoğalt", + "duplicate": "Kopyala", "putback": "Geri Koy", "update": "Güncelle", "share": "Paylaş", - "removeFromFavorites": "Favorilerden kaldır", - "removeFromRecent": "Son'dan kaldır", + "removeFromFavorites": "Favorilerden çıkar", "addToFavorites": "Favorilere ekle", - "favoriteSuccessfully": "Favorilere eklendi", - "unfavoriteSuccessfully": "Favorilerden kaldırıldı", - "duplicateSuccessfully": "Başarıyla çoğaltıldı", - "rename": "Yeniden adlandır", + "rename": "Yeniden Adlandır", "helpCenter": "Yardım Merkezi", "add": "Ekle", "yes": "Evet", - "no": "Hayır", "clear": "Temizle", "remove": "Kaldır", "dontRemove": "Kaldırma", @@ -445,631 +295,63 @@ "logout": "Çıkış yap", "deleteAccount": "Hesabı sil", "back": "Geri", - "signInGoogle": "Google ile devam et", - "signInGithub": "GitHub ile devam et", - "signInDiscord": "Discord ile devam et", - "more": "Daha fazla", - "create": "Oluştur", - "close": "Kapat", - "next": "İleri", - "previous": "Geri", - "submit": "Gönder", - "download": "İndir", - "backToHome": "Ana Sayfaya Dön", - "viewing": "Görüntüleme", - "editing": "Düzenleme", - "gotIt": "Anladım", - "retry": "Tekrar dene", - "uploadFailed": "Yükleme başarısız.", - "copyLinkOriginal": "Orijinal bağlantıyı kopyala" + "signInGoogle": "Google ile giriş yap", + "signInGithub": "Github ile giriş yap", + "signInDiscord": "Discord ile giriş yap" }, "label": { "welcome": "Hoş Geldiniz!", "firstName": "Ad", "middleName": "İkinci Ad", "lastName": "Soyad", - "stepX": "Adım {X}" + "stepX": "{X}. Adım" }, "oAuth": { "err": { "failedTitle": "Hesabınıza bağlanılamıyor.", - "failedMsg": "Lütfen tarayıcınızda oturum açma işlemini tamamladığınızdan emin olun." + "failedMsg": "Lütfen tarayıcınızda giriş işlemini tamamladığınızdan emin olun." }, "google": { - "title": "GOOGLE İLE GİRİŞ", - "instruction1": "Google Kişilerinizi içe aktarmak için, bu uygulamaya web tarayıcınızı kullanarak yetki vermeniz gerekecek.", + "title": "GOOGLE GİRİŞİ", + "instruction1": "Google Kişilerinizi içe aktarmak için, bu uygulamayı web tarayıcınızdan yetkilendirmeniz gerekecek.", "instruction2": "Simgeye tıklayarak veya metni seçerek bu kodu panonuza kopyalayın:", "instruction3": "Web tarayıcınızda aşağıdaki bağlantıya gidin ve yukarıdaki kodu girin:", - "instruction4": "Kaydı tamamladığınızda aşağıdaki düğmeye basın:" + "instruction4": "Kayıt işlemini tamamladığınızda aşağıdaki butona basın:" } }, "settings": { "title": "Ayarlar", - "popupMenuItem": { - "settings": "Ayarlar", - "members": "Üyeler", - "trash": "Çöp Kutusu", - "helpAndSupport": "Yardım ve Destek" - }, - "sites": { - "title": "Siteler", - "namespaceTitle": "Alan Adı", - "namespaceDescription": "Alan adınızı ve ana sayfanızı yönetin", - "namespaceHeader": "Alan Adı", - "homepageHeader": "Ana Sayfa", - "updateNamespace": "Alan adını güncelle", - "removeHomepage": "Ana sayfayı kaldır", - "selectHomePage": "Bir sayfa seç", - "clearHomePage": "Bu alan adı için ana sayfayı temizle", - "customUrl": "Özel URL", - "namespace": { - "description": "Bu değişiklik, bu alan adında yayınlanan tüm canlı sayfalara uygulanacak", - "tooltip": "Uygunsuz alan adlarını kaldırma hakkını saklı tutarız", - "updateExistingNamespace": "Mevcut alan adını güncelle", - "upgradeToPro": "Ana sayfa ayarlamak için Pro Plana yükseltin", - "redirectToPayment": "Ödeme sayfasına yönlendiriliyor...", - "onlyWorkspaceOwnerCanSetHomePage": "Yalnızca çalışma alanı sahibi ana sayfa ayarlayabilir", - "pleaseAskOwnerToSetHomePage": "Lütfen çalışma alanı sahibinden Pro Plana yükseltmesini isteyin" - }, - "publishedPage": { - "title": "Tüm yayınlanan sayfalar", - "description": "Yayınlanan sayfalarınızı yönetin", - "page": "Sayfa", - "pathName": "Yol adı", - "date": "Yayınlanma tarihi", - "emptyHinText": "Bu çalışma alanında yayınlanmış sayfanız yok", - "noPublishedPages": "Yayınlanmış sayfa yok", - "settings": "Yayın ayarları", - "clickToOpenPageInApp": "Sayfayı uygulamada aç", - "clickToOpenPageInBrowser": "Sayfayı tarayıcıda aç" - }, - "error": { - "failedToGeneratePaymentLink": "Pro Plan için ödeme bağlantısı oluşturulamadı", - "failedToUpdateNamespace": "Alan adı güncellenemedi", - "proPlanLimitation": "Alan adını güncellemek için Pro Plana yükseltmeniz gerekiyor", - "namespaceAlreadyInUse": "Bu alan adı zaten alınmış, lütfen başka bir tane deneyin", - "invalidNamespace": "Geçersiz alan adı, lütfen başka bir tane deneyin", - "namespaceLengthAtLeast2Characters": "Alan adı en az 2 karakter uzunluğunda olmalıdır", - "onlyWorkspaceOwnerCanUpdateNamespace": "Alan adını yalnızca çalışma alanı sahibi güncelleyebilir", - "onlyWorkspaceOwnerCanRemoveHomepage": "Ana sayfayı yalnızca çalışma alanı sahibi kaldırabilir", - "setHomepageFailed": "Ana sayfa ayarlanamadı", - "namespaceTooLong": "Alan adı çok uzun, lütfen başka bir tane deneyin", - "namespaceTooShort": "Alan adı çok kısa, lütfen başka bir tane deneyin", - "namespaceIsReserved": "Bu alan adı rezerve edilmiş, lütfen başka bir tane deneyin", - "updatePathNameFailed": "Yol adı güncellenemedi", - "removeHomePageFailed": "Ana sayfa kaldırılamadı", - "publishNameContainsInvalidCharacters": "Yol adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin", - "publishNameTooShort": "Yol adı çok kısa, lütfen başka bir tane deneyin", - "publishNameTooLong": "Yol adı çok uzun, lütfen başka bir tane deneyin", - "publishNameAlreadyInUse": "Bu yol adı zaten kullanımda, lütfen başka bir tane deneyin", - "namespaceContainsInvalidCharacters": "Alan adı geçersiz karakter(ler) içeriyor, lütfen başka bir tane deneyin", - "publishPermissionDenied": "Yayın ayarlarını yalnızca çalışma alanı sahibi veya sayfa yayıncısı yönetebilir", - "publishNameCannotBeEmpty": "Yol adı boş olamaz, lütfen başka bir tane deneyin" - }, - "success": { - "namespaceUpdated": "Alan adı başarıyla güncellendi", - "setHomepageSuccess": "Ana sayfa başarıyla ayarlandı", - "updatePathNameSuccess": "Yol adı başarıyla güncellendi", - "removeHomePageSuccess": "Ana sayfa başarıyla kaldırıldı" - } - }, "accountPage": { - "menuLabel": "Hesabım", - "title": "Hesabım", "general": { - "title": "Hesap adı ve profil resmi", - "changeProfilePicture": "Profil resmini değiştir" + "title": "Hesap adı & profil resmi", + "changeProfilePicture": "Profil Resmi Değiştir" }, "email": { - "title": "E-posta", + "title": "Mail", "actions": { - "change": "E-postayı değiştir" + "change": "Email Değiş" } }, + "keys": { + "openAIHint": "OpenAI API Anahtarını Gir" + }, "login": { - "title": "Hesap girişi", - "loginLabel": "Giriş yap", - "logoutLabel": "Çıkış yap" + "loginLabel": "Giriş Yap", + "logoutLabel": "Çıkış Yap" } }, "workspacePage": { - "menuLabel": "Çalışma Alanı", - "title": "Çalışma Alanı", - "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih/saat biçimini ve dilini özelleştirin.", - "workspaceName": { - "title": "Çalışma alanı adı" - }, - "workspaceIcon": { - "title": "Çalışma alanı simgesi", - "description": "Çalışma alanınız için bir resim yükleyin veya emoji kullanın. Simge, kenar çubuğunuzda ve bildirimlerinizde görünecektir." - }, "appearance": { "title": "Görünüm", - "description": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.", "options": { - "system": "Otomatik", - "light": "Aydınlık", - "dark": "Karanlık" + "light": "Açık", + "dark": "Koyu" } }, - "resetCursorColor": { - "title": "Belge imleç rengini sıfırla", - "description": "İmleç rengini sıfırlamak istediğinizden emin misiniz?" - }, - "resetSelectionColor": { - "title": "Belge seçim rengini sıfırla", - "description": "Seçim rengini sıfırlamak istediğinizden emin misiniz?" - }, - "resetWidth": { - "resetSuccess": "Belge genişliği başarıyla sıfırlandı" - }, "theme": { - "title": "Tema", - "description": "Önceden ayarlanmış bir tema seçin veya kendi özel temanızı yükleyin.", - "uploadCustomThemeTooltip": "Özel tema yükle" - }, - "workspaceFont": { - "title": "Çalışma alanı yazı tipi", - "noFontHint": "Yazı tipi bulunamadı, başka bir terim deneyin." - }, - "textDirection": { - "title": "Metin yönü", - "leftToRight": "Soldan sağa", - "rightToLeft": "Sağdan sola", - "auto": "Otomatik", - "enableRTLItems": "RTL araç çubuğu öğelerini etkinleştir" - }, - "layoutDirection": { - "title": "Düzen yönü", - "leftToRight": "Soldan sağa", - "rightToLeft": "Sağdan sola" - }, - "dateTime": { - "title": "Tarih ve saat", - "example": "{} {} ({})", - "24HourTime": "24 saat biçimi", - "dateFormat": { - "label": "Tarih biçimi", - "local": "Yerel", - "us": "ABD", - "iso": "ISO", - "friendly": "Kullanıcı dostu", - "dmy": "G/A/Y" - } - }, - "language": { - "title": "Dil" - }, - "deleteWorkspacePrompt": { - "title": "Çalışma alanını sil", - "content": "Bu çalışma alanını silmek istediğinizden emin misiniz? Bu işlem geri alınamaz ve yayınladığınız tüm sayfaların yayını kaldırılacaktır." - }, - "leaveWorkspacePrompt": { - "title": "Çalışma alanından ayrıl", - "content": "Bu çalışma alanından ayrılmak istediğinizden emin misiniz? İçindeki tüm sayfalara ve verilere erişiminizi kaybedeceksiniz.", - "success": "Çalışma alanından başarıyla ayrıldınız.", - "fail": "Çalışma alanından ayrılınamadı." - }, - "manageWorkspace": { - "title": "Çalışma alanını yönet", - "leaveWorkspace": "Çalışma alanından ayrıl", - "deleteWorkspace": "Çalışma alanını sil" + "title": "Tema" } }, - "manageDataPage": { - "menuLabel": "Verileri yönet", - "title": "Verileri yönet", - "description": "Yerel depolama verilerini yönetin veya mevcut verilerinizi @:appName'e aktarın.", - "dataStorage": { - "title": "Dosya depolama konumu", - "tooltip": "Dosyalarınızın depolandığı konum", - "actions": { - "change": "Yolu değiştir", - "open": "Klasörü aç", - "openTooltip": "Mevcut veri klasörü konumunu aç", - "copy": "Yolu kopyala", - "copiedHint": "Yol kopyalandı!", - "resetTooltip": "Varsayılan konuma sıfırla" - }, - "resetDialog": { - "title": "Emin misiniz?", - "description": "Yolu varsayılan veri konumuna sıfırlamak verilerinizi silmeyecektir. Mevcut verilerinizi yeniden içe aktarmak istiyorsanız, önce mevcut konumunuzun yolunu kopyalamalısınız." - } - }, - "importData": { - "title": "Veri içe aktar", - "tooltip": "@:appName yedeklerinden/veri klasörlerinden veri içe aktar", - "description": "Harici bir @:appName veri klasöründen veri kopyala", - "action": "Dosyaya göz at" - }, - "encryption": { - "title": "Şifreleme", - "tooltip": "Verilerinizin nasıl depolandığını ve şifrelendiğini yönetin", - "descriptionNoEncryption": "Şifrelemeyi açmak tüm verileri şifreleyecektir. Bu işlem geri alınamaz.", - "descriptionEncrypted": "Verileriniz şifrelenmiş.", - "action": "Verileri şifrele", - "dialog": { - "title": "Tüm verileriniz şifrelensin mi?", - "description": "Tüm verilerinizi şifrelemek, verilerinizi güvenli ve emniyetli tutacaktır. Bu işlem GERİ ALINAMAZ. Devam etmek istediğinizden emin misiniz?" - } - }, - "cache": { - "title": "Önbelleği temizle", - "description": "Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.", - "dialog": { - "title": "Önbelleği temizle", - "description": "Görsel yüklenmemesi, bir alanda eksik sayfalar ve yazı tiplerinin yüklenmemesi gibi sorunları çözmeye yardımcı olur. Bu, verilerinizi etkilemeyecektir.", - "successHint": "Önbellek temizlendi!" - } - }, - "data": { - "fixYourData": "Verilerinizi düzeltin", - "fixButton": "Düzelt", - "fixYourDataDescription": "Verilerinizle ilgili sorunlar yaşıyorsanız, burada düzeltmeyi deneyebilirsiniz." - } - }, - "shortcutsPage": { - "menuLabel": "Kısayollar", - "title": "Kısayollar", - "editBindingHint": "Yeni bağlama girin", - "searchHint": "Ara", - "actions": { - "resetDefault": "Varsayılana sıfırla" - }, - "errorPage": { - "message": "Kısayollar yüklenemedi: {}", - "howToFix": "Lütfen tekrar deneyin, sorun devam ederse GitHub üzerinden bize ulaşın." - }, - "resetDialog": { - "title": "Kısayolları sıfırla", - "description": "Bu işlem tüm tuş bağlamalarınızı varsayılana sıfırlayacak, daha sonra geri alamazsınız, devam etmek istediğinizden emin misiniz?", - "buttonLabel": "Sıfırla" - }, - "conflictDialog": { - "title": "{} şu anda kullanımda", - "descriptionPrefix": "Bu tuş bağlaması şu anda ", - "descriptionSuffix": " tarafından kullanılıyor. Bu tuş bağlamasını değiştirirseniz, {} üzerinden kaldırılacak.", - "confirmLabel": "Devam et" - }, - "editTooltip": "Tuş bağlamasını düzenlemeye başlamak için basın", - "keybindings": { - "toggleToDoList": "Yapılacaklar listesini aç/kapat", - "insertNewParagraphInCodeblock": "Yeni paragraf ekle", - "pasteInCodeblock": "Kod bloğuna yapıştır", - "selectAllCodeblock": "Tümünü seç", - "indentLineCodeblock": "Satır başına iki boşluk ekle", - "outdentLineCodeblock": "Satır başından iki boşluk sil", - "twoSpacesCursorCodeblock": "İmleç konumuna iki boşluk ekle", - "copy": "Seçimi kopyala", - "paste": "İçeriği yapıştır", - "cut": "Seçimi kes", - "alignLeft": "Metni sola hizala", - "alignCenter": "Metni ortala", - "alignRight": "Metni sağa hizala", - "undo": "Geri al", - "redo": "Yinele", - "convertToParagraph": "Bloğu paragrafa dönüştür", - "backspace": "Sil", - "deleteLeftWord": "Sol kelimeyi sil", - "deleteLeftSentence": "Sol cümleyi sil", - "delete": "Sağdaki karakteri sil", - "deleteMacOS": "Soldaki karakteri sil", - "deleteRightWord": "Sağdaki kelimeyi sil", - "moveCursorLeft": "İmleci sola taşı", - "moveCursorBeginning": "İmleci başa taşı", - "moveCursorLeftWord": "İmleci bir kelime sola taşı", - "moveCursorLeftSelect": "Seç ve imleci sola taşı", - "moveCursorBeginSelect": "Seç ve imleci başa taşı", - "moveCursorLeftWordSelect": "Seç ve imleci bir kelime sola taşı", - "moveCursorRight": "İmleci sağa taşı", - "moveCursorEnd": "İmleci sona taşı", - "moveCursorRightWord": "İmleci bir kelime sağa taşı", - "moveCursorRightSelect": "Seç ve imleci sağa taşı", - "moveCursorEndSelect": "Seç ve imleci sona taşı", - "moveCursorRightWordSelect": "Seç ve imleci bir kelime sağa taşı", - "moveCursorUp": "İmleci yukarı taşı", - "moveCursorTopSelect": "Seç ve imleci en üste taşı", - "moveCursorTop": "İmleci en üste taşı", - "moveCursorUpSelect": "Seç ve imleci yukarı taşı", - "moveCursorBottomSelect": "Seç ve imleci en alta taşı", - "moveCursorBottom": "İmleci en alta taşı", - "moveCursorDown": "İmleci aşağı taşı", - "moveCursorDownSelect": "Seç ve imleci aşağı taşı", - "home": "En üste kaydır", - "end": "En alta kaydır", - "toggleBold": "Kalın yazıyı aç/kapat", - "toggleItalic": "İtalik yazıyı aç/kapat", - "toggleUnderline": "Altı çizili yazıyı aç/kapat", - "toggleStrikethrough": "Üstü çizili yazıyı aç/kapat", - "toggleCode": "Satır içi kodu aç/kapat", - "toggleHighlight": "Vurgulamayı aç/kapat", - "showLinkMenu": "Bağlantı menüsünü göster", - "openInlineLink": "Satır içi bağlantıyı aç", - "openLinks": "Seçili tüm bağlantıları aç", - "indent": "Girinti ekle", - "outdent": "Girintiyi azalt", - "exit": "Düzenlemeden çık", - "pageUp": "Bir sayfa yukarı kaydır", - "pageDown": "Bir sayfa aşağı kaydır", - "selectAll": "Tümünü seç", - "pasteWithoutFormatting": "İçeriği biçimlendirme olmadan yapıştır", - "showEmojiPicker": "Emoji seçiciyi göster", - "enterInTableCell": "Tabloda satır sonu ekle", - "leftInTableCell": "Tabloda bir hücre sola git", - "rightInTableCell": "Tabloda bir hücre sağa git", - "upInTableCell": "Tabloda bir hücre yukarı git", - "downInTableCell": "Tabloda bir hücre aşağı git", - "tabInTableCell": "Tabloda sonraki kullanılabilir hücreye git", - "shiftTabInTableCell": "Tabloda önceki kullanılabilir hücreye git", - "backSpaceInTableCell": "Hücrenin başında dur" - }, - "commands": { - "codeBlockNewParagraph": "Kod bloğunun yanına yeni bir paragraf ekle", - "codeBlockIndentLines": "Kod bloğunda satır başına iki boşluk ekle", - "codeBlockOutdentLines": "Kod bloğunda satır başından iki boşluk sil", - "codeBlockAddTwoSpaces": "Kod bloğunda imleç konumuna iki boşluk ekle", - "codeBlockSelectAll": "Kod bloğu içindeki tüm içeriği seç", - "codeBlockPasteText": "Kod bloğuna metin yapıştır", - "textAlignLeft": "Metni sola hizala", - "textAlignCenter": "Metni ortala", - "textAlignRight": "Metni sağa hizala" - }, - "couldNotLoadErrorMsg": "Kısayollar yüklenemedi, tekrar deneyin", - "couldNotSaveErrorMsg": "Kısayollar kaydedilemedi, tekrar deneyin" - }, - "aiPage": { - "title": "Yapay Zeka Ayarları", - "menuLabel": "Yapay Zeka Ayarları", - "keys": { - "enableAISearchTitle": "Yapay Zeka Arama", - "aiSettingsDescription": "AppFlowy Yapay Zeka'yı güçlendirmek için tercih ettiğiniz modeli seçin. Şu anda GPT 4-o, Claude 3,5, Llama 3.1 ve Mistral 7B içerir", - "loginToEnableAIFeature": "Yapay Zeka özellikleri yalnızca @:appName Cloud ile giriş yaptıktan sonra etkinleştirilir. Bir @:appName hesabınız yoksa, kaydolmak için 'Hesabım'a gidin", - "llmModel": "Dil Modeli", - "llmModelType": "Dil Modeli Türü", - "downloadLLMPrompt": "{} İndir", - "downloadAppFlowyOfflineAI": "Yapay Zeka çevrimdışı paketini indirmek, Yapay Zeka'nın cihazınızda çalışmasını sağlayacak. Devam etmek istiyor musunuz?", - "downloadLLMPromptDetail": "{} yerel modelini indirmek {} depolama alanı kullanacak. Devam etmek istiyor musunuz?", - "downloadBigFilePrompt": "İndirmenin tamamlanması yaklaşık 10 dakika sürebilir", - "downloadAIModelButton": "İndir", - "downloadingModel": "İndiriliyor", - "localAILoaded": "Yerel Yapay Zeka Modeli başarıyla eklendi ve kullanıma hazır", - "localAIStart": "Yerel Yapay Zeka Sohbeti başlatılıyor...", - "localAILoading": "Yerel Yapay Zeka Sohbet Modeli yükleniyor...", - "localAIStopped": "Yerel Yapay Zeka durduruldu", - "failToLoadLocalAI": "Yerel Yapay Zeka başlatılamadı", - "restartLocalAI": "Yerel Yapay Zeka'yı Yeniden Başlat", - "disableLocalAITitle": "Yerel Yapay Zeka'yı devre dışı bırak", - "disableLocalAIDescription": "Yerel Yapay Zeka'yı devre dışı bırakmak istiyor musunuz?", - "localAIToggleTitle": "Yerel Yapay Zeka'yı etkinleştirmek veya devre dışı bırakmak için değiştirin", - "offlineAIInstruction1": "Çevrimdışı Yapay Zeka'yı etkinleştirmek için", - "offlineAIInstruction2": "talimatları", - "offlineAIInstruction3": "takip edin.", - "offlineAIDownload1": "AppFlowy Yapay Zeka'yı henüz indirmediyseniz, lütfen", - "offlineAIDownload2": "indirin", - "offlineAIDownload3": "önce", - "activeOfflineAI": "Etkin", - "downloadOfflineAI": "İndir", - "openModelDirectory": "Klasörü aç" - } - }, - "planPage": { - "menuLabel": "Plan", - "title": "Fiyatlandırma planı", - "planUsage": { - "title": "Plan kullanım özeti", - "storageLabel": "Depolama", - "storageUsage": "{} / {} GB", - "unlimitedStorageLabel": "Sınırsız depolama", - "collaboratorsLabel": "Üyeler", - "collaboratorsUsage": "{} / {}", - "aiResponseLabel": "Yapay Zeka Yanıtları", - "aiResponseUsage": "{} / {}", - "unlimitedAILabel": "Sınırsız yanıt", - "proBadge": "Pro", - "aiMaxBadge": "Yapay Zeka Max", - "aiOnDeviceBadge": "Mac için Cihaz Üzerinde Yapay Zeka", - "memberProToggle": "Daha fazla üye ve sınırsız Yapay Zeka", - "aiMaxToggle": "Sınırsız Yapay Zeka ve gelişmiş modellere erişim", - "aiOnDeviceToggle": "Maksimum gizlilik için yerel Yapay Zeka", - "aiCredit": { - "title": "@:appName Yapay Zeka Kredisi Ekle", - "price": "{}", - "priceDescription": "1.000 kredi için", - "purchase": "Yapay Zeka Satın Al", - "info": "Çalışma alanı başına 1.000 Yapay Zeka kredisi ekleyin ve özelleştirilebilir Yapay Zeka'yı iş akışınıza sorunsuz bir şekilde entegre ederek daha akıllı, daha hızlı sonuçlar elde edin:", - "infoItemOne": "Veritabanı başına 10.000 yanıt", - "infoItemTwo": "Çalışma alanı başına 1.000 yanıt" - }, - "currentPlan": { - "bannerLabel": "Mevcut plan", - "freeTitle": "Ücretsiz", - "proTitle": "Pro", - "teamTitle": "Takım", - "freeInfo": "2 üyeye kadar bireyler için her şeyi düzenlemek için mükemmel", - "proInfo": "10 üyeye kadar küçük ve orta ölçekli takımlar için mükemmel.", - "teamInfo": "Tüm üretken ve iyi organize edilmiş takımlar için mükemmel.", - "upgrade": "Planı değiştir", - "canceledInfo": "Planınız iptal edildi, {} tarihinde Ücretsiz plana düşürüleceksiniz." - }, - "addons": { - "title": "Eklentiler", - "addLabel": "Ekle", - "activeLabel": "Eklendi", - "aiMax": { - "title": "Yapay Zeka Max", - "description": "Gelişmiş Yapay Zeka modelleri tarafından desteklenen sınırsız Yapay Zeka yanıtları ve ayda 50 Yapay Zeka görüntüsü", - "price": "{}", - "priceInfo": "Kullanıcı başına aylık, yıllık faturalandırma" - }, - "aiOnDevice": { - "title": "Mac için Cihaz Üzerinde Yapay Zeka", - "description": "Mistral 7B, LLAMA 3 ve daha fazla yerel modeli makinenizde çalıştırın", - "price": "{}", - "priceInfo": "Kullanıcı başına aylık, yıllık faturalandırma", - "recommend": "M1 veya daha yenisi önerilir" - } - }, - "deal": { - "bannerLabel": "Yeni yıl fırsatı!", - "title": "Takımınızı büyütün!", - "info": "Yükseltin ve Pro ve Takım planlarında %10 indirim kazanın! @:appName Yapay Zeka dahil güçlü yeni özelliklerle çalışma alanı verimliliğinizi artırın.", - "viewPlans": "Planları görüntüle" - } - } - }, - "billingPage": { - "menuLabel": "Faturalandırma", - "title": "Faturalandırma", - "plan": { - "title": "Plan", - "freeLabel": "Ücretsiz", - "proLabel": "Pro", - "planButtonLabel": "Planı değiştir", - "billingPeriod": "Faturalandırma dönemi", - "periodButtonLabel": "Dönemi düzenle" - }, - "paymentDetails": { - "title": "Ödeme detayları", - "methodLabel": "Ödeme yöntemi", - "methodButtonLabel": "Yöntemi düzenle" - }, - "addons": { - "title": "Eklentiler", - "addLabel": "Ekle", - "removeLabel": "Kaldır", - "renewLabel": "Yenile", - "aiMax": { - "label": "Yapay Zeka Max", - "description": "Sınırsız Yapay Zeka ve gelişmiş modellerin kilidini açın", - "activeDescription": "Sonraki fatura tarihi: {}", - "canceledDescription": "Yapay Zeka Max {} tarihine kadar kullanılabilir olacak" - }, - "aiOnDevice": { - "label": "Mac için Cihaz Üzerinde Yapay Zeka", - "description": "Cihazınızda sınırsız Cihaz Üzerinde Yapay Zeka'nın kilidini açın", - "activeDescription": "Sonraki fatura tarihi: {}", - "canceledDescription": "Mac için Cihaz Üzerinde Yapay Zeka {} tarihine kadar kullanılabilir olacak" - }, - "removeDialog": { - "title": "{} Kaldır", - "description": "{plan} planını kaldırmak istediğinizden emin misiniz? {plan} planının özelliklerine ve avantajlarına erişiminizi hemen kaybedeceksiniz." - } - }, - "currentPeriodBadge": "MEVCUT", - "changePeriod": "Dönemi değiştir", - "planPeriod": "{} dönemi", - "monthlyInterval": "Aylık", - "monthlyPriceInfo": "koltuk başına aylık faturalandırma", - "annualInterval": "Yıllık", - "annualPriceInfo": "koltuk başına yıllık faturalandırma" - }, - "comparePlanDialog": { - "title": "Plan karşılaştır ve seç", - "planFeatures": "Plan\nÖzellikleri", - "current": "Mevcut", - "actions": { - "upgrade": "Yükselt", - "downgrade": "Düşür", - "current": "Mevcut" - }, - "freePlan": { - "title": "Ücretsiz", - "description": "2 üyeye kadar bireyler için her şeyi düzenlemek için", - "price": "{}", - "priceInfo": "Sonsuza kadar ücretsiz" - }, - "proPlan": { - "title": "Pro", - "description": "Küçük takımların projeleri ve takım bilgisini yönetmesi için", - "price": "{}", - "priceInfo": "Kullanıcı başına aylık \nyıllık faturalandırma\n\n{} aylık faturalandırma" - }, - "planLabels": { - "itemOne": "Çalışma Alanları", - "itemTwo": "Üyeler", - "itemThree": "Depolama", - "itemFour": "Gerçek zamanlı işbirliği", - "itemFive": "Mobil uygulama", - "itemSix": "Yapay Zeka Yanıtları", - "itemFileUpload": "Dosya yüklemeleri", - "customNamespace": "Özel alan adı", - "tooltipSix": "Ömür boyu demek, yanıt sayısının asla sıfırlanmayacağı anlamına gelir", - "intelligentSearch": "Akıllı arama", - "tooltipSeven": "Çalışma alanınızın URL'sinin bir kısmını özelleştirmenize olanak tanır", - "customNamespaceTooltip": "Özel yayınlanmış site URL'si" - }, - "freeLabels": { - "itemOne": "Çalışma alanı başına ücretlendirilir", - "itemTwo": "2'ye kadar", - "itemThree": "5 GB", - "itemFour": "evet", - "itemFive": "evet", - "itemSix": "10 ömür boyu", - "itemFileUpload": "7 MB'a kadar", - "intelligentSearch": "Akıllı arama" - }, - "proLabels": { - "itemOne": "Çalışma alanı başına ücretlendirilir", - "itemTwo": "10'a kadar", - "itemThree": "Sınırsız", - "itemFour": "evet", - "itemFive": "evet", - "itemSix": "Sınırsız", - "itemFileUpload": "Sınırsız", - "intelligentSearch": "Akıllı arama" - }, - "paymentSuccess": { - "title": "Artık {} planındasınız!", - "description": "Ödemeniz başarıyla işleme alındı ve planınız @:appName {}'e yükseltildi. Plan detaylarınızı Plan sayfasında görüntüleyebilirsiniz" - }, - "downgradeDialog": { - "title": "Planınızı düşürmek istediğinizden emin misiniz?", - "description": "Planınızı düşürmek sizi Ücretsiz plana geri döndürecek. Üyeler bu çalışma alanına erişimlerini kaybedebilir ve Ücretsiz planın depolama sınırlarına uymak için alan açmanız gerekebilir.", - "downgradeLabel": "Planı düşür" - } - }, - "cancelSurveyDialog": { - "title": "Gitmenize üzüldük", - "description": "Gitmenize üzüldük. @:appName'i geliştirmemize yardımcı olmak için geri bildiriminizi duymak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.", - "commonOther": "Diğer", - "otherHint": "Yanıtınızı buraya yazın", - "questionOne": { - "question": "@:appName Pro aboneliğinizi iptal etmenize ne sebep oldu?", - "answerOne": "Maliyet çok yüksek", - "answerTwo": "Özellikler beklentileri karşılamadı", - "answerThree": "Daha iyi bir alternatif buldum", - "answerFour": "Maliyeti haklı çıkaracak kadar kullanmadım", - "answerFive": "Hizmet sorunu veya teknik zorluklar" - }, - "questionTwo": { - "question": "Gelecekte @:appName Pro'ya yeniden abone olma olasılığınız nedir?", - "answerOne": "Çok muhtemel", - "answerTwo": "Biraz muhtemel", - "answerThree": "Emin değilim", - "answerFour": "Muhtemel değil", - "answerFive": "Hiç muhtemel değil" - }, - "questionThree": { - "question": "Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?", - "answerOne": "Çoklu kullanıcı işbirliği", - "answerTwo": "Daha uzun süreli versiyon geçmişi", - "answerThree": "Sınırsız Yapay Zeka yanıtları", - "answerFour": "Yerel Yapay Zeka modellerine erişim" - }, - "questionFour": { - "question": "@:appName ile genel deneyiminizi nasıl tanımlarsınız?", - "answerOne": "Harika", - "answerTwo": "İyi", - "answerThree": "Ortalama", - "answerFour": "Ortalamanın altında", - "answerFive": "Memnun değilim" - } - }, - "common": { - "uploadingFile": "Dosya yükleniyor. Lütfen uygulamadan çıkmayın", - "uploadNotionSuccess": "Notion zip dosyanız başarıyla yüklendi. İçe aktarma tamamlandığında bir onay e-postası alacaksınız", - "reset": "Sıfırla" - }, "menu": { "appearance": "Görünüm", "language": "Dil", @@ -1079,128 +361,76 @@ "open": "Ayarları Aç", "logout": "Çıkış Yap", "logoutPrompt": "Çıkış yapmak istediğinizden emin misiniz?", - "selfEncryptionLogoutPrompt": "Çıkış yapmak istediğinizden emin misiniz? Lütfen şifreleme anahtarınızı kopyaladığınızdan emin olun", + "selfEncryptionLogoutPrompt": "Çıkış yapmak istediğinizden emin misiniz? Lütfen şifreleme anahtarını kopyaladığınızdan emin olun", "syncSetting": "Senkronizasyon Ayarı", "cloudSettings": "Bulut Ayarları", "enableSync": "Senkronizasyonu etkinleştir", - "enableSyncLog": "Senkronizasyon günlüğünü etkinleştir", - "enableSyncLogWarning": "Senkronizasyon sorunlarını teşhis etmeye yardımcı olduğunuz için teşekkür ederiz. Bu, belge düzenlemelerinizi yerel bir dosyaya kaydedecek. Lütfen etkinleştirdikten sonra uygulamayı kapatıp yeniden açın", "enableEncrypt": "Verileri şifrele", "cloudURL": "Temel URL", - "webURL": "Web URL'si", "invalidCloudURLScheme": "Geçersiz Şema", "cloudServerType": "Bulut sunucusu", - "cloudServerTypeTip": "Lütfen bulut sunucusunu değiştirdikten sonra mevcut hesabınızdan çıkış yapabileceğinizi unutmayın", + "cloudServerTypeTip": "Bulut sunucusunu değiştirdikten sonra geçerli hesabınızdan çıkış yapabileceğini lütfen unutmayın", "cloudLocal": "Yerel", - "cloudAppFlowy": "@:appName Cloud", - "cloudAppFlowySelfHost": "@:appName Cloud Kendi Kendine Barındırma", - "appFlowyCloudUrlCanNotBeEmpty": "Bulut URL'si boş olamaz", - "clickToCopy": "Panoya kopyala", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL'si", + "cloudSupabaseUrlCanNotBeEmpty": "Supabase url'si boş olamaz", + "cloudSupabaseAnonKey": "Supabase anonim anahtarı", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Anonim anahtar boş olamaz", + "cloudAppFlowy": "@:appName Bulutu Beta", + "cloudAppFlowySelfHost": "@:appName Bulutu Kendi Sunucunuzda", + "appFlowyCloudUrlCanNotBeEmpty": "Bulut url'si boş olamaz", + "clickToCopy": "Kopyalamak için tıklayın", "selfHostStart": "Bir sunucunuz yoksa, lütfen", - "selfHostContent": "belgesine", - "selfHostEnd": "bakarak kendi sunucunuzu nasıl barındıracağınızı öğrenin", - "pleaseInputValidURL": "Lütfen geçerli bir URL girin", - "changeUrl": "Kendi kendine barındırılan URL'yi {} olarak değiştir", + "selfHostContent": "belgeye", + "selfHostEnd": "bakın. Kendi sunucunuzu nasıl kuracağınız konusunda rehberlik için", "cloudURLHint": "Sunucunuzun temel URL'sini girin", - "webURLHint": "Web sunucunuzun temel URL'sini girin", "cloudWSURL": "Websocket URL'si", "cloudWSURLHint": "Sunucunuzun websocket adresini girin", "restartApp": "Yeniden Başlat", - "restartAppTip": "Değişikliklerin etkili olması için uygulamayı yeniden başlatın. Bu işlemin mevcut hesabınızdan çıkış yapabileceğini unutmayın.", - "changeServerTip": "Sunucuyu değiştirdikten sonra, değişikliklerin etkili olması için yeniden başlat düğmesine tıklamalısınız", - "enableEncryptPrompt": "Verilerinizi bu anahtar ile güvence altına almak için şifrelemeyi etkinleştirin. Güvenli bir şekilde saklayın; etkinleştirildikten sonra kapatılamaz. Kaybedilirse, verileriniz kurtarılamaz hale gelir. Kopyalamak için tıklayın", - "inputEncryptPrompt": "Lütfen şifreleme anahtarlarını girin", + "restartAppTip": "Değişikliklerin etkili olması için uygulamayı yeniden başlatın. Lütfen bunun geçerli hesabınızdan çıkış yapabileceğini unutmayın", + "changeServerTip": "Sunucuyu değiştirdikten sonra, değişikliklerin etkili olması için yeniden başlat düğmesine tıklamanız gerekir", + "enableEncryptPrompt": "Verilerinizi bu anahtarla güvence altına almak için şifrelemeyi etkinleştirin. Güvenli bir şekilde saklayın; etkinleştirildikten sonra kapatılamaz. Kaybedilirse, verilerinize ulaşılamaz. Kopyalamak için tıklayın", + "inputEncryptPrompt": "Lütfen şifreleme anahtarınızı girin", "clickToCopySecret": "Anahtarı kopyalamak için tıklayın", "configServerSetting": "Sunucu ayarlarınızı yapılandırın", - "configServerGuide": "'Hızlı Başlangıç'ı seçtikten sonra, 'Ayarlar'a ve ardından \"Bulut Ayarları\"na giderek kendi kendine barındırılan sunucunuzu yapılandırın.", + "configServerGuide": "`Hızlı Başlangıç`ı seçtikten sonra, kendi sunucunuzu yapılandırmak için `Ayarlar` ve ardından \"Bulut Ayarları\"na gidin.", "inputTextFieldHint": "Anahtarınız", "historicalUserList": "Kullanıcı giriş geçmişi", - "historicalUserListTooltip": "Bu liste anonim hesaplarınızı gösterir. Detaylarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başla' düğmesine tıklanarak oluşturulur", + "historicalUserListTooltip": "Bu liste anonim hesaplarınızı görüntüler. Ayrıntılarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başlayın' düğmesine tıklanarak oluşturulur", "openHistoricalUser": "Anonim hesabı açmak için tıklayın", - "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulut senkronizasyonlu bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmaları meydana gelebilir", - "importAppFlowyData": "Harici @:appName Klasöründen Veri İçe Aktar", - "importingAppFlowyDataTip": "Veri içe aktarma devam ediyor. Lütfen uygulamayı kapatmayın", - "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve mevcut AppFlowy veri klasörüne aktarın", - "importSuccess": "@:appName veri klasörü başarıyla içe aktarıldı", - "importFailed": "@:appName veri klasörünün içe aktarılması başarısız oldu", - "importGuide": "Daha fazla detay için lütfen referans belgeyi kontrol edin" + "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", + "importAppFlowyData": "Harici @:appName Klasöründen Veri Al", + "importingAppFlowyDataTip": "Veri aktarımı devam ediyor. Lütfen uygulamayı kapatmayın", + "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve geçerli @:appName veri klasörüne aktarın", + "importSuccess": "@:appName veri klasörü başarıyla alındı", + "importFailed": "@:appName veri klasörü alınamadı", + "importGuide": "Daha fazla bilgi için lütfen referans belgeyi kontrol edin" }, "notifications": { "enableNotifications": { "label": "Bildirimleri etkinleştir", "hint": "Yerel bildirimlerin görünmesini durdurmak için kapatın." - }, - "showNotificationsIcon": { - "label": "Bildirim simgesini göster", - "hint": "Kenar çubuğundaki bildirim simgesini gizlemek için kapatın." - }, - "archiveNotifications": { - "allSuccess": "Tüm bildirimler başarıyla arşivlendi", - "success": "Bildirim başarıyla arşivlendi" - }, - "markAsReadNotifications": { - "allSuccess": "Tümü okundu olarak işaretlendi", - "success": "Okundu olarak işaretlendi" - }, - "action": { - "markAsRead": "Okundu olarak işaretle", - "multipleChoice": "Daha fazla seç", - "archive": "Arşivle" - }, - "settings": { - "settings": "Ayarlar", - "markAllAsRead": "Tümünü okundu olarak işaretle", - "archiveAll": "Tümünü arşivle" - }, - "emptyInbox": { - "title": "Gelen Kutusu Boş!", - "description": "Burada bildirim almak için hatırlatıcılar ayarlayın." - }, - "emptyUnread": { - "title": "Okunmamış bildirim yok", - "description": "Hepsini okudunuz!" - }, - "emptyArchived": { - "title": "Arşivlenmiş öğe yok", - "description": "Arşivlenen bildirimler burada görünecek." - }, - "tabs": { - "inbox": "Gelen Kutusu", - "unread": "Okunmamış", - "archived": "Arşivlenmiş" - }, - "refreshSuccess": "Bildirimler başarıyla yenilendi", - "titles": { - "notifications": "Bildirimler", - "reminder": "Hatırlatıcı" } }, "appearance": { "resetSetting": "Sıfırla", "fontFamily": { "label": "Yazı Tipi Ailesi", - "search": "Ara", - "defaultFont": "Sistem" + "search": "Ara" }, "themeMode": { "label": "Tema Modu", - "light": "Aydınlık Mod", - "dark": "Karanlık Mod", - "system": "Sisteme Uyum Sağla" + "light": "Açık Mod", + "dark": "Koyu Mod", + "system": "Sisteme Uyarla" }, - "fontScaleFactor": "Yazı Tipi Ölçek Faktörü", - "displaySize": "Görüntüleme Boyutu", + "fontScaleFactor": "Yazı Tipi Ölçeklendirme Faktörü", "documentSettings": { "cursorColor": "Belge imleç rengi", - "selectionColor": "Belge seçim rengi", - "width": "Belge genişliği", - "changeWidth": "Değiştir", - "pickColor": "Bir renk seç", - "colorShade": "Renk tonu", - "opacity": "Opaklık", - "hexEmptyError": "Hex renk boş olamaz", - "hexLengthError": "Hex renk 6 haneli olmalıdır", - "hexInvalidError": "Geçersiz hex renk", + "selectionColor": "Belge seçimi rengi", + "hexEmptyError": "Hex rengi boş olamaz", + "hexLengthError": "Hex değeri 6 haneli olmalıdır", + "hexInvalidError": "Geçersiz hex değeri", "opacityEmptyError": "Opaklık boş olamaz", "opacityRangeError": "Opaklık 1 ile 100 arasında olmalıdır", "app": "Uygulama", @@ -1209,110 +439,97 @@ }, "layoutDirection": { "label": "Düzen Yönü", - "hint": "Ekranınızdaki içeriğin akışını soldan sağa veya sağdan sola olarak kontrol edin.", - "ltr": "Soldan Sağa", - "rtl": "Sağdan Sola" + "hint": "Ekranınızdaki içeriğin akışını soldan sağa veya sağdan sola doğru kontrol edin.", + "ltr": "LTR", + "rtl": "RTL" }, "textDirection": { "label": "Varsayılan Metin Yönü", - "hint": "Metnin varsayılan olarak soldan mı yoksa sağdan mı başlayacağını belirtin.", - "ltr": "Soldan Sağa", - "rtl": "Sağdan Sola", + "hint": "Metnin varsayılan olarak soldan mı yoksa sağdan mı başlaması gerektiğini belirtin.", + "ltr": "LTR", + "rtl": "RTL", "auto": "OTOMATİK", - "fallback": "Düzen yönü ile aynı" + "fallback": "Düzen yönüyle aynı" }, "themeUpload": { "button": "Yükle", - "uploadTheme": "Tema yükle", + "uploadTheme": "Temayı yükle", "description": "Aşağıdaki düğmeyi kullanarak kendi @:appName temanızı yükleyin.", - "loading": "Temanızı doğrulayıp yüklerken lütfen bekleyin...", + "loading": "Lütfen temanızı doğrularken ve yüklerken bekleyin...", "uploadSuccess": "Temanız başarıyla yüklendi", - "deletionFailure": "Tema silinemedi. Manuel olarak silmeyi deneyin.", - "filePickerDialogTitle": "Bir .flowy_plugin dosyası seçin", + "deletionFailure": "Temayı silinemedi. Manuel olarak silmeyi deneyin.", + "filePickerDialogTitle": ".flowy_plugin dosyası seçin", "urlUploadFailure": "URL açılamadı: {}" }, "theme": "Tema", "builtInsLabel": "Yerleşik Temalar", "pluginsLabel": "Eklentiler", "dateFormat": { - "label": "Tarih biçimi", + "label": "Tarih formatı", "local": "Yerel", "us": "ABD", "iso": "ISO", - "friendly": "Kullanıcı dostu", + "friendly": "Dostça", "dmy": "G/A/Y" }, "timeFormat": { - "label": "Saat biçimi", - "twelveHour": "12 saat", - "twentyFourHour": "24 saat" + "label": "Saat formatı", + "twelveHour": "12 saatlik", + "twentyFourHour": "24 saatlik" }, "showNamingDialogWhenCreatingPage": "Sayfa oluştururken adlandırma iletişim kutusunu göster", - "enableRTLToolbarItems": "Sağdan sola araç çubuğu öğelerini etkinleştir", + "enableRTLToolbarItems": "RTL araç çubuğu öğelerini etkinleştir", "members": { - "title": "Üye ayarları", - "inviteMembers": "Üye davet et", - "inviteHint": "E-posta ile davet et", - "sendInvite": "Davet gönder", - "copyInviteLink": "Davet bağlantısını kopyala", + "title": "Üye Ayarları", + "inviteMembers": "Üye Davet Et", + "sendInvite": "Davetiye Gönder", + "copyInviteLink": "Davetiye Bağlantısını Kopyala", "label": "Üyeler", "user": "Kullanıcı", "role": "Rol", "removeFromWorkspace": "Çalışma Alanından Kaldır", - "removeFromWorkspaceSuccess": "Çalışma alanından başarıyla kaldırıldı", - "removeFromWorkspaceFailed": "Çalışma alanından kaldırma başarısız oldu", "owner": "Sahip", "guest": "Misafir", "member": "Üye", - "memberHintText": "Bir üye sayfaları okuyabilir ve düzenleyebilir", + "memberHintText": "Bir üye sayfaları okuyabilir, yorum yapabilir ve düzenleyebilir. Üye ve misafir davet edin.", "guestHintText": "Bir Misafir okuyabilir, tepki verebilir, yorum yapabilir ve izin verilen belirli sayfaları düzenleyebilir.", - "emailInvalidError": "Geçersiz e-posta, lütfen kontrol edip tekrar deneyin", + "emailInvalidError": "Geçersiz e-posta, lütfen kontrol edin ve tekrar deneyin", "emailSent": "E-posta gönderildi, lütfen gelen kutunuzu kontrol edin", - "members": "üye", + "members": "üyeler", "membersCount": { "zero": "{} üye", "one": "{} üye", "other": "{} üye" }, - "inviteFailedDialogTitle": "Davet gönderilemedi", - "inviteFailedMemberLimit": "Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen yükseltin.", - "inviteFailedMemberLimitMobile": "Çalışma alanınız üye sınırına ulaştı.", - "memberLimitExceeded": "Üye sınırına ulaşıldı, daha fazla üye davet etmek için lütfen ", - "memberLimitExceededUpgrade": "yükseltin", - "memberLimitExceededPro": "Üye sınırına ulaşıldı, daha fazla üye gerekiyorsa ", - "memberLimitExceededProContact": "support@appflowy.io", + "memberLimitExceeded": "Hesabınız için izin verilen maksimum üye sınırına ulaştınız. Çalışmanıza devam etmek için ek üye eklemek istiyorsanız, lütfen GitHub'da talepte bulunun", "failedToAddMember": "Üye eklenemedi", "addMemberSuccess": "Üye başarıyla eklendi", "removeMember": "Üyeyi Kaldır", - "areYouSureToRemoveMember": "Bu üyeyi kaldırmak istediğinizden emin misiniz?", - "inviteMemberSuccess": "Davet başarıyla gönderildi", - "failedToInviteMember": "Üye davet edilemedi", - "workspaceMembersError": "Hay aksi, bir şeyler yanlış gitti", - "workspaceMembersErrorDescription": "Şu anda üye listesini yükleyemedik. Lütfen daha sonra tekrar deneyin" + "areYouSureToRemoveMember": "Bu üyeyi kaldırmak istediğinizden emin misiniz?" } }, "files": { "copy": "Kopyala", - "defaultLocation": "Dosyaları ve veri depolama konumunu oku", + "defaultLocation": "Dosyaları ve verileri okuma konumu", "exportData": "Verilerinizi dışa aktarın", - "doubleTapToCopy": "Yolu kopyalamak için çift dokunun", + "doubleTapToCopy": "Yolu kopyalamak için iki kez dokunun", "restoreLocation": "@:appName varsayılan yoluna geri yükle", "customizeLocation": "Başka bir klasör aç", "restartApp": "Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın.", "exportDatabase": "Veritabanını dışa aktar", "selectFiles": "Dışa aktarılması gereken dosyaları seçin", "selectAll": "Tümünü seç", - "deselectAll": "Tüm seçimi kaldır", - "createNewFolder": "Yeni klasör oluştur", - "createNewFolderDesc": "Verilerinizi nerede saklamak istediğinizi bize söyleyin", - "defineWhereYourDataIsStored": "Verilerinizin nerede saklanacağını tanımlayın", + "deselectAll": "Tüm seçimleri kaldır", + "createNewFolder": "Yeni bir klasör oluştur", + "createNewFolderDesc": "Verilerinizi nerede saklamak istediğinizi bize bildirin", + "defineWhereYourDataIsStored": "Verilerinizin nerede saklandığını tanımlayın", "open": "Aç", - "openFolder": "Mevcut bir klasör aç", - "openFolderDesc": "Mevcut @:appName klasörünüzü okuyun ve yazın", + "openFolder": "Mevcut bir klasörü aç", + "openFolderDesc": "Mevcut @:appName klasörünüze okuyun ve yazın", "folderHintText": "klasör adı", - "location": "Yeni klasör oluşturma", + "location": "Yeni bir klasör oluşturuluyor", "locationDesc": "@:appName veri klasörünüz için bir ad seçin", - "browser": "Göz at", + "browser": "Gözat", "create": "Oluştur", "set": "Ayarla", "folderPath": "Klasörünüzü saklamak için yol", @@ -1321,13 +538,13 @@ "changeLocationTooltips": "Veri dizinini değiştir", "change": "Değiştir", "openLocationTooltips": "Başka bir veri dizini aç", - "openCurrentDataFolder": "Mevcut veri dizinini aç", - "recoverLocationTooltips": "@:appName'in varsayılan veri dizinine sıfırla", + "openCurrentDataFolder": "Geçerli veri dizinini aç", + "recoverLocationTooltips": "@:appName'nin varsayılan veri dizinine sıfırla", "exportFileSuccess": "Dosya başarıyla dışa aktarıldı!", "exportFileFail": "Dosya dışa aktarılamadı!", - "export": "Dışa aktar", + "export": "Dışa Aktar", "clearCache": "Önbelleği temizle", - "clearCacheDesc": "Resimlerin yüklenmemesi veya yazı tiplerinin doğru görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleği temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmayacaktır.", + "clearCacheDesc": "Görüntülerin yüklenmemesinde veya yazı tiplerinin düzgün görüntülenmemesi gibi sorunlarla karşılaşırsanız, önbelleğinizi temizlemeyi deneyin. Bu işlem kullanıcı verilerinizi kaldırmaz.", "areYouSureToClearCache": "Önbelleği temizlemek istediğinizden emin misiniz?", "clearCacheSuccess": "Önbellek başarıyla temizlendi!" }, @@ -1335,25 +552,49 @@ "name": "Ad", "email": "E-posta", "tooltipSelectIcon": "Simge seç", - "selectAnIcon": "Bir simge seç", - "pleaseInputYourOpenAIKey": "lütfen Yapay Zeka anahtarınızı girin", - "clickToLogout": "Mevcut kullanıcının oturumunu kapatmak için tıklayın" + "selectAnIcon": "Bir simge seçin", + "pleaseInputYourOpenAIKey": "Lütfen OpenAI anahtarınızı girin", + "pleaseInputYourStabilityAIKey": "Lütfen Stability AI anahtarınızı girin", + "clickToLogout": "Geçerli kullanıcıdan çıkış yapmak için tıklayın" + }, + "shortcuts": { + "shortcutsLabel": "Kısayollar", + "command": "Komut", + "keyBinding": "Tuş Bağlantısı", + "addNewCommand": "Yeni Komut Ekle", + "updateShortcutStep": "İstediğiniz tuş kombinasyonuna basın ve ENTER tuşuna basın", + "shortcutIsAlreadyUsed": "Bu kısayol zaten şunun için kullanılıyor: {conflict}", + "resetToDefault": "Varsayılan tuş bağlantılarını sıfırla", + "couldNotLoadErrorMsg": "Kısayollar yüklenemedi, tekrar deneyin", + "couldNotSaveErrorMsg": "Kısayollar kaydedilemedi, tekrar deneyin", + "commands": { + "codeBlockNewParagraph": "Kod bloğunun yanına yeni bir paragraf ekle", + "codeBlockIndentLines": "Tablonuzdaki kod bloğunda satır başında iki boşluk ekleyin", + "codeBlockOutdentLines": "Kod bloğundaki satır başındaki iki boşluğu silin", + "codeBlockAddTwoSpaces": "Kod bloğunda satır başına iki boşluk ekle", + "codeBlockSelectAll": "Kod bloğunun içindeki tüm içeriği seç", + "codeBlockPasteText": "Kod bloğuna metin yapıştır", + "textAlignLeft": "Metni sola hizala", + "textAlignCenter": "Metni ortaya hizala", + "textAlignRight": "Metni sağa hizala", + "codeBlockDeleteTwoSpaces": "Kod bloğunda satır başındaki iki boşluğu sil" + } }, "mobile": { "personalInfo": "Kişisel Bilgiler", "username": "Kullanıcı Adı", "usernameEmptyError": "Kullanıcı adı boş olamaz", "about": "Hakkında", - "pushNotifications": "Anlık Bildirimler", + "pushNotifications": "Anında Bildirimler", "support": "Destek", "joinDiscord": "Discord'da bize katılın", "privacyPolicy": "Gizlilik Politikası", "userAgreement": "Kullanıcı Sözleşmesi", "termsAndConditions": "Şartlar ve Koşullar", "userprofileError": "Kullanıcı profili yüklenemedi", - "userprofileErrorDescription": "Lütfen sorunun devam edip etmediğini kontrol etmek için oturumu kapatıp yeniden giriş yapmayı deneyin.", - "selectLayout": "Düzen seç", - "selectStartingDay": "Başlangıç gününü seç", + "userprofileErrorDescription": "Lütfen sorunun devam edip etmediğini kontrol etmek için çıkış yapıp tekrar giriş yapmayı deneyin.", + "selectLayout": "Düzen seçin", + "selectStartingDay": "Başlangıç gününü seçin", "version": "Sürüm" } }, @@ -1361,18 +602,18 @@ "deleteView": "Bu görünümü silmek istediğinizden emin misiniz?", "createView": "Yeni", "title": { - "placeholder": "Başlıksız" + "placeholder": "İsimsiz" }, "settings": { "filter": "Filtre", "sort": "Sırala", - "sortBy": "Sıralama ölçütü", + "sortBy": "Şuna göre sırala", "properties": "Özellikler", "reorderPropertiesTooltip": "Özellikleri yeniden sıralamak için sürükleyin", "group": "Grupla", "addFilter": "Filtre Ekle", "deleteFilter": "Filtreyi sil", - "filterBy": "Filtreleme ölçütü", + "filterBy": "Şuna göre filtrele...", "typeAValue": "Bir değer yazın...", "layout": "Düzen", "databaseLayout": "Düzen", @@ -1385,113 +626,95 @@ "boardSettings": "Pano ayarları", "calendarSettings": "Takvim ayarları", "createView": "Yeni görünüm", - "duplicateView": "Görünümü çoğalt", + "duplicateView": "Görünümü kopyala", "deleteView": "Görünümü sil", "numberOfVisibleFields": "{} gösteriliyor" }, - "filter": { - "empty": "Aktif filtre yok", - "addFilter": "Filtre ekle", - "cannotFindCreatableField": "Filtrelenecek uygun bir alan bulunamadı", - "conditon": "Koşul", - "where": "Koşul" - }, "textFilter": { "contains": "İçerir", "doesNotContain": "İçermez", - "endsWith": "İle biter", - "startWith": "İle başlar", - "is": "Eşittir", - "isNot": "Eşit değildir", - "isEmpty": "Boştur", - "isNotEmpty": "Boş değildir", + "endsWith": "Şununla biter", + "startWith": "Şununla başlar", + "is": "Şudur", + "isNot": "Şu değildir", + "isEmpty": "Boş", + "isNotEmpty": "Boş değil", "choicechipPrefix": { "isNot": "Değil", - "startWith": "İle başlar", - "endWith": "İle biter", - "isEmpty": "boştur", - "isNotEmpty": "boş değildir" + "startWith": "Şununla başlar", + "endWith": "Şununla biter", + "isEmpty": "boş", + "isNotEmpty": "boş değil" } }, "checkboxFilter": { "isChecked": "İşaretli", "isUnchecked": "İşaretsiz", "choicechipPrefix": { - "is": "eşittir" + "is": "durum" } }, "checklistFilter": { - "isComplete": "Tamamlandı", - "isIncomplted": "Tamamlanmadı" + "isComplete": "tamamlandı", + "isIncomplted": "tamamlanmadı" }, "selectOptionFilter": { - "is": "Eşittir", - "isNot": "Eşit değildir", + "is": "Şudur", + "isNot": "Şu değildir", "contains": "İçerir", "doesNotContain": "İçermez", - "isEmpty": "Boştur", - "isNotEmpty": "Boş değildir" + "isEmpty": "Boş", + "isNotEmpty": "Boş değil" }, "dateFilter": { - "is": "Tarihinde", - "before": "Öncesinde", - "after": "Sonrasında", - "onOrBefore": "Tarihinde veya öncesinde", - "onOrAfter": "Tarihinde veya sonrasında", - "between": "Arasında", - "empty": "Boştur", - "notEmpty": "Boş değildir", - "startDate": "Başlangıç tarihi", - "endDate": "Bitiş tarihi", + "is": "Şudur", + "before": "Şundan önce", + "after": "Şundan sonra", + "onOrBefore": "Şunda veya önce", + "onOrAfter": "Şunda veya sonra", + "between": "Şunlar arasında", + "empty": "Boş", + "notEmpty": "Boş değil", "choicechipPrefix": { "before": "Önce", "after": "Sonra", - "between": "Arasında", - "onOrBefore": "Tarihinde veya önce", - "onOrAfter": "Tarihinde veya sonra", - "isEmpty": "Boştur", - "isNotEmpty": "Boş değildir" + "onOrBefore": "Şunda veya önce", + "onOrAfter": "Şunda veya sonra", + "isEmpty": "Boş", + "isNotEmpty": "Boş değil" } }, "numberFilter": { "equal": "Eşittir", "notEqual": "Eşit değildir", - "lessThan": "Küçüktür", - "greaterThan": "Büyüktür", - "lessThanOrEqualTo": "Küçük veya eşittir", - "greaterThanOrEqualTo": "Büyük veya eşittir", - "isEmpty": "Boştur", - "isNotEmpty": "Boş değildir" + "lessThan": "Şundan küçüktür", + "greaterThan": "Şundan büyüktür", + "lessThanOrEqualTo": "Şundan küçük veya eşittir", + "greaterThanOrEqualTo": "Şundan büyük veya eşittir", + "isEmpty": "Boş", + "isNotEmpty": "Boş değil" }, "field": { - "label": "Özellik", - "hide": "Özelliği gizle", - "show": "Özelliği göster", - "insertLeft": "Sola ekle", - "insertRight": "Sağa ekle", - "duplicate": "Çoğalt", + "hide": "Gizle", + "show": "Göster", + "insertLeft": "Sola Ekle", + "insertRight": "Sağa Ekle", + "duplicate": "Kopyala", "delete": "Sil", - "wrapCellContent": "Metni kaydır", "clear": "Hücreleri temizle", - "switchPrimaryFieldTooltip": "Birincil alanın alan türü değiştirilemez", "textFieldName": "Metin", - "checkboxFieldName": "Onay kutusu", + "checkboxFieldName": "Onay Kutusu", "dateFieldName": "Tarih", "updatedAtFieldName": "Son değiştirilme", "createdAtFieldName": "Oluşturulma tarihi", "numberFieldName": "Sayılar", - "singleSelectFieldName": "Seçim", - "multiSelectFieldName": "Çoklu seçim", + "singleSelectFieldName": "Seçenek", + "multiSelectFieldName": "Çoklu Seçenek", "urlFieldName": "URL", - "checklistFieldName": "Kontrol listesi", + "checklistFieldName": "Kontrol Listesi", "relationFieldName": "İlişki", - "summaryFieldName": "Yapay Zeka Özeti", - "timeFieldName": "Saat", - "mediaFieldName": "Dosyalar ve medya", - "translateFieldName": "Yapay Zeka Çeviri", - "translateTo": "Şu dile çevir", - "numberFormat": "Sayı biçimi", - "dateFormat": "Tarih biçimi", + "numberFormat": "Sayı formatı", + "dateFormat": "Tarih formatı", "includeTime": "Saati dahil et", "isRange": "Bitiş tarihi", "dateFormatFriendly": "Ay Gün, Yıl", @@ -1499,8 +722,8 @@ "dateFormatLocal": "Ay/Gün/Yıl", "dateFormatUS": "Yıl/Ay/Gün", "dateFormatDayMonthYear": "Gün/Ay/Yıl", - "timeFormat": "Saat biçimi", - "invalidTimeFormat": "Geçersiz biçim", + "timeFormat": "Saat formatı", + "invalidTimeFormat": "Geçersiz format", "timeFormatTwelveHour": "12 saat", "timeFormatTwentyFourHour": "24 saat", "clearDate": "Tarihi temizle", @@ -1518,16 +741,16 @@ "addOption": "Seçenek ekle", "editProperty": "Özelliği düzenle", "newProperty": "Yeni özellik", - "openRowDocument": "Sayfa olarak aç", - "deleteFieldPromptMessage": "Emin misiniz? Bu özellik ve tüm verileri silinecek", + "deleteFieldPromptMessage": "Emin misiniz? Bu özellik silinecek", "clearFieldPromptMessage": "Emin misiniz? Bu sütundaki tüm hücreler boşaltılacak", - "newColumn": "Yeni sütun", - "format": "Biçim", - "reminderOnDateTooltip": "Bu hücrede planlanmış bir hatırlatıcı var", - "optionAlreadyExist": "Seçenek zaten mevcut" + "newColumn": "Yeni Sütun", + "format": "Format", + "reminderOnDateTooltip": "Bu hücrenin planlanmış bir hatırlatıcısı var", + "optionAlreadyExist": "Seçenek zaten mevcut", + "wrap": "Sar" }, "rowPage": { - "newField": "Yeni alan ekle", + "newField": "Yeni bir alan ekle", "fieldDragElementTooltip": "Menüyü açmak için tıklayın", "showHiddenFields": { "one": "{count} gizli alanı göster", @@ -1538,44 +761,33 @@ "one": "{count} gizli alanı gizle", "many": "{count} gizli alanı gizle", "other": "{count} gizli alanı gizle" - }, - "openAsFullPage": "Tam sayfa olarak aç", - "moreRowActions": "Daha fazla satır işlemi" + } }, "sort": { "ascending": "Artan", "descending": "Azalan", - "by": "Göre", + "by": "Şuna göre", "empty": "Aktif sıralama yok", - "cannotFindCreatableField": "Sıralanacak uygun bir alan bulunamadı", + "cannotFindCreatableField": "Sıralama yapmak için uygun bir alan bulunamadı", "deleteAllSorts": "Tüm sıralamaları sil", - "addSort": "Sıralama ekle", - "sortsActive": "Sıralama yaparken {intention} yapılamaz", - "removeSorting": "Bu görünümdeki tüm sıralamaları kaldırıp devam etmek istiyor musunuz?", - "fieldInUse": "Bu alana göre zaten sıralama yapıyorsunuz" + "addSort": "Yeni sıralama ekle", + "removeSorting": "Sıralamayı kaldırmak ister misiniz?", + "fieldInUse": "Zaten bu alana göre sıralama yapıyorsunuz" }, "row": { - "label": "Satır", - "duplicate": "Çoğalt", + "duplicate": "Kopyala", "delete": "Sil", - "titlePlaceholder": "Başlıksız", + "titlePlaceholder": "İsimsiz", "textPlaceholder": "Boş", "copyProperty": "Özellik panoya kopyalandı", "count": "Sayı", "newRow": "Yeni satır", - "loadMore": "Daha fazla yükle", - "action": "İşlem", - "add": "Aşağıya eklemek için tıklayın", + "action": "Eylem", + "add": "Alta eklemek için tıklayın", "drag": "Taşımak için sürükleyin", - "deleteRowPrompt": "Bu satırı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", - "deleteCardPrompt": "Bu kartı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", "dragAndClick": "Taşımak için sürükleyin, menüyü açmak için tıklayın", "insertRecordAbove": "Üste kayıt ekle", - "insertRecordBelow": "Alta kayıt ekle", - "noContent": "İçerik yok", - "reorderRowDescription": "satırı yeniden sırala", - "createRowAboveDescription": "üste bir satır oluştur", - "createRowBelowDescription": "alta bir satır ekle" + "insertRecordBelow": "Alta kayıt ekle" }, "selectOption": { "create": "Oluştur", @@ -1584,15 +796,15 @@ "lightPinkColor": "Açık Pembe", "orangeColor": "Turuncu", "yellowColor": "Sarı", - "limeColor": "Limon", + "limeColor": "Limoni", "greenColor": "Yeşil", - "aquaColor": "Su Mavisi", + "aquaColor": "Su yeşili", "blueColor": "Mavi", "deleteTag": "Etiketi sil", "colorPanelTitle": "Renk", - "panelTitle": "Bir seçenek seçin veya oluşturun", + "panelTitle": "Bir seçenek seçin veya yeni bir tane oluşturun", "searchOption": "Bir seçenek arayın", - "searchOrCreateOption": "Bir seçenek arayın veya oluşturun", + "searchOrCreateOption": "Bir seçenek arayın veya yeni bir tane oluşturun", "createNew": "Yeni oluştur", "orSelectOne": "Veya bir seçenek seçin", "typeANewOption": "Yeni bir seçenek yazın", @@ -1600,7 +812,7 @@ }, "checklist": { "taskHint": "Görev açıklaması", - "addNew": "Yeni görev ekle", + "addNew": "Yeni bir görev ekle", "submitNewTask": "Oluştur", "hideComplete": "Tamamlanan görevleri gizle", "showComplete": "Tüm görevleri göster" @@ -1608,17 +820,18 @@ "url": { "launch": "Bağlantıyı tarayıcıda aç", "copy": "Bağlantıyı panoya kopyala", - "textFieldHint": "Bir URL girin" + "textFieldHint": "Bir URL girin", + "copiedNotification": "Panoya kopyalandı!" }, "relation": { - "relatedDatabasePlaceLabel": "İlişkili Veritabanı", + "relatedDatabasePlaceLabel": "İlgili Veritabanı", "relatedDatabasePlaceholder": "Yok", "inRelatedDatabase": "İçinde", "rowSearchTextFieldPlaceholder": "Ara", - "noDatabaseSelected": "Veritabanı seçilmedi, lütfen aşağıdaki listeden önce bir tane seçin:", + "noDatabaseSelected": "Veritabanı seçilmedi, lütfen önce aşağıdaki listeden bir tane seçin:", "emptySearchResult": "Kayıt bulunamadı", - "linkedRowListLabel": "{count} bağlantılı satır", - "unlinkedRowListLabel": "Başka bir satır bağla" + "linkedRowListLabel": "{count} bağlı satır", + "unlinkedRowListLabel": "Başka bir satırı bağla" }, "menuName": "Tablo", "referencedGridPrefix": "Görünümü", @@ -1626,33 +839,15 @@ "calculationTypeLabel": { "none": "Yok", "average": "Ortalama", - "max": "En büyük", + "max": "Maksimum", "median": "Medyan", - "min": "En küçük", + "min": "Minimum", "sum": "Toplam", "count": "Sayı", "countEmpty": "Boş sayısı", "countEmptyShort": "BOŞ", - "countNonEmpty": "Boş olmayan sayısı", + "countNonEmpty": "Dolu sayısı", "countNonEmptyShort": "DOLU" - }, - "media": { - "rename": "Yeniden adlandır", - "download": "İndir", - "expand": "Genişlet", - "delete": "Sil", - "moreFilesHint": "+{}", - "addFileOrImage": "Dosya veya bağlantı ekle", - "attachmentsHint": "{}", - "addFileMobile": "Dosya ekle", - "extraCount": "+{}", - "deleteFileDescription": "Bu dosyayı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.", - "showFileNames": "Dosya adını göster", - "downloadSuccess": "Dosya indirildi", - "downloadFailedToken": "Dosya indirilemedi, kullanıcı jetonu mevcut değil", - "setAsCover": "Kapak olarak ayarla", - "openInBrowser": "Tarayıcıda aç", - "embedLink": "Dosya bağlantısını yerleştir" } }, "document": { @@ -1661,67 +856,21 @@ "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, - "creating": "Oluşturuluyor...", "slashMenu": { "board": { - "selectABoardToLinkTo": "Bağlanacak bir Pano seçin", - "createANewBoard": "Yeni bir Pano oluştur" + "selectABoardToLinkTo": "Bağlantı kurulacak bir Pano seçin", + "createANewBoard": "Yeni bir Pano oluşturun" }, "grid": { - "selectAGridToLinkTo": "Bağlanacak bir Tablo seçin", - "createANewGrid": "Yeni bir Tablo oluştur" + "selectAGridToLinkTo": "Bağlantı kurulacak bir Tablo seçin", + "createANewGrid": "Yeni bir Tablo oluşturun" }, "calendar": { - "selectACalendarToLinkTo": "Bağlanacak bir Takvim seçin", - "createANewCalendar": "Yeni bir Takvim oluştur" + "selectACalendarToLinkTo": "Bağlantı kurulacak bir Takvim seçin", + "createANewCalendar": "Yeni bir Takvim oluşturun" }, "document": { - "selectADocumentToLinkTo": "Bağlanacak bir Belge seçin" - }, - "name": { - "text": "Metin", - "heading1": "Başlık 1", - "heading2": "Başlık 2", - "heading3": "Başlık 3", - "image": "Görsel", - "bulletedList": "Madde işaretli liste", - "numberedList": "Numaralı liste", - "todoList": "Yapılacaklar listesi", - "doc": "Belge", - "linkedDoc": "Sayfaya bağlantı", - "grid": "Tablo", - "linkedGrid": "Bağlantılı Tablo", - "kanban": "Kanban", - "linkedKanban": "Bağlantılı Kanban", - "calendar": "Takvim", - "linkedCalendar": "Bağlantılı Takvim", - "quote": "Alıntı", - "divider": "Ayırıcı", - "table": "Tablo", - "callout": "Not Kutusu", - "outline": "Ana Hat", - "mathEquation": "Matematik Denklemi", - "code": "Kod", - "toggleList": "Açılır liste", - "toggleHeading1": "Açılır başlık 1", - "toggleHeading2": "Açılır başlık 2", - "toggleHeading3": "Açılır başlık 3", - "emoji": "Emoji", - "aiWriter": "Yapay Zeka Yazar", - "dateOrReminder": "Tarih veya Hatırlatıcı", - "photoGallery": "Fotoğraf Galerisi", - "file": "Dosya" - }, - "subPage": { - "name": "Belge", - "keyword1": "alt sayfa", - "keyword2": "sayfa", - "keyword3": "alt sayfa", - "keyword4": "sayfa ekle", - "keyword5": "sayfa yerleştir", - "keyword6": "yeni sayfa", - "keyword7": "sayfa oluştur", - "keyword8": "belge" + "selectADocumentToLinkTo": "Bağlantı kurulacak bir Belge seçin" } }, "selectionMenu": { @@ -1729,95 +878,61 @@ "codeBlock": "Kod Bloğu" }, "plugins": { - "referencedBoard": "Referans Pano", - "referencedGrid": "Referans Tablo", - "referencedCalendar": "Referans Takvim", - "referencedDocument": "Referans Belge", - "autoGeneratorMenuItemName": "Yapay Zeka Yazar", - "autoGeneratorTitleName": "Yapay Zeka: Yapay zekadan herhangi bir şey yazmasını isteyin...", - "autoGeneratorLearnMore": "Daha fazla bilgi", + "referencedBoard": "Referans Gösterilen Pano", + "referencedGrid": "Referans Gösterilen Tablo", + "referencedCalendar": "Referans Gösterilen Takvim", + "referencedDocument": "Referans Gösterilen Belge", + "autoGeneratorMenuItemName": "OpenAI Yazar", + "autoGeneratorTitleName": "OpenAI: AI'dan istediğinizi yazmasını isteyin...", + "autoGeneratorLearnMore": "Daha fazla bilgi edinin", "autoGeneratorGenerate": "Oluştur", - "autoGeneratorHintText": "Yapay zekaya sorun ...", - "autoGeneratorCantGetOpenAIKey": "Yapay zeka anahtarı alınamıyor", + "autoGeneratorHintText": "OpenAI'ya sorun ...", + "autoGeneratorCantGetOpenAIKey": "OpenAI anahtarı alınamıyor", "autoGeneratorRewrite": "Yeniden yaz", - "smartEdit": "Yapay Zekaya Sor", - "aI": "Yapay Zeka", - "smartEditFixSpelling": "Yazım ve dilbilgisini düzelt", - "warning": "⚠️ Yapay zeka yanıtları yanlış veya yanıltıcı olabilir.", + "smartEdit": "AI Asistanları", + "openAI": "OpenAI", + "smartEditFixSpelling": "Yazımı düzelt", + "warning": "⚠️ AI yanıtları yanlış veya yanıltıcı olabilir.", "smartEditSummarize": "Özetle", "smartEditImproveWriting": "Yazımı geliştir", "smartEditMakeLonger": "Daha uzun yap", - "smartEditCouldNotFetchResult": "Yapay zekadan sonuç alınamadı", - "smartEditCouldNotFetchKey": "Yapay zeka anahtarı alınamadı", - "smartEditDisabled": "Ayarlar'dan Yapay Zeka'yı bağlayın", - "appflowyAIEditDisabled": "Yapay Zeka özelliklerini etkinleştirmek için giriş yapın", - "discardResponse": "Yapay Zeka yanıtlarını silmek istiyor musunuz?", + "smartEditCouldNotFetchResult": "OpenAI'dan sonuç alınamadı", + "smartEditCouldNotFetchKey": "OpenAI anahtarı alınamadı", + "smartEditDisabled": "Ayarlar'da OpenAI'yı bağlayın", + "discardResponse": "AI yanıtlarını silmek ister misiniz?", "createInlineMathEquation": "Denklem oluştur", - "fonts": "Yazı tipleri", + "fonts": "Yazı Tipleri", "insertDate": "Tarih ekle", "emoji": "Emoji", - "toggleList": "Açılır liste", - "emptyToggleHeading": "Boş açılır başlık {}. İçerik eklemek için tıklayın.", - "emptyToggleList": "Boş açılır liste. İçerik eklemek için tıklayın.", - "emptyToggleHeadingWeb": "Boş açılır başlık {level}. İçerik eklemek için tıklayın", + "toggleList": "Listeyi değiştir", "quoteList": "Alıntı listesi", "numberedList": "Numaralı liste", "bulletedList": "Madde işaretli liste", - "todoList": "Yapılacaklar listesi", - "callout": "Not Kutusu", - "simpleTable": { - "moreActions": { - "color": "Renk", - "align": "Hizala", - "delete": "Sil", - "duplicate": "Çoğalt", - "insertLeft": "Sola ekle", - "insertRight": "Sağa ekle", - "insertAbove": "Üste ekle", - "insertBelow": "Alta ekle", - "headerColumn": "Başlık sütunu", - "headerRow": "Başlık satırı", - "clearContents": "İçeriği temizle", - "setToPageWidth": "Sayfa genişliğine ayarla", - "distributeColumnsWidth": "Sütunları eşit dağıt", - "duplicateRow": "Satırı çoğalt", - "duplicateColumn": "Sütunu çoğalt", - "textColor": "Metin rengi", - "cellBackgroundColor": "Hücre arka plan rengi", - "duplicateTable": "Tabloyu çoğalt" - }, - "clickToAddNewRow": "Yeni satır eklemek için tıklayın", - "clickToAddNewColumn": "Yeni sütun eklemek için tıklayın", - "clickToAddNewRowAndColumn": "Yeni satır ve sütun eklemek için tıklayın", - "headerName": { - "table": "Tablo", - "alignText": "Metni hizala" - } - }, + "todoList": "Yapılacaklar Listesi", + "callout": "Bilgi Kutusu", "cover": { - "changeCover": "Kapağı Değiştir", + "changeCover": "Kapak Resmi Değiştir", "colors": "Renkler", - "images": "Görseller", + "images": "Resimler", "clearAll": "Tümünü Temizle", "abstract": "Soyut", - "addCover": "Kapak Ekle", - "addLocalImage": "Yerel görsel ekle", - "invalidImageUrl": "Geçersiz görsel URL'si", - "failedToAddImageToGallery": "Görsel galeriye eklenemedi", - "enterImageUrl": "Görsel URL'si girin", + "addCover": "Kapak Resmi Ekle", + "addLocalImage": "Yerel resim ekle", + "invalidImageUrl": "Geçersiz resim URL'si", + "failedToAddImageToGallery": "Resim galeriye eklenemedi", + "enterImageUrl": "Resim URL'sini girin", "add": "Ekle", "back": "Geri", "saveToGallery": "Galeriye kaydet", "removeIcon": "Simgeyi kaldır", - "removeCover": "Kapağı kaldır", - "pasteImageUrl": "Görsel URL'sini yapıştırın", + "pasteImageUrl": "Resim URL'sini yapıştır", "or": "VEYA", "pickFromFiles": "Dosyalardan seç", - "couldNotFetchImage": "Görsel alınamadı", - "imageSavingFailed": "Görsel Kaydedilemedi", + "couldNotFetchImage": "Resim alınamadı", + "imageSavingFailed": "Resim Kaydedilemedi", "addIcon": "Simge ekle", "changeIcon": "Simgeyi değiştir", - "coverRemoveAlert": "Silindikten sonra kapaktan kaldırılacaktır.", + "coverRemoveAlert": "Silindikten sonra kapak resminden kaldırılacaktır.", "alertDialogConfirmation": "Devam etmek istediğinizden emin misiniz?" }, "mathEquation": { @@ -1828,11 +943,9 @@ "optionAction": { "click": "Tıkla", "toOpenMenu": " menüyü açmak için", - "drag": "Sürükle", - "toMove": " taşımak için", "delete": "Sil", - "duplicate": "Çoğalt", - "turnInto": "Dönüştür", + "duplicate": "Kopyala", + "turnInto": "Şuna dönüştür", "moveUp": "Yukarı taşı", "moveDown": "Aşağı taşı", "color": "Renk", @@ -1841,113 +954,44 @@ "center": "Orta", "right": "Sağ", "defaultColor": "Varsayılan", - "depth": "Derinlik", - "copyLinkToBlock": "Bloğa bağlantıyı kopyala" + "depth": "Derinlik" }, "image": { - "addAnImage": "Görsel ekle", - "copiedToPasteBoard": "Görsel bağlantısı panoya kopyalandı", - "addAnImageDesktop": "Bir görsel ekle", - "addAnImageMobile": "Bir veya daha fazla görsel eklemek için tıklayın", - "dropImageToInsert": "Eklemek için görselleri bırakın", - "imageUploadFailed": "Görsel yüklenemedi", - "imageDownloadFailed": "Görsel indirilemedi, lütfen tekrar deneyin", - "imageDownloadFailedToken": "Kullanıcı jetonu eksik olduğu için görsel indirilemedi, lütfen tekrar deneyin", - "errorCode": "Hata kodu" - }, - "photoGallery": { - "name": "Fotoğraf galerisi", - "imageKeyword": "görsel", - "imageGalleryKeyword": "görsel galerisi", - "photoKeyword": "fotoğraf", - "photoBrowserKeyword": "fotoğraf tarayıcısı", - "galleryKeyword": "galeri", - "addImageTooltip": "Görsel ekle", - "changeLayoutTooltip": "Düzeni değiştir", - "browserLayout": "Tarayıcı", - "gridLayout": "Izgara", - "deleteBlockTooltip": "Tüm galeriyi sil" - }, - "math": { - "copiedToPasteBoard": "Matematik denklemi panoya kopyalandı" + "copiedToPasteBoard": "Resim bağlantısı panoya kopyalandı", + "addAnImage": "Bir resim ekle", + "imageUploadFailed": "Resim yükleme başarısız" }, "urlPreview": { "copiedToPasteBoard": "Bağlantı panoya kopyalandı", - "convertToLink": "Yerleşik bağlantıya dönüştür" + "convertToLink": "Gömülü bağlantıya dönüştür" }, "outline": { "addHeadingToCreateOutline": "İçindekiler tablosu oluşturmak için başlıklar ekleyin.", "noMatchHeadings": "Eşleşen başlık bulunamadı." }, "table": { - "addAfter": "Sonrasına ekle", - "addBefore": "Öncesine ekle", + "addAfter": "Sonra ekle", + "addBefore": "Önce ekle", "delete": "Sil", "clear": "İçeriği temizle", - "duplicate": "Çoğalt", + "duplicate": "Kopyala", "bgColor": "Arka plan rengi" }, "contextMenu": { "copy": "Kopyala", "cut": "Kes", - "paste": "Yapıştır", - "pasteAsPlainText": "Düz metin olarak yapıştır" + "paste": "Yapıştır" }, - "action": "İşlemler", + "action": "Eylemler", "database": { - "selectDataSource": "Veri kaynağı seç", + "selectDataSource": "Veri kaynağını seçin", "noDataSource": "Veri kaynağı yok", - "selectADataSource": "Bir veri kaynağı seç", + "selectADataSource": "Bir veri kaynağı seçin", "toContinue": "devam etmek için", "newDatabase": "Yeni Veritabanı", - "linkToDatabase": "Veritabanına Bağlantı" + "linkToDatabase": "Veritabanına Bağla" }, - "date": "Tarih", - "video": { - "label": "Video", - "emptyLabel": "Video ekle", - "placeholder": "Video bağlantısını yapıştırın", - "copiedToPasteBoard": "Video bağlantısı panoya kopyalandı", - "insertVideo": "Video ekle", - "invalidVideoUrl": "Kaynak URL'si henüz desteklenmiyor.", - "invalidVideoUrlYouTube": "YouTube henüz desteklenmiyor.", - "supportedFormats": "Desteklenen formatlar: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "Dosya", - "uploadTab": "Yükle", - "uploadMobile": "Bir dosya seç", - "uploadMobileGallery": "Fotoğraf Galerisinden", - "networkTab": "Bağlantı yerleştir", - "placeholderText": "Bir dosya yükleyin veya yerleştirin", - "placeholderDragging": "Yüklemek için dosyayı bırakın", - "dropFileToUpload": "Yüklemek için bir dosya bırakın", - "fileUploadHint": "Bir dosya sürükleyip bırakın veya tıklayarak ", - "fileUploadHintSuffix": "Göz atın", - "networkHint": "Bir dosya bağlantısı yapıştırın", - "networkUrlInvalid": "Geçersiz URL. URL'yi kontrol edip tekrar deneyin.", - "networkAction": "Yerleştir", - "fileTooBigError": "Dosya boyutu çok büyük, lütfen 10MB'dan küçük bir dosya yükleyin", - "renameFile": { - "title": "Dosyayı yeniden adlandır", - "description": "Bu dosya için yeni bir ad girin", - "nameEmptyError": "Dosya adı boş bırakılamaz." - }, - "uploadedAt": "{} tarihinde yüklendi", - "linkedAt": "Bağlantısı {} tarihinde eklendi", - "failedToOpenMsg": "Açılamadı, dosya bulunamadı" - }, - "subPage": { - "handlingPasteHint": " - (yapıştırma işlemi)", - "errors": { - "failedDeletePage": "Sayfa silinemedi", - "failedCreatePage": "Sayfa oluşturulamadı", - "failedMovePage": "Sayfa bu belgeye taşınamadı", - "failedDuplicatePage": "Sayfa çoğaltılamadı", - "failedDuplicateFindView": "Sayfa çoğaltılamadı - orijinal görünüm bulunamadı" - } - }, - "cannotMoveToItsChildren": "Alt öğelerine taşınamaz" + "date": "Tarih" }, "outlineBlock": { "placeholder": "İçindekiler" @@ -1956,66 +1000,51 @@ "placeholder": "Komutlar için '/' yazın" }, "title": { - "placeholder": "Başlıksız" + "placeholder": "İsimsiz" }, "imageBlock": { - "placeholder": "Görsel eklemek için tıklayın", + "placeholder": "Resim eklemek için tıklayın", "upload": { "label": "Yükle", - "placeholder": "Görsel yüklemek için tıklayın" + "placeholder": "Resim yüklemek için tıklayın" }, "url": { - "label": "Görsel URL'si", - "placeholder": "Görsel URL'si girin" + "label": "Resim URL'si", + "placeholder": "Resim URL'sini girin" }, "ai": { - "label": "Yapay zeka ile görsel oluştur", - "placeholder": "Yapay zekanın görsel oluşturması için bir istek girin" + "label": "OpenAI ile resim oluştur", + "placeholder": "Lütfen OpenAI'nin resim oluşturması için komutu girin" }, "stability_ai": { - "label": "Stability AI ile görsel oluştur", - "placeholder": "Stability AI'nın görsel oluşturması için bir istek girin" + "label": "Stability AI ile resim oluştur", + "placeholder": "Lütfen Stability AI'nin resim oluşturması için komutu girin" }, - "support": "Görsel boyut sınırı 5MB'dır. Desteklenen formatlar: JPEG, PNG, GIF, SVG", + "support": "Resim boyutu sınırı 5MB'dir. Desteklenen formatlar: JPEG, PNG, GIF, SVG", "error": { - "invalidImage": "Geçersiz görsel", - "invalidImageSize": "Görsel boyutu 5MB'dan küçük olmalıdır", - "invalidImageFormat": "Görsel formatı desteklenmiyor. Desteklenen formatlar: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "Geçersiz görsel URL'si", - "noImage": "Böyle bir dosya veya dizin yok", - "multipleImagesFailed": "Bir veya daha fazla görsel yüklenemedi, lütfen tekrar deneyin" + "invalidImage": "Geçersiz resim", + "invalidImageSize": "Resim boyutu 5MB'den küçük olmalıdır", + "invalidImageFormat": "Resim formatı desteklenmiyor. Desteklenen formatlar: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "Geçersiz resim URL'si", + "noImage": "Böyle bir dosya veya dizin yok" }, "embedLink": { - "label": "Bağlantı yerleştir", - "placeholder": "Bir görsel bağlantısı yapıştırın veya yazın" + "label": "Bağlantıyı göm", + "placeholder": "Bir resim bağlantısını yapıştırın veya yazın" }, "unsplash": { "label": "Unsplash" }, - "searchForAnImage": "Bir görsel ara", - "pleaseInputYourOpenAIKey": "lütfen Ayarlar sayfasından yapay zeka anahtarınızı girin", - "saveImageToGallery": "Görseli kaydet", - "failedToAddImageToGallery": "Görsel galeriye eklenemedi", - "successToAddImageToGallery": "Görsel başarıyla galeriye eklendi", - "unableToLoadImage": "Görsel yüklenemedi", - "maximumImageSize": "Desteklenen maksimum görsel yükleme boyutu 10MB'dır", - "uploadImageErrorImageSizeTooBig": "Görsel boyutu 10MB'dan küçük olmalıdır", - "imageIsUploading": "Görsel yükleniyor", - "openFullScreen": "Tam ekran aç", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Önceki görsel", - "nextImageTooltip": "Sonraki görsel", - "zoomOutTooltip": "Uzaklaştır", - "zoomInTooltip": "Yakınlaştır", - "changeZoomLevelTooltip": "Yakınlaştırma seviyesini değiştir", - "openLocalImage": "Görseli aç", - "downloadImage": "Görseli indir", - "closeViewer": "Etkileşimli görüntüleyiciyi kapat", - "scalePercentage": "%{}", - "deleteImageTooltip": "Görseli sil" - } - } + "searchForAnImage": "Bir resim arayın", + "pleaseInputYourOpenAIKey": "Lütfen Ayarlar sayfasında OpenAI anahtarınızı girin", + "pleaseInputYourStabilityAIKey": "Lütfen Ayarlar sayfasında Stability AI anahtarınızı girin", + "saveImageToGallery": "Resmi kaydet", + "failedToAddImageToGallery": "Resim galeriye eklenemedi", + "successToAddImageToGallery": "Resim başarıyla galeriye eklendi", + "unableToLoadImage": "Resim yüklenemedi", + "maximumImageSize": "Desteklenen maksimum yükleme resim boyutu 10MB'dir", + "uploadImageErrorImageSizeTooBig": "Resim boyutu 10MB'den küçük olmalıdır", + "imageIsUploading": "Resim yükleniyor" }, "codeBlock": { "language": { @@ -2023,71 +1052,53 @@ "placeholder": "Dil seçin", "auto": "Otomatik" }, - "copyTooltip": "Kopyala", + "copyTooltip": "Kod bloğunun içeriğini kopyala", "searchLanguageHint": "Bir dil arayın", "codeCopiedSnackbar": "Kod panoya kopyalandı!" }, "inlineLink": { - "placeholder": "Bir bağlantı yapıştırın veya yazın", + "placeholder": "Bir bağlantıyı yapıştırın veya yazın", "openInNewTab": "Yeni sekmede aç", "copyLink": "Bağlantıyı kopyala", "removeLink": "Bağlantıyı kaldır", "url": { "label": "Bağlantı URL'si", - "placeholder": "Bağlantı URL'si girin" + "placeholder": "Bağlantı URL'sini girin" }, "title": { "label": "Bağlantı Başlığı", - "placeholder": "Bağlantı başlığı girin" + "placeholder": "Bağlantı başlığını girin" } }, "mention": { - "placeholder": "Bir kişiden, sayfadan veya tarihten bahsedin...", + "placeholder": "Bir kişiye, sayfaya veya tarihe bahset...", "page": { "label": "Sayfaya bağlantı", "tooltip": "Sayfayı açmak için tıklayın" }, "deleted": "Silindi", - "deletedContent": "Bu içerik mevcut değil veya silinmiş", - "noAccess": "Erişim Yok", - "deletedPage": "Silinmiş sayfa", - "trashHint": " - çöp kutusunda", - "morePages": "daha fazla sayfa" + "deletedContent": "Bu içerik mevcut değil veya silinmiş" }, "toolbar": { "resetToDefaultFont": "Varsayılana sıfırla" }, "errorBlock": { - "theBlockIsNotSupported": "Mevcut sürüm bu Bloğu desteklemiyor.", - "clickToCopyTheBlockContent": "Blok içeriğini kopyalamak için tıklayın", - "blockContentHasBeenCopied": "Blok içeriği kopyalandı.", - "parseError": "{} bloğu ayrıştırılırken bir hata oluştu.", - "copyBlockContent": "Blok içeriğini kopyala" - }, - "mobilePageSelector": { - "title": "Sayfa seç", - "failedToLoad": "Sayfa listesi yüklenemedi", - "noPagesFound": "Sayfa bulunamadı" - }, - "attachmentMenu": { - "choosePhoto": "Fotoğraf seç", - "takePicture": "Fotoğraf çek", - "chooseFile": "Dosya seç" + "theBlockIsNotSupported": "Geçerli sürüm bu bloğu desteklemiyor.", + "blockContentHasBeenCopied": "Blok içeriği kopyalandı." } }, "board": { "column": { - "label": "Sütun", "createNewCard": "Yeni", "renameGroupTooltip": "Grubu yeniden adlandırmak için basın", "createNewColumn": "Yeni bir grup ekle", "addToColumnTopTooltip": "Üste yeni bir kart ekle", "addToColumnBottomTooltip": "Alta yeni bir kart ekle", - "renameColumn": "Yeniden adlandır", + "renameColumn": "Yeniden Adlandır", "hideColumn": "Gizle", - "newGroup": "Yeni grup", + "newGroup": "Yeni Grup", "deleteColumn": "Sil", - "deleteColumnConfirmation": "Bu işlem bu grubu ve içindeki tüm kartları silecektir. Devam etmek istediğinizden emin misiniz?" + "deleteColumnConfirmation": "Bu, bu grubu ve içindeki tüm kartları silecektir.\nDevam etmek istediğinizden emin misiniz?" }, "hiddenGroupSection": { "sectionTitle": "Gizli Gruplar", @@ -2095,70 +1106,47 @@ "expandTooltip": "Gizli grupları görüntüle" }, "cardDetail": "Kart Detayı", - "cardActions": "Kart İşlemleri", - "cardDuplicated": "Kart çoğaltıldı", + "cardActions": "Kart Eylemleri", + "cardDuplicated": "Kart kopyalandı", "cardDeleted": "Kart silindi", "showOnCard": "Kart detayında göster", "setting": "Ayar", "propertyName": "Özellik adı", "menuName": "Pano", - "showUngrouped": "Gruplanmamış öğeleri göster", - "ungroupedButtonText": "Gruplanmamış", + "showUngrouped": "Grupsuz öğeleri göster", + "ungroupedButtonText": "Grupsuz", "ungroupedButtonTooltip": "Herhangi bir gruba ait olmayan kartları içerir", "ungroupedItemsTitle": "Panoya eklemek için tıklayın", - "groupBy": "Grupla", - "groupCondition": "Gruplama koşulu", + "groupBy": "Şuna göre grupla", "referencedBoardPrefix": "Görünümü", - "notesTooltip": "İçerideki notlar", + "notesTooltip": "İçindeki notlar", "mobile": { - "editURL": "URL'yi düzenle", + "editURL": "URL'yi Düzenle", "showGroup": "Grubu göster", "showGroupContent": "Bu grubu panoda göstermek istediğinizden emin misiniz?", "failedToLoad": "Pano görünümü yüklenemedi" - }, - "dateCondition": { - "weekOf": "{} - {} haftası", - "today": "Bugün", - "yesterday": "Dün", - "tomorrow": "Yarın", - "lastSevenDays": "Son 7 gün", - "nextSevenDays": "Gelecek 7 gün", - "lastThirtyDays": "Son 30 gün", - "nextThirtyDays": "Gelecek 30 gün" - }, - "noGroup": "Özelliğe göre gruplama yok", - "noGroupDesc": "Pano görünümleri görüntülemek için gruplamak üzere bir özellik gerektirir", - "media": { - "cardText": "{} {}", - "fallbackName": "dosyalar" } }, "calendar": { "menuName": "Takvim", - "defaultNewCalendarTitle": "Başlıksız", - "newEventButtonTooltip": "Yeni etkinlik ekle", + "defaultNewCalendarTitle": "İsimsiz", + "newEventButtonTooltip": "Yeni bir etkinlik ekle", "navigation": { "today": "Bugün", - "jumpToday": "Bugüne git", + "jumpToday": "Bugüne Git", "previousMonth": "Önceki Ay", - "nextMonth": "Sonraki Ay", - "views": { - "day": "Gün", - "week": "Hafta", - "month": "Ay", - "year": "Yıl" - } + "nextMonth": "Sonraki Ay" }, "mobileEventScreen": { "emptyTitle": "Henüz etkinlik yok", - "emptyBody": "Bu güne etkinlik eklemek için artı düğmesine basın." + "emptyBody": "Bu güne bir etkinlik oluşturmak için artı düğmesine basın." }, "settings": { "showWeekNumbers": "Hafta numaralarını göster", "showWeekends": "Hafta sonlarını göster", - "firstDayOfWeek": "Haftanın başlangıç günü", + "firstDayOfWeek": "Haftayı şunda başlat", "layoutDateField": "Takvimi şuna göre düzenle", - "changeLayoutDateField": "Düzen alanını değiştir", + "changeLayoutDateField": "Düzenleme alanını değiştir", "noDateTitle": "Tarih Yok", "noDateHint": { "zero": "Planlanmamış etkinlikler burada görünecek", @@ -2167,23 +1155,19 @@ }, "unscheduledEventsTitle": "Planlanmamış etkinlikler", "clickToAdd": "Takvime eklemek için tıklayın", - "name": "Takvim ayarları", - "clickToOpen": "Kaydı açmak için tıklayın" + "name": "Takvim ayarları" }, "referencedCalendarPrefix": "Görünümü", "quickJumpYear": "Şuraya git", - "duplicateEvent": "Etkinliği çoğalt" + "duplicateEvent": "Etkinliği kopyala" }, "errorDialog": { "title": "@:appName Hatası", - "howToFixFallback": "Rahatsızlık için özür dileriz! GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", - "howToFixFallbackHint1": "Rahatsızlık için özür dileriz! ", - "howToFixFallbackHint2": " sayfamızda hatanızı açıklayan bir sorun bildirin.", - "github": "GitHub'da görüntüle" + "howToFixFallback": "Verdiğimiz rahatsızlıktan dolayı özür dileriz! Lütfen GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", + "github": "GitHub'da Görüntüle" }, "search": { "label": "Ara", - "sidebarSearchIcon": "Ara ve hızlıca bir sayfaya git", "placeholder": { "actions": "Eylemleri ara..." } @@ -2194,7 +1178,7 @@ "fail": "Kopyalanamadı" } }, - "unSupportBlock": "Mevcut sürüm bu Bloğu desteklemiyor.", + "unSupportBlock": "Geçerli sürüm bu Bloğu desteklemiyor.", "views": { "deleteContentTitle": "{pageType} silmek istediğinizden emin misiniz?", "deleteContentCaption": "Bu {pageType} silerseniz, çöp kutusundan geri yükleyebilirsiniz." @@ -2217,22 +1201,22 @@ "search": "Emoji ara", "noRecent": "Son kullanılan emoji yok", "noEmojiFound": "Emoji bulunamadı", - "filter": "Filtrele", + "filter": "Filtre", "random": "Rastgele", - "selectSkinTone": "Ten rengi seç", + "selectSkinTone": "Cilt tonunu seç", "remove": "Emojiyi kaldır", "categories": { - "smileys": "İfadeler ve Duygular", - "people": "insanlar", - "animals": "doğa", - "food": "yiyecekler", - "activities": "aktiviteler", - "places": "yerler", - "objects": "nesneler", - "symbols": "semboller", - "flags": "bayraklar", - "nature": "doğa", - "frequentlyUsed": "sık kullanılanlar" + "smileys": "Gülen Yüzler & Duygular", + "people": "İnsanlar & Vücut", + "animals": "Hayvanlar & Doğa", + "food": "Yiyecek & İçecek", + "activities": "Aktiviteler", + "places": "Seyahat & Yerler", + "objects": "Nesneler", + "symbols": "Semboller", + "flags": "Bayraklar", + "nature": "Doğa", + "frequentlyUsed": "Sık Kullanılanlar" }, "skinTone": { "default": "Varsayılan", @@ -2241,12 +1225,10 @@ "medium": "Orta", "mediumDark": "Orta-Koyu", "dark": "Koyu" - }, - "openSourceIconsFrom": "Açık kaynak ikonlar" + } }, "inlineActions": { "noResults": "Sonuç yok", - "recentPages": "Son sayfalar", "pageReference": "Sayfa referansı", "docReference": "Belge referansı", "boardReference": "Pano referansı", @@ -2256,21 +1238,20 @@ "reminder": { "groupTitle": "Hatırlatıcı", "shortKeyword": "hatırlat" - }, - "createPage": "\"{}\"-alt sayfası oluştur" + } }, "datePicker": { - "dateTimeFormatTooltip": "Tarih ve saat biçimini ayarlardan değiştirin", - "dateFormat": "Tarih biçimi", + "dateTimeFormatTooltip": "Ayarlardan tarih ve saat formatını değiştirin", + "dateFormat": "Tarih formatı", "includeTime": "Saati dahil et", "isRange": "Bitiş tarihi", - "timeFormat": "Saat biçimi", + "timeFormat": "Saat formatı", "clearDate": "Tarihi temizle", "reminderLabel": "Hatırlatıcı", "selectReminder": "Hatırlatıcı seç", "reminderOptions": { "none": "Yok", - "atTimeOfEvent": "Etkinlik zamanında", + "atTimeOfEvent": "Etkinlik zamanı", "fiveMinsBefore": "5 dakika önce", "tenMinsBefore": "10 dakika önce", "fifteenMinsBefore": "15 dakika önce", @@ -2295,8 +1276,8 @@ "mobile": { "title": "Güncellemeler" }, - "emptyTitle": "Hepsi tamamlandı!", - "emptyBody": "Bekleyen bildirim veya eylem yok. Huzurun tadını çıkarın.", + "emptyTitle": "Her şey tamam!", + "emptyBody": "Bekleyen bildirim veya eylem yok. Sakinliğin tadını çıkarın.", "tabs": { "inbox": "Gelen Kutusu", "upcoming": "Yaklaşan" @@ -2310,16 +1291,16 @@ "ascending": "Artan", "descending": "Azalan", "groupByDate": "Tarihe göre grupla", - "showUnreadsOnly": "Sadece okunmamışları göster", + "showUnreadsOnly": "Yalnızca okunmamışları göster", "resetToDefault": "Varsayılana sıfırla" } }, "reminderNotification": { "title": "Hatırlatıcı", - "message": "Unutmadan önce bunu kontrol etmeyi unutmayın!", + "message": "Unutmadan önce bunu kontrol etmeyi unutma!", "tooltipDelete": "Sil", "tooltipMarkRead": "Okundu olarak işaretle", - "tooltipMarkUnread": "Okunmadı olarak işaretle" + "tooltipMarkUnread": "Okunmamış olarak işaretle" }, "findAndReplace": { "find": "Bul", @@ -2330,40 +1311,33 @@ "replaceAll": "Tümünü değiştir", "noResult": "Sonuç yok", "caseSensitive": "Büyük/küçük harf duyarlı", - "searchMore": "Daha fazla sonuç bulmak için arama yapın" + "searchMore": "Daha fazla sonuç bulmak için ara" }, "error": { "weAreSorry": "Üzgünüz", - "loadingViewError": "Bu görünümü yüklerken sorun yaşıyoruz. Lütfen internet bağlantınızı kontrol edin, uygulamayı yenileyin ve sorun devam ederse ekiple iletişime geçmekten çekinmeyin.", - "syncError": "Veriler başka bir cihazdan senkronize edilmedi", - "syncErrorHint": "Lütfen bu sayfayı son düzenlemenin yapıldığı cihazda yeniden açın, ardından mevcut cihazda tekrar açın.", - "clickToCopy": "Hata kodunu kopyalamak için tıklayın" + "loadingViewError": "Bu görünümü yüklemede sorun yaşıyoruz. Lütfen internet bağlantınızı kontrol edin, uygulamayı yenileyin ve sorun devam ederse ekibe ulaşmaktan çekinmeyin." }, "editor": { "bold": "Kalın", - "bulletedList": "Madde işaretli liste", - "bulletedListShortForm": "Madde işaretli", - "checkbox": "Onay kutusu", - "embedCode": "Kod Yerleştir", + "bulletedList": "Madde İşaretli Liste", + "bulletedListShortForm": "Madde", + "checkbox": "Onay Kutusu", + "embedCode": "Kodu Göm", "heading1": "H1", "heading2": "H2", "heading3": "H3", "highlight": "Vurgula", "color": "Renk", - "image": "Görsel", + "image": "Resim", "date": "Tarih", - "page": "Sayfa", "italic": "İtalik", "link": "Bağlantı", - "numberedList": "Numaralı liste", + "numberedList": "Numaralı Liste", "numberedListShortForm": "Numaralı", - "toggleHeading1ShortForm": "B1'i aç/kapat", - "toggleHeading2ShortForm": "B2'yi aç/kapat", - "toggleHeading3ShortForm": "B3'ü aç/kapat", "quote": "Alıntı", - "strikethrough": "Üstü çizili", + "strikethrough": "Üstü Çizili", "text": "Metin", - "underline": "Altı çizili", + "underline": "Altı Çizili", "fontColorDefault": "Varsayılan", "fontColorGray": "Gri", "fontColorBrown": "Kahverengi", @@ -2384,9 +1358,9 @@ "backgroundColorPurple": "Mor arka plan", "backgroundColorPink": "Pembe arka plan", "backgroundColorRed": "Kırmızı arka plan", - "backgroundColorLime": "Limon yeşili arka plan", - "backgroundColorAqua": "Su mavisi arka plan", - "done": "Tamam", + "backgroundColorLime": "Yeşil arka plan", + "backgroundColorAqua": "Su yeşili arka plan", + "done": "Bitti", "cancel": "İptal", "tint1": "Ton 1", "tint2": "Ton 2", @@ -2402,17 +1376,14 @@ "lightLightTint3": "Açık Pembe", "lightLightTint4": "Turuncu", "lightLightTint5": "Sarı", - "lightLightTint6": "Limon yeşili", + "lightLightTint6": "Limoni", "lightLightTint7": "Yeşil", - "lightLightTint8": "Su mavisi", + "lightLightTint8": "Su yeşili", "lightLightTint9": "Mavi", "urlHint": "URL", "mobileHeading1": "Başlık 1", "mobileHeading2": "Başlık 2", "mobileHeading3": "Başlık 3", - "mobileHeading4": "Başlık 4", - "mobileHeading5": "Başlık 5", - "mobileHeading6": "Başlık 6", "textColor": "Metin Rengi", "backgroundColor": "Arka Plan Rengi", "addYourLink": "Bağlantınızı ekleyin", @@ -2423,14 +1394,14 @@ "linkText": "Metin", "linkTextHint": "Lütfen metin girin", "linkAddressHint": "Lütfen URL girin", - "highlightColor": "Vurgulama rengi", - "clearHighlightColor": "Vurgulama rengini temizle", + "highlightColor": "Vurgu rengi", + "clearHighlightColor": "Vurgu rengini temizle", "customColor": "Özel renk", "hexValue": "Hex değeri", "opacity": "Opaklık", "resetToDefaultColor": "Varsayılan renge sıfırla", - "ltr": "Soldan sağa", - "rtl": "Sağdan sola", + "ltr": "LTR", + "rtl": "RTL", "auto": "Otomatik", "cut": "Kes", "copy": "Kopyala", @@ -2443,15 +1414,15 @@ "closeFind": "Kapat", "replace": "Değiştir", "replaceAll": "Tümünü değiştir", - "regex": "Düzenli ifade", + "regex": "Regex", "caseSensitive": "Büyük/küçük harf duyarlı", - "uploadImage": "Görsel Yükle", - "urlImage": "URL Görseli", + "uploadImage": "Resim Yükle", + "urlImage": "URL Resmi", "incorrectLink": "Hatalı Bağlantı", "upload": "Yükle", - "chooseImage": "Bir görsel seçin", + "chooseImage": "Bir resim seçin", "loading": "Yükleniyor", - "imageLoadFailed": "Görsel yüklenemedi", + "imageLoadFailed": "Resim yüklenemedi", "divider": "Ayırıcı", "table": "Tablo", "colAddBefore": "Önce ekle", @@ -2460,25 +1431,23 @@ "rowAddAfter": "Sonra ekle", "colRemove": "Kaldır", "rowRemove": "Kaldır", - "colDuplicate": "Çoğalt", - "rowDuplicate": "Çoğalt", + "colDuplicate": "Kopyala", + "rowDuplicate": "Kopyala", "colClear": "İçeriği Temizle", "rowClear": "İçeriği Temizle", - "slashPlaceHolder": "Blok eklemek için '/' yazın veya yazmaya başlayın", + "slashPlaceHolder": "Bir blok eklemek için '/' yazın veya yazmaya başlayın", "typeSomething": "Bir şeyler yazın...", - "toggleListShortForm": "Aç/Kapat", + "toggleListShortForm": "Değiştir", "quoteListShortForm": "Alıntı", "mathEquationShortForm": "Formül", "codeBlockShortForm": "Kod" }, "favorite": { "noFavorite": "Favori sayfa yok", - "noFavoriteHintText": "Favorilerinize eklemek için sayfayı sola kaydırın", - "removeFromSidebar": "Kenar çubuğundan kaldır", - "addToSidebar": "Kenar çubuğuna sabitle" + "noFavoriteHintText": "Sayfayı favorilerinize eklemek için sola kaydırın" }, "cardDetails": { - "notesPlaceholder": "Blok eklemek için / yazın veya yazmaya başlayın" + "notesPlaceholder": "Bir blok eklemek için '/' yazın veya yazmaya başlayın" }, "blockPlaceholders": { "todoList": "Yapılacaklar", @@ -2488,60 +1457,51 @@ "heading": "Başlık {}" }, "titleBar": { - "pageIcon": "Sayfa ikonu", + "pageIcon": "Sayfa simgesi", "language": "Dil", - "font": "Yazı tipi", + "font": "Yazı Tipi", "actions": "Eylemler", "date": "Tarih", "addField": "Alan ekle", - "userIcon": "Kullanıcı ikonu" + "userIcon": "Kullanıcı simgesi" }, "noLogFiles": "Günlük dosyası yok", "newSettings": { "myAccount": { "title": "Hesabım", - "subtitle": "Profilinizi özelleştirin, hesap güvenliğini yönetin, yapay zeka anahtarlarını ayarlayın veya hesabınıza giriş yapın.", - "profileLabel": "Hesap adı ve Profil resmi", + "subtitle": "Profilinizi özelleştirin, hesap güvenliğini yönetin, Open AI anahtarlarını yönetin veya hesabınıza giriş yapın.", + "profileLabel": "Hesap adı & Profil resmi", "profileNamePlaceholder": "Adınızı girin", "accountSecurity": "Hesap güvenliği", "2FA": "2 Adımlı Doğrulama", - "aiKeys": "Yapay zeka anahtarları", + "aiKeys": "AI anahtarları", "accountLogin": "Hesap Girişi", "updateNameError": "Ad güncellenemedi", - "updateIconError": "İkon güncellenemedi", + "updateIconError": "Simge güncellenemedi", "deleteAccount": { "title": "Hesabı Sil", "subtitle": "Hesabınızı ve tüm verilerinizi kalıcı olarak silin.", - "description": "Hesabınızı kalıcı olarak silin ve tüm çalışma alanlarından erişimi kaldırın.", "deleteMyAccount": "Hesabımı sil", "dialogTitle": "Hesabı sil", "dialogContent1": "Hesabınızı kalıcı olarak silmek istediğinizden emin misiniz?", - "dialogContent2": "Bu işlem GERİ ALINAMAZ ve tüm çalışma alanlarından erişiminizi kaldıracak, özel çalışma alanları dahil tüm hesabınızı silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır.", - "confirmHint1": "Onaylamak için lütfen \"@:newSettings.myAccount.deleteAccount.confirmHint3\" yazın.", - "confirmHint2": "Bu işlemin GERİ ALINAMAZ olduğunu ve hesabımı ve ilişkili tüm verileri kalıcı olarak sileceğini anlıyorum.", - "confirmHint3": "HESABIMI SİL", - "checkToConfirmError": "Silme işlemini onaylamak için kutuyu işaretlemelisiniz", - "failedToGetCurrentUser": "Mevcut kullanıcı e-postası alınamadı", - "confirmTextValidationFailed": "Onay metniniz \"@:newSettings.myAccount.deleteAccount.confirmHint3\" ile eşleşmiyor", - "deleteAccountSuccess": "Hesap başarıyla silindi" + "dialogContent2": "Bu işlem geri alınamaz ve tüm ekip alanlarına erişiminizi kaldıracak, hesabınızı (özel çalışma alanları dahil) tamamen silecek ve sizi tüm paylaşılan çalışma alanlarından çıkaracaktır." } }, "workplace": { - "name": "Çalışma alanı", + "name": "Çalışma Alanı", "title": "Çalışma Alanı Ayarları", - "subtitle": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarih, saat ve dilini özelleştirin.", + "subtitle": "Çalışma alanınızın görünümünü, temasını, yazı tipini, metin düzenini, tarihini, saatini ve dilini özelleştirin.", "workplaceName": "Çalışma alanı adı", "workplaceNamePlaceholder": "Çalışma alanı adını girin", - "workplaceIcon": "Çalışma alanı ikonu", - "workplaceIconSubtitle": "Bir görsel yükleyin veya çalışma alanınız için bir emoji kullanın. İkon kenar çubuğunuzda ve bildirimlerde görünecektir.", + "workplaceIcon": "Çalışma alanı simgesi", + "workplaceIconSubtitle": "Çalışma alanınız için bir resim yükleyin veya bir emoji kullanın. Simge kenar çubuğunuzda ve bildirimlerinizde görünecektir", "renameError": "Çalışma alanı yeniden adlandırılamadı", - "updateIconError": "İkon güncellenemedi", - "chooseAnIcon": "Bir ikon seçin", + "updateIconError": "Simge güncellenemedi", "appearance": { "name": "Görünüm", "themeMode": { "auto": "Otomatik", - "light": "Aydınlık", + "light": "Açık", "dark": "Koyu" }, "language": "Dil" @@ -2553,531 +1513,14 @@ "noNetworkConnected": "Ağ bağlantısı yok" } }, - "pageStyle": { - "title": "Sayfa stili", - "layout": "Düzen", - "coverImage": "Kapak görseli", - "pageIcon": "Sayfa ikonu", - "colors": "Renkler", - "gradient": "Gradyan", - "backgroundImage": "Arka plan görseli", - "presets": "Hazır ayarlar", - "photo": "Fotoğraf", - "unsplash": "Unsplash", - "pageCover": "Sayfa kapağı", - "none": "Yok", - "openSettings": "Ayarları Aç", - "photoPermissionTitle": "@:appName fotoğraf kitaplığınıza erişmek istiyor", - "photoPermissionDescription": "@:appName belgelerinize görsel ekleyebilmeniz için fotoğraflarınıza erişmeye ihtiyaç duyuyor", - "cameraPermissionTitle": "@:appName kameranıza erişmek istiyor", - "cameraPermissionDescription": "@:appName belgelerinize kameradan görsel ekleyebilmeniz için kameranıza erişmeye ihtiyaç duyuyor", - "doNotAllow": "İzin Verme", - "image": "Görsel" - }, "commandPalette": { - "placeholder": "Ara veya bir soru sor...", + "placeholder": "Görünümleri aramak için yazın...", "bestMatches": "En iyi eşleşmeler", "recentHistory": "Son geçmiş", "navigateHint": "gezinmek için", - "loadingTooltip": "Sonuçları arıyoruz...", + "loadingTooltip": "Sonuçlar aranıyor...", "betaLabel": "BETA", - "betaTooltip": "Şu anda yalnızca sayfalarda ve belgelerde içerik aramayı destekliyoruz", - "fromTrashHint": "Çöp kutusundan", - "noResultsHint": "Aradığınızı bulamadık, başka bir terim aramayı deneyin.", - "clearSearchTooltip": "Arama alanını temizle" - }, - "space": { - "delete": "Sil", - "deleteConfirmation": "Sil: ", - "deleteConfirmationDescription": "Bu Alan içindeki tüm sayfalar silinecek ve Çöp Kutusuna taşınacak, yayınlanmış sayfaların yayını kaldırılacaktır.", - "rename": "Alanı Yeniden Adlandır", - "changeIcon": "İkonu değiştir", - "manage": "Alanı Yönet", - "addNewSpace": "Alan Oluştur", - "collapseAllSubPages": "Tüm alt sayfaları daralt", - "createNewSpace": "Yeni bir alan oluştur", - "createSpaceDescription": "İşlerinizi daha iyi organize etmek için birden fazla genel ve özel alan oluşturun.", - "spaceName": "Alan adı", - "spaceNamePlaceholder": "örn. Pazarlama, Mühendislik, İK", - "permission": "Alan izni", - "publicPermission": "Genel", - "publicPermissionDescription": "Tam erişime sahip tüm çalışma alanı üyeleri", - "privatePermission": "Özel", - "privatePermissionDescription": "Bu alana yalnızca siz erişebilirsiniz", - "spaceIconBackground": "Arka plan rengi", - "spaceIcon": "İkon", - "dangerZone": "Tehlikeli Bölge", - "unableToDeleteLastSpace": "Son Alan silinemez", - "unableToDeleteSpaceNotCreatedByYou": "Başkaları tarafından oluşturulan alanlar silinemez", - "enableSpacesForYourWorkspace": "Çalışma alanınız için Alanları etkinleştirin", - "title": "Alanlar", - "defaultSpaceName": "Genel", - "upgradeSpaceTitle": "Alanları Etkinleştir", - "upgradeSpaceDescription": "Çalışma alanınızı daha iyi organize etmek için birden fazla genel ve özel Alan oluşturun.", - "upgrade": "Güncelle", - "upgradeYourSpace": "Birden fazla Alan oluştur", - "quicklySwitch": "Hızlıca sonraki alana geç", - "duplicate": "Alanı Çoğalt", - "movePageToSpace": "Sayfayı alana taşı", - "cannotMovePageToDatabase": "Sayfa veritabanına taşınamaz", - "switchSpace": "Alan değiştir", - "spaceNameCannotBeEmpty": "Alan adı boş olamaz", - "success": { - "deleteSpace": "Alan başarıyla silindi", - "renameSpace": "Alan başarıyla yeniden adlandırıldı", - "duplicateSpace": "Alan başarıyla çoğaltıldı", - "updateSpace": "Alan başarıyla güncellendi" - }, - "error": { - "deleteSpace": "Alan silinemedi", - "renameSpace": "Alan yeniden adlandırılamadı", - "duplicateSpace": "Alan çoğaltılamadı", - "updateSpace": "Alan güncellenemedi" - }, - "createSpace": "Alan oluştur", - "manageSpace": "Alanı yönet", - "renameSpace": "Alanı yeniden adlandır", - "mSpaceIconColor": "Alan ikonu rengi", - "mSpaceIcon": "Alan ikonu" - }, - "publish": { - "hasNotBeenPublished": "Bu sayfa henüz yayınlanmadı", - "spaceHasNotBeenPublished": "Henüz bir alanın yayınlanması desteklenmiyor", - "reportPage": "Sayfayı bildir", - "databaseHasNotBeenPublished": "Veritabanı yayınlama henüz desteklenmiyor.", - "createdWith": "Şununla oluşturuldu", - "downloadApp": "AppFlowy'yi İndir", - "copy": { - "codeBlock": "Kod bloğunun içeriği panoya kopyalandı", - "imageBlock": "Görsel bağlantısı panoya kopyalandı", - "mathBlock": "Matematik denklemi panoya kopyalandı", - "fileBlock": "Dosya bağlantısı panoya kopyalandı" - }, - "containsPublishedPage": "Bu sayfa bir veya daha fazla yayınlanmış sayfa içeriyor. Devam ederseniz, yayından kaldırılacaklar. Silme işlemine devam etmek istiyor musunuz?", - "publishSuccessfully": "Başarıyla yayınlandı", - "unpublishSuccessfully": "Yayından kaldırma başarılı", - "publishFailed": "Yayınlanamadı", - "unpublishFailed": "Yayından kaldırılamadı", - "noAccessToVisit": "Bu sayfaya erişim yok...", - "createWithAppFlowy": "AppFlowy ile bir web sitesi oluşturun", - "fastWithAI": "Yapay zeka ile hızlı ve kolay.", - "tryItNow": "Şimdi deneyin", - "onlyGridViewCanBePublished": "Yalnızca Tablo görünümü yayınlanabilir", - "database": { - "zero": "{} seçili görünümü yayınla", - "one": "{} seçili görünümü yayınla", - "many": "{} seçili görünümü yayınla", - "other": "{} seçili görünümü yayınla" - }, - "mustSelectPrimaryDatabase": "Ana görünüm seçilmelidir", - "noDatabaseSelected": "Veritabanı seçilmedi, lütfen en az bir veritabanı seçin.", - "unableToDeselectPrimaryDatabase": "Ana veritabanının seçimi kaldırılamaz", - "saveThisPage": "Bu şablonla başlayın", - "duplicateTitle": "Nereye eklemek istersiniz", - "selectWorkspace": "Bir çalışma alanı seçin", - "addTo": "Şuraya ekle", - "duplicateSuccessfully": "Çalışma alanınıza eklendi", - "duplicateSuccessfullyDescription": "AppFlowy yüklü değil mi? 'İndir'e tıkladıktan sonra indirme otomatik olarak başlayacak.", - "downloadIt": "İndir", - "openApp": "Uygulamada aç", - "duplicateFailed": "Çoğaltma başarısız", - "membersCount": { - "zero": "Üye yok", - "one": "1 üye", - "many": "{count} üye", - "other": "{count} üye" - }, - "useThisTemplate": "Bu şablonu kullan" - }, - "web": { - "continue": "Devam et", - "or": "veya", - "continueWithGoogle": "Google ile devam et", - "continueWithGithub": "GitHub ile devam et", - "continueWithDiscord": "Discord ile devam et", - "continueWithApple": "Apple ile devam et", - "moreOptions": "Daha fazla seçenek", - "collapse": "Daralt", - "signInAgreement": "Yukarıdaki \"Devam et\" düğmesine tıklayarak AppFlowy'nin şunlarını kabul etmiş olursunuz:", - "and": "ve", - "termOfUse": "Kullanım Koşulları", - "privacyPolicy": "Gizlilik Politikası", - "signInError": "Giriş hatası", - "login": "Kaydol veya giriş yap", - "fileBlock": { - "uploadedAt": "{time} tarihinde yüklendi", - "linkedAt": "Bağlantısı {time} tarihinde eklendi", - "empty": "Bir dosya yükleyin veya yerleştirin", - "uploadFailed": "Yükleme başarısız, lütfen tekrar deneyin", - "retry": "Tekrar dene" - }, - "importNotion": "Notion'dan içe aktar", - "import": "İçe aktar", - "importSuccess": "Başarıyla yüklendi", - "importSuccessMessage": "İçe aktarma tamamlandığında size bildirim göndereceğiz. Bundan sonra, içe aktarılan sayfalarınızı kenar çubuğunda görüntüleyebilirsiniz.", - "importFailed": "İçe aktarma başarısız, lütfen dosya formatını kontrol edin", - "dropNotionFile": "Yüklemek için Notion zip dosyanızı buraya bırakın veya göz atmak için tıklayın", - "error": { - "pageNameIsEmpty": "Sayfa adı boş, lütfen başka bir tane deneyin" - } - }, - "globalComment": { - "comments": "Yorumlar", - "addComment": "Yorum ekle", - "reactedBy": "tepki verenler", - "addReaction": "Tepki ekle", - "reactedByMore": "ve {count} diğer", - "showSeconds": { - "one": "1 saniye önce", - "other": "{count} saniye önce", - "zero": "Az önce", - "many": "{count} saniye önce" - }, - "showMinutes": { - "one": "1 dakika önce", - "other": "{count} dakika önce", - "many": "{count} dakika önce" - }, - "showHours": { - "one": "1 saat önce", - "other": "{count} saat önce", - "many": "{count} saat önce" - }, - "showDays": { - "one": "1 gün önce", - "other": "{count} gün önce", - "many": "{count} gün önce" - }, - "showMonths": { - "one": "1 ay önce", - "other": "{count} ay önce", - "many": "{count} ay önce" - }, - "showYears": { - "one": "1 yıl önce", - "other": "{count} yıl önce", - "many": "{count} yıl önce" - }, - "reply": "Yanıtla", - "deleteComment": "Yorumu sil", - "youAreNotOwner": "Bu yorumun sahibi siz değilsiniz", - "confirmDeleteDescription": "Bu yorumu silmek istediğinizden emin misiniz?", - "hasBeenDeleted": "Silindi", - "replyingTo": "Yanıtlanıyor", - "noAccessDeleteComment": "Bu yorumu silme izniniz yok", - "collapse": "Daralt", - "readMore": "Devamını oku", - "failedToAddComment": "Yorum eklenemedi", - "commentAddedSuccessfully": "Yorum başarıyla eklendi.", - "commentAddedSuccessTip": "Az önce bir yorum eklediniz veya yanıtladınız. En son yorumları görmek için başa dönmek ister misiniz?" - }, - "template": { - "asTemplate": "Şablon olarak kaydet", - "name": "Şablon adı", - "description": "Şablon Açıklaması", - "about": "Şablon Hakkında", - "deleteFromTemplate": "Şablonlardan sil", - "preview": "Şablon Önizleme", - "categories": "Şablon Kategorileri", - "isNewTemplate": "Yeni şablona SABİTLE", - "featured": "Öne Çıkanlara SABİTLE", - "relatedTemplates": "İlgili Şablonlar", - "requiredField": "{field} gereklidir", - "addCategory": "\"{category}\" ekle", - "addNewCategory": "Yeni kategori ekle", - "addNewCreator": "Yeni oluşturucu ekle", - "deleteCategory": "Kategoriyi sil", - "editCategory": "Kategoriyi düzenle", - "editCreator": "Oluşturucuyu düzenle", - "category": { - "name": "Kategori adı", - "icon": "Kategori ikonu", - "bgColor": "Kategori arka plan rengi", - "priority": "Kategori önceliği", - "desc": "Kategori açıklaması", - "type": "Kategori türü", - "icons": "Kategori İkonları", - "colors": "Kategori Renkleri", - "byUseCase": "Kullanım Durumuna Göre", - "byFeature": "Özelliğe Göre", - "deleteCategory": "Kategoriyi sil", - "deleteCategoryDescription": "Bu kategoriyi silmek istediğinizden emin misiniz?", - "typeToSearch": "Kategorilerde arama yapmak için yazın..." - }, - "creator": { - "label": "Şablon Oluşturucu", - "name": "Oluşturucu adı", - "avatar": "Oluşturucu avatarı", - "accountLinks": "Oluşturucu hesap bağlantıları", - "uploadAvatar": "Avatar yüklemek için tıklayın", - "deleteCreator": "Oluşturucuyu sil", - "deleteCreatorDescription": "Bu oluşturucuyu silmek istediğinizden emin misiniz?", - "typeToSearch": "Oluşturucularda arama yapmak için yazın..." - }, - "uploadSuccess": "Şablon başarıyla yüklendi", - "uploadSuccessDescription": "Şablonunuz başarıyla yüklendi. Artık şablon galerisinde görüntüleyebilirsiniz.", - "viewTemplate": "Şablonu görüntüle", - "deleteTemplate": "Şablonu sil", - "deleteSuccess": "Şablon başarıyla silindi", - "deleteTemplateDescription": "Bu işlem mevcut sayfayı veya yayınlanma durumunu etkilemeyecektir. Bu şablonu silmek istediğinizden emin misiniz?", - "addRelatedTemplate": "İlgili şablon ekle", - "removeRelatedTemplate": "İlgili şablonu kaldır", - "uploadAvatar": "Avatar yükle", - "searchInCategory": "{category} içinde ara", - "label": "Şablonlar" - }, - "fileDropzone": { - "dropFile": "Yüklemek için dosyayı bu alana tıklayın veya sürükleyin", - "uploading": "Yükleniyor...", - "uploadFailed": "Yükleme başarısız", - "uploadSuccess": "Yükleme başarılı", - "uploadSuccessDescription": "Dosya başarıyla yüklendi", - "uploadFailedDescription": "Dosya yüklenemedi", - "uploadingDescription": "Dosya yükleniyor" - }, - "gallery": { - "preview": "Tam ekran aç", - "copy": "Kopyala", - "download": "İndir", - "prev": "Önceki", - "next": "Sonraki", - "resetZoom": "Yakınlaştırmayı sıfırla", - "zoomIn": "Yakınlaştır", - "zoomOut": "Uzaklaştır" - }, - "invitation": { - "join": "Katıl", - "on": "tarihinde", - "invitedBy": "Davet eden", - "membersCount": { - "zero": "{count} üye", - "one": "{count} üye", - "many": "{count} üye", - "other": "{count} üye" - }, - "tip": "Aşağıdaki iletişim bilgileriyle bu çalışma alanına katılmaya davet edildiniz. Bu bilgiler yanlışsa, davetiyeyi yeniden göndermesi için yöneticinizle iletişime geçin.", - "joinWorkspace": "Çalışma alanına katıl", - "success": "Çalışma alanına başarıyla katıldınız", - "successMessage": "Artık içindeki tüm sayfalara ve çalışma alanlarına erişebilirsiniz.", - "openWorkspace": "AppFlowy'yi Aç", - "alreadyAccepted": "Bu daveti zaten kabul ettiniz", - "errorModal": { - "title": "Bir hata oluştu", - "description": "Mevcut hesabınızın {email} bu çalışma alanına erişimi olmayabilir. Lütfen doğru hesapla giriş yapın veya yardım için çalışma alanı sahibiyle iletişime geçin.", - "contactOwner": "Sahiple iletişime geç", - "close": "Ana sayfaya dön", - "changeAccount": "Hesap değiştir" - } - }, - "requestAccess": { - "title": "Bu sayfaya erişim yok", - "subtitle": "Bu sayfanın sahibinden erişim talep edebilirsiniz. Onaylandığında, sayfayı görüntüleyebilirsiniz.", - "requestAccess": "Erişim talep et", - "backToHome": "Ana sayfaya dön", - "tip": "Şu anda <link/> olarak giriş yapmış durumdasınız.", - "mightBe": "Farklı bir hesapla <login/> yapmanız gerekebilir.", - "successful": "Talep başarıyla gönderildi", - "successfulMessage": "Sahibi talebinizi onayladığında size bildirim gönderilecek.", - "requestError": "Erişim talebi başarısız oldu", - "repeatRequestError": "Bu sayfa için zaten erişim talebinde bulundunuz" - }, - "approveAccess": { - "title": "Çalışma Alanı Katılım Talebini Onayla", - "requestSummary": "<user/>, <workspace/>'a katılmak ve <page/>'a erişmek istiyor", - "upgrade": "yükselt", - "downloadApp": "AppFlowy'yi İndir", - "approveButton": "Onayla", - "approveSuccess": "Başarıyla onaylandı", - "approveError": "Onaylama başarısız, çalışma alanı plan limitinin aşılmadığından emin olun", - "getRequestInfoError": "Talep bilgisi alınamadı", - "memberCount": { - "zero": "Üye yok", - "one": "1 üye", - "many": "{count} üye", - "other": "{count} üye" - }, - "alreadyProTitle": "Çalışma alanı plan limitine ulaştınız", - "alreadyProMessage": "Daha fazla üye eklemek için <email/> ile iletişime geçmelerini isteyin", - "repeatApproveError": "Bu talebi zaten onayladınız", - "ensurePlanLimit": "Çalışma alanı plan limitinin aşılmadığından emin olun. Limit aşılırsa, çalışma alanı planını <upgrade/> veya <download/>.", - "requestToJoin": "katılmak için talep etti", - "asMember": "üye olarak" - }, - "upgradePlanModal": { - "title": "Pro'ya Yükselt", - "message": "{name} ücretsiz üye limitine ulaştı. Daha fazla üye davet etmek için Pro Plana yükseltin.", - "upgradeSteps": "AppFlowy'de planınızı nasıl yükseltirsiniz:", - "step1": "1. Ayarlar'a gidin", - "step2": "2. 'Plan'a tıklayın", - "step3": "3. 'Planı Değiştir'i seçin", - "appNote": "Not: ", - "actionButton": "Yükselt", - "downloadLink": "Uygulamayı İndir", - "laterButton": "Sonra", - "refreshNote": "Başarılı yükseltmeden sonra, yeni özelliklerinizi etkinleştirmek için <refresh/> tıklayın.", - "refresh": "buraya" - }, - "breadcrumbs": { - "label": "Gezinti menüsü" - }, - "time": { - "justNow": "Az önce", - "seconds": { - "one": "1 saniye", - "other": "{count} saniye" - }, - "minutes": { - "one": "1 dakika", - "other": "{count} dakika" - }, - "hours": { - "one": "1 saat", - "other": "{count} saat" - }, - "days": { - "one": "1 gün", - "other": "{count} gün" - }, - "weeks": { - "one": "1 hafta", - "other": "{count} hafta" - }, - "months": { - "one": "1 ay", - "other": "{count} ay" - }, - "years": { - "one": "1 yıl", - "other": "{count} yıl" - }, - "ago": "önce", - "yesterday": "Dün", - "today": "Bugün" - }, - "members": { - "zero": "Üye yok", - "one": "1 üye", - "many": "{count} üye", - "other": "{count} üye" - }, - "tabMenu": { - "close": "Kapat", - "closeDisabledHint": "Sabitlenmiş bir sekme kapatılamaz, lütfen önce sabitlemeyi kaldırın", - "closeOthers": "Diğer sekmeleri kapat", - "closeOthersHint": "Bu işlem, bu sekme dışındaki tüm sabitlenmemiş sekmeleri kapatacaktır", - "closeOthersDisabledHint": "Tüm sekmeler sabitlenmiş, kapatılacak sekme bulunamadı", - "favorite": "Favorilere ekle", - "unfavorite": "Favorilerden kaldır", - "favoriteDisabledHint": "Bu görünüm favorilere eklenemez", - "pinTab": "Sabitle", - "unpinTab": "Sabitlemeyi kaldır" - }, - "openFileMessage": { - "success": "Dosya başarıyla açıldı", - "fileNotFound": "Dosya bulunamadı", - "noAppToOpenFile": "Bu dosyayı açacak uygulama yok", - "permissionDenied": "Bu dosyayı açma izni yok", - "unknownError": "Dosya açılamadı" - }, - "inviteMember": { - "requestInviteMembers": "Çalışma alanınıza davet et", - "inviteFailedMemberLimit": "Üye limitine ulaşıldı, lütfen ", - "upgrade": "yükselt", - "addEmail": "email@example.com, email2@example.com...", - "requestInvites": "Davetleri gönder", - "inviteAlready": "Bu e-postayı zaten davet ettiniz: {email}", - "inviteSuccess": "Davet başarıyla gönderildi", - "description": "E-postaları aralarına virgül koyarak aşağıya girin. Ücretlendirme üye sayısına göre yapılır.", - "emails": "E-posta" - }, - "quickNote": { - "label": "Hızlı Not", - "quickNotes": "Hızlı Notlar", - "search": "Hızlı Notlarda Ara", - "collapseFullView": "Tam görünümü daralt", - "expandFullView": "Tam görünümü genişlet", - "createFailed": "Hızlı Not oluşturulamadı", - "quickNotesEmpty": "Hızlı Not yok", - "emptyNote": "Boş not", - "deleteNotePrompt": "Seçilen not kalıcı olarak silinecektir. Silmek istediğinizden emin misiniz?", - "addNote": "Yeni Not", - "noAdditionalText": "Ek metin yok" - }, - "subscribe": { - "upgradePlanTitle": "Planları karşılaştır ve seç", - "yearly": "Yıllık", - "save": "%{discount} tasarruf", - "monthly": "Aylık", - "priceIn": "Fiyat: ", - "free": "Ücretsiz", - "pro": "Pro", - "freeDescription": "2 üyeye kadar bireyler için her şeyi organize etmek için", - "proDescription": "Küçük ekipler için projeleri ve ekip bilgisini yönetmek için", - "proDuration": { - "monthly": "üye başına aylık\naylık faturalandırma", - "yearly": "üye başına aylık\nyıllık faturalandırma" - }, - "cancel": "Alt plana geç", - "changePlan": "Pro Plana yükselt", - "everythingInFree": "Ücretsiz plandaki her şey +", - "currentPlan": "Mevcut", - "freeDuration": "süresiz", - "freePoints": { - "first": "2 üyeye kadar 1 işbirliği çalışma alanı", - "second": "Sınırsız sayfa ve blok", - "three": "5 GB depolama", - "four": "Akıllı arama", - "five": "20 yapay zeka yanıtı", - "six": "Mobil uygulama", - "seven": "Gerçek zamanlı işbirliği" - }, - "proPoints": { - "first": "Sınırsız depolama", - "second": "10 çalışma alanı üyesine kadar", - "three": "Sınırsız yapay zeka yanıtı", - "four": "Sınırsız dosya yükleme", - "five": "Özel alan adı" - }, - "cancelPlan": { - "title": "Gitmenize üzüldük", - "success": "Aboneliğiniz başarıyla iptal edildi", - "description": "Gitmenize üzüldük. AppFlowy'yi geliştirmemize yardımcı olmak için geri bildiriminizi almak isteriz. Lütfen birkaç soruyu yanıtlamak için zaman ayırın.", - "commonOther": "Diğer", - "otherHint": "Yanıtınızı buraya yazın", - "questionOne": { - "question": "AppFlowy Pro aboneliğinizi iptal etmenize ne sebep oldu?", - "answerOne": "Maliyet çok yüksek", - "answerTwo": "Özellikler beklentileri karşılamadı", - "answerThree": "Daha iyi bir alternatif buldum", - "answerFour": "Maliyeti karşılayacak kadar kullanmadım", - "answerFive": "Hizmet sorunu veya teknik zorluklar" - }, - "questionTwo": { - "question": "Gelecekte AppFlowy Pro'ya yeniden abone olma olasılığınız nedir?", - "answerOne": "Çok muhtemel", - "answerTwo": "Biraz muhtemel", - "answerThree": "Emin değilim", - "answerFour": "Muhtemel değil", - "answerFive": "Hiç muhtemel değil" - }, - "questionThree": { - "question": "Aboneliğiniz sırasında en çok hangi Pro özelliğine değer verdiniz?", - "answerOne": "Çoklu kullanıcı işbirliği", - "answerTwo": "Daha uzun süreli versiyon geçmişi", - "answerThree": "Sınırsız yapay zeka yanıtları", - "answerFour": "Yerel yapay zeka modellerine erişim" - }, - "questionFour": { - "question": "AppFlowy ile genel deneyiminizi nasıl tanımlarsınız?", - "answerOne": "Harika", - "answerTwo": "İyi", - "answerThree": "Ortalama", - "answerFour": "Ortalamanın altında", - "answerFive": "Memnun değilim" - } - } - }, - "ai": { - "contentPolicyViolation": "Hassas içerik nedeniyle görsel oluşturma başarısız oldu. Lütfen girdinizi yeniden düzenleyip tekrar deneyin" + "betaTooltip": "Şu anda yalnızca sayfaları aramayı destekliyoruz", + "fromTrashHint": "Çöp kutusundan" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 394801ed21..9e0cdf64f4 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -2,14 +2,12 @@ "appName": "AppFlowy", "defaultUsername": "Я", "welcomeText": "Ласкаво просимо в @:appName", - "welcomeTo": "Ласкаво просимо до", "githubStarText": "Поставити зірку на GitHub", "subscribeNewsletterText": "Підпишіться на розсилку новин", "letsGoButtonText": "Почнемо", "title": "Заголовок", "youCanAlso": "Ви також можете", "and": "та", - "failedToOpenUrl": "Не вдалося відкрити URL-адресу: {}", "blockActions": { "addBelowTooltip": "Клацніть, щоб додати нижче", "addAboveCmd": "Alt+click", @@ -36,39 +34,16 @@ "loginButtonText": "Увійти", "loginStartWithAnonymous": "Почати анонімну сесію", "continueAnonymousUser": "Продовжити анонімну сесію", - "anonymous": "Анонім", "buttonText": "Увійти", - "signingInText": "Вхід...", "forgotPassword": "Забули пароль?", "emailHint": "Електронна пошта", "passwordHint": "Пароль", "dontHaveAnAccount": "Немаєте облікового запису?", - "createAccount": "Створити акаунт", "repeatPasswordEmptyError": "Повторний пароль не може бути порожнім", "unmatchedPasswordError": "Повторний пароль не співпадає з паролем", "syncPromptMessage": "Синхронізація даних може зайняти трохи часу. Будь ласка, не закривайте цю сторінку", "or": "АБО", - "signInWithGoogle": "Продовжуйте з Google", - "signInWithGithub": "Продовжуйте з Github", - "signInWithDiscord": "Продовжуйте з Discord", - "signInWithApple": "Продовжуйте з Apple", - "continueAnotherWay": "Продовжуйте іншим шляхом", - "signUpWithGoogle": "Зареєструватися в Google", - "signUpWithGithub": "Зареєструйтеся на Github", - "signUpWithDiscord": "Зареєструйтеся в Discord", "signInWith": "Увійти за допомогою:", - "signInWithEmail": "Продовжити з електронною поштою", - "signInWithMagicLink": "Продовжити", - "signUpWithMagicLink": "Зареєструйтеся за допомогою Magic Link", - "pleaseInputYourEmail": "Будь ласка, введіть адресу електронної пошти", - "settings": "Налаштування", - "magicLinkSent": "Magic Link надіслано!", - "invalidEmail": "Будь ласка, введіть дійсну адресу електронної пошти", - "alreadyHaveAnAccount": "Вже є акаунт?", - "logIn": "Авторизуватися", - "generalError": "Щось пішло не так. Будь ласка спробуйте пізніше", - "limitRateError": "З міркувань безпеки ви можете запитувати чарівне посилання лише кожні 60 секунд", - "magicLinkSentDescription": "Чарівне посилання надіслано на вашу електронну адресу. Натисніть посилання, щоб завершити вхід. Термін дії посилання закінчиться через 5 хвилин.", "LogInWithGoogle": "Увійти за допомогою Google", "LogInWithGithub": "Увійти за допомогою Github", "LogInWithDiscord": "Увійти за допомогою Discord" @@ -77,51 +52,21 @@ "chooseWorkspace": "Виберіть свій робочий простір", "create": "Створити робочий простір", "reset": "Скинути робочий простір", - "renameWorkspace": "Перейменувати робочу область", "resetWorkspacePrompt": "Скидання робочого простору призведе до видалення всіх сторінок та даних у ньому. Ви впевнені, що хочете скинути робочий простір? Також ви можете звернутися до служби підтримки для відновлення робочого простору", "hint": "робочий простір", "notFoundError": "Робочий простір не знайдено", "failedToLoad": "Щось пішло не так! Не вдалося завантажити робочий простір. Спробуйте закрити будь-який відкритий екземпляр AppFlowy та спробуйте ще раз.", "errorActions": { "reportIssue": "Повідомити про проблему", - "reportIssueOnGithub": "Повідомити про проблему на Github", - "exportLogFiles": "Експорт файлів журналу", "reachOut": "Звернутися на Discord" - }, - "menuTitle": "Робочі області", - "deleteWorkspaceHintText": "Ви впевнені, що хочете видалити робочу область? Цю дію неможливо скасувати, і всі опубліковані вами сторінки буде скасовано.", - "createSuccess": "Робочу область успішно створено", - "createFailed": "Не вдалося створити робочу область", - "createLimitExceeded": "Ви досягли максимального ліміту робочого простору, дозволеного для вашого облікового запису. Якщо вам потрібні додаткові робочі області, щоб продовжити роботу, надішліть запит на Github", - "deleteSuccess": "Робочу область успішно видалено", - "deleteFailed": "Не вдалося видалити робочу область", - "openSuccess": "Робочу область відкрито", - "openFailed": "Не вдалося відкрити робочу область", - "renameSuccess": "Робочу область успішно перейменовано", - "renameFailed": "Не вдалося перейменувати робочу область", - "updateIconSuccess": "Значок робочої області успішно оновлено", - "updateIconFailed": "Не вдалося оновити значок робочої області", - "cannotDeleteTheOnlyWorkspace": "Неможливо видалити єдину робочу область", - "fetchWorkspacesFailed": "Не вдалося отримати робочі області", - "leaveCurrentWorkspace": "Залишити робочу область", - "leaveCurrentWorkspacePrompt": "Ви впевнені, що бажаєте залишити поточну робочу область?" + } }, "shareAction": { "buttonText": "Поділитися", "workInProgress": "Скоро буде доступно", "markdown": "Markdown", - "html": "HTML", - "clipboard": "Копіювати в буфер обміну", "csv": "CSV", - "copyLink": "Скопіювати посилання", - "publishToTheWeb": "Опублікувати в Інтернеті", - "publishToTheWebHint": "Створіть веб-сайт за допомогою AppFlowy", - "publish": "Опублікувати", - "unPublish": "Скасувати публікацію", - "visitSite": "Відвідайте сайт", - "exportAsTab": "Експортувати як", - "publishTab": "Опублікувати", - "shareTab": "Поділіться" + "copyLink": "Скопіювати посилання" }, "moreAction": { "small": "малий", @@ -129,12 +74,7 @@ "large": "великий", "fontSize": "Розмір шрифту", "import": "Імпортувати", - "moreOptions": "Більше опцій", - "wordCount": "Кількість слів: {}", - "charCount": "Кількість символів: {}", - "createdAt": "Створено: {}", - "deleteView": "Видалити", - "duplicateView": "Дублікат" + "moreOptions": "Більше опцій" }, "importPanel": { "textAndMarkdown": "Текст і Markdown", @@ -152,9 +92,7 @@ "openNewTab": "Відкрити в новій вкладці", "moveTo": "Перемістити в", "addToFavorites": "Додати до обраного", - "copyLink": "Скопіювати посилання", - "changeIcon": "Змінити значок", - "collapseAllPages": "Згорнути всі підсторінки" + "copyLink": "Скопіювати посилання" }, "blankPageTitle": "Порожня сторінка", "newPageText": "Нова сторінка", @@ -162,34 +100,6 @@ "newGridText": "Нова таблиця", "newCalendarText": "Новий календар", "newBoardText": "Нова дошка", - "chat": { - "newChat": "ШІ Чат", - "inputMessageHint": "Запитайте @:appName AI", - "inputLocalAIMessageHint": "Запитайте @:appName локальний AI", - "unsupportedCloudPrompt": "Ця функція доступна лише під час використання @:appName Cloud", - "relatedQuestion": "Пов'язані", - "serverUnavailable": "Сервіс тимчасово недоступний. Будь ласка спробуйте пізніше.", - "aiServerUnavailable": "🌈 Ой-ой! 🌈. Єдиноріг з'їв нашу відповідь. Будь ласка, повторіть спробу!", - "clickToRetry": "Натисніть, щоб повторити спробу", - "regenerateAnswer": "Регенерувати", - "question1": "Як використовувати Kanban для керування завданнями", - "question2": "Поясніть метод GTD", - "question3": "Навіщо використовувати Rust", - "question4": "Рецепт з тим, що є на моїй кухні", - "aiMistakePrompt": "ШІ може помилятися. Перевірте важливу інформацію.", - "chatWithFilePrompt": "Хочете поспілкуватися з файлом?", - "indexFileSuccess": "Файл успішно індексується", - "inputActionNoPages": "Сторінка не знайдена", - "referenceSource": { - "zero": "0 джерел знайдено", - "one": "{count} джерело знайдено", - "other": "{count} джерел знайдено" - }, - "clickToMention": "Натисніть, щоб згадати сторінку", - "uploadFile": "Завантажуйте PDF-файли, файли md або txt для спілкування в чаті", - "questionDetail": "Привіт {}! Чим я можу тобі допомогти сьогодні?", - "indexingFile": "Індексація {}" - }, "trash": { "text": "Смітник", "restoreAll": "Відновити все", @@ -206,15 +116,7 @@ "confirmRestoreAll": { "title": "Ви впевнені, що хочете відновити всі сторінки у кошику?", "caption": "Цю дію неможливо скасувати." - }, - "mobile": { - "actions": "Дії щодо сміття", - "empty": "Кошик порожній", - "emptyDescription": "У вас немає видалених файлів", - "isDeleted": "видалено", - "isRestored": "відновлено" - }, - "confirmDeleteTitle": "Ви впевнені, що хочете остаточно видалити цю сторінку?" + } }, "deletePagePrompt": { "text": "Ця сторінка знаходиться у кошику", @@ -225,14 +127,14 @@ "questionBubble": { "shortcuts": "Комбінації клавіш", "whatsNew": "Що нового?", + "help": "Довідка та підтримка", "markdown": "Markdown", "debug": { "name": "Інформація для налагодження", "success": "Інформацію для налагодження скопійовано в буфер обміну!", "fail": "Не вдалося скопіювати інформацію для налагодження в буфер обміну" }, - "feedback": "Зворотний зв'язок", - "help": "Довідка та підтримка" + "feedback": "Зворотний зв'язок" }, "menuAppHeader": { "moreButtonToolTip": "Видалити, перейменувати та інше...", @@ -240,7 +142,6 @@ "defaultNewPageName": "Без назви", "renameDialog": "Перейменувати" }, - "noPagesInside": "Всередині немає сторінок", "toolbar": { "undo": "Скасувати", "redo": "Повторити", @@ -268,53 +169,16 @@ "dragRow": "Тримайте натиснутим для зміни порядку рядка", "viewDataBase": "Переглянути базу даних", "referencePage": "Ця {name} знаходиться у зв'язку", - "addBlockBelow": "Додати блок нижче", - "aiGenerate": "Генерувати" + "addBlockBelow": "Додати блок нижче" }, "sideBar": { "closeSidebar": "Закрити бічну панель", "openSidebar": "Відкрити бічну панель", "personal": "Особисте", - "private": "Приватний", - "workspace": "Робоча область", "favorites": "Вибране", - "clickToHidePrivate": "Натисніть, щоб приховати особистий простір\nСторінки, які ви тут створили, бачите лише ви", - "clickToHideWorkspace": "Натисніть, щоб приховати робочу область\nСторінки, які ви тут створили, бачать усі учасники", "clickToHidePersonal": "Натисніть, щоб приховати особистий розділ", "clickToHideFavorites": "Натисніть, щоб приховати вибраний розділ", - "addAPage": "Додати сторінку", - "addAPageToPrivate": "Додайте сторінку до особистого простору", - "addAPageToWorkspace": "Додати сторінку до робочої області", - "recent": "Останні", - "today": "Сьогодні", - "thisWeek": "Цього тижня", - "others": "Раніше улюблені", - "justNow": "прямо зараз", - "minutesAgo": "{count} хвилин тому", - "lastViewed": "Останній перегляд", - "favoriteAt": "Вибране", - "emptyRecent": "Немає останніх документів", - "emptyRecentDescription": "Під час перегляду документів вони з’являтимуться тут, щоб їх було легко знайти", - "emptyFavorite": "Немає вибраних документів", - "emptyFavoriteDescription": "Почніть досліджувати та позначайте документи як вибрані. Вони будуть перераховані тут для швидкого доступу!", - "removePageFromRecent": "Видалити цю сторінку з останніх?", - "removeSuccess": "Успішно видалено", - "favoriteSpace": "Вибране", - "RecentSpace": "Останні", - "Spaces": "Пробіли", - "upgradeToPro": "Оновлення до Pro", - "upgradeToAIMax": "Розблокуйте необмежений ШІ", - "storageLimitDialogTitle": "У вас вичерпано безкоштовне сховище. Оновіть, щоб розблокувати необмежений обсяг пам’яті", - "aiResponseLimitTitle": "У вас закінчилися безкоштовні відповіді ШІ. Перейдіть до плану Pro або придбайте доповнення AI, щоб розблокувати необмежену кількість відповідей", - "aiResponseLimitDialogTitle": "Досягнуто ліміту відповідей ШІ", - "aiResponseLimit": "У вас закінчилися безкоштовні відповіді ШІ.<inlang-LineFeed>\nПерейдіть до Налаштування -> План -> Натисніть AI Max або Pro Plan, щоб отримати більше відповідей AI", - "askOwnerToUpgradeToPro": "У вашому робочому просторі закінчується безкоштовна пам’ять. Попросіть свого власника робочого місця перейти на план Pro", - "askOwnerToUpgradeToAIMax": "У вашій робочій області закінчуються безкоштовні відповіді ШІ. Будь ласка, попросіть свого власника робочого простору оновити план або придбати додатки AI", - "purchaseStorageSpace": "Придбайте місце для зберігання", - "purchaseAIResponse": "Придбати", - "askOwnerToUpgradeToLocalAI": "Попросіть власника робочої області ввімкнути ШІ на пристрої", - "upgradeToAILocal": "Запустіть локальні моделі на своєму пристрої для повної конфіденційності", - "upgradeToAILocalDesc": "Спілкуйтеся в чаті з PDF-файлами, вдосконалюйте свій текст і автоматично заповнюйте таблиці за допомогою локального штучного інтелекту" + "addAPage": "Додати сторінку" }, "notifications": { "export": { @@ -329,8 +193,7 @@ "editContact": "Редагувати контакт" }, "button": { - "ok": "Oк", - "confirm": "Підтвердити", + "ok": "OK", "done": "Готово", "cancel": "Скасувати", "signIn": "Увійти", @@ -344,44 +207,11 @@ "discard": "Відхилити", "replace": "Замінити", "insertBelow": "Вставити нижче", - "insertAbove": "Вставте вище", "upload": "Завантажити", "edit": "Редагувати", "delete": "Видалити", "duplicate": "Дублювати", - "putback": "Повернути", - "update": "Оновлення", - "share": "Поділіться", - "removeFromFavorites": "Видалити з вибраного", - "removeFromRecent": "Вилучити з останніх", - "addToFavorites": "Додати в обране", - "favoriteSuccessfully": "Улюблений успіх", - "unfavoriteSuccessfully": "Неулюблений успіх", - "duplicateSuccessfully": "Продубльовано успішно", - "rename": "Перейменувати", - "helpCenter": "Центр допомоги", - "add": "Додати", - "yes": "Так", - "no": "Ні", - "clear": "Очистити", - "remove": "Видалити", - "dontRemove": "Не видаляйте", - "copyLink": "Копіювати посилання", - "align": "Вирівняти", - "login": "Логін", - "logout": "Вийти", - "deleteAccount": "Видалити аккаунт", - "back": "Назад", - "signInGoogle": "Продовжуйте з Google", - "signInGithub": "Продовжуйте з Github", - "signInDiscord": "Продовжуйте з Discord", - "more": "Більше", - "create": "Створити", - "close": "Закрити", - "next": "Далі", - "previous": "Попередній", - "submit": "Надіслати", - "download": "Завантажити" + "putback": "Повернути" }, "label": { "welcome": "Ласкаво просимо!", @@ -405,628 +235,31 @@ }, "settings": { "title": "Налаштування", - "popupMenuItem": { - "settings": "Налаштування", - "members": "Члени", - "trash": "Cміття", - "helpAndSupport": "Допомога та підтримка" - }, - "accountPage": { - "menuLabel": "Мій рахунок", - "title": "Мій рахунок", - "general": { - "title": "Назва облікового запису та зображення профілю", - "changeProfilePicture": "Змінити зображення профілю" - }, - "email": { - "title": "Електронна пошта", - "actions": { - "change": "Змінити електронну адресу" - } - }, - "login": { - "title": "Вхід в обліковий запис", - "loginLabel": "Авторизуватися", - "logoutLabel": "Вийти" - } - }, - "workspacePage": { - "menuLabel": "Робоча область", - "title": "Робоча область", - "description": "Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, формат дати/часу та мову.", - "workspaceName": { - "title": "Назва робочої області" - }, - "workspaceIcon": { - "title": "Значок робочої області", - "description": "Завантажте зображення або використовуйте емодзі для свого робочого простору. Значок відображатиметься на бічній панелі та в сповіщеннях." - }, - "appearance": { - "title": "Зовнішній вигляд", - "description": "Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, дату, час і мову.", - "options": { - "system": "Авто", - "light": "Світлий", - "dark": "Темний" - } - }, - "resetCursorColor": { - "title": "Скинути колір курсора документа", - "description": "Ви впевнені, що хочете скинути колір курсору?" - }, - "resetSelectionColor": { - "title": "Скинути колір вибору документа", - "description": "Ви впевнені, що бажаєте скинути колір виділення?" - }, - "theme": { - "title": "Тема", - "description": "Виберіть попередньо встановлену тему або завантажте власну тему.", - "uploadCustomThemeTooltip": "Завантажте спеціальну тему" - }, - "workspaceFont": { - "title": "Шрифт робочої області", - "noFontHint": "Шрифт не знайдено, спробуйте інший термін." - }, - "textDirection": { - "title": "Напрямок тексту", - "leftToRight": "Зліва направо", - "rightToLeft": "Справа наліво", - "auto": "Авто", - "enableRTLItems": "Увімкнути елементи панелі інструментів RTL" - }, - "layoutDirection": { - "title": "Напрямок макета", - "leftToRight": "Зліва направо", - "rightToLeft": "Справа наліво" - }, - "dateTime": { - "title": "Дата, час", - "example": "{} у {} ({})", - "24HourTime": "24-годинний час", - "dateFormat": { - "label": "Формат дати", - "local": "Місцевий", - "us": "US", - "iso": "ISO", - "friendly": "дружній", - "dmy": "Д/М/Р" - } - }, - "language": { - "title": "Мова" - }, - "deleteWorkspacePrompt": { - "title": "Видалити робочу область", - "content": "Ви впевнені, що хочете видалити цю робочу область? Цю дію неможливо скасувати, і всі опубліковані вами сторінки буде скасовано." - }, - "leaveWorkspacePrompt": { - "title": "Залиште робочу область", - "content": "Ви впевнені, що бажаєте залишити цю робочу область? Ви втратите доступ до всіх сторінок і даних на них." - }, - "manageWorkspace": { - "title": "Керуйте робочим простором", - "leaveWorkspace": "Залиште робочу область", - "deleteWorkspace": "Видалити робочу область" - } - }, - "manageDataPage": { - "menuLabel": "Керувати даними", - "title": "Керувати даними", - "description": "Керуйте локальним сховищем даних або імпортуйте наявні дані в @:appName .", - "dataStorage": { - "title": "Місце зберігання файлів", - "tooltip": "Місце, де зберігаються ваші файли", - "actions": { - "change": "Змінити шлях", - "open": "Відкрити папку", - "openTooltip": "Відкрити поточне розташування папки даних", - "copy": "Копіювати шлях", - "copiedHint": "Шлях скопійовано!", - "resetTooltip": "Відновити розташування за умовчанням" - }, - "resetDialog": { - "title": "Ти впевнений?", - "description": "Скидання шляху до розташування даних за умовчанням не призведе до видалення даних. Якщо ви хочете повторно імпортувати поточні дані, вам слід спочатку скопіювати шлях вашого поточного місцезнаходження." - } - }, - "importData": { - "title": "Імпорт даних", - "tooltip": "Імпорт даних із резервних копій/папок даних @:appName", - "description": "Скопіюйте дані із зовнішньої папки даних @:appName", - "action": "Переглянути файл" - }, - "encryption": { - "title": "Шифрування", - "tooltip": "Керуйте тим, як ваші дані зберігаються та шифруються", - "descriptionNoEncryption": "Якщо ввімкнути шифрування, усі дані будуть зашифровані. Це не може бути скасоване.", - "descriptionEncrypted": "Ваші дані зашифровані.", - "action": "Шифрувати дані", - "dialog": { - "title": "Зашифрувати всі ваші дані?", - "description": "Шифрування всіх ваших даних збереже ваші дані в безпеці. Цю дію НЕ можна скасувати. Ви впевнені, що бажаєте продовжити?" - } - }, - "cache": { - "title": "Очистити кеш", - "description": "Допоможіть вирішити такі проблеми, як не завантаження зображення, відсутність сторінок у просторі та не завантаження шрифтів. Це не вплине на ваші дані.", - "dialog": { - "title": "Очистити кеш", - "description": "Допоможіть вирішити такі проблеми, як не завантаження зображення, відсутність сторінок у просторі та не завантаження шрифтів. Це не вплине на ваші дані.", - "successHint": "Кеш очищено!" - } - }, - "data": { - "fixYourData": "Виправте свої дані", - "fixButton": "Виправити", - "fixYourDataDescription": "Якщо у вас виникли проблеми з даними, ви можете спробувати їх виправити тут." - } - }, - "shortcutsPage": { - "menuLabel": "Ярлики", - "title": "Ярлики", - "editBindingHint": "Введіть нову прив'язку", - "searchHint": "Пошук", - "actions": { - "resetDefault": "Скинути за замовчуванням" - }, - "errorPage": { - "message": "Не вдалося завантажити ярлики: {}", - "howToFix": "Спробуйте ще раз. Якщо проблема не зникне, зв’яжіться з GitHub." - }, - "resetDialog": { - "title": "Скинути ярлики", - "description": "Це скине всі ваші прив’язки клавіш до стандартних, ви не зможете скасувати це пізніше. Ви впевнені, що бажаєте продовжити?", - "buttonLabel": "Скинути" - }, - "conflictDialog": { - "title": "{} зараз використовується", - "descriptionPrefix": "Цю комбінацію клавіш зараз використовує ", - "descriptionSuffix": ". Якщо ви заміните цю комбінацію клавіш, її буде видалено з {}.", - "confirmLabel": "Продовжити" - }, - "editTooltip": "Натисніть, щоб почати редагування сполучення клавіш", - "keybindings": { - "toggleToDoList": "Перемкнути список справ", - "insertNewParagraphInCodeblock": "Вставте новий абзац", - "pasteInCodeblock": "Вставте кодовий блок", - "selectAllCodeblock": "Вибрати все", - "indentLineCodeblock": "Вставте два пропуски на початку рядка", - "outdentLineCodeblock": "Видалити два пропуски на початку рядка", - "twoSpacesCursorCodeblock": "Вставте два пробіли біля курсору", - "copy": "Вибір копії", - "paste": "Вставте вміст", - "cut": "Вирізати виділення", - "alignLeft": "Вирівняти текст по лівому краю", - "alignCenter": "Вирівняти текст по центру", - "alignRight": "Вирівняти текст праворуч", - "undo": "Скасувати", - "redo": "Повторити", - "convertToParagraph": "Перетворити блок на абзац", - "backspace": "Видалити", - "deleteLeftWord": "Видалити ліве слово", - "deleteLeftSentence": "Видалити ліве речення", - "delete": "Видалити правильний символ", - "deleteMacOS": "Видалити лівий символ", - "deleteRightWord": "Видалити правильне слово", - "moveCursorLeft": "Перемістити курсор ліворуч", - "moveCursorBeginning": "Перемістіть курсор на початок", - "moveCursorLeftWord": "Перемістити курсор на одне слово вліво", - "moveCursorLeftSelect": "Виберіть і перемістіть курсор ліворуч", - "moveCursorBeginSelect": "Виберіть і перемістіть курсор на початок", - "moveCursorLeftWordSelect": "Виберіть і перемістіть курсор ліворуч на одне слово", - "moveCursorRight": "Перемістіть курсор праворуч", - "moveCursorEnd": "Перемістіть курсор до кінця", - "moveCursorRightWord": "Перемістіть курсор на одне слово вправо", - "moveCursorRightSelect": "Виберіть і перемістіть курсор праворуч", - "moveCursorEndSelect": "Виберіть і перемістіть курсор до кінця", - "moveCursorRightWordSelect": "Виділіть і перемістіть курсор на одне слово вправо", - "moveCursorUp": "Перемістіть курсор вгору", - "moveCursorTopSelect": "Виберіть і перемістіть курсор угору", - "moveCursorTop": "Перемістіть курсор вгору", - "moveCursorUpSelect": "Виберіть і перемістіть курсор вгору", - "moveCursorBottomSelect": "Виберіть і перемістіть курсор униз", - "moveCursorBottom": "Перемістіть курсор униз", - "moveCursorDown": "Перемістіть курсор вниз", - "moveCursorDownSelect": "Виберіть і перемістіть курсор вниз", - "home": "Прокрутіть до верху", - "end": "Прокрутіть донизу", - "toggleBold": "Переключити жирний шрифт", - "toggleItalic": "Перемкнути курсив", - "toggleUnderline": "Перемкнути підкреслення", - "toggleStrikethrough": "Переключити закреслене", - "toggleCode": "Перемкнути вбудований код", - "toggleHighlight": "Перемкнути виділення", - "showLinkMenu": "Показати меню посилань", - "openInlineLink": "Відкрити вбудоване посилання", - "openLinks": "Відкрити всі вибрані посилання", - "indent": "Відступ", - "outdent": "Відступ", - "exit": "Вийти з редагування", - "pageUp": "Прокрутіть одну сторінку вгору", - "pageDown": "Прокрутіть одну сторінку вниз", - "selectAll": "Вибрати все", - "pasteWithoutFormatting": "Вставте вміст без форматування", - "showEmojiPicker": "Показати засіб вибору емодзі", - "enterInTableCell": "Додати розрив рядка в таблицю", - "leftInTableCell": "Перемістити таблицю на одну клітинку вліво", - "rightInTableCell": "Перемістити таблицю на одну клітинку праворуч", - "upInTableCell": "Перейти на одну клітинку вгору в таблиці", - "downInTableCell": "Перемістіть таблицю на одну клітинку вниз", - "tabInTableCell": "Перейти до наступної доступної клітинки в таблиці", - "shiftTabInTableCell": "Перейти до попередньо доступної клітинки в таблиці", - "backSpaceInTableCell": "Зупинка на початку клітини" - }, - "commands": { - "codeBlockNewParagraph": "Вставте новий абзац поруч із блоком коду", - "codeBlockIndentLines": "Вставте два пробіли на початку рядка в блоці коду", - "codeBlockOutdentLines": "Видалити два пробіли на початку рядка в блоці коду", - "codeBlockAddTwoSpaces": "Вставте два пробіли в позиції курсора в блоці коду", - "codeBlockSelectAll": "Виберіть весь вміст у блоці коду", - "codeBlockPasteText": "Вставте текст у кодовий блок", - "textAlignLeft": "Вирівняти текст по лівому краю", - "textAlignCenter": "Вирівняти текст по центру", - "textAlignRight": "Вирівняти текст праворуч" - }, - "couldNotLoadErrorMsg": "Не вдалося завантажити ярлики. Повторіть спробу", - "couldNotSaveErrorMsg": "Не вдалося зберегти ярлики. Повторіть спробу" - }, - "aiPage": { - "title": "Налаштування AI", - "menuLabel": "Налаштування AI", - "keys": { - "enableAISearchTitle": "AI Пошук", - "aiSettingsDescription": "Виберіть або налаштуйте моделі ШІ, які використовуються в @:appName . Для найкращої продуктивності ми рекомендуємо використовувати параметри моделі за замовчуванням", - "loginToEnableAIFeature": "Функції ШІ вмикаються лише після входу в @:appName Cloud. Якщо у вас немає облікового запису @:appName , перейдіть до «Мого облікового запису», щоб зареєструватися", - "llmModel": "Модель мови", - "llmModelType": "Тип мовної моделі", - "downloadLLMPrompt": "Завантажити {}", - "downloadAppFlowyOfflineAI": "Завантаження офлайн-пакету AI дозволить працювати AI на вашому пристрої. Ви хочете продовжити?", - "downloadLLMPromptDetail": "Завантаження {} локальної моделі займе до {} пам’яті. Ви хочете продовжити?", - "downloadBigFilePrompt": "Завантаження може зайняти близько 10 хвилин", - "downloadAIModelButton": "Завантажити", - "downloadingModel": "Завантаження", - "localAILoaded": "Локальну модель AI успішно додано та готово до використання", - "localAIStart": "Розпочинається локальний AI Chat...", - "localAILoading": "Локальна модель чату ШІ завантажується...", - "localAIStopped": "Місцевий ШІ зупинився", - "failToLoadLocalAI": "Не вдалося запустити локальний ШІ", - "restartLocalAI": "Перезапустіть локальний AI", - "disableLocalAITitle": "Вимкнути локальний ШІ", - "disableLocalAIDescription": "Ви бажаєте вимкнути локальний ШІ?", - "localAIToggleTitle": "Перемикач, щоб увімкнути або вимкнути локальний ШІ", - "offlineAIInstruction1": "Дотримуйтесь", - "offlineAIInstruction2": "інструкція", - "offlineAIInstruction3": "увімкнути автономний AI.", - "offlineAIDownload1": "Якщо ви не завантажили AppFlowy AI, будь ласка", - "offlineAIDownload2": "завантажити", - "offlineAIDownload3": "це перше", - "activeOfflineAI": "Активний", - "downloadOfflineAI": "Завантажити", - "openModelDirectory": "Відкрити папку" - } - }, - "planPage": { - "menuLabel": "План", - "title": "Ціновий план", - "planUsage": { - "title": "Підсумок використання плану", - "storageLabel": "Зберігання", - "storageUsage": "{} із {} Гб", - "unlimitedStorageLabel": "Необмежене зберігання", - "collaboratorsLabel": "Члени", - "collaboratorsUsage": "{} з {}", - "aiResponseLabel": "Відповіді ШІ", - "aiResponseUsage": "{} з {}", - "unlimitedAILabel": "Необмежена кількість відповідей", - "proBadge": "Pro", - "aiMaxBadge": "AI Макс", - "aiOnDeviceBadge": "ШІ на пристрої для Mac", - "memberProToggle": "Більше учасників і необмежений ШІ", - "aiMaxToggle": "Необмежений ШІ та доступ до вдосконалених моделей", - "aiOnDeviceToggle": "Локальний штучний інтелект для повної конфіденційності", - "aiCredit": { - "title": "Додати кредит @:appName AI", - "price": "{}", - "priceDescription": "за 1000 кредитів", - "purchase": "Придбайте ШІ", - "info": "Додайте 1000 кредитів штучного інтелекту на робочий простір і плавно інтегруйте настроюваний штучний інтелект у свій робочий процес, щоб отримати розумніші та швидші результати з до:", - "infoItemOne": "10 000 відповідей на базу даних", - "infoItemTwo": "1000 відповідей на робочу область" - }, - "currentPlan": { - "bannerLabel": "Поточний план", - "freeTitle": "Безкоштовно", - "proTitle": "Pro", - "teamTitle": "Команда", - "freeInfo": "Ідеально підходить для окремих осіб до 2 учасників, щоб організувати все", - "proInfo": "Ідеально підходить для малих і середніх команд до 10 учасників.", - "teamInfo": "Ідеально підходить для всіх продуктивних і добре організованих команд.", - "upgrade": "Змінити план", - "canceledInfo": "Ваш план скасовано, вас буде переведено на безкоштовний план {}." - }, - "addons": { - "title": "Додатки", - "addLabel": "Додати", - "activeLabel": "Додано", - "aiMax": { - "title": "AI Макс", - "description": "Необмежені відповіді AI на основі GPT-4o, Claude 3.5 Sonnet тощо", - "price": "{}", - "priceInfo": "на користувача на місяць", - "billingInfo": "рахунок виставляється щорічно або {} щомісяця" - }, - "aiOnDevice": { - "title": "ШІ на пристрої для Mac", - "description": "Запустіть Mistral 7B, LLAMA 3 та інші локальні моделі на вашому комп’ютері", - "price": "{}", - "priceInfo": "Оплата за користувача на місяць виставляється щорічно", - "recommend": "Рекомендовано M1 або новіше", - "billingInfo": "рахунок виставляється щорічно або {} щомісяця" - } - }, - "deal": { - "bannerLabel": "Новорічна угода!", - "title": "Розвивайте свою команду!", - "info": "Оновіть та заощаджуйте 10% на планах Pro та Team! Підвищте продуктивність робочого простору за допомогою нових потужних функцій, зокрема @:appName .", - "viewPlans": "Переглянути плани" - } - } - }, - "billingPage": { - "menuLabel": "Виставлення рахунків", - "title": "Виставлення рахунків", - "plan": { - "title": "План", - "freeLabel": "Безкоштовно", - "proLabel": "Pro", - "planButtonLabel": "Змінити план", - "billingPeriod": "Розрахунковий період", - "periodButtonLabel": "Період редагування" - }, - "paymentDetails": { - "title": "Платіжні реквізити", - "methodLabel": "Спосіб оплати", - "methodButtonLabel": "Метод редагування" - }, - "addons": { - "title": "Додатки", - "addLabel": "Додати", - "removeLabel": "Видалити", - "renewLabel": "Відновити", - "aiMax": { - "label": "AI Макс", - "description": "Розблокуйте необмежену кількість штучного інтелекту та вдосконалених моделей", - "activeDescription": "Наступний рахунок-фактура має бути виставлено {}", - "canceledDescription": "AI Max буде доступний до {}" - }, - "aiOnDevice": { - "label": "ШІ на пристрої для Mac", - "description": "Розблокуйте необмежений ШІ офлайн на своєму пристрої", - "activeDescription": "Наступний рахунок-фактура має бути виставлено {}", - "canceledDescription": "AI On-device для Mac буде доступний до {}" - }, - "removeDialog": { - "title": "Видалити {}", - "description": "Ви впевнені, що хочете видалити {plan}? Ви негайно втратите доступ до функцій і переваг {plan}." - } - }, - "currentPeriodBadge": "ПОТОЧНИЙ", - "changePeriod": "Період зміни", - "planPeriod": "{} період", - "monthlyInterval": "Щомісяця", - "monthlyPriceInfo": "за місце сплачується щомісяця", - "annualInterval": "Щорічно", - "annualPriceInfo": "за місце, що виставляється щорічно" - }, - "comparePlanDialog": { - "title": "Порівняйте та виберіть план", - "planFeatures": "План\nособливості", - "current": "Поточний", - "actions": { - "upgrade": "Оновлення", - "downgrade": "Понизити", - "current": "Поточний" - }, - "freePlan": { - "title": "Безкоштовно", - "description": "Для окремих осіб до 2 учасників, щоб організувати все", - "price": "{}", - "priceInfo": "Безплатний назавжди" - }, - "proPlan": { - "title": "Pro", - "description": "Для невеликих команд для управління проектами та знаннями команди", - "price": "{}", - "priceInfo": "на користувача на місяць\nвиставляється щорічно<inlang-LineFeed>\n{} виставляється щомісяця" - }, - "planLabels": { - "itemOne": "Робочі області", - "itemTwo": "Члени", - "itemThree": "Зберігання", - "itemFour": "Співпраця в реальному часі", - "itemFive": "Мобільний додаток", - "itemSix": "Відповіді ШІ", - "itemSeven": "Спеціальний простір імен", - "itemFileUpload": "Завантаження файлів", - "tooltipSix": "Термін служби означає кількість відповідей, які ніколи не скидаються", - "intelligentSearch": "Інтелектуальний пошук", - "tooltipSeven": "Дозволяє налаштувати частину URL-адреси для вашої робочої області" - }, - "freeLabels": { - "itemOne": "сплачується за робоче місце", - "itemTwo": "До 2", - "itemThree": "5 ГБ", - "itemFour": "так", - "itemFive": "так", - "itemSix": "10 років життя", - "itemFileUpload": "До 7 Мб", - "intelligentSearch": "Інтелектуальний пошук" - }, - "proLabels": { - "itemOne": "Оплата за робоче місце", - "itemTwo": "До 10", - "itemThree": "Необмежений", - "itemFour": "так", - "itemFive": "так", - "itemSix": "Необмежений", - "itemFileUpload": "Необмежений", - "intelligentSearch": "Інтелектуальний пошук" - }, - "paymentSuccess": { - "title": "Тепер ви на плані {}!", - "description": "Ваш платіж успішно оброблено, і ваш план оновлено до @:appName {}. Ви можете переглянути деталі свого плану на сторінці План" - }, - "downgradeDialog": { - "title": "Ви впевнені, що хочете понизити свій план?", - "description": "Пониження вашого плану призведе до повернення до безкоштовного плану. Учасники можуть втратити доступ до цього робочого простору, і вам може знадобитися звільнити місце, щоб досягти лімітів пам’яті безкоштовного плану.", - "downgradeLabel": "Пониження плану" - } - }, - "cancelSurveyDialog": { - "title": "Шкода, що ви йдете", - "description": "Нам шкода, що ви йдете. Ми будемо раді почути ваш відгук, щоб допомогти нам покращити @:appName . Знайдіть хвилинку, щоб відповісти на кілька запитань.", - "commonOther": "Інший", - "otherHint": "Напишіть свою відповідь тут", - "questionOne": { - "question": "Що спонукало вас скасувати підписку на AppFlowy Pro?", - "answerOne": "Зависока вартість", - "answerTwo": "Характеристики не виправдали очікувань", - "answerThree": "Знайшов кращу альтернативу", - "answerFour": "Використовував недостатньо, щоб виправдати витрати", - "answerFive": "Проблема з обслуговуванням або технічні проблеми" - }, - "questionTwo": { - "question": "Наскільки ймовірно, що ви подумаєте про повторну підписку на AppFlowy Pro у майбутньому?", - "answerOne": "Ймовірно", - "answerTwo": "Певною мірою ймовірно", - "answerThree": "Не впевнений", - "answerFour": "Малоймовірно", - "answerFive": "Дуже малоймовірно" - }, - "questionThree": { - "question": "Яку функцію Pro ви цінували найбільше під час підписки?", - "answerOne": "Багатокористувацька співпраця", - "answerTwo": "Довша історія версій", - "answerThree": "Необмежена кількість відповідей ШІ", - "answerFour": "Доступ до локальних моделей ШІ" - }, - "questionFour": { - "question": "Як би ви описали свій загальний досвід роботи з AppFlowy?", - "answerOne": "Чудово", - "answerTwo": "Добре", - "answerThree": "Середній", - "answerFour": "Нижче середнього", - "answerFive": "Незадоволений" - } - }, - "common": { - "reset": "Скинути" - }, "menu": { "appearance": "Вигляд", "language": "Мова", "user": "Користувач", "files": "Файли", - "notifications": "Сповіщення", "open": "Відкрити налаштування", "logout": "Вийти", "logoutPrompt": "Ви впевнені, що хочете вийти?", "selfEncryptionLogoutPrompt": "Ви впевнені, що хочете вийти? Будь ласка, переконайтеся, що ви скопіювали секрет шифрування", "syncSetting": "Налаштування синхронізації", - "cloudSettings": "Налаштування хмари", "enableSync": "Увімкнути синхронізацію", "enableEncrypt": "Шифрувати дані", - "cloudURL": "Базовий URL", - "invalidCloudURLScheme": "Недійсна схема", - "cloudServerType": "Хмарний сервер", - "cloudServerTypeTip": "Зауважте, що після перемикання хмарного сервера ваш поточний обліковий запис може вийти з системи", - "cloudLocal": "Місцевий", - "cloudAppFlowy": "@:appName Cloud Beta", - "cloudAppFlowySelfHost": "@:appName Cloud на власному сервері", - "appFlowyCloudUrlCanNotBeEmpty": "URL-адреса хмари не може бути пустою", - "clickToCopy": "Натисніть, щоб скопіювати", - "selfHostStart": "Якщо у вас немає сервера, зверніться до", - "selfHostContent": "документ", - "selfHostEnd": "для вказівок щодо самостійного розміщення власного сервера", - "cloudURLHint": "Введіть базову URL-адресу вашого сервера", - "cloudWSURL": "URL-адреса Websocket", - "cloudWSURLHint": "Введіть адресу веб-сокета вашого сервера", - "restartApp": "Перезапустіть", - "restartAppTip": "Перезапустіть програму, щоб зміни набули чинності. Зауважте, що це може привести до виходу з вашого поточного облікового запису.", - "changeServerTip": "Після зміни сервера необхідно натиснути кнопку перезавантаження, щоб зміни набули чинності", "enableEncryptPrompt": "Активуйте шифрування для захисту ваших даних за допомогою цього секретного ключа. Зберігайте його надійно; після увімкнення його неможливо вимкнути. Якщо втрачено, ваші дані стають недоступними. Клацніть, щоб скопіювати", "inputEncryptPrompt": "Будь ласка, введіть свій секрет для шифрування для", "clickToCopySecret": "Клацніть, щоб скопіювати секрет", - "configServerSetting": "Налаштуйте параметри свого сервера", - "configServerGuide": "Вибравши `Швидкий старт`, перейдіть до `Налаштування`, а потім до «Налаштування хмари», щоб налаштувати свій автономний сервер.", "inputTextFieldHint": "Ваш секрет", "historicalUserList": "Історія входу користувача", "historicalUserListTooltip": "У цьому списку відображаються ваші анонімні облікові записи. Ви можете клацнути на обліковий запис, щоб переглянути його деталі. Анонімні облікові записи створюються, натискаючи кнопку 'Розпочати роботу'", - "openHistoricalUser": "Клацніть, щоб відкрити анонімний обліковий запис", - "customPathPrompt": "Зберігання папки даних @:appName у папці, що синхронізується з хмарою, як-от Google Drive, може становити ризик. Якщо база даних у цій папці доступна або змінена з кількох місць одночасно, це може призвести до конфліктів синхронізації та можливого пошкодження даних", - "importAppFlowyData": "Імпорт даних із зовнішньої @:appName", - "importingAppFlowyDataTip": "Виконується імпорт даних. Будь ласка, не закривайте додаток", - "importAppFlowyDataDescription": "Скопіюйте дані із зовнішньої папки @:appName та імпортуйте їх у поточну папку даних AppFlowy", - "importSuccess": "Папку даних @:appName успішно імпортовано", - "importFailed": "Не вдалося імпортувати папку даних @:appName", - "importGuide": "Щоб отримати додаткові відомості, перегляньте документ, на який посилається" - }, - "notifications": { - "enableNotifications": { - "label": "Увімкнути сповіщення", - "hint": "Вимкніть, щоб локальні сповіщення не з’являлися." - }, - "showNotificationsIcon": { - "label": "Показати значок сповіщень", - "hint": "Вимкніть, щоб приховати піктограму сповіщень на бічній панелі." - }, - "archiveNotifications": { - "allSuccess": "Усі сповіщення успішно заархівовано", - "success": "Сповіщення успішно заархівовано" - }, - "markAsReadNotifications": { - "allSuccess": "Усі позначені як успішно прочитані", - "success": "Позначено як успішно прочитане" - }, - "action": { - "markAsRead": "Відзначити як прочитане", - "multipleChoice": "Виберіть більше", - "archive": "Архів" - }, - "settings": { - "settings": "Налаштування", - "markAllAsRead": "Позначити все як прочитане", - "archiveAll": "Архівувати все" - }, - "emptyInbox": { - "title": "Ще немає сповіщень", - "description": "Тут ви отримуватимете сповіщення про @згадки" - }, - "emptyUnread": { - "title": "Немає непрочитаних сповіщень", - "description": "Ви все наздогнали!" - }, - "emptyArchived": { - "title": "Немає архівованих сповіщень", - "description": "Ви ще не заархівували жодного сповіщення" - }, - "tabs": { - "inbox": "Вхідні", - "unread": "Непрочитаний", - "archived": "Архівовано" - }, - "refreshSuccess": "Сповіщення успішно оновлено", - "titles": { - "notifications": "Сповіщення", - "reminder": "Нагадування" - } + "openHistoricalUser": "Клацніть, щоб відкрити анонімний обліковий запис" }, "appearance": { "resetSetting": "Скинути це налаштування", "fontFamily": { "label": "Шрифт", - "search": "Пошук", - "defaultFont": "Система" + "search": "Пошук" }, "themeMode": { "label": "Режим теми", @@ -1034,22 +267,6 @@ "dark": "Темний режим", "system": "Системна" }, - "fontScaleFactor": "Коефіцієнт масштабування шрифту", - "documentSettings": { - "cursorColor": "Колір курсору документа", - "selectionColor": "Колір виділення документа", - "pickColor": "Виберіть колір", - "colorShade": "Колірний відтінок", - "opacity": "Непрозорість", - "hexEmptyError": "Шістнадцяткове поле кольору не може бути порожнім", - "hexLengthError": "Шістнадцяткове значення має містити 6 цифр", - "hexInvalidError": "Недійсне шістнадцяткове значення", - "opacityEmptyError": "Непрозорість не може бути пустою", - "opacityRangeError": "Непрозорість має бути від 1 до 100", - "app": "Додаток", - "flowy": "Текучий", - "apply": "Застосувати" - }, "layoutDirection": { "label": "Напрямок макету", "hint": "Контролюйте напрямок контенту на вашому екрані, зліва направо або справа наліво.", @@ -1067,7 +284,7 @@ "themeUpload": { "button": "Завантажити", "uploadTheme": "Завантажити тему", - "description": "Завантажте свою власну тему @:appName, скориставшись кнопкою нижче.", + "description": "Завантажте свою власну тему AppFlowy, скориставшись кнопкою нижче.", "loading": "Будь ласка, зачекайте, поки ми перевіряємо та завантажуємо вашу тему...", "uploadSuccess": "Вашу тему успішно завантажено", "deletionFailure": "Не вдалося видалити тему. Спробуйте видалити її вручну.", @@ -1081,7 +298,7 @@ "dateFormat": { "label": "Формат дати", "local": "Локальний", - "us": "US", + "us": "США", "iso": "ISO", "friendly": "Дружній", "dmy": "Д/М/Р" @@ -1091,56 +308,14 @@ "twelveHour": "Дванадцятигодинний", "twentyFourHour": "Двадцять чотири години" }, - "showNamingDialogWhenCreatingPage": "Показувати діалогове вікно імені при створенні сторінки", - "enableRTLToolbarItems": "Увімкнути елементи панелі інструментів RTL", - "members": { - "title": "Налаштування учасників", - "inviteMembers": "Запросити учасників", - "inviteHint": "Запросити електронною поштою", - "sendInvite": "Надіслати запрошення", - "copyInviteLink": "Копіювати посилання для запрошення", - "label": "Члени", - "user": "Користувач", - "role": "Роль", - "removeFromWorkspace": "Видалити з робочої області", - "removeFromWorkspaceSuccess": "Успішно видалено з робочої області", - "removeFromWorkspaceFailed": "Не вдалося видалити з робочої області", - "owner": "Власник", - "guest": "Гість", - "member": "Член", - "memberHintText": "Учасник може читати та редагувати сторінки", - "guestHintText": "Гість може читати, реагувати, коментувати та редагувати певні сторінки з дозволу.", - "emailInvalidError": "Недійсна електронна адреса, будь ласка, перевірте та повторіть спробу", - "emailSent": "Електронний лист надіслано, перевірте папку \"Вхідні\".", - "members": "членів", - "membersCount": { - "zero": "{} учасників", - "one": "{} член", - "other": "{} учасників" - }, - "inviteFailedDialogTitle": "Не вдалося надіслати запрошення", - "inviteFailedMemberLimit": "Досягнуто ліміту учасників. Оновіть його, щоб запросити більше учасників.", - "inviteFailedMemberLimitMobile": "Ваша робоча область досягла ліміту учасників. Оновіть комп’ютер, щоб розблокувати більше функцій.", - "memberLimitExceeded": "Досягнуто обмеження кількості учасників, будь ласка, запросіть більше учасників ", - "memberLimitExceededUpgrade": "оновлення", - "memberLimitExceededPro": "Досягнуто ліміту учасників, якщо вам потрібно більше учасників, зв’яжіться ", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "Не вдалося додати учасника", - "addMemberSuccess": "Учасника успішно додано", - "removeMember": "Видалити учасника", - "areYouSureToRemoveMember": "Ви впевнені, що хочете видалити цього учасника?", - "inviteMemberSuccess": "Запрошення успішно надіслано", - "failedToInviteMember": "Не вдалося запросити учасника", - "workspaceMembersError": "На жаль, сталася помилка", - "workspaceMembersErrorDescription": "Не вдалося завантажити список учасників. Спробуйте пізніше" - } + "showNamingDialogWhenCreatingPage": "Показувати діалогове вікно імені при створенні сторінки" }, "files": { "copy": "Копіювати", "defaultLocation": "Де зараз зберігаються ваші дані", "exportData": "Експортуйте свої дані", "doubleTapToCopy": "Подвійний натиск для копіювання шляху", - "restoreLocation": "Відновити до шляху за замовчуванням @:appName", + "restoreLocation": "Відновити до шляху за замовчуванням AppFlowy", "customizeLocation": "Відкрити іншу папку", "restartApp": "Будь ласка, перезапустіть програму для врахування змін.", "exportDatabase": "Експорт бази даних", @@ -1152,10 +327,10 @@ "defineWhereYourDataIsStored": "Визначте, де зберігаються ваші дані", "open": "Відкрити", "openFolder": "Відкрити існуючу папку", - "openFolderDesc": "Читати та записувати в вашу існуючу папку @:appName", + "openFolderDesc": "Читати та записувати в вашу існуючу папку AppFlowy", "folderHintText": "ім'я папки", "location": "Створення нової папки", - "locationDesc": "Оберіть ім'я для папки з даними @:appName", + "locationDesc": "Оберіть ім'я для папки з даними AppFlowy", "browser": "Перегляд", "create": "Створити", "set": "Встановити", @@ -1166,40 +341,18 @@ "change": "Змінити", "openLocationTooltips": "Відкрити інший каталог даних", "openCurrentDataFolder": "Відкрити поточний каталог даних", - "recoverLocationTooltips": "Скинути до каталогу даних за замовчуванням @:appName", + "recoverLocationTooltips": "Скинути до каталогу даних за замовчуванням AppFlowy", "exportFileSuccess": "Файл успішно експортовано!", "exportFileFail": "Помилка експорту файлу!", - "export": "Експорт", - "clearCache": "Очистити кеш", - "clearCacheDesc": "Якщо у вас виникли проблеми із завантаженням зображень або неправильним відображенням шрифтів, спробуйте очистити кеш. Ця дія не видалить ваші дані користувача.", - "areYouSureToClearCache": "Ви впевнені, що хочете очистити кеш?", - "clearCacheSuccess": "Кеш успішно очищено!" + "export": "Експорт" }, "user": { "name": "Ім'я", "email": "Електронна пошта", "tooltipSelectIcon": "Обрати значок", "selectAnIcon": "Обрати значок", - "pleaseInputYourOpenAIKey": "Будь ласка, введіть ваш ключ AI", - "clickToLogout": "Натисніть, щоб вийти з поточного облікового запису", - "pleaseInputYourStabilityAIKey": "будь ласка, введіть ключ стабільності ШІ" - }, - "mobile": { - "personalInfo": "Персональна інформація", - "username": "Ім'я користувача", - "usernameEmptyError": "Ім'я користувача не може бути пустим", - "about": "Про", - "pushNotifications": "Push-сповіщення", - "support": "Підтримка", - "joinDiscord": "Приєднуйтесь до нас у Discord", - "privacyPolicy": "Політика конфіденційності", - "userAgreement": "Угода користувача", - "termsAndConditions": "Правила та умови", - "userprofileError": "Не вдалося завантажити профіль користувача", - "userprofileErrorDescription": "Будь ласка, спробуйте вийти та увійти знову, щоб перевірити, чи проблема не зникає.", - "selectLayout": "Виберіть макет", - "selectStartingDay": "Виберіть день початку", - "version": "Версія" + "pleaseInputYourOpenAIKey": "Будь ласка, введіть ваш ключ OpenAI", + "clickToLogout": "Натисніть, щоб вийти з поточного облікового запису" }, "shortcuts": { "shortcutsLabel": "Сполучення клавіш", @@ -1231,27 +384,15 @@ "filterBy": "Фільтрувати за...", "typeAValue": "Введіть значення...", "layout": "Макет", - "databaseLayout": "Вид бази даних", - "viewList": { - "zero": "0 переглядів", - "one": "{count} перегляд", - "other": "{count} переглядів" - }, - "editView": "Редагувати перегляд", - "boardSettings": "Налаштування дошки", - "calendarSettings": "Налаштування календаря", - "createView": "Новий вигляд", - "duplicateView": "Дубльований перегляд", - "deleteView": "Видалити перегляд", - "numberOfVisibleFields": "показано {}" + "databaseLayout": "Вид бази даних" }, "textFilter": { "contains": "Містить", "doesNotContain": "Не містить", "endsWith": "Закінчується на", "startWith": "Починається з", - "is": "Є", - "isNot": "Не є", + "is": "Дорівнює", + "isNot": "Не дорівнює", "isEmpty": "Порожнє", "isNotEmpty": "Не порожнє", "choicechipPrefix": { @@ -1278,36 +419,8 @@ "isNot": "не є", "contains": "Містить", "doesNotContain": "Не містить", - "isEmpty": "Порожнє", - "isNotEmpty": "Не порожнє" - }, - "dateFilter": { - "is": "Є", - "before": "Є раніше", - "after": "Є після", - "onOrBefore": "Увімкнено або раніше", - "onOrAfter": "Увімкнено або після", - "between": "Знаходиться між", - "empty": "Пусто", - "notEmpty": "Не порожній", - "choicechipPrefix": { - "before": "Раніше", - "after": "Після", - "onOrBefore": "На або раніше", - "onOrAfter": "На або після", - "isEmpty": "Пусто", - "isNotEmpty": "Не порожній" - } - }, - "numberFilter": { - "equal": "Дорівнює", - "notEqual": "Не дорівнює", - "lessThan": "Менше ніж", - "greaterThan": "Більше ніж", - "lessThanOrEqualTo": "Менше або дорівнює", - "greaterThanOrEqualTo": "Більше або дорівнює", - "isEmpty": "Пусто", - "isNotEmpty": "Не порожній" + "isEmpty": "порожнє", + "isNotEmpty": "не порожнє" }, "field": { "hide": "Сховати", @@ -1316,8 +429,6 @@ "insertRight": "Вставити справа", "duplicate": "Дублювати", "delete": "Видалити", - "wrapCellContent": "Обернути текст", - "clear": "Очистити комірки", "textFieldName": "Текст", "checkboxFieldName": "Чекбокс", "dateFieldName": "Дата", @@ -1328,11 +439,6 @@ "multiSelectFieldName": "Вибір кількох", "urlFieldName": "URL", "checklistFieldName": "Чек-лист", - "relationFieldName": "Відношення", - "summaryFieldName": "AI Резюме", - "timeFieldName": "Час", - "translateFieldName": "ШІ Перекладач", - "translateTo": "Перекласти на", "numberFormat": "Формат числа", "dateFormat": "Формат дати", "includeTime": "Включити час", @@ -1347,31 +453,16 @@ "timeFormatTwelveHour": "12 годин", "timeFormatTwentyFourHour": "24 години", "clearDate": "Очистити дату", - "dateTime": "Дата, час", - "startDateTime": "Дата початку час", - "endDateTime": "Кінцева дата час", - "failedToLoadDate": "Не вдалося завантажити значення дати", - "selectTime": "Виберіть час", - "selectDate": "Виберіть дату", - "visibility": "Видимість", - "propertyType": "Тип власності", "addSelectOption": "Додати опцію", - "typeANewOption": "Введіть новий параметр", "optionTitle": "Опції", "addOption": "Додати опцію", "editProperty": "Редагувати властивість", "newProperty": "Додати колонку", - "openRowDocument": "Відкрити як сторінку", "deleteFieldPromptMessage": "Ви впевнені? Ця властивість буде видалена", - "clearFieldPromptMessage": "Ти впевнений? Усі клітинки в цьому стовпці будуть порожні", - "newColumn": "Нова колонка", - "format": "Формат", - "reminderOnDateTooltip": "Ця клітинка має заплановане нагадування", - "optionAlreadyExist": "Варіант вже існує" + "newColumn": "Нова колонка" }, "rowPage": { "newField": "Додати нове поле", - "fieldDragElementTooltip": "Натисніть, щоб відкрити меню", "showHiddenFields": { "one": "Показати {} приховане поле", "many": "Показати {} приховані поля", @@ -1381,20 +472,13 @@ "one": "Сховати {} приховане поле", "many": "Сховати {} приховані поля", "other": "Сховати {} приховані поля" - }, - "openAsFullPage": "Відкрити як повну сторінку", - "moreRowActions": "Більше дій рядків" + } }, "sort": { "ascending": "У висхідному порядку", "descending": "У спадному порядку", - "by": "За", - "empty": "Немає активних сортувань", - "cannotFindCreatableField": "Не вдається знайти відповідне поле для сортування", "deleteAllSorts": "Видалити всі сортування", - "addSort": "Додати сортування", - "removeSorting": "Ви хочете видалити сортування?", - "fieldInUse": "Ви вже сортуєте за цим полем" + "addSort": "Додати сортування" }, "row": { "duplicate": "Дублювати", @@ -1406,13 +490,7 @@ "newRow": "Новий рядок", "action": "Дія", "add": "Клацніть, щоб додати нижче", - "drag": "Перетягніть для переміщення", - "deleteRowPrompt": "Ви впевнені, що хочете видалити цей рядок? Цю дію не можна скасувати", - "deleteCardPrompt": "Ви впевнені, що хочете видалити цю картку? Цю дію не можна скасувати", - "dragAndClick": "Перетягніть, щоб перемістити, натисніть, щоб відкрити меню", - "insertRecordAbove": "Вставте запис вище", - "insertRecordBelow": "Вставте запис нижче", - "noContent": "Немає вмісту" + "drag": "Перетягніть для переміщення" }, "selectOption": { "create": "Створити", @@ -1431,48 +509,15 @@ "searchOption": "Шукати опцію", "searchOrCreateOption": "Шукати чи створити опцію...", "createNew": "Створити нову", - "orSelectOne": "Або виберіть опцію", - "typeANewOption": "Введіть новий параметр", - "tagName": "Назва тегу" + "orSelectOne": "Або виберіть опцію" }, "checklist": { "taskHint": "Опис завдання", "addNew": "Додати нове завдання", - "submitNewTask": "Створити", - "hideComplete": "Сховати виконані завдання", - "showComplete": "Показати всі завдання" - }, - "url": { - "launch": "Відкрити посилання в браузері", - "copy": "Копіювати посилання в буфер обміну", - "textFieldHint": "Введіть URL" - }, - "relation": { - "relatedDatabasePlaceLabel": "Пов'язана база даних", - "relatedDatabasePlaceholder": "Жодного", - "inRelatedDatabase": "В", - "rowSearchTextFieldPlaceholder": "Пошук", - "noDatabaseSelected": "Базу даних не вибрано, будь ласка, спочатку виберіть одну зі списку нижче:", - "emptySearchResult": "Записів не знайдено", - "linkedRowListLabel": "{count} пов’язаних рядків", - "unlinkedRowListLabel": "Зв'яжіть інший ряд" + "submitNewTask": "Створити" }, "menuName": "Сітка", - "referencedGridPrefix": "Вигляд", - "calculate": "Обчислити", - "calculationTypeLabel": { - "none": "Жодного", - "average": "Середній", - "max": "Макс", - "median": "Медіана", - "min": "Мін", - "sum": "Сума", - "count": "Рахувати", - "countEmpty": "Рахунок порожній", - "countEmptyShort": "ПУСТИЙ", - "countNonEmpty": "Граф не пустий", - "countNonEmptyShort": "ЗАПОВНЕНО" - } + "referencedGridPrefix": "Вигляд" }, "document": { "menuName": "Документ", @@ -1492,41 +537,6 @@ "calendar": { "selectACalendarToLinkTo": "Виберіть календар для посилання", "createANewCalendar": "Створити новий календар" - }, - "document": { - "selectADocumentToLinkTo": "Виберіть документ для посилання" - }, - "name": { - "text": "Текст", - "heading1": "Заголовок 1", - "heading2": "Заголовок 2", - "heading3": "Заголовок 3", - "image": "Зображення", - "bulletedList": "Маркірований список", - "numberedList": "Нумерований список", - "todoList": "Список справ", - "doc": "Док", - "linkedDoc": "Посилання на сторінку", - "grid": "Сітка", - "linkedGrid": "Зв'язана сітка", - "kanban": "Канбан", - "linkedKanban": "Пов’язаний канбан", - "calendar": "Календар", - "linkedCalendar": "Пов’язаний календар", - "quote": "Цитата", - "divider": "Роздільник", - "table": "Таблиця", - "callout": "Виноска", - "outline": "Контур", - "mathEquation": "Математичне рівняння", - "code": "Код", - "toggleList": "Переключити список", - "emoji": "Emoji", - "aiWriter": "ШІ Письменник", - "dateOrReminder": "Дата або нагадування", - "photoGallery": "Фотогалерея", - "file": "Файл", - "checkbox": "Прапорець" } }, "selectionMenu": { @@ -1537,36 +547,26 @@ "referencedBoard": "Пов'язані дошки", "referencedGrid": "Пов'язані сітки", "referencedCalendar": "Календар посилань", - "referencedDocument": "Посилальний документ", - "autoGeneratorMenuItemName": "ШІ Письменник", - "autoGeneratorTitleName": "AI: Запитайте штучний інтелект написати будь-що...", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Запитайте штучний інтелект написати будь-що...", "autoGeneratorLearnMore": "Дізнатися більше", "autoGeneratorGenerate": "Генерувати", - "autoGeneratorHintText": "Запитайте AI...", - "autoGeneratorCantGetOpenAIKey": "Не вдається отримати ключ AI", + "autoGeneratorHintText": "Запитайте OpenAI...", + "autoGeneratorCantGetOpenAIKey": "Не вдається отримати ключ OpenAI", "autoGeneratorRewrite": "Переписати", - "smartEdit": "Запитай ШІ", - "aI": "ШІ", + "smartEdit": "AI Асистенти", + "openAI": "OpenAI", "smartEditFixSpelling": "Виправити правопис", - "warning": "⚠️ Відповіді ШІ можуть бути неточними або вводити в оману.", + "warning": "⚠️ Відповіді AI можуть бути неточними або вводити в оману.", "smartEditSummarize": "Підсумувати", "smartEditImproveWriting": "Покращити написання", "smartEditMakeLonger": "Зробити довше", - "smartEditCouldNotFetchResult": "Не вдалося отримати результат від ШІ", - "smartEditCouldNotFetchKey": "Не вдалося отримати ключ ШІ", - "smartEditDisabled": "Підключіть ШІ в Налаштуваннях", - "appflowyAIEditDisabled": "Увійдіть, щоб увімкнути функції ШІ", + "smartEditCouldNotFetchResult": "Не вдалося отримати результат від OpenAI", + "smartEditCouldNotFetchKey": "Не вдалося отримати ключ OpenAI", + "smartEditDisabled": "Підключіть OpenAI в Налаштуваннях", "discardResponse": "Ви хочете відкинути відповіді AI?", "createInlineMathEquation": "Створити рівняння", - "fonts": "Шрифти", - "insertDate": "Вставте дату", - "emoji": "Emoji", "toggleList": "Перемкнути список", - "quoteList": "Цитатний список", - "numberedList": "Нумерований список", - "bulletedList": "Маркірований список", - "todoList": "Список справ", - "callout": "Виноска", "cover": { "changeCover": "Змінити Обгортку", "colors": "Кольори", @@ -1588,12 +588,10 @@ "couldNotFetchImage": "Не вдалося отримати зображення", "imageSavingFailed": "Не вдалося зберегти зображення", "addIcon": "Додати іконку", - "changeIcon": "Змінити значок", "coverRemoveAlert": "Це буде видалено з обгортки після видалення.", "alertDialogConfirmation": "Ви впевнені, що хочете продовжити?" }, "mathEquation": { - "name": "Математичне рівняння", "addMathEquation": "Додати математичне рівняння", "editMathEquation": "Редагувати математичне рівняння" }, @@ -1610,43 +608,14 @@ "left": "Ліворуч", "center": "По центру", "right": "По праву", - "defaultColor": "За замовчуванням", - "depth": "Глибина" + "defaultColor": "За замовчуванням" }, "image": { - "addAnImage": "Додати зображення", "copiedToPasteBoard": "Посилання на зображення скопійовано в буфер обміну", - "addAnImageDesktop": "Перетягніть зображення або натисніть, щоб додати зображення", - "addAnImageMobile": "Натисніть, щоб додати одне або кілька зображень", - "dropImageToInsert": "Перетягніть зображення, щоб вставити", - "imageUploadFailed": "Помилка завантаження зображення", - "imageDownloadFailed": "Помилка завантаження зображення, повторіть спробу", - "imageDownloadFailedToken": "Помилка завантаження зображення через відсутність маркера користувача. Повторіть спробу", - "errorCode": "Код помилки" - }, - "photoGallery": { - "name": "Фотогалерея", - "imageKeyword": "зображення", - "imageGalleryKeyword": "галерея зображень", - "photoKeyword": "фото", - "photoBrowserKeyword": "браузер фотографій", - "galleryKeyword": "галерея", - "addImageTooltip": "Додайте зображення", - "changeLayoutTooltip": "Змінити макет", - "browserLayout": "Браузер", - "gridLayout": "Сітка", - "deleteBlockTooltip": "Видалити всю галерею" - }, - "math": { - "copiedToPasteBoard": "Математичне рівняння скопійовано в буфер обміну" - }, - "urlPreview": { - "copiedToPasteBoard": "Посилання скопійовано в буфер обміну", - "convertToLink": "Перетворити на вбудоване посилання" + "addAnImage": "Додати зображення" }, "outline": { - "addHeadingToCreateOutline": "Додайте заголовки, щоб створити зміст.", - "noMatchHeadings": "Відповідних заголовків не знайдено." + "addHeadingToCreateOutline": "Додайте заголовки, щоб створити зміст." }, "table": { "addAfter": "Додати після", @@ -1660,51 +629,8 @@ "copy": "Копіювати", "cut": "Вирізати", "paste": "Вставити" - }, - "action": "Дії", - "database": { - "selectDataSource": "Виберіть джерело даних", - "noDataSource": "Немає джерела даних", - "selectADataSource": "Виберіть джерело даних", - "toContinue": "продовжувати", - "newDatabase": "Нова база даних", - "linkToDatabase": "Посилання на базу даних" - }, - "date": "Дата", - "video": { - "label": "Відео", - "emptyLabel": "Додайте відео", - "placeholder": "Вставте посилання на відео", - "copiedToPasteBoard": "Посилання на відео скопійовано в буфер обміну", - "insertVideo": "Додайте відео", - "invalidVideoUrl": "Вихідна URL-адреса ще не підтримується.", - "invalidVideoUrlYouTube": "YouTube ще не підтримується.", - "supportedFormats": "Підтримувані формати: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "Файл", - "uploadTab": "Завантажити", - "networkTab": "Інтегрувати посилання", - "placeholderText": "Натисніть або перетягніть, щоб завантажити файл", - "placeholderDragging": "Перетягніть файл, щоб завантажити", - "dropFileToUpload": "Перетягніть файл, щоб завантажити", - "fileUploadHint": "Перетягніть файл сюди\nабо натисніть, щоб вибрати файл.", - "networkHint": "Введіть посилання на файл", - "networkUrlInvalid": "Недійсна URL-адреса, виправте URL-адресу та повторіть спробу", - "networkAction": "Вставити посилання на файл", - "fileTooBigError": "Розмір файлу завеликий, будь ласка, завантажте файл розміром менше 10 МБ", - "renameFile": { - "title": "Перейменувати файл", - "description": "Введіть нову назву для цього файлу", - "nameEmptyError": "Ім'я файлу не може бути пустим." - }, - "uploadedAt": "Завантажено {}", - "linkedAt": "Посилання додано {}" } }, - "outlineBlock": { - "placeholder": "Зміст" - }, "textBlock": { "placeholder": "Тип '/' для команд" }, @@ -1721,65 +647,24 @@ "label": "URL зображення", "placeholder": "Введіть URL зображення" }, - "ai": { - "label": "Створення зображення за допомогою AI", - "placeholder": "Будь ласка, введіть підказку для AI для створення зображення" - }, - "stability_ai": { - "label": "Створюйте зображення за допомогою Stability AI", - "placeholder": "Будь ласка, введіть підказку для ШІ стабільності, щоб створити зображення" - }, "support": "Розмір зображення обмежений 5 МБ. Підтримувані формати: JPEG, PNG, GIF, SVG", "error": { "invalidImage": "Невірне зображення", "invalidImageSize": "Розмір зображення повинен бути менше 5 МБ", "invalidImageFormat": "Формат зображення не підтримується. Підтримувані формати: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "Невірний URL зображення", - "noImage": "Такого файлу чи каталогу немає", - "multipleImagesFailed": "Не вдалося завантажити одне чи кілька зображень. Повторіть спробу" + "invalidImageUrl": "Невірний URL зображення" }, "embedLink": { "label": "Вставити посилання", "placeholder": "Вставте або введіть посилання на зображення" }, - "unsplash": { - "label": "Unsplash" - }, - "searchForAnImage": "Пошук зображення", - "pleaseInputYourOpenAIKey": "будь ласка, введіть ключ ШІ на сторінці налаштувань", - "saveImageToGallery": "Зберегти зображення", - "failedToAddImageToGallery": "Не вдалося додати зображення до галереї", - "successToAddImageToGallery": "Зображення додано до галереї", - "unableToLoadImage": "Не вдалося завантажити зображення", - "maximumImageSize": "Максимальний підтримуваний розмір зображення для завантаження становить 10 МБ", - "uploadImageErrorImageSizeTooBig": "Розмір зображення має бути менше 10 Мб", - "imageIsUploading": "Зображення завантажується", - "openFullScreen": "Відкрити на весь екран", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Попереднє зображення", - "nextImageTooltip": "Наступне зображення", - "zoomOutTooltip": "Зменшення", - "zoomInTooltip": "Збільшувати", - "changeZoomLevelTooltip": "Змінити рівень масштабування", - "openLocalImage": "Відкрите зображення", - "downloadImage": "Завантажити зображення", - "closeViewer": "Закрити інтерактивний засіб перегляду", - "scalePercentage": "{}%", - "deleteImageTooltip": "Видалити зображення" - } - }, - "pleaseInputYourStabilityAIKey": "будь ласка, введіть ключ стабільності AI на сторінці налаштувань" + "searchForAnImage": "Пошук зображення" }, "codeBlock": { "language": { "label": "Мова", - "placeholder": "Виберіть мову", - "auto": "Авто" - }, - "copyTooltip": "Скопіюйте вміст блоку коду", - "searchLanguageHint": "Пошук мови", - "codeCopiedSnackbar": "Код скопійовано в буфер обміну!" + "placeholder": "Виберіть мову" + } }, "inlineLink": { "placeholder": "Вставте або введіть посилання", @@ -1800,24 +685,11 @@ "page": { "label": "Посилання на сторінку", "tooltip": "Клацніть, щоб відкрити сторінку" - }, - "deleted": "Видалено", - "deletedContent": "Цей вміст не існує або його видалено", - "noAccess": "Немає доступу" + } }, "toolbar": { "resetToDefaultFont": "Скинути до стандартного" }, - "errorBlock": { - "theBlockIsNotSupported": "Не вдалося проаналізувати вміст блоку", - "clickToCopyTheBlockContent": "Натисніть, щоб скопіювати вміст блоку", - "blockContentHasBeenCopied": "Вміст блоку скопійовано." - }, - "mobilePageSelector": { - "title": "Виберіть сторінку", - "failedToLoad": "Не вдалося завантажити список сторінок", - "noPagesFound": "Сторінок не знайдено" - }, "board": { "column": { "createNewCard": "Нова" @@ -1848,7 +720,7 @@ "referencedCalendarPrefix": "Вид на" }, "errorDialog": { - "title": "Помилка в @:appName", + "title": "Помилка в AppFlowy", "howToFixFallback": "Вибачте за незручності! Надішліть звіт про помилку на нашу сторінку GitHub, де ви опишіть свою помилку.", "github": "Переглянути на GitHub" }, @@ -1883,140 +755,7 @@ "gray": "Сірий" } }, - "board": { - "column": { - "createNewCard": "Новий", - "renameGroupTooltip": "Натисніть, щоб перейменувати групу", - "createNewColumn": "Додайте нову групу", - "addToColumnTopTooltip": "Додайте нову картку вгорі", - "addToColumnBottomTooltip": "Додайте нову картку внизу", - "renameColumn": "Перейменувати", - "hideColumn": "Сховати", - "newGroup": "Нова група", - "deleteColumn": "Видалити", - "deleteColumnConfirmation": "Це призведе до видалення цієї групи та всіх карток у ній.\nВи впевнені, що бажаєте продовжити?" - }, - "hiddenGroupSection": { - "sectionTitle": "Приховані групи", - "collapseTooltip": "Приховати приховані групи", - "expandTooltip": "Переглянути приховані групи" - }, - "cardDetail": "Деталі картки", - "cardActions": "Дії з картками", - "cardDuplicated": "Картку дублювали", - "cardDeleted": "Картку видалено", - "showOnCard": "Показати на реквізитах картки", - "setting": "Налаштування", - "propertyName": "Назва власності", - "menuName": "Дошка", - "showUngrouped": "Показати не згруповані елементи", - "ungroupedButtonText": "Розгрупований", - "ungroupedButtonTooltip": "Містить картки, які не належать до жодної групи", - "ungroupedItemsTitle": "Натисніть, щоб додати до дошки", - "groupBy": "Групувати за", - "groupCondition": "Стан групи", - "referencedBoardPrefix": "Вид на", - "notesTooltip": "Нотатки всередині", - "mobile": { - "editURL": "Редагувати URL", - "showGroup": "Показати групу", - "showGroupContent": "Ви впевнені, що хочете показати цю групу на дошці?", - "failedToLoad": "Не вдалося завантажити вигляд дошки" - }, - "dateCondition": { - "weekOf": "Тиждень з {} по {}", - "today": "Сьогодні", - "yesterday": "вчора", - "tomorrow": "Завтра", - "lastSevenDays": "Останні 7 днів", - "nextSevenDays": "Наступні 7 днів", - "lastThirtyDays": "Останні 30 днів", - "nextThirtyDays": "Наступні 30 днів" - }, - "noGroup": "Немає груп за властивостями", - "noGroupDesc": "Для відображення подання дошки потрібна властивість для групування" - }, - "calendar": { - "menuName": "Календар", - "defaultNewCalendarTitle": "Без назви", - "newEventButtonTooltip": "Додайте нову подію", - "navigation": { - "today": "Сьогодні", - "jumpToday": "Перейти до сьогоднішнього дня", - "previousMonth": "Попередній місяць", - "nextMonth": "Наступного місяця", - "views": { - "day": "День", - "week": "Тиждень", - "month": "Місяць", - "year": "Рік" - } - }, - "mobileEventScreen": { - "emptyTitle": "Подій ще немає", - "emptyBody": "Натисніть кнопку плюс, щоб створити подію в цей день." - }, - "settings": { - "showWeekNumbers": "Показати номери тижнів", - "showWeekends": "Показати вихідні", - "firstDayOfWeek": "Почніть тиждень", - "layoutDateField": "Верстка календаря по", - "changeLayoutDateField": "Змінити поле макета", - "noDateTitle": "Без дати", - "noDateHint": { - "zero": "Тут відображатимуться незаплановані події", - "one": "{count} незапланована подія", - "other": "{count} незапланованих подій" - }, - "unscheduledEventsTitle": "Позапланові заходи", - "clickToAdd": "Натисніть, щоб додати до календаря", - "name": "Налаштування календаря", - "clickToOpen": "Натисніть, щоб відкрити запис" - }, - "referencedCalendarPrefix": "Вид на", - "quickJumpYear": "Перейти до", - "duplicateEvent": "Дубльована подія" - }, - "errorDialog": { - "title": "@:appName Помилка", - "howToFixFallback": "Просимо вибачення за незручності! Надішліть проблему на нашій сторінці GitHub з описом вашої помилки.", - "howToFixFallbackHint1": "Просимо вибачення за незручності! Надішліть питання на нашому ", - "howToFixFallbackHint2": " сторінка, яка описує вашу помилку.", - "github": "Переглянути на GitHub" - }, - "search": { - "label": "Пошук", - "sidebarSearchIcon": "Шукайте та швидко переходьте на сторінку", - "placeholder": { - "actions": "Пошукові дії..." - } - }, - "message": { - "copy": { - "success": "Скопійовано!", - "fail": "Неможливо скопіювати" - } - }, - "unSupportBlock": "Поточна версія не підтримує цей блок.", - "views": { - "deleteContentTitle": "Ви дійсно хочете видалити {pageType}?", - "deleteContentCaption": "якщо ви видалите цей {pageType}, ви зможете відновити його з кошика." - }, - "colors": { - "custom": "Користувальницькі", - "default": "За замовчуванням", - "red": "Червоний", - "orange": "Помаранчевий", - "yellow": "Жовтий", - "green": "Зелений", - "blue": "Синій", - "purple": "Фіолетовий", - "pink": "Рожевий", - "brown": "Коричневий", - "gray": "Сірий" - }, "emoji": { - "emojiTab": "Emoji", "search": "Пошук емодзі", "noRecent": "Немає недавніх емодзі", "noEmojiFound": "Емодзі не знайдено", @@ -2026,66 +765,27 @@ "remove": "Видалити емодзі", "categories": { "smileys": "Смайли та емоції", - "people": "люди", - "animals": "природа", - "food": "їжа", - "activities": "активності", - "places": "місця", - "objects": "об'єкти", - "symbols": "символи", - "flags": "прапори", - "nature": "природа", - "frequentlyUsed": "часто використовувані" - }, - "skinTone": { - "default": "За замовчуванням", - "light": "Світла", - "mediumLight": "Середньосвітлий", - "medium": "Середній", - "mediumDark": "Середньо-темний", - "dark": "Темний" - }, - "openSourceIconsFrom": "Піктограми з відкритим кодом" + "people": "Люди та тіло", + "animals": "Тварини та природа", + "food": "Їжа та напої", + "activities": "Активності", + "places": "Подорожі та місця", + "objects": "Об'єкти", + "symbols": "Символи", + "flags": "Прапори", + "nature": "Природа", + "frequentlyUsed": "Часто використовувані" + } }, "inlineActions": { "noResults": "Немає результатів", - "recentPages": "Останні сторінки", "pageReference": "Посилання на сторінку", - "docReference": "Посилання на документ", - "boardReference": "Довідка дошки", - "calReference": "Довідка календаря", - "gridReference": "Посилання на сітку", "date": "Дата", "reminder": { "groupTitle": "Нагадування", "shortKeyword": "нагадати" } }, - "datePicker": { - "dateTimeFormatTooltip": "Змініть формат дати та часу в налаштуваннях", - "dateFormat": "Формат дати", - "includeTime": "Включіть час", - "isRange": "Дата закінчення", - "timeFormat": "Формат часу", - "clearDate": "Ясна дата", - "reminderLabel": "Нагадування", - "selectReminder": "Виберіть нагадування", - "reminderOptions": { - "none": "Жодного", - "atTimeOfEvent": "Час події", - "fiveMinsBefore": "5 хвилин до", - "tenMinsBefore": "за 10 хвилин до", - "fifteenMinsBefore": "15 хвилин до", - "thirtyMinsBefore": "30 хвилин до", - "oneHourBefore": "за 1 годину до", - "twoHoursBefore": "за 2 години до", - "onDayOfEvent": "У день події", - "oneDayBefore": "за 1 день до", - "twoDaysBefore": "за 2 дні до", - "oneWeekBefore": "1 тиждень тому", - "custom": "Налаштовуване" - } - }, "relativeDates": { "yesterday": "Вчора", "today": "Сьогодні", @@ -2094,27 +794,6 @@ }, "notificationHub": { "title": "Сповіщення", - "mobile": { - "title": "Оновлення" - }, - "emptyTitle": "Все наздогнали!", - "emptyBody": "Немає незавершених сповіщень або дій. Насолоджуйся спокоєм.", - "tabs": { - "inbox": "Вхідні", - "upcoming": "Майбутні" - }, - "actions": { - "markAllRead": "Позначити все як прочитане", - "showAll": "Все", - "showUnreads": "Непрочитаний" - }, - "filters": { - "ascending": "Висхідний", - "descending": "Спускається", - "groupByDate": "Групувати за датою", - "showUnreadsOnly": "Показати лише непрочитані", - "resetToDefault": "Скинути до замовчування" - }, "empty": "Тут нічого немає!" }, "reminderNotification": { @@ -2132,458 +811,6 @@ "replace": "Замінити", "replaceAll": "Замінити всі", "noResult": "Немає результатів", - "caseSensitive": "З урахуванням регістру", - "searchMore": "Шукайте, щоб знайти більше результатів" - }, - "error": { - "weAreSorry": "Нам шкода", - "loadingViewError": "У нас виникла проблема із завантаженням цього перегляду. Перевірте підключення до Інтернету, оновіть програму та не соромтеся зв’язатися з командою, якщо проблема не зникне.", - "syncError": "Дані не синхронізуються з іншого пристрою", - "syncErrorHint": "Будь ласка, повторно відкрийте цю сторінку на пристрої, де її востаннє редагували, а потім знову відкрийте її на поточному пристрої.", - "clickToCopy": "Натисніть, щоб скопіювати код помилки" - }, - "editor": { - "bold": "Жирний", - "bulletedList": "Маркірований список", - "bulletedListShortForm": "Маркірований", - "checkbox": "Прапорець", - "embedCode": "Вставити код", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "Виділіть", - "color": "Колір", - "image": "Зображення", - "date": "Дата", - "page": "Сторінка", - "italic": "Курсив", - "link": "Посилання", - "numberedList": "Нумерований список", - "numberedListShortForm": "Пронумерований", - "quote": "Цитата", - "strikethrough": "Закреслення", - "text": "Текст", - "underline": "Підкреслити", - "fontColorDefault": "За замовчуванням", - "fontColorGray": "Сірий", - "fontColorBrown": "Коричневий", - "fontColorOrange": "Помаранчевий", - "fontColorYellow": "Жовтий", - "fontColorGreen": "Зелений", - "fontColorBlue": "Синій", - "fontColorPurple": "Фіолетовий", - "fontColorPink": "Рожевий", - "fontColorRed": "Червоний", - "backgroundColorDefault": "Фон за умовчанням", - "backgroundColorGray": "Сірий фон", - "backgroundColorBrown": "Коричневий фон", - "backgroundColorOrange": "Помаранчевий фон", - "backgroundColorYellow": "Жовтий фон", - "backgroundColorGreen": "Зелений фон", - "backgroundColorBlue": "Блакитний фон", - "backgroundColorPurple": "Фіолетовий фон", - "backgroundColorPink": "Рожевий фон", - "backgroundColorRed": "Червоний фон", - "backgroundColorLime": "Вапно фону", - "backgroundColorAqua": "Аква фону", - "done": "Готово", - "cancel": "Скасувати", - "tint1": "Відтінок 1", - "tint2": "Відтінок 2", - "tint3": "Відтінок 3", - "tint4": "Відтінок 4", - "tint5": "Відтінок 5", - "tint6": "Відтінок 6", - "tint7": "Відтінок 7", - "tint8": "Відтінок 8", - "tint9": "Відтінок 9", - "lightLightTint1": "Фіолетовий", - "lightLightTint2": "Рожевий", - "lightLightTint3": "Світло-рожевий", - "lightLightTint4": "Помаранчевий", - "lightLightTint5": "Жовтий", - "lightLightTint6": "Вапно", - "lightLightTint7": "Зелений", - "lightLightTint8": "Аква", - "lightLightTint9": "Синій", - "urlHint": "URL", - "mobileHeading1": "Заголовок 1", - "mobileHeading2": "Заголовок 2", - "mobileHeading3": "Заголовок 3", - "textColor": "Колір тексту", - "backgroundColor": "Колір фону", - "addYourLink": "Додайте своє посилання", - "openLink": "Відкрити посилання", - "copyLink": "Копіювати посилання", - "removeLink": "Видалити посилання", - "editLink": "Редагувати посилання", - "linkText": "Текст", - "linkTextHint": "Будь ласка, введіть текст", - "linkAddressHint": "Будь ласка, введіть URL", - "highlightColor": "Колір виділення", - "clearHighlightColor": "Чіткий колір виділення", - "customColor": "Індивідуальний колір", - "hexValue": "Шістнадцяткове значення", - "opacity": "Непрозорість", - "resetToDefaultColor": "Відновити колір за замовчуванням", - "ltr": "LTR", - "rtl": "RTL", - "auto": "Авто", - "cut": "Вирізати", - "copy": "Копіювати", - "paste": "Вставити", - "find": "Знайти", - "select": "Виберіть", - "selectAll": "Вибрати все", - "previousMatch": "Попередній матч", - "nextMatch": "Наступний матч", - "closeFind": "Закрити", - "replace": "Замінити", - "replaceAll": "Замінити все", - "regex": "Регулярний вираз", - "caseSensitive": "Чутливий до регістру", - "uploadImage": "Завантажити зображення", - "urlImage": "URL зображення", - "incorrectLink": "Невірне посилання", - "upload": "Завантажити", - "chooseImage": "Виберіть зображення", - "loading": "Завантаження", - "imageLoadFailed": "Не вдалося завантажити зображення", - "divider": "Роздільник", - "table": "Таблиця", - "colAddBefore": "Додати перед", - "rowAddBefore": "Додати перед", - "colAddAfter": "Додайте після", - "rowAddAfter": "Додайте після", - "colRemove": "Видалити", - "rowRemove": "Видалити", - "colDuplicate": "Дублювати", - "rowDuplicate": "Дублювати", - "colClear": "Очистити вміст", - "rowClear": "Очистити вміст", - "slashPlaceHolder": "Введіть '/', щоб вставити блок, або почніть вводити", - "typeSomething": "Введіть щось...", - "toggleListShortForm": "Перемикач", - "quoteListShortForm": "Цитата", - "mathEquationShortForm": "Формула", - "codeBlockShortForm": "Код" - }, - "favorite": { - "noFavorite": "Немає улюбленої сторінки", - "noFavoriteHintText": "Проведіть пальцем по сторінці вліво, щоб додати її до вибраного", - "removeFromSidebar": "Видалити з бічної панелі", - "addToSidebar": "Закріпити на бічній панелі" - }, - "cardDetails": { - "notesPlaceholder": "Введіть /, щоб вставити блок, або почніть вводити текст" - }, - "blockPlaceholders": { - "todoList": "Зробити", - "bulletList": "Список", - "numberList": "Список", - "quote": "Цитата", - "heading": "Заголовок {}" - }, - "titleBar": { - "pageIcon": "Значок сторінки", - "language": "Мова", - "font": "Шрифт", - "actions": "Дії", - "date": "Дата", - "addField": "Додати поле", - "userIcon": "Значок користувача" - }, - "noLogFiles": "Немає файлів журналу", - "newSettings": { - "myAccount": { - "title": "Мій рахунок", - "subtitle": "Налаштуйте свій профіль, керуйте безпекою облікового запису, відкривайте ключі AI або ввійдіть у свій обліковий запис.", - "profileLabel": "Назва облікового запису та зображення профілю", - "profileNamePlaceholder": "Введіть ім'я", - "accountSecurity": "Безпека облікового запису", - "2FA": "2-етапна автентифікація", - "aiKeys": "AI ключі", - "accountLogin": "Вхід до облікового запису", - "updateNameError": "Не вдалося оновити назву", - "updateIconError": "Не вдалося оновити значок", - "deleteAccount": { - "title": "Видалити аккаунт", - "subtitle": "Назавжди видалити свій обліковий запис і всі ваші дані.", - "deleteMyAccount": "Видалити мій обліковий запис", - "dialogTitle": "Видалити аккаунт", - "dialogContent1": "Ви впевнені, що хочете остаточно видалити свій обліковий запис?", - "dialogContent2": "Цю дію неможливо скасувати. Вона призведе до скасування доступу до всіх командних просторів, видалення всього вашого облікового запису, включаючи приватні робочі простори, і видалення вас із усіх спільних робочих просторів." - } - }, - "workplace": { - "name": "Робоче місце", - "title": "Налаштування робочого місця", - "subtitle": "Налаштуйте зовнішній вигляд робочої області, тему, шрифт, макет тексту, дату, час і мову.", - "workplaceName": "Назва робочого місця", - "workplaceNamePlaceholder": "Введіть назву робочого місця", - "workplaceIcon": "Значок на робочому місці", - "workplaceIconSubtitle": "Завантажте зображення або використовуйте емодзі для свого робочого простору. Значок з’явиться на бічній панелі та в сповіщеннях.", - "renameError": "Не вдалося перейменувати робоче місце", - "updateIconError": "Не вдалося оновити значок", - "chooseAnIcon": "Виберіть значок", - "appearance": { - "name": "Зовнішній вигляд", - "themeMode": { - "auto": "Авто", - "light": "Світла", - "dark": "Темна" - }, - "language": "Мова" - } - }, - "syncState": { - "syncing": "Синхронізація", - "synced": "Синхронізовано", - "noNetworkConnected": "Немає підключення до мережі" - } - }, - "pageStyle": { - "title": "Стиль сторінки", - "layout": "Макет", - "coverImage": "Зображення обкладинки", - "pageIcon": "Значок сторінки", - "colors": "Кольори", - "gradient": "Градієнт", - "backgroundImage": "Фонове зображення", - "presets": "Предустановки", - "photo": "Фото", - "unsplash": "Unsplash", - "pageCover": "Обкладинка сторінки", - "none": "Жодного", - "openSettings": "Відкрийте налаштування", - "photoPermissionTitle": "@:appName хоче отримати доступ до вашої бібліотеки фотографій", - "photoPermissionDescription": "Дозвольте доступ до бібліотеки фотографій для завантаження зображень.", - "doNotAllow": "Не дозволяти", - "image": "Зображення" - }, - "commandPalette": { - "placeholder": "Введіть для пошуку...", - "bestMatches": "Найкращі збіги", - "recentHistory": "Новітня історія", - "navigateHint": "для навігації", - "loadingTooltip": "Чекаємо результатів...", - "betaLabel": "БЕТА", - "betaTooltip": "Зараз ми підтримуємо лише пошук сторінок і вмісту в документах", - "fromTrashHint": "Зі сміття", - "noResultsHint": "Ми не знайшли те, що ви шукали, спробуйте знайти інший термін.", - "clearSearchTooltip": "Очистити поле пошуку" - }, - "space": { - "delete": "Видалити", - "deleteConfirmation": "Видалити: ", - "deleteConfirmationDescription": "Усі сторінки в цьому просторі буде видалено та переміщено до кошика, а опубліковані сторінки буде скасовано.", - "rename": "Перейменувати простір", - "changeIcon": "Змінити значок", - "manage": "Керуйте простором", - "addNewSpace": "Створити простір", - "collapseAllSubPages": "Згорнути всі підсторінки", - "createNewSpace": "Створіть новий простір", - "createSpaceDescription": "Створіть кілька публічних і приватних просторів, щоб краще організувати свою роботу.", - "spaceName": "Назва простору", - "spaceNamePlaceholder": "наприклад, маркетинг, інженерія, HR", - "permission": "Дозвіл", - "publicPermission": "Громадський", - "publicPermissionDescription": "Усі учасники робочої області з повним доступом", - "privatePermission": "Приватний", - "privatePermissionDescription": "Тільки ви маєте доступ до цього простору", - "spaceIconBackground": "Колір фону", - "spaceIcon": "Значок", - "dangerZone": "НЕБЕЗПЕЧНА ЗОНА", - "unableToDeleteLastSpace": "Неможливо видалити останній пробіл", - "unableToDeleteSpaceNotCreatedByYou": "Неможливо видалити простори, створені іншими", - "enableSpacesForYourWorkspace": "Увімкніть Spaces для своєї робочої області", - "title": "Пробіли", - "defaultSpaceName": "Загальний", - "upgradeSpaceTitle": "Увімкнути Spaces", - "upgradeSpaceDescription": "Створіть кілька публічних і приватних просторів, щоб краще організувати свій робочий простір.", - "upgrade": "Оновити", - "upgradeYourSpace": "Створіть кілька просторів", - "quicklySwitch": "Швидко перейдіть до наступного місця", - "duplicate": "Дубльований простір", - "movePageToSpace": "Перемістити сторінку в простір", - "switchSpace": "Переключити простір", - "spaceNameCannotBeEmpty": "Назва групи не може бути пустою" - }, - "publish": { - "hasNotBeenPublished": "Ця сторінка ще не опублікована", - "reportPage": "Сторінка звіту", - "databaseHasNotBeenPublished": "Публікація бази даних ще не підтримується.", - "createdWith": "Створено за допомогою", - "downloadApp": "Завантажити AppFlowy", - "copy": { - "codeBlock": "Вміст блоку коду скопійовано в буфер обміну", - "imageBlock": "Посилання на зображення скопійовано в буфер обміну", - "mathBlock": "Математичне рівняння скопійовано в буфер обміну", - "fileBlock": "Посилання на файл скопійовано в буфер обміну" - }, - "containsPublishedPage": "Ця сторінка містить одну або кілька опублікованих сторінок. Якщо ви продовжите, їх публікацію буде скасовано. Ви хочете продовжити видалення?", - "publishSuccessfully": "Успішно опубліковано", - "unpublishSuccessfully": "Публікацію скасовано", - "publishFailed": "Не вдалося опублікувати", - "unpublishFailed": "Не вдалося скасувати публікацію", - "noAccessToVisit": "Немає доступу до цієї сторінки...", - "createWithAppFlowy": "Створіть веб-сайт за допомогою AppFlowy", - "fastWithAI": "Швидко та легко з AI.", - "tryItNow": "Спробуй зараз", - "onlyGridViewCanBePublished": "Можна опублікувати лише режим сітки", - "database": { - "zero": "Опублікувати {} вибране представлення", - "one": "Опублікувати {} вибраних представлень", - "many": "Опублікувати {} вибраних представлень", - "other": "Опублікувати {} вибраних представлень" - }, - "mustSelectPrimaryDatabase": "Необхідно вибрати основний вид", - "noDatabaseSelected": "Базу даних не вибрано, виберіть принаймні одну базу даних.", - "unableToDeselectPrimaryDatabase": "Неможливо скасувати вибір основної бази даних", - "saveThisPage": "Зберегти цю сторінку", - "duplicateTitle": "Куди б ви хотіли додати", - "selectWorkspace": "Виберіть робочу область", - "addTo": "Додати до", - "duplicateSuccessfully": "Дубльований успіх. Хочете переглянути документи?", - "duplicateSuccessfullyDescription": "Немає програми? Ваше завантаження розпочнеться автоматично після натискання кнопки «Завантажити».", - "downloadIt": "Завантажити", - "openApp": "Відкрити в додатку", - "duplicateFailed": "Не вдалося створити копію", - "membersCount": { - "zero": "Немає учасників", - "one": "1 учасник", - "many": "{count} учасників", - "other": "{count} учасників" - } - }, - "web": { - "continue": "Продовжити", - "or": "або", - "continueWithGoogle": "Продовжуйте з Google", - "continueWithGithub": "Продовжити з GitHub", - "continueWithDiscord": "Продовжуйте з Discord", - "signInAgreement": "Натиснувши «Продовжити» вище, ви підтверджуєте це\nви прочитали, зрозуміли та погодилися\nAppFlowy", - "and": "і", - "termOfUse": "Умови", - "privacyPolicy": "Політика конфіденційності", - "signInError": "Помилка входу", - "login": "Зареєструйтесь або увійдіть", - "fileBlock": { - "uploadedAt": "Завантажено {time}", - "linkedAt": "Посилання додано {time}", - "empty": "Завантажте або вставте файл" - } - }, - "globalComment": { - "comments": "Коментарі", - "addComment": "Додати коментар", - "reactedBy": "відреагував", - "addReaction": "Додайте реакцію", - "reactedByMore": "та {count} інших", - "showSeconds": { - "one": "1 секунду тому", - "other": "{count} секунд тому", - "zero": "Прямо зараз", - "many": "{count} секунд тому" - }, - "showMinutes": { - "one": "1 хвилину тому", - "other": "{count} хвилин тому", - "many": "{count} хвилин тому" - }, - "showHours": { - "one": "1 годину тому", - "other": "{count} годин тому", - "many": "{count} годин тому" - }, - "showDays": { - "one": "1 день тому", - "other": "{count} днів тому", - "many": "{count} днів тому" - }, - "showMonths": { - "one": "1 місяць тому", - "other": "{count} місяців тому", - "many": "{count} місяців тому" - }, - "showYears": { - "one": "1 рік тому", - "other": "{count} років тому", - "many": "{count} років тому" - }, - "reply": "Відповісти", - "deleteComment": "Видалити коментар", - "youAreNotOwner": "Ви не є власником цього коментаря", - "confirmDeleteDescription": "Ви впевнені, що хочете видалити цей коментар?", - "hasBeenDeleted": "Видалено", - "replyingTo": "Відповідаючи на", - "noAccessDeleteComment": "Ви не можете видалити цей коментар", - "collapse": "Згорнути", - "readMore": "Читати далі", - "failedToAddComment": "Не вдалося додати коментар", - "commentAddedSuccessfully": "Коментар успішно додано.", - "commentAddedSuccessTip": "Ви щойно додали коментар або відповіли на нього. Бажаєте перейти вгору, щоб переглянути останні коментарі?" - }, - "template": { - "asTemplate": "Як шаблон", - "name": "Назва шаблону", - "description": "Опис шаблону", - "about": "Шаблон Про", - "preview": "Попередній перегляд шаблону", - "categories": "Категорії шаблонів", - "isNewTemplate": "PIN для нового шаблону", - "featured": "PIN-код для рекомендованого", - "relatedTemplates": "Пов’язані шаблони", - "requiredField": "{field} є обов’язковим", - "addCategory": "Додати \"{category}\"", - "addNewCategory": "Додати нову категорію", - "addNewCreator": "Додати нового творця", - "deleteCategory": "Видалити категорію", - "editCategory": "Редагувати категорію", - "editCreator": "Редагувати творця", - "category": { - "name": "Назва категорії", - "icon": "Значок категорії", - "bgColor": "Колір фону категорії", - "priority": "Пріоритет категорії", - "desc": "Опис категорії", - "type": "Тип категорії", - "icons": "Іконки категорій", - "colors": "Категорія кольори", - "byUseCase": "За випадком використання", - "byFeature": "За ознакою", - "deleteCategory": "Видалити категорію", - "deleteCategoryDescription": "Ви впевнені, що хочете видалити цю категорію?", - "typeToSearch": "Введіть для пошуку категорій..." - }, - "creator": { - "label": "Творець шаблонів", - "name": "Ім'я творця", - "avatar": "Аватар творця", - "accountLinks": "Посилання на акаунт творця", - "uploadAvatar": "Натисніть, щоб завантажити аватар", - "deleteCreator": "Видалити творця", - "deleteCreatorDescription": "Ви впевнені, що хочете видалити цього творця?", - "typeToSearch": "Введіть для пошуку творців..." - }, - "uploadSuccess": "Шаблон успішно завантажено", - "uploadSuccessDescription": "Ваш шаблон успішно завантажено. Тепер ви можете переглянути його в галереї шаблонів.", - "viewTemplate": "Переглянути шаблон", - "deleteTemplate": "Видалити шаблон", - "deleteTemplateDescription": "Ви впевнені, що хочете видалити цей шаблон?", - "addRelatedTemplate": "Додайте відповідний шаблон", - "removeRelatedTemplate": "Видалити пов’язаний шаблон", - "uploadAvatar": "Завантажити аватар", - "searchInCategory": "Шукати в {category}", - "label": "Шаблон" - }, - "fileDropzone": { - "dropFile": "Натисніть або перетягніть файл у цю область, щоб завантажити", - "uploading": "Завантаження...", - "uploadFailed": "Помилка завантаження", - "uploadSuccess": "Завантаження успішне", - "uploadSuccessDescription": "Файл успішно завантажено", - "uploadFailedDescription": "Не вдалося завантажити файл", - "uploadingDescription": "Файл завантажується" + "caseSensitive": "З урахуванням регістру" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ur.json b/frontend/resources/translations/ur.json index e981a41f3b..1d4f936d37 100644 --- a/frontend/resources/translations/ur.json +++ b/frontend/resources/translations/ur.json @@ -331,7 +331,7 @@ "name": "نام", "email": "ای میل", "selectAnIcon": "آئیکن منتخب کریں", - "pleaseInputYourOpenAIKey": "براہ کرم اپنی AI کی درج کریں", + "pleaseInputYourOpenAIKey": "براہ کرم اپنی OpenAI کی درج کریں", "clickToLogout": "موجودہ صارف سے لاگ آؤٹ کرنے کے لیے کلک کریں" }, "shortcuts": { @@ -511,23 +511,23 @@ "referencedBoard": "حوالہ شدہ بورڈ", "referencedGrid": "حوالہ شدہ گرِڈ", "referencedCalendar": "حوالہ شدہ کیلنڈر", - "autoGeneratorMenuItemName": "AI رائٹر", - "autoGeneratorTitleName": "AI: AI سے کچھ بھی لکھنے کے لیے کہیں...", + "autoGeneratorMenuItemName": "OpenAI رائٹر", + "autoGeneratorTitleName": "OpenAI: AI سے کچھ بھی لکھنے کے لیے کہیں...", "autoGeneratorLearnMore": "مزید جانئے", "autoGeneratorGenerate": "جنریٹ کریں", - "autoGeneratorHintText": "AI سے پوچھیں...", - "autoGeneratorCantGetOpenAIKey": "AI کی حاصل نہیں کر سکتا", + "autoGeneratorHintText": "OpenAI سے پوچھیں...", + "autoGeneratorCantGetOpenAIKey": "OpenAI کی حاصل نہیں کر سکتا", "autoGeneratorRewrite": "دوبارہ لکھیں", "smartEdit": "AI اسسٹنٹ", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "املا درست کریں", "warning": "⚠️ AI کی پاسخیں غلط یا گمراہ کن ہو سکتی ہیں۔", "smartEditSummarize": "سارے لکھیں", "smartEditImproveWriting": "تحریر بہتر بنائیں", "smartEditMakeLonger": "طویل تر بنائیں", - "smartEditCouldNotFetchResult": "AI سے نتیجہ حاصل نہیں کر سکا", - "smartEditCouldNotFetchKey": "AI کی حاصل نہیں کر سکا", - "smartEditDisabled": "Settings میں AI سے منسلک کریں", + "smartEditCouldNotFetchResult": "OpenAI سے نتیجہ حاصل نہیں کر سکا", + "smartEditCouldNotFetchKey": "OpenAI کی حاصل نہیں کر سکا", + "smartEditDisabled": "Settings میں OpenAI سے منسلک کریں", "discardResponse": "کیا آپ AI کی پاسخیں حذف کرنا چاہتے ہیں؟", "createInlineMathEquation": "مساوات بنائیں", "toggleList": "فہرست ٹوگل کریں", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index e60648590d..f0b4c0b10d 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -26,7 +26,7 @@ "repeatPasswordEmptyError": "Mật khẩu nhập lại không được để trống", "unmatchedPasswordError": "Mật khẩu nhập lại không giống mật khẩu", "alreadyHaveAnAccount": "Đã có tài khoản?", - "emailHint": "E-mail", + "emailHint": "Email", "passwordHint": "Mật khẩu", "repeatPasswordHint": "Nhập lại mật khẩu", "signUpWith": "Đăng ký với:" @@ -36,39 +36,18 @@ "loginButtonText": "Đăng nhập", "loginStartWithAnonymous": "Bắt đầu với một phiên ẩn danh", "continueAnonymousUser": "Tiếp tục với một phiên ẩn danh", - "anonymous": "Ẩn danh", "buttonText": "Đăng nhập", "signingInText": "Đang đăng nhập...", "forgotPassword": "Quên mật khẩu?", - "emailHint": "E-mail", + "emailHint": "Email", "passwordHint": "Mật khẩu", "dontHaveAnAccount": "Bạn chưa có tài khoản?", - "createAccount": "Tạo tài khoản", "repeatPasswordEmptyError": "Mật khẩu nhập lại không được để trống", "unmatchedPasswordError": "Mật khẩu nhập lại không giống mật khẩu", "syncPromptMessage": "Việc đồng bộ hóa dữ liệu có thể mất một lúc. Xin đừng đóng trang này", "or": "HOẶC", - "signInWithGoogle": "Tiếp tục với Google", - "signInWithGithub": "Tiếp tục với Github", - "signInWithDiscord": "Tiếp tục với Discord", - "signInWithApple": "Tiếp tục với Apple", - "continueAnotherWay": "Tiếp tục theo cách khác", - "signUpWithGoogle": "Đăng ký với Google", - "signUpWithGithub": "Đăng ký với Github", - "signUpWithDiscord": "Đăng ký với Discord", "signInWith": "Đăng nhập bằng:", "signInWithEmail": "Đăng nhập bằng Email", - "signInWithMagicLink": "Tiếp tục", - "signUpWithMagicLink": "Đăng ký với Magic Link", - "pleaseInputYourEmail": "Vui lòng nhập địa chỉ email của bạn", - "settings": "Cài đặt", - "magicLinkSent": "Đã gửi Magic Link!", - "invalidEmail": "Vui lòng nhập địa chỉ email hợp lệ", - "alreadyHaveAnAccount": "Bạn đã có tài khoản?", - "logIn": "Đăng nhập", - "generalError": "Có gì đó không ổn. Vui lòng thử lại sau", - "limitRateError": "Vì lý do bảo mật, bạn chỉ có thể yêu cầu Magic Link sau mỗi 60 giây", - "magicLinkSentDescription": "Một Magic Link đã được gửi đến email của bạn. Nhấp vào liên kết để hoàn tất đăng nhập. Liên kết sẽ hết hạn sau 5 phút.", "LogInWithGoogle": "Đăng nhập bằng Google", "LogInWithGithub": "Đăng nhập bằng Github", "LogInWithDiscord": "Đăng nhập bằng Discord", @@ -78,7 +57,6 @@ "chooseWorkspace": "Chọn không gian làm việc của bạn", "create": "Tạo không gian làm việc", "reset": "Đặt lại không gian làm việc", - "renameWorkspace": "Đổi tên không gian làm việc", "resetWorkspacePrompt": "Đặt lại không gian làm việc sẽ xóa tất cả các trang và dữ liệu trong đó. Bạn có chắc chắn muốn đặt lại không gian làm việc? Ngoài ra, bạn có thể liên hệ với nhóm hỗ trợ để khôi phục không gian làm việc", "hint": "không gian làm việc", "notFoundError": "Không tìm thấy không gian làm việc", @@ -111,25 +89,15 @@ "buttonText": "Chia sẻ", "workInProgress": "Sắp ra mắt", "markdown": "Markdown", - "html": "HTML", - "clipboard": "Sao chép vào clipboard", "csv": "CSV", - "copyLink": "Sao chép đường dẫn", - "publishToTheWeb": "Xuất bản lên Web", - "publishToTheWebHint": "Tạo trang web với AppFlowy", - "publish": "Xuất bản", - "unPublish": "Hủy xuất bản", - "visitSite": "Truy cập trang web", - "exportAsTab": "Xuất khẩu dưới dạng", - "publishTab": "Xuất bản", - "shareTab": "Chia sẻ" + "copyLink": "Sao chép đường dẫn" }, "moreAction": { "small": "nhỏ", "medium": "trung bình", "large": "lớn", "fontSize": "Cỡ chữ", - "import": "Nhập", + "import": "Import", "moreOptions": "Lựa chọn khác", "wordCount": "Số từ: {}", "charCount": "Số ký tự: {}", @@ -153,9 +121,7 @@ "openNewTab": "Mở trong tab mới", "moveTo": "Chuyển tới", "addToFavorites": "Thêm vào mục yêu thích", - "copyLink": "Sao chép đường dẫn", - "changeIcon": "Thay đổi biểu tượng", - "collapseAllPages": "Thu gọn tất cả các trang con" + "copyLink": "Sao chép đường dẫn" }, "blankPageTitle": "Trang trống", "newPageText": "Trang mới", @@ -163,34 +129,6 @@ "newGridText": "Lưới mới", "newCalendarText": "Lịch mới", "newBoardText": "Bảng mới", - "chat": { - "newChat": "AI Chat", - "inputMessageHint": "Hỏi @:appName AI", - "inputLocalAIMessageHint": "Hỏi @:appName AI cục bộ", - "unsupportedCloudPrompt": "Tính năng này chỉ khả dụng khi sử dụng @:appName Cloud", - "relatedQuestion": "Có liên quan", - "serverUnavailable": "Dịch vụ tạm thời không khả dụng. Vui lòng thử lại sau.", - "aiServerUnavailable": "🌈 Ồ không! 🌈. Một con kỳ lân đã ăn mất câu trả lời của chúng tôi. Vui lòng thử lại!", - "clickToRetry": "Nhấp để thử lại", - "regenerateAnswer": "Tạo lại", - "question1": "Cách sử dụng Kanban để quản lý nhiệm vụ", - "question2": "Giải thích phương pháp GTD", - "question3": "Tại sao sử dụng Rust", - "question4": "Công thức với những gì đang có", - "aiMistakePrompt": "AI có thể mắc lỗi. Hãy kiểm tra thông tin quan trọng.", - "chatWithFilePrompt": "Bạn có muốn trò chuyện với tập tin không?", - "indexFileSuccess": "Đang lập chỉ mục tệp thành công", - "inputActionNoPages": "Không có kết quả trang", - "referenceSource": { - "zero": "0 nguồn được tìm thấy", - "one": "{count} nguồn đã tìm thấy", - "other": "{count} nguồn được tìm thấy" - }, - "clickToMention": "Nhấp để đề cập đến một trang", - "uploadFile": "Tải lên các tệp PDF, md hoặc txt để trò chuyện", - "questionDetail": "Xin chào {}! Tôi có thể giúp gì cho bạn hôm nay?", - "indexingFile": "Đang lập chỉ mục {}" - }, "trash": { "text": "Thùng rác", "restoreAll": "Khôi phục lại tất cả", @@ -226,14 +164,14 @@ "questionBubble": { "shortcuts": "Phím tắt", "whatsNew": "Có gì mới?", + "help": "Trợ giúp & Hỗ trợ", "markdown": "Markdown", "debug": { "name": "Thông tin gỡ lỗi", "success": "Đã sao chép thông tin gỡ lỗi vào khay nhớ tạm!", "fail": "Không thể sao chép thông tin gỡ lỗi vào khay nhớ tạm" }, - "feedback": "Nhận xét", - "help": "Trợ giúp & Hỗ trợ" + "feedback": "Nhận xét" }, "menuAppHeader": { "moreButtonToolTip": "Xóa, đổi tên và hơn thế nữa...", @@ -252,8 +190,6 @@ "numList": "Danh sách đánh số", "bulletList": "Danh sách có dấu đầu dòng", "checkList": "Danh mục", - "inlineCode": "Mã nội tuyến", - "quote": "Khối trích dẫn", "header": "Tiêu đề", "highlight": "Điểm nổi bật", "color": "Màu", @@ -269,8 +205,7 @@ "dragRow": "Nhấn và giữ để sắp xếp lại hàng", "viewDataBase": "Xem cơ sở dữ liệu", "referencePage": "{name} này được tham chiếu", - "addBlockBelow": "Thêm một khối bên dưới", - "aiGenerate": "Tạo" + "addBlockBelow": "Thêm một khối bên dưới" }, "sideBar": { "closeSidebar": "Đóng thanh bên", @@ -279,46 +214,10 @@ "private": "Riêng tư", "workspace": "Không gian làm việc", "favorites": "Yêu thích", - "clickToHidePrivate": "Nhấp để ẩn không gian riêng tư\nCác trang bạn tạo ở đây chỉ hiển thị với bạn", - "clickToHideWorkspace": "Nhấp để ẩn không gian làm việc\nCác trang bạn tạo ở đây có thể được mọi thành viên nhìn thấy", "clickToHidePersonal": "Bấm để ẩn mục riêng tư", "clickToHideFavorites": "Bấm để ẩn mục yêu thích", "addAPage": "Thêm một trang", - "addAPageToPrivate": "Thêm một trang vào không gian riêng tư", - "addAPageToWorkspace": "Thêm một trang vào không gian làm việc", - "recent": "Gần đây", - "today": "Hôm nay", - "thisWeek": "Tuần này", - "others": "Yêu thích trước đó", - "justNow": "ngay bây giờ", - "minutesAgo": "{count} phút trước", - "lastViewed": "Đã xem lần cuối", - "favoriteAt": "Đã yêu thích", - "emptyRecent": "Không có tài liệu gần đây", - "emptyRecentDescription": "Khi bạn xem tài liệu, chúng sẽ xuất hiện ở đây để dễ dàng truy xuất", - "emptyFavorite": "Không có tài liệu yêu thích", - "emptyFavoriteDescription": "Bắt đầu khám phá và đánh dấu tài liệu là mục yêu thích. Chúng sẽ được liệt kê ở đây để truy cập nhanh!", - "removePageFromRecent": "Xóa trang này khỏi mục Gần đây?", - "removeSuccess": "Đã xóa thành công", - "favoriteSpace": "Yêu thích", - "RecentSpace": "Gần đây", - "Spaces": "Khoảng cách", - "upgradeToPro": "Nâng cấp lên Pro", - "upgradeToAIMax": "Mở khóa AI không giới hạn", - "storageLimitDialogTitle": "Bạn đã hết dung lượng lưu trữ miễn phí. Nâng cấp để mở khóa dung lượng lưu trữ không giới hạn", - "storageLimitDialogTitleIOS": "Bạn đã hết dung lượng lưu trữ miễn phí.", - "aiResponseLimitTitle": "Bạn đã hết phản hồi AI miễn phí. Nâng cấp lên Gói Pro hoặc mua tiện ích bổ sung AI để mở khóa phản hồi không giới hạn", - "aiResponseLimitDialogTitle": "Đã đạt đến giới hạn sử dụng AI", - "aiResponseLimit": "Bạn đã hết lượt dùng AI miễn phí.\nVào Cài đặt -> Gói đăng ký -> Nhấp vào AI Max hoặc Gói Pro để có thêm lượt dùng AI", - "askOwnerToUpgradeToPro": "Không gian làm việc của bạn sắp hết dung lượng lưu trữ miễn phí. Vui lòng yêu cầu chủ sở hữu không gian làm việc của bạn nâng cấp lên Gói Pro", - "askOwnerToUpgradeToProIOS": "Không gian làm việc của bạn sắp hết dung lượng lưu trữ miễn phí.", - "askOwnerToUpgradeToAIMax": "Không gian làm việc của bạn sắp hết phản hồi AI miễn phí. Vui lòng yêu cầu chủ sở hữu không gian làm việc của bạn nâng cấp gói hoặc mua tiện ích bổ sung AI", - "askOwnerToUpgradeToAIMaxIOS": "Không gian làm việc của bạn sắp hết lượt sử dụng AI miễn phí.", - "purchaseStorageSpace": "Mua không gian lưu trữ", - "purchaseAIResponse": "Mua ", - "askOwnerToUpgradeToLocalAI": "Yêu cầu chủ sở hữu không gian làm việc bật AI trên thiết bị", - "upgradeToAILocal": "Chạy các mô hình cục bộ trên thiết bị của bạn để có quyền riêng tư tối đa", - "upgradeToAILocalDesc": "Trò chuyện với PDF, cải thiện khả năng viết và tự động điền bảng bằng AI cục bộ" + "recent": "Gần đây" }, "notifications": { "export": { @@ -334,7 +233,6 @@ }, "button": { "ok": "OK", - "confirm": "Xác nhận", "done": "Xong", "cancel": "Hủy", "signIn": "Đăng nhập", @@ -357,35 +255,19 @@ "update": "Cập nhật", "share": "Chia sẻ", "removeFromFavorites": "Loại bỏ khỏi mục yêu thích", - "removeFromRecent": "Xóa khỏi gần đây", "addToFavorites": "Thêm vào mục yêu thích", - "favoriteSuccessfully": "Đã thêm vào yêu thích", - "unfavoriteSuccessfully": "Đã xoá khỏi yêu thích", - "duplicateSuccessfully": "Đã sao chép thành công", "rename": "Đổi tên", "helpCenter": "Trung tâm trợ giúp", "add": "Thêm", "yes": "Đúng", - "no": "Không", - "clear": "Xoá", - "remove": "Di chuyển", - "dontRemove": "Không xóa", "copyLink": "Sao chép đường dẫn", "align": "Căn chỉnh", "login": "Đăng nhập", "logout": "Đăng xuất", "deleteAccount": "Xóa tài khoản", - "back": "Quay lại", "signInGoogle": "Đăng nhập bằng Google", "signInGithub": "Đăng nhập bằng Github", "signInDiscord": "Đăng nhập bằng Discord", - "more": "Hơn nữa", - "create": "Tạo mới", - "close": "Đóng", - "next": "Kế tiếp", - "previous": "Trước đó", - "submit": "Gửi", - "download": "Tải về", "tryAGain": "Thử lại" }, "label": { @@ -410,520 +292,6 @@ }, "settings": { "title": "Cài đặt", - "popupMenuItem": { - "settings": "Cài đặt", - "members": "Thành viên", - "trash": "Thùng Rác", - "helpAndSupport": "Trợ giúp & Hỗ trợ" - }, - "accountPage": { - "menuLabel": "Tài khoản của tôi", - "title": "Tài khoản của tôi", - "general": { - "title": "Tên tài khoản & ảnh đại diện", - "changeProfilePicture": "Thay đổi ảnh đại diện" - }, - "email": { - "title": "E-mail", - "actions": { - "change": "Thay đổi email" - } - }, - "login": { - "title": "Đăng nhập tài khoản", - "loginLabel": "Đăng nhập", - "logoutLabel": "Đăng xuất" - } - }, - "workspacePage": { - "menuLabel": "Không gian làm việc", - "title": "Không gian làm việc", - "description": "Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, định dạng ngày/giờ và ngôn ngữ.", - "workspaceName": { - "title": "Tên không gian làm việc" - }, - "workspaceIcon": { - "title": "Biểu tượng không gian làm việc", - "description": "Tải lên hình ảnh hoặc sử dụng biểu tượng cảm xúc cho không gian làm việc của bạn. Biểu tượng sẽ hiển thị trên thanh bên và thông báo của bạn." - }, - "appearance": { - "title": "Vẻ bề ngoài", - "description": "Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, ngày, giờ và ngôn ngữ.", - "options": { - "system": "Tự động", - "light": "Sáng", - "dark": "Tối" - } - }, - "resetCursorColor": { - "title": "Đặt lại màu con trỏ tài liệu", - "description": "Bạn có chắc chắn muốn thiết lập lại màu con trỏ không?" - }, - "resetSelectionColor": { - "title": "Đặt lại màu lựa chọn tài liệu", - "description": "Bạn có chắc chắn muốn thiết lập lại màu đã chọn không?" - }, - "theme": { - "title": "Chủ đề", - "description": "Chọn chủ đề có sẵn hoặc tải lên chủ đề tùy chỉnh của riêng bạn.", - "uploadCustomThemeTooltip": "Tải lên một chủ đề tùy chỉnh" - }, - "workspaceFont": { - "title": "Phông chữ không gian làm việc", - "noFontHint": "Không tìm thấy phông chữ, hãy thử thuật ngữ khác." - }, - "textDirection": { - "title": "Hướng văn bản", - "leftToRight": "Từ trái sang phải", - "rightToLeft": "Từ phải sang trái", - "auto": "Tự động", - "enableRTLItems": "Bật các mục thanh công cụ RTL" - }, - "layoutDirection": { - "title": "Hướng bố trí", - "leftToRight": "Từ trái sang phải", - "rightToLeft": "Từ phải sang trái" - }, - "dateTime": { - "title": "Ngày & giờ", - "example": "{} tại {} ({})", - "24HourTime": "thời gian 24 giờ", - "dateFormat": { - "label": "Định dạng ngày tháng", - "local": "Địa phương", - "us": "US", - "iso": "ISO", - "friendly": "Thân thiện", - "dmy": "D/M/Y" - } - }, - "language": { - "title": "Ngôn ngữ" - }, - "deleteWorkspacePrompt": { - "title": "Xóa không gian làm việc", - "content": "Bạn có chắc chắn muốn xóa không gian làm việc này không? Hành động này không thể hoàn tác và bất kỳ trang nào bạn đã xuất bản sẽ bị hủy xuất bản." - }, - "leaveWorkspacePrompt": { - "title": "Rời khỏi không gian làm việc", - "content": "Bạn có chắc chắn muốn rời khỏi không gian làm việc này không? Bạn sẽ mất quyền truy cập vào tất cả các trang và dữ liệu trong đó." - }, - "manageWorkspace": { - "title": "Quản lý không gian làm việc", - "leaveWorkspace": "Rời khỏi không gian làm việc", - "deleteWorkspace": "Xóa không gian làm việc" - } - }, - "manageDataPage": { - "menuLabel": "Quản lý dữ liệu", - "title": "Quản lý dữ liệu", - "description": "Quản lý dữ liệu lưu trữ cục bộ hoặc Nhập dữ liệu hiện có của bạn vào @:appName .", - "dataStorage": { - "title": "Vị trí lưu trữ tập tin", - "tooltip": "Vị trí lưu trữ các tập tin của bạn", - "actions": { - "change": "Thay đổi đường dẫn", - "open": "Mở thư mục", - "openTooltip": "Mở vị trí thư mục dữ liệu hiện tại", - "copy": "Sao chép đường dẫn", - "copiedHint": "Đã sao chép đường dẫn!", - "resetTooltip": "Đặt lại về vị trí mặc định" - }, - "resetDialog": { - "title": "Bạn có chắc không?", - "description": "Đặt lại đường dẫn đến vị trí dữ liệu mặc định sẽ không xóa dữ liệu của bạn. Nếu bạn muốn nhập lại dữ liệu hiện tại, trước tiên bạn nên sao chép đường dẫn đến vị trí hiện tại của mình." - } - }, - "importData": { - "title": "Nhập dữ liệu", - "tooltip": "Nhập dữ liệu từ các thư mục sao lưu/dữ liệu @:appName", - "description": "Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài", - "action": "Duyệt tập tin" - }, - "encryption": { - "title": "Mã hóa", - "tooltip": "Quản lý cách dữ liệu của bạn được lưu trữ và mã hóa", - "descriptionNoEncryption": "Bật mã hóa sẽ mã hóa toàn bộ dữ liệu. Không thể hoàn tác thao tác này.", - "descriptionEncrypted": "Dữ liệu của bạn được mã hóa.", - "action": "Mã hóa dữ liệu", - "dialog": { - "title": "Mã hóa toàn bộ dữ liệu của bạn?", - "description": "Mã hóa tất cả dữ liệu của bạn sẽ giữ cho dữ liệu của bạn an toàn và bảo mật. Hành động này KHÔNG THỂ hoàn tác. Bạn có chắc chắn muốn tiếp tục không?" - } - }, - "cache": { - "title": "Xóa bộ nhớ đệm", - "description": "Giúp giải quyết các vấn đề như hình ảnh không tải được, thiếu trang trong một khoảng trắng và phông chữ không tải được. Điều này sẽ không ảnh hưởng đến dữ liệu của bạn.", - "dialog": { - "title": "Xóa bộ nhớ đệm", - "description": "Giúp giải quyết các vấn đề như hình ảnh không tải được, thiếu trang trong một khoảng trắng và phông chữ không tải được. Điều này sẽ không ảnh hưởng đến dữ liệu của bạn.", - "successHint": "Đã xóa bộ nhớ đệm!" - } - }, - "data": { - "fixYourData": "Sửa dữ liệu của bạn", - "fixButton": "Sửa", - "fixYourDataDescription": "Nếu bạn gặp sự cố với dữ liệu, bạn có thể thử khắc phục tại đây." - } - }, - "shortcutsPage": { - "menuLabel": "Phím tắt", - "title": "Phím tắt", - "editBindingHint": "Nhập phím tắt mới", - "searchHint": "Tìm kiếm", - "actions": { - "resetDefault": "Đặt lại mặc định" - }, - "errorPage": { - "message": "Không tải được phím tắt: {}", - "howToFix": "Vui lòng thử lại. Nếu sự cố vẫn tiếp diễn, vui lòng liên hệ trên GitHub." - }, - "resetDialog": { - "title": "Đặt lại phím tắt", - "description": "Thao tác này sẽ khôi phục tất cả các phím tắt của bạn về mặc định, bạn không thể hoàn tác thao tác này sau đó, bạn có chắc chắn muốn tiếp tục không?", - "buttonLabel": "Cài lại" - }, - "conflictDialog": { - "title": "{} hiện đang được sử dụng", - "descriptionPrefix": "Phím tắt này hiện đang được sử dụng bởi ", - "descriptionSuffix": ". Nếu bạn thay thế phím tắt này, nó sẽ bị xóa khỏi {}.", - "confirmLabel": "Tiếp tục" - }, - "editTooltip": "Nhấn để bắt đầu chỉnh sửa phím tắt", - "keybindings": { - "toggleToDoList": "Chuyển sang danh sách việc cần làm", - "insertNewParagraphInCodeblock": "Chèn đoạn văn mới", - "pasteInCodeblock": "Dán vào codeblock", - "selectAllCodeblock": "Chọn tất cả", - "indentLineCodeblock": "Chèn hai khoảng trắng vào đầu dòng", - "outdentLineCodeblock": "Xóa hai khoảng trắng ở đầu dòng", - "twoSpacesCursorCodeblock": "Chèn hai khoảng trắng vào con trỏ", - "copy": "Sao chép lựa chọn", - "paste": "Dán vào nội dung", - "cut": "Cắt lựa chọn", - "alignLeft": "Căn chỉnh văn bản sang trái", - "alignCenter": "Căn giữa văn bản", - "alignRight": "Căn chỉnh văn bản bên phải", - "undo": "Hoàn tác", - "redo": "Làm lại", - "convertToParagraph": "Chuyển đổi khối thành đoạn văn", - "backspace": "Xóa bỏ", - "deleteLeftWord": "Xóa từ bên trái", - "deleteLeftSentence": "Xóa câu bên trái", - "delete": "Xóa ký tự bên phải", - "deleteMacOS": "Xóa ký tự bên trái", - "deleteRightWord": "Xóa từ bên phải", - "moveCursorLeft": "Di chuyển con trỏ sang trái", - "moveCursorBeginning": "Di chuyển con trỏ đến đầu", - "moveCursorLeftWord": "Di chuyển con trỏ sang trái một từ", - "moveCursorLeftSelect": "Chọn và di chuyển con trỏ sang trái", - "moveCursorBeginSelect": "Chọn và di chuyển con trỏ đến đầu", - "moveCursorLeftWordSelect": "Chọn và di chuyển con trỏ sang trái một từ", - "moveCursorRight": "Di chuyển con trỏ sang phải", - "moveCursorEnd": "Di chuyển con trỏ đến cuối", - "moveCursorRightWord": "Di chuyển con trỏ sang phải một từ", - "moveCursorRightSelect": "Chọn và di chuyển con trỏ sang phải một", - "moveCursorEndSelect": "Chọn và di chuyển con trỏ đến cuối", - "moveCursorRightWordSelect": "Chọn và di chuyển con trỏ sang phải một từ", - "moveCursorUp": "Di chuyển con trỏ lên", - "moveCursorTopSelect": "Chọn và di chuyển con trỏ lên trên cùng", - "moveCursorTop": "Di chuyển con trỏ lên trên cùng", - "moveCursorUpSelect": "Chọn và di chuyển con trỏ lên", - "moveCursorBottomSelect": "Chọn và di chuyển con trỏ xuống dưới cùng", - "moveCursorBottom": "Di chuyển con trỏ xuống dưới cùng", - "moveCursorDown": "Di chuyển con trỏ xuống", - "moveCursorDownSelect": "Chọn và di chuyển con trỏ xuống", - "home": "Cuộn lên đầu trang", - "end": "Cuộn xuống dưới cùng", - "toggleBold": "Chuyển đổi chữ đậm", - "toggleItalic": "Chuyển đổi nghiêng", - "toggleUnderline": "Chuyển đổi gạch chân", - "toggleStrikethrough": "Chuyển đổi gạch ngang", - "toggleCode": "Chuyển đổi mã trong dòng", - "toggleHighlight": "Chuyển đổi nổi bật", - "showLinkMenu": "Hiển thị menu liên kết", - "openInlineLink": "Mở liên kết nội tuyến", - "openLinks": "Mở tất cả các liên kết đã chọn", - "indent": "thụt lề", - "outdent": "Nhô ra ngoài", - "exit": "Thoát khỏi chỉnh sửa", - "pageUp": "Cuộn lên một trang", - "pageDown": "Cuộn xuống một trang", - "selectAll": "Chọn tất cả", - "pasteWithoutFormatting": "Dán nội dung không định dạng", - "showEmojiPicker": "Hiển thị bộ chọn biểu tượng cảm xúc", - "enterInTableCell": "Thêm ngắt dòng trong bảng", - "leftInTableCell": "Di chuyển sang trái một ô trong bảng", - "rightInTableCell": "Di chuyển sang phải một ô trong bảng", - "upInTableCell": "Di chuyển lên một ô trong bảng", - "downInTableCell": "Di chuyển xuống một ô trong bảng", - "tabInTableCell": "Đi tới ô có sẵn tiếp theo trong bảng", - "shiftTabInTableCell": "Đi đến ô có sẵn trước đó trong bảng", - "backSpaceInTableCell": "Dừng lại ở đầu ô" - }, - "commands": { - "codeBlockNewParagraph": "Chèn một đoạn văn mới bên cạnh khối mã", - "codeBlockIndentLines": "Chèn hai khoảng trắng vào đầu dòng trong khối mã", - "codeBlockOutdentLines": "Xóa hai khoảng trắng ở đầu dòng trong khối mã", - "codeBlockAddTwoSpaces": "Chèn hai khoảng trắng vào vị trí con trỏ trong khối mã", - "codeBlockSelectAll": "Chọn tất cả nội dung bên trong một khối mã", - "codeBlockPasteText": "Dán văn bản vào codeblock", - "textAlignLeft": "Căn chỉnh văn bản sang trái", - "textAlignCenter": "Căn chỉnh văn bản vào giữa", - "textAlignRight": "Căn chỉnh văn bản sang phải" - }, - "couldNotLoadErrorMsg": "Không thể tải phím tắt, hãy thử lại", - "couldNotSaveErrorMsg": "Không thể lưu phím tắt, hãy thử lại" - }, - "aiPage": { - "title": "Cài đặt AI", - "menuLabel": "Cài đặt AI", - "keys": { - "enableAISearchTitle": "Tìm kiếm AI", - "aiSettingsDescription": "Chọn mô hình ưa thích của bạn để hỗ trợ AppFlowy AI. Bây giờ bao gồm GPT 4-o, Claude 3,5, Llama 3.1 và Mistral 7B", - "loginToEnableAIFeature": "Các tính năng AI chỉ được bật sau khi đăng nhập bằng @:appName Cloud. Nếu bạn không có tài khoản @:appName , hãy vào 'Tài khoản của tôi' để đăng ký", - "llmModel": "Mô hình ngôn ngữ", - "llmModelType": "Kiểu mô hình ngôn ngữ", - "downloadLLMPrompt": "Tải xuống {}", - "downloadAppFlowyOfflineAI": "Tải xuống gói AI ngoại tuyến sẽ cho phép AI chạy trên thiết bị của bạn. Bạn có muốn tiếp tục không?", - "downloadLLMPromptDetail": "Tải xuống mô hình cục bộ {} sẽ chiếm tới {} dung lượng lưu trữ. Bạn có muốn tiếp tục không?", - "downloadBigFilePrompt": "Có thể mất khoảng 10 phút để hoàn tất việc tải xuống", - "downloadAIModelButton": "Tải về", - "downloadingModel": "Đang tải xuống", - "localAILoaded": "Mô hình AI cục bộ đã được thêm thành công và sẵn sàng sử dụng", - "localAIStart": "Trò chuyện AI cục bộ đang bắt đầu...", - "localAILoading": "Mô hình trò chuyện AI cục bộ đang tải...", - "localAIStopped": "AI cục bộ đã dừng", - "failToLoadLocalAI": "Không thể khởi động AI cục bộ", - "restartLocalAI": "Khởi động lại AI cục bộ", - "disableLocalAITitle": "Vô hiệu hóa AI cục bộ", - "disableLocalAIDescription": "Bạn có muốn tắt AI cục bộ không?", - "localAIToggleTitle": "Chuyển đổi để bật hoặc tắt AI cục bộ", - "offlineAIInstruction1": "Theo dõi", - "offlineAIInstruction2": "chỉ dẫn", - "offlineAIInstruction3": "để kích hoạt AI ngoại tuyến.", - "offlineAIDownload1": "Nếu bạn chưa tải xuống AppFlowy AI, vui lòng", - "offlineAIDownload2": "tải về", - "offlineAIDownload3": "nó đầu tiên", - "activeOfflineAI": "Tích cực", - "downloadOfflineAI": "Tải về", - "openModelDirectory": "Mở thư mục" - } - }, - "planPage": { - "menuLabel": "Kế hoạch", - "title": "Giá gói", - "planUsage": { - "title": "Tóm tắt sử dụng kế hoạch", - "storageLabel": "Kho", - "storageUsage": "{} của {} GB", - "unlimitedStorageLabel": "Lưu trữ không giới hạn", - "collaboratorsLabel": "Thành viên", - "collaboratorsUsage": "{} của {}", - "aiResponseLabel": "Phản hồi của AI", - "aiResponseUsage": "{} của {}", - "unlimitedAILabel": "Phản hồi không giới hạn", - "proBadge": "Chuyên nghiệp", - "aiMaxBadge": "AI Tối đa", - "aiOnDeviceBadge": "AI trên thiết bị dành cho máy Mac", - "memberProToggle": "Thêm thành viên và AI không giới hạn", - "aiMaxToggle": "AI không giới hạn và quyền truy cập vào các mô hình tiên tiến", - "aiOnDeviceToggle": "AI cục bộ cho sự riêng tư tối đa", - "aiCredit": { - "title": "Thêm @:appName Tín dụng AI", - "price": "{}", - "priceDescription": "cho 1.000 tín dụng", - "purchase": "Mua AI", - "info": "Thêm 1.000 tín dụng Ai cho mỗi không gian làm việc và tích hợp AI tùy chỉnh vào quy trình làm việc của bạn một cách liền mạch để có kết quả thông minh hơn, nhanh hơn với tối đa:", - "infoItemOne": "10.000 phản hồi cho mỗi cơ sở dữ liệu", - "infoItemTwo": "1.000 phản hồi cho mỗi không gian làm việc" - }, - "currentPlan": { - "bannerLabel": "Gói hiện tại", - "freeTitle": "Miễn phí", - "proTitle": "Pro", - "teamTitle": "Nhóm", - "freeInfo": "Hoàn hảo cho cá nhân có tối đa 2 thành viên để sắp xếp mọi thứ", - "proInfo": "Hoàn hảo cho các nhóm vừa và nhỏ có tối đa 10 thành viên.", - "teamInfo": "Hoàn hảo cho tất cả các nhóm làm việc hiệu quả và có tổ chức tốt.", - "upgrade": "Thay đổi gói đăng ký", - "canceledInfo": "Gói đăng ký của bạn đã bị hủy, bạn sẽ được hạ cấp xuống gói Miễn phí vào ngày {}." - }, - "addons": { - "title": "Tiện ích bổ sung", - "addLabel": "Thêm vào", - "activeLabel": "Đã thêm", - "aiMax": { - "title": "AI Max", - "description": "Phản hồi AI không giới hạn được hỗ trợ bởi GPT-4o, Claude 3.5 Sonnet và nhiều hơn nữa", - "price": "{}", - "priceInfo": "cho mỗi người dùng mỗi tháng được thanh toán hàng năm" - }, - "aiOnDevice": { - "title": "AI trên thiết bị dành cho máy Mac", - "description": "Chạy Mistral 7B, LLAMA 3 và nhiều mô hình cục bộ khác trên máy của bạn", - "price": "{}", - "priceInfo": "cho mỗi người dùng mỗi tháng được thanh toán hàng năm", - "recommend": "Khuyến nghị M1 hoặc mới hơn" - } - }, - "deal": { - "bannerLabel": "Khuyến mãi năm mới!", - "title": "Phát triển nhóm của bạn!", - "info": "Nâng cấp và tiết kiệm 10% cho gói Pro và Team! Tăng năng suất làm việc của bạn với các tính năng mới mạnh mẽ bao gồm @:appName AI.", - "viewPlans": "Xem các gói đăng ký" - } - } - }, - "billingPage": { - "menuLabel": "Thanh toán", - "title": "Thanh toán", - "plan": { - "title": "Gói đăng ký", - "freeLabel": "Miễn phí", - "proLabel": "Pro", - "planButtonLabel": "Thay đổi gói đăng ký", - "billingPeriod": "Chu kỳ thanh toán", - "periodButtonLabel": "Chỉnh sửa chu kỳ" - }, - "paymentDetails": { - "title": "Chi tiết thanh toán", - "methodLabel": "Phương thức thanh toán", - "methodButtonLabel": "Phương pháp chỉnh sửa" - }, - "addons": { - "title": "Tiện ích bổ sung", - "addLabel": "Thêm vào", - "removeLabel": "Xoá", - "renewLabel": "Làm mới", - "aiMax": { - "label": "AI Max", - "description": "Mở khóa AI không giới hạn và các mô hình tiên tiến", - "activeDescription": "Hóa đơn tiếp theo phải trả vào ngày {}", - "canceledDescription": "AI Max sẽ có sẵn cho đến {}" - }, - "aiOnDevice": { - "label": "AI trên thiết bị dành cho máy Mac", - "description": "Mở khóa AI không giới hạn trên thiết bị của bạn", - "activeDescription": "Hóa đơn tiếp theo phải trả vào ngày {}", - "canceledDescription": "AI On-device dành cho Mac sẽ khả dụng cho đến {}" - }, - "removeDialog": { - "title": "Xoá {}", - "description": "Bạn có chắc chắn muốn xóa {plan} không? Bạn sẽ mất quyền truy cập vào các tính năng và lợi ích của {plan} ngay lập tức." - } - }, - "currentPeriodBadge": "HIỆN TẠI", - "changePeriod": "Thay đổi chu kỳ", - "planPeriod": "{} chu kỳ", - "monthlyInterval": "Hàng tháng", - "monthlyPriceInfo": "mỗi thành viên được thanh toán hàng tháng", - "annualInterval": "Hàng năm", - "annualPriceInfo": "mỗi thành viên được thanh toán hàng năm" - }, - "comparePlanDialog": { - "title": "So sánh và lựa chọn gói đăng ký", - "planFeatures": "Gói đăng ký\nTính năng", - "current": "Hiện tại", - "actions": { - "upgrade": "Nâng cấp", - "downgrade": "Hạ cấp", - "current": "Hiện tại" - }, - "freePlan": { - "title": "Miễn phí", - "description": "Dành cho cá nhân có tối đa 2 thành viên để tổ chức mọi thứ", - "price": "{}", - "priceInfo": "miễn phí mãi mãi" - }, - "proPlan": { - "title": "Pro", - "description": "Dành cho các nhóm nhỏ để quản lý dự án và kiến thức của nhóm", - "price": "{}", - "priceInfo": "cho mỗi người dùng mỗi tháng\nđược thanh toán hàng năm\n\n{} được thanh toán hàng tháng" - }, - "planLabels": { - "itemOne": "Không gian làm việc", - "itemTwo": "Thành viên", - "itemThree": "Kho", - "itemFour": "Chỉnh sửa thời gian thực", - "itemFive": "Ứng dụng di động", - "itemSix": "Phản hồi của AI", - "itemFileUpload": "Tải tập tin lên", - "tooltipSix": "Trọn đời có nghĩa là số lượng phản hồi không bao giờ được thiết lập lại", - "intelligentSearch": "Tìm kiếm thông minh", - "tooltipSeven": "Cho phép bạn tùy chỉnh một phần URL cho không gian làm việc của bạn" - }, - "freeLabels": { - "itemOne": "tính phí theo không gian làm việc", - "itemTwo": "lên đến 2", - "itemThree": "5 GB", - "itemFour": "Đúng", - "itemFive": "Đúng", - "itemSix": "10 trọn đời", - "itemFileUpload": "Lên đến 7 MB", - "intelligentSearch": "Tìm kiếm thông minh" - }, - "proLabels": { - "itemOne": "tính phí theo không gian làm việc", - "itemTwo": "lên đến 10", - "itemThree": "không giới hạn", - "itemFour": "Đúng", - "itemFive": "Đúng", - "itemSix": "không giới hạn", - "itemFileUpload": "Không giới hạn", - "intelligentSearch": "Tìm kiếm thông minh" - }, - "paymentSuccess": { - "title": "Bạn hiện đang sử dụng gói {}!", - "description": "Thanh toán của bạn đã được xử lý thành công và gói của bạn đã được nâng cấp lên @:appName {}. Bạn có thể xem chi tiết gói đăng ký của mình trên trang Gói đăng ký" - }, - "downgradeDialog": { - "title": "Bạn có chắc chắn muốn hạ cấp gói đăng ký của mình không?", - "description": "Việc hạ cấp gói đăng ký của bạn sẽ đưa bạn trở lại gói Miễn phí. Các thành viên có thể mất quyền truy cập vào không gian làm việc này và bạn có thể cần giải phóng dung lượng để đáp ứng giới hạn lưu trữ của gói Miễn phí.", - "downgradeLabel": "Hạ cấp gói đăng ký" - } - }, - "cancelSurveyDialog": { - "title": "Rất tiếc khi phải tạm biệt bạn", - "description": "Chúng tôi rất tiếc khi phải tạm biệt bạn. Chúng tôi rất muốn nghe phản hồi của bạn để giúp chúng tôi cải thiện @:appName . Vui lòng dành chút thời gian để trả lời một vài câu hỏi.", - "commonOther": "Khác", - "otherHint": "Viết câu trả lời của bạn ở đây", - "questionOne": { - "question": "Điều gì khiến bạn hủy đăng ký @:appName Pro?", - "answerOne": "Chi phí quá cao", - "answerTwo": "Các tính năng không đáp ứng được kỳ vọng", - "answerThree": "Đã tìm thấy một giải pháp thay thế tốt hơn", - "answerFour": "Không sử dụng đủ để bù đắp chi phí", - "answerFive": "Vấn đề dịch vụ hoặc khó khăn kỹ thuật" - }, - "questionTwo": { - "question": "Bạn có khả năng cân nhắc đăng ký lại @:appName Pro trong tương lai không?", - "answerOne": "Rất có thể", - "answerTwo": "Có khả năng", - "answerThree": "Không chắc chắn", - "answerFour": "Không có khả năng", - "answerFive": "Rất không có khả năng" - }, - "questionThree": { - "question": "Bạn đánh giá cao tính năng Pro nào nhất trong suốt thời gian đăng ký?", - "answerOne": "Sự hợp tác của nhiều người dùng", - "answerTwo": "Lịch sử phiên bản thời gian dài hơn", - "answerThree": "Phản hồi AI không giới hạn", - "answerFour": "Truy cập vào các mô hình AI cục bộ" - }, - "questionFour": { - "question": "Bạn sẽ mô tả trải nghiệm chung của bạn với @:appName như thế nào?", - "answerOne": "Tuyệt", - "answerTwo": "Tốt", - "answerThree": "Trung bình", - "answerFour": "Dưới mức trung bình", - "answerFive": "Không hài lòng" - } - }, - "common": { - "reset": "Đặt lại" - }, "menu": { "appearance": "Giao diện", "language": "Ngôn ngữ", @@ -934,7 +302,7 @@ "logout": "Đăng xuất", "logoutPrompt": "Bạn chắc chắn muốn đăng xuất?", "selfEncryptionLogoutPrompt": "Bạn có chắc chắn bạn muốn thoát? Hãy đảm bảo bạn đã sao chép bí mật mã hóa", - "syncSetting": "Cài đặt đồng bộ", + "syncSetting": "Đồng bộ cài đặt", "cloudSettings": "Cài đặt đám mây", "enableSync": "Bật tính năng đồng bộ", "enableEncrypt": "Mã hoá dữ liệu", @@ -942,10 +310,11 @@ "invalidCloudURLScheme": "Scheme không hợp lệ", "cloudServerType": "Máy chủ đám mây", "cloudServerTypeTip": "Xin lưu ý rằng có thể bạn sẽ bị đăng xuất sau khi chuyển máy chủ đám mây", - "cloudLocal": "Cục bộ", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Anon key không được để trống nếu url supabase không trống", "cloudAppFlowy": "@:appName Cloud Beta", - "cloudAppFlowySelfHost": "@:appName Cloud Tự lưu trữ", - "appFlowyCloudUrlCanNotBeEmpty": "URL đám mây không được để trống", "clickToCopy": "Bấm để sao chép", "selfHostStart": "Nếu bạn không có máy chủ, vui lòng tham khảo", "selfHostContent": "tài liệu", @@ -955,7 +324,6 @@ "cloudWSURLHint": "Nhập địa chỉ websocket của máy chủ của bạn", "restartApp": "Khởi động lại", "restartAppTip": "Khởi động lại ứng dụng để thay đổi có hiệu lực. Xin lưu ý rằng điều này có thể đăng xuất tài khoản hiện tại của bạn", - "changeServerTip": "Sau khi thay đổi máy chủ, bạn phải nhấp vào nút khởi động lại để những thay đổi có hiệu lực", "enableEncryptPrompt": "Kích hoạt mã hóa để bảo mật dữ liệu của bạn với bí mật này. Lưu trữ nó một cách an toàn; một khi đã bật thì không thể tắt được. Nếu bị mất, dữ liệu của bạn sẽ không thể phục hồi được. Bấm để sao chép", "inputEncryptPrompt": "Vui lòng nhập bí mật mã hóa của bạn cho", "clickToCopySecret": "Bấm để sao chép bí mật", @@ -963,9 +331,7 @@ "configServerGuide": "Sau khi chọn `Bắt đầu nhanh`, điều hướng đến `Cài đặt` rồi đến \"Cài đặt đám mây\" để định cấu hình máy chủ tự lưu trữ của bạn.", "inputTextFieldHint": "Bí mật của bạn", "historicalUserList": "Lịch sử đăng nhập", - "historicalUserListTooltip": "Danh sách này hiển thị các tài khoản ẩn danh của bạn. Bạn có thể nhấp vào một tài khoản để xem thông tin chi tiết của tài khoản đó. Các tài khoản ẩn danh được tạo bằng cách nhấp vào nút 'Bắt đầu'", "openHistoricalUser": "Ấn để mở tài khoản ẩn danh", - "customPathPrompt": "Lưu trữ thư mục dữ liệu @:appName trong thư mục được đồng bộ hóa trên đám mây như Google Drive có thể gây ra rủi ro. Nếu cơ sở dữ liệu trong thư mục này được truy cập hoặc sửa đổi từ nhiều vị trí cùng một lúc, có thể dẫn đến xung đột đồng bộ hóa và hỏng dữ liệu tiềm ẩn", "importAppFlowyData": "Nhập dữ liệu từ thư mục @:appName bên ngoài", "importingAppFlowyDataTip": "Quá trình nhập dữ liệu đang diễn ra. Vui lòng không đóng ứng dụng", "importAppFlowyDataDescription": "Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu @:appName hiện tại", @@ -977,58 +343,13 @@ "enableNotifications": { "label": "Bật thông báo", "hint": "Tắt để ngăn thông báo xuất hiện." - }, - "showNotificationsIcon": { - "label": "Hiển thị biểu tượng thông báo", - "hint": "Tắt để ẩn biểu tượng thông báo ở thanh bên." - }, - "archiveNotifications": { - "allSuccess": "Đã lưu trữ tất cả thông báo thành công", - "success": "Đã lưu trữ thông báo thành công" - }, - "markAsReadNotifications": { - "allSuccess": "Đã đánh dấu tất cả là đã đọc thành công", - "success": "Đã đánh dấu là đã đọc thành công" - }, - "action": { - "markAsRead": "Đánh dấu là đã đọc", - "multipleChoice": "Chọn thêm", - "archive": "Lưu trữ" - }, - "settings": { - "settings": "Cài đặt", - "markAllAsRead": "Đánh dấu tất cả là đã đọc", - "archiveAll": "Lưu trữ tất cả" - }, - "emptyInbox": { - "title": "Chưa có thông báo nào", - "description": "Bạn sẽ được thông báo ở đây về @đề cập" - }, - "emptyUnread": { - "title": "Không có thông báo chưa đọc", - "description": "Bạn đã hiểu hết rồi!" - }, - "emptyArchived": { - "title": "Không có thông báo lưu trữ", - "description": "Bạn chưa lưu trữ bất kỳ thông báo nào" - }, - "tabs": { - "inbox": "Hộp thư đến", - "unread": "Chưa đọc", - "archived": "Đã lưu trữ" - }, - "refreshSuccess": "Thông báo đã được làm mới thành công", - "titles": { - "notifications": "Thông báo", - "reminder": "Lời nhắc nhở" } }, "appearance": { "resetSetting": "Đặt lại cài đặt", "fontFamily": { "label": "Phông chữ", - "search": "Tìm kiếm", - "defaultFont": "Hệ thống" + "search": "Tìm kiếm" }, "themeMode": { "label": "Chế độ Theme", @@ -1036,13 +357,9 @@ "dark": "Chế độ tối", "system": "Thích ứng với hệ thống" }, - "fontScaleFactor": "Hệ số tỷ lệ phông chữ", "documentSettings": { "cursorColor": "Màu con trỏ", "selectionColor": "Màu lựa chọn tài liệu", - "pickColor": "Chọn một màu", - "colorShade": "Màu sắc bóng râm", - "opacity": "Độ mờ đục", "hexEmptyError": "Màu hex không được để trống", "hexLengthError": "Giá trị hex phải dài 6 chữ số", "hexInvalidError": "Giá trị hex không hợp lệ", @@ -1069,16 +386,16 @@ "themeUpload": { "button": "Tải lên", "uploadTheme": "Tải theme lên", - "description": "Tải lên @:appName theme của riêng bạn bằng nút bên dưới.", + "description": "Tải lên AppFlowy theme của riêng bạn bằng nút bên dưới.", "loading": "Vui lòng đợi trong khi chúng tôi xác thực và tải theme của bạn lên...", "uploadSuccess": "Theme của bạn đã được tải lên thành công", "deletionFailure": "Không xóa được theme. Hãy thử xóa nó bằng tay.", "filePickerDialogTitle": "Chọn tệp .flowy_plugin", "urlUploadFailure": "Không mở được url: {}" }, - "theme": "Chủ đề", + "theme": "Theme", "builtInsLabel": "Theme có sẵn", - "pluginsLabel": "Các plugin", + "pluginsLabel": "Plugins", "dateFormat": { "label": "Định dạng ngày", "local": "Địa phương", @@ -1093,47 +410,19 @@ "twentyFourHour": "Hai mươi bốn tiếng" }, "showNamingDialogWhenCreatingPage": "Hiển thị hộp thoại đặt tên khi tạo trang", - "enableRTLToolbarItems": "Bật các mục thanh công cụ RTL", "members": { "title": "Cài đặt thành viên", "inviteMembers": "Mời thành viên", - "inviteHint": "Mời qua email", "sendInvite": "Gửi lời mời", "copyInviteLink": "Sao chép liên kết mời", "label": "Các thành viên", "user": "Người dùng", "role": "Vai trò", "removeFromWorkspace": "Xóa khỏi Workspace", - "removeFromWorkspaceSuccess": "Xóa khỏi không gian làm việc thành công", - "removeFromWorkspaceFailed": "Xóa khỏi không gian làm việc không thành công", "owner": "Người sở hữu", "guest": "Khách", "member": "Thành viên", - "memberHintText": "Một thành viên có thể đọc và chỉnh sửa các trang", - "guestHintText": "Khách có thể đọc, phản hồi, bình luận và chỉnh sửa một số trang nhất định khi được phép.", - "emailInvalidError": "Email không hợp lệ, vui lòng kiểm tra và thử lại", - "emailSent": "Email đã được gửi, vui lòng kiểm tra hộp thư đến", - "members": "các thành viên", - "membersCount": { - "zero": "{} thành viên", - "one": "{} thành viên", - "other": "{} thành viên" - }, - "inviteFailedDialogTitle": "Không gửi được lời mời", - "inviteFailedMemberLimit": "Đã đạt đến giới hạn thành viên, vui lòng nâng cấp để mời thêm thành viên.", - "inviteFailedMemberLimitMobile": "Không gian làm việc của bạn đã đạt đến giới hạn thành viên. Nâng cấp trên Desktop để mở khóa thêm nhiều tính năng.", - "memberLimitExceeded": "Đã đạt đến giới hạn thành viên, vui lòng mời thêm thành viên ", - "memberLimitExceededUpgrade": "nâng cấp", - "memberLimitExceededPro": "Đã đạt đến giới hạn thành viên, nếu bạn cần thêm thành viên hãy liên hệ ", - "memberLimitExceededProContact": "support@appflowy.io", - "failedToAddMember": "Không thêm được thành viên", - "addMemberSuccess": "Thành viên đã được thêm thành công", - "removeMember": "Xóa thành viên", - "areYouSureToRemoveMember": "Bạn có chắc chắn muốn xóa thành viên này không?", - "inviteMemberSuccess": "Lời mời đã được gửi thành công", - "failedToInviteMember": "Không mời được thành viên", - "workspaceMembersError": "Ồ, có gì đó không ổn", - "workspaceMembersErrorDescription": "Chúng tôi không thể tải danh sách thành viên vào lúc này. Vui lòng thử lại sau" + "members": "các thành viên" } }, "files": { @@ -1159,7 +448,6 @@ "locationDesc": "Chọn tên cho thư mục dữ liệu @:appName của bạn", "browser": "Duyệt", "create": "Tạo", - "set": "Bộ", "folderPath": "Đường dẫn lưu trữ thư mục của bạn", "locationCannotBeEmpty": "Đường dẫn không thể trống", "pathCopiedSnackbar": "Đã sao chép đường dẫn lưu trữ tệp vào khay nhớ tạm!", @@ -1170,26 +458,27 @@ "recoverLocationTooltips": "Đặt lại về thư mục dữ liệu mặc định của @:appName", "exportFileSuccess": "Xuất tập tin thành công!", "exportFileFail": "Xuất tập tin thất bại!", - "export": "Xuất", - "clearCache": "Xóa bộ nhớ đệm", - "clearCacheDesc": "Nếu bạn gặp sự cố với hình ảnh không tải hoặc phông chữ không hiển thị đúng, hãy thử xóa bộ nhớ đệm. Hành động này sẽ không xóa dữ liệu người dùng của bạn.", - "areYouSureToClearCache": "Bạn có chắc chắn muốn xóa bộ nhớ đệm không?", - "clearCacheSuccess": "Đã xóa bộ nhớ đệm thành công!" + "export": "Xuất" }, "user": { "name": "Tên", - "email": "E-mail", + "email": "Email", "tooltipSelectIcon": "Chọn biểu tượng", "selectAnIcon": "Chọn một biểu tượng", - "pleaseInputYourOpenAIKey": "vui lòng nhập khóa AI của bạn", - "clickToLogout": "Nhấn để đăng xuất", - "pleaseInputYourStabilityAIKey": "vui lòng nhập khóa Stability AI của bạn" + "pleaseInputYourOpenAIKey": "vui lòng nhập khóa OpenAI của bạn", + "pleaseInputYourStabilityAIKey": "vui lòng nhập khóa Stability AI của bạn", + "clickToLogout": "Nhấn để đăng xuất" + }, + "shortcuts": { + "shortcutsLabel": "Phím tắt", + "updateShortcutStep": "Nhấn tổ hợp phím mong muốn và nhấn ENTER", + "shortcutIsAlreadyUsed": "Phím tắt này đã được sử dụng cho: {conflict}" }, "mobile": { "personalInfo": "Thông tin cá nhân", "username": "Tên người dùng ", "usernameEmptyError": "Tên người dùng không được để trống", - "about": "Về", + "about": "About", "pushNotifications": "Thông báo", "support": "Ủng hộ", "joinDiscord": "Tham gia cùng chúng tôi trên Discord", @@ -1201,11 +490,6 @@ "selectLayout": "Chọn bố cục", "selectStartingDay": "Chọn ngày bắt đầu", "version": "Phiên bản" - }, - "shortcuts": { - "shortcutsLabel": "Phím tắt", - "updateShortcutStep": "Nhấn tổ hợp phím mong muốn và nhấn ENTER", - "shortcutIsAlreadyUsed": "Phím tắt này đã được sử dụng cho: {conflict}" } }, "grid": { @@ -1225,15 +509,12 @@ "deleteFilter": "Xóa bộ lọc", "filterBy": "Lọc bằng...", "typeAValue": "Nhập một giá trị...", - "layout": "Bố cục", - "databaseLayout": "Bố cục", + "layout": "Layout", + "databaseLayout": "Layout", "editView": "Chỉnh sửa chế độ xem", "boardSettings": "Cài đặt bảng", "calendarSettings": "Cài đặt lịch", - "createView": "Góc nhìn mới", - "duplicateView": "Xem trùng lặp", "deleteView": "Xóa chế độ xem", - "numberOfVisibleFields": "{} đã hiển thị", "Properties": "Thuộc tính", "viewList": "Database Views" }, @@ -1277,56 +558,25 @@ "is": "Là", "before": "Trước", "after": "Sau", - "onOrBefore": "Đang diễn ra hoặc trước đó", - "onOrAfter": "Đang bật hoặc sau", "between": "Ở giữa", "empty": "Rỗng", - "notEmpty": "Không rỗng", - "choicechipPrefix": { - "before": "Trước", - "after": "Sau đó", - "onOrBefore": "Vào hoặc trước", - "onOrAfter": "Vào hoặc sau", - "isEmpty": "Đang trống", - "isNotEmpty": "Không trống" - } - }, - "numberFilter": { - "equal": "Bằng nhau", - "notEqual": "Không bằng", - "lessThan": "Ít hơn", - "greaterThan": "Lớn hơn", - "lessThanOrEqualTo": "Nhỏ hơn hoặc bằng", - "greaterThanOrEqualTo": "Lớn hơn hoặc bằng", - "isEmpty": "Đang trống", - "isNotEmpty": "Không trống" + "notEmpty": "Không rỗng" }, "field": { - "label": "Thuộc tính", "hide": "Ẩn", "show": "Hiện", "insertLeft": "Chèn trái", "insertRight": "Chèn phải", "duplicate": "Nhân bản", "delete": "Xóa", - "wrapCellContent": "Bao quanh văn bản", - "clear": "Xóa tế bào", "textFieldName": "Chữ", "checkboxFieldName": "Hộp kiểm", "dateFieldName": "Ngày", "updatedAtFieldName": "Sửa đổi lần cuối", "createdAtFieldName": "Được tạo vào lúc", "numberFieldName": "Số", - "singleSelectFieldName": "Lựa chọn", - "multiSelectFieldName": "Chọn nhiều", "urlFieldName": "URL", "checklistFieldName": "Danh mục", - "relationFieldName": "Mối quan hệ", - "summaryFieldName": "Tóm tắt AI", - "timeFieldName": "Thời gian", - "mediaFieldName": "Tệp tin & phương tiện", - "translateFieldName": "AI Dịch", - "translateTo": "Dịch sang", "numberFormat": "Định dạng số", "dateFormat": "Định dạng ngày tháng", "includeTime": "Bao gồm thời gian", @@ -1355,13 +605,10 @@ "addOption": "Thêm tùy chọn", "editProperty": "Chỉnh sửa thuộc tính", "newProperty": "Thuộc tính mới", - "openRowDocument": "Mở như một trang", "deleteFieldPromptMessage": "Bạn có chắc không? Thuộc tính này sẽ bị xóa", - "clearFieldPromptMessage": "Bạn có chắc chắn không? Tất cả các ô trong cột này sẽ được làm trống", "newColumn": "Cột mới", "format": "Định dạng", - "reminderOnDateTooltip": "Ô này có lời nhắc được lên lịch", - "optionAlreadyExist": "Tùy chọn đã tồn tại" + "reminderOnDateTooltip": "Ô này có lời nhắc được lên lịch" }, "rowPage": { "newField": "Thêm một trường mới", @@ -1375,20 +622,13 @@ "one": "Ẩn {count} trường ẩn", "many": "Ẩn {count} trường ẩn", "other": "Ẩn {count} trường ẩn" - }, - "openAsFullPage": "Mở dưới dạng trang đầy đủ", - "moreRowActions": "Thêm hành động hàng" + } }, "sort": { "ascending": "Tăng dần", "descending": "Giảm dần", - "by": "Qua", - "empty": "Không có loại hoạt động", - "cannotFindCreatableField": "Không tìm thấy trường phù hợp để sắp xếp theo", "deleteAllSorts": "Xóa tất cả sắp xếp", - "addSort": "Thêm sắp xếp", - "removeSorting": "Bạn có muốn xóa chế độ sắp xếp không?", - "fieldInUse": "Bạn đang sắp xếp theo trường này" + "addSort": "Thêm sắp xếp" }, "row": { "duplicate": "Nhân bản", @@ -1399,14 +639,10 @@ "count": "Số lượng", "newRow": "Hàng mới", "action": "Hành động", - "add": "Nhấp vào thêm vào bên dưới", "drag": "Kéo để di chuyển", - "deleteRowPrompt": "Bạn có chắc chắn muốn xóa hàng này không? Hành động này không thể hoàn tác", - "deleteCardPrompt": "Bạn có chắc chắn muốn xóa thẻ này không? Hành động này không thể hoàn tác", "dragAndClick": "Kéo để di chuyển, nhấp để mở menu", "insertRecordAbove": "Chèn bản ghi ở trên", - "insertRecordBelow": "Chèn bản ghi bên dưới", - "noContent": "Không có nội dung" + "insertRecordBelow": "Chèn bản ghi bên dưới" }, "selectOption": { "create": "Tạo", @@ -1424,247 +660,33 @@ "panelTitle": "Chọn một tùy chọn hoặc tạo mới", "searchOption": "Tìm kiếm một lựa chọn", "searchOrCreateOption": "Tìm kiếm hoặc tạo một tùy chọn...", - "createNew": "Tạo một cái mới", - "orSelectOne": "Hoặc chọn một tùy chọn", - "typeANewOption": "Nhập một tùy chọn mới", "tagName": "Tên thẻ" }, - "checklist": { - "taskHint": "Mô tả nhiệm vụ", - "addNew": "Thêm một nhiệm vụ mới", - "submitNewTask": "Tạo nên", - "hideComplete": "Ẩn các tác vụ đã hoàn thành", - "showComplete": "Hiển thị tất cả các nhiệm vụ" - }, "url": { - "launch": "Mở liên kết trong trình duyệt", - "copy": "Sao chép URL", - "textFieldHint": "Nhập một URL" + "copy": "Sao chép URL" }, - "relation": { - "relatedDatabasePlaceLabel": "Cơ sở dữ liệu liên quan", - "relatedDatabasePlaceholder": "Không có", - "inRelatedDatabase": "TRONG", - "rowSearchTextFieldPlaceholder": "Tìm kiếm", - "noDatabaseSelected": "Chưa chọn cơ sở dữ liệu, vui lòng chọn một cơ sở dữ liệu trước từ danh sách bên dưới:", - "emptySearchResult": "Không tìm thấy hồ sơ nào", - "linkedRowListLabel": "{count} hàng được liên kết", - "unlinkedRowListLabel": "Liên kết một hàng khác" - }, - "menuName": "Lưới", - "referencedGridPrefix": "Xem của", - "calculate": "Tính toán", - "calculationTypeLabel": { - "none": "Không có", - "average": "Trung bình", - "max": "Tối đa", - "median": "Trung vị", - "min": "Tối thiểu", - "sum": "Tổng", - "count": "Đếm", - "countEmpty": "Đếm trống", - "countEmptyShort": "TRỐNG", - "countNonEmpty": "Đếm không trống", - "countNonEmptyShort": "ĐIỀN" - }, - "media": { - "rename": "Đổi tên", - "download": "Tải về", - "delete": "Xóa bỏ", - "moreFilesHint": "+{}", - "addFileOrImage": "Thêm tệp, hình ảnh hoặc liên kết", - "attachmentsHint": "{}", - "addFileMobile": "Thêm tập tin", - "deleteFileDescription": "Bạn có chắc chắn muốn xóa tệp này không? Hành động này không thể hoàn tác.", - "downloadSuccess": "Đã lưu tập tin thành công", - "downloadFailedToken": "Không tải được tệp, mã thông báo người dùng không khả dụng", - "open": "Mở", - "hideFileNames": "Ẩn tên tập tin", - "showFile": "Hiển thị 1 tập tin", - "showFiles": "Hiển thị {} tập tin", - "hideFile": "Ẩn 1 tập tin", - "hideFiles": "Ẩn {} tập tin" - } + "menuName": "Lưới" }, "document": { - "menuName": "Tài liệu", - "date": { - "timeHintTextInTwelveHour": "01:00 Chiều", - "timeHintTextInTwentyFourHour": "13:00" - }, "slashMenu": { "board": { "selectABoardToLinkTo": "Chọn một bảng để liên kết", "createANewBoard": "Tạo một bảng mới" - }, - "grid": { - "selectAGridToLinkTo": "Chọn một lưới để liên kết đến", - "createANewGrid": "Tạo một lưới mới" - }, - "calendar": { - "selectACalendarToLinkTo": "Chọn Lịch để liên kết đến", - "createANewCalendar": "Tạo một Lịch mới" - }, - "document": { - "selectADocumentToLinkTo": "Chọn một Tài liệu để liên kết đến" - }, - "name": { - "text": "Chữ", - "heading1": "Tiêu đề 1", - "heading2": "Tiêu đề 2", - "heading3": "Tiêu đề 3", - "image": "Hình ảnh", - "bulletedList": "Danh sách có dấu đầu dòng", - "numberedList": "Danh sách được đánh số", - "todoList": "Danh sách việc cần làm", - "doc": "Tài liệu", - "linkedDoc": "Liên kết đến trang", - "grid": "Lưới", - "linkedGrid": "Lưới liên kết", - "kanban": "Kanban", - "linkedKanban": "Kanban liên kết", - "calendar": "Lịch", - "linkedCalendar": "Lịch liên kết", - "quote": "Trích dẫn", - "divider": "Bộ chia", - "table": "Bàn", - "callout": "Chú thích", - "outline": "phác thảo", - "mathEquation": "Phương trình toán học", - "code": "Mã số", - "toggleList": "Chuyển đổi danh sách", - "emoji": "Biểu tượng cảm xúc", - "aiWriter": "Nhà văn AI", - "dateOrReminder": "Ngày hoặc nhắc nhở", - "photoGallery": "Thư viện ảnh", - "file": "Tài liệu" } }, - "selectionMenu": { - "outline": "phác thảo", - "codeBlock": "Khối mã" - }, "plugins": { - "referencedBoard": "Hội đồng tham khảo", - "referencedGrid": "Lưới tham chiếu", - "referencedCalendar": "Lịch tham khảo", - "referencedDocument": "Tài liệu tham khảo", - "autoGeneratorMenuItemName": "Nhà văn AI", - "autoGeneratorTitleName": "AI: Yêu cầu AI viết bất cứ điều gì...", - "autoGeneratorLearnMore": "Tìm hiểu thêm", - "autoGeneratorGenerate": "Phát ra", - "autoGeneratorHintText": "Hỏi AI ...", - "autoGeneratorCantGetOpenAIKey": "Không thể lấy được chìa khóa AI", - "autoGeneratorRewrite": "Viết lại", - "smartEdit": "Hỏi AI", - "aI": "AI", - "smartEditFixSpelling": "Sửa lỗi chính tả và ngữ pháp", - "warning": "⚠️ Phản hồi của AI có thể không chính xác hoặc gây hiểu lầm.", - "smartEditSummarize": "Tóm tắt", - "smartEditImproveWriting": "Cải thiện khả năng viết", - "smartEditMakeLonger": "Làm dài hơn", - "smartEditCouldNotFetchResult": "Không thể lấy kết quả từ AI", - "smartEditCouldNotFetchKey": "Không thể lấy được khóa AI", - "smartEditDisabled": "Kết nối AI trong Cài đặt", - "appflowyAIEditDisabled": "Đăng nhập để bật tính năng AI", - "discardResponse": "Bạn có muốn loại bỏ phản hồi của AI không?", - "createInlineMathEquation": "Tạo phương trình", - "fonts": "Phông chữ", - "insertDate": "Chèn ngày", - "emoji": "Biểu tượng cảm xúc", - "toggleList": "Chuyển đổi danh sách", - "quoteList": "Danh sách trích dẫn", - "numberedList": "Danh sách được đánh số", - "bulletedList": "Danh sách có dấu đầu dòng", - "todoList": "Danh sách việc cần làm", - "callout": "Chú thích", - "cover": { - "changeCover": "Thay đổi bìa", - "colors": "Màu sắc", - "images": "Hình ảnh", - "clearAll": "Xóa tất cả", - "abstract": "Tóm tắt", - "addCover": "Thêm Bìa", - "addLocalImage": "Thêm hình ảnh cục bộ", - "invalidImageUrl": "URL hình ảnh không hợp lệ", - "failedToAddImageToGallery": "Không thể thêm hình ảnh vào thư viện", - "enterImageUrl": "Nhập URL hình ảnh", - "add": "Thêm vào", - "back": "Quay lại", - "saveToGallery": "Lưu vào thư viện", - "removeIcon": "Xóa biểu tượng", - "pasteImageUrl": "Dán URL hình ảnh", - "or": "HOẶC", - "pickFromFiles": "Chọn từ các tập tin", - "couldNotFetchImage": "Không thể tải hình ảnh", - "imageSavingFailed": "Lưu hình ảnh không thành công", - "addIcon": "Thêm biểu tượng", - "changeIcon": "Thay đổi biểu tượng", - "coverRemoveAlert": "Nó sẽ được gỡ bỏ khỏi trang bìa sau khi bị xóa.", - "alertDialogConfirmation": "Bạn có chắc chắn muốn tiếp tục không?" - }, - "mathEquation": { - "name": "Phương trình toán học", - "addMathEquation": "Thêm một phương trình TeX", - "editMathEquation": "Chỉnh sửa phương trình toán học" - }, + "openAI": "OpenAI", "optionAction": { - "click": "Nhấp chuột", - "toOpenMenu": " để mở menu", "delete": "Xóa", - "duplicate": "Nhân bản", - "turnInto": "Biến thành", - "moveUp": "Di chuyển lên", - "moveDown": "Di chuyển xuống", "color": "Màu", "align": "Căn chỉnh", "left": "Trái", "center": "Giữa", "right": "Phải", - "defaultColor": "Mặc định", - "depth": "Độ sâu" - }, - "image": { - "addAnImage": "Thêm hình ảnh", - "copiedToPasteBoard": "Liên kết hình ảnh đã được sao chép vào clipboard", - "addAnImageDesktop": "Thêm một hình ảnh", - "addAnImageMobile": "Nhấp để thêm một hoặc nhiều hình ảnh", - "dropImageToInsert": "Thả hình ảnh để chèn", - "imageUploadFailed": "Tải hình ảnh lên không thành công", - "imageDownloadFailed": "Tải hình ảnh lên không thành công, vui lòng thử lại", - "imageDownloadFailedToken": "Tải lên hình ảnh không thành công do thiếu mã thông báo người dùng, vui lòng thử lại", - "errorCode": "Mã lỗi" - }, - "photoGallery": { - "name": "Thư viện ảnh", - "imageKeyword": "hình ảnh", - "imageGalleryKeyword": "thư viện hình ảnh", - "photoKeyword": "ảnh", - "photoBrowserKeyword": "trình duyệt ảnh", - "galleryKeyword": "phòng trưng bày", - "addImageTooltip": "Thêm hình ảnh", - "changeLayoutTooltip": "Thay đổi bố cục", - "browserLayout": "Trình duyệt", - "gridLayout": "Lưới", - "deleteBlockTooltip": "Xóa toàn bộ thư viện" - }, - "math": { - "copiedToPasteBoard": "Phương trình toán học đã được sao chép vào clipboard" - }, - "urlPreview": { - "copiedToPasteBoard": "Liên kết đã được sao chép vào clipboard", - "convertToLink": "Chuyển đổi thành liên kết nhúng" - }, - "outline": { - "addHeadingToCreateOutline": "Thêm tiêu đề để tạo mục lục.", - "noMatchHeadings": "Không tìm thấy tiêu đề phù hợp." + "defaultColor": "Mặc định" }, "table": { - "addAfter": "Thêm sau", - "addBefore": "Thêm trước", "delete": "Xoá", - "clear": "Xóa nội dung", - "duplicate": "Nhân bản", "bgColor": "Màu nền" }, "contextMenu": { @@ -1672,54 +694,7 @@ "cut": "Cắt", "paste": "Dán" }, - "action": "Hành động", - "database": { - "selectDataSource": "Chọn nguồn dữ liệu", - "noDataSource": "Không có nguồn dữ liệu", - "selectADataSource": "Chọn nguồn dữ liệu", - "toContinue": "để tiếp tục", - "newDatabase": "Cơ sở dữ liệu mới", - "linkToDatabase": "Liên kết đến Cơ sở dữ liệu" - }, - "date": "Ngày", - "video": { - "label": "Băng hình", - "emptyLabel": "Thêm video", - "placeholder": "Dán liên kết video", - "copiedToPasteBoard": "Liên kết video đã được sao chép vào clipboard", - "insertVideo": "Thêm video", - "invalidVideoUrl": "URL nguồn chưa được hỗ trợ.", - "invalidVideoUrlYouTube": "YouTube hiện chưa được hỗ trợ.", - "supportedFormats": "Các định dạng được hỗ trợ: MP4, WebM, MOV, AVI, FLV, MPEG/M4V, H.264" - }, - "file": { - "name": "Tài liệu", - "uploadTab": "Tải lên", - "uploadMobile": "Chọn tệp", - "networkTab": "Nhúng liên kết", - "placeholderText": "Tải lên hoặc nhúng một tập tin", - "placeholderDragging": "Thả tệp để tải lên", - "dropFileToUpload": "Thả tệp để tải lên", - "fileUploadHint": "Thả một tập tin ở đây để tải lên\nhoặc nhấp để duyệt", - "networkHint": "Dán liên kết tệp", - "networkUrlInvalid": "URL không hợp lệ, vui lòng sửa URL và thử lại", - "networkAction": "Nhúng liên kết tập tin", - "fileTooBigError": "Kích thước tệp quá lớn, vui lòng tải lên tệp có kích thước nhỏ hơn 10MB", - "renameFile": { - "title": "Đổi tên tập tin", - "description": "Nhập tên mới cho tập tin này", - "nameEmptyError": "Tên tệp không được để trống." - }, - "uploadedAt": "Đã tải lên vào {}", - "linkedAt": "Liên kết đã được thêm vào {}", - "failedToOpenMsg": "Không mở được, không tìm thấy tệp" - } - }, - "outlineBlock": { - "placeholder": "Mục lục" - }, - "textBlock": { - "placeholder": "Gõ '/' cho lệnh" + "action": "Hành động" }, "title": { "placeholder": "Trống" @@ -1734,68 +709,17 @@ "label": "Đường dẫn đến ảnh", "placeholder": "Nhập đường dẫn đến ảnh" }, - "ai": { - "label": "Tạo hình ảnh từ AI", - "placeholder": "Vui lòng nhập lời nhắc để AI tạo hình ảnh" - }, - "stability_ai": { - "label": "Tạo hình ảnh từ Stability AI", - "placeholder": "Vui lòng nhập lời nhắc cho Stability AI để tạo hình ảnh" - }, - "support": "Giới hạn kích thước hình ảnh là 5MB. Định dạng được hỗ trợ: JPEG, PNG, GIF, SVG", "error": { - "invalidImage": "Ảnh không hợp lệ", - "invalidImageSize": "Kích thước hình ảnh phải nhỏ hơn 5MB", - "invalidImageFormat": "Định dạng hình ảnh không được hỗ trợ. Các định dạng được hỗ trợ: JPEG, PNG, JPG, GIF, SVG, WEBP", - "invalidImageUrl": "URL hình ảnh không hợp lệ", - "noImage": "Không có tập tin hoặc thư mục như vậy", - "multipleImagesFailed": "Một hoặc nhiều hình ảnh không tải lên được, vui lòng thử lại" - }, - "embedLink": { - "label": "Nhúng liên kết", - "placeholder": "Dán hoặc nhập liên kết hình ảnh" - }, - "unsplash": { - "label": "Bỏ qua" - }, - "searchForAnImage": "Tìm kiếm hình ảnh", - "pleaseInputYourOpenAIKey": "vui lòng nhập khóa AI của bạn vào trang Cài đặt", - "saveImageToGallery": "Lưu hình ảnh", - "failedToAddImageToGallery": "Không thể thêm hình ảnh vào thư viện", - "successToAddImageToGallery": "Hình ảnh đã được thêm vào thư viện thành công", - "unableToLoadImage": "Không thể tải hình ảnh", - "maximumImageSize": "Kích thước hình ảnh tải lên được hỗ trợ tối đa là 10MB", - "uploadImageErrorImageSizeTooBig": "Kích thước hình ảnh phải nhỏ hơn 10MB", - "imageIsUploading": "Hình ảnh đang được tải lên", - "openFullScreen": "Mở toàn màn hình", - "interactiveViewer": { - "toolbar": { - "previousImageTooltip": "Hình ảnh trước đó", - "nextImageTooltip": "Hình ảnh tiếp theo", - "zoomOutTooltip": "Thu nhỏ", - "zoomInTooltip": "Phóng to", - "changeZoomLevelTooltip": "Thay đổi mức độ thu phóng", - "openLocalImage": "Mở hình ảnh", - "downloadImage": "Tải xuống hình ảnh", - "closeViewer": "Đóng trình xem tương tác", - "scalePercentage": "{}%", - "deleteImageTooltip": "Xóa hình ảnh" - } + "invalidImage": "Ảnh không hợp lệ" } }, "codeBlock": { "language": { "label": "Ngôn ngữ", - "placeholder": "Chọn ngôn ngữ", - "auto": "Tự động" - }, - "copyTooltip": "Sao chép", - "searchLanguageHint": "Tìm kiếm một ngôn ngữ", - "codeCopiedSnackbar": "Đã sao chép mã vào bảng tạm!" + "placeholder": "Chọn ngôn ngữ" + } }, "inlineLink": { - "placeholder": "Dán hoặc nhập liên kết", - "openInNewTab": "Mở trong tab mới", "copyLink": "Sao chép liên kết", "removeLink": "Xóa liên kết", "url": { @@ -1803,148 +727,50 @@ "placeholder": "Nhập URL liên kết" }, "title": { - "label": "Tiêu đề liên kết", - "placeholder": "Nhập tiêu đề liên kết" + "label": "Tiêu đề liên kết" } }, - "mention": { - "placeholder": "Nhắc đến một người, một trang hoặc một ngày...", - "page": { - "label": "Liên kết đến trang", - "tooltip": "Nhấp để mở trang" - }, - "deleted": "Đã xóa", - "deletedContent": "Nội dung này không tồn tại hoặc đã bị xóa", - "noAccess": "Không có quyền truy cập" - }, "toolbar": { "resetToDefaultFont": "Chỉnh về mặc định" - }, - "errorBlock": { - "theBlockIsNotSupported": "Không thể phân tích nội dung khối", - "clickToCopyTheBlockContent": "Nhấp để sao chép nội dung khối", - "blockContentHasBeenCopied": "Nội dung khối đã được sao chép.", - "parseError": "Đã xảy ra lỗi khi phân tích khối {}.", - "copyBlockContent": "Sao chép nội dung khối" - }, - "mobilePageSelector": { - "title": "Chọn trang", - "failedToLoad": "Không tải được danh sách trang", - "noPagesFound": "Không tìm thấy trang nào" } }, "board": { "column": { - "label": "Cột", "createNewCard": "Mới", - "renameGroupTooltip": "Nhấn để đổi tên nhóm", - "createNewColumn": "Thêm một nhóm mới", - "addToColumnTopTooltip": "Thêm một thẻ mới ở trên cùng", - "addToColumnBottomTooltip": "Thêm một thẻ mới ở phía dưới", "renameColumn": "Đổi tên", "hideColumn": "Ẩn", - "newGroup": "Nhóm mới", - "deleteColumn": "Xoá", - "deleteColumnConfirmation": "Thao tác này sẽ xóa nhóm này và tất cả các thẻ trong đó.\nBạn có chắc chắn muốn tiếp tục không?" + "deleteColumn": "Xoá" }, - "hiddenGroupSection": { - "sectionTitle": "Nhóm ẩn", - "collapseTooltip": "Ẩn các nhóm ẩn", - "expandTooltip": "Xem các nhóm ẩn" - }, - "cardDetail": "Chi tiết thẻ", - "cardActions": "Hành động thẻ", - "cardDuplicated": "Thẻ đã được sao chép", - "cardDeleted": "Thẻ đã bị xóa", - "showOnCard": "Hiển thị trên chi tiết thẻ", - "setting": "Cài đặt", - "propertyName": "Tên tài sản", "menuName": "Bảng", - "showUngrouped": "Hiển thị các mục chưa nhóm", - "ungroupedButtonText": "Không nhóm", - "ungroupedButtonTooltip": "Chứa các thẻ không thuộc bất kỳ nhóm nào", - "ungroupedItemsTitle": "Nhấp để thêm vào bảng", - "groupBy": "Nhóm theo", - "groupCondition": "Điều kiện nhóm", - "referencedBoardPrefix": "Xem của", - "notesTooltip": "Ghi chú bên trong", "mobile": { - "editURL": "Chỉnh sửa URL", "showGroup": "Hiển thị nhóm", "showGroupContent": "Bạn có chắc chắn muốn hiển thị nhóm này trên bảng không?", "failedToLoad": "Không tải được chế độ xem bảng" - }, - "dateCondition": { - "weekOf": "Tuần của {} - {}", - "today": "Hôm nay", - "yesterday": "Hôm qua", - "tomorrow": "Ngày mai", - "lastSevenDays": "7 ngày qua", - "nextSevenDays": "7 ngày tiếp theo", - "lastThirtyDays": "30 ngày qua", - "nextThirtyDays": "30 ngày tiếp theo" - }, - "noGroup": "Không có nhóm theo tài sản", - "noGroupDesc": "Các chế độ xem bảng yêu cầu một thuộc tính để nhóm theo để hiển thị", - "media": { - "cardText": "{} {}", - "fallbackName": "tập tin" } }, "calendar": { "menuName": "Lịch", "defaultNewCalendarTitle": "Trống", - "newEventButtonTooltip": "Thêm sự kiện mới", "navigation": { "today": "Hôm nay", - "jumpToday": "Nhảy tới Hôm nay", "previousMonth": "Tháng trước", - "nextMonth": "Tháng sau", - "views": { - "day": "Ngày", - "week": "Tuần", - "month": "Tháng", - "year": "Năm" - } - }, - "mobileEventScreen": { - "emptyTitle": "Chưa có sự kiện nào", - "emptyBody": "Nhấn nút dấu cộng để tạo sự kiện vào ngày này." + "nextMonth": "Tháng sau" }, "settings": { "showWeekNumbers": "Hiện thứ tự của tuần", "showWeekends": "Hiện cuối tuần", "firstDayOfWeek": "Ngày bắt đầu trong tuần", - "layoutDateField": "Bố trí lịch theo", - "changeLayoutDateField": "Thay đổi trường bố trí", "noDateTitle": "Không có ngày", - "noDateHint": { - "zero": "Các sự kiện không theo lịch trình sẽ hiển thị ở đây", - "one": "{count} sự kiện không theo lịch trình", - "other": "{count} sự kiện không theo lịch trình" - }, - "unscheduledEventsTitle": "Sự kiện không theo lịch trình", - "clickToAdd": "Ấn để thêm lịch", - "name": "Cài đặt lịch", - "clickToOpen": "Nhấp để mở hồ sơ" - }, - "referencedCalendarPrefix": "Xem của", - "quickJumpYear": "Nhảy tới", - "duplicateEvent": "Sự kiện trùng lặp" + "clickToAdd": "Ấn để thêm lịch" + } }, "errorDialog": { "title": "Lỗi của @:appName", "howToFixFallback": "Chúng tôi xin lỗi vì sự cố này! Vui lòng mở sự cố trên GitHub để báo lỗi.", - "howToFixFallbackHint1": "Chúng tôi xin lỗi vì sự bất tiện này! Gửi một vấn đề trên ", - "howToFixFallbackHint2": " trang mô tả lỗi của bạn.", "github": "Xem trên GitHub" }, "search": { - "label": "Tìm kiếm", - "sidebarSearchIcon": "Tìm kiếm và nhanh chóng chuyển đến một trang", - "placeholder": { - "actions": "Hành động tìm kiếm..." - } + "label": "Tìm kiếm" }, "message": { "copy": { @@ -1971,16 +797,12 @@ "gray": "Xám" }, "emoji": { - "emojiTab": "Biểu tượng cảm xúc", "search": "Tìm kiếm biểu tượng", - "noRecent": "Không có biểu tượng cảm xúc gần đây", "noEmojiFound": "Không tìm thấy biểu tượng", "filter": "Bộ lọc", "random": "Ngẫu nhiên", - "selectSkinTone": "Chọn tông màu da", "remove": "Loại bỏ biểu tượng", "categories": { - "smileys": "Biểu tượng mặt cười & Cảm xúc", "people": "Con người và cơ thể", "animals": "Động vật và thiên nhiên", "food": "Đồ ăn và thức uống", @@ -1989,58 +811,21 @@ "objects": "Vật thể", "symbols": "Biểu tượng", "flags": "Cờ", - "nature": "Tự nhiên", + "nature": "Thiên nhiên", "frequentlyUsed": "Thường dùng" }, "skinTone": { - "default": "Mặc định", - "light": "Sáng", - "mediumLight": "Sáng-Vừa", - "medium": "Vừa", - "mediumDark": "Tối-Vừa", - "dark": "Tối" - }, - "openSourceIconsFrom": "Biểu tượng nguồn mở từ" + "default": "Mặc định" + } }, "inlineActions": { "noResults": "Không có kết quả", - "recentPages": "Các trang gần đây", - "pageReference": "Trang tham khảo", - "docReference": "Tài liệu tham khảo", - "boardReference": "Tham khảo bảng", - "calReference": "Tham khảo lịch", - "gridReference": "Tham chiếu lưới", "date": "Ngày", "reminder": { "groupTitle": "Lời nhắc", "shortKeyword": "nhắc nhở" } }, - "datePicker": { - "dateTimeFormatTooltip": "Thay đổi định dạng ngày và giờ trong cài đặt", - "dateFormat": "Định dạng ngày tháng", - "includeTime": "Bao gồm thời gian", - "isRange": "Ngày kết thúc", - "timeFormat": "Định dạng thời gian", - "clearDate": "Ngày xóa", - "reminderLabel": "Lời nhắc nhở", - "selectReminder": "Chọn lời nhắc", - "reminderOptions": { - "none": "Không có", - "atTimeOfEvent": "Thời gian diễn ra sự kiện", - "fiveMinsBefore": "5 phút trước", - "tenMinsBefore": "10 phút trước", - "fifteenMinsBefore": "15 phút trước", - "thirtyMinsBefore": "30 phút trước", - "oneHourBefore": "1 giờ trước", - "twoHoursBefore": "2 giờ trước", - "onDayOfEvent": "Vào ngày diễn ra sự kiện", - "oneDayBefore": "1 ngày trước", - "twoDaysBefore": "2 ngày trước", - "oneWeekBefore": "1 tuần trước", - "custom": "Phong tục" - } - }, "relativeDates": { "yesterday": "Hôm qua", "today": "Hôm nay", @@ -2049,35 +834,18 @@ }, "notificationHub": { "title": "Thông báo", - "mobile": { - "title": "Cập nhật" - }, - "emptyTitle": "Đã cập nhật đầy đủ!", - "emptyBody": "Không có thông báo hoặc hành động nào đang chờ xử lý. Hãy tận hưởng sự bình yên.", - "tabs": { - "inbox": "Hộp thư đến", - "upcoming": "Sắp tới" - }, "actions": { - "markAllRead": "Đánh dấu tất cả là đã đọc", - "showAll": "Tất cả", "showUnreads": "Chưa đọc" }, "filters": { "ascending": "Tăng dần", - "descending": "Giảm dần", - "groupByDate": "Nhóm theo ngày", - "showUnreadsOnly": "Chỉ hiển thị những tin chưa đọc", - "resetToDefault": "Đặt lại về mặc định" + "descending": "Giảm dần" }, "empty": "Không có gì ở đây!" }, "reminderNotification": { "title": "Lời nhắc", - "message": "Hãy nhớ kiểm tra điều này trước khi bạn quên nhé!", - "tooltipDelete": "Xoá", - "tooltipMarkRead": "Đánh dấu là đã đọc", - "tooltipMarkUnread": "Đánh dấu là chưa đọc" + "tooltipDelete": "Xoá" }, "findAndReplace": { "find": "Tìm kiếm", @@ -2086,482 +854,18 @@ "close": "Đóng", "replace": "Thay thế", "replaceAll": "Thay thế tất cả", - "noResult": "Không thấy kết quả", - "caseSensitive": "Phân biệt chữ hoa chữ thường", - "searchMore": "Tìm kiếm để tìm thêm kết quả" - }, - "error": { - "weAreSorry": "Chúng tôi xin lỗi", - "loadingViewError": "Chúng tôi đang gặp sự cố khi tải chế độ xem này. Vui lòng kiểm tra kết nối internet, làm mới ứng dụng và đừng ngần ngại liên hệ với nhóm nếu sự cố vẫn tiếp diễn.", - "syncError": "Dữ liệu không được đồng bộ từ thiết bị khác", - "syncErrorHint": "Vui lòng mở lại trang này trên thiết bị mà bạn đã chỉnh sửa lần cuối, sau đó mở lại trên thiết bị hiện tại.", - "clickToCopy": "Nhấp để sao chép mã lỗi" + "noResult": "Không thấy kết quả" }, "editor": { - "bold": "In đậm", - "bulletedList": "Danh sách có dấu đầu dòng", - "bulletedListShortForm": "Có dấu đầu dòng", - "checkbox": "Đánh dấu", - "embedCode": "Mã nhúng", - "heading1": "H1", - "heading2": "H2", - "heading3": "H3", - "highlight": "Điểm nổi bật", - "color": "Màu sắc", - "image": "Hình ảnh", - "date": "Ngày", - "page": "Trang", - "italic": "nghiêng", - "link": "Liên kết", - "numberedList": "Danh sách được đánh số", - "numberedListShortForm": "Đã đánh số", - "quote": "Trích dẫn", - "strikethrough": "gạch xuyên", - "text": "Chữ", - "underline": "Gạch chân", - "fontColorDefault": "Mặc định", - "fontColorGray": "Xám", - "fontColorBrown": "Màu nâu", - "fontColorOrange": "Quả cam", - "fontColorYellow": "Màu vàng", - "fontColorGreen": "Màu xanh lá", - "fontColorBlue": "Màu xanh da trời", - "fontColorPurple": "Màu tím", - "fontColorPink": "Hồng", - "fontColorRed": "Màu đỏ", - "backgroundColorDefault": "Nền mặc định", - "backgroundColorGray": "Nền xám", - "backgroundColorBrown": "Nền màu nâu", - "backgroundColorOrange": "Nền màu cam", - "backgroundColorYellow": "Nền vàng", - "backgroundColorGreen": "Nền xanh", - "backgroundColorBlue": "Nền xanh", - "backgroundColorPurple": "Nền màu tím", - "backgroundColorPink": "Nền màu hồng", - "backgroundColorRed": "Nền đỏ", - "backgroundColorLime": "Nền vôi", - "backgroundColorAqua": "Nền màu nước", - "done": "Xong", - "cancel": "Hủy bỏ", - "tint1": "Màu 1", - "tint2": "Màu 2", - "tint3": "Màu 3", - "tint4": "Màu 4", - "tint5": "Màu 5", - "tint6": "Màu 6", - "tint7": "Màu 7", - "tint8": "Màu 8", - "tint9": "Màu 9", - "lightLightTint1": "Màu tím", - "lightLightTint2": "Hồng", - "lightLightTint3": "Hồng nhạt", - "lightLightTint4": "Quả cam", - "lightLightTint5": "Màu vàng", - "lightLightTint6": "Chanh xanh", - "lightLightTint7": "Màu xanh lá", - "lightLightTint8": "Nước", - "lightLightTint9": "Màu xanh da trời", - "urlHint": "Địa chỉ URL", - "mobileHeading1": "Tiêu đề 1", - "mobileHeading2": "Tiêu đề 2", - "mobileHeading3": "Tiêu đề 3", - "textColor": "Màu chữ", - "backgroundColor": "Màu nền", - "addYourLink": "Thêm liên kết của bạn", - "openLink": "Mở liên kết", - "copyLink": "Sao chép liên kết", - "removeLink": "Xóa liên kết", - "editLink": "Chỉnh sửa liên kết", - "linkText": "Chữ", - "linkTextHint": "Vui lòng nhập văn bản", - "linkAddressHint": "Vui lòng nhập URL", - "highlightColor": "Màu sắc nổi bật", - "clearHighlightColor": "Xóa màu nổi bật", - "customColor": "Màu tùy chỉnh", - "hexValue": "Giá trị hex", - "opacity": "Độ mờ đục", - "resetToDefaultColor": "Đặt lại màu mặc định", - "ltr": "LTR", - "rtl": "RTL", - "auto": "Tự động", "cut": "Cắt", "copy": "Sao chép", "paste": "Dán", - "find": "Tìm thấy", - "select": "Lựa chọn", - "selectAll": "Chọn tất cả", - "previousMatch": "Trùng khớp trước đó", - "nextMatch": "Trùng khớp tiếp theo", - "closeFind": "Đóng", - "replace": "Thay thế", - "replaceAll": "Thay thế tất cả", - "regex": "Biểu thức chính quy", - "caseSensitive": "Phân biệt chữ hoa chữ thường", - "uploadImage": "Tải lên hình ảnh", - "urlImage": "Hình ảnh URL", - "incorrectLink": "Liên kết không đúng", - "upload": "Tải lên", - "chooseImage": "Chọn một hình ảnh", - "loading": "Đang tải", - "imageLoadFailed": "Tải hình ảnh không thành công", - "divider": "Bộ chia", - "table": "Bàn", - "colAddBefore": "Thêm trước", - "rowAddBefore": "Thêm trước", - "colAddAfter": "Thêm sau", - "rowAddAfter": "Thêm sau", "colRemove": "Xoá", - "rowRemove": "Xoá", - "colDuplicate": "Nhân bản", - "rowDuplicate": "Nhân bản", - "colClear": "Xóa nội dung", - "rowClear": "Xóa nội dung", - "slashPlaceHolder": "Nhập '/' để chèn một khối hoặc bắt đầu nhập", - "typeSomething": "Hãy nhập gì đó...", - "toggleListShortForm": "Chuyển đổi", - "quoteListShortForm": "Trích dẫn", - "mathEquationShortForm": "Công thức", - "codeBlockShortForm": "Mã" - }, - "favorite": { - "noFavorite": "Không có trang yêu thích", - "noFavoriteHintText": "Vuốt trang sang trái để thêm vào mục yêu thích của bạn", - "removeFromSidebar": "Xóa khỏi thanh bên", - "addToSidebar": "Ghim vào thanh bên" - }, - "cardDetails": { - "notesPlaceholder": "Nhập / để chèn một khối hoặc bắt đầu nhập" - }, - "blockPlaceholders": { - "todoList": "Việc cần làm", - "bulletList": "Danh sách", - "numberList": "Danh sách", - "quote": "Trích dẫn", - "heading": "Tiêu đề {}" + "rowRemove": "Xoá" }, "titleBar": { - "pageIcon": "Biểu tượng trang", "language": "Ngôn ngữ", "font": "Phông chữ", - "actions": "Hành động", - "date": "Ngày", - "addField": "Thêm trường", - "userIcon": "Biểu tượng người dùng" - }, - "noLogFiles": "Không có tệp nhật ký nào", - "newSettings": { - "myAccount": { - "title": "Tài khoản của tôi", - "subtitle": "Tùy chỉnh hồ sơ, quản lý bảo mật tài khoản, mở khóa AI hoặc đăng nhập vào tài khoản của bạn.", - "profileLabel": "Tên tài khoản & Ảnh đại diện", - "profileNamePlaceholder": "Nhập tên của bạn", - "accountSecurity": "Bảo mật tài khoản", - "2FA": "Xác thực 2 bước", - "aiKeys": "Phím AI", - "accountLogin": "Đăng nhập tài khoản", - "updateNameError": "Không cập nhật được tên", - "updateIconError": "Không cập nhật được biểu tượng", - "deleteAccount": { - "title": "Xóa tài khoản", - "subtitle": "Xóa vĩnh viễn tài khoản và toàn bộ dữ liệu của bạn.", - "description": "Xóa vĩnh viễn tài khoản của bạn và xóa quyền truy cập khỏi mọi không gian làm việc.", - "deleteMyAccount": "Xóa tài khoản của tôi", - "dialogTitle": "Xóa tài khoản", - "dialogContent1": "Bạn có chắc chắn muốn xóa vĩnh viễn tài khoản của mình không?", - "dialogContent2": "Không thể hoàn tác hành động này và sẽ xóa quyền truy cập khỏi mọi không gian làm việc nhóm, xóa toàn bộ tài khoản của bạn, bao gồm cả không gian làm việc riêng tư và xóa bạn khỏi mọi không gian làm việc được chia sẻ.", - "confirmHint1": "Vui lòng nhập \"@:newSettings.myAccount.deleteAccount.confirmHint3\" để xác nhận.", - "confirmHint2": "Tôi hiểu rằng hành động này là không thể đảo ngược và sẽ xóa vĩnh viễn tài khoản của tôi cùng mọi dữ liệu liên quan.", - "confirmHint3": "XÓA TÀI KHOẢN CỦA TÔI", - "checkToConfirmError": "Bạn phải đánh dấu vào ô để xác nhận việc xóa", - "failedToGetCurrentUser": "Không lấy được email người dùng hiện tại", - "confirmTextValidationFailed": "Văn bản xác nhận của bạn không khớp với \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", - "deleteAccountSuccess": "Tài khoản đã được xóa thành công" - } - }, - "workplace": { - "name": "Nơi làm việc", - "title": "Cài đặt nơi làm việc", - "subtitle": "Tùy chỉnh giao diện không gian làm việc, chủ đề, phông chữ, bố cục văn bản, ngày, giờ và ngôn ngữ.", - "workplaceName": "Tên nơi làm việc", - "workplaceNamePlaceholder": "Nhập tên nơi làm việc", - "workplaceIcon": "Biểu tượng nơi làm việc", - "workplaceIconSubtitle": "Tải lên hình ảnh hoặc sử dụng biểu tượng cảm xúc cho không gian làm việc của bạn. Biểu tượng sẽ hiển thị trên thanh bên và thông báo của bạn.", - "renameError": "Không đổi được tên nơi làm việc", - "updateIconError": "Không cập nhật được biểu tượng", - "chooseAnIcon": "Chọn một biểu tượng", - "appearance": { - "name": "Vẻ bề ngoài", - "themeMode": { - "auto": "Tự động", - "light": "Ánh sáng", - "dark": "Tối tăm" - }, - "language": "Ngôn ngữ" - } - }, - "syncState": { - "syncing": "Đồng bộ hóa", - "synced": "Đã đồng bộ", - "noNetworkConnected": "Không có kết nối mạng" - } - }, - "pageStyle": { - "title": "Kiểu trang", - "layout": "Cách trình bày", - "coverImage": "Ảnh bìa", - "pageIcon": "Biểu tượng trang", - "colors": "Màu sắc", - "gradient": "Độ dốc", - "backgroundImage": "Hình nền", - "presets": "Cài đặt trước", - "photo": "Ảnh", - "unsplash": "Bỏ qua", - "pageCover": "Trang bìa", - "none": "Không có", - "openSettings": "Mở Cài đặt", - "photoPermissionTitle": "@:appName muốn truy cập vào thư viện ảnh của bạn", - "photoPermissionDescription": "Cho phép truy cập vào thư viện ảnh để tải ảnh lên.", - "doNotAllow": "Không cho phép", - "image": "Hình ảnh" - }, - "commandPalette": { - "placeholder": "Nhập để tìm kiếm...", - "bestMatches": "Trận đấu hay nhất", - "recentHistory": "Lịch sử gần đây", - "navigateHint": "để điều hướng", - "loadingTooltip": "Chúng tôi đang tìm kiếm kết quả...", - "betaLabel": "BETA", - "betaTooltip": "Hiện tại chúng tôi chỉ hỗ trợ tìm kiếm các trang và nội dung trong tài liệu", - "fromTrashHint": "Từ rác", - "noResultsHint": "Chúng tôi không tìm thấy những gì bạn đang tìm kiếm, hãy thử tìm kiếm thuật ngữ khác.", - "clearSearchTooltip": "Xóa trường tìm kiếm" - }, - "space": { - "delete": "Xóa bỏ", - "deleteConfirmation": "Xóa bỏ: ", - "deleteConfirmationDescription": "Tất cả các trang trong Không gian này sẽ bị xóa và chuyển vào Thùng rác, và bất kỳ trang nào đã xuất bản sẽ bị hủy xuất bản.", - "rename": "Đổi tên không gian", - "changeIcon": "Thay đổi biểu tượng", - "manage": "Quản lý không gian", - "addNewSpace": "Tạo không gian", - "collapseAllSubPages": "Thu gọn tất cả các trang con", - "createNewSpace": "Tạo không gian mới", - "createSpaceDescription": "Tạo nhiều không gian công cộng và riêng tư để sắp xếp công việc tốt hơn.", - "spaceName": "Tên không gian", - "spaceNamePlaceholder": "ví dụ Marketing, Kỹ thuật, Nhân sự", - "permission": "Sự cho phép", - "publicPermission": "Công cộng", - "publicPermissionDescription": "Tất cả các thành viên không gian làm việc có quyền truy cập đầy đủ", - "privatePermission": "Riêng tư", - "privatePermissionDescription": "Chỉ bạn mới có thể truy cập vào không gian này", - "spaceIconBackground": "Màu nền", - "spaceIcon": "Biểu tượng", - "dangerZone": "Khu vực nguy hiểm", - "unableToDeleteLastSpace": "Không thể xóa khoảng trắng cuối cùng", - "unableToDeleteSpaceNotCreatedByYou": "Không thể xóa các Không gian do người khác tạo", - "enableSpacesForYourWorkspace": "Bật Spaces cho không gian làm việc của bạn", - "title": "Khoảng cách", - "defaultSpaceName": "Tổng quan", - "upgradeSpaceTitle": "Kích hoạt Khoảng cách", - "upgradeSpaceDescription": "Tạo nhiều Không gian công cộng và riêng tư để sắp xếp không gian làm việc của bạn tốt hơn.", - "upgrade": "Cập nhật", - "upgradeYourSpace": "Tạo nhiều khoảng trống", - "quicklySwitch": "Nhanh chóng chuyển sang không gian tiếp theo", - "duplicate": "Không gian trùng lặp", - "movePageToSpace": "Di chuyển trang đến khoảng trống", - "switchSpace": "Chuyển đổi không gian", - "spaceNameCannotBeEmpty": "Tên khoảng trắng không được để trống" - }, - "publish": { - "hasNotBeenPublished": "Trang này chưa được xuất bản", - "spaceHasNotBeenPublished": "Chưa hỗ trợ xuất bản không gian", - "reportPage": "Báo cáo trang", - "databaseHasNotBeenPublished": "Việc xuất bản cơ sở dữ liệu hiện chưa được hỗ trợ.", - "createdWith": "Được tạo ra với", - "downloadApp": "Tải AppFlowy", - "copy": { - "codeBlock": "Nội dung của khối mã đã được sao chép vào bảng tạm", - "imageBlock": "Liên kết hình ảnh đã được sao chép vào clipboard", - "mathBlock": "Phương trình toán học đã được sao chép vào clipboard", - "fileBlock": "Liên kết tập tin đã được sao chép vào clipboard" - }, - "containsPublishedPage": "Trang này chứa một hoặc nhiều trang đã xuất bản. Nếu bạn tiếp tục, chúng sẽ bị hủy xuất bản. Bạn có muốn tiếp tục xóa không?", - "publishSuccessfully": "Đã xuất bản thành công", - "unpublishSuccessfully": "Đã hủy xuất bản thành công", - "publishFailed": "Không thể xuất bản", - "unpublishFailed": "Không thể hủy xuất bản", - "noAccessToVisit": "Không thể truy cập vào trang này...", - "createWithAppFlowy": "Tạo trang web với AppFlowy", - "fastWithAI": "Nhanh chóng và dễ dàng với AI.", - "tryItNow": "Thử ngay bây giờ", - "onlyGridViewCanBePublished": "Chỉ có thể xuất bản chế độ xem Lưới", - "database": { - "zero": "Xuất bản {} chế độ xem đã chọn", - "one": "Xuất bản {} chế độ xem đã chọn", - "many": "Xuất bản {} chế độ xem đã chọn", - "other": "Xuất bản {} chế độ xem đã chọn" - }, - "mustSelectPrimaryDatabase": "Phải chọn chế độ xem chính", - "noDatabaseSelected": "Chưa chọn cơ sở dữ liệu, vui lòng chọn ít nhất một cơ sở dữ liệu.", - "unableToDeselectPrimaryDatabase": "Không thể bỏ chọn cơ sở dữ liệu chính", - "saveThisPage": "Lưu trang này", - "duplicateTitle": "Bạn muốn thêm vào đâu?", - "selectWorkspace": "Chọn không gian làm việc", - "addTo": "Thêm vào", - "duplicateSuccessfully": "Tạo bản sao thành công. Bạn muốn xem tài liệu?", - "duplicateSuccessfullyDescription": "Bạn không có ứng dụng? Quá trình tải xuống của bạn sẽ tự động bắt đầu sau khi nhấp vào 'Tải xuống'.", - "downloadIt": "Tải về", - "openApp": "Mở trong ứng dụng", - "duplicateFailed": "Sao chép không thành công", - "membersCount": { - "zero": "Không có thành viên", - "one": "1 thành viên", - "many": "{đếm} thành viên", - "other": "{đếm} thành viên" - }, - "useThisTemplate": "Sử dụng mẫu" - }, - "web": { - "continue": "Tiếp tục", - "or": "hoặc", - "continueWithGoogle": "Tiếp tục với Google", - "continueWithGithub": "Tiếp tục với GitHub", - "continueWithDiscord": "Tiếp tục với Discord", - "continueWithApple": "Tiếp tục với Apple ", - "moreOptions": "Thêm tùy chọn", - "collapse": "Thu gọn", - "signInAgreement": "Bằng cách nhấp vào \"Tiếp tục\" ở trên, bạn đã đồng ý với AppFlowy", - "and": "và", - "termOfUse": "Điều khoản", - "privacyPolicy": "Chính sách bảo mật", - "signInError": "Lỗi đăng nhập", - "login": "Đăng ký hoặc đăng nhập", - "fileBlock": { - "uploadedAt": "Đã tải lên vào {time}", - "linkedAt": "Liên kết được thêm vào {time}", - "empty": "Tải lên hoặc nhúng một tập tin" - } - }, - "globalComment": { - "comments": "Bình luận", - "addComment": "Thêm bình luận", - "reactedBy": "phản ứng bởi", - "addReaction": "Thêm phản ứng", - "reactedByMore": "và {đếm} người khác", - "showSeconds": { - "one": "1 giây trước", - "other": "{count} giây trước", - "zero": "Vừa xong", - "many": "{count} giây trước" - }, - "showMinutes": { - "one": "1 phút trước", - "other": "{count} phút trước", - "many": "{count} phút trước" - }, - "showHours": { - "one": "1 giờ trước", - "other": "{count} giờ trước", - "many": "{count} giờ trước" - }, - "showDays": { - "one": "1 ngày trước", - "other": "{count} ngày trước", - "many": "{count} ngày trước" - }, - "showMonths": { - "one": "1 tháng trước", - "other": "{count} tháng trước", - "many": "{count} tháng trước" - }, - "showYears": { - "one": "1 năm trước", - "other": "{đếm} năm trước", - "many": "{đếm} năm trước" - }, - "reply": "Hồi đáp", - "deleteComment": "Xóa bình luận", - "youAreNotOwner": "Bạn không phải là chủ sở hữu của bình luận này", - "confirmDeleteDescription": "Bạn có chắc chắn muốn xóa bình luận này không?", - "hasBeenDeleted": "Đã xóa", - "replyingTo": "Trả lời cho", - "noAccessDeleteComment": "Bạn không được phép xóa bình luận này", - "collapse": "Sụp đổ", - "readMore": "Đọc thêm", - "failedToAddComment": "Không thêm được bình luận", - "commentAddedSuccessfully": "Đã thêm bình luận thành công.", - "commentAddedSuccessTip": "Bạn vừa thêm hoặc trả lời bình luận. Bạn có muốn chuyển lên đầu trang để xem các bình luận mới nhất không?" - }, - "template": { - "asTemplate": "Như mẫu", - "name": "Tên mẫu", - "description": "Mô tả mẫu", - "about": "Mẫu Giới Thiệu", - "preview": "Bản xem trước mẫu", - "categories": "Danh mục mẫu", - "isNewTemplate": "PIN vào mẫu mới", - "featured": "PIN vào mục Nổi bật", - "relatedTemplates": "Mẫu liên quan", - "requiredField": "{field} là bắt buộc", - "addCategory": "Thêm \"{category}\"", - "addNewCategory": "Thêm danh mục mới", - "addNewCreator": "Thêm người sáng tạo mới", - "deleteCategory": "Xóa danh mục", - "editCategory": "Chỉnh sửa danh mục", - "editCreator": "Chỉnh sửa người tạo", - "category": { - "name": "Tên danh mục", - "icon": "Biểu tượng danh mục", - "bgColor": "Màu nền danh mục", - "priority": "Ưu tiên danh mục", - "desc": "Mô tả danh mục", - "type": "Loại danh mục", - "icons": "Biểu tượng danh mục", - "colors": "Thể loại Màu sắc", - "byUseCase": "Theo trường hợp sử dụng", - "byFeature": "Theo tính năng", - "deleteCategory": "Xóa danh mục", - "deleteCategoryDescription": "Bạn có chắc chắn muốn xóa danh mục này không?", - "typeToSearch": "Nhập để tìm kiếm theo danh mục..." - }, - "creator": { - "label": "Người tạo mẫu", - "name": "Tên người sáng tạo", - "avatar": "Avatar của người sáng tạo", - "accountLinks": "Liên kết tài khoản người sáng tạo", - "uploadAvatar": "Nhấp để tải lên hình đại diện", - "deleteCreator": "Xóa người tạo", - "deleteCreatorDescription": "Bạn có chắc chắn muốn xóa người sáng tạo này không?", - "typeToSearch": "Nhập để tìm kiếm người sáng tạo..." - }, - "uploadSuccess": "Mẫu đã được tải lên thành công", - "uploadSuccessDescription": "Mẫu của bạn đã được tải lên thành công. Bây giờ bạn có thể xem nó trong thư viện mẫu.", - "viewTemplate": "Xem mẫu", - "deleteTemplate": "Xóa mẫu", - "deleteTemplateDescription": "Bạn có chắc chắn muốn xóa mẫu này không?", - "addRelatedTemplate": "Thêm mẫu liên quan", - "removeRelatedTemplate": "Xóa mẫu liên quan", - "uploadAvatar": "Tải lên hình đại diện", - "searchInCategory": "Tìm kiếm trong {category}", - "label": "Bản mẫu" - }, - "fileDropzone": { - "dropFile": "Nhấp hoặc kéo tệp vào khu vực này để tải lên", - "uploading": "Đang tải lên...", - "uploadFailed": "Tải lên không thành công", - "uploadSuccess": "Tải lên thành công", - "uploadSuccessDescription": "Tệp đã được tải lên thành công", - "uploadFailedDescription": "Tải tệp lên không thành công", - "uploadingDescription": "Tập tin đang được tải lên" - }, - "gallery": { - "preview": "Mở toàn màn hình", - "copy": "Sao chép", - "download": "Tải về", - "prev": "Trước", - "next": "Kế tiếp", - "resetZoom": "Đặt lại chế độ thu phóng", - "zoomIn": "Phóng to", - "zoomOut": "Thu nhỏ" + "date": "Ngày" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/vi.json b/frontend/resources/translations/vi.json index b921c1844e..4d1716447a 100644 --- a/frontend/resources/translations/vi.json +++ b/frontend/resources/translations/vi.json @@ -6,4 +6,4 @@ "failedToLoad": "Không tải được chế độ xem bảng" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index d1433929bc..ce81120a68 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -36,7 +36,6 @@ "loginButtonText": "登录", "loginStartWithAnonymous": "从匿名会话开始", "continueAnonymousUser": "继续匿名会话", - "anonymous": "匿名", "buttonText": "登录", "signingInText": "登录中...", "forgotPassword": "忘记密码?", @@ -51,8 +50,6 @@ "signInWithGoogle": "使用 Google 账户登录", "signInWithGithub": "使用 Github 账户登录", "signInWithDiscord": "使用 Discord 账户登录", - "signInWithApple": "使用 Apple 账户登录", - "continueAnotherWay": "使用其他方式登录", "signUpWithGoogle": "使用 Google 账户注册", "signUpWithGithub": "使用 Github 账户注册", "signUpWithDiscord": "使用 Discord 账户注册", @@ -68,7 +65,6 @@ "logIn": "登陆", "generalError": "出现错误,请稍后再试", "limitRateError": "出于安全原因,每60秒仅可以发送一次链接", - "magicLinkSentDescription": "一个验证链接已发送到您的电子邮箱。点击该链接即可完成登录。该链接将在 5 分钟后失效。", "LogInWithGoogle": "使用 Google 登录", "LogInWithGithub": "使用 Github 登录", "LogInWithDiscord": "使用 Discord 登录", @@ -77,14 +73,9 @@ }, "workspace": { "chooseWorkspace": "选择您的工作区", - "defaultName": "我的工作区", "create": "新建工作区", - "new": "新的工作空间", - "importFromNotion": "从 Notion 导入", - "learnMore": "了解更多", "reset": "重置工作区", "renameWorkspace": "重命名工作区", - "workspaceNameCannotBeEmpty": "工作区名称不可为空", "resetWorkspacePrompt": "重置工作区将删除其中的所有页面和数据。您确定要重置工作区吗?您也可以联系技术支持团队来恢复工作区", "hint": "工作区", "notFoundError": "找不到工作区", @@ -120,25 +111,7 @@ "html": "HTML", "clipboard": "拷贝到剪贴板", "csv": "CSV", - "copyLink": "复制链接", - "publishToTheWeb": "发布至网络", - "publishToTheWebHint": "利用AppFlowy创建一个网站", - "publish": "发布", - "unPublish": "取消发布", - "visitSite": "访问网站", - "exportAsTab": "导出为", - "publishTab": "发布", - "shareTab": "分享", - "publishOnAppFlowy": "在 AppFlowy 发布", - "shareTabTitle": "邀请他人来协作", - "shareTabDescription": "与任何人轻松协作", - "copyLinkSuccess": "链接已复制到剪贴板", - "copyShareLink": "复制分享链接", - "copyLinkFailed": "无法将链接复制到剪贴板", - "copyLinkToBlockSuccess": "区块链接已复制到剪贴板", - "copyLinkToBlockFailed": "无法将区块链接复制到剪贴板", - "manageAllSites": "管理所有站点", - "updatePathName": "更新路径名称" + "copyLink": "复制链接" }, "moreAction": { "small": "小", @@ -151,18 +124,12 @@ "charCount": "字符数:{}", "createdAt": "创建于 :{}", "deleteView": "删除", - "duplicateView": "复制", - "wordCountLabel": "词总数:", - "charCountLabel": "字符总数:", - "createdAtLabel": "已创建的:", - "syncedAtLabel": "已同步的:", - "saveAsNewPage": "在页面中添加消息" + "duplicateView": "复制" }, "importPanel": { "textAndMarkdown": "文本 和 Markdown", "documentFromV010": "来自 v0.1.0 的文档", "databaseFromV010": "来自 v0.1.0 的数据库", - "notionZip": "Notion 导出的 Zip 文件", "csv": "CSV", "database": "数据库" }, @@ -176,11 +143,8 @@ "moveTo": "移动", "addToFavorites": "添加到收藏夹", "copyLink": "复制链接", - "changeIcon": "更改图标", - "collapseAllPages": "收起全部子页面", - "movePageTo": "将页面移动至", - "move": "移动", - "lockPage": "锁定页面" + "changeIcon": "更改图表", + "collapseAllPages": "收起全部子页面" }, "blankPageTitle": "空白页", "newPageText": "新页面", @@ -190,49 +154,20 @@ "newBoardText": "新建看板", "chat": { "newChat": "AI 对话", - "inputMessageHint": "问 @:appName AI", - "inputLocalAIMessageHint": "问 @:appName 本地 AI", - "unsupportedCloudPrompt": "该功能仅在使用 @:appName Cloud 时可用", + "unsupportedCloudPrompt": "该功能仅在使用 AppFlowy Cloud 时可用", "relatedQuestion": "相关问题", "serverUnavailable": "服务暂时不可用,请稍后再试", - "aiServerUnavailable": "🌈 不妙!🌈 一只独角兽吃掉了我们的回复。请重试!", - "retry": "重试", "clickToRetry": "点击重试", "regenerateAnswer": "重新生成", "question1": "如何使用 Kanban 来管理任务", "question2": "介绍一下 GTD 工作法", "question3": "为什么使用 Rust", "question4": "使用现有食材制定一份菜谱", - "aiMistakePrompt": "AI 可能出错,请验证重要信息", - "chatWithFilePrompt": "你想与文件对话吗?", - "indexFileSuccess": "文件索引成功", - "inputActionNoPages": "无页面结果", - "clickToMention": "点击以提及页面", - "uploadFile": "上传聊天使用的 PDF、md 或 txt 文件", - "questionDetail": "{} 你好!我能怎么帮到你?", - "indexingFile": "正在索引 {}", - "generatingResponse": "正在生成相应", - "sourceUnsupported": "我们当前不支持基于数据库的交流", - "regenerate": "请重试", - "addToPageTitle": "添加消息至......", - "addToNewPage": "创建新的页面", - "openPagePreviewFailedToast": "打开页面失败", - "changeFormat": { - "actionButton": "变更样式", - "confirmButton": "基于该样式重新生成", - "imageOnly": "仅图片", - "textAndImage": "文本与图片", - "text": "段落", - "table": "表格" - }, - "referenceSource": "找到 {} 个来源", - "referenceSources": "找到 {} 个来源", - "questionTitle": "想法" + "aiMistakePrompt": "AI 可能出错,请验证重要信息" }, "trash": { "text": "回收站", "restoreAll": "全部恢复", - "restore": "恢复", "deleteAll": "全部删除", "pageHeader": { "fileName": "文件名", @@ -247,10 +182,6 @@ "title": "您确定要恢复回收站中的所有页面吗?", "caption": "此操作无法撤消。" }, - "restorePage": { - "title": "恢复:{}", - "caption": "你确定要恢复此页面吗?" - }, "mobile": { "actions": "垃圾桶操作", "empty": "垃圾桶是空的", @@ -263,28 +194,26 @@ "deletePagePrompt": { "text": "此页面已被移动至垃圾桶", "restore": "恢复页面", - "deletePermanent": "彻底删除", - "deletePermanentDescription": "你确定要永久删除此页面吗?此操作无法撤销。" + "deletePermanent": "彻底删除" }, "dialogCreatePageNameHint": "页面名称", "questionBubble": { "shortcuts": "快捷键", "whatsNew": "新功能", + "help": "帮助和支持", "markdown": "Markdown", "debug": { "name": "调试信息", "success": "将调试信息复制到剪贴板!", "fail": "无法将调试信息复制到剪贴板" }, - "feedback": "反馈", - "help": "帮助和支持" + "feedback": "反馈" }, "menuAppHeader": { "moreButtonToolTip": "删除、重命名等等...", "addPageTooltip": "在其中快速添加页面", "defaultNewPageName": "未命名页面", - "renameDialog": "重命名", - "pageNameSuffix": "复制" + "renameDialog": "重命名" }, "noPagesInside": "里面没有页面", "toolbar": { @@ -315,7 +244,6 @@ "viewDataBase": "查看数据库", "referencePage": "这个 {name} 正在被引用", "addBlockBelow": "在下面添加一个块", - "aiGenerate": "生成", "urlLaunchAccessory": "在浏览器中打开", "urlCopyAccessory": "复制链接" }, @@ -326,8 +254,6 @@ "private": "私人的", "workspace": "工作区", "favorites": "收藏夹", - "clickToHidePrivate": "点击以隐藏私人空间\n您在此处创建的页面仅对您可见", - "clickToHideWorkspace": "点击以隐藏私人空间\n您在此处创建的页面对所有人可见", "clickToHidePersonal": "点击隐藏个人部分", "clickToHideFavorites": "单击隐藏收藏夹栏目", "addAPage": "添加页面", @@ -337,37 +263,10 @@ "today": "今日", "thisWeek": "本周", "others": "其他", - "earlier": "更早", "justNow": "现在", "minutesAgo": "{count} 分钟以前", "lastViewed": "最近一次查看", - "favoriteAt": "已收藏", - "emptyRecent": "没有最近页面", - "emptyRecentDescription": "在你查看页面时,它们会出现在这里,方便检索。", - "emptyFavorite": "无收藏页面", - "emptyFavoriteDescription": "将页面收藏起来——它们会列在这里,方便快速访问!", - "removePageFromRecent": "从最近页移除此页面吗?", - "removeSuccess": "成功移除", - "favoriteSpace": "收藏", - "RecentSpace": "最近", - "Spaces": "空间", - "upgradeToPro": "升级至专业版", - "upgradeToAIMax": "解锁无限制 AI", - "storageLimitDialogTitle": "你已用尽免费存储。升级以解锁无限制存储", - "storageLimitDialogTitleIOS": "你已用尽免费存储。", - "aiResponseLimitTitle": "你已用尽免费 AI 回应。升级到专业版或者购买 AI 插件来解锁无限制回应", - "aiResponseLimitDialogTitle": "已达到 AI 回应限额", - "aiResponseLimit": "你已用尽了免费 AI 回应。\n\n转到“设置 -> 计划 -> 点击 AI Max 或 Pro 计划”获取更多 AI 回应", - "askOwnerToUpgradeToPro": "你的工作区即将用尽免费存储。请联系工作区所有者升级到专业版计划", - "askOwnerToUpgradeToProIOS": "你的工作区即将用尽免费存储。", - "askOwnerToUpgradeToAIMax": "你的工作区即将用尽免费 AI 回应。请联系工作区所有者升级计划或购买 AI 插件", - "askOwnerToUpgradeToAIMaxIOS": "你的工作区即将用尽免费 AI 回应限额。", - "aiImageResponseLimit": "你已消耗完你的 AI 图像响应额度。\n转到 设置 -> 方案 -> 点击 AI Max 去获得更多的图像响应额度", - "purchaseStorageSpace": "购买存储空间", - "purchaseAIResponse": "购买", - "askOwnerToUpgradeToLocalAI": "联系工作区所有者启用设备上 AI", - "upgradeToAILocal": "在你的设备上运行本地模型,极致保护隐私", - "upgradeToAILocalDesc": "使用本地 AI 用 PDF 聊天、改善写作、自动填充表格" + "removeSuccess": "成功移除" }, "notifications": { "export": { @@ -401,22 +300,16 @@ "upload": "上传", "edit": "编辑", "delete": "删除", - "copy": "复制", "duplicate": "复制", "putback": "放回去", "update": "更新", "share": "分享", "removeFromFavorites": "从收藏夹中", - "removeFromRecent": "从最近页移除", "addToFavorites": "添加到收藏夹", - "favoriteSuccessfully": "收藏成功", - "unfavoriteSuccessfully": "取消收藏成功", - "duplicateSuccessfully": "副本创建成功", "rename": "重命名", "helpCenter": "帮助中心", "add": "添加", "yes": "是", - "no": "否", "clear": "清空", "remove": "移除", "dontRemove": "不移除", @@ -430,18 +323,6 @@ "signInGithub": "使用 Github 账户登录", "signInDiscord": "使用 Discord 账户登录", "more": "更多", - "create": "创建", - "close": "关闭", - "next": "下一个", - "previous": "上一个", - "submit": "提交", - "download": "下载", - "backToHome": "返回主页", - "viewing": "查看", - "editing": "编辑", - "gotIt": "我知道了", - "retry": "重试", - "uploadFailed": "上传失败", "Done": "完成", "Cancel": "取消", "OK": "确认" @@ -468,87 +349,28 @@ }, "settings": { "title": "设置", - "popupMenuItem": { - "settings": "设置", - "members": "成员", - "trash": "回收站", - "helpAndSupport": "帮助与支持" - }, - "sites": { - "title": "站点", - "namespaceTitle": "名字空间", - "namespaceDescription": "管理你的名字空间与主页", - "namespaceHeader": "名字空间", - "homepageHeader": "主页", - "updateNamespace": "更新名字空间", - "removeHomepage": "移除主页", - "selectHomePage": "选择一个页面", - "customUrl": "自定义 URL", - "namespace": { - "updateExistingNamespace": "更新现有的名称空间", - "upgradeToPro": "请升级至 Pro 订阅计划以设置主页", - "redirectToPayment": "正在重定向至付款页面......", - "onlyWorkspaceOwnerCanSetHomePage": "仅工作空间的所有者能为其设置主页", - "pleaseAskOwnerToSetHomePage": "请联系工作空间所有者更新至 Pro 订阅计划" - }, - "publishedPage": { - "title": "所有已发布页面", - "description": "管理您已发布的页面", - "date": "已发布的数据", - "emptyHinText": "在当前工作空间没有已发布的页面", - "noPublishedPages": "没有已发布的页面", - "settings": "发布设置", - "clickToOpenPageInApp": "在 App 中打开页面", - "clickToOpenPageInBrowser": "在浏览器中打开页面" - }, - "error": { - "failedToUpdateNamespace": "更新名称空间失败", - "proPlanLimitation": "您需要升级至 Pro 方案以更新名称空间", - "namespaceAlreadyInUse": "该名称空间已被占用你,请尝试其他名称空间", - "invalidNamespace": "无效的名称空间,请尝试其他的名称空间", - "namespaceLengthAtLeast2Characters": "名称空间应不少于 2 个字符长度", - "onlyWorkspaceOwnerCanUpdateNamespace": "仅工作空间所有者可更新名称空间", - "onlyWorkspaceOwnerCanRemoveHomepage": "仅工作空间所有者可移除主页", - "setHomepageFailed": "设置主页失败", - "namespaceTooLong": "该名称空间过长,请尝试其他名称空间", - "namespaceTooShort": "该名称空间过短,请尝试其他名称空间", - "namespaceIsReserved": "该名称空间已被占用,请尝试其他名称空间", - "updatePathNameFailed": "更新路径名称失败", - "removeHomePageFailed": "移除主页失败", - "publishNameContainsInvalidCharacters": "该路径名称包含无效的字符,请尝试其他的路径名称", - "publishNameTooShort": "路径名称过短,请尝试其他路径名称", - "publishNameTooLong": "路径名称过长,请尝试其他路径名称", - "publishNameAlreadyInUse": "该路径名称已被使用,请尝试其他路径名称", - "namespaceContainsInvalidCharacters": "该名称空间包含无效的字符,请尝试其他名称空间", - "publishPermissionDenied": "仅工作空间所有者或页面发布者可管理发布设置", - "publishNameCannotBeEmpty": "该路径名称不能为空,请尝试其他路径名称" - }, - "success": { - "namespaceUpdated": "更新名称空间成功", - "setHomepageSuccess": "成功设置主页", - "updatePathNameSuccess": "更新路径名称成功", - "removeHomePageSuccess": "成功删除主页" - } - }, "accountPage": { "menuLabel": "我的账户", "title": "我的账户", - "general": { - "title": "账户名与资料图像", - "changeProfilePicture": "更换头像" - }, + "description": "自定义您的简介,管理账户安全信息和 AI API keys,或登陆您的账户", "email": { "title": "邮箱", "actions": { "change": "更改邮箱" } }, + "keys": { + "title": "AI API 密钥", + "openAILabel": "OpenAI API 密钥", + "openAIHint": "输入你的 OpenAI API Key", + "stabilityAILabel": "Stability API key", + "stabilityAIHint": "输入你的 Stability API Key" + }, "login": { "title": "登录账户", "loginLabel": "登录", "logoutLabel": "退出登录" - }, - "description": "自定义您的简介,管理账户安全信息和 AI API keys,或登陆您的账户" + } }, "workspacePage": { "menuLabel": "工作区", @@ -572,32 +394,18 @@ "dark": "黑暗" } }, - "resetCursorColor": { - "title": "重置文档光标颜色", - "description": "确定要重置光标颜色吗?" - }, - "resetSelectionColor": { - "title": "重置文档选取颜色", - "description": "确定要重置选区颜色吗?" - }, - "resetWidth": { - "resetSuccess": "文档宽度重置成功" - }, "theme": { "title": "主题", - "description": "选择预设主题,或上传你自己的自定义主题。", - "uploadCustomThemeTooltip": "上传自定义主题" + "description": "选择预设主题,或上传你自己的自定义主题。" }, "workspaceFont": { - "title": "工作区字体", - "noFontHint": "找不到字体,换个词试试。" + "title": "工作区字体" }, "textDirection": { "title": "文字方向", "leftToRight": "从左到右", "rightToLeft": "从右到左", - "auto": "自动", - "enableRTLItems": "启用从右向左工具栏项目" + "auto": "自动" }, "layoutDirection": { "title": "布局方向", @@ -606,14 +414,9 @@ }, "dateTime": { "title": "日期 & 时间", - "example": "{} 于 {} ({})", "24HourTime": "24 小时制", "dateFormat": { "label": "日期格式", - "local": "本地", - "us": "US", - "iso": "ISO", - "friendly": "友好", "dmy": "日/月/年" } }, @@ -626,9 +429,7 @@ }, "leaveWorkspacePrompt": { "title": "离开工作区", - "content": "你确定要离开此工作区吗?你将无法访问其中的所有页面和数据。", - "success": "你已经成功离开工作区。", - "fail": "无法离开工作区。" + "content": "你确定要离开此工作区吗?你将无法访问其中的所有页面和数据。" }, "manageWorkspace": { "title": "管理工作区", @@ -652,14 +453,12 @@ "resetTooltip": "重置为默认位置" }, "resetDialog": { - "title": "你确定吗?", "description": "将数据路径重置为默认位置不会删除你的数据。如果你想重新导入当前数据,你应该先复制当前位置的路径。" } }, "importData": { "title": "导入数据", "tooltip": "从 @:appName 备份/数据文件夹导入数据", - "description": "从外部 @:appName 数据文件夹复制数据", "action": "浏览文件夹" }, "encryption": { @@ -669,7 +468,6 @@ "descriptionEncrypted": "你的数据已加密。", "action": "加密数据", "dialog": { - "title": "加密您的所有数据?", "description": "加密所有数据将保证数据安全。此操作无法撤消。您确定要继续吗?" } }, @@ -677,67 +475,9 @@ "title": "清除缓存", "description": "如果你遇到图片无法加载或字体无法正确显示的问题,请尝试清除缓存。此操作不会删除你的用户数据。", "dialog": { - "title": "清除缓存", "description": "清除缓存会导致加载时重新下载图像和字体。此操作不会删除或修改你的数据。", "successHint": "缓存已清除!" } - }, - "data": { - "fixYourData": "修复数据", - "fixButton": "修复" - } - }, - "shortcutsPage": { - "menuLabel": "快捷键", - "title": "快捷键", - "actions": { - "resetDefault": "重置为默认" - }, - "errorPage": { - "howToFix": "请再次尝试,如果该问题依然存在,请在 Github 上联系我们" - }, - "resetDialog": { - "title": "重置快捷键", - "description": "这将会将所有按键绑定重置为默认,之后无法撤销。你确定要继续吗?" - }, - "conflictDialog": { - "confirmLabel": "继续" - }, - "keybindings": { - "insertNewParagraphInCodeblock": "插入新的段落", - "pasteInCodeblock": "粘贴为代码块", - "selectAllCodeblock": "全选", - "copy": "复制选中的内容", - "alignLeft": "文本居左对齐", - "alignCenter": "文本居中对齐", - "alignRight": "文本居右对齐", - "undo": "撤销", - "redo": "重做", - "convertToParagraph": "将块转换为段落", - "backspace": "删除", - "deleteLeftWord": "删除左侧文字", - "deleteLeftSentence": "删除左侧句子", - "delete": "删除右侧字符", - "deleteMacOS": "删除左侧字符", - "deleteRightWord": "删除右侧文字", - "moveCursorLeft": "将光标移至左侧", - "moveCursorBeginning": "将光标移至开头", - "moveCursorLeftWord": "将光标移至文字左侧", - "moveCursorRight": "将光标移至右侧", - "moveCursorEnd": "将光标移至末尾", - "moveCursorRightWord": "将光标移至文字右侧", - "home": "滚动至顶部", - "end": "滚动至底部" - } - }, - "aiPage": { - "title": "AI 设置", - "menuLabel": "AI 设置", - "keys": { - "enableAISearchTitle": "AI 搜索", - "aiSettingsDescription": "选择你偏好的模型来赋能 AppFlowy AI。目前可以使用 GPT 4-o、Claude 3,5、Llama 3.1 与 Mistral 7B", - "llmModel": "语言模型", - "llmModelType": "语言模型类别" } }, "planPage": { @@ -747,40 +487,6 @@ } } }, - "comparePlanDialog": { - "proLabels": { - "itemTwo": "最多十个", - "itemThree": "无限", - "itemFour": "是", - "itemFive": "是", - "itemSix": "无限", - "itemFileUpload": "无限" - } - }, - "cancelSurveyDialog": { - "questionOne": { - "answerOne": "价格太高", - "answerTwo": "功能未达预期", - "answerThree": "找到更好替代方案" - }, - "questionTwo": { - "answerOne": "非常可能", - "answerTwo": "稍有可能", - "answerThree": "不确定", - "answerFour": "不太可能", - "answerFive": "非常不可能" - }, - "questionFour": { - "answerOne": "极好", - "answerTwo": "良好", - "answerThree": "一般", - "answerFour": "较差", - "answerFive": "未满足" - } - }, - "common": { - "reset": "重置" - }, "menu": { "appearance": "外观", "language": "语言", @@ -800,6 +506,11 @@ "cloudServerType": "云服务器", "cloudServerTypeTip": "请注意,切换云服务器后可能会登出您当前的账户", "cloudLocal": "本地", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "supabase url 不能为空", + "cloudSupabaseAnonKey": "Supabase Anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase url 不为空,则 Anon key 不能为空", "cloudAppFlowy": "@:appName Cloud Beta", "cloudAppFlowySelfHost": "@:appName Cloud 自托管", "appFlowyCloudUrlCanNotBeEmpty": "云地址不能为空", @@ -829,43 +540,20 @@ "importSuccess": "成功导入@:appName数据文件夹", "importFailed": "导入 @:appName 数据文件夹失败", "importGuide": "有关详细信息,请参阅参考文档", + "supabaseSetting": "Supabase 设置", "cloudSetting": "云设置" }, "notifications": { "enableNotifications": { "label": "启用通知", "hint": "关闭以阻止本地通知出现。" - }, - "showNotificationsIcon": { - "label": "显示通知图标", - "hint": "关闭开关以隐藏侧边栏中的通知图标。" - }, - "markAsReadNotifications": { - "allSuccess": "成功全部标为已读", - "success": "成功标为已读" - }, - "action": { - "markAsRead": "标为已读", - "multipleChoice": "选择更多" - }, - "settings": { - "settings": "设置" - }, - "tabs": { - "unread": "未读" - }, - "refreshSuccess": "成功刷新通知", - "titles": { - "notifications": "通知", - "reminder": "提醒" } }, "appearance": { "resetSetting": "重置此设置", "fontFamily": { "label": "字体系列", - "search": "搜索", - "defaultFont": "系统" + "search": "搜索" }, "themeMode": { "label": "主题模式", @@ -877,9 +565,6 @@ "documentSettings": { "cursorColor": "文档光标颜色", "selectionColor": "文档选择颜色", - "pickColor": "选择颜色", - "colorShade": "色深", - "opacity": "透明度", "hexEmptyError": "十六进制颜色不能为空", "hexLengthError": "十六进制值必须为 6 位数字", "hexInvalidError": "十六进制值无效", @@ -935,7 +620,6 @@ "members": { "title": "成员设置", "inviteMembers": "添加成员", - "inviteHint": "使用电子邮件邀请", "sendInvite": "发送邀请", "copyInviteLink": "复制邀请链接", "label": "成员", @@ -946,14 +630,10 @@ "guest": "访客", "member": "成员", "emailSent": "邮件已发送,请检查您的邮箱", - "memberLimitExceededUpgrade": "升级", - "memberLimitExceededProContact": "support@appflowy.io", "failedToAddMember": "添加成员失败", "addMemberSuccess": "添加成员成功", "removeMember": "移除成员", - "areYouSureToRemoveMember": "您确定要删除该成员吗?", - "inviteMemberSuccess": "成功发送邀请", - "failedToInviteMember": "邀请成员失败" + "areYouSureToRemoveMember": "您确定要删除该成员吗?" } }, "files": { @@ -1001,26 +681,9 @@ "email": "电子邮件", "tooltipSelectIcon": "选择图标", "selectAnIcon": "选择一个图标", - "pleaseInputYourOpenAIKey": "请输入您的 AI 密钥", - "clickToLogout": "点击退出当前用户", - "pleaseInputYourStabilityAIKey": "请输入您的 Stability AI 密钥" - }, - "mobile": { - "personalInfo": "个人信息", - "username": "用户名", - "usernameEmptyError": "用户名不能为空", - "about": "关于", - "pushNotifications": "推送通知", - "support": "支持", - "joinDiscord": "在 Discord 中加入我们", - "privacyPolicy": "隐私政策", - "userAgreement": "用户协议", - "termsAndConditions": "条款和条件", - "userprofileError": "无法加载用户配置文件", - "userprofileErrorDescription": "请尝试注销并重新登录以检查问题是否仍然存在。", - "selectLayout": "选择布局", - "selectStartingDay": "选择开始日期", - "version": "版本" + "pleaseInputYourOpenAIKey": "请输入您的 OpenAI 密钥", + "pleaseInputYourStabilityAIKey": "请输入您的 Stability AI 密钥", + "clickToLogout": "点击退出当前用户" }, "shortcuts": { "shortcutsLabel": "快捷方式", @@ -1042,6 +705,23 @@ "textAlignRight": "右对齐文本", "codeBlockDeleteTwoSpaces": "删除代码块中行首的两个空格" } + }, + "mobile": { + "personalInfo": "个人信息", + "username": "用户名", + "usernameEmptyError": "用户名不能为空", + "about": "关于", + "pushNotifications": "推送通知", + "support": "支持", + "joinDiscord": "在 Discord 中加入我们", + "privacyPolicy": "隐私政策", + "userAgreement": "用户协议", + "termsAndConditions": "条款和条件", + "userprofileError": "无法加载用户配置文件", + "userprofileErrorDescription": "请尝试注销并重新登录以检查问题是否仍然存在。", + "selectLayout": "选择布局", + "selectStartingDay": "选择开始日期", + "version": "版本" } }, "grid": { @@ -1142,7 +822,6 @@ "isNotEmpty": "不为空" }, "field": { - "label": "属性", "hide": "隐藏", "show": "展示", "insertLeft": "左侧插入", @@ -1160,8 +839,6 @@ "multiSelectFieldName": "多项选择器", "urlFieldName": "链接", "checklistFieldName": "清单", - "summaryFieldName": "AI 总结", - "timeFieldName": "时间", "numberFormat": "数字格式", "dateFormat": "日期格式", "includeTime": "包含时间", @@ -1270,7 +947,8 @@ "url": { "launch": "在浏览器中打开链接", "copy": "将链接复制到剪贴板", - "textFieldHint": "输入 URL" + "textFieldHint": "输入 URL", + "copiedNotification": "已复制到剪贴板!" }, "relation": { "rowSearchTextFieldPlaceholder": "搜索" @@ -1286,12 +964,6 @@ "min": "分钟", "sum": "和" }, - "media": { - "rename": "重命名", - "download": "下载", - "delete": "删除", - "open": "打开" - }, "singleSelectOptionFilter": { "is": "等于", "isNot": "不等于", @@ -1326,62 +998,6 @@ }, "document": { "selectADocumentToLinkTo": "选择要链接到的文档" - }, - "name": { - "textStyle": "文本样式", - "list": "列表", - "toggle": "切换", - "fileAndMedia": "文件与媒体", - "simpleTable": "简单表格", - "visuals": "视觉元素", - "document": "文档", - "advanced": "高级", - "text": "文本", - "heading1": "一级标题", - "heading2": "二级标题", - "heading3": "三级标题", - "image": "图片", - "bulletedList": "项目符号列表", - "numberedList": "编号列表", - "todoList": "待办事项列表", - "doc": "文档", - "linkedDoc": "链接到页面", - "grid": "网格", - "linkedGrid": "链接网格", - "kanban": "看板", - "linkedKanban": "链接看板", - "calendar": "日历", - "linkedCalendar": "链接日历", - "quote": "引用", - "divider": "分隔符", - "table": "表格", - "callout": "提示框", - "outline": "大纲", - "mathEquation": "数学公式", - "code": "代码", - "toggleList": "切换列表", - "toggleHeading1": "切换标题1", - "toggleHeading2": "切换标题2", - "toggleHeading3": "切换标题3", - "emoji": "表情符号", - "aiWriter": "向AI提问", - "dateOrReminder": "日期或提醒", - "photoGallery": "图片库", - "file": "文件", - "twoColumns": "两列", - "threeColumns": "三列", - "fourColumns": "四列" - }, - "subPage": { - "name": "文档", - "keyword1": "子页面", - "keyword2": "页面", - "keyword3": "子页面", - "keyword4": "插入页面", - "keyword5": "嵌入页面", - "keyword6": "新页面", - "keyword7": "创建页面", - "keyword8": "文档" } }, "selectionMenu": { @@ -1393,33 +1009,23 @@ "referencedGrid": "引用的网格", "referencedCalendar": "引用的日历", "referencedDocument": "参考文档", - "aiWriter": { - "userQuestion": "向AI提问", - "continueWriting": "继续写作", - "fixSpelling": "修正拼写和语法", - "improveWriting": "优化写作", - "summarize": "总结", - "explain": "解释", - "makeShorter": "缩短", - "makeLonger": "扩展" - }, - "autoGeneratorMenuItemName": "AI 创作", - "autoGeneratorTitleName": "AI: 让 AI 写些什么...", + "autoGeneratorMenuItemName": "OpenAI 创作", + "autoGeneratorTitleName": "OpenAI: 让 AI 写些什么...", "autoGeneratorLearnMore": "学习更多", "autoGeneratorGenerate": "生成", - "autoGeneratorHintText": "让 AI ...", - "autoGeneratorCantGetOpenAIKey": "无法获得 AI 密钥", + "autoGeneratorHintText": "让 OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "无法获得 OpenAI 密钥", "autoGeneratorRewrite": "重写", "smartEdit": "AI 助手", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "修正拼写", "warning": "⚠️ AI 可能不准确或具有误导性.", "smartEditSummarize": "总结", "smartEditImproveWriting": "提高写作水平", "smartEditMakeLonger": "丰富内容", - "smartEditCouldNotFetchResult": "无法从 AI 获取到结果", - "smartEditCouldNotFetchKey": "无法获取到 AI 密钥", - "smartEditDisabled": "在设置中连接 AI", + "smartEditCouldNotFetchResult": "无法从 OpenAI 获取到结果", + "smartEditCouldNotFetchKey": "无法获取到 OpenAI 密钥", + "smartEditDisabled": "在设置中连接 OpenAI", "discardResponse": "您是否要放弃 AI 继续写作?", "createInlineMathEquation": "创建方程", "fonts": "字体", @@ -1463,7 +1069,7 @@ }, "optionAction": { "click": "点击", - "toOpenMenu": "打开菜单", + "toOpenMenu": " 来打开菜单", "delete": "删除", "duplicate": "复制", "turnInto": "变成", @@ -1473,14 +1079,12 @@ "align": "对齐", "left": "左", "center": "中心", - "right": "右", - "defaultColor": "默认", - "depth": "深度", - "copyLinkToBlock": "粘贴块链接" + "right": "又", + "defaultColor": "默认" }, "image": { - "addAnImage": "添加图像", - "copiedToPasteBoard": "图片链接已复制到剪贴板" + "copiedToPasteBoard": "图片链接已复制到剪贴板", + "addAnImage": "添加图像" }, "urlPreview": { "copiedToPasteBoard": "链接已复制到剪贴板" @@ -1489,8 +1093,8 @@ "addHeadingToCreateOutline": "添加标题以创建目录。" }, "table": { - "addAfter": "在后面添加", - "addBefore": "在前面添加", + "addAfter": "在前面添加", + "addBefore": "在后面添加", "delete": "删除", "clear": "清空内容", "duplicate": "创建副本", @@ -1517,27 +1121,6 @@ "placeholder": "粘贴视频链接", "copiedToPasteBoard": "视频链接已复制到剪贴板", "insertVideo": "添加视频" - }, - "linkPreview": { - "typeSelection": { - "pasteAs": "粘贴为", - "mention": "提及", - "URL": "URL", - "bookmark": "书签", - "embed": "嵌入" - }, - "linkPreviewMenu": { - "toMetion": "转换为提及", - "toUrl": "转换为URL", - "toEmbed": "转换为嵌入", - "toBookmark": "转换为书签", - "copyLink": "复制链接", - "replace": "替换", - "reload": "重新加载", - "removeLink": "移除链接", - "pasteHint": "粘贴 https://...", - "unableToDisplay": "无法显示" - } } }, "outlineBlock": { @@ -1560,8 +1143,8 @@ "placeholder": "输入图片网址" }, "ai": { - "label": "从 AI 生成图像", - "placeholder": "请输入 AI 生成图像的提示" + "label": "从 OpenAI 生成图像", + "placeholder": "请输入 OpenAI 生成图像的提示" }, "stability_ai": { "label": "从 Stability AI 生成图像", @@ -1583,15 +1166,15 @@ "label": "Unsplash" }, "searchForAnImage": "搜索图像", - "pleaseInputYourOpenAIKey": "请在设置页面输入您的 AI 密钥", + "pleaseInputYourOpenAIKey": "请在设置页面输入您的 OpenAI 密钥", + "pleaseInputYourStabilityAIKey": "请在设置页面输入您的 Stability AI 密钥", "saveImageToGallery": "保存图片", "failedToAddImageToGallery": "无法将图像添加到图库", "successToAddImageToGallery": "图片已成功添加到图库", "unableToLoadImage": "无法加载图像", "maximumImageSize": "支持的最大上传图片大小为 10MB", "uploadImageErrorImageSizeTooBig": "图片大小必须小于 10MB", - "imageIsUploading": "图片正在上传", - "pleaseInputYourStabilityAIKey": "请在设置页面输入您的 Stability AI 密钥" + "imageIsUploading": "图片正在上传" }, "codeBlock": { "language": { @@ -2004,18 +1587,10 @@ "deleteAccount": { "title": "删除帐户", "subtitle": "永久删除你的帐户和所有数据。", - "description": "永久删除你的账户,并移除所有工作区的访问权限。", "deleteMyAccount": "删除我的账户", "dialogTitle": "删除帐户", "dialogContent1": "你确定要永久删除您的帐户吗?", - "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。", - "confirmHint1": "请输入 \"@:newSettings.myAccount.deleteAccount.confirmHint3\" 以确认。", - "confirmHint2": "我理解此操作是不可逆的,并且将永久删除我的帐户和所有关联数据。", - "confirmHint3": "删除我的账户", - "checkToConfirmError": "你必须勾选以确认删除。", - "failedToGetCurrentUser": "获取当前用户邮箱失败", - "confirmTextValidationFailed": "你的确认文本不匹配 \"@:newSettings.myAccount.deleteAccount.confirmHint3\"", - "deleteAccountSuccess": "账户删除成功" + "dialogContent2": "此操作无法撤消,并且将删除所有团队空间的访问权限,删除你的整个帐户(包括私人工作区),并将你从所有共享工作区中删除。" } }, "workplace": { @@ -2039,57 +1614,5 @@ }, "commandPalette": { "placeholder": "输入你要搜索的内容..." - }, - "space": { - "defaultSpaceName": "一般" - }, - "publish": { - "saveThisPage": "使用此模板创建" - }, - "web": { - "continueWithGoogle": "使用 Google 账户登录", - "continueWithGithub": "使用 GitHub 账户登录", - "continueWithDiscord": "使用 Discord 账户登录" - }, - "template": { - "deleteFromTemplate": "从模板中删除", - "relatedTemplates": "相关模板", - "deleteTemplate": "删除模板", - "removeRelatedTemplate": "移除相关模板", - "label": "模板" - }, - "time": { - "justNow": "刚刚", - "seconds": { - "one": "1 秒", - "other": "{count} 秒" - }, - "minutes": { - "one": "1 分钟", - "other": "{count} 分钟" - }, - "hours": { - "one": "1 小时", - "other": "{count} 小时" - }, - "days": { - "one": "1 天", - "other": "{count} 天" - }, - "weeks": { - "one": "1 周", - "other": "{count} 周" - }, - "months": { - "one": "1 月", - "other": "{count} 月" - }, - "years": { - "one": "1 年", - "other": "{count} 年" - }, - "ago": "前", - "yesterday": "昨天", - "today": "今天" } } \ No newline at end of file diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index b5f4ff3d5f..e49eca0e7e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -9,7 +9,6 @@ "title": "標題", "youCanAlso": "你也可以", "and": "和", - "failedToOpenUrl": "無法開啟網址:{}", "blockActions": { "addBelowTooltip": "點選以在下方新增", "addAboveCmd": "Alt+點選", @@ -36,14 +35,12 @@ "loginButtonText": "登入", "loginStartWithAnonymous": "以匿名身份開始", "continueAnonymousUser": "以匿名身份繼續", - "anonymous": "匿名的", "buttonText": "登入", "signingInText": "正在登入...", "forgotPassword": "忘記密碼?", "emailHint": "電子郵件", "passwordHint": "密碼", "dontHaveAnAccount": "還沒有帳號?", - "createAccount": "建立帳戶", "repeatPasswordEmptyError": "確認密碼不能為空", "unmatchedPasswordError": "密碼重複輸入不一致", "syncPromptMessage": "同步資料可能需要一些時間。請不要關閉此頁面", @@ -51,8 +48,6 @@ "signInWithGoogle": "使用Google 登入", "signInWithGithub": "使用Github 登入", "signInWithDiscord": "使用Discord 登入", - "signInWithApple": "使用 Apple 帳號登入", - "continueAnotherWay": "使用其他方式登入", "signUpWithGoogle": "使用Google 註冊", "signUpWithGithub": "使用Github 註冊", "signUpWithDiscord": "使用Discord 註冊", @@ -68,7 +63,6 @@ "logIn": "登入", "generalError": "出了些問題。請稍後再試", "limitRateError": "出於安全原因,您只能每60 秒申請一次Magic Link", - "magicLinkSentDescription": "連結已發送到您的電子信箱。點擊連結即可登入。連結將在 5 分鐘後過期。", "LogInWithGoogle": "使用 Google 登入", "LogInWithGithub": "使用 Github 登入", "LogInWithDiscord": "使用 Discord 登入", @@ -78,7 +72,6 @@ "chooseWorkspace": "選擇你的工作區", "create": "建立工作區", "reset": "重設工作區", - "renameWorkspace": "重新命名工作區", "resetWorkspacePrompt": "重設工作區將刪除其中所有頁面和資料。你確定要重設工作區嗎?或者,你可以聯絡支援團隊來恢復工作區。", "hint": "工作區", "notFoundError": "找不到工作區", @@ -93,7 +86,6 @@ "deleteWorkspaceHintText": "您確定要刪除工作區嗎?此操作無法撤銷", "createSuccess": "成功創建工作區", "createFailed": "無法創建工作區", - "createLimitExceeded": "您已達到帳戶允許的最大工作區限制。如果您需要額外的工作空間,請在 Github 上申請", "deleteSuccess": "工作區刪除成功", "deleteFailed": "工作區刪除失敗", "openSuccess": "成功開啟工作區", @@ -114,14 +106,7 @@ "html": "HTML", "clipboard": "複製到剪貼簿", "csv": "CSV", - "copyLink": "複製連結", - "publishToTheWeb": "發佈到公開網路", - "publishToTheWebHint": "使用 AppFlowy 建立網站", - "publish": "發佈", - "unPublish": "取消發佈", - "exportAsTab": "導出為", - "publishTab": "發佈", - "shareTab": "分享" + "copyLink": "複製連結" }, "moreAction": { "small": "小", @@ -129,12 +114,7 @@ "large": "大", "fontSize": "字型大小", "import": "匯入", - "moreOptions": "更多選項", - "wordCount": "字數:{}", - "charCount": "字符數:{}", - "createdAt": "建立於:{}", - "deleteView": "刪除", - "duplicateView": "複製" + "moreOptions": "更多選項" }, "importPanel": { "textAndMarkdown": "文字 & Markdown", @@ -152,9 +132,7 @@ "openNewTab": "在新分頁中開啟", "moveTo": "移動到", "addToFavorites": "加入最愛", - "copyLink": "複製連結", - "changeIcon": "更改圖示", - "collapseAllPages": "折疊所有子頁面" + "copyLink": "複製連結" }, "blankPageTitle": "空白頁面", "newPageText": "新增頁面", @@ -162,25 +140,6 @@ "newGridText": "新增網格", "newCalendarText": "新增日曆", "newBoardText": "新增看板", - "chat": { - "newChat": "AI 聊天", - "inputMessageHint": "詢問 @:appName AI", - "inputLocalAIMessageHint": "詢問 @:appName 本機 AI", - "unsupportedCloudPrompt": "此功能僅在使用 @:appName Cloud 時可用", - "relatedQuestion": "相關問題", - "serverUnavailable": "服務暫時無法使用,請稍後再試。", - "aiServerUnavailable": "🌈 哎呀! 🌈 我們的回覆被一隻獨角獸吃掉了!請重新試一次吧! ", - "clickToRetry": "點擊重試", - "regenerateAnswer": "重新產生", - "question1": "如何使用 Kanban 來管理任務", - "question2": "解釋 GTD 工作法", - "question3": "為什麼要使用 Rust", - "question4": "清冰箱食譜", - "aiMistakePrompt": "AI 可能會犯錯,請檢查重要資訊。", - "clickToMention": "單擊以提及頁面", - "uploadFile": "上傳 PDF、md 或 txt 檔案以進行聊天", - "questionDetail": "{} 你好!我能為您提供什麼幫助?" - }, "trash": { "text": "垃圾桶", "restoreAll": "全部還原", @@ -216,14 +175,14 @@ "questionBubble": { "shortcuts": "快捷鍵", "whatsNew": "有什麼新功能?", + "help": "幫助 & 支援", "markdown": "Markdown", "debug": { "name": "除錯資訊", "success": "已將除錯資訊複製到剪貼簿!", "fail": "無法將除錯資訊複製到剪貼簿" }, - "feedback": "意見回饋", - "help": "幫助 & 支援" + "feedback": "意見回饋" }, "menuAppHeader": { "moreButtonToolTip": "移除、重新命名等等...", @@ -260,7 +219,6 @@ "viewDataBase": "檢視資料庫", "referencePage": "這個 {name} 已被引用", "addBlockBelow": "在下方新增一個區塊", - "aiGenerate": "產生", "genSummary": "產成摘要" }, "sideBar": { @@ -277,29 +235,7 @@ "addAPage": "新增頁面", "addAPageToPrivate": "新增頁面到私人空間", "addAPageToWorkspace": "將頁面新增至工作區", - "recent": "最近", - "today": "今天", - "thisWeek": "本週", - "justNow": "剛才", - "minutesAgo": "{count} 分鐘前", - "lastViewed": "上次查看", - "favoriteAt": "已收藏", - "emptyRecent": "沒有最近的頁面", - "emptyFavorite": "沒有收藏的頁面", - "emptyFavoriteDescription": "將頁面標記為收藏 - 它們將列在此處以便您可以快速存取!", - "removePageFromRecent": "要從最近的頁面中刪除此頁面嗎?", - "removeSuccess": "刪除成功", - "favoriteSpace": "收藏", - "RecentSpace": "最近的", - "Spaces": "空間", - "upgradeToPro": "升級到 Pro", - "upgradeToAIMax": "解鎖無限 AI", - "storageLimitDialogTitle": "您的免費儲存空間已用完,升級以解鎖無限儲存空間", - "storageLimitDialogTitleIOS": "您的免費儲存空間已用完。", - "aiResponseLimitTitle": "您的免費 AI 回覆已用完,升級到 Pro 或購買 AI 附加方案以解鎖無限回覆", - "aiResponseLimitDialogTitle": "AI 回覆已達到限制", - "purchaseStorageSpace": "購買儲存空間", - "purchaseAIResponse": "購買" + "recent": "最近" }, "notifications": { "export": { @@ -338,15 +274,11 @@ "update": "更新", "share": "分享", "removeFromFavorites": "從最愛中移除", - "removeFromRecent": "從最近刪除", "addToFavorites": "加入最愛", - "favoriteSuccessfully": "收藏成功", - "duplicateSuccessfully": "複製成功", "rename": "重新命名", "helpCenter": "支援中心", "add": "新增", "yes": "是", - "no": "否", "clear": "清除", "remove": "刪除", "dontRemove": "不要刪除", @@ -359,13 +291,6 @@ "signInGoogle": "使用Google 登入", "signInGithub": "使用Github 登入", "signInDiscord": "使用Discord 登入", - "more": "更多", - "create": "新增", - "close": "關閉", - "next": "下一個", - "previous": "上一個", - "download": "下載", - "backToHome": "回首頁", "tryAGain": "再試一次" }, "label": { @@ -390,14 +315,10 @@ }, "settings": { "title": "設定", - "popupMenuItem": { - "settings": "設定", - "members": "成員", - "trash": "垃圾桶" - }, "accountPage": { "menuLabel": "我的帳號", "title": "我的帳號", + "description": "自訂您的個人資料、管理帳戶安全性和 AI API 金鑰,或登入您的帳號", "general": { "title": "帳號名稱和個人資料圖片", "changeProfilePicture": "更改個人資料圖片" @@ -408,12 +329,20 @@ "change": "更改電子郵件" } }, + "keys": { + "title": "AI API 金鑰", + "openAILabel": "Open AI API 金鑰", + "openAITooltip": "以OpenAI API 金鑰使用AI 模型", + "openAIHint": "輸入您的 OpenAI API 金鑰", + "stabilityAILabel": "Stability API 金鑰", + "stabilityAITooltip": "以Stability API 金鑰使用AI 模型", + "stabilityAIHint": "輸入您的Stability API 金鑰" + }, "login": { "title": "帳號登入", "loginLabel": "登入", "logoutLabel": "登出" - }, - "description": "自訂您的個人資料、管理帳戶安全性和 AI API 金鑰,或登入您的帳號" + } }, "menu": { "appearance": "外觀", @@ -434,6 +363,11 @@ "cloudServerType": "雲端伺服器種類", "cloudServerTypeTip": "請注意,切換雲端伺服器後可能會登出您目前的帳號", "cloudLocal": "本地", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase 網址", + "cloudSupabaseUrlCanNotBeEmpty": "Supabase 網址不能為空", + "cloudSupabaseAnonKey": "Supabase 匿名金鑰", + "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase 網址不為空,則匿名金鑰不得為空", "cloudAppFlowy": "@:appName 雲端測試版 (Beta)", "cloudAppFlowySelfHost": "自架 @:appName 雲端伺服器", "appFlowyCloudUrlCanNotBeEmpty": "雲端網址不能為空", @@ -462,7 +396,8 @@ "importAppFlowyDataDescription": "從外部 @:appName 資料夾複製資料並匯入到目前的 @:appName 資料夾", "importSuccess": "成功匯入 @:appName 資料夾", "importFailed": "匯入 @:appName 資料夾失敗", - "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件" + "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件", + "supabaseSetting": "supabase 設定" }, "notifications": { "enableNotifications": { @@ -607,9 +542,23 @@ "email": "電子郵件", "tooltipSelectIcon": "選擇圖示", "selectAnIcon": "選擇圖示", - "pleaseInputYourOpenAIKey": "請輸入您的 AI 金鑰", - "clickToLogout": "點選以登出目前使用者", - "pleaseInputYourStabilityAIKey": "請輸入您的 Stability AI 金鑰" + "pleaseInputYourOpenAIKey": "請輸入您的 OpenAI 金鑰", + "pleaseInputYourStabilityAIKey": "請輸入您的 Stability AI 金鑰", + "clickToLogout": "點選以登出目前使用者" + }, + "shortcuts": { + "shortcutsLabel": "快捷鍵", + "command": "指令", + "keyBinding": "鍵盤綁定", + "addNewCommand": "新增指令", + "updateShortcutStep": "按下您想要的鍵盤組合並按下 ENTER", + "shortcutIsAlreadyUsed": "此快捷鍵已被使用於:{conflict}", + "resetToDefault": "重設為預設鍵盤綁定", + "couldNotLoadErrorMsg": "無法載入快捷鍵,請再試一次", + "couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次", + "commands": { + "textAlignRight": "向右對齊文字" + } }, "mobile": { "personalInfo": "個人資料", @@ -627,20 +576,6 @@ "selectLayout": "選擇版面配置", "selectStartingDay": "選擇一週的起始日", "version": "版本" - }, - "shortcuts": { - "shortcutsLabel": "快捷鍵", - "command": "指令", - "keyBinding": "鍵盤綁定", - "addNewCommand": "新增指令", - "updateShortcutStep": "按下您想要的鍵盤組合並按下 ENTER", - "shortcutIsAlreadyUsed": "此快捷鍵已被使用於:{conflict}", - "resetToDefault": "重設為預設鍵盤綁定", - "couldNotLoadErrorMsg": "無法載入快捷鍵,請再試一次", - "couldNotSaveErrorMsg": "無法儲存快捷鍵,請再試一次", - "commands": { - "textAlignRight": "向右對齊文字" - } } }, "grid": { @@ -838,7 +773,8 @@ "url": { "launch": "在瀏覽器中開啟", "copy": "複製網址", - "textFieldHint": "輸入網址" + "textFieldHint": "輸入網址", + "copiedNotification": "已複製到剪貼簿" }, "menuName": "網格", "referencedGridPrefix": "檢視", @@ -884,23 +820,23 @@ "referencedGrid": "已連結的網格", "referencedCalendar": "已連結的日曆", "referencedDocument": "已連結的文件", - "autoGeneratorMenuItemName": "AI 寫手", - "autoGeneratorTitleName": "AI:讓 AI 撰寫任何內容……", + "autoGeneratorMenuItemName": "OpenAI 寫手", + "autoGeneratorTitleName": "OpenAI:讓 AI 撰寫任何內容……", "autoGeneratorLearnMore": "瞭解更多", "autoGeneratorGenerate": "產生", - "autoGeneratorHintText": "問 AI……", - "autoGeneratorCantGetOpenAIKey": "無法取得 AI 金鑰", + "autoGeneratorHintText": "問 OpenAI……", + "autoGeneratorCantGetOpenAIKey": "無法取得 OpenAI 金鑰", "autoGeneratorRewrite": "改寫", "smartEdit": "AI 助理", - "aI": "AI", + "openAI": "OpenAI", "smartEditFixSpelling": "修正拼寫", "warning": "⚠️ AI 的回覆可能不準確或具有誤導性。", "smartEditSummarize": "總結", "smartEditImproveWriting": "提高寫作水準", "smartEditMakeLonger": "做得更長", - "smartEditCouldNotFetchResult": "無法取得 AI 的結果", - "smartEditCouldNotFetchKey": "無法取得 AI 金鑰", - "smartEditDisabled": "在設定連結 AI ", + "smartEditCouldNotFetchResult": "無法取得 OpenAI 的結果", + "smartEditCouldNotFetchKey": "無法取得 OpenAI 金鑰", + "smartEditDisabled": "在設定連結 OpenAI ", "discardResponse": "確定捨棄 AI 的回覆?", "createInlineMathEquation": "建立公式", "fonts": "字型", @@ -958,8 +894,8 @@ "defaultColor": "預設" }, "image": { - "addAnImage": "新增圖片", "copiedToPasteBoard": "圖片連結已複製到剪貼簿", + "addAnImage": "新增圖片", "imageUploadFailed": "圖片上傳失敗", "errorCode": "錯誤代碼" }, @@ -1015,8 +951,8 @@ "placeholder": "輸入圖片網址" }, "ai": { - "label": "由 AI 生成圖片", - "placeholder": "請輸入提示讓 AI 生成圖片" + "label": "由 OpenAI 生成圖片", + "placeholder": "請輸入提示讓 OpenAI 生成圖片" }, "stability_ai": { "label": "由 Stability AI 生成圖片", @@ -1038,15 +974,15 @@ "label": "Unsplash" }, "searchForAnImage": "搜尋圖片", - "pleaseInputYourOpenAIKey": "請在設定頁面輸入您的 AI 金鑰", + "pleaseInputYourOpenAIKey": "請在設定頁面輸入您的 OpenAI 金鑰", + "pleaseInputYourStabilityAIKey": "請在設定頁面輸入您的 Stability AI 金鑰", "saveImageToGallery": "儲存圖片", "failedToAddImageToGallery": "無法將圖片新增到相簿", "successToAddImageToGallery": "圖片已成功新增到相簿", "unableToLoadImage": "無法載入圖片", "maximumImageSize": "支援的最大上傳圖片大小為 10MB", "uploadImageErrorImageSizeTooBig": "圖片大小必須小於 10MB", - "imageIsUploading": "圖片上傳中", - "pleaseInputYourStabilityAIKey": "請在設定頁面輸入您的 Stability AI 金鑰" + "imageIsUploading": "圖片上傳中" }, "codeBlock": { "language": { @@ -1531,9 +1467,9 @@ "photo": "圖片", "pageCover": "封面", "none": "無", + "photoPermissionDescription": "允許存取圖片庫以上傳圖片", "openSettings": "打開設定", "photoPermissionTitle": "@:appName 希望存取您的圖片庫", - "photoPermissionDescription": "允許存取圖片庫以上傳圖片", "doNotAllow": "不允許" }, "commandPalette": { @@ -1543,4 +1479,4 @@ "betaLabel": "BETA", "betaTooltip": "目前我們只支援搜尋頁面" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/.vscode/launch.json b/frontend/rust-lib/.vscode/launch.json deleted file mode 100644 index 3b1e6e62a7..0000000000 --- a/frontend/rust-lib/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "AF-desktop: Debug Rust", - "type": "lldb", - // "request": "attach", - // "pid": "${command:pickMyProcess}" - // To launch the application directly, use the following configuration: - "request": "launch", - "program": "/Users/lucas.xu/Desktop/appflowy_backup/frontend/appflowy_flutter/build/macos/Build/Products/Debug/AppFlowy.app", - }, - ] - } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 51a3f1a3b2..71fa48ce99 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -11,285 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.4.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0346d8c1f762b41b458ed3145eea914966bb9ad20b9be0d6d463b20d45586370" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "129d4c88e98860e1758c5de288d1632b07970a16d59bdf7b8d66053d582bb71f" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "ahash 0.8.6", - "base64 0.21.5", - "bitflags 2.4.0", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2 0.3.21", - "http 0.2.9", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd 0.13.2", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.9", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6398974fd4284f4768af07965701efbbb5fdc0616bff20cade1bb14b77675e24" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio 1.0.3", - "socket2 0.5.5", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots 0.22.6", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e43428f3bf11dee6d166b00ec2df4e3aa8cc1606aaa0b7433c146852e2f4e03b" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "ahash 0.8.6", - "bytes", - "bytestring", - "cfg-if", - "cookie 0.16.2", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.5", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-web-lab" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6" -dependencies = [ - "actix-http", - "actix-router", - "actix-service", - "actix-utils", - "actix-web", - "actix-web-lab-derive", - "ahash 0.8.6", - "arc-swap", - "async-trait", - "bytes", - "bytestring", - "csv", - "derive_more", - "futures-core", - "futures-util", - "http 0.2.9", - "impl-more", - "itertools 0.12.1", - "local-channel", - "mediatype", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "serde", - "serde_html_form", - "serde_json", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "actix-web-lab-derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "actix-ws" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535aec173810be3ca6f25dd5b4d431ae7125d62000aa3cbae1ec739921b02cf3" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "futures-core", - "tokio", + "syn 2.0.47", ] [[package]] @@ -319,9 +41,9 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -342,58 +64,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "af-local-ai" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "af-plugin", - "anyhow", - "bytes", - "reqwest 0.11.27", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "af-mcp" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "anyhow", - "futures-util", - "mcp_daemon", - "serde", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "af-plugin" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa#093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" -dependencies = [ - "anyhow", - "cfg-if", - "crossbeam-utils", - "log", - "once_cell", - "parking_lot 0.12.1", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tracing", - "winreg 0.55.0", - "xattr", -] - [[package]] name = "again" version = "0.1.2" @@ -486,23 +156,23 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", "bincode", "getrandom 0.2.10", - "reqwest 0.12.15", + "reqwest", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", + "thiserror", "tokio", "tsify", "url", @@ -513,7 +183,7 @@ dependencies = [ [[package]] name = "appflowy-ai-client" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", "bytes", @@ -521,17 +191,7 @@ dependencies = [ "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", - "uuid", -] - -[[package]] -name = "arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" -dependencies = [ - "derive_arbitrary", + "thiserror", ] [[package]] @@ -556,85 +216,11 @@ dependencies = [ "serde_json", ] -[[package]] -name = "async-compression" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" -dependencies = [ - "bzip2", - "deflate64", - "flate2", - "futures-core", - "futures-io", - "memchr", - "pin-project-lite", - "xz2", - "zstd 0.13.2", - "zstd-safe 7.2.0", -] - -[[package]] -name = "async-convert" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" -dependencies = [ - "async-trait", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-openai" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc0b1877fb1bc415caa14d1899f0f477e8eb38f2fe16f54be196d7c4a92e15c" -dependencies = [ - "async-convert", - "backoff", - "base64 0.21.5", - "bytes", - "derive_builder 0.12.0", - "futures", - "rand 0.8.5", - "reqwest 0.11.27", - "reqwest-eventsource", - "secrecy", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "async-stream" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -643,40 +229,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.6" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "async_zip" -version = "0.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" -dependencies = [ - "async-compression", - "chrono", - "crc32fast", - "futures-lite", - "pin-project", - "thiserror 1.0.64", - "tokio", - "tokio-util", + "syn 2.0.47", ] [[package]] @@ -685,12 +255,6 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "atomic_refcell" version = "0.1.11" @@ -714,9 +278,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http 0.2.9", - "http-body 0.4.5", - "hyper 0.14.27", + "http", + "http-body", + "hyper", "itoa", "matchit", "memchr", @@ -725,8 +289,8 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper 0.1.2", - "tower 0.4.13", + "sync_wrapper", + "tower", "tower-layer", "tower-service", ] @@ -740,28 +304,14 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 0.2.9", - "http-body 0.4.5", + "http", + "http-body", "mime", "rustversion", "tower-layer", "tower-service", ] -[[package]] -name = "backoff" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" -dependencies = [ - "futures-core", - "getrandom 0.2.10", - "instant", - "pin-project-lite", - "rand 0.8.5", - "tokio", -] - [[package]] name = "backtrace" version = "0.3.69" @@ -789,12 +339,6 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "base64ct" version = "1.6.0" @@ -812,22 +356,23 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ - "bitflags 2.4.0", + "bitflags 1.3.2", "cexpr", "clang-sys", - "itertools 0.12.1", "lazy_static", "lazycell", + "peeking_take_while", + "prettyplease", "proc-macro2", "quote", "regex", - "rustc-hash 1.1.0", + "rustc-hash", "shlex", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -859,9 +404,9 @@ checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bitpacking" -version = "0.9.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" dependencies = [ "crunchy", ] @@ -887,53 +432,49 @@ dependencies = [ "generic-array", ] -[[package]] -name = "bon" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c5f8abc69af414cbd6f2103bb668b91e584072f2105e4b38bed79b6ad0975f" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69edf39b6f321cb2699a93fc20c256adb839719c42676d03f7aa975e4e5581d" -dependencies = [ - "darling 0.20.11", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.94", -] - [[package]] name = "borsh" -version = "1.5.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" +checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive", - "cfg_aliases", + "hashbrown 0.13.2", ] [[package]] name = "borsh-derive" -version = "1.5.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" +checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7" dependencies = [ - "once_cell", + "borsh-derive-internal", + "borsh-schema-derive-internal", "proc-macro-crate", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb438156919598d2c7bad7e1c0adf3d26ed3840dbc010db1a882a65583ca2fb" +dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", - "syn_derive", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634205cc43f74a1b9046ef87c4540ebda95696ec0f315024860cad7c5b0f5ccd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", ] [[package]] @@ -969,9 +510,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytecheck" @@ -1003,22 +544,13 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.9.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" dependencies = [ "serde", ] -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] - [[package]] name = "bzip2" version = "0.4.4" @@ -1071,17 +603,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1089,7 +615,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-targets 0.52.0", ] [[package]] @@ -1099,18 +625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" dependencies = [ "chrono", - "chrono-tz-build 0.2.0", - "phf 0.11.2", -] - -[[package]] -name = "chrono-tz" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6" -dependencies = [ - "chrono", - "chrono-tz-build 0.4.0", + "chrono-tz-build", "phf 0.11.2", ] @@ -1125,16 +640,6 @@ dependencies = [ "phf_codegen 0.11.2", ] -[[package]] -name = "chrono-tz-build" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" -dependencies = [ - "parse-zoneinfo", - "phf_codegen 0.11.2", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1159,96 +664,75 @@ dependencies = [ [[package]] name = "client-api" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "again", "anyhow", "app-error", - "arc-swap", "async-trait", - "base64 0.22.1", + "bincode", "brotli", "bytes", "chrono", - "client-api-entity", "client-websocket", "collab", + "collab-entity", "collab-rt-entity", "collab-rt-protocol", - "futures", + "database-entity", "futures-core", "futures-util", "getrandom 0.2.10", "gotrue", - "infra", - "lazy_static", - "md5", + "gotrue-entity", "mime", - "mime_guess", "parking_lot 0.12.1", - "percent-encoding", - "pin-project", - "prost 0.13.3", - "rayon", - "reqwest 0.12.15", + "prost", + "reqwest", "scraper 0.17.1", "semver", "serde", "serde_json", + "serde_repr", "serde_urlencoded", "shared-entity", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-retry", "tokio-stream", - "tokio-tungstenite 0.20.1", "tokio-util", "tracing", "url", "uuid", "wasm-bindgen-futures", "yrs", - "zstd 0.13.2", -] - -[[package]] -name = "client-api-entity" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" -dependencies = [ - "collab-entity", - "collab-rt-entity", - "database-entity", - "gotrue-entity", - "shared-entity", - "uuid", ] [[package]] name = "client-websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "futures-channel", "futures-util", + "http", "httparse", "js-sys", "percent-encoding", - "thiserror 1.0.64", + "thiserror", "tokio", - "tokio-tungstenite 0.20.1", + "tokio-tungstenite", "wasm-bindgen", "web-sys", ] [[package]] name = "cmd_lib" -version = "1.9.5" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "371c15a3c178d0117091bd84414545309ca979555b1aad573ef591ad58818d41" +checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62" dependencies = [ "cmd_lib_macros", - "env_logger 0.10.2", "faccess", "lazy_static", "log", @@ -1257,33 +741,32 @@ dependencies = [ [[package]] name = "cmd_lib_macros" -version = "1.9.5" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb844bd05be34d91eb67101329aeba9d3337094c04fd8507d821db7ebb488eaf" +checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0" dependencies = [ - "proc-macro-error2", + "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.94", + "syn 1.0.109", ] [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", - "arc-swap", "async-trait", "bincode", "bytes", "chrono", "js-sys", - "lazy_static", + "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -1295,58 +778,47 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", "async-trait", - "base64 0.22.1", "chrono", - "chrono-tz 0.10.0", "collab", "collab-entity", - "csv", - "dashmap 5.5.3", - "fancy-regex 0.13.0", - "futures", + "collab-plugins", + "dashmap", "getrandom 0.2.10", - "iana-time-zone", "js-sys", "lazy_static", "nanoid", - "percent-encoding", + "parking_lot 0.12.1", "rayon", - "rust_decimal", - "rusty-money", "serde", "serde_json", "serde_repr", - "sha2", "strum", "strum_macros 0.25.2", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-stream", - "tokio-util", "tracing", "uuid", - "yrs", ] [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", - "arc-swap", "collab", "collab-entity", "getrandom 0.2.10", - "markdown", "nanoid", + "parking_lot 0.12.1", "serde", "serde_json", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-stream", "tracing", @@ -1356,82 +828,36 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", "bytes", "collab", "getrandom 0.2.10", - "prost 0.13.3", - "prost-build", - "protoc-bin-vendored", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", "uuid", - "walkdir", ] [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", - "arc-swap", "chrono", "collab", "collab-entity", - "dashmap 5.5.3", "getrandom 0.2.10", + "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-stream", "tracing", - "uuid", -] - -[[package]] -name = "collab-importer" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" -dependencies = [ - "anyhow", - "async-recursion", - "async-trait", - "async_zip", - "base64 0.22.1", - "chrono", - "collab", - "collab-database", - "collab-document", - "collab-entity", - "collab-folder", - "csv", - "fancy-regex 0.13.0", - "futures", - "futures-lite", - "futures-util", - "fxhash", - "hex", - "markdown", - "percent-encoding", - "rayon", - "sanitize-filename", - "serde", - "serde_json", - "sha2", - "thiserror 1.0.64", - "tokio", - "tokio-util", - "tracing", - "uuid", - "walkdir", - "zip 0.6.6", ] [[package]] @@ -1439,29 +865,23 @@ name = "collab-integrate" version = "0.1.0" dependencies = [ "anyhow", - "arc-swap", + "async-trait", "collab", - "collab-database", - "collab-document", "collab-entity", - "collab-folder", "collab-plugins", - "collab-user", - "diesel", - "flowy-error", - "flowy-sqlite", + "futures", "lib-infra", + "parking_lot 0.12.1", "serde", "serde_json", "tokio", "tracing", - "uuid", ] [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", "async-stream", @@ -1477,13 +897,14 @@ dependencies = [ "indexed_db_futures", "js-sys", "lazy_static", + "parking_lot 0.12.1", "rand 0.8.5", "rocksdb", "serde", "serde_json", "similar 2.2.1", "smallvec", - "thiserror 1.0.64", + "thiserror", "tokio", "tokio-retry", "tokio-stream", @@ -1499,29 +920,32 @@ dependencies = [ [[package]] name = "collab-rt-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", "bincode", "bytes", "chrono", + "client-websocket", "collab", "collab-entity", "collab-rt-protocol", "database-entity", - "prost 0.13.3", + "prost", "prost-build", "protoc-bin-vendored", "serde", + "serde_json", "serde_repr", - "tokio-tungstenite 0.20.1", + "thiserror", + "tokio-tungstenite", "yrs", ] [[package]] name = "collab-rt-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", "async-trait", @@ -1529,22 +953,22 @@ dependencies = [ "collab", "collab-entity", "serde", - "thiserror 1.0.64", + "thiserror", "tokio", "tracing", - "uuid", "yrs", ] [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3#f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=3a58d95#3a58d95a202b2814920650fa71c458fb0b49293d" dependencies = [ "anyhow", "collab", "collab-entity", "getrandom 0.2.10", + "parking_lot 0.12.1", "serde", "serde_json", "tokio", @@ -1552,15 +976,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.14.1" @@ -1583,7 +998,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd326812b3fd01da5bb1af7d340d0d555fd3d4b641e7f1dfcf5962a902952787" dependencies = [ "futures-core", - "prost 0.12.3", + "prost", "prost-types", "tonic", "tracing-core", @@ -1619,34 +1034,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" -[[package]] -name = "constant_time_eq" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" -version = "0.16.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cookie" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ "percent-encoding", "time", @@ -1655,13 +1047,12 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.21.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" dependencies = [ - "cookie 0.18.1", - "document-features", - "idna 1.0.3", + "cookie", + "idna 0.3.0", "log", "publicsuffix", "serde", @@ -1696,26 +1087,11 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] @@ -1756,9 +1132,12 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] [[package]] name = "crunchy" @@ -1786,7 +1165,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1797,14 +1176,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "csv" -version = "1.3.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" dependencies = [ "csv-core", "itoa", @@ -1814,9 +1193,9 @@ dependencies = [ [[package]] name = "csv-core" -version = "0.1.11" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" dependencies = [ "memchr", ] @@ -1830,76 +1209,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core 0.14.4", - "darling_macro 0.14.4", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling_core" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn 1.0.109", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.94", -] - -[[package]] -name = "darling_macro" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" -dependencies = [ - "darling_core 0.14.4", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.94", -] - [[package]] name = "dart-ffi" version = "0.1.0" @@ -1910,6 +1219,7 @@ dependencies = [ "collab-integrate", "crossbeam-utils", "flowy-codegen", + "flowy-config", "flowy-core", "flowy-date", "flowy-derive", @@ -1919,10 +1229,10 @@ dependencies = [ "flowy-server", "flowy-server-pub", "flowy-user", - "futures", "lazy_static", "lib-dispatch", "lib-log", + "parking_lot 0.12.1", "protobuf", "semver", "serde", @@ -1946,20 +1256,6 @@ dependencies = [ "parking_lot_core 0.9.8", ] -[[package]] -name = "dashmap" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.3", - "lock_api", - "once_cell", - "parking_lot_core 0.9.8", -] - [[package]] name = "data-encoding" version = "2.4.0" @@ -1969,21 +1265,20 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ + "anyhow", + "app-error", "bincode", - "bytes", "chrono", "collab-entity", - "infra", - "prost 0.13.3", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", + "thiserror", "tracing", "uuid", - "validator 0.19.0", + "validator", ] [[package]] @@ -1996,12 +1291,6 @@ dependencies = [ "regex", ] -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - [[package]] name = "delegate-display" version = "2.1.1" @@ -2011,16 +1300,15 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "deranged" -version = "0.3.11" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ - "powerfmt", "serde", ] @@ -2035,89 +1323,14 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "derive_arbitrary" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "derive_builder" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" -dependencies = [ - "derive_builder_macro 0.12.0", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro 0.20.2", -] - -[[package]] -name = "derive_builder_core" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" -dependencies = [ - "darling 0.14.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "derive_builder_macro" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" -dependencies = [ - "derive_builder_core 0.12.0", - "syn 1.0.109", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core 0.20.2", - "syn 2.0.94", -] - [[package]] name = "derive_more" version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", "proc-macro2", "quote", - "rustc_version", "syn 1.0.109", ] @@ -2156,7 +1369,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -2176,7 +1389,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -2190,26 +1403,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "document-features" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" -dependencies = [ - "litrs", -] - [[package]] name = "dotenv" version = "0.15.0" @@ -2218,9 +1411,9 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" [[package]] name = "downcast-rs" -version = "2.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "dtoa" @@ -2263,9 +1456,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -2280,19 +1473,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -2313,6 +1493,7 @@ dependencies = [ name = "event-integration-test" version = "0.1.0" dependencies = [ + "anyhow", "assert-json-diff", "bytes", "chrono", @@ -2321,11 +1502,16 @@ dependencies = [ "collab-document", "collab-entity", "collab-folder", - "flowy-ai", - "flowy-ai-pub", + "collab-plugins", + "dotenv", + "flowy-chat", + "flowy-chat-pub", "flowy-core", + "flowy-database-pub", "flowy-database2", "flowy-document", + "flowy-document-pub", + "flowy-encrypt", "flowy-folder", "flowy-folder-pub", "flowy-notification", @@ -2333,56 +1519,28 @@ dependencies = [ "flowy-server", "flowy-server-pub", "flowy-storage", - "flowy-storage-pub", "flowy-user", "flowy-user-pub", "futures", + "futures-util", "lib-dispatch", "lib-infra", "nanoid", + "parking_lot 0.12.1", "protobuf", "rand 0.8.5", "semver", "serde", "serde_json", "strum", + "tempdir", + "thread-id", "tokio", + "tokio-postgres", "tracing", "uuid", "walkdir", - "zip 2.2.0", -] - -[[package]] -name = "event-listener" -version = "5.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "eventsource-stream" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" -dependencies = [ - "futures-core", - "nom", - "pin-project-lite", + "zip", ] [[package]] @@ -2406,6 +1564,12 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fancy-regex" version = "0.10.0" @@ -2426,17 +1590,6 @@ dependencies = [ "regex", ] -[[package]] -name = "fancy-regex" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" -dependencies = [ - "bit-set", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", -] - [[package]] name = "fancy_constructor" version = "1.2.2" @@ -2446,7 +1599,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -2465,16 +1618,10 @@ dependencies = [ ] [[package]] -name = "filetime" -version = "0.2.23" +name = "finl_unicode" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.4.1", - "windows-sys 0.52.0", -] +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] name = "fixedbitset" @@ -2484,73 +1631,14 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", ] -[[package]] -name = "flowy-ai" -version = "0.1.0" -dependencies = [ - "af-local-ai", - "af-mcp", - "af-plugin", - "allo-isolate", - "anyhow", - "arc-swap", - "base64 0.21.5", - "bytes", - "collab-integrate", - "dashmap 6.0.1", - "dotenv", - "flowy-ai-pub", - "flowy-codegen", - "flowy-derive", - "flowy-error", - "flowy-notification", - "flowy-sqlite", - "flowy-storage-pub", - "futures", - "futures-util", - "lib-dispatch", - "lib-infra", - "log", - "notify", - "pin-project", - "protobuf", - "reqwest 0.11.27", - "serde", - "serde_json", - "sha2", - "simsimd", - "strum_macros 0.21.1", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "tracing-subscriber", - "uuid", - "validator 0.18.1", -] - -[[package]] -name = "flowy-ai-pub" -version = "0.1.0" -dependencies = [ - "client-api", - "flowy-error", - "flowy-sqlite", - "futures", - "lib-infra", - "serde", - "serde_json", - "uuid", -] - [[package]] name = "flowy-ast" version = "0.1.0" @@ -2560,6 +1648,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "flowy-chat" +version = "0.1.0" +dependencies = [ + "allo-isolate", + "bytes", + "dashmap", + "flowy-chat-pub", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-notification", + "flowy-sqlite", + "futures", + "lib-dispatch", + "lib-infra", + "log", + "protobuf", + "strum_macros 0.21.1", + "tokio", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-chat-pub" +version = "0.1.0" +dependencies = [ + "bytes", + "client-api", + "flowy-error", + "futures", + "lib-infra", +] + [[package]] name = "flowy-codegen" version = "0.1.0" @@ -2584,27 +1708,37 @@ dependencies = [ "walkdir", ] +[[package]] +name = "flowy-config" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", +] + [[package]] name = "flowy-core" version = "0.1.0" dependencies = [ - "af-local-ai", - "af-plugin", "anyhow", - "arc-swap", "base64 0.21.5", "bytes", "client-api", "collab", "collab-entity", - "collab-folder", "collab-integrate", "collab-plugins", "console-subscriber", - "dashmap 6.0.1", "diesel", - "flowy-ai", - "flowy-ai-pub", + "flowy-chat", + "flowy-chat-pub", + "flowy-config", "flowy-database-pub", "flowy-database2", "flowy-date", @@ -2619,13 +1753,14 @@ dependencies = [ "flowy-server-pub", "flowy-sqlite", "flowy-storage", - "flowy-storage-pub", "flowy-user", "flowy-user-pub", + "futures", "futures-core", "lib-dispatch", "lib-infra", "lib-log", + "parking_lot 0.12.1", "semver", "serde", "serde_json", @@ -2634,20 +1769,19 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "url", "uuid", + "walkdir", ] [[package]] name = "flowy-database-pub" version = "0.1.0" dependencies = [ + "anyhow", "client-api", "collab", "collab-entity", - "flowy-error", "lib-infra", - "uuid", ] [[package]] @@ -2655,24 +1789,22 @@ name = "flowy-database2" version = "0.1.0" dependencies = [ "anyhow", - "arc-swap", "async-stream", "async-trait", "bytes", "chrono", - "chrono-tz 0.8.3", + "chrono-tz", "collab", "collab-database", "collab-entity", "collab-integrate", "collab-plugins", "csv", - "dashmap 6.0.1", + "dashmap", "event-integration-test", "fancy-regex 0.11.0", "flowy-codegen", "flowy-database-pub", - "flowy-database2", "flowy-derive", "flowy-error", "flowy-notification", @@ -2681,8 +1813,8 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "moka", "nanoid", + "parking_lot 0.12.1", "protobuf", "rayon", "rust_decimal", @@ -2693,11 +1825,9 @@ dependencies = [ "strum", "strum_macros 0.25.2", "tokio", - "tokio-util", "tracing", "url", - "uuid", - "validator 0.18.1", + "validator", ] [[package]] @@ -2721,7 +1851,7 @@ dependencies = [ name = "flowy-derive" version = "0.1.0" dependencies = [ - "dashmap 6.0.1", + "dashmap", "flowy-ast", "flowy-codegen", "lazy_static", @@ -2743,19 +1873,20 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", - "dashmap 6.0.1", + "dashmap", "flowy-codegen", "flowy-derive", "flowy-document-pub", "flowy-error", "flowy-notification", - "flowy-storage-pub", + "flowy-storage", "futures", "getrandom 0.2.10", "indexmap 2.1.0", "lib-dispatch", "lib-infra", "nanoid", + "parking_lot 0.12.1", "protobuf", "scraper 0.18.1", "serde", @@ -2767,18 +1898,32 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", - "validator 0.18.1", + "validator", ] [[package]] name = "flowy-document-pub" version = "0.1.0" dependencies = [ + "anyhow", "collab", "collab-document", "flowy-error", "lib-infra", - "uuid", +] + +[[package]] +name = "flowy-encrypt" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.21.5", + "getrandom 0.2.10", + "hmac", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2", ] [[package]] @@ -2788,7 +1933,6 @@ dependencies = [ "anyhow", "bytes", "client-api", - "collab", "collab-database", "collab-document", "collab-folder", @@ -2800,34 +1944,30 @@ dependencies = [ "lib-dispatch", "protobuf", "r2d2", - "reqwest 0.11.27", + "reqwest", "serde", "serde_json", "serde_repr", "tantivy", - "thiserror 1.0.64", + "thiserror", "tokio", "url", - "uuid", - "validator 0.18.1", + "validator", ] [[package]] name = "flowy-folder" version = "0.1.0" dependencies = [ - "arc-swap", "async-trait", "bytes", "chrono", - "client-api", "collab", "collab-document", "collab-entity", "collab-folder", "collab-integrate", "collab-plugins", - "dashmap 6.0.1", "flowy-codegen", "flowy-derive", "flowy-error", @@ -2835,14 +1975,12 @@ dependencies = [ "flowy-notification", "flowy-search-pub", "flowy-sqlite", - "flowy-user-pub", - "futures", "lazy_static", "lib-dispatch", "lib-infra", "nanoid", + "parking_lot 0.12.1", "protobuf", - "regex", "serde", "serde_json", "strum_macros 0.21.1", @@ -2851,7 +1989,7 @@ dependencies = [ "tracing", "unicode-segmentation", "uuid", - "validator 0.18.1", + "validator", ] [[package]] @@ -2859,14 +1997,10 @@ name = "flowy-folder-pub" version = "0.1.0" dependencies = [ "anyhow", - "client-api", "collab", "collab-entity", "collab-folder", - "flowy-error", "lib-infra", - "serde", - "serde_json", "tokio", "uuid", ] @@ -2876,7 +2010,7 @@ name = "flowy-notification" version = "0.1.0" dependencies = [ "bytes", - "dashmap 6.0.1", + "dashmap", "flowy-codegen", "flowy-derive", "lazy_static", @@ -2892,17 +2026,20 @@ dependencies = [ name = "flowy-search" version = "0.1.0" dependencies = [ - "allo-isolate", "async-stream", "bytes", "collab", "collab-folder", - "derive_builder 0.20.2", + "diesel", + "diesel_derives", + "diesel_migrations", "flowy-codegen", "flowy-derive", "flowy-error", "flowy-folder", + "flowy-notification", "flowy-search-pub", + "flowy-sqlite", "flowy-user", "futures", "lib-dispatch", @@ -2910,13 +2047,13 @@ dependencies = [ "protobuf", "serde", "serde_json", - "strsim 0.11.1", + "strsim", "strum_macros 0.26.1", "tantivy", + "tempfile", "tokio", - "tokio-stream", "tracing", - "uuid", + "validator", ] [[package]] @@ -2927,8 +2064,8 @@ dependencies = [ "collab", "collab-folder", "flowy-error", + "futures", "lib-infra", - "uuid", ] [[package]] @@ -2936,46 +2073,51 @@ name = "flowy-server" version = "0.1.0" dependencies = [ "anyhow", - "arc-swap", "assert-json-diff", "bytes", "chrono", "client-api", "collab", - "collab-database", "collab-document", "collab-entity", "collab-folder", "collab-plugins", - "collab-user", "dotenv", - "flowy-ai", - "flowy-ai-pub", + "flowy-chat-pub", "flowy-database-pub", "flowy-document-pub", + "flowy-encrypt", "flowy-error", "flowy-folder-pub", "flowy-search-pub", "flowy-server-pub", - "flowy-sqlite", "flowy-storage", - "flowy-storage-pub", "flowy-user-pub", "futures", "futures-util", + "hex", + "hyper", "lazy_static", + "lib-dispatch", "lib-infra", + "mime_guess", + "parking_lot 0.12.1", + "postgrest", "rand 0.8.5", + "reqwest", "semver", "serde", "serde_json", - "thiserror 1.0.64", + "thiserror", "tokio", + "tokio-retry", "tokio-stream", "tokio-util", "tracing", "tracing-subscriber", + "url", "uuid", + "yrs", ] [[package]] @@ -2998,12 +2140,13 @@ dependencies = [ "libsqlite3-sys", "openssl", "openssl-sys", + "parking_lot 0.12.1", "r2d2", "scheduled-thread-pool", "serde", "serde_json", "tempfile", - "thiserror 1.0.64", + "thiserror", "tracing", ] @@ -3011,47 +2154,19 @@ dependencies = [ name = "flowy-storage" version = "0.1.0" dependencies = [ - "allo-isolate", "async-trait", "bytes", - "chrono", - "collab-importer", - "dashmap 6.0.1", - "flowy-codegen", - "flowy-derive", - "flowy-error", - "flowy-notification", - "flowy-sqlite", - "flowy-storage-pub", - "lib-dispatch", - "lib-infra", - "mime_guess", - "protobuf", - "rand 0.8.5", - "serde", - "serde_json", - "strum_macros 0.25.2", - "tokio", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "flowy-storage-pub" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "bytes", - "client-api-entity", "flowy-error", + "fxhash", "lib-infra", "mime", "mime_guess", + "reqwest", "serde", + "serde_json", "tokio", - "uuid", + "tracing", + "url", ] [[package]] @@ -3059,11 +2174,9 @@ name = "flowy-user" version = "0.1.0" dependencies = [ "anyhow", - "arc-swap", "base64 0.21.5", "bytes", "chrono", - "client-api", "collab", "collab-database", "collab-document", @@ -3072,12 +2185,13 @@ dependencies = [ "collab-integrate", "collab-plugins", "collab-user", - "dashmap 6.0.1", "diesel", + "diesel_derives", "fake", "fancy-regex 0.11.0", "flowy-codegen", "flowy-derive", + "flowy-encrypt", "flowy-error", "flowy-folder-pub", "flowy-notification", @@ -3087,15 +2201,18 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", + "nanoid", + "once_cell", + "parking_lot 0.12.1", "protobuf", "quickcheck", "quickcheck_macros", "rand 0.8.5", "rand_core 0.6.4", - "rayon", "semver", "serde", "serde_json", + "serde_repr", "strum", "strum_macros 0.25.2", "tokio", @@ -3103,22 +2220,20 @@ dependencies = [ "tracing", "unicode-segmentation", "uuid", - "validator 0.18.1", + "validator", ] [[package]] name = "flowy-user-pub" version = "0.1.0" dependencies = [ + "anyhow", "base64 0.21.5", "chrono", - "client-api", "collab", "collab-entity", - "collab-folder", "flowy-error", "flowy-folder-pub", - "flowy-sqlite", "lib-infra", "serde", "serde_json", @@ -3161,22 +2276,19 @@ dependencies = [ [[package]] name = "fs4" -version = "0.8.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" dependencies = [ "rustix", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] -name = "fsevent-sys" -version = "4.1.0" +name = "fuchsia-cprng" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "funty" @@ -3196,9 +2308,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -3211,9 +2323,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -3221,15 +2333,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -3238,57 +2350,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -3311,6 +2404,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3426,24 +2532,28 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", + "futures-util", "getrandom 0.2.10", "gotrue-entity", "infra", - "reqwest 0.12.15", + "reqwest", "serde", "serde_json", + "tokio", "tracing", ] [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ + "anyhow", "app-error", + "chrono", "jsonwebtoken", "lazy_static", "serde", @@ -3461,7 +2571,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.9", + "http", "indexmap 1.9.3", "slab", "tokio", @@ -3469,25 +2579,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "h2" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.2.0", - "indexmap 2.1.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -3497,6 +2588,15 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.6", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -3537,9 +2637,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.5.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -3596,17 +2696,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http-body" version = "0.4.5" @@ -3614,30 +2703,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http 0.2.9", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.2.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" -dependencies = [ - "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", + "http", "pin-project-lite", ] @@ -3678,9 +2744,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.21", - "http 0.2.9", - "http-body 0.4.5", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -3692,26 +2758,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.7", - "http 1.2.0", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - [[package]] name = "hyper-rustls" version = "0.24.1" @@ -3719,29 +2765,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", - "http 0.2.9", - "hyper 0.14.27", - "rustls 0.21.7", + "http", + "hyper", + "rustls", "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" -dependencies = [ - "futures-util", - "http 1.2.0", - "hyper 1.5.2", - "hyper-util", - "rustls 0.23.20", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.1", - "tower-service", - "webpki-roots 0.26.7", + "tokio-rustls", ] [[package]] @@ -3750,7 +2778,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper 0.14.27", + "hyper", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -3763,68 +2791,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.27", + "hyper", "native-tls", "tokio", "tokio-native-tls", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.5.2", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.5.2", - "pin-project-lite", - "socket2 0.5.5", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "hyperloglogplus" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "621debdf94dcac33e50475fdd76d34d5ea9c0362a834b9db08c3024696c1fbe3" -dependencies = [ - "serde", -] - [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows 0.48.0", ] [[package]] @@ -3836,130 +2820,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.3.0" @@ -3970,6 +2830,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -3981,25 +2851,10 @@ dependencies = [ ] [[package]] -name = "idna" -version = "1.0.3" +name = "if_chain" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "ignore" @@ -4018,17 +2873,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexed_db_futures" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0704b71f13f81b5933d791abf2de26b33c40935143985220299a357721166706" +checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" dependencies = [ "accessory", "cfg-if", @@ -4065,38 +2914,13 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", - "bytes", - "futures", - "pin-project", - "reqwest 0.12.15", + "reqwest", "serde", "serde_json", - "tokio", "tracing", - "validator 0.19.0", -] - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", ] [[package]] @@ -4115,6 +2939,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -4123,17 +2950,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "itertools" version = "0.10.5" @@ -4145,18 +2961,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -4178,11 +2985,10 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ - "once_cell", "wasm-bindgen", ] @@ -4194,38 +3000,12 @@ checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ "base64 0.21.5", "pem", - "ring 0.16.20", + "ring", "serde", "serde_json", "simple_asn1", ] -[[package]] -name = "kqueue" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "lazy_static" version = "1.4.0" @@ -4258,6 +3038,7 @@ dependencies = [ "futures-util", "getrandom 0.2.10", "nanoid", + "parking_lot 0.12.1", "pin-project", "protobuf", "serde", @@ -4266,7 +3047,7 @@ dependencies = [ "thread-id", "tokio", "tracing", - "validator 0.18.1", + "validator", "wasm-bindgen", "wasm-bindgen-futures", ] @@ -4275,31 +3056,24 @@ dependencies = [ name = "lib-infra" version = "0.1.0" dependencies = [ - "aes-gcm", "allo-isolate", "anyhow", "async-trait", "atomic_refcell", - "base64 0.22.1", "brotli", "bytes", - "cfg-if", "chrono", "futures", "futures-core", - "futures-util", - "hmac", "md5", - "pbkdf2 0.12.2", "pin-project", "rand 0.8.5", - "sha2", "tempfile", "tokio", "tracing", - "validator 0.18.1", + "validator", "walkdir", - "zip 2.2.0", + "zip", ] [[package]] @@ -4320,9 +3094,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.169" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libloading" @@ -4342,8 +3116,8 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "librocksdb-sys" -version = "0.17.0+9.0.0" -source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=1710120e4549e04ba3baa6a1ee5a5a801fa45a72#1710120e4549e04ba3baa6a1ee5a5a801fa45a72" +version = "0.11.0+8.1.1" +source = "git+https://github.com/LucasXu0/rust-rocksdb?rev=21cf4a23ec131b9d82dc94e178fe8efc0c147b09#21cf4a23ec131b9d82dc94e178fe8efc0c147b09" dependencies = [ "bindgen", "bzip2-sys", @@ -4382,35 +3156,6 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" -[[package]] -name = "litemap" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" - -[[package]] -name = "litrs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.10" @@ -4421,12 +3166,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.21" @@ -4434,10 +3173,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] -name = "lru" -version = "0.12.3" +name = "loom" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21" dependencies = [ "hashbrown 0.14.3", ] @@ -4448,27 +3201,6 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "912b45c753ff5f7f5208307e8ace7d2a2e30d024e26d3509f3dce546c044ce15" -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "mac" version = "0.1.1" @@ -4495,7 +3227,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -4506,7 +3238,7 @@ checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -4519,16 +3251,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "markdown" -version = "1.0.0-alpha.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81" -dependencies = [ - "unicode-id", + "syn 2.0.47", ] [[package]] @@ -4561,37 +3284,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" [[package]] -name = "mcp_daemon" -version = "0.2.1" +name = "md-5" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0bdbb83765c69f4bf506d318119a25776dbad54906de9c17c1eae566088100" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "actix-cors", - "actix-web", - "actix-web-lab", - "actix-ws", - "anyhow", - "async-openai", - "async-trait", - "bytes", - "bytestring", - "futures", - "futures-core", - "futures-util", - "jsonwebtoken", - "pin-project-lite", - "reqwest 0.12.15", - "rustls 0.20.9", - "rustls-pemfile 1.0.3", - "serde", - "serde_json", - "thiserror 1.0.64", - "tokio", - "tokio-stream", - "tokio-tungstenite 0.21.0", - "tracing", - "url", - "uuid", + "digest", ] [[package]] @@ -4602,30 +3300,25 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "measure_time" -version = "0.9.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" dependencies = [ + "instant", "log", ] -[[package]] -name = "mediatype" -version = "0.19.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" - [[package]] name = "memchr" -version = "2.7.4" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memmap2" -version = "0.9.4" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6" dependencies = [ "libc", ] @@ -4668,9 +3361,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.5" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" dependencies = [ "mime", "unicase", @@ -4698,47 +3391,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", -] - -[[package]] -name = "moka" -version = "0.12.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" -dependencies = [ - "async-lock", - "async-trait", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "event-listener", - "futures-util", - "once_cell", - "parking_lot 0.12.1", - "quanta", - "rustc_version", - "smallvec", - "tagptr", - "thiserror 1.0.64", - "triomphe", - "uuid", -] - [[package]] name = "multimap" version = "0.8.3" @@ -4794,25 +3450,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.4.0", - "crossbeam-channel", - "filetime", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio 0.8.9", - "walkdir", - "windows-sys 0.48.0", -] - [[package]] name = "ntapi" version = "0.4.1" @@ -4843,12 +3480,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-integer" version = "0.1.45" @@ -4866,7 +3497,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", - "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", ] [[package]] @@ -4880,15 +3520,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "oneshot" -version = "0.1.11" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea" +checksum = "6f6640c6bda7731b1fdbab747981a0f896dd1fedaf9f4a53fa237a04a84431f4" +dependencies = [ + "loom", +] [[package]] name = "opaque-debug" @@ -4919,7 +3562,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -4952,12 +3595,12 @@ dependencies = [ [[package]] name = "os_pipe" -version = "1.2.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" +checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213" dependencies = [ "libc", - "windows-sys 0.59.0", + "winapi", ] [[package]] @@ -4968,19 +3611,13 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "ownedbytes" -version = "0.9.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" +checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7" dependencies = [ "stable_deref_trait", ] -[[package]] -name = "parking" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" - [[package]] name = "parking_lot" version = "0.11.2" @@ -5071,6 +3708,12 @@ dependencies = [ "hmac", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem" version = "1.1.1" @@ -5093,7 +3736,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a4d085fd991ac8d5b05a147b437791b4260b76326baf0fc60cf7c9c27ecd33" dependencies = [ "memchr", - "thiserror 1.0.64", + "thiserror", "ucd-trie", ] @@ -5117,7 +3760,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -5147,7 +3790,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -5167,6 +3810,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -5234,6 +3878,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -5263,22 +3920,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.7" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.7" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -5312,10 +3969,42 @@ dependencies = [ ] [[package]] -name = "powerfmt" -version = "0.2.0" +name = "postgres-protocol" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +dependencies = [ + "base64 0.21.5", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + +[[package]] +name = "postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" +dependencies = [ + "reqwest", +] [[package]] name = "ppv-lite86" @@ -5336,16 +4025,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8832c0f9be7e3cae60727e6256cfd2cd3c3e2b6cd5dad4190ecb2fd658c9030b" dependencies = [ "proc-macro2", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml_edit 0.21.1", + "toml 0.5.11", ] [[package]] @@ -5372,28 +4061,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -5402,9 +4069,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "907a61bd0f64c2f29cd1cf1dc34d05176426a3f504a78010f08416ddb7b13708" dependencies = [ "unicode-ident", ] @@ -5416,17 +4083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive 0.12.3", -] - -[[package]] -name = "prost" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" -dependencies = [ - "bytes", - "prost-derive 0.13.3", + "prost-derive", ] [[package]] @@ -5437,16 +4094,16 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", "petgraph", "prettyplease", - "prost 0.12.3", + "prost", "prost-types", "regex", - "syn 2.0.94", + "syn 2.0.47", "tempfile", "which", ] @@ -5458,23 +4115,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "prost-derive" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -5483,7 +4127,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost 0.12.3", + "prost", ] [[package]] @@ -5513,60 +4157,53 @@ dependencies = [ [[package]] name = "protoc-bin-vendored" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd89a830d0eab2502c81a9b8226d446a52998bb78e5e33cb2637c0cdd6068d99" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" dependencies = [ "protoc-bin-vendored-linux-aarch_64", "protoc-bin-vendored-linux-ppcle_64", "protoc-bin-vendored-linux-x86_32", "protoc-bin-vendored-linux-x86_64", - "protoc-bin-vendored-macos-aarch_64", "protoc-bin-vendored-macos-x86_64", "protoc-bin-vendored-win32", ] [[package]] name = "protoc-bin-vendored-linux-aarch_64" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f563627339f1653ea1453dfbcb4398a7369b768925eb14499457aeaa45afe22c" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" [[package]] name = "protoc-bin-vendored-linux-ppcle_64" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5025c949a02cd3b60c02501dd0f348c16e8fff464f2a7f27db8a9732c608b746" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" [[package]] name = "protoc-bin-vendored-linux-x86_32" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9500ce67d132c2f3b572504088712db715755eb9adf69d55641caa2cb68a07" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" [[package]] name = "protoc-bin-vendored-linux-x86_64" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5462592380cefdc9f1f14635bcce70ba9c91c1c2464c7feb2ce564726614cc41" - -[[package]] -name = "protoc-bin-vendored-macos-aarch_64" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c637745681b68b4435484543667a37606c95ddacf15e917710801a0877506030" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" [[package]] name = "protoc-bin-vendored-macos-x86_64" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38943f3c90319d522f94a6dfd4a134ba5e36148b9506d2d9723a82ebc57c8b55" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" [[package]] name = "protoc-bin-vendored-win32" -version = "3.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" [[package]] name = "protoc-rust" @@ -5616,28 +4253,13 @@ dependencies = [ "psl-types", ] -[[package]] -name = "quanta" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.0+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ - "env_logger 0.8.4", + "env_logger", "log", "rand 0.8.5", ] @@ -5653,58 +4275,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "quinn" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" -dependencies = [ - "bytes", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash 2.1.0", - "rustls 0.23.20", - "socket2 0.5.5", - "thiserror 2.0.9", - "tokio", - "tracing", -] - -[[package]] -name = "quinn-proto" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" -dependencies = [ - "bytes", - "getrandom 0.2.10", - "rand 0.8.5", - "ring 0.17.8", - "rustc-hash 2.1.0", - "rustls 0.23.20", - "rustls-pki-types", - "slab", - "thiserror 2.0.9", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.5", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "quote" version = "1.0.35" @@ -5731,6 +4301,19 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + [[package]] name = "rand" version = "0.7.3" @@ -5776,6 +4359,21 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + [[package]] name = "rand_core" version = "0.5.1" @@ -5794,16 +4392,6 @@ dependencies = [ "getrandom 0.2.10", ] -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - [[package]] name = "rand_hc" version = "0.2.0" @@ -5822,20 +4410,11 @@ dependencies = [ "rand_core 0.5.1", ] -[[package]] -name = "raw-cpuid" -version = "11.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" -dependencies = [ - "bitflags 2.4.0", -] - [[package]] name = "rayon" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -5851,6 +4430,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.1.57" @@ -5875,15 +4463,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "regex" version = "1.9.5" @@ -5916,23 +4495,6 @@ dependencies = [ "regex-syntax 0.7.5", ] -[[package]] -name = "regex-automata" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.8.4", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.6.29" @@ -5946,10 +4508,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] -name = "regex-syntax" -version = "0.8.4" +name = "remove_dir_all" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] [[package]] name = "rend" @@ -5968,66 +4533,17 @@ checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.5", "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.21", - "http 0.2.9", - "http-body 0.4.5", - "hyper 0.14.27", - "hyper-rustls 0.24.1", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "mime_guess", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.7", - "rustls-native-certs", - "rustls-pemfile 1.0.3", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.24.1", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" -dependencies = [ - "base64 0.22.1", - "bytes", - "cookie 0.18.1", + "cookie", "cookie_store", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.7", - "http 1.2.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.5.2", - "hyper-rustls 0.27.5", - "hyper-tls 0.6.0", - "hyper-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", "ipnet", "js-sys", "log", @@ -6037,44 +4553,25 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "quinn", - "rustls 0.23.20", - "rustls-pemfile 2.2.0", - "rustls-pki-types", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", - "system-configuration 0.6.1", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.1", + "tokio-rustls", "tokio-util", - "tower 0.5.2", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.7", - "windows-registry", -] - -[[package]] -name = "reqwest-eventsource" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" -dependencies = [ - "eventsource-stream", - "futures-core", - "futures-timer", - "mime", - "nom", - "pin-project-lite", - "reqwest 0.11.27", - "thiserror 1.0.64", + "webpki-roots", + "winreg", ] [[package]] @@ -6086,27 +4583,12 @@ dependencies = [ "cc", "libc", "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", + "spin", + "untrusted", "web-sys", "winapi", ] -[[package]] -name = "ring" -version = "0.17.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.10", - "libc", - "spin 0.9.8", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - [[package]] name = "rkyv" version = "0.7.42" @@ -6137,8 +4619,8 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.22.0" -source = "git+https://github.com/rust-rocksdb/rust-rocksdb?rev=1710120e4549e04ba3baa6a1ee5a5a801fa45a72#1710120e4549e04ba3baa6a1ee5a5a801fa45a72" +version = "0.21.0" +source = "git+https://github.com/LucasXu0/rust-rocksdb?rev=21cf4a23ec131b9d82dc94e178fe8efc0c147b09#21cf4a23ec131b9d82dc94e178fe8efc0c147b09" dependencies = [ "libc", "librocksdb-sys", @@ -6156,9 +4638,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.36.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +checksum = "a4c4216490d5a413bc6d10fa4742bd7d4955941d062c0ef873141d6b0e7b30fd" dependencies = [ "arrayvec", "borsh", @@ -6192,21 +4674,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc-hash" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustix" version = "0.38.31" @@ -6220,18 +4687,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.7" @@ -6239,37 +4694,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", - "ring 0.16.20", - "rustls-webpki 0.101.4", + "ring", + "rustls-webpki", "sct", ] -[[package]] -name = "rustls" -version = "0.23.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" -dependencies = [ - "once_cell", - "ring 0.17.8", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.3", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -6279,43 +4708,14 @@ dependencies = [ "base64 0.21.5", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" -dependencies = [ - "web-time", -] - [[package]] name = "rustls-webpki" version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring 0.17.8", - "rustls-pki-types", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -6349,16 +4749,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "sanitize-filename" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" -dependencies = [ - "lazy_static", - "regex", -] - [[package]] name = "schannel" version = "0.1.22" @@ -6377,6 +4767,12 @@ dependencies = [ "parking_lot 0.12.1", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6422,8 +4818,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring", + "untrusted", ] [[package]] @@ -6432,16 +4828,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "serde", - "zeroize", -] - [[package]] name = "security-framework" version = "2.9.2" @@ -6489,28 +4875,25 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" -dependencies = [ - "serde", -] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -6521,30 +4904,16 @@ checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "serde_html_form" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2de91cf02bbc07cde38891769ccd5d4f073d22a40683aa4bc7a95781aaa2c4" -dependencies = [ - "form_urlencoded", - "indexmap 2.1.0", - "itoa", - "ryu", - "serde", + "syn 2.0.47", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", - "memchr", "ryu", "serde", ] @@ -6557,7 +4926,7 @@ checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -6605,9 +4974,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.6" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", @@ -6622,9 +4991,9 @@ checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -6643,7 +5012,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=6c6f1c5cc3ce2161c247f63f6132361da8dc0a32#6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=430e3e15c9a1dc6aba2a9599d17d946a61ac7cae#430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" dependencies = [ "anyhow", "app-error", @@ -6654,15 +5023,14 @@ dependencies = [ "database-entity", "futures", "gotrue-entity", - "infra", + "log", "pin-project", - "reqwest 0.12.15", + "reqwest", "serde", "serde_json", "serde_repr", - "thiserror 1.0.64", + "thiserror", "uuid", - "validator 0.19.0", ] [[package]] @@ -6680,12 +5048,6 @@ dependencies = [ "libc", ] -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - [[package]] name = "simdutf8" version = "0.1.4" @@ -6712,19 +5074,10 @@ checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" dependencies = [ "num-bigint", "num-traits", - "thiserror 1.0.64", + "thiserror", "time", ] -[[package]] -name = "simsimd" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc843bc8f12d9c8e6b734a0fe8918fc497b42f6ae0f347dbfdad5b5138ab9b4" -dependencies = [ - "cc", -] - [[package]] name = "siphasher" version = "0.3.11" @@ -6733,9 +5086,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "sketches-ddsketch" -version = "0.3.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" +checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" dependencies = [ "serde", ] @@ -6799,12 +5152,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -6838,16 +5185,21 @@ dependencies = [ ] [[package]] -name = "strsim" -version = "0.10.0" +name = "stringprep" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] [[package]] name = "strsim" -version = "0.11.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "strum" @@ -6877,7 +5229,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -6890,7 +5242,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -6912,53 +5264,21 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.94" +version = "2.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" +checksum = "1726efe18f42ae774cc644f330953a5e7b3c3003d3edcecf18850fe9d4dd9afb" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "syn_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "sync_wrapper" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - [[package]] name = "sysinfo" version = "0.30.5" @@ -6971,7 +5291,7 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "windows", + "windows 0.52.0", ] [[package]] @@ -6982,18 +5302,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys 0.5.0", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.4.0", - "core-foundation", - "system-configuration-sys 0.6.0", + "system-configuration-sys", ] [[package]] @@ -7006,56 +5315,40 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "tantivy" -version = "0.24.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b21ad8b222d71c57aa979353ed702f0bc6d97e66d368962cbded57fbd19eedd7" +checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2" dependencies = [ "aho-corasick", "arc-swap", - "base64 0.22.1", + "async-trait", + "base64 0.21.5", "bitpacking", - "bon", "byteorder", "census", "crc32fast", "crossbeam-channel", "downcast-rs", "fastdivide", - "fnv", "fs4", "htmlescape", - "hyperloglogplus", - "itertools 0.14.0", + "itertools 0.11.0", "levenshtein_automata", "log", "lru", "lz4_flex", "measure_time", "memmap2", + "murmurhash32", + "num_cpus", "once_cell", "oneshot", "rayon", "regex", "rust-stemmers", - "rustc-hash 2.1.0", + "rustc-hash", "serde", "serde_json", "sketches-ddsketch", @@ -7068,7 +5361,7 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 2.0.9", + "thiserror", "time", "uuid", "winapi", @@ -7076,22 +5369,22 @@ dependencies = [ [[package]] name = "tantivy-bitpacker" -version = "0.8.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc286a39e089ae9938935cd488d7d34f14502544a36607effd2239ff0e2494" +checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.5.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6300428e0c104c4f7db6f95b466a6f5c1b9aece094ec57cdd365337908dc7344" +checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70" dependencies = [ - "downcast-rs", "fastdivide", - "itertools 0.14.0", + "fnv", + "itertools 0.11.0", "serde", "tantivy-bitpacker", "tantivy-common", @@ -7101,9 +5394,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.9.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b6ea6090ce03dc72c27d0619e77185d26cc3b20775966c346c6d4f7e99d7f" +checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1" dependencies = [ "async-trait", "byteorder", @@ -7114,56 +5407,50 @@ dependencies = [ [[package]] name = "tantivy-fst" -version = "0.5.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" dependencies = [ "byteorder", - "regex-syntax 0.8.4", + "regex-syntax 0.6.29", "utf8-ranges", ] [[package]] name = "tantivy-query-grammar" -version = "0.24.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e810cdeeebca57fc3f7bfec5f85fdbea9031b2ac9b990eb5ff49b371d52bbe6a" +checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77" dependencies = [ "nom", - "serde", - "serde_json", ] [[package]] name = "tantivy-sstable" -version = "0.5.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "709f22c08a4c90e1b36711c1c6cad5ae21b20b093e535b69b18783dd2cb99416" +checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df" dependencies = [ - "futures-util", - "itertools 0.14.0", - "tantivy-bitpacker", "tantivy-common", "tantivy-fst", - "zstd 0.13.2", + "zstd 0.12.4", ] [[package]] name = "tantivy-stacker" -version = "0.5.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bcdebb267671311d1e8891fd9d1301803fdb8ad21ba22e0a30d0cab49ba59c1" +checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c" dependencies = [ "murmurhash32", - "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.5.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa942fcee81e213e09715bbce8734ae2180070b97b33839a795ba1de201547d" +checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66" dependencies = [ "serde", ] @@ -7175,16 +5462,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "tempfile" -version = "3.12.0" +name = "tempdir" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +dependencies = [ + "rand 0.4.6", + "remove_dir_all", +] + +[[package]] +name = "tempfile" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -7205,7 +5501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" dependencies = [ "chrono", - "chrono-tz 0.8.3", + "chrono-tz", "globwalk", "humansize", "lazy_static", @@ -7220,15 +5516,6 @@ dependencies = [ "unic-segment", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "terminal_size" version = "0.1.17" @@ -7241,42 +5528,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl 1.0.64", -] - -[[package]] -name = "thiserror" -version = "2.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" -dependencies = [ - "thiserror-impl 2.0.9", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -7302,14 +5569,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", - "num-conv", - "powerfmt", "serde", "time-core", "time-macros", @@ -7317,30 +5582,19 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ - "num-conv", "time-core", ] -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -7358,21 +5612,22 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.3", + "mio", + "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.48.0", ] [[package]] @@ -7387,13 +5642,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -7406,6 +5661,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf 0.11.2", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2 0.5.5", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-retry" version = "0.3.0" @@ -7417,42 +5698,21 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - [[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.7", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" -dependencies = [ - "rustls 0.23.20", + "rustls", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -7471,38 +5731,21 @@ dependencies = [ "native-tls", "tokio", "tokio-native-tls", - "tungstenite 0.20.1", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "native-tls", - "tokio", - "tokio-native-tls", - "tungstenite 0.21.0", + "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", - "futures-util", - "hashbrown 0.14.3", "pin-project-lite", - "slab", "tokio", + "tracing", ] [[package]] @@ -7523,7 +5766,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.19.15", + "toml_edit", ] [[package]] @@ -7548,17 +5791,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_edit" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" -dependencies = [ - "indexmap 2.1.0", - "toml_datetime", - "winnow", -] - [[package]] name = "tonic" version = "0.10.2" @@ -7570,17 +5802,17 @@ dependencies = [ "axum", "base64 0.21.5", "bytes", - "h2 0.3.21", - "http 0.2.9", - "http-body 0.4.5", - "hyper 0.14.27", + "h2", + "http", + "http-body", + "hyper", "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.12.3", + "prost", "tokio", "tokio-stream", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", "tracing", @@ -7606,38 +5838,23 @@ dependencies = [ "tracing", ] -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper 1.0.2", - "tokio", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -7652,27 +5869,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.64", + "thiserror", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] name = "tracing-bunyan-formatter" -version = "0.3.10" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d637245a0d8774bd48df6482e086c59a8b5348a910c3b0579354045a9d82411" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" dependencies = [ "ahash 0.8.6", "gethostname", @@ -7688,9 +5905,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -7720,9 +5937,9 @@ dependencies = [ [[package]] name = "tracing-serde" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" dependencies = [ "serde", "tracing-core", @@ -7730,9 +5947,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -7760,12 +5977,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "triomphe" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" - [[package]] name = "try-lock" version = "0.2.4" @@ -7794,7 +6005,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -7806,33 +6017,13 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 0.2.9", + "http", "httparse", "log", "native-tls", "rand 0.8.5", "sha1", - "thiserror 1.0.64", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.2.0", - "httparse", - "log", - "native-tls", - "rand 0.8.5", - "sha1", - "thiserror 1.0.64", + "thiserror", "url", "utf-8", ] @@ -7914,12 +6105,6 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" -[[package]] -name = "unicode-id" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" - [[package]] name = "unicode-ident" version = "1.0.11" @@ -7969,12 +6154,6 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - [[package]] name = "url" version = "2.5.0" @@ -7984,7 +6163,6 @@ dependencies = [ "form_urlencoded", "idna 0.5.0", "percent-encoding", - "serde", ] [[package]] @@ -7993,29 +6171,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8-ranges" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "uuid" -version = "1.10.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom 0.2.10", "serde", @@ -8025,62 +6191,44 @@ dependencies = [ [[package]] name = "validator" -version = "0.18.1" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna 0.5.0", - "once_cell", + "idna 0.4.0", + "lazy_static", "regex", "serde", "serde_derive", "serde_json", "url", - "validator_derive 0.18.2", -] - -[[package]] -name = "validator" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303" -dependencies = [ - "idna 1.0.3", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive 0.19.0", + "validator_derive", ] [[package]] name = "validator_derive" -version = "0.18.2" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df0bcf92720c40105ac4b2dda2a4ea3aa717d4d6a862cc217da653a4bd5c6b10" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" dependencies = [ - "darling 0.20.11", - "once_cell", + "if_chain", + "lazy_static", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.94", + "regex", + "syn 1.0.109", + "validator_types", ] [[package]] -name = "validator_derive" -version = "0.19.0" +name = "validator_types" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" dependencies = [ - "darling 0.20.11", - "once_cell", - "proc-macro-error2", "proc-macro2", - "quote", - "syn 2.0.94", + "syn 1.0.109", ] [[package]] @@ -8134,27 +6282,26 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", - "once_cell", - "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", + "once_cell", "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", "wasm-bindgen-shared", ] @@ -8172,9 +6319,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8182,25 +6329,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", + "syn 2.0.47", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" @@ -8240,43 +6384,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "0.26.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" -dependencies = [ - "rustls-pki-types", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "which" @@ -8290,6 +6402,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -8321,6 +6443,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows" version = "0.52.0" @@ -8328,7 +6459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core", - "windows-targets 0.52.6", + "windows-targets 0.52.0", ] [[package]] @@ -8337,42 +6468,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-registry" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" -dependencies = [ - "windows-result", - "windows-strings", - "windows-targets 0.53.0", -] - -[[package]] -name = "windows-result" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" -dependencies = [ - "windows-link", + "windows-targets 0.52.0", ] [[package]] @@ -8390,16 +6486,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.52.0", ] [[package]] @@ -8419,34 +6506,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -8457,15 +6527,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" @@ -8475,15 +6539,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" @@ -8493,27 +6551,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" @@ -8523,15 +6563,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" @@ -8541,15 +6575,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" @@ -8559,15 +6587,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" @@ -8577,15 +6599,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" @@ -8606,28 +6622,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - [[package]] name = "wyz" version = "0.5.1" @@ -8637,66 +6631,20 @@ dependencies = [ "tap", ] -[[package]] -name = "xattr" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" -dependencies = [ - "libc", - "linux-raw-sys", - "rustix", -] - -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", - "synstructure", -] - [[package]] name = "yrs" -version = "0.21.3" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81de5913bca29f43a1d12ca92a7b39a2945e9420e01602a7563917c7bfc60f70" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", - "async-lock", - "async-trait", - "dashmap 6.0.1", + "atomic_refcell", "fastrand", "serde", "serde_json", "smallstr", "smallvec", - "thiserror 1.0.64", + "thiserror", ] [[package]] @@ -8716,70 +6664,7 @@ checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.94", -] - -[[package]] -name = "zerofrom" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.94", + "syn 2.0.47", ] [[package]] @@ -8791,7 +6676,7 @@ dependencies = [ "aes", "byteorder", "bzip2", - "constant_time_eq 0.1.5", + "constant_time_eq", "crc32fast", "crossbeam-utils", "flate2", @@ -8802,49 +6687,6 @@ dependencies = [ "zstd 0.11.2+zstd.1.5.2", ] -[[package]] -name = "zip" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" -dependencies = [ - "aes", - "arbitrary", - "bzip2", - "constant_time_eq 0.3.0", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "hmac", - "indexmap 2.1.0", - "lzma-rs", - "memchr", - "pbkdf2 0.12.2", - "rand 0.8.5", - "sha1", - "thiserror 1.0.64", - "time", - "zeroize", - "zopfli", - "zstd 0.13.2", -] - -[[package]] -name = "zopfli" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" -dependencies = [ - "bumpalo", - "crc32fast", - "lockfree-object-pool", - "log", - "once_cell", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" @@ -8856,11 +6698,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ - "zstd-safe 7.2.0", + "zstd-safe 6.0.6", ] [[package]] @@ -8875,19 +6717,21 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.0" +version = "6.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" dependencies = [ + "libc", "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.12+zstd.1.5.6" +version = "2.0.8+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" dependencies = [ "cc", + "libc", "pkg-config", ] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 1561c7ea7d..f561310fec 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -1,89 +1,84 @@ [workspace] members = [ - "lib-dispatch", - "lib-log", - "flowy-core", - "dart-ffi", - "flowy-user", - "flowy-user-pub", - "event-integration-test", - "flowy-sqlite", - "flowy-folder", - "flowy-folder-pub", - "flowy-notification", - "flowy-document", - "flowy-document-pub", - "flowy-error", - "flowy-database2", - "flowy-database-pub", - "flowy-server", - "flowy-server-pub", - "flowy-storage", - "collab-integrate", - "flowy-date", - "flowy-search", - "lib-infra", - "build-tool/flowy-ast", - "build-tool/flowy-codegen", - "build-tool/flowy-derive", - "flowy-search-pub", - "flowy-ai", - "flowy-ai-pub", - "flowy-storage-pub", + "lib-dispatch", + "lib-log", + "flowy-core", + "dart-ffi", + "flowy-user", + "flowy-user-pub", + "event-integration-test", + "flowy-sqlite", + "flowy-folder", + "flowy-folder-pub", + "flowy-notification", + "flowy-document", + "flowy-document-pub", + "flowy-error", + "flowy-database2", + "flowy-database-pub", + "flowy-server", + "flowy-server-pub", + "flowy-config", + "flowy-encrypt", + "flowy-storage", + "collab-integrate", + "flowy-date", + "flowy-search", + "lib-infra", + "build-tool/flowy-ast", + "build-tool/flowy-codegen", + "build-tool/flowy-derive", + "flowy-search-pub", + "flowy-chat", + "flowy-chat-pub", ] - resolver = "2" [workspace.dependencies] -lib-dispatch = { path = "lib-dispatch" } -lib-log = { path = "lib-log" } -lib-infra = { path = "lib-infra" } -flowy-ast = { path = "build-tool/flowy-ast" } -flowy-codegen = { path = "build-tool/flowy-codegen" } -flowy-derive = { path = "build-tool/flowy-derive" } -flowy-core = { path = "flowy-core" } -dart-ffi = { path = "dart-ffi" } -flowy-user = { path = "flowy-user" } -flowy-user-pub = { path = "flowy-user-pub" } -flowy-sqlite = { path = "flowy-sqlite" } -flowy-folder = { path = "flowy-folder" } -flowy-folder-pub = { path = "flowy-folder-pub" } -flowy-notification = { path = "flowy-notification" } -flowy-document = { path = "flowy-document" } -flowy-document-pub = { path = "flowy-document-pub" } -flowy-error = { path = "flowy-error" } -flowy-database2 = { path = "flowy-database2" } -flowy-database-pub = { path = "flowy-database-pub" } -flowy-server = { path = "flowy-server" } -flowy-server-pub = { path = "flowy-server-pub" } -flowy-storage = { path = "flowy-storage" } -flowy-storage-pub = { path = "flowy-storage-pub" } -flowy-search = { path = "flowy-search" } -flowy-search-pub = { path = "flowy-search-pub" } -collab-integrate = { path = "collab-integrate" } -flowy-date = { path = "flowy-date" } -flowy-ai = { path = "flowy-ai" } -flowy-ai-pub = { path = "flowy-ai-pub" } +lib-dispatch = { workspace = true, path = "lib-dispatch" } +lib-log = { workspace = true, path = "lib-log" } +lib-infra = { workspace = true, path = "lib-infra" } +flowy-ast = { workspace = true, path = "build-tool/flowy-ast" } +flowy-codegen = { workspace = true, path = "build-tool/flowy-codegen" } +flowy-derive = { workspace = true, path = "build-tool/flowy-derive" } +flowy-core = { workspace = true, path = "flowy-core" } +dart-ffi = { workspace = true, path = "dart-ffi" } +flowy-user = { workspace = true, path = "flowy-user" } +flowy-user-pub = { workspace = true, path = "flowy-user-pub" } +flowy-sqlite = { workspace = true, path = "flowy-sqlite" } +flowy-folder = { workspace = true, path = "flowy-folder" } +flowy-folder-pub = { workspace = true, path = "flowy-folder-pub" } +flowy-notification = { workspace = true, path = "flowy-notification" } +flowy-document = { workspace = true, path = "flowy-document" } +flowy-document-pub = { workspace = true, path = "flowy-document-pub" } +flowy-error = { workspace = true, path = "flowy-error" } +flowy-database2 = { workspace = true, path = "flowy-database2" } +flowy-database-pub = { workspace = true, path = "flowy-database-pub" } +flowy-server = { workspace = true, path = "flowy-server" } +flowy-server-pub = { workspace = true, path = "flowy-server-pub" } +flowy-config = { workspace = true, path = "flowy-config" } +flowy-encrypt = { workspace = true, path = "flowy-encrypt" } +flowy-storage = { workspace = true, path = "flowy-storage" } +flowy-search = { workspace = true, path = "flowy-search" } +flowy-search-pub = { workspace = true, path = "flowy-search-pub" } +collab-integrate = { workspace = true, path = "collab-integrate" } +flowy-date = { workspace = true, path = "flowy-date" } +flowy-chat = { workspace = true, path = "flowy-chat" } +flowy-chat-pub = { workspace = true, path = "flowy-chat-pub" } anyhow = "1.0" -arc-swap = "1.7" tracing = "0.1.40" bytes = "1.5.0" serde_json = "1.0.108" serde = "1.0.194" protobuf = { version = "2.28.0" } -diesel = { version = "2.1.0", features = [ - "sqlite", - "chrono", - "r2d2", - "serde_json", -] } -diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] } uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] } serde_repr = "0.1" -futures = "0.3.31" +parking_lot = "0.12" +futures = "0.3.29" tokio = "1.38.0" tokio-stream = "0.1.14" -async-trait = "0.1.81" +async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } collab = { version = "0.2" } collab-entity = { version = "0.2" } @@ -92,27 +87,18 @@ collab-document = { version = "0.2" } collab-database = { version = "0.2" } collab-plugins = { version = "0.2" } collab-user = { version = "0.2" } -collab-importer = { version = "0.1" } -yrs = "0.21.0" -validator = { version = "0.18", features = ["derive"] } -tokio-util = "0.7.11" -zip = "2.2.0" -dashmap = "6.0.1" -derive_builder = "0.20.2" -tantivy = { version = "0.24.0" } -af-plugin = { version = "0.1" } -af-local-ai = { version = "0.1" } +yrs = "0.18.8" +validator = { version = "0.16.1", features = ["derive"] } # Please using the following command to update the revision id # Current directory: frontend # Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } -client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "6c6f1c5cc3ce2161c247f63f6132361da8dc0a32" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "430e3e15c9a1dc6aba2a9599d17d946a61ac7cae" } [profile.dev] -opt-level = 0 +opt-level = 1 lto = false codegen-units = 16 debug = true @@ -128,13 +114,17 @@ debug = true codegen-units = 16 lto = false +## debuginfo — it makes ./target much bigger, which again harms caching. Depending on your preferred workflow, +## you might consider disabling debuginfo unconditionally, this brings some benefits for local builds as well. #strip = "debuginfo" -incremental = true +## For from-scratch builds, incremental adds an extra dependency-tracking overhead. It also significantly increases +## the amount of IO and the size of ./target, which make caching less effective. +incremental = false [patch.crates-io] -# We're using a specific commit here because rust-rocksdb doesn't publish the latest version that includes the memory alignment fix. -# For more details, see https://github.com/rust-rocksdb/rust-rocksdb/pull/868 -rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120e4549e04ba3baa6a1ee5a5a801fa45a72" } +# TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged. +# Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0. +rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" } # Please use the following script to update collab. # Working directory: frontend # @@ -144,19 +134,10 @@ rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb", rev = "1710120 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } -collab-importer = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "f029a79e6112c296286cd7bb4c6dcaa4cf0d33f3" } - -# Working directory: frontend -# To update the commit ID, run: -# scripts/tool/update_local_ai_rev.sh new_rev_id -# ⚠️⚠️⚠️️ -af-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } -af-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } -af-mcp = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "093d3f4916a7b924ac66b4f0a9d81cc5fcc72eaa" } +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "3a58d95" } diff --git a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml index ae07268ee9..92a273ad04 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml +++ b/frontend/rust-lib/build-tool/flowy-codegen/Cargo.toml @@ -7,42 +7,41 @@ edition = "2021" [dependencies] log = "0.4.17" -serde = { workspace = true, features = ["derive"] } +serde = { workspace = true, features = ["derive"]} serde_json.workspace = true flowy-ast.workspace = true quote = "1.0" -cmd_lib = { version = "1.9.5", optional = true } -protoc-rust = { version = "2.28.0", optional = true } -#protobuf-codegen = { version = "3.7.1" } +cmd_lib = { version = "1.3.0", optional = true } +protoc-rust = { version = "2", optional = true } walkdir = { version = "2", optional = true } similar = { version = "1.3.0", optional = true } syn = { version = "1.0.109", features = ["extra-traits", "parsing", "derive", "full"] } fancy-regex = { version = "0.10.0", optional = true } lazy_static = { version = "1.4.0", optional = true } -tera = { version = "1.17.1", optional = true } +tera = { version = "1.17.1", optional = true} itertools = { version = "0.10", optional = true } phf = { version = "0.8.0", features = ["macros"], optional = true } -console = { version = "0.14.1", optional = true } -protoc-bin-vendored = { version = "3.1.0", optional = true } -toml = { version = "0.5.11", optional = true } +console = {version = "0.14.1", optional = true} +protoc-bin-vendored = { version = "3.0", optional = true } +toml = {version = "0.5.11", optional = true} [features] proto_gen = [ - "similar", - "fancy-regex", - "lazy_static", - "tera", - "itertools", - "phf", - "walkdir", - "console", - "toml", - "cmd_lib", - "protoc-rust", - "walkdir", - "protoc-bin-vendored", + "similar", + "fancy-regex", + "lazy_static", + "tera", + "itertools", + "phf", + "walkdir", + "console", + "toml", + "cmd_lib", + "protoc-rust", + "walkdir", + "protoc-bin-vendored", ] dart_event = ["walkdir", "tera", ] dart = ["proto_gen", "dart_event"] diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/dart_event.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/dart_event.rs index 0a1849f877..8ab6d3fb59 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/dart_event.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/dart_event/dart_event.rs @@ -143,7 +143,8 @@ pub fn parse_event_crate(event_crate: &DartEventCrate) -> Vec<EventASTContext> { attrs .iter() .filter(|attr| !attr.attrs.event_attrs.ignore) - .map(|variant| EventASTContext::from(&variant.attrs)) + .enumerate() + .map(|(_index, variant)| EventASTContext::from(&variant.attrs)) .collect::<Vec<_>>() }, _ => vec![], diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs index 2df1e98ee9..768147c10a 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs @@ -23,6 +23,7 @@ pub struct ProtoCache { pub enum Project { Tauri, TauriApp, + Web { relative_path: String }, Native, } @@ -33,6 +34,7 @@ impl Project { Project::TauriApp => { "appflowy_web_app/src/application/services/tauri-services/backend".to_string() }, + Project::Web { .. } => "appflowy_web/src/services/backend".to_string(), Project::Native => panic!("Native project is not supported yet."), } } @@ -40,6 +42,7 @@ impl Project { pub fn event_root(&self) -> String { match self { Project::Tauri | Project::TauriApp => "../../".to_string(), + Project::Web { relative_path } => relative_path.to_string(), Project::Native => panic!("Native project is not supported yet."), } } @@ -47,6 +50,7 @@ impl Project { pub fn model_root(&self) -> String { match self { Project::Tauri | Project::TauriApp => "../../".to_string(), + Project::Web { relative_path } => relative_path.to_string(), Project::Native => panic!("Native project is not supported yet."), } } @@ -58,6 +62,13 @@ impl Project { import { Ok, Err, Result } from "ts-results"; import { invoke } from "@tauri-apps/api/tauri"; import * as pb from "../.."; +"# + .to_string(), + Project::Web { .. } => r#" +/// Auto generate. Do not edit +import { Ok, Err, Result } from "ts-results"; +import { invoke } from "@/application/app.ts"; +import * as pb from "../.."; "# .to_string(), Project::Native => panic!("Native project is not supported yet."), diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs index 677e7bcddf..75b4b8eb4f 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs @@ -12,9 +12,8 @@ use itertools::Itertools; use log::info; pub use proto_gen::*; pub use proto_info::*; -use std::fs; use std::fs::File; -use std::io::{BufRead, BufReader, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; use walkdir::WalkDir; @@ -76,64 +75,64 @@ pub fn dart_gen(crate_name: &str) { } } -// #[allow(unused_variables)] -// fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { -// // 1. generate the proto files to proto_file_dir -// #[cfg(feature = "proto_gen")] -// let proto_crates = gen_proto_files(crate_name); -// -// for proto_crate in proto_crates { -// let mut proto_file_paths = vec![]; -// let mut file_names = vec![]; -// let proto_file_output_path = proto_crate -// .proto_output_path() -// .to_str() -// .unwrap() -// .to_string(); -// let protobuf_output_path = proto_crate -// .protobuf_crate_path() -// .to_str() -// .unwrap() -// .to_string(); -// -// for (path, file_name) in WalkDir::new(&proto_file_output_path) -// .into_iter() -// .filter_map(|e| e.ok()) -// .map(|e| { -// let path = e.path().to_str().unwrap().to_string(); -// let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); -// (path, file_name) -// }) -// { -// if path.ends_with(".proto") { -// // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project -// println!("cargo:rerun-if-changed={}", path); -// proto_file_paths.push(path); -// file_names.push(file_name); -// } -// } -// let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); -// -// // 2. generate the protobuf files(Dart) -// #[cfg(feature = "ts")] -// generate_ts_protobuf_files( -// dest_folder_name, -// &proto_file_output_path, -// &proto_file_paths, -// &file_names, -// &protoc_bin_path, -// &project, -// ); -// -// // 3. generate the protobuf files(Rust) -// generate_rust_protobuf_files( -// &protoc_bin_path, -// &proto_file_paths, -// &proto_file_output_path, -// &protobuf_output_path, -// ); -// } -// } +#[allow(unused_variables)] +pub fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { + // 1. generate the proto files to proto_file_dir + #[cfg(feature = "proto_gen")] + let proto_crates = gen_proto_files(crate_name); + + for proto_crate in proto_crates { + let mut proto_file_paths = vec![]; + let mut file_names = vec![]; + let proto_file_output_path = proto_crate + .proto_output_path() + .to_str() + .unwrap() + .to_string(); + let protobuf_output_path = proto_crate + .protobuf_crate_path() + .to_str() + .unwrap() + .to_string(); + + for (path, file_name) in WalkDir::new(&proto_file_output_path) + .into_iter() + .filter_map(|e| e.ok()) + .map(|e| { + let path = e.path().to_str().unwrap().to_string(); + let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); + (path, file_name) + }) + { + if path.ends_with(".proto") { + // https://stackoverflow.com/questions/49077147/how-can-i-force-build-rs-to-run-again-without-cleaning-my-whole-project + println!("cargo:rerun-if-changed={}", path); + proto_file_paths.push(path); + file_names.push(file_name); + } + } + let protoc_bin_path = protoc_bin_vendored::protoc_bin_path().unwrap(); + + // 2. generate the protobuf files(Dart) + #[cfg(feature = "ts")] + generate_ts_protobuf_files( + dest_folder_name, + &proto_file_output_path, + &proto_file_paths, + &file_names, + &protoc_bin_path, + &project, + ); + + // 3. generate the protobuf files(Rust) + generate_rust_protobuf_files( + &protoc_bin_path, + &proto_file_paths, + &proto_file_output_path, + &protobuf_output_path, + ); + } +} fn generate_rust_protobuf_files( protoc_bin_path: &Path, @@ -148,38 +147,6 @@ fn generate_rust_protobuf_files( .include(proto_file_output_path) .run() .expect("Running rust protoc failed."); - remove_box_pointers_lint_from_all_except_mod(protobuf_output_path); -} -fn remove_box_pointers_lint_from_all_except_mod(dir_path: &str) { - let dir = fs::read_dir(dir_path).expect("Failed to read directory"); - for entry in dir { - let entry = entry.expect("Failed to read directory entry"); - let path = entry.path(); - - // Skip directories and mod.rs - if path.is_file() { - if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) { - if file_name != "mod.rs" { - remove_box_pointers_lint(&path); - } - } - } - } -} - -fn remove_box_pointers_lint(file_path: &Path) { - let file = File::open(file_path).expect("Failed to open file"); - let reader = BufReader::new(file); - let lines: Vec<String> = reader - .lines() - .map_while(Result::ok) - .filter(|line| !line.contains("#![allow(box_pointers)]")) - .collect(); - - let mut file = File::create(file_path).expect("Failed to create file"); - for line in lines { - writeln!(file, "{}", line).expect("Failed to write line"); - } } #[cfg(feature = "ts")] diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs index 97a7f5f529..ff51ff952b 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs @@ -153,7 +153,8 @@ pub fn parse_event_crate(event_crate: &TsEventCrate) -> Vec<EventASTContext> { attrs .iter() .filter(|attr| !attr.attrs.event_attrs.ignore) - .map(|variant| EventASTContext::from(&variant.attrs)) + .enumerate() + .map(|(_index, variant)| EventASTContext::from(&variant.attrs)) .collect::<Vec<_>>() }, _ => vec![], diff --git a/frontend/rust-lib/build-tool/flowy-derive/Cargo.toml b/frontend/rust-lib/build-tool/flowy-derive/Cargo.toml index 763210c558..ce84acd0eb 100644 --- a/frontend/rust-lib/build-tool/flowy-derive/Cargo.toml +++ b/frontend/rust-lib/build-tool/flowy-derive/Cargo.toml @@ -14,8 +14,8 @@ syn = { version = "1.0.109", features = ["extra-traits", "visit"] } quote = "1.0" proc-macro2 = "1.0" flowy-ast.workspace = true -lazy_static = { version = "1.4.0" } -dashmap.workspace = true +lazy_static = {version = "1.4.0"} +dashmap = "5" flowy-codegen.workspace = true serde_json.workspace = true walkdir = "2.3.2" diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index 0cbdd41ccd..ffddb6a911 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -11,21 +11,15 @@ crate-type = ["cdylib", "rlib"] collab = { workspace = true } collab-plugins = { workspace = true } collab-entity = { workspace = true } -collab-document = { workspace = true } -collab-folder = { workspace = true } -collab-user = { workspace = true } -collab-database = { workspace = true } serde.workspace = true serde_json.workspace = true anyhow.workspace = true tracing.workspace = true +parking_lot.workspace = true +async-trait.workspace = true tokio = { workspace = true, features = ["sync"] } lib-infra = { workspace = true } -arc-swap = "1.7" -flowy-sqlite = { workspace = true } -diesel.workspace = true -flowy-error.workspace = true -uuid.workspace = true +futures = "0.3" [features] default = [] diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 223ebacc91..571264d1d2 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -1,23 +1,13 @@ -use std::borrow::BorrowMut; use std::fmt::{Debug, Display}; use std::sync::{Arc, Weak}; use crate::CollabKVDB; -use anyhow::{anyhow, Error}; -use arc_swap::{ArcSwap, ArcSwapOption}; -use collab::core::collab::DataSource; -use collab::core::collab_plugin::CollabPersistence; -use collab::entity::EncodedCollab; -use collab::error::CollabError; -use collab::preclude::{Collab, CollabBuilder}; -use collab_database::workspace_database::{DatabaseCollabService, WorkspaceDatabaseManager}; -use collab_document::blocks::DocumentData; -use collab_document::document::Document; +use anyhow::Error; +use collab::core::collab::{DataSource, MutexCollab}; +use collab::preclude::CollabBuilder; use collab_entity::{CollabObject, CollabType}; -use collab_folder::{Folder, FolderData, FolderNotify}; use collab_plugins::connect_state::{CollabConnectReachability, CollabConnectState}; use collab_plugins::local_storage::kv::snapshot::SnapshotPersistence; - if_native! { use collab_plugins::local_storage::rocksdb::rocksdb_plugin::{RocksdbBackup, RocksdbDiskPlugin}; } @@ -27,21 +17,17 @@ use collab_plugins::local_storage::indexeddb::IndexeddbDiskPlugin; } pub use crate::plugin_provider::CollabCloudPluginProvider; -use collab::lock::RwLock; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; use collab_plugins::local_storage::CollabPersistenceConfig; -use collab_user::core::{UserAwareness, UserAwarenessNotifier}; -use flowy_error::FlowyError; use lib_infra::{if_native, if_wasm}; -use tracing::{error, instrument, trace, warn}; -use uuid::Uuid; +use parking_lot::{Mutex, RwLock}; +use tracing::{instrument, trace}; #[derive(Clone, Debug)] pub enum CollabPluginProviderType { Local, AppFlowyCloud, + Supabase, } pub enum CollabPluginProviderContext { @@ -49,7 +35,13 @@ pub enum CollabPluginProviderContext { AppFlowyCloud { uid: i64, collab_object: CollabObject, - local_collab: Weak<RwLock<dyn BorrowMut<Collab> + Send + Sync + 'static>>, + local_collab: Weak<MutexCollab>, + }, + Supabase { + uid: i64, + collab_object: CollabObject, + local_collab: Weak<MutexCollab>, + local_collab_db: Weak<CollabKVDB>, }, } @@ -60,7 +52,13 @@ impl Display for CollabPluginProviderContext { CollabPluginProviderContext::AppFlowyCloud { uid: _, collab_object, - .. + local_collab: _, + } => collab_object.to_string(), + CollabPluginProviderContext::Supabase { + uid: _, + collab_object, + local_collab: _, + local_collab_db: _, } => collab_object.to_string(), }; write!(f, "{}", str) @@ -68,16 +66,16 @@ impl Display for CollabPluginProviderContext { } pub trait WorkspaceCollabIntegrate: Send + Sync { - fn workspace_id(&self) -> Result<Uuid, FlowyError>; - fn device_id(&self) -> Result<String, FlowyError>; + fn workspace_id(&self) -> Result<String, Error>; + fn device_id(&self) -> Result<String, Error>; } pub struct AppFlowyCollabBuilder { network_reachability: CollabConnectReachability, - plugin_provider: ArcSwap<Arc<dyn CollabCloudPluginProvider>>, - snapshot_persistence: ArcSwapOption<Arc<dyn SnapshotPersistence + 'static>>, + plugin_provider: RwLock<Arc<dyn CollabCloudPluginProvider>>, + snapshot_persistence: Mutex<Option<Arc<dyn SnapshotPersistence>>>, #[cfg(not(target_arch = "wasm32"))] - rocksdb_backup: ArcSwapOption<Arc<dyn RocksdbBackup>>, + rocksdb_backup: Mutex<Option<Arc<dyn RocksdbBackup>>>, workspace_integrate: Arc<dyn WorkspaceCollabIntegrate>, } @@ -88,7 +86,7 @@ impl AppFlowyCollabBuilder { ) -> Self { Self { network_reachability: CollabConnectReachability::new(), - plugin_provider: ArcSwap::new(Arc::new(Arc::new(storage_provider))), + plugin_provider: RwLock::new(Arc::new(storage_provider)), snapshot_persistence: Default::default(), #[cfg(not(target_arch = "wasm32"))] rocksdb_backup: Default::default(), @@ -97,14 +95,12 @@ impl AppFlowyCollabBuilder { } pub fn set_snapshot_persistence(&self, snapshot_persistence: Arc<dyn SnapshotPersistence>) { - self - .snapshot_persistence - .store(Some(snapshot_persistence.into())); + *self.snapshot_persistence.lock() = Some(snapshot_persistence); } #[cfg(not(target_arch = "wasm32"))] pub fn set_rocksdb_backup(&self, rocksdb_backup: Arc<dyn RocksdbBackup>) { - self.rocksdb_backup.store(Some(rocksdb_backup.into())); + *self.rocksdb_backup.lock() = Some(rocksdb_backup); } pub fn update_network(&self, reachable: bool) { @@ -119,278 +115,205 @@ impl AppFlowyCollabBuilder { } } - pub fn collab_object( + fn collab_object( &self, - workspace_id: &Uuid, uid: i64, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, ) -> Result<CollabObject, Error> { + let device_id = self.workspace_integrate.device_id()?; + let workspace_id = self.workspace_integrate.workspace_id()?; + Ok(CollabObject::new( + uid, + object_id.to_string(), + collab_type, + workspace_id, + device_id, + )) + } + + /// Creates a new collaboration builder with the default configuration. + /// + /// This function will initiate the creation of a [MutexCollab] object if it does not already exist. + /// To check for the existence of the object prior to creation, you should utilize a transaction + /// returned by the [read_txn] method of the [CollabKVDB]. Then, invoke the [is_exist] method + /// to confirm the object's presence. + /// + /// # Parameters + /// - `uid`: The user ID associated with the collaboration. + /// - `object_id`: A string reference representing the ID of the object. + /// - `object_type`: The type of the collaboration, defined by the [CollabType] enum. + /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabDocState] type. + /// - `collab_db`: A weak reference to the [CollabKVDB]. + /// + #[allow(clippy::too_many_arguments)] + pub async fn build( + &self, + workspace_id: &str, + uid: i64, + object_id: &str, + object_type: CollabType, + collab_doc_state: DataSource, + collab_db: Weak<CollabKVDB>, + build_config: CollabBuilderConfig, + ) -> Result<Arc<MutexCollab>, Error> { + self.build_with_config( + workspace_id, + uid, + object_id, + object_type, + collab_db, + collab_doc_state, + build_config, + ) + } + + /// Creates a new collaboration builder with the custom configuration. + /// + /// This function will initiate the creation of a [MutexCollab] object if it does not already exist. + /// To check for the existence of the object prior to creation, you should utilize a transaction + /// returned by the [read_txn] method of the [CollabKVDB]. Then, invoke the [is_exist] method + /// to confirm the object's presence. + /// + /// # Parameters + /// - `uid`: The user ID associated with the collaboration. + /// - `object_id`: A string reference representing the ID of the object. + /// - `object_type`: The type of the collaboration, defined by the [CollabType] enum. + /// - `raw_data`: The raw data of the collaboration object, defined by the [CollabDocState] type. + /// - `collab_db`: A weak reference to the [CollabKVDB]. + /// + #[allow(clippy::too_many_arguments)] + #[instrument(level = "trace", skip(self, collab_db, collab_doc_state, build_config))] + pub fn build_with_config( + &self, + workspace_id: &str, + uid: i64, + object_id: &str, + object_type: CollabType, + collab_db: Weak<CollabKVDB>, + collab_doc_state: DataSource, + build_config: CollabBuilderConfig, + ) -> Result<Arc<MutexCollab>, Error> { + let collab = CollabBuilder::new(uid, object_id) + .with_doc_state(collab_doc_state) + .with_device_id(self.workspace_integrate.device_id()?) + .build()?; + // Compare the workspace_id with the currently opened workspace_id. Return an error if they do not match. // This check is crucial in asynchronous code contexts where the workspace_id might change during operation. let actual_workspace_id = self.workspace_integrate.workspace_id()?; - if workspace_id != &actual_workspace_id { + if workspace_id != actual_workspace_id { return Err(anyhow::anyhow!( "workspace_id not match when build collab. expect workspace_id: {}, actual workspace_id: {}", workspace_id, actual_workspace_id )); } - let device_id = self.workspace_integrate.device_id()?; - Ok(CollabObject::new( - uid, - object_id.to_string(), - collab_type, - workspace_id.to_string(), - device_id, - )) - } + let persistence_config = CollabPersistenceConfig::default(); - #[allow(clippy::too_many_arguments)] - #[instrument( - level = "trace", - skip(self, data_source, collab_db, builder_config, data) - )] - pub async fn create_document( - &self, - object: CollabObject, - data_source: DataSource, - collab_db: Weak<CollabKVDB>, - builder_config: CollabBuilderConfig, - data: Option<DocumentData>, - ) -> Result<Arc<RwLock<Document>>, Error> { - let expected_collab_type = CollabType::Document; - assert_eq!(object.collab_type, expected_collab_type); - let mut collab = self.build_collab(&object, &collab_db, data_source).await?; - collab.enable_undo_redo(); - - let document = match data { - None => Document::open(collab)?, - Some(data) => { - let document = Document::create_with_data(collab, data)?; - if let Err(err) = self.write_collab_to_disk( - object.uid, - &object.workspace_id, - &object.object_id, - collab_db.clone(), - &object.collab_type, - &document, - ) { - error!( - "build_collab: flush document collab to disk failed: {}", - err - ); - } - document - }, - }; - let document = Arc::new(RwLock::new(document)); - self.finalize(object, builder_config, document) - } - - #[allow(clippy::too_many_arguments)] - #[instrument( - level = "trace", - skip(self, object, doc_state, collab_db, builder_config, folder_notifier) - )] - pub async fn create_folder( - &self, - object: CollabObject, - doc_state: DataSource, - collab_db: Weak<CollabKVDB>, - builder_config: CollabBuilderConfig, - folder_notifier: Option<FolderNotify>, - folder_data: Option<FolderData>, - ) -> Result<Arc<RwLock<Folder>>, Error> { - let expected_collab_type = CollabType::Folder; - assert_eq!(object.collab_type, expected_collab_type); - let folder = match folder_data { - None => { - let collab = self.build_collab(&object, &collab_db, doc_state).await?; - Folder::open(object.uid, collab, folder_notifier)? - }, - Some(data) => { - let collab = self.build_collab(&object, &collab_db, doc_state).await?; - let folder = Folder::create(object.uid, collab, folder_notifier, data); - if let Err(err) = self.write_collab_to_disk( - object.uid, - &object.workspace_id, - &object.object_id, - collab_db.clone(), - &object.collab_type, - &folder, - ) { - error!("build_collab: flush folder collab to disk failed: {}", err); - } - folder - }, - }; - let folder = Arc::new(RwLock::new(folder)); - self.finalize(object, builder_config, folder) - } - - #[allow(clippy::too_many_arguments)] - #[instrument( - level = "trace", - skip(self, object, doc_state, collab_db, builder_config, notifier) - )] - pub async fn create_user_awareness( - &self, - object: CollabObject, - doc_state: DataSource, - collab_db: Weak<CollabKVDB>, - builder_config: CollabBuilderConfig, - notifier: Option<UserAwarenessNotifier>, - ) -> Result<Arc<RwLock<UserAwareness>>, Error> { - let expected_collab_type = CollabType::UserAwareness; - assert_eq!(object.collab_type, expected_collab_type); - let collab = self.build_collab(&object, &collab_db, doc_state).await?; - let user_awareness = UserAwareness::create(collab, notifier)?; - let user_awareness = Arc::new(RwLock::new(user_awareness)); - self.finalize(object, builder_config, user_awareness) - } - - #[allow(clippy::too_many_arguments)] - #[instrument(level = "trace", skip_all)] - pub fn create_workspace_database_manager( - &self, - object: CollabObject, - collab: Collab, - _collab_db: Weak<CollabKVDB>, - builder_config: CollabBuilderConfig, - collab_service: impl DatabaseCollabService, - ) -> Result<Arc<RwLock<WorkspaceDatabaseManager>>, Error> { - let expected_collab_type = CollabType::WorkspaceDatabase; - assert_eq!(object.collab_type, expected_collab_type); - let workspace = WorkspaceDatabaseManager::open(&object.object_id, collab, collab_service)?; - let workspace = Arc::new(RwLock::new(workspace)); - self.finalize(object, builder_config, workspace) - } - - pub async fn build_collab( - &self, - object: &CollabObject, - collab_db: &Weak<CollabKVDB>, - data_source: DataSource, - ) -> Result<Collab, Error> { - let object = object.clone(); - let collab_db = collab_db.clone(); - let device_id = self.workspace_integrate.device_id()?; - let collab = tokio::task::spawn_blocking(move || { - let collab = CollabBuilder::new(object.uid, &object.object_id, data_source) - .with_device_id(device_id) - .build()?; - let persistence_config = CollabPersistenceConfig::default(); - let db_plugin = RocksdbDiskPlugin::new_with_config( - object.uid, - object.workspace_id.clone(), - object.object_id.to_string(), - object.collab_type, - collab_db, - persistence_config, - ); - collab.add_plugin(Box::new(db_plugin)); - Ok::<_, Error>(collab) - }) - .await??; - - Ok(collab) - } - - pub fn finalize<T>( - &self, - object: CollabObject, - build_config: CollabBuilderConfig, - collab: Arc<RwLock<T>>, - ) -> Result<Arc<RwLock<T>>, Error> - where - T: BorrowMut<Collab> + Send + Sync + 'static, - { - let mut write_collab = collab.try_write()?; - let has_cloud_plugin = write_collab.borrow().has_cloud_plugin(); - if has_cloud_plugin { - drop(write_collab); - return Ok(collab); + #[cfg(target_arch = "wasm32")] + { + collab.lock().add_plugin(Box::new(IndexeddbDiskPlugin::new( + uid, + object_id.to_string(), + object_type.clone(), + collab_db.clone(), + ))); } - if build_config.sync_enable { - trace!("🚀finalize collab:{}", object); - let plugin_provider = self.plugin_provider.load_full(); - let provider_type = plugin_provider.provider_type(); - let span = - tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object.object_id); - let _enter = span.enter(); - match provider_type { - CollabPluginProviderType::AppFlowyCloud => { - let local_collab = Arc::downgrade(&collab); - let plugins = plugin_provider.get_plugins(CollabPluginProviderContext::AppFlowyCloud { - uid: object.uid, - collab_object: object, - local_collab, - }); + #[cfg(not(target_arch = "wasm32"))] + { + collab + .lock() + .add_plugin(Box::new(RocksdbDiskPlugin::new_with_config( + uid, + object_id.to_string(), + object_type.clone(), + collab_db.clone(), + persistence_config.clone(), + None, + ))); + } - // at the moment when we get the lock, the collab object is not yet exposed outside - for plugin in plugins { - write_collab.borrow().add_plugin(plugin); - } - }, - CollabPluginProviderType::Local => {}, + let arc_collab = Arc::new(collab); + + { + let collab_object = self.collab_object(uid, object_id, object_type.clone())?; + if build_config.sync_enable { + let provider_type = self.plugin_provider.read().provider_type(); + let span = tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object_id); + let _enter = span.enter(); + match provider_type { + CollabPluginProviderType::AppFlowyCloud => { + let local_collab = Arc::downgrade(&arc_collab); + let plugins = + self + .plugin_provider + .read() + .get_plugins(CollabPluginProviderContext::AppFlowyCloud { + uid, + collab_object, + local_collab, + }); + + for plugin in plugins { + arc_collab.lock().add_plugin(plugin); + } + }, + CollabPluginProviderType::Supabase => { + #[cfg(not(target_arch = "wasm32"))] + { + trace!("init supabase collab plugins"); + let local_collab = Arc::downgrade(&arc_collab); + let local_collab_db = collab_db.clone(); + let plugins = + self + .plugin_provider + .read() + .get_plugins(CollabPluginProviderContext::Supabase { + uid, + collab_object, + local_collab, + local_collab_db, + }); + for plugin in plugins { + arc_collab.lock().add_plugin(plugin); + } + } + }, + CollabPluginProviderType::Local => {}, + } } } - (*write_collab).borrow_mut().initialize(); - drop(write_collab); - Ok(collab) - } + if build_config.auto_initialize { + #[cfg(target_arch = "wasm32")] + futures::executor::block_on(arc_collab.lock().initialize()); - /// Remove all updates in disk and write the final state vector to disk. - #[instrument(level = "trace", skip_all, err)] - pub fn write_collab_to_disk<T>( - &self, - uid: i64, - workspace_id: &str, - object_id: &str, - collab_db: Weak<CollabKVDB>, - collab_type: &CollabType, - collab: &T, - ) -> Result<(), Error> - where - T: BorrowMut<Collab> + Send + Sync + 'static, - { - if let Some(collab_db) = collab_db.upgrade() { - let write_txn = collab_db.write_txn(); - trace!( - "flush workspace: {} {}:collab:{} to disk", - workspace_id, - collab_type, - object_id - ); - let collab: &Collab = collab.borrow(); - let encode_collab = - collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab))?; - write_txn.flush_doc( - uid, - workspace_id, - object_id, - encode_collab.state_vector.to_vec(), - encode_collab.doc_state.to_vec(), - )?; - write_txn.commit_transaction()?; - } else { - error!("collab_db is dropped"); + #[cfg(not(target_arch = "wasm32"))] + arc_collab.lock().initialize(); } - Ok(()) + trace!("collab initialized: {}:{}", object_type, object_id); + Ok(arc_collab) } } pub struct CollabBuilderConfig { pub sync_enable: bool, + /// If auto_initialize is false, the collab object will not be initialized automatically. + /// You need to call collab.initialize() manually. + /// + /// Default is true. + pub auto_initialize: bool, } impl Default for CollabBuilderConfig { fn default() -> Self { - Self { sync_enable: true } + Self { + sync_enable: true, + auto_initialize: true, + } } } @@ -399,85 +322,9 @@ impl CollabBuilderConfig { self.sync_enable = sync_enable; self } -} -pub struct CollabPersistenceImpl { - pub db: Weak<CollabKVDB>, - pub uid: i64, - pub workspace_id: Uuid, -} - -impl CollabPersistenceImpl { - pub fn new(db: Weak<CollabKVDB>, uid: i64, workspace_id: Uuid) -> Self { - Self { - db, - uid, - workspace_id, - } - } - - pub fn into_data_source(self) -> DataSource { - DataSource::Disk(Some(Box::new(self))) - } -} - -impl CollabPersistence for CollabPersistenceImpl { - fn load_collab_from_disk(&self, collab: &mut Collab) -> Result<(), CollabError> { - let collab_db = self - .db - .upgrade() - .ok_or_else(|| CollabError::Internal(anyhow!("collab_db is dropped")))?; - - let object_id = collab.object_id().to_string(); - let rocksdb_read = collab_db.read_txn(); - let workspace_id = self.workspace_id.to_string(); - - if rocksdb_read.is_exist(self.uid, &workspace_id, &object_id) { - let mut txn = collab.transact_mut(); - match rocksdb_read.load_doc_with_txn(self.uid, &workspace_id, &object_id, &mut txn) { - Ok(update_count) => { - trace!( - "did load collab:{}-{} from disk, update_count:{}", - self.uid, - object_id, - update_count - ); - }, - Err(err) => { - error!("🔴 load doc:{} failed: {}", object_id, err); - }, - } - drop(rocksdb_read); - txn.commit(); - drop(txn); - } - Ok(()) - } - - fn save_collab_to_disk( - &self, - object_id: &str, - encoded_collab: EncodedCollab, - ) -> Result<(), CollabError> { - let workspace_id = self.workspace_id.to_string(); - let collab_db = self - .db - .upgrade() - .ok_or_else(|| CollabError::Internal(anyhow!("collab_db is dropped")))?; - let write_txn = collab_db.write_txn(); - write_txn - .flush_doc( - self.uid, - workspace_id.as_str(), - object_id, - encoded_collab.state_vector.to_vec(), - encoded_collab.doc_state.to_vec(), - ) - .map_err(|err| CollabError::Internal(err.into()))?; - - write_txn - .commit_transaction() - .map_err(|err| CollabError::Internal(err.into()))?; - Ok(()) + pub fn auto_initialize(mut self, auto_initialize: bool) -> Self { + self.auto_initialize = auto_initialize; + self } } diff --git a/frontend/rust-lib/collab-integrate/src/lib.rs b/frontend/rust-lib/collab-integrate/src/lib.rs index afa6f0c2a8..a7df75d72e 100644 --- a/frontend/rust-lib/collab-integrate/src/lib.rs +++ b/frontend/rust-lib/collab-integrate/src/lib.rs @@ -1,11 +1,25 @@ +pub use collab::core::collab::MutexCollab; pub use collab::preclude::Snapshot; pub use collab_plugins::local_storage::CollabPersistenceConfig; pub use collab_plugins::CollabKVDB; +use collab_plugins::{if_native, if_wasm}; pub mod collab_builder; pub mod config; -pub mod persistence; -mod plugin_provider; + +if_native! { + mod native; + mod plugin_provider { + pub use crate::native::plugin_provider::*; + } +} + +if_wasm! { + mod wasm; + mod plugin_provider { + pub use crate::wasm::plugin_provider::*; + } +} pub use collab_plugins::local_storage::kv::doc::CollabKVAction; pub use collab_plugins::local_storage::kv::error::PersistenceError; diff --git a/frontend/rust-lib/collab-integrate/src/native/mod.rs b/frontend/rust-lib/collab-integrate/src/native/mod.rs new file mode 100644 index 0000000000..59b6b263d5 --- /dev/null +++ b/frontend/rust-lib/collab-integrate/src/native/mod.rs @@ -0,0 +1 @@ +pub mod plugin_provider; diff --git a/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs new file mode 100644 index 0000000000..a26fb8d933 --- /dev/null +++ b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs @@ -0,0 +1,56 @@ +use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; +use collab::preclude::CollabPlugin; + +#[cfg(target_arch = "wasm32")] +pub trait CollabCloudPluginProvider: 'static { + fn provider_type(&self) -> CollabPluginProviderType; + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>>; + + fn is_sync_enabled(&self) -> bool; +} + +#[cfg(target_arch = "wasm32")] +impl<T> CollabCloudPluginProvider for std::rc::Rc<T> +where + T: CollabCloudPluginProvider, +{ + fn provider_type(&self) -> CollabPluginProviderType { + (**self).provider_type() + } + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { + (**self).get_plugins(context) + } + + fn is_sync_enabled(&self) -> bool { + (**self).is_sync_enabled() + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub trait CollabCloudPluginProvider: Send + Sync + 'static { + fn provider_type(&self) -> CollabPluginProviderType; + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>>; + + fn is_sync_enabled(&self) -> bool; +} + +#[cfg(not(target_arch = "wasm32"))] +impl<T> CollabCloudPluginProvider for std::sync::Arc<T> +where + T: CollabCloudPluginProvider, +{ + fn provider_type(&self) -> CollabPluginProviderType { + (**self).provider_type() + } + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { + (**self).get_plugins(context) + } + + fn is_sync_enabled(&self) -> bool { + (**self).is_sync_enabled() + } +} diff --git a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs b/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs deleted file mode 100644 index adb8b72de1..0000000000 --- a/frontend/rust-lib/collab-integrate/src/persistence/collab_metadata_sql.rs +++ /dev/null @@ -1,62 +0,0 @@ -use diesel::upsert::excluded; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::{ - diesel, insert_into, - query_dsl::*, - schema::{af_collab_metadata, af_collab_metadata::dsl}, - DBConnection, ExpressionMethods, Identifiable, Insertable, Queryable, -}; -use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; - -#[derive(Queryable, Insertable, Identifiable)] -#[diesel(table_name = af_collab_metadata)] -#[diesel(primary_key(object_id))] -pub struct AFCollabMetadata { - pub object_id: String, - pub updated_at: i64, - pub prev_sync_state_vector: Vec<u8>, - pub collab_type: i32, -} - -pub fn batch_insert_collab_metadata( - mut conn: DBConnection, - new_metadata: &[AFCollabMetadata], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - for metadata in new_metadata { - let _ = insert_into(af_collab_metadata::table) - .values(metadata) - .on_conflict(af_collab_metadata::object_id) - .do_update() - .set(( - af_collab_metadata::updated_at.eq(excluded(af_collab_metadata::updated_at)), - af_collab_metadata::prev_sync_state_vector - .eq(excluded(af_collab_metadata::prev_sync_state_vector)), - )) - .execute(conn)?; - } - Ok::<(), FlowyError>(()) - })?; - - Ok(()) -} - -pub fn batch_select_collab_metadata( - mut conn: DBConnection, - object_ids: &[Uuid], -) -> FlowyResult<HashMap<Uuid, AFCollabMetadata>> { - let object_ids = object_ids - .iter() - .map(|id| id.to_string()) - .collect::<Vec<String>>(); - - let metadata = dsl::af_collab_metadata - .filter(af_collab_metadata::object_id.eq_any(&object_ids)) - .load::<AFCollabMetadata>(&mut conn)? - .into_iter() - .flat_map(|m| Uuid::from_str(&m.object_id).map(|v| (v, m))) - .collect(); - Ok(metadata) -} diff --git a/frontend/rust-lib/collab-integrate/src/persistence/mod.rs b/frontend/rust-lib/collab-integrate/src/persistence/mod.rs deleted file mode 100644 index dc0eb77c28..0000000000 --- a/frontend/rust-lib/collab-integrate/src/persistence/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod collab_metadata_sql; diff --git a/frontend/rust-lib/collab-integrate/src/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/plugin_provider.rs deleted file mode 100644 index fcbcbbe9ee..0000000000 --- a/frontend/rust-lib/collab-integrate/src/plugin_provider.rs +++ /dev/null @@ -1,28 +0,0 @@ -use collab::preclude::CollabPlugin; - -use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; - -pub trait CollabCloudPluginProvider: Send + Sync + 'static { - fn provider_type(&self) -> CollabPluginProviderType; - - fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>>; - - fn is_sync_enabled(&self) -> bool; -} - -impl<U> CollabCloudPluginProvider for std::sync::Arc<U> -where - U: CollabCloudPluginProvider, -{ - fn provider_type(&self) -> CollabPluginProviderType { - (**self).provider_type() - } - - fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { - (**self).get_plugins(context) - } - - fn is_sync_enabled(&self) -> bool { - (**self).is_sync_enabled() - } -} diff --git a/frontend/rust-lib/collab-integrate/src/wasm/mod.rs b/frontend/rust-lib/collab-integrate/src/wasm/mod.rs new file mode 100644 index 0000000000..59b6b263d5 --- /dev/null +++ b/frontend/rust-lib/collab-integrate/src/wasm/mod.rs @@ -0,0 +1 @@ +pub mod plugin_provider; diff --git a/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs new file mode 100644 index 0000000000..545e6c461c --- /dev/null +++ b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs @@ -0,0 +1,29 @@ +use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; +use collab::preclude::CollabPlugin; +use lib_infra::future::Fut; +use std::rc::Rc; + +pub trait CollabCloudPluginProvider: 'static { + fn provider_type(&self) -> CollabPluginProviderType; + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>>; + + fn is_sync_enabled(&self) -> bool; +} + +impl<T> CollabCloudPluginProvider for Rc<T> +where + T: CollabCloudPluginProvider, +{ + fn provider_type(&self) -> CollabPluginProviderType { + (**self).provider_type() + } + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { + (**self).get_plugins(context) + } + + fn is_sync_enabled(&self) -> bool { + (**self).is_sync_enabled() + } +} diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 969f64e6f9..bca0489e7b 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -22,12 +22,13 @@ serde_json.workspace = true bytes.workspace = true crossbeam-utils = "0.8.15" lazy_static = "1.4.0" +parking_lot.workspace = true tracing.workspace = true lib-log.workspace = true semver = "1.0.22" # workspace -lib-dispatch = { workspace = true, features = ["local_set"] } +lib-dispatch = { workspace = true } # Core #flowy-core = { workspace = true, features = ["profiling"] } @@ -36,6 +37,7 @@ flowy-core = { workspace = true } flowy-notification = { workspace = true, features = ["dart"] } flowy-document = { workspace = true, features = ["dart"] } +flowy-config = { workspace = true, features = ["dart"] } flowy-user = { workspace = true, features = ["dart"] } flowy-date = { workspace = true, features = ["dart"] } flowy-server = { workspace = true } @@ -44,7 +46,6 @@ collab-integrate = { workspace = true } flowy-derive.workspace = true serde_yaml = "0.9.27" flowy-error = { workspace = true, features = ["impl_from_sqlite", "impl_from_dispatch_error", "impl_from_appflowy_cloud", "impl_from_reqwest", "impl_from_serde", "dart"] } -futures = "0.3.31" [features] default = ["dart"] diff --git a/frontend/rust-lib/dart-ffi/src/env_serde.rs b/frontend/rust-lib/dart-ffi/src/env_serde.rs index a59b28361f..db443a78f7 100644 --- a/frontend/rust-lib/dart-ffi/src/env_serde.rs +++ b/frontend/rust-lib/dart-ffi/src/env_serde.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use serde::Deserialize; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_server_pub::AuthenticatorType; #[derive(Deserialize, Debug)] @@ -16,6 +17,7 @@ pub struct AppFlowyDartConfiguration { pub device_id: String, pub platform: String, pub authenticator_type: AuthenticatorType, + pub(crate) supabase_config: SupabaseConfiguration, pub(crate) appflowy_cloud_config: AFCloudConfiguration, #[serde(default)] pub(crate) envs: HashMap<String, String>, @@ -29,6 +31,7 @@ impl AppFlowyDartConfiguration { pub fn write_env(&self) { self.authenticator_type.write_env(); self.appflowy_cloud_config.write_env(); + self.supabase_config.write_env(); for (k, v) in self.envs.iter() { std::env::set_var(k, v); diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 6c3d08ae01..aecd9bef28 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -1,16 +1,11 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] use allo_isolate::Isolate; -use futures::ready; use lazy_static::lazy_static; +use parking_lot::Mutex; use semver::Version; -use std::future::Future; -use std::pin::Pin; -use std::sync::{Arc, RwLock}; -use std::task::{Context, Poll}; +use std::sync::Arc; use std::{ffi::CStr, os::raw::c_char}; -use tokio::sync::mpsc; -use tokio::task::LocalSet; use tracing::{debug, error, info, trace, warn}; use flowy_core::config::AppFlowyCoreConfig; @@ -38,77 +33,33 @@ mod notification; mod protobuf; lazy_static! { - static ref DART_APPFLOWY_CORE: DartAppFlowyCore = DartAppFlowyCore::new(); - static ref LOG_STREAM_ISOLATE: RwLock<Option<Isolate>> = RwLock::new(None); + static ref APPFLOWY_CORE: MutexAppFlowyCore = MutexAppFlowyCore::new(); + static ref LOG_STREAM_ISOLATE: Mutex<Option<Isolate>> = Mutex::new(None); } -pub struct Task { - dispatcher: Arc<AFPluginDispatcher>, - request: AFPluginRequest, - port: i64, - ret: Option<mpsc::Sender<AFPluginEventResponse>>, -} +struct MutexAppFlowyCore(Arc<Mutex<Option<AppFlowyCore>>>); -unsafe impl Send for Task {} -unsafe impl Sync for DartAppFlowyCore {} - -struct DartAppFlowyCore { - core: Arc<RwLock<Option<AppFlowyCore>>>, - handle: RwLock<Option<std::thread::JoinHandle<()>>>, - sender: RwLock<Option<mpsc::UnboundedSender<Task>>>, -} - -impl DartAppFlowyCore { +impl MutexAppFlowyCore { fn new() -> Self { - Self { - #[allow(clippy::arc_with_non_send_sync)] - core: Arc::new(RwLock::new(None)), - handle: RwLock::new(None), - sender: RwLock::new(None), - } + Self(Arc::new(Mutex::new(None))) } fn dispatcher(&self) -> Option<Arc<AFPluginDispatcher>> { - let binding = self - .core - .read() - .expect("Failed to acquire read lock for core"); + let binding = self.0.lock(); let core = binding.as_ref(); core.map(|core| core.event_dispatcher.clone()) } - - fn dispatch( - &self, - request: AFPluginRequest, - port: i64, - ret: Option<mpsc::Sender<AFPluginEventResponse>>, - ) { - if let Ok(sender_guard) = self.sender.read() { - if let Err(e) = sender_guard.as_ref().unwrap().send(Task { - dispatcher: self.dispatcher().unwrap(), - request, - port, - ret, - }) { - error!("Failed to send task: {}", e); - } - } else { - warn!("Failed to acquire read lock for sender"); - } - } } +unsafe impl Sync for MutexAppFlowyCore {} +unsafe impl Send for MutexAppFlowyCore {} + #[no_mangle] pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { - let c_str = unsafe { - if data.is_null() { - return -1; - } - CStr::from_ptr(data) - }; - let serde_str = c_str - .to_str() - .expect("Failed to convert C string to Rust string"); + // and sent it the `Rust's` result + // no need to convert anything :) + let c_str = unsafe { CStr::from_ptr(data) }; + let serde_str = c_str.to_str().unwrap(); let configuration = AppFlowyDartConfiguration::from_str(serde_str); configuration.write_env(); @@ -133,28 +84,25 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { DEFAULT_NAME.to_string(), ); - if let Some(core) = &*DART_APPFLOWY_CORE.core.write().unwrap() { + // Ensure that the database is closed before initialization. Also, verify that the init_sdk function can be called + // multiple times (is reentrant). Currently, only the database resource is exclusive. + if let Some(core) = &*APPFLOWY_CORE.0.lock() { core.close_db(); } - let log_stream = LOG_STREAM_ISOLATE - .write() - .unwrap() - .take() - .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc<dyn StreamLogSender>); - let (sender, task_rx) = mpsc::unbounded_channel::<Task>(); let runtime = Arc::new(AFPluginRuntime::new().unwrap()); let cloned_runtime = runtime.clone(); - let handle = std::thread::spawn(move || { - let local_set = LocalSet::new(); - cloned_runtime.block_on(local_set.run_until(Runner { rx: task_rx })); - }); - *DART_APPFLOWY_CORE.sender.write().unwrap() = Some(sender); - *DART_APPFLOWY_CORE.handle.write().unwrap() = Some(handle); - let cloned_runtime = runtime.clone(); - *DART_APPFLOWY_CORE.core.write().unwrap() = runtime - .block_on(async move { Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) }); + let log_stream = LOG_STREAM_ISOLATE + .lock() + .take() + .map(|isolate| Arc::new(LogStreamSenderImpl { isolate }) as Arc<dyn StreamLogSender>); + + // let isolate = allo_isolate::Isolate::new(port); + *APPFLOWY_CORE.0.lock() = runtime.block_on(async move { + Some(AppFlowyCore::new(config, cloned_runtime, log_stream).await) + // isolate.post("".to_string()); + }); 0 } @@ -162,7 +110,7 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { #[allow(clippy::let_underscore_future)] pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); - #[cfg(feature = "verbose_log")] + #[cfg(feature = "sync_verbose_log")] trace!( "[FFI]: {} Async Event: {:?} with {} port", &request.id, @@ -170,55 +118,40 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { port ); - DART_APPFLOWY_CORE.dispatch(request, port, None); -} - -/// A persistent future that processes [Arbiter] commands. -struct Runner { - rx: mpsc::UnboundedReceiver<Task>, -} - -impl Future for Runner { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { - loop { - match ready!(self.rx.poll_recv(cx)) { - None => return Poll::Ready(()), - Some(task) => { - let Task { - dispatcher, - request, - port, - ret, - } = task; - - tokio::task::spawn_local(async move { - let resp = AFPluginDispatcher::boxed_async_send_with_callback( - dispatcher.as_ref(), - request, - move |resp: AFPluginEventResponse| { - #[cfg(feature = "verbose_log")] - trace!("[FFI]: Post data to dart through {} port", port); - Box::pin(post_to_flutter(resp, port)) - }, - ) - .await; - - if let Some(ret) = ret { - let _ = ret.send(resp).await; - } - }); - }, - } - } - } + let dispatcher = match APPFLOWY_CORE.dispatcher() { + None => { + error!("sdk not init yet."); + return; + }, + Some(dispatcher) => dispatcher, + }; + AFPluginDispatcher::boxed_async_send_with_callback( + dispatcher.as_ref(), + request, + move |resp: AFPluginEventResponse| { + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: Post data to dart through {} port", port); + Box::pin(post_to_flutter(resp, port)) + }, + ); } #[no_mangle] -pub extern "C" fn sync_event(_input: *const u8, _len: usize) -> *const u8 { - error!("unimplemented sync_event"); +pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 { + let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); + #[cfg(feature = "sync_verbose_log")] + trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,); + let dispatcher = match APPFLOWY_CORE.dispatcher() { + None => { + error!("sdk not init yet."); + return forget_rust(Vec::default()); + }, + Some(dispatcher) => dispatcher, + }; + let _response = AFPluginDispatcher::sync_send(dispatcher, request); + + // FFIResponse { } let response_bytes = vec![]; let result = extend_front_four_bytes_into_bytes(&response_bytes); forget_rust(result) @@ -226,6 +159,7 @@ pub extern "C" fn sync_event(_input: *const u8, _len: usize) -> *const u8 { #[no_mangle] pub extern "C" fn set_stream_port(notification_port: i64) -> i32 { + // Make sure hot reload won't register the notification sender twice unregister_all_notification_sender(); register_notification_sender(DartNotificationSender::new(notification_port)); 0 @@ -233,7 +167,8 @@ pub extern "C" fn set_stream_port(notification_port: i64) -> i32 { #[no_mangle] pub extern "C" fn set_log_stream_port(port: i64) -> i32 { - *LOG_STREAM_ISOLATE.write().unwrap() = Some(Isolate::new(port)); + *LOG_STREAM_ISOLATE.lock() = Some(Isolate::new(port)); + 0 } @@ -242,9 +177,9 @@ pub extern "C" fn set_log_stream_port(port: i64) -> i32 { pub extern "C" fn link_me_please() {} #[inline(always)] -#[allow(clippy::blocks_in_conditions)] async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { let isolate = allo_isolate::Isolate::new(port); + #[allow(clippy::blocks_in_conditions)] match isolate .catch_unwind(async { let ffi_resp = FFIResponse::from(response); @@ -252,18 +187,23 @@ async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { }) .await { - Ok(_) => { - #[cfg(feature = "verbose_log")] + Ok(_success) => { + #[cfg(feature = "sync_verbose_log")] trace!("[FFI]: Post data to dart success"); }, - Err(err) => { - error!("[FFI]: allo_isolate post failed: {:?}", err); + Err(e) => { + if let Some(msg) = e.downcast_ref::<&str>() { + error!("[FFI]: {:?}", msg); + } else { + error!("[FFI]: allo_isolate post panic"); + } }, } } #[no_mangle] pub extern "C" fn rust_log(level: i64, data: *const c_char) { + // Check if the data pointer is not null if data.is_null() { error!("[flutter error]: null pointer provided to backend_log"); return; @@ -271,6 +211,7 @@ pub extern "C" fn rust_log(level: i64, data: *const c_char) { let log_result = unsafe { CStr::from_ptr(data) }.to_str(); + // Handle potential UTF-8 conversion error let log_str = match log_result { Ok(str) => str, Err(e) => { @@ -282,13 +223,29 @@ pub extern "C" fn rust_log(level: i64, data: *const c_char) { }, }; - match level { - 0 => info!("[Flutter]: {}", log_str), - 1 => debug!("[Flutter]: {}", log_str), - 2 => trace!("[Flutter]: {}", log_str), - 3 => warn!("[Flutter]: {}", log_str), - 4 => error!("[Flutter]: {}", log_str), - _ => warn!("[flutter error]: Unsupported log level: {}", level), + // Simplify logging by determining the log level outside of the match + let log_level = match level { + 0 => "info", + 1 => "debug", + 2 => "trace", + 3 => "warn", + 4 => "error", + _ => { + warn!("[flutter error]: Unsupported log level: {}", level); + return; + }, + }; + + // Log the message at the appropriate level + match log_level { + "info" => info!("[Flutter]: {}", log_str), + "debug" => debug!("[Flutter]: {}", log_str), + "trace" => trace!("[Flutter]: {}", log_str), + "warn" => warn!("[Flutter]: {}", log_str), + "error" => error!("[Flutter]: {}", log_str), + _ => { + warn!("[flutter error]: Unsupported log level: {}", log_level); + }, } } diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 6b2d5af7ba..aca158bfaf 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -12,15 +12,18 @@ flowy-user-pub = { workspace = true } flowy-folder = { path = "../flowy-folder", features = ["test_helper"] } flowy-folder-pub = { workspace = true } flowy-database2 = { path = "../flowy-database2" } +flowy-database-pub = { workspace = true } flowy-document = { path = "../flowy-document" } -flowy-ai = { workspace = true } +flowy-document-pub = { workspace = true } +flowy-encrypt = { workspace = true } +flowy-chat = { workspace = true } lib-dispatch = { workspace = true } lib-infra = { workspace = true } flowy-server = { path = "../flowy-server" } flowy-server-pub = { workspace = true } flowy-notification = { workspace = true } +anyhow.workspace = true flowy-storage = { workspace = true } -flowy-storage-pub = { workspace = true } flowy-search = { workspace = true } semver = "1.0.23" @@ -28,26 +31,33 @@ serde.workspace = true serde_json.workspace = true protobuf.workspace = true tokio = { workspace = true, features = ["full"] } +futures-util = "0.3.26" +thread-id = "3.3.0" bytes.workspace = true nanoid = "0.4.0" tracing.workspace = true +parking_lot.workspace = true uuid.workspace = true collab = { workspace = true } collab-document = { workspace = true } collab-folder = { workspace = true } collab-database = { workspace = true } +collab-plugins = { workspace = true } collab-entity = { workspace = true } rand = { version = "0.8.5", features = [] } strum = "0.25.0" [dev-dependencies] +dotenv = "0.15.0" +tempdir = "0.3.7" uuid.workspace = true assert-json-diff = "2.0.2" +tokio-postgres = { version = "0.7.8" } chrono = "0.4.31" -zip.workspace = true +zip = "0.6.6" walkdir = "2.5.0" -futures = "0.3.31" -flowy-ai-pub = { workspace = true } +futures = "0.3.30" +flowy-chat-pub = { workspace = true } [features] default = ["supabase_cloud_test"] diff --git a/frontend/rust-lib/event-integration-test/src/chat_event.rs b/frontend/rust-lib/event-integration-test/src/chat_event.rs index c8c638bc85..bb0090fe35 100644 --- a/frontend/rust-lib/event-integration-test/src/chat_event.rs +++ b/frontend/rust-lib/event-integration-test/src/chat_event.rs @@ -1,10 +1,10 @@ use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; -use flowy_ai::entities::{ +use flowy_chat::entities::{ ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB, SendChatPayloadPB, }; -use flowy_ai::event_map::AIEvent; +use flowy_chat::event_map::ChatEvent; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder::event_map::FolderEvent; @@ -13,6 +13,7 @@ impl EventIntegrationTest { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name: "chat".to_string(), + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Chat, initial_data: vec![], @@ -21,7 +22,6 @@ impl EventIntegrationTest { index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -44,7 +44,7 @@ impl EventIntegrationTest { }; EventBuilder::new(self.clone()) - .event(AIEvent::StreamMessage) + .event(ChatEvent::StreamMessage) .payload(payload) .async_send() .await; @@ -62,7 +62,7 @@ impl EventIntegrationTest { before_message_id, }; EventBuilder::new(self.clone()) - .event(AIEvent::LoadPrevMessage) + .event(ChatEvent::LoadPrevMessage) .payload(payload) .async_send() .await @@ -81,7 +81,7 @@ impl EventIntegrationTest { after_message_id, }; EventBuilder::new(self.clone()) - .event(AIEvent::LoadNextMessage) + .event(ChatEvent::LoadNextMessage) .payload(payload) .async_send() .await diff --git a/frontend/rust-lib/event-integration-test/src/database_event.rs b/frontend/rust-lib/event-integration-test/src/database_event.rs index fa195863a1..4b6968ec06 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -3,15 +3,14 @@ use std::convert::TryFrom; use bytes::Bytes; use collab_database::database::timestamp; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SingleSelectTypeOption, -}; use collab_database::fields::Field; use collab_database::rows::{Row, RowId}; use flowy_database2::entities::*; use flowy_database2::event_map::DatabaseEvent; use flowy_database2::services::cell::CellBuilder; -use flowy_database2::services::field::checklist_filter::ChecklistCellInsertChangeset; +use flowy_database2::services::field::{ + MultiSelectTypeOption, SelectOption, SingleSelectTypeOption, +}; use flowy_database2::services::share::csv::CSVFormat; use flowy_folder::entities::*; use flowy_folder::event_map::FolderEvent; @@ -25,7 +24,7 @@ impl EventIntegrationTest { self .appflowy_core .database_manager - .get_database_editor_with_view_id(database_view_id) + .get_database_with_view_id(database_view_id) .await .unwrap() .export_csv(CSVFormat::Original) @@ -38,6 +37,7 @@ impl EventIntegrationTest { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Grid, initial_data, @@ -46,7 +46,6 @@ impl EventIntegrationTest { index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -56,21 +55,21 @@ impl EventIntegrationTest { .parse::<ViewPB>() } - pub async fn open_database(&self, view_id: &str) -> DatabasePB { + pub async fn open_database(&self, view_id: &str) { EventBuilder::new(self.clone()) .event(DatabaseEvent::GetDatabase) .payload(DatabaseViewIdPB { value: view_id.to_string(), }) .async_send() - .await - .parse::<DatabasePB>() + .await; } pub async fn create_board(&self, parent_id: &str, name: String, initial_data: Vec<u8>) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Board, initial_data, @@ -79,7 +78,6 @@ impl EventIntegrationTest { index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -98,6 +96,7 @@ impl EventIntegrationTest { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Calendar, initial_data, @@ -106,7 +105,6 @@ impl EventIntegrationTest { index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -172,41 +170,6 @@ impl EventIntegrationTest { .error() } - pub async fn remove_calculate( - &self, - changeset: RemoveCalculationChangesetPB, - ) -> Option<FlowyError> { - EventBuilder::new(self.clone()) - .event(DatabaseEvent::RemoveCalculation) - .payload(changeset) - .async_send() - .await - .error() - } - - pub async fn get_all_calculations(&self, database_view_id: &str) -> RepeatedCalculationsPB { - EventBuilder::new(self.clone()) - .event(DatabaseEvent::GetAllCalculations) - .payload(DatabaseViewIdPB { - value: database_view_id.to_string(), - }) - .async_send() - .await - .parse::<RepeatedCalculationsPB>() - } - - pub async fn update_calculation( - &self, - changeset: UpdateCalculationChangesetPB, - ) -> Option<FlowyError> { - EventBuilder::new(self.clone()) - .event(DatabaseEvent::UpdateCalculation) - .payload(changeset) - .async_send() - .await - .error() - } - pub async fn update_field_type( &self, view_id: &str, @@ -219,7 +182,6 @@ impl EventIntegrationTest { view_id: view_id.to_string(), field_id: field_id.to_string(), field_type, - field_name: None, }) .async_send() .await @@ -298,7 +260,7 @@ impl EventIntegrationTest { pub async fn get_row(&self, view_id: &str, row_id: &str) -> OptionalRowPB { EventBuilder::new(self.clone()) .event(DatabaseEvent::GetRow) - .payload(DatabaseViewRowIdPB { + .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), group_id: None, @@ -311,7 +273,7 @@ impl EventIntegrationTest { pub async fn get_row_meta(&self, view_id: &str, row_id: &str) -> RowMetaPB { EventBuilder::new(self.clone()) .event(DatabaseEvent::GetRowMeta) - .payload(DatabaseViewRowIdPB { + .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), group_id: None, @@ -333,7 +295,7 @@ impl EventIntegrationTest { pub async fn duplicate_row(&self, view_id: &str, row_id: &str) -> Option<FlowyError> { EventBuilder::new(self.clone()) .event(DatabaseEvent::DuplicateRow) - .payload(DatabaseViewRowIdPB { + .payload(RowIdPB { view_id: view_id.to_string(), row_id: row_id.to_string(), group_id: None, @@ -507,6 +469,7 @@ impl EventIntegrationTest { &self, view_id: &str, group_id: &str, + field_id: &str, name: Option<String>, visible: Option<bool>, ) -> Option<FlowyError> { @@ -515,6 +478,7 @@ impl EventIntegrationTest { .payload(UpdateGroupPB { view_id: view_id.to_string(), group_id: group_id.to_string(), + field_id: field_id.to_string(), name, visible, }) @@ -623,14 +587,15 @@ impl<'a> TestRowBuilder<'a> { pub fn insert_date_cell( &mut self, - timestamp: i64, + date: i64, + time: Option<String>, include_time: Option<bool>, field_type: &FieldType, ) -> String { let date_field = self.field_with_type(field_type); self .cell_build - .insert_date_cell(&date_field.id, timestamp, include_time); + .insert_date_cell(&date_field.id, date, time, include_time); date_field.id.clone() } @@ -658,8 +623,7 @@ impl<'a> TestRowBuilder<'a> { let single_select_field = self.field_with_type(&FieldType::SingleSelect); let type_option = single_select_field .get_type_option::<SingleSelectTypeOption>(FieldType::SingleSelect) - .unwrap() - .0; + .unwrap(); let option = f(type_option.options); self .cell_build @@ -675,8 +639,7 @@ impl<'a> TestRowBuilder<'a> { let multi_select_field = self.field_with_type(&FieldType::MultiSelect); let type_option = multi_select_field .get_type_option::<MultiSelectTypeOption>(FieldType::MultiSelect) - .unwrap() - .0; + .unwrap(); let options = f(type_option.options); let ops_ids = options .iter() @@ -689,11 +652,11 @@ impl<'a> TestRowBuilder<'a> { multi_select_field.id.clone() } - pub fn insert_checklist_cell(&mut self, new_tasks: Vec<ChecklistCellInsertChangeset>) -> String { + pub fn insert_checklist_cell(&mut self, options: Vec<(String, bool)>) -> String { let checklist_field = self.field_with_type(&FieldType::Checklist); self .cell_build - .insert_checklist_cell(&checklist_field.id, new_tasks); + .insert_checklist_cell(&checklist_field.id, options); checklist_field.id.clone() } @@ -703,12 +666,6 @@ impl<'a> TestRowBuilder<'a> { time_field.id.clone() } - pub fn insert_media_cell(&mut self, media: String) -> String { - let media_field = self.field_with_type(&FieldType::Media); - self.cell_build.insert_text_cell(&media_field.id, media); - media_field.id.clone() - } - pub fn field_with_type(&self, field_type: &FieldType) -> Field { self .fields diff --git a/frontend/rust-lib/event-integration-test/src/document/document_event.rs b/frontend/rust-lib/event-integration-test/src/document/document_event.rs index 28fb03e9ed..8595ac056b 100644 --- a/frontend/rust-lib/event-integration-test/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document/document_event.rs @@ -1,6 +1,8 @@ use collab::entity::EncodedCollab; use std::collections::HashMap; +use serde_json::Value; + use flowy_document::entities::*; use flowy_document::event_map::DocumentEvent; use flowy_document::parser::parser_entities::{ @@ -9,8 +11,6 @@ use flowy_document::parser::parser_entities::{ }; use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder::event_map::FolderEvent; -use serde_json::Value; -use uuid::Uuid; use crate::document::utils::{gen_delta_str, gen_id, gen_text_block_data}; use crate::event_builder::EventBuilder; @@ -37,31 +37,18 @@ impl DocumentEventTest { Self { event_test: core } } - pub async fn get_encoded_v1(&self, doc_id: &Uuid) -> EncodedCollab { + pub async fn get_encoded_v1(&self, doc_id: &str) -> EncodedCollab { let doc = self .event_test .appflowy_core .document_manager - .editable_document(doc_id) + .get_document(doc_id) .await .unwrap(); - let guard = doc.read().await; + let guard = doc.lock(); guard.encode_collab().unwrap() } - pub async fn get_encoded_collab(&self, doc_id: &str) -> EncodedCollabPB { - let core = &self.event_test; - let payload = OpenDocumentPayloadPB { - document_id: doc_id.to_string(), - }; - EventBuilder::new(core.clone()) - .event(DocumentEvent::GetDocEncodedCollab) - .payload(payload) - .async_send() - .await - .parse::<EncodedCollabPB>() - } - pub async fn create_document(&self) -> ViewPB { let core = &self.event_test; let current_workspace = core.get_current_workspace().await; @@ -70,6 +57,7 @@ impl DocumentEventTest { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name: "document".to_string(), + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Document, initial_data: vec![], @@ -78,7 +66,6 @@ impl DocumentEventTest { index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(core.clone()) .event(FolderEvent::CreateView) diff --git a/frontend/rust-lib/event-integration-test/src/document_event.rs b/frontend/rust-lib/event-integration-test/src/document_event.rs index 5928c223b8..8374bc9de3 100644 --- a/frontend/rust-lib/event-integration-test/src/document_event.rs +++ b/frontend/rust-lib/event-integration-test/src/document_event.rs @@ -1,3 +1,6 @@ +use std::sync::Arc; + +use collab::core::collab::MutexCollab; use collab::core::origin::CollabOrigin; use collab::preclude::updates::decoder::Decode; use collab::preclude::{Collab, Update}; @@ -31,6 +34,7 @@ impl EventIntegrationTest { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, + desc: "".to_string(), thumbnail: None, layout: ViewLayoutPB::Document, initial_data, @@ -39,7 +43,6 @@ impl EventIntegrationTest { index: None, section: None, view_id: None, - extra: None, }; let view = EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -61,7 +64,6 @@ impl EventIntegrationTest { view } - pub async fn open_document(&self, doc_id: String) -> OpenDocumentData { let payload = OpenDocumentPayloadPB { document_id: doc_id.clone(), @@ -103,13 +105,17 @@ impl EventIntegrationTest { } pub fn assert_document_data_equal(doc_state: &[u8], doc_id: &str, expected: DocumentData) { - let mut collab = Collab::new_with_origin(CollabOrigin::Server, doc_id, vec![], false); - { + let collab = MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Server, + doc_id, + vec![], + false, + )); + collab.lock().with_origin_transact_mut(|txn| { let update = Update::decode_v1(doc_state).unwrap(); - let mut txn = collab.transact_mut(); - txn.apply_update(update).unwrap(); - }; - let document = Document::open(collab).unwrap(); + txn.apply_update(update); + }); + let document = Document::open(Arc::new(collab)).unwrap(); let actual = document.get_document_data().unwrap(); assert_eq!(actual, expected); } diff --git a/frontend/rust-lib/event-integration-test/src/event_builder.rs b/frontend/rust-lib/event-integration-test/src/event_builder.rs index b3d4a313f0..0d083b1037 100644 --- a/frontend/rust-lib/event-integration-test/src/event_builder.rs +++ b/frontend/rust-lib/event-integration-test/src/event_builder.rs @@ -1,27 +1,26 @@ -use crate::EventIntegrationTest; -use flowy_user::errors::{internal_error, FlowyError}; -use lib_dispatch::prelude::{ - AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, -}; -use std::sync::Arc; use std::{ convert::TryFrom, fmt::{Debug, Display}, hash::Hash, + sync::Arc, }; -use tokio::task::LocalSet; -// #[derive(Clone)] +use flowy_user::errors::{internal_error, FlowyError}; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginFromBytes, AFPluginRequest, ToBytes, *, +}; + +use crate::EventIntegrationTest; + +#[derive(Clone)] pub struct EventBuilder { context: TestContext, - local_set: LocalSet, } impl EventBuilder { pub fn new(sdk: EventIntegrationTest) -> Self { Self { context: TestContext::new(sdk), - local_set: Default::default(), } } @@ -51,13 +50,7 @@ impl EventBuilder { pub async fn async_send(mut self) -> Self { let request = self.get_request(); - let resp = self - .local_set - .run_until(AFPluginDispatcher::async_send( - self.dispatch().as_ref(), - request, - )) - .await; + let resp = AFPluginDispatcher::async_send(self.dispatch().as_ref(), request).await; self.context.response = Some(resp); self } diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 26515ab5af..47c2bfa45c 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -1,5 +1,3 @@ -use flowy_folder::view_operation::{GatherEncodedCollab, ViewData}; -use std::str::FromStr; use std::sync::Arc; use collab_folder::{FolderData, View}; @@ -7,22 +5,35 @@ use flowy_folder::entities::icon::UpdateViewIconPayloadPB; use flowy_folder::event_map::FolderEvent; use flowy_folder::event_map::FolderEvent::*; use flowy_folder::{entities::*, ViewLayout}; -use flowy_folder_pub::entities::PublishPayload; use flowy_search::services::manager::{SearchHandler, SearchType}; use flowy_user::entities::{ - AcceptWorkspaceInvitationPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, - RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, UserWorkspaceIdPB, UserWorkspacePB, - WorkspaceMemberInvitationPB, WorkspaceMemberPB, + AcceptWorkspaceInvitationPB, AddWorkspaceMemberPB, QueryWorkspacePB, RemoveWorkspaceMemberPB, + RepeatedWorkspaceInvitationPB, RepeatedWorkspaceMemberPB, WorkspaceMemberInvitationPB, + WorkspaceMemberPB, }; use flowy_user::errors::FlowyError; use flowy_user::event_map::UserEvent; use flowy_user_pub::entities::Role; -use uuid::Uuid; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; impl EventIntegrationTest { + pub async fn add_workspace_member(&self, workspace_id: &str, email: &str) { + if let Some(err) = EventBuilder::new(self.clone()) + .event(UserEvent::AddWorkspaceMember) + .payload(AddWorkspaceMemberPB { + workspace_id: workspace_id.to_string(), + email: email.to_string(), + }) + .async_send() + .await + .error() + { + panic!("Add workspace member failed: {:?}", err); + } + } + pub async fn invite_workspace_member(&self, workspace_id: &str, email: &str, role: Role) { EventBuilder::new(self.clone()) .event(UserEvent::InviteWorkspaceMember) @@ -35,26 +46,6 @@ impl EventIntegrationTest { .await; } - // convenient function to add workspace member by inviting and accepting the invitation - pub async fn add_workspace_member(&self, workspace_id: &str, other: &EventIntegrationTest) { - let other_email = other.get_user_profile().await.unwrap().email; - - self - .invite_workspace_member(workspace_id, &other_email, Role::Member) - .await; - - let invitations = other.list_workspace_invitations().await; - let target_invi = invitations - .items - .into_iter() - .find(|i| i.workspace_id == workspace_id) - .unwrap(); - - other - .accept_workspace_invitation(&target_invi.invite_id) - .await; - } - pub async fn list_workspace_invitations(&self) -> RepeatedWorkspaceInvitationPB { EventBuilder::new(self.clone()) .event(UserEvent::ListWorkspaceInvitations) @@ -112,18 +103,6 @@ impl EventIntegrationTest { .parse::<WorkspacePB>() } - pub async fn get_user_workspace(&self, workspace_id: &str) -> UserWorkspacePB { - let payload = UserWorkspaceIdPB { - workspace_id: workspace_id.to_string(), - }; - EventBuilder::new(self.clone()) - .event(UserEvent::GetUserWorkspace) - .payload(payload) - .async_send() - .await - .parse::<UserWorkspacePB>() - } - pub fn get_folder_search_handler(&self) -> &Arc<dyn SearchHandler> { self .appflowy_core @@ -137,17 +116,16 @@ impl EventIntegrationTest { let create_view_params = views .into_iter() .map(|view| CreateViewParams { - parent_view_id: Uuid::from_str(&view.parent_view_id).unwrap(), + parent_view_id: view.parent_view_id, name: view.name, + desc: "".to_string(), layout: view.layout.into(), - view_id: Uuid::from_str(&view.id).unwrap(), - initial_data: ViewData::Empty, + view_id: view.id, + initial_data: vec![], meta: Default::default(), set_as_current: false, index: None, section: None, - icon: view.icon, - extra: view.extra, }) .collect::<Vec<_>>(); @@ -155,7 +133,7 @@ impl EventIntegrationTest { self .appflowy_core .folder_manager - .create_view_with_params(params, true) + .create_view_with_params(params) .await .unwrap(); } @@ -167,6 +145,7 @@ impl EventIntegrationTest { pub async fn create_orphan_view(&self, name: &str, view_id: &str, layout: ViewLayoutPB) { let payload = CreateOrphanViewPayloadPB { name: name.to_string(), + desc: "".to_string(), layout, view_id: view_id.to_string(), initial_data: vec![], @@ -178,43 +157,12 @@ impl EventIntegrationTest { .await; } - pub async fn get_folder_data(&self) -> FolderData { - self - .appflowy_core - .folder_manager - .get_folder_data() - .await - .unwrap() - } - - pub async fn get_publish_payload( - &self, - view_id: &str, - include_children: bool, - ) -> Vec<PublishPayload> { - let manager = self.folder_manager.clone(); - let payload = manager - .get_batch_publish_payload(view_id, None, include_children) - .await; - - if payload.is_err() { - panic!("Get publish payload failed") - } - - payload.unwrap() - } - - pub async fn gather_encode_collab_from_disk( - &self, - view_id: &str, - layout: ViewLayout, - ) -> GatherEncodedCollab { - let view_id = Uuid::from_str(view_id).unwrap(); - self - .folder_manager - .gather_publish_encode_collab(&view_id, &layout) - .await - .unwrap() + pub fn get_folder_data(&self) -> FolderData { + let mutex_folder = self.appflowy_core.folder_manager.get_mutex_folder().clone(); + let folder_lock_guard = mutex_folder.read(); + let folder = folder_lock_guard.as_ref().unwrap(); + let workspace_id = self.appflowy_core.user_manager.workspace_id().unwrap(); + folder.get_folder_data(&workspace_id).clone().unwrap() } pub async fn get_all_workspace_views(&self) -> Vec<ViewPB> { @@ -226,16 +174,6 @@ impl EventIntegrationTest { .items } - // get all the views in the current workspace, including the views in the trash and the orphan views - pub async fn get_all_views(&self) -> Vec<ViewPB> { - EventBuilder::new(self.clone()) - .event(FolderEvent::GetAllViews) - .async_send() - .await - .parse::<RepeatedViewPB>() - .items - } - pub async fn get_trash(&self) -> RepeatedTrashPB { EventBuilder::new(self.clone()) .event(FolderEvent::ListTrashItems) @@ -281,29 +219,18 @@ impl EventIntegrationTest { } pub async fn create_view(&self, parent_id: &str, name: String) -> ViewPB { - self - .create_view_with_layout(parent_id, name, Default::default()) - .await - } - - pub async fn create_view_with_layout( - &self, - parent_id: &str, - name: String, - layout: ViewLayoutPB, - ) -> ViewPB { let payload = CreateViewPayloadPB { parent_view_id: parent_id.to_string(), name, + desc: "".to_string(), thumbnail: None, - layout, + layout: Default::default(), initial_data: vec![], meta: Default::default(), set_as_current: false, index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -324,14 +251,13 @@ impl EventIntegrationTest { .parse::<ViewPB>() } - pub async fn import_data(&self, data: ImportPayloadPB) -> Vec<ViewPB> { + pub async fn import_data(&self, data: ImportPB) -> ViewPB { EventBuilder::new(self.clone()) .event(FolderEvent::ImportData) .payload(data) .async_send() .await - .parse::<RepeatedViewPB>() - .items + .parse::<ViewPB>() } pub async fn get_view_ancestors(&self, view_id: &str) -> Vec<ViewPB> { @@ -360,6 +286,7 @@ impl ViewTest { let payload = CreateViewPayloadPB { parent_view_id: workspace.id.clone(), name: "View A".to_string(), + desc: "".to_string(), thumbnail: Some("http://1.png".to_string()), layout: layout.into(), initial_data: data, @@ -368,7 +295,6 @@ impl ViewTest { index: None, section: None, view_id: None, - extra: None, }; let view = EventBuilder::new(sdk.clone()) @@ -397,3 +323,18 @@ impl ViewTest { Self::new(sdk, ViewLayout::Calendar, data).await } } + +#[allow(dead_code)] +async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { + let request = CreateWorkspacePayloadPB { + name: name.to_owned(), + desc: desc.to_owned(), + }; + + EventBuilder::new(sdk.clone()) + .event(CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .parse::<WorkspacePB>() +} diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index ff0a3847df..2ae16c74b3 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -1,28 +1,28 @@ -use crate::user_event::TestNotificationSender; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; use collab_document::blocks::DocumentData; use collab_document::document::Document; use collab_entity::CollabType; +use std::env::temp_dir; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; + +use nanoid::nanoid; +use parking_lot::RwLock; +use semver::Version; +use tokio::select; +use tokio::time::sleep; + use flowy_core::config::AppFlowyCoreConfig; use flowy_core::AppFlowyCore; use flowy_notification::register_notification_sender; -use flowy_user::entities::AuthTypePB; +use flowy_server::AppFlowyServer; +use flowy_user::entities::AuthenticatorPB; use flowy_user::errors::FlowyError; use lib_dispatch::runtime::AFPluginRuntime; -use nanoid::nanoid; -use semver::Version; -use std::env::temp_dir; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::select; -use tokio::task::LocalSet; -use tokio::time::sleep; -use uuid::Uuid; + +use crate::user_event::TestNotificationSender; mod chat_event; pub mod database_event; @@ -34,16 +34,13 @@ pub mod user_event; #[derive(Clone)] pub struct EventIntegrationTest { - pub authenticator: Arc<AtomicU8>, + pub authenticator: Arc<RwLock<AuthenticatorPB>>, pub appflowy_core: AppFlowyCore, #[allow(dead_code)] cleaner: Arc<Cleaner>, pub notification_sender: TestNotificationSender, - local_set: Arc<LocalSet>, } -pub const SINGLE_FILE_UPLOAD_SIZE: usize = 15 * 1024 * 1024; - impl EventIntegrationTest { pub async fn new() -> Self { Self::new_with_name(nanoid!(6)).await @@ -55,30 +52,12 @@ impl EventIntegrationTest { Self::new_with_user_data_path(temp_dir, name.to_string()).await } - pub async fn new_with_config(config: AppFlowyCoreConfig) -> Self { - let clean_path = config.storage_path.clone(); - let inner = init_core(config).await; - let notification_sender = TestNotificationSender::new(); - let authenticator = Arc::new(AtomicU8::new(AuthTypePB::Local as u8)); - register_notification_sender(notification_sender.clone()); - - // In case of dropping the runtime that runs the core, we need to forget the dispatcher - std::mem::forget(inner.dispatcher()); - Self { - appflowy_core: inner, - authenticator, - notification_sender, - cleaner: Arc::new(Cleaner::new(PathBuf::from(clean_path))), - #[allow(clippy::arc_with_non_send_sync)] - local_set: Arc::new(Default::default()), - } - } - pub async fn new_with_user_data_path(path_buf: PathBuf, name: String) -> Self { let path = path_buf.to_str().unwrap().to_string(); let device_id = uuid::Uuid::new_v4().to_string(); - let mut config = AppFlowyCoreConfig::new( - Version::new(0, 7, 0), + + let config = AppFlowyCoreConfig::new( + Version::new(0, 5, 8), path.clone(), path, device_id, @@ -94,14 +73,19 @@ impl EventIntegrationTest { ], ); - if let Some(cloud_config) = config.cloud_config.as_mut() { - cloud_config.maximum_upload_file_size_in_bytes = Some(SINGLE_FILE_UPLOAD_SIZE as u64); - } - Self::new_with_config(config).await - } + let inner = init_core(config).await; + let notification_sender = TestNotificationSender::new(); + let authenticator = Arc::new(RwLock::new(AuthenticatorPB::Local)); + register_notification_sender(notification_sender.clone()); - pub fn skip_clean(&mut self) { - self.cleaner.should_clean.store(false, Ordering::Release); + // In case of dropping the runtime that runs the core, we need to forget the dispatcher + std::mem::forget(inner.dispatcher()); + Self { + appflowy_core: inner, + authenticator, + notification_sender, + cleaner: Arc::new(Cleaner(path_buf)), + } } pub fn instance_name(&self) -> String { @@ -112,25 +96,16 @@ impl EventIntegrationTest { self.appflowy_core.config.application_path.clone() } + pub fn get_server(&self) -> Arc<dyn AppFlowyServer> { + self.appflowy_core.server_provider.get_server().unwrap() + } + pub async fn wait_ws_connected(&self) { - if self - .appflowy_core - .server_provider - .get_server() - .unwrap() - .get_ws_state() - .is_connected() - { + if self.get_server().get_ws_state().is_connected() { return; } - let mut ws_state = self - .appflowy_core - .server_provider - .get_server() - .unwrap() - .subscribe_ws_state() - .unwrap(); + let mut ws_state = self.get_server().subscribe_ws_state().unwrap(); loop { select! { _ = sleep(Duration::from_secs(20)) => { @@ -152,19 +127,12 @@ impl EventIntegrationTest { oid: &str, collab_type: CollabType, ) -> Result<Vec<u8>, FlowyError> { - let server = self.server_provider.get_server()?; - + let server = self.server_provider.get_server().unwrap(); let workspace_id = self.get_current_workspace().await.id; - let oid = Uuid::from_str(oid)?; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_folder_doc_state( - &Uuid::from_str(&workspace_id).unwrap(), - uid, - collab_type, - &oid, - ) + .get_folder_doc_state(&workspace_id, uid, collab_type, oid) .await?; Ok(doc_state) @@ -178,21 +146,23 @@ pub fn document_data_from_document_doc_state(doc_id: &str, doc_state: Vec<u8>) - } pub fn document_from_document_doc_state(doc_id: &str, doc_state: Vec<u8>) -> Document { - let collab = Collab::new_with_source( + Document::from_doc_state( CollabOrigin::Empty, - doc_id, DataSource::DocStateV1(doc_state), + doc_id, vec![], - true, ) - .unwrap(); - Document::open(collab).unwrap() + .unwrap() } async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { - let runtime = Arc::new(AFPluginRuntime::new().unwrap()); - let cloned_runtime = runtime.clone(); - AppFlowyCore::new(config, cloned_runtime, None).await + std::thread::spawn(|| { + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(async move { AppFlowyCore::new(config, cloned_runtime, None).await }) + }) + .join() + .unwrap() } impl std::ops::Deref for EventIntegrationTest { @@ -203,17 +173,11 @@ impl std::ops::Deref for EventIntegrationTest { } } -pub struct Cleaner { - dir: PathBuf, - should_clean: AtomicBool, -} +pub struct Cleaner(PathBuf); impl Cleaner { pub fn new(dir: PathBuf) -> Self { - Self { - dir, - should_clean: AtomicBool::new(true), - } + Cleaner(dir) } fn cleanup(dir: &PathBuf) { @@ -223,8 +187,6 @@ impl Cleaner { impl Drop for Cleaner { fn drop(&mut self) { - if self.should_clean.load(Ordering::Acquire) { - Self::cleanup(&self.dir) - } + Self::cleanup(&self.0) } } diff --git a/frontend/rust-lib/event-integration-test/src/user_event.rs b/frontend/rust-lib/event-integration-test/src/user_event.rs index 821c3c9a1d..40e8044806 100644 --- a/frontend/rust-lib/event-integration-test/src/user_event.rs +++ b/frontend/rust-lib/event-integration-test/src/user_event.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; use std::convert::TryFrom; -use std::sync::atomic::Ordering; use std::sync::Arc; use bytes::Bytes; + use flowy_folder::entities::{RepeatedViewPB, WorkspacePB}; +use nanoid::nanoid; use protobuf::ProtobufError; use tokio::sync::broadcast::{channel, Sender}; use tracing::error; @@ -17,15 +18,14 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthTypePB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, - OauthSignInPB, OpenUserWorkspacePB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, - SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, - UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, + AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, + OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, + SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, + UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; -use flowy_user_pub::entities::AuthType; -use lib_dispatch::prelude::{AFPluginDispatcher, AFPluginRequest, ToBytes}; +use lib_dispatch::prelude::{af_spawn, AFPluginDispatcher, AFPluginRequest, ToBytes}; use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; @@ -65,19 +65,14 @@ impl EventIntegrationTest { email, name: "appflowy".to_string(), password: password.clone(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: uuid::Uuid::new_v4().to_string(), } .into_bytes() .unwrap(); let request = AFPluginRequest::new(UserEvent::SignUp).payload(payload); - let user_profile = self - .local_set - .run_until(AFPluginDispatcher::async_send( - &self.appflowy_core.dispatcher(), - request, - )) + let user_profile = AFPluginDispatcher::async_send(&self.appflowy_core.dispatcher(), request) .await .parse::<UserProfilePB, FlowyError>() .unwrap() @@ -92,18 +87,22 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_up(&self) -> UserProfilePB { let email = unique_email(); - match self.af_cloud_sign_in_with_email(&email).await { - Ok(profile) => profile, - Err(err) => { - tracing::warn!( - "Failed to sign up with email: {}, error: {}, retrying", - email, - err - ); - tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - self.af_cloud_sign_in_with_email(&email).await.unwrap() - }, - } + self.af_cloud_sign_in_with_email(&email).await.unwrap() + } + + pub async fn supabase_party_sign_up(&self) -> UserProfilePB { + let map = third_party_sign_up_param(Uuid::new_v4().to_string()); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, + }; + + EventBuilder::new(self.clone()) + .event(UserEvent::OauthSignIn) + .payload(payload) + .async_send() + .await + .parse::<UserProfilePB>() } pub async fn sign_out(&self) { @@ -113,8 +112,8 @@ impl EventIntegrationTest { .await; } - pub fn set_auth_type(&self, auth_type: AuthTypePB) { - self.authenticator.store(auth_type as u8, Ordering::Release); + pub fn set_auth_type(&self, auth_type: AuthenticatorPB) { + *self.authenticator.write() = auth_type; } pub async fn init_anon_user(&self) -> UserProfilePB { @@ -140,7 +139,7 @@ impl EventIntegrationTest { pub async fn af_cloud_sign_in_with_email(&self, email: &str) -> FlowyResult<UserProfilePB> { let payload = SignInUrlPayloadPB { email: email.to_string(), - authenticator: AuthTypePB::Server, + authenticator: AuthenticatorPB::AppFlowyCloud, }; let sign_in_url = EventBuilder::new(self.clone()) .event(UserEvent::GenerateSignInURL) @@ -155,7 +154,34 @@ impl EventIntegrationTest { map.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); let payload = OauthSignInPB { map, - authenticator: AuthTypePB::Server, + authenticator: AuthenticatorPB::AppFlowyCloud, + }; + + let user_profile = EventBuilder::new(self.clone()) + .event(UserEvent::OauthSignIn) + .payload(payload) + .async_send() + .await + .try_parse::<UserProfilePB>()?; + + Ok(user_profile) + } + + pub async fn supabase_sign_up_with_uuid( + &self, + uuid: &str, + email: Option<String>, + ) -> FlowyResult<UserProfilePB> { + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid.to_string()); + map.insert(USER_DEVICE_ID.to_string(), uuid.to_string()); + map.insert( + USER_EMAIL.to_string(), + email.unwrap_or_else(|| format!("{}@appflowy.io", nanoid!(10))), + ); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, }; let user_profile = EventBuilder::new(self.clone()) @@ -176,7 +202,6 @@ impl EventIntegrationTest { let payload = ImportAppFlowyDataPB { path, import_container_name: name, - parent_view_id: None, }; match EventBuilder::new(self.clone()) .event(UserEvent::ImportAppFlowyDataFolder) @@ -190,10 +215,9 @@ impl EventIntegrationTest { } } - pub async fn create_workspace(&self, name: &str, auth_type: AuthType) -> UserWorkspacePB { + pub async fn create_workspace(&self, name: &str) -> UserWorkspacePB { let payload = CreateWorkspacePB { name: name.to_string(), - auth_type: auth_type.into(), }; EventBuilder::new(self.clone()) .event(UserEvent::CreateWorkspace) @@ -280,10 +304,9 @@ impl EventIntegrationTest { .await; } - pub async fn open_workspace(&self, workspace_id: &str, auth_type: AuthTypePB) { - let payload = OpenUserWorkspacePB { + pub async fn open_workspace(&self, workspace_id: &str) { + let payload = UserWorkspaceIdPB { workspace_id: workspace_id.to_string(), - workspace_auth_type: auth_type, }; EventBuilder::new(self.clone()) .event(UserEvent::OpenWorkspace) @@ -331,7 +354,7 @@ impl TestNotificationSender { let (tx, rx) = tokio::sync::mpsc::channel::<T>(10); let mut receiver = self.sender.subscribe(); let ty = ty.into(); - tokio::spawn(async move { + af_spawn(async move { // DatabaseNotification::DidUpdateDatabaseSnapshotState while let Ok(value) = receiver.recv().await { if value.id == id && value.ty == ty { @@ -364,7 +387,7 @@ impl TestNotificationSender { let (tx, rx) = tokio::sync::mpsc::channel::<()>(10); let mut receiver = self.sender.subscribe(); let ty = ty.into(); - tokio::spawn(async move { + af_spawn(async move { // DatabaseNotification::DidUpdateDatabaseSnapshotState while let Ok(value) = receiver.recv().await { if value.id == id && value.ty == ty { @@ -383,7 +406,7 @@ impl TestNotificationSender { let id = id.to_string(); let (tx, rx) = tokio::sync::mpsc::channel::<T>(1); let mut receiver = self.sender.subscribe(); - tokio::spawn(async move { + af_spawn(async move { while let Ok(value) = receiver.recv().await { if value.id == id { if let Some(payload) = value.payload { @@ -435,7 +458,7 @@ pub struct SignUpContext { pub password: String, } -pub async fn use_localhost_af_cloud() { +pub async fn user_localhost_af_cloud() { AuthenticatorType::AppFlowyCloud.write_env(); let base_url = std::env::var("af_cloud_test_base_url").unwrap_or("http://localhost:8000".to_string()); @@ -447,8 +470,6 @@ pub async fn use_localhost_af_cloud() { base_url, ws_base_url, gotrue_url, - enable_sync_trace: true, - maximum_upload_file_size_in_bytes: None, } .write_env(); std::env::set_var("GOTRUE_ADMIN_EMAIL", "admin@example.com"); @@ -460,5 +481,5 @@ pub async fn user_localhost_af_cloud_with_nginx() { std::env::set_var("af_cloud_test_base_url", "http://localhost"); std::env::set_var("af_cloud_test_ws_url", "ws://localhost/ws/v1"); std::env::set_var("af_cloud_test_gotrue_url", "http://localhost/gotrue"); - use_localhost_af_cloud().await + user_localhost_af_cloud().await } diff --git a/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip b/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip deleted file mode 100644 index 835bfdf527..0000000000 Binary files a/frontend/rust-lib/event-integration-test/tests/asset/064_database_publish.zip and /dev/null differ diff --git a/frontend/rust-lib/event-integration-test/tests/asset/data_ref_doc.zip b/frontend/rust-lib/event-integration-test/tests/asset/data_ref_doc.zip deleted file mode 100644 index 43fc7dde2f..0000000000 Binary files a/frontend/rust-lib/event-integration-test/tests/asset/data_ref_doc.zip and /dev/null differ diff --git a/frontend/rust-lib/event-integration-test/tests/asset/project.csv b/frontend/rust-lib/event-integration-test/tests/asset/project.csv deleted file mode 100644 index 7f55c29797..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/asset/project.csv +++ /dev/null @@ -1,5 +0,0 @@ -Project name,Status,Owner,Dates,Priority,Completion,Tasks,Blocked By,Is Blocking,Summary,Delay,Checkbox,Files & media -Research study,In Progress,Nate Martins,"April 23, 2024 → May 22, 2024",High,0.25,"Develop survey questions (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20survey%20questions%2086ce25c6bb214b4b9e35beed8bc8b821.md), Interpret findings (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Interpret%20findings%20c418ae6385f94dcdadca1423ad60146b.md), Write research report (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Write%20research%20report%20cca8f4321cd44dceba9c266cdf544f15.md), Conduct interviews (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Conduct%20interviews%208f40c17883904d769fee19baeb0d46a5.md)",,Marketing campaign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Marketing%20campaign%2088ac0cea4cb245efb44d63ace0a37d1e.md),"A research study is underway to understand customer satisfaction and identify improvement areas. The team is developing a survey to gather data from customers, aiming to analyze insights for strategic decision-making and enhance satisfaction.",7,No,Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/DO010003572.jpeg -Marketing campaign,In Progress,Sohrab Amin,"April 24, 2024 → May 7, 2024",Low,0.5,"Define target audience (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Define%20target%20audience%2045e2d3342bfe44848009f8e19e65b2af.md), Develop advertising plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20advertising%20plan%20a8e534ad763040029d0feb27fdb1820d.md), Report on marketing ROI (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Report%20on%20marketing%20ROI%202458b6a0f0174f72af3e5daac8b36b08.md), Create social media plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20social%20media%20plan%204e70ea0b7d40427a9648bcf554a121f6.md), Create performance marketing plan (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20performance%20marketing%20plan%20b6aa6a9e9cc1446490984eaecc4930c7.md), Develop new creative assets (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20new%20creative%20assets%203268a1d63424427ba1f7e3cac109cefa.md)",Research study (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Research%20study%20e445ee1fb7ff4591be2de17d906df97e.md),Website redesign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Website%20redesign%20bb934cb9f0e44daab4368247d62b0f39.md),"The marketing campaign, led by Sohrab Amin, focuses on enhancing mobile performance, building on last year's success in reducing page load times by 50%. The team aims to further improve performance by investing in native components for iOS and Android from April 24 to May 7, 2024.",19,No,Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/appflowy_2x.png -Website redesign,Planning,Nate Martins,"May 10, 2024 → May 26, 2024",Medium,,"Conduct website audit (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Conduct%20website%20audit%20aee812f0c962462e8b91e8044a268eb5.md), Write website copy (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Write%20website%20copy%209ab9309ceae34f6f8dc19b2eeda8f8f2.md), Test website functionality (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Test%20website%20functionality%208ccf7153028747b19a351f6d36100f0d.md), Develop creative assets (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Develop%20creative%20assets%206fe86230e30843ebb60c67724f0f922f.md)",Marketing campaign (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Projects%2058b8977d6e4444a98ec4d64176a071e5/Marketing%20campaign%2088ac0cea4cb245efb44d63ace0a37d1e.md),,"The website redesign project, led by Nate Martins from May 10 to May 26, 2024, aims to create a more effective onboarding process to enhance user experience and increase 7-day retention by 25%.",,Yes, -Product launch,In Progress,Ben Lang,"May 8, 2024 → May 22, 2024",High,0.6666666666666666,"Create product demo video (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20product%20demo%20video%202e2e2eb53df84019a613fe386e4c79da.md), Create product positioning (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Create%20product%20positioning%20c0d2728f79594cc3a1869a3c67bcdf45.md), Monitor launch performance (Projects%20&%20Tasks%20104d4deadd2c805fb3abcaab6d3727e7/Tasks%2076aaf8a4637542ed8175259692ca08bb/Monitor%20launch%20performance%207c0b846d1da2463892eff490f06e0d0b.md)",,,"A new product launch is scheduled from May 8 to May 22, 2024, initiated to fill a market gap and expand the product line. The development team is focused on creating a high-quality product, while the marketing team is crafting a strategy to achieve a 10% market share within the first six months post-launch.",16,Yes, \ No newline at end of file diff --git a/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip b/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip deleted file mode 100644 index 785449c8be..0000000000 Binary files a/frontend/rust-lib/event-integration-test/tests/asset/publish_grid_primary.csv.zip and /dev/null differ diff --git a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs index aacba827c4..8594193ab6 100644 --- a/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/chat/chat_message_test.rs @@ -1,40 +1,36 @@ use crate::util::receive_with_timeout; -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; -use flowy_ai::entities::ChatMessageListPB; -use flowy_ai::notification::ChatNotification; -use std::str::FromStr; - -use flowy_ai_pub::cloud::ChatMessageType; +use flowy_chat::entities::ChatMessageListPB; +use flowy_chat::notification::ChatNotification; +use flowy_chat_pub::cloud::ChatMessageType; +use futures_util::StreamExt; use std::time::Duration; -use uuid::Uuid; #[tokio::test] async fn af_cloud_create_chat_message_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; test.af_cloud_sign_up().await; let current_workspace = test.get_current_workspace().await; let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test - .appflowy_core - .server_provider - .get_server() - .unwrap() - .chat_service(); + let chat_service = test.server_provider.get_server().unwrap().chat_service(); for i in 0..10 { - let _ = chat_service - .create_question( - &Uuid::from_str(¤t_workspace.id).unwrap(), - &Uuid::from_str(&chat_id).unwrap(), + let mut stream = chat_service + .send_chat_message( + ¤t_workspace.id, + &chat_id, &format!("hello world {}", i), ChatMessageType::System, ) .await .unwrap(); + while let Some(message) = stream.next().await { + message.unwrap(); + } } let rx = test .notification_sender @@ -71,7 +67,7 @@ async fn af_cloud_create_chat_message_test() { #[tokio::test] async fn af_cloud_load_remote_system_message_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; test.af_cloud_sign_up().await; @@ -79,30 +75,30 @@ async fn af_cloud_load_remote_system_message_test() { let view = test.create_chat(¤t_workspace.id).await; let chat_id = view.id.clone(); - let chat_service = test - .appflowy_core - .server_provider - .get_server() - .unwrap() - .chat_service(); + let chat_service = test.server_provider.get_server().unwrap().chat_service(); for i in 0..10 { - let _ = chat_service - .create_question( - &Uuid::from_str(¤t_workspace.id).unwrap(), - &Uuid::from_str(&chat_id).unwrap(), + let mut stream = chat_service + .send_chat_message( + ¤t_workspace.id, + &chat_id, &format!("hello server {}", i), ChatMessageType::System, ) .await .unwrap(); + while let Some(message) = stream.next().await { + message.unwrap(); + } } let rx = test .notification_sender .subscribe::<ChatMessageListPB>(&chat_id, ChatNotification::DidLoadLatestChatMessage); + // Previous messages were created by the server, so there are no messages in the local cache. + // It will try to load messages in the background. let all = test.load_next_message(&chat_id, 5, None).await; - assert_eq!(all.messages.len(), 5); + assert!(all.messages.is_empty()); // Wait for the messages to be loaded. let next_back_five = receive_with_timeout(rx, Duration::from_secs(60)) @@ -127,6 +123,7 @@ async fn af_cloud_load_remote_system_message_test() { let first_five_messages = receive_with_timeout(rx, Duration::from_secs(60)) .await .unwrap(); + assert!(!first_five_messages.has_more); assert_eq!(first_five_messages.messages[0].content, "hello server 4"); assert_eq!(first_five_messages.messages[1].content, "hello server 3"); assert_eq!(first_five_messages.messages[2].content, "hello server 2"); diff --git a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs index e040b3d23b..25ef9920c8 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/af_cloud/util.rs @@ -1,18 +1,16 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; -use collab_database::entity::DatabaseView; -use collab_database::views::DatabaseLayout; +use collab_database::views::{DatabaseLayout, DatabaseView}; use event_integration_test::database_event::TestRowBuilder; -use collab_database::fields::number_type_option::{NumberFormat, NumberTypeOption}; -use collab_database::fields::select_type_option::{ - SelectOption, SelectOptionColor, SingleSelectTypeOption, -}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::translate_type_option::TranslateTypeOption; use collab_database::fields::Field; use collab_database::rows::Row; use flowy_database2::entities::FieldType; -use flowy_database2::services::field::FieldBuilder; +use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption; +use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption; +use flowy_database2::services::field::{ + FieldBuilder, NumberFormat, NumberTypeOption, SelectOption, SelectOptionColor, + SingleSelectTypeOption, +}; use flowy_database2::services::field_settings::default_field_settings_for_fields; use strum::IntoEnumIterator; @@ -31,14 +29,15 @@ pub fn make_test_summary_grid() -> DatabaseData { .type_options .get(&FieldType::SingleSelect.to_string()) .cloned() - .map(|t| SingleSelectTypeOption::from(t).0.options) + .map(|t| SingleSelectTypeOption::from(t).options) .unwrap(); let rows = create_rows(&database_id, &fields, options); + let inline_view_id = gen_database_view_id(); let view = DatabaseView { database_id: database_id.clone(), - id: gen_database_view_id(), + id: inline_view_id.clone(), name: "".to_string(), layout: DatabaseLayout::Grid, field_settings, @@ -47,6 +46,7 @@ pub fn make_test_summary_grid() -> DatabaseData { DatabaseData { database_id, + inline_view_id, views: vec![view], fields, rows, diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/calculate_test.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/calculate_test.rs deleted file mode 100644 index 7412f54951..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/database/local_test/calculate_test.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::time::Duration; - -use crate::util::gen_csv_import_data; -use event_integration_test::EventIntegrationTest; -use flowy_database2::entities::{ - CalculationType, CellChangesetPB, DatabasePB, RemoveCalculationChangesetPB, - UpdateCalculationChangesetPB, -}; -use tokio::time::sleep; - -#[tokio::test] -async fn calculation_integration_test1() { - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let workspace_id = test.get_current_workspace().await.id; - let payload = gen_csv_import_data("project.csv", &workspace_id); - let view = test.import_data(payload).await.pop().unwrap(); - let database = test.open_database(&view.id).await; - - average_calculation(test, database, &view.id).await; -} - -// Tests for the CalculationType::Average -// Is done on the Delay column in the project.csv -async fn average_calculation( - test: EventIntegrationTest, - database: DatabasePB, - database_view_id: &str, -) { - // Delay column is the 11th column (index 10) in the project.csv - let delay_field = database.fields.get(10).unwrap(); - - let calculation_changeset = UpdateCalculationChangesetPB { - view_id: database_view_id.to_string(), - calculation_id: None, - field_id: delay_field.field_id.clone(), - calculation_type: CalculationType::Average, - }; - - test.update_calculation(calculation_changeset).await; - - // Wait for calculation update - sleep(Duration::from_secs(1)).await; - - let all_calculations = test.get_all_calculations(database_view_id).await; - assert!(all_calculations.items.len() == 1); - - let average_calc = all_calculations.items.first().unwrap(); - assert!( - average_calc.value == "14.00", - "Expected 14.00, got {}", - average_calc.value - ); - - // Update a cell in the delay column at fourth row (3rd index) - let cell_changeset = CellChangesetPB { - view_id: database_view_id.to_string(), - row_id: database.rows.get(3).unwrap().id.clone(), - field_id: delay_field.field_id.clone(), - cell_changeset: "22".to_string(), - }; - test.update_cell(cell_changeset).await; - - // wait for awhile because the calculate is done in the background - tokio::time::sleep(Duration::from_secs(6)).await; - - let all_calculations = test.get_all_calculations(database_view_id).await; - assert!( - all_calculations.items.len() == 1, - "Expected 1, got {}", - all_calculations.items.len() - ); - - let average_calc = all_calculations.items.first().unwrap(); - assert!( - average_calc.value == "16.00", - "Expected 16.00, got {}", - average_calc.value - ); - - // Remove the calculation - test - .remove_calculate(RemoveCalculationChangesetPB { - view_id: database_view_id.to_string(), - field_id: delay_field.field_id.clone(), - calculation_id: average_calc.id.clone(), - }) - .await; -} diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/event_test.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/event_test.rs deleted file mode 100644 index 885d1b6817..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/database/local_test/event_test.rs +++ /dev/null @@ -1,917 +0,0 @@ -use std::convert::TryFrom; - -use bytes::Bytes; - -use event_integration_test::event_builder::EventBuilder; -use event_integration_test::EventIntegrationTest; -use flowy_database2::entities::{ - CellChangesetPB, CellIdPB, CheckboxCellDataPB, ChecklistCellDataChangesetPB, - ChecklistCellInsertPB, DatabaseLayoutPB, DatabaseSettingChangesetPB, DatabaseViewIdPB, - DateCellChangesetPB, FieldType, OrderObjectPositionPB, RelationCellChangesetPB, - SelectOptionCellDataPB, UpdateRowMetaChangesetPB, -}; -use lib_infra::util::timestamp; - -#[tokio::test] -async fn get_database_id_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // The view id can be used to get the database id. - let database_id = EventBuilder::new(test.clone()) - .event(flowy_database2::event_map::DatabaseEvent::GetDatabaseId) - .payload(DatabaseViewIdPB { - value: grid_view.id.clone(), - }) - .async_send() - .await - .parse::<flowy_database2::entities::DatabaseIdPB>() - .value; - - assert_ne!(database_id, grid_view.id); -} - -#[tokio::test] -async fn get_database_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.fields.len(), 3); - assert_eq!(database.rows.len(), 3); - assert_eq!(database.layout_type, DatabaseLayoutPB::Grid); -} - -#[tokio::test] -async fn get_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields[0].field_type, FieldType::RichText); - assert_eq!(fields[1].field_type, FieldType::SingleSelect); - assert_eq!(fields[2].field_type, FieldType::Checkbox); - assert_eq!(fields.len(), 3); -} - -#[tokio::test] -async fn create_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - test.create_field(&grid_view.id, FieldType::Checkbox).await; - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields.len(), 4); - assert_eq!(fields[3].field_type, FieldType::Checkbox); -} - -#[tokio::test] -async fn delete_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields[0].field_type, FieldType::RichText); - assert_eq!(fields[1].field_type, FieldType::SingleSelect); - assert_eq!(fields[2].field_type, FieldType::Checkbox); - - let error = test.delete_field(&grid_view.id, &fields[1].id).await; - assert!(error.is_none()); - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields.len(), 2); -} - -// The primary field is not allowed to be deleted. -#[tokio::test] -async fn delete_primary_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - // the primary field is not allowed to be deleted. - assert!(fields[0].is_primary); - let error = test.delete_field(&grid_view.id, &fields[0].id).await; - assert!(error.is_some()); -} - -#[tokio::test] -async fn update_field_type_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - let error = test - .update_field_type(&grid_view.id, &fields[1].id, FieldType::Checklist) - .await; - assert!(error.is_none()); - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields[1].field_type, FieldType::Checklist); -} - -#[tokio::test] -async fn update_primary_field_type_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - // the primary field is not allowed to be deleted. - assert!(fields[0].is_primary); - - // the primary field is not allowed to be updated. - let error = test - .update_field_type(&grid_view.id, &fields[0].id, FieldType::Checklist) - .await; - assert!(error.is_some()); -} - -#[tokio::test] -async fn duplicate_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - // the primary field is not allowed to be updated. - let error = test.duplicate_field(&grid_view.id, &fields[1].id).await; - assert!(error.is_none()); - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - assert_eq!(fields.len(), 4); -} - -// The primary field is not allowed to be duplicated. So this test should return an error. -#[tokio::test] -async fn duplicate_primary_field_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let fields = test.get_all_database_fields(&grid_view.id).await.items; - // the primary field is not allowed to be duplicated. - let error = test.duplicate_field(&grid_view.id, &fields[0].id).await; - assert!(error.is_some()); -} - -#[tokio::test] -async fn get_primary_field_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // By default the primary field type is RichText. - let field = test.get_primary_field(&grid_view.id).await; - assert_eq!(field.field_type, FieldType::RichText); -} - -#[tokio::test] -async fn create_row_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let _ = test - .create_row(&grid_view.id, OrderObjectPositionPB::default(), None) - .await; - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 4); -} - -#[tokio::test] -async fn delete_row_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // delete the row - let database = test.get_database(&grid_view.id).await; - let remove_row_id = database.rows[0].id.clone(); - assert_eq!(database.rows.len(), 3); - let error = test.delete_row(&grid_view.id, &remove_row_id).await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 2); - - // get the row again and check if it is deleted. - let optional_row = test.get_row(&grid_view.id, &remove_row_id).await; - assert!(optional_row.row.is_none()); -} - -#[tokio::test] -async fn get_row_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - - let row = test.get_row(&grid_view.id, &database.rows[0].id).await.row; - assert!(row.is_some()); - - let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; - assert!(row.document_id.is_some()); -} - -#[tokio::test] -async fn update_row_meta_event_with_url_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - - // By default the row icon is None. - let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; - assert_eq!(row.icon, None); - - // Insert icon url to the row. - let changeset = UpdateRowMetaChangesetPB { - id: database.rows[0].id.clone(), - view_id: grid_view.id.clone(), - icon_url: Some("icon_url".to_owned()), - cover: None, - is_document_empty: None, - attachment_count: None, - }; - let error = test.update_row_meta(changeset).await; - assert!(error.is_none()); - - // Check if the icon is updated. - let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; - assert_eq!(row.icon, Some("icon_url".to_owned())); -} - -#[tokio::test] -async fn update_row_meta_event_with_cover_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - - // By default the row icon is None. - let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; - assert_eq!(row.icon, None); - - // Insert cover to the row. - let changeset = UpdateRowMetaChangesetPB { - id: database.rows[0].id.clone(), - view_id: grid_view.id.clone(), - icon_url: Some("cover url".to_owned()), - cover: None, - is_document_empty: None, - attachment_count: None, - }; - let error = test.update_row_meta(changeset).await; - assert!(error.is_none()); - - // Check if the icon is updated. - let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; - assert_eq!(row.icon, Some("cover url".to_owned())); -} - -#[tokio::test] -async fn delete_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // delete the row with empty row_id. It should do nothing - let error = test.delete_row(&grid_view.id, "").await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 3); -} - -#[tokio::test] -async fn duplicate_row_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let error = test - .duplicate_row(&grid_view.id, &database.rows[0].id) - .await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 4); -} - -#[tokio::test] -async fn duplicate_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 3); - - let error = test.duplicate_row(&grid_view.id, "").await; - assert!(error.is_some()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows.len(), 3); -} - -#[tokio::test] -async fn move_row_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let row_1 = database.rows[0].id.clone(); - let row_2 = database.rows[1].id.clone(); - let row_3 = database.rows[2].id.clone(); - let error = test.move_row(&grid_view.id, &row_1, &row_3).await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows[0].id, row_2); - assert_eq!(database.rows[1].id, row_3); - assert_eq!(database.rows[2].id, row_1); -} - -#[tokio::test] -async fn move_row_event_test2() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let row_1 = database.rows[0].id.clone(); - let row_2 = database.rows[1].id.clone(); - let row_3 = database.rows[2].id.clone(); - let error = test.move_row(&grid_view.id, &row_2, &row_1).await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows[0].id, row_2); - assert_eq!(database.rows[1].id, row_1); - assert_eq!(database.rows[2].id, row_3); -} - -#[tokio::test] -async fn move_row_event_with_invalid_row_id_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let row_1 = database.rows[0].id.clone(); - let row_2 = database.rows[1].id.clone(); - let row_3 = database.rows[2].id.clone(); - - for i in 0..2 { - if i == 0 { - let error = test.move_row(&grid_view.id, &row_1, "").await; - assert!(error.is_some()); - } else { - let error = test.move_row(&grid_view.id, "", &row_1).await; - assert!(error.is_some()); - } - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.rows[0].id, row_1); - assert_eq!(database.rows[1].id, row_2); - assert_eq!(database.rows[2].id, row_3); - } -} - -#[tokio::test] -async fn update_text_cell_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let fields = test.get_all_database_fields(&grid_view.id).await.items; - - let row_id = database.rows[0].id.clone(); - let field_id = fields[0].id.clone(); - assert_eq!(fields[0].field_type, FieldType::RichText); - - // Update the first cell of the first row. - let error = test - .update_cell(CellChangesetPB { - view_id: grid_view.id.clone(), - row_id: row_id.clone(), - field_id: field_id.clone(), - cell_changeset: "hello world".to_string(), - }) - .await; - assert!(error.is_none()); - - let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; - let s = String::from_utf8(cell.data).unwrap(); - assert_eq!(s, "hello world"); -} - -#[tokio::test] -async fn update_checkbox_cell_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let fields = test.get_all_database_fields(&grid_view.id).await.items; - - let row_id = database.rows[0].id.clone(); - let field_id = fields[2].id.clone(); - assert_eq!(fields[2].field_type, FieldType::Checkbox); - - for input in &["yes", "true", "1"] { - let error = test - .update_cell(CellChangesetPB { - view_id: grid_view.id.clone(), - row_id: row_id.clone(), - field_id: field_id.clone(), - cell_changeset: input.to_string(), - }) - .await; - assert!(error.is_none()); - - let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; - let output = CheckboxCellDataPB::try_from(Bytes::from(cell.data)).unwrap(); - assert!(output.is_checked); - } -} - -#[tokio::test] -async fn update_single_select_cell_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let fields = test.get_all_database_fields(&grid_view.id).await.items; - let row_id = database.rows[0].id.clone(); - let field_id = fields[1].id.clone(); - assert_eq!(fields[1].field_type, FieldType::SingleSelect); - - // Insert a new option. This should update the cell with the new option. - let error = test - .insert_option(&grid_view.id, &field_id, &row_id, "task 1") - .await; - assert!(error.is_none()); - - // Check that the cell data is updated. - let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; - let select_option_cell = SelectOptionCellDataPB::try_from(Bytes::from(cell.data)).unwrap(); - - assert_eq!(select_option_cell.select_options.len(), 1); -} - -#[tokio::test] -async fn update_date_cell_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - - // Create a date field - let date_field = test.create_field(&grid_view.id, FieldType::DateTime).await; - - let cell_path = CellIdPB { - view_id: grid_view.id.clone(), - field_id: date_field.id.clone(), - row_id: database.rows[0].id.clone(), - }; - - // Insert data into the date cell of the first row. - let timestamp = 1686300557; - let error = test - .update_date_cell(DateCellChangesetPB { - cell_id: cell_path, - timestamp: Some(timestamp), - ..Default::default() - }) - .await; - assert!(error.is_none()); - - // Check that the cell data is updated. - let cell = test - .get_date_cell(&grid_view.id, &database.rows[0].id, &date_field.id) - .await; - assert_eq!(cell.timestamp, Some(timestamp)); -} - -#[tokio::test] -async fn update_date_cell_event_with_empty_time_str_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let database = test.get_database(&grid_view.id).await; - let row_id = database.rows[0].id.clone(); - - // Create a date field - let date_field = test.create_field(&grid_view.id, FieldType::DateTime).await; - let cell_path = CellIdPB { - view_id: grid_view.id.clone(), - field_id: date_field.id.clone(), - row_id: row_id.clone(), - }; - - // Insert empty timestamp string - let error = test - .update_date_cell(DateCellChangesetPB { - cell_id: cell_path, - timestamp: None, - ..Default::default() - }) - .await; - assert!(error.is_none()); - - // Check that the cell data is updated. - let cell = test - .get_date_cell(&grid_view.id, &row_id, &date_field.id) - .await; - assert_eq!(cell.timestamp, None); -} - -#[tokio::test] -async fn create_checklist_field_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // create checklist field - let checklist_field = test.create_field(&grid_view.id, FieldType::Checklist).await; - let database = test.get_database(&grid_view.id).await; - - // Get the checklist cell - let cell = test - .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) - .await; - assert_eq!(cell.options.len(), 0); - assert_eq!(cell.selected_options.len(), 0); - assert_eq!(cell.percentage, 0.0); -} - -#[tokio::test] -async fn update_checklist_cell_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - // create checklist field - let checklist_field = test.create_field(&grid_view.id, FieldType::Checklist).await; - let database = test.get_database(&grid_view.id).await; - - // update the checklist cell - let changeset = ChecklistCellDataChangesetPB { - cell_id: CellIdPB { - view_id: grid_view.id.clone(), - row_id: database.rows[0].id.clone(), - field_id: checklist_field.id.clone(), - }, - insert_task: vec![ - ChecklistCellInsertPB { - name: "task 1".to_string(), - index: None, - }, - ChecklistCellInsertPB { - name: "task 2".to_string(), - index: None, - }, - ChecklistCellInsertPB { - name: "task 3".to_string(), - index: None, - }, - ], - completed_tasks: vec![], - delete_tasks: vec![], - update_tasks: vec![], - reorder: "".to_string(), - }; - test.update_checklist_cell(changeset).await; - - // get the cell - let cell = test - .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) - .await; - - assert_eq!(cell.options.len(), 3); - assert_eq!(cell.selected_options.len(), 0); - - // select some options - let changeset = ChecklistCellDataChangesetPB { - cell_id: CellIdPB { - view_id: grid_view.id.clone(), - row_id: database.rows[0].id.clone(), - field_id: checklist_field.id.clone(), - }, - completed_tasks: vec![cell.options[0].id.clone(), cell.options[1].id.clone()], - ..Default::default() - }; - test.update_checklist_cell(changeset).await; - - // get the cell - let cell = test - .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) - .await; - - assert_eq!(cell.options.len(), 3); - assert_eq!(cell.selected_options.len(), 2); - assert_eq!(cell.percentage, 0.67); -} - -// Update the database layout type from grid to board -#[tokio::test] -async fn update_database_layout_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - - let error = test - .update_setting(DatabaseSettingChangesetPB { - view_id: grid_view.id.clone(), - layout_type: Some(DatabaseLayoutPB::Board), - ..Default::default() - }) - .await; - assert!(error.is_none()); - - let database = test.get_database(&grid_view.id).await; - assert_eq!(database.layout_type, DatabaseLayoutPB::Board); -} - -// Update the database layout type from grid to board. Set the checkbox field as the grouping field -#[tokio::test] -async fn update_database_layout_event_test2() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let fields = test.get_all_database_fields(&grid_view.id).await.items; - - let checkbox_field = fields - .iter() - .find(|field| field.field_type == FieldType::Checkbox) - .unwrap(); - test - .set_group_by_field(&grid_view.id, &checkbox_field.id, vec![]) - .await; - - let error = test - .update_setting(DatabaseSettingChangesetPB { - view_id: grid_view.id.clone(), - layout_type: Some(DatabaseLayoutPB::Board), - ..Default::default() - }) - .await; - assert!(error.is_none()); - - // Empty to group id - let groups = test.get_groups(&grid_view.id).await; - assert_eq!(groups.len(), 2); -} - -// Create a checkbox field in the default board and then set it as the grouping field. -#[tokio::test] -async fn set_group_by_checkbox_field_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - let checkbox_field = test.create_field(&board_view.id, FieldType::Checkbox).await; - test - .set_group_by_field(&board_view.id, &checkbox_field.id, vec![]) - .await; - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 2); -} - -#[tokio::test] -async fn get_all_calendar_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let calendar_view = test - .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) - .await; - - // By default, there should be no events - let events = test.get_all_calendar_events(&calendar_view.id).await; - assert!(events.is_empty()); -} - -#[tokio::test] -async fn create_calendar_event_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let calendar_view = test - .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) - .await; - let fields = test.get_all_database_fields(&calendar_view.id).await.items; - let date_field = fields - .iter() - .find(|field| field.field_type == FieldType::DateTime) - .unwrap(); - - // create a new row - let row = test - .create_row(&calendar_view.id, OrderObjectPositionPB::default(), None) - .await; - - // Insert data into the date cell of the first row. - let error = test - .update_date_cell(DateCellChangesetPB { - cell_id: CellIdPB { - view_id: calendar_view.id.clone(), - field_id: date_field.id.clone(), - row_id: row.id, - }, - timestamp: Some(timestamp()), - ..Default::default() - }) - .await; - assert!(error.is_none()); - - let events = test.get_all_calendar_events(&calendar_view.id).await; - assert_eq!(events.len(), 1); -} - -#[tokio::test] -async fn update_relation_cell_test() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) - .await; - let relation_field = test.create_field(&grid_view.id, FieldType::Relation).await; - let database = test.get_database(&grid_view.id).await; - - // update the relation cell - let changeset = RelationCellChangesetPB { - view_id: grid_view.id.clone(), - cell_id: CellIdPB { - view_id: grid_view.id.clone(), - field_id: relation_field.id.clone(), - row_id: database.rows[0].id.clone(), - }, - inserted_row_ids: vec![ - "row1rowid".to_string(), - "row2rowid".to_string(), - "row3rowid".to_string(), - ], - ..Default::default() - }; - test.update_relation_cell(changeset).await; - - // get the cell - let cell = test - .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) - .await; - - assert_eq!(cell.row_ids.len(), 3); - - // update the relation cell - let changeset = RelationCellChangesetPB { - view_id: grid_view.id.clone(), - cell_id: CellIdPB { - view_id: grid_view.id.clone(), - field_id: relation_field.id.clone(), - row_id: database.rows[0].id.clone(), - }, - removed_row_ids: vec![ - "row1rowid".to_string(), - "row3rowid".to_string(), - "row4rowid".to_string(), - ], - ..Default::default() - }; - test.update_relation_cell(changeset).await; - - // get the cell - let cell = test - .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) - .await; - - assert_eq!(cell.row_ids.len(), 1); -} - -#[tokio::test] -async fn get_detailed_relation_cell_data() { - let test = EventIntegrationTest::new_anon().await; - let current_workspace = test.get_current_workspace().await; - - let origin_grid_view = test - .create_grid(¤t_workspace.id, "origin".to_owned(), vec![]) - .await; - let relation_grid_view = test - .create_grid(¤t_workspace.id, "relation grid".to_owned(), vec![]) - .await; - let relation_field = test - .create_field(&relation_grid_view.id, FieldType::Relation) - .await; - - let origin_database = test.get_database(&origin_grid_view.id).await; - let origin_fields = test.get_all_database_fields(&origin_grid_view.id).await; - let linked_row = origin_database.rows[0].clone(); - - test - .update_cell(CellChangesetPB { - view_id: origin_grid_view.id.clone(), - row_id: linked_row.id.clone(), - field_id: origin_fields.items[0].id.clone(), - cell_changeset: "hello world".to_string(), - }) - .await; - - let new_database = test.get_database(&relation_grid_view.id).await; - - // update the relation cell - let changeset = RelationCellChangesetPB { - view_id: relation_grid_view.id.clone(), - cell_id: CellIdPB { - view_id: relation_grid_view.id.clone(), - field_id: relation_field.id.clone(), - row_id: new_database.rows[0].id.clone(), - }, - inserted_row_ids: vec![linked_row.id.clone()], - ..Default::default() - }; - test.update_relation_cell(changeset).await; - - // get the cell - let cell = test - .get_relation_cell( - &relation_grid_view.id, - &relation_field.id, - &new_database.rows[0].id, - ) - .await; - - // using the row ids, get the row data - let rows = test - .get_related_row_data(origin_database.id.clone(), cell.row_ids) - .await; - - assert_eq!(rows.len(), 1); - assert_eq!(rows[0].name, "hello world"); -} diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/group_test.rs index 7a54deebb9..8c97b3e7ce 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/local_test/group_test.rs @@ -94,6 +94,7 @@ async fn rename_group_event_test() { .update_group( &board_view.id, &groups[1].group_id, + &groups[1].field_id, Some("new name".to_owned()), None, ) @@ -114,7 +115,13 @@ async fn hide_group_event_test() { assert_eq!(groups.len(), 4); let error = test - .update_group(&board_view.id, &groups[0].group_id, None, Some(false)) + .update_group( + &board_view.id, + &groups[0].group_id, + &groups[0].field_id, + None, + Some(false), + ) .await; assert!(error.is_none()); @@ -138,6 +145,7 @@ async fn update_group_name_test() { .update_group( &board_view.id, &groups[1].group_id, + &groups[1].field_id, Some("To Do?".to_string()), None, ) diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/mod.rs index f1c1b64a54..8b91f85113 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/local_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/local_test/mod.rs @@ -1,3 +1,2 @@ -mod calculate_test; -mod event_test; mod group_test; +mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs new file mode 100644 index 0000000000..54427d4b47 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs @@ -0,0 +1,902 @@ +use std::convert::TryFrom; + +use bytes::Bytes; + +use event_integration_test::event_builder::EventBuilder; +use event_integration_test::EventIntegrationTest; +use flowy_database2::entities::{ + CellChangesetPB, CellIdPB, CheckboxCellDataPB, ChecklistCellDataChangesetPB, DatabaseLayoutPB, + DatabaseSettingChangesetPB, DatabaseViewIdPB, DateCellChangesetPB, FieldType, + OrderObjectPositionPB, RelationCellChangesetPB, SelectOptionCellDataPB, UpdateRowMetaChangesetPB, +}; +use lib_infra::util::timestamp; + +#[tokio::test] +async fn get_database_id_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // The view id can be used to get the database id. + let database_id = EventBuilder::new(test.clone()) + .event(flowy_database2::event_map::DatabaseEvent::GetDatabaseId) + .payload(DatabaseViewIdPB { + value: grid_view.id.clone(), + }) + .async_send() + .await + .parse::<flowy_database2::entities::DatabaseIdPB>() + .value; + + assert_ne!(database_id, grid_view.id); +} + +#[tokio::test] +async fn get_database_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.fields.len(), 3); + assert_eq!(database.rows.len(), 3); + assert_eq!(database.layout_type, DatabaseLayoutPB::Grid); +} + +#[tokio::test] +async fn get_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[0].field_type, FieldType::RichText); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + assert_eq!(fields.len(), 3); +} + +#[tokio::test] +async fn create_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + test.create_field(&grid_view.id, FieldType::Checkbox).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 4); + assert_eq!(fields[3].field_type, FieldType::Checkbox); +} + +#[tokio::test] +async fn delete_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[0].field_type, FieldType::RichText); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + + let error = test.delete_field(&grid_view.id, &fields[1].id).await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 2); +} + +// The primary field is not allowed to be deleted. +#[tokio::test] +async fn delete_primary_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be deleted. + assert!(fields[0].is_primary); + let error = test.delete_field(&grid_view.id, &fields[0].id).await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn update_field_type_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + let error = test + .update_field_type(&grid_view.id, &fields[1].id, FieldType::Checklist) + .await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields[1].field_type, FieldType::Checklist); +} + +#[tokio::test] +async fn update_primary_field_type_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be deleted. + assert!(fields[0].is_primary); + + // the primary field is not allowed to be updated. + let error = test + .update_field_type(&grid_view.id, &fields[0].id, FieldType::Checklist) + .await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn duplicate_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be updated. + let error = test.duplicate_field(&grid_view.id, &fields[1].id).await; + assert!(error.is_none()); + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + assert_eq!(fields.len(), 4); +} + +// The primary field is not allowed to be duplicated. So this test should return an error. +#[tokio::test] +async fn duplicate_primary_field_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let fields = test.get_all_database_fields(&grid_view.id).await.items; + // the primary field is not allowed to be duplicated. + let error = test.duplicate_field(&grid_view.id, &fields[0].id).await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn get_primary_field_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // By default the primary field type is RichText. + let field = test.get_primary_field(&grid_view.id).await; + assert_eq!(field.field_type, FieldType::RichText); +} + +#[tokio::test] +async fn create_row_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let _ = test + .create_row(&grid_view.id, OrderObjectPositionPB::default(), None) + .await; + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 4); +} + +#[tokio::test] +async fn delete_row_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // delete the row + let database = test.get_database(&grid_view.id).await; + let remove_row_id = database.rows[0].id.clone(); + assert_eq!(database.rows.len(), 3); + let error = test.delete_row(&grid_view.id, &remove_row_id).await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 2); + + // get the row again and check if it is deleted. + let optional_row = test.get_row(&grid_view.id, &remove_row_id).await; + assert!(optional_row.row.is_none()); +} + +#[tokio::test] +async fn get_row_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + let row = test.get_row(&grid_view.id, &database.rows[0].id).await.row; + assert!(row.is_some()); + + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert!(!row.document_id.is_empty()); +} + +#[tokio::test] +async fn update_row_meta_event_with_url_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + // By default the row icon is None. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.icon, None); + + // Insert icon url to the row. + let changeset = UpdateRowMetaChangesetPB { + id: database.rows[0].id.clone(), + view_id: grid_view.id.clone(), + icon_url: Some("icon_url".to_owned()), + cover_url: None, + is_document_empty: None, + }; + let error = test.update_row_meta(changeset).await; + assert!(error.is_none()); + + // Check if the icon is updated. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.icon, Some("icon_url".to_owned())); +} + +#[tokio::test] +async fn update_row_meta_event_with_cover_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + // By default the row icon is None. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.cover, None); + + // Insert cover to the row. + let changeset = UpdateRowMetaChangesetPB { + id: database.rows[0].id.clone(), + view_id: grid_view.id.clone(), + cover_url: Some("cover url".to_owned()), + icon_url: None, + is_document_empty: None, + }; + let error = test.update_row_meta(changeset).await; + assert!(error.is_none()); + + // Check if the icon is updated. + let row = test.get_row_meta(&grid_view.id, &database.rows[0].id).await; + assert_eq!(row.cover, Some("cover url".to_owned())); +} + +#[tokio::test] +async fn delete_row_event_with_invalid_row_id_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // delete the row with empty row_id. It should do nothing + let error = test.delete_row(&grid_view.id, "").await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 3); +} + +#[tokio::test] +async fn duplicate_row_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let error = test + .duplicate_row(&grid_view.id, &database.rows[0].id) + .await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 4); +} + +#[tokio::test] +async fn duplicate_row_event_with_invalid_row_id_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 3); + + let error = test.duplicate_row(&grid_view.id, "").await; + assert!(error.is_some()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows.len(), 3); +} + +#[tokio::test] +async fn move_row_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let row_1 = database.rows[0].id.clone(); + let row_2 = database.rows[1].id.clone(); + let row_3 = database.rows[2].id.clone(); + let error = test.move_row(&grid_view.id, &row_1, &row_3).await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows[0].id, row_2); + assert_eq!(database.rows[1].id, row_3); + assert_eq!(database.rows[2].id, row_1); +} + +#[tokio::test] +async fn move_row_event_test2() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let row_1 = database.rows[0].id.clone(); + let row_2 = database.rows[1].id.clone(); + let row_3 = database.rows[2].id.clone(); + let error = test.move_row(&grid_view.id, &row_2, &row_1).await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows[0].id, row_2); + assert_eq!(database.rows[1].id, row_1); + assert_eq!(database.rows[2].id, row_3); +} + +#[tokio::test] +async fn move_row_event_with_invalid_row_id_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let row_1 = database.rows[0].id.clone(); + let row_2 = database.rows[1].id.clone(); + let row_3 = database.rows[2].id.clone(); + + for i in 0..2 { + if i == 0 { + let error = test.move_row(&grid_view.id, &row_1, "").await; + assert!(error.is_some()); + } else { + let error = test.move_row(&grid_view.id, "", &row_1).await; + assert!(error.is_some()); + } + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.rows[0].id, row_1); + assert_eq!(database.rows[1].id, row_2); + assert_eq!(database.rows[2].id, row_3); + } +} + +#[tokio::test] +async fn update_text_cell_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + + let row_id = database.rows[0].id.clone(); + let field_id = fields[0].id.clone(); + assert_eq!(fields[0].field_type, FieldType::RichText); + + // Update the first cell of the first row. + let error = test + .update_cell(CellChangesetPB { + view_id: grid_view.id.clone(), + row_id: row_id.clone(), + field_id: field_id.clone(), + cell_changeset: "hello world".to_string(), + }) + .await; + assert!(error.is_none()); + + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let s = String::from_utf8(cell.data).unwrap(); + assert_eq!(s, "hello world"); +} + +#[tokio::test] +async fn update_checkbox_cell_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + + let row_id = database.rows[0].id.clone(); + let field_id = fields[2].id.clone(); + assert_eq!(fields[2].field_type, FieldType::Checkbox); + + for input in &["yes", "true", "1"] { + let error = test + .update_cell(CellChangesetPB { + view_id: grid_view.id.clone(), + row_id: row_id.clone(), + field_id: field_id.clone(), + cell_changeset: input.to_string(), + }) + .await; + assert!(error.is_none()); + + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let output = CheckboxCellDataPB::try_from(Bytes::from(cell.data)).unwrap(); + assert!(output.is_checked); + } +} + +#[tokio::test] +async fn update_single_select_cell_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + let row_id = database.rows[0].id.clone(); + let field_id = fields[1].id.clone(); + assert_eq!(fields[1].field_type, FieldType::SingleSelect); + + // Insert a new option. This should update the cell with the new option. + let error = test + .insert_option(&grid_view.id, &field_id, &row_id, "task 1") + .await; + assert!(error.is_none()); + + // Check that the cell data is updated. + let cell = test.get_cell(&grid_view.id, &row_id, &field_id).await; + let select_option_cell = SelectOptionCellDataPB::try_from(Bytes::from(cell.data)).unwrap(); + + assert_eq!(select_option_cell.select_options.len(), 1); +} + +#[tokio::test] +async fn update_date_cell_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + + // Create a date field + let date_field = test.create_field(&grid_view.id, FieldType::DateTime).await; + + let cell_path = CellIdPB { + view_id: grid_view.id.clone(), + field_id: date_field.id.clone(), + row_id: database.rows[0].id.clone(), + }; + + // Insert data into the date cell of the first row. + let timestamp = 1686300557; + let error = test + .update_date_cell(DateCellChangesetPB { + cell_id: cell_path, + date: Some(timestamp), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + // Check that the cell data is updated. + let cell = test + .get_date_cell(&grid_view.id, &database.rows[0].id, &date_field.id) + .await; + assert_eq!(cell.date, "Jun 09, 2023"); + assert_eq!(cell.timestamp, timestamp); +} + +#[tokio::test] +async fn update_date_cell_event_with_empty_time_str_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let database = test.get_database(&grid_view.id).await; + let row_id = database.rows[0].id.clone(); + + // Create a date field + let date_field = test.create_field(&grid_view.id, FieldType::DateTime).await; + let cell_path = CellIdPB { + view_id: grid_view.id.clone(), + field_id: date_field.id.clone(), + row_id: row_id.clone(), + }; + + // Insert empty timestamp string + let error = test + .update_date_cell(DateCellChangesetPB { + cell_id: cell_path, + date: None, + ..Default::default() + }) + .await; + assert!(error.is_none()); + + // Check that the cell data is updated. + let cell = test + .get_date_cell(&grid_view.id, &row_id, &date_field.id) + .await; + assert_eq!(cell.date, ""); + assert_eq!(cell.timestamp, 0); +} + +#[tokio::test] +async fn create_checklist_field_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // create checklist field + let checklist_field = test.create_field(&grid_view.id, FieldType::Checklist).await; + let database = test.get_database(&grid_view.id).await; + + // Get the checklist cell + let cell = test + .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) + .await; + assert_eq!(cell.options.len(), 0); + assert_eq!(cell.selected_options.len(), 0); + assert_eq!(cell.percentage, 0.0); +} + +#[tokio::test] +async fn update_checklist_cell_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + // create checklist field + let checklist_field = test.create_field(&grid_view.id, FieldType::Checklist).await; + let database = test.get_database(&grid_view.id).await; + + // update the checklist cell + let changeset = ChecklistCellDataChangesetPB { + view_id: grid_view.id.clone(), + row_id: database.rows[0].id.clone(), + field_id: checklist_field.id.clone(), + insert_options: vec![ + "task 1".to_string(), + "task 2".to_string(), + "task 3".to_string(), + ], + selected_option_ids: vec![], + delete_option_ids: vec![], + update_options: vec![], + }; + test.update_checklist_cell(changeset).await; + + // get the cell + let cell = test + .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.options.len(), 3); + assert_eq!(cell.selected_options.len(), 0); + + // select some options + let changeset = ChecklistCellDataChangesetPB { + view_id: grid_view.id.clone(), + row_id: database.rows[0].id.clone(), + field_id: checklist_field.id.clone(), + selected_option_ids: vec![cell.options[0].id.clone(), cell.options[1].id.clone()], + ..Default::default() + }; + test.update_checklist_cell(changeset).await; + + // get the cell + let cell = test + .get_checklist_cell(&grid_view.id, &checklist_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.options.len(), 3); + assert_eq!(cell.selected_options.len(), 2); + assert_eq!(cell.percentage, 0.67); +} + +// Update the database layout type from grid to board +#[tokio::test] +async fn update_database_layout_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + + let error = test + .update_setting(DatabaseSettingChangesetPB { + view_id: grid_view.id.clone(), + layout_type: Some(DatabaseLayoutPB::Board), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + let database = test.get_database(&grid_view.id).await; + assert_eq!(database.layout_type, DatabaseLayoutPB::Board); +} + +// Update the database layout type from grid to board. Set the checkbox field as the grouping field +#[tokio::test] +async fn update_database_layout_event_test2() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let fields = test.get_all_database_fields(&grid_view.id).await.items; + + let checkbox_field = fields + .iter() + .find(|field| field.field_type == FieldType::Checkbox) + .unwrap(); + test + .set_group_by_field(&grid_view.id, &checkbox_field.id, vec![]) + .await; + + let error = test + .update_setting(DatabaseSettingChangesetPB { + view_id: grid_view.id.clone(), + layout_type: Some(DatabaseLayoutPB::Board), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + // Empty to group id + let groups = test.get_groups(&grid_view.id).await; + assert_eq!(groups.len(), 2); +} + +// Create a checkbox field in the default board and then set it as the grouping field. +#[tokio::test] +async fn set_group_by_checkbox_field_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let checkbox_field = test.create_field(&board_view.id, FieldType::Checkbox).await; + test + .set_group_by_field(&board_view.id, &checkbox_field.id, vec![]) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 2); +} + +#[tokio::test] +async fn get_all_calendar_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let calendar_view = test + .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) + .await; + + // By default, there should be no events + let events = test.get_all_calendar_events(&calendar_view.id).await; + assert!(events.is_empty()); +} + +#[tokio::test] +async fn create_calendar_event_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let calendar_view = test + .create_calendar(¤t_workspace.id, "my calendar view".to_owned(), vec![]) + .await; + let fields = test.get_all_database_fields(&calendar_view.id).await.items; + let date_field = fields + .iter() + .find(|field| field.field_type == FieldType::DateTime) + .unwrap(); + + // create a new row + let row = test + .create_row(&calendar_view.id, OrderObjectPositionPB::default(), None) + .await; + + // Insert data into the date cell of the first row. + let error = test + .update_date_cell(DateCellChangesetPB { + cell_id: CellIdPB { + view_id: calendar_view.id.clone(), + field_id: date_field.id.clone(), + row_id: row.id, + }, + date: Some(timestamp()), + ..Default::default() + }) + .await; + assert!(error.is_none()); + + let events = test.get_all_calendar_events(&calendar_view.id).await; + assert_eq!(events.len(), 1); +} + +#[tokio::test] +async fn update_relation_cell_test() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my grid view".to_owned(), vec![]) + .await; + let relation_field = test.create_field(&grid_view.id, FieldType::Relation).await; + let database = test.get_database(&grid_view.id).await; + + // update the relation cell + let changeset = RelationCellChangesetPB { + view_id: grid_view.id.clone(), + cell_id: CellIdPB { + view_id: grid_view.id.clone(), + field_id: relation_field.id.clone(), + row_id: database.rows[0].id.clone(), + }, + inserted_row_ids: vec![ + "row1rowid".to_string(), + "row2rowid".to_string(), + "row3rowid".to_string(), + ], + ..Default::default() + }; + test.update_relation_cell(changeset).await; + + // get the cell + let cell = test + .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.row_ids.len(), 3); + + // update the relation cell + let changeset = RelationCellChangesetPB { + view_id: grid_view.id.clone(), + cell_id: CellIdPB { + view_id: grid_view.id.clone(), + field_id: relation_field.id.clone(), + row_id: database.rows[0].id.clone(), + }, + removed_row_ids: vec![ + "row1rowid".to_string(), + "row3rowid".to_string(), + "row4rowid".to_string(), + ], + ..Default::default() + }; + test.update_relation_cell(changeset).await; + + // get the cell + let cell = test + .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.row_ids.len(), 1); +} + +#[tokio::test] +async fn get_detailed_relation_cell_data() { + let test = EventIntegrationTest::new_anon().await; + let current_workspace = test.get_current_workspace().await; + + let origin_grid_view = test + .create_grid(¤t_workspace.id, "origin".to_owned(), vec![]) + .await; + let relation_grid_view = test + .create_grid(¤t_workspace.id, "relation grid".to_owned(), vec![]) + .await; + let relation_field = test + .create_field(&relation_grid_view.id, FieldType::Relation) + .await; + + let origin_database = test.get_database(&origin_grid_view.id).await; + let origin_fields = test.get_all_database_fields(&origin_grid_view.id).await; + let linked_row = origin_database.rows[0].clone(); + + test + .update_cell(CellChangesetPB { + view_id: origin_grid_view.id.clone(), + row_id: linked_row.id.clone(), + field_id: origin_fields.items[0].id.clone(), + cell_changeset: "hello world".to_string(), + }) + .await; + + let new_database = test.get_database(&relation_grid_view.id).await; + + // update the relation cell + let changeset = RelationCellChangesetPB { + view_id: relation_grid_view.id.clone(), + cell_id: CellIdPB { + view_id: relation_grid_view.id.clone(), + field_id: relation_field.id.clone(), + row_id: new_database.rows[0].id.clone(), + }, + inserted_row_ids: vec![linked_row.id.clone()], + ..Default::default() + }; + test.update_relation_cell(changeset).await; + + // get the cell + let cell = test + .get_relation_cell( + &relation_grid_view.id, + &relation_field.id, + &new_database.rows[0].id, + ) + .await; + + // using the row ids, get the row data + let rows = test + .get_related_row_data(origin_database.id.clone(), cell.row_ids) + .await; + + assert_eq!(rows.len(), 1); + assert_eq!(rows[0].name, "hello world"); +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/mod.rs index 0468498a58..ee1335d7ff 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/mod.rs @@ -1,2 +1,5 @@ mod af_cloud; mod local_test; + +// #[cfg(feature = "supabase_cloud_test")] +// mod supabase_test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs new file mode 100644 index 0000000000..8c668afeac --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/helper.rs @@ -0,0 +1,106 @@ +use std::ops::Deref; + +use assert_json_diff::assert_json_eq; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::{Collab, JsonValue, Update}; +use collab_entity::CollabType; + +use event_integration_test::event_builder::EventBuilder; +use flowy_database2::entities::{DatabasePB, DatabaseViewIdPB, RepeatedDatabaseSnapshotPB}; +use flowy_database2::event_map::DatabaseEvent::*; +use flowy_folder::entities::ViewPB; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseDatabaseTest { + pub uuid: String, + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDatabaseTest { + #[allow(dead_code)] + pub async fn new_with_user(uuid: String) -> Option<Self> { + let inner = FlowySupabaseTest::new().await?; + inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + Some(Self { uuid, inner }) + } + + pub async fn new_with_new_user() -> Option<Self> { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + Some(Self { uuid, inner }) + } + + pub async fn create_database(&self) -> (ViewPB, DatabasePB) { + let current_workspace = self.inner.get_current_workspace().await; + let view = self + .inner + .create_grid(¤t_workspace.id, "my database".to_string(), vec![]) + .await; + let database = self.inner.get_database(&view.id).await; + (view, database) + } + + pub async fn get_collab_json(&self, database_id: &str) -> JsonValue { + let database_editor = self + .database_manager + .get_database(database_id) + .await + .unwrap(); + // let address = Arc::into_raw(database_editor.clone()); + let database = database_editor.get_mutex_database().lock(); + database.get_mutex_collab().to_json_value() + } + + pub async fn get_database_snapshots(&self, view_id: &str) -> RepeatedDatabaseSnapshotPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDatabaseSnapshots) + .payload(DatabaseViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::<RepeatedDatabaseSnapshotPB>() + } + + pub async fn get_database_collab_update(&self, database_id: &str) -> Vec<u8> { + let workspace_id = self.user_manager.workspace_id().unwrap(); + let cloud_service = self.database_manager.get_cloud_service().clone(); + cloud_service + .get_database_object_doc_state(database_id, CollabType::Database, &workspace_id) + .await + .unwrap() + .unwrap() + } +} + +pub fn assert_database_collab_content( + database_id: &str, + collab_update: &[u8], + expected: JsonValue, +) { + let collab = MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Server, + database_id, + vec![], + false, + )); + collab.lock().with_origin_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json, expected); +} + +impl Deref for FlowySupabaseDatabaseTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs new file mode 100644 index 0000000000..537cdf80d8 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/database/supabase_test/test.rs @@ -0,0 +1,108 @@ +use std::time::Duration; + +use flowy_database2::entities::{ + DatabaseSnapshotStatePB, DatabaseSyncState, DatabaseSyncStatePB, FieldChangesetPB, FieldType, +}; +use flowy_database2::notification::DatabaseNotification::DidUpdateDatabaseSnapshotState; + +use crate::database::supabase_test::helper::{ + assert_database_collab_content, FlowySupabaseDatabaseTest, +}; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn supabase_initial_database_snapshot_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let rx = test + .notification_sender + .subscribe::<DatabaseSnapshotStatePB>(&database.id, DidUpdateDatabaseSnapshotState); + + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let expected = test.get_collab_json(&database.id).await; + let snapshots = test.get_database_snapshots(&view.id).await; + assert_eq!(snapshots.items.len(), 1); + assert_database_collab_content(&database.id, &snapshots.items[0].data, expected); + } +} + +#[tokio::test] +async fn supabase_edit_database_test() { + if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { + let (view, database) = test.create_database().await; + let existing_fields = test.get_all_database_fields(&view.id).await; + for field in existing_fields.items { + if !field.is_primary { + test.delete_field(&view.id, &field.id).await; + } + } + + let field = test.create_field(&view.id, FieldType::Checklist).await; + test + .update_field(FieldChangesetPB { + field_id: field.id.clone(), + view_id: view.id.clone(), + name: Some("hello world".to_string()), + ..Default::default() + }) + .await; + + // wait all updates are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| { + pb.value == DatabaseSyncState::SyncFinished + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + assert_eq!(test.get_all_database_fields(&view.id).await.items.len(), 2); + let expected = test.get_collab_json(&database.id).await; + let update = test.get_database_collab_update(&database.id).await; + assert_database_collab_content(&database.id, &update, expected); + } +} + +// #[tokio::test] +// async fn cloud_test_supabase_login_sync_database_test() { +// if let Some(test) = FlowySupabaseDatabaseTest::new_with_new_user().await { +// let uuid = test.uuid.clone(); +// let (view, database) = test.create_database().await; +// // wait all updates are send to the remote +// let mut rx = test +// .notification_sender +// .subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// let expected = test.get_collab_json(&database.id).await; +// test.sign_out().await; +// // Drop the test will cause the test resources to be dropped, which will +// // delete the user data folder. +// drop(test); +// +// let new_test = FlowySupabaseDatabaseTest::new_with_user(uuid) +// .await +// .unwrap(); +// // let actual = new_test.get_collab_json(&database.id).await; +// // assert_json_eq!(actual, json!("")); +// +// new_test.open_database(&view.id).await; +// +// // wait all updates are synced from the remote +// let mut rx = new_test +// .notification_sender +// .subscribe_with_condition::<DatabaseSyncStatePB, _>(&database.id, |pb| pb.is_finish); +// receive_with_timeout(&mut rx, Duration::from_secs(30)) +// .await +// .unwrap(); +// +// // when the new sync is finished, the database should be the same as the old one +// let actual = new_test.get_collab_json(&database.id).await; +// assert_json_eq!(actual, expected); +// } +// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/edit_test.rs index fb0417bdd0..50a8fd3fd8 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/edit_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/edit_test.rs @@ -1,17 +1,18 @@ -use crate::util::{receive_with_timeout, unzip}; use collab_document::blocks::DocumentData; -use collab_folder::SpaceInfo; -use event_integration_test::document_event::assert_document_data_equal; -use event_integration_test::user_event::use_localhost_af_cloud; -use event_integration_test::EventIntegrationTest; -use flowy_core::DEFAULT_NAME; -use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; use serde_json::json; use std::time::Duration; +use event_integration_test::document_event::assert_document_data_equal; +use event_integration_test::user_event::user_localhost_af_cloud; +use event_integration_test::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; + +use crate::util::{receive_with_timeout, unzip}; + #[tokio::test] async fn af_cloud_edit_document_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; test.af_cloud_sign_up().await; test.wait_ws_connected().await; @@ -30,7 +31,7 @@ async fn af_cloud_edit_document_test() { let rx = test .notification_sender .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| { - pb.value == DocumentSyncState::SyncFinished + pb.value != DocumentSyncState::Syncing }); let _ = receive_with_timeout(rx, Duration::from_secs(30)).await; @@ -42,8 +43,8 @@ async fn af_cloud_edit_document_test() { #[tokio::test] async fn af_cloud_sync_anon_user_document_test() { - let user_db_path = unzip("./tests/asset", "040_sync_local_document").unwrap(); - use_localhost_af_cloud().await; + let (cleaner, user_db_path) = unzip("./tests/asset", "040_sync_local_document").unwrap(); + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) .await; @@ -54,13 +55,8 @@ async fn af_cloud_sync_anon_user_document_test() { // workspace: // view: SyncDocument let views = test.get_all_workspace_views().await; - assert_eq!(views.len(), 3); - for view in views.iter() { - let space_info = serde_json::from_str::<SpaceInfo>(view.extra.as_ref().unwrap()).unwrap(); - assert!(space_info.is_space); - } - - let document_id = views[2].id.clone(); + assert_eq!(views.len(), 2); + let document_id = views[1].id.clone(); test.open_document(document_id.clone()).await; // wait all update are send to the remote @@ -77,6 +73,8 @@ async fn af_cloud_sync_anon_user_document_test() { &document_id, expected_040_sync_local_document_data(), ); + + drop(cleaner); } fn expected_040_sync_local_document_data() -> DocumentData { diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs deleted file mode 100644 index 7d8ecc9680..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/file_upload_test.rs +++ /dev/null @@ -1,207 +0,0 @@ -use crate::document::generate_random_bytes; -use event_integration_test::user_event::use_localhost_af_cloud; -use event_integration_test::{EventIntegrationTest, SINGLE_FILE_UPLOAD_SIZE}; -use flowy_storage_pub::storage::FileUploadState; -use lib_infra::util::md5; -use std::env::temp_dir; -use std::sync::Arc; -use std::time::Duration; -use tokio::fs; -use tokio::fs::File; -use tokio::io::AsyncWriteExt; -use tokio::sync::Mutex; -use tokio::time::timeout; - -#[tokio::test] -async fn af_cloud_upload_big_file_test() { - use_localhost_af_cloud().await; - let mut test = EventIntegrationTest::new().await; - test.af_cloud_sign_up().await; - tokio::time::sleep(Duration::from_secs(6)).await; - let parent_dir = "temp_test"; - let workspace_id = test.get_current_workspace().await.id; - let (file_path, upload_data) = generate_file_with_bytes_len(15 * 1024 * 1024).await; - let (created_upload, rx) = test - .storage_manager - .storage_service - .create_upload(&workspace_id, parent_dir, &file_path) - .await - .unwrap(); - - let mut rx = rx.unwrap(); - while let Ok(state) = rx.recv().await { - if let FileUploadState::Uploading { progress } = state { - if progress > 0.1 { - break; - } - } - } - - // Simulate a restart - let config = test.config.clone(); - test.skip_clean(); - drop(test); - tokio::time::sleep(Duration::from_secs(3)).await; - - // Restart the test. It will load unfinished uploads - let test = EventIntegrationTest::new_with_config(config).await; - if let Some(mut rx) = test - .storage_manager - .subscribe_file_state(parent_dir, &created_upload.file_id) - .await - .unwrap() - { - let timeout_duration = Duration::from_secs(180); - while let Ok(state) = match timeout(timeout_duration, rx.recv()).await { - Ok(result) => result, - Err(_) => { - panic!("Timed out waiting for file upload completion"); - }, - } { - if let FileUploadState::Finished { .. } = state { - break; - } - } - } - - // download the file and then compare the data. - let file_service = test - .appflowy_core - .server_provider - .get_server() - .unwrap() - .file_storage() - .unwrap(); - let file = file_service.get_object(created_upload.url).await.unwrap(); - assert_eq!(md5(file.raw), md5(upload_data)); - let _ = fs::remove_file(file_path).await; -} - -#[tokio::test] -async fn af_cloud_upload_6_files_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.af_cloud_sign_up().await; - let workspace_id = test.get_current_workspace().await.id; - - let mut created_uploads = vec![]; - let mut receivers = vec![]; - for file_size in [1, 2, 5, 8, 10, 12] { - let file_path = generate_file_with_bytes_len(file_size * 1024 * 1024) - .await - .0; - let (created_upload, rx) = test - .storage_manager - .storage_service - .create_upload(&workspace_id, "temp_test", &file_path) - .await - .unwrap(); - receivers.push(rx.unwrap()); - created_uploads.push(created_upload); - let _ = fs::remove_file(file_path).await; - } - - // Wait for all uploads to finish - let uploads = Arc::new(Mutex::new(created_uploads)); - let mut handles = vec![]; - - for mut receiver in receivers { - let cloned_uploads = uploads.clone(); - let state = test.storage_manager.get_file_state(&receiver.file_id).await; - let handle = tokio::spawn(async move { - if let Some(FileUploadState::Finished { file_id }) = state { - cloned_uploads - .lock() - .await - .retain(|upload| upload.file_id != file_id); - } - while let Ok(value) = receiver.recv().await { - if let FileUploadState::Finished { file_id } = value { - cloned_uploads - .lock() - .await - .retain(|upload| upload.file_id != file_id); - break; - } - } - }); - handles.push(handle); - } - - // join all handles - futures::future::join_all(handles).await; - assert_eq!(uploads.lock().await.len(), 0); -} - -#[tokio::test] -async fn af_cloud_delete_upload_file_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.af_cloud_sign_up().await; - let workspace_id = test.get_current_workspace().await.id; - - // Pause sync - test.storage_manager.update_network_reachable(false); - let mut created_uploads = vec![]; - let mut receivers = vec![]; - - // file that exceeds limit will be deleted automatically - let exceed_file_limit_size = SINGLE_FILE_UPLOAD_SIZE + 1; - for file_size in [8 * 1024 * 1024, exceed_file_limit_size, 10 * 1024 * 1024] { - let file_path = generate_file_with_bytes_len(file_size).await.0; - let (created_upload, rx) = test - .storage_manager - .storage_service - .create_upload(&workspace_id, "temp_test", &file_path) - .await - .unwrap(); - receivers.push(rx.unwrap()); - created_uploads.push(created_upload); - let _ = fs::remove_file(file_path).await; - } - - test - .storage_manager - .storage_service - .delete_object(created_uploads[0].clone().url) - .await - .unwrap(); - - let mut handles = vec![]; - // start sync - test.storage_manager.update_network_reachable(true); - - for mut receiver in receivers { - let handle = tokio::spawn(async move { - while let Ok(Ok(value)) = timeout(Duration::from_secs(60), receiver.recv()).await { - if let FileUploadState::Finished { file_id } = value { - println!("file_id: {} was uploaded", file_id); - break; - } - } - }); - handles.push(handle); - } - - // join all handles - futures::future::join_all(handles).await; - let tasks = test.storage_manager.get_all_tasks().await.unwrap(); - assert!(tasks.is_empty()); - - let state = test - .storage_manager - .get_file_state(&created_uploads[2].file_id) - .await - .unwrap(); - assert!(matches!(state, FileUploadState::Finished { .. })); -} - -async fn generate_file_with_bytes_len(len: usize) -> (String, Vec<u8>) { - let data = generate_random_bytes(len); - let file_dir = temp_dir().join(uuid::Uuid::new_v4().to_string()); - let file_path = file_dir.to_str().unwrap().to_string(); - let mut file = File::create(file_dir).await.unwrap(); - file.write_all(&data).await.unwrap(); - - (file_path, data) -} diff --git a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/mod.rs index c63deb8798..0e50d38f75 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/af_cloud_test/mod.rs @@ -1,2 +1 @@ mod edit_test; -mod file_upload_test; diff --git a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs index d9273dbe8b..cfcefeb506 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/local_test/edit_test.rs @@ -8,8 +8,6 @@ use flowy_document::parser::parser_entities::{ }; use serde_json::{json, Value}; use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; #[tokio::test] async fn get_document_event_test() { @@ -21,16 +19,6 @@ async fn get_document_event_test() { assert!(document_data.blocks.len() > 1); } -#[tokio::test] -async fn get_encoded_collab_event_test() { - let test = DocumentEventTest::new().await; - let view = test.create_document().await; - let doc_id = view.id.clone(); - let encoded_v1 = test.get_encoded_collab(&doc_id).await; - assert!(!encoded_v1.doc_state.is_empty()); - assert!(!encoded_v1.state_vector.is_empty()); -} - #[tokio::test] async fn apply_document_event_test() { let test = DocumentEventTest::new().await; @@ -103,8 +91,8 @@ async fn document_size_test() { let s = generate_random_string(string_size); test.insert_index(&view.id, &s, 1, None).await; } - let view_id = Uuid::from_str(&view.id).unwrap(); - let encoded_v1 = test.get_encoded_v1(&view_id).await; + + let encoded_v1 = test.get_encoded_v1(&view.id).await; if encoded_v1.doc_state.len() > max_size { panic!( "The document size is too large. {}", diff --git a/frontend/rust-lib/event-integration-test/tests/document/mod.rs b/frontend/rust-lib/event-integration-test/tests/document/mod.rs index e11cb782f4..ba2833ee49 100644 --- a/frontend/rust-lib/event-integration-test/tests/document/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/document/mod.rs @@ -4,7 +4,7 @@ mod af_cloud_test; // #[cfg(feature = "supabase_cloud_test")] // mod supabase_test; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use rand::{distributions::Alphanumeric, Rng}; pub fn generate_random_string(len: usize) -> String { let rng = rand::thread_rng(); @@ -14,12 +14,3 @@ pub fn generate_random_string(len: usize) -> String { .map(char::from) .collect() } - -pub fn generate_random_bytes(size: usize) -> Vec<u8> { - let s: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(size) - .map(char::from) - .collect(); - s.into_bytes() -} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs new file mode 100644 index 0000000000..d05e1ef95c --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/edit_test.rs @@ -0,0 +1,65 @@ +use std::time::Duration; + +use event_integration_test::document_event::assert_document_data_equal; +use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; + +use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn supabase_document_edit_sync_test() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + + let cloned_test = test.clone(); + let cloned_document_id = document_id.clone(); + test.appflowy_core.dispatcher().spawn(async move { + cloned_test + .insert_document_text(&cloned_document_id, "hello world", 0) + .await; + }); + + // wait all update are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_document_doc_state(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} + +#[tokio::test] +async fn supabase_document_edit_sync_test2() { + if let Some(test) = FlowySupabaseDocumentTest::new().await { + let view = test.create_document().await; + let document_id = view.id.clone(); + + for i in 0..10 { + test + .insert_document_text(&document_id, "hello world", i) + .await; + } + + // wait all update are send to the remote + let rx = test + .notification_sender + .subscribe_with_condition::<DocumentSyncStatePB, _>(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap(); + + let document_data = test.get_document_data(&document_id).await; + let update = test.get_document_doc_state(&document_id).await; + assert_document_data_equal(&update, &document_id, document_data); + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs new file mode 100644 index 0000000000..e73273cde6 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/file_test.rs @@ -0,0 +1,118 @@ +// use std::fs::File; +// use std::io::{Cursor, Read}; +// use std::path::Path; +// +// use uuid::Uuid; +// use zip::ZipArchive; +// +// use flowy_storage::StorageObject; +// +// use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; +// +// #[tokio::test] +// async fn supabase_document_upload_text_file_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// let object = StorageObject::from_bytes( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "hello world".as_bytes(), +// "text/plain".to_string(), +// ); +// +// let url = storage_service.create_object(object).await.unwrap(); +// +// let bytes = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// let s = String::from_utf8(bytes.to_vec()).unwrap(); +// assert_eq!(s, "hello world"); +// +// // Delete the text file +// let _ = storage_service.delete_object(url).await; +// } +// } +// +// #[tokio::test] +// async fn supabase_document_upload_zip_file_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// // Upload zip file +// let object = StorageObject::from_file( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "./tests/asset/test.txt.zip", +// ); +// let url = storage_service.create_object(object).await.unwrap(); +// +// // Read zip file +// let zip_data = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// let reader = Cursor::new(zip_data); +// let mut archive = ZipArchive::new(reader).unwrap(); +// for i in 0..archive.len() { +// let mut file = archive.by_index(i).unwrap(); +// let name = file.name().to_string(); +// let mut out = Vec::new(); +// file.read_to_end(&mut out).unwrap(); +// +// if name.starts_with("__MACOSX/") { +// continue; +// } +// assert_eq!(name, "test.txt"); +// assert_eq!(String::from_utf8(out).unwrap(), "hello world"); +// } +// +// // Delete the zip file +// let _ = storage_service.delete_object(url).await; +// } +// } +// #[tokio::test] +// async fn supabase_document_upload_image_test() { +// if let Some(test) = FlowySupabaseDocumentTest::new().await { +// let workspace_id = test.get_current_workspace().await.id; +// let storage_service = test +// .document_manager +// .get_file_storage_service() +// .upgrade() +// .unwrap(); +// +// // Upload zip file +// let object = StorageObject::from_file( +// &workspace_id, +// &Uuid::new_v4().to_string(), +// "./tests/asset/logo.png", +// ); +// let url = storage_service.create_object(object).await.unwrap(); +// +// let image_data = storage_service +// .get_object(url.clone()) +// .await +// .unwrap(); +// +// // Read the image file +// let mut file = File::open(Path::new("./tests/asset/logo.png")).unwrap(); +// let mut local_data = Vec::new(); +// file.read_to_end(&mut local_data).unwrap(); +// +// assert_eq!(image_data, local_data); +// +// // Delete the image +// let _ = storage_service.delete_object(url).await; +// } +// } diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs new file mode 100644 index 0000000000..07ff2d96fe --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/helper.rs @@ -0,0 +1,49 @@ +use std::ops::Deref; + +use event_integration_test::event_builder::EventBuilder; +use flowy_document::entities::{OpenDocumentPayloadPB, RepeatedDocumentSnapshotMetaPB}; +use flowy_document::event_map::DocumentEvent::GetDocumentSnapshotMeta; +use flowy_folder::entities::ViewPB; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseDocumentTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseDocumentTest { + pub async fn new() -> Option<Self> { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; + Some(Self { inner }) + } + + pub async fn create_document(&self) -> ViewPB { + let current_workspace = self.inner.get_current_workspace().await; + self + .inner + .create_and_open_document(¤t_workspace.id, "my document".to_string(), vec![]) + .await + } + + #[allow(dead_code)] + pub async fn get_document_snapshots(&self, view_id: &str) -> RepeatedDocumentSnapshotMetaPB { + EventBuilder::new(self.inner.deref().clone()) + .event(GetDocumentSnapshotMeta) + .payload(OpenDocumentPayloadPB { + document_id: view_id.to_string(), + }) + .async_send() + .await + .parse::<RepeatedDocumentSnapshotMetaPB>() + } +} + +impl Deref for FlowySupabaseDocumentTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs new file mode 100644 index 0000000000..165f5fdfc0 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/document/supabase_test/mod.rs @@ -0,0 +1,3 @@ +mod edit_test; +mod file_test; +mod helper; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs index 5857190b8b..d0a4a28429 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs @@ -1,8 +1,8 @@ use collab_folder::ViewLayout; + use event_integration_test::EventIntegrationTest; use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB}; use flowy_folder::entities::ViewLayoutPB; -use uuid::Uuid; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; @@ -338,11 +338,11 @@ async fn move_view_event_test() { async fn create_orphan_child_view_and_get_its_ancestors_test() { let test = EventIntegrationTest::new_anon().await; let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); + let view_id = "20240521"; test - .create_orphan_view(name, &view_id, ViewLayoutPB::Grid) + .create_orphan_view(name, view_id, ViewLayoutPB::Grid) .await; - let ancestors = test.get_view_ancestors(&view_id).await; + let ancestors = test.get_view_ancestors(view_id).await; assert_eq!(ancestors.len(), 1); assert_eq!(ancestors[0].name, "Orphan View"); assert_eq!(ancestors[0].id, view_id); diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/import_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/import_test.rs index 28a7fdffa5..a93a232921 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/import_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/import_test.rs @@ -1,13 +1,13 @@ use crate::util::unzip; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_folder::entities::{ImportItemPayloadPB, ImportPayloadPB, ImportTypePB, ViewLayoutPB}; +use flowy_folder::entities::{ImportPB, ImportTypePB, ViewLayoutPB}; #[tokio::test] async fn import_492_row_csv_file_test() { // csv_500r_15c.csv is a file with 492 rows and 17 columns let file_name = "csv_492r_17c.csv".to_string(); - let csv_file_path = unzip("./tests/asset", &file_name).unwrap(); + let (cleaner, csv_file_path) = unzip("./tests/asset", &file_name).unwrap(); let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; @@ -16,17 +16,17 @@ async fn import_492_row_csv_file_test() { let workspace_id = test.get_current_workspace().await.id; let import_data = gen_import_data(file_name, csv_string, workspace_id); - let views = test.import_data(import_data).await; - let view_id = views[0].clone().id; - let database = test.get_database(&view_id).await; + let view = test.import_data(import_data).await; + let database = test.get_database(&view.id).await; assert_eq!(database.rows.len(), 492); + drop(cleaner); } #[tokio::test] async fn import_10240_row_csv_file_test() { // csv_22577r_15c.csv is a file with 10240 rows and 15 columns let file_name = "csv_10240r_15c.csv".to_string(); - let csv_file_path = unzip("./tests/asset", &file_name).unwrap(); + let (cleaner, csv_file_path) = unzip("./tests/asset", &file_name).unwrap(); let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; @@ -35,21 +35,21 @@ async fn import_10240_row_csv_file_test() { let workspace_id = test.get_current_workspace().await.id; let import_data = gen_import_data(file_name, csv_string, workspace_id); - let views = test.import_data(import_data).await; - let view_id = views[0].clone().id; - let database = test.get_database(&view_id).await; + let view = test.import_data(import_data).await; + let database = test.get_database(&view.id).await; assert_eq!(database.rows.len(), 10240); + + drop(cleaner); } -fn gen_import_data(file_name: String, csv_string: String, workspace_id: String) -> ImportPayloadPB { - ImportPayloadPB { +fn gen_import_data(file_name: String, csv_string: String, workspace_id: String) -> ImportPB { + let import_data = ImportPB { parent_view_id: workspace_id.clone(), - items: vec![ImportItemPayloadPB { - name: file_name, - data: Some(csv_string.as_bytes().to_vec()), - file_path: None, - view_layout: ViewLayoutPB::Grid, - import_type: ImportTypePB::CSV, - }], - } + name: file_name, + data: Some(csv_string.as_bytes().to_vec()), + file_path: None, + view_layout: ViewLayoutPB::Grid, + import_type: ImportTypePB::CSV, + }; + import_data } diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs index 66a0a62937..aa58a02baf 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/mod.rs @@ -3,6 +3,3 @@ mod import_test; mod script; mod subscription_test; mod test; - -mod publish_database_test; -mod publish_document_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs deleted file mode 100644 index 2754c027d7..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_database_test.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::collections::HashMap; - -use collab_folder::ViewLayout; -use event_integration_test::EventIntegrationTest; -use flowy_folder::entities::{ - ImportItemPayloadPB, ImportPayloadPB, ImportTypePB, ViewLayoutPB, ViewPB, -}; -use flowy_folder::view_operation::GatherEncodedCollab; - -use crate::util::unzip; - -#[tokio::test] -async fn publish_single_database_test() { - let test = EventIntegrationTest::new_anon().await; - test.sign_up_as_anon().await; - - // import a csv file and try to get its publish collab - let grid = import_csv("publish_grid_primary.csv", &test).await; - - let grid_encoded_collab = test - .gather_encode_collab_from_disk(&grid.id, ViewLayout::Grid) - .await; - - match grid_encoded_collab { - GatherEncodedCollab::Database(encoded_collab) => { - // the len of row collabs should be the same as the number of rows in the csv file - let rows_len = encoded_collab.database_row_encoded_collabs.len(); - assert_eq!(rows_len, 18); - }, - _ => panic!("Expected database collab"), - } -} - -#[tokio::test] -async fn publish_databases_from_existing_workspace() { - let test = EventIntegrationTest::new_anon().await; - test.sign_up_as_anon().await; - - // import a workspace - // there's a sample screenshot of the workspace in the asset folder, - // unzip it and check the views if needed - let _ = import_workspace("064_database_publish", &test).await; - - let publish_database_set = test.get_all_views().await; - - let publish_grid_set = publish_database_set - .iter() - // there're 8 built-in grids in the workspace with the name starting with "publish grid" - .filter(|view| view.layout == ViewLayoutPB::Grid && view.name.starts_with("publish grid")) - .collect::<Vec<_>>(); - - let publish_calendar_set = publish_database_set - .iter() - // there's 1 built-in calender in the workspace with the name starting with "publish calendar" - .filter(|view| view.layout == ViewLayoutPB::Calendar && view.name.starts_with("publish calendar")) - .collect::<Vec<_>>(); - - let publish_board_set = publish_database_set - .iter() - // there's 1 built-in board in the workspace with the name starting with "publish board" - .filter(|view| view.layout == ViewLayoutPB::Board && view.name.starts_with("publish board")) - .collect::<Vec<_>>(); - - let mut expectations: HashMap<&str, usize> = HashMap::new(); - // grid - // 5 rows - expectations.insert("publish grid (deprecated)", 5); - - // the following 7 grids are the same, just with different filters or sorting or layout - // to check if the collab is correctly generated - // 18 rows - expectations.insert("publish grid", 18); - // 18 rows - expectations.insert("publish grid (with board)", 18); - // 18 rows - expectations.insert("publish grid (with calendar)", 18); - // 18 rows - expectations.insert("publish grid (with grid)", 18); - // 18 rows - expectations.insert("publish grid (filtered)", 18); - // 18 rows - expectations.insert("publish grid (sorted)", 18); - - // calendar - expectations.insert("publish calendar", 2); - - // board - expectations.insert("publish board", 15); - - test_publish_encode_collab_result(&test, publish_grid_set, expectations.clone()).await; - - test_publish_encode_collab_result(&test, publish_calendar_set, expectations.clone()).await; - - test_publish_encode_collab_result(&test, publish_board_set, expectations.clone()).await; -} - -async fn test_publish_encode_collab_result( - test: &EventIntegrationTest, - views: Vec<&ViewPB>, - expectations: HashMap<&str, usize>, -) { - for view in views { - let id = view.id.clone(); - let layout = view.layout.clone(); - - test.open_database(&id).await; - - let encoded_collab = test - .gather_encode_collab_from_disk(&id, layout.into()) - .await; - - match encoded_collab { - GatherEncodedCollab::Database(encoded_collab) => { - if let Some(rows_len) = expectations.get(&view.name.as_str()) { - assert_eq!(encoded_collab.database_row_encoded_collabs.len(), *rows_len); - } - }, - _ => panic!("Expected database collab"), - } - } -} - -async fn import_workspace(file_name: &str, test: &EventIntegrationTest) -> Vec<ViewPB> { - let file_path = unzip("./tests/asset", file_name).unwrap(); - test - .import_appflowy_data(file_path.to_str().unwrap().to_string(), None) - .await - .unwrap(); - - test.get_all_workspace_views().await -} - -async fn import_csv(file_name: &str, test: &EventIntegrationTest) -> ViewPB { - let file_path = unzip("./tests/asset", file_name).unwrap(); - let csv_string = std::fs::read_to_string(file_path).unwrap(); - let workspace_id = test.get_current_workspace().await.id; - let import_data = gen_import_data(file_name.to_string(), csv_string, workspace_id); - let views = test.import_data(import_data).await; - views[0].clone() -} - -fn gen_import_data(file_name: String, csv_string: String, workspace_id: String) -> ImportPayloadPB { - ImportPayloadPB { - parent_view_id: workspace_id.clone(), - items: vec![ImportItemPayloadPB { - name: file_name, - data: Some(csv_string.as_bytes().to_vec()), - file_path: None, - view_layout: ViewLayoutPB::Grid, - import_type: ImportTypePB::CSV, - }], - } -} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs deleted file mode 100644 index 089310b260..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/publish_document_test.rs +++ /dev/null @@ -1,228 +0,0 @@ -use collab_folder::ViewLayout; -use event_integration_test::EventIntegrationTest; -use flowy_folder::entities::{ViewLayoutPB, ViewPB}; -use flowy_folder::publish_util::generate_publish_name; -use flowy_folder::view_operation::GatherEncodedCollab; -use flowy_folder_pub::entities::{ - PublishDocumentPayload, PublishPayload, PublishViewInfo, PublishViewMeta, PublishViewMetaData, -}; -use uuid::Uuid; - -async fn mock_single_document_view_publish_payload( - test: &EventIntegrationTest, - view: &ViewPB, - publish_name: String, -) -> Vec<PublishPayload> { - let view_id = &view.id; - let layout: ViewLayout = view.layout.clone().into(); - let view_encoded_collab = test.gather_encode_collab_from_disk(view_id, layout).await; - let publish_view_info = PublishViewInfo { - view_id: view_id.to_string(), - name: view.name.to_string(), - icon: None, - layout: ViewLayout::Document, - extra: None, - created_by: view.created_by, - last_edited_by: view.last_edited_by, - last_edited_time: view.last_edited, - created_at: view.create_time, - child_views: None, - }; - - let data = match view_encoded_collab { - GatherEncodedCollab::Document(doc) => doc.doc_state.to_vec(), - _ => panic!("Expected document collab"), - }; - - vec![PublishPayload::Document(PublishDocumentPayload { - meta: PublishViewMeta { - metadata: PublishViewMetaData { - view: publish_view_info.clone(), - child_views: vec![], - ancestor_views: vec![publish_view_info], - }, - view_id: view_id.to_string(), - publish_name, - }, - data, - })] -} - -async fn mock_nested_document_view_publish_payload( - test: &EventIntegrationTest, - view: &ViewPB, - publish_name: String, -) -> Vec<PublishPayload> { - let view_id = &view.id; - let layout: ViewLayout = view.layout.clone().into(); - let view_encoded_collab = test.gather_encode_collab_from_disk(view_id, layout).await; - let publish_view_info = PublishViewInfo { - view_id: view_id.to_string(), - name: view.name.to_string(), - icon: None, - layout: ViewLayout::Document, - extra: None, - created_by: view.created_by, - last_edited_by: view.last_edited_by, - last_edited_time: view.last_edited, - created_at: view.create_time, - child_views: None, - }; - - let child_view_id = &view.child_views[0].id; - let child_view = test.get_view(child_view_id).await; - let child_layout: ViewLayout = child_view.layout.clone().into(); - let child_view_encoded_collab = test - .gather_encode_collab_from_disk(child_view_id, child_layout) - .await; - let child_publish_view_info = PublishViewInfo { - view_id: child_view_id.to_string(), - name: child_view.name.to_string(), - icon: None, - layout: ViewLayout::Document, - extra: None, - created_by: child_view.created_by, - last_edited_by: child_view.last_edited_by, - last_edited_time: child_view.last_edited, - created_at: child_view.create_time, - child_views: None, - }; - let child_publish_name = generate_publish_name(&child_view.id, &child_view.name); - - let data = match view_encoded_collab { - GatherEncodedCollab::Document(doc) => doc.doc_state.to_vec(), - _ => panic!("Expected document collab"), - }; - - let child_data = match child_view_encoded_collab { - GatherEncodedCollab::Document(doc) => doc.doc_state.to_vec(), - _ => panic!("Expected document collab"), - }; - - vec![ - PublishPayload::Document(PublishDocumentPayload { - meta: PublishViewMeta { - metadata: PublishViewMetaData { - view: publish_view_info.clone(), - child_views: vec![child_publish_view_info.clone()], - ancestor_views: vec![publish_view_info.clone()], - }, - view_id: view_id.to_string(), - publish_name, - }, - data, - }), - PublishPayload::Document(PublishDocumentPayload { - meta: PublishViewMeta { - metadata: PublishViewMetaData { - view: child_publish_view_info.clone(), - child_views: vec![], - ancestor_views: vec![publish_view_info.clone(), child_publish_view_info.clone()], - }, - view_id: child_view_id.to_string(), - publish_name: child_publish_name, - }, - data: child_data, - }), - ] -} - -async fn create_single_document(test: &EventIntegrationTest, view_id: &str, name: &str) { - test - .create_orphan_view(name, view_id, ViewLayoutPB::Document) - .await; -} - -async fn create_nested_document(test: &EventIntegrationTest, view_id: &str, name: &str) { - create_single_document(test, view_id, name).await; - let child_name = "Child View"; - test.create_view(view_id, child_name.to_string()).await; -} -#[tokio::test] -async fn single_document_get_publish_view_payload_test() { - let test = EventIntegrationTest::new_anon().await; - let view_id = Uuid::new_v4().to_string(); - let name = "Orphan View"; - create_single_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, true).await; - - let expect_payload = mock_single_document_view_publish_payload( - &test, - &view, - format!("{}-{}", "Orphan-View", view_id), - ) - .await; - - assert_eq!(payload, expect_payload); -} - -#[tokio::test] -async fn nested_document_get_publish_view_payload_test() { - let test = EventIntegrationTest::new_anon().await; - let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); - create_nested_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, true).await; - - let expect_payload = mock_nested_document_view_publish_payload( - &test, - &view, - format!("{}-{}", "Orphan-View", view_id), - ) - .await; - - assert_eq!(payload.len(), 2); - assert_eq!(payload, expect_payload); -} - -#[tokio::test] -async fn no_children_publish_view_payload_test() { - let test = EventIntegrationTest::new_anon().await; - let name = "Orphan View"; - let view_id = Uuid::new_v4().to_string(); - create_nested_document(&test, &view_id, name).await; - let view = test.get_view(&view_id).await; - let payload = test.get_publish_payload(&view_id, false).await; - - let data = mock_single_document_view_publish_payload( - &test, - &view, - format!("{}-{}", "Orphan-View", view_id), - ) - .await - .iter() - .filter_map(|p| match p { - PublishPayload::Document(payload) => Some(payload.data.clone()), - _ => None, - }) - .collect::<Vec<_>>(); - let meta = mock_nested_document_view_publish_payload( - &test, - &view, - format!("{}-{}", "Orphan-View", view_id), - ) - .await - .iter() - .filter_map(|p| match p { - PublishPayload::Document(payload) => Some(payload.meta.clone()), - _ => None, - }) - .collect::<Vec<_>>(); - - assert_eq!(payload.len(), 1); - - let payload_data = match &payload[0] { - PublishPayload::Document(payload) => payload.data.clone(), - _ => panic!("Expected document payload"), - }; - - let payload_meta = match &payload[0] { - PublishPayload::Document(payload) => payload.meta.clone(), - _ => panic!("Expected document payload"), - }; - - assert_eq!(&payload_data, &data[0]); - assert_eq!(&payload_meta, &meta[0]); -} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index 22001d9973..0ebbd64efa 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs @@ -207,22 +207,6 @@ impl FolderTest { }, } } - - // pub async fn duplicate_view(&self, view_id: &str) { - // let payload = DuplicateViewPayloadPB { - // view_id: view_id.to_string(), - // open_after_duplicate: false, - // include_children: false, - // parent_view_id: None, - // suffix: None, - // sync_after_create: false, - // }; - // EventBuilder::new(self.sdk.clone()) - // .event(DuplicateView) - // .payload(payload) - // .async_send() - // .await; - // } } pub async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> WorkspacePB { let request = CreateWorkspacePayloadPB { @@ -254,12 +238,13 @@ pub async fn create_view( sdk: &EventIntegrationTest, parent_view_id: &str, name: &str, - _desc: &str, + desc: &str, layout: ViewLayout, ) -> ViewPB { let request = CreateViewPayloadPB { parent_view_id: parent_view_id.to_string(), name: name.to_string(), + desc: desc.to_string(), thumbnail: None, layout: layout.into(), initial_data: vec![], @@ -268,7 +253,6 @@ pub async fn create_view( index: None, section: None, view_id: None, - extra: None, }; EventBuilder::new(sdk.clone()) .event(CreateView) diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/subscription_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/subscription_test.rs index c9460d9db0..089bbae7ba 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/subscription_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/subscription_test.rs @@ -24,9 +24,11 @@ async fn create_child_view_in_workspace_subscription_test() { let cloned_test = test.clone(); let cloned_workspace_id = workspace.id.clone(); - cloned_test - .create_view(&cloned_workspace_id, "workspace child view".to_string()) - .await; + test.appflowy_core.dispatcher().spawn(async move { + cloned_test + .create_view(&cloned_workspace_id, "workspace child view".to_string()) + .await; + }); let views = receive_with_timeout(rx, Duration::from_secs(30)) .await @@ -48,17 +50,14 @@ async fn create_child_view_in_view_subscription_test() { let cloned_test = test.clone(); let child_view_id = workspace_child_view.id.clone(); - let local_set = tokio::task::LocalSet::new(); - local_set - .run_until(async move { - cloned_test - .create_view( - &child_view_id, - "workspace child view's child view".to_string(), - ) - .await; - }) - .await; + test.appflowy_core.dispatcher().spawn(async move { + cloned_test + .create_view( + &child_view_id, + "workspace child view's child view".to_string(), + ) + .await; + }); let update = receive_with_timeout(rx, Duration::from_secs(30)) .await @@ -82,11 +81,22 @@ async fn delete_view_subscription_test() { let cloned_test = test.clone(); let delete_view_id = workspace.views.first().unwrap().id.clone(); let cloned_delete_view_id = delete_view_id.clone(); - - cloned_test.delete_view(&cloned_delete_view_id).await; - let update = receive_with_timeout(rx, Duration::from_secs(60)) + test + .appflowy_core + .dispatcher() + .spawn(async move { + cloned_test.delete_view(&cloned_delete_view_id).await; + }) .await .unwrap(); + + let update = test + .appflowy_core + .dispatcher() + .run_until(receive_with_timeout(rx, Duration::from_secs(60))) + .await + .unwrap(); + assert_eq!(update.delete_child_views.len(), 1); assert_eq!(update.delete_child_views[0], delete_view_id); } @@ -104,14 +114,17 @@ async fn update_view_subscription_test() { assert!(!view.is_favorite); let update_view_id = view.id.clone(); - cloned_test - .update_view(UpdateViewPayloadPB { - view_id: update_view_id, - name: Some("hello world".to_string()), - is_favorite: Some(true), - ..Default::default() - }) - .await; + test.appflowy_core.dispatcher().spawn(async move { + cloned_test + .update_view(UpdateViewPayloadPB { + view_id: update_view_id, + name: Some("hello world".to_string()), + is_favorite: Some(true), + ..Default::default() + }) + .await; + }); + let update = receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs index 2297324c53..09af815d65 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/test.rs @@ -4,6 +4,23 @@ use flowy_folder::entities::icon::{UpdateViewIconPayloadPB, ViewIconPB, ViewIcon use flowy_folder::entities::*; use flowy_user::errors::ErrorCode; +#[tokio::test] +async fn create_workspace_event_test() { + let test = EventIntegrationTest::new_anon().await; + let request = CreateWorkspacePayloadPB { + name: "my second workspace".to_owned(), + desc: "".to_owned(), + }; + let view_pb = EventBuilder::new(test) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .parse::<flowy_folder::entities::ViewPB>(); + + assert_eq!(view_pb.parent_view_id, "my second workspace".to_owned()); +} + // #[tokio::test] // async fn open_workspace_event_test() { // let test = EventIntegrationTest::new_with_guest_user().await; @@ -447,6 +464,35 @@ async fn move_view_event_after_delete_view_test2() { assert_eq!(views[3].name, "My 1-5 view"); } +#[tokio::test] +async fn create_parent_view_with_invalid_name() { + for (name, code) in invalid_workspace_name_test_case() { + let sdk = EventIntegrationTest::new().await; + let request = CreateWorkspacePayloadPB { + name, + desc: "".to_owned(), + }; + assert_eq!( + EventBuilder::new(sdk) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) + .payload(request) + .async_send() + .await + .error() + .unwrap() + .code, + code + ) + } +} + +fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { + vec![ + ("".to_owned(), ErrorCode::WorkspaceNameInvalid), + ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong), + ] +} + #[tokio::test] async fn move_view_across_parent_test() { let test = EventIntegrationTest::new_anon().await; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs index c5566e1b80..01d3a22023 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/mod.rs @@ -1,3 +1,4 @@ mod local_test; + // #[cfg(feature = "supabase_cloud_test")] // mod supabase_test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs new file mode 100644 index 0000000000..ed701fd1a7 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/helper.rs @@ -0,0 +1,91 @@ +use std::ops::Deref; + +use assert_json_diff::assert_json_eq; +use collab::core::collab::MutexCollab; +use collab::core::origin::CollabOrigin; +use collab::preclude::updates::decoder::Decode; +use collab::preclude::{Collab, JsonValue, Update}; +use collab_entity::CollabType; +use collab_folder::FolderData; + +use event_integration_test::event_builder::EventBuilder; +use flowy_folder::entities::{FolderSnapshotPB, RepeatedFolderSnapshotPB, WorkspaceIdPB}; +use flowy_folder::event_map::FolderEvent::GetFolderSnapshots; + +use crate::util::FlowySupabaseTest; + +pub struct FlowySupabaseFolderTest { + inner: FlowySupabaseTest, +} + +impl FlowySupabaseFolderTest { + pub async fn new() -> Option<Self> { + let inner = FlowySupabaseTest::new().await?; + let uuid = uuid::Uuid::new_v4().to_string(); + let _ = inner.supabase_sign_up_with_uuid(&uuid, None).await; + Some(Self { inner }) + } + + pub async fn get_collab_json(&self) -> JsonValue { + let folder = self.folder_manager.get_mutex_folder().lock(); + folder.as_ref().unwrap().to_json_value() + } + + pub async fn get_local_folder_data(&self) -> FolderData { + let folder = self.folder_manager.get_mutex_folder().lock(); + folder.as_ref().unwrap().get_folder_data().unwrap() + } + + pub async fn get_folder_snapshots(&self, workspace_id: &str) -> Vec<FolderSnapshotPB> { + EventBuilder::new(self.inner.deref().clone()) + .event(GetFolderSnapshots) + .payload(WorkspaceIdPB { + value: workspace_id.to_string(), + }) + .async_send() + .await + .parse::<RepeatedFolderSnapshotPB>() + .items + } + + pub async fn get_collab_update(&self, workspace_id: &str) -> Vec<u8> { + let cloud_service = self.folder_manager.get_cloud_service().clone(); + cloud_service + .get_folder_doc_state( + workspace_id, + self.user_manager.user_id().unwrap(), + CollabType::Folder, + workspace_id, + ) + .await + .unwrap() + } +} + +pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], expected: JsonValue) { + if collab_update.is_empty() { + panic!("collab update is empty"); + } + + let collab = MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Server, + workspace_id, + vec![], + false, + )); + collab.lock().with_origin_transact_mut(|txn| { + let update = Update::decode_v1(collab_update).unwrap(); + txn.apply_update(update); + }); + + let json = collab.to_json_value(); + assert_json_eq!(json["folder"], expected); +} + +impl Deref for FlowySupabaseFolderTest { + type Target = FlowySupabaseTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs new file mode 100644 index 0000000000..05fa1b00ed --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod helper; +mod test; diff --git a/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs new file mode 100644 index 0000000000..5f6a50988a --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/folder/supabase_test/test.rs @@ -0,0 +1,122 @@ +use std::time::Duration; + +use assert_json_diff::assert_json_eq; +use serde_json::json; + +use flowy_folder::entities::{FolderSnapshotStatePB, FolderSyncStatePB}; +use flowy_folder::notification::FolderNotification::DidUpdateFolderSnapshotState; + +use crate::folder::supabase_test::helper::{assert_folder_collab_content, FlowySupabaseFolderTest}; +use crate::util::{get_folder_data_from_server, receive_with_timeout}; + +#[tokio::test] +async fn supabase_encrypt_folder_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let secret = test.enable_encryption().await; + + let local_folder_data = test.get_local_folder_data().await; + let workspace_id = test.get_current_workspace().await.id; + let remote_folder_data = get_folder_data_from_server(&uid, &workspace_id, Some(secret)) + .await + .unwrap() + .unwrap(); + + assert_json_eq!(json!(local_folder_data), json!(remote_folder_data)); + } +} + +#[tokio::test] +async fn supabase_decrypt_folder_data_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let secret = Some(test.enable_encryption().await); + let workspace_id = test.get_current_workspace().await.id; + test + .create_view(&workspace_id, "encrypt view".to_string()) + .await; + + let rx = test + .notification_sender + .subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish); + + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + let folder_data = get_folder_data_from_server(&uid, &workspace_id, secret) + .await + .unwrap() + .unwrap(); + assert_eq!(folder_data.views.len(), 2); + assert_eq!(folder_data.views[1].name, "encrypt view"); + } +} + +#[tokio::test] +#[should_panic] +async fn supabase_decrypt_with_invalid_secret_folder_data_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let uid = test.user_manager.user_id().unwrap(); + let _ = Some(test.enable_encryption().await); + let workspace_id = test.get_current_workspace().await.id; + test + .create_view(&workspace_id, "encrypt view".to_string()) + .await; + let rx = test + .notification_sender + .subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish); + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let _ = get_folder_data_from_server(&uid, &workspace_id, Some("invalid secret".to_string())) + .await + .unwrap(); + } +} +#[tokio::test] +async fn supabase_folder_snapshot_test() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.id; + let rx = test + .notification_sender + .subscribe::<FolderSnapshotStatePB>(&workspace_id, DidUpdateFolderSnapshotState); + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let snapshots = test.get_folder_snapshots(&workspace_id).await; + assert_eq!(snapshots.len(), 1); + assert_folder_collab_content(&workspace_id, &snapshots[0].data, expected); + } +} + +#[tokio::test] +async fn supabase_initial_folder_snapshot_test2() { + if let Some(test) = FlowySupabaseFolderTest::new().await { + let workspace_id = test.get_current_workspace().await.id; + + test + .create_view(&workspace_id, "supabase test view1".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view2".to_string()) + .await; + test + .create_view(&workspace_id, "supabase test view3".to_string()) + .await; + + let rx = test + .notification_sender + .subscribe_with_condition::<FolderSyncStatePB, _>(&workspace_id, |pb| pb.is_finish); + + receive_with_timeout(rx, Duration::from_secs(10)) + .await + .unwrap(); + + let expected = test.get_collab_json().await; + let update = test.get_collab_update(&workspace_id).await; + assert_folder_collab_content(&workspace_id, &update, expected); + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/main.rs b/frontend/rust-lib/event-integration-test/tests/main.rs index cf4c1591ac..05f19e9b75 100644 --- a/frontend/rust-lib/event-integration-test/tests/main.rs +++ b/frontend/rust-lib/event-integration-test/tests/main.rs @@ -4,8 +4,6 @@ mod folder; // TODO(Mathias): Enable tests for search // mod search; - -mod sql_test; mod user; pub mod util; diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs deleted file mode 100644 index 3294ad26db..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/chat_message_test.rs +++ /dev/null @@ -1,609 +0,0 @@ -use event_integration_test::user_event::use_localhost_af_cloud; -use event_integration_test::EventIntegrationTest; -use flowy_ai_pub::cloud::MessageCursor; -use flowy_ai_pub::persistence::{ - select_answer_where_match_reply_message_id, select_chat_messages, select_message, - select_message_content, total_message_count, upsert_chat_messages, ChatMessageTable, -}; -use uuid::Uuid; - -#[tokio::test] -async fn chat_message_table_insert_select_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id_1 = 1000; - let message_id_2 = 2000; - - // Create test messages - let messages = vec![ - ChatMessageTable { - message_id: message_id_1, - chat_id: chat_id.clone(), - content: "Hello, this is a test message".to_string(), - created_at: 1625097600, // 2021-07-01 - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ChatMessageTable { - message_id: message_id_2, - chat_id: chat_id.clone(), - content: "This is a reply to the test message".to_string(), - created_at: 1625097700, // 2021-07-01, 100 seconds later - author_type: 0, // AI - author_id: "ai".to_string(), - reply_message_id: Some(message_id_1), - metadata: Some(r#"{"source": "test"}"#.to_string()), - is_sync: false, - }, - ]; - - // Test insert_chat_messages - let result = upsert_chat_messages(db_conn, &messages); - assert!( - result.is_ok(), - "Failed to insert chat messages: {:?}", - result - ); - - // Test select_chat_messages - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let messages_result = - select_chat_messages(db_conn, &chat_id, 10, MessageCursor::Offset(0)).unwrap(); - - assert_eq!(messages_result.messages.len(), 2); - assert_eq!(messages_result.total_count, 2); - assert!(!messages_result.has_more); - - // Verify the content of the returned messages - let first_message = messages_result - .messages - .iter() - .find(|m| m.message_id == message_id_1) - .unwrap(); - assert_eq!(first_message.content, "Hello, this is a test message"); - assert_eq!(first_message.author_type, 1); - - let second_message = messages_result - .messages - .iter() - .find(|m| m.message_id == message_id_2) - .unwrap(); - assert_eq!( - second_message.content, - "This is a reply to the test message" - ); - assert_eq!(second_message.reply_message_id, Some(message_id_1)); -} - -#[tokio::test] -async fn chat_message_table_cursor_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create multiple test messages with sequential IDs - let mut messages = Vec::new(); - for i in 1..6 { - messages.push(ChatMessageTable { - message_id: i * 1000, - chat_id: chat_id.clone(), - content: format!("Message {}", i), - created_at: 1625097600 + (i * 100), // Increasing timestamps - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }); - } - - // Insert messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Test MessageCursor::Offset - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_offset = select_chat_messages( - db_conn, - &chat_id, - 2, // Limit to 2 messages - MessageCursor::Offset(0), - ) - .unwrap(); - - assert_eq!(result_offset.messages.len(), 2); - assert!(result_offset.has_more); - - // Test MessageCursor::AfterMessageId - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_after = select_chat_messages( - db_conn, - &chat_id, - 3, // Limit to 3 messages - MessageCursor::AfterMessageId(2000), - ) - .unwrap(); - - assert_eq!(result_after.messages.len(), 3); // Should get message IDs 3000, 4000, 5000 - assert!(result_after.messages.iter().all(|m| m.message_id > 2000)); - - // Test MessageCursor::BeforeMessageId - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_before = select_chat_messages( - db_conn, - &chat_id, - 2, // Limit to 2 messages - MessageCursor::BeforeMessageId(4000), - ) - .unwrap(); - - assert_eq!(result_before.messages.len(), 2); // Should get message IDs 1000, 2000, 3000 - assert!(result_before.messages.iter().all(|m| m.message_id < 4000)); -} - -#[tokio::test] -async fn chat_message_total_count_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create test messages - let messages = vec![ - ChatMessageTable { - message_id: 1001, - chat_id: chat_id.clone(), - content: "Message 1".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ChatMessageTable { - message_id: 1002, - chat_id: chat_id.clone(), - content: "Message 2".to_string(), - created_at: 1625097700, - author_type: 0, - author_id: "ai".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }, - ]; - - // Insert messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Test total_message_count - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 2); - - // Add one more message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let additional_message = ChatMessageTable { - message_id: 1003, - chat_id: chat_id.clone(), - content: "Message 3".to_string(), - created_at: 1625097800, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - upsert_chat_messages(db_conn, &[additional_message]).unwrap(); - - // Verify count increased - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let updated_count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(updated_count, 3); - - // Test count for non-existent chat - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let empty_count = total_message_count(db_conn, "non_existent_chat").unwrap(); - assert_eq!(empty_count, 0); -} - -#[tokio::test] -async fn chat_message_select_message_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 2001; - - // Create test message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: "This is a test message for select_message".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: Some(r#"{"test_key": "test_value"}"#.to_string()), - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Test select_message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message(db_conn, message_id).unwrap(); - assert!(result.is_some()); - - let retrieved_message = result.unwrap(); - assert_eq!(retrieved_message.message_id, message_id); - assert_eq!(retrieved_message.chat_id, chat_id); - assert_eq!( - retrieved_message.content, - "This is a test message for select_message" - ); - assert_eq!(retrieved_message.author_id, "user_1"); - assert_eq!( - retrieved_message.metadata, - Some(r#"{"test_key": "test_value"}"#.to_string()) - ); - - // Test select_message with non-existent ID - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let non_existent = select_message(db_conn, 9999).unwrap(); - assert!(non_existent.is_none()); -} - -#[tokio::test] -async fn chat_message_select_content_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 3001; - let message_content = "This is the content to retrieve"; - - // Create test message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: message_content.to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Test select_message_content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let content = select_message_content(db_conn, message_id).unwrap(); - assert!(content.is_some()); - assert_eq!(content.unwrap(), message_content); - - // Test with non-existent message - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_content = select_message_content(db_conn, 9999).unwrap(); - assert!(no_content.is_none()); -} - -#[tokio::test] -async fn chat_message_reply_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let question_id = 4001; - let answer_id = 4002; - - // Create question and answer messages - let question = ChatMessageTable { - message_id: question_id, - chat_id: chat_id.clone(), - content: "What is the question?".to_string(), - created_at: 1625097600, - author_type: 1, // User - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - let answer = ChatMessageTable { - message_id: answer_id, - chat_id: chat_id.clone(), - content: "This is the answer".to_string(), - created_at: 1625097700, - author_type: 0, // AI - author_id: "ai".to_string(), - reply_message_id: Some(question_id), // Link to question - metadata: None, - is_sync: false, - }; - - // Insert messages - upsert_chat_messages(db_conn, &[question, answer]).unwrap(); - - // Test select_message_where_match_reply_message_id - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_answer_where_match_reply_message_id(db_conn, &chat_id, question_id).unwrap(); - - assert!(result.is_some()); - let reply = result.unwrap(); - assert_eq!(reply.message_id, answer_id); - assert_eq!(reply.content, "This is the answer"); - assert_eq!(reply.reply_message_id, Some(question_id)); - - // Test with non-existent reply relation - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let no_reply = select_answer_where_match_reply_message_id( - db_conn, &chat_id, 9999, // Non-existent question ID - ) - .unwrap(); - - assert!(no_reply.is_none()); - - // Test with wrong chat_id - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let wrong_chat = - select_answer_where_match_reply_message_id(db_conn, "wrong_chat_id", question_id).unwrap(); - - assert!(wrong_chat.is_none()); -} - -#[tokio::test] -async fn chat_message_upsert_test() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - let message_id = 5001; - - // Create initial message - let message = ChatMessageTable { - message_id, - chat_id: chat_id.clone(), - content: "Original content".to_string(), - created_at: 1625097600, - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: None, - metadata: None, - is_sync: false, - }; - - // Insert message - upsert_chat_messages(db_conn, &[message]).unwrap(); - - // Check original content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let original = select_message(db_conn, message_id).unwrap().unwrap(); - assert_eq!(original.content, "Original content"); - - // Create updated message with same ID but different content - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let updated_message = ChatMessageTable { - message_id, // Same ID - chat_id: chat_id.clone(), - content: "Updated content".to_string(), // New content - created_at: 1625097700, // Updated timestamp - author_type: 1, - author_id: "user_1".to_string(), - reply_message_id: Some(1000), // Added reply ID - metadata: Some(r#"{"updated": true}"#.to_string()), - is_sync: false, - }; - - // Upsert message - upsert_chat_messages(db_conn, &[updated_message]).unwrap(); - - // Verify update - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result = select_message(db_conn, message_id).unwrap().unwrap(); - assert_eq!(result.content, "Updated content"); - assert_eq!(result.created_at, 1625097700); - assert_eq!(result.reply_message_id, Some(1000)); - assert_eq!(result.metadata, Some(r#"{"updated": true}"#.to_string())); - - // Count should still be 1 (update, not insert) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 1); -} - -#[tokio::test] -async fn chat_message_select_with_large_dataset() { - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - test.sign_up_as_anon().await; - - let uid = test.user_manager.get_anon_user().await.unwrap().id; - let db_conn = test.user_manager.db_connection(uid).unwrap(); - - let chat_id = Uuid::new_v4().to_string(); - - // Create 100 test messages with sequential IDs - let mut messages = Vec::new(); - for i in 1..=100 { - messages.push(ChatMessageTable { - message_id: i * 100, - chat_id: chat_id.clone(), - content: format!("Message {}", i), - created_at: 1625097600 + (i * 10), // Increasing timestamps - author_type: if i % 2 == 0 { 0 } else { 1 }, // Alternate between AI and User - author_id: if i % 2 == 0 { - "ai".to_string() - } else { - "user_1".to_string() - }, - reply_message_id: if i > 1 && i % 2 == 0 { - Some((i - 1) * 100) - } else { - None - }, // Even messages reply to previous message - metadata: if i % 5 == 0 { - Some(format!(r#"{{"index": {}}}"#, i)) - } else { - None - }, - is_sync: false, - }); - } - - // Insert all 100 messages - upsert_chat_messages(db_conn, &messages).unwrap(); - - // Verify total count - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let count = total_message_count(db_conn, &chat_id).unwrap(); - assert_eq!(count, 100, "Should have 100 messages in the database"); - - // Test 1: MessageCursor::Offset with small page size - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let page_size = 10; - let result_offset = - select_chat_messages(db_conn, &chat_id, page_size, MessageCursor::Offset(0)).unwrap(); - - assert_eq!( - result_offset.messages.len(), - page_size as usize, - "Should return exactly {page_size} messages" - ); - assert!( - result_offset.has_more, - "Should have more messages available" - ); - assert_eq!(result_offset.total_count, 100, "Total count should be 100"); - - // Test 2: Pagination with offset - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_page2 = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::Offset(page_size), - ) - .unwrap(); - - assert_eq!(result_page2.messages.len(), page_size as usize); - assert!( - result_page2.messages[0].message_id != result_offset.messages[0].message_id, - "Second page should have different messages than first page" - ); - - // Test 3: MessageCursor::AfterMessageId (forward pagination) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let middle_message_id = 5000; // Message ID from the middle - let result_after = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::AfterMessageId(middle_message_id), - ) - .unwrap(); - - assert_eq!(result_after.messages.len(), page_size as usize); - assert!( - result_after - .messages - .iter() - .all(|m| m.message_id > middle_message_id), - "All messages should have ID greater than the cursor" - ); - - // Test 4: MessageCursor::BeforeMessageId (backward pagination) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_before = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::BeforeMessageId(middle_message_id), - ) - .unwrap(); - - assert_eq!(result_before.messages.len(), page_size as usize); - assert!( - result_before - .messages - .iter() - .all(|m| m.message_id < middle_message_id), - "All messages should have ID less than the cursor" - ); - - // Test 5: Large page size (retrieve all) - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_all = select_chat_messages( - db_conn, - &chat_id, - 200, // More than we have - MessageCursor::Offset(0), - ) - .unwrap(); - - assert_eq!( - result_all.messages.len(), - 100, - "Should return all 100 messages" - ); - assert!(!result_all.has_more, "Should not have more messages"); - - // Test 6: Empty result when using out of range cursor - let db_conn = test.user_manager.db_connection(uid).unwrap(); - let result_out_of_range = select_chat_messages( - db_conn, - &chat_id, - page_size, - MessageCursor::AfterMessageId(10000), // After the last message - ) - .unwrap(); - - assert_eq!( - result_out_of_range.messages.len(), - 0, - "Should return no messages" - ); - assert!( - !result_out_of_range.has_more, - "Should not have more messages" - ); -} diff --git a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs deleted file mode 100644 index 773bdab81f..0000000000 --- a/frontend/rust-lib/event-integration-test/tests/sql_test/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod chat_message_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs index 301b6e5a62..dbcfb7097f 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/anon_user_test.rs @@ -1,13 +1,13 @@ -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_user::entities::AuthTypePB; +use flowy_user::entities::AuthenticatorPB; use crate::util::unzip; #[tokio::test] async fn reading_039_anon_user_data_test() { - let user_db_path = unzip("./tests/asset", "039_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "039_local").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let first_level_views = test.get_all_workspace_views().await; @@ -36,18 +36,20 @@ async fn reading_039_anon_user_data_test() { let trash_items = test.get_trash().await.items; assert_eq!(trash_items.len(), 1); + + drop(cleaner); } #[tokio::test] async fn migrate_anon_user_data_to_af_cloud_test() { - let user_db_path = unzip("./tests/asset", "040_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "040_local").unwrap(); // In the 040_local, the structure is: // workspace: // view: Document1 // view: Document2 // view: Grid1 // view: Grid2 - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) .await; @@ -72,14 +74,14 @@ async fn migrate_anon_user_data_to_af_cloud_test() { let user = test.af_cloud_sign_up().await; let workspace = test.get_current_workspace().await; println!("user workspace: {:?}", workspace.id); - assert_eq!(user.user_auth_type, AuthTypePB::Server); + assert_eq!(user.authenticator, AuthenticatorPB::AppFlowyCloud); let user_first_level_views = test.get_all_workspace_views().await; - assert_eq!(user_first_level_views.len(), 3); + assert_eq!(user_first_level_views.len(), 2); println!("user first level views: {:?}", user_first_level_views); let user_second_level_views = test - .get_view(&user_first_level_views[2].id) + .get_view(&user_first_level_views[1].id) .await .child_views; println!("user second level views: {:?}", user_second_level_views); @@ -93,14 +95,15 @@ async fn migrate_anon_user_data_to_af_cloud_test() { assert_eq!(anon_first_level_views.len(), 1); // the first view of user_first_level_views is the default get started view - assert_eq!(user_first_level_views.len(), 3); + assert_eq!(user_first_level_views.len(), 2); assert_ne!(anon_first_level_views[0].id, user_first_level_views[1].id); assert_eq!( anon_first_level_views[0].name, - user_first_level_views[2].name + user_first_level_views[1].name ); // check second level + assert_eq!(anon_second_level_views.len(), user_second_level_views.len()); assert_ne!(anon_second_level_views[0].id, user_second_level_views[0].id); assert_eq!( anon_second_level_views[0].name, @@ -111,4 +114,6 @@ async fn migrate_anon_user_data_to_af_cloud_test() { assert_eq!(anon_third_level_views.len(), 2); assert_eq!(user_third_level_views[0].name, "Grid1".to_string()); assert_eq!(user_third_level_views[1].name, "Grid2".to_string()); + + drop(cleaner); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs index eaec8f7540..3f9a5b3f01 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/auth_test.rs @@ -1,14 +1,41 @@ -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; +use flowy_user::entities::UpdateUserProfilePayloadPB; use crate::util::generate_test_email; #[tokio::test] async fn af_cloud_sign_up_test() { // user_localhost_af_cloud_with_nginx().await; - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let email = generate_test_email(); let user = test.af_cloud_sign_in_with_email(&email).await.unwrap(); assert_eq!(user.email, email); } + +#[tokio::test] +async fn af_cloud_update_user_metadata() { + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + let user = test.af_cloud_sign_up().await; + + let old_profile = test.get_user_profile().await.unwrap(); + assert_eq!(old_profile.openai_key, "".to_string()); + + test + .update_user_profile(UpdateUserProfilePayloadPB { + id: user.id, + openai_key: Some("new openai key".to_string()), + stability_ai_key: Some("new stability ai key".to_string()), + ..Default::default() + }) + .await; + + let new_profile = test.get_user_profile().await.unwrap(); + assert_eq!(new_profile.openai_key, "new openai key".to_string()); + assert_eq!( + new_profile.stability_ai_key, + "new stability ai key".to_string() + ); +} diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/import_af_data_folder_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/import_af_data_folder_test.rs index cc02bb1f2e..455d4db140 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/import_af_data_folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/import_af_data_folder_test.rs @@ -1,28 +1,24 @@ use crate::util::unzip; use assert_json_diff::assert_json_include; -use collab::core::collab::DataSource; -use collab::core::origin::CollabOrigin; -use collab::preclude::{Any, Collab}; use collab_database::rows::database_row_document_id_from_row_id; -use collab_document::blocks::TextDelta; -use collab_document::document::Document; -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_folder::entities::ViewLayoutPB; use flowy_user::errors::ErrorCode; use serde_json::{json, Value}; use std::env::temp_dir; #[tokio::test] -async fn import_appflowy_data_with_ref_views_test() { - let import_container_name = "data_ref_doc".to_string(); - let user_db_path = unzip("./tests/asset", &import_container_name).unwrap(); - use_localhost_af_cloud().await; +async fn import_appflowy_data_need_migration_test() { + // In 037, the workspace array will be migrated to view. + let import_container_name = "037_local".to_string(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); + // Getting started + // Document1 + // Document2(fav) + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; - let views = test.get_all_workspace_views().await; - let shared_space_id = views[1].id.clone(); test .import_appflowy_data( user_db_path.to_str().unwrap().to_string(), @@ -30,98 +26,44 @@ async fn import_appflowy_data_with_ref_views_test() { ) .await .unwrap(); + // after import, the structure is: + // workspace: + // view: Getting Started + // view: 037_local + // view: Getting Started + // view: Document1 + // view: Document2 - let general_space = test.get_view(&shared_space_id).await; - let shared_sub_views = &general_space.child_views; - assert_eq!(shared_sub_views.len(), 1); - assert_eq!(shared_sub_views[0].name, import_container_name); + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, import_container_name); - let imported_view_id = shared_sub_views[0].id.clone(); - let imported_sub_views = test.get_view(&imported_view_id).await.child_views; - assert_eq!(imported_sub_views.len(), 1); + let child_views = test.get_view(&views[1].id).await.child_views; + assert_eq!(child_views.len(), 1); - let imported_get_started_view_id = imported_sub_views[0].id.clone(); - let doc_state = test - .get_document_doc_state(&imported_get_started_view_id) - .await; - let collab = Collab::new_with_source( - CollabOrigin::Empty, - &imported_get_started_view_id, - DataSource::DocStateV1(doc_state), - vec![], - false, - ) - .unwrap(); - let document = Document::open(collab).unwrap(); - - let page_id = document.get_page_id().unwrap(); - let block_ids = document.get_block_children_ids(&page_id); - let mut page_ids = vec![]; - let mut link_ids = vec![]; - for block_id in block_ids.iter() { - // Process block deltas - if let Some(mut block_deltas) = document.get_block_delta(block_id).map(|t| t.1) { - for d in block_deltas.iter_mut() { - if let TextDelta::Inserted(_, Some(attrs)) = d { - if let Some(Any::Map(mention)) = attrs.get_mut("mention") { - if let Some(page_id) = mention.get("page_id").map(|v| v.to_string()) { - page_ids.push(page_id); - } - } - } - } - } - - if let Some((_, data)) = document.get_block_data(block_id) { - if let Some(link_view_id) = data.get("view_id").and_then(|v| v.as_str()) { - link_ids.push(link_view_id.to_string()); - } - } - } - - assert_eq!(page_ids.len(), 1); - for page_id in page_ids { - let view = test.get_view(&page_id).await; - assert_eq!(view.name, "1"); - let data = serde_json::to_string(&test.get_document_data(&view.id).await).unwrap(); - assert!(data.contains("hello world")); - } - - assert_eq!(link_ids.len(), 1); - for link_id in link_ids { - let database_view = test.get_view(&link_id).await; - assert_eq!(database_view.layout, ViewLayoutPB::Grid); - assert_eq!(database_view.name, "Untitled"); - } + let child_views = test.get_view(&child_views[0].id).await.child_views; + assert_eq!(child_views.len(), 2); + assert_eq!(child_views[0].name, "Document1"); + assert_eq!(child_views[1].name, "Document2"); + drop(cleaner); } #[tokio::test] async fn import_appflowy_data_folder_into_new_view_test() { let import_container_name = "040_local".to_string(); - let user_db_path = unzip("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local, the structure is: - // Document1 - // Document2 - // Grid1 - // Grid2 - use_localhost_af_cloud().await; + // workspace: + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; - let views = test.get_all_workspace_views().await; - assert_eq!(views[0].name, "General"); - assert_eq!(views[1].name, "Shared"); - assert_eq!(views.len(), 2); - let shared_space_id = views[1].id.clone(); - let shared_space = test.get_view(&shared_space_id).await; - - // by default, shared space is empty - assert!(shared_space.child_views.is_empty()); // after sign up, the initial workspace is created, so the structure is: // workspace: - // General - // template_document - // template_document - // Shared + // view: Getting Started test .import_appflowy_data( @@ -132,26 +74,24 @@ async fn import_appflowy_data_folder_into_new_view_test() { .unwrap(); // after import, the structure is: // workspace: - // General - // template_document - // template_document - // 040_local - // Shared - let general_space = test.get_view(&shared_space_id).await; - let shared_sub_views = &general_space.child_views; - assert_eq!(shared_sub_views.len(), 1); - assert_eq!(shared_sub_views[0].name, import_container_name); + // view: Getting Started + // view: 040_local + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, import_container_name); // the 040_local should be an empty document, so try to get the document data - let _ = test.get_document_data(&shared_sub_views[0].id).await; + let _ = test.get_document_data(&views[1].id).await; - let t_040_local_child_views = test.get_view(&shared_sub_views[0].id).await.child_views; - assert_eq!(t_040_local_child_views[0].name, "Document1"); + let local_child_views = test.get_view(&views[1].id).await.child_views; + assert_eq!(local_child_views.len(), 1); + assert_eq!(local_child_views[0].name, "Document1"); - let document1_child_views = test - .get_view(&t_040_local_child_views[0].id) - .await - .child_views; + let document1_child_views = test.get_view(&local_child_views[0].id).await.child_views; assert_eq!(document1_child_views.len(), 1); assert_eq!(document1_child_views[0].name, "Document2"); @@ -173,18 +113,20 @@ async fn import_appflowy_data_folder_into_new_view_test() { let row_document_data = test.get_document_data(&row_document_id).await; assert_json_include!(actual: json!(row_document_data), expected: expected_row_doc_json()); + drop(cleaner); } #[tokio::test] async fn import_appflowy_data_folder_into_current_workspace_test() { let import_container_name = "040_local".to_string(); - let user_db_path = unzip("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local, the structure is: - // Document1 - // Document2 - // Grid1 - // Grid2 - use_localhost_af_cloud().await; + // workspace: + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; // after sign up, the initial workspace is created, so the structure is: @@ -195,25 +137,19 @@ async fn import_appflowy_data_folder_into_current_workspace_test() { .import_appflowy_data(user_db_path.to_str().unwrap().to_string(), None) .await .unwrap(); - let views = test.get_all_workspace_views().await; - assert_eq!(views[0].name, "General"); - assert_eq!(views[1].name, "Shared"); - assert_eq!(views.len(), 2); - let shared_space_id = views[1].id.clone(); - let shared_space_child_views = test.get_view(&shared_space_id).await.child_views; - assert_eq!(shared_space_child_views.len(), 1); - // after import, the structure is: // workspace: - // General - // Shared - // Document1 - // Document2 - // Grid1 - // Grid2 - let document_1 = test.get_view(&shared_space_child_views[0].id).await; - assert_eq!(document_1.name, "Document1"); - let document_1_child_views = test.get_view(&document_1.id).await.child_views; + // view: Getting Started + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, "Document1"); + + let document_1_child_views = test.get_view(&views[1].id).await.child_views; + assert_eq!(document_1_child_views.len(), 1); assert_eq!(document_1_child_views[0].name, "Document2"); let document2_child_views = test @@ -223,12 +159,37 @@ async fn import_appflowy_data_folder_into_current_workspace_test() { assert_eq!(document2_child_views.len(), 2); assert_eq!(document2_child_views[0].name, "Grid1"); assert_eq!(document2_child_views[1].name, "Grid2"); + + drop(cleaner); +} + +#[tokio::test] +async fn import_appflowy_data_folder_into_new_view_test2() { + let import_container_name = "040_local_2".to_string(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; + let _ = test.af_cloud_sign_up().await; + test + .import_appflowy_data( + user_db_path.to_str().unwrap().to_string(), + Some(import_container_name.clone()), + ) + .await + .unwrap(); + + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, import_container_name); + assert_040_local_2_import_content(&test, &views[1].id).await; + + drop(cleaner); } #[tokio::test] async fn import_empty_appflowy_data_folder_test() { let path = temp_dir(); - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; let error = test @@ -244,7 +205,7 @@ async fn import_empty_appflowy_data_folder_test() { #[tokio::test] async fn import_appflowy_data_folder_multiple_times_test() { let import_container_name = "040_local_2".to_string(); - let user_db_path = unzip("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); // In the 040_local_2, the structure is: // Getting Started // Doc1 @@ -254,17 +215,24 @@ async fn import_appflowy_data_folder_multiple_times_test() { // Doc3_grid_1 // Doc3_grid_2 // Doc3_calendar_1 - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; let _ = test.af_cloud_sign_up().await; + test + .import_appflowy_data( + user_db_path.to_str().unwrap().to_string(), + Some(import_container_name.clone()), + ) + .await + .unwrap(); + // after import, the structure is: + // Getting Started + // 040_local_2 + let views = test.get_all_workspace_views().await; - assert_eq!(views[0].name, "General"); - assert_eq!(views[1].name, "Shared"); assert_eq!(views.len(), 2); - let shared_space_id = views[1].id.clone(); - let shared_space = test.get_view(&shared_space_id).await; - // by default, shared space is empty - assert!(shared_space.child_views.is_empty()); + assert_eq!(views[1].name, import_container_name); + assert_040_local_2_import_content(&test, &views[1].id).await; test .import_appflowy_data( @@ -274,42 +242,17 @@ async fn import_appflowy_data_folder_multiple_times_test() { .await .unwrap(); // after import, the structure is: - // General - // Shared - // 040_local_2 - // Getting Started - // Doc1 - // Doc2 - // Grid1 - // Doc3 - // Doc3_grid_1 - // Doc3_grid_2 - // Doc3_calendar_1 - - let shared_space_children_views = test.get_view(&shared_space_id).await.child_views; - assert_eq!(shared_space_children_views.len(), 1); - let _040_local_view_id = shared_space_children_views[0].id.clone(); - let _040_local_view = test.get_view(&_040_local_view_id).await; - assert_eq!(_040_local_view.name, import_container_name); - assert_040_local_2_import_content(&test, &_040_local_view_id).await; - - test - .import_appflowy_data( - user_db_path.to_str().unwrap().to_string(), - Some(import_container_name.clone()), - ) - .await - .unwrap(); - // after import, the structure is: - // Generate - // Shared - // 040_local_2 - // 040_local_2 - let shared_space_children_views = test.get_view(&shared_space_id).await.child_views; - assert_eq!(shared_space_children_views.len(), 2); - for view in shared_space_children_views { - assert_040_local_2_import_content(&test, &view.id).await; - } + // Getting Started + // 040_local_2 + // Getting started + // 040_local_2 + // Getting started + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 3); + assert_eq!(views[2].name, import_container_name); + assert_040_local_2_import_content(&test, &views[1].id).await; + assert_040_local_2_import_content(&test, &views[2].id).await; + drop(cleaner); } async fn assert_040_local_2_import_content(test: &EventIntegrationTest, view_id: &str) { diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs index e10f6b45d0..c5fc2479bb 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/member_test.rs @@ -1,5 +1,5 @@ use crate::user::af_cloud_test::util::get_synced_workspaces; -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; #[tokio::test] @@ -35,21 +35,22 @@ async fn af_cloud_invite_workspace_member() { #[tokio::test] async fn af_cloud_add_workspace_member_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; - let workspace_id_1 = test_1.get_current_workspace().await.id; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - let members = test_1.get_workspace_members(&workspace_id_1).await; + let members = test_1.get_workspace_members(&user_1.workspace_id).await; assert_eq!(members.len(), 1); assert_eq!(members[0].email, user_1.email); - test_1.add_workspace_member(&workspace_id_1, &test_2).await; + test_1 + .add_workspace_member(&user_1.workspace_id, &user_2.email) + .await; - let members = test_1.get_workspace_members(&workspace_id_1).await; + let members = test_1.get_workspace_members(&user_1.workspace_id).await; assert_eq!(members.len(), 2); assert_eq!(members[0].email, user_1.email); assert_eq!(members[1].email, user_2.email); @@ -57,43 +58,45 @@ async fn af_cloud_add_workspace_member_test() { #[tokio::test] async fn af_cloud_delete_workspace_member_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; let user_1 = test_1.af_cloud_sign_up().await; - let workspace_id_1 = test_1.get_current_workspace().await.id; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - test_1.add_workspace_member(&workspace_id_1, &test_2).await; - test_1 - .delete_workspace_member(&workspace_id_1, &user_2.email) + .add_workspace_member(&user_1.workspace_id, &user_2.email) .await; - let members = test_1.get_workspace_members(&workspace_id_1).await; + test_1 + .delete_workspace_member(&user_1.workspace_id, &user_2.email) + .await; + + let members = test_1.get_workspace_members(&user_1.workspace_id).await; assert_eq!(members.len(), 1); assert_eq!(members[0].email, user_1.email); } #[tokio::test] async fn af_cloud_leave_workspace_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test_1 = EventIntegrationTest::new().await; - test_1.af_cloud_sign_up().await; - let workspace_id_1 = test_1.get_current_workspace().await.id; + let user_1 = test_1.af_cloud_sign_up().await; let test_2 = EventIntegrationTest::new().await; let user_2 = test_2.af_cloud_sign_up().await; - test_1.add_workspace_member(&workspace_id_1, &test_2).await; + test_1 + .add_workspace_member(&user_1.workspace_id, &user_2.email) + .await; // test_2 should have 2 workspace let workspaces = get_synced_workspaces(&test_2, user_2.id).await; assert_eq!(workspaces.len(), 2); // user_2 leaves the workspace - test_2.leave_workspace(&workspace_id_1).await; + test_2.leave_workspace(&user_1.workspace_id).await; // user_2 should have 1 workspace let workspaces = get_synced_workspaces(&test_2, user_2.id).await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs index 0caa9a6227..9830656bb3 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/util.rs @@ -12,7 +12,7 @@ pub async fn get_synced_workspaces( test: &EventIntegrationTest, user_id: i64, ) -> Vec<UserWorkspacePB> { - let workspaces = test.get_all_workspaces().await.items; + let _workspaces = test.get_all_workspaces().await.items; let sub_id = user_id.to_string(); let rx = test .notification_sender @@ -20,9 +20,8 @@ pub async fn get_synced_workspaces( &sub_id, UserNotification::DidUpdateUserWorkspaces as i32, ); - if let Some(result) = receive_with_timeout(rx, Duration::from_secs(10)).await { - result.items - } else { - workspaces - } + receive_with_timeout(rx, Duration::from_secs(60)) + .await + .unwrap() + .items } diff --git a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs index d390c0558e..af743e7ced 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/af_cloud_test/workspace_test.rs @@ -1,28 +1,23 @@ -use crate::user::af_cloud_test::util::get_synced_workspaces; use collab::core::collab::DataSource::DocStateV1; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::Folder; -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; -use flowy_user::entities::AFRolePB; -use flowy_user_pub::cloud::UserCloudServiceProvider; -use flowy_user_pub::entities::AuthType; use std::time::Duration; -use tokio::task::LocalSet; use tokio::time::sleep; +use crate::user::af_cloud_test::util::get_synced_workspaces; + #[tokio::test] async fn af_cloud_workspace_delete() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let user_profile_pb = test.af_cloud_sign_up().await; let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 1); - let created_workspace = test - .create_workspace("my second workspace", AuthType::AppFlowyCloud) - .await; + let created_workspace = test.create_workspace("my second workspace").await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); @@ -37,7 +32,7 @@ async fn af_cloud_workspace_delete() { #[tokio::test] async fn af_cloud_workspace_change_name_and_icon() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let user_profile_pb = test.af_cloud_sign_up().await; let workspaces = test.get_all_workspaces().await; @@ -62,7 +57,7 @@ async fn af_cloud_workspace_change_name_and_icon() { #[tokio::test] async fn af_cloud_create_workspace_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let user_profile_pb = test.af_cloud_sign_up().await; @@ -70,9 +65,7 @@ async fn af_cloud_create_workspace_test() { let first_workspace_id = workspaces[0].workspace_id.as_str(); assert_eq!(workspaces.len(), 1); - let created_workspace = test - .create_workspace("my second workspace", AuthType::AppFlowyCloud) - .await; + let created_workspace = test.create_workspace("my second workspace").await; assert_eq!(created_workspace.name, "my second workspace"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; @@ -91,12 +84,7 @@ async fn af_cloud_create_workspace_test() { } { // after opening new workspace - test - .open_workspace( - &created_workspace.workspace_id, - created_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&created_workspace.workspace_id).await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); let views = test.folder_read_current_workspace_views().await; @@ -109,63 +97,42 @@ async fn af_cloud_create_workspace_test() { #[tokio::test] async fn af_cloud_open_workspace_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let _ = test.af_cloud_sign_up().await; - let default_document_name = "General"; + let default_document_name = "Getting started"; test.create_document("A").await; test.create_document("B").await; let first_workspace = test.get_current_workspace().await; - let first_workspace = test.get_user_workspace(&first_workspace.id).await; let views = test.get_all_workspace_views().await; - assert_eq!(views.len(), 4); + assert_eq!(views.len(), 3); assert_eq!(views[0].name, default_document_name); - assert_eq!(views[1].name, "Shared"); - assert_eq!(views[2].name, "A"); - assert_eq!(views[3].name, "B"); + assert_eq!(views[1].name, "A"); + assert_eq!(views[2].name, "B"); - let user_workspace = test - .create_workspace("second workspace", AuthType::AppFlowyCloud) - .await; - test - .open_workspace( - &user_workspace.workspace_id, - user_workspace.workspace_auth_type, - ) - .await; + let user_workspace = test.create_workspace("second workspace").await; + test.open_workspace(&user_workspace.workspace_id).await; let second_workspace = test.get_current_workspace().await; - let second_workspace = test.get_user_workspace(&second_workspace.id).await; test.create_document("C").await; test.create_document("D").await; let views = test.get_all_workspace_views().await; - assert_eq!(views.len(), 4); + assert_eq!(views.len(), 3); assert_eq!(views[0].name, default_document_name); - assert_eq!(views[1].name, "Shared"); - assert_eq!(views[2].name, "C"); - assert_eq!(views[3].name, "D"); + assert_eq!(views[1].name, "C"); + assert_eq!(views[2].name, "D"); // simulate open workspace and check if the views are correct - for i in 0..10 { + for i in 0..30 { if i % 2 == 0 { - test - .open_workspace( - &first_workspace.workspace_id, - first_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&first_workspace.id).await; sleep(Duration::from_millis(300)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) .await; } else { - test - .open_workspace( - &second_workspace.workspace_id, - second_workspace.workspace_auth_type, - ) - .await; + test.open_workspace(&second_workspace.id).await; sleep(Duration::from_millis(200)).await; test .create_document(&uuid::Uuid::new_v4().to_string()) @@ -173,39 +140,30 @@ async fn af_cloud_open_workspace_test() { } } - test - .open_workspace( - &first_workspace.workspace_id, - first_workspace.workspace_auth_type, - ) - .await; - let views_1 = test.get_all_workspace_views().await; - assert_eq!(views_1[0].name, default_document_name); - assert_eq!(views_1[1].name, "Shared"); - assert_eq!(views_1[2].name, "A"); - assert_eq!(views_1[3].name, "B"); + test.open_workspace(&first_workspace.id).await; + let views = test.get_all_workspace_views().await; + assert_eq!(views[0].name, default_document_name); + assert_eq!(views[1].name, "A"); + assert_eq!(views[2].name, "B"); - test - .open_workspace( - &second_workspace.workspace_id, - second_workspace.workspace_auth_type, - ) - .await; - let views_2 = test.get_all_workspace_views().await; - assert_eq!(views_2[0].name, default_document_name); - assert_eq!(views_2[1].name, "Shared"); - assert_eq!(views_2[2].name, "C"); - assert_eq!(views_2[3].name, "D"); + test.open_workspace(&second_workspace.id).await; + let views = test.get_all_workspace_views().await; + assert_eq!(views[0].name, default_document_name); + assert_eq!(views[1].name, "C"); + assert_eq!(views[2].name, "D"); } #[tokio::test] async fn af_cloud_different_open_same_workspace_test() { - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; // Set up the primary client and sign them up to the cloud. - let test_runner = EventIntegrationTest::new().await; - let owner_profile = test_runner.af_cloud_sign_up().await; - let shared_workspace_id = test_runner.get_current_workspace().await.id.clone(); + let client_1 = EventIntegrationTest::new().await; + let owner_profile = client_1.af_cloud_sign_up().await; + let shared_workspace_id = client_1.get_current_workspace().await.id.clone(); + + // Verify that the workspace ID from the profile matches the current session's workspace ID. + assert_eq!(shared_workspace_id, owner_profile.workspace_id); // Define the number of additional clients let num_clients = 5; @@ -218,13 +176,13 @@ async fn af_cloud_different_open_same_workspace_test() { let views = client.get_all_workspace_views().await; // only the getting started view should be present - assert_eq!(views.len(), 2); + assert_eq!(views.len(), 1); for view in views { client.delete_view(&view.id).await; } - test_runner - .add_workspace_member(&shared_workspace_id, &client) + client_1 + .add_workspace_member(&owner_profile.workspace_id, &client_profile.email) .await; clients.push((client, client_profile)); } @@ -237,21 +195,18 @@ async fn af_cloud_different_open_same_workspace_test() { // Simulate each client open different workspace 30 times let mut handles = vec![]; - let local_set = LocalSet::new(); for client in clients.clone() { let cloned_shared_workspace_id = shared_workspace_id.clone(); - let handle = local_set.spawn_local(async move { + let handle = tokio::spawn(async move { let (client, profile) = client; let all_workspaces = get_synced_workspaces(&client, profile.id).await; for i in 0..30 { let index = i % 2; let iter_workspace_id = &all_workspaces[index].workspace_id; - client - .open_workspace(iter_workspace_id, all_workspaces[index].workspace_auth_type) - .await; + client.open_workspace(iter_workspace_id).await; if iter_workspace_id == &cloned_shared_workspace_id { let views = client.get_all_workspace_views().await; - assert_eq!(views.len(), 2); + assert_eq!(views.len(), 1); sleep(Duration::from_millis(300)).await; } else { let views = client.get_all_workspace_views().await; @@ -261,16 +216,10 @@ async fn af_cloud_different_open_same_workspace_test() { }); handles.push(handle); } - let results = local_set - .run_until(futures::future::join_all(handles)) - .await; - - for result in results { - assert!(result.is_ok()); - } + futures::future::join_all(handles).await; // Retrieve and verify the collaborative document state for Client 1's workspace. - let doc_state = test_runner + let doc_state = client_1 .get_collab_doc_state(&shared_workspace_id, CollabType::Folder) .await .unwrap(); @@ -286,135 +235,8 @@ async fn af_cloud_different_open_same_workspace_test() { // Retrieve and verify the views associated with the workspace. let views = folder.get_views_belong_to(&shared_workspace_id); let folder_workspace_id = folder.get_workspace_id(); - assert_eq!(folder_workspace_id, Some(shared_workspace_id)); + assert_eq!(folder_workspace_id, shared_workspace_id); - assert_eq!(views.len(), 2, "only get: {:?}", views); // Expecting two views. - assert_eq!(views[0].name, "General"); -} - -#[tokio::test] -async fn af_cloud_create_local_workspace_test() { - // Setup: Initialize test environment with AppFlowyCloud - use_localhost_af_cloud().await; - let test = EventIntegrationTest::new().await; - let _ = test.af_cloud_sign_up().await; - - // Verify initial state: User should have one default workspace - let initial_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - initial_workspaces.len(), - 1, - "User should start with one default workspace" - ); - - // make sure the workspaces order is consistent - // tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; - - // Test: Create a local workspace - let local_workspace = test - .create_workspace("my local workspace", AuthType::Local) - .await; - - // Verify: Local workspace was created correctly - assert_eq!(local_workspace.name, "my local workspace"); - let updated_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - updated_workspaces.len(), - 2, - "Should now have two workspaces" - ); - dbg!(&updated_workspaces); - - // Find local workspace by name instead of using index - let found_local_workspace = updated_workspaces - .iter() - .find(|workspace| workspace.name == "my local workspace") - .expect("Local workspace should exist"); - assert_eq!(found_local_workspace.name, "my local workspace"); - - // Test: Open the local workspace - test - .open_workspace( - &local_workspace.workspace_id, - local_workspace.workspace_auth_type, - ) - .await; - - // Verify: Views in the local workspace - let views = test.get_all_views().await; - assert_eq!( - views.len(), - 2, - "Local workspace should have 2 default views" - ); - assert!( - views - .iter() - .any(|view| view.parent_view_id == local_workspace.workspace_id), - "Views should belong to the local workspace" - ); - - // Verify: Can access all views - for view in views { - test.get_view(&view.id).await; - } - - // Verify: Local workspace members - let members = test - .get_workspace_members(&local_workspace.workspace_id) - .await; - assert_eq!( - members.len(), - 1, - "Local workspace should have only one member" - ); - assert_eq!(members[0].role, AFRolePB::Owner, "User should be the owner"); - - // Test: Create a server workspace - let server_workspace = test - .create_workspace("my server workspace", AuthType::AppFlowyCloud) - .await; - - // Verify: Server workspace was created correctly - assert_eq!(server_workspace.name, "my server workspace"); - let final_workspaces = test.get_all_workspaces().await.items; - assert_eq!( - final_workspaces.len(), - 3, - "Should now have three workspaces" - ); - - dbg!(&final_workspaces); - - // Find workspaces by name instead of using indices - let found_local_workspace = final_workspaces - .iter() - .find(|workspace| workspace.name == "my local workspace") - .expect("Local workspace should exist"); - assert_eq!(found_local_workspace.name, "my local workspace"); - - let found_server_workspace = final_workspaces - .iter() - .find(|workspace| workspace.name == "my server workspace") - .expect("Server workspace should exist"); - assert_eq!(found_server_workspace.name, "my server workspace"); - - // Verify: Server-side only recognizes cloud workspaces (not local ones) - let user_profile = test.get_user_profile().await.unwrap(); - test - .server_provider - .set_server_auth_type(&AuthType::AppFlowyCloud, Some(user_profile.token.clone())) - .unwrap(); - test.server_provider.set_token(&user_profile.token).unwrap(); - - let user_service = test.server_provider.get_server().unwrap().user_service(); - let server_workspaces = user_service - .get_all_workspace(user_profile.id) - .await - .unwrap(); - assert_eq!( - server_workspaces.len(), - 2, - "Server should only see 2 workspaces (the default and server workspace, not the local one)" - ); + assert_eq!(views.len(), 1, "only get: {:?}", views); // Expecting two views. + assert_eq!(views[0].name, "Getting started"); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs index 138f6f0258..3cd3733837 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/auth_test.rs @@ -1,6 +1,6 @@ use event_integration_test::user_event::{login_password, unique_email}; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, SignInPayloadPB, SignUpPayloadPB}; +use flowy_user::entities::{AuthenticatorPB, SignInPayloadPB, SignUpPayloadPB}; use flowy_user::errors::ErrorCode; use flowy_user::event_map::UserEvent::*; @@ -14,7 +14,7 @@ async fn sign_up_with_invalid_email() { email: email.to_string(), name: valid_name(), password: login_password(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -31,6 +31,29 @@ async fn sign_up_with_invalid_email() { ); } } +#[tokio::test] +async fn sign_up_with_long_password() { + let sdk = EventIntegrationTest::new().await; + let request = SignUpPayloadPB { + email: unique_email(), + name: valid_name(), + password: "1234".repeat(100).as_str().to_string(), + auth_type: AuthenticatorPB::Local, + device_id: "".to_string(), + }; + + assert_eq!( + EventBuilder::new(sdk) + .event(SignUp) + .payload(request) + .async_send() + .await + .error() + .unwrap() + .code, + ErrorCode::PasswordTooLong + ); +} #[tokio::test] async fn sign_in_with_invalid_email() { @@ -40,7 +63,7 @@ async fn sign_in_with_invalid_email() { email: email.to_string(), password: login_password(), name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; @@ -67,7 +90,7 @@ async fn sign_in_with_invalid_password() { email: unique_email(), password, name: "".to_string(), - auth_type: AuthTypePB::Local, + auth_type: AuthenticatorPB::Local, device_id: "".to_string(), }; diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/import_af_data_local_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/import_af_data_local_test.rs index 4f1ea45fee..c8c4d2def1 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/import_af_data_local_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/import_af_data_local_test.rs @@ -1,5 +1,5 @@ use crate::util::unzip; -use event_integration_test::user_event::use_localhost_af_cloud; +use event_integration_test::user_event::user_localhost_af_cloud; use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use std::time::Duration; @@ -7,10 +7,11 @@ use std::time::Duration; #[tokio::test] async fn import_appflowy_data_folder_into_new_view_test() { let import_container_name = "040_local".to_string(); - let user_db_path = unzip("./tests/asset", &import_container_name).unwrap(); - let imported_af_data_path = unzip("./tests/asset", &import_container_name).unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", &import_container_name).unwrap(); + let (imported_af_folder_cleaner, imported_af_data_path) = + unzip("./tests/asset", &import_container_name).unwrap(); - use_localhost_af_cloud().await; + user_localhost_af_cloud().await; let test = EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) .await; @@ -33,20 +34,16 @@ async fn import_appflowy_data_folder_into_new_view_test() { // after import, the structure is: // workspace: - // Document1 - // Document2 - // Grid1 - // Grid2 - // 040_local + // view: Getting Started + // view: 040_local + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 let views = test.get_all_workspace_views().await; - assert_eq!(views.len(), 1); - assert_eq!(views[0].name, "Document1"); - assert_eq!(views[0].child_views.len(), 2); + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, import_container_name); - for (index, view) in views[0].child_views.iter().enumerate() { - let view = test.get_view(&view.id).await; - if index == 1 { - assert_eq!(view.name, import_container_name); - } - } + drop(cleaner); + drop(imported_af_folder_cleaner); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs index 438b120483..798054dccf 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/local_test/user_profile_test.rs @@ -1,6 +1,6 @@ use crate::user::local_test::helper::*; use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; -use flowy_user::entities::{AuthTypePB, UpdateUserProfilePayloadPB, UserProfilePB}; +use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use nanoid::nanoid; #[tokio::test] @@ -24,7 +24,10 @@ async fn anon_user_profile_get() { .await .parse::<UserProfilePB>(); assert_eq!(user_profile.id, user.id); - assert_eq!(user_profile.user_auth_type, AuthTypePB::Local); + assert_eq!(user_profile.openai_key, user.openai_key); + assert_eq!(user_profile.stability_ai_key, user.stability_ai_key); + assert_eq!(user_profile.workspace_id, user.workspace_id); + assert_eq!(user_profile.authenticator, AuthenticatorPB::Local); } #[tokio::test] @@ -48,6 +51,31 @@ async fn user_update_with_name() { assert_eq!(user_profile.name, new_name,); } +#[tokio::test] +async fn user_update_with_ai_key() { + let sdk = EventIntegrationTest::new().await; + let user = sdk.init_anon_user().await; + let openai_key = "openai_key".to_owned(); + let stability_ai_key = "stability_ai_key".to_owned(); + let request = UpdateUserProfilePayloadPB::new(user.id) + .openai_key(&openai_key) + .stability_ai_key(&stability_ai_key); + let _ = EventBuilder::new(sdk.clone()) + .event(UpdateUserProfile) + .payload(request) + .async_send() + .await; + + let user_profile = EventBuilder::new(sdk.clone()) + .event(GetUserProfile) + .async_send() + .await + .parse::<UserProfilePB>(); + + assert_eq!(user_profile.openai_key, openai_key,); + assert_eq!(user_profile.stability_ai_key, stability_ai_key,); +} + #[tokio::test] async fn anon_user_update_with_email() { let sdk = EventIntegrationTest::new().await; diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/collab_db_restore.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/collab_db_restore.rs new file mode 100644 index 0000000000..45f33c3cd9 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/collab_db_restore.rs @@ -0,0 +1,20 @@ +use event_integration_test::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; + +use crate::util::unzip; + +#[tokio::test] +async fn collab_db_restore_test() { + let (cleaner, user_db_path) = unzip( + "./tests/user/migration_test/history_user_db", + "038_collab_db_corrupt_restore", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 1); + + drop(cleaner); +} diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/document_test.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/document_test.rs index f4ba5ec831..6b16407dec 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/document_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/document_test.rs @@ -6,7 +6,7 @@ use crate::util::unzip; #[tokio::test] async fn migrate_historical_empty_document_test() { - let user_db_path = unzip( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "historical_empty_document", ) @@ -23,4 +23,6 @@ async fn migrate_historical_empty_document_test() { assert_eq!(data.blocks.len(), 2); assert!(!data.meta.children_map.is_empty()); } + + drop(cleaner); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/mod.rs index 940f03e64f..42748e823a 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/mod.rs @@ -1,2 +1,4 @@ mod document_test; mod version_test; + +mod collab_db_restore; diff --git a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs index 61833429aa..3e925ba0ec 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/migration_test/version_test.rs @@ -1,13 +1,49 @@ use event_integration_test::EventIntegrationTest; use flowy_core::DEFAULT_NAME; +use flowy_folder::entities::ViewLayoutPB; use std::time::Duration; use crate::util::unzip; +#[tokio::test] +async fn migrate_020_historical_empty_document_test() { + let (cleaner, user_db_path) = unzip( + "./tests/user/migration_test/history_user_db", + "020_historical_user_data", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + + let mut views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 1); + + // Check the parent view + let parent_view = views.pop().unwrap(); + assert_eq!(parent_view.layout, ViewLayoutPB::Document); + let data = test.open_document(parent_view.id.clone()).await.data; + assert!(!data.page_id.is_empty()); + assert_eq!(data.blocks.len(), 2); + assert!(!data.meta.children_map.is_empty()); + + // Check the child views of the parent view + let child_views = test.get_view(&parent_view.id).await.child_views; + assert_eq!(child_views.len(), 4); + assert_eq!(child_views[0].layout, ViewLayoutPB::Document); + assert_eq!(child_views[1].layout, ViewLayoutPB::Grid); + assert_eq!(child_views[2].layout, ViewLayoutPB::Calendar); + assert_eq!(child_views[3].layout, ViewLayoutPB::Board); + + let database = test.get_database(&child_views[1].id).await; + assert_eq!(database.fields.len(), 8); + assert_eq!(database.rows.len(), 3); + drop(cleaner); +} + #[tokio::test] async fn migrate_036_fav_v1_workspace_array_test() { // Used to test migration: FavoriteV1AndWorkspaceArrayMigration - let user_db_path = unzip( + let (cleaner, user_db_path) = unzip( "./tests/user/migration_test/history_user_db", "036_fav_v1_workspace_array", ) @@ -23,12 +59,13 @@ async fn migrate_036_fav_v1_workspace_array_test() { let views = test.get_view(&views[1].id).await; assert_eq!(views.child_views.len(), 3); assert!(views.child_views[2].is_favorite); + drop(cleaner); } #[tokio::test] async fn migrate_038_trash_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let user_db_path = unzip("./tests/asset", "038_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_local").unwrap(); // Getting started // Document1 // Document2(deleted) @@ -58,12 +95,14 @@ async fn migrate_038_trash_test() { assert_eq!(trash_items[0].name, "Document3"); assert_eq!(trash_items[1].name, "Document2"); assert_eq!(trash_items[2].name, "Document4"); + + drop(cleaner); } #[tokio::test] async fn migrate_038_trash_test2() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let user_db_path = unzip("./tests/asset", "038_document_with_grid").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_document_with_grid").unwrap(); // Getting started // document // grid @@ -84,19 +123,18 @@ async fn migrate_038_trash_test2() { let views = test.get_view(&views[0].id).await.child_views; assert_eq!(views[0].name, "board"); + + drop(cleaner); } #[tokio::test] async fn collab_db_backup_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let user_db_path = unzip("./tests/asset", "038_local").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "038_local").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; let uid = test.get_user_profile().await.unwrap().id; - // sleep a bit to make sure the backup is generated - - tokio::time::sleep(Duration::from_secs(10)).await; let backups = test.user_manager.get_collab_backup_list(uid); assert_eq!(backups.len(), 1); @@ -104,12 +142,13 @@ async fn collab_db_backup_test() { backups[0], format!("collab_db_{}", chrono::Local::now().format("%Y%m%d")) ); + drop(cleaner); } #[tokio::test] async fn delete_outdated_collab_db_backup_test() { // Used to test migration: WorkspaceTrashMapToSectionMigration - let user_db_path = unzip("./tests/asset", "040_collab_backups").unwrap(); + let (cleaner, user_db_path) = unzip("./tests/asset", "040_collab_backups").unwrap(); let test = EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; @@ -137,4 +176,5 @@ async fn delete_outdated_collab_db_backup_test() { backups[9], format!("collab_db_{}", chrono::Local::now().format("%Y%m%d")) ); + drop(cleaner); } diff --git a/frontend/rust-lib/event-integration-test/tests/user/mod.rs b/frontend/rust-lib/event-integration-test/tests/user/mod.rs index ad053eb0f9..ab778a29c1 100644 --- a/frontend/rust-lib/event-integration-test/tests/user/mod.rs +++ b/frontend/rust-lib/event-integration-test/tests/user/mod.rs @@ -2,3 +2,5 @@ mod local_test; mod migration_test; mod af_cloud_test; +// #[cfg(feature = "supabase_cloud_test")] +// mod supabase_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs new file mode 100644 index 0000000000..1b6d5f9cc6 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/auth_test.rs @@ -0,0 +1,502 @@ +use std::collections::HashMap; + +use assert_json_diff::assert_json_eq; +use collab_database::rows::database_row_document_id_from_row_id; +use collab_document::blocks::DocumentData; +use collab_entity::CollabType; +use collab_folder::FolderData; +use nanoid::nanoid; +use serde_json::json; + +use event_integration_test::document::document_event::DocumentEventTest; +use event_integration_test::event_builder::EventBuilder; +use event_integration_test::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use flowy_encrypt::decrypt_text; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; +use flowy_user::entities::{ + AuthenticatorPB, OauthSignInPB, UpdateUserProfilePayloadPB, UserProfilePB, +}; +use flowy_user::errors::ErrorCode; +use flowy_user::event_map::UserEvent::*; + +use crate::util::*; + +#[tokio::test] +async fn third_party_sign_up_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert( + USER_EMAIL.to_string(), + format!("{}@appflowy.io", nanoid!(6)), + ); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, + }; + + let response = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(payload) + .async_send() + .await + .parse::<UserProfilePB>(); + dbg!(&response); + } +} + +#[tokio::test] +async fn third_party_sign_up_with_encrypt_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + test.supabase_party_sign_up().await; + let user_profile = test.get_user_profile().await.unwrap(); + assert!(user_profile.encryption_sign.is_empty()); + + let secret = test.enable_encryption().await; + let user_profile = test.get_user_profile().await.unwrap(); + assert!(!user_profile.encryption_sign.is_empty()); + + let decryption_sign = decrypt_text(user_profile.encryption_sign, &secret).unwrap(); + assert_eq!(decryption_sign, user_profile.id.to_string()); + } +} + +#[tokio::test] +async fn third_party_sign_up_with_duplicated_uuid() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let email = format!("{}@appflowy.io", nanoid!(6)); + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert(USER_EMAIL.to_string(), email.clone()); + map.insert(USER_DEVICE_ID.to_string(), uuid::Uuid::new_v4().to_string()); + + let response_1 = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(OauthSignInPB { + map: map.clone(), + authenticator: AuthenticatorPB::Supabase, + }) + .async_send() + .await + .parse::<UserProfilePB>(); + dbg!(&response_1); + + let response_2 = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(OauthSignInPB { + map: map.clone(), + authenticator: AuthenticatorPB::Supabase, + }) + .async_send() + .await + .parse::<UserProfilePB>(); + assert_eq!(response_1, response_2); + }; +} + +#[tokio::test] +async fn third_party_sign_up_with_duplicated_email() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let email = format!("{}@appflowy.io", nanoid!(6)); + test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await + .unwrap(); + let error = test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await + .err() + .unwrap(); + assert_eq!(error.code, ErrorCode::Conflict); + }; +} + +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new_anon().await; + let old_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + let old_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + let uuid = uuid::Uuid::new_v4().to_string(); + test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + let new_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + + assert_eq!(old_views.len(), new_views.len()); + assert_eq!(old_workspace.name, new_workspace.name); + assert_eq!(old_workspace.views.len(), new_workspace.views.len()); + for (index, view) in old_views.iter().enumerate() { + assert_eq!(view.name, new_views[index].name); + assert_eq!(view.layout, new_views[index].layout); + assert_eq!(view.create_time, new_views[index].create_time); + } + } +} + +#[tokio::test] +async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new_anon().await; + let uuid = uuid::Uuid::new_v4().to_string(); + + let email = format!("{}@appflowy.io", nanoid!(6)); + // The workspace of the guest will be migrated to the new user with given uuid + let _user_profile = test + .supabase_sign_up_with_uuid(&uuid, Some(email.clone())) + .await + .unwrap(); + let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let old_cloud_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + assert_eq!(old_cloud_views.len(), 1); + assert_eq!(old_cloud_views.first().unwrap().child_views.len(), 1); + + // sign out and then sign in as a guest + test.sign_out().await; + + let _sign_up_context = test.sign_up_as_anon().await; + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + test + .create_view(&new_workspace.id, "new workspace child view".to_string()) + .await; + let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + assert_eq!(new_workspace.views.len(), 2); + + // upload to cloud user with given uuid. This time the workspace of the guest will not be merged + // because the cloud user already has a workspace + test + .supabase_sign_up_with_uuid(&uuid, Some(email)) + .await + .unwrap(); + let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); + let new_cloud_views = test + .folder_manager + .get_current_workspace_public_views() + .await + .unwrap(); + assert_eq!(new_cloud_workspace, old_cloud_workspace); + assert_eq!(new_cloud_views, old_cloud_views); + } +} + +#[tokio::test] +async fn get_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let uuid = uuid::Uuid::new_v4().to_string(); + test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + + let result = test.get_user_profile().await; + assert!(result.is_ok()); + } +} + +#[tokio::test] +async fn update_user_profile_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let uuid = uuid::Uuid::new_v4().to_string(); + let profile = test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); + test + .update_user_profile(UpdateUserProfilePayloadPB::new(profile.id).name("lucas")) + .await; + + let new_profile = test.get_user_profile().await.unwrap(); + assert_eq!(new_profile.name, "lucas") + } +} + +#[tokio::test] +async fn update_user_profile_with_existing_email_test() { + if let Some(test) = FlowySupabaseTest::new().await { + let email = format!("{}@appflowy.io", nanoid!(6)); + let _ = test + .supabase_sign_up_with_uuid(&uuid::Uuid::new_v4().to_string(), Some(email.clone())) + .await; + + let profile = test + .supabase_sign_up_with_uuid( + &uuid::Uuid::new_v4().to_string(), + Some(format!("{}@appflowy.io", nanoid!(6))), + ) + .await + .unwrap(); + let error = test + .update_user_profile( + UpdateUserProfilePayloadPB::new(profile.id) + .name("lucas") + .email(&email), + ) + .await + .unwrap(); + assert_eq!(error.code, ErrorCode::Conflict); + } +} + +#[tokio::test] +async fn migrate_anon_document_on_cloud_signup() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let user_profile = test.sign_up_as_anon().await.user_profile; + + let view = test + .create_view(&user_profile.workspace_id, "My first view".to_string()) + .await; + let document_event = DocumentEventTest::new_with_core(test.clone()); + let block_id = document_event + .insert_index(&view.id, "hello world", 1, None) + .await; + + let _ = test.supabase_party_sign_up().await; + + let workspace_id = test.user_manager.workspace_id().unwrap(); + // After sign up, the documents should be migrated to the cloud + // So, we can get the document data from the cloud + let data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&view.id, &workspace_id) + .await + .unwrap() + .unwrap(); + let block = data.blocks.get(&block_id).unwrap(); + assert_json_eq!( + block.data, + json!({ + "delta": [ + { + "insert": "hello world" + } + ] + }) + ); + } +} + +#[tokio::test] +async fn migrate_anon_data_on_cloud_signup() { + if get_supabase_config().is_some() { + let (cleaner, user_db_path) = unzip( + "./tests/user/supabase_test/history_user_db", + "workspace_sync", + ) + .unwrap(); + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await; + let user_profile = test.supabase_party_sign_up().await; + + // Get the folder data from remote + let folder_data: FolderData = test + .folder_manager + .get_cloud_service() + .get_folder_data(&user_profile.workspace_id, &user_profile.id) + .await + .unwrap() + .unwrap(); + + let expected_folder_data = expected_workspace_sync_folder_data(); + assert_eq!(folder_data.views.len(), expected_folder_data.views.len()); + + // After migration, the ids of the folder_data should be different from the expected_folder_data + for i in 0..folder_data.views.len() { + let left_view = &folder_data.views[i]; + let right_view = &expected_folder_data.views[i]; + assert_ne!(left_view.id, right_view.id); + assert_ne!(left_view.parent_view_id, right_view.parent_view_id); + assert_eq!(left_view.name, right_view.name); + } + + assert_ne!(folder_data.workspace.id, expected_folder_data.workspace.id); + assert_ne!(folder_data.current_view, expected_folder_data.current_view); + + let database_views = folder_data + .views + .iter() + .filter(|view| view.layout.is_database()) + .collect::<Vec<_>>(); + + // Try to load the database from the cloud. + for (i, database_view) in database_views.iter().enumerate() { + let cloud_service = test.database_manager.get_cloud_service(); + let database_id = test + .database_manager + .get_database_id_with_view_id(&database_view.id) + .await + .unwrap(); + let editor = test + .database_manager + .get_database(&database_id) + .await + .unwrap(); + + // The database view setting should be loaded by the view id + let _ = editor + .get_database_view_setting(&database_view.id) + .await + .unwrap(); + + let rows = editor.get_rows(&database_view.id).await.unwrap(); + assert_eq!(rows.len(), 3); + + let workspace_id = test.user_manager.workspace_id().unwrap(); + if i == 0 { + let first_row = rows.first().unwrap().as_ref(); + let icon_url = first_row.meta.icon_url.clone().unwrap(); + assert_eq!(icon_url, "😄"); + + let document_id = database_row_document_id_from_row_id(&first_row.row.id); + let document_data: DocumentData = test + .document_manager + .get_cloud_service() + .get_document_data(&document_id, &workspace_id) + .await + .unwrap() + .unwrap(); + + let editor = test + .document_manager + .get_document(&document_id) + .await + .unwrap(); + let expected_document_data = editor.lock().get_document_data().unwrap(); + + // let expected_document_data = test + // .document_manager + // .get_document_data(&document_id) + // .await + // .unwrap(); + assert_eq!(document_data, expected_document_data); + let json = json!(document_data); + assert_eq!( + json["blocks"]["LPMpo0Qaab"]["data"]["delta"][0]["insert"], + json!("Row document") + ); + } + assert!(cloud_service + .get_database_object_doc_state(&database_id, CollabType::Database, &workspace_id) + .await + .is_ok()); + } + + drop(cleaner); + } +} + +fn expected_workspace_sync_folder_data() -> FolderData { + serde_json::from_value::<FolderData>(json!({ + "current_view": "e0811131-9928-4541-a174-20b7553d9e4c", + "current_workspace_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "views": [ + { + "children": { + "items": [ + { + "id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "id": "53333949-c262-447b-8597-107589697059" + } + ] + }, + "created_at": 1693147093, + "desc": "", + "icon": null, + "id": "e203afb3-de5d-458a-8380-33cd788a756e", + "is_favorite": false, + "layout": 0, + "name": "⭐️ Getting started", + "parent_view_id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3" + }, + { + "children": { + "items": [ + { + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b" + }, + { + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c" + } + ] + }, + "created_at": 1693147096, + "desc": "", + "icon": null, + "id": "e0811131-9928-4541-a174-20b7553d9e4c", + "is_favorite": false, + "layout": 1, + "name": "database", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147124, + "desc": "", + "icon": null, + "id": "11c697ba-5ed1-41c0-adfc-576db28ad27b", + "is_favorite": false, + "layout": 3, + "name": "calendar", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147125, + "desc": "", + "icon": null, + "id": "4a5c25e2-a734-440c-973b-4c0e7ab0039c", + "is_favorite": false, + "layout": 2, + "name": "board", + "parent_view_id": "e0811131-9928-4541-a174-20b7553d9e4c" + }, + { + "children": { + "items": [] + }, + "created_at": 1693147133, + "desc": "", + "icon": null, + "id": "53333949-c262-447b-8597-107589697059", + "is_favorite": false, + "layout": 0, + "name": "document", + "parent_view_id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ], + "workspaces": [ + { + "child_views": { + "items": [ + { + "id": "e203afb3-de5d-458a-8380-33cd788a756e" + } + ] + }, + "created_at": 1693147093, + "id": "8df7f755-fa5d-480e-9f8e-48ea0fed12b3", + "name": "Workspace" + } + ] + })) + .unwrap() +} diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md new file mode 100644 index 0000000000..426255b00d --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/README.md @@ -0,0 +1,4 @@ + +## Don't modify the zip files in this folder + +The zip files in this folder are used for integration tests. If the tests fail, it means users upgrading to this version of AppFlowy will encounter issues \ No newline at end of file diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip new file mode 100644 index 0000000000..6fd5ca0871 Binary files /dev/null and b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/history_user_db/workspace_sync.zip differ diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs new file mode 100644 index 0000000000..b31fdaa002 --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/mod.rs @@ -0,0 +1,2 @@ +mod auth_test; +mod workspace_test; diff --git a/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs new file mode 100644 index 0000000000..2ccbc9438f --- /dev/null +++ b/frontend/rust-lib/event-integration-test/tests/user/supabase_test/workspace_test.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use event_integration_test::{event_builder::EventBuilder, EventIntegrationTest}; +use flowy_folder::entities::WorkspaceSettingPB; +use flowy_folder::event_map::FolderEvent::GetCurrentWorkspaceSetting; +use flowy_server::supabase::define::{USER_EMAIL, USER_UUID}; +use flowy_user::entities::{AuthenticatorPB, OauthSignInPB, UserProfilePB}; +use flowy_user::event_map::UserEvent::*; + +use crate::util::*; + +#[tokio::test] +async fn initial_workspace_test() { + if get_supabase_config().is_some() { + let test = EventIntegrationTest::new().await; + let mut map = HashMap::new(); + map.insert(USER_UUID.to_string(), uuid::Uuid::new_v4().to_string()); + map.insert( + USER_EMAIL.to_string(), + format!("{}@gmail.com", uuid::Uuid::new_v4()), + ); + let payload = OauthSignInPB { + map, + authenticator: AuthenticatorPB::Supabase, + }; + + let _ = EventBuilder::new(test.clone()) + .event(OauthSignIn) + .payload(payload) + .async_send() + .await + .parse::<UserProfilePB>(); + + let workspace_settings = EventBuilder::new(test.clone()) + .event(GetCurrentWorkspaceSetting) + .async_send() + .await + .parse::<WorkspaceSettingPB>(); + + assert!(workspace_settings.latest_view.is_some()); + dbg!(&workspace_settings); + } +} diff --git a/frontend/rust-lib/event-integration-test/tests/util.rs b/frontend/rust-lib/event-integration-test/tests/util.rs index 2e2c2d578f..0a63ccaac7 100644 --- a/frontend/rust-lib/event-integration-test/tests/util.rs +++ b/frontend/rust-lib/event-integration-test/tests/util.rs @@ -1,12 +1,17 @@ -use nanoid::nanoid; -use std::env::temp_dir; use std::fs::{create_dir_all, File, OpenOptions}; use std::io::copy; use std::ops::Deref; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::Duration; use std::{fs, io}; + +use anyhow::Error; +use collab_folder::FolderData; +use collab_plugins::cloud_storage::RemoteCollabStorage; +use nanoid::nanoid; use tokio::sync::mpsc::Receiver; + use tokio::time::timeout; use uuid::Uuid; use walkdir::WalkDir; @@ -14,12 +19,24 @@ use zip::write::FileOptions; use zip::{CompressionMethod, ZipArchive, ZipWriter}; use event_integration_test::event_builder::EventBuilder; - +use event_integration_test::Cleaner; use event_integration_test::EventIntegrationTest; -use flowy_folder::entities::{ImportItemPayloadPB, ImportPayloadPB, ImportTypePB, ViewLayoutPB}; -use flowy_user::entities::UpdateUserProfilePayloadPB; +use flowy_database_pub::cloud::DatabaseCloudService; +use flowy_folder_pub::cloud::{FolderCloudService, FolderSnapshot}; +use flowy_server::supabase::api::*; +use flowy_server::{AppFlowyEncryption, EncryptionImpl}; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB}; use flowy_user::errors::FlowyError; + use flowy_user::event_map::UserEvent::*; +use flowy_user_pub::cloud::UserCloudService; +use flowy_user_pub::entities::Authenticator; + +pub fn get_supabase_config() -> Option<SupabaseConfiguration> { + dotenv::from_path(".env.ci").ok()?; + SupabaseConfiguration::from_env().ok() +} pub struct FlowySupabaseTest { event_test: EventIntegrationTest, @@ -27,7 +44,13 @@ pub struct FlowySupabaseTest { impl FlowySupabaseTest { pub async fn new() -> Option<Self> { + let _ = get_supabase_config()?; let event_test = EventIntegrationTest::new().await; + event_test.set_auth_type(AuthenticatorPB::Supabase); + event_test + .server_provider + .set_authenticator(Authenticator::Supabase); + Some(Self { event_test }) } @@ -56,6 +79,93 @@ pub async fn receive_with_timeout<T>(mut receiver: Receiver<T>, duration: Durati timeout(duration, receiver.recv()).await.ok()? } +pub fn get_supabase_ci_config() -> Option<SupabaseConfiguration> { + dotenv::from_filename("./.env.ci").ok()?; + SupabaseConfiguration::from_env().ok() +} + +#[allow(dead_code)] +pub fn get_supabase_dev_config() -> Option<SupabaseConfiguration> { + dotenv::from_filename("./.env.dev").ok()?; + SupabaseConfiguration::from_env().ok() +} + +pub fn collab_service() -> Arc<dyn RemoteCollabStorage> { + let (server, encryption_impl) = appflowy_server(None); + Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )) +} + +pub fn database_service() -> Arc<dyn DatabaseCloudService> { + let (server, _encryption_impl) = appflowy_server(None); + Arc::new(SupabaseDatabaseServiceImpl::new(server)) +} + +pub fn user_auth_service() -> Arc<dyn UserCloudService> { + let (server, _encryption_impl) = appflowy_server(None); + Arc::new(SupabaseUserServiceImpl::new(server, vec![], None)) +} + +pub fn folder_service() -> Arc<dyn FolderCloudService> { + let (server, _encryption_impl) = appflowy_server(None); + Arc::new(SupabaseFolderServiceImpl::new(server)) +} + +#[allow(dead_code)] +pub fn encryption_folder_service( + secret: Option<String>, +) -> (Arc<dyn FolderCloudService>, Arc<dyn AppFlowyEncryption>) { + let (server, encryption_impl) = appflowy_server(secret); + let service = Arc::new(SupabaseFolderServiceImpl::new(server)); + (service, encryption_impl) +} + +pub fn encryption_collab_service( + secret: Option<String>, +) -> (Arc<dyn RemoteCollabStorage>, Arc<dyn AppFlowyEncryption>) { + let (server, encryption_impl) = appflowy_server(secret); + let service = Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )); + (service, encryption_impl) +} + +pub async fn get_folder_data_from_server( + uid: &i64, + folder_id: &str, + encryption_secret: Option<String>, +) -> Result<Option<FolderData>, Error> { + let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); + cloud_service.get_folder_data(folder_id, uid).await +} + +pub async fn get_folder_snapshots( + folder_id: &str, + encryption_secret: Option<String>, +) -> Vec<FolderSnapshot> { + let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); + cloud_service + .get_folder_snapshots(folder_id, 10) + .await + .unwrap() +} + +pub fn appflowy_server( + encryption_secret: Option<String>, +) -> (SupabaseServerServiceImpl, Arc<dyn AppFlowyEncryption>) { + let config = SupabaseConfiguration::from_env().unwrap(); + let encryption_impl: Arc<dyn AppFlowyEncryption> = + Arc::new(EncryptionImpl::new(encryption_secret)); + let encryption = Arc::downgrade(&encryption_impl); + let server = Arc::new(RESTfulPostgresServer::new(config, encryption)); + (SupabaseServerServiceImpl::new(server), encryption_impl) +} + /// zip the asset to the destination /// Zips the specified directory into a zip file. /// @@ -80,7 +190,7 @@ pub fn zip(src_dir: PathBuf, output_file_path: PathBuf) -> io::Result<()> { .truncate(true) .open(&output_file_path)?; - let options = FileOptions::<()>::default().compression_method(CompressionMethod::Deflated); + let options = FileOptions::default().compression_method(CompressionMethod::Deflated); let mut zip = ZipWriter::new(file); @@ -123,23 +233,26 @@ pub fn zip(src_dir: PathBuf, output_file_path: PathBuf) -> io::Result<()> { zip.finish()?; Ok(()) } -pub fn unzip_test_asset(folder_name: &str) -> io::Result<PathBuf> { +pub fn unzip_test_asset(folder_name: &str) -> io::Result<(Cleaner, PathBuf)> { unzip("./tests/asset", folder_name) } -pub fn unzip(test_asset_dir: &str, folder_name: &str) -> io::Result<PathBuf> { +pub fn unzip(root: &str, folder_name: &str) -> io::Result<(Cleaner, PathBuf)> { // Open the zip file - let zip_file_path = format!("{}/{}.zip", test_asset_dir, folder_name); + let zip_file_path = format!("{}/{}.zip", root, folder_name); let reader = File::open(zip_file_path)?; - // let output_folder_path = format!("{}/unit_test_{}", test_asset_dir, nanoid!(6)); - let output_folder_path = temp_dir().join(nanoid!(6)).to_str().unwrap().to_string(); + let output_folder_path = format!("{}/unit_test_{}", root, nanoid!(6)); // Create a ZipArchive from the file let mut archive = ZipArchive::new(reader)?; + + // Iterate through each file in the zip for i in 0..archive.len() { let mut file = archive.by_index(i)?; let output_path = Path::new(&output_folder_path).join(file.mangled_name()); + if file.name().ends_with('/') { + // Create directory create_dir_all(&output_path)?; } else { // Write file @@ -153,23 +266,12 @@ pub fn unzip(test_asset_dir: &str, folder_name: &str) -> io::Result<PathBuf> { } } let path = format!("{}/{}", output_folder_path, folder_name); - Ok(PathBuf::from(path)) + Ok(( + Cleaner::new(PathBuf::from(output_folder_path)), + PathBuf::from(path), + )) } pub fn generate_test_email() -> String { format!("{}@test.com", Uuid::new_v4()) } - -pub fn gen_csv_import_data(file_name: &str, workspace_id: &str) -> ImportPayloadPB { - let file_path = format!("./tests/asset/{}", file_name); - ImportPayloadPB { - parent_view_id: workspace_id.to_string(), - items: vec![ImportItemPayloadPB { - name: file_name.to_string(), - data: None, - file_path: Some(file_path), - view_layout: ViewLayoutPB::Grid, - import_type: ImportTypePB::CSV, - }], - } -} diff --git a/frontend/rust-lib/flowy-ai-pub/Cargo.toml b/frontend/rust-lib/flowy-ai-pub/Cargo.toml deleted file mode 100644 index 93ea79bcab..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "flowy-ai-pub" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lib-infra = { workspace = true } -flowy-error = { workspace = true } -client-api = { workspace = true } -futures.workspace = true -serde_json.workspace = true -serde.workspace = true -uuid.workspace = true -flowy-sqlite = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs b/frontend/rust-lib/flowy-ai-pub/src/cloud.rs deleted file mode 100644 index 2292e0f332..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/cloud.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::cloud::ai_dto::AvailableModel; -pub use client_api::entity::ai_dto::{ - AppFlowyOfflineAI, CompleteTextParams, CompletionMessage, CompletionMetadata, CompletionType, - CreateChatContext, CustomPrompt, LLMModel, LocalAIConfig, ModelInfo, ModelList, OutputContent, - OutputLayout, RelatedQuestion, RepeatedRelatedQuestion, ResponseFormat, StringOrMessage, -}; -pub use client_api::entity::billing_dto::SubscriptionPlan; -pub use client_api::entity::chat_dto::{ - ChatMessage, ChatMessageType, ChatRAGData, ChatSettings, ContextLoader, MessageCursor, - RepeatedChatMessage, UpdateChatParams, -}; -pub use client_api::entity::QuestionStreamValue; -pub use client_api::entity::*; -pub use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; -use flowy_error::FlowyError; -use futures::stream::BoxStream; -use lib_infra::async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use uuid::Uuid; - -pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>; -pub type StreamAnswer = BoxStream<'static, Result<QuestionStreamValue, FlowyError>>; -pub type StreamComplete = BoxStream<'static, Result<CompletionStreamValue, FlowyError>>; - -#[derive(Debug, Eq, PartialEq, Serialize, Deserialize, Clone)] -pub struct AIModel { - pub name: String, - pub is_local: bool, - #[serde(default)] - pub desc: String, -} - -impl From<AvailableModel> for AIModel { - fn from(value: AvailableModel) -> Self { - let desc = value - .metadata - .as_ref() - .and_then(|v| v.get("desc").map(|v| v.as_str().unwrap_or(""))) - .unwrap_or(""); - Self { - name: value.name, - is_local: false, - desc: desc.to_string(), - } - } -} - -impl AIModel { - pub fn server(name: String, desc: String) -> Self { - Self { - name, - is_local: false, - desc, - } - } - - pub fn local(name: String, desc: String) -> Self { - Self { - name, - is_local: true, - desc, - } - } -} - -pub const DEFAULT_AI_MODEL_NAME: &str = "Auto"; -impl Default for AIModel { - fn default() -> Self { - Self { - name: DEFAULT_AI_MODEL_NAME.to_string(), - is_local: false, - desc: "".to_string(), - } - } -} - -#[async_trait] -pub trait ChatCloudService: Send + Sync + 'static { - async fn create_chat( - &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - name: &str, - metadata: serde_json::Value, - ) -> Result<(), FlowyError>; - - async fn create_question( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError>; - - async fn create_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option<serde_json::Value>, - ) -> Result<ChatMessage, FlowyError>; - - async fn stream_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, - ) -> Result<StreamAnswer, FlowyError>; - - async fn get_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError>; - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError>; - - async fn get_question_from_answer_id( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError>; - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError>; - - async fn stream_complete( - &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError>; - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError>; - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError>; - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError>; - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError>; - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError>; -} diff --git a/frontend/rust-lib/flowy-ai-pub/src/lib.rs b/frontend/rust-lib/flowy-ai-pub/src/lib.rs deleted file mode 100644 index df7dc957e2..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod cloud; -pub mod persistence; -pub mod user_service; diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs deleted file mode 100644 index 230e5761d2..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_message_sql.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::cloud::MessageCursor; -use client_api::entity::chat_dto::ChatMessage; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::upsert::excluded; -use flowy_sqlite::{ - diesel, insert_into, - query_dsl::*, - schema::{chat_message_table, chat_message_table::dsl}, - DBConnection, ExpressionMethods, Identifiable, Insertable, OptionalExtension, QueryResult, - Queryable, -}; - -#[derive(Queryable, Insertable, Identifiable)] -#[diesel(table_name = chat_message_table)] -#[diesel(primary_key(message_id))] -pub struct ChatMessageTable { - pub message_id: i64, - pub chat_id: String, - pub content: String, - pub created_at: i64, - pub author_type: i64, - pub author_id: String, - pub reply_message_id: Option<i64>, - pub metadata: Option<String>, - pub is_sync: bool, -} -impl ChatMessageTable { - pub fn from_message(chat_id: String, message: ChatMessage, is_sync: bool) -> Self { - ChatMessageTable { - message_id: message.message_id, - chat_id, - content: message.content, - created_at: message.created_at.timestamp(), - author_type: message.author.author_type as i64, - author_id: message.author.author_id.to_string(), - reply_message_id: message.reply_message_id, - metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), - is_sync, - } - } -} - -pub fn update_chat_message_is_sync( - mut conn: DBConnection, - chat_id_val: &str, - message_id_val: i64, - is_sync_val: bool, -) -> FlowyResult<()> { - diesel::update(chat_message_table::table) - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .filter(chat_message_table::message_id.eq(message_id_val)) - .set(chat_message_table::is_sync.eq(is_sync_val)) - .execute(&mut *conn)?; - - Ok(()) -} - -pub fn upsert_chat_messages( - mut conn: DBConnection, - new_messages: &[ChatMessageTable], -) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - for message in new_messages { - let _ = insert_into(chat_message_table::table) - .values(message) - .on_conflict(chat_message_table::message_id) - .do_update() - .set(( - chat_message_table::content.eq(excluded(chat_message_table::content)), - chat_message_table::metadata.eq(excluded(chat_message_table::metadata)), - chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), - chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), - chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), - chat_message_table::reply_message_id.eq(excluded(chat_message_table::reply_message_id)), - )) - .execute(conn)?; - } - Ok::<(), FlowyError>(()) - })?; - - Ok(()) -} - -pub struct ChatMessagesResult { - pub messages: Vec<ChatMessageTable>, - pub total_count: i64, - pub has_more: bool, -} - -pub fn select_chat_messages( - mut conn: DBConnection, - chat_id_val: &str, - limit_val: u64, - offset: MessageCursor, -) -> QueryResult<ChatMessagesResult> { - let mut query = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .into_boxed(); - - match offset { - MessageCursor::AfterMessageId(after_message_id) => { - query = query.filter(chat_message_table::message_id.gt(after_message_id)); - }, - MessageCursor::BeforeMessageId(before_message_id) => { - query = query.filter(chat_message_table::message_id.lt(before_message_id)); - }, - MessageCursor::Offset(offset_val) => { - query = query.offset(offset_val as i64); - }, - MessageCursor::NextBack => {}, - } - - // Get total count before applying limit - let total_count = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .count() - .first::<i64>(&mut *conn)?; - - query = query - .order(( - chat_message_table::created_at.desc(), - chat_message_table::message_id.desc(), - )) - .limit(limit_val as i64); - - let messages: Vec<ChatMessageTable> = query.load::<ChatMessageTable>(&mut *conn)?; - - // Check if there are more messages - let has_more = if let Some(last_message) = messages.last() { - let remaining_count = dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .filter(chat_message_table::message_id.lt(last_message.message_id)) - .count() - .first::<i64>(&mut *conn)?; - - remaining_count > 0 - } else { - false - }; - - Ok(ChatMessagesResult { - messages, - total_count, - has_more, - }) -} - -pub fn total_message_count(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<i64> { - dsl::chat_message_table - .filter(chat_message_table::chat_id.eq(chat_id_val)) - .count() - .first::<i64>(&mut *conn) -} - -pub fn select_message( - mut conn: DBConnection, - message_id_val: i64, -) -> QueryResult<Option<ChatMessageTable>> { - let message = dsl::chat_message_table - .filter(chat_message_table::message_id.eq(message_id_val)) - .first::<ChatMessageTable>(&mut *conn) - .optional()?; - Ok(message) -} - -pub fn select_message_content( - mut conn: DBConnection, - message_id_val: i64, -) -> QueryResult<Option<String>> { - let message = dsl::chat_message_table - .filter(chat_message_table::message_id.eq(message_id_val)) - .select(chat_message_table::content) - .first::<String>(&mut *conn) - .optional()?; - Ok(message) -} - -pub fn select_answer_where_match_reply_message_id( - mut conn: DBConnection, - chat_id: &str, - answer_message_id_val: i64, -) -> QueryResult<Option<ChatMessageTable>> { - dsl::chat_message_table - .filter(chat_message_table::reply_message_id.eq(answer_message_id_val)) - .filter(chat_message_table::chat_id.eq(chat_id)) - .first::<ChatMessageTable>(&mut *conn) - .optional() -} diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs deleted file mode 100644 index f5398c48c0..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/persistence/chat_sql.rs +++ /dev/null @@ -1,177 +0,0 @@ -use diesel::sqlite::SqliteConnection; -use flowy_error::FlowyResult; -use flowy_sqlite::upsert::excluded; -use flowy_sqlite::{ - diesel, - query_dsl::*, - schema::{chat_table, chat_table::dsl}, - AsChangeset, DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, -}; -use lib_infra::util::timestamp; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use uuid::Uuid; - -#[derive(Clone, Default, Queryable, Insertable, Identifiable)] -#[diesel(table_name = chat_table)] -#[diesel(primary_key(chat_id))] -pub struct ChatTable { - pub chat_id: String, - pub created_at: i64, - pub name: String, - pub metadata: String, - pub rag_ids: Option<String>, - pub is_sync: bool, -} - -impl ChatTable { - pub fn new(chat_id: String, metadata: Value, rag_ids: Vec<Uuid>, is_sync: bool) -> Self { - let rag_ids = rag_ids.iter().map(|v| v.to_string()).collect::<Vec<_>>(); - let metadata = serialize_chat_metadata(&metadata); - let rag_ids = Some(serialize_rag_ids(&rag_ids)); - Self { - chat_id, - created_at: timestamp(), - name: "".to_string(), - metadata, - rag_ids, - is_sync, - } - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct ChatTableMetadata { - pub files: Vec<ChatTableFile>, -} - -impl ChatTableMetadata { - pub fn add_file(&mut self, name: String, id: String) { - if let Some(file) = self.files.iter_mut().find(|f| f.name == name) { - file.id = id; - } else { - self.files.push(ChatTableFile { name, id }); - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ChatTableFile { - pub name: String, - pub id: String, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = chat_table)] -#[diesel(primary_key(chat_id))] -pub struct ChatTableChangeset { - pub chat_id: String, - pub name: Option<String>, - pub metadata: Option<String>, - pub rag_ids: Option<String>, - pub is_sync: Option<bool>, -} - -pub fn serialize_rag_ids(rag_ids: &[String]) -> String { - serde_json::to_string(rag_ids).unwrap_or_default() -} - -pub fn deserialize_rag_ids(rag_ids_str: &Option<String>) -> Vec<String> { - match rag_ids_str { - Some(str) => serde_json::from_str(str).unwrap_or_default(), - None => Vec::new(), - } -} - -pub fn deserialize_chat_metadata<T>(metadata: &str) -> T -where - T: serde::de::DeserializeOwned + Default, -{ - serde_json::from_str(metadata).unwrap_or_default() -} - -pub fn serialize_chat_metadata<T>(metadata: &T) -> String -where - T: Serialize, -{ - serde_json::to_string(metadata).unwrap_or_default() -} - -pub fn upsert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<usize> { - diesel::insert_into(chat_table::table) - .values(new_chat) - .on_conflict(chat_table::chat_id) - .do_update() - .set(( - chat_table::created_at.eq(excluded(chat_table::created_at)), - chat_table::name.eq(excluded(chat_table::name)), - chat_table::metadata.eq(excluded(chat_table::metadata)), - chat_table::rag_ids.eq(excluded(chat_table::rag_ids)), - chat_table::is_sync.eq(excluded(chat_table::is_sync)), - )) - .execute(&mut *conn) -} - -pub fn update_chat( - conn: &mut SqliteConnection, - changeset: ChatTableChangeset, -) -> QueryResult<usize> { - let filter = dsl::chat_table.filter(chat_table::chat_id.eq(changeset.chat_id.clone())); - let affected_row = diesel::update(filter).set(changeset).execute(conn)?; - Ok(affected_row) -} - -pub fn update_chat_is_sync( - mut conn: DBConnection, - chat_id_val: &str, - is_sync_val: bool, -) -> QueryResult<usize> { - diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) - .set(chat_table::is_sync.eq(is_sync_val)) - .execute(&mut *conn) -} - -pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<ChatTable> { - let row = dsl::chat_table - .filter(chat_table::chat_id.eq(chat_id_val)) - .first::<ChatTable>(&mut *conn)?; - Ok(row) -} - -pub fn read_chat_rag_ids( - conn: &mut SqliteConnection, - chat_id_val: &str, -) -> FlowyResult<Vec<String>> { - let chat = dsl::chat_table - .filter(chat_table::chat_id.eq(chat_id_val)) - .first::<ChatTable>(conn)?; - - Ok(deserialize_rag_ids(&chat.rag_ids)) -} - -pub fn read_chat_metadata( - conn: &mut SqliteConnection, - chat_id_val: &str, -) -> FlowyResult<ChatTableMetadata> { - let metadata_str = dsl::chat_table - .select(chat_table::metadata) - .filter(chat_table::chat_id.eq(chat_id_val)) - .first::<String>(&mut *conn)?; - Ok(deserialize_chat_metadata(&metadata_str)) -} - -#[allow(dead_code)] -pub fn update_chat_name( - mut conn: DBConnection, - chat_id_val: &str, - new_name: &str, -) -> QueryResult<usize> { - diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) - .set(chat_table::name.eq(new_name)) - .execute(&mut *conn) -} - -#[allow(dead_code)] -pub fn delete_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<usize> { - diesel::delete(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))).execute(&mut *conn) -} diff --git a/frontend/rust-lib/flowy-ai-pub/src/user_service.rs b/frontend/rust-lib/flowy-ai-pub/src/user_service.rs deleted file mode 100644 index e227c977fe..0000000000 --- a/frontend/rust-lib/flowy-ai-pub/src/user_service.rs +++ /dev/null @@ -1,14 +0,0 @@ -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use uuid::Uuid; - -#[async_trait] -pub trait AIUserService: Send + Sync + 'static { - fn user_id(&self) -> Result<i64, FlowyError>; - async fn is_local_model(&self) -> FlowyResult<bool>; - fn workspace_id(&self) -> Result<Uuid, FlowyError>; - fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>; - fn application_root_dir(&self) -> Result<PathBuf, FlowyError>; -} diff --git a/frontend/rust-lib/flowy-ai/Cargo.toml b/frontend/rust-lib/flowy-ai/Cargo.toml deleted file mode 100644 index 3a6aaf5898..0000000000 --- a/frontend/rust-lib/flowy-ai/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "flowy-ai" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -flowy-derive.workspace = true -flowy-notification = { workspace = true } -flowy-error = { path = "../flowy-error", features = [ - "impl_from_dispatch_error", - "impl_from_collab_folder", - "impl_from_sqlite", - "impl_from_appflowy_cloud", -] } -lib-dispatch = { workspace = true } -tracing.workspace = true -uuid.workspace = true -strum_macros = "0.21" -protobuf.workspace = true -bytes.workspace = true -arc-swap.workspace = true -validator = { workspace = true, features = ["derive"] } -lib-infra = { workspace = true, features = ["isolate_flutter"] } -flowy-ai-pub.workspace = true -dashmap.workspace = true -flowy-sqlite = { workspace = true } -tokio.workspace = true -futures.workspace = true -allo-isolate = { version = "^0.1", features = ["catch-unwind"] } -log = "0.4.21" -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -anyhow = "1.0.86" -tokio-stream = "0.1.15" -tokio-util = { workspace = true, features = ["full"] } -af-local-ai = { workspace = true } -af-plugin = { workspace = true } -reqwest = { version = "0.11.27", features = ["json"] } -sha2 = "0.10.7" -base64 = "0.21.5" -futures-util = "0.3.30" -pin-project = "1.1.5" -flowy-storage-pub = { workspace = true } -collab-integrate.workspace = true - - -[target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] -notify = "6.1.1" -af-mcp = { version = "0.1.0" } - -[dev-dependencies] -dotenv = "0.15.0" -uuid.workspace = true -tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", "ansi", "json"] } -simsimd = "4.4.0" - -[build-dependencies] -flowy-codegen.workspace = true - -[features] -dart = ["flowy-codegen/dart", "flowy-notification/dart"] diff --git a/frontend/rust-lib/flowy-ai/build.rs b/frontend/rust-lib/flowy-ai/build.rs deleted file mode 100644 index 77c0c8125b..0000000000 --- a/frontend/rust-lib/flowy-ai/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - #[cfg(feature = "dart")] - { - flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); - } -} diff --git a/frontend/rust-lib/flowy-ai/dev.env b/frontend/rust-lib/flowy-ai/dev.env deleted file mode 100644 index 5cff5dd858..0000000000 --- a/frontend/rust-lib/flowy-ai/dev.env +++ /dev/null @@ -1,5 +0,0 @@ - -CHAT_BIN_PATH= -LOCAL_AI_MODEL_DIR= -LOCAL_AI_CHAT_MODEL_NAME= -LOCAL_AI_EMBEDDING_MODEL_NAME= diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs deleted file mode 100644 index 2e9fc7e720..0000000000 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ /dev/null @@ -1,806 +0,0 @@ -use crate::chat::Chat; -use crate::entities::{ - AIModelPB, AvailableModelsPB, ChatInfoPB, ChatMessageListPB, ChatMessagePB, ChatSettingsPB, - FilePB, PredefinedFormatPB, RepeatedRelatedQuestionPB, StreamMessageParams, -}; -use crate::local_ai::controller::{LocalAIController, LocalAISetting}; -use crate::middleware::chat_service_mw::ChatServiceMiddleware; -use flowy_ai_pub::persistence::read_chat_metadata; -use std::collections::HashMap; - -use dashmap::DashMap; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatSettings, UpdateChatParams, DEFAULT_AI_MODEL_NAME, -}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; - -use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::util::ai_available_models_key; -use collab_integrate::persistence::collab_metadata_sql::{ - batch_insert_collab_metadata, batch_select_collab_metadata, AFCollabMetadata, -}; -use flowy_ai_pub::cloud::ai_dto::AvailableModel; -use flowy_ai_pub::user_service::AIUserService; -use flowy_storage_pub::storage::StorageService; -use lib_infra::async_trait::async_trait; -use lib_infra::util::timestamp; -use serde_json::json; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::{Arc, Weak}; -use tokio::sync::RwLock; -use tracing::{error, info, instrument, trace}; -use uuid::Uuid; - -/// AIExternalService is an interface for external services that AI plugin can interact with. -#[async_trait] -pub trait AIExternalService: Send + Sync + 'static { - async fn query_chat_rag_ids( - &self, - parent_view_id: &Uuid, - chat_id: &Uuid, - ) -> Result<Vec<Uuid>, FlowyError>; - - async fn sync_rag_documents( - &self, - workspace_id: &Uuid, - rag_ids: Vec<Uuid>, - rag_metadata_map: HashMap<Uuid, AFCollabMetadata>, - ) -> Result<Vec<AFCollabMetadata>, FlowyError>; - - async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError>; -} - -#[derive(Debug, Default)] -struct ServerModelsCache { - models: Vec<AvailableModel>, - timestamp: Option<i64>, -} - -pub const GLOBAL_ACTIVE_MODEL_KEY: &str = "global_active_model"; - -pub struct AIManager { - pub cloud_service_wm: Arc<ChatServiceMiddleware>, - pub user_service: Arc<dyn AIUserService>, - pub external_service: Arc<dyn AIExternalService>, - chats: Arc<DashMap<Uuid, Arc<Chat>>>, - pub local_ai: Arc<LocalAIController>, - pub store_preferences: Arc<KVStorePreferences>, - server_models: Arc<RwLock<ServerModelsCache>>, -} - -impl AIManager { - pub fn new( - chat_cloud_service: Arc<dyn ChatCloudService>, - user_service: impl AIUserService, - store_preferences: Arc<KVStorePreferences>, - storage_service: Weak<dyn StorageService>, - query_service: impl AIExternalService, - local_ai: Arc<LocalAIController>, - ) -> AIManager { - let user_service = Arc::new(user_service); - let cloned_local_ai = local_ai.clone(); - tokio::spawn(async move { - cloned_local_ai.observe_plugin_resource().await; - }); - - let external_service = Arc::new(query_service); - let cloud_service_wm = Arc::new(ChatServiceMiddleware::new( - user_service.clone(), - chat_cloud_service, - local_ai.clone(), - storage_service, - )); - - Self { - cloud_service_wm, - user_service, - chats: Arc::new(DashMap::new()), - local_ai, - external_service, - store_preferences, - server_models: Arc::new(Default::default()), - } - } - - async fn reload_with_workspace_id(&self, workspace_id: &str) { - // Check if local AI is enabled for this workspace and if we're in local mode - let result = self.user_service.is_local_model().await; - if let Err(err) = &result { - if matches!(err.code, ErrorCode::UserNotLogin) { - info!("[AI Manager] User not logged in, skipping local AI reload"); - return; - } - } - - let is_local = result.unwrap_or(false); - let is_enabled = self.local_ai.is_enabled_on_workspace(workspace_id); - let is_running = self.local_ai.is_running(); - info!( - "[AI Manager] Reloading workspace: {}, is_local: {}, is_enabled: {}, is_running: {}", - workspace_id, is_local, is_enabled, is_running - ); - - // Shutdown AI if it's running but shouldn't be (not enabled and not in local mode) - if is_running && !is_enabled && !is_local { - info!("[AI Manager] Local AI is running but not enabled, shutting it down"); - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - // Wait for 5 seconds to allow other services to initialize - // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - if let Err(err) = local_ai.toggle_plugin(false).await { - error!("[AI Manager] failed to shutdown local AI: {:?}", err); - } - }); - return; - } - - // Start AI if it's enabled but not running - if is_enabled && !is_running { - info!("[AI Manager] Local AI is enabled but not running, starting it now"); - let local_ai = self.local_ai.clone(); - tokio::spawn(async move { - // Wait for 5 seconds to allow other services to initialize - // TODO: pick a right time to start plugin service. Maybe [UserStatusCallback::did_launch] - tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; - - if let Err(err) = local_ai.toggle_plugin(true).await { - error!("[AI Manager] failed to start local AI: {:?}", err); - } - }); - return; - } - - // Log status for other cases - if is_running { - info!("[AI Manager] Local AI is already running"); - } - } - - #[instrument(skip_all, err)] - pub async fn on_launch_if_authenticated(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; - Ok(()) - } - - pub async fn initialize_after_sign_in(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; - Ok(()) - } - - pub async fn initialize_after_sign_up(&self, workspace_id: &str) -> Result<(), FlowyError> { - self.reload_with_workspace_id(workspace_id).await; - Ok(()) - } - - #[instrument(skip_all, err)] - pub async fn initialize_after_open_workspace( - &self, - workspace_id: &Uuid, - ) -> Result<(), FlowyError> { - self - .reload_with_workspace_id(&workspace_id.to_string()) - .await; - Ok(()) - } - - pub async fn open_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - self.chats.entry(*chat_id).or_insert_with(|| { - Arc::new(Chat::new( - self.user_service.user_id().unwrap(), - *chat_id, - self.user_service.clone(), - self.cloud_service_wm.clone(), - )) - }); - if self.local_ai.is_running() { - trace!("[AI Plugin] notify open chat: {}", chat_id); - self.local_ai.open_chat(chat_id); - } - - let user_service = self.user_service.clone(); - let cloud_service_wm = self.cloud_service_wm.clone(); - let store_preferences = self.store_preferences.clone(); - let external_service = self.external_service.clone(); - let chat_id = *chat_id; - tokio::spawn(async move { - match refresh_chat_setting( - &user_service, - &cloud_service_wm, - &store_preferences, - &chat_id, - ) - .await - { - Ok(settings) => { - let rag_ids = settings - .rag_ids - .into_iter() - .flat_map(|r| Uuid::from_str(&r).ok()) - .collect(); - let _ = sync_chat_documents(user_service, external_service, rag_ids).await; - }, - Err(err) => { - error!("failed to refresh chat settings: {}", err); - }, - } - }); - - Ok(()) - } - - pub async fn close_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - trace!("close chat: {}", chat_id); - self.local_ai.close_chat(chat_id); - Ok(()) - } - - pub async fn delete_chat(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - if let Some((_, chat)) = self.chats.remove(chat_id) { - chat.close(); - - if self.local_ai.is_running() { - info!("[AI Plugin] notify close chat: {}", chat_id); - self.local_ai.close_chat(chat_id); - } - } - Ok(()) - } - - pub async fn get_chat_info(&self, chat_id: &str) -> FlowyResult<ChatInfoPB> { - let uid = self.user_service.user_id()?; - let mut conn = self.user_service.sqlite_connection(uid)?; - let metadata = read_chat_metadata(&mut conn, chat_id)?; - let files = metadata - .files - .into_iter() - .map(|file| FilePB { - id: file.id, - name: file.name, - }) - .collect(); - - Ok(ChatInfoPB { - chat_id: chat_id.to_string(), - files, - }) - } - - pub async fn create_chat( - &self, - uid: &i64, - parent_view_id: &Uuid, - chat_id: &Uuid, - ) -> Result<Arc<Chat>, FlowyError> { - let workspace_id = self.user_service.workspace_id()?; - let rag_ids = self - .external_service - .query_chat_rag_ids(parent_view_id, chat_id) - .await - .unwrap_or_default(); - info!("[Chat] create chat with rag_ids: {:?}", rag_ids); - - self - .cloud_service_wm - .create_chat(uid, &workspace_id, chat_id, rag_ids, "", json!({})) - .await?; - - let chat = Arc::new(Chat::new( - self.user_service.user_id()?, - *chat_id, - self.user_service.clone(), - self.cloud_service_wm.clone(), - )); - self.chats.insert(*chat_id, chat.clone()); - Ok(chat) - } - - pub async fn stream_chat_message( - &self, - params: StreamMessageParams, - ) -> Result<ChatMessagePB, FlowyError> { - let chat = self.get_or_create_chat_instance(¶ms.chat_id).await?; - let ai_model = self.get_active_model(¶ms.chat_id.to_string()).await; - let question = chat.stream_chat_message(¶ms, ai_model).await?; - let _ = self - .external_service - .notify_did_send_message(¶ms.chat_id, ¶ms.message) - .await; - Ok(question) - } - - pub async fn stream_regenerate_response( - &self, - chat_id: &Uuid, - answer_message_id: i64, - answer_stream_port: i64, - format: Option<PredefinedFormatPB>, - model: Option<AIModelPB>, - ) -> FlowyResult<()> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let question_message_id = chat - .get_question_id_from_answer_id(chat_id, answer_message_id) - .await?; - - let model = model.map_or_else( - || { - self - .store_preferences - .get_object::<AIModel>(&ai_available_models_key(&chat_id.to_string())) - }, - |model| Some(model.into()), - ); - chat - .stream_regenerate_response(question_message_id, answer_stream_port, format, model) - .await?; - Ok(()) - } - - pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - let previous_model = self.local_ai.get_local_ai_setting().chat_model_name; - self.local_ai.update_local_ai_setting(setting).await?; - let current_model = self.local_ai.get_local_ai_setting().chat_model_name; - - if previous_model != current_model { - info!( - "[AI Plugin] update global active model, previous: {}, current: {}", - previous_model, current_model - ); - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - let model = AIModel::local(current_model, "".to_string()); - self.update_selected_model(source_key, model).await?; - } - - Ok(()) - } - - async fn get_workspace_select_model(&self) -> FlowyResult<String> { - let workspace_id = self.user_service.workspace_id()?; - let model = self - .cloud_service_wm - .get_workspace_default_model(&workspace_id) - .await?; - - if model.is_empty() { - return Ok(DEFAULT_AI_MODEL_NAME.to_string()); - } - Ok(model) - } - - async fn get_server_available_models(&self) -> FlowyResult<Vec<AvailableModel>> { - let workspace_id = self.user_service.workspace_id()?; - let now = timestamp(); - - // First, try reading from the cache with expiration check - let should_fetch = { - let cached_models = self.server_models.read().await; - cached_models.models.is_empty() || cached_models.timestamp.map_or(true, |ts| now - ts >= 300) - }; - - if !should_fetch { - // Cache is still valid, return cached data - let cached_models = self.server_models.read().await; - return Ok(cached_models.models.clone()); - } - - // Cache miss or expired: fetch from the cloud. - match self - .cloud_service_wm - .get_available_models(&workspace_id) - .await - { - Ok(list) => { - let models = list.models; - if let Err(err) = self.update_models_cache(&models, now).await { - error!("Failed to update models cache: {}", err); - } - - Ok(models) - }, - Err(err) => { - error!("Failed to fetch available models: {}", err); - - // Return cached data if available, even if expired - let cached_models = self.server_models.read().await; - if !cached_models.models.is_empty() { - info!("Returning expired cached models due to fetch failure"); - return Ok(cached_models.models.clone()); - } - - // If no cached data, return empty list - Ok(Vec::new()) - }, - } - } - - async fn update_models_cache( - &self, - models: &[AvailableModel], - timestamp: i64, - ) -> FlowyResult<()> { - match self.server_models.try_write() { - Ok(mut cache) => { - cache.models = models.to_vec(); - cache.timestamp = Some(timestamp); - Ok(()) - }, - Err(_) => { - // Handle lock acquisition failure - Err(FlowyError::internal().with_context("Failed to acquire write lock for models cache")) - }, - } - } - - pub async fn update_selected_model(&self, source: String, model: AIModel) -> FlowyResult<()> { - info!( - "[Model Selection] update {} selected model: {:?}", - source, model - ); - let source_key = ai_available_models_key(&source); - self - .store_preferences - .set_object::<AIModel>(&source_key, &model)?; - - chat_notification_builder(&source, ChatNotification::DidUpdateSelectedModel) - .payload(AIModelPB::from(model)) - .send(); - Ok(()) - } - - #[instrument(skip_all, level = "debug")] - pub async fn toggle_local_ai(&self) -> FlowyResult<()> { - let enabled = self.local_ai.toggle_local_ai().await?; - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - if enabled { - if let Some(name) = self.local_ai.get_plugin_chat_model() { - info!("Set global active model to local ai: {}", name); - let model = AIModel::local(name, "".to_string()); - self.update_selected_model(source_key, model).await?; - } - } else { - info!("Set global active model to default"); - let global_active_model = self.get_workspace_select_model().await?; - let models = self.get_server_available_models().await?; - if let Some(model) = models.into_iter().find(|m| m.name == global_active_model) { - self - .update_selected_model(source_key, AIModel::from(model)) - .await?; - } - } - - Ok(()) - } - - pub async fn get_active_model(&self, source: &str) -> Option<AIModel> { - let mut model = self - .store_preferences - .get_object::<AIModel>(&ai_available_models_key(source)); - - if model.is_none() { - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - model = Some(AIModel::local(local_model, "".to_string())); - } - } - - model - } - - pub async fn get_available_models(&self, source: String) -> FlowyResult<AvailableModelsPB> { - let is_local_mode = self.user_service.is_local_model().await?; - if is_local_mode { - let setting = self.local_ai.get_local_ai_setting(); - let selected_model = AIModel::local(setting.chat_model_name, "".to_string()); - let models = vec![selected_model.clone()]; - - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::from(selected_model), - }) - } else { - // Build the models list from server models and mark them as non-local. - let mut models: Vec<AIModel> = self - .get_server_available_models() - .await? - .into_iter() - .map(AIModel::from) - .collect(); - - trace!("[Model Selection]: Available models: {:?}", models); - let mut current_active_local_ai_model = None; - - // If user enable local ai, then add local ai model to the list. - if let Some(local_model) = self.local_ai.get_plugin_chat_model() { - let model = AIModel::local(local_model, "".to_string()); - current_active_local_ai_model = Some(model.clone()); - trace!("[Model Selection] current local ai model: {}", model.name); - models.push(model); - } - - if models.is_empty() { - return Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model: AIModelPB::default(), - }); - } - - // Global active model is the model selected by the user in the workspace settings. - let mut server_active_model = self - .get_workspace_select_model() - .await - .map(|m| AIModel::server(m, "".to_string())) - .unwrap_or_else(|_| AIModel::default()); - - trace!( - "[Model Selection] server active model: {:?}", - server_active_model - ); - - let mut user_selected_model = server_active_model.clone(); - // when current select model is deprecated, reset the model to default - if !models.iter().any(|m| m.name == server_active_model.name) { - server_active_model = AIModel::default(); - } - - let source_key = ai_available_models_key(&source); - // We use source to identify user selected model. source can be document id or chat id. - match self.store_preferences.get_object::<AIModel>(&source_key) { - None => { - // when there is selected model and current local ai is active, then use local ai - if let Some(local_ai_model) = models.iter().find(|m| m.is_local) { - user_selected_model = local_ai_model.clone(); - } - }, - Some(mut model) => { - trace!("[Model Selection] user previous select model: {:?}", model); - // If source is provided, try to get the user-selected model from the store. User selected - // model will be used as the active model if it exists. - if model.is_local { - if let Some(local_ai_model) = ¤t_active_local_ai_model { - if local_ai_model.name != model.name { - model = local_ai_model.clone(); - } - } - } - - user_selected_model = model; - }, - } - - // If user selected model is not available in the list, use the global active model. - let active_model = models - .iter() - .find(|m| m.name == user_selected_model.name) - .cloned() - .or(Some(server_active_model.clone())); - - // Update the stored preference if a different model is used. - if let Some(ref active_model) = active_model { - if active_model.name != user_selected_model.name { - self - .store_preferences - .set_object::<AIModel>(&source_key, &active_model.clone())?; - } - } - - trace!("[Model Selection] final active model: {:?}", active_model); - let selected_model = AIModelPB::from(active_model.unwrap_or_default()); - Ok(AvailableModelsPB { - models: models.into_iter().map(|m| m.into()).collect(), - selected_model, - }) - } - } - - pub async fn get_or_create_chat_instance(&self, chat_id: &Uuid) -> Result<Arc<Chat>, FlowyError> { - let chat = self.chats.get(chat_id).as_deref().cloned(); - match chat { - None => { - let chat = Arc::new(Chat::new( - self.user_service.user_id()?, - *chat_id, - self.user_service.clone(), - self.cloud_service_wm.clone(), - )); - self.chats.insert(*chat_id, chat.clone()); - Ok(chat) - }, - Some(chat) => Ok(chat), - } - } - - /// Load chat messages for a given `chat_id`. - /// - /// 1. When opening a chat: - /// - Loads local chat messages. - /// - `after_message_id` and `before_message_id` are `None`. - /// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded. - /// - /// 2. Loading more messages in an existing chat with `after_message_id`: - /// - `after_message_id` is the last message ID in the current chat messages. - /// - /// 3. Loading more messages in an existing chat with `before_message_id`: - /// - `before_message_id` is the first message ID in the current chat messages. - /// - /// 4. `after_message_id` and `before_message_id` cannot be specified at the same time. - - pub async fn load_prev_chat_messages( - &self, - chat_id: &Uuid, - limit: u64, - before_message_id: Option<i64>, - ) -> Result<ChatMessageListPB, FlowyError> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let list = chat - .load_prev_chat_messages(limit, before_message_id) - .await?; - Ok(list) - } - - pub async fn load_latest_chat_messages( - &self, - chat_id: &Uuid, - limit: u64, - after_message_id: Option<i64>, - ) -> Result<ChatMessageListPB, FlowyError> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let list = chat - .load_latest_chat_messages(limit, after_message_id) - .await?; - Ok(list) - } - - pub async fn get_related_questions( - &self, - chat_id: &Uuid, - message_id: i64, - ) -> Result<RepeatedRelatedQuestionPB, FlowyError> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let ai_model = self.get_active_model(&chat_id.to_string()).await; - let resp = chat.get_related_question(message_id, ai_model).await?; - Ok(resp) - } - - pub async fn generate_answer( - &self, - chat_id: &Uuid, - question_message_id: i64, - ) -> Result<ChatMessagePB, FlowyError> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - let resp = chat.generate_answer(question_message_id).await?; - Ok(resp) - } - - pub async fn stop_stream(&self, chat_id: &Uuid) -> Result<(), FlowyError> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - chat.stop_stream_message().await; - Ok(()) - } - - pub async fn chat_with_file(&self, chat_id: &Uuid, file_path: PathBuf) -> FlowyResult<()> { - let chat = self.get_or_create_chat_instance(chat_id).await?; - chat.index_file(file_path).await?; - Ok(()) - } - - pub async fn get_rag_ids(&self, chat_id: &Uuid) -> FlowyResult<Vec<String>> { - if let Some(settings) = self - .store_preferences - .get_object::<ChatSettings>(&setting_store_key(chat_id)) - { - return Ok(settings.rag_ids); - } - - let settings = refresh_chat_setting( - &self.user_service, - &self.cloud_service_wm, - &self.store_preferences, - chat_id, - ) - .await?; - Ok(settings.rag_ids) - } - - pub async fn update_rag_ids(&self, chat_id: &Uuid, rag_ids: Vec<String>) -> FlowyResult<()> { - info!("[Chat] update chat:{} rag ids: {:?}", chat_id, rag_ids); - let workspace_id = self.user_service.workspace_id()?; - let update_setting = UpdateChatParams { - name: None, - metadata: None, - rag_ids: Some(rag_ids.clone()), - }; - self - .cloud_service_wm - .update_chat_settings(&workspace_id, chat_id, update_setting) - .await?; - - let chat_setting_store_key = setting_store_key(chat_id); - if let Some(settings) = self - .store_preferences - .get_object::<ChatSettings>(&chat_setting_store_key) - { - if let Err(err) = self.store_preferences.set_object( - &chat_setting_store_key, - &ChatSettings { - rag_ids: rag_ids.clone(), - ..settings - }, - ) { - error!("failed to set chat settings: {}", err); - } - } - - let user_service = self.user_service.clone(); - let external_service = self.external_service.clone(); - let rag_ids = rag_ids - .into_iter() - .flat_map(|r| Uuid::from_str(&r).ok()) - .collect(); - sync_chat_documents(user_service, external_service, rag_ids).await?; - Ok(()) - } -} - -async fn sync_chat_documents( - user_service: Arc<dyn AIUserService>, - external_service: Arc<dyn AIExternalService>, - rag_ids: Vec<Uuid>, -) -> FlowyResult<()> { - if rag_ids.is_empty() { - return Ok(()); - } - - let uid = user_service.user_id()?; - let conn = user_service.sqlite_connection(uid)?; - let metadata_map = batch_select_collab_metadata(conn, &rag_ids)?; - - let user_service = user_service.clone(); - tokio::spawn(async move { - if let Ok(workspace_id) = user_service.workspace_id() { - if let Ok(metadatas) = external_service - .sync_rag_documents(&workspace_id, rag_ids, metadata_map) - .await - { - if let Ok(uid) = user_service.user_id() { - if let Ok(conn) = user_service.sqlite_connection(uid) { - info!("sync rag documents success: {}", metadatas.len()); - batch_insert_collab_metadata(conn, &metadatas).unwrap(); - } - } - } - } - }); - - Ok(()) -} - -async fn refresh_chat_setting( - user_service: &Arc<dyn AIUserService>, - cloud_service: &Arc<ChatServiceMiddleware>, - store_preferences: &Arc<KVStorePreferences>, - chat_id: &Uuid, -) -> FlowyResult<ChatSettings> { - info!("[Chat] refresh chat:{} setting", chat_id); - let workspace_id = user_service.workspace_id()?; - let settings = cloud_service - .get_chat_settings(&workspace_id, chat_id) - .await?; - - if let Err(err) = store_preferences.set_object(&setting_store_key(chat_id), &settings) { - error!("failed to set chat settings: {}", err); - } - - chat_notification_builder(chat_id.to_string(), ChatNotification::DidUpdateChatSettings) - .payload(ChatSettingsPB { - rag_ids: settings.rag_ids.clone(), - }) - .send(); - - Ok(settings) -} - -fn setting_store_key(chat_id: &Uuid) -> String { - format!("chat_settings_{}", chat_id) -} diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs deleted file mode 100644 index 052599ef48..0000000000 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ /dev/null @@ -1,668 +0,0 @@ -use crate::entities::{ - ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, PredefinedFormatPB, - RepeatedRelatedQuestionPB, StreamMessageParams, -}; -use crate::middleware::chat_service_mw::ChatServiceMiddleware; -use crate::notification::{chat_notification_builder, ChatNotification}; -use crate::stream_message::StreamMessage; -use allo_isolate::Isolate; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, MessageCursor, QuestionStreamValue, ResponseFormat, -}; -use flowy_ai_pub::persistence::{ - select_answer_where_match_reply_message_id, select_chat_messages, upsert_chat_messages, - ChatMessageTable, -}; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use futures::{SinkExt, StreamExt}; -use lib_infra::isolate_stream::IsolateSink; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicI64}; -use std::sync::Arc; -use tokio::sync::{Mutex, RwLock}; -use tracing::{error, instrument, trace}; -use uuid::Uuid; - -enum PrevMessageState { - HasMore, - NoMore, - Loading, -} - -pub struct Chat { - chat_id: Uuid, - uid: i64, - user_service: Arc<dyn AIUserService>, - chat_service: Arc<ChatServiceMiddleware>, - prev_message_state: Arc<RwLock<PrevMessageState>>, - latest_message_id: Arc<AtomicI64>, - stop_stream: Arc<AtomicBool>, - stream_buffer: Arc<Mutex<StringBuffer>>, -} - -impl Chat { - pub fn new( - uid: i64, - chat_id: Uuid, - user_service: Arc<dyn AIUserService>, - chat_service: Arc<ChatServiceMiddleware>, - ) -> Chat { - Chat { - uid, - chat_id, - chat_service, - user_service, - prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)), - latest_message_id: Default::default(), - stop_stream: Arc::new(AtomicBool::new(false)), - stream_buffer: Arc::new(Mutex::new(StringBuffer::default())), - } - } - - pub fn close(&self) {} - - pub async fn stop_stream_message(&self) { - self - .stop_stream - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - #[instrument(level = "info", skip_all, err)] - pub async fn stream_chat_message( - &self, - params: &StreamMessageParams, - preferred_ai_model: Option<AIModel>, - ) -> Result<ChatMessagePB, FlowyError> { - trace!( - "[Chat] stream chat message: chat_id={}, message={}, message_type={:?}, format={:?}", - self.chat_id, - params.message, - params.message_type, - params.format, - ); - - // clear - self - .stop_stream - .store(false, std::sync::atomic::Ordering::SeqCst); - self.stream_buffer.lock().await.clear(); - - let mut question_sink = IsolateSink::new(Isolate::new(params.question_stream_port)); - let answer_stream_buffer = self.stream_buffer.clone(); - let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; - - let _ = question_sink - .send(StreamMessage::Text(params.message.to_string()).to_string()) - .await; - let question = self - .chat_service - .create_question( - &workspace_id, - &self.chat_id, - ¶ms.message, - params.message_type.clone(), - ) - .await - .map_err(|err| { - error!("Failed to send question: {}", err); - FlowyError::server_error() - })?; - - let _ = question_sink - .send(StreamMessage::MessageId(question.message_id).to_string()) - .await; - - // Save message to disk - notify_message(&self.chat_id, question.clone())?; - let format = params.format.clone().map(Into::into).unwrap_or_default(); - self.stream_response( - params.answer_stream_port, - answer_stream_buffer, - uid, - workspace_id, - question.message_id, - format, - preferred_ai_model, - ); - - let question_pb = ChatMessagePB::from(question); - Ok(question_pb) - } - - #[instrument(level = "info", skip_all, err)] - pub async fn stream_regenerate_response( - &self, - question_id: i64, - answer_stream_port: i64, - format: Option<PredefinedFormatPB>, - ai_model: Option<AIModel>, - ) -> FlowyResult<()> { - trace!( - "[Chat] regenerate and stream chat message: chat_id={}", - self.chat_id, - ); - - // clear - self - .stop_stream - .store(false, std::sync::atomic::Ordering::SeqCst); - self.stream_buffer.lock().await.clear(); - - let format = format.map(Into::into).unwrap_or_default(); - - let answer_stream_buffer = self.stream_buffer.clone(); - let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; - - self.stream_response( - answer_stream_port, - answer_stream_buffer, - uid, - workspace_id, - question_id, - format, - ai_model, - ); - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - fn stream_response( - &self, - answer_stream_port: i64, - answer_stream_buffer: Arc<Mutex<StringBuffer>>, - _uid: i64, - workspace_id: Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, - ) { - let stop_stream = self.stop_stream.clone(); - let chat_id = self.chat_id; - let cloud_service = self.chat_service.clone(); - tokio::spawn(async move { - let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); - match cloud_service - .stream_answer(&workspace_id, &chat_id, question_id, format, ai_model) - .await - { - Ok(mut stream) => { - while let Some(message) = stream.next().await { - match message { - Ok(message) => { - if stop_stream.load(std::sync::atomic::Ordering::Relaxed) { - trace!("[Chat] client stop streaming message"); - break; - } - match message { - QuestionStreamValue::Answer { value } => { - answer_stream_buffer.lock().await.push_str(&value); - if let Err(err) = answer_sink - .send(StreamMessage::OnData(value).to_string()) - .await - { - error!("Failed to stream answer via IsolateSink: {}", err); - } - }, - QuestionStreamValue::Metadata { value } => { - if let Ok(s) = serde_json::to_string(&value) { - // trace!("[Chat] stream metadata: {}", s); - answer_stream_buffer.lock().await.set_metadata(value); - let _ = answer_sink - .send(StreamMessage::Metadata(s).to_string()) - .await; - } - }, - QuestionStreamValue::KeepAlive => { - // trace!("[Chat] stream keep alive"); - }, - } - }, - Err(err) => { - if err.code == ErrorCode::RequestTimeout || err.code == ErrorCode::Internal { - error!("[Chat] unexpected stream error: {}", err); - let _ = answer_sink.send(StreamMessage::Done.to_string()).await; - } else { - error!("[Chat] failed to stream answer: {}", err); - let _ = answer_sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; - let pb = ChatMessageErrorPB { - chat_id: chat_id.to_string(), - error_message: err.to_string(), - }; - chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) - .payload(pb) - .send(); - return Err(err); - } - }, - } - } - }, - Err(err) => { - error!("[Chat] failed to start streaming: {}", err); - if err.is_ai_response_limit_exceeded() { - let _ = answer_sink.send("AI_RESPONSE_LIMIT".to_string()).await; - } else if err.is_ai_image_response_limit_exceeded() { - let _ = answer_sink - .send("AI_IMAGE_RESPONSE_LIMIT".to_string()) - .await; - } else if err.is_ai_max_required() { - let _ = answer_sink - .send(format!("AI_MAX_REQUIRED:{}", err.msg)) - .await; - } else if err.is_local_ai_not_ready() { - let _ = answer_sink - .send(format!("LOCAL_AI_NOT_READY:{}", err.msg)) - .await; - } else if err.is_local_ai_disabled() { - let _ = answer_sink - .send(format!("LOCAL_AI_DISABLED:{}", err.msg)) - .await; - } else { - let _ = answer_sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; - } - - let pb = ChatMessageErrorPB { - chat_id: chat_id.to_string(), - error_message: err.to_string(), - }; - chat_notification_builder(chat_id, ChatNotification::StreamChatMessageError) - .payload(pb) - .send(); - return Err(err); - }, - } - - chat_notification_builder(chat_id, ChatNotification::FinishStreaming).send(); - trace!("[Chat] finish streaming"); - - if answer_stream_buffer.lock().await.is_empty() { - return Ok(()); - } - let content = answer_stream_buffer.lock().await.take_content(); - let metadata = answer_stream_buffer.lock().await.take_metadata(); - let answer = cloud_service - .create_answer( - &workspace_id, - &chat_id, - content.trim(), - question_id, - metadata, - ) - .await?; - notify_message(&chat_id, answer)?; - Ok::<(), FlowyError>(()) - }); - } - - /// Load chat messages for a given `chat_id`. - /// - /// 1. When opening a chat: - /// - Loads local chat messages. - /// - `after_message_id` and `before_message_id` are `None`. - /// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded. - /// - /// 2. Loading more messages in an existing chat with `after_message_id`: - /// - `after_message_id` is the last message ID in the current chat messages. - /// - /// 3. Loading more messages in an existing chat with `before_message_id`: - /// - `before_message_id` is the first message ID in the current chat messages. - pub async fn load_prev_chat_messages( - &self, - limit: u64, - before_message_id: Option<i64>, - ) -> Result<ChatMessageListPB, FlowyError> { - trace!( - "[Chat] Loading messages from disk: chat_id={}, limit={}, before_message_id={:?}", - self.chat_id, - limit, - before_message_id - ); - - let offset = before_message_id.map_or(MessageCursor::NextBack, MessageCursor::BeforeMessageId); - let messages = self.load_local_chat_messages(limit, offset).await?; - - // If the number of messages equals the limit, then no need to load more messages from remote - if messages.len() == limit as usize { - let pb = ChatMessageListPB { - messages, - has_more: true, - total: 0, - }; - chat_notification_builder(self.chat_id, ChatNotification::DidLoadPrevChatMessage) - .payload(pb.clone()) - .send(); - return Ok(pb); - } - - if matches!( - *self.prev_message_state.read().await, - PrevMessageState::HasMore - ) { - *self.prev_message_state.write().await = PrevMessageState::Loading; - if let Err(err) = self - .load_remote_chat_messages(limit, before_message_id, None) - .await - { - error!("Failed to load previous chat messages: {}", err); - } - } - - Ok(ChatMessageListPB { - messages, - has_more: true, - total: 0, - }) - } - - pub async fn load_latest_chat_messages( - &self, - limit: u64, - after_message_id: Option<i64>, - ) -> Result<ChatMessageListPB, FlowyError> { - trace!( - "[Chat] Loading new messages: chat_id={}, limit={}, after_message_id={:?}", - self.chat_id, - limit, - after_message_id, - ); - let offset = after_message_id.map_or(MessageCursor::NextBack, MessageCursor::AfterMessageId); - let messages = self.load_local_chat_messages(limit, offset).await?; - - trace!( - "[Chat] Loaded local chat messages: chat_id={}, messages={}", - self.chat_id, - messages.len() - ); - - // If the number of messages equals the limit, then no need to load more messages from remote - let has_more = !messages.is_empty(); - let _ = self - .load_remote_chat_messages(limit, None, after_message_id) - .await; - Ok(ChatMessageListPB { - messages, - has_more, - total: 0, - }) - } - - async fn load_remote_chat_messages( - &self, - limit: u64, - before_message_id: Option<i64>, - after_message_id: Option<i64>, - ) -> FlowyResult<()> { - trace!( - "[Chat] start loading messages from remote: chat_id={}, limit={}, before_message_id={:?}, after_message_id={:?}", - self.chat_id, - limit, - before_message_id, - after_message_id - ); - let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id; - let cloud_service = self.chat_service.clone(); - let user_service = self.user_service.clone(); - let uid = self.uid; - let prev_message_state = self.prev_message_state.clone(); - let latest_message_id = self.latest_message_id.clone(); - tokio::spawn(async move { - let cursor = match (before_message_id, after_message_id) { - (Some(bid), _) => MessageCursor::BeforeMessageId(bid), - (_, Some(aid)) => MessageCursor::AfterMessageId(aid), - _ => MessageCursor::NextBack, - }; - match cloud_service - .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit) - .await - { - Ok(resp) => { - // Save chat messages to local disk - if let Err(err) = save_chat_message_disk( - user_service.sqlite_connection(uid)?, - &chat_id, - resp.messages.clone(), - true, - ) { - error!("Failed to save chat:{} messages: {}", chat_id, err); - } - - // Update latest message ID - if !resp.messages.is_empty() { - latest_message_id.store( - resp.messages[0].message_id, - std::sync::atomic::Ordering::Relaxed, - ); - } - - let pb = ChatMessageListPB::from(resp); - trace!( - "[Chat] Loaded messages from remote: chat_id={}, messages={}, hasMore: {}, cursor:{:?}", - chat_id, - pb.messages.len(), - pb.has_more, - cursor, - ); - if matches!(cursor, MessageCursor::BeforeMessageId(_)) { - if pb.has_more { - *prev_message_state.write().await = PrevMessageState::HasMore; - } else { - *prev_message_state.write().await = PrevMessageState::NoMore; - } - chat_notification_builder(chat_id, ChatNotification::DidLoadPrevChatMessage) - .payload(pb) - .send(); - } else { - chat_notification_builder(chat_id, ChatNotification::DidLoadLatestChatMessage) - .payload(pb) - .send(); - } - }, - Err(err) => error!("Failed to load chat messages: {}", err), - } - Ok::<(), FlowyError>(()) - }); - Ok(()) - } - - pub async fn get_question_id_from_answer_id( - &self, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<i64, FlowyError> { - let conn = self.user_service.sqlite_connection(self.uid)?; - - let local_result = - select_answer_where_match_reply_message_id(conn, &chat_id.to_string(), answer_message_id)? - .map(|message| message.message_id); - - if let Some(message_id) = local_result { - return Ok(message_id); - } - - let workspace_id = self.user_service.workspace_id()?; - let chat_id = self.chat_id; - let cloud_service = self.chat_service.clone(); - - let question = cloud_service - .get_question_from_answer_id(&workspace_id, &chat_id, answer_message_id) - .await?; - - Ok(question.message_id) - } - - pub async fn get_related_question( - &self, - message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestionPB, FlowyError> { - let workspace_id = self.user_service.workspace_id()?; - let resp = self - .chat_service - .get_related_message(&workspace_id, &self.chat_id, message_id, ai_model) - .await?; - - trace!( - "[Chat] related messages: chat_id={}, message_id={}, messages:{:?}", - self.chat_id, - message_id, - resp.items - ); - Ok(RepeatedRelatedQuestionPB::from(resp)) - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn generate_answer(&self, question_message_id: i64) -> FlowyResult<ChatMessagePB> { - trace!( - "[Chat] generate answer: chat_id={}, question_message_id={}", - self.chat_id, - question_message_id - ); - let workspace_id = self.user_service.workspace_id()?; - let answer = self - .chat_service - .get_answer(&workspace_id, &self.chat_id, question_message_id) - .await?; - - notify_message(&self.chat_id, answer.clone())?; - let pb = ChatMessagePB::from(answer); - Ok(pb) - } - - async fn load_local_chat_messages( - &self, - limit: u64, - offset: MessageCursor, - ) -> Result<Vec<ChatMessagePB>, FlowyError> { - let conn = self.user_service.sqlite_connection(self.uid)?; - let rows = select_chat_messages(conn, &self.chat_id.to_string(), limit, offset)?.messages; - let messages = rows - .into_iter() - .map(|record| ChatMessagePB { - message_id: record.message_id, - content: record.content, - created_at: record.created_at, - author_type: record.author_type, - author_id: record.author_id, - reply_message_id: record.reply_message_id, - metadata: record.metadata, - }) - .collect::<Vec<_>>(); - - Ok(messages) - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn index_file(&self, file_path: PathBuf) -> FlowyResult<()> { - if !file_path.exists() { - return Err( - FlowyError::record_not_found().with_context(format!("{:?} not exist", file_path)), - ); - } - - if !file_path.is_file() { - return Err( - FlowyError::invalid_data().with_context(format!("{:?} is not a file ", file_path)), - ); - } - - trace!( - "[Chat] index file: chat_id={}, file_path={:?}", - self.chat_id, - file_path - ); - self - .chat_service - .embed_file( - &self.user_service.workspace_id()?, - &file_path, - &self.chat_id, - None, - ) - .await?; - - trace!( - "[Chat] created index file record: chat_id={}, file_path={:?}", - self.chat_id, - file_path - ); - - Ok(()) - } -} - -fn save_chat_message_disk( - conn: DBConnection, - chat_id: &Uuid, - messages: Vec<ChatMessage>, - is_sync: bool, -) -> FlowyResult<()> { - let records = messages - .into_iter() - .map(|message| ChatMessageTable { - message_id: message.message_id, - chat_id: chat_id.to_string(), - content: message.content, - created_at: message.created_at.timestamp(), - author_type: message.author.author_type as i64, - author_id: message.author.author_id.to_string(), - reply_message_id: message.reply_message_id, - metadata: Some(serde_json::to_string(&message.metadata).unwrap_or_default()), - is_sync, - }) - .collect::<Vec<_>>(); - upsert_chat_messages(conn, &records)?; - Ok(()) -} - -#[derive(Debug, Default)] -struct StringBuffer { - content: String, - metadata: Option<serde_json::Value>, -} - -impl StringBuffer { - fn clear(&mut self) { - self.content.clear(); - self.metadata = None; - } - - fn push_str(&mut self, value: &str) { - self.content.push_str(value); - } - - fn set_metadata(&mut self, value: serde_json::Value) { - self.metadata = Some(value); - } - - fn is_empty(&self) -> bool { - self.content.is_empty() - } - - fn take_metadata(&mut self) -> Option<serde_json::Value> { - self.metadata.take() - } - - fn take_content(&mut self) -> String { - std::mem::take(&mut self.content) - } -} - -pub(crate) fn notify_message(chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { - trace!("[Chat] save answer: answer={:?}", message); - let pb = ChatMessagePB::from(message); - chat_notification_builder(chat_id, ChatNotification::DidReceiveChatMessage) - .payload(pb) - .send(); - - Ok(()) -} diff --git a/frontend/rust-lib/flowy-ai/src/completion.rs b/frontend/rust-lib/flowy-ai/src/completion.rs deleted file mode 100644 index ffdccd0680..0000000000 --- a/frontend/rust-lib/flowy-ai/src/completion.rs +++ /dev/null @@ -1,206 +0,0 @@ -use crate::entities::{CompleteTextPB, CompleteTextTaskPB, CompletionTypePB}; -use allo_isolate::Isolate; -use std::str::FromStr; - -use dashmap::DashMap; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, CompleteTextParams, CompletionMetadata, CompletionStreamValue, - CompletionType, CustomPrompt, -}; -use flowy_error::{FlowyError, FlowyResult}; - -use futures::{SinkExt, StreamExt}; -use lib_infra::isolate_stream::IsolateSink; - -use crate::stream_message::StreamMessage; -use flowy_ai_pub::user_service::AIUserService; -use std::sync::{Arc, Weak}; -use tokio::select; -use tracing::{error, info}; -use uuid::Uuid; - -pub struct AICompletion { - tasks: Arc<DashMap<String, tokio::sync::mpsc::Sender<()>>>, - cloud_service: Weak<dyn ChatCloudService>, - user_service: Weak<dyn AIUserService>, -} - -impl AICompletion { - pub fn new( - cloud_service: Weak<dyn ChatCloudService>, - user_service: Weak<dyn AIUserService>, - ) -> Self { - Self { - tasks: Arc::new(DashMap::new()), - cloud_service, - user_service, - } - } - - pub async fn create_complete_task( - &self, - complete: CompleteTextPB, - preferred_model: Option<AIModel>, - ) -> FlowyResult<CompleteTextTaskPB> { - if matches!(complete.completion_type, CompletionTypePB::CustomPrompt) - && complete.custom_prompt.is_none() - { - return Err( - FlowyError::invalid_data() - .with_context("custom_prompt is required when completion_type is CustomPrompt"), - ); - } - - let workspace_id = self - .user_service - .upgrade() - .ok_or_else(FlowyError::internal)? - .workspace_id()?; - let (tx, rx) = tokio::sync::mpsc::channel(1); - let task = CompletionTask::new( - workspace_id, - complete, - preferred_model, - self.cloud_service.clone(), - rx, - ); - let task_id = task.task_id.clone(); - self.tasks.insert(task_id.clone(), tx); - - task.start().await; - Ok(CompleteTextTaskPB { task_id }) - } - - pub async fn cancel_complete_task(&self, task_id: &str) { - if let Some(entry) = self.tasks.remove(task_id) { - let _ = entry.1.send(()).await; - } - } -} - -pub struct CompletionTask { - workspace_id: Uuid, - task_id: String, - stop_rx: tokio::sync::mpsc::Receiver<()>, - context: CompleteTextPB, - cloud_service: Weak<dyn ChatCloudService>, - preferred_model: Option<AIModel>, -} - -impl CompletionTask { - pub fn new( - workspace_id: Uuid, - context: CompleteTextPB, - preferred_model: Option<AIModel>, - cloud_service: Weak<dyn ChatCloudService>, - stop_rx: tokio::sync::mpsc::Receiver<()>, - ) -> Self { - Self { - workspace_id, - task_id: uuid::Uuid::new_v4().to_string(), - context, - cloud_service, - stop_rx, - preferred_model, - } - } - - pub async fn start(mut self) { - tokio::spawn(async move { - let mut sink = IsolateSink::new(Isolate::new(self.context.stream_port)); - - if let Some(cloud_service) = self.cloud_service.upgrade() { - let complete_type = match self.context.completion_type { - CompletionTypePB::ImproveWriting => CompletionType::ImproveWriting, - CompletionTypePB::SpellingAndGrammar => CompletionType::SpellingAndGrammar, - CompletionTypePB::MakeShorter => CompletionType::MakeShorter, - CompletionTypePB::MakeLonger => CompletionType::MakeLonger, - CompletionTypePB::ContinueWriting => CompletionType::ContinueWriting, - CompletionTypePB::ExplainSelected => CompletionType::Explain, - CompletionTypePB::UserQuestion => CompletionType::UserQuestion, - CompletionTypePB::CustomPrompt => CompletionType::CustomPrompt, - }; - - let _ = sink.send("start:".to_string()).await; - let completion_history = Some(self.context.history.iter().map(Into::into).collect()); - let format = self.context.format.map(Into::into).unwrap_or_default(); - if let Ok(object_id) = Uuid::from_str(&self.context.object_id) { - let params = CompleteTextParams { - text: self.context.text, - completion_type: Some(complete_type), - metadata: Some(CompletionMetadata { - object_id, - workspace_id: Some(self.workspace_id), - rag_ids: Some(self.context.rag_ids), - completion_history, - custom_prompt: self - .context - .custom_prompt - .map(|v| CustomPrompt { system: v }), - }), - format, - }; - - info!("start completion: {:?}", params); - match cloud_service - .stream_complete(&self.workspace_id, params, self.preferred_model) - .await - { - Ok(mut stream) => loop { - select! { - _ = self.stop_rx.recv() => { - return; - }, - result = stream.next() => { - match result { - Some(Ok(data)) => { - match data { - CompletionStreamValue::Answer{ value } => { - let _ = sink.send(format!("data:{}", value)).await; - } - CompletionStreamValue::Comment{ value } => { - let _ = sink.send(format!("comment:{}", value)).await; - } - } - }, - Some(Err(error)) => { - handle_error(&mut sink, error).await; - return; - }, - None => { - let _ = sink.send(format!("finish:{}", self.task_id)).await; - return; - }, - } - } - } - }, - Err(error) => { - handle_error(&mut sink, error).await; - }, - } - } else { - error!("Invalid uuid: {}", self.context.object_id); - } - } - }); - } -} - -async fn handle_error(sink: &mut IsolateSink, err: FlowyError) { - if err.is_ai_response_limit_exceeded() { - let _ = sink.send("AI_RESPONSE_LIMIT".to_string()).await; - } else if err.is_ai_image_response_limit_exceeded() { - let _ = sink.send("AI_IMAGE_RESPONSE_LIMIT".to_string()).await; - } else if err.is_ai_max_required() { - let _ = sink.send(format!("AI_MAX_REQUIRED:{}", err.msg)).await; - } else if err.is_local_ai_not_ready() { - let _ = sink.send(format!("LOCAL_AI_NOT_READY:{}", err.msg)).await; - } else if err.is_local_ai_disabled() { - let _ = sink.send(format!("LOCAL_AI_DISABLED:{}", err.msg)).await; - } else { - let _ = sink - .send(StreamMessage::OnError(err.msg.clone()).to_string()) - .await; - } -} diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs deleted file mode 100644 index 5a4aecbbd7..0000000000 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ /dev/null @@ -1,750 +0,0 @@ -use crate::local_ai::controller::LocalAISetting; -use crate::local_ai::resource::PendingResource; -use af_plugin::core::plugin::RunningState; -use flowy_ai_pub::cloud::{ - AIModel, ChatMessage, ChatMessageType, CompletionMessage, LLMModel, OutputContent, OutputLayout, - RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, -}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use lib_infra::validator_fn::required_not_empty_str; -use std::collections::HashMap; -use uuid::Uuid; -use validator::Validate; - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct ChatId { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub value: String, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ChatInfoPB { - #[pb(index = 1)] - pub chat_id: String, - - #[pb(index = 2)] - pub files: Vec<FilePB>, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct FilePB { - #[pb(index = 1)] - pub id: String, - #[pb(index = 2)] - pub name: String, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct SendChatPayloadPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub message: String, - - #[pb(index = 3)] - pub message_type: ChatMessageTypePB, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct StreamChatPayloadPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub message: String, - - #[pb(index = 3)] - pub message_type: ChatMessageTypePB, - - #[pb(index = 4)] - pub answer_stream_port: i64, - - #[pb(index = 5)] - pub question_stream_port: i64, - - #[pb(index = 6, one_of)] - pub format: Option<PredefinedFormatPB>, -} - -#[derive(Default, Debug)] -pub struct StreamMessageParams { - pub chat_id: Uuid, - pub message: String, - pub message_type: ChatMessageType, - pub answer_stream_port: i64, - pub question_stream_port: i64, - pub format: Option<PredefinedFormatPB>, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct RegenerateResponsePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, - - #[pb(index = 2)] - pub answer_message_id: i64, - - #[pb(index = 3)] - pub answer_stream_port: i64, - - #[pb(index = 4, one_of)] - pub format: Option<PredefinedFormatPB>, - - #[pb(index = 5, one_of)] - pub model: Option<AIModelPB>, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct ChatMessageMetaPB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub name: String, - - #[pb(index = 3)] - pub data: String, - - #[pb(index = 4)] - pub loader_type: ContextLoaderTypePB, - - #[pb(index = 5)] - pub source: String, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] -pub enum ContextLoaderTypePB { - #[default] - UnknownLoaderType = 0, - Txt = 1, - Markdown = 2, - PDF = 3, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct StopStreamPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] -pub enum ChatMessageTypePB { - #[default] - System = 0, - User = 1, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct LoadPrevChatMessagePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, - - #[pb(index = 2)] - pub limit: i64, - - #[pb(index = 4, one_of)] - pub before_message_id: Option<i64>, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct LoadNextChatMessagePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, - - #[pb(index = 2)] - pub limit: i64, - - #[pb(index = 4, one_of)] - pub after_message_id: Option<i64>, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct ChatMessageListPB { - #[pb(index = 1)] - pub has_more: bool, - - #[pb(index = 2)] - pub messages: Vec<ChatMessagePB>, - - /// If the total number of messages is 0, then the total number of messages is unknown. - #[pb(index = 3)] - pub total: i64, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ServerAvailableModelsPB { - #[pb(index = 1)] - pub models: Vec<AvailableModelPB>, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AvailableModelPB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2)] - pub is_default: bool, - - #[pb(index = 3)] - pub desc: String, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct AvailableModelsQueryPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub source: String, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct UpdateSelectedModelPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub source: String, - - #[pb(index = 2)] - pub selected_model: AIModelPB, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AvailableModelsPB { - #[pb(index = 1)] - pub models: Vec<AIModelPB>, - - #[pb(index = 2)] - pub selected_model: AIModelPB, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct AIModelPB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2)] - pub is_local: bool, - - #[pb(index = 3)] - pub desc: String, -} - -impl From<AIModel> for AIModelPB { - fn from(model: AIModel) -> Self { - Self { - name: model.name, - is_local: model.is_local, - desc: model.desc, - } - } -} - -impl From<AIModelPB> for AIModel { - fn from(value: AIModelPB) -> Self { - AIModel { - name: value.name, - is_local: value.is_local, - desc: value.desc, - } - } -} - -impl From<RepeatedChatMessage> for ChatMessageListPB { - fn from(repeated_chat_message: RepeatedChatMessage) -> Self { - let messages = repeated_chat_message - .messages - .into_iter() - .map(ChatMessagePB::from) - .collect(); - ChatMessageListPB { - has_more: repeated_chat_message.has_more, - messages, - total: repeated_chat_message.total, - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct ChatMessagePB { - #[pb(index = 1)] - pub message_id: i64, - - #[pb(index = 2)] - pub content: String, - - #[pb(index = 3)] - pub created_at: i64, - - #[pb(index = 4)] - pub author_type: i64, - - #[pb(index = 5)] - pub author_id: String, - - #[pb(index = 6, one_of)] - pub reply_message_id: Option<i64>, - - #[pb(index = 7, one_of)] - pub metadata: Option<String>, -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct ChatMessageErrorPB { - #[pb(index = 1)] - pub chat_id: String, - - #[pb(index = 2)] - pub error_message: String, -} - -impl From<ChatMessage> for ChatMessagePB { - fn from(chat_message: ChatMessage) -> Self { - ChatMessagePB { - message_id: chat_message.message_id, - content: chat_message.content, - created_at: chat_message.created_at.timestamp(), - author_type: chat_message.author.author_type as i64, - author_id: chat_message.author.author_id.to_string(), - reply_message_id: None, - metadata: Some(serde_json::to_string(&chat_message.metadata).unwrap_or_default()), - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RepeatedChatMessagePB { - #[pb(index = 1)] - items: Vec<ChatMessagePB>, -} - -impl From<Vec<ChatMessage>> for RepeatedChatMessagePB { - fn from(messages: Vec<ChatMessage>) -> Self { - RepeatedChatMessagePB { - items: messages.into_iter().map(ChatMessagePB::from).collect(), - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct ChatMessageIdPB { - #[pb(index = 1)] - pub chat_id: String, - - #[pb(index = 2)] - pub message_id: i64, -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RelatedQuestionPB { - #[pb(index = 1)] - pub content: String, -} - -impl From<RelatedQuestion> for RelatedQuestionPB { - fn from(value: RelatedQuestion) -> Self { - RelatedQuestionPB { - content: value.content, - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RepeatedRelatedQuestionPB { - #[pb(index = 1)] - pub message_id: i64, - - #[pb(index = 2)] - pub items: Vec<RelatedQuestionPB>, -} - -impl From<RepeatedRelatedQuestion> for RepeatedRelatedQuestionPB { - fn from(value: RepeatedRelatedQuestion) -> Self { - RepeatedRelatedQuestionPB { - message_id: value.message_id, - items: value - .items - .into_iter() - .map(RelatedQuestionPB::from) - .collect(), - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct LLMModelPB { - #[pb(index = 1)] - pub llm_id: i64, - - #[pb(index = 2)] - pub embedding_model: String, - - #[pb(index = 3)] - pub chat_model: String, - - #[pb(index = 4)] - pub requirement: String, - - #[pb(index = 5)] - pub file_size: i64, -} - -impl From<LLMModel> for LLMModelPB { - fn from(value: LLMModel) -> Self { - LLMModelPB { - llm_id: value.llm_id, - embedding_model: value.embedding_model.name, - chat_model: value.chat_model.name, - requirement: value.chat_model.requirements, - file_size: value.chat_model.file_size, - } - } -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct CompleteTextPB { - #[pb(index = 1)] - pub text: String, - - #[pb(index = 2)] - pub completion_type: CompletionTypePB, - - #[pb(index = 3, one_of)] - pub format: Option<PredefinedFormatPB>, - - #[pb(index = 4)] - pub stream_port: i64, - - #[pb(index = 5)] - pub object_id: String, - - #[pb(index = 6)] - pub rag_ids: Vec<String>, - - #[pb(index = 7)] - pub history: Vec<CompletionRecordPB>, - - #[pb(index = 8, one_of)] - pub custom_prompt: Option<String>, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct CompleteTextTaskPB { - #[pb(index = 1)] - pub task_id: String, -} - -#[derive(Clone, Debug, ProtoBuf_Enum, Default)] -pub enum CompletionTypePB { - #[default] - UserQuestion = 0, - ExplainSelected = 1, - ContinueWriting = 2, - SpellingAndGrammar = 3, - ImproveWriting = 4, - MakeShorter = 5, - MakeLonger = 6, - CustomPrompt = 7, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct CompletionRecordPB { - #[pb(index = 1)] - pub role: ChatMessageTypePB, - - #[pb(index = 2)] - pub content: String, -} - -impl From<&CompletionRecordPB> for CompletionMessage { - fn from(value: &CompletionRecordPB) -> Self { - CompletionMessage { - role: match value.role { - // Coerce ChatMessageTypePB::System to AI - ChatMessageTypePB::System => "ai".to_string(), - ChatMessageTypePB::User => "human".to_string(), - }, - content: value.content.clone(), - } - } -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ChatStatePB { - #[pb(index = 1)] - pub model_type: ModelTypePB, - - #[pb(index = 2)] - pub available: bool, -} - -#[derive(Clone, Debug, ProtoBuf_Enum, Default)] -pub enum ModelTypePB { - LocalAI = 0, - #[default] - RemoteAI = 1, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct ChatFilePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub file_path: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalModelStatePB { - #[pb(index = 1)] - pub model_name: String, - - #[pb(index = 2)] - pub model_size: String, - - #[pb(index = 3)] - pub need_download: bool, - - #[pb(index = 4)] - pub requirements: String, - - #[pb(index = 5)] - pub is_downloading: bool, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct PendingResourcePB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2)] - pub file_size: String, - - #[pb(index = 3)] - pub requirements: String, - - #[pb(index = 4)] - pub res_type: PendingResourceTypePB, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] -pub enum PendingResourceTypePB { - #[default] - LocalAIAppRes = 0, - ModelRes = 1, -} - -impl From<PendingResource> for PendingResourceTypePB { - fn from(value: PendingResource) -> Self { - match value { - PendingResource::PluginExecutableNotReady { .. } => PendingResourceTypePB::LocalAIAppRes, - _ => PendingResourceTypePB::ModelRes, - } - } -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] -pub enum RunningStatePB { - #[default] - ReadyToRun = 0, - Connecting = 1, - Connected = 2, - Running = 3, - Stopped = 4, -} - -impl From<RunningState> for RunningStatePB { - fn from(value: RunningState) -> Self { - match value { - RunningState::ReadyToConnect => RunningStatePB::ReadyToRun, - RunningState::Connecting => RunningStatePB::Connecting, - RunningState::Connected { .. } => RunningStatePB::Connected, - RunningState::Running { .. } => RunningStatePB::Running, - RunningState::Stopped { .. } => RunningStatePB::Stopped, - RunningState::UnexpectedStop { .. } => RunningStatePB::Stopped, - } - } -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LocalAIPB { - #[pb(index = 1)] - pub enabled: bool, - - #[pb(index = 2, one_of)] - pub lack_of_resource: Option<LackOfAIResourcePB>, - - #[pb(index = 3)] - pub state: RunningStatePB, - - #[pb(index = 4, one_of)] - pub plugin_version: Option<String>, - - #[pb(index = 5)] - pub plugin_downloaded: bool, -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct CreateChatContextPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub content_type: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub text: String, - - #[pb(index = 3)] - pub metadata: HashMap<String, String>, - - #[pb(index = 4)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_id: String, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ChatSettingsPB { - #[pb(index = 1)] - pub rag_ids: Vec<String>, -} - -#[derive(Default, ProtoBuf, Clone, Debug, Validate)] -pub struct UpdateChatSettingsPB { - #[pb(index = 1)] - #[validate(nested)] - pub chat_id: ChatId, - - #[pb(index = 2)] - pub rag_ids: Vec<String>, - - #[pb(index = 3)] - pub chat_model: String, -} - -#[derive(Debug, Default, Clone, ProtoBuf)] -pub struct PredefinedFormatPB { - #[pb(index = 1)] - pub image_format: ResponseImageFormatPB, - - #[pb(index = 2, one_of)] - pub text_format: Option<ResponseTextFormatPB>, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum)] -pub enum ResponseImageFormatPB { - #[default] - TextOnly = 0, - ImageOnly = 1, - TextAndImage = 2, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum)] -pub enum ResponseTextFormatPB { - #[default] - Paragraph = 0, - BulletedList = 1, - NumberedList = 2, - Table = 3, -} - -impl From<PredefinedFormatPB> for ResponseFormat { - fn from(value: PredefinedFormatPB) -> Self { - Self { - output_layout: match value.text_format { - Some(format) => match format { - ResponseTextFormatPB::Paragraph => OutputLayout::Paragraph, - ResponseTextFormatPB::BulletedList => OutputLayout::BulletList, - ResponseTextFormatPB::NumberedList => OutputLayout::NumberedList, - ResponseTextFormatPB::Table => OutputLayout::SimpleTable, - }, - None => OutputLayout::Paragraph, - }, - output_content: match value.image_format { - ResponseImageFormatPB::TextOnly => OutputContent::TEXT, - ResponseImageFormatPB::ImageOnly => OutputContent::IMAGE, - ResponseImageFormatPB::TextAndImage => OutputContent::RichTextImage, - }, - output_content_metadata: None, - } - } -} - -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] -pub struct LocalAISettingPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub server_url: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub chat_model_name: String, - - #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] - pub embedding_model_name: String, -} - -impl From<LocalAISetting> for LocalAISettingPB { - fn from(value: LocalAISetting) -> Self { - LocalAISettingPB { - server_url: value.ollama_server_url, - chat_model_name: value.chat_model_name, - embedding_model_name: value.embedding_model_name, - } - } -} - -impl From<LocalAISettingPB> for LocalAISetting { - fn from(value: LocalAISettingPB) -> Self { - LocalAISetting { - ollama_server_url: value.server_url, - chat_model_name: value.chat_model_name, - embedding_model_name: value.embedding_model_name, - } - } -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct LackOfAIResourcePB { - #[pb(index = 1)] - pub resource_type: LackOfAIResourceTypePB, - - #[pb(index = 2)] - pub missing_model_names: Vec<String>, -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum)] -pub enum LackOfAIResourceTypePB { - #[default] - PluginExecutableNotReady = 0, - OllamaServerNotReady = 1, - MissingModel = 2, -} - -impl From<PendingResource> for LackOfAIResourcePB { - fn from(value: PendingResource) -> Self { - match value { - PendingResource::PluginExecutableNotReady => Self { - resource_type: LackOfAIResourceTypePB::PluginExecutableNotReady, - missing_model_names: vec![], - }, - PendingResource::OllamaServerNotReady => Self { - resource_type: LackOfAIResourceTypePB::OllamaServerNotReady, - missing_model_names: vec![], - }, - PendingResource::MissingModel(model_name) => Self { - resource_type: LackOfAIResourceTypePB::MissingModel, - missing_model_names: vec![model_name], - }, - } - } -} diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs deleted file mode 100644 index f85858b1c2..0000000000 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ /dev/null @@ -1,352 +0,0 @@ -use crate::ai_manager::{AIManager, GLOBAL_ACTIVE_MODEL_KEY}; -use crate::completion::AICompletion; -use crate::entities::*; -use crate::util::ai_available_models_key; -use flowy_ai_pub::cloud::{AIModel, ChatMessageType}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use std::fs; -use std::path::PathBuf; -use std::str::FromStr; -use std::sync::{Arc, Weak}; -use uuid::Uuid; -use validator::Validate; - -fn upgrade_ai_manager(ai_manager: AFPluginState<Weak<AIManager>>) -> FlowyResult<Arc<AIManager>> { - let ai_manager = ai_manager - .upgrade() - .ok_or(FlowyError::internal().with_context("The chat manager is already dropped"))?; - Ok(ai_manager) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn stream_chat_message_handler( - data: AFPluginData<StreamChatPayloadPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatMessagePB, FlowyError> { - let data = data.into_inner(); - data.validate()?; - - let StreamChatPayloadPB { - chat_id, - message, - message_type, - answer_stream_port, - question_stream_port, - format, - } = data; - - let message_type = match message_type { - ChatMessageTypePB::System => ChatMessageType::System, - ChatMessageTypePB::User => ChatMessageType::User, - }; - - let chat_id = Uuid::from_str(&chat_id)?; - let params = StreamMessageParams { - chat_id, - message, - message_type, - answer_stream_port, - question_stream_port, - format, - }; - - let ai_manager = upgrade_ai_manager(ai_manager)?; - let result = ai_manager.stream_chat_message(params).await?; - data_result_ok(result) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn regenerate_response_handler( - data: AFPluginData<RegenerateResponsePB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> FlowyResult<()> { - let data = data.try_into_inner()?; - let chat_id = Uuid::from_str(&data.chat_id)?; - - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager - .stream_regenerate_response( - &chat_id, - data.answer_message_id, - data.answer_stream_port, - data.format, - data.model, - ) - .await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_server_model_list_handler( - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<AvailableModelsPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let source_key = ai_available_models_key(GLOBAL_ACTIVE_MODEL_KEY); - let models = ai_manager.get_available_models(source_key).await?; - data_result_ok(models) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_chat_models_handler( - data: AFPluginData<AvailableModelsQueryPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<AvailableModelsPB, FlowyError> { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - let models = ai_manager.get_available_models(data.source).await?; - data_result_ok(models) -} - -pub(crate) async fn update_selected_model_handler( - data: AFPluginData<UpdateSelectedModelPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> Result<(), FlowyError> { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager - .update_selected_model(data.source, AIModel::from(data.selected_model)) - .await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn load_prev_message_handler( - data: AFPluginData<LoadPrevChatMessagePB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatMessageListPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let data = data.into_inner(); - data.validate()?; - - let chat_id = Uuid::from_str(&data.chat_id)?; - let messages = ai_manager - .load_prev_chat_messages(&chat_id, data.limit as u64, data.before_message_id) - .await?; - data_result_ok(messages) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn load_next_message_handler( - data: AFPluginData<LoadNextChatMessagePB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatMessageListPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let data = data.into_inner(); - data.validate()?; - - let chat_id = Uuid::from_str(&data.chat_id)?; - let messages = ai_manager - .load_latest_chat_messages(&chat_id, data.limit as u64, data.after_message_id) - .await?; - data_result_ok(messages) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_related_question_handler( - data: AFPluginData<ChatMessageIdPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<RepeatedRelatedQuestionPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let data = data.into_inner(); - let chat_id = Uuid::from_str(&data.chat_id)?; - let messages = ai_manager - .get_related_questions(&chat_id, data.message_id) - .await?; - data_result_ok(messages) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_answer_handler( - data: AFPluginData<ChatMessageIdPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatMessagePB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let data = data.into_inner(); - let chat_id = Uuid::from_str(&data.chat_id)?; - let message = ai_manager - .generate_answer(&chat_id, data.message_id) - .await?; - data_result_ok(message) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn stop_stream_handler( - data: AFPluginData<StopStreamPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> Result<(), FlowyError> { - let data = data.into_inner(); - data.validate()?; - - let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(&data.chat_id)?; - ai_manager.stop_stream(&chat_id).await?; - Ok(()) -} - -pub(crate) async fn start_complete_text_handler( - data: AFPluginData<CompleteTextPB>, - ai_manager: AFPluginState<Weak<AIManager>>, - tools: AFPluginState<Arc<AICompletion>>, -) -> DataResult<CompleteTextTaskPB, FlowyError> { - let data = data.into_inner(); - let ai_manager = upgrade_ai_manager(ai_manager)?; - let ai_model = ai_manager.get_active_model(&data.object_id).await; - let task = tools.create_complete_task(data, ai_model).await?; - data_result_ok(task) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn stop_complete_text_handler( - data: AFPluginData<CompleteTextTaskPB>, - tools: AFPluginState<Arc<AICompletion>>, -) -> Result<(), FlowyError> { - let data = data.into_inner(); - tools.cancel_complete_task(&data.task_id).await; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn chat_file_handler( - data: AFPluginData<ChatFilePB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> Result<(), FlowyError> { - let data = data.try_into_inner()?; - let file_path = PathBuf::from(&data.file_path); - - let allowed_extensions = ["pdf", "md", "txt"]; - let extension = file_path - .extension() - .and_then(|ext| ext.to_str()) - .ok_or_else(|| { - FlowyError::new( - ErrorCode::UnsupportedFileFormat, - "Can't find file extension", - ) - })?; - - if !allowed_extensions.contains(&extension) { - return Err(FlowyError::new( - ErrorCode::UnsupportedFileFormat, - "Only support pdf,md and txt", - )); - } - let file_size = fs::metadata(&file_path) - .map_err(|_| { - FlowyError::new( - ErrorCode::UnsupportedFileFormat, - "Failed to get file metadata", - ) - })? - .len(); - - const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024; - if file_size > MAX_FILE_SIZE { - return Err(FlowyError::new( - ErrorCode::PayloadTooLarge, - "File size is too large. Max file size is 10MB", - )); - } - - tracing::debug!("File size: {} bytes", file_size); - let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(&data.chat_id)?; - ai_manager.chat_with_file(&chat_id, file_path).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn restart_local_ai_handler( - ai_manager: AFPluginState<Weak<AIManager>>, -) -> Result<(), FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.local_ai.restart_plugin().await; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn toggle_local_ai_handler( - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<LocalAIPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.toggle_local_ai().await?; - let state = ai_manager.local_ai.get_local_ai_state().await; - data_result_ok(state) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_local_ai_state_handler( - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<LocalAIPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let state = ai_manager.local_ai.get_local_ai_state().await; - data_result_ok(state) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn create_chat_context_handler( - data: AFPluginData<CreateChatContextPB>, - _ai_manager: AFPluginState<Weak<AIManager>>, -) -> Result<(), FlowyError> { - let _data = data.try_into_inner()?; - - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_chat_info_handler( - data: AFPluginData<ChatId>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatInfoPB, FlowyError> { - let chat_id = data.try_into_inner()?.value; - let ai_manager = upgrade_ai_manager(ai_manager)?; - let pb = ai_manager.get_chat_info(&chat_id).await?; - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_chat_settings_handler( - data: AFPluginData<ChatId>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<ChatSettingsPB, FlowyError> { - let chat_id = data.try_into_inner()?.value; - let chat_id = Uuid::from_str(&chat_id)?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - let rag_ids = ai_manager.get_rag_ids(&chat_id).await?; - let pb = ChatSettingsPB { rag_ids }; - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_chat_settings_handler( - data: AFPluginData<UpdateChatSettingsPB>, - ai_manager: AFPluginState<Weak<AIManager>>, -) -> FlowyResult<()> { - let params = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - let chat_id = Uuid::from_str(¶ms.chat_id.value)?; - ai_manager.update_rag_ids(&chat_id, params.rag_ids).await?; - - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all)] -pub(crate) async fn get_local_ai_setting_handler( - ai_manager: AFPluginState<Weak<AIManager>>, -) -> DataResult<LocalAISettingPB, FlowyError> { - let ai_manager = upgrade_ai_manager(ai_manager)?; - let setting = ai_manager.local_ai.get_local_ai_setting(); - let pb = LocalAISettingPB::from(setting); - data_result_ok(pb) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_local_ai_setting_handler( - ai_manager: AFPluginState<Weak<AIManager>>, - data: AFPluginData<LocalAISettingPB>, -) -> Result<(), FlowyError> { - let data = data.try_into_inner()?; - let ai_manager = upgrade_ai_manager(ai_manager)?; - ai_manager.update_local_ai_setting(data.into()).await?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-ai/src/event_map.rs b/frontend/rust-lib/flowy-ai/src/event_map.rs deleted file mode 100644 index 5020836a30..0000000000 --- a/frontend/rust-lib/flowy-ai/src/event_map.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::sync::{Arc, Weak}; - -use strum_macros::Display; - -use crate::completion::AICompletion; -use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; -use lib_dispatch::prelude::*; - -use crate::ai_manager::AIManager; -use crate::event_handler::*; - -pub fn init(ai_manager: Weak<AIManager>) -> AFPlugin { - let strong_ai_manager = ai_manager.upgrade().unwrap(); - let user_service = Arc::downgrade(&strong_ai_manager.user_service); - let cloud_service = Arc::downgrade(&strong_ai_manager.cloud_service_wm); - let ai_tools = Arc::new(AICompletion::new(cloud_service, user_service)); - AFPlugin::new() - .name("flowy-ai") - .state(ai_manager) - .state(ai_tools) - .event(AIEvent::StreamMessage, stream_chat_message_handler) - .event(AIEvent::LoadPrevMessage, load_prev_message_handler) - .event(AIEvent::LoadNextMessage, load_next_message_handler) - .event(AIEvent::GetRelatedQuestion, get_related_question_handler) - .event(AIEvent::GetAnswerForQuestion, get_answer_handler) - .event(AIEvent::StopStream, stop_stream_handler) - .event(AIEvent::CompleteText, start_complete_text_handler) - .event(AIEvent::StopCompleteText, stop_complete_text_handler) - .event(AIEvent::ChatWithFile, chat_file_handler) - .event(AIEvent::RestartLocalAI, restart_local_ai_handler) - .event(AIEvent::ToggleLocalAI, toggle_local_ai_handler) - .event(AIEvent::GetLocalAIState, get_local_ai_state_handler) - .event(AIEvent::GetLocalAISetting, get_local_ai_setting_handler) - .event( - AIEvent::UpdateLocalAISetting, - update_local_ai_setting_handler, - ) - .event( - AIEvent::GetServerAvailableModels, - get_server_model_list_handler, - ) - .event(AIEvent::CreateChatContext, create_chat_context_handler) - .event(AIEvent::GetChatInfo, create_chat_context_handler) - .event(AIEvent::GetChatSettings, get_chat_settings_handler) - .event(AIEvent::UpdateChatSettings, update_chat_settings_handler) - .event(AIEvent::RegenerateResponse, regenerate_response_handler) - .event(AIEvent::GetAvailableModels, get_chat_models_handler) - .event(AIEvent::UpdateSelectedModel, update_selected_model_handler) -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] -#[event_err = "FlowyError"] -pub enum AIEvent { - /// Create a new workspace - #[event(input = "LoadPrevChatMessagePB", output = "ChatMessageListPB")] - LoadPrevMessage = 0, - - #[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")] - LoadNextMessage = 1, - - #[event(input = "StreamChatPayloadPB", output = "ChatMessagePB")] - StreamMessage = 2, - - #[event(input = "StopStreamPB")] - StopStream = 3, - - #[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")] - GetRelatedQuestion = 4, - - #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] - GetAnswerForQuestion = 5, - - #[event(input = "CompleteTextPB", output = "CompleteTextTaskPB")] - CompleteText = 9, - - #[event(input = "CompleteTextTaskPB")] - StopCompleteText = 10, - - #[event(input = "ChatFilePB")] - ChatWithFile = 11, - - /// Restart local AI chat. When plugin quit or user terminate in task manager or activity monitor, - /// the plugin will need to restart. - #[event()] - RestartLocalAI = 17, - - /// Enable or disable local AI - #[event(output = "LocalAIPB")] - ToggleLocalAI = 18, - - /// Return LocalAIPB that contains the current state of the local AI - #[event(output = "LocalAIPB")] - GetLocalAIState = 19, - - #[event(input = "CreateChatContextPB")] - CreateChatContext = 23, - - #[event(input = "ChatId", output = "ChatInfoPB")] - GetChatInfo = 24, - - #[event(input = "ChatId", output = "ChatSettingsPB")] - GetChatSettings = 25, - - #[event(input = "UpdateChatSettingsPB")] - UpdateChatSettings = 26, - - #[event(input = "RegenerateResponsePB")] - RegenerateResponse = 27, - - #[event(output = "AvailableModelsPB")] - GetServerAvailableModels = 28, - - #[event(output = "LocalAISettingPB")] - GetLocalAISetting = 29, - - #[event(input = "LocalAISettingPB")] - UpdateLocalAISetting = 30, - - #[event(input = "AvailableModelsQueryPB", output = "AvailableModelsPB")] - GetAvailableModels = 31, - - #[event(input = "UpdateSelectedModelPB")] - UpdateSelectedModel = 32, -} diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs deleted file mode 100644 index 5b582b2577..0000000000 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -mod event_handler; -pub mod event_map; - -pub mod ai_manager; -mod chat; -mod completion; -pub mod entities; -pub mod local_ai; - -// #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -// pub mod mcp; - -mod middleware; -pub mod notification; -pub mod offline; -mod protobuf; -mod stream_message; -mod util; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs b/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs deleted file mode 100644 index 1ec08854e0..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/controller.rs +++ /dev/null @@ -1,622 +0,0 @@ -use crate::entities::{LocalAIPB, RunningStatePB}; -use crate::local_ai::resource::{LLMResourceService, LocalAIResourceController}; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use af_plugin::manager::PluginManager; -use anyhow::Error; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; -use futures::Sink; -use lib_infra::async_trait::async_trait; -use std::collections::HashMap; - -use crate::stream_message::StreamMessage; -use af_local_ai::ollama_plugin::OllamaAIPlugin; -use af_plugin::core::path::is_plugin_ready; -use af_plugin::core::plugin::RunningState; -use arc_swap::ArcSwapOption; -use flowy_ai_pub::user_service::AIUserService; -use futures_util::SinkExt; -use lib_infra::util::get_operating_system; -use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use tokio::select; -use tokio_stream::StreamExt; -use tracing::{debug, error, info, instrument, warn}; -use uuid::Uuid; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct LocalAISetting { - pub ollama_server_url: String, - pub chat_model_name: String, - pub embedding_model_name: String, -} - -impl Default for LocalAISetting { - fn default() -> Self { - Self { - ollama_server_url: "http://localhost:11434".to_string(), - chat_model_name: "llama3.1".to_string(), - embedding_model_name: "nomic-embed-text".to_string(), - } - } -} - -const LOCAL_AI_SETTING_KEY: &str = "appflowy_local_ai_setting:v1"; - -pub struct LocalAIController { - ai_plugin: Arc<OllamaAIPlugin>, - resource: Arc<LocalAIResourceController>, - current_chat_id: ArcSwapOption<Uuid>, - store_preferences: Weak<KVStorePreferences>, - user_service: Arc<dyn AIUserService>, -} - -impl Deref for LocalAIController { - type Target = Arc<OllamaAIPlugin>; - - fn deref(&self) -> &Self::Target { - &self.ai_plugin - } -} - -impl LocalAIController { - pub fn new( - plugin_manager: Arc<PluginManager>, - store_preferences: Weak<KVStorePreferences>, - user_service: Arc<dyn AIUserService>, - ) -> Self { - debug!( - "[AI Plugin] init local ai controller, thread: {:?}", - std::thread::current().id() - ); - - // Create the core plugin and resource controller - let local_ai = Arc::new(OllamaAIPlugin::new(plugin_manager)); - let res_impl = LLMResourceServiceImpl { - store_preferences: store_preferences.clone(), - }; - let local_ai_resource = Arc::new(LocalAIResourceController::new( - user_service.clone(), - res_impl, - )); - // Subscribe to state changes - let mut running_state_rx = local_ai.subscribe_running_state(); - - let cloned_llm_res = Arc::clone(&local_ai_resource); - let cloned_store_preferences = store_preferences.clone(); - let cloned_local_ai = Arc::clone(&local_ai); - let cloned_user_service = Arc::clone(&user_service); - - // Spawn a background task to listen for plugin state changes - tokio::spawn(async move { - while let Some(state) = running_state_rx.next().await { - // Skip if we can’t get workspace_id - let Ok(workspace_id) = cloned_user_service.workspace_id() else { - continue; - }; - - let key = local_ai_enabled_key(&workspace_id.to_string()); - info!("[AI Plugin] state: {:?}", state); - - // Read whether plugin is enabled from store; default to true - if let Some(store_preferences) = cloned_store_preferences.upgrade() { - let enabled = store_preferences.get_bool(&key).unwrap_or(true); - // Only check resource status if the plugin isn’t in "UnexpectedStop" and is enabled - let (plugin_downloaded, lack_of_resource) = - if !matches!(state, RunningState::UnexpectedStop { .. }) && enabled { - // Possibly check plugin readiness and resource concurrency in parallel, - // but here we do it sequentially for clarity. - let downloaded = is_plugin_ready(); - let resource_lack = cloned_llm_res.get_lack_of_resource().await; - (downloaded, resource_lack) - } else { - (false, None) - }; - - // If plugin is running, retrieve version - let plugin_version = if matches!(state, RunningState::Running { .. }) { - match cloned_local_ai.plugin_info().await { - Ok(info) => Some(info.version), - Err(_) => None, - } - } else { - None - }; - - // Broadcast the new local AI state - let new_state = RunningStatePB::from(state); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded, - lack_of_resource, - state: new_state, - plugin_version, - }) - .send(); - } else { - warn!("[AI Plugin] store preferences is dropped"); - } - } - }); - - Self { - ai_plugin: local_ai, - resource: local_ai_resource, - current_chat_id: ArcSwapOption::default(), - store_preferences, - user_service, - } - } - #[instrument(level = "debug", skip_all)] - pub async fn observe_plugin_resource(&self) { - let sys = get_operating_system(); - if !sys.is_desktop() { - return; - } - - debug!( - "[AI Plugin] observer plugin state. thread: {:?}", - std::thread::current().id() - ); - async fn try_init_plugin( - resource: &Arc<LocalAIResourceController>, - ai_plugin: &Arc<OllamaAIPlugin>, - ) { - if let Err(err) = initialize_ai_plugin(ai_plugin, resource, None).await { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } - - // Clone what is needed for the background task. - let resource_clone = self.resource.clone(); - let ai_plugin_clone = self.ai_plugin.clone(); - let mut resource_notify = self.resource.subscribe_resource_notify(); - let mut app_state_watcher = self.resource.subscribe_app_state(); - tokio::spawn(async move { - loop { - select! { - _ = app_state_watcher.recv() => { - info!("[AI Plugin] app state changed, try to init plugin"); - try_init_plugin(&resource_clone, &ai_plugin_clone).await; - }, - _ = resource_notify.recv() => { - info!("[AI Plugin] resource changed, try to init plugin"); - try_init_plugin(&resource_clone, &ai_plugin_clone).await; - }, - else => break, - } - } - }); - } - - fn upgrade_store_preferences(&self) -> FlowyResult<Arc<KVStorePreferences>> { - self - .store_preferences - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) - } - - /// Indicate whether the local AI plugin is running. - pub fn is_running(&self) -> bool { - self.ai_plugin.get_plugin_running_state().is_running() - } - - /// Indicate whether the local AI is enabled. - /// AppFlowy store the value in local storage isolated by workspace id. Each workspace can have - /// different settings. - pub fn is_enabled(&self) -> bool { - if !get_operating_system().is_desktop() { - return false; - } - - if let Ok(workspace_id) = self.user_service.workspace_id() { - self.is_enabled_on_workspace(&workspace_id.to_string()) - } else { - false - } - } - - pub fn is_enabled_on_workspace(&self, workspace_id: &str) -> bool { - let key = local_ai_enabled_key(workspace_id); - if !get_operating_system().is_desktop() { - return false; - } - - match self.upgrade_store_preferences() { - Ok(store) => store.get_bool(&key).unwrap_or(false), - Err(_) => false, - } - } - - pub fn get_plugin_chat_model(&self) -> Option<String> { - if !self.is_enabled() { - return None; - } - Some(self.resource.get_llm_setting().chat_model_name) - } - - pub fn open_chat(&self, chat_id: &Uuid) { - if !self.is_enabled() { - return; - } - - // Only keep one chat open at a time. Since loading multiple models at the same time will cause - // memory issues. - if let Some(current_chat_id) = self.current_chat_id.load().as_ref() { - debug!("[AI Plugin] close previous chat: {}", current_chat_id); - self.close_chat(current_chat_id); - } - - self.current_chat_id.store(Some(Arc::new(*chat_id))); - let chat_id = chat_id.to_string(); - let weak_ctrl = Arc::downgrade(&self.ai_plugin); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.create_chat(&chat_id).await { - error!("[AI Plugin] failed to open chat: {:?}", err); - } - } - }); - } - - pub fn close_chat(&self, chat_id: &Uuid) { - if !self.is_running() { - return; - } - info!("[AI Plugin] notify close chat: {}", chat_id); - let weak_ctrl = Arc::downgrade(&self.ai_plugin); - let chat_id = chat_id.to_string(); - tokio::spawn(async move { - if let Some(ctrl) = weak_ctrl.upgrade() { - if let Err(err) = ctrl.close_chat(&chat_id).await { - error!("[AI Plugin] failed to close chat: {:?}", err); - } - } - }); - } - - pub fn get_local_ai_setting(&self) -> LocalAISetting { - self.resource.get_llm_setting() - } - - pub async fn update_local_ai_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - info!( - "[AI Plugin] update local ai setting: {:?}, thread: {:?}", - setting, - std::thread::current().id() - ); - - if self.resource.set_llm_setting(setting).await.is_ok() { - let is_enabled = self.is_enabled(); - self.toggle_plugin(is_enabled).await?; - } - Ok(()) - } - - #[instrument(level = "debug", skip_all)] - pub async fn get_local_ai_state(&self) -> LocalAIPB { - let start = std::time::Instant::now(); - let enabled = self.is_enabled(); - - // If not enabled, return immediately. - if !enabled { - debug!( - "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", - start.elapsed(), - std::thread::current().id() - ); - return LocalAIPB { - enabled, - plugin_downloaded: false, - state: RunningStatePB::from(RunningState::ReadyToConnect), - lack_of_resource: None, - plugin_version: None, - }; - } - - let plugin_downloaded = is_plugin_ready(); - let state = self.ai_plugin.get_plugin_running_state(); - - // If the plugin is running, run both requests in parallel. - // Otherwise, only fetch the resource info. - let (plugin_version, lack_of_resource) = if matches!(state, RunningState::Running { .. }) { - // Launch both futures at once - let plugin_info_fut = self.ai_plugin.plugin_info(); - let resource_fut = self.resource.get_lack_of_resource(); - - let (plugin_info_res, resource_res) = tokio::join!(plugin_info_fut, resource_fut); - let plugin_version = plugin_info_res.ok().map(|info| info.version); - (plugin_version, resource_res) - } else { - let resource_res = self.resource.get_lack_of_resource().await; - (None, resource_res) - }; - - let elapsed = start.elapsed(); - debug!( - "[AI Plugin] get local ai state, elapsed: {:?}, thread: {:?}", - elapsed, - std::thread::current().id() - ); - - LocalAIPB { - enabled, - plugin_downloaded, - state: RunningStatePB::from(state), - lack_of_resource, - plugin_version, - } - } - #[instrument(level = "debug", skip_all)] - pub async fn restart_plugin(&self) { - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, None).await { - error!("[AI Plugin] failed to setup plugin: {:?}", err); - } - } - - pub fn get_model_storage_directory(&self) -> FlowyResult<String> { - self - .resource - .user_model_folder() - .map(|path| path.to_string_lossy().to_string()) - } - - pub async fn toggle_local_ai(&self) -> FlowyResult<bool> { - let workspace_id = self.user_service.workspace_id()?; - let key = local_ai_enabled_key(&workspace_id.to_string()); - let store_preferences = self.upgrade_store_preferences()?; - let enabled = !store_preferences.get_bool(&key).unwrap_or(false); - tracing::trace!("[AI Plugin] toggle local ai, enabled: {}", enabled,); - store_preferences.set_bool(&key, enabled)?; - self.toggle_plugin(enabled).await?; - Ok(enabled) - } - - // #[instrument(level = "debug", skip_all)] - // pub async fn index_message_metadata( - // &self, - // chat_id: &Uuid, - // metadata_list: &[ChatMessageMetadata], - // index_process_sink: &mut (impl Sink<String> + Unpin), - // ) -> FlowyResult<()> { - // if !self.is_enabled() { - // info!("[AI Plugin] local ai is disabled, skip indexing"); - // return Ok(()); - // } - // - // for metadata in metadata_list { - // let mut file_metadata = HashMap::new(); - // file_metadata.insert("id".to_string(), json!(&metadata.id)); - // file_metadata.insert("name".to_string(), json!(&metadata.name)); - // file_metadata.insert("source".to_string(), json!(&metadata.source)); - // - // let file_path = Path::new(&metadata.data.content); - // if !file_path.exists() { - // return Err( - // FlowyError::record_not_found().with_context(format!("File not found: {:?}", file_path)), - // ); - // } - // info!( - // "[AI Plugin] embed file: {:?}, with metadata: {:?}", - // file_path, file_metadata - // ); - // - // match &metadata.data.content_type { - // ContextLoader::Unknown => { - // error!( - // "[AI Plugin] unsupported content type: {:?}", - // metadata.data.content_type - // ); - // }, - // ContextLoader::Text | ContextLoader::Markdown | ContextLoader::PDF => { - // self - // .process_index_file( - // chat_id, - // file_path.to_path_buf(), - // &file_metadata, - // index_process_sink, - // ) - // .await?; - // }, - // } - // } - // - // Ok(()) - // } - - #[allow(dead_code)] - async fn process_index_file( - &self, - chat_id: &Uuid, - file_path: PathBuf, - index_metadata: &HashMap<String, serde_json::Value>, - index_process_sink: &mut (impl Sink<String> + Unpin), - ) -> Result<(), FlowyError> { - let file_name = file_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(); - - let _ = index_process_sink - .send( - StreamMessage::StartIndexFile { - file_name: file_name.clone(), - } - .to_string(), - ) - .await; - - let result = self - .ai_plugin - .embed_file( - &chat_id.to_string(), - file_path, - Some(index_metadata.clone()), - ) - .await; - match result { - Ok(_) => { - let _ = index_process_sink - .send(StreamMessage::EndIndexFile { file_name }.to_string()) - .await; - }, - Err(err) => { - let _ = index_process_sink - .send(StreamMessage::IndexFileError { file_name }.to_string()) - .await; - error!("[AI Plugin] failed to index file: {:?}", err); - }, - } - - Ok(()) - } - - #[instrument(level = "debug", skip_all)] - pub(crate) async fn toggle_plugin(&self, enabled: bool) -> FlowyResult<()> { - info!( - "[AI Plugin] enable: {}, thread id: {:?}", - enabled, - std::thread::current().id() - ); - if enabled { - let (tx, rx) = tokio::sync::oneshot::channel(); - if let Err(err) = initialize_ai_plugin(&self.ai_plugin, &self.resource, Some(tx)).await { - error!("[AI Plugin] failed to initialize local ai: {:?}", err); - } - let _ = rx.await; - } else { - if let Err(err) = self.ai_plugin.destroy_plugin().await { - error!("[AI Plugin] failed to destroy plugin: {:?}", err); - } - - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled, - plugin_downloaded: true, - state: RunningStatePB::Stopped, - lack_of_resource: None, - plugin_version: None, - }) - .send(); - } - Ok(()) - } -} - -#[instrument(level = "debug", skip_all, err)] -async fn initialize_ai_plugin( - plugin: &Arc<OllamaAIPlugin>, - llm_resource: &Arc<LocalAIResourceController>, - ret: Option<tokio::sync::oneshot::Sender<()>>, -) -> FlowyResult<()> { - let lack_of_resource = llm_resource.get_lack_of_resource().await; - - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(LocalAIPB { - enabled: true, - plugin_downloaded: true, - state: RunningStatePB::ReadyToRun, - lack_of_resource: lack_of_resource.clone(), - plugin_version: None, - }) - .send(); - - if let Some(lack_of_resource) = lack_of_resource { - info!( - "[AI Plugin] lack of resource: {:?} to initialize plugin, thread: {:?}", - lack_of_resource, - std::thread::current().id() - ); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::LocalAIResourceUpdated, - ) - .payload(lack_of_resource) - .send(); - - return Ok(()); - } - - if let Err(err) = plugin.destroy_plugin().await { - error!( - "[AI Plugin] failed to destroy plugin when lack of resource: {:?}", - err - ); - } - - let plugin = plugin.clone(); - let cloned_llm_res = llm_resource.clone(); - tokio::task::spawn_blocking(move || { - futures::executor::block_on(async move { - match cloned_llm_res.get_plugin_config(true).await { - Ok(config) => { - info!( - "[AI Plugin] initialize plugin with config: {:?}, thread: {:?}", - config, - std::thread::current().id() - ); - - match plugin.init_plugin(config).await { - Ok(_) => {}, - Err(err) => error!("[AI Plugin] failed to setup plugin: {:?}", err), - } - - if let Some(ret) = ret { - let _ = ret.send(()); - } - }, - Err(err) => { - error!("[AI Plugin] failed to get plugin config: {:?}", err); - }, - }; - }) - }); - - Ok(()) -} - -pub struct LLMResourceServiceImpl { - store_preferences: Weak<KVStorePreferences>, -} - -impl LLMResourceServiceImpl { - fn upgrade_store_preferences(&self) -> FlowyResult<Arc<KVStorePreferences>> { - self - .store_preferences - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Store preferences is dropped")) - } -} -#[async_trait] -impl LLMResourceService for LLMResourceServiceImpl { - fn store_setting(&self, setting: LocalAISetting) -> Result<(), Error> { - let store_preferences = self.upgrade_store_preferences()?; - store_preferences.set_object(LOCAL_AI_SETTING_KEY, &setting)?; - Ok(()) - } - - fn retrieve_setting(&self) -> Option<LocalAISetting> { - let store_preferences = self.upgrade_store_preferences().ok()?; - store_preferences.get_object::<LocalAISetting>(LOCAL_AI_SETTING_KEY) - } -} - -const APPFLOWY_LOCAL_AI_ENABLED: &str = "appflowy_local_ai_enabled"; -fn local_ai_enabled_key(workspace_id: &str) -> String { - format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id) -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs b/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs deleted file mode 100644 index c0fd967d43..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod controller; -mod request; -pub mod resource; - -pub mod stream_util; -pub mod watch; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs b/frontend/rust-lib/flowy-ai/src/local_ai/request.rs deleted file mode 100644 index 6d4bd3289d..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/request.rs +++ /dev/null @@ -1,118 +0,0 @@ -use anyhow::{anyhow, Result}; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; -use reqwest::{Client, Response, StatusCode}; -use sha2::{Digest, Sha256}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::fs::{self, File}; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; - -use tokio_util::sync::CancellationToken; -use tracing::{instrument, trace}; - -#[allow(dead_code)] -type ProgressCallback = Arc<dyn Fn(u64, u64) + Send + Sync>; - -#[instrument(level = "trace", skip_all, err)] -pub async fn download_model( - url: &str, - model_path: &Path, - model_filename: &str, - progress_callback: Option<ProgressCallback>, - cancel_token: Option<CancellationToken>, -) -> Result<PathBuf, anyhow::Error> { - let client = Client::new(); - let mut response = make_request(&client, url, None).await?; - let total_size_in_bytes = response.content_length().unwrap_or(0); - let partial_path = model_path.join(format!("{}.part", model_filename)); - let download_path = model_path.join(model_filename); - let mut part_file = File::create(&partial_path).await?; - let mut downloaded: u64 = 0; - - let debounce_duration = Duration::from_millis(100); - let mut last_update = Instant::now() - .checked_sub(debounce_duration) - .unwrap_or(Instant::now()); - - while let Some(chunk) = response.chunk().await? { - if let Some(cancel_token) = &cancel_token { - if cancel_token.is_cancelled() { - trace!("Download canceled by client"); - fs::remove_file(&partial_path).await?; - return Err(anyhow!("Download canceled")); - } - } - - part_file.write_all(&chunk).await?; - downloaded += chunk.len() as u64; - - if let Some(progress_callback) = &progress_callback { - let now = Instant::now(); - if now.duration_since(last_update) >= debounce_duration { - progress_callback(downloaded, total_size_in_bytes); - last_update = now; - } - } - } - - // Verify file integrity - let header_sha256 = response - .headers() - .get("SHA256") - .and_then(|value| value.to_str().ok()) - .and_then(|value| STANDARD.decode(value).ok()); - - part_file.seek(tokio::io::SeekFrom::Start(0)).await?; - let mut hasher = Sha256::new(); - let block_size = 2_usize.pow(20); // 1 MB - let mut buffer = vec![0; block_size]; - while let Ok(bytes_read) = part_file.read(&mut buffer).await { - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - let calculated_sha256 = hasher.finalize(); - if let Some(header_sha256) = header_sha256 { - if calculated_sha256.as_slice() != header_sha256.as_slice() { - trace!( - "Header Sha256: {:?}, calculated Sha256:{:?}", - header_sha256, - calculated_sha256 - ); - - fs::remove_file(&partial_path).await?; - return Err(anyhow!( - "Sha256 mismatch: expected {:?}, got {:?}", - header_sha256, - calculated_sha256 - )); - } - } - - fs::rename(&partial_path, &download_path).await?; - Ok(download_path) -} - -#[allow(dead_code)] -async fn make_request( - client: &Client, - url: &str, - offset: Option<u64>, -) -> Result<Response, anyhow::Error> { - let mut request = client.get(url); - if let Some(offset) = offset { - println!( - "\nDownload interrupted, resuming from byte position {}", - offset - ); - request = request.header("Range", format!("bytes={}-", offset)); - } - let response = request.send().await?; - if !(response.status().is_success() || response.status() == StatusCode::PARTIAL_CONTENT) { - return Err(anyhow!(response.text().await?)); - } - Ok(response) -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs b/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs deleted file mode 100644 index 36a56e171d..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/resource.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::local_ai::controller::LocalAISetting; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use lib_infra::async_trait::async_trait; - -use crate::entities::LackOfAIResourcePB; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use crate::local_ai::watch::{watch_offline_app, WatchContext}; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use af_local_ai::ollama_plugin::OllamaPluginConfig; -use af_plugin::core::path::{is_plugin_ready, ollama_plugin_path}; -use flowy_ai_pub::user_service::AIUserService; -use lib_infra::util::{get_operating_system, OperatingSystem}; -use reqwest::Client; -use serde::Deserialize; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::Duration; -use tracing::{error, info, instrument, trace}; - -#[derive(Debug, Deserialize)] -struct TagsResponse { - models: Vec<ModelEntry>, -} - -#[derive(Debug, Deserialize)] -struct ModelEntry { - name: String, -} - -#[async_trait] -pub trait LLMResourceService: Send + Sync + 'static { - /// Get local ai configuration from remote server - fn store_setting(&self, setting: LocalAISetting) -> Result<(), anyhow::Error>; - fn retrieve_setting(&self) -> Option<LocalAISetting>; -} - -const LLM_MODEL_DIR: &str = "models"; - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub enum WatchDiskEvent { - Create, - Remove, -} - -#[derive(Debug, Clone)] -pub enum PendingResource { - PluginExecutableNotReady, - OllamaServerNotReady, - MissingModel(String), -} - -pub struct LocalAIResourceController { - user_service: Arc<dyn AIUserService>, - resource_service: Arc<dyn LLMResourceService>, - resource_notify: tokio::sync::broadcast::Sender<()>, - #[cfg(any(target_os = "macos", target_os = "linux"))] - #[allow(dead_code)] - app_disk_watch: Option<WatchContext>, - app_state_sender: tokio::sync::broadcast::Sender<WatchDiskEvent>, -} - -impl LocalAIResourceController { - pub fn new( - user_service: Arc<dyn AIUserService>, - resource_service: impl LLMResourceService, - ) -> Self { - let (resource_notify, _) = tokio::sync::broadcast::channel(1); - let (app_state_sender, _) = tokio::sync::broadcast::channel(1); - #[cfg(any(target_os = "macos", target_os = "linux"))] - let mut offline_app_disk_watch: Option<WatchContext> = None; - - #[cfg(any(target_os = "macos", target_os = "linux"))] - { - match watch_offline_app() { - Ok((new_watcher, mut rx)) => { - let sender = app_state_sender.clone(); - tokio::spawn(async move { - while let Some(event) = rx.recv().await { - if let Err(err) = sender.send(event) { - error!("[LLM Resource] Failed to send offline app state: {:?}", err); - } - } - }); - offline_app_disk_watch = Some(new_watcher); - }, - Err(err) => { - error!("[LLM Resource] Failed to watch offline app path: {:?}", err); - }, - } - } - - Self { - user_service, - resource_service: Arc::new(resource_service), - #[cfg(any(target_os = "macos", target_os = "linux"))] - app_disk_watch: offline_app_disk_watch, - app_state_sender, - resource_notify, - } - } - - pub fn subscribe_resource_notify(&self) -> tokio::sync::broadcast::Receiver<()> { - self.resource_notify.subscribe() - } - - pub fn subscribe_app_state(&self) -> tokio::sync::broadcast::Receiver<WatchDiskEvent> { - self.app_state_sender.subscribe() - } - - /// Returns true when all resources are downloaded and ready to use. - pub async fn is_resource_ready(&self) -> bool { - let sys = get_operating_system(); - if !sys.is_desktop() { - return false; - } - - self - .calculate_pending_resources() - .await - .is_ok_and(|r| r.is_none()) - } - - /// Retrieves model information and updates the current model settings. - pub fn get_llm_setting(&self) -> LocalAISetting { - self.resource_service.retrieve_setting().unwrap_or_default() - } - - #[instrument(level = "info", skip_all, err)] - pub async fn set_llm_setting(&self, setting: LocalAISetting) -> FlowyResult<()> { - self.resource_service.store_setting(setting)?; - if let Some(resource) = self.calculate_pending_resources().await? { - let resource = LackOfAIResourcePB::from(resource); - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::LocalAIResourceUpdated, - ) - .payload(resource.clone()) - .send(); - return Err(FlowyError::local_ai().with_context(format!("{:?}", resource))); - } - Ok(()) - } - - pub async fn get_lack_of_resource(&self) -> Option<LackOfAIResourcePB> { - self - .calculate_pending_resources() - .await - .ok()? - .map(Into::into) - } - - pub async fn calculate_pending_resources(&self) -> FlowyResult<Option<PendingResource>> { - let app_path = ollama_plugin_path(); - if !is_plugin_ready() { - trace!("[LLM Resource] offline app not found: {:?}", app_path); - return Ok(Some(PendingResource::PluginExecutableNotReady)); - } - - let setting = self.get_llm_setting(); - let client = Client::builder().timeout(Duration::from_secs(5)).build()?; - - match client.get(&setting.ollama_server_url).send().await { - Ok(resp) if resp.status().is_success() => { - info!( - "[LLM Resource] Ollama server is running at {}", - setting.ollama_server_url - ); - }, - _ => { - info!( - "[LLM Resource] Ollama server is not responding at {}", - setting.ollama_server_url - ); - return Ok(Some(PendingResource::OllamaServerNotReady)); - }, - } - - let required_models = vec![setting.chat_model_name, setting.embedding_model_name]; - - // Query the /api/tags endpoint to get a structured list of locally available models. - let tags_url = format!("{}/api/tags", setting.ollama_server_url); - - match client.get(&tags_url).send().await { - Ok(resp) if resp.status().is_success() => { - let tags: TagsResponse = resp.json().await.inspect_err(|e| { - log::error!("[LLM Resource] Failed to parse /api/tags JSON response: {e:?}") - })?; - // Check if each of our required models exists in the list of available models - trace!("[LLM Resource] ollama available models: {:?}", tags.models); - for required in &required_models { - if !tags - .models - .iter() - .any(|m| m.name == *required || m.name == format!("{}:latest", required)) - { - log::trace!( - "[LLM Resource] required model '{}' not found in API response", - required - ); - return Ok(Some(PendingResource::MissingModel(required.clone()))); - } - } - }, - _ => { - error!( - "[LLM Resource] Failed to fetch models from {} (GET /api/tags)", - setting.ollama_server_url - ); - return Ok(Some(PendingResource::OllamaServerNotReady)); - }, - } - - Ok(None) - } - - #[instrument(level = "info", skip_all)] - pub async fn get_plugin_config(&self, rag_enabled: bool) -> FlowyResult<OllamaPluginConfig> { - if !self.is_resource_ready().await { - return Err(FlowyError::new( - ErrorCode::AppFlowyLAINotReady, - "AppFlowyLAI not found", - )); - } - - let llm_setting = self.get_llm_setting(); - let bin_path = match get_operating_system() { - OperatingSystem::MacOS | OperatingSystem::Windows | OperatingSystem::Linux => { - ollama_plugin_path() - }, - _ => { - return Err( - FlowyError::local_ai_unavailable() - .with_context("Local AI not available on current platform"), - ); - }, - }; - - let mut config = OllamaPluginConfig::new( - bin_path, - "af_ollama_plugin".to_string(), - llm_setting.chat_model_name.clone(), - llm_setting.embedding_model_name.clone(), - Some(llm_setting.ollama_server_url.clone()), - )?; - - //config = config.with_log_level("debug".to_string()); - - if rag_enabled { - let resource_dir = self.resource_dir()?; - let persist_directory = resource_dir.join("vectorstore"); - if !persist_directory.exists() { - std::fs::create_dir_all(&persist_directory)?; - } - config.set_rag_enabled(&persist_directory)?; - } - - if cfg!(debug_assertions) { - config = config.with_verbose(true); - } - trace!("[AI Chat] config: {:?}", config); - Ok(config) - } - - pub(crate) fn user_model_folder(&self) -> FlowyResult<PathBuf> { - self.resource_dir().map(|dir| dir.join(LLM_MODEL_DIR)) - } - - pub(crate) fn resource_dir(&self) -> FlowyResult<PathBuf> { - let user_data_dir = self.user_service.application_root_dir()?; - Ok(user_data_dir.join("ai")) - } -} - -#[allow(dead_code)] -fn bytes_to_readable_format(bytes: u64) -> String { - const BYTES_IN_GIGABYTE: u64 = 1024 * 1024 * 1024; - const BYTES_IN_MEGABYTE: u64 = 1024 * 1024; - - if bytes >= BYTES_IN_GIGABYTE { - let gigabytes = (bytes as f64) / (BYTES_IN_GIGABYTE as f64); - format!("{:.1} GB", gigabytes) - } else { - let megabytes = (bytes as f64) / (BYTES_IN_MEGABYTE as f64); - format!("{:.2} MB", megabytes) - } -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs b/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs deleted file mode 100644 index fbe4157c8c..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/stream_util.rs +++ /dev/null @@ -1,65 +0,0 @@ -use af_plugin::error::PluginError; - -use flowy_ai_pub::cloud::QuestionStreamValue; -use flowy_error::FlowyError; -use futures::{ready, Stream}; -use pin_project::pin_project; -use serde_json::Value; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tracing::error; - -pub const STEAM_METADATA_KEY: &str = "0"; -pub const STEAM_ANSWER_KEY: &str = "1"; - -#[pin_project] -pub struct QuestionStream { - stream: Pin<Box<dyn Stream<Item = Result<Value, PluginError>> + Send>>, - buffer: Vec<u8>, -} - -impl QuestionStream { - pub fn new<S>(stream: S) -> Self - where - S: Stream<Item = Result<Value, PluginError>> + Send + 'static, - { - QuestionStream { - stream: Box::pin(stream), - buffer: Vec::new(), - } - } -} - -impl Stream for QuestionStream { - type Item = Result<QuestionStreamValue, FlowyError>; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { - let this = self.project(); - - match ready!(this.stream.as_mut().poll_next(cx)) { - Some(Ok(value)) => match value { - Value::Object(mut value) => { - if let Some(metadata) = value.remove(STEAM_METADATA_KEY) { - return Poll::Ready(Some(Ok(QuestionStreamValue::Metadata { value: metadata }))); - } - - if let Some(answer) = value - .remove(STEAM_ANSWER_KEY) - .and_then(|s| s.as_str().map(ToString::to_string)) - { - return Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: answer }))); - } - - error!("Invalid streaming value: {:?}", value); - Poll::Ready(None) - }, - _ => { - error!("Unexpected JSON value type: {:?}", value); - Poll::Ready(None) - }, - }, - Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))), - None => Poll::Ready(None), - } - } -} diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs b/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs deleted file mode 100644 index 2baed3f0a5..0000000000 --- a/frontend/rust-lib/flowy-ai/src/local_ai/watch.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::local_ai::resource::WatchDiskEvent; -use af_plugin::core::path::{install_path, ollama_plugin_path}; -use flowy_error::{FlowyError, FlowyResult}; -use std::path::PathBuf; -use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}; -use tracing::{error, trace}; - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -#[allow(dead_code)] -pub struct WatchContext { - watcher: notify::RecommendedWatcher, - pub path: PathBuf, -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -#[allow(dead_code)] -pub fn watch_offline_app() -> FlowyResult<(WatchContext, UnboundedReceiver<WatchDiskEvent>)> { - use notify::{Event, Watcher}; - - let install_path = install_path().ok_or_else(|| { - FlowyError::internal().with_context("Unsupported platform for offline app watching") - })?; - let (tx, rx) = unbounded_channel(); - let app_path = ollama_plugin_path(); - let mut watcher = notify::recommended_watcher(move |res: Result<Event, _>| match res { - Ok(event) => { - if event.paths.iter().any(|path| path == &app_path) { - trace!("watch event: {:?}", event); - match event.kind { - notify::EventKind::Create(_) => { - if let Err(err) = tx.send(WatchDiskEvent::Create) { - error!("watch send error: {:?}", err) - } - }, - notify::EventKind::Remove(_) => { - if let Err(err) = tx.send(WatchDiskEvent::Remove) { - error!("watch send error: {:?}", err) - } - }, - _ => { - trace!("unhandle watch event: {:?}", event); - }, - } - } - }, - Err(e) => error!("watch error: {:?}", e), - }) - .map_err(|err| FlowyError::internal().with_context(err))?; - watcher - .watch(&install_path, notify::RecursiveMode::NonRecursive) - .map_err(|err| FlowyError::internal().with_context(err))?; - - Ok(( - WatchContext { - watcher, - path: install_path, - }, - rx, - )) -} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs b/frontend/rust-lib/flowy-ai/src/mcp/manager.rs deleted file mode 100644 index 9e40a51f68..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/manager.rs +++ /dev/null @@ -1,39 +0,0 @@ -use af_mcp::client::{MCPClient, MCPServerConfig}; -use af_mcp::entities::ToolsList; -use dashmap::DashMap; -use flowy_error::FlowyError; -use std::sync::Arc; - -pub struct MCPClientManager { - stdio_clients: Arc<DashMap<String, MCPClient>>, -} - -impl MCPClientManager { - pub fn new() -> MCPClientManager { - Self { - stdio_clients: Arc::new(DashMap::new()), - } - } - - pub async fn connect_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { - let client = MCPClient::new_stdio(config.clone()).await?; - self.stdio_clients.insert(config.server_cmd, client.clone()); - client.initialize().await?; - Ok(()) - } - - pub async fn remove_server(&self, config: MCPServerConfig) -> Result<(), FlowyError> { - let client = self.stdio_clients.remove(&config.server_cmd); - if let Some((_, mut client)) = client { - client.stop().await?; - } - Ok(()) - } - - pub async fn tool_list(&self, server_cmd: &str) -> Option<ToolsList> { - let client = self.stdio_clients.get(server_cmd)?; - let tools = client.list_tools().await.ok(); - tracing::trace!("{}: tool list: {:?}", server_cmd, tools); - tools - } -} diff --git a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs b/frontend/rust-lib/flowy-ai/src/mcp/mod.rs deleted file mode 100644 index 8f73c8326c..0000000000 --- a/frontend/rust-lib/flowy-ai/src/mcp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod manager; diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs deleted file mode 100644 index 74f5d5560b..0000000000 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ /dev/null @@ -1,375 +0,0 @@ -use crate::entities::{ChatStatePB, ModelTypePB}; -use crate::local_ai::controller::LocalAIController; -use crate::notification::{ - chat_notification_builder, ChatNotification, APPFLOWY_AI_NOTIFICATION_KEY, -}; -use af_plugin::error::PluginError; -use flowy_ai_pub::persistence::select_message_content; -use std::collections::HashMap; - -use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, - ChatSettings, CompleteTextParams, CompletionStream, MessageCursor, ModelList, RelatedQuestion, - RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, StreamAnswer, StreamComplete, - UpdateChatParams, -}; -use flowy_error::{FlowyError, FlowyResult}; -use futures::{stream, StreamExt, TryStreamExt}; -use lib_infra::async_trait::async_trait; - -use crate::local_ai::stream_util::QuestionStream; -use flowy_ai_pub::user_service::AIUserService; -use flowy_storage_pub::storage::StorageService; -use serde_json::{json, Value}; -use std::path::Path; -use std::sync::{Arc, Weak}; -use tracing::{info, trace}; -use uuid::Uuid; - -pub struct ChatServiceMiddleware { - cloud_service: Arc<dyn ChatCloudService>, - user_service: Arc<dyn AIUserService>, - local_ai: Arc<LocalAIController>, - #[allow(dead_code)] - storage_service: Weak<dyn StorageService>, -} - -impl ChatServiceMiddleware { - pub fn new( - user_service: Arc<dyn AIUserService>, - cloud_service: Arc<dyn ChatCloudService>, - local_ai: Arc<LocalAIController>, - storage_service: Weak<dyn StorageService>, - ) -> Self { - Self { - user_service, - cloud_service, - local_ai, - storage_service, - } - } - - fn get_message_content(&self, message_id: i64) -> FlowyResult<String> { - let uid = self.user_service.user_id()?; - let conn = self.user_service.sqlite_connection(uid)?; - let content = select_message_content(conn, message_id)?.ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) - })?; - Ok(content) - } - - fn handle_plugin_error(&self, err: PluginError) { - if matches!( - err, - PluginError::PluginNotConnected | PluginError::PeerDisconnect - ) { - chat_notification_builder( - APPFLOWY_AI_NOTIFICATION_KEY, - ChatNotification::UpdateLocalAIState, - ) - .payload(ChatStatePB { - model_type: ModelTypePB::LocalAI, - available: false, - }) - .send(); - } - } -} - -#[async_trait] -impl ChatCloudService for ChatServiceMiddleware { - async fn create_chat( - &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - name: &str, - metadata: serde_json::Value, - ) -> Result<(), FlowyError> { - self - .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) - .await - } - - async fn create_question( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError> { - self - .cloud_service - .create_question(workspace_id, chat_id, message, message_type) - .await - } - - async fn create_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option<serde_json::Value>, - ) -> Result<ChatMessage, FlowyError> { - self - .cloud_service - .create_answer(workspace_id, chat_id, message, question_id, metadata) - .await - } - - async fn stream_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, - ) -> Result<StreamAnswer, FlowyError> { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; - - info!("stream_answer use model: {:?}", ai_model); - if use_local_ai { - if self.local_ai.is_running() { - let content = self.get_message_content(question_id)?; - match self - .local_ai - .stream_question( - &chat_id.to_string(), - &content, - Some(json!(format)), - json!({}), - ) - .await - { - Ok(stream) => Ok(QuestionStream::new(stream).boxed()), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } else { - self - .cloud_service - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) - .await - } - } - - async fn get_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError> { - if self.local_ai.is_running() { - let content = self.get_message_content(question_id)?; - match self - .local_ai - .ask_question(&chat_id.to_string(), &content) - .await - { - Ok(answer) => { - let message = self - .cloud_service - .create_answer(workspace_id, chat_id, &answer, question_id, None) - .await?; - Ok(message) - }, - Err(err) => { - self.handle_plugin_error(err); - Err(FlowyError::local_ai_unavailable()) - }, - } - } else { - self - .cloud_service - .get_answer(workspace_id, chat_id, question_id) - .await - } - } - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError> { - self - .cloud_service - .get_chat_messages(workspace_id, chat_id, offset, limit) - .await - } - - async fn get_question_from_answer_id( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError> { - self - .cloud_service - .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) - .await - } - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError> { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; - - if use_local_ai { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); - - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, - }) - .collect::<Vec<_>>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) - } else { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], - }) - } - } else { - self - .cloud_service - .get_related_message(workspace_id, chat_id, message_id, ai_model) - .await - } - } - - async fn stream_complete( - &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError> { - let use_local_ai = match &ai_model { - None => false, - Some(model) => model.is_local, - }; - - info!("stream_complete use custom model: {:?}", ai_model); - if use_local_ai { - if self.local_ai.is_running() { - match self - .local_ai - .complete_text_v2( - ¶ms.text, - params.completion_type.unwrap() as u8, - Some(json!(params.format)), - Some(json!(params.metadata)), - ) - .await - { - Ok(stream) => Ok( - CompletionStream::new( - stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), - ) - .map_err(FlowyError::from) - .boxed(), - ), - Err(err) => { - self.handle_plugin_error(err); - Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()) - }, - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } else { - self - .cloud_service - .stream_complete(workspace_id, params, ai_model) - .await - } - } - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError> { - if self.local_ai.is_running() { - self - .local_ai - .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - Ok(()) - } else { - self - .cloud_service - .embed_file(workspace_id, file_path, chat_id, metadata) - .await - } - } - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError> { - self - .cloud_service - .get_chat_settings(workspace_id, chat_id) - .await - } - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError> { - self - .cloud_service - .update_chat_settings(workspace_id, chat_id, params) - .await - } - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> { - self.cloud_service.get_available_models(workspace_id).await - } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - self - .cloud_service - .get_workspace_default_model(workspace_id) - .await - } -} diff --git a/frontend/rust-lib/flowy-ai/src/middleware/mod.rs b/frontend/rust-lib/flowy-ai/src/middleware/mod.rs deleted file mode 100644 index e1c0f454da..0000000000 --- a/frontend/rust-lib/flowy-ai/src/middleware/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod chat_service_mw; diff --git a/frontend/rust-lib/flowy-ai/src/notification.rs b/frontend/rust-lib/flowy-ai/src/notification.rs deleted file mode 100644 index 6fbf3a8e7a..0000000000 --- a/frontend/rust-lib/flowy-ai/src/notification.rs +++ /dev/null @@ -1,51 +0,0 @@ -use flowy_derive::ProtoBuf_Enum; -use flowy_notification::NotificationBuilder; -use tracing::trace; - -const CHAT_OBSERVABLE_SOURCE: &str = "Chat"; -pub const APPFLOWY_AI_NOTIFICATION_KEY: &str = "appflowy_ai_plugin"; -#[derive(ProtoBuf_Enum, Debug, Default)] -pub enum ChatNotification { - #[default] - Unknown = 0, - DidLoadLatestChatMessage = 1, - DidLoadPrevChatMessage = 2, - DidReceiveChatMessage = 3, - StreamChatMessageError = 4, - FinishStreaming = 5, - UpdateLocalAIState = 6, - DidUpdateChatSettings = 7, - LocalAIResourceUpdated = 8, - DidUpdateSelectedModel = 9, -} - -impl std::convert::From<ChatNotification> for i32 { - fn from(notification: ChatNotification) -> Self { - notification as i32 - } -} -impl std::convert::From<i32> for ChatNotification { - fn from(notification: i32) -> Self { - match notification { - 1 => ChatNotification::DidLoadLatestChatMessage, - 2 => ChatNotification::DidLoadPrevChatMessage, - 3 => ChatNotification::DidReceiveChatMessage, - 4 => ChatNotification::StreamChatMessageError, - 5 => ChatNotification::FinishStreaming, - 6 => ChatNotification::UpdateLocalAIState, - 7 => ChatNotification::DidUpdateChatSettings, - 8 => ChatNotification::LocalAIResourceUpdated, - _ => ChatNotification::Unknown, - } - } -} - -#[tracing::instrument(level = "trace", skip_all)] -pub(crate) fn chat_notification_builder<T: ToString>( - id: T, - ty: ChatNotification, -) -> NotificationBuilder { - let id = id.to_string(); - trace!("chat_notification_builder: id = {id}, ty = {ty:?}"); - NotificationBuilder::new(&id, ty, CHAT_OBSERVABLE_SOURCE) -} diff --git a/frontend/rust-lib/flowy-ai/src/offline/mod.rs b/frontend/rust-lib/flowy-ai/src/offline/mod.rs deleted file mode 100644 index e55b43fdb2..0000000000 --- a/frontend/rust-lib/flowy-ai/src/offline/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod offline_message_sync; diff --git a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs b/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs deleted file mode 100644 index 55daf6b77f..0000000000 --- a/frontend/rust-lib/flowy-ai/src/offline/offline_message_sync.rs +++ /dev/null @@ -1,258 +0,0 @@ -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, - MessageCursor, ModelList, RepeatedChatMessage, RepeatedRelatedQuestion, ResponseFormat, - StreamAnswer, StreamComplete, UpdateChatParams, -}; -use flowy_ai_pub::persistence::{ - update_chat_is_sync, update_chat_message_is_sync, upsert_chat, upsert_chat_messages, - ChatMessageTable, ChatTable, -}; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::sync::Arc; -use uuid::Uuid; - -pub struct AutoSyncChatService { - cloud_service: Arc<dyn ChatCloudService>, - user_service: Arc<dyn AIUserService>, -} - -impl AutoSyncChatService { - pub fn new( - cloud_service: Arc<dyn ChatCloudService>, - user_service: Arc<dyn AIUserService>, - ) -> Self { - Self { - cloud_service, - user_service, - } - } - - async fn upsert_message( - &self, - chat_id: &Uuid, - message: ChatMessage, - is_sync: bool, - ) -> Result<(), FlowyError> { - let uid = self.user_service.user_id()?; - let conn = self.user_service.sqlite_connection(uid)?; - let row = ChatMessageTable::from_message(chat_id.to_string(), message, is_sync); - upsert_chat_messages(conn, &[row])?; - Ok(()) - } - - #[allow(dead_code)] - async fn update_message_is_sync( - &self, - chat_id: &Uuid, - message_id: i64, - ) -> Result<(), FlowyError> { - let uid = self.user_service.user_id()?; - let conn = self.user_service.sqlite_connection(uid)?; - update_chat_message_is_sync(conn, &chat_id.to_string(), message_id, true)?; - Ok(()) - } -} - -#[async_trait] -impl ChatCloudService for AutoSyncChatService { - async fn create_chat( - &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - name: &str, - metadata: Value, - ) -> Result<(), FlowyError> { - let conn = self.user_service.sqlite_connection(*uid)?; - let chat = ChatTable::new( - chat_id.to_string(), - metadata.clone(), - rag_ids.clone(), - false, - ); - upsert_chat(conn, &chat)?; - - if self - .cloud_service - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) - .await - .is_ok() - { - let conn = self.user_service.sqlite_connection(*uid)?; - update_chat_is_sync(conn, &chat_id.to_string(), true)?; - } - Ok(()) - } - - async fn create_question( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError> { - let message = self - .cloud_service - .create_question(workspace_id, chat_id, message, message_type) - .await?; - self.upsert_message(chat_id, message.clone(), true).await?; - // TODO: implement background sync - // self - // .update_message_is_sync(chat_id, message.message_id) - // .await?; - Ok(message) - } - - async fn create_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option<Value>, - ) -> Result<ChatMessage, FlowyError> { - let message = self - .cloud_service - .create_answer(workspace_id, chat_id, message, question_id, metadata) - .await?; - - // TODO: implement background sync - self.upsert_message(chat_id, message.clone(), true).await?; - Ok(message) - } - - async fn stream_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, - ) -> Result<StreamAnswer, FlowyError> { - self - .cloud_service - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) - .await - } - - async fn get_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let message = self - .cloud_service - .get_answer(workspace_id, chat_id, question_id) - .await?; - - // TODO: implement background sync - self.upsert_message(chat_id, message.clone(), true).await?; - Ok(message) - } - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError> { - self - .cloud_service - .get_chat_messages(workspace_id, chat_id, offset, limit) - .await - } - - async fn get_question_from_answer_id( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError> { - self - .cloud_service - .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) - .await - } - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError> { - self - .cloud_service - .get_related_message(workspace_id, chat_id, message_id, ai_model) - .await - } - - async fn stream_complete( - &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError> { - self - .cloud_service - .stream_complete(workspace_id, params, ai_model) - .await - } - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError> { - self - .cloud_service - .embed_file(workspace_id, file_path, chat_id, metadata) - .await - } - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError> { - // TODO: implement background sync - self - .cloud_service - .get_chat_settings(workspace_id, chat_id) - .await - } - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError> { - // TODO: implement background sync - self - .cloud_service - .update_chat_settings(workspace_id, chat_id, params) - .await - } - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> { - self.cloud_service.get_available_models(workspace_id).await - } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - self - .cloud_service - .get_workspace_default_model(workspace_id) - .await - } -} diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs deleted file mode 100644 index 3f7b37bd34..0000000000 --- a/frontend/rust-lib/flowy-ai/src/stream_message.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::fmt::Display; - -#[allow(dead_code)] -pub enum StreamMessage { - MessageId(i64), - IndexStart, - IndexEnd, - Text(String), - OnData(String), - OnError(String), - Metadata(String), - Done, - StartIndexFile { file_name: String }, - EndIndexFile { file_name: String }, - IndexFileError { file_name: String }, -} - -impl Display for StreamMessage { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StreamMessage::MessageId(message_id) => write!(f, "message_id:{}", message_id), - StreamMessage::IndexStart => write!(f, "index_start:"), - StreamMessage::IndexEnd => write!(f, "index_end"), - StreamMessage::Text(text) => { - write!(f, "data:{}", text) - }, - StreamMessage::OnData(message) => write!(f, "data:{message}"), - StreamMessage::OnError(message) => write!(f, "error:{message}"), - StreamMessage::Done => write!(f, "done:"), - StreamMessage::Metadata(s) => write!(f, "metadata:{s}"), - StreamMessage::StartIndexFile { file_name } => { - write!(f, "start_index_file:{}", file_name) - }, - StreamMessage::EndIndexFile { file_name } => { - write!(f, "end_index_file:{}", file_name) - }, - StreamMessage::IndexFileError { file_name } => { - write!(f, "index_file_error:{}", file_name) - }, - } - } -} diff --git a/frontend/rust-lib/flowy-ai/src/util.rs b/frontend/rust-lib/flowy-ai/src/util.rs deleted file mode 100644 index a181d1b1d3..0000000000 --- a/frontend/rust-lib/flowy-ai/src/util.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn ai_available_models_key(object_id: &str) -> String { - format!("ai_models_{}", object_id) -} diff --git a/frontend/rust-lib/flowy-chat-pub/Cargo.toml b/frontend/rust-lib/flowy-chat-pub/Cargo.toml new file mode 100644 index 0000000000..f320fb2133 --- /dev/null +++ b/frontend/rust-lib/flowy-chat-pub/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "flowy-chat-pub" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lib-infra = { workspace = true } +flowy-error = { workspace = true } +client-api = { workspace = true } +bytes.workspace = true +futures.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-chat-pub/src/cloud.rs b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs new file mode 100644 index 0000000000..8ab60069a5 --- /dev/null +++ b/frontend/rust-lib/flowy-chat-pub/src/cloud.rs @@ -0,0 +1,75 @@ +use bytes::Bytes; +pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage}; +pub use client_api::entity::{ + ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage, +}; +use client_api::error::AppResponseError; +use flowy_error::FlowyError; +use futures::stream::BoxStream; +use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; + +pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>; +pub type StreamAnswer = BoxStream<'static, Result<Bytes, AppResponseError>>; +#[async_trait] +pub trait ChatCloudService: Send + Sync + 'static { + fn create_chat( + &self, + uid: &i64, + workspace_id: &str, + chat_id: &str, + ) -> FutureResult<(), FlowyError>; + + async fn send_chat_message( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + ) -> Result<ChatMessageStream, FlowyError>; + + fn send_question( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + ) -> FutureResult<ChatMessage, FlowyError>; + + fn save_answer( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + question_id: i64, + ) -> FutureResult<ChatMessage, FlowyError>; + + async fn stream_answer( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> Result<StreamAnswer, FlowyError>; + + fn get_chat_messages( + &self, + workspace_id: &str, + chat_id: &str, + offset: MessageCursor, + limit: u64, + ) -> FutureResult<RepeatedChatMessage, FlowyError>; + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult<RepeatedRelatedQuestion, FlowyError>; + + fn generate_answer( + &self, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, + ) -> FutureResult<ChatMessage, FlowyError>; +} diff --git a/frontend/rust-lib/flowy-chat-pub/src/lib.rs b/frontend/rust-lib/flowy-chat-pub/src/lib.rs new file mode 100644 index 0000000000..1ede32218e --- /dev/null +++ b/frontend/rust-lib/flowy-chat-pub/src/lib.rs @@ -0,0 +1 @@ +pub mod cloud; diff --git a/frontend/rust-lib/flowy-chat/Cargo.toml b/frontend/rust-lib/flowy-chat/Cargo.toml new file mode 100644 index 0000000000..1abbb62a17 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "flowy-chat" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +flowy-derive.workspace = true +flowy-notification = { workspace = true } +flowy-error = { path = "../flowy-error", features = [ + "impl_from_dispatch_error", + "impl_from_collab_folder", +] } +lib-dispatch = { workspace = true } +tracing.workspace = true +uuid.workspace = true +strum_macros = "0.21" +protobuf.workspace = true +bytes.workspace = true +validator = { workspace = true, features = ["derive"] } +lib-infra = { workspace = true, features = ["isolate_flutter"] } +flowy-chat-pub.workspace = true +dashmap = "5.5" +flowy-sqlite = { workspace = true } +tokio.workspace = true +futures.workspace = true +allo-isolate = { version = "^0.1", features = ["catch-unwind"] } +log = "0.4.21" + +[build-dependencies] +flowy-codegen.workspace = true + +[features] +dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] diff --git a/frontend/rust-lib/flowy-ai/Flowy.toml b/frontend/rust-lib/flowy-chat/Flowy.toml similarity index 100% rename from frontend/rust-lib/flowy-ai/Flowy.toml rename to frontend/rust-lib/flowy-chat/Flowy.toml diff --git a/frontend/rust-lib/flowy-chat/build.rs b/frontend/rust-lib/flowy-chat/build.rs new file mode 100644 index 0000000000..fac4cc65ae --- /dev/null +++ b/frontend/rust-lib/flowy-chat/build.rs @@ -0,0 +1,40 @@ +fn main() { + #[cfg(feature = "dart")] + { + flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); + } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } + + #[cfg(feature = "web_ts")] + { + flowy_codegen::ts_event::gen( + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + } +} diff --git a/frontend/rust-lib/flowy-chat/src/chat.rs b/frontend/rust-lib/flowy-chat/src/chat.rs new file mode 100644 index 0000000000..cb32c342c8 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/chat.rs @@ -0,0 +1,452 @@ +use crate::entities::{ + ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB, +}; +use crate::manager::ChatUserService; +use crate::notification::{send_notification, ChatNotification}; +use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; +use allo_isolate::Isolate; +use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageType, MessageCursor}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::DBConnection; +use futures::{SinkExt, StreamExt}; +use lib_infra::isolate_stream::IsolateSink; +use std::sync::atomic::{AtomicBool, AtomicI64}; +use std::sync::Arc; +use tokio::sync::{Mutex, RwLock}; +use tracing::{error, instrument, trace}; + +enum PrevMessageState { + HasMore, + NoMore, + Loading, +} + +pub struct Chat { + chat_id: String, + uid: i64, + user_service: Arc<dyn ChatUserService>, + cloud_service: Arc<dyn ChatCloudService>, + prev_message_state: Arc<RwLock<PrevMessageState>>, + latest_message_id: Arc<AtomicI64>, + stop_stream: Arc<AtomicBool>, + steam_buffer: Arc<Mutex<String>>, +} + +impl Chat { + pub fn new( + uid: i64, + chat_id: String, + user_service: Arc<dyn ChatUserService>, + cloud_service: Arc<dyn ChatCloudService>, + ) -> Chat { + Chat { + uid, + chat_id, + cloud_service, + user_service, + prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)), + latest_message_id: Default::default(), + stop_stream: Arc::new(AtomicBool::new(false)), + steam_buffer: Arc::new(Mutex::new("".to_string())), + } + } + + pub fn close(&self) {} + + #[allow(dead_code)] + pub async fn pull_latest_message(&self, limit: i64) { + let latest_message_id = self + .latest_message_id + .load(std::sync::atomic::Ordering::Relaxed); + if latest_message_id > 0 { + let _ = self + .load_remote_chat_messages(limit, None, Some(latest_message_id)) + .await; + } + } + + pub async fn stop_stream_message(&self) { + self + .stop_stream + .store(true, std::sync::atomic::Ordering::SeqCst); + } + + #[instrument(level = "info", skip_all, err)] + pub async fn stream_chat_message( + &self, + message: &str, + message_type: ChatMessageType, + text_stream_port: i64, + ) -> Result<ChatMessagePB, FlowyError> { + if message.len() > 2000 { + return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length")); + } + // clear + self + .stop_stream + .store(false, std::sync::atomic::Ordering::SeqCst); + self.steam_buffer.lock().await.clear(); + + let stream_buffer = self.steam_buffer.clone(); + let uid = self.user_service.user_id()?; + let workspace_id = self.user_service.workspace_id()?; + + let question = self + .cloud_service + .send_question(&workspace_id, &self.chat_id, message, message_type) + .await + .map_err(|err| { + error!("Failed to send question: {}", err); + FlowyError::server_error() + })?; + + save_chat_message( + self.user_service.sqlite_connection(uid)?, + &self.chat_id, + vec![question.clone()], + )?; + + let stop_stream = self.stop_stream.clone(); + let chat_id = self.chat_id.clone(); + let question_id = question.message_id; + let cloud_service = self.cloud_service.clone(); + let user_service = self.user_service.clone(); + tokio::spawn(async move { + let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port)); + match cloud_service + .stream_answer(&workspace_id, &chat_id, question_id) + .await + { + Ok(mut stream) => { + while let Some(message) = stream.next().await { + match message { + Ok(message) => { + if stop_stream.load(std::sync::atomic::Ordering::Relaxed) { + trace!("[Chat] stop streaming message"); + break; + } + let s = String::from_utf8(message.to_vec()).unwrap_or_default(); + stream_buffer.lock().await.push_str(&s); + let _ = text_sink.send(format!("data:{}", s)).await; + }, + Err(err) => { + error!("[Chat] failed to stream answer: {}", err); + let _ = text_sink.send(format!("error:{}", err)).await; + let pb = ChatMessageErrorPB { + chat_id: chat_id.clone(), + error_message: err.to_string(), + }; + send_notification(&chat_id, ChatNotification::StreamChatMessageError) + .payload(pb) + .send(); + break; + }, + } + } + }, + Err(err) => { + let pb = ChatMessageErrorPB { + chat_id: chat_id.clone(), + error_message: err.to_string(), + }; + send_notification(&chat_id, ChatNotification::StreamChatMessageError) + .payload(pb) + .send(); + }, + } + + send_notification(&chat_id, ChatNotification::FinishStreaming).send(); + let answer = cloud_service + .save_answer( + &workspace_id, + &chat_id, + &stream_buffer.lock().await, + question_id, + ) + .await?; + Self::save_answer(uid, &chat_id, &user_service, answer)?; + + Ok::<(), FlowyError>(()) + }); + + let question_pb = ChatMessagePB::from(question); + Ok(question_pb) + } + + fn save_answer( + uid: i64, + chat_id: &str, + user_service: &Arc<dyn ChatUserService>, + answer: ChatMessage, + ) -> Result<(), FlowyError> { + save_chat_message( + user_service.sqlite_connection(uid)?, + chat_id, + vec![answer.clone()], + )?; + let pb = ChatMessagePB::from(answer); + send_notification(chat_id, ChatNotification::DidReceiveChatMessage) + .payload(pb) + .send(); + + Ok(()) + } + + /// Load chat messages for a given `chat_id`. + /// + /// 1. When opening a chat: + /// - Loads local chat messages. + /// - `after_message_id` and `before_message_id` are `None`. + /// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded. + /// + /// 2. Loading more messages in an existing chat with `after_message_id`: + /// - `after_message_id` is the last message ID in the current chat messages. + /// + /// 3. Loading more messages in an existing chat with `before_message_id`: + /// - `before_message_id` is the first message ID in the current chat messages. + pub async fn load_prev_chat_messages( + &self, + limit: i64, + before_message_id: Option<i64>, + ) -> Result<ChatMessageListPB, FlowyError> { + trace!( + "[Chat] Loading messages from disk: chat_id={}, limit={}, before_message_id={:?}", + self.chat_id, + limit, + before_message_id + ); + let messages = self + .load_local_chat_messages(limit, None, before_message_id) + .await?; + + // If the number of messages equals the limit, then no need to load more messages from remote + if messages.len() == limit as usize { + let pb = ChatMessageListPB { + messages, + has_more: true, + total: 0, + }; + send_notification(&self.chat_id, ChatNotification::DidLoadPrevChatMessage) + .payload(pb.clone()) + .send(); + return Ok(pb); + } + + if matches!( + *self.prev_message_state.read().await, + PrevMessageState::HasMore + ) { + *self.prev_message_state.write().await = PrevMessageState::Loading; + if let Err(err) = self + .load_remote_chat_messages(limit, before_message_id, None) + .await + { + error!("Failed to load previous chat messages: {}", err); + } + } + + Ok(ChatMessageListPB { + messages, + has_more: true, + total: 0, + }) + } + + pub async fn load_latest_chat_messages( + &self, + limit: i64, + after_message_id: Option<i64>, + ) -> Result<ChatMessageListPB, FlowyError> { + trace!( + "[Chat] Loading new messages: chat_id={}, limit={}, after_message_id={:?}", + self.chat_id, + limit, + after_message_id, + ); + let messages = self + .load_local_chat_messages(limit, after_message_id, None) + .await?; + + trace!( + "[Chat] Loaded local chat messages: chat_id={}, messages={}", + self.chat_id, + messages.len() + ); + + // If the number of messages equals the limit, then no need to load more messages from remote + let has_more = !messages.is_empty(); + let _ = self + .load_remote_chat_messages(limit, None, after_message_id) + .await; + Ok(ChatMessageListPB { + messages, + has_more, + total: 0, + }) + } + + async fn load_remote_chat_messages( + &self, + limit: i64, + before_message_id: Option<i64>, + after_message_id: Option<i64>, + ) -> FlowyResult<()> { + trace!( + "[Chat] start loading messages from remote: chat_id={}, limit={}, before_message_id={:?}, after_message_id={:?}", + self.chat_id, + limit, + before_message_id, + after_message_id + ); + let workspace_id = self.user_service.workspace_id()?; + let chat_id = self.chat_id.clone(); + let cloud_service = self.cloud_service.clone(); + let user_service = self.user_service.clone(); + let uid = self.uid; + let prev_message_state = self.prev_message_state.clone(); + let latest_message_id = self.latest_message_id.clone(); + tokio::spawn(async move { + let cursor = match (before_message_id, after_message_id) { + (Some(bid), _) => MessageCursor::BeforeMessageId(bid), + (_, Some(aid)) => MessageCursor::AfterMessageId(aid), + _ => MessageCursor::NextBack, + }; + match cloud_service + .get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit as u64) + .await + { + Ok(resp) => { + // Save chat messages to local disk + if let Err(err) = save_chat_message( + user_service.sqlite_connection(uid)?, + &chat_id, + resp.messages.clone(), + ) { + error!("Failed to save chat:{} messages: {}", chat_id, err); + } + + // Update latest message ID + if !resp.messages.is_empty() { + latest_message_id.store( + resp.messages[0].message_id, + std::sync::atomic::Ordering::Relaxed, + ); + } + + let pb = ChatMessageListPB::from(resp); + trace!( + "[Chat] Loaded messages from remote: chat_id={}, messages={}, hasMore: {}, cursor:{:?}", + chat_id, + pb.messages.len(), + pb.has_more, + cursor, + ); + if matches!(cursor, MessageCursor::BeforeMessageId(_)) { + if pb.has_more { + *prev_message_state.write().await = PrevMessageState::HasMore; + } else { + *prev_message_state.write().await = PrevMessageState::NoMore; + } + send_notification(&chat_id, ChatNotification::DidLoadPrevChatMessage) + .payload(pb) + .send(); + } else { + send_notification(&chat_id, ChatNotification::DidLoadLatestChatMessage) + .payload(pb) + .send(); + } + }, + Err(err) => error!("Failed to load chat messages: {}", err), + } + Ok::<(), FlowyError>(()) + }); + Ok(()) + } + + pub async fn get_related_question( + &self, + message_id: i64, + ) -> Result<RepeatedRelatedQuestionPB, FlowyError> { + let workspace_id = self.user_service.workspace_id()?; + let resp = self + .cloud_service + .get_related_message(&workspace_id, &self.chat_id, message_id) + .await?; + + trace!( + "[Chat] related messages: chat_id={}, message_id={}, messages:{:?}", + self.chat_id, + message_id, + resp.items + ); + Ok(RepeatedRelatedQuestionPB::from(resp)) + } + + #[instrument(level = "debug", skip_all, err)] + pub async fn generate_answer(&self, question_message_id: i64) -> FlowyResult<ChatMessagePB> { + trace!( + "[Chat] generate answer: chat_id={}, question_message_id={}", + self.chat_id, + question_message_id + ); + let workspace_id = self.user_service.workspace_id()?; + let answer = self + .cloud_service + .generate_answer(&workspace_id, &self.chat_id, question_message_id) + .await?; + + Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?; + let pb = ChatMessagePB::from(answer); + Ok(pb) + } + + async fn load_local_chat_messages( + &self, + limit: i64, + after_message_id: Option<i64>, + before_message_id: Option<i64>, + ) -> Result<Vec<ChatMessagePB>, FlowyError> { + let conn = self.user_service.sqlite_connection(self.uid)?; + let records = select_chat_messages( + conn, + &self.chat_id, + limit, + after_message_id, + before_message_id, + )?; + let messages = records + .into_iter() + .map(|record| ChatMessagePB { + message_id: record.message_id, + content: record.content, + created_at: record.created_at, + author_type: record.author_type, + author_id: record.author_id, + reply_message_id: record.reply_message_id, + }) + .collect::<Vec<_>>(); + + Ok(messages) + } +} + +fn save_chat_message( + conn: DBConnection, + chat_id: &str, + messages: Vec<ChatMessage>, +) -> FlowyResult<()> { + let records = messages + .into_iter() + .map(|message| ChatMessageTable { + message_id: message.message_id, + chat_id: chat_id.to_string(), + content: message.content, + created_at: message.created_at.timestamp(), + author_type: message.author.author_type as i64, + author_id: message.author.author_id.to_string(), + reply_message_id: message.reply_message_id, + }) + .collect::<Vec<_>>(); + insert_chat_messages(conn, &records)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-chat/src/entities.rs b/frontend/rust-lib/flowy-chat/src/entities.rs new file mode 100644 index 0000000000..4ef687c3c4 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/entities.rs @@ -0,0 +1,207 @@ +use flowy_chat_pub::cloud::{ + ChatMessage, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion, +}; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use lib_infra::validator_fn::required_not_empty_str; +use validator::Validate; + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct SendChatPayloadPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub message: String, + + #[pb(index = 3)] + pub message_type: ChatMessageTypePB, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct StreamChatPayloadPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub message: String, + + #[pb(index = 3)] + pub message_type: ChatMessageTypePB, + + #[pb(index = 4)] + pub text_stream_port: i64, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct StopStreamPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, +} + +#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] +pub enum ChatMessageTypePB { + #[default] + System = 0, + User = 1, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct LoadPrevChatMessagePB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, + + #[pb(index = 2)] + pub limit: i64, + + #[pb(index = 4, one_of)] + pub before_message_id: Option<i64>, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct LoadNextChatMessagePB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub chat_id: String, + + #[pb(index = 2)] + pub limit: i64, + + #[pb(index = 4, one_of)] + pub after_message_id: Option<i64>, +} + +#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +pub struct ChatMessageListPB { + #[pb(index = 1)] + pub has_more: bool, + + #[pb(index = 2)] + pub messages: Vec<ChatMessagePB>, + + /// If the total number of messages is 0, then the total number of messages is unknown. + #[pb(index = 3)] + pub total: i64, +} + +impl From<RepeatedChatMessage> for ChatMessageListPB { + fn from(repeated_chat_message: RepeatedChatMessage) -> Self { + let messages = repeated_chat_message + .messages + .into_iter() + .map(ChatMessagePB::from) + .collect(); + ChatMessageListPB { + has_more: repeated_chat_message.has_more, + messages, + total: repeated_chat_message.total, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct ChatMessagePB { + #[pb(index = 1)] + pub message_id: i64, + + #[pb(index = 2)] + pub content: String, + + #[pb(index = 3)] + pub created_at: i64, + + #[pb(index = 4)] + pub author_type: i64, + + #[pb(index = 5)] + pub author_id: String, + + #[pb(index = 6, one_of)] + pub reply_message_id: Option<i64>, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct ChatMessageErrorPB { + #[pb(index = 1)] + pub chat_id: String, + + #[pb(index = 2)] + pub error_message: String, +} + +impl From<ChatMessage> for ChatMessagePB { + fn from(chat_message: ChatMessage) -> Self { + ChatMessagePB { + message_id: chat_message.message_id, + content: chat_message.content, + created_at: chat_message.created_at.timestamp(), + author_type: chat_message.author.author_type as i64, + author_id: chat_message.author.author_id.to_string(), + reply_message_id: None, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RepeatedChatMessagePB { + #[pb(index = 1)] + items: Vec<ChatMessagePB>, +} + +impl From<Vec<ChatMessage>> for RepeatedChatMessagePB { + fn from(messages: Vec<ChatMessage>) -> Self { + RepeatedChatMessagePB { + items: messages.into_iter().map(ChatMessagePB::from).collect(), + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct ChatMessageIdPB { + #[pb(index = 1)] + pub chat_id: String, + + #[pb(index = 2)] + pub message_id: i64, +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RelatedQuestionPB { + #[pb(index = 1)] + pub content: String, +} + +impl From<RelatedQuestion> for RelatedQuestionPB { + fn from(value: RelatedQuestion) -> Self { + RelatedQuestionPB { + content: value.content, + } + } +} + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct RepeatedRelatedQuestionPB { + #[pb(index = 1)] + pub message_id: i64, + + #[pb(index = 2)] + pub items: Vec<RelatedQuestionPB>, +} + +impl From<RepeatedRelatedQuestion> for RepeatedRelatedQuestionPB { + fn from(value: RepeatedRelatedQuestion) -> Self { + RepeatedRelatedQuestionPB { + message_id: value.message_id, + items: value + .items + .into_iter() + .map(RelatedQuestionPB::from) + .collect(), + } + } +} diff --git a/frontend/rust-lib/flowy-chat/src/event_handler.rs b/frontend/rust-lib/flowy-chat/src/event_handler.rs new file mode 100644 index 0000000000..1d4499c6b2 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/event_handler.rs @@ -0,0 +1,112 @@ +use flowy_chat_pub::cloud::ChatMessageType; +use std::sync::{Arc, Weak}; +use validator::Validate; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; + +use crate::entities::*; +use crate::manager::ChatManager; + +fn upgrade_chat_manager( + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> FlowyResult<Arc<ChatManager>> { + let chat_manager = chat_manager + .upgrade() + .ok_or(FlowyError::internal().with_context("The chat manager is already dropped"))?; + Ok(chat_manager) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn stream_chat_message_handler( + data: AFPluginData<StreamChatPayloadPB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> DataResult<ChatMessagePB, FlowyError> { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + data.validate()?; + + let message_type = match data.message_type { + ChatMessageTypePB::System => ChatMessageType::System, + ChatMessageTypePB::User => ChatMessageType::User, + }; + + let question = chat_manager + .stream_chat_message( + &data.chat_id, + &data.message, + message_type, + data.text_stream_port, + ) + .await?; + data_result_ok(question) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn load_prev_message_handler( + data: AFPluginData<LoadPrevChatMessagePB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> DataResult<ChatMessageListPB, FlowyError> { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + data.validate()?; + + let messages = chat_manager + .load_prev_chat_messages(&data.chat_id, data.limit, data.before_message_id) + .await?; + data_result_ok(messages) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn load_next_message_handler( + data: AFPluginData<LoadNextChatMessagePB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> DataResult<ChatMessageListPB, FlowyError> { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + data.validate()?; + + let messages = chat_manager + .load_latest_chat_messages(&data.chat_id, data.limit, data.after_message_id) + .await?; + data_result_ok(messages) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_related_question_handler( + data: AFPluginData<ChatMessageIdPB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> DataResult<RepeatedRelatedQuestionPB, FlowyError> { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + let messages = chat_manager + .get_related_questions(&data.chat_id, data.message_id) + .await?; + data_result_ok(messages) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_answer_handler( + data: AFPluginData<ChatMessageIdPB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> DataResult<ChatMessagePB, FlowyError> { + let chat_manager = upgrade_chat_manager(chat_manager)?; + let data = data.into_inner(); + let message = chat_manager + .generate_answer(&data.chat_id, data.message_id) + .await?; + data_result_ok(message) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn stop_stream_handler( + data: AFPluginData<StopStreamPB>, + chat_manager: AFPluginState<Weak<ChatManager>>, +) -> Result<(), FlowyError> { + let data = data.into_inner(); + data.validate()?; + + let chat_manager = upgrade_chat_manager(chat_manager)?; + chat_manager.stop_stream(&data.chat_id).await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-chat/src/event_map.rs b/frontend/rust-lib/flowy-chat/src/event_map.rs new file mode 100644 index 0000000000..e3b7828936 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/event_map.rs @@ -0,0 +1,44 @@ +use std::sync::Weak; + +use strum_macros::Display; + +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use lib_dispatch::prelude::*; + +use crate::event_handler::*; +use crate::manager::ChatManager; + +pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin { + AFPlugin::new() + .name("Flowy-Chat") + .state(chat_manager) + .event(ChatEvent::StreamMessage, stream_chat_message_handler) + .event(ChatEvent::LoadPrevMessage, load_prev_message_handler) + .event(ChatEvent::LoadNextMessage, load_next_message_handler) + .event(ChatEvent::GetRelatedQuestion, get_related_question_handler) + .event(ChatEvent::GetAnswerForQuestion, get_answer_handler) + .event(ChatEvent::StopStream, stop_stream_handler) +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] +#[event_err = "FlowyError"] +pub enum ChatEvent { + /// Create a new workspace + #[event(input = "LoadPrevChatMessagePB", output = "ChatMessageListPB")] + LoadPrevMessage = 0, + + #[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")] + LoadNextMessage = 1, + + #[event(input = "StreamChatPayloadPB", output = "ChatMessagePB")] + StreamMessage = 2, + + #[event(input = "StopStreamPB")] + StopStream = 3, + + #[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")] + GetRelatedQuestion = 4, + + #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] + GetAnswerForQuestion = 5, +} diff --git a/frontend/rust-lib/flowy-chat/src/lib.rs b/frontend/rust-lib/flowy-chat/src/lib.rs new file mode 100644 index 0000000000..2244af5802 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/lib.rs @@ -0,0 +1,9 @@ +mod event_handler; +pub mod event_map; + +mod chat; +pub mod entities; +pub mod manager; +pub mod notification; +mod persistence; +mod protobuf; diff --git a/frontend/rust-lib/flowy-chat/src/manager.rs b/frontend/rust-lib/flowy-chat/src/manager.rs new file mode 100644 index 0000000000..b72cdfb87d --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/manager.rs @@ -0,0 +1,190 @@ +use crate::chat::Chat; +use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB}; +use crate::persistence::{insert_chat, ChatTable}; +use dashmap::DashMap; +use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType}; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::DBConnection; +use lib_infra::util::timestamp; +use std::sync::Arc; +use tracing::trace; + +pub trait ChatUserService: Send + Sync + 'static { + fn user_id(&self) -> Result<i64, FlowyError>; + fn device_id(&self) -> Result<String, FlowyError>; + fn workspace_id(&self) -> Result<String, FlowyError>; + fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>; +} + +pub struct ChatManager { + cloud_service: Arc<dyn ChatCloudService>, + user_service: Arc<dyn ChatUserService>, + chats: Arc<DashMap<String, Arc<Chat>>>, +} + +impl ChatManager { + pub fn new( + cloud_service: Arc<dyn ChatCloudService>, + user_service: impl ChatUserService, + ) -> ChatManager { + let user_service = Arc::new(user_service); + + Self { + cloud_service, + user_service, + chats: Arc::new(DashMap::new()), + } + } + + pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> { + trace!("open chat: {}", chat_id); + self.chats.entry(chat_id.to_string()).or_insert_with(|| { + Arc::new(Chat::new( + self.user_service.user_id().unwrap(), + chat_id.to_string(), + self.user_service.clone(), + self.cloud_service.clone(), + )) + }); + + Ok(()) + } + + pub async fn close_chat(&self, _chat_id: &str) -> Result<(), FlowyError> { + Ok(()) + } + + pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> { + if let Some((_, chat)) = self.chats.remove(chat_id) { + chat.close(); + } + Ok(()) + } + + pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result<Arc<Chat>, FlowyError> { + let workspace_id = self.user_service.workspace_id()?; + self + .cloud_service + .create_chat(uid, &workspace_id, chat_id) + .await?; + save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?; + + let chat = Arc::new(Chat::new( + self.user_service.user_id().unwrap(), + chat_id.to_string(), + self.user_service.clone(), + self.cloud_service.clone(), + )); + self.chats.insert(chat_id.to_string(), chat.clone()); + Ok(chat) + } + + pub async fn stream_chat_message( + &self, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + text_stream_port: i64, + ) -> Result<ChatMessagePB, FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let question = chat + .stream_chat_message(message, message_type, text_stream_port) + .await?; + Ok(question) + } + + pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> { + let chat = self.chats.get(chat_id).as_deref().cloned(); + match chat { + None => { + let chat = Arc::new(Chat::new( + self.user_service.user_id().unwrap(), + chat_id.to_string(), + self.user_service.clone(), + self.cloud_service.clone(), + )); + self.chats.insert(chat_id.to_string(), chat.clone()); + Ok(chat) + }, + Some(chat) => Ok(chat), + } + } + + /// Load chat messages for a given `chat_id`. + /// + /// 1. When opening a chat: + /// - Loads local chat messages. + /// - `after_message_id` and `before_message_id` are `None`. + /// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded. + /// + /// 2. Loading more messages in an existing chat with `after_message_id`: + /// - `after_message_id` is the last message ID in the current chat messages. + /// + /// 3. Loading more messages in an existing chat with `before_message_id`: + /// - `before_message_id` is the first message ID in the current chat messages. + /// + /// 4. `after_message_id` and `before_message_id` cannot be specified at the same time. + + pub async fn load_prev_chat_messages( + &self, + chat_id: &str, + limit: i64, + before_message_id: Option<i64>, + ) -> Result<ChatMessageListPB, FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let list = chat + .load_prev_chat_messages(limit, before_message_id) + .await?; + Ok(list) + } + + pub async fn load_latest_chat_messages( + &self, + chat_id: &str, + limit: i64, + after_message_id: Option<i64>, + ) -> Result<ChatMessageListPB, FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let list = chat + .load_latest_chat_messages(limit, after_message_id) + .await?; + Ok(list) + } + + pub async fn get_related_questions( + &self, + chat_id: &str, + message_id: i64, + ) -> Result<RepeatedRelatedQuestionPB, FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let resp = chat.get_related_question(message_id).await?; + Ok(resp) + } + + pub async fn generate_answer( + &self, + chat_id: &str, + question_message_id: i64, + ) -> Result<ChatMessagePB, FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + let resp = chat.generate_answer(question_message_id).await?; + Ok(resp) + } + + pub async fn stop_stream(&self, chat_id: &str) -> Result<(), FlowyError> { + let chat = self.get_or_create_chat_instance(chat_id).await?; + chat.stop_stream_message().await; + Ok(()) + } +} + +fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { + let row = ChatTable { + chat_id: chat_id.to_string(), + created_at: timestamp(), + name: "".to_string(), + }; + + insert_chat(conn, &row)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-chat/src/notification.rs b/frontend/rust-lib/flowy-chat/src/notification.rs new file mode 100644 index 0000000000..12f0470784 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/notification.rs @@ -0,0 +1,38 @@ +use flowy_derive::ProtoBuf_Enum; +use flowy_notification::NotificationBuilder; + +const CHAT_OBSERVABLE_SOURCE: &str = "Chat"; + +#[derive(ProtoBuf_Enum, Debug, Default)] +pub enum ChatNotification { + #[default] + Unknown = 0, + DidLoadLatestChatMessage = 1, + DidLoadPrevChatMessage = 2, + DidReceiveChatMessage = 3, + StreamChatMessageError = 4, + FinishStreaming = 5, +} + +impl std::convert::From<ChatNotification> for i32 { + fn from(notification: ChatNotification) -> Self { + notification as i32 + } +} +impl std::convert::From<i32> for ChatNotification { + fn from(notification: i32) -> Self { + match notification { + 1 => ChatNotification::DidLoadLatestChatMessage, + 2 => ChatNotification::DidLoadPrevChatMessage, + 3 => ChatNotification::DidReceiveChatMessage, + 4 => ChatNotification::StreamChatMessageError, + 5 => ChatNotification::FinishStreaming, + _ => ChatNotification::Unknown, + } + } +} + +#[tracing::instrument(level = "trace")] +pub(crate) fn send_notification(id: &str, ty: ChatNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, CHAT_OBSERVABLE_SOURCE) +} diff --git a/frontend/rust-lib/flowy-chat/src/persistence/chat_message_sql.rs b/frontend/rust-lib/flowy-chat/src/persistence/chat_message_sql.rs new file mode 100644 index 0000000000..6d9202def0 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/persistence/chat_message_sql.rs @@ -0,0 +1,71 @@ +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::upsert::excluded; +use flowy_sqlite::{ + diesel, insert_into, + query_dsl::*, + schema::{chat_message_table, chat_message_table::dsl}, + DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, +}; + +#[derive(Queryable, Insertable, Identifiable)] +#[diesel(table_name = chat_message_table)] +#[diesel(primary_key(message_id))] +pub struct ChatMessageTable { + pub message_id: i64, + pub chat_id: String, + pub content: String, + pub created_at: i64, + pub author_type: i64, + pub author_id: String, + pub reply_message_id: Option<i64>, +} + +pub fn insert_chat_messages( + mut conn: DBConnection, + new_messages: &[ChatMessageTable], +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + for message in new_messages { + let _ = insert_into(chat_message_table::table) + .values(message) + .on_conflict(chat_message_table::message_id) + .do_update() + .set(( + chat_message_table::content.eq(excluded(chat_message_table::content)), + chat_message_table::created_at.eq(excluded(chat_message_table::created_at)), + chat_message_table::author_type.eq(excluded(chat_message_table::author_type)), + chat_message_table::author_id.eq(excluded(chat_message_table::author_id)), + chat_message_table::reply_message_id.eq(excluded(chat_message_table::reply_message_id)), + )) + .execute(conn)?; + } + Ok::<(), FlowyError>(()) + })?; + + Ok(()) +} + +pub fn select_chat_messages( + mut conn: DBConnection, + chat_id_val: &str, + limit_val: i64, + after_message_id: Option<i64>, + before_message_id: Option<i64>, +) -> QueryResult<Vec<ChatMessageTable>> { + let mut query = dsl::chat_message_table + .filter(chat_message_table::chat_id.eq(chat_id_val)) + .into_boxed(); + if let Some(after_message_id) = after_message_id { + query = query.filter(chat_message_table::message_id.gt(after_message_id)); + } + + if let Some(before_message_id) = before_message_id { + query = query.filter(chat_message_table::message_id.lt(before_message_id)); + } + query = query + .order((chat_message_table::message_id.desc(),)) + .limit(limit_val); + + let messages: Vec<ChatMessageTable> = query.load::<ChatMessageTable>(&mut *conn)?; + Ok(messages) +} diff --git a/frontend/rust-lib/flowy-chat/src/persistence/chat_sql.rs b/frontend/rust-lib/flowy-chat/src/persistence/chat_sql.rs new file mode 100644 index 0000000000..1fd0480c54 --- /dev/null +++ b/frontend/rust-lib/flowy-chat/src/persistence/chat_sql.rs @@ -0,0 +1,52 @@ +use flowy_sqlite::upsert::excluded; +use flowy_sqlite::{ + diesel, + query_dsl::*, + schema::{chat_table, chat_table::dsl}, + DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable, +}; + +#[derive(Clone, Default, Queryable, Insertable, Identifiable)] +#[diesel(table_name = chat_table)] +#[diesel(primary_key(chat_id))] +pub struct ChatTable { + pub chat_id: String, + pub created_at: i64, + pub name: String, +} + +pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<usize> { + diesel::insert_into(chat_table::table) + .values(new_chat) + .on_conflict(chat_table::chat_id) + .do_update() + .set(( + chat_table::created_at.eq(excluded(chat_table::created_at)), + chat_table::name.eq(excluded(chat_table::name)), + )) + .execute(&mut *conn) +} + +#[allow(dead_code)] +pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<ChatTable> { + let row = dsl::chat_table + .filter(chat_table::chat_id.eq(chat_id_val)) + .first::<ChatTable>(&mut *conn)?; + Ok(row) +} + +#[allow(dead_code)] +pub fn update_chat_name( + mut conn: DBConnection, + chat_id_val: &str, + new_name: &str, +) -> QueryResult<usize> { + diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))) + .set(chat_table::name.eq(new_name)) + .execute(&mut *conn) +} + +#[allow(dead_code)] +pub fn delete_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<usize> { + diesel::delete(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))).execute(&mut *conn) +} diff --git a/frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs b/frontend/rust-lib/flowy-chat/src/persistence/mod.rs similarity index 100% rename from frontend/rust-lib/flowy-ai-pub/src/persistence/mod.rs rename to frontend/rust-lib/flowy-chat/src/persistence/mod.rs diff --git a/frontend/rust-lib/flowy-config/Cargo.toml b/frontend/rust-lib/flowy-config/Cargo.toml new file mode 100644 index 0000000000..821fcda2fc --- /dev/null +++ b/frontend/rust-lib/flowy-config/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "flowy-config" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# workspace +flowy-sqlite = { workspace = true } +lib-dispatch = { workspace = true } +flowy-error = { workspace = true } + +flowy-derive.workspace = true +protobuf.workspace = true +bytes.workspace = true +strum_macros = "0.21" + +[build-dependencies] +flowy-codegen.workspace = true + +[features] +dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-config/Flowy.toml b/frontend/rust-lib/flowy-config/Flowy.toml new file mode 100644 index 0000000000..0dbe74b3e3 --- /dev/null +++ b/frontend/rust-lib/flowy-config/Flowy.toml @@ -0,0 +1,3 @@ +# Check out the FlowyConfig (located in flowy_toml.rs) for more details. +proto_input = ["src/event_map.rs", "src/entities.rs"] +event_files = ["src/event_map.rs"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-config/build.rs b/frontend/rust-lib/flowy-config/build.rs new file mode 100644 index 0000000000..e015eb2580 --- /dev/null +++ b/frontend/rust-lib/flowy-config/build.rs @@ -0,0 +1,23 @@ +fn main() { + #[cfg(feature = "dart")] + { + flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); + } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } +} diff --git a/frontend/rust-lib/flowy-config/src/entities.rs b/frontend/rust-lib/flowy-config/src/entities.rs new file mode 100644 index 0000000000..931724d542 --- /dev/null +++ b/frontend/rust-lib/flowy-config/src/entities.rs @@ -0,0 +1,16 @@ +use flowy_derive::ProtoBuf; + +#[derive(Default, ProtoBuf)] +pub struct KeyValuePB { + #[pb(index = 1)] + pub key: String, + + #[pb(index = 2, one_of)] + pub value: Option<String>, +} + +#[derive(Default, ProtoBuf)] +pub struct KeyPB { + #[pb(index = 1)] + pub key: String, +} diff --git a/frontend/rust-lib/flowy-config/src/event_handler.rs b/frontend/rust-lib/flowy-config/src/event_handler.rs new file mode 100644 index 0000000000..46cd1262c3 --- /dev/null +++ b/frontend/rust-lib/flowy-config/src/event_handler.rs @@ -0,0 +1,56 @@ +use std::sync::Weak; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sqlite::kv::StorePreferences; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; + +use crate::entities::{KeyPB, KeyValuePB}; + +pub(crate) async fn set_key_value_handler( + store_preferences: AFPluginState<Weak<StorePreferences>>, + data: AFPluginData<KeyValuePB>, +) -> FlowyResult<()> { + let data = data.into_inner(); + + if let Some(store_preferences) = store_preferences.upgrade() { + match data.value { + None => store_preferences.remove(&data.key), + Some(value) => { + store_preferences.set_str(&data.key, value); + }, + } + } + + Ok(()) +} + +pub(crate) async fn get_key_value_handler( + store_preferences: AFPluginState<Weak<StorePreferences>>, + data: AFPluginData<KeyPB>, +) -> DataResult<KeyValuePB, FlowyError> { + match store_preferences.upgrade() { + None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?, + Some(store_preferences) => { + let data = data.into_inner(); + let value = store_preferences.get_str(&data.key); + data_result_ok(KeyValuePB { + key: data.key, + value, + }) + }, + } +} + +pub(crate) async fn remove_key_value_handler( + store_preferences: AFPluginState<Weak<StorePreferences>>, + data: AFPluginData<KeyPB>, +) -> FlowyResult<()> { + match store_preferences.upgrade() { + None => Err(FlowyError::internal().with_context("The store preferences is already drop"))?, + Some(store_preferences) => { + let data = data.into_inner(); + store_preferences.remove(&data.key); + Ok(()) + }, + } +} diff --git a/frontend/rust-lib/flowy-config/src/event_map.rs b/frontend/rust-lib/flowy-config/src/event_map.rs new file mode 100644 index 0000000000..68c6ceb454 --- /dev/null +++ b/frontend/rust-lib/flowy-config/src/event_map.rs @@ -0,0 +1,31 @@ +use std::sync::Weak; + +use strum_macros::Display; + +use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; +use flowy_sqlite::kv::StorePreferences; +use lib_dispatch::prelude::AFPlugin; + +use crate::event_handler::*; + +pub fn init(store_preferences: Weak<StorePreferences>) -> AFPlugin { + AFPlugin::new() + .name(env!("CARGO_PKG_NAME")) + .state(store_preferences) + .event(ConfigEvent::SetKeyValue, set_key_value_handler) + .event(ConfigEvent::GetKeyValue, get_key_value_handler) + .event(ConfigEvent::RemoveKeyValue, remove_key_value_handler) +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] +#[event_err = "FlowyError"] +pub enum ConfigEvent { + #[event(input = "KeyValuePB")] + SetKeyValue = 0, + + #[event(input = "KeyPB", output = "KeyValuePB")] + GetKeyValue = 1, + + #[event(input = "KeyPB")] + RemoveKeyValue = 2, +} diff --git a/frontend/rust-lib/flowy-config/src/lib.rs b/frontend/rust-lib/flowy-config/src/lib.rs new file mode 100644 index 0000000000..e08a6c9ce6 --- /dev/null +++ b/frontend/rust-lib/flowy-config/src/lib.rs @@ -0,0 +1,4 @@ +pub mod entities; +mod event_handler; +pub mod event_map; +mod protobuf; diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index b4e7bd5fec..6cdcb0f5bd 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -20,26 +20,20 @@ flowy-document-pub = { workspace = true } flowy-error = { workspace = true } flowy-server = { workspace = true, features = ["enable_supabase"] } flowy-server-pub = { workspace = true } +flowy-config = { workspace = true } flowy-date = { workspace = true } collab-integrate = { workspace = true } flowy-search = { workspace = true } flowy-search-pub = { workspace = true } collab-entity = { workspace = true } collab-plugins = { workspace = true } -collab-folder = { workspace = true } - collab = { workspace = true } -#collab = { workspace = true, features = ["verbose_log"] } - diesel.workspace = true +uuid.workspace = true flowy-storage = { workspace = true } -flowy-storage-pub = { workspace = true } client-api.workspace = true -flowy-ai = { workspace = true } -flowy-ai-pub = { workspace = true } -af-local-ai = { workspace = true } -af-plugin = { workspace = true } - +flowy-chat = { workspace = true } +flowy-chat-pub = { workspace = true } tracing.workspace = true futures-core = { version = "0.3", default-features = false } @@ -47,38 +41,43 @@ bytes.workspace = true tokio = { workspace = true, features = ["full"] } tokio-stream = { workspace = true, features = ["sync"] } console-subscriber = { version = "0.2", optional = true } +parking_lot.workspace = true anyhow.workspace = true -dashmap.workspace = true -arc-swap.workspace = true base64 = "0.21.5" lib-infra = { workspace = true } serde.workspace = true serde_json.workspace = true serde_repr.workspace = true -uuid.workspace = true +futures.workspace = true +walkdir = "2.4.0" sysinfo = "0.30.5" -semver = { version = "1.0.22", features = ["serde"] } -url = "2.5.0" +semver = "1.0.22" [features] profiling = ["console-subscriber", "tokio/tracing"] http_sync = [] native_sync = [] dart = [ - "flowy-user/dart", - "flowy-date/dart", - "flowy-search/dart", - "flowy-folder/dart", - "flowy-database2/dart", - "flowy-ai/dart", - "flowy-storage/dart", + "flowy-user/dart", + "flowy-date/dart", + "flowy-search/dart", + "flowy-folder/dart", + "flowy-database2/dart", + "flowy-chat/dart", +] +ts = [ + "flowy-user/tauri_ts", + "flowy-folder/tauri_ts", + "flowy-search/tauri_ts", + "flowy-database2/ts", + "flowy-config/tauri_ts", + "flowy-chat/tauri_ts", ] openssl_vendored = ["flowy-sqlite/openssl_vendored"] # Enable/Disable AppFlowy Verbose Log Configuration verbose_log = [ - "flowy-document/verbose_log", - "flowy-database2/verbose_log", - "client-api/sync_verbose_log" + "flowy-document/verbose_log", + "client-api/sync_verbose_log" ] diff --git a/frontend/rust-lib/flowy-core/assets/read_me.json b/frontend/rust-lib/flowy-core/assets/read_me.json index 57f6b8a19b..22924bc42b 100644 --- a/frontend/rust-lib/flowy-core/assets/read_me.json +++ b/frontend/rust-lib/flowy-core/assets/read_me.json @@ -1,5 +1,10 @@ { "type": "page", + "data": { + "delta": [ + {"insert": ""} + ] + }, "children": [ { "type": "heading", diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 2bad578627..fd8fbe335f 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -1,16 +1,17 @@ use std::fmt; -use std::path::{Path, PathBuf}; +use std::path::Path; use base64::Engine; use semver::Version; use tracing::{error, info}; -use url::Url; -use crate::log_filter::create_log_filter; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_user::services::entities::URL_SAFE_ENGINE; use lib_infra::file_util::copy_dir_recursive; -use lib_infra::util::OperatingSystem; +use lib_infra::util::Platform; + +use crate::integrate::log::create_log_filter; #[derive(Clone)] pub struct AppFlowyCoreConfig { @@ -27,27 +28,9 @@ pub struct AppFlowyCoreConfig { /// the origin_application_path. pub application_path: String, pub(crate) log_filter: String, - pub cloud_config: Option<AFCloudConfiguration>, + cloud_config: Option<AFCloudConfiguration>, } -impl AppFlowyCoreConfig { - pub fn ensure_path(&self) { - let create_if_needed = |path_str: &str, label: &str| { - let dir = std::path::Path::new(path_str); - if !dir.exists() { - match std::fs::create_dir_all(dir) { - Ok(_) => info!("Created {} path: {}", label, path_str), - Err(err) => error!( - "Failed to create {} path: {}. Error: {}", - label, path_str, err - ), - } - } - }; - create_if_needed(&self.storage_path, "storage"); - create_if_needed(&self.application_path, "application"); - } -} impl fmt::Debug for AppFlowyCoreConfig { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut debug = f.debug_struct("AppFlowy Configuration"); @@ -58,67 +41,36 @@ impl fmt::Debug for AppFlowyCoreConfig { debug.field("base_url", &config.base_url); debug.field("ws_url", &config.ws_base_url); debug.field("gotrue_url", &config.gotrue_url); - debug.field("enable_sync_trace", &config.enable_sync_trace); } debug.finish() } } fn make_user_data_folder(root: &str, url: &str) -> String { - // If a URL is provided, try to parse it and extract the domain name. - // This isolates the user data folder by the domain, which prevents data sharing - // between different AppFlowy cloud instances. - print!("Creating user data folder for URL: {}, root:{}", url, root); - let mut storage_path = if url.is_empty() { - PathBuf::from(root) - } else { + // Isolate the user data folder by using the base url of AppFlowy cloud. This is to avoid + // the user data folder being shared by different AppFlowy cloud. + let storage_path = if !url.is_empty() { let server_base64 = URL_SAFE_ENGINE.encode(url); - PathBuf::from(format!("{}_{}", root, server_base64)) + format!("{}_{}", root, server_base64) + } else { + root.to_string() }; - // Only use new storage path if the old one doesn't exist - if !storage_path.exists() { - let anon_path = format!("{}_anonymous", root); - // We use domain name as suffix to isolate the user data folder since version 0.8.9 - let new_storage_path = if url.is_empty() { - // if the url is empty, then it's anonymous mode - anon_path - } else { - match Url::parse(url) { - Ok(parsed_url) => { - if let Some(domain) = parsed_url.host_str() { - format!("{}_{}", root, domain) - } else { - anon_path - } - }, - Err(_) => anon_path, - } - }; - - storage_path = PathBuf::from(new_storage_path); - } - // Copy the user data folder from the root path to the isolated path // The root path without any suffix is the created by the local version AppFlowy - if !storage_path.exists() && Path::new(root).exists() { - info!("Copy dir from {} to {:?}", root, storage_path); + if !Path::new(&storage_path).exists() && Path::new(root).exists() { + info!("Copy dir from {} to {}", root, storage_path); let src = Path::new(root); - match copy_dir_recursive(src, &storage_path) { - Ok(_) => storage_path - .into_os_string() - .into_string() - .unwrap_or_else(|_| root.to_string()), + match copy_dir_recursive(src, Path::new(&storage_path)) { + Ok(_) => storage_path, Err(err) => { + // when the copy dir failed, use the root path as the storage path error!("Copy dir failed: {}", err); root.to_string() }, } } else { storage_path - .into_os_string() - .into_string() - .unwrap_or_else(|_| root.to_string()) } } @@ -132,18 +84,17 @@ impl AppFlowyCoreConfig { name: String, ) -> Self { let cloud_config = AFCloudConfiguration::from_env().ok(); - // By default enable sync trace log - let log_crates = vec!["sync_trace_log".to_string()]; let storage_path = match &cloud_config { - None => custom_application_path, + None => { + let supabase_config = SupabaseConfiguration::from_env().ok(); + match &supabase_config { + None => custom_application_path, + Some(config) => make_user_data_folder(&custom_application_path, &config.url), + } + }, Some(config) => make_user_data_folder(&custom_application_path, &config.base_url), }; - - let log_filter = create_log_filter( - "info".to_owned(), - log_crates, - OperatingSystem::from(&platform), - ); + let log_filter = create_log_filter("info".to_owned(), vec![], Platform::from(&platform)); AppFlowyCoreConfig { app_version, @@ -161,7 +112,7 @@ impl AppFlowyCoreConfig { self.log_filter = create_log_filter( level.to_owned(), with_crates, - OperatingSystem::from(&self.platform), + Platform::from(&self.platform), ); self } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs index a7d2bc15c1..9ba1604182 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs @@ -1,29 +1,9 @@ -use collab::core::collab::DataSource; -use collab::core::origin::CollabOrigin; -use collab::preclude::updates::decoder::Decode; -use collab::preclude::{Collab, StateVector}; -use collab::util::is_change_since_sv; -use collab_entity::CollabType; -use collab_integrate::persistence::collab_metadata_sql::AFCollabMetadata; -use flowy_ai::ai_manager::{AIExternalService, AIManager}; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder::ViewLayout; -use flowy_folder_pub::cloud::{FolderCloudService, FullSyncCollabParams}; -use flowy_folder_pub::query::FolderService; -use flowy_sqlite::kv::KVStorePreferences; +use flowy_chat::manager::{ChatManager, ChatUserService}; +use flowy_chat_pub::cloud::ChatCloudService; +use flowy_error::FlowyError; use flowy_sqlite::DBConnection; -use flowy_storage_pub::storage::StorageService; use flowy_user::services::authenticate_user::AuthenticateUser; -use lib_infra::async_trait::async_trait; -use lib_infra::util::timestamp; -use std::collections::HashMap; -use std::path::PathBuf; use std::sync::{Arc, Weak}; -use tracing::{error, info}; -use uuid::Uuid; pub struct ChatDepsResolver; @@ -31,130 +11,13 @@ impl ChatDepsResolver { pub fn resolve( authenticate_user: Weak<AuthenticateUser>, cloud_service: Arc<dyn ChatCloudService>, - store_preferences: Arc<KVStorePreferences>, - storage_service: Weak<dyn StorageService>, - folder_cloud_service: Arc<dyn FolderCloudService>, - folder_service: impl FolderService, - local_ai: Arc<LocalAIController>, - ) -> Arc<AIManager> { + ) -> Arc<ChatManager> { let user_service = ChatUserServiceImpl(authenticate_user); - Arc::new(AIManager::new( - cloud_service, - user_service, - store_preferences, - storage_service, - ChatQueryServiceImpl { - folder_service: Box::new(folder_service), - folder_cloud_service, - }, - local_ai, - )) + Arc::new(ChatManager::new(cloud_service, user_service)) } } -struct ChatQueryServiceImpl { - folder_service: Box<dyn FolderService>, - folder_cloud_service: Arc<dyn FolderCloudService>, -} - -#[async_trait] -impl AIExternalService for ChatQueryServiceImpl { - async fn query_chat_rag_ids( - &self, - parent_view_id: &Uuid, - chat_id: &Uuid, - ) -> Result<Vec<Uuid>, FlowyError> { - let mut ids = self - .folder_service - .get_surrounding_view_ids_with_view_layout(parent_view_id, ViewLayout::Document) - .await; - - if !ids.is_empty() { - ids.retain(|id| id != chat_id); - } - - Ok(ids) - } - async fn sync_rag_documents( - &self, - workspace_id: &Uuid, - rag_ids: Vec<Uuid>, - mut rag_metadata_map: HashMap<Uuid, AFCollabMetadata>, - ) -> Result<Vec<AFCollabMetadata>, FlowyError> { - let mut result = Vec::new(); - - for rag_id in rag_ids { - // Retrieve the collab object for the current rag_id - let query_collab = match self - .folder_service - .get_collab(&rag_id, CollabType::Document) - .await - { - Some(collab) => collab, - None => { - continue; - }, - }; - - // Check if the state vector exists and detect changes - if let Some(metadata) = rag_metadata_map.remove(&rag_id) { - if let Ok(prev_sv) = StateVector::decode_v1(&metadata.prev_sync_state_vector) { - let collab = Collab::new_with_source( - CollabOrigin::Empty, - &rag_id.to_string(), - DataSource::DocStateV1(query_collab.encoded_collab.doc_state.to_vec()), - vec![], - false, - )?; - - if !is_change_since_sv(&collab, &prev_sv) { - info!("[Chat] no change since sv: {}", rag_id); - continue; - } - } - } - - // Perform full sync if changes are detected or no state vector is found - let params = FullSyncCollabParams { - object_id: rag_id, - collab_type: CollabType::Document, - encoded_collab: query_collab.encoded_collab.clone(), - }; - - if let Err(err) = self - .folder_cloud_service - .full_sync_collab_object(workspace_id, params) - .await - { - error!("Failed to sync rag document: {} error: {}", rag_id, err); - } else { - info!("[Chat] full sync rag document: {}", rag_id); - result.push(AFCollabMetadata { - object_id: rag_id.to_string(), - updated_at: timestamp(), - prev_sync_state_vector: query_collab.encoded_collab.state_vector.to_vec(), - collab_type: CollabType::Document as i32, - }); - } - } - - Ok(result) - } - - async fn notify_did_send_message(&self, chat_id: &Uuid, message: &str) -> Result<(), FlowyError> { - info!( - "notify_did_send_message: chat_id: {}, message: {}", - chat_id, message - ); - self - .folder_service - .set_view_title_if_empty(chat_id, message) - .await?; - Ok(()) - } -} - -pub struct ChatUserServiceImpl(Weak<AuthenticateUser>); +struct ChatUserServiceImpl(Weak<AuthenticateUser>); impl ChatUserServiceImpl { fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> { let user = self @@ -165,27 +28,20 @@ impl ChatUserServiceImpl { } } -#[async_trait] -impl AIUserService for ChatUserServiceImpl { +impl ChatUserService for ChatUserServiceImpl { fn user_id(&self) -> Result<i64, FlowyError> { self.upgrade_user()?.user_id() } - async fn is_local_model(&self) -> FlowyResult<bool> { - self.upgrade_user()?.is_local_mode().await + fn device_id(&self) -> Result<String, FlowyError> { + self.upgrade_user()?.device_id() } - fn workspace_id(&self) -> Result<Uuid, FlowyError> { + fn workspace_id(&self) -> Result<String, FlowyError> { self.upgrade_user()?.workspace_id() } fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> { self.upgrade_user()?.get_sqlite_connection(uid) } - - fn application_root_dir(&self) -> Result<PathBuf, FlowyError> { - Ok(PathBuf::from( - self.upgrade_user()?.get_application_root_dir(), - )) - } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs deleted file mode 100644 index c49757f735..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/cloud_service_impl.rs +++ /dev/null @@ -1,853 +0,0 @@ -use crate::server_layer::ServerProvider; -use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; -use client_api::entity::ai_dto::RepeatedRelatedQuestion; -use client_api::entity::workspace_dto::PublishInfoView; -use client_api::entity::PublishInfo; -use collab::core::origin::{CollabClient, CollabOrigin}; -use collab::entity::EncodedCollab; -use collab::preclude::CollabPlugin; -use collab_entity::CollabType; -use collab_integrate::collab_builder::{ - CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, -}; -use flowy_ai_pub::cloud::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, CompleteTextParams, - MessageCursor, ModelList, RepeatedChatMessage, ResponseFormat, StreamAnswer, StreamComplete, - UpdateChatParams, -}; -use flowy_database_pub::cloud::{ - DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, - TranslateRowContent, TranslateRowResponse, -}; -use flowy_document::deps::DocumentData; -use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, -}; -use flowy_folder_pub::entities::PublishPayload; -use flowy_search_pub::cloud::SearchCloudService; -use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; -use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; -use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; -use flowy_user_pub::entities::{AuthType, UserTokenState}; -use lib_infra::async_trait::async_trait; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use std::str::FromStr; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::time::Duration; -use tokio_stream::wrappers::WatchStream; -use tracing::log::error; -use tracing::{debug, info}; -use uuid::Uuid; - -#[async_trait] -impl StorageCloudService for ServerProvider { - async fn get_object_url(&self, object_id: ObjectIdentity) -> Result<String, FlowyError> { - let storage = self - .get_server()? - .file_storage() - .ok_or(FlowyError::internal())?; - storage.get_object_url(object_id).await - } - - async fn put_object(&self, url: String, val: ObjectValue) -> Result<(), FlowyError> { - let storage = self - .get_server()? - .file_storage() - .ok_or(FlowyError::internal())?; - storage.put_object(url, val).await - } - - async fn delete_object(&self, url: &str) -> Result<(), FlowyError> { - let storage = self - .get_server()? - .file_storage() - .ok_or(FlowyError::internal())?; - storage.delete_object(url).await - } - - async fn get_object(&self, url: String) -> Result<ObjectValue, FlowyError> { - let storage = self - .get_server()? - .file_storage() - .ok_or(FlowyError::internal())?; - storage.get_object(url).await - } - - async fn get_object_url_v1( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - ) -> FlowyResult<String> { - let server = self.get_server()?; - let storage = server.file_storage().ok_or(FlowyError::internal())?; - storage - .get_object_url_v1(workspace_id, parent_dir, file_id) - .await - } - - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { - self - .get_server() - .ok()? - .file_storage()? - .parse_object_url_v1(url) - .await - } - - async fn create_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - content_type: &str, - file_size: u64, - ) -> Result<CreateUploadResponse, FlowyError> { - let server = self.get_server()?; - let storage = server.file_storage().ok_or(FlowyError::internal())?; - storage - .create_upload(workspace_id, parent_dir, file_id, content_type, file_size) - .await - } - - async fn upload_part( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - part_number: i32, - body: Vec<u8>, - ) -> Result<UploadPartResponse, FlowyError> { - let server = self.get_server(); - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage - .upload_part( - workspace_id, - parent_dir, - upload_id, - file_id, - part_number, - body, - ) - .await - } - - async fn complete_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - parts: Vec<CompletedPartRequest>, - ) -> Result<(), FlowyError> { - let server = self.get_server(); - let storage = server?.file_storage().ok_or(FlowyError::internal())?; - storage - .complete_upload(workspace_id, parent_dir, upload_id, file_id, parts) - .await - } -} - -impl UserCloudServiceProvider for ServerProvider { - fn set_token(&self, token: &str) -> Result<(), FlowyError> { - let server = self.get_server()?; - info!("Set token"); - server.set_token(token)?; - Ok(()) - } - - fn set_ai_model(&self, ai_model: &str) -> Result<(), FlowyError> { - info!("Set AI model: {}", ai_model); - let server = self.get_server()?; - server.set_ai_model(ai_model)?; - Ok(()) - } - - fn subscribe_token_state(&self) -> Option<WatchStream<UserTokenState>> { - let server = self.get_server().ok()?; - server.subscribe_token_state() - } - - fn set_enable_sync(&self, uid: i64, enable_sync: bool) { - if let Ok(server) = self.get_server() { - server.set_enable_sync(uid, enable_sync); - self.user_enable_sync.store(enable_sync, Ordering::Release); - self.uid.store(Some(uid.into())); - } - } - - /// When user login, the provider type is set by the [AuthType] and save to disk for next use. - /// - /// Each [AuthType] has a corresponding [AuthType]. The [AuthType] is used - /// to create a new [AppFlowyServer] if it doesn't exist. Once the [AuthType] is set, - /// it will be used when user open the app again. - /// - fn set_server_auth_type(&self, auth_type: &AuthType, token: Option<String>) -> FlowyResult<()> { - self.set_auth_type(*auth_type); - if let Some(token) = token { - self.set_token(&token)?; - } - Ok(()) - } - - fn get_server_auth_type(&self) -> AuthType { - self.get_auth_type() - } - - fn set_network_reachable(&self, reachable: bool) { - if let Ok(server) = self.get_server() { - server.set_network_reachable(reachable); - } - } - - fn set_encrypt_secret(&self, secret: String) { - tracing::info!("🔑Set encrypt secret"); - self.encryption.set_secret(secret); - } - - /// Returns the [UserCloudService] base on the current [AuthType]. - /// Creates a new [AppFlowyServer] if it doesn't exist. - fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> { - let user_service = self.get_server()?.user_service(); - Ok(user_service) - } - - fn service_url(&self) -> String { - match self.get_auth_type() { - AuthType::Local => "".to_string(), - AuthType::AppFlowyCloud => AFCloudConfiguration::from_env() - .map(|config| config.base_url) - .unwrap_or_default(), - } - } -} - -#[async_trait] -impl FolderCloudService for ServerProvider { - async fn get_folder_snapshots( - &self, - workspace_id: &str, - limit: usize, - ) -> Result<Vec<FolderSnapshot>, FlowyError> { - self - .get_server()? - .folder_service() - .get_folder_snapshots(workspace_id, limit) - .await - } - - async fn get_folder_doc_state( - &self, - workspace_id: &Uuid, - uid: i64, - collab_type: CollabType, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { - self - .get_server()? - .folder_service() - .get_folder_doc_state(workspace_id, uid, collab_type, object_id) - .await - } - - async fn full_sync_collab_object( - &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .full_sync_collab_object(workspace_id, params) - .await - } - - async fn batch_create_folder_collab_objects( - &self, - workspace_id: &Uuid, - objects: Vec<FolderCollabParams>, - ) -> Result<(), FlowyError> { - let server = self.get_server()?; - - server - .folder_service() - .batch_create_folder_collab_objects(workspace_id, objects) - .await - } - - fn service_name(&self) -> String { - self - .get_server() - .map(|provider| provider.folder_service().service_name()) - .unwrap_or_default() - } - - async fn publish_view( - &self, - workspace_id: &Uuid, - payload: Vec<PublishPayload>, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .publish_view(workspace_id, payload) - .await - } - - async fn unpublish_views( - &self, - workspace_id: &Uuid, - view_ids: Vec<Uuid>, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .unpublish_views(workspace_id, view_ids) - .await - } - - async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError> { - let server = self.get_server()?; - server.folder_service().get_publish_info(view_id).await - } - - async fn set_publish_name( - &self, - workspace_id: &Uuid, - view_id: Uuid, - new_name: String, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .set_publish_name(workspace_id, view_id, new_name) - .await - } - - async fn set_publish_namespace( - &self, - workspace_id: &Uuid, - new_namespace: String, - ) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .set_publish_namespace(workspace_id, new_namespace) - .await - } - - /// List all published views of the current workspace. - async fn list_published_views( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<PublishInfoView>, FlowyError> { - let server = self.get_server()?; - server - .folder_service() - .list_published_views(workspace_id) - .await - } - - async fn get_default_published_view_info( - &self, - workspace_id: &Uuid, - ) -> Result<PublishInfo, FlowyError> { - let server = self.get_server()?; - server - .folder_service() - .get_default_published_view_info(workspace_id) - .await - } - - async fn set_default_published_view( - &self, - workspace_id: &Uuid, - view_id: uuid::Uuid, - ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server - .folder_service() - .set_default_published_view(workspace_id, view_id) - .await - } - - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - let server = self.get_server()?; - server - .folder_service() - .remove_default_published_view(workspace_id) - .await - } - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - let server = self.get_server()?; - server - .folder_service() - .get_publish_namespace(workspace_id) - .await - } - - async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { - self - .get_server()? - .folder_service() - .import_zip(file_path) - .await - } -} - -#[async_trait] -impl DatabaseCloudService for ServerProvider { - async fn get_database_encode_collab( - &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - ) -> Result<Option<EncodedCollab>, FlowyError> { - let server = self.get_server()?; - server - .database_service() - .get_database_encode_collab(object_id, collab_type, workspace_id) - .await - } - - async fn create_database_encode_collab( - &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server - .database_service() - .create_database_encode_collab(object_id, collab_type, workspace_id, encoded_collab) - .await - } - - async fn batch_get_database_encode_collab( - &self, - object_ids: Vec<Uuid>, - object_ty: CollabType, - workspace_id: &Uuid, - ) -> Result<EncodeCollabByOid, FlowyError> { - let server = self.get_server()?; - - server - .database_service() - .batch_get_database_encode_collab(object_ids, object_ty, workspace_id) - .await - } - - async fn get_database_collab_object_snapshots( - &self, - object_id: &Uuid, - limit: usize, - ) -> Result<Vec<DatabaseSnapshot>, FlowyError> { - let server = self.get_server()?; - - server - .database_service() - .get_database_collab_object_snapshots(object_id, limit) - .await - } -} - -#[async_trait] -impl DatabaseAIService for ServerProvider { - async fn summary_database_row( - &self, - _workspace_id: &Uuid, - _object_id: &Uuid, - _summary_row: SummaryRowContent, - ) -> Result<String, FlowyError> { - self - .get_server()? - .database_ai_service() - .ok_or_else(FlowyError::not_support)? - .summary_database_row(_workspace_id, _object_id, _summary_row) - .await - } - - async fn translate_database_row( - &self, - _workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, - ) -> Result<TranslateRowResponse, FlowyError> { - self - .get_server()? - .database_ai_service() - .ok_or_else(FlowyError::not_support)? - .translate_database_row(_workspace_id, _translate_row, _language) - .await - } -} - -#[async_trait] -impl DocumentCloudService for ServerProvider { - async fn get_document_doc_state( - &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { - let server = self.get_server()?; - server - .document_service() - .get_document_doc_state(document_id, workspace_id) - .await - } - - async fn get_document_snapshots( - &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, - ) -> Result<Vec<DocumentSnapshot>, FlowyError> { - let server = self.get_server()?; - - server - .document_service() - .get_document_snapshots(document_id, limit, workspace_id) - .await - } - - async fn get_document_data( - &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Option<DocumentData>, FlowyError> { - let server = self.get_server()?; - server - .document_service() - .get_document_data(document_id, workspace_id) - .await - } - - async fn create_document_collab( - &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - let server = self.get_server()?; - server - .document_service() - .create_document_collab(workspace_id, document_id, encoded_collab) - .await - } -} - -impl CollabCloudPluginProvider for ServerProvider { - fn provider_type(&self) -> CollabPluginProviderType { - match self.get_auth_type() { - AuthType::Local => CollabPluginProviderType::Local, - AuthType::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, - } - } - - fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { - // If the user is local, we don't need to create a sync plugin. - if self.get_auth_type().is_local() { - debug!( - "User authenticator is local, skip create sync plugin for: {}", - context - ); - return vec![]; - } - - match context { - CollabPluginProviderContext::Local => vec![], - CollabPluginProviderContext::AppFlowyCloud { - uid: _, - collab_object, - local_collab, - } => { - if let Ok(server) = self.get_server() { - // to_fut(async move { - let mut plugins: Vec<Box<dyn CollabPlugin>> = vec![]; - // If the user is local, we don't need to create a sync plugin. - - match server.collab_ws_channel(&collab_object.object_id) { - Ok(Some((channel, ws_connect_state, _is_connected))) => { - let origin = CollabOrigin::Client(CollabClient::new( - collab_object.uid, - collab_object.device_id.clone(), - )); - - if let (Ok(object_id), Ok(workspace_id)) = ( - Uuid::from_str(&collab_object.object_id), - Uuid::from_str(&collab_object.workspace_id), - ) { - let sync_object = SyncObject::new( - object_id, - workspace_id, - collab_object.collab_type, - &collab_object.device_id, - ); - let (sink, stream) = (channel.sink(), channel.stream()); - let sink_config = SinkConfig::new().send_timeout(8); - let sync_plugin = SyncPlugin::new( - origin, - sync_object, - local_collab, - sink, - sink_config, - stream, - Some(channel), - ws_connect_state, - Some(Duration::from_secs(60)), - ); - plugins.push(Box::new(sync_plugin)); - } else { - error!( - "Failed to parse collab object id: {}", - collab_object.object_id - ); - } - }, - Ok(None) => { - tracing::error!("🔴Failed to get collab ws channel: channel is none"); - }, - Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err), - } - plugins - } else { - vec![] - } - }, - } - } - - fn is_sync_enabled(&self) -> bool { - self.user_enable_sync.load(Ordering::Acquire) - } -} - -#[async_trait] -impl ChatCloudService for ServerProvider { - async fn create_chat( - &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - name: &str, - metadata: serde_json::Value, - ) -> Result<(), FlowyError> { - let server = self.get_server(); - server? - .chat_service() - .create_chat(uid, workspace_id, chat_id, rag_ids, name, metadata) - .await - } - - async fn create_question( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError> { - let message = message.to_string(); - self - .get_server()? - .chat_service() - .create_question(workspace_id, chat_id, &message, message_type) - .await - } - - async fn create_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option<serde_json::Value>, - ) -> Result<ChatMessage, FlowyError> { - let server = self.get_server(); - server? - .chat_service() - .create_answer(workspace_id, chat_id, message, question_id, metadata) - .await - } - - async fn stream_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, - ) -> Result<StreamAnswer, FlowyError> { - let server = self.get_server()?; - server - .chat_service() - .stream_answer(workspace_id, chat_id, question_id, format, ai_model) - .await - } - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError> { - self - .get_server()? - .chat_service() - .get_chat_messages(workspace_id, chat_id, offset, limit) - .await - } - - async fn get_question_from_answer_id( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError> { - self - .get_server()? - .chat_service() - .get_question_from_answer_id(workspace_id, chat_id, answer_message_id) - .await - } - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError> { - self - .get_server()? - .chat_service() - .get_related_message(workspace_id, chat_id, message_id, ai_model) - .await - } - - async fn get_answer( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let server = self.get_server(); - server? - .chat_service() - .get_answer(workspace_id, chat_id, question_id) - .await - } - - async fn stream_complete( - &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError> { - let server = self.get_server()?; - server - .chat_service() - .stream_complete(workspace_id, params, ai_model) - .await - } - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError> { - self - .get_server()? - .chat_service() - .embed_file(workspace_id, file_path, chat_id, metadata) - .await - } - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError> { - self - .get_server()? - .chat_service() - .get_chat_settings(workspace_id, chat_id) - .await - } - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError> { - self - .get_server()? - .chat_service() - .update_chat_settings(workspace_id, chat_id, params) - .await - } - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> { - self - .get_server()? - .chat_service() - .get_available_models(workspace_id) - .await - } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - self - .get_server()? - .chat_service() - .get_workspace_default_model(workspace_id) - .await - } -} - -#[async_trait] -impl SearchCloudService for ServerProvider { - async fn document_search( - &self, - workspace_id: &Uuid, - query: String, - ) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> { - let server = self.get_server()?; - match server.search_service() { - Some(search_service) => search_service.document_search(workspace_id, query).await, - None => Err(FlowyError::internal().with_context("SearchCloudService not found")), - } - } - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec<SearchResult>, - ) -> Result<SearchSummaryResult, FlowyError> { - let server = self.get_server()?; - match server.search_service() { - Some(search_service) => { - search_service - .generate_search_summary(workspace_id, query, search_results) - .await - }, - None => Err(FlowyError::internal().with_context("SearchCloudService not found")), - } - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs index 078ee7359b..a8827e06b0 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/collab_deps.rs @@ -13,7 +13,6 @@ use collab_integrate::collab_builder::WorkspaceCollabIntegrate; use lib_infra::util::timestamp; use std::sync::{Arc, Weak}; use tracing::debug; -use uuid::Uuid; pub struct SnapshotDBImpl(pub Weak<AuthenticateUser>); @@ -25,7 +24,7 @@ impl SnapshotPersistence for SnapshotDBImpl { collab_type: &CollabType, encoded_v1: Vec<u8>, ) -> Result<(), PersistenceError> { - let collab_type = *collab_type; + let collab_type = collab_type.clone(); let object_id = object_id.to_string(); let weak_user = self.0.clone(); tokio::task::spawn_blocking(move || { @@ -223,12 +222,12 @@ impl WorkspaceCollabIntegrateImpl { } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result<Uuid, FlowyError> { + fn workspace_id(&self) -> Result<String, anyhow::Error> { let workspace_id = self.upgrade_user()?.workspace_id()?; Ok(workspace_id) } - fn device_id(&self) -> Result<String, FlowyError> { + fn device_id(&self) -> Result<String, anyhow::Error> { Ok(self.upgrade_user()?.user_config.device_id.clone()) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs index 1bd3223946..2ef0046dc7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/database_deps.rs @@ -1,20 +1,12 @@ -use af_local_ai::ai_ops::{LocalAITranslateItem, LocalAITranslateRowData}; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_ai::ai_manager::AIManager; use flowy_database2::{DatabaseManager, DatabaseUser}; -use flowy_database_pub::cloud::{ - DatabaseAIService, DatabaseCloudService, SummaryRowContent, TranslateRowContent, - TranslateRowResponse, -}; +use flowy_database_pub::cloud::DatabaseCloudService; use flowy_error::FlowyError; use flowy_user::services::authenticate_user::AuthenticateUser; -use lib_infra::async_trait::async_trait; use lib_infra::priority_task::TaskDispatcher; use std::sync::{Arc, Weak}; use tokio::sync::RwLock; -use uuid::Uuid; - pub struct DatabaseDepsResolver(); impl DatabaseDepsResolver { @@ -23,8 +15,6 @@ impl DatabaseDepsResolver { task_scheduler: Arc<RwLock<TaskDispatcher>>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DatabaseCloudService>, - ai_service: Arc<dyn DatabaseAIService>, - ai_manager: Arc<AIManager>, ) -> Arc<DatabaseManager> { let user = Arc::new(DatabaseUserImpl(authenticate_user)); Arc::new(DatabaseManager::new( @@ -32,76 +22,10 @@ impl DatabaseDepsResolver { task_scheduler, collab_builder, cloud_service, - Arc::new(DatabaseAIServiceMiddleware { - ai_manager, - ai_service, - }), )) } } -struct DatabaseAIServiceMiddleware { - ai_manager: Arc<AIManager>, - ai_service: Arc<dyn DatabaseAIService>, -} -#[async_trait] -impl DatabaseAIService for DatabaseAIServiceMiddleware { - async fn summary_database_row( - &self, - workspace_id: &Uuid, - object_id: &Uuid, - _summary_row: SummaryRowContent, - ) -> Result<String, FlowyError> { - if self.ai_manager.local_ai.is_running() { - self - .ai_manager - .local_ai - .summary_database_row(_summary_row) - .await - .map_err(|err| FlowyError::local_ai().with_context(err)) - } else { - self - .ai_service - .summary_database_row(workspace_id, object_id, _summary_row) - .await - } - } - - async fn translate_database_row( - &self, - _workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, - ) -> Result<TranslateRowResponse, FlowyError> { - if self.ai_manager.local_ai.is_running() { - let data = LocalAITranslateRowData { - cells: _translate_row - .into_iter() - .map(|row| LocalAITranslateItem { - title: row.title, - content: row.content, - }) - .collect(), - language: _language.to_string(), - include_header: false, - }; - let resp = self - .ai_manager - .local_ai - .translate_database_row(data) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - - Ok(TranslateRowResponse { items: resp.items }) - } else { - self - .ai_service - .translate_database_row(_workspace_id, _translate_row, _language) - .await - } - } -} - struct DatabaseUserImpl(Weak<AuthenticateUser>); impl DatabaseUserImpl { fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> { @@ -122,11 +46,11 @@ impl DatabaseUser for DatabaseUserImpl { self.upgrade_user()?.get_collab_db(uid) } - fn workspace_id(&self) -> Result<Uuid, FlowyError> { + fn workspace_id(&self) -> Result<String, FlowyError> { self.upgrade_user()?.workspace_id() } - fn workspace_database_object_id(&self) -> Result<Uuid, FlowyError> { + fn workspace_database_object_id(&self) -> Result<String, FlowyError> { self.upgrade_user()?.workspace_database_object_id() } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index 3527bc42d6..1876392eeb 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Weak}; + use crate::deps_resolve::CollabSnapshotSql; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; @@ -6,10 +8,8 @@ use flowy_document::entities::{DocumentSnapshotData, DocumentSnapshotMeta}; use flowy_document::manager::{DocumentManager, DocumentSnapshotService, DocumentUserService}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{FlowyError, FlowyResult}; -use flowy_storage_pub::storage::StorageService; +use flowy_storage::ObjectStorageService; use flowy_user::services::authenticate_user::AuthenticateUser; -use std::sync::{Arc, Weak}; -use uuid::Uuid; pub struct DocumentDepsResolver(); impl DocumentDepsResolver { @@ -18,7 +18,7 @@ impl DocumentDepsResolver { _database_manager: &Arc<DatabaseManager>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DocumentCloudService>, - storage_service: Weak<dyn StorageService>, + storage_service: Weak<dyn ObjectStorageService>, ) -> Arc<DocumentManager> { let user_service: Arc<dyn DocumentUserService> = Arc::new(DocumentUserImpl(authenticate_user.clone())); @@ -97,7 +97,7 @@ impl DocumentUserService for DocumentUserImpl { .device_id() } - fn workspace_id(&self) -> Result<Uuid, FlowyError> { + fn workspace_id(&self) -> Result<String, FlowyError> { self .0 .upgrade() diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs deleted file mode 100644 index bee5f19ced..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/file_storage_deps.rs +++ /dev/null @@ -1,55 +0,0 @@ -use flowy_error::FlowyError; -use flowy_sqlite::DBConnection; -use flowy_storage::manager::{StorageManager, StorageUserService}; -use flowy_storage_pub::cloud::StorageCloudService; -use flowy_user::services::authenticate_user::AuthenticateUser; -use std::sync::{Arc, Weak}; -use uuid::Uuid; - -pub struct FileStorageResolver; - -impl FileStorageResolver { - pub fn resolve( - authenticate_user: Weak<AuthenticateUser>, - cloud_service: Arc<dyn StorageCloudService>, - root: &str, - ) -> Arc<StorageManager> { - let user_service = FileStorageServiceImpl { - user: authenticate_user, - root_dir: root.to_owned(), - }; - Arc::new(StorageManager::new(cloud_service, Arc::new(user_service))) - } -} - -struct FileStorageServiceImpl { - user: Weak<AuthenticateUser>, - root_dir: String, -} -impl FileStorageServiceImpl { - fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> { - let user = self - .user - .upgrade() - .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?; - Ok(user) - } -} - -impl StorageUserService for FileStorageServiceImpl { - fn user_id(&self) -> Result<i64, FlowyError> { - self.upgrade_user()?.user_id() - } - - fn workspace_id(&self) -> Result<Uuid, FlowyError> { - self.upgrade_user()?.workspace_id() - } - - fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> { - self.upgrade_user()?.get_sqlite_connection(uid) - } - - fn get_application_root_dir(&self) -> &str { - &self.root_dir - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs new file mode 100644 index 0000000000..e3a86d4fc7 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -0,0 +1,550 @@ +use bytes::Bytes; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use collab_integrate::CollabKVDB; +use flowy_chat::manager::ChatManager; +use flowy_database2::entities::DatabaseLayoutPB; +use flowy_database2::services::share::csv::CSVFormat; +use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid}; +use flowy_database2::DatabaseManager; +use flowy_document::entities::DocumentDataPB; +use flowy_document::manager::DocumentManager; +use flowy_document::parser::json::parser::JsonToDocumentParser; +use flowy_error::FlowyError; +use flowy_folder::manager::{FolderManager, FolderUser}; +use flowy_folder::share::ImportType; +use flowy_folder::view_operation::{ + FolderOperationHandler, FolderOperationHandlers, View, ViewData, +}; +use flowy_folder::ViewLayout; +use flowy_folder_pub::folder_builder::NestedViewBuilder; +use flowy_search::folder::indexer::FolderIndexManagerImpl; +use flowy_sqlite::kv::StorePreferences; +use flowy_user::services::authenticate_user::AuthenticateUser; +use lib_dispatch::prelude::ToBytes; +use lib_infra::future::FutureResult; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::sync::{Arc, Weak}; +use tokio::sync::RwLock; + +use crate::integrate::server::ServerProvider; + +pub struct FolderDepsResolver(); +#[allow(clippy::too_many_arguments)] +impl FolderDepsResolver { + pub async fn resolve( + authenticate_user: Weak<AuthenticateUser>, + collab_builder: Arc<AppFlowyCollabBuilder>, + server_provider: Arc<ServerProvider>, + folder_indexer: Arc<FolderIndexManagerImpl>, + store_preferences: Arc<StorePreferences>, + operation_handlers: FolderOperationHandlers, + ) -> Arc<FolderManager> { + let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl { + authenticate_user: authenticate_user.clone(), + }); + + Arc::new( + FolderManager::new( + user.clone(), + collab_builder, + operation_handlers, + server_provider.clone(), + folder_indexer, + store_preferences, + ) + .unwrap(), + ) + } +} + +pub fn folder_operation_handlers( + document_manager: Arc<DocumentManager>, + database_manager: Arc<DatabaseManager>, + chat_manager: Arc<ChatManager>, +) -> FolderOperationHandlers { + let mut map: HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>> = HashMap::new(); + + let document_folder_operation = Arc::new(DocumentFolderOperation(document_manager)); + map.insert(ViewLayout::Document, document_folder_operation); + + let database_folder_operation = Arc::new(DatabaseFolderOperation(database_manager)); + let chat_folder_operation = Arc::new(ChatFolderOperation(chat_manager)); + map.insert(ViewLayout::Board, database_folder_operation.clone()); + map.insert(ViewLayout::Grid, database_folder_operation.clone()); + map.insert(ViewLayout::Calendar, database_folder_operation); + map.insert(ViewLayout::Chat, chat_folder_operation); + Arc::new(map) +} + +struct FolderUserImpl { + authenticate_user: Weak<AuthenticateUser>, +} + +impl FolderUserImpl { + fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> { + let user = self + .authenticate_user + .upgrade() + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?; + Ok(user) + } +} + +impl FolderUser for FolderUserImpl { + fn user_id(&self) -> Result<i64, FlowyError> { + self.upgrade_user()?.user_id() + } + + fn workspace_id(&self) -> Result<String, FlowyError> { + self.upgrade_user()?.workspace_id() + } + + fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError> { + self.upgrade_user()?.get_collab_db(uid) + } +} + +struct DocumentFolderOperation(Arc<DocumentManager>); +impl FolderOperationHandler for DocumentFolderOperation { + fn create_workspace_view( + &self, + uid: i64, + workspace_view_builder: Arc<RwLock<NestedViewBuilder>>, + ) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + FutureResult::new(async move { + let mut write_guard = workspace_view_builder.write().await; + + // Create a view named "Getting started" with an icon ⭐️ and the built-in README data. + // Don't modify this code unless you know what you are doing. + write_guard + .with_view_builder(|view_builder| async { + let view = view_builder + .with_name("Getting started") + .with_icon("⭐️") + .build(); + // create a empty document + let json_str = include_str!("../../assets/read_me.json"); + let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); + manager + .create_document(uid, &view.parent_view.id, Some(document_pb.into())) + .await + .unwrap(); + view + }) + .await; + Ok(()) + }) + } + + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.open_document(&view_id).await?; + Ok(()) + }) + } + + /// Close the document view. + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.close_document(&view_id).await?; + Ok(()) + }) + } + + fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + match manager.delete_document(&view_id).await { + Ok(_) => tracing::trace!("Delete document: {}", view_id), + Err(e) => tracing::error!("🔴delete document failed: {}", e), + } + Ok(()) + }) + } + + fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + let data: DocumentDataPB = manager.get_document_data(&view_id).await?.into(); + let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; + Ok(data_bytes) + }) + } + + fn create_view_with_view_data( + &self, + user_id: i64, + view_id: &str, + _name: &str, + data: Vec<u8>, + layout: ViewLayout, + _meta: HashMap<String, String>, + ) -> FutureResult<(), FlowyError> { + debug_assert_eq!(layout, ViewLayout::Document); + let view_id = view_id.to_string(); + let manager = self.0.clone(); + FutureResult::new(async move { + let data = DocumentDataPB::try_from(Bytes::from(data))?; + manager + .create_document(user_id, &view_id, Some(data.into())) + .await?; + Ok(()) + }) + } + + /// Create a view with built-in data. + fn create_built_in_view( + &self, + user_id: i64, + view_id: &str, + _name: &str, + layout: ViewLayout, + ) -> FutureResult<(), FlowyError> { + debug_assert_eq!(layout, ViewLayout::Document); + let view_id = view_id.to_string(); + let manager = self.0.clone(); + FutureResult::new(async move { + match manager.create_document(user_id, &view_id, None).await { + Ok(_) => Ok(()), + Err(err) => { + if err.is_already_exists() { + Ok(()) + } else { + Err(err) + } + }, + } + }) + } + + fn import_from_bytes( + &self, + uid: i64, + view_id: &str, + _name: &str, + _import_type: ImportType, + bytes: Vec<u8>, + ) -> FutureResult<(), FlowyError> { + let view_id = view_id.to_string(); + let manager = self.0.clone(); + FutureResult::new(async move { + let data = DocumentDataPB::try_from(Bytes::from(bytes))?; + manager + .create_document(uid, &view_id, Some(data.into())) + .await?; + Ok(()) + }) + } + + // will implement soon + fn import_from_file_path( + &self, + _view_id: &str, + _name: &str, + _path: String, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Ok(()) }) + } +} + +struct DatabaseFolderOperation(Arc<DatabaseManager>); +impl FolderOperationHandler for DatabaseFolderOperation { + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + database_manager.open_database_view(view_id).await?; + Ok(()) + }) + } + + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + database_manager.close_database_view(view_id).await?; + Ok(()) + }) + } + + fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + match database_manager.delete_database_view(&view_id).await { + Ok(_) => tracing::trace!("Delete database view: {}", view_id), + Err(e) => tracing::error!("🔴delete database failed: {}", e), + } + Ok(()) + }) + } + + fn duplicate_view(&self, view_id: &str) -> FutureResult<Bytes, FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_owned(); + FutureResult::new(async move { + let delta_bytes = database_manager.duplicate_database(&view_id).await?; + Ok(Bytes::from(delta_bytes)) + }) + } + + /// Create a database view with duplicated data. + /// If the ext contains the {"database_id": "xx"}, then it will link + /// to the existing database. + fn create_view_with_view_data( + &self, + _user_id: i64, + view_id: &str, + name: &str, + data: Vec<u8>, + layout: ViewLayout, + meta: HashMap<String, String>, + ) -> FutureResult<(), FlowyError> { + match CreateDatabaseExtParams::from_map(meta) { + None => { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + database_manager + .create_database_with_database_data(&view_id, data) + .await?; + Ok(()) + }) + }, + Some(params) => { + let database_manager = self.0.clone(); + + let layout = match layout { + ViewLayout::Board => DatabaseLayoutPB::Board, + ViewLayout::Calendar => DatabaseLayoutPB::Calendar, + ViewLayout::Grid => DatabaseLayoutPB::Grid, + ViewLayout::Document | ViewLayout::Chat => { + return FutureResult::new(async move { Err(FlowyError::not_support()) }); + }, + }; + let name = name.to_string(); + let database_view_id = view_id.to_string(); + + FutureResult::new(async move { + database_manager + .create_linked_view(name, layout.into(), params.database_id, database_view_id) + .await?; + Ok(()) + }) + }, + } + } + + /// Create a database view with build-in data. + /// If the ext contains the {"database_id": "xx"}, then it will link to + /// the existing database. The data of the database will be shared within + /// these references views. + fn create_built_in_view( + &self, + _user_id: i64, + view_id: &str, + name: &str, + layout: ViewLayout, + ) -> FutureResult<(), FlowyError> { + let name = name.to_string(); + let database_manager = self.0.clone(); + let data = match layout { + ViewLayout::Grid => make_default_grid(view_id, &name), + ViewLayout::Board => make_default_board(view_id, &name), + ViewLayout::Calendar => make_default_calendar(view_id, &name), + ViewLayout::Document => { + return FutureResult::new(async move { + Err(FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout))) + }); + }, + ViewLayout::Chat => { + // TODO(nathan): AI + todo!("AI") + }, + }; + FutureResult::new(async move { + let result = database_manager.create_database_with_params(data).await; + match result { + Ok(_) => Ok(()), + Err(err) => { + if err.is_already_exists() { + Ok(()) + } else { + Err(err) + } + }, + } + }) + } + + fn import_from_bytes( + &self, + _uid: i64, + view_id: &str, + _name: &str, + import_type: ImportType, + bytes: Vec<u8>, + ) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + let format = match import_type { + ImportType::CSV => CSVFormat::Original, + ImportType::HistoryDatabase => CSVFormat::META, + ImportType::RawDatabase => CSVFormat::META, + _ => CSVFormat::Original, + }; + FutureResult::new(async move { + let content = tokio::task::spawn_blocking(move || { + String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err)) + }) + .await??; + + database_manager + .import_csv(view_id, content, format) + .await?; + Ok(()) + }) + } + + fn import_from_file_path( + &self, + _view_id: &str, + _name: &str, + path: String, + ) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + FutureResult::new(async move { + database_manager + .import_csv_from_file(path, CSVFormat::META) + .await?; + Ok(()) + }) + } + + fn did_update_view(&self, old: &View, new: &View) -> FutureResult<(), FlowyError> { + let database_layout = match new.layout { + ViewLayout::Document | ViewLayout::Chat => { + return FutureResult::new(async { + Err(FlowyError::internal().with_context("Can't handle document layout type")) + }); + }, + ViewLayout::Grid => DatabaseLayoutPB::Grid, + ViewLayout::Board => DatabaseLayoutPB::Board, + ViewLayout::Calendar => DatabaseLayoutPB::Calendar, + }; + + let database_manager = self.0.clone(); + let view_id = new.id.clone(); + if old.layout != new.layout { + FutureResult::new(async move { + database_manager + .update_database_layout(&view_id, database_layout) + .await?; + Ok(()) + }) + } else { + FutureResult::new(async move { Ok(()) }) + } + } +} + +#[derive(Debug, serde::Deserialize)] +struct CreateDatabaseExtParams { + database_id: String, +} + +impl CreateDatabaseExtParams { + pub fn from_map(map: HashMap<String, String>) -> Option<Self> { + let value = serde_json::to_value(map).ok()?; + serde_json::from_value::<Self>(value).ok() + } +} + +struct ChatFolderOperation(Arc<ChatManager>); +impl FolderOperationHandler for ChatFolderOperation { + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.open_chat(&view_id).await?; + Ok(()) + }) + } + + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.close_chat(&view_id).await?; + Ok(()) + }) + } + + fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.delete_chat(&view_id).await?; + Ok(()) + }) + } + + fn duplicate_view(&self, _view_id: &str) -> FutureResult<ViewData, FlowyError> { + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } + + fn create_view_with_view_data( + &self, + _user_id: i64, + _view_id: &str, + _name: &str, + _data: Vec<u8>, + _layout: ViewLayout, + _meta: HashMap<String, String>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } + + fn create_built_in_view( + &self, + user_id: i64, + view_id: &str, + _name: &str, + _layout: ViewLayout, + ) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.create_chat(&user_id, &view_id).await?; + Ok(()) + }) + } + + fn import_from_bytes( + &self, + _uid: i64, + _view_id: &str, + _name: &str, + _import_type: ImportType, + _bytes: Vec<u8>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } + + fn import_from_file_path( + &self, + _view_id: &str, + _name: &str, + _path: String, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Err(FlowyError::not_support()) }) + } +} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs deleted file mode 100644 index e2791827ee..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_chat_impl.rs +++ /dev/null @@ -1,79 +0,0 @@ -use bytes::Bytes; -use collab::entity::EncodedCollab; -use collab_folder::ViewLayout; -use flowy_ai::ai_manager::AIManager; -use flowy_error::FlowyError; -use flowy_folder::entities::CreateViewParams; -use flowy_folder::share::ImportType; -use flowy_folder::view_operation::{FolderOperationHandler, ImportedData}; -use lib_infra::async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; - -pub struct ChatFolderOperation(pub Arc<AIManager>); - -#[async_trait] -impl FolderOperationHandler for ChatFolderOperation { - fn name(&self) -> &str { - "ChatFolderOperationHandler" - } - - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.open_chat(view_id).await - } - - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.close_chat(view_id).await - } - - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.delete_chat(view_id).await - } - - async fn duplicate_view(&self, _view_id: &Uuid) -> Result<Bytes, FlowyError> { - Err(FlowyError::not_support().with_context("Duplicate view")) - } - - async fn create_view_with_view_data( - &self, - _user_id: i64, - _params: CreateViewParams, - ) -> Result<Option<EncodedCollab>, FlowyError> { - Err(FlowyError::not_support().with_context("Can't create view")) - } - - async fn create_default_view( - &self, - user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, - _name: &str, - _layout: ViewLayout, - ) -> Result<(), FlowyError> { - self - .0 - .create_chat(&user_id, parent_view_id, view_id) - .await?; - Ok(()) - } - - async fn import_from_bytes( - &self, - _uid: i64, - _view_id: &Uuid, - _name: &str, - _import_type: ImportType, - _bytes: Vec<u8>, - ) -> Result<Vec<ImportedData>, FlowyError> { - Err(FlowyError::not_support().with_context("import from data")) - } - - async fn import_from_file_path( - &self, - _view_id: &str, - _name: &str, - _path: String, - ) -> Result<(), FlowyError> { - Err(FlowyError::not_support().with_context("import file from path")) - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs deleted file mode 100644 index edc40c6d5b..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_database_impl.rs +++ /dev/null @@ -1,354 +0,0 @@ -#![allow(unused_variables)] -use bytes::Bytes; -use collab::entity::EncodedCollab; -use collab_entity::CollabType; -use collab_folder::{View, ViewLayout}; -use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_database2::entities::DatabaseLayoutPB; -use flowy_database2::services::share::csv::CSVFormat; -use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid}; -use flowy_database2::DatabaseManager; -use flowy_error::FlowyError; -use flowy_folder::entities::{CreateViewParams, ViewLayoutPB}; -use flowy_folder::manager::FolderUser; -use flowy_folder::share::ImportType; -use flowy_folder::view_operation::{ - DatabaseEncodedCollab, FolderOperationHandler, GatherEncodedCollab, ImportedData, ViewData, -}; -use flowy_user::services::data_import::{load_collab_by_object_id, load_collab_by_object_ids}; -use lib_infra::async_trait::async_trait; -use std::collections::HashMap; -use std::path::Path; -use std::sync::Arc; -use uuid::Uuid; - -pub struct DatabaseFolderOperation(pub Arc<DatabaseManager>); - -#[async_trait] -impl FolderOperationHandler for DatabaseFolderOperation { - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.open_database_view(view_id).await?; - Ok(()) - } - - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self - .0 - .close_database_view(view_id.to_string().as_str()) - .await?; - Ok(()) - } - - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - match self - .0 - .delete_database_view(view_id.to_string().as_str()) - .await - { - Ok(_) => tracing::trace!("Delete database view: {}", view_id), - Err(e) => tracing::error!("🔴delete database failed: {}", e), - } - Ok(()) - } - - async fn gather_publish_encode_collab( - &self, - _user: &Arc<dyn FolderUser>, - view_id: &Uuid, - ) -> Result<GatherEncodedCollab, FlowyError> { - let workspace_id = _user.workspace_id()?; - let view_id_str = view_id.to_string(); - // get the collab_object_id for the database. - // - // the collab object_id for the database is not the view_id, - // we should use the view_id to get the database_id - let oid = self.0.get_database_id_with_view_id(&view_id_str).await?; - let row_oids = self - .0 - .get_database_row_ids_with_view_id(&view_id_str) - .await?; - let row_metas = self - .0 - .get_database_row_metas_with_view_id(view_id, row_oids.clone()) - .await?; - let row_document_ids = row_metas - .iter() - .filter_map(|meta| meta.document_id.clone()) - .collect::<Vec<_>>(); - let row_oids = row_oids - .into_iter() - .map(|oid| oid.into_inner()) - .collect::<Vec<_>>(); - let database_metas = self.0.get_all_databases_meta().await; - - let uid = _user - .user_id() - .map_err(|e| e.with_context("unable to get the uid: {}"))?; - - // get the collab db - let collab_db = _user - .collab_db(uid) - .map_err(|e| e.with_context("unable to get the collab"))?; - let collab_db = collab_db.upgrade().ok_or_else(|| { - FlowyError::internal().with_context( - "The collab db has been dropped, indicating that the user has switched to a new account", - ) - })?; - - tokio::task::spawn_blocking(move || { - let collab_read_txn = collab_db.read_txn(); - let database_collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), &oid) - .map_err(|e| { - FlowyError::internal().with_context(format!("load database collab failed: {}", e)) - })?; - - let database_encoded_collab = database_collab - // encode the collab and check the integrity of the collab - .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab)) - .map_err(|e| { - FlowyError::internal().with_context(format!("encode database collab failed: {}", e)) - })?; - - let database_row_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_oids) - .0 - .into_iter() - .map(|(oid, collab)| { - collab - .encode_collab_v1(|c| CollabType::DatabaseRow.validate_require_data(c)) - .map(|encoded| (oid, encoded)) - .map_err(|e| { - FlowyError::internal().with_context(format!("Database row collab error: {}", e)) - }) - }) - .collect::<Result<HashMap<_, _>, FlowyError>>()?; - - let database_relations = database_metas - .into_iter() - .filter_map(|meta| { - meta - .linked_views - .clone() - .into_iter() - .next() - .map(|lv| (meta.database_id, lv)) - }) - .collect::<HashMap<_, _>>(); - - let database_row_document_encoded_collabs = - load_collab_by_object_ids(uid, &workspace_id.to_string(), &collab_read_txn, &row_document_ids) - .0 - .into_iter() - .map(|(oid, collab)| { - collab - .encode_collab_v1(|c| CollabType::Document.validate_require_data(c)) - .map(|encoded| (oid, encoded)) - .map_err(|e| { - FlowyError::internal() - .with_context(format!("Database row document collab error: {}", e)) - }) - }) - .collect::<Result<HashMap<_, _>, FlowyError>>()?; - - Ok(GatherEncodedCollab::Database(DatabaseEncodedCollab { - database_encoded_collab, - database_row_encoded_collabs, - database_row_document_encoded_collabs, - database_relations, - })) - }) - .await? - } - - async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError> { - Ok(Bytes::from(view_id.to_string())) - } - - /// Create a database view with duplicated data. - /// If the ext contains the {"database_id": "xx"}, then it will link - /// to the existing database. - async fn create_view_with_view_data( - &self, - _user_id: i64, - params: CreateViewParams, - ) -> Result<Option<EncodedCollab>, FlowyError> { - match CreateDatabaseExtParams::from_map(params.meta.clone()) { - None => match params.initial_data { - ViewData::DuplicateData(data) => { - let duplicated_view_id = - String::from_utf8(data.to_vec()).map_err(|_| FlowyError::invalid_data())?; - let encoded_collab = self - .0 - .duplicate_database(&duplicated_view_id, ¶ms.view_id.to_string()) - .await?; - Ok(Some(encoded_collab)) - }, - ViewData::Data(data) => { - let encoded_collab = self - .0 - .create_database_with_data(¶ms.view_id.to_string(), data.to_vec()) - .await?; - Ok(Some(encoded_collab)) - }, - ViewData::Empty => Ok(None), - }, - Some(database_params) => { - let layout = match params.layout { - ViewLayoutPB::Board => DatabaseLayoutPB::Board, - ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar, - ViewLayoutPB::Grid => DatabaseLayoutPB::Grid, - ViewLayoutPB::Document | ViewLayoutPB::Chat => { - return Err( - FlowyError::invalid_data().with_context("Can't handle document layout type"), - ); - }, - }; - let name = params.name.to_string(); - let database_view_id = params.view_id.to_string(); - let database_parent_view_id = params.parent_view_id.to_string(); - self - .0 - .create_linked_view( - name, - layout.into(), - database_params.database_id, - database_view_id, - database_parent_view_id, - ) - .await?; - Ok(None) - }, - } - } - - /// Create a database view with build-in data. - /// If the ext contains the {"database_id": "xx"}, then it will link to - /// the existing database. The data of the database will be shared within - /// these references views. - async fn create_default_view( - &self, - user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, - name: &str, - layout: ViewLayout, - ) -> Result<(), FlowyError> { - let name = name.to_string(); - let view_id = view_id.to_string(); - let data = match layout { - ViewLayout::Grid => make_default_grid(&view_id, &name), - ViewLayout::Board => make_default_board(&view_id, &name), - ViewLayout::Calendar => make_default_calendar(&view_id, &name), - ViewLayout::Document | ViewLayout::Chat => { - return Err( - FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)), - ); - }, - }; - let result = self.0.import_database(data).await; - match result { - Ok(_) => Ok(()), - Err(err) => { - if err.is_already_exists() { - Ok(()) - } else { - Err(err) - } - }, - } - } - - async fn import_from_bytes( - &self, - uid: i64, - view_id: &Uuid, - name: &str, - import_type: ImportType, - bytes: Vec<u8>, - ) -> Result<Vec<ImportedData>, FlowyError> { - let format = match import_type { - ImportType::CSV => CSVFormat::Original, - ImportType::AFDatabase => CSVFormat::META, - _ => CSVFormat::Original, - }; - let content = tokio::task::spawn_blocking(move || { - String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err)) - }) - .await??; - let result = self - .0 - .import_csv(view_id.to_string(), content, format) - .await?; - Ok( - result - .encoded_collabs - .into_iter() - .map(|encoded| { - ( - encoded.object_id, - encoded.collab_type, - encoded.encoded_collab, - ) - }) - .collect(), - ) - } - - async fn import_from_file_path( - &self, - view_id: &str, - _name: &str, - path: String, - ) -> Result<(), FlowyError> { - let file_path = Path::new(&path); - if !file_path.exists() { - return Err(FlowyError::record_not_found().with_context("File not found")); - } - - let data = tokio::fs::read(file_path).await?; - let content = - String::from_utf8(data).map_err(|e| FlowyError::invalid_data().with_context(e))?; - let _ = self - .0 - .import_csv(view_id.to_string(), content, CSVFormat::Original) - .await?; - Ok(()) - } - - async fn did_update_view(&self, old: &View, new: &View) -> Result<(), FlowyError> { - let database_layout = match new.layout { - ViewLayout::Document | ViewLayout::Chat => { - return Err(FlowyError::internal().with_context("Can't handle document layout type")); - }, - ViewLayout::Grid => DatabaseLayoutPB::Grid, - ViewLayout::Board => DatabaseLayoutPB::Board, - ViewLayout::Calendar => DatabaseLayoutPB::Calendar, - }; - - if old.layout != new.layout { - self - .0 - .update_database_layout(&new.id, database_layout) - .await?; - Ok(()) - } else { - Ok(()) - } - } - - fn name(&self) -> &str { - "DatabaseFolderOperationHandler" - } -} - -#[derive(Debug, serde::Deserialize)] -struct CreateDatabaseExtParams { - database_id: String, -} - -impl CreateDatabaseExtParams { - pub fn from_map(map: HashMap<String, String>) -> Option<Self> { - let value = serde_json::to_value(map).ok()?; - serde_json::from_value::<Self>(value).ok() - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs deleted file mode 100644 index a843a8eb1f..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/folder_deps_doc_impl.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::deps_resolve::folder_deps::get_encoded_collab_v1_from_disk; -use bytes::Bytes; -use collab::entity::EncodedCollab; -use collab_entity::CollabType; -use collab_folder::hierarchy_builder::NestedViewBuilder; -use collab_folder::ViewLayout; -use flowy_document::entities::DocumentDataPB; -use flowy_document::manager::DocumentManager; -use flowy_document::parser::json::parser::JsonToDocumentParser; -use flowy_error::FlowyError; -use flowy_folder::entities::{CreateViewParams, ViewLayoutPB}; -use flowy_folder::manager::FolderUser; -use flowy_folder::share::ImportType; -use flowy_folder::view_operation::{ - FolderOperationHandler, GatherEncodedCollab, ImportedData, ViewData, -}; -use lib_dispatch::prelude::ToBytes; -use lib_infra::async_trait::async_trait; -use std::convert::TryFrom; -use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::RwLock; -use uuid::Uuid; - -pub struct DocumentFolderOperation(pub Arc<DocumentManager>); -#[async_trait] -impl FolderOperationHandler for DocumentFolderOperation { - fn name(&self) -> &str { - "DocumentFolderOperationHandler" - } - - async fn create_workspace_view( - &self, - uid: i64, - workspace_view_builder: Arc<RwLock<NestedViewBuilder>>, - ) -> Result<(), FlowyError> { - let manager = self.0.clone(); - - let mut write_guard = workspace_view_builder.write().await; - // Create a view named "Getting started" with an icon ⭐️ and the built-in README data. - // Don't modify this code unless you know what you are doing. - write_guard - .with_view_builder(|view_builder| async { - let view = view_builder - .with_name("Getting started") - .with_icon("⭐️") - .build(); - // create a empty document - let json_str = include_str!("../../../assets/read_me.json"); - let document_pb = JsonToDocumentParser::json_str_to_document(json_str).unwrap(); - let view_id = Uuid::from_str(&view.view.id).unwrap(); - manager - .create_document(uid, &view_id, Some(document_pb.into())) - .await - .unwrap(); - view - }) - .await; - Ok(()) - } - - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.open_document(view_id).await?; - Ok(()) - } - - /// Close the document view. - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - self.0.close_document(view_id).await?; - Ok(()) - } - - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError> { - match self.0.delete_document(view_id).await { - Ok(_) => tracing::trace!("Delete document: {}", view_id), - Err(e) => tracing::error!("🔴delete document failed: {}", e), - } - Ok(()) - } - - async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError> { - let data: DocumentDataPB = self.0.get_document_data(view_id).await?.into(); - let data_bytes = data.into_bytes().map_err(|_| FlowyError::invalid_data())?; - Ok(data_bytes) - } - - async fn gather_publish_encode_collab( - &self, - user: &Arc<dyn FolderUser>, - view_id: &Uuid, - ) -> Result<GatherEncodedCollab, FlowyError> { - let encoded_collab = - get_encoded_collab_v1_from_disk(user, view_id.to_string().as_str(), CollabType::Document) - .await?; - Ok(GatherEncodedCollab::Document(encoded_collab)) - } - - async fn create_view_with_view_data( - &self, - user_id: i64, - params: CreateViewParams, - ) -> Result<Option<EncodedCollab>, FlowyError> { - debug_assert_eq!(params.layout, ViewLayoutPB::Document); - let data = match params.initial_data { - ViewData::DuplicateData(data) => Some(DocumentDataPB::try_from(data)?), - ViewData::Data(data) => Some(DocumentDataPB::try_from(data)?), - ViewData::Empty => None, - }; - let encoded_collab = self - .0 - .create_document(user_id, ¶ms.view_id, data.map(|d| d.into())) - .await?; - Ok(Some(encoded_collab)) - } - - /// Create a view with built-in data. - async fn create_default_view( - &self, - user_id: i64, - _parent_view_id: &Uuid, - view_id: &Uuid, - _name: &str, - layout: ViewLayout, - ) -> Result<(), FlowyError> { - debug_assert_eq!(layout, ViewLayout::Document); - match self.0.create_document(user_id, view_id, None).await { - Ok(_) => Ok(()), - Err(err) => { - if err.is_already_exists() { - Ok(()) - } else { - Err(err) - } - }, - } - } - - async fn import_from_bytes( - &self, - uid: i64, - view_id: &Uuid, - _name: &str, - _import_type: ImportType, - bytes: Vec<u8>, - ) -> Result<Vec<ImportedData>, FlowyError> { - let data = DocumentDataPB::try_from(Bytes::from(bytes))?; - let encoded_collab = self - .0 - .create_document(uid, view_id, Some(data.into())) - .await?; - Ok(vec![( - view_id.to_string(), - CollabType::Document, - encoded_collab, - )]) - } - - async fn import_from_file_path( - &self, - _view_id: &str, - _name: &str, - _path: String, - ) -> Result<(), FlowyError> { - // TODO(lucas): import file from local markdown file - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs deleted file mode 100644 index 02b26e71b6..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps/mod.rs +++ /dev/null @@ -1,255 +0,0 @@ -mod folder_deps_chat_impl; -mod folder_deps_database_impl; -mod folder_deps_doc_impl; - -use crate::server_layer::ServerProvider; -use collab_entity::{CollabType, EncodedCollab}; -use collab_integrate::collab_builder::AppFlowyCollabBuilder; -use collab_integrate::CollabKVDB; -use flowy_ai::ai_manager::AIManager; -use flowy_database2::DatabaseManager; -use flowy_document::manager::DocumentManager; -use flowy_error::{internal_error, FlowyError, FlowyResult}; -use flowy_folder::entities::UpdateViewParams; -use flowy_folder::manager::{FolderManager, FolderUser}; -use flowy_folder::ViewLayout; -use flowy_search::folder::indexer::FolderIndexManagerImpl; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_user::services::authenticate_user::AuthenticateUser; -use flowy_user::services::data_import::load_collab_by_object_id; -use std::str::FromStr; -use std::sync::{Arc, Weak}; - -use crate::deps_resolve::folder_deps::folder_deps_chat_impl::ChatFolderOperation; -use crate::deps_resolve::folder_deps::folder_deps_database_impl::DatabaseFolderOperation; -use crate::deps_resolve::folder_deps::folder_deps_doc_impl::DocumentFolderOperation; -use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_folder_pub::query::{FolderQueryService, FolderService, FolderViewEdit, QueryCollab}; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; - -pub struct FolderDepsResolver(); -#[allow(clippy::too_many_arguments)] -impl FolderDepsResolver { - pub async fn resolve( - authenticate_user: Weak<AuthenticateUser>, - collab_builder: Arc<AppFlowyCollabBuilder>, - server_provider: Arc<ServerProvider>, - folder_indexer: Arc<FolderIndexManagerImpl>, - store_preferences: Arc<KVStorePreferences>, - ) -> Arc<FolderManager> { - let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl { - authenticate_user: authenticate_user.clone(), - }); - - Arc::new( - FolderManager::new( - user.clone(), - collab_builder, - server_provider.clone(), - folder_indexer, - store_preferences, - ) - .unwrap(), - ) - } -} - -pub fn register_handlers( - folder_manager: &Arc<FolderManager>, - document_manager: Arc<DocumentManager>, - database_manager: Arc<DatabaseManager>, - chat_manager: Arc<AIManager>, -) { - let document_folder_operation = Arc::new(DocumentFolderOperation(document_manager)); - folder_manager.register_operation_handler(ViewLayout::Document, document_folder_operation); - - let database_folder_operation = Arc::new(DatabaseFolderOperation(database_manager)); - let chat_folder_operation = Arc::new(ChatFolderOperation(chat_manager)); - folder_manager.register_operation_handler(ViewLayout::Board, database_folder_operation.clone()); - folder_manager.register_operation_handler(ViewLayout::Grid, database_folder_operation.clone()); - folder_manager.register_operation_handler(ViewLayout::Calendar, database_folder_operation); - folder_manager.register_operation_handler(ViewLayout::Chat, chat_folder_operation); -} - -struct FolderUserImpl { - authenticate_user: Weak<AuthenticateUser>, -} - -impl FolderUserImpl { - fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> { - let user = self - .authenticate_user - .upgrade() - .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?; - Ok(user) - } -} - -impl FolderUser for FolderUserImpl { - fn user_id(&self) -> Result<i64, FlowyError> { - self.upgrade_user()?.user_id() - } - - fn workspace_id(&self) -> Result<Uuid, FlowyError> { - self.upgrade_user()?.workspace_id() - } - - fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError> { - self.upgrade_user()?.get_collab_db(uid) - } - - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult<bool> { - self - .upgrade_user()? - .is_collab_on_disk(uid, workspace_id.to_string().as_str()) - } -} - -#[derive(Clone)] -pub struct FolderServiceImpl { - folder_manager: Weak<FolderManager>, - user: Arc<dyn FolderUser>, -} -impl FolderService for FolderServiceImpl {} - -impl FolderServiceImpl { - pub fn new( - folder_manager: Weak<FolderManager>, - authenticate_user: Weak<AuthenticateUser>, - ) -> Self { - let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl { authenticate_user }); - Self { - folder_manager, - user, - } - } -} - -#[async_trait] -impl FolderViewEdit for FolderServiceImpl { - async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()> { - if title.is_empty() { - return Ok(()); - } - - if let Some(folder_manager) = self.folder_manager.upgrade() { - if let Ok(view) = folder_manager.get_view(view_id.to_string().as_str()).await { - if view.name.is_empty() { - let title = if title.len() > 50 { - title.chars().take(50).collect() - } else { - title.to_string() - }; - - folder_manager - .update_view_with_params(UpdateViewParams { - view_id: view_id.to_string(), - name: Some(title), - desc: None, - thumbnail: None, - layout: None, - is_favorite: None, - extra: None, - }) - .await?; - } - } - } - Ok(()) - } -} - -#[async_trait] -impl FolderQueryService for FolderServiceImpl { - async fn get_surrounding_view_ids_with_view_layout( - &self, - parent_view_id: &Uuid, - view_layout: ViewLayout, - ) -> Vec<Uuid> { - let folder_manager = match self.folder_manager.upgrade() { - Some(folder_manager) => folder_manager, - None => return vec![], - }; - - if let Ok(view) = folder_manager - .get_view(parent_view_id.to_string().as_str()) - .await - { - if view.space_info().is_some() { - return vec![]; - } - } - - match folder_manager - .get_untrashed_views_belong_to(parent_view_id.to_string().as_str()) - .await - { - Ok(views) => { - let mut children = views - .into_iter() - .filter_map(|child| { - if child.layout == view_layout { - Uuid::from_str(&child.id).ok() - } else { - None - } - }) - .collect::<Vec<_>>(); - children.push(*parent_view_id); - children - }, - _ => vec![], - } - } - - async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option<QueryCollab> { - let encode_collab = - get_encoded_collab_v1_from_disk(&self.user, object_id.to_string().as_str(), collab_type) - .await - .ok(); - - encode_collab.map(|encoded_collab| QueryCollab { - collab_type, - encoded_collab, - }) - } -} - -#[inline] -async fn get_encoded_collab_v1_from_disk( - user: &Arc<dyn FolderUser>, - view_id: &str, - collab_type: CollabType, -) -> Result<EncodedCollab, FlowyError> { - let workspace_id = user.workspace_id()?; - let uid = user - .user_id() - .map_err(|e| e.with_context("unable to get the uid: {}"))?; - - // get the collab db - let collab_db = user - .collab_db(uid) - .map_err(|e| e.with_context("unable to get the collab"))?; - let collab_db = collab_db.upgrade().ok_or_else(|| { - FlowyError::internal().with_context( - "The collab db has been dropped, indicating that the user has switched to a new account", - ) - })?; - let collab_read_txn = collab_db.read_txn(); - let collab = load_collab_by_object_id(uid, &collab_read_txn, &workspace_id.to_string(), view_id) - .map_err(|e| { - FlowyError::internal().with_context(format!("load document collab failed: {}", e)) - })?; - - tokio::task::spawn_blocking(move || { - let data = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .map_err(|e| { - FlowyError::internal().with_context(format!("encode document collab failed: {}", e)) - })?; - Ok::<_, FlowyError>(data) - }) - .await - .map_err(internal_error)? -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs index 7e1f8b942f..a75589e89e 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/mod.rs @@ -8,12 +8,9 @@ pub use user_deps::*; mod collab_deps; mod document_deps; +mod folder_deps; mod chat_deps; -mod cloud_service_impl; mod database_deps; -pub mod file_storage_deps; -mod folder_deps; -pub(crate) mod reminder_deps; mod search_deps; mod user_deps; diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/reminder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/reminder_deps.rs deleted file mode 100644 index 29022b6fdc..0000000000 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/reminder_deps.rs +++ /dev/null @@ -1,57 +0,0 @@ -use collab_entity::reminder::Reminder; -use std::convert::TryFrom; -use std::sync::Weak; - -use flowy_database2::DatabaseManager; -use flowy_document::manager::DocumentManager; -use flowy_document::reminder::{DocumentReminder, DocumentReminderAction}; -use flowy_folder_pub::cloud::Error; -use flowy_user::services::collab_interact::UserReminder; -use lib_infra::async_trait::async_trait; - -pub struct CollabInteractImpl { - #[allow(dead_code)] - pub(crate) database_manager: Weak<DatabaseManager>, - #[allow(dead_code)] - pub(crate) document_manager: Weak<DocumentManager>, -} - -#[async_trait] -impl UserReminder for CollabInteractImpl { - async fn add_reminder(&self, reminder: Reminder) -> Result<(), Error> { - if let Some(document_manager) = self.document_manager.upgrade() { - match DocumentReminder::try_from(reminder) { - Ok(reminder) => { - document_manager - .handle_reminder_action(DocumentReminderAction::Add { reminder }) - .await; - }, - Err(e) => tracing::error!("Failed to add reminder: {:?}", e), - } - } - Ok(()) - } - - async fn remove_reminder(&self, reminder_id: &str) -> Result<(), Error> { - let reminder_id = reminder_id.to_string(); - if let Some(document_manager) = self.document_manager.upgrade() { - let action = DocumentReminderAction::Remove { reminder_id }; - document_manager.handle_reminder_action(action).await; - } - Ok(()) - } - - async fn update_reminder(&self, reminder: Reminder) -> Result<(), Error> { - if let Some(document_manager) = self.document_manager.upgrade() { - match DocumentReminder::try_from(reminder) { - Ok(reminder) => { - document_manager - .handle_reminder_action(DocumentReminderAction::Update { reminder }) - .await; - }, - Err(e) => tracing::error!("Failed to update reminder: {:?}", e), - } - } - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs index b31853a803..cbb6e3c7d7 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/search_deps.rs @@ -1,5 +1,4 @@ use flowy_folder::manager::FolderManager; -use flowy_search::document::handler::DocumentSearchHandler; use flowy_search::folder::handler::FolderSearchHandler; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; @@ -10,11 +9,12 @@ pub struct SearchDepsResolver(); impl SearchDepsResolver { pub async fn resolve( folder_indexer: Arc<FolderIndexManagerImpl>, - cloud_service: Arc<dyn SearchCloudService>, - folder_manager: Arc<FolderManager>, + _cloud_service: Arc<dyn SearchCloudService>, + _folder_manager: Arc<FolderManager>, ) -> Arc<SearchManager> { let folder_handler = Arc::new(FolderSearchHandler::new(folder_indexer)); - let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager)); - Arc::new(SearchManager::new(vec![folder_handler, document_handler])) + // TODO(Mathias): Enable when Cloud Search is ready + // let document_handler = Arc::new(DocumentSearchHandler::new(cloud_service, folder_manager)); + Arc::new(SearchManager::new(vec![folder_handler])) } } diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs index 73c2844a23..1d580e6cee 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/user_deps.rs @@ -1,19 +1,16 @@ -use crate::server_layer::ServerProvider; -use collab_folder::hierarchy_builder::ParentChildViews; +use crate::integrate::server::ServerProvider; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_database2::DatabaseManager; use flowy_error::FlowyResult; use flowy_folder::manager::FolderManager; -use flowy_folder_pub::entities::ImportFrom; -use flowy_sqlite::kv::KVStorePreferences; +use flowy_folder_pub::folder_builder::ParentChildViews; +use flowy_sqlite::kv::StorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::user_manager::UserManager; use flowy_user_pub::workspace_service::UserWorkspaceService; use lib_infra::async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; -use tracing::info; -use uuid::Uuid; pub struct UserDepsResolver(); @@ -22,7 +19,7 @@ impl UserDepsResolver { authenticate_user: Arc<AuthenticateUser>, collab_builder: Arc<AppFlowyCollabBuilder>, server_provider: Arc<ServerProvider>, - store_preference: Arc<KVStorePreferences>, + store_preference: Arc<StorePreferences>, database_manager: Arc<DatabaseManager>, folder_manager: Arc<FolderManager>, ) -> Arc<UserManager> { @@ -47,31 +44,12 @@ pub struct UserWorkspaceServiceImpl { #[async_trait] impl UserWorkspaceService for UserWorkspaceServiceImpl { - async fn import_views( - &self, - source: &ImportFrom, - views: Vec<ParentChildViews>, - orphan_views: Vec<ParentChildViews>, - parent_view_id: Option<String>, - ) -> FlowyResult<()> { - match source { - ImportFrom::AnonUser => { - self - .folder_manager - .insert_views_as_spaces(views, orphan_views) - .await?; - }, - ImportFrom::AppFlowyDataFolder => { - self - .folder_manager - .insert_views_with_parent(views, orphan_views, parent_view_id) - .await?; - }, - } + async fn did_import_views(&self, views: Vec<ParentChildViews>) -> FlowyResult<()> { + self.folder_manager.insert_parent_child_views(views).await?; Ok(()) } - async fn import_database_views( + async fn did_import_database_views( &self, ids_by_database_id: HashMap<String, Vec<String>>, ) -> FlowyResult<()> { @@ -82,16 +60,10 @@ impl UserWorkspaceService for UserWorkspaceServiceImpl { Ok(()) } - async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { - // The remove_indices_for_workspace should not block the deletion of the workspace - // Log the error and continue - if let Err(err) = self + fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()> { + self .folder_manager - .remove_indices_for_workspace(workspace_id) - .await - { - info!("Error removing indices for workspace: {}", err); - } + .remove_indices_for_workspace(workspace_id)?; Ok(()) } diff --git a/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs new file mode 100644 index 0000000000..171fc20010 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs @@ -0,0 +1,65 @@ +use collab_entity::reminder::Reminder; +use std::convert::TryFrom; +use std::sync::Weak; + +use flowy_database2::DatabaseManager; +use flowy_document::manager::DocumentManager; +use flowy_document::reminder::{DocumentReminder, DocumentReminderAction}; +use flowy_folder_pub::cloud::Error; +use flowy_user::services::collab_interact::CollabInteract; +use lib_infra::future::FutureResult; + +pub struct CollabInteractImpl { + #[allow(dead_code)] + pub(crate) database_manager: Weak<DatabaseManager>, + #[allow(dead_code)] + pub(crate) document_manager: Weak<DocumentManager>, +} + +impl CollabInteract for CollabInteractImpl { + fn add_reminder(&self, reminder: Reminder) -> FutureResult<(), Error> { + let cloned_document_manager = self.document_manager.clone(); + FutureResult::new(async move { + if let Some(document_manager) = cloned_document_manager.upgrade() { + match DocumentReminder::try_from(reminder) { + Ok(reminder) => { + document_manager + .handle_reminder_action(DocumentReminderAction::Add { reminder }) + .await; + }, + Err(e) => tracing::error!("Failed to add reminder: {:?}", e), + } + } + Ok(()) + }) + } + + fn remove_reminder(&self, reminder_id: &str) -> FutureResult<(), Error> { + let reminder_id = reminder_id.to_string(); + let cloned_document_manager = self.document_manager.clone(); + FutureResult::new(async move { + if let Some(document_manager) = cloned_document_manager.upgrade() { + let action = DocumentReminderAction::Remove { reminder_id }; + document_manager.handle_reminder_action(action).await; + } + Ok(()) + }) + } + + fn update_reminder(&self, reminder: Reminder) -> FutureResult<(), Error> { + let cloned_document_manager = self.document_manager.clone(); + FutureResult::new(async move { + if let Some(document_manager) = cloned_document_manager.upgrade() { + match DocumentReminder::try_from(reminder) { + Ok(reminder) => { + document_manager + .handle_reminder_action(DocumentReminderAction::Update { reminder }) + .await; + }, + Err(e) => tracing::error!("Failed to update reminder: {:?}", e), + } + } + Ok(()) + }) + } +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs new file mode 100644 index 0000000000..c855a557ac --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -0,0 +1,83 @@ +use lib_infra::util::Platform; +use lib_log::stream_log::StreamLogSender; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use crate::AppFlowyCoreConfig; + +static INIT_LOG: AtomicBool = AtomicBool::new(false); +pub(crate) fn init_log( + config: &AppFlowyCoreConfig, + platform: &Platform, + stream_log_sender: Option<Arc<dyn StreamLogSender>>, +) { + #[cfg(debug_assertions)] + if get_bool_from_env_var("DISABLE_CI_TEST_LOG") { + return; + } + + if !INIT_LOG.load(Ordering::SeqCst) { + INIT_LOG.store(true, Ordering::SeqCst); + + let _ = lib_log::Builder::new("log", &config.storage_path, platform, stream_log_sender) + .env_filter(&config.log_filter) + .build(); + } +} + +pub fn create_log_filter(level: String, with_crates: Vec<String>, platform: Platform) -> String { + let mut level = std::env::var("RUST_LOG").unwrap_or(level); + + #[cfg(debug_assertions)] + if matches!(platform, Platform::IOS) { + level = "trace".to_string(); + } + + let mut filters = with_crates + .into_iter() + .map(|crate_name| format!("{}={}", crate_name, level)) + .collect::<Vec<String>>(); + filters.push(format!("flowy_core={}", level)); + filters.push(format!("flowy_folder={}", level)); + filters.push(format!("collab_sync={}", level)); + filters.push(format!("collab_folder={}", level)); + filters.push(format!("collab_database={}", level)); + filters.push(format!("collab_plugins={}", level)); + filters.push(format!("collab_integrate={}", level)); + filters.push(format!("collab={}", level)); + filters.push(format!("flowy_user={}", level)); + filters.push(format!("flowy_document={}", level)); + filters.push(format!("flowy_database2={}", level)); + filters.push(format!("flowy_server={}", level)); + filters.push(format!("flowy_notification={}", "info")); + filters.push(format!("lib_infra={}", level)); + filters.push(format!("flowy_search={}", level)); + filters.push(format!("flowy_chat={}", level)); + // Enable the frontend logs. DO NOT DISABLE. + // These logs are essential for debugging and verifying frontend behavior. + filters.push(format!("dart_ffi={}", level)); + + // Most of the time, we don't need to see the logs from the following crates + // filters.push(format!("flowy_sqlite={}", "info")); + // filters.push(format!("lib_dispatch={}", level)); + + filters.push(format!("client_api={}", level)); + #[cfg(feature = "profiling")] + filters.push(format!("tokio={}", level)); + #[cfg(feature = "profiling")] + filters.push(format!("runtime={}", level)); + + filters.join(",") +} + +#[cfg(debug_assertions)] +fn get_bool_from_env_var(env_var_name: &str) -> bool { + match std::env::var(env_var_name) { + Ok(value) => match value.to_lowercase().as_str() { + "true" | "1" => true, + "false" | "0" => false, + _ => false, + }, + Err(_) => false, + } +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/mod.rs b/frontend/rust-lib/flowy-core/src/integrate/mod.rs new file mode 100644 index 0000000000..129a22a99f --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod collab_interact; +pub mod log; +pub(crate) mod server; +mod trait_impls; +pub(crate) mod user; diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs new file mode 100644 index 0000000000..2959ae7f21 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -0,0 +1,217 @@ +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::sync::{Arc, Weak}; + +use parking_lot::RwLock; +use serde_repr::*; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::local_server::{LocalServer, LocalServerDB}; +use flowy_server::supabase::SupabaseServer; +use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_server_pub::AuthenticatorType; +use flowy_sqlite::kv::StorePreferences; +use flowy_user_pub::entities::*; + +use crate::AppFlowyCoreConfig; + +#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum Server { + /// Local server provider. + /// Offline mode, no user authentication and the data is stored locally. + Local = 0, + /// AppFlowy Cloud server provider. + /// The [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Cloud) is still a work in + /// progress. + AppFlowyCloud = 1, + /// Supabase server provider. + /// It uses supabase postgresql database to store data and user authentication. + Supabase = 2, +} + +impl Server { + pub fn is_local(&self) -> bool { + matches!(self, Server::Local) + } +} + +impl Display for Server { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Server::Local => write!(f, "Local"), + Server::AppFlowyCloud => write!(f, "AppFlowyCloud"), + Server::Supabase => write!(f, "Supabase"), + } + } +} + +/// The [ServerProvider] provides list of [AppFlowyServer] base on the [Authenticator]. Using +/// the auth type, the [ServerProvider] will create a new [AppFlowyServer] if it doesn't +/// exist. +/// Each server implements the [AppFlowyServer] trait, which provides the [UserCloudService], etc. +pub struct ServerProvider { + config: AppFlowyCoreConfig, + providers: RwLock<HashMap<Server, Arc<dyn AppFlowyServer>>>, + pub(crate) encryption: RwLock<Arc<dyn AppFlowyEncryption>>, + #[allow(dead_code)] + pub(crate) store_preferences: Weak<StorePreferences>, + pub(crate) user_enable_sync: RwLock<bool>, + + /// The authenticator type of the user. + authenticator: RwLock<Authenticator>, + user: Arc<dyn ServerUser>, + pub(crate) uid: Arc<RwLock<Option<i64>>>, +} + +impl ServerProvider { + pub fn new( + config: AppFlowyCoreConfig, + server: Server, + store_preferences: Weak<StorePreferences>, + server_user: impl ServerUser + 'static, + ) -> Self { + let user = Arc::new(server_user); + let encryption = EncryptionImpl::new(None); + Self { + config, + providers: RwLock::new(HashMap::new()), + user_enable_sync: RwLock::new(true), + authenticator: RwLock::new(Authenticator::from(server)), + encryption: RwLock::new(Arc::new(encryption)), + store_preferences, + uid: Default::default(), + user, + } + } + + pub fn get_server_type(&self) -> Server { + match &*self.authenticator.read() { + Authenticator::Local => Server::Local, + Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + Authenticator::Supabase => Server::Supabase, + } + } + + pub fn set_authenticator(&self, authenticator: Authenticator) { + let old_server_type = self.get_server_type(); + *self.authenticator.write() = authenticator; + let new_server_type = self.get_server_type(); + + if old_server_type != new_server_type { + self.providers.write().remove(&old_server_type); + } + } + + pub fn get_authenticator(&self) -> Authenticator { + self.authenticator.read().clone() + } + + /// Returns a [AppFlowyServer] trait implementation base on the provider_type. + pub fn get_server(&self) -> FlowyResult<Arc<dyn AppFlowyServer>> { + let server_type = self.get_server_type(); + + if let Some(provider) = self.providers.read().get(&server_type) { + return Ok(provider.clone()); + } + + let server = match server_type { + Server::Local => { + let local_db = Arc::new(LocalServerDBImpl { + storage_path: self.config.storage_path.clone(), + }); + let server = Arc::new(LocalServer::new(local_db)); + Ok::<Arc<dyn AppFlowyServer>, FlowyError>(server) + }, + Server::AppFlowyCloud => { + let config = AFCloudConfiguration::from_env()?; + let server = Arc::new(AppFlowyCloudServer::new( + config, + *self.user_enable_sync.read(), + self.config.device_id.clone(), + self.config.app_version.clone(), + self.user.clone(), + )); + + Ok::<Arc<dyn AppFlowyServer>, FlowyError>(server) + }, + Server::Supabase => { + let config = SupabaseConfiguration::from_env()?; + let uid = self.uid.clone(); + tracing::trace!("🔑Supabase config: {:?}", config); + let encryption = Arc::downgrade(&*self.encryption.read()); + Ok::<Arc<dyn AppFlowyServer>, FlowyError>(Arc::new(SupabaseServer::new( + uid, + config, + *self.user_enable_sync.read(), + self.config.device_id.clone(), + encryption, + ))) + }, + }?; + + self + .providers + .write() + .insert(server_type.clone(), server.clone()); + Ok(server) + } +} + +impl From<Authenticator> for Server { + fn from(auth_provider: Authenticator) -> Self { + match auth_provider { + Authenticator::Local => Server::Local, + Authenticator::AppFlowyCloud => Server::AppFlowyCloud, + Authenticator::Supabase => Server::Supabase, + } + } +} + +impl From<Server> for Authenticator { + fn from(ty: Server) -> Self { + match ty { + Server::Local => Authenticator::Local, + Server::AppFlowyCloud => Authenticator::AppFlowyCloud, + Server::Supabase => Authenticator::Supabase, + } + } +} +impl From<&Authenticator> for Server { + fn from(auth_provider: &Authenticator) -> Self { + Self::from(auth_provider.clone()) + } +} + +pub fn current_server_type() -> Server { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => Server::Local, + AuthenticatorType::Supabase => Server::Supabase, + AuthenticatorType::AppFlowyCloud => Server::AppFlowyCloud, + } +} + +struct LocalServerDBImpl { + #[allow(dead_code)] + storage_path: String, +} + +impl LocalServerDB for LocalServerDBImpl { + fn get_user_profile(&self, _uid: i64) -> Result<UserProfile, FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("LocalServer doesn't support get_user_profile"), + ) + } + + fn get_user_workspace(&self, _uid: i64) -> Result<Option<UserWorkspace>, FlowyError> { + Err( + FlowyError::local_version_not_support() + .with_context("LocalServer doesn't support get_user_workspace"), + ) + } +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs new file mode 100644 index 0000000000..1853fd12d3 --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -0,0 +1,620 @@ +use client_api::entity::search_dto::SearchDocumentResponseItem; +use flowy_search_pub::cloud::SearchCloudService; +use flowy_storage::{ObjectIdentity, ObjectStorageService}; +use std::sync::Arc; + +use anyhow::Error; +use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; +use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::ChatMessageType; +use collab::core::origin::{CollabClient, CollabOrigin}; + +use collab::preclude::CollabPlugin; +use collab_entity::CollabType; +use collab_plugins::cloud_storage::postgres::SupabaseDBPlugin; +use tokio_stream::wrappers::WatchStream; +use tracing::debug; + +use collab_integrate::collab_builder::{ + CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, +}; +use flowy_chat_pub::cloud::{ + ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage, + StreamAnswer, +}; +use flowy_database_pub::cloud::{ + CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, +}; +use flowy_document::deps::DocumentData; +use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::{ + FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, +}; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_storage::ObjectValue; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; +use flowy_user_pub::entities::{Authenticator, UserTokenState}; +use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; + +use crate::integrate::server::{Server, ServerProvider}; + +impl ObjectStorageService for ServerProvider { + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult<String, FlowyError> { + let server = self.get_server(); + FutureResult::new(async move { + let storage = server?.file_storage().ok_or(FlowyError::internal())?; + storage.get_object_url(object_id).await + }) + } + + fn put_object(&self, url: String, val: ObjectValue) -> FutureResult<(), FlowyError> { + let server = self.get_server(); + FutureResult::new(async move { + let storage = server?.file_storage().ok_or(FlowyError::internal())?; + storage.put_object(url, val).await + }) + } + + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError> { + let server = self.get_server(); + FutureResult::new(async move { + let storage = server?.file_storage().ok_or(FlowyError::internal())?; + storage.delete_object(url).await + }) + } + + fn get_object(&self, url: String) -> FutureResult<flowy_storage::ObjectValue, FlowyError> { + let server = self.get_server(); + FutureResult::new(async move { + let storage = server?.file_storage().ok_or(FlowyError::internal())?; + storage.get_object(url).await + }) + } +} + +impl UserCloudServiceProvider for ServerProvider { + fn set_token(&self, token: &str) -> Result<(), FlowyError> { + let server = self.get_server()?; + server.set_token(token)?; + Ok(()) + } + + fn subscribe_token_state(&self) -> Option<WatchStream<UserTokenState>> { + let server = self.get_server().ok()?; + server.subscribe_token_state() + } + + fn set_enable_sync(&self, uid: i64, enable_sync: bool) { + if let Ok(server) = self.get_server() { + server.set_enable_sync(uid, enable_sync); + *self.user_enable_sync.write() = enable_sync; + *self.uid.write() = Some(uid); + } + } + + /// When user login, the provider type is set by the [Authenticator] and save to disk for next use. + /// + /// Each [Authenticator] has a corresponding [Server]. The [Server] is used + /// to create a new [AppFlowyServer] if it doesn't exist. Once the [Server] is set, + /// it will be used when user open the app again. + /// + fn set_user_authenticator(&self, authenticator: &Authenticator) { + self.set_authenticator(authenticator.clone()); + } + + fn get_user_authenticator(&self) -> Authenticator { + self.get_authenticator() + } + + fn set_network_reachable(&self, reachable: bool) { + if let Ok(server) = self.get_server() { + server.set_network_reachable(reachable); + } + } + + fn set_encrypt_secret(&self, secret: String) { + tracing::info!("🔑Set encrypt secret"); + self.encryption.write().set_secret(secret); + } + + /// Returns the [UserCloudService] base on the current [Server]. + /// Creates a new [AppFlowyServer] if it doesn't exist. + fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> { + let user_service = self.get_server()?.user_service(); + Ok(user_service) + } + + fn service_url(&self) -> String { + match self.get_server_type() { + Server::Local => "".to_string(), + Server::AppFlowyCloud => AFCloudConfiguration::from_env() + .map(|config| config.base_url) + .unwrap_or_default(), + Server::Supabase => SupabaseConfiguration::from_env() + .map(|config| config.url) + .unwrap_or_default(), + } + } +} + +impl FolderCloudService for ServerProvider { + fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, Error> { + let server = self.get_server(); + let name = name.to_string(); + FutureResult::new(async move { server?.folder_service().create_workspace(uid, &name).await }) + } + + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { server?.folder_service().open_workspace(&workspace_id).await }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + let server = self.get_server(); + FutureResult::new(async move { server?.folder_service().get_all_workspace().await }) + } + + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult<Option<FolderData>, Error> { + let uid = *uid; + let server = self.get_server(); + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + server? + .folder_service() + .get_folder_data(&workspace_id, &uid) + .await + }) + } + + fn get_folder_snapshots( + &self, + workspace_id: &str, + limit: usize, + ) -> FutureResult<Vec<FolderSnapshot>, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .get_folder_snapshots(&workspace_id, limit) + .await + }) + } + + fn get_folder_doc_state( + &self, + workspace_id: &str, + uid: i64, + collab_type: CollabType, + object_id: &str, + ) -> FutureResult<Vec<u8>, Error> { + let object_id = object_id.to_string(); + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .get_folder_doc_state(&workspace_id, uid, collab_type, &object_id) + .await + }) + } + + fn batch_create_folder_collab_objects( + &self, + workspace_id: &str, + objects: Vec<FolderCollabParams>, + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .folder_service() + .batch_create_folder_collab_objects(&workspace_id, objects) + .await + }) + } + + fn service_name(&self) -> String { + self + .get_server() + .map(|provider| provider.folder_service().service_name()) + .unwrap_or_default() + } +} + +impl DatabaseCloudService for ServerProvider { + fn get_database_object_doc_state( + &self, + object_id: &str, + collab_type: CollabType, + workspace_id: &str, + ) -> FutureResult<Option<Vec<u8>>, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let database_id = object_id.to_string(); + FutureResult::new(async move { + server? + .database_service() + .get_database_object_doc_state(&database_id, collab_type, &workspace_id) + .await + }) + } + + fn batch_get_database_object_doc_state( + &self, + object_ids: Vec<String>, + object_ty: CollabType, + workspace_id: &str, + ) -> FutureResult<CollabDocStateByOid, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .database_service() + .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) + .await + }) + } + + fn get_database_collab_object_snapshots( + &self, + object_id: &str, + limit: usize, + ) -> FutureResult<Vec<DatabaseSnapshot>, Error> { + let server = self.get_server(); + let database_id = object_id.to_string(); + FutureResult::new(async move { + server? + .database_service() + .get_database_collab_object_snapshots(&database_id, limit) + .await + }) + } + + fn summary_database_row( + &self, + workspace_id: &str, + object_id: &str, + summary_row: SummaryRowContent, + ) -> FutureResult<String, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let object_id = object_id.to_string(); + FutureResult::new(async move { + server? + .database_service() + .summary_database_row(&workspace_id, &object_id, summary_row) + .await + }) + } + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult<TranslateRowResponse, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let language = language.to_string(); + FutureResult::new(async move { + server? + .database_service() + .translate_database_row(&workspace_id, translate_row, &language) + .await + }) + } +} + +impl DocumentCloudService for ServerProvider { + fn get_document_doc_state( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { + let workspace_id = workspace_id.to_string(); + let document_id = document_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .document_service() + .get_document_doc_state(&document_id, &workspace_id) + .await + }) + } + + fn get_document_snapshots( + &self, + document_id: &str, + limit: usize, + workspace_id: &str, + ) -> FutureResult<Vec<DocumentSnapshot>, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let document_id = document_id.to_string(); + FutureResult::new(async move { + server? + .document_service() + .get_document_snapshots(&document_id, limit, &workspace_id) + .await + }) + } + + fn get_document_data( + &self, + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Option<DocumentData>, Error> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let document_id = document_id.to_string(); + FutureResult::new(async move { + server? + .document_service() + .get_document_data(&document_id, &workspace_id) + .await + }) + } +} + +impl CollabCloudPluginProvider for ServerProvider { + fn provider_type(&self) -> CollabPluginProviderType { + self.get_server_type().into() + } + + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec<Box<dyn CollabPlugin>> { + // If the user is local, we don't need to create a sync plugin. + if self.get_server_type().is_local() { + debug!( + "User authenticator is local, skip create sync plugin for: {}", + context + ); + return vec![]; + } + + match context { + CollabPluginProviderContext::Local => vec![], + CollabPluginProviderContext::AppFlowyCloud { + uid: _, + collab_object, + local_collab, + } => { + if let Ok(server) = self.get_server() { + // to_fut(async move { + let mut plugins: Vec<Box<dyn CollabPlugin>> = vec![]; + // If the user is local, we don't need to create a sync plugin. + + match server.collab_ws_channel(&collab_object.object_id) { + Ok(Some((channel, ws_connect_state, _is_connected))) => { + let origin = CollabOrigin::Client(CollabClient::new( + collab_object.uid, + collab_object.device_id.clone(), + )); + let sync_object = SyncObject::new( + &collab_object.object_id, + &collab_object.workspace_id, + collab_object.collab_type, + &collab_object.device_id, + ); + let (sink, stream) = (channel.sink(), channel.stream()); + let sink_config = SinkConfig::new().send_timeout(8); + let sync_plugin = SyncPlugin::new( + origin, + sync_object, + local_collab, + sink, + sink_config, + stream, + Some(channel), + ws_connect_state, + ); + plugins.push(Box::new(sync_plugin)); + }, + Ok(None) => { + tracing::error!("🔴Failed to get collab ws channel: channel is none"); + }, + Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err), + } + plugins + } else { + vec![] + } + }, + CollabPluginProviderContext::Supabase { + uid, + collab_object, + local_collab, + local_collab_db, + } => { + let mut plugins: Vec<Box<dyn CollabPlugin>> = vec![]; + if let Some(remote_collab_storage) = self + .get_server() + .ok() + .and_then(|provider| provider.collab_storage(&collab_object)) + { + plugins.push(Box::new(SupabaseDBPlugin::new( + uid, + collab_object, + local_collab, + 1, + remote_collab_storage, + local_collab_db, + ))); + } + plugins + }, + } + } + + fn is_sync_enabled(&self) -> bool { + *self.user_enable_sync.read() + } +} + +#[async_trait] +impl ChatCloudService for ServerProvider { + fn create_chat( + &self, + uid: &i64, + workspace_id: &str, + chat_id: &str, + ) -> FutureResult<(), FlowyError> { + let workspace_id = workspace_id.to_string(); + let server = self.get_server(); + let chat_id = chat_id.to_string(); + let uid = *uid; + FutureResult::new(async move { + server? + .chat_service() + .create_chat(&uid, &workspace_id, &chat_id) + .await + }) + } + + async fn send_chat_message( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + ) -> Result<ChatMessageStream, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let message = message.to_string(); + let server = self.get_server()?; + server + .chat_service() + .send_chat_message(&workspace_id, &chat_id, &message, message_type) + .await + } + + fn send_question( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let message = message.to_string(); + let server = self.get_server(); + + FutureResult::new(async move { + server? + .chat_service() + .send_question(&workspace_id, &chat_id, &message, message_type) + .await + }) + } + + fn save_answer( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + question_id: i64, + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let message = message.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .chat_service() + .save_answer(&workspace_id, &chat_id, &message, question_id) + .await + }) + } + + async fn stream_answer( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> Result<StreamAnswer, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let server = self.get_server()?; + server + .chat_service() + .stream_answer(&workspace_id, &chat_id, message_id) + .await + } + + fn get_chat_messages( + &self, + workspace_id: &str, + chat_id: &str, + offset: MessageCursor, + limit: u64, + ) -> FutureResult<RepeatedChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .chat_service() + .get_chat_messages(&workspace_id, &chat_id, offset, limit) + .await + }) + } + + fn get_related_message( + &self, + workspace_id: &str, + chat_id: &str, + message_id: i64, + ) -> FutureResult<RepeatedRelatedQuestion, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .chat_service() + .get_related_message(&workspace_id, &chat_id, message_id) + .await + }) + } + + fn generate_answer( + &self, + workspace_id: &str, + chat_id: &str, + question_message_id: i64, + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let server = self.get_server(); + FutureResult::new(async move { + server? + .chat_service() + .generate_answer(&workspace_id, &chat_id, question_message_id) + .await + }) + } +} + +#[async_trait] +impl SearchCloudService for ServerProvider { + async fn document_search( + &self, + workspace_id: &str, + query: String, + ) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> { + let server = self.get_server()?; + match server.search_service() { + Some(search_service) => search_service.document_search(workspace_id, query).await, + None => Err(FlowyError::internal().with_context("SearchCloudService not found")), + } + } +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs new file mode 100644 index 0000000000..f8219cdaff --- /dev/null +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -0,0 +1,218 @@ +use std::sync::Arc; + +use anyhow::Context; +use tracing::event; + +use collab_entity::CollabType; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_database2::DatabaseManager; +use flowy_document::manager::DocumentManager; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_folder::manager::{FolderInitDataSource, FolderManager}; +use flowy_user::event_map::UserStatusCallback; +use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; +use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; +use lib_infra::future::{to_fut, Fut}; + +use crate::integrate::server::{Server, ServerProvider}; +use crate::AppFlowyCoreConfig; + +pub(crate) struct UserStatusCallbackImpl { + pub(crate) collab_builder: Arc<AppFlowyCollabBuilder>, + pub(crate) folder_manager: Arc<FolderManager>, + pub(crate) database_manager: Arc<DatabaseManager>, + pub(crate) document_manager: Arc<DocumentManager>, + pub(crate) server_provider: Arc<ServerProvider>, + #[allow(dead_code)] + pub(crate) config: AppFlowyCoreConfig, +} + +impl UserStatusCallback for UserStatusCallbackImpl { + fn did_init( + &self, + user_id: i64, + user_authenticator: &Authenticator, + cloud_config: &Option<UserCloudConfig>, + user_workspace: &UserWorkspace, + _device_id: &str, + ) -> Fut<FlowyResult<()>> { + let user_id = user_id.to_owned(); + let user_workspace = user_workspace.clone(); + let folder_manager = self.folder_manager.clone(); + let database_manager = self.database_manager.clone(); + let document_manager = self.document_manager.clone(); + + self + .server_provider + .set_user_authenticator(user_authenticator); + + if let Some(cloud_config) = cloud_config { + self + .server_provider + .set_enable_sync(user_id, cloud_config.enable_sync); + if cloud_config.enable_encrypt { + self + .server_provider + .set_encrypt_secret(cloud_config.encrypt_secret.clone()); + } + } + + to_fut(async move { + folder_manager + .initialize( + user_id, + &user_workspace.id, + FolderInitDataSource::LocalDisk { + create_if_not_exist: false, + }, + ) + .await?; + database_manager.initialize(user_id).await?; + document_manager.initialize(user_id).await?; + Ok(()) + }) + } + + fn did_sign_in( + &self, + user_id: i64, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut<FlowyResult<()>> { + let device_id = device_id.to_owned(); + let user_id = user_id.to_owned(); + let user_workspace = user_workspace.clone(); + let folder_manager = self.folder_manager.clone(); + let database_manager = self.database_manager.clone(); + let document_manager = self.document_manager.clone(); + + to_fut(async move { + event!( + tracing::Level::TRACE, + "Notify did sign in: latest_workspace: {:?}, device_id: {}", + user_workspace, + device_id + ); + + folder_manager.initialize_with_workspace_id(user_id).await?; + database_manager.initialize(user_id).await?; + document_manager.initialize(user_id).await?; + Ok(()) + }) + } + + fn did_sign_up( + &self, + is_new_user: bool, + user_profile: &UserProfile, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut<FlowyResult<()>> { + let device_id = device_id.to_owned(); + let user_profile = user_profile.clone(); + let folder_manager = self.folder_manager.clone(); + let database_manager = self.database_manager.clone(); + let user_workspace = user_workspace.clone(); + let document_manager = self.document_manager.clone(); + self + .server_provider + .set_user_authenticator(&user_profile.authenticator); + let server_type = self.server_provider.get_server_type(); + + to_fut(async move { + event!( + tracing::Level::TRACE, + "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", + is_new_user, + user_workspace, + device_id + ); + + // In the current implementation, when a user signs up for AppFlowy Cloud, a default workspace + // is automatically created for them. However, for users who sign up through Supabase, the creation + // of the default workspace relies on the client-side operation. This means that the process + // for initializing a default workspace differs depending on the sign-up method used. + let data_source = match folder_manager + .cloud_service + .get_folder_doc_state( + &user_workspace.id, + user_profile.uid, + CollabType::Folder, + &user_workspace.id, + ) + .await + { + Ok(doc_state) => match server_type { + Server::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + Server::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), + Server::Supabase => { + if is_new_user { + FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + } + } else { + FolderInitDataSource::Cloud(doc_state) + } + }, + }, + Err(err) => match server_type { + Server::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + Server::AppFlowyCloud | Server::Supabase => { + return Err(FlowyError::from(err)); + }, + }, + }; + + folder_manager + .initialize_with_new_user( + user_profile.uid, + &user_profile.token, + is_new_user, + data_source, + &user_workspace.id, + ) + .await + .context("FolderManager error")?; + + database_manager + .initialize_with_new_user(user_profile.uid) + .await + .context("DatabaseManager error")?; + + document_manager + .initialize_with_new_user(user_profile.uid) + .await + .context("DocumentManager error")?; + Ok(()) + }) + } + + fn did_expired(&self, _token: &str, user_id: i64) -> Fut<FlowyResult<()>> { + let folder_manager = self.folder_manager.clone(); + to_fut(async move { + folder_manager.clear(user_id).await; + Ok(()) + }) + } + + fn open_workspace(&self, user_id: i64, _user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>> { + let folder_manager = self.folder_manager.clone(); + let database_manager = self.database_manager.clone(); + let document_manager = self.document_manager.clone(); + + to_fut(async move { + folder_manager.initialize_with_workspace_id(user_id).await?; + database_manager.initialize(user_id).await?; + document_manager.initialize(user_id).await?; + Ok(()) + }) + } + + fn did_update_network(&self, reachable: bool) { + self.collab_builder.update_network(reachable); + } +} diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c2800bd73b..818fdfe356 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,25 +1,23 @@ #![allow(unused_doc_comments)] -use collab_integrate::collab_builder::AppFlowyCollabBuilder; -use collab_plugins::CollabKVDB; -use flowy_ai::ai_manager::AIManager; -use flowy_database2::DatabaseManager; -use flowy_document::manager::DocumentManager; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder::manager::FolderManager; use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; -use flowy_server::af_cloud::define::LoggedUser; -use std::path::PathBuf; +use flowy_storage::ObjectStorageService; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; use tokio::sync::RwLock; use tracing::{debug, error, event, info, instrument}; -use uuid::Uuid; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_storage::manager::StorageManager; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType}; +use flowy_chat::manager::ChatManager; +use flowy_database2::DatabaseManager; +use flowy_document::manager::DocumentManager; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_folder::manager::FolderManager; +use flowy_server::af_cloud::define::ServerUser; + +use flowy_sqlite::kv::StorePreferences; use flowy_user::services::authenticate_user::AuthenticateUser; use flowy_user::services::entities::UserConfig; use flowy_user::user_manager::UserManager; @@ -27,26 +25,21 @@ use flowy_user::user_manager::UserManager; use lib_dispatch::prelude::*; use lib_dispatch::runtime::AFPluginRuntime; use lib_infra::priority_task::{TaskDispatcher, TaskRunner}; -use lib_infra::util::OperatingSystem; +use lib_infra::util::Platform; use lib_log::stream_log::StreamLogSender; use module::make_plugins; use crate::config::AppFlowyCoreConfig; -use crate::deps_resolve::file_storage_deps::FileStorageResolver; use crate::deps_resolve::*; -use crate::log_filter::init_log; -use crate::server_layer::ServerProvider; -use deps_resolve::reminder_deps::CollabInteractImpl; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; -use user_state_callback::UserStatusCallbackImpl; +use crate::integrate::collab_interact::CollabInteractImpl; +use crate::integrate::log::init_log; +use crate::integrate::server::{current_server_type, Server, ServerProvider}; +use crate::integrate::user::UserStatusCallbackImpl; pub mod config; mod deps_resolve; -mod log_filter; +pub mod integrate; pub mod module; -pub(crate) mod server_layer; -pub(crate) mod user_state_callback; /// This name will be used as to identify the current [AppFlowyCore] instance. /// Don't change this. @@ -63,10 +56,9 @@ pub struct AppFlowyCore { pub event_dispatcher: Arc<AFPluginDispatcher>, pub server_provider: Arc<ServerProvider>, pub task_dispatcher: Arc<RwLock<TaskDispatcher>>, - pub store_preference: Arc<KVStorePreferences>, + pub store_preference: Arc<StorePreferences>, pub search_manager: Arc<SearchManager>, - pub ai_manager: Arc<AIManager>, - pub storage_manager: Arc<StorageManager>, + pub chat_manager: Arc<ChatManager>, } impl AppFlowyCore { @@ -75,7 +67,7 @@ impl AppFlowyCore { runtime: Arc<AFPluginRuntime>, stream_log_sender: Option<Arc<dyn StreamLogSender>>, ) -> Self { - let platform = OperatingSystem::from(&config.platform); + let platform = Platform::from(&config.platform); #[allow(clippy::if_same_then_else)] if cfg!(debug_assertions) { @@ -92,13 +84,11 @@ impl AppFlowyCore { init_log(&config, &platform, stream_log_sender); } - if sysinfo::IS_SUPPORTED_SYSTEM { - info!( - "💡{:?}, platform: {:?}", - System::long_os_version(), - platform - ); - } + info!( + "💡{:?}, platform: {:?}", + System::long_os_version(), + platform + ); Self::init(config, runtime).await } @@ -109,13 +99,11 @@ impl AppFlowyCore { #[instrument(skip(config, runtime))] async fn init(config: AppFlowyCoreConfig, runtime: Arc<AFPluginRuntime>) -> Self { - config.ensure_path(); - // Init the key value database - let store_preference = Arc::new(KVStorePreferences::new(&config.storage_path).unwrap()); + let store_preference = Arc::new(StorePreferences::new(&config.storage_path).unwrap()); info!("🔥{:?}", &config); - let task_scheduler = TaskDispatcher::new(Duration::from_secs(10)); + let task_scheduler = TaskDispatcher::new(Duration::from_secs(2)); let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); @@ -132,10 +120,12 @@ impl AppFlowyCore { store_preference.clone(), )); - debug!("🔥runtime:{}", runtime); + let server_type = current_server_type(); + debug!("🔥runtime:{}, server:{}", runtime, server_type); let server_provider = Arc::new(ServerProvider::new( config.clone(), + server_type, Arc::downgrade(&store_preference), ServerUserImpl(Arc::downgrade(&authenticate_user)), )); @@ -149,14 +139,8 @@ impl AppFlowyCore { document_manager, collab_builder, search_manager, - ai_manager, - storage_manager, + chat_manager, ) = async { - let storage_manager = FileStorageResolver::resolve( - Arc::downgrade(&authenticate_user), - server_provider.clone(), - &user_config.storage_path, - ); /// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded /// on demand based on the [CollabPluginConfig]. let collab_builder = Arc::new(AppFlowyCollabBuilder::new( @@ -167,41 +151,11 @@ impl AppFlowyCore { collab_builder .set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&authenticate_user)))); - let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Arc::downgrade( - &authenticate_user, - ))); - - let folder_manager = FolderDepsResolver::resolve( - Arc::downgrade(&authenticate_user), - collab_builder.clone(), - server_provider.clone(), - folder_indexer.clone(), - store_preference.clone(), - ) - .await; - - let folder_query_service = FolderServiceImpl::new( - Arc::downgrade(&folder_manager), - Arc::downgrade(&authenticate_user), - ); - - let ai_manager = ChatDepsResolver::resolve( - Arc::downgrade(&authenticate_user), - server_provider.clone(), - store_preference.clone(), - Arc::downgrade(&storage_manager.storage_service), - server_provider.clone(), - folder_query_service.clone(), - server_provider.local_ai.clone(), - ); - let database_manager = DatabaseDepsResolver::resolve( Arc::downgrade(&authenticate_user), task_dispatcher.clone(), collab_builder.clone(), server_provider.clone(), - server_provider.clone(), - ai_manager.clone(), ) .await; @@ -210,9 +164,32 @@ impl AppFlowyCore { &database_manager, collab_builder.clone(), server_provider.clone(), - Arc::downgrade(&storage_manager.storage_service), + Arc::downgrade(&(server_provider.clone() as Arc<dyn ObjectStorageService>)), ); + let chat_manager = + ChatDepsResolver::resolve(Arc::downgrade(&authenticate_user), server_provider.clone()); + + let folder_indexer = Arc::new(FolderIndexManagerImpl::new(Some(Arc::downgrade( + &authenticate_user, + )))); + + let folder_operation_handlers = folder_operation_handlers( + document_manager.clone(), + database_manager.clone(), + chat_manager.clone(), + ); + + let folder_manager = FolderDepsResolver::resolve( + Arc::downgrade(&authenticate_user), + collab_builder.clone(), + server_provider.clone(), + folder_indexer.clone(), + store_preference.clone(), + folder_operation_handlers, + ) + .await; + let user_manager = UserDepsResolver::resolve( authenticate_user.clone(), collab_builder.clone(), @@ -230,14 +207,6 @@ impl AppFlowyCore { ) .await; - // Register the folder operation handlers - register_handlers( - &folder_manager, - document_manager.clone(), - database_manager.clone(), - ai_manager.clone(), - ); - ( user_manager, folder_manager, @@ -246,22 +215,18 @@ impl AppFlowyCore { document_manager, collab_builder, search_manager, - ai_manager, - storage_manager, + chat_manager, ) } .await; let user_status_callback = UserStatusCallbackImpl { - user_manager: user_manager.clone(), collab_builder, folder_manager: folder_manager.clone(), database_manager: database_manager.clone(), document_manager: document_manager.clone(), server_provider: server_provider.clone(), - storage_manager: storage_manager.clone(), - ai_manager: ai_manager.clone(), - runtime: runtime.clone(), + config: config.clone(), }; let collab_interact_impl = CollabInteractImpl { @@ -278,7 +243,6 @@ impl AppFlowyCore { error!("Init user failed: {}", err) } } - #[allow(clippy::arc_with_non_send_sync)] let event_dispatcher = Arc::new(AFPluginDispatcher::new( runtime, make_plugins( @@ -287,8 +251,7 @@ impl AppFlowyCore { Arc::downgrade(&user_manager), Arc::downgrade(&document_manager), Arc::downgrade(&search_manager), - Arc::downgrade(&ai_manager), - Arc::downgrade(&storage_manager), + Arc::downgrade(&chat_manager), ), )); @@ -303,8 +266,7 @@ impl AppFlowyCore { task_dispatcher, store_preference, search_manager, - ai_manager, - storage_manager, + chat_manager, } } @@ -314,6 +276,16 @@ impl AppFlowyCore { } } +impl From<Server> for CollabPluginProviderType { + fn from(server_type: Server) -> Self { + match server_type { + Server::Local => CollabPluginProviderType::Local, + Server::AppFlowyCloud => CollabPluginProviderType::AppFlowyCloud, + Server::Supabase => CollabPluginProviderType::Supabase, + } + } +} + struct ServerUserImpl(Weak<AuthenticateUser>); impl ServerUserImpl { @@ -325,32 +297,8 @@ impl ServerUserImpl { Ok(user) } } - -#[async_trait] -impl LoggedUser for ServerUserImpl { - fn workspace_id(&self) -> FlowyResult<Uuid> { +impl ServerUser for ServerUserImpl { + fn workspace_id(&self) -> FlowyResult<String> { self.upgrade_user()?.workspace_id() } - - fn user_id(&self) -> FlowyResult<i64> { - self.upgrade_user()?.user_id() - } - - async fn is_local_mode(&self) -> FlowyResult<bool> { - self.upgrade_user()?.is_local_mode().await - } - - fn get_sqlite_db(&self, uid: i64) -> Result<DBConnection, FlowyError> { - self.upgrade_user()?.get_sqlite_connection(uid) - } - - fn get_collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError> { - self.upgrade_user()?.get_collab_db(uid) - } - - fn application_root_dir(&self) -> Result<PathBuf, FlowyError> { - Ok(PathBuf::from( - self.upgrade_user()?.get_application_root_dir(), - )) - } } diff --git a/frontend/rust-lib/flowy-core/src/log_filter.rs b/frontend/rust-lib/flowy-core/src/log_filter.rs deleted file mode 100644 index 6704ad0507..0000000000 --- a/frontend/rust-lib/flowy-core/src/log_filter.rs +++ /dev/null @@ -1,92 +0,0 @@ -use lib_infra::util::OperatingSystem; -use lib_log::stream_log::StreamLogSender; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use crate::AppFlowyCoreConfig; - -static INIT_LOG: AtomicBool = AtomicBool::new(false); -pub(crate) fn init_log( - config: &AppFlowyCoreConfig, - platform: &OperatingSystem, - stream_log_sender: Option<Arc<dyn StreamLogSender>>, -) { - #[cfg(debug_assertions)] - if get_bool_from_env_var("DISABLE_CI_TEST_LOG") { - return; - } - - if !INIT_LOG.load(Ordering::SeqCst) { - INIT_LOG.store(true, Ordering::SeqCst); - - let _ = lib_log::Builder::new("log", &config.storage_path, platform, stream_log_sender) - .env_filter(&config.log_filter) - .build(); - } -} - -pub fn create_log_filter( - level: String, - with_crates: Vec<String>, - platform: OperatingSystem, -) -> String { - let mut level = std::env::var("RUST_LOG").unwrap_or(level); - - #[cfg(debug_assertions)] - if matches!(platform, OperatingSystem::IOS) { - level = "trace".to_string(); - } - - let mut filters = with_crates - .into_iter() - .map(|crate_name| format!("{}={}", crate_name, level)) - .collect::<Vec<String>>(); - filters.push(format!("flowy_core={}", level)); - filters.push(format!("flowy_folder={}", level)); - filters.push(format!("collab_sync={}", level)); - filters.push(format!("collab_folder={}", level)); - filters.push(format!("collab_database={}", level)); - filters.push(format!("collab_plugins={}", level)); - filters.push(format!("collab_integrate={}", level)); - filters.push(format!("collab={}", level)); - filters.push(format!("flowy_user={}", level)); - filters.push(format!("flowy_document={}", level)); - filters.push(format!("flowy_database2={}", level)); - filters.push(format!("flowy_server={}", level)); - filters.push(format!("flowy_notification={}", "info")); - filters.push(format!("lib_infra={}", level)); - filters.push(format!("flowy_search={}", level)); - filters.push(format!("flowy_chat={}", level)); - filters.push(format!("af_local_ai={}", level)); - filters.push(format!("af_plugin={}", level)); - filters.push(format!("flowy_ai={}", level)); - filters.push(format!("flowy_storage={}", level)); - // Enable the frontend logs. DO NOT DISABLE. - // These logs are essential for debugging and verifying frontend behavior. - filters.push(format!("dart_ffi={}", level)); - - // Most of the time, we don't need to see the logs from the following crates - // filters.push(format!("flowy_sqlite={}", "info")); - // filters.push(format!("lib_dispatch={}", level)); - - filters.push(format!("client_api={}", level)); - filters.push(format!("infra={}", level)); - #[cfg(feature = "profiling")] - filters.push(format!("tokio={}", level)); - #[cfg(feature = "profiling")] - filters.push(format!("runtime={}", level)); - - filters.join(",") -} - -#[cfg(debug_assertions)] -fn get_bool_from_env_var(env_var_name: &str) -> bool { - match std::env::var(env_var_name) { - Ok(value) => match value.to_lowercase().as_str() { - "true" | "1" => true, - "false" | "0" => false, - _ => false, - }, - Err(_) => false, - } -} diff --git a/frontend/rust-lib/flowy-core/src/module.rs b/frontend/rust-lib/flowy-core/src/module.rs index 2e657bd9ca..7077007915 100644 --- a/frontend/rust-lib/flowy-core/src/module.rs +++ b/frontend/rust-lib/flowy-core/src/module.rs @@ -1,11 +1,10 @@ -use flowy_ai::ai_manager::AIManager; +use flowy_chat::manager::ChatManager; use std::sync::Weak; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager as DocumentManager2; use flowy_folder::manager::FolderManager; use flowy_search::services::manager::SearchManager; -use flowy_storage::manager::StorageManager; use flowy_user::user_manager::UserManager; use lib_dispatch::prelude::AFPlugin; @@ -15,25 +14,28 @@ pub fn make_plugins( user_session: Weak<UserManager>, document_manager2: Weak<DocumentManager2>, search_manager: Weak<SearchManager>, - ai_manager: Weak<AIManager>, - file_storage_manager: Weak<StorageManager>, + chat_manager: Weak<ChatManager>, ) -> Vec<AFPlugin> { + let store_preferences = user_session + .upgrade() + .map(|session| session.get_store_preferences()) + .unwrap(); let user_plugin = flowy_user::event_map::init(user_session); let folder_plugin = flowy_folder::event_map::init(folder_manager); let database_plugin = flowy_database2::event_map::init(database_manager); let document_plugin2 = flowy_document::event_map::init(document_manager2); + let config_plugin = flowy_config::event_map::init(store_preferences); let date_plugin = flowy_date::event_map::init(); let search_plugin = flowy_search::event_map::init(search_manager); - let ai_plugin = flowy_ai::event_map::init(ai_manager); - let file_storage_plugin = flowy_storage::event_map::init(file_storage_manager); + let chat_plugin = flowy_chat::event_map::init(chat_manager); vec![ user_plugin, folder_plugin, database_plugin, document_plugin2, + config_plugin, date_plugin, search_plugin, - ai_plugin, - file_storage_plugin, + chat_plugin, ] } diff --git a/frontend/rust-lib/flowy-core/src/server_layer.rs b/frontend/rust-lib/flowy-core/src/server_layer.rs deleted file mode 100644 index 6e5d35d726..0000000000 --- a/frontend/rust-lib/flowy-core/src/server_layer.rs +++ /dev/null @@ -1,134 +0,0 @@ -use crate::AppFlowyCoreConfig; -use af_plugin::manager::PluginManager; -use arc_swap::{ArcSwap, ArcSwapOption}; -use dashmap::mapref::one::Ref; -use dashmap::DashMap; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_server::af_cloud::define::AIUserServiceImpl; -use flowy_server::af_cloud::{define::LoggedUser, AppFlowyCloudServer}; -use flowy_server::local_server::LocalServer; -use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl}; -use flowy_server_pub::AuthenticatorType; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::entities::*; -use std::ops::Deref; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; -use tracing::info; - -pub struct ServerProvider { - config: AppFlowyCoreConfig, - providers: DashMap<AuthType, Arc<dyn AppFlowyServer>>, - auth_type: ArcSwap<AuthType>, - logged_user: Arc<dyn LoggedUser>, - pub local_ai: Arc<LocalAIController>, - pub uid: Arc<ArcSwapOption<i64>>, - pub user_enable_sync: Arc<AtomicBool>, - pub encryption: Arc<dyn AppFlowyEncryption>, -} - -// Our little guard wrapper: -pub struct ServerHandle<'a>(Ref<'a, AuthType, Arc<dyn AppFlowyServer>>); - -impl<'a> Deref for ServerHandle<'a> { - type Target = dyn AppFlowyServer; - fn deref(&self) -> &Self::Target { - // `self.0.value()` is an `&Arc<dyn AppFlowyServer>` - // so `&**` gives us a `&dyn AppFlowyServer` - &**self.0.value() - } -} - -/// Determine current server type from ENV -pub fn current_server_type() -> AuthType { - match AuthenticatorType::from_env() { - AuthenticatorType::Local => AuthType::Local, - AuthenticatorType::AppFlowyCloud => AuthType::AppFlowyCloud, - } -} - -impl ServerProvider { - pub fn new( - config: AppFlowyCoreConfig, - store_preferences: Weak<KVStorePreferences>, - user_service: impl LoggedUser + 'static, - ) -> Self { - let initial_auth = current_server_type(); - let logged_user = Arc::new(user_service) as Arc<dyn LoggedUser>; - let auth_type = ArcSwap::from(Arc::new(initial_auth)); - let encryption = Arc::new(EncryptionImpl::new(None)) as Arc<dyn AppFlowyEncryption>; - let ai_user = Arc::new(AIUserServiceImpl(Arc::downgrade(&logged_user))); - let plugins = Arc::new(PluginManager::new()); - let local_ai = Arc::new(LocalAIController::new( - plugins, - store_preferences, - ai_user.clone(), - )); - - ServerProvider { - config, - providers: DashMap::new(), - encryption, - user_enable_sync: Arc::new(AtomicBool::new(true)), - auth_type, - logged_user, - uid: Default::default(), - local_ai, - } - } - - pub fn set_auth_type(&self, new_auth_type: AuthType) { - let old_type = self.get_auth_type(); - if old_type != new_auth_type { - info!( - "ServerProvider: auth type from {:?} to {:?}", - old_type, new_auth_type - ); - - self.auth_type.store(Arc::new(new_auth_type)); - if let Some((auth_type, _)) = self.providers.remove(&old_type) { - info!("ServerProvider: remove old auth type: {:?}", auth_type); - } - } - } - - pub fn get_auth_type(&self) -> AuthType { - *self.auth_type.load_full().as_ref() - } - - /// Lazily create or fetch an AppFlowyServer instance - pub fn get_server(&self) -> FlowyResult<ServerHandle> { - let auth_type = self.get_auth_type(); - if let Some(r) = self.providers.get(&auth_type) { - return Ok(ServerHandle(r)); - } - - let server: Arc<dyn AppFlowyServer> = match auth_type { - AuthType::Local => Arc::new(LocalServer::new( - self.logged_user.clone(), - self.local_ai.clone(), - )), - AuthType::AppFlowyCloud => { - let cfg = self - .config - .cloud_config - .clone() - .ok_or_else(|| FlowyError::internal().with_context("Missing cloud config"))?; - let ai_user_service = Arc::new(AIUserServiceImpl(Arc::downgrade(&self.logged_user))); - Arc::new(AppFlowyCloudServer::new( - cfg, - self.user_enable_sync.load(Ordering::Acquire), - self.config.device_id.clone(), - self.config.app_version.clone(), - Arc::downgrade(&self.logged_user), - ai_user_service, - )) - }, - }; - - self.providers.insert(auth_type, server); - let guard = self.providers.get(&auth_type).unwrap(); - Ok(ServerHandle(guard)) - } -} diff --git a/frontend/rust-lib/flowy-core/src/user_state_callback.rs b/frontend/rust-lib/flowy-core/src/user_state_callback.rs deleted file mode 100644 index f191d3c1ad..0000000000 --- a/frontend/rust-lib/flowy-core/src/user_state_callback.rs +++ /dev/null @@ -1,303 +0,0 @@ -use std::sync::Arc; - -use anyhow::Context; -use client_api::entity::billing_dto::SubscriptionPlan; -use tracing::{error, event, info}; - -use crate::server_layer::ServerProvider; -use collab_entity::CollabType; -use collab_integrate::collab_builder::AppFlowyCollabBuilder; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_ai::ai_manager::AIManager; -use flowy_database2::DatabaseManager; -use flowy_document::manager::DocumentManager; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder::manager::{FolderInitDataSource, FolderManager}; -use flowy_storage::manager::StorageManager; -use flowy_user::event_map::UserStatusCallback; -use flowy_user::user_manager::UserManager; -use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; -use flowy_user_pub::entities::{AuthType, UserProfile, UserWorkspace}; -use lib_dispatch::runtime::AFPluginRuntime; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; - -pub(crate) struct UserStatusCallbackImpl { - pub(crate) user_manager: Arc<UserManager>, - pub(crate) collab_builder: Arc<AppFlowyCollabBuilder>, - pub(crate) folder_manager: Arc<FolderManager>, - pub(crate) database_manager: Arc<DatabaseManager>, - pub(crate) document_manager: Arc<DocumentManager>, - pub(crate) server_provider: Arc<ServerProvider>, - pub(crate) storage_manager: Arc<StorageManager>, - pub(crate) ai_manager: Arc<AIManager>, - // By default, all callback will run on the caller thread. If you don't want to block the caller - // thread, you can use runtime to spawn a new task. - pub(crate) runtime: Arc<AFPluginRuntime>, -} - -impl UserStatusCallbackImpl { - async fn folder_init_data_source( - &self, - user_id: i64, - workspace_id: &Uuid, - auth_type: &AuthType, - ) -> FlowyResult<FolderInitDataSource> { - if self.is_object_exist_on_disk(user_id, workspace_id, workspace_id)? { - return Ok(FolderInitDataSource::LocalDisk { - create_if_not_exist: false, - }); - } - let doc_state_result = self - .folder_manager - .cloud_service - .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) - .await; - resolve_data_source(auth_type, doc_state_result) - } - - fn is_object_exist_on_disk( - &self, - user_id: i64, - workspace_id: &Uuid, - object_id: &Uuid, - ) -> FlowyResult<bool> { - let db = self - .user_manager - .get_collab_db(user_id)? - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Collab db is not initialized"))?; - let read = db.read_txn(); - let workspace_id = workspace_id.to_string(); - let object_id = object_id.to_string(); - Ok(read.is_exist(user_id, &workspace_id, &object_id)) - } -} - -#[async_trait] -impl UserStatusCallback for UserStatusCallbackImpl { - async fn on_launch_if_authenticated( - &self, - user_id: i64, - cloud_config: &Option<UserCloudConfig>, - user_workspace: &UserWorkspace, - _device_id: &str, - auth_type: &AuthType, - ) -> FlowyResult<()> { - let workspace_id = user_workspace.workspace_id()?; - if let Some(cloud_config) = cloud_config { - self - .server_provider - .set_enable_sync(user_id, cloud_config.enable_sync); - if cloud_config.enable_encrypt { - self - .server_provider - .set_encrypt_secret(cloud_config.encrypt_secret.clone()); - } - } - - self - .folder_manager - .initialize( - user_id, - &workspace_id, - FolderInitDataSource::LocalDisk { - create_if_not_exist: false, - }, - ) - .await?; - self - .database_manager - .initialize(user_id, auth_type == &AuthType::Local) - .await?; - self.document_manager.initialize(user_id).await?; - - let workspace_id = user_workspace.id.clone(); - let cloned_ai_manager = self.ai_manager.clone(); - self.runtime.spawn(async move { - if let Err(err) = cloned_ai_manager - .on_launch_if_authenticated(&workspace_id) - .await - { - error!("Failed to initialize AIManager: {:?}", err); - } - }); - Ok(()) - } - - async fn on_sign_in( - &self, - user_id: i64, - user_workspace: &UserWorkspace, - device_id: &str, - auth_type: &AuthType, - ) -> FlowyResult<()> { - event!( - tracing::Level::TRACE, - "Notify did sign in: latest_workspace: {:?}, device_id: {}", - user_workspace, - device_id - ); - let workspace_id = user_workspace.workspace_id()?; - let data_source = self - .folder_init_data_source(user_id, &workspace_id, auth_type) - .await?; - self - .folder_manager - .initialize_after_sign_in(user_id, data_source) - .await?; - self - .database_manager - .initialize_after_sign_in(user_id, auth_type.is_local()) - .await?; - self - .document_manager - .initialize_after_sign_in(user_id) - .await?; - - self - .ai_manager - .initialize_after_sign_in(&user_workspace.id) - .await?; - - Ok(()) - } - - async fn on_sign_up( - &self, - is_new_user: bool, - user_profile: &UserProfile, - user_workspace: &UserWorkspace, - device_id: &str, - auth_type: &AuthType, - ) -> FlowyResult<()> { - event!( - tracing::Level::TRACE, - "Notify did sign up: is new: {} user_workspace: {:?}, device_id: {}", - is_new_user, - user_workspace, - device_id - ); - let workspace_id = user_workspace.workspace_id()?; - let data_source = self - .folder_init_data_source(user_profile.uid, &workspace_id, auth_type) - .await?; - - self - .folder_manager - .initialize_after_sign_up( - user_profile.uid, - &user_profile.token, - is_new_user, - data_source, - &workspace_id, - ) - .await - .context("FolderManager error")?; - - self - .database_manager - .initialize_after_sign_up(user_profile.uid, auth_type.is_local()) - .await - .context("DatabaseManager error")?; - - self - .document_manager - .initialize_after_sign_up(user_profile.uid) - .await - .context("DocumentManager error")?; - - self - .ai_manager - .initialize_after_sign_up(&user_workspace.id) - .await?; - Ok(()) - } - - async fn on_token_expired(&self, _token: &str, user_id: i64) -> FlowyResult<()> { - self.folder_manager.clear(user_id).await; - Ok(()) - } - - async fn on_workspace_opened( - &self, - user_id: i64, - workspace_id: &Uuid, - _user_workspace: &UserWorkspace, - auth_type: &AuthType, - ) -> FlowyResult<()> { - let data_source = self - .folder_init_data_source(user_id, workspace_id, auth_type) - .await?; - - self - .folder_manager - .initialize_after_open_workspace(user_id, data_source) - .await?; - self - .database_manager - .initialize_after_open_workspace(user_id, auth_type.is_local()) - .await?; - self - .document_manager - .initialize_after_open_workspace(user_id) - .await?; - self - .ai_manager - .initialize_after_open_workspace(workspace_id) - .await?; - self - .storage_manager - .initialize_after_open_workspace(workspace_id) - .await; - Ok(()) - } - - fn on_network_status_changed(&self, reachable: bool) { - info!("Notify did update network: reachable: {}", reachable); - self.collab_builder.update_network(reachable); - self.storage_manager.update_network_reachable(reachable); - } - - fn on_subscription_plans_updated(&self, plans: Vec<SubscriptionPlan>) { - let mut storage_plan_changed = false; - for plan in &plans { - match plan { - SubscriptionPlan::Pro | SubscriptionPlan::Team => storage_plan_changed = true, - _ => {}, - } - } - if storage_plan_changed { - self.storage_manager.enable_storage_write_access(); - } - } - - fn on_storage_permission_updated(&self, can_write: bool) { - if can_write { - self.storage_manager.enable_storage_write_access(); - } else { - self.storage_manager.disable_storage_write_access(); - } - } -} - -fn resolve_data_source( - auth_type: &AuthType, - doc_state_result: Result<Vec<u8>, FlowyError>, -) -> FlowyResult<FolderInitDataSource> { - match doc_state_result { - Ok(doc_state) => Ok(match auth_type { - AuthType::Local => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }, - AuthType::AppFlowyCloud => FolderInitDataSource::Cloud(doc_state), - }), - Err(err) => match auth_type { - AuthType::Local => Ok(FolderInitDataSource::LocalDisk { - create_if_not_exist: true, - }), - AuthType::AppFlowyCloud => Err(err), - }, - } -} diff --git a/frontend/rust-lib/flowy-database-pub/Cargo.toml b/frontend/rust-lib/flowy-database-pub/Cargo.toml index 088c7b6465..fb258183a8 100644 --- a/frontend/rust-lib/flowy-database-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-database-pub/Cargo.toml @@ -9,6 +9,5 @@ edition = "2021" lib-infra = { workspace = true } collab-entity = { workspace = true } collab = { workspace = true } +anyhow.workspace = true client-api = { workspace = true } -flowy-error = { workspace = true } -uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 8666e6c764..3a43eb36da 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -1,72 +1,53 @@ +use anyhow::Error; pub use client_api::entity::ai_dto::{TranslateItem, TranslateRowResponse}; -use collab::entity::EncodedCollab; +use collab::core::collab::DataSource; use collab_entity::CollabType; -use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; use std::collections::HashMap; -use uuid::Uuid; -pub type EncodeCollabByOid = HashMap<Uuid, EncodedCollab>; +pub type CollabDocStateByOid = HashMap<String, DataSource>; pub type SummaryRowContent = HashMap<String, String>; pub type TranslateRowContent = Vec<TranslateItem>; - -#[async_trait] -pub trait DatabaseAIService: Send + Sync { - async fn summary_database_row( - &self, - _workspace_id: &Uuid, - _object_id: &Uuid, - _summary_row: SummaryRowContent, - ) -> Result<String, FlowyError> { - Ok("".to_string()) - } - - async fn translate_database_row( - &self, - _workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, - ) -> Result<TranslateRowResponse, FlowyError> { - Ok(TranslateRowResponse::default()) - } -} - /// A trait for database cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. /// /// returns the doc state of the object with the given object_id. /// None if the object is not found. -/// -#[async_trait] pub trait DatabaseCloudService: Send + Sync { - async fn get_database_encode_collab( + fn get_database_object_doc_state( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, - ) -> Result<Option<EncodedCollab>, FlowyError>; + workspace_id: &str, + ) -> FutureResult<Option<Vec<u8>>, Error>; - async fn create_database_encode_collab( + fn batch_get_database_object_doc_state( &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError>; - - async fn batch_get_database_encode_collab( - &self, - object_ids: Vec<Uuid>, + object_ids: Vec<String>, object_ty: CollabType, - workspace_id: &Uuid, - ) -> Result<EncodeCollabByOid, FlowyError>; + workspace_id: &str, + ) -> FutureResult<CollabDocStateByOid, Error>; - async fn get_database_collab_object_snapshots( + fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, + object_id: &str, limit: usize, - ) -> Result<Vec<DatabaseSnapshot>, FlowyError>; + ) -> FutureResult<Vec<DatabaseSnapshot>, Error>; + + fn summary_database_row( + &self, + workspace_id: &str, + object_id: &str, + summary_row: SummaryRowContent, + ) -> FutureResult<String, Error>; + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult<TranslateRowResponse, Error>; } pub struct DatabaseSnapshot { diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index ec0eb94210..8a5be01139 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -15,12 +15,12 @@ flowy-database-pub = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } +parking_lot.workspace = true protobuf.workspace = true -flowy-error = { path = "../flowy-error", features = [ - "impl_from_dispatch_error", - "impl_from_collab_database", +flowy-error = { workspace = true, features = [ + "impl_from_dispatch_error", + "impl_from_collab_database", ] } - lib-dispatch = { workspace = true } tokio = { workspace = true, features = ["sync"] } bytes.workspace = true @@ -28,7 +28,6 @@ tracing.workspace = true serde.workspace = true serde_json.workspace = true serde_repr.workspace = true -arc-swap.workspace = true lib-infra = { workspace = true } chrono = { workspace = true, default-features = false, features = ["clock"] } rust_decimal = "1.28.1" @@ -38,24 +37,20 @@ indexmap = { version = "2.1.0", features = ["serde"] } url = { version = "2" } fancy-regex = "0.11.0" futures.workspace = true -dashmap.workspace = true +dashmap = "5" anyhow.workspace = true async-stream = "0.3.4" rayon = "1.9.0" nanoid = "0.4.0" async-trait.workspace = true chrono-tz = "0.8.2" -csv = "1.3.0" +csv = "1.1.6" strum = "0.25" strum_macros = "0.25" validator = { workspace = true, features = ["derive"] } -tokio-util.workspace = true -moka = { version = "0.12.8", features = ["future"] } -uuid.workspace = true [dev-dependencies] event-integration-test = { path = "../event-integration-test", default-features = false } -flowy-database2 = { path = ".", features = ["verbose_log"] } [build-dependencies] flowy-codegen.workspace = true @@ -63,4 +58,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -verbose_log = ["collab-database/verbose_log"] \ No newline at end of file +ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs index e10aed7956..aeaaee42f3 100644 --- a/frontend/rust-lib/flowy-database2/build.rs +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -5,19 +5,19 @@ fn main() { flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - // #[cfg(feature = "ts")] - // { - // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - // flowy_codegen::protobuf_file::ts_gen( - // env!("CARGO_PKG_NAME"), - // env!("CARGO_PKG_NAME"), - // flowy_codegen::Project::Tauri, - // ); - // flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); - // flowy_codegen::protobuf_file::ts_gen( - // env!("CARGO_PKG_NAME"), - // env!("CARGO_PKG_NAME"), - // flowy_codegen::Project::TauriApp, - // ); - // } + #[cfg(feature = "ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs index c7fbb3368f..a760c3cee5 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs @@ -8,14 +8,14 @@ use validator::Validate; #[derive(Default, ProtoBuf, Validate)] pub struct UpdateCalculationChangesetPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub view_id: String, #[pb(index = 2, one_of)] pub calculation_id: Option<String>, #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub field_id: String, #[pb(index = 4)] @@ -25,15 +25,15 @@ pub struct UpdateCalculationChangesetPB { #[derive(Default, ProtoBuf, Validate)] pub struct RemoveCalculationChangesetPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub field_id: String, #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub calculation_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs index 36451e9032..8137b905d9 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs @@ -23,19 +23,6 @@ pub struct CalculationPB { pub value: String, } -impl std::convert::From<&CalculationPB> for Calculation { - fn from(calculation: &CalculationPB) -> Self { - let calculation_type = calculation.calculation_type.into(); - - Self { - id: calculation.id.clone(), - field_id: calculation.field_id.clone(), - calculation_type, - value: calculation.value.clone(), - } - } -} - impl std::convert::From<&Calculation> for CalculationPB { fn from(calculation: &Calculation) -> Self { let calculation_type = calculation.calculation_type.into(); diff --git a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs index 6ab424a230..e757ce4407 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/calendar_entities.rs @@ -108,8 +108,11 @@ pub struct CalendarEventPB { #[pb(index = 3)] pub title: String, - #[pb(index = 4, one_of)] - pub timestamp: Option<i64>, + #[pb(index = 4)] + pub timestamp: i64, + + #[pb(index = 5)] + pub is_scheduled: bool, } #[derive(Debug, Clone, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs index 9af549c698..94ecc9866b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/cell_entities.rs @@ -2,8 +2,6 @@ use collab_database::rows::RowId; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; -use lib_infra::validator_fn::required_not_empty_str; -use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::FieldType; @@ -42,18 +40,15 @@ impl TryInto<CreateSelectOptionParams> for CreateSelectOptionPayloadPB { } } -#[derive(Debug, Clone, Default, ProtoBuf, Validate)] +#[derive(Debug, Clone, Default, ProtoBuf)] pub struct CellIdPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] pub field_id: String, #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] pub row_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 2562bd84f7..688e878caa 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -26,6 +26,9 @@ pub struct DatabasePB { #[pb(index = 4)] pub layout_type: DatabaseLayoutPB, + + #[pb(index = 5)] + pub is_linked: bool, } #[derive(ProtoBuf, Default)] @@ -74,7 +77,7 @@ pub struct RepeatedDatabaseIdPB { #[derive(Clone, ProtoBuf, Default, Debug, Validate)] pub struct DatabaseViewIdPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub value: String, } @@ -152,7 +155,6 @@ impl TryInto<MoveRowParams> for MoveRowPayloadPB { }) } } - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct MoveGroupRowPayloadPB { #[pb(index = 1)] @@ -205,7 +207,7 @@ pub struct DatabaseMetaPB { pub database_id: String, #[pb(index = 2)] - pub view_id: String, + pub inline_view_id: String, } #[derive(Debug, Default, ProtoBuf)] @@ -321,31 +323,3 @@ pub struct DatabaseSnapshotPB { #[pb(index = 4)] pub data: Vec<u8>, } - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RemoveCoverPayloadPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub row_id: String, -} - -pub struct RemoveCoverParams { - pub view_id: String, - pub row_id: RowId, -} - -impl TryInto<RemoveCoverParams> for RemoveCoverPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result<RemoveCoverParams, Self::Error> { - let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)?; - let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - - Ok(RemoveCoverParams { - view_id: view_id.0, - row_id: RowId::from(row_id.0), - }) - } -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 8731b63b66..3263986db8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -10,7 +10,6 @@ use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use lib_infra::validator_fn::required_not_empty_str; use validator::Validate; use crate::entities::parser::NotEmptyStr; @@ -28,15 +27,12 @@ pub struct FieldPB { pub name: String, #[pb(index = 3)] - pub icon: String, - - #[pb(index = 4)] pub field_type: FieldType, - #[pb(index = 5)] + #[pb(index = 6)] pub is_primary: bool, - #[pb(index = 6)] + #[pb(index = 7)] pub type_option_data: Vec<u8>, } @@ -49,7 +45,6 @@ impl FieldPB { Self { id: field.id, name: field.name, - icon: field.icon, field_type, is_primary: field.is_primary, type_option_data: type_option_to_pb(type_option, &field_type).to_vec(), @@ -157,15 +152,12 @@ pub struct CreateFieldPayloadPB { #[pb(index = 3, one_of)] pub field_name: Option<String>, - #[pb(index = 4, one_of)] - pub field_icon: Option<String>, - /// If the type_option_data is not empty, it will be used to create the field. /// Otherwise, the default value will be used. - #[pb(index = 5, one_of)] + #[pb(index = 4, one_of)] pub type_option_data: Option<Vec<u8>>, - #[pb(index = 6)] + #[pb(index = 5)] pub field_position: OrderObjectPositionPB, } @@ -215,16 +207,12 @@ pub struct UpdateFieldTypePayloadPB { #[pb(index = 3)] pub field_type: FieldType, - - #[pb(index = 4, one_of)] - pub field_name: Option<String>, } pub struct EditFieldParams { pub view_id: String, pub field_id: String, pub field_type: FieldType, - pub field_name: Option<String>, } impl TryInto<EditFieldParams> for UpdateFieldTypePayloadPB { @@ -237,7 +225,6 @@ impl TryInto<EditFieldParams> for UpdateFieldTypePayloadPB { view_id: view_id.0, field_id: field_id.0, field_type: self.field_type, - field_name: self.field_name, }) } } @@ -377,29 +364,53 @@ impl TryInto<GetFieldParams> for GetFieldPayloadPB { /// Pass in None if you don't want to modify a property /// Pass in Some(Value) if you want to modify a property /// -#[derive(Debug, Clone, Default, ProtoBuf, Validate)] +#[derive(Debug, Clone, Default, ProtoBuf)] pub struct FieldChangesetPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] pub field_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] pub view_id: String, #[pb(index = 3, one_of)] pub name: Option<String>, #[pb(index = 4, one_of)] - pub icon: Option<String>, - - #[pb(index = 5, one_of)] pub desc: Option<String>, - #[pb(index = 6, one_of)] + #[pb(index = 5, one_of)] pub frozen: Option<bool>, } +impl TryInto<FieldChangesetParams> for FieldChangesetPB { + type Error = ErrorCode; + + fn try_into(self) -> Result<FieldChangesetParams, Self::Error> { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + + Ok(FieldChangesetParams { + field_id: field_id.0, + view_id: view_id.0, + name: self.name, + desc: self.desc, + frozen: self.frozen, + }) + } +} + +#[derive(Debug, Clone, Default)] +pub struct FieldChangesetParams { + pub field_id: String, + + pub view_id: String, + + pub name: Option<String>, + + pub desc: Option<String>, + + pub frozen: Option<bool>, +} /// Certain field types have user-defined options such as color, date format, number format, /// or a list of values for a multi-select list. These options are defined within a specialization /// of the FieldTypeOption class. @@ -440,7 +451,6 @@ pub enum FieldType { Summary = 11, Translate = 12, Time = 13, - Media = 14, } impl Display for FieldType { @@ -483,7 +493,6 @@ impl FieldType { FieldType::Summary => "Summarize", FieldType::Translate => "Translate", FieldType::Time => "Time", - FieldType::Media => "Media", }; s.to_string() } @@ -544,10 +553,6 @@ impl FieldType { matches!(self, FieldType::Time) } - pub fn is_media(&self) -> bool { - matches!(self, FieldType::Media) - } - pub fn can_be_group(&self) -> bool { self.is_select_option() || self.is_checkbox() || self.is_url() } @@ -606,11 +611,11 @@ impl TryInto<FieldIdParams> for DuplicateFieldPayloadPB { #[derive(Debug, Clone, Default, ProtoBuf, Validate)] pub struct ClearFieldPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub field_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs index 8171ed11a8..1d59b0b87e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_settings_entities.rs @@ -101,11 +101,11 @@ impl std::convert::From<Vec<FieldSettingsPB>> for RepeatedFieldSettingsPB { #[derive(Debug, Default, Clone, ProtoBuf, Validate)] pub struct FieldSettingsChangesetPB { - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] #[pb(index = 1)] pub view_id: String, - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] #[pb(index = 2)] pub field_id: String, diff --git a/frontend/rust-lib/flowy-database2/src/entities/file_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/file_entities.rs deleted file mode 100644 index 6c2b0bd5ec..0000000000 --- a/frontend/rust-lib/flowy-database2/src/entities/file_entities.rs +++ /dev/null @@ -1,53 +0,0 @@ -use collab_database::entity::FileUploadType; -use collab_database::fields::media_type_option::MediaUploadType; -use flowy_derive::ProtoBuf_Enum; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy, Serialize, Deserialize)] -#[repr(u8)] -pub enum FileUploadTypePB { - #[default] - LocalFile = 0, - NetworkFile = 1, - CloudFile = 2, -} - -impl From<FileUploadTypePB> for MediaUploadType { - fn from(file_upload_type: FileUploadTypePB) -> Self { - match file_upload_type { - FileUploadTypePB::LocalFile => MediaUploadType::Local, - FileUploadTypePB::NetworkFile => MediaUploadType::Network, - FileUploadTypePB::CloudFile => MediaUploadType::Cloud, - } - } -} - -impl From<MediaUploadType> for FileUploadTypePB { - fn from(file_upload_type: MediaUploadType) -> Self { - match file_upload_type { - MediaUploadType::Local => FileUploadTypePB::LocalFile, - MediaUploadType::Network => FileUploadTypePB::NetworkFile, - MediaUploadType::Cloud => FileUploadTypePB::CloudFile, - } - } -} - -impl From<FileUploadType> for FileUploadTypePB { - fn from(data: FileUploadType) -> Self { - match data { - FileUploadType::LocalFile => FileUploadTypePB::LocalFile, - FileUploadType::NetworkFile => FileUploadTypePB::NetworkFile, - FileUploadType::CloudFile => FileUploadTypePB::CloudFile, - } - } -} - -impl From<FileUploadTypePB> for FileUploadType { - fn from(data: FileUploadTypePB) -> Self { - match data { - FileUploadTypePB::LocalFile => FileUploadType::LocalFile, - FileUploadTypePB::NetworkFile => FileUploadType::NetworkFile, - FileUploadTypePB::CloudFile => FileUploadType::CloudFile, - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs index 4b4683eb35..01c3c9687c 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs @@ -29,8 +29,8 @@ pub struct DateFilterContent { pub timestamp: Option<i64>, } -impl DateFilterContent { - pub fn to_json_string(&self) -> String { +impl ToString for DateFilterContent { + fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } @@ -47,38 +47,14 @@ impl FromStr for DateFilterContent { #[repr(u8)] pub enum DateFilterConditionPB { #[default] - DateStartsOn = 0, - DateStartsBefore = 1, - DateStartsAfter = 2, - DateStartsOnOrBefore = 3, - DateStartsOnOrAfter = 4, - DateStartsBetween = 5, - DateStartIsEmpty = 6, - DateStartIsNotEmpty = 7, - DateEndsOn = 8, - DateEndsBefore = 9, - DateEndsAfter = 10, - DateEndsOnOrBefore = 11, - DateEndsOnOrAfter = 12, - DateEndsBetween = 13, - DateEndIsEmpty = 14, - DateEndIsNotEmpty = 15, -} - -impl DateFilterConditionPB { - pub fn is_filter_on_start_timestamp(&self) -> bool { - matches!( - self, - Self::DateStartsOn - | Self::DateStartsBefore - | Self::DateStartsAfter - | Self::DateStartsOnOrBefore - | Self::DateStartsOnOrAfter - | Self::DateStartsBetween - | Self::DateStartIsEmpty - | Self::DateStartIsNotEmpty, - ) - } + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, } impl std::convert::From<DateFilterConditionPB> for u32 { @@ -92,22 +68,13 @@ impl std::convert::TryFrom<u8> for DateFilterConditionPB { fn try_from(value: u8) -> Result<Self, Self::Error> { match value { - 0 => Ok(Self::DateStartsOn), - 1 => Ok(Self::DateStartsBefore), - 2 => Ok(Self::DateStartsAfter), - 3 => Ok(Self::DateStartsOnOrBefore), - 4 => Ok(Self::DateStartsOnOrAfter), - 5 => Ok(Self::DateStartsBetween), - 6 => Ok(Self::DateStartIsEmpty), - 7 => Ok(Self::DateStartIsNotEmpty), - 8 => Ok(Self::DateEndsOn), - 9 => Ok(Self::DateEndsBefore), - 10 => Ok(Self::DateEndsAfter), - 11 => Ok(Self::DateEndsOnOrBefore), - 12 => Ok(Self::DateEndsOnOrAfter), - 13 => Ok(Self::DateEndsBetween), - 14 => Ok(Self::DateEndIsEmpty), - 15 => Ok(Self::DateEndIsNotEmpty), + 0 => Ok(DateFilterConditionPB::DateIs), + 1 => Ok(DateFilterConditionPB::DateBefore), + 2 => Ok(DateFilterConditionPB::DateAfter), + 3 => Ok(DateFilterConditionPB::DateOnOrBefore), + 4 => Ok(DateFilterConditionPB::DateOnOrAfter), + 5 => Ok(DateFilterConditionPB::DateWithIn), + 6 => Ok(DateFilterConditionPB::DateIsEmpty), _ => Err(ErrorCode::InvalidParams), } } @@ -116,7 +83,7 @@ impl std::convert::TryFrom<u8> for DateFilterConditionPB { impl ParseFilterData for DateFilterPB { fn parse(condition: u8, content: String) -> Self { let condition = - DateFilterConditionPB::try_from(condition).unwrap_or(DateFilterConditionPB::DateStartsOn); + DateFilterConditionPB::try_from(condition).unwrap_or(DateFilterConditionPB::DateIs); let mut date_filter = Self { condition, ..Default::default() @@ -131,13 +98,3 @@ impl ParseFilterData for DateFilterPB { date_filter } } - -impl DateFilterPB { - pub fn remove_end_date_conditions(self) -> Self { - if self.condition.is_filter_on_start_timestamp() { - self - } else { - Self::default() - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/media_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/media_filter.rs deleted file mode 100644 index 9cc9adc481..0000000000 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/media_filter.rs +++ /dev/null @@ -1,49 +0,0 @@ -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::ErrorCode; - -use crate::services::filter::ParseFilterData; - -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct MediaFilterPB { - #[pb(index = 1)] - pub condition: MediaFilterConditionPB, - - #[pb(index = 2)] - pub content: String, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] -#[repr(u8)] -pub enum MediaFilterConditionPB { - #[default] - MediaIsEmpty = 0, - MediaIsNotEmpty = 1, -} - -impl std::convert::From<MediaFilterConditionPB> for u32 { - fn from(value: MediaFilterConditionPB) -> Self { - value as u32 - } -} - -impl std::convert::TryFrom<u8> for MediaFilterConditionPB { - type Error = ErrorCode; - - fn try_from(value: u8) -> Result<Self, Self::Error> { - match value { - 0 => Ok(MediaFilterConditionPB::MediaIsEmpty), - 1 => Ok(MediaFilterConditionPB::MediaIsNotEmpty), - _ => Err(ErrorCode::InvalidParams), - } - } -} - -impl ParseFilterData for MediaFilterPB { - fn parse(condition: u8, content: String) -> Self { - Self { - condition: MediaFilterConditionPB::try_from(condition) - .unwrap_or(MediaFilterConditionPB::MediaIsEmpty), - content, - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs index 0adb930020..a6a990a458 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs @@ -2,7 +2,6 @@ mod checkbox_filter; mod checklist_filter; mod date_filter; mod filter_changeset; -mod media_filter; mod number_filter; mod relation_filter; mod select_option_filter; @@ -14,7 +13,6 @@ pub use checkbox_filter::*; pub use checklist_filter::*; pub use date_filter::*; pub use filter_changeset::*; -pub use media_filter::*; pub use number_filter::*; pub use relation_filter::*; pub use select_option_filter::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs index 7c5d3d358d..1a186eb038 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs @@ -16,7 +16,7 @@ impl ParseFilterData for RelationFilterPB { } impl PreFillCellsWithFilter for RelationFilterPB { - fn get_compliant_cell(&self, _field: &Field) -> Option<Cell> { - None + fn get_compliant_cell(&self, _field: &Field) -> (Option<Cell>, bool) { + (None, false) } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs index 37fd35aaa9..1643116ccb 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs @@ -1,9 +1,9 @@ -use collab_database::fields::select_type_option::SelectOptionIds; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::ErrorCode; use std::str::FromStr; -use crate::services::filter::ParseFilterData; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; + +use crate::services::{field::SelectOptionIds, filter::ParseFilterData}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct SelectOptionFilterPB { @@ -58,28 +58,3 @@ impl ParseFilterData for SelectOptionFilterPB { } } } - -impl SelectOptionFilterPB { - pub fn to_single_select_filter(mut self) -> Self { - match self.condition { - SelectOptionFilterConditionPB::OptionContains - | SelectOptionFilterConditionPB::OptionDoesNotContain => { - self.condition = SelectOptionFilterConditionPB::OptionIs; - }, - _ => {}, - } - - self - } - - pub fn to_multi_select_filter(mut self) -> Self { - match self.condition { - SelectOptionFilterConditionPB::OptionIs | SelectOptionFilterConditionPB::OptionIsNot => { - self.condition = SelectOptionFilterConditionPB::OptionContains; - }, - _ => {}, - } - - self - } -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index 64331c9542..af1288506e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -14,8 +14,6 @@ use crate::entities::{ }; use crate::services::filter::{Filter, FilterChangeset, FilterInner}; -use super::MediaFilterPB; - #[derive(Debug, Default, Clone, ProtoBuf_Enum, Eq, PartialEq, Copy)] #[repr(u8)] pub enum FilterType { @@ -119,10 +117,6 @@ impl From<&Filter> for FilterPB { .cloned::<TextFilterPB>() .unwrap() .try_into(), - FieldType::Media => condition_and_content - .cloned::<MediaFilterPB>() - .unwrap() - .try_into(), }; Self { @@ -176,9 +170,6 @@ impl TryFrom<FilterDataPB> for FilterInner { FieldType::Translate => { BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) }, - FieldType::Media => { - BoxAny::new(MediaFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) - }, }; Ok(Self::Data { @@ -213,7 +204,7 @@ impl From<Vec<FilterPB>> for RepeatedFilterPB { pub struct InsertFilterPB { /// If None, the filter will be the root of a new filter tree #[pb(index = 1, one_of)] - #[validate(custom(function = "crate::entities::utils::validate_filter_id"))] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub parent_filter_id: Option<String>, #[pb(index = 2)] @@ -223,7 +214,7 @@ pub struct InsertFilterPB { #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateFilterTypePB { #[pb(index = 1)] - #[validate(custom(function = "crate::entities::utils::validate_filter_id"))] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub filter_id: String, #[pb(index = 2)] @@ -233,7 +224,7 @@ pub struct UpdateFilterTypePB { #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateFilterDataPB { #[pb(index = 1)] - #[validate(custom(function = "crate::entities::utils::validate_filter_id"))] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub filter_id: String, #[pb(index = 2)] @@ -243,8 +234,12 @@ pub struct UpdateFilterDataPB { #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct DeleteFilterPB { #[pb(index = 1)] - #[validate(custom(function = "crate::entities::utils::validate_filter_id"))] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub filter_id: String, + + #[pb(index = 2)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub field_id: String, } impl TryFrom<InsertFilterPB> for FilterChangeset { @@ -293,6 +288,7 @@ impl From<DeleteFilterPB> for FilterChangeset { fn from(value: DeleteFilterPB) -> Self { Self::Delete { filter_id: value.filter_id, + field_id: value.field_id, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index eef2b740c4..ba61bb53dc 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -2,7 +2,6 @@ use std::convert::TryInto; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; -use lib_infra::validator_fn::required_not_empty_str; use validator::Validate; use crate::entities::parser::NotEmptyStr; @@ -146,17 +145,21 @@ pub struct GroupByFieldParams { #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateGroupPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub group_id: String, - #[pb(index = 3, one_of)] - pub name: Option<String>, + #[pb(index = 3)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub field_id: String, #[pb(index = 4, one_of)] + pub name: Option<String>, + + #[pb(index = 5, one_of)] pub visible: Option<bool>, } @@ -170,10 +173,14 @@ impl TryInto<UpdateGroupParams> for UpdateGroupPB { let group_id = NotEmptyStr::parse(self.group_id) .map_err(|_| ErrorCode::GroupIdIsEmpty)? .0; + let field_id = NotEmptyStr::parse(self.field_id) + .map_err(|_| ErrorCode::FieldIdIsEmpty)? + .0; Ok(UpdateGroupParams { view_id, group_id, + field_id, name: self.name, visible: self.visible, }) @@ -183,6 +190,7 @@ impl TryInto<UpdateGroupParams> for UpdateGroupPB { pub struct UpdateGroupParams { pub view_id: String, pub group_id: String, + pub field_id: String, pub name: Option<String>, pub visible: Option<bool>, } @@ -191,6 +199,7 @@ impl From<UpdateGroupParams> for GroupChangeset { fn from(params: UpdateGroupParams) -> Self { Self { group_id: params.group_id, + field_id: params.field_id, name: params.name, visible: params.visible, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index 6fc6965bbc..2d30eb15f0 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -18,7 +18,6 @@ macro_rules! impl_into_field_type { 11 => FieldType::Summary, 12 => FieldType::Translate, 13 => FieldType::Time, - 14 => FieldType::Media, _ => { tracing::error!("🔴Can't parse FieldType from value: {}", ty); FieldType::RichText diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs index 56a9769b7a..6bacbd0539 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/mod.rs @@ -5,7 +5,6 @@ mod cell_entities; mod database_entities; mod field_entities; mod field_settings_entities; -pub mod file_entities; pub mod filter_entities; mod group_entities; pub mod parser; @@ -27,7 +26,6 @@ pub use cell_entities::*; pub use database_entities::*; pub use field_entities::*; pub use field_settings_entities::*; -pub use file_entities::*; pub use filter_entities::*; pub use group_entities::*; pub use position_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 53030a142f..5c31d11b0d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -1,21 +1,18 @@ use std::collections::HashMap; -use collab_database::rows::{CoverType, Row, RowCover, RowDetail, RowId}; +use collab_database::rows::{Row, RowDetail, RowId}; use collab_database::views::RowOrder; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use lib_infra::validator_fn::required_not_empty_str; use serde::{Deserialize, Serialize}; -use serde_repr::{Deserialize_repr, Serialize_repr}; use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::position_entities::OrderObjectPositionPB; use crate::services::database::{InsertedRow, UpdatedRow}; -use super::FileUploadTypePB; - /// [RowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. #[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] pub struct RowPB { @@ -58,156 +55,42 @@ pub struct RowMetaPB { #[pb(index = 1)] pub id: String, - #[pb(index = 2, one_of)] - pub document_id: Option<String>, + #[pb(index = 2)] + pub document_id: String, #[pb(index = 3, one_of)] pub icon: Option<String>, #[pb(index = 4, one_of)] - pub is_document_empty: Option<bool>, + pub cover: Option<String>, - #[pb(index = 5, one_of)] - pub attachment_count: Option<i64>, - - #[pb(index = 6, one_of)] - pub cover: Option<RowCoverPB>, + #[pb(index = 5)] + pub is_document_empty: bool, } -#[derive(Debug, Default, Clone, ProtoBuf, Serialize, Deserialize)] -pub struct RowCoverPB { - #[pb(index = 1)] - pub data: String, - - #[pb(index = 2)] - pub upload_type: FileUploadTypePB, - - #[pb(index = 3)] - pub cover_type: CoverTypePB, -} - -impl From<RowCoverPB> for RowCover { - fn from(cover: RowCoverPB) -> Self { - Self { - data: cover.data, - upload_type: cover.upload_type.into(), - cover_type: cover.cover_type.into(), - } - } -} - -impl From<RowCover> for RowCoverPB { - fn from(cover: RowCover) -> Self { - Self { - data: cover.data, - upload_type: cover.upload_type.into(), - cover_type: cover.cover_type.into(), - } - } -} - -#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Serialize_repr, Deserialize_repr)] -#[repr(u8)] -pub enum CoverTypePB { - #[default] - ColorCover = 0, - FileCover = 1, - AssetCover = 2, - GradientCover = 3, -} - -impl From<CoverTypePB> for CoverType { - fn from(data: CoverTypePB) -> Self { - match data { - CoverTypePB::ColorCover => CoverType::ColorCover, - CoverTypePB::FileCover => CoverType::FileCover, - CoverTypePB::AssetCover => CoverType::AssetCover, - CoverTypePB::GradientCover => CoverType::GradientCover, - } - } -} - -impl From<CoverType> for CoverTypePB { - fn from(data: CoverType) -> Self { - match data { - CoverType::ColorCover => CoverTypePB::ColorCover, - CoverType::FileCover => CoverTypePB::FileCover, - CoverType::AssetCover => CoverTypePB::AssetCover, - CoverType::GradientCover => CoverTypePB::GradientCover, - } - } -} - -#[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedRowMetaPB { - #[pb(index = 1)] - pub items: Vec<RowMetaPB>, -} - -impl From<RowOrder> for RowMetaPB { - fn from(data: RowOrder) -> Self { - Self { - id: data.id.into_inner(), - document_id: None, - icon: None, - is_document_empty: None, - attachment_count: None, - cover: None, - } - } -} - -impl From<&Row> for RowMetaPB { - fn from(data: &Row) -> Self { - Self { - id: data.id.clone().into_inner(), - document_id: None, - icon: None, - cover: None, - is_document_empty: None, - attachment_count: None, - } - } -} - -impl From<Row> for RowMetaPB { - fn from(data: Row) -> Self { - Self { - id: data.id.into_inner(), - document_id: None, - icon: None, - is_document_empty: None, - attachment_count: None, - cover: None, - } - } -} - -impl From<RowDetail> for RowMetaPB { - fn from(row_detail: RowDetail) -> Self { - Self { - id: row_detail.row.id.to_string(), - document_id: Some(row_detail.document_id.clone()), - icon: row_detail.meta.icon_url.clone(), - is_document_empty: Some(row_detail.meta.is_document_empty), - attachment_count: Some(row_detail.meta.attachment_count), - cover: row_detail.meta.cover.map(|cover| cover.into()), - } - } -} - -impl From<&RowDetail> for RowMetaPB { +impl std::convert::From<&RowDetail> for RowMetaPB { fn from(row_detail: &RowDetail) -> Self { Self { id: row_detail.row.id.to_string(), - document_id: Some(row_detail.document_id.clone()), + document_id: row_detail.document_id.clone(), icon: row_detail.meta.icon_url.clone(), - is_document_empty: Some(row_detail.meta.is_document_empty), - attachment_count: Some(row_detail.meta.attachment_count), - cover: row_detail.meta.clone().cover.map(|cover| cover.into()), + cover: row_detail.meta.cover_url.clone(), + is_document_empty: row_detail.meta.is_document_empty, } } } +impl std::convert::From<RowDetail> for RowMetaPB { + fn from(row_detail: RowDetail) -> Self { + Self { + id: row_detail.row.id.to_string(), + document_id: row_detail.document_id, + icon: row_detail.meta.icon_url, + cover: row_detail.meta.cover_url, + is_document_empty: row_detail.meta.is_document_empty, + } + } +} +// #[derive(Debug, Default, Clone, ProtoBuf)] pub struct UpdateRowMetaChangesetPB { @@ -221,23 +104,19 @@ pub struct UpdateRowMetaChangesetPB { pub icon_url: Option<String>, #[pb(index = 4, one_of)] - pub cover: Option<RowCoverPB>, + pub cover_url: Option<String>, #[pb(index = 5, one_of)] pub is_document_empty: Option<bool>, - - #[pb(index = 6, one_of)] - pub attachment_count: Option<i64>, } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct UpdateRowMetaParams { pub id: String, pub view_id: String, pub icon_url: Option<String>, - pub cover: Option<RowCover>, + pub cover_url: Option<String>, pub is_document_empty: Option<bool>, - pub attachment_count: Option<i64>, } impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB { @@ -255,9 +134,8 @@ impl TryInto<UpdateRowMetaParams> for UpdateRowMetaChangesetPB { id: row_id, view_id, icon_url: self.icon_url, - cover: self.cover.map(|cover| cover.into()), + cover_url: self.cover_url, is_document_empty: self.is_document_empty, - attachment_count: self.attachment_count, }) } } @@ -335,6 +213,18 @@ pub struct OptionalRowPB { pub row: Option<RowPB>, } +#[derive(Debug, Default, ProtoBuf)] +pub struct RepeatedRowPB { + #[pb(index = 1)] + pub items: Vec<RowPB>, +} + +impl std::convert::From<Vec<RowPB>> for RepeatedRowPB { + fn from(items: Vec<RowPB>) -> Self { + Self { items } + } +} + #[derive(Debug, Clone, Default, ProtoBuf)] pub struct InsertedRowPB { #[pb(index = 1)] @@ -345,9 +235,6 @@ pub struct InsertedRowPB { #[pb(index = 3)] pub is_new: bool, - - #[pb(index = 4)] - pub is_hidden_in_view: bool, } impl InsertedRowPB { @@ -356,7 +243,6 @@ impl InsertedRowPB { row_meta, index: None, is_new: false, - is_hidden_in_view: false, } } @@ -372,7 +258,6 @@ impl std::convert::From<RowMetaPB> for InsertedRowPB { row_meta, index: None, is_new: false, - is_hidden_in_view: false, } } } @@ -383,7 +268,6 @@ impl From<InsertedRow> for InsertedRowPB { row_meta: data.row_detail.into(), index: data.index, is_new: data.is_new, - is_hidden_in_view: false, } } } @@ -414,7 +298,7 @@ impl From<UpdatedRow> for UpdatedRowPB { } #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct DatabaseViewRowIdPB { +pub struct RowIdPB { #[pb(index = 1)] pub view_id: String, @@ -431,7 +315,7 @@ pub struct RowIdParams { pub group_id: Option<String>, } -impl TryInto<RowIdParams> for DatabaseViewRowIdPB { +impl TryInto<RowIdParams> for RowIdPB { type Error = ErrorCode; fn try_into(self) -> Result<RowIdParams, Self::Error> { @@ -466,20 +350,25 @@ pub struct RepeatedRowIdPB { #[derive(ProtoBuf, Default, Validate)] pub struct CreateRowPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub view_id: String, #[pb(index = 2)] pub row_position: OrderObjectPositionPB, #[pb(index = 3, one_of)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub group_id: Option<String>, #[pb(index = 4)] pub data: HashMap<String, String>, } +pub struct CreateRowParams { + pub collab_params: collab_database::rows::CreateRowParams, + pub open_after_create: bool, +} + #[derive(Debug, Default, Clone, ProtoBuf)] pub struct SummaryRowPB { #[pb(index = 1)] @@ -495,14 +384,14 @@ pub struct SummaryRowPB { #[derive(Debug, Default, Clone, ProtoBuf, Validate)] pub struct TranslateRowPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub row_id: String, #[pb(index = 3)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub field_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs index 4b8f0eebe9..2cb2f3613d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -71,42 +71,42 @@ impl std::convert::From<DatabaseLayoutPB> for DatabaseLayout { #[derive(Default, Validate, ProtoBuf)] pub struct DatabaseSettingChangesetPB { #[pb(index = 1)] - #[validate(custom(function = "lib_infra::validator_fn::required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2, one_of)] pub layout_type: Option<DatabaseLayoutPB>, #[pb(index = 3, one_of)] - #[validate(nested)] + #[validate] pub insert_filter: Option<InsertFilterPB>, #[pb(index = 4, one_of)] - #[validate(nested)] + #[validate] pub update_filter_type: Option<UpdateFilterTypePB>, #[pb(index = 5, one_of)] - #[validate(nested)] + #[validate] pub update_filter_data: Option<UpdateFilterDataPB>, #[pb(index = 6, one_of)] - #[validate(nested)] + #[validate] pub delete_filter: Option<DeleteFilterPB>, #[pb(index = 7, one_of)] - #[validate(nested)] + #[validate] pub update_group: Option<UpdateGroupPB>, #[pb(index = 8, one_of)] - #[validate(nested)] + #[validate] pub update_sort: Option<UpdateSortPayloadPB>, #[pb(index = 9, one_of)] - #[validate(nested)] + #[validate] pub reorder_sort: Option<ReorderSortPayloadPB>, #[pb(index = 10, one_of)] - #[validate(nested)] + #[validate] pub delete_sort: Option<DeleteSortPayloadPB>, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs index 981140e041..b9fc85387f 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/share_entities.rs @@ -4,9 +4,6 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; pub enum DatabaseExportDataType { #[default] CSV = 0, - - // DatabaseData - RawDatabaseData = 1, } #[derive(Debug, ProtoBuf, Default, Clone)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs index 59dc683087..307b261d0f 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs @@ -1,5 +1,4 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use lib_infra::validator_fn::required_not_empty_str; use validator::Validate; use crate::services::sort::{Sort, SortCondition}; @@ -97,16 +96,16 @@ impl std::convert::From<SortConditionPB> for SortCondition { #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateSortPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub field_id: String, /// Create a new sort if the sort_id is None #[pb(index = 3, one_of)] - #[validate(custom(function = "super::utils::validate_sort_id"))] + #[validate(custom = "super::utils::validate_sort_id")] pub sort_id: Option<String>, #[pb(index = 4)] @@ -116,26 +115,26 @@ pub struct UpdateSortPayloadPB { #[derive(Debug, Default, Clone, Validate, ProtoBuf)] pub struct ReorderSortPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "super::utils::validate_sort_id"))] + #[validate(custom = "super::utils::validate_sort_id")] pub from_sort_id: String, #[pb(index = 3)] - #[validate(custom(function = "super::utils::validate_sort_id"))] + #[validate(custom = "super::utils::validate_sort_id")] pub to_sort_id: String, } #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct DeleteSortPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - #[validate(custom(function = "super::utils::validate_sort_id"))] + #[validate(custom = "super::utils::validate_sort_id")] pub sort_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs index 1b284a9927..d6d4f84a8a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checkbox_entities.rs @@ -1,6 +1,4 @@ -use crate::services::field::{CHECK, UNCHECK}; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::template::util::ToCellString; +use crate::services::field::CheckboxTypeOption; use flowy_derive::ProtoBuf; #[derive(Default, Debug, Clone, ProtoBuf)] @@ -15,16 +13,6 @@ impl CheckboxCellDataPB { } } -impl ToCellString for CheckboxCellDataPB { - fn to_cell_string(&self) -> String { - if self.is_checked { - CHECK.to_string() - } else { - UNCHECK.to_string() - } - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct CheckboxTypeOptionPB { /// unused @@ -40,6 +28,6 @@ impl From<CheckboxTypeOption> for CheckboxTypeOptionPB { impl From<CheckboxTypeOptionPB> for CheckboxTypeOption { fn from(_type_option: CheckboxTypeOptionPB) -> Self { - CheckboxTypeOption + Self() } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs index b62a0182df..ac27d959ab 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs @@ -1,7 +1,11 @@ -use flowy_derive::ProtoBuf; -use validator::Validate; +use collab_database::rows::RowId; -use crate::entities::{CellIdPB, SelectOptionPB}; +use flowy_derive::ProtoBuf; +use flowy_error::{ErrorCode, FlowyError}; + +use crate::entities::parser::NotEmptyStr; +use crate::entities::SelectOptionPB; +use crate::services::field::SelectOption; #[derive(Debug, Clone, Default, ProtoBuf)] pub struct ChecklistCellDataPB { @@ -15,33 +19,61 @@ pub struct ChecklistCellDataPB { pub percentage: f64, } -#[derive(Debug, Clone, Default, ProtoBuf, Validate)] +#[derive(Debug, Clone, Default, ProtoBuf)] pub struct ChecklistCellDataChangesetPB { #[pb(index = 1)] - #[validate(nested)] - pub cell_id: CellIdPB, + pub view_id: String, #[pb(index = 2)] - pub insert_task: Vec<ChecklistCellInsertPB>, + pub field_id: String, #[pb(index = 3)] - pub delete_tasks: Vec<String>, + pub row_id: String, #[pb(index = 4)] - pub update_tasks: Vec<SelectOptionPB>, + pub insert_options: Vec<String>, #[pb(index = 5)] - pub completed_tasks: Vec<String>, + pub selected_option_ids: Vec<String>, #[pb(index = 6)] - pub reorder: String, + pub delete_option_ids: Vec<String>, + + #[pb(index = 7)] + pub update_options: Vec<SelectOptionPB>, } -#[derive(Debug, Clone, Default, ProtoBuf, Validate)] -pub struct ChecklistCellInsertPB { - #[pb(index = 1)] - pub name: String, - - #[pb(index = 2, one_of)] - pub index: Option<i32>, +#[derive(Debug)] +pub struct ChecklistCellDataChangesetParams { + pub view_id: String, + pub field_id: String, + pub row_id: RowId, + pub insert_options: Vec<String>, + pub selected_option_ids: Vec<String>, + pub delete_option_ids: Vec<String>, + pub update_options: Vec<SelectOption>, +} + +impl TryInto<ChecklistCellDataChangesetParams> for ChecklistCellDataChangesetPB { + type Error = FlowyError; + + fn try_into(self) -> Result<ChecklistCellDataChangesetParams, Self::Error> { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; + + Ok(ChecklistCellDataChangesetParams { + view_id: view_id.0, + field_id: field_id.0, + row_id: RowId::from(row_id.0), + insert_options: self.insert_options, + selected_option_ids: self.selected_option_ids, + delete_option_ids: self.delete_option_ids, + update_options: self + .update_options + .into_iter() + .map(SelectOption::from) + .collect(), + }) + } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs index 4e9730f5ed..5fdb0195ef 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs @@ -1,42 +1,40 @@ #![allow(clippy::upper_case_acronyms)] -use collab_database::fields::date_type_option::{ - DateCellData, DateFormat, DateTypeOption, TimeFormat, -}; use strum_macros::EnumIter; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use crate::entities::CellIdPB; +use crate::services::field::{DateFormat, DateTypeOption, TimeFormat}; #[derive(Clone, Debug, Default, ProtoBuf)] pub struct DateCellDataPB { - #[pb(index = 1, one_of)] - pub timestamp: Option<i64>, + #[pb(index = 1)] + pub date: String, - #[pb(index = 2, one_of)] - pub end_timestamp: Option<i64>, + #[pb(index = 2)] + pub time: String, #[pb(index = 3)] - pub include_time: bool, + pub timestamp: i64, #[pb(index = 4)] - pub is_range: bool, + pub end_date: String, #[pb(index = 5)] - pub reminder_id: String, -} + pub end_time: String, -impl From<&DateCellDataPB> for DateCellData { - fn from(data: &DateCellDataPB) -> Self { - Self { - timestamp: data.timestamp, - end_timestamp: data.end_timestamp, - include_time: data.include_time, - is_range: data.is_range, - reminder_id: data.reminder_id.to_owned(), - } - } + #[pb(index = 6)] + pub end_timestamp: i64, + + #[pb(index = 7)] + pub include_time: bool, + + #[pb(index = 8)] + pub is_range: bool, + + #[pb(index = 9)] + pub reminder_id: String, } #[derive(Clone, Debug, Default, ProtoBuf)] @@ -45,21 +43,27 @@ pub struct DateCellChangesetPB { pub cell_id: CellIdPB, #[pb(index = 2, one_of)] - pub timestamp: Option<i64>, + pub date: Option<i64>, #[pb(index = 3, one_of)] - pub end_timestamp: Option<i64>, + pub time: Option<String>, #[pb(index = 4, one_of)] - pub include_time: Option<bool>, + pub end_date: Option<i64>, #[pb(index = 5, one_of)] - pub is_range: Option<bool>, + pub end_time: Option<String>, #[pb(index = 6, one_of)] - pub clear_flag: Option<bool>, + pub include_time: Option<bool>, #[pb(index = 7, one_of)] + pub is_range: Option<bool>, + + #[pb(index = 8, one_of)] + pub clear_flag: Option<bool>, + + #[pb(index = 9, one_of)] pub reminder_id: Option<String>, } @@ -104,7 +108,6 @@ pub enum DateFormatPB { #[default] Friendly = 3, DayMonthYear = 4, - FriendlyFull = 5, } impl From<DateFormatPB> for DateFormat { @@ -115,7 +118,6 @@ impl From<DateFormatPB> for DateFormat { DateFormatPB::ISO => DateFormat::ISO, DateFormatPB::Friendly => DateFormat::Friendly, DateFormatPB::DayMonthYear => DateFormat::DayMonthYear, - DateFormatPB::FriendlyFull => DateFormat::FriendlyFull, } } } @@ -128,7 +130,6 @@ impl From<DateFormat> for DateFormatPB { DateFormat::ISO => DateFormatPB::ISO, DateFormat::Friendly => DateFormatPB::Friendly, DateFormat::DayMonthYear => DateFormatPB::DayMonthYear, - DateFormat::FriendlyFull => DateFormatPB::FriendlyFull, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/media_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/media_entities.rs deleted file mode 100644 index e337dca57a..0000000000 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/media_entities.rs +++ /dev/null @@ -1,179 +0,0 @@ -use collab_database::fields::media_type_option::{ - MediaCellData, MediaFile, MediaFileType, MediaTypeOption, -}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; - -use crate::entities::{CellIdPB, FileUploadTypePB}; - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MediaCellDataPB { - #[pb(index = 1)] - pub files: Vec<MediaFilePB>, -} - -impl From<MediaCellData> for MediaCellDataPB { - fn from(data: MediaCellData) -> Self { - Self { - files: data.files.into_iter().map(Into::into).collect(), - } - } -} - -impl From<MediaCellDataPB> for MediaCellData { - fn from(data: MediaCellDataPB) -> Self { - Self { - files: data.files.into_iter().map(Into::into).collect(), - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MediaTypeOptionPB { - #[pb(index = 1)] - pub hide_file_names: bool, -} - -impl From<MediaTypeOption> for MediaTypeOptionPB { - fn from(value: MediaTypeOption) -> Self { - Self { - hide_file_names: value.hide_file_names, - } - } -} - -impl From<MediaTypeOptionPB> for MediaTypeOption { - fn from(value: MediaTypeOptionPB) -> Self { - Self { - hide_file_names: value.hide_file_names, - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MediaFilePB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub name: String, - - #[pb(index = 3)] - pub url: String, - - #[pb(index = 4)] - pub upload_type: FileUploadTypePB, - - #[pb(index = 5)] - pub file_type: MediaFileTypePB, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] -#[repr(u8)] -pub enum MediaFileTypePB { - #[default] - Other = 0, - // Eg. jpg, png, gif, etc. - Image = 1, - // Eg. https://appflowy.io - Link = 2, - // Eg. pdf, doc, etc. - Document = 3, - // Eg. zip, rar, etc. - Archive = 4, - // Eg. mp4, avi, etc. - Video = 5, - // Eg. mp3, wav, etc. - Audio = 6, - // Eg. txt, csv, etc. - Text = 7, -} - -impl From<MediaFileType> for MediaFileTypePB { - fn from(data: MediaFileType) -> Self { - match data { - MediaFileType::Other => MediaFileTypePB::Other, - MediaFileType::Image => MediaFileTypePB::Image, - MediaFileType::Link => MediaFileTypePB::Link, - MediaFileType::Document => MediaFileTypePB::Document, - MediaFileType::Archive => MediaFileTypePB::Archive, - MediaFileType::Video => MediaFileTypePB::Video, - MediaFileType::Audio => MediaFileTypePB::Audio, - MediaFileType::Text => MediaFileTypePB::Text, - } - } -} - -impl From<MediaFileTypePB> for MediaFileType { - fn from(data: MediaFileTypePB) -> Self { - match data { - MediaFileTypePB::Other => MediaFileType::Other, - MediaFileTypePB::Image => MediaFileType::Image, - MediaFileTypePB::Link => MediaFileType::Link, - MediaFileTypePB::Document => MediaFileType::Document, - MediaFileTypePB::Archive => MediaFileType::Archive, - MediaFileTypePB::Video => MediaFileType::Video, - MediaFileTypePB::Audio => MediaFileType::Audio, - MediaFileTypePB::Text => MediaFileType::Text, - } - } -} - -impl From<MediaFile> for MediaFilePB { - fn from(data: MediaFile) -> Self { - Self { - id: data.id, - name: data.name, - url: data.url, - upload_type: data.upload_type.into(), - file_type: data.file_type.into(), - } - } -} - -impl From<MediaFilePB> for MediaFile { - fn from(data: MediaFilePB) -> Self { - Self { - id: data.id, - name: data.name, - url: data.url, - upload_type: data.upload_type.into(), - file_type: data.file_type.into(), - } - } -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MediaCellChangesetPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub cell_id: CellIdPB, - - #[pb(index = 3)] - pub inserted_files: Vec<MediaFilePB>, - - #[pb(index = 4)] - pub removed_ids: Vec<String>, -} - -#[derive(Debug, Clone, Default)] -pub struct MediaCellChangeset { - pub inserted_files: Vec<MediaFile>, - pub removed_ids: Vec<String>, -} - -#[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RenameMediaChangesetPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub cell_id: CellIdPB, - - #[pb(index = 3)] - pub file_id: String, - - #[pb(index = 4)] - pub name: String, -} diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs index 633f000e53..f92072eabd 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs @@ -1,7 +1,6 @@ mod checkbox_entities; mod checklist_entities; mod date_entities; -mod media_entities; mod number_entities; mod relation_entities; mod select_option_entities; @@ -15,7 +14,6 @@ mod url_entities; pub use checkbox_entities::*; pub use checklist_entities::*; pub use date_entities::*; -pub use media_entities::*; pub use number_entities::*; pub use relation_entities::*; pub use select_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs index bb34178cd7..3b7a301851 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/number_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::number_type_option::{NumberFormat, NumberTypeOption}; +use crate::services::field::{NumberFormat, NumberTypeOption}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; // Number diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/relation_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/relation_entities.rs index 17d7434c08..919ca220ae 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/relation_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/relation_entities.rs @@ -1,8 +1,7 @@ -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::template::relation_parse::RelationCellData; use flowy_derive::ProtoBuf; use crate::entities::CellIdPB; +use crate::services::field::{RelationCellData, RelationTypeOption}; #[derive(Debug, Clone, Default, ProtoBuf)] pub struct RelationCellDataPB { diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option_entities.rs index 69b79305a0..c5e931b017 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/select_option_entities.rs @@ -1,8 +1,8 @@ use crate::entities::parser::NotEmptyStr; use crate::entities::{CellIdPB, CellIdParams}; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionColor, SelectTypeOption, SingleSelectTypeOption, +use crate::services::field::checklist_type_option::ChecklistTypeOption; +use crate::services::field::{ + MultiSelectTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -55,8 +55,9 @@ pub struct RepeatedSelectOptionPayload { pub items: Vec<SelectOptionPB>, } -#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone, Default)] +#[derive(ProtoBuf_Enum, PartialEq, Eq, Debug, Clone)] #[repr(u8)] +#[derive(Default)] pub enum SelectOptionColorPB { #[default] Purple = 0, @@ -212,8 +213,8 @@ pub struct SingleSelectTypeOptionPB { pub disable_color: bool, } -impl From<SelectTypeOption> for SingleSelectTypeOptionPB { - fn from(data: SelectTypeOption) -> Self { +impl From<SingleSelectTypeOption> for SingleSelectTypeOptionPB { + fn from(data: SingleSelectTypeOption) -> Self { Self { options: data .options @@ -227,14 +228,14 @@ impl From<SelectTypeOption> for SingleSelectTypeOptionPB { impl From<SingleSelectTypeOptionPB> for SingleSelectTypeOption { fn from(data: SingleSelectTypeOptionPB) -> Self { - SingleSelectTypeOption(SelectTypeOption { + Self { options: data .options .into_iter() .map(|option| option.into()) .collect(), disable_color: data.disable_color, - }) + } } } @@ -247,8 +248,8 @@ pub struct MultiSelectTypeOptionPB { pub disable_color: bool, } -impl From<SelectTypeOption> for MultiSelectTypeOptionPB { - fn from(data: SelectTypeOption) -> Self { +impl From<MultiSelectTypeOption> for MultiSelectTypeOptionPB { + fn from(data: MultiSelectTypeOption) -> Self { Self { options: data .options @@ -262,14 +263,14 @@ impl From<SelectTypeOption> for MultiSelectTypeOptionPB { impl From<MultiSelectTypeOptionPB> for MultiSelectTypeOption { fn from(data: MultiSelectTypeOptionPB) -> Self { - MultiSelectTypeOption(SelectTypeOption { + Self { options: data .options .into_iter() .map(|option| option.into()) .collect(), disable_color: data.disable_color, - }) + } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs index cb9cc8eca9..c8f4a9b5c4 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/summary_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::summary_type_option::SummarizationTypeOption; +use crate::services::field::summary_type_option::summary::SummarizationTypeOption; use flowy_derive::ProtoBuf; #[derive(Debug, Clone, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs index 5a61fe5410..cce32dc64a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/text_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::text_type_option::RichTextTypeOption; +use crate::services::field::RichTextTypeOption; use flowy_derive::ProtoBuf; #[derive(Debug, Clone, Default, ProtoBuf)] @@ -8,15 +8,13 @@ pub struct RichTextTypeOptionPB { } impl From<RichTextTypeOption> for RichTextTypeOptionPB { - fn from(_data: RichTextTypeOption) -> Self { - RichTextTypeOptionPB { - data: "".to_string(), - } + fn from(data: RichTextTypeOption) -> Self { + Self { data: data.inner } } } impl From<RichTextTypeOptionPB> for RichTextTypeOption { - fn from(_data: RichTextTypeOptionPB) -> Self { - RichTextTypeOption + fn from(data: RichTextTypeOptionPB) -> Self { + Self { inner: data.data } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs index c699b1ca67..fdb3bdb6fd 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::date_type_option::TimeTypeOption; +use crate::services::field::TimeTypeOption; use flowy_derive::ProtoBuf; #[derive(Clone, Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs index 1bf20be587..b4afcadaf4 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs @@ -1,7 +1,7 @@ -use collab_database::fields::timestamp_type_option::TimestampTypeOption; use flowy_derive::ProtoBuf; use crate::entities::{DateFormatPB, FieldType, TimeFormatPB}; +use crate::services::field::TimestampTypeOption; #[derive(Clone, Debug, Default, ProtoBuf)] pub struct TimestampCellDataPB { @@ -33,7 +33,7 @@ impl From<TimestampTypeOption> for TimestampTypeOptionPB { date_format: data.date_format.into(), time_format: data.time_format.into(), include_time: data.include_time, - field_type: data.field_type.into(), + field_type: data.field_type, } } } @@ -44,8 +44,7 @@ impl From<TimestampTypeOptionPB> for TimestampTypeOption { date_format: data.date_format.into(), time_format: data.time_format.into(), include_time: data.include_time, - field_type: data.field_type.into(), - timezone: None, + field_type: data.field_type, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs index 326abf2c2b..050d83c77d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/translate_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::translate_type_option::TranslateTypeOption; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; #[derive(Debug, Clone, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs index 727c4ce759..1a14299d01 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/url_entities.rs @@ -1,4 +1,4 @@ -use collab_database::fields::url_type_option::URLTypeOption; +use crate::services::field::URLTypeOption; use flowy_derive::ProtoBuf; #[derive(Clone, Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs index b04fcb3aff..9809516bed 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs @@ -1,5 +1,4 @@ use collab_database::rows::RowDetail; -use std::fmt::{Display, Formatter}; use flowy_derive::ProtoBuf; @@ -27,54 +26,6 @@ pub struct RowsChangePB { #[pb(index = 3)] pub updated_rows: Vec<UpdatedRowPB>, - - #[pb(index = 4)] - pub is_move_row: bool, -} - -impl RowsChangePB { - pub fn new() -> Self { - Default::default() - } -} -impl Display for RowsChangePB { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let mut parts = Vec::new(); - - // Conditionally add inserted rows if not empty - if !self.inserted_rows.is_empty() { - let inserted_rows = self - .inserted_rows - .iter() - .map(|row| format!("{}:{:?}", row.row_meta.id, row.index)) - .collect::<Vec<String>>() - .join(", "); - parts.push(format!("Inserted rows: {}", inserted_rows)); - } - - // Conditionally add deleted rows if not empty - if !self.deleted_rows.is_empty() { - let deleted_rows = self.deleted_rows.join(", "); - parts.push(format!("Deleted rows: {}", deleted_rows)); - } - - // Conditionally add updated rows if not empty - if !self.updated_rows.is_empty() { - let updated_rows = self - .updated_rows - .iter() - .map(|row| row.row_id.to_string()) - .collect::<Vec<String>>() - .join(", "); - parts.push(format!("Updated rows: {}", updated_rows)); - } - - // Always add is_move_row - parts.push(format!("is_move_row: {}", self.is_move_row)); - - // Join the parts together and write them to the formatter - f.write_str(&parts.join(", ")) - } } impl RowsChangePB { diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 63d6fdf2c3..f9c4651e25 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1,19 +1,18 @@ -use collab_database::fields::media_type_option::MediaCellData; -use collab_database::rows::{Cell, RowCover, RowId}; -use lib_infra::box_any::BoxAny; use std::sync::{Arc, Weak}; + +use collab_database::rows::RowId; +use lib_infra::box_any::BoxAny; use tokio::sync::oneshot; -use tracing::{info, instrument}; +use tracing::error; use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; use crate::manager::DatabaseManager; -use crate::services::field::checklist_filter::ChecklistCellChangeset; -use crate::services::field::date_filter::DateCellChangeset; use crate::services::field::{ - type_option_data_from_pb, RelationCellChangeset, SelectOptionCellChangeset, TypeOptionCellExt, + type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, RelationCellChangeset, + SelectOptionCellChangeset, }; use crate::services::group::GroupChangeset; use crate::services::share::csv::CSVFormat; @@ -34,43 +33,11 @@ pub(crate) async fn get_database_data_handler( ) -> DataResult<DatabasePB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_id = manager - .get_database_id_with_view_id(view_id.as_ref()) - .await?; - let database_editor = manager.get_or_init_database_editor(&database_id).await?; - let start = std::time::Instant::now(); - let data = database_editor - .open_database_view(view_id.as_ref(), None) - .await?; - info!( - "[Database]: {} layout: {:?}, rows: {}, fields: {}, cost time: {} milliseconds", - database_id, - data.layout_type, - data.rows.len(), - data.fields.len(), - start.elapsed().as_millis() - ); + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + let data = database_editor.get_database_data(view_id.as_ref()).await?; data_result_ok(data) } -#[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn get_all_rows_handler( - data: AFPluginData<DatabaseViewIdPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> DataResult<RepeatedRowMetaPB, FlowyError> { - let manager = upgrade_manager(manager)?; - let view_id: DatabaseViewIdPB = data.into_inner(); - let database_id = manager - .get_database_id_with_view_id(view_id.as_ref()) - .await?; - let database_editor = manager.get_or_init_database_editor(&database_id).await?; - let row_details = database_editor.get_all_rows(view_id.as_ref()).await?; - let rows = row_details - .into_iter() - .map(|detail| RowMetaPB::from(detail.as_ref())) - .collect::<Vec<RowMetaPB>>(); - data_result_ok(RepeatedRowMetaPB { items: rows }) -} #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn open_database_handler( data: AFPluginData<DatabaseViewIdPB>, @@ -105,9 +72,7 @@ pub(crate) async fn get_database_setting_handler( ) -> DataResult<DatabaseViewSettingPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; let data = database_editor .get_database_view_setting(view_id.as_ref()) .await?; @@ -121,9 +86,7 @@ pub(crate) async fn update_database_setting_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; if let Some(payload) = params.insert_filter { database_editor @@ -176,9 +139,7 @@ pub(crate) async fn get_all_filters_handler( ) -> DataResult<RepeatedFilterPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; let filters = database_editor.get_all_filters(view_id.as_ref()).await; data_result_ok(filters) } @@ -190,9 +151,7 @@ pub(crate) async fn get_all_sorts_handler( ) -> DataResult<RepeatedSortPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; let sorts = database_editor.get_all_sorts(view_id.as_ref()).await; data_result_ok(sorts) } @@ -204,9 +163,7 @@ pub(crate) async fn delete_all_sorts_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let view_id: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; database_editor.delete_all_sorts(view_id.as_ref()).await; Ok(()) } @@ -218,12 +175,9 @@ pub(crate) async fn get_fields_handler( ) -> DataResult<RepeatedFieldPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: GetFieldParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let fields = database_editor .get_fields(¶ms.view_id, params.field_ids) - .await .into_iter() .map(FieldPB::new) .collect::<Vec<FieldPB>>() @@ -238,10 +192,9 @@ pub(crate) async fn get_primary_field_handler( ) -> DataResult<FieldPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id = data.into_inner().value; - let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; let mut fields = database_editor .get_fields(&view_id, None) - .await .into_iter() .filter(|field| field.is_primary) .map(FieldPB::new) @@ -267,10 +220,8 @@ pub(crate) async fn update_field_handler( manager: AFPluginState<Weak<DatabaseManager>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; - let params = data.try_into_inner()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let params: FieldChangesetParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.update_field(params).await?; Ok(()) } @@ -282,10 +233,8 @@ pub(crate) async fn update_field_type_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: TypeOptionChangesetParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; - if let Some(old_field) = database_editor.get_field(¶ms.field_id).await { + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + if let Some(old_field) = database_editor.get_field(¶ms.field_id) { let field_type = FieldType::from(old_field.field_type); let type_option_data = type_option_data_from_pb(params.type_option_data, &field_type)?; database_editor @@ -302,9 +251,7 @@ pub(crate) async fn delete_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.delete_field(¶ms.field_id).await?; Ok(()) } @@ -316,9 +263,7 @@ pub(crate) async fn clear_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: FieldIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .clear_field(¶ms.view_id, ¶ms.field_id) .await?; @@ -332,18 +277,27 @@ pub(crate) async fn switch_to_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: EditFieldParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let old_field = database_editor.get_field(¶ms.field_id); database_editor - .switch_to_field_type( - ¶ms.view_id, - ¶ms.field_id, - params.field_type, - params.field_name, - ) + .switch_to_field_type(¶ms.field_id, params.field_type) .await?; + if let Some(new_type_option) = database_editor + .get_field(¶ms.field_id) + .map(|field| field.get_any_type_option(field.field_type)) + { + match (old_field, new_type_option) { + (Some(old_field), Some(new_type_option)) => { + database_editor + .update_field_type_option(¶ms.field_id, new_type_option, old_field) + .await?; + }, + _ => { + tracing::warn!("Old field and the new type option should not be empty"); + }, + } + } Ok(()) } @@ -354,9 +308,7 @@ pub(crate) async fn duplicate_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: DuplicateFieldPayloadPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .duplicate_field(¶ms.view_id, ¶ms.field_id) .await?; @@ -371,9 +323,7 @@ pub(crate) async fn create_field_handler( ) -> DataResult<FieldPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: CreateFieldParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let data = database_editor .create_field_with_type_option(params) .await?; @@ -388,56 +338,33 @@ pub(crate) async fn move_field_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: MoveFieldParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.move_field(params).await?; Ok(()) } // #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_row_handler( - data: AFPluginData<DatabaseViewRowIdPB>, + data: AFPluginData<RowIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<OptionalRowPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let row = database_editor .get_row(¶ms.view_id, ¶ms.row_id) - .await .map(RowPB::from); data_result_ok(OptionalRowPB { row }) } -pub(crate) async fn init_row_handler( - data: AFPluginData<DatabaseViewRowIdPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; - database_editor.init_database_row(¶ms.row_id).await?; - Ok(()) -} - pub(crate) async fn get_row_meta_handler( - data: AFPluginData<DatabaseViewRowIdPB>, + data: AFPluginData<RowIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<RowMetaPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; - match database_editor - .get_row_meta(¶ms.view_id, ¶ms.row_id) - .await - { + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + match database_editor.get_row_meta(¶ms.view_id, ¶ms.row_id) { None => Err(FlowyError::record_not_found()), Some(row) => data_result_ok(row), } @@ -449,9 +376,7 @@ pub(crate) async fn update_row_meta_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: UpdateRowMetaParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let row_id = RowId::from(params.id.clone()); database_editor .update_row_meta(&row_id.clone(), params) @@ -466,9 +391,7 @@ pub(crate) async fn delete_rows_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RepeatedRowIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let row_ids = params .row_ids .into_iter() @@ -480,14 +403,12 @@ pub(crate) async fn delete_rows_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn duplicate_row_handler( - data: AFPluginData<DatabaseViewRowIdPB>, + data: AFPluginData<RowIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .duplicate_row(¶ms.view_id, ¶ms.row_id) .await?; @@ -501,40 +422,13 @@ pub(crate) async fn move_row_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: MoveRowParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_row(¶ms.view_id, params.from_row_id, params.to_row_id) .await?; Ok(()) } -#[tracing::instrument(level = "debug", skip(data, manager), err)] -pub(crate) async fn remove_cover_handler( - data: AFPluginData<RemoveCoverPayloadPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - let params: RemoveCoverParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; - - let update_row_changeset = UpdateRowMetaParams { - id: params.row_id.clone().into(), - view_id: params.view_id.clone(), - cover: Some(RowCover::default()), - ..Default::default() - }; - - database_editor - .update_row_meta(¶ms.row_id, update_row_changeset) - .await; - - Ok(()) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn create_row_handler( data: AFPluginData<CreateRowPayloadPB>, @@ -542,9 +436,7 @@ pub(crate) async fn create_row_handler( ) -> DataResult<RowMetaPB, FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; match database_editor.create_row(params).await? { Some(row) => data_result_ok(RowMetaPB::from(row)), @@ -559,9 +451,7 @@ pub(crate) async fn get_cell_handler( ) -> DataResult<CellPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let cell = database_editor .get_cell_pb(¶ms.field_id, ¶ms.row_id) .await @@ -576,9 +466,7 @@ pub(crate) async fn update_cell_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: CellChangesetPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .update_cell_with_changeset( ¶ms.view_id, @@ -597,9 +485,7 @@ pub(crate) async fn new_select_option_handler( ) -> DataResult<SelectOptionPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: CreateSelectOptionParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let result = database_editor .create_select_option(¶ms.field_id, params.option_name) .await; @@ -619,9 +505,7 @@ pub(crate) async fn insert_or_update_select_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .insert_select_options( ¶ms.view_id, @@ -640,9 +524,7 @@ pub(crate) async fn delete_select_option_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .delete_select_options( ¶ms.view_id, @@ -662,7 +544,7 @@ pub(crate) async fn update_select_option_cell_handler( let manager = upgrade_manager(manager)?; let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?; let database_editor = manager - .get_database_editor_with_view_id(¶ms.cell_identifier.view_id) + .get_database_with_view_id(¶ms.cell_identifier.view_id) .await?; let changeset = SelectOptionCellChangeset { insert_option_ids: params.insert_option_ids, @@ -685,19 +567,24 @@ pub(crate) async fn update_checklist_cell_handler( manager: AFPluginState<Weak<DatabaseManager>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; - let params = data.try_into_inner()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.cell_id.view_id) - .await?; - - let cell_id = params.cell_id.clone(); - + let params: ChecklistCellDataChangesetParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let changeset = ChecklistCellChangeset { + insert_options: params + .insert_options + .into_iter() + .map(|name| (name, false)) + .collect(), + selected_option_ids: params.selected_option_ids, + delete_option_ids: params.delete_option_ids, + update_options: params.update_options, + }; database_editor .update_cell_with_changeset( - &cell_id.view_id, - &(RowId::from(cell_id.row_id)), - &cell_id.field_id, - BoxAny::new(ChecklistCellChangeset::from(params)), + ¶ms.view_id, + ¶ms.row_id, + ¶ms.field_id, + BoxAny::new(changeset), ) .await?; Ok(()) @@ -712,17 +599,17 @@ pub(crate) async fn update_date_cell_handler( let data = data.into_inner(); let cell_id: CellIdParams = data.cell_id.try_into()?; let cell_changeset = DateCellChangeset { - timestamp: data.timestamp, - end_timestamp: data.end_timestamp, + date: data.date, + time: data.time, + end_date: data.end_date, + end_time: data.end_time, include_time: data.include_time, is_range: data.is_range, clear_flag: data.clear_flag, reminder_id: data.reminder_id, }; - let database_editor = manager - .get_database_editor_with_view_id(&cell_id.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; database_editor .update_cell_with_changeset( &cell_id.view_id, @@ -741,9 +628,7 @@ pub(crate) async fn get_groups_handler( ) -> DataResult<RepeatedGroupPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: DatabaseViewIdPB = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(params.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(params.as_ref()).await?; let groups = database_editor.load_groups(params.as_ref()).await?; data_result_ok(groups) } @@ -755,9 +640,7 @@ pub(crate) async fn get_group_handler( ) -> DataResult<GroupPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: DatabaseGroupIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let group = database_editor .get_group(¶ms.view_id, ¶ms.group_id) .await?; @@ -771,9 +654,7 @@ pub(crate) async fn set_group_by_field_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: GroupByFieldParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .set_group_by_field(¶ms.view_id, ¶ms.field_id, params.setting_content) .await?; @@ -788,11 +669,17 @@ pub(crate) async fn update_group_handler( let manager = upgrade_manager(manager)?; let params: UpdateGroupParams = data.into_inner().try_into()?; let view_id = params.view_id.clone(); - let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; let group_changeset = GroupChangeset::from(params); - database_editor - .update_group(&view_id, vec![group_changeset]) - .await?; + let (tx, rx) = oneshot::channel(); + af_spawn(async move { + let result = database_editor + .update_group(&view_id, vec![group_changeset]) + .await; + let _ = tx.send(result); + }); + + let _ = rx.await?; Ok(()) } @@ -803,9 +690,7 @@ pub(crate) async fn move_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: MoveGroupParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_group(¶ms.view_id, ¶ms.from_group_id, ¶ms.to_group_id) .await?; @@ -819,9 +704,7 @@ pub(crate) async fn move_group_row_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: MoveGroupRowParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .move_group_row( ¶ms.view_id, @@ -841,9 +724,7 @@ pub(crate) async fn create_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: CreateGroupParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .create_group(¶ms.view_id, ¶ms.name) .await?; @@ -857,33 +738,23 @@ pub(crate) async fn delete_group_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params: DeleteGroupParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor.delete_group(params).await?; Ok(()) } #[tracing::instrument(level = "debug", skip(manager), err)] -pub(crate) async fn get_default_database_view_id_handler( +pub(crate) async fn get_database_meta_handler( data: AFPluginData<DatabaseIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, -) -> DataResult<DatabaseViewIdPB, FlowyError> { +) -> DataResult<DatabaseMetaPB, FlowyError> { let manager = upgrade_manager(manager)?; let database_id = data.into_inner().value; - let database_view_id = manager - .get_database_meta(&database_id) - .await? - .and_then(|mut d| d.linked_views.pop()) - .ok_or_else(|| { - FlowyError::internal().with_context(format!( - "Can't find any database view for given database id: {}", - database_id - )) - })?; + let inline_view_id = manager.get_database_inline_view_id(&database_id).await?; - let data = DatabaseViewIdPB { - value: database_view_id, + let data = DatabaseMetaPB { + database_id, + inline_view_id, }; data_result_ok(data) } @@ -897,11 +768,14 @@ pub(crate) async fn get_databases_handler( let mut items = Vec::with_capacity(metas.len()); for meta in metas { - if let Some(link_view) = meta.linked_views.first() { - items.push(DatabaseMetaPB { + match manager.get_database_inline_view_id(&meta.database_id).await { + Ok(view_id) => items.push(DatabaseMetaPB { database_id: meta.database_id, - view_id: link_view.clone(), - }) + inline_view_id: view_id, + }), + Err(err) => { + error!(?err); + }, } } @@ -918,19 +792,18 @@ pub(crate) async fn set_layout_setting_handler( let changeset = data.into_inner(); let view_id = changeset.view_id.clone(); let params: LayoutSettingChangeset = changeset.try_into()?; - let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; database_editor.set_layout_setting(&view_id, params).await?; Ok(()) } + pub(crate) async fn get_layout_setting_handler( data: AFPluginData<DatabaseLayoutMetaPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<DatabaseLayoutSettingPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: DatabaseLayoutMeta = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let layout_setting_pb = database_editor .get_layout_setting(¶ms.view_id, params.layout) .await @@ -946,9 +819,7 @@ pub(crate) async fn get_calendar_events_handler( ) -> DataResult<RepeatedCalendarEventPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: CalendarEventRequestParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let events = database_editor .get_all_calendar_events(¶ms.view_id) .await; @@ -962,9 +833,7 @@ pub(crate) async fn get_no_date_calendar_events_handler( ) -> DataResult<RepeatedNoDateCalendarEventPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: CalendarEventRequestParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let _events = database_editor .get_all_no_date_calendar_events(¶ms.view_id) .await; @@ -973,14 +842,12 @@ pub(crate) async fn get_no_date_calendar_events_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_calendar_event_handler( - data: AFPluginData<DatabaseViewRowIdPB>, + data: AFPluginData<RowIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<CalendarEventPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: RowIdParams = data.into_inner().try_into()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; let event = database_editor .get_calendar_event(¶ms.view_id, params.row_id) .await; @@ -999,12 +866,10 @@ pub(crate) async fn move_calendar_event_handler( let data = data.into_inner(); let cell_id: CellIdParams = data.cell_path.try_into()?; let cell_changeset = DateCellChangeset { - timestamp: Some(data.timestamp), + date: Some(data.timestamp), ..Default::default() }; - let database_editor = manager - .get_database_editor_with_view_id(&cell_id.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; database_editor .update_cell_with_changeset( &cell_id.view_id, @@ -1032,7 +897,7 @@ pub(crate) async fn export_csv_handler( ) -> DataResult<DatabaseExportDataPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id = data.into_inner().value; - let database = manager.get_database_editor_with_view_id(&view_id).await?; + let database = manager.get_database_with_view_id(&view_id).await?; let data = database.export_csv(CSVFormat::Original).await?; data_result_ok(DatabaseExportDataPB { export_type: DatabaseExportDataType::CSV, @@ -1040,20 +905,6 @@ pub(crate) async fn export_csv_handler( }) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn export_raw_database_data_handler( - data: AFPluginData<DatabaseViewIdPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> DataResult<DatabaseExportDataPB, FlowyError> { - let manager = upgrade_manager(manager)?; - let view_id = data.into_inner().value; - let data = manager.get_database_json_string(&view_id).await?; - data_result_ok(DatabaseExportDataPB { - export_type: DatabaseExportDataType::RawDatabaseData, - data, - }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_snapshots_handler( data: AFPluginData<DatabaseViewIdPB>, @@ -1072,7 +923,7 @@ pub(crate) async fn get_field_settings_handler( ) -> DataResult<RepeatedFieldSettingsPB, FlowyError> { let manager = upgrade_manager(manager)?; let (view_id, field_ids) = data.into_inner().try_into()?; - let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; let field_settings = database_editor .get_field_settings(&view_id, field_ids.clone()) @@ -1093,9 +944,7 @@ pub(crate) async fn get_all_field_settings_handler( ) -> DataResult<RepeatedFieldSettingsPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; let field_settings = database_editor .get_all_field_settings(view_id.as_ref()) @@ -1116,9 +965,7 @@ pub(crate) async fn update_field_settings_handler( ) -> FlowyResult<()> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let database_editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .update_field_settings_with_changeset(params) .await?; @@ -1132,11 +979,10 @@ pub(crate) async fn get_all_calculations_handler( ) -> DataResult<RepeatedCalculationsPB, FlowyError> { let manager = upgrade_manager(manager)?; let view_id = data.into_inner(); - let database_editor = manager - .get_database_editor_with_view_id(view_id.as_ref()) - .await?; + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; let calculations = database_editor.get_all_calculations(view_id.as_ref()).await; + data_result_ok(calculations) } @@ -1147,9 +993,7 @@ pub(crate) async fn update_calculation_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: UpdateCalculationChangesetPB = data.into_inner(); - let editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let editor = manager.get_database_with_view_id(¶ms.view_id).await?; editor.update_calculation(params).await?; @@ -1163,9 +1007,7 @@ pub(crate) async fn remove_calculation_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params: RemoveCalculationChangesetPB = data.into_inner(); - let editor = manager - .get_database_editor_with_view_id(¶ms.view_id) - .await?; + let editor = manager.get_database_with_view_id(¶ms.view_id).await?; editor.remove_calculation(params).await?; @@ -1199,7 +1041,7 @@ pub(crate) async fn update_relation_cell_handler( removed_row_ids: params.removed_row_ids.into_iter().map(Into::into).collect(), }; - let database_editor = manager.get_database_editor_with_view_id(&view_id).await?; + let database_editor = manager.get_database_with_view_id(&view_id).await?; // // get the related database // let related_database_id = database_editor @@ -1224,39 +1066,30 @@ pub(crate) async fn update_relation_cell_handler( Ok(()) } -#[instrument(level = "debug", skip_all, err)] pub(crate) async fn get_related_row_datas_handler( data: AFPluginData<GetRelatedRowDataPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> { let manager = upgrade_manager(manager)?; let params: GetRelatedRowDataPB = data.into_inner(); - let database_editor = manager - .get_or_init_database_editor(¶ms.database_id) - .await?; - + let database_editor = manager.get_database(¶ms.database_id).await?; let row_datas = database_editor - .get_related_rows(Some(params.row_ids)) + .get_related_rows(Some(¶ms.row_ids)) .await?; data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas }) } -#[instrument(level = "debug", skip_all, err)] pub(crate) async fn get_related_database_rows_handler( data: AFPluginData<DatabaseIdPB>, manager: AFPluginState<Weak<DatabaseManager>>, ) -> DataResult<RepeatedRelatedRowDataPB, FlowyError> { let manager = upgrade_manager(manager)?; let database_id = data.into_inner().value; + let database_editor = manager.get_database(&database_id).await?; + let row_datas = database_editor.get_related_rows(None).await?; - info!( - "[Database]: get related database rows from database_id: {}", - database_id - ); - let database_editor = manager.get_or_init_database_editor(&database_id).await?; - let rows = database_editor.get_related_rows(None).await?; - data_result_ok(RepeatedRelatedRowDataPB { rows }) + data_result_ok(RepeatedRelatedRowDataPB { rows: row_datas }) } pub(crate) async fn summarize_row_handler( @@ -1266,15 +1099,9 @@ pub(crate) async fn summarize_row_handler( let manager = upgrade_manager(manager)?; let data = data.into_inner(); let row_id = RowId::from(data.row_id); - let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { - let result = manager - .summarize_row(&data.view_id, row_id, data.field_id) - .await; - let _ = tx.send(result); - }); - - rx.await??; + manager + .summarize_row(data.view_id, row_id, data.field_id) + .await?; Ok(()) } @@ -1285,131 +1112,8 @@ pub(crate) async fn translate_row_handler( let manager = upgrade_manager(manager)?; let data = data.try_into_inner()?; let row_id = RowId::from(data.row_id); - let (tx, rx) = oneshot::channel(); - tokio::spawn(async move { - let result = manager - .translate_row(&data.view_id, row_id, data.field_id) - .await; - let _ = tx.send(result); - }); - - rx.await??; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn update_media_cell_handler( - data: AFPluginData<MediaCellChangesetPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> FlowyResult<()> { - let manager = upgrade_manager(manager)?; - let params: MediaCellChangesetPB = data.into_inner(); - let cell_id: CellIdParams = params.cell_id.try_into()?; - let cell_changeset = MediaCellChangeset { - inserted_files: params - .inserted_files - .clone() - .into_iter() - .map(Into::into) - .collect(), - removed_ids: params.removed_ids, - }; - - let database_editor = manager - .get_database_editor_with_view_id(&cell_id.view_id) - .await?; - - database_editor - .update_cell_with_changeset( - &cell_id.view_id, - &cell_id.row_id, - &cell_id.field_id, - BoxAny::new(cell_changeset), - ) - .await?; - - Ok(()) -} - -/// We use a custom handler to rename the media file, as the ordering -/// of the files must be maintained while renaming a file. -/// -/// The changeset in [update_media_cell_handler] contains removals and inserts, -/// and if we were to remove and insert the file, it would mess up the ordering. -/// -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn rename_media_cell_file_handler( - data: AFPluginData<RenameMediaChangesetPB>, - manager: AFPluginState<Weak<DatabaseManager>>, -) -> FlowyResult<()> { - let manager = upgrade_manager(manager)?; - let params: RenameMediaChangesetPB = data.into_inner(); - let cell_id: CellIdParams = params.cell_id.try_into()?; - - let database_editor = manager - .get_database_editor_with_view_id(&cell_id.view_id) - .await?; - - let cell = database_editor - .get_cell(&cell_id.field_id, &cell_id.row_id) - .await; - if cell.is_none() { - return Err(FlowyError::record_not_found()); - } - - let cell = cell.unwrap(); - let field = database_editor - .get_field(&cell_id.field_id) - .await - .ok_or_else(FlowyError::record_not_found)?; - let handler = TypeOptionCellExt::new(&field, None) - .get_type_option_cell_data_handler_with_field_type(FieldType::Media); - if handler.is_none() { - return Err( - FlowyError::internal().with_context("Error renaming media file: field type is not Media"), - ); - } - let handler = handler.unwrap(); - let data = handler - .handle_get_boxed_cell_data(&cell, &field) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(MediaCellData::default); - - let file = data - .files - .iter() - .find(|file| file.id == params.file_id) - .ok_or_else(FlowyError::record_not_found)?; - - let new_file = file.rename(params.name); - let new_data = MediaCellData { - files: data - .files - .iter() - .map(|file| { - if file.id == params.file_id { - new_file.clone() - } else { - file.clone() - } - }) - .collect(), - }; - - let result = database_editor - .update_cell( - &cell_id.view_id, - &cell_id.row_id, - &cell_id.field_id, - Cell::from(new_data), - ) - .await; - - if result.is_err() { - return Err( - FlowyError::internal().with_context("Error renaming media file: update cell failed"), - ); - } - + manager + .translate_row(data.view_id, row_id, data.field_id) + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 824565e5b8..02c64da785 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -13,91 +13,85 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin { .name(env!("CARGO_PKG_NAME")) .state(database_manager); plugin - .event(DatabaseEvent::GetDatabase, get_database_data_handler) - .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) - .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) - .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) - .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) - .event(DatabaseEvent::GetAllFilters, get_all_filters_handler) - .event(DatabaseEvent::GetAllSorts, get_all_sorts_handler) - .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) - // Field - .event(DatabaseEvent::GetFields, get_fields_handler) - .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler) - .event(DatabaseEvent::UpdateField, update_field_handler) - .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) - .event(DatabaseEvent::DeleteField, delete_field_handler) - .event(DatabaseEvent::ClearField, clear_field_handler) - .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) - .event(DatabaseEvent::DuplicateField, duplicate_field_handler) - .event(DatabaseEvent::MoveField, move_field_handler) - .event(DatabaseEvent::CreateField, create_field_handler) - // Row - .event(DatabaseEvent::CreateRow, create_row_handler) - .event(DatabaseEvent::GetRow, get_row_handler) - .event(DatabaseEvent::InitRow, init_row_handler) - .event(DatabaseEvent::GetRowMeta, get_row_meta_handler) - .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler) - .event(DatabaseEvent::DeleteRows, delete_rows_handler) - .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) - .event(DatabaseEvent::MoveRow, move_row_handler) - .event(DatabaseEvent::RemoveCover, remove_cover_handler) - // Cell - .event(DatabaseEvent::GetCell, get_cell_handler) - .event(DatabaseEvent::UpdateCell, update_cell_handler) - // SelectOption - .event(DatabaseEvent::CreateSelectOption, new_select_option_handler) - .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) - .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) - .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) - // Checklist - .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) - // Date - .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) - // Group - .event(DatabaseEvent::SetGroupByField, set_group_by_field_handler) - .event(DatabaseEvent::MoveGroup, move_group_handler) - .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) - .event(DatabaseEvent::GetGroups, get_groups_handler) - .event(DatabaseEvent::GetGroup, get_group_handler) - .event(DatabaseEvent::UpdateGroup, update_group_handler) - .event(DatabaseEvent::CreateGroup, create_group_handler) - .event(DatabaseEvent::DeleteGroup, delete_group_handler) - // Database - .event(DatabaseEvent::GetDefaultDatabaseViewId, get_default_database_view_id_handler) - .event(DatabaseEvent::GetDatabases, get_databases_handler) - // Calendar - .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) - .event(DatabaseEvent::GetNoDateCalendarEvents, get_no_date_calendar_events_handler) - .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler) - .event(DatabaseEvent::MoveCalendarEvent, move_calendar_event_handler) - // Layout setting - .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler) - .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler) - .event(DatabaseEvent::CreateDatabaseView, create_database_view) - // Export - .event(DatabaseEvent::ExportCSV, export_csv_handler) - .event(DatabaseEvent::ExportRawDatabaseData, export_raw_database_data_handler) - .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) - // Field settings - .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) - .event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler) - .event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler) - // Calculations - .event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler) - .event(DatabaseEvent::UpdateCalculation, update_calculation_handler) - .event(DatabaseEvent::RemoveCalculation, remove_calculation_handler) - // Relation - .event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler) - .event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler) - .event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler) - .event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler) - // AI - .event(DatabaseEvent::SummarizeRow, summarize_row_handler) - .event(DatabaseEvent::TranslateRow, translate_row_handler) - // Media - .event(DatabaseEvent::UpdateMediaCell, update_media_cell_handler) - .event(DatabaseEvent::RenameMediaFile, rename_media_cell_file_handler) + .event(DatabaseEvent::GetDatabase, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) + .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) + .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) + .event(DatabaseEvent::GetAllFilters, get_all_filters_handler) + .event(DatabaseEvent::GetAllSorts, get_all_sorts_handler) + .event(DatabaseEvent::DeleteAllSorts, delete_all_sorts_handler) + // Field + .event(DatabaseEvent::GetFields, get_fields_handler) + .event(DatabaseEvent::GetPrimaryField, get_primary_field_handler) + .event(DatabaseEvent::UpdateField, update_field_handler) + .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) + .event(DatabaseEvent::DeleteField, delete_field_handler) + .event(DatabaseEvent::ClearField, clear_field_handler) + .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) + .event(DatabaseEvent::DuplicateField, duplicate_field_handler) + .event(DatabaseEvent::MoveField, move_field_handler) + .event(DatabaseEvent::CreateField, create_field_handler) + // Row + .event(DatabaseEvent::CreateRow, create_row_handler) + .event(DatabaseEvent::GetRow, get_row_handler) + .event(DatabaseEvent::GetRowMeta, get_row_meta_handler) + .event(DatabaseEvent::UpdateRowMeta, update_row_meta_handler) + .event(DatabaseEvent::DeleteRows, delete_rows_handler) + .event(DatabaseEvent::DuplicateRow, duplicate_row_handler) + .event(DatabaseEvent::MoveRow, move_row_handler) + // Cell + .event(DatabaseEvent::GetCell, get_cell_handler) + .event(DatabaseEvent::UpdateCell, update_cell_handler) + // SelectOption + .event(DatabaseEvent::CreateSelectOption, new_select_option_handler) + .event(DatabaseEvent::InsertOrUpdateSelectOption, insert_or_update_select_option_handler) + .event(DatabaseEvent::DeleteSelectOption, delete_select_option_handler) + .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) + // Checklist + .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) + // Date + .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) + // Group + .event(DatabaseEvent::SetGroupByField, set_group_by_field_handler) + .event(DatabaseEvent::MoveGroup, move_group_handler) + .event(DatabaseEvent::MoveGroupRow, move_group_row_handler) + .event(DatabaseEvent::GetGroups, get_groups_handler) + .event(DatabaseEvent::GetGroup, get_group_handler) + .event(DatabaseEvent::UpdateGroup, update_group_handler) + .event(DatabaseEvent::CreateGroup, create_group_handler) + .event(DatabaseEvent::DeleteGroup, delete_group_handler) + // Database + .event(DatabaseEvent::GetDatabaseMeta, get_database_meta_handler) + .event(DatabaseEvent::GetDatabases, get_databases_handler) + // Calendar + .event(DatabaseEvent::GetAllCalendarEvents, get_calendar_events_handler) + .event(DatabaseEvent::GetNoDateCalendarEvents, get_no_date_calendar_events_handler) + .event(DatabaseEvent::GetCalendarEvent, get_calendar_event_handler) + .event(DatabaseEvent::MoveCalendarEvent, move_calendar_event_handler) + // Layout setting + .event(DatabaseEvent::SetLayoutSetting, set_layout_setting_handler) + .event(DatabaseEvent::GetLayoutSetting, get_layout_setting_handler) + .event(DatabaseEvent::CreateDatabaseView, create_database_view) + // Export + .event(DatabaseEvent::ExportCSV, export_csv_handler) + .event(DatabaseEvent::GetDatabaseSnapshots, get_snapshots_handler) + // Field settings + .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) + .event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler) + .event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler) + // Calculations + .event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler) + .event(DatabaseEvent::UpdateCalculation, update_calculation_handler) + .event(DatabaseEvent::RemoveCalculation, remove_calculation_handler) + // Relation + .event(DatabaseEvent::GetRelatedDatabaseIds, get_related_database_ids_handler) + .event(DatabaseEvent::UpdateRelationCell, update_relation_cell_handler) + .event(DatabaseEvent::GetRelatedRowDatas, get_related_row_datas_handler) + .event(DatabaseEvent::GetRelatedDatabaseRows, get_related_database_rows_handler) + // AI + .event(DatabaseEvent::SummarizeRow, summarize_row_handler) + .event(DatabaseEvent::TranslateRow, translate_row_handler) } /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) @@ -227,27 +221,24 @@ pub enum DatabaseEvent { /// [GetRow] event is used to get the row data,[RowPB]. [OptionalRowPB] is a wrapper that enables /// to return a nullable row data. - #[event(input = "DatabaseViewRowIdPB", output = "OptionalRowPB")] + #[event(input = "RowIdPB", output = "OptionalRowPB")] GetRow = 51, #[event(input = "RepeatedRowIdPB")] DeleteRows = 52, - #[event(input = "DatabaseViewRowIdPB")] + #[event(input = "RowIdPB")] DuplicateRow = 53, #[event(input = "MoveRowPayloadPB")] MoveRow = 54, - #[event(input = "DatabaseViewRowIdPB", output = "RowMetaPB")] + #[event(input = "RowIdPB", output = "RowMetaPB")] GetRowMeta = 55, #[event(input = "UpdateRowMetaChangesetPB")] UpdateRowMeta = 56, - #[event(input = "RemoveCoverPayloadPB")] - RemoveCover = 57, - #[event(input = "CellIdPB", output = "CellPB")] GetCell = 70, @@ -305,8 +296,8 @@ pub enum DatabaseEvent { #[event(input = "DeleteGroupPayloadPB")] DeleteGroup = 115, - #[event(input = "DatabaseIdPB", output = "DatabaseViewIdPB")] - GetDefaultDatabaseViewId = 119, + #[event(input = "DatabaseIdPB", output = "DatabaseMetaPB")] + GetDatabaseMeta = 119, /// Returns all the databases #[event(output = "RepeatedDatabaseDescriptionPB")] @@ -327,7 +318,7 @@ pub enum DatabaseEvent { )] GetNoDateCalendarEvents = 124, - #[event(input = "DatabaseViewRowIdPB", output = "CalendarEventPB")] + #[event(input = "RowIdPB", output = "CalendarEventPB")] GetCalendarEvent = 125, #[event(input = "MoveCalendarEventPB")] @@ -386,19 +377,4 @@ pub enum DatabaseEvent { #[event(input = "TranslateRowPB")] TranslateRow = 175, - - #[event(input = "DatabaseViewRowIdPB")] - InitRow = 176, - - #[event(input = "DatabaseViewIdPB", output = "RepeatedRowMetaPB")] - GetAllRows = 177, - - #[event(input = "DatabaseViewIdPB", output = "DatabaseExportDataPB")] - ExportRawDatabaseData = 178, - - #[event(input = "MediaCellChangesetPB")] - UpdateMediaCell = 200, - - #[event(input = "RenameMediaChangesetPB")] - RenameMediaFile = 201, } diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 666d2f8eaf..598d3cec37 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -1,91 +1,83 @@ use anyhow::anyhow; -use arc_swap::ArcSwapOption; -use async_trait::async_trait; -use collab::core::collab::DataSource; -use collab::core::origin::CollabOrigin; -use collab::lock::RwLock; -use collab::preclude::Collab; -use collab_database::database::{Database, DatabaseData}; -use collab_database::entity::{CreateDatabaseParams, CreateViewParams, EncodedDatabase}; -use collab_database::error::DatabaseError; -use collab_database::fields::translate_type_option::TranslateTypeOption; -use collab_database::rows::RowId; -use collab_database::template::csv::CSVTemplate; -use collab_database::views::DatabaseLayout; -use collab_database::workspace_database::{ - CollabPersistenceImpl, DatabaseCollabPersistenceService, DatabaseCollabService, DatabaseMeta, - EncodeCollabByOid, WorkspaceDatabaseManager, -}; -use collab_entity::{CollabObject, CollabType, EncodedCollab}; -use collab_plugins::local_storage::kv::KVTransactionDB; -use rayon::prelude::*; use std::collections::HashMap; -use std::str::FromStr; use std::sync::{Arc, Weak}; -use std::time::Duration; -use tokio::sync::Mutex; -use tracing::{error, info, instrument, trace}; + +use collab::core::collab::{DataSource, MutexCollab}; +use collab_database::database::DatabaseData; +use collab_database::error::DatabaseError; +use collab_database::rows::RowId; +use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; +use collab_database::workspace_database::{ + CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, +}; +use collab_entity::CollabType; +use collab_plugins::local_storage::kv::KVTransactionDB; +use tokio::sync::{Mutex, RwLock}; +use tracing::{event, instrument, trace}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; -use collab_integrate::{CollabKVAction, CollabKVDB}; +use collab_integrate::{CollabKVAction, CollabKVDB, CollabPersistenceConfig}; use flowy_database_pub::cloud::{ - DatabaseAIService, DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent, + DatabaseCloudService, SummaryRowContent, TranslateItem, TranslateRowContent, }; use flowy_error::{internal_error, FlowyError, FlowyResult}; - use lib_infra::box_any::BoxAny; use lib_infra::priority_task::TaskDispatcher; -use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, FieldType, RowMetaPB}; +use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, FieldType}; use crate::services::cell::stringify_cell; use crate::services::database::DatabaseEditor; use crate::services::database_view::DatabaseLayoutDepsResolver; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; + use crate::services::field_settings::default_field_settings_by_layout_map; use crate::services::share::csv::{CSVFormat, CSVImporter, ImportResult}; -use tokio::sync::RwLock as TokioRwLock; -use uuid::Uuid; pub trait DatabaseUser: Send + Sync { fn user_id(&self) -> Result<i64, FlowyError>; fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>; - fn workspace_id(&self) -> Result<Uuid, FlowyError>; - fn workspace_database_object_id(&self) -> Result<Uuid, FlowyError>; + fn workspace_id(&self) -> Result<String, FlowyError>; + fn workspace_database_object_id(&self) -> Result<String, FlowyError>; } -pub(crate) type DatabaseEditorMap = HashMap<String, Arc<DatabaseEditor>>; pub struct DatabaseManager { user: Arc<dyn DatabaseUser>, - workspace_database_manager: ArcSwapOption<RwLock<WorkspaceDatabaseManager>>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, - pub(crate) editors: Mutex<DatabaseEditorMap>, - removing_editor: Arc<Mutex<HashMap<String, Arc<DatabaseEditor>>>>, + workspace_database: Arc<RwLock<Option<Arc<WorkspaceDatabase>>>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, + editors: Mutex<HashMap<String, Arc<DatabaseEditor>>>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DatabaseCloudService>, - ai_service: Arc<dyn DatabaseAIService>, } impl DatabaseManager { pub fn new( database_user: Arc<dyn DatabaseUser>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DatabaseCloudService>, - ai_service: Arc<dyn DatabaseAIService>, ) -> Self { Self { user: database_user, - workspace_database_manager: Default::default(), + workspace_database: Default::default(), task_scheduler, editors: Default::default(), - removing_editor: Default::default(), collab_builder, cloud_service, - ai_service, + } + } + + fn is_collab_exist(&self, uid: i64, collab_db: &Weak<CollabKVDB>, object_id: &str) -> bool { + match collab_db.upgrade() { + None => false, + Some(collab_db) => { + let read_txn = collab_db.read_txn(); + read_txn.is_exist(uid, object_id) + }, } } /// When initialize with new workspace, all the resources will be cleared. - pub async fn initialize(&self, uid: i64, is_local_user: bool) -> FlowyResult<()> { + pub async fn initialize(&self, uid: i64) -> FlowyResult<()> { // 1. Clear all existing tasks self.task_scheduler.write().await.clear_task(); // 2. Release all existing editors @@ -93,104 +85,106 @@ impl DatabaseManager { editor.close_all_views().await; } self.editors.lock().await.clear(); - self.removing_editor.lock().await.clear(); // 3. Clear the workspace database - if let Some(old_workspace_database) = self.workspace_database_manager.swap(None) { - info!("Close the old workspace database"); - let wdb = old_workspace_database.read().await; - wdb.close(); + if let Some(old_workspace_database) = self.workspace_database.write().await.take() { + old_workspace_database.close(); } + *self.workspace_database.write().await = None; let collab_db = self.user.collab_db(uid)?; - let collab_service = WorkspaceDatabaseCollabServiceImpl::new( - is_local_user, - self.user.clone(), - self.collab_builder.clone(), - self.cloud_service.clone(), - ); + let collab_builder = UserDatabaseCollabServiceImpl { + user: self.user.clone(), + collab_builder: self.collab_builder.clone(), + cloud_service: self.cloud_service.clone(), + }; + let config = CollabPersistenceConfig::new().snapshot_per_update(100); + let workspace_id = self.user.workspace_id()?; let workspace_database_object_id = self.user.workspace_database_object_id()?; - let workspace_database_collab = collab_service - .build_collab( - workspace_database_object_id.to_string().as_str(), - CollabType::WorkspaceDatabase, - None, - ) - .await?; - let collab_object = collab_service - .build_collab_object(&workspace_database_object_id, CollabType::WorkspaceDatabase)?; - let workspace_database = self.collab_builder.create_workspace_database_manager( - collab_object, - workspace_database_collab, - collab_db, - CollabBuilderConfig::default().sync_enable(true), - collab_service, - )?; + let mut workspace_database_doc_state = DataSource::Disk; + // If the workspace database not exist in disk, try to fetch from remote. + if !self.is_collab_exist(uid, &collab_db, &workspace_database_object_id) { + trace!("workspace database not exist, try to fetch from remote"); + match self + .cloud_service + .get_database_object_doc_state( + &workspace_database_object_id, + CollabType::WorkspaceDatabase, + &workspace_id, + ) + .await + { + Ok(doc_state) => match doc_state { + Some(doc_state) => { + workspace_database_doc_state = DataSource::DocStateV1(doc_state); + }, + None => { + workspace_database_doc_state = DataSource::Disk; + }, + }, + Err(err) => { + return Err(FlowyError::record_not_found().with_context(format!( + "get workspace database :{} failed: {}", + workspace_database_object_id, err, + ))); + }, + } + } - self - .workspace_database_manager - .store(Some(workspace_database)); + // Construct the workspace database. + event!( + tracing::Level::INFO, + "open aggregate database views object: {}", + &workspace_database_object_id + ); + let collab = collab_builder.build_collab_with_config( + uid, + &workspace_database_object_id, + CollabType::WorkspaceDatabase, + collab_db.clone(), + workspace_database_doc_state, + config.clone(), + )?; + let workspace_database = + WorkspaceDatabase::open(uid, collab, collab_db, config, collab_builder); + *self.workspace_database.write().await = Some(Arc::new(workspace_database)); Ok(()) } #[instrument( - name = "database_initialize_after_sign_up", + name = "database_initialize_with_new_user", level = "debug", skip_all, err )] - pub async fn initialize_after_sign_up( - &self, - user_id: i64, - is_local_user: bool, - ) -> FlowyResult<()> { - self.initialize(user_id, is_local_user).await?; + pub async fn initialize_with_new_user(&self, user_id: i64) -> FlowyResult<()> { + self.initialize(user_id).await?; Ok(()) } - pub async fn initialize_after_open_workspace( - &self, - user_id: i64, - is_local_user: bool, - ) -> FlowyResult<()> { - self.initialize(user_id, is_local_user).await?; - Ok(()) - } + pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult<String> { + let wdb = self.get_database_indexer().await?; + let database_collab = wdb.get_database(database_id).await.ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id)) + })?; - pub async fn initialize_after_sign_in( - &self, - user_id: i64, - is_local_user: bool, - ) -> FlowyResult<()> { - self.initialize(user_id, is_local_user).await?; - Ok(()) + let lock_guard = database_collab.lock(); + Ok(lock_guard.get_inline_view_id()) } pub async fn get_all_databases_meta(&self) -> Vec<DatabaseMeta> { let mut items = vec![]; - if let Some(lock) = self.workspace_database_manager.load_full() { - let wdb = lock.read().await; + if let Ok(wdb) = self.get_database_indexer().await { items = wdb.get_all_database_meta() } items } - pub async fn get_database_meta(&self, database_id: &str) -> FlowyResult<Option<DatabaseMeta>> { - let mut database_meta = None; - if let Some(lock) = self.workspace_database_manager.load_full() { - let wdb = lock.read().await; - database_meta = wdb.get_database_meta(database_id); - } - Ok(database_meta) - } - - #[instrument(level = "trace", skip_all, err)] pub async fn update_database_indexing( &self, view_ids_by_database_id: HashMap<String, Vec<String>>, ) -> FlowyResult<()> { - let lock = self.workspace_database()?; - let mut wdb = lock.write().await; + let wdb = self.get_database_indexer().await?; view_ids_by_database_id .into_iter() .for_each(|(database_id, view_ids)| { @@ -199,96 +193,37 @@ impl DatabaseManager { Ok(()) } + pub async fn get_database_with_view_id(&self, view_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { + let database_id = self.get_database_id_with_view_id(view_id).await?; + self.get_database(&database_id).await + } + pub async fn get_database_id_with_view_id(&self, view_id: &str) -> FlowyResult<String> { - let lock = self.workspace_database()?; - let wdb = lock.read().await; - let database_id = wdb.get_database_id_with_view_id(view_id); - database_id.ok_or_else(|| { + let wdb = self.get_database_indexer().await?; + wdb.get_database_id_with_view_id(view_id).ok_or_else(|| { FlowyError::record_not_found() .with_context(format!("The database for view id: {} not found", view_id)) }) } - pub async fn encode_database(&self, view_id: &Uuid) -> FlowyResult<EncodedDatabase> { - let editor = self - .get_database_editor_with_view_id(view_id.to_string().as_str()) - .await?; - let collabs = editor - .database - .read() - .await - .encode_database_collabs() - .await?; - Ok(collabs) - } - - pub async fn get_database_row_ids_with_view_id(&self, view_id: &str) -> FlowyResult<Vec<RowId>> { - let database = self.get_database_editor_with_view_id(view_id).await?; - Ok(database.get_row_ids().await) - } - - pub async fn get_database_row_metas_with_view_id( - &self, - view_id: &Uuid, - row_ids: Vec<RowId>, - ) -> FlowyResult<Vec<RowMetaPB>> { - let database = self - .get_database_editor_with_view_id(view_id.to_string().as_str()) - .await?; - let view_id = view_id.to_string(); - let mut row_metas: Vec<RowMetaPB> = vec![]; - for row_id in row_ids { - if let Some(row_meta) = database.get_row_meta(&view_id, &row_id).await { - row_metas.push(row_meta); - } - } - Ok(row_metas) - } - - pub async fn get_database_editor_with_view_id( - &self, - view_id: &str, - ) -> FlowyResult<Arc<DatabaseEditor>> { - let database_id = self.get_database_id_with_view_id(view_id).await?; - self.get_or_init_database_editor(&database_id).await - } - - pub async fn get_or_init_database_editor( - &self, - database_id: &str, - ) -> FlowyResult<Arc<DatabaseEditor>> { + pub async fn get_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { if let Some(editor) = self.editors.lock().await.get(database_id).cloned() { return Ok(editor); } - let editor = self.open_database(database_id).await?; - Ok(editor) + // TODO(nathan): refactor the get_database that split the database creation and database opening. + self.open_database(database_id).await } - #[instrument(level = "trace", skip_all, err)] pub async fn open_database(&self, database_id: &str) -> FlowyResult<Arc<DatabaseEditor>> { - let workspace_database = self.workspace_database()?; - if let Some(database_editor) = self.removing_editor.lock().await.remove(database_id) { - self - .editors - .lock() - .await - .insert(database_id.to_string(), database_editor.clone()); - return Ok(database_editor); - } - - trace!("[Database]: init database editor:{}", database_id); - // When the user opens the database from the left-side bar, it may fail because the workspace database - // hasn't finished syncing yet. In such cases, get_or_create_database will return None. - // The workaround is to add a retry mechanism to attempt fetching the database again. - let database = open_database_with_retry(workspace_database, database_id).await?; - let editor = DatabaseEditor::new( - self.user.clone(), - database, - self.task_scheduler.clone(), - self.collab_builder.clone(), - ) - .await?; + trace!("open database editor:{}", database_id); + let database = self + .get_database_indexer() + .await? + .get_database(database_id) + .await + .ok_or_else(|| FlowyError::collab_not_sync().with_context("open database error"))?; + let editor = Arc::new(DatabaseEditor::new(database, self.task_scheduler.clone()).await?); self .editors .lock() @@ -297,71 +232,38 @@ impl DatabaseManager { Ok(editor) } - /// Open the database view - #[instrument(level = "trace", skip_all, err)] - pub async fn open_database_view(&self, view_id: &Uuid) -> FlowyResult<()> { - let view_id = view_id.to_string(); - let lock = self.workspace_database()?; - let workspace_database = lock.read().await; - let result = workspace_database.get_database_id_with_view_id(&view_id); - drop(workspace_database); - - if let Some(database_id) = result { - let is_not_exist = self.editors.lock().await.get(&database_id).is_none(); - if is_not_exist { - let _ = self.open_database(&database_id).await?; + pub async fn open_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); + let wdb = self.get_database_indexer().await?; + if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) { + if let Some(database) = wdb.open_database(&database_id) { + if let Some(lock_database) = database.try_lock() { + if let Some(lock_collab) = lock_database.get_collab().try_lock() { + trace!("{} database start init sync", view_id); + lock_collab.start_init_sync(); + } + } } } Ok(()) } - #[instrument(level = "trace", skip_all, err)] - pub async fn close_database_view(&self, view_id: &str) -> FlowyResult<()> { - let lock = self.workspace_database()?; - let workspace_database = lock.read().await; - let database_id = workspace_database.get_database_id_with_view_id(view_id); - drop(workspace_database); - + pub async fn close_database_view<T: AsRef<str>>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); + let wdb = self.get_database_indexer().await?; + let database_id = wdb.get_database_id_with_view_id(view_id); if let Some(database_id) = database_id { let mut editors = self.editors.lock().await; let mut should_remove = false; if let Some(editor) = editors.get(&database_id) { editor.close_view(view_id).await; - // when there is no opening views, mark the database to be removed. - trace!( - "[Database]: {} has {} opening views", - database_id, - editor.num_of_opening_views().await - ); - should_remove = editor.num_of_opening_views().await == 0; + should_remove = editor.num_views().await == 0; } if should_remove { - let editor = editors.remove(&database_id); - drop(editors); - - if let Some(editor) = editor { - editor.close_database().await; - self - .removing_editor - .lock() - .await - .insert(database_id.to_string(), editor); - - let weak_workspace_database = Arc::downgrade(&self.workspace_database()?); - let weak_removing_editors = Arc::downgrade(&self.removing_editor); - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_secs(120)).await; - if let Some(removing_editors) = weak_removing_editors.upgrade() { - if removing_editors.lock().await.remove(&database_id).is_some() { - if let Some(workspace_database) = weak_workspace_database.upgrade() { - let wdb = workspace_database.write().await; - wdb.close_database(&database_id); - } - } - } - }); - } + trace!("remove database editor:{}", database_id); + editors.remove(&database_id); + wdb.close_database(&database_id); } } @@ -369,92 +271,48 @@ impl DatabaseManager { } pub async fn delete_database_view(&self, view_id: &str) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; + let database = self.get_database_with_view_id(view_id).await?; let _ = database.delete_database_view(view_id).await?; Ok(()) } - pub async fn get_database_data(&self, view_id: &str) -> FlowyResult<DatabaseData> { - let lock = self.workspace_database()?; - let wdb = lock.read().await; + pub async fn duplicate_database(&self, view_id: &str) -> FlowyResult<Vec<u8>> { + let wdb = self.get_database_indexer().await?; let data = wdb.get_database_data(view_id).await?; - Ok(data) - } - - pub async fn get_database_json_string(&self, view_id: &str) -> FlowyResult<String> { - let lock = self.workspace_database()?; - let wdb = lock.read().await; - let data = wdb.get_database_data(view_id).await?; - let json_string = serde_json::to_string(&data)?; - Ok(json_string) + let json_bytes = data.to_json_bytes()?; + Ok(json_bytes) } /// Create a new database with the given data that can be deserialized to [DatabaseData]. #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn create_database_with_data( + pub async fn create_database_with_database_data( &self, - new_database_view_id: &str, + view_id: &str, data: Vec<u8>, - ) -> FlowyResult<EncodedCollab> { + ) -> FlowyResult<()> { let database_data = DatabaseData::from_json_bytes(data)?; - if database_data.views.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database data is empty")); + + let mut create_database_params = CreateDatabaseParams::from_database_data(database_data); + let old_view_id = create_database_params.inline_view_id.clone(); + create_database_params.inline_view_id = view_id.to_string(); + + if let Some(create_view_params) = create_database_params + .views + .iter_mut() + .find(|view| view.view_id == old_view_id) + { + create_view_params.view_id = view_id.to_string(); } - // choose the first view as the display view. The new database_view_id is the ID in the Folder. - let database_view_id = database_data.views[0].id.clone(); - let create_database_params = CreateDatabaseParams::from_database_data( - database_data, - &database_view_id, - new_database_view_id, - ); - - let lock = self.workspace_database()?; - let mut wdb = lock.write().await; - let database = wdb.create_database(create_database_params).await?; - drop(wdb); - - let encoded_collab = database - .read() - .await - .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab)) - .map_err(|err| FlowyError::internal().with_context(err))?; - Ok(encoded_collab) + let wdb = self.get_database_indexer().await?; + let _ = wdb.create_database(create_database_params)?; + Ok(()) } - /// When duplicating a database view, it will duplicate all the database views and replace the duplicated - /// database_view_id with the new_database_view_id. The new database id is the ID created by Folder. - #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn duplicate_database( - &self, - database_view_id: &str, - new_database_view_id: &str, - ) -> FlowyResult<EncodedCollab> { - let lock = self.workspace_database()?; - let mut wdb = lock.write().await; - let database = wdb - .duplicate_database(database_view_id, new_database_view_id) - .await?; - drop(wdb); - - let encoded_collab = database - .read() - .await - .encode_collab_v1(|collab| CollabType::Database.validate_require_data(collab)) - .map_err(|err| FlowyError::internal().with_context(err))?; - Ok(encoded_collab) - } - - pub async fn import_database( - &self, - params: CreateDatabaseParams, - ) -> FlowyResult<Arc<RwLock<Database>>> { - let lock = self.workspace_database()?; - let mut wdb = lock.write().await; - let database = wdb.create_database(params).await?; - drop(wdb); - - Ok(database) + pub async fn create_database_with_params(&self, params: CreateDatabaseParams) -> FlowyResult<()> { + let wdb = self.get_database_indexer().await?; + let _ = wdb.create_database(params)?; + Ok(()) } /// A linked view is a view that is linked to existing database. @@ -465,25 +323,18 @@ impl DatabaseManager { layout: DatabaseLayout, database_id: String, database_view_id: String, - database_parent_view_id: String, ) -> FlowyResult<()> { - let workspace_database = self.workspace_database()?; - let mut wdb = workspace_database.write().await; + let wdb = self.get_database_indexer().await?; let mut params = CreateViewParams::new(database_id.clone(), database_view_id, name, layout); - if let Ok(database) = wdb.get_or_init_database(&database_id).await { - let (field, layout_setting, field_settings_map) = - DatabaseLayoutDepsResolver::new(database, layout) - .resolve_deps_when_create_database_linked_view(&database_parent_view_id) - .await; + if let Some(database) = wdb.get_database(&database_id).await { + let (field, layout_setting) = DatabaseLayoutDepsResolver::new(database, layout) + .resolve_deps_when_create_database_linked_view(); if let Some(field) = field { params = params.with_deps_fields(vec![field], vec![default_field_settings_by_layout_map()]); } if let Some(layout_setting) = layout_setting { params = params.with_layout_setting(layout_setting); } - if let Some(field_settings_map) = field_settings_map { - params = params.with_field_settings_map(field_settings_map); - } }; wdb.create_database_linked_view(params).await?; Ok(()) @@ -495,43 +346,36 @@ impl DatabaseManager { content: String, format: CSVFormat, ) -> FlowyResult<ImportResult> { - let params = match format { - CSVFormat::Original => { - let mut csv_template = CSVTemplate::try_from_reader(content.as_bytes(), true, None)?; - csv_template.reset_view_id(view_id.clone()); + let params = tokio::task::spawn_blocking(move || { + CSVImporter.import_csv_from_string(view_id, content, format) + }) + .await + .map_err(internal_error)??; - let database_template = csv_template.try_into_database_template(None).await?; - database_template.into_params() - }, - - CSVFormat::META => { - let cloned_view_id = view_id.clone(); - tokio::task::spawn_blocking(move || { - CSVImporter.import_csv_from_string(cloned_view_id, content, format) - }) - .await - .map_err(internal_error)?? - }, - }; - - let database_id = params.database_id.clone(); - let database = self.import_database(params).await?; - let encoded_database = database.read().await.encode_database_collabs().await?; - let encoded_collabs = std::iter::once(encoded_database.encoded_database_collab) - .chain(encoded_database.encoded_row_collabs.into_iter()) - .collect::<Vec<_>>(); + // Currently, we only support importing up to 500 rows. We can support more rows in the future. + if !cfg!(debug_assertions) && params.rows.len() > 500 { + return Err(FlowyError::internal().with_context("The number of rows exceeds the limit")); + } let result = ImportResult { - database_id, - view_id, - encoded_collabs, + database_id: params.database_id.clone(), + view_id: params.inline_view_id.clone(), }; - info!("import csv result: {}", result); + self.create_database_with_params(params).await?; Ok(result) } + // will implement soon + pub async fn import_csv_from_file( + &self, + _file_path: String, + _format: CSVFormat, + ) -> FlowyResult<()> { + Ok(()) + } + pub async fn export_csv(&self, view_id: &str, style: CSVFormat) -> FlowyResult<String> { - let database = self.get_database_editor_with_view_id(view_id).await?; + let database = self.get_database_with_view_id(view_id).await?; database.export_csv(style).await } @@ -540,10 +384,8 @@ impl DatabaseManager { view_id: &str, layout: DatabaseLayoutPB, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; - database - .update_view_layout(view_id.to_string().as_str(), layout.into()) - .await + let database = self.get_database_with_view_id(view_id).await?; + database.update_view_layout(view_id, layout.into()).await } pub async fn get_database_snapshots( @@ -551,7 +393,7 @@ impl DatabaseManager { view_id: &str, limit: usize, ) -> FlowyResult<Vec<DatabaseSnapshotPB>> { - let database_id = Uuid::from_str(&self.get_database_id_with_view_id(view_id).await?)?; + let database_id = self.get_database_id_with_view_id(view_id).await?; let snapshots = self .cloud_service .get_database_collab_object_snapshots(&database_id, limit) @@ -568,24 +410,27 @@ impl DatabaseManager { Ok(snapshots) } - fn workspace_database(&self) -> FlowyResult<Arc<RwLock<WorkspaceDatabaseManager>>> { - self - .workspace_database_manager - .load_full() - .ok_or_else(|| FlowyError::internal().with_context("Workspace database not initialized")) + /// Return the database indexer. + /// Each workspace has itw own Database indexer that manages all the databases and database views + async fn get_database_indexer(&self) -> FlowyResult<Arc<WorkspaceDatabase>> { + let database = self.workspace_database.read().await; + match &*database { + None => Err(FlowyError::internal().with_context("Workspace database not initialized")), + Some(user_database) => Ok(user_database.clone()), + } } #[instrument(level = "debug", skip_all)] pub async fn summarize_row( &self, - view_id: &str, + view_id: String, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; + let database = self.get_database_with_view_id(&view_id).await?; let mut summary_row_content = SummaryRowContent::new(); - if let Some(row) = database.get_row(view_id, &row_id).await { - let fields = database.get_fields(view_id, None).await; + if let Some(row) = database.get_row(&view_id, &row_id) { + let fields = database.get_fields(&view_id, None); for field in fields { // When summarizing a row, skip the content in the "AI summary" cell; it does not need to // be summarized. @@ -607,18 +452,14 @@ impl DatabaseManager { summary_row_content ); let response = self - .ai_service - .summary_database_row( - &self.user.workspace_id()?, - &Uuid::from_str(&row_id)?, - summary_row_content, - ) + .cloud_service + .summary_database_row(&self.user.workspace_id()?, &row_id, summary_row_content) .await?; trace!("[AI]:summarize row response: {}", response); // Update the cell with the response from the cloud service. database - .update_cell_with_changeset(view_id, &row_id, &field_id, BoxAny::new(response)) + .update_cell_with_changeset(&view_id, &row_id, &field_id, BoxAny::new(response)) .await?; Ok(()) } @@ -626,17 +467,16 @@ impl DatabaseManager { #[instrument(level = "debug", skip_all)] pub async fn translate_row( &self, - view_id: &str, + view_id: String, row_id: RowId, field_id: String, ) -> FlowyResult<()> { - let database = self.get_database_editor_with_view_id(view_id).await?; - let view_id = view_id.to_string(); + let database = self.get_database_with_view_id(&view_id).await?; let mut translate_row_content = TranslateRowContent::new(); let mut language = "english".to_string(); - if let Some(row) = database.get_row(&view_id, &row_id).await { - let fields = database.get_fields(&view_id, None).await; + if let Some(row) = database.get_row(&view_id, &row_id) { + let fields = database.get_fields(&view_id, None); for field in fields { // When translate a row, skip the content in the "AI Translate" cell; it does not need to // be translated. @@ -673,7 +513,7 @@ impl DatabaseManager { translate_row_content ); let response = self - .ai_service + .cloud_service .translate_database_row(&self.user.workspace_id()?, translate_row_content, &language) .await?; @@ -706,501 +546,85 @@ impl DatabaseManager { } } -struct WorkspaceDatabaseCollabServiceImpl { - is_local_user: bool, +struct UserDatabaseCollabServiceImpl { user: Arc<dyn DatabaseUser>, collab_builder: Arc<AppFlowyCollabBuilder>, - persistence: Arc<dyn DatabaseCollabPersistenceService>, cloud_service: Arc<dyn DatabaseCloudService>, } -impl WorkspaceDatabaseCollabServiceImpl { - fn new( - is_local_user: bool, - user: Arc<dyn DatabaseUser>, - collab_builder: Arc<AppFlowyCollabBuilder>, - cloud_service: Arc<dyn DatabaseCloudService>, - ) -> Self { - let persistence = DatabasePersistenceImpl { user: user.clone() }; - Self { - is_local_user, - user, - collab_builder, - persistence: Arc::new(persistence), - cloud_service, - } - } - - async fn get_encode_collab( - &self, - object_id: &Uuid, - object_ty: CollabType, - ) -> Result<Option<EncodedCollab>, DatabaseError> { - let workspace_id = self - .user - .workspace_id() - .map_err(|e| DatabaseError::Internal(e.into()))?; - trace!("[Database]: fetch {}:{} from remote", object_id, object_ty); - let encode_collab = self - .cloud_service - .get_database_encode_collab(object_id, object_ty, &workspace_id) - .await - .map_err(|err| DatabaseError::Internal(err.into()))?; - Ok(encode_collab) - } - - async fn batch_get_encode_collab( - &self, - object_ids: Vec<Uuid>, - object_ty: CollabType, - ) -> Result<EncodeCollabByOid, DatabaseError> { - let workspace_id = self - .user - .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - let updates = self - .cloud_service - .batch_get_database_encode_collab(object_ids, object_ty, &workspace_id) - .await - .map_err(|err| DatabaseError::Internal(err.into()))?; - - Ok( - updates - .into_iter() - .map(|(k, v)| (k.to_string(), v)) - .collect(), - ) - } - - fn collab_db(&self) -> Result<Weak<CollabKVDB>, DatabaseError> { - let uid = self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - self - .user - .collab_db(uid) - .map_err(|err| DatabaseError::Internal(err.into())) - } - - fn build_collab_object( - &self, - object_id: &Uuid, - object_type: CollabType, - ) -> Result<CollabObject, DatabaseError> { - let uid = self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - let workspace_id = self - .user - .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - let object = self - .collab_builder - .collab_object(&workspace_id, uid, object_id, object_type) - .map_err(|err| DatabaseError::Internal(anyhow!("Failed to build collab object: {}", err)))?; - Ok(object) - } -} - -#[async_trait] -impl DatabaseCollabService for WorkspaceDatabaseCollabServiceImpl { - ///NOTE: this method doesn't initialize plugins, however it is passed into WorkspaceDatabase, - /// therefore all Database/DatabaseRow creation methods must initialize plugins thmselves. - #[instrument(level = "trace", skip_all)] - async fn build_collab( +impl DatabaseCollabService for UserDatabaseCollabServiceImpl { + fn get_collab_doc_state( &self, object_id: &str, - collab_type: CollabType, - encoded_collab: Option<(EncodedCollab, bool)>, - ) -> Result<Collab, DatabaseError> { - let object_id = Uuid::parse_str(object_id)?; - let object = self.build_collab_object(&object_id, collab_type)?; - let data_source = if self - .persistence - .is_collab_exist(object_id.to_string().as_str()) - { - trace!( - "build collab: {}:{} from local encode collab", - collab_type, - object_id - ); - CollabPersistenceImpl { - persistence: Some(self.persistence.clone()), + object_ty: CollabType, + ) -> CollabFuture<Result<DataSource, DatabaseError>> { + let workspace_id = self.user.workspace_id().unwrap(); + let object_id = object_id.to_string(); + let weak_cloud_service = Arc::downgrade(&self.cloud_service); + Box::pin(async move { + match weak_cloud_service.upgrade() { + None => Err(DatabaseError::Internal(anyhow!("Cloud service is dropped"))), + Some(cloud_service) => { + let doc_state = cloud_service + .get_database_object_doc_state(&object_id, object_ty, &workspace_id) + .await?; + match doc_state { + None => Ok(DataSource::Disk), + Some(doc_state) => Ok(DataSource::DocStateV1(doc_state)), + } + }, } - .into() - } else { - match encoded_collab { + }) + } + + fn batch_get_collab_update( + &self, + object_ids: Vec<String>, + object_ty: CollabType, + ) -> CollabFuture<Result<CollabDocStateByOid, DatabaseError>> { + let cloned_user = self.user.clone(); + let weak_cloud_service = Arc::downgrade(&self.cloud_service); + Box::pin(async move { + let workspace_id = cloned_user + .workspace_id() + .map_err(|err| DatabaseError::Internal(err.into()))?; + match weak_cloud_service.upgrade() { None => { - info!( - "build collab: fetch {}:{} from remote, is_new:{}", - collab_type, - object_id, - encoded_collab.is_none(), - ); - match self.get_encode_collab(&object_id, collab_type).await { - Ok(Some(encode_collab)) => { - info!( - "build collab: {}:{} with remote encode collab, {} bytes", - collab_type, - object_id, - encode_collab.doc_state.len() - ); - DataSource::from(encode_collab) - }, - Ok(None) => { - if self.is_local_user { - info!( - "build collab: {}:{} with empty encode collab", - collab_type, object_id - ); - CollabPersistenceImpl { - persistence: Some(self.persistence.clone()), - } - .into() - } else { - return Err(DatabaseError::RecordNotFound); - } - }, - Err(err) => { - if !matches!(err, DatabaseError::ActionCancelled) { - error!("build collab: failed to get encode collab: {}", err); - } - return Err(err); - }, - } + tracing::warn!("Cloud service is dropped"); + Ok(CollabDocStateByOid::default()) }, - Some((encoded_collab, _)) => { - info!( - "build collab: {}:{} with new encode collab, {} bytes", - collab_type, - object_id, - encoded_collab.doc_state.len() - ); - self - .persistence - .save_collab(object_id.to_string().as_str(), encoded_collab.clone())?; - - // TODO(nathan): cover database rows and other database collab type - if matches!(collab_type, CollabType::Database) { - if let Ok(workspace_id) = self.user.workspace_id() { - let cloned_encoded_collab = encoded_collab.clone(); - let cloud_service = self.cloud_service.clone(); - tokio::spawn(async move { - let _ = cloud_service - .create_database_encode_collab( - &object_id, - collab_type, - &workspace_id, - cloned_encoded_collab, - ) - .await; - }); - } - } - encoded_collab.into() + Some(cloud_service) => { + let updates = cloud_service + .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) + .await?; + Ok(updates) }, } - }; + }) + } - let collab_db = self.collab_db()?; - let collab = self - .collab_builder - .build_collab(&object, &collab_db, data_source) - .await?; + fn build_collab_with_config( + &self, + uid: i64, + object_id: &str, + object_type: CollabType, + collab_db: Weak<CollabKVDB>, + collab_raw_data: DataSource, + _persistence_config: CollabPersistenceConfig, + ) -> Result<Arc<MutexCollab>, DatabaseError> { + let workspace_id = self + .user + .workspace_id() + .map_err(|err| DatabaseError::Internal(err.into()))?; + let collab = self.collab_builder.build_with_config( + &workspace_id, + uid, + object_id, + object_type.clone(), + collab_db.clone(), + collab_raw_data, + CollabBuilderConfig::default().sync_enable(true), + )?; Ok(collab) } - - async fn get_collabs( - &self, - mut object_ids: Vec<String>, - collab_type: CollabType, - ) -> Result<EncodeCollabByOid, DatabaseError> { - if object_ids.is_empty() { - return Ok(EncodeCollabByOid::new()); - } - - let mut encoded_collab_by_id = EncodeCollabByOid::new(); - // 1. Collect local disk collabs into a HashMap - let local_disk_encoded_collab: HashMap<String, EncodedCollab> = object_ids - .par_iter() - .filter_map(|object_id| { - self - .persistence - .get_encoded_collab(object_id.as_str(), collab_type) - .map(|encoded_collab| (object_id.clone(), encoded_collab)) - }) - .collect(); - trace!( - "[Database]: load {} database row from local disk", - local_disk_encoded_collab.len() - ); - - object_ids.retain(|object_id| !local_disk_encoded_collab.contains_key(object_id)); - for (k, v) in local_disk_encoded_collab { - encoded_collab_by_id.insert(k, v); - } - - if !object_ids.is_empty() { - let object_ids = object_ids - .into_iter() - .flat_map(|v| Uuid::from_str(&v).ok()) - .collect::<Vec<_>>(); - // 2. Fetch remaining collabs from remote - let remote_collabs = self - .batch_get_encode_collab(object_ids, collab_type) - .await?; - - trace!( - "[Database]: load {} database row from remote", - remote_collabs.len() - ); - for (k, v) in remote_collabs { - encoded_collab_by_id.insert(k, v); - } - } - - Ok(encoded_collab_by_id) - } - - fn persistence(&self) -> Option<Arc<dyn DatabaseCollabPersistenceService>> { - Some(self.persistence.clone()) - } -} - -pub struct DatabasePersistenceImpl { - user: Arc<dyn DatabaseUser>, -} - -impl DatabasePersistenceImpl { - fn workspace_id(&self) -> Result<Uuid, DatabaseError> { - let workspace_id = self - .user - .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - Ok(workspace_id) - } -} - -impl DatabaseCollabPersistenceService for DatabasePersistenceImpl { - fn load_collab(&self, collab: &mut Collab) { - let result = self - .user - .user_id() - .map(|uid| (uid, self.user.collab_db(uid).map(|weak| weak.upgrade()))); - - if let Ok(workspace_id) = self.user.workspace_id() { - if let Ok((uid, Ok(Some(collab_db)))) = result { - let object_id = collab.object_id().to_string(); - let db_read = collab_db.read_txn(); - if !db_read.is_exist(uid, workspace_id.to_string().as_str(), &object_id) { - trace!( - "[Database]: collab:{} not exist in local storage", - object_id - ); - return; - } - - trace!("[Database]: start loading collab:{} from disk", object_id); - let mut txn = collab.transact_mut(); - match db_read.load_doc_with_txn( - uid, - workspace_id.to_string().as_str(), - &object_id, - &mut txn, - ) { - Ok(update_count) => { - trace!( - "[Database]: did load collab:{}, update_count:{}", - object_id, - update_count - ); - }, - Err(err) => { - if !err.is_record_not_found() { - error!("[Database]: load collab:{} failed:{}", object_id, err); - } - }, - } - } - } - } - - fn get_encoded_collab(&self, object_id: &str, collab_type: CollabType) -> Option<EncodedCollab> { - let workspace_id = self.user.workspace_id().ok()?.to_string(); - let uid = self.user.user_id().ok()?; - let db = self.user.collab_db(uid).ok()?.upgrade()?; - let read_txn = db.read_txn(); - if !read_txn.is_exist(uid, workspace_id.as_str(), object_id) { - return None; - } - - let mut collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - let mut txn = collab.transact_mut(); - let _ = read_txn.load_doc_with_txn(uid, workspace_id.as_str(), object_id, &mut txn); - drop(txn); - - collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .ok() - } - - fn delete_collab(&self, object_id: &str) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?.to_string(); - let uid = self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { - let write_txn = collab_db.write_txn(); - write_txn - .delete_doc(uid, workspace_id.as_str(), object_id) - .unwrap(); - write_txn - .commit_transaction() - .map_err(|err| DatabaseError::Internal(anyhow!("failed to commit transaction: {}", err)))?; - } - Ok(()) - } - - fn save_collab( - &self, - object_id: &str, - encoded_collab: EncodedCollab, - ) -> Result<(), DatabaseError> { - let workspace_id = self.workspace_id()?.to_string(); - let uid = self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { - let write_txn = collab_db.write_txn(); - write_txn - .flush_doc( - uid, - workspace_id.as_str(), - object_id, - encoded_collab.state_vector.to_vec(), - encoded_collab.doc_state.to_vec(), - ) - .map_err(|err| DatabaseError::Internal(anyhow!("failed to flush doc: {}", err)))?; - write_txn - .commit_transaction() - .map_err(|err| DatabaseError::Internal(anyhow!("failed to commit transaction: {}", err)))?; - } - Ok(()) - } - - fn is_collab_exist(&self, object_id: &str) -> bool { - match self.user.workspace_id() { - Ok(workspace_id) => { - match self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into())) - { - Ok(uid) => { - if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { - let read_txn = collab_db.read_txn(); - return read_txn.is_exist(uid, workspace_id.to_string().as_str(), object_id); - } - false - }, - Err(_) => false, - } - }, - Err(_) => false, - } - } - - fn flush_collabs( - &self, - encoded_collabs: Vec<(String, EncodedCollab)>, - ) -> Result<(), DatabaseError> { - let uid = self - .user - .user_id() - .map_err(|err| DatabaseError::Internal(err.into()))?; - let workspace_id = self - .user - .workspace_id() - .map_err(|err| DatabaseError::Internal(err.into()))? - .to_string(); - - if let Ok(Some(collab_db)) = self.user.collab_db(uid).map(|weak| weak.upgrade()) { - let write_txn = collab_db.write_txn(); - for (object_id, encode_collab) in encoded_collabs { - write_txn - .flush_doc( - uid, - &workspace_id, - &object_id, - encode_collab.state_vector.to_vec(), - encode_collab.doc_state.to_vec(), - ) - .map_err(|err| DatabaseError::Internal(anyhow!("failed to flush doc: {}", err)))?; - } - write_txn - .commit_transaction() - .map_err(|err| DatabaseError::Internal(anyhow!("failed to commit transaction: {}", err)))?; - } - Ok(()) - } -} -async fn open_database_with_retry( - workspace_database_manager: Arc<RwLock<WorkspaceDatabaseManager>>, - database_id: &str, -) -> Result<Arc<RwLock<Database>>, DatabaseError> { - let max_retries = 3; - let retry_interval = Duration::from_secs(4); - for attempt in 1..=max_retries { - trace!( - "[Database]: attempt {} to open database:{}", - attempt, - database_id - ); - - let result = workspace_database_manager - .try_read() - .map_err(|err| DatabaseError::Internal(anyhow!("workspace database lock fail: {}", err)))? - .get_or_init_database(database_id) - .await; - - // Attempt to open the database - match result { - Ok(database) => return Ok(database), - Err(err) => { - if matches!(err, DatabaseError::RecordNotFound) - || matches!(err, DatabaseError::NoRequiredData(_)) - { - error!( - "[Database]: retry {} to open database:{}, error:{}", - attempt, database_id, err - ); - - if attempt < max_retries { - tokio::time::sleep(retry_interval).await; - } else { - error!( - "[Database]: exhausted retries to open database:{}, error:{}", - database_id, err - ); - return Err(err); - } - } else { - error!( - "[Database]: stop retrying to open database:{}, error:{}", - database_id, err - ); - return Err(err); - } - }, - } - } - - Err(DatabaseError::Internal(anyhow!( - "Exhausted retries to open database: {}", - database_id - ))) } diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index 93e438452f..eadaa7e031 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -90,9 +90,6 @@ impl std::convert::From<i32> for DatabaseNotification { } #[tracing::instrument(level = "trace")] -pub fn database_notification_builder(id: &str, ty: DatabaseNotification) -> NotificationBuilder { - #[cfg(feature = "verbose_log")] - tracing::trace!("[Database Notification]: id:{}, ty:{:?}", id, ty); - +pub fn send_notification(id: &str, ty: DatabaseNotification) -> NotificationBuilder { NotificationBuilder::new(id, ty, DATABASE_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs index 4b6307b095..d406c88f04 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs @@ -1,5 +1,6 @@ +use parking_lot::RwLock; use std::sync::Arc; use crate::utils::cache::AnyTypeCache; -pub type CalculationsByFieldIdCache = Arc<AnyTypeCache<String>>; +pub type CalculationsByFieldIdCache = Arc<RwLock<AnyTypeCache<String>>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs index 4e114261f8..5e199b84ad 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs @@ -1,15 +1,14 @@ -use async_trait::async_trait; use std::str::FromStr; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row}; -use dashmap::DashMap; +use collab_database::rows::{Row, RowCell}; use flowy_error::FlowyResult; -use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock as TokioRwLock; -use tracing::{error, instrument, trace}; +use tokio::sync::RwLock; + +use lib_infra::future::Fut; +use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use crate::entities::{ CalculationChangesetNotificationPB, CalculationPB, CalculationType, FieldType, @@ -20,14 +19,13 @@ use crate::utils::cache::AnyTypeCache; use super::{Calculation, CalculationChangeset, CalculationsService}; -#[async_trait] pub trait CalculationsDelegate: Send + Sync + 'static { - async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec<Arc<Cell>>; - async fn get_field(&self, field_id: &str) -> Option<Field>; - async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Arc<Calculation>>; - async fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>>; - async fn update_calculation(&self, view_id: &str, calculation: Calculation); - async fn remove_calculation(&self, view_id: &str, calculation_id: &str); + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>; + fn get_field(&self, field_id: &str) -> Option<Field>; + fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut<Option<Arc<Calculation>>>; + fn get_all_calculations(&self, view_id: &str) -> Fut<Arc<Vec<Arc<Calculation>>>>; + fn update_calculation(&self, view_id: &str, calculation: Calculation); + fn remove_calculation(&self, view_id: &str, calculation_id: &str); } pub struct CalculationsController { @@ -35,7 +33,7 @@ pub struct CalculationsController { handler_id: String, delegate: Box<dyn CalculationsDelegate>, calculations_by_field_cache: CalculationsByFieldIdCache, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, calculations_service: CalculationsService, notifier: DatabaseViewChangedNotifier, } @@ -47,12 +45,12 @@ impl Drop for CalculationsController { } impl CalculationsController { - pub fn new<T>( + pub async fn new<T>( view_id: &str, handler_id: &str, delegate: T, calculations: Vec<Arc<Calculation>>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, notifier: DatabaseViewChangedNotifier, ) -> Self where @@ -67,26 +65,25 @@ impl CalculationsController { calculations_service: CalculationsService::new(), notifier, }; - this.update_cache(calculations); + this.update_cache(calculations).await; this } pub async fn close(&self) { - self - .task_scheduler - .write() - .await - .unregister_handler(&self.handler_id) - .await; + if let Ok(mut task_scheduler) = self.task_scheduler.try_write() { + task_scheduler.unregister_handler(&self.handler_id).await; + } else { + tracing::error!("Attempt to get the lock of task_scheduler failed"); + } } #[tracing::instrument(name = "schedule_calculation_task", level = "trace", skip(self))] - pub(crate) async fn gen_task(&self, task_type: CalculationEvent, qos: QualityOfService) { + async fn gen_task(&self, task_type: CalculationEvent, qos: QualityOfService) { let task_id = self.task_scheduler.read().await.next_task_id(); let task = Task::new( &self.handler_id, task_id, - TaskContent::Text(task_type.to_json_string()), + TaskContent::Text(task_type.to_string()), qos, ); self.task_scheduler.write().await.add_task(task); @@ -101,12 +98,8 @@ impl CalculationsController { )] pub async fn process(&self, predicate: &str) -> FlowyResult<()> { let event_type = CalculationEvent::from_str(predicate).unwrap(); - trace!( - "[Database Calculate] Processing calculation event: {:?}", - event_type - ); match event_type { - CalculationEvent::RowChanged(row) => self.handle_row_changed(&row).await, + CalculationEvent::RowChanged(row) => self.handle_row_changed(row).await, CalculationEvent::CellUpdated(field_id) => self.handle_cell_changed(field_id).await, CalculationEvent::FieldDeleted(field_id) => self.handle_field_deleted(field_id).await, CalculationEvent::FieldTypeChanged(field_id, new_field_type) => { @@ -123,7 +116,7 @@ impl CalculationsController { self .gen_task( CalculationEvent::FieldDeleted(field_id), - QualityOfService::Background, + QualityOfService::UserInteractive, ) .await } @@ -137,8 +130,7 @@ impl CalculationsController { if let Some(calculation) = calculation { self .delegate - .remove_calculation(&self.view_id, &calculation.id) - .await; + .remove_calculation(&self.view_id, &calculation.id); let notification = CalculationChangesetNotificationPB::from_delete( &self.view_id, @@ -157,7 +149,7 @@ impl CalculationsController { self .gen_task( CalculationEvent::FieldTypeChanged(field_id, new_field_type), - QualityOfService::Background, + QualityOfService::UserInteractive, ) .await } @@ -173,8 +165,7 @@ impl CalculationsController { if !calc_type.is_allowed(new_field_type) { self .delegate - .remove_calculation(&self.view_id, &calculation.id) - .await; + .remove_calculation(&self.view_id, &calculation.id); let notification = CalculationChangesetNotificationPB::from_delete( &self.view_id, @@ -194,7 +185,7 @@ impl CalculationsController { self .gen_task( CalculationEvent::CellUpdated(field_id), - QualityOfService::Background, + QualityOfService::UserInteractive, ) .await } @@ -206,37 +197,22 @@ impl CalculationsController { .await; if let Some(calculation) = calculation { - if let Some(field) = self.delegate.get_field(&field_id).await { - let cells = self + let update = self.get_updated_calculation(calculation).await; + if let Some(update) = update { + self .delegate - .get_cells_for_field(&self.view_id, &calculation.field_id) - .await; + .update_calculation(&self.view_id, update.clone()); - // Update the calculation - if let Some(update) = self - .update_calculation(calculation.as_ref(), &field, cells) - .await - { - self - .delegate - .update_calculation(&self.view_id, update.clone()) - .await; + let notification = CalculationChangesetNotificationPB::from_update( + &self.view_id, + vec![CalculationPB::from(&update)], + ); - // Send notification - let notification = CalculationChangesetNotificationPB::from_update( - &self.view_id, - vec![CalculationPB::from(&update)], - ); - - if let Err(err) = self - .notifier - .send(DatabaseViewChanged::CalculationValueNotification( - notification, - )) - { - error!("Failed to send calculation notification: {:?}", err); - } - } + let _ = self + .notifier + .send(DatabaseViewChanged::CalculationValueNotification( + notification, + )); } } } @@ -245,116 +221,64 @@ impl CalculationsController { self .gen_task( CalculationEvent::RowChanged(row), - QualityOfService::Background, + QualityOfService::UserInteractive, ) .await } - async fn handle_row_changed(&self, row: &Row) { - let cells = &row.cells; + async fn handle_row_changed(&self, row: Row) { + let cells = row.cells.iter(); let mut updates = vec![]; - let mut cells_by_field = DashMap::<String, Vec<Arc<Cell>>>::new(); // In case there are calculations where empty cells are counted // as a contribution to the value. - if cells.is_empty() { + if cells.len() == 0 { let calculations = self.delegate.get_all_calculations(&self.view_id).await; - for calculation in calculations.into_iter() { - if let Some(field) = self.delegate.get_field(&calculation.field_id).await { - let cells = self - .get_or_fetch_cells(&calculation.field_id, &mut cells_by_field) - .await; - updates.extend( - self - .handle_cells_changed(&field, calculation.as_ref(), cells) - .await, - ); + for calculation in calculations.iter() { + let update = self.get_updated_calculation(calculation.clone()).await; + if let Some(update) = update { + updates.push(CalculationPB::from(&update)); + self.delegate.update_calculation(&self.view_id, update); } } } // Iterate each cell in the row for cell in cells { - let field_id = &cell.0; + let field_id = cell.0; let calculation = self.delegate.get_calculation(&self.view_id, field_id).await; if let Some(calculation) = calculation { - let cells = self - .get_or_fetch_cells(&calculation.field_id, &mut cells_by_field) - .await; + let update = self.get_updated_calculation(calculation.clone()).await; - if let Some(field) = self.delegate.get_field(field_id).await { - let changes = self - .handle_cells_changed(&field, calculation.as_ref(), cells) - .await; - updates.extend(changes); + if let Some(update) = update { + updates.push(CalculationPB::from(&update)); + self.delegate.update_calculation(&self.view_id, update); } } } if !updates.is_empty() { let notification = CalculationChangesetNotificationPB::from_update(&self.view_id, updates); - if let Err(err) = self + + let _ = self .notifier .send(DatabaseViewChanged::CalculationValueNotification( notification, - )) - { - error!("Failed to send calculation notification: {:?}", err); - } + )); } } - async fn get_or_fetch_cells<'a>( - &'a self, - field_id: &'a str, - cells_by_field: &'a mut DashMap<String, Vec<Arc<Cell>>>, - ) -> Vec<Arc<Cell>> { - let cells = cells_by_field.get(field_id).map(|entry| entry.to_vec()); - match cells { - None => { - let fetch_cells = self - .delegate - .get_cells_for_field(&self.view_id, field_id) - .await; - cells_by_field.insert(field_id.to_string(), fetch_cells.clone()); - fetch_cells - }, - Some(cells) => cells, - } - } - - /// field_cells will be the cells that belong to the field with field_id - pub async fn handle_cells_changed( - &self, - field: &Field, - calculation: &Calculation, - field_cells: Vec<Arc<Cell>>, - ) -> Vec<CalculationPB> { - let mut updates = vec![]; - let update = self - .update_calculation(calculation, field, field_cells) + async fn get_updated_calculation(&self, calculation: Arc<Calculation>) -> Option<Calculation> { + let field_cells = self + .delegate + .get_cells_for_field(&self.view_id, &calculation.field_id) .await; - if let Some(update) = update { - updates.push(CalculationPB::from(&update)); + let field = self.delegate.get_field(&calculation.field_id)?; + + let value = self - .delegate - .update_calculation(&self.view_id, update) - .await; - } - - updates - } - - #[instrument(level = "trace", skip_all)] - async fn update_calculation( - &self, - calculation: &Calculation, - field: &Field, - cells: Vec<Arc<Cell>>, - ) -> Option<Calculation> { - let value = self - .calculations_service - .calculate(field, calculation.calculation_type, cells); + .calculations_service + .calculate(&field, calculation.calculation_type, field_cells); if value != calculation.value { return Some(calculation.with_value(value)); @@ -370,16 +294,16 @@ impl CalculationsController { let mut notification: Option<CalculationChangesetNotificationPB> = None; if let Some(insert) = &changeset.insert_calculation { - let cells = self + let row_cells: Vec<Arc<RowCell>> = self .delegate .get_cells_for_field(&self.view_id, &insert.field_id) .await; - let field = self.delegate.get_field(&insert.field_id).await?; + let field = self.delegate.get_field(&insert.field_id)?; let value = self .calculations_service - .calculate(&field, insert.calculation_type, cells); + .calculate(&field, insert.calculation_type, row_cells); notification = Some(CalculationChangesetNotificationPB::from_insert( &self.view_id, @@ -407,26 +331,27 @@ impl CalculationsController { notification } - fn update_cache(&self, calculations: Vec<Arc<Calculation>>) { + async fn update_cache(&self, calculations: Vec<Arc<Calculation>>) { for calculation in calculations { let field_id = &calculation.field_id; self .calculations_by_field_cache + .write() .insert(field_id, calculation.clone()); } } } #[derive(Serialize, Deserialize, Clone, Debug)] -pub(crate) enum CalculationEvent { +enum CalculationEvent { RowChanged(Row), CellUpdated(String), FieldTypeChanged(String, FieldType), FieldDeleted(String), } -impl CalculationEvent { - fn to_json_string(&self) -> String { +impl ToString for CalculationEvent { + fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs index 34688b7367..f4502020ac 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs @@ -1,15 +1,14 @@ -use collab::preclude::encoding::serde::from_any; -use collab::preclude::Any; +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; use collab_database::views::{CalculationMap, CalculationMapBuilder}; -use serde::Deserialize; -#[derive(Debug, Clone, Deserialize)] +use crate::entities::CalculationPB; + +#[derive(Debug, Clone)] pub struct Calculation { pub id: String, pub field_id: String, - #[serde(default, rename = "ty")] pub calculation_type: i64, - #[serde(default, rename = "calculation_value")] pub value: String, } @@ -20,12 +19,25 @@ const CALCULATION_VALUE: &str = "calculation_value"; impl From<Calculation> for CalculationMap { fn from(data: Calculation) -> Self { - CalculationMapBuilder::from([ - (CALCULATION_ID.into(), data.id.into()), - (FIELD_ID.into(), data.field_id.into()), - (CALCULATION_TYPE.into(), Any::BigInt(data.calculation_type)), - (CALCULATION_VALUE.into(), data.value.into()), - ]) + CalculationMapBuilder::new() + .insert_str_value(CALCULATION_ID, data.id) + .insert_str_value(FIELD_ID, data.field_id) + .insert_i64_value(CALCULATION_TYPE, data.calculation_type) + .insert_str_value(CALCULATION_VALUE, data.value) + .build() + } +} + +impl std::convert::From<&CalculationPB> for Calculation { + fn from(calculation: &CalculationPB) -> Self { + let calculation_type = calculation.calculation_type.into(); + + Self { + id: calculation.id.clone(), + field_id: calculation.field_id.clone(), + calculation_type, + value: calculation.value.clone(), + } } } @@ -33,7 +45,29 @@ impl TryFrom<CalculationMap> for Calculation { type Error = anyhow::Error; fn try_from(calculation: CalculationMap) -> Result<Self, Self::Error> { - from_any(&Any::from(calculation)).map_err(|e| e.into()) + match ( + calculation.get_str_value(CALCULATION_ID), + calculation.get_str_value(FIELD_ID), + ) { + (Some(id), Some(field_id)) => { + let value = calculation + .get_str_value(CALCULATION_VALUE) + .unwrap_or_default(); + let calculation_type = calculation + .get_i64_value(CALCULATION_TYPE) + .unwrap_or_default(); + + Ok(Calculation { + id, + field_id, + calculation_type, + value, + }) + }, + _ => { + bail!("Invalid calculation data") + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs index 43feeb6f19..bf48136622 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs @@ -1,133 +1,74 @@ use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::Cell; +use collab_database::rows::RowCell; use crate::entities::CalculationType; use crate::services::field::TypeOptionCellExt; -use rayon::prelude::*; -pub struct CalculationsService; +pub struct CalculationsService {} + impl CalculationsService { pub fn new() -> Self { - Self + Self {} } - pub fn calculate(&self, field: &Field, calculation_type: i64, cells: Vec<Arc<Cell>>) -> String { + pub fn calculate( + &self, + field: &Field, + calculation_type: i64, + row_cells: Vec<Arc<RowCell>>, + ) -> String { let ty: CalculationType = calculation_type.into(); match ty { - CalculationType::Average => self.calculate_average(field, cells), - CalculationType::Max => self.calculate_max(field, cells), - CalculationType::Median => self.calculate_median(field, cells), - CalculationType::Min => self.calculate_min(field, cells), - CalculationType::Sum => self.calculate_sum(field, cells), - CalculationType::Count => self.calculate_count(cells), - CalculationType::CountEmpty => self.calculate_count_empty(field, cells), - CalculationType::CountNonEmpty => self.calculate_count_non_empty(field, cells), + CalculationType::Average => self.calculate_average(field, row_cells), + CalculationType::Max => self.calculate_max(field, row_cells), + CalculationType::Median => self.calculate_median(field, row_cells), + CalculationType::Min => self.calculate_min(field, row_cells), + CalculationType::Sum => self.calculate_sum(field, row_cells), + CalculationType::Count => self.calculate_count(row_cells), + CalculationType::CountEmpty => self.calculate_count_empty(field, row_cells), + CalculationType::CountNonEmpty => self.calculate_count_non_empty(field, row_cells), } } - fn calculate_average(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { + fn calculate_average(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + let mut sum = 0.0; + let mut len = 0.0; if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { - let (sum, len): (f64, usize) = cells - .par_iter() - .filter_map(|cell| handler.handle_numeric_cell(cell)) - .map(|value| (value, 1)) - .reduce( - || (0.0, 0), - |(sum1, len1), (sum2, len2)| (sum1 + sum2, len1 + len2), - ); - - if len > 0 { - format!("{:.2}", sum / len as f64) - } else { - String::new() + for row_cell in row_cells { + if let Some(cell) = &row_cell.cell { + if let Some(value) = handler.handle_numeric_cell(cell) { + sum += value; + len += 1.0; + } + } } + } + + if len > 0.0 { + format!("{:.5}", sum / len) } else { String::new() } } - fn calculate_median(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - let mut values = self.reduce_values_f64(field, cells); - values.par_sort_by(|a, b| a.partial_cmp(b).unwrap()); + fn calculate_median(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); if !values.is_empty() { - format!("{:.2}", Self::median(&values)) + format!("{:.5}", Self::median(&values)) } else { String::new() } } - fn calculate_min(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - let values = self.reduce_values_f64(field, cells); - if let Some(min) = values.par_iter().min_by(|a, b| a.total_cmp(b)) { - format!("{:.2}", min) - } else { - String::new() - } - } - - fn calculate_max(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - let values = self.reduce_values_f64(field, cells); - if let Some(max) = values.par_iter().max_by(|a, b| a.total_cmp(b)) { - format!("{:.2}", max) - } else { - String::new() - } - } - - fn calculate_sum(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - let values = self.reduce_values_f64(field, cells); - if !values.is_empty() { - format!("{:.2}", values.par_iter().sum::<f64>()) - } else { - String::new() - } - } - - fn calculate_count(&self, cells: Vec<Arc<Cell>>) -> String { - format!("{}", cells.len()) - } - - fn calculate_count_empty(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { - let empty_count = cells - .par_iter() - .filter(|cell| handler.handle_is_empty(cell, field)) - .count(); - empty_count.to_string() - } else { - "".to_string() - } - } - - fn calculate_count_non_empty(&self, field: &Field, cells: Vec<Arc<Cell>>) -> String { - if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { - let non_empty_count = cells - .par_iter() - .filter(|cell| !handler.handle_is_empty(cell, field)) - .count(); - non_empty_count.to_string() - } else { - "".to_string() - } - } - - fn reduce_values_f64(&self, field: &Field, row_cells: Vec<Arc<Cell>>) -> Vec<f64> { - if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { - row_cells - .par_iter() - .filter_map(|cell| handler.handle_numeric_cell(cell)) - .collect::<Vec<_>>() - } else { - vec![] - } - } - fn median(array: &[f64]) -> f64 { - if array.len() % 2 == 0 { + if (array.len() % 2) == 0 { let left = array.len() / 2 - 1; let right = array.len() / 2; (array[left] + array[right]) / 2.0 @@ -135,4 +76,109 @@ impl CalculationsService { array[array.len() / 2] } } + + fn calculate_min(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); + + if !values.is_empty() { + let min = values.iter().min_by(|a, b| a.total_cmp(b)); + if let Some(min) = min { + return format!("{:.5}", min); + } + } + + String::new() + } + + fn calculate_max(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); + + if !values.is_empty() { + let max = values.iter().max_by(|a, b| a.total_cmp(b)); + if let Some(max) = max { + return format!("{:.5}", max); + } + } + + String::new() + } + + fn calculate_sum(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| values.clone()); + + if !values.is_empty() { + format!("{:.5}", values.iter().sum::<f64>()) + } else { + String::new() + } + } + + fn calculate_count(&self, row_cells: Vec<Arc<RowCell>>) -> String { + if !row_cells.is_empty() { + format!("{}", row_cells.len()) + } else { + String::new() + } + } + + fn calculate_count_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { + Some(handler) if !row_cells.is_empty() => row_cells + .iter() + .filter(|row_cell| { + if let Some(cell) = &row_cell.cell { + handler.handle_is_cell_empty(cell, field) + } else { + true + } + }) + .collect::<Vec<_>>() + .len() + .to_string(), + _ => "".to_string(), + } + } + + fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec<Arc<RowCell>>) -> String { + match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { + Some(handler) if !row_cells.is_empty() => row_cells + .iter() + .filter(|row_cell| { + if let Some(cell) = &row_cell.cell { + !handler.handle_is_cell_empty(cell, field) + } else { + false + } + }) + .collect::<Vec<_>>() + .len() + .to_string(), + _ => "".to_string(), + } + } + + fn reduce_values_f64<F, T>(&self, field: &Field, row_cells: Vec<Arc<RowCell>>, f: F) -> T + where + F: FnOnce(&mut Vec<f64>) -> T, + { + let mut values = vec![]; + + if let Some(handler) = TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler() { + for row_cell in row_cells { + if let Some(cell) = &row_cell.cell { + if let Some(value) = handler.handle_numeric_cell(cell) { + values.push(value); + } + } + } + } + + f(&mut values) + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs index 073e35c143..b8ae249c4b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs @@ -1,9 +1,9 @@ -use crate::services::calculations::CalculationsController; -use async_trait::async_trait; - +use lib_infra::future::BoxResultFuture; use lib_infra::priority_task::{TaskContent, TaskHandler}; use std::sync::Arc; +use crate::services::calculations::CalculationsController; + pub struct CalculationsTaskHandler { handler_id: String, calculations_controller: Arc<CalculationsController>, @@ -18,7 +18,6 @@ impl CalculationsTaskHandler { } } -#[async_trait] impl TaskHandler for CalculationsTaskHandler { fn handler_id(&self) -> &str { &self.handler_id @@ -28,14 +27,16 @@ impl TaskHandler for CalculationsTaskHandler { "CalculationsTaskHandler" } - async fn run(&self, content: TaskContent) -> Result<(), anyhow::Error> { + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { let calculations_controller = self.calculations_controller.clone(); - if let TaskContent::Text(predicate) = content { - calculations_controller - .process(&predicate) - .await - .map_err(anyhow::Error::from)?; - } - Ok(()) + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + calculations_controller + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs index b7606fedbd..07864351d4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs @@ -1,5 +1,6 @@ +use parking_lot::RwLock; use std::sync::Arc; use crate::utils::cache::AnyTypeCache; -pub type CellCache = Arc<AnyTypeCache<u64>>; +pub type CellCache = Arc<RwLock<AnyTypeCache<u64>>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 5009c674d0..d1bae644ea 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -1,21 +1,14 @@ use std::collections::HashMap; use std::str::FromStr; -use collab_database::fields::media_type_option::MediaCellData; -use collab_database::fields::select_type_option::SelectOptionIds; use collab_database::fields::Field; use collab_database::rows::{get_field_type_from_cell, Cell, Cells}; -use collab_database::template::relation_parse::RelationCellData; + use flowy_error::{FlowyError, FlowyResult}; use lib_infra::box_any::BoxAny; -use tracing::trace; use crate::entities::{CheckboxCellDataPB, FieldType}; use crate::services::cell::{CellCache, CellProtobufBlob}; -use crate::services::field::checklist_filter::{ - ChecklistCellChangeset, ChecklistCellInsertChangeset, -}; -use crate::services::field::date_filter::DateCellChangeset; use crate::services::field::*; use crate::services::group::make_no_status_group; @@ -29,9 +22,7 @@ pub trait CellDataDecoder: TypeOption { /// /// * `cell`: the cell to be decoded /// - fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { - Ok(Self::CellData::from(cell)) - } + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>; /// Decodes the [Cell] that is of a particular field type into a `CellData` of this `TypeOption`'s field type. /// @@ -55,6 +46,12 @@ pub trait CellDataDecoder: TypeOption { /// separated by a comma. /// fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String; + + /// Decode the cell into f64 + /// Different field type has different way to decode the cell data into f64 + /// If the field type doesn't support to decode the cell data into f64, it will return None + /// + fn numeric_cell(&self, cell: &Cell) -> Option<f64>; } pub trait CellDataChangeset: TypeOption { @@ -173,13 +170,13 @@ pub fn insert_checkbox_cell(is_checked: bool, field: &Field) -> Cell { pub fn insert_date_cell( timestamp: i64, - end_timestamp: Option<i64>, + time: Option<String>, include_time: Option<bool>, field: &Field, ) -> Cell { let cell_data = DateCellChangeset { - timestamp: Some(timestamp), - end_timestamp, + date: Some(timestamp), + time, include_time, ..Default::default() }; @@ -191,12 +188,9 @@ pub fn insert_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell apply_cell_changeset(BoxAny::new(changeset), None, field, None).unwrap() } -pub fn insert_checklist_cell( - insert_options: Vec<ChecklistCellInsertChangeset>, - field: &Field, -) -> Cell { +pub fn insert_checklist_cell(insert_options: Vec<(String, bool)>, field: &Field) -> Cell { let changeset = ChecklistCellChangeset { - insert_tasks: insert_options, + insert_options, ..Default::default() }; apply_cell_changeset(BoxAny::new(changeset), None, field, None).unwrap() @@ -224,9 +218,8 @@ impl<'a> CellBuilder<'a> { for (field_id, cell_str) in cell_by_field_id { if let Some(field) = field_maps.get(&field_id) { let field_type = FieldType::from(field.field_type); - trace!("Field type: {:?}, cell_str: {}", field_type, cell_str); match field_type { - FieldType::RichText | FieldType::Translate | FieldType::Summary => { + FieldType::RichText => { cells.insert(field_id, insert_text_cell(cell_str, field)); }, FieldType::Number | FieldType::Time => { @@ -264,14 +257,13 @@ impl<'a> CellBuilder<'a> { } }, FieldType::Relation => { - if let Ok(cell_data) = RelationCellData::from_str(&cell_str) { - cells.insert(field_id, cell_data.into()); - } + cells.insert(field_id, (&RelationCellData::from(cell_str)).into()); }, - FieldType::Media => { - if let Ok(cell_data) = MediaCellData::from_str(&cell_str) { - cells.insert(field_id, cell_data.into()); - } + FieldType::Summary => { + cells.insert(field_id, insert_text_cell(cell_str, field)); + }, + FieldType::Translate => { + cells.insert(field_id, insert_text_cell(cell_str, field)); }, } } @@ -285,7 +277,7 @@ impl<'a> CellBuilder<'a> { } pub fn insert_text_cell(&mut self, field_id: &str, data: String) { - match self.field_maps.get(field_id) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the text field with id: {}", field_id), Some(field) => { self @@ -296,7 +288,7 @@ impl<'a> CellBuilder<'a> { } pub fn insert_url_cell(&mut self, field_id: &str, data: String) { - match self.field_maps.get(field_id) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the url field with id: {}", field_id), Some(field) => { self @@ -307,7 +299,7 @@ impl<'a> CellBuilder<'a> { } pub fn insert_number_cell(&mut self, field_id: &str, num: i64) { - match self.field_maps.get(field_id) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the number field with id: {}", field_id), Some(field) => { self @@ -318,7 +310,7 @@ impl<'a> CellBuilder<'a> { } pub fn insert_checkbox_cell(&mut self, field_id: &str, is_checked: bool) { - match self.field_maps.get(field_id) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the checkbox field with id: {}", field_id), Some(field) => { self @@ -328,20 +320,26 @@ impl<'a> CellBuilder<'a> { } } - pub fn insert_date_cell(&mut self, field_id: &str, timestamp: i64, include_time: Option<bool>) { - match self.field_maps.get(field_id) { + pub fn insert_date_cell( + &mut self, + field_id: &str, + timestamp: i64, + time: Option<String>, + include_time: Option<bool>, + ) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the date field with id: {}", field_id), Some(field) => { self.cells.insert( field_id.to_owned(), - insert_date_cell(timestamp, None, include_time, field), + insert_date_cell(timestamp, time, include_time, field), ); }, } } pub fn insert_select_option_cell(&mut self, field_id: &str, option_ids: Vec<String>) { - match self.field_maps.get(field_id) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the select option field with id: {}", field_id), Some(field) => { self.cells.insert( @@ -351,17 +349,13 @@ impl<'a> CellBuilder<'a> { }, } } - pub fn insert_checklist_cell( - &mut self, - field_id: &str, - new_tasks: Vec<ChecklistCellInsertChangeset>, - ) { - match self.field_maps.get(field_id) { + pub fn insert_checklist_cell(&mut self, field_id: &str, options: Vec<(String, bool)>) { + match self.field_maps.get(&field_id.to_owned()) { None => tracing::warn!("Can't find the field with id: {}", field_id), Some(field) => { self .cells - .insert(field_id.to_owned(), insert_checklist_cell(new_tasks, field)); + .insert(field_id.to_owned(), insert_checklist_cell(options, field)); }, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs index 09bd13793a..ccc877059a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs @@ -1,5 +1,4 @@ use bytes::Bytes; -use std::fmt::Display; use flowy_error::{internal_error, FlowyResult}; @@ -65,10 +64,15 @@ impl CellProtobufBlob { // } } -impl Display for CellProtobufBlob { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = String::from_utf8(self.0.to_vec()).unwrap_or_default(); - write!(f, "{}", s) +impl ToString for CellProtobufBlob { + fn to_string(&self) -> String { + match String::from_utf8(self.0.to_vec()) { + Ok(s) => s, + Err(e) => { + tracing::error!("DecodedCellData to string failed: {:?}", e); + "".to_string() + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 227b96df4f..48f16f20bd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1,5 +1,5 @@ use crate::entities::*; -use crate::notification::{database_notification_builder, DatabaseNotification}; +use crate::notification::{send_notification, DatabaseNotification}; use crate::services::calculations::Calculation; use crate::services::cell::{apply_cell_changeset, get_cell_protobuf, CellCache}; use crate::services::database::database_observe::*; @@ -7,97 +7,60 @@ use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database_view::{ DatabaseViewChanged, DatabaseViewEditor, DatabaseViewOperation, DatabaseViews, EditorByViewId, }; -use crate::services::field::checklist_filter::ChecklistCellChangeset; -use crate::services::field::type_option_transform::transform_type_option; use crate::services::field::{ - default_type_option_data_from_type, select_type_option_from_field, type_option_data_from_pb, - SelectOptionCellChangeset, StringCellData, TypeOptionCellDataHandler, TypeOptionCellExt, + default_type_option_data_from_type, select_type_option_from_field, transform_type_option, + type_option_data_from_pb, ChecklistCellChangeset, RelationTypeOption, SelectOptionCellChangeset, + StringCellData, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellDataHandler, + TypeOptionCellExt, }; use crate::services::field_settings::{default_field_settings_by_layout_map, FieldSettings}; use crate::services::filter::{Filter, FilterChangeset}; -use crate::services::group::{default_group_setting, GroupChangeset, GroupSetting}; +use crate::services::group::{default_group_setting, GroupChangeset, GroupSetting, RowChangeset}; use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; use crate::utils::cache::AnyTypeCache; -use crate::DatabaseUser; -use arc_swap::ArcSwapOption; -use async_trait::async_trait; -use collab::core::collab_plugin::CollabPluginType; -use collab::lock::RwLock; -use collab_database::database::Database; -use collab_database::entity::DatabaseView; -use collab_database::fields::media_type_option::MediaCellData; -use collab_database::fields::relation_type_option::RelationTypeOption; +use collab_database::database::MutexDatabase; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, DatabaseRow, Row, RowCell, RowDetail, RowId, RowUpdate}; -use collab_database::template::timestamp_parse::TimestampCellData; +use collab_database::rows::{Cell, Cells, Row, RowCell, RowDetail, RowId}; use collab_database::views::{ - DatabaseLayout, FilterMap, LayoutSetting, OrderObjectPosition, RowOrder, + DatabaseLayout, DatabaseView, FilterMap, LayoutSetting, OrderObjectPosition, }; -use collab_entity::CollabType; -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; use flowy_notification::DebounceNotificationSender; -use futures::future::join_all; -use futures::{pin_mut, StreamExt}; use lib_infra::box_any::BoxAny; +use lib_infra::future::{to_fut, Fut, FutureResult}; use lib_infra::priority_task::TaskDispatcher; use lib_infra::util::timestamp; use std::collections::HashMap; -use std::str::FromStr; -use std::sync::{Arc, Weak}; -use std::time::Duration; -use tokio::select; -use tokio::sync::oneshot::Sender; -use tokio::sync::RwLock as TokioRwLock; -use tokio::sync::{broadcast, oneshot}; -use tokio::task::yield_now; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, event, info, instrument, trace, warn}; -use uuid::Uuid; - -type OpenDatabaseResult = oneshot::Sender<FlowyResult<DatabasePB>>; +use std::sync::Arc; +use tokio::sync::{broadcast, RwLock}; +use tracing::{event, instrument, warn}; +#[derive(Clone)] pub struct DatabaseEditor { - database_id: Uuid, - pub(crate) database: Arc<RwLock<Database>>, + database: Arc<MutexDatabase>, pub cell_cache: CellCache, - pub(crate) database_views: Arc<DatabaseViews>, + database_views: Arc<DatabaseViews>, #[allow(dead_code)] - user: Arc<dyn DatabaseUser>, - collab_builder: Arc<AppFlowyCollabBuilder>, - is_loading_rows: ArcSwapOption<broadcast::Sender<()>>, - opening_ret_txs: Arc<RwLock<Vec<OpenDatabaseResult>>>, - #[allow(dead_code)] - database_cancellation: Arc<RwLock<Option<CancellationToken>>>, - un_finalized_rows_cancellation: Arc<ArcSwapOption<CancellationToken>>, - finalized_rows: Arc<moka::future::Cache<String, Weak<RwLock<DatabaseRow>>>>, + /// Used to send notification to the frontend. + notification_sender: Arc<DebounceNotificationSender>, } impl DatabaseEditor { pub async fn new( - user: Arc<dyn DatabaseUser>, - database: Arc<RwLock<Database>>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, - collab_builder: Arc<AppFlowyCollabBuilder>, - ) -> FlowyResult<Arc<Self>> { - let finalized_rows: moka::future::Cache<String, Weak<RwLock<DatabaseRow>>> = - moka::future::Cache::builder() - .max_capacity(50) - .async_eviction_listener(|key, value, _| { - Box::pin(async move { - database_row_evict_listener(key, value).await; - }) - }) - .build(); + database: Arc<MutexDatabase>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, + ) -> FlowyResult<Self> { let notification_sender = Arc::new(DebounceNotificationSender::new(200)); let cell_cache = AnyTypeCache::<u64>::new(); - let database_id = database.read().await.get_database_id(); - let database_cancellation = Arc::new(RwLock::new(None)); + let database_id = database.lock().get_database_id(); + // Receive database sync state and send to frontend via the notification observe_sync_state(&database_id, &database).await; + // observe_view_change(&database_id, &database).await; // observe_field_change(&database_id, &database).await; observe_rows_change(&database_id, &database, ¬ification_sender).await; + // observe_block_event(&database_id, &database).await; // Used to cache the view of the database for fast access. let editor_by_view_id = Arc::new(RwLock::new(EditorByViewId::default())); @@ -106,7 +69,6 @@ impl DatabaseEditor { task_scheduler: task_scheduler.clone(), cell_cache: cell_cache.clone(), editor_by_view_id: editor_by_view_id.clone(), - database_cancellation: database_cancellation.clone(), }); let database_views = Arc::new( @@ -119,54 +81,19 @@ impl DatabaseEditor { .await?, ); - let database_id = Uuid::from_str(&database_id)?; - let collab_object = collab_builder.collab_object( - &user.workspace_id()?, - user.user_id()?, - &database_id, - CollabType::Database, - )?; - - let database = collab_builder.finalize( - collab_object, - CollabBuilderConfig::default(), - database.clone(), - )?; - let this = Arc::new(Self { - database_id, - user, + Ok(Self { database, cell_cache, database_views, - collab_builder, - is_loading_rows: Default::default(), - opening_ret_txs: Arc::new(Default::default()), - database_cancellation, - un_finalized_rows_cancellation: Arc::new(Default::default()), - finalized_rows: Arc::new(finalized_rows), - }); - observe_block_event(&database_id, &this).await; - observe_view_change(&database_id, &this).await; - Ok(this) + notification_sender, + }) } pub async fn close_view(&self, view_id: &str) { - self.database_views.remove_view(view_id).await; + self.database_views.close_view(view_id).await; } - pub async fn get_row_ids(&self) -> Vec<RowId> { - self - .database - .read() - .await - .get_all_row_orders() - .await - .into_iter() - .map(|entry| entry.id) - .collect() - } - - pub async fn num_of_opening_views(&self) -> usize { + pub async fn num_views(&self) -> usize { self.database_views.num_editors().await } @@ -178,11 +105,7 @@ impl DatabaseEditor { } pub async fn get_layout_type(&self, view_id: &str) -> DatabaseLayout { - let view = self - .database_views - .get_or_init_view_editor(view_id) - .await - .ok(); + let view = self.database_views.get_view_editor(view_id).await.ok(); if let Some(editor) = view { editor.v_get_layout_type().await } else { @@ -195,7 +118,7 @@ impl DatabaseEditor { view_id: &str, layout_type: DatabaseLayout, ) -> FlowyResult<()> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_update_layout_type(layout_type).await?; Ok(()) @@ -205,12 +128,12 @@ impl DatabaseEditor { &self, view_id: &str, ) -> FlowyResult<broadcast::Receiver<DatabaseViewChanged>> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; Ok(view_editor.notifier.subscribe()) } - pub async fn get_field(&self, field_id: &str) -> Option<Field> { - self.database.read().await.get_field(field_id) + pub fn get_field(&self, field_id: &str) -> Option<Field> { + self.database.lock().fields.get_field(field_id) } pub async fn set_group_by_field( @@ -222,15 +145,15 @@ impl DatabaseEditor { let old_group_settings: Vec<GroupSetting>; let mut setting_content = "".to_string(); { - let mut database = self.database.write().await; - let field = database.get_field(field_id); + let database = self.database.lock(); + let field = database.fields.get_field(field_id); old_group_settings = database.get_all_group_setting(view_id); if let Some(field) = field { let field_type = FieldType::from(field.field_type); setting_content = group_config_pb_to_json_str(data, &field_type)?; let mut group_setting = default_group_setting(&field); - group_setting.content.clone_from(&setting_content); - database.update_database_view(view_id, |view| { + group_setting.content = setting_content.clone(); + database.views.update_database_view(view_id, |view| { view.set_groups(vec![group_setting.into()]); }); } @@ -240,7 +163,7 @@ impl DatabaseEditor { let has_same_content = old_group_setting.is_some() && old_group_setting.unwrap().content == setting_content; - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; if !view_editor.is_grouping_field(field_id).await || !has_same_content { view_editor.v_initialize_new_group(field_id).await?; } @@ -248,14 +171,12 @@ impl DatabaseEditor { } pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> { - let view_editor = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; let changes = view_editor.v_delete_group(¶ms.group_id).await?; + if !changes.is_empty() { for view in self.database_views.editors().await { - database_notification_builder(&view.view_id, DatabaseNotification::DidUpdateRow) + send_notification(&view.view_id, DatabaseNotification::DidUpdateRow) .payload(changes.clone()) .send(); } @@ -269,7 +190,7 @@ impl DatabaseEditor { /// will be the reference view ids and the inline view id. Otherwise, the return value will /// be the view id. pub async fn delete_database_view(&self, view_id: &str) -> FlowyResult<Vec<String>> { - Ok(self.database.write().await.delete_view(view_id)) + Ok(self.database.lock().delete_view(view_id)) } pub async fn update_group( @@ -277,7 +198,7 @@ impl DatabaseEditor { view_id: &str, changesets: Vec<GroupChangeset>, ) -> FlowyResult<()> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_update_group(changesets).await?; Ok(()) } @@ -287,40 +208,31 @@ impl DatabaseEditor { view_id: &str, changeset: FilterChangeset, ) -> FlowyResult<()> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_modify_filters(changeset).await?; Ok(()) } pub async fn create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult<Sort> { - let view_editor = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; let sort = view_editor.v_create_or_update_sort(params).await?; Ok(sort) } pub async fn reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> { - let view_editor = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; view_editor.v_reorder_sort(params).await?; Ok(()) } pub async fn delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { - let view_editor = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; view_editor.v_delete_sort(params).await?; Ok(()) } pub async fn get_all_calculations(&self, view_id: &str) -> RepeatedCalculationsPB { - if let Ok(view_editor) = self.database_views.get_or_init_view_editor(view_id).await { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { view_editor.v_get_all_calculations().await.into() } else { RepeatedCalculationsPB { items: vec![] } @@ -328,25 +240,19 @@ impl DatabaseEditor { } pub async fn update_calculation(&self, update: UpdateCalculationChangesetPB) -> FlowyResult<()> { - let view_editor = self - .database_views - .get_or_init_view_editor(&update.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(&update.view_id).await?; view_editor.v_update_calculations(update).await?; Ok(()) } pub async fn remove_calculation(&self, remove: RemoveCalculationChangesetPB) -> FlowyResult<()> { - let view_editor = self - .database_views - .get_or_init_view_editor(&remove.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(&remove.view_id).await?; view_editor.v_remove_calculation(remove).await?; Ok(()) } pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB { - if let Ok(view_editor) = self.database_views.get_or_init_view_editor(view_id).await { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { let filters = view_editor.v_get_all_filters().await; RepeatedFilterPB::from(&filters) } else { @@ -355,14 +261,14 @@ impl DatabaseEditor { } pub async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter> { - if let Ok(view_editor) = self.database_views.get_or_init_view_editor(view_id).await { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { Some(view_editor.v_get_filter(filter_id).await?) } else { None } } pub async fn get_all_sorts(&self, view_id: &str) -> RepeatedSortPB { - if let Ok(view_editor) = self.database_views.get_or_init_view_editor(view_id).await { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { view_editor.v_get_all_sorts().await.into() } else { RepeatedSortPB { items: vec![] } @@ -370,7 +276,7 @@ impl DatabaseEditor { } pub async fn delete_all_sorts(&self, view_id: &str) { - if let Ok(view_editor) = self.database_views.get_or_init_view_editor(view_id).await { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { let _ = view_editor.v_delete_all_sorts().await; } } @@ -378,10 +284,11 @@ impl DatabaseEditor { /// Returns a list of fields of the view. /// If `field_ids` is not provided, all the fields will be returned in the order of the field that /// defined in the view. Otherwise, the fields will be returned in the order of the `field_ids`. - pub async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> { - let database = self.database.read().await; + pub fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> { + let database = self.database.lock(); let field_ids = field_ids.unwrap_or_else(|| { database + .fields .get_all_field_orders() .into_iter() .map(|field| field.id) @@ -390,22 +297,23 @@ impl DatabaseEditor { database.get_fields_in_view(view_id, Some(field_ids)) } - pub async fn update_field(&self, params: FieldChangesetPB) -> FlowyResult<()> { - let mut database = self.database.write().await; - database.update_field(¶ms.field_id, |update| { - update - .set_name_if_not_none(params.name) - .set_icon_if_not_none(params.icon); - }); - notify_did_update_database_field(&database, ¶ms.field_id)?; + pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { + self + .database + .lock() + .fields + .update_field(¶ms.field_id, |update| { + update.set_name_if_not_none(params.name); + }); + notify_did_update_database_field(&self.database, ¶ms.field_id)?; Ok(()) } pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { let is_primary = self .database - .write() - .await + .lock() + .fields .get_field(field_id) .map(|field| field.is_primary) .unwrap_or(false); @@ -418,7 +326,7 @@ impl DatabaseEditor { } let database_id = { - let mut database = self.database.write().await; + let database = self.database.lock(); database.delete_field(field_id); database.get_database_id() }; @@ -436,7 +344,6 @@ impl DatabaseEditor { pub async fn clear_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { let field_type: FieldType = self .get_field(field_id) - .await .map(|field| field.field_type.into()) .unwrap_or_default(); @@ -467,77 +374,64 @@ impl DatabaseEditor { old_field: Field, ) -> FlowyResult<()> { let view_editors = self.database_views.editors().await; - { - let mut database = self.database.write().await; - update_field_type_option_fn(&mut database, type_option_data, &old_field).await?; - drop(database); - } + update_field_type_option_fn(&self.database, &view_editors, type_option_data, old_field).await?; - for view_editor in view_editors { - view_editor - .v_did_update_field_type_option(&old_field) - .await?; - } Ok(()) } pub async fn switch_to_field_type( &self, - view_id: &str, field_id: &str, new_field_type: FieldType, - field_name: Option<String>, ) -> FlowyResult<()> { - let mut database = self.database.write().await; - if let Some(field) = database.get_field(field_id) { - if field.is_primary { - return Err(FlowyError::new( - ErrorCode::Internal, - "Can not update primary field's field type", - )); - } + let field = self.database.lock().fields.get_field(field_id); + match field { + None => {}, + Some(field) => { + if field.is_primary { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not update primary field's field type", + )); + } - let old_field_type = FieldType::from(field.field_type); - let old_type_option_data = field.get_any_type_option(old_field_type); - let new_type_option_data = field - .get_any_type_option(new_field_type) - .unwrap_or_else(|| default_type_option_data_from_type(new_field_type)); + let old_field_type = FieldType::from(field.field_type); + let old_type_option_data = field.get_any_type_option(old_field_type); + let new_type_option_data = field + .get_any_type_option(new_field_type) + .unwrap_or_else(|| default_type_option_data_from_type(new_field_type)); - let transformed_type_option = transform_type_option( - view_id, - field_id, - old_field_type, - new_field_type, - old_type_option_data, - new_type_option_data, - &mut database, - ) - .await; + let transformed_type_option = transform_type_option( + old_field_type, + new_field_type, + old_type_option_data, + new_type_option_data, + ); + self + .database + .lock() + .fields + .update_field(field_id, |update| { + update + .set_field_type(new_field_type.into()) + .set_type_option(new_field_type.into(), Some(transformed_type_option)); + }); - database.update_field(field_id, |update| { - update - .set_field_type(new_field_type.into()) - .set_name_if_not_none(field_name) - .set_type_option(new_field_type.into(), Some(transformed_type_option)); - }); - - drop(database); - - for view in self.database_views.editors().await { - view.v_did_update_field_type(field_id, new_field_type).await; - } - - let database = self.database.read().await; - - notify_did_update_database_field(&database, field_id)?; + for view in self.database_views.editors().await { + view.v_did_update_field_type(field_id, new_field_type).await; + } + }, } + notify_did_update_database_field(&self.database, field_id)?; Ok(()) } pub async fn duplicate_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { - let mut database = self.database.write().await; - let is_primary = database + let is_primary = self + .database + .lock() + .fields .get_field(field_id) .map(|field| field.is_primary) .unwrap_or(false); @@ -549,10 +443,10 @@ impl DatabaseEditor { )); } - let value = - database.duplicate_field(view_id, field_id, |field| format!("{} (copy)", field.name)); - drop(database); - + let value = self + .database + .lock() + .duplicate_field(view_id, field_id, |field| format!("{} (copy)", field.name)); if let Some((index, duplicated_field)) = value { let _ = self .notify_did_insert_database_field(duplicated_field.clone(), index) @@ -572,129 +466,89 @@ impl DatabaseEditor { } pub async fn duplicate_row(&self, view_id: &str, row_id: &RowId) -> FlowyResult<()> { - let mut database = self.database.write().await; - let params = database - .duplicate_row(row_id) - .await - .ok_or_else(|| FlowyError::internal().with_context("error while copying row"))?; - let (index, row_order) = database.create_row_in_view(view_id, params).await?; + let (row_detail, index) = { + let database = self.database.lock(); - let row_meta = database.get_row_meta(row_id).await; - if let Some(row_meta) = row_meta { - database - .update_row_meta(&row_order.id, |meta_update| { - meta_update - .insert_cover_if_not_none(row_meta.cover) - .insert_icon_if_not_none(row_meta.icon_url) - .update_is_document_empty_if_not_none(Some(row_meta.is_document_empty)) - .update_attachment_count_if_not_none(Some(row_meta.attachment_count)); - }) - .await; + let params = database + .duplicate_row(row_id) + .ok_or_else(|| FlowyError::internal().with_context("error while copying row"))?; + + let (index, row_order) = database + .create_row_in_view(view_id, params) + .ok_or_else(|| { + FlowyError::internal().with_context("error while inserting duplicated row") + })?; + + tracing::trace!("duplicated row: {:?} at {}", row_order, index); + let row_detail = database.get_row_detail(&row_order.id); + + (row_detail, index) + }; + + if let Some(row_detail) = row_detail { + for view in self.database_views.editors().await { + view.v_did_create_row(&row_detail, index).await; + } } - drop(database); - - trace!( - "duplicate row: {:?} at index:{}, new row:{:?}", - row_id, - index, - row_order - ); - Ok(()) } - #[tracing::instrument(level = "trace", skip_all, err)] pub async fn move_row( &self, view_id: &str, from_row_id: RowId, to_row_id: RowId, ) -> FlowyResult<()> { - let mut database = self.database.write().await; - database.update_database_view(view_id, |view| { + let database = self.database.lock(); + + let row_detail = database.get_row_detail(&from_row_id).ok_or_else(|| { + let msg = format!("Cannot find row {}", from_row_id); + FlowyError::internal().with_context(msg) + })?; + + database.views.update_database_view(view_id, |view| { view.move_row_order(&from_row_id, &to_row_id); }); - Ok(()) - } + let new_index = database.index_of_row(view_id, &from_row_id); + drop(database); - #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn move_group_row( - &self, - view_id: &str, - from_group: &str, - to_group: &str, - from_row: RowId, - to_row: Option<RowId>, - ) -> FlowyResult<()> { - let row = self.get_row(view_id, &from_row).await.ok_or_else(|| { - let msg = format!("Can not find the row:{}", from_row); - FlowyError::internal().with_context(msg) - })?; - trace!( - "Move row:{} from group:{} to group:{}", - from_row, - from_group, - to_group - ); + if let Some(index) = new_index { + let delete_row_id = from_row_id.into_inner(); + let insert_row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(index as i32); + let changes = RowsChangePB::from_move(vec![delete_row_id], vec![insert_row]); - // when moving row between groups, the cells of the row should be updated - // if the updated cells is not empty, we need to update cells for given row - let updated_cells = self - .database_views - .get_or_init_view_editor(view_id) - .await? - .v_move_group_row(&row, to_group, to_row.clone()) - .await; - if !updated_cells.is_empty() { - self - .update_row(row.id, |row| { - row - .set_last_modified(timestamp()) - .set_cells(Cells::from(updated_cells)); - }) - .await?; - } - - let to_row = if to_row.is_some() { - to_row - } else { - self - .database - .read() - .await - .get_row_orders_for_view(view_id) - .last() - .map(|row| row.id.clone()) - }; - - if let Some(to_row_id) = to_row.clone() { - self.move_row(view_id, from_row.clone(), to_row_id).await?; + send_notification(view_id, DatabaseNotification::DidUpdateRow) + .payload(changes) + .send(); } Ok(()) } pub async fn create_row(&self, params: CreateRowPayloadPB) -> FlowyResult<Option<RowDetail>> { - let view_editor = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - let params = view_editor.v_will_create_row(params).await?; + let CreateRowParams { + collab_params, + open_after_create: _, + } = view_editor.v_will_create_row(params).await?; - let mut database = self.database.write().await; - let (index, row_order) = database - .create_row_in_view(&view_editor.view_id, params) - .await?; - let row_detail = database.get_row_detail(&row_order.id).await; - drop(database); + let result = self + .database + .lock() + .create_row_in_view(&view_editor.view_id, collab_params); - trace!("[Database]: did create row: {} at {}", row_order.id, index); - if let Some(row_detail) = row_detail { - trace!("created row: {:?} at {}", row_detail, index); - return Ok(Some(row_detail)); + if let Some((index, row_order)) = result { + tracing::trace!("created row: {:?} at {}", row_order, index); + let row_detail = self.database.lock().get_row_detail(&row_order.id); + if let Some(row_detail) = row_detail { + for view in self.database_views.editors().await { + view.v_did_create_row(&row_detail, index).await; + } + return Ok(Some(row_detail)); + } } Ok(None) @@ -714,7 +568,7 @@ impl DatabaseEditor { .and_then(|data| type_option_data_from_pb(data, ¶ms.field_type).ok()) .unwrap_or(default_type_option_data_from_type(params.field_type)); - let (index, field) = self.database.write().await.create_field_with_mut( + let (index, field) = self.database.lock().create_field_with_mut( ¶ms.view_id, name, params.field_type.into(), @@ -736,16 +590,21 @@ impl DatabaseEditor { pub async fn move_field(&self, params: MoveFieldParams) -> FlowyResult<()> { let (field, new_index) = { - let mut database = self.database.write().await; + let database = self.database.lock(); - let field = database.get_field(¶ms.from_field_id).ok_or_else(|| { - let msg = format!("Field with id: {} not found", ¶ms.from_field_id); - FlowyError::internal().with_context(msg) - })?; + let field = database + .fields + .get_field(¶ms.from_field_id) + .ok_or_else(|| { + let msg = format!("Field with id: {} not found", ¶ms.from_field_id); + FlowyError::internal().with_context(msg) + })?; - database.update_database_view(¶ms.view_id, |view_update| { - view_update.move_field_order(¶ms.from_field_id, ¶ms.to_field_id); - }); + database + .views + .update_database_view(¶ms.view_id, |view_update| { + view_update.move_field_order(¶ms.from_field_id, ¶ms.to_field_id); + }); let new_index = database.index_of_field(¶ms.view_id, ¶ms.from_field_id); @@ -765,7 +624,7 @@ impl DatabaseEditor { updated_fields: vec![], }; - database_notification_builder(¶ms.view_id, DatabaseNotification::DidUpdateFields) + send_notification(¶ms.view_id, DatabaseNotification::DidUpdateFields) .payload(notified_changeset) .send(); } @@ -773,145 +632,107 @@ impl DatabaseEditor { Ok(()) } - pub async fn get_all_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<Row>>> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; - Ok(view_editor.v_get_all_rows().await) + pub async fn get_rows(&self, view_id: &str) -> FlowyResult<Vec<Arc<RowDetail>>> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + Ok(view_editor.v_get_rows().await) } - pub async fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<Row> { - let database = self.database.read().await; - if database.contains_row(view_id, row_id) { - Some(database.get_row(row_id).await) + pub fn get_row(&self, view_id: &str, row_id: &RowId) -> Option<Row> { + if self.database.lock().views.is_row_exist(view_id, row_id) { + Some(self.database.lock().get_row(row_id)) } else { None } } - pub async fn init_database_row(&self, row_id: &RowId) -> FlowyResult<Arc<RwLock<DatabaseRow>>> { - if let Some(is_loading) = self.is_loading_rows.load_full() { - let mut rx = is_loading.subscribe(); - trace!("[Database]: wait for loading rows when trying to init database row"); - let _ = tokio::time::timeout(Duration::from_secs(10), rx.recv()).await; - } - - debug!("[Database]: Init database row: {}", row_id); - let database_row = self - .database - .read() - .await - .get_or_init_database_row(row_id) - .await - .ok_or_else(|| { - FlowyError::record_not_found() - .with_context(format!("The row:{} in database not found", row_id)) - })?; - - let is_finalized = self.finalized_rows.get(row_id.as_str()).await.is_some(); - if !is_finalized { - trace!("[Database]: finalize database row: {}", row_id); - let row_id = Uuid::from_str(row_id.as_str())?; - let collab_object = self.collab_builder.collab_object( - &self.user.workspace_id()?, - self.user.user_id()?, - &row_id, - CollabType::DatabaseRow, - )?; - - if let Err(err) = self.collab_builder.finalize( - collab_object, - CollabBuilderConfig::default(), - database_row.clone(), - ) { - error!("Failed to init database row: {}", err); - } - self - .finalized_rows - .insert(row_id.to_string(), Arc::downgrade(&database_row)) - .await; - } - - Ok(database_row) - } - - pub async fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option<RowMetaPB> { - let database = self.database.read().await; - if database.contains_row(view_id, row_id) { - let row_meta = database.get_row_meta(row_id).await?; - let row_document_id = database.get_row_document_id(row_id)?; + pub fn get_row_meta(&self, view_id: &str, row_id: &RowId) -> Option<RowMetaPB> { + if self.database.lock().views.is_row_exist(view_id, row_id) { + let row_meta = self.database.lock().get_row_meta(row_id)?; + let row_document_id = self.database.lock().get_row_document_id(row_id)?; Some(RowMetaPB { id: row_id.clone().into_inner(), - document_id: Some(row_document_id), + document_id: row_document_id, icon: row_meta.icon_url, - is_document_empty: Some(row_meta.is_document_empty), - attachment_count: Some(row_meta.attachment_count), - cover: row_meta.cover.map(|cover| cover.into()), + cover: row_meta.cover_url, + is_document_empty: row_meta.is_document_empty, }) } else { - warn!( - "the row:{} is not exist in view:{}", - row_id.as_str(), - view_id - ); + warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); + None + } + } + + pub fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<RowDetail> { + if self.database.lock().views.is_row_exist(view_id, row_id) { + self.database.lock().get_row_detail(row_id) + } else { + warn!("the row:{} is exist in view:{}", row_id.as_str(), view_id); None } } pub async fn delete_rows(&self, row_ids: &[RowId]) { - let _ = self.database.write().await.remove_rows(row_ids).await; + let rows = self.database.lock().remove_rows(row_ids); + + for row in rows { + tracing::trace!("Did delete row:{:?}", row); + for view in self.database_views.editors().await { + view.v_did_delete_row(&row).await; + } + } } #[tracing::instrument(level = "trace", skip_all)] pub async fn update_row_meta(&self, row_id: &RowId, changeset: UpdateRowMetaParams) { - let mut database = self.database.write().await; - database - .update_row_meta(row_id, |meta_update| { - meta_update - .insert_cover_if_not_none(changeset.cover) - .insert_icon_if_not_none(changeset.icon_url) - .update_is_document_empty_if_not_none(changeset.is_document_empty) - .update_attachment_count_if_not_none(changeset.attachment_count); - }) - .await; + self.database.lock().update_row_meta(row_id, |meta_update| { + meta_update + .insert_cover_if_not_none(changeset.cover_url) + .insert_icon_if_not_none(changeset.icon_url) + .update_is_document_empty_if_not_none(changeset.is_document_empty); + }); // Use the temporary row meta to get rid of the lock that not implement the `Send` or 'Sync' trait. - let row_detail = database.get_row_detail(row_id).await; - drop(database); - + let row_detail = self.database.lock().get_row_detail(row_id); if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { view.v_did_update_row_meta(row_id, &row_detail).await; } // Notifies the client that the row meta has been updated. - database_notification_builder(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta) - .payload(RowMetaPB::from(row_detail)) + send_notification(row_id.as_str(), DatabaseNotification::DidUpdateRowMeta) + .payload(RowMetaPB::from(&row_detail)) .send(); + + // Update the last modified time of the row + self + .update_last_modified_time(row_detail.clone(), &changeset.view_id) + .await; } } pub async fn get_cell(&self, field_id: &str, row_id: &RowId) -> Option<Cell> { - let database = self.database.read().await; - let field = database.get_field(field_id)?; + let database = self.database.lock(); + let field = database.fields.get_field(field_id)?; let field_type = FieldType::from(field.field_type); // If the cell data is referenced, return the reference data. Otherwise, return an empty cell. match field_type { FieldType::LastEditedTime | FieldType::CreatedTime => { - let row = database.get_row(row_id).await; - let cell_data = if field_type.is_created_time() { - TimestampCellData::new(row.created_at) + let row = database.get_row(row_id); + let wrapped_cell_data = if field_type.is_created_time() { + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(row.created_at))) } else { - TimestampCellData::new(row.modified_at) + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(row.modified_at))) }; - Some(cell_data.to_cell(field.field_type)) + Some(Cell::from(wrapped_cell_data)) }, - _ => database.get_cell(field_id, row_id).await.cell, + _ => database.get_cell(field_id, row_id).cell, } } pub async fn get_cell_pb(&self, field_id: &str, row_id: &RowId) -> Option<CellPB> { let (field, cell) = { let cell = self.get_cell(field_id, row_id).await?; - let field = self.database.read().await.get_field(field_id)?; + let field = self.database.lock().fields.get_field(field_id)?; (field, cell) }; @@ -926,34 +747,26 @@ impl DatabaseEditor { } pub async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec<RowCell> { - let database = self.database.read().await; - if let Some(field) = database.get_field(field_id) { + let database = self.database.lock(); + if let Some(field) = database.fields.get_field(field_id) { let field_type = FieldType::from(field.field_type); match field_type { - FieldType::LastEditedTime | FieldType::CreatedTime => { - database - .get_rows_for_view(view_id, 10, None) - .await - .filter_map(|result| async { - match result { - Ok(row) => { - let data = if field_type.is_created_time() { - TimestampCellData::new(row.created_at) - } else { - TimestampCellData::new(row.modified_at) - }; - Some(RowCell { - row_id: row.id, - cell: Some(data.to_cell(field.field_type)), - }) - }, - Err(_) => None, - } - }) - .collect() - .await - }, - _ => database.get_cells_for_field(view_id, field_id).await, + FieldType::LastEditedTime | FieldType::CreatedTime => database + .get_rows_for_view(view_id) + .into_iter() + .map(|row| { + let data = if field_type.is_created_time() { + TimestampCellData::new(row.created_at) + } else { + TimestampCellData::new(row.modified_at) + }; + RowCell { + row_id: row.id, + cell: Some(Cell::from(data)), + } + }) + .collect(), + _ => database.get_cells_for_field(view_id, field_id), } } else { vec![] @@ -969,15 +782,15 @@ impl DatabaseEditor { cell_changeset: BoxAny, ) -> FlowyResult<()> { let (field, cell) = { - let database = self.database.read().await; - let field = match database.get_field(field_id) { + let database = self.database.lock(); + let field = match database.fields.get_field(field_id) { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); Err(FlowyError::internal().with_context(msg)) }, }?; - (field, database.get_cell(field_id, row_id).await.cell) + (field, database.get_cell(field_id, row_id).cell) }; let new_cell = @@ -985,9 +798,24 @@ impl DatabaseEditor { self.update_cell(view_id, row_id, field_id, new_cell).await } + async fn update_last_modified_time(&self, row_detail: RowDetail, view_id: &str) { + self + .database + .lock() + .update_row(&row_detail.row.id, |row_update| { + row_update.set_last_modified(timestamp()); + }); + + let editor = self.database_views.get_view_editor(view_id).await; + if let Ok(editor) = editor { + editor + .v_did_update_row(&Some(row_detail.clone()), &row_detail, None) + .await; + } + } + /// Update a cell in the database. /// This will notify all views that the cell has been updated. - #[instrument(level = "trace", skip_all)] pub async fn update_cell( &self, view_id: &str, @@ -996,17 +824,12 @@ impl DatabaseEditor { new_cell: Cell, ) -> FlowyResult<()> { // Get the old row before updating the cell. It would be better to get the old cell - let old_row = self.get_row(view_id, row_id).await; - trace!("[Database Row]: update cell: {:?}", new_cell); - self - .update_row(row_id.clone(), |row_update| { - row_update - .set_last_modified(timestamp()) - .update_cells(|cell_update| { - cell_update.insert(field_id, new_cell); - }); - }) - .await?; + let old_row = { self.get_row_detail(view_id, row_id) }; + self.database.lock().update_row(row_id, |row_update| { + row_update.update_cells(|cell_update| { + cell_update.insert(field_id, new_cell); + }); + }); self .did_update_row(view_id, row_id, field_id, old_row) @@ -1015,31 +838,15 @@ impl DatabaseEditor { Ok(()) } - pub async fn update_row<F>(&self, row_id: RowId, modify: F) -> FlowyResult<()> - where - F: FnOnce(RowUpdate), - { - if self.finalized_rows.get(row_id.as_str()).await.is_none() { - info!( - "[Database Row]: row:{} is not finalized when editing, init it", - row_id - ); - self.init_database_row(&row_id).await?; - } - self.database.write().await.update_row(row_id, modify).await; - Ok(()) - } - pub async fn clear_cell(&self, view_id: &str, row_id: RowId, field_id: &str) -> FlowyResult<()> { // Get the old row before updating the cell. It would be better to get the old cell - let old_row = self.get_row(view_id, &row_id).await; - self - .update_row(row_id.clone(), |row_update| { - row_update.update_cells(|cell_update| { - cell_update.clear(field_id); - }); - }) - .await?; + let old_row = { self.get_row_detail(view_id, &row_id) }; + + self.database.lock().update_row(&row_id, |row_update| { + row_update.update_cells(|cell_update| { + cell_update.clear(field_id); + }); + }); self .did_update_row(view_id, &row_id, field_id, old_row) @@ -1053,116 +860,26 @@ impl DatabaseEditor { view_id: &str, row_id: &RowId, field_id: &str, - old_row: Option<Row>, + old_row: Option<RowDetail>, ) { - let option_row = self.get_row(view_id, row_id).await; - let field_type = self - .database - .read() - .await - .get_field(field_id) - .map(|field| field.field_type); - - if let Some(row) = option_row { + let option_row = self.get_row_detail(view_id, row_id); + if let Some(new_row_detail) = option_row { for view in self.database_views.editors().await { view - .v_did_update_row(&old_row, &row, Some(field_id.to_owned())) - .await; - - let field_id = field_id.to_string(); - tokio::spawn(async move { - view.v_update_calculate(&field_id).await; - }); - } - - if let Some(field_type) = field_type { - if FieldType::from(field_type) == FieldType::Media { - self - .did_update_attachments(view_id, row_id, field_id, old_row.clone()) - .await; - } - } - } - } - - async fn did_update_attachments( - &self, - view_id: &str, - row_id: &RowId, - field_id: &str, - old_row: Option<Row>, - ) { - let field = self.get_field(field_id).await; - if let Some(field) = field { - let handler = TypeOptionCellExt::new(&field, None) - .get_type_option_cell_data_handler_with_field_type(FieldType::Media); - if handler.is_none() { - return; - } - let handler = handler.unwrap(); - - let cell = self.get_cell(field_id, row_id).await; - let new_count = match cell { - Some(cell) => { - let data = handler - .handle_get_boxed_cell_data(&cell, &field) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(MediaCellData::default); - - data.files.len() as i64 - }, - None => 0, - }; - - let old_cell = old_row.and_then(|row| row.cells.get(field_id).cloned()); - let old_count = match old_cell { - Some(old_cell) => { - let data = handler - .handle_get_boxed_cell_data(&old_cell, &field) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(MediaCellData::default); - - data.files.len() as i64 - }, - None => 0, - }; - - if new_count != old_count { - let attachment_count = self - .get_row_meta(view_id, row_id) - .await - .and_then(|meta| meta.attachment_count); - - let new_attachment_count = match attachment_count { - Some(attachment_count) => attachment_count + new_count - old_count, - None => new_count, - }; - - self - .update_row_meta( - row_id, - UpdateRowMetaParams { - id: row_id.clone().into_inner(), - view_id: view_id.to_string(), - cover: None, - icon_url: None, - is_document_empty: None, - attachment_count: Some(new_attachment_count), - }, - ) + .v_did_update_row(&old_row, &new_row_detail, Some(field_id.to_owned())) .await; } } } - pub async fn get_auto_updated_fields_changesets( + pub fn get_auto_updated_fields_changesets( &self, view_id: &str, row_id: RowId, ) -> Vec<CellChangesetNotifyPB> { // Get all auto updated fields. It will be used to notify the frontend // that the fields have been updated. - let auto_updated_fields = self.get_auto_updated_fields(view_id).await; + let auto_updated_fields = self.get_auto_updated_fields(view_id); // Collect all the updated field's id. Notify the frontend that all of them have been updated. let auto_updated_field_ids = auto_updated_fields @@ -1185,7 +902,7 @@ impl DatabaseEditor { field_id: &str, option_name: String, ) -> Option<SelectOptionPB> { - let field = self.database.read().await.get_field(field_id)?; + let field = self.database.lock().fields.get_field(field_id)?; let type_option = select_type_option_from_field(&field).ok()?; let select_option = type_option.create_option(&option_name); Some(SelectOptionPB::from(select_option)) @@ -1200,10 +917,15 @@ impl DatabaseEditor { row_id: RowId, options: Vec<SelectOptionPB>, ) -> FlowyResult<()> { - let mut database = self.database.write().await; - let field = database.get_field(field_id).ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("Field with id:{} not found", &field_id)) - })?; + let field = self + .database + .lock() + .fields + .get_field(field_id) + .ok_or_else(|| { + FlowyError::record_not_found() + .with_context(format!("Field with id:{} not found", &field_id)) + })?; debug_assert!(FieldType::from(field.field_type).is_select_option()); let mut type_option = select_type_option_from_field(&field)?; @@ -1217,12 +939,13 @@ impl DatabaseEditor { // Update the field's type option let view_editors = self.database_views.editors().await; - update_field_type_option_fn(&mut database, type_option.to_type_option_data(), &field).await?; - drop(database); - - for view_editor in view_editors { - view_editor.v_did_update_field_type_option(&field).await?; - } + update_field_type_option_fn( + &self.database, + &view_editors, + type_option.to_type_option_data(), + field.clone(), + ) + .await?; // Insert the options into the cell self @@ -1238,8 +961,7 @@ impl DatabaseEditor { row_id: RowId, options: Vec<SelectOptionPB>, ) -> FlowyResult<()> { - let mut database = self.database.write().await; - let field = match database.get_field(field_id) { + let field = match self.database.lock().fields.get_field(field_id) { Some(field) => Ok(field), None => { let msg = format!("Field with id:{} not found", &field_id); @@ -1257,14 +979,13 @@ impl DatabaseEditor { } let view_editors = self.database_views.editors().await; - update_field_type_option_fn(&mut database, type_option.to_type_option_data(), &field).await?; - - // Drop the database write lock ASAP - drop(database); - - for view_editor in view_editors { - view_editor.v_did_update_field_type_option(&field).await?; - } + update_field_type_option_fn( + &self.database, + &view_editors, + type_option.to_type_option_data(), + field.clone(), + ) + .await?; self .update_cell_with_changeset(view_id, &row_id, field_id, BoxAny::new(cell_changeset)) @@ -1281,8 +1002,8 @@ impl DatabaseEditor { ) -> FlowyResult<()> { let field = self .database - .read() - .await + .lock() + .fields .get_field(field_id) .ok_or_else(|| { FlowyError::record_not_found() @@ -1298,14 +1019,14 @@ impl DatabaseEditor { #[tracing::instrument(level = "trace", skip_all, err)] pub async fn load_groups(&self, view_id: &str) -> FlowyResult<RepeatedGroupPB> { - let view = self.database_views.get_or_init_view_editor(view_id).await?; + let view = self.database_views.get_view_editor(view_id).await?; let groups = view.v_load_groups().await.unwrap_or_default(); Ok(RepeatedGroupPB { items: groups }) } #[tracing::instrument(level = "trace", skip_all, err)] pub async fn get_group(&self, view_id: &str, group_id: &str) -> FlowyResult<GroupPB> { - let view = self.database_views.get_or_init_view_editor(view_id).await?; + let view = self.database_views.get_view_editor(view_id).await?; let group = view.v_get_group(group_id).await?; Ok(group) } @@ -1322,19 +1043,69 @@ impl DatabaseEditor { return Ok(()); } - let view = self.database_views.get_or_init_view_editor(view_id).await?; + let view = self.database_views.get_view_editor(view_id).await?; view.v_move_group(from_group, to_group).await?; Ok(()) } + #[tracing::instrument(level = "trace", skip_all, err)] + pub async fn move_group_row( + &self, + view_id: &str, + from_group: &str, + to_group: &str, + from_row: RowId, + to_row: Option<RowId>, + ) -> FlowyResult<()> { + let row_detail = self.get_row_detail(view_id, &from_row); + match row_detail { + None => { + warn!( + "Move row between group failed, can not find the row:{}", + from_row + ) + }, + Some(row_detail) => { + let view = self.database_views.get_view_editor(view_id).await?; + let mut row_changeset = RowChangeset::new(row_detail.row.id.clone()); + view + .v_move_group_row(&row_detail, &mut row_changeset, to_group, to_row.clone()) + .await; + + let to_row = if to_row.is_some() { + to_row + } else { + let row_details = self.get_rows(view_id).await?; + row_details + .last() + .map(|row_detail| row_detail.row.id.clone()) + }; + if let Some(row_id) = to_row.clone() { + self.move_row(view_id, from_row.clone(), row_id).await?; + } + + if from_group == to_group { + return Ok(()); + } + + tracing::trace!("Row data changed: {:?}", row_changeset); + self.database.lock().update_row(&row_detail.row.id, |row| { + row.set_cells(Cells::from(row_changeset.cell_by_field_id.clone())); + }); + }, + } + + Ok(()) + } + pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { - let view = self.database_views.get_or_init_view_editor(view_id).await?; + let view = self.database_views.get_view_editor(view_id).await?; view.v_group_by_field(field_id).await?; Ok(()) } pub async fn create_group(&self, view_id: &str, name: &str) -> FlowyResult<()> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_create_group(name).await?; Ok(()) } @@ -1345,7 +1116,7 @@ impl DatabaseEditor { view_id: &str, layout_setting: LayoutSettingChangeset, ) -> FlowyResult<()> { - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; + let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_set_layout_settings(layout_setting).await?; Ok(()) } @@ -1355,18 +1126,14 @@ impl DatabaseEditor { view_id: &str, layout_ty: DatabaseLayout, ) -> Option<LayoutSettingParams> { - let view = self - .database_views - .get_or_init_view_editor(view_id) - .await - .ok()?; + let view = self.database_views.get_view_editor(view_id).await.ok()?; let layout_setting = view.v_get_layout_settings(&layout_ty).await; Some(layout_setting) } #[tracing::instrument(level = "trace", skip_all)] pub async fn get_all_calendar_events(&self, view_id: &str) -> Vec<CalendarEventPB> { - match self.database_views.get_or_init_view_editor(view_id).await { + match self.database_views.get_view_editor(view_id).await { Ok(view) => view.v_get_all_calendar_events().await.unwrap_or_default(), Err(_) => { warn!("Can not find the view: {}", view_id); @@ -1380,23 +1147,19 @@ impl DatabaseEditor { &self, view_id: &str, ) -> FlowyResult<Vec<NoDateCalendarEventPB>> { - let _database_view = self.database_views.get_or_init_view_editor(view_id).await?; + let _database_view = self.database_views.get_view_editor(view_id).await?; Ok(vec![]) } #[tracing::instrument(level = "trace", skip_all)] pub async fn get_calendar_event(&self, view_id: &str, row_id: RowId) -> Option<CalendarEventPB> { - let view = self - .database_views - .get_or_init_view_editor(view_id) - .await - .ok()?; + let view = self.database_views.get_view_editor(view_id).await.ok()?; view.v_get_calendar_event(row_id).await } #[tracing::instrument(level = "trace", skip_all, err)] async fn notify_did_insert_database_field(&self, field: Field, index: usize) -> FlowyResult<()> { - let database_id = self.database.read().await.get_database_id(); + let database_id = self.database.lock().get_database_id(); let index_field = IndexFieldPB { field: FieldPB::new(field), index: index as i32, @@ -1410,9 +1173,9 @@ impl DatabaseEditor { &self, changeset: DatabaseFieldChangesetPB, ) -> FlowyResult<()> { - let views = self.database.read().await.get_all_database_views_meta(); + let views = self.database.lock().get_all_database_views_meta(); for view in views { - database_notification_builder(&view.id, DatabaseNotification::DidUpdateFields) + send_notification(&view.id, DatabaseNotification::DidUpdateFields) .payload(changeset.clone()) .send(); } @@ -1424,282 +1187,55 @@ impl DatabaseEditor { &self, view_id: &str, ) -> FlowyResult<DatabaseViewSettingPB> { - let view = self - .database - .read() - .await - .get_view(view_id) - .ok_or_else(|| { - FlowyError::record_not_found() - .with_context(format!("Can't find the database view:{}", view_id)) + let view = + self.database.lock().get_view(view_id).ok_or_else(|| { + FlowyError::record_not_found().with_context("Can't find the database view") })?; Ok(database_view_setting_pb_from_view(view)) } - pub async fn close_database(&self) { - info!("[Database]: {} close", self.database_id); - let token = CancellationToken::new(); - let cloned_finalized_rows = self.finalized_rows.clone(); - self - .un_finalized_rows_cancellation - .store(Some(Arc::new(token.clone()))); - tokio::spawn(async move { - // Using select! to concurrently run two asynchronous futures: - // 1. Wait for 30 seconds, then invalidate all the finalized rows. - // 2. If the cancellation token (`token`) is triggered before the 30 seconds expire, - // cancel the invalidation action and do nothing. - select! { - _ = tokio::time::sleep(Duration::from_secs(30)) => { - for (row_id, row) in cloned_finalized_rows.iter() { - remove_row_sync_plugin(row_id.as_str(), row).await; - } - cloned_finalized_rows.invalidate_all(); - }, - _ = token.cancelled() => { - trace!("Invalidate action cancelled"); - } - } - }); + pub async fn get_database_data(&self, view_id: &str) -> FlowyResult<DatabasePB> { + let database_view = self.database_views.get_view_editor(view_id).await?; + let view = database_view + .v_get_view() + .await + .ok_or_else(FlowyError::record_not_found)?; + let rows = database_view.v_get_rows().await; + let (database_id, fields, is_linked) = { + let database = self.database.lock(); + let database_id = database.get_database_id(); + let fields = database + .fields + .get_all_field_orders() + .into_iter() + .map(FieldIdPB::from) + .collect(); + let is_linked = database.is_inline_view(view_id); + (database_id, fields, is_linked) + }; - let cancellation = self.database_cancellation.read().await; - if let Some(cancellation) = &*cancellation { - info!("Cancel database operation"); - cancellation.cancel(); - } - } - - /// Open database view - /// When opening database view, it will load database rows from remote if they are not exist in local disk. - /// After load all rows, it will apply filters and sorts to the rows. - /// - /// If notify_finish is not None, it will send a notification to the sender when the opening process is complete. - pub async fn open_database_view( - &self, - view_id: &str, - notify_finish: Option<tokio::sync::oneshot::Sender<()>>, - ) -> FlowyResult<DatabasePB> { - if let Some(un_finalized_rows_token) = self.un_finalized_rows_cancellation.load_full() { - un_finalized_rows_token.cancel(); - } - - let (tx, rx) = oneshot::channel(); - self.opening_ret_txs.write().await.push(tx); - // Check if the database is currently being opened - if self.is_loading_rows.load_full().is_none() { - self - .is_loading_rows - .store(Some(Arc::new(broadcast::channel(500).0))); - let view_layout = self.database.read().await.get_database_view_layout(view_id); - let new_token = CancellationToken::new(); - if let Some(old_token) = self - .database_cancellation - .write() - .await - .replace(new_token.clone()) - { - old_token.cancel(); - } - - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; - let row_orders = view_editor.get_all_row_orders().await?; - view_editor.set_row_orders(row_orders.clone()).await; - - // Collect database details in a single block holding the `read` lock - let (database_id, fields) = { - let database = self.database.read().await; - ( - database.get_database_id(), - database - .get_all_field_orders() - .into_iter() - .map(FieldIdPB::from) - .collect::<Vec<_>>(), - ) - }; - - // the order_rows are not filtered and sorted yet - let mut order_rows = row_orders - .iter() - .map(|row_order| RowMetaPB::from(row_order.clone())) - .collect::<Vec<RowMetaPB>>(); - - trace!( - "[Database]: database: {}, num fields: {}, num rows: {}", - database_id, - fields.len(), - order_rows.len() - ); - - let view_editor = self.database_views.get_or_init_view_editor(view_id).await?; - let blocking_read = notify_finish.is_some() || order_rows.len() < 50; - - let (tx, rx) = oneshot::channel(); - self - .async_load_rows(view_editor, Some(tx), new_token, blocking_read, row_orders) - .await; - if blocking_read { - // the rows returned here are applied with filters and sorts - if let Ok(rows) = rx.await { - order_rows = rows - .into_iter() - .map(|row| RowMetaPB::from(row.as_ref())) - .collect(); - } - } - - if let Some(tx) = notify_finish { - let _ = tx.send(()); - } - - let result = Ok(DatabasePB { - id: database_id, - fields, - rows: order_rows, - layout_type: view_layout.into(), - }); - // Mark that the opening process is complete - if let Some(tx) = self.is_loading_rows.load_full() { - let _ = tx.send(()); - } - self.is_loading_rows.store(None); - // Collect all waiting tasks and send the result - let txs = std::mem::take(&mut *self.opening_ret_txs.write().await); - for tx in txs { - let _ = tx.send(result.clone()); - } - } - - // Wait for the result or timeout after 60 seconds - match tokio::time::timeout(Duration::from_secs(60), rx).await { - Ok(result) => result.map_err(internal_error)?, - Err(_) => Err(FlowyError::internal().with_context("Timeout while opening database view")), - } - } - - async fn async_load_rows( - &self, - view_editor: Arc<DatabaseViewEditor>, - notify_finish: Option<Sender<Vec<Arc<Row>>>>, - new_token: CancellationToken, - blocking_read: bool, - original_row_orders: Vec<RowOrder>, - ) { - trace!( - "[Database]: start loading rows, blocking: {}", - blocking_read - ); - let cloned_database = Arc::downgrade(&self.database); - let fields = self.get_fields(&view_editor.view_id, None).await; - tokio::spawn(async move { - let apply_filter_and_sort = - |mut loaded_rows: Vec<Arc<Row>>, view_editor: Arc<DatabaseViewEditor>| async move { - for loaded_row in loaded_rows.iter() { - view_editor - .row_by_row_id - .insert(loaded_row.id.to_string(), loaded_row.clone()); - } - - if view_editor.has_filters().await { - trace!("[Database]: filtering rows:{}", loaded_rows.len()); - if blocking_read { - loaded_rows = view_editor.v_filter_rows(loaded_rows).await; - } else { - view_editor.v_filter_rows_and_notify(&mut loaded_rows).await; - } - } else { - trace!("[Database]: no filter to apply"); - } - - if view_editor.has_sorts().await { - trace!("[Database]: sorting rows:{}", loaded_rows.len()); - if blocking_read { - view_editor.v_sort_rows(&mut loaded_rows).await; - } else { - view_editor.v_sort_rows_and_notify(&mut loaded_rows).await; - } - loaded_rows - } else { - trace!("[Database]: no sort to apply"); - // if there are no sorts, use the original row orders to sort the loaded rows - let mut map = loaded_rows - .into_iter() - .map(|row| (row.id.clone(), row)) - .collect::<HashMap<RowId, Arc<Row>>>(); - - let mut order_rows = vec![]; - for row_order in original_row_orders { - if let Some(row) = map.remove(&row_order.id) { - order_rows.push(row); - } - } - order_rows - } - }; - - let mut loaded_rows = vec![]; - const CHUNK_SIZE: usize = 10; - let row_orders = view_editor.row_orders.read().await; - let row_orders_chunks = row_orders.chunks(CHUNK_SIZE).collect::<Vec<_>>(); - - // Iterate over chunks and load rows concurrently - for chunk_row_orders in row_orders_chunks { - // Check if the database is still available - let database = match cloned_database.upgrade() { - None => break, // If the database is dropped, stop the operation - Some(database) => database, - }; - - let row_ids = chunk_row_orders - .iter() - .map(|row_order| row_order.id.clone()) - .collect(); - - let new_loaded_rows: Vec<Arc<Row>> = database - .read() - .await - .init_database_rows(row_ids, chunk_row_orders.len(), None) - .filter_map(|result| async { - let database_row = result.ok()?; - let read_guard = database_row.read().await; - read_guard.get_row().map(Arc::new) - }) - .collect() - .await; - loaded_rows.extend(new_loaded_rows); - - // Check for cancellation after each chunk - if new_token.is_cancelled() { - info!("[Database]: stop loading database rows"); - return; - } - yield_now().await; - } - drop(row_orders); - - info!( - "[Database]: Finish loading all rows: {}, blocking: {}", - loaded_rows.len(), - blocking_read - ); - let loaded_rows = apply_filter_and_sort(loaded_rows, view_editor.clone()).await; - // Update calculation values - let calculate_rows = loaded_rows.clone(); - if let Some(notify_finish) = notify_finish { - let _ = notify_finish.send(loaded_rows); - } - tokio::spawn(async move { - let _ = view_editor.v_calculate_rows(fields, calculate_rows).await; - }); - }); + let rows = rows + .into_iter() + .map(|row_detail| RowMetaPB::from(row_detail.as_ref())) + .collect::<Vec<RowMetaPB>>(); + Ok(DatabasePB { + id: database_id, + fields, + rows, + layout_type: view.layout.into(), + is_linked, + }) } pub async fn export_csv(&self, style: CSVFormat) -> FlowyResult<String> { let database = self.database.clone(); - let database_guard = database.read().await; - let csv = CSVExport - .export_database(&database_guard, style) - .await - .map_err(internal_error)?; + let csv = tokio::task::spawn_blocking(move || { + let database_guard = database.lock(); + let csv = CSVExport.export_database(&database_guard, style)?; + Ok::<String, FlowyError>(csv) + }) + .await + .map_err(internal_error)??; Ok(csv) } @@ -1708,7 +1244,7 @@ impl DatabaseEditor { view_id: &str, field_ids: Vec<String>, ) -> FlowyResult<Vec<FieldSettings>> { - let view = self.database_views.get_or_init_view_editor(view_id).await?; + let view = self.database_views.get_view_editor(view_id).await?; let field_settings = view .v_get_field_settings(&field_ids) @@ -1722,7 +1258,6 @@ impl DatabaseEditor { pub async fn get_all_field_settings(&self, view_id: &str) -> FlowyResult<Vec<FieldSettings>> { let field_ids = self .get_fields(view_id, None) - .await .iter() .map(|field| field.id.clone()) .collect(); @@ -1734,10 +1269,7 @@ impl DatabaseEditor { &self, params: FieldSettingsChangesetPB, ) -> FlowyResult<()> { - let view = self - .database_views - .get_or_init_view_editor(¶ms.view_id) - .await?; + let view = self.database_views.get_view_editor(¶ms.view_id).await?; view.v_update_field_settings(params).await?; Ok(()) @@ -1746,8 +1278,7 @@ impl DatabaseEditor { pub async fn get_related_database_id(&self, field_id: &str) -> FlowyResult<String> { let mut field = self .database - .read() - .await + .lock() .get_fields(Some(vec![field_id.to_string()])); let field = field.pop().ok_or(FlowyError::internal())?; @@ -1758,99 +1289,46 @@ impl DatabaseEditor { Ok(type_option.database_id) } - pub async fn get_row_index(&self, view_id: &str, row_id: &RowId) -> Option<usize> { - self.database.read().await.get_row_index(view_id, row_id) - } - - pub async fn get_row_order_at_index(&self, view_id: &str, index: u32) -> Option<RowOrder> { - self - .database - .read() - .await - .get_row_order_at_index(view_id, index) - .await - } - pub async fn get_related_rows( &self, - row_ids: Option<Vec<String>>, + row_ids: Option<&Vec<String>>, ) -> FlowyResult<Vec<RelatedRowDataPB>> { - let database = self.database.read().await; - let primary_field = Arc::new( - database - .get_primary_field() - .ok_or_else(|| FlowyError::internal().with_context("Primary field is not exist"))?, - ); + let primary_field = self.database.lock().fields.get_primary_field().unwrap(); + let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone())) + .get_type_option_cell_data_handler_with_field_type(FieldType::RichText) + .ok_or(FlowyError::internal())?; - let handler = Arc::new( - TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone())) - .get_type_option_cell_data_handler_with_field_type(FieldType::RichText) - .ok_or(FlowyError::internal())?, - ); + let row_data = { + let database = self.database.lock(); + let mut rows = database.get_database_rows(); + if let Some(row_ids) = row_ids { + rows.retain(|row| row_ids.contains(&row.id)); + } + rows + .iter() + .map(|row| { + let title = database + .get_cell(&primary_field.id, &row.id) + .cell + .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field)) + .and_then(|cell_data| cell_data.unbox_or_none()) + .unwrap_or_else(|| StringCellData("".to_string())); - match row_ids { - None => { - let mut row_data = vec![]; - let rows_stream = database.get_all_rows(10, None).await; - pin_mut!(rows_stream); - while let Some(result) = rows_stream.next().await { - if let Ok(row) = result { - let title = database - .get_cell(&primary_field.id, &row.id) - .await - .cell - .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &primary_field)) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(|| StringCellData("".to_string())); // Default to empty string - - row_data.push(RelatedRowDataPB { - row_id: row.id.to_string(), - name: title.0, - }); + RelatedRowDataPB { + row_id: row.id.to_string(), + name: title.0, } - } + }) + .collect::<Vec<_>>() + }; - Ok(row_data) - }, - Some(row_ids) => { - let mut database_rows = vec![]; - for row_id in row_ids { - let row_id = RowId::from(row_id); - if let Some(database_row) = database.get_or_init_database_row(&row_id).await { - database_rows.push(database_row); - } - } - - let row_data_futures = database_rows.into_iter().map(|database_row| { - let handler = handler.clone(); - let cloned_primary_field = primary_field.clone(); - async move { - let row_id = database_row.read().await.row_id.to_string(); - let title = database_row - .read() - .await - .get_cell(&cloned_primary_field.id) - .and_then(|cell| handler.handle_get_boxed_cell_data(&cell, &cloned_primary_field)) - .and_then(|cell_data| cell_data.unbox_or_none()) - .unwrap_or_else(|| StringCellData("".to_string())); - - RelatedRowDataPB { - row_id, - name: title.0, - } - } - }); - let row_data = join_all(row_data_futures).await; - Ok(row_data) - }, - } + Ok(row_data) } - async fn get_auto_updated_fields(&self, view_id: &str) -> Vec<Field> { + fn get_auto_updated_fields(&self, view_id: &str) -> Vec<Field> { self .database - .read() - .await + .lock() .get_fields_in_view(view_id, None) .into_iter() .filter(|f| FieldType::from(f.field_type).is_auto_update()) @@ -1859,50 +1337,45 @@ impl DatabaseEditor { /// Only expose this method for testing #[cfg(debug_assertions)] - pub fn get_mutex_database(&self) -> &RwLock<Database> { + pub fn get_mutex_database(&self) -> &MutexDatabase { &self.database } } struct DatabaseViewOperationImpl { - database: Arc<RwLock<Database>>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + database: Arc<MutexDatabase>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, cell_cache: CellCache, editor_by_view_id: Arc<RwLock<EditorByViewId>>, - #[allow(dead_code)] - database_cancellation: Arc<RwLock<Option<CancellationToken>>>, } -#[async_trait] impl DatabaseViewOperation for DatabaseViewOperationImpl { - fn get_database(&self) -> Arc<RwLock<Database>> { + fn get_database(&self) -> Arc<MutexDatabase> { self.database.clone() } - async fn get_view(&self, view_id: &str) -> Option<DatabaseView> { - self.database.read().await.get_view(view_id) + fn get_view(&self, view_id: &str) -> Fut<Option<DatabaseView>> { + let view = self.database.lock().get_view(view_id); + to_fut(async move { view }) } - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> { - self - .database - .read() - .await - .get_fields_in_view(view_id, field_ids) + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>> { + let fields = self.database.lock().get_fields_in_view(view_id, field_ids); + to_fut(async move { fields }) } - async fn get_field(&self, field_id: &str) -> Option<Field> { - self.database.read().await.get_field(field_id) + fn get_field(&self, field_id: &str) -> Option<Field> { + self.database.lock().fields.get_field(field_id) } - async fn create_field( + fn create_field( &self, view_id: &str, name: &str, field_type: FieldType, type_option_data: TypeOptionData, - ) -> Field { - let (_, field) = self.database.write().await.create_field_with_mut( + ) -> Fut<Field> { + let (_, field) = self.database.lock().create_field_with_mut( view_id, name.to_string(), field_type.into(), @@ -1914,217 +1387,199 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { }, default_field_settings_by_layout_map(), ); - field + to_fut(async move { field }) } - async fn update_field( + fn update_field( &self, type_option_data: TypeOptionData, old_field: Field, - ) -> Result<(), FlowyError> { - let view_editors = self - .editor_by_view_id - .read() - .await - .values() - .cloned() - .collect::<Vec<_>>(); - - // - { - let mut database = self.database.write().await; - let _ = update_field_type_option_fn(&mut database, type_option_data, &old_field).await; - drop(database); - } - - for view_editor in view_editors { - view_editor - .v_did_update_field_type_option(&old_field) - .await?; - } - Ok(()) - } - - async fn get_primary_field(&self) -> Option<Arc<Field>> { - self.database.read().await.get_primary_field().map(Arc::new) - } - - async fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Option<usize> { - self.database.read().await.index_of_row(view_id, row_id) - } - - async fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc<RowDetail>)> { - let database = self.database.read().await; - let index = database.index_of_row(view_id, row_id); - let row_detail = database.get_row_detail(row_id).await; - match (index, row_detail) { - (Some(index), Some(row_detail)) => Some((index, Arc::new(row_detail))), - _ => None, - } - } - - async fn get_all_rows(&self, view_id: &str, row_orders: Vec<RowOrder>) -> Vec<Arc<Row>> { - let view_id = view_id.to_string(); - trace!("{} has total row orders: {}", view_id, row_orders.len()); - let mut all_rows = vec![]; - let read_guard = self.database.read().await; - let rows_stream = read_guard - .get_rows_from_row_orders(&row_orders, 10, None) - .await; - pin_mut!(rows_stream); - - while let Some(result) = rows_stream.next().await { - match result { - Ok(row) => { - all_rows.push(row); - }, - Err(err) => error!("Error while loading rows: {}", err), + ) -> FutureResult<(), FlowyError> { + let weak_editor_by_view_id = Arc::downgrade(&self.editor_by_view_id); + let weak_database = Arc::downgrade(&self.database); + FutureResult::new(async move { + if let (Some(database), Some(editor_by_view_id)) = + (weak_database.upgrade(), weak_editor_by_view_id.upgrade()) + { + let view_editors = editor_by_view_id.read().await.values().cloned().collect(); + let _ = + update_field_type_option_fn(&database, &view_editors, type_option_data, old_field).await; } - } - - trace!("total row details: {}", all_rows.len()); - all_rows.into_iter().map(Arc::new).collect() + Ok(()) + }) } - async fn get_all_row_orders(&self, view_id: &str) -> Vec<RowOrder> { - self.database.read().await.get_row_orders_for_view(view_id) + fn get_primary_field(&self) -> Fut<Option<Arc<Field>>> { + let field = self + .database + .lock() + .fields + .get_primary_field() + .map(Arc::new); + to_fut(async move { field }) } - async fn remove_row(&self, row_id: &RowId) -> Option<Row> { - self.database.write().await.remove_row(row_id).await + fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<usize>> { + let index = self.database.lock().index_of_row(view_id, row_id); + to_fut(async move { index }) } - async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec<RowCell> { - let editor = self.editor_by_view_id.read().await.get(view_id).cloned(); - match editor { - None => vec![], - Some(editor) => editor.v_get_cells_for_field(field_id).await, - } + fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>> { + let index = self.database.lock().index_of_row(view_id, row_id); + let row_detail = self.database.lock().get_row_detail(row_id); + to_fut(async move { + match (index, row_detail) { + (Some(index), Some(row_detail)) => Some((index, Arc::new(row_detail))), + _ => None, + } + }) } - async fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Arc<RowCell> { - let cell = self.database.read().await.get_cell(field_id, row_id).await; - cell.into() + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> { + let database = self.database.clone(); + let view_id = view_id.to_string(); + to_fut(async move { + let cloned_database = database.clone(); + // offloads the blocking operation to a thread where blocking is acceptable. This prevents + // blocking the main asynchronous runtime + let row_orders = tokio::task::spawn_blocking(move || { + cloned_database.lock().get_row_orders_for_view(&view_id) + }) + .await + .unwrap_or_default(); + tokio::task::yield_now().await; + + let mut all_rows = vec![]; + + // Loading the rows in chunks of 10 rows in order to prevent blocking the main asynchronous runtime + for chunk in row_orders.chunks(10) { + let cloned_database = database.clone(); + let chunk = chunk.to_vec(); + let rows = tokio::task::spawn_blocking(move || { + let orders = cloned_database.lock().get_rows_from_row_orders(&chunk); + let lock_guard = cloned_database.lock(); + orders + .into_iter() + .flat_map(|row| lock_guard.get_row_detail(&row.id)) + .collect::<Vec<RowDetail>>() + }) + .await + .unwrap_or_default(); + + all_rows.extend(rows); + tokio::task::yield_now().await; + } + + all_rows.into_iter().map(Arc::new).collect() + }) } - async fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout { - self.database.read().await.get_database_view_layout(view_id) + fn remove_row(&self, row_id: &RowId) -> Option<Row> { + self.database.lock().remove_row(row_id) } - async fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting> { - self.database.read().await.get_all_group_setting(view_id) + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> { + let cells = self.database.lock().get_cells_for_field(view_id, field_id); + to_fut(async move { cells.into_iter().map(Arc::new).collect() }) } - async fn insert_group_setting(&self, view_id: &str, setting: GroupSetting) { + fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>> { + let cell = self.database.lock().get_cell(field_id, row_id); + to_fut(async move { Arc::new(cell) }) + } + + fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout { + self.database.lock().views.get_database_view_layout(view_id) + } + + fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting> { + self.database.lock().get_all_group_setting(view_id) + } + + fn insert_group_setting(&self, view_id: &str, setting: GroupSetting) { + self.database.lock().insert_group_setting(view_id, setting); + } + + fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Sort> { + self.database.lock().get_sort::<Sort>(view_id, sort_id) + } + + fn insert_sort(&self, view_id: &str, sort: Sort) { + self.database.lock().insert_sort(view_id, sort); + } + + fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) { self .database - .write() - .await - .insert_group_setting(view_id, setting); - } - - async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Sort> { - self - .database - .read() - .await - .get_sort::<Sort>(view_id, sort_id) - } - - async fn insert_sort(&self, view_id: &str, sort: Sort) { - self.database.write().await.insert_sort(view_id, sort); - } - - async fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) { - self - .database - .write() - .await + .lock() .move_sort(view_id, from_sort_id, to_sort_id); } - async fn remove_sort(&self, view_id: &str, sort_id: &str) { - self.database.write().await.remove_sort(view_id, sort_id); + fn remove_sort(&self, view_id: &str, sort_id: &str) { + self.database.lock().remove_sort(view_id, sort_id); } - async fn get_all_sorts(&self, view_id: &str) -> Vec<Sort> { - self.database.read().await.get_all_sorts::<Sort>(view_id) + fn get_all_sorts(&self, view_id: &str) -> Vec<Sort> { + self.database.lock().get_all_sorts::<Sort>(view_id) } - async fn remove_all_sorts(&self, view_id: &str) { - self.database.write().await.remove_all_sorts(view_id); + fn remove_all_sorts(&self, view_id: &str) { + self.database.lock().remove_all_sorts(view_id); } - async fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>> { + fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>> { self .database - .read() - .await + .lock() .get_all_calculations(view_id) .into_iter() .map(Arc::new) .collect() } - async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation> { + fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation> { self .database - .read() - .await + .lock() .get_calculation::<Calculation>(view_id, field_id) } - async fn get_all_filters(&self, view_id: &str) -> Vec<Filter> { + fn get_all_filters(&self, view_id: &str) -> Vec<Filter> { self .database - .read() - .await + .lock() .get_all_filters(view_id) .into_iter() .collect() } - async fn delete_filter(&self, view_id: &str, filter_id: &str) { - self - .database - .write() - .await - .remove_filter(view_id, filter_id); + fn delete_filter(&self, view_id: &str, filter_id: &str) { + self.database.lock().remove_filter(view_id, filter_id); } - async fn insert_filter(&self, view_id: &str, filter: Filter) { - self.database.write().await.insert_filter(view_id, &filter); + fn insert_filter(&self, view_id: &str, filter: Filter) { + self.database.lock().insert_filter(view_id, &filter); } - async fn save_filters(&self, view_id: &str, filters: &[Filter]) { + fn save_filters(&self, view_id: &str, filters: &[Filter]) { self .database - .write() - .await + .lock() .save_filters::<Filter, FilterMap>(view_id, filters); } - async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter> { + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter> { self .database - .read() - .await + .lock() .get_filter::<Filter>(view_id, filter_id) } - async fn get_layout_setting( - &self, - view_id: &str, - layout_ty: &DatabaseLayout, - ) -> Option<LayoutSetting> { - self - .database - .read() - .await - .get_layout_setting(view_id, layout_ty) + fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting> { + self.database.lock().get_layout_setting(view_id, layout_ty) } - async fn insert_layout_setting( + fn insert_layout_setting( &self, view_id: &str, layout_ty: &DatabaseLayout, @@ -2132,20 +1587,18 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { ) { self .database - .write() - .await + .lock() .insert_layout_setting(view_id, layout_ty, layout_setting); } - async fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout) { + fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout) { self .database - .write() - .await + .lock() .update_layout_type(view_id, layout_type); } - fn get_task_scheduler(&self) -> Arc<TokioRwLock<TaskDispatcher>> { + fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>> { self.task_scheduler.clone() } @@ -2156,14 +1609,14 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { TypeOptionCellExt::new(field, Some(self.cell_cache.clone())).get_type_option_cell_data_handler() } - async fn get_field_settings( + fn get_field_settings( &self, view_id: &str, field_ids: &[String], ) -> HashMap<String, FieldSettings> { let (layout_type, field_settings_map) = { - let database = self.database.read().await; - let layout_type = database.get_database_view_layout(view_id); + let database = self.database.lock(); + let layout_type = database.views.get_database_view_layout(view_id); let field_settings_map = database.get_field_settings(view_id, Some(field_ids)); (layout_type, field_settings_map) }; @@ -2194,20 +1647,19 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { field_settings } - async fn update_field_settings(&self, params: FieldSettingsChangesetPB) { - let field_settings_map = self - .get_field_settings(¶ms.view_id, &[params.field_id.clone()]) - .await; + fn update_field_settings(&self, params: FieldSettingsChangesetPB) { + let field_settings_map = self.get_field_settings(¶ms.view_id, &[params.field_id.clone()]); - let field_settings = match field_settings_map.get(¶ms.field_id).cloned() { - Some(field_settings) => field_settings, - None => { - let layout_type = self.get_layout_for_view(¶ms.view_id).await; + let field_settings = field_settings_map + .get(¶ms.field_id) + .cloned() + .unwrap_or_else(|| { + let layout_type = self.get_layout_for_view(¶ms.view_id); let default_field_settings = default_field_settings_by_layout_map(); let default_field_settings = default_field_settings.get(&layout_type).unwrap(); + FieldSettings::from_any_map(¶ms.field_id, layout_type, default_field_settings) - }, - }; + }); let new_field_settings = FieldSettings { visibility: params @@ -2220,13 +1672,13 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { ..field_settings }; - self.database.write().await.update_field_settings( + self.database.lock().update_field_settings( ¶ms.view_id, Some(vec![params.field_id]), new_field_settings.clone(), ); - database_notification_builder( + send_notification( ¶ms.view_id, DatabaseNotification::DidUpdateFieldSettings, ) @@ -2234,59 +1686,70 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { .send() } - async fn update_calculation(&self, view_id: &str, calculation: Calculation) { + fn update_calculation(&self, view_id: &str, calculation: Calculation) { self .database - .write() - .await + .lock() .update_calculation(view_id, calculation) } - async fn remove_calculation(&self, view_id: &str, field_id: &str) { - self - .database - .write() - .await - .remove_calculation(view_id, field_id) + fn remove_calculation(&self, view_id: &str, field_id: &str) { + self.database.lock().remove_calculation(view_id, field_id) } } #[tracing::instrument(level = "trace", skip_all, err)] pub async fn update_field_type_option_fn( - database: &mut Database, + database: &Arc<MutexDatabase>, + view_editors: &Vec<Arc<DatabaseViewEditor>>, type_option_data: TypeOptionData, - old_field: &Field, + old_field: Field, ) -> FlowyResult<()> { if type_option_data.is_empty() { warn!("Update type option with empty data"); return Ok(()); } let field_type = FieldType::from(old_field.field_type); - database.update_field(&old_field.id, |update| { - if old_field.is_primary { - warn!("Cannot update primary field type"); - } else { - update.update_type_options(|type_options_update| { - event!( - tracing::Level::TRACE, - "insert type option to field type: {:?}, {:?}", - field_type, - type_option_data - ); - type_options_update.insert(&field_type.to_string(), type_option_data); - }); - } - }); + database + .lock() + .fields + .update_field(&old_field.id, |update| { + if old_field.is_primary { + warn!("Cannot update primary field type"); + } else { + update.update_type_options(|type_options_update| { + event!( + tracing::Level::TRACE, + "insert type option to field type: {:?}, {:?}", + field_type, + type_option_data + ); + type_options_update.insert(&field_type.to_string(), type_option_data); + }); + } + }); let _ = notify_did_update_database_field(database, &old_field.id); + for view_editor in view_editors { + view_editor + .v_did_update_field_type_option(&old_field) + .await?; + } + Ok(()) } #[tracing::instrument(level = "trace", skip_all, err)] -fn notify_did_update_database_field(database: &Database, field_id: &str) -> FlowyResult<()> { +fn notify_did_update_database_field( + database: &Arc<MutexDatabase>, + field_id: &str, +) -> FlowyResult<()> { let (database_id, field, views) = { + let database = database + .try_lock() + .ok_or(FlowyError::internal().with_context("fail to acquire the lock of database"))?; let database_id = database.get_database_id(); - let field = database.get_field(field_id); + let field = database.fields.get_field(field_id); let views = database.get_all_database_views_meta(); (database_id, field, views) }; @@ -2297,31 +1760,14 @@ fn notify_did_update_database_field(database: &Database, field_id: &str) -> Flow DatabaseFieldChangesetPB::update(&database_id, vec![updated_field.clone()]); for view in views { - database_notification_builder(&view.id, DatabaseNotification::DidUpdateFields) + send_notification(&view.id, DatabaseNotification::DidUpdateFields) .payload(notified_changeset.clone()) .send(); } - database_notification_builder(field_id, DatabaseNotification::DidUpdateField) + send_notification(field_id, DatabaseNotification::DidUpdateField) .payload(updated_field) .send(); } Ok(()) } - -async fn database_row_evict_listener(key: Arc<String>, row: Weak<RwLock<DatabaseRow>>) { - remove_row_sync_plugin(key.as_str(), row).await -} - -async fn remove_row_sync_plugin(row_id: &str, row: Weak<RwLock<DatabaseRow>>) { - if let Some(row) = row.upgrade() { - let should_remove = row.read().await.has_cloud_plugin(); - if should_remove { - trace!("[Database]: un-finalize row: {}", row_id); - row - .write() - .await - .remove_plugins_for_types(vec![CollabPluginType::CloudStorage]); - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs index 1c965995ec..682001948d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_observe.rs @@ -1,34 +1,28 @@ use crate::entities::{DatabaseSyncStatePB, DidFetchRowPB, RowsChangePB}; -use crate::notification::{ - database_notification_builder, DatabaseNotification, DATABASE_OBSERVABLE_SOURCE, -}; -use crate::services::database::{DatabaseEditor, UpdatedRow}; -use crate::services::database_view::DatabaseViewEditor; -use collab::lock::RwLock; +use crate::notification::{send_notification, DatabaseNotification, DATABASE_OBSERVABLE_SOURCE}; +use crate::services::database::UpdatedRow; use collab_database::blocks::BlockEvent; -use collab_database::database::Database; +use collab_database::database::MutexDatabase; use collab_database::fields::FieldChange; use collab_database::rows::{RowChange, RowId}; -use collab_database::views::{DatabaseViewChange, RowOrder}; -use dashmap::DashMap; +use collab_database::views::DatabaseViewChange; use flowy_notification::{DebounceNotificationSender, NotificationBuilder}; use futures::StreamExt; - +use lib_dispatch::prelude::af_spawn; use std::sync::Arc; -use tracing::{error, trace, warn}; -use uuid::Uuid; +use tracing::{trace, warn}; -pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc<RwLock<Database>>) { +pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc<MutexDatabase>) { let weak_database = Arc::downgrade(database); - let mut sync_state = database.read().await.subscribe_sync_state(); + let mut sync_state = database.lock().subscribe_sync_state(); let database_id = database_id.to_string(); - tokio::spawn(async move { + af_spawn(async move { while let Some(sync_state) = sync_state.next().await { if weak_database.upgrade().is_none() { break; } - database_notification_builder( + send_notification( &database_id, DatabaseNotification::DidUpdateDatabaseSyncUpdate, ) @@ -38,269 +32,117 @@ pub(crate) async fn observe_sync_state(database_id: &str, database: &Arc<RwLock< }); } +#[allow(dead_code)] pub(crate) async fn observe_rows_change( database_id: &str, - database: &Arc<RwLock<Database>>, + database: &Arc<MutexDatabase>, notification_sender: &Arc<DebounceNotificationSender>, ) { let notification_sender = notification_sender.clone(); let database_id = database_id.to_string(); let weak_database = Arc::downgrade(database); - let sub = database.read().await.subscribe_row_change(); - if let Some(mut row_change) = sub { - tokio::spawn(async move { - while let Ok(row_change) = row_change.recv().await { + let mut row_change = database.lock().subscribe_row_change(); + af_spawn(async move { + while let Ok(row_change) = row_change.recv().await { + if let Some(database) = weak_database.upgrade() { trace!( "[Database Observe]: {} row change:{:?}", database_id, row_change ); - if let Some(database) = weak_database.upgrade() { - match row_change { - RowChange::DidUpdateCell { - field_id, - row_id, - value: _, - } => { - let cell_id = format!("{}:{}", row_id, field_id); - notify_cell(¬ification_sender, &cell_id); + match row_change { + RowChange::DidUpdateCell { + field_id, + row_id, + value: _, + } => { + let cell_id = format!("{}:{}", row_id, field_id); + notify_cell(¬ification_sender, &cell_id); - let views = database.read().await.get_all_database_views_meta(); - for view in views { - notify_row(¬ification_sender, &view.id, &field_id, &row_id); - } - }, - _ => { - warn!("unhandled row change: {:?}", row_change); - }, - } - } else { - trace!( - "[Database Observe]: {} row change: database dropped", - database_id - ); - break; - } - } - }); - } -} -#[allow(dead_code)] -pub(crate) async fn observe_field_change(database_id: &str, database: &Arc<RwLock<Database>>) { - let database_id = database_id.to_string(); - let weak_database = Arc::downgrade(database); - let sub = database.read().await.subscribe_field_change(); - if let Some(mut field_change) = sub { - tokio::spawn(async move { - while let Ok(field_change) = field_change.recv().await { - if weak_database.upgrade().is_none() { - break; - } - - trace!( - "[Database Observe]: {} field change:{:?}", - database_id, - field_change - ); - match field_change { - FieldChange::DidUpdateField { .. } => {}, - FieldChange::DidCreateField { .. } => {}, - FieldChange::DidDeleteField { .. } => {}, - } - } - }); - } -} - -#[allow(dead_code)] -pub(crate) async fn observe_view_change(database_id: &Uuid, database_editor: &Arc<DatabaseEditor>) { - let database_id = database_id.to_string(); - let weak_database_editor = Arc::downgrade(database_editor); - let view_change = database_editor - .database - .read() - .await - .subscribe_view_change(); - - if let Some(mut view_change) = view_change { - tokio::spawn(async move { - while let Ok(view_change) = view_change.recv().await { - trace!( - "[Database View Observe]: {} view change:{:?}", - database_id, - view_change - ); - match weak_database_editor.upgrade() { - None => break, - Some(database_editor) => match view_change { - DatabaseViewChange::DidCreateView { .. } => {}, - DatabaseViewChange::DidUpdateView { .. } => {}, - DatabaseViewChange::DidDeleteView { .. } => {}, - DatabaseViewChange::LayoutSettingChanged { .. } => {}, - DatabaseViewChange::DidUpdateRowOrders { - database_view_id, - is_local_change, - insert_row_orders, - delete_row_indexes, - } => { - handle_did_update_row_orders( - database_editor, - &database_view_id, - is_local_change, - insert_row_orders, - delete_row_indexes, - ) - .await; - }, - DatabaseViewChange::DidCreateFilters { .. } => {}, - DatabaseViewChange::DidUpdateFilter { .. } => {}, - DatabaseViewChange::DidCreateGroupSettings { .. } => {}, - DatabaseViewChange::DidUpdateGroupSetting { .. } => {}, - DatabaseViewChange::DidCreateSorts { .. } => {}, - DatabaseViewChange::DidUpdateSort { .. } => {}, - DatabaseViewChange::DidCreateFieldOrder { .. } => {}, - DatabaseViewChange::DidDeleteFieldOrder { .. } => {}, + let views = database.lock().get_all_database_views_meta(); + for view in views { + notify_row(¬ification_sender, &view.id, &field_id, &row_id); + } + }, + _ => { + warn!("unhandled row change: {:?}", row_change); }, } - } - }); - } -} - -async fn handle_did_update_row_orders( - database_editor: Arc<DatabaseEditor>, - view_id: &str, - is_local_change: bool, - insert_row_orders: Vec<(RowOrder, u32)>, - delete_row_indexes: Vec<u32>, -) { - // DidUpdateRowOrders is triggered whenever a user performs operations such as - // deleting, inserting, or moving a row in the database. - // - // Before DidUpdateRowOrders is called, the changes (insert/move/delete) have already been - // applied to the underlying database. This means the current order of rows reflects these updates. - // - // Example: - // Imagine the current state of rows is: - // Before any changes: [a, b, c] - // - // Operation: Move 'a' before 'c' - // Initial state: [a, b, c] - // - // Move 'a' to before 'c': This operation is divided into two parts: - // Insert row orders: Insert a at position 2 (right before c). - // Delete row indexes: Delete a from its original position (index 0). - // The steps are: - // - // Insert row: After inserting a at position 2, the rows temporarily look like this: - // Insert row orders: [(a, 2)] - // State after insert: [a, b, a, c] - // Delete row: Next, we delete a from its original position at index 0. - // Delete row indexes: [0] - // Final state after delete: [b, a, c] - let row_changes = DashMap::new(); - // 1. handle insert row orders - for (row_order, index) in insert_row_orders { - let row = match database_editor.init_database_row(&row_order.id).await { - Ok(database_row) => database_row.read().await.get_row().map(Arc::new), - Err(err) => { - error!("Failed to init row: {:?}", err); - None - }, - }; - - if let Some(view_editor) = database_editor - .database_views - .get_view_editor(view_id) - .await - { - trace!( - "[RowOrder]: insert row:{} at index:{}, is_local:{}", - row_order.id, - index, - is_local_change - ); - - // insert row order in database view cache - view_editor.insert_row(row.clone(), index, &row_order).await; - - let is_move_row = is_move_row(&view_editor, &row_order, &delete_row_indexes).await; - if let Some((index, row_detail)) = view_editor.v_get_row(&row_order.id).await { - view_editor - .v_did_create_row( - &row_detail, - index as u32, - is_move_row, - is_local_change, - &row_changes, - ) - .await; - } - } - } - - // handle delete row orders - for index in delete_row_indexes { - let index = index as usize; - if let Some(view_editor) = database_editor - .database_views - .get_view_editor(view_id) - .await - { - let mut view_row_orders = view_editor.row_orders.write().await; - if view_row_orders.len() > index { - let lazy_row = view_row_orders.remove(index); - // Update changeset in RowsChangePB - let row_id = lazy_row.id.to_string(); - let mut row_change = row_changes.entry(view_editor.view_id.clone()).or_default(); - row_change.deleted_rows.push(row_id); - - // notify the view - if let Some(row) = view_editor.row_by_row_id.get(lazy_row.id.as_str()) { - trace!( - "[RowOrder]: delete row:{} at index:{}, is_move_row: {}, is_local:{}", - row.id, - index, - row_change.is_move_row, - is_local_change - ); - view_editor - .v_did_delete_row(&row, row_change.is_move_row, is_local_change) - .await; - } else { - error!("[RowOrder]: row not found: {} in cache", lazy_row.id); - } } else { - warn!( - "[RowOrder]: delete row at index:{} out of range:{}", - index, - view_row_orders.len() - ); + break; } } - } + }); +} +#[allow(dead_code)] +pub(crate) async fn observe_field_change(database_id: &str, database: &Arc<MutexDatabase>) { + let database_id = database_id.to_string(); + let weak_database = Arc::downgrade(database); + let mut field_change = database.lock().subscribe_field_change(); + af_spawn(async move { + while let Ok(field_change) = field_change.recv().await { + if weak_database.upgrade().is_none() { + break; + } - // 3. notify the view - for entry in row_changes.into_iter() { - let (view_id, changes) = entry; - trace!("[RowOrder]: {}", changes); - database_notification_builder(&view_id, DatabaseNotification::DidUpdateRow) - .payload(changes) - .send(); - } + trace!( + "[Database Observe]: {} field change:{:?}", + database_id, + field_change + ); + match field_change { + FieldChange::DidUpdateField { .. } => {}, + FieldChange::DidCreateField { .. } => {}, + FieldChange::DidDeleteField { .. } => {}, + } + } + }); } -pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Arc<DatabaseEditor>) { +#[allow(dead_code)] +pub(crate) async fn observe_view_change(database_id: &str, database: &Arc<MutexDatabase>) { let database_id = database_id.to_string(); - let mut block_event_rx = database_editor - .database - .read() - .await - .subscribe_block_event(); - let database_editor = Arc::downgrade(database_editor); - tokio::spawn(async move { + let weak_database = Arc::downgrade(database); + let mut view_change = database.lock().subscribe_view_change(); + af_spawn(async move { + while let Ok(view_change) = view_change.recv().await { + if weak_database.upgrade().is_none() { + break; + } + + trace!( + "[Database Observe]: {} view change:{:?}", + database_id, + view_change + ); + match view_change { + DatabaseViewChange::DidCreateView { .. } => {}, + DatabaseViewChange::DidUpdateView { .. } => {}, + DatabaseViewChange::DidDeleteView { .. } => {}, + DatabaseViewChange::LayoutSettingChanged { .. } => {}, + DatabaseViewChange::DidInsertRowOrders { .. } => {}, + DatabaseViewChange::DidDeleteRowAtIndex { .. } => {}, + DatabaseViewChange::DidCreateFilters { .. } => {}, + DatabaseViewChange::DidUpdateFilter { .. } => {}, + DatabaseViewChange::DidCreateGroupSettings { .. } => {}, + DatabaseViewChange::DidUpdateGroupSetting { .. } => {}, + DatabaseViewChange::DidCreateSorts { .. } => {}, + DatabaseViewChange::DidUpdateSort { .. } => {}, + DatabaseViewChange::DidCreateFieldOrder { .. } => {}, + DatabaseViewChange::DidDeleteFieldOrder { .. } => {}, + } + } + }); +} + +#[allow(dead_code)] +pub(crate) async fn observe_block_event(database_id: &str, database: &Arc<MutexDatabase>) { + let database_id = database_id.to_string(); + let weak_database = Arc::downgrade(database); + let mut block_event_rx = database.lock().subscribe_block_event(); + af_spawn(async move { while let Ok(event) = block_event_rx.recv().await { - if database_editor.upgrade().is_none() { + if weak_database.upgrade().is_none() { break; } @@ -315,7 +157,7 @@ pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Ar trace!("Did fetch row: {:?}", row_detail.row.id); let row_id = row_detail.row.id.clone(); let pb = DidFetchRowPB::from(row_detail); - database_notification_builder(&row_id, DatabaseNotification::DidFetchRow) + send_notification(&row_id, DatabaseNotification::DidFetchRow) .payload(pb) .send(); } @@ -325,6 +167,7 @@ pub(crate) async fn observe_block_event(database_id: &Uuid, database_editor: &Ar }); } +#[allow(dead_code)] fn notify_row( notification_sender: &Arc<DebounceNotificationSender>, view_id: &str, @@ -352,26 +195,3 @@ fn notify_cell(notification_sender: &Arc<DebounceNotificationSender>, cell_id: & .build(); notification_sender.send_subject(subject); } - -async fn is_move_row( - database_view: &Arc<DatabaseViewEditor>, - insert_row_order: &RowOrder, - delete_row_indexes: &[u32], -) -> bool { - let mut is_move_row = false; - for index in delete_row_indexes.iter() { - is_move_row = database_view - .row_orders - .read() - .await - .get(*index as usize) - .map(|deleted_row_order| deleted_row_order == insert_row_order) - .unwrap_or(false); - - if is_move_row { - break; - } - } - - is_move_row -} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/util.rs b/frontend/rust-lib/flowy-database2/src/services/database/util.rs index 2c6b012747..4a3810b198 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/util.rs @@ -1,3 +1,5 @@ +use collab_database::views::{DatabaseLayout, DatabaseView}; + use crate::entities::{ DatabaseLayoutPB, DatabaseLayoutSettingPB, DatabaseViewSettingPB, FieldSettingsPB, FilterPB, GroupSettingPB, SortPB, @@ -6,9 +8,6 @@ use crate::services::field_settings::FieldSettings; use crate::services::filter::Filter; use crate::services::group::GroupSetting; use crate::services::sort::Sort; -use collab_database::entity::DatabaseView; -use collab_database::views::DatabaseLayout; -use tracing::error; pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> DatabaseViewSettingPB { let layout_type: DatabaseLayoutPB = view.layout.into(); @@ -33,10 +32,7 @@ pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> Database .into_iter() .flat_map(|value| match Filter::try_from(value) { Ok(filter) => Some(FilterPB::from(&filter)), - Err(err) => { - error!("Error converting filter: {:?}", err); - None - }, + Err(_) => None, }) .collect::<Vec<FilterPB>>(); @@ -45,10 +41,7 @@ pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> Database .into_iter() .flat_map(|value| match GroupSetting::try_from(value) { Ok(setting) => Some(GroupSettingPB::from(&setting)), - Err(err) => { - error!("Error converting group setting: {:?}", err); - None - }, + Err(_) => None, }) .collect::<Vec<GroupSettingPB>>(); @@ -57,10 +50,7 @@ pub(crate) fn database_view_setting_pb_from_view(view: DatabaseView) -> Database .into_iter() .flat_map(|value| match Sort::try_from(value) { Ok(sort) => Some(SortPB::from(&sort)), - Err(err) => { - error!("Error converting sort: {:?}", err); - None - }, + Err(_) => None, }) .collect::<Vec<SortPB>>(); diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs index 9316726663..5d5e3b4c3f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/layout_deps.rs @@ -1,87 +1,67 @@ +use collab_database::database::{gen_field_id, MutexDatabase}; +use collab_database::fields::Field; +use collab_database::views::{DatabaseLayout, LayoutSetting, OrderObjectPosition}; +use std::sync::Arc; + use crate::entities::FieldType; +use crate::services::field::{DateTypeOption, SingleSelectTypeOption}; use crate::services::field_settings::default_field_settings_by_layout_map; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; -use collab::lock::RwLock; -use collab_database::database::{gen_field_id, Database}; -use collab_database::fields::date_type_option::DateTypeOption; -use collab_database::fields::select_type_option::SingleSelectTypeOption; -use collab_database::fields::Field; -use collab_database::views::{ - DatabaseLayout, FieldSettingsByFieldIdMap, LayoutSetting, OrderObjectPosition, -}; -use std::sync::Arc; /// When creating a database, we need to resolve the dependencies of the views. /// Different database views have different dependencies. For example, a board /// view depends on a field that can be used to group rows while a calendar view /// depends on a date field. pub struct DatabaseLayoutDepsResolver { - pub database: Arc<RwLock<Database>>, + pub database: Arc<MutexDatabase>, /// The new database layout. pub database_layout: DatabaseLayout, } impl DatabaseLayoutDepsResolver { - pub fn new(database: Arc<RwLock<Database>>, database_layout: DatabaseLayout) -> Self { + pub fn new(database: Arc<MutexDatabase>, database_layout: DatabaseLayout) -> Self { Self { database, database_layout, } } - pub async fn resolve_deps_when_create_database_linked_view( + pub fn resolve_deps_when_create_database_linked_view( &self, - view_id: &str, - ) -> ( - Option<Field>, - Option<LayoutSetting>, - Option<FieldSettingsByFieldIdMap>, - ) { + ) -> (Option<Field>, Option<LayoutSetting>) { match self.database_layout { - DatabaseLayout::Grid => (None, None, None), + DatabaseLayout::Grid => (None, None), DatabaseLayout::Board => { let layout_settings = BoardLayoutSetting::new().into(); - - let database = self.database.read().await; - let field = if !database + if !self + .database + .lock() .get_fields(None) .into_iter() .any(|field| FieldType::from(field.field_type).can_be_group()) { - Some(self.create_select_field()) + let select_field = self.create_select_field(); + (Some(select_field), Some(layout_settings)) } else { - None - }; - - let field_settings_map = database.get_field_settings(view_id, None); - tracing::info!( - "resolve_deps_when_create_database_linked_view {:?}", - field_settings_map - ); - - ( - field, - Some(layout_settings), - Some(field_settings_map.into()), - ) + (None, Some(layout_settings)) + } }, DatabaseLayout::Calendar => { match self .database - .read() - .await + .lock() .get_fields(None) .into_iter() .find(|field| FieldType::from(field.field_type) == FieldType::DateTime) { Some(field) => { let layout_setting = CalendarLayoutSetting::new(field.id).into(); - (None, Some(layout_setting), None) + (None, Some(layout_setting)) }, None => { let date_field = self.create_date_field(); let layout_setting = CalendarLayoutSetting::new(date_field.clone().id).into(); - (Some(date_field), Some(layout_setting), None) + (Some(date_field), Some(layout_setting)) }, } }, @@ -90,20 +70,13 @@ impl DatabaseLayoutDepsResolver { /// If the new layout type is a calendar and there is not date field in the database, it will add /// a new date field to the database and create the corresponding layout setting. - pub async fn resolve_deps_when_update_layout_type(&self, view_id: &str) { - let mut database = self.database.write().await; - let fields = database.get_fields(None); + pub fn resolve_deps_when_update_layout_type(&self, view_id: &str) { + let fields = self.database.lock().get_fields(None); // Insert the layout setting if it's not exist match &self.database_layout { DatabaseLayout::Grid => {}, DatabaseLayout::Board => { - if database - .get_layout_setting::<BoardLayoutSetting>(view_id, &self.database_layout) - .is_none() - { - let layout_setting = BoardLayoutSetting::new(); - database.insert_layout_setting(view_id, &self.database_layout, layout_setting); - } + self.create_board_layout_setting_if_need(view_id); }, DatabaseLayout::Calendar => { let date_field_id = match fields @@ -114,7 +87,7 @@ impl DatabaseLayoutDepsResolver { tracing::trace!("Create a new date field after layout type change"); let field = self.create_date_field(); let field_id = field.id.clone(); - database.create_field( + self.database.lock().create_field( None, field, &OrderObjectPosition::End, @@ -124,17 +97,41 @@ impl DatabaseLayoutDepsResolver { }, Some(date_field) => date_field.id, }; - if database - .get_layout_setting::<CalendarLayoutSetting>(view_id, &self.database_layout) - .is_none() - { - let layout_setting = CalendarLayoutSetting::new(date_field_id); - database.insert_layout_setting(view_id, &self.database_layout, layout_setting); - } + self.create_calendar_layout_setting_if_need(view_id, &date_field_id); }, } } + fn create_board_layout_setting_if_need(&self, view_id: &str) { + if self + .database + .lock() + .get_layout_setting::<BoardLayoutSetting>(view_id, &self.database_layout) + .is_none() + { + let layout_setting = BoardLayoutSetting::new(); + self + .database + .lock() + .insert_layout_setting(view_id, &self.database_layout, layout_setting); + } + } + + fn create_calendar_layout_setting_if_need(&self, view_id: &str, field_id: &str) { + if self + .database + .lock() + .get_layout_setting::<CalendarLayoutSetting>(view_id, &self.database_layout) + .is_none() + { + let layout_setting = CalendarLayoutSetting::new(field_id.to_string()); + self + .database + .lock() + .insert_layout_setting(view_id, &self.database_layout, layout_setting); + } + } + fn create_date_field(&self) -> Field { let field_type = FieldType::DateTime; let default_date_type_option = DateTypeOption::default(); @@ -151,3 +148,27 @@ impl DatabaseLayoutDepsResolver { .with_type_option_data(field_type, default_select_type_option.into()) } } + +// pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams { +// let mut layout_setting = LayoutSettingParams::default(); +// match layout_ty { +// DatabaseLayout::Grid => {}, +// DatabaseLayout::Board => {}, +// DatabaseLayout::Calendar => { +// if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { +// let calendar_setting = CalendarLayoutSetting::from(value); +// // Check the field exist or not +// if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { +// let field_type = FieldType::from(field.field_type); +// +// // Check the type of field is Datetime or not +// if field_type == FieldType::DateTime { +// layout_setting.calendar = Some(calendar_setting); +// } +// } +// } +// }, +// } +// +// layout_setting +// } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs index a3a5e6f484..99c3efa45d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs @@ -1,21 +1,22 @@ #![allow(clippy::while_let_loop)] use crate::entities::{ CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB, - GroupChangesPB, GroupRowsNotificationPB, ReorderAllRowsPB, ReorderSingleRowPB, - RowsVisibilityChangePB, SortChangesetNotificationPB, + GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, ReorderAllRowsPB, ReorderSingleRowPB, + RowMetaPB, RowsChangePB, RowsVisibilityChangePB, SortChangesetNotificationPB, }; -use crate::notification::{database_notification_builder, DatabaseNotification}; +use crate::notification::{send_notification, DatabaseNotification}; use crate::services::filter::FilterResultNotification; -use crate::services::sort::{ReorderAllRowsResult, ReorderSingleRowResult}; +use crate::services::sort::{InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult}; use async_stream::stream; use futures::stream::StreamExt; use tokio::sync::broadcast; -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum DatabaseViewChanged { FilterNotification(FilterResultNotification), ReorderAllRowsNotification(ReorderAllRowsResult), ReorderSingleRowNotification(ReorderSingleRowResult), + InsertRowNotification(InsertRowResult), CalculationValueNotification(CalculationChangesetNotificationPB), } @@ -50,7 +51,7 @@ impl DatabaseViewChangedReceiverRunner { .collect(), }; - database_notification_builder( + send_notification( &changeset.view_id, DatabaseNotification::DidUpdateViewRowsVisibility, ) @@ -61,12 +62,9 @@ impl DatabaseViewChangedReceiverRunner { let row_orders = ReorderAllRowsPB { row_orders: notification.row_orders, }; - database_notification_builder( - ¬ification.view_id, - DatabaseNotification::DidReorderRows, - ) - .payload(row_orders) - .send() + send_notification(¬ification.view_id, DatabaseNotification::DidReorderRows) + .payload(row_orders) + .send() }, DatabaseViewChanged::ReorderSingleRowNotification(notification) => { let reorder_row = ReorderSingleRowPB { @@ -74,21 +72,30 @@ impl DatabaseViewChangedReceiverRunner { old_index: notification.old_index as i32, new_index: notification.new_index as i32, }; - database_notification_builder( + send_notification( ¬ification.view_id, DatabaseNotification::DidReorderSingleRow, ) .payload(reorder_row) .send() }, - DatabaseViewChanged::CalculationValueNotification(notification) => { - database_notification_builder( - ¬ification.view_id, - DatabaseNotification::DidUpdateCalculation, - ) - .payload(notification) - .send() + DatabaseViewChanged::InsertRowNotification(result) => { + let inserted_row = InsertedRowPB { + row_meta: RowMetaPB::from(result.row), + index: Some(result.index as i32), + is_new: true, + }; + let changes = RowsChangePB::from_insert(inserted_row); + send_notification(&result.view_id, DatabaseNotification::DidUpdateRow) + .payload(changes) + .send(); }, + DatabaseViewChanged::CalculationValueNotification(notification) => send_notification( + ¬ification.view_id, + DatabaseNotification::DidUpdateCalculation, + ) + .payload(notification) + .send(), } }) .await; @@ -96,19 +103,19 @@ impl DatabaseViewChangedReceiverRunner { } pub async fn notify_did_update_group_rows(payload: GroupRowsNotificationPB) { - database_notification_builder(&payload.group_id, DatabaseNotification::DidUpdateGroupRow) + send_notification(&payload.group_id, DatabaseNotification::DidUpdateGroupRow) .payload(payload) .send(); } pub async fn notify_did_update_filter(notification: FilterChangesetNotificationPB) { - database_notification_builder(¬ification.view_id, DatabaseNotification::DidUpdateFilter) + send_notification(¬ification.view_id, DatabaseNotification::DidUpdateFilter) .payload(notification) .send(); } pub async fn notify_did_update_calculation(notification: CalculationChangesetNotificationPB) { - database_notification_builder( + send_notification( ¬ification.view_id, DatabaseNotification::DidUpdateCalculation, ) @@ -118,20 +125,20 @@ pub async fn notify_did_update_calculation(notification: CalculationChangesetNot pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) { if !notification.is_empty() { - database_notification_builder(¬ification.view_id, DatabaseNotification::DidUpdateSort) + send_notification(¬ification.view_id, DatabaseNotification::DidUpdateSort) .payload(notification) .send(); } } pub(crate) async fn notify_did_update_num_of_groups(view_id: &str, changeset: GroupChangesPB) { - database_notification_builder(view_id, DatabaseNotification::DidUpdateNumOfGroups) + send_notification(view_id, DatabaseNotification::DidUpdateNumOfGroups) .payload(changeset) .send(); } pub(crate) async fn notify_did_update_setting(view_id: &str, setting: DatabaseViewSettingPB) { - database_notification_builder(view_id, DatabaseNotification::DidUpdateSettings) + send_notification(view_id, DatabaseNotification::DidUpdateSettings) .payload(setting) .send(); } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs index 301be9fbe9..32ddecc667 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs @@ -1,8 +1,8 @@ -use async_trait::async_trait; use collab_database::fields::Field; use std::sync::Arc; -use collab_database::rows::Cell; +use collab_database::rows::RowCell; +use lib_infra::future::{to_fut, Fut}; use crate::services::calculations::{ Calculation, CalculationsController, CalculationsDelegate, CalculationsTaskHandler, @@ -17,7 +17,7 @@ pub async fn make_calculations_controller( delegate: Arc<dyn DatabaseViewOperation>, notifier: DatabaseViewChangedNotifier, ) -> Arc<CalculationsController> { - let calculations = delegate.get_all_calculations(view_id).await; + let calculations = delegate.get_all_calculations(view_id); let task_scheduler = delegate.get_task_scheduler(); let calculations_delegate = DatabaseViewCalculationsDelegateImpl(delegate.clone()); let handler_id = gen_handler_id(); @@ -29,7 +29,8 @@ pub async fn make_calculations_controller( calculations, task_scheduler.clone(), notifier, - ); + ) + .await; let calculations_controller = Arc::new(calculations_controller); task_scheduler @@ -44,39 +45,30 @@ pub async fn make_calculations_controller( struct DatabaseViewCalculationsDelegateImpl(Arc<dyn DatabaseViewOperation>); -#[async_trait] impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl { - async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec<Arc<Cell>> { - self - .0 - .get_cells_for_field(view_id, field_id) - .await - .into_iter() - .filter_map(|row_cell| row_cell.cell.map(Arc::new)) - .collect() + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> { + self.0.get_cells_for_field(view_id, field_id) } - async fn get_field(&self, field_id: &str) -> Option<Field> { - self.0.get_field(field_id).await + fn get_field(&self, field_id: &str) -> Option<Field> { + self.0.get_field(field_id) } - async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Arc<Calculation>> { - self - .0 - .get_calculation(view_id, field_id) - .await - .map(Arc::new) + fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut<Option<Arc<Calculation>>> { + let calculation = self.0.get_calculation(view_id, field_id).map(Arc::new); + to_fut(async move { calculation }) } - async fn update_calculation(&self, view_id: &str, calculation: Calculation) { - self.0.update_calculation(view_id, calculation).await + fn update_calculation(&self, view_id: &str, calculation: Calculation) { + self.0.update_calculation(view_id, calculation) } - async fn remove_calculation(&self, view_id: &str, calculation_id: &str) { - self.0.remove_calculation(view_id, calculation_id).await + fn remove_calculation(&self, view_id: &str, calculation_id: &str) { + self.0.remove_calculation(view_id, calculation_id) } - async fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>> { - self.0.get_all_calculations(view_id).await + fn get_all_calculations(&self, view_id: &str) -> Fut<Arc<Vec<Arc<Calculation>>>> { + let calculations = Arc::new(self.0.get_all_calculations(view_id)); + to_fut(async move { calculations }) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 8b9b7032c7..1d5d8cf1b4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -2,21 +2,32 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use super::{notify_did_update_calculation, DatabaseViewChanged}; +use collab_database::database::{gen_database_calculation_id, gen_database_sort_id, gen_row_id}; +use collab_database::fields::Field; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView}; +use lib_infra::util::timestamp; +use tokio::sync::{broadcast, RwLock}; +use tracing::instrument; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::af_spawn; + use crate::entities::{ - CalculationChangesetNotificationPB, CalendarEventPB, CreateRowPayloadPB, DatabaseLayoutMetaPB, + CalendarEventPB, CreateRowParams, CreateRowPayloadPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteSortPayloadPB, FieldSettingsChangesetPB, FieldType, - GroupChangesPB, GroupPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, + GroupChangesPB, GroupPB, LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, UpdateSortPayloadPB, }; -use crate::notification::{database_notification_builder, DatabaseNotification}; +use crate::notification::{send_notification, DatabaseNotification}; use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController}; use crate::services::cell::{CellBuilder, CellCache}; use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; -use crate::services::database_view::view_calculations::make_calculations_controller; use crate::services::database_view::view_filter::make_filter_controller; -use crate::services::database_view::view_group::{get_cell_for_row, new_group_controller}; +use crate::services::database_view::view_group::{ + get_cell_for_row, get_cells_for_field, new_group_controller, +}; use crate::services::database_view::view_operation::DatabaseViewOperation; use crate::services::database_view::view_sort::make_sort_controller; use crate::services::database_view::{ @@ -26,22 +37,12 @@ use crate::services::database_view::{ }; use crate::services::field_settings::FieldSettings; use crate::services::filter::{Filter, FilterChangeset, FilterController}; -use crate::services::group::{ - DidMoveGroupRowResult, GroupChangeset, GroupController, MoveGroupRowContext, UpdatedCells, -}; +use crate::services::group::{GroupChangeset, GroupController, MoveGroupRowContext, RowChangeset}; use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{Sort, SortChangeset, SortController}; -use collab_database::database::{gen_database_calculation_id, gen_database_sort_id, gen_row_id}; -use collab_database::entity::DatabaseView; -use collab_database::fields::Field; -use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId}; -use collab_database::views::{DatabaseLayout, RowOrder}; -use dashmap::DashMap; -use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::util::timestamp; -use tokio::sync::{broadcast, RwLock}; -use tracing::{error, instrument, trace, warn}; +use super::notify_did_update_calculation; +use super::view_calculations::make_calculations_controller; pub struct DatabaseViewEditor { database_id: String, @@ -51,13 +52,6 @@ pub struct DatabaseViewEditor { filter_controller: Arc<FilterController>, sort_controller: Arc<RwLock<SortController>>, calculations_controller: Arc<CalculationsController>, - /// Use lazy_rows as cache that represents the row's order for given view - /// It can't get the row id when deleting a row. it only returns the deleted index. - /// So using this cache to get the row id by index - /// - /// Check out this link (https://github.com/y-crdt/y-crdt/issues/341) for more information. - pub(crate) row_orders: RwLock<Vec<RowOrder>>, - pub(crate) row_by_row_id: DashMap<String, Arc<Row>>, pub notifier: DatabaseViewChangedNotifier, } @@ -68,14 +62,6 @@ impl Drop for DatabaseViewEditor { } impl DatabaseViewEditor { - /// Create a new Database View Editor. - /// - /// After creating the editor, you must call [DatabaseViewEditor::initialize] to properly initialize it. - /// This initialization step will load essential data, such as group information. - /// - /// Avoid calling any methods of [DatabaseViewOperation] before the editor is fully initialized, - /// as some actions may rely on the current editor state. Failing to follow this order could result - /// in unexpected behavior, including potential deadlocks. pub async fn new( database_id: String, view_id: String, @@ -83,7 +69,7 @@ impl DatabaseViewEditor { cell_cache: CellCache, ) -> FlowyResult<Self> { let (notifier, _) = broadcast::channel(100); - tokio::spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); + af_spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); // Filter let filter_controller = make_filter_controller( @@ -127,61 +113,16 @@ impl DatabaseViewEditor { filter_controller, sort_controller, calculations_controller, - row_orders: Default::default(), - row_by_row_id: Default::default(), notifier, }) } - /// Initialize the editor after creating it - /// You should call [DatabaseViewEditor::initialize] after creating the editor - pub async fn initialize(&self) -> FlowyResult<()> { - if let Some(group) = self.group_controller.write().await.as_mut() { - group.load_group_data().await?; - } - - Ok(()) - } - - pub async fn insert_row(&self, row: Option<Arc<Row>>, index: u32, row_order: &RowOrder) { - let mut row_orders = self.row_orders.write().await; - if row_orders.len() >= index as usize { - row_orders.insert(index as usize, row_order.clone()); - } else { - warn!( - "[RowOrder]: insert row at index:{} out of range:{}", - index, - row_orders.len() - ); - } - if let Some(row) = row { - self.row_by_row_id.insert(row.id.to_string(), row); - } - } - - pub async fn set_row_orders(&self, row_orders: Vec<RowOrder>) { - *self.row_orders.write().await = row_orders; - } - - pub async fn get_all_row_orders(&self) -> FlowyResult<Vec<RowOrder>> { - let row_orders = self.delegate.get_all_row_orders(&self.view_id).await; - Ok(row_orders) - } - pub async fn close(&self) { self.sort_controller.write().await.close().await; self.filter_controller.close().await; self.calculations_controller.close().await; } - pub async fn has_filters(&self) -> bool { - self.filter_controller.has_filters().await - } - - pub async fn has_sorts(&self) -> bool { - self.sort_controller.read().await.has_sorts().await - } - pub async fn v_get_view(&self) -> Option<DatabaseView> { self.delegate.get_view(&self.view_id).await } @@ -191,16 +132,18 @@ impl DatabaseViewEditor { params: CreateRowPayloadPB, ) -> FlowyResult<CreateRowParams> { let timestamp = timestamp(); - trace!("[Database]: will create row at: {:?}", params.row_position); let mut result = CreateRowParams { - id: gen_row_id(), - database_id: self.database_id.clone(), - cells: Cells::new(), - height: 60, - visibility: true, - row_position: params.row_position.try_into()?, - created_at: timestamp, - modified_at: timestamp, + collab_params: collab_database::rows::CreateRowParams { + id: gen_row_id(), + database_id: self.database_id.clone(), + cells: Cells::new(), + height: 60, + visibility: true, + row_position: params.row_position.try_into()?, + created_at: timestamp, + modified_at: timestamp, + }, + open_after_create: false, }; // fill in cells from the frontend @@ -213,7 +156,6 @@ impl DatabaseViewEditor { let field = self .delegate .get_field(controller.get_grouping_field_id()) - .await .ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?; controller.will_create_row(&mut cells, &field, &group_id); } @@ -223,89 +165,72 @@ impl DatabaseViewEditor { let filter_controller = self.filter_controller.clone(); filter_controller.fill_cells(&mut cells).await; - result.cells = cells; + result.collab_params.cells = cells; + Ok(result) } pub async fn v_did_update_row_meta(&self, row_id: &RowId, row_detail: &RowDetail) { - let rows = vec![Arc::new(row_detail.row.clone())]; - let mut rows = self.v_filter_rows(rows).await; - if rows.pop().is_some() { - let update_row = UpdatedRow::new(row_id.as_str()).with_row_meta(row_detail.clone()); - let changeset = RowsChangePB::from_update(update_row.into()); - database_notification_builder(&self.view_id, DatabaseNotification::DidUpdateRow) - .payload(changeset) - .send(); - } + let update_row = UpdatedRow::new(row_id.as_str()).with_row_meta(row_detail.clone()); + let changeset = RowsChangePB::from_update(update_row.into()); + send_notification(&self.view_id, DatabaseNotification::DidUpdateRow) + .payload(changeset) + .send(); } - pub async fn v_did_create_row( - &self, - row_detail: &RowDetail, - index: u32, - is_move_row: bool, - is_local_change: bool, - row_changes: &DashMap<String, RowsChangePB>, - ) { + pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) { // Send the group notification if the current view has groups if let Some(controller) = self.group_controller.write().await.as_mut() { - let rows = vec![Arc::new(row_detail.row.clone())]; - let mut rows = self.v_filter_rows(rows).await; - if let Some(row) = rows.pop() { - let changesets = controller.did_create_row(&row, index as usize); + let mut row_details = vec![Arc::new(row_detail.clone())]; + self.v_filter_rows(&mut row_details).await; + + if let Some(row_detail) = row_details.pop() { + let changesets = controller.did_create_row(&row_detail, index); + for changeset in changesets { notify_did_update_group_rows(changeset).await; } } } - let index = self - .sort_controller - .write() - .await - .did_create_row(&row_detail.row) - .await; - - row_changes - .entry(self.view_id.clone()) - .or_insert_with(|| { - let mut change = RowsChangePB::new(); - change.is_move_row = is_move_row; - change - }) - .inserted_rows - .push(InsertedRowPB { - row_meta: RowMetaPB::from(row_detail), - index: index.map(|index| index as i32), - is_new: true, - is_hidden_in_view: is_local_change && index.is_none(), - }); - self - .gen_did_create_row_view_tasks(row_detail.row.clone()) + .gen_did_create_row_view_tasks(index, row_detail.clone()) .await; } #[tracing::instrument(level = "trace", skip_all)] - pub async fn v_did_delete_row(&self, row: &Row, is_move_row: bool, is_local_change: bool) { + pub async fn v_did_delete_row(&self, row: &Row) { let deleted_row = row.clone(); - // Only update group rows - // 1. when the row is deleted locally. If the row is moved, we don't need to send the group - // notification. Because it's handled by the move_group_row function - // 2. when the row is deleted remotely - if !is_move_row || !is_local_change { - // Send the group notification if the current view has groups; - let result = self - .mut_group_controller(|group_controller, _| group_controller.did_delete_row(row)) - .await; - handle_mut_group_result(&self.view_id, result).await; + // Send the group notification if the current view has groups; + let result = self + .mut_group_controller(|group_controller, _| group_controller.did_delete_row(row)) + .await; + + if let Some(result) = result { + tracing::trace!("Delete row in view changeset: {:?}", result); + for changeset in result.row_changesets { + notify_did_update_group_rows(changeset).await; + } + if let Some(deleted_group) = result.deleted_group { + let payload = GroupChangesPB { + view_id: self.view_id.clone(), + deleted_groups: vec![deleted_group.group_id], + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, payload).await; + } } + let changes = RowsChangePB::from_delete(row.id.clone().into_inner()); + + send_notification(&self.view_id, DatabaseNotification::DidUpdateRow) + .payload(changes) + .send(); // Updating calculations for each of the Rows cells is a tedious task // Therefore we spawn a separate task for this let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); - tokio::spawn(async move { + af_spawn(async move { if let Some(calculations_controller) = weak_calculations_controller.upgrade() { calculations_controller .did_receive_row_changed(deleted_row) @@ -317,56 +242,46 @@ impl DatabaseViewEditor { /// Notify the view that the row has been updated. If the view has groups, /// send the group notification with [GroupRowsNotificationPB]. Otherwise, /// send the view notification with [RowsChangePB] - #[instrument(level = "trace", skip_all)] - pub async fn v_did_update_row(&self, old_row: &Option<Row>, row: &Row, field_id: Option<String>) { + pub async fn v_did_update_row( + &self, + old_row: &Option<RowDetail>, + row_detail: &RowDetail, + field_id: Option<String>, + ) { if let Some(controller) = self.group_controller.write().await.as_mut() { - let field = self - .delegate - .get_field(controller.get_grouping_field_id()) - .await; + let field = self.delegate.get_field(controller.get_grouping_field_id()); if let Some(field) = field { - let rows = vec![Arc::new(row.clone())]; - let mut rows = self.v_filter_rows(rows).await; + let mut row_details = vec![Arc::new(row_detail.clone())]; + self.v_filter_rows(&mut row_details).await; - let mut group_changes = GroupChangesPB { - view_id: self.view_id.clone(), - ..Default::default() - }; + if let Some(row_detail) = row_details.pop() { + let result = controller.did_update_group_row(old_row, &row_detail, &field); - let (inserted_group, deleted_group, row_changesets) = if let Some(row) = rows.pop() { - if let Ok(result) = controller.did_update_group_row(old_row, &row, &field) { - ( - result.inserted_group, - result.deleted_group, - result.row_changesets, - ) - } else { - (None, None, vec![]) - } - } else if let Ok(result) = controller.did_delete_row(row) { - (None, result.deleted_group, result.row_changesets) - } else { - (None, None, vec![]) - }; + if let Ok(result) = result { + let mut group_changes = GroupChangesPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + if let Some(inserted_group) = result.inserted_group { + tracing::trace!("Create group after editing the row: {:?}", inserted_group); + group_changes.inserted_groups.push(inserted_group); + } + if let Some(delete_group) = result.deleted_group { + tracing::trace!("Delete group after editing the row: {:?}", delete_group); + group_changes.deleted_groups.push(delete_group.group_id); + } - if let Some(inserted_group) = inserted_group { - tracing::trace!("Create group after editing the row: {:?}", inserted_group); - group_changes.inserted_groups.push(inserted_group); - } - if let Some(delete_group) = deleted_group { - tracing::trace!("Delete group after editing the row: {:?}", delete_group); - group_changes.deleted_groups.push(delete_group.group_id); - } + if !group_changes.is_empty() { + notify_did_update_num_of_groups(&self.view_id, group_changes).await; + } - if !group_changes.is_empty() { - notify_did_update_num_of_groups(&self.view_id, group_changes).await; - } - - for changeset in row_changesets { - if !changeset.is_empty() { - tracing::trace!("Group change after editing the row: {:?}", changeset); - notify_did_update_group_rows(changeset).await; + for changeset in result.row_changesets { + if !changeset.is_empty() { + tracing::trace!("Group change after editing the row: {:?}", changeset); + notify_did_update_group_rows(changeset).await; + } + } } } } @@ -374,78 +289,46 @@ impl DatabaseViewEditor { // Each row update will trigger a calculations, filter and sort operation. We don't want // to block the main thread, so we spawn a new task to do the work. - self - .gen_did_update_row_view_tasks(row.id.clone(), field_id) - .await; + if let Some(field_id) = field_id { + self + .gen_did_update_row_view_tasks(row_detail.row.id.clone(), field_id) + .await; + } } - pub async fn v_filter_rows(&self, rows: Vec<Arc<Row>>) -> Vec<Arc<Row>> { - self.filter_controller.filter_rows(rows).await + pub async fn v_filter_rows(&self, row_details: &mut Vec<Arc<RowDetail>>) { + self.filter_controller.filter_rows(row_details).await } - pub async fn v_filter_rows_and_notify(&self, rows: &mut Vec<Arc<Row>>) { - let _ = self.filter_controller.filter_rows_and_notify(rows).await; - } - - pub async fn v_sort_rows(&self, rows: &mut Vec<Arc<Row>>) { - self.sort_controller.write().await.sort_rows(rows).await - } - - pub async fn v_sort_rows_and_notify(&self, rows: &mut Vec<Arc<Row>>) { + pub async fn v_sort_rows(&self, row_details: &mut Vec<Arc<RowDetail>>) { self .sort_controller .write() .await - .sort_rows_and_notify(rows) - .await; + .sort_rows(row_details) + .await } #[instrument(level = "info", skip(self))] - pub async fn v_get_all_rows(&self) -> Vec<Arc<Row>> { - let row_orders = self.delegate.get_all_row_orders(&self.view_id).await; - let rows = self.delegate.get_all_rows(&self.view_id, row_orders).await; - let mut rows = self.v_filter_rows(rows).await; + pub async fn v_get_rows(&self) -> Vec<Arc<RowDetail>> { + let mut rows = self.delegate.get_rows(&self.view_id).await; + self.v_filter_rows(&mut rows).await; self.v_sort_rows(&mut rows).await; rows } - pub async fn v_get_cells_for_field(&self, field_id: &str) -> Vec<RowCell> { - let row_orders = self.delegate.get_all_row_orders(&self.view_id).await; - let rows = self.delegate.get_all_rows(&self.view_id, row_orders).await; - let rows = self.v_filter_rows(rows).await; - let rows = rows - .into_iter() - .filter_map(|row| { - row - .cells - .get(field_id) - .map(|cell| RowCell::new(row.id.clone(), Some(cell.clone()))) - }) - .collect::<Vec<_>>(); - trace!( - "[Database]: get cells for field: {}, total rows:{}", - field_id, - rows.len() - ); - rows - } - - pub async fn v_get_row(&self, row_id: &RowId) -> Option<(usize, Arc<RowDetail>)> { - self.delegate.get_row_detail(&self.view_id, row_id).await - } - pub async fn v_move_group_row( &self, - row: &Row, + row_detail: &RowDetail, + row_changeset: &mut RowChangeset, to_group_id: &str, to_row_id: Option<RowId>, - ) -> UpdatedCells { - let mut updated_cells = UpdatedCells::new(); + ) { let result = self .mut_group_controller(|group_controller, field| { let move_row_context = MoveGroupRowContext { - row, - updated_cells: &mut updated_cells, + row_detail, + row_changeset, field: &field, to_group_id, to_row_id, @@ -454,8 +337,21 @@ impl DatabaseViewEditor { }) .await; - handle_mut_group_result(&self.view_id, result).await; - updated_cells + if let Some(result) = result { + if let Some(delete_group) = result.deleted_group { + tracing::trace!("Delete group after moving the row: {:?}", delete_group); + let changes = GroupChangesPB { + view_id: self.view_id.clone(), + deleted_groups: vec![delete_group.group_id], + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, changes).await; + } + + for changeset in result.row_changesets { + notify_did_update_group_rows(changeset).await; + } + } } /// Only call once after database view editor initialized @@ -517,11 +413,8 @@ impl DatabaseViewEditor { pub async fn v_create_group(&self, name: &str) -> FlowyResult<()> { let mut old_field: Option<Field> = None; let result = if let Some(controller) = self.group_controller.write().await.as_mut() { - let create_group_results = controller.create_group(name.to_string()).await?; - old_field = self - .delegate - .get_field(controller.get_grouping_field_id()) - .await; + let create_group_results = controller.create_group(name.to_string())?; + old_field = self.delegate.get_field(controller.get_grouping_field_id()); create_group_results } else { (None, None) @@ -554,21 +447,20 @@ impl DatabaseViewEditor { None => return Ok(RowsChangePB::default()), }; - let old_field = self - .delegate - .get_field(controller.get_grouping_field_id()) - .await; - let (row_ids, type_option_data) = controller.delete_group(group_id).await?; + let old_field = self.delegate.get_field(controller.get_grouping_field_id()); + let (row_ids, type_option_data) = controller.delete_group(group_id)?; drop(group_controller); let mut changes = RowsChangePB::default(); + if let Some(field) = old_field { - for row_id in row_ids { - if let Some(row) = self.delegate.remove_row(&row_id).await { - changes.deleted_rows.push(row.id.into_inner()); - } - } + let deleted_rows = row_ids + .iter() + .filter_map(|row_id| self.delegate.remove_row(row_id)) + .map(|row| row.id.into_inner()); + + changes.deleted_rows.extend(deleted_rows); if let Some(type_option) = type_option_data { self.delegate.update_field(type_option, field).await?; @@ -586,23 +478,19 @@ impl DatabaseViewEditor { pub async fn v_update_group(&self, changeset: Vec<GroupChangeset>) -> FlowyResult<()> { let mut type_option_data = None; - let (old_field, updated_groups) = if let Some(controller) = - self.group_controller.write().await.as_mut() - { - let old_field = self - .delegate - .get_field(controller.get_grouping_field_id()) - .await; - let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?; + let (old_field, updated_groups) = + if let Some(controller) = self.group_controller.write().await.as_mut() { + let old_field = self.delegate.get_field(controller.get_grouping_field_id()); + let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?; - if new_type_option.is_some() { - type_option_data = new_type_option; - } + if new_type_option.is_some() { + type_option_data = new_type_option; + } - (old_field, updated_groups) - } else { - (None, vec![]) - }; + (old_field, updated_groups) + } else { + (None, vec![]) + }; if let Some(old_field) = old_field { if let Some(type_option_data) = type_option_data { @@ -623,7 +511,7 @@ impl DatabaseViewEditor { } pub async fn v_get_all_sorts(&self) -> Vec<Sort> { - self.delegate.get_all_sorts(&self.view_id).await + self.delegate.get_all_sorts(&self.view_id) } #[tracing::instrument(level = "trace", skip(self), err)] @@ -640,8 +528,10 @@ impl DatabaseViewEditor { condition: params.condition.into(), }; - self.delegate.insert_sort(&self.view_id, sort.clone()).await; + self.delegate.insert_sort(&self.view_id, sort.clone()); + let mut sort_controller = self.sort_controller.write().await; + let notification = if is_exist { sort_controller .apply_changeset(SortChangeset::from_update(sort.clone())) @@ -659,8 +549,7 @@ impl DatabaseViewEditor { pub async fn v_reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> { self .delegate - .move_sort(&self.view_id, ¶ms.from_sort_id, ¶ms.to_sort_id) - .await; + .move_sort(&self.view_id, ¶ms.from_sort_id, ¶ms.to_sort_id); let notification = self .sort_controller @@ -684,110 +573,17 @@ impl DatabaseViewEditor { .apply_changeset(SortChangeset::from_delete(params.sort_id.clone())) .await; - self - .delegate - .remove_sort(&self.view_id, ¶ms.sort_id) - .await; + self.delegate.remove_sort(&self.view_id, ¶ms.sort_id); notify_did_update_sort(notification).await; Ok(()) } - pub async fn v_update_calculate(&self, field_id: &str) -> Option<()> { - let field = self.delegate.get_field(field_id).await?; - let cal = self - .delegate - .get_calculation(&self.view_id, &field.id) - .await?; - - let cells = self - .delegate - .get_cells_for_field(&self.view_id, field_id) - .await - .into_iter() - .flat_map(|row_cell| row_cell.cell.map(Arc::new)) - .collect::<Vec<_>>(); - - let changes = self - .calculations_controller - .handle_cells_changed(&field, &cal, cells) - .await; - - if !changes.is_empty() { - let notification = CalculationChangesetNotificationPB::from_update(&self.view_id, changes); - if let Err(_err) = self - .notifier - .send(DatabaseViewChanged::CalculationValueNotification( - notification, - )) - { - error!("Failed to send CalculationValueNotification"); - } - } - - None - } - - pub async fn v_calculate_rows(&self, fields: Vec<Field>, rows: Vec<Arc<Row>>) -> FlowyResult<()> { - let mut updates = vec![]; - // Filter fields to only those with calculations - let fields_with_calculations: Vec<(&Field, Calculation)> = - futures::future::join_all(fields.iter().map(|field| async move { - self - .delegate - .get_calculation(&self.view_id, &field.id) - .await - .map(|cal| (field, cal)) - })) - .await - .into_iter() - .flatten() - .collect(); - - // Pre-compute cells by field ID only for fields that have calculations - let mut cells_by_field_id: HashMap<String, Vec<Arc<Cell>>> = fields_with_calculations - .iter() - .map(|(field, _)| { - let cells = rows - .iter() - .filter_map(|row| row.cells.get(&field.id).cloned().map(Arc::new)) - .collect::<Vec<Arc<Cell>>>(); - (field.id.clone(), cells) - }) - .collect(); - - // Perform calculations for the filtered fields - for (field, calculation) in fields_with_calculations { - if let Some(cells) = cells_by_field_id.remove(&field.id) { - let changes = self - .calculations_controller - .handle_cells_changed(field, &calculation, cells) - .await; - updates.extend(changes); - } - } - - // Send notification if updates were made - if !updates.is_empty() { - let notification = CalculationChangesetNotificationPB::from_update(&self.view_id, updates); - if let Err(_err) = self - .notifier - .send(DatabaseViewChanged::CalculationValueNotification( - notification, - )) - { - error!("Failed to send CalculationValueNotification"); - } - } - - Ok(()) - } - pub async fn v_delete_all_sorts(&self) -> FlowyResult<()> { let all_sorts = self.v_get_all_sorts().await; self.sort_controller.write().await.delete_all_sorts().await; - self.delegate.remove_all_sorts(&self.view_id).await; + self.delegate.remove_all_sorts(&self.view_id); let mut notification = SortChangesetNotificationPB::new(self.view_id.clone()); notification.delete_sorts = all_sorts.into_iter().map(SortPB::from).collect(); notify_did_update_sort(notification).await; @@ -795,16 +591,18 @@ impl DatabaseViewEditor { } pub async fn v_get_all_calculations(&self) -> Vec<Arc<Calculation>> { - self.delegate.get_all_calculations(&self.view_id).await + self.delegate.get_all_calculations(&self.view_id) } pub async fn v_update_calculations( &self, params: UpdateCalculationChangesetPB, ) -> FlowyResult<()> { - let calculation_id = params - .calculation_id - .unwrap_or_else(gen_database_calculation_id); + let calculation_id = match params.calculation_id { + None => gen_database_calculation_id(), + Some(calculation_id) => calculation_id, + }; + let calculation = Calculation::none( calculation_id, params.field_id, @@ -822,8 +620,7 @@ impl DatabaseViewEditor { let calculation: Calculation = Calculation::from(&insert); self .delegate - .update_calculation(¶ms.view_id, calculation) - .await; + .update_calculation(¶ms.view_id, calculation); } } @@ -839,8 +636,7 @@ impl DatabaseViewEditor { ) -> FlowyResult<()> { self .delegate - .remove_calculation(¶ms.view_id, ¶ms.calculation_id) - .await; + .remove_calculation(¶ms.view_id, ¶ms.calculation_id); let calculation = Calculation::none(params.calculation_id, params.field_id, None); @@ -857,16 +653,17 @@ impl DatabaseViewEditor { } pub async fn v_get_all_filters(&self) -> Vec<Filter> { - self.delegate.get_all_filters(&self.view_id).await + self.delegate.get_all_filters(&self.view_id) } pub async fn v_get_filter(&self, filter_id: &str) -> Option<Filter> { - self.delegate.get_filter(&self.view_id, filter_id).await + self.delegate.get_filter(&self.view_id, filter_id) } #[tracing::instrument(level = "trace", skip(self), err)] pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> { let notification = self.filter_controller.apply_changeset(changeset).await; + notify_did_update_filter(notification).await; let group_controller_read_guard = self.group_controller.read().await; @@ -879,10 +676,6 @@ impl DatabaseViewEditor { self.v_group_by_field(&field_id).await?; } - let row_orders = self.delegate.get_all_row_orders(&self.view_id).await; - let rows = self.delegate.get_all_rows(&self.view_id, row_orders).await; - let fields = self.delegate.get_fields(&self.view_id, None).await; - self.v_calculate_rows(fields, rows).await?; Ok(()) } @@ -893,23 +686,15 @@ impl DatabaseViewEditor { match layout_ty { DatabaseLayout::Grid => {}, DatabaseLayout::Board => { - if let Some(value) = self - .delegate - .get_layout_setting(&self.view_id, layout_ty) - .await - { + if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { layout_setting.board = Some(value.into()); } }, DatabaseLayout::Calendar => { - if let Some(value) = self - .delegate - .get_layout_setting(&self.view_id, layout_ty) - .await - { + if let Some(value) = self.delegate.get_layout_setting(&self.view_id, layout_ty) { let calendar_setting = CalendarLayoutSetting::from(value); // Check the field exist or not - if let Some(field) = self.delegate.get_field(&calendar_setting.field_id).await { + if let Some(field) = self.delegate.get_field(&calendar_setting.field_id) { let field_type = FieldType::from(field.field_type); // Check the type of field is Datetime or not @@ -938,33 +723,27 @@ impl DatabaseViewEditor { DatabaseLayout::Board => { let layout_setting = params.board.unwrap(); - self - .delegate - .insert_layout_setting( - &self.view_id, - ¶ms.layout_type, - layout_setting.clone().into(), - ) - .await; + self.delegate.insert_layout_setting( + &self.view_id, + ¶ms.layout_type, + layout_setting.clone().into(), + ); Some(DatabaseLayoutSettingPB::from_board(layout_setting)) }, DatabaseLayout::Calendar => { let layout_setting = params.calendar.unwrap(); - if let Some(field) = self.delegate.get_field(&layout_setting.field_id).await { + if let Some(field) = self.delegate.get_field(&layout_setting.field_id) { if FieldType::from(field.field_type) != FieldType::DateTime { return Err(FlowyError::unexpect_calendar_field_type()); } - self - .delegate - .insert_layout_setting( - &self.view_id, - ¶ms.layout_type, - layout_setting.clone().into(), - ) - .await; + self.delegate.insert_layout_setting( + &self.view_id, + ¶ms.layout_type, + layout_setting.clone().into(), + ); Some(DatabaseLayoutSettingPB::from_calendar(layout_setting)) } else { @@ -975,7 +754,7 @@ impl DatabaseViewEditor { }; if let Some(payload) = layout_setting_pb { - database_notification_builder(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings) + send_notification(&self.view_id, DatabaseNotification::DidUpdateLayoutSettings) .payload(payload) .send(); } @@ -990,10 +769,10 @@ impl DatabaseViewEditor { let notification = self.filter_controller.apply_changeset(changeset).await; notify_did_update_filter(notification).await; - let sorts = self.delegate.get_all_sorts(&self.view_id).await; + let sorts = self.delegate.get_all_sorts(&self.view_id); if let Some(sort) = sorts.iter().find(|sort| sort.field_id == deleted_field_id) { - self.delegate.remove_sort(&self.view_id, &sort.id).await; + self.delegate.remove_sort(&self.view_id, &sort.id); let notification = self .sort_controller .write() @@ -1022,16 +801,6 @@ impl DatabaseViewEditor { .calculations_controller .did_receive_field_type_changed(field_id.to_owned(), new_field_type) .await; - if self.filter_controller.has_filters().await { - let changeset = FilterChangeset::DeleteAllWithFieldId { - field_id: field_id.to_string(), - }; - let notification = self.filter_controller.apply_changeset(changeset).await; - notify_did_update_filter(notification).await; - } - if self.is_grouping_field(field_id).await { - let _ = self.v_group_by_field(field_id).await; - } } /// Notifies the view's field type-option data is changed @@ -1041,7 +810,7 @@ impl DatabaseViewEditor { pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> { let field_id = &old_field.id; - if let Some(field) = self.delegate.get_field(field_id).await { + if let Some(field) = self.delegate.get_field(field_id) { self .sort_controller .read() @@ -1049,28 +818,31 @@ impl DatabaseViewEditor { .did_update_field_type_option(&field) .await; - // If the id of the grouping field is equal to the updated field's id - // and something critical changed, then we need to update the group setting - if self.is_grouping_field(field_id).await - && matches!( - FieldType::from(field.field_type), - FieldType::SingleSelect | FieldType::MultiSelect - ) - { - self.v_group_by_field(field_id).await?; + if old_field.field_type != field.field_type { + let changeset = FilterChangeset::DeleteAllWithFieldId { + field_id: field.id.clone(), + }; + let notification = self.filter_controller.apply_changeset(changeset).await; + notify_did_update_filter(notification).await; } } + // If the id of the grouping field is equal to the updated field's id, then we need to + // update the group setting + if self.is_grouping_field(field_id).await { + self.v_group_by_field(field_id).await?; + } + Ok(()) } /// Called when a grouping field is updated. #[tracing::instrument(level = "debug", skip_all, err)] pub async fn v_group_by_field(&self, field_id: &str) -> FlowyResult<()> { - if let Some(field) = self.delegate.get_field(field_id).await { + if let Some(field) = self.delegate.get_field(field_id) { tracing::trace!("create new group controller"); - let mut new_group_controller = new_group_controller( + let new_group_controller = new_group_controller( self.view_id.clone(), self.delegate.clone(), self.filter_controller.clone(), @@ -1078,9 +850,7 @@ impl DatabaseViewEditor { ) .await?; - if let Some(controller) = &mut new_group_controller { - (*controller).load_group_data().await?; - + if let Some(controller) = &new_group_controller { let new_groups = controller .get_all_groups() .into_iter() @@ -1092,14 +862,16 @@ impl DatabaseViewEditor { initial_groups: new_groups, ..Default::default() }; + tracing::trace!("notify did group by field1"); debug_assert!(!changeset.is_empty()); if !changeset.is_empty() { - database_notification_builder(&changeset.view_id, DatabaseNotification::DidGroupByField) + send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField) .payload(changeset) .send(); } } + tracing::trace!("notify did group by field2"); *self.group_controller.write().await = new_group_controller; @@ -1118,7 +890,7 @@ impl DatabaseViewEditor { let text_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, &row_id).await?; // Date - let date_field = self.delegate.get_field(&calendar_setting.field_id).await?; + let date_field = self.delegate.get_field(&calendar_setting.field_id)?; let date_cell = get_cell_for_row(self.delegate.clone(), &date_field.id, &row_id).await?; let title = text_cell @@ -1129,15 +901,16 @@ impl DatabaseViewEditor { let timestamp = date_cell .into_date_field_cell_data() .unwrap_or_default() - .timestamp; - - let (_, row_detail) = self.delegate.get_row_detail(&self.view_id, &row_id).await?; + .timestamp + .unwrap_or_default(); + let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; Some(CalendarEventPB { - row_meta: RowMetaPB::from(row_detail.as_ref().clone()), + row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: date_field.id.clone(), title, timestamp, + is_scheduled: timestamp != 0, }) } @@ -1155,83 +928,89 @@ impl DatabaseViewEditor { Some(calendar_setting) => calendar_setting, }; + // Text let primary_field = self.delegate.get_primary_field().await?; + let text_cells = + get_cells_for_field(self.delegate.clone(), &self.view_id, &primary_field.id).await; - let mut events: Vec<CalendarEventPB> = vec![]; + // Date + let timestamp_by_row_id = get_cells_for_field( + self.delegate.clone(), + &self.view_id, + &calendar_setting.field_id, + ) + .await + .into_iter() + .map(|date_cell| { + let row_id = date_cell.row_id.clone(); - let rows = self.v_get_all_rows().await; - - for row in rows { - let primary_cell = get_cell_for_row(self.delegate.clone(), &primary_field.id, &row.id).await; - let timestamp_cell = - get_cell_for_row(self.delegate.clone(), &calendar_setting.field_id, &row.id).await; - - let timestamp = timestamp_cell - .and_then(|cell| cell.into_date_field_cell_data()) - .and_then(|cell_data| cell_data.timestamp); - - let title = primary_cell - .and_then(|cell| cell.into_text_field_cell_data()) - .map(|cell_data| cell_data.into()) + // timestamp + let timestamp = date_cell + .into_date_field_cell_data() + .map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default()) .unwrap_or_default(); - let (_, row_detail) = self.delegate.get_row_detail(&self.view_id, &row.id).await?; + (row_id, timestamp) + }) + .collect::<HashMap<RowId, i64>>(); + + let mut events: Vec<CalendarEventPB> = vec![]; + for text_cell in text_cells { + let row_id = text_cell.row_id.clone(); + let timestamp = timestamp_by_row_id + .get(&row_id) + .cloned() + .unwrap_or_default(); + + let title = text_cell + .into_text_field_cell_data() + .unwrap_or_default() + .into(); + + let (_, row_detail) = self.delegate.get_row(&self.view_id, &row_id).await?; let event = CalendarEventPB { - row_meta: RowMetaPB::from(row_detail.as_ref().clone()), + row_meta: RowMetaPB::from(row_detail.as_ref()), date_field_id: calendar_setting.field_id.clone(), title, timestamp, + is_scheduled: timestamp != 0, }; - events.push(event); } - Some(events) } pub async fn v_get_layout_type(&self) -> DatabaseLayout { - self.delegate.get_layout_for_view(&self.view_id).await + self.delegate.get_layout_for_view(&self.view_id) } #[tracing::instrument(level = "trace", skip_all)] pub async fn v_update_layout_type(&self, new_layout_type: DatabaseLayout) -> FlowyResult<()> { self .delegate - .update_layout_type(&self.view_id, &new_layout_type) - .await; + .update_layout_type(&self.view_id, &new_layout_type); // using the {} brackets to denote the lifetime of the resolver. Because the DatabaseLayoutDepsResolver // is not sync and send, so we can't pass it to the async block. { let resolver = DatabaseLayoutDepsResolver::new(self.delegate.get_database(), new_layout_type); - resolver - .resolve_deps_when_update_layout_type(&self.view_id) - .await; + resolver.resolve_deps_when_update_layout_type(&self.view_id); } // initialize the group controller if the current layout support grouping - let new_group_controller = match new_group_controller( + *self.group_controller.write().await = new_group_controller( self.view_id.clone(), self.delegate.clone(), self.filter_controller.clone(), None, ) - .await? - { - Some(mut controller) => { - controller.load_group_data().await?; - Some(controller) - }, - None => None, - }; - - *self.group_controller.write().await = new_group_controller; + .await?; let payload = DatabaseLayoutMetaPB { view_id: self.view_id.clone(), layout: new_layout_type.into(), }; - database_notification_builder(&self.view_id, DatabaseNotification::DidUpdateDatabaseLayout) + send_notification(&self.view_id, DatabaseNotification::DidUpdateDatabaseLayout) .payload(payload) .send(); @@ -1249,20 +1028,18 @@ impl DatabaseViewEditor { } => RowsChangePB::from_move(vec![deleted_row_id.into_inner()], vec![inserted_row.into()]), }; - database_notification_builder(&self.view_id, DatabaseNotification::DidUpdateRow) + send_notification(&self.view_id, DatabaseNotification::DidUpdateRow) .payload(changeset) .send(); } pub async fn v_get_field_settings(&self, field_ids: &[String]) -> HashMap<String, FieldSettings> { - self - .delegate - .get_field_settings(&self.view_id, field_ids) - .await + self.delegate.get_field_settings(&self.view_id, field_ids) } pub async fn v_update_field_settings(&self, params: FieldSettingsChangesetPB) -> FlowyResult<()> { - self.delegate.update_field_settings(params).await; + self.delegate.update_field_settings(params); + Ok(()) } @@ -1276,7 +1053,7 @@ impl DatabaseViewEditor { .await .as_ref() .map(|controller| controller.get_grouping_field_id().to_owned())?; - let field = self.delegate.get_field(&group_field_id).await?; + let field = self.delegate.get_field(&group_field_id)?; let mut write_guard = self.group_controller.write().await; if let Some(group_controller) = &mut *write_guard { f(group_controller, field).ok() @@ -1285,11 +1062,11 @@ impl DatabaseViewEditor { } } - async fn gen_did_update_row_view_tasks(&self, row_id: RowId, field_id: Option<String>) { + async fn gen_did_update_row_view_tasks(&self, row_id: RowId, field_id: String) { let weak_filter_controller = Arc::downgrade(&self.filter_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); - tokio::spawn(async move { + af_spawn(async move { if let Some(filter_controller) = weak_filter_controller.upgrade() { filter_controller .did_receive_row_changed(row_id.clone()) @@ -1302,43 +1079,31 @@ impl DatabaseViewEditor { .did_receive_row_changed(row_id.clone()) .await; } - if let Some(calculations_controller) = weak_calculations_controller.upgrade() { - if let Some(field_id) = field_id { - calculations_controller - .did_receive_cell_changed(field_id) - .await; - } + calculations_controller + .did_receive_cell_changed(field_id) + .await; } }); } - async fn gen_did_create_row_view_tasks(&self, row: Row) { + async fn gen_did_create_row_view_tasks(&self, preliminary_index: usize, row_detail: RowDetail) { + let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); - tokio::spawn(async move { + af_spawn(async move { + if let Some(sort_controller) = weak_sort_controller.upgrade() { + sort_controller + .read() + .await + .did_create_row(preliminary_index, &row_detail) + .await; + } + if let Some(calculations_controller) = weak_calculations_controller.upgrade() { calculations_controller - .did_receive_row_changed(row.clone()) + .did_receive_row_changed(row_detail.row.clone()) .await; } }); } } - -async fn handle_mut_group_result(view_id: &str, result: Option<DidMoveGroupRowResult>) { - if let Some(result) = result { - if let Some(deleted_group) = result.deleted_group { - trace!("Delete group after moving the row: {:?}", deleted_group); - let payload = GroupChangesPB { - view_id: view_id.to_string(), - deleted_groups: vec![deleted_group.group_id], - ..Default::default() - }; - notify_did_update_num_of_groups(view_id, payload).await; - } - for changeset in result.row_changesets { - trace!("[RowOrder]: group row changeset: {:?}", changeset); - notify_did_update_group_rows(changeset).await; - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 91341ae3b3..f710144e60 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -1,13 +1,15 @@ -use async_trait::async_trait; use std::sync::Arc; +use collab_database::fields::Field; +use collab_database::rows::{RowDetail, RowId}; + +use lib_infra::future::Fut; + use crate::services::cell::CellCache; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, }; use crate::services::filter::{Filter, FilterController, FilterDelegate, FilterTaskHandler}; -use collab_database::fields::Field; -use collab_database::rows::{Row, RowDetail, RowId}; pub async fn make_filter_controller( view_id: &str, @@ -41,30 +43,28 @@ pub async fn make_filter_controller( struct DatabaseViewFilterDelegateImpl(Arc<dyn DatabaseViewOperation>); -#[async_trait] impl FilterDelegate for DatabaseViewFilterDelegateImpl { - async fn get_field(&self, field_id: &str) -> Option<Field> { - self.0.get_field(field_id).await + fn get_field(&self, field_id: &str) -> Option<Field> { + self.0.get_field(field_id) } - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> { - self.0.get_fields(view_id, field_ids).await + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>> { + self.0.get_fields(view_id, field_ids) } - async fn get_rows(&self, view_id: &str) -> Vec<Arc<Row>> { - let row_orders = self.0.get_all_row_orders(view_id).await; - self.0.get_all_rows(view_id, row_orders).await + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> { + self.0.get_rows(view_id) } - async fn get_row(&self, view_id: &str, rows_id: &RowId) -> Option<(usize, Arc<RowDetail>)> { - self.0.get_row_detail(view_id, rows_id).await + fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>> { + self.0.get_row(view_id, rows_id) } - async fn get_all_filters(&self, view_id: &str) -> Vec<Filter> { - self.0.get_all_filters(view_id).await + fn get_all_filters(&self, view_id: &str) -> Vec<Filter> { + self.0.get_all_filters(view_id) } - async fn save_filters(&self, view_id: &str, filters: &[Filter]) { - self.0.save_filters(view_id, filters).await + fn save_filters(&self, view_id: &str, filters: &[Filter]) { + self.0.save_filters(view_id, filters) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs index 63d4ca99a0..504511608a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -1,10 +1,10 @@ -use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::{RowDetail, RowId}; use flowy_error::FlowyResult; +use lib_infra::future::{to_fut, Fut}; use crate::entities::FieldType; use crate::services::database_view::DatabaseViewOperation; @@ -21,19 +21,20 @@ pub async fn new_group_controller( filter_controller: Arc<FilterController>, grouping_field: Option<Field>, ) -> FlowyResult<Option<Box<dyn GroupController>>> { - if !delegate.get_layout_for_view(&view_id).await.is_board() { + if !delegate.get_layout_for_view(&view_id).is_board() { return Ok(None); } let controller_delegate = GroupControllerDelegateImpl { delegate: delegate.clone(), - filter_controller, + filter_controller: filter_controller.clone(), }; let grouping_field = match grouping_field { Some(field) => Some(field), None => { let group_setting = controller_delegate.get_group_setting(&view_id).await; + let fields = delegate.get_fields(&view_id, None).await; group_setting @@ -60,46 +61,45 @@ pub(crate) struct GroupControllerDelegateImpl { filter_controller: Arc<FilterController>, } -#[async_trait] impl GroupContextDelegate for GroupControllerDelegateImpl { - async fn get_group_setting(&self, view_id: &str) -> Option<Arc<GroupSetting>> { - let mut settings = self.delegate.get_group_setting(view_id).await; - if settings.is_empty() { - None - } else { - Some(Arc::new(settings.remove(0))) - } + fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>> { + let mut settings = self.delegate.get_group_setting(view_id); + to_fut(async move { + if settings.is_empty() { + None + } else { + Some(Arc::new(settings.remove(0))) + } + }) } - async fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Vec<RowSingleCellData> { + fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>> { + let field_id = field_id.to_owned(); + let view_id = view_id.to_owned(); let delegate = self.delegate.clone(); - get_cells_for_field(delegate, view_id, field_id).await + to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await }) } - async fn save_configuration( - &self, - view_id: &str, - group_setting: GroupSetting, - ) -> FlowyResult<()> { - self - .delegate - .insert_group_setting(view_id, group_setting) - .await; - Ok(()) + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>> { + self.delegate.insert_group_setting(view_id, group_setting); + to_fut(async move { Ok(()) }) } } -#[async_trait] impl GroupControllerDelegate for GroupControllerDelegateImpl { - async fn get_field(&self, field_id: &str) -> Option<Field> { - self.delegate.get_field(field_id).await + fn get_field(&self, field_id: &str) -> Option<Field> { + self.delegate.get_field(field_id) } - async fn get_all_rows(&self, view_id: &str) -> Vec<Arc<Row>> { - let row_orders = self.delegate.get_all_row_orders(view_id).await; - let rows = self.delegate.get_all_rows(view_id, row_orders).await; - - self.filter_controller.filter_rows(rows).await + fn get_all_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> { + let view_id = view_id.to_string(); + let delegate = self.delegate.clone(); + let filter_controller = self.filter_controller.clone(); + to_fut(async move { + let mut row_details = delegate.get_rows(&view_id).await; + filter_controller.filter_rows(&mut row_details).await; + row_details + }) } } @@ -108,7 +108,7 @@ pub(crate) async fn get_cell_for_row( field_id: &str, row_id: &RowId, ) -> Option<RowSingleCellData> { - let field = delegate.get_field(field_id).await?; + let field = delegate.get_field(field_id)?; let row_cell = delegate.get_cell_in_row(field_id, row_id).await; let field_type = FieldType::from(field.field_type); let handler = delegate.get_type_option_cell_handler(&field)?; @@ -131,7 +131,7 @@ pub(crate) async fn get_cells_for_field( view_id: &str, field_id: &str, ) -> Vec<RowSingleCellData> { - if let Some(field) = delegate.get_field(field_id).await { + if let Some(field) = delegate.get_field(field_id) { let field_type = FieldType::from(field.field_type); if let Some(handler) = delegate.get_type_option_cell_handler(&field) { let cells = delegate.get_cells_for_field(view_id, field_id).await; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 30816587d6..3a912646cd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -1,15 +1,14 @@ -use async_trait::async_trait; -use collab::lock::RwLock; -use collab_database::database::Database; -use collab_database::entity::DatabaseView; -use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Row, RowCell, RowDetail, RowId}; -use collab_database::views::{DatabaseLayout, LayoutSetting, RowOrder}; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::RwLock as TokioRwLock; + +use collab_database::database::MutexDatabase; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{Row, RowCell, RowDetail, RowId}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; +use tokio::sync::RwLock; use flowy_error::FlowyError; +use lib_infra::future::{Fut, FutureResult}; use lib_infra::priority_task::TaskDispatcher; use crate::entities::{FieldSettingsChangesetPB, FieldType}; @@ -21,117 +20,111 @@ use crate::services::group::GroupSetting; use crate::services::sort::Sort; /// Defines the operation that can be performed on a database view -#[async_trait] pub trait DatabaseViewOperation: Send + Sync + 'static { /// Get the database that the view belongs to - fn get_database(&self) -> Arc<RwLock<Database>>; + fn get_database(&self) -> Arc<MutexDatabase>; /// Get the view of the database with the view_id - async fn get_view(&self, view_id: &str) -> Option<DatabaseView>; + fn get_view(&self, view_id: &str) -> Fut<Option<DatabaseView>>; /// If the field_ids is None, then it will return all the field revisions - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field>; + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>>; /// Returns the field with the field_id - async fn get_field(&self, field_id: &str) -> Option<Field>; + fn get_field(&self, field_id: &str) -> Option<Field>; - async fn create_field( + fn create_field( &self, view_id: &str, name: &str, field_type: FieldType, type_option_data: TypeOptionData, - ) -> Field; + ) -> Fut<Field>; - async fn update_field( + fn update_field( &self, type_option_data: TypeOptionData, old_field: Field, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), FlowyError>; - async fn get_primary_field(&self) -> Option<Arc<Field>>; + fn get_primary_field(&self) -> Fut<Option<Arc<Field>>>; /// Returns the index of the row with row_id - async fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Option<usize>; + fn index_of_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<usize>>; /// Returns the `index` and `RowRevision` with row_id - async fn get_row_detail(&self, view_id: &str, row_id: &RowId) -> Option<(usize, Arc<RowDetail>)>; + fn get_row(&self, view_id: &str, row_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>>; /// Returns all the rows in the view - async fn get_all_rows(&self, view_id: &str, row_orders: Vec<RowOrder>) -> Vec<Arc<Row>>; - async fn get_all_row_orders(&self, view_id: &str) -> Vec<RowOrder>; + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>; - async fn remove_row(&self, row_id: &RowId) -> Option<Row>; + fn remove_row(&self, row_id: &RowId) -> Option<Row>; - async fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Vec<RowCell>; + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>; - async fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Arc<RowCell>; + fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>>; /// Return the database layout type for the view with given view_id /// The default layout type is [DatabaseLayout::Grid] - async fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; + fn get_layout_for_view(&self, view_id: &str) -> DatabaseLayout; - async fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting>; + fn get_group_setting(&self, view_id: &str) -> Vec<GroupSetting>; - async fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); + fn insert_group_setting(&self, view_id: &str, setting: GroupSetting); - async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Sort>; + fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Sort>; - async fn insert_sort(&self, view_id: &str, sort: Sort); + fn insert_sort(&self, view_id: &str, sort: Sort); - async fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str); + fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str); - async fn remove_sort(&self, view_id: &str, sort_id: &str); + fn remove_sort(&self, view_id: &str, sort_id: &str); - async fn get_all_sorts(&self, view_id: &str) -> Vec<Sort>; + fn get_all_sorts(&self, view_id: &str) -> Vec<Sort>; - async fn remove_all_sorts(&self, view_id: &str); + fn remove_all_sorts(&self, view_id: &str); - async fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>>; + fn get_all_calculations(&self, view_id: &str) -> Vec<Arc<Calculation>>; - async fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation>; + fn get_calculation(&self, view_id: &str, field_id: &str) -> Option<Calculation>; - async fn update_calculation(&self, view_id: &str, calculation: Calculation); + fn update_calculation(&self, view_id: &str, calculation: Calculation); - async fn remove_calculation(&self, view_id: &str, calculation_id: &str); + fn remove_calculation(&self, view_id: &str, calculation_id: &str); - async fn get_all_filters(&self, view_id: &str) -> Vec<Filter>; + fn get_all_filters(&self, view_id: &str) -> Vec<Filter>; - async fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter>; + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option<Filter>; - async fn delete_filter(&self, view_id: &str, filter_id: &str); + fn delete_filter(&self, view_id: &str, filter_id: &str); - async fn insert_filter(&self, view_id: &str, filter: Filter); + fn insert_filter(&self, view_id: &str, filter: Filter); - async fn save_filters(&self, view_id: &str, filters: &[Filter]); + fn save_filters(&self, view_id: &str, filters: &[Filter]); - async fn get_layout_setting( - &self, - view_id: &str, - layout_ty: &DatabaseLayout, - ) -> Option<LayoutSetting>; + fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option<LayoutSetting>; - async fn insert_layout_setting( + fn insert_layout_setting( &self, view_id: &str, layout_ty: &DatabaseLayout, layout_setting: LayoutSetting, ); - async fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); + fn update_layout_type(&self, view_id: &str, layout_type: &DatabaseLayout); /// Returns a `TaskDispatcher` used to poll a `Task` - fn get_task_scheduler(&self) -> Arc<TokioRwLock<TaskDispatcher>>; + fn get_task_scheduler(&self) -> Arc<RwLock<TaskDispatcher>>; fn get_type_option_cell_handler( &self, field: &Field, ) -> Option<Box<dyn TypeOptionCellDataHandler>>; - async fn get_field_settings( + fn get_field_settings( &self, view_id: &str, field_ids: &[String], ) -> HashMap<String, FieldSettings>; - async fn update_field_settings(&self, params: FieldSettingsChangesetPB); + fn update_field_settings(&self, params: FieldSettingsChangesetPB); } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index 53b70a42ca..0397526b66 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -1,10 +1,11 @@ -use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::Row; +use collab_database::rows::RowDetail; use tokio::sync::RwLock; +use lib_infra::future::{to_fut, Fut}; + use crate::services::cell::CellCache; use crate::services::database_view::{ gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, @@ -22,7 +23,6 @@ pub(crate) async fn make_sort_controller( let handler_id = gen_handler_id(); let sorts = delegate .get_all_sorts(view_id) - .await .into_iter() .map(Arc::new) .collect(); @@ -53,31 +53,38 @@ struct DatabaseViewSortDelegateImpl { filter_controller: Arc<FilterController>, } -#[async_trait] impl SortDelegate for DatabaseViewSortDelegateImpl { - async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Arc<Sort>> { - self.delegate.get_sort(view_id, sort_id).await.map(Arc::new) + fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut<Option<Arc<Sort>>> { + let sort = self.delegate.get_sort(view_id, sort_id).map(Arc::new); + to_fut(async move { sort }) } - async fn get_rows(&self, view_id: &str) -> Vec<Arc<Row>> { + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>> { let view_id = view_id.to_string(); - let row_orders = self.delegate.get_all_row_orders(&view_id).await; - let rows = self.delegate.get_all_rows(&view_id, row_orders).await; - - self.filter_controller.filter_rows(rows).await + let delegate = self.delegate.clone(); + let filter_controller = self.filter_controller.clone(); + to_fut(async move { + let mut row_details = delegate.get_rows(&view_id).await; + filter_controller.filter_rows(&mut row_details).await; + row_details + }) } - async fn filter_row(&self, row: &Row) -> bool { - let rows = vec![Arc::new(row.clone())]; - let rows = self.filter_controller.filter_rows(rows).await; - !rows.is_empty() + fn filter_row(&self, row_detail: &RowDetail) -> Fut<bool> { + let filter_controller = self.filter_controller.clone(); + let row_detail = row_detail.clone(); + to_fut(async move { + let mut row_details = vec![Arc::new(row_detail)]; + filter_controller.filter_rows(&mut row_details).await; + !row_details.is_empty() + }) } - async fn get_field(&self, field_id: &str) -> Option<Field> { - self.delegate.get_field(field_id).await + fn get_field(&self, field_id: &str) -> Option<Field> { + self.delegate.get_field(field_id) } - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field> { - self.delegate.get_fields(view_id, field_ids).await + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>> { + self.delegate.get_fields(view_id, field_ids) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index 76466cae1c..132b480123 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -1,14 +1,15 @@ -use collab_database::database::Database; use std::collections::HashMap; use std::sync::Arc; +use collab_database::database::MutexDatabase; +use nanoid::nanoid; +use tokio::sync::{broadcast, RwLock}; + +use flowy_error::{FlowyError, FlowyResult}; + use crate::services::cell::CellCache; use crate::services::database::DatabaseRowEvent; use crate::services::database_view::{DatabaseViewEditor, DatabaseViewOperation}; -use collab::lock::RwLock; -use flowy_error::FlowyResult; -use nanoid::nanoid; -use tokio::sync::broadcast; pub type RowEventSender = broadcast::Sender<DatabaseRowEvent>; pub type RowEventReceiver = broadcast::Receiver<DatabaseRowEvent>; @@ -16,7 +17,7 @@ pub type EditorByViewId = HashMap<String, Arc<DatabaseViewEditor>>; pub struct DatabaseViews { #[allow(dead_code)] - database: Arc<RwLock<Database>>, + database: Arc<MutexDatabase>, cell_cache: CellCache, view_operation: Arc<dyn DatabaseViewOperation>, view_editors: Arc<RwLock<EditorByViewId>>, @@ -24,7 +25,7 @@ pub struct DatabaseViews { impl DatabaseViews { pub async fn new( - database: Arc<RwLock<Database>>, + database: Arc<MutexDatabase>, cell_cache: CellCache, view_operation: Arc<dyn DatabaseViewOperation>, view_editors: Arc<RwLock<EditorByViewId>>, @@ -37,7 +38,7 @@ impl DatabaseViews { }) } - pub async fn remove_view(&self, view_id: &str) { + pub async fn close_view(&self, view_id: &str) { let mut lock_guard = self.view_editors.write().await; if let Some(view) = lock_guard.remove(view_id) { view.close().await; @@ -52,19 +53,19 @@ impl DatabaseViews { self.view_editors.read().await.values().cloned().collect() } - pub async fn get_or_init_view_editor( - &self, - view_id: &str, - ) -> FlowyResult<Arc<DatabaseViewEditor>> { + pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult<Arc<DatabaseViewEditor>> { debug_assert!(!view_id.is_empty()); if let Some(editor) = self.view_editors.read().await.get(view_id) { return Ok(editor.clone()); } - let database_id = self.database.read().await.get_database_id(); - // Acquire the write lock to insert the new editor. We need to acquire the lock first to avoid - // initializing the editor multiple times. - let mut editor_map = self.view_editors.write().await; + let mut editor_map = self.view_editors.try_write().map_err(|err| { + FlowyError::internal().with_context(format!( + "fail to acquire the lock of editor_by_view_id: {}", + err + )) + })?; + let database_id = self.database.lock().get_database_id(); let editor = Arc::new( DatabaseViewEditor::new( database_id, @@ -75,15 +76,8 @@ impl DatabaseViews { .await?, ); editor_map.insert(view_id.to_owned(), editor.clone()); - drop(editor_map); - - editor.initialize().await?; Ok(editor) } - - pub async fn get_view_editor(&self, view_id: &str) -> Option<Arc<DatabaseViewEditor>> { - self.view_editors.read().await.get(view_id).cloned() - } } pub fn gen_handler_id() -> String { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs index 323ff2c815..18c72313c0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_builder.rs @@ -22,7 +22,7 @@ impl FieldBuilder { } pub fn name(mut self, name: &str) -> Self { - name.clone_into(&mut self.field.name); + self.field.name = name.to_owned(); self } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs index b5f4841393..e9db74358f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/field_operation.rs @@ -1,25 +1,24 @@ -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; -use flowy_error::{FlowyError, FlowyResult}; use std::sync::Arc; +use flowy_error::FlowyResult; + use crate::entities::FieldType; use crate::services::database::DatabaseEditor; -use crate::services::field::TypeOption; +use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption, TypeOption}; pub async fn edit_field_type_option<T: TypeOption>( field_id: &str, editor: Arc<DatabaseEditor>, action: impl FnOnce(&mut T), ) -> FlowyResult<()> { - let field = editor - .get_field(field_id) - .await - .ok_or_else(FlowyError::field_record_not_found)?; - let field_type = FieldType::from(field.field_type); - let get_type_option = field.get_type_option::<T>(field_type); + let get_type_option = async { + let field = editor.get_field(field_id)?; + let field_type = FieldType::from(field.field_type); + field.get_type_option::<T>(field_type) + }; - if let Some(mut type_option) = get_type_option { - if let Some(old_field) = editor.get_field(field_id).await { + if let Some(mut type_option) = get_type_option.await { + if let Some(old_field) = editor.get_field(field_id) { action(&mut type_option); let type_option_data = type_option.into(); editor diff --git a/frontend/rust-lib/flowy-database2/src/services/field/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/mod.rs index 55c052fe33..72cc377c60 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/mod.rs @@ -1,6 +1,5 @@ mod field_builder; mod field_operation; -pub(crate) mod type_option_transform; pub mod type_options; pub use field_builder::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_option_transform.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_option_transform.rs deleted file mode 100644 index e5972ea064..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_option_transform.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::entities::FieldType; -use crate::services::field::TypeOptionTransform; -use async_trait::async_trait; -use collab_database::database::Database; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::date_type_option::{DateTypeOption, TimeTypeOption}; -use collab_database::fields::media_type_option::MediaTypeOption; -use collab_database::fields::number_type_option::NumberTypeOption; -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::text_type_option::RichTextTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::fields::translate_type_option::TranslateTypeOption; -use collab_database::fields::url_type_option::URLTypeOption; -use collab_database::fields::TypeOptionData; - -pub async fn transform_type_option( - view_id: &str, - field_id: &str, - old_field_type: FieldType, - new_field_type: FieldType, - old_type_option_data: Option<TypeOptionData>, - new_type_option_data: TypeOptionData, - database: &mut Database, -) -> TypeOptionData { - if let Some(old_type_option_data) = old_type_option_data { - let mut transform_handler = - get_type_option_transform_handler(new_type_option_data, new_field_type); - transform_handler - .transform( - view_id, - field_id, - old_field_type, - old_type_option_data, - new_field_type, - database, - ) - .await; - transform_handler.to_type_option_data() - } else { - new_type_option_data - } -} - -/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait. -#[async_trait] -pub trait TypeOptionTransformHandler: Send + Sync { - async fn transform( - &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - old_type_option_data: TypeOptionData, - new_type_option_field_type: FieldType, - database: &mut Database, - ); - - fn to_type_option_data(&self) -> TypeOptionData; -} - -#[async_trait] -impl<T> TypeOptionTransformHandler for T -where - T: TypeOptionTransform + Clone, -{ - async fn transform( - &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - old_type_option_data: TypeOptionData, - new_type_option_field_type: FieldType, - database: &mut Database, - ) { - self - .transform_type_option( - view_id, - field_id, - old_type_option_field_type, - old_type_option_data, - new_type_option_field_type, - database, - ) - .await - } - - fn to_type_option_data(&self) -> TypeOptionData { - self.clone().into() - } -} - -fn get_type_option_transform_handler( - type_option_data: TypeOptionData, - field_type: FieldType, -) -> Box<dyn TypeOptionTransformHandler> { - match field_type { - FieldType::RichText => { - Box::new(RichTextTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Number => { - Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::DateTime => { - Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::LastEditedTime | FieldType::CreatedTime => { - Box::new(TimestampTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data)) - as Box<dyn TypeOptionTransformHandler>, - FieldType::MultiSelect => { - Box::new(MultiSelectTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Checkbox => { - Box::new(CheckboxTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::URL => { - Box::new(URLTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Checklist => { - Box::new(ChecklistTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Relation => { - Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)) - as Box<dyn TypeOptionTransformHandler>, - FieldType::Time => { - Box::new(TimeTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Translate => { - Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - FieldType::Media => { - Box::new(MediaTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> - }, - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs index 79e18c58aa..e2aa56de94 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs @@ -1,7 +1,8 @@ +use collab_database::{fields::Field, rows::Cell}; + use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB}; use crate::services::cell::insert_checkbox_cell; use crate::services::filter::PreFillCellsWithFilter; -use collab_database::{fields::Field, rows::Cell}; impl CheckboxFilterPB { pub fn is_visible(&self, cell_data: &CheckboxCellDataPB) -> bool { @@ -13,13 +14,16 @@ impl CheckboxFilterPB { } impl PreFillCellsWithFilter for CheckboxFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { let is_checked = match self.condition { CheckboxFilterConditionPB::IsChecked => Some(true), CheckboxFilterConditionPB::IsUnChecked => None, }; - is_checked.map(|is_checked| insert_checkbox_cell(is_checked, field)) + ( + is_checked.map(|is_checked| insert_checkbox_cell(is_checked, field)), + false, + ) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs index c0a0eadd86..3003357dea 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -9,12 +9,10 @@ mod tests { use crate::services::cell::CellDataDecoder; use crate::services::field::type_options::checkbox_type_option::*; use crate::services::field::FieldBuilder; - use collab_database::fields::checkbox_type_option::CheckboxTypeOption; - use collab_database::template::util::ToCellString; #[test] fn checkout_box_description_test() { - let type_option = CheckboxTypeOption; + let type_option = CheckboxTypeOption::default(); let field_type = FieldType::Checkbox; let field_rev = FieldBuilder::from_field_type(field_type).build(); @@ -47,7 +45,7 @@ mod tests { type_option .decode_cell(&CheckboxCellDataPB::from_str(input_str).unwrap().into()) .unwrap() - .to_cell_string(), + .to_string(), expected_str.to_owned() ); } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index 6afe4c4b57..de95ba058c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -1,20 +1,23 @@ use std::cmp::Ordering; use std::str::FromStr; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::fields::Field; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; -use collab_database::template::util::ToCellString; +use serde::{Deserialize, Serialize}; + use flowy_error::FlowyResult; use crate::entities::{CheckboxCellDataPB, CheckboxFilterPB, FieldType}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CheckboxTypeOption(); + impl TypeOption for CheckboxTypeOption { type CellData = CheckboxCellDataPB; type CellChangeset = CheckboxCellChangeset; @@ -24,16 +27,36 @@ impl TypeOption for CheckboxTypeOption { impl TypeOptionTransform for CheckboxTypeOption {} -impl CellDataProtobufEncoder for CheckboxTypeOption { +impl From<TypeOptionData> for CheckboxTypeOption { + fn from(_data: TypeOptionData) -> Self { + Self() + } +} + +impl From<CheckboxTypeOption> for TypeOptionData { + fn from(_data: CheckboxTypeOption) -> Self { + TypeOptionDataBuilder::new().build() + } +} + +impl TypeOptionCellDataSerde for CheckboxTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { cell_data } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(CheckboxCellDataPB::from(cell)) + } } impl CellDataDecoder for CheckboxTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + fn decode_cell_with_transform( &self, cell: &Cell, @@ -48,7 +71,16 @@ impl CellDataDecoder for CheckboxTypeOption { } fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { - cell_data.to_cell_string() + cell_data.to_string() + } + + fn numeric_cell(&self, cell: &Cell) -> Option<f64> { + let cell_data = self.parse_cell(cell).ok()?; + if cell_data.is_checked { + Some(1.0) + } else { + Some(0.0) + } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs index 55ca8dd77f..35de68136b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -1,9 +1,9 @@ use std::str::FromStr; use bytes::Bytes; -use collab::util::AnyMapExt; +use collab::core::any_map::AnyMapExtension; use collab_database::rows::{new_cell_builder, Cell}; -use collab_database::template::util::ToCellString; + use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{CheckboxCellDataPB, FieldType}; @@ -21,16 +21,16 @@ impl TypeOptionCellData for CheckboxCellDataPB { impl From<&Cell> for CheckboxCellDataPB { fn from(cell: &Cell) -> Self { - let value: String = cell.get_as(CELL_DATA).unwrap_or_default(); + let value = cell.get_str_value(CELL_DATA).unwrap_or_default(); CheckboxCellDataPB::from_str(&value).unwrap_or_default() } } impl From<CheckboxCellDataPB> for Cell { fn from(data: CheckboxCellDataPB) -> Self { - let mut cell = new_cell_builder(FieldType::Checkbox); - cell.insert(CELL_DATA.into(), data.to_cell_string().into()); - cell + new_cell_builder(FieldType::Checkbox) + .insert_str_value(CELL_DATA, data.to_string()) + .build() } } @@ -49,6 +49,16 @@ impl FromStr for CheckboxCellDataPB { } } +impl ToString for CheckboxCellDataPB { + fn to_string(&self) -> String { + if self.is_checked { + CHECK.to_string() + } else { + UNCHECK.to_string() + } + } +} + pub struct CheckboxCellDataParser(); impl CellProtobufBlobParser for CheckboxCellDataParser { type Object = CheckboxCellDataPB; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs new file mode 100644 index 0000000000..ceddeadce6 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs @@ -0,0 +1,198 @@ +use std::cmp::Ordering; + +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; + +use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, SelectOptionPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; +use crate::services::field::{ + SelectOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, SELECTION_IDS_SEPARATOR, +}; +use crate::services::sort::SortCondition; + +#[derive(Debug, Clone, Default)] +pub struct ChecklistTypeOption; + +impl TypeOption for ChecklistTypeOption { + type CellData = ChecklistCellData; + type CellChangeset = ChecklistCellChangeset; + type CellProtobufType = ChecklistCellDataPB; + type CellFilter = ChecklistFilterPB; +} + +impl From<TypeOptionData> for ChecklistTypeOption { + fn from(_data: TypeOptionData) -> Self { + Self + } +} + +impl From<ChecklistTypeOption> for TypeOptionData { + fn from(_data: ChecklistTypeOption) -> Self { + TypeOptionDataBuilder::new().build() + } +} + +impl TypeOptionCellDataSerde for ChecklistTypeOption { + fn protobuf_encode( + &self, + cell_data: <Self as TypeOption>::CellData, + ) -> <Self as TypeOption>::CellProtobufType { + let percentage = cell_data.percentage_complete(); + let selected_options = cell_data + .options + .iter() + .filter(|option| cell_data.selected_option_ids.contains(&option.id)) + .map(|option| SelectOptionPB::from(option.clone())) + .collect(); + + let options = cell_data + .options + .into_iter() + .map(SelectOptionPB::from) + .collect(); + + ChecklistCellDataPB { + options, + selected_options, + percentage, + } + } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(ChecklistCellData::from(cell)) + } +} + +impl CellDataChangeset for ChecklistTypeOption { + fn apply_changeset( + &self, + changeset: <Self as TypeOption>::CellChangeset, + cell: Option<Cell>, + ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { + match cell { + Some(cell) => { + let mut cell_data = self.parse_cell(&cell)?; + update_cell_data_with_changeset(&mut cell_data, changeset); + Ok((Cell::from(cell_data.clone()), cell_data)) + }, + None => { + let cell_data = ChecklistCellData::from_options(changeset.insert_options); + Ok((Cell::from(cell_data.clone()), cell_data)) + }, + } + } +} + +#[inline] +fn update_cell_data_with_changeset( + cell_data: &mut ChecklistCellData, + changeset: ChecklistCellChangeset, +) { + // Delete the options + cell_data + .options + .retain(|option| !changeset.delete_option_ids.contains(&option.id)); + cell_data + .selected_option_ids + .retain(|option_id| !changeset.delete_option_ids.contains(option_id)); + + // Insert new options + changeset + .insert_options + .into_iter() + .for_each(|(option_name, is_selected)| { + let option = SelectOption::new(&option_name); + if is_selected { + cell_data.selected_option_ids.push(option.id.clone()) + } + cell_data.options.push(option); + }); + + // Update options + changeset + .update_options + .into_iter() + .for_each(|updated_option| { + if let Some(option) = cell_data + .options + .iter_mut() + .find(|option| option.id == updated_option.id) + { + option.name = updated_option.name; + } + }); + + // Select the options + changeset + .selected_option_ids + .into_iter() + .for_each(|option_id| { + if let Some(index) = cell_data + .selected_option_ids + .iter() + .position(|id| **id == option_id) + { + cell_data.selected_option_ids.remove(index); + } else { + cell_data.selected_option_ids.push(option_id); + } + }); +} + +impl CellDataDecoder for ChecklistTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + + fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { + cell_data + .options + .into_iter() + .map(|option| option.name) + .collect::<Vec<_>>() + .join(SELECTION_IDS_SEPARATOR) + } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + // return the percentage complete if needed + None + } +} + +impl TypeOptionCellDataFilter for ChecklistTypeOption { + fn apply_filter( + &self, + filter: &<Self as TypeOption>::CellFilter, + cell_data: &<Self as TypeOption>::CellData, + ) -> bool { + let selected_options = cell_data.selected_options(); + filter.is_visible(&cell_data.options, &selected_options) + } +} + +impl TypeOptionCellDataCompare for ChecklistTypeOption { + fn apply_cmp( + &self, + cell_data: &<Self as TypeOption>::CellData, + other_cell_data: &<Self as TypeOption>::CellData, + sort_condition: SortCondition, + ) -> Ordering { + match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) { + (true, true) => Ordering::Equal, + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + (false, false) => { + let left = cell_data.percentage_complete(); + let right = other_cell_data.percentage_complete(); + // safe to unwrap because the two floats won't be NaN + let order = left.partial_cmp(&right).unwrap(); + sort_condition.evaluate_order(order) + }, + } + } +} + +impl TypeOptionTransform for ChecklistTypeOption {} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs new file mode 100644 index 0000000000..12b3e07527 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs @@ -0,0 +1,101 @@ +use crate::entities::FieldType; +use crate::services::field::{SelectOption, TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct ChecklistCellData { + pub options: Vec<SelectOption>, + pub selected_option_ids: Vec<String>, +} + +impl ToString for ChecklistCellData { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap_or_default() + } +} + +impl TypeOptionCellData for ChecklistCellData { + fn is_cell_empty(&self) -> bool { + self.options.is_empty() + } +} + +impl ChecklistCellData { + pub fn selected_options(&self) -> Vec<SelectOption> { + self + .options + .iter() + .filter(|option| self.selected_option_ids.contains(&option.id)) + .cloned() + .collect() + } + + pub fn percentage_complete(&self) -> f64 { + let selected_options = self.selected_option_ids.len(); + let total_options = self.options.len(); + + if total_options == 0 { + return 0.0; + } + ((selected_options as f64) / (total_options as f64) * 100.0).round() / 100.0 + } + + pub fn from_options(options: Vec<(String, bool)>) -> Self { + let (options, selected_ids): (Vec<_>, Vec<_>) = options + .into_iter() + .map(|(name, is_selected)| { + let option = SelectOption::new(&name); + let selected_id = is_selected.then(|| option.id.clone()); + (option, selected_id) + }) + .unzip(); + let selected_option_ids = selected_ids.into_iter().flatten().collect(); + + Self { + options, + selected_option_ids, + } + } +} + +impl From<&Cell> for ChecklistCellData { + fn from(cell: &Cell) -> Self { + cell + .get_str_value(CELL_DATA) + .map(|data| serde_json::from_str::<ChecklistCellData>(&data).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From<ChecklistCellData> for Cell { + fn from(cell_data: ChecklistCellData) -> Self { + let data = serde_json::to_string(&cell_data).unwrap_or_default(); + new_cell_builder(FieldType::Checklist) + .insert_str_value(CELL_DATA, data) + .build() + } +} + +#[derive(Debug, Clone, Default)] +pub struct ChecklistCellChangeset { + /// List of option names that will be inserted + pub insert_options: Vec<(String, bool)>, + pub selected_option_ids: Vec<String>, + pub delete_option_ids: Vec<String>, + pub update_options: Vec<SelectOption>, +} + +#[cfg(test)] +mod tests { + #[test] + fn test() { + let a = 1; + let b = 2; + + let c = (a as f32) / (b as f32); + println!("{}", c); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs index 5fa9c11242..91768a5cf3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs @@ -1,13 +1,9 @@ -use crate::entities::ChecklistCellDataChangesetPB; -use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB}; -use crate::services::filter::PreFillCellsWithFilter; - -use collab_database::fields::select_type_option::SelectOption; use collab_database::fields::Field; use collab_database::rows::Cell; -use collab_database::template::check_list_parse::ChecklistCellData; -use std::fmt::Debug; +use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB}; +use crate::services::field::SelectOption; +use crate::services::filter::PreFillCellsWithFilter; impl ChecklistFilterPB { pub fn is_visible( @@ -47,86 +43,7 @@ impl ChecklistFilterPB { } impl PreFillCellsWithFilter for ChecklistFilterPB { - fn get_compliant_cell(&self, _field: &Field) -> Option<Cell> { - None - } -} - -pub fn checklist_from_options(new_tasks: Vec<ChecklistCellInsertChangeset>) -> ChecklistCellData { - let (options, selected_ids): (Vec<_>, Vec<_>) = new_tasks - .into_iter() - .map(|new_task| { - let option = SelectOption::new(&new_task.name); - let selected_id = new_task.is_complete.then(|| option.id.clone()); - (option, selected_id) - }) - .unzip(); - let selected_option_ids = selected_ids.into_iter().flatten().collect(); - - ChecklistCellData { - options, - selected_option_ids, - } -} - -#[derive(Debug, Clone, Default)] -pub struct ChecklistCellChangeset { - pub insert_tasks: Vec<ChecklistCellInsertChangeset>, - pub delete_tasks: Vec<String>, - pub update_tasks: Vec<SelectOption>, - pub completed_task_ids: Vec<String>, - pub reorder: String, -} - -impl From<ChecklistCellDataChangesetPB> for ChecklistCellChangeset { - fn from(value: ChecklistCellDataChangesetPB) -> Self { - ChecklistCellChangeset { - insert_tasks: value - .insert_task - .into_iter() - .map(|pb| ChecklistCellInsertChangeset { - name: pb.name, - is_complete: false, - index: pb.index, - }) - .collect(), - delete_tasks: value.delete_tasks, - update_tasks: value - .update_tasks - .into_iter() - .map(SelectOption::from) - .collect(), - completed_task_ids: value.completed_tasks, - reorder: value.reorder, - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct ChecklistCellInsertChangeset { - pub name: String, - pub is_complete: bool, - pub index: Option<i32>, -} - -impl ChecklistCellInsertChangeset { - pub fn new(name: String, is_complete: bool) -> Self { - Self { - name, - is_complete, - index: None, - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test() { - let a = 1; - let b = 2; - - let c = (a as f32) / (b as f32); - println!("{}", c); + fn get_compliant_cell(&self, _field: &Field) -> (Option<Cell>, bool) { + (None, true) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_type_option.rs deleted file mode 100644 index a9043f227c..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_type_option.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::entities::{ChecklistCellDataPB, ChecklistFilterPB, SelectOptionPB}; -use crate::services::cell::{CellDataChangeset, CellDataDecoder}; -use crate::services::field::checklist_filter::{checklist_from_options, ChecklistCellChangeset}; -use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, - TypeOptionTransform, -}; -use crate::services::sort::SortCondition; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::select_type_option::{SelectOption, SELECTION_IDS_SEPARATOR}; -use collab_database::rows::Cell; -use collab_database::template::check_list_parse::ChecklistCellData; -use collab_database::template::util::TypeOptionCellData; -use flowy_error::FlowyResult; -use std::cmp::Ordering; - -impl TypeOption for ChecklistTypeOption { - type CellData = ChecklistCellData; - type CellChangeset = ChecklistCellChangeset; - type CellProtobufType = ChecklistCellDataPB; - type CellFilter = ChecklistFilterPB; -} - -impl CellDataProtobufEncoder for ChecklistTypeOption { - fn protobuf_encode( - &self, - cell_data: <Self as TypeOption>::CellData, - ) -> <Self as TypeOption>::CellProtobufType { - let percentage = cell_data.percentage_complete(); - let selected_options = cell_data - .options - .iter() - .filter(|option| cell_data.selected_option_ids.contains(&option.id)) - .map(|option| SelectOptionPB::from(option.clone())) - .collect(); - - let options = cell_data - .options - .into_iter() - .map(SelectOptionPB::from) - .collect(); - - ChecklistCellDataPB { - options, - selected_options, - percentage, - } - } -} - -impl CellDataChangeset for ChecklistTypeOption { - fn apply_changeset( - &self, - changeset: <Self as TypeOption>::CellChangeset, - cell: Option<Cell>, - ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { - match cell { - Some(cell) => { - let mut cell_data = self.decode_cell(&cell)?; - update_cell_data_with_changeset(&mut cell_data, changeset); - Ok((Cell::from(cell_data.clone()), cell_data)) - }, - None => { - let cell_data = checklist_from_options(changeset.insert_tasks); - Ok((Cell::from(cell_data.clone()), cell_data)) - }, - } - } -} - -#[inline] -fn update_cell_data_with_changeset( - cell_data: &mut ChecklistCellData, - changeset: ChecklistCellChangeset, -) { - // Delete the options - cell_data - .options - .retain(|option| !changeset.delete_tasks.contains(&option.id)); - cell_data - .selected_option_ids - .retain(|option_id| !changeset.delete_tasks.contains(option_id)); - - // Insert new options - changeset.insert_tasks.into_iter().for_each(|new_task| { - let option = SelectOption::new(&new_task.name); - if new_task.is_complete { - cell_data.selected_option_ids.push(option.id.clone()) - } - match new_task.index { - Some(index) => cell_data.options.insert(index as usize, option), - None => cell_data.options.push(option), - }; - }); - - // Update options - changeset - .update_tasks - .into_iter() - .for_each(|updated_option| { - if let Some(option) = cell_data - .options - .iter_mut() - .find(|option| option.id == updated_option.id) - { - option.name = updated_option.name; - } - }); - - // Select the options - changeset - .completed_task_ids - .into_iter() - .for_each(|option_id| { - if let Some(index) = cell_data - .selected_option_ids - .iter() - .position(|id| **id == option_id) - { - cell_data.selected_option_ids.remove(index); - } else { - cell_data.selected_option_ids.push(option_id); - } - }); - - // Reorder - let mut split = changeset.reorder.split(' ').take(2); - if let (Some(from), Some(to)) = (split.next(), split.next()) { - if let (Some(from_index), Some(to_index)) = ( - cell_data - .options - .iter() - .position(|option| option.id == from), - cell_data.options.iter().position(|option| option.id == to), - ) { - let option = cell_data.options.remove(from_index); - cell_data.options.insert(to_index, option); - } - } -} - -impl CellDataDecoder for ChecklistTypeOption { - fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { - cell_data - .options - .into_iter() - .map(|option| option.name) - .collect::<Vec<_>>() - .join(SELECTION_IDS_SEPARATOR) - } -} - -impl TypeOptionCellDataFilter for ChecklistTypeOption { - fn apply_filter( - &self, - filter: &<Self as TypeOption>::CellFilter, - cell_data: &<Self as TypeOption>::CellData, - ) -> bool { - let selected_options = cell_data.selected_options(); - filter.is_visible(&cell_data.options, &selected_options) - } -} - -impl TypeOptionCellDataCompare for ChecklistTypeOption { - fn apply_cmp( - &self, - cell_data: &<Self as TypeOption>::CellData, - other_cell_data: &<Self as TypeOption>::CellData, - sort_condition: SortCondition, - ) -> Ordering { - match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) { - (true, true) => Ordering::Equal, - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (false, false) => { - let left = cell_data.percentage_complete(); - let right = other_cell_data.percentage_complete(); - // safe to unwrap because the two floats won't be NaN - let order = left.partial_cmp(&right).unwrap(); - sort_condition.evaluate_order(order) - }, - } - } -} - -impl TypeOptionTransform for ChecklistTypeOption {} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/mod.rs index 85ff615900..be51a38db8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/mod.rs @@ -1,3 +1,6 @@ -#![allow(clippy::module_inception)] -pub mod checklist_filter; -pub mod checklist_type_option; +mod checklist; +mod checklist_entities; +mod checklist_filter; + +pub use checklist::*; +pub use checklist_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs index 3f0808fac2..42a0300e18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs @@ -1,81 +1,38 @@ use crate::entities::{DateFilterConditionPB, DateFilterPB}; use crate::services::cell::insert_date_cell; +use crate::services::field::DateCellData; use crate::services::filter::PreFillCellsWithFilter; -use bytes::Bytes; -use chrono::{Duration, Local, NaiveDate, TimeZone}; -use collab_database::fields::date_type_option::DateCellData; +use chrono::{Duration, NaiveDate, NaiveDateTime}; use collab_database::fields::Field; use collab_database::rows::Cell; -use collab_database::template::timestamp_parse::TimestampCellData; -use flowy_error::{internal_error, FlowyResult}; - -use crate::entities::DateCellDataPB; -use crate::services::cell::CellProtobufBlobParser; impl DateFilterPB { /// Returns `None` if the DateFilterPB doesn't have the necessary data for /// the condition. For example, `start` and `end` timestamps for - /// `DateFilterConditionPB::DateStartsBetween`. + /// `DateFilterConditionPB::DateWithin`. pub fn is_visible(&self, cell_data: &DateCellData) -> Option<bool> { - let strategy = self.get_strategy()?; - - let timestamp = if self.condition.is_filter_on_start_timestamp() { - cell_data.timestamp - } else { - cell_data.end_timestamp.or(cell_data.timestamp) - }; - - Some(strategy.filter(timestamp)) - } - - pub fn is_timestamp_cell_data_visible(&self, cell_data: &TimestampCellData) -> Option<bool> { - let strategy = self.get_strategy()?; - - Some(strategy.filter(cell_data.timestamp)) - } - - fn get_strategy(&self) -> Option<DateFilterStrategy> { let strategy = match self.condition { - DateFilterConditionPB::DateStartsOn | DateFilterConditionPB::DateEndsOn => { - DateFilterStrategy::On(self.timestamp?) - }, - DateFilterConditionPB::DateStartsBefore | DateFilterConditionPB::DateEndsBefore => { - DateFilterStrategy::Before(self.timestamp?) - }, - DateFilterConditionPB::DateStartsAfter | DateFilterConditionPB::DateEndsAfter => { - DateFilterStrategy::After(self.timestamp?) - }, - DateFilterConditionPB::DateStartsOnOrBefore | DateFilterConditionPB::DateEndsOnOrBefore => { - DateFilterStrategy::OnOrBefore(self.timestamp?) - }, - DateFilterConditionPB::DateStartsOnOrAfter | DateFilterConditionPB::DateEndsOnOrAfter => { - DateFilterStrategy::OnOrAfter(self.timestamp?) - }, - DateFilterConditionPB::DateStartsBetween | DateFilterConditionPB::DateEndsBetween => { - DateFilterStrategy::DateBetween { - start: self.start?, - end: self.end?, - } - }, - DateFilterConditionPB::DateStartIsEmpty | DateFilterConditionPB::DateEndIsEmpty => { - DateFilterStrategy::Empty - }, - DateFilterConditionPB::DateStartIsNotEmpty | DateFilterConditionPB::DateEndIsNotEmpty => { - DateFilterStrategy::NotEmpty + DateFilterConditionPB::DateIs => DateFilterStrategy::On(self.timestamp?), + DateFilterConditionPB::DateBefore => DateFilterStrategy::Before(self.timestamp?), + DateFilterConditionPB::DateAfter => DateFilterStrategy::After(self.timestamp?), + DateFilterConditionPB::DateOnOrBefore => DateFilterStrategy::OnOrBefore(self.timestamp?), + DateFilterConditionPB::DateOnOrAfter => DateFilterStrategy::OnOrAfter(self.timestamp?), + DateFilterConditionPB::DateWithIn => DateFilterStrategy::DateWithin { + start: self.start?, + end: self.end?, }, + DateFilterConditionPB::DateIsEmpty => DateFilterStrategy::Empty, + DateFilterConditionPB::DateIsNotEmpty => DateFilterStrategy::NotEmpty, }; - Some(strategy) + Some(strategy.filter(cell_data)) } } #[inline] fn naive_date_from_timestamp(timestamp: i64) -> Option<NaiveDate> { - Local - .timestamp_opt(timestamp, 0) - .single() - .map(|date_time| date_time.date_naive()) + NaiveDateTime::from_timestamp_opt(timestamp, 0).map(|date_time: NaiveDateTime| date_time.date()) } enum DateFilterStrategy { @@ -84,193 +41,134 @@ enum DateFilterStrategy { After(i64), OnOrBefore(i64), OnOrAfter(i64), - DateBetween { start: i64, end: i64 }, + DateWithin { start: i64, end: i64 }, Empty, NotEmpty, } impl DateFilterStrategy { - fn filter(self, cell_data: Option<i64>) -> bool { + fn filter(self, cell_data: &DateCellData) -> bool { match self { - DateFilterStrategy::On(expected_timestamp) => cell_data.is_some_and(|timestamp| { + DateFilterStrategy::On(expected_timestamp) => cell_data.timestamp.is_some_and(|timestamp| { let cell_date = naive_date_from_timestamp(timestamp); let expected_date = naive_date_from_timestamp(expected_timestamp); cell_date == expected_date }), - DateFilterStrategy::Before(expected_timestamp) => cell_data.is_some_and(|timestamp| { - let cell_date = naive_date_from_timestamp(timestamp); - let expected_date = naive_date_from_timestamp(expected_timestamp); - cell_date < expected_date - }), - DateFilterStrategy::After(expected_timestamp) => cell_data.is_some_and(|timestamp| { - let cell_date = naive_date_from_timestamp(timestamp); - let expected_date = naive_date_from_timestamp(expected_timestamp); - cell_date > expected_date - }), - DateFilterStrategy::OnOrBefore(expected_timestamp) => cell_data.is_some_and(|timestamp| { - let cell_date = naive_date_from_timestamp(timestamp); - let expected_date = naive_date_from_timestamp(expected_timestamp); - cell_date <= expected_date - }), - DateFilterStrategy::OnOrAfter(expected_timestamp) => cell_data.is_some_and(|timestamp| { - let cell_date = naive_date_from_timestamp(timestamp); - let expected_date = naive_date_from_timestamp(expected_timestamp); - cell_date >= expected_date - }), - DateFilterStrategy::DateBetween { start, end } => cell_data.is_some_and(|timestamp| { - let cell_date = naive_date_from_timestamp(timestamp); - let expected_start_date = naive_date_from_timestamp(start); - let expected_end_date = naive_date_from_timestamp(end); - cell_date >= expected_start_date && cell_date <= expected_end_date - }), - DateFilterStrategy::Empty => match cell_data { - None => true, - Some(timestamp) if naive_date_from_timestamp(timestamp).is_none() => true, - _ => false, + DateFilterStrategy::Before(expected_timestamp) => { + cell_data.timestamp.is_some_and(|timestamp| { + let cell_date = naive_date_from_timestamp(timestamp); + let expected_date = naive_date_from_timestamp(expected_timestamp); + cell_date < expected_date + }) }, - DateFilterStrategy::NotEmpty => { - matches!(cell_data, Some(timestamp) if naive_date_from_timestamp(timestamp).is_some() ) + DateFilterStrategy::After(expected_timestamp) => { + cell_data.timestamp.is_some_and(|timestamp| { + let cell_date = naive_date_from_timestamp(timestamp); + let expected_date = naive_date_from_timestamp(expected_timestamp); + cell_date > expected_date + }) }, + DateFilterStrategy::OnOrBefore(expected_timestamp) => { + cell_data.timestamp.is_some_and(|timestamp| { + let cell_date = naive_date_from_timestamp(timestamp); + let expected_date = naive_date_from_timestamp(expected_timestamp); + cell_date <= expected_date + }) + }, + DateFilterStrategy::OnOrAfter(expected_timestamp) => { + cell_data.timestamp.is_some_and(|timestamp| { + let cell_date = naive_date_from_timestamp(timestamp); + let expected_date = naive_date_from_timestamp(expected_timestamp); + cell_date >= expected_date + }) + }, + DateFilterStrategy::DateWithin { start, end } => { + cell_data.timestamp.is_some_and(|timestamp| { + let cell_date = naive_date_from_timestamp(timestamp); + let expected_start_date = naive_date_from_timestamp(start); + let expected_end_date = naive_date_from_timestamp(end); + cell_date >= expected_start_date && cell_date <= expected_end_date + }) + }, + DateFilterStrategy::Empty => { + cell_data.timestamp.is_none() && cell_data.end_timestamp.is_none() + }, + DateFilterStrategy::NotEmpty => cell_data.timestamp.is_some(), } } } impl PreFillCellsWithFilter for DateFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { - let start_timestamp = match self.condition { - DateFilterConditionPB::DateStartsOn - | DateFilterConditionPB::DateStartsOnOrBefore - | DateFilterConditionPB::DateStartsOnOrAfter - | DateFilterConditionPB::DateEndsOn - | DateFilterConditionPB::DateEndsOnOrBefore - | DateFilterConditionPB::DateEndsOnOrAfter => self.timestamp, - DateFilterConditionPB::DateStartsBefore | DateFilterConditionPB::DateEndsBefore => self + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { + let timestamp = match self.condition { + DateFilterConditionPB::DateIs + | DateFilterConditionPB::DateOnOrBefore + | DateFilterConditionPB::DateOnOrAfter => self.timestamp, + DateFilterConditionPB::DateBefore => self .timestamp - .and_then(|timestamp| { - Local - .timestamp_opt(timestamp, 0) - .single() - .map(|date| date.naive_local()) - }) - .and_then(|date_time| { + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { let answer = date_time - Duration::days(1); - Local - .from_local_datetime(&answer) - .single() - .map(|date_time| date_time.timestamp()) + answer.timestamp() }), - DateFilterConditionPB::DateStartsAfter | DateFilterConditionPB::DateEndsAfter => self + DateFilterConditionPB::DateAfter => self .timestamp - .and_then(|timestamp| { - Local - .timestamp_opt(timestamp, 0) - .single() - .map(|date| date.naive_local()) - }) - .and_then(|date_time| { + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { let answer = date_time + Duration::days(1); - Local - .from_local_datetime(&answer) - .single() - .map(|date_time| date_time.timestamp()) + answer.timestamp() }), - DateFilterConditionPB::DateStartsBetween | DateFilterConditionPB::DateEndsBetween => { - self.start - }, + DateFilterConditionPB::DateWithIn => self.start, _ => None, }; - start_timestamp.map(|timestamp| insert_date_cell(timestamp, None, None, field)) - } -} + let open_after_create = matches!(self.condition, DateFilterConditionPB::DateIsNotEmpty); -#[derive(Clone, Debug, Default)] -pub struct DateCellChangeset { - pub timestamp: Option<i64>, - pub end_timestamp: Option<i64>, - pub include_time: Option<bool>, - pub is_range: Option<bool>, - pub clear_flag: Option<bool>, - pub reminder_id: Option<String>, -} - -pub struct DateCellDataParser(); -impl CellProtobufBlobParser for DateCellDataParser { - type Object = DateCellDataPB; - - fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> { - DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + ( + timestamp.map(|timestamp| insert_date_cell(timestamp, None, None, field)), + open_after_create, + ) } } #[cfg(test)] mod tests { use crate::entities::{DateFilterConditionPB, DateFilterPB}; - use collab_database::fields::date_type_option::DateCellData; + use crate::services::field::DateCellData; - fn to_cell_data(timestamp: Option<i64>, end_timestamp: Option<i64>) -> DateCellData { - DateCellData { - timestamp, - end_timestamp, - ..Default::default() - } + fn to_cell_data(timestamp: i32) -> DateCellData { + DateCellData::new(timestamp as i64, false, false, "".to_string()) } #[test] fn date_filter_is_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsOn, + condition: DateFilterConditionPB::DateIs, timestamp: Some(1668387885), end: None, start: None, }; - for (start, end, is_visible) in [ - (Some(1668387885), None, true), - (Some(1647251762), None, false), - (None, None, false), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible - ); - } - - let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsOn, - timestamp: None, - end: None, - start: None, - }; - - for (start, end, is_visible) in [ - (Some(1668387885), None, true), - (Some(1647251762), None, true), - (None, None, true), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible - ); + for (val, visible) in [(1668387885, true), (1647251762, false)] { + assert_eq!(filter.is_visible(&to_cell_data(val)).unwrap(), visible); } } #[test] fn date_filter_before_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsBefore, + condition: DateFilterConditionPB::DateBefore, timestamp: Some(1668387885), start: None, end: None, }; - for (start, end, is_visible) in [ - (Some(1668387884), None, false), - (Some(1647251762), None, true), - ] { + for (val, visible, msg) in [(1668387884, false, "1"), (1647251762, true, "2")] { assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible, + filter.is_visible(&to_cell_data(val)).unwrap(), + visible, + "{}", + msg ); } } @@ -278,327 +176,67 @@ mod tests { #[test] fn date_filter_before_or_on_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsOnOrBefore, + condition: DateFilterConditionPB::DateOnOrBefore, timestamp: Some(1668387885), start: None, end: None, }; - for (start, end, is_visible) in [ - (Some(1668387884), None, true), - (Some(1668387885), None, true), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible - ); + for (val, visible) in [(1668387884, true), (1668387885, true)] { + assert_eq!(filter.is_visible(&to_cell_data(val)).unwrap(), visible); } } #[test] fn date_filter_after_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsAfter, + condition: DateFilterConditionPB::DateAfter, timestamp: Some(1668387885), start: None, end: None, }; - for (start, end, is_visible) in [ - (Some(1668387888), None, false), - (Some(1668531885), None, true), - (Some(0), None, false), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible - ); + for (val, visible) in [(1668387888, false), (1668531885, true), (0, false)] { + assert_eq!(filter.is_visible(&to_cell_data(val)).unwrap(), visible); } } #[test] fn date_filter_within_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsBetween, + condition: DateFilterConditionPB::DateWithIn, start: Some(1668272685), // 11/13 end: Some(1668618285), // 11/17 timestamp: None, }; - for (start, end, is_visible, msg) in [ - (Some(1668272685), None, true, "11/13"), - (Some(1668359085), None, true, "11/14"), - (Some(1668704685), None, false, "11/18"), - (None, None, false, "empty"), + for (val, visible, _msg) in [ + (1668272685, true, "11/13"), + (1668359085, true, "11/14"), + (1668704685, false, "11/18"), ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible, - "{msg}" - ); - } - - let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartsBetween, - start: None, - end: Some(1668618285), // 11/17 - timestamp: None, - }; - - for (start, end, is_visible, msg) in [ - (Some(1668272685), None, true, "11/13"), - (Some(1668359085), None, true, "11/14"), - (Some(1668704685), None, true, "11/18"), - (None, None, true, "empty"), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible, - "{msg}" - ); + assert_eq!(filter.is_visible(&to_cell_data(val)).unwrap(), visible); } } #[test] fn date_filter_is_empty_test() { let filter = DateFilterPB { - condition: DateFilterConditionPB::DateStartIsEmpty, + condition: DateFilterConditionPB::DateIsEmpty, start: None, end: None, timestamp: None, }; - for (start, end, is_visible) in [(None, None, true), (Some(123), None, false)] { + for (val, visible) in [(None, true), (Some(123), false)] { assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible + filter + .is_visible(&DateCellData { + timestamp: val, + ..Default::default() + }) + .unwrap(), + visible ); } } - - #[test] - fn date_filter_end_test() { - let filter = DateFilterPB { - condition: DateFilterConditionPB::DateEndsOnOrBefore, - timestamp: Some(1668359085), // 11/14 - end: None, - start: None, - }; - - for (start, end, is_visible, msg) in [ - (Some(1668272685), None, true, "11/13"), - (Some(1668359085), None, true, "11/14"), - (Some(1668704685), None, false, "11/18"), - (None, None, false, "empty"), - (Some(1668272685), Some(1668272685), true, "11/13"), - (Some(1668272685), Some(1668359085), true, "11/14"), - (Some(1668272685), Some(1668704685), false, "11/18"), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible, - "{msg}" - ); - } - - let filter = DateFilterPB { - condition: DateFilterConditionPB::DateEndsOnOrBefore, - timestamp: None, - start: None, - end: None, - }; - - for (start, end, is_visible, msg) in [ - (Some(1668272685), Some(1668272685), true, "11/13"), - (Some(1668272685), Some(1668359085), true, "11/14"), - (Some(1668272685), Some(1668704685), true, "11/18"), - (None, None, true, "empty"), - ] { - assert_eq!( - filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - is_visible, - "{msg}" - ); - } - } - - // #[test] - // fn timezoned_filter_test() { - // let filter = DateFilterPB { - // condition: DateFilterConditionPB::DateStartsOn, - // timestamp: Some(1728975660), // Oct 15, 2024 00:00 PDT - // end: None, - // start: None, - // }; - // - // for (start, end, is_visible, msg) in [ - // ( - // Some(1728889200), - // None, - // false, - // "10/14/2024 00:00 PDT, 10/14/2024 07:00 GMT", - // ), - // ( - // Some(1728889260), - // None, - // false, - // "10/14/2024 00:01 PDT, 10/14/2024 07:01 GMT", - // ), - // ( - // Some(1728900000), - // None, - // false, - // "10/14/2024 03:00 PDT, 10/14/2024 10:00 GMT", - // ), - // ( - // Some(1728921600), - // None, - // false, - // "10/14/2024 09:00 PDT, 10/14/2024 16:00 GMT", - // ), - // ( - // Some(1728932400), - // None, - // false, - // "10/14/2024 12:00 PDT, 10/14/2024 19:00 GMT", - // ), - // ( - // Some(1728943200), - // None, - // false, - // "10/14/2024 15:00 PDT, 10/14/2024 22:00 GMT", - // ), - // ( - // Some(1728954000), - // None, - // false, - // "10/14/2024 18:00 PDT, 10/15/2024 01:00 GMT", - // ), - // ( - // Some(1728964800), - // None, - // false, - // "10/14/2024 21:00 PDT, 10/15/2024 04:00 GMT", - // ), - // ( - // Some(1728975540), - // None, - // false, - // "10/14/2024 23:59 PDT, 10/15/2024 06:59 GMT", - // ), - // ( - // Some(1728975600), - // None, - // true, - // "10/15/2024 00:00 PDT, 10/15/2024 07:00 GMT", - // ), - // ( - // Some(1728975660), - // None, - // true, - // "10/15/2024 00:01 PDT, 10/15/2024 07:01 GMT", - // ), - // ( - // Some(1728986400), - // None, - // true, - // "10/15/2024 03:00 PDT, 10/15/2024 10:00 GMT", - // ), - // ( - // Some(1729008000), - // None, - // true, - // "10/15/2024 09:00 PDT, 10/15/2024 16:00 GMT", - // ), - // ( - // Some(1729018800), - // None, - // true, - // "10/15/2024 12:00 PDT, 10/15/2024 19:00 GMT", - // ), - // ( - // Some(1729029600), - // None, - // true, - // "10/15/2024 15:00 PDT, 10/15/2024 22:00 GMT", - // ), - // ( - // Some(1729040400), - // None, - // true, - // "10/15/2024 18:00 PDT, 10/16/2024 01:00 GMT", - // ), - // ( - // Some(1729051200), - // None, - // true, - // "10/15/2024 21:00 PDT, 10/16/2024 04:00 GMT", - // ), - // ( - // Some(1729061940), - // None, - // true, - // "10/15/2024 23:59 PDT, 10/16/2024 06:59 GMT", - // ), - // ( - // Some(1729062000), - // None, - // false, - // "10/16/2024 00:00 PDT, 10/16/2024 07:00 GMT", - // ), - // ( - // Some(1729062060), - // None, - // false, - // "10/16/2024 00:01 PDT, 10/16/2024 07:01 GMT", - // ), - // ( - // Some(1729072800), - // None, - // false, - // "10/16/2024 03:00 PDT, 10/16/2024 10:00 GMT", - // ), - // ( - // Some(1729094400), - // None, - // false, - // "10/16/2024 09:00 PDT, 10/16/2024 16:00 GMT", - // ), - // ( - // Some(1729105200), - // None, - // false, - // "10/16/2024 12:00 PDT, 10/16/2024 19:00 GMT", - // ), - // ( - // Some(1729116000), - // None, - // false, - // "10/16/2024 15:00 PDT, 10/16/2024 22:00 GMT", - // ), - // ( - // Some(1729126800), - // None, - // false, - // "10/16/2024 18:00 PDT, 10/17/2024 01:00 GMT", - // ), - // ( - // Some(1729137600), - // None, - // false, - // "10/16/2024 21:00 PDT, 10/17/2024 04:00 GMT", - // ), - // ( - // Some(1729148340), - // None, - // false, - // "10/16/2024 23:59 PDT, 10/17/2024 06:59 GMT", - // ), - // ] { - // assert_eq!( - // filter.is_visible(&to_cell_data(start, end)).unwrap_or(true), - // is_visible, - // "{msg}" - // ); - // } - // } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs index dbaf0be3ea..8c67f8bc5c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs @@ -1,238 +1,554 @@ #[cfg(test)] mod tests { + use chrono::format::strftime::StrftimeItems; + use chrono::{FixedOffset, NaiveDateTime}; + use collab_database::fields::Field; use collab_database::rows::Cell; + use strum::IntoEnumIterator; + use crate::entities::FieldType; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; - use crate::services::field::date_type_option::date_filter::DateCellChangeset; - use collab_database::fields::date_type_option::{DateCellData, DateTypeOption}; + use crate::services::field::{ + DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat, + }; #[test] - fn apply_changeset_to_empty_cell() { - let type_option = DateTypeOption::default_utc(); + fn date_type_option_date_format_test() { + let mut type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + for date_format in DateFormat::iter() { + type_option.date_format = date_format; + match date_format { + DateFormat::Friendly => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1647251762), + time: None, + include_time: None, + ..Default::default() + }, + None, + "Mar 14, 2022", + ); + }, + DateFormat::US => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1647251762), + time: None, + include_time: None, + ..Default::default() + }, + None, + "2022/03/14", + ); + }, + DateFormat::ISO => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1647251762), + time: None, + include_time: None, + ..Default::default() + }, + None, + "2022-03-14", + ); + }, + DateFormat::Local => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1647251762), + time: None, + include_time: None, + ..Default::default() + }, + None, + "03/14/2022", + ); + }, + DateFormat::DayMonthYear => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1647251762), + time: None, + include_time: None, + ..Default::default() + }, + None, + "14/03/2022", + ); + }, + } + } + } + + #[test] + fn date_type_option_different_time_format_test() { + let mut type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + for time_format in TimeFormat::iter() { + type_option.time_format = time_format; + match time_format { + TimeFormat::TwentyFourHour => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: None, + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 00:00", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("9:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 09:00", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("23:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 23:00", + ); + }, + TimeFormat::TwelveHour => { + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: None, + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 12:00 AM", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("9:00 AM".to_owned()), + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 09:00 AM", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("11:23 pm".to_owned()), + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 11:23 PM", + ); + }, + } + } + } + + #[test] + #[should_panic] + fn date_type_option_invalid_include_time_str_test() { + let field_type = FieldType::DateTime; + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(field_type).build(); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: Some(1653782400), + date: Some(1653609600), + time: Some("1:".to_owned()), include_time: Some(true), ..Default::default() }, None, - &DateCellData { - timestamp: Some(1653782400), - include_time: true, - ..Default::default() - }, - ); - - assert_date( - &type_option, - DateCellChangeset { - timestamp: Some(1625130000), - end_timestamp: Some(1653782400), - is_range: Some(true), - ..Default::default() - }, - None, - &DateCellData { - timestamp: Some(1625130000), - end_timestamp: Some(1653782400), - is_range: true, - ..Default::default() - }, + "May 27, 2022 01:00", ); } #[test] - fn apply_changeset_to_exsiting_cell() { - let type_option = DateTypeOption::default_utc(); + #[should_panic] + fn date_type_option_empty_include_time_str_test() { + let field_type = FieldType::DateTime; + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(field_type).build(); - let date_cell = initialize_date_cell( - &type_option, - DateCellChangeset { - timestamp: Some(1653782400), - ..Default::default() - }, - ); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: Some(1625130000), - ..Default::default() - }, - Some(date_cell.clone()), - &DateCellData { - timestamp: Some(1625130000), - ..Default::default() - }, - ); - - let date_cell = initialize_date_cell( - &type_option, - DateCellChangeset { - timestamp: Some(1653782400), - ..Default::default() - }, - ); - assert_date( - &type_option, - DateCellChangeset { - timestamp: Some(1653782400), + date: Some(1653609600), + time: Some("".to_owned()), include_time: Some(true), ..Default::default() }, - Some(date_cell.clone()), - &DateCellData { - timestamp: Some(1653782400), - include_time: true, - ..Default::default() - }, - ); - - let date_cell = initialize_date_cell( - &type_option, - DateCellChangeset { - timestamp: Some(1653782400), - end_timestamp: Some(1653782400), - is_range: Some(true), - ..Default::default() - }, - ); - assert_date( - &type_option, - DateCellChangeset { - timestamp: Some(1625130000), - end_timestamp: Some(1625130000), - ..Default::default() - }, - Some(date_cell.clone()), - &DateCellData { - timestamp: Some(1625130000), - end_timestamp: Some(1625130000), - is_range: true, - ..Default::default() - }, - ); - - let date_cell = initialize_date_cell( - &type_option, - DateCellChangeset { - timestamp: Some(1653782400), - end_timestamp: Some(1653782400), - is_range: Some(true), - ..Default::default() - }, - ); - assert_date( - &type_option, - DateCellChangeset { - is_range: Some(false), - ..Default::default() - }, - Some(date_cell.clone()), - &DateCellData { - timestamp: Some(1653782400), - is_range: false, - ..Default::default() - }, + None, + "May 27, 2022 01:00", ); } #[test] - fn apply_invalid_changeset_to_empty_cell() { - let type_option = DateTypeOption::default_utc(); - + fn date_type_midnight_include_time_str_test() { + let field_type = FieldType::DateTime; + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(field_type).build(); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: Some(1653782400), - end_timestamp: Some(1653782400), - is_range: Some(false), + date: Some(1653609600), + time: Some("00:00".to_owned()), + include_time: Some(true), ..Default::default() }, None, - &DateCellData::default(), + "May 27, 2022 00:00", ); + } + /// The default time format is TwentyFourHour, so the include_time_str in + /// twelve_hours_format will cause parser error. + #[test] + #[should_panic] + fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: None, - end_timestamp: Some(1653782400), - is_range: Some(true), + date: Some(1653609600), + time: Some("1:00 am".to_owned()), + include_time: Some(true), ..Default::default() }, None, - &DateCellData::default(), + "May 27, 2022 01:00 AM", + ); + } + + /// Attempting to parse include_time_str as TwelveHour when TwentyFourHour + /// format is given should cause parser error. + #[test] + #[should_panic] + fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() { + let field_type = FieldType::DateTime; + let mut type_option = DateTypeOption::test(); + type_option.time_format = TimeFormat::TwelveHour; + let field = FieldBuilder::from_field_type(field_type).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("20:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 08:00 PM", ); } #[test] - fn apply_invalid_changeset_to_existing_cell() { - let type_option = DateTypeOption::default_utc(); + fn utc_to_native_test() { + let native_timestamp = 1647251762; + let native = NaiveDateTime::from_timestamp_opt(native_timestamp, 0).unwrap(); - // is_range is false but a date range is passed in - let date_cell = initialize_date_cell( + let utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(native, chrono::Utc); + // utc_timestamp doesn't carry timezone + let utc_timestamp = utc.timestamp(); + assert_eq!(native_timestamp, utc_timestamp); + + let format = "%m/%d/%Y %I:%M %p".to_string(); + let native_time_str = format!("{}", native.format_with_items(StrftimeItems::new(&format))); + let utc_time_str = format!("{}", utc.format_with_items(StrftimeItems::new(&format))); + assert_eq!(native_time_str, utc_time_str); + + // Mon Mar 14 2022 17:56:02 GMT+0800 (China Standard Time) + let gmt_8_offset = FixedOffset::east_opt(8 * 3600).unwrap(); + let china_local = + chrono::DateTime::<chrono::Local>::from_naive_utc_and_offset(native, gmt_8_offset); + let china_local_time = format!( + "{}", + china_local.format_with_items(StrftimeItems::new(&format)) + ); + + assert_eq!(china_local_time, "03/14/2022 05:56 PM"); + } + + /// The time component shouldn't remain the same since the timestamp is + /// completely overwritten. To achieve the desired result, also pass in the + /// time string along with the new timestamp. + #[test] + #[should_panic] + fn update_date_keep_time() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( &type_option, DateCellChangeset { - timestamp: Some(1653782400), - is_range: Some(false), + date: Some(1700006400), + time: Some("08:00".to_owned()), + include_time: Some(true), ..Default::default() }, ); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: Some(1653782400), - end_timestamp: Some(1653782400), + date: Some(1701302400), + time: None, + include_time: None, ..Default::default() }, - Some(date_cell.clone()), - &decode_cell_data(&date_cell, &type_option), + Some(old_cell_data), + "Nov 30, 2023 08:00", ); + } - // is_range is true but either the start or end is missing - let date_cell = initialize_date_cell( + #[test] + fn update_time_keep_date() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( &type_option, DateCellChangeset { - timestamp: Some(1653782400), - end_timestamp: Some(1653782400), - is_range: Some(false), + date: Some(1700006400), + time: Some("08:00".to_owned()), + include_time: Some(true), ..Default::default() }, ); assert_date( &type_option, + &field, DateCellChangeset { - timestamp: None, - end_timestamp: Some(1653782400), + date: None, + time: Some("14:00".to_owned()), + include_time: None, ..Default::default() }, - Some(date_cell.clone()), - &decode_cell_data(&date_cell, &type_option), + Some(old_cell_data), + "Nov 15, 2023 14:00", + ); + } + + #[test] + fn clear_date() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some(1700006400), + time: Some("08:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + time: None, + include_time: Some(true), + clear_flag: Some(true), + ..Default::default() + }, + Some(old_cell_data), + "", + ); + } + + #[test] + fn end_date_time_test() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + end_date: Some(1653782400), + include_time: Some(false), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 → May 29, 2022", + ); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("20:00".to_owned()), + end_date: Some(1653782400), + end_time: Some("08:00".to_owned()), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 20:00 → May 29, 2022 08:00", + ); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("20:00".to_owned()), + end_date: Some(1653782400), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 20:00 → May 29, 2022 00:00", + ); + } + + #[test] + fn turn_on_date_range() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some(1653609600), + time: Some("08:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + is_range: Some(true), + ..Default::default() + }, + Some(old_cell_data), + "May 27, 2022 08:00 → May 27, 2022 08:00", + ); + } + + #[test] + fn add_an_end_time() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some(1653609600), + time: Some("08:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + time: None, + end_date: Some(1700006400), + end_time: Some("16:00".to_owned()), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + Some(old_cell_data), + "May 27, 2022 08:00 → Nov 15, 2023 16:00", + ); + } + + #[test] + #[should_panic] + fn end_date_with_no_start_date() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + end_date: Some(1653782400), + include_time: Some(false), + is_range: Some(true), + ..Default::default() + }, + None, + "→ May 29, 2022", ); } fn assert_date( type_option: &DateTypeOption, + field: &Field, changeset: DateCellChangeset, old_cell_data: Option<Cell>, - expected: &DateCellData, + expected_str: &str, ) { let (cell, _) = type_option .apply_changeset(changeset, old_cell_data) .unwrap(); - let actual = decode_cell_data(&cell, type_option); - - assert_eq!(expected.timestamp, actual.timestamp); - assert_eq!(expected.end_timestamp, actual.end_timestamp); - assert_eq!(expected.include_time, actual.include_time); - assert_eq!(expected.is_range, actual.is_range); + assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,); } - fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption) -> DateCellData { - type_option.decode_cell(cell).unwrap() + fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, _field: &Field) -> String { + let decoded_data = type_option.decode_cell(cell).unwrap(); + type_option.stringify_cell_data(decoded_data) } fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index d9739fa792..6214dc3f24 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -1,24 +1,31 @@ use std::cmp::Ordering; +use std::str::FromStr; -use async_trait::async_trait; -use collab::util::AnyMapExt; -use collab_database::database::Database; -use collab_database::fields::date_type_option::{DateCellData, DateTypeOption}; -use collab_database::fields::{Field, TypeOptionData}; +use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; +use chrono_tz::Tz; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; -use collab_database::template::date_parse::cast_string_to_timestamp; -use flowy_error::FlowyResult; -use tracing::info; +use serde::{Deserialize, Serialize}; -use crate::entities::{DateCellDataPB, DateFilterPB, FieldType}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; + +use crate::entities::{DateCellDataPB, DateFilterPB}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; -use crate::services::field::date_type_option::date_filter::DateCellChangeset; use crate::services::field::{ - default_order, CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, CELL_DATA, + default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption, + TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, + TypeOptionTransform, }; use crate::services::sort::SortCondition; +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct DateTypeOption { + pub date_format: DateFormat, + pub time_format: TimeFormat, + pub timezone_id: String, +} + impl TypeOption for DateTypeOption { type CellData = DateCellData; type CellChangeset = DateCellChangeset; @@ -26,7 +33,36 @@ impl TypeOption for DateTypeOption { type CellFilter = DateFilterPB; } -impl CellDataProtobufEncoder for DateTypeOption { +impl From<TypeOptionData> for DateTypeOption { + fn from(data: TypeOptionData) -> Self { + let date_format = data + .get_i64_value("date_format") + .map(DateFormat::from) + .unwrap_or_default(); + let time_format = data + .get_i64_value("time_format") + .map(TimeFormat::from) + .unwrap_or_default(); + let timezone_id = data.get_str_value("timezone_id").unwrap_or_default(); + Self { + date_format, + time_format, + timezone_id, + } + } +} + +impl From<DateTypeOption> for TypeOptionData { + fn from(data: DateTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("date_format", data.date_format.value()) + .insert_i64_value("time_format", data.time_format.value()) + .insert_str_value("timezone_id", data.timezone_id) + .build() + } +} + +impl TypeOptionCellDataSerde for DateTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, @@ -35,72 +71,138 @@ impl CellDataProtobufEncoder for DateTypeOption { let is_range = cell_data.is_range; let timestamp = cell_data.timestamp; - let end_timestamp = if is_range { - cell_data.end_timestamp.or(timestamp) - } else { - None - }; + let (date, time) = self.formatted_date_time_from_timestamp(×tamp); + + let end_timestamp = cell_data.end_timestamp; + let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp); let reminder_id = cell_data.reminder_id; DateCellDataPB { - timestamp, - end_timestamp, + date, + time, + timestamp: timestamp.unwrap_or_default(), + end_date, + end_time, + end_timestamp: end_timestamp.unwrap_or_default(), include_time, is_range, reminder_id, } } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(DateCellData::from(cell)) + } } -#[async_trait] -impl TypeOptionTransform for DateTypeOption { - async fn transform_type_option( - &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - _old_type_option_data: TypeOptionData, - _new_type_option_field_type: FieldType, - database: &mut Database, - ) { - match old_type_option_field_type { - FieldType::RichText => { - let rows = database - .get_cells_for_field(view_id, field_id) - .await - .into_iter() - .filter_map(|row| row.cell.map(|cell| (row.row_id, cell))) - .collect::<Vec<_>>(); +impl DateTypeOption { + pub fn new() -> Self { + Self::default() + } - info!( - "Transforming RichText to DateTypeOption, updating {} row's cell content", - rows.len() - ); - for (row_id, cell_data) in rows { - if let Some(cell_data) = cell_data - .get_as::<String>(CELL_DATA) - .and_then(|s| cast_string_to_timestamp(&s)) - .map(DateCellData::from_timestamp) - { - database - .update_row(row_id, |row| { - row.update_cells(|cell| { - cell.insert(field_id, Cell::from(&cell_data)); - }); - }) - .await; - } + pub fn test() -> Self { + Self { + timezone_id: "Etc/UTC".to_owned(), + ..Self::default() + } + } + + fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) { + if let Some(timestamp) = timestamp { + let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap(); + let offset = self.get_timezone_offset(naive); + let date_time = DateTime::<Local>::from_naive_utc_and_offset(naive, offset); + + let fmt = self.date_format.format_str(); + let date = format!("{}", date_time.format(fmt)); + let fmt = self.time_format.format_str(); + let time = format!("{}", date_time.format(fmt)); + (date, time) + } else { + ("".to_owned(), "".to_owned()) + } + } + + fn naive_time_from_time_string( + &self, + include_time: bool, + time_str: Option<String>, + ) -> FlowyResult<Option<NaiveTime>> { + match (include_time, time_str) { + (true, Some(time_str)) => { + let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str()); + match result { + Ok(time) => Ok(Some(time)), + Err(_e) => { + let msg = format!("Parse {} failed", time_str); + Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg)) + }, } }, - _ => { - // do nothing - }, + _ => Ok(None), + } + } + + /// combine the changeset_timestamp and parsed_time if provided. if + /// changeset_timestamp is None, fallback to previous_timestamp + fn timestamp_from_parsed_time_previous_and_new_timestamp( + &self, + parsed_time: Option<NaiveTime>, + previous_timestamp: Option<i64>, + changeset_timestamp: Option<i64>, + ) -> Option<i64> { + if let Some(time) = parsed_time { + // a valid time is provided, so we replace the time component of old timestamp + // (or new timestamp if provided) with it. + let utc_date = changeset_timestamp + .or(previous_timestamp) + .map(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap()) + .unwrap(); + let offset = self.get_timezone_offset(utc_date); + + let local_date = changeset_timestamp.or(previous_timestamp).map(|timestamp| { + offset + .from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap()) + .date_naive() + }); + + match local_date { + Some(date) => { + let local_datetime = offset + .from_local_datetime(&NaiveDateTime::new(date, time)) + .unwrap(); + + Some(local_datetime.timestamp()) + }, + None => None, + } + } else { + changeset_timestamp.or(previous_timestamp) + } + } + + /// returns offset of Tz timezone if provided or of the local timezone otherwise + fn get_timezone_offset(&self, date_time: NaiveDateTime) -> FixedOffset { + let current_timezone_offset = Local::now().offset().fix(); + if self.timezone_id.is_empty() { + current_timezone_offset + } else { + match Tz::from_str(&self.timezone_id) { + Ok(timezone) => timezone.offset_from_utc_datetime(&date_time).fix(), + Err(_) => current_timezone_offset, + } } } } +impl TypeOptionTransform for DateTypeOption {} + impl CellDataDecoder for DateTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { let include_time = cell_data.include_time; let timestamp = cell_data.timestamp; @@ -129,15 +231,8 @@ impl CellDataDecoder for DateTypeOption { } } - fn decode_cell_with_transform( - &self, - cell: &Cell, - _from_field_type: FieldType, - _field: &Field, - ) -> Option<<Self as TypeOption>::CellData> { - let s = cell.get_as::<String>(CELL_DATA)?; - let timestamp = cast_string_to_timestamp(&s)?; - Some(DateCellData::from_timestamp(timestamp)) + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None } } @@ -147,46 +242,71 @@ impl CellDataChangeset for DateTypeOption { changeset: <Self as TypeOption>::CellChangeset, cell: Option<Cell>, ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { - if let Some(true) = changeset.clear_flag { - let cell_data = DateCellData::default(); - return Ok((Cell::from(&cell_data), cell_data)); - } - // old date cell data - let cell_data = match cell { - Some(cell) => DateCellData::from(&cell), - None => DateCellData::default(), - }; + let (previous_timestamp, previous_end_timestamp, include_time, is_range, reminder_id) = + match cell { + Some(cell) => { + let cell_data = DateCellData::from(&cell); + ( + cell_data.timestamp, + cell_data.end_timestamp, + cell_data.include_time, + cell_data.is_range, + cell_data.reminder_id, + ) + }, + None => (None, None, false, false, String::new()), + }; - let is_range = changeset.is_range.unwrap_or(cell_data.is_range); + if changeset.clear_flag == Some(true) { + let cell_data = DateCellData { + timestamp: None, + end_timestamp: None, + include_time, + is_range, + reminder_id: String::new(), + }; - let has_timestamp = changeset.timestamp.is_some(); - let has_end_timestamp = changeset.end_timestamp.is_some(); - let unexpected_end_changeset = !is_range && has_end_timestamp; - let missing_timestamp = is_range && has_timestamp != has_end_timestamp; - - if unexpected_end_changeset || missing_timestamp { return Ok((Cell::from(&cell_data), cell_data)); } - let DateCellData { - timestamp, - end_timestamp, - include_time, - is_range: _, - reminder_id, - } = cell_data; - - // update include_time and reminder_id if necessary + // update include_time and is_range if necessary let include_time = changeset.include_time.unwrap_or(include_time); + let is_range = changeset.is_range.unwrap_or(is_range); let reminder_id = changeset.reminder_id.unwrap_or(reminder_id); - let timestamp = changeset.timestamp.or(timestamp); - let end_timestamp = if is_range && timestamp.is_some() { - changeset.end_timestamp.or(end_timestamp).or(timestamp) - } else { - None - }; + // Calculate the timestamp in the time zone specified in type option. If + // a new timestamp is included in the changeset without an accompanying + // time string, the old timestamp will simply be overwritten. Meaning, in + // order to change the day without changing the time, the old time string + // should be passed in as well. + + // parse the time string, which is in the local timezone + let parsed_start_time = self.naive_time_from_time_string(include_time, changeset.time)?; + + let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp( + parsed_start_time, + previous_timestamp, + changeset.date, + ); + + let end_timestamp = + if is_range && changeset.end_date.is_none() && previous_end_timestamp.is_none() { + // just toggled is_range so no passed in or existing end time data + timestamp + } else if is_range { + // parse the changeset's end time data or fallback to previous version + let parsed_end_time = self.naive_time_from_time_string(include_time, changeset.end_time)?; + + self.timestamp_from_parsed_time_previous_and_new_timestamp( + parsed_end_time, + previous_end_timestamp, + changeset.end_date, + ) + } else { + // clear the end time data + None + }; let cell_data = DateCellData { timestamp, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs new file mode 100644 index 0000000000..c2b0259aff --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -0,0 +1,288 @@ +#![allow(clippy::upper_case_acronyms)] + +use bytes::Bytes; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::de::Visitor; +use serde::{Deserialize, Serialize}; +use std::fmt; +use strum_macros::EnumIter; + +use flowy_error::{internal_error, FlowyResult}; + +use crate::entities::{DateCellDataPB, FieldType}; +use crate::services::cell::CellProtobufBlobParser; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; + +#[derive(Clone, Debug, Default)] +pub struct DateCellChangeset { + pub date: Option<i64>, + pub time: Option<String>, + pub end_date: Option<i64>, + pub end_time: Option<String>, + pub include_time: Option<bool>, + pub is_range: Option<bool>, + pub clear_flag: Option<bool>, + pub reminder_id: Option<String>, +} + +#[derive(Default, Clone, Debug, Serialize)] +pub struct DateCellData { + pub timestamp: Option<i64>, + pub end_timestamp: Option<i64>, + #[serde(default)] + pub include_time: bool, + #[serde(default)] + pub is_range: bool, + pub reminder_id: String, +} + +impl DateCellData { + pub fn new(timestamp: i64, include_time: bool, is_range: bool, reminder_id: String) -> Self { + Self { + timestamp: Some(timestamp), + end_timestamp: None, + include_time, + is_range, + reminder_id, + } + } +} + +impl TypeOptionCellData for DateCellData { + fn is_cell_empty(&self) -> bool { + self.timestamp.is_none() + } +} + +impl From<&Cell> for DateCellData { + fn from(cell: &Cell) -> Self { + let timestamp = cell + .get_str_value(CELL_DATA) + .and_then(|data| data.parse::<i64>().ok()); + let end_timestamp = cell + .get_str_value("end_timestamp") + .and_then(|data| data.parse::<i64>().ok()); + let include_time = cell.get_bool_value("include_time").unwrap_or_default(); + let is_range = cell.get_bool_value("is_range").unwrap_or_default(); + let reminder_id = cell.get_str_value("reminder_id").unwrap_or_default(); + + Self { + timestamp, + end_timestamp, + include_time, + is_range, + reminder_id, + } + } +} + +impl From<&DateCellDataPB> for DateCellData { + fn from(data: &DateCellDataPB) -> Self { + Self { + timestamp: Some(data.timestamp), + end_timestamp: Some(data.end_timestamp), + include_time: data.include_time, + is_range: data.is_range, + reminder_id: data.reminder_id.to_owned(), + } + } +} + +impl From<&DateCellData> for Cell { + fn from(cell_data: &DateCellData) -> Self { + let timestamp_string = match cell_data.timestamp { + Some(timestamp) => timestamp.to_string(), + None => "".to_owned(), + }; + let end_timestamp_string = match cell_data.end_timestamp { + Some(timestamp) => timestamp.to_string(), + None => "".to_owned(), + }; + // Most of the case, don't use these keys in other places. Otherwise, we should define + // constants for them. + new_cell_builder(FieldType::DateTime) + .insert_str_value(CELL_DATA, timestamp_string) + .insert_str_value("end_timestamp", end_timestamp_string) + .insert_bool_value("include_time", cell_data.include_time) + .insert_bool_value("is_range", cell_data.is_range) + .insert_str_value("reminder_id", cell_data.reminder_id.to_owned()) + .build() + } +} + +impl<'de> serde::Deserialize<'de> for DateCellData { + fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + struct DateCellVisitor(); + + impl<'de> Visitor<'de> for DateCellVisitor { + type Value = DateCellData; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str( + "DateCellData with type: str containing either an integer timestamp or the JSON representation", + ) + } + + fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + Ok(DateCellData { + timestamp: Some(value), + end_timestamp: None, + include_time: false, + is_range: false, + reminder_id: String::new(), + }) + } + + fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> + where + E: serde::de::Error, + { + self.visit_i64(value as i64) + } + + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> + where + M: serde::de::MapAccess<'de>, + { + let mut timestamp: Option<i64> = None; + let mut end_timestamp: Option<i64> = None; + let mut include_time: Option<bool> = None; + let mut is_range: Option<bool> = None; + let mut reminder_id: Option<String> = None; + + while let Some(key) = map.next_key()? { + match key { + "timestamp" => { + timestamp = map.next_value()?; + }, + "end_timestamp" => { + end_timestamp = map.next_value()?; + }, + "include_time" => { + include_time = map.next_value()?; + }, + "is_range" => { + is_range = map.next_value()?; + }, + "reminder_id" => { + reminder_id = map.next_value()?; + }, + _ => {}, + } + } + + let include_time = include_time.unwrap_or_default(); + let is_range = is_range.unwrap_or_default(); + let reminder_id = reminder_id.unwrap_or_default(); + + Ok(DateCellData { + timestamp, + end_timestamp, + include_time, + is_range, + reminder_id, + }) + } + } + + deserializer.deserialize_any(DateCellVisitor()) + } +} + +impl ToString for DateCellData { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, Default)] +pub enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + #[default] + Friendly = 3, + DayMonthYear = 4, +} + +impl std::convert::From<i64> for DateFormat { + fn from(value: i64) -> Self { + match value { + 0 => DateFormat::Local, + 1 => DateFormat::US, + 2 => DateFormat::ISO, + 3 => DateFormat::Friendly, + 4 => DateFormat::DayMonthYear, + _ => { + tracing::error!("Unsupported date format, fallback to friendly"); + DateFormat::Friendly + }, + } + } +} + +impl DateFormat { + pub fn value(&self) -> i64 { + *self as i64 + } + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + DateFormat::Local => "%m/%d/%Y", + DateFormat::US => "%Y/%m/%d", + DateFormat::ISO => "%Y-%m-%d", + DateFormat::Friendly => "%b %d, %Y", + DateFormat::DayMonthYear => "%d/%m/%Y", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, Default)] +pub enum TimeFormat { + TwelveHour = 0, + #[default] + TwentyFourHour = 1, +} + +impl std::convert::From<i64> for TimeFormat { + fn from(value: i64) -> Self { + match value { + 0 => TimeFormat::TwelveHour, + 1 => TimeFormat::TwentyFourHour, + _ => { + tracing::error!("Unsupported time format, fallback to TwentyFourHour"); + TimeFormat::TwentyFourHour + }, + } + } +} + +impl TimeFormat { + pub fn value(&self) -> i64 { + *self as i64 + } + + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + TimeFormat::TwelveHour => "%I:%M %p", + TimeFormat::TwentyFourHour => "%R", + } + } +} + +pub struct DateCellDataParser(); +impl CellProtobufBlobParser for DateCellDataParser { + type Object = DateCellDataPB; + + fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> { + DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs index 3d50a36a82..ff0c344957 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/mod.rs @@ -1,4 +1,8 @@ #![allow(clippy::module_inception)] -pub mod date_filter; +mod date_filter; mod date_tests; -pub mod date_type_option; +mod date_type_option; +mod date_type_option_entities; + +pub use date_type_option::*; +pub use date_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_file.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_file.rs deleted file mode 100644 index b4a24febb6..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_file.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use collab_database::entity::FileUploadType; -use serde::{Deserialize, Serialize}; - -use crate::entities::FileUploadTypePB; - -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -#[repr(u8)] -pub enum MediaUploadType { - #[default] - LocalMedia = 0, - NetworkMedia = 1, - CloudMedia = 2, -} - -impl From<MediaUploadType> for FileUploadTypePB { - fn from(media_upload_type: MediaUploadType) -> Self { - match media_upload_type { - MediaUploadType::LocalMedia => FileUploadTypePB::LocalFile, - MediaUploadType::NetworkMedia => FileUploadTypePB::NetworkFile, - MediaUploadType::CloudMedia => FileUploadTypePB::CloudFile, - } - } -} - -impl From<FileUploadTypePB> for MediaUploadType { - fn from(file_upload_type: FileUploadTypePB) -> Self { - match file_upload_type { - FileUploadTypePB::LocalFile => MediaUploadType::LocalMedia, - FileUploadTypePB::NetworkFile => MediaUploadType::NetworkMedia, - FileUploadTypePB::CloudFile => MediaUploadType::CloudMedia, - } - } -} - -impl From<MediaUploadType> for FileUploadType { - fn from(media_upload_type: MediaUploadType) -> Self { - match media_upload_type { - MediaUploadType::LocalMedia => FileUploadType::LocalFile, - MediaUploadType::NetworkMedia => FileUploadType::NetworkFile, - MediaUploadType::CloudMedia => FileUploadType::CloudFile, - } - } -} - -impl From<FileUploadType> for MediaUploadType { - fn from(file_upload_type: FileUploadType) -> Self { - match file_upload_type { - FileUploadType::LocalFile => MediaUploadType::LocalMedia, - FileUploadType::NetworkFile => MediaUploadType::NetworkMedia, - FileUploadType::CloudFile => MediaUploadType::CloudMedia, - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct MediaFile { - pub id: String, - pub name: String, - pub url: String, - pub upload_type: MediaUploadType, - pub file_type: MediaFileType, -} - -impl MediaFile { - pub fn rename(&self, new_name: String) -> Self { - Self { - id: self.id.clone(), - name: new_name, - url: self.url.clone(), - upload_type: self.upload_type.clone(), - file_type: self.file_type.clone(), - } - } -} - -impl Display for MediaFile { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "MediaFile(id: {}, name: {}, url: {}, upload_type: {:?}, file_type: {:?})", - self.id, self.name, self.url, self.upload_type, self.file_type - ) - } -} - -#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Default, Clone)] -#[repr(u8)] -pub enum MediaFileType { - #[default] - Other = 0, - Image = 1, - Link = 2, - Document = 3, - Archive = 4, - Video = 5, - Audio = 6, - Text = 7, -} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_filter.rs deleted file mode 100644 index f3a02b137c..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_filter.rs +++ /dev/null @@ -1,22 +0,0 @@ -use collab_database::{fields::Field, rows::Cell}; - -use crate::{ - entities::{MediaFilterConditionPB, MediaFilterPB}, - services::filter::PreFillCellsWithFilter, -}; - -impl MediaFilterPB { - pub fn is_visible<T: AsRef<str>>(&self, cell_data: T) -> bool { - let cell_data = cell_data.as_ref().to_lowercase(); - match self.condition { - MediaFilterConditionPB::MediaIsEmpty => cell_data.is_empty(), - MediaFilterConditionPB::MediaIsNotEmpty => !cell_data.is_empty(), - } - } -} - -impl PreFillCellsWithFilter for MediaFilterPB { - fn get_compliant_cell(&self, _field: &Field) -> Option<Cell> { - None - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_type_option.rs deleted file mode 100644 index 6fca683358..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/media_type_option.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::{ - entities::{FieldType, MediaCellChangeset, MediaCellDataPB, MediaFilterPB}, - services::{ - cell::{CellDataChangeset, CellDataDecoder}, - field::{ - default_order, CellDataProtobufEncoder, TypeOption, TypeOptionCellData, - TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, - }, - sort::SortCondition, - }, -}; -use collab_database::fields::media_type_option::{MediaCellData, MediaTypeOption}; -use collab_database::template::util::ToCellString; -use collab_database::{fields::Field, rows::Cell}; -use flowy_error::FlowyResult; -use std::cmp::Ordering; - -impl TypeOption for MediaTypeOption { - type CellData = MediaCellData; - type CellChangeset = MediaCellChangeset; - type CellProtobufType = MediaCellDataPB; - type CellFilter = MediaFilterPB; -} - -impl TypeOptionTransform for MediaTypeOption {} - -impl CellDataProtobufEncoder for MediaTypeOption { - fn protobuf_encode( - &self, - cell_data: <Self as TypeOption>::CellData, - ) -> <Self as TypeOption>::CellProtobufType { - cell_data.into() - } -} - -impl CellDataDecoder for MediaTypeOption { - fn decode_cell_with_transform( - &self, - _cell: &Cell, - from_field_type: FieldType, - _field: &Field, - ) -> Option<<Self as TypeOption>::CellData> { - match from_field_type { - FieldType::RichText - | FieldType::Number - | FieldType::DateTime - | FieldType::SingleSelect - | FieldType::MultiSelect - | FieldType::Checkbox - | FieldType::URL - | FieldType::Summary - | FieldType::Translate - | FieldType::Time - | FieldType::Checklist - | FieldType::LastEditedTime - | FieldType::CreatedTime - | FieldType::Relation - | FieldType::Media => None, - } - } - - fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { - cell_data.to_cell_string() - } -} - -impl CellDataChangeset for MediaTypeOption { - fn apply_changeset( - &self, - changeset: <Self as TypeOption>::CellChangeset, - cell: Option<Cell>, - ) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> { - if cell.is_none() { - let cell_data = MediaCellData { - files: changeset.inserted_files, - }; - return Ok((cell_data.clone().into(), cell_data)); - } - - let cell_data: MediaCellData = MediaCellData::from(&cell.unwrap()); - let mut files = cell_data.files.clone(); - for removed_id in changeset.removed_ids.iter() { - if let Some(index) = files.iter().position(|file| file.id == removed_id.clone()) { - files.remove(index); - } - } - - for inserted in changeset.inserted_files.iter() { - if !files.iter().any(|file| file.id == inserted.id) { - files.push(inserted.clone()) - } - } - - let cell_data = MediaCellData { files }; - - Ok((Cell::from(cell_data.clone()), cell_data)) - } -} - -impl TypeOptionCellDataFilter for MediaTypeOption { - fn apply_filter( - &self, - _filter: &<Self as TypeOption>::CellFilter, - _cell_data: &<Self as TypeOption>::CellData, - ) -> bool { - true - } -} - -impl TypeOptionCellDataCompare for MediaTypeOption { - fn apply_cmp( - &self, - cell_data: &<Self as TypeOption>::CellData, - other_cell_data: &<Self as TypeOption>::CellData, - _sort_condition: SortCondition, - ) -> Ordering { - match (cell_data.files.is_empty(), other_cell_data.is_cell_empty()) { - (true, true) => Ordering::Equal, - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (false, false) => default_order(), - } - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/mod.rs deleted file mode 100644 index b9bd6e471a..0000000000 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/media_type_option/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#![allow(clippy::module_inception)] -// mod media_file; -mod media_filter; -mod media_type_option; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs index 0530eb746e..a6515c9db4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs @@ -1,7 +1,6 @@ pub mod checkbox_type_option; pub mod checklist_type_option; pub mod date_type_option; -pub mod media_type_option; pub mod number_type_option; pub mod relation_type_option; pub mod selection_type_option; @@ -18,13 +17,12 @@ mod util; pub use checkbox_type_option::*; pub use checklist_type_option::*; pub use date_type_option::*; - pub use number_type_option::*; pub use relation_type_option::*; pub use selection_type_option::*; pub use text_type_option::*; pub use time_type_option::*; - +pub use timestamp_type_option::*; pub use type_option::*; pub use type_option_cell::*; pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs new file mode 100644 index 0000000000..5a4727984b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/format.rs @@ -0,0 +1,498 @@ +#![allow(clippy::upper_case_acronyms)] + +use lazy_static::lazy_static; +use rusty_money::define_currency_set; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +lazy_static! { + pub static ref CURRENCY_SYMBOL: Vec<String> = NumberFormat::iter() + .map(|format| format.symbol()) + .collect::<Vec<String>>(); +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, Serialize, Deserialize, Default)] +pub enum NumberFormat { + #[default] + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +impl NumberFormat { + pub fn value(&self) -> i64 { + *self as i64 + } +} + +impl From<i64> for NumberFormat { + fn from(value: i64) -> Self { + match value { + 0 => NumberFormat::Num, + 1 => NumberFormat::USD, + 2 => NumberFormat::CanadianDollar, + 4 => NumberFormat::EUR, + 5 => NumberFormat::Pound, + 6 => NumberFormat::Yen, + 7 => NumberFormat::Ruble, + 8 => NumberFormat::Rupee, + 9 => NumberFormat::Won, + 10 => NumberFormat::Yuan, + 11 => NumberFormat::Real, + 12 => NumberFormat::Lira, + 13 => NumberFormat::Rupiah, + 14 => NumberFormat::Franc, + 15 => NumberFormat::HongKongDollar, + 16 => NumberFormat::NewZealandDollar, + 17 => NumberFormat::Krona, + 18 => NumberFormat::NorwegianKrone, + 19 => NumberFormat::MexicanPeso, + 20 => NumberFormat::Rand, + 21 => NumberFormat::NewTaiwanDollar, + 22 => NumberFormat::DanishKrone, + 23 => NumberFormat::Baht, + 24 => NumberFormat::Forint, + 25 => NumberFormat::Koruna, + 26 => NumberFormat::Shekel, + 27 => NumberFormat::ChileanPeso, + 28 => NumberFormat::PhilippinePeso, + 29 => NumberFormat::Dirham, + 30 => NumberFormat::ColombianPeso, + 31 => NumberFormat::Riyal, + 32 => NumberFormat::Ringgit, + 33 => NumberFormat::Leu, + 34 => NumberFormat::ArgentinePeso, + 35 => NumberFormat::UruguayanPeso, + 36 => NumberFormat::Percent, + _ => NumberFormat::Num, + } + } +} + +define_currency_set!( + number_currency { + NUMBER : { + code: "", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "number", + symbol: "RUB", + symbol_first: false, + }, + PERCENT : { + code: "", + exponent: 2, + locale: EnIn, + minor_units: 1, + name: "percent", + symbol: "%", + symbol_first: false, + }, + USD : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "United States Dollar", + symbol: "$", + symbol_first: true, + }, + CANADIAN_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Canadian Dollar", + symbol: "CA$", + symbol_first: true, + }, + NEW_TAIWAN_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "NewTaiwan Dollar", + symbol: "NT$", + symbol_first: true, + }, + HONG_KONG_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "HongKong Dollar", + symbol: "HZ$", + symbol_first: true, + }, + NEW_ZEALAND_DOLLAR : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "NewZealand Dollar", + symbol: "NZ$", + symbol_first: true, + }, + EUR : { + code: "EUR", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Euro", + symbol: "€", + symbol_first: true, + }, + GIP : { + code: "GIP", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Gibraltar Pound", + symbol: "£", + symbol_first: true, + }, + CNY : { + code: "CNY", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Chinese Renminbi Yuan", + symbol: "¥", + symbol_first: true, + }, + YUAN : { + code: "CNY", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Chinese Renminbi Yuan", + symbol: "CN¥", + symbol_first: true, + }, + RUB : { + code: "RUB", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Russian Ruble", + symbol: "RUB", + symbol_first: false, + }, + INR : { + code: "INR", + exponent: 2, + locale: EnIn, + minor_units: 50, + name: "Indian Rupee", + symbol: "₹", + symbol_first: true, + }, + KRW : { + code: "KRW", + exponent: 0, + locale: EnUs, + minor_units: 1, + name: "South Korean Won", + symbol: "₩", + symbol_first: true, + }, + BRL : { + code: "BRL", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Brazilian real", + symbol: "R$", + symbol_first: true, + }, + TRY : { + code: "TRY", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Turkish Lira", + // symbol: "₺", + symbol: "TRY", + symbol_first: true, + }, + IDR : { + code: "IDR", + exponent: 2, + locale: EnUs, + minor_units: 5000, + name: "Indonesian Rupiah", + // symbol: "Rp", + symbol: "IDR", + symbol_first: true, + }, + CHF : { + code: "CHF", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Swiss Franc", + // symbol: "Fr", + symbol: "CHF", + symbol_first: true, + }, + SEK : { + code: "SEK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Swedish Krona", + // symbol: "kr", + symbol: "SEK", + symbol_first: false, + }, + NOK : { + code: "NOK", + exponent: 2, + locale: EnUs, + minor_units: 100, + name: "Norwegian Krone", + // symbol: "kr", + symbol: "NOK", + symbol_first: false, + }, + MEXICAN_PESO : { + code: "USD", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Mexican Peso", + symbol: "MX$", + symbol_first: true, + }, + ZAR : { + code: "ZAR", + exponent: 2, + locale: EnUs, + minor_units: 10, + name: "South African Rand", + // symbol: "R", + symbol: "ZAR", + symbol_first: true, + }, + DKK : { + code: "DKK", + exponent: 2, + locale: EnEu, + minor_units: 50, + name: "Danish Krone", + // symbol: "kr.", + symbol: "DKK", + symbol_first: false, + }, + THB : { + code: "THB", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Thai Baht", + // symbol: "฿", + symbol: "THB", + symbol_first: true, + }, + HUF : { + code: "HUF", + exponent: 0, + locale: EnBy, + minor_units: 5, + name: "Hungarian Forint", + // symbol: "Ft", + symbol: "HUF", + symbol_first: false, + }, + KORUNA : { + code: "CZK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Czech Koruna", + // symbol: "Kč", + symbol: "CZK", + symbol_first: false, + }, + SHEKEL : { + code: "CZK", + exponent: 2, + locale: EnBy, + minor_units: 100, + name: "Czech Koruna", + symbol: "Kč", + symbol_first: false, + }, + CLP : { + code: "CLP", + exponent: 0, + locale: EnEu, + minor_units: 1, + name: "Chilean Peso", + // symbol: "$", + symbol: "CLP", + symbol_first: true, + }, + PHP : { + code: "PHP", + exponent: 2, + locale: EnUs, + minor_units: 1, + name: "Philippine Peso", + symbol: "₱", + symbol_first: true, + }, + AED : { + code: "AED", + exponent: 2, + locale: EnUs, + minor_units: 25, + name: "United Arab Emirates Dirham", + // symbol: "د.إ", + symbol: "AED", + symbol_first: false, + }, + COP : { + code: "COP", + exponent: 2, + locale: EnEu, + minor_units: 20, + name: "Colombian Peso", + // symbol: "$", + symbol: "COP", + symbol_first: true, + }, + SAR : { + code: "SAR", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Saudi Riyal", + // symbol: "ر.س", + symbol: "SAR", + symbol_first: true, + }, + MYR : { + code: "MYR", + exponent: 2, + locale: EnUs, + minor_units: 5, + name: "Malaysian Ringgit", + // symbol: "RM", + symbol: "MYR", + symbol_first: true, + }, + RON : { + code: "RON", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Romanian Leu", + // symbol: "ر.ق", + symbol: "RON", + symbol_first: false, + }, + ARS : { + code: "ARS", + exponent: 2, + locale: EnEu, + minor_units: 1, + name: "Argentine Peso", + // symbol: "$", + symbol: "ARS", + symbol_first: true, + }, + UYU : { + code: "UYU", + exponent: 2, + locale: EnEu, + minor_units: 100, + name: "Uruguayan Peso", + // symbol: "$U", + symbol: "UYU", + symbol_first: true, + } + } +); + +impl NumberFormat { + pub fn currency(&self) -> &'static number_currency::Currency { + match self { + NumberFormat::Num => number_currency::NUMBER, + NumberFormat::USD => number_currency::USD, + NumberFormat::CanadianDollar => number_currency::CANADIAN_DOLLAR, + NumberFormat::EUR => number_currency::EUR, + NumberFormat::Pound => number_currency::GIP, + NumberFormat::Yen => number_currency::CNY, + NumberFormat::Ruble => number_currency::RUB, + NumberFormat::Rupee => number_currency::INR, + NumberFormat::Won => number_currency::KRW, + NumberFormat::Yuan => number_currency::YUAN, + NumberFormat::Real => number_currency::BRL, + NumberFormat::Lira => number_currency::TRY, + NumberFormat::Rupiah => number_currency::IDR, + NumberFormat::Franc => number_currency::CHF, + NumberFormat::HongKongDollar => number_currency::HONG_KONG_DOLLAR, + NumberFormat::NewZealandDollar => number_currency::NEW_ZEALAND_DOLLAR, + NumberFormat::Krona => number_currency::SEK, + NumberFormat::NorwegianKrone => number_currency::NOK, + NumberFormat::MexicanPeso => number_currency::MEXICAN_PESO, + NumberFormat::Rand => number_currency::ZAR, + NumberFormat::NewTaiwanDollar => number_currency::NEW_TAIWAN_DOLLAR, + NumberFormat::DanishKrone => number_currency::DKK, + NumberFormat::Baht => number_currency::THB, + NumberFormat::Forint => number_currency::HUF, + NumberFormat::Koruna => number_currency::KORUNA, + NumberFormat::Shekel => number_currency::SHEKEL, + NumberFormat::ChileanPeso => number_currency::CLP, + NumberFormat::PhilippinePeso => number_currency::PHP, + NumberFormat::Dirham => number_currency::AED, + NumberFormat::ColombianPeso => number_currency::COP, + NumberFormat::Riyal => number_currency::SAR, + NumberFormat::Ringgit => number_currency::MYR, + NumberFormat::Leu => number_currency::RON, + NumberFormat::ArgentinePeso => number_currency::ARS, + NumberFormat::UruguayanPeso => number_currency::UYU, + NumberFormat::Percent => number_currency::PERCENT, + } + } + + pub fn symbol(&self) -> String { + self.currency().symbol.to_string() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs index 714b6baca8..8136fb57c5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/mod.rs @@ -1,8 +1,10 @@ #![allow(clippy::module_inception)] +mod format; mod number_filter; +mod number_tests; mod number_type_option; mod number_type_option_entities; -// pub use format::*; +pub use format::*; pub use number_type_option::*; pub use number_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs index 01b6f6dd90..ba95dd8843 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs @@ -1,12 +1,12 @@ use std::str::FromStr; -use collab_database::fields::number_type_option::NumberCellFormat; use collab_database::fields::Field; use collab_database::rows::Cell; use rust_decimal::Decimal; use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; use crate::services::cell::insert_text_cell; +use crate::services::field::NumberCellFormat; use crate::services::filter::PreFillCellsWithFilter; impl NumberFilterPB { @@ -35,7 +35,7 @@ impl NumberFilterPB { } impl PreFillCellsWithFilter for NumberFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { let expected_decimal = || Decimal::from_str(&self.content).ok(); let text = match self.condition { @@ -61,8 +61,10 @@ impl PreFillCellsWithFilter for NumberFilterPB { _ => None, }; + let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty); + // use `insert_text_cell` because self.content might not be a parsable i64. - text.map(|s| insert_text_cell(s, field)) + (text.map(|s| insert_text_cell(s, field)), open_after_create) } } enum NumberFilterStrategy { @@ -106,7 +108,7 @@ impl NumberFilterStrategy { #[cfg(test)] mod tests { use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; - use collab_database::fields::number_type_option::{NumberCellFormat, NumberFormat}; + use crate::services::field::{NumberCellFormat, NumberFormat}; #[test] fn number_filter_equal_test() { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs new file mode 100644 index 0000000000..a998cdf87e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_tests.rs @@ -0,0 +1,80 @@ +#[cfg(test)] +mod tests { + + use crate::services::cell::CellDataDecoder; + use crate::services::field::NumberCellData; + use crate::services::field::{NumberFormat, NumberTypeOption}; + + /// Testing when the input is not a number. + #[test] + fn number_type_option_input_test() { + let type_option = NumberTypeOption::default(); + + // Input is empty String + assert_number(&type_option, "", ""); + assert_number(&type_option, "abc", ""); + assert_number(&type_option, "-123", "-123"); + assert_number(&type_option, "abc-123", "-123"); + assert_number(&type_option, "+123", "123"); + assert_number(&type_option, "0.2", "0.2"); + assert_number(&type_option, "-0.2", "-0.2"); + assert_number(&type_option, "-$0.2", "0.2"); + assert_number(&type_option, ".2", "0.2"); + } + + #[test] + fn dollar_type_option_test() { + let mut type_option = NumberTypeOption::new(); + type_option.format = NumberFormat::USD; + + assert_number(&type_option, "", ""); + assert_number(&type_option, "abc", ""); + assert_number(&type_option, "-123", "-$123"); + assert_number(&type_option, "+123", "$123"); + assert_number(&type_option, "0.2", "$0.2"); + assert_number(&type_option, "-0.2", "-$0.2"); + assert_number(&type_option, "-$0.2", "-$0.2"); + assert_number(&type_option, "-€0.2", "-$0.2"); + assert_number(&type_option, ".2", "$0.2"); + } + + #[test] + fn dollar_type_option_test2() { + let mut type_option = NumberTypeOption::new(); + type_option.format = NumberFormat::USD; + + assert_number(&type_option, "99999999999", "$99,999,999,999"); + assert_number(&type_option, "$99,999,999,999", "$99,999,999,999"); + } + #[test] + fn other_symbol_to_dollar_type_option_test() { + let mut type_option = NumberTypeOption::new(); + type_option.format = NumberFormat::USD; + + assert_number(&type_option, "€0.2", "$0.2"); + assert_number(&type_option, "-€0.2", "-$0.2"); + assert_number(&type_option, "-CN¥0.2", "-$0.2"); + assert_number(&type_option, "CN¥0.2", "$0.2"); + assert_number(&type_option, "0.2", "$0.2"); + } + + #[test] + fn euro_type_option_test() { + let mut type_option = NumberTypeOption::new(); + type_option.format = NumberFormat::EUR; + + assert_number(&type_option, "0.2", "€0,2"); + assert_number(&type_option, "1000", "€1.000"); + assert_number(&type_option, "1234.56", "€1.234,56"); + } + + fn assert_number(type_option: &NumberTypeOption, input_str: &str, expected_str: &str) { + assert_eq!( + type_option + .decode_cell(&NumberCellData(input_str.to_owned()).into(),) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs index 1b0cfa85ae..0fc7cd5920 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs @@ -1,29 +1,71 @@ -use async_trait::async_trait; - -use collab_database::database::Database; -use collab_database::fields::number_type_option::{ - NumberCellFormat, NumberFormat, NumberTypeOption, -}; -use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::Cell; -use fancy_regex::Regex; -use flowy_error::FlowyResult; -use lazy_static::lazy_static; - -use collab_database::template::number_parse::NumberCellData; use std::cmp::Ordering; +use std::default::Default; +use std::str::FromStr; -use tracing::info; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::{new_cell_builder, Cell}; +use fancy_regex::Regex; +use lazy_static::lazy_static; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +use flowy_error::FlowyResult; use crate::entities::{FieldType, NumberFilterPB}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::type_options::number_type_option::format::*; use crate::services::field::type_options::util::ProtobufStr; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, + NumberCellFormat, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, CELL_DATA, }; use crate::services::sort::SortCondition; +// Number +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NumberTypeOption { + pub format: NumberFormat, + pub scale: u32, + pub symbol: String, + pub name: String, +} + +#[derive(Clone, Debug, Default)] +pub struct NumberCellData(pub String); + +impl TypeOptionCellData for NumberCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From<&Cell> for NumberCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + } +} + +impl From<NumberCellData> for Cell { + fn from(data: NumberCellData) -> Self { + new_cell_builder(FieldType::Number) + .insert_str_value(CELL_DATA, data.0) + .build() + } +} + +impl std::convert::From<String> for NumberCellData { + fn from(s: String) -> Self { + Self(s) + } +} + +impl ToString for NumberCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + impl TypeOption for NumberTypeOption { type CellData = NumberCellData; type CellChangeset = NumberCellChangeset; @@ -31,86 +73,125 @@ impl TypeOption for NumberTypeOption { type CellFilter = NumberFilterPB; } -impl CellDataProtobufEncoder for NumberTypeOption { +impl From<TypeOptionData> for NumberTypeOption { + fn from(data: TypeOptionData) -> Self { + let format = data + .get_i64_value("format") + .map(NumberFormat::from) + .unwrap_or_default(); + let scale = data.get_i64_value("scale").unwrap_or_default() as u32; + let symbol = data.get_str_value("symbol").unwrap_or_default(); + let name = data.get_str_value("name").unwrap_or_default(); + Self { + format, + scale, + symbol, + name, + } + } +} + +impl From<NumberTypeOption> for TypeOptionData { + fn from(data: NumberTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("format", data.format.value()) + .insert_i64_value("scale", data.scale as i64) + .insert_str_value("name", data.name) + .insert_str_value("symbol", data.symbol) + .build() + } +} + +impl TypeOptionCellDataSerde for NumberTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { ProtobufStr::from(cell_data.0) } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(NumberCellData::from(cell)) + } } -#[async_trait] -impl TypeOptionTransform for NumberTypeOption { - async fn transform_type_option( - &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - _old_type_option_data: TypeOptionData, - _new_type_option_field_type: FieldType, - database: &mut Database, - ) { - match old_type_option_field_type { - FieldType::RichText => { - let rows = database - .get_cells_for_field(view_id, field_id) - .await - .into_iter() - .filter_map(|row| row.cell.map(|cell| (row.row_id, cell))) - .collect::<Vec<_>>(); +impl NumberTypeOption { + pub fn new() -> Self { + Self::default() + } - info!( - "Transforming RichText to NumberTypeOption, updating {} row's cell content", - rows.len() - ); - for (row_id, cell_data) in rows { - if let Ok(num_cell) = self - .decode_cell(&cell_data) - .and_then(|num_cell_data| self.format_cell_data(num_cell_data).map_err(Into::into)) - { - database - .update_row(row_id, |row| { - row.update_cells(|cell| { - cell.insert(field_id, NumberCellData::from(num_cell.to_string())); - }); - }) - .await; + fn format_cell_data(&self, num_cell_data: &NumberCellData) -> FlowyResult<NumberCellFormat> { + match self.format { + NumberFormat::Num => { + if SCIENTIFIC_NOTATION_REGEX + .is_match(&num_cell_data.0) + .unwrap() + { + match Decimal::from_scientific(&num_cell_data.0.to_lowercase()) { + Ok(value, ..) => Ok(NumberCellFormat::from_decimal(value)), + Err(_) => Ok(NumberCellFormat::new()), + } + } else { + // Test the input string is start with dot and only contains number. + // If it is, add a 0 before the dot. For example, ".123" -> "0.123" + let num_str = match START_WITH_DOT_NUM_REGEX.captures(&num_cell_data.0) { + Ok(Some(captures)) => match captures.get(0).map(|m| m.as_str().to_string()) { + Some(s) => { + format!("0{}", s) + }, + None => "".to_string(), + }, + // Extract the number from the string. + // For example, "123abc" -> "123". check out the number_type_option_input_test test for + // more examples. + _ => match EXTRACT_NUM_REGEX.captures(&num_cell_data.0) { + Ok(Some(captures)) => captures + .get(0) + .map(|m| m.as_str().to_string()) + .unwrap_or_default(), + _ => "".to_string(), + }, + }; + + match Decimal::from_str(&num_str) { + Ok(decimal, ..) => Ok(NumberCellFormat::from_decimal(decimal)), + Err(_) => Ok(NumberCellFormat::new()), } } }, _ => { - // do nothing + // If the format is not number, use the format string to format the number. + NumberCellFormat::from_format_str(&num_cell_data.0, &self.format) }, } } + + pub fn set_format(&mut self, format: NumberFormat) { + self.format = format; + self.symbol = format.symbol(); + } } +impl TypeOptionTransform for NumberTypeOption {} + impl CellDataDecoder for NumberTypeOption { fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { - let num_cell_data = Self::CellData::from(cell); + let num_cell_data = self.parse_cell(cell)?; Ok(NumberCellData::from( - self.format_cell_data(num_cell_data)?.to_string(), + self.format_cell_data(&num_cell_data)?.to_string(), )) } fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { - match self.format_cell_data(cell_data) { + match self.format_cell_data(&cell_data) { Ok(cell_data) => cell_data.to_string(), Err(_) => "".to_string(), } } - fn decode_cell_with_transform( - &self, - cell: &Cell, - _from_field_type: FieldType, - _field: &Field, - ) -> Option<<Self as TypeOption>::CellData> { - let num_cell = Self::CellData::from(cell); - Some(Self::CellData::from( - self.format_cell_data(num_cell).ok()?.to_string(), - )) + fn numeric_cell(&self, cell: &Cell) -> Option<f64> { + let num_cell_data = self.parse_cell(cell).ok()?; + num_cell_data.0.parse::<f64>().ok() } } @@ -126,12 +207,7 @@ impl CellDataChangeset for NumberTypeOption { let number_cell_data = NumberCellData(num_str); let formatter = self.format_cell_data(&number_cell_data)?; - tracing::trace!( - "NumberTypeOption: {:?}, {}, {}", - number_cell_data, - formatter.to_string(), - formatter.to_unformatted_string() - ); + tracing::trace!("number: {:?}", number_cell_data); match self.format { NumberFormat::Num => Ok(( NumberCellData(formatter.to_string()).into(), @@ -194,6 +270,19 @@ impl TypeOptionCellDataCompare for NumberTypeOption { } } +impl std::default::Default for NumberTypeOption { + fn default() -> Self { + let format = NumberFormat::default(); + let symbol = format.symbol(); + NumberTypeOption { + format, + scale: 0, + symbol, + name: "Number".to_string(), + } + } +} + lazy_static! { static ref SCIENTIFIC_NOTATION_REGEX: Regex = Regex::new(r"([+-]?\d*\.?\d+)e([+-]?\d+)").unwrap(); pub(crate) static ref EXTRACT_NUM_REGEX: Regex = Regex::new(r"-?\d+(\.\d+)?").unwrap(); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs index 6f9cb36c49..5085bc3db3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option_entities.rs @@ -1,14 +1,119 @@ use crate::services::cell::{CellBytesCustomParser, CellProtobufBlobParser}; +use crate::services::field::number_currency::Currency; +use crate::services::field::{NumberFormat, EXTRACT_NUM_REGEX, START_WITH_DOT_NUM_REGEX}; use bytes::Bytes; -use collab_database::fields::number_type_option::{NumberCellFormat, NumberFormat}; use flowy_error::FlowyResult; +use rust_decimal::Decimal; +use rusty_money::Money; +use std::str::FromStr; + +#[derive(Debug, Default)] +pub struct NumberCellFormat { + decimal: Option<Decimal>, + money: Option<String>, +} + +impl NumberCellFormat { + pub fn new() -> Self { + Self { + decimal: Default::default(), + money: None, + } + } + + /// The num_str might contain currency symbol, e.g. $1,000.00 + pub fn from_format_str(num_str: &str, format: &NumberFormat) -> FlowyResult<Self> { + if num_str.is_empty() { + return Ok(Self::default()); + } + // If the first char is not '-', then it is a sign. + let sign_positive = match num_str.find('-') { + None => true, + Some(offset) => offset != 0, + }; + + let num_str = auto_fill_zero_at_start_if_need(num_str); + let num_str = extract_number(&num_str); + match Decimal::from_str(&num_str) { + Ok(mut decimal) => { + decimal.set_sign_positive(sign_positive); + let money = Money::from_decimal(decimal, format.currency()); + Ok(Self::from_money(money)) + }, + Err(_) => match Money::from_str(&num_str, format.currency()) { + Ok(money) => Ok(Self::from_money(money)), + Err(_) => Ok(Self::default()), + }, + } + } + + pub fn from_decimal(decimal: Decimal) -> Self { + Self { + decimal: Some(decimal), + money: None, + } + } + + pub fn from_money(money: Money<Currency>) -> Self { + Self { + decimal: Some(*money.amount()), + money: Some(money.to_string()), + } + } + + pub fn decimal(&self) -> &Option<Decimal> { + &self.decimal + } + + pub fn is_empty(&self) -> bool { + self.decimal.is_none() + } + + pub fn to_unformatted_string(&self) -> String { + match self.decimal { + None => String::default(), + Some(decimal) => decimal.to_string(), + } + } +} + +fn auto_fill_zero_at_start_if_need(num_str: &str) -> String { + match START_WITH_DOT_NUM_REGEX.captures(num_str) { + Ok(Some(captures)) => match captures.get(0).map(|m| m.as_str().to_string()) { + Some(s) => format!("0{}", s), + None => num_str.to_string(), + }, + _ => num_str.to_string(), + } +} + +fn extract_number(num_str: &str) -> String { + let mut matches = EXTRACT_NUM_REGEX.find_iter(num_str); + let mut values = vec![]; + while let Some(Ok(m)) = matches.next() { + values.push(m.as_str().to_string()); + } + values.join("") +} + +impl ToString for NumberCellFormat { + fn to_string(&self) -> String { + match &self.money { + None => match self.decimal { + None => String::default(), + Some(decimal) => decimal.to_string(), + }, + Some(money) => money.to_string(), + } + } +} pub struct NumberCellDataParser(); impl CellProtobufBlobParser for NumberCellDataParser { type Object = NumberCellFormat; fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> { match String::from_utf8(bytes.to_vec()) { - Ok(s) => NumberCellFormat::from_format_str(&s, &NumberFormat::Num).map_err(Into::into), + Ok(s) => NumberCellFormat::from_format_str(&s, &NumberFormat::Num), Err(_) => Ok(NumberCellFormat::default()), } } @@ -19,7 +124,7 @@ impl CellBytesCustomParser for NumberCellCustomDataParser { type Object = NumberCellFormat; fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> { match String::from_utf8(bytes.to_vec()) { - Ok(s) => NumberCellFormat::from_format_str(&s, &self.0).map_err(Into::into), + Ok(s) => NumberCellFormat::from_format_str(&s, &self.0), Err(_) => Ok(NumberCellFormat::default()), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/mod.rs index 92a224c235..4ae30a6589 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/mod.rs @@ -1,4 +1,5 @@ mod relation; mod relation_entities; +pub use relation::*; pub use relation_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs index 1ed1d6b1be..ac2548b89d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation.rs @@ -1,21 +1,40 @@ use std::cmp::Ordering; -use collab_database::fields::relation_type_option::RelationTypeOption; - +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; -use collab_database::template::relation_parse::RelationCellData; -use collab_database::template::util::ToCellString; use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; use crate::entities::{RelationCellDataPB, RelationFilterPB}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ - default_order, CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, + default_order, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use super::RelationCellChangeset; +use super::{RelationCellChangeset, RelationCellData}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RelationTypeOption { + pub database_id: String, +} + +impl From<TypeOptionData> for RelationTypeOption { + fn from(value: TypeOptionData) -> Self { + let database_id = value.get_str_value("database_id").unwrap_or_default(); + Self { database_id } + } +} + +impl From<RelationTypeOption> for TypeOptionData { + fn from(value: RelationTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_str_value("database_id", value.database_id) + .build() + } +} impl TypeOption for RelationTypeOption { type CellData = RelationCellData; @@ -34,10 +53,11 @@ impl CellDataChangeset for RelationTypeOption { let cell_data = RelationCellData { row_ids: changeset.inserted_row_ids, }; - return Ok(((cell_data.clone()).into(), cell_data)); + + return Ok(((&cell_data).into(), cell_data)); } - let cell_data: RelationCellData = cell.as_ref().unwrap().into(); + let cell_data: RelationCellData = cell.unwrap().as_ref().into(); let mut row_ids = cell_data.row_ids.clone(); for inserted in changeset.inserted_row_ids.iter() { if !row_ids.iter().any(|row_id| row_id == inserted) { @@ -52,13 +72,21 @@ impl CellDataChangeset for RelationTypeOption { let cell_data = RelationCellData { row_ids }; - Ok(((cell_data.clone()).into(), cell_data)) + Ok(((&cell_data).into(), cell_data)) } } impl CellDataDecoder for RelationTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<RelationCellData> { + Ok(cell.into()) + } + fn stringify_cell_data(&self, cell_data: RelationCellData) -> String { - cell_data.to_cell_string() + cell_data.to_string() + } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None } } @@ -81,8 +109,12 @@ impl TypeOptionCellDataFilter for RelationTypeOption { impl TypeOptionTransform for RelationTypeOption {} -impl CellDataProtobufEncoder for RelationTypeOption { +impl TypeOptionCellDataSerde for RelationTypeOption { fn protobuf_encode(&self, cell_data: RelationCellData) -> RelationCellDataPB { cell_data.into() } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<RelationCellData> { + Ok(cell.into()) + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs index 6c61bca8fe..97b18590af 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/relation_type_option/relation_entities.rs @@ -1,4 +1,82 @@ -use collab_database::rows::RowId; +use std::sync::Arc; + +use collab::preclude::Any; +use collab_database::rows::{new_cell_builder, Cell, RowId}; + +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; + +#[derive(Debug, Clone, Default)] +pub struct RelationCellData { + pub row_ids: Vec<RowId>, +} + +impl From<&Cell> for RelationCellData { + fn from(value: &Cell) -> Self { + let row_ids = match value.get(CELL_DATA) { + Some(Any::Array(array)) => array + .iter() + .flat_map(|item| { + if let Any::String(string) = item { + Some(RowId::from(string.clone().to_string())) + } else { + None + } + }) + .collect(), + _ => vec![], + }; + Self { row_ids } + } +} + +impl From<&RelationCellData> for Cell { + fn from(value: &RelationCellData) -> Self { + let data = Any::Array(Arc::from( + value + .row_ids + .clone() + .into_iter() + .map(|id| Any::String(Arc::from(id.to_string()))) + .collect::<Vec<_>>(), + )); + new_cell_builder(FieldType::Relation) + .insert_any(CELL_DATA, data) + .build() + } +} + +impl From<String> for RelationCellData { + fn from(s: String) -> Self { + if s.is_empty() { + return RelationCellData { row_ids: vec![] }; + } + + let ids = s + .split(", ") + .map(|id| id.to_string().into()) + .collect::<Vec<_>>(); + + RelationCellData { row_ids: ids } + } +} + +impl TypeOptionCellData for RelationCellData { + fn is_cell_empty(&self) -> bool { + self.row_ids.is_empty() + } +} + +impl ToString for RelationCellData { + fn to_string(&self) -> String { + self + .row_ids + .iter() + .map(|id| id.to_string()) + .collect::<Vec<_>>() + .join(", ") + } +} #[derive(Debug, Clone, Default)] pub struct RelationCellChangeset { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs index 8a4d4e40c9..dbcac0b8c2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/mod.rs @@ -1,7 +1,13 @@ mod multi_select_type_option; mod select_filter; +mod select_ids; +mod select_option; mod select_type_option; mod single_select_type_option; mod type_option_transform; +pub use multi_select_type_option::*; +pub use select_ids::*; +pub use select_option::*; pub use select_type_option::*; +pub use single_select_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 5f1cbeb5d8..8ebd0d1db4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -1,20 +1,27 @@ +use std::cmp::Ordering; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use serde::{Deserialize, Serialize}; + +use flowy_error::FlowyResult; + use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB}; use crate::services::cell::CellDataChangeset; use crate::services::field::{ - default_order, CellDataProtobufEncoder, SelectOptionCellChangeset, SelectTypeOptionSharedAction, - TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + default_order, SelectOption, SelectOptionCellChangeset, SelectOptionIds, + SelectTypeOptionSharedAction, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, }; use crate::services::sort::SortCondition; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionIds, -}; -use collab_database::fields::TypeOptionData; -use collab_database::rows::Cell; -use flowy_error::FlowyResult; - -use collab_database::template::util::ToCellString; -use std::cmp::Ordering; +// Multiple select +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct MultiSelectTypeOption { + pub options: Vec<SelectOption>, + pub disable_color: bool, +} impl TypeOption for MultiSelectTypeOption { type CellData = SelectOptionIds; @@ -23,13 +30,35 @@ impl TypeOption for MultiSelectTypeOption { type CellFilter = SelectOptionFilterPB; } -impl CellDataProtobufEncoder for MultiSelectTypeOption { +impl From<TypeOptionData> for MultiSelectTypeOption { + fn from(data: TypeOptionData) -> Self { + data + .get_str_value("content") + .map(|s| serde_json::from_str::<MultiSelectTypeOption>(&s).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From<MultiSelectTypeOption> for TypeOptionData { + fn from(data: MultiSelectTypeOption) -> Self { + let content = serde_json::to_string(&data).unwrap_or_default(); + TypeOptionDataBuilder::new() + .insert_str_value("content", content) + .build() + } +} + +impl TypeOptionCellDataSerde for MultiSelectTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { self.get_selected_options(cell_data).into() } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(SelectOptionIds::from(cell)) + } } impl SelectTypeOptionSharedAction for MultiSelectTypeOption { @@ -81,12 +110,12 @@ impl CellDataChangeset for MultiSelectTypeOption { select_ids.retain(|id| id != &delete_option_id); } - tracing::trace!("Multi-select cell data: {}", select_ids.to_cell_string()); + tracing::trace!("Multi-select cell data: {}", select_ids.to_string()); select_ids }, }; Ok(( - select_option_ids.to_cell(FieldType::MultiSelect), + select_option_ids.to_cell_data(FieldType::MultiSelect), select_option_ids, )) } @@ -149,21 +178,48 @@ impl TypeOptionCellDataCompare for MultiSelectTypeOption { #[cfg(test)] mod tests { + use crate::entities::FieldType; use crate::services::cell::CellDataChangeset; use crate::services::field::type_options::selection_type_option::*; - use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionIds, SelectTypeOption, - }; - use collab_database::template::util::ToCellString; + use crate::services::field::MultiSelectTypeOption; + use crate::services::field::{CheckboxTypeOption, TypeOptionTransform}; + + #[test] + fn multi_select_transform_with_checkbox_type_option_test() { + let checkbox_type_option = CheckboxTypeOption(); + let mut multi_select = MultiSelectTypeOption::default(); + multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.clone().into()); + debug_assert_eq!(multi_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + multi_select.transform_type_option(FieldType::Checkbox, checkbox_type_option.into()); + debug_assert_eq!(multi_select.options.len(), 2); + } + + #[test] + fn multi_select_transform_with_single_select_type_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let single_select = SingleSelectTypeOption { + options: vec![google, facebook], + disable_color: false, + }; + let mut multi_select = MultiSelectTypeOption { + options: vec![], + disable_color: false, + }; + multi_select.transform_type_option(FieldType::MultiSelect, single_select.into()); + debug_assert_eq!(multi_select.options.len(), 2); + } #[test] fn multi_select_insert_multi_option_test() { let google = SelectOption::new("Google"); let facebook = SelectOption::new("Facebook"); - let multi_select = MultiSelectTypeOption(SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![google.clone(), facebook.clone()], disable_color: false, - }); + }; let option_ids = vec![google.id, facebook.id]; let changeset = SelectOptionCellChangeset::from_insert_options(option_ids.clone()); @@ -177,10 +233,10 @@ mod tests { fn multi_select_unselect_multi_option_test() { let google = SelectOption::new("Google"); let facebook = SelectOption::new("Facebook"); - let multi_select = MultiSelectTypeOption(SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![google.clone(), facebook.clone()], disable_color: false, - }); + }; let option_ids = vec![google.id, facebook.id]; // insert @@ -197,23 +253,23 @@ mod tests { #[test] fn multi_select_insert_single_option_test() { let google = SelectOption::new("Google"); - let multi_select = MultiSelectTypeOption(SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![google.clone()], disable_color: false, - }); + }; let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id); let select_option_ids = multi_select.apply_changeset(changeset, None).unwrap().1; - assert_eq!(select_option_ids.to_cell_string(), google.id); + assert_eq!(select_option_ids.to_string(), google.id); } #[test] fn multi_select_insert_non_exist_option_test() { let google = SelectOption::new("Google"); - let multi_select = MultiSelectTypeOption(SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![], disable_color: false, - }); + }; let changeset = SelectOptionCellChangeset::from_insert_option_id(&google.id); let (_, select_option_ids) = multi_select.apply_changeset(changeset, None).unwrap(); @@ -223,10 +279,10 @@ mod tests { #[test] fn multi_select_insert_invalid_option_id_test() { let google = SelectOption::new("Google"); - let multi_select = MultiSelectTypeOption(SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![google], disable_color: false, - }); + }; // empty option id string let changeset = SelectOptionCellChangeset::from_insert_option_id(""); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs index af1ff97368..a0e1ce096b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs @@ -1,10 +1,9 @@ -use collab_database::fields::select_type_option::SelectOption; use collab_database::fields::Field; use collab_database::rows::Cell; use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; use crate::services::cell::insert_select_option_cell; -use crate::services::field::select_type_option_from_field; +use crate::services::field::{select_type_option_from_field, SelectOption}; use crate::services::filter::PreFillCellsWithFilter; impl SelectOptionFilterPB { @@ -55,14 +54,16 @@ impl SelectOptionFilterStrategy { return false; } - selected_option_ids.iter().all(|id| option_ids.contains(id)) + selected_option_ids.len() == option_ids.len() + && selected_option_ids.iter().all(|id| option_ids.contains(id)) }, SelectOptionFilterStrategy::IsNot(option_ids) => { if selected_option_ids.is_empty() { return true; } - !selected_option_ids.iter().all(|id| option_ids.contains(id)) + selected_option_ids.len() != option_ids.len() + || !selected_option_ids.iter().all(|id| option_ids.contains(id)) }, SelectOptionFilterStrategy::Contains(option_ids) => { if selected_option_ids.is_empty() { @@ -95,10 +96,19 @@ impl SelectOptionFilterStrategy { } impl PreFillCellsWithFilter for SelectOptionFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { + let get_non_empty_expected_options = || { + if !self.option_ids.is_empty() { + Some(self.option_ids.clone()) + } else { + None + } + }; + let option_ids = match self.condition { - SelectOptionFilterConditionPB::OptionIs | SelectOptionFilterConditionPB::OptionContains => { - self.option_ids.first().map(|id| vec![id.clone()]) + SelectOptionFilterConditionPB::OptionIs => get_non_empty_expected_options(), + SelectOptionFilterConditionPB::OptionContains => { + get_non_empty_expected_options().map(|mut options| vec![options.swap_remove(0)]) }, SelectOptionFilterConditionPB::OptionIsNotEmpty => select_type_option_from_field(field) .ok() @@ -113,14 +123,16 @@ impl PreFillCellsWithFilter for SelectOptionFilterPB { _ => None, }; - option_ids.map(|ids| insert_select_option_cell(ids, field)) + ( + option_ids.map(|ids| insert_select_option_cell(ids, field)), + false, + ) } } - #[cfg(test)] mod tests { use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; - use collab_database::fields::select_type_option::SelectOption; + use crate::services::field::SelectOption; #[test] fn select_option_filter_is_empty_test() { @@ -140,12 +152,11 @@ mod tests { let option_2 = SelectOption::new("B"); let filter = SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, - option_ids: vec![], + option_ids: vec![option_1.id.clone(), option_2.id.clone()], }; assert_eq!(filter.is_visible(&[]), Some(false)); assert_eq!(filter.is_visible(&[option_1.clone()]), Some(true)); - assert_eq!(filter.is_visible(&[option_1, option_2]), Some(true)); } #[test] @@ -176,6 +187,8 @@ mod tests { (vec![], Some(false)), (vec![option_1.clone()], Some(true)), (vec![option_2.clone()], Some(false)), + (vec![option_3.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(false)), ] { assert_eq!(filter.is_visible(&options), is_visible); } @@ -187,9 +200,12 @@ mod tests { }; for (options, is_visible) in [ (vec![], Some(false)), - (vec![option_1.clone()], Some(true)), - (vec![option_2.clone()], Some(true)), - (vec![option_3.clone()], Some(false)), + (vec![option_1.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(true)), + ( + vec![option_1.clone(), option_2.clone(), option_3.clone()], + Some(false), + ), ] { assert_eq!(filter.is_visible(&options), is_visible); } @@ -209,7 +225,7 @@ mod tests { for (options, is_visible) in [ (vec![], None), (vec![option_1.clone()], None), - (vec![option_2.clone()], None), + (vec![option_1.clone(), option_2.clone()], None), ] { assert_eq!(filter.is_visible(&options), is_visible); } @@ -224,6 +240,7 @@ mod tests { (vec![option_1.clone()], Some(false)), (vec![option_2.clone()], Some(true)), (vec![option_3.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(true)), ] { assert_eq!(filter.is_visible(&options), is_visible); } @@ -235,9 +252,12 @@ mod tests { }; for (options, is_visible) in [ (vec![], Some(true)), - (vec![option_1.clone()], Some(false)), - (vec![option_2.clone()], Some(false)), - (vec![option_3.clone()], Some(true)), + (vec![option_1.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(false)), + ( + vec![option_1.clone(), option_2.clone(), option_3.clone()], + Some(true), + ), ] { assert_eq!(filter.is_visible(&options), is_visible); } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs new file mode 100644 index 0000000000..c47738b788 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_ids.rs @@ -0,0 +1,102 @@ +use std::str::FromStr; + +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +use flowy_error::FlowyError; + +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; + +pub const SELECTION_IDS_SEPARATOR: &str = ","; + +/// List of select option ids +/// +/// Calls [to_string] will return a string consists list of ids, +/// placing a commas separator between each +/// +#[derive(Default, Clone, Debug)] +pub struct SelectOptionIds(Vec<String>); + +impl SelectOptionIds { + pub fn new() -> Self { + Self::default() + } + pub fn into_inner(self) -> Vec<String> { + self.0 + } + pub fn to_cell_data(&self, field_type: FieldType) -> Cell { + new_cell_builder(field_type) + .insert_str_value(CELL_DATA, self.to_string()) + .build() + } +} + +impl TypeOptionCellData for SelectOptionIds { + fn is_cell_empty(&self) -> bool { + self.is_empty() + } +} + +impl From<&Cell> for SelectOptionIds { + fn from(cell: &Cell) -> Self { + let value = cell.get_str_value(CELL_DATA).unwrap_or_default(); + Self::from_str(&value).unwrap_or_default() + } +} + +impl FromStr for SelectOptionIds { + type Err = FlowyError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s.is_empty() { + return Ok(Self(vec![])); + } + let ids = s + .split(SELECTION_IDS_SEPARATOR) + .map(|id| id.to_string()) + .collect::<Vec<String>>(); + Ok(Self(ids)) + } +} + +impl std::convert::From<Vec<String>> for SelectOptionIds { + fn from(ids: Vec<String>) -> Self { + let ids = ids + .into_iter() + .filter(|id| !id.is_empty()) + .collect::<Vec<String>>(); + Self(ids) + } +} + +impl ToString for SelectOptionIds { + /// Returns a string that consists list of ids, placing a commas + /// separator between each + fn to_string(&self) -> String { + self.0.join(SELECTION_IDS_SEPARATOR) + } +} + +impl std::convert::From<Option<String>> for SelectOptionIds { + fn from(s: Option<String>) -> Self { + match s { + None => Self(vec![]), + Some(s) => Self::from_str(&s).unwrap_or_default(), + } + } +} + +impl std::ops::Deref for SelectOptionIds { + type Target = Vec<String>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SelectOptionIds { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs new file mode 100644 index 0000000000..f7755f55b5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_option.rs @@ -0,0 +1,75 @@ +use crate::entities::SelectOptionCellDataPB; +use crate::services::field::SelectOptionIds; +use collab_database::database::gen_option_id; +use serde::{Deserialize, Serialize}; + +/// [SelectOption] represents an option for a single select, and multiple select. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SelectOption { + pub id: String, + pub name: String, + pub color: SelectOptionColor, +} + +impl SelectOption { + pub fn new(name: &str) -> Self { + SelectOption { + id: gen_option_id(), + name: name.to_owned(), + color: SelectOptionColor::default(), + } + } + + pub fn with_color(name: &str, color: SelectOptionColor) -> Self { + SelectOption { + id: gen_option_id(), + name: name.to_owned(), + color, + } + } +} + +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[repr(u8)] +#[derive(Default)] +pub enum SelectOptionColor { + #[default] + Purple = 0, + Pink = 1, + LightPink = 2, + Orange = 3, + Yellow = 4, + Lime = 5, + Green = 6, + Aqua = 7, + Blue = 8, +} + +#[derive(Debug)] +pub struct SelectOptionCellData { + pub select_options: Vec<SelectOption>, +} + +impl From<SelectOptionCellData> for SelectOptionCellDataPB { + fn from(data: SelectOptionCellData) -> Self { + SelectOptionCellDataPB { + select_options: data + .select_options + .into_iter() + .map(|option| option.into()) + .collect(), + } + } +} + +pub fn make_selected_options(ids: SelectOptionIds, options: &[SelectOption]) -> Vec<SelectOption> { + ids + .iter() + .flat_map(|option_id| { + options + .iter() + .find(|option| &option.id == option_id) + .cloned() + }) + .collect() +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index dde93d4f50..4e558b5c83 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -1,21 +1,19 @@ +use std::str::FromStr; + +use bytes::Bytes; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::Cell; + +use flowy_error::{internal_error, ErrorCode, FlowyResult}; + use crate::entities::{CheckboxCellDataPB, FieldType, SelectOptionCellDataPB}; use crate::services::cell::{CellDataDecoder, CellProtobufBlobParser}; use crate::services::field::selection_type_option::type_option_transform::SelectOptionTypeOptionTransformHelper; use crate::services::field::{ - CellDataProtobufEncoder, StringCellData, TypeOption, TypeOptionTransform, + make_selected_options, MultiSelectTypeOption, SelectOption, SelectOptionCellData, + SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, TypeOption, TypeOptionCellDataSerde, + TypeOptionTransform, SELECTION_IDS_SEPARATOR, }; -use async_trait::async_trait; -use bytes::Bytes; -use collab_database::database::Database; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionColor, SelectOptionIds, SingleSelectTypeOption, - SELECTION_IDS_SEPARATOR, -}; -use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::Cell; -use collab_database::template::util::ToCellString; -use flowy_error::{internal_error, ErrorCode, FlowyResult}; -use std::str::FromStr; /// Defines the shared actions used by SingleSelect or Multi-Select. pub trait SelectTypeOptionSharedAction: Send + Sync { @@ -70,38 +68,32 @@ pub trait SelectTypeOptionSharedAction: Send + Sync { fn mut_options(&mut self) -> &mut Vec<SelectOption>; } -#[async_trait] impl<T> TypeOptionTransform for T where T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + CellDataDecoder, { - async fn transform_type_option( + fn transform_type_option( &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - old_type_option_data: TypeOptionData, - new_type_option_field_type: FieldType, - database: &mut Database, + _old_type_option_field_type: FieldType, + _old_type_option_data: TypeOptionData, ) { SelectOptionTypeOptionTransformHelper::transform_type_option( self, - view_id, - field_id, - &old_type_option_field_type, - old_type_option_data, - new_type_option_field_type, - database, - ) - .await; + &_old_type_option_field_type, + _old_type_option_data, + ); } } impl<T> CellDataDecoder for T where T: - SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + CellDataProtobufEncoder, + SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds> + TypeOptionCellDataSerde, { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + fn decode_cell_with_transform( &self, cell: &Cell, @@ -109,17 +101,8 @@ where _field: &Field, ) -> Option<<Self as TypeOption>::CellData> { match from_field_type { - FieldType::RichText => { - let text_cell = StringCellData::from(cell).into_inner(); - let mut transformed_ids = Vec::new(); - let options = self.options(); - if let Some(option) = options.iter().find(|option| option.name == text_cell) { - transformed_ids.push(option.id.clone()); - } - Some(SelectOptionIds::from(transformed_ids)) - }, FieldType::Checkbox => { - let cell_content = CheckboxCellDataPB::from(cell).to_cell_string(); + let cell_content = CheckboxCellDataPB::from(cell).to_string(); let mut transformed_ids = Vec::new(); let options = self.options(); if let Some(option) = options.iter().find(|option| option.name == cell_content) { @@ -127,6 +110,7 @@ where } Some(SelectOptionIds::from(transformed_ids)) }, + FieldType::RichText => Some(SelectOptionIds::from(cell)), FieldType::SingleSelect | FieldType::MultiSelect => Some(SelectOptionIds::from(cell)), _ => None, } @@ -141,6 +125,10 @@ where .collect::<Vec<String>>() .join(SELECTION_IDS_SEPARATOR) } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None + } } pub fn select_type_option_from_field( @@ -199,7 +187,7 @@ impl CellProtobufBlobParser for SelectOptionIdsParser { type Object = SelectOptionIds; fn parser(bytes: &Bytes) -> FlowyResult<Self::Object> { match String::from_utf8(bytes.to_vec()) { - Ok(s) => SelectOptionIds::from_str(&s).map_err(Into::into), + Ok(s) => SelectOptionIds::from_str(&s), Err(_) => Ok(SelectOptionIds::default()), } } @@ -249,32 +237,3 @@ impl SelectOptionCellChangeset { } } } - -#[derive(Debug)] -pub struct SelectOptionCellData { - pub select_options: Vec<SelectOption>, -} - -impl From<SelectOptionCellData> for SelectOptionCellDataPB { - fn from(data: SelectOptionCellData) -> Self { - SelectOptionCellDataPB { - select_options: data - .select_options - .into_iter() - .map(|option| option.into()) - .collect(), - } - } -} - -pub fn make_selected_options(ids: SelectOptionIds, options: &[SelectOption]) -> Vec<SelectOption> { - ids - .iter() - .flat_map(|option_id| { - options - .iter() - .find(|option| &option.id == option_id) - .cloned() - }) - .collect() -} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs index b91cb5be3b..fa0745133b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -1,22 +1,26 @@ use crate::entities::{FieldType, SelectOptionCellDataPB, SelectOptionFilterPB}; use crate::services::cell::CellDataChangeset; use crate::services::field::{ - default_order, CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, + default_order, SelectOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, +}; +use crate::services::field::{ + SelectOptionCellChangeset, SelectOptionIds, SelectTypeOptionSharedAction, }; -use crate::services::field::{SelectOptionCellChangeset, SelectTypeOptionSharedAction}; use crate::services::sort::SortCondition; - -use collab_database::fields::select_type_option::{ - SelectOption, SelectOptionIds, SingleSelectTypeOption, -}; -use collab_database::fields::TypeOptionData; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; - +use serde::{Deserialize, Serialize}; use std::cmp::Ordering; // Single select +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct SingleSelectTypeOption { + pub options: Vec<SelectOption>, + pub disable_color: bool, +} impl TypeOption for SingleSelectTypeOption { type CellData = SelectOptionIds; @@ -25,13 +29,35 @@ impl TypeOption for SingleSelectTypeOption { type CellFilter = SelectOptionFilterPB; } -impl CellDataProtobufEncoder for SingleSelectTypeOption { +impl From<TypeOptionData> for SingleSelectTypeOption { + fn from(data: TypeOptionData) -> Self { + data + .get_str_value("content") + .map(|s| serde_json::from_str::<SingleSelectTypeOption>(&s).unwrap_or_default()) + .unwrap_or_default() + } +} + +impl From<SingleSelectTypeOption> for TypeOptionData { + fn from(data: SingleSelectTypeOption) -> Self { + let content = serde_json::to_string(&data).unwrap_or_default(); + TypeOptionDataBuilder::new() + .insert_str_value("content", content) + .build() + } +} + +impl TypeOptionCellDataSerde for SingleSelectTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { self.get_selected_options(cell_data).into() } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(SelectOptionIds::from(cell)) + } } impl SelectTypeOptionSharedAction for SingleSelectTypeOption { @@ -80,7 +106,7 @@ impl CellDataChangeset for SingleSelectTypeOption { SelectOptionIds::from(insert_option_ids) }; Ok(( - select_option_ids.to_cell(FieldType::SingleSelect), + select_option_ids.to_cell_data(FieldType::SingleSelect), select_option_ids, )) } @@ -125,20 +151,49 @@ impl TypeOptionCellDataCompare for SingleSelectTypeOption { #[cfg(test)] mod tests { + use crate::entities::FieldType; use crate::services::cell::CellDataChangeset; use crate::services::field::type_options::*; - use collab_database::fields::select_type_option::{ - SelectOption, SelectTypeOption, SingleSelectTypeOption, - }; + + #[test] + fn single_select_transform_with_checkbox_type_option_test() { + let checkbox = CheckboxTypeOption::default(); + + let mut single_select = SingleSelectTypeOption::default(); + single_select.transform_type_option(FieldType::Checkbox, checkbox.clone().into()); + debug_assert_eq!(single_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + single_select.transform_type_option(FieldType::Checkbox, checkbox.into()); + debug_assert_eq!(single_select.options.len(), 2); + } + + #[test] + fn single_select_transform_with_multi_select_type_option_test() { + let google = SelectOption::new("Google"); + let facebook = SelectOption::new("Facebook"); + let multi_select = MultiSelectTypeOption { + options: vec![google, facebook], + disable_color: false, + }; + + let mut single_select = SingleSelectTypeOption::default(); + single_select.transform_type_option(FieldType::MultiSelect, multi_select.clone().into()); + debug_assert_eq!(single_select.options.len(), 2); + + // Already contain the yes/no option. It doesn't need to insert new options + single_select.transform_type_option(FieldType::MultiSelect, multi_select.into()); + debug_assert_eq!(single_select.options.len(), 2); + } #[test] fn single_select_insert_multi_option_test() { let google = SelectOption::new("Google"); let facebook = SelectOption::new("Facebook"); - let single_select = SingleSelectTypeOption(SelectTypeOption { + let single_select = SingleSelectTypeOption { options: vec![google.clone(), facebook.clone()], disable_color: false, - }); + }; let option_ids = vec![google.id.clone(), facebook.id]; let changeset = SelectOptionCellChangeset::from_insert_options(option_ids); @@ -150,10 +205,10 @@ mod tests { fn single_select_unselect_multi_option_test() { let google = SelectOption::new("Google"); let facebook = SelectOption::new("Facebook"); - let single_select = SingleSelectTypeOption(SelectTypeOption { + let single_select = SingleSelectTypeOption { options: vec![google.clone(), facebook.clone()], disable_color: false, - }); + }; let option_ids = vec![google.id.clone(), facebook.id]; // insert @@ -164,6 +219,6 @@ mod tests { // delete let changeset = SelectOptionCellChangeset::from_delete_options(option_ids); let select_option_ids = single_select.apply_changeset(changeset, None).unwrap().1; - assert!(select_option_ids.is_empty()); + assert!(select_option_ids.is_cell_empty()); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs index b085bab09d..427069c182 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/type_option_transform.rs @@ -1,14 +1,9 @@ use crate::entities::FieldType; -use crate::services::cell::CellDataDecoder; -use crate::services::field::{SelectTypeOptionSharedAction, TypeOption, CHECK, UNCHECK}; -use collab_database::database::Database; -use collab_database::fields::select_type_option::{ - SelectOption, SelectOptionColor, SelectOptionIds, SelectTypeOption, SELECTION_IDS_SEPARATOR, +use crate::services::field::{ + MultiSelectTypeOption, SelectOption, SelectOptionColor, SelectOptionIds, + SelectTypeOptionSharedAction, SingleSelectTypeOption, TypeOption, CHECK, UNCHECK, }; -use collab_database::fields::text_type_option::RichTextTypeOption; use collab_database::fields::TypeOptionData; -use collab_database::template::option_parse::build_options_from_cells; -use tracing::info; /// Handles how to transform the cell data when switching between different field types pub(crate) struct SelectOptionTypeOptionTransformHelper(); @@ -19,70 +14,14 @@ impl SelectOptionTypeOptionTransformHelper { /// /// * `old_field_type`: the FieldType of the passed-in TypeOptionData /// - pub async fn transform_type_option<T>( + pub fn transform_type_option<T>( shared: &mut T, - view_id: &str, - field_id: &str, old_field_type: &FieldType, old_type_option_data: TypeOptionData, - new_field_type: FieldType, - database: &mut Database, ) where T: SelectTypeOptionSharedAction + TypeOption<CellData = SelectOptionIds>, { match old_field_type { - FieldType::RichText => { - if !shared.options().is_empty() { - return; - } - let text_type_option = RichTextTypeOption::from(old_type_option_data); - let rows = database - .get_cells_for_field(view_id, field_id) - .await - .into_iter() - .filter_map(|row| row.cell.map(|cell| (row.row_id, cell))) - .map(|(row_id, cell)| { - let text = text_type_option - .decode_cell(&cell) - .unwrap_or_default() - .into_inner(); - (row_id, text) - }) - .collect::<Vec<_>>(); - - let options = - build_options_from_cells(&rows.iter().map(|row| row.1.clone()).collect::<Vec<_>>()); - info!( - "Transforming RichText to SelectOption, updating {} row's cell content", - rows.len() - ); - for (row_id, text_cell) in rows { - let mut transformed_ids = Vec::new(); - let names = text_cell - .split(SELECTION_IDS_SEPARATOR) - .map(|name| name.to_string()) - .collect::<Vec<_>>(); - - names.into_iter().for_each(|name| { - if let Some(option) = options.iter().find(|option| option.name == name) { - transformed_ids.push(option.id.clone()); - } - }); - - database - .update_row(row_id, |row| { - row.update_cells(|cell| { - cell.insert( - field_id, - SelectOptionIds::from(transformed_ids).to_cell(new_field_type), - ); - }); - }) - .await; - } - - shared.mut_options().extend(options); - }, FieldType::Checkbox => { // add Yes and No options if it does not exist. if !shared.options().iter().any(|option| option.name == CHECK) { @@ -96,7 +35,7 @@ impl SelectOptionTypeOptionTransformHelper { } }, FieldType::MultiSelect => { - let options = SelectTypeOption::from(old_type_option_data).options; + let options = MultiSelectTypeOption::from(old_type_option_data).options; options.iter().for_each(|new_option| { if !shared .options() @@ -108,7 +47,7 @@ impl SelectOptionTypeOptionTransformHelper { }) }, FieldType::SingleSelect => { - let options = SelectTypeOption::from(old_type_option_data).options; + let options = SingleSelectTypeOption::from(old_type_option_data).options; options.iter().for_each(|new_option| { if !shared .options() diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs index 1c631a9922..e927cc4feb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/mod.rs @@ -1 +1,2 @@ pub mod summary; +pub mod summary_entities; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs index 87aed94576..920f76de8e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary.rs @@ -1,17 +1,38 @@ use crate::entities::TextFilterPB; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::summary_type_option::summary_entities::SummaryCellData; use crate::services::field::type_options::util::ProtobufStr; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab_database::fields::summary_type_option::SummarizationTypeOption; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; -use collab_database::template::summary_parse::SummaryCellData; use flowy_error::FlowyResult; use std::cmp::Ordering; +#[derive(Default, Debug, Clone)] +pub struct SummarizationTypeOption { + pub auto_fill: bool, +} + +impl From<TypeOptionData> for SummarizationTypeOption { + fn from(value: TypeOptionData) -> Self { + let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); + Self { auto_fill } + } +} + +impl From<SummarizationTypeOption> for TypeOptionData { + fn from(value: SummarizationTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_bool_value("auto_fill", value.auto_fill) + .build() + } +} + impl TypeOption for SummarizationTypeOption { type CellData = SummaryCellData; type CellChangeset = String; @@ -60,17 +81,29 @@ impl TypeOptionCellDataCompare for SummarizationTypeOption { } impl CellDataDecoder for SummarizationTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<SummaryCellData> { + Ok(SummaryCellData::from(cell)) + } + fn stringify_cell_data(&self, cell_data: SummaryCellData) -> String { cell_data.to_string() } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None + } } impl TypeOptionTransform for SummarizationTypeOption {} -impl CellDataProtobufEncoder for SummarizationTypeOption { +impl TypeOptionCellDataSerde for SummarizationTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { ProtobufStr::from(cell_data.0) } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(SummaryCellData::from(cell)) + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs new file mode 100644 index 0000000000..8d45578e38 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/summary_type_option/summary_entities.rs @@ -0,0 +1,46 @@ +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +#[derive(Default, Debug, Clone)] +pub struct SummaryCellData(pub String); +impl std::ops::Deref for SummaryCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TypeOptionCellData for SummaryCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From<&Cell> for SummaryCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + } +} + +impl From<SummaryCellData> for Cell { + fn from(data: SummaryCellData) -> Self { + new_cell_builder(FieldType::Summary) + .insert_str_value(CELL_DATA, data.0) + .build() + } +} + +impl ToString for SummaryCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef<str> for SummaryCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs index 8ba903f217..8f090f5802 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs @@ -8,18 +8,7 @@ impl TextFilterPB { pub fn is_visible<T: AsRef<str>>(&self, cell_data: T) -> bool { let cell_data = cell_data.as_ref().to_lowercase(); let content = &self.content.to_lowercase(); - match self.condition { - TextFilterConditionPB::TextIs - | TextFilterConditionPB::TextIsNot - | TextFilterConditionPB::TextContains - | TextFilterConditionPB::TextDoesNotContain - | TextFilterConditionPB::TextStartsWith - | TextFilterConditionPB::TextEndsWith - if content.is_empty() => - { - true - }, TextFilterConditionPB::TextIs => &cell_data == content, TextFilterConditionPB::TextIsNot => &cell_data != content, TextFilterConditionPB::TextContains => cell_data.contains(content), @@ -33,7 +22,7 @@ impl TextFilterPB { } impl PreFillCellsWithFilter for TextFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { let text = match self.condition { TextFilterConditionPB::TextIs | TextFilterConditionPB::TextContains @@ -46,7 +35,9 @@ impl PreFillCellsWithFilter for TextFilterPB { _ => None, }; - text.map(|s| insert_text_cell(s, field)) + let open_after_create = matches!(self.condition, TextFilterConditionPB::TextIsNotEmpty); + + (text.map(|s| insert_text_cell(s, field)), open_after_create) } } @@ -62,22 +53,10 @@ mod tests { content: "appflowy".to_owned(), }; - assert_eq!(text_filter.is_visible("AppFlowy"), true); + assert!(text_filter.is_visible("AppFlowy")); assert_eq!(text_filter.is_visible("appflowy"), true); assert_eq!(text_filter.is_visible("Appflowy"), true); assert_eq!(text_filter.is_visible("AppFlowy.io"), false); - assert_eq!(text_filter.is_visible(""), false); - - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextIs, - content: "".to_owned(), - }; - - assert_eq!(text_filter.is_visible("AppFlowy"), true); - assert_eq!(text_filter.is_visible("appflowy"), true); - assert_eq!(text_filter.is_visible("Appflowy"), true); - assert_eq!(text_filter.is_visible("AppFlowy.io"), true); - assert_eq!(text_filter.is_visible(""), true); } #[test] fn text_filter_start_with_test() { @@ -89,17 +68,6 @@ mod tests { assert_eq!(text_filter.is_visible("AppFlowy.io"), true); assert_eq!(text_filter.is_visible(""), false); assert_eq!(text_filter.is_visible("https"), false); - assert_eq!(text_filter.is_visible(""), false); - - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextStartsWith, - content: "".to_owned(), - }; - - assert_eq!(text_filter.is_visible("AppFlowy.io"), true); - assert_eq!(text_filter.is_visible(""), true); - assert_eq!(text_filter.is_visible("https"), true); - assert_eq!(text_filter.is_visible(""), true); } #[test] @@ -112,17 +80,6 @@ mod tests { assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true); assert_eq!(text_filter.is_visible("App"), false); assert_eq!(text_filter.is_visible("appflowy.io"), false); - assert_eq!(text_filter.is_visible(""), false); - - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextEndsWith, - content: "".to_owned(), - }; - - assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true); - assert_eq!(text_filter.is_visible("App"), true); - assert_eq!(text_filter.is_visible("appflowy.io"), true); - assert_eq!(text_filter.is_visible(""), true); } #[test] fn text_filter_empty_test() { @@ -133,14 +90,6 @@ mod tests { assert_eq!(text_filter.is_visible(""), true); assert_eq!(text_filter.is_visible("App"), false); - - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextIsEmpty, - content: "".to_owned(), - }; - - assert_eq!(text_filter.is_visible(""), true); - assert_eq!(text_filter.is_visible("App"), false); } #[test] fn text_filter_contain_test() { @@ -154,16 +103,5 @@ mod tests { assert_eq!(text_filter.is_visible("App"), false); assert_eq!(text_filter.is_visible(""), false); assert_eq!(text_filter.is_visible("github"), false); - - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextContains, - content: "".to_owned(), - }; - - assert_eq!(text_filter.is_visible("https://github.com/appflowy"), true); - assert_eq!(text_filter.is_visible("AppFlowy"), true); - assert_eq!(text_filter.is_visible("App"), true); - assert_eq!(text_filter.is_visible(""), true); - assert_eq!(text_filter.is_visible("github"), true); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs index e5101a2775..5728642b9e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs @@ -3,16 +3,14 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::{insert_select_option_cell, stringify_cell}; use crate::services::field::FieldBuilder; - - use collab_database::fields::date_type_option::{DateCellData, DateTypeOption}; - use collab_database::fields::select_type_option::{SelectOption, SelectTypeOption}; + use crate::services::field::*; // Test parser the cell data which field's type is FieldType::Date to cell data // which field's type is FieldType::Text #[test] fn date_type_to_text_type() { let field_type = FieldType::DateTime; - let field = FieldBuilder::new(field_type, DateTypeOption::default_utc()).build(); + let field = FieldBuilder::new(field_type, DateTypeOption::test()).build(); let data = DateCellData { timestamp: Some(1647251762), @@ -69,7 +67,7 @@ mod tests { let done_option = SelectOption::new("Done"); let option_id = done_option.id.clone(); - let single_select = SelectTypeOption { + let single_select = SingleSelectTypeOption { options: vec![done_option.clone()], disable_color: false, }; @@ -86,7 +84,7 @@ mod tests { let france = SelectOption::new("france"); let argentina = SelectOption::new("argentina"); - let multi_select = SelectTypeOption { + let multi_select = MultiSelectTypeOption { options: vec![france.clone(), argentina.clone()], disable_color: false, }; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index 6872d97d95..5cb2875de5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -1,21 +1,29 @@ -use collab::util::AnyMapExt; use std::cmp::Ordering; -use collab_database::fields::text_type_option::RichTextTypeOption; -use collab_database::fields::Field; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::{new_cell_builder, Cell}; -use collab_database::template::util::ToCellString; +use serde::{Deserialize, Serialize}; + use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{FieldType, TextFilterPB}; use crate::services::cell::{stringify_cell, CellDataChangeset, CellDataDecoder}; use crate::services::field::type_options::util::ProtobufStr; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, CELL_DATA, + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, CELL_DATA, }; use crate::services::sort::SortCondition; +/// For the moment, the `RichTextTypeOptionPB` is empty. The `data` property is not +/// used yet. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct RichTextTypeOption { + #[serde(default)] + pub inner: String, +} + impl TypeOption for RichTextTypeOption { type CellData = StringCellData; type CellChangeset = String; @@ -23,18 +31,41 @@ impl TypeOption for RichTextTypeOption { type CellFilter = TextFilterPB; } +impl From<TypeOptionData> for RichTextTypeOption { + fn from(data: TypeOptionData) -> Self { + let s = data.get_str_value(CELL_DATA).unwrap_or_default(); + Self { inner: s } + } +} + +impl From<RichTextTypeOption> for TypeOptionData { + fn from(data: RichTextTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_str_value(CELL_DATA, data.inner) + .build() + } +} + impl TypeOptionTransform for RichTextTypeOption {} -impl CellDataProtobufEncoder for RichTextTypeOption { +impl TypeOptionCellDataSerde for RichTextTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { ProtobufStr::from(cell_data.0) } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(StringCellData::from(cell)) + } } impl CellDataDecoder for RichTextTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(StringCellData::from(cell)) + } + fn decode_cell_with_transform( &self, cell: &Cell, @@ -51,7 +82,6 @@ impl CellDataDecoder for RichTextTypeOption { | FieldType::URL | FieldType::Summary | FieldType::Translate - | FieldType::Media | FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))), FieldType::Checklist | FieldType::LastEditedTime @@ -63,6 +93,10 @@ impl CellDataDecoder for RichTextTypeOption { fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { cell_data.to_string() } + + fn numeric_cell(&self, cell: &Cell) -> Option<f64> { + StringCellData::from(cell).0.parse::<f64>().ok() + } } impl CellDataChangeset for RichTextTypeOption { @@ -114,11 +148,6 @@ impl TypeOptionCellDataCompare for RichTextTypeOption { #[derive(Default, Debug, Clone)] pub struct StringCellData(pub String); -impl StringCellData { - pub fn into_inner(self) -> String { - self.0 - } -} impl std::ops::Deref for StringCellData { type Target = String; @@ -135,15 +164,15 @@ impl TypeOptionCellData for StringCellData { impl From<&Cell> for StringCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_as(CELL_DATA).unwrap_or_default()) + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) } } impl From<StringCellData> for Cell { fn from(data: StringCellData) -> Self { - let mut cell = new_cell_builder(FieldType::RichText); - cell.insert(CELL_DATA.into(), data.0.into()); - cell + new_cell_builder(FieldType::RichText) + .insert_str_value(CELL_DATA, data.0) + .build() } } @@ -159,8 +188,8 @@ impl std::convert::From<String> for StringCellData { } } -impl ToCellString for StringCellData { - fn to_cell_string(&self) -> String { +impl ToString for StringCellData { + fn to_string(&self) -> String { self.0.clone() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs index 7f700fc4c7..d64ecf45a3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs @@ -1,4 +1,6 @@ mod time; +mod time_entities; mod time_filter; pub use time::*; +pub use time_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs index 218ac8daf4..0b7c141cb8 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs @@ -1,18 +1,19 @@ use crate::entities::{TimeCellDataPB, TimeFilterPB}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, - TypeOptionTransform, + TimeCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab_database::fields::date_type_option::TimeTypeOption; - +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; - -use collab_database::template::time_parse::TimeCellData; +use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TimeTypeOption; + impl TypeOption for TimeTypeOption { type CellData = TimeCellData; type CellChangeset = TimeCellChangeset; @@ -20,7 +21,19 @@ impl TypeOption for TimeTypeOption { type CellFilter = TimeFilterPB; } -impl CellDataProtobufEncoder for TimeTypeOption { +impl From<TypeOptionData> for TimeTypeOption { + fn from(_data: TypeOptionData) -> Self { + Self + } +} + +impl From<TimeTypeOption> for TypeOptionData { + fn from(_data: TimeTypeOption) -> Self { + TypeOptionDataBuilder::new().build() + } +} + +impl TypeOptionCellDataSerde for TimeTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, @@ -32,17 +45,36 @@ impl CellDataProtobufEncoder for TimeTypeOption { time: i64::default(), } } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(TimeCellData::from(cell)) + } +} + +impl TimeTypeOption { + pub fn new() -> Self { + Self + } } impl TypeOptionTransform for TimeTypeOption {} impl CellDataDecoder for TimeTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { if let Some(time) = cell_data.0 { return time.to_string(); } "".to_string() } + + fn numeric_cell(&self, cell: &Cell) -> Option<f64> { + let time_cell_data = self.parse_cell(cell).ok()?; + Some(time_cell_data.0.unwrap() as f64) + } } pub type TimeCellChangeset = String; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs new file mode 100644 index 0000000000..6084c80b5f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs @@ -0,0 +1,47 @@ +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +#[derive(Clone, Debug, Default)] +pub struct TimeCellData(pub Option<i64>); + +impl TypeOptionCellData for TimeCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_none() + } +} + +impl From<&Cell> for TimeCellData { + fn from(cell: &Cell) -> Self { + Self( + cell + .get_str_value(CELL_DATA) + .and_then(|data| data.parse::<i64>().ok()), + ) + } +} + +impl std::convert::From<String> for TimeCellData { + fn from(s: String) -> Self { + Self(s.trim().to_string().parse::<i64>().ok()) + } +} + +impl ToString for TimeCellData { + fn to_string(&self) -> String { + if let Some(time) = self.0 { + time.to_string() + } else { + "".to_string() + } + } +} + +impl From<&TimeCellData> for Cell { + fn from(data: &TimeCellData) -> Self { + new_cell_builder(FieldType::Time) + .insert_str_value(CELL_DATA, data.to_string()) + .build() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs index 13455a4ad0..0620317dc0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs @@ -38,7 +38,7 @@ impl TimeFilterPB { } impl PreFillCellsWithFilter for TimeFilterPB { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell> { + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) { let expected_decimal = || self.content.parse::<i64>().ok(); let text = match self.condition { @@ -64,7 +64,9 @@ impl PreFillCellsWithFilter for TimeFilterPB { _ => None, }; + let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty); + // use `insert_text_cell` because self.content might not be a parsable i64. - text.map(|s| insert_text_cell(s, field)) + (text.map(|s| insert_text_cell(s, field)), open_after_create) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs index 579d1b9ea6..3041a7947b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs @@ -1,2 +1,6 @@ #![allow(clippy::module_inception)] mod timestamp_type_option; +mod timestamp_type_option_entities; + +pub use timestamp_type_option::*; +pub use timestamp_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs index 528f4df01b..17b9f54dd3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs @@ -1,15 +1,38 @@ -use crate::entities::{DateFilterPB, TimestampCellDataPB}; +use std::cmp::Ordering; + +use chrono::{DateTime, Local, Offset}; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use serde::{Deserialize, Serialize}; + +use crate::entities::{DateFilterPB, FieldType, TimestampCellDataPB}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ - default_order, CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, + default_order, DateFormat, TimeFormat, TimestampCellData, TypeOption, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::rows::Cell; -use collab_database::template::timestamp_parse::TimestampCellData; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use std::cmp::Ordering; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TimestampTypeOption { + pub date_format: DateFormat, + pub time_format: TimeFormat, + pub include_time: bool, + pub field_type: FieldType, +} + +impl Default for TimestampTypeOption { + fn default() -> Self { + Self { + date_format: Default::default(), + time_format: Default::default(), + include_time: true, + field_type: FieldType::LastEditedTime, + } + } +} impl TypeOption for TimestampTypeOption { type CellData = TimestampCellData; @@ -18,7 +41,42 @@ impl TypeOption for TimestampTypeOption { type CellFilter = DateFilterPB; } -impl CellDataProtobufEncoder for TimestampTypeOption { +impl From<TypeOptionData> for TimestampTypeOption { + fn from(data: TypeOptionData) -> Self { + let date_format = data + .get_i64_value("date_format") + .map(DateFormat::from) + .unwrap_or_default(); + let time_format = data + .get_i64_value("time_format") + .map(TimeFormat::from) + .unwrap_or_default(); + let include_time = data.get_bool_value("include_time").unwrap_or_default(); + let field_type = data + .get_i64_value("field_type") + .map(FieldType::from) + .unwrap_or(FieldType::LastEditedTime); + Self { + date_format, + time_format, + include_time, + field_type, + } + } +} + +impl From<TimestampTypeOption> for TypeOptionData { + fn from(option: TimestampTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("date_format", option.date_format.value()) + .insert_i64_value("time_format", option.time_format.value()) + .insert_bool_value("include_time", option.include_time) + .insert_i64_value("field_type", option.field_type.value()) + .build() + } +} + +impl TypeOptionCellDataSerde for TimestampTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, @@ -31,11 +89,45 @@ impl CellDataProtobufEncoder for TimestampTypeOption { timestamp, } } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(TimestampCellData::from(cell)) + } +} + +impl TimestampTypeOption { + pub fn new(field_type: FieldType) -> Self { + Self { + field_type, + include_time: true, + ..Default::default() + } + } + + fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) { + if let Some(timestamp) = timestamp { + let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap(); + let offset = Local::now().offset().fix(); + let date_time = DateTime::<Local>::from_naive_utc_and_offset(naive, offset); + + let fmt = self.date_format.format_str(); + let date = format!("{}", date_time.format(fmt)); + let fmt = self.time_format.format_str(); + let time = format!("{}", date_time.format(fmt)); + (date, time) + } else { + ("".to_owned(), "".to_owned()) + } + } } impl TypeOptionTransform for TimestampTypeOption {} impl CellDataDecoder for TimestampTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) + } + fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { let timestamp = cell_data.timestamp; let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp); @@ -45,6 +137,10 @@ impl CellDataDecoder for TimestampTypeOption { date_string } } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None + } } impl CellDataChangeset for TimestampTypeOption { @@ -63,12 +159,10 @@ impl CellDataChangeset for TimestampTypeOption { impl TypeOptionCellDataFilter for TimestampTypeOption { fn apply_filter( &self, - filter: &<Self as TypeOption>::CellFilter, - cell_data: &<Self as TypeOption>::CellData, + _filter: &<Self as TypeOption>::CellFilter, + _cell_data: &<Self as TypeOption>::CellData, ) -> bool { - filter - .is_timestamp_cell_data_visible(cell_data) - .unwrap_or(true) + true } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs new file mode 100644 index 0000000000..307b7637b8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs @@ -0,0 +1,69 @@ +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::Serialize; + +use crate::{ + entities::FieldType, + services::field::{TypeOptionCellData, CELL_DATA}, +}; + +#[derive(Clone, Debug, Default, Serialize)] +pub struct TimestampCellData { + pub timestamp: Option<i64>, +} + +impl TimestampCellData { + pub fn new(timestamp: i64) -> Self { + Self { + timestamp: Some(timestamp), + } + } +} + +impl From<&Cell> for TimestampCellData { + fn from(cell: &Cell) -> Self { + let timestamp = cell + .get_str_value(CELL_DATA) + .and_then(|data| data.parse::<i64>().ok()); + Self { timestamp } + } +} + +/// Wrapper for DateCellData that also contains the field type. +/// Handy struct to use when you need to convert a DateCellData to a Cell. +pub struct TimestampCellDataWrapper { + data: TimestampCellData, + field_type: FieldType, +} + +impl From<(FieldType, TimestampCellData)> for TimestampCellDataWrapper { + fn from((field_type, data): (FieldType, TimestampCellData)) -> Self { + Self { data, field_type } + } +} + +impl From<TimestampCellDataWrapper> for Cell { + fn from(wrapper: TimestampCellDataWrapper) -> Self { + let (field_type, data) = (wrapper.field_type, wrapper.data); + let timestamp_string = data.timestamp.unwrap_or_default(); + + new_cell_builder(field_type) + .insert_str_value(CELL_DATA, timestamp_string) + .build() + } +} + +impl From<TimestampCellData> for Cell { + fn from(data: TimestampCellData) -> Self { + let data: TimestampCellDataWrapper = (FieldType::LastEditedTime, data).into(); + Cell::from(data) + } +} + +impl TypeOptionCellData for TimestampCellData {} + +impl ToString for TimestampCellData { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs index d6edf7c9e9..08c163748f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/mod.rs @@ -1 +1,2 @@ pub mod translate; +pub mod translate_entities; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs index ecf9a4de26..5403782387 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate.rs @@ -1,17 +1,71 @@ use crate::entities::TextFilterPB; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::type_options::translate_type_option::translate_entities::TranslateCellData; use crate::services::field::type_options::util::ProtobufStr; use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, - TypeOptionTransform, + TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; -use collab_database::fields::translate_type_option::TranslateTypeOption; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; -use collab_database::template::translate_parse::TranslateCellData; use flowy_error::FlowyResult; use std::cmp::Ordering; +#[derive(Debug, Clone)] +pub struct TranslateTypeOption { + pub auto_fill: bool, + /// Use [TranslateTypeOption::language_from_type] to get the language name + pub language_type: i64, +} + +impl TranslateTypeOption { + pub fn language_from_type(language_type: i64) -> &'static str { + match language_type { + 0 => "Traditional Chinese", + 1 => "English", + 2 => "French", + 3 => "German", + 4 => "Hindi", + 5 => "Spanish", + 6 => "Portuguese", + 7 => "Standard Arabic", + 8 => "Simplified Chinese", + _ => "English", + } + } +} + +impl Default for TranslateTypeOption { + fn default() -> Self { + Self { + auto_fill: false, + language_type: 1, + } + } +} + +impl From<TypeOptionData> for TranslateTypeOption { + fn from(value: TypeOptionData) -> Self { + let auto_fill = value.get_bool_value("auto_fill").unwrap_or_default(); + let language = value.get_i64_value("language").unwrap_or_default(); + Self { + auto_fill, + language_type: language, + } + } +} + +impl From<TranslateTypeOption> for TypeOptionData { + fn from(value: TranslateTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_bool_value("auto_fill", value.auto_fill) + .insert_i64_value("language", value.language_type) + .build() + } +} + impl TypeOption for TranslateTypeOption { type CellData = TranslateCellData; type CellChangeset = String; @@ -47,7 +101,7 @@ impl TypeOptionCellDataCompare for TranslateTypeOption { other_cell_data: &<Self as TypeOption>::CellData, sort_condition: SortCondition, ) -> Ordering { - match (cell_data.is_empty(), other_cell_data.is_empty()) { + match (cell_data.is_cell_empty(), other_cell_data.is_cell_empty()) { (true, true) => Ordering::Equal, (true, false) => Ordering::Greater, (false, true) => Ordering::Less, @@ -60,17 +114,29 @@ impl TypeOptionCellDataCompare for TranslateTypeOption { } impl CellDataDecoder for TranslateTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<TranslateCellData> { + Ok(TranslateCellData::from(cell)) + } + fn stringify_cell_data(&self, cell_data: TranslateCellData) -> String { cell_data.to_string() } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None + } } impl TypeOptionTransform for TranslateTypeOption {} -impl CellDataProtobufEncoder for TranslateTypeOption { +impl TypeOptionCellDataSerde for TranslateTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { ProtobufStr::from(cell_data.0) } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(TranslateCellData::from(cell)) + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs new file mode 100644 index 0000000000..b52b746ab5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/translate_type_option/translate_entities.rs @@ -0,0 +1,46 @@ +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +#[derive(Default, Debug, Clone)] +pub struct TranslateCellData(pub String); +impl std::ops::Deref for TranslateCellData { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TypeOptionCellData for TranslateCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From<&Cell> for TranslateCellData { + fn from(cell: &Cell) -> Self { + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) + } +} + +impl From<TranslateCellData> for Cell { + fn from(data: TranslateCellData) -> Self { + new_cell_builder(FieldType::Translate) + .insert_str_value(CELL_DATA, data.0) + .build() + } +} + +impl ToString for TranslateCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl AsRef<str> for TranslateCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index 86a187f3b6..a8b9d13b7e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -1,36 +1,31 @@ +use std::cmp::Ordering; +use std::fmt::Debug; + +use bytes::Bytes; +use collab_database::fields::TypeOptionData; +use collab_database::rows::Cell; +use protobuf::ProtobufError; + +use flowy_error::FlowyResult; + use crate::entities::{ - CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MediaTypeOptionPB, + CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB, TranslateTypeOptionPB, URLTypeOptionPB, }; use crate::services::cell::CellDataDecoder; +use crate::services::field::checklist_type_option::ChecklistTypeOption; +use crate::services::field::summary_type_option::summary::SummarizationTypeOption; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; +use crate::services::field::{ + CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, + RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, TimestampTypeOption, URLTypeOption, +}; use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; use crate::services::sort::SortCondition; -use async_trait::async_trait; -use bytes::Bytes; -use collab_database::database::Database; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::date_type_option::{DateTypeOption, TimeTypeOption}; -use collab_database::fields::media_type_option::MediaTypeOption; -use collab_database::fields::number_type_option::NumberTypeOption; -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::text_type_option::RichTextTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::fields::translate_type_option::TranslateTypeOption; -use collab_database::fields::url_type_option::URLTypeOption; -use collab_database::fields::{TypeOptionCellReader, TypeOptionData}; -use collab_database::rows::Cell; -use collab_database::template::util::ToCellString; -pub use collab_database::template::util::TypeOptionCellData; -use protobuf::ProtobufError; -use std::cmp::Ordering; -use std::fmt::Debug; -pub trait TypeOption: From<TypeOptionData> + Into<TypeOptionData> + TypeOptionCellReader { +pub trait TypeOption: From<TypeOptionData> + Into<TypeOptionData> { /// `CellData` represents the decoded model for the current type option. Each of them must /// implement the From<&Cell> trait. If the `Cell` cannot be decoded into this type, the default /// value will be returned. @@ -43,7 +38,7 @@ pub trait TypeOption: From<TypeOptionData> + Into<TypeOptionData> + TypeOptionCe /// type CellData: for<'a> From<&'a Cell> + TypeOptionCellData - + ToCellString + + ToString + Default + Send + Sync @@ -72,7 +67,7 @@ pub trait TypeOption: From<TypeOptionData> + Into<TypeOptionData> + TypeOptionCe /// /// This trait ensures that a type which implements both `TypeOption` and `TypeOptionCellDataSerde` can /// be converted to and from a corresponding `Protobuf struct`, and can be parsed from an opaque [Cell] structure. -pub trait CellDataProtobufEncoder: TypeOption { +pub trait TypeOptionCellDataSerde: TypeOption { /// Encode the cell data into corresponding `Protobuf struct`. /// For example: /// FieldType::URL => URLCellDataPB @@ -81,10 +76,23 @@ pub trait CellDataProtobufEncoder: TypeOption { &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType; + + /// Parse the opaque [Cell] to corresponding data struct. + /// The [Cell] is a map that stores list of key/value data. Each [TypeOption::CellData] + /// should implement the From<&Cell> trait to parse the [Cell] to corresponding data struct. + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData>; } -#[async_trait] -pub trait TypeOptionTransform: TypeOption + Send + Sync { +/// This trait that provides methods to extend the [TypeOption::CellData] functionalities. +pub trait TypeOptionCellData { + /// Checks if the cell content is considered empty based on certain criteria. e.g. empty text, + /// no date selected, no selected options + fn is_cell_empty(&self) -> bool { + false + } +} + +pub trait TypeOptionTransform: TypeOption { /// Transform the TypeOption from one field type to another /// For example, when switching from `Checkbox` type option to `Single-Select` /// type option, adding the `Yes` option if the `Single-select` type-option doesn't contain it. @@ -96,14 +104,10 @@ pub trait TypeOptionTransform: TypeOption + Send + Sync { /// * `old_type_option_field_type`: the FieldType of the passed-in TypeOption /// * `old_type_option_data`: the data that can be parsed into corresponding `TypeOption`. /// - async fn transform_type_option( + fn transform_type_option( &mut self, - _view_id: &str, - _field_id: &str, _old_type_option_field_type: FieldType, _old_type_option_data: TypeOptionData, - _new_type_option_field_type: FieldType, - _database: &mut Database, ) { } } @@ -187,9 +191,6 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>( FieldType::Translate => { TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into()) }, - FieldType::Media => { - MediaTypeOptionPB::try_from(bytes).map(|pb| MediaTypeOption::from(pb).into()) - }, } } @@ -219,13 +220,13 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> }, FieldType::SingleSelect => { let single_select_type_option: SingleSelectTypeOption = type_option.into(); - SingleSelectTypeOptionPB::from(single_select_type_option.0) + SingleSelectTypeOptionPB::from(single_select_type_option) .try_into() .unwrap() }, FieldType::MultiSelect => { let multi_select_type_option: MultiSelectTypeOption = type_option.into(); - MultiSelectTypeOptionPB::from(multi_select_type_option.0) + MultiSelectTypeOptionPB::from(multi_select_type_option) .try_into() .unwrap() }, @@ -267,34 +268,27 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> .try_into() .unwrap() }, - FieldType::Media => { - let media_type_option: MediaTypeOption = type_option.into(); - MediaTypeOptionPB::from(media_type_option) - .try_into() - .unwrap() - }, } } pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionData { match field_type { - FieldType::RichText => RichTextTypeOption.into(), + FieldType::RichText => RichTextTypeOption::default().into(), FieldType::Number => NumberTypeOption::default().into(), FieldType::DateTime => DateTypeOption::default().into(), FieldType::LastEditedTime | FieldType::CreatedTime => TimestampTypeOption { - field_type: field_type.into(), + field_type, ..Default::default() } .into(), FieldType::SingleSelect => SingleSelectTypeOption::default().into(), FieldType::MultiSelect => MultiSelectTypeOption::default().into(), - FieldType::Checkbox => CheckboxTypeOption.into(), + FieldType::Checkbox => CheckboxTypeOption::default().into(), FieldType::URL => URLTypeOption::default().into(), FieldType::Checklist => ChecklistTypeOption.into(), FieldType::Relation => RelationTypeOption::default().into(), FieldType::Summary => SummarizationTypeOption::default().into(), FieldType::Translate => TranslateTypeOption::default().into(), FieldType::Time => TimeTypeOption.into(), - FieldType::Media => MediaTypeOption::default().into(), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 27b2f05f9a..415f694164 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -1,32 +1,25 @@ -use crate::entities::FieldType; -use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob}; -use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, - TypeOptionCellDataFilter, TypeOptionTransform, -}; -use crate::services::sort::SortCondition; -use collab::preclude::Any; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::date_type_option::{DateTypeOption, TimeTypeOption}; -use collab_database::fields::media_type_option::MediaTypeOption; -use collab_database::fields::number_type_option::NumberTypeOption; -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::text_type_option::RichTextTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::fields::translate_type_option::TranslateTypeOption; -use collab_database::fields::url_type_option::URLTypeOption; -use collab_database::fields::Field; -use collab_database::rows::{get_field_type_from_cell, Cell, RowId}; -use flowy_error::FlowyResult; -use lib_infra::box_any::BoxAny; use std::cmp::Ordering; use std::collections::hash_map::DefaultHasher; -use std::collections::HashMap; use std::hash::{Hash, Hasher}; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{get_field_type_from_cell, Cell, RowId}; + +use flowy_error::FlowyResult; +use lib_infra::box_any::BoxAny; + +use crate::entities::FieldType; +use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob}; +use crate::services::field::summary_type_option::summary::SummarizationTypeOption; +use crate::services::field::translate_type_option::translate::TranslateTypeOption; +use crate::services::field::{ + CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, + RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, + TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption, +}; +use crate::services::sort::SortCondition; + pub const CELL_DATA: &str = "data"; /// Each [FieldType] has its own [TypeOptionCellDataHandler]. @@ -95,49 +88,23 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static { fn handle_numeric_cell(&self, cell: &Cell) -> Option<f64>; - fn handle_is_empty(&self, cell: &Cell, field: &Field) -> bool; + fn handle_is_cell_empty(&self, cell: &Cell, field: &Field) -> bool; } -#[derive(Debug)] struct CellDataCacheKey(u64); impl CellDataCacheKey { pub fn new(field_rev: &Field, decoded_field_type: FieldType, cell: &Cell) -> Self { let mut hasher = DefaultHasher::new(); if let Some(type_option_data) = field_rev.get_any_type_option(decoded_field_type) { - map_hash(&type_option_data, &mut hasher); + type_option_data.hash(&mut hasher); } hasher.write(field_rev.id.as_bytes()); hasher.write_u8(decoded_field_type as u8); - map_hash(cell, &mut hasher); + cell.hash(&mut hasher); Self(hasher.finish()) } } -fn any_hash<H: Hasher>(any: &Any, hasher: &mut H) { - //FIXME: this is very bad idea for hash calculation - match any { - Any::Null | Any::Undefined => hasher.write_u8(0), - Any::Bool(v) => v.hash(hasher), - Any::Number(v) => v.to_be_bytes().hash(hasher), - Any::BigInt(v) => v.hash(hasher), - Any::String(v) => v.hash(hasher), - Any::Buffer(v) => v.hash(hasher), - Any::Array(v) => { - for v in v.iter() { - any_hash(v, hasher); - } - }, - Any::Map(v) => map_hash(v, hasher), - } -} - -fn map_hash<H: Hasher>(map: &HashMap<String, Any>, hasher: &mut H) { - for (k, v) in map.iter() { - k.hash(hasher); - any_hash(v, hasher); - } -} - impl AsRef<u64> for CellDataCacheKey { fn as_ref(&self) -> &u64 { &self.0 @@ -155,7 +122,7 @@ where T: TypeOption + CellDataDecoder + CellDataChangeset - + CellDataProtobufEncoder + + TypeOptionCellDataSerde + TypeOptionTransform + TypeOptionCellDataFilter + TypeOptionCellDataCompare @@ -191,22 +158,23 @@ where fn get_cell_data_from_cache(&self, cell: &Cell, field: &Field) -> Option<T::CellData> { let key = self.get_cell_data_cache_key(cell, field); - let cell_data_cache = self.cell_data_cache.as_ref()?; - let cell = cell_data_cache.get::<T::CellData>(key.as_ref())?; - Some(cell.value().clone()) + + let cell_data_cache = self.cell_data_cache.as_ref()?.read(); + + cell_data_cache.get(key.as_ref()).cloned() } fn set_cell_data_in_cache(&self, cell: &Cell, cell_data: T::CellData, field: &Field) { if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { let field_type = FieldType::from(field.field_type); let key = CellDataCacheKey::new(field, field_type, cell); - // tracing::trace!( - // "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}", - // field_type, - // cell, - // cell_data - // ); - cell_data_cache.insert(key.as_ref(), cell_data); + tracing::trace!( + "Cell cache update: field_type:{}, cell: {:?}, cell_data: {:?}", + field_type, + cell, + cell_data + ); + cell_data_cache.write().insert(key.as_ref(), cell_data); } } @@ -251,7 +219,7 @@ where T: TypeOption + CellDataDecoder + CellDataChangeset - + CellDataProtobufEncoder + + TypeOptionCellDataSerde + TypeOptionTransform + TypeOptionCellDataFilter + TypeOptionCellDataCompare @@ -270,6 +238,7 @@ where field_rev: &Field, ) -> FlowyResult<CellProtobufBlob> { let cell_data = self.get_cell_data(cell, field_rev).unwrap_or_default(); + CellProtobufBlob::from(self.protobuf_encode(cell_data)) } @@ -344,7 +313,7 @@ where self.numeric_cell(cell) } - fn handle_is_empty(&self, cell: &Cell, field: &Field) -> bool { + fn handle_is_cell_empty(&self, cell: &Cell, field: &Field) -> bool { let cell_data = self.get_cell_data(cell, field).unwrap_or_default(); cell_data.is_cell_empty() @@ -501,16 +470,6 @@ impl<'a> TypeOptionCellExt<'a> { self.cell_data_cache.clone(), ) }), - FieldType::Media => self - .field - .get_type_option::<MediaTypeOption>(field_type) - .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - field_type, - self.cell_data_cache.clone(), - ) - }), } } @@ -520,29 +479,109 @@ impl<'a> TypeOptionCellExt<'a> { } } -/// when return true, the to_field_type must implement [CellDataDecoder]'s decode_cell_with_transform pub fn is_type_option_cell_transformable( from_field_type: FieldType, to_field_type: FieldType, ) -> bool { matches!( (from_field_type, to_field_type), - // Checkbox (FieldType::Checkbox, FieldType::SingleSelect) | (FieldType::Checkbox, FieldType::MultiSelect) - // SingleSelect or MultiSelect | (FieldType::SingleSelect, FieldType::MultiSelect) | (FieldType::MultiSelect, FieldType::SingleSelect) - // Text - | (FieldType::RichText, FieldType::SingleSelect) - | (FieldType::RichText, FieldType::MultiSelect) - | (FieldType::RichText, FieldType::URL) - | (FieldType::RichText, FieldType::Number) - | (FieldType::RichText, FieldType::DateTime) | (_, FieldType::RichText) ) } +pub fn transform_type_option( + old_field_type: FieldType, + new_field_type: FieldType, + old_type_option_data: Option<TypeOptionData>, + new_type_option_data: TypeOptionData, +) -> TypeOptionData { + if let Some(old_type_option_data) = old_type_option_data { + let mut transform_handler = + get_type_option_transform_handler(new_type_option_data, new_field_type); + transform_handler.transform(old_field_type, old_type_option_data); + transform_handler.to_type_option_data() + } else { + new_type_option_data + } +} + +/// A helper trait that used to erase the `Self` of `TypeOption` trait to make it become a Object-safe trait. +pub trait TypeOptionTransformHandler { + fn transform( + &mut self, + old_type_option_field_type: FieldType, + old_type_option_data: TypeOptionData, + ); + + fn to_type_option_data(&self) -> TypeOptionData; +} + +impl<T> TypeOptionTransformHandler for T +where + T: TypeOptionTransform + Clone, +{ + fn transform( + &mut self, + old_type_option_field_type: FieldType, + old_type_option_data: TypeOptionData, + ) { + self.transform_type_option(old_type_option_field_type, old_type_option_data) + } + + fn to_type_option_data(&self) -> TypeOptionData { + self.clone().into() + } +} + +fn get_type_option_transform_handler( + type_option_data: TypeOptionData, + field_type: FieldType, +) -> Box<dyn TypeOptionTransformHandler> { + match field_type { + FieldType::RichText => { + Box::new(RichTextTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Number => { + Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::DateTime => { + Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + Box::new(TimestampTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data)) + as Box<dyn TypeOptionTransformHandler>, + FieldType::MultiSelect => { + Box::new(MultiSelectTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Checkbox => { + Box::new(CheckboxTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::URL => { + Box::new(URLTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Checklist => { + Box::new(ChecklistTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Relation => { + Box::new(RelationTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)) + as Box<dyn TypeOptionTransformHandler>, + FieldType::Time => { + Box::new(TimeTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + FieldType::Translate => { + Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler> + }, + } +} + pub type BoxCellData = BoxAny; pub struct RowSingleCellData { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs index 9c54ed2f0d..7f32aa4c18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_tests.rs @@ -5,7 +5,7 @@ mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataChangeset; use crate::services::field::FieldBuilder; - use collab_database::fields::url_type_option::URLTypeOption; + use crate::services::field::URLTypeOption; #[test] fn url_test() { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs index 907837c0fc..3a95c6bae0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs @@ -1,19 +1,24 @@ -use crate::entities::{FieldType, TextFilterPB, URLCellDataPB}; -use crate::services::cell::{CellDataChangeset, CellDataDecoder}; -use crate::services::field::{ - CellDataProtobufEncoder, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, - TypeOptionTransform, -}; -use crate::services::sort::SortCondition; -use async_trait::async_trait; -use collab_database::database::Database; -use collab_database::fields::url_type_option::{URLCellData, URLTypeOption}; -use collab_database::fields::{Field, TypeOptionData}; +use std::cmp::Ordering; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; use collab_database::rows::Cell; use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use tracing::trace; +use crate::entities::{TextFilterPB, URLCellDataPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, + TypeOptionTransform, URLCellData, +}; +use crate::services::sort::SortCondition; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct URLTypeOption { + pub url: String, + pub content: String, +} impl TypeOption for URLTypeOption { type CellData = URLCellData; @@ -22,72 +27,50 @@ impl TypeOption for URLTypeOption { type CellFilter = TextFilterPB; } -#[async_trait] -impl TypeOptionTransform for URLTypeOption { - async fn transform_type_option( - &mut self, - view_id: &str, - field_id: &str, - old_type_option_field_type: FieldType, - _old_type_option_data: TypeOptionData, - _new_type_option_field_type: FieldType, - database: &mut Database, - ) { - match old_type_option_field_type { - FieldType::RichText => { - let rows = database - .get_cells_for_field(view_id, field_id) - .await - .into_iter() - .filter_map(|row| row.cell.map(|cell| (row.row_id, cell))) - .collect::<Vec<_>>(); - - trace!( - "Transforming RichText to URLTypeOption, updating {} row's cell content", - rows.len() - ); - for (row_id, cell_data) in rows { - database - .update_row(row_id, |row| { - row.update_cells(|cell| { - cell.insert(field_id, Self::CellData::from(&cell_data)); - }); - }) - .await; - } - }, - _ => { - // Do nothing - }, - } +impl From<TypeOptionData> for URLTypeOption { + fn from(data: TypeOptionData) -> Self { + let url = data.get_str_value("url").unwrap_or_default(); + let content = data.get_str_value("content").unwrap_or_default(); + Self { url, content } } } -impl CellDataProtobufEncoder for URLTypeOption { +impl From<URLTypeOption> for TypeOptionData { + fn from(data: URLTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_str_value("url", data.url) + .insert_str_value("content", data.content) + .build() + } +} + +impl TypeOptionTransform for URLTypeOption {} + +impl TypeOptionCellDataSerde for URLTypeOption { fn protobuf_encode( &self, cell_data: <Self as TypeOption>::CellData, ) -> <Self as TypeOption>::CellProtobufType { cell_data.into() } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + Ok(URLCellData::from(cell)) + } } impl CellDataDecoder for URLTypeOption { - fn decode_cell_with_transform( - &self, - cell: &Cell, - from_field_type: FieldType, - _field: &Field, - ) -> Option<<Self as TypeOption>::CellData> { - match from_field_type { - FieldType::RichText => Some(Self::CellData::from(cell)), - _ => None, - } + fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> { + self.parse_cell(cell) } fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String { cell_data.data } + + fn numeric_cell(&self, _cell: &Cell) -> Option<f64> { + None + } } pub type URLCellChangeset = String; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs index b9d1a07823..2b286e0604 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -1,11 +1,51 @@ use bytes::Bytes; - -use collab_database::fields::url_type_option::URLCellData; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::{Deserialize, Serialize}; use flowy_error::{internal_error, FlowyResult}; -use crate::entities::URLCellDataPB; +use crate::entities::{FieldType, URLCellDataPB}; use crate::services::cell::CellProtobufBlobParser; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct URLCellData { + pub data: String, +} + +impl URLCellData { + pub fn new(s: &str) -> Self { + Self { + data: s.to_string(), + } + } + + pub fn to_json(&self) -> FlowyResult<String> { + serde_json::to_string(self).map_err(internal_error) + } +} + +impl TypeOptionCellData for URLCellData { + fn is_cell_empty(&self) -> bool { + self.data.is_empty() + } +} + +impl From<&Cell> for URLCellData { + fn from(cell: &Cell) -> Self { + let data = cell.get_str_value(CELL_DATA).unwrap_or_default(); + Self { data } + } +} + +impl From<URLCellData> for Cell { + fn from(data: URLCellData) -> Self { + new_cell_builder(FieldType::URL) + .insert_str_value(CELL_DATA, data.data) + .build() + } +} impl From<URLCellData> for URLCellDataPB { fn from(data: URLCellData) -> Self { @@ -19,6 +59,12 @@ impl From<URLCellDataPB> for URLCellData { } } +impl AsRef<str> for URLCellData { + fn as_ref(&self) -> &str { + &self.data + } +} + pub struct URLCellDataParser(); impl CellProtobufBlobParser for URLCellDataParser { type Object = URLCellDataPB; @@ -27,3 +73,9 @@ impl CellProtobufBlobParser for URLCellDataParser { URLCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) } } + +impl ToString for URLCellData { + fn to_string(&self) -> String { + self.to_json().unwrap() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs index ec06f84e61..6bf03b127a 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/util.rs @@ -1,6 +1,5 @@ use bytes::Bytes; use protobuf::ProtobufError; -use std::fmt::Display; #[derive(Default, Debug, Clone)] pub struct ProtobufStr(pub String); @@ -24,9 +23,9 @@ impl std::convert::From<String> for ProtobufStr { } } -impl Display for ProtobufStr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.clone()) +impl ToString for ProtobufStr { + fn to_string(&self) -> String { + self.0.clone() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs index 65d58441bc..9f9e82311f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/entities.rs @@ -1,5 +1,4 @@ -use collab::preclude::Any; -use collab::util::AnyMapExt; +use collab::core::any_map::AnyMapExtension; use collab_database::views::{DatabaseLayout, FieldSettingsMap, FieldSettingsMapBuilder}; use crate::entities::FieldVisibility; @@ -26,11 +25,16 @@ impl FieldSettings { field_settings: &FieldSettingsMap, ) -> Self { let visibility = field_settings - .get_as::<i64>(VISIBILITY) + .get_i64_value(VISIBILITY) .map(Into::into) .unwrap_or_else(|| default_field_visibility(layout_type)); - let width = field_settings.get_as::<i32>(WIDTH).unwrap_or(DEFAULT_WIDTH); - let wrap_cell_content: bool = field_settings.get_as(WRAP_CELL_CONTENT).unwrap_or(true); + let width = field_settings + .get_i64_value(WIDTH) + .map(|value| value as i32) + .unwrap_or(DEFAULT_WIDTH); + let wrap_cell_content = field_settings + .get_bool_value(WRAP_CELL_CONTENT) + .unwrap_or(true); Self { field_id: field_id.to_string(), @@ -43,16 +47,10 @@ impl FieldSettings { impl From<FieldSettings> for FieldSettingsMap { fn from(field_settings: FieldSettings) -> Self { - FieldSettingsMapBuilder::from([ - ( - VISIBILITY.into(), - Any::BigInt(i64::from(field_settings.visibility)), - ), - (WIDTH.into(), Any::BigInt(field_settings.width as i64)), - ( - WRAP_CELL_CONTENT.into(), - Any::Bool(field_settings.wrap_cell_content), - ), - ]) + FieldSettingsMapBuilder::new() + .insert_i64_value(VISIBILITY, field_settings.visibility.into()) + .insert_i64_value(WIDTH, field_settings.width as i64) + .insert_bool_value(WRAP_CELL_CONTENT, field_settings.wrap_cell_content) + .build() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs index 95d70184c2..7602224acd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field_settings/field_settings_builder.rs @@ -1,9 +1,9 @@ -use collab::preclude::Any; +use std::collections::HashMap; + use collab_database::fields::Field; use collab_database::views::{ DatabaseLayout, FieldSettingsByFieldIdMap, FieldSettingsMap, FieldSettingsMapBuilder, }; -use std::collections::HashMap; use strum::IntoEnumIterator; use crate::entities::FieldVisibility; @@ -86,8 +86,9 @@ pub fn default_field_settings_by_layout_map() -> HashMap<DatabaseLayout, FieldSe let mut map = HashMap::new(); for layout_ty in DatabaseLayout::iter() { let visibility = default_field_visibility(layout_ty); - let field_settings = - FieldSettingsMapBuilder::from([(VISIBILITY.into(), Any::BigInt(i64::from(visibility)))]); + let field_settings = FieldSettingsMapBuilder::new() + .insert_i64_value(VISIBILITY, visibility.into()) + .build(); map.insert(layout_ty, field_settings); } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 0578298365..975faa0995 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -1,21 +1,17 @@ -use async_trait::async_trait; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use collab::lock::RwLock; use collab_database::database::gen_database_filter_id; use collab_database::fields::Field; use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; -use collab_database::template::timestamp_parse::TimestampCellData; use dashmap::DashMap; -use flowy_error::FlowyResult; -use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; -use rayon::prelude::*; - use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock as TokioRwLock; -use tracing::{error, trace}; +use tokio::sync::RwLock; + +use flowy_error::FlowyResult; +use lib_infra::future::Fut; +use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use crate::entities::filter_entities::*; use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; @@ -24,18 +20,17 @@ use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNot use crate::services::field::TypeOptionCellExt; use crate::services::filter::{Filter, FilterChangeset, FilterInner, FilterResultNotification}; -#[async_trait] pub trait FilterDelegate: Send + Sync + 'static { - async fn get_field(&self, field_id: &str) -> Option<Field>; - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field>; - async fn get_rows(&self, view_id: &str) -> Vec<Arc<Row>>; - async fn get_row(&self, view_id: &str, rows_id: &RowId) -> Option<(usize, Arc<RowDetail>)>; - async fn get_all_filters(&self, view_id: &str) -> Vec<Filter>; - async fn save_filters(&self, view_id: &str, filters: &[Filter]); + fn get_field(&self, field_id: &str) -> Option<Field>; + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>>; + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>; + fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut<Option<(usize, Arc<RowDetail>)>>; + fn get_all_filters(&self, view_id: &str) -> Vec<Filter>; + fn save_filters(&self, view_id: &str, filters: &[Filter]); } pub trait PreFillCellsWithFilter { - fn get_compliant_cell(&self, field: &Field) -> Option<Cell>; + fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool); } pub struct FilterController { @@ -45,7 +40,7 @@ pub struct FilterController { result_by_row_id: DashMap<RowId, bool>, cell_cache: CellCache, filters: RwLock<Vec<Filter>>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, notifier: DatabaseViewChangedNotifier, } @@ -60,7 +55,7 @@ impl FilterController { view_id: &str, handler_id: &str, delegate: T, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, cell_cache: CellCache, notifier: DatabaseViewChangedNotifier, ) -> Self @@ -77,14 +72,15 @@ impl FilterController { let mut need_save = false; - let mut filters = delegate.get_all_filters(view_id).await; - trace!("[Database]: filters: {:?}", filters); + let mut filters = delegate.get_all_filters(view_id); let mut filtering_field_ids: HashMap<String, Vec<String>> = HashMap::new(); + for filter in filters.iter() { filter.get_all_filtering_field_ids(&mut filtering_field_ids); } let mut delete_filter_ids = vec![]; + for (field_id, filter_ids) in &filtering_field_ids { if !field_ids.contains(field_id) { need_save = true; @@ -97,7 +93,7 @@ impl FilterController { } if need_save { - delegate.save_filters(view_id, &filters).await; + delegate.save_filters(view_id, &filters); } Self { @@ -112,17 +108,12 @@ impl FilterController { } } - pub async fn has_filters(&self) -> bool { - !self.filters.read().await.is_empty() - } - pub async fn close(&self) { - self - .task_scheduler - .write() - .await - .unregister_handler(&self.handler_id) - .await; + if let Ok(mut task_scheduler) = self.task_scheduler.try_write() { + task_scheduler.unregister_handler(&self.handler_id).await; + } else { + tracing::error!("Try to get the lock of task_scheduler failed"); + } } #[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))] @@ -131,12 +122,38 @@ impl FilterController { let task = Task::new( &self.handler_id, task_id, - TaskContent::Text(task_type.to_json_string()), + TaskContent::Text(task_type.to_string()), qos, ); self.task_scheduler.write().await.add_task(task); } + pub async fn filter_rows(&self, rows: &mut Vec<Arc<RowDetail>>) { + let filters = self.filters.read().await; + + if filters.is_empty() { + return; + } + let field_by_field_id = self.get_field_map().await; + rows.iter().for_each(|row_detail| { + let _ = filter_row( + &row_detail.row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &filters, + ); + }); + + rows.retain(|row_detail| { + self + .result_by_row_id + .get(&row_detail.row.id) + .map(|result| *result) + .unwrap_or(false) + }); + } + pub async fn did_receive_row_changed(&self, row_id: RowId) { if !self.filters.read().await.is_empty() { self @@ -167,9 +184,8 @@ impl FilterController { .iter_mut() .find_map(|filter| filter.find_filter(&parent_filter_id)) { - if let Err(err) = parent_filter.insert_filter(new_filter) { - error!("error while inserting filter: {}", err); - } + // TODO(RS): error handling for inserting filters + let _result = parent_filter.insert_filter(new_filter); } }, None => { @@ -197,12 +213,13 @@ impl FilterController { .find_map(|filter| filter.find_filter(&filter_id)) { // TODO(RS): error handling for updating filter data - if let Err(error) = filter.update_filter_data(data) { - error!("error while updating filter data: {}", error); - } + let _result = filter.update_filter_data(data); } }, - FilterChangeset::Delete { filter_id } => Self::delete_filter(&mut filters, &filter_id), + FilterChangeset::Delete { + filter_id, + field_id: _, + } => Self::delete_filter(&mut filters, &filter_id), FilterChangeset::DeleteAllWithFieldId { field_id } => { let mut filter_ids = vec![]; for filter in filters.iter() { @@ -214,7 +231,7 @@ impl FilterController { }, } - self.delegate.save_filters(&self.view_id, &filters).await; + self.delegate.save_filters(&self.view_id, &filters); self .gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background) @@ -223,9 +240,11 @@ impl FilterController { FilterChangesetNotificationPB::from_filters(&self.view_id, &filters) } - pub async fn fill_cells(&self, cells: &mut Cells) { + pub async fn fill_cells(&self, cells: &mut Cells) -> bool { let filters = self.filters.read().await; + let mut open_after_create = false; + let mut min_required_filters: Vec<&FilterInner> = vec![]; for filter in filters.iter() { filter.get_min_effective_filters(&mut min_required_filters); @@ -246,11 +265,12 @@ impl FilterController { min_required_filters.retain( |inner| matches!(inner, FilterInner::Data { field_id: other_id, .. } if other_id != field_id), ); + open_after_create = true; continue; } if let Some(field) = field_map.get(field_id) { - let cell = match field_type { + let (cell, flag) = match field_type { FieldType::RichText | FieldType::URL => { let filter = condition_and_content.cloned::<TextFilterPB>().unwrap(); filter.get_compliant_cell(field) @@ -287,15 +307,21 @@ impl FilterController { let filter = condition_and_content.cloned::<TimeFilterPB>().unwrap(); filter.get_compliant_cell(field) }, - _ => None, + _ => (None, false), }; if let Some(cell) = cell { cells.insert(field_id.clone(), cell); } + + if flag { + open_after_create = flag; + } } } } + + open_after_create } #[tracing::instrument( @@ -308,10 +334,7 @@ impl FilterController { pub async fn process(&self, predicate: &str) -> FlowyResult<()> { let event_type = FilterEvent::from_str(predicate).unwrap(); match event_type { - FilterEvent::FilterDidChanged => { - let mut rows = self.delegate.get_rows(&self.view_id).await; - self.filter_rows_and_notify(&mut rows).await? - }, + FilterEvent::FilterDidChanged => self.filter_all_rows_handler().await?, FilterEvent::RowDidChanged(row_id) => self.filter_single_row_handler(row_id).await?, } Ok(()) @@ -323,21 +346,22 @@ impl FilterController { if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await { let field_by_field_id = self.get_field_map().await; let mut notification = FilterResultNotification::new(self.view_id.clone()); - if filter_row( + if let Some(is_visible) = filter_row( &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, &filters, ) { - if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { - notification.visible_rows.push( - InsertedRowPB::new(RowMetaPB::from(row_detail.as_ref().clone())) - .with_index(index as i32), - ) + if is_visible { + if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { + notification.visible_rows.push( + InsertedRowPB::new(RowMetaPB::from(row_detail.as_ref())).with_index(index as i32), + ) + } + } else { + notification.invisible_rows.push(row_id); } - } else { - notification.invisible_rows.push(row_id); } let _ = self @@ -347,35 +371,42 @@ impl FilterController { Ok(()) } - pub async fn filter_rows_and_notify(&self, rows: &mut Vec<Arc<Row>>) -> FlowyResult<()> { + async fn filter_all_rows_handler(&self) -> FlowyResult<()> { let filters = self.filters.read().await; - let field_by_field_id = self.get_field_map().await; - let (visible_rows, invisible_rows): (Vec<_>, Vec<_>) = - rows.par_iter().enumerate().partition_map(|(index, row)| { - if filter_row( - row, - &self.result_by_row_id, - &field_by_field_id, - &self.cell_cache, - &filters, - ) { - let row_meta = RowMetaPB::from(row.as_ref()); - // Visible rows go into the left partition - rayon::iter::Either::Left(InsertedRowPB::new(row_meta).with_index(index as i32)) - } else { - // Invisible rows (just IDs) go into the right partition - rayon::iter::Either::Right(row.id.clone()) - } - }); - let len = rows.len(); - rows.retain(|row| !invisible_rows.iter().any(|id| id == &row.id)); - trace!("[Database]: filter out {} invisible rows", len - rows.len()); + let field_by_field_id = self.get_field_map().await; + let mut visible_rows = vec![]; + let mut invisible_rows = vec![]; + + for (index, row_detail) in self + .delegate + .get_rows(&self.view_id) + .await + .into_iter() + .enumerate() + { + if let Some(is_visible) = filter_row( + &row_detail.row, + &self.result_by_row_id, + &field_by_field_id, + &self.cell_cache, + &filters, + ) { + if is_visible { + let row_meta = RowMetaPB::from(row_detail.as_ref()); + visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) + } else { + invisible_rows.push(row_detail.row.id.clone()); + } + } + } + let notification = FilterResultNotification { view_id: self.view_id.clone(), invisible_rows, visible_rows, }; + tracing::trace!("filter result {:?}", filters); let _ = self .notifier .send(DatabaseViewChanged::FilterNotification(notification)); @@ -383,31 +414,6 @@ impl FilterController { Ok(()) } - pub async fn filter_rows(&self, mut rows: Vec<Arc<Row>>) -> Vec<Arc<Row>> { - let filters = self.filters.read().await; - let field_by_field_id = self.get_field_map().await; - rows.par_iter().for_each(|row| { - let _ = filter_row( - row, - &self.result_by_row_id, - &field_by_field_id, - &self.cell_cache, - &filters, - ); - }); - - let len = rows.len(); - rows.retain(|row| { - self - .result_by_row_id - .get(&row.id) - .map(|result| *result) - .unwrap_or(true) - }); - trace!("[Database]: filter out {} invisible rows", len - rows.len()); - rows - } - async fn get_field_map(&self) -> HashMap<String, Field> { self .delegate @@ -452,14 +458,17 @@ fn filter_row( field_by_field_id: &HashMap<String, Field>, cell_data_cache: &CellCache, filters: &Vec<Filter>, -) -> bool { +) -> Option<bool> { // Create a filter result cache if it doesn't exist let mut filter_result = result_by_row_id.entry(row.id.clone()).or_insert(true); + let old_is_visible = *filter_result; + let mut new_is_visible = true; for filter in filters { if let Some(is_visible) = apply_filter(row, field_by_field_id, cell_data_cache, filter) { new_is_visible = new_is_visible && is_visible; + // short-circuit as soon as one filter tree returns false if !new_is_visible { break; @@ -468,7 +477,12 @@ fn filter_row( } *filter_result = new_is_visible; - new_is_visible + + if old_is_visible != new_is_visible { + Some(new_is_visible) + } else { + None + } } /// Recursively applies a `Filter` to a `Row`'s cells. @@ -514,22 +528,10 @@ fn apply_filter( }, }; if *field_type != FieldType::from(field.field_type) { - error!("field type of filter doesn't match field type of field"); + tracing::error!("field type of filter doesn't match field type of field"); return Some(false); } - let timestamp_cell = match field_type { - FieldType::LastEditedTime | FieldType::CreatedTime => { - let timestamp = if field_type.is_created_time() { - row.created_at - } else { - row.modified_at - }; - let cell = TimestampCellData::new(Some(timestamp)).to_cell(field.field_type); - Some(cell) - }, - _ => None, - }; - let cell = timestamp_cell.or_else(|| row.cells.get(field_id).cloned()); + let cell = row.cells.get(field_id).cloned(); if let Some(handler) = TypeOptionCellExt::new(field, Some(cell_data_cache.clone())) .get_type_option_cell_data_handler() { @@ -547,8 +549,8 @@ enum FilterEvent { RowDidChanged(RowId), } -impl FilterEvent { - fn to_json_string(&self) -> String { +impl ToString for FilterEvent { + fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 52e368596c..718d062fbb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -1,24 +1,20 @@ use std::collections::HashMap; use std::mem; -use std::ops::Deref; use anyhow::bail; -use collab::preclude::Any; -use collab::util::AnyMapExt; +use collab::core::any_map::AnyMapExtension; use collab_database::database::gen_database_filter_id; -use collab_database::fields::select_type_option::SelectOptionIds; use collab_database::rows::RowId; -use collab_database::template::util::ToCellString; use collab_database::views::{FilterMap, FilterMapBuilder}; use flowy_error::{FlowyError, FlowyResult}; use lib_infra::box_any::BoxAny; -use tracing::error; use crate::entities::{ CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType, - InsertedRowPB, MediaFilterPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, - TextFilterPB, TimeFilterPB, + InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB, + TimeFilterPB, }; +use crate::services::field::SelectOptionIds; pub trait ParseFilterData { fn parse(condition: u8, content: String) -> Self; @@ -213,7 +209,7 @@ impl Filter { /// /// 1. a Data filter, then it should be included. /// 2. an AND filter, then all of its effective children should be - /// included. + /// included. /// 3. an OR filter, then only the first child should be included. pub fn get_min_effective_filters<'a>(&'a self, min_effective_filters: &mut Vec<&'a FilterInner>) { match &self.inner { @@ -276,19 +272,11 @@ impl FilterInner { BoxAny::new(TextFilterPB::parse(condition as u8, content)) }, FieldType::Number => BoxAny::new(NumberFilterPB::parse(condition as u8, content)), - FieldType::DateTime => BoxAny::new(DateFilterPB::parse(condition as u8, content)), - FieldType::CreatedTime | FieldType::LastEditedTime => { - let filter = DateFilterPB::parse(condition as u8, content).remove_end_date_conditions(); - BoxAny::new(filter) + FieldType::DateTime | FieldType::CreatedTime | FieldType::LastEditedTime => { + BoxAny::new(DateFilterPB::parse(condition as u8, content)) }, - FieldType::SingleSelect => { - let filter = - SelectOptionFilterPB::parse(condition as u8, content).to_single_select_filter(); - BoxAny::new(filter) - }, - FieldType::MultiSelect => { - let filter = SelectOptionFilterPB::parse(condition as u8, content).to_multi_select_filter(); - BoxAny::new(filter) + FieldType::SingleSelect | FieldType::MultiSelect => { + BoxAny::new(SelectOptionFilterPB::parse(condition as u8, content)) }, FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)), FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)), @@ -296,7 +284,6 @@ impl FilterInner { FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)), FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)), FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)), - FieldType::Media => BoxAny::new(MediaFilterPB::parse(condition as u8, content)), }; FilterInner::Data { @@ -329,20 +316,13 @@ const FILTER_DATA_INDEX: i64 = 2; impl<'a> From<&'a Filter> for FilterMap { fn from(filter: &'a Filter) -> Self { - let mut builder = FilterMapBuilder::from([ - (FILTER_ID.into(), filter.id.as_str().into()), - (FILTER_TYPE.into(), Any::BigInt(filter.inner.get_int_repr())), - ]); + let mut builder = FilterMapBuilder::new() + .insert_str_value(FILTER_ID, &filter.id) + .insert_i64_value(FILTER_TYPE, filter.inner.get_int_repr()); builder = match &filter.inner { FilterInner::And { children } | FilterInner::Or { children } => { - let mut vec = Vec::with_capacity(children.len()); - for child in children.iter() { - let any: Any = FilterMap::from(child).into(); - vec.push(any); - } - builder.insert(FILTER_CHILDREN.into(), Any::from(vec)); - builder + builder.insert_maps(FILTER_CHILDREN, children.iter().collect::<Vec<&Filter>>()) }, FilterInner::Data { field_id, @@ -366,12 +346,12 @@ impl<'a> From<&'a Filter> for FilterMap { end: filter.end, timestamp: filter.timestamp, } - .to_json_string(); + .to_string(); (filter.condition as u8, content) }, FieldType::SingleSelect | FieldType::MultiSelect => { let filter = condition_and_content.cloned::<SelectOptionFilterPB>()?; - let content = SelectOptionIds::from(filter.option_ids).to_cell_string(); + let content = SelectOptionIds::from(filter.option_ids).to_string(); (filter.condition as u8, content) }, FieldType::Checkbox => { @@ -398,10 +378,6 @@ impl<'a> From<&'a Filter> for FilterMap { let filter = condition_and_content.cloned::<TextFilterPB>()?; (filter.condition as u8, filter.content) }, - FieldType::Media => { - let filter = condition_and_content.cloned::<MediaFilterPB>()?; - (filter.condition as u8, filter.content) - }, }; Some((condition, content)) }; @@ -411,15 +387,15 @@ impl<'a> From<&'a Filter> for FilterMap { Default::default() }); - builder.insert(FIELD_ID.into(), field_id.as_str().into()); - builder.insert(FIELD_TYPE.into(), Any::BigInt(i64::from(field_type))); - builder.insert(FILTER_CONDITION.into(), Any::BigInt(condition as i64)); - builder.insert(FILTER_CONTENT.into(), content.into()); builder + .insert_str_value(FIELD_ID, field_id) + .insert_i64_value(FIELD_TYPE, field_type.into()) + .insert_i64_value(FILTER_CONDITION, condition as i64) + .insert_str_value(FILTER_CONTENT, content) }, }; - builder + builder.build() } } @@ -427,30 +403,32 @@ impl TryFrom<FilterMap> for Filter { type Error = anyhow::Error; fn try_from(filter_map: FilterMap) -> Result<Self, Self::Error> { - let filter_id: String = filter_map - .get_as(FILTER_ID) + let filter_id = filter_map + .get_str_value(FILTER_ID) .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; - let filter_type: i64 = filter_map.get_as(FILTER_TYPE).unwrap_or(FILTER_DATA_INDEX); + let filter_type = filter_map + .get_i64_value(FILTER_TYPE) + .unwrap_or(FILTER_DATA_INDEX); let filter = Filter { id: filter_id, inner: match filter_type { FILTER_AND_INDEX => FilterInner::And { - children: get_children(filter_map), + children: filter_map.try_get_array(FILTER_CHILDREN), }, FILTER_OR_INDEX => FilterInner::Or { - children: get_children(filter_map), + children: filter_map.try_get_array(FILTER_CHILDREN), }, FILTER_DATA_INDEX => { - let field_id: String = filter_map - .get_as(FIELD_ID) + let field_id = filter_map + .get_str_value(FIELD_ID) .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; let field_type = filter_map - .get_as::<i64>(FIELD_TYPE) + .get_i64_value(FIELD_TYPE) .map(FieldType::from) .unwrap_or_default(); - let condition: i64 = filter_map.get_as(FILTER_CONDITION).unwrap_or_default(); - let content: String = filter_map.get_as(FILTER_CONTENT).unwrap_or_default(); + let condition = filter_map.get_i64_value(FILTER_CONDITION).unwrap_or(0); + let content = filter_map.get_str_value(FILTER_CONTENT).unwrap_or_default(); FilterInner::new_data(field_id, field_type, condition, content) }, @@ -462,27 +440,6 @@ impl TryFrom<FilterMap> for Filter { } } -fn get_children(filter_map: FilterMap) -> Vec<Filter> { - //TODO: this method wouldn't be necessary if we could make Filters serializable in backward - // compatible way - let mut result = Vec::new(); - if let Some(Any::Array(children)) = filter_map.get(FILTER_CHILDREN) { - for child in children.iter() { - if let Any::Map(child_map) = child { - match Filter::try_from(child_map.deref().clone()) { - Ok(filter) => { - result.push(filter); - }, - Err(err) => { - error!("Failed to deserialize filter: {:?}", err); - }, - } - } - } - } - result -} - #[derive(Debug)] pub enum FilterChangeset { Insert { @@ -499,6 +456,7 @@ pub enum FilterChangeset { }, Delete { filter_id: String, + field_id: String, }, DeleteAllWithFieldId { field_id: String, diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/task.rs b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs index e6c2ae905a..03ed453f89 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/task.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs @@ -1,6 +1,5 @@ use crate::services::filter::FilterController; -use async_trait::async_trait; - +use lib_infra::future::BoxResultFuture; use lib_infra::priority_task::{TaskContent, TaskHandler}; use std::sync::Arc; @@ -18,7 +17,6 @@ impl FilterTaskHandler { } } -#[async_trait] impl TaskHandler for FilterTaskHandler { fn handler_id(&self) -> &str { &self.handler_id @@ -28,14 +26,16 @@ impl TaskHandler for FilterTaskHandler { "FilterTaskHandler" } - async fn run(&self, content: TaskContent) -> Result<(), anyhow::Error> { + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { let filter_controller = self.filter_controller.clone(); - if let TaskContent::Text(predicate) = content { - filter_controller - .process(&predicate) - .await - .map_err(anyhow::Error::from)?; - } - Ok(()) + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + filter_controller + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 7b876e0ddb..b540fb5fa3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,17 +1,16 @@ -use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, Row, RowId}; +use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use flowy_error::FlowyResult; -use crate::entities::{GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; +use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::field::TypeOption; use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext}; /// [GroupCustomize] is implemented by parameterized `BaseGroupController`s to provide different /// behaviors. This allows the BaseGroupController to call these actions indescriminantly using /// polymorphism. -#[async_trait] +/// pub trait GroupCustomize: Send + Sync { type GroupTypeOption: TypeOption; /// Returns the a value of the cell if the cell data is not exist. @@ -33,7 +32,7 @@ pub trait GroupCustomize: Send + Sync { fn create_or_delete_group_when_cell_changed( &mut self, - _row: &Row, + _row_detail: &RowDetail, _old_cell_data: Option<&<Self::GroupTypeOption as TypeOption>::CellProtobufType>, _cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> { @@ -45,7 +44,7 @@ pub trait GroupCustomize: Send + Sync { /// fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB>; @@ -60,7 +59,7 @@ pub trait GroupCustomize: Send + Sync { fn move_row(&mut self, context: MoveGroupRowContext) -> Vec<GroupRowsNotificationPB>; /// Returns None if there is no need to delete the group when corresponding row get removed - fn delete_group_after_moving_row( + fn delete_group_when_move_row( &mut self, _row: &Row, _cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, @@ -68,14 +67,14 @@ pub trait GroupCustomize: Send + Sync { None } - async fn create_group( + fn create_group( &mut self, _name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { Ok((None, None)) } - async fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>; + fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>; fn update_type_option_when_update_group( &mut self, @@ -96,9 +95,8 @@ pub trait GroupCustomize: Send + Sync { /// or a `DefaultGroupController` may be the actual object that provides the functionality of /// this trait. For example, a `Single-Select` group controller will be a `BaseGroupController`, /// while a `URL` group controller will be a `DefaultGroupController`. -#[async_trait] +/// pub trait GroupController: Send + Sync { - async fn load_group_data(&mut self) -> FlowyResult<()>; /// Returns the id of field that is being used to group the rows fn get_grouping_field_id(&self) -> &str; @@ -114,14 +112,14 @@ pub trait GroupController: Send + Sync { /// /// * `rows`: rows to be inserted /// * `field`: reference to the field being sorted (currently unused) - fn fill_groups(&mut self, rows: &[&Row], field: &Field) -> FlowyResult<()>; + fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()>; /// Create a new group, currently only supports single and multi-select. /// /// Returns a new type option data for the grouping field if it's altered. /// /// * `name`: name of the new group - async fn create_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)>; @@ -138,7 +136,11 @@ pub trait GroupController: Send + Sync { /// Returns a changeset payload to be sent as a notification. /// /// * `row_detail`: the newly-created row - fn did_create_row(&mut self, row: &Row, index: usize) -> Vec<GroupRowsNotificationPB>; + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB>; /// Called after a row's cell data is changed, this moves the row to the /// correct group. It may also insert a new group and/or remove an old group. @@ -150,8 +152,8 @@ pub trait GroupController: Send + Sync { /// * `field`: fn did_update_group_row( &mut self, - old_row: &Option<Row>, - new_row: &Row, + old_row_detail: &Option<RowDetail>, + row_detail: &RowDetail, field: &Field, ) -> FlowyResult<DidUpdateGroupRowResult>; @@ -166,16 +168,18 @@ pub trait GroupController: Send + Sync { /// * `context`: information about the row being moved and its destination fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult<DidMoveGroupRowResult>; + /// Updates the groups after a field change. (currently never does anything) + /// + /// * `field`: new changeset + fn did_update_group_field(&mut self, field: &Field) -> FlowyResult<Option<GroupChangesPB>>; + /// Delete a group from the group configuration. /// /// Return a list of deleted row ids and/or a new `TypeOptionData` if /// successful. /// /// * `group_id`: the id of the group to be deleted - async fn delete_group( - &mut self, - group_id: &str, - ) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)>; + fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)>; /// Updates the name and/or visibility of groups. /// @@ -183,7 +187,7 @@ pub trait GroupController: Send + Sync { /// in the field type option data. /// /// * `changesets`: list of changesets to be made to one or more groups - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, changesets: &[GroupChangeset], ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)>; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 95c1048b60..980fee21b2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -1,4 +1,3 @@ -use async_trait::async_trait; use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; @@ -10,6 +9,8 @@ use serde::Serialize; use tracing::event; use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::af_spawn; +use lib_infra::future::Fut; use crate::entities::{GroupChangesPB, GroupPB, InsertedGroupPB}; use crate::services::field::RowSingleCellData; @@ -17,14 +18,12 @@ use crate::services::group::{ default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting, }; -#[async_trait] pub trait GroupContextDelegate: Send + Sync + 'static { - async fn get_group_setting(&self, view_id: &str) -> Option<Arc<GroupSetting>>; + fn get_group_setting(&self, view_id: &str) -> Fut<Option<Arc<GroupSetting>>>; - async fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Vec<RowSingleCellData>; + fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut<Vec<RowSingleCellData>>; - async fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) - -> FlowyResult<()>; + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut<FlowyResult<()>>; } impl<T> std::fmt::Display for GroupControllerContext<T> { @@ -351,7 +350,7 @@ where /// # Arguments /// /// * `mut_configuration_fn`: mutate the [GroupSetting] and return whether the [GroupSetting] is - /// changed. If the [GroupSetting] is changed, the [GroupSetting] will be saved to the storage. + /// changed. If the [GroupSetting] is changed, the [GroupSetting] will be saved to the storage. /// fn mut_configuration( &mut self, @@ -363,7 +362,7 @@ where let configuration = (*self.setting).clone(); let delegate = self.delegate.clone(); let view_id = self.view_id.clone(); - tokio::spawn(async move { + af_spawn(async move { match delegate.save_configuration(&view_id, configuration).await { Ok(_) => {}, Err(e) => { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index eed12e492a..a918e7f7c2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -1,16 +1,18 @@ -use async_trait::async_trait; use std::marker::PhantomData; use std::sync::Arc; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowId}; -use flowy_error::{FlowyError, FlowyResult}; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; +use futures::executor::block_on; +use lib_infra::future::Fut; use serde::de::DeserializeOwned; use serde::Serialize; -use tracing::trace; + +use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{ - FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, + FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, + RowMetaPB, }; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; @@ -21,11 +23,10 @@ use crate::services::group::configuration::GroupControllerContext; use crate::services::group::entities::GroupData; use crate::services::group::{GroupChangeset, GroupsBuilder, MoveGroupRowContext}; -#[async_trait] pub trait GroupControllerDelegate: Send + Sync + 'static { - async fn get_field(&self, field_id: &str) -> Option<Field>; + fn get_field(&self, field_id: &str) -> Option<Field>; - async fn get_all_rows(&self, view_id: &str) -> Vec<Arc<Row>>; + fn get_all_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>; } /// [BaseGroupController] is a generic group controller that provides customized implementations @@ -53,29 +54,37 @@ where { pub async fn new( grouping_field: &Field, - context: GroupControllerContext<C>, + mut configuration: GroupControllerContext<C>, delegate: Arc<dyn GroupControllerDelegate>, ) -> FlowyResult<Self> { + let field_type = FieldType::from(grouping_field.field_type); + let type_option = grouping_field + .get_type_option::<T>(&field_type) + .unwrap_or_else(|| T::from(default_type_option_data_from_type(field_type))); + + // TODO(nathan): remove block_on + let generated_groups = block_on(G::build(grouping_field, &configuration, &type_option)); + let _ = configuration.init_groups(generated_groups)?; + Ok(Self { grouping_field_id: grouping_field.id.clone(), - context, + context: configuration, group_builder_phantom: PhantomData, cell_parser_phantom: PhantomData, delegate, }) } - pub async fn get_grouping_field_type_option(&self) -> Option<T> { + pub fn get_grouping_field_type_option(&self) -> Option<T> { self .delegate .get_field(&self.grouping_field_id) - .await .and_then(|field| field.get_type_option::<T>(FieldType::from(field.field_type))) } fn update_no_status_group( &mut self, - row: &Row, + row_detail: &RowDetail, other_group_changesets: &[GroupRowsNotificationPB], ) -> Option<GroupRowsNotificationPB> { let no_status_group = self.context.get_mut_no_status_group()?; @@ -104,8 +113,8 @@ where if !no_status_group_rows.is_empty() { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row))); - no_status_group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + no_status_group.add_row(row_detail.clone()); } // [other_group_delete_rows] contains all the deleted rows except the default group. @@ -128,8 +137,8 @@ where .collect::<Vec<&InsertedRowPB>>(); let mut deleted_row_ids = vec![]; - for row in &no_status_group.rows { - let row_id = row.id.to_string(); + for row_detail in &no_status_group.rows { + let row_id = row_detail.row.id.to_string(); if default_group_deleted_rows .iter() .any(|deleted_row| deleted_row.row_meta.id == row_id) @@ -139,13 +148,12 @@ where } no_status_group .rows - .retain(|row| !deleted_row_ids.contains(&row.id)); + .retain(|row_detail| !deleted_row_ids.contains(&row_detail.row.id)); changeset.deleted_rows.extend(deleted_row_ids); Some(changeset) } } -#[async_trait] impl<C, T, G, P> GroupController for BaseGroupController<C, G, P> where P: CellProtobufBlobParser<Object = <T as TypeOption>::CellProtobufType>, @@ -154,30 +162,6 @@ where G: GroupsBuilder<Context = GroupControllerContext<C>, GroupTypeOption = T>, Self: GroupCustomize<GroupTypeOption = T>, { - async fn load_group_data(&mut self) -> FlowyResult<()> { - let grouping_field = self - .delegate - .get_field(&self.grouping_field_id) - .await - .ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?; - - let field_type = FieldType::from(grouping_field.field_type); - let type_option = grouping_field - .get_type_option::<T>(&field_type) - .unwrap_or_else(|| T::from(default_type_option_data_from_type(field_type))); - - let generated_groups = G::build(&grouping_field, &self.context, &type_option).await; - let _ = self.context.init_groups(generated_groups)?; - - let row_details = self.delegate.get_all_rows(&self.context.view_id).await; - let rows = row_details - .iter() - .map(|row| row.as_ref()) - .collect::<Vec<_>>(); - self.fill_groups(rows.as_slice(), &grouping_field)?; - Ok(()) - } - fn get_grouping_field_id(&self) -> &str { &self.grouping_field_id } @@ -192,9 +176,9 @@ where } #[tracing::instrument(level = "trace", skip_all, fields(row_count=%rows.len(), group_result))] - fn fill_groups(&mut self, rows: &[&Row], _field: &Field) -> FlowyResult<()> { - for row in rows { - let cell = match row.cells.get(&self.grouping_field_id) { + fn fill_groups(&mut self, rows: &[&RowDetail], _field: &Field) -> FlowyResult<()> { + for row_detail in rows { + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { None => self.placeholder_cell(), Some(cell) => Some(cell.clone()), }; @@ -205,7 +189,7 @@ where for group in self.context.groups() { if self.can_group(&group.id, &cell_data) { grouped_rows.push(GroupedRow { - row: (*row).clone(), + row_detail: (*row_detail).clone(), group_id: group.id.clone(), }); } @@ -214,7 +198,7 @@ where if !grouped_rows.is_empty() { for group_row in grouped_rows { if let Some(group) = self.context.get_mut_group(&group_row.group_id) { - group.add_row(group_row.row); + group.add_row(group_row.row_detail); } } continue; @@ -223,7 +207,7 @@ where match self.context.get_mut_no_status_group() { None => {}, - Some(no_status_group) => no_status_group.add_row((*row).clone()), + Some(no_status_group) => no_status_group.add_row((*row_detail).clone()), } } @@ -231,38 +215,43 @@ where Ok(()) } - async fn create_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { - <Self as GroupCustomize>::create_group(self, name).await + <Self as GroupCustomize>::create_group(self, name) } fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { self.context.move_group(from_group_id, to_group_id) } - fn did_create_row(&mut self, row: &Row, index: usize) -> Vec<GroupRowsNotificationPB> { + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB> { let mut changesets: Vec<GroupRowsNotificationPB> = vec![]; - let cell = match row.cells.get(&self.grouping_field_id) { + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { None => self.placeholder_cell(), Some(cell) => Some(cell.clone()), }; if let Some(cell) = cell { let cell_data = <T as TypeOption>::CellData::from(&cell); + let mut suitable_group_ids = vec![]; + for group in self.get_all_groups() { if self.can_group(&group.id, &cell_data) { suitable_group_ids.push(group.id.clone()); let changeset = GroupRowsNotificationPB::insert( group.id.clone(), vec![InsertedRowPB { - row_meta: row.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, - is_hidden_in_view: false, }], ); changesets.push(changeset); @@ -271,18 +260,17 @@ where if !suitable_group_ids.is_empty() { for group_id in suitable_group_ids.iter() { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row((*row).clone()); + group.add_row((*row_detail).clone()); } } } else if let Some(no_status_group) = self.context.get_mut_no_status_group() { - no_status_group.add_row((*row).clone()); + no_status_group.add_row((*row_detail).clone()); let changeset = GroupRowsNotificationPB::insert( no_status_group.id.clone(), vec![InsertedRowPB { - row_meta: row.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, - is_hidden_in_view: false, }], ); changesets.push(changeset); @@ -294,8 +282,8 @@ where fn did_update_group_row( &mut self, - old_row: &Option<Row>, - new_row: &Row, + old_row_detail: &Option<RowDetail>, + row_detail: &RowDetail, field: &Field, ) -> FlowyResult<DidUpdateGroupRowResult> { let mut result = DidUpdateGroupRowResult { @@ -303,17 +291,20 @@ where deleted_group: None, row_changesets: vec![], }; - if let Some(cell_data) = get_cell_data_from_row::<P>(Some(new_row), field) { - let old_cell_data = get_cell_data_from_row::<P>(old_row.as_ref(), field); - if let Ok((insert, delete)) = - self.create_or_delete_group_when_cell_changed(new_row, old_cell_data.as_ref(), &cell_data) - { + if let Some(cell_data) = get_cell_data_from_row::<P>(Some(&row_detail.row), field) { + let old_cell_data = + get_cell_data_from_row::<P>(old_row_detail.as_ref().map(|detail| &detail.row), field); + if let Ok((insert, delete)) = self.create_or_delete_group_when_cell_changed( + row_detail, + old_cell_data.as_ref(), + &cell_data, + ) { result.inserted_group = insert; result.deleted_group = delete; } - let mut changesets = self.add_or_remove_row_when_cell_changed(new_row, &cell_data); - if let Some(changeset) = self.update_no_status_group(new_row, &changesets) { + let mut changesets = self.add_or_remove_row_when_cell_changed(row_detail, &cell_data); + if let Some(changeset) = self.update_no_status_group(row_detail, &changesets) { if !changeset.is_empty() { changesets.push(changeset); } @@ -325,13 +316,11 @@ where } fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> { - trace!("[RowOrder]: group did_delete_row: {:?}", row.id); let mut result = DidMoveGroupRowResult { deleted_group: None, row_changesets: vec![], }; - - // remove row from its group if it is in a group + // early return if the row is not in the default group if let Some(cell) = row.cells.get(&self.grouping_field_id) { let cell_data = <T as TypeOption>::CellData::from(cell); if !cell_data.is_cell_empty() { @@ -340,7 +329,6 @@ where } } - // when the deleted row is not in current group. It should be in the no status group match self.context.get_mut_no_status_group() { None => { tracing::error!("Unexpected None value. It should have the no status group"); @@ -365,7 +353,7 @@ where deleted_group: None, row_changesets: vec![], }; - let cell = match context.row.cells.get(&self.grouping_field_id) { + let cell = match context.row_detail.row.cells.get(&self.grouping_field_id) { Some(cell) => Some(cell.clone()), None => self.placeholder_cell(), }; @@ -373,7 +361,7 @@ where if let Some(cell) = cell { let cell_bytes = get_cell_protobuf(&cell, context.field, None); let cell_data = cell_bytes.parser::<P>()?; - result.deleted_group = self.delete_group_after_moving_row(context.row, &cell_data); + result.deleted_group = self.delete_group_when_move_row(&context.row_detail.row, &cell_data); result.row_changesets = self.move_row(context); } else { tracing::warn!("Unexpected moving group row, changes should not be empty"); @@ -381,10 +369,11 @@ where Ok(result) } - async fn delete_group( - &mut self, - group_id: &str, - ) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> { + fn did_update_group_field(&mut self, _field: &Field) -> FlowyResult<Option<GroupChangesPB>> { + Ok(None) + } + + fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> { let group = if group_id != self.get_grouping_field_id() { self.get_group(group_id) } else { @@ -393,15 +382,19 @@ where match group { Some((_index, group_data)) => { - let row_ids = group_data.rows.iter().map(|row| row.id.clone()).collect(); - let type_option_data = <Self as GroupCustomize>::delete_group(self, group_id).await?; + let row_ids = group_data + .rows + .iter() + .map(|row| row.row.id.clone()) + .collect(); + let type_option_data = <Self as GroupCustomize>::delete_group(self, group_id)?; Ok((row_ids, type_option_data)) }, None => Ok((vec![], None)), } } - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, changeset: &[GroupChangeset], ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)> { @@ -411,7 +404,7 @@ where } // update group name - let type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { + let type_option = self.get_grouping_field_type_option().ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; @@ -447,7 +440,7 @@ where } struct GroupedRow { - row: Row, + row_detail: RowDetail, group_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index b33d402dca..a3057b24a0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -1,13 +1,14 @@ use async_trait::async_trait; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; -use crate::services::field::{CheckboxCellDataParser, TypeOption, CHECK, UNCHECK}; +use crate::services::field::{ + CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK, +}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupControllerContext; use crate::services::group::controller::BaseGroupController; @@ -24,14 +25,14 @@ pub type CheckboxGroupController = BaseGroupController<CheckboxGroupConfiguration, CheckboxGroupBuilder, CheckboxCellDataParser>; pub type CheckboxGroupControllerContext = GroupControllerContext<CheckboxGroupConfiguration>; - -#[async_trait] impl GroupCustomize for CheckboxGroupController { type GroupTypeOption = CheckboxTypeOption; fn placeholder_cell(&self) -> Option<Cell> { - let mut cell = new_cell_builder(FieldType::Checkbox); - cell.insert("data".into(), UNCHECK.into()); - Some(cell) + Some( + new_cell_builder(FieldType::Checkbox) + .insert_str_value("data", UNCHECK) + .build(), + ) } fn can_group( @@ -48,25 +49,27 @@ impl GroupCustomize for CheckboxGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB> { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); - let is_not_contained = !group.contains_row(&row.id); + let is_not_contained = !group.contains_row(&row_detail.row.id); if group.id == CHECK { if !cell_data.is_checked { // Remove the row if the group.id is CHECK but the cell_data is UNCHECK - changeset.deleted_rows.push(row.id.clone().into_inner()); - group.remove_row(&row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + group.remove_row(&row_detail.row.id); } else { // Add the row to the group if the group didn't contain the row if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + group.add_row(row_detail.clone()); } } } @@ -74,15 +77,17 @@ impl GroupCustomize for CheckboxGroupController { if group.id == UNCHECK { if cell_data.is_checked { // Remove the row if the group.id is UNCHECK but the cell_data is CHECK - changeset.deleted_rows.push(row.id.clone().into_inner()); - group.remove_row(&row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + group.remove_row(&row_detail.row.id); } else { // Add the row to the group if the group didn't contain the row if is_not_contained { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + group.add_row(row_detail.clone()); } } } @@ -124,7 +129,7 @@ impl GroupCustomize for CheckboxGroupController { group_changeset } - async fn delete_group(&mut self, _group_id: &str) -> FlowyResult<Option<TypeOptionData>> { + fn delete_group(&mut self, _group_id: &str) -> FlowyResult<Option<TypeOptionData>> { Ok(None) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index f3d03c6856..1402793264 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -1,19 +1,18 @@ use async_trait::async_trait; -use chrono::{DateTime, Datelike, Days, Duration, Local}; +use chrono::{DateTime, Datelike, Days, Duration, Local, NaiveDateTime}; use collab_database::database::timestamp; -use collab_database::fields::date_type_option::{DateCellData, DateTypeOption}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; -use flowy_error::{internal_error, FlowyResult}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +use flowy_error::{internal_error, FlowyResult}; + use crate::entities::{ FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::insert_date_cell; -use crate::services::field::date_filter::DateCellDataParser; -use crate::services::field::TypeOption; +use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption, TypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupControllerContext; use crate::services::group::controller::BaseGroupController; @@ -54,14 +53,15 @@ pub type DateGroupController = pub type DateGroupControllerContext = GroupControllerContext<DateGroupConfiguration>; -#[async_trait] impl GroupCustomize for DateGroupController { type GroupTypeOption = DateTypeOption; fn placeholder_cell(&self) -> Option<Cell> { - let mut cell = new_cell_builder(FieldType::DateTime); - cell.insert("data".into(), "".into()); - Some(cell) + Some( + new_cell_builder(FieldType::DateTime) + .insert_str_value("data", "") + .build(), + ) } fn can_group( @@ -74,7 +74,7 @@ impl GroupCustomize for DateGroupController { fn create_or_delete_group_when_cell_changed( &mut self, - _row: &Row, + _row_detail: &RowDetail, _old_cell_data: Option<&<Self::GroupTypeOption as TypeOption>::CellProtobufType>, _cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> { @@ -87,7 +87,7 @@ impl GroupCustomize for DateGroupController { { let group = make_group_from_date_cell(&_cell_data.into(), &setting_content); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(_row.clone())); + new_group.group.rows.push(RowMetaPB::from(_row_detail)); inserted_group = Some(new_group); } @@ -120,7 +120,7 @@ impl GroupCustomize for DateGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB> { let mut changesets = vec![]; @@ -128,15 +128,17 @@ impl GroupCustomize for DateGroupController { self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); if group.id == get_date_group_id(&cell_data.into(), &setting_content) { - if !group.contains_row(&row.id) { + if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row.clone()))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + group.add_row(row_detail.clone()); } - } else if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } if !changeset.is_empty() { @@ -191,7 +193,7 @@ impl GroupCustomize for DateGroupController { group_changeset } - fn delete_group_after_moving_row( + fn delete_group_when_move_row( &mut self, _row: &Row, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, @@ -212,7 +214,7 @@ impl GroupCustomize for DateGroupController { deleted_group } - async fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { + fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { self.context.delete_group(group_id)?; Ok(None) } @@ -220,7 +222,7 @@ impl GroupCustomize for DateGroupController { fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { None => tracing::warn!("Can not find the group: {}", group_id), - _ => { + Some((_, _)) => { let date = DateTime::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap(); let cell = insert_date_cell(date.timestamp(), None, Some(false), field); cells.insert(field.id.clone(), cell); @@ -328,9 +330,7 @@ fn get_date_group_id(cell_data: &DateCellData, setting_content: &str) -> String fn date_time_from_timestamp(timestamp: Option<i64>) -> DateTime<Local> { match timestamp { Some(timestamp) => { - let naive = DateTime::from_timestamp(timestamp, 0) - .unwrap_or_default() - .naive_utc(); + let naive = NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(); let offset = *Local::now().offset(); DateTime::<Local>::from_naive_utc_and_offset(naive, offset) @@ -341,11 +341,13 @@ fn date_time_from_timestamp(timestamp: Option<i64>) -> DateTime<Local> { #[cfg(test)] mod tests { + use chrono::{offset, Days, Duration, NaiveDateTime}; + + use crate::services::field::date_type_option::DateTypeOption; + use crate::services::field::DateCellData; use crate::services::group::controller_impls::date_controller::{ get_date_group_id, GROUP_ID_DATE_FORMAT, }; - use chrono::{offset, Days, Duration}; - use collab_database::fields::date_type_option::{DateCellData, DateTypeOption}; #[test] fn group_id_name_test() { @@ -355,11 +357,9 @@ mod tests { exp_group_id: String, } - let mar_14_2022 = chrono::DateTime::from_timestamp(1647251762, 0) - .unwrap() - .naive_utc(); + let mar_14_2022 = NaiveDateTime::from_timestamp_opt(1647251762, 0).unwrap(); let mar_14_2022_cd = DateCellData { - timestamp: Some(mar_14_2022.and_utc().timestamp()), + timestamp: Some(mar_14_2022.timestamp()), include_time: false, ..Default::default() }; @@ -410,7 +410,6 @@ mod tests { mar_14_2022 .checked_add_signed(Duration::days(3)) .unwrap() - .and_utc() .timestamp(), ), include_time: false, diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 0ce7dd036c..bcfd48bc09 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -1,12 +1,13 @@ -use async_trait::async_trait; use std::sync::Arc; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowId}; -use flowy_error::FlowyResult; -use tracing::trace; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; -use crate::entities::{GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB}; +use flowy_error::FlowyResult; + +use crate::entities::{ + GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, +}; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController, }; @@ -19,7 +20,6 @@ use crate::services::group::{ /// means all rows will be grouped in the same group. /// pub struct DefaultGroupController { - pub view_id: String, pub field_id: String, pub group: GroupData, pub delegate: Arc<dyn GroupControllerDelegate>, @@ -28,10 +28,9 @@ pub struct DefaultGroupController { const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController"; impl DefaultGroupController { - pub fn new(view_id: &str, field: &Field, delegate: Arc<dyn GroupControllerDelegate>) -> Self { + pub fn new(field: &Field, delegate: Arc<dyn GroupControllerDelegate>) -> Self { let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true); Self { - view_id: view_id.to_owned(), field_id: field.id.clone(), group, delegate, @@ -39,21 +38,7 @@ impl DefaultGroupController { } } -#[async_trait] impl GroupController for DefaultGroupController { - async fn load_group_data(&mut self) -> FlowyResult<()> { - let row_details = self.delegate.get_all_rows(&self.view_id).await; - let rows = row_details - .iter() - .map(|row| row.as_ref()) - .collect::<Vec<_>>(); - - rows.iter().for_each(|row| { - self.group.add_row((*row).clone()); - }); - Ok(()) - } - fn get_grouping_field_id(&self) -> &str { &self.field_id } @@ -66,14 +51,14 @@ impl GroupController for DefaultGroupController { Some((0, self.group.clone())) } - fn fill_groups(&mut self, rows: &[&Row], _field: &Field) -> FlowyResult<()> { + fn fill_groups(&mut self, rows: &[&RowDetail], _field: &Field) -> FlowyResult<()> { rows.iter().for_each(|row| { self.group.add_row((*row).clone()); }); Ok(()) } - async fn create_group( + fn create_group( &mut self, _name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { @@ -84,24 +69,27 @@ impl GroupController for DefaultGroupController { Ok(()) } - fn did_create_row(&mut self, row: &Row, index: usize) -> Vec<GroupRowsNotificationPB> { - self.group.add_row((*row).clone()); + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec<GroupRowsNotificationPB> { + self.group.add_row((*row_detail).clone()); vec![GroupRowsNotificationPB::insert( self.group.id.clone(), vec![InsertedRowPB { - row_meta: row.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, - is_hidden_in_view: false, }], )] } fn did_update_group_row( &mut self, - _old_row: &Option<Row>, - _new_row: &Row, + _old_row_detail: &Option<RowDetail>, + _row_detail: &RowDetail, _field: &Field, ) -> FlowyResult<DidUpdateGroupRowResult> { Ok(DidUpdateGroupRowResult { @@ -114,11 +102,6 @@ impl GroupController for DefaultGroupController { fn did_delete_row(&mut self, row: &Row) -> FlowyResult<DidMoveGroupRowResult> { let mut changeset = GroupRowsNotificationPB::new(self.group.id.clone()); if self.group.contains_row(&row.id) { - trace!( - "[RowOrder]: delete row:{} from group: {}", - row.id, - self.group.id - ); self.group.remove_row(&row.id); changeset.deleted_rows.push(row.id.clone().into_inner()); } @@ -138,14 +121,15 @@ impl GroupController for DefaultGroupController { }) } - async fn delete_group( - &mut self, - _group_id: &str, - ) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> { + fn did_update_group_field(&mut self, _field: &Field) -> FlowyResult<Option<GroupChangesPB>> { + Ok(None) + } + + fn delete_group(&mut self, _group_id: &str) -> FlowyResult<(Vec<RowId>, Option<TypeOptionData>)> { Ok((vec![], None)) } - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, _changeset: &[GroupChangeset], ) -> FlowyResult<(Vec<GroupPB>, Option<TypeOptionData>)> { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index e5bae5ba8d..cae19109f6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,14 +1,14 @@ use async_trait::async_trait; -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SelectOption}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use flowy_error::{FlowyError, FlowyResult}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ - SelectOptionCellDataParser, SelectTypeOptionSharedAction, TypeOption, + MultiSelectTypeOption, SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, + TypeOption, }; use crate::services::group::action::GroupCustomize; use crate::services::group::controller::BaseGroupController; @@ -31,7 +31,6 @@ pub type MultiSelectGroupController = BaseGroupController< SelectOptionCellDataParser, >; -#[async_trait] impl GroupCustomize for MultiSelectGroupController { type GroupTypeOption = MultiSelectTypeOption; @@ -44,19 +43,21 @@ impl GroupCustomize for MultiSelectGroupController { } fn placeholder_cell(&self) -> Option<Cell> { - let mut cell = new_cell_builder(FieldType::MultiSelect); - cell.insert("data".into(), "".into()); - Some(cell) + Some( + new_cell_builder(FieldType::MultiSelect) + .insert_str_value("data", "") + .build(), + ) } fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB> { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { - if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row_detail) { changesets.push(changeset); } }); @@ -87,11 +88,11 @@ impl GroupCustomize for MultiSelectGroupController { group_changeset } - async fn create_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { - let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; let new_select_option = new_type_option.create_option(&name); @@ -103,8 +104,8 @@ impl GroupCustomize for MultiSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - async fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { - let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { + fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; if let Some(option_index) = new_type_option @@ -114,7 +115,6 @@ impl GroupCustomize for MultiSelectGroupController { { // Remove the option if the group is found new_type_option.options.remove(option_index); - self.context.delete_group(group_id)?; Ok(Some(new_type_option.into())) } else { Ok(None) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 0a82fe1fd4..d26ef50b70 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,14 +1,14 @@ use async_trait::async_trait; -use collab_database::fields::select_type_option::{SelectOption, SingleSelectTypeOption}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use flowy_error::{FlowyError, FlowyResult}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ - SelectOptionCellDataParser, SelectTypeOptionSharedAction, TypeOption, + SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, SingleSelectTypeOption, + TypeOption, }; use crate::services::group::action::GroupCustomize; use crate::services::group::controller::BaseGroupController; @@ -33,7 +33,6 @@ pub type SingleSelectGroupController = BaseGroupController< SelectOptionCellDataParser, >; -#[async_trait] impl GroupCustomize for SingleSelectGroupController { type GroupTypeOption = SingleSelectTypeOption; @@ -46,19 +45,21 @@ impl GroupCustomize for SingleSelectGroupController { } fn placeholder_cell(&self) -> Option<Cell> { - let mut cell = new_cell_builder(FieldType::SingleSelect); - cell.insert("data".into(), "".into()); - Some(cell) + Some( + new_cell_builder(FieldType::SingleSelect) + .insert_str_value("data", "") + .build(), + ) } fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB> { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { - if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row) { + if let Some(changeset) = add_or_remove_select_option_row(group, cell_data, row_detail) { changesets.push(changeset); } }); @@ -89,11 +90,11 @@ impl GroupCustomize for SingleSelectGroupController { group_changeset } - async fn create_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option<TypeOptionData>, Option<InsertedGroupPB>)> { - let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; let new_select_option = new_type_option.create_option(&name); @@ -105,8 +106,8 @@ impl GroupCustomize for SingleSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - async fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { - let mut new_type_option = self.get_grouping_field_type_option().await.ok_or_else(|| { + fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { FlowyError::internal().with_context("Failed to get grouping field type option") })?; if let Some(option_index) = new_type_option @@ -116,7 +117,6 @@ impl GroupCustomize for SingleSelectGroupController { { // Remove the option if the group is found new_type_option.options.remove(option_index); - self.context.delete_group(group_id)?; Ok(Some(new_type_option.into())) } else { Ok(None) diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index 827e4f0e53..01bd4cdc0d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -1,40 +1,43 @@ +use chrono::NaiveDateTime; +use collab_database::fields::Field; +use collab_database::rows::{Cell, Row, RowDetail}; + use crate::entities::{ FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, SelectOptionCellDataPB, }; use crate::services::cell::{ insert_checkbox_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; -use crate::services::field::CHECK; +use crate::services::field::{SelectOption, SelectOptionIds, CHECK}; use crate::services::group::{Group, GroupData, MoveGroupRowContext}; -use chrono::NaiveDateTime; -use collab_database::fields::select_type_option::{SelectOption, SelectOptionIds}; -use collab_database::fields::Field; -use collab_database::rows::{Cell, Row}; -use tracing::debug; pub fn add_or_remove_select_option_row( group: &mut GroupData, cell_data: &SelectOptionCellDataPB, - row: &Row, + row_detail: &RowDetail, ) -> Option<GroupRowsNotificationPB> { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); if cell_data.select_options.is_empty() { - if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } } else { cell_data.select_options.iter().for_each(|option| { if option.id == group.id { - if !group.contains_row(&row.id) { + if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row.clone()))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + group.add_row(row_detail.clone()); } - } else if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } }); } @@ -72,42 +75,55 @@ pub fn move_group_row( ) -> Option<GroupRowsNotificationPB> { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); let MoveGroupRowContext { - row, - updated_cells, + row_detail, + row_changeset, field, to_group_id, to_row_id, } = context; - let from_index = group.index_of_row(&row.id); + let from_index = group.index_of_row(&row_detail.row.id); let to_index = match to_row_id { None => None, Some(to_row_id) => group.index_of_row(to_row_id), }; // Remove the row in which group contains it - if from_index.is_some() { - changeset.deleted_rows.push(row.id.clone().into_inner()); - group.remove_row(&row.id); + if let Some(from_index) = &from_index { + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + tracing::debug!( + "Group:{} remove {} at {}", + group.id, + row_detail.row.id, + from_index + ); + group.remove_row(&row_detail.row.id); } if group.id == *to_group_id { - let mut inserted_row = InsertedRowPB::new(RowMetaPB::from((*row).clone())); + let mut inserted_row = InsertedRowPB::new(RowMetaPB::from((*row_detail).clone())); match to_index { None => { changeset.inserted_rows.push(inserted_row); - group.add_row(row.clone()); + tracing::debug!("Group:{} append row:{}", group.id, row_detail.row.id); + group.add_row(row_detail.clone()); }, Some(to_index) => { if to_index < group.number_of_row() { - inserted_row.index = Some(to_index as i32); - group.insert_row(to_index, row.clone()); - } else { - tracing::warn!( - "[Database Group]: Move to index: {} is out of bounds", + tracing::debug!( + "Group:{} insert {} at {} ", + group.id, + row_detail.row.id, to_index ); - group.add_row(row.clone()); + inserted_row.index = Some(to_index as i32); + group.insert_row(to_index, (*row_detail).clone()); + } else { + tracing::warn!("Move to index: {} is out of bounds", to_index); + tracing::debug!("Group:{} append row:{}", group.id, row_detail.row.id); + group.add_row((*row_detail).clone()); } changeset.inserted_rows.push(inserted_row); }, @@ -119,11 +135,14 @@ pub fn move_group_row( if from_index.is_none() { let cell = make_inserted_cell(&group.id, field); if let Some(cell) = cell { - debug!( - "[Database Group]: Update content of the cell in the row:{} to group:{}", - row.id, group.id + tracing::debug!( + "Update content of the cell in the row:{} to group:{}", + row_detail.row.id, + group.id ); - updated_cells.insert(field.id.clone(), cell); + row_changeset + .cell_by_field_id + .insert(field.id.clone(), cell); } } } @@ -157,7 +176,7 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option<Cell> { let date = NaiveDateTime::parse_from_str(&format!("{} 00:00:00", group_id), "%Y/%m/%d %H:%M:%S") .unwrap(); - let cell = insert_date_cell(date.and_utc().timestamp(), None, Some(false), field); + let cell = insert_date_cell(date.timestamp(), None, Some(false), field); Some(cell) }, _ => { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index aae0777b71..9d9a0468cb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -1,7 +1,6 @@ use async_trait::async_trait; -use collab_database::fields::url_type_option::{URLCellData, URLTypeOption}; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{new_cell_builder, Cell, Cells, Row}; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use flowy_error::FlowyResult; @@ -10,7 +9,7 @@ use crate::entities::{ FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::insert_url_cell; -use crate::services::field::{TypeOption, URLCellDataParser}; +use crate::services::field::{TypeOption, URLCellData, URLCellDataParser, URLTypeOption}; use crate::services::group::action::GroupCustomize; use crate::services::group::configuration::GroupControllerContext; use crate::services::group::controller::BaseGroupController; @@ -28,14 +27,15 @@ pub type URLGroupController = pub type URLGroupControllerContext = GroupControllerContext<URLGroupConfiguration>; -#[async_trait] impl GroupCustomize for URLGroupController { type GroupTypeOption = URLTypeOption; fn placeholder_cell(&self) -> Option<Cell> { - let mut cell = new_cell_builder(FieldType::URL); - cell.insert("data".into(), "".into()); - Some(cell) + Some( + new_cell_builder(FieldType::URL) + .insert_str_value("data", "") + .build(), + ) } fn can_group( @@ -48,7 +48,7 @@ impl GroupCustomize for URLGroupController { fn create_or_delete_group_when_cell_changed( &mut self, - _row: &Row, + _row_detail: &RowDetail, _old_cell_data: Option<&<Self::GroupTypeOption as TypeOption>::CellProtobufType>, _cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> FlowyResult<(Option<InsertedGroupPB>, Option<GroupPB>)> { @@ -58,7 +58,7 @@ impl GroupCustomize for URLGroupController { let cell_data: URLCellData = _cell_data.clone().into(); let group = Group::new(cell_data.data); let mut new_group = self.context.add_new_group(group)?; - new_group.group.rows.push(RowMetaPB::from(_row.clone())); + new_group.group.rows.push(RowMetaPB::from(_row_detail)); inserted_group = Some(new_group); } @@ -89,22 +89,24 @@ impl GroupCustomize for URLGroupController { fn add_or_remove_row_when_cell_changed( &mut self, - row: &Row, + row_detail: &RowDetail, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, ) -> Vec<GroupRowsNotificationPB> { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); if group.id == cell_data.content { - if !group.contains_row(&row.id) { + if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows - .push(InsertedRowPB::new(RowMetaPB::from(row))); - group.add_row(row.clone()); + .push(InsertedRowPB::new(RowMetaPB::from(row_detail))); + group.add_row(row_detail.clone()); } - } else if group.contains_row(&row.id) { - group.remove_row(&row.id); - changeset.deleted_rows.push(row.id.clone().into_inner()); + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); } if !changeset.is_empty() { @@ -155,7 +157,7 @@ impl GroupCustomize for URLGroupController { group_changeset } - fn delete_group_after_moving_row( + fn delete_group_when_move_row( &mut self, _row: &Row, cell_data: &<Self::GroupTypeOption as TypeOption>::CellProtobufType, @@ -172,7 +174,7 @@ impl GroupCustomize for URLGroupController { deleted_group } - async fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { + fn delete_group(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>> { self.context.delete_group(group_id)?; Ok(None) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 296a9960f7..12692fd812 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -1,27 +1,23 @@ -use collab::preclude::encoding::serde::{from_any, to_any}; -use collab::preclude::Any; +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; use collab_database::database::gen_database_group_id; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{GroupMap, GroupMapBuilder, GroupSettingBuilder, GroupSettingMap}; use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use std::sync::Arc; -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Default)] pub struct GroupSetting { pub id: String, pub field_id: String, - #[serde(rename = "ty")] pub field_type: i64, - #[serde(default)] pub groups: Vec<Group>, - #[serde(default)] pub content: String, } #[derive(Clone, Default, Debug)] pub struct GroupChangeset { pub group_id: String, + pub field_id: String, pub name: Option<String>, pub visible: Option<bool>, } @@ -48,20 +44,38 @@ impl TryFrom<GroupSettingMap> for GroupSetting { type Error = anyhow::Error; fn try_from(value: GroupSettingMap) -> Result<Self, Self::Error> { - from_any(&Any::from(value)).map_err(|e| e.into()) + match ( + value.get_str_value(GROUP_ID), + value.get_str_value(FIELD_ID), + value.get_i64_value(FIELD_TYPE), + ) { + (Some(id), Some(field_id), Some(field_type)) => { + let content = value.get_str_value(CONTENT).unwrap_or_default(); + let groups = value.try_get_array(GROUPS); + Ok(Self { + id, + field_id, + field_type, + groups, + content, + }) + }, + _ => { + bail!("Invalid group setting data") + }, + } } } impl From<GroupSetting> for GroupSettingMap { fn from(setting: GroupSetting) -> Self { - let groups = to_any(&setting.groups).unwrap_or_else(|_| Any::Array(Arc::from([]))); - GroupSettingBuilder::from([ - (GROUP_ID.into(), setting.id.into()), - (FIELD_ID.into(), setting.field_id.into()), - (FIELD_TYPE.into(), Any::BigInt(setting.field_type)), - (GROUPS.into(), groups), - (CONTENT.into(), setting.content.into()), - ]) + GroupSettingBuilder::new() + .insert_str_value(GROUP_ID, setting.id) + .insert_str_value(FIELD_ID, setting.field_id) + .insert_i64_value(FIELD_TYPE, setting.field_type) + .insert_maps(GROUPS, setting.groups) + .insert_str_value(CONTENT, setting.content) + .build() } } @@ -76,16 +90,22 @@ impl TryFrom<GroupMap> for Group { type Error = anyhow::Error; fn try_from(value: GroupMap) -> Result<Self, Self::Error> { - from_any(&Any::from(value)).map_err(|e| e.into()) + match value.get_str_value("id") { + None => bail!("Invalid group data"), + Some(id) => { + let visible = value.get_bool_value("visible").unwrap_or_default(); + Ok(Self { id, visible }) + }, + } } } impl From<Group> for GroupMap { fn from(group: Group) -> Self { - GroupMapBuilder::from([ - ("id".into(), group.id.into()), - ("visible".into(), group.visible.into()), - ]) + GroupMapBuilder::new() + .insert_str_value("id", group.id) + .insert_bool_value("visible", group.visible) + .build() } } @@ -103,13 +123,7 @@ pub struct GroupData { pub field_id: String, pub is_default: bool, pub is_visible: bool, - pub(crate) rows: Vec<Row>, -} - -impl Display for GroupData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "GroupData:{}, {} rows", self.id, self.rows.len()) - } + pub(crate) rows: Vec<RowDetail>, } impl GroupData { @@ -125,18 +139,18 @@ impl GroupData { } pub fn contains_row(&self, row_id: &RowId) -> bool { - self.rows.iter().any(|row| &row.id == row_id) + self + .rows + .iter() + .any(|row_detail| &row_detail.row.id == row_id) } pub fn remove_row(&mut self, row_id: &RowId) { - #[cfg(feature = "verbose_log")] - tracing::trace!( - "[Database Group]: Remove row:{} from group:{}", - row_id, - self.id - ); - - match self.rows.iter().position(|row| &row.id == row_id) { + match self + .rows + .iter() + .position(|row_detail| &row_detail.row.id == row_id) + { None => {}, Some(pos) => { self.rows.remove(pos); @@ -144,27 +158,18 @@ impl GroupData { } } - pub fn add_row(&mut self, row: Row) { - #[cfg(feature = "verbose_log")] - tracing::trace!("[Database Group]: Add row:{} to group:{}", row.id, self.id); - match self.rows.iter().find(|r| r.id == row.id) { + pub fn add_row(&mut self, row_detail: RowDetail) { + match self.rows.iter().find(|r| r.row.id == row_detail.row.id) { None => { - self.rows.push(row); + self.rows.push(row_detail); }, Some(_) => {}, } } - pub fn insert_row(&mut self, index: usize, row: Row) { - #[cfg(feature = "verbose_log")] - tracing::trace!( - "[Database Group]: Insert row:{} to group:{} at index:{}", - row.id, - self.id, - index - ); + pub fn insert_row(&mut self, index: usize, row_detail: RowDetail) { if index < self.rows.len() { - self.rows.insert(index, row); + self.rows.insert(index, row_detail); } else { tracing::error!( "Insert row index:{} beyond the bounds:{},", @@ -175,7 +180,10 @@ impl GroupData { } pub fn index_of_row(&self, row_id: &RowId) -> Option<usize> { - self.rows.iter().position(|row| &row.id == row_id) + self + .rows + .iter() + .position(|row_detail| &row_detail.row.id == row_id) } pub fn number_of_row(&self) -> usize { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index 96b8a0837c..8eb677ed26 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use async_trait::async_trait; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row, RowId}; +use collab_database::rows::{Cell, RowDetail, RowId}; + use flowy_error::FlowyResult; use crate::entities::FieldType; @@ -35,16 +36,37 @@ pub struct GeneratedGroups { } pub struct MoveGroupRowContext<'a> { - pub row: &'a Row, - pub updated_cells: &'a mut UpdatedCells, + pub row_detail: &'a RowDetail, + pub row_changeset: &'a mut RowChangeset, pub field: &'a Field, pub to_group_id: &'a str, pub to_row_id: Option<RowId>, } -/// A map of the updated cells. -/// The key is the field id, the value is the updated cell. -pub type UpdatedCells = HashMap<String, Cell>; +#[derive(Debug, Clone)] +pub struct RowChangeset { + pub row_id: RowId, + pub height: Option<i32>, + pub visibility: Option<bool>, + // Contains the key/value changes represents as the update of the cells. For example, + // if there is one cell was changed, then the `cell_by_field_id` will only have one key/value. + pub cell_by_field_id: HashMap<String, Cell>, +} + +impl RowChangeset { + pub fn new(row_id: RowId) -> Self { + Self { + row_id, + height: None, + visibility: None, + cell_by_field_id: Default::default(), + } + } + + pub fn is_empty(&self) -> bool { + self.height.is_none() && self.visibility.is_none() && self.cell_by_field_id.is_empty() + } +} /// Returns a group controller. /// @@ -63,7 +85,7 @@ pub type UpdatedCells = HashMap<String, Cell>; fields(grouping_field_id=%grouping_field.id, grouping_field_type) err )] -pub(crate) async fn make_group_controller<D>( +pub async fn make_group_controller<D>( view_id: &str, grouping_field: Field, delegate: D, @@ -72,9 +94,9 @@ where D: GroupContextDelegate + GroupControllerDelegate, { let grouping_field_type = FieldType::from(grouping_field.field_type); - tracing::Span::current().record("grouping_field", grouping_field_type.default_name()); + tracing::Span::current().record("grouping_field", &grouping_field_type.default_name()); - let group_controller: Box<dyn GroupController>; + let mut group_controller: Box<dyn GroupController>; let delegate = Arc::new(delegate); match grouping_field_type { @@ -135,19 +157,22 @@ where }, _ => { group_controller = Box::new(DefaultGroupController::new( - view_id, &grouping_field, delegate.clone(), )); }, } - #[cfg(feature = "verbose_log")] - { - for group in group_controller.get_all_groups() { - tracing::trace!("[Database]: group: {}", group); - } - } + // Separates the rows into different groups + let row_details = delegate.get_all_rows(view_id).await; + + let rows = row_details + .iter() + .map(|row| row.as_ref()) + .collect::<Vec<_>>(); + + group_controller.fill_groups(rows.as_slice(), &grouping_field)?; + Ok(group_controller) } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs index 9fba427e11..c2ac8300b4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs @@ -5,7 +5,7 @@ mod controller_impls; mod entities; mod group_builder; -pub(crate) use action::*; +pub(crate) use action::GroupController; pub(crate) use configuration::*; pub(crate) use controller::*; pub(crate) use controller_impls::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs index d40ab58d72..7cfe093725 100644 --- a/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/setting/entities.rs @@ -1,44 +1,50 @@ -use collab::preclude::encoding::serde::from_any; -use collab::preclude::Any; +use collab::core::any_map::AnyMapExtension; use collab_database::views::{LayoutSetting, LayoutSettingBuilder}; use serde::{Deserialize, Serialize}; use serde_repr::*; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CalendarLayoutSetting { - #[serde(default)] pub layout_ty: CalendarLayout, - #[serde(default)] pub first_day_of_week: i32, - #[serde(default)] pub show_weekends: bool, - #[serde(default)] pub show_week_numbers: bool, - #[serde(default)] pub field_id: String, } impl From<LayoutSetting> for CalendarLayoutSetting { fn from(setting: LayoutSetting) -> Self { - from_any(&Any::from(setting)).unwrap() + let layout_ty = setting + .get_i64_value("layout_ty") + .map(CalendarLayout::from) + .unwrap_or_default(); + let first_day_of_week = setting + .get_i64_value("first_day_of_week") + .unwrap_or(DEFAULT_FIRST_DAY_OF_WEEK as i64) as i32; + let show_weekends = setting.get_bool_value("show_weekends").unwrap_or_default(); + let show_week_numbers = setting + .get_bool_value("show_week_numbers") + .unwrap_or_default(); + let field_id = setting.get_str_value("field_id").unwrap_or_default(); + Self { + layout_ty, + first_day_of_week, + show_weekends, + show_week_numbers, + field_id, + } } } impl From<CalendarLayoutSetting> for LayoutSetting { fn from(setting: CalendarLayoutSetting) -> Self { - LayoutSettingBuilder::from([ - ("layout_ty".into(), Any::BigInt(setting.layout_ty.value())), - ( - "first_day_of_week".into(), - Any::BigInt(setting.first_day_of_week as i64), - ), - ( - "show_week_numbers".into(), - Any::Bool(setting.show_week_numbers), - ), - ("show_weekends".into(), Any::Bool(setting.show_weekends)), - ("field_id".into(), setting.field_id.into()), - ]) + LayoutSettingBuilder::new() + .insert_i64_value("layout_ty", setting.layout_ty.value()) + .insert_i64_value("first_day_of_week", setting.first_day_of_week as i64) + .insert_bool_value("show_week_numbers", setting.show_week_numbers) + .insert_bool_value("show_weekends", setting.show_weekends) + .insert_str_value("field_id", setting.field_id) + .build() } } @@ -84,40 +90,36 @@ pub const DEFAULT_FIRST_DAY_OF_WEEK: i32 = 0; pub const DEFAULT_SHOW_WEEKENDS: bool = true; pub const DEFAULT_SHOW_WEEK_NUMBERS: bool = true; -#[derive(Debug, Clone, Default, Deserialize)] +#[derive(Debug, Clone, Default)] pub struct BoardLayoutSetting { - #[serde(default)] pub hide_ungrouped_column: bool, - #[serde(default)] pub collapse_hidden_groups: bool, } impl BoardLayoutSetting { pub fn new() -> Self { - Self { - hide_ungrouped_column: false, - collapse_hidden_groups: true, - } + Self::default() } } impl From<LayoutSetting> for BoardLayoutSetting { fn from(setting: LayoutSetting) -> Self { - from_any(&Any::from(setting)).unwrap() + Self { + hide_ungrouped_column: setting + .get_bool_value("hide_ungrouped_column") + .unwrap_or_default(), + collapse_hidden_groups: setting + .get_bool_value("collapse_hidden_groups") + .unwrap_or_default(), + } } } impl From<BoardLayoutSetting> for LayoutSetting { fn from(setting: BoardLayoutSetting) -> Self { - LayoutSettingBuilder::from([ - ( - "hide_ungrouped_column".into(), - setting.hide_ungrouped_column.into(), - ), - ( - "collapse_hidden_groups".into(), - setting.collapse_hidden_groups.into(), - ), - ]) + LayoutSettingBuilder::new() + .insert_bool_value("hide_ungrouped_column", setting.hide_ungrouped_column) + .insert_bool_value("collapse_hidden_groups", setting.collapse_hidden_groups) + .build() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs index 3eab243fd7..8cb59a1872 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/export.rs @@ -1,14 +1,13 @@ use collab_database::database::Database; use collab_database::fields::Field; use collab_database::rows::Cell; -use collab_database::template::timestamp_parse::TimestampCellData; -use futures::StreamExt; use indexmap::IndexMap; use flowy_error::{FlowyError, FlowyResult}; use crate::entities::FieldType; use crate::services::cell::stringify_cell; +use crate::services::field::{TimestampCellData, TimestampCellDataWrapper}; #[derive(Debug, Clone, Copy)] pub enum CSVFormat { @@ -22,16 +21,10 @@ pub enum CSVFormat { pub struct CSVExport; impl CSVExport { - pub async fn export_database( - &self, - database: &Database, - style: CSVFormat, - ) -> FlowyResult<String> { + pub fn export_database(&self, database: &Database, style: CSVFormat) -> FlowyResult<String> { let mut wtr = csv::Writer::from_writer(vec![]); - let view_id = database - .get_first_database_view_id() - .ok_or_else(|| FlowyError::internal().with_context("failed to get first database view"))?; - let fields = database.get_fields_in_view(&view_id, None); + let inline_view_id = database.get_inline_view_id(); + let fields = database.get_fields_in_view(&inline_view_id, None); // Write fields let field_records = fields @@ -50,12 +43,7 @@ impl CSVExport { fields.into_iter().for_each(|field| { field_by_field_id.insert(field.id.clone(), field); }); - let rows = database - .get_rows_for_view(&view_id, 20, None) - .await - .filter_map(|result| async { result.ok() }) - .collect::<Vec<_>>() - .await; + let rows = database.get_rows_for_view(&inline_view_id); let stringify = |cell: &Cell, field: &Field, style: CSVFormat| match style { CSVFormat::Original => stringify_cell(cell, field), @@ -74,7 +62,7 @@ impl CSVExport { } else { TimestampCellData::new(row.modified_at) }; - let cell = cell_data.to_cell(field.field_type); + let cell = Cell::from(TimestampCellDataWrapper::from((field_type, cell_data))); stringify(&cell, field, style) }, _ => match row.cells.get(field_id) { diff --git a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs index 5f10691879..dd1f0c1f6b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs +++ b/frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs @@ -1,11 +1,11 @@ +use std::{fs::File, io::prelude::*}; + use collab_database::database::{gen_database_id, gen_field_id, gen_row_id, timestamp}; -use collab_database::entity::{CreateDatabaseParams, CreateViewParams, EncodedCollabInfo}; use collab_database::fields::Field; use collab_database::rows::{new_cell_builder, Cell, CreateRowParams}; -use collab_database::views::DatabaseLayout; +use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; + use flowy_error::{FlowyError, FlowyResult}; -use std::fmt::Display; -use std::{fs::File, io::prelude::*}; use crate::entities::FieldType; use crate::services::field::{default_type_option_data_from_type, CELL_DATA}; @@ -108,18 +108,17 @@ fn database_from_fields_and_rows( let field_type = FieldType::from(field.field_type); // Make the cell based on the style. - let mut cell = new_cell_builder(field_type); - match format { - CSVFormat::Original => { - cell.insert(CELL_DATA.into(), cell_content.as_str().into()); - }, + let cell = match format { + CSVFormat::Original => new_cell_builder(field_type) + .insert_str_value(CELL_DATA, cell_content.to_string()) + .build(), CSVFormat::META => match serde_json::from_str::<Cell>(cell_content) { - Ok(cell_json) => cell = cell_json, - Err(_) => { - cell.insert(CELL_DATA.into(), "".into()); - }, + Ok(cell) => cell, + Err(_) => new_cell_builder(field_type) + .insert_str_value(CELL_DATA, "".to_string()) + .build(), }, - } + }; params.cells.insert(field.id.clone(), cell); } } @@ -131,6 +130,7 @@ fn database_from_fields_and_rows( CreateDatabaseParams { database_id: database_id.clone(), + inline_view_id: view_id.to_string(), rows, fields, views: vec![CreateViewParams { @@ -166,26 +166,8 @@ impl FieldsRows { pub struct ImportResult { pub database_id: String, pub view_id: String, - pub encoded_collabs: Vec<EncodedCollabInfo>, } -impl Display for ImportResult { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let total_size: usize = self - .encoded_collabs - .iter() - .map(|c| c.encoded_collab.doc_state.len()) - .sum(); - write!( - f, - "ImportResult {{ database_id: {}, view_id: {}, num collabs: {}, size: {} }}", - self.database_id, - self.view_id, - self.encoded_collabs.len(), - total_size - ) - } -} #[cfg(test)] mod tests { use collab_database::database::gen_database_view_id; diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index 8dc3393a0f..330f46f7f7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -1,43 +1,43 @@ -use async_trait::async_trait; use std::cmp::Ordering; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row, RowId}; -use collab_database::template::timestamp_parse::TimestampCellData; +use collab_database::rows::{Cell, Row, RowDetail, RowId}; use rayon::prelude::ParallelSliceMut; use serde::{Deserialize, Serialize}; -use tokio::sync::RwLock as TokioRwLock; +use tokio::sync::RwLock; use flowy_error::FlowyResult; +use lib_infra::future::Fut; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use crate::entities::SortChangesetNotificationPB; use crate::entities::{FieldType, SortWithIndexPB}; use crate::services::cell::CellCache; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; -use crate::services::field::{default_order, TypeOptionCellExt}; +use crate::services::field::{ + default_order, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellExt, +}; use crate::services::sort::{ - ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, + InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, }; -#[async_trait] pub trait SortDelegate: Send + Sync { - async fn get_sort(&self, view_id: &str, sort_id: &str) -> Option<Arc<Sort>>; + fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut<Option<Arc<Sort>>>; /// Returns all the rows after applying grid's filter - async fn get_rows(&self, view_id: &str) -> Vec<Arc<Row>>; - async fn filter_row(&self, row_detail: &Row) -> bool; - async fn get_field(&self, field_id: &str) -> Option<Field>; - async fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Vec<Field>; + fn get_rows(&self, view_id: &str) -> Fut<Vec<Arc<RowDetail>>>; + fn filter_row(&self, row_detail: &RowDetail) -> Fut<bool>; + fn get_field(&self, field_id: &str) -> Option<Field>; + fn get_fields(&self, view_id: &str, field_ids: Option<Vec<String>>) -> Fut<Vec<Field>>; } pub struct SortController { view_id: String, handler_id: String, delegate: Box<dyn SortDelegate>, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, sorts: Vec<Arc<Sort>>, cell_cache: CellCache, row_index_cache: HashMap<RowId, usize>, @@ -56,7 +56,7 @@ impl SortController { handler_id: &str, sorts: Vec<Arc<Sort>>, delegate: T, - task_scheduler: Arc<TokioRwLock<TaskDispatcher>>, + task_scheduler: Arc<RwLock<TaskDispatcher>>, cell_cache: CellCache, notifier: DatabaseViewChangedNotifier, ) -> Self @@ -83,10 +83,6 @@ impl SortController { } } - pub async fn has_sorts(&self) -> bool { - !self.sorts.is_empty() - } - pub async fn did_receive_row_changed(&self, row_id: RowId) { if !self.sorts.is_empty() { self @@ -98,31 +94,27 @@ impl SortController { } } - pub async fn did_create_row(&mut self, row: &Row) -> Option<u32> { - if !self.delegate.filter_row(row).await { - return None; + pub async fn did_create_row(&self, preliminary_index: usize, row_detail: &RowDetail) { + if !self.delegate.filter_row(row_detail).await { + return; } if !self.sorts.is_empty() { - let mut rows = self.delegate.get_rows(&self.view_id).await; - self.sort_rows(&mut rows).await; - - let row_index = self - .row_index_cache - .get(&row.id) - .cloned() - .map(|val| val as u32); - - if row_index.is_none() { - tracing::trace!("The row index cache is outdated"); - } - row_index + self + .gen_task( + SortEvent::NewRowInserted(row_detail.clone()), + QualityOfService::Background, + ) + .await; } else { - let rows = self.delegate.get_rows(&self.view_id).await; - rows - .iter() - .position(|val| val.id == row.id) - .map(|val| val as u32) + let result = InsertRowResult { + view_id: self.view_id.clone(), + row: row_detail.clone(), + index: preliminary_index, + }; + let _ = self + .notifier + .send(DatabaseViewChanged::InsertRowNotification(result)); } } @@ -137,15 +129,31 @@ impl SortController { // #[tracing::instrument(name = "process_sort_task", level = "trace", skip_all, err)] pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { let event_type = SortEvent::from_str(predicate).unwrap(); - let mut rows = self.delegate.get_rows(&self.view_id).await; + let mut row_details = self.delegate.get_rows(&self.view_id).await; match event_type { SortEvent::SortDidChanged | SortEvent::DeleteAllSorts => { - self.sort_rows_and_notify(&mut rows).await; + self.sort_rows(&mut row_details).await; + let row_orders = row_details + .iter() + .map(|row_detail| row_detail.row.id.to_string()) + .collect::<Vec<String>>(); + + let notification = ReorderAllRowsResult { + view_id: self.view_id.clone(), + row_orders, + }; + + let _ = self + .notifier + .send(DatabaseViewChanged::ReorderAllRowsNotification( + notification, + )); }, SortEvent::RowDidChanged(row_id) => { let old_row_index = self.row_index_cache.get(&row_id).cloned(); - self.sort_rows(&mut rows).await; + + self.sort_rows(&mut row_details).await; let new_row_index = self.row_index_cache.get(&row_id).cloned(); match (old_row_index, new_row_index) { (Some(old_row_index), Some(new_row_index)) => { @@ -167,6 +175,24 @@ impl SortController { _ => tracing::trace!("The row index cache is outdated"), } }, + SortEvent::NewRowInserted(row_detail) => { + self.sort_rows(&mut row_details).await; + let row_index = self.row_index_cache.get(&row_detail.row.id).cloned(); + match row_index { + Some(row_index) => { + let notification = InsertRowResult { + view_id: self.view_id.clone(), + row: row_detail.clone(), + index: row_index, + }; + self.row_index_cache.insert(row_detail.row.id, row_index); + let _ = self + .notifier + .send(DatabaseViewChanged::InsertRowNotification(notification)); + }, + _ => tracing::trace!("The row index cache is outdated"), + } + }, } Ok(()) } @@ -177,38 +203,26 @@ impl SortController { let task = Task::new( &self.handler_id, task_id, - TaskContent::Text(task_type.to_json_string()), + TaskContent::Text(task_type.to_string()), qos, ); self.task_scheduler.write().await.add_task(task); } - pub async fn sort_rows_and_notify(&mut self, rows: &mut Vec<Arc<Row>>) { - self.sort_rows(rows).await; - let row_orders = rows - .iter() - .map(|row| row.id.to_string()) - .collect::<Vec<String>>(); + pub async fn sort_rows(&mut self, rows: &mut Vec<Arc<RowDetail>>) { + if self.sorts.is_empty() { + return; + } - let notification = ReorderAllRowsResult { - view_id: self.view_id.clone(), - row_orders, - }; - - let _ = self - .notifier - .send(DatabaseViewChanged::ReorderAllRowsNotification( - notification, - )); - } - - pub async fn sort_rows(&mut self, rows: &mut Vec<Arc<Row>>) { let fields = self.delegate.get_fields(&self.view_id, None).await; for sort in self.sorts.iter().rev() { - rows.par_sort_by(|left, right| cmp_row(left, right, sort, &fields, &self.cell_cache)); + rows + .par_sort_by(|left, right| cmp_row(&left.row, &right.row, sort, &fields, &self.cell_cache)); } - rows.iter().enumerate().for_each(|(index, row)| { - self.row_index_cache.insert(row.id.clone(), index); + rows.iter().enumerate().for_each(|(index, row_detail)| { + self + .row_index_cache + .insert(row_detail.row.id.clone(), index); }); } @@ -306,10 +320,10 @@ fn cmp_row( (left.modified_at, right.modified_at) }; let (left_cell, right_cell) = ( - TimestampCellData::new(left_cell).to_cell(field_rev.field_type), - TimestampCellData::new(right_cell).to_cell(field_rev.field_type), + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(left_cell))), + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(right_cell))), ); - Some((Some(left_cell), Some(right_cell))) + Some((Some(left_cell.into()), Some(right_cell.into()))) }, _ => None, }; @@ -348,11 +362,12 @@ fn cmp_cell( enum SortEvent { SortDidChanged, RowDidChanged(RowId), + NewRowInserted(RowDetail), DeleteAllSorts, } -impl SortEvent { - fn to_json_string(&self) -> String { +impl ToString for SortEvent { + fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs index 9d66ece5cc..9f9d37d4fb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -1,9 +1,8 @@ use std::cmp::Ordering; use anyhow::bail; -use collab::preclude::Any; -use collab::util::AnyMapExt; -use collab_database::rows::RowId; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{SortMap, SortMapBuilder}; #[derive(Debug, Clone)] @@ -21,13 +20,10 @@ impl TryFrom<SortMap> for Sort { type Error = anyhow::Error; fn try_from(value: SortMap) -> Result<Self, Self::Error> { - match ( - value.get_as::<String>(SORT_ID), - value.get_as::<String>(FIELD_ID), - ) { + match (value.get_str_value(SORT_ID), value.get_str_value(FIELD_ID)) { (Some(id), Some(field_id)) => { let condition = value - .get_as::<i64>(SORT_CONDITION) + .get_i64_value(SORT_CONDITION) .map(SortCondition::from) .unwrap_or_default(); Ok(Self { @@ -45,11 +41,11 @@ impl TryFrom<SortMap> for Sort { impl From<Sort> for SortMap { fn from(data: Sort) -> Self { - SortMapBuilder::from([ - (SORT_ID.into(), data.id.into()), - (FIELD_ID.into(), data.field_id.into()), - (SORT_CONDITION.into(), Any::BigInt(data.condition.value())), - ]) + SortMapBuilder::new() + .insert_str_value(SORT_ID, data.id) + .insert_str_value(FIELD_ID, data.field_id) + .insert_i64_value(SORT_CONDITION, data.condition.value()) + .build() } } @@ -87,7 +83,7 @@ impl From<i64> for SortCondition { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ReorderAllRowsResult { pub view_id: String, pub row_orders: Vec<String>, @@ -102,7 +98,7 @@ impl ReorderAllRowsResult { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ReorderSingleRowResult { pub view_id: String, pub row_id: RowId, @@ -110,6 +106,13 @@ pub struct ReorderSingleRowResult { pub new_index: usize, } +#[derive(Clone)] +pub struct InsertRowResult { + pub view_id: String, + pub row: RowDetail, + pub index: usize, +} + #[derive(Debug, Default)] pub struct SortChangeset { pub(crate) insert_sort: Option<Sort>, diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/task.rs b/frontend/rust-lib/flowy-database2/src/services/sort/task.rs index 6b77e87a33..107f318dec 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/task.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/task.rs @@ -1,6 +1,5 @@ use crate::services::sort::SortController; -use async_trait::async_trait; - +use lib_infra::future::BoxResultFuture; use lib_infra::priority_task::{TaskContent, TaskHandler}; use std::sync::Arc; use tokio::sync::RwLock; @@ -20,7 +19,6 @@ impl SortTaskHandler { } } -#[async_trait] impl TaskHandler for SortTaskHandler { fn handler_id(&self) -> &str { &self.handler_id @@ -30,16 +28,18 @@ impl TaskHandler for SortTaskHandler { "SortTaskHandler" } - async fn run(&self, content: TaskContent) -> Result<(), anyhow::Error> { + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { let sort_controller = self.sort_controller.clone(); - if let TaskContent::Text(predicate) = content { - sort_controller - .write() - .await - .process(&predicate) - .await - .map_err(anyhow::Error::from)?; - } - Ok(()) + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + sort_controller + .write() + .await + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) } } diff --git a/frontend/rust-lib/flowy-database2/src/template.rs b/frontend/rust-lib/flowy-database2/src/template.rs index 32d8df5062..c0347be580 100644 --- a/frontend/rust-lib/flowy-database2/src/template.rs +++ b/frontend/rust-lib/flowy-database2/src/template.rs @@ -1,14 +1,14 @@ use collab_database::database::{gen_database_id, gen_row_id, timestamp}; -use collab_database::entity::{CreateDatabaseParams, CreateViewParams}; -use collab_database::fields::select_type_option::{ - SelectOption, SelectOptionColor, SingleSelectTypeOption, -}; use collab_database::rows::CreateRowParams; -use collab_database::views::{DatabaseLayout, LayoutSettings}; +use collab_database::views::{ + CreateDatabaseParams, CreateViewParams, DatabaseLayout, LayoutSettings, +}; use crate::entities::FieldType; use crate::services::cell::{insert_select_option_cell, insert_text_cell}; -use crate::services::field::FieldBuilder; +use crate::services::field::{ + FieldBuilder, SelectOption, SelectOptionColor, SingleSelectTypeOption, +}; use crate::services::field_settings::default_field_settings_for_fields; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; @@ -35,6 +35,7 @@ pub fn make_default_grid(view_id: &str, name: &str) -> CreateDatabaseParams { CreateDatabaseParams { database_id: database_id.clone(), + inline_view_id: view_id.to_string(), views: vec![CreateViewParams { database_id: database_id.clone(), view_id: view_id.to_string(), @@ -105,6 +106,7 @@ pub fn make_default_board(view_id: &str, name: &str) -> CreateDatabaseParams { CreateDatabaseParams { database_id: database_id.clone(), + inline_view_id: view_id.to_string(), views: vec![CreateViewParams { database_id, view_id: view_id.to_string(), @@ -157,6 +159,7 @@ pub fn make_default_calendar(view_id: &str, name: &str) -> CreateDatabaseParams CreateDatabaseParams { database_id: database_id.clone(), + inline_view_id: view_id.to_string(), views: vec![CreateViewParams { database_id, view_id: view_id.to_string(), diff --git a/frontend/rust-lib/flowy-database2/src/utils/cache.rs b/frontend/rust-lib/flowy-database2/src/utils/cache.rs index 840bdbb1b4..5f9bda50c9 100644 --- a/frontend/rust-lib/flowy-database2/src/utils/cache.rs +++ b/frontend/rust-lib/flowy-database2/src/utils/cache.rs @@ -1,25 +1,23 @@ -use dashmap::mapref::one::{MappedRef, MappedRefMut}; -use dashmap::DashMap; +use parking_lot::RwLock; use std::any::{type_name, Any}; +use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; use std::sync::Arc; #[derive(Default, Debug)] /// The better option is use LRU cache -pub struct AnyTypeCache<K>(DashMap<K, TypeValue>) -where - K: Clone + Hash + Eq; +pub struct AnyTypeCache<TypeValueKey>(HashMap<TypeValueKey, TypeValue>); -impl<K> AnyTypeCache<K> +impl<TypeValueKey> AnyTypeCache<TypeValueKey> where - K: Clone + Hash + Eq, + TypeValueKey: Clone + Hash + Eq, { - pub fn new() -> Arc<AnyTypeCache<K>> { - Arc::new(AnyTypeCache(DashMap::default())) + pub fn new() -> Arc<RwLock<AnyTypeCache<TypeValueKey>>> { + Arc::new(RwLock::new(AnyTypeCache(HashMap::default()))) } - pub fn insert<T>(&self, key: &K, val: T) -> Option<T> + pub fn insert<T>(&mut self, key: &TypeValueKey, val: T) -> Option<T> where T: 'static + Send + Sync, { @@ -29,27 +27,31 @@ where .and_then(downcast_owned) } - pub fn remove(&self, key: &K) { + pub fn remove(&mut self, key: &TypeValueKey) { self.0.remove(key); } - pub fn get<T>(&self, key: &K) -> Option<MappedRef<'_, K, TypeValue, T>> + pub fn get<T>(&self, key: &TypeValueKey) -> Option<&T> where T: 'static + Send + Sync, { - let cell = self.0.get(key)?; - cell.try_map(|v| v.boxed.downcast_ref()).ok() + self + .0 + .get(key) + .and_then(|type_value| type_value.boxed.downcast_ref()) } - pub fn get_mut<T>(&self, key: &K) -> Option<MappedRefMut<'_, K, TypeValue, T>> + pub fn get_mut<T>(&mut self, key: &TypeValueKey) -> Option<&mut T> where T: 'static + Send + Sync, { - let cell = self.0.get_mut(key)?; - cell.try_map(|v| v.boxed.downcast_mut()).ok() + self + .0 + .get_mut(key) + .and_then(|type_value| type_value.boxed.downcast_mut()) } - pub fn contains(&self, key: &K) -> bool { + pub fn contains(&self, key: &TypeValueKey) -> bool { self.0.contains_key(key) } @@ -63,7 +65,7 @@ fn downcast_owned<T: 'static + Send + Sync>(type_value: TypeValue) -> Option<T> } #[derive(Debug)] -pub struct TypeValue { +struct TypeValue { boxed: Box<dyn Any + Send + Sync + 'static>, #[allow(dead_code)] ty: &'static str, diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs index 08ce2e954e..648de5edc7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs @@ -1,27 +1,27 @@ -use collab_database::fields::date_type_option::DateCellData; -use flowy_database2::entities::FieldType; -use lib_infra::util::timestamp; use std::time::Duration; -use crate::database::block_test::script::DatabaseRowTest; +use flowy_database2::entities::FieldType; +use flowy_database2::services::field::DateCellData; +use lib_infra::util::timestamp; +use crate::database::block_test::script::DatabaseRowTest; +use crate::database::block_test::script::RowScript::*; + +// Create a new row at the end of the grid and check the create time is valid. #[tokio::test] async fn created_at_field_test() { let mut test = DatabaseRowTest::new().await; - - // Get initial row count - let row_count = test.rows.len(); - - // Create a new row and assert the row count has increased by 1 - test.create_empty_row().await; - test.assert_row_count(row_count + 1).await; + let row_count = test.row_details.len(); + test + .run_scripts(vec![CreateEmptyRow, AssertRowCount(row_count + 1)]) + .await; // Get created time of the new row. - let row = test.get_rows().await.last().cloned().unwrap(); - let created_at_field = test.get_first_field(FieldType::CreatedTime).await; + let row_detail = test.get_rows().await.last().cloned().unwrap(); + let updated_at_field = test.get_first_field(FieldType::CreatedTime); let cell = test .editor - .get_cell(&created_at_field.id, &row.id) + .get_cell(&updated_at_field.id, &row_detail.row.id) .await .unwrap(); let created_at_timestamp = DateCellData::from(&cell).timestamp.unwrap(); @@ -30,35 +30,35 @@ async fn created_at_field_test() { assert!(created_at_timestamp <= timestamp()); } +// Update row and check the update time is valid. #[tokio::test] async fn update_at_field_test() { let mut test = DatabaseRowTest::new().await; - - // Get the first row and the current LastEditedTime field - let row = test.get_rows().await.remove(0); - let last_edit_field = test.get_first_field(FieldType::LastEditedTime).await; + let row_detail = test.get_rows().await.remove(0); + let last_edit_field = test.get_first_field(FieldType::LastEditedTime); let cell = test .editor - .get_cell(&last_edit_field.id, &row.id) + .get_cell(&last_edit_field.id, &row_detail.row.id) .await .unwrap(); let old_updated_at = DateCellData::from(&cell).timestamp.unwrap(); - // Wait for 1 second before updating the row tokio::time::sleep(Duration::from_millis(1000)).await; + test + .run_script(UpdateTextCell { + row_id: row_detail.row.id.clone(), + content: "test".to_string(), + }) + .await; - // Update the text cell of the first row - test.update_text_cell(row.id.clone(), "test").await; - - // Get the updated time of the row - let row = test.get_rows().await.remove(0); - let last_edit_field = test.get_first_field(FieldType::LastEditedTime).await; + // Get the updated time of the row. + let row_detail = test.get_rows().await.remove(0); + let last_edit_field = test.get_first_field(FieldType::LastEditedTime); let cell = test .editor - .get_cell(&last_edit_field.id, &row.id) + .get_cell(&last_edit_field.id, &row_detail.row.id) .await .unwrap(); let new_updated_at = DateCellData::from(&cell).timestamp.unwrap(); - assert!(old_updated_at < new_updated_at); } diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs index b8035767ef..72b62b55df 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs @@ -1,7 +1,15 @@ -use crate::database::database_editor::DatabaseEditorTest; use collab_database::rows::RowId; + use flowy_database2::entities::CreateRowPayloadPB; +use crate::database::database_editor::DatabaseEditorTest; + +pub enum RowScript { + CreateEmptyRow, + UpdateTextCell { row_id: RowId, content: String }, + AssertRowCount(usize), +} + pub struct DatabaseRowTest { inner: DatabaseEditorTest, } @@ -12,24 +20,32 @@ impl DatabaseRowTest { Self { inner: editor_test } } - pub async fn create_empty_row(&mut self) { - let params = CreateRowPayloadPB { - view_id: self.view_id.clone(), - ..Default::default() - }; - let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); - self - .row_by_row_id - .insert(row_detail.row.id.to_string(), row_detail.into()); - self.rows = self.get_rows().await; + pub async fn run_scripts(&mut self, scripts: Vec<RowScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn update_text_cell(&mut self, row_id: RowId, content: &str) { - self.inner.update_text_cell(row_id, content).await.unwrap(); - } - - pub async fn assert_row_count(&self, expected_row_count: usize) { - assert_eq!(expected_row_count, self.rows.len()); + pub async fn run_script(&mut self, script: RowScript) { + match script { + RowScript::CreateEmptyRow => { + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + RowScript::UpdateTextCell { row_id, content } => { + self.update_text_cell(row_id, &content).await.unwrap(); + }, + RowScript::AssertRowCount(expected_row_count) => { + assert_eq!(expected_row_count, self.row_details.len()); + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs index 23e70976e2..2800596900 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs @@ -1,21 +1,21 @@ use std::sync::Arc; -use crate::database::calculations_test::script::DatabaseCalculationTest; +use crate::database::calculations_test::script::{CalculationScript::*, DatabaseCalculationTest}; + use collab_database::fields::Field; use flowy_database2::entities::{CalculationType, FieldType, UpdateCalculationChangesetPB}; -use lib_infra::box_any::BoxAny; #[tokio::test] async fn calculations_test() { let mut test = DatabaseCalculationTest::new().await; - let expected_sum = 25.00; - let expected_min = 1.00; - let expected_average = 5.00; - let expected_max = 14.00; - let expected_median = 3.00; + let expected_sum = 25.00000; + let expected_min = 1.00000; + let expected_average = 5.00000; + let expected_max = 14.00000; + let expected_median = 3.00000; - let view_id = &test.view_id(); + let view_id = &test.view_id; let number_fields = test .fields .clone() @@ -25,171 +25,63 @@ async fn calculations_test() { let field_id = &number_fields.first().unwrap().id; let calculation_id = "calc_id".to_owned(); - - // Insert Sum calculation and assert its value - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.to_owned(), - field_id: field_id.to_owned(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Sum, - }) - .await; - - test.assert_calculation_float_value(expected_sum).await; - - // Insert Min calculation and assert its value - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.to_owned(), - field_id: field_id.to_owned(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Min, - }) - .await; - - test.assert_calculation_float_value(expected_min).await; - - // Insert Average calculation and assert its value - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.to_owned(), - field_id: field_id.to_owned(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Average, - }) - .await; - - test.assert_calculation_float_value(expected_average).await; - - // Insert Max calculation and assert its value - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.to_owned(), - field_id: field_id.to_owned(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Max, - }) - .await; - - test.assert_calculation_float_value(expected_max).await; - - // Insert Median calculation and assert its value - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.to_owned(), - field_id: field_id.to_owned(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Median, - }) - .await; - - test.assert_calculation_float_value(expected_median).await; -} - -#[tokio::test] -async fn calculations_empty_test() { - let mut test = DatabaseCalculationTest::new().await; - - let view_id = &test.view_id(); - let text_fields = test - .fields - .clone() - .into_iter() - .filter(|field| field.field_type == FieldType::RichText as i64) - .collect::<Vec<Arc<Field>>>(); - let field_id = &text_fields.first().unwrap().id.clone(); - let calculation_id = "calc_id".to_owned(); - - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.clone(), - field_id: field_id.clone(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::CountEmpty, - }) - .await; - test.assert_calculation_value("1").await; - - // Update the cell with a non-empty value - test - .update_cell( - field_id, - test.rows[1].id.clone(), - BoxAny::new("change".to_string()), - ) - .await - .unwrap(); - - // sleep for 3 seconds to wait for the calculation to update - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - test.assert_calculation_value("0").await; -} - -#[tokio::test] -async fn calculations_non_empty_test() { - let mut test = DatabaseCalculationTest::new().await; - - let view_id = &test.view_id(); - let text_fields = test - .fields - .clone() - .into_iter() - .filter(|field| field.field_type == FieldType::RichText as i64) - .collect::<Vec<Arc<Field>>>(); - let field_id = &text_fields.first().unwrap().id.clone(); - let calculation_id = "calc_id".to_owned(); - - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.clone(), - field_id: field_id.clone(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::CountNonEmpty, - }) - .await; - test.assert_calculation_value("6").await; - - // Update the cell with a non-empty value - test - .update_cell( - field_id, - test.rows[1].id.clone(), - BoxAny::new("change".to_string()), - ) - .await - .unwrap(); - - // sleep for 3 seconds to wait for the calculation to update - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - test.assert_calculation_value("7").await; -} - -#[tokio::test] -async fn calculations_count_test() { - let mut test = DatabaseCalculationTest::new().await; - - let view_id = &test.view_id(); - let text_fields = test - .fields - .clone() - .into_iter() - .filter(|field| field.field_type == FieldType::RichText as i64) - .collect::<Vec<Arc<Field>>>(); - let field_id = &text_fields.first().unwrap().id.clone(); - let calculation_id = "calc_id".to_owned(); - - test - .insert_calculation(UpdateCalculationChangesetPB { - view_id: view_id.clone(), - field_id: field_id.clone(), - calculation_id: Some(calculation_id.clone()), - calculation_type: CalculationType::Count, - }) - .await; - test.assert_calculation_value("7").await; - test.duplicate_row(&test.rows[1].id).await; - - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - test.assert_calculation_value("8").await; + let scripts = vec![ + // Insert Sum calculation first time + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Sum, + }, + }, + AssertCalculationValue { + expected: expected_sum, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Min, + }, + }, + AssertCalculationValue { + expected: expected_min, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Average, + }, + }, + AssertCalculationValue { + expected: expected_average, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Max, + }, + }, + AssertCalculationValue { + expected: expected_max, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id), + calculation_type: CalculationType::Median, + }, + }, + AssertCalculationValue { + expected: expected_median, + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs index d11ca64c4a..978acd8463 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs @@ -1,4 +1,3 @@ -use collab_database::rows::RowId; use tokio::sync::broadcast::Receiver; use flowy_database2::entities::UpdateCalculationChangesetPB; @@ -6,6 +5,15 @@ use flowy_database2::services::database_view::DatabaseViewChanged; use crate::database::database_editor::DatabaseEditorTest; +pub enum CalculationScript { + InsertCalculation { + payload: UpdateCalculationChangesetPB, + }, + AssertCalculationValue { + expected: f64, + }, +} + pub struct DatabaseCalculationTest { inner: DatabaseEditorTest, recv: Option<Receiver<DatabaseViewChanged>>, @@ -24,35 +32,30 @@ impl DatabaseCalculationTest { self.view_id.clone() } - pub async fn insert_calculation(&mut self, payload: UpdateCalculationChangesetPB) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.editor.update_calculation(payload).await.unwrap(); + pub async fn run_scripts(&mut self, scripts: Vec<CalculationScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn assert_calculation_float_value(&mut self, expected: f64) { - let calculations = self.editor.get_all_calculations(&self.view_id()).await; - let calculation = calculations.items.first().unwrap(); - assert_eq!(calculation.value, format!("{:.2}", expected)); - } - - pub async fn assert_calculation_value(&mut self, expected: &str) { - let calculations = self.editor.get_all_calculations(&self.view_id()).await; - let calculation = calculations.items.first().unwrap(); - assert_eq!(calculation.value, expected); - } - - pub async fn duplicate_row(&self, row_id: &RowId) { - self - .editor - .duplicate_row(&self.view_id, row_id) - .await - .unwrap(); + pub async fn run_script(&mut self, script: CalculationScript) { + match script { + CalculationScript::InsertCalculation { payload } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id()) + .await + .unwrap(), + ); + self.editor.update_calculation(payload).await.unwrap(); + }, + CalculationScript::AssertCalculationValue { expected } => { + let calculations = self.editor.get_all_calculations(&self.view_id()).await; + let calculation = calculations.items.first().unwrap(); + assert_eq!(calculation.value, format!("{:.5}", expected)); + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs index 68b3ebe8ca..833ce832b5 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/script.rs @@ -1,7 +1,19 @@ -use crate::database::database_editor::DatabaseEditorTest; use collab_database::rows::RowId; + use lib_infra::box_any::BoxAny; +use crate::database::database_editor::DatabaseEditorTest; + +pub enum CellScript { + UpdateCell { + view_id: String, + field_id: String, + row_id: RowId, + changeset: BoxAny, + is_err: bool, + }, +} + pub struct DatabaseCellTest { inner: DatabaseEditorTest, } @@ -12,18 +24,41 @@ impl DatabaseCellTest { Self { inner } } - pub async fn update_cell( - &self, - view_id: &str, - field_id: &str, - row_id: &RowId, - changeset: BoxAny, - ) { - self - .editor - .update_cell_with_changeset(view_id, row_id, field_id, changeset) - .await - .unwrap(); + pub async fn run_scripts(&mut self, scripts: Vec<CellScript>) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: CellScript) { + // let grid_manager = self.sdk.grid_manager.clone(); + // let pool = self.sdk.user_session.db_pool().unwrap(); + // let rev_manager = self.editor.rev_manager(); + // let _cache = rev_manager.revision_cache().await; + + match script { + CellScript::UpdateCell { + view_id, + field_id, + row_id, + changeset, + is_err: _, + } => { + self + .editor + .update_cell_with_changeset(&view_id, &row_id, &field_id, changeset) + .await + .unwrap(); + }, + // CellScript::AssertGridRevisionPad => { + // sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await; + // let mut grid_rev_manager = grid_manager + // .make_grid_rev_manager(&self.grid_id, pool.clone()) + // .unwrap(); + // let grid_pad = grid_rev_manager.load::<GridPadBuilder>(None).await.unwrap(); + // println!("{}", grid_pad.delta_str()); + // }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs index 379d558b8e..1c1f633e47 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -1,27 +1,24 @@ -use crate::database::cell_test::script::DatabaseCellTest; -use collab_database::fields::date_type_option::DateCellData; -use collab_database::fields::media_type_option::{MediaFile, MediaFileType, MediaUploadType}; -use collab_database::fields::select_type_option::{MultiSelectTypeOption, SingleSelectTypeOption}; -use collab_database::fields::url_type_option::URLCellData; -use collab_database::template::time_parse::TimeCellData; -use flowy_database2::entities::{FieldType, MediaCellChangeset}; -use flowy_database2::services::field::checklist_filter::{ - ChecklistCellChangeset, ChecklistCellInsertChangeset, -}; -use flowy_database2::services::field::date_filter::DateCellChangeset; +use std::time::Duration; + +use flowy_database2::entities::FieldType; use flowy_database2::services::field::{ - RelationCellChangeset, SelectOptionCellChangeset, StringCellData, + ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption, + RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData, + TimeCellData, URLCellData, }; use lib_infra::box_any::BoxAny; -use std::time::Duration; + +use crate::database::cell_test::script::CellScript::UpdateCell; +use crate::database::cell_test::script::DatabaseCellTest; #[tokio::test] async fn grid_cell_update() { - let test = DatabaseCellTest::new().await; - let fields = test.get_fields().await; - let rows = &test.rows; + let mut test = DatabaseCellTest::new().await; + let fields = test.get_fields(); + let rows = &test.row_details; - for row in rows.iter() { + let mut scripts = vec![]; + for row_detail in rows.iter() { for field in &fields { let field_type = FieldType::from(field.field_type); if field_type == FieldType::LastEditedTime || field_type == FieldType::CreatedTime { @@ -31,7 +28,7 @@ async fn grid_cell_update() { FieldType::RichText => BoxAny::new("".to_string()), FieldType::Number => BoxAny::new("123".to_string()), FieldType::DateTime => BoxAny::new(DateCellChangeset { - timestamp: Some(123), + date: Some(123), ..Default::default() }), FieldType::SingleSelect => { @@ -51,10 +48,7 @@ async fn grid_cell_update() { )) }, FieldType::Checklist => BoxAny::new(ChecklistCellChangeset { - insert_tasks: vec![ChecklistCellInsertChangeset::new( - "new option".to_string(), - false, - )], + insert_options: vec![("new option".to_string(), false)], ..Default::default() }), FieldType::Checkbox => BoxAny::new("1".to_string()), @@ -63,31 +57,26 @@ async fn grid_cell_update() { inserted_row_ids: vec!["abcdefabcdef".to_string().into()], ..Default::default() }), - FieldType::Media => BoxAny::new(MediaCellChangeset { - inserted_files: vec![MediaFile { - id: "abcdefghijk".to_string(), - name: "link".to_string(), - url: "https://www.appflowy.io".to_string(), - file_type: MediaFileType::Link, - upload_type: MediaUploadType::Network, - }], - removed_ids: vec![], - }), _ => BoxAny::new("".to_string()), }; - // Call the new `update_cell` function directly - test - .update_cell(&test.view_id, &field.id, &row.id, cell_changeset) - .await; + scripts.push(UpdateCell { + view_id: test.view_id.clone(), + field_id: field.id.clone(), + row_id: row_detail.row.id.clone(), + changeset: cell_changeset, + is_err: false, + }); } } + + test.run_scripts(scripts).await; } #[tokio::test] async fn text_cell_data_test() { let test = DatabaseCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; + let text_field = test.get_first_field(FieldType::RichText); let cells = test .editor @@ -111,7 +100,7 @@ async fn text_cell_data_test() { #[tokio::test] async fn url_cell_data_test() { let test = DatabaseCellTest::new().await; - let url_field = test.get_first_field(FieldType::URL).await; + let url_field = test.get_first_field(FieldType::URL); let cells = test .editor .get_cells_for_field(&test.view_id, &url_field.id) @@ -132,8 +121,8 @@ async fn url_cell_data_test() { #[tokio::test] async fn update_updated_at_field_on_other_cell_update() { - let test = DatabaseCellTest::new().await; - let updated_at_field = test.get_first_field(FieldType::LastEditedTime).await; + let mut test = DatabaseCellTest::new().await; + let updated_at_field = test.get_first_field(FieldType::LastEditedTime); let text_field = test .fields @@ -142,15 +131,14 @@ async fn update_updated_at_field_on_other_cell_update() { .unwrap(); let before_update_timestamp = chrono::offset::Utc::now().timestamp(); - - // Directly call the `update_cell` function test - .update_cell( - &test.view_id, - &text_field.id, - &test.rows[0].id, - BoxAny::new("change".to_string()), - ) + .run_script(UpdateCell { + view_id: test.view_id.clone(), + row_id: test.row_details[0].row.id.clone(), + field_id: text_field.id.clone(), + changeset: BoxAny::new("change".to_string()), + is_err: false, + }) .await; let cells = test @@ -178,12 +166,37 @@ async fn update_updated_at_field_on_other_cell_update() { timestamp, after_update_timestamp ), - _ => assert!( + 1 => assert!( timestamp <= before_update_timestamp, "{} <= {}", timestamp, before_update_timestamp ), + 2 => assert!( + timestamp <= before_update_timestamp, + "{} <= {}", + timestamp, + before_update_timestamp + ), + 3 => assert!( + timestamp <= before_update_timestamp, + "{} <= {}", + timestamp, + before_update_timestamp + ), + 4 => assert!( + timestamp <= before_update_timestamp, + "{} <= {}", + timestamp, + before_update_timestamp + ), + 5 => assert!( + timestamp <= before_update_timestamp, + "{} <= {}", + timestamp, + before_update_timestamp + ), + _ => {}, } } } @@ -191,7 +204,7 @@ async fn update_updated_at_field_on_other_cell_update() { #[tokio::test] async fn time_cell_data_test() { let test = DatabaseCellTest::new().await; - let time_field = test.get_first_field(FieldType::Time).await; + let time_field = test.get_first_field(FieldType::Time); let cells = test .editor .get_cells_for_field(&test.view_id, &time_field.id) diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 852a4ea591..2d087cce00 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -2,23 +2,23 @@ use std::collections::HashMap; use std::sync::Arc; use collab_database::database::gen_database_view_id; -use collab_database::fields::checkbox_type_option::CheckboxTypeOption; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SingleSelectTypeOption, -}; use collab_database::fields::Field; -use collab_database::rows::{Row, RowId}; +use collab_database::rows::{RowDetail, RowId}; use lib_infra::box_any::BoxAny; use strum::EnumCount; use event_integration_test::folder_event::ViewTest; use event_integration_test::EventIntegrationTest; -use flowy_database2::entities::{DatabasePB, FieldType, FilterPB, RowMetaPB}; +use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB}; use flowy_database2::services::database::DatabaseEditor; -use flowy_database2::services::field::checklist_filter::ChecklistCellChangeset; -use flowy_database2::services::field::SelectOptionCellChangeset; +use flowy_database2::services::field::checklist_type_option::{ + ChecklistCellChangeset, ChecklistTypeOption, +}; +use flowy_database2::services::field::{ + CheckboxTypeOption, MultiSelectTypeOption, SelectOption, SelectOptionCellChangeset, + SingleSelectTypeOption, +}; use flowy_database2::services::share::csv::{CSVFormat, ImportResult}; use flowy_error::FlowyResult; @@ -31,7 +31,7 @@ pub struct DatabaseEditorTest { pub view_id: String, pub editor: Arc<DatabaseEditor>, pub fields: Vec<Arc<Field>>, - pub rows: Vec<Arc<Row>>, + pub row_details: Vec<Arc<RowDetail>>, pub field_count: usize, pub row_by_row_id: HashMap<String, RowMetaPB>, } @@ -76,54 +76,45 @@ impl DatabaseEditorTest { pub async fn new(sdk: EventIntegrationTest, test: ViewTest) -> Self { let editor = sdk .database_manager - .get_database_editor_with_view_id(&test.child_view.id) + .get_database_with_view_id(&test.child_view.id) .await .unwrap(); let fields = editor .get_fields(&test.child_view.id, None) - .await .into_iter() .map(Arc::new) .collect(); let rows = editor - .get_all_rows(&test.child_view.id) + .get_rows(&test.child_view.id) .await .unwrap() .into_iter() .collect(); let view_id = test.child_view.id; - let this = Self { + Self { sdk, - view_id: view_id.clone(), + view_id, editor, fields, - rows, + row_details: rows, field_count: FieldType::COUNT, row_by_row_id: HashMap::default(), - }; - this.get_database_data(&view_id).await; - this + } } - pub async fn get_database_data(&self, view_id: &str) -> DatabasePB { - self.editor.open_database_view(view_id, None).await.unwrap() - } - - #[allow(dead_code)] pub async fn database_filters(&self) -> Vec<FilterPB> { self.editor.get_all_filters(&self.view_id).await.items } - pub async fn get_rows(&self) -> Vec<Arc<Row>> { - self.editor.get_all_rows(&self.view_id).await.unwrap() + pub async fn get_rows(&self) -> Vec<Arc<RowDetail>> { + self.editor.get_rows(&self.view_id).await.unwrap() } - pub async fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { + pub fn get_field(&self, field_id: &str, field_type: FieldType) -> Field { self .editor .get_fields(&self.view_id, None) - .await .into_iter() .filter(|field| { let t_field_type = FieldType::from(field.field_type); @@ -136,11 +127,10 @@ impl DatabaseEditorTest { /// returns the first `Field` in the build-in test grid. /// Not support duplicate `FieldType` in test grid yet. - pub async fn get_first_field(&self, field_type: FieldType) -> Field { + pub fn get_first_field(&self, field_type: FieldType) -> Field { self .editor .get_fields(&self.view_id, None) - .await .into_iter() .filter(|field| { let t_field_type = FieldType::from(field.field_type); @@ -151,50 +141,48 @@ impl DatabaseEditorTest { .unwrap() } - pub async fn get_fields(&self) -> Vec<Field> { - self.editor.get_fields(&self.view_id, None).await + pub fn get_fields(&self) -> Vec<Field> { + self.editor.get_fields(&self.view_id, None) } - pub async fn get_multi_select_type_option(&self, field_id: &str) -> Vec<SelectOption> { + pub fn get_multi_select_type_option(&self, field_id: &str) -> Vec<SelectOption> { let field_type = FieldType::MultiSelect; - let field = self.get_field(field_id, field_type).await; + let field = self.get_field(field_id, field_type); let type_option = field .get_type_option::<MultiSelectTypeOption>(field_type) - .unwrap() - .0; + .unwrap(); type_option.options } - pub async fn get_single_select_type_option(&self, field_id: &str) -> Vec<SelectOption> { + pub fn get_single_select_type_option(&self, field_id: &str) -> Vec<SelectOption> { let field_type = FieldType::SingleSelect; - let field = self.get_field(field_id, field_type).await; + let field = self.get_field(field_id, field_type); let type_option = field .get_type_option::<SingleSelectTypeOption>(field_type) - .unwrap() - .0; + .unwrap(); type_option.options } #[allow(dead_code)] - pub async fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOption { + pub fn get_checklist_type_option(&self, field_id: &str) -> ChecklistTypeOption { let field_type = FieldType::Checklist; - let field = self.get_field(field_id, field_type).await; + let field = self.get_field(field_id, field_type); field .get_type_option::<ChecklistTypeOption>(field_type) .unwrap() } #[allow(dead_code)] - pub async fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOption { + pub fn get_checkbox_type_option(&self, field_id: &str) -> CheckboxTypeOption { let field_type = FieldType::Checkbox; - let field = self.get_field(field_id, field_type).await; + let field = self.get_field(field_id, field_type); field .get_type_option::<CheckboxTypeOption>(field_type) .unwrap() } pub async fn update_cell( - &self, + &mut self, field_id: &str, row_id: RowId, cell_changeset: BoxAny, @@ -202,7 +190,6 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) - .await .into_iter() .find(|field| field.id == field_id) .unwrap(); @@ -217,7 +204,6 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) - .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -239,7 +225,6 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) - .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -248,7 +233,7 @@ impl DatabaseEditorTest { .unwrap() .clone(); let cell_changeset = ChecklistCellChangeset { - completed_task_ids: selected_options, + selected_option_ids: selected_options, ..Default::default() }; self @@ -265,7 +250,6 @@ impl DatabaseEditorTest { let field = self .editor .get_fields(&self.view_id, None) - .await .iter() .find(|field| { let field_type = FieldType::from(field.field_type); @@ -293,7 +277,7 @@ impl DatabaseEditorTest { self .sdk .database_manager - .get_or_init_database_editor(database_id) + .get_database(database_id) .await .ok() } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs index c6755ef009..b87684163b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/script.rs @@ -1,102 +1,79 @@ -use crate::database::database_editor::DatabaseEditorTest; -use collab_database::fields::{Field, TypeOptionData}; -use flowy_database2::entities::{CreateFieldParams, FieldChangesetPB, FieldType}; -use flowy_database2::services::cell::stringify_cell; +use flowy_database2::entities::{FieldSettingsChangesetPB, FieldVisibility}; -pub struct DatabaseFieldTest { +use crate::database::database_editor::DatabaseEditorTest; + +pub struct FieldSettingsTest { inner: DatabaseEditorTest, } -impl DatabaseFieldTest { - pub async fn new() -> Self { - let editor_test = DatabaseEditorTest::new_grid().await; - Self { inner: editor_test } +impl FieldSettingsTest { + pub async fn new_grid() -> Self { + let inner = DatabaseEditorTest::new_grid().await; + Self { inner } } - pub fn view_id(&self) -> String { - self.view_id.clone() + pub async fn new_board() -> Self { + let inner = DatabaseEditorTest::new_board().await; + Self { inner } } - pub fn field_count(&self) -> usize { - self.field_count + pub async fn new_calendar() -> Self { + let inner = DatabaseEditorTest::new_calendar().await; + Self { inner } } - pub async fn create_field(&mut self, params: CreateFieldParams) { - self.field_count += 1; - let _ = self.editor.create_field_with_type_option(params).await; - let fields = self.editor.get_fields(&self.view_id, None).await; - assert_eq!(self.field_count, fields.len()); - } - - pub async fn update_field(&mut self, changeset: FieldChangesetPB) { - self.editor.update_field(changeset).await.unwrap(); - } - - pub async fn delete_field(&mut self, field: Field) { - if self.editor.get_field(&field.id).await.is_some() { - self.field_count -= 1; - } - - self.editor.delete_field(&field.id).await.unwrap(); - let fields = self.editor.get_fields(&self.view_id, None).await; - assert_eq!(self.field_count, fields.len()); - } - - pub async fn switch_to_field( + pub async fn assert_field_settings( &mut self, - view_id: String, - field_id: String, - new_field_type: FieldType, + field_ids: Vec<String>, + visibility: FieldVisibility, + width: i32, ) { - self + let field_settings = self .editor - .switch_to_field_type(&view_id, &field_id, new_field_type, None) + .get_field_settings(&self.view_id, field_ids) .await .unwrap(); + + for field_setting in field_settings { + assert_eq!(field_setting.width, width); + assert_eq!(field_setting.visibility, visibility); + } } - pub async fn update_type_option(&mut self, field_id: String, type_option: TypeOptionData) { - let old_field = self.editor.get_field(&field_id).await.unwrap(); - self + pub async fn assert_all_field_settings(&mut self, visibility: FieldVisibility, width: i32) { + let field_settings = self .editor - .update_field_type_option(&field_id, type_option, old_field) + .get_all_field_settings(&self.view_id) .await .unwrap(); + + for field_setting in field_settings { + assert_eq!(field_setting.width, width); + assert_eq!(field_setting.visibility, visibility); + } } - pub async fn assert_field_count(&self, count: usize) { - assert_eq!(self.get_fields().await.len(), count); - } - - pub async fn assert_field_type_option_equal( - &self, - field_index: usize, - expected_type_option_data: TypeOptionData, - ) { - let fields = self.get_fields().await; - let field = &fields[field_index]; - let type_option_data = field.get_any_type_option(field.field_type).unwrap(); - assert_eq!(type_option_data, expected_type_option_data); - } - - pub async fn assert_cell_content( - &self, + pub async fn update_field_settings( + &mut self, field_id: String, - row_index: usize, - expected_content: String, + visibility: Option<FieldVisibility>, + width: Option<i32>, ) { - let field = self.editor.get_field(&field_id).await.unwrap(); - - let rows = self.editor.get_all_rows(&self.view_id()).await.unwrap(); - let row = rows.get(row_index).unwrap(); - - let cell = row.cells.get(&field_id).unwrap().clone(); - let content = stringify_cell(&cell, &field); - assert_eq!(content, expected_content); + let params = FieldSettingsChangesetPB { + view_id: self.view_id.clone(), + field_id, + visibility, + width, + wrap_cell_content: None, + }; + let _ = self + .editor + .update_field_settings_with_changeset(params) + .await; } } -impl std::ops::Deref for DatabaseFieldTest { +impl std::ops::Deref for FieldSettingsTest { type Target = DatabaseEditorTest; fn deref(&self) -> &Self::Target { @@ -104,7 +81,7 @@ impl std::ops::Deref for DatabaseFieldTest { } } -impl std::ops::DerefMut for DatabaseFieldTest { +impl std::ops::DerefMut for FieldSettingsTest { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs index ec2f188be8..b550567699 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_settings_test/test.rs @@ -1,253 +1,119 @@ -use crate::database::field_settings_test::script::DatabaseFieldTest; -use crate::database::field_test::util::*; -use collab_database::database::gen_option_id; -use collab_database::fields::select_type_option::SingleSelectTypeOption; -use collab_database::fields::select_type_option::{SelectOption, SelectTypeOption}; -use flowy_database2::entities::{FieldChangesetPB, FieldType}; -use flowy_database2::services::field::{CHECK, UNCHECK}; +use flowy_database2::entities::FieldType; +use flowy_database2::entities::FieldVisibility; +use flowy_database2::services::field_settings::DEFAULT_WIDTH; + +use crate::database::field_settings_test::script::FieldSettingsTest; + +/// Check default field settings for grid, kanban and calendar +#[tokio::test] +async fn get_default_grid_field_settings() { + // grid + let mut test = FieldSettingsTest::new_grid().await; + test + .assert_all_field_settings(FieldVisibility::AlwaysShown, DEFAULT_WIDTH) + .await; +} #[tokio::test] -async fn grid_create_field() { - let mut test = DatabaseFieldTest::new().await; - - // Create and assert a text field - let (params, field) = create_text_field(&test.view_id()); - test.create_field(params).await; +async fn get_default_board_field_settings() { + // board + let mut test = FieldSettingsTest::new_board().await; + let non_primary_field_ids: Vec<String> = test + .get_fields() + .into_iter() + .filter(|field| !field.is_primary) + .map(|field| field.id) + .collect(); + let primary_field_id = test.get_first_field(FieldType::RichText).id; test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), + .assert_field_settings( + non_primary_field_ids.clone(), + FieldVisibility::HideWhenEmpty, + DEFAULT_WIDTH, ) .await; - - // Create and assert a single select field - let (params, field) = create_single_select_field(&test.view_id()); - test.create_field(params).await; test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; - - // Create and assert a timestamp field - let (params, field) = create_timestamp_field(&test.view_id(), FieldType::CreatedTime); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; - - // Create and assert a time field - let (params, field) = create_time_field(&test.view_id()); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), + .assert_field_settings( + vec![primary_field_id.clone()], + FieldVisibility::AlwaysShown, + DEFAULT_WIDTH, ) .await; } #[tokio::test] -async fn grid_create_duplicate_field() { - let mut test = DatabaseFieldTest::new().await; - let (params, _) = create_text_field(&test.view_id()); - let field_count = test.field_count(); - let expected_field_count = field_count + 1; - - test.create_field(params.clone()).await; - test.assert_field_count(expected_field_count).await; -} - -#[tokio::test] -async fn grid_update_field_with_empty_change() { - let mut test = DatabaseFieldTest::new().await; - let (params, _) = create_single_select_field(&test.view_id()); - let create_field_index = test.field_count(); - test.create_field(params).await; - - let field = test.get_fields().await.pop().unwrap().clone(); - let changeset = FieldChangesetPB { - field_id: field.id.clone(), - view_id: test.view_id(), - ..Default::default() - }; - - test.update_field(changeset).await; +async fn get_default_calendar_field_settings() { + // calendar + let mut test = FieldSettingsTest::new_calendar().await; + let non_primary_field_ids: Vec<String> = test + .get_fields() + .into_iter() + .filter(|field| !field.is_primary) + .map(|field| field.id) + .collect(); + let primary_field_id = test.get_first_field(FieldType::RichText).id; test - .assert_field_type_option_equal( - create_field_index, - field.get_any_type_option(field.field_type).unwrap(), + .assert_field_settings( + non_primary_field_ids.clone(), + FieldVisibility::HideWhenEmpty, + DEFAULT_WIDTH, + ) + .await; + test + .assert_field_settings( + vec![primary_field_id.clone()], + FieldVisibility::AlwaysShown, + DEFAULT_WIDTH, ) .await; } +/// Update field settings for a field #[tokio::test] -async fn grid_delete_field() { - let mut test = DatabaseFieldTest::new().await; - let original_field_count = test.field_count(); - let (params, _) = create_text_field(&test.view_id()); - test.create_field(params).await; - - let field = test.get_fields().await.pop().unwrap(); - test.delete_field(field).await; - test.assert_field_count(original_field_count).await; -} - -#[tokio::test] -async fn grid_switch_from_select_option_to_checkbox_test() { - let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect).await; - let view_id = test.view_id(); - - // Update the type option data of the single select option - let mut options = test.get_single_select_type_option(&field.id).await; - options.clear(); - options.push(SelectOption { - id: gen_option_id(), - name: CHECK.to_string(), - color: Default::default(), - }); - options.push(SelectOption { - id: gen_option_id(), - name: UNCHECK.to_string(), - color: Default::default(), - }); +async fn update_field_settings_test() { + let mut test = FieldSettingsTest::new_board().await; + let non_primary_field_ids: Vec<String> = test + .get_fields() + .into_iter() + .filter(|field| !field.is_primary) + .map(|field| field.id) + .collect(); + let primary_field_id = test.get_first_field(FieldType::RichText).id; test - .update_type_option( - field.id.clone(), - SingleSelectTypeOption(SelectTypeOption { - options, - disable_color: false, - }) - .into(), + .assert_field_settings( + non_primary_field_ids.clone(), + FieldVisibility::HideWhenEmpty, + DEFAULT_WIDTH, + ) + .await; + test + .assert_field_settings( + vec![primary_field_id.clone()], + FieldVisibility::AlwaysShown, + DEFAULT_WIDTH, ) .await; - // Switch to checkbox field test - .switch_to_field(view_id, field.id.clone(), FieldType::Checkbox) - .await; -} - -#[tokio::test] -async fn grid_switch_from_checkbox_to_select_option_test() { - let mut test = DatabaseFieldTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await.clone(); - - // Switch to single-select field - test - .switch_to_field( - test.view_id(), - checkbox_field.id.clone(), - FieldType::SingleSelect, + .update_field_settings( + primary_field_id.clone(), + Some(FieldVisibility::HideWhenEmpty), + None, ) .await; - - // Assert cell content after switching the field type test - .assert_cell_content( - checkbox_field.id.clone(), - 1, // row_index - CHECK.to_string(), // expected content + .assert_field_settings( + non_primary_field_ids.clone(), + FieldVisibility::HideWhenEmpty, + DEFAULT_WIDTH, ) .await; - - // Check that the options contain both "CHECK" and "UNCHECK" - let options = test.get_single_select_type_option(&checkbox_field.id).await; - assert_eq!(options.len(), 2); - assert!(options.iter().any(|option| option.name == UNCHECK)); - assert!(options.iter().any(|option| option.name == CHECK)); -} - -#[tokio::test] -async fn grid_switch_from_multi_select_to_text_test() { - let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::MultiSelect).await.clone(); - let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id).await; - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content( - field_rev.id.clone(), - 0, // row_index - format!( - "{},{}", - multi_select_type_option.first().unwrap().name, - multi_select_type_option.get(1).unwrap().name - ), + .assert_field_settings( + vec![primary_field_id.clone()], + FieldVisibility::HideWhenEmpty, + DEFAULT_WIDTH, ) .await; } - -#[tokio::test] -async fn grid_switch_from_checkbox_to_text_test() { - let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checkbox).await; - - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field_rev.id.clone(), 1, "Yes".to_string()) - .await; - test - .assert_cell_content(field_rev.id.clone(), 2, "No".to_string()) - .await; -} - -#[tokio::test] -async fn grid_switch_from_date_to_text_test() { - let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::DateTime).await.clone(); - - test - .switch_to_field(test.view_id(), field.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field.id.clone(), 2, "2022/03/14".to_string()) - .await; - test - .assert_cell_content(field.id.clone(), 3, "2022/11/17".to_string()) - .await; -} - -#[tokio::test] -async fn grid_switch_from_number_to_text_test() { - let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::Number).await.clone(); - - test - .switch_to_field(test.view_id(), field.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field.id.clone(), 0, "$1".to_string()) - .await; - test - .assert_cell_content(field.id.clone(), 4, "".to_string()) - .await; -} - -#[tokio::test] -async fn grid_switch_from_checklist_to_text_test() { - let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checklist).await; - - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field_rev.id.clone(), 0, "First thing".to_string()) - .await; -} diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs index c6755ef009..554b5a7b21 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/script.rs @@ -1,8 +1,40 @@ -use crate::database::database_editor::DatabaseEditorTest; use collab_database::fields::{Field, TypeOptionData}; -use flowy_database2::entities::{CreateFieldParams, FieldChangesetPB, FieldType}; + +use flowy_database2::entities::{CreateFieldParams, FieldChangesetParams, FieldType}; use flowy_database2::services::cell::stringify_cell; +use crate::database::database_editor::DatabaseEditorTest; + +pub enum FieldScript { + CreateField { + params: CreateFieldParams, + }, + UpdateField { + changeset: FieldChangesetParams, + }, + DeleteField { + field: Field, + }, + SwitchToField { + field_id: String, + new_field_type: FieldType, + }, + UpdateTypeOption { + field_id: String, + type_option: TypeOptionData, + }, + AssertFieldCount(usize), + AssertFieldTypeOptionEqual { + field_index: usize, + expected_type_option_data: TypeOptionData, + }, + AssertCellContent { + field_id: String, + row_index: usize, + expected_content: String, + }, +} + pub struct DatabaseFieldTest { inner: DatabaseEditorTest, } @@ -21,78 +53,82 @@ impl DatabaseFieldTest { self.field_count } - pub async fn create_field(&mut self, params: CreateFieldParams) { - self.field_count += 1; - let _ = self.editor.create_field_with_type_option(params).await; - let fields = self.editor.get_fields(&self.view_id, None).await; - assert_eq!(self.field_count, fields.len()); - } - - pub async fn update_field(&mut self, changeset: FieldChangesetPB) { - self.editor.update_field(changeset).await.unwrap(); - } - - pub async fn delete_field(&mut self, field: Field) { - if self.editor.get_field(&field.id).await.is_some() { - self.field_count -= 1; + pub async fn run_scripts(&mut self, scripts: Vec<FieldScript>) { + for script in scripts { + self.run_script(script).await; } - - self.editor.delete_field(&field.id).await.unwrap(); - let fields = self.editor.get_fields(&self.view_id, None).await; - assert_eq!(self.field_count, fields.len()); } - pub async fn switch_to_field( - &mut self, - view_id: String, - field_id: String, - new_field_type: FieldType, - ) { - self - .editor - .switch_to_field_type(&view_id, &field_id, new_field_type, None) - .await - .unwrap(); - } + pub async fn run_script(&mut self, script: FieldScript) { + match script { + FieldScript::CreateField { params } => { + self.field_count += 1; + let _ = self.editor.create_field_with_type_option(params).await; + let fields = self.editor.get_fields(&self.view_id, None); + assert_eq!(self.field_count, fields.len()); + }, + FieldScript::UpdateField { changeset: change } => { + self.editor.update_field(change).await.unwrap(); + }, + FieldScript::DeleteField { field } => { + if self.editor.get_field(&field.id).is_some() { + self.field_count -= 1; + } - pub async fn update_type_option(&mut self, field_id: String, type_option: TypeOptionData) { - let old_field = self.editor.get_field(&field_id).await.unwrap(); - self - .editor - .update_field_type_option(&field_id, type_option, old_field) - .await - .unwrap(); - } + self.editor.delete_field(&field.id).await.unwrap(); + let fields = self.editor.get_fields(&self.view_id, None); + assert_eq!(self.field_count, fields.len()); + }, + FieldScript::SwitchToField { + field_id, + new_field_type, + } => { + // + self + .editor + .switch_to_field_type(&field_id, new_field_type) + .await + .unwrap(); + }, + FieldScript::UpdateTypeOption { + field_id, + type_option, + } => { + // + let old_field = self.editor.get_field(&field_id).unwrap(); + self + .editor + .update_field_type_option(&field_id, type_option, old_field) + .await + .unwrap(); + }, + FieldScript::AssertFieldCount(count) => { + assert_eq!(self.get_fields().len(), count); + }, + FieldScript::AssertFieldTypeOptionEqual { + field_index, + expected_type_option_data, + } => { + let fields = self.get_fields(); + let field = &fields[field_index]; + let type_option_data = field.get_any_type_option(field.field_type).unwrap(); + assert_eq!(type_option_data, expected_type_option_data); + }, + FieldScript::AssertCellContent { + field_id, + row_index, + expected_content, + } => { + let field = self.editor.get_field(&field_id).unwrap(); - pub async fn assert_field_count(&self, count: usize) { - assert_eq!(self.get_fields().await.len(), count); - } + let rows = self.editor.get_rows(&self.view_id()).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); - pub async fn assert_field_type_option_equal( - &self, - field_index: usize, - expected_type_option_data: TypeOptionData, - ) { - let fields = self.get_fields().await; - let field = &fields[field_index]; - let type_option_data = field.get_any_type_option(field.field_type).unwrap(); - assert_eq!(type_option_data, expected_type_option_data); - } - - pub async fn assert_cell_content( - &self, - field_id: String, - row_index: usize, - expected_content: String, - ) { - let field = self.editor.get_field(&field_id).await.unwrap(); - - let rows = self.editor.get_all_rows(&self.view_id()).await.unwrap(); - let row = rows.get(row_index).unwrap(); - - let cell = row.cells.get(&field_id).unwrap().clone(); - let content = stringify_cell(&cell, &field); - assert_eq!(content, expected_content); + let cell = row_detail.row.cells.get(&field_id).unwrap().clone(); + let content = stringify_cell(&cell, &field); + assert_eq!(content, expected_content); + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index 488ae4d577..7cd9f9f3d1 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -1,55 +1,65 @@ use collab_database::database::gen_option_id; -use collab_database::fields::select_type_option::{SelectOption, SelectTypeOption}; -use flowy_database2::entities::{FieldChangesetPB, FieldType}; -use flowy_database2::services::field::{CHECK, UNCHECK}; + +use flowy_database2::entities::{FieldChangesetParams, FieldType}; +use flowy_database2::services::field::{SelectOption, SingleSelectTypeOption, CHECK, UNCHECK}; use crate::database::field_test::script::DatabaseFieldTest; +use crate::database::field_test::script::FieldScript::*; use crate::database::field_test::util::*; -use collab_database::fields::select_type_option::SingleSelectTypeOption; #[tokio::test] async fn grid_create_field() { let mut test = DatabaseFieldTest::new().await; - - // Create and assert text field let (params, field) = create_text_field(&test.view_id()); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; - // Create and assert single select field + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; + let (params, field) = create_single_select_field(&test.view_id()); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; - // Create and assert timestamp field let (params, field) = create_timestamp_field(&test.view_id(), FieldType::CreatedTime); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; - // Create and assert time field let (params, field) = create_time_field(&test.view_id()); - test.create_field(params).await; - test - .assert_field_type_option_equal( - test.field_count() - 1, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; + + let (params, field) = create_time_field(&test.view_id()); + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] @@ -58,9 +68,13 @@ async fn grid_create_duplicate_field() { let (params, _) = create_text_field(&test.view_id()); let field_count = test.field_count(); let expected_field_count = field_count + 1; - - test.create_field(params.clone()).await; - test.assert_field_count(expected_field_count).await; + let scripts = vec![ + CreateField { + params: params.clone(), + }, + AssertFieldCount(expected_field_count), + ]; + test.run_scripts(scripts).await; } #[tokio::test] @@ -68,23 +82,24 @@ async fn grid_update_field_with_empty_change() { let mut test = DatabaseFieldTest::new().await; let (params, _) = create_single_select_field(&test.view_id()); let create_field_index = test.field_count(); + let scripts = vec![CreateField { params }]; + test.run_scripts(scripts).await; - test.create_field(params).await; - - let field = test.get_fields().await.pop().unwrap().clone(); - let changeset = FieldChangesetPB { + let field = test.get_fields().pop().unwrap().clone(); + let changeset = FieldChangesetParams { field_id: field.id.clone(), view_id: test.view_id(), ..Default::default() }; - test.update_field(changeset).await; - test - .assert_field_type_option_equal( - create_field_index, - field.get_any_type_option(field.field_type).unwrap(), - ) - .await; + let scripts = vec![ + UpdateField { changeset }, + AssertFieldTypeOptionEqual { + field_index: create_field_index, + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] @@ -92,166 +107,215 @@ async fn grid_delete_field() { let mut test = DatabaseFieldTest::new().await; let original_field_count = test.field_count(); let (params, _) = create_text_field(&test.view_id()); + let scripts = vec![CreateField { params }]; + test.run_scripts(scripts).await; - test.create_field(params).await; - - let field = test.get_fields().await.pop().unwrap(); - test.delete_field(field).await; - test.assert_field_count(original_field_count).await; + let field = test.get_fields().pop().unwrap(); + let scripts = vec![ + DeleteField { field }, + AssertFieldCount(original_field_count), + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_switch_from_select_option_to_checkbox_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect).await; - let view_id = test.view_id(); + let field = test.get_first_field(FieldType::SingleSelect); // Update the type option data of single select option - let mut options = test.get_single_select_type_option(&field.id).await; + let mut options = test.get_single_select_type_option(&field.id); options.clear(); + // Add a new option with name CHECK options.push(SelectOption { id: gen_option_id(), name: CHECK.to_string(), color: Default::default(), }); + // Add a new option with name UNCHECK options.push(SelectOption { id: gen_option_id(), name: UNCHECK.to_string(), color: Default::default(), }); - test - .update_type_option( - field.id.clone(), - SingleSelectTypeOption(SelectTypeOption { + let scripts = vec![ + UpdateTypeOption { + field_id: field.id.clone(), + type_option: SingleSelectTypeOption { options, disable_color: false, - }) + } .into(), - ) - .await; - - // Switch to checkbox field - test - .switch_to_field(view_id, field.id.clone(), FieldType::Checkbox) - .await; + }, + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::Checkbox, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_switch_from_checkbox_to_select_option_test() { let mut test = DatabaseFieldTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await.clone(); + let checkbox_field = test.get_first_field(FieldType::Checkbox).clone(); + let scripts = vec![ + // switch to single-select field type + SwitchToField { + field_id: checkbox_field.id.clone(), + new_field_type: FieldType::SingleSelect, + }, + // Assert the cell content after switch the field type. The cell content will be changed if + // the FieldType::SingleSelect implement the cell data TypeOptionTransform. Check out the + // TypeOptionTransform trait for more information. + // + // Make sure which cell of the row you want to check. + AssertCellContent { + field_id: checkbox_field.id.clone(), + // the mock data of the checkbox with row_index one is "true" + row_index: 1, + // The content of the checkbox should transform to the corresponding option name. + expected_content: CHECK.to_string(), + }, + ]; + test.run_scripts(scripts).await; - // Switch to single-select field - test - .switch_to_field( - test.view_id(), - checkbox_field.id.clone(), - FieldType::SingleSelect, - ) - .await; - - // Assert cell content after switching the field type - test - .assert_cell_content( - checkbox_field.id.clone(), - 1, // row_index - CHECK.to_string(), // expected content - ) - .await; - - // Check that the options contain both "CHECK" and "UNCHECK" - let options = test.get_single_select_type_option(&checkbox_field.id).await; + let options = test.get_single_select_type_option(&checkbox_field.id); assert_eq!(options.len(), 2); assert!(options.iter().any(|option| option.name == UNCHECK)); assert!(options.iter().any(|option| option.name == CHECK)); } +// Test when switching the current field from Multi-select to Text test +// The build-in test data is located in `make_test_grid` method(flowy-database/tests/grid_editor.rs). +// input: +// option1, option2 -> "option1.name, option2.name" #[tokio::test] async fn grid_switch_from_multi_select_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::MultiSelect).await.clone(); + let field_rev = test.get_first_field(FieldType::MultiSelect).clone(); - let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id).await; + let multi_select_type_option = test.get_multi_select_type_option(&field_rev.id); - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; + let script_switch_field = vec![SwitchToField { + field_id: field_rev.id.clone(), + new_field_type: FieldType::RichText, + }]; - test - .assert_cell_content( - field_rev.id.clone(), - 0, // row_index - format!( - "{},{}", - multi_select_type_option.first().unwrap().name, - multi_select_type_option.get(1).unwrap().name - ), - ) - .await; + test.run_scripts(script_switch_field).await; + + let script_assert_field = vec![AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 0, + expected_content: format!( + "{},{}", + multi_select_type_option.first().unwrap().name, + multi_select_type_option.get(1).unwrap().name + ), + }]; + + test.run_scripts(script_assert_field).await; } +// Test when switching the current field from Checkbox to Text test +// input: +// check -> "Yes" +// unchecked -> "No" #[tokio::test] async fn grid_switch_from_checkbox_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checkbox).await; + let field_rev = test.get_first_field(FieldType::Checkbox); - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field_rev.id.clone(), 1, "Yes".to_string()) - .await; - test - .assert_cell_content(field_rev.id.clone(), 2, "No".to_string()) - .await; + let scripts = vec![ + SwitchToField { + field_id: field_rev.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 1, + expected_content: "Yes".to_string(), + }, + AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 2, + expected_content: "No".to_string(), + }, + ]; + test.run_scripts(scripts).await; } +// Test when switching the current field from Date to Text test +// input: +// 1647251762 -> Mar 14,2022 (This string will be different base on current data setting) #[tokio::test] async fn grid_switch_from_date_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::DateTime).await.clone(); - - test - .switch_to_field(test.view_id(), field.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field.id.clone(), 2, "2022/03/14".to_string()) - .await; - test - .assert_cell_content(field.id.clone(), 3, "2022/11/17".to_string()) - .await; + let field = test.get_first_field(FieldType::DateTime).clone(); + let scripts = vec![ + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 2, + expected_content: "2022/03/14".to_string(), + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 3, + expected_content: "2022/11/17".to_string(), + }, + ]; + test.run_scripts(scripts).await; } +// Test when switching the current field from Number to Text test +// input: +// $1 -> "$1"(This string will be different base on current data setting) #[tokio::test] async fn grid_switch_from_number_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field = test.get_first_field(FieldType::Number).await.clone(); + let field = test.get_first_field(FieldType::Number).clone(); - test - .switch_to_field(test.view_id(), field.id.clone(), FieldType::RichText) - .await; + let scripts = vec![ + SwitchToField { + field_id: field.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 0, + expected_content: "$1".to_string(), + }, + AssertCellContent { + field_id: field.id.clone(), + row_index: 4, + expected_content: "".to_string(), + }, + ]; - test - .assert_cell_content(field.id.clone(), 0, "$1".to_string()) - .await; - test - .assert_cell_content(field.id.clone(), 4, "".to_string()) - .await; + test.run_scripts(scripts).await; } +/// Test when switching the current field from Checklist to Text test #[tokio::test] async fn grid_switch_from_checklist_to_text_test() { let mut test = DatabaseFieldTest::new().await; - let field_rev = test.get_first_field(FieldType::Checklist).await; + let field_rev = test.get_first_field(FieldType::Checklist); - test - .switch_to_field(test.view_id(), field_rev.id.clone(), FieldType::RichText) - .await; - - test - .assert_cell_content(field_rev.id.clone(), 0, "First thing".to_string()) - .await; + let scripts = vec![ + SwitchToField { + field_id: field_rev.id.clone(), + new_field_type: FieldType::RichText, + }, + AssertCellContent { + field_id: field_rev.id.clone(), + row_index: 0, + expected_content: "First thing".to_string(), + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs index 0728cbab95..a648f7a442 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs @@ -1,18 +1,15 @@ -use collab_database::fields::date_type_option::{ - DateFormat, DateTypeOption, TimeFormat, TimeTypeOption, -}; -use collab_database::fields::select_type_option::{SelectOption, SingleSelectTypeOption}; -use collab_database::fields::text_type_option::RichTextTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; use collab_database::fields::Field; use collab_database::views::OrderObjectPosition; use flowy_database2::entities::{CreateFieldParams, FieldType}; -use flowy_database2::services::field::{type_option_to_pb, FieldBuilder}; +use flowy_database2::services::field::{ + type_option_to_pb, DateFormat, DateTypeOption, FieldBuilder, RichTextTypeOption, SelectOption, + SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption, +}; pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) { let field_type = FieldType::RichText; - let type_option = RichTextTypeOption; + let type_option = RichTextTypeOption::default(); let text_field = FieldBuilder::new(field_type, type_option.clone()) .name("Name") .primary(true) @@ -77,8 +74,7 @@ pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFi date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, include_time: true, - field_type: field_type.into(), - timezone: None, + field_type, }; let field: Field = match field_type { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs index d077b4c61f..107e588fed 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs @@ -1,4 +1,3 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use bytes::Bytes; use flowy_database2::entities::{ CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType, @@ -8,6 +7,8 @@ use lib_infra::box_any::BoxAny; use protobuf::ProtobufError; use std::convert::TryInto; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged, FilterScript::*}; + /// Create a single advanced filter: /// /// 1. Add an OR filter @@ -26,7 +27,7 @@ async fn create_advanced_filter_test() { let create_date_filter = || -> DateFilterPB { DateFilterPB { - condition: DateFilterConditionPB::DateStartsAfter, + condition: DateFilterConditionPB::DateAfter, timestamp: Some(1651366800), ..Default::default() } @@ -39,69 +40,73 @@ async fn create_advanced_filter_test() { } }; - // Create OR Filter - test.create_or_filter(None, None).await; - test.wait(100).await; - - test - .assert_filters(vec![FilterPB { - id: "".to_string(), - filter_type: FilterType::Or, - children: vec![], - data: None, - }]) - .await; + let scripts = vec![ + CreateOrFilter { + parent_filter_id: None, + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![], + data: None, + }], + }, + ]; + test.run_scripts(scripts).await; + // OR let or_filter = test.get_filter(FilterType::Or, None).await.unwrap(); let checkbox_filter_bytes: Result<Bytes, ProtobufError> = create_checkbox_filter().try_into(); let checkbox_filter_bytes = checkbox_filter_bytes.unwrap().to_vec(); - // Create Checkbox Filter and AND Filter - test - .create_data_filter( - Some(or_filter.id.clone()), - FieldType::Checkbox, - BoxAny::new(create_checkbox_filter()), - Some(FilterRowChanged { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::Checkbox, + data: BoxAny::new(create_checkbox_filter()), + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 4, }), - ) - .await; - - test - .create_and_filter(Some(or_filter.id.clone()), None) - .await; - test.wait(100).await; - - test - .assert_filters(vec![FilterPB { - id: "".to_string(), - filter_type: FilterType::Or, - children: vec![ - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::Checkbox, - data: checkbox_filter_bytes.clone(), - }), - }, - FilterPB { - id: "".to_string(), - filter_type: FilterType::And, - children: vec![], - data: None, - }, - ], - data: None, - }]) - .await; - - test.assert_number_of_visible_rows(3).await; + }, + CreateAndFilter { + parent_filter_id: Some(or_filter.id), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes.clone(), + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 3 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR AND let and_filter = test.get_filter(FilterType::And, None).await.unwrap(); @@ -110,75 +115,70 @@ async fn create_advanced_filter_test() { let number_filter_bytes: Result<Bytes, ProtobufError> = create_number_filter().try_into(); let number_filter_bytes = number_filter_bytes.unwrap().to_vec(); - // Create Date Filter and Number Filter - test - .create_data_filter( - Some(and_filter.id.clone()), - FieldType::DateTime, - BoxAny::new(create_date_filter()), - None, - ) - .await; - - test - .create_data_filter( - Some(and_filter.id), - FieldType::Number, - BoxAny::new(create_number_filter()), - None, - ) - .await; - - test.wait(100).await; - - test - .assert_filters(vec![FilterPB { - id: "".to_string(), - filter_type: FilterType::Or, - children: vec![ - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::Checkbox, - data: checkbox_filter_bytes, - }), - }, - FilterPB { - id: "".to_string(), - filter_type: FilterType::And, - children: vec![ - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::DateTime, - data: date_filter_bytes, - }), - }, - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::Number, - data: number_filter_bytes, - }), - }, - ], - data: None, - }, - ], - data: None, - }]) - .await; - - test.assert_number_of_visible_rows(4).await; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(and_filter.id.clone()), + field_type: FieldType::DateTime, + data: BoxAny::new(create_date_filter()), + changed: None, + }, + CreateDataFilter { + parent_filter_id: Some(and_filter.id), + field_type: FieldType::Number, + data: BoxAny::new(create_number_filter()), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::DateTime, + data: date_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Number, + data: number_filter_bytes, + }), + }, + ], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY) } /// Create the same advanced filter single advanced filter: @@ -199,7 +199,7 @@ async fn create_advanced_filter_with_conversion_test() { let create_date_filter = || -> DateFilterPB { DateFilterPB { - condition: DateFilterConditionPB::DateStartsAfter, + condition: DateFilterConditionPB::DateAfter, timestamp: Some(1651366800), ..Default::default() } @@ -212,32 +212,34 @@ async fn create_advanced_filter_with_conversion_test() { } }; - // Create OR Filter - test.create_or_filter(None, None).await; + let scripts = vec![CreateOrFilter { + parent_filter_id: None, + changed: None, + }]; + test.run_scripts(scripts).await; + // IS_CHECK OR DATE > 1651366800 let or_filter = test.get_filter(FilterType::Or, None).await.unwrap(); - // Create Checkbox Filter and Date Filter - test - .create_data_filter( - Some(or_filter.id.clone()), - FieldType::Checkbox, - BoxAny::new(create_checkbox_filter()), - Some(FilterRowChanged { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::Checkbox, + data: BoxAny::new(create_checkbox_filter()), + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 4, }), - ) - .await; - - test - .create_data_filter( - Some(or_filter.id.clone()), - FieldType::DateTime, - BoxAny::new(create_date_filter()), - None, - ) - .await; + }, + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::DateTime, + data: BoxAny::new(create_date_filter()), + changed: None, + }, + ]; + test.run_scripts(scripts).await; + // OR let date_filter = test .get_filter(FilterType::Data, Some(FieldType::DateTime)) @@ -251,64 +253,62 @@ async fn create_advanced_filter_with_conversion_test() { let number_filter_bytes: Result<Bytes, ProtobufError> = create_number_filter().try_into(); let number_filter_bytes = number_filter_bytes.unwrap().to_vec(); - // Create Number Filter - test - .create_data_filter( - Some(date_filter.id), - FieldType::Number, - BoxAny::new(create_number_filter()), - None, - ) - .await; - - test.wait(100).await; - - test - .assert_filters(vec![FilterPB { - id: "".to_string(), - filter_type: FilterType::Or, - children: vec![ - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::Checkbox, - data: checkbox_filter_bytes, - }), - }, - FilterPB { - id: "".to_string(), - filter_type: FilterType::And, - children: vec![ - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::DateTime, - data: date_filter_bytes, - }), - }, - FilterPB { - id: "".to_string(), - filter_type: FilterType::Data, - children: vec![], - data: Some(FilterDataPB { - field_id: "".to_string(), - field_type: FieldType::Number, - data: number_filter_bytes, - }), - }, - ], - data: None, - }, - ], - data: None, - }]) - .await; - - test.assert_number_of_visible_rows(4).await; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(date_filter.id), + field_type: FieldType::Number, + data: BoxAny::new(create_number_filter()), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::DateTime, + data: date_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Number, + data: number_filter_bytes, + }), + }, + ], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY) } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs index 26a018d8de..881a1cebf9 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs @@ -1,51 +1,51 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, FieldType}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_checkbox_is_check_test() { let mut test = DatabaseFilterTest::new().await; let expected = 3; - let row_count = test.rows.len(); - + let row_count = test.row_details.len(); // The initial number of checked is 3 // The initial number of unchecked is 4 - test - .create_data_filter( - None, - FieldType::Checkbox, - BoxAny::new(CheckboxFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checkbox, + data: BoxAny::new(CheckboxFilterPB { condition: CheckboxFilterConditionPB::IsChecked, }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_checkbox_is_uncheck_test() { let mut test = DatabaseFilterTest::new().await; let expected = 4; - let row_count = test.rows.len(); - - test - .create_data_filter( - None, - FieldType::Checkbox, - BoxAny::new(CheckboxFilterPB { + let row_count = test.row_details.len(); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checkbox, + data: BoxAny::new(CheckboxFilterPB { condition: CheckboxFilterConditionPB::IsUnChecked, }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index 88d48bceca..3da9cab5a2 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -1,73 +1,71 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; -use collab_database::template::check_list_parse::ChecklistCellData; use flowy_database2::entities::{ChecklistFilterConditionPB, ChecklistFilterPB, FieldType}; +use flowy_database2::services::field::checklist_type_option::ChecklistCellData; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_checklist_is_incomplete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 5; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let option_ids = get_checklist_cell_options(&test).await; - // Update checklist cell with selected option IDs - test - .update_checklist_cell(test.rows[0].id.clone(), option_ids) - .await; - - // Create Checklist "Is Incomplete" filter - test - .create_data_filter( - None, - FieldType::Checklist, - BoxAny::new(ChecklistFilterPB { + let scripts = vec![ + UpdateChecklistCell { + row_id: test.row_details[0].row.id.clone(), + selected_option_ids: option_ids, + }, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checklist, + data: BoxAny::new(ChecklistFilterPB { condition: ChecklistFilterConditionPB::IsIncomplete, }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_checklist_is_complete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 2; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let option_ids = get_checklist_cell_options(&test).await; - - // Update checklist cell with selected option IDs - test - .update_checklist_cell(test.rows[0].id.clone(), option_ids) - .await; - - // Create Checklist "Is Complete" filter - test - .create_data_filter( - None, - FieldType::Checklist, - BoxAny::new(ChecklistFilterPB { + let scripts = vec![ + UpdateChecklistCell { + row_id: test.row_details[0].row.id.clone(), + selected_option_ids: option_ids, + }, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checklist, + data: BoxAny::new(ChecklistFilterPB { condition: ChecklistFilterConditionPB::IsComplete, }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } async fn get_checklist_cell_options(test: &DatabaseFilterTest) -> Vec<String> { - let field = test.get_first_field(FieldType::Checklist).await; - let row_cell = test.editor.get_cell(&field.id, &test.rows[0].id).await; + let field = test.get_first_field(FieldType::Checklist); + let row_cell = test + .editor + .get_cell(&field.id, &test.row_details[0].row.id) + .await; row_cell .map_or(ChecklistCellData::default(), |cell| { ChecklistCellData::from(&cell) diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs index deff2a5888..34964b9720 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs @@ -1,143 +1,130 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{DateFilterConditionPB, DateFilterPB, FieldType}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_date_is_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; - - // Create "Date Is" filter - test - .create_data_filter( - None, - FieldType::DateTime, - BoxAny::new(DateFilterPB { - condition: DateFilterConditionPB::DateStartsOn, + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateIs, start: None, end: None, timestamp: Some(1647251762), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_date_after_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; - - // Create "Date After" filter - test - .create_data_filter( - None, - FieldType::DateTime, - BoxAny::new(DateFilterPB { - condition: DateFilterConditionPB::DateStartsAfter, + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateAfter, start: None, end: None, timestamp: Some(1647251762), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_date_on_or_after_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; - - // Create "Date On Or After" filter - test - .create_data_filter( - None, - FieldType::DateTime, - BoxAny::new(DateFilterPB { - condition: DateFilterConditionPB::DateStartsOnOrAfter, + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateOnOrAfter, start: None, end: None, timestamp: Some(1668359085), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_date_on_or_before_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 4; - - // Create "Date On Or Before" filter - test - .create_data_filter( - None, - FieldType::DateTime, - BoxAny::new(DateFilterPB { - condition: DateFilterConditionPB::DateStartsOnOrBefore, + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateOnOrBefore, start: None, end: None, timestamp: Some(1668359085), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_date_within_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 5; - - // Create "Date Within Range" filter - test - .create_data_filter( - None, - FieldType::DateTime, - BoxAny::new(DateFilterPB { - condition: DateFilterConditionPB::DateStartsBetween, + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateWithIn, start: Some(1647251762), end: Some(1668704685), timestamp: None, }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs index 509b520361..e041ba1b4c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs @@ -1,160 +1,144 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{FieldType, NumberFilterConditionPB, NumberFilterPB}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_number_is_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; - - // Create Number "Equal" filter - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::Equal, content: "1".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_number_is_less_than_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 2; - - // Create Number "Less Than" filter - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::LessThan, content: "3".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] #[should_panic] async fn grid_filter_number_is_less_than_test2() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 2; - - // Create Number "Less Than" filter with invalid content (should panic) - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::LessThan, content: "$3".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_number_is_less_than_or_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 3; - - // Create Number "Less Than Or Equal" filter - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::LessThanOrEqualTo, content: "3".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_number_is_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 2; - - // Create Number "Is Empty" filter - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::NumberIsEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_number_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 5; - - // Create Number "Is Not Empty" filter - test - .create_data_filter( - None, - FieldType::Number, - BoxAny::new(NumberFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { condition: NumberFilterConditionPB::NumberIsNotEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index 6592c04305..f2b58070e7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -11,6 +11,7 @@ use flowy_database2::entities::{ DatabaseViewSettingPB, FieldType, FilterPB, FilterType, TextFilterConditionPB, TextFilterPB, }; use flowy_database2::services::database_view::DatabaseViewChanged; +use lib_dispatch::prelude::af_spawn; use crate::database::database_editor::DatabaseEditorTest; @@ -19,6 +20,72 @@ pub struct FilterRowChanged { pub(crate) hiding_num_of_rows: usize, } +pub enum FilterScript { + UpdateTextCell { + row_id: RowId, + text: String, + changed: Option<FilterRowChanged>, + }, + UpdateChecklistCell { + row_id: RowId, + selected_option_ids: Vec<String>, + }, + UpdateSingleSelectCell { + row_id: RowId, + option_id: String, + changed: Option<FilterRowChanged>, + }, + CreateDataFilter { + parent_filter_id: Option<String>, + field_type: FieldType, + data: BoxAny, + changed: Option<FilterRowChanged>, + }, + UpdateTextFilter { + filter: FilterPB, + condition: TextFilterConditionPB, + content: String, + changed: Option<FilterRowChanged>, + }, + CreateAndFilter { + parent_filter_id: Option<String>, + changed: Option<FilterRowChanged>, + }, + CreateOrFilter { + parent_filter_id: Option<String>, + changed: Option<FilterRowChanged>, + }, + DeleteFilter { + filter_id: String, + field_id: String, + changed: Option<FilterRowChanged>, + }, + // CreateSimpleAdvancedFilter, + // CreateComplexAdvancedFilter, + AssertFilterCount { + count: usize, + }, + AssertNumberOfVisibleRows { + expected: usize, + }, + AssertFilters { + /// 1. assert that the filter type is correct + /// 2. if the filter is data, assert that the field_type, condition and content are correct + /// (no field_id) + /// 3. if the filter is and/or, assert that each child is correct as well. + expected: Vec<FilterPB>, + }, + // AssertSimpleAdvancedFilter, + // AssertComplexAdvancedFilterResult, + #[allow(dead_code)] + AssertGridSetting { + expected_setting: DatabaseViewSettingPB, + }, + Wait { + millisecond: u64, + }, +} + pub struct DatabaseFilterTest { inner: DatabaseEditorTest, recv: Option<Receiver<DatabaseViewChanged>>, @@ -81,171 +148,166 @@ impl DatabaseFilterTest { } } - pub async fn update_text_cell_with_change( - &mut self, - row_id: RowId, - text: String, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - self.update_text_cell(row_id, &text).await.unwrap(); - } - - pub async fn update_checklist_cell(&mut self, row_id: RowId, selected_option_ids: Vec<String>) { - self - .set_checklist_cell(row_id, selected_option_ids) - .await - .unwrap(); - } - - pub async fn update_single_select_cell_with_change( - &mut self, - row_id: RowId, - option_id: String, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - self - .update_single_select_cell(row_id, &option_id) - .await - .unwrap(); - } - - pub async fn create_data_filter( - &mut self, - parent_filter_id: Option<String>, - field_type: FieldType, - data: BoxAny, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - let field = self.get_first_field(field_type).await; - let params = FilterChangeset::Insert { - parent_filter_id, - data: FilterInner::Data { - field_id: field.id, - field_type, - condition_and_content: data, - }, - }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn update_text_filter( - &mut self, - filter: FilterPB, - condition: TextFilterConditionPB, - content: String, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - let current_filter = filter.data.unwrap(); - let params = FilterChangeset::UpdateData { - filter_id: filter.id, - data: FilterInner::Data { - field_id: current_filter.field_id, - field_type: current_filter.field_type, - condition_and_content: BoxAny::new(TextFilterPB { condition, content }), - }, - }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn create_and_filter( - &mut self, - parent_filter_id: Option<String>, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - let params = FilterChangeset::Insert { - parent_filter_id, - data: FilterInner::And { children: vec![] }, - }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn create_or_filter( - &mut self, - parent_filter_id: Option<String>, - changed: Option<FilterRowChanged>, - ) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - let params = FilterChangeset::Insert { - parent_filter_id, - data: FilterInner::Or { children: vec![] }, - }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn delete_filter(&mut self, filter_id: String, changed: Option<FilterRowChanged>) { - self.subscribe_view_changed().await; - self.assert_future_changed(changed).await; - let params = FilterChangeset::Delete { filter_id }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn assert_filter_count(&self, count: usize) { - let filters = self.editor.get_all_filters(&self.view_id).await.items; - assert_eq!(count, filters.len()); - } - - pub async fn assert_grid_setting(&self, expected_setting: DatabaseViewSettingPB) { - let setting = self - .editor - .get_database_view_setting(&self.view_id) - .await - .unwrap(); - assert_eq!(expected_setting, setting); - } - - pub async fn assert_filters(&self, expected: Vec<FilterPB>) { - let actual = self.get_all_filters().await; - for (actual_filter, expected_filter) in actual.iter().zip(expected.iter()) { - Self::assert_filter(actual_filter, expected_filter); + pub async fn run_scripts(&mut self, scripts: Vec<FilterScript>) { + for script in scripts { + self.run_script(script).await; } } - pub async fn assert_number_of_visible_rows(&self, expected: usize) { - let (tx, rx) = tokio::sync::oneshot::channel(); - let _ = self - .editor - .open_database_view(&self.view_id, Some(tx)) - .await - .unwrap(); - rx.await.unwrap(); + pub async fn run_script(&mut self, script: FilterScript) { + match script { + FilterScript::UpdateTextCell { + row_id, + text, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + self.update_text_cell(row_id, &text).await.unwrap(); + }, + FilterScript::UpdateChecklistCell { + row_id, + selected_option_ids, + } => { + self + .set_checklist_cell(row_id, selected_option_ids) + .await + .unwrap(); + }, + FilterScript::UpdateSingleSelectCell { + row_id, + option_id, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + self + .update_single_select_cell(row_id, &option_id) + .await + .unwrap(); + }, + FilterScript::CreateDataFilter { + parent_filter_id, + field_type, + data, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + let field = self.get_first_field(field_type); + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::Data { + field_id: field.id, + field_type, + condition_and_content: data, + }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); + }, + FilterScript::UpdateTextFilter { + filter, + condition, + content, + changed, + } => { + self.subscribe_view_changed().await; - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - assert_eq!(rows.len(), expected); - } - - pub async fn wait(&self, millisecond: u64) { - tokio::time::sleep(Duration::from_millis(millisecond)).await; + self.assert_future_changed(changed).await; + let current_filter = filter.data.unwrap(); + let params = FilterChangeset::UpdateData { + filter_id: filter.id, + data: FilterInner::Data { + field_id: current_filter.field_id, + field_type: current_filter.field_type, + condition_and_content: BoxAny::new(TextFilterPB { condition, content }), + }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); + }, + FilterScript::CreateAndFilter { + parent_filter_id, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::And { children: vec![] }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); + }, + FilterScript::CreateOrFilter { + parent_filter_id, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::Or { children: vec![] }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); + }, + FilterScript::AssertFilterCount { count } => { + let filters = self.editor.get_all_filters(&self.view_id).await.items; + assert_eq!(count, filters.len()); + }, + FilterScript::DeleteFilter { + filter_id, + field_id, + changed, + } => { + self.subscribe_view_changed().await; + self.assert_future_changed(changed).await; + let params = FilterChangeset::Delete { + filter_id, + field_id, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); + }, + FilterScript::AssertGridSetting { expected_setting } => { + let setting = self + .editor + .get_database_view_setting(&self.view_id) + .await + .unwrap(); + assert_eq!(expected_setting, setting); + }, + FilterScript::AssertFilters { expected } => { + let actual = self.get_all_filters().await; + for (actual_filter, expected_filter) in actual.iter().zip(expected.iter()) { + Self::assert_filter(actual_filter, expected_filter); + } + }, + FilterScript::AssertNumberOfVisibleRows { expected } => { + let grid = self.editor.get_database_data(&self.view_id).await.unwrap(); + assert_eq!(grid.rows.len(), expected); + }, + FilterScript::Wait { millisecond } => { + tokio::time::sleep(Duration::from_millis(millisecond)).await; + }, + } } async fn subscribe_view_changed(&mut self) { @@ -264,7 +326,7 @@ impl DatabaseFilterTest { } let change = change.unwrap(); let mut receiver = self.recv.take().unwrap(); - tokio::spawn(async move { + af_spawn(async move { match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { Ok(changed) => { if let DatabaseViewChanged::FilterNotification(notification) = changed.unwrap() { diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs index 4a92f4c93f..eb808d0bc3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -1,192 +1,211 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{FieldType, SelectOptionFilterConditionPB, SelectOptionFilterPB}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_multi_select_is_empty_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Multi-Select "Is Empty" filter - test - .create_data_filter( - None, - FieldType::MultiSelect, - BoxAny::new(SelectOptionFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIsEmpty, option_ids: vec![], }), - None, - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(2).await; + changed: None, + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_multi_select_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Multi-Select "Is Not Empty" filter - test - .create_data_filter( - None, - FieldType::MultiSelect, - BoxAny::new(SelectOptionFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, option_ids: vec![], }), - None, - ) - .await; + changed: None, + }, + AssertNumberOfVisibleRows { expected: 5 }, + ]; + test.run_scripts(scripts).await; +} - // Assert the number of visible rows - test.assert_number_of_visible_rows(5).await; +#[tokio::test] +async fn grid_filter_multi_select_is_test() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![options.remove(0).id, options.remove(0).id], + }), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_multi_select_is_test2() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![options.remove(1).id], + }), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_single_select_is_empty_test() { let mut test = DatabaseFilterTest::new().await; let expected = 3; - let row_count = test.rows.len(); - - // Create Single-Select "Is Empty" filter - test - .create_data_filter( - None, - FieldType::SingleSelect, - BoxAny::new(SelectOptionFilterPB { + let row_count = test.row_details.len(); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIsEmpty, option_ids: vec![], }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_single_select_is_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect).await; - let mut options = test.get_single_select_type_option(&field.id).await; + let field = test.get_first_field(FieldType::SingleSelect); + let mut options = test.get_single_select_type_option(&field.id); let expected = 2; - let row_count = test.rows.len(); - - // Create Single-Select "Is" filter with a specific option ID - test - .create_data_filter( - None, - FieldType::SingleSelect, - BoxAny::new(SelectOptionFilterPB { + let row_count = test.row_details.len(); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIs, option_ids: vec![options.remove(0).id], }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_single_select_is_test2() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::SingleSelect).await; + let field = test.get_first_field(FieldType::SingleSelect); let row_details = test.get_rows().await; - let mut options = test.get_single_select_type_option(&field.id).await; + let mut options = test.get_single_select_type_option(&field.id); let option = options.remove(0); - let row_count = test.rows.len(); + let row_count = test.row_details.len(); - // Create Single-Select "Is" filter - test - .create_data_filter( - None, - FieldType::SingleSelect, - BoxAny::new(SelectOptionFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionIs, option_ids: vec![option.id.clone()], }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - 2, }), - ) - .await; - - test.assert_number_of_visible_rows(2).await; - - // Update single-select cell to match the filter - test - .update_single_select_cell_with_change(row_details[1].id.clone(), option.id.clone(), None) - .await; - test.assert_number_of_visible_rows(3).await; - - // Update single-select cell to remove the option - test - .update_single_select_cell_with_change( - row_details[1].id.clone(), - "".to_string(), - Some(FilterRowChanged { + }, + AssertNumberOfVisibleRows { expected: 2 }, + UpdateSingleSelectCell { + row_id: row_details[1].row.id.clone(), + option_id: option.id.clone(), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 3 }, + UpdateSingleSelectCell { + row_id: row_details[1].row.id.clone(), + option_id: "".to_string(), + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 1, }), - ) - .await; - test.assert_number_of_visible_rows(2).await; + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_multi_select_contains_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect).await; - let mut options = test.get_multi_select_type_option(&field.id).await; - - // Create Multi-Select "Contains" filter - test - .create_data_filter( - None, - FieldType::MultiSelect, - BoxAny::new(SelectOptionFilterPB { + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionContains, option_ids: vec![options.remove(0).id, options.remove(0).id], }), - None, - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(5).await; + changed: None, + }, + AssertNumberOfVisibleRows { expected: 5 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_multi_select_contains_test2() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::MultiSelect).await; - let mut options = test.get_multi_select_type_option(&field.id).await; - - // Create Multi-Select "Contains" filter with a specific option ID - test - .create_data_filter( - None, - FieldType::MultiSelect, - BoxAny::new(SelectOptionFilterPB { + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { condition: SelectOptionFilterConditionPB::OptionContains, option_ids: vec![options.remove(1).id], }), - None, - ) - .await; - - // Assert the number of visible rows - test.assert_number_of_visible_rows(4).await; + changed: None, + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs index 88b2c0382f..600f4342fa 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs @@ -1,314 +1,285 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{FieldType, TextFilterConditionPB, TextFilterPB}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::*; + #[tokio::test] async fn grid_filter_text_is_empty_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Is Empty" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextIsEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 5, }), - ) - .await; - - // Assert filter count - test.assert_filter_count(1).await; + }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_text_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Is Not Empty" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + // Only one row's text of the initial rows is "" + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextIsNotEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 1, }), - ) - .await; + }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; - // Assert filter count - test.assert_filter_count(1).await; - - // Delete the filter - let filter = test.get_all_filters().await.pop().unwrap(); + let filter = test.database_filters().await.pop().unwrap(); test - .delete_filter( - filter.id, - Some(FilterRowChanged { - showing_num_of_rows: 1, - hiding_num_of_rows: 0, - }), - ) + .run_scripts(vec![ + DeleteFilter { + filter_id: filter.id, + field_id: filter.data.unwrap().field_id, + changed: Some(FilterRowChanged { + showing_num_of_rows: 1, + hiding_num_of_rows: 0, + }), + }, + AssertFilterCount { count: 0 }, + ]) .await; - - // Assert filter count after deletion - test.assert_filter_count(0).await; } #[tokio::test] async fn grid_filter_is_text_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Is" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { - condition: TextFilterConditionPB::TextIs, - content: "A".to_string(), - }), - Some(FilterRowChanged { - showing_num_of_rows: 0, - hiding_num_of_rows: 5, - }), - ) - .await; + // Only one row's text of the initial rows is "A" + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIs, + content: "A".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 5, + }), + }]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_contain_text_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Contains" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { - condition: TextFilterConditionPB::TextContains, - content: "A".to_string(), - }), - Some(FilterRowChanged { - showing_num_of_rows: 0, - hiding_num_of_rows: 2, - }), - ) - .await; + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "A".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 2, + }), + }]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_contain_text_test2() { let mut test = DatabaseFilterTest::new().await; - let row_detail = test.rows.clone(); + let row_detail = test.row_details.clone(); - // Create Text "Contains" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextContains, content: "A".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 2, }), - ) - .await; - - // Update the text of a row - test - .update_text_cell_with_change( - row_detail[1].id.clone(), - "ABC".to_string(), - Some(FilterRowChanged { + }, + UpdateTextCell { + row_id: row_detail[1].row.id.clone(), + text: "ABC".to_string(), + changed: Some(FilterRowChanged { showing_num_of_rows: 1, hiding_num_of_rows: 0, }), - ) - .await; + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_does_not_contain_text_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Does Not Contain" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { - condition: TextFilterConditionPB::TextDoesNotContain, - content: "AB".to_string(), - }), - Some(FilterRowChanged { - showing_num_of_rows: 0, - hiding_num_of_rows: 0, - }), - ) - .await; + // None of the initial rows contains the text "AB" + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextDoesNotContain, + content: "AB".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 0, + }), + }]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_start_with_text_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Starts With" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { - condition: TextFilterConditionPB::TextStartsWith, - content: "A".to_string(), - }), - Some(FilterRowChanged { - showing_num_of_rows: 0, - hiding_num_of_rows: 3, - }), - ) - .await; + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextStartsWith, + content: "A".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 3, + }), + }]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_ends_with_text_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Ends With" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextEndsWith, content: "A".to_string(), }), - None, - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(2).await; + changed: None, + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_update_text_filter_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Ends With" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextEndsWith, content: "A".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 4, }), - ) - .await; - - // Assert number of visible rows and filter count - test.assert_number_of_visible_rows(2).await; - test.assert_filter_count(1).await; + }, + AssertNumberOfVisibleRows { expected: 2 }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; // Update the filter let filter = test.get_all_filters().await.pop().unwrap(); - test - .update_text_filter( + let scripts = vec![ + UpdateTextFilter { filter, - TextFilterConditionPB::TextIs, - "A".to_string(), - Some(FilterRowChanged { + condition: TextFilterConditionPB::TextIs, + content: "A".to_string(), + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 1, }), - ) - .await; - - // Assert number of visible rows after update - test.assert_number_of_visible_rows(1).await; + }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_delete_test() { let mut test = DatabaseFilterTest::new().await; - - // Create Text "Is Empty" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + changed: None, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextIsEmpty, content: "".to_string(), }), - None, - ) + }, + AssertFilterCount { count: 1 }, + AssertNumberOfVisibleRows { expected: 1 }, + ]; + test.run_scripts(scripts).await; + + let filter = test.database_filters().await.pop().unwrap(); + test + .run_scripts(vec![ + DeleteFilter { + filter_id: filter.id, + field_id: filter.data.unwrap().field_id, + changed: None, + }, + AssertFilterCount { count: 0 }, + AssertNumberOfVisibleRows { expected: 7 }, + ]) .await; - - // Assert filter count and number of visible rows - test.assert_filter_count(1).await; - test.assert_number_of_visible_rows(1).await; - - // Delete the filter - let filter = test.get_all_filters().await.pop().unwrap(); - test.delete_filter(filter.id, None).await; - - // Assert filter count and number of visible rows after deletion - test.assert_filter_count(0).await; - test.assert_number_of_visible_rows(7).await; } #[tokio::test] async fn grid_filter_update_empty_text_cell_test() { let mut test = DatabaseFilterTest::new().await; - let row = test.rows.clone(); - - // Create Text "Is Empty" filter - test - .create_data_filter( - None, - FieldType::RichText, - BoxAny::new(TextFilterPB { + let row_details = test.row_details.clone(); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { condition: TextFilterConditionPB::TextIsEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 5, }), - ) - .await; - - // Assert filter count - test.assert_filter_count(1).await; - - // Update the text of a row - test - .update_text_cell_with_change( - row[0].id.clone(), - "".to_string(), - Some(FilterRowChanged { + }, + AssertFilterCount { count: 1 }, + UpdateTextCell { + row_id: row_details[0].row.id.clone(), + text: "".to_string(), + changed: Some(FilterRowChanged { showing_num_of_rows: 1, hiding_num_of_rows: 0, }), - ) - .await; + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs index 6512947c16..503483a7b5 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs @@ -1,133 +1,121 @@ -use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; use flowy_database2::entities::{FieldType, NumberFilterConditionPB, TimeFilterPB}; use lib_infra::box_any::BoxAny; +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + #[tokio::test] async fn grid_filter_time_is_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; - - // Create Time "Equal" filter - test - .create_data_filter( - None, - FieldType::Time, - BoxAny::new(TimeFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { condition: NumberFilterConditionPB::Equal, content: "75".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_time_is_less_than_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, - // Create Time "Less Than" filter - test - .create_data_filter( - None, - FieldType::Time, - BoxAny::new(TimeFilterPB { + data: BoxAny::new(TimeFilterPB { condition: NumberFilterConditionPB::LessThan, content: "80".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_time_is_less_than_or_equal_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; - - // Create Time "Less Than or Equal" filter - test - .create_data_filter( - None, - FieldType::Time, - BoxAny::new(TimeFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { condition: NumberFilterConditionPB::LessThanOrEqualTo, content: "75".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_time_is_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 6; - - // Create Time "Is Empty" filter - test - .create_data_filter( - None, - FieldType::Time, - BoxAny::new(TimeFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { condition: NumberFilterConditionPB::NumberIsEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_filter_time_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; - let row_count = test.rows.len(); + let row_count = test.row_details.len(); let expected = 1; - - // Create Time "Is Not Empty" filter - test - .create_data_filter( - None, - FieldType::Time, - BoxAny::new(TimeFilterPB { + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { condition: NumberFilterConditionPB::NumberIsNotEmpty, content: "".to_string(), }), - Some(FilterRowChanged { + changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, }), - ) - .await; - - // Assert number of visible rows - test.assert_number_of_visible_rows(expected).await; + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs index 1110e0a8d7..418dafa0f7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs @@ -1,13 +1,19 @@ -use crate::database::group_test::script::DatabaseGroupTest; -use chrono::{offset, Duration, NaiveDateTime}; -use collab_database::fields::date_type_option::DateCellData; -use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; use std::collections::HashMap; +use std::vec; + +use chrono::NaiveDateTime; +use chrono::{offset, Duration}; + +use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; +use flowy_database2::services::field::DateCellData; + +use crate::database::group_test::script::DatabaseGroupTest; +use crate::database::group_test::script::GroupScript::*; #[tokio::test] async fn group_by_date_test() { let date_diffs = vec![-1, 0, 7, -15, -1]; - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let date_field = test.get_field(FieldType::DateTime).await; for diff in date_diffs { @@ -47,82 +53,147 @@ async fn group_by_date_test() { .format("%Y/%m/%d") .to_string(); - // Group by date field - test.group_by_field(&date_field.id).await; - test.assert_group_count(7).await; - - test.assert_group_row_count(0, 0).await; // Empty group - test.assert_group_id(1, "2022/03/01").await; - test.assert_group_row_count(1, 3).await; - test.assert_group_id(2, "2022/11/01").await; - test.assert_group_row_count(2, 2).await; - test.assert_group_id(3, &last_30_days).await; - test.assert_group_row_count(3, 1).await; - test.assert_group_id(4, &last_day).await; - test.assert_group_row_count(4, 2).await; - test - .assert_group_id(5, &today.format("%Y/%m/%d").to_string()) - .await; - test.assert_group_row_count(5, 1).await; - test.assert_group_id(6, &next_7_days).await; - test.assert_group_row_count(6, 1).await; + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(7), + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + // Added via `make_test_board` + AssertGroupId { + group_index: 1, + group_id: "2022/03/01".to_string(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + // Added via `make_test_board` + AssertGroupId { + group_index: 2, + group_id: "2022/11/01".to_string(), + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupId { + group_index: 3, + group_id: last_30_days, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupId { + group_index: 4, + group_id: last_day, + }, + AssertGroupRowCount { + group_index: 4, + row_count: 2, + }, + AssertGroupId { + group_index: 5, + group_id: today.format("%Y/%m/%d").to_string(), + }, + AssertGroupRowCount { + group_index: 5, + row_count: 1, + }, + AssertGroupId { + group_index: 6, + group_id: next_7_days, + }, + AssertGroupRowCount { + group_index: 6, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn change_row_group_on_date_cell_changed_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let date_field = test.get_field(FieldType::DateTime).await; - - // Group by date field - test.group_by_field(&date_field.id).await; - test.assert_group_count(3).await; - - // Update date cell to a new timestamp - test - .update_grouped_cell_with_data(1, 0, "1667408732".to_string()) - .await; - - // Check that row counts in groups have updated correctly - test.assert_group_row_count(1, 2).await; - test.assert_group_row_count(2, 3).await; + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(3), + // Nov 2, 2022 + UpdateGroupedCellWithData { + from_group_index: 1, + row_index: 0, + cell_data: "1667408732".to_string(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn change_date_on_moving_row_to_another_group() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let date_field = test.get_field(FieldType::DateTime).await; + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(3), + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertGroupId { + group_index: 2, + group_id: "2022/11/01".to_string(), + }, + ]; + test.run_scripts(scripts).await; - // Group by date field - test.group_by_field(&date_field.id).await; - test.assert_group_count(3).await; - test.assert_group_row_count(1, 3).await; - test.assert_group_row_count(2, 2).await; - - // Move a row from one group to another - test.move_row(1, 0, 2, 0).await; - - // Verify row counts after the move - test.assert_group_row_count(1, 2).await; - test.assert_group_row_count(2, 3).await; - test.assert_group_id(2, "2022/11/01").await; - - // Verify the timestamp of the moved row matches the new group's date let group = test.group_at_index(2).await; - let rows = group.rows; + let rows = group.clone().rows; let row_id = &rows.first().unwrap().id; - let row = test + let row_detail = test .get_rows() .await .into_iter() - .find(|r| r.id.to_string() == *row_id) + .find(|r| r.row.id.to_string() == *row_id) .unwrap(); - let cell = row.cells.get(&date_field.id).unwrap(); + let cell = row_detail.row.cells.get(&date_field.id.clone()).unwrap(); let date_cell = DateCellData::from(cell); - let expected_date_time = + let date_time = NaiveDateTime::parse_from_str("2022/11/01 00:00:00", "%Y/%m/%d %H:%M:%S").unwrap(); - assert_eq!( - expected_date_time.and_utc().timestamp(), - date_cell.timestamp.unwrap() - ); + assert_eq!(date_time.timestamp(), date_cell.timestamp.unwrap()); } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index 1b700b96f3..1fe883e041 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -1,15 +1,73 @@ -use crate::database::database_editor::DatabaseEditorTest; -use collab_database::fields::select_type_option::{SelectOption, SingleSelectTypeOption}; use collab_database::fields::Field; use collab_database::rows::RowId; + use flowy_database2::entities::{CreateRowPayloadPB, FieldType, GroupPB, RowMetaPB}; use flowy_database2::services::cell::{ delete_select_option_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; use flowy_database2::services::field::{ - edit_single_select_type_option, SelectTypeOptionSharedAction, + edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction, + SingleSelectTypeOption, }; -use std::time::Duration; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum GroupScript { + AssertGroupRowCount { + group_index: usize, + row_count: usize, + }, + AssertGroupCount(usize), + AssertGroup { + group_index: usize, + expected_group: GroupPB, + }, + AssertRow { + group_index: usize, + row_index: usize, + row: RowMetaPB, + }, + MoveRow { + from_group_index: usize, + from_row_index: usize, + to_group_index: usize, + to_row_index: usize, + }, + CreateRow { + group_index: usize, + }, + DeleteRow { + group_index: usize, + row_index: usize, + }, + UpdateGroupedCell { + from_group_index: usize, + row_index: usize, + to_group_index: usize, + }, + UpdateGroupedCellWithData { + from_group_index: usize, + row_index: usize, + cell_data: String, + }, + MoveGroup { + from_group_index: usize, + to_group_index: usize, + }, + UpdateSingleSelectSelectOption { + inserted_options: Vec<SelectOption>, + }, + GroupByField { + field_id: String, + }, + AssertGroupId { + group_index: usize, + group_id: String, + }, + CreateGroup { + name: String, + }, +} pub struct DatabaseGroupTest { inner: DatabaseEditorTest, @@ -21,171 +79,197 @@ impl DatabaseGroupTest { Self { inner: editor_test } } - pub async fn assert_group_row_count(&self, group_index: usize, row_count: usize) { - tokio::time::sleep(Duration::from_secs(3)).await; // Sleep to allow updates to complete - assert_eq!(row_count, self.group_at_index(group_index).await.rows.len()); + pub async fn run_scripts(&mut self, scripts: Vec<GroupScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn assert_group_count(&self, count: usize) { - let groups = self.editor.load_groups(&self.view_id).await.unwrap(); - assert_eq!(count, groups.len()); - } - - pub async fn move_row( - &self, - from_group_index: usize, - from_row_index: usize, - to_group_index: usize, - to_row_index: usize, - ) { - let groups: Vec<GroupPB> = self.editor.load_groups(&self.view_id).await.unwrap().items; - let from_group = groups.get(from_group_index).unwrap(); - let from_row = from_group.rows.get(from_row_index).unwrap(); - let to_group = groups.get(to_group_index).unwrap(); - let to_row = to_group.rows.get(to_row_index).unwrap(); - let from_row = RowId::from(from_row.id.clone()); - let to_row = RowId::from(to_row.id.clone()); - - self - .editor - .move_group_row( - &self.view_id, - &from_group.group_id, - &to_group.group_id, - from_row, - Some(to_row), - ) - .await - .unwrap(); - } - - pub async fn assert_row(&self, group_index: usize, row_index: usize, row: RowMetaPB) { - let group = self.group_at_index(group_index).await; - let compare_row = group.rows.get(row_index).unwrap().clone(); - assert_eq!(row.id, compare_row.id); - } - - pub async fn create_row(&self, group_index: usize) { - let group = self.group_at_index(group_index).await; - let params = CreateRowPayloadPB { - view_id: self.view_id.clone(), - row_position: Default::default(), - group_id: Some(group.group_id), - data: Default::default(), - }; - self.editor.create_row(params).await.unwrap(); - } - - pub async fn delete_row(&self, group_index: usize, row_index: usize) { - let row = self.row_at_index(group_index, row_index).await; - let row_ids = vec![RowId::from(row.id)]; - self.editor.delete_rows(&row_ids).await; - tokio::time::sleep(Duration::from_secs(1)).await; // Sleep to allow deletion to propagate - } - - pub async fn update_grouped_cell( - &self, - from_group_index: usize, - row_index: usize, - to_group_index: usize, - ) { - let from_group = self.group_at_index(from_group_index).await; - let to_group = self.group_at_index(to_group_index).await; - let field_id = from_group.field_id; - let field = self.editor.get_field(&field_id).await.unwrap(); - let field_type = FieldType::from(field.field_type); - - let cell = if to_group.is_default { - match field_type { - FieldType::SingleSelect | FieldType::MultiSelect => { - delete_select_option_cell(vec![to_group.group_id.clone()], &field) - }, - _ => panic!("Unsupported group field type"), - } - } else { - match field_type { - FieldType::SingleSelect | FieldType::MultiSelect => { - insert_select_option_cell(vec![to_group.group_id.clone()], &field) - }, - FieldType::URL => insert_url_cell(to_group.group_id.clone(), &field), - _ => panic!("Unsupported group field type"), - } - }; - - let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); - self - .editor - .update_cell(&self.view_id, &row_id, &field_id, cell) - .await - .unwrap(); - } - - pub async fn update_grouped_cell_with_data( - &self, - from_group_index: usize, - row_index: usize, - cell_data: String, - ) { - let from_group = self.group_at_index(from_group_index).await; - let field_id = from_group.field_id; - let field = self.editor.get_field(&field_id).await.unwrap(); - let field_type = FieldType::from(field.field_type); - let cell = match field_type { - FieldType::URL => insert_url_cell(cell_data, &field), - FieldType::DateTime => { - insert_date_cell(cell_data.parse::<i64>().unwrap(), None, Some(true), &field) + pub async fn run_script(&mut self, script: GroupScript) { + match script { + GroupScript::AssertGroupRowCount { + group_index, + row_count, + } => { + assert_eq!(row_count, self.group_at_index(group_index).await.rows.len()); }, - _ => panic!("Unsupported group field type"), - }; + GroupScript::AssertGroupCount(count) => { + let groups = self.editor.load_groups(&self.view_id).await.unwrap(); + assert_eq!(count, groups.len()); + }, + GroupScript::MoveRow { + from_group_index, + from_row_index, + to_group_index, + to_row_index, + } => { + let groups: Vec<GroupPB> = self.editor.load_groups(&self.view_id).await.unwrap().items; + let from_group = groups.get(from_group_index).unwrap(); + let from_row = from_group.rows.get(from_row_index).unwrap(); + let to_group = groups.get(to_group_index).unwrap(); + let to_row = to_group.rows.get(to_row_index).unwrap(); + let from_row = RowId::from(from_row.id.clone()); + let to_row = RowId::from(to_row.id.clone()); - let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); - self - .editor - .update_cell(&self.view_id, &row_id, &field_id, cell) - .await - .unwrap(); - } + self + .editor + .move_group_row( + &self.view_id, + &from_group.group_id, + &to_group.group_id, + from_row, + Some(to_row), + ) + .await + .unwrap(); + }, + GroupScript::AssertRow { + group_index, + row_index, + row, + } => { + // + let group = self.group_at_index(group_index).await; + let compare_row = group.rows.get(row_index).unwrap().clone(); + assert_eq!(row.id, compare_row.id); + }, + GroupScript::CreateRow { group_index } => { + let group = self.group_at_index(group_index).await; + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + row_position: Default::default(), + group_id: Some(group.group_id), + data: Default::default(), + }; + let _ = self.editor.create_row(params).await.unwrap(); + }, + GroupScript::DeleteRow { + group_index, + row_index, + } => { + let row = self.row_at_index(group_index, row_index).await; + let row_ids = vec![RowId::from(row.id)]; + self.editor.delete_rows(&row_ids).await; + }, + GroupScript::UpdateGroupedCell { + from_group_index, + row_index, + to_group_index, + } => { + let from_group = self.group_at_index(from_group_index).await; + let to_group = self.group_at_index(to_group_index).await; + let field_id = from_group.field_id; + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); - pub async fn move_group(&self, from_group_index: usize, to_group_index: usize) { - let from_group = self.group_at_index(from_group_index).await; - let to_group = self.group_at_index(to_group_index).await; - self - .editor - .move_group(&self.view_id, &from_group.group_id, &to_group.group_id) - .await - .unwrap(); - } + let cell = if to_group.is_default { + match field_type { + FieldType::SingleSelect => { + delete_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::MultiSelect => { + delete_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + _ => { + panic!("Unsupported group field type"); + }, + } + } else { + match field_type { + FieldType::SingleSelect => { + insert_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::MultiSelect => { + insert_select_option_cell(vec![to_group.group_id.clone()], &field) + }, + FieldType::URL => insert_url_cell(to_group.group_id.clone(), &field), + _ => { + panic!("Unsupported group field type"); + }, + } + }; - pub async fn assert_group(&self, group_index: usize, expected_group: GroupPB) { - let group = self.group_at_index(group_index).await; - assert_eq!(group.group_id, expected_group.group_id); - } + let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); + self + .editor + .update_cell(&self.view_id, &row_id, &field_id, cell) + .await + .unwrap(); + }, + GroupScript::UpdateGroupedCellWithData { + from_group_index, + row_index, + cell_data, + } => { + let from_group = self.group_at_index(from_group_index).await; + let field_id = from_group.field_id; + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + let cell = match field_type { + FieldType::URL => insert_url_cell(cell_data, &field), + FieldType::DateTime => { + insert_date_cell(cell_data.parse::<i64>().unwrap(), None, Some(true), &field) + }, + _ => { + panic!("Unsupported group field type"); + }, + }; - pub async fn update_single_select_option(&self, inserted_options: Vec<SelectOption>) { - self - .edit_single_select_type_option(|type_option| { - for inserted_option in inserted_options { - type_option.insert_option(inserted_option); - } - }) - .await; - } - - pub async fn group_by_field(&self, field_id: &str) { - self - .editor - .group_by_field(&self.view_id, field_id) - .await - .unwrap(); - } - - pub async fn assert_group_id(&self, group_index: usize, group_id: &str) { - let group = self.group_at_index(group_index).await; - assert_eq!(group_id, group.group_id, "group index: {}", group_index); - } - - pub async fn create_group(&self, name: &str) { - self.editor.create_group(&self.view_id, name).await.unwrap(); + let row_id = RowId::from(self.row_at_index(from_group_index, row_index).await.id); + self + .editor + .update_cell(&self.view_id, &row_id, &field_id, cell) + .await + .unwrap(); + }, + GroupScript::MoveGroup { + from_group_index, + to_group_index, + } => { + let from_group = self.group_at_index(from_group_index).await; + let to_group = self.group_at_index(to_group_index).await; + self + .editor + .move_group(&self.view_id, &from_group.group_id, &to_group.group_id) + .await + .unwrap(); + }, + GroupScript::AssertGroup { + group_index, + expected_group: group_pb, + } => { + let group = self.group_at_index(group_index).await; + assert_eq!(group.group_id, group_pb.group_id); + }, + GroupScript::UpdateSingleSelectSelectOption { inserted_options } => { + self + .edit_single_select_type_option(|type_option| { + for inserted_option in inserted_options { + type_option.insert_option(inserted_option); + } + }) + .await; + }, + GroupScript::GroupByField { field_id } => { + self + .editor + .group_by_field(&self.view_id, &field_id) + .await + .unwrap(); + }, + GroupScript::AssertGroupId { + group_index, + group_id, + } => { + let group = self.group_at_index(group_index).await; + assert_eq!(group_id, group.group_id, "group index: {}", group_index); + }, + GroupScript::CreateGroup { name } => self + .editor + .create_group(&self.view_id, &name) + .await + .unwrap(), + } } pub async fn group_at_index(&self, index: usize) -> GroupPB { @@ -225,9 +309,11 @@ impl DatabaseGroupTest { self .inner .get_fields() - .await .into_iter() - .find(|field| FieldType::from(field.field_type) == field_type) + .find(|field| { + let ft = FieldType::from(field.field_type); + ft == field_type + }) .unwrap() } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs index b93df800e1..33e2b1563c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -1,234 +1,506 @@ +use flowy_database2::services::field::SelectOption; + use crate::database::group_test::script::DatabaseGroupTest; -use collab_database::fields::select_type_option::SelectOption; +use crate::database::group_test::script::GroupScript::*; #[tokio::test] async fn group_init_test() { - let test = DatabaseGroupTest::new().await; - test.assert_group_count(4).await; - test.assert_group_row_count(1, 2).await; - test.assert_group_row_count(2, 2).await; - test.assert_group_row_count(3, 1).await; - test.assert_group_row_count(0, 0).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + AssertGroupCount(4), + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_row_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group = test.group_at_index(1).await; - - test.move_row(1, 0, 1, 1).await; - test.assert_group_row_count(1, 2).await; - test - .assert_row(1, 1, group.rows.first().unwrap().clone()) - .await; + let scripts = vec![ + // Move the row at 0 in group0 to group1 at 1 + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 1, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertRow { + group_index: 1, + row_index: 1, + row: group.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] -async fn test_row_movement_between_groups_with_assertions() { - let test = DatabaseGroupTest::new().await; - for _ in 0..5 { - test.move_row(1, 0, 2, 1).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(2, 3).await; - - // Move the row back to the original group - test.move_row(2, 1, 1, 0).await; - test.assert_group_row_count(2, 2).await; - test.assert_group_row_count(1, 2).await; - - tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - } +async fn group_move_row_to_other_group_test() { + let mut test = DatabaseGroupTest::new().await; + let group = test.group_at_index(1).await; + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_two_row_to_other_group_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group_1 = test.group_at_index(1).await; - - test.move_row(1, 0, 2, 1).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(2, 3).await; - test - .assert_row(2, 1, group_1.rows.first().unwrap().clone()) - .await; + let scripts = vec![ + // Move row at index 0 from group 1 to group 2 at index 1 + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; let group_1 = test.group_at_index(1).await; - test.move_row(1, 0, 2, 1).await; - test.assert_group_row_count(1, 0).await; - test.assert_group_row_count(2, 4).await; - test - .assert_row(2, 1, group_1.rows.first().unwrap().clone()) - .await; + // Move row at index 0 from group 1 to group 2 at index 1 + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 4, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_row_to_other_group_and_reorder_from_up_to_down_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group_1 = test.group_at_index(1).await; let group_2 = test.group_at_index(2).await; + let scripts = vec![ + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }, + AssertRow { + group_index: 2, + row_index: 1, + row: group_1.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; - test.move_row(1, 0, 2, 1).await; - test - .assert_row(2, 1, group_1.rows.first().unwrap().clone()) - .await; - - test.move_row(2, 0, 2, 2).await; - test - .assert_row(2, 2, group_2.rows.first().unwrap().clone()) - .await; + let scripts = vec![ + MoveRow { + from_group_index: 2, + from_row_index: 0, + to_group_index: 2, + to_row_index: 2, + }, + AssertRow { + group_index: 2, + row_index: 2, + row: group_2.rows.first().unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_row_to_other_group_and_reorder_from_bottom_to_up_test() { - let test = DatabaseGroupTest::new().await; - test.move_row(1, 0, 2, 1).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 1, + }]; + test.run_scripts(scripts).await; let group = test.group_at_index(2).await; - test.assert_group_row_count(2, 3).await; - - test.move_row(2, 2, 2, 0).await; - test - .assert_row(2, 0, group.rows.get(2).unwrap().clone()) - .await; + let scripts = vec![ + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + MoveRow { + from_group_index: 2, + from_row_index: 2, + to_group_index: 2, + to_row_index: 0, + }, + AssertRow { + group_index: 2, + row_index: 0, + row: group.rows.get(2).unwrap().clone(), + }, + ]; + test.run_scripts(scripts).await; } - #[tokio::test] async fn group_create_row_test() { - let test = DatabaseGroupTest::new().await; - test.create_row(1).await; - test.assert_group_row_count(1, 3).await; - - test.create_row(2).await; - test.create_row(2).await; - test.assert_group_row_count(2, 4).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + CreateRow { group_index: 1 }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + CreateRow { group_index: 2 }, + CreateRow { group_index: 2 }, + AssertGroupRowCount { + group_index: 2, + row_count: 4, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_delete_row_test() { - let test = DatabaseGroupTest::new().await; - test.delete_row(1, 0).await; - test.assert_group_row_count(1, 1).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + DeleteRow { + group_index: 1, + row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_delete_all_row_test() { - let test = DatabaseGroupTest::new().await; - test.delete_row(1, 0).await; - test.delete_row(1, 0).await; - test.assert_group_row_count(1, 0).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + DeleteRow { + group_index: 1, + row_index: 0, + }, + DeleteRow { + group_index: 1, + row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_update_row_test() { - let test = DatabaseGroupTest::new().await; - test.update_grouped_cell(1, 0, 2).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(2, 3).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + // Update the row at 0 in group0 by setting the row's group field data + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 2, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_reorder_group_test() { - let test = DatabaseGroupTest::new().await; - test.update_grouped_cell(1, 0, 2).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(2, 3).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + // Update the row at 0 in group0 by setting the row's group field data + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 2, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_to_default_group_test() { - let test = DatabaseGroupTest::new().await; - test.update_grouped_cell(1, 0, 0).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(0, 1).await; + let mut test = DatabaseGroupTest::new().await; + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_from_default_group_test() { - let test = DatabaseGroupTest::new().await; - test.update_grouped_cell(1, 0, 0).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(0, 1).await; + let mut test = DatabaseGroupTest::new().await; + // Move one row from group 1 to group 0 + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 1, + row_index: 0, + to_group_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; - test.update_grouped_cell(0, 0, 1).await; - test.assert_group_row_count(1, 2).await; - test.assert_group_row_count(0, 0).await; + // Move one row from group 0 to group 1 + let scripts = vec![ + UpdateGroupedCell { + from_group_index: 0, + row_index: 0, + to_group_index: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_group_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group_0 = test.group_at_index(0).await; let group_1 = test.group_at_index(1).await; - - test.move_group(0, 1).await; - test.assert_group_row_count(0, 2).await; - test.assert_group(0, group_1).await; - test.assert_group_row_count(1, 0).await; - test.assert_group(1, group_0).await; + let scripts = vec![ + MoveGroup { + from_group_index: 0, + to_group_index: 1, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + AssertGroup { + group_index: 0, + expected_group: group_1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 0, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_group_row_after_move_group_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group_1 = test.group_at_index(1).await; let group_2 = test.group_at_index(2).await; - - test.move_group(1, 2).await; - test.assert_group(1, group_2).await; - test.assert_group(2, group_1).await; - - test.move_row(1, 0, 2, 0).await; - test.assert_group_row_count(1, 1).await; - test.assert_group_row_count(2, 3).await; + let scripts = vec![ + MoveGroup { + from_group_index: 1, + to_group_index: 2, + }, + AssertGroup { + group_index: 1, + expected_group: group_2, + }, + AssertGroup { + group_index: 2, + expected_group: group_1, + }, + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_group_to_default_group_pos_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let group_0 = test.group_at_index(0).await; let group_3 = test.group_at_index(3).await; - - test.move_group(3, 0).await; - test.assert_group(0, group_3).await; - test.assert_group(1, group_0).await; + let scripts = vec![ + MoveGroup { + from_group_index: 3, + to_group_index: 0, + }, + AssertGroup { + group_index: 0, + expected_group: group_3, + }, + AssertGroup { + group_index: 1, + expected_group: group_0, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_insert_single_select_option_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let new_option_name = "New option"; - - test.assert_group_count(4).await; - test - .update_single_select_option(vec![SelectOption { - id: new_option_name.to_string(), - name: new_option_name.to_string(), - color: Default::default(), - }]) - .await; - - test.assert_group_count(5).await; + let scripts = vec![ + AssertGroupCount(4), + UpdateSingleSelectSelectOption { + inserted_options: vec![SelectOption { + id: new_option_name.to_string(), + name: new_option_name.to_string(), + color: Default::default(), + }], + }, + AssertGroupCount(5), + ]; + test.run_scripts(scripts).await; let new_group = test.group_at_index(4).await; assert_eq!(new_group.group_id, new_option_name); } #[tokio::test] async fn group_group_by_other_field() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let multi_select_field = test.get_multi_select_field().await; - - test.group_by_field(&multi_select_field.id).await; - test.assert_group_row_count(1, 3).await; - test.assert_group_row_count(2, 2).await; - test.assert_group_count(4).await; + let scripts = vec![ + GroupByField { + field_id: multi_select_field.id.clone(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupCount(4), + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_manual_create_new_group() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let new_group_name = "Resumed"; - - test.assert_group_count(4).await; - test.create_group(new_group_name).await; - test.assert_group_count(5).await; + let scripts = vec![ + AssertGroupCount(4), + CreateGroup { + name: new_group_name.to_string(), + }, + AssertGroupCount(5), + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs index b27e8b4060..83a38b07d3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/url_group_test.rs @@ -1,102 +1,148 @@ use crate::database::group_test::script::DatabaseGroupTest; +use crate::database::group_test::script::GroupScript::*; #[tokio::test] async fn group_group_by_url() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let url_field = test.get_url_field().await; - - // Group by URL field - test.group_by_field(&url_field.id).await; - - // Check group row counts - test.assert_group_row_count(0, 2).await; // No status group - test.assert_group_row_count(1, 2).await; // https://appflowy.io group - test.assert_group_row_count(2, 1).await; // https://github.com/AppFlowy-IO/AppFlowy group - test.assert_group_count(3).await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupCount(3), + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_alter_url_to_another_group_url_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let url_field = test.get_url_field().await; - - // Group by URL field - test.group_by_field(&url_field.id).await; - - // Check initial group row counts - test.assert_group_row_count(0, 2).await; // No status group - test.assert_group_row_count(1, 2).await; // https://appflowy.io group - test.assert_group_row_count(2, 1).await; // https://github.com/AppFlowy-IO/AppFlowy group - - // Move the last row from group 2 to group 1 - test.update_grouped_cell(2, 0, 1).await; - - // Verify group counts after moving - test.assert_group_count(2).await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed + UpdateGroupedCell { + from_group_index: 2, + row_index: 0, + to_group_index: 1, + }, + AssertGroupCount(2), + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_alter_url_to_new_url_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let url_field = test.get_url_field().await; - - // Group by URL field - test.group_by_field(&url_field.id).await; - - // Change the URL of a row to a new value - test - .update_grouped_cell_with_data(0, 0, "https://github.com/AppFlowy-IO".to_string()) - .await; - - // Verify group row counts after URL update - test.assert_group_row_count(0, 1).await; // No status group - test.assert_group_row_count(1, 2).await; // https://appflowy.io group - test.assert_group_row_count(2, 1).await; // https://github.com/AppFlowy-IO/AppFlowy group - test.assert_group_row_count(3, 1).await; // https://github.com/AppFlowy-IO group - test.assert_group_count(4).await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // When moving the last row from 2nd group to 1nd group, the 2nd group will be removed + UpdateGroupedCellWithData { + from_group_index: 0, + row_index: 0, + cell_data: "https://github.com/AppFlowy-IO".to_string(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupCount(4), + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn group_move_url_group_row_test() { - let test = DatabaseGroupTest::new().await; + let mut test = DatabaseGroupTest::new().await; let url_field = test.get_url_field().await; - - // Group by URL field - test.group_by_field(&url_field.id).await; - - // Check initial group row counts - test.assert_group_row_count(0, 2).await; // No status group - test.assert_group_row_count(1, 2).await; // https://appflowy.io group - test.assert_group_row_count(2, 1).await; // https://github.com/AppFlowy-IO/AppFlowy group - test.assert_group_count(3).await; - - // Move a row from one group to another - test.move_row(0, 0, 1, 0).await; - - // Verify row counts after the move - test.assert_group_row_count(0, 1).await; - test.assert_group_row_count(1, 3).await; - test.assert_group_row_count(2, 1).await; -} - -// Create a URL field in the default board and then set it as the grouping field. -#[tokio::test] -async fn set_group_by_url_field_test() { - let test = DatabaseGroupTest::new().await; - let url_field = test.get_url_field().await; - - // group by URL field - test - .editor - .set_group_by_field(&test.view_id, &url_field.id, vec![]) - .await - .unwrap(); - - // assert number of groups - test.assert_group_count(3).await; - - // close the database view - test.editor.close_view(&test.view_id).await; - - test.assert_group_count(3).await; + let scripts = vec![ + GroupByField { + field_id: url_field.id.clone(), + }, + // no status group + AssertGroupRowCount { + group_index: 0, + row_count: 2, + }, + // https://appflowy.io + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + // https://github.com/AppFlowy-IO/AppFlowy + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + AssertGroupCount(3), + MoveRow { + from_group_index: 0, + from_row_index: 0, + to_group_index: 1, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 0, + row_count: 1, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs index 61a6193898..6800a7e4db 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/script.rs @@ -6,6 +6,15 @@ use flowy_database2::services::setting::{BoardLayoutSetting, CalendarLayoutSetti use crate::database::database_editor::DatabaseEditorTest; +pub enum LayoutScript { + AssertBoardLayoutSetting { expected: BoardLayoutSetting }, + AssertCalendarLayoutSetting { expected: CalendarLayoutSetting }, + UpdateBoardLayoutSetting { new_setting: BoardLayoutSetting }, + AssertDefaultAllCalendarEvents, + AssertAllCalendarEventsCount { expected: usize }, + UpdateDatabaseLayout { layout: DatabaseLayout }, +} + pub struct DatabaseLayoutTest { database_test: DatabaseEditorTest, } @@ -27,10 +36,7 @@ impl DatabaseLayoutTest { } pub async fn get_first_date_field(&self) -> Field { - self - .database_test - .get_first_field(FieldType::DateTime) - .await + self.database_test.get_first_field(FieldType::DateTime) } async fn get_layout_setting( @@ -46,97 +52,100 @@ impl DatabaseLayoutTest { .unwrap() } - pub async fn update_database_layout(&mut self, layout: DatabaseLayout) { - self - .database_test - .editor - .update_view_layout(&self.database_test.view_id, layout) - .await - .unwrap(); + pub async fn run_scripts(&mut self, scripts: Vec<LayoutScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn assert_all_calendar_events_count(&self, expected: usize) { - let events = self - .database_test - .editor - .get_all_calendar_events(&self.database_test.view_id) - .await; - assert_eq!(events.len(), expected); - } + pub async fn run_script(&mut self, script: LayoutScript) { + match script { + LayoutScript::UpdateDatabaseLayout { layout } => { + self + .database_test + .editor + .update_view_layout(&self.database_test.view_id, layout) + .await + .unwrap(); + }, + LayoutScript::AssertAllCalendarEventsCount { expected } => { + let events = self + .database_test + .editor + .get_all_calendar_events(&self.database_test.view_id) + .await; + assert_eq!(events.len(), expected); + }, + LayoutScript::AssertBoardLayoutSetting { expected } => { + let view_id = self.database_test.view_id.clone(); + let layout_ty = DatabaseLayout::Board; - pub async fn assert_board_layout_setting(&self, expected: BoardLayoutSetting) { - let view_id = self.database_test.view_id.clone(); - let layout_ty = DatabaseLayout::Board; + let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; - let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; + assert!(layout_settings.calendar.is_none()); + assert_eq!( + layout_settings.board.unwrap().hide_ungrouped_column, + expected.hide_ungrouped_column + ); + }, + LayoutScript::AssertCalendarLayoutSetting { expected } => { + let view_id = self.database_test.view_id.clone(); + let layout_ty = DatabaseLayout::Calendar; - assert!(layout_settings.calendar.is_none()); - assert_eq!( - layout_settings.board.unwrap().hide_ungrouped_column, - expected.hide_ungrouped_column - ); - } + let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; - pub async fn assert_calendar_layout_setting(&self, expected: CalendarLayoutSetting) { - let view_id = self.database_test.view_id.clone(); - let layout_ty = DatabaseLayout::Calendar; + assert!(layout_settings.board.is_none()); - let layout_settings = self.get_layout_setting(&view_id, layout_ty).await; + let calendar_setting = layout_settings.calendar.unwrap(); + assert_eq!(calendar_setting.layout_ty, expected.layout_ty); + assert_eq!( + calendar_setting.first_day_of_week, + expected.first_day_of_week + ); + assert_eq!(calendar_setting.show_weekends, expected.show_weekends); + }, + LayoutScript::UpdateBoardLayoutSetting { new_setting } => { + let changeset = LayoutSettingChangeset { + view_id: self.database_test.view_id.clone(), + layout_type: DatabaseLayout::Board, + board: Some(new_setting), + calendar: None, + }; + self + .database_test + .editor + .set_layout_setting(&self.database_test.view_id, changeset) + .await + .unwrap() + }, + LayoutScript::AssertDefaultAllCalendarEvents => { + let events = self + .database_test + .editor + .get_all_calendar_events(&self.database_test.view_id) + .await; + assert_eq!(events.len(), 5); - assert!(layout_settings.board.is_none()); + for (index, event) in events.into_iter().enumerate() { + if index == 0 { + assert_eq!(event.title, "A"); + assert_eq!(event.timestamp, 1678090778); + } - let calendar_setting = layout_settings.calendar.unwrap(); - assert_eq!(calendar_setting.layout_ty, expected.layout_ty); - assert_eq!( - calendar_setting.first_day_of_week, - expected.first_day_of_week - ); - assert_eq!(calendar_setting.show_weekends, expected.show_weekends); - } - - pub async fn update_board_layout_setting(&mut self, new_setting: BoardLayoutSetting) { - let changeset = LayoutSettingChangeset { - view_id: self.database_test.view_id.clone(), - layout_type: DatabaseLayout::Board, - board: Some(new_setting), - calendar: None, - }; - self - .database_test - .editor - .set_layout_setting(&self.database_test.view_id, changeset) - .await - .unwrap(); - } - - pub async fn assert_default_all_calendar_events(&self) { - let events = self - .database_test - .editor - .get_all_calendar_events(&self.database_test.view_id) - .await; - assert_eq!(events.len(), 5); - - for (index, event) in events.into_iter().enumerate() { - match index { - 0 => { - assert_eq!(event.title, "A"); - assert_eq!(event.timestamp, Some(1678090778)); - }, - 1 => { - assert_eq!(event.title, "B"); - assert_eq!(event.timestamp, Some(1677917978)); - }, - 2 => { - assert_eq!(event.title, "C"); - assert_eq!(event.timestamp, Some(1679213978)); - }, - 4 => { - assert_eq!(event.title, "E"); - assert_eq!(event.timestamp, Some(1678695578)); - }, - _ => {}, - } + if index == 1 { + assert_eq!(event.title, "B"); + assert_eq!(event.timestamp, 1677917978); + } + if index == 2 { + assert_eq!(event.title, "C"); + assert_eq!(event.timestamp, 1679213978); + } + if index == 4 { + assert_eq!(event.title, "E"); + assert_eq!(event.timestamp, 1678695578); + } + } + }, } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs index e949bd9179..41f2f88d0e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/layout_test/test.rs @@ -1,6 +1,9 @@ -use crate::database::layout_test::script::DatabaseLayoutTest; use collab_database::views::DatabaseLayout; -use flowy_database2::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; +use flowy_database2::services::setting::BoardLayoutSetting; +use flowy_database2::services::setting::CalendarLayoutSetting; + +use crate::database::layout_test::script::DatabaseLayoutTest; +use crate::database::layout_test::script::LayoutScript::*; #[tokio::test] async fn board_layout_setting_test() { @@ -10,44 +13,46 @@ async fn board_layout_setting_test() { hide_ungrouped_column: true, ..default_board_setting }; - - // Assert the initial default board layout setting - test - .assert_board_layout_setting(default_board_setting) - .await; - - // Update the board layout setting and assert the changes - test - .update_board_layout_setting(new_board_setting.clone()) - .await; - test.assert_board_layout_setting(new_board_setting).await; + let scripts = vec![ + AssertBoardLayoutSetting { + expected: default_board_setting, + }, + UpdateBoardLayoutSetting { + new_setting: new_board_setting.clone(), + }, + AssertBoardLayoutSetting { + expected: new_board_setting, + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn calendar_initial_layout_setting_test() { - let test = DatabaseLayoutTest::new_calendar().await; + let mut test = DatabaseLayoutTest::new_calendar().await; let date_field = test.get_first_date_field().await; let default_calendar_setting = CalendarLayoutSetting::new(date_field.id.clone()); - - // Assert the initial calendar layout setting - test - .assert_calendar_layout_setting(default_calendar_setting) - .await; + let scripts = vec![AssertCalendarLayoutSetting { + expected: default_calendar_setting, + }]; + test.run_scripts(scripts).await; } #[tokio::test] async fn calendar_get_events_test() { - let test = DatabaseLayoutTest::new_calendar().await; - - // Assert the default calendar events - test.assert_default_all_calendar_events().await; + let mut test = DatabaseLayoutTest::new_calendar().await; + let scripts = vec![AssertDefaultAllCalendarEvents]; + test.run_scripts(scripts).await; } #[tokio::test] async fn grid_to_calendar_layout_test() { let mut test = DatabaseLayoutTest::new_no_date_grid().await; - - // Update layout to calendar and assert the number of calendar events - test.update_database_layout(DatabaseLayout::Calendar).await; - test.assert_all_calendar_events_count(3).await; + let scripts = vec![ + UpdateDatabaseLayout { + layout: DatabaseLayout::Calendar, + }, + AssertAllCalendarEventsCount { expected: 3 }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index c2c4851e86..d722a352f3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -1,20 +1,16 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; -use collab_database::entity::DatabaseView; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::date_type_option::{DateFormat, DateTypeOption, TimeFormat}; -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, -}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::views::{DatabaseLayout, LayoutSetting, LayoutSettings}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; use strum::IntoEnumIterator; use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; use event_integration_test::database_event::TestRowBuilder; use flowy_database2::entities::FieldType; -use flowy_database2::services::field::FieldBuilder; +use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption; +use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption; +use flowy_database2::services::field::{ + DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, RelationTypeOption, + SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, +}; use flowy_database2::services::field_settings::default_field_settings_for_fields; use flowy_database2::services::setting::BoardLayoutSetting; @@ -59,8 +55,7 @@ pub fn make_test_board() -> DatabaseData { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, include_time: true, - field_type: field_type.into(), - timezone: None, + field_type, }; let name = match field_type { FieldType::LastEditedTime => "Last Modified", @@ -145,7 +140,7 @@ pub fn make_test_board() -> DatabaseData { .build(); fields.push(time_field); }, - FieldType::Translate | FieldType::Media => {}, + FieldType::Translate => {}, } } @@ -163,7 +158,9 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("A"), FieldType::Number => row_builder.insert_number_cell("1"), // 1647251762 => Mar 14,2022 - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(0)) }, @@ -181,7 +178,9 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("B"), FieldType::Number => row_builder.insert_number_cell("2"), // 1647251762 => Mar 14,2022 - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(0)) }, @@ -198,7 +197,9 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("C"), FieldType::Number => row_builder.insert_number_cell("3"), // 1647251762 => Mar 14,2022 - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(1)) }, @@ -218,7 +219,9 @@ pub fn make_test_board() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("DA"), FieldType::Number => row_builder.insert_number_cell("4"), - FieldType::DateTime => row_builder.insert_date_cell(1668704685, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1668704685, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(1)) }, @@ -233,7 +236,9 @@ pub fn make_test_board() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell(""), - FieldType::DateTime => row_builder.insert_date_cell(1668359085, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1668359085, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(2)) }, @@ -252,8 +257,10 @@ pub fn make_test_board() -> DatabaseData { let mut layout_settings = LayoutSettings::new(); layout_settings.insert(DatabaseLayout::Board, board_setting); + let inline_view_id = gen_database_view_id(); + let view = DatabaseView { - id: gen_database_view_id(), + id: inline_view_id.clone(), database_id: database_id.clone(), name: "".to_string(), layout: DatabaseLayout::Board, @@ -266,11 +273,11 @@ pub fn make_test_board() -> DatabaseData { created_at: 0, modified_at: 0, field_settings, - is_inline: false, }; DatabaseData { database_id, + inline_view_id, views: vec![view], fields, rows, diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs index 4e789305ba..4c7553f754 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs @@ -1,13 +1,11 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; -use collab_database::entity::DatabaseView; -use collab_database::fields::select_type_option::MultiSelectTypeOption; -use collab_database::views::{DatabaseLayout, LayoutSetting, LayoutSettings}; +use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, LayoutSettings}; use flowy_database2::services::field_settings::default_field_settings_for_fields; use strum::IntoEnumIterator; use event_integration_test::database_event::TestRowBuilder; use flowy_database2::entities::FieldType; -use flowy_database2::services::field::FieldBuilder; +use flowy_database2::services::field::{FieldBuilder, MultiSelectTypeOption}; use flowy_database2::services::setting::CalendarLayoutSetting; // Calendar unit test mock data @@ -48,7 +46,9 @@ pub fn make_test_calendar() -> DatabaseData { for field_type in FieldType::iter() { match field_type { FieldType::RichText => row_builder.insert_text_cell("A"), - FieldType::DateTime => row_builder.insert_date_cell(1678090778, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1678090778, None, None, &field_type) + }, _ => "".to_owned(), }; } @@ -57,7 +57,9 @@ pub fn make_test_calendar() -> DatabaseData { for field_type in FieldType::iter() { match field_type { FieldType::RichText => row_builder.insert_text_cell("B"), - FieldType::DateTime => row_builder.insert_date_cell(1677917978, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1677917978, None, None, &field_type) + }, _ => "".to_owned(), }; } @@ -66,7 +68,9 @@ pub fn make_test_calendar() -> DatabaseData { for field_type in FieldType::iter() { match field_type { FieldType::RichText => row_builder.insert_text_cell("C"), - FieldType::DateTime => row_builder.insert_date_cell(1679213978, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1679213978, None, None, &field_type) + }, _ => "".to_owned(), }; } @@ -75,7 +79,9 @@ pub fn make_test_calendar() -> DatabaseData { for field_type in FieldType::iter() { match field_type { FieldType::RichText => row_builder.insert_text_cell("D"), - FieldType::DateTime => row_builder.insert_date_cell(1678695578, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1678695578, None, None, &field_type) + }, _ => "".to_owned(), }; } @@ -84,7 +90,9 @@ pub fn make_test_calendar() -> DatabaseData { for field_type in FieldType::iter() { match field_type { FieldType::RichText => row_builder.insert_text_cell("E"), - FieldType::DateTime => row_builder.insert_date_cell(1678695578, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1678695578, None, None, &field_type) + }, _ => "".to_owned(), }; } @@ -98,9 +106,11 @@ pub fn make_test_calendar() -> DatabaseData { let mut layout_settings = LayoutSettings::new(); layout_settings.insert(DatabaseLayout::Calendar, calendar_setting); + let inline_view_id = gen_database_view_id(); + let view = DatabaseView { database_id: database_id.clone(), - id: gen_database_view_id(), + id: inline_view_id.clone(), name: "".to_string(), layout: DatabaseLayout::Calendar, layout_settings, @@ -112,11 +122,11 @@ pub fn make_test_calendar() -> DatabaseData { created_at: 0, modified_at: 0, field_settings, - is_inline: false, }; DatabaseData { database_id, + inline_view_id, views: vec![view], fields, rows, diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 5039a37b39..6896e47ccb 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -1,26 +1,17 @@ use collab_database::database::{gen_database_id, gen_database_view_id, gen_row_id, DatabaseData}; -use collab_database::entity::DatabaseView; -use collab_database::fields::checklist_type_option::ChecklistTypeOption; -use collab_database::fields::date_type_option::{ - DateFormat, DateTypeOption, TimeFormat, TimeTypeOption, -}; -use collab_database::fields::media_type_option::MediaTypeOption; -use collab_database::fields::number_type_option::{NumberFormat, NumberTypeOption}; -use collab_database::fields::relation_type_option::RelationTypeOption; -use collab_database::fields::select_type_option::{ - MultiSelectTypeOption, SelectOption, SelectOptionColor, SingleSelectTypeOption, -}; -use collab_database::fields::summary_type_option::SummarizationTypeOption; -use collab_database::fields::timestamp_type_option::TimestampTypeOption; -use collab_database::fields::translate_type_option::TranslateTypeOption; -use collab_database::views::DatabaseLayout; +use collab_database::views::{DatabaseLayout, DatabaseView}; use strum::IntoEnumIterator; use crate::database::mock_data::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, PLANNED, TWITTER}; use event_integration_test::database_event::TestRowBuilder; use flowy_database2::entities::FieldType; -use flowy_database2::services::field::checklist_filter::ChecklistCellInsertChangeset; -use flowy_database2::services::field::FieldBuilder; +use flowy_database2::services::field::summary_type_option::summary::SummarizationTypeOption; +use flowy_database2::services::field::translate_type_option::translate::TranslateTypeOption; +use flowy_database2::services::field::{ + ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, + NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor, + SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption, +}; use flowy_database2::services::field_settings::default_field_settings_for_fields; pub fn make_test_grid() -> DatabaseData { @@ -67,8 +58,7 @@ pub fn make_test_grid() -> DatabaseData { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, include_time: true, - field_type: field_type.into(), - timezone: None, + field_type, }; let name = match field_type { FieldType::LastEditedTime => "Last Modified", @@ -160,16 +150,6 @@ pub fn make_test_grid() -> DatabaseData { .build(); fields.push(translate_field); }, - FieldType::Media => { - let type_option = MediaTypeOption { - hide_file_names: true, - }; - - let media_field = FieldBuilder::new(field_type, type_option) - .name("Media") - .build(); - fields.push(media_field); - }, } } @@ -183,7 +163,9 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("A"), FieldType::Number => row_builder.insert_number_cell("1"), - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::MultiSelect => row_builder .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]), FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), @@ -191,10 +173,7 @@ pub fn make_test_grid() -> DatabaseData { row_builder.insert_url_cell("AppFlowy website - https://www.appflowy.io") }, FieldType::Checklist => { - row_builder.insert_checklist_cell(vec![ChecklistCellInsertChangeset::new( - "First thing".to_string(), - false, - )]) + row_builder.insert_checklist_cell(vec![("First thing".to_string(), false)]) }, FieldType::Time => row_builder.insert_time_cell(75), _ => "".to_owned(), @@ -206,16 +185,18 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell(""), FieldType::Number => row_builder.insert_number_cell("2"), - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::MultiSelect => row_builder .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]), FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), FieldType::Checklist => row_builder.insert_checklist_cell(vec![ - ChecklistCellInsertChangeset::new("Have breakfast".to_string(), true), - ChecklistCellInsertChangeset::new("Have lunch".to_string(), true), - ChecklistCellInsertChangeset::new("Take a nap".to_string(), false), - ChecklistCellInsertChangeset::new("Have dinner".to_string(), true), - ChecklistCellInsertChangeset::new("Shower and head to bed".to_string(), false), + ("Have breakfast".to_string(), true), + ("Have lunch".to_string(), true), + ("Take a nap".to_string(), false), + ("Have dinner".to_string(), true), + ("Shower and head to bed".to_string(), false), ]), _ => "".to_owned(), }; @@ -226,7 +207,9 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("C"), FieldType::Number => row_builder.insert_number_cell("3"), - FieldType::DateTime => row_builder.insert_date_cell(1647251762, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1647251762, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(0)) }, @@ -243,16 +226,15 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("DA"), FieldType::Number => row_builder.insert_number_cell("14"), - FieldType::DateTime => row_builder.insert_date_cell(1668704685, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1668704685, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(0)) }, FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), FieldType::Checklist => { - row_builder.insert_checklist_cell(vec![ChecklistCellInsertChangeset::new( - "Task 1".to_string(), - true, - )]) + row_builder.insert_checklist_cell(vec![("Task 1".to_string(), true)]) }, _ => "".to_owned(), }; @@ -263,7 +245,9 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell(""), - FieldType::DateTime => row_builder.insert_date_cell(1668359085, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1668359085, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(1)) }, @@ -279,7 +263,9 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell("5"), - FieldType::DateTime => row_builder.insert_date_cell(1671938394, None, &field_type), + FieldType::DateTime => { + row_builder.insert_date_cell(1671938394, None, None, &field_type) + }, FieldType::SingleSelect => { row_builder.insert_single_select_cell(|mut options| options.remove(1)) }, @@ -288,9 +274,9 @@ pub fn make_test_grid() -> DatabaseData { }, FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), FieldType::Checklist => row_builder.insert_checklist_cell(vec![ - ChecklistCellInsertChangeset::new("Sprint".to_string(), true), - ChecklistCellInsertChangeset::new("Sprint some more".to_string(), false), - ChecklistCellInsertChangeset::new("Rest".to_string(), true), + ("Sprint".to_string(), true), + ("Sprint some more".to_string(), false), + ("Rest".to_string(), true), ]), _ => "".to_owned(), }; @@ -306,9 +292,11 @@ pub fn make_test_grid() -> DatabaseData { rows.push(row); } + let inline_view_id = gen_database_view_id(); + let view = DatabaseView { database_id: database_id.clone(), - id: gen_database_view_id(), + id: inline_view_id.clone(), name: "".to_string(), layout: DatabaseLayout::Grid, field_settings, @@ -317,6 +305,7 @@ pub fn make_test_grid() -> DatabaseData { DatabaseData { database_id, + inline_view_id, views: vec![view], fields, rows, @@ -391,9 +380,11 @@ pub fn make_no_date_test_grid() -> DatabaseData { rows.push(row); } + let inline_view_id = gen_database_view_id(); + let view = DatabaseView { database_id: database_id.clone(), - id: gen_database_view_id(), + id: inline_view_id.clone(), name: "".to_string(), layout: DatabaseLayout::Grid, field_settings, @@ -402,6 +393,7 @@ pub fn make_no_date_test_grid() -> DatabaseData { DatabaseData { database_id, + inline_view_id, views: vec![view], fields, rows, diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs index a0b3e5da3e..b47bf2e99b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs @@ -1,231 +1,297 @@ -use crate::database::pre_fill_cell_test::script::DatabasePreFillRowCellTest; use flowy_database2::entities::{ CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType, FilterDataPB, SelectOptionFilterConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB, }; +use flowy_database2::services::field::SELECTION_IDS_SEPARATOR; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating an empty row into a database that has +// active filters. Where appropriate, the row's cell data will be pre-filled +// into the row's cells before creating it in collab. #[tokio::test] async fn according_to_text_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - test - .insert_filter(FilterDataPB { + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "sample".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { field_id: text_field.id.clone(), - field_type: FieldType::RichText, - data: TextFilterPB { - condition: TextFilterConditionPB::TextContains, - content: "sample".to_string(), - } - .try_into() - .unwrap(), - }) - .await; + row_index: test.row_details.len() - 1, + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len() - 1, - test.wait(100).await; - test.create_empty_row().await; - test.wait(100).await; + expected_content: "sample".to_string(), + }, + ]; - test - .assert_cell_existence(text_field.id.clone(), test.rows.len() - 1, true) - .await; - test - .assert_cell_content(text_field.id, test.rows.len() - 1, "sample".to_string()) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_empty_text_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - test - .insert_filter(FilterDataPB { - field_id: text_field.id.clone(), - field_type: FieldType::RichText, - data: TextFilterPB { - condition: TextFilterConditionPB::TextContains, - content: "".to_string(), - } - .try_into() - .unwrap(), - }) - .await; + let text_field = test.get_first_field(FieldType::RichText); - test.wait(100).await; - test.create_empty_row().await; - test.wait(100).await; + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; - test - .assert_cell_existence(text_field.id.clone(), test.rows.len() - 1, false) - .await; + test.run_scripts(scripts).await; + + let scripts = vec![AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len() - 1, + exists: false, + }]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_text_is_not_empty_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - test.assert_row_count(7).await; + let text_field = test.get_first_field(FieldType::RichText); - test - .insert_filter(FilterDataPB { - field_id: text_field.id.clone(), - field_type: FieldType::RichText, - data: TextFilterPB { - condition: TextFilterConditionPB::TextIsNotEmpty, - content: "".to_string(), - } - .try_into() - .unwrap(), - }) - .await; + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextIsNotEmpty, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(6), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + ]; - test.wait(100).await; - test.assert_row_count(6).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(6).await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_checkbox_is_unchecked_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - test.assert_row_count(7).await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); - test - .insert_filter(FilterDataPB { - field_id: checkbox_field.id.clone(), - field_type: FieldType::Checkbox, - data: CheckboxFilterPB { - condition: CheckboxFilterConditionPB::IsUnChecked, - } - .try_into() - .unwrap(), - }) - .await; + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsUnChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(4), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(5), + ]; - test.wait(100).await; - test.assert_row_count(4).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(5).await; + test.run_scripts(scripts).await; - test - .assert_cell_existence(checkbox_field.id.clone(), 4, false) - .await; + let scripts = vec![AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: 4, + exists: false, + }]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_checkbox_is_checked_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - test.assert_row_count(7).await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); - test - .insert_filter(FilterDataPB { + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(3), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(4), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { field_id: checkbox_field.id.clone(), - field_type: FieldType::Checkbox, - data: CheckboxFilterPB { - condition: CheckboxFilterConditionPB::IsChecked, - } - .try_into() - .unwrap(), - }) - .await; + row_index: 3, + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id, + row_index: 3, - test.wait(100).await; - test.assert_row_count(3).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(4).await; + expected_content: "Yes".to_string(), + }, + ]; - test - .assert_cell_existence(checkbox_field.id.clone(), 3, true) - .await; - test - .assert_cell_content(checkbox_field.id, 3, "Yes".to_string()) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_date_time_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let datetime_field = test.get_first_field(FieldType::DateTime).await; - test.assert_row_count(7).await; + let datetime_field = test.get_first_field(FieldType::DateTime); - test - .insert_filter(FilterDataPB { + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: Some(1710510086), + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(0), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(1), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { field_id: datetime_field.id.clone(), - field_type: FieldType::DateTime, - data: DateFilterPB { - condition: DateFilterConditionPB::DateStartsOn, - timestamp: Some(1710510086), - ..Default::default() - } - .try_into() - .unwrap(), - }) - .await; + row_index: 0, + exists: true, + }, + AssertCellContent { + field_id: datetime_field.id, + row_index: 0, - test.wait(100).await; - test.assert_row_count(0).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(1).await; + expected_content: "2024/03/15".to_string(), + }, + ]; - test - .assert_cell_existence(datetime_field.id.clone(), 0, true) - .await; - test - .assert_cell_content(datetime_field.id, 0, "2024/03/15".to_string()) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_invalid_date_time_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let datetime_field = test.get_first_field(FieldType::DateTime).await; - test.assert_row_count(7).await; + let datetime_field = test.get_first_field(FieldType::DateTime); - test - .insert_filter(FilterDataPB { + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: None, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(7), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(8), + AssertCellExistence { field_id: datetime_field.id.clone(), - field_type: FieldType::DateTime, - data: DateFilterPB { - condition: DateFilterConditionPB::DateStartsOn, - timestamp: None, - ..Default::default() - } - .try_into() - .unwrap(), - }) - .await; + row_index: test.row_details.len(), + exists: false, + }, + ]; - test.wait(100).await; - test.assert_row_count(7).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(8).await; - - test - .assert_cell_existence(datetime_field.id.clone(), test.rows.len() - 1, false) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_select_option_is_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let single_select_field = test.get_first_field(FieldType::SingleSelect).await; - let options = test - .get_single_select_type_option(&single_select_field.id) - .await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); let filtering_options = [options[1].clone(), options[2].clone()]; let ids = filtering_options @@ -233,46 +299,52 @@ async fn according_to_select_option_is_filter_test() { .map(|option| option.id.clone()) .collect(); let stringified_expected = filtering_options - .first() + .iter() .map(|option| option.name.clone()) - .unwrap_or_default(); + .collect::<Vec<_>>() + .join(SELECTION_IDS_SEPARATOR); - test.assert_row_count(7).await; + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(1), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(2), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 1, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 1, - test - .insert_filter(FilterDataPB { - field_id: single_select_field.id.clone(), - field_type: FieldType::SingleSelect, - data: SelectOptionFilterPB { - condition: SelectOptionFilterConditionPB::OptionIs, - option_ids: ids, - } - .try_into() - .unwrap(), - }) - .await; + expected_content: stringified_expected, + }, + ]; - test.wait(100).await; - test.assert_row_count(2).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(3).await; - - test - .assert_cell_existence(single_select_field.id.clone(), 1, true) - .await; - test - .assert_cell_content(single_select_field.id, 1, stringified_expected) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_select_option_contains_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; - let options = test - .get_multi_select_type_option(&multi_select_field.id) - .await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); let filtering_options = [options[1].clone(), options[2].clone()]; let ids = filtering_options @@ -281,70 +353,81 @@ async fn according_to_select_option_contains_filter_test() { .collect(); let stringified_expected = filtering_options.first().unwrap().name.clone(); - test.assert_row_count(7).await; - - test - .insert_filter(FilterDataPB { + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { field_id: multi_select_field.id.clone(), - field_type: FieldType::MultiSelect, - data: SelectOptionFilterPB { - condition: SelectOptionFilterConditionPB::OptionContains, - option_ids: ids, - } - .try_into() - .unwrap(), - }) - .await; + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, - test.wait(100).await; - test.assert_row_count(5).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(6).await; + expected_content: stringified_expected, + }, + ]; - test - .assert_cell_existence(multi_select_field.id.clone(), 5, true) - .await; - test - .assert_cell_content(multi_select_field.id, 5, stringified_expected) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn according_to_select_option_is_not_empty_filter_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; - let options = test - .get_multi_select_type_option(&multi_select_field.id) - .await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); let stringified_expected = options.first().unwrap().name.clone(); - test.assert_row_count(7).await; - - test - .insert_filter(FilterDataPB { + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { field_id: multi_select_field.id.clone(), - field_type: FieldType::MultiSelect, - data: SelectOptionFilterPB { - condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, - ..Default::default() - } - .try_into() - .unwrap(), - }) - .await; + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, - test.wait(100).await; - test.assert_row_count(5).await; - test.create_empty_row().await; - test.wait(100).await; - test.assert_row_count(6).await; + expected_content: stringified_expected, + }, + ]; - test - .assert_cell_existence(multi_select_field.id.clone(), 5, true) - .await; - test - .assert_cell_content(multi_select_field.id, 5, stringified_expected) - .await; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs index 8007051037..a67bad48f3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs @@ -1,235 +1,315 @@ -use crate::database::pre_fill_cell_test::script::DatabasePreFillRowCellTest; -use collab_database::fields::date_type_option::DateCellData; -use collab_database::fields::select_type_option::SELECTION_IDS_SEPARATOR; -use collab_database::template::util::ToCellString; -use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; use std::collections::HashMap; +use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; +use flowy_database2::services::field::{DateCellData, SELECTION_IDS_SEPARATOR}; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating a row using `CreateRowPayloadPB` that passes +// in some cell data in its `data` field of `HashMap<String, String>` which is a +// map of `field_id` to its corresponding cell data as a String. If valid, the cell +// data will be pre-filled into the row's cells before creating it in collab. + #[tokio::test] async fn row_data_payload_with_empty_hashmap_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::new(), - ..Default::default() - }) - .await; + let text_field = test.get_first_field(FieldType::RichText); - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(text_field.id.clone(), index, false) - .await; - test - .assert_cell_content(text_field.id, index, "".to_string()) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::new(), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + + expected_content: "".to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_unknown_field_id_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; + + let text_field = test.get_first_field(FieldType::RichText); let malformed_field_id = "this_field_id_will_never_exist"; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([( - malformed_field_id.to_string(), - "sample cell data".to_string(), - )]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([( + malformed_field_id.to_string(), + "sample cell data".to_string(), + )]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(text_field.id.clone(), index, false) - .await; - test - .assert_cell_content(text_field.id.clone(), index, "".to_string()) - .await; - test - .assert_cell_existence(malformed_field_id.to_string(), index, false) - .await; + expected_content: "".to_string(), + }, + AssertCellExistence { + field_id: malformed_field_id.to_string(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_empty_string_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; + + let text_field = test.get_first_field(FieldType::RichText); let cell_data = ""; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(text_field.id.clone(), index, true) - .await; - test - .assert_cell_content(text_field.id, index, cell_data.to_string()) - .await; + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; + + let text_field = test.get_first_field(FieldType::RichText); let cell_data = "sample cell data"; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(text_field.id.clone(), index, true) - .await; - test - .assert_cell_content(text_field.id.clone(), index, cell_data.to_string()) - .await; + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_multi_text_data_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - let number_field = test.get_first_field(FieldType::Number).await; - let url_field = test.get_first_field(FieldType::URL).await; + + let text_field = test.get_first_field(FieldType::RichText); + let number_field = test.get_first_field(FieldType::Number); + let url_field = test.get_first_field(FieldType::URL); let text_cell_data = "sample cell data"; let number_cell_data = "1234"; let url_cell_data = "appflowy.io"; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([ - (text_field.id.clone(), text_cell_data.to_string()), - (number_field.id.clone(), number_cell_data.to_string()), - (url_field.id.clone(), url_cell_data.to_string()), - ]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([ + (text_field.id.clone(), text_cell_data.to_string()), + (number_field.id.clone(), number_cell_data.to_string()), + (url_field.id.clone(), url_cell_data.to_string()), + ]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(text_field.id.clone(), index, true) - .await; - test - .assert_cell_content(text_field.id, index, text_cell_data.to_string()) - .await; - test - .assert_cell_existence(number_field.id.clone(), index, true) - .await; - test - .assert_cell_content(number_field.id, index, "$1,234".to_string()) - .await; - test - .assert_cell_existence(url_field.id.clone(), index, true) - .await; - test - .assert_cell_content(url_field.id, index, url_cell_data.to_string()) - .await; + expected_content: text_cell_data.to_string(), + }, + AssertCellExistence { + field_id: number_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: number_field.id, + row_index: test.row_details.len(), + + expected_content: "$1,234".to_string(), + }, + AssertCellExistence { + field_id: url_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: url_field.id, + row_index: test.row_details.len(), + + expected_content: url_cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_date_time_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime).await; + + let date_field = test.get_first_field(FieldType::DateTime); let cell_data = "1710510086"; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(date_field.id.clone(), index, true) - .await; - test - .assert_cell_content(date_field.id.clone(), index, "2024/03/15".to_string()) - .await; + expected_content: "2024/03/15".to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_invalid_date_time_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime).await; + + let date_field = test.get_first_field(FieldType::DateTime); let cell_data = DateCellData { timestamp: Some(1710510086), ..Default::default() } - .to_cell_string(); + .to_string(); - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + ]; - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(date_field.id.clone(), index, false) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_checkbox_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); let cell_data = "Yes"; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(checkbox_field.id.clone(), cell_data.to_string())]), - ..Default::default() - }) - .await; - let index = test.rows.len() - 1; - test.wait(100).await; - test - .assert_cell_existence(checkbox_field.id.clone(), index, true) - .await; - test - .assert_cell_content(checkbox_field.id.clone(), index, cell_data.to_string()) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(checkbox_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_select_option_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; - let options = test - .get_multi_select_type_option(&multi_select_field.id) - .await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); let ids = options .iter() @@ -243,60 +323,71 @@ async fn row_data_payload_with_select_option_test() { .collect::<Vec<_>>() .join(SELECTION_IDS_SEPARATOR); - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(multi_select_field.id.clone(), ids)]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids)]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(multi_select_field.id.clone(), index, true) - .await; - test - .assert_cell_content(multi_select_field.id.clone(), index, stringified_cell_data) - .await; + expected_content: stringified_cell_data, + }, + ]; + + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_invalid_select_option_id_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let multi_select_field = test.get_first_field(FieldType::MultiSelect).await; - let mut options = test - .get_multi_select_type_option(&multi_select_field.id) - .await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&multi_select_field.id); let first_id = options.swap_remove(0).id; let ids = [first_id.clone(), "nonsense".to_string()].join(SELECTION_IDS_SEPARATOR); - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(multi_select_field.id.clone(), ids.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: first_id, + }, + ]; - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(multi_select_field.id.clone(), index, true) - .await; - test - .assert_select_option_cell_strict(multi_select_field.id.clone(), index, first_id) - .await; + test.run_scripts(scripts).await; } #[tokio::test] async fn row_data_payload_with_too_many_select_option_test() { let mut test = DatabasePreFillRowCellTest::new().await; - let single_select_field = test.get_first_field(FieldType::SingleSelect).await; - let mut options = test - .get_single_select_type_option(&single_select_field.id) - .await; + + let single_select_field = test.get_first_field(FieldType::SingleSelect); + let mut options = test.get_single_select_type_option(&single_select_field.id); let ids = options .iter() @@ -306,20 +397,26 @@ async fn row_data_payload_with_too_many_select_option_test() { let stringified_cell_data = options.swap_remove(0).id; - test - .create_row_with_payload(CreateRowPayloadPB { - view_id: test.view_id.clone(), - data: HashMap::from([(single_select_field.id.clone(), ids.to_string())]), - ..Default::default() - }) - .await; + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(single_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: stringified_cell_data, + }, + ]; - test.wait(100).await; - let index = test.rows.len() - 1; - test - .assert_cell_existence(single_select_field.id.clone(), index, true) - .await; - test - .assert_select_option_cell_strict(single_select_field.id.clone(), index, stringified_cell_data) - .await; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs index 8bc93cf0de..e41e42207e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs @@ -1,10 +1,41 @@ -use crate::database::database_editor::DatabaseEditorTest; -use collab_database::fields::select_type_option::{SelectOptionIds, SELECTION_IDS_SEPARATOR}; -use flowy_database2::entities::{CreateRowPayloadPB, FilterDataPB, InsertFilterPB}; -use flowy_database2::services::cell::stringify_cell; use std::ops::{Deref, DerefMut}; use std::time::Duration; +use flowy_database2::entities::{CreateRowPayloadPB, FilterDataPB, InsertFilterPB}; +use flowy_database2::services::cell::stringify_cell; +use flowy_database2::services::field::{SelectOptionIds, SELECTION_IDS_SEPARATOR}; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum PreFillRowCellTestScript { + CreateEmptyRow, + CreateRowWithPayload { + payload: CreateRowPayloadPB, + }, + InsertFilter { + filter: FilterDataPB, + }, + AssertRowCount(usize), + AssertCellExistence { + field_id: String, + row_index: usize, + exists: bool, + }, + AssertCellContent { + field_id: String, + row_index: usize, + expected_content: String, + }, + AssertSelectOptionCellStrict { + field_id: String, + row_index: usize, + expected_content: String, + }, + Wait { + milliseconds: u64, + }, +} + pub struct DatabasePreFillRowCellTest { inner: DatabaseEditorTest, } @@ -15,83 +46,103 @@ impl DatabasePreFillRowCellTest { Self { inner: editor_test } } - pub async fn create_empty_row(&mut self) { - let params = CreateRowPayloadPB { - view_id: self.view_id.clone(), - ..Default::default() - }; - let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); - self - .row_by_row_id - .insert(row_detail.row.id.to_string(), row_detail.into()); - self.rows = self.get_rows().await; + pub async fn run_scripts(&mut self, scripts: Vec<PreFillRowCellTestScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn create_row_with_payload(&mut self, payload: CreateRowPayloadPB) { - let row_detail = self.editor.create_row(payload).await.unwrap().unwrap(); - self - .row_by_row_id - .insert(row_detail.row.id.to_string(), row_detail.into()); - self.rows = self.get_rows().await; - } - - pub async fn insert_filter(&mut self, filter: FilterDataPB) { - self - .editor - .modify_view_filters( - &self.view_id, - InsertFilterPB { - parent_filter_id: None, - data: filter, - } - .try_into() + pub async fn run_script(&mut self, script: PreFillRowCellTestScript) { + match script { + PreFillRowCellTestScript::CreateEmptyRow => { + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::CreateRowWithPayload { payload } => { + let row_detail = self.editor.create_row(payload).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::InsertFilter { filter } => self + .editor + .modify_view_filters( + &self.view_id, + InsertFilterPB { + parent_filter_id: None, + data: filter, + } + .try_into() + .unwrap(), + ) + .await .unwrap(), - ) - .await - .unwrap(); - } + PreFillRowCellTestScript::AssertRowCount(expected_row_count) => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + assert_eq!(expected_row_count, rows.len()); + }, + PreFillRowCellTestScript::AssertCellExistence { + field_id, + row_index, + exists, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); - pub async fn assert_row_count(&self, expected_row_count: usize) { - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - assert_eq!(expected_row_count, rows.len()); - } + let cell = row_detail.row.cells.get(&field_id).cloned(); - pub async fn assert_cell_existence(&self, field_id: String, row_index: usize, exists: bool) { - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - let row = rows.get(row_index).unwrap(); - let cell = row.cells.get(&field_id).cloned(); - assert_eq!(exists, cell.is_some()); - } + assert_eq!(exists, cell.is_some()); + }, + PreFillRowCellTestScript::AssertCellContent { + field_id, + row_index, + expected_content, + } => { + let field = self.editor.get_field(&field_id).unwrap(); - pub async fn assert_cell_content( - &self, - field_id: String, - row_index: usize, - expected_content: String, - ) { - let field = self.editor.get_field(&field_id).await.unwrap(); - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - let row = rows.get(row_index).unwrap(); - let cell = row.cells.get(&field_id).cloned().unwrap_or_default(); - let content = stringify_cell(&cell, &field); - assert_eq!(content, expected_content); - } + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); - pub async fn assert_select_option_cell_strict( - &self, - field_id: String, - row_index: usize, - expected_content: String, - ) { - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - let row = rows.get(row_index).unwrap(); - let cell = row.cells.get(&field_id).cloned().unwrap_or_default(); - let content = SelectOptionIds::from(&cell).join(SELECTION_IDS_SEPARATOR); - assert_eq!(content, expected_content); - } + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + let content = stringify_cell(&cell, &field); + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::AssertSelectOptionCellStrict { + field_id, + row_index, + expected_content, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); - pub async fn wait(&self, milliseconds: u64) { - tokio::time::sleep(Duration::from_millis(milliseconds)).await; + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + + let content = SelectOptionIds::from(&cell).join(SELECTION_IDS_SEPARATOR); + + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::Wait { milliseconds } => { + tokio::time::sleep(Duration::from_millis(milliseconds)).await; + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index 72a85d1340..3fbb0aafe2 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -32,8 +32,8 @@ async fn export_and_then_import_meta_csv_test() { let result = test.import(csv_1.clone(), format).await; let database = test.get_database(&result.database_id).await.unwrap(); - let fields = database.get_fields(&result.view_id, None).await; - let rows = database.get_all_rows(&result.view_id).await.unwrap(); + let fields = database.get_fields(&result.view_id, None); + let rows = database.get_rows(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); assert_eq!(fields[2].field_type, 2); @@ -46,8 +46,8 @@ async fn export_and_then_import_meta_csv_test() { assert_eq!(fields[9].field_type, 9); for field in fields { - for (index, row) in rows.iter().enumerate() { - if let Some(cell) = row.cells.get(&field.id) { + for (index, row_detail) in rows.iter().enumerate() { + if let Some(cell) = row_detail.row.cells.get(&field.id) { let field_type = FieldType::from(field.field_type); let s = stringify_cell(cell, &field); match &field_type { @@ -76,21 +76,20 @@ async fn export_and_then_import_meta_csv_test() { assert_eq!(s, "Google,Facebook"); } }, - FieldType::Checkbox - | FieldType::URL - | FieldType::Checklist - | FieldType::LastEditedTime - | FieldType::CreatedTime - | FieldType::Relation - | FieldType::Summary - | FieldType::Time - | FieldType::Translate - | FieldType::Media => {}, + FieldType::Checkbox => {}, + FieldType::URL => {}, + FieldType::Checklist => {}, + FieldType::LastEditedTime => {}, + FieldType::CreatedTime => {}, + FieldType::Relation => {}, + FieldType::Summary => {}, + FieldType::Time => {}, + FieldType::Translate => {}, } } else { panic!( "Can not found the cell with id: {} in {:?}", - field.id, row.cells + field.id, row_detail.row.cells ); } } @@ -112,8 +111,8 @@ async fn history_database_import_test() { let result = test.import(csv.to_string(), format).await; let database = test.get_database(&result.database_id).await.unwrap(); - let fields = database.get_fields(&result.view_id, None).await; - let rows = database.get_all_rows(&result.view_id).await.unwrap(); + let fields = database.get_fields(&result.view_id, None); + let rows = database.get_rows(&result.view_id).await.unwrap(); assert_eq!(fields[0].field_type, 0); assert_eq!(fields[1].field_type, 1); assert_eq!(fields[2].field_type, 2); @@ -124,8 +123,8 @@ async fn history_database_import_test() { assert_eq!(fields[7].field_type, 7); for field in fields { - for (index, row) in rows.iter().enumerate() { - if let Some(cell) = row.cells.get(&field.id) { + for (index, row_detail) in rows.iter().enumerate() { + if let Some(cell) = row_detail.row.cells.get(&field.id) { let field_type = FieldType::from(field.field_type); let s = stringify_cell(cell, &field); match &field_type { @@ -164,19 +163,18 @@ async fn history_database_import_test() { assert_eq!(s, "AppFlowy website - https://www.appflowy.io"); } }, - FieldType::Checklist - | FieldType::LastEditedTime - | FieldType::CreatedTime - | FieldType::Relation - | FieldType::Summary - | FieldType::Time - | FieldType::Translate - | FieldType::Media => {}, + FieldType::Checklist => {}, + FieldType::LastEditedTime => {}, + FieldType::CreatedTime => {}, + FieldType::Relation => {}, + FieldType::Summary => {}, + FieldType::Time => {}, + FieldType::Translate => {}, } } else { panic!( "Can not found the cell with id: {} in {:?}", - field.id, row.cells + field.id, row_detail.row.cells ); } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs index ca15cc309b..7fe1874984 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs @@ -2,125 +2,100 @@ use flowy_database2::entities::FieldType; use flowy_database2::services::sort::SortCondition; use crate::database::sort_test::script::DatabaseSortTest; +use crate::database::sort_test::script::SortScript::*; #[tokio::test] async fn sort_checkbox_and_then_text_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - let text_field = test.get_first_field(FieldType::RichText).await; - - // Assert initial cell content order for checkbox and text fields - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; - - // Insert checkbox sort (Descending) - test - .insert_sort(checkbox_field.clone(), SortCondition::Descending) - .await; - - // Assert sorted order for checkbox and text fields - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "Yes", "No", "No", "No", ""], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "AE", "C", "DA", "AE", "CB"], - ) - .await; - - // Insert text sort (Ascending) - test - .insert_sort(text_field.clone(), SortCondition::Ascending) - .await; - - // Assert sorted order after adding text sort - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "Yes", "No", "No", "", "No"], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "", "AE", "C", "CB", "DA"], - ) - .await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + // Insert checkbox sort + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "No", ""], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "AE", "C", "DA", "AE", "CB"], + }, + // Insert text sort + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "", "No"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "", "AE", "C", "CB", "DA"], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn reorder_sort_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - let text_field = test.get_first_field(FieldType::RichText).await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let text_field = test.get_first_field(FieldType::RichText); + // Use the same sort set up as above + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "", "No"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "", "AE", "C", "CB", "DA"], + }, + ]; + test.run_scripts(scripts).await; - // Assert initial cell content order for checkbox and text fields - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; - - // Insert checkbox and text sorts - test - .insert_sort(checkbox_field.clone(), SortCondition::Descending) - .await; - test - .insert_sort(text_field.clone(), SortCondition::Ascending) - .await; - - // Assert sorted order after applying both sorts - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "Yes", "No", "No", "", "No"], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "", "AE", "C", "CB", "DA"], - ) - .await; - - // Reorder sorts let sorts = test.editor.get_all_sorts(&test.view_id).await.items; - test - .reorder_sort(sorts[1].id.clone(), sorts[0].id.clone()) - .await; - - // Assert the order after reorder - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "", "No", "Yes"], - ) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "AE", "C", "CB", "DA", ""], - ) - .await; + let scripts = vec![ + ReorderSort { + from_sort_id: sorts[1].id.clone(), + to_sort_id: sorts[0].id.clone(), + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "", "No", "Yes"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs index 75694401b6..a6b99dc99c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -7,15 +7,44 @@ use collab_database::rows::RowId; use futures::stream::StreamExt; use tokio::sync::broadcast::Receiver; -use crate::database::database_editor::DatabaseEditorTest; use flowy_database2::entities::{ - CreateRowPayloadPB, DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB, + CreateRowPayloadPB, DeleteSortPayloadPB, ReorderSortPayloadPB, UpdateSortPayloadPB, }; use flowy_database2::services::cell::stringify_cell; use flowy_database2::services::database_view::DatabaseViewChanged; -use flowy_database2::services::filter::{FilterChangeset, FilterInner}; use flowy_database2::services::sort::SortCondition; -use lib_infra::box_any::BoxAny; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum SortScript { + InsertSort { + field: Field, + condition: SortCondition, + }, + ReorderSort { + from_sort_id: String, + to_sort_id: String, + }, + DeleteSort { + sort_id: String, + }, + AssertCellContentOrder { + field_id: String, + orders: Vec<&'static str>, + }, + UpdateTextCell { + row_id: RowId, + text: String, + }, + AddNewRow, + AssertSortChanged { + old_row_orders: Vec<&'static str>, + new_row_orders: Vec<&'static str>, + }, + Wait { + millis: u64, + }, +} pub struct DatabaseSortTest { inner: DatabaseEditorTest, @@ -30,144 +59,131 @@ impl DatabaseSortTest { recv: None, } } - - pub async fn insert_sort(&mut self, field: Field, condition: SortCondition) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id) - .await - .unwrap(), - ); - let params = UpdateSortPayloadPB { - view_id: self.view_id.clone(), - field_id: field.id.clone(), - sort_id: None, - condition: condition.into(), - }; - self.editor.create_or_update_sort(params).await.unwrap(); + pub async fn run_scripts(&mut self, scripts: Vec<SortScript>) { + for script in scripts { + self.run_script(script).await; + } } - pub async fn insert_filter(&mut self, field_type: FieldType, data: BoxAny) { - let field = self.get_first_field(field_type).await; - let params = FilterChangeset::Insert { - parent_filter_id: None, - data: FilterInner::Data { - field_id: field.id, - field_type, - condition_and_content: data, + pub async fn run_script(&mut self, script: SortScript) { + match script { + SortScript::InsertSort { condition, field } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = UpdateSortPayloadPB { + view_id: self.view_id.clone(), + field_id: field.id.clone(), + sort_id: None, + condition: condition.into(), + }; + let _ = self.editor.create_or_update_sort(params).await.unwrap(); + }, + SortScript::ReorderSort { + from_sort_id, + to_sort_id, + } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = ReorderSortPayloadPB { + view_id: self.view_id.clone(), + from_sort_id, + to_sort_id, + }; + self.editor.reorder_sort(params).await.unwrap(); + }, + SortScript::DeleteSort { sort_id } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = DeleteSortPayloadPB { + view_id: self.view_id.clone(), + sort_id, + }; + self.editor.delete_sort(params).await.unwrap(); + }, + SortScript::AssertCellContentOrder { field_id, orders } => { + let mut cells = vec![]; + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let field = self.editor.get_field(&field_id).unwrap(); + for row_detail in rows { + if let Some(cell) = row_detail.row.cells.get(&field_id) { + let content = stringify_cell(cell, &field); + cells.push(content); + } else { + cells.push("".to_string()); + } + } + if orders.is_empty() { + assert_eq!(cells, orders); + } else { + let len = min(cells.len(), orders.len()); + assert_eq!(cells.split_at(len).0, orders); + } + }, + SortScript::UpdateTextCell { row_id, text } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + self.update_text_cell(row_id, &text).await.unwrap(); + }, + SortScript::AddNewRow => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + self + .editor + .create_row(CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }) + .await + .unwrap(); + }, + SortScript::AssertSortChanged { + new_row_orders, + old_row_orders, + } => { + if let Some(receiver) = self.recv.take() { + assert_sort_changed( + receiver, + new_row_orders + .into_iter() + .map(|order| order.to_owned()) + .collect(), + old_row_orders + .into_iter() + .map(|order| order.to_owned()) + .collect(), + ) + .await; + } + }, + SortScript::Wait { millis } => { + tokio::time::sleep(Duration::from_millis(millis)).await; }, - }; - self - .editor - .modify_view_filters(&self.view_id, params) - .await - .unwrap(); - } - - pub async fn reorder_sort(&mut self, from_sort_id: String, to_sort_id: String) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id) - .await - .unwrap(), - ); - let params = ReorderSortPayloadPB { - view_id: self.view_id.clone(), - from_sort_id, - to_sort_id, - }; - self.editor.reorder_sort(params).await.unwrap(); - } - - pub async fn delete_sort(&mut self, sort_id: String) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id) - .await - .unwrap(), - ); - let params = DeleteSortPayloadPB { - view_id: self.view_id.clone(), - sort_id, - }; - self.editor.delete_sort(params).await.unwrap(); - } - - pub async fn assert_cell_content_order(&mut self, field_id: String, orders: Vec<&'static str>) { - let mut cells = vec![]; - let rows = self.editor.get_all_rows(&self.view_id).await.unwrap(); - let field = self.editor.get_field(&field_id).await.unwrap(); - for row in rows { - if let Some(cell) = row.cells.get(&field_id) { - let content = stringify_cell(cell, &field); - cells.push(content); - } else { - cells.push("".to_string()); - } } - if orders.is_empty() { - assert_eq!(cells, orders); - } else { - let len = min(cells.len(), orders.len()); - assert_eq!(cells.split_at(len).0, orders); - } - } - - pub async fn update_text_cell(&mut self, row_id: RowId, text: String) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id) - .await - .unwrap(), - ); - self.inner.update_text_cell(row_id, &text).await.unwrap(); - } - - pub async fn add_new_row(&mut self) { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id) - .await - .unwrap(), - ); - self - .editor - .create_row(CreateRowPayloadPB { - view_id: self.view_id.clone(), - ..Default::default() - }) - .await - .unwrap(); - } - - pub async fn assert_sort_changed( - &mut self, - new_row_orders: Vec<&'static str>, - old_row_orders: Vec<&'static str>, - ) { - if let Some(receiver) = self.recv.take() { - assert_sort_changed( - receiver, - new_row_orders - .into_iter() - .map(|order| order.to_owned()) - .collect(), - old_row_orders - .into_iter() - .map(|order| order.to_owned()) - .collect(), - ) - .await; - } - } - - pub async fn wait(&mut self, millis: u64) { - tokio::time::sleep(Duration::from_millis(millis)).await; } } @@ -195,6 +211,7 @@ async fn assert_sort_changed( old_row_orders.insert(changed.new_index, old); assert_eq!(old_row_orders, new_row_orders); }, + DatabaseViewChanged::InsertRowNotification(_changed) => {}, _ => {}, } }) diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs index dbcf86ad4f..63f3b08422 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -1,205 +1,189 @@ -use crate::database::sort_test::script::DatabaseSortTest; -use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, FieldType}; +use flowy_database2::entities::FieldType; use flowy_database2::services::sort::SortCondition; -use lib_infra::box_any::BoxAny; + +use crate::database::sort_test::script::{DatabaseSortTest, SortScript::*}; #[tokio::test] async fn sort_text_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; - test - .insert_sort(text_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "AE", "C", "CB", "DA", ""], - ) - .await; - - let checkbox_filter = CheckboxFilterPB { - condition: CheckboxFilterConditionPB::IsChecked, - }; - test - .insert_filter(FieldType::Checkbox, BoxAny::new(checkbox_filter)) - .await; - - test - .assert_cell_content_order(text_field.id.clone(), vec!["A", "AE", ""]) - .await; + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_text_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; - test - .insert_sort(text_field.clone(), SortCondition::Descending) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["DA", "CB", "C", "AE", "AE", "A", ""], - ) - .await; + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["DA", "CB", "C", "AE", "AE", "A", ""], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_change_notification_by_update_text_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; + let text_field = test.get_first_field(FieldType::RichText).clone(); + let scripts = vec![ + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], + }, + // Wait the insert task to finish. The cost of time should be less than 200 milliseconds. + Wait { millis: 200 }, + ]; + test.run_scripts(scripts).await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; - test - .insert_sort(text_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "AE", "C", "CB", "DA", ""], - ) - .await; - test.wait(200).await; - - let row = test.get_rows().await; - test - .update_text_cell(row[1].id.clone(), "E".to_string()) - .await; - test - .assert_sort_changed( - vec!["A", "AE", "C", "CB", "DA", "E", ""], - vec!["A", "E", "AE", "C", "CB", "DA", ""], - ) - .await; + let row_details = test.get_rows().await; + let scripts = vec![ + UpdateTextCell { + row_id: row_details[1].row.id.clone(), + text: "E".to_string(), + }, + AssertSortChanged { + old_row_orders: vec!["A", "E", "AE", "C", "CB", "DA", ""], + new_row_orders: vec!["A", "AE", "C", "CB", "DA", "E", ""], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_after_new_row_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], - ) - .await; - test - .insert_sort(checkbox_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["No", "No", "No", "", "Yes", "Yes", "Yes"], - ) - .await; - - test.add_new_row().await; - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["No", "No", "No", "", "", "Yes", "Yes", "Yes"], - ) - .await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["No", "No", "No", "", "Yes", "Yes", "Yes"], + }, + AddNewRow {}, + AssertCellContentOrder { + field_id: checkbox_field.id, + orders: vec!["No", "No", "No", "", "", "Yes", "Yes", "Yes"], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_text_by_ascending_and_delete_sort_test() { let mut test = DatabaseSortTest::new().await; - let text_field = test.get_first_field(FieldType::RichText).await; - - test - .insert_sort(text_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "AE", "AE", "C", "CB", "DA", ""], - ) - .await; + let text_field = test.get_first_field(FieldType::RichText); + let scripts = vec![ + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], + }, + ]; + test.run_scripts(scripts).await; let sort = test.editor.get_all_sorts(&test.view_id).await.items[0].clone(); - test.delete_sort(sort.id.clone()).await; - test - .assert_cell_content_order( - text_field.id.clone(), - vec!["A", "", "C", "DA", "AE", "AE", "CB"], - ) - .await; + let scripts = vec![ + DeleteSort { sort_id: sort.id }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_checkbox_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], - ) - .await; - test - .insert_sort(checkbox_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["No", "No", "No", "", "Yes", "Yes", "Yes"], - ) - .await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["No", "No", "No", "", "Yes", "Yes", "Yes"], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_checkbox_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let checkbox_field = test.get_first_field(FieldType::Checkbox).await; - - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], - ) - .await; - test - .insert_sort(checkbox_field.clone(), SortCondition::Descending) - .await; - test - .assert_cell_content_order( - checkbox_field.id.clone(), - vec!["Yes", "Yes", "Yes", "No", "No", "No", ""], - ) - .await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "No", ""], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_date_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime).await; - - test - .assert_cell_content_order( - date_field.id.clone(), - vec![ + let date_field = test.get_first_field(FieldType::DateTime); + let scripts = vec![ + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ "2022/03/14", "2022/03/14", "2022/03/14", @@ -208,15 +192,14 @@ async fn sort_date_by_ascending_test() { "2022/12/25", "", ], - ) - .await; - test - .insert_sort(date_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - date_field.id.clone(), - vec![ + }, + InsertSort { + field: date_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ "2022/03/14", "2022/03/14", "2022/03/14", @@ -225,19 +208,19 @@ async fn sort_date_by_ascending_test() { "2022/12/25", "", ], - ) - .await; + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_date_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let date_field = test.get_first_field(FieldType::DateTime).await; - - test - .assert_cell_content_order( - date_field.id.clone(), - vec![ + let date_field = test.get_first_field(FieldType::DateTime); + let scripts = vec![ + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ "2022/03/14", "2022/03/14", "2022/03/14", @@ -246,15 +229,14 @@ async fn sort_date_by_descending_test() { "2022/12/25", "", ], - ) - .await; - test - .insert_sort(date_field.clone(), SortCondition::Descending) - .await; - test - .assert_cell_content_order( - date_field.id.clone(), - vec![ + }, + InsertSort { + field: date_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: date_field.id.clone(), + orders: vec![ "2022/12/25", "2022/11/17", "2022/11/13", @@ -263,50 +245,239 @@ async fn sort_date_by_descending_test() { "2022/03/14", "", ], - ) - .await; + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_number_by_ascending_test() { let mut test = DatabaseSortTest::new().await; - let number_field = test.get_first_field(FieldType::Number).await; - - test - .assert_cell_content_order( - number_field.id.clone(), - vec!["$1", "$2", "$3", "$14", "", "$5", ""], - ) - .await; - test - .insert_sort(number_field.clone(), SortCondition::Ascending) - .await; - test - .assert_cell_content_order( - number_field.id.clone(), - vec!["$1", "$2", "$3", "$5", "$14", "", ""], - ) - .await; + let number_field = test.get_first_field(FieldType::Number); + let scripts = vec![ + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$1", "$2", "$3", "$14", "", "$5", ""], + }, + InsertSort { + field: number_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$1", "$2", "$3", "$5", "$14", "", ""], + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] async fn sort_number_by_descending_test() { let mut test = DatabaseSortTest::new().await; - let number_field = test.get_first_field(FieldType::Number).await; - - test - .assert_cell_content_order( - number_field.id.clone(), - vec!["$1", "$2", "$3", "$14", "", "$5", ""], - ) - .await; - test - .insert_sort(number_field.clone(), SortCondition::Descending) - .await; - test - .assert_cell_content_order( - number_field.id.clone(), - vec!["$14", "$5", "$3", "$2", "$1", "", ""], - ) - .await; + let number_field = test.get_first_field(FieldType::Number); + let scripts = vec![ + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$1", "$2", "$3", "$14", "", "$5", ""], + }, + InsertSort { + field: number_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: number_field.id.clone(), + orders: vec!["$14", "$5", "$3", "$2", "$1", "", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_single_select_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let single_select = test.get_first_field(FieldType::SingleSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["", "", "Completed", "Completed", "Planned", "Planned", ""], + }, + InsertSort { + field: single_select.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["Completed", "Completed", "Planned", "Planned", "", "", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_single_select_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let single_select = test.get_first_field(FieldType::SingleSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["", "", "Completed", "Completed", "Planned", "Planned", ""], + }, + InsertSort { + field: single_select.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: single_select.id.clone(), + orders: vec!["Planned", "Planned", "Completed", "Completed", "", "", ""], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_multi_select_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let multi_select = test.get_first_field(FieldType::MultiSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec![ + "Google,Facebook", + "Google,Twitter", + "Facebook,Google,Twitter", + "", + "Facebook,Twitter", + "Facebook", + "", + ], + }, + InsertSort { + field: multi_select.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec![ + "Facebook", + "Facebook,Twitter", + "Google,Facebook", + "Google,Twitter", + "Facebook,Google,Twitter", + "", + "", + ], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_multi_select_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let multi_select = test.get_first_field(FieldType::MultiSelect); + let scripts = vec![ + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec![ + "Google,Facebook", + "Google,Twitter", + "Facebook,Google,Twitter", + "", + "Facebook,Twitter", + "Facebook", + "", + ], + }, + InsertSort { + field: multi_select.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: multi_select.id.clone(), + orders: vec![ + "Facebook,Google,Twitter", + "Google,Twitter", + "Google,Facebook", + "Facebook,Twitter", + "Facebook", + "", + "", + ], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_checklist_by_ascending_test() { + let mut test = DatabaseSortTest::new().await; + let checklist_field = test.get_first_field(FieldType::Checklist); + let scripts = vec![ + AssertCellContentOrder { + field_id: checklist_field.id.clone(), + orders: vec![ + "First thing", + "Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed", + "", + "Task 1", + "", + "Sprint,Sprint some more,Rest", + "", + ], + }, + InsertSort { + field: checklist_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checklist_field.id.clone(), + orders: vec![ + "First thing", + "Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed", + "Sprint,Sprint some more,Rest", + "Task 1", + "", + "", + "", + ], + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn sort_checklist_by_descending_test() { + let mut test = DatabaseSortTest::new().await; + let checklist_field = test.get_first_field(FieldType::Checklist); + let scripts = vec![ + AssertCellContentOrder { + field_id: checklist_field.id.clone(), + orders: vec![ + "First thing", + "Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed", + "", + "Task 1", + "", + "Sprint,Sprint some more,Rest", + "", + ], + }, + InsertSort { + field: checklist_field.clone(), + condition: SortCondition::Descending, + }, + AssertCellContentOrder { + field_id: checklist_field.id.clone(), + orders: vec![ + "Task 1", + "Sprint,Sprint some more,Rest", + "Have breakfast,Have lunch,Take a nap,Have dinner,Shower and head to bed", + "First thing", + "", + "", + "", + ], + }, + ]; + test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-date/Cargo.toml b/frontend/rust-lib/flowy-date/Cargo.toml index d04dfd8416..40015cad77 100644 --- a/frontend/rust-lib/flowy-date/Cargo.toml +++ b/frontend/rust-lib/flowy-date/Cargo.toml @@ -24,3 +24,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-date/build.rs b/frontend/rust-lib/flowy-date/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-date/build.rs +++ b/frontend/rust-lib/flowy-date/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-document-pub/Cargo.toml b/frontend/rust-lib/flowy-document-pub/Cargo.toml index cbb74de5c4..93a282f5cc 100644 --- a/frontend/rust-lib/flowy-document-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-document-pub/Cargo.toml @@ -9,5 +9,5 @@ edition = "2021" lib-infra = { workspace = true } flowy-error = { workspace = true } collab-document = { workspace = true } -collab = { workspace = true } -uuid.workspace = true \ No newline at end of file +anyhow.workspace = true +collab = { workspace = true } \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document-pub/src/cloud.rs b/frontend/rust-lib/flowy-document-pub/src/cloud.rs index d5c25053a8..2f4da1bd37 100644 --- a/frontend/rust-lib/flowy-document-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-pub/src/cloud.rs @@ -1,39 +1,31 @@ -use collab::entity::EncodedCollab; +use anyhow::Error; pub use collab_document::blocks::DocumentData; + use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; +use lib_infra::future::FutureResult; /// A trait for document cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. -#[async_trait] pub trait DocumentCloudService: Send + Sync + 'static { - async fn get_document_doc_state( + fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError>; + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError>; - async fn get_document_snapshots( + fn get_document_snapshots( &self, - document_id: &Uuid, + document_id: &str, limit: usize, workspace_id: &str, - ) -> Result<Vec<DocumentSnapshot>, FlowyError>; + ) -> FutureResult<Vec<DocumentSnapshot>, Error>; - async fn get_document_data( + fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Option<DocumentData>, FlowyError>; - - async fn create_document_collab( - &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError>; + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Option<DocumentData>, Error>; } pub struct DocumentSnapshot { diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index aaaef4938e..1b2cf3c593 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -14,16 +14,17 @@ collab-entity = { workspace = true } collab-plugins = { workspace = true } collab-integrate = { workspace = true } flowy-document-pub = { workspace = true } -flowy-storage-pub = { workspace = true } +flowy-storage = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } flowy-error = { path = "../flowy-error", features = ["impl_from_serde", "impl_from_dispatch_error", "impl_from_collab_document", "impl_from_collab_persistence"] } lib-dispatch = { workspace = true } lib-infra = { workspace = true } -validator = { workspace = true, features = ["derive"] } +validator = { version = "0.16.0", features = ["derive"] } protobuf.workspace = true bytes.workspace = true nanoid = "0.4.0" +parking_lot.workspace = true strum_macros = "0.21" serde.workspace = true serde_json.workspace = true @@ -34,7 +35,7 @@ indexmap = { version = "2.1.0", features = ["serde"] } uuid.workspace = true futures.workspace = true tokio-stream = { workspace = true, features = ["sync"] } -dashmap.workspace = true +dashmap = "5" scraper = "0.18.0" [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -43,6 +44,7 @@ getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] tempfile = "3.4.0" tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } +collab-integrate = { workspace = true } tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } [build-dependencies] @@ -50,5 +52,10 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = [ + "flowy-codegen/ts", +] + # search "Enable/Disable AppFlowy Verbose Log" to find the place that can enable verbose log verbose_log = ["collab-document/verbose_log"] diff --git a/frontend/rust-lib/flowy-document/build.rs b/frontend/rust-lib/flowy-document/build.rs index 77c0c8125b..9fdde3edf6 100644 --- a/frontend/rust-lib/flowy-document/build.rs +++ b/frontend/rust-lib/flowy-document/build.rs @@ -4,4 +4,37 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } + + #[cfg(feature = "web_ts")] + { + flowy_codegen::ts_event::gen( + "document", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "document", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + } } diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index aa871cf4bc..6ec018f171 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -1,42 +1,91 @@ use crate::entities::{ DocEventPB, DocumentAwarenessStatesPB, DocumentSnapshotStatePB, DocumentSyncStatePB, }; -use crate::notification::{document_notification_builder, DocumentNotification}; -use collab::preclude::Collab; -use collab_document::document::Document; +use crate::notification::{send_notification, DocumentNotification}; +use collab::core::collab::MutexCollab; +use collab_document::document::DocumentIndexContent; +use collab_document::{blocks::DocumentData, document::Document}; +use flowy_error::FlowyResult; use futures::StreamExt; -use lib_infra::sync_trace; -use uuid::Uuid; +use lib_dispatch::prelude::af_spawn; +use parking_lot::Mutex; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; +use tracing::{instrument, warn}; -pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) { - let doc_id_clone_for_block_changed = doc_id.to_string(); - document.subscribe_block_changed("key", move |events, is_remote| { - sync_trace!( - "[Document] block changed in doc_id: {}, is_remote: {}, events: {:?}", - doc_id_clone_for_block_changed, - is_remote, - events - ); +/// This struct wrap the document::Document +#[derive(Clone)] +pub struct MutexDocument(Arc<Mutex<Document>>); - // send notification to the client. - document_notification_builder( - &doc_id_clone_for_block_changed, - DocumentNotification::DidReceiveUpdate, - ) - .payload::<DocEventPB>((events, is_remote, None).into()) - .send(); - }); +impl MutexDocument { + /// Open a document with the given collab. + /// # Arguments + /// * `collab` - the identifier of the collaboration instance + /// + /// # Returns + /// * `Result<Document, FlowyError>` - a Result containing either a new Document object or an Error if the document creation failed + pub fn open(doc_id: &str, collab: Arc<MutexCollab>) -> FlowyResult<Self> { + #[allow(clippy::arc_with_non_send_sync)] + let document = Document::open(collab.clone()).map(|inner| Self(Arc::new(Mutex::new(inner))))?; + subscribe_document_changed(doc_id, &document); + subscribe_document_snapshot_state(&collab); + subscribe_document_sync_state(&collab); + Ok(document) + } + + /// Creates and returns a new Document object with initial data. + /// # Arguments + /// * `collab` - the identifier of the collaboration instance + /// * `data` - the initial data to include in the document + /// + /// # Returns + /// * `Result<Document, FlowyError>` - a Result containing either a new Document object or an Error if the document creation failed + pub fn create_with_data(collab: Arc<MutexCollab>, data: DocumentData) -> FlowyResult<Self> { + #[allow(clippy::arc_with_non_send_sync)] + let document = + Document::create_with_data(collab, data).map(|inner| Self(Arc::new(Mutex::new(inner))))?; + Ok(document) + } + + #[instrument(level = "debug", skip_all)] + pub fn start_init_sync(&self) { + if let Some(document) = self.0.try_lock() { + if let Some(collab) = document.get_collab().try_lock() { + collab.start_init_sync(); + } else { + warn!("Failed to start init sync, collab is locked"); + } + } else { + warn!("Failed to start init sync, document is locked"); + } + } +} + +fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { + let doc_id_clone_for_block_changed = doc_id.to_owned(); + document + .lock() + .subscribe_block_changed(move |events, is_remote| { + #[cfg(feature = "verbose_log")] + tracing::trace!("subscribe_document_changed: {:?}", events); + + // send notification to the client. + send_notification( + &doc_id_clone_for_block_changed, + DocumentNotification::DidReceiveUpdate, + ) + .payload::<DocEventPB>((events, is_remote, None).into()) + .send(); + }); let doc_id_clone_for_awareness_state = doc_id.to_owned(); - document.subscribe_awareness_state("key", move |events| { - sync_trace!( - "[Document] awareness state in doc_id: {}, events: {:?}", - doc_id_clone_for_awareness_state, - events - ); - - document_notification_builder( - &doc_id_clone_for_awareness_state.to_string(), + document.lock().subscribe_awareness_state(move |events| { + #[cfg(feature = "verbose_log")] + tracing::trace!("subscribe_awareness_state: {:?}", events); + send_notification( + &doc_id_clone_for_awareness_state, DocumentNotification::DidUpdateDocumentAwarenessState, ) .payload::<DocumentAwarenessStatesPB>(events.into()) @@ -44,14 +93,14 @@ pub fn subscribe_document_changed(doc_id: &Uuid, document: &mut Document) { }); } -pub fn subscribe_document_snapshot_state(collab: &Collab) { - let document_id = collab.object_id().to_string(); - let mut snapshot_state = collab.subscribe_snapshot_state(); - tokio::spawn(async move { +fn subscribe_document_snapshot_state(collab: &Arc<MutexCollab>) { + let document_id = collab.lock().object_id.clone(); + let mut snapshot_state = collab.lock().subscribe_snapshot_state(); + af_spawn(async move { while let Some(snapshot_state) = snapshot_state.next().await { if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { tracing::debug!("Did create document remote snapshot: {}", new_snapshot_id); - document_notification_builder( + send_notification( &document_id, DocumentNotification::DidUpdateDocumentSnapshotState, ) @@ -62,12 +111,12 @@ pub fn subscribe_document_snapshot_state(collab: &Collab) { }); } -pub fn subscribe_document_sync_state(collab: &Collab) { - let document_id = collab.object_id().to_string(); - let mut sync_state_stream = collab.subscribe_sync_state(); - tokio::spawn(async move { +fn subscribe_document_sync_state(collab: &Arc<MutexCollab>) { + let document_id = collab.lock().object_id.clone(); + let mut sync_state_stream = collab.lock().subscribe_sync_state(); + af_spawn(async move { while let Some(sync_state) = sync_state_stream.next().await { - document_notification_builder( + send_notification( &document_id, DocumentNotification::DidUpdateDocumentSyncState, ) @@ -76,3 +125,27 @@ pub fn subscribe_document_sync_state(collab: &Collab) { } }); } + +unsafe impl Sync for MutexDocument {} +unsafe impl Send for MutexDocument {} + +impl Deref for MutexDocument { + type Target = Arc<Mutex<Document>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for MutexDocument { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From<&MutexDocument> for DocumentIndexContent { + fn from(doc: &MutexDocument) -> Self { + let doc = doc.lock(); + DocumentIndexContent::from(&*doc) + } +} diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index c8a6765fd6..ad5912dfeb 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use collab::core::collab_state::SyncState; use collab_document::{ blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData}, @@ -6,24 +8,14 @@ use collab_document::{ DocumentAwarenessUser, }, }; + use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use lib_infra::validator_fn::{required_not_empty_str, required_valid_path}; -use std::collections::HashMap; -use std::str::FromStr; -use uuid::Uuid; use validator::Validate; use crate::parse::{NotEmptyStr, NotEmptyVec}; -#[derive(Default, ProtoBuf)] -pub struct EncodedCollabPB { - #[pb(index = 1)] - pub state_vector: Vec<u8>, - #[pb(index = 2)] - pub doc_state: Vec<u8>, -} - #[derive(Default, ProtoBuf)] pub struct OpenDocumentPayloadPB { #[pb(index = 1)] @@ -31,7 +23,7 @@ pub struct OpenDocumentPayloadPB { } pub struct OpenDocumentParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB { @@ -39,9 +31,9 @@ impl TryInto<OpenDocumentParams> for OpenDocumentPayloadPB { fn try_into(self) -> Result<OpenDocumentParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - - Ok(OpenDocumentParams { document_id }) + Ok(OpenDocumentParams { + document_id: document_id.0, + }) } } @@ -52,7 +44,7 @@ pub struct DocumentRedoUndoPayloadPB { } pub struct DocumentRedoUndoParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB { @@ -60,8 +52,9 @@ impl TryInto<DocumentRedoUndoParams> for DocumentRedoUndoPayloadPB { fn try_into(self) -> Result<DocumentRedoUndoParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - Ok(DocumentRedoUndoParams { document_id }) + Ok(DocumentRedoUndoParams { + document_id: document_id.0, + }) } } @@ -80,16 +73,15 @@ pub struct DocumentRedoUndoResponsePB { #[derive(Default, ProtoBuf, Validate)] pub struct UploadFileParamsPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] - pub document_id: String, + #[validate(custom = "required_valid_path")] + pub local_file_path: String, #[pb(index = 3)] - #[validate(custom(function = "required_valid_path"))] - pub local_file_path: String, + pub is_async: bool, } #[derive(Default, ProtoBuf, Validate)] @@ -99,28 +91,10 @@ pub struct UploadedFilePB { pub url: String, #[pb(index = 2)] - #[validate(custom(function = "required_valid_path"))] + #[validate(custom = "required_valid_path")] pub local_file_path: String, } -#[derive(Default, ProtoBuf, Validate)] -pub struct DownloadFilePB { - #[pb(index = 1)] - #[validate(url)] - pub url: String, - - #[pb(index = 2)] - #[validate(custom(function = "required_valid_path"))] - pub local_file_path: String, -} - -#[derive(Default, ProtoBuf, Validate)] -pub struct DeleteFilePB { - #[pb(index = 1)] - #[validate(url)] - pub url: String, -} - #[derive(Default, ProtoBuf)] pub struct CreateDocumentPayloadPB { #[pb(index = 1)] @@ -131,7 +105,7 @@ pub struct CreateDocumentPayloadPB { } pub struct CreateDocumentParams { - pub document_id: Uuid, + pub document_id: String, pub initial_data: Option<DocumentData>, } @@ -140,10 +114,9 @@ impl TryInto<CreateDocumentParams> for CreateDocumentPayloadPB { fn try_into(self) -> Result<CreateDocumentParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let initial_data = self.initial_data.map(|data| data.into()); Ok(CreateDocumentParams { - document_id, + document_id: document_id.0, initial_data, }) } @@ -156,7 +129,7 @@ pub struct CloseDocumentPayloadPB { } pub struct CloseDocumentParams { - pub document_id: Uuid, + pub document_id: String, } impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB { @@ -164,8 +137,9 @@ impl TryInto<CloseDocumentParams> for CloseDocumentPayloadPB { fn try_into(self) -> Result<CloseDocumentParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; - Ok(CloseDocumentParams { document_id }) + Ok(CloseDocumentParams { + document_id: document_id.0, + }) } } @@ -179,7 +153,7 @@ pub struct ApplyActionPayloadPB { } pub struct ApplyActionParams { - pub document_id: Uuid, + pub document_id: String, pub actions: Vec<BlockAction>, } @@ -188,11 +162,10 @@ impl TryInto<ApplyActionParams> for ApplyActionPayloadPB { fn try_into(self) -> Result<ApplyActionParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let actions = NotEmptyVec::parse(self.actions).map_err(|_| ErrorCode::ApplyActionsIsEmpty)?; let actions = actions.0.into_iter().map(BlockAction::from).collect(); Ok(ApplyActionParams { - document_id, + document_id: document_id.0, actions, }) } @@ -210,11 +183,6 @@ pub struct DocumentDataPB { pub meta: MetaPB, } -#[derive(Default, Debug, ProtoBuf)] -pub struct DocumentTextPB { - #[pb(index = 1)] - pub text: String, -} #[derive(Default, ProtoBuf, Debug, Clone)] pub struct BlockPB { #[pb(index = 1)] @@ -525,7 +493,7 @@ pub struct TextDeltaPayloadPB { } pub struct TextDeltaParams { - pub document_id: Uuid, + pub document_id: String, pub text_id: String, pub delta: String, } @@ -535,11 +503,10 @@ impl TryInto<TextDeltaParams> for TextDeltaPayloadPB { fn try_into(self) -> Result<TextDeltaParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::from_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let text_id = NotEmptyStr::parse(self.text_id).map_err(|_| ErrorCode::TextIdIsEmpty)?; let delta = self.delta.map_or_else(|| "".to_string(), |delta| delta); Ok(TextDeltaParams { - document_id, + document_id: document_id.0, text_id: text_id.0, delta, }) diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index acf45777eb..029405c6cc 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -3,7 +3,7 @@ * as well as performing actions on documents. These functions make use of a DocumentManager, * which you can think of as a higher-level interface to interact with documents. */ -use std::str::FromStr; + use std::sync::{Arc, Weak}; use collab_document::blocks::{ @@ -11,6 +11,10 @@ use collab_document::blocks::{ DocumentData, }; +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; +use tracing::instrument; + use crate::entities::*; use crate::parser::document_data_parser::DocumentDataParser; use crate::parser::external::parser::ExternalDataToNestedJSONParser; @@ -19,11 +23,6 @@ use crate::parser::parser_entities::{ ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, }; use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use lib_infra::sync_trace; -use tracing::instrument; -use uuid::Uuid; fn upgrade_document( document_manager: AFPluginState<Weak<DocumentManager>>, @@ -34,22 +33,6 @@ fn upgrade_document( Ok(manager) } -// Handler for getting the document state -#[instrument(level = "debug", skip_all, err)] -pub(crate) async fn get_encode_collab_handler( - data: AFPluginData<OpenDocumentPayloadPB>, - manager: AFPluginState<Weak<DocumentManager>>, -) -> DataResult<EncodedCollabPB, FlowyError> { - let manager = upgrade_document(manager)?; - let params: OpenDocumentParams = data.into_inner().try_into()?; - let doc_id = params.document_id; - let state = manager.get_encoded_collab_with_view_id(&doc_id).await?; - data_result_ok(EncodedCollabPB { - state_vector: Vec::from(state.state_vector), - doc_state: Vec::from(state.doc_state), - }) -} - // Handler for creating a new document pub(crate) async fn create_document_handler( data: AFPluginData<CreateDocumentPayloadPB>, @@ -75,8 +58,8 @@ pub(crate) async fn open_document_handler( let doc_id = params.document_id; manager.open_document(&doc_id).await?; - let document = manager.editable_document(&doc_id).await?; - let document_data = document.read().await.get_document_data()?; + let document = manager.get_document(&doc_id).await?; + let document_data = document.lock().get_document_data()?; data_result_ok(DocumentDataPB::from(document_data)) } @@ -104,17 +87,6 @@ pub(crate) async fn get_document_data_handler( data_result_ok(DocumentDataPB::from(document_data)) } -pub(crate) async fn get_document_text_handler( - data: AFPluginData<OpenDocumentPayloadPB>, - manager: AFPluginState<Weak<DocumentManager>>, -) -> DataResult<DocumentTextPB, FlowyError> { - let manager = upgrade_document(manager)?; - let params: OpenDocumentParams = data.into_inner().try_into()?; - let doc_id = params.document_id; - let text = manager.get_document_text(&doc_id).await?; - data_result_ok(DocumentTextPB { text }) -} - // Handler for applying an action to a document pub(crate) async fn apply_action_handler( data: AFPluginData<ApplyActionPayloadPB>, @@ -123,10 +95,12 @@ pub(crate) async fn apply_action_handler( let manager = upgrade_document(manager)?; let params: ApplyActionParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; + let document = manager.get_document(&doc_id).await?; let actions = params.actions; - sync_trace!("{} applying action: {:?}", doc_id, actions); - document.write().await.apply_action(actions)?; + if cfg!(feature = "verbose_log") { + tracing::trace!("{} applying actions: {:?}", doc_id, actions); + } + document.lock().apply_action(actions); Ok(()) } @@ -138,10 +112,9 @@ pub(crate) async fn create_text_handler( let manager = upgrade_document(manager)?; let params: TextDeltaParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; - let mut document = document.write().await; - sync_trace!("{} creating text: {:?}", doc_id, params.delta); - document.apply_text_delta(¶ms.text_id, params.delta); + let document = manager.get_document(&doc_id).await?; + let document = document.lock(); + document.create_text(¶ms.text_id, params.delta); Ok(()) } @@ -153,11 +126,13 @@ pub(crate) async fn apply_text_delta_handler( let manager = upgrade_document(manager)?; let params: TextDeltaParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; + let document = manager.get_document(&doc_id).await?; let text_id = params.text_id; let delta = params.delta; - let mut document = document.write().await; - sync_trace!("{} applying delta: {:?}", doc_id, delta); + let document = document.lock(); + if cfg!(feature = "verbose_log") { + tracing::trace!("{} applying delta: {:?}", doc_id, delta); + } document.apply_text_delta(&text_id, delta); Ok(()) } @@ -192,8 +167,8 @@ pub(crate) async fn redo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; - let mut document = document.write().await; + let document = manager.get_document(&doc_id).await?; + let document = document.lock(); let redo = document.redo(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); @@ -211,8 +186,8 @@ pub(crate) async fn undo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; - let mut document = document.write().await; + let document = manager.get_document(&doc_id).await?; + let document = document.lock(); let undo = document.undo(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); @@ -230,10 +205,11 @@ pub(crate) async fn can_undo_redo_handler( let manager = upgrade_document(manager)?; let params: DocumentRedoUndoParams = data.into_inner().try_into()?; let doc_id = params.document_id; - let document = manager.editable_document(&doc_id).await?; - let document = document.read().await; + let document = manager.get_document(&doc_id).await?; + let document = document.lock(); let can_redo = document.can_redo(); let can_undo = document.can_undo(); + drop(document); data_result_ok(DocumentRedoUndoResponsePB { can_redo, can_undo, @@ -385,7 +361,8 @@ pub async fn convert_document_handler( let manager = upgrade_document(manager)?; let params: ConvertDocumentParams = data.into_inner().try_into()?; - let document_data = manager.get_document_data(¶ms.document_id).await?; + let document = manager.get_document(¶ms.document_id).await?; + let document_data = document.lock().get_document_data()?; let parser = DocumentDataParser::new(Arc::new(document_data), params.range); if !params.parse_types.any_enabled() { @@ -443,39 +420,32 @@ pub(crate) async fn upload_file_handler( params: AFPluginData<UploadFileParamsPB>, manager: AFPluginState<Weak<DocumentManager>>, ) -> DataResult<UploadedFilePB, FlowyError> { - let UploadFileParamsPB { + let AFPluginData(UploadFileParamsPB { workspace_id, - document_id, local_file_path, - } = params.try_into_inner()?; + is_async, + }) = params; let manager = upgrade_document(manager)?; - let (tx, rx) = tokio::sync::oneshot::channel(); - let cloned_local_file_path = local_file_path.clone(); - tokio::spawn(async move { - let result = manager - .upload_file(workspace_id, &document_id, &cloned_local_file_path) - .await; + let url = manager + .upload_file(workspace_id, &local_file_path, is_async) + .await?; - let _ = tx.send(result); - Ok::<(), FlowyError>(()) - }); - let upload = rx.await??; - data_result_ok(UploadedFilePB { - url: upload.url, + Ok(AFPluginData(UploadedFilePB { + url, local_file_path, - }) + })) } #[instrument(level = "debug", skip_all, err)] pub(crate) async fn download_file_handler( - params: AFPluginData<DownloadFilePB>, + params: AFPluginData<UploadedFilePB>, manager: AFPluginState<Weak<DocumentManager>>, ) -> FlowyResult<()> { - let DownloadFilePB { + let AFPluginData(UploadedFilePB { url, local_file_path, - } = params.try_into_inner()?; + }) = params; let manager = upgrade_document(manager)?; manager.download_file(local_file_path, url).await @@ -483,12 +453,15 @@ pub(crate) async fn download_file_handler( // Handler for deleting file pub(crate) async fn delete_file_handler( - params: AFPluginData<DeleteFilePB>, + params: AFPluginData<UploadedFilePB>, manager: AFPluginState<Weak<DocumentManager>>, ) -> FlowyResult<()> { - let DeleteFilePB { url } = params.try_into_inner()?; + let AFPluginData(UploadedFilePB { + url, + local_file_path, + }) = params; let manager = upgrade_document(manager)?; - manager.delete_file(url).await + manager.delete_file(local_file_path, url).await } pub(crate) async fn set_awareness_local_state_handler( @@ -497,7 +470,7 @@ pub(crate) async fn set_awareness_local_state_handler( ) -> FlowyResult<()> { let manager = upgrade_document(manager)?; let data = data.into_inner(); - let doc_id = Uuid::from_str(&data.document_id)?; + let doc_id = data.document_id.clone(); manager .set_document_awareness_local_state(&doc_id, data) .await?; diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index 1931d32161..1e11db6356 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -18,11 +18,6 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin { .event(DocumentEvent::CloseDocument, close_document_handler) .event(DocumentEvent::ApplyAction, apply_action_handler) .event(DocumentEvent::GetDocumentData, get_document_data_handler) - .event(DocumentEvent::GetDocumentText, get_document_text_handler) - .event( - DocumentEvent::GetDocEncodedCollab, - get_encode_collab_handler, - ) .event( DocumentEvent::ConvertDataToDocument, convert_data_to_document, @@ -124,17 +119,11 @@ pub enum DocumentEvent { #[event(input = "UploadFileParamsPB", output = "UploadedFilePB")] UploadFile = 15, - #[event(input = "DownloadFilePB")] + #[event(input = "UploadedFilePB")] DownloadFile = 16, - #[event(input = "DeleteFilePB")] + #[event(input = "UploadedFilePB")] DeleteFile = 17, #[event(input = "UpdateDocumentAwarenessStatePB")] SetAwarenessState = 18, - - #[event(input = "OpenDocumentPayloadPB", output = "EncodedCollabPB")] - GetDocEncodedCollab = 19, - - #[event(input = "OpenDocumentPayloadPB", output = "DocumentTextPB")] - GetDocumentText = 20, } diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 9c6a383bae..9edaccc796 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -1,11 +1,9 @@ use std::sync::Arc; use std::sync::Weak; -use collab::core::collab::DataSource; -use collab::core::collab_plugin::CollabPersistence; +use collab::core::collab::{DataSource, MutexCollab}; use collab::core::origin::CollabOrigin; use collab::entity::EncodedCollab; -use collab::lock::RwLock; use collab::preclude::Collab; use collab_document::blocks::DocumentData; use collab_document::document::Document; @@ -13,23 +11,21 @@ use collab_document::document_awareness::DocumentAwarenessState; use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; - -use crate::document::{ - subscribe_document_changed, subscribe_document_snapshot_state, subscribe_document_sync_state, -}; -use collab_integrate::collab_builder::{ - AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, -}; use collab_plugins::CollabKVDB; use dashmap::DashMap; +use flowy_storage::object_from_disk; +use lib_infra::util::timestamp; +use tokio::io::AsyncWriteExt; +use tracing::{error, trace}; +use tracing::{event, instrument}; + +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use flowy_document_pub::cloud::DocumentCloudService; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use flowy_storage_pub::storage::{CreatedUpload, StorageService}; -use lib_infra::util::timestamp; -use tracing::{event, instrument}; -use tracing::{info, trace}; -use uuid::Uuid; +use flowy_storage::ObjectStorageService; +use lib_dispatch::prelude::af_spawn; +use crate::document::MutexDocument; use crate::entities::UpdateDocumentAwarenessStatePB; use crate::entities::{ DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB, @@ -39,7 +35,7 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUserService: Send + Sync { fn user_id(&self) -> Result<i64, FlowyError>; fn device_id(&self) -> Result<String, FlowyError>; - fn workspace_id(&self) -> Result<Uuid, FlowyError>; + fn workspace_id(&self) -> Result<String, FlowyError>; fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>; } @@ -54,10 +50,10 @@ pub trait DocumentSnapshotService: Send + Sync { pub struct DocumentManager { pub user_service: Arc<dyn DocumentUserService>, collab_builder: Arc<AppFlowyCollabBuilder>, - documents: Arc<DashMap<Uuid, Arc<RwLock<Document>>>>, - removing_documents: Arc<DashMap<Uuid, Arc<RwLock<Document>>>>, + documents: Arc<DashMap<String, Arc<MutexDocument>>>, + removing_documents: Arc<DashMap<String, Arc<MutexDocument>>>, cloud_service: Arc<dyn DocumentCloudService>, - storage_service: Weak<dyn StorageService>, + storage_service: Weak<dyn ObjectStorageService>, snapshot_service: Arc<dyn DocumentSnapshotService>, } @@ -66,7 +62,7 @@ impl DocumentManager { user_service: Arc<dyn DocumentUserService>, collab_builder: Arc<AppFlowyCollabBuilder>, cloud_service: Arc<dyn DocumentCloudService>, - storage_service: Weak<dyn StorageService>, + storage_service: Weak<dyn ObjectStorageService>, snapshot_service: Arc<dyn DocumentSnapshotService>, ) -> Self { Self { @@ -80,24 +76,6 @@ impl DocumentManager { } } - /// Get the encoded collab of the document. - pub async fn get_encoded_collab_with_view_id(&self, doc_id: &Uuid) -> FlowyResult<EncodedCollab> { - let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; - let doc_state = - CollabPersistenceImpl::new(self.user_service.collab_db(uid)?, uid, workspace_id) - .into_data_source(); - let collab = self - .collab_for_document(uid, doc_id, doc_state, false) - .await?; - let encoded_collab = collab - .try_read() - .unwrap() - .encode_collab_v1(|collab| CollabType::Document.validate_require_data(collab)) - .map_err(internal_error)?; - Ok(encoded_collab) - } - pub async fn initialize(&self, _uid: i64) -> FlowyResult<()> { trace!("initialize document manager"); self.documents.clear(); @@ -106,23 +84,12 @@ impl DocumentManager { } #[instrument( - name = "document_initialize_after_sign_up", + name = "document_initialize_with_new_user", level = "debug", skip_all, err )] - pub async fn initialize_after_sign_up(&self, uid: i64) -> FlowyResult<()> { - self.initialize(uid).await?; - Ok(()) - } - - pub async fn initialize_after_open_workspace(&self, uid: i64) -> FlowyResult<()> { - self.initialize(uid).await?; - Ok(()) - } - - #[instrument(level = "debug", skip_all, err)] - pub async fn initialize_after_sign_in(&self, uid: i64) -> FlowyResult<()> { + pub async fn initialize_with_new_user(&self, uid: i64) -> FlowyResult<()> { self.initialize(uid).await?; Ok(()) } @@ -135,13 +102,6 @@ impl DocumentManager { } } - fn persistence(&self) -> FlowyResult<CollabPersistenceImpl> { - let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; - let db = self.user_service.collab_db(uid)?; - Ok(CollabPersistenceImpl::new(db, uid, workspace_id)) - } - /// Create a new document. /// /// if the document already exists, return the existing document. @@ -149,64 +109,33 @@ impl DocumentManager { #[instrument(level = "info", skip(self, data))] pub async fn create_document( &self, - _uid: i64, - doc_id: &Uuid, + uid: i64, + doc_id: &str, data: Option<DocumentData>, - ) -> FlowyResult<EncodedCollab> { + ) -> FlowyResult<()> { if self.is_doc_exist(doc_id).await.unwrap_or(false) { Err(FlowyError::new( ErrorCode::RecordAlreadyExists, format!("document {} already exists", doc_id), )) } else { - let encoded_collab = doc_state_from_document_data(doc_id, data).await?; - self - .persistence()? - .save_collab_to_disk(doc_id.to_string().as_str(), encoded_collab.clone()) - .map_err(internal_error)?; - - // Send the collab data to server with a background task. - let cloud_service = self.cloud_service.clone(); - let cloned_encoded_collab = encoded_collab.clone(); - let workspace_id = self.user_service.workspace_id()?; - let doc_id = *doc_id; - tokio::spawn(async move { - let _ = cloud_service - .create_document_collab(&workspace_id, &doc_id, cloned_encoded_collab) - .await; - }); - Ok(encoded_collab) + let doc_state = doc_state_from_document_data( + doc_id, + data.unwrap_or_else(|| default_document_data(doc_id)), + ) + .await? + .doc_state + .to_vec(); + let collab = self + .collab_for_document(uid, doc_id, DataSource::DocStateV1(doc_state), false) + .await?; + collab.lock().flush(); + Ok(()) } } - async fn collab_for_document( - &self, - uid: i64, - doc_id: &Uuid, - data_source: DataSource, - sync_enable: bool, - ) -> FlowyResult<Arc<RwLock<Document>>> { - let db = self.user_service.collab_db(uid)?; - let workspace_id = self.user_service.workspace_id()?; - let collab_object = - self - .collab_builder - .collab_object(&workspace_id, uid, doc_id, CollabType::Document)?; - let document = self - .collab_builder - .create_document( - collab_object, - data_source, - db, - CollabBuilderConfig::default().sync_enable(sync_enable), - None, - ) - .await?; - Ok(document) - } - - /// Return a document instance if the document is already opened. - pub async fn editable_document(&self, doc_id: &Uuid) -> FlowyResult<Arc<RwLock<Document>>> { + #[tracing::instrument(level = "info", skip(self), err)] + pub async fn get_document(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> { if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } @@ -214,28 +143,22 @@ impl DocumentManager { if let Some(doc) = self.restore_document_from_removing(doc_id) { return Ok(doc); } - - Err(FlowyError::internal().with_context("Call open document first")) + return Err(FlowyError::internal().with_context("Call open document first")); } /// Returns Document for given object id /// If the document does not exist in local disk, try get the doc state from the cloud. /// If the document exists, open the document and cache it #[tracing::instrument(level = "info", skip(self), err)] - async fn create_document_instance( - &self, - doc_id: &Uuid, - enable_sync: bool, - ) -> FlowyResult<Arc<RwLock<Document>>> { - let uid = self.user_service.user_id()?; - let mut doc_state = self.persistence()?.into_data_source(); + async fn create_document_instance(&self, doc_id: &str) -> FlowyResult<Arc<MutexDocument>> { + if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { + return Ok(doc); + } + + let mut doc_state = DataSource::Disk; // If the document does not exist in local disk, try get the doc state from the cloud. This happens // When user_device_a create a document and user_device_b open the document. if !self.is_doc_exist(doc_id).await? { - info!( - "document {} not found in local disk, try to get the doc state from the cloud", - doc_id - ); doc_state = DataSource::DocStateV1( self .cloud_service @@ -252,93 +175,75 @@ impl DocumentManager { } } + let uid = self.user_service.user_id()?; event!( tracing::Level::DEBUG, "Initialize document: {}, workspace_id: {:?}", doc_id, self.user_service.workspace_id() ); - let result = self - .collab_for_document(uid, doc_id, doc_state, enable_sync) - .await; - match result { + let collab = self + .collab_for_document(uid, doc_id, doc_state, true) + .await?; + + match MutexDocument::open(doc_id, collab) { Ok(document) => { - // Only push the document to the cache if the sync is enabled. - if enable_sync { - { - let mut lock = document.write().await; - subscribe_document_changed(doc_id, &mut lock); - subscribe_document_snapshot_state(&lock); - subscribe_document_sync_state(&lock); - } - self.documents.insert(*doc_id, document.clone()); - } + let document = Arc::new(document); + self.documents.insert(doc_id.to_string(), document.clone()); Ok(document) }, Err(err) => { if err.is_invalid_data() { - self.delete_document(doc_id).await?; + if let Some(db) = self.user_service.collab_db(uid)?.upgrade() { + db.delete_doc(uid, doc_id).await?; + } } return Err(err); }, } } - pub async fn get_document_data(&self, doc_id: &Uuid) -> FlowyResult<DocumentData> { - let document = self.get_document(doc_id).await?; - let document = document.read().await; - document.get_document_data().map_err(internal_error) - } - pub async fn get_document_text(&self, doc_id: &Uuid) -> FlowyResult<String> { - let document = self.get_document(doc_id).await?; - let document = document.read().await; - let text = document.paragraphs().join("\n"); - Ok(text) - } - - /// Return a document instance. - /// The returned document might or might not be able to sync with the cloud. - async fn get_document(&self, doc_id: &Uuid) -> FlowyResult<Arc<RwLock<Document>>> { - if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { - return Ok(doc); + pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult<DocumentData> { + let mut doc_state = DataSource::Disk; + if !self.is_doc_exist(doc_id).await? { + doc_state = DataSource::DocStateV1( + self + .cloud_service + .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) + .await?, + ); } - - if let Some(doc) = self.restore_document_from_removing(doc_id) { - return Ok(doc); - } - - let document = self.create_document_instance(doc_id, false).await?; - Ok(document) + let uid = self.user_service.user_id()?; + let collab = self + .collab_for_document(uid, doc_id, doc_state, false) + .await?; + Document::open(collab)? + .get_document_data() + .map_err(internal_error) } - pub async fn open_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some(mutex_document) = self.restore_document_from_removing(doc_id) { - let lock = mutex_document.read().await; - lock.start_init_sync(); + mutex_document.start_init_sync(); } - if self.documents.contains_key(doc_id) { - return Ok(()); - } - - let _ = self.create_document_instance(doc_id, true).await?; + let _ = self.create_document_instance(doc_id).await?; Ok(()) } - pub async fn close_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> { if let Some((doc_id, document)) = self.documents.remove(doc_id) { - { + if let Some(doc) = document.try_lock() { // clear the awareness state when close the document - let mut lock = document.write().await; - lock.clean_awareness_local_state(); + doc.clean_awareness_local_state(); + let _ = doc.flush(); } - - let clone_doc_id = doc_id; + let clone_doc_id = doc_id.clone(); trace!("move document to removing_documents: {}", doc_id); self.removing_documents.insert(doc_id, document); let weak_removing_documents = Arc::downgrade(&self.removing_documents); - tokio::spawn(async move { + af_spawn(async move { tokio::time::sleep(std::time::Duration::from_secs(120)).await; if let Some(removing_documents) = weak_removing_documents.upgrade() { if removing_documents.remove(&clone_doc_id).is_some() { @@ -351,12 +256,10 @@ impl DocumentManager { Ok(()) } - pub async fn delete_document(&self, doc_id: &Uuid) -> FlowyResult<()> { + pub async fn delete_document(&self, doc_id: &str) -> FlowyResult<()> { let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; if let Some(db) = self.user_service.collab_db(uid)?.upgrade() { - db.delete_doc(uid, &workspace_id.to_string(), &doc_id.to_string()) - .await?; + db.delete_doc(uid, doc_id).await?; // When deleting a document, we need to remove it from the cache. self.documents.remove(doc_id); } @@ -366,24 +269,25 @@ impl DocumentManager { #[instrument(level = "debug", skip_all, err)] pub async fn set_document_awareness_local_state( &self, - doc_id: &Uuid, + doc_id: &str, state: UpdateDocumentAwarenessStatePB, ) -> FlowyResult<bool> { let uid = self.user_service.user_id()?; let device_id = self.user_service.device_id()?; - if let Ok(doc) = self.editable_document(doc_id).await { - let doc = doc.write().await; - let user = DocumentAwarenessUser { uid, device_id }; - let selection = state.selection.map(|s| s.into()); - let state = DocumentAwarenessState { - version: 1, - user, - selection, - metadata: state.metadata, - timestamp: timestamp(), - }; - doc.set_awareness_local_state(state); - return Ok(true); + if let Ok(doc) = self.get_document(doc_id).await { + if let Some(doc) = doc.try_lock() { + let user = DocumentAwarenessUser { uid, device_id }; + let selection = state.selection.map(|s| s.into()); + let state = DocumentAwarenessState { + version: 1, + user, + selection, + metadata: state.metadata, + timestamp: timestamp(), + }; + doc.set_awareness_local_state(state); + return Ok(true); + } } Ok(false) } @@ -391,12 +295,12 @@ impl DocumentManager { /// Return the list of snapshots of the document. pub async fn get_document_snapshot_meta( &self, - document_id: &Uuid, + document_id: &str, _limit: usize, ) -> FlowyResult<Vec<DocumentSnapshotMetaPB>> { let metas = self .snapshot_service - .get_document_snapshot_metas(document_id.to_string().as_str())? + .get_document_snapshot_metas(document_id)? .into_iter() .map(|meta| DocumentSnapshotMetaPB { snapshot_id: meta.snapshot_id, @@ -419,47 +323,108 @@ impl DocumentManager { Ok(snapshot) } - #[instrument(level = "debug", skip_all, err)] pub async fn upload_file( &self, workspace_id: String, - document_id: &str, local_file_path: &str, - ) -> FlowyResult<CreatedUpload> { + is_async: bool, + ) -> FlowyResult<String> { + let (object_identity, object_value) = object_from_disk(&workspace_id, local_file_path).await?; let storage_service = self.storage_service_upgrade()?; - let upload = storage_service - .create_upload(&workspace_id, document_id, local_file_path) - .await? - .0; - Ok(upload) + let url = storage_service.get_object_url(object_identity).await?; + + let clone_url = url.clone(); + + match is_async { + false => storage_service.put_object(clone_url, object_value).await?, + true => { + // let the upload happen in the background + af_spawn(async move { + if let Err(e) = storage_service.put_object(clone_url, object_value).await { + error!("upload file failed: {}", e); + } + }); + }, + } + Ok(url) } pub async fn download_file(&self, local_file_path: String, url: String) -> FlowyResult<()> { - let storage_service = self.storage_service_upgrade()?; - storage_service.download_object(url, local_file_path)?; - Ok(()) - } + // TODO(nathan): save file when the current target is wasm + #[cfg(not(target_arch = "wasm32"))] + { + if tokio::fs::metadata(&local_file_path).await.is_ok() { + tracing::warn!("file already exist in user local disk: {}", local_file_path); + return Ok(()); + } - pub async fn delete_file(&self, url: String) -> FlowyResult<()> { - let storage_service = self.storage_service_upgrade()?; - storage_service.delete_object(url).await?; - Ok(()) - } - - async fn is_doc_exist(&self, doc_id: &Uuid) -> FlowyResult<bool> { - let uid = self.user_service.user_id()?; - let workspace_id = self.user_service.workspace_id()?; - if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() { - let is_exist = collab_db - .is_exist(uid, &workspace_id.to_string(), &doc_id.to_string()) + let storage_service = self.storage_service_upgrade()?; + let object_value = storage_service.get_object(url).await?; + // create file if not exist + let mut file = tokio::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&local_file_path) .await?; + + let n = file.write(&object_value.raw).await?; + tracing::info!("downloaded {} bytes to file: {}", n, local_file_path); + } + Ok(()) + } + + pub async fn delete_file(&self, local_file_path: String, url: String) -> FlowyResult<()> { + // TODO(nathan): delete file when the current target is wasm + #[cfg(not(target_arch = "wasm32"))] + // delete file from local + tokio::fs::remove_file(local_file_path).await?; + + // delete from cloud + let storage_service = self.storage_service_upgrade()?; + af_spawn(async move { + if let Err(e) = storage_service.delete_object(url).await { + // TODO: add WAL to log the delete operation. + // keep a list of files to be deleted, and retry later + error!("delete file failed: {}", e); + } + }); + + Ok(()) + } + + async fn collab_for_document( + &self, + uid: i64, + doc_id: &str, + doc_state: DataSource, + sync_enable: bool, + ) -> FlowyResult<Arc<MutexCollab>> { + let db = self.user_service.collab_db(uid)?; + let workspace_id = self.user_service.workspace_id()?; + let collab = self.collab_builder.build_with_config( + &workspace_id, + uid, + doc_id, + CollabType::Document, + db, + doc_state, + CollabBuilderConfig::default().sync_enable(sync_enable), + )?; + Ok(collab) + } + + async fn is_doc_exist(&self, doc_id: &str) -> FlowyResult<bool> { + let uid = self.user_service.user_id()?; + if let Some(collab_db) = self.user_service.collab_db(uid)?.upgrade() { + let is_exist = collab_db.is_exist(uid, doc_id).await?; Ok(is_exist) } else { Ok(false) } } - fn storage_service_upgrade(&self) -> FlowyResult<Arc<dyn StorageService>> { + fn storage_service_upgrade(&self) -> FlowyResult<Arc<dyn ObjectStorageService>> { let storage_service = self.storage_service.upgrade().ok_or_else(|| { FlowyError::internal().with_context("The file storage service is already dropped") })?; @@ -473,11 +438,11 @@ impl DocumentManager { } /// Only expose this method for testing #[cfg(debug_assertions)] - pub fn get_file_storage_service(&self) -> &Weak<dyn StorageService> { + pub fn get_file_storage_service(&self) -> &Weak<dyn ObjectStorageService> { &self.storage_service } - fn restore_document_from_removing(&self, doc_id: &Uuid) -> Option<Arc<RwLock<Document>>> { + fn restore_document_from_removing(&self, doc_id: &str) -> Option<Arc<MutexDocument>> { let (doc_id, doc) = self.removing_documents.remove(doc_id)?; trace!( "move document {} from removing_documents to documents", @@ -489,21 +454,19 @@ impl DocumentManager { } async fn doc_state_from_document_data( - doc_id: &Uuid, - data: Option<DocumentData>, + doc_id: &str, + data: DocumentData, ) -> Result<EncodedCollab, FlowyError> { let doc_id = doc_id.to_string(); - let data = data.unwrap_or_else(|| { - trace!( - "{} document data is None, use default document data", - doc_id.to_string() - ); - default_document_data(&doc_id) - }); // spawn_blocking is used to avoid blocking the tokio thread pool if the document is large. let encoded_collab = tokio::task::spawn_blocking(move || { - let collab = Collab::new_with_origin(CollabOrigin::Empty, doc_id, vec![], false); - let document = Document::create_with_data(collab, data).map_err(internal_error)?; + let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Empty, + doc_id, + vec![], + false, + ))); + let document = Document::create_with_data(collab.clone(), data).map_err(internal_error)?; let encode_collab = document.encode_collab()?; Ok::<_, FlowyError>(encode_collab) }) diff --git a/frontend/rust-lib/flowy-document/src/notification.rs b/frontend/rust-lib/flowy-document/src/notification.rs index 5d843014e7..9909971667 100644 --- a/frontend/rust-lib/flowy-document/src/notification.rs +++ b/frontend/rust-lib/flowy-document/src/notification.rs @@ -32,9 +32,6 @@ impl std::convert::From<i32> for DocumentNotification { } #[tracing::instrument(level = "trace")] -pub(crate) fn document_notification_builder( - id: &str, - ty: DocumentNotification, -) -> NotificationBuilder { +pub(crate) fn send_notification(id: &str, ty: DocumentNotification) -> NotificationBuilder { NotificationBuilder::new(id, ty, DOCUMENT_OBSERVABLE_SOURCE) } diff --git a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs index 94680b32d3..cb7bf35e27 100644 --- a/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs +++ b/frontend/rust-lib/flowy-document/src/parser/parser_entities.rs @@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; -use uuid::Uuid; use validator::Validate; #[derive(Default, ProtoBuf)] @@ -97,7 +96,7 @@ pub struct ParseType { } pub struct ConvertDocumentParams { - pub document_id: Uuid, + pub document_id: String, pub range: Option<Range>, pub parse_types: ParseType, } @@ -141,11 +140,10 @@ impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB { fn try_into(self) -> Result<ConvertDocumentParams, Self::Error> { let document_id = NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; - let document_id = Uuid::parse_str(&document_id.0).map_err(|_| ErrorCode::InvalidParams)?; let range = self.range.map(|data| data.into()); Ok(ConvertDocumentParams { - document_id, + document_id: document_id.0, range, parse_types: self.parse_types.into(), }) @@ -510,7 +508,7 @@ pub enum InputType { #[derive(Default, ProtoBuf, Debug, Validate)] pub struct ConvertDataToJsonPayloadPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub data: String, #[pb(index = 2)] diff --git a/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs index 28c02641e8..1181395cae 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_insert_test.rs @@ -31,13 +31,9 @@ async fn document_apply_insert_block_with_empty_parent_id() { text_id: None, }, }; - document - .write() - .await - .apply_action(vec![insert_text_action]) - .unwrap(); + document.lock().apply_action(vec![insert_text_action]); // read the text block and it's parent id should be the page id - let block = document.read().await.get_block(&text_block_id).unwrap(); + let block = document.lock().get_block(&text_block_id).unwrap(); assert_eq!(block.parent, page_id); } diff --git a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs index 2a47ec93c4..ad2fa54e34 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_redo_undo_test.rs @@ -9,8 +9,8 @@ use crate::document::util::{gen_document_id, gen_id, DocumentTest}; async fn undo_redo_test() { let test = DocumentTest::new(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test @@ -23,8 +23,8 @@ async fn undo_redo_test() { // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.editable_document(&doc_id).await.unwrap(); - let mut document = document.write().await; + let document = test.get_document(&doc_id).await.unwrap(); + let document = document.lock(); let page_block = document.get_block(&data.page_id).unwrap(); let page_id = page_block.id; let text_block_id = gen_id(); @@ -49,7 +49,7 @@ async fn undo_redo_test() { text_id: None, }, }; - document.apply_action(vec![insert_text_action]).unwrap(); + document.apply_action(vec![insert_text_action]); let can_undo = document.can_undo(); assert!(can_undo); diff --git a/frontend/rust-lib/flowy-document/tests/document/document_test.rs b/frontend/rust-lib/flowy-document/tests/document/document_test.rs index 8323a645c7..084e58693e 100644 --- a/frontend/rust-lib/flowy-document/tests/document/document_test.rs +++ b/frontend/rust-lib/flowy-document/tests/document/document_test.rs @@ -11,8 +11,8 @@ async fn restore_document() { let test = DocumentTest::new(); // create a document - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); let uid = test.user_service.user_id().unwrap(); test .create_document(uid, &doc_id, Some(data.clone())) @@ -23,11 +23,10 @@ async fn restore_document() { test.open_document(&doc_id).await.unwrap(); let data_b = test - .editable_document(&doc_id) + .get_document(&doc_id) .await .unwrap() - .read() - .await + .lock() .get_document_data() .unwrap(); // close a document @@ -38,11 +37,10 @@ async fn restore_document() { _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document let data_b = test - .editable_document(&doc_id) + .get_document(&doc_id) .await .unwrap() - .read() - .await + .lock() .get_document_data() .unwrap(); // close a document @@ -55,17 +53,16 @@ async fn restore_document() { async fn document_apply_insert_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.editable_document(&doc_id).await.unwrap(); - let mut document = document.write().await; - let page_block = document.get_block(&data.page_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); + let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block let text_block = Block { @@ -87,19 +84,17 @@ async fn document_apply_insert_action() { text_id: None, }, }; - document.apply_action(vec![insert_text_action]).unwrap(); - let data_a = document.get_document_data().unwrap(); - drop(document); + document.lock().apply_action(vec![insert_text_action]); + let data_a = document.lock().get_document_data().unwrap(); // close the original document _ = test.close_document(&doc_id).await; // re-open the document let data_b = test - .editable_document(&doc_id) + .get_document(&doc_id) .await .unwrap() - .read() - .await + .lock() .get_document_data() .unwrap(); // close a document @@ -111,18 +106,17 @@ async fn document_apply_insert_action() { #[tokio::test] async fn document_apply_update_page_action() { let test = DocumentTest::new(); - let doc_id = gen_document_id(); + let doc_id: String = gen_document_id(); let uid = test.user_service.user_id().unwrap(); - let data = default_document_data(&doc_id.to_string()); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.editable_document(&doc_id).await.unwrap(); - let mut document = document.write().await; - let page_block = document.get_block(&data.page_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); + let page_block = document.lock().get_block(&data.page_id).unwrap(); let mut page_block_clone = page_block; page_block_clone.data = HashMap::new(); @@ -142,14 +136,13 @@ async fn document_apply_update_page_action() { }; let actions = vec![action]; tracing::trace!("{:?}", &actions); - document.apply_action(actions).unwrap(); - let page_block_old = document.get_block(&data.page_id).unwrap(); - drop(document); + document.lock().apply_action(actions); + let page_block_old = document.lock().get_block(&data.page_id).unwrap(); _ = test.close_document(&doc_id).await; // re-open the document - let document = test.editable_document(&doc_id).await.unwrap(); - let page_block_new = document.read().await.get_block(&data.page_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); + let page_block_new = document.lock().get_block(&data.page_id).unwrap(); assert_eq!(page_block_old, page_block_new); assert!(page_block_new.data.contains_key("delta")); } @@ -158,17 +151,16 @@ async fn document_apply_update_page_action() { async fn document_apply_update_action() { let test = DocumentTest::new(); let uid = test.user_service.user_id().unwrap(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); // create a document _ = test.create_document(uid, &doc_id, Some(data.clone())).await; // open a document test.open_document(&doc_id).await.unwrap(); - let document = test.editable_document(&doc_id).await.unwrap(); - let mut document = document.write().await; - let page_block = document.get_block(&data.page_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); + let page_block = document.lock().get_block(&data.page_id).unwrap(); // insert a text block let text_block_id = gen_id(); @@ -191,10 +183,10 @@ async fn document_apply_update_action() { text_id: None, }, }; - document.apply_action(vec![insert_text_action]).unwrap(); + document.lock().apply_action(vec![insert_text_action]); // update the text block - let existing_text_block = document.get_block(&text_block_id).unwrap(); + let existing_text_block = document.lock().get_block(&text_block_id).unwrap(); let mut updated_text_block_data = HashMap::new(); updated_text_block_data.insert("delta".to_string(), Value::String("delta".to_string())); let updated_text_block = Block { @@ -216,14 +208,13 @@ async fn document_apply_update_action() { text_id: None, }, }; - document.apply_action(vec![update_text_action]).unwrap(); - drop(document); + document.lock().apply_action(vec![update_text_action]); // close the original document _ = test.close_document(&doc_id).await; // re-open the document - let document = test.editable_document(&doc_id).await.unwrap(); - let block = document.read().await.get_block(&text_block_id).unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); + let block = document.lock().get_block(&text_block_id).unwrap(); assert_eq!(block.data, updated_text_block_data); // close a document _ = test.close_document(&doc_id).await; diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 231bb3852e..12ad16f44c 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -1,28 +1,28 @@ use std::ops::Deref; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; -use collab::entity::EncodedCollab; +use anyhow::Error; use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; -use collab_document::document::Document; use collab_document::document_data::default_document_data; +use nanoid::nanoid; +use parking_lot::Once; +use tempfile::TempDir; +use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; + use collab_integrate::collab_builder::{ AppFlowyCollabBuilder, CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType, WorkspaceCollabIntegrate, }; use collab_integrate::CollabKVDB; +use flowy_document::document::MutexDocument; use flowy_document::entities::{DocumentSnapshotData, DocumentSnapshotMeta}; use flowy_document::manager::{DocumentManager, DocumentSnapshotService, DocumentUserService}; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_storage_pub::storage::{CreatedUpload, FileProgressReceiver, StorageService}; +use flowy_storage::ObjectStorageService; use lib_infra::async_trait::async_trait; -use lib_infra::box_any::BoxAny; -use nanoid::nanoid; -use tempfile::TempDir; -use tokio::sync::RwLock; -use tracing_subscriber::{fmt::Subscriber, util::SubscriberInitExt, EnvFilter}; -use uuid::Uuid; +use lib_infra::future::FutureResult; pub struct DocumentTest { inner: DocumentManager, @@ -32,13 +32,13 @@ impl DocumentTest { pub fn new() -> Self { let user = FakeUser::new(); let cloud_service = Arc::new(LocalTestDocumentCloudServiceImpl()); - let file_storage = Arc::new(DocumentTestFileStorageService) as Arc<dyn StorageService>; + let file_storage = Arc::new(DocumentTestFileStorageService) as Arc<dyn ObjectStorageService>; let document_snapshot = Arc::new(DocumentTestSnapshot); let builder = Arc::new(AppFlowyCollabBuilder::new( DefaultCollabStorageProvider(), WorkspaceCollabIntegrateImpl { - workspace_id: user.workspace_id, + workspace_id: user.workspace_id.clone(), }, )); @@ -62,7 +62,7 @@ impl Deref for DocumentTest { } pub struct FakeUser { - workspace_id: Uuid, + workspace_id: String, collab_db: Arc<CollabKVDB>, } @@ -73,7 +73,7 @@ impl FakeUser { let tempdir = TempDir::new().unwrap(); let path = tempdir.into_path(); let collab_db = Arc::new(CollabKVDB::open(path).unwrap()); - let workspace_id = uuid::Uuid::new_v4(); + let workspace_id = uuid::Uuid::new_v4().to_string(); Self { collab_db, @@ -87,8 +87,8 @@ impl DocumentUserService for FakeUser { Ok(1) } - fn workspace_id(&self) -> Result<Uuid, FlowyError> { - Ok(self.workspace_id) + fn workspace_id(&self) -> Result<String, FlowyError> { + Ok(self.workspace_id.clone()) } fn collab_db(&self, _uid: i64) -> Result<std::sync::Weak<CollabKVDB>, FlowyError> { @@ -101,8 +101,8 @@ impl DocumentUserService for FakeUser { } pub fn setup_log() { - static START: OnceLock<()> = OnceLock::new(); - START.get_or_init(|| { + static START: Once = Once::new(); + START.call_once(|| { std::env::set_var("RUST_LOG", "collab_persistence=trace"); let subscriber = Subscriber::builder() .with_env_filter(EnvFilter::from_default_env()) @@ -112,10 +112,10 @@ pub fn setup_log() { }); } -pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<RwLock<Document>>, String) { +pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<MutexDocument>, String) { let test = DocumentTest::new(); - let doc_id = gen_document_id(); - let data = default_document_data(&doc_id.to_string()); + let doc_id: String = gen_document_id(); + let data = default_document_data(&doc_id); let uid = test.user_service.user_id().unwrap(); // create a document test @@ -124,13 +124,14 @@ pub async fn create_and_open_empty_document() -> (DocumentTest, Arc<RwLock<Docum .unwrap(); test.open_document(&doc_id).await.unwrap(); - let document = test.editable_document(&doc_id).await.unwrap(); + let document = test.get_document(&doc_id).await.unwrap(); (test, document, data.page_id) } -pub fn gen_document_id() -> Uuid { - uuid::Uuid::new_v4() +pub fn gen_document_id() -> String { + let uuid = uuid::Uuid::new_v4(); + uuid.to_string() } pub fn gen_id() -> String { @@ -138,87 +139,61 @@ pub fn gen_id() -> String { } pub struct LocalTestDocumentCloudServiceImpl(); - -#[async_trait] impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { - async fn get_document_doc_state( + fn get_document_doc_state( &self, - document_id: &Uuid, - _workspace_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { + document_id: &str, + _workspace_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { let document_id = document_id.to_string(); - Err(FlowyError::new( - ErrorCode::RecordNotFound, - format!("Document {} not found", document_id), - )) + FutureResult::new(async move { + Err(FlowyError::new( + ErrorCode::RecordNotFound, + format!("Document {} not found", document_id), + )) + }) } - async fn get_document_snapshots( + fn get_document_snapshots( &self, - _document_id: &Uuid, + _document_id: &str, _limit: usize, _workspace_id: &str, - ) -> Result<Vec<DocumentSnapshot>, FlowyError> { - Ok(vec![]) + ) -> FutureResult<Vec<DocumentSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } - async fn get_document_data( + fn get_document_data( &self, - _document_id: &Uuid, - _workspace_id: &Uuid, - ) -> Result<Option<DocumentData>, FlowyError> { - Ok(None) - } - - async fn create_document_collab( - &self, - _workspace_id: &Uuid, - _document_id: &Uuid, - _encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - Ok(()) + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult<Option<DocumentData>, Error> { + FutureResult::new(async move { Ok(None) }) } } pub struct DocumentTestFileStorageService; - -#[async_trait] -impl StorageService for DocumentTestFileStorageService { - async fn delete_object(&self, _url: String) -> FlowyResult<()> { - todo!() - } - - fn download_object(&self, _url: String, _local_file_path: String) -> FlowyResult<()> { - todo!() - } - - async fn create_upload( +impl ObjectStorageService for DocumentTestFileStorageService { + fn get_object_url( &self, - _workspace_id: &str, - _parent_dir: &str, - _local_file_path: &str, - ) -> Result<(CreatedUpload, Option<FileProgressReceiver>), flowy_error::FlowyError> { + _object_id: flowy_storage::ObjectIdentity, + ) -> FutureResult<String, FlowyError> { todo!() } - async fn start_upload(&self, _record: &BoxAny) -> Result<(), FlowyError> { - todo!() - } - - async fn resume_upload( + fn put_object( &self, - _workspace_id: &str, - _parent_dir: &str, - _file_id: &str, - ) -> Result<(), FlowyError> { + _url: String, + _object_value: flowy_storage::ObjectValue, + ) -> FutureResult<(), FlowyError> { todo!() } - async fn subscribe_file_progress( - &self, - _parent_idr: &str, - _url: &str, - ) -> Result<Option<FileProgressReceiver>, FlowyError> { + fn delete_object(&self, _url: String) -> FutureResult<(), FlowyError> { + todo!() + } + + fn get_object(&self, _url: String) -> FutureResult<flowy_storage::ObjectValue, FlowyError> { todo!() } } @@ -255,14 +230,14 @@ impl DocumentSnapshotService for DocumentTestSnapshot { } struct WorkspaceCollabIntegrateImpl { - workspace_id: Uuid, + workspace_id: String, } impl WorkspaceCollabIntegrate for WorkspaceCollabIntegrateImpl { - fn workspace_id(&self) -> Result<Uuid, FlowyError> { - Ok(self.workspace_id) + fn workspace_id(&self) -> Result<String, Error> { + Ok(self.workspace_id.clone()) } - fn device_id(&self) -> Result<String, FlowyError> { + fn device_id(&self) -> Result<String, Error> { Ok("fake_device_id".to_string()) } } diff --git a/frontend/rust-lib/flowy-document/tests/parser/json/parser_test.rs b/frontend/rust-lib/flowy-document/tests/parser/json/parser_test.rs index f60c19e945..096b4fd5ce 100644 --- a/frontend/rust-lib/flowy-document/tests/parser/json/parser_test.rs +++ b/frontend/rust-lib/flowy-document/tests/parser/json/parser_test.rs @@ -112,7 +112,7 @@ async fn parse_readme_test() { let data = json_str_to_hashmap(&block.data).ok(); assert!(data.is_some()); if let Some(data) = data { - assert!(!data.contains_key("delta")); + assert!(data.get("delta").is_none()); } if let Some(external_id) = &block.external_id { diff --git a/frontend/rust-lib/flowy-encrypt/Cargo.toml b/frontend/rust-lib/flowy-encrypt/Cargo.toml new file mode 100644 index 0000000000..5ea42c60e2 --- /dev/null +++ b/frontend/rust-lib/flowy-encrypt/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "flowy-encrypt" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +aes-gcm = "0.10.2" +rand = "0.8" +pbkdf2 = "0.12.2" +hmac = "0.12.1" +sha2 = "0.10.7" +anyhow.workspace = true +base64 = "0.21.2" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"]} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-encrypt/src/encrypt.rs b/frontend/rust-lib/flowy-encrypt/src/encrypt.rs new file mode 100644 index 0000000000..88658858bb --- /dev/null +++ b/frontend/rust-lib/flowy-encrypt/src/encrypt.rs @@ -0,0 +1,172 @@ +use aes_gcm::aead::generic_array::GenericArray; +use aes_gcm::aead::Aead; +use aes_gcm::{Aes256Gcm, KeyInit}; +use anyhow::Result; +use base64::engine::general_purpose::STANDARD; +use base64::Engine; +use pbkdf2::hmac::Hmac; +use pbkdf2::pbkdf2; +use rand::distributions::Alphanumeric; +use rand::Rng; +use sha2::Sha256; + +/// The length of the salt in bytes. +const SALT_LENGTH: usize = 16; + +/// The length of the derived encryption key in bytes. +const KEY_LENGTH: usize = 32; + +/// The number of iterations for the PBKDF2 key derivation. +const ITERATIONS: u32 = 1000; + +/// The length of the nonce for AES-GCM encryption. +const NONCE_LENGTH: usize = 12; + +/// Delimiter used to concatenate the passphrase and salt. +const CONCATENATED_DELIMITER: &str = "$"; + +/// Generate a new encryption secret consisting of a passphrase and a salt. +pub fn generate_encryption_secret() -> String { + let passphrase = generate_random_passphrase(); + let salt = generate_random_salt(); + combine_passphrase_and_salt(&passphrase, &salt) +} + +/// Encrypt a byte slice using AES-GCM. +/// +/// # Arguments +/// * `data`: The data to encrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn encrypt_data<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> { + let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?; + let key = derive_key(passphrase, &salt)?; + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); + let nonce: [u8; NONCE_LENGTH] = rand::thread_rng().gen(); + let ciphertext = cipher + .encrypt(GenericArray::from_slice(&nonce), data.as_ref()) + .unwrap(); + + Ok(nonce.into_iter().chain(ciphertext).collect()) +} + +/// Decrypt a byte slice using AES-GCM. +/// +/// # Arguments +/// * `data`: The data to decrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn decrypt_data<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> { + if data.as_ref().len() <= NONCE_LENGTH { + return Err(anyhow::anyhow!("Ciphertext too short to include nonce.")); + } + let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?; + let key = derive_key(passphrase, &salt)?; + let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); + let (nonce, cipher_data) = data.as_ref().split_at(NONCE_LENGTH); + cipher + .decrypt(GenericArray::from_slice(nonce), cipher_data) + .map_err(|e| anyhow::anyhow!("Decryption error: {:?}", e)) +} + +/// Encrypt a string using AES-GCM and return the result as a base64 encoded string. +/// +/// # Arguments +/// * `data`: The string data to encrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn encrypt_text<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> { + let encrypted = encrypt_data(data.as_ref(), combined_passphrase_salt)?; + Ok(STANDARD.encode(encrypted)) +} + +/// Decrypt a base64 encoded string using AES-GCM. +/// +/// # Arguments +/// * `data`: The base64 encoded string to decrypt. +/// * `combined_passphrase_salt`: The concatenated passphrase and salt. +pub fn decrypt_text<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> { + let encrypted = STANDARD.decode(data)?; + let decrypted = decrypt_data(encrypted, combined_passphrase_salt)?; + Ok(String::from_utf8(decrypted)?) +} + +/// Generates a random passphrase consisting of alphanumeric characters. +/// +/// This function creates a passphrase with both uppercase and lowercase letters +/// as well as numbers. The passphrase is 30 characters in length. +/// +/// # Returns +/// +/// A `String` representing the generated passphrase. +/// +/// # Security Considerations +/// +/// The passphrase is derived from the `Alphanumeric` character set which includes 62 possible +/// characters (26 lowercase letters, 26 uppercase letters, 10 numbers). This results in a total +/// of `62^30` possible combinations, making it strong against brute force attacks. +/// +fn generate_random_passphrase() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(30) // e.g., 30 characters + .map(char::from) + .collect() +} + +fn generate_random_salt() -> [u8; SALT_LENGTH] { + let mut rng = rand::thread_rng(); + let salt: [u8; SALT_LENGTH] = rng.gen(); + salt +} + +fn combine_passphrase_and_salt(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> String { + let salt_base64 = STANDARD.encode(salt); + format!("{}{}{}", passphrase, CONCATENATED_DELIMITER, salt_base64) +} + +fn split_passphrase_and_salt(combined: &str) -> Result<(&str, [u8; SALT_LENGTH]), anyhow::Error> { + let parts: Vec<&str> = combined.split(CONCATENATED_DELIMITER).collect(); + if parts.len() != 2 { + return Err(anyhow::anyhow!("Invalid combined format")); + } + let passphrase = parts[0]; + let salt = STANDARD.decode(parts[1])?; + if salt.len() != SALT_LENGTH { + return Err(anyhow::anyhow!("Incorrect salt length")); + } + let mut salt_array = [0u8; SALT_LENGTH]; + salt_array.copy_from_slice(&salt); + Ok((passphrase, salt_array)) +} + +fn derive_key(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> Result<[u8; KEY_LENGTH]> { + let mut key = [0u8; KEY_LENGTH]; + pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), salt, ITERATIONS, &mut key)?; + Ok(key) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_decrypt_test() { + let secret = generate_encryption_secret(); + let data = b"hello world"; + let encrypted = encrypt_data(data, &secret).unwrap(); + let decrypted = decrypt_data(encrypted, &secret).unwrap(); + assert_eq!(data, decrypted.as_slice()); + + let s = "123".to_string(); + let encrypted = encrypt_text(&s, &secret).unwrap(); + let decrypted_str = decrypt_text(encrypted, &secret).unwrap(); + assert_eq!(s, decrypted_str); + } + + #[test] + fn decrypt_with_invalid_secret_test() { + let secret = generate_encryption_secret(); + let data = b"hello world"; + let encrypted = encrypt_data(data, &secret).unwrap(); + let decrypted = decrypt_data(encrypted, "invalid secret"); + assert!(decrypted.is_err()) + } +} diff --git a/frontend/rust-lib/flowy-encrypt/src/lib.rs b/frontend/rust-lib/flowy-encrypt/src/lib.rs new file mode 100644 index 0000000000..a72d275af9 --- /dev/null +++ b/frontend/rust-lib/flowy-encrypt/src/lib.rs @@ -0,0 +1,3 @@ +pub use encrypt::*; + +mod encrypt; diff --git a/frontend/rust-lib/flowy-error/Cargo.toml b/frontend/rust-lib/flowy-error/Cargo.toml index 61a7422f17..ae0d8bf9cd 100644 --- a/frontend/rust-lib/flowy-error/Cargo.toml +++ b/frontend/rust-lib/flowy-error/Cargo.toml @@ -22,30 +22,28 @@ serde_json.workspace = true serde_repr.workspace = true serde.workspace = true reqwest = { version = "0.11.14", optional = true, features = [ - "native-tls-vendored", + "native-tls-vendored", ] } flowy-sqlite = { workspace = true, optional = true } r2d2 = { version = "0.8", optional = true } url = { version = "2.2", optional = true } -collab = { workspace = true } collab-database = { workspace = true, optional = true } collab-document = { workspace = true, optional = true } collab-plugins = { workspace = true, optional = true } collab-folder = { workspace = true, optional = true } client-api = { workspace = true, optional = true } -tantivy = { workspace = true, optional = true } -uuid.workspace = true +tantivy = { version = "0.21.1", optional = true } + [features] -default = ["impl_from_dispatch_error", "impl_from_serde", "impl_from_reqwest", "impl_from_sqlite"] impl_from_dispatch_error = ["lib-dispatch"] impl_from_serde = [] impl_from_reqwest = ["reqwest"] impl_from_collab_persistence = ["collab-plugins"] impl_from_collab_document = [ - "collab-document", - "impl_from_reqwest", - "collab-plugins", + "collab-document", + "impl_from_reqwest", + "collab-plugins", ] impl_from_collab_folder = ["collab-folder"] impl_from_collab_database = ["collab-database"] @@ -55,6 +53,8 @@ impl_from_tantivy = ["tantivy"] impl_from_sqlite = ["flowy-sqlite", "r2d2"] impl_from_appflowy_cloud = ["client-api"] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = ["flowy-codegen/ts"] [build-dependencies] flowy-codegen = { workspace = true, features = ["proto_gen"] } diff --git a/frontend/rust-lib/flowy-error/build.rs b/frontend/rust-lib/flowy-error/build.rs index 8dfda67156..c3081d7488 100644 --- a/frontend/rust-lib/flowy-error/build.rs +++ b/frontend/rust-lib/flowy-error/build.rs @@ -1,4 +1,27 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } + + #[cfg(feature = "web_ts")] + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "error", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 4112883e61..b013c1fbe5 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -164,8 +164,8 @@ pub enum ErrorCode { #[error("Sql error")] SqlError = 58, - #[error("Network error")] - NetworkError = 59, + #[error("Http error")] + HttpError = 59, #[error("The content should not be empty")] UnexpectedEmpty = 60, @@ -257,8 +257,8 @@ pub enum ErrorCode { #[error("AppFlowy data folder import error")] AppFlowyDataFolderImportError = 89, - #[error("payload too large")] - PayloadTooLarge = 90, + #[error("Cloud request payload too large")] + CloudRequestPayloadTooLarge = 90, #[error("Workspace limit exceeded")] WorkspaceLimitExceeded = 91, @@ -280,109 +280,6 @@ pub enum ErrorCode { #[error("Workspace data not match")] WorkspaceDataNotMatch = 97, - - #[error("Local AI error")] - LocalAIError = 98, - - #[error("Local AI unavailable")] - LocalAIUnavailable = 99, - - #[error("Storage limit exceeded")] - FileStorageLimitExceeded = 100, - - #[error("AI Response limit exceeded")] - AIResponseLimitExceeded = 101, - - #[error("Duplicate record")] - DuplicateSqliteRecord = 102, - - #[error("Response timeout")] - ResponseTimeout = 103, - - #[error("Unsupported file format")] - UnsupportedFileFormat = 104, - - #[error("AppFlowy LAI not ready")] - AppFlowyLAINotReady = 105, - - #[error("Invalid Request")] - InvalidRequest = 106, - - #[error("In progress")] - // when client receives InProgress, it should retry - InProgress = 107, - - #[error("Upload part size exceeds the limit")] - SingleUploadLimitExceeded = 108, - - #[error("Group name is empty")] - GroupNameIsEmpty = 109, - - #[error("Not available for current workspace plan")] - LimitedByWorkspacePlan = 110, - - #[error("Invalid namespace")] - InvalidNamespace = 111, - - #[error("Invalid publish name")] - InvalidPublishName = 112, - - #[error("Custom namespace requires Pro Plan upgrade")] - CustomNamespaceRequirePlanUpgrade = 113, - - #[error("Requested namespace is not allowed")] - CustomNamespaceNotAllowed = 114, - - #[error("Requested namespace is already taken")] - CustomNamespaceAlreadyTaken = 115, - - #[error("Requested namespace is too short")] - CustomNamespaceTooShort = 116, - - #[error("Requested namespace is too long")] - CustomNamespaceTooLong = 117, - - #[error("Requested namespace is reserved")] - CustomNamespaceReserved = 118, - - #[error("Publish name is already used for another published view")] - PublishNameAlreadyExists = 119, - - #[error("Publish name contains one or more invalid characters")] - PublishNameInvalidCharacter = 120, - - #[error("Publish name has exceeded the maximum length allowable")] - PublishNameTooLong = 121, - - #[error("Requested namespace has one or more invalid characters")] - CustomNamespaceInvalidCharacter = 122, - - #[error("AI Service is unavailable")] - AIServiceUnavailable = 123, - - #[error("AI Image Response limit exceeded")] - AIImageResponseLimitExceeded = 124, - - #[error("AI Max Required")] - AIMaxRequired = 125, - - #[error("View is locked")] - ViewIsLocked = 126, - - #[error("Request timeout")] - RequestTimeout = 127, - - #[error("Local AI is not ready")] - LocalAINotReady = 128, - - #[error("MCP error")] - MCPError = 129, - - #[error("Local AI disabled")] - LocalAIDisabled = 130, - - #[error("User not login")] - UserNotLogin = 131, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index a9a2b6fa2b..a54ac046b6 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -1,7 +1,7 @@ -use collab::error::CollabError; -use protobuf::ProtobufError; use std::convert::TryInto; -use std::fmt::{Debug, Display}; +use std::fmt::Debug; + +use protobuf::ProtobufError; use thiserror::Error; use tokio::task::JoinError; use validator::{ValidationError, ValidationErrors}; @@ -13,7 +13,7 @@ use crate::code::ErrorCode; pub type FlowyResult<T> = anyhow::Result<T, FlowyError>; #[derive(Debug, Default, Clone, ProtoBuf, Error)] -#[error("code:{code}, message:{msg}")] +#[error("{code:?}: {msg}")] pub struct FlowyError { #[pb(index = 1)] pub code: ErrorCode, @@ -42,8 +42,8 @@ impl FlowyError { payload: vec![], } } - pub fn with_context<T: Display>(mut self, error: T) -> Self { - self.msg = format!("{}", error); + pub fn with_context<T: Debug>(mut self, error: T) -> Self { + self.msg = format!("{:?}", error); self } @@ -72,41 +72,6 @@ impl FlowyError { self.code == ErrorCode::LocalVersionNotSupport } - pub fn is_file_limit_exceeded(&self) -> bool { - self.code == ErrorCode::FileStorageLimitExceeded - } - - pub fn is_single_file_limit_exceeded(&self) -> bool { - self.code == ErrorCode::SingleUploadLimitExceeded - } - - pub fn should_retry_upload(&self) -> bool { - !matches!( - self.code, - ErrorCode::FileStorageLimitExceeded | ErrorCode::SingleUploadLimitExceeded - ) - } - - pub fn is_ai_response_limit_exceeded(&self) -> bool { - self.code == ErrorCode::AIResponseLimitExceeded - } - - pub fn is_ai_image_response_limit_exceeded(&self) -> bool { - self.code == ErrorCode::AIImageResponseLimitExceeded - } - - pub fn is_local_ai_not_ready(&self) -> bool { - self.code == ErrorCode::LocalAINotReady - } - - pub fn is_local_ai_disabled(&self) -> bool { - self.code == ErrorCode::LocalAIDisabled - } - - pub fn is_ai_max_required(&self) -> bool { - self.code == ErrorCode::AIMaxRequired - } - static_flowy_error!(internal, ErrorCode::Internal); static_flowy_error!(record_not_found, ErrorCode::RecordNotFound); static_flowy_error!(workspace_initialize, ErrorCode::WorkspaceInitializeError); @@ -139,7 +104,7 @@ impl FlowyError { static_flowy_error!(serde, ErrorCode::Serde); static_flowy_error!(field_record_not_found, ErrorCode::FieldRecordNotFound); static_flowy_error!(payload_none, ErrorCode::UnexpectedEmpty); - static_flowy_error!(http, ErrorCode::NetworkError); + static_flowy_error!(http, ErrorCode::HttpError); static_flowy_error!( unexpect_calendar_field_type, ErrorCode::UnexpectedCalendarFieldType @@ -153,15 +118,6 @@ impl FlowyError { ErrorCode::FolderIndexManagerUnavailable ); static_flowy_error!(workspace_data_not_match, ErrorCode::WorkspaceDataNotMatch); - static_flowy_error!(local_ai, ErrorCode::LocalAIError); - static_flowy_error!(local_ai_unavailable, ErrorCode::LocalAIUnavailable); - static_flowy_error!(response_timeout, ErrorCode::ResponseTimeout); - static_flowy_error!(file_storage_limit, ErrorCode::FileStorageLimitExceeded); - - static_flowy_error!(view_is_locked, ErrorCode::ViewIsLocked); - static_flowy_error!(local_ai_not_ready, ErrorCode::LocalAINotReady); - static_flowy_error!(local_ai_disabled, ErrorCode::LocalAIDisabled); - static_flowy_error!(user_not_login, ErrorCode::UserNotLogin); } impl std::convert::From<ErrorCode> for FlowyError { @@ -179,7 +135,7 @@ pub fn internal_error<T>(e: T) -> FlowyError where T: std::fmt::Debug, { - FlowyError::internal().with_context(format!("{:?}", e)) + FlowyError::internal().with_context(e) } impl std::convert::From<std::io::Error> for FlowyError { @@ -230,36 +186,3 @@ impl From<tokio::sync::oneshot::error::RecvError> for FlowyError { FlowyError::internal().with_context(e) } } - -impl From<String> for FlowyError { - fn from(e: String) -> Self { - FlowyError::internal().with_context(e) - } -} - -impl From<collab::error::CollabError> for FlowyError { - fn from(value: CollabError) -> Self { - match value { - CollabError::SerdeJson(err) => FlowyError::serde().with_context(err), - CollabError::UnexpectedEmpty(err) => FlowyError::payload_none().with_context(err), - CollabError::AcquiredWriteTxnFail => FlowyError::internal(), - CollabError::AcquiredReadTxnFail => FlowyError::internal(), - CollabError::YrsTransactionError(err) => FlowyError::internal().with_context(err), - CollabError::YrsEncodeStateError(err) => FlowyError::internal().with_context(err), - CollabError::UndoManagerNotEnabled => { - FlowyError::not_support().with_context("UndoManager is not enabled") - }, - CollabError::DecodeUpdate(err) => FlowyError::internal().with_context(err), - CollabError::NoRequiredData(err) => FlowyError::internal().with_context(err), - CollabError::Awareness(err) => FlowyError::internal().with_context(err), - CollabError::UpdateFailed(err) => FlowyError::internal().with_context(err), - CollabError::Internal(err) => FlowyError::internal().with_context(err), - } - } -} - -impl From<uuid::Error> for FlowyError { - fn from(value: uuid::Error) -> Self { - FlowyError::internal().with_context(value) - } -} diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index 53617c8c36..b7af102c26 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -1,6 +1,7 @@ -use crate::{ErrorCode, FlowyError}; use client_api::error::{AppResponseError, ErrorCode as AppErrorCode}; +use crate::{ErrorCode, FlowyError}; + impl From<AppResponseError> for FlowyError { fn from(error: AppResponseError) -> Self { let code = match error.code { @@ -14,32 +15,15 @@ impl From<AppResponseError> for FlowyError { AppErrorCode::MissingPayload => ErrorCode::MissingPayload, AppErrorCode::OpenError => ErrorCode::Internal, AppErrorCode::InvalidUrl => ErrorCode::InvalidURL, - AppErrorCode::InvalidRequest => ErrorCode::InvalidRequest, + AppErrorCode::InvalidRequest => ErrorCode::InvalidParams, AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig, AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, - AppErrorCode::NetworkError => ErrorCode::NetworkError, - AppErrorCode::RequestTimeout => ErrorCode::RequestTimeout, - AppErrorCode::PayloadTooLarge => ErrorCode::PayloadTooLarge, + AppErrorCode::NetworkError => ErrorCode::HttpError, + AppErrorCode::PayloadTooLarge => ErrorCode::CloudRequestPayloadTooLarge, AppErrorCode::UserUnAuthorized => ErrorCode::UserUnauthorized, AppErrorCode::WorkspaceLimitExceeded => ErrorCode::WorkspaceLimitExceeded, AppErrorCode::WorkspaceMemberLimitExceeded => ErrorCode::WorkspaceMemberLimitExceeded, - AppErrorCode::AIResponseLimitExceeded => ErrorCode::AIResponseLimitExceeded, - AppErrorCode::AIImageResponseLimitExceeded => ErrorCode::AIImageResponseLimitExceeded, - AppErrorCode::AIMaxRequired => ErrorCode::AIMaxRequired, - AppErrorCode::FileStorageLimitExceeded => ErrorCode::FileStorageLimitExceeded, - AppErrorCode::SingleUploadLimitExceeded => ErrorCode::SingleUploadLimitExceeded, - AppErrorCode::CustomNamespaceDisabled => ErrorCode::CustomNamespaceRequirePlanUpgrade, - AppErrorCode::CustomNamespaceDisallowed => ErrorCode::CustomNamespaceNotAllowed, - AppErrorCode::PublishNamespaceAlreadyTaken => ErrorCode::CustomNamespaceAlreadyTaken, - AppErrorCode::CustomNamespaceTooShort => ErrorCode::CustomNamespaceTooShort, - AppErrorCode::CustomNamespaceTooLong => ErrorCode::CustomNamespaceTooLong, - AppErrorCode::CustomNamespaceReserved => ErrorCode::CustomNamespaceReserved, - AppErrorCode::PublishNameAlreadyExists => ErrorCode::PublishNameAlreadyExists, - AppErrorCode::PublishNameInvalidCharacter => ErrorCode::PublishNameInvalidCharacter, - AppErrorCode::PublishNameTooLong => ErrorCode::PublishNameTooLong, - AppErrorCode::CustomNamespaceInvalidCharacter => ErrorCode::CustomNamespaceInvalidCharacter, - AppErrorCode::AIServiceUnavailable => ErrorCode::AIServiceUnavailable, _ => ErrorCode::Internal, }; diff --git a/frontend/rust-lib/flowy-error/src/impl_from/database.rs b/frontend/rust-lib/flowy-error/src/impl_from/database.rs index 077ff2b708..3a72a7cdf3 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/database.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/database.rs @@ -1,12 +1,8 @@ use crate::FlowyError; -use flowy_sqlite::Error; impl std::convert::From<flowy_sqlite::Error> for FlowyError { fn from(error: flowy_sqlite::Error) -> Self { - match error { - Error::NotFound => FlowyError::record_not_found(), - _ => FlowyError::internal().with_context(error), - } + FlowyError::internal().with_context(error) } } diff --git a/frontend/rust-lib/flowy-error/src/impl_from/mod.rs b/frontend/rust-lib/flowy-error/src/impl_from/mod.rs index a2d11c66e4..b3d0351cd4 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/mod.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/mod.rs @@ -13,11 +13,7 @@ pub mod reqwest; #[cfg(feature = "impl_from_sqlite")] pub mod database; -#[cfg(any( - feature = "impl_from_collab_document", - feature = "impl_from_collab_folder", - feature = "impl_from_collab_database" -))] +#[cfg(feature = "impl_from_collab_document")] pub mod collab; #[cfg(feature = "impl_from_collab_persistence")] diff --git a/frontend/rust-lib/flowy-folder-pub/Cargo.toml b/frontend/rust-lib/flowy-folder-pub/Cargo.toml index 9f33bfe1c4..13f13935f7 100644 --- a/frontend/rust-lib/flowy-folder-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-folder-pub/Cargo.toml @@ -12,10 +12,6 @@ collab = { workspace = true } collab-entity = { workspace = true } uuid.workspace = true anyhow.workspace = true -serde = { version = "1.0.202", features = ["derive"] } -serde_json.workspace = true -client-api = { workspace = true } -flowy-error.workspace = true [dev-dependencies] tokio.workspace = true diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 05cc8f867d..d88b4df203 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -1,108 +1,58 @@ -use crate::entities::PublishPayload; pub use anyhow::Error; -use client_api::entity::{workspace_dto::PublishInfoView, PublishInfo}; -use collab::entity::EncodedCollab; use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; -use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; use uuid::Uuid; +use lib_infra::future::FutureResult; + /// [FolderCloudService] represents the cloud service for folder. -#[async_trait] pub trait FolderCloudService: Send + Sync + 'static { - async fn get_folder_snapshots( + /// Creates a new workspace for the user. + /// Returns error if the cloud service doesn't support multiple workspaces + fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, Error>; + + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error>; + + /// Returns all workspaces of the user. + /// Returns vec![] if the cloud service doesn't support multiple workspaces + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error>; + + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult<Option<FolderData>, Error>; + + fn get_folder_snapshots( &self, workspace_id: &str, limit: usize, - ) -> Result<Vec<FolderSnapshot>, FlowyError>; + ) -> FutureResult<Vec<FolderSnapshot>, Error>; - async fn get_folder_doc_state( + fn get_folder_doc_state( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, collab_type: CollabType, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError>; + object_id: &str, + ) -> FutureResult<Vec<u8>, Error>; - async fn full_sync_collab_object( + fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError>; - - async fn batch_create_folder_collab_objects( - &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec<FolderCollabParams>, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), Error>; fn service_name(&self) -> String; - - async fn publish_view( - &self, - workspace_id: &Uuid, - payload: Vec<PublishPayload>, - ) -> Result<(), FlowyError>; - - async fn unpublish_views( - &self, - workspace_id: &Uuid, - view_ids: Vec<Uuid>, - ) -> Result<(), FlowyError>; - - async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError>; - - async fn set_publish_name( - &self, - workspace_id: &Uuid, - view_id: Uuid, - new_name: String, - ) -> Result<(), FlowyError>; - - async fn set_publish_namespace( - &self, - workspace_id: &Uuid, - new_namespace: String, - ) -> Result<(), FlowyError>; - - async fn list_published_views( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<PublishInfoView>, FlowyError>; - - async fn get_default_published_view_info( - &self, - workspace_id: &Uuid, - ) -> Result<PublishInfo, FlowyError>; - - async fn set_default_published_view( - &self, - workspace_id: &Uuid, - view_id: uuid::Uuid, - ) -> Result<(), FlowyError>; - - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError>; - - async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError>; } #[derive(Debug)] pub struct FolderCollabParams { - pub object_id: Uuid, + pub object_id: String, pub encoded_collab_v1: Vec<u8>, pub collab_type: CollabType, } -#[derive(Debug)] -pub struct FullSyncCollabParams { - pub object_id: Uuid, - pub encoded_collab: EncodedCollab, - pub collab_type: CollabType, -} - pub struct FolderSnapshot { pub snapshot_id: i64, pub database_id: String, diff --git a/frontend/rust-lib/flowy-folder-pub/src/entities.rs b/frontend/rust-lib/flowy-folder-pub/src/entities.rs index d06eb50e25..41163fae73 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/entities.rs @@ -1,30 +1,21 @@ -use collab_folder::hierarchy_builder::ParentChildViews; -use collab_folder::{ViewIcon, ViewLayout}; -use serde::{Deserialize, Serialize}; +use crate::folder_builder::ParentChildViews; use std::collections::HashMap; -pub struct ImportedAppFlowyData { - pub source: ImportFrom, - pub folder_data: ImportedFolderData, - pub collab_data: ImportedCollabData, - pub parent_view_id: Option<String>, +pub enum ImportData { + AppFlowyDataFolder { items: Vec<AppFlowyData> }, } -pub enum ImportFrom { - AnonUser, - AppFlowyDataFolder, -} - -pub struct ImportedFolderData { - pub views: Vec<ParentChildViews>, - pub orphan_views: Vec<ParentChildViews>, - /// Used to update the [DatabaseViewTrackerList] when importing the database. - pub database_view_ids_by_database_id: HashMap<String, Vec<String>>, -} -pub struct ImportedCollabData { - pub row_object_ids: Vec<String>, - pub document_object_ids: Vec<String>, - pub database_object_ids: Vec<String>, +pub enum AppFlowyData { + Folder { + views: Vec<ParentChildViews>, + /// Used to update the [DatabaseViewTrackerList] when importing the database. + database_view_ids_by_database_id: HashMap<String, Vec<String>>, + }, + CollabObject { + row_object_ids: Vec<String>, + document_object_ids: Vec<String>, + database_object_ids: Vec<String>, + }, } pub struct ImportViews { @@ -48,71 +39,3 @@ pub struct SearchData { /// The data that is stored in the search index row. pub data: String, } - -#[derive(Serialize, Clone, Debug, Eq, PartialEq)] -pub struct PublishViewInfo { - pub view_id: String, - pub name: String, - pub icon: Option<ViewIcon>, - pub layout: ViewLayout, - pub extra: Option<String>, - pub created_by: Option<i64>, - pub last_edited_by: Option<i64>, - pub last_edited_time: i64, - pub created_at: i64, - pub child_views: Option<Vec<PublishViewInfo>>, -} - -#[derive(Serialize, Clone, Debug, Eq, PartialEq)] -pub struct PublishViewMetaData { - pub view: PublishViewInfo, - pub child_views: Vec<PublishViewInfo>, - pub ancestor_views: Vec<PublishViewInfo>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublishViewMeta { - pub metadata: PublishViewMetaData, - pub view_id: String, - pub publish_name: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, Eq, PartialEq)] -pub struct PublishDatabaseData { - /// The encoded collab data for the database itself - pub database_collab: Vec<u8>, - - /// The encoded collab data for the database rows - /// Use the row_id as the key - pub database_row_collabs: HashMap<String, Vec<u8>>, - - /// The encoded collab data for the documents inside the database rows - pub database_row_document_collabs: HashMap<String, Vec<u8>>, - - /// Visible view ids - pub visible_database_view_ids: Vec<String>, - - /// Relation view id map - pub database_relations: HashMap<String, String>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublishDocumentPayload { - pub meta: PublishViewMeta, - - /// The encoded collab data for the document - pub data: Vec<u8>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct PublishDatabasePayload { - pub meta: PublishViewMeta, - pub data: PublishDatabaseData, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum PublishPayload { - Document(PublishDocumentPayload), - Database(PublishDatabasePayload), - Unknown, -} diff --git a/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs new file mode 100644 index 0000000000..20604b0018 --- /dev/null +++ b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs @@ -0,0 +1,321 @@ +use std::future::Future; + +use crate::cloud::gen_view_id; +use collab_folder::{RepeatedViewIdentifier, View, ViewIcon, ViewIdentifier, ViewLayout}; +use lib_infra::util::timestamp; + +/// A builder for creating views, each able to have children views of +/// their own. +pub struct NestedViewBuilder { + pub uid: i64, + pub parent_view_id: String, + pub views: Vec<ParentChildViews>, +} + +impl NestedViewBuilder { + pub fn new(parent_view_id: String, uid: i64) -> Self { + Self { + uid, + parent_view_id, + views: vec![], + } + } + + pub async fn with_view_builder<F, O>(&mut self, view_builder: F) + where + F: Fn(ViewBuilder) -> O, + O: Future<Output = ParentChildViews>, + { + let builder = ViewBuilder::new(self.uid, self.parent_view_id.clone()); + self.views.push(view_builder(builder).await); + } + + pub fn build(&mut self) -> Vec<ParentChildViews> { + std::mem::take(&mut self.views) + } +} + +/// A builder for creating a view. +/// The default layout of the view is [ViewLayout::Document] +pub struct ViewBuilder { + uid: i64, + parent_view_id: String, + view_id: String, + name: String, + desc: String, + layout: ViewLayout, + child_views: Vec<ParentChildViews>, + is_favorite: bool, + icon: Option<ViewIcon>, +} + +impl ViewBuilder { + pub fn new(uid: i64, parent_view_id: String) -> Self { + Self { + uid, + parent_view_id, + view_id: gen_view_id().to_string(), + name: Default::default(), + desc: Default::default(), + layout: ViewLayout::Document, + child_views: vec![], + is_favorite: false, + + icon: None, + } + } + + pub fn view_id(&self) -> &str { + &self.view_id + } + + pub fn with_view_id<T: ToString>(mut self, view_id: T) -> Self { + self.view_id = view_id.to_string(); + self + } + + pub fn with_layout(mut self, layout: ViewLayout) -> Self { + self.layout = layout; + self + } + + pub fn with_name<T: ToString>(mut self, name: T) -> Self { + self.name = name.to_string(); + self + } + + pub fn with_desc(mut self, desc: &str) -> Self { + self.desc = desc.to_string(); + self + } + + pub fn with_icon(mut self, icon: &str) -> Self { + self.icon = Some(ViewIcon { + ty: collab_folder::IconType::Emoji, + value: icon.to_string(), + }); + self + } + + pub fn with_view(mut self, view: ParentChildViews) -> Self { + self.child_views.push(view); + self + } + + pub fn with_child_views(mut self, mut views: Vec<ParentChildViews>) -> Self { + self.child_views.append(&mut views); + self + } + + /// Create a child view for the current view. + /// The view created by this builder will be the next level view of the current view. + pub async fn with_child_view_builder<F, O>(mut self, child_view_builder: F) -> Self + where + F: Fn(ViewBuilder) -> O, + O: Future<Output = ParentChildViews>, + { + let builder = ViewBuilder::new(self.uid, self.view_id.clone()); + self.child_views.push(child_view_builder(builder).await); + self + } + + pub fn build(self) -> ParentChildViews { + let view = View { + id: self.view_id, + parent_view_id: self.parent_view_id, + name: self.name, + desc: self.desc, + created_at: timestamp(), + is_favorite: self.is_favorite, + layout: self.layout, + icon: self.icon, + created_by: Some(self.uid), + last_edited_time: 0, + children: RepeatedViewIdentifier::new( + self + .child_views + .iter() + .map(|v| ViewIdentifier { + id: v.parent_view.id.clone(), + }) + .collect(), + ), + last_edited_by: Some(self.uid), + extra: None, + }; + ParentChildViews { + parent_view: view, + child_views: self.child_views, + } + } +} + +#[derive(Clone)] +pub struct ParentChildViews { + pub parent_view: View, + pub child_views: Vec<ParentChildViews>, +} + +impl ParentChildViews { + pub fn new(view: View) -> Self { + Self { + parent_view: view, + child_views: vec![], + } + } + + pub fn flatten(self) -> Vec<View> { + FlattedViews::flatten_views(vec![self]) + } +} + +pub struct FlattedViews; + +impl FlattedViews { + pub fn flatten_views(views: Vec<ParentChildViews>) -> Vec<View> { + let mut result = vec![]; + for view in views { + result.push(view.parent_view); + result.append(&mut Self::flatten_views(view.child_views)); + } + result + } +} + +#[cfg(test)] +mod tests { + use crate::folder_builder::{FlattedViews, NestedViewBuilder}; + + #[tokio::test] + async fn create_first_level_views_test() { + let workspace_id = "w1".to_string(); + let mut builder = NestedViewBuilder::new(workspace_id, 1); + builder + .with_view_builder(|view_builder| async { view_builder.with_name("1").build() }) + .await; + builder + .with_view_builder(|view_builder| async { view_builder.with_name("2").build() }) + .await; + builder + .with_view_builder(|view_builder| async { view_builder.with_name("3").build() }) + .await; + let workspace_views = builder.build(); + assert_eq!(workspace_views.len(), 3); + + let views = FlattedViews::flatten_views(workspace_views); + assert_eq!(views.len(), 3); + } + + #[tokio::test] + async fn create_view_with_child_views_test() { + let workspace_id = "w1".to_string(); + let mut builder = NestedViewBuilder::new(workspace_id, 1); + builder + .with_view_builder(|view_builder| async { + view_builder + .with_name("1") + .with_child_view_builder(|child_view_builder| async { + child_view_builder.with_name("1_1").build() + }) + .await + .with_child_view_builder(|child_view_builder| async { + child_view_builder.with_name("1_2").build() + }) + .await + .build() + }) + .await; + builder + .with_view_builder(|view_builder| async { + view_builder + .with_name("2") + .with_child_view_builder(|child_view_builder| async { + child_view_builder.with_name("2_1").build() + }) + .await + .build() + }) + .await; + let workspace_views = builder.build(); + assert_eq!(workspace_views.len(), 2); + + assert_eq!(workspace_views[0].parent_view.name, "1"); + assert_eq!(workspace_views[0].child_views.len(), 2); + assert_eq!(workspace_views[0].child_views[0].parent_view.name, "1_1"); + assert_eq!(workspace_views[0].child_views[1].parent_view.name, "1_2"); + assert_eq!(workspace_views[1].child_views.len(), 1); + assert_eq!(workspace_views[1].child_views[0].parent_view.name, "2_1"); + + let views = FlattedViews::flatten_views(workspace_views); + assert_eq!(views.len(), 5); + } + + #[tokio::test] + async fn create_three_level_view_test() { + let workspace_id = "w1".to_string(); + let mut builder = NestedViewBuilder::new(workspace_id, 1); + builder + .with_view_builder(|view_builder| async { + view_builder + .with_name("1") + .with_child_view_builder(|child_view_builder| async { + child_view_builder + .with_name("1_1") + .with_child_view_builder(|b| async { b.with_name("1_1_1").build() }) + .await + .with_child_view_builder(|b| async { b.with_name("1_1_2").build() }) + .await + .build() + }) + .await + .with_child_view_builder(|child_view_builder| async { + child_view_builder + .with_name("1_2") + .with_child_view_builder(|b| async { b.with_name("1_2_1").build() }) + .await + .with_child_view_builder(|b| async { b.with_name("1_2_2").build() }) + .await + .build() + }) + .await + .build() + }) + .await; + let workspace_views = builder.build(); + assert_eq!(workspace_views.len(), 1); + + assert_eq!(workspace_views[0].parent_view.name, "1"); + assert_eq!(workspace_views[0].child_views.len(), 2); + assert_eq!(workspace_views[0].child_views[0].parent_view.name, "1_1"); + assert_eq!(workspace_views[0].child_views[1].parent_view.name, "1_2"); + + assert_eq!( + workspace_views[0].child_views[0].child_views[0] + .parent_view + .name, + "1_1_1" + ); + assert_eq!( + workspace_views[0].child_views[0].child_views[1] + .parent_view + .name, + "1_1_2" + ); + + assert_eq!( + workspace_views[0].child_views[1].child_views[0] + .parent_view + .name, + "1_2_1" + ); + assert_eq!( + workspace_views[0].child_views[1].child_views[1] + .parent_view + .name, + "1_2_2" + ); + + let views = FlattedViews::flatten_views(workspace_views); + assert_eq!(views.len(), 7); + } +} diff --git a/frontend/rust-lib/flowy-folder-pub/src/lib.rs b/frontend/rust-lib/flowy-folder-pub/src/lib.rs index 38d61c8e9c..feaa5c2a0e 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/lib.rs @@ -1,3 +1,3 @@ pub mod cloud; pub mod entities; -pub mod query; +pub mod folder_builder; diff --git a/frontend/rust-lib/flowy-folder-pub/src/query.rs b/frontend/rust-lib/flowy-folder-pub/src/query.rs deleted file mode 100644 index 74761e44db..0000000000 --- a/frontend/rust-lib/flowy-folder-pub/src/query.rs +++ /dev/null @@ -1,31 +0,0 @@ -use collab::entity::EncodedCollab; -use collab_entity::CollabType; -use collab_folder::ViewLayout; -use flowy_error::FlowyResult; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; - -pub struct QueryCollab { - pub collab_type: CollabType, - pub encoded_collab: EncodedCollab, -} - -pub trait FolderService: FolderQueryService + FolderViewEdit {} - -#[async_trait] -pub trait FolderQueryService: Send + Sync + 'static { - /// gets the parent view and all of the ids of its children views matching - /// the provided view layout, given that the parent view is not a space - async fn get_surrounding_view_ids_with_view_layout( - &self, - parent_view_id: &Uuid, - view_layout: ViewLayout, - ) -> Vec<Uuid>; - - async fn get_collab(&self, object_id: &Uuid, collab_type: CollabType) -> Option<QueryCollab>; -} - -#[async_trait] -pub trait FolderViewEdit: Send + Sync + 'static { - async fn set_view_title_if_empty(&self, view_id: &Uuid, title: &str) -> FlowyResult<()>; -} diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 13b19e48b8..b8ed79720f 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -14,16 +14,15 @@ collab-plugins = { workspace = true } collab-integrate = { workspace = true } flowy-folder-pub = { workspace = true } flowy-search-pub = { workspace = true } -flowy-user-pub = { workspace = true } flowy-sqlite = { workspace = true } flowy-derive.workspace = true flowy-notification = { workspace = true } -arc-swap.workspace = true +parking_lot.workspace = true unicode-segmentation = "1.10" tracing.workspace = true flowy-error = { path = "../flowy-error", features = [ - "impl_from_dispatch_error", - "impl_from_collab_folder", + "impl_from_dispatch_error", + "impl_from_collab_folder", ] } lib-dispatch = { workspace = true } bytes.workspace = true @@ -40,15 +39,12 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true validator.workspace = true async-trait.workspace = true -client-api = { workspace = true } -regex = "1.9.5" -futures = "0.3.31" -dashmap.workspace = true - [build-dependencies] flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] test_helper = [] diff --git a/frontend/rust-lib/flowy-folder/build.rs b/frontend/rust-lib/flowy-folder/build.rs index 77c0c8125b..fac4cc65ae 100644 --- a/frontend/rust-lib/flowy-folder/build.rs +++ b/frontend/rust-lib/flowy-folder/build.rs @@ -4,4 +4,37 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } + + #[cfg(feature = "web_ts")] + { + flowy_codegen::ts_event::gen( + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + } } diff --git a/frontend/rust-lib/flowy-folder/src/entities/icon.rs b/frontend/rust-lib/flowy-folder/src/entities/icon.rs index 62e1760293..2342b02246 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/icon.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/icon.rs @@ -31,16 +31,6 @@ impl From<IconType> for ViewIconTypePB { } } -impl From<client_api::entity::workspace_dto::IconType> for ViewIconTypePB { - fn from(val: client_api::entity::workspace_dto::IconType) -> Self { - match val { - client_api::entity::workspace_dto::IconType::Emoji => ViewIconTypePB::Emoji, - client_api::entity::workspace_dto::IconType::Url => ViewIconTypePB::Url, - client_api::entity::workspace_dto::IconType::Icon => ViewIconTypePB::Icon, - } - } -} - #[derive(Default, ProtoBuf, Debug, Clone, PartialEq, Eq)] pub struct ViewIconPB { #[pb(index = 1)] @@ -49,7 +39,7 @@ pub struct ViewIconPB { pub value: String, } -impl From<ViewIconPB> for ViewIcon { +impl std::convert::From<ViewIconPB> for ViewIcon { fn from(rev: ViewIconPB) -> Self { ViewIcon { ty: rev.ty.into(), @@ -67,15 +57,6 @@ impl From<ViewIcon> for ViewIconPB { } } -impl From<client_api::entity::workspace_dto::ViewIcon> for ViewIconPB { - fn from(val: client_api::entity::workspace_dto::ViewIcon) -> Self { - ViewIconPB { - ty: val.ty.into(), - value: val.value, - } - } -} - #[derive(Default, ProtoBuf)] pub struct UpdateViewIconPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-folder/src/entities/import.rs b/frontend/rust-lib/flowy-folder/src/entities/import.rs index 83e8bdf874..363ad2b2c2 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/import.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/import.rs @@ -1,20 +1,15 @@ use crate::entities::parser::empty_str::NotEmptyStr; use crate::entities::ViewLayoutPB; -use crate::share::{ImportData, ImportItem, ImportParams, ImportType}; +use crate::share::{ImportParams, ImportType}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::FlowyError; -use lib_infra::validator_fn::required_not_empty_str; -use std::str::FromStr; -use uuid::Uuid; -use validator::Validate; #[derive(Clone, Debug, ProtoBuf_Enum)] pub enum ImportTypePB { HistoryDocument = 0, HistoryDatabase = 1, - Markdown = 2, - AFDatabase = 3, - CSV = 4, + RawDatabase = 2, + CSV = 3, } impl From<ImportTypePB> for ImportType { @@ -22,8 +17,7 @@ impl From<ImportTypePB> for ImportType { match pb { ImportTypePB::HistoryDocument => ImportType::HistoryDocument, ImportTypePB::HistoryDatabase => ImportType::HistoryDatabase, - ImportTypePB::Markdown => ImportType::Markdown, - ImportTypePB::AFDatabase => ImportType::AFDatabase, + ImportTypePB::RawDatabase => ImportType::RawDatabase, ImportTypePB::CSV => ImportType::CSV, } } @@ -31,46 +25,32 @@ impl From<ImportTypePB> for ImportType { impl Default for ImportTypePB { fn default() -> Self { - Self::Markdown + Self::HistoryDocument } } #[derive(Clone, Debug, ProtoBuf, Default)] -pub struct ImportItemPayloadPB { - // the name of the import page +pub struct ImportPB { #[pb(index = 1)] - pub name: String, - - // the data of the import page - // if the data is empty, the file_path must be provided - #[pb(index = 2, one_of)] - pub data: Option<Vec<u8>>, - - // the file path of the import page - // if the file_path is empty, the data must be provided - #[pb(index = 3, one_of)] - pub file_path: Option<String>, - - // the layout of the import page - #[pb(index = 4)] - pub view_layout: ViewLayoutPB, - - // the type of the import page - #[pb(index = 5)] - pub import_type: ImportTypePB, -} - -#[derive(Clone, Debug, Validate, ProtoBuf, Default)] -pub struct ImportPayloadPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] pub parent_view_id: String, #[pb(index = 2)] - pub items: Vec<ImportItemPayloadPB>, + pub name: String, + + #[pb(index = 3, one_of)] + pub data: Option<Vec<u8>>, + + #[pb(index = 4, one_of)] + pub file_path: Option<String>, + + #[pb(index = 5)] + pub view_layout: ViewLayoutPB, + + #[pb(index = 6)] + pub import_type: ImportTypePB, } -impl TryInto<ImportParams> for ImportPayloadPB { +impl TryInto<ImportParams> for ImportPB { type Error = FlowyError; fn try_into(self) -> Result<ImportParams, Self::Error> { @@ -78,48 +58,28 @@ impl TryInto<ImportParams> for ImportPayloadPB { .map_err(|_| FlowyError::invalid_view_id())? .0; - let parent_view_id = Uuid::from_str(&parent_view_id)?; + let name = if self.name.is_empty() { + "Untitled".to_string() + } else { + self.name + }; - let items = self - .items - .into_iter() - .map(|item| { - let name = if item.name.is_empty() { - "Untitled".to_string() - } else { - item.name - }; - - let data = match (item.file_path, item.data) { - (Some(file_path), None) => ImportData::FilePath { file_path }, - (None, Some(bytes)) => ImportData::Bytes { bytes }, - (None, None) => { - return Err(FlowyError::invalid_data().with_context("The import data is empty")); - }, - (Some(_), Some(_)) => { - return Err(FlowyError::invalid_data().with_context("The import data is ambiguous")); - }, - }; - - Ok(ImportItem { - name, - data, - view_layout: item.view_layout.into(), - import_type: item.import_type.into(), - }) - }) - .collect::<Result<Vec<_>, _>>()?; + let file_path = match self.file_path { + None => None, + Some(file_path) => Some( + NotEmptyStr::parse(file_path) + .map_err(|_| FlowyError::invalid_data().with_context("The import file path is empty"))? + .0, + ), + }; Ok(ImportParams { parent_view_id, - items, + name, + data: self.data, + file_path, + view_layout: self.view_layout.into(), + import_type: self.import_type.into(), }) } } - -#[derive(Clone, Debug, Validate, ProtoBuf, Default)] -pub struct ImportZipPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub file_path: String, -} diff --git a/frontend/rust-lib/flowy-folder/src/entities/mod.rs b/frontend/rust-lib/flowy-folder/src/entities/mod.rs index 24e5475caa..b496f334b5 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/mod.rs @@ -1,14 +1,12 @@ pub mod icon; mod import; mod parser; -pub mod publish; pub mod trash; pub mod view; pub mod workspace; pub use icon::*; pub use import::*; -pub use publish::*; pub use trash::*; pub use view::*; pub use workspace::*; diff --git a/frontend/rust-lib/flowy-folder/src/entities/parser/trash/trash_id.rs b/frontend/rust-lib/flowy-folder/src/entities/parser/trash/trash_id.rs index 45f5a46ac3..3b6a4c4f6d 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/parser/trash/trash_id.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/parser/trash/trash_id.rs @@ -17,3 +17,18 @@ impl AsRef<str> for TrashIdentify { &self.0 } } + +#[derive(Debug)] +pub struct TrashIds(pub Vec<String>); + +impl TrashIds { + #[allow(dead_code)] + pub fn parse(ids: Vec<String>) -> Result<TrashIds, String> { + let mut trash_ids = vec![]; + for id in ids { + let id = TrashIdentify::parse(id)?; + trash_ids.push(id.0); + } + Ok(Self(trash_ids)) + } +} diff --git a/frontend/rust-lib/flowy-folder/src/entities/publish.rs b/frontend/rust-lib/flowy-folder/src/entities/publish.rs deleted file mode 100644 index ac23d810f5..0000000000 --- a/frontend/rust-lib/flowy-folder/src/entities/publish.rs +++ /dev/null @@ -1,110 +0,0 @@ -use client_api::entity::workspace_dto::{FolderViewMinimal, PublishInfoView}; -use client_api::entity::PublishInfo; -use flowy_derive::ProtoBuf; - -use super::{RepeatedViewIdPB, ViewIconPB, ViewLayoutPB}; - -#[derive(Default, ProtoBuf)] -pub struct PublishViewParamsPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2, one_of)] - pub publish_name: Option<String>, - - #[pb(index = 3, one_of)] - pub selected_view_ids: Option<RepeatedViewIdPB>, -} - -#[derive(Default, ProtoBuf)] -pub struct UnpublishViewsPayloadPB { - #[pb(index = 1)] - pub view_ids: Vec<String>, -} - -#[derive(Default, ProtoBuf)] -pub struct PublishInfoViewPB { - #[pb(index = 1)] - pub view: FolderViewMinimalPB, - #[pb(index = 2)] - pub info: PublishInfoResponsePB, -} - -impl From<PublishInfoView> for PublishInfoViewPB { - fn from(info_view: PublishInfoView) -> Self { - Self { - view: info_view.view.into(), - info: info_view.info.into(), - } - } -} - -#[derive(Default, ProtoBuf)] -pub struct FolderViewMinimalPB { - #[pb(index = 1)] - pub view_id: String, - #[pb(index = 2)] - pub name: String, - #[pb(index = 3, one_of)] - pub icon: Option<ViewIconPB>, - #[pb(index = 4)] - pub layout: ViewLayoutPB, -} - -impl From<FolderViewMinimal> for FolderViewMinimalPB { - fn from(view: FolderViewMinimal) -> Self { - Self { - view_id: view.view_id, - name: view.name, - icon: view.icon.map(Into::into), - layout: view.layout.into(), - } - } -} - -#[derive(Default, ProtoBuf)] -pub struct PublishInfoResponsePB { - #[pb(index = 1)] - pub view_id: String, - #[pb(index = 2)] - pub publish_name: String, - #[pb(index = 3, one_of)] - pub namespace: Option<String>, - #[pb(index = 4)] - pub publisher_email: String, - #[pb(index = 5)] - pub publish_timestamp_sec: i64, - #[pb(index = 6, one_of)] - pub unpublished_at_timestamp_sec: Option<i64>, -} - -impl From<PublishInfo> for PublishInfoResponsePB { - fn from(info: PublishInfo) -> Self { - Self { - view_id: info.view_id.to_string(), - publish_name: info.publish_name, - namespace: Some(info.namespace), - publisher_email: info.publisher_email, - publish_timestamp_sec: info.publish_timestamp.timestamp(), - unpublished_at_timestamp_sec: info.unpublished_timestamp.map(|t| t.timestamp()), - } - } -} - -#[derive(Default, ProtoBuf)] -pub struct RepeatedPublishInfoViewPB { - #[pb(index = 1)] - pub items: Vec<PublishInfoViewPB>, -} - -#[derive(Default, ProtoBuf)] -pub struct SetPublishNamespacePayloadPB { - #[pb(index = 1)] - pub new_namespace: String, -} - -#[derive(Default, ProtoBuf)] -pub struct PublishNamespacePB { - #[pb(index = 1)] - pub namespace: String, -} diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 171bc39c7d..d0568995f6 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -1,19 +1,16 @@ -use collab_folder::{View, ViewIcon, ViewLayout}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::ErrorCode; -use flowy_folder_pub::cloud::gen_view_id; -use lib_infra::validator_fn::required_not_empty_str; use std::collections::HashMap; use std::convert::TryInto; use std::ops::{Deref, DerefMut}; -use std::str::FromStr; use std::sync::Arc; -use uuid::Uuid; -use validator::Validate; + +use collab_folder::{View, ViewLayout}; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::ErrorCode; +use flowy_folder_pub::cloud::gen_view_id; use crate::entities::icon::ViewIconPB; use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail}; -use crate::view_operation::ViewData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ChildViewUpdatePB { @@ -75,11 +72,6 @@ pub struct ViewPB { // user_id #[pb(index = 12, one_of)] pub last_edited_by: Option<i64>, - - // is_locked - // If true, the view is locked and cannot be edited. - #[pb(index = 13, one_of)] - pub is_locked: Option<bool>, } pub fn view_pb_without_child_views(view: View) -> ViewPB { @@ -96,7 +88,6 @@ pub fn view_pb_without_child_views(view: View) -> ViewPB { created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, - is_locked: view.is_locked, } } @@ -114,7 +105,6 @@ pub fn view_pb_without_child_views_from_arc(view: Arc<View>) -> ViewPB { created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, - is_locked: view.is_locked, } } @@ -136,7 +126,6 @@ pub fn view_pb_with_child_views(view: Arc<View>, child_views: Vec<Arc<View>>) -> created_by: view.created_by, last_edited: view.last_edited_time, last_edited_by: view.last_edited_by, - is_locked: view.is_locked, } } @@ -171,18 +160,6 @@ impl std::convert::From<ViewLayout> for ViewLayoutPB { } } -impl From<client_api::entity::workspace_dto::ViewLayout> for ViewLayoutPB { - fn from(val: client_api::entity::workspace_dto::ViewLayout) -> Self { - match val { - client_api::entity::workspace_dto::ViewLayout::Document => ViewLayoutPB::Document, - client_api::entity::workspace_dto::ViewLayout::Grid => ViewLayoutPB::Grid, - client_api::entity::workspace_dto::ViewLayout::Board => ViewLayoutPB::Board, - client_api::entity::workspace_dto::ViewLayout::Calendar => ViewLayoutPB::Calendar, - client_api::entity::workspace_dto::ViewLayout::Chat => ViewLayoutPB::Chat, - } - } -} - #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] pub struct SectionViewsPB { #[pb(index = 1)] @@ -204,15 +181,6 @@ pub struct RepeatedFavoriteViewPB { pub items: Vec<SectionViewPB>, } -#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] -pub struct ReadRecentViewsPB { - #[pb(index = 1)] - pub start: u64, - - #[pb(index = 2)] - pub limit: u64, -} - #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] pub struct RepeatedRecentViewPB { #[pb(index = 1)] @@ -261,40 +229,38 @@ pub struct CreateViewPayloadPB { #[pb(index = 2)] pub name: String, - #[pb(index = 3, one_of)] + #[pb(index = 3)] + pub desc: String, + + #[pb(index = 4, one_of)] pub thumbnail: Option<String>, - #[pb(index = 4)] + #[pb(index = 5)] pub layout: ViewLayoutPB, - #[pb(index = 5)] + #[pb(index = 6)] pub initial_data: Vec<u8>, - #[pb(index = 6)] + #[pb(index = 7)] pub meta: HashMap<String, String>, // Mark the view as current view after creation. - #[pb(index = 7)] + #[pb(index = 8)] pub set_as_current: bool, // The index of the view in the parent view. // If the index is None or the index is out of range, the view will be appended to the end of the parent view. - #[pb(index = 8, one_of)] + #[pb(index = 9, one_of)] pub index: Option<u32>, // The section of the view. // Only the view in public section will be shown in the shared workspace view list. // The view in private section will only be shown in the user's private view list. - #[pb(index = 9, one_of)] + #[pb(index = 10, one_of)] pub section: Option<ViewSectionPB>, - #[pb(index = 10, one_of)] - pub view_id: Option<String>, - - // The extra data of the view. - // Refer to the extra field in the collab #[pb(index = 11, one_of)] - pub extra: Option<String>, + pub view_id: Option<String>, } #[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone, Default)] @@ -317,19 +283,23 @@ pub struct CreateOrphanViewPayloadPB { pub name: String, #[pb(index = 3)] - pub layout: ViewLayoutPB, + pub desc: String, #[pb(index = 4)] + pub layout: ViewLayoutPB, + + #[pb(index = 5)] pub initial_data: Vec<u8>, } #[derive(Debug, Clone)] pub struct CreateViewParams { - pub parent_view_id: Uuid, + pub parent_view_id: String, pub name: String, + pub desc: String, pub layout: ViewLayoutPB, - pub view_id: Uuid, - pub initial_data: ViewData, + pub view_id: String, + pub initial_data: Vec<u8>, pub meta: HashMap<String, String>, // Mark the view as current view after creation. pub set_as_current: bool, @@ -338,10 +308,6 @@ pub struct CreateViewParams { pub index: Option<u32>, // The section of the view. pub section: Option<ViewSectionPB>, - // The icon of the view. - pub icon: Option<ViewIcon>, - // The extra data of the view. - pub extra: Option<String>, } impl TryInto<CreateViewParams> for CreateViewPayloadPB { @@ -349,26 +315,21 @@ impl TryInto<CreateViewParams> for CreateViewPayloadPB { fn try_into(self) -> Result<CreateViewParams, Self::Error> { let name = ViewName::parse(self.name)?.0; - let parent_view_id = ViewIdentify::parse(self.parent_view_id) - .and_then(|id| Uuid::from_str(&id.0).map_err(|_| ErrorCode::InvalidParams))?; + let parent_view_id = ViewIdentify::parse(self.parent_view_id)?.0; // if view_id is not provided, generate a new view_id - let view_id = self - .view_id - .and_then(|v| Uuid::parse_str(&v).ok()) - .unwrap_or_else(gen_view_id); + let view_id = self.view_id.unwrap_or_else(|| gen_view_id().to_string()); Ok(CreateViewParams { parent_view_id, name, + desc: self.desc, layout: self.layout, view_id, - initial_data: ViewData::Data(self.initial_data.into()), + initial_data: self.initial_data, meta: self.meta, set_as_current: self.set_as_current, index: self.index, section: self.section, - icon: None, - extra: self.extra, }) } } @@ -378,28 +339,26 @@ impl TryInto<CreateViewParams> for CreateOrphanViewPayloadPB { fn try_into(self) -> Result<CreateViewParams, Self::Error> { let name = ViewName::parse(self.name)?.0; - let view_id = Uuid::parse_str(&self.view_id).map_err(|_| ErrorCode::InvalidParams)?; + let parent_view_id = ViewIdentify::parse(self.view_id.clone())?.0; Ok(CreateViewParams { - parent_view_id: view_id, + parent_view_id, name, + desc: self.desc, layout: self.layout, - view_id, - initial_data: ViewData::Data(self.initial_data.into()), + view_id: self.view_id, + initial_data: self.initial_data, meta: Default::default(), set_as_current: false, index: None, section: None, - icon: None, - extra: None, }) } } -#[derive(Default, ProtoBuf, Validate, Clone, Debug)] +#[derive(Default, ProtoBuf, Clone, Debug)] pub struct ViewIdPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] pub value: String, } @@ -411,15 +370,6 @@ impl std::convert::From<&str> for ViewIdPB { } } -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct SetPublishNamePB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub new_name: String, -} - #[derive(Default, ProtoBuf, Clone, Debug)] pub struct DeletedViewPB { #[pb(index = 1)] @@ -572,9 +522,9 @@ impl TryInto<MoveViewParams> for MoveViewPayloadPB { #[derive(Debug)] pub struct MoveNestedViewParams { - pub view_id: Uuid, - pub new_parent_id: Uuid, - pub prev_view_id: Option<Uuid>, + pub view_id: String, + pub new_parent_id: String, + pub prev_view_id: Option<String>, pub from_section: Option<ViewSectionPB>, pub to_section: Option<ViewSectionPB>, } @@ -583,20 +533,9 @@ impl TryInto<MoveNestedViewParams> for MoveNestedViewPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result<MoveNestedViewParams, Self::Error> { - let view_id = Uuid::from_str(&ViewIdentify::parse(self.view_id)?.0) - .map_err(|_| ErrorCode::InvalidParams)?; - + let view_id = ViewIdentify::parse(self.view_id)?.0; let new_parent_id = ViewIdentify::parse(self.new_parent_id)?.0; - let new_parent_id = Uuid::from_str(&new_parent_id).map_err(|_| ErrorCode::InvalidParams)?; - - let prev_view_id = match self.prev_view_id { - Some(prev_view_id) => Some( - Uuid::from_str(&ViewIdentify::parse(prev_view_id)?.0) - .map_err(|_| ErrorCode::InvalidParams)?, - ), - None => None, - }; - + let prev_view_id = self.prev_view_id; Ok(MoveNestedViewParams { view_id, new_parent_id, @@ -627,62 +566,6 @@ pub struct UpdateViewVisibilityStatusPayloadPB { pub is_public: bool, } -#[derive(Default, ProtoBuf)] -pub struct DuplicateViewPayloadPB { - #[pb(index = 1)] - pub view_id: String, - - #[pb(index = 2)] - pub open_after_duplicate: bool, - - #[pb(index = 3)] - pub include_children: bool, - - // duplicate the view to the specified parent view. - // if the parent_view_id is None, the view will be duplicated to the same parent view. - #[pb(index = 4, one_of)] - pub parent_view_id: Option<String>, - - // The suffix of the duplicated view name. - // If the suffix is None, the duplicated view will have the same name with (copy) suffix. - #[pb(index = 5, one_of)] - pub suffix: Option<String>, - - #[pb(index = 6)] - pub sync_after_create: bool, -} - -#[derive(Debug)] -pub struct DuplicateViewParams { - pub view_id: String, - - pub open_after_duplicate: bool, - - pub include_children: bool, - - pub parent_view_id: Option<String>, - - pub suffix: Option<String>, - - pub sync_after_create: bool, -} - -impl TryInto<DuplicateViewParams> for DuplicateViewPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result<DuplicateViewParams, Self::Error> { - let view_id = ViewIdentify::parse(self.view_id)?.0; - Ok(DuplicateViewParams { - view_id, - open_after_duplicate: self.open_after_duplicate, - include_children: self.include_children, - parent_view_id: self.parent_view_id, - suffix: self.suffix, - sync_after_create: self.sync_after_create, - }) - } -} - // impl<'de> Deserialize<'de> for ViewDataType { // fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error> // where diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 72e50562f3..21ff046226 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -134,7 +134,7 @@ impl TryInto<GetWorkspaceViewParams> for GetWorkspaceViewPB { } #[derive(Default, ProtoBuf, Debug, Clone)] -pub struct WorkspaceLatestPB { +pub struct WorkspaceSettingPB { #[pb(index = 1)] pub workspace_id: String, diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 809651a262..888c26d2a6 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -1,9 +1,8 @@ -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use std::str::FromStr; use std::sync::{Arc, Weak}; use tracing::instrument; -use uuid::Uuid; + +use flowy_error::{FlowyError, FlowyResult}; +use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; use crate::manager::FolderManager; @@ -18,6 +17,28 @@ fn upgrade_folder( Ok(folder) } +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn create_workspace_handler( + data: AFPluginData<CreateWorkspacePayloadPB>, + folder: AFPluginState<Weak<FolderManager>>, +) -> DataResult<WorkspacePB, FlowyError> { + let folder = upgrade_folder(folder)?; + let params: CreateWorkspaceParams = data.into_inner().try_into()?; + let workspace = folder.create_workspace(params).await?; + let views = folder + .get_views_belong_to(&workspace.id) + .await? + .into_iter() + .map(|view| view_pb_without_child_views(view.as_ref().clone())) + .collect::<Vec<ViewPB>>(); + data_result_ok(WorkspacePB { + id: workspace.id, + name: workspace.name, + views, + create_time: workspace.created_at, + }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_all_workspace_handler( _data: AFPluginData<CreateWorkspacePayloadPB>, @@ -62,7 +83,7 @@ pub(crate) async fn read_private_views_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_current_workspace_setting_handler( folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<WorkspaceLatestPB, FlowyError> { +) -> DataResult<WorkspaceSettingPB, FlowyError> { let folder = upgrade_folder(folder)?; let setting = folder.get_workspace_setting_pb().await?; data_result_ok(setting) @@ -84,9 +105,9 @@ pub(crate) async fn create_view_handler( let folder = upgrade_folder(folder)?; let params: CreateViewParams = data.into_inner().try_into()?; let set_as_current = params.set_as_current; - let (view, _) = folder.create_view_with_params(params, true).await?; + let view = folder.create_view_with_params(params).await?; if set_as_current { - let _ = folder.set_current_view(view.id.clone()).await; + let _ = folder.set_current_view(&view.id).await; } data_result_ok(view_pb_without_child_views(view)) } @@ -100,7 +121,7 @@ pub(crate) async fn create_orphan_view_handler( let set_as_current = params.set_as_current; let view = folder.create_orphan_view_with_params(params).await?; if set_as_current { - let _ = folder.set_current_view(view.id.clone()).await; + let _ = folder.set_current_view(&view.id).await; } data_result_ok(view_pb_without_child_views(view)) } @@ -111,7 +132,7 @@ pub(crate) async fn get_view_handler( folder: AFPluginState<Weak<FolderManager>>, ) -> DataResult<ViewPB, FlowyError> { let folder = upgrade_folder(folder)?; - let view_id = data.try_into_inner()?; + let view_id: ViewIdPB = data.into_inner(); let view_pb = folder.get_view_pb(&view_id.value).await?; data_result_ok(view_pb) } @@ -205,7 +226,7 @@ pub(crate) async fn set_latest_view_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let view_id: ViewIdPB = data.into_inner(); - let _ = folder.set_current_view(view_id.value.clone()).await; + let _ = folder.set_current_view(&view_id.value).await; Ok(()) } @@ -245,14 +266,13 @@ pub(crate) async fn move_nested_view_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn duplicate_view_handler( - data: AFPluginData<DuplicateViewPayloadPB>, + data: AFPluginData<ViewPB>, folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<ViewPB, FlowyError> { +) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; - let params: DuplicateViewParams = data.into_inner().try_into()?; - - let view_pb = folder.duplicate_view(params).await?; - data_result_ok(view_pb) + let view: ViewPB = data.into_inner(); + folder.duplicate_view(&view.id).await?; + Ok(()) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -275,30 +295,20 @@ pub(crate) async fn read_favorites_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_recent_views_handler( - data: AFPluginData<ReadRecentViewsPB>, folder: AFPluginState<Weak<FolderManager>>, ) -> DataResult<RepeatedRecentViewPB, FlowyError> { let folder = upgrade_folder(folder)?; let recent_items = folder.get_my_recent_sections().await; - let start = data.start; - let limit = data.limit; - let ids = recent_items - .iter() - .rev() // the most recent view is at the end of the list - .map(|item| item.id.clone()) - .skip(start as usize) - .take(limit as usize) - .collect::<Vec<_>>(); - let views = folder.get_view_pbs_without_children(ids).await?; - let items = views - .into_iter() - .zip(recent_items.into_iter().rev()) - .map(|(view, item)| SectionViewPB { - item: view, - timestamp: item.timestamp, - }) - .collect::<Vec<_>>(); - data_result_ok(RepeatedRecentViewPB { items }) + let mut views = vec![]; + for item in recent_items { + if let Ok(view) = folder.get_view_pb(&item.id).await { + views.push(SectionViewPB { + item: view, + timestamp: item.timestamp, + }); + } + } + data_result_ok(RepeatedRecentViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -353,24 +363,14 @@ pub(crate) async fn delete_my_trash_handler( #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn import_data_handler( - data: AFPluginData<ImportPayloadPB>, + data: AFPluginData<ImportPB>, folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<RepeatedViewPB, FlowyError> { +) -> DataResult<ViewPB, FlowyError> { let folder = upgrade_folder(folder)?; let params: ImportParams = data.into_inner().try_into()?; - let views = folder.import(params).await?; - data_result_ok(views) -} - -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn import_zip_file_handler( - data: AFPluginData<ImportZipPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let data = data.try_into_inner()?; - folder.import_zip_file(&data.file_path).await?; - Ok(()) + let view = folder.import(params).await?; + let view_pb = view_pb_without_child_views(view); + data_result_ok(view_pb) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -391,153 +391,6 @@ pub(crate) async fn update_view_visibility_status_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params = data.into_inner(); - folder - .set_views_visibility(params.view_ids, params.is_public) - .await; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn publish_view_handler( - data: AFPluginData<PublishViewParamsPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let params = data.into_inner(); - let selected_view_ids = params.selected_view_ids.map(|ids| ids.items); - folder - .publish_view( - params.view_id.as_str(), - params.publish_name, - selected_view_ids, - ) - .await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn unpublish_views_handler( - data: AFPluginData<UnpublishViewsPayloadPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let params = data.into_inner(); - let view_ids = params - .view_ids - .into_iter() - .flat_map(|id| Uuid::from_str(&id).ok()) - .collect::<Vec<_>>(); - folder.unpublish_views(view_ids).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder))] -pub(crate) async fn get_publish_info_handler( - data: AFPluginData<ViewIdPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<PublishInfoResponsePB, FlowyError> { - let folder = upgrade_folder(folder)?; - let view_id = data.into_inner().value; - let view_id = Uuid::from_str(&view_id)?; - let info = folder.get_publish_info(&view_id).await?; - data_result_ok(PublishInfoResponsePB::from(info)) -} - -#[tracing::instrument(level = "debug", skip(data, folder))] -pub(crate) async fn set_publish_name_handler( - data: AFPluginData<SetPublishNamePB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let SetPublishNamePB { view_id, new_name } = data.into_inner(); - let view_id = Uuid::from_str(&view_id)?; - folder.set_publish_name(view_id, new_name).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn set_publish_namespace_handler( - data: AFPluginData<SetPublishNamespacePayloadPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let namespace = data.into_inner().new_namespace; - folder.set_publish_namespace(namespace).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(folder), err)] -pub(crate) async fn get_publish_namespace_handler( - folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<PublishNamespacePB, FlowyError> { - let folder = upgrade_folder(folder)?; - let namespace = folder.get_publish_namespace().await?; - data_result_ok(PublishNamespacePB { namespace }) -} - -#[tracing::instrument(level = "debug", skip(folder), err)] -pub(crate) async fn list_published_views_handler( - folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<RepeatedPublishInfoViewPB, FlowyError> { - let folder = upgrade_folder(folder)?; - let published_views = folder.list_published_views().await?; - let items: Vec<PublishInfoViewPB> = published_views - .into_iter() - .map(|view| view.into()) - .collect(); - data_result_ok(RepeatedPublishInfoViewPB { items }) -} - -#[tracing::instrument(level = "debug", skip(folder))] -pub(crate) async fn get_default_publish_info_handler( - folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<PublishInfoResponsePB, FlowyError> { - let folder = upgrade_folder(folder)?; - let default_published_view = folder.get_default_published_view_info().await?; - data_result_ok(default_published_view.into()) -} - -#[tracing::instrument(level = "debug", skip(folder))] -pub(crate) async fn set_default_publish_view_handler( - data: AFPluginData<ViewIdPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let view_id: uuid::Uuid = data.into_inner().value.parse().map_err(|err| { - tracing::error!("Failed to parse view id: {}", err); - FlowyError::invalid_data() - })?; - folder.set_default_published_view(view_id).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(folder))] -pub(crate) async fn remove_default_publish_view_handler( - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - folder.remove_default_published_view().await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder))] -pub(crate) async fn lock_view_handler( - data: AFPluginData<ViewIdPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let view_id = data.into_inner().value; - folder.lock_view(&view_id).await?; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip(data, folder))] -pub(crate) async fn unlock_view_handler( - data: AFPluginData<ViewIdPB>, - folder: AFPluginState<Weak<FolderManager>>, -) -> Result<(), FlowyError> { - let folder = upgrade_folder(folder)?; - let view_id = data.into_inner().value; - folder.unlock_view(&view_id).await?; + folder.set_views_visibility(params.view_ids, params.is_public); Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index c857353c4b..febfc49b5e 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -11,6 +11,7 @@ use crate::manager::FolderManager; pub fn init(folder: Weak<FolderManager>) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace + .event(FolderEvent::CreateFolderWorkspace, create_workspace_handler) .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) @@ -31,7 +32,6 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin { .event(FolderEvent::RecoverAllTrashItems, restore_all_trash_handler) .event(FolderEvent::PermanentlyDeleteAllTrashItem, delete_my_trash_handler) .event(FolderEvent::ImportData, import_data_handler) - .event(FolderEvent::ImportZipFile, import_zip_file_handler) .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) .event(FolderEvent::ReadFavorites, read_favorites_handler) @@ -42,28 +42,17 @@ pub fn init(folder: Weak<FolderManager>) -> AFPlugin { .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) .event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler) .event(FolderEvent::GetViewAncestors, get_view_ancestors_handler) - .event(FolderEvent::PublishView, publish_view_handler) - .event(FolderEvent::GetPublishInfo, get_publish_info_handler) - .event(FolderEvent::SetPublishName, set_publish_name_handler) - .event(FolderEvent::UnpublishViews, unpublish_views_handler) - .event(FolderEvent::SetPublishNamespace, set_publish_namespace_handler) - .event(FolderEvent::GetPublishNamespace, get_publish_namespace_handler) - .event(FolderEvent::ListPublishedViews, list_published_views_handler) - .event(FolderEvent::GetDefaultPublishInfo, get_default_publish_info_handler) - .event(FolderEvent::SetDefaultPublishView, set_default_publish_view_handler) - .event(FolderEvent::RemoveDefaultPublishView, remove_default_publish_view_handler) - .event(FolderEvent::LockView, lock_view_handler) - .event(FolderEvent::UnlockView, unlock_view_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum FolderEvent { - /// Deprecated: Create a new workspace + /// Create a new workspace + #[event(input = "CreateWorkspacePayloadPB", output = "WorkspacePB")] CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace - #[event(output = "WorkspaceLatestPB")] + #[event(output = "WorkspaceSettingPB")] GetCurrentWorkspaceSetting = 1, /// Return a list of workspaces that the current user can access. @@ -96,7 +85,7 @@ pub enum FolderEvent { DeleteView = 13, /// Duplicate the view - #[event(input = "DuplicateViewPayloadPB", output = "ViewPB")] + #[event(input = "ViewPB")] DuplicateView = 14, /// Close and release the resources that are used by this view. @@ -143,7 +132,7 @@ pub enum FolderEvent { #[event()] PermanentlyDeleteAllTrashItem = 27, - #[event(input = "ImportPayloadPB", output = "RepeatedViewPB")] + #[event(input = "ImportPB", output = "ViewPB")] ImportData = 30, #[event(input = "WorkspaceIdPB", output = "RepeatedFolderSnapshotPB")] @@ -166,7 +155,7 @@ pub enum FolderEvent { #[event(input = "UpdateViewIconPayloadPB")] UpdateViewIcon = 35, - #[event(input = "ReadRecentViewsPB", output = "RepeatedRecentViewPB")] + #[event(output = "RepeatedRecentViewPB")] ReadRecentViews = 36, // used for add or remove recent views, like history @@ -187,43 +176,4 @@ pub enum FolderEvent { /// Return the ancestors of the view #[event(input = "ViewIdPB", output = "RepeatedViewPB")] GetViewAncestors = 42, - - #[event(input = "PublishViewParamsPB")] - PublishView = 43, - - #[event(input = "ViewIdPB", output = "PublishInfoResponsePB")] - GetPublishInfo = 44, - - #[event(output = "PublishNamespacePB")] - GetPublishNamespace = 45, - - #[event(input = "SetPublishNamespacePayloadPB")] - SetPublishNamespace = 46, - - #[event(input = "UnpublishViewsPayloadPB")] - UnpublishViews = 47, - - #[event(input = "ImportZipPB")] - ImportZipFile = 48, - - #[event(output = "RepeatedPublishInfoViewPB")] - ListPublishedViews = 49, - - #[event(output = "PublishInfoResponsePB")] - GetDefaultPublishInfo = 50, - - #[event(input = "ViewIdPB")] - SetDefaultPublishView = 51, - - #[event(input = "SetPublishNamePB")] - SetPublishName = 52, - - #[event()] - RemoveDefaultPublishView = 53, - - #[event(input = "ViewIdPB")] - LockView = 54, - - #[event(input = "ViewIdPB")] - UnlockView = 55, } diff --git a/frontend/rust-lib/flowy-folder/src/lib.rs b/frontend/rust-lib/flowy-folder/src/lib.rs index d08b94dbfa..bc927d20c7 100644 --- a/frontend/rust-lib/flowy-folder/src/lib.rs +++ b/frontend/rust-lib/flowy-folder/src/lib.rs @@ -11,7 +11,10 @@ pub mod view_operation; mod manager_init; mod manager_observer; +#[cfg(debug_assertions)] +pub mod manager_test_util; -pub mod publish_util; pub mod share; +#[cfg(feature = "test_helper")] +mod test_helper; mod util; diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 37533ae500..fc39940569 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,86 +1,74 @@ use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, view_pb_without_child_views_from_arc, - CreateViewParams, DeletedViewPB, DuplicateViewParams, FolderSnapshotPB, MoveNestedViewParams, - RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewLayoutPB, ViewPB, - ViewSectionPB, WorkspaceLatestPB, WorkspacePB, + CreateViewParams, CreateWorkspaceParams, DeletedViewPB, FolderSnapshotPB, MoveNestedViewParams, + RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, UpdateViewParams, ViewPB, ViewSectionPB, + WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, ChildViewChangeReason, }; use crate::notification::{ - folder_notification_builder, send_current_workspace_notification, FolderNotification, + send_notification, send_workspace_setting_notification, FolderNotification, }; -use crate::publish_util::{generate_publish_name, view_pb_to_publish_view}; -use crate::share::{ImportData, ImportItem, ImportParams}; -use crate::util::{folder_not_init_error, workspace_data_not_sync_error}; -use crate::view_operation::{ - create_view, FolderOperationHandler, FolderOperationHandlers, GatherEncodedCollab, ViewData, +use crate::share::ImportParams; +use crate::util::{ + folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, }; -use arc_swap::ArcSwapOption; -use client_api::entity::workspace_dto::PublishInfoView; -use client_api::entity::PublishInfo; -use collab::core::collab::DataSource; -use collab::lock::RwLock; -use collab_entity::{CollabType, EncodedCollab}; -use collab_folder::hierarchy_builder::{ParentChildViews, ViewExtraBuilder}; +use crate::view_operation::{create_view, FolderOperationHandler, FolderOperationHandlers}; +use collab::core::collab::{DataSource, MutexCollab}; +use collab_entity::CollabType; +use collab_folder::error::FolderError; use collab_folder::{ - Folder, FolderData, FolderNotify, Section, SectionItem, TrashInfo, View, ViewLayout, ViewUpdate, + Folder, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, ViewUpdate, Workspace, }; -use collab_integrate::collab_builder::{ - AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, -}; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use collab_integrate::CollabKVDB; -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService, FolderCollabParams}; -use flowy_folder_pub::entities::{ - PublishDatabaseData, PublishDatabasePayload, PublishDocumentPayload, PublishPayload, - PublishViewInfo, PublishViewMeta, PublishViewMetaData, -}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService}; +use flowy_folder_pub::folder_builder::ParentChildViews; use flowy_search_pub::entities::FolderIndexManager; -use flowy_sqlite::kv::KVStorePreferences; -use futures::future; -use std::collections::HashMap; +use flowy_sqlite::kv::StorePreferences; +use parking_lot::RwLock; use std::fmt::{Display, Formatter}; -use std::str::FromStr; +use std::ops::Deref; use std::sync::{Arc, Weak}; -use tokio::sync::RwLockWriteGuard; use tracing::{error, info, instrument}; -use uuid::Uuid; pub trait FolderUser: Send + Sync { fn user_id(&self) -> Result<i64, FlowyError>; - fn workspace_id(&self) -> Result<Uuid, FlowyError>; + fn workspace_id(&self) -> Result<String, FlowyError>; fn collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>; - - fn is_folder_exist_on_disk(&self, uid: i64, workspace_id: &Uuid) -> FlowyResult<bool>; } pub struct FolderManager { - pub(crate) mutex_folder: ArcSwapOption<RwLock<Folder>>, + /// MutexFolder is the folder that is used to store the data. + pub(crate) mutex_folder: Arc<MutexFolder>, pub(crate) collab_builder: Arc<AppFlowyCollabBuilder>, pub(crate) user: Arc<dyn FolderUser>, pub(crate) operation_handlers: FolderOperationHandlers, pub cloud_service: Arc<dyn FolderCloudService>, pub(crate) folder_indexer: Arc<dyn FolderIndexManager>, - pub(crate) store_preferences: Arc<KVStorePreferences>, + pub(crate) store_preferences: Arc<StorePreferences>, } impl FolderManager { pub fn new( user: Arc<dyn FolderUser>, collab_builder: Arc<AppFlowyCollabBuilder>, + operation_handlers: FolderOperationHandlers, cloud_service: Arc<dyn FolderCloudService>, folder_indexer: Arc<dyn FolderIndexManager>, - store_preferences: Arc<KVStorePreferences>, + store_preferences: Arc<StorePreferences>, ) -> FlowyResult<Self> { + let mutex_folder = Arc::new(MutexFolder::default()); let manager = Self { user, - mutex_folder: Default::default(), + mutex_folder, collab_builder, - operation_handlers: Default::default(), + operation_handlers, cloud_service, folder_indexer, store_preferences, @@ -89,61 +77,27 @@ impl FolderManager { Ok(manager) } - pub fn register_operation_handler( - &self, - layout: ViewLayout, - handler: Arc<dyn FolderOperationHandler + Send + Sync>, - ) { - self.operation_handlers.insert(layout, handler); - } - #[instrument(level = "debug", skip(self), err)] pub async fn get_current_workspace(&self) -> FlowyResult<WorkspacePB> { let workspace_id = self.user.workspace_id()?; - match self.mutex_folder.load_full() { - None => { + self.with_folder( + || { let uid = self.user.user_id()?; Err(workspace_data_not_sync_error(uid, &workspace_id)) }, - Some(lock) => { - let folder = lock.read().await; + |folder| { let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { let views = get_workspace_public_view_pbs(&workspace_id, folder); let workspace: WorkspacePB = (workspace, views).into(); Ok::<WorkspacePB, FlowyError>(workspace) }; - match folder.get_workspace_info(&workspace_id.to_string()) { + match folder.get_workspace_info(&workspace_id) { None => Err(FlowyError::record_not_found().with_context("Can not find the workspace")), - Some(workspace) => workspace_pb_from_workspace(workspace, &folder), + Some(workspace) => workspace_pb_from_workspace(workspace, folder), } }, - } - } - - pub async fn get_folder_data(&self) -> FlowyResult<FolderData> { - let workspace_id = self.user.workspace_id()?; - let data = self - .mutex_folder - .load_full() - .ok_or_else(|| internal_error("The folder is not initialized"))? - .read() - .await - .get_folder_data(&workspace_id.to_string()) - .ok_or_else(|| internal_error("Workspace id not match the id in current folder"))?; - Ok(data) - } - - pub async fn gather_publish_encode_collab( - &self, - view_id: &Uuid, - layout: &ViewLayout, - ) -> FlowyResult<GatherEncodedCollab> { - let handler = self.get_handler(layout)?; - let encoded_collab = handler - .gather_publish_encode_collab(&self.user, view_id) - .await?; - Ok(encoded_collab) + ) } /// Return a list of views of the current workspace. @@ -155,127 +109,119 @@ impl FolderManager { pub async fn get_workspace_public_views(&self) -> FlowyResult<Vec<ViewPB>> { let workspace_id = self.user.workspace_id()?; - match self.mutex_folder.load_full() { - None => Ok(Vec::default()), - Some(lock) => { - let folder = lock.read().await; - Ok(get_workspace_public_view_pbs(&workspace_id, &folder)) - }, - } + Ok(self.with_folder(Vec::new, |folder| { + get_workspace_public_view_pbs(&workspace_id, folder) + })) } pub async fn get_workspace_private_views(&self) -> FlowyResult<Vec<ViewPB>> { let workspace_id = self.user.workspace_id()?; - match self.mutex_folder.load_full() { - None => Ok(Vec::default()), - Some(folder) => { - let folder = folder.read().await; - Ok(get_workspace_private_view_pbs(&workspace_id, &folder)) - }, - } + Ok(self.with_folder(Vec::new, |folder| { + get_workspace_private_view_pbs(&workspace_id, folder) + })) } #[instrument(level = "trace", skip_all, err)] pub(crate) async fn make_folder<T: Into<Option<FolderNotify>>>( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak<CollabKVDB>, - data_source: Option<DataSource>, + doc_state: DataSource, folder_notifier: T, - ) -> Result<Arc<RwLock<Folder>>, FlowyError> { + ) -> Result<Folder, FlowyError> { let folder_notifier = folder_notifier.into(); // only need the check the workspace id when the doc state is not from the disk. - let config = CollabBuilderConfig::default().sync_enable(true); - - let data_source = data_source.unwrap_or_else(|| { - CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source() - }); + let should_check_workspace_id = !matches!(doc_state, DataSource::Disk); + let should_auto_initialize = !should_check_workspace_id; + let config = CollabBuilderConfig::default() + .sync_enable(true) + .auto_initialize(should_auto_initialize); let object_id = workspace_id; - let collab_object = - self - .collab_builder - .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; - let result = self - .collab_builder - .create_folder( - collab_object, - data_source, - collab_db, - config, - folder_notifier, - None, - ) - .await; + let collab = self.collab_builder.build_with_config( + workspace_id, + uid, + object_id, + CollabType::Folder, + collab_db, + doc_state, + config, + )?; + let (should_clear, err) = match Folder::open(UserId::from(uid), collab.clone(), folder_notifier) + { + Ok(folder) => { + if should_check_workspace_id { + // check the workspace id in the folder is matched with the workspace id. Just in case the folder + // is overwritten by another workspace. + let folder_workspace_id = folder.get_workspace_id(); + if folder_workspace_id != workspace_id { + error!( + "expect workspace_id: {}, actual workspace_id: {}", + workspace_id, folder_workspace_id + ); + return Err(FlowyError::workspace_data_not_match()); + } + // Initialize the folder manually + collab.lock().initialize(); + } + return Ok(folder); + }, + Err(err) => (matches!(err, FolderError::NoRequiredData(_)), err), + }; // If opening the folder fails due to missing required data (indicated by a `FolderError::NoRequiredData`), // the function logs an informational message and attempts to clear the folder data by deleting its // document from the collaborative database. It then returns the encountered error. - match result { - Ok(folder) => Ok(folder), - Err(err) => { - info!( - "Clear the folder data and try to open the folder again due to: {}", - err - ); - - if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { - let _ = db - .delete_doc(uid, &workspace_id.to_string(), &object_id.to_string()) - .await; - } - Err(err.into()) - }, + if should_clear { + info!("Clear the folder data and try to open the folder again"); + if let Some(db) = self.user.collab_db(uid).ok().and_then(|a| a.upgrade()) { + let _ = db.delete_doc(uid, workspace_id).await; + } } + Err(err.into()) } - pub(crate) async fn create_folder_with_data( + pub(crate) async fn create_empty_collab( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak<CollabKVDB>, - notifier: Option<FolderNotify>, - folder_data: Option<FolderData>, - ) -> Result<Arc<RwLock<Folder>>, FlowyError> { + ) -> Result<Arc<MutexCollab>, FlowyError> { let object_id = workspace_id; - let collab_object = - self - .collab_builder - .collab_object(workspace_id, uid, object_id, CollabType::Folder)?; - - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), uid, *workspace_id).into_data_source(); - let folder = self - .collab_builder - .create_folder( - collab_object, - doc_state, - collab_db, - CollabBuilderConfig::default().sync_enable(true), - notifier, - folder_data, - ) - .await?; - Ok(folder) + let collab = self.collab_builder.build_with_config( + workspace_id, + uid, + object_id, + CollabType::Folder, + collab_db, + DataSource::Disk, + CollabBuilderConfig::default().sync_enable(true), + )?; + Ok(collab) } /// Initialize the folder with the given workspace id. /// Fetch the folder updates from the cloud service and initialize the folder. - #[tracing::instrument(skip_all, err)] - pub async fn initialize_after_sign_in( - &self, - user_id: i64, - data_source: FolderInitDataSource, - ) -> FlowyResult<()> { + #[tracing::instrument(skip(self, user_id), err)] + pub async fn initialize_with_workspace_id(&self, user_id: i64) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; - if let Err(err) = self.initialize(user_id, &workspace_id, data_source).await { + let object_id = &workspace_id; + let folder_doc_state = self + .cloud_service + .get_folder_doc_state(&workspace_id, user_id, CollabType::Folder, object_id) + .await?; + if let Err(err) = self + .initialize( + user_id, + &workspace_id, + FolderInitDataSource::Cloud(folder_doc_state), + ) + .await + { // If failed to open folder with remote data, open from local disk. After open from the local // disk. the data will be synced to the remote server. - error!( - "initialize folder for user {} with workspace {} encountered error: {:?}, fallback local", - user_id, workspace_id, err - ); + error!("initialize folder with error {:?}, fallback local", err); self .initialize( user_id, @@ -286,28 +232,19 @@ impl FolderManager { ) .await?; } - Ok(()) } - pub async fn initialize_after_open_workspace( - &self, - uid: i64, - data_source: FolderInitDataSource, - ) -> FlowyResult<()> { - self.initialize_after_sign_in(uid, data_source).await - } - /// Initialize the folder for the new user. /// Using the [DefaultFolderBuilder] to create the default workspace for the new user. #[instrument(level = "info", skip_all, err)] - pub async fn initialize_after_sign_up( + pub async fn initialize_with_new_user( &self, user_id: i64, _token: &str, is_new: bool, data_source: FolderInitDataSource, - workspace_id: &Uuid, + workspace_id: &str, ) -> FlowyResult<()> { // Create the default workspace if the user is new info!("initialize_when_sign_up: is_new: {}", is_new); @@ -353,136 +290,59 @@ impl FolderManager { /// pub async fn clear(&self, _user_id: i64) {} - pub async fn get_workspace_setting_pb(&self) -> FlowyResult<WorkspaceLatestPB> { + #[tracing::instrument(level = "info", skip_all, err)] + pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult<Workspace> { + let uid = self.user.user_id()?; + let new_workspace = self + .cloud_service + .create_workspace(uid, ¶ms.name) + .await?; + Ok(new_workspace) + } + + pub async fn get_workspace_setting_pb(&self) -> FlowyResult<WorkspaceSettingPB> { let workspace_id = self.user.workspace_id()?; let latest_view = self.get_current_view().await; - Ok(WorkspaceLatestPB { - workspace_id: workspace_id.to_string(), + Ok(WorkspaceSettingPB { + workspace_id, latest_view, }) } - /// All the views will become a space under the workspace. - pub async fn insert_views_as_spaces( + pub async fn insert_parent_child_views( &self, - mut views: Vec<ParentChildViews>, - orphan_views: Vec<ParentChildViews>, + views: Vec<ParentChildViews>, ) -> Result<(), FlowyError> { - let lock = self - .mutex_folder - .load_full() - .ok_or_else(|| FlowyError::internal().with_context("The folder is not initialized"))?; - let mut folder = lock.write().await; - let workspace_id = folder - .get_workspace_id() - .ok_or_else(|| FlowyError::internal().with_context("Cannot find the workspace ID"))?; - - views.iter_mut().for_each(|view| { - view.view.parent_view_id.clone_from(&workspace_id); - view.view.extra = - Some(serde_json::to_string(&ViewExtraBuilder::new().is_space(true).build()).unwrap()); - }); - let all_views = views.into_iter().chain(orphan_views.into_iter()).collect(); - folder.insert_nested_views(all_views); - - Ok(()) - } - - /// Inserts parent-child views into the folder. If a `parent_view_id` is provided, - /// it will be used to set the `parent_view_id` for all child views. If not, the latest - /// view (by `last_edited_time`) from the workspace will be used as the parent view. - /// - #[instrument(level = "info", skip_all, err)] - pub async fn insert_views_with_parent( - &self, - mut views: Vec<ParentChildViews>, - orphan_views: Vec<ParentChildViews>, - parent_view_id: Option<String>, - ) -> Result<(), FlowyError> { - let lock = self - .mutex_folder - .load_full() - .ok_or_else(|| FlowyError::internal().with_context("The folder is not initialized"))?; - - // Obtain a write lock on the folder. - let mut folder = lock.write().await; - let parent_view_id = parent_view_id.as_deref().filter(|id| !id.is_empty()); - // Set the parent view ID for the child views. - if let Some(parent_view_id) = parent_view_id { - // If a valid parent_view_id is provided, set it for each child view. - if folder.get_view(parent_view_id).is_some() { - info!( - "[AppFlowyData]: Attach parent-child views with the latest view: {:?}", - parent_view_id - ); - views.iter_mut().for_each(|child_view| { - child_view.view.parent_view_id = parent_view_id.to_string(); - }); - } else { - error!( - "[AppFlowyData]: The provided parent_view_id: {} is not found in the folder", - parent_view_id - ); - Self::insert_into_latest_view(&mut views, &mut folder)?; - } - } else { - // If no parent_view_id is provided, find the latest view in the workspace. - Self::insert_into_latest_view(&mut views, &mut folder)?; - } - - // Insert the views into the folder. - let all_views = views.into_iter().chain(orphan_views.into_iter()).collect(); - folder.insert_nested_views(all_views); - Ok(()) - } - - #[instrument(level = "info", skip_all, err)] - fn insert_into_latest_view( - views: &mut [ParentChildViews], - folder: &mut RwLockWriteGuard<Folder>, - ) -> Result<(), FlowyError> { - let workspace_id = folder - .get_workspace_id() - .ok_or_else(|| FlowyError::internal().with_context("Cannot find the workspace ID"))?; - - // Get the latest view based on the last_edited_time in the workspace. - match folder - .get_views_belong_to(&workspace_id) - .iter() - .max_by_key(|view| view.last_edited_time) - { - None => info!("[AppFlowyData]: No views found in the workspace"), - Some(latest_view) => { - info!( - "[AppFlowyData]: Attach parent-child views with the latest view: {}:{}, is_space: {:?}", - latest_view.id, - latest_view.name, - latest_view.space_info(), - ); - views.iter_mut().for_each(|child_view| { - child_view.view.parent_view_id.clone_from(&latest_view.id); - }); + self.with_folder( + || Err(FlowyError::internal().with_context("The folder is not initialized")), + |folder| { + for view in views { + insert_parent_child_views(folder, view); + } + Ok(()) }, - } + )?; + Ok(()) } pub async fn get_workspace_pb(&self) -> FlowyResult<WorkspacePB> { let workspace_id = self.user.workspace_id()?; - let lock = self - .mutex_folder - .load_full() - .ok_or_else(|| FlowyError::internal().with_context("folder is not initialized"))?; - let folder = lock.read().await; + let guard = self.mutex_folder.read(); + let folder = guard + .as_ref() + .ok_or(FlowyError::internal().with_context("folder is not initialized"))?; let workspace = folder - .get_workspace_info(&workspace_id.to_string()) + .get_workspace_info(&workspace_id) .ok_or_else(|| FlowyError::record_not_found().with_context("Can not find the workspace"))?; let views = folder + .views .get_views_belong_to(&workspace.id) .into_iter() .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect::<Vec<ViewPB>>(); + drop(guard); Ok(WorkspacePB { id: workspace.id, @@ -492,42 +352,48 @@ impl FolderManager { }) } - /// Asynchronously creates a view with provided parameters and notifies the workspace if update is needed. + /// This function acquires a lock on the `mutex_folder` and checks its state. + /// If the folder is `None`, it invokes the `none_callback`, otherwise, it passes the folder to the `f2` callback. /// - /// Commonly, the notify_workspace_update parameter is set to true when the view is created in the workspace. - /// If you're handling multiple views in the same hierarchy and want to notify the workspace only after the last view is created, - /// you can set notify_workspace_update to false to avoid multiple notifications. - pub async fn create_view_with_params( - &self, - params: CreateViewParams, - notify_workspace_update: bool, - ) -> FlowyResult<(View, Option<EncodedCollab>)> { + /// # Parameters + /// + /// * `none_callback`: A callback function that is invoked when `mutex_folder` contains `None`. + /// * `f2`: A callback function that is invoked when `mutex_folder` contains a `Some` value. The contained folder is passed as an argument to this callback. + fn with_folder<F1, F2, Output>(&self, none_callback: F1, f2: F2) -> Output + where + F1: FnOnce() -> Output, + F2: FnOnce(&Folder) -> Output, + { + let folder = self.mutex_folder.read(); + match &*folder { + None => none_callback(), + Some(folder) => f2(folder), + } + } + + pub async fn create_view_with_params(&self, params: CreateViewParams) -> FlowyResult<View> { let workspace_id = self.user.workspace_id()?; let view_layout: ViewLayout = params.layout.clone().into(); let handler = self.get_handler(&view_layout)?; let user_id = self.user.user_id()?; - let mut encoded_collab: Option<EncodedCollab> = None; + let meta = params.meta.clone(); - info!( - "{} create view {}, name:{}, layout:{:?}", - handler.name(), - params.view_id, - params.name, - params.layout - ); - if params.meta.is_empty() && params.initial_data.is_empty() { + if meta.is_empty() && params.initial_data.is_empty() { + tracing::trace!("Create view with build-in data"); handler - .create_default_view( - user_id, - ¶ms.parent_view_id, - ¶ms.view_id, - ¶ms.name, - view_layout.clone(), - ) + .create_built_in_view(user_id, ¶ms.view_id, ¶ms.name, view_layout.clone()) .await?; } else { - encoded_collab = handler - .create_view_with_view_data(user_id, params.clone()) + tracing::trace!("Create view with view data"); + handler + .create_view_with_view_data( + user_id, + ¶ms.view_id, + ¶ms.name, + params.initial_data.clone(), + view_layout.clone(), + meta, + ) .await?; } @@ -535,18 +401,22 @@ impl FolderManager { let section = params.section.clone().unwrap_or(ViewSectionPB::Public); let is_private = section == ViewSectionPB::Private; let view = create_view(self.user.user_id()?, params, view_layout); - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.insert_view(view.clone(), index); - if is_private { - folder.add_private_view_ids(vec![view.id.clone()]); - } - if notify_workspace_update { - notify_did_update_workspace(&workspace_id, &folder); - } + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), index); + if is_private { + folder.add_private_view_ids(vec![view.id.clone()]); + } + }, + ); + + let folder = &self.mutex_folder.read(); + if let Some(folder) = folder.as_ref() { + notify_did_update_workspace(&workspace_id, folder); } - Ok((view, encoded_collab)) + Ok(view) } /// The orphan view is meant to be a view that is not attached to any parent view. By default, this @@ -561,35 +431,24 @@ impl FolderManager { let handler = self.get_handler(&view_layout)?; let user_id = self.user.user_id()?; handler - .create_default_view( - user_id, - ¶ms.parent_view_id, - ¶ms.view_id, - ¶ms.name, - view_layout.clone(), - ) + .create_built_in_view(user_id, ¶ms.view_id, ¶ms.name, view_layout.clone()) .await?; let view = create_view(self.user.user_id()?, params, view_layout); - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.insert_view(view.clone(), None); - } + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), None); + }, + ); Ok(view) } #[tracing::instrument(level = "debug", skip(self), err)] pub(crate) async fn close_view(&self, view_id: &str) -> Result<(), FlowyError> { - if let Some(lock) = self.mutex_folder.load_full() { - let folder = lock.read().await; - if let Some(view) = folder.get_view(view_id) { - // Drop the folder lock explicitly to avoid deadlock when following calls contains 'self' - drop(folder); - - let view_id = Uuid::from_str(view_id)?; - let handler = self.get_handler(&view.layout)?; - handler.close_view(&view_id).await?; - } + if let Some(view) = self.with_folder(|| None, |folder| folder.views.get_view(view_id)) { + let handler = self.get_handler(&view.layout)?; + handler.close_view(view_id).await?; } Ok(()) } @@ -605,14 +464,11 @@ impl FolderManager { pub async fn get_view_pb(&self, view_id: &str) -> FlowyResult<ViewPB> { let view_id = view_id.to_string(); - let lock = self - .mutex_folder - .load_full() - .ok_or_else(folder_not_init_error)?; - let folder = lock.read().await; + let folder = self.mutex_folder.read(); + let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; // trash views and other private views should not be accessed - let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); + let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); if view_ids_should_be_filtered.contains(&view_id) { return Err(FlowyError::new( @@ -621,13 +477,14 @@ impl FolderManager { )); } - match folder.get_view(&view_id) { + match folder.views.get_view(&view_id) { None => { error!("Can't find the view with id: {}", view_id); Err(FlowyError::record_not_found()) }, Some(view) => { let child_views = folder + .views .get_views_belong_to(&view.id) .into_iter() .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) @@ -638,41 +495,6 @@ impl FolderManager { } } - /// Retrieves the views corresponding to the specified view IDs. - /// - /// It is important to note that if the target view contains child views, - /// this method only provides access to the first level of child views. - /// - /// Therefore, to access a nested child view within one of the initial child views, you must invoke this method - /// again using the ID of the child view you wish to access. - #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_view_pbs_without_children( - &self, - view_ids: Vec<String>, - ) -> FlowyResult<Vec<ViewPB>> { - let lock = self - .mutex_folder - .load_full() - .ok_or_else(folder_not_init_error)?; - - // trash views and other private views should not be accessed - let folder = lock.read().await; - let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); - - let views = view_ids - .into_iter() - .filter_map(|view_id| { - if view_ids_should_be_filtered.contains(&view_id) { - return None; - } - folder.get_view(&view_id) - }) - .map(view_pb_without_child_views_from_arc) - .collect::<Vec<_>>(); - - Ok(views) - } - /// Retrieves all views. /// /// It is important to note that this will return a flat map of all views, @@ -681,16 +503,13 @@ impl FolderManager { /// #[tracing::instrument(level = "debug", skip(self))] pub async fn get_all_views_pb(&self) -> FlowyResult<Vec<ViewPB>> { - let lock = self - .mutex_folder - .load_full() - .ok_or_else(folder_not_init_error)?; + let folder = self.mutex_folder.read(); + let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; // trash views and other private views should not be accessed - let folder = lock.read().await; - let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); + let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); - let all_views = folder.get_all_views(); + let all_views = folder.views.get_all_views(); let views = all_views .into_iter() .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) @@ -712,18 +531,17 @@ impl FolderManager { pub async fn get_view_ancestors_pb(&self, view_id: &str) -> FlowyResult<Vec<ViewPB>> { let mut ancestors = vec![]; let mut parent_view_id = view_id.to_string(); - if let Some(lock) = self.mutex_folder.load_full() { - let folder = lock.read().await; - while let Some(view) = folder.get_view(&parent_view_id) { - // If the view is already in the ancestors list, then break the loop - if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { - break; - } - ancestors.push(view_pb_without_child_views(view.as_ref().clone())); - parent_view_id.clone_from(&view.parent_view_id); + while let Some(view) = + self.with_folder(|| None, |folder| folder.views.get_view(&parent_view_id)) + { + // If the view is already in the ancestors list, then break the loop + if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { + break; } - ancestors.reverse(); + ancestors.push(view_pb_without_child_views(view.as_ref().clone())); + parent_view_id = view.parent_view_id.clone(); } + ancestors.reverse(); Ok(ancestors) } @@ -732,52 +550,34 @@ impl FolderManager { /// All the favorite views being trashed will be unfavorited first to remove it from favorites list as well. The process of unfavoriting concerned view is handled by `unfavorite_view_and_decendants()` #[tracing::instrument(level = "debug", skip(self), err)] pub async fn move_view_to_trash(&self, view_id: &str) -> FlowyResult<()> { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - // Check if the view is already in trash, if not we can move the same - // view to trash multiple times (duplicates) - let trash_info = folder.get_my_trash_info(); - if trash_info.into_iter().any(|info| info.id == view_id) { - return Err(FlowyError::new( - ErrorCode::Internal, - format!( - "Can't move the view({}) to trash, it is already in trash", - view_id - ), - )); - } + self.with_folder( + || (), + |folder| { + if let Some(view) = folder.views.get_view(view_id) { + self.unfavorite_view_and_decendants(view.clone(), folder); + folder.add_trash_view_ids(vec![view_id.to_string()]); + // notify the parent view that the view is moved to trash + send_notification(view_id, FolderNotification::DidMoveViewToTrash) + .payload(DeletedViewPB { + view_id: view_id.to_string(), + index: None, + }) + .send(); - if let Some(view) = folder.get_view(view_id) { - // if the view is locked, the view can't be moved to trash - if view.is_locked.unwrap_or(false) { - return Err(FlowyError::view_is_locked()); + notify_child_views_changed( + view_pb_without_child_views(view.as_ref().clone()), + ChildViewChangeReason::Delete, + ); } - - Self::unfavorite_view_and_decendants(view.clone(), &mut folder); - folder.add_trash_view_ids(vec![view_id.to_string()]); - drop(folder); - - // notify the parent view that the view is moved to trash - folder_notification_builder(view_id, FolderNotification::DidMoveViewToTrash) - .payload(DeletedViewPB { - view_id: view_id.to_string(), - index: None, - }) - .send(); - - notify_child_views_changed( - view_pb_without_child_views(view.as_ref().clone()), - ChildViewChangeReason::Delete, - ); - } - } + }, + ); Ok(()) } - fn unfavorite_view_and_decendants(view: Arc<View>, folder: &mut Folder) { + fn unfavorite_view_and_decendants(&self, view: Arc<View>, folder: &Folder) { let mut all_descendant_views: Vec<Arc<View>> = vec![view.clone()]; - all_descendant_views.extend(folder.get_views_belong_to(&view.id)); + all_descendant_views.extend(folder.views.get_views_belong_to(&view.id)); let favorite_descendant_views: Vec<ViewPB> = all_descendant_views .iter() @@ -792,7 +592,7 @@ impl FolderManager { .map(|v| v.id.clone()) .collect(), ); - folder_notification_builder("favorite", FolderNotification::DidUnfavoriteView) + send_notification("favorite", FolderNotification::DidUnfavoriteView) .payload(RepeatedViewPB { items: favorite_descendant_views, }) @@ -825,29 +625,27 @@ impl FolderManager { let prev_view_id = params.prev_view_id; let from_section = params.from_section; let to_section = params.to_section; - let view = self.get_view_pb(&view_id.to_string()).await?; - // if the view is locked, the view can't be moved - if view.is_locked.unwrap_or(false) { - return Err(FlowyError::view_is_locked()); - } + let view = self.get_view_pb(&view_id).await?; + let old_parent_id = view.parent_view_id; + self.with_folder( + || (), + |folder| { + folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); - let old_parent_id = Uuid::from_str(&view.parent_view_id)?; - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.move_nested_view( - &view_id.to_string(), - &new_parent_id.to_string(), - prev_view_id.map(|s| s.to_string()), - ); - if from_section != to_section { - if to_section == Some(ViewSectionPB::Private) { - folder.add_private_view_ids(vec![view_id.to_string()]); - } else { - folder.delete_private_view_ids(vec![view_id.to_string()]); + if from_section != to_section { + if to_section == Some(ViewSectionPB::Private) { + folder.add_private_view_ids(vec![view_id.clone()]); + } else { + folder.delete_private_view_ids(vec![view_id.clone()]); + } } - } - notify_parent_view_did_change(workspace_id, &folder, vec![new_parent_id, old_parent_id]); - } + }, + ); + notify_parent_view_did_change( + &workspace_id, + self.mutex_folder.clone(), + vec![new_parent_id, old_parent_id], + ); Ok(()) } @@ -858,12 +656,6 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self), err)] pub async fn move_view(&self, view_id: &str, from: usize, to: usize) -> FlowyResult<()> { let workspace_id = self.user.workspace_id()?; - let view = self.get_view_pb(view_id).await?; - // if the view is locked, the view can't be moved - if view.is_locked.unwrap_or(false) { - return Err(FlowyError::view_is_locked()); - } - if let Some((is_workspace, parent_view_id, child_views)) = self.get_view_relation(view_id).await { // The display parent view is the view that is displayed in the UI @@ -894,12 +686,17 @@ impl FolderManager { if let (Some(actual_from_index), Some(actual_to_index)) = (actual_from_index, actual_to_index) { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); - let parent_view_id = Uuid::from_str(&parent_view_id)?; - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - } + self.with_folder( + || (), + |folder| { + folder.move_view(view_id, actual_from_index as u32, actual_to_index as u32); + }, + ); + notify_parent_view_did_change( + &workspace_id, + self.mutex_folder.clone(), + vec![parent_view_id], + ); } } } @@ -909,52 +706,17 @@ impl FolderManager { /// Return a list of views that belong to the given parent view id. #[tracing::instrument(level = "debug", skip(self, parent_view_id), err)] pub async fn get_views_belong_to(&self, parent_view_id: &str) -> FlowyResult<Vec<Arc<View>>> { - match self.mutex_folder.load_full() { - Some(folder) => Ok(folder.read().await.get_views_belong_to(parent_view_id)), - None => Ok(Vec::default()), - } - } - - /// Return a list of views that belong to the given parent view id, and not - /// in the trash section. - pub async fn get_untrashed_views_belong_to( - &self, - parent_view_id: &str, - ) -> FlowyResult<Vec<Arc<View>>> { - match self.mutex_folder.load_full() { - Some(folder) => { - let folder = folder.read().await; - let views = folder - .get_views_belong_to(parent_view_id) - .into_iter() - .filter(|view| !folder.is_view_in_section(Section::Trash, &view.id)) - .collect(); - - Ok(views) - }, - None => Ok(vec![]), - } - } - - pub async fn get_view(&self, view_id: &str) -> FlowyResult<Arc<View>> { - match self.mutex_folder.load_full() { - Some(folder) => { - let folder = folder.read().await; - Ok( - folder - .get_view(view_id) - .ok_or_else(FlowyError::record_not_found)?, - ) - }, - None => Err(FlowyError::internal().with_context("The folder is not initialized")), - } + let views = self.with_folder(Vec::new, |folder| { + folder.views.get_views_belong_to(parent_view_id) + }); + Ok(views) } /// Update the view with the given params. #[tracing::instrument(level = "trace", skip(self), err)] pub async fn update_view_with_params(&self, params: UpdateViewParams) -> FlowyResult<()> { self - .update_view(¶ms.view_id, true, |update| { + .update_view(¶ms.view_id, |update| { update .set_name_if_not_none(params.name) .set_desc_if_not_none(params.desc) @@ -973,304 +735,104 @@ impl FolderManager { params: UpdateViewIconParams, ) -> FlowyResult<()> { self - .update_view(¶ms.view_id, true, |update| { + .update_view(¶ms.view_id, |update| { update.set_icon(params.icon).done() }) .await } - /// Lock the view with the given view id. - /// - /// If the view is locked, it cannot be edited. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn lock_view(&self, view_id: &str) -> FlowyResult<()> { - self - .update_view(view_id, false, |update| { - update.set_page_lock_status(true).done() - }) - .await - } - - /// Unlock the view with the given view id. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn unlock_view(&self, view_id: &str) -> FlowyResult<()> { - self - .update_view(view_id, false, |update| { - update.set_page_lock_status(false).done() - }) - .await - } - /// Duplicate the view with the given view id. - /// - /// Including the view data (icon, cover, extra) and the child views. #[tracing::instrument(level = "debug", skip(self), err)] - pub(crate) async fn duplicate_view( - &self, - params: DuplicateViewParams, - ) -> Result<ViewPB, FlowyError> { - let lock = self - .mutex_folder - .load_full() - .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?; - let folder = lock.read().await; - let view = folder - .get_view(¶ms.view_id) + pub(crate) async fn duplicate_view(&self, view_id: &str) -> Result<(), FlowyError> { + let view = self + .with_folder(|| None, |folder| folder.views.get_view(view_id)) .ok_or_else(|| FlowyError::record_not_found().with_context("Can't duplicate the view"))?; - // Explicitly drop the folder lock to avoid deadlock when following calls contains 'self' - drop(folder); + let handler = self.get_handler(&view.layout)?; + let view_data = handler.duplicate_view(&view.id).await?; - let parent_view_id = params - .parent_view_id - .clone() - .unwrap_or(view.parent_view_id.clone()); - self - .duplicate_view_with_parent_id( - &view.id, - &parent_view_id, - params.open_after_duplicate, - params.include_children, - params.suffix, - params.sync_after_create, - ) - .await - } - - /// Duplicate the view with the given view id and parent view id. - /// - /// If the view id is the same as the parent view id, it will return an error. - /// If the view id is not found, it will return an error. - pub(crate) async fn duplicate_view_with_parent_id( - &self, - view_id: &str, - parent_view_id: &str, - open_after_duplicated: bool, - include_children: bool, - suffix: Option<String>, - sync_after_create: bool, - ) -> Result<ViewPB, FlowyError> { - if view_id == parent_view_id { - return Err(FlowyError::new( - ErrorCode::Internal, - format!("Can't duplicate the view({}) to itself", view_id), - )); - } - - // filter the view ids that in the trash or private section - let filtered_view_ids = match self.mutex_folder.load_full() { - None => Vec::default(), - Some(lock) => { - let folder = lock.read().await; - Self::get_view_ids_should_be_filtered(&folder) - }, + // get the current view index in the parent view, because we need to insert the duplicated view below the current view. + let index = if let Some((_, __, views)) = self.get_view_relation(&view.parent_view_id).await { + views.iter().position(|id| id == view_id).map(|i| i as u32) + } else { + None }; - // only apply the `open_after_duplicated` and the `include_children` to the first view - let mut is_source_view = true; - let mut new_view_id = String::default(); - // use a stack to duplicate the view and its children - let mut stack = vec![(view_id.to_string(), parent_view_id.to_string())]; - let mut objects = vec![]; - let suffix = suffix.unwrap_or(" (copy)".to_string()); - - let lock = match self.mutex_folder.load_full() { - None => { - return Err( - FlowyError::record_not_found() - .with_context(format!("Can't duplicate the view({})", view_id)), - ) - }, - Some(lock) => lock, + let is_private = self.with_folder( + || false, + |folder| folder.is_view_in_section(Section::Private, &view.id), + ); + let section = if is_private { + ViewSectionPB::Private + } else { + ViewSectionPB::Public }; - while let Some((current_view_id, current_parent_id)) = stack.pop() { - let view = lock - .read() - .await - .get_view(¤t_view_id) - .ok_or_else(|| { - FlowyError::record_not_found() - .with_context(format!("Can't duplicate the view({})", view_id)) - })?; - let handler = self.get_handler(&view.layout)?; - info!( - "{} duplicate view{}, name:{}, layout:{:?}", - handler.name(), - view.id, - view.name, - view.layout - ); - let view_id = Uuid::from_str(&view.id)?; - let view_data = handler.duplicate_view(&view_id).await?; + let duplicate_params = CreateViewParams { + parent_view_id: view.parent_view_id.clone(), + name: format!("{} (copy)", &view.name), + desc: view.desc.clone(), + layout: view.layout.clone().into(), + initial_data: view_data.to_vec(), + view_id: gen_view_id().to_string(), + meta: Default::default(), + set_as_current: true, + index, + section: Some(section), + }; - let index = self - .get_view_relation(¤t_parent_id) - .await - .and_then(|(_, _, views)| { - views - .iter() - .filter(|id| filtered_view_ids.contains(id)) - .position(|id| *id == current_view_id) - .map(|i| i as u32) - }); - - let section = { - let folder = lock.read().await; - if folder.is_view_in_section(Section::Private, &view.id) { - ViewSectionPB::Private - } else { - ViewSectionPB::Public - } - }; - - let name = if is_source_view { - format!( - "{}{}", - if view.name.is_empty() { - "Untitled" - } else { - view.name.as_str() - }, - suffix - ) - } else { - view.name.clone() - }; - - let parent_view_id = Uuid::from_str(¤t_parent_id)?; - let duplicate_params = CreateViewParams { - parent_view_id, - name, - layout: view.layout.clone().into(), - initial_data: ViewData::DuplicateData(view_data), - view_id: gen_view_id(), - meta: Default::default(), - set_as_current: is_source_view && open_after_duplicated, - index, - section: Some(section), - extra: view.extra.clone(), - icon: view.icon.clone(), - }; - - // set the notify_workspace_update to false to avoid multiple notifications - let (duplicated_view, encoded_collab) = self - .create_view_with_params(duplicate_params, false) - .await?; - - if is_source_view { - new_view_id.clone_from(&duplicated_view.id); - } - - if sync_after_create { - if let Some(encoded_collab) = encoded_collab { - let object_id = Uuid::from_str(&duplicated_view.id)?; - let collab_type = match duplicated_view.layout { - ViewLayout::Document => CollabType::Document, - ViewLayout::Board | ViewLayout::Grid | ViewLayout::Calendar => CollabType::Database, - ViewLayout::Chat => CollabType::Unknown, - }; - // don't block the whole import process if the view can't be encoded - if collab_type != CollabType::Unknown { - match self.get_folder_collab_params(object_id, collab_type, encoded_collab) { - Ok(params) => objects.push(params), - Err(e) => { - error!("duplicate error {}", e); - }, - } - } - } - } - - if include_children { - let child_views = self.get_views_belong_to(¤t_view_id).await?; - // reverse the child views to keep the order - for child_view in child_views.iter().rev() { - // skip the view_id should be filtered and the child_view is the duplicated view - if !filtered_view_ids.contains(&child_view.id) && child_view.layout != ViewLayout::Chat { - stack.push((child_view.id.clone(), duplicated_view.id.clone())); - } - } - } - - is_source_view = false - } - - let workspace_id = self.user.workspace_id()?; - let parent_view_id = Uuid::from_str(parent_view_id)?; - - // Sync the view to the cloud - if sync_after_create { - self - .cloud_service - .batch_create_folder_collab_objects(&workspace_id, objects) - .await?; - } - - // notify the update here - let folder = lock.read().await; - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - let duplicated_view = self.get_view_pb(&new_view_id).await?; - - Ok(duplicated_view) + self.create_view_with_params(duplicate_params).await?; + Ok(()) } #[tracing::instrument(level = "trace", skip(self), err)] - pub(crate) async fn set_current_view(&self, view_id: String) -> Result<(), FlowyError> { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.set_current_view(view_id.clone()); - folder.add_recent_view_ids(vec![view_id.clone()]); - } else { - return Err(FlowyError::record_not_found()); - } + pub(crate) async fn set_current_view(&self, view_id: &str) -> Result<(), FlowyError> { + self.with_folder( + || Err(FlowyError::record_not_found()), + |folder| { + folder.set_current_view(view_id); + folder.add_recent_view_ids(vec![view_id.to_string()]); + Ok(()) + }, + )?; let view = self.get_current_view().await; if let Some(view) = &view { let view_layout: ViewLayout = view.layout.clone().into(); if let Some(handle) = self.operation_handlers.get(&view_layout) { - info!("Open view: {}-{}", view.name, view.id); - let view_id = Uuid::from_str(&view.id)?; - if let Err(err) = handle.open_view(&view_id).await { + info!("Open view: {}", view.id); + if let Err(err) = handle.open_view(view_id).await { error!("Open view error: {:?}", err); } } } let workspace_id = self.user.workspace_id()?; - let setting = WorkspaceLatestPB { - workspace_id: workspace_id.to_string(), - latest_view: view, - }; - send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting); + send_workspace_setting_notification(workspace_id, view); Ok(()) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_current_view(&self) -> Option<ViewPB> { - let view_id = { - let lock = self.mutex_folder.load_full()?; - let folder = lock.read().await; - let view = folder.get_current_view()?; - drop(folder); - view - }; + let view_id = self.with_folder(|| None, |folder| folder.get_current_view())?; self.get_view_pb(&view_id).await.ok() } /// Toggles the favorite status of a view identified by `view_id`If the view is not a favorite, it will be added to the favorites list; otherwise, it will be removed from the list. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn toggle_favorites(&self, view_id: &str) -> FlowyResult<()> { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - if let Some(old_view) = folder.get_view(view_id) { - if old_view.is_favorite { - folder.delete_favorite_view_ids(vec![view_id.to_string()]); - } else { - folder.add_favorite_view_ids(vec![view_id.to_string()]); + self.with_folder( + || (), + |folder| { + if let Some(old_view) = folder.views.get_view(view_id) { + if old_view.is_favorite { + folder.delete_favorite_view_ids(vec![view_id.to_string()]); + } else { + folder.add_favorite_view_ids(vec![view_id.to_string()]); + } } - } - } + }, + ); self.send_toggle_favorite_notification(view_id).await; Ok(()) } @@ -1278,10 +840,12 @@ impl FolderManager { /// Add the view to the recent view list / history. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn add_recent_views(&self, view_ids: Vec<String>) -> FlowyResult<()> { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.add_recent_view_ids(view_ids); - } + self.with_folder( + || (), + |folder| { + folder.add_recent_view_ids(view_ids); + }, + ); self.send_update_recent_views_notification().await; Ok(()) } @@ -1289,329 +853,16 @@ impl FolderManager { /// Add the view to the recent view list / history. #[tracing::instrument(level = "debug", skip(self), err)] pub async fn remove_recent_views(&self, view_ids: Vec<String>) -> FlowyResult<()> { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.delete_recent_view_ids(view_ids); - } + self.with_folder( + || (), + |folder| { + folder.delete_recent_view_ids(view_ids); + }, + ); self.send_update_recent_views_notification().await; Ok(()) } - /// Publishes a view identified by the given `view_id`. - /// - /// If `publish_name` is `None`, a default name will be generated using the view name and view id. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn publish_view( - &self, - view_id: &str, - publish_name: Option<String>, - selected_view_ids: Option<Vec<String>>, - ) -> FlowyResult<()> { - let view = { - let lock = match self.mutex_folder.load_full() { - None => { - return Err( - FlowyError::record_not_found() - .with_context(format!("Can't find the view with ID: {}", view_id)), - ) - }, - Some(lock) => lock, - }; - let read_guard = lock.read().await; - read_guard.get_view(view_id).ok_or_else(|| { - FlowyError::record_not_found() - .with_context(format!("Can't find the view with ID: {}", view_id)) - })? - }; - - if view.layout == ViewLayout::Chat { - return Err(FlowyError::new( - ErrorCode::NotSupportYet, - "The chat view is not supported to publish.".to_string(), - )); - } - - // Retrieve the view payload and its child views recursively - let payload = self - .get_batch_publish_payload(view_id, publish_name, false) - .await?; - - // set the selected view ids to the payload - let payload = if let Some(selected_view_ids) = selected_view_ids { - payload - .into_iter() - .map(|mut p| { - if let PublishPayload::Database(p) = &mut p { - p.data - .visible_database_view_ids - .clone_from(&selected_view_ids); - } - p - }) - .collect::<Vec<_>>() - } else { - payload - }; - - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .publish_view(&workspace_id, payload) - .await?; - Ok(()) - } - - /// Unpublish the view with the given view id. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn unpublish_views(&self, view_ids: Vec<Uuid>) -> FlowyResult<()> { - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .unpublish_views(&workspace_id, view_ids) - .await?; - Ok(()) - } - - /// Get the publish info of the view with the given view id. - /// The publish info contains the namespace and publish_name of the view. - #[tracing::instrument(level = "debug", skip(self))] - pub async fn get_publish_info(&self, view_id: &Uuid) -> FlowyResult<PublishInfo> { - let publish_info = self.cloud_service.get_publish_info(view_id).await?; - Ok(publish_info) - } - - /// Sets the publish name of the view with the given view id. - #[tracing::instrument(level = "debug", skip(self))] - pub async fn set_publish_name(&self, view_id: Uuid, new_name: String) -> FlowyResult<()> { - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .set_publish_name(&workspace_id, view_id, new_name) - .await?; - Ok(()) - } - - /// Get the namespace of the current workspace. - /// The namespace is used to generate the URL of the published view. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn set_publish_namespace(&self, new_namespace: String) -> FlowyResult<()> { - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .set_publish_namespace(&workspace_id, new_namespace) - .await?; - Ok(()) - } - - /// Get the namespace of the current workspace. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn get_publish_namespace(&self) -> FlowyResult<String> { - let workspace_id = self.user.workspace_id()?; - let namespace = self - .cloud_service - .get_publish_namespace(&workspace_id) - .await?; - Ok(namespace) - } - - /// List all published views of the current workspace. - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn list_published_views(&self) -> FlowyResult<Vec<PublishInfoView>> { - let workspace_id = self.user.workspace_id()?; - let published_views = self - .cloud_service - .list_published_views(&workspace_id) - .await?; - Ok(published_views) - } - - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn get_default_published_view_info(&self) -> FlowyResult<PublishInfo> { - let workspace_id = self.user.workspace_id()?; - let default_published_view_info = self - .cloud_service - .get_default_published_view_info(&workspace_id) - .await?; - Ok(default_published_view_info) - } - - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn set_default_published_view(&self, view_id: uuid::Uuid) -> FlowyResult<()> { - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .set_default_published_view(&workspace_id, view_id) - .await?; - Ok(()) - } - - #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn remove_default_published_view(&self) -> FlowyResult<()> { - let workspace_id = self.user.workspace_id()?; - self - .cloud_service - .remove_default_published_view(&workspace_id) - .await?; - Ok(()) - } - - /// Retrieves the publishing payload for a specified view and optionally its child views. - /// - /// # Arguments - /// * `view_id` - The ID of the view to publish. - /// * `publish_name` - Optional name for the published view. - /// * `include_children` - Flag to include child views in the payload. - pub async fn get_batch_publish_payload( - &self, - view_id: &str, - publish_name: Option<String>, - include_children: bool, - ) -> FlowyResult<Vec<PublishPayload>> { - let mut stack = vec![view_id.to_string()]; - let mut payloads = Vec::new(); - - while let Some(current_view_id) = stack.pop() { - let view = match self.get_view_pb(¤t_view_id).await { - Ok(view) => view, - Err(_) => continue, - }; - - // Skip the chat view - if view.layout == ViewLayoutPB::Chat { - continue; - } - - let layout: ViewLayout = view.layout.into(); - - // Only support set the publish_name for the current view, not for the child views - let publish_name = if current_view_id == view_id { - publish_name.clone() - } else { - None - }; - - if let Ok(payload) = self - .get_publish_payload(&Uuid::from_str(¤t_view_id)?, publish_name, layout) - .await - { - payloads.push(payload); - } - - if include_children { - // Add the child views to the stack - stack.extend(view.child_views.iter().map(|child| child.id.clone())); - } - } - - Ok(payloads) - } - - async fn build_publish_views(&self, view_id: &str) -> Option<PublishViewInfo> { - let view_pb = self.get_view_pb(view_id).await.ok()?; - - let mut child_views_futures = vec![]; - - for child in &view_pb.child_views { - let future = self.build_publish_views(&child.id); - child_views_futures.push(future); - } - - let child_views = future::join_all(child_views_futures) - .await - .into_iter() - .flatten() - .collect::<Vec<PublishViewInfo>>(); - - let view_child_views = if child_views.is_empty() { - None - } else { - Some(child_views) - }; - - let view = view_pb_to_publish_view(&view_pb); - - let view = PublishViewInfo { - child_views: view_child_views, - ..view - }; - - Some(view) - } - - async fn get_publish_payload( - &self, - view_id: &Uuid, - publish_name: Option<String>, - layout: ViewLayout, - ) -> FlowyResult<PublishPayload> { - let handler = self.get_handler(&layout)?; - let encoded_collab_wrapper: GatherEncodedCollab = handler - .gather_publish_encode_collab(&self.user, view_id) - .await?; - - let view_str_id = view_id.to_string(); - let view = self.get_view_pb(&view_str_id).await?; - - let publish_name = publish_name.unwrap_or_else(|| generate_publish_name(&view.id, &view.name)); - - let child_views = self - .build_publish_views(&view_str_id) - .await - .and_then(|v| v.child_views) - .unwrap_or_default(); - - let ancestor_views = self - .get_view_ancestors_pb(&view_str_id) - .await? - .iter() - .map(view_pb_to_publish_view) - .collect::<Vec<PublishViewInfo>>(); - - let metadata = PublishViewMetaData { - view: view_pb_to_publish_view(&view), - child_views, - ancestor_views, - }; - let meta = PublishViewMeta { - view_id: view.id.clone(), - publish_name, - metadata, - }; - - let payload = match encoded_collab_wrapper { - GatherEncodedCollab::Database(v) => { - let database_collab = v.database_encoded_collab.doc_state.to_vec(); - let database_relations = v.database_relations; - let database_row_collabs = v - .database_row_encoded_collabs - .into_iter() - .map(|v| (v.0, v.1.doc_state.to_vec())) // Convert to HashMap - .collect::<HashMap<String, Vec<u8>>>(); - let database_row_document_collabs = v - .database_row_document_encoded_collabs - .into_iter() - .map(|v| (v.0, v.1.doc_state.to_vec())) // Convert to HashMap - .collect::<HashMap<String, Vec<u8>>>(); - - let data = PublishDatabaseData { - database_collab, - database_row_collabs, - database_relations, - database_row_document_collabs, - ..Default::default() - }; - PublishPayload::Database(PublishDatabasePayload { meta, data }) - }, - GatherEncodedCollab::Document(v) => { - let data = v.doc_state.to_vec(); - PublishPayload::Document(PublishDocumentPayload { meta, data }) - }, - GatherEncodedCollab::Unknown => PublishPayload::Unknown, - }; - - Ok(payload) - } - // Used by toggle_favorites to send notification to frontend, after the favorite status of view has been changed.It sends two distinct notifications: one to correctly update the concerned view's is_favorite status, and another to update the list of favorites that is to be displayed. async fn send_toggle_favorite_notification(&self, view_id: &str) { if let Ok(view) = self.get_view_pb(view_id).await { @@ -1620,13 +871,13 @@ impl FolderManager { } else { FolderNotification::DidUnfavoriteView }; - folder_notification_builder("favorite", notification_type) + send_notification("favorite", notification_type) .payload(RepeatedViewPB { items: vec![view.clone()], }) .send(); - folder_notification_builder(&view.id, FolderNotification::DidUpdateView) + send_notification(&view.id, FolderNotification::DidUpdateView) .payload(view) .send() } @@ -1634,7 +885,7 @@ impl FolderManager { async fn send_update_recent_views_notification(&self) { let recent_views = self.get_my_recent_sections().await; - folder_notification_builder("recent_views", FolderNotification::DidUpdateRecentViews) + send_notification("recent_views", FolderNotification::DidUpdateRecentViews) .payload(RepeatedViewIdPB { items: recent_views.into_iter().map(|item| item.id).collect(), }) @@ -1643,57 +894,52 @@ impl FolderManager { #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_all_favorites(&self) -> Vec<SectionItem> { - self.get_sections(Section::Favorite).await + self.get_sections(Section::Favorite) } #[tracing::instrument(level = "debug", skip(self))] pub(crate) async fn get_my_recent_sections(&self) -> Vec<SectionItem> { - self.get_sections(Section::Recent).await + self.get_sections(Section::Recent) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn get_my_trash_info(&self) -> Vec<TrashInfo> { - match self.mutex_folder.load_full() { - None => Vec::default(), - Some(folder) => folder.read().await.get_my_trash_info(), - } + self.with_folder(Vec::new, |folder| folder.get_my_trash_info()) } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_all_trash(&self) { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.remove_all_my_trash_sections(); - folder_notification_builder("trash", FolderNotification::DidUpdateTrash) - .payload(RepeatedTrashPB { items: vec![] }) - .send(); - } + self.with_folder( + || (), + |folder| { + folder.remove_all_my_trash_sections(); + }, + ); + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(RepeatedTrashPB { items: vec![] }) + .send(); } #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn restore_trash(&self, trash_id: &str) { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.delete_trash_view_ids(vec![trash_id.to_string()]); - } + self.with_folder( + || (), + |folder| { + folder.delete_trash_view_ids(vec![trash_id.to_string()]); + }, + ); } /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] pub(crate) async fn delete_my_trash(&self) { - if let Some(lock) = self.mutex_folder.load_full() { - let deleted_trash = lock.read().await.get_my_trash_info(); - - // Explicitly drop the folder lock to avoid deadlock when following calls contains 'self' - drop(lock); - - for trash in deleted_trash { - let _ = self.delete_trash(&trash.id).await; - } - folder_notification_builder("trash", FolderNotification::DidUpdateTrash) - .payload(RepeatedTrashPB { items: vec![] }) - .send(); + let deleted_trash = self.with_folder(Vec::new, |folder| folder.get_my_trash_info()); + for trash in deleted_trash { + let _ = self.delete_trash(&trash.id).await; } + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(RepeatedTrashPB { items: vec![] }) + .send(); } /// Delete the trash permanently. @@ -1701,152 +947,95 @@ impl FolderManager { /// is a database view. Then the database will be deleted as well. #[tracing::instrument(level = "debug", skip(self, view_id), err)] pub async fn delete_trash(&self, view_id: &str) -> FlowyResult<()> { - if let Some(lock) = self.mutex_folder.load_full() { - let view = { - let mut folder = lock.write().await; - let view = folder.get_view(view_id); + let view = self.with_folder(|| None, |folder| folder.views.get_view(view_id)); + self.with_folder( + || (), + |folder| { folder.delete_trash_view_ids(vec![view_id.to_string()]); - folder.delete_views(vec![view_id]); - view - }; - - if let Some(view) = view { - let view_id = Uuid::from_str(view_id)?; - if let Ok(handler) = self.get_handler(&view.layout) { - handler.delete_view(&view_id).await?; - } + folder.views.delete_views(vec![view_id]); + }, + ); + if let Some(view) = view { + if let Ok(handler) = self.get_handler(&view.layout) { + handler.delete_view(view_id).await?; } } Ok(()) } - /// Imports a single file to the folder and returns the encoded collab for immediate cloud sync. - #[allow(clippy::type_complexity)] - #[instrument(level = "debug", skip_all, err)] - pub(crate) async fn import_single_file( - &self, - parent_view_id: Uuid, - import_data: ImportItem, - ) -> FlowyResult<(View, Vec<(String, CollabType, EncodedCollab)>)> { - let handler = self.get_handler(&import_data.view_layout)?; - let view_id = gen_view_id(); - let uid = self.user.user_id()?; - let mut encoded_collab = vec![]; + pub(crate) async fn import(&self, import_data: ImportParams) -> FlowyResult<View> { + let workspace_id = self.user.workspace_id()?; + if import_data.data.is_none() && import_data.file_path.is_none() { + return Err(FlowyError::new( + ErrorCode::InvalidParams, + "data or file_path is required", + )); + } - info!("import single file from:{}", import_data.data); - match import_data.data { - ImportData::FilePath { file_path } => { - handler - .import_from_file_path(&view_id.to_string(), &import_data.name, file_path) - .await?; - }, - ImportData::Bytes { bytes } => { - encoded_collab = handler - .import_from_bytes( - uid, - &view_id, - &import_data.name, - import_data.import_type, - bytes, - ) - .await?; - }, + let handler = self.get_handler(&import_data.view_layout)?; + let view_id = gen_view_id().to_string(); + let uid = self.user.user_id()?; + if let Some(data) = import_data.data { + handler + .import_from_bytes( + uid, + &view_id, + &import_data.name, + import_data.import_type, + data, + ) + .await?; + } + + if let Some(file_path) = import_data.file_path { + handler + .import_from_file_path(&view_id, &import_data.name, file_path) + .await?; } let params = CreateViewParams { - parent_view_id, + parent_view_id: import_data.parent_view_id, name: import_data.name, + desc: "".to_string(), layout: import_data.view_layout.clone().into(), - initial_data: ViewData::Empty, + initial_data: vec![], view_id, meta: Default::default(), set_as_current: false, index: None, section: None, - extra: None, - icon: None, }; let view = create_view(self.user.user_id()?, params, import_data.view_layout); - - // Insert the new view into the folder - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - folder.insert_view(view.clone(), None); - } - - Ok((view, encoded_collab)) - } - - pub(crate) async fn import_zip_file(&self, zip_file_path: &str) -> FlowyResult<()> { - self.cloud_service.import_zip(zip_file_path).await?; - Ok(()) - } - - /// Import function to handle the import of data. - pub(crate) async fn import(&self, import_data: ImportParams) -> FlowyResult<RepeatedViewPB> { - let workspace_id = self.user.workspace_id()?; - let mut objects = vec![]; - let mut views = vec![]; - for data in import_data.items { - // Import a single file and get the view and encoded collab data - let (view, encoded_collabs) = self - .import_single_file(import_data.parent_view_id, data) - .await?; - views.push(view_pb_without_child_views(view)); - - for (object_id, collab_type, encode_collab) in encoded_collabs { - if let Ok(object_id) = Uuid::from_str(&object_id) { - match self.get_folder_collab_params(object_id, collab_type, encode_collab) { - Ok(params) => objects.push(params), - Err(e) => { - error!("import error {}", e); - }, - } - } - } - } - - info!("Syncing the imported {} collab to the cloud", objects.len()); - self - .cloud_service - .batch_create_folder_collab_objects(&workspace_id, objects) - .await?; - - // Notify that the parent view has changed - if let Some(lock) = self.mutex_folder.load_full() { - let folder = lock.read().await; - notify_parent_view_did_change(workspace_id, &folder, vec![import_data.parent_view_id]); - } - - Ok(RepeatedViewPB { items: views }) + self.with_folder( + || (), + |folder| { + folder.insert_view(view.clone(), None); + }, + ); + notify_parent_view_did_change( + &workspace_id, + self.mutex_folder.clone(), + vec![view.parent_view_id.clone()], + ); + Ok(view) } /// Update the view with the provided view_id using the specified function. - /// - /// If the check_locked is true, it will check the lock status of the view. If the view is locked, - /// it will return an error. - async fn update_view<F>(&self, view_id: &str, check_locked: bool, f: F) -> FlowyResult<()> + async fn update_view<F>(&self, view_id: &str, f: F) -> FlowyResult<()> where F: FnOnce(ViewUpdate) -> Option<View>, { let workspace_id = self.user.workspace_id()?; - let value = match self.mutex_folder.load_full() { - None => None, - Some(lock) => { - let mut folder = lock.write().await; - let old_view = folder.get_view(view_id); - - // Check if the view is locked - if check_locked && old_view.as_ref().and_then(|v| v.is_locked).unwrap_or(false) { - return Err(FlowyError::view_is_locked()); - } - - let new_view = folder.update_view(view_id, f); + let value = self.with_folder( + || None, + |folder| { + let old_view = folder.views.get_view(view_id); + let new_view = folder.views.update_view(view_id, f); Some((old_view, new_view)) }, - }; + ); if let Some((Some(old_view), Some(new_view))) = value { if let Ok(handler) = self.get_handler(&old_view.layout) { @@ -1855,13 +1044,13 @@ impl FolderManager { } if let Ok(view_pb) = self.get_view_pb(view_id).await { - folder_notification_builder(&view_pb.id, FolderNotification::DidUpdateView) + send_notification(&view_pb.id, FolderNotification::DidUpdateView) .payload(view_pb) .send(); - if let Some(lock) = self.mutex_folder.load_full() { - let folder = lock.read().await; - notify_did_update_workspace(&workspace_id, &folder); + let folder = &self.mutex_folder.read(); + if let Some(folder) = folder.as_ref() { + notify_did_update_workspace(&workspace_id, folder); } } @@ -1869,7 +1058,10 @@ impl FolderManager { } /// Returns a handler that implements the [FolderOperationHandler] trait - fn get_handler(&self, view_layout: &ViewLayout) -> FlowyResult<Arc<dyn FolderOperationHandler>> { + fn get_handler( + &self, + view_layout: &ViewLayout, + ) -> FlowyResult<Arc<dyn FolderOperationHandler + Send + Sync>> { match self.operation_handlers.get(view_layout) { None => Err(FlowyError::internal().with_context(format!( "Get data processor failed. Unknown layout type: {:?}", @@ -1879,58 +1071,43 @@ impl FolderManager { } } - fn get_folder_collab_params( - &self, - object_id: Uuid, - collab_type: CollabType, - encoded_collab: EncodedCollab, - ) -> FlowyResult<FolderCollabParams> { - // Try to encode the collaboration data to bytes - let encoded_collab_v1: Result<Vec<u8>, FlowyError> = - encoded_collab.encode_to_bytes().map_err(internal_error); - encoded_collab_v1.map(|encoded_collab_v1| FolderCollabParams { - object_id, - encoded_collab_v1, - collab_type, - }) - } - /// Returns the relation of the view. The relation is a tuple of (is_workspace, parent_view_id, /// child_view_ids). If the view is a workspace, then the parent_view_id is the workspace id. /// Otherwise, the parent_view_id is the parent view id of the view. The child_view_ids is the /// child view ids of the view. async fn get_view_relation(&self, view_id: &str) -> Option<(bool, String, Vec<String>)> { let workspace_id = self.user.workspace_id().ok()?; - let lock = self.mutex_folder.load_full()?; - let folder = lock.read().await; - let view = folder.get_view(view_id)?; - match folder.get_view(&view.parent_view_id) { - None => folder - .get_workspace_info(&workspace_id.to_string()) - .map(|workspace| { - ( - true, - workspace.id, - workspace - .child_views + self.with_folder( + || None, + |folder| { + let view = folder.views.get_view(view_id)?; + match folder.views.get_view(&view.parent_view_id) { + None => folder.get_workspace_info(&workspace_id).map(|workspace| { + ( + true, + workspace.id, + workspace + .child_views + .items + .into_iter() + .map(|view| view.id) + .collect::<Vec<String>>(), + ) + }), + Some(parent_view) => Some(( + false, + parent_view.id.clone(), + parent_view + .children .items + .clone() .into_iter() .map(|view| view.id) .collect::<Vec<String>>(), - ) - }), - Some(parent_view) => Some(( - false, - parent_view.id.clone(), - parent_view - .children - .items - .clone() - .into_iter() - .map(|view| view.id) - .collect::<Vec<String>>(), - )), - } + )), + } + }, + ) } pub async fn get_folder_snapshots( @@ -1954,41 +1131,39 @@ impl FolderManager { Ok(snapshots) } - pub async fn set_views_visibility(&self, view_ids: Vec<String>, is_public: bool) { - if let Some(lock) = self.mutex_folder.load_full() { - let mut folder = lock.write().await; - if is_public { - folder.delete_private_view_ids(view_ids); - } else { - folder.add_private_view_ids(view_ids); - } - } + pub fn set_views_visibility(&self, view_ids: Vec<String>, is_public: bool) { + self.with_folder( + || (), + |folder| { + if is_public { + folder.delete_private_view_ids(view_ids); + } else { + folder.add_private_view_ids(view_ids); + } + }, + ); } /// Only support getting the Favorite and Recent sections. - async fn get_sections(&self, section_type: Section) -> Vec<SectionItem> { - match self.mutex_folder.load_full() { - None => Vec::default(), - Some(lock) => { - let folder = lock.read().await; - let views = match section_type { - Section::Favorite => folder.get_my_favorite_sections(), - Section::Recent => folder.get_my_recent_sections(), - _ => vec![], - }; - let view_ids_should_be_filtered = Self::get_view_ids_should_be_filtered(&folder); - views - .into_iter() - .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) - .collect() - }, - } + fn get_sections(&self, section_type: Section) -> Vec<SectionItem> { + self.with_folder(Vec::new, |folder| { + let views = match section_type { + Section::Favorite => folder.get_my_favorite_sections(), + Section::Recent => folder.get_my_recent_sections(), + _ => vec![], + }; + let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); + views + .into_iter() + .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) + .collect() + }) } /// Get all the view that are in the trash, including the child views of the child views. /// For example, if A view which is in the trash has a child view B, this function will return /// both A and B. - fn get_all_trash_ids(folder: &Folder) -> Vec<String> { + fn get_all_trash_ids(&self, folder: &Folder) -> Vec<String> { let trash_ids = folder .get_all_trash_sections() .into_iter() @@ -2002,13 +1177,13 @@ impl FolderManager { } /// Filter the views that are in the trash and belong to the other private sections. - fn get_view_ids_should_be_filtered(folder: &Folder) -> Vec<String> { - let trash_ids = Self::get_all_trash_ids(folder); - let other_private_view_ids = Self::get_other_private_view_ids(folder); + fn get_view_ids_should_be_filtered(&self, folder: &Folder) -> Vec<String> { + let trash_ids = self.get_all_trash_ids(folder); + let other_private_view_ids = self.get_other_private_view_ids(folder); [trash_ids, other_private_view_ids].concat() } - fn get_other_private_view_ids(folder: &Folder) -> Vec<String> { + fn get_other_private_view_ids(&self, folder: &Folder) -> Vec<String> { let my_private_view_ids = folder .get_my_private_sections() .into_iter() @@ -2027,18 +1202,17 @@ impl FolderManager { .collect() } - pub async fn remove_indices_for_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub fn remove_indices_for_workspace(&self, workspace_id: String) -> FlowyResult<()> { self .folder_indexer - .remove_indices_for_workspace(*workspace_id) - .await?; + .remove_indices_for_workspace(workspace_id)?; Ok(()) } } /// Return the views that belong to the workspace. The views are filtered by the trash and all the private views. -pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec<ViewPB> { +pub(crate) fn get_workspace_public_view_pbs(workspace_id: &str, folder: &Folder) -> Vec<ViewPB> { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2053,7 +1227,7 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder .map(|view| view.id) .collect::<Vec<String>>(); - let mut views = folder.get_views_belong_to(&workspace_id.to_string()); + let mut views = folder.views.get_views_belong_to(workspace_id); // filter the views that are in the trash and all the private views views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); @@ -2061,8 +1235,11 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder .into_iter() .map(|view| { // Get child views - let mut child_views: Vec<Arc<View>> = - folder.get_views_belong_to(&view.id).into_iter().collect(); + let mut child_views: Vec<Arc<View>> = folder + .views + .get_views_belong_to(&view.id) + .into_iter() + .collect(); child_views.retain(|view| !trash_ids.contains(&view.id)); view_pb_with_child_views(view, child_views) }) @@ -2071,15 +1248,21 @@ pub(crate) fn get_workspace_public_view_pbs(workspace_id: &Uuid, folder: &Folder /// Get all the child views belong to the view id, including the child views of the child views. fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec<String> { - folder - .get_view_recursively(view_id) - .iter() + let child_view_ids = folder + .views + .get_views_belong_to(view_id) + .into_iter() .map(|view| view.id.clone()) - .collect() + .collect::<Vec<String>>(); + let mut all_child_view_ids = child_view_ids.clone(); + for child_view_id in child_view_ids { + all_child_view_ids.extend(get_all_child_view_ids(folder, &child_view_id)); + } + all_child_view_ids } /// Get the current private views of the user. -pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folder) -> Vec<ViewPB> { +pub(crate) fn get_workspace_private_view_pbs(workspace_id: &str, folder: &Folder) -> Vec<ViewPB> { // get the trash ids let trash_ids = folder .get_all_trash_sections() @@ -2094,7 +1277,7 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde .map(|view| view.id) .collect::<Vec<String>>(); - let mut views = folder.get_views_belong_to(&workspace_id.to_string()); + let mut views = folder.views.get_views_belong_to(workspace_id); // filter the views that are in the trash and not in the private view ids views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); @@ -2102,16 +1285,31 @@ pub(crate) fn get_workspace_private_view_pbs(workspace_id: &Uuid, folder: &Folde .into_iter() .map(|view| { // Get child views - let mut child_views: Vec<Arc<View>> = - folder.get_views_belong_to(&view.id).into_iter().collect(); + let mut child_views: Vec<Arc<View>> = folder + .views + .get_views_belong_to(&view.id) + .into_iter() + .collect(); child_views.retain(|view| !trash_ids.contains(&view.id)); view_pb_with_child_views(view, child_views) }) .collect() } +/// The MutexFolder is a wrapper of the [Folder] that is used to share the folder between different +/// threads. +#[derive(Clone, Default)] +pub struct MutexFolder(Arc<RwLock<Option<Folder>>>); +impl Deref for MutexFolder { + type Target = Arc<RwLock<Option<Folder>>>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +unsafe impl Sync for MutexFolder {} +unsafe impl Send for MutexFolder {} + #[allow(clippy::large_enum_variant)] -#[derive(Debug)] pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index c581031f54..d1266a146e 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -2,15 +2,13 @@ use crate::manager::{FolderInitDataSource, FolderManager}; use crate::manager_observer::*; use crate::user_default::DefaultFolderBuilder; use collab::core::collab::DataSource; -use collab::lock::RwLock; use collab_entity::{CollabType, EncodedCollab}; -use collab_folder::{Folder, FolderNotify}; +use collab_folder::{Folder, FolderNotify, UserId}; use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; use std::sync::{Arc, Weak}; use tokio::task::spawn_blocking; -use tracing::{error, event, info, Level}; -use uuid::Uuid; +use tracing::{event, info, Level}; impl FolderManager { /// Called immediately after the application launched if the user already sign in/sign up. @@ -18,7 +16,7 @@ impl FolderManager { pub async fn initialize( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, initial_data: FolderInitDataSource, ) -> FlowyResult<()> { // Update the workspace id @@ -29,15 +27,12 @@ impl FolderManager { initial_data ); - if let Some(old_folder) = self.mutex_folder.swap(None) { - let old_folder = old_folder.read().await; + if let Some(old_folder) = self.mutex_folder.write().take() { old_folder.close(); - info!( - "remove old folder: {}", - old_folder.get_workspace_id().unwrap_or_default() - ); + info!("remove old folder: {}", old_folder.get_workspace_id()); } + let workspace_id = workspace_id.to_string(); // Get the collab db for the user with given user id. let collab_db = self.user.collab_db(uid)?; @@ -52,37 +47,40 @@ impl FolderManager { FolderInitDataSource::LocalDisk { create_if_not_exist, } => { - let is_exist = self - .user - .is_folder_exist_on_disk(uid, workspace_id) - .unwrap_or(false); + let is_exist = self.is_workspace_exist_in_local(uid, &workspace_id).await; // 1. if the folder exists, open it from local disk if is_exist { event!(Level::INFO, "Init folder from local disk"); self - .make_folder(uid, workspace_id, collab_db, None, folder_notifier) + .make_folder( + uid, + &workspace_id, + collab_db, + DataSource::Disk, + folder_notifier, + ) .await? } else if create_if_not_exist { // 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder // Currently, this branch is only used when the server type is supabase. For appflowy cloud, // the default workspace is already created when the user sign up. self - .create_default_folder(uid, workspace_id, collab_db, folder_notifier) + .create_default_folder(uid, &workspace_id, collab_db, folder_notifier) .await? } else { // 3. If the folder doesn't exist and create_if_not_exist is false, try to fetch the folder data from cloud/ // This will happen user can't fetch the folder data when the user sign in. let doc_state = self .cloud_service - .get_folder_doc_state(workspace_id, uid, CollabType::Folder, workspace_id) + .get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id) .await?; self .make_folder( uid, - workspace_id, + &workspace_id, collab_db.clone(), - Some(DataSource::DocStateV1(doc_state)), + DataSource::DocStateV1(doc_state), folder_notifier.clone(), ) .await? @@ -92,16 +90,22 @@ impl FolderManager { if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self - .make_folder(uid, workspace_id, collab_db, None, folder_notifier) + .make_folder( + uid, + &workspace_id, + collab_db, + DataSource::Disk, + folder_notifier, + ) .await? } else { event!(Level::INFO, "Restore folder from remote data"); self .make_folder( uid, - workspace_id, + &workspace_id, collab_db.clone(), - Some(DataSource::DocStateV1(doc_state)), + DataSource::DocStateV1(doc_state), folder_notifier.clone(), ) .await? @@ -109,55 +113,58 @@ impl FolderManager { }, }; - let folder_state_rx = { - let folder = folder.read().await; - let folder_state_rx = folder.subscribe_sync_state(); - let index_content_rx = folder.subscribe_index_content(); - self - .folder_indexer - .set_index_content_receiver(index_content_rx, *workspace_id) - .await; - self.handle_index_folder(*workspace_id, &folder).await; - folder_state_rx - }; + let folder_state_rx = folder.subscribe_sync_state(); + let index_content_rx = folder.subscribe_index_content(); + self + .folder_indexer + .set_index_content_receiver(index_content_rx, workspace_id.clone()); + self.handle_index_folder(workspace_id.clone(), &folder); - self.mutex_folder.store(Some(folder.clone())); + *self.mutex_folder.write() = Some(folder); - let weak_mutex_folder = Arc::downgrade(&folder); - subscribe_folder_sync_state_changed(*workspace_id, folder_state_rx, Arc::downgrade(&self.user)); + let weak_mutex_folder = Arc::downgrade(&self.mutex_folder); + subscribe_folder_sync_state_changed( + workspace_id.clone(), + folder_state_rx, + Arc::downgrade(&self.user), + ); + subscribe_folder_snapshot_state_changed( + workspace_id.clone(), + &weak_mutex_folder, + Arc::downgrade(&self.user), + ); subscribe_folder_trash_changed( - *workspace_id, + workspace_id.clone(), section_change_rx, - weak_mutex_folder.clone(), + &weak_mutex_folder, Arc::downgrade(&self.user), ); subscribe_folder_view_changed( - *workspace_id, + workspace_id.clone(), view_rx, - weak_mutex_folder.clone(), + &weak_mutex_folder, Arc::downgrade(&self.user), ); - let weak_folder_indexer = Arc::downgrade(&self.folder_indexer); - let workspace_id = *workspace_id; - tokio::spawn(async move { - if let Some(folder_indexer) = weak_folder_indexer.upgrade() { - if let Err(err) = folder_indexer.initialize(&workspace_id).await { - error!("Failed to initialize folder indexer: {:?}", err); - } - } - }); - Ok(()) } + async fn is_workspace_exist_in_local(&self, uid: i64, workspace_id: &str) -> bool { + if let Ok(weak_collab) = self.user.collab_db(uid) { + if let Some(collab_db) = weak_collab.upgrade() { + return collab_db.is_exist(uid, workspace_id).await.unwrap_or(false); + } + } + false + } + async fn create_default_folder( &self, uid: i64, - workspace_id: &Uuid, + workspace_id: &str, collab_db: Weak<CollabKVDB>, folder_notifier: FolderNotify, - ) -> Result<Arc<RwLock<Folder>>, FlowyError> { + ) -> Result<Folder, FlowyError> { event!( Level::INFO, "Create folder:{} with default folder builder", @@ -165,34 +172,35 @@ impl FolderManager { ); let folder_data = DefaultFolderBuilder::build(uid, workspace_id.to_string(), &self.operation_handlers).await; - let folder = self - .create_folder_with_data( - uid, - workspace_id, - collab_db, - Some(folder_notifier), - Some(folder_data), - ) + let collab = self + .create_empty_collab(uid, workspace_id, collab_db) .await?; - Ok(folder) + Ok(Folder::create( + UserId::from(uid), + collab, + Some(folder_notifier), + folder_data, + )) } - async fn handle_index_folder(&self, workspace_id: Uuid, folder: &Folder) { + fn handle_index_folder(&self, workspace_id: String, folder: &Folder) { let mut index_all = true; let encoded_collab = self .store_preferences - .get_object::<EncodedCollab>(workspace_id.to_string().as_str()); + .get_object::<EncodedCollab>(&workspace_id); if let Some(encoded_collab) = encoded_collab { if let Ok(changes) = folder.calculate_view_changes(encoded_collab) { let folder_indexer = self.folder_indexer.clone(); - let views = folder.get_all_views(); + let views = folder.views.get_all_views(); + let wid = workspace_id.clone(); + if !changes.is_empty() && !views.is_empty() { spawn_blocking(move || { // We index the changes - folder_indexer.index_view_changes(views, changes, workspace_id); + folder_indexer.index_view_changes(views, changes, wid); }); index_all = false; } @@ -200,14 +208,17 @@ impl FolderManager { } if index_all { - let views = folder.get_all_views(); + let views = folder.views.get_all_views(); let folder_indexer = self.folder_indexer.clone(); - let _ = folder_indexer - .remove_indices_for_workspace(workspace_id) - .await; + let wid = workspace_id.clone(); + // We spawn a blocking task to index all views in the folder spawn_blocking(move || { - folder_indexer.index_all_views(views, workspace_id); + // We remove old indexes just in case + let _ = folder_indexer.remove_indices_for_workspace(wid.clone()); + + // We index all views from the workspace + folder_indexer.index_all_views(views, wid); }); } @@ -215,12 +226,12 @@ impl FolderManager { } fn save_collab_to_preferences(&self, folder: &Folder) { - if let Some(workspace_id) = folder.get_workspace_id() { - let encoded_collab = folder.encode_collab(); + let encoded_collab = folder.encode_collab_v1(); - if let Ok(encoded) = encoded_collab { - let _ = self.store_preferences.set_object(&workspace_id, &encoded); - } + if let Ok(encoded) = encoded_collab { + let _ = self + .store_preferences + .set_object(&folder.get_workspace_id(), encoded); } } } diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index 5d3034b5aa..ef604b3a11 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -1,33 +1,32 @@ use crate::entities::{ - view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSyncStatePB, - RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB, + view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSnapshotStatePB, + FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB, }; -use crate::manager::{get_workspace_private_view_pbs, get_workspace_public_view_pbs, FolderUser}; -use crate::notification::{folder_notification_builder, FolderNotification}; +use crate::manager::{ + get_workspace_private_view_pbs, get_workspace_public_view_pbs, FolderUser, MutexFolder, +}; +use crate::notification::{send_notification, FolderNotification}; use collab::core::collab_state::SyncState; -use collab::lock::RwLock; use collab_folder::{ Folder, SectionChange, SectionChangeReceiver, TrashSectionChange, View, ViewChange, ViewChangeReceiver, }; -use lib_infra::sync_trace; - +use lib_dispatch::prelude::af_spawn; use std::collections::HashSet; -use std::str::FromStr; -use std::sync::Weak; +use std::sync::{Arc, Weak}; use tokio_stream::wrappers::WatchStream; use tokio_stream::StreamExt; use tracing::{event, trace, Level}; -use uuid::Uuid; /// Listen on the [ViewChange] after create/delete/update events happened pub(crate) fn subscribe_folder_view_changed( - workspace_id: Uuid, + workspace_id: String, mut rx: ViewChangeReceiver, - weak_mutex_folder: Weak<RwLock<Folder>>, + weak_mutex_folder: &Weak<MutexFolder>, user: Weak<dyn FolderUser>, ) { - tokio::spawn(async move { + let weak_mutex_folder = weak_mutex_folder.clone(); + af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(user) = user.upgrade() { if let Ok(actual_workspace_id) = user.workspace_id() { @@ -39,24 +38,18 @@ pub(crate) fn subscribe_folder_view_changed( } } - if let Some(lock) = weak_mutex_folder.upgrade() { - trace!("Did receive view change: {:?}", value); + if let Some(folder) = weak_mutex_folder.upgrade() { + tracing::trace!("Did receive view change: {:?}", value); match value { ViewChange::DidCreateView { view } => { notify_child_views_changed( view_pb_without_child_views(view.clone()), ChildViewChangeReason::Create, ); - let folder = lock.read().await; - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - sync_trace!("[Folder] create view: {:?}", view); - } + notify_parent_view_did_change(&workspace_id, folder.clone(), vec![view.parent_view_id]); }, ViewChange::DidDeleteView { views } => { for view in views { - sync_trace!("[Folder] delete view: {:?}", view); - notify_child_views_changed( view_pb_without_child_views(view.as_ref().clone()), ChildViewChangeReason::Delete, @@ -64,17 +57,16 @@ pub(crate) fn subscribe_folder_view_changed( } }, ViewChange::DidUpdate { view } => { - sync_trace!("[Folder] update view: {:?}", view); - notify_view_did_change(view.clone()); notify_child_views_changed( view_pb_without_child_views(view.clone()), ChildViewChangeReason::Update, ); - let folder = lock.read().await; - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - notify_parent_view_did_change(workspace_id, &folder, vec![parent_view_id]); - } + notify_parent_view_did_change( + &workspace_id, + folder.clone(), + vec![view.parent_view_id.clone()], + ); }, }; } @@ -82,12 +74,49 @@ pub(crate) fn subscribe_folder_view_changed( }); } +pub(crate) fn subscribe_folder_snapshot_state_changed( + workspace_id: String, + weak_mutex_folder: &Weak<MutexFolder>, + user: Weak<dyn FolderUser>, +) { + let weak_mutex_folder = weak_mutex_folder.clone(); + af_spawn(async move { + if let Some(mutex_folder) = weak_mutex_folder.upgrade() { + let stream = mutex_folder + .read() + .as_ref() + .map(|folder| folder.subscribe_snapshot_state()); + if let Some(mut state_stream) = stream { + while let Some(snapshot_state) = state_stream.next().await { + if let Some(user) = user.upgrade() { + if let Ok(actual_workspace_id) = user.workspace_id() { + if actual_workspace_id != workspace_id { + // break the loop when the workspace id is not matched. + break; + } + } + } + if let Some(new_snapshot_id) = snapshot_state.snapshot_id() { + tracing::debug!("Did create folder remote snapshot: {}", new_snapshot_id); + send_notification( + &workspace_id, + FolderNotification::DidUpdateFolderSnapshotState, + ) + .payload(FolderSnapshotStatePB { new_snapshot_id }) + .send(); + } + } + } + } + }); +} + pub(crate) fn subscribe_folder_sync_state_changed( - workspace_id: Uuid, + workspace_id: String, mut folder_sync_state_rx: WatchStream<SyncState>, user: Weak<dyn FolderUser>, ) { - tokio::spawn(async move { + af_spawn(async move { while let Some(state) = folder_sync_state_rx.next().await { if let Some(user) = user.upgrade() { if let Ok(actual_workspace_id) = user.workspace_id() { @@ -98,24 +127,22 @@ pub(crate) fn subscribe_folder_sync_state_changed( } } - folder_notification_builder( - workspace_id.to_string(), - FolderNotification::DidUpdateFolderSyncUpdate, - ) - .payload(FolderSyncStatePB::from(state)) - .send(); + send_notification(&workspace_id, FolderNotification::DidUpdateFolderSyncUpdate) + .payload(FolderSyncStatePB::from(state)) + .send(); } }); } /// Listen on the [TrashChange]s and notify the frontend some views were changed. pub(crate) fn subscribe_folder_trash_changed( - workspace_id: Uuid, + workspace_id: String, mut rx: SectionChangeReceiver, - weak_mutex_folder: Weak<RwLock<Folder>>, + weak_mutex_folder: &Weak<MutexFolder>, user: Weak<dyn FolderUser>, ) { - tokio::spawn(async move { + let weak_mutex_folder = weak_mutex_folder.clone(); + af_spawn(async move { while let Ok(value) = rx.recv().await { if let Some(user) = user.upgrade() { if let Ok(actual_workspace_id) = user.workspace_id() { @@ -126,7 +153,7 @@ pub(crate) fn subscribe_folder_trash_changed( } } - if let Some(lock) = weak_mutex_folder.upgrade() { + if let Some(folder) = weak_mutex_folder.upgrade() { let mut unique_ids = HashSet::new(); tracing::trace!("Did receive trash change: {:?}", value); @@ -136,21 +163,20 @@ pub(crate) fn subscribe_folder_trash_changed( TrashSectionChange::TrashItemAdded { ids } => ids, TrashSectionChange::TrashItemRemoved { ids } => ids, }; - let folder = lock.read().await; - let views = folder.get_views(&ids); - for view in views { - if let Ok(parent_view_id) = Uuid::from_str(&view.parent_view_id) { - unique_ids.insert(parent_view_id); + if let Some(folder) = folder.read().as_ref() { + let views = folder.views.get_views(&ids); + for view in views { + unique_ids.insert(view.parent_view_id.clone()); } + + let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); + send_notification("trash", FolderNotification::DidUpdateTrash) + .payload(repeated_trash) + .send(); } - let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); - folder_notification_builder("trash", FolderNotification::DidUpdateTrash) - .payload(repeated_trash) - .send(); - let parent_view_ids = unique_ids.into_iter().collect(); - notify_parent_view_did_change(workspace_id, &folder, parent_view_ids); + notify_parent_view_did_change(&workspace_id, folder.clone(), parent_view_ids); }, } } @@ -160,11 +186,13 @@ pub(crate) fn subscribe_folder_trash_changed( /// Notify the list of parent view ids that its child views were changed. #[tracing::instrument(level = "debug", skip(folder, parent_view_ids))] -pub(crate) fn notify_parent_view_did_change( - workspace_id: Uuid, - folder: &Folder, - parent_view_ids: Vec<Uuid>, +pub(crate) fn notify_parent_view_did_change<T: AsRef<str>>( + workspace_id: &str, + folder: Arc<MutexFolder>, + parent_view_ids: Vec<T>, ) -> Option<()> { + let folder = folder.read(); + let folder = folder.as_ref()?; let trash_ids = folder .get_all_trash_sections() .into_iter() @@ -172,23 +200,24 @@ pub(crate) fn notify_parent_view_did_change( .collect::<Vec<String>>(); for parent_view_id in parent_view_ids { + let parent_view_id = parent_view_id.as_ref(); + // if the view's parent id equal to workspace id. Then it will fetch the current // workspace views. Because the workspace is not a view stored in the views map. if parent_view_id == workspace_id { - notify_did_update_workspace(&workspace_id, folder); - notify_did_update_section_views(&workspace_id, folder); + notify_did_update_workspace(workspace_id, folder); + notify_did_update_section_views(workspace_id, folder); } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. - let parent_view_id = parent_view_id.to_string(); - let parent_view = folder.get_view(&parent_view_id)?; - let mut child_views = folder.get_views_belong_to(&parent_view_id); + let parent_view = folder.views.get_view(parent_view_id)?; + let mut child_views = folder.views.get_views_belong_to(parent_view_id); child_views.retain(|view| !trash_ids.contains(&view.id)); event!(Level::DEBUG, child_views_count = child_views.len()); // Post the notification let parent_view_pb = view_pb_with_child_views(parent_view, child_views); - folder_notification_builder(&parent_view_id, FolderNotification::DidUpdateView) + send_notification(parent_view_id, FolderNotification::DidUpdateView) .payload(parent_view_pb) .send(); } @@ -197,17 +226,18 @@ pub(crate) fn notify_parent_view_did_change( None } -pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Folder) { +pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) { let public_views = get_workspace_public_view_pbs(workspace_id, folder); let private_views = get_workspace_private_view_pbs(workspace_id, folder); - trace!( + tracing::trace!( "Did update section views: public len = {}, private len = {}", public_views.len(), private_views.len() ); + // TODO(Lucas.xu) - Only notify the section changed, not the public/private both. // Notify the public views - folder_notification_builder(workspace_id, FolderNotification::DidUpdateSectionViews) + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) .payload(SectionViewsPB { section: ViewSectionPB::Public, views: public_views, @@ -215,7 +245,7 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Fold .send(); // Notify the private views - folder_notification_builder(workspace_id, FolderNotification::DidUpdateSectionViews) + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) .payload(SectionViewsPB { section: ViewSectionPB::Private, views: private_views, @@ -223,9 +253,9 @@ pub(crate) fn notify_did_update_section_views(workspace_id: &Uuid, folder: &Fold .send(); } -pub(crate) fn notify_did_update_workspace(workspace_id: &Uuid, folder: &Folder) { +pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) { let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into(); - folder_notification_builder(workspace_id, FolderNotification::DidUpdateWorkspaceViews) + send_notification(workspace_id, FolderNotification::DidUpdateWorkspaceViews) .payload(repeated_view) .send(); } @@ -233,7 +263,7 @@ pub(crate) fn notify_did_update_workspace(workspace_id: &Uuid, folder: &Folder) fn notify_view_did_change(view: View) -> Option<()> { let view_id = view.id.clone(); let view_pb = view_pb_without_child_views(view); - folder_notification_builder(&view_id, FolderNotification::DidUpdateView) + send_notification(&view_id, FolderNotification::DidUpdateView) .payload(view_pb) .send(); None @@ -266,7 +296,7 @@ pub(crate) fn notify_child_views_changed(view_pb: ViewPB, reason: ChildViewChang }, } - folder_notification_builder(&parent_view_id, FolderNotification::DidUpdateChildViews) + send_notification(&parent_view_id, FolderNotification::DidUpdateChildViews) .payload(payload) .send(); } diff --git a/frontend/rust-lib/flowy-folder/src/manager_test_util.rs b/frontend/rust-lib/flowy-folder/src/manager_test_util.rs new file mode 100644 index 0000000000..4280c788d9 --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/manager_test_util.rs @@ -0,0 +1,32 @@ +use crate::manager::{FolderManager, FolderUser, MutexFolder}; +use crate::view_operation::FolderOperationHandlers; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_folder_pub::cloud::FolderCloudService; +use flowy_search_pub::entities::FolderIndexManager; +use std::sync::Arc; + +impl FolderManager { + pub fn get_mutex_folder(&self) -> Arc<MutexFolder> { + self.mutex_folder.clone() + } + + pub fn get_cloud_service(&self) -> Arc<dyn FolderCloudService> { + self.cloud_service.clone() + } + + pub fn get_user(&self) -> Arc<dyn FolderUser> { + self.user.clone() + } + + pub fn get_indexer(&self) -> Arc<dyn FolderIndexManager> { + self.folder_indexer.clone() + } + + pub fn get_collab_builder(&self) -> Arc<AppFlowyCollabBuilder> { + self.collab_builder.clone() + } + + pub fn get_operation_handlers(&self) -> FolderOperationHandlers { + self.operation_handlers.clone() + } +} diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index 5629ef4133..1ddcebcafd 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -1,7 +1,8 @@ use flowy_derive::ProtoBuf_Enum; use flowy_notification::NotificationBuilder; use lib_dispatch::prelude::ToBytes; -use tracing::trace; + +use crate::entities::{ViewPB, WorkspaceSettingPB}; const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; @@ -69,21 +70,28 @@ impl std::convert::From<i32> for FolderNotification { } } -#[tracing::instrument(level = "trace", skip_all)] -pub(crate) fn folder_notification_builder<T: ToString>( - id: T, - ty: FolderNotification, -) -> NotificationBuilder { - let id = id.to_string(); - trace!("folder_notification_builder: id = {id}, ty = {ty:?}"); - NotificationBuilder::new(&id, ty, FOLDER_OBSERVABLE_SOURCE) +#[tracing::instrument(level = "trace")] +pub(crate) fn send_notification(id: &str, ty: FolderNotification) -> NotificationBuilder { + NotificationBuilder::new(id, ty, FOLDER_OBSERVABLE_SOURCE) } /// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the /// user. Only one workspace can be opened at a time. const CURRENT_WORKSPACE: &str = "current-workspace"; -pub(crate) fn send_current_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) { - folder_notification_builder(CURRENT_WORKSPACE, ty) +pub(crate) fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) { + send_notification(CURRENT_WORKSPACE, ty) .payload(payload) .send(); } + +pub(crate) fn send_workspace_setting_notification( + workspace_id: String, + latest_view: Option<ViewPB>, +) -> Option<()> { + let setting = WorkspaceSettingPB { + workspace_id, + latest_view, + }; + send_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting); + None +} diff --git a/frontend/rust-lib/flowy-folder/src/publish_util.rs b/frontend/rust-lib/flowy-folder/src/publish_util.rs deleted file mode 100644 index 735614ffa4..0000000000 --- a/frontend/rust-lib/flowy-folder/src/publish_util.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::entities::ViewPB; -use flowy_folder_pub::entities::PublishViewInfo; -use regex::Regex; -use tracing::trace; - -fn replace_invalid_url_chars(input: &str) -> String { - let regex = Regex::new(r"[^\w-]").unwrap(); - regex.replace_all(input, "-").to_string() -} - -pub fn generate_publish_name(id: &str, name: &str) -> String { - let id_len = id.len(); - let name = replace_invalid_url_chars(name); - let name_len = name.len(); - // The backend limits the publish name to a maximum of 50 characters. - // If the combined length of the ID and the name exceeds 50 characters, - // we will truncate the name to ensure the final result is within the limit. - // The name should only contain alphanumeric characters and hyphens. - let result = format!("{}-{}", &name[..std::cmp::min(49 - id_len, name_len)], id); - trace!("generate_publish_name: {}", result); - result -} - -pub fn view_pb_to_publish_view(view: &ViewPB) -> PublishViewInfo { - PublishViewInfo { - view_id: view.id.clone(), - name: view.name.clone(), - layout: view.layout.clone().into(), - icon: view.icon.clone().map(|icon| icon.into()), - child_views: None, - extra: view.extra.clone(), - created_by: view.created_by, - last_edited_by: view.last_edited_by, - last_edited_time: view.last_edited, - created_at: view.create_time, - } -} diff --git a/frontend/rust-lib/flowy-folder/src/share/import.rs b/frontend/rust-lib/flowy-folder/src/share/import.rs index 6fd8d8feab..531461a232 100644 --- a/frontend/rust-lib/flowy-folder/src/share/import.rs +++ b/frontend/rust-lib/flowy-folder/src/share/import.rs @@ -1,41 +1,19 @@ use collab_folder::ViewLayout; -use std::fmt::{Display, Formatter}; -use uuid::Uuid; #[derive(Clone, Debug)] pub enum ImportType { HistoryDocument = 0, HistoryDatabase = 1, - Markdown = 2, - AFDatabase = 3, - CSV = 4, -} - -#[derive(Clone, Debug)] -pub struct ImportItem { - pub name: String, - pub data: ImportData, - pub view_layout: ViewLayout, - pub import_type: ImportType, -} - -#[derive(Clone, Debug)] -pub enum ImportData { - FilePath { file_path: String }, - Bytes { bytes: Vec<u8> }, -} - -impl Display for ImportData { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ImportData::FilePath { file_path } => write!(f, "file: {}", file_path), - ImportData::Bytes { .. } => write!(f, "binary"), - } - } + RawDatabase = 2, + CSV = 3, } #[derive(Clone, Debug)] pub struct ImportParams { - pub parent_view_id: Uuid, - pub items: Vec<ImportItem>, + pub parent_view_id: String, + pub name: String, + pub data: Option<Vec<u8>>, + pub file_path: Option<String>, + pub view_layout: ViewLayout, + pub import_type: ImportType, } diff --git a/frontend/rust-lib/flowy-folder/src/test_helper.rs b/frontend/rust-lib/flowy-folder/src/test_helper.rs new file mode 100644 index 0000000000..50e4b290ff --- /dev/null +++ b/frontend/rust-lib/flowy-folder/src/test_helper.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use flowy_folder_pub::cloud::gen_view_id; + +use crate::entities::{CreateViewParams, ViewLayoutPB, ViewSectionPB}; +use crate::manager::FolderManager; + +#[cfg(feature = "test_helper")] +impl FolderManager { + pub async fn create_test_grid_view( + &self, + app_id: &str, + name: &str, + ext: HashMap<String, String>, + ) -> String { + self + .create_test_view(app_id, name, ViewLayoutPB::Grid, ext) + .await + } + + pub async fn create_test_board_view( + &self, + app_id: &str, + name: &str, + ext: HashMap<String, String>, + ) -> String { + self + .create_test_view(app_id, name, ViewLayoutPB::Board, ext) + .await + } + + async fn create_test_view( + &self, + app_id: &str, + name: &str, + layout: ViewLayoutPB, + ext: HashMap<String, String>, + ) -> String { + let view_id = gen_view_id().to_string(); + let params = CreateViewParams { + parent_view_id: app_id.to_string(), + name: name.to_string(), + desc: "".to_string(), + layout, + view_id: view_id.clone(), + initial_data: vec![], + meta: ext, + set_as_current: true, + index: None, + section: Some(ViewSectionPB::Public), + }; + self.create_view_with_params(params).await.unwrap(); + view_id + } +} diff --git a/frontend/rust-lib/flowy-folder/src/user_default.rs b/frontend/rust-lib/flowy-folder/src/user_default.rs index 82fe1730fc..0f4fca0d54 100644 --- a/frontend/rust-lib/flowy-folder/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder/src/user_default.rs @@ -1,13 +1,13 @@ use std::sync::Arc; -use collab_folder::hierarchy_builder::{FlattedViews, NestedViewBuilder, ParentChildViews}; use collab_folder::{FolderData, RepeatedViewIdentifier, ViewIdentifier, Workspace}; +use flowy_folder_pub::folder_builder::{FlattedViews, NestedViewBuilder, ParentChildViews}; use tokio::sync::RwLock; use lib_infra::util::timestamp; use crate::entities::{view_pb_with_child_views, ViewPB}; -use crate::view_operation::{FolderOperationHandler, FolderOperationHandlers}; +use crate::view_operation::FolderOperationHandlers; pub struct DefaultFolderBuilder(); impl DefaultFolderBuilder { @@ -20,19 +20,7 @@ impl DefaultFolderBuilder { workspace_id.clone(), uid, ))); - - // Collect all handlers from the DashMap into a vector. - // - // - `DashMap::iter()` returns references to the stored values, which are not `Send` - // and can cause issues in an `async` context where thread-safety is required. - // - By cloning the values into a `Vec`, we ensure they are owned and implement - // `Send + Sync`, making them safe to use in asynchronous operations. - // - This avoids lifetime conflicts and allows the handlers to be used in the - // asynchronous loop without tying their lifetimes to the DashMap. - // - let handler_clones: Vec<Arc<dyn FolderOperationHandler + Send + Sync>> = - handlers.iter().map(|entry| entry.value().clone()).collect(); - for handler in handler_clones { + for handler in handlers.values() { let _ = handler .create_workspace_view(uid, workspace_view_builder.clone()) .await; @@ -40,12 +28,12 @@ impl DefaultFolderBuilder { let views = workspace_view_builder.write().await.build(); // Safe to unwrap because we have at least one view. check out the DocumentFolderOperation. - let first_view = views.first().unwrap().view.clone(); + let first_view = views.first().unwrap().parent_view.clone(); let first_level_views = views .iter() .map(|value| ViewIdentifier { - id: value.view.id.clone(), + id: value.parent_view.id.clone(), }) .collect::<Vec<_>>(); @@ -62,7 +50,7 @@ impl DefaultFolderBuilder { FolderData { workspace, current_view: first_view.id, - views: FlattedViews::flatten_views(views.into_inner()), + views: FlattedViews::flatten_views(views), favorites: Default::default(), recent: Default::default(), trash: Default::default(), @@ -74,11 +62,11 @@ impl DefaultFolderBuilder { impl From<&ParentChildViews> for ViewPB { fn from(value: &ParentChildViews) -> Self { view_pb_with_child_views( - Arc::new(value.view.clone()), + Arc::new(value.parent_view.clone()), value - .children + .child_views .iter() - .map(|v| Arc::new(v.view.clone())) + .map(|v| Arc::new(v.parent_view.clone())) .collect(), ) } diff --git a/frontend/rust-lib/flowy-folder/src/util.rs b/frontend/rust-lib/flowy-folder/src/util.rs index 98b87be52d..a56db33511 100644 --- a/frontend/rust-lib/flowy-folder/src/util.rs +++ b/frontend/rust-lib/flowy-folder/src/util.rs @@ -1,14 +1,30 @@ use crate::entities::UserFolderPB; +use collab_folder::Folder; use flowy_error::{ErrorCode, FlowyError}; -use uuid::Uuid; +use flowy_folder_pub::folder_builder::ParentChildViews; +use tracing::{event, instrument}; pub(crate) fn folder_not_init_error() -> FlowyError { FlowyError::internal().with_context("Folder not initialized") } -pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &Uuid) -> FlowyError { +pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError { FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB { uid, workspace_id: workspace_id.to_string(), }) } + +#[instrument(level = "debug", skip(folder, view))] +pub(crate) fn insert_parent_child_views(folder: &Folder, view: ParentChildViews) { + event!( + tracing::Level::DEBUG, + "Inserting view: {}, view children: {}", + view.parent_view.id, + view.child_views.len() + ); + folder.insert_view(view.parent_view, None); + for child_view in view.child_views { + insert_parent_child_views(folder, child_view); + } +} diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index 17919e07b1..10d173394c 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -1,76 +1,49 @@ -use async_trait::async_trait; -use bytes::Bytes; -use collab::entity::EncodedCollab; -use collab_entity::CollabType; -use collab_folder::hierarchy_builder::NestedViewBuilder; -pub use collab_folder::View; -use collab_folder::ViewLayout; -use dashmap::DashMap; -use flowy_error::FlowyError; use std::collections::HashMap; use std::sync::Arc; -use tokio::sync::RwLock; -use uuid::Uuid; +use bytes::Bytes; + +pub use collab_folder::View; +use collab_folder::ViewLayout; +use tokio::sync::RwLock; + +use flowy_error::FlowyError; + +use flowy_folder_pub::folder_builder::NestedViewBuilder; +use lib_infra::future::FutureResult; use lib_infra::util::timestamp; use crate::entities::{CreateViewParams, ViewLayoutPB}; -use crate::manager::FolderUser; use crate::share::ImportType; -#[derive(Debug, Clone)] -pub enum GatherEncodedCollab { - Document(EncodedCollab), - Database(DatabaseEncodedCollab), - Unknown, -} - -#[derive(Debug, Clone)] -pub struct DatabaseEncodedCollab { - pub database_encoded_collab: EncodedCollab, - pub database_row_encoded_collabs: HashMap<String, EncodedCollab>, - pub database_row_document_encoded_collabs: HashMap<String, EncodedCollab>, - pub database_relations: HashMap<String, String>, -} - -pub type ImportedData = (String, CollabType, EncodedCollab); +pub type ViewData = Bytes; /// The handler will be used to handler the folder operation for a specific /// view layout. Each [ViewLayout] will have a handler. So when creating a new /// view, the [ViewLayout] will be used to get the handler. -#[async_trait] -pub trait FolderOperationHandler: Send + Sync { - fn name(&self) -> &str; +/// +pub trait FolderOperationHandler { /// Create the view for the workspace of new user. /// Only called once when the user is created. - async fn create_workspace_view( + fn create_workspace_view( &self, _uid: i64, _workspace_view_builder: Arc<RwLock<NestedViewBuilder>>, - ) -> Result<(), FlowyError> { - Ok(()) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn open_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; /// Closes the view and releases the resources that this view has in /// the backend - async fn close_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; /// Called when the view is deleted. /// This will called after the view is deleted from the trash. - async fn delete_view(&self, view_id: &Uuid) -> Result<(), FlowyError>; + fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; /// Returns the [ViewData] that can be used to create the same view. - async fn duplicate_view(&self, view_id: &Uuid) -> Result<Bytes, FlowyError>; - - /// get the encoded collab data from the disk. - async fn gather_publish_encode_collab( - &self, - _user: &Arc<dyn FolderUser>, - _view_id: &Uuid, - ) -> Result<GatherEncodedCollab, FlowyError> { - Err(FlowyError::not_support()) - } + fn duplicate_view(&self, view_id: &str) -> FutureResult<ViewData, FlowyError>; /// Create a view with the data. /// @@ -87,55 +60,53 @@ pub trait FolderOperationHandler: Send + Sync { /// * `layout`: the layout of the view /// * `meta`: use to carry extra information. For example, the database view will use this /// to carry the reference database id. - /// - /// The return value is the [Option<EncodedCollab>] that can be used to create the view. - /// It can be used in syncing the view data to cloud. - async fn create_view_with_view_data( + fn create_view_with_view_data( &self, user_id: i64, - params: CreateViewParams, - ) -> Result<Option<EncodedCollab>, FlowyError>; + view_id: &str, + name: &str, + data: Vec<u8>, + layout: ViewLayout, + meta: HashMap<String, String>, + ) -> FutureResult<(), FlowyError>; /// Create a view with the pre-defined data. /// For example, the initial data of the grid/calendar/kanban board when /// you create a new view. - async fn create_default_view( + fn create_built_in_view( &self, user_id: i64, - parent_view_id: &Uuid, - view_id: &Uuid, + view_id: &str, name: &str, layout: ViewLayout, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), FlowyError>; /// Create a view by importing data - /// - /// The return value - async fn import_from_bytes( + fn import_from_bytes( &self, uid: i64, - view_id: &Uuid, + view_id: &str, name: &str, import_type: ImportType, bytes: Vec<u8>, - ) -> Result<Vec<ImportedData>, FlowyError>; + ) -> FutureResult<(), FlowyError>; /// Create a view by importing data from a file - async fn import_from_file_path( + fn import_from_file_path( &self, view_id: &str, name: &str, path: String, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), FlowyError>; /// Called when the view is updated. The handler is the `old` registered handler. - async fn did_update_view(&self, _old: &View, _new: &View) -> Result<(), FlowyError> { - Ok(()) + fn did_update_view(&self, _old: &View, _new: &View) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { Ok(()) }) } } pub type FolderOperationHandlers = - Arc<DashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>>>; + Arc<HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>>>; impl From<ViewLayoutPB> for ViewLayout { fn from(pb: ViewLayoutPB) -> Self { @@ -152,37 +123,18 @@ impl From<ViewLayoutPB> for ViewLayout { pub(crate) fn create_view(uid: i64, params: CreateViewParams, layout: ViewLayout) -> View { let time = timestamp(); View { - id: params.view_id.to_string(), - parent_view_id: params.parent_view_id.to_string(), + id: params.view_id, + parent_view_id: params.parent_view_id, name: params.name, + desc: params.desc, + children: Default::default(), created_at: time, is_favorite: false, layout, - icon: params.icon, + icon: None, created_by: Some(uid), last_edited_time: 0, last_edited_by: Some(uid), - extra: params.extra, - children: Default::default(), - is_locked: None, - } -} - -#[derive(Debug, Clone)] -pub enum ViewData { - /// Indicate the data is duplicated from another view. - DuplicateData(Bytes), - /// Indicate the data is created by the user. - Data(Bytes), - Empty, -} - -impl ViewData { - pub fn is_empty(&self) -> bool { - match self { - ViewData::DuplicateData(data) => data.is_empty(), - ViewData::Data(data) => data.is_empty(), - ViewData::Empty => true, - } + extra: None, } } diff --git a/frontend/rust-lib/flowy-notification/Cargo.toml b/frontend/rust-lib/flowy-notification/Cargo.toml index 3851546541..b459c9afbf 100644 --- a/frontend/rust-lib/flowy-notification/Cargo.toml +++ b/frontend/rust-lib/flowy-notification/Cargo.toml @@ -13,7 +13,7 @@ protobuf.workspace = true tracing.workspace = true bytes.workspace = true serde = { workspace = true, features = ["derive"] } -dashmap.workspace = true +dashmap = "5.5" tokio-util = "0.7" tokio = { workspace = true, features = ["time"] } @@ -25,3 +25,5 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] +web_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-notification/build.rs b/frontend/rust-lib/flowy-notification/build.rs index 8dfda67156..0be74ea9bc 100644 --- a/frontend/rust-lib/flowy-notification/build.rs +++ b/frontend/rust-lib/flowy-notification/build.rs @@ -1,4 +1,27 @@ fn main() { #[cfg(feature = "dart")] flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } + + #[cfg(feature = "web_ts")] + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "notification", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); } diff --git a/frontend/rust-lib/flowy-search-pub/Cargo.toml b/frontend/rust-lib/flowy-search-pub/Cargo.toml index 907942303d..631f2d2c83 100644 --- a/frontend/rust-lib/flowy-search-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-search-pub/Cargo.toml @@ -11,4 +11,4 @@ collab = { workspace = true } collab-folder = { workspace = true } flowy-error = { workspace = true } client-api = { workspace = true } -uuid.workspace = true \ No newline at end of file +futures = { workspace = true } diff --git a/frontend/rust-lib/flowy-search-pub/src/cloud.rs b/frontend/rust-lib/flowy-search-pub/src/cloud.rs index 8108cbed9a..f2ffb3c439 100644 --- a/frontend/rust-lib/flowy-search-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-search-pub/src/cloud.rs @@ -1,22 +1,12 @@ -pub use client_api::entity::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; +use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use lib_infra::async_trait::async_trait; -use uuid::Uuid; #[async_trait] pub trait SearchCloudService: Send + Sync + 'static { async fn document_search( &self, - workspace_id: &Uuid, + workspace_id: &str, query: String, ) -> Result<Vec<SearchDocumentResponseItem>, FlowyError>; - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec<SearchResult>, - ) -> Result<SearchSummaryResult, FlowyError>; } diff --git a/frontend/rust-lib/flowy-search-pub/src/entities.rs b/frontend/rust-lib/flowy-search-pub/src/entities.rs index 4cc625af46..65e23a9ddb 100644 --- a/frontend/rust-lib/flowy-search-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-search-pub/src/entities.rs @@ -1,51 +1,47 @@ +use std::any::Any; use std::sync::Arc; use collab::core::collab::IndexContentReceiver; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewLayout}; use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; pub struct IndexableData { pub id: String, pub data: String, pub icon: Option<ViewIcon>, pub layout: ViewLayout, - pub workspace_id: Uuid, + pub workspace_id: String, } impl IndexableData { - pub fn from_view(view: Arc<View>, workspace_id: Uuid) -> Self { + pub fn from_view(view: Arc<View>, workspace_id: String) -> Self { IndexableData { id: view.id.clone(), data: view.name.clone(), icon: view.icon.clone(), layout: view.layout.clone(), - workspace_id, + workspace_id: workspace_id.clone(), } } } -#[async_trait] pub trait IndexManager: Send + Sync { - async fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: Uuid); - async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; - async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; - async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>; - async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError>; - async fn is_indexed(&self) -> bool; + fn set_index_content_receiver(&self, rx: IndexContentReceiver, workspace_id: String); + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError>; + fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError>; + fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError>; + fn is_indexed(&self) -> bool; + + fn as_any(&self) -> &dyn Any; } -#[async_trait] pub trait FolderIndexManager: IndexManager { - async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; - - fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid); - + fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: String); fn index_view_changes( &self, views: Vec<Arc<View>>, changes: Vec<FolderViewChange>, - workspace_id: Uuid, + workspace_id: String, ); } diff --git a/frontend/rust-lib/flowy-search/Cargo.toml b/frontend/rust-lib/flowy-search/Cargo.toml index a803ad894f..dbd2b3ecf1 100644 --- a/frontend/rust-lib/flowy-search/Cargo.toml +++ b/frontend/rust-lib/flowy-search/Cargo.toml @@ -11,16 +11,20 @@ collab-folder = { workspace = true } flowy-derive.workspace = true flowy-error = { workspace = true, features = [ - "impl_from_sqlite", - "impl_from_dispatch_error", - "impl_from_collab_document", - "impl_from_tantivy", - "impl_from_serde", + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_collab_document", + "impl_from_tantivy", + "impl_from_serde", ] } +flowy-notification.workspace = true +flowy-sqlite.workspace = true flowy-user.workspace = true flowy-search-pub.workspace = true flowy-folder = { workspace = true } + bytes.workspace = true +futures.workspace = true lib-dispatch.workspace = true lib-infra = { workspace = true } protobuf.workspace = true @@ -28,18 +32,24 @@ serde.workspace = true serde_json.workspace = true tokio = { workspace = true, features = ["full", "rt-multi-thread", "tracing"] } tracing.workspace = true -derive_builder.workspace = true + +async-stream = "0.3.4" strsim = "0.11.0" strum_macros = "0.26.1" -tantivy.workspace = true -uuid.workspace = true -allo-isolate = { version = "^0.1", features = ["catch-unwind"] } -futures.workspace = true -tokio-stream.workspace = true -async-stream = "0.3.6" +tantivy = { version = "0.21.1" } +tempfile = "3.9.0" +validator = { version = "0.16.0", features = ["derive"] } + +diesel.workspace = true +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +diesel_migrations = { version = "2.1.0", features = ["sqlite"] } [build-dependencies] flowy-codegen.workspace = true +[dev-dependencies] +tempfile = "3.10.0" + [features] dart = ["flowy-codegen/dart"] +tauri_ts = ["flowy-codegen/ts"] diff --git a/frontend/rust-lib/flowy-search/build.rs b/frontend/rust-lib/flowy-search/build.rs index 77c0c8125b..2600d32fb7 100644 --- a/frontend/rust-lib/flowy-search/build.rs +++ b/frontend/rust-lib/flowy-search/build.rs @@ -1,7 +1,19 @@ +#[cfg(feature = "tauri_ts")] +use flowy_codegen::Project; + fn main() { + #[cfg(any(feature = "dart", feature = "tauri_ts"))] + let crate_name = env!("CARGO_PKG_NAME"); + #[cfg(feature = "dart")] { - flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); + flowy_codegen::protobuf_file::dart_gen(crate_name); + flowy_codegen::dart_event::gen(crate_name); + } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::protobuf_file::ts_gen(crate_name, crate_name, Project::Tauri); + flowy_codegen::ts_event::gen(crate_name, Project::Tauri); } } diff --git a/frontend/rust-lib/flowy-search/src/document/handler.rs b/frontend/rust-lib/flowy-search/src/document/handler.rs index 2127ef0d98..ffdafb8cc7 100644 --- a/frontend/rust-lib/flowy-search/src/document/handler.rs +++ b/frontend/rust-lib/flowy-search/src/document/handler.rs @@ -1,23 +1,14 @@ -use crate::entities::{ - CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, - SearchResponsePB, SearchSourcePB, SearchSummaryPB, -}; +use std::sync::Arc; + +use flowy_error::FlowyResult; +use flowy_folder::{manager::FolderManager, ViewLayout}; +use flowy_search_pub::cloud::SearchCloudService; +use lib_infra::async_trait::async_trait; + use crate::{ - entities::{ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB}, + entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResultPB}, services::manager::{SearchHandler, SearchType}, }; -use async_stream::stream; -use flowy_error::FlowyResult; -use flowy_folder::entities::ViewPB; -use flowy_folder::{manager::FolderManager, ViewLayout}; -use flowy_search_pub::cloud::{SearchCloudService, SearchResult}; -use lib_infra::async_trait::async_trait; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::Arc; -use tokio_stream::{self, Stream}; -use tracing::{trace, warn}; -use uuid::Uuid; pub struct DocumentSearchHandler { pub cloud_service: Arc<dyn SearchCloudService>, @@ -35,6 +26,7 @@ impl DocumentSearchHandler { } } } + #[async_trait] impl SearchHandler for DocumentSearchHandler { fn search_type(&self) -> SearchType { @@ -45,148 +37,63 @@ impl SearchHandler for DocumentSearchHandler { &self, query: String, filter: Option<SearchFilterPB>, - ) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> { - let cloud_service = self.cloud_service.clone(); - let folder_manager = self.folder_manager.clone(); + ) -> FlowyResult<Vec<SearchResultPB>> { + let filter = match filter { + Some(filter) => filter, + None => return Ok(vec![]), + }; - Box::pin(stream! { - // Exit early if there is no filter. - let filter = if let Some(f) = filter { - f - } else { - yield Ok(CreateSearchResultPBArgs::default().build().unwrap()); - return; - }; + let workspace_id = match filter.workspace_id { + Some(workspace_id) => workspace_id, + None => return Ok(vec![]), + }; - // Parse workspace id. - let workspace_id = match Uuid::from_str(&filter.workspace_id) { - Ok(id) => id, - Err(e) => { - yield Err(e.into()); - return; - } - }; + let results = self + .cloud_service + .document_search(&workspace_id, query) + .await?; - // Retrieve all available views. - let views = match folder_manager.get_all_views_pb().await { - Ok(views) => views, - Err(e) => { - yield Err(e); - return; - } - }; + // Grab all views from folder cache + // Notice that `get_all_view_pb` returns Views that don't include trashed and private views + let mut views = self.folder_manager.get_all_views_pb().await?.into_iter(); + let mut search_results: Vec<SearchResultPB> = vec![]; - // Execute document search. - yield Ok( - CreateSearchResultPBArgs::default().searching(true) - .build() - .unwrap(), - ); - - let result_items = match cloud_service.document_search(&workspace_id, query.clone()).await { - Ok(items) => items, - Err(e) => { - yield Err(e); - return; - } - }; - trace!("[Search] search result: {:?}", result_items); - - // Prepare input for search summary generation. - let summary_input: Vec<SearchResult> = result_items - .iter() - .map(|v| SearchResult { - object_id: v.object_id, - content: v.content.clone(), - }) - .collect(); - - // Build search response items. - let mut items: Vec<SearchResponseItemPB> = Vec::new(); - for item in &result_items { - if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) { - items.push(SearchResponseItemPB { - id: item.object_id.to_string(), - display_name: view.name.clone(), - icon: extract_icon(view), - workspace_id: item.workspace_id.to_string(), - content: item.content.clone()} - ); - } else { - warn!("No view found for search result: {:?}", item); - } - } - - // Yield primary search result. - let search_result = RepeatedSearchResponseItemPB { items }; - yield Ok( - CreateSearchResultPBArgs::default() - .searching(false) - .search_result(Some(search_result)) - .generating_ai_summary(!result_items.is_empty()) - .build() - .unwrap(), - ); - - if result_items.is_empty() { - return; - } - - // Generate and yield search summary. - match cloud_service.generate_search_summary(&workspace_id, query.clone(), summary_input).await { - Ok(summary_result) => { - trace!("[Search] search summary: {:?}", summary_result); - let summaries: Vec<SearchSummaryPB> = summary_result - .summaries - .into_iter() - .map(|v| { - let sources: Vec<SearchSourcePB> = v.sources - .iter() - .flat_map(|id| { - views.iter().find(|v| v.id == id.to_string()).map(|view| SearchSourcePB { - id: id.to_string(), - display_name: view.name.clone(), - icon: extract_icon(view), - }) - }) - .collect(); - - SearchSummaryPB { content: v.content, sources, highlights: v.highlights } + for result in results { + if let Some(view) = views.find(|v| v.id == result.object_id) { + // If there is no View for the result, we don't add it to the results + // If possible we will extract the icon to display for the result + let icon: Option<ResultIconPB> = match view.icon.clone() { + Some(view_icon) => Some(ResultIconPB::from(view_icon)), + None => { + let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); + Some(ResultIconPB { + ty: ResultIconTypePB::Icon, + value: view_layout_ty.to_string(), }) - .collect(); + }, + }; - let summary_result = RepeatedSearchSummaryPB { items: summaries }; - yield Ok( - CreateSearchResultPBArgs::default() - .search_summary(Some(summary_result)) - .generating_ai_summary(false) - .build() - .unwrap(), - ); - } - Err(e) => { - warn!("Failed to generate search summary: {:?}", e); - yield Ok( - CreateSearchResultPBArgs::default() - .generating_ai_summary(false) - .build() - .unwrap(), - ); - } + search_results.push(SearchResultPB { + index_type: IndexTypePB::Document, + view_id: result.object_id.clone(), + id: result.object_id.clone(), + data: view.name.clone(), + icon, + // We reverse the score, the cloud search score is based on + // 1 being the worst result, and closer to 0 being good result, that is + // the opposite of local search. + score: 1.0 - result.score, + workspace_id: result.workspace_id, + preview: result.preview, + }); } - }) - } -} + } -fn extract_icon(view: &ViewPB) -> Option<ResultIconPB> { - match view.icon.clone() { - Some(view_icon) => Some(ResultIconPB::from(view_icon)), - None => { - let view_layout_ty: i64 = ViewLayout::from(view.layout.clone()).into(); - Some(ResultIconPB { - ty: ResultIconTypePB::Icon, - value: view_layout_ty.to_string(), - }) - }, + Ok(search_results) + } + + /// Ignore for [DocumentSearchHandler] + fn index_count(&self) -> u64 { + 0 } } diff --git a/frontend/rust-lib/flowy-search/src/entities/index_type.rs b/frontend/rust-lib/flowy-search/src/entities/index_type.rs new file mode 100644 index 0000000000..77adc76a97 --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/entities/index_type.rs @@ -0,0 +1,31 @@ +use flowy_derive::ProtoBuf_Enum; + +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum IndexTypePB { + View = 0, + Document = 1, + DocumentBlock = 2, + DatabaseRow = 3, +} + +impl Default for IndexTypePB { + fn default() -> Self { + Self::View + } +} + +impl std::convert::From<IndexTypePB> for i32 { + fn from(notification: IndexTypePB) -> Self { + notification as i32 + } +} + +impl std::convert::From<i32> for IndexTypePB { + fn from(notification: i32) -> Self { + match notification { + 1 => IndexTypePB::View, + 2 => IndexTypePB::DocumentBlock, + _ => IndexTypePB::DatabaseRow, + } + } +} diff --git a/frontend/rust-lib/flowy-search/src/entities/mod.rs b/frontend/rust-lib/flowy-search/src/entities/mod.rs index dc6aaace08..b4d7c682b9 100644 --- a/frontend/rust-lib/flowy-search/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-search/src/entities/mod.rs @@ -1,8 +1,10 @@ +mod index_type; mod notification; mod query; mod result; mod search_filter; +pub use index_type::*; pub use notification::*; pub use query::*; pub use result::*; diff --git a/frontend/rust-lib/flowy-search/src/entities/notification.rs b/frontend/rust-lib/flowy-search/src/entities/notification.rs index 4f12305d9a..e05f46dd09 100644 --- a/frontend/rust-lib/flowy-search/src/entities/notification.rs +++ b/frontend/rust-lib/flowy-search/src/entities/notification.rs @@ -1,13 +1,17 @@ -use super::SearchResponsePB; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use super::SearchResultPB; + #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchStatePB { - #[pb(index = 1, one_of)] - pub response: Option<SearchResponsePB>, +pub struct SearchResultNotificationPB { + #[pb(index = 1)] + pub items: Vec<SearchResultPB>, #[pb(index = 2)] - pub search_id: String, + pub sends: u64, + + #[pb(index = 3, one_of)] + pub channel: Option<String>, } #[derive(ProtoBuf_Enum, Debug, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/query.rs b/frontend/rust-lib/flowy-search/src/entities/query.rs index 65c92ebed0..8ffbcf3d46 100644 --- a/frontend/rust-lib/flowy-search/src/entities/query.rs +++ b/frontend/rust-lib/flowy-search/src/entities/query.rs @@ -13,9 +13,13 @@ pub struct SearchQueryPB { #[pb(index = 3, one_of)] pub filter: Option<SearchFilterPB>, - #[pb(index = 4)] - pub search_id: String, - - #[pb(index = 5)] - pub stream_port: i64, + /// Used to identify the channel of the search + /// + /// This can be used to have multiple search notification listeners in place. + /// It is up to the client to decide how to handle this. + /// + /// If not set, then no channel is used. + /// + #[pb(index = 4, one_of)] + pub channel: Option<String>, } diff --git a/frontend/rust-lib/flowy-search/src/entities/result.rs b/frontend/rust-lib/flowy-search/src/entities/result.rs index a01f01b074..0f5ea4dc23 100644 --- a/frontend/rust-lib/flowy-search/src/entities/result.rs +++ b/frontend/rust-lib/flowy-search/src/entities/result.rs @@ -1,106 +1,55 @@ use collab_folder::{IconType, ViewIcon}; -use derive_builder::Builder; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_folder::entities::ViewIconPB; -#[derive(Debug, Default, ProtoBuf, Builder, Clone)] -#[builder(name = "CreateSearchResultPBArgs")] -#[builder(pattern = "mutable")] -pub struct SearchResponsePB { - #[pb(index = 1, one_of)] - #[builder(default)] - pub search_result: Option<RepeatedSearchResponseItemPB>, +use super::IndexTypePB; - #[pb(index = 2, one_of)] - #[builder(default)] - pub search_summary: Option<RepeatedSearchSummaryPB>, - - #[pb(index = 3, one_of)] - #[builder(default)] - pub local_search_result: Option<RepeatedLocalSearchResponseItemPB>, - - #[pb(index = 4)] - #[builder(default)] - pub searching: bool, - - #[pb(index = 5)] - #[builder(default)] - pub generating_ai_summary: bool, +#[derive(Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedSearchResultPB { + #[pb(index = 1)] + pub items: Vec<SearchResultPB>, } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedSearchSummaryPB { +pub struct SearchResultPB { #[pb(index = 1)] - pub items: Vec<SearchSummaryPB>, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchSummaryPB { - #[pb(index = 1)] - pub content: String, + pub index_type: IndexTypePB, #[pb(index = 2)] - pub sources: Vec<SearchSourcePB>, + pub view_id: String, #[pb(index = 3)] - pub highlights: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchSourcePB { - #[pb(index = 1)] pub id: String, - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] - pub icon: Option<ResultIconPB>, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedSearchResponseItemPB { - #[pb(index = 1)] - pub items: Vec<SearchResponseItemPB>, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct SearchResponseItemPB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] - pub icon: Option<ResultIconPB>, - #[pb(index = 4)] - pub workspace_id: String, + pub data: String, - #[pb(index = 5)] - pub content: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedLocalSearchResponseItemPB { - #[pb(index = 1)] - pub items: Vec<LocalSearchResponseItemPB>, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct LocalSearchResponseItemPB { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub display_name: String, - - #[pb(index = 3, one_of)] + #[pb(index = 5, one_of)] pub icon: Option<ResultIconPB>, - #[pb(index = 4)] + #[pb(index = 6)] + pub score: f64, + + #[pb(index = 7)] pub workspace_id: String, + + #[pb(index = 8, one_of)] + pub preview: Option<String>, +} + +impl SearchResultPB { + pub fn with_score(&self, score: f64) -> Self { + SearchResultPB { + index_type: self.index_type.clone(), + view_id: self.view_id.clone(), + id: self.id.clone(), + data: self.data.clone(), + icon: self.icon.clone(), + score, + workspace_id: self.workspace_id.clone(), + preview: self.preview.clone(), + } + } } #[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)] diff --git a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs index 2059971a0d..33031b3b2c 100644 --- a/frontend/rust-lib/flowy-search/src/entities/search_filter.rs +++ b/frontend/rust-lib/flowy-search/src/entities/search_filter.rs @@ -2,6 +2,6 @@ use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] pub struct SearchFilterPB { - #[pb(index = 1)] - pub workspace_id: String, + #[pb(index = 1, one_of)] + pub workspace_id: Option<String>, } diff --git a/frontend/rust-lib/flowy-search/src/event_handler.rs b/frontend/rust-lib/flowy-search/src/event_handler.rs index d79a719f6f..de611a078f 100644 --- a/frontend/rust-lib/flowy-search/src/event_handler.rs +++ b/frontend/rust-lib/flowy-search/src/event_handler.rs @@ -21,14 +21,7 @@ pub(crate) async fn search_handler( ) -> Result<(), FlowyError> { let query = data.into_inner(); let manager = upgrade_manager(manager)?; - manager - .perform_search( - query.search, - query.stream_port, - query.filter, - query.search_id, - ) - .await; + manager.perform_search(query.search, query.filter, query.channel); Ok(()) } diff --git a/frontend/rust-lib/flowy-search/src/folder/entities.rs b/frontend/rust-lib/flowy-search/src/folder/entities.rs index 1bb763b4a6..b3837668b8 100644 --- a/frontend/rust-lib/flowy-search/src/folder/entities.rs +++ b/frontend/rust-lib/flowy-search/src/folder/entities.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::entities::{LocalSearchResponseItemPB, ResultIconPB}; +use crate::entities::{IndexTypePB, ResultIconPB, SearchResultPB}; #[derive(Debug, Serialize, Deserialize)] pub struct FolderIndexData { @@ -11,7 +11,7 @@ pub struct FolderIndexData { pub workspace_id: String, } -impl From<FolderIndexData> for LocalSearchResponseItemPB { +impl From<FolderIndexData> for SearchResultPB { fn from(data: FolderIndexData) -> Self { let icon = if data.icon.is_empty() { None @@ -23,10 +23,14 @@ impl From<FolderIndexData> for LocalSearchResponseItemPB { }; Self { + index_type: IndexTypePB::View, + view_id: data.id.clone(), id: data.id, - display_name: data.title, + data: data.title, + score: 0.0, icon, workspace_id: data.workspace_id, + preview: None, } } } diff --git a/frontend/rust-lib/flowy-search/src/folder/handler.rs b/frontend/rust-lib/flowy-search/src/folder/handler.rs index e21ce1c98c..f92e17cda1 100644 --- a/frontend/rust-lib/flowy-search/src/folder/handler.rs +++ b/frontend/rust-lib/flowy-search/src/folder/handler.rs @@ -1,14 +1,12 @@ -use super::indexer::FolderIndexManagerImpl; -use crate::entities::{ - CreateSearchResultPBArgs, RepeatedLocalSearchResponseItemPB, SearchFilterPB, SearchResponsePB, +use crate::{ + entities::{SearchFilterPB, SearchResultPB}, + services::manager::{SearchHandler, SearchType}, }; -use crate::services::manager::{SearchHandler, SearchType}; -use async_stream::stream; use flowy_error::FlowyResult; use lib_infra::async_trait::async_trait; -use std::pin::Pin; use std::sync::Arc; -use tokio_stream::{self, Stream}; + +use super::indexer::FolderIndexManagerImpl; pub struct FolderSearchHandler { pub index_manager: Arc<FolderIndexManagerImpl>, @@ -30,26 +28,19 @@ impl SearchHandler for FolderSearchHandler { &self, query: String, filter: Option<SearchFilterPB>, - ) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> { - let index_manager = self.index_manager.clone(); + ) -> FlowyResult<Vec<SearchResultPB>> { + let mut results = self.index_manager.search(query, filter.clone())?; + if let Some(filter) = filter { + if let Some(workspace_id) = filter.workspace_id { + // Filter results by workspace ID + results.retain(|result| result.workspace_id == workspace_id); + } + } - Box::pin(stream! { - // Perform search (if search() returns a Result) - let mut items = match index_manager.search(query).await { - Ok(items) => items, - Err(err) => { - yield Err(err); - return; - } - }; + Ok(results) + } - if let Some(filter) = filter { - items.retain(|result| result.workspace_id == filter.workspace_id); - } - - // Build the search result. - let search_result = RepeatedLocalSearchResponseItemPB {items}; - yield Ok(CreateSearchResultPBArgs::default().local_search_result(Some(search_result)).build().unwrap()) - }) + fn index_count(&self) -> u64 { + self.index_manager.num_docs() } } diff --git a/frontend/rust-lib/flowy-search/src/folder/indexer.rs b/frontend/rust-lib/flowy-search/src/folder/indexer.rs index 59622852d5..5831e0871a 100644 --- a/frontend/rust-lib/flowy-search/src/folder/indexer.rs +++ b/frontend/rust-lib/flowy-search/src/folder/indexer.rs @@ -1,124 +1,190 @@ -use super::entities::FolderIndexData; -use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB}; -use crate::folder::schema::{ - FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, - FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, +use std::{ + any::Any, + collections::HashMap, + fs, + ops::Deref, + path::Path, + sync::{Arc, Mutex, MutexGuard, Weak}, +}; + +use crate::{ + entities::{ResultIconTypePB, SearchFilterPB, SearchResultPB}, + folder::schema::{ + FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME, + FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME, + }, }; use collab::core::collab::{IndexContent, IndexContentReceiver}; use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexContent, ViewLayout}; use flowy_error::{FlowyError, FlowyResult}; use flowy_search_pub::entities::{FolderIndexManager, IndexManager, IndexableData}; use flowy_user::services::authenticate_user::AuthenticateUser; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use std::{collections::HashMap, fs}; +use lib_dispatch::prelude::af_spawn; +use strsim::levenshtein; use tantivy::{ - collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document, - Index, IndexReader, IndexWriter, TantivyDocument, TantivyError, Term, + collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Index, + IndexReader, IndexWriter, Term, }; -use tokio::sync::RwLock; -use tracing::{error, info}; -use uuid::Uuid; -pub struct TantivyState { - pub path: PathBuf, - pub index: Index, - pub folder_schema: FolderSchema, - pub index_reader: IndexReader, - pub index_writer: IndexWriter, -} - -impl Drop for TantivyState { - fn drop(&mut self) { - tracing::trace!("Dropping TantivyState at {:?}", self.path); - } -} +use super::entities::FolderIndexData; #[derive(Clone)] pub struct FolderIndexManagerImpl { - auth_user: Weak<AuthenticateUser>, - state: Arc<RwLock<Option<TantivyState>>>, + folder_schema: Option<FolderSchema>, + index: Option<Index>, + index_reader: Option<IndexReader>, + index_writer: Option<Arc<Mutex<IndexWriter>>>, } +const FOLDER_INDEX_DIR: &str = "folder_index"; + impl FolderIndexManagerImpl { - pub fn new(auth_user: Weak<AuthenticateUser>) -> Self { - Self { - auth_user, - state: Arc::new(RwLock::new(None)), - } - } - - async fn with_writer<F, R>(&self, f: F) -> FlowyResult<R> - where - F: FnOnce(&mut IndexWriter, &FolderSchema) -> FlowyResult<R>, - { - let mut lock = self.state.write().await; - if let Some(ref mut state) = *lock { - f(&mut state.index_writer, &state.folder_schema) - } else { - Err(FlowyError::internal().with_context("Index not initialized. Call initialize first")) - } - } - - /// Initializes the state using the workspace directory. - async fn initialize(&self, workspace_id: &Uuid) -> FlowyResult<()> { - if let Some(state) = self.state.write().await.take() { - info!("Re-initializing folder indexer"); - drop(state); - } - - // Since the directory lock may not be immediately released, - // a workaround is implemented by waiting for 3 seconds before proceeding further. This delay helps - // to avoid errors related to trying to open an index directory while an IndexWriter is still active. - // - // Also, we don't need to initialize the indexer immediately. - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - - let auth_user = self - .auth_user - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("AuthenticateUser is not available"))?; - - let index_path = auth_user.get_index_path()?.join(workspace_id.to_string()); - if !index_path.exists() { - fs::create_dir_all(&index_path).map_err(|e| { - error!("Failed to create folder index directory: {:?}", e); - FlowyError::internal().with_context("Failed to create folder index") - })?; - } - - info!("Folder indexer initialized at: {:?}", index_path); - let folder_schema = FolderSchema::new(); - let dir = MmapDirectory::open(index_path.clone())?; - let index = Index::open_or_create(dir, folder_schema.schema.clone())?; - let index_reader = index.reader()?; - - let index_writer = match index.writer::<_>(50_000_000) { - Ok(index_writer) => index_writer, - Err(err) => { - if let TantivyError::LockFailure(_, _) = err { - error!( - "Failed to acquire lock for index writer: {:?}, retry later", - err - ); - tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; - } - index.writer::<_>(50_000_000)? + pub fn new(auth_user: Option<Weak<AuthenticateUser>>) -> Self { + let auth_user = match auth_user { + Some(auth_user) => auth_user, + None => { + return FolderIndexManagerImpl::empty(); }, }; - *self.state.write().await = Some(TantivyState { - path: index_path, - index, - folder_schema, - index_reader, - index_writer, - }); + // AuthenticateUser is required to get the index path + let authenticate_user = auth_user.upgrade(); + + // Storage path is the users data path with an index directory + // Eg. /usr/flowy-data/indexes + let storage_path = match authenticate_user { + Some(auth_user) => auth_user.get_index_path(), + None => { + tracing::error!("FolderIndexManager: AuthenticateUser is not available"); + return FolderIndexManagerImpl::empty(); + }, + }; + + // We check if the `folder_index` directory exists, if not we create it + let index_path = storage_path.join(Path::new(FOLDER_INDEX_DIR)); + if !index_path.exists() { + let res = fs::create_dir_all(&index_path); + if let Err(e) = res { + tracing::error!( + "FolderIndexManager failed to create index directory: {:?}", + e + ); + return FolderIndexManagerImpl::empty(); + } + } + + // The folder schema is used to define the fields of the index along + // with how they are stored and if the field is indexed + let folder_schema = FolderSchema::new(); + + // We open the existing or newly created folder_index directory + // This is required by the Tantivy Index, as it will use it to store + // and read index data + let index = match MmapDirectory::open(index_path) { + // We open or create an index that takes the directory r/w and the schema. + Ok(dir) => match Index::open_or_create(dir, folder_schema.schema.clone()) { + Ok(index) => index, + Err(e) => { + tracing::error!("FolderIndexManager failed to open index: {:?}", e); + return FolderIndexManagerImpl::empty(); + }, + }, + Err(e) => { + tracing::error!("FolderIndexManager failed to open index directory: {:?}", e); + return FolderIndexManagerImpl::empty(); + }, + }; + + // We only need one IndexReader per index + let index_reader = index.reader(); + let index_writer = index.writer(50_000_000); + + let (index_reader, index_writer) = match (index_reader, index_writer) { + (Ok(reader), Ok(writer)) => (reader, writer), + _ => { + tracing::error!("FolderIndexManager failed to instantiate index writer and/or reader"); + return FolderIndexManagerImpl::empty(); + }, + }; + + Self { + folder_schema: Some(folder_schema), + index: Some(index), + index_reader: Some(index_reader), + index_writer: Some(Arc::new(Mutex::new(index_writer))), + } + } + + fn index_all(&self, indexes: Vec<IndexableData>) -> Result<(), FlowyError> { + if indexes.is_empty() { + return Ok(()); + } + + let mut index_writer = self.get_index_writer()?; + let folder_schema = self.get_folder_schema()?; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + let workspace_id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + for data in indexes { + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data.clone(), + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.clone(), + ]); + } + + index_writer.commit()?; Ok(()) } + pub fn num_docs(&self) -> u64 { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs()) + .unwrap_or(0) + } + + fn empty() -> Self { + Self { + folder_schema: None, + index: None, + index_reader: None, + index_writer: None, + } + } + + fn get_index_writer(&self) -> FlowyResult<MutexGuard<IndexWriter>> { + match &self.index_writer { + Some(index_writer) => match index_writer.deref().lock() { + Ok(writer) => Ok(writer), + Err(e) => { + tracing::error!("FolderIndexManager failed to lock index writer: {:?}", e); + Err(FlowyError::folder_index_manager_unavailable()) + }, + }, + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + + fn get_folder_schema(&self) -> FlowyResult<FolderSchema> { + match &self.folder_schema { + Some(folder_schema) => Ok(folder_schema.clone()), + None => Err(FlowyError::folder_index_manager_unavailable()), + } + } + fn extract_icon( &self, view_icon: Option<ViewIcon>, @@ -134,99 +200,132 @@ impl FolderIndexManagerImpl { icon = Some(view_icon.value); } else { icon_ty = ResultIconTypePB::Icon.into(); - let layout_ty = view_layout as i64; + let layout_ty: i64 = view_layout.into(); icon = Some(layout_ty.to_string()); } + (icon, icon_ty) } - /// Simple implementation to index all given data by spawning async tasks. - fn index_all(&self, data_vec: Vec<IndexableData>) -> Result<(), FlowyError> { - for data in data_vec { - let indexer = self.clone(); - tokio::spawn(async move { - let _ = indexer.add_index(data).await; - }); - } - Ok(()) - } + pub fn search( + &self, + query: String, + _filter: Option<SearchFilterPB>, + ) -> Result<Vec<SearchResultPB>, FlowyError> { + let folder_schema = self.get_folder_schema()?; - /// Searches the index using the given query string. - pub async fn search(&self, query: String) -> Result<Vec<LocalSearchResponseItemPB>, FlowyError> { - let lock = self.state.read().await; - let state = lock + let (index, index_reader) = self + .index .as_ref() + .zip(self.index_reader.as_ref()) .ok_or_else(FlowyError::folder_index_manager_unavailable)?; - let schema = &state.folder_schema; - let index = &state.index; - let reader = &state.index_reader; - let title_field = schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let mut parser = QueryParser::for_index(index, vec![title_field]); - parser.set_field_fuzzy(title_field, true, 2, true); + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let built_query = parser.parse_query(&query)?; - let searcher = reader.searcher(); + let length = query.len(); + let distance: u8 = if length >= 2 { 2 } else { 1 }; + + let mut query_parser = QueryParser::for_index(&index.clone(), vec![title_field]); + query_parser.set_field_fuzzy(title_field, true, distance, true); + let built_query = query_parser.parse_query(&query.clone())?; + + let searcher = index_reader.searcher(); + let mut search_results: Vec<SearchResultPB> = vec![]; let top_docs = searcher.search(&built_query, &TopDocs::with_limit(10))?; - - let mut results = Vec::new(); for (_score, doc_address) in top_docs { - let doc: TantivyDocument = searcher.doc(doc_address)?; - let named_doc = doc.to_named_doc(&schema.schema); + let retrieved_doc = searcher.doc(doc_address)?; + let mut content = HashMap::new(); + let named_doc = folder_schema.schema.to_named_doc(&retrieved_doc); for (k, v) in named_doc.0 { content.insert(k, v[0].clone()); } - if !content.is_empty() { - let s = serde_json::to_string(&content)?; - let result: LocalSearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into(); - results.push(result); + + if content.is_empty() { + continue; } + + let s = serde_json::to_string(&content)?; + let result: SearchResultPB = serde_json::from_str::<FolderIndexData>(&s)?.into(); + let score = self.score_result(&query, &result.data); + search_results.push(result.with_score(score)); } - Ok(results) + Ok(search_results) + } + + // Score result by distance + fn score_result(&self, query: &str, term: &str) -> f64 { + let distance = levenshtein(query, term) as f64; + 1.0 / (distance + 1.0) + } + + fn get_schema_fields(&self) -> Result<(Field, Field, Field, Field, Field), FlowyError> { + let folder_schema = match self.folder_schema.clone() { + Some(schema) => schema, + _ => return Err(FlowyError::folder_index_manager_unavailable()), + }; + + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; + let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; + let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; + let workspace_id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + + Ok(( + id_field, + title_field, + icon_field, + icon_ty_field, + workspace_id_field, + )) } } -#[async_trait] impl IndexManager for FolderIndexManagerImpl { - async fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: Uuid) { + fn is_indexed(&self) -> bool { + self + .index_reader + .clone() + .map(|reader| reader.searcher().num_docs() > 0) + .unwrap_or(false) + } + + fn set_index_content_receiver(&self, mut rx: IndexContentReceiver, workspace_id: String) { let indexer = self.clone(); - let wid = workspace_id; - tokio::spawn(async move { + let wid = workspace_id.clone(); + af_spawn(async move { while let Ok(msg) = rx.recv().await { match msg { IndexContent::Create(value) => match serde_json::from_value::<ViewIndexContent>(value) { Ok(view) => { - let _ = indexer - .add_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }) - .await; + let _ = indexer.add_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid.clone(), + }); }, - Err(err) => tracing::error!("FolderIndexManager error deserialize (create): {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), }, IndexContent::Update(value) => match serde_json::from_value::<ViewIndexContent>(value) { Ok(view) => { - let _ = indexer - .update_index(IndexableData { - id: view.id, - data: view.name, - icon: view.icon, - layout: view.layout, - workspace_id: wid, - }) - .await; + let _ = indexer.update_index(IndexableData { + id: view.id, + data: view.name, + icon: view.icon, + layout: view.layout, + workspace_id: wid.clone(), + }); }, - Err(err) => error!("FolderIndexManager error deserialize (update): {:?}", err), + Err(err) => tracing::error!("FolderIndexManager error deserialize: {:?}", err), }, IndexContent::Delete(ids) => { - if let Err(e) = indexer.remove_indices(ids).await { - error!("FolderIndexManager error (delete): {:?}", e); + if let Err(e) = indexer.remove_indices(ids) { + tracing::error!("FolderIndexManager error deserialize: {:?}", e); } }, } @@ -234,107 +333,100 @@ impl IndexManager for FolderIndexManagerImpl { }); } - async fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { + fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + self.get_schema_fields()?; + + let delete_term = Term::from_field_text(id_field, &data.id.clone()); + + // Remove old index + index_writer.delete_term(delete_term); + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - self - .with_writer(|index_writer, folder_schema| { - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - get_schema_fields(folder_schema)?; - let _ = index_writer.add_document(doc![ - id_field => data.id, - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - index_writer.commit()?; - Ok(()) - }) - .await?; + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id.clone(), + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id.clone(), + ]); + + index_writer.commit()?; Ok(()) } - async fn update_index(&self, data: IndexableData) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = - get_schema_fields(folder_schema)?; - let delete_term = Term::from_field_text(id_field, &data.id); - index_writer.delete_term(delete_term); + fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; - let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); - let _ = index_writer.add_document(doc![ - id_field => data.id, - title_field => data.data, - icon_field => icon.unwrap_or_default(), - icon_ty_field => icon_ty, - workspace_id_field => data.workspace_id.to_string(), - ]); - - index_writer.commit()?; - Ok(()) - }) - .await?; - - Ok(()) - } - - async fn remove_indices(&self, ids: Vec<String>) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - for id in ids { - let delete_term = Term::from_field_text(id_field, &id); - index_writer.delete_term(delete_term); - } - - index_writer.commit()?; - Ok(()) - }) - .await?; - - Ok(()) - } - - async fn remove_indices_for_workspace(&self, workspace_id: Uuid) -> Result<(), FlowyError> { - self - .with_writer(|index_writer, folder_schema| { - let id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - - let delete_term = Term::from_field_text(id_field, &workspace_id.to_string()); - index_writer.delete_term(delete_term); - index_writer.commit()?; - Ok(()) - }) - .await?; - Ok(()) - } - - async fn is_indexed(&self) -> bool { - let lock = self.state.read().await; - if let Some(ref state) = *lock { - state.index_reader.searcher().num_docs() > 0 - } else { - false + let folder_schema = self.get_folder_schema()?; + let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; + for id in ids { + let delete_term = Term::from_field_text(id_field, &id); + index_writer.delete_term(delete_term); } + + index_writer.commit()?; + + Ok(()) + } + + fn add_index(&self, data: IndexableData) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let (id_field, title_field, icon_field, icon_ty_field, workspace_id_field) = + self.get_schema_fields()?; + + let (icon, icon_ty) = self.extract_icon(data.icon, data.layout); + + // Add new index + let _ = index_writer.add_document(doc![ + id_field => data.id, + title_field => data.data, + icon_field => icon.unwrap_or_default(), + icon_ty_field => icon_ty, + workspace_id_field => data.workspace_id, + ]); + + index_writer.commit()?; + + Ok(()) + } + + /// Removes all indexes that are related by workspace id. This is useful + /// for cleaning indexes when eg. removing/leaving a workspace. + /// + fn remove_indices_for_workspace(&self, workspace_id: String) -> Result<(), FlowyError> { + let mut index_writer = self.get_index_writer()?; + + let folder_schema = self.get_folder_schema()?; + let id_field = folder_schema + .schema + .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; + let delete_term = Term::from_field_text(id_field, &workspace_id); + index_writer.delete_term(delete_term); + + index_writer.commit()?; + + Ok(()) + } + + fn as_any(&self) -> &dyn Any { + self } } -#[async_trait] impl FolderIndexManager for FolderIndexManagerImpl { - async fn initialize(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - self.initialize(workspace_id).await?; - Ok(()) - } - - fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: Uuid) { + fn index_all_views(&self, views: Vec<Arc<View>>, workspace_id: String) { let indexable_data = views .into_iter() - .map(|view| IndexableData::from_view(view, workspace_id)) + .map(|view| IndexableData::from_view(view, workspace_id.clone())) .collect(); + let _ = self.index_all(indexable_data); } @@ -342,56 +434,29 @@ impl FolderIndexManager for FolderIndexManagerImpl { &self, views: Vec<Arc<View>>, changes: Vec<FolderViewChange>, - workspace_id: Uuid, + workspace_id: String, ) { let mut views_iter = views.into_iter(); for change in changes { match change { FolderViewChange::Inserted { view_id } => { - if let Some(view) = views_iter.find(|view| view.id == view_id) { - let indexable_data = IndexableData::from_view(view, workspace_id); - let f = self.clone(); - tokio::spawn(async move { - let _ = f.add_index(indexable_data).await; - }); + let view = views_iter.find(|view| view.id == view_id); + if let Some(view) = view { + let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let _ = self.add_index(indexable_data); } }, FolderViewChange::Updated { view_id } => { - if let Some(view) = views_iter.find(|view| view.id == view_id) { - let indexable_data = IndexableData::from_view(view, workspace_id); - let f = self.clone(); - tokio::spawn(async move { - let _ = f.update_index(indexable_data).await; - }); + let view = views_iter.find(|view| view.id == view_id); + if let Some(view) = view { + let indexable_data = IndexableData::from_view(view, workspace_id.clone()); + let _ = self.update_index(indexable_data); } }, FolderViewChange::Deleted { view_ids } => { - let f = self.clone(); - tokio::spawn(async move { - let _ = f.remove_indices(view_ids).await; - }); + let _ = self.remove_indices(view_ids); }, - } + }; } } } - -fn get_schema_fields( - folder_schema: &FolderSchema, -) -> Result<(Field, Field, Field, Field, Field), FlowyError> { - let id_field = folder_schema.schema.get_field(FOLDER_ID_FIELD_NAME)?; - let title_field = folder_schema.schema.get_field(FOLDER_TITLE_FIELD_NAME)?; - let icon_field = folder_schema.schema.get_field(FOLDER_ICON_FIELD_NAME)?; - let icon_ty_field = folder_schema.schema.get_field(FOLDER_ICON_TY_FIELD_NAME)?; - let workspace_id_field = folder_schema - .schema - .get_field(FOLDER_WORKSPACE_ID_FIELD_NAME)?; - - Ok(( - id_field, - title_field, - icon_field, - icon_ty_field, - workspace_id_field, - )) -} diff --git a/frontend/rust-lib/flowy-search/src/services/manager.rs b/frontend/rust-lib/flowy-search/src/services/manager.rs index a71449d5d2..aec33a5b60 100644 --- a/frontend/rust-lib/flowy-search/src/services/manager.rs +++ b/frontend/rust-lib/flowy-search/src/services/manager.rs @@ -1,13 +1,12 @@ -use crate::entities::{SearchFilterPB, SearchResponsePB, SearchStatePB}; -use allo_isolate::Isolate; -use flowy_error::FlowyResult; -use lib_infra::async_trait::async_trait; -use lib_infra::isolate_stream::{IsolateSink, SinkExt}; use std::collections::HashMap; -use std::pin::Pin; use std::sync::Arc; -use tokio_stream::{self, Stream, StreamExt}; -use tracing::{error, trace}; + +use super::notifier::{SearchNotifier, SearchResultChanged, SearchResultReceiverRunner}; +use crate::entities::{SearchFilterPB, SearchResultNotificationPB, SearchResultPB}; +use flowy_error::FlowyResult; +use lib_dispatch::prelude::af_spawn; +use lib_infra::async_trait::async_trait; +use tokio::sync::broadcast; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum SearchType { @@ -20,12 +19,15 @@ pub trait SearchHandler: Send + Sync + 'static { /// returns the type of search this handler is responsible for fn search_type(&self) -> SearchType; - /// performs a search and returns a stream of results + /// performs a search and returns the results async fn perform_search( &self, query: String, filter: Option<SearchFilterPB>, - ) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>>; + ) -> FlowyResult<Vec<SearchResultPB>>; + + /// returns the number of indexed objects + fn index_count(&self) -> u64; } /// The [SearchManager] is used to inject multiple [SearchHandler]'s @@ -34,7 +36,7 @@ pub trait SearchHandler: Send + Sync + 'static { /// pub struct SearchManager { pub handlers: HashMap<SearchType, Arc<dyn SearchHandler>>, - current_search: Arc<tokio::sync::Mutex<Option<String>>>, + notifier: SearchNotifier, } impl SearchManager { @@ -44,87 +46,44 @@ impl SearchManager { .map(|handler| (handler.search_type(), handler)) .collect(); - Self { - handlers, - current_search: Arc::new(tokio::sync::Mutex::new(None)), - } + // Initialize Search Notifier + let (notifier, _) = broadcast::channel(100); + af_spawn(SearchResultReceiverRunner(Some(notifier.subscribe())).run()); + + Self { handlers, notifier } } pub fn get_handler(&self, search_type: SearchType) -> Option<&Arc<dyn SearchHandler>> { self.handlers.get(&search_type) } - pub async fn perform_search( + pub fn perform_search( &self, query: String, - stream_port: i64, filter: Option<SearchFilterPB>, - search_id: String, + channel: Option<String>, ) { - // Cancel previous search by updating current_search - *self.current_search.lock().await = Some(search_id.clone()); - + let max: usize = self.handlers.len(); let handlers = self.handlers.clone(); - let sink = IsolateSink::new(Isolate::new(stream_port)); - let mut join_handles = vec![]; - let current_search = self.current_search.clone(); - - tracing::info!("[Search] perform search: {}", query); for (_, handler) in handlers { - let mut clone_sink = sink.clone(); - let query = query.clone(); - let filter = filter.clone(); - let search_id = search_id.clone(); - let current_search = current_search.clone(); + let q = query.clone(); + let f = filter.clone(); + let ch = channel.clone(); + let notifier = self.notifier.clone(); - let handle = tokio::spawn(async move { - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] cancel search: {}", query); - return; - } + af_spawn(async move { + let res = handler.perform_search(q, f).await; - let mut stream = handler.perform_search(query.clone(), filter).await; - while let Some(Ok(search_result)) = stream.next().await { - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] discard search stream: {}", query); - return; - } + let items = res.unwrap_or_default(); - let resp = SearchStatePB { - response: Some(search_result), - search_id: search_id.clone(), - }; - if let Ok::<Vec<u8>, _>(data) = resp.try_into() { - if let Err(err) = clone_sink.send(data).await { - error!("Failed to send search result: {}", err); - break; - } - } - } - - if !is_current_search(¤t_search, &search_id).await { - trace!("[Search] discard search result: {}", query); - return; - } - - let resp = SearchStatePB { - response: None, - search_id: search_id.clone(), + let notification = SearchResultNotificationPB { + items, + sends: max as u64, + channel: ch, }; - if let Ok::<Vec<u8>, _>(data) = resp.try_into() { - let _ = clone_sink.send(data).await; - } + + let _ = notifier.send(SearchResultChanged::SearchResultUpdate(notification)); }); - join_handles.push(handle); } - futures::future::join_all(join_handles).await; } } - -async fn is_current_search( - current_search: &Arc<tokio::sync::Mutex<Option<String>>>, - search_id: &str, -) -> bool { - let current = current_search.lock().await; - current.as_ref().map_or(false, |id| id == search_id) -} diff --git a/frontend/rust-lib/flowy-search/src/services/mod.rs b/frontend/rust-lib/flowy-search/src/services/mod.rs index ff8de9eb9a..2a417e6c62 100644 --- a/frontend/rust-lib/flowy-search/src/services/mod.rs +++ b/frontend/rust-lib/flowy-search/src/services/mod.rs @@ -1 +1,2 @@ pub mod manager; +pub mod notifier; diff --git a/frontend/rust-lib/flowy-search/src/services/notifier.rs b/frontend/rust-lib/flowy-search/src/services/notifier.rs new file mode 100644 index 0000000000..abbf5d4b0c --- /dev/null +++ b/frontend/rust-lib/flowy-search/src/services/notifier.rs @@ -0,0 +1,61 @@ +use async_stream::stream; +use flowy_notification::NotificationBuilder; +use futures::stream::StreamExt; +use tokio::sync::broadcast; + +use crate::entities::{SearchNotification, SearchResultNotificationPB}; + +const SEARCH_OBSERVABLE_SOURCE: &str = "Search"; +const SEARCH_ID: &str = "SEARCH_IDENTIFIER"; + +#[derive(Clone)] +pub enum SearchResultChanged { + SearchResultUpdate(SearchResultNotificationPB), +} + +pub type SearchNotifier = broadcast::Sender<SearchResultChanged>; + +pub(crate) struct SearchResultReceiverRunner( + pub(crate) Option<broadcast::Receiver<SearchResultChanged>>, +); + +impl SearchResultReceiverRunner { + pub(crate) async fn run(mut self) { + let mut receiver = self.0.take().expect("Only take once"); + let stream = stream! { + while let Ok(changed) = receiver.recv().await { + yield changed; + } + }; + stream + .for_each(|changed| async { + match changed { + SearchResultChanged::SearchResultUpdate(notification) => { + send_notification( + SEARCH_ID, + SearchNotification::DidUpdateResults, + notification.channel.clone(), + ) + .payload(notification) + .send(); + }, + } + }) + .await; + } +} + +#[tracing::instrument(level = "trace")] +pub fn send_notification( + id: &str, + ty: SearchNotification, + channel: Option<String>, +) -> NotificationBuilder { + let observable_source = &format!( + "{}{}", + SEARCH_OBSERVABLE_SOURCE, + channel.unwrap_or_default() + ); + + NotificationBuilder::new(id, ty, observable_source) +} diff --git a/frontend/rust-lib/flowy-search/tests/tantivy_test.rs b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs index f68b06d610..b07853c7de 100644 --- a/frontend/rust-lib/flowy-search/tests/tantivy_test.rs +++ b/frontend/rust-lib/flowy-search/tests/tantivy_test.rs @@ -47,7 +47,7 @@ fn search_folder_test() { for (_score, doc_address) in top_docs { // Retrieve the actual content of documents given its `doc_address`. - let retrieved_doc: TantivyDocument = searcher.doc(doc_address).unwrap(); - println!("{}", retrieved_doc.to_json(&schema)); + let retrieved_doc = searcher.doc(doc_address).unwrap(); + println!("{}", schema.to_json(&retrieved_doc)); } } diff --git a/frontend/rust-lib/flowy-server-pub/src/lib.rs b/frontend/rust-lib/flowy-server-pub/src/lib.rs index ee43b3c40c..4736587f4e 100644 --- a/frontend/rust-lib/flowy-server-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-server-pub/src/lib.rs @@ -28,12 +28,15 @@ if_wasm! { } } +pub mod supabase_config; + pub const CLOUT_TYPE_STR: &str = "APPFLOWY_CLOUD_ENV_CLOUD_TYPE"; #[derive(Deserialize_repr, Debug, Clone, PartialEq, Eq)] #[repr(u8)] pub enum AuthenticatorType { Local = 0, + Supabase = 1, AppFlowyCloud = 2, } @@ -47,6 +50,7 @@ impl AuthenticatorType { fn from_str(s: &str) -> Self { match s { "0" => AuthenticatorType::Local, + "1" => AuthenticatorType::Supabase, "2" => AuthenticatorType::AppFlowyCloud, _ => AuthenticatorType::Local, } diff --git a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs index 9c74850fcd..d75c30a673 100644 --- a/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs +++ b/frontend/rust-lib/flowy-server-pub/src/native/af_cloud_config.rs @@ -7,17 +7,12 @@ use flowy_error::{ErrorCode, FlowyError}; pub const APPFLOWY_CLOUD_BASE_URL: &str = "APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL"; pub const APPFLOWY_CLOUD_WS_BASE_URL: &str = "APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL"; pub const APPFLOWY_CLOUD_GOTRUE_URL: &str = "APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL"; -pub const APPFLOWY_ENABLE_SYNC_TRACE: &str = "APPFLOWY_ENABLE_SYNC_TRACE"; #[derive(Debug, Serialize, Deserialize, Clone, Default)] pub struct AFCloudConfiguration { pub base_url: String, pub ws_base_url: String, pub gotrue_url: String, - #[serde(default)] - pub enable_sync_trace: bool, - #[serde(default)] - pub maximum_upload_file_size_in_bytes: Option<u64>, } impl Display for AFCloudConfiguration { @@ -58,16 +53,10 @@ impl AFCloudConfiguration { ); } - let enable_sync_trace = std::env::var(APPFLOWY_ENABLE_SYNC_TRACE) - .map(|v| v == "true" || v == "1") - .unwrap_or(true); - Ok(Self { base_url, ws_base_url, gotrue_url, - enable_sync_trace, - maximum_upload_file_size_in_bytes: None, }) } @@ -76,13 +65,5 @@ impl AFCloudConfiguration { std::env::set_var(APPFLOWY_CLOUD_BASE_URL, &self.base_url); std::env::set_var(APPFLOWY_CLOUD_WS_BASE_URL, &self.ws_base_url); std::env::set_var(APPFLOWY_CLOUD_GOTRUE_URL, &self.gotrue_url); - std::env::set_var( - APPFLOWY_ENABLE_SYNC_TRACE, - if self.enable_sync_trace { - "true" - } else { - "false" - }, - ); } } diff --git a/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs b/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs new file mode 100644 index 0000000000..90dbe39bc5 --- /dev/null +++ b/frontend/rust-lib/flowy-server-pub/src/supabase_config.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +use flowy_error::{ErrorCode, FlowyError}; + +pub const SUPABASE_URL: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_URL"; +pub const SUPABASE_ANON_KEY: &str = "APPFLOWY_CLOUD_ENV_SUPABASE_ANON_KEY"; + +/// The configuration for the postgres database. It supports deserializing from the json string that +/// passed from the frontend application. [AppFlowyEnv::parser] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct SupabaseConfiguration { + /// The url of the supabase server. + pub url: String, + /// The key of the supabase server. + pub anon_key: String, +} + +impl SupabaseConfiguration { + pub fn from_env() -> Result<Self, FlowyError> { + let url = std::env::var(SUPABASE_URL) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_URL"))?; + + let anon_key = std::env::var(SUPABASE_ANON_KEY) + .map_err(|_| FlowyError::new(ErrorCode::InvalidAuthConfig, "Missing SUPABASE_ANON_KEY"))?; + + if url.is_empty() || anon_key.is_empty() { + return Err(FlowyError::new( + ErrorCode::InvalidAuthConfig, + "Missing SUPABASE_URL or SUPABASE_ANON_KEY", + )); + } + + Ok(Self { url, anon_key }) + } + + /// Write the configuration to the environment variables. + pub fn write_env(&self) { + std::env::set_var(SUPABASE_URL, &self.url); + std::env::set_var(SUPABASE_ANON_KEY, &self.anon_key); + } +} diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index c8710470b0..184731f204 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -12,22 +12,26 @@ crate-type = ["cdylib", "rlib"] tracing.workspace = true futures.workspace = true futures-util = "0.3.26" +reqwest = { version = "0.11.20", features = ["native-tls-vendored", "multipart", "blocking"] } +hyper = "0.14" serde.workspace = true serde_json.workspace = true thiserror = "1.0" tokio = { workspace = true, features = ["sync"] } +parking_lot.workspace = true lazy_static = "1.4.0" bytes = { workspace = true, features = ["serde"] } +tokio-retry = "0.3" anyhow.workspace = true -arc-swap.workspace = true uuid.workspace = true +chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } collab = { workspace = true } collab-plugins = { workspace = true } collab-document = { workspace = true } collab-entity = { workspace = true } collab-folder = { workspace = true } -collab-database = { workspace = true } -collab-user = { workspace = true } +hex = "0.4.3" +postgrest = "1.0" lib-infra = { workspace = true } flowy-user-pub = { workspace = true } flowy-folder-pub = { workspace = true } @@ -36,25 +40,26 @@ flowy-document-pub = { workspace = true } flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqwest", "impl_from_url", "impl_from_appflowy_cloud"] } flowy-server-pub = { workspace = true } flowy-search-pub = { workspace = true } +flowy-encrypt = { workspace = true } flowy-storage = { workspace = true } -flowy-storage-pub = { workspace = true } -flowy-ai-pub = { workspace = true } +flowy-chat-pub = { workspace = true } +mime_guess = "2.0" +url = "2.4" tokio-util = "0.7" tokio-stream = { workspace = true, features = ["sync"] } +lib-dispatch = { workspace = true } +yrs.workspace = true rand = "0.8.5" semver = "1.0.23" -flowy-sqlite = { workspace = true } -flowy-ai = { workspace = true } -chrono.workspace = true [dependencies.client-api] workspace = true features = [ - "collab-sync", - "test_util", - "enable_brotli", - # Uncomment the following line to enable verbose logging for sync - # "sync_verbose_log", + "collab-sync", + "test_util", + "enable_brotli", + # Uncomment the following line to enable verbose logging for sync + # "sync_verbose_log", ] [dev-dependencies] diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs index 31114629ac..b0f09b1530 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/define.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/define.rs @@ -1,11 +1,4 @@ -use collab_plugins::CollabKVDB; -use flowy_ai_pub::user_service::AIUserService; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use lib_infra::async_trait::async_trait; -use std::path::PathBuf; -use std::sync::{Arc, Weak}; -use uuid::Uuid; +use flowy_error::FlowyResult; pub const USER_SIGN_IN_URL: &str = "sign_in_url"; pub const USER_UUID: &str = "uuid"; @@ -13,52 +6,7 @@ pub const USER_EMAIL: &str = "email"; pub const USER_DEVICE_ID: &str = "device_id"; /// Represents a user that is currently using the server. -#[async_trait] -pub trait LoggedUser: Send + Sync { +pub trait ServerUser: Send + Sync { /// different user might return different workspace id. - fn workspace_id(&self) -> FlowyResult<Uuid>; - - fn user_id(&self) -> FlowyResult<i64>; - async fn is_local_mode(&self) -> FlowyResult<bool>; - - fn get_sqlite_db(&self, uid: i64) -> Result<DBConnection, FlowyError>; - - fn get_collab_db(&self, uid: i64) -> Result<Weak<CollabKVDB>, FlowyError>; - - fn application_root_dir(&self) -> Result<PathBuf, FlowyError>; -} - -// -pub struct AIUserServiceImpl(pub Weak<dyn LoggedUser>); - -impl AIUserServiceImpl { - fn logged_user(&self) -> FlowyResult<Arc<dyn LoggedUser>> { - self - .0 - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("User is not logged in")) - } -} - -#[async_trait] -impl AIUserService for AIUserServiceImpl { - fn user_id(&self) -> Result<i64, FlowyError> { - self.logged_user()?.user_id() - } - - async fn is_local_model(&self) -> FlowyResult<bool> { - self.logged_user()?.is_local_mode().await - } - - fn workspace_id(&self) -> Result<Uuid, FlowyError> { - self.logged_user()?.workspace_id() - } - - fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> { - self.logged_user()?.get_sqlite_db(uid) - } - - fn application_root_dir(&self) -> Result<PathBuf, FlowyError> { - self.logged_user()?.application_root_dir() - } + fn workspace_id(&self) -> FlowyResult<String>; } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs index 6086f7084b..09469f5b35 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs @@ -1,65 +1,79 @@ -#![allow(unused_variables)] use crate::af_cloud::AFServer; -use client_api::entity::ai_dto::{ - ChatQuestionQuery, CompleteTextParams, RepeatedRelatedQuestion, ResponseFormat, -}; -use client_api::entity::chat_dto::{ +use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::{ CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage, }; -use flowy_ai_pub::cloud::{ - AIModel, ChatCloudService, ChatMessage, ChatMessageType, ChatSettings, ModelList, StreamAnswer, - StreamComplete, UpdateChatParams, +use flowy_chat_pub::cloud::{ + ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType, StreamAnswer, }; use flowy_error::FlowyError; -use futures_util::{StreamExt, TryStreamExt}; +use futures_util::StreamExt; use lib_infra::async_trait::async_trait; -use serde_json::Value; -use std::collections::HashMap; -use std::path::Path; -use tracing::trace; -use uuid::Uuid; +use lib_infra::future::FutureResult; -pub(crate) struct CloudChatServiceImpl<T> { +pub(crate) struct AFCloudChatCloudServiceImpl<T> { pub inner: T, } #[async_trait] -impl<T> ChatCloudService for CloudChatServiceImpl<T> +impl<T> ChatCloudService for AFCloudChatCloudServiceImpl<T> where T: AFServer, { - async fn create_chat( + fn create_chat( &self, - uid: &i64, - workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - name: &str, - metadata: serde_json::Value, - ) -> Result<(), FlowyError> { + _uid: &i64, + workspace_id: &str, + chat_id: &str, + ) -> FutureResult<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); - let params = CreateChatParams { - chat_id, - name: name.to_string(), - rag_ids, + + FutureResult::new(async move { + let params = CreateChatParams { + chat_id, + name: "".to_string(), + rag_ids: vec![], + }; + try_get_client? + .create_chat(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + + Ok(()) + }) + } + + async fn send_chat_message( + &self, + workspace_id: &str, + chat_id: &str, + message: &str, + message_type: ChatMessageType, + ) -> Result<ChatMessageStream, FlowyError> { + let try_get_client = self.inner.try_get_client(); + let params = CreateChatMessageParams { + content: message.to_string(), + message_type, }; - try_get_client? - .create_chat(workspace_id, params) + let stream = try_get_client? + .create_chat_qa_message(workspace_id, chat_id, params) .await .map_err(FlowyError::from)?; - Ok(()) + Ok(stream.boxed()) } - async fn create_question( + fn send_question( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError> { + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateChatMessageParams { @@ -67,204 +81,110 @@ where message_type, }; - let message = try_get_client? - .create_question(workspace_id, &chat_id, params) - .await - .map_err(FlowyError::from)?; - Ok(message) + FutureResult::new(async move { + let message = try_get_client? + .create_question(&workspace_id, &chat_id, params) + .await + .map_err(FlowyError::from)?; + Ok(message) + }) } - async fn create_answer( + fn save_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message: &str, question_id: i64, - metadata: Option<serde_json::Value>, - ) -> Result<ChatMessage, FlowyError> { + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); let params = CreateAnswerMessageParams { content: message.to_string(), - metadata, question_message_id: question_id, }; - let message = try_get_client? - .save_answer(workspace_id, chat_id.to_string().as_str(), params) - .await - .map_err(FlowyError::from)?; - Ok(message) + + FutureResult::new(async move { + let message = try_get_client? + .create_answer(&workspace_id, &chat_id, params) + .await + .map_err(FlowyError::from)?; + Ok(message) + }) } async fn stream_answer( &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - format: ResponseFormat, - ai_model: Option<AIModel>, ) -> Result<StreamAnswer, FlowyError> { - trace!( - "stream_answer: workspace_id={}, chat_id={}, format={:?}, model: {:?}", - workspace_id, - chat_id, - format, - ai_model, - ); let try_get_client = self.inner.try_get_client(); - let result = try_get_client? - .stream_answer_v3( - workspace_id, - ChatQuestionQuery { - chat_id: chat_id.to_string(), - question_id: message_id, - format, - }, - ai_model.map(|v| v.name), - ) - .await; - - let stream = result.map_err(FlowyError::from)?.map_err(FlowyError::from); + let stream = try_get_client? + .stream_answer(workspace_id, chat_id, message_id) + .await + .map_err(FlowyError::from)?; Ok(stream.boxed()) } - async fn get_answer( + fn get_chat_messages( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let try_get_client = self.inner.try_get_client(); - let resp = try_get_client? - .get_answer(workspace_id, chat_id.to_string().as_str(), question_id) - .await - .map_err(FlowyError::from)?; - Ok(resp) - } - - async fn get_chat_messages( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, offset: MessageCursor, limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError> { + ) -> FutureResult<RepeatedChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); - let resp = try_get_client? - .get_chat_messages(workspace_id, chat_id.to_string().as_str(), offset, limit) - .await - .map_err(FlowyError::from)?; - Ok(resp) + FutureResult::new(async move { + let resp = try_get_client? + .get_chat_messages(&workspace_id, &chat_id, offset, limit) + .await + .map_err(FlowyError::from)?; + + Ok(resp) + }) } - async fn get_question_from_answer_id( + fn get_related_message( &self, - workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let try_get_client = self.inner.try_get_client()?; - let resp = try_get_client - .get_question_message_from_answer_id( - workspace_id, - chat_id.to_string().as_str(), - answer_message_id, - ) - .await - .map_err(FlowyError::from)? - .ok_or_else(FlowyError::record_not_found)?; - - Ok(resp) - } - - async fn get_related_message( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, + workspace_id: &str, + chat_id: &str, message_id: i64, - ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError> { + ) -> FutureResult<RepeatedRelatedQuestion, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); let try_get_client = self.inner.try_get_client(); - let resp = try_get_client? - .get_chat_related_question(workspace_id, chat_id.to_string().as_str(), message_id) - .await - .map_err(FlowyError::from)?; - Ok(resp) + FutureResult::new(async move { + let resp = try_get_client? + .get_chat_related_question(&workspace_id, &chat_id, message_id) + .await + .map_err(FlowyError::from)?; + + Ok(resp) + }) } - async fn stream_complete( + fn generate_answer( &self, - workspace_id: &Uuid, - params: CompleteTextParams, - ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError> { - let stream = self - .inner - .try_get_client()? - .stream_completion_v2(workspace_id, params, ai_model.map(|v| v.name)) - .await - .map_err(FlowyError::from)? - .map_err(FlowyError::from); + workspace_id: &str, + chat_id: &str, + question_message_id: i64, + ) -> FutureResult<ChatMessage, FlowyError> { + let workspace_id = workspace_id.to_string(); + let chat_id = chat_id.to_string(); + let try_get_client = self.inner.try_get_client(); - Ok(stream.boxed()) - } - - async fn embed_file( - &self, - workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError> { - Err( - FlowyError::not_support() - .with_context("indexing file with appflowy cloud is not suppotred yet"), - ) - } - - async fn get_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError> { - let settings = self - .inner - .try_get_client()? - .get_chat_settings(workspace_id, chat_id.to_string().as_str()) - .await?; - Ok(settings) - } - - async fn update_chat_settings( - &self, - workspace_id: &Uuid, - chat_id: &Uuid, - params: UpdateChatParams, - ) -> Result<(), FlowyError> { - self - .inner - .try_get_client()? - .update_chat_settings(workspace_id, chat_id.to_string().as_str(), params) - .await?; - Ok(()) - } - - async fn get_available_models(&self, workspace_id: &Uuid) -> Result<ModelList, FlowyError> { - let list = self - .inner - .try_get_client()? - .get_model_list(workspace_id) - .await?; - Ok(list) - } - - async fn get_workspace_default_model(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - let setting = self - .inner - .try_get_client()? - .get_workspace_settings(workspace_id.to_string().as_str()) - .await?; - Ok(setting.ai_model) + FutureResult::new(async move { + let resp = try_get_client? + .get_answer(&workspace_id, &chat_id, question_message_id) + .await + .map_err(FlowyError::from)?; + Ok(resp) + }) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index f29a7f89ad..f44a82ad3e 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -1,184 +1,167 @@ -#![allow(unused_variables)] -use crate::af_cloud::define::LoggedUser; -use crate::af_cloud::impls::util::check_request_workspace_id_is_match; -use crate::af_cloud::AFServer; +use anyhow::Error; use client_api::entity::ai_dto::{ SummarizeRowData, SummarizeRowParams, TranslateRowData, TranslateRowParams, }; use client_api::entity::QueryCollabResult::{Failed, Success}; -use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; +use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; +use collab::core::collab::DataSource; use collab::entity::EncodedCollab; use collab_entity::CollabType; +use serde_json::{Map, Value}; +use std::sync::Arc; +use tracing::{error, instrument}; + use flowy_database_pub::cloud::{ - DatabaseAIService, DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid, SummaryRowContent, + CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, TranslateRowContent, TranslateRowResponse, }; -use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use serde_json::{Map, Value}; -use std::sync::Weak; -use tracing::{error, instrument}; -use uuid::Uuid; +use lib_infra::future::FutureResult; + +use crate::af_cloud::define::ServerUser; +use crate::af_cloud::impls::util::check_request_workspace_id_is_match; +use crate::af_cloud::AFServer; pub(crate) struct AFCloudDatabaseCloudServiceImpl<T> { pub inner: T, - pub logged_user: Weak<dyn LoggedUser>, + pub user: Arc<dyn ServerUser>, } -#[async_trait] impl<T> DatabaseCloudService for AFCloudDatabaseCloudServiceImpl<T> where T: AFServer, { - #[instrument(level = "debug", skip_all, err)] - #[allow(clippy::blocks_in_conditions)] - async fn get_database_encode_collab( + #[instrument(level = "debug", skip_all)] + fn get_database_object_doc_state( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - workspace_id: &Uuid, - ) -> Result<Option<EncodedCollab>, FlowyError> { + workspace_id: &str, + ) -> FutureResult<Option<Vec<u8>>, Error> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); let try_get_client = self.inner.try_get_client(); - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, collab_type), - }; - let result = try_get_client?.get_collab(params).await; - match result { - Ok(data) => { - check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, - format!("get database object: {}:{}", object_id, collab_type), - )?; - Ok(Some(data.encode_collab)) - }, - Err(err) => { - if err.code == RecordNotFound { - Ok(None) - } else { - Err(err.into()) - } - }, - } - } - - #[instrument(level = "debug", skip_all, err)] - #[allow(clippy::blocks_in_conditions)] - async fn create_database_encode_collab( - &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - let encoded_collab_v1 = encoded_collab - .encode_to_bytes() - .map_err(|err| FlowyError::internal().with_context(err))?; - let params = CreateCollabParams { - workspace_id: *workspace_id, - object_id: *object_id, - encoded_collab_v1, - collab_type, - }; - self.inner.try_get_client()?.create_collab(params).await?; - Ok(()) + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id.clone(), collab_type.clone()), + }; + match try_get_client?.get_collab(params).await { + Ok(data) => { + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("get database object: {}:{}", object_id, collab_type), + )?; + Ok(Some(data.encode_collab.doc_state.to_vec())) + }, + Err(err) => { + if err.code == RecordNotFound { + Ok(None) + } else { + Err(Error::new(err)) + } + }, + } + }) } #[instrument(level = "debug", skip_all)] - async fn batch_get_database_encode_collab( + fn batch_get_database_object_doc_state( &self, - object_ids: Vec<Uuid>, + object_ids: Vec<String>, object_ty: CollabType, - workspace_id: &Uuid, - ) -> Result<EncodeCollabByOid, FlowyError> { + workspace_id: &str, + ) -> FutureResult<CollabDocStateByOid, Error> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - let client = try_get_client?; - let params = object_ids - .into_iter() - .map(|object_id| QueryCollab::new(object_id, object_ty)) - .collect(); - let results = client.batch_get_collab(workspace_id, params).await?; - check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, - "batch get database object", - )?; - Ok( - results - .0 + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let client = try_get_client?; + let params = object_ids .into_iter() - .flat_map(|(object_id, result)| match result { - Success { encode_collab_v1 } => { - match EncodedCollab::decode_from_bytes(&encode_collab_v1) { - Ok(encode) => Some((object_id, encode)), - Err(err) => { - error!("Failed to decode collab: {}", err); - None - }, - } - }, - Failed { error } => { - error!("Failed to get {} update: {}", object_id, error); - None - }, - }) - .collect::<EncodeCollabByOid>(), - ) + .map(|object_id| QueryCollab::new(object_id, object_ty.clone())) + .collect(); + let results = client.batch_get_collab(&workspace_id, params).await?; + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + "batch get database object", + )?; + Ok( + results + .0 + .into_iter() + .flat_map(|(object_id, result)| match result { + Success { encode_collab_v1 } => { + match EncodedCollab::decode_from_bytes(&encode_collab_v1) { + Ok(encode) => Some((object_id, DataSource::DocStateV1(encode.doc_state.to_vec()))), + Err(err) => { + error!("Failed to decode collab: {}", err); + None + }, + } + }, + Failed { error } => { + error!("Failed to get {} update: {}", object_id, error); + None + }, + }) + .collect::<CollabDocStateByOid>(), + ) + }) } - async fn get_database_collab_object_snapshots( + fn get_database_collab_object_snapshots( &self, - object_id: &Uuid, - limit: usize, - ) -> Result<Vec<DatabaseSnapshot>, FlowyError> { - Ok(vec![]) - } -} - -#[async_trait] -impl<T> DatabaseAIService for AFCloudDatabaseCloudServiceImpl<T> -where - T: AFServer, -{ - async fn summary_database_row( - &self, - workspace_id: &Uuid, - _object_id: &Uuid, - _summary_row: SummaryRowContent, - ) -> Result<String, FlowyError> { - let try_get_client = self.inner.try_get_client(); - let map: Map<String, Value> = _summary_row - .into_iter() - .map(|(key, value)| (key, Value::String(value))) - .collect(); - let params = SummarizeRowParams { - workspace_id: *workspace_id, - data: SummarizeRowData::Content(map), - }; - let data = try_get_client?.summarize_row(params).await?; - Ok(data.text) - } - - async fn translate_database_row( - &self, - workspace_id: &Uuid, - _translate_row: TranslateRowContent, - _language: &str, - ) -> Result<TranslateRowResponse, FlowyError> { - let try_get_client = self.inner.try_get_client(); - let data = TranslateRowData { - cells: _translate_row, - language: _language.to_string(), - include_header: false, - }; - - let params = TranslateRowParams { - workspace_id: workspace_id.to_string(), - data, - }; - let data = try_get_client?.translate_row(params).await?; - Ok(data) + _object_id: &str, + _limit: usize, + ) -> FutureResult<Vec<DatabaseSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) + } + + fn summary_database_row( + &self, + workspace_id: &str, + _object_id: &str, + summary_row: SummaryRowContent, + ) -> FutureResult<String, Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let map: Map<String, Value> = summary_row + .into_iter() + .map(|(key, value)| (key, Value::String(value))) + .collect(); + let params = SummarizeRowParams { + workspace_id, + data: SummarizeRowData::Content(map), + }; + let data = try_get_client?.summarize_row(params).await?; + Ok(data.text) + }) + } + + fn translate_database_row( + &self, + workspace_id: &str, + translate_row: TranslateRowContent, + language: &str, + ) -> FutureResult<TranslateRowResponse, Error> { + let language = language.to_string(); + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let data = TranslateRowData { + cells: translate_row, + language, + include_header: false, + }; + + let params = TranslateRowParams { workspace_id, data }; + let data = try_get_client?.translate_row(params).await?; + Ok(data) + }) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 1e000d5971..98732aa521 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -1,119 +1,105 @@ -#![allow(unused_variables)] -use client_api::entity::{CreateCollabParams, QueryCollab, QueryCollabParams}; +use anyhow::Error; +use client_api::entity::{QueryCollab, QueryCollabParams}; use collab::core::collab::DataSource; use collab::core::origin::CollabOrigin; -use collab::entity::EncodedCollab; -use collab::preclude::Collab; use collab_document::document::Document; use collab_entity::CollabType; +use std::sync::Arc; +use tracing::instrument; + use flowy_document_pub::cloud::*; use flowy_error::FlowyError; -use lib_infra::async_trait::async_trait; -use std::sync::Weak; -use tracing::instrument; -use uuid::Uuid; +use lib_infra::future::FutureResult; -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudDocumentCloudServiceImpl<T> { pub inner: T, - pub logged_user: Weak<dyn LoggedUser>, + pub user: Arc<dyn ServerUser>, } -#[async_trait] impl<T> DocumentCloudService for AFCloudDocumentCloudServiceImpl<T> where T: AFServer, { #[instrument(level = "debug", skip_all, fields(document_id = %document_id))] - async fn get_document_doc_state( + fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*document_id, CollabType::Document), - }; - let doc_state = self - .inner - .try_get_client()? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + let document_id = document_id.to_string(); + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.to_string(), CollabType::Document), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); - check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, - format!("get document doc state:{}", document_id), - )?; + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("get document doc state:{}", document_id), + )?; - Ok(doc_state) + Ok(doc_state) + }) } - async fn get_document_snapshots( + fn get_document_snapshots( &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, - ) -> Result<Vec<DocumentSnapshot>, FlowyError> { - Ok(vec![]) + _document_id: &str, + _limit: usize, + _workspace_id: &str, + ) -> FutureResult<Vec<DocumentSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } #[instrument(level = "debug", skip_all)] - async fn get_document_data( + fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Option<DocumentData>, FlowyError> { - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*document_id, CollabType::Document), - }; - let doc_state = self - .inner - .try_get_client()? - .get_collab(params) - .await? - .encode_collab - .doc_state - .to_vec(); - check_request_workspace_id_is_match( - workspace_id, - &self.logged_user, - format!("Get {} document", document_id), - )?; - let collab = Collab::new_with_source( - CollabOrigin::Empty, - document_id.to_string().as_str(), - DataSource::DocStateV1(doc_state), - vec![], - false, - )?; - let document = Document::open(collab)?; - Ok(document.get_document_data().ok()) - } - - async fn create_document_collab( - &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - let params = CreateCollabParams { - workspace_id: *workspace_id, - object_id: *document_id, - encoded_collab_v1: encoded_collab - .encode_to_bytes() - .map_err(|err| FlowyError::internal().with_context(err))?, - collab_type: CollabType::Document, - }; - self.inner.try_get_client()?.create_collab(params).await?; - Ok(()) + document_id: &str, + workspace_id: &str, + ) -> FutureResult<Option<DocumentData>, Error> { + let try_get_client = self.inner.try_get_client(); + let document_id = document_id.to_string(); + let workspace_id = workspace_id.to_string(); + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(document_id.clone(), CollabType::Document), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + format!("Get {} document", document_id), + )?; + let document = Document::from_doc_state( + CollabOrigin::Empty, + DataSource::DocStateV1(doc_state), + &document_id, + vec![], + )?; + Ok(document.get_document_data().ok()) + }) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs index 8db806a0da..839a8b5ed1 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/file_storage.rs @@ -1,158 +1,59 @@ +use flowy_error::FlowyError; +use flowy_storage::{ObjectIdentity, ObjectStorageService, ObjectValue}; +use lib_infra::future::FutureResult; + use crate::af_cloud::AFServer; -use client_api::entity::{CompleteUploadRequest, CreateUploadRequest}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_storage_pub::cloud::{ObjectIdentity, ObjectValue, StorageCloudService}; -use flowy_storage_pub::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; -pub struct AFCloudFileStorageServiceImpl<T> { - pub client: T, - - /// Only use in debug mode - pub maximum_upload_file_size_in_bytes: Option<u64>, -} +pub struct AFCloudFileStorageServiceImpl<T>(pub T); impl<T> AFCloudFileStorageServiceImpl<T> { - pub fn new(client: T, maximum_upload_file_size_in_bytes: Option<u64>) -> Self { - Self { - client, - maximum_upload_file_size_in_bytes, - } + pub fn new(client: T) -> Self { + Self(client) } } -#[async_trait] -impl<T> StorageCloudService for AFCloudFileStorageServiceImpl<T> +impl<T> ObjectStorageService for AFCloudFileStorageServiceImpl<T> where T: AFServer, { - async fn get_object_url(&self, object_id: ObjectIdentity) -> Result<String, FlowyError> { - let file_name = format!("{}.{}", object_id.file_id, object_id.ext); - let url = self - .client - .try_get_client()? - .get_blob_url(&object_id.workspace_id, &file_name); - Ok(url) - } - - async fn put_object(&self, url: String, file: ObjectValue) -> Result<(), FlowyError> { - let client = self.client.try_get_client()?; - client.put_blob(&url, file.raw, &file.mime).await?; - Ok(()) - } - - async fn delete_object(&self, url: &str) -> Result<(), FlowyError> { - self.client.try_get_client()?.delete_blob(url).await?; - Ok(()) - } - - async fn get_object(&self, url: String) -> Result<ObjectValue, FlowyError> { - let (mime, raw) = self.client.try_get_client()?.get_blob(&url).await?; - Ok(ObjectValue { - raw: raw.into(), - mime, + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult<String, FlowyError> { + let try_get_client = self.0.try_get_client(); + FutureResult::new(async move { + let file_name = format!("{}.{}", object_id.file_id, object_id.ext); + let client = try_get_client?; + let url = client.get_blob_url(&object_id.workspace_id, &file_name); + Ok(url) }) } - async fn get_object_url_v1( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - ) -> FlowyResult<String> { - let url = self - .client - .try_get_client()? - .get_blob_url_v1(workspace_id, parent_dir, file_id); - Ok(url) + fn put_object(&self, url: String, file: ObjectValue) -> FutureResult<(), FlowyError> { + let try_get_client = self.0.try_get_client(); + let file = file.clone(); + FutureResult::new(async move { + let client = try_get_client?; + client.put_blob(&url, file.raw, &file.mime).await?; + Ok(()) + }) } - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)> { - let value = self.client.try_get_client().ok()?.parse_blob_url_v1(url)?; - Some(value) + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError> { + let try_get_client = self.0.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + client.delete_blob(&url).await?; + Ok(()) + }) } - async fn create_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - content_type: &str, - file_size: u64, - ) -> Result<CreateUploadResponse, FlowyError> { - let parent_dir = parent_dir.to_string(); - let content_type = content_type.to_string(); - let file_id = file_id.to_string(); - let try_get_client = self.client.try_get_client(); - let client = try_get_client?; - let req = CreateUploadRequest { - file_id, - parent_dir, - content_type, - file_size: Some(file_size), - }; - - if cfg!(debug_assertions) { - if let Some(maximum_upload_size) = self.maximum_upload_file_size_in_bytes { - if file_size > maximum_upload_size { - return Err(FlowyError::new( - ErrorCode::SingleUploadLimitExceeded, - "File size exceeds the maximum limit", - )); - } - } - } - - let resp = client.create_upload(workspace_id, req).await?; - Ok(resp) - } - - async fn upload_part( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - part_number: i32, - body: Vec<u8>, - ) -> Result<UploadPartResponse, FlowyError> { - let try_get_client = self.client.try_get_client(); - let client = try_get_client?; - let resp = client - .upload_part( - workspace_id, - parent_dir, - file_id, - upload_id, - part_number, - body, - ) - .await?; - - Ok(resp) - } - - async fn complete_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - parts: Vec<CompletedPartRequest>, - ) -> Result<(), FlowyError> { - let parent_dir = parent_dir.to_string(); - let upload_id = upload_id.to_string(); - let file_id = file_id.to_string(); - let try_get_client = self.client.try_get_client(); - let client = try_get_client?; - let request = CompleteUploadRequest { - file_id, - parent_dir, - upload_id, - parts, - }; - client.complete_upload(workspace_id, request).await?; - Ok(()) + fn get_object(&self, url: String) -> FutureResult<ObjectValue, FlowyError> { + let try_get_client = self.0.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let (mime, raw) = client.get_blob(&url).await?; + Ok(ObjectValue { + raw: raw.into(), + mime, + }) + }) } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index 578f2870c6..fe58f3fc16 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -1,270 +1,183 @@ -use client_api::entity::workspace_dto::PublishInfoView; +use anyhow::Error; use client_api::entity::{ - CollabParams, PublishCollabItem, PublishCollabMetadata, QueryCollab, QueryCollabParams, + workspace_dto::CreateWorkspaceParam, CollabParams, QueryCollab, QueryCollabParams, }; -use client_api::entity::{PatchPublishedCollab, PublishInfo}; +use collab::core::collab::DataSource; +use collab::core::origin::CollabOrigin; use collab_entity::CollabType; -use serde_json::to_vec; -use std::path::PathBuf; -use std::sync::Weak; -use tracing::{instrument, trace}; -use uuid::Uuid; +use collab_folder::RepeatedViewIdentifier; +use std::sync::Arc; +use tracing::instrument; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, + Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, + WorkspaceRecord, }; -use flowy_folder_pub::entities::PublishPayload; -use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::AFServer; pub(crate) struct AFCloudFolderCloudServiceImpl<T> { pub inner: T, - pub logged_user: Weak<dyn LoggedUser>, + pub user: Arc<dyn ServerUser>, } -#[async_trait] impl<T> FolderCloudService for AFCloudFolderCloudServiceImpl<T> where T: AFServer, { - async fn get_folder_snapshots( + fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult<Workspace, Error> { + let try_get_client = self.inner.try_get_client(); + let cloned_name = name.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(cloned_name), + }) + .await?; + + Ok(Workspace { + id: new_workspace.workspace_id.to_string(), + name: new_workspace.workspace_name, + created_at: new_workspace.created_at.timestamp(), + child_views: RepeatedViewIdentifier::new(vec![]), + created_by: Some(new_workspace.owner_uid), + last_edited_time: new_workspace.created_at.timestamp(), + last_edited_by: Some(new_workspace.owner_uid), + }) + }) + } + + fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let _ = client.open_workspace(&workspace_id).await?; + Ok(()) + }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + let try_get_client = self.inner.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let records = client + .get_user_workspace_info() + .await? + .workspaces + .into_iter() + .map(|af_workspace| WorkspaceRecord { + id: af_workspace.workspace_id.to_string(), + name: af_workspace.workspace_name, + created_at: af_workspace.created_at.timestamp(), + }) + .collect::<Vec<_>>(); + Ok(records) + }) + } + #[instrument(level = "debug", skip_all)] + fn get_folder_data( + &self, + workspace_id: &str, + uid: &i64, + ) -> FutureResult<Option<FolderData>, Error> { + let uid = *uid; + let workspace_id = workspace_id.to_string(); + let try_get_client = self.inner.try_get_client(); + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(workspace_id.clone(), CollabType::Folder), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder data")?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Empty, + DataSource::DocStateV1(doc_state), + &workspace_id, + vec![], + )?; + Ok(folder.get_folder_data(&workspace_id)) + }) + } + + fn get_folder_snapshots( &self, _workspace_id: &str, _limit: usize, - ) -> Result<Vec<FolderSnapshot>, FlowyError> { - Ok(vec![]) + ) -> FutureResult<Vec<FolderSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } #[instrument(level = "debug", skip_all)] - async fn get_folder_doc_state( + fn get_folder_doc_state( &self, - workspace_id: &Uuid, + workspace_id: &str, _uid: i64, collab_type: CollabType, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { + object_id: &str, + ) -> FutureResult<Vec<u8>, Error> { + let object_id = object_id.to_string(); + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, collab_type), - }; - let doc_state = try_get_client? - .get_collab(params) - .await - .map_err(FlowyError::from)? - .encode_collab - .doc_state - .to_vec(); - check_request_workspace_id_is_match(workspace_id, &self.logged_user, "get folder doc state")?; - Ok(doc_state) + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, collab_type), + }; + let doc_state = try_get_client? + .get_collab(params) + .await + .map_err(FlowyError::from)? + .encode_collab + .doc_state + .to_vec(); + check_request_workspace_id_is_match(&workspace_id, &cloned_user, "get folder doc state")?; + Ok(doc_state) + }) } - async fn full_sync_collab_object( + fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client(); - try_get_client? - .collab_full_sync( - workspace_id, - ¶ms.object_id, - params.collab_type, - params.encoded_collab.doc_state.to_vec(), - params.encoded_collab.state_vector.to_vec(), - ) - .await?; - Ok(()) - } - - async fn batch_create_folder_collab_objects( - &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec<FolderCollabParams>, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), Error> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.inner.try_get_client(); - let params = objects - .into_iter() - .map(|object| { - CollabParams::new( - object.object_id, - object.collab_type, - object.encoded_collab_v1, - ) - }) - .collect::<Vec<_>>(); - try_get_client? - .create_collab_list(workspace_id, params) - .await?; - Ok(()) + FutureResult::new(async move { + let params = objects + .into_iter() + .map(|object| { + CollabParams::new( + object.object_id, + object.collab_type, + object.encoded_collab_v1, + ) + }) + .collect::<Vec<_>>(); + try_get_client? + .create_collab_list(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) } fn service_name(&self) -> String { "AppFlowy Cloud".to_string() } - - async fn publish_view( - &self, - workspace_id: &Uuid, - payload: Vec<PublishPayload>, - ) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client(); - let params = payload - .into_iter() - .filter_map(|object| { - let (meta, data) = match object { - PublishPayload::Document(payload) => (payload.meta, payload.data), - PublishPayload::Database(payload) => { - (payload.meta, to_vec(&payload.data).unwrap_or_default()) - }, - PublishPayload::Unknown => return None, - }; - Some(PublishCollabItem { - meta: PublishCollabMetadata { - view_id: Uuid::parse_str(&meta.view_id).unwrap_or(Uuid::nil()), - publish_name: meta.publish_name, - metadata: meta.metadata, - }, - data, - comments_enabled: true, - duplicate_enabled: true, - }) - }) - .collect::<Vec<_>>(); - try_get_client? - .publish_collabs(workspace_id, params) - .await?; - Ok(()) - } - - async fn unpublish_views( - &self, - workspace_id: &Uuid, - view_ids: Vec<Uuid>, - ) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client(); - try_get_client? - .unpublish_collabs(workspace_id, &view_ids) - .await?; - Ok(()) - } - - async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError> { - let try_get_client = self.inner.try_get_client(); - let info = try_get_client? - .get_published_collab_info(view_id) - .await - .map_err(FlowyError::from)?; - Ok(info) - } - - async fn set_publish_name( - &self, - workspace_id: &Uuid, - view_id: Uuid, - new_name: String, - ) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client()?; - try_get_client - .patch_published_collabs( - workspace_id, - &[PatchPublishedCollab { - view_id, - publish_name: Some(new_name), - comments_enabled: Some(true), - duplicate_enabled: Some(true), - }], - ) - .await - .map_err(FlowyError::from)?; - Ok(()) - } - - async fn set_publish_namespace( - &self, - workspace_id: &Uuid, - new_namespace: String, - ) -> Result<(), FlowyError> { - let try_get_client = self.inner.try_get_client(); - try_get_client? - .set_workspace_publish_namespace(workspace_id, new_namespace) - .await?; - Ok(()) - } - - async fn list_published_views( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<PublishInfoView>, FlowyError> { - let published_views = self - .inner - .try_get_client()? - .list_published_views(workspace_id) - .await - .map_err(FlowyError::from)?; - Ok(published_views) - } - - async fn get_default_published_view_info( - &self, - workspace_id: &Uuid, - ) -> Result<PublishInfo, FlowyError> { - let default_published_view_info = self - .inner - .try_get_client()? - .get_default_publish_view_info(workspace_id) - .await - .map_err(FlowyError::from)?; - Ok(default_published_view_info) - } - - async fn set_default_published_view( - &self, - workspace_id: &Uuid, - view_id: uuid::Uuid, - ) -> Result<(), FlowyError> { - self - .inner - .try_get_client()? - .set_default_publish_view(workspace_id, view_id) - .await - .map_err(FlowyError::from)?; - Ok(()) - } - - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - self - .inner - .try_get_client()? - .delete_default_publish_view(workspace_id) - .await - .map_err(FlowyError::from)?; - Ok(()) - } - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - let namespace = self - .inner - .try_get_client()? - .get_workspace_publish_namespace(workspace_id) - .await?; - Ok(namespace) - } - - async fn import_zip(&self, file_path: &str) -> Result<(), FlowyError> { - let file_path = PathBuf::from(file_path); - let client = self.inner.try_get_client()?; - let url = client.create_import(&file_path).await?.presigned_url; - trace!( - "Importing zip file: {} to url: {}", - file_path.display(), - url - ); - client.upload_import_file(&file_path, &url).await?; - Ok(()) - } } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs index 1ce0995144..c0bf6e2dea 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/search.rs @@ -1,16 +1,17 @@ -use crate::af_cloud::AFServer; -use flowy_ai_pub::cloud::search_dto::{ - SearchDocumentResponseItem, SearchResult, SearchSummaryResult, -}; +use client_api::entity::search_dto::SearchDocumentResponseItem; use flowy_error::FlowyError; use flowy_search_pub::cloud::SearchCloudService; use lib_infra::async_trait::async_trait; -use uuid::Uuid; + +use crate::af_cloud::AFServer; pub(crate) struct AFCloudSearchCloudServiceImpl<T> { pub inner: T, } +// The limit of what the score should be for results, used to +// filter out irrelevant results. +const SCORE_LIMIT: f64 = 0.8; const DEFAULT_PREVIEW: u32 = 80; #[async_trait] @@ -20,27 +21,19 @@ where { async fn document_search( &self, - workspace_id: &Uuid, + workspace_id: &str, query: String, ) -> Result<Vec<SearchDocumentResponseItem>, FlowyError> { let client = self.inner.try_get_client()?; let result = client - .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW, None) + .search_documents(workspace_id, &query, 10, DEFAULT_PREVIEW) .await?; - Ok(result) - } - - async fn generate_search_summary( - &self, - workspace_id: &Uuid, - query: String, - search_results: Vec<SearchResult>, - ) -> Result<SearchSummaryResult, FlowyError> { - let client = self.inner.try_get_client()?; - let result = client - .generate_search_summary(workspace_id, &query, search_results) - .await?; + // Filter out irrelevant results + let result = result + .into_iter() + .filter(|r| r.score < SCORE_LIMIT) + .collect(); Ok(result) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 4e46f310c5..a7928a9747 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -1,586 +1,597 @@ use std::collections::HashMap; -use std::str::FromStr; -use std::sync::{Arc, Weak}; +use std::sync::Arc; use anyhow::anyhow; -use arc_swap::ArcSwapOption; use client_api::entity::billing_dto::{ - RecurringInterval, SetSubscriptionRecurringInterval, SubscriptionCancelRequest, SubscriptionPlan, - SubscriptionPlanDetail, WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit, + SubscriptionPlan, SubscriptionStatus, WorkspaceSubscriptionPlan, WorkspaceSubscriptionStatus, }; use client_api::entity::workspace_dto::{ - CreateWorkspaceParam, PatchWorkspaceParam, QueryWorkspaceParam, WorkspaceMemberChangeset, + CreateWorkspaceMember, CreateWorkspaceParam, PatchWorkspaceParam, WorkspaceMemberChangeset, WorkspaceMemberInvitation, }; use client_api::entity::{ - AFWorkspace, AFWorkspaceInvitation, AFWorkspaceSettings, AFWorkspaceSettingsChange, AuthProvider, - CollabParams, CreateCollabParams, GotrueTokenResponse, QueryWorkspaceMember, + AFRole, AFWorkspace, AFWorkspaceInvitation, AuthProvider, CollabParams, CreateCollabParams, + QueryWorkspaceMember, }; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; use collab_entity::{CollabObject, CollabType}; -use tracing::{instrument, trace}; +use parking_lot::RwLock; +use tracing::instrument; -use crate::af_cloud::define::{LoggedUser, USER_SIGN_IN_URL}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; +use flowy_user_pub::entities::{ + AFCloudOAuthParams, AuthResponse, Role, UpdateUserProfileParams, UserCredentials, UserProfile, + UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + WorkspaceSubscription, WorkspaceUsage, +}; +use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; +use uuid::Uuid; + +use crate::af_cloud::define::{ServerUser, USER_SIGN_IN_URL}; use crate::af_cloud::impls::user::dto::{ af_update_from_update_params, from_af_workspace_member, to_af_role, user_profile_from_af_profile, }; use crate::af_cloud::impls::user::util::encryption_type_from_profile; use crate::af_cloud::impls::util::check_request_workspace_id_is_match; use crate::af_cloud::{AFCloudClient, AFServer}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::cloud::{UserCloudService, UserCollabParams, UserUpdate, UserUpdateReceiver}; -use flowy_user_pub::entities::{ - AFCloudOAuthParams, AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, -}; -use flowy_user_pub::sql::select_user_workspace; -use lib_infra::async_trait::async_trait; -use lib_infra::box_any::BoxAny; -use uuid::Uuid; use super::dto::{from_af_workspace_invitation_status, to_workspace_invitation_status}; pub(crate) struct AFCloudUserAuthServiceImpl<T> { server: T, - user_change_recv: ArcSwapOption<tokio::sync::mpsc::Receiver<UserUpdate>>, - logged_user: Weak<dyn LoggedUser>, + user_change_recv: RwLock<Option<tokio::sync::mpsc::Receiver<UserUpdate>>>, + user: Arc<dyn ServerUser>, } impl<T> AFCloudUserAuthServiceImpl<T> { pub(crate) fn new( server: T, user_change_recv: tokio::sync::mpsc::Receiver<UserUpdate>, - logged_user: Weak<dyn LoggedUser>, + user: Arc<dyn ServerUser>, ) -> Self { Self { server, - user_change_recv: ArcSwapOption::new(Some(Arc::new(user_change_recv))), - logged_user, + user_change_recv: RwLock::new(Some(user_change_recv)), + user, } } } -#[async_trait] impl<T> UserCloudService for AFCloudUserAuthServiceImpl<T> where T: AFServer, { - async fn sign_up(&self, params: BoxAny) -> Result<AuthResponse, FlowyError> { + fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError> { let try_get_client = self.server.try_get_client(); - let params = oauth_params_from_box_any(params)?; - let resp = user_sign_up_request(try_get_client?, params).await?; - Ok(resp) + FutureResult::new(async move { + let params = oauth_params_from_box_any(params)?; + let resp = user_sign_up_request(try_get_client?, params).await?; + Ok(resp) + }) } // Zack: Not sure if this is needed anymore since sign_up handles both cases - async fn sign_in(&self, params: BoxAny) -> Result<AuthResponse, FlowyError> { + fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let params = oauth_params_from_box_any(params)?; - let resp = user_sign_in_with_url(client, params).await?; - Ok(resp) + FutureResult::new(async move { + let client = try_get_client?; + let params = oauth_params_from_box_any(params)?; + let resp = user_sign_in_with_url(client, params).await?; + Ok(resp) + }) } - async fn sign_out(&self, _token: Option<String>) -> Result<(), FlowyError> { + fn sign_out(&self, _token: Option<String>) -> FutureResult<(), FlowyError> { // Calling the sign_out method that will revoke all connected devices' refresh tokens. // So do nothing here. - Ok(()) + FutureResult::new(async move { Ok(()) }) } - async fn delete_account(&self) -> Result<(), FlowyError> { - let client = self.server.try_get_client()?; - client.delete_user().await?; - Ok(()) - } - - async fn generate_sign_in_url_with_email(&self, email: &str) -> Result<String, FlowyError> { + fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult<String, FlowyError> { let email = email.to_string(); let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let admin_client = get_admin_client(&client).await?; - let action_link = admin_client.generate_sign_in_action_link(&email).await?; - let sign_in_url = client.extract_sign_in_url(&action_link).await?; - Ok(sign_in_url) + FutureResult::new(async move { + let client = try_get_client?; + let admin_client = get_admin_client(&client).await?; + let action_link = admin_client.generate_sign_in_action_link(&email).await?; + let sign_in_url = client.extract_sign_in_url(&action_link).await?; + Ok(sign_in_url) + }) } - async fn create_user(&self, email: &str, password: &str) -> Result<(), FlowyError> { + fn create_user(&self, email: &str, password: &str) -> FutureResult<(), FlowyError> { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let admin_client = get_admin_client(&client).await?; - admin_client - .create_email_verified_user(&email, &password) - .await?; + FutureResult::new(async move { + let client = try_get_client?; + let admin_client = get_admin_client(&client).await?; + admin_client + .create_email_verified_user(&email, &password) + .await?; - Ok(()) + Ok(()) + }) } - async fn sign_in_with_password( + fn sign_in_with_password( &self, email: &str, password: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { + ) -> FutureResult<UserProfile, FlowyError> { let password = password.to_string(); let email = email.to_string(); let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let response = client.sign_in_password(&email, &password).await?; - Ok(response.gotrue_response) + FutureResult::new(async move { + let client = try_get_client?; + client.sign_in_password(&email, &password).await?; + let profile = client.get_profile().await?; + let token = client.get_token()?; + let profile = user_profile_from_af_profile(token, profile)?; + Ok(profile) + }) } - async fn sign_in_with_magic_link( + fn sign_in_with_magic_link( &self, email: &str, redirect_to: &str, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), FlowyError> { let email = email.to_owned(); let redirect_to = redirect_to.to_owned(); let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client - .sign_in_with_magic_link(&email, Some(redirect_to)) - .await?; - Ok(()) + FutureResult::new(async move { + let client = try_get_client?; + client + .sign_in_with_magic_link(&email, Some(redirect_to)) + .await?; + Ok(()) + }) } - async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { - let email = email.to_owned(); - let passcode = passcode.to_owned(); - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let response = client.sign_in_with_passcode(&email, &passcode).await?; - Ok(response) - } - - async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result<String, FlowyError> { + fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult<String, FlowyError> { let provider = AuthProvider::from(provider); let try_get_client = self.server.try_get_client(); - let provider = provider.ok_or(anyhow!("invalid provider"))?; - let url = try_get_client? - .generate_oauth_url_with_provider(&provider) - .await?; - Ok(url) + FutureResult::new(async move { + let provider = provider.ok_or(anyhow!("invalid provider"))?; + let url = try_get_client? + .generate_oauth_url_with_provider(&provider) + .await?; + Ok(url) + }) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { + fn update_user( + &self, + _credential: UserCredentials, + params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client - .update_user(af_update_from_update_params(params)) - .await?; - Ok(()) + FutureResult::new(async move { + let client = try_get_client?; + client + .update_user(af_update_from_update_params(params)) + .await?; + Ok(()) + }) } #[instrument(level = "debug", skip_all)] - async fn get_user_profile( + fn get_user_profile( &self, - uid: i64, - workspace_id: &str, - ) -> Result<UserProfile, FlowyError> { - let client = self.server.try_get_client()?; - let logged_user = self - .logged_user - .upgrade() - .ok_or_else(FlowyError::user_not_login)?; - - let profile = client.get_profile().await?; - let token = client.get_token()?; - - let mut conn = logged_user.get_sqlite_db(uid)?; - let workspace_auth_type = select_user_workspace(workspace_id, &mut conn) - .map(|row| AuthType::from(row.workspace_type)) - .unwrap_or(AuthType::AppFlowyCloud); - let profile = user_profile_from_af_profile(token, profile, workspace_auth_type)?; - - // Discard the response if the user has switched to a new workspace. This avoids updating the - // user profile with potentially outdated information when the workspace ID no longer matches. - let workspace_id = Uuid::from_str(workspace_id)?; - check_request_workspace_id_is_match(&workspace_id, &self.logged_user, "get user profile")?; - Ok(profile) - } - - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<UserWorkspace, FlowyError> { + _credential: UserCredentials, + ) -> FutureResult<UserProfile, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let af_workspace = client.open_workspace(workspace_id).await?; - Ok(to_user_workspace(af_workspace)) + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let expected_workspace_id = cloned_user.workspace_id()?; + let client = try_get_client?; + let profile = client.get_profile().await?; + let token = client.get_token()?; + let profile = user_profile_from_af_profile(token, profile)?; + + // Discard the response if the user has switched to a new workspace. This avoids updating the + // user profile with potentially outdated information when the workspace ID no longer matches. + check_request_workspace_id_is_match( + &expected_workspace_id, + &cloned_user, + "get user profile", + )?; + Ok(profile) + }) } - async fn get_all_workspace(&self, _uid: i64) -> Result<Vec<UserWorkspace>, FlowyError> { + fn open_workspace(&self, workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError> { let try_get_client = self.server.try_get_client(); - let workspaces = try_get_client? - .get_workspaces_opt(QueryWorkspaceParam { - include_member_count: Some(true), - include_role: Some(true), - }) - .await?; - to_user_workspaces(workspaces) + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + let af_workspace = client.open_workspace(&workspace_id).await?; + Ok(to_user_workspace(af_workspace)) + }) } - async fn create_workspace(&self, workspace_name: &str) -> Result<UserWorkspace, FlowyError> { - let workspace_name_owned = workspace_name.to_owned(); - let new_workspace = self - .server - .try_get_client()? - .create_workspace(CreateWorkspaceParam { - workspace_name: Some(workspace_name_owned), - }) - .await?; - Ok(to_user_workspace(new_workspace)) + fn get_all_workspace(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, FlowyError> { + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let workspaces = try_get_client?.get_workspaces().await?; + to_user_workspaces(workspaces.0) + }) } - async fn patch_workspace( + #[allow(deprecated)] + fn add_workspace_member( &self, - workspace_id: &Uuid, - new_workspace_name: Option<String>, - new_workspace_icon: Option<String>, - ) -> Result<(), FlowyError> { - let workspace_id = workspace_id.to_owned(); - self - .server - .try_get_client()? - .patch_workspace(PatchWorkspaceParam { - workspace_id, - workspace_name: new_workspace_name, - workspace_icon: new_workspace_icon, - }) - .await?; - Ok(()) - } - - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + user_email: String, + workspace_id: String, + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client.delete_workspace(workspace_id).await?; - Ok(()) + FutureResult::new(async move { + // TODO(zack): add_workspace_members will be deprecated after finishing the invite logic. Don't forget to remove the #[allow(deprecated)] + try_get_client? + .add_workspace_members( + workspace_id, + vec![CreateWorkspaceMember { + email: user_email, + role: AFRole::Member, + }], + ) + .await?; + Ok(()) + }) } - async fn invite_workspace_member( + fn invite_workspace_member( &self, invitee_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - try_get_client? - .invite_workspace_members( - &workspace_id, - vec![WorkspaceMemberInvitation { - email: invitee_email, - role: to_af_role(role), - skip_email_send: false, - wait_email_send: false, - }], - ) - .await?; - Ok(()) + FutureResult::new(async move { + try_get_client? + .invite_workspace_members( + &workspace_id, + vec![WorkspaceMemberInvitation { + email: invitee_email, + role: to_af_role(role), + }], + ) + .await?; + Ok(()) + }) } - async fn list_workspace_invitations( + fn list_workspace_invitations( &self, filter: Option<WorkspaceInvitationStatus>, - ) -> Result<Vec<WorkspaceInvitation>, FlowyError> { + ) -> FutureResult<Vec<WorkspaceInvitation>, FlowyError> { let try_get_client = self.server.try_get_client(); let filter = filter.map(to_workspace_invitation_status); - let r = try_get_client? - .list_workspace_invitations(filter) - .await? - .into_iter() - .map(to_workspace_invitation) - .collect(); - Ok(r) + FutureResult::new(async move { + let r = try_get_client? + .list_workspace_invitations(filter) + .await? + .into_iter() + .map(to_workspace_invitation) + .collect(); + Ok(r) + }) } - async fn accept_workspace_invitations(&self, invite_id: String) -> Result<(), FlowyError> { + fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - try_get_client? - .accept_workspace_invitation(&invite_id) - .await?; - Ok(()) + FutureResult::new(async move { + try_get_client? + .accept_workspace_invitation(&invite_id) + .await?; + Ok(()) + }) } - async fn remove_workspace_member( + fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, - ) -> Result<(), FlowyError> { + workspace_id: String, + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - try_get_client? - .remove_workspace_members(&workspace_id, vec![user_email]) - .await?; - Ok(()) + FutureResult::new(async move { + try_get_client? + .remove_workspace_members(workspace_id, vec![user_email]) + .await?; + Ok(()) + }) } - async fn update_workspace_member( + fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); - try_get_client? - .update_workspace_member(&workspace_id, changeset) - .await?; - Ok(()) + FutureResult::new(async move { + let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); + try_get_client? + .update_workspace_member(workspace_id, changeset) + .await?; + Ok(()) + }) } - async fn get_workspace_members( + fn get_workspace_members( &self, - workspace_id: Uuid, - ) -> Result<Vec<WorkspaceMember>, FlowyError> { + workspace_id: String, + ) -> FutureResult<Vec<WorkspaceMember>, FlowyError> { let try_get_client = self.server.try_get_client(); - let members = try_get_client? - .get_workspace_members(&workspace_id) - .await? - .into_iter() - .map(from_af_workspace_member) - .collect(); - Ok(members) + FutureResult::new(async move { + let members = try_get_client? + .get_workspace_members(&workspace_id) + .await? + .into_iter() + .map(from_af_workspace_member) + .collect(); + Ok(members) + }) + } + + fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> FutureResult<WorkspaceMember, FlowyError> { + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let query = QueryWorkspaceMember { + workspace_id: workspace_id.clone(), + uid, + }; + let member = client.get_workspace_member(query).await?; + Ok(from_af_workspace_member(member)) + }) } #[instrument(level = "debug", skip_all)] - async fn get_user_awareness_doc_state( + fn get_user_awareness_doc_state( &self, _uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { + workspace_id: &str, + object_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); let try_get_client = self.server.try_get_client(); - let cloned_user = self.logged_user.clone(); - let params = QueryCollabParams { - workspace_id: *workspace_id, - inner: QueryCollab::new(*object_id, CollabType::UserAwareness), - }; - let resp = try_get_client?.get_collab(params).await?; - check_request_workspace_id_is_match(workspace_id, &cloned_user, "get user awareness object")?; - Ok(resp.encode_collab.doc_state.to_vec()) + let cloned_user = self.user.clone(); + FutureResult::new(async move { + let params = QueryCollabParams { + workspace_id: workspace_id.clone(), + inner: QueryCollab::new(object_id, CollabType::UserAwareness), + }; + let resp = try_get_client?.get_collab(params).await?; + check_request_workspace_id_is_match( + &workspace_id, + &cloned_user, + "get user awareness object", + )?; + Ok(resp.encode_collab.doc_state.to_vec()) + }) } fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> { - let rx = self.user_change_recv.swap(None)?; - Arc::into_inner(rx) + self.user_change_recv.write().take() } - async fn create_collab_object( + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } + + fn create_collab_object( &self, collab_object: &CollabObject, data: Vec<u8>, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); let collab_object = collab_object.clone(); - let client = try_get_client?; - let workspace_id = Uuid::from_str(&collab_object.workspace_id)?; - let object_id = Uuid::from_str(&collab_object.object_id)?; - - let params = CreateCollabParams { - workspace_id, - object_id, - collab_type: collab_object.collab_type, - encoded_collab_v1: data, - }; - client.create_collab(params).await?; - Ok(()) + FutureResult::new(async move { + let client = try_get_client?; + let params = CreateCollabParams { + workspace_id: collab_object.workspace_id, + object_id: collab_object.object_id, + collab_type: collab_object.collab_type, + encoded_collab_v1: data, + }; + client.create_collab(params).await?; + Ok(()) + }) } - async fn batch_create_collab_object( + fn batch_create_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec<UserCollabParams>, - ) -> Result<(), FlowyError> { + ) -> FutureResult<(), FlowyError> { + let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); - let params = objects - .into_iter() - .flat_map(|object| { - Uuid::from_str(&object.object_id) - .map(|object_id| { - CollabParams::new( - object_id, - u8::from(object.collab_type).into(), - object.encoded_collab, - ) - }) - .ok() - }) - .collect::<Vec<_>>(); - try_get_client? - .create_collab_list(workspace_id, params) - .await - .map_err(FlowyError::from)?; - Ok(()) + FutureResult::new(async move { + let params = objects + .into_iter() + .map(|object| { + CollabParams::new( + object.object_id, + u8::from(object.collab_type).into(), + object.encoded_collab, + ) + }) + .collect::<Vec<_>>(); + try_get_client? + .create_collab_list(&workspace_id, params) + .await + .map_err(FlowyError::from)?; + Ok(()) + }) } - async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { + fn create_workspace(&self, workspace_name: &str) -> FutureResult<UserWorkspace, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client.leave_workspace(workspace_id).await?; - Ok(()) + let workspace_name_owned = workspace_name.to_owned(); + FutureResult::new(async move { + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(workspace_name_owned), + }) + .await?; + Ok(to_user_workspace(new_workspace)) + }) } - async fn subscribe_workspace( + fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let workspace_id_owned = workspace_id.to_owned(); + FutureResult::new(async move { + let client = try_get_client?; + client.delete_workspace(&workspace_id_owned).await?; + Ok(()) + }) + } + + fn patch_workspace( &self, - workspace_id: Uuid, - recurring_interval: RecurringInterval, - workspace_subscription_plan: SubscriptionPlan, - success_url: String, - ) -> Result<String, FlowyError> { + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let owned_workspace_id = workspace_id.to_owned(); + let owned_workspace_name = new_workspace_name.map(|s| s.to_owned()); + let owned_workspace_icon = new_workspace_icon.map(|s| s.to_owned()); + FutureResult::new(async move { + let workspace_id: Uuid = owned_workspace_id + .parse() + .map_err(|_| ErrorCode::InvalidParams)?; + let client = try_get_client?; + client + .patch_workspace(PatchWorkspaceParam { + workspace_id, + workspace_name: owned_workspace_name, + workspace_icon: owned_workspace_icon, + }) + .await?; + Ok(()) + }) + } + + fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); let workspace_id = workspace_id.to_string(); - let client = try_get_client?; - let payment_link = client - .create_subscription( - &workspace_id, - recurring_interval, - workspace_subscription_plan, - &success_url, - ) - .await?; - Ok(payment_link) + FutureResult::new(async move { + let client = try_get_client?; + client.leave_workspace(&workspace_id).await?; + Ok(()) + }) } - async fn get_workspace_member( - &self, - workspace_id: &Uuid, - uid: i64, - ) -> Result<WorkspaceMember, FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let params = QueryWorkspaceMember { - workspace_id: *workspace_id, - uid, - }; - let member = client.get_workspace_member(params).await?; - - Ok(from_af_workspace_member(member)) - } - - async fn get_workspace_subscriptions( - &self, - ) -> Result<Vec<WorkspaceSubscriptionStatus>, FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let workspace_subscriptions = client.list_subscription().await?; - Ok(workspace_subscriptions) - } - - async fn get_workspace_subscription_one( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<WorkspaceSubscriptionStatus>, FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let workspace_subscriptions = client - .get_workspace_subscriptions(&workspace_id.to_string()) - .await?; - Ok(workspace_subscriptions) - } - - async fn cancel_workspace_subscription( + fn subscribe_workspace( &self, workspace_id: String, - plan: SubscriptionPlan, - reason: Option<String>, - ) -> Result<(), FlowyError> { + recurring_interval: flowy_user_pub::entities::RecurringInterval, + workspace_subscription_plan: flowy_user_pub::entities::SubscriptionPlan, + success_url: String, + ) -> FutureResult<String, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client - .cancel_subscription(&SubscriptionCancelRequest { - workspace_id, - plan, - sync: true, - reason, - }) - .await?; - Ok(()) + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + let subscription_plan = to_workspace_subscription_plan(workspace_subscription_plan)?; + let client = try_get_client?; + let payment_link = client + .create_subscription( + &workspace_id, + to_recurring_interval(recurring_interval), + subscription_plan, + &success_url, + ) + .await?; + Ok(payment_link) + }) } - async fn get_workspace_plan( + fn get_workspace_member_info( &self, - workspace_id: Uuid, - ) -> Result<Vec<SubscriptionPlan>, FlowyError> { + workspace_id: &str, + uid: i64, + ) -> FutureResult<WorkspaceMember, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let plans = client - .get_active_workspace_subscriptions(&workspace_id.to_string()) - .await?; - Ok(plans) - } - - async fn get_workspace_usage( - &self, - workspace_id: &Uuid, - ) -> Result<WorkspaceUsageAndLimit, FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let usage = client - .get_workspace_usage_and_limit(&workspace_id.to_string()) - .await?; - Ok(usage) - } - - async fn get_billing_portal_url(&self) -> Result<String, FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let url = client.get_portal_session_link().await?; - Ok(url) - } - - async fn update_workspace_subscription_payment_period( - &self, - workspace_id: &Uuid, - plan: SubscriptionPlan, - recurring_interval: RecurringInterval, - ) -> Result<(), FlowyError> { - let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - client - .set_subscription_recurring_interval(&SetSubscriptionRecurringInterval { + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + let params = QueryWorkspaceMember { workspace_id: workspace_id.to_string(), - plan, - recurring_interval, + uid, + }; + let member = client.get_workspace_member(params).await?; + let role = match member.role { + AFRole::Owner => Role::Owner, + AFRole::Member => Role::Member, + AFRole::Guest => Role::Guest, + }; + Ok(WorkspaceMember { + email: member.email, + role, + name: member.name, + avatar_url: member.avatar_url, }) - .await?; - Ok(()) + }) } - async fn get_subscription_plan_details(&self) -> Result<Vec<SubscriptionPlanDetail>, FlowyError> { + fn get_workspace_subscriptions(&self) -> FutureResult<Vec<WorkspaceSubscription>, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let plan_details = client.get_subscription_plan_details().await?; - Ok(plan_details) + FutureResult::new(async move { + let client = try_get_client?; + let workspace_subscriptions = client + .list_subscription() + .await? + .into_iter() + .map(to_workspace_subscription) + .collect(); + Ok(workspace_subscriptions) + }) } - async fn get_workspace_setting( - &self, - workspace_id: &Uuid, - ) -> Result<AFWorkspaceSettings, FlowyError> { - let workspace_id = workspace_id.to_string(); + fn cancel_workspace_subscription(&self, workspace_id: String) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let settings = client.get_workspace_settings(&workspace_id).await?; - Ok(settings) + FutureResult::new(async move { + let client = try_get_client?; + client.cancel_subscription(&workspace_id).await?; + Ok(()) + }) } - async fn update_workspace_setting( - &self, - workspace_id: &Uuid, - workspace_settings: AFWorkspaceSettingsChange, - ) -> Result<AFWorkspaceSettings, FlowyError> { - trace!("Sync workspace settings: {:?}", workspace_settings); - let workspace_id = workspace_id.to_string(); + fn get_workspace_usage(&self, workspace_id: String) -> FutureResult<WorkspaceUsage, FlowyError> { let try_get_client = self.server.try_get_client(); - let client = try_get_client?; - let settings = client - .update_workspace_settings(&workspace_id, &workspace_settings) - .await?; - Ok(settings) + FutureResult::new(async move { + let client = try_get_client?; + let usage = client.get_billing_workspace_usage(&workspace_id).await?; + Ok(WorkspaceUsage { + member_count: usage.member_count, + member_count_limit: usage.member_count_limit, + total_blob_bytes: usage.total_blob_bytes, + total_blob_bytes_limit: usage.total_blob_bytes_limit, + }) + }) + } + + fn get_billing_portal_url(&self) -> FutureResult<String, FlowyError> { + let try_get_client = self.server.try_get_client(); + FutureResult::new(async move { + let client = try_get_client?; + let url = client.get_portal_session_link().await?; + Ok(url) + }) } } @@ -597,18 +608,9 @@ async fn get_admin_client(client: &Arc<AFCloudClient>) -> FlowyResult<Client> { ClientConfiguration::default(), &client.client_version.to_string(), ); - // When multiple admin_client instances attempt to sign in concurrently, multiple admin user - // creation transaction will be created, but only the first attempt will succeed due to the - // unique email constraint. Once the user has been created, admin_client instances can sign in - // concurrently without issue. - let resp = admin_client + admin_client .sign_in_password(&admin_email, &admin_password) - .await; - if resp.is_err() { - admin_client - .sign_in_password(&admin_email, &admin_password) - .await?; - }; + .await?; Ok(admin_client) } @@ -652,11 +654,8 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace { id: af_workspace.workspace_id.to_string(), name: af_workspace.workspace_name, created_at: af_workspace.created_at, - workspace_database_id: af_workspace.database_storage_id.to_string(), + database_indexer_id: af_workspace.database_storage_id.to_string(), icon: af_workspace.icon, - member_count: af_workspace.member_count.unwrap_or(0), - role: af_workspace.role.map(|r| r.into()), - workspace_type: AuthType::AppFlowyCloud, } } @@ -690,3 +689,50 @@ fn oauth_params_from_box_any(any: BoxAny) -> Result<AFCloudOAuthParams, FlowyErr sign_in_url: sign_in_url.to_string(), }) } + +fn to_recurring_interval( + r: flowy_user_pub::entities::RecurringInterval, +) -> client_api::entity::billing_dto::RecurringInterval { + match r { + flowy_user_pub::entities::RecurringInterval::Month => { + client_api::entity::billing_dto::RecurringInterval::Month + }, + flowy_user_pub::entities::RecurringInterval::Year => { + client_api::entity::billing_dto::RecurringInterval::Year + }, + } +} + +fn to_workspace_subscription_plan( + s: flowy_user_pub::entities::SubscriptionPlan, +) -> Result<SubscriptionPlan, FlowyError> { + match s { + flowy_user_pub::entities::SubscriptionPlan::Pro => Ok(SubscriptionPlan::Pro), + flowy_user_pub::entities::SubscriptionPlan::Team => Ok(SubscriptionPlan::Team), + flowy_user_pub::entities::SubscriptionPlan::None => Err(FlowyError::new( + ErrorCode::InvalidParams, + "Invalid subscription plan", + )), + } +} + +fn to_workspace_subscription(s: WorkspaceSubscriptionStatus) -> WorkspaceSubscription { + WorkspaceSubscription { + workspace_id: s.workspace_id, + subscription_plan: match s.workspace_plan { + WorkspaceSubscriptionPlan::Pro => flowy_user_pub::entities::SubscriptionPlan::Pro, + WorkspaceSubscriptionPlan::Team => flowy_user_pub::entities::SubscriptionPlan::Team, + _ => flowy_user_pub::entities::SubscriptionPlan::None, + }, + recurring_interval: match s.recurring_interval { + client_api::entity::billing_dto::RecurringInterval::Month => { + flowy_user_pub::entities::RecurringInterval::Month + }, + client_api::entity::billing_dto::RecurringInterval::Year => { + flowy_user_pub::entities::RecurringInterval::Year + }, + }, + is_active: matches!(s.subscription_status, SubscriptionStatus::Active), + canceled_at: s.canceled_at, + } +} diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs index 838e9dd6ca..a33852dd53 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/dto.rs @@ -3,12 +3,22 @@ use client_api::entity::auth_dto::{UpdateUserParams, UserMetaData}; use client_api::entity::{AFRole, AFUserProfile, AFWorkspaceInvitationStatus, AFWorkspaceMember}; use flowy_user_pub::entities::{ - AuthType, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, WorkspaceMember, - USER_METADATA_ICON_URL, + Authenticator, Role, UpdateUserProfileParams, UserProfile, WorkspaceInvitationStatus, + WorkspaceMember, USER_METADATA_ICON_URL, USER_METADATA_OPEN_AI_KEY, + USER_METADATA_STABILITY_AI_KEY, }; +use crate::af_cloud::impls::user::util::encryption_type_from_profile; + pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUserParams { let mut user_metadata = UserMetaData::new(); + if let Some(openai_key) = update.openai_key { + user_metadata.insert(USER_METADATA_OPEN_AI_KEY, openai_key); + } + + if let Some(stability_ai_key) = update.stability_ai_key { + user_metadata.insert(USER_METADATA_STABILITY_AI_KEY, stability_ai_key); + } if let Some(icon_url) = update.icon_url { user_metadata.insert(USER_METADATA_ICON_URL, icon_url); @@ -25,14 +35,20 @@ pub fn af_update_from_update_params(update: UpdateUserProfileParams) -> UpdateUs pub fn user_profile_from_af_profile( token: String, profile: AFUserProfile, - workspace_auth_type: AuthType, ) -> Result<UserProfile, Error> { - let icon_url = { + let encryption_type = encryption_type_from_profile(&profile); + let (icon_url, openai_key, stability_ai_key) = { profile .metadata .map(|m| { - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + ( + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + m.get(USER_METADATA_OPEN_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + m.get(USER_METADATA_STABILITY_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()), + ) }) .unwrap_or_default() }; @@ -42,10 +58,13 @@ pub fn user_profile_from_af_profile( name: profile.name.unwrap_or("".to_string()), token, icon_url: icon_url.unwrap_or_default(), - auth_type: AuthType::AppFlowyCloud, + openai_key: openai_key.unwrap_or_default(), + stability_ai_key: stability_ai_key.unwrap_or_default(), + workspace_id: profile.latest_workspace_id.to_string(), + authenticator: Authenticator::AppFlowyCloud, + encryption_type, uid: profile.uid, updated_at: profile.updated_at, - workspace_auth_type, }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs index 300738c833..4075a5b908 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/util.rs @@ -1,24 +1,22 @@ -use crate::af_cloud::define::LoggedUser; +use crate::af_cloud::define::ServerUser; use flowy_error::{FlowyError, FlowyResult}; -use std::sync::Weak; +use std::sync::Arc; use tracing::warn; -use uuid::Uuid; /// Validates the workspace_id provided in the request. /// It checks that the workspace_id from the request matches the current user's active workspace_id. /// This ensures that the operation is being performed in the correct workspace context, enhancing security. pub fn check_request_workspace_id_is_match( - expected_workspace_id: &Uuid, - user: &Weak<dyn LoggedUser>, + expected_workspace_id: &str, + user: &Arc<dyn ServerUser>, action: impl AsRef<str>, ) -> FlowyResult<()> { - let user = user.upgrade().ok_or_else(FlowyError::user_not_login)?; let actual_workspace_id = user.workspace_id()?; - if expected_workspace_id != &actual_workspace_id { + if expected_workspace_id != actual_workspace_id { warn!( "{}, expect workspace_id: {}, actual workspace_id: {}", action.as_ref(), - expected_workspace_id.to_string(), + expected_workspace_id, actual_workspace_id ); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 500c78c930..6e50eb3b59 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -1,10 +1,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Weak}; +use std::sync::Arc; use std::time::Duration; -use crate::af_cloud::define::LoggedUser; use anyhow::Error; -use arc_swap::ArcSwap; use client_api::collab_sync::ServerCollabMessage; use client_api::entity::UserMessage; use client_api::notify::{TokenState, TokenStateReceiver}; @@ -12,35 +10,35 @@ use client_api::ws::{ ConnectState, WSClient, WSClientConfig, WSConnectStateReceiver, WebSocketChannel, }; use client_api::{Client, ClientConfiguration}; - -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; -use flowy_document_pub::cloud::DocumentCloudService; -use flowy_error::{ErrorCode, FlowyError}; -use flowy_folder_pub::cloud::FolderCloudService; +use flowy_chat_pub::cloud::ChatCloudService; use flowy_search_pub::cloud::SearchCloudService; -use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_storage_pub::cloud::StorageCloudService; -use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; -use flowy_user_pub::entities::UserTokenState; - -use crate::af_cloud::impls::{ - AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, - AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, CloudChatServiceImpl, -}; -use crate::AppFlowyServer; -use flowy_ai::offline::offline_message_sync::AutoSyncChatService; -use flowy_ai_pub::user_service::AIUserService; +use flowy_storage::ObjectStorageService; use rand::Rng; use semver::Version; use tokio::select; -use tokio::sync::watch; -use tokio::task::JoinHandle; +use tokio::sync::{watch, Mutex}; use tokio_stream::wrappers::WatchStream; use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; +use tracing::{error, event, info, warn}; use uuid::Uuid; +use crate::af_cloud::define::ServerUser; +use flowy_database_pub::cloud::DatabaseCloudService; +use flowy_document_pub::cloud::DocumentCloudService; +use flowy_error::{ErrorCode, FlowyError}; +use flowy_folder_pub::cloud::FolderCloudService; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; +use flowy_user_pub::entities::UserTokenState; +use lib_dispatch::prelude::af_spawn; + +use crate::af_cloud::impls::{ + AFCloudChatCloudServiceImpl, AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, + AFCloudFileStorageServiceImpl, AFCloudFolderCloudServiceImpl, AFCloudUserAuthServiceImpl, +}; + +use crate::AppFlowyServer; + use super::impls::AFCloudSearchCloudServiceImpl; pub(crate) type AFCloudClient = Client; @@ -53,8 +51,7 @@ pub struct AppFlowyCloudServer { network_reachable: Arc<AtomicBool>, pub device_id: String, ws_client: Arc<WSClient>, - logged_user: Weak<dyn LoggedUser>, - ai_user_service: Arc<dyn AIUserService>, + user: Arc<dyn ServerUser>, } impl AppFlowyCloudServer { @@ -63,8 +60,7 @@ impl AppFlowyCloudServer { enable_sync: bool, mut device_id: String, client_version: Version, - logged_user: Weak<dyn LoggedUser>, - ai_user_service: Arc<dyn AIUserService>, + user: Arc<dyn ServerUser>, ) -> Self { // The device id can't be empty, so we generate a new one if it is. if device_id.is_empty() { @@ -93,8 +89,15 @@ impl AppFlowyCloudServer { ); let ws_client = Arc::new(ws_client); let api_client = Arc::new(api_client); - spawn_ws_conn(token_state_rx, &ws_client, &api_client, &enable_sync); + let ws_connect_cancellation_token = Arc::new(Mutex::new(CancellationToken::new())); + spawn_ws_conn( + token_state_rx, + &ws_client, + ws_connect_cancellation_token, + &api_client, + &enable_sync, + ); Self { config, client: api_client, @@ -102,18 +105,16 @@ impl AppFlowyCloudServer { network_reachable, device_id, ws_client, - logged_user, - ai_user_service, + user, } } - fn get_server_impl(&self) -> AFServerImpl { - let client = if self.enable_sync.load(Ordering::SeqCst) { + fn get_client(&self) -> Option<Arc<AFCloudClient>> { + if self.enable_sync.load(Ordering::SeqCst) { Some(self.client.clone()) } else { None - }; - AFServerImpl { client } + } } } @@ -125,24 +126,17 @@ impl AppFlowyServer for AppFlowyCloudServer { .map_err(|err| Error::new(FlowyError::unauthorized().with_context(err))) } - fn set_ai_model(&self, ai_model: &str) -> Result<(), Error> { - self.client.set_ai_model(ai_model.to_string()); - Ok(()) - } - fn subscribe_token_state(&self) -> Option<WatchStream<UserTokenState>> { let mut token_state_rx = self.client.subscribe_token_state(); let (watch_tx, watch_rx) = watch::channel(UserTokenState::Init); let weak_client = Arc::downgrade(&self.client); - tokio::spawn(async move { + af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { if let Some(client) = weak_client.upgrade() { match token_state { TokenState::Refresh => match client.get_token() { Ok(token) => { - if let Err(err) = watch_tx.send(UserTokenState::Refresh { token }) { - error!("Failed to send token after token state changed: {}", err); - } + let _ = watch_tx.send(UserTokenState::Refresh { token }); }, Err(err) => { error!("Failed to get token after token state changed: {}", err); @@ -169,9 +163,12 @@ impl AppFlowyServer for AppFlowyCloudServer { } fn user_service(&self) -> Arc<dyn UserCloudService> { + let server = AFServerImpl { + client: self.get_client(), + }; let mut user_change = self.ws_client.subscribe_user_changed(); let (tx, rx) = tokio::sync::mpsc::channel(1); - tokio::spawn(async move { + af_spawn(async move { while let Ok(user_message) = user_change.recv().await { if let UserMessage::ProfileChange(change) = user_message { let user_update = UserUpdate { @@ -186,47 +183,47 @@ impl AppFlowyServer for AppFlowyCloudServer { }); Arc::new(AFCloudUserAuthServiceImpl::new( - self.get_server_impl(), + server, rx, - self.logged_user.clone(), + self.user.clone(), )) } fn folder_service(&self) -> Arc<dyn FolderCloudService> { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudFolderCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } fn database_service(&self) -> Arc<dyn DatabaseCloudService> { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } - fn database_ai_service(&self) -> Option<Arc<dyn DatabaseAIService>> { - Some(Arc::new(AFCloudDatabaseCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), - })) - } - fn document_service(&self) -> Arc<dyn DocumentCloudService> { + let server = AFServerImpl { + client: self.get_client(), + }; Arc::new(AFCloudDocumentCloudServiceImpl { - inner: self.get_server_impl(), - logged_user: self.logged_user.clone(), + inner: server, + user: self.user.clone(), }) } fn chat_service(&self) -> Arc<dyn ChatCloudService> { - Arc::new(AutoSyncChatService::new( - Arc::new(CloudChatServiceImpl { - inner: self.get_server_impl(), - }), - self.ai_user_service.clone(), - )) + let server = AFServerImpl { + client: self.get_client(), + }; + Arc::new(AFCloudChatCloudServiceImpl { inner: server }) } fn subscribe_ws_state(&self) -> Option<WSConnectStateReceiver> { @@ -255,17 +252,19 @@ impl AppFlowyServer for AppFlowyCloudServer { Ok(channel.map(|c| (c, connect_state_recv, self.ws_client.is_connected()))) } - fn file_storage(&self) -> Option<Arc<dyn StorageCloudService>> { - Some(Arc::new(AFCloudFileStorageServiceImpl::new( - self.get_server_impl(), - self.config.maximum_upload_file_size_in_bytes, - ))) + fn file_storage(&self) -> Option<Arc<dyn ObjectStorageService>> { + let client = AFServerImpl { + client: self.get_client(), + }; + Some(Arc::new(AFCloudFileStorageServiceImpl::new(client))) } fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> { - Some(Arc::new(AFCloudSearchCloudServiceImpl { - inner: self.get_server_impl(), - })) + let server = AFServerImpl { + client: self.get_client(), + }; + + Some(Arc::new(AFCloudSearchCloudServiceImpl { inner: server })) } } @@ -276,17 +275,16 @@ impl AppFlowyServer for AppFlowyCloudServer { fn spawn_ws_conn( mut token_state_rx: TokenStateReceiver, ws_client: &Arc<WSClient>, + conn_cancellation_token: Arc<Mutex<CancellationToken>>, api_client: &Arc<Client>, enable_sync: &Arc<AtomicBool>, ) { let weak_ws_client = Arc::downgrade(ws_client); let weak_api_client = Arc::downgrade(api_client); let enable_sync = enable_sync.clone(); + let cloned_conn_cancellation_token = conn_cancellation_token.clone(); - let cancellation_token = Arc::new(ArcSwap::new(Arc::new(CancellationToken::new()))); - let cloned_cancellation_token = cancellation_token.clone(); - - tokio::spawn(async move { + af_spawn(async move { if let Some(ws_client) = weak_ws_client.upgrade() { let mut state_recv = ws_client.subscribe_connect_state(); while let Ok(state) = state_recv.recv().await { @@ -295,7 +293,7 @@ fn spawn_ws_conn( ConnectState::PingTimeout | ConnectState::Lost => { // Try to reconnect if the connection is timed out. if weak_api_client.upgrade().is_some() && enable_sync.load(Ordering::SeqCst) { - attempt_reconnect(&ws_client, 2, &cloned_cancellation_token).await; + attempt_reconnect(&ws_client, 2, &cloned_conn_cancellation_token).await; } }, ConnectState::Unauthorized => { @@ -315,13 +313,13 @@ fn spawn_ws_conn( }); let weak_ws_client = Arc::downgrade(ws_client); - tokio::spawn(async move { + af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { info!("🟢token state: {:?}", token_state); match token_state { TokenState::Refresh => { if let Some(ws_client) = weak_ws_client.upgrade() { - attempt_reconnect(&ws_client, 5, &cancellation_token).await; + attempt_reconnect(&ws_client, 5, &conn_cancellation_token).await; } }, TokenState::Invalid => { @@ -344,29 +342,34 @@ fn spawn_ws_conn( async fn attempt_reconnect( ws_client: &Arc<WSClient>, minimum_delay_in_secs: u64, - cancellation_token: &Arc<ArcSwap<CancellationToken>>, -) -> JoinHandle<()> { - cancellation_token.load_full().cancel(); - let new_cancel_token = CancellationToken::new(); - cancellation_token.store(Arc::new(new_cancel_token.clone())); + conn_cancellation_token: &Arc<Mutex<CancellationToken>>, +) { + // Cancel the previous reconnection attempt + let mut cancel_token_lock = conn_cancellation_token.lock().await; + cancel_token_lock.cancel(); + let new_cancel_token = CancellationToken::new(); + *cancel_token_lock = new_cancel_token.clone(); + drop(cancel_token_lock); + + // randomness in the reconnection attempts to avoid thundering herd problem let delay_seconds = rand::thread_rng().gen_range(minimum_delay_in_secs..10); - let ws_client_clone = ws_client.clone(); + let ws_client = ws_client.clone(); tokio::spawn(async move { select! { - // If the new cancellation token is triggered, log cancellation - _ = new_cancel_token.cancelled() => { - tracing::trace!("🟢 websocket reconnection attempt cancelled."); - }, - _ = tokio::time::sleep(Duration::from_secs(delay_seconds)) => { - if let Err(e) = ws_client_clone.connect().await { - error!("❌ Failed to reconnect websocket: {}", e); - } else { - info!("✅ Reconnected websocket successfully."); - } + _ = new_cancel_token.cancelled() => { + event!( + tracing::Level::TRACE, + "🟢websocket reconnection attempt cancelled." + ); + }, + _ = tokio::time::sleep(Duration::from_secs(delay_seconds)) => { + if let Err(e) = ws_client.connect().await { + error!("Failed to reconnect websocket: {}", e); } + } } - }) + }); } pub trait AFServer: Send + Sync + 'static { diff --git a/frontend/rust-lib/flowy-server/src/default_impl.rs b/frontend/rust-lib/flowy-server/src/default_impl.rs new file mode 100644 index 0000000000..90d9bf15a7 --- /dev/null +++ b/frontend/rust-lib/flowy-server/src/default_impl.rs @@ -0,0 +1,99 @@ +use client_api::entity::ai_dto::RepeatedRelatedQuestion; +use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage}; +use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, StreamAnswer}; +use flowy_error::FlowyError; +use lib_infra::async_trait::async_trait; +use lib_infra::future::FutureResult; + +pub(crate) struct DefaultChatCloudServiceImpl; + +#[async_trait] +impl ChatCloudService for DefaultChatCloudServiceImpl { + fn create_chat( + &self, + _uid: &i64, + _workspace_id: &str, + _chat_id: &str, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } + + async fn send_chat_message( + &self, + _workspace_id: &str, + _chat_id: &str, + _message: &str, + _message_type: ChatMessageType, + ) -> Result<ChatMessageStream, FlowyError> { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + fn send_question( + &self, + _workspace_id: &str, + _chat_id: &str, + _message: &str, + _message_type: ChatMessageType, + ) -> FutureResult<ChatMessage, FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } + + fn save_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _message: &str, + _question_id: i64, + ) -> FutureResult<ChatMessage, FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } + + async fn stream_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _message_id: i64, + ) -> Result<StreamAnswer, FlowyError> { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + } + + fn get_chat_messages( + &self, + _workspace_id: &str, + _chat_id: &str, + _offset: MessageCursor, + _limit: u64, + ) -> FutureResult<RepeatedChatMessage, FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } + + fn get_related_message( + &self, + _workspace_id: &str, + _chat_id: &str, + _message_id: i64, + ) -> FutureResult<RepeatedRelatedQuestion, FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } + + fn generate_answer( + &self, + _workspace_id: &str, + _chat_id: &str, + _question_message_id: i64, + ) -> FutureResult<ChatMessage, FlowyError> { + FutureResult::new(async move { + Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) + }) + } +} diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index 034991a984..704e9e0e49 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -5,4 +5,8 @@ pub mod local_server; mod response; mod server; +#[cfg(feature = "enable_supabase")] +pub mod supabase; + +mod default_impl; pub mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs deleted file mode 100644 index 845b6dec1c..0000000000 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/chat.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::af_cloud::define::LoggedUser; -use chrono::{TimeZone, Utc}; -use client_api::entity::ai_dto::RepeatedRelatedQuestion; -use client_api::entity::CompletionStream; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_ai::local_ai::stream_util::QuestionStream; -use flowy_ai_pub::cloud::chat_dto::{ChatAuthor, ChatAuthorType}; -use flowy_ai_pub::cloud::{ - AIModel, AppErrorCode, AppResponseError, ChatCloudService, ChatMessage, ChatMessageType, - ChatSettings, CompleteTextParams, MessageCursor, ModelList, RelatedQuestion, RepeatedChatMessage, - ResponseFormat, StreamAnswer, StreamComplete, UpdateChatParams, DEFAULT_AI_MODEL_NAME, -}; -use flowy_ai_pub::persistence::{ - deserialize_chat_metadata, deserialize_rag_ids, read_chat, - select_answer_where_match_reply_message_id, select_chat_messages, select_message_content, - serialize_chat_metadata, serialize_rag_ids, update_chat, upsert_chat, upsert_chat_messages, - ChatMessageTable, ChatTable, ChatTableChangeset, -}; -use flowy_error::{FlowyError, FlowyResult}; -use futures_util::{stream, StreamExt, TryStreamExt}; -use lib_infra::async_trait::async_trait; -use lib_infra::util::timestamp; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::path::Path; -use std::sync::Arc; -use tracing::trace; -use uuid::Uuid; - -pub struct LocalChatServiceImpl { - pub logged_user: Arc<dyn LoggedUser>, - pub local_ai: Arc<LocalAIController>, -} - -impl LocalChatServiceImpl { - fn get_message_content(&self, message_id: i64) -> FlowyResult<String> { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let content = select_message_content(db, message_id)?.ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("Message not found: {}", message_id)) - })?; - Ok(content) - } - - async fn upsert_message(&self, chat_id: &Uuid, message: ChatMessage) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let conn = self.logged_user.get_sqlite_db(uid)?; - let row = ChatMessageTable::from_message(chat_id.to_string(), message, true); - upsert_chat_messages(conn, &[row])?; - Ok(()) - } -} - -#[async_trait] -impl ChatCloudService for LocalChatServiceImpl { - async fn create_chat( - &self, - _uid: &i64, - _workspace_id: &Uuid, - chat_id: &Uuid, - rag_ids: Vec<Uuid>, - _name: &str, - metadata: Value, - ) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = ChatTable::new(chat_id.to_string(), metadata, rag_ids, true); - upsert_chat(db, &row)?; - Ok(()) - } - - async fn create_question( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - message_type: ChatMessageType, - ) -> Result<ChatMessage, FlowyError> { - let message = match message_type { - ChatMessageType::System => ChatMessage::new_system(timestamp(), message.to_string()), - ChatMessageType::User => ChatMessage::new_human(timestamp(), message.to_string(), None), - }; - - self.upsert_message(chat_id, message.clone()).await?; - Ok(message) - } - - async fn create_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message: &str, - question_id: i64, - metadata: Option<serde_json::Value>, - ) -> Result<ChatMessage, FlowyError> { - let mut message = ChatMessage::new_ai(timestamp(), message.to_string(), Some(question_id)); - if let Some(metadata) = metadata { - message.metadata = metadata; - } - self.upsert_message(chat_id, message.clone()).await?; - Ok(message) - } - - async fn stream_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - format: ResponseFormat, - _ai_model: Option<AIModel>, - ) -> Result<StreamAnswer, FlowyError> { - if self.local_ai.is_running() { - let content = self.get_message_content(message_id)?; - match self - .local_ai - .stream_question( - &chat_id.to_string(), - &content, - Some(json!(format)), - json!({}), - ) - .await - { - Ok(stream) => Ok(QuestionStream::new(stream).boxed()), - Err(err) => Ok( - stream::once(async { Err(FlowyError::local_ai_unavailable().with_context(err)) }).boxed(), - ), - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } - - async fn get_answer( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - question_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - - match select_answer_where_match_reply_message_id(db, &chat_id.to_string(), question_id)? { - None => Err(FlowyError::record_not_found()), - Some(message) => Ok(chat_message_from_row(message)), - } - } - - async fn get_chat_messages( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - offset: MessageCursor, - limit: u64, - ) -> Result<RepeatedChatMessage, FlowyError> { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let result = select_chat_messages(db, &chat_id, limit, offset)?; - - let messages = result - .messages - .into_iter() - .map(chat_message_from_row) - .collect(); - - Ok(RepeatedChatMessage { - messages, - has_more: result.has_more, - total: result.total_count, - }) - } - - async fn get_question_from_answer_id( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - answer_message_id: i64, - ) -> Result<ChatMessage, FlowyError> { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = select_answer_where_match_reply_message_id(db, &chat_id, answer_message_id)? - .map(chat_message_from_row) - .ok_or_else(FlowyError::record_not_found)?; - Ok(row) - } - - async fn get_related_message( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - message_id: i64, - _ai_model: Option<AIModel>, - ) -> Result<RepeatedRelatedQuestion, FlowyError> { - if self.local_ai.is_running() { - let questions = self - .local_ai - .get_related_question(&chat_id.to_string()) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - trace!("LocalAI related questions: {:?}", questions); - - let items = questions - .into_iter() - .map(|content| RelatedQuestion { - content, - metadata: None, - }) - .collect::<Vec<_>>(); - - Ok(RepeatedRelatedQuestion { message_id, items }) - } else { - Ok(RepeatedRelatedQuestion { - message_id, - items: vec![], - }) - } - } - - async fn stream_complete( - &self, - _workspace_id: &Uuid, - params: CompleteTextParams, - _ai_model: Option<AIModel>, - ) -> Result<StreamComplete, FlowyError> { - if self.local_ai.is_running() { - match self - .local_ai - .complete_text_v2( - ¶ms.text, - params.completion_type.unwrap() as u8, - Some(json!(params.format)), - Some(json!(params.metadata)), - ) - .await - { - Ok(stream) => Ok( - CompletionStream::new( - stream.map_err(|err| AppResponseError::new(AppErrorCode::Internal, err.to_string())), - ) - .map_err(FlowyError::from) - .boxed(), - ), - Err(_) => Ok(stream::once(async { Err(FlowyError::local_ai_unavailable()) }).boxed()), - } - } else if self.local_ai.is_enabled() { - Err(FlowyError::local_ai_not_ready()) - } else { - Err(FlowyError::local_ai_disabled()) - } - } - - async fn embed_file( - &self, - _workspace_id: &Uuid, - file_path: &Path, - chat_id: &Uuid, - metadata: Option<HashMap<String, Value>>, - ) -> Result<(), FlowyError> { - if self.local_ai.is_running() { - self - .local_ai - .embed_file(&chat_id.to_string(), file_path.to_path_buf(), metadata) - .await - .map_err(|err| FlowyError::local_ai().with_context(err))?; - Ok(()) - } else { - Err(FlowyError::local_ai_not_ready()) - } - } - - async fn get_chat_settings( - &self, - _workspace_id: &Uuid, - chat_id: &Uuid, - ) -> Result<ChatSettings, FlowyError> { - let chat_id = chat_id.to_string(); - let uid = self.logged_user.user_id()?; - let db = self.logged_user.get_sqlite_db(uid)?; - let row = read_chat(db, &chat_id)?; - let rag_ids = deserialize_rag_ids(&row.rag_ids); - let metadata = deserialize_chat_metadata::<Value>(&row.metadata); - let setting = ChatSettings { - name: row.name, - rag_ids, - metadata, - }; - - Ok(setting) - } - - async fn update_chat_settings( - &self, - _workspace_id: &Uuid, - id: &Uuid, - s: UpdateChatParams, - ) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let mut db = self.logged_user.get_sqlite_db(uid)?; - let changeset = ChatTableChangeset { - chat_id: id.to_string(), - name: s.name, - metadata: s.metadata.map(|s| serialize_chat_metadata(&s)), - rag_ids: s.rag_ids.map(|s| serialize_rag_ids(&s)), - is_sync: None, - }; - - update_chat(&mut db, changeset)?; - Ok(()) - } - - async fn get_available_models(&self, _workspace_id: &Uuid) -> Result<ModelList, FlowyError> { - Ok(ModelList { models: vec![] }) - } - - async fn get_workspace_default_model(&self, _workspace_id: &Uuid) -> Result<String, FlowyError> { - Ok(DEFAULT_AI_MODEL_NAME.to_string()) - } -} - -fn chat_message_from_row(row: ChatMessageTable) -> ChatMessage { - let created_at = Utc - .timestamp_opt(row.created_at, 0) - .single() - .unwrap_or_else(Utc::now); - - let author_id = row.author_id.parse::<i64>().unwrap_or_default(); - let author_type = match row.author_type { - 1 => ChatAuthorType::Human, - 2 => ChatAuthorType::System, - 3 => ChatAuthorType::AI, - _ => ChatAuthorType::Unknown, - }; - - let metadata = row - .metadata - .map(|s| deserialize_chat_metadata::<Value>(&s)) - .unwrap_or_else(|| json!({})); - - ChatMessage { - author: ChatAuthor { - author_id, - author_type, - meta: None, - }, - message_id: row.message_id, - content: row.content, - created_at, - metadata, - reply_message_id: row.reply_message_id, - } -} diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index ad1184a09a..6c923d7d89 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,65 +1,99 @@ -#![allow(unused_variables)] - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::util::default_encode_collab_for_collab_type; -use collab::entity::EncodedCollab; +use anyhow::Error; +use collab::preclude::Collab; +use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES}; use collab_entity::CollabType; -use flowy_database_pub::cloud::{DatabaseCloudService, DatabaseSnapshot, EncodeCollabByOid}; -use flowy_error::{ErrorCode, FlowyError}; -use lib_infra::async_trait::async_trait; -use std::sync::Arc; -use uuid::Uuid; +use yrs::{Any, MapPrelim}; -pub(crate) struct LocalServerDatabaseCloudServiceImpl { - pub logged_user: Arc<dyn LoggedUser>, -} +use flowy_database_pub::cloud::{ + CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, +}; +use lib_infra::future::FutureResult; + +pub(crate) struct LocalServerDatabaseCloudServiceImpl(); -#[async_trait] impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { - async fn get_database_encode_collab( + fn get_database_object_doc_state( &self, - object_id: &Uuid, + object_id: &str, collab_type: CollabType, - _workspace_id: &Uuid, // underscore to silence “unused” warning - ) -> Result<Option<EncodedCollab>, FlowyError> { - let uid = self.logged_user.user_id()?; + _workspace_id: &str, + ) -> FutureResult<Option<Vec<u8>>, Error> { let object_id = object_id.to_string(); - default_encode_collab_for_collab_type(uid, &object_id, collab_type) - .await - .map(Some) - .or_else(|err| { - if matches!(err.code, ErrorCode::NotSupportYet) { - Ok(None) - } else { - Err(err) - } - }) + // create the minimal required data for the given collab type + FutureResult::new(async move { + let data = match collab_type { + CollabType::Database => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.insert_map_with_txn(txn, DATABASE); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + CollabType::WorkspaceDatabase => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.create_array_with_txn::<MapPrelim<Any>>(txn, WORKSPACE_DATABASES, vec![]); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + CollabType::DatabaseRow => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.insert_map_with_txn(txn, DATABASE_ROW_DATA); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + _ => vec![], + }; + + Ok(Some(data)) + }) } - async fn create_database_encode_collab( + fn batch_get_database_object_doc_state( &self, - object_id: &Uuid, - collab_type: CollabType, - workspace_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - Ok(()) + _object_ids: Vec<String>, + _object_ty: CollabType, + _workspace_id: &str, + ) -> FutureResult<CollabDocStateByOid, Error> { + FutureResult::new(async move { Ok(CollabDocStateByOid::default()) }) } - async fn batch_get_database_encode_collab( + fn get_database_collab_object_snapshots( &self, - object_ids: Vec<Uuid>, - object_ty: CollabType, - workspace_id: &Uuid, - ) -> Result<EncodeCollabByOid, FlowyError> { - Ok(EncodeCollabByOid::default()) + _object_id: &str, + _limit: usize, + ) -> FutureResult<Vec<DatabaseSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } - async fn get_database_collab_object_snapshots( + fn summary_database_row( &self, - object_id: &Uuid, - limit: usize, - ) -> Result<Vec<DatabaseSnapshot>, FlowyError> { - Ok(vec![]) + _workspace_id: &str, + _object_id: &str, + _summary_row: SummaryRowContent, + ) -> FutureResult<String, Error> { + // TODO(lucas): local ai + FutureResult::new(async move { Ok("".to_string()) }) + } + + fn translate_database_row( + &self, + _workspace_id: &str, + _translate_row: TranslateRowContent, + _language: &str, + ) -> FutureResult<TranslateRowResponse, Error> { + // TODO(lucas): local ai + FutureResult::new(async move { Ok(TranslateRowResponse::default()) }) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index c553026274..bc712d03d0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,50 +1,40 @@ -#![allow(unused_variables)] -use collab::entity::EncodedCollab; +use anyhow::Error; + use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; -use lib_infra::async_trait::async_trait; -use uuid::Uuid; +use lib_infra::future::FutureResult; pub(crate) struct LocalServerDocumentCloudServiceImpl(); -#[async_trait] impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { - async fn get_document_doc_state( + fn get_document_doc_state( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { + document_id: &str, + _workspace_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { let document_id = document_id.to_string(); - - Err(FlowyError::new( - ErrorCode::RecordNotFound, - format!("Document {} not found", document_id), - )) + FutureResult::new(async move { + Err(FlowyError::new( + ErrorCode::RecordNotFound, + format!("Document {} not found", document_id), + )) + }) } - async fn get_document_snapshots( + fn get_document_snapshots( &self, - document_id: &Uuid, - limit: usize, - workspace_id: &str, - ) -> Result<Vec<DocumentSnapshot>, FlowyError> { - Ok(vec![]) + _document_id: &str, + _limit: usize, + _workspace_id: &str, + ) -> FutureResult<Vec<DocumentSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } - async fn get_document_data( + fn get_document_data( &self, - document_id: &Uuid, - workspace_id: &Uuid, - ) -> Result<Option<DocumentData>, FlowyError> { - Ok(None) - } - - async fn create_document_collab( - &self, - workspace_id: &Uuid, - document_id: &Uuid, - encoded_collab: EncodedCollab, - ) -> Result<(), FlowyError> { - Ok(()) + _document_id: &str, + _workspace_id: &str, + ) -> FutureResult<Option<DocumentData>, Error> { + FutureResult::new(async move { Ok(None) }) } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 79b1d4be12..ea0ee027b9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,156 +1,80 @@ -#![allow(unused_variables)] - -use crate::af_cloud::define::LoggedUser; -use crate::local_server::util::default_encode_collab_for_collab_type; -use client_api::entity::workspace_dto::PublishInfoView; -use client_api::entity::PublishInfo; -use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; -use collab_entity::CollabType; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_error::FlowyError; -use flowy_folder_pub::cloud::{ - FolderCloudService, FolderCollabParams, FolderSnapshot, FullSyncCollabParams, -}; -use flowy_folder_pub::entities::PublishPayload; -use lib_infra::async_trait::async_trait; use std::sync::Arc; -use uuid::Uuid; + +use anyhow::{anyhow, Error}; +use collab_entity::CollabType; + +use flowy_folder_pub::cloud::{ + gen_workspace_id, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, + WorkspaceRecord, +}; +use lib_infra::future::FutureResult; + +use crate::local_server::LocalServerDB; pub(crate) struct LocalServerFolderCloudServiceImpl { #[allow(dead_code)] - pub logged_user: Arc<dyn LoggedUser>, + pub db: Arc<dyn LocalServerDB>, } -#[async_trait] impl FolderCloudService for LocalServerFolderCloudServiceImpl { - async fn get_folder_snapshots( + fn create_workspace(&self, uid: i64, name: &str) -> FutureResult<Workspace, Error> { + let name = name.to_string(); + FutureResult::new(async move { + Ok(Workspace::new( + gen_workspace_id().to_string(), + name.to_string(), + uid, + )) + }) + } + + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } + + fn get_all_workspace(&self) -> FutureResult<Vec<WorkspaceRecord>, Error> { + FutureResult::new(async { Ok(vec![]) }) + } + + fn get_folder_data( + &self, + _workspace_id: &str, + _uid: &i64, + ) -> FutureResult<Option<FolderData>, Error> { + FutureResult::new(async move { Ok(None) }) + } + + fn get_folder_snapshots( &self, _workspace_id: &str, _limit: usize, - ) -> Result<Vec<FolderSnapshot>, FlowyError> { - Ok(vec![]) + ) -> FutureResult<Vec<FolderSnapshot>, Error> { + FutureResult::new(async move { Ok(vec![]) }) } - async fn get_folder_doc_state( + fn get_folder_doc_state( &self, - workspace_id: &Uuid, - uid: i64, - collab_type: CollabType, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { - let object_id = object_id.to_string(); - let workspace_id = workspace_id.to_string(); - let collab_db = self.logged_user.get_collab_db(uid)?.upgrade().unwrap(); - let read_txn = collab_db.read_txn(); - let is_exist = read_txn.is_exist(uid, &workspace_id.to_string(), &object_id.to_string()); - if is_exist { - // load doc - let collab = Collab::new_with_origin(CollabOrigin::Empty, &object_id, vec![], false); - read_txn.load_doc(uid, &workspace_id, &object_id, collab.doc())?; - let data = collab.encode_collab_v1(|c| { - collab_type - .validate_require_data(c) - .map_err(|err| FlowyError::invalid_data().with_context(err))?; - Ok::<_, FlowyError>(()) - })?; - Ok(data.doc_state.to_vec()) - } else { - let data = default_encode_collab_for_collab_type(uid, &object_id, collab_type).await?; - drop(read_txn); - Ok(data.doc_state.to_vec()) - } + _workspace_id: &str, + _uid: i64, + _collab_type: CollabType, + _object_id: &str, + ) -> FutureResult<Vec<u8>, Error> { + FutureResult::new(async { + Err(anyhow!( + "Local server doesn't support get collab doc state from remote" + )) + }) } - async fn full_sync_collab_object( + fn batch_create_folder_collab_objects( &self, - workspace_id: &Uuid, - params: FullSyncCollabParams, - ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn batch_create_folder_collab_objects( - &self, - workspace_id: &Uuid, - objects: Vec<FolderCollabParams>, - ) -> Result<(), FlowyError> { - Ok(()) + _workspace_id: &str, + _objects: Vec<FolderCollabParams>, + ) -> FutureResult<(), Error> { + FutureResult::new(async { Err(anyhow!("Local server doesn't support create collab")) }) } fn service_name(&self) -> String { "Local".to_string() } - - async fn publish_view( - &self, - workspace_id: &Uuid, - payload: Vec<PublishPayload>, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn unpublish_views( - &self, - workspace_id: &Uuid, - view_ids: Vec<Uuid>, - ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_publish_info(&self, view_id: &Uuid) -> Result<PublishInfo, FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn set_publish_name( - &self, - workspace_id: &Uuid, - view_id: Uuid, - new_name: String, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn set_publish_namespace( - &self, - workspace_id: &Uuid, - new_namespace: String, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn list_published_views( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<PublishInfoView>, FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn get_default_published_view_info( - &self, - workspace_id: &Uuid, - ) -> Result<PublishInfo, FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn set_default_published_view( - &self, - workspace_id: &Uuid, - view_id: uuid::Uuid, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn remove_default_published_view(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn get_publish_namespace(&self, workspace_id: &Uuid) -> Result<String, FlowyError> { - Err(FlowyError::local_version_not_support()) - } - - async fn import_zip(&self, _file_path: &str) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support()) - } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs index f63265e734..0280cfbefb 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/mod.rs @@ -1,10 +1,8 @@ -pub(crate) use chat::*; pub(crate) use database::*; pub(crate) use document::*; pub(crate) use folder::*; pub(crate) use user::*; -mod chat; mod database; mod document; mod folder; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index f011c16d90..d5fa1524b6 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,340 +1,237 @@ -#![allow(unused_variables)] +use std::sync::Arc; -use crate::af_cloud::define::LoggedUser; -use crate::local_server::uid::UserIDGenerator; -use anyhow::Context; -use client_api::entity::GotrueTokenResponse; -use collab::core::origin::CollabOrigin; -use collab::preclude::Collab; use collab_entity::CollabObject; -use collab_user::core::UserAwareness; -use flowy_ai_pub::cloud::billing_dto::WorkspaceUsageAndLimit; -use flowy_ai_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; +use lazy_static::lazy_static; +use parking_lot::Mutex; +use uuid::Uuid; + use flowy_error::FlowyError; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; use flowy_user_pub::entities::*; -use flowy_user_pub::sql::{ - insert_local_workspace, select_all_user_workspace, select_user_profile, select_user_workspace, - select_workspace_member, select_workspace_setting, update_user_profile, update_workspace_setting, - upsert_workspace_member, upsert_workspace_setting, UserTableChangeset, WorkspaceMemberTable, - WorkspaceSettingsChangeset, WorkspaceSettingsTable, -}; use flowy_user_pub::DEFAULT_USER_NAME; -use lazy_static::lazy_static; -use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; use lib_infra::util::timestamp; -use std::sync::Arc; -use tokio::sync::Mutex; -use uuid::Uuid; + +use crate::local_server::uid::UserIDGenerator; +use crate::local_server::LocalServerDB; lazy_static! { static ref ID_GEN: Mutex<UserIDGenerator> = Mutex::new(UserIDGenerator::new(1)); } -pub(crate) struct LocalServerUserServiceImpl { - pub logged_user: Arc<dyn LoggedUser>, +pub(crate) struct LocalServerUserAuthServiceImpl { + #[allow(dead_code)] + pub db: Arc<dyn LocalServerDB>, } -#[async_trait] -impl UserCloudService for LocalServerUserServiceImpl { - async fn sign_up(&self, params: BoxAny) -> Result<AuthResponse, FlowyError> { - let params = params.unbox_or_error::<SignUpParams>()?; - let uid = ID_GEN.lock().await.next_id(); - let workspace_id = Uuid::new_v4().to_string(); - let user_workspace = UserWorkspace::new_local(workspace_id, "My Workspace"); - let user_name = if params.name.is_empty() { - DEFAULT_USER_NAME() - } else { - params.name.clone() - }; - Ok(AuthResponse { - user_id: uid, - user_uuid: Uuid::new_v4(), - name: user_name, - latest_workspace: user_workspace.clone(), - user_workspaces: vec![user_workspace], - is_new_user: true, - // Anon user doesn't have email - email: None, - token: None, - encryption_type: EncryptionType::NoEncryption, - updated_at: timestamp(), - metadata: None, +impl UserCloudService for LocalServerUserAuthServiceImpl { + fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError> { + FutureResult::new(async move { + let params = params.unbox_or_error::<SignUpParams>()?; + let uid = ID_GEN.lock().next_id(); + let workspace_id = uuid::Uuid::new_v4().to_string(); + let user_workspace = UserWorkspace::new(&workspace_id, uid); + let user_name = if params.name.is_empty() { + DEFAULT_USER_NAME() + } else { + params.name.clone() + }; + Ok(AuthResponse { + user_id: uid, + user_uuid: Uuid::new_v4(), + name: user_name, + latest_workspace: user_workspace.clone(), + user_workspaces: vec![user_workspace], + is_new_user: true, + email: Some(params.email), + token: None, + encryption_type: EncryptionType::NoEncryption, + updated_at: timestamp(), + metadata: None, + }) }) } - async fn sign_in(&self, params: BoxAny) -> Result<AuthResponse, FlowyError> { - let params: SignInParams = params.unbox_or_error::<SignInParams>()?; - let uid = ID_GEN.lock().await.next_id(); + fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError> { + let db = self.db.clone(); + FutureResult::new(async move { + let params: SignInParams = params.unbox_or_error::<SignInParams>()?; + let uid = ID_GEN.lock().next_id(); - let workspace_id = Uuid::new_v4(); - let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), "My Workspace"); - Ok(AuthResponse { - user_id: uid, - user_uuid: Uuid::new_v4(), - name: params.name, - latest_workspace: user_workspace.clone(), - user_workspaces: vec![user_workspace], - is_new_user: false, - email: Some(params.email), - token: None, - encryption_type: EncryptionType::NoEncryption, - updated_at: timestamp(), - metadata: None, + let user_workspace = db + .get_user_workspace(uid)? + .unwrap_or_else(make_user_workspace); + Ok(AuthResponse { + user_id: uid, + user_uuid: Uuid::new_v4(), + name: params.name, + latest_workspace: user_workspace.clone(), + user_workspaces: vec![user_workspace], + is_new_user: false, + email: Some(params.email), + token: None, + encryption_type: EncryptionType::NoEncryption, + updated_at: timestamp(), + metadata: None, + }) }) } - async fn sign_out(&self, _token: Option<String>) -> Result<(), FlowyError> { - Ok(()) + fn sign_out(&self, _token: Option<String>) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn generate_sign_in_url_with_email(&self, _email: &str) -> Result<String, FlowyError> { - Err( - FlowyError::local_version_not_support() - .with_context("Not support generate sign in url with email"), - ) + fn generate_sign_in_url_with_email(&self, _email: &str) -> FutureResult<String, FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("Not support generate sign in url with email"), + ) + }) } - async fn create_user(&self, _email: &str, _password: &str) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support().with_context("Not support create user")) + fn create_user(&self, _email: &str, _password: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err(FlowyError::local_version_not_support().with_context("Not support create user")) + }) } - async fn sign_in_with_password( + fn sign_in_with_password( &self, _email: &str, _password: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { - Err(FlowyError::local_version_not_support().with_context("Not support")) + ) -> FutureResult<UserProfile, FlowyError> { + FutureResult::new(async { + Err(FlowyError::local_version_not_support().with_context("Not support")) + }) } - async fn sign_in_with_magic_link( + fn sign_in_with_magic_link( &self, _email: &str, _redirect_to: &str, - ) -> Result<(), FlowyError> { - Err(FlowyError::local_version_not_support().with_context("Not support")) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err(FlowyError::local_version_not_support().with_context("Not support")) + }) } - async fn sign_in_with_passcode( + fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult<String, FlowyError> { + FutureResult::new(async { + Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) + }) + } + + fn update_user( &self, - _email: &str, - _passcode: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { - Err(FlowyError::local_version_not_support().with_context("Not support")) + _credential: UserCredentials, + _params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn generate_oauth_url_with_provider(&self, _provider: &str) -> Result<String, FlowyError> { - Err(FlowyError::internal().with_context("Can't oauth url when using offline mode")) + fn get_user_profile(&self, credential: UserCredentials) -> FutureResult<UserProfile, FlowyError> { + let result = match credential.uid { + None => Err(FlowyError::record_not_found()), + Some(uid) => { + self.db.get_user_profile(uid).map(|mut profile| { + // We don't want to expose the email in the local server + profile.email = "".to_string(); + profile + }) + }, + }; + FutureResult::new(async { result }) } - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError> { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let changeset = UserTableChangeset::new(params); - update_user_profile(&mut conn, changeset)?; - Ok(()) + fn open_workspace(&self, _workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support open workspace"), + ) + }) } - async fn get_user_profile( + fn get_all_workspace(&self, _uid: i64) -> FutureResult<Vec<UserWorkspace>, FlowyError> { + FutureResult::new(async { Ok(vec![]) }) + } + + fn get_user_awareness_doc_state( &self, - uid: i64, - workspace_id: &str, - ) -> Result<UserProfile, FlowyError> { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, workspace_id, &mut conn)?; - Ok(profile) + _uid: i64, + _workspace_id: &str, + _object_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError> { + // must return record not found error + FutureResult::new(async { Err(FlowyError::record_not_found()) }) } - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<UserWorkspace, FlowyError> { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - let workspace = select_user_workspace(&workspace_id.to_string(), &mut conn)?; - Ok(UserWorkspace::from(workspace)) + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn get_all_workspace(&self, uid: i64) -> Result<Vec<UserWorkspace>, FlowyError> { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let workspaces = select_all_user_workspace(uid, &mut conn)?; - Ok(workspaces) - } - - async fn create_workspace(&self, workspace_name: &str) -> Result<UserWorkspace, FlowyError> { - let workspace_id = Uuid::new_v4(); - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let user_workspace = - insert_local_workspace(uid, &workspace_id.to_string(), workspace_name, &mut conn)?; - Ok(user_workspace) - } - - async fn patch_workspace( - &self, - workspace_id: &Uuid, - new_workspace_name: Option<String>, - new_workspace_icon: Option<String>, - ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_workspace_members( - &self, - workspace_id: Uuid, - ) -> Result<Vec<WorkspaceMember>, FlowyError> { - let uid = self.logged_user.user_id()?; - let member = self.get_workspace_member(&workspace_id, uid).await?; - Ok(vec![member]) - } - - async fn get_user_awareness_doc_state( - &self, - uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError> { - let collab = Collab::new_with_origin( - CollabOrigin::Empty, - object_id.to_string().as_str(), - vec![], - false, - ); - let awareness = UserAwareness::create(collab, None)?; - let encode_collab = awareness.encode_collab_v1(|_collab| Ok::<_, FlowyError>(()))?; - Ok(encode_collab.doc_state.to_vec()) - } - - async fn create_collab_object( + fn create_collab_object( &self, _collab_object: &CollabObject, _data: Vec<u8>, - ) -> Result<(), FlowyError> { - Ok(()) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn batch_create_collab_object( + fn batch_create_collab_object( &self, - workspace_id: &Uuid, - objects: Vec<UserCollabParams>, - ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_workspace_member( - &self, - workspace_id: &Uuid, - uid: i64, - ) -> Result<WorkspaceMember, FlowyError> { - // For local server, only current user is the member - let conn = self.logged_user.get_sqlite_db(uid)?; - let result = select_workspace_member(conn, &workspace_id.to_string(), uid); - - match result { - Ok(row) => Ok(WorkspaceMember::from(row)), - Err(err) => { - if err.is_record_not_found() { - let mut conn = self.logged_user.get_sqlite_db(uid)?; - let profile = select_user_profile(uid, &workspace_id.to_string(), &mut conn) - .context("Can't find user profile when create workspace member")?; - let row = WorkspaceMemberTable { - email: profile.email.to_string(), - role: Role::Owner as i32, - name: profile.name.to_string(), - avatar_url: Some(profile.icon_url), - uid, - workspace_id: workspace_id.to_string(), - updated_at: chrono::Utc::now().naive_utc(), - }; - - let member = WorkspaceMember::from(row.clone()); - upsert_workspace_member(&mut conn, row)?; - Ok(member) - } else { - Err(err) - } - }, - } - } - - async fn get_workspace_usage( - &self, - workspace_id: &Uuid, - ) -> Result<WorkspaceUsageAndLimit, FlowyError> { - Ok(WorkspaceUsageAndLimit { - member_count: 1, - member_count_limit: 1, - storage_bytes: i64::MAX, - storage_bytes_limit: i64::MAX, - storage_bytes_unlimited: true, - single_upload_limit: i64::MAX, - single_upload_unlimited: true, - ai_responses_count: i64::MAX, - ai_responses_count_limit: i64::MAX, - ai_image_responses_count: i64::MAX, - ai_image_responses_count_limit: 0, - local_ai: true, - ai_responses_unlimited: true, + _workspace_id: &str, + _objects: Vec<UserCollabParams>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support batch create collab object"), + ) }) } - async fn get_workspace_setting( - &self, - workspace_id: &Uuid, - ) -> Result<AFWorkspaceSettings, FlowyError> { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - // By default, workspace setting is existed in local server - let result = select_workspace_setting(&mut conn, &workspace_id.to_string()); - match result { - Ok(row) => Ok(AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model, - }), - Err(err) => { - if err.is_record_not_found() { - let row = WorkspaceSettingsTable { - id: workspace_id.to_string(), - disable_search_indexing: false, - ai_model: "".to_string(), - }; - let setting = AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model.clone(), - }; - upsert_workspace_setting(&mut conn, row)?; - Ok(setting) - } else { - Err(err) - } - }, - } + fn create_workspace(&self, _workspace_name: &str) -> FutureResult<UserWorkspace, FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + }) } - async fn update_workspace_setting( + fn delete_workspace(&self, _workspace_id: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) + }) + } + + fn patch_workspace( &self, - workspace_id: &Uuid, - workspace_settings: AFWorkspaceSettingsChange, - ) -> Result<AFWorkspaceSettings, FlowyError> { - let uid = self.logged_user.user_id()?; - let mut conn = self.logged_user.get_sqlite_db(uid)?; - - let changeset = WorkspaceSettingsChangeset { - id: workspace_id.to_string(), - disable_search_indexing: workspace_settings.disable_search_indexing, - ai_model: workspace_settings.ai_model, - }; - - update_workspace_setting(&mut conn, changeset)?; - let row = select_workspace_setting(&mut conn, &workspace_id.to_string())?; - - Ok(AFWorkspaceSettings { - disable_search_indexing: row.disable_search_indexing, - ai_model: row.ai_model, + _workspace_id: &str, + _new_workspace_name: Option<&str>, + _new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support multiple workspaces"), + ) }) } } + +fn make_user_workspace() -> UserWorkspace { + UserWorkspace { + id: uuid::Uuid::new_v4().to_string(), + name: "My Workspace".to_string(), + created_at: Default::default(), + database_indexer_id: uuid::Uuid::new_v4().to_string(), + icon: "".to_string(), + } +} diff --git a/frontend/rust-lib/flowy-server/src/local_server/mod.rs b/frontend/rust-lib/flowy-server/src/local_server/mod.rs index 2b9fe07250..6e67356fd9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/mod.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/mod.rs @@ -3,4 +3,3 @@ pub use server::*; pub mod impls; mod server; pub(crate) mod uid; -mod util; diff --git a/frontend/rust-lib/flowy-server/src/local_server/server.rs b/frontend/rust-lib/flowy-server/src/local_server/server.rs index 8829ded3fc..b2c17c900d 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/server.rs @@ -1,38 +1,46 @@ -use crate::af_cloud::define::LoggedUser; -use crate::local_server::impls::{ - LocalChatServiceImpl, LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, - LocalServerFolderCloudServiceImpl, LocalServerUserServiceImpl, -}; -use crate::AppFlowyServer; -use anyhow::Error; -use flowy_ai::local_ai::controller::LocalAIController; -use flowy_ai_pub::cloud::ChatCloudService; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; -use flowy_document_pub::cloud::DocumentCloudService; -use flowy_folder_pub::cloud::FolderCloudService; use flowy_search_pub::cloud::SearchCloudService; -use flowy_storage_pub::cloud::StorageCloudService; -use flowy_user_pub::cloud::UserCloudService; +use flowy_storage::ObjectStorageService; use std::sync::Arc; + +use parking_lot::RwLock; use tokio::sync::mpsc; +use flowy_database_pub::cloud::DatabaseCloudService; +use flowy_document_pub::cloud::DocumentCloudService; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::FolderCloudService; +// use flowy_user::services::database::{ +// get_user_profile, get_user_workspace, open_collab_db, open_user_db, +// }; +use flowy_user_pub::cloud::UserCloudService; +use flowy_user_pub::entities::*; + +use crate::local_server::impls::{ + LocalServerDatabaseCloudServiceImpl, LocalServerDocumentCloudServiceImpl, + LocalServerFolderCloudServiceImpl, LocalServerUserAuthServiceImpl, +}; +use crate::AppFlowyServer; + +pub trait LocalServerDB: Send + Sync + 'static { + fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError>; + fn get_user_workspace(&self, uid: i64) -> Result<Option<UserWorkspace>, FlowyError>; +} + pub struct LocalServer { - logged_user: Arc<dyn LoggedUser>, - local_ai: Arc<LocalAIController>, - stop_tx: Option<mpsc::Sender<()>>, + local_db: Arc<dyn LocalServerDB>, + stop_tx: RwLock<Option<mpsc::Sender<()>>>, } impl LocalServer { - pub fn new(logged_user: Arc<dyn LoggedUser>, local_ai: Arc<LocalAIController>) -> Self { + pub fn new(local_db: Arc<dyn LocalServerDB>) -> Self { Self { - logged_user, - local_ai, + local_db, stop_tx: Default::default(), } } pub async fn stop(&self) { - let sender = self.stop_tx.clone(); + let sender = self.stop_tx.read().clone(); if let Some(stop_tx) = sender { let _ = stop_tx.send(()).await; } @@ -40,48 +48,31 @@ impl LocalServer { } impl AppFlowyServer for LocalServer { - fn set_token(&self, _token: &str) -> Result<(), Error> { - Ok(()) - } - fn user_service(&self) -> Arc<dyn UserCloudService> { - Arc::new(LocalServerUserServiceImpl { - logged_user: self.logged_user.clone(), + Arc::new(LocalServerUserAuthServiceImpl { + db: self.local_db.clone(), }) } fn folder_service(&self) -> Arc<dyn FolderCloudService> { Arc::new(LocalServerFolderCloudServiceImpl { - logged_user: self.logged_user.clone(), + db: self.local_db.clone(), }) } fn database_service(&self) -> Arc<dyn DatabaseCloudService> { - Arc::new(LocalServerDatabaseCloudServiceImpl { - logged_user: self.logged_user.clone(), - }) - } - - fn database_ai_service(&self) -> Option<Arc<dyn DatabaseAIService>> { - None + Arc::new(LocalServerDatabaseCloudServiceImpl()) } fn document_service(&self) -> Arc<dyn DocumentCloudService> { Arc::new(LocalServerDocumentCloudServiceImpl()) } - fn chat_service(&self) -> Arc<dyn ChatCloudService> { - Arc::new(LocalChatServiceImpl { - logged_user: self.logged_user.clone(), - local_ai: self.local_ai.clone(), - }) + fn file_storage(&self) -> Option<Arc<dyn ObjectStorageService>> { + None } fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> { None } - - fn file_storage(&self) -> Option<Arc<dyn StorageCloudService>> { - None - } } diff --git a/frontend/rust-lib/flowy-server/src/local_server/util.rs b/frontend/rust-lib/flowy-server/src/local_server/util.rs deleted file mode 100644 index 378ccee6a2..0000000000 --- a/frontend/rust-lib/flowy-server/src/local_server/util.rs +++ /dev/null @@ -1,47 +0,0 @@ -use collab::core::origin::CollabOrigin; -use collab::entity::EncodedCollab; -use collab::preclude::Collab; -use collab_database::database::default_database_data; -use collab_database::workspace_database::default_workspace_database_data; -use collab_document::document_data::default_document_collab_data; -use collab_entity::CollabType; -use collab_user::core::default_user_awareness_data; -use flowy_error::{FlowyError, FlowyResult}; - -pub async fn default_encode_collab_for_collab_type( - _uid: i64, - object_id: &str, - collab_type: CollabType, -) -> FlowyResult<EncodedCollab> { - match collab_type { - CollabType::Document => { - let encode_collab = default_document_collab_data(object_id)?; - Ok(encode_collab) - }, - CollabType::Database => default_database_data(object_id).await.map_err(Into::into), - CollabType::WorkspaceDatabase => Ok(default_workspace_database_data(object_id)), - CollabType::Folder => { - // let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - // let workspace = Workspace::new(object_id.to_string(), "".to_string(), uid); - // let folder_data = FolderData::new(workspace); - // let folder = Folder::create(uid, collab, None, folder_data); - // let data = folder.encode_collab_v1(|c| { - // collab_type - // .validate_require_data(c) - // .map_err(|err| FlowyError::invalid_data().with_context(err))?; - // Ok::<_, FlowyError>(()) - // })?; - // Ok(data) - Err(FlowyError::not_support().with_context("Can not create default folder")) - }, - CollabType::DatabaseRow => { - Err(FlowyError::not_support().with_context("Can not create default database row")) - }, - CollabType::UserAwareness => Ok(default_user_awareness_data(object_id)), - CollabType::Unknown => { - let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - let data = collab.encode_collab_v1(|_| Ok::<_, FlowyError>(()))?; - Ok(data) - }, - } -} diff --git a/frontend/rust-lib/flowy-server/src/response.rs b/frontend/rust-lib/flowy-server/src/response.rs index dff2faf961..3e58fae69b 100644 --- a/frontend/rust-lib/flowy-server/src/response.rs +++ b/frontend/rust-lib/flowy-server/src/response.rs @@ -1,9 +1,13 @@ use std::fmt; +use anyhow::Error; use bytes::Bytes; +use reqwest::{Response, StatusCode}; use serde::{Deserialize, Serialize}; +use serde_json::Value; -use flowy_error::ErrorCode; +use flowy_error::{ErrorCode, FlowyError}; +use lib_infra::future::{to_fut, Fut}; #[derive(Debug, Serialize, Deserialize)] pub struct HttpResponse { @@ -30,3 +34,116 @@ impl fmt::Display for HttpError { write!(f, "{:?}: {}", self.code, self.msg) } } + +/// Trait `ExtendedResponse` provides an extension method to handle and transform the response data. +/// +/// This trait introduces a single method: +/// +/// - `get_value`: It extracts the value from the response, and returns it as an instance of a type `T`. +/// This method will return an error if the status code of the response signifies a failure (not success). +/// Otherwise, it attempts to parse the response body into an instance of type `T`, which must implement +/// `serde::de::DeserializeOwned`, `Send`, `Sync`, and have a static lifetime ('static). +pub trait ExtendedResponse { + /// Returns the value of the response as a Future of `Result<T, Error>`. + /// + /// If the status code of the response is not a success, returns an `Error`. + /// Otherwise, attempts to parse the response into an instance of type `T`. + /// + /// # Type Parameters + /// + /// * `T`: The type of the value to be returned. Must implement `serde::de::DeserializeOwned`, + /// `Send`, `Sync`, and have a static lifetime ('static). + fn get_value<T>(self) -> Fut<Result<T, Error>> + where + T: serde::de::DeserializeOwned + Send + Sync + 'static; + + fn get_bytes(self) -> Fut<Result<Bytes, Error>>; + + fn get_json(self) -> Fut<Result<Value, Error>>; + + fn success(self) -> Fut<Result<(), Error>>; + + fn success_with_body(self) -> Fut<Result<String, Error>>; +} + +impl ExtendedResponse for Response { + fn get_value<T>(self) -> Fut<Result<T, Error>> + where + T: serde::de::DeserializeOwned + Send + Sync + 'static, + { + to_fut(async move { + let status_code = self.status(); + if !status_code.is_success() { + return Err(parse_response_as_error(self).await.into()); + } + let bytes = self.bytes().await?; + let value = serde_json::from_slice(&bytes).map_err(|e| { + FlowyError::new( + ErrorCode::Serde, + format!( + "failed to parse json: {}, body: {}", + e, + String::from_utf8_lossy(&bytes) + ), + ) + })?; + Ok(value) + }) + } + + fn get_bytes(self) -> Fut<Result<Bytes, Error>> { + to_fut(async move { + let status_code = self.status(); + if !status_code.is_success() { + return Err(parse_response_as_error(self).await.into()); + } + let bytes = self.bytes().await?; + Ok(bytes) + }) + } + + fn get_json(self) -> Fut<Result<Value, Error>> { + to_fut(async move { + if !self.status().is_success() { + return Err(parse_response_as_error(self).await.into()); + } + let bytes = self.bytes().await?; + let value = serde_json::from_slice::<Value>(&bytes)?; + Ok(value) + }) + } + + fn success(self) -> Fut<Result<(), Error>> { + to_fut(async move { + if !self.status().is_success() { + return Err(parse_response_as_error(self).await.into()); + } + Ok(()) + }) + } + + fn success_with_body(self) -> Fut<Result<String, Error>> { + to_fut(async move { + if !self.status().is_success() { + return Err(parse_response_as_error(self).await.into()); + } + Ok(self.text().await?) + }) + } +} + +async fn parse_response_as_error(response: Response) -> FlowyError { + let status_code = response.status(); + let msg = response.text().await.unwrap_or_default(); + if status_code == StatusCode::CONFLICT { + return FlowyError::new(ErrorCode::Conflict, msg); + } + + FlowyError::new( + ErrorCode::HttpError, + format!( + "expected status code 2XX, but got {}, body: {}", + status_code, msg + ), + ) +} diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index 2702b4f104..bebfc81bcb 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -2,20 +2,21 @@ use client_api::ws::ConnectState; use client_api::ws::WSConnectStateReceiver; use client_api::ws::WebSocketChannel; use flowy_search_pub::cloud::SearchCloudService; +use flowy_storage::ObjectStorageService; use std::sync::Arc; use anyhow::Error; -use arc_swap::ArcSwapOption; use client_api::collab_sync::ServerCollabMessage; -use flowy_ai_pub::cloud::ChatCloudService; +use flowy_chat_pub::cloud::ChatCloudService; +use parking_lot::RwLock; use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] use {collab_entity::CollabObject, collab_plugins::cloud_storage::RemoteCollabStorage}; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; +use crate::default_impl::DefaultChatCloudServiceImpl; +use flowy_database_pub::cloud::DatabaseCloudService; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; -use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::UserCloudService; use flowy_user_pub::entities::UserTokenState; @@ -41,9 +42,7 @@ where /// and functionalities in AppFlowy. The methods provided ensure efficient, asynchronous operations /// for managing and accessing user data, folders, collaborative objects, and documents in a cloud environment. pub trait AppFlowyServer: Send + Sync + 'static { - fn set_token(&self, _token: &str) -> Result<(), Error>; - - fn set_ai_model(&self, _ai_model: &str) -> Result<(), Error> { + fn set_token(&self, _token: &str) -> Result<(), Error> { Ok(()) } @@ -90,8 +89,6 @@ pub trait AppFlowyServer: Send + Sync + 'static { /// An `Arc` wrapping the `DatabaseCloudService` interface. fn database_service(&self) -> Arc<dyn DatabaseCloudService>; - fn database_ai_service(&self) -> Option<Arc<dyn DatabaseAIService>>; - /// Facilitates cloud-based document management. This service offers operations for updating documents, /// fetching snapshots, and accessing primary document data in an asynchronous manner. /// @@ -100,7 +97,9 @@ pub trait AppFlowyServer: Send + Sync + 'static { /// An `Arc` wrapping the `DocumentCloudService` interface. fn document_service(&self) -> Arc<dyn DocumentCloudService>; - fn chat_service(&self) -> Arc<dyn ChatCloudService>; + fn chat_service(&self) -> Arc<dyn ChatCloudService> { + Arc::new(DefaultChatCloudServiceImpl) + } /// Bridge for the Cloud AI Search features /// @@ -145,27 +144,27 @@ pub trait AppFlowyServer: Send + Sync + 'static { Ok(None) } - fn file_storage(&self) -> Option<Arc<dyn StorageCloudService>>; + fn file_storage(&self) -> Option<Arc<dyn ObjectStorageService>>; } pub struct EncryptionImpl { - secret: ArcSwapOption<String>, + secret: RwLock<Option<String>>, } impl EncryptionImpl { pub fn new(secret: Option<String>) -> Self { Self { - secret: ArcSwapOption::from(secret.map(Arc::new)), + secret: RwLock::new(secret), } } } impl AppFlowyEncryption for EncryptionImpl { fn get_secret(&self) -> Option<String> { - self.secret.load().as_ref().map(|s| s.to_string()) + self.secret.read().clone() } fn set_secret(&self, secret: String) { - self.secret.store(Some(secret.into())); + *self.secret.write() = Some(secret); } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index bb5705cbc8..388ff184da 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use std::sync::{Arc, Weak}; use anyhow::Error; -use arc_swap::ArcSwapOption; use chrono::{DateTime, Utc}; use client_api::collab_sync::MsgId; use collab::core::collab::DataSource; @@ -11,6 +10,7 @@ use collab_entity::CollabObject; use collab_plugins::cloud_storage::{ RemoteCollabSnapshot, RemoteCollabState, RemoteCollabStorage, RemoteUpdateReceiver, }; +use parking_lot::Mutex; use tokio::task::spawn_blocking; use lib_infra::async_trait::async_trait; @@ -28,7 +28,7 @@ use crate::AppFlowyEncryption; pub struct SupabaseCollabStorageImpl<T> { server: T, - rx: ArcSwapOption<RemoteUpdateReceiver>, + rx: Mutex<Option<RemoteUpdateReceiver>>, encryption: Weak<dyn AppFlowyEncryption>, } @@ -40,7 +40,7 @@ impl<T> SupabaseCollabStorageImpl<T> { ) -> Self { Self { server, - rx: ArcSwapOption::new(rx.map(Arc::new)), + rx: Mutex::new(rx), encryption, } } @@ -186,14 +186,11 @@ where } fn subscribe_remote_updates(&self, _object: &CollabObject) -> Option<RemoteUpdateReceiver> { - let rx = self.rx.swap(None); - match rx { - Some(rx) => Arc::into_inner(rx), - None => { - tracing::warn!("The receiver is already taken"); - None - }, + let rx = self.rx.lock().take(); + if rx.is_none() { + tracing::warn!("The receiver is already taken"); } + rx } } @@ -281,8 +278,12 @@ fn merge_updates(update_items: Vec<UpdateItem>, new_update: Vec<u8>) -> Result<M if !new_update.is_empty() { updates.push(new_update); } + let updates = updates + .iter() + .map(|update| update.as_ref()) + .collect::<Vec<&[u8]>>(); - let new_update = merge_updates_v1(updates)?; + let new_update = merge_updates_v1(&updates)?; Ok(MergeResult { merged_keys, new_update, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index af1732b500..afba36d585 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -2,9 +2,11 @@ use anyhow::Error; use collab_entity::CollabType; use tokio::sync::oneshot::channel; -use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot}; - -use lib_infra::async_trait::async_trait; +use flowy_database_pub::cloud::{ + CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, + TranslateRowContent, TranslateRowResponse, +}; +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::supabase::api::request::{ @@ -22,7 +24,6 @@ impl<T> SupabaseDatabaseServiceImpl<T> { } } -#[async_trait] impl<T> DatabaseCloudService for SupabaseDatabaseServiceImpl<T> where T: SupabaseServerService, @@ -36,7 +37,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -59,7 +60,7 @@ where ) -> FutureResult<CollabDocStateByOid, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -96,4 +97,22 @@ where Ok(snapshots) }) } + + fn summary_database_row( + &self, + _workspace_id: &str, + _object_id: &str, + _summary_row: SummaryRowContent, + ) -> FutureResult<String, Error> { + FutureResult::new(async move { Ok("".to_string()) }) + } + + fn translate_database_row( + &self, + _workspace_id: &str, + _translate_row: TranslateRowContent, + _language: &str, + ) -> FutureResult<TranslateRowResponse, Error> { + FutureResult::new(async move { Ok(TranslateRowResponse::default()) }) + } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index b3f45a7670..a0e5087938 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -8,7 +8,7 @@ use tokio::sync::oneshot::channel; use flowy_document_pub::cloud::{DocumentCloudService, DocumentSnapshot}; use flowy_error::FlowyError; - +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use crate::supabase::api::request::{get_snapshots_from_server, FetchObjectUpdateAction}; @@ -37,7 +37,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -87,14 +87,14 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; let action = FetchObjectUpdateAction::new(document_id.clone(), CollabType::Document, postgrest); let doc_state = action.run_with_fix_interval(5, 10).await?; - let document = Document::open_with_options( + let document = Document::from_doc_state( CollabOrigin::Empty, DataSource::DocStateV1(doc_state), &document_id, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index 253d11e0d8..ca0957c375 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -13,8 +13,7 @@ use flowy_folder_pub::cloud::{ gen_workspace_id, Folder, FolderCloudService, FolderCollabParams, FolderData, FolderSnapshot, Workspace, WorkspaceRecord, }; -use flowy_folder_pub::entities::PublishPayload; - +use lib_dispatch::prelude::af_spawn; use lib_infra::future::FutureResult; use lib_infra::util::timestamp; @@ -96,8 +95,11 @@ where if items.is_empty() { return Ok(None); } - let updates = items.into_iter().map(|update| update.value); - let doc_state = merge_updates_v1(updates) + let updates = items + .iter() + .map(|update| update.value.as_ref()) + .collect::<Vec<&[u8]>>(); + let doc_state = merge_updates_v1(&updates) .map_err(|err| anyhow::anyhow!("merge updates failed: {:?}", err))?; let folder = Folder::from_collab_doc_state( @@ -144,7 +146,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -172,46 +174,6 @@ where fn service_name(&self) -> String { "Supabase".to_string() } - - fn publish_view( - &self, - _workspace_id: &str, - _payload: Vec<PublishPayload>, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish view")) }) - } - - fn unpublish_views( - &self, - _workspace_id: &str, - _view_ids: Vec<String>, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("supabase server doesn't support unpublish views")) }) - } - - fn get_publish_info(&self, _view_id: &str) -> FutureResult<PublishInfo, Error> { - FutureResult::new(async { Err(anyhow!("supabase server doesn't support publish info")) }) - } - - fn set_publish_namespace( - &self, - _workspace_id: &str, - _new_namespace: &str, - ) -> FutureResult<(), FlowyError> { - FutureResult::new(async { - Err(anyhow!( - "supabase server doesn't support set publish namespace" - )) - }) - } - - fn get_publish_namespace(&self, _workspace_id: &str) -> FutureResult<String, Error> { - FutureResult::new(async { - Err(anyhow!( - "supabase server doesn't support get publish namespace" - )) - }) - } } fn workspace_from_json_value(value: Value) -> Result<Workspace, Error> { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs b/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs index 9ab3379486..8db0910896 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/postgres_server.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use std::sync::{Arc, Weak}; use anyhow::Error; -use arc_swap::ArcSwapOption; +use parking_lot::RwLock; use postgrest::Postgrest; use flowy_error::{ErrorCode, FlowyError}; @@ -77,11 +77,11 @@ where } #[derive(Clone)] -pub struct SupabaseServerServiceImpl(pub Arc<ArcSwapOption<RESTfulPostgresServer>>); +pub struct SupabaseServerServiceImpl(pub Arc<RwLock<Option<Arc<RESTfulPostgresServer>>>>); impl SupabaseServerServiceImpl { pub fn new(postgrest: Arc<RESTfulPostgresServer>) -> Self { - Self(Arc::new(ArcSwapOption::from(Some(postgrest)))) + Self(Arc::new(RwLock::new(Some(postgrest)))) } } @@ -89,7 +89,7 @@ impl SupabaseServerService for SupabaseServerServiceImpl { fn get_postgrest(&self) -> Option<Arc<PostgresWrapper>> { self .0 - .load() + .read() .as_ref() .map(|server| server.postgrest.clone()) } @@ -97,7 +97,7 @@ impl SupabaseServerService for SupabaseServerServiceImpl { fn try_get_postgrest(&self) -> Result<Arc<PostgresWrapper>, Error> { self .0 - .load() + .read() .as_ref() .map(|server| server.postgrest.clone()) .ok_or_else(|| { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs index fa13c9711b..326529e06e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs @@ -77,8 +77,11 @@ impl Action for FetchObjectUpdateAction { return Ok(vec![]); } - let updates = items.into_iter().map(|update| update.value); - let doc_state = merge_updates_v1(updates) + let updates = items + .iter() + .map(|update| update.value.as_ref()) + .collect::<Vec<&[u8]>>(); + let doc_state = merge_updates_v1(&updates) .map_err(|err| anyhow::anyhow!("merge updates failed: {:?}", err))?; Ok(doc_state) }, @@ -283,9 +286,12 @@ pub async fn batch_get_updates_from_server( if items.is_empty() { updates_by_oid.insert(oid.to_string(), DataSource::Disk); } else { - let updates = items.into_iter().map(|update| update.value); + let updates = items + .iter() + .map(|update| update.value.as_ref()) + .collect::<Vec<&[u8]>>(); - let doc_state = merge_updates_v1(updates) + let doc_state = merge_updates_v1(&updates) .map_err(|err| anyhow::anyhow!("merge updates failed: {:?}", err))?; updates_by_oid.insert(oid.to_string(), DataSource::DocStateV1(doc_state)); } diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 3712307af4..a691ab36a9 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -6,10 +6,11 @@ use std::sync::{Arc, Weak}; use std::time::Duration; use anyhow::Error; -use arc_swap::ArcSwapOption; +use collab::core::collab::MutexCollab; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_entity::{CollabObject, CollabType}; +use parking_lot::RwLock; use serde_json::Value; use tokio::sync::oneshot::channel; use tokio_retry::strategy::FixedInterval; @@ -21,7 +22,7 @@ use flowy_folder_pub::cloud::{Folder, FolderData, Workspace}; use flowy_user_pub::cloud::*; use flowy_user_pub::entities::*; use flowy_user_pub::DEFAULT_USER_NAME; - +use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; use lib_infra::future::FutureResult; @@ -43,7 +44,7 @@ use crate::AppFlowyEncryption; pub struct SupabaseUserServiceImpl<T> { server: T, realtime_event_handlers: Vec<Box<dyn RealtimeEventHandler>>, - user_update_rx: ArcSwapOption<UserUpdateReceiver>, + user_update_rx: RwLock<Option<UserUpdateReceiver>>, } impl<T> SupabaseUserServiceImpl<T> { @@ -55,7 +56,7 @@ impl<T> SupabaseUserServiceImpl<T> { Self { server, realtime_event_handlers, - user_update_rx: ArcSwapOption::from(user_update_rx.map(Arc::new)), + user_update_rx: RwLock::new(user_update_rx), } } } @@ -199,16 +200,6 @@ where }) } - fn sign_in_with_passcode( - &self, - _email: &str, - _passcode: &str, - ) -> FutureResult<GotrueTokenResponse, FlowyError> { - FutureResult::new(async { - Err(FlowyError::not_support().with_context("Can't sign in with passcode when using supabase")) - }) - } - fn generate_oauth_url_with_provider(&self, _provider: &str) -> FutureResult<String, FlowyError> { FutureResult::new(async { Err(FlowyError::internal().with_context("Can't generate oauth url when using supabase")) @@ -251,7 +242,6 @@ where authenticator: Authenticator::Supabase, encryption_type: EncryptionType::from_sign(&response.encryption_sign), updated_at: response.updated_at.timestamp(), - ai_model: "".to_string(), }), } }) @@ -281,7 +271,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); let object_id = object_id.to_string(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; @@ -315,15 +305,14 @@ where } fn subscribe_user_update(&self) -> Option<UserUpdateReceiver> { - let rx = self.user_update_rx.swap(None)?; - Arc::into_inner(rx) + self.user_update_rx.write().take() } fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); let init_update = default_workspace_doc_state(&collab_object); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest? @@ -361,7 +350,7 @@ where let try_get_postgrest = self.server.try_get_weak_postgrest(); let cloned_collab_object = collab_object.clone(); let (tx, rx) = channel(); - tokio::spawn(async move { + af_spawn(async move { tx.send( async move { CreateCollabAction::new(cloned_collab_object, try_get_postgrest?, data) @@ -657,7 +646,7 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { serde_json::from_value::<RealtimeCollabUpdateEvent>(event.new.clone()) { if let Some(sender_by_oid) = self.sender_by_oid.upgrade() { - if let Some(sender) = sender_by_oid.get(collab_update.oid.as_str()) { + if let Some(sender) = sender_by_oid.read().get(collab_update.oid.as_str()) { tracing::trace!( "current device: {}, event device: {}", self.device_id, @@ -698,16 +687,15 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler { fn default_workspace_doc_state(collab_object: &CollabObject) -> Vec<u8> { let workspace_id = collab_object.object_id.clone(); - let collab = - Collab::new_with_origin(CollabOrigin::Empty, &collab_object.object_id, vec![], false); + let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( + CollabOrigin::Empty, + &collab_object.object_id, + vec![], + false, + ))); let workspace = Workspace::new(workspace_id, "My workspace".to_string(), collab_object.uid); - let folder = Folder::open_with( - collab_object.uid, - collab, - None, - Some(FolderData::new(workspace)), - ); - folder.encode_collab().unwrap().doc_state.to_vec() + let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); + folder.encode_collab_v1().unwrap().doc_state.to_vec() } fn oauth_params_from_box_any(any: BoxAny) -> Result<SupabaseOAuthParams, Error> { diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/builder.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/builder.rs index 6db01f1cc6..89dfc39971 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/builder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/builder.rs @@ -1,5 +1,7 @@ +use std::borrow::Cow; + use anyhow::Error; -use flowy_storage_pub::cloud::StorageObject; +use flowy_storage::StorageObject; use hyper::header::CONTENT_TYPE; use reqwest::header::IntoHeaderName; use reqwest::multipart::{Form, Part}; @@ -7,14 +9,12 @@ use reqwest::{ header::{HeaderMap, HeaderValue}, Client, Method, RequestBuilder, }; -use std::borrow::Cow; use tokio::fs::File; use tokio::io::AsyncReadExt; use url::Url; use crate::supabase::file_storage::{DeleteObjects, FileOptions, NewBucket, RequestBody}; -#[allow(dead_code)] pub struct StorageRequestBuilder { pub url: Url, headers: HeaderMap, @@ -23,7 +23,6 @@ pub struct StorageRequestBuilder { body: RequestBody, } -#[allow(dead_code)] impl StorageRequestBuilder { pub fn new(url: Url, headers: HeaderMap, client: Client) -> Self { Self { diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/core.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/core.rs index f150084c2d..b00bf8f9a6 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/core.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/core.rs @@ -1,13 +1,141 @@ -#![allow(clippy::all)] -#![allow(unknown_lints)] -#![allow(unused_attributes)] -use std::sync::Weak; +use std::sync::{Arc, Weak}; use anyhow::{anyhow, Error}; +use reqwest::{ + header::{HeaderMap, HeaderValue}, + Client, +}; use url::Url; -use crate::AppFlowyEncryption; use flowy_encrypt::{decrypt_data, encrypt_data}; +use flowy_error::FlowyError; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_storage::{FileStoragePlan, ObjectStorageService}; +use lib_infra::future::FutureResult; + +use crate::supabase::file_storage::builder::StorageRequestBuilder; +use crate::AppFlowyEncryption; + +pub struct SupabaseFileStorage { + url: Url, + headers: HeaderMap, + client: Client, + #[allow(dead_code)] + encryption: ObjectEncryption, + #[allow(dead_code)] + storage_plan: Arc<dyn FileStoragePlan>, +} + +impl ObjectStorageService for SupabaseFileStorage { + fn get_object_url( + &self, + _object_id: flowy_storage::ObjectIdentity, + ) -> FutureResult<String, FlowyError> { + todo!() + } + + fn put_object( + &self, + _url: String, + _object_value: flowy_storage::ObjectValue, + ) -> FutureResult<(), FlowyError> { + todo!() + } + + fn delete_object(&self, _url: String) -> FutureResult<(), FlowyError> { + todo!() + } + + fn get_object(&self, _url: String) -> FutureResult<flowy_storage::ObjectValue, FlowyError> { + todo!() + } + + // fn create_object(&self, object: StorageObject) -> FutureResult<String, FlowyError> { + // let mut storage = self.storage(); + // let storage_plan = Arc::downgrade(&self.storage_plan); + + // FutureResult::new(async move { + // let plan = storage_plan + // .upgrade() + // .ok_or(anyhow!("Storage plan is not available"))?; + // plan.check_upload_object(&object).await?; + + // storage = storage.upload_object("data", object); + // let url = storage.url.to_string(); + // storage.build().await?.send().await?.success().await?; + // Ok(url) + // }) + // } + + // fn delete_object_by_url(&self, object_url: String) -> FutureResult<(), FlowyError> { + // let storage = self.storage(); + + // FutureResult::new(async move { + // let url = Url::parse(&object_url)?; + // let location = get_object_location_from(&url)?; + // storage + // .delete_object(location.bucket_id, location.file_name) + // .build() + // .await? + // .send() + // .await? + // .success() + // .await?; + // Ok(()) + // }) + // } + + // fn get_object_by_url(&self, object_url: String) -> FutureResult<Bytes, FlowyError> { + // let storage = self.storage(); + // FutureResult::new(async move { + // let url = Url::parse(&object_url)?; + // let location = get_object_location_from(&url)?; + // let bytes = storage + // .get_object(location.bucket_id, location.file_name) + // .build() + // .await? + // .send() + // .await? + // .get_bytes() + // .await?; + // Ok(bytes) + // }) + // } +} + +impl SupabaseFileStorage { + pub fn new( + config: &SupabaseConfiguration, + encryption: Weak<dyn AppFlowyEncryption>, + storage_plan: Arc<dyn FileStoragePlan>, + ) -> Result<Self, Error> { + let mut headers = HeaderMap::new(); + let url = format!("{}/storage/v1", config.url); + let auth = format!("Bearer {}", config.anon_key); + + headers.insert( + "Authorization", + HeaderValue::from_str(&auth).expect("Authorization is invalid"), + ); + headers.insert( + "apikey", + HeaderValue::from_str(&config.anon_key).expect("apikey value is invalid"), + ); + + let encryption = ObjectEncryption::new(encryption); + Ok(Self { + url: Url::parse(&url)?, + headers, + client: Client::new(), + encryption, + storage_plan, + }) + } + + pub fn storage(&self) -> StorageRequestBuilder { + StorageRequestBuilder::new(self.url.clone(), self.headers.clone(), self.client.clone()) + } +} #[allow(dead_code)] struct ObjectEncryption { @@ -15,7 +143,6 @@ struct ObjectEncryption { } impl ObjectEncryption { - #[allow(dead_code)] fn new(encryption: Weak<dyn AppFlowyEncryption>) -> Self { Self { encryption } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/entities.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/entities.rs index ec1ffa6c7b..768ae27b3e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/entities.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/entities.rs @@ -1,7 +1,8 @@ use bytes::Bytes; -use flowy_storage_pub::cloud::ObjectValueSupabase; use serde::{Deserialize, Serialize}; +use flowy_storage::ObjectValueSupabase; + use crate::supabase; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs index 5da091c22c..ebfc707dcb 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/mod.rs @@ -1,5 +1,7 @@ pub use entities::*; +pub use plan::*; mod builder; pub mod core; mod entities; +pub mod plan; diff --git a/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs b/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs index 39a33c8853..63cf2cb6e0 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/file_storage/plan.rs @@ -1,7 +1,9 @@ use std::sync::Weak; +use parking_lot::RwLock; + use flowy_error::FlowyError; -use flowy_storage_pub::cloud::{FileStoragePlan, StorageObject}; +use flowy_storage::{FileStoragePlan, StorageObject}; use lib_infra::future::FutureResult; use crate::supabase::api::RESTfulPostgresServer; @@ -9,13 +11,16 @@ use crate::supabase::api::RESTfulPostgresServer; #[derive(Default)] pub struct FileStoragePlanImpl { #[allow(dead_code)] - uid: Weak<Option<i64>>, + uid: Weak<RwLock<Option<i64>>>, #[allow(dead_code)] postgrest: Option<Weak<RESTfulPostgresServer>>, } impl FileStoragePlanImpl { - pub fn new(uid: Weak<Option<i64>>, postgrest: Option<Weak<RESTfulPostgresServer>>) -> Self { + pub fn new( + uid: Weak<RwLock<Option<i64>>>, + postgrest: Option<Weak<RESTfulPostgresServer>>, + ) -> Self { Self { uid, postgrest } } } diff --git a/frontend/rust-lib/flowy-server/src/supabase/server.rs b/frontend/rust-lib/flowy-server/src/supabase/server.rs index 00dd46e8ba..fa59931fa2 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/server.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/server.rs @@ -1,16 +1,16 @@ -use arc_swap::ArcSwapOption; use flowy_search_pub::cloud::SearchCloudService; +use flowy_storage::ObjectStorageService; +use std::collections::HashMap; use std::sync::{Arc, Weak}; use collab_entity::CollabObject; use collab_plugins::cloud_storage::{RemoteCollabStorage, RemoteUpdateSender}; -use dashmap::DashMap; +use parking_lot::RwLock; -use flowy_database_pub::cloud::{DatabaseAIService, DatabaseCloudService}; +use flowy_database_pub::cloud::DatabaseCloudService; use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; use flowy_server_pub::supabase_config::SupabaseConfiguration; -use flowy_storage_pub::cloud::StorageCloudService; use flowy_user_pub::cloud::UserCloudService; use crate::supabase::api::{ @@ -18,7 +18,8 @@ use crate::supabase::api::{ SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, SupabaseDocumentServiceImpl, SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl, }; - +use crate::supabase::file_storage::core::SupabaseFileStorage; +use crate::supabase::file_storage::FileStoragePlanImpl; use crate::{AppFlowyEncryption, AppFlowyServer}; /// https://www.pgbouncer.org/features.html @@ -55,23 +56,23 @@ impl PgPoolMode { } } -pub type CollabUpdateSenderByOid = DashMap<String, RemoteUpdateSender>; +pub type CollabUpdateSenderByOid = RwLock<HashMap<String, RemoteUpdateSender>>; /// Supabase server is used to provide the implementation of the [AppFlowyServer] trait. /// It contains the configuration of the supabase server and the postgres server. pub struct SupabaseServer { #[allow(dead_code)] config: SupabaseConfiguration, device_id: String, - #[allow(dead_code)] - uid: Arc<ArcSwapOption<i64>>, + uid: Arc<RwLock<Option<i64>>>, collab_update_sender: Arc<CollabUpdateSenderByOid>, - restful_postgres: Arc<ArcSwapOption<RESTfulPostgresServer>>, + restful_postgres: Arc<RwLock<Option<Arc<RESTfulPostgresServer>>>>, + file_storage: Arc<RwLock<Option<Arc<SupabaseFileStorage>>>>, encryption: Weak<dyn AppFlowyEncryption>, } impl SupabaseServer { pub fn new( - uid: Arc<ArcSwapOption<i64>>, + uid: Arc<RwLock<Option<i64>>>, config: SupabaseConfiguration, enable_sync: bool, device_id: String, @@ -86,11 +87,23 @@ impl SupabaseServer { } else { None }; + let file_storage = if enable_sync { + let plan = FileStoragePlanImpl::new( + Arc::downgrade(&uid), + restful_postgres.as_ref().map(Arc::downgrade), + ); + Some(Arc::new( + SupabaseFileStorage::new(&config, encryption.clone(), Arc::new(plan)).unwrap(), + )) + } else { + None + }; Self { config, device_id, collab_update_sender, - restful_postgres: Arc::new(ArcSwapOption::from(restful_postgres)), + restful_postgres: Arc::new(RwLock::new(restful_postgres)), + file_storage: Arc::new(RwLock::new(file_storage)), encryption, uid, } @@ -102,18 +115,23 @@ impl AppFlowyServer for SupabaseServer { tracing::info!("{} supabase sync: {}", uid, enable); if enable { - self.restful_postgres.rcu(|old| match old { - Some(existing) => Some(existing.clone()), - None => { - let postgres = Arc::new(RESTfulPostgresServer::new( - self.config.clone(), - self.encryption.clone(), - )); - Some(postgres) - }, - }); + if self.restful_postgres.read().is_none() { + let postgres = RESTfulPostgresServer::new(self.config.clone(), self.encryption.clone()); + *self.restful_postgres.write() = Some(Arc::new(postgres)); + } + + if self.file_storage.read().is_none() { + let plan = FileStoragePlanImpl::new( + Arc::downgrade(&self.uid), + self.restful_postgres.read().as_ref().map(Arc::downgrade), + ); + let file_storage = + SupabaseFileStorage::new(&self.config, self.encryption.clone(), Arc::new(plan)).unwrap(); + *self.file_storage.write() = Some(Arc::new(file_storage)); + } } else { - self.restful_postgres.store(None); + *self.restful_postgres.write() = None; + *self.file_storage.write() = None; } } @@ -150,10 +168,6 @@ impl AppFlowyServer for SupabaseServer { ))) } - fn database_ai_service(&self) -> Option<Arc<dyn DatabaseAIService>> { - None - } - fn document_service(&self) -> Arc<dyn DocumentCloudService> { Arc::new(SupabaseDocumentServiceImpl::new(SupabaseServerServiceImpl( self.restful_postgres.clone(), @@ -164,6 +178,7 @@ impl AppFlowyServer for SupabaseServer { let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); self .collab_update_sender + .write() .insert(collab_object.object_id.clone(), tx); Some(Arc::new(SupabaseCollabStorageImpl::new( @@ -173,8 +188,12 @@ impl AppFlowyServer for SupabaseServer { ))) } - fn file_storage(&self) -> Option<Arc<dyn StorageCloudService>> { - None + fn file_storage(&self) -> Option<Arc<dyn ObjectStorageService>> { + self + .file_storage + .read() + .clone() + .map(|s| s as Arc<dyn ObjectStorageService>) } fn search_service(&self) -> Option<Arc<dyn SearchCloudService>> { diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs new file mode 100644 index 0000000000..94ad2e2e1d --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/mod.rs @@ -0,0 +1,2 @@ +mod user_test; +mod util; diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs new file mode 100644 index 0000000000..a14d8eaf25 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/user_test.rs @@ -0,0 +1,21 @@ +use flowy_server::AppFlowyServer; +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::af_cloud_test::util::{ + af_cloud_server, af_cloud_sign_up_param, generate_test_email, get_af_cloud_config, +}; + +#[tokio::test] +async fn sign_up_test() { + if let Some(config) = get_af_cloud_config() { + let server = af_cloud_server(config.clone()); + let user_service = server.user_service(); + let email = generate_test_email(); + let params = af_cloud_sign_up_param(&email, &config).await; + let resp: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert_eq!(resp.email.unwrap(), email); + assert!(resp.is_new_user); + assert_eq!(resp.user_workspaces.len(), 1); + } +} diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs new file mode 100644 index 0000000000..71dacfab04 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -0,0 +1,94 @@ +use client_api::ClientConfiguration; +use semver::Version; +use std::collections::HashMap; +use std::sync::Arc; + +use flowy_error::FlowyResult; +use uuid::Uuid; + +use flowy_server::af_cloud::define::ServerUser; +use flowy_server::af_cloud::AppFlowyCloudServer; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL}; +use flowy_server_pub::af_cloud_config::AFCloudConfiguration; + +use crate::setup_log; + +/// To run the test, create a .env.ci file in the 'flowy-server' directory and set the following environment variables: +/// +/// - `APPFLOWY_CLOUD_BASE_URL=http://localhost:8000` +/// - `APPFLOWY_CLOUD_WS_BASE_URL=ws://localhost:8000/ws` +/// - `APPFLOWY_CLOUD_GOTRUE_URL=http://localhost:9998` +/// +/// - `GOTRUE_ADMIN_EMAIL=admin@example.com` +/// - `GOTRUE_ADMIN_PASSWORD=password` +pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> { + dotenv::from_filename("./.env.ci").ok()?; + setup_log(); + AFCloudConfiguration::from_env().ok() +} + +pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AppFlowyCloudServer> { + let fake_device_id = uuid::Uuid::new_v4().to_string(); + Arc::new(AppFlowyCloudServer::new( + config, + true, + fake_device_id, + Version::new(0, 5, 8), + Arc::new(FakeServerUserImpl), + )) +} + +struct FakeServerUserImpl; +impl ServerUser for FakeServerUserImpl { + fn workspace_id(&self) -> FlowyResult<String> { + todo!() + } +} + +pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { + let client = client_api::Client::new( + &config.base_url, + &config.ws_base_url, + &config.gotrue_url, + "fake_device_id", + ClientConfiguration::default(), + "test", + ); + let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); + let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); + let admin_client = client_api::Client::new( + client.base_url(), + client.ws_addr(), + client.gotrue_url(), + "fake_device_id", + ClientConfiguration::default(), + &client.client_version.to_string(), + ); + admin_client + .sign_in_password(&admin_email, &admin_password) + .await + .unwrap(); + + let action_link = admin_client + .generate_sign_in_action_link(user_email) + .await + .unwrap(); + client.extract_sign_in_url(&action_link).await.unwrap() +} + +pub async fn af_cloud_sign_up_param( + email: &str, + config: &AFCloudConfiguration, +) -> HashMap<String, String> { + let mut params = HashMap::new(); + params.insert( + USER_SIGN_IN_URL.to_string(), + generate_sign_in_url(email, config).await, + ); + params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); + params +} + +pub fn generate_test_email() -> String { + format!("{}@test.com", Uuid::new_v4()) +} diff --git a/frontend/rust-lib/flowy-server/tests/logo.png b/frontend/rust-lib/flowy-server/tests/logo.png new file mode 100644 index 0000000000..d6f09e3e2e Binary files /dev/null and b/frontend/rust-lib/flowy-server/tests/logo.png differ diff --git a/frontend/rust-lib/flowy-server/tests/main.rs b/frontend/rust-lib/flowy-server/tests/main.rs new file mode 100644 index 0000000000..cd827b9b9d --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/main.rs @@ -0,0 +1,24 @@ +use std::sync::Once; + +use tracing_subscriber::fmt::Subscriber; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::EnvFilter; + +mod af_cloud_test; +mod supabase_test; + +pub fn setup_log() { + static START: Once = Once::new(); + START.call_once(|| { + let level = "trace"; + let mut filters = vec![]; + filters.push(format!("flowy_server={}", level)); + std::env::set_var("RUST_LOG", filters.join(",")); + + let subscriber = Subscriber::builder() + .with_env_filter(EnvFilter::from_default_env()) + .with_ansi(true) + .finish(); + subscriber.try_init().unwrap(); + }); +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs new file mode 100644 index 0000000000..841c76b443 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs @@ -0,0 +1,63 @@ +use collab::core::collab::DataSource; +use collab_entity::{CollabObject, CollabType}; +use uuid::Uuid; + +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + collab_service, database_service, get_supabase_ci_config, third_party_sign_up_param, + user_auth_service, +}; + +#[tokio::test] +async fn supabase_create_database_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_service = collab_service(); + let database_service = database_service(); + + let mut row_ids = vec![]; + for _i in 0..3 { + let row_id = uuid::Uuid::new_v4().to_string(); + row_ids.push(row_id.clone()); + let collab_object = CollabObject::new( + user.user_id, + row_id, + CollabType::DatabaseRow, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + collab_service + .send_update(&collab_object, 0, vec![1, 2, 3]) + .await + .unwrap(); + collab_service + .send_update(&collab_object, 0, vec![4, 5, 6]) + .await + .unwrap(); + } + + let updates_by_oid = database_service + .batch_get_database_object_doc_state(row_ids, CollabType::DatabaseRow, "fake_workspace_id") + .await + .unwrap(); + + assert_eq!(updates_by_oid.len(), 3); + for (_, source) in updates_by_oid { + match source { + DataSource::Disk => panic!("should not be from disk"), + DataSource::DocStateV1(doc_state) => { + assert_eq!(doc_state.len(), 2); + }, + DataSource::DocStateV2(_) => {}, + } + } +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs new file mode 100644 index 0000000000..4377ce8e68 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/file_test.rs @@ -0,0 +1,78 @@ +// use url::Url; +// use uuid::Uuid; +// +// use flowy_storage::StorageObject; +// +// use crate::supabase_test::util::{file_storage_service, get_supabase_ci_config}; +// +// #[tokio::test] +// async fn supabase_get_object_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("test-{}.txt", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); +// +// // Upload a file +// let url = service +// .create_object(object) +// .await +// .unwrap() +// .parse::<Url>() +// .unwrap(); +// +// // The url would be something like: +// // https://acfrqdbdtbsceyjbxsfc.supabase.co/storage/v1/object/data/test-1693472809.txt +// let name = url.path_segments().unwrap().last().unwrap(); +// assert_eq!(name, &file_name); +// +// // Download the file +// let bytes = service.get_object(url.to_string()).await.unwrap(); +// let s = String::from_utf8(bytes.to_vec()).unwrap(); +// assert_eq!(s, "hello world"); +// } +// +// #[tokio::test] +// async fn supabase_upload_image_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("image-{}.png", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/logo.png"); +// +// // Upload a file +// let url = service +// .create_object(object) +// .await +// .unwrap() +// .parse::<Url>() +// .unwrap(); +// +// // Download object by url +// let bytes = service.get_object(url.to_string()).await.unwrap(); +// assert_eq!(bytes.len(), 15694); +// } +// +// #[tokio::test] +// async fn supabase_delete_object_test() { +// if get_supabase_ci_config().is_none() { +// return; +// } +// +// let service = file_storage_service(); +// let file_name = format!("test-{}.txt", Uuid::new_v4()); +// let object = StorageObject::from_file("1", &file_name, "tests/test.txt"); +// let url = service.create_object(object).await.unwrap(); +// +// let result = service.get_object(url.clone()).await; +// assert!(result.is_ok()); +// +// let _ = service.delete_object(url.clone()).await; +// +// let result = service.get_object(url.clone()).await; +// assert!(result.is_err()); +// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs new file mode 100644 index 0000000000..5d7922a1e4 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs @@ -0,0 +1,316 @@ +use assert_json_diff::assert_json_eq; +use collab_entity::{CollabObject, CollabType}; +use serde_json::json; +use uuid::Uuid; +use yrs::types::ToJson; +use yrs::updates::decoder::Decode; +use yrs::{merge_updates_v1, Array, Doc, Map, MapPrelim, ReadTxn, StateVector, Transact, Update}; + +use flowy_user_pub::entities::AuthResponse; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + collab_service, folder_service, get_supabase_ci_config, third_party_sign_up_param, + user_auth_service, +}; + +#[tokio::test] +async fn supabase_create_workspace_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let service = folder_service(); + // will replace the uid with the real uid + let workspace = service.create_workspace(1, "test").await.unwrap(); + dbg!(workspace); +} + +#[tokio::test] +async fn supabase_get_folder_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + collab_service + .send_update(&collab_object, 0, txn.encode_update_v1()) + .await + .unwrap(); + }; + + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + collab_service + .send_update(&collab_object, 1, txn.encode_update_v1()) + .await + .unwrap(); + }; + + // let updates = collab_service.get_all_updates(&collab_object).await.unwrap(); + let updates = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + assert_eq!(updates.len(), 2); + + for _ in 0..5 { + collab_service + .send_init_sync(&collab_object, 3, vec![]) + .await + .unwrap(); + } + let updates = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + // Other the init sync, try to get the updates from the server. + let expected_update = doc + .transact_mut() + .encode_state_as_update_v1(&StateVector::default()); + + // check the update is the same as local document update. + assert_eq!(updates, expected_update); +} + +/// This async test function checks the behavior of updates duplication in Supabase. +/// It creates a new user and simulates two updates to the user's workspace with different values. +/// Then, it merges these updates and sends an initial synchronization request to test duplication handling. +/// Finally, it asserts that the duplicated updates don't affect the overall data consistency in Supabase. +#[tokio::test] +async fn supabase_duplicate_updates_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + let mut duplicated_updates = vec![]; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + let update = txn.encode_update_v1(); + duplicated_updates.push(update.clone()); + collab_service + .send_update(&collab_object, 0, update) + .await + .unwrap(); + }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + let update = txn.encode_update_v1(); + duplicated_updates.push(update.clone()); + collab_service + .send_update(&collab_object, 1, update) + .await + .unwrap(); + }; + // send init sync + collab_service + .send_init_sync(&collab_object, 3, vec![]) + .await + .unwrap(); + let first_init_sync_update = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + // simulate the duplicated updates. + let merged_update = merge_updates_v1( + &duplicated_updates + .iter() + .map(|update| update.as_ref()) + .collect::<Vec<&[u8]>>(), + ) + .unwrap(); + collab_service + .send_init_sync(&collab_object, 4, merged_update) + .await + .unwrap(); + let second_init_sync_update = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + + let doc_2 = Doc::new(); + assert_eq!(first_init_sync_update.len(), second_init_sync_update.len()); + let map = { doc_2.get_or_insert_map("map") }; + { + let mut txn = doc_2.transact_mut(); + let update = Update::decode_v1(&second_init_sync_update).unwrap(); + txn.apply_update(update); + } + { + let txn = doc_2.transact(); + let json = map.to_json(&txn); + assert_json_eq!( + json, + json!({ + "1": "a", + "2": "b" + }) + ); + } +} + +/// The state vector of doc; +/// ```json +/// "map": {}, +/// "array": [] +/// ``` +/// The old version of doc: +/// ```json +/// "map": {} +/// ``` +/// +/// Try to apply the updates from doc to old version doc and check the result. +#[tokio::test] +async fn supabase_diff_state_vector_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let folder_service = folder_service(); + let user_service = user_auth_service(); + let collab_service = collab_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + let collab_object = CollabObject::new( + user.user_id, + user.latest_workspace.id.clone(), + CollabType::Folder, + user.latest_workspace.id.clone(), + "fake_device_id".to_string(), + ); + let doc = Doc::with_client_id(1); + let map = { doc.get_or_insert_map("map") }; + let array = { doc.get_or_insert_array("array") }; + + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "1", "a"); + map.insert(&mut txn, "inner_map", MapPrelim::<String>::new()); + + array.push_back(&mut txn, "element 1"); + let update = txn.encode_update_v1(); + collab_service + .send_update(&collab_object, 0, update) + .await + .unwrap(); + }; + { + let mut txn = doc.transact_mut(); + map.insert(&mut txn, "2", "b"); + array.push_back(&mut txn, "element 2"); + let update = txn.encode_update_v1(); + collab_service + .send_update(&collab_object, 1, update) + .await + .unwrap(); + }; + + // restore the doc with given updates. + let old_version_doc = Doc::new(); + let map = { old_version_doc.get_or_insert_map("map") }; + let doc_state = folder_service + .get_folder_doc_state( + &user.latest_workspace.id, + user.user_id, + CollabType::Folder, + &user.latest_workspace.id, + ) + .await + .unwrap(); + { + let mut txn = old_version_doc.transact_mut(); + let update = Update::decode_v1(&doc_state).unwrap(); + txn.apply_update(update); + } + let txn = old_version_doc.transact(); + let json = map.to_json(&txn); + assert_json_eq!( + json, + json!({ + "1": "a", + "2": "b", + "inner_map": {} + }) + ); +} + +// #[tokio::test] +// async fn print_folder_object_test() { +// if get_supabase_dev_config().is_none() { +// return; +// } +// let secret = Some("43bSxEPHeNkk5ZxxEYOfAjjd7sK2DJ$vVnxwuNc5ru0iKFvhs8wLg==".to_string()); +// print_encryption_folder("f8b14b84-e8ec-4cf4-a318-c1e008ecfdfa", secret).await; +// } +// +// #[tokio::test] +// async fn print_folder_snapshot_object_test() { +// if get_supabase_dev_config().is_none() { +// return; +// } +// let secret = Some("NTXRXrDSybqFEm32jwMBDzbxvCtgjU$8np3TGywbBdJAzHtu1QIyQ==".to_string()); +// // let secret = None; +// print_encryption_folder_snapshot("12533251-bdd4-41f4-995f-ff12fceeaa42", secret).await; +// } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs new file mode 100644 index 0000000000..ab82d37866 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/mod.rs @@ -0,0 +1,5 @@ +mod database_test; +mod file_test; +mod folder_test; +mod user_test; +mod util; diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs new file mode 100644 index 0000000000..13df930601 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/user_test.rs @@ -0,0 +1,141 @@ +use uuid::Uuid; + +use flowy_encrypt::{encrypt_text, generate_encryption_secret}; +use flowy_error::FlowyError; +use flowy_user_pub::entities::*; +use lib_infra::box_any::BoxAny; + +use crate::supabase_test::util::{ + get_supabase_ci_config, third_party_sign_up_param, user_auth_service, +}; + +// ‼️‼️‼️ Warning: this test will create a table in the database +#[tokio::test] +async fn supabase_user_sign_up_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert!(!user.latest_workspace.id.is_empty()); + assert!(!user.user_workspaces.is_empty()); + assert!(!user.latest_workspace.database_indexer_id.is_empty()); +} + +#[tokio::test] +async fn supabase_user_sign_up_with_existing_uuid_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let _user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + assert!(!user.latest_workspace.id.is_empty()); + assert!(!user.latest_workspace.database_indexer_id.is_empty()); + assert!(!user.user_workspaces.is_empty()); +} + +#[tokio::test] +async fn supabase_update_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + let params = UpdateUserProfileParams::new(user.user_id) + .with_name("123") + .with_email(format!("{}@test.com", Uuid::new_v4())); + + user_service + .update_user(UserCredentials::from_uid(user.user_id), params) + .await + .unwrap(); + + let user_profile = user_service + .get_user_profile(UserCredentials::from_uid(user.user_id)) + .await + .unwrap(); + + assert_eq!(user_profile.name, "123"); +} + +#[tokio::test] +async fn supabase_get_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service + .sign_up(BoxAny::new(params.clone())) + .await + .unwrap(); + + let credential = UserCredentials::from_uid(user.user_id); + user_service + .get_user_profile(credential.clone()) + .await + .unwrap(); +} + +#[tokio::test] +async fn supabase_get_not_exist_user_profile_test() { + if get_supabase_ci_config().is_none() { + return; + } + + let user_service = user_auth_service(); + let result: FlowyError = user_service + .get_user_profile(UserCredentials::from_uid(i64::MAX)) + .await + .unwrap_err(); + // user not found + assert!(result.is_record_not_found()); +} + +#[tokio::test] +async fn user_encryption_sign_test() { + if get_supabase_ci_config().is_none() { + return; + } + let user_service = user_auth_service(); + let uuid = Uuid::new_v4().to_string(); + let params = third_party_sign_up_param(uuid); + let user: AuthResponse = user_service.sign_up(BoxAny::new(params)).await.unwrap(); + + // generate encryption sign + let secret = generate_encryption_secret(); + let sign = encrypt_text(user.user_id.to_string(), &secret).unwrap(); + + user_service + .update_user( + UserCredentials::from_uid(user.user_id), + UpdateUserProfileParams::new(user.user_id) + .with_encryption_type(EncryptionType::SelfEncryption(sign.clone())), + ) + .await + .unwrap(); + + let user_profile: UserProfile = user_service + .get_user_profile(UserCredentials::from_uid(user.user_id)) + .await + .unwrap(); + assert_eq!( + user_profile.encryption_type, + EncryptionType::SelfEncryption(sign) + ); +} diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs new file mode 100644 index 0000000000..a0f3d1fbdc --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs @@ -0,0 +1,181 @@ +use flowy_storage::ObjectStorageService; +use std::collections::HashMap; +use std::sync::Arc; + +use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::origin::CollabOrigin; +use collab::preclude::Collab; +use collab_plugins::cloud_storage::RemoteCollabStorage; +use uuid::Uuid; + +use flowy_database_pub::cloud::DatabaseCloudService; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::{Folder, FolderCloudService}; +use flowy_server::supabase::api::{ + RESTfulPostgresServer, SupabaseCollabStorageImpl, SupabaseDatabaseServiceImpl, + SupabaseFolderServiceImpl, SupabaseServerServiceImpl, SupabaseUserServiceImpl, +}; +use flowy_server::supabase::define::{USER_DEVICE_ID, USER_EMAIL, USER_UUID}; +use flowy_server::supabase::file_storage::core::SupabaseFileStorage; +use flowy_server::{AppFlowyEncryption, EncryptionImpl}; +use flowy_server_pub::supabase_config::SupabaseConfiguration; +use flowy_storage::{FileStoragePlan, StorageObject}; +use flowy_user_pub::cloud::UserCloudService; +use lib_infra::future::FutureResult; + +use crate::setup_log; + +pub fn get_supabase_ci_config() -> Option<SupabaseConfiguration> { + dotenv::from_filename("./.env.ci").ok()?; + setup_log(); + SupabaseConfiguration::from_env().ok() +} + +#[allow(dead_code)] +pub fn get_supabase_dev_config() -> Option<SupabaseConfiguration> { + dotenv::from_filename("./.env.dev").ok()?; + setup_log(); + SupabaseConfiguration::from_env().ok() +} + +pub fn collab_service() -> Arc<dyn RemoteCollabStorage> { + let (server, encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )) +} + +pub fn database_service() -> Arc<dyn DatabaseCloudService> { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseDatabaseServiceImpl::new(server)) +} + +pub fn user_auth_service() -> Arc<dyn UserCloudService> { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseUserServiceImpl::new(server, vec![], None)) +} + +pub fn folder_service() -> Arc<dyn FolderCloudService> { + let (server, _encryption_impl) = supabase_server_service(None); + Arc::new(SupabaseFolderServiceImpl::new(server)) +} + +#[allow(dead_code)] +pub fn file_storage_service() -> Arc<dyn ObjectStorageService> { + let encryption_impl: Arc<dyn AppFlowyEncryption> = Arc::new(EncryptionImpl::new(None)); + let config = SupabaseConfiguration::from_env().unwrap(); + Arc::new( + SupabaseFileStorage::new( + &config, + Arc::downgrade(&encryption_impl), + Arc::new(TestFileStoragePlan), + ) + .unwrap(), + ) +} + +#[allow(dead_code)] +pub fn encryption_folder_service( + secret: Option<String>, +) -> (Arc<dyn FolderCloudService>, Arc<dyn AppFlowyEncryption>) { + let (server, encryption_impl) = supabase_server_service(secret); + let service = Arc::new(SupabaseFolderServiceImpl::new(server)); + (service, encryption_impl) +} + +#[allow(dead_code)] +pub fn encryption_collab_service( + secret: Option<String>, +) -> (Arc<dyn RemoteCollabStorage>, Arc<dyn AppFlowyEncryption>) { + let (server, encryption_impl) = supabase_server_service(secret); + let service = Arc::new(SupabaseCollabStorageImpl::new( + server, + None, + Arc::downgrade(&encryption_impl), + )); + (service, encryption_impl) +} + +#[allow(dead_code)] +pub async fn print_encryption_folder( + uid: &i64, + folder_id: &str, + encryption_secret: Option<String>, +) { + let (cloud_service, _encryption) = encryption_folder_service(encryption_secret); + let folder_data = cloud_service.get_folder_data(folder_id, uid).await.unwrap(); + let json = serde_json::to_value(folder_data).unwrap(); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +#[allow(dead_code)] +pub async fn print_encryption_folder_snapshot( + uid: &i64, + folder_id: &str, + encryption_secret: Option<String>, +) { + let (cloud_service, _encryption) = encryption_collab_service(encryption_secret); + let snapshot = cloud_service + .get_snapshots(folder_id, 1) + .await + .pop() + .unwrap(); + let collab = Arc::new(MutexCollab::new( + Collab::new_with_source( + CollabOrigin::Empty, + folder_id, + DataSource::DocStateV1(snapshot.blob), + vec![], + false, + ) + .unwrap(), + )); + let folder_data = Folder::open(uid, collab, None) + .unwrap() + .get_folder_data(folder_id) + .unwrap(); + let json = serde_json::to_value(folder_data).unwrap(); + println!("{}", serde_json::to_string_pretty(&json).unwrap()); +} + +pub fn supabase_server_service( + encryption_secret: Option<String>, +) -> (SupabaseServerServiceImpl, Arc<dyn AppFlowyEncryption>) { + let config = SupabaseConfiguration::from_env().unwrap(); + let encryption_impl: Arc<dyn AppFlowyEncryption> = + Arc::new(EncryptionImpl::new(encryption_secret)); + let encryption = Arc::downgrade(&encryption_impl); + let server = Arc::new(RESTfulPostgresServer::new(config, encryption)); + (SupabaseServerServiceImpl::new(server), encryption_impl) +} + +pub fn third_party_sign_up_param(uuid: String) -> HashMap<String, String> { + let mut params = HashMap::new(); + params.insert(USER_UUID.to_string(), uuid); + params.insert( + USER_EMAIL.to_string(), + format!("{}@test.com", Uuid::new_v4()), + ); + params.insert(USER_DEVICE_ID.to_string(), Uuid::new_v4().to_string()); + params +} + +pub struct TestFileStoragePlan; + +impl FileStoragePlan for TestFileStoragePlan { + fn storage_size(&self) -> FutureResult<u64, FlowyError> { + // 1 GB + FutureResult::new(async { Ok(1024 * 1024 * 1024) }) + } + + fn maximum_file_size(&self) -> FutureResult<u64, FlowyError> { + // 5 MB + FutureResult::new(async { Ok(5 * 1024 * 1024) }) + } + + fn check_upload_object(&self, _object: &StorageObject) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } +} diff --git a/frontend/rust-lib/flowy-server/tests/test.txt b/frontend/rust-lib/flowy-server/tests/test.txt new file mode 100644 index 0000000000..95d09f2b10 --- /dev/null +++ b/frontend/rust-lib/flowy-server/tests/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/Cargo.toml b/frontend/rust-lib/flowy-sqlite/Cargo.toml index 345b05f903..e49452df75 100644 --- a/frontend/rust-lib/flowy-sqlite/Cargo.toml +++ b/frontend/rust-lib/flowy-sqlite/Cargo.toml @@ -7,12 +7,13 @@ edition = "2018" [dependencies] diesel.workspace = true -diesel_derives = { workspace = true, features = ["sqlite", "r2d2"] } +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } diesel_migrations = { version = "2.1.0", features = ["sqlite"] } tracing.workspace = true serde.workspace = true serde_json.workspace = true anyhow.workspace = true +parking_lot.workspace = true r2d2 = "0.8.10" libsqlite3-sys = { version = "0.27.0", features = ["bundled"] } diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/down.sql deleted file mode 100644 index dddef11ffa..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -drop table upload_file_table; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/up.sql deleted file mode 100644 index 13c7b01e9a..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-16-131359_file_upload/up.sql +++ /dev/null @@ -1,20 +0,0 @@ --- Your SQL goes here -CREATE TABLE upload_file_table ( - workspace_id TEXT NOT NULL, - file_id TEXT NOT NULL, - parent_dir TEXT NOT NULL, - local_file_path TEXT NOT NULL, - content_type TEXT NOT NULL, - chunk_size INTEGER NOT NULL, - num_chunk INTEGER NOT NULL, - upload_id TEXT NOT NULL DEFAULT '', - created_at BIGINT NOT NULL, - PRIMARY KEY (workspace_id, parent_dir, file_id) -); - -CREATE TABLE upload_file_part ( - upload_id TEXT NOT NULL, - e_tag TEXT NOT NULL, - part_num INTEGER NOT NULL, - PRIMARY KEY (upload_id, e_tag) -); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql deleted file mode 100644 index d9a93fe9a1..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/down.sql +++ /dev/null @@ -1 +0,0 @@ --- This file should undo anything in `up.sql` diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql deleted file mode 100644 index 7143a90355..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-22-082201_user_ai_model/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE user_table ADD COLUMN ai_model TEXT NOT NULL DEFAULT ''; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/down.sql deleted file mode 100644 index d9a93fe9a1..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/down.sql +++ /dev/null @@ -1 +0,0 @@ --- This file should undo anything in `up.sql` diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/up.sql deleted file mode 100644 index 2361adeb34..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-06-26-015936_chat_setting/up.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Your SQL goes here -ALTER TABLE chat_table ADD COLUMN local_model_path TEXT NOT NULL DEFAULT ''; -ALTER TABLE chat_table ADD COLUMN local_model_name TEXT NOT NULL DEFAULT ''; -ALTER TABLE chat_table ADD COLUMN local_enabled BOOLEAN NOT NULL DEFAULT FALSE; -ALTER TABLE chat_table ADD COLUMN sync_to_cloud BOOLEAN NOT NULL DEFAULT TRUE; - - -CREATE TABLE chat_local_setting_table -( - chat_id TEXT PRIMARY KEY NOT NULL, - local_model_path TEXT NOT NULL, - local_model_name TEXT NOT NULL DEFAULT '' -); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/down.sql deleted file mode 100644 index c19ec5e34e..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE chat_message_table DROP COLUMN metadata; - diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/up.sql deleted file mode 100644 index d184c20b6f..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-05-024351_chat_message_metadata/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE chat_message_table ADD COLUMN metadata TEXT; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/down.sql deleted file mode 100644 index d9a93fe9a1..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/down.sql +++ /dev/null @@ -1 +0,0 @@ --- This file should undo anything in `up.sql` diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/up.sql deleted file mode 100644 index 948cf866a7..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-07-093650_chat_metadata/up.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Your SQL goes here -ALTER TABLE chat_table RENAME COLUMN local_model_path TO local_files; -ALTER TABLE chat_table RENAME COLUMN local_model_name TO metadata; - diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql deleted file mode 100644 index 8c072ae1ce..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE upload_file_table DROP COLUMN is_finish; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql deleted file mode 100644 index 088564dca4..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-08-20-061727_file_upload_finish/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE upload_file_table ADD COLUMN is_finish BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/down.sql deleted file mode 100644 index 88bc9048aa..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE user_workspace_table DROP COLUMN member_count; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/up.sql deleted file mode 100644 index 8198d4f3f5..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-11-08-102351_workspace_member_count/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE user_workspace_table ADD COLUMN member_count BIGINT NOT NULL DEFAULT 1; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/down.sql deleted file mode 100644 index da7c4c54cc..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/down.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE user_workspace_table DROP COLUMN role; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/up.sql deleted file mode 100644 index 38842fde4b..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-12-102351_workspace_role/up.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE user_workspace_table ADD COLUMN role INT; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/down.sql deleted file mode 100644 index ebe24324fe..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE af_collab_metadata; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/up.sql deleted file mode 100644 index 84668f23fa..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2024-12-29-061706_collab_metadata/up.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Your SQL goes here -CREATE TABLE af_collab_metadata ( - object_id TEXT PRIMARY KEY NOT NULL, - updated_at BIGINT NOT NULL, - prev_sync_state_vector BLOB NOT NULL, - collab_type INTEGER NOT NULL -); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql deleted file mode 100644 index 8b07e6189d..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/down.sql +++ /dev/null @@ -1,9 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE chat_table - ADD COLUMN local_enabled INTEGER; -ALTER TABLE chat_table - ADD COLUMN sync_to_cloud INTEGER; -ALTER TABLE chat_table - ADD COLUMN local_files TEXT; - -ALTER TABLE chat_table DROP COLUMN rag_ids; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql deleted file mode 100644 index 0604601486..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-042326_chat_metadata/up.sql +++ /dev/null @@ -1,4 +0,0 @@ -ALTER TABLE chat_table DROP COLUMN local_enabled; -ALTER TABLE chat_table DROP COLUMN local_files; -ALTER TABLE chat_table DROP COLUMN sync_to_cloud; -ALTER TABLE chat_table ADD COLUMN rag_ids TEXT; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql deleted file mode 100644 index 65dec0f30a..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE chat_table DROP COLUMN is_sync; -ALTER TABLE chat_message_table DROP COLUMN is_sync; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql deleted file mode 100644 index ff8dce94bc..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-17-142713_offline_chat_message/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here -ALTER TABLE chat_table - ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; -ALTER TABLE chat_message_table - ADD COLUMN is_sync BOOLEAN DEFAULT TRUE NOT NULL; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql deleted file mode 100644 index 50602eb129..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE user_workspace_table -DROP COLUMN auth_type; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql deleted file mode 100644 index 7d986e3e57..0000000000 --- a/frontend/rust-lib/flowy-sqlite/migrations/2025-04-18-132232_user_workspace_auth_type/up.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Your SQL goes here -ALTER TABLE user_workspace_table - ADD COLUMN workspace_type INTEGER NOT NULL DEFAULT 1; - --- 2. Back‑fill from user_table.auth_type -UPDATE user_workspace_table -SET workspace_type = (SELECT ut.auth_type - FROM user_table ut - WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)) -WHERE EXISTS (SELECT 1 - FROM user_table ut - WHERE ut.id = CAST(user_workspace_table.uid AS TEXT)); - -ALTER TABLE user_table DROP COLUMN stability_ai_key; -ALTER TABLE user_table DROP COLUMN openai_key; -ALTER TABLE user_table DROP COLUMN workspace; -ALTER TABLE user_table DROP COLUMN encryption_type; -ALTER TABLE user_table DROP COLUMN ai_model; - -CREATE TABLE workspace_setting_table ( - id TEXT PRIMARY KEY NOT NULL , - disable_search_indexing BOOLEAN DEFAULT FALSE NOT NULL , - ai_model TEXT DEFAULT "" NOT NULL -); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs index 799f5b0666..1ec71688c5 100644 --- a/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs +++ b/frontend/rust-lib/flowy-sqlite/src/kv/kv.rs @@ -11,13 +11,13 @@ use crate::sqlite_impl::{Database, PoolConfig}; const DB_NAME: &str = "cache.db"; -/// [KVStorePreferences] uses a sqlite database to store key value pairs. +/// [StorePreferences] uses a sqlite database to store key value pairs. /// Most of the time, it used to storage AppFlowy configuration. #[derive(Clone)] -pub struct KVStorePreferences { +pub struct StorePreferences { database: Option<Database>, } -impl KVStorePreferences { +impl StorePreferences { #[tracing::instrument(level = "trace", err)] pub fn new(root: &str) -> Result<Self, anyhow::Error> { if !Path::new(root).exists() { @@ -46,8 +46,8 @@ impl KVStorePreferences { } /// Set a object that implements [Serialize] trait of a key - pub fn set_object<T: Serialize>(&self, key: &str, value: &T) -> Result<(), anyhow::Error> { - let value = serde_json::to_string(value)?; + pub fn set_object<T: Serialize>(&self, key: &str, value: T) -> Result<(), anyhow::Error> { + let value = serde_json::to_string(&value)?; self.set_key_value(key, Some(value))?; Ok(()) } @@ -63,7 +63,7 @@ impl KVStorePreferences { } /// Get a bool value of a key - pub fn get_bool_or_default(&self, key: &str) -> bool { + pub fn get_bool(&self, key: &str) -> bool { self .get_key_value(key) .and_then(|kv| kv.value) @@ -71,13 +71,6 @@ impl KVStorePreferences { .unwrap_or(false) } - pub fn get_bool(&self, key: &str) -> Option<bool> { - self - .get_key_value(key) - .and_then(|kv| kv.value) - .and_then(|v| v.parse::<bool>().ok()) - } - /// Get a i64 value of a key pub fn get_i64(&self, key: &str) -> Option<i64> { self @@ -145,7 +138,7 @@ mod tests { use serde::{Deserialize, Serialize}; use tempfile::TempDir; - use crate::kv::KVStorePreferences; + use crate::kv::StorePreferences; #[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] struct Person { @@ -157,15 +150,15 @@ mod tests { fn kv_store_test() { let tempdir = TempDir::new().unwrap(); let path = tempdir.into_path(); - let store = KVStorePreferences::new(path.to_str().unwrap()).unwrap(); + let store = StorePreferences::new(path.to_str().unwrap()).unwrap(); store.set_str("1", "hello".to_string()); assert_eq!(store.get_str("1").unwrap(), "hello"); assert_eq!(store.get_str("2"), None); store.set_bool("1", true).unwrap(); - assert!(store.get_bool_or_default("1")); - assert!(!store.get_bool_or_default("2")); + assert!(store.get_bool("1")); + assert!(!store.get_bool("2")); store.set_i64("1", 1).unwrap(); assert_eq!(store.get_i64("1").unwrap(), 1); @@ -175,7 +168,7 @@ mod tests { name: "nathan".to_string(), age: 30, }; - store.set_object("1", &person.clone()).unwrap(); + store.set_object("1", person.clone()).unwrap(); assert_eq!(store.get_object::<Person>("1").unwrap(), person); } } diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index f91d187b75..5dbbc78501 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -1,22 +1,5 @@ // @generated automatically by Diesel CLI. -diesel::table! { - af_collab_metadata (object_id) { - object_id -> Text, - updated_at -> BigInt, - prev_sync_state_vector -> Binary, - collab_type -> Integer, - } -} - -diesel::table! { - chat_local_setting_table (chat_id) { - chat_id -> Text, - local_model_path -> Text, - local_model_name -> Text, - } -} - diesel::table! { chat_message_table (message_id) { message_id -> BigInt, @@ -26,8 +9,6 @@ diesel::table! { author_type -> BigInt, author_id -> Text, reply_message_id -> Nullable<BigInt>, - metadata -> Nullable<Text>, - is_sync -> Bool, } } @@ -36,9 +17,6 @@ diesel::table! { chat_id -> Text, created_at -> BigInt, name -> Text, - metadata -> Text, - rag_ids -> Nullable<Text>, - is_sync -> Bool, } } @@ -54,29 +32,6 @@ diesel::table! { } } -diesel::table! { - upload_file_part (upload_id, e_tag) { - upload_id -> Text, - e_tag -> Text, - part_num -> Integer, - } -} - -diesel::table! { - upload_file_table (workspace_id, file_id, parent_dir) { - workspace_id -> Text, - file_id -> Text, - parent_dir -> Text, - local_file_path -> Text, - content_type -> Text, - chunk_size -> Integer, - num_chunk -> Integer, - upload_id -> Text, - created_at -> BigInt, - is_finish -> Bool, - } -} - diesel::table! { user_data_migration_records (id) { id -> Integer, @@ -89,10 +44,14 @@ diesel::table! { user_table (id) { id -> Text, name -> Text, + workspace -> Text, icon_url -> Text, + openai_key -> Text, token -> Text, email -> Text, auth_type -> Integer, + encryption_type -> Text, + stability_ai_key -> Text, updated_at -> BigInt, } } @@ -105,9 +64,6 @@ diesel::table! { created_at -> BigInt, database_storage_id -> Text, icon -> Text, - member_count -> BigInt, - role -> Nullable<Integer>, - workspace_type -> Integer, } } @@ -123,25 +79,12 @@ diesel::table! { } } -diesel::table! { - workspace_setting_table (id) { - id -> Text, - disable_search_indexing -> Bool, - ai_model -> Text, - } -} - diesel::allow_tables_to_appear_in_same_query!( - af_collab_metadata, - chat_local_setting_table, chat_message_table, chat_table, collab_snapshot, - upload_file_part, - upload_file_table, user_data_migration_records, user_table, user_workspace_table, workspace_members_table, - workspace_setting_table, ); diff --git a/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs index 10dfda7a9d..05054a6406 100644 --- a/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs +++ b/frontend/rust-lib/flowy-sqlite/src/sqlite_impl/pragma.rs @@ -51,7 +51,6 @@ pub trait PragmaExtension: ConnectionExtension { self.query::<ST, T>(&query) } - #[allow(dead_code)] fn pragma_get<'query, ST, T>(&mut self, key: &str, schema: Option<&str>) -> Result<T> where SqlLiteral<ST>: LoadQuery<'query, SqliteConnection, T>, @@ -65,12 +64,10 @@ pub trait PragmaExtension: ConnectionExtension { self.query::<ST, T>(&query) } - #[allow(dead_code)] fn pragma_set_busy_timeout(&mut self, timeout_ms: i32) -> Result<i32> { self.pragma_ret::<Integer, i32, i32>("busy_timeout", timeout_ms, None) } - #[allow(dead_code)] fn pragma_get_busy_timeout(&mut self) -> Result<i32> { self.pragma_get::<Integer, i32>("busy_timeout", None) } @@ -83,14 +80,12 @@ pub trait PragmaExtension: ConnectionExtension { self.pragma_ret::<Integer, i32, SQLiteJournalMode>("journal_mode", mode, schema) } - #[allow(dead_code)] fn pragma_get_journal_mode(&mut self, schema: Option<&str>) -> Result<SQLiteJournalMode> { self .pragma_get::<Text, String>("journal_mode", schema)? .parse() } - #[allow(dead_code)] fn pragma_set_synchronous( &mut self, synchronous: SQLiteSynchronous, @@ -99,7 +94,6 @@ pub trait PragmaExtension: ConnectionExtension { self.pragma("synchronous", synchronous as u8, schema) } - #[allow(dead_code)] fn pragma_get_synchronous(&mut self, schema: Option<&str>) -> Result<SQLiteSynchronous> { self .pragma_get::<Integer, i32>("synchronous", schema)? diff --git a/frontend/rust-lib/flowy-storage-pub/Cargo.toml b/frontend/rust-lib/flowy-storage-pub/Cargo.toml deleted file mode 100644 index d36c997432..0000000000 --- a/frontend/rust-lib/flowy-storage-pub/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "flowy-storage-pub" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -lib-infra.workspace = true -serde.workspace = true -async-trait.workspace = true -mime = "0.3.17" -flowy-error = { workspace = true, features = ["impl_from_reqwest"] } -bytes.workspace = true -mime_guess = "2.0.4" -client-api-entity = { workspace = true } -tokio = { workspace = true, features = ["sync", "io-util"] } -anyhow = "1.0.86" -uuid.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs b/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs deleted file mode 100644 index e5a78e2974..0000000000 --- a/frontend/rust-lib/flowy-storage-pub/src/chunked_byte.rs +++ /dev/null @@ -1,417 +0,0 @@ -use anyhow::anyhow; -use bytes::Bytes; -use std::fmt::Display; -use std::path::Path; -use tokio::fs::File; -use tokio::io::AsyncReadExt; -use tokio::io::SeekFrom; -use tokio::io::{self, AsyncSeekExt}; - -/// In Amazon S3, the minimum chunk size for multipart uploads is 5 MB,except for the last part, -/// which can be smaller.(https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html) -pub const MIN_CHUNK_SIZE: usize = 5 * 1024 * 1024; // Minimum Chunk Size 5 MB -#[derive(Debug)] -pub struct ChunkedBytes { - file: File, - chunk_size: usize, - file_size: u64, - current_offset: u64, -} - -impl ChunkedBytes { - /// Create a `ChunkedBytes` instance from a file. - pub async fn from_file<P: AsRef<Path>>( - file_path: P, - chunk_size: usize, - ) -> Result<Self, anyhow::Error> { - if chunk_size < MIN_CHUNK_SIZE { - return Err(anyhow!( - "Chunk size should be greater than or equal to {} bytes", - MIN_CHUNK_SIZE - )); - } - - let file = File::open(file_path).await?; - let file_size = file.metadata().await?.len(); - - Ok(ChunkedBytes { - file, - chunk_size, - file_size, - current_offset: 0, - }) - } - - /// Read the next chunk from the file. - pub async fn next_chunk(&mut self) -> Option<Result<Bytes, io::Error>> { - if self.current_offset >= self.file_size { - return None; // End of file - } - - let mut buffer = vec![0u8; self.chunk_size]; - let mut total_bytes_read = 0; - - // Loop to ensure the buffer is filled or EOF is reached - while total_bytes_read < self.chunk_size { - let read_result = self.file.read(&mut buffer[total_bytes_read..]).await; - match read_result { - Ok(0) => break, // EOF - Ok(n) => total_bytes_read += n, - Err(e) => return Some(Err(e)), - } - } - - if total_bytes_read == 0 { - return None; // EOF - } - - self.current_offset += total_bytes_read as u64; - Some(Ok(Bytes::from(buffer[..total_bytes_read].to_vec()))) - } - - /// Set the offset for the next chunk to be read. - pub async fn set_offset(&mut self, offset: u64) -> Result<(), io::Error> { - if offset > self.file_size { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Offset out of range", - )); - } - self.current_offset = offset; - self.file.seek(SeekFrom::Start(offset)).await?; - Ok(()) - } - - /// Get the total number of chunks in the file. - pub fn total_chunks(&self) -> usize { - ((self.file_size + self.chunk_size as u64 - 1) / self.chunk_size as u64) as usize - } - - /// Get the current offset in the file. - pub fn current_offset(&self) -> u64 { - self.current_offset - } -} - -impl Display for ChunkedBytes { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "file_size: {}, chunk_size: {}, total_chunks: {}, current_offset: {}", - self.file_size, - self.chunk_size, - self.total_chunks(), - self.current_offset - ) - } -} - -// Function to split input bytes into several chunks and return offsets -pub fn split_into_chunks(data: &Bytes, chunk_size: usize) -> Vec<(usize, usize)> { - calculate_offsets(data.len(), chunk_size) -} - -pub fn calculate_offsets(data_len: usize, chunk_size: usize) -> Vec<(usize, usize)> { - let mut offsets = Vec::new(); - let mut start = 0; - - while start < data_len { - let end = std::cmp::min(start + chunk_size, data_len); - offsets.push((start, end)); - start = end; - } - - offsets -} - -#[cfg(test)] -mod tests { - use super::*; - use std::env::temp_dir; - use tokio::io::AsyncWriteExt; - - #[tokio::test] - async fn test_chunked_bytes_small_file() { - // Create a small file of 1 MB - let mut file_path = temp_dir(); - file_path.push("test_small_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 1024 * 1024]).await.unwrap(); // 1 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks and read the data - assert_eq!(chunked_bytes.total_chunks(), 1); // Only 1 chunk due to file size - let chunk = chunked_bytes.next_chunk().await.unwrap().unwrap(); - assert_eq!(chunk.len(), 1024 * 1024); // The full 1 MB - - // Ensure no more chunks are available - assert!(chunked_bytes.next_chunk().await.is_none()); - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_chunked_bytes_large_file() { - // Create a large file of 15 MB - let mut file_path = temp_dir(); - file_path.push("test_large_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 15 * 1024 * 1024]).await.unwrap(); // 15 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - assert_eq!(chunked_bytes.total_chunks(), 3); // 15 MB split into 3 chunks of 5 MB - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!( - chunk_sizes, - vec![5 * 1024 * 1024, 5 * 1024 * 1024, 5 * 1024 * 1024] - ); - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_set_offset() { - // Create a file of 10 MB - let mut file_path = temp_dir(); - file_path.push("test_offset_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 10 * 1024 * 1024]).await.unwrap(); // 10 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Set the offset to 5 MB and read the next chunk - chunked_bytes.set_offset(5 * 1024 * 1024).await.unwrap(); - let chunk = chunked_bytes.next_chunk().await.unwrap().unwrap(); - assert_eq!(chunk.len(), 5 * 1024 * 1024); // Read the second chunk - - // Ensure no more chunks are available - assert!(chunked_bytes.next_chunk().await.is_none()); - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_partial_chunk() { - // Create a file of 6 MB (one full chunk and one partial chunk) - let mut file_path = temp_dir(); - file_path.push("test_partial_chunk_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 6 * 1024 * 1024]).await.unwrap(); // 6 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - assert_eq!(chunked_bytes.total_chunks(), 2); // 6 MB split into 1 full chunk and 1 partial chunk - - // Read the first chunk - let chunk1 = chunked_bytes.next_chunk().await.unwrap().unwrap(); - assert_eq!(chunk1.len(), 5 * 1024 * 1024); // Full chunk - - // Read the second chunk - let chunk2 = chunked_bytes.next_chunk().await.unwrap().unwrap(); - assert_eq!(chunk2.len(), 1024 * 1024); // Partial chunk - - // Ensure no more chunks are available - assert!(chunked_bytes.next_chunk().await.is_none()); - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_invalid_offset() { - // Create a file of 5 MB - let mut file_path = temp_dir(); - file_path.push("test_invalid_offset_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 5 * 1024 * 1024]).await.unwrap(); // 5 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Try setting an invalid offset - let result = chunked_bytes.set_offset(10 * 1024 * 1024).await; - assert!(result.is_err()); // Offset out of range - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_exact_multiple_chunk_file() { - // Create a file of 10 MB (exact multiple of 5 MB) - let mut file_path = temp_dir(); - file_path.push("test_exact_multiple_chunk_file"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 10 * 1024 * 1024]).await.unwrap(); // 10 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - let expected_offsets = calculate_offsets(10 * 1024 * 1024, MIN_CHUNK_SIZE); - assert_eq!(chunked_bytes.total_chunks(), expected_offsets.len()); // 2 chunks - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!(chunk_sizes, vec![5 * 1024 * 1024, 5 * 1024 * 1024]); // 2 full chunks - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_small_file_less_than_chunk_size() { - // Create a file of 2 MB (smaller than 5 MB) - let mut file_path = temp_dir(); - file_path.push("test_small_file_less_than_chunk_size"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 2 * 1024 * 1024]).await.unwrap(); // 2 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - let expected_offsets = calculate_offsets(2 * 1024 * 1024, MIN_CHUNK_SIZE); - assert_eq!(chunked_bytes.total_chunks(), expected_offsets.len()); // 1 chunk - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!(chunk_sizes, vec![2 * 1024 * 1024]); // 1 partial chunk - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_slightly_larger_than_chunk_size() { - // Create a file of 5.5 MB (slightly larger than 1 chunk) - let mut file_path = temp_dir(); - file_path.push("test_file_slightly_larger_than_chunk_size"); - - let mut file = File::create(&file_path).await.unwrap(); - file - .write_all(&vec![0; 5 * 1024 * 1024 + 512 * 1024]) - .await - .unwrap(); // 5.5 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - let expected_offsets = calculate_offsets(5 * 1024 * 1024 + 512 * 1024, MIN_CHUNK_SIZE); - assert_eq!(chunked_bytes.total_chunks(), expected_offsets.len()); // 2 chunks - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!(chunk_sizes, vec![5 * 1024 * 1024, 512 * 1024]); // 1 full chunk, 1 partial chunk - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_large_file_with_many_chunks() { - // Create a file of 50 MB (10 chunks of 5 MB) - let mut file_path = temp_dir(); - file_path.push("test_large_file_with_many_chunks"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 50 * 1024 * 1024]).await.unwrap(); // 50 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - let expected_offsets = calculate_offsets(50 * 1024 * 1024, MIN_CHUNK_SIZE); - assert_eq!(chunked_bytes.total_chunks(), expected_offsets.len()); // 10 chunks - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!(chunk_sizes, vec![5 * 1024 * 1024; 10]); // 10 full chunks - - tokio::fs::remove_file(file_path).await.unwrap(); - } - - #[tokio::test] - async fn test_file_with_exact_chunk_size() { - // Create a file of exactly 5 MB - let mut file_path = temp_dir(); - file_path.push("test_file_with_exact_chunk_size"); - - let mut file = File::create(&file_path).await.unwrap(); - file.write_all(&vec![0; 5 * 1024 * 1024]).await.unwrap(); // 5 MB - file.flush().await.unwrap(); - - // Create ChunkedBytes instance - let mut chunked_bytes = ChunkedBytes::from_file(&file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Validate total chunks - let expected_offsets = calculate_offsets(5 * 1024 * 1024, MIN_CHUNK_SIZE); - assert_eq!(chunked_bytes.total_chunks(), expected_offsets.len()); // 1 chunk - - // Read and validate all chunks - let mut chunk_sizes = vec![]; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - chunk_sizes.push(chunk_result.unwrap().len()); - } - assert_eq!(chunk_sizes, vec![5 * 1024 * 1024]); // 1 full chunk - - tokio::fs::remove_file(file_path).await.unwrap(); - } -} diff --git a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs b/frontend/rust-lib/flowy-storage-pub/src/cloud.rs deleted file mode 100644 index 5a72262ac9..0000000000 --- a/frontend/rust-lib/flowy-storage-pub/src/cloud.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::storage::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; -use async_trait::async_trait; -use bytes::Bytes; -use flowy_error::{FlowyError, FlowyResult}; -use mime::Mime; -use uuid::Uuid; - -#[async_trait] -pub trait StorageCloudService: Send + Sync { - /// Creates a new storage object. - /// - /// # Parameters - /// - `url`: url of the object to be created. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - async fn get_object_url(&self, object_id: ObjectIdentity) -> Result<String, FlowyError>; - - /// Creates a new storage object. - /// - /// # Parameters - /// - `url`: url of the object to be created. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - async fn put_object(&self, url: String, object_value: ObjectValue) -> Result<(), FlowyError>; - - /// Deletes a storage object by its URL. - /// - /// # Parameters - /// - `url`: url of the object to be deleted. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - async fn delete_object(&self, url: &str) -> Result<(), FlowyError>; - - /// Fetches a storage object by its URL. - /// - /// # Parameters - /// - `url`: url of the object - /// - /// # Returns - /// - `Ok(File)`: The returned file object. - /// - `Err(Error)`: An error occurred during the operation. - async fn get_object(&self, url: String) -> Result<ObjectValue, FlowyError>; - async fn get_object_url_v1( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - ) -> FlowyResult<String>; - - /// Return workspace_id, parent_dir, file_id - async fn parse_object_url_v1(&self, url: &str) -> Option<(Uuid, String, String)>; - - async fn create_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - file_id: &str, - content_type: &str, - file_size: u64, - ) -> Result<CreateUploadResponse, FlowyError>; - - async fn upload_part( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - part_number: i32, - body: Vec<u8>, - ) -> Result<UploadPartResponse, FlowyError>; - - async fn complete_upload( - &self, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - parts: Vec<CompletedPartRequest>, - ) -> Result<(), FlowyError>; -} - -pub struct ObjectIdentity { - pub workspace_id: Uuid, - pub file_id: String, - pub ext: String, -} - -#[derive(Clone)] -pub struct ObjectValue { - pub raw: Bytes, - pub mime: Mime, -} - -pub struct StorageObject { - pub workspace_id: Uuid, - pub file_name: String, - pub value: ObjectValueSupabase, -} - -pub enum ObjectValueSupabase { - File { file_path: String }, - Bytes { bytes: Bytes, mime: String }, -} - -impl ObjectValueSupabase { - pub fn mime_type(&self) -> String { - match self { - ObjectValueSupabase::File { file_path } => mime_guess::from_path(file_path) - .first_or_octet_stream() - .to_string(), - ObjectValueSupabase::Bytes { mime, .. } => mime.clone(), - } - } -} - -impl StorageObject { - /// Creates a `StorageObject` from a file. - /// - /// # Parameters - /// - /// * `name`: The name of the storage object. - /// * `file_path`: The file path to the storage object's data. - /// - pub fn from_file<T: ToString>(workspace_id: &Uuid, file_name: &str, file_path: T) -> Self { - Self { - workspace_id: *workspace_id, - file_name: file_name.to_string(), - value: ObjectValueSupabase::File { - file_path: file_path.to_string(), - }, - } - } - - /// Creates a `StorageObject` from bytes. - /// - /// # Parameters - /// - /// * `name`: The name of the storage object. - /// * `bytes`: The byte data of the storage object. - /// * `mime`: The MIME type of the storage object. - /// - pub fn from_bytes<B: Into<Bytes>>( - workspace_id: &Uuid, - file_name: &str, - bytes: B, - mime: String, - ) -> Self { - let bytes = bytes.into(); - Self { - workspace_id: *workspace_id, - file_name: file_name.to_string(), - value: ObjectValueSupabase::Bytes { bytes, mime }, - } - } - - /// Gets the file size of the `StorageObject`. - /// - /// # Returns - /// - /// The file size in bytes. - pub fn file_size(&self) -> u64 { - match &self.value { - ObjectValueSupabase::File { file_path } => std::fs::metadata(file_path).unwrap().len(), - ObjectValueSupabase::Bytes { bytes, .. } => bytes.len() as u64, - } - } -} diff --git a/frontend/rust-lib/flowy-storage-pub/src/lib.rs b/frontend/rust-lib/flowy-storage-pub/src/lib.rs deleted file mode 100644 index fa646847a8..0000000000 --- a/frontend/rust-lib/flowy-storage-pub/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod chunked_byte; -pub mod cloud; -pub mod storage; diff --git a/frontend/rust-lib/flowy-storage-pub/src/storage.rs b/frontend/rust-lib/flowy-storage-pub/src/storage.rs deleted file mode 100644 index 061584dc6d..0000000000 --- a/frontend/rust-lib/flowy-storage-pub/src/storage.rs +++ /dev/null @@ -1,133 +0,0 @@ -use async_trait::async_trait; -pub use client_api_entity::{CompletedPartRequest, CreateUploadResponse, UploadPartResponse}; -use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::box_any::BoxAny; -use serde::Serialize; -use std::fmt::Display; -use std::ops::{Deref, DerefMut}; -use tokio::sync::broadcast; - -#[async_trait] -pub trait StorageService: Send + Sync { - async fn delete_object(&self, url: String) -> FlowyResult<()>; - - fn download_object(&self, url: String, local_file_path: String) -> FlowyResult<()>; - - async fn create_upload( - &self, - workspace_id: &str, - parent_dir: &str, - local_file_path: &str, - ) -> Result<(CreatedUpload, Option<FileProgressReceiver>), FlowyError>; - - async fn start_upload(&self, record: &BoxAny) -> Result<(), FlowyError>; - - async fn resume_upload( - &self, - workspace_id: &str, - parent_dir: &str, - file_id: &str, - ) -> Result<(), FlowyError>; - - async fn subscribe_file_progress( - &self, - parent_idr: &str, - file_id: &str, - ) -> Result<Option<FileProgressReceiver>, FlowyError>; -} - -pub struct FileProgressReceiver { - pub rx: broadcast::Receiver<FileUploadState>, - pub file_id: String, -} - -impl Deref for FileProgressReceiver { - type Target = broadcast::Receiver<FileUploadState>; - - fn deref(&self) -> &Self::Target { - &self.rx - } -} - -impl DerefMut for FileProgressReceiver { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.rx - } -} - -#[derive(Clone, Debug)] -pub enum FileUploadState { - NotStarted, - Uploading { progress: f64 }, - Finished { file_id: String }, -} - -#[derive(Clone, Debug, Serialize)] -pub struct FileProgress { - pub file_url: String, - pub file_id: String, - pub progress: f64, - pub error: Option<String>, -} - -impl FileProgress { - pub fn new_progress(file_url: String, file_id: String, progress: f64) -> Self { - FileProgress { - file_url, - file_id, - progress: (progress * 10.0).round() / 10.0, - error: None, - } - } - - pub fn new_error(file_url: String, file_id: String, error: String) -> Self { - FileProgress { - file_url, - file_id, - progress: 0.0, - error: Some(error), - } - } -} - -impl Display for FileProgress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FileProgress: {} - {}", self.file_id, self.progress) - } -} - -#[derive(Debug)] -pub struct ProgressNotifier { - file_id: String, - tx: broadcast::Sender<FileUploadState>, - pub current_value: Option<FileUploadState>, -} - -impl ProgressNotifier { - pub fn new(file_id: String) -> Self { - let (tx, _) = broadcast::channel(100); - ProgressNotifier { - file_id, - tx, - current_value: None, - } - } - - pub fn subscribe(&self) -> FileProgressReceiver { - FileProgressReceiver { - rx: self.tx.subscribe(), - file_id: self.file_id.clone(), - } - } - - pub async fn notify(&mut self, progress: FileUploadState) { - self.current_value = Some(progress.clone()); - let _ = self.tx.send(progress); - } -} - -#[derive(Clone)] -pub struct CreatedUpload { - pub url: String, - pub file_id: String, -} diff --git a/frontend/rust-lib/flowy-storage/Cargo.toml b/frontend/rust-lib/flowy-storage/Cargo.toml index add7996439..d35c17565e 100644 --- a/frontend/rust-lib/flowy-storage/Cargo.toml +++ b/frontend/rust-lib/flowy-storage/Cargo.toml @@ -3,37 +3,21 @@ name = "flowy-storage" version = "0.1.0" edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] -lib-dispatch = { workspace = true } -flowy-storage-pub.workspace = true +reqwest = { version = "0.11", features = ["json", "stream"] } serde_json.workspace = true serde.workspace = true async-trait.workspace = true bytes.workspace = true +mime_guess = "2.0" lib-infra = { workspace = true } url = "2.2.2" -flowy-error = { workspace = true, features = ["impl_from_reqwest", "impl_from_sqlite"] } -tokio = { workspace = true, features = ["sync", "io-util"] } +flowy-error = { workspace = true, features = ["impl_from_reqwest"] } +mime = "0.3.17" +tokio = { workspace = true, features = ["sync", "io-util"]} tracing.workspace = true -flowy-sqlite.workspace = true -mime_guess = "2.0.4" -chrono = "0.4.33" -flowy-notification = { workspace = true } -flowy-derive.workspace = true -protobuf = { workspace = true } -dashmap.workspace = true -strum_macros = "0.25.2" -allo-isolate = { version = "^0.1", features = ["catch-unwind"] } -collab-importer = { workspace = true } -uuid.workspace = true - -[dev-dependencies] -tokio = { workspace = true, features = ["full"] } -uuid = "1.6.1" -rand = { version = "0.8", features = ["std_rng"] } - -[features] -dart = ["flowy-codegen/dart", "flowy-notification/dart"] - -[build-dependencies] -flowy-codegen.workspace = true \ No newline at end of file +fxhash = "0.2.1" \ No newline at end of file diff --git a/frontend/rust-lib/flowy-storage/Flowy.toml b/frontend/rust-lib/flowy-storage/Flowy.toml deleted file mode 100644 index b0097e98d2..0000000000 --- a/frontend/rust-lib/flowy-storage/Flowy.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Check out the FlowyConfig (located in flowy_toml.rs) for more details. -proto_input = ["src/entities.rs", "src/event_map.rs", "src/notification.rs"] -event_files = ["src/event_map.rs"] diff --git a/frontend/rust-lib/flowy-storage/build.rs b/frontend/rust-lib/flowy-storage/build.rs deleted file mode 100644 index 77c0c8125b..0000000000 --- a/frontend/rust-lib/flowy-storage/build.rs +++ /dev/null @@ -1,7 +0,0 @@ -fn main() { - #[cfg(feature = "dart")] - { - flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); - flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); - } -} diff --git a/frontend/rust-lib/flowy-storage/src/entities.rs b/frontend/rust-lib/flowy-storage/src/entities.rs deleted file mode 100644 index 1decf5bc66..0000000000 --- a/frontend/rust-lib/flowy-storage/src/entities.rs +++ /dev/null @@ -1,22 +0,0 @@ -use flowy_derive::ProtoBuf; - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct RegisterStreamPB { - #[pb(index = 1)] - pub port: i64, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct QueryFilePB { - #[pb(index = 1)] - pub url: String, -} - -#[derive(Default, ProtoBuf, Clone, Debug)] -pub struct FileStatePB { - #[pb(index = 1)] - pub file_id: String, - - #[pb(index = 2)] - pub is_finish: bool, -} diff --git a/frontend/rust-lib/flowy-storage/src/event_handler.rs b/frontend/rust-lib/flowy-storage/src/event_handler.rs deleted file mode 100644 index 8b34918e6b..0000000000 --- a/frontend/rust-lib/flowy-storage/src/event_handler.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::entities::{FileStatePB, QueryFilePB, RegisterStreamPB}; -use crate::manager::StorageManager; -use flowy_error::{FlowyError, FlowyResult}; -use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; -use std::sync::{Arc, Weak}; - -fn upgrade_storage_manager( - ai_manager: AFPluginState<Weak<StorageManager>>, -) -> FlowyResult<Arc<StorageManager>> { - let manager = ai_manager - .upgrade() - .ok_or(FlowyError::internal().with_context("The storage manager is already dropped"))?; - Ok(manager) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn register_stream_handler( - data: AFPluginData<RegisterStreamPB>, - storage_manager: AFPluginState<Weak<StorageManager>>, -) -> Result<(), FlowyError> { - let manager = upgrade_storage_manager(storage_manager)?; - let data = data.into_inner(); - manager.register_file_progress_stream(data.port).await; - Ok(()) -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub(crate) async fn query_file_handler( - data: AFPluginData<QueryFilePB>, - storage_manager: AFPluginState<Weak<StorageManager>>, -) -> DataResult<FileStatePB, FlowyError> { - let manager = upgrade_storage_manager(storage_manager)?; - let data = data.into_inner(); - let pb = manager.query_file_state(&data.url).await.ok_or_else(|| { - FlowyError::record_not_found().with_context(format!("File not found: {}", data.url)) - })?; - data_result_ok(pb) -} diff --git a/frontend/rust-lib/flowy-storage/src/event_map.rs b/frontend/rust-lib/flowy-storage/src/event_map.rs deleted file mode 100644 index 0509f26884..0000000000 --- a/frontend/rust-lib/flowy-storage/src/event_map.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::event_handler::{query_file_handler, register_stream_handler}; -use crate::manager::StorageManager; -use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; -use lib_dispatch::prelude::*; -use std::sync::Weak; -use strum_macros::Display; - -pub fn init(manager: Weak<StorageManager>) -> AFPlugin { - AFPlugin::new() - .name("file-storage") - .state(manager) - .event(FileStorageEvent::RegisterStream, register_stream_handler) - .event(FileStorageEvent::QueryFile, query_file_handler) -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] -#[event_err = "FlowyError"] -pub enum FileStorageEvent { - /// Create a new workspace - #[event(input = "RegisterStreamPB")] - RegisterStream = 0, - - #[event(input = "QueryFilePB", output = "FileStatePB")] - QueryFile = 1, -} diff --git a/frontend/rust-lib/flowy-storage/src/file_cache.rs b/frontend/rust-lib/flowy-storage/src/file_cache.rs deleted file mode 100644 index 2a71c5f72f..0000000000 --- a/frontend/rust-lib/flowy-storage/src/file_cache.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::path::{Path, PathBuf}; -use tokio::fs::{self, File}; -use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; -use tracing::error; - -/// [FileTempStorage] is used to store the temporary files for uploading. After the file is uploaded, -/// the file will be deleted. -pub struct FileTempStorage { - storage_dir: PathBuf, -} - -impl FileTempStorage { - /// Creates a new `FileTempStorage` with the specified temporary directory. - pub fn new(storage_dir: PathBuf) -> Self { - if !storage_dir.exists() { - if let Err(err) = std::fs::create_dir_all(&storage_dir) { - error!("Failed to create temporary storage directory: {:?}", err); - } - } - - FileTempStorage { storage_dir } - } - - /// Generates a temporary file path using the given file name. - fn generate_temp_file_path_with_name(&self, file_name: &str) -> PathBuf { - self.storage_dir.join(file_name) - } - - /// Creates a temporary file from an existing local file path. - #[allow(dead_code)] - pub async fn create_temp_file_from_existing( - &self, - existing_file_path: &Path, - ) -> io::Result<String> { - let file_name = existing_file_path - .file_name() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid file name"))? - .to_str() - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid file name"))?; - - let temp_file_path = self.generate_temp_file_path_with_name(file_name); - fs::copy(existing_file_path, &temp_file_path).await?; - Ok( - temp_file_path - .to_str() - .ok_or(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid file path", - ))? - .to_owned(), - ) - } - - /// Creates a temporary file from bytes and a specified file name. - #[allow(dead_code)] - pub async fn create_temp_file_from_bytes( - &self, - file_name: &str, - data: &[u8], - ) -> io::Result<PathBuf> { - let temp_file_path = self.generate_temp_file_path_with_name(file_name); - let mut file = File::create(&temp_file_path).await?; - file.write_all(data).await?; - Ok(temp_file_path) - } - - /// Writes data to the specified temporary file. - #[allow(dead_code)] - pub async fn write_to_temp_file(&self, file_path: &Path, data: &[u8]) -> io::Result<()> { - let mut file = File::create(file_path).await?; - file.write_all(data).await?; - Ok(()) - } - - /// Reads data from the specified temporary file. - #[allow(dead_code)] - pub async fn read_from_temp_file(&self, file_path: &Path) -> io::Result<Vec<u8>> { - let mut file = File::open(file_path).await?; - let mut data = Vec::new(); - file.read_to_end(&mut data).await?; - Ok(data) - } - - /// Deletes the specified temporary file. - pub async fn delete_temp_file<T: AsRef<Path>>(&self, file_path: T) -> io::Result<()> { - fs::remove_file(file_path).await?; - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-storage/src/lib.rs b/frontend/rust-lib/flowy-storage/src/lib.rs index ca0b5d05ce..b318b55cb5 100644 --- a/frontend/rust-lib/flowy-storage/src/lib.rs +++ b/frontend/rust-lib/flowy-storage/src/lib.rs @@ -1,9 +1,154 @@ -mod entities; -mod event_handler; -pub mod event_map; -mod file_cache; -pub mod manager; -mod notification; -mod protobuf; -pub mod sqlite_sql; -mod uploader; +if_native! { + mod native; + pub use native::*; +} + +if_wasm! { + mod wasm; + pub use wasm::*; +} + +use bytes::Bytes; + +use flowy_error::FlowyError; +use lib_infra::future::FutureResult; +use lib_infra::{conditional_send_sync_trait, if_native, if_wasm}; +use mime::Mime; + +pub struct ObjectIdentity { + pub workspace_id: String, + pub file_id: String, + pub ext: String, +} + +#[derive(Clone)] +pub struct ObjectValue { + pub raw: Bytes, + pub mime: Mime, +} +conditional_send_sync_trait! { + "Provides a service for object storage. The trait includes methods for CRUD operations on storage objects."; + ObjectStorageService { + /// Creates a new storage object. + /// + /// # Parameters + /// - `url`: url of the object to be created. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult<String, FlowyError>; + + /// Creates a new storage object. + /// + /// # Parameters + /// - `url`: url of the object to be created. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError>; + + /// Deletes a storage object by its URL. + /// + /// # Parameters + /// - `url`: url of the object to be deleted. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError>; + + /// Fetches a storage object by its URL. + /// + /// # Parameters + /// - `url`: url of the object + /// + /// # Returns + /// - `Ok(File)`: The returned file object. + /// - `Err(Error)`: An error occurred during the operation. + fn get_object(&self, url: String) -> FutureResult<ObjectValue, FlowyError>; + } +} + +pub trait FileStoragePlan: Send + Sync + 'static { + fn storage_size(&self) -> FutureResult<u64, FlowyError>; + fn maximum_file_size(&self) -> FutureResult<u64, FlowyError>; + + fn check_upload_object(&self, object: &StorageObject) -> FutureResult<(), FlowyError>; +} + +pub struct StorageObject { + pub workspace_id: String, + pub file_name: String, + pub value: ObjectValueSupabase, +} + +pub enum ObjectValueSupabase { + File { file_path: String }, + Bytes { bytes: Bytes, mime: String }, +} + +impl ObjectValueSupabase { + pub fn mime_type(&self) -> String { + match self { + ObjectValueSupabase::File { file_path } => mime_guess::from_path(file_path) + .first_or_octet_stream() + .to_string(), + ObjectValueSupabase::Bytes { mime, .. } => mime.clone(), + } + } +} + +impl StorageObject { + /// Creates a `StorageObject` from a file. + /// + /// # Parameters + /// + /// * `name`: The name of the storage object. + /// * `file_path`: The file path to the storage object's data. + /// + pub fn from_file<T: ToString>(workspace_id: &str, file_name: &str, file_path: T) -> Self { + Self { + workspace_id: workspace_id.to_string(), + file_name: file_name.to_string(), + value: ObjectValueSupabase::File { + file_path: file_path.to_string(), + }, + } + } + + /// Creates a `StorageObject` from bytes. + /// + /// # Parameters + /// + /// * `name`: The name of the storage object. + /// * `bytes`: The byte data of the storage object. + /// * `mime`: The MIME type of the storage object. + /// + pub fn from_bytes<B: Into<Bytes>>( + workspace_id: &str, + file_name: &str, + bytes: B, + mime: String, + ) -> Self { + let bytes = bytes.into(); + Self { + workspace_id: workspace_id.to_string(), + file_name: file_name.to_string(), + value: ObjectValueSupabase::Bytes { bytes, mime }, + } + } + + /// Gets the file size of the `StorageObject`. + /// + /// # Returns + /// + /// The file size in bytes. + pub fn file_size(&self) -> u64 { + match &self.value { + ObjectValueSupabase::File { file_path } => std::fs::metadata(file_path).unwrap().len(), + ObjectValueSupabase::Bytes { bytes, .. } => bytes.len() as u64, + } + } +} diff --git a/frontend/rust-lib/flowy-storage/src/manager.rs b/frontend/rust-lib/flowy-storage/src/manager.rs deleted file mode 100644 index 0dd729b087..0000000000 --- a/frontend/rust-lib/flowy-storage/src/manager.rs +++ /dev/null @@ -1,895 +0,0 @@ -use crate::entities::FileStatePB; -use crate::file_cache::FileTempStorage; -use crate::notification::{make_notification, StorageNotification}; -use crate::sqlite_sql::{ - batch_select_upload_file, delete_all_upload_parts, delete_upload_file, - delete_upload_file_by_file_id, insert_upload_file, insert_upload_part, is_upload_completed, - is_upload_exist, select_upload_file, select_upload_parts, update_upload_file_completed, - update_upload_file_upload_id, UploadFilePartTable, UploadFileTable, -}; -use crate::uploader::{FileUploader, FileUploaderRunner, Signal, UploadTask, UploadTaskQueue}; -use allo_isolate::Isolate; -use async_trait::async_trait; -use collab_importer::util::FileId; -use dashmap::DashMap; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::DBConnection; -use flowy_storage_pub::chunked_byte::{calculate_offsets, ChunkedBytes, MIN_CHUNK_SIZE}; -use flowy_storage_pub::cloud::StorageCloudService; -use flowy_storage_pub::storage::{ - CompletedPartRequest, CreatedUpload, FileProgress, FileProgressReceiver, FileUploadState, - ProgressNotifier, StorageService, UploadPartResponse, -}; -use lib_infra::box_any::BoxAny; -use lib_infra::isolate_stream::{IsolateSink, SinkExt}; -use lib_infra::util::timestamp; -use std::path::{Path, PathBuf}; -use std::str::FromStr; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use tokio::io::AsyncWriteExt; -use tokio::sync::{broadcast, watch}; -use tracing::{debug, error, info, instrument, trace}; -use uuid::Uuid; - -pub trait StorageUserService: Send + Sync + 'static { - fn user_id(&self) -> Result<i64, FlowyError>; - fn workspace_id(&self) -> Result<Uuid, FlowyError>; - fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>; - fn get_application_root_dir(&self) -> &str; -} - -type GlobalNotifier = broadcast::Sender<FileProgress>; -pub struct StorageManager { - pub storage_service: Arc<dyn StorageService>, - cloud_service: Arc<dyn StorageCloudService>, - user_service: Arc<dyn StorageUserService>, - uploader: Arc<FileUploader>, - progress_notifiers: Arc<DashMap<String, ProgressNotifier>>, - global_notifier: GlobalNotifier, -} - -impl Drop for StorageManager { - fn drop(&mut self) { - info!("[File] StorageManager is dropped"); - } -} - -impl StorageManager { - pub fn new( - cloud_service: Arc<dyn StorageCloudService>, - user_service: Arc<dyn StorageUserService>, - ) -> Self { - let is_exceed_storage_limit = Arc::new(AtomicBool::new(false)); - let temp_storage_path = PathBuf::from(format!( - "{}/cache_files", - user_service.get_application_root_dir() - )); - let (global_notifier, _) = broadcast::channel(2000); - let temp_storage = Arc::new(FileTempStorage::new(temp_storage_path)); - let (notifier, notifier_rx) = watch::channel(Signal::Proceed); - let task_queue = Arc::new(UploadTaskQueue::new(notifier)); - let progress_notifiers = Arc::new(DashMap::new()); - let storage_service = Arc::new(StorageServiceImpl { - cloud_service: cloud_service.clone(), - user_service: user_service.clone(), - temp_storage, - task_queue: task_queue.clone(), - is_exceed_storage_limit: is_exceed_storage_limit.clone(), - progress_notifiers: progress_notifiers.clone(), - global_notifier: global_notifier.clone(), - }); - - let uploader = Arc::new(FileUploader::new( - storage_service.clone(), - task_queue, - is_exceed_storage_limit, - )); - tokio::spawn(FileUploaderRunner::run( - Arc::downgrade(&uploader), - notifier_rx, - )); - - let weak_uploader = Arc::downgrade(&uploader); - let cloned_user_service = user_service.clone(); - tokio::spawn(async move { - if let Some(uploader) = weak_uploader.upgrade() { - if let Err(err) = prepare_upload_task(uploader, cloned_user_service).await { - error!("prepare upload task failed: {}", err); - } - } - }); - - let mut rx = global_notifier.subscribe(); - let weak_notifier = Arc::downgrade(&progress_notifiers); - tokio::spawn(async move { - while let Ok(progress) = rx.recv().await { - if let Some(notifiers) = weak_notifier.upgrade() { - if let Some(mut notifier) = notifiers.get_mut(&progress.file_id) { - if progress.progress >= 1.0 { - let finish = FileUploadState::Finished { - file_id: progress.file_id, - }; - notifier.notify(finish).await; - } else { - let progress = FileUploadState::Uploading { - progress: progress.progress, - }; - notifier.notify(progress).await; - } - } - } else { - info!("progress notifiers is dropped"); - break; - } - } - }); - - Self { - storage_service, - cloud_service, - user_service, - uploader, - progress_notifiers, - global_notifier, - } - } - - pub async fn register_file_progress_stream(&self, port: i64) { - info!("register file progress stream: {}", port); - let mut sink = IsolateSink::new(Isolate::new(port)); - let mut rx = self.global_notifier.subscribe(); - tokio::spawn(async move { - while let Ok(progress) = rx.recv().await { - if let Ok(s) = serde_json::to_string(&progress) { - if let Err(err) = sink.send(s).await { - error!("[File]: send file progress failed: {}", err); - } - } - } - }); - } - - pub async fn query_file_state(&self, url: &str) -> Option<FileStatePB> { - let (workspace_id, parent_dir, file_id) = self.cloud_service.parse_object_url_v1(url).await?; - let current_workspace_id = self.user_service.workspace_id().ok()?; - if workspace_id != current_workspace_id { - return None; - } - - let uid = self.user_service.user_id().ok()?; - let mut conn = self.user_service.sqlite_connection(uid).ok()?; - let is_finish = - is_upload_completed(&mut conn, &workspace_id.to_string(), &parent_dir, &file_id).ok()?; - - if let Err(err) = self.global_notifier.send(FileProgress::new_progress( - url.to_string(), - file_id.clone(), - if is_finish { 1.0 } else { 0.0 }, - )) { - error!("[File] send global notifier failed: {}", err); - } - - Some(FileStatePB { file_id, is_finish }) - } - - pub async fn initialize(&self, workspace_id: &str) { - self.enable_storage_write_access(); - - if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { - error!("prepare {} upload task failed: {}", workspace_id, err); - } - } - - pub async fn initialize_after_open_workspace(&self, workspace_id: &Uuid) { - self.enable_storage_write_access(); - - if let Err(err) = prepare_upload_task(self.uploader.clone(), self.user_service.clone()).await { - error!("prepare {} upload task failed: {}", workspace_id, err); - } - } - - pub fn update_network_reachable(&self, reachable: bool) { - if reachable { - self.uploader.resume(); - } else { - self.uploader.pause(); - } - } - - pub fn disable_storage_write_access(&self) { - // when storage is purchased, resume the uploader - self.uploader.disable_storage_write(); - } - - pub fn enable_storage_write_access(&self) { - // when storage is purchased, resume the uploader - self.uploader.enable_storage_write(); - } - - pub async fn subscribe_file_state( - &self, - parent_dir: &str, - file_id: &str, - ) -> Result<Option<FileProgressReceiver>, FlowyError> { - self - .storage_service - .subscribe_file_progress(parent_dir, file_id) - .await - } - - /// Returns None if the file with given file_id is not exist - /// When delete a file, the progress notifier for given file_id will be deleted too - pub async fn get_file_state(&self, file_id: &str) -> Option<FileUploadState> { - self - .progress_notifiers - .get(file_id) - .and_then(|notifier| notifier.value().current_value.clone()) - } - - pub async fn get_all_tasks(&self) -> FlowyResult<Vec<UploadTask>> { - let tasks = self.uploader.all_tasks().await; - Ok(tasks) - } -} - -async fn prepare_upload_task( - uploader: Arc<FileUploader>, - user_service: Arc<dyn StorageUserService>, -) -> FlowyResult<()> { - if let Ok(uid) = user_service.user_id() { - let workspace_id = user_service.workspace_id()?; - let conn = user_service.sqlite_connection(uid)?; - let upload_files = batch_select_upload_file(conn, &workspace_id.to_string(), 100, false)?; - let tasks = upload_files - .into_iter() - .map(|upload_file| UploadTask::BackgroundTask { - workspace_id: upload_file.workspace_id, - file_id: upload_file.file_id, - parent_dir: upload_file.parent_dir, - created_at: upload_file.created_at, - retry_count: 0, - }) - .collect::<Vec<_>>(); - info!("[File] prepare upload task: {}", tasks.len()); - uploader.queue_tasks(tasks).await; - } - Ok(()) -} - -pub struct StorageServiceImpl { - cloud_service: Arc<dyn StorageCloudService>, - user_service: Arc<dyn StorageUserService>, - temp_storage: Arc<FileTempStorage>, - task_queue: Arc<UploadTaskQueue>, - is_exceed_storage_limit: Arc<AtomicBool>, - progress_notifiers: Arc<DashMap<String, ProgressNotifier>>, - global_notifier: GlobalNotifier, -} - -#[async_trait] -impl StorageService for StorageServiceImpl { - async fn delete_object(&self, url: String) -> FlowyResult<()> { - if let Some((workspace_id, parent_dir, file_id)) = - self.cloud_service.parse_object_url_v1(&url).await - { - info!( - "[File] delete object: workspace: {}, parent_dir: {}, file_id: {}", - workspace_id, parent_dir, file_id - ); - - self - .task_queue - .remove_task(&workspace_id.to_string(), &parent_dir, &file_id) - .await; - - trace!("[File] delete progress notifier: {}", file_id); - self.progress_notifiers.remove(&file_id); - match delete_upload_file_by_file_id( - self - .user_service - .sqlite_connection(self.user_service.user_id()?)?, - &workspace_id.to_string(), - &parent_dir, - &file_id, - ) { - Ok(Some(file)) => { - let file_path = file.local_file_path; - match tokio::fs::remove_file(&file_path).await { - Ok(_) => debug!("[File] deleted file from local disk: {}", file_path), - Err(err) => { - error!("[File] delete file at {} failed: {}", file_path, err); - }, - } - }, - Ok(None) => { - info!( - "[File]: can not find file record for url: {} when delete", - url - ); - }, - Err(err) => { - error!("[File] delete upload file failed: {}", err); - }, - } - } - - let _ = self.cloud_service.delete_object(&url).await; - Ok(()) - } - - fn download_object(&self, url: String, local_file_path: String) -> FlowyResult<()> { - let cloud_service = self.cloud_service.clone(); - tokio::spawn(async move { - if tokio::fs::metadata(&local_file_path).await.is_ok() { - tracing::warn!("file already exist in user local disk: {}", local_file_path); - return Ok(()); - } - let object_value = cloud_service.get_object(url).await?; - let mut file = tokio::fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&local_file_path) - .await?; - - match file.write(&object_value.raw).await { - Ok(n) => { - info!("downloaded {} bytes to file: {}", n, local_file_path); - }, - Err(err) => { - error!("write file failed: {}", err); - }, - } - Ok::<_, FlowyError>(()) - }); - Ok(()) - } - - async fn create_upload( - &self, - workspace_id: &str, - parent_dir: &str, - file_path: &str, - ) -> Result<(CreatedUpload, Option<FileProgressReceiver>), FlowyError> { - if workspace_id.is_empty() { - return Err(FlowyError::internal().with_context("workspace id is empty")); - } - - if parent_dir.is_empty() { - return Err(FlowyError::internal().with_context("parent dir is empty")); - } - - if file_path.is_empty() { - return Err(FlowyError::internal().with_context("local file path is empty")); - } - - let workspace_id = workspace_id.to_string(); - let parent_dir = parent_dir.to_string(); - let file_path = file_path.to_string(); - - let is_exceed_limit = self - .is_exceed_storage_limit - .load(std::sync::atomic::Ordering::Relaxed); - if is_exceed_limit { - make_notification(StorageNotification::FileStorageLimitExceeded) - .payload(FlowyError::file_storage_limit()) - .send(); - - return Err(FlowyError::file_storage_limit()); - } - - let local_file_path = self - .temp_storage - .create_temp_file_from_existing(Path::new(&file_path)) - .await - .map_err(|err| { - error!("[File] create temp file failed: {}", err); - FlowyError::internal() - .with_context(format!("create temp file for upload file failed: {}", err)) - })?; - - // 1. create a file record and chunk the file - let record = create_upload_record(workspace_id, parent_dir, local_file_path.clone()).await?; - // 2. save the record to sqlite - let conn = self - .user_service - .sqlite_connection(self.user_service.user_id()?)?; - let workspace_id = Uuid::from_str(&record.workspace_id)?; - let url = self - .cloud_service - .get_object_url_v1(&workspace_id, &record.parent_dir, &record.file_id) - .await?; - let file_id = record.file_id.clone(); - match insert_upload_file(conn, &record) { - Ok(_) => { - // 3. generate url for given file - self - .task_queue - .queue_task(UploadTask::Task { - local_file_path, - record, - retry_count: 3, - }) - .await; - - let notifier = ProgressNotifier::new(file_id.to_string()); - let receiver = notifier.subscribe(); - trace!("[File] create upload progress notifier: {}", file_id); - self - .progress_notifiers - .insert(file_id.to_string(), notifier); - Ok::<_, FlowyError>((CreatedUpload { url, file_id }, Some(receiver))) - }, - Err(err) => { - if matches!(err.code, ErrorCode::DuplicateSqliteRecord) { - info!("[File] upload record already exists, skip creating new upload task"); - Ok::<_, FlowyError>((CreatedUpload { url, file_id }, None)) - } else { - Err(err) - } - }, - } - } - - async fn start_upload(&self, record: &BoxAny) -> Result<(), FlowyError> { - let file_record = record.downcast_ref::<UploadFileTable>().ok_or_else(|| { - FlowyError::internal().with_context("failed to downcast record to UploadFileTable") - })?; - - // If the file is already uploaded, skip the upload process - if !is_upload_exist( - self - .user_service - .sqlite_connection(self.user_service.user_id()?)?, - &file_record.upload_id, - )? { - info!( - "[File] skip upload, {} was deleted", - file_record.local_file_path - ); - return Ok(()); - } - - start_upload(self, file_record).await?; - - Ok(()) - } - - async fn resume_upload( - &self, - workspace_id: &str, - parent_dir: &str, - file_id: &str, - ) -> Result<(), FlowyError> { - // Gathering the upload record and parts from the sqlite database. - let mut conn = self - .user_service - .sqlite_connection(self.user_service.user_id()?)?; - - if let Some(upload_file) = select_upload_file(&mut conn, workspace_id, parent_dir, file_id)? { - resume_upload(self, upload_file).await?; - } else { - error!( - "[File] resume upload failed: can not found {}:{}", - parent_dir, file_id - ); - } - Ok(()) - } - - async fn subscribe_file_progress( - &self, - parent_idr: &str, - file_id: &str, - ) -> Result<Option<FileProgressReceiver>, FlowyError> { - trace!("[File]: subscribe file progress: {}", file_id); - - let is_completed = { - let mut conn = self - .user_service - .sqlite_connection(self.user_service.user_id()?)?; - let workspace_id = self.user_service.workspace_id()?; - is_upload_completed(&mut conn, &workspace_id.to_string(), parent_idr, file_id) - .unwrap_or(false) - }; - - if is_completed { - return Ok(None); - } - - let notifier = self - .progress_notifiers - .entry(file_id.to_string()) - .or_insert_with(|| ProgressNotifier::new(file_id.to_string())); - Ok(Some(notifier.subscribe())) - } -} - -async fn create_upload_record( - workspace_id: String, - parent_dir: String, - local_file_path: String, -) -> FlowyResult<UploadFileTable> { - let file_path = Path::new(&local_file_path); - let file = tokio::fs::File::open(&file_path).await?; - let metadata = file.metadata().await?; - let file_size = metadata.len() as usize; - - // Calculate the total number of chunks - let num_chunk = calculate_offsets(file_size, MIN_CHUNK_SIZE).len(); - let content_type = mime_guess::from_path(file_path) - .first_or_octet_stream() - .to_string(); - let file_id = FileId::from_path(&file_path.to_path_buf()).await?; - let record = UploadFileTable { - workspace_id, - file_id, - // When the upload_id is empty string, we will create a new upload using [Self::start_upload] method - upload_id: "".to_string(), - parent_dir, - local_file_path, - content_type, - chunk_size: MIN_CHUNK_SIZE as i32, - num_chunk: num_chunk as i32, - created_at: timestamp(), - is_finish: false, - }; - Ok(record) -} - -#[instrument(level = "debug", skip_all, err)] -async fn start_upload( - storage_service: &StorageServiceImpl, - upload_file: &UploadFileTable, -) -> FlowyResult<()> { - let temp_storage = &storage_service.temp_storage; - let user_service = &storage_service.user_service; - let global_notifier = storage_service.global_notifier.clone(); - let cloud_service = &storage_service.cloud_service; - - // 4. gather existing completed parts - let mut conn = user_service.sqlite_connection(user_service.user_id()?)?; - let mut completed_parts = select_upload_parts(&mut conn, &upload_file.upload_id) - .unwrap_or_default() - .into_iter() - .map(|part| CompletedPartRequest { - e_tag: part.e_tag, - part_number: part.part_num, - }) - .collect::<Vec<_>>(); - let upload_offset = completed_parts.len() as u64; - - let file_path = Path::new(&upload_file.local_file_path); - if !file_path.exists() { - error!("[File] file not found: {}", upload_file.local_file_path); - if let Ok(uid) = user_service.user_id() { - if let Ok(conn) = user_service.sqlite_connection(uid) { - delete_upload_file(conn, &upload_file.upload_id)?; - } - } - } - let file_size = file_path - .metadata() - .map(|metadata| metadata.len()) - .unwrap_or(0); - - let mut chunked_bytes = - ChunkedBytes::from_file(&upload_file.local_file_path, MIN_CHUNK_SIZE).await?; - let total_parts = chunked_bytes.total_chunks(); - if let Err(err) = chunked_bytes.set_offset(upload_offset).await { - error!( - "[File] set offset failed: {} for file: {}", - err, upload_file.local_file_path - ); - if let Ok(uid) = user_service.user_id() { - if let Ok(conn) = user_service.sqlite_connection(uid) { - delete_upload_file(conn, &upload_file.upload_id)?; - } - } - } - - info!( - "[File] start upload: workspace: {}, parent_dir: {}, file_id: {}, chunk: {}", - upload_file.workspace_id, upload_file.parent_dir, upload_file.file_id, chunked_bytes, - ); - - let mut upload_file = upload_file.clone(); - // 1. create upload - trace!( - "[File] create upload for workspace: {}, parent_dir: {}, file_id: {}", - upload_file.workspace_id, - upload_file.parent_dir, - upload_file.file_id - ); - - let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; - let create_upload_resp_result = cloud_service - .create_upload( - &workspace_id, - &upload_file.parent_dir, - &upload_file.file_id, - &upload_file.content_type, - file_size, - ) - .await; - - let file_url = cloud_service - .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) - .await?; - - if let Err(err) = create_upload_resp_result.as_ref() { - handle_upload_error(storage_service, err, &file_url).await; - } - let create_upload_resp = create_upload_resp_result?; - - // 2. update upload_id - let conn = user_service.sqlite_connection(user_service.user_id()?)?; - update_upload_file_upload_id( - conn, - &upload_file.workspace_id, - &upload_file.parent_dir, - &upload_file.file_id, - &create_upload_resp.upload_id, - )?; - - trace!( - "[File] {} update upload_id: {}", - upload_file.file_id, - create_upload_resp.upload_id - ); - upload_file.upload_id = create_upload_resp.upload_id; - - // 3. start uploading parts - info!( - "[File] {} start uploading parts:{}, offset:{}", - upload_file.file_id, - chunked_bytes.total_chunks(), - upload_offset, - ); - - let mut part_number = upload_offset + 1; - while let Some(chunk_result) = chunked_bytes.next_chunk().await { - match chunk_result { - Ok(chunk_bytes) => { - info!( - "[File] {} uploading {}th part, size:{}KB", - upload_file.file_id, - part_number, - chunk_bytes.len() / 1000, - ); - - // start uploading parts - match upload_part( - cloud_service, - user_service, - &workspace_id, - &upload_file.parent_dir, - &upload_file.upload_id, - &upload_file.file_id, - part_number as i32, - chunk_bytes.to_vec(), - ) - .await - { - Ok(resp) => { - trace!( - "[File] {} part {} uploaded", - upload_file.file_id, - part_number - ); - let mut progress_value = (part_number as f64 / total_parts as f64).clamp(0.0, 1.0); - // The 0.1 is reserved for the complete_upload progress - if progress_value >= 0.9 { - progress_value = 0.9; - } - let progress = FileProgress::new_progress( - file_url.clone(), - upload_file.file_id.clone(), - progress_value, - ); - trace!("[File] upload progress: {}", progress); - - if let Err(err) = global_notifier.send(progress) { - error!("[File] send global notifier failed: {}", err); - } - - // gather completed part - completed_parts.push(CompletedPartRequest { - e_tag: resp.e_tag, - part_number: resp.part_num, - }); - }, - Err(err) => { - error!( - "[File] {} failed to upload part: {}", - upload_file.file_id, err - ); - handle_upload_error(storage_service, &err, &file_url).await; - if let Err(err) = global_notifier.send(FileProgress::new_error( - file_url.clone(), - upload_file.file_id.clone(), - err.msg.clone(), - )) { - error!("[File] send global notifier failed: {}", err); - } - return Err(err); - }, - } - part_number += 1; // Increment part number - }, - Err(e) => { - error!( - "[File] {} failed to read chunk: {:?}", - upload_file.file_id, e - ); - break; - }, - } - } - - // mark it as completed - let complete_upload_result = complete_upload( - cloud_service, - user_service, - temp_storage, - &upload_file, - completed_parts, - &global_notifier, - ) - .await; - if let Err(err) = complete_upload_result { - handle_upload_error(storage_service, &err, &file_url).await; - return Err(err); - } - - Ok(()) -} - -async fn handle_upload_error( - storage_service: &StorageServiceImpl, - err: &FlowyError, - file_url: &str, -) { - if err.is_file_limit_exceeded() { - make_notification(StorageNotification::FileStorageLimitExceeded) - .payload(err.clone()) - .send(); - } - - if err.is_single_file_limit_exceeded() { - info!("[File] file exceed limit:{}", file_url); - if let Err(err) = storage_service.delete_object(file_url.to_string()).await { - error!("[File] delete upload file:{} error:{}", file_url, err); - } - - make_notification(StorageNotification::SingleFileLimitExceeded) - .payload(err.clone()) - .send(); - } -} - -#[instrument(level = "debug", skip_all, err)] -async fn resume_upload( - storage_service: &StorageServiceImpl, - upload_file: UploadFileTable, -) -> FlowyResult<()> { - trace!( - "[File] resume upload for workspace: {}, parent_dir: {}, file_id: {}, local_file_path:{}", - upload_file.workspace_id, - upload_file.parent_dir, - upload_file.file_id, - upload_file.local_file_path - ); - - start_upload(storage_service, &upload_file).await?; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -#[instrument(level = "debug", skip_all)] -async fn upload_part( - cloud_service: &Arc<dyn StorageCloudService>, - user_service: &Arc<dyn StorageUserService>, - workspace_id: &Uuid, - parent_dir: &str, - upload_id: &str, - file_id: &str, - part_number: i32, - body: Vec<u8>, -) -> Result<UploadPartResponse, FlowyError> { - let resp = cloud_service - .upload_part( - workspace_id, - parent_dir, - upload_id, - file_id, - part_number, - body, - ) - .await?; - - // save uploaded part to sqlite - let conn = user_service.sqlite_connection(user_service.user_id()?)?; - insert_upload_part( - conn, - &UploadFilePartTable { - upload_id: upload_id.to_string(), - e_tag: resp.e_tag.clone(), - part_num: resp.part_num, - }, - )?; - - Ok(resp) -} - -async fn complete_upload( - cloud_service: &Arc<dyn StorageCloudService>, - user_service: &Arc<dyn StorageUserService>, - temp_storage: &Arc<FileTempStorage>, - upload_file: &UploadFileTable, - parts: Vec<CompletedPartRequest>, - global_notifier: &GlobalNotifier, -) -> Result<(), FlowyError> { - let workspace_id = Uuid::from_str(&upload_file.workspace_id)?; - let file_url = cloud_service - .get_object_url_v1(&workspace_id, &upload_file.parent_dir, &upload_file.file_id) - .await?; - - info!( - "[File]: completing file upload: {}, num parts: {}, url:{}", - upload_file.file_id, - parts.len(), - file_url - ); - match cloud_service - .complete_upload( - &workspace_id, - &upload_file.parent_dir, - &upload_file.upload_id, - &upload_file.file_id, - parts, - ) - .await - { - Ok(_) => { - info!("[File] completed upload file: {}", upload_file.file_id); - let progress = FileProgress::new_progress(file_url, upload_file.file_id.clone(), 1.0); - info!( - "[File]: notify upload progress:{}, {}", - upload_file.file_id, progress - ); - - if let Err(err) = global_notifier.send(progress) { - error!("[File] send global notifier failed: {}", err); - } - - let conn = user_service.sqlite_connection(user_service.user_id()?)?; - update_upload_file_completed(conn, &upload_file.upload_id)?; - - if let Err(err) = temp_storage - .delete_temp_file(&upload_file.local_file_path) - .await - { - trace!("[File] delete temp file failed: {}", err); - } - }, - Err(err) => { - error!("[File] complete upload failed: {}", err); - - let progress = - FileProgress::new_error(file_url, upload_file.file_id.clone(), err.msg.clone()); - if let Err(send_err) = global_notifier.send(progress) { - error!("[File] send global notifier failed: {}", send_err); - } - - let mut conn = user_service.sqlite_connection(user_service.user_id()?)?; - if let Err(err) = delete_all_upload_parts(&mut conn, &upload_file.upload_id) { - error!("[File] delete all upload parts failed: {}", err); - } - return Err(err); - }, - } - Ok(()) -} diff --git a/frontend/rust-lib/flowy-storage/src/native/mod.rs b/frontend/rust-lib/flowy-storage/src/native/mod.rs new file mode 100644 index 0000000000..777a8b08dc --- /dev/null +++ b/frontend/rust-lib/flowy-storage/src/native/mod.rs @@ -0,0 +1,34 @@ +use crate::{ObjectIdentity, ObjectValue}; +use flowy_error::FlowyError; +use std::path::Path; +use tokio::io::AsyncReadExt; +use tracing::info; + +pub async fn object_from_disk( + workspace_id: &str, + local_file_path: &str, +) -> Result<(ObjectIdentity, ObjectValue), FlowyError> { + let ext = Path::new(local_file_path) + .extension() + .and_then(std::ffi::OsStr::to_str) + .unwrap_or("") + .to_owned(); + let mut file = tokio::fs::File::open(local_file_path).await?; + let mut content = Vec::new(); + let n = file.read_to_end(&mut content).await?; + info!("read {} bytes from file: {}", n, local_file_path); + let mime = mime_guess::from_path(local_file_path).first_or_octet_stream(); + let hash = fxhash::hash(&content); + + Ok(( + ObjectIdentity { + workspace_id: workspace_id.to_owned(), + file_id: hash.to_string(), + ext, + }, + ObjectValue { + raw: content.into(), + mime, + }, + )) +} diff --git a/frontend/rust-lib/flowy-storage/src/notification.rs b/frontend/rust-lib/flowy-storage/src/notification.rs deleted file mode 100644 index 86af2d222c..0000000000 --- a/frontend/rust-lib/flowy-storage/src/notification.rs +++ /dev/null @@ -1,23 +0,0 @@ -use flowy_derive::ProtoBuf_Enum; -use flowy_notification::NotificationBuilder; - -const OBSERVABLE_SOURCE: &str = "storage"; - -#[derive(ProtoBuf_Enum, Debug, Default)] -pub(crate) enum StorageNotification { - #[default] - FileStorageLimitExceeded = 0, - - SingleFileLimitExceeded = 1, -} - -impl std::convert::From<StorageNotification> for i32 { - fn from(notification: StorageNotification) -> Self { - notification as i32 - } -} - -#[tracing::instrument(level = "trace")] -pub(crate) fn make_notification(ty: StorageNotification) -> NotificationBuilder { - NotificationBuilder::new("appflowy_file_storage_notification", ty, OBSERVABLE_SOURCE) -} diff --git a/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs b/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs deleted file mode 100644 index 36e24b7ef9..0000000000 --- a/frontend/rust-lib/flowy-storage/src/sqlite_sql.rs +++ /dev/null @@ -1,258 +0,0 @@ -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::result::DatabaseErrorKind; -use flowy_sqlite::result::Error::DatabaseError; -use flowy_sqlite::schema::{upload_file_part, upload_file_table}; -use flowy_sqlite::{ - diesel, AsChangeset, BoolExpressionMethods, DBConnection, ExpressionMethods, Identifiable, - Insertable, OptionalExtension, QueryDsl, Queryable, RunQueryDsl, SqliteConnection, -}; -use tracing::{trace, warn}; - -#[derive(Queryable, Insertable, AsChangeset, Identifiable, Debug, Clone)] -#[diesel(table_name = upload_file_table)] -#[diesel(primary_key(workspace_id, parent_dir, file_id))] -pub struct UploadFileTable { - pub workspace_id: String, - pub file_id: String, - pub parent_dir: String, - pub local_file_path: String, - pub content_type: String, - pub chunk_size: i32, - pub num_chunk: i32, - pub upload_id: String, - pub created_at: i64, - pub is_finish: bool, -} - -#[derive(Queryable, Insertable, AsChangeset, Identifiable, Debug)] -#[diesel(table_name = upload_file_part)] -#[diesel(primary_key(upload_id, part_num))] -pub struct UploadFilePartTable { - pub upload_id: String, - pub e_tag: String, - pub part_num: i32, -} - -pub fn is_upload_file_exist( - conn: &mut SqliteConnection, - workspace_id: &str, - parent_dir: &str, - file_id: &str, -) -> FlowyResult<bool> { - let result = upload_file_table::dsl::upload_file_table - .filter( - upload_file_table::workspace_id - .eq(workspace_id) - .and(upload_file_table::parent_dir.eq(parent_dir)) - .and(upload_file_table::file_id.eq(file_id)), - ) - .first::<UploadFileTable>(conn) - .optional()?; - Ok(result.is_some()) -} - -pub fn insert_upload_file( - mut conn: DBConnection, - upload_file: &UploadFileTable, -) -> FlowyResult<()> { - trace!("[File]: insert upload file: {:?}", upload_file); - match diesel::insert_into(upload_file_table::table) - .values(upload_file) - .execute(&mut *conn) - { - Ok(_) => Ok(()), - Err(DatabaseError(DatabaseErrorKind::UniqueViolation, _)) => Err(FlowyError::new( - flowy_error::ErrorCode::DuplicateSqliteRecord, - "Upload file already exists", - )), - Err(e) => Err(e.into()), - } -} - -pub fn update_upload_file_upload_id( - mut conn: DBConnection, - workspace_id: &str, - parent_dir: &str, - file_id: &str, - upload_id: &str, -) -> FlowyResult<()> { - diesel::update( - upload_file_table::dsl::upload_file_table.filter( - upload_file_table::workspace_id - .eq(workspace_id) - .and(upload_file_table::parent_dir.eq(parent_dir)) - .and(upload_file_table::file_id.eq(file_id)), - ), - ) - .set(upload_file_table::upload_id.eq(upload_id)) - .execute(&mut *conn)?; - Ok(()) -} - -pub fn update_upload_file_completed(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> { - diesel::update( - upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)), - ) - .set(upload_file_table::is_finish.eq(true)) - .execute(&mut *conn)?; - Ok(()) -} - -pub fn is_upload_exist(mut conn: DBConnection, upload_id: &str) -> FlowyResult<bool> { - let result = upload_file_table::dsl::upload_file_table - .filter(upload_file_table::upload_id.eq(upload_id)) - .first::<UploadFileTable>(&mut *conn) - .optional()?; - Ok(result.is_some()) -} - -pub fn is_upload_completed( - conn: &mut SqliteConnection, - workspace_id: &str, - parent_dir: &str, - file_id: &str, -) -> FlowyResult<bool> { - let result = upload_file_table::dsl::upload_file_table - .filter( - upload_file_table::workspace_id - .eq(workspace_id) - .and(upload_file_table::parent_dir.eq(parent_dir)) - .and(upload_file_table::file_id.eq(file_id)) - .and(upload_file_table::is_finish.eq(true)), - ) - .first::<UploadFileTable>(conn) - .optional()?; - Ok(result.is_some()) -} - -/// Delete upload file and its parts -pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - _delete_upload_file(upload_id, conn)?; - Ok::<_, FlowyError>(()) - })?; - Ok(()) -} - -pub fn delete_upload_file_by_file_id( - mut conn: DBConnection, - workspace_id: &str, - parent_dir: &str, - file_id: &str, -) -> FlowyResult<Option<UploadFileTable>> { - let file = conn.immediate_transaction(|conn| { - let file = select_upload_file(&mut *conn, workspace_id, parent_dir, file_id)?; - - if let Some(file) = &file { - // when the upload is not started, the upload id will be empty - if file.upload_id.is_empty() { - diesel::delete( - upload_file_table::dsl::upload_file_table.filter( - upload_file_table::workspace_id - .eq(workspace_id) - .and(upload_file_table::parent_dir.eq(parent_dir)) - .and(upload_file_table::file_id.eq(file_id)), - ), - ) - .execute(&mut *conn)?; - } else { - _delete_upload_file(&file.upload_id, &mut *conn)?; - } - } - - Ok::<_, FlowyError>(file) - })?; - - Ok(file) -} - -fn _delete_upload_file(upload_id: &str, conn: &mut SqliteConnection) -> Result<(), FlowyError> { - if upload_id.is_empty() { - warn!("[File]: upload_id is empty when delete upload file"); - return Ok(()); - } - - trace!("[File]: delete upload file: {}", upload_id); - diesel::delete( - upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)), - ) - .execute(&mut *conn)?; - if let Err(err) = delete_all_upload_parts(&mut *conn, upload_id) { - warn!("Failed to delete upload parts: {:?}", err) - } - Ok(()) -} - -pub fn delete_all_upload_parts(conn: &mut SqliteConnection, upload_id: &str) -> FlowyResult<()> { - diesel::delete( - upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)), - ) - .execute(&mut *conn)?; - Ok(()) -} - -pub fn insert_upload_part( - mut conn: DBConnection, - upload_part: &UploadFilePartTable, -) -> FlowyResult<()> { - diesel::insert_into(upload_file_part::table) - .values(upload_part) - .execute(&mut *conn)?; - Ok(()) -} - -pub fn select_latest_upload_part( - mut conn: DBConnection, - upload_id: &str, -) -> FlowyResult<Option<UploadFilePartTable>> { - let result = upload_file_part::dsl::upload_file_part - .filter(upload_file_part::upload_id.eq(upload_id)) - .order(upload_file_part::part_num.desc()) - .first::<UploadFilePartTable>(&mut *conn) - .optional()?; - Ok(result) -} - -pub fn select_upload_parts( - conn: &mut SqliteConnection, - upload_id: &str, -) -> FlowyResult<Vec<UploadFilePartTable>> { - let results = upload_file_part::dsl::upload_file_part - .filter(upload_file_part::upload_id.eq(upload_id)) - .load::<UploadFilePartTable>(conn)?; - Ok(results) -} - -pub fn batch_select_upload_file( - mut conn: DBConnection, - workspace_id: &str, - limit: i32, - is_finish: bool, -) -> FlowyResult<Vec<UploadFileTable>> { - let results = upload_file_table::dsl::upload_file_table - .filter(upload_file_table::workspace_id.eq(workspace_id)) - .filter(upload_file_table::is_finish.eq(is_finish)) - .order(upload_file_table::created_at.desc()) - .limit(limit.into()) - .load::<UploadFileTable>(&mut *conn)?; - - Ok(results) -} - -pub fn select_upload_file( - conn: &mut SqliteConnection, - workspace_id: &str, - parent_dir: &str, - file_id: &str, -) -> FlowyResult<Option<UploadFileTable>> { - let result = upload_file_table::dsl::upload_file_table - .filter( - upload_file_table::workspace_id - .eq(workspace_id) - .and(upload_file_table::parent_dir.eq(parent_dir)) - .and(upload_file_table::file_id.eq(file_id)), - ) - .first::<UploadFileTable>(conn) - .optional()?; - Ok(result) -} diff --git a/frontend/rust-lib/flowy-storage/src/uploader.rs b/frontend/rust-lib/flowy-storage/src/uploader.rs deleted file mode 100644 index a230b3de83..0000000000 --- a/frontend/rust-lib/flowy-storage/src/uploader.rs +++ /dev/null @@ -1,380 +0,0 @@ -use crate::sqlite_sql::UploadFileTable; -use crate::uploader::UploadTask::BackgroundTask; -use flowy_storage_pub::storage::StorageService; -use lib_infra::box_any::BoxAny; -use std::cmp::Ordering; -use std::collections::BinaryHeap; -use std::fmt::Display; -use std::sync::atomic::{AtomicBool, AtomicU8}; -use std::sync::{Arc, Weak}; -use std::time::Duration; -use tokio::sync::{watch, RwLock}; -use tracing::{error, info, instrument, trace, warn}; - -#[derive(Clone)] -pub enum Signal { - Stop, - Proceed, - ProceedAfterSecs(u64), -} - -pub struct UploadTaskQueue { - tasks: RwLock<BinaryHeap<UploadTask>>, - notifier: watch::Sender<Signal>, -} - -impl UploadTaskQueue { - pub fn new(notifier: watch::Sender<Signal>) -> Self { - Self { - tasks: Default::default(), - notifier, - } - } - pub async fn queue_task(&self, task: UploadTask) { - trace!("[File] Queued task: {}", task); - self.tasks.write().await.push(task); - let _ = self.notifier.send_replace(Signal::Proceed); - } - - pub async fn remove_task(&self, workspace_id: &str, parent_dir: &str, file_id: &str) { - let mut tasks = self.tasks.write().await; - - tasks.retain(|task| match task { - UploadTask::BackgroundTask { - workspace_id: w_id, - parent_dir: p_dir, - file_id: f_id, - .. - } => !(w_id == workspace_id && p_dir == parent_dir && f_id == file_id), - UploadTask::Task { record, .. } => { - !(record.workspace_id == workspace_id - && record.parent_dir == parent_dir - && record.file_id == file_id) - }, - }); - } -} - -pub struct FileUploader { - storage_service: Arc<dyn StorageService>, - queue: Arc<UploadTaskQueue>, - max_uploads: u8, - current_uploads: AtomicU8, - pause_sync: AtomicBool, - disable_upload: Arc<AtomicBool>, -} - -impl Drop for FileUploader { - fn drop(&mut self) { - let _ = self.queue.notifier.send(Signal::Stop); - } -} - -impl FileUploader { - pub fn new( - storage_service: Arc<dyn StorageService>, - queue: Arc<UploadTaskQueue>, - is_exceed_limit: Arc<AtomicBool>, - ) -> Self { - Self { - storage_service, - queue, - max_uploads: 3, - current_uploads: Default::default(), - pause_sync: Default::default(), - disable_upload: is_exceed_limit, - } - } - - pub async fn all_tasks(&self) -> Vec<UploadTask> { - let tasks = self.queue.tasks.read().await; - tasks.iter().cloned().collect() - } - - pub async fn queue_tasks(&self, tasks: Vec<UploadTask>) { - let mut queue_lock = self.queue.tasks.write().await; - for task in tasks { - queue_lock.push(task); - } - let _ = self.queue.notifier.send(Signal::Proceed); - } - - pub fn pause(&self) { - self - .pause_sync - .store(true, std::sync::atomic::Ordering::SeqCst); - } - - pub fn disable_storage_write(&self) { - self - .disable_upload - .store(true, std::sync::atomic::Ordering::SeqCst); - self.pause(); - } - - pub fn enable_storage_write(&self) { - self - .disable_upload - .store(false, std::sync::atomic::Ordering::SeqCst); - self.resume(); - } - - pub fn resume(&self) { - self - .pause_sync - .store(false, std::sync::atomic::Ordering::SeqCst); - trace!("[File] Uploader resumed"); - let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(3)); - } - - #[instrument(name = "[File]: process next", level = "debug", skip(self))] - pub async fn process_next(&self) -> Option<()> { - // Do not proceed if the uploader is paused. - if self.pause_sync.load(std::sync::atomic::Ordering::Relaxed) { - info!("[File] Uploader is paused"); - return None; - } - - let current_uploads = self - .current_uploads - .load(std::sync::atomic::Ordering::SeqCst); - if current_uploads > 0 { - trace!("[File] current upload tasks: {}", current_uploads) - } - - if self - .current_uploads - .load(std::sync::atomic::Ordering::SeqCst) - >= self.max_uploads - { - // If the current uploads count is greater than or equal to the max uploads, do not proceed. - let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(10)); - trace!("[File] max uploads reached, process_next after 10 seconds"); - return None; - } - - if self - .disable_upload - .load(std::sync::atomic::Ordering::SeqCst) - { - // If the storage limitation is enabled, do not proceed. - error!("[File] storage limit exceeded, uploader is disabled"); - return None; - } - - let task = self.queue.tasks.write().await.pop()?; - if task.retry_count() > 5 { - // If the task has been retried more than 5 times, we should not retry it anymore. - let _ = self.queue.notifier.send(Signal::ProceedAfterSecs(2)); - warn!("[File] Task has been retried more than 5 times: {}", task); - return None; - } - - // increment the current uploads count - self - .current_uploads - .fetch_add(1, std::sync::atomic::Ordering::SeqCst); - - match task { - UploadTask::Task { - local_file_path, - record, - mut retry_count, - } => { - let record = BoxAny::new(record); - if let Err(err) = self.storage_service.start_upload(&record).await { - if err.is_file_limit_exceeded() { - self.disable_storage_write(); - } - - if err.should_retry_upload() { - info!( - "[File] Failed to upload file: {}, retry_count:{}", - err, retry_count - ); - let record = record.unbox_or_error().unwrap(); - retry_count += 1; - self.queue.tasks.write().await.push(UploadTask::Task { - local_file_path, - record, - retry_count, - }); - } - } - }, - UploadTask::BackgroundTask { - workspace_id, - parent_dir, - file_id, - created_at, - mut retry_count, - } => { - if let Err(err) = self - .storage_service - .resume_upload(&workspace_id, &parent_dir, &file_id) - .await - { - if err.is_file_limit_exceeded() { - error!("[File] failed to upload file: {}", err); - self.disable_storage_write(); - } - - if err.should_retry_upload() { - info!( - "[File] failed to resume upload file: {}, retry_count:{}", - err, retry_count - ); - retry_count += 1; - self.queue.tasks.write().await.push(BackgroundTask { - workspace_id, - parent_dir, - file_id, - created_at, - retry_count, - }); - } - } - }, - } - - self - .current_uploads - .fetch_sub(1, std::sync::atomic::Ordering::SeqCst); - trace!("[File] process_next after 2 seconds"); - self - .queue - .notifier - .send_replace(Signal::ProceedAfterSecs(2)); - None - } -} - -pub struct FileUploaderRunner; - -impl FileUploaderRunner { - pub async fn run(weak_uploader: Weak<FileUploader>, mut notifier: watch::Receiver<Signal>) { - // Start uploading after 20 seconds - tokio::time::sleep(Duration::from_secs(20)).await; - - loop { - // stops the runner if the notifier was closed. - if notifier.changed().await.is_err() { - info!("[File]:Uploader runner stopped, notifier closed"); - break; - } - - if let Some(uploader) = weak_uploader.upgrade() { - let value = notifier.borrow().clone(); - trace!( - "[File]: Uploader runner received signal, thread_id: {:?}", - std::thread::current().id() - ); - match value { - Signal::Stop => { - info!("[File]:Uploader runner stopped, stop signal received"); - break; - }, - Signal::Proceed => { - tokio::spawn(async move { - uploader.process_next().await; - }); - }, - Signal::ProceedAfterSecs(secs) => { - tokio::time::sleep(Duration::from_secs(secs)).await; - tokio::spawn(async move { - uploader.process_next().await; - }); - }, - } - } else { - info!("[File]:Uploader runner stopped, uploader dropped"); - break; - } - } - } -} - -#[derive(Clone)] -pub enum UploadTask { - Task { - local_file_path: String, - record: UploadFileTable, - retry_count: u8, - }, - BackgroundTask { - workspace_id: String, - file_id: String, - parent_dir: String, - created_at: i64, - retry_count: u8, - }, -} - -impl UploadTask { - pub fn retry_count(&self) -> u8 { - match self { - UploadTask::Task { retry_count, .. } => *retry_count, - UploadTask::BackgroundTask { retry_count, .. } => *retry_count, - } - } -} - -impl Display for UploadTask { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UploadTask::Task { record, .. } => write!(f, "Task: {}", record.file_id), - UploadTask::BackgroundTask { file_id, .. } => write!(f, "BackgroundTask: {}", file_id), - } - } -} - -impl Eq for UploadTask {} - -impl PartialEq for UploadTask { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Task { record: lhs, .. }, Self::Task { record: rhs, .. }) => { - lhs.local_file_path == rhs.local_file_path - }, - ( - Self::BackgroundTask { - workspace_id: l_workspace_id, - file_id: l_file_id, - .. - }, - Self::BackgroundTask { - workspace_id: r_workspace_id, - file_id: r_file_id, - .. - }, - ) => l_workspace_id == r_workspace_id && l_file_id == r_file_id, - _ => false, - } - } -} - -impl PartialOrd for UploadTask { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -impl Ord for UploadTask { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Self::Task { record: lhs, .. }, Self::Task { record: rhs, .. }) => { - lhs.created_at.cmp(&rhs.created_at) - }, - (_, Self::Task { .. }) => Ordering::Less, - (Self::Task { .. }, _) => Ordering::Greater, - ( - Self::BackgroundTask { - created_at: lhs, .. - }, - Self::BackgroundTask { - created_at: rhs, .. - }, - ) => lhs.cmp(rhs), - } - } -} diff --git a/frontend/rust-lib/flowy-storage/src/wasm/mod.rs b/frontend/rust-lib/flowy-storage/src/wasm/mod.rs new file mode 100644 index 0000000000..8d4d3b1bfc --- /dev/null +++ b/frontend/rust-lib/flowy-storage/src/wasm/mod.rs @@ -0,0 +1,12 @@ +use crate::{ObjectIdentity, ObjectValue}; +use flowy_error::FlowyError; + +pub async fn object_from_disk( + _workspace_id: &str, + _local_file_path: &str, +) -> Result<(ObjectIdentity, ObjectValue), FlowyError> { + Err( + FlowyError::not_support() + .with_context(format!("object_from_disk is not implemented for wasm32")), + ) +} diff --git a/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs b/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs deleted file mode 100644 index 4fddb78458..0000000000 --- a/frontend/rust-lib/flowy-storage/tests/multiple_part_upload_test.rs +++ /dev/null @@ -1,190 +0,0 @@ -use collab_importer::util::FileId; -use flowy_sqlite::Database; -use flowy_storage::sqlite_sql::{ - batch_select_upload_file, delete_upload_file, insert_upload_file, insert_upload_part, - select_latest_upload_part, select_upload_parts, UploadFilePartTable, UploadFileTable, -}; -use flowy_storage_pub::chunked_byte::{ChunkedBytes, MIN_CHUNK_SIZE}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; -use std::env::temp_dir; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::time::Duration; - -pub fn test_database() -> (Database, PathBuf) { - let db_path = temp_dir().join(format!("test-{}.db", generate_random_string(8))); - (flowy_sqlite::init(&db_path).unwrap(), db_path) -} - -#[tokio::test] -async fn test_insert_new_upload() { - let (db, _) = test_database(); - - let workspace_id = uuid::Uuid::new_v4().to_string(); - - // test insert one upload file record - let mut upload_ids = vec![]; - for _i in 0..5 { - let upload_id = uuid::Uuid::new_v4().to_string(); - let local_file_path = create_temp_file_with_random_content(8 * 1024 * 1024).unwrap(); - let upload_file = - create_upload_file_record(workspace_id.clone(), upload_id.clone(), local_file_path).await; - upload_ids.push(upload_file.upload_id.clone()); - - // insert - let conn = db.get_connection().unwrap(); - insert_upload_file(conn, &upload_file).unwrap(); - tokio::time::sleep(Duration::from_secs(1)).await; - } - upload_ids.reverse(); - - // select - let conn = db.get_connection().unwrap(); - let records = batch_select_upload_file(conn, &workspace_id, 100, false).unwrap(); - - assert_eq!(records.len(), 5); - // compare the upload id order is the same as upload_ids - for i in 0..5 { - assert_eq!(records[i].upload_id, upload_ids[i]); - - // delete - let conn = db.get_connection().unwrap(); - delete_upload_file(conn, &records[i].upload_id).unwrap(); - } - - let conn = db.get_connection().unwrap(); - let records = batch_select_upload_file(conn, &workspace_id, 100, false).unwrap(); - assert!(records.is_empty()); -} - -#[tokio::test] -async fn test_upload_part_test() { - let (db, _) = test_database(); - - let workspace_id = uuid::Uuid::new_v4().to_string(); - - // test insert one upload file record - let upload_id = uuid::Uuid::new_v4().to_string(); - let local_file_path = create_temp_file_with_random_content(20 * 1024 * 1024).unwrap(); - let upload_file = - create_upload_file_record(workspace_id.clone(), upload_id.clone(), local_file_path).await; - - // insert - let conn = db.get_connection().unwrap(); - insert_upload_file(conn, &upload_file).unwrap(); - tokio::time::sleep(Duration::from_secs(1)).await; - - // insert uploaded part 1 - let part = UploadFilePartTable { - upload_id: upload_id.clone(), - e_tag: "1".to_string(), - part_num: 1, - }; - let conn = db.get_connection().unwrap(); - insert_upload_part(conn, &part).unwrap(); - - // insert uploaded part 2 - let part = UploadFilePartTable { - upload_id: upload_id.clone(), - e_tag: "2".to_string(), - part_num: 2, - }; - let conn = db.get_connection().unwrap(); - insert_upload_part(conn, &part).unwrap(); - - // get latest part - let conn = db.get_connection().unwrap(); - let part = select_latest_upload_part(conn, &upload_id) - .unwrap() - .unwrap(); - assert_eq!(part.part_num, 2); - - // get all existing parts - let mut conn = db.get_connection().unwrap(); - let parts = select_upload_parts(&mut conn, &upload_id).unwrap(); - assert_eq!(parts.len(), 2); - assert_eq!(parts[0].part_num, 1); - assert_eq!(parts[1].part_num, 2); - - // delete upload file and then all existing parts will be deleted - let conn = db.get_connection().unwrap(); - delete_upload_file(conn, &upload_id).unwrap(); - - let mut conn = db.get_connection().unwrap(); - let parts = select_upload_parts(&mut conn, &upload_id).unwrap(); - assert!(parts.is_empty()) -} - -pub fn generate_random_string(len: usize) -> String { - let rng = thread_rng(); - rng - .sample_iter(&Alphanumeric) - .take(len) - .map(char::from) - .collect() -} - -fn create_temp_file_with_random_content( - size_in_bytes: usize, -) -> Result<String, Box<dyn std::error::Error>> { - // Generate a random string of the specified size - let content: String = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(size_in_bytes) - .map(char::from) - .collect(); - - // Create a temporary file path - let file_path = std::env::temp_dir().join("test.txt"); - - // Write the content to the temporary file - let mut file = File::create(&file_path)?; - file.write_all(content.as_bytes())?; - - // Return the file path - Ok(file_path.to_str().unwrap().to_string()) -} - -pub async fn create_upload_file_record( - workspace_id: String, - upload_id: String, - local_file_path: String, -) -> UploadFileTable { - // Create ChunkedBytes from file - let chunked_bytes = ChunkedBytes::from_file(&local_file_path, MIN_CHUNK_SIZE) - .await - .unwrap(); - - // Determine content type - let content_type = mime_guess::from_path(&local_file_path) - .first_or_octet_stream() - .to_string(); - - // let mut file_path = temp_dir(); - // file_path.push("test_large_file_with_many_chunks"); - // let mut file = File::create(&file_path).await.unwrap(); - // file.write_all(&vec![0; 50 * 1024 * 1024]).await.unwrap(); // 50 MB - // file.flush().await.unwrap(); - - // Calculate file ID - let file_id = FileId::from_path(&PathBuf::from(&local_file_path)) - .await - .unwrap(); - let num_chunk = chunked_bytes.total_chunks(); - - // Create UploadFileTable record - UploadFileTable { - workspace_id, - file_id, - upload_id, - parent_dir: "test".to_string(), - local_file_path, - content_type, - chunk_size: MIN_CHUNK_SIZE as i32, - num_chunk: num_chunk as i32, - created_at: chrono::Utc::now().timestamp(), - is_finish: false, - } -} diff --git a/frontend/rust-lib/flowy-user-pub/Cargo.toml b/frontend/rust-lib/flowy-user-pub/Cargo.toml index f8a673e918..f70e53c248 100644 --- a/frontend/rust-lib/flowy-user-pub/Cargo.toml +++ b/frontend/rust-lib/flowy-user-pub/Cargo.toml @@ -15,11 +15,9 @@ collab-entity = { workspace = true } serde_json.workspace = true serde_repr.workspace = true chrono = { workspace = true, default-features = false, features = ["clock", "serde"] } +anyhow.workspace = true tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.14" flowy-folder-pub.workspace = true -collab-folder = { workspace = true } tracing.workspace = true base64 = "0.21" -client-api = { workspace = true } -flowy-sqlite.workspace = true \ No newline at end of file diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index a99e8b8672..a97f7b28e1 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -1,15 +1,7 @@ -use client_api::entity::billing_dto::RecurringInterval; -use client_api::entity::billing_dto::SubscriptionPlan; -use client_api::entity::billing_dto::SubscriptionPlanDetail; -pub use client_api::entity::billing_dto::SubscriptionStatus; -use client_api::entity::billing_dto::WorkspaceSubscriptionStatus; -use client_api::entity::billing_dto::WorkspaceUsageAndLimit; -use client_api::entity::GotrueTokenResponse; -pub use client_api::entity::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; use collab_entity::{CollabObject, CollabType}; use flowy_error::{internal_error, ErrorCode, FlowyError}; -use lib_infra::async_trait::async_trait; use lib_infra::box_any::BoxAny; +use lib_infra::future::FutureResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -20,8 +12,9 @@ use tokio_stream::wrappers::WatchStream; use uuid::Uuid; use crate::entities::{ - AuthResponse, AuthType, Role, UpdateUserProfileParams, UserProfile, UserTokenState, - UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + AuthResponse, Authenticator, RecurringInterval, Role, SubscriptionPlan, UpdateUserProfileParams, + UserCredentials, UserProfile, UserTokenState, UserWorkspace, WorkspaceInvitation, + WorkspaceInvitationStatus, WorkspaceMember, WorkspaceSubscription, WorkspaceUsage, }; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -68,7 +61,6 @@ pub trait UserCloudServiceProvider: Send + Sync { /// # Returns /// A `Result` which is `Ok` if the token is successfully set, or a `FlowyError` otherwise. fn set_token(&self, token: &str) -> Result<(), FlowyError>; - fn set_ai_model(&self, ai_model: &str) -> Result<(), FlowyError>; /// Subscribes to the state of the authentication token. /// @@ -84,13 +76,13 @@ pub trait UserCloudServiceProvider: Send + Sync { /// * `enable_sync`: A boolean indicating whether synchronization should be enabled or disabled. fn set_enable_sync(&self, uid: i64, enable_sync: bool); - fn set_server_auth_type( - &self, - auth_type: &AuthType, - token: Option<String>, - ) -> Result<(), FlowyError>; + /// Sets the authenticator when user sign in or sign up. + /// + /// # Arguments + /// * `authenticator`: An `Authenticator` object. + fn set_user_authenticator(&self, authenticator: &Authenticator); - fn get_server_auth_type(&self) -> AuthType; + fn get_user_authenticator(&self) -> Authenticator; /// Sets the network reachability /// @@ -120,129 +112,139 @@ pub trait UserCloudServiceProvider: Send + Sync { /// Provide the generic interface for the user cloud service /// The user cloud service is responsible for the user authentication and user profile management #[allow(unused_variables)] -#[async_trait] pub trait UserCloudService: Send + Sync + 'static { /// Sign up a new account. /// The type of the params is defined the this trait's implementation. /// Use the `unbox_or_error` of the [BoxAny] to get the params. - async fn sign_up(&self, params: BoxAny) -> Result<AuthResponse, FlowyError>; + fn sign_up(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError>; /// Sign in an account /// The type of the params is defined the this trait's implementation. - async fn sign_in(&self, params: BoxAny) -> Result<AuthResponse, FlowyError>; + fn sign_in(&self, params: BoxAny) -> FutureResult<AuthResponse, FlowyError>; /// Sign out an account - async fn sign_out(&self, token: Option<String>) -> Result<(), FlowyError>; - - /// Delete an account and all the data associated with the account - async fn delete_account(&self) -> Result<(), FlowyError> { - Ok(()) - } + fn sign_out(&self, token: Option<String>) -> FutureResult<(), FlowyError>; /// Generate a sign in url for the user with the given email /// Currently, only use the admin client for testing - async fn generate_sign_in_url_with_email(&self, email: &str) -> Result<String, FlowyError>; + fn generate_sign_in_url_with_email(&self, email: &str) -> FutureResult<String, FlowyError>; - async fn create_user(&self, email: &str, password: &str) -> Result<(), FlowyError>; + fn create_user(&self, email: &str, password: &str) -> FutureResult<(), FlowyError>; - async fn sign_in_with_password( + fn sign_in_with_password( &self, email: &str, password: &str, - ) -> Result<GotrueTokenResponse, FlowyError>; + ) -> FutureResult<UserProfile, FlowyError>; - async fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) - -> Result<(), FlowyError>; - - async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result<GotrueTokenResponse, FlowyError>; + fn sign_in_with_magic_link(&self, email: &str, redirect_to: &str) + -> FutureResult<(), FlowyError>; /// When the user opens the OAuth URL, it redirects to the corresponding provider's OAuth web page. /// After the user is authenticated, the browser will open a deep link to the AppFlowy app (iOS, macOS, etc.), /// which will call [Client::sign_in_with_url]generate_sign_in_url_with_email to sign in. /// /// For example, the OAuth URL on Google looks like `https://appflowy.io/authorize?provider=google`. - async fn generate_oauth_url_with_provider(&self, provider: &str) -> Result<String, FlowyError>; + fn generate_oauth_url_with_provider(&self, provider: &str) -> FutureResult<String, FlowyError>; /// Using the user's token to update the user information - async fn update_user(&self, params: UpdateUserProfileParams) -> Result<(), FlowyError>; + fn update_user( + &self, + credential: UserCredentials, + params: UpdateUserProfileParams, + ) -> FutureResult<(), FlowyError>; /// Get the user information using the user's token or uid /// return None if the user is not found - async fn get_user_profile(&self, uid: i64, workspace_id: &str) - -> Result<UserProfile, FlowyError>; + fn get_user_profile(&self, credential: UserCredentials) -> FutureResult<UserProfile, FlowyError>; - async fn open_workspace(&self, workspace_id: &Uuid) -> Result<UserWorkspace, FlowyError>; + fn open_workspace(&self, workspace_id: &str) -> FutureResult<UserWorkspace, FlowyError>; /// Return the all the workspaces of the user - async fn get_all_workspace(&self, uid: i64) -> Result<Vec<UserWorkspace>, FlowyError>; + fn get_all_workspace(&self, uid: i64) -> FutureResult<Vec<UserWorkspace>, FlowyError>; /// Creates a new workspace for the user. /// Returns the new workspace if successful - async fn create_workspace(&self, workspace_name: &str) -> Result<UserWorkspace, FlowyError>; + fn create_workspace(&self, workspace_name: &str) -> FutureResult<UserWorkspace, FlowyError>; // Updates the workspace name and icon - async fn patch_workspace( + fn patch_workspace( &self, - workspace_id: &Uuid, - new_workspace_name: Option<String>, - new_workspace_icon: Option<String>, - ) -> Result<(), FlowyError>; + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, + ) -> FutureResult<(), FlowyError>; /// Deletes a workspace owned by the user. - async fn delete_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError>; + fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError>; - async fn invite_workspace_member( + // Deprecated, use invite instead + fn add_workspace_member( + &self, + user_email: String, + workspace_id: String, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } + + fn invite_workspace_member( &self, invitee_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, - ) -> Result<(), FlowyError> { - Ok(()) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn list_workspace_invitations( + fn list_workspace_invitations( &self, filter: Option<WorkspaceInvitationStatus>, - ) -> Result<Vec<WorkspaceInvitation>, FlowyError> { - Ok(vec![]) + ) -> FutureResult<Vec<WorkspaceInvitation>, FlowyError> { + FutureResult::new(async { Ok(vec![]) }) } - async fn accept_workspace_invitations(&self, invite_id: String) -> Result<(), FlowyError> { - Ok(()) + fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn remove_workspace_member( + fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, - ) -> Result<(), FlowyError> { - Ok(()) + workspace_id: String, + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn update_workspace_member( + fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, - ) -> Result<(), FlowyError> { - Ok(()) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn get_workspace_members( + fn get_workspace_members( &self, - workspace_id: Uuid, - ) -> Result<Vec<WorkspaceMember>, FlowyError>; + workspace_id: String, + ) -> FutureResult<Vec<WorkspaceMember>, FlowyError> { + FutureResult::new(async { Ok(vec![]) }) + } - async fn get_user_awareness_doc_state( + fn get_workspace_member( + &self, + workspace_id: String, + uid: i64, + ) -> FutureResult<WorkspaceMember, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) + } + + fn get_user_awareness_doc_state( &self, uid: i64, - workspace_id: &Uuid, - object_id: &Uuid, - ) -> Result<Vec<u8>, FlowyError>; + workspace_id: &str, + object_id: &str, + ) -> FutureResult<Vec<u8>, FlowyError>; fn receive_realtime_event(&self, _json: Value) {} @@ -250,100 +252,57 @@ pub trait UserCloudService: Send + Sync + 'static { None } - async fn create_collab_object( + fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError>; + + fn create_collab_object( &self, collab_object: &CollabObject, data: Vec<u8>, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), FlowyError>; - async fn batch_create_collab_object( + fn batch_create_collab_object( &self, - workspace_id: &Uuid, + workspace_id: &str, objects: Vec<UserCollabParams>, - ) -> Result<(), FlowyError>; + ) -> FutureResult<(), FlowyError>; - async fn leave_workspace(&self, workspace_id: &Uuid) -> Result<(), FlowyError> { - Ok(()) + fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) } - async fn subscribe_workspace( + fn subscribe_workspace( &self, - workspace_id: Uuid, + workspace_id: String, recurring_interval: RecurringInterval, workspace_subscription_plan: SubscriptionPlan, success_url: String, - ) -> Result<String, FlowyError> { - Err(FlowyError::not_support()) + ) -> FutureResult<String, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - async fn get_workspace_member( + fn get_workspace_member_info( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, - ) -> Result<WorkspaceMember, FlowyError>; - /// Get all subscriptions for all workspaces for a user (email) - async fn get_workspace_subscriptions( - &self, - ) -> Result<Vec<WorkspaceSubscriptionStatus>, FlowyError> { - Ok(vec![]) + ) -> FutureResult<WorkspaceMember, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - /// Get the workspace subscriptions for a workspace - async fn get_workspace_subscription_one( - &self, - workspace_id: &Uuid, - ) -> Result<Vec<WorkspaceSubscriptionStatus>, FlowyError> { - Ok(vec![]) + fn get_workspace_subscriptions(&self) -> FutureResult<Vec<WorkspaceSubscription>, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - async fn cancel_workspace_subscription( - &self, - workspace_id: String, - plan: SubscriptionPlan, - reason: Option<String>, - ) -> Result<(), FlowyError> { - Ok(()) + fn cancel_workspace_subscription(&self, workspace_id: String) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - async fn get_workspace_plan( - &self, - workspace_id: Uuid, - ) -> Result<Vec<SubscriptionPlan>, FlowyError> { - Ok(vec![]) + fn get_workspace_usage(&self, workspace_id: String) -> FutureResult<WorkspaceUsage, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - async fn get_workspace_usage( - &self, - workspace_id: &Uuid, - ) -> Result<WorkspaceUsageAndLimit, FlowyError>; - - async fn get_billing_portal_url(&self) -> Result<String, FlowyError> { - Err(FlowyError::not_support()) + fn get_billing_portal_url(&self) -> FutureResult<String, FlowyError> { + FutureResult::new(async { Err(FlowyError::not_support()) }) } - - async fn update_workspace_subscription_payment_period( - &self, - workspace_id: &Uuid, - plan: SubscriptionPlan, - recurring_interval: RecurringInterval, - ) -> Result<(), FlowyError> { - Ok(()) - } - - async fn get_subscription_plan_details(&self) -> Result<Vec<SubscriptionPlanDetail>, FlowyError> { - Ok(vec![]) - } - - async fn get_workspace_setting( - &self, - workspace_id: &Uuid, - ) -> Result<AFWorkspaceSettings, FlowyError>; - - async fn update_workspace_setting( - &self, - workspace_id: &Uuid, - workspace_settings: AFWorkspaceSettingsChange, - ) -> Result<AFWorkspaceSettings, FlowyError>; } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver<UserUpdate>; diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index a870b9c0b0..566aabe1c0 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -1,15 +1,13 @@ -use std::fmt::{Display, Formatter}; use std::str::FromStr; use chrono::{DateTime, Utc}; -pub use client_api::entity::billing_dto::RecurringInterval; -use client_api::entity::AFRole; -use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_repr::*; use uuid::Uuid; +pub const USER_METADATA_OPEN_AI_KEY: &str = "openai_key"; +pub const USER_METADATA_STABILITY_AI_KEY: &str = "stability_ai_key"; pub const USER_METADATA_ICON_URL: &str = "icon_url"; pub const USER_METADATA_UPDATE_AT: &str = "updated_at"; @@ -31,7 +29,7 @@ pub struct SignInParams { pub email: String, pub password: String, pub name: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, } #[derive(Serialize, Deserialize, Default, Debug)] @@ -39,7 +37,7 @@ pub struct SignUpParams { pub email: String, pub name: String, pub password: String, - pub auth_type: AuthType, + pub auth_type: Authenticator, pub device_id: String, } @@ -100,6 +98,40 @@ impl UserAuthResponse for AuthResponse { } } +#[derive(Clone, Debug)] +pub struct UserCredentials { + /// Currently, the token is only used when the [Authenticator] is AppFlowyCloud + pub token: Option<String>, + + /// The user id + pub uid: Option<i64>, + + /// The user id + pub uuid: Option<String>, +} + +impl UserCredentials { + pub fn from_uid(uid: i64) -> Self { + Self { + token: None, + uid: Some(uid), + uuid: None, + } + } + + pub fn from_uuid(uuid: String) -> Self { + Self { + token: None, + uid: None, + uuid: Some(uuid), + } + } + + pub fn new(token: Option<String>, uid: Option<i64>, uuid: Option<String>) -> Self { + Self { token, uid, uuid } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UserWorkspace { pub id: String, @@ -107,50 +139,37 @@ pub struct UserWorkspace { pub created_at: DateTime<Utc>, /// The database storage id is used indexing all the database views in current workspace. #[serde(rename = "database_storage_id")] - pub workspace_database_id: String, + pub database_indexer_id: String, #[serde(default)] pub icon: String, - #[serde(default)] - pub member_count: i64, - #[serde(default)] - pub role: Option<Role>, - #[serde(default = "default_workspace_type")] - pub workspace_type: AuthType, -} - -fn default_workspace_type() -> AuthType { - AuthType::AppFlowyCloud } impl UserWorkspace { - pub fn workspace_id(&self) -> FlowyResult<Uuid> { - let id = Uuid::from_str(&self.id)?; - Ok(id) - } - - pub fn new_local(workspace_id: String, name: &str) -> Self { + pub fn new(workspace_id: &str, _uid: i64) -> Self { Self { - id: workspace_id, - name: name.to_string(), + id: workspace_id.to_string(), + name: "".to_string(), created_at: Utc::now(), - workspace_database_id: Uuid::new_v4().to_string(), + database_indexer_id: Uuid::new_v4().to_string(), icon: "".to_string(), - member_count: 1, - role: Some(Role::Owner), - workspace_type: AuthType::Local, } } } -#[derive(Default, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, Debug, Clone)] pub struct UserProfile { + #[serde(rename = "id")] pub uid: i64, pub email: String, pub name: String, pub token: String, pub icon_url: String, - pub auth_type: AuthType, - pub workspace_auth_type: AuthType, + pub openai_key: String, + pub stability_ai_key: String, + pub workspace_id: String, + pub authenticator: Authenticator, + // If the encryption_sign is not empty, which means the user has enabled the encryption. + pub encryption_type: EncryptionType, pub updated_at: i64, } @@ -193,29 +212,42 @@ impl FromStr for EncryptionType { } } -impl<T> From<(&T, &AuthType)> for UserProfile +impl<T> From<(&T, &Authenticator)> for UserProfile where T: UserAuthResponse, { - fn from(params: (&T, &AuthType)) -> Self { + fn from(params: (&T, &Authenticator)) -> Self { let (value, auth_type) = params; - let icon_url = value - .metadata() - .as_ref() - .map(|m| { - m.get(USER_METADATA_ICON_URL) - .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) - .unwrap_or_default() - }) - .unwrap_or_default(); + let (icon_url, openai_key, stability_ai_key) = { + value + .metadata() + .as_ref() + .map(|m| { + ( + m.get(USER_METADATA_ICON_URL) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + m.get(USER_METADATA_OPEN_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + m.get(USER_METADATA_STABILITY_AI_KEY) + .map(|v| v.as_str().map(|s| s.to_string()).unwrap_or_default()) + .unwrap_or_default(), + ) + }) + .unwrap_or_default() + }; Self { uid: value.user_id(), email: value.user_email().unwrap_or_default(), name: value.user_name().to_owned(), token: value.user_token().unwrap_or_default(), icon_url, - auth_type: *auth_type, - workspace_auth_type: *auth_type, + openai_key, + workspace_id: value.latest_workspace().id.to_owned(), + authenticator: auth_type.clone(), + encryption_type: value.encryption_type(), + stability_ai_key, updated_at: value.updated_at(), } } @@ -228,6 +260,9 @@ pub struct UpdateUserProfileParams { pub email: Option<String>, pub password: Option<String>, pub icon_url: Option<String>, + pub openai_key: Option<String>, + pub stability_ai_key: Option<String>, + pub encryption_sign: Option<String>, pub token: Option<String>, } @@ -263,49 +298,72 @@ impl UpdateUserProfileParams { self.icon_url = Some(icon_url.to_string()); self } + + pub fn with_openai_key(mut self, openai_key: &str) -> Self { + self.openai_key = Some(openai_key.to_owned()); + self + } + + pub fn with_stability_ai_key(mut self, stability_ai_key: &str) -> Self { + self.stability_ai_key = Some(stability_ai_key.to_owned()); + self + } + + pub fn with_encryption_type(mut self, encryption_type: EncryptionType) -> Self { + let sign = match encryption_type { + EncryptionType::NoEncryption => "".to_string(), + EncryptionType::SelfEncryption(sign) => sign, + }; + self.encryption_sign = Some(sign); + self + } + + pub fn is_empty(&self) -> bool { + self.name.is_none() + && self.email.is_none() + && self.password.is_none() + && self.icon_url.is_none() + && self.openai_key.is_none() + && self.encryption_sign.is_none() + && self.stability_ai_key.is_none() + } } -#[derive(Debug, Clone, Copy, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Debug, Clone, Hash, Serialize_repr, Deserialize_repr, Eq, PartialEq)] #[repr(u8)] -pub enum AuthType { +pub enum Authenticator { /// It's a local server, we do fake sign in default. Local = 0, /// Currently not supported. It will be supported in the future when the /// [AppFlowy-Server](https://github.com/AppFlowy-IO/AppFlowy-Server) ready. AppFlowyCloud = 1, + /// It uses Supabase as the backend. + Supabase = 2, } -impl Display for AuthType { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AuthType::Local => write!(f, "Local"), - AuthType::AppFlowyCloud => write!(f, "AppFlowyCloud"), - } - } -} - -impl Default for AuthType { +impl Default for Authenticator { fn default() -> Self { Self::Local } } -impl AuthType { +impl Authenticator { pub fn is_local(&self) -> bool { - matches!(self, AuthType::Local) + matches!(self, Authenticator::Local) } pub fn is_appflowy_cloud(&self) -> bool { - matches!(self, AuthType::AppFlowyCloud) + matches!(self, Authenticator::AppFlowyCloud) } } -impl From<i32> for AuthType { +impl From<i32> for Authenticator { fn from(value: i32) -> Self { match value { - 0 => AuthType::Local, - 1 => AuthType::AppFlowyCloud, - _ => AuthType::Local, + 0 => Authenticator::Local, + 1 => Authenticator::AppFlowyCloud, + 2 => Authenticator::Supabase, + _ => Authenticator::Local, } } } @@ -326,7 +384,7 @@ pub enum UserTokenState { } // Workspace Role -#[derive(Clone, Copy, Debug, Serialize_repr, Deserialize_repr, Eq, PartialEq)] +#[derive(Clone, Debug, Serialize_repr, Deserialize_repr)] #[repr(u8)] pub enum Role { Owner = 0, @@ -355,16 +413,6 @@ impl From<Role> for i32 { } } -impl From<AFRole> for Role { - fn from(value: AFRole) -> Self { - match value { - AFRole::Owner => Role::Owner, - AFRole::Member => Role::Member, - AFRole::Guest => Role::Guest, - } - } -} - pub struct WorkspaceMember { pub email: String, pub role: Role, @@ -396,3 +444,29 @@ pub struct WorkspaceInvitation { pub status: WorkspaceInvitationStatus, pub updated_at: DateTime<Utc>, } + +pub enum RecurringInterval { + Month, + Year, +} + +pub enum SubscriptionPlan { + None, + Pro, + Team, +} + +pub struct WorkspaceSubscription { + pub workspace_id: String, + pub subscription_plan: SubscriptionPlan, + pub recurring_interval: RecurringInterval, + pub is_active: bool, + pub canceled_at: Option<i64>, +} + +pub struct WorkspaceUsage { + pub member_count: usize, + pub member_count_limit: usize, + pub total_blob_bytes: usize, + pub total_blob_bytes_limit: usize, +} diff --git a/frontend/rust-lib/flowy-user-pub/src/lib.rs b/frontend/rust-lib/flowy-user-pub/src/lib.rs index 773ae96a9a..2e51ecc626 100644 --- a/frontend/rust-lib/flowy-user-pub/src/lib.rs +++ b/frontend/rust-lib/flowy-user-pub/src/lib.rs @@ -1,7 +1,6 @@ pub mod cloud; pub mod entities; pub mod session; -pub mod sql; pub mod workspace_service; pub const DEFAULT_USER_NAME: fn() -> String = || "Me".to_string(); diff --git a/frontend/rust-lib/flowy-user-pub/src/session.rs b/frontend/rust-lib/flowy-user-pub/src/session.rs index 83a5670ddb..82306e6e72 100644 --- a/frontend/rust-lib/flowy-user-pub/src/session.rs +++ b/frontend/rust-lib/flowy-user-pub/src/session.rs @@ -1,4 +1,4 @@ -use crate::entities::{AuthType, UserAuthResponse, UserWorkspace}; +use crate::entities::{UserAuthResponse, UserWorkspace}; use base64::engine::general_purpose::STANDARD; use base64::Engine; use chrono::Utc; @@ -73,11 +73,8 @@ impl<'de> Visitor<'de> for SessionVisitor { name: "My Workspace".to_string(), created_at: Utc::now(), // For historical reasons, the database_storage_id is constructed by the user_id. - workspace_database_id: STANDARD.encode(format!("{}:user:database", user_id)), + database_indexer_id: STANDARD.encode(format!("{}:user:database", user_id)), icon: "".to_owned(), - member_count: 1, - role: None, - workspace_type: AuthType::Local, }) } } diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs deleted file mode 100644 index 58ca65e732..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/member_sql.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::entities::{Role, WorkspaceMember}; -use diesel::{insert_into, RunQueryDsl}; -use flowy_error::FlowyResult; -use flowy_sqlite::schema::workspace_members_table; -use flowy_sqlite::schema::workspace_members_table::dsl; -use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods}; - -#[derive(Queryable, Insertable, AsChangeset, Debug, Clone)] -#[diesel(table_name = workspace_members_table)] -#[diesel(primary_key(email, workspace_id))] -pub struct WorkspaceMemberTable { - pub email: String, - pub role: i32, - pub name: String, - pub avatar_url: Option<String>, - pub uid: i64, - pub workspace_id: String, - pub updated_at: chrono::NaiveDateTime, -} - -impl From<WorkspaceMemberTable> for WorkspaceMember { - fn from(value: WorkspaceMemberTable) -> Self { - Self { - email: value.email, - role: Role::from(value.role), - name: value.name, - avatar_url: value.avatar_url, - } - } -} - -pub fn upsert_workspace_member<T: Into<WorkspaceMemberTable>>( - conn: &mut SqliteConnection, - member: T, -) -> FlowyResult<()> { - let member = member.into(); - - insert_into(workspace_members_table::table) - .values(&member) - .on_conflict(( - workspace_members_table::email, - workspace_members_table::workspace_id, - )) - .do_update() - .set(&member) - .execute(conn)?; - - Ok(()) -} - -pub fn select_workspace_member( - mut conn: DBConnection, - workspace_id: &str, - uid: i64, -) -> FlowyResult<WorkspaceMemberTable> { - let member = dsl::workspace_members_table - .filter(workspace_members_table::workspace_id.eq(workspace_id)) - .filter(workspace_members_table::uid.eq(uid)) - .first::<WorkspaceMemberTable>(&mut conn)?; - - Ok(member) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs b/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs deleted file mode 100644 index 2a5f7bf891..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod member_sql; -mod user_sql; -mod workspace_setting_sql; -mod workspace_sql; - -pub use member_sql::*; -pub use user_sql::*; -pub use workspace_setting_sql::*; -pub use workspace_sql::*; diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs deleted file mode 100644 index ca117300f2..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/user_sql.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::cloud::UserUpdate; -use crate::entities::{AuthType, Role, UpdateUserProfileParams, UserProfile, UserWorkspace}; -use crate::sql::{ - select_user_workspace, upsert_user_workspace, upsert_workspace_member, WorkspaceMemberTable, -}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_table; -use flowy_sqlite::{prelude::*, DBConnection, ExpressionMethods, RunQueryDsl}; -use tracing::trace; - -/// The order of the fields in the struct must be the same as the order of the fields in the table. -/// Check out the [schema.rs] for table schema. -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_table)] -pub struct UserTable { - pub(crate) id: String, - pub(crate) name: String, - pub(crate) icon_url: String, - pub(crate) token: String, - pub(crate) email: String, - pub(crate) auth_type: i32, - pub(crate) updated_at: i64, -} - -#[allow(deprecated)] -impl From<(UserProfile, AuthType)> for UserTable { - fn from(value: (UserProfile, AuthType)) -> Self { - let (user_profile, auth_type) = value; - UserTable { - id: user_profile.uid.to_string(), - name: user_profile.name, - #[allow(deprecated)] - icon_url: user_profile.icon_url, - token: user_profile.token, - email: user_profile.email, - auth_type: auth_type as i32, - updated_at: user_profile.updated_at, - } - } -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_table)] -pub struct UserTableChangeset { - pub id: String, - pub name: Option<String>, - pub email: Option<String>, - pub icon_url: Option<String>, - pub token: Option<String>, -} - -impl UserTableChangeset { - pub fn new(params: UpdateUserProfileParams) -> Self { - UserTableChangeset { - id: params.uid.to_string(), - name: params.name, - email: params.email, - icon_url: params.icon_url, - token: params.token, - } - } - - pub fn from_user_profile(user_profile: UserProfile) -> Self { - UserTableChangeset { - id: user_profile.uid.to_string(), - name: Some(user_profile.name), - email: Some(user_profile.email), - icon_url: Some(user_profile.icon_url), - token: Some(user_profile.token), - } - } -} - -impl From<UserUpdate> for UserTableChangeset { - fn from(value: UserUpdate) -> Self { - UserTableChangeset { - id: value.uid.to_string(), - name: value.name, - email: value.email, - ..Default::default() - } - } -} - -pub fn update_user_profile( - conn: &mut SqliteConnection, - changeset: UserTableChangeset, -) -> Result<(), FlowyError> { - trace!("update user profile: {:?}", changeset); - let user_id = changeset.id.clone(); - update(user_table::dsl::user_table.filter(user_table::id.eq(&user_id))) - .set(changeset) - .execute(conn)?; - Ok(()) -} - -pub fn insert_local_workspace( - uid: i64, - workspace_id: &str, - workspace_name: &str, - conn: &mut SqliteConnection, -) -> FlowyResult<UserWorkspace> { - let user_workspace = UserWorkspace::new_local(workspace_id.to_string(), workspace_name); - conn.immediate_transaction(|conn| { - let row = select_user_table_row(uid, conn)?; - let row = WorkspaceMemberTable { - email: row.email, - role: Role::Owner as i32, - name: row.name, - avatar_url: Some(row.icon_url), - uid, - workspace_id: workspace_id.to_string(), - updated_at: chrono::Utc::now().naive_utc(), - }; - - upsert_user_workspace(uid, AuthType::Local, user_workspace.clone(), conn)?; - upsert_workspace_member(conn, row)?; - Ok::<_, FlowyError>(()) - })?; - - Ok(user_workspace) -} - -fn select_user_table_row(uid: i64, conn: &mut SqliteConnection) -> Result<UserTable, FlowyError> { - let row = user_table::dsl::user_table - .filter(user_table::id.eq(&uid.to_string())) - .first::<UserTable>(conn) - .map_err(|err| { - FlowyError::record_not_found().with_context(format!( - "Can't find the user profile for user id: {}, error: {:?}", - uid, err - )) - })?; - Ok(row) -} - -pub fn select_user_profile( - uid: i64, - workspace_id: &str, - conn: &mut SqliteConnection, -) -> Result<UserProfile, FlowyError> { - let workspace = select_user_workspace(workspace_id, conn)?; - let workspace_auth_type = AuthType::from(workspace.workspace_type); - let row = select_user_table_row(uid, conn)?; - - let user = UserProfile { - uid: row.id.parse::<i64>().unwrap_or(0), - email: row.email, - name: row.name, - token: row.token, - icon_url: row.icon_url, - auth_type: AuthType::from(row.auth_type), - workspace_auth_type, - updated_at: row.updated_at, - }; - - Ok(user) -} - -pub fn select_user_auth_type( - uid: i64, - conn: &mut SqliteConnection, -) -> Result<AuthType, FlowyError> { - let row = select_user_table_row(uid, conn)?; - Ok(AuthType::from(row.auth_type)) -} - -pub fn select_user_token(uid: i64, conn: &mut SqliteConnection) -> Result<String, FlowyError> { - let row = select_user_table_row(uid, conn)?; - Ok(row.token) -} - -pub fn upsert_user(user: UserTable, mut conn: DBConnection) -> FlowyResult<()> { - conn.immediate_transaction(|conn| { - // delete old user if exists - diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) - .execute(conn)?; - - let _ = diesel::insert_into(user_table::table) - .values(user) - .execute(conn)?; - Ok::<(), FlowyError>(()) - })?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs deleted file mode 100644 index 7eeafaf1e4..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_setting_sql.rs +++ /dev/null @@ -1,72 +0,0 @@ -use client_api::entity::AFWorkspaceSettings; -use flowy_error::FlowyError; -use flowy_sqlite::schema::workspace_setting_table; -use flowy_sqlite::schema::workspace_setting_table::dsl; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{prelude::*, ExpressionMethods}; -use uuid::Uuid; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = workspace_setting_table)] -pub struct WorkspaceSettingsTable { - pub id: String, - pub disable_search_indexing: bool, - pub ai_model: String, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = workspace_setting_table)] -pub struct WorkspaceSettingsChangeset { - pub id: String, - pub disable_search_indexing: Option<bool>, - pub ai_model: Option<String>, -} - -impl WorkspaceSettingsTable { - pub fn from_workspace_settings(workspace_id: &Uuid, settings: &AFWorkspaceSettings) -> Self { - Self { - id: workspace_id.to_string(), - disable_search_indexing: settings.disable_search_indexing, - ai_model: settings.ai_model.clone(), - } - } -} - -pub fn update_workspace_setting( - conn: &mut DBConnection, - changeset: WorkspaceSettingsChangeset, -) -> Result<(), FlowyError> { - diesel::update(dsl::workspace_setting_table) - .filter(workspace_setting_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(conn)?; - Ok(()) -} - -/// Upserts a workspace setting into the database. -pub fn upsert_workspace_setting( - conn: &mut SqliteConnection, - settings: WorkspaceSettingsTable, -) -> Result<(), FlowyError> { - diesel::insert_into(dsl::workspace_setting_table) - .values(settings.clone()) - .on_conflict(workspace_setting_table::id) - .do_update() - .set(( - workspace_setting_table::disable_search_indexing.eq(settings.disable_search_indexing), - workspace_setting_table::ai_model.eq(settings.ai_model), - )) - .execute(conn)?; - Ok(()) -} - -/// Selects a workspace setting by id from the database. -pub fn select_workspace_setting( - conn: &mut SqliteConnection, - workspace_id: &str, -) -> Result<WorkspaceSettingsTable, FlowyError> { - let setting = dsl::workspace_setting_table - .filter(workspace_setting_table::id.eq(workspace_id)) - .first::<WorkspaceSettingsTable>(conn)?; - Ok(setting) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs b/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs deleted file mode 100644 index 27f63e3de6..0000000000 --- a/frontend/rust-lib/flowy-user-pub/src/sql/workspace_sql.rs +++ /dev/null @@ -1,265 +0,0 @@ -use crate::entities::{AuthType, UserWorkspace}; -use chrono::{TimeZone, Utc}; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_sqlite::schema::user_workspace_table; -use flowy_sqlite::schema::user_workspace_table::dsl; -use flowy_sqlite::DBConnection; -use flowy_sqlite::{prelude::*, ExpressionMethods, RunQueryDsl, SqliteConnection}; -use std::collections::{HashMap, HashSet}; -use tracing::{info, warn}; - -#[derive(Clone, Default, Queryable, Identifiable, Insertable)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceTable { - pub id: String, - pub name: String, - pub uid: i64, - pub created_at: i64, - pub database_storage_id: String, - pub icon: String, - pub member_count: i64, - pub role: Option<i32>, - pub workspace_type: i32, -} - -#[derive(AsChangeset, Identifiable, Default, Debug)] -#[diesel(table_name = user_workspace_table)] -pub struct UserWorkspaceChangeset { - pub id: String, - pub name: Option<String>, - pub icon: Option<String>, - pub role: Option<i32>, - pub member_count: Option<i64>, -} - -impl UserWorkspaceChangeset { - pub fn has_changes(&self) -> bool { - self.name.is_some() || self.icon.is_some() || self.role.is_some() || self.member_count.is_some() - } - pub fn from_version(old: &UserWorkspace, new: &UserWorkspace) -> Self { - let mut changeset = Self { - id: new.id.clone(), - name: None, - icon: None, - role: None, - member_count: None, - }; - - if old.name != new.name { - changeset.name = Some(new.name.clone()); - } - if old.icon != new.icon { - changeset.icon = Some(new.icon.clone()); - } - if old.role != new.role { - changeset.role = new.role.map(|v| v as i32); - } - if old.member_count != new.member_count { - changeset.member_count = Some(new.member_count); - } - - changeset - } -} - -impl UserWorkspaceTable { - pub fn from_workspace( - uid_val: i64, - workspace: &UserWorkspace, - auth_type: AuthType, - ) -> Result<Self, FlowyError> { - if workspace.id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The id is empty")); - } - if workspace.workspace_database_id.is_empty() { - return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); - } - - Ok(Self { - id: workspace.id.clone(), - name: workspace.name.clone(), - uid: uid_val, - created_at: workspace.created_at.timestamp(), - database_storage_id: workspace.workspace_database_id.clone(), - icon: workspace.icon.clone(), - member_count: workspace.member_count, - role: workspace.role.map(|v| v as i32), - workspace_type: auth_type as i32, - }) - } -} - -pub fn select_user_workspace( - workspace_id: &str, - conn: &mut SqliteConnection, -) -> FlowyResult<UserWorkspaceTable> { - let row = dsl::user_workspace_table - .filter(user_workspace_table::id.eq(workspace_id)) - .first::<UserWorkspaceTable>(conn)?; - Ok(row) -} - -pub fn select_all_user_workspace( - uid: i64, - conn: &mut SqliteConnection, -) -> Result<Vec<UserWorkspace>, FlowyError> { - let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .order(user_workspace_table::created_at.desc()) - .load::<UserWorkspaceTable>(conn)?; - Ok(rows.into_iter().map(UserWorkspace::from).collect()) -} - -pub fn update_user_workspace( - mut conn: DBConnection, - changeset: UserWorkspaceChangeset, -) -> Result<(), FlowyError> { - diesel::update(user_workspace_table::dsl::user_workspace_table) - .filter(user_workspace_table::id.eq(changeset.id.clone())) - .set(changeset) - .execute(&mut conn)?; - - Ok(()) -} - -pub fn delete_user_workspace(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { - let n = conn.immediate_transaction(|conn| { - let rows_affected: usize = - diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) - .execute(conn)?; - Ok::<usize, FlowyError>(rows_affected) - })?; - - if n != 1 { - warn!("expected to delete 1 row, but deleted {} rows", n); - } - Ok(()) -} - -impl From<UserWorkspaceTable> for UserWorkspace { - fn from(value: UserWorkspaceTable) -> Self { - Self { - id: value.id, - name: value.name, - created_at: Utc - .timestamp_opt(value.created_at, 0) - .single() - .unwrap_or_default(), - workspace_database_id: value.database_storage_id, - icon: value.icon, - member_count: value.member_count, - role: value.role.map(|v| v.into()), - workspace_type: AuthType::from(value.workspace_type), - } - } -} - -/// Delete all user workspaces for the given user and auth type. -pub fn delete_user_all_workspace( - uid: i64, - auth_type: AuthType, - conn: &mut SqliteConnection, -) -> FlowyResult<()> { - let n = diesel::delete( - dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .filter(user_workspace_table::workspace_type.eq(auth_type as i32)), - ) - .execute(conn)?; - info!( - "Delete {} workspaces for user {} and auth type {:?}", - n, uid, auth_type - ); - Ok(()) -} - -#[derive(Debug)] -pub enum WorkspaceChange { - Inserted(String), - Updated(String), -} - -pub fn upsert_user_workspace( - uid_val: i64, - auth_type: AuthType, - user_workspace: UserWorkspace, - conn: &mut SqliteConnection, -) -> Result<usize, FlowyError> { - let row = UserWorkspaceTable::from_workspace(uid_val, &user_workspace, auth_type)?; - let n = insert_into(user_workspace_table::table) - .values(row.clone()) - .on_conflict(user_workspace_table::id) - .do_update() - .set(( - user_workspace_table::name.eq(row.name), - user_workspace_table::uid.eq(row.uid), - user_workspace_table::created_at.eq(row.created_at), - user_workspace_table::database_storage_id.eq(row.database_storage_id), - user_workspace_table::icon.eq(row.icon), - user_workspace_table::member_count.eq(row.member_count), - user_workspace_table::role.eq(row.role), - )) - .execute(conn)?; - - Ok(n) -} - -pub fn sync_user_workspaces_with_diff( - uid_val: i64, - auth_type: AuthType, - user_workspaces: &[UserWorkspace], - conn: &mut SqliteConnection, -) -> FlowyResult<Vec<WorkspaceChange>> { - let diff = conn.immediate_transaction(|conn| { - // 1) Load all existing workspaces into a map - let existing_rows: Vec<UserWorkspaceTable> = dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid_val)) - .filter(user_workspace_table::workspace_type.eq(auth_type as i32)) - .load(conn)?; - let mut existing_map: HashMap<String, UserWorkspaceTable> = existing_rows - .into_iter() - .map(|r| (r.id.clone(), r)) - .collect(); - - // 2) Build incoming ID set and delete any stale ones - let incoming_ids: HashSet<String> = user_workspaces.iter().map(|uw| uw.id.clone()).collect(); - let to_delete: Vec<String> = existing_map - .keys() - .filter(|id| !incoming_ids.contains(*id)) - .cloned() - .collect(); - - if !to_delete.is_empty() { - diesel::delete(dsl::user_workspace_table.filter(user_workspace_table::id.eq_any(&to_delete))) - .execute(conn)?; - } - - // 3) For each incoming workspace, either INSERT or UPDATE if changed - let mut diffs = Vec::new(); - for uw in user_workspaces { - match existing_map.remove(&uw.id) { - None => { - // new workspace → insert - let new_row = UserWorkspaceTable::from_workspace(uid_val, uw, auth_type)?; - diesel::insert_into(user_workspace_table::table) - .values(new_row) - .execute(conn)?; - diffs.push(WorkspaceChange::Inserted(uw.id.clone())); - }, - - Some(old) => { - let changes = UserWorkspaceChangeset::from_version(&UserWorkspace::from(old), uw); - if changes.has_changes() { - diesel::update(dsl::user_workspace_table.find(&uw.id)) - .set(&changes) - .execute(conn)?; - diffs.push(WorkspaceChange::Updated(uw.id.clone())); - } - }, - } - } - - Ok::<_, FlowyError>(diffs) - })?; - Ok(diff) -} diff --git a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs index 84185d310f..d588945618 100644 --- a/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs +++ b/frontend/rust-lib/flowy-user-pub/src/workspace_service.rs @@ -1,24 +1,16 @@ -use collab_folder::hierarchy_builder::ParentChildViews; use flowy_error::FlowyResult; -use flowy_folder_pub::entities::ImportFrom; +use flowy_folder_pub::folder_builder::ParentChildViews; use lib_infra::async_trait::async_trait; use std::collections::HashMap; -use uuid::Uuid; #[async_trait] pub trait UserWorkspaceService: Send + Sync { - async fn import_views( - &self, - source: &ImportFrom, - views: Vec<ParentChildViews>, - orphan_views: Vec<ParentChildViews>, - parent_view_id: Option<String>, - ) -> FlowyResult<()>; - async fn import_database_views( + async fn did_import_views(&self, views: Vec<ParentChildViews>) -> FlowyResult<()>; + async fn did_import_database_views( &self, ids_by_database_id: HashMap<String, Vec<String>>, ) -> FlowyResult<()>; /// Removes local indexes when a workspace is left/deleted - async fn did_delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()>; + fn did_delete_workspace(&self, workspace_id: String) -> FlowyResult<()>; } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index 65be4cc3f9..0d53b9f6b7 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -8,9 +8,10 @@ edition = "2018" [dependencies] flowy-derive.workspace = true flowy-sqlite = { workspace = true } +flowy-encrypt = { workspace = true } flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_sqlite", "impl_from_collab_folder", "impl_from_collab_persistence", "impl_from_collab_document"] } flowy-folder-pub = { workspace = true } -lib-infra = { workspace = true, features = ["encryption"] } +lib-infra = { workspace = true } flowy-notification = { workspace = true } flowy-server-pub = { workspace = true } lib-dispatch = { workspace = true } @@ -23,17 +24,18 @@ collab-user = { workspace = true } collab-entity = { workspace = true } collab-plugins = { workspace = true } flowy-user-pub = { workspace = true } -client-api = { workspace = true } anyhow.workspace = true -arc-swap.workspace = true -dashmap.workspace = true tracing.workspace = true bytes.workspace = true -serde = { workspace = true, features = ["rc"] } +serde.workspace = true serde_json.workspace = true +serde_repr.workspace = true protobuf.workspace = true lazy_static = "1.4.0" diesel.workspace = true +diesel_derives = { version = "2.1.0", features = ["sqlite", "r2d2"] } +once_cell = "1.17.1" +parking_lot.workspace = true strum = "0.25" strum_macros = "0.25.2" tokio = { workspace = true, features = ["rt"] } @@ -45,9 +47,9 @@ base64 = "^0.21" tokio-stream = "0.1.14" semver = "1.0.22" validator = { workspace = true, features = ["derive"] } -rayon = "1.10.0" [dev-dependencies] +nanoid = "0.4.0" fake = "2.0.0" rand = "0.8.4" quickcheck = "1.0.3" @@ -56,6 +58,7 @@ quickcheck_macros = "1.0" [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] [build-dependencies] flowy-codegen.workspace = true diff --git a/frontend/rust-lib/flowy-user/build.rs b/frontend/rust-lib/flowy-user/build.rs index 77c0c8125b..e015eb2580 100644 --- a/frontend/rust-lib/flowy-user/build.rs +++ b/frontend/rust-lib/flowy-user/build.rs @@ -4,4 +4,20 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } + + #[cfg(feature = "tauri_ts")] + { + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } } diff --git a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs new file mode 100644 index 0000000000..a7adcbe803 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs @@ -0,0 +1,502 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::Arc; + +use anyhow::anyhow; +use collab::core::collab::{DataSource, MutexCollab}; +use collab::core::origin::{CollabClient, CollabOrigin}; +use collab::preclude::Collab; +use collab_database::database::{ + is_database_collab, mut_database_views_with_collab, reset_inline_view_id, +}; +use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; +use collab_database::workspace_database::DatabaseMetaList; +use collab_folder::{Folder, UserId}; +use collab_plugins::local_storage::kv::KVTransactionDB; +use parking_lot::{Mutex, RwLock}; +use tracing::info; + +use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::cloud::gen_view_id; + +use crate::migrations::AnonUser; +use flowy_user_pub::session::Session; + +/// Migration the collab objects of the old user to new user. Currently, it only happens when +/// the user is a local user and try to use AppFlowy cloud service. +pub fn migration_anon_user_on_sign_up( + old_user: &AnonUser, + old_collab_db: &Arc<CollabKVDB>, + new_user_session: &Session, + new_collab_db: &Arc<CollabKVDB>, +) -> FlowyResult<()> { + new_collab_db + .with_write_txn(|new_collab_w_txn| { + let old_collab_r_txn = old_collab_db.read_txn(); + let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); + + migrate_user_awareness( + old_to_new_id_map.lock().deref_mut(), + old_user, + new_user_session, + )?; + + migrate_database_with_views_object( + &mut old_to_new_id_map.lock(), + old_user, + &old_collab_r_txn, + new_user_session, + new_collab_w_txn, + )?; + + let mut object_ids = old_collab_r_txn + .get_all_docs() + .map(|iter| iter.collect::<Vec<String>>()) + .unwrap_or_default(); + + // Migration of all objects except the folder and database_with_views + object_ids.retain(|id| { + id != &old_user.session.user_workspace.id + && id != &old_user.session.user_workspace.database_indexer_id + }); + + info!("migrate collab objects: {:?}", object_ids.len()); + let collab_by_oid = make_collab_by_oid(old_user, &old_collab_r_txn, &object_ids); + migrate_databases( + &old_to_new_id_map, + new_user_session, + new_collab_w_txn, + &mut object_ids, + &collab_by_oid, + )?; + + // Migrates the folder, replacing all existing view IDs with new ones. + // This function handles the process of migrating folder data between two users. As a part of this migration, + // all existing view IDs associated with the old user will be replaced by new IDs relevant to the new user. + migrate_workspace_folder( + &mut old_to_new_id_map.lock(), + old_user, + &old_collab_r_txn, + new_user_session, + new_collab_w_txn, + )?; + + // Migrate other collab objects + for object_id in &object_ids { + if let Some(collab) = collab_by_oid.get(object_id) { + let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + tracing::debug!("migrate from: {}, to: {}", object_id, new_object_id,); + migrate_collab_object( + collab, + new_user_session.user_id, + &new_object_id, + new_collab_w_txn, + ); + } + } + + Ok(()) + }) + .map_err(|err| FlowyError::new(ErrorCode::Internal, err))?; + + Ok(()) +} + +#[derive(Default)] +pub struct OldToNewIdMap(HashMap<String, String>); + +impl OldToNewIdMap { + fn new() -> Self { + Self::default() + } + fn exchange_new_id(&mut self, old_id: &str) -> String { + let view_id = self + .0 + .entry(old_id.to_string()) + .or_insert(gen_view_id().to_string()); + (*view_id).clone() + } +} + +impl Deref for OldToNewIdMap { + type Target = HashMap<String, String>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OldToNewIdMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +fn migrate_database_with_views_object<'a, 'b, W, R>( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &AnonUser, + old_collab_r_txn: &R, + new_user_session: &Session, + new_collab_w_txn: &W, +) -> Result<(), PersistenceError> +where + 'a: 'b, + W: CollabKVAction<'a>, + R: CollabKVAction<'b>, + PersistenceError: From<W::Error>, + PersistenceError: From<R::Error>, +{ + let database_with_views_collab = Collab::new( + old_user.session.user_id, + &old_user.session.user_workspace.database_indexer_id, + "phantom", + vec![], + false, + ); + database_with_views_collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc_with_txn( + old_user.session.user_id, + &old_user.session.user_workspace.database_indexer_id, + txn, + ) + })?; + + let new_uid = new_user_session.user_id; + let new_object_id = &new_user_session.user_workspace.database_indexer_id; + + let array = DatabaseMetaList::from_collab(&database_with_views_collab); + for database_meta in array.get_all_database_meta() { + array.update_database(&database_meta.database_id, |update| { + let new_linked_views = update + .linked_views + .iter() + .map(|view_id| old_to_new_id_map.exchange_new_id(view_id)) + .collect(); + update.database_id = old_to_new_id_map.exchange_new_id(&update.database_id); + update.linked_views = new_linked_views; + }) + } + + let txn = database_with_views_collab.transact(); + if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_object_id, &txn) { + tracing::error!("🔴migrate database storage failed: {:?}", err); + } + drop(txn); + Ok(()) +} + +fn migrate_collab_object<'a, W>(collab: &Collab, new_uid: i64, new_object_id: &str, w_txn: &'a W) +where + W: CollabKVAction<'a>, + PersistenceError: From<W::Error>, +{ + let txn = collab.transact(); + if let Err(err) = w_txn.create_new_doc(new_uid, &new_object_id, &txn) { + tracing::error!("🔴migrate collab failed: {:?}", err); + } +} + +fn migrate_workspace_folder<'a, 'b, W, R>( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &AnonUser, + old_collab_r_txn: &R, + new_user_session: &Session, + new_collab_w_txn: &W, +) -> Result<(), PersistenceError> +where + 'a: 'b, + W: CollabKVAction<'a>, + R: CollabKVAction<'b>, + PersistenceError: From<W::Error>, + PersistenceError: From<R::Error>, +{ + let old_uid = old_user.session.user_id; + let old_workspace_id = &old_user.session.user_workspace.id; + let new_uid = new_user_session.user_id; + let new_workspace_id = &new_user_session.user_workspace.id; + + let old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![], false); + old_folder_collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc_with_txn(old_uid, old_workspace_id, txn) + })?; + let old_user_id = UserId::from(old_uid); + let old_folder = Folder::open( + old_user_id.clone(), + Arc::new(MutexCollab::new(old_folder_collab)), + None, + ) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; + let mut folder_data = + old_folder + .get_folder_data(old_workspace_id) + .ok_or(PersistenceError::Internal(anyhow!( + "Can't migrate the folder data" + )))?; + + if let Some(old_fav_map) = folder_data.favorites.remove(&old_user_id) { + let fav_map = old_fav_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.exchange_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.favorites.insert(UserId::from(new_uid), fav_map); + } + if let Some(old_trash_map) = folder_data.trash.remove(&old_user_id) { + let trash_map = old_trash_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.exchange_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.trash.insert(UserId::from(new_uid), trash_map); + } + + if let Some(old_recent_map) = folder_data.recent.remove(&old_user_id) { + let recent_map = old_recent_map + .into_iter() + .map(|mut item| { + let new_view_id = old_to_new_id_map.exchange_new_id(&item.id); + item.id = new_view_id; + item + }) + .collect(); + folder_data.recent.insert(UserId::from(new_uid), recent_map); + } + + old_to_new_id_map + .0 + .insert(old_workspace_id.to_string(), new_workspace_id.to_string()); + + // 1. Replace the workspace views id to new id + folder_data.workspace.id = new_workspace_id.clone(); + folder_data + .workspace + .child_views + .iter_mut() + .for_each(|view_identifier| { + view_identifier.id = old_to_new_id_map.exchange_new_id(&view_identifier.id); + }); + + folder_data.views.iter_mut().for_each(|view| { + // 2. replace the old parent view id of the view + view.parent_view_id = old_to_new_id_map.exchange_new_id(&view.parent_view_id); + + // 3. replace the old id of the view + view.id = old_to_new_id_map.exchange_new_id(&view.id); + + // 4. replace the old id of the children views + view.children.iter_mut().for_each(|view_identifier| { + view_identifier.id = old_to_new_id_map.exchange_new_id(&view_identifier.id); + }); + }); + + match old_to_new_id_map.get(&folder_data.current_view) { + Some(new_view_id) => { + folder_data.current_view = new_view_id.clone(); + }, + None => { + tracing::error!("🔴migrate folder: current view id not found"); + folder_data.current_view = "".to_string(); + }, + } + + let origin = CollabOrigin::Client(CollabClient::new(new_uid, "phantom")); + let new_folder_collab = + Collab::new_with_source(origin, new_workspace_id, DataSource::Disk, vec![], false) + .map_err(|err| PersistenceError::Internal(err.into()))?; + let mutex_collab = Arc::new(MutexCollab::new(new_folder_collab)); + let new_user_id = UserId::from(new_uid); + info!("migrated folder: {:?}", folder_data); + let _ = Folder::create(new_user_id, mutex_collab.clone(), None, folder_data); + + { + let mutex_collab = mutex_collab.lock(); + let txn = mutex_collab.transact(); + if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_workspace_id, &txn) { + tracing::error!("🔴migrate folder failed: {:?}", err); + } + } + Ok(()) +} + +fn migrate_user_awareness( + old_to_new_id_map: &mut OldToNewIdMap, + old_user: &AnonUser, + new_user_session: &Session, +) -> Result<(), PersistenceError> { + let old_uid = old_user.session.user_id; + let new_uid = new_user_session.user_id; + tracing::debug!("migrate user awareness from: {}, to: {}", old_uid, new_uid); + old_to_new_id_map.insert(old_uid.to_string(), new_uid.to_string()); + Ok(()) +} + +fn migrate_databases<'a, W>( + old_to_new_id_map: &Arc<Mutex<OldToNewIdMap>>, + new_user_session: &Session, + new_collab_w_txn: &'a W, + object_ids: &mut Vec<String>, + collab_by_oid: &HashMap<String, Collab>, +) -> Result<(), PersistenceError> +where + W: CollabKVAction<'a>, + PersistenceError: From<W::Error>, +{ + // Migrate databases + let mut database_object_ids = vec![]; + let imported_database_row_object_ids: RwLock<HashMap<String, HashSet<String>>> = + RwLock::new(HashMap::new()); + + for object_id in &mut *object_ids { + if let Some(collab) = collab_by_oid.get(object_id) { + if !is_database_collab(collab) { + continue; + } + + database_object_ids.push(object_id.clone()); + reset_inline_view_id(collab, |old_inline_view_id| { + old_to_new_id_map + .lock() + .exchange_new_id(&old_inline_view_id) + }); + + mut_database_views_with_collab(collab, |database_view| { + let old_database_id = database_view.database_id.clone(); + let new_view_id = old_to_new_id_map.lock().exchange_new_id(&database_view.id); + let new_database_id = old_to_new_id_map + .lock() + .exchange_new_id(&database_view.database_id); + + tracing::trace!( + "migrate database view id from: {}, to: {}", + database_view.id, + new_view_id, + ); + tracing::trace!( + "migrate database view database id from: {}, to: {}", + database_view.database_id, + new_database_id, + ); + + database_view.id = new_view_id; + database_view.database_id = new_database_id; + database_view.row_orders.iter_mut().for_each(|row_order| { + let old_row_id = String::from(row_order.id.clone()); + let old_row_document_id = database_row_document_id_from_row_id(&old_row_id); + let new_row_id = old_to_new_id_map.lock().exchange_new_id(&old_row_id); + let new_row_document_id = database_row_document_id_from_row_id(&new_row_id); + tracing::debug!("migrate row id: {} to {}", row_order.id, new_row_id); + tracing::debug!( + "migrate row document id: {} to {}", + old_row_document_id, + new_row_document_id + ); + old_to_new_id_map + .lock() + .insert(old_row_document_id, new_row_document_id); + + row_order.id = RowId::from(new_row_id); + imported_database_row_object_ids + .write() + .entry(old_database_id.clone()) + .or_default() + .insert(old_row_id); + }); + }); + + let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + tracing::debug!( + "migrate database from: {}, to: {}", + object_id, + new_object_id, + ); + migrate_collab_object( + collab, + new_user_session.user_id, + &new_object_id, + new_collab_w_txn, + ); + } + } + + let imported_database_row_object_ids = imported_database_row_object_ids.read(); + // remove the database object ids from the object ids + object_ids.retain(|id| !database_object_ids.contains(id)); + + // remove database row object ids from the imported object ids + object_ids.retain(|id| { + !imported_database_row_object_ids + .values() + .flatten() + .any(|row_id| row_id == id) + }); + for (database_id, imported_row_ids) in &*imported_database_row_object_ids { + for imported_row_id in imported_row_ids { + if let Some(imported_collab) = collab_by_oid.get(imported_row_id) { + let new_database_id = old_to_new_id_map.lock().exchange_new_id(database_id); + let new_row_id = old_to_new_id_map.lock().exchange_new_id(imported_row_id); + info!( + "import database row from: {}, to: {}", + imported_row_id, new_row_id, + ); + mut_row_with_collab(imported_collab, |row_update| { + row_update.set_row_id(RowId::from(new_row_id.clone()), new_database_id.clone()); + }); + migrate_collab_object( + imported_collab, + new_user_session.user_id, + &new_row_id, + new_collab_w_txn, + ); + } + + // imported_collab_by_oid contains all the collab object ids, including the row document collab object ids. + // So, if the id exist in the imported_collab_by_oid, it means the row document collab object is exist. + let imported_row_document_id = database_row_document_id_from_row_id(imported_row_id); + if collab_by_oid.get(&imported_row_document_id).is_some() { + let _ = old_to_new_id_map + .lock() + .exchange_new_id(&imported_row_document_id); + } + } + } + + Ok(()) +} + +fn make_collab_by_oid<'a, R>( + old_user: &AnonUser, + old_collab_r_txn: &R, + object_ids: &[String], +) -> HashMap<String, Collab> +where + R: CollabKVAction<'a>, + PersistenceError: From<R::Error>, +{ + let mut collab_by_oid = HashMap::new(); + for object_id in object_ids { + let collab = Collab::new( + old_user.session.user_id, + object_id, + "migrate_device", + vec![], + false, + ); + match collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc_with_txn(old_user.session.user_id, &object_id, txn) + }) { + Ok(_) => { + collab_by_oid.insert(object_id.clone(), collab); + }, + Err(err) => tracing::error!("🔴Initialize migration collab failed: {:?} ", err), + } + } + + collab_by_oid +} diff --git a/frontend/rust-lib/flowy-user/src/anon_user/mod.rs b/frontend/rust-lib/flowy-user/src/anon_user/mod.rs new file mode 100644 index 0000000000..974850755f --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/anon_user/mod.rs @@ -0,0 +1,5 @@ +pub use migrate_anon_user_collab::*; +pub use sync_supabase_user_collab::*; + +mod migrate_anon_user_collab; +mod sync_supabase_user_collab; diff --git a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs new file mode 100644 index 0000000000..cee388e77b --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs @@ -0,0 +1,383 @@ +use std::future::Future; +use std::ops::Deref; +use std::pin::Pin; +use std::sync::Arc; + +use anyhow::{anyhow, Error}; +use collab::core::collab::MutexCollab; +use collab::preclude::Collab; +use collab_database::database::get_database_row_ids; +use collab_database::rows::database_row_document_id_from_row_id; +use collab_database::workspace_database::{get_all_database_meta, DatabaseMeta}; +use collab_entity::{CollabObject, CollabType}; +use collab_folder::{Folder, View, ViewLayout}; +use collab_plugins::local_storage::kv::KVTransactionDB; +use parking_lot::Mutex; + +use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; +use flowy_error::FlowyResult; +use flowy_user_pub::cloud::UserCloudService; +use flowy_user_pub::session::Session; + +#[tracing::instrument(level = "info", skip_all, err)] +pub async fn sync_supabase_user_data_to_cloud( + user_service: Arc<dyn UserCloudService>, + device_id: &str, + new_user_session: &Session, + collab_db: &Arc<CollabKVDB>, +) -> FlowyResult<()> { + let workspace_id = new_user_session.user_workspace.id.clone(); + let uid = new_user_session.user_id; + let folder = Arc::new( + sync_folder( + uid, + &workspace_id, + device_id, + collab_db, + user_service.clone(), + ) + .await?, + ); + + let database_records = sync_database_views( + uid, + &workspace_id, + device_id, + &new_user_session.user_workspace.database_indexer_id, + collab_db, + user_service.clone(), + ) + .await; + + let views = folder.lock().get_views_belong_to(&workspace_id); + for view in views { + let view_id = view.id.clone(); + if let Err(err) = sync_view( + uid, + folder.clone(), + database_records.clone(), + workspace_id.to_string(), + device_id.to_string(), + view, + collab_db.clone(), + user_service.clone(), + ) + .await + { + tracing::error!("🔴sync {} failed: {:?}", view_id, err); + } + } + tokio::task::yield_now().await; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn sync_view( + uid: i64, + folder: Arc<MutexFolder>, + database_metas: Vec<Arc<DatabaseMeta>>, + workspace_id: String, + device_id: String, + view: Arc<View>, + collab_db: Arc<CollabKVDB>, + user_service: Arc<dyn UserCloudService>, +) -> Pin<Box<dyn Future<Output = Result<(), Error>> + Send + Sync>> { + Box::pin(async move { + let collab_type = collab_type_from_view_layout(&view.layout); + let object_id = object_id_from_view(&view, &database_metas)?; + tracing::debug!( + "sync view: {:?}:{} with object_id: {}", + view.layout, + view.id, + object_id + ); + + let collab_object = CollabObject::new( + uid, + object_id, + collab_type, + workspace_id.to_string(), + device_id.clone(), + ); + + match view.layout { + ViewLayout::Document => { + let doc_state = get_collab_doc_state(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + doc_state.len() + ); + user_service + .create_collab_object(&collab_object, doc_state) + .await?; + }, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => { + let (database_doc_state, row_ids) = + get_database_doc_state(uid, &collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + collab_object, + database_doc_state.len() + ); + user_service + .create_collab_object(&collab_object, database_doc_state) + .await?; + + // sync database's row + for row_id in row_ids { + tracing::debug!("sync row: {}", row_id); + let document_id = database_row_document_id_from_row_id(&row_id); + + let database_row_collab_object = CollabObject::new( + uid, + row_id, + CollabType::DatabaseRow, + workspace_id.to_string(), + device_id.clone(), + ); + let database_row_doc_state = + get_collab_doc_state(uid, &database_row_collab_object, &collab_db)?; + tracing::info!( + "sync object: {} with update: {}", + database_row_collab_object, + database_row_doc_state.len() + ); + + let _ = user_service + .create_collab_object(&database_row_collab_object, database_row_doc_state) + .await; + + let database_row_document = CollabObject::new( + uid, + document_id, + CollabType::Document, + workspace_id.to_string(), + device_id.to_string(), + ); + // sync document in the row if exist + if let Ok(document_doc_state) = + get_collab_doc_state(uid, &database_row_document, &collab_db) + { + tracing::info!( + "sync database row document: {} with update: {}", + database_row_document, + document_doc_state.len() + ); + let _ = user_service + .create_collab_object(&database_row_document, document_doc_state) + .await; + } + } + }, + ViewLayout::Chat => {}, + } + + tokio::task::yield_now().await; + + let child_views = folder.lock().views.get_views_belong_to(&view.id); + for child_view in child_views { + let cloned_child_view = child_view.clone(); + if let Err(err) = Box::pin(sync_view( + uid, + folder.clone(), + database_metas.clone(), + workspace_id.clone(), + device_id.to_string(), + child_view, + collab_db.clone(), + user_service.clone(), + )) + .await + { + tracing::error!( + "🔴sync {:?}:{} failed: {:?}", + cloned_child_view.layout, + cloned_child_view.id, + err + ) + } + tokio::task::yield_now().await; + } + Ok(()) + }) +} + +fn get_collab_doc_state( + uid: i64, + collab_object: &CollabObject, + collab_db: &Arc<CollabKVDB>, +) -> Result<Vec<u8>, PersistenceError> { + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); + let _ = collab.with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc_with_txn(uid, &collab_object.object_id, txn) + })?; + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; + if doc_state.is_empty() { + return Err(PersistenceError::UnexpectedEmptyUpdates); + } + + Ok(doc_state.to_vec()) +} + +fn get_database_doc_state( + uid: i64, + collab_object: &CollabObject, + collab_db: &Arc<CollabKVDB>, +) -> Result<(Vec<u8>, Vec<String>), PersistenceError> { + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); + let _ = collab.with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc_with_txn(uid, &collab_object.object_id, txn) + })?; + + let row_ids = get_database_row_ids(&collab).unwrap_or_default(); + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; + if doc_state.is_empty() { + return Err(PersistenceError::UnexpectedEmptyUpdates); + } + + Ok((doc_state.to_vec(), row_ids)) +} + +async fn sync_folder( + uid: i64, + workspace_id: &str, + device_id: &str, + collab_db: &Arc<CollabKVDB>, + user_service: Arc<dyn UserCloudService>, +) -> Result<MutexFolder, Error> { + let (folder, update) = { + let collab = Collab::new(uid, workspace_id, "phantom", vec![], false); + // Use the temporary result to short the lifetime of the TransactionMut + collab.with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc_with_txn(uid, workspace_id, txn) + })?; + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; + ( + MutexFolder::new(Folder::open(uid, Arc::new(MutexCollab::new(collab)), None)?), + doc_state, + ) + }; + + let collab_object = CollabObject::new( + uid, + workspace_id.to_string(), + CollabType::Folder, + workspace_id.to_string(), + device_id.to_string(), + ); + tracing::info!( + "sync object: {} with update: {}", + collab_object, + update.len() + ); + if let Err(err) = user_service + .create_collab_object(&collab_object, update.to_vec()) + .await + { + tracing::error!("🔴sync folder failed: {:?}", err); + } + + Ok(folder) +} + +async fn sync_database_views( + uid: i64, + workspace_id: &str, + device_id: &str, + database_views_aggregate_id: &str, + collab_db: &Arc<CollabKVDB>, + user_service: Arc<dyn UserCloudService>, +) -> Vec<Arc<DatabaseMeta>> { + let collab_object = CollabObject::new( + uid, + database_views_aggregate_id.to_string(), + CollabType::WorkspaceDatabase, + workspace_id.to_string(), + device_id.to_string(), + ); + + // Use the temporary result to short the lifetime of the TransactionMut + let result = { + let collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![], false); + collab + .with_origin_transact_mut(|txn| { + collab_db + .read_txn() + .load_doc_with_txn(uid, database_views_aggregate_id, txn) + }) + .map(|_| { + ( + get_all_database_meta(&collab), + collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .unwrap() + .doc_state, + ) + }) + }; + + if let Ok((records, doc_state)) = result { + let _ = user_service + .create_collab_object(&collab_object, doc_state.to_vec()) + .await; + records.into_iter().map(Arc::new).collect() + } else { + vec![] + } +} + +struct MutexFolder(Mutex<Folder>); +impl MutexFolder { + pub fn new(folder: Folder) -> Self { + Self(Mutex::new(folder)) + } +} +impl Deref for MutexFolder { + type Target = Mutex<Folder>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} +unsafe impl Sync for MutexFolder {} +unsafe impl Send for MutexFolder {} + +fn collab_type_from_view_layout(view_layout: &ViewLayout) -> CollabType { + match view_layout { + ViewLayout::Document => CollabType::Document, + ViewLayout::Grid | ViewLayout::Board | ViewLayout::Calendar => CollabType::Database, + ViewLayout::Chat => CollabType::Unknown, + } +} + +fn object_id_from_view( + view: &Arc<View>, + database_records: &[Arc<DatabaseMeta>], +) -> Result<String, Error> { + if view.layout.is_database() { + match database_records + .iter() + .find(|record| record.linked_views.contains(&view.id)) + { + None => Err(anyhow!( + "🔴sync view: {} failed: no database for this view", + view.id + )), + Some(record) => Ok(record.database_id.clone()), + } + } else { + Ok(view.id.clone()) + } +} diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index a61ba5cc96..edad70387b 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -1,13 +1,12 @@ use std::collections::HashMap; use std::convert::TryInto; -use crate::entities::parser::*; -use crate::entities::AuthTypePB; -use crate::errors::ErrorCode; -use client_api::entity::GotrueTokenResponse; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_user_pub::entities::*; +use crate::entities::parser::*; +use crate::errors::ErrorCode; + #[derive(ProtoBuf, Default)] pub struct SignInPayloadPB { #[pb(index = 1)] @@ -20,7 +19,7 @@ pub struct SignInPayloadPB { pub name: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -31,10 +30,11 @@ impl TryInto<SignInParams> for SignInPayloadPB { fn try_into(self) -> Result<SignInParams, Self::Error> { let email = UserEmail::parse(self.email)?; + let password = UserPassword::parse(self.password)?; Ok(SignInParams { email: email.0, - password: self.password, + password: password.0, name: self.name, auth_type: self.auth_type.into(), }) @@ -53,7 +53,7 @@ pub struct SignUpPayloadPB { pub password: String, #[pb(index = 4)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, #[pb(index = 5)] pub device_id: String, @@ -64,13 +64,13 @@ impl TryInto<SignUpParams> for SignUpPayloadPB { fn try_into(self) -> Result<SignUpParams, Self::Error> { let email = UserEmail::parse(self.email)?; - let password = self.password; + let password = UserPassword::parse(self.password)?; let name = UserName::parse(self.name)?; Ok(SignUpParams { email: email.0, name: name.0, - password, + password: password.0, auth_type: self.auth_type.into(), device_id: self.device_id, }) @@ -86,53 +86,6 @@ pub struct MagicLinkSignInPB { pub redirect_to: String, } -#[derive(ProtoBuf, Default)] -pub struct PasscodeSignInPB { - #[pb(index = 1)] - pub email: String, - - #[pb(index = 2)] - pub passcode: String, -} - -#[derive(ProtoBuf, Default, Debug, Clone)] -pub struct GotrueTokenResponsePB { - #[pb(index = 1)] - pub access_token: String, - - #[pb(index = 2)] - pub token_type: String, - - #[pb(index = 3)] - pub expires_in: i64, - - #[pb(index = 4)] - pub expires_at: i64, - - #[pb(index = 5)] - pub refresh_token: String, - - #[pb(index = 6, one_of)] - pub provider_access_token: Option<String>, - - #[pb(index = 7, one_of)] - pub provider_refresh_token: Option<String>, -} - -impl From<GotrueTokenResponse> for GotrueTokenResponsePB { - fn from(response: GotrueTokenResponse) -> Self { - Self { - access_token: response.access_token, - token_type: response.token_type, - expires_in: response.expires_in, - expires_at: response.expires_at, - refresh_token: response.refresh_token, - provider_access_token: response.provider_access_token, - provider_refresh_token: response.provider_refresh_token, - } - } -} - #[derive(ProtoBuf, Default)] pub struct OauthSignInPB { /// Use this field to store the third party auth information. @@ -144,7 +97,7 @@ pub struct OauthSignInPB { pub map: HashMap<String, String>, #[pb(index = 2)] - pub authenticator: AuthTypePB, + pub authenticator: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -153,7 +106,7 @@ pub struct SignInUrlPayloadPB { pub email: String, #[pb(index = 2)] - pub authenticator: AuthTypePB, + pub authenticator: AuthenticatorPB, } #[derive(ProtoBuf, Default)] @@ -228,10 +181,87 @@ pub struct OauthProviderDataPB { pub oauth_url: String, } +#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] +pub enum AuthenticatorPB { + Local = 0, + Supabase = 1, + AppFlowyCloud = 2, +} + +impl From<Authenticator> for AuthenticatorPB { + fn from(auth_type: Authenticator) -> Self { + match auth_type { + Authenticator::Supabase => AuthenticatorPB::Supabase, + Authenticator::Local => AuthenticatorPB::Local, + Authenticator::AppFlowyCloud => AuthenticatorPB::AppFlowyCloud, + } + } +} + +impl From<AuthenticatorPB> for Authenticator { + fn from(pb: AuthenticatorPB) -> Self { + match pb { + AuthenticatorPB::Supabase => Authenticator::Supabase, + AuthenticatorPB::Local => Authenticator::Local, + AuthenticatorPB::AppFlowyCloud => Authenticator::AppFlowyCloud, + } + } +} + +impl Default for AuthenticatorPB { + fn default() -> Self { + Self::Local + } +} + +#[derive(Debug, ProtoBuf, Default)] +pub struct UserCredentialsPB { + #[pb(index = 1, one_of)] + pub uid: Option<i64>, + + #[pb(index = 2, one_of)] + pub uuid: Option<String>, + + #[pb(index = 3, one_of)] + pub token: Option<String>, +} + +impl UserCredentialsPB { + pub fn from_uid(uid: i64) -> Self { + Self { + uid: Some(uid), + uuid: None, + token: None, + } + } + + pub fn from_token(token: &str) -> Self { + Self { + uid: None, + uuid: None, + token: Some(token.to_owned()), + } + } + + pub fn from_uuid(uuid: &str) -> Self { + Self { + uid: None, + uuid: Some(uuid.to_owned()), + token: None, + } + } +} + +impl From<UserCredentialsPB> for UserCredentials { + fn from(value: UserCredentialsPB) -> Self { + Self::new(value.token, value.uid, value.uuid) + } +} + #[derive(Default, ProtoBuf)] pub struct UserStatePB { #[pb(index = 1)] - pub auth_type: AuthTypePB, + pub auth_type: AuthenticatorPB, } #[derive(ProtoBuf, Debug, Default, Clone)] diff --git a/frontend/rust-lib/flowy-user/src/entities/import_data.rs b/frontend/rust-lib/flowy-user/src/entities/import_data.rs index 687b65835e..023e3f9cfd 100644 --- a/frontend/rust-lib/flowy-user/src/entities/import_data.rs +++ b/frontend/rust-lib/flowy-user/src/entities/import_data.rs @@ -1,16 +1,12 @@ use flowy_derive::ProtoBuf; -use lib_infra::validator_fn::required_not_empty_str; use validator::Validate; #[derive(ProtoBuf, Validate, Default)] pub struct ImportAppFlowyDataPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub path: String, #[pb(index = 2, one_of)] pub import_container_name: Option<String>, - - #[pb(index = 3, one_of)] - pub parent_view_id: Option<String>, } diff --git a/frontend/rust-lib/flowy-user/src/entities/parser/user_email.rs b/frontend/rust-lib/flowy-user/src/entities/parser/user_email.rs index f9a1979c53..a3ec143317 100644 --- a/frontend/rust-lib/flowy-user/src/entities/parser/user_email.rs +++ b/frontend/rust-lib/flowy-user/src/entities/parser/user_email.rs @@ -1,5 +1,5 @@ use flowy_error::ErrorCode; -use validator::ValidateEmail; +use validator::validate_email; #[derive(Debug)] pub struct UserEmail(pub String); @@ -10,7 +10,7 @@ impl UserEmail { return Err(ErrorCode::EmailIsEmpty); } - if ValidateEmail::validate_email(&s) { + if validate_email(&s) { Ok(Self(s)) } else { Err(ErrorCode::EmailFormatInvalid) diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 77d92fb33a..80dcfd1b7f 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -1,14 +1,15 @@ -use super::AFRolePB; -use crate::entities::parser::{UserEmail, UserIcon, UserName}; -use crate::entities::AuthTypePB; -use crate::errors::ErrorCode; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_user_pub::entities::*; -use flowy_user_pub::sql::UserWorkspaceTable; -use lib_infra::validator_fn::required_not_empty_str; use std::convert::TryInto; use validator::Validate; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_user_pub::entities::*; + +use crate::entities::parser::{UserEmail, UserIcon, UserName, UserOpenaiKey, UserPassword}; +use crate::entities::AuthenticatorPB; +use crate::errors::ErrorCode; + +use super::parser::UserStabilityAIKey; + #[derive(Default, ProtoBuf)] pub struct UserTokenPB { #[pb(index = 1)] @@ -39,10 +40,22 @@ pub struct UserProfilePB { pub icon_url: String, #[pb(index = 6)] - pub user_auth_type: AuthTypePB, + pub openai_key: String, #[pb(index = 7)] - pub workspace_auth_type: AuthTypePB, + pub authenticator: AuthenticatorPB, + + #[pb(index = 8)] + pub encryption_sign: String, + + #[pb(index = 9)] + pub encryption_type: EncryptionTypePB, + + #[pb(index = 10)] + pub workspace_id: String, + + #[pb(index = 11)] + pub stability_ai_key: String, } #[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)] @@ -59,14 +72,22 @@ impl Default for EncryptionTypePB { impl From<UserProfile> for UserProfilePB { fn from(user_profile: UserProfile) -> Self { + let (encryption_sign, encryption_ty) = match user_profile.encryption_type { + EncryptionType::NoEncryption => ("".to_string(), EncryptionTypePB::NoEncryption), + EncryptionType::SelfEncryption(sign) => (sign, EncryptionTypePB::Symmetric), + }; Self { id: user_profile.uid, email: user_profile.email, name: user_profile.name, token: user_profile.token, icon_url: user_profile.icon_url, - user_auth_type: user_profile.auth_type.into(), - workspace_auth_type: user_profile.workspace_auth_type.into(), + openai_key: user_profile.openai_key, + authenticator: user_profile.authenticator.into(), + encryption_sign, + encryption_type: encryption_ty, + workspace_id: user_profile.workspace_id, + stability_ai_key: user_profile.stability_ai_key, } } } @@ -87,6 +108,12 @@ pub struct UpdateUserProfilePayloadPB { #[pb(index = 5, one_of)] pub icon_url: Option<String>, + + #[pb(index = 6, one_of)] + pub openai_key: Option<String>, + + #[pb(index = 7, one_of)] + pub stability_ai_key: Option<String>, } impl UpdateUserProfilePayloadPB { @@ -116,6 +143,16 @@ impl UpdateUserProfilePayloadPB { self.icon_url = Some(icon_url.to_owned()); self } + + pub fn openai_key(mut self, openai_key: &str) -> Self { + self.openai_key = Some(openai_key.to_owned()); + self + } + + pub fn stability_ai_key(mut self, stability_ai_key: &str) -> Self { + self.stability_ai_key = Some(stability_ai_key.to_owned()); + self + } } impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB { @@ -132,20 +169,36 @@ impl TryInto<UpdateUserProfileParams> for UpdateUserProfilePayloadPB { Some(email) => Some(UserEmail::parse(email)?.0), }; - let password = self.password; + let password = match self.password { + None => None, + Some(password) => Some(UserPassword::parse(password)?.0), + }; let icon_url = match self.icon_url { None => None, Some(icon_url) => Some(UserIcon::parse(icon_url)?.0), }; + let openai_key = match self.openai_key { + None => None, + Some(openai_key) => Some(UserOpenaiKey::parse(openai_key)?.0), + }; + + let stability_ai_key = match self.stability_ai_key { + None => None, + Some(stability_ai_key) => Some(UserStabilityAIKey::parse(stability_ai_key)?.0), + }; + Ok(UpdateUserProfileParams { uid: self.id, name, email, password, icon_url, + openai_key, + encryption_sign: None, token: None, + stability_ai_key, }) } } @@ -167,7 +220,7 @@ impl From<Vec<UserWorkspace>> for RepeatedUserWorkspacePB { #[derive(ProtoBuf, Default, Debug, Clone, Validate)] pub struct UserWorkspacePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -178,41 +231,15 @@ pub struct UserWorkspacePB { #[pb(index = 4)] pub icon: String, - - #[pb(index = 5)] - pub member_count: i64, - - #[pb(index = 6, one_of)] - pub role: Option<AFRolePB>, - - #[pb(index = 7)] - pub workspace_auth_type: AuthTypePB, } impl From<UserWorkspace> for UserWorkspacePB { - fn from(workspace: UserWorkspace) -> Self { - Self { - workspace_id: workspace.id, - name: workspace.name, - created_at_timestamp: workspace.created_at.timestamp(), - icon: workspace.icon, - member_count: workspace.member_count, - role: workspace.role.map(AFRolePB::from), - workspace_auth_type: AuthTypePB::from(workspace.workspace_type), - } - } -} - -impl From<UserWorkspaceTable> for UserWorkspacePB { - fn from(value: UserWorkspaceTable) -> Self { + fn from(value: UserWorkspace) -> Self { Self { workspace_id: value.id, name: value.name, - created_at_timestamp: value.created_at, + created_at_timestamp: value.created_at.timestamp(), icon: value.icon, - member_count: value.member_count, - role: value.role.map(AFRolePB::from), - workspace_auth_type: AuthTypePB::from(value.workspace_type), } } } diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs index 860bda3be7..d860385de0 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -1,14 +1,10 @@ -use client_api::entity::billing_dto::{ - Currency, RecurringInterval, SubscriptionPlan, SubscriptionPlanDetail, - WorkspaceSubscriptionStatus, WorkspaceUsageAndLimit, -}; -use serde::{Deserialize, Serialize}; use validator::Validate; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_user_pub::cloud::{AFWorkspaceSettings, AFWorkspaceSettingsChange}; -use flowy_user_pub::entities::{AuthType, Role, WorkspaceInvitation, WorkspaceMember}; -use flowy_user_pub::sql::WorkspaceSettingsTable; +use flowy_user_pub::entities::{ + RecurringInterval, Role, SubscriptionPlan, WorkspaceInvitation, WorkspaceMember, + WorkspaceSubscription, +}; use lib_infra::validator_fn::required_not_empty_str; #[derive(ProtoBuf, Default, Clone)] @@ -46,7 +42,7 @@ pub struct RepeatedWorkspaceMemberPB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct WorkspaceMemberInvitationPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -98,15 +94,14 @@ impl From<WorkspaceInvitation> for WorkspaceInvitationPB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct AcceptWorkspaceInvitationPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub invite_id: String, } -// Deprecated #[derive(ProtoBuf, Default, Clone, Validate)] pub struct AddWorkspaceMemberPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -117,14 +112,14 @@ pub struct AddWorkspaceMemberPB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct QueryWorkspacePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, } #[derive(ProtoBuf, Default, Clone, Validate)] pub struct RemoveWorkspaceMemberPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -135,7 +130,7 @@ pub struct RemoveWorkspaceMemberPB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct UpdateWorkspaceMemberPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -147,7 +142,7 @@ pub struct UpdateWorkspaceMemberPB { } // Workspace Role -#[derive(Debug, ProtoBuf_Enum, Clone, Default, Eq, PartialEq)] +#[derive(ProtoBuf_Enum, Clone, Default)] pub enum AFRolePB { Owner = 0, Member = 1, @@ -155,17 +150,6 @@ pub enum AFRolePB { Guest = 2, } -impl From<i32> for AFRolePB { - fn from(value: i32) -> Self { - match value { - 0 => AFRolePB::Owner, - 1 => AFRolePB::Member, - 2 => AFRolePB::Guest, - _ => AFRolePB::Guest, - } - } -} - impl From<AFRolePB> for Role { fn from(value: AFRolePB) -> Self { match value { @@ -189,43 +173,10 @@ impl From<Role> for AFRolePB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct UserWorkspaceIdPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, } -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct OpenUserWorkspacePB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - - #[pb(index = 2)] - pub workspace_auth_type: AuthTypePB, -} - -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct CancelWorkspaceSubscriptionPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - - #[pb(index = 2)] - pub plan: SubscriptionPlanPB, - - #[pb(index = 3)] - pub reason: String, -} - -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct SuccessWorkspaceSubscriptionPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - - #[pb(index = 2, one_of)] - pub plan: Option<SubscriptionPlanPB>, -} - #[derive(ProtoBuf, Default, Clone)] pub struct WorkspaceMemberIdPB { #[pb(index = 1)] @@ -235,64 +186,25 @@ pub struct WorkspaceMemberIdPB { #[derive(ProtoBuf, Default, Clone, Validate)] pub struct CreateWorkspacePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub name: String, - - #[pb(index = 2)] - pub auth_type: AuthTypePB, -} - -#[derive(ProtoBuf_Enum, Copy, Default, Debug, Clone, Eq, PartialEq)] -#[repr(u8)] -pub enum AuthTypePB { - #[default] - Local = 0, - Server = 1, -} - -impl From<i32> for AuthTypePB { - fn from(value: i32) -> Self { - match value { - 0 => AuthTypePB::Local, - 1 => AuthTypePB::Server, - _ => AuthTypePB::Server, - } - } -} - -impl From<AuthType> for AuthTypePB { - fn from(value: AuthType) -> Self { - match value { - AuthType::Local => AuthTypePB::Local, - AuthType::AppFlowyCloud => AuthTypePB::Server, - } - } -} - -impl From<AuthTypePB> for AuthType { - fn from(value: AuthTypePB) -> Self { - match value { - AuthTypePB::Local => AuthType::Local, - AuthTypePB::Server => AuthType::AppFlowyCloud, - } - } } #[derive(ProtoBuf, Default, Clone, Validate)] pub struct RenameWorkspacePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub new_name: String, } #[derive(ProtoBuf, Default, Clone, Validate)] pub struct ChangeWorkspaceIconPB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -302,7 +214,7 @@ pub struct ChangeWorkspaceIconPB { #[derive(ProtoBuf, Default, Clone, Validate, Debug)] pub struct SubscribeWorkspacePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] + #[validate(custom = "required_not_empty_str")] pub workspace_id: String, #[pb(index = 2)] @@ -315,7 +227,7 @@ pub struct SubscribeWorkspacePB { pub success_url: String, } -#[derive(ProtoBuf_Enum, Clone, Default, Debug, Serialize, Deserialize)] +#[derive(ProtoBuf_Enum, Clone, Default, Debug)] pub enum RecurringIntervalPB { #[default] Month = 0, @@ -340,26 +252,12 @@ impl From<RecurringInterval> for RecurringIntervalPB { } } -#[derive(ProtoBuf_Enum, Clone, Default, Debug, Serialize, Deserialize)] +#[derive(ProtoBuf_Enum, Clone, Default, Debug)] pub enum SubscriptionPlanPB { #[default] - Free = 0, + None = 0, Pro = 1, Team = 2, - - // Add-ons - AiMax = 3, - AiLocal = 4, -} - -impl From<WorkspacePlanPB> for SubscriptionPlanPB { - fn from(value: WorkspacePlanPB) -> Self { - match value { - WorkspacePlanPB::FreePlan => SubscriptionPlanPB::Free, - WorkspacePlanPB::ProPlan => SubscriptionPlanPB::Pro, - WorkspacePlanPB::TeamPlan => SubscriptionPlanPB::Team, - } - } } impl From<SubscriptionPlanPB> for SubscriptionPlan { @@ -367,9 +265,7 @@ impl From<SubscriptionPlanPB> for SubscriptionPlan { match value { SubscriptionPlanPB::Pro => SubscriptionPlan::Pro, SubscriptionPlanPB::Team => SubscriptionPlan::Team, - SubscriptionPlanPB::Free => SubscriptionPlan::Free, - SubscriptionPlanPB::AiMax => SubscriptionPlan::AiMax, - SubscriptionPlanPB::AiLocal => SubscriptionPlan::AiLocal, + SubscriptionPlanPB::None => SubscriptionPlan::None, } } } @@ -379,9 +275,7 @@ impl From<SubscriptionPlan> for SubscriptionPlanPB { match value { SubscriptionPlan::Pro => SubscriptionPlanPB::Pro, SubscriptionPlan::Team => SubscriptionPlanPB::Team, - SubscriptionPlan::Free => SubscriptionPlanPB::Free, - SubscriptionPlan::AiMax => SubscriptionPlanPB::AiMax, - SubscriptionPlan::AiLocal => SubscriptionPlanPB::AiLocal, + SubscriptionPlan::None => SubscriptionPlanPB::None, } } } @@ -393,220 +287,13 @@ pub struct PaymentLinkPB { } #[derive(Debug, ProtoBuf, Default, Clone)] -pub struct WorkspaceUsagePB { +pub struct RepeatedWorkspaceSubscriptionPB { #[pb(index = 1)] - pub member_count: u64, - #[pb(index = 2)] - pub member_count_limit: u64, - #[pb(index = 3)] - pub storage_bytes: u64, - #[pb(index = 4)] - pub storage_bytes_limit: u64, - #[pb(index = 5)] - pub storage_bytes_unlimited: bool, - #[pb(index = 6)] - pub ai_responses_count: u64, - #[pb(index = 7)] - pub ai_responses_count_limit: u64, - #[pb(index = 8)] - pub ai_responses_unlimited: bool, - #[pb(index = 9)] - pub local_ai: bool, -} - -impl From<WorkspaceUsageAndLimit> for WorkspaceUsagePB { - fn from(workspace_usage: WorkspaceUsageAndLimit) -> Self { - WorkspaceUsagePB { - member_count: workspace_usage.member_count as u64, - member_count_limit: workspace_usage.member_count_limit as u64, - storage_bytes: workspace_usage.storage_bytes as u64, - storage_bytes_limit: workspace_usage.storage_bytes_limit as u64, - storage_bytes_unlimited: workspace_usage.storage_bytes_unlimited, - ai_responses_count: workspace_usage.ai_responses_count as u64, - ai_responses_count_limit: workspace_usage.ai_responses_count_limit as u64, - ai_responses_unlimited: workspace_usage.ai_responses_unlimited, - local_ai: workspace_usage.local_ai, - } - } + pub items: Vec<WorkspaceSubscriptionPB>, } #[derive(Debug, ProtoBuf, Default, Clone)] -pub struct BillingPortalPB { - #[pb(index = 1)] - pub url: String, -} - -#[derive(ProtoBuf, Default, Clone, Validate, Eq, PartialEq)] -pub struct WorkspaceSettingsPB { - #[pb(index = 1)] - pub disable_search_indexing: bool, - - #[pb(index = 2)] - pub ai_model: String, -} - -impl From<&AFWorkspaceSettings> for WorkspaceSettingsPB { - fn from(value: &AFWorkspaceSettings) -> Self { - Self { - disable_search_indexing: value.disable_search_indexing, - ai_model: value.ai_model.clone(), - } - } -} - -impl From<WorkspaceSettingsTable> for WorkspaceSettingsPB { - fn from(value: WorkspaceSettingsTable) -> Self { - Self { - disable_search_indexing: value.disable_search_indexing, - ai_model: value.ai_model, - } - } -} - -#[derive(ProtoBuf, Default, Clone, Validate, Debug)] -pub struct UpdateUserWorkspaceSettingPB { - #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - - #[pb(index = 2, one_of)] - pub disable_search_indexing: Option<bool>, - - #[pb(index = 3, one_of)] - pub ai_model: Option<String>, -} - -impl From<UpdateUserWorkspaceSettingPB> for AFWorkspaceSettingsChange { - fn from(value: UpdateUserWorkspaceSettingPB) -> Self { - let mut change = AFWorkspaceSettingsChange::new(); - if let Some(disable_search_indexing) = value.disable_search_indexing { - change.disable_search_indexing = Some(disable_search_indexing); - } - if let Some(ai_model) = value.ai_model { - change.ai_model = Some(ai_model); - } - change - } -} - -#[derive(Debug, ProtoBuf, Default, Clone)] -pub struct WorkspaceSubscriptionInfoPB { - #[pb(index = 1)] - pub plan: WorkspacePlanPB, - #[pb(index = 2)] - pub plan_subscription: WorkspaceSubscriptionV2PB, // valid if plan is not WorkspacePlanFree - #[pb(index = 3)] - pub add_ons: Vec<WorkspaceAddOnPB>, -} - -impl WorkspaceSubscriptionInfoPB { - pub fn default_from_workspace_id(workspace_id: String) -> Self { - Self { - plan: WorkspacePlanPB::FreePlan, - plan_subscription: WorkspaceSubscriptionV2PB { - workspace_id, - subscription_plan: SubscriptionPlanPB::Free, - status: WorkspaceSubscriptionStatusPB::Active, - end_date: 0, - interval: RecurringIntervalPB::Month, - }, - add_ons: Vec::new(), - } - } -} - -impl From<Vec<WorkspaceSubscriptionStatus>> for WorkspaceSubscriptionInfoPB { - fn from(subs: Vec<WorkspaceSubscriptionStatus>) -> Self { - let mut plan = WorkspacePlanPB::FreePlan; - let mut plan_subscription = WorkspaceSubscriptionV2PB::default(); - let mut add_ons = Vec::new(); - for sub in subs { - match sub.workspace_plan { - SubscriptionPlan::Free => { - plan = WorkspacePlanPB::FreePlan; - }, - SubscriptionPlan::Pro => { - plan = WorkspacePlanPB::ProPlan; - plan_subscription = sub.into(); - }, - SubscriptionPlan::Team => { - plan = WorkspacePlanPB::TeamPlan; - }, - SubscriptionPlan::AiMax => { - if plan_subscription.workspace_id.is_empty() { - plan_subscription = - WorkspaceSubscriptionV2PB::default_with_workspace_id(sub.workspace_id.clone()); - } - - add_ons.push(WorkspaceAddOnPB { - type_: WorkspaceAddOnPBType::AddOnAiMax, - add_on_subscription: sub.into(), - }); - }, - SubscriptionPlan::AiLocal => { - if plan_subscription.workspace_id.is_empty() { - plan_subscription = - WorkspaceSubscriptionV2PB::default_with_workspace_id(sub.workspace_id.clone()); - } - - add_ons.push(WorkspaceAddOnPB { - type_: WorkspaceAddOnPBType::AddOnAiLocal, - add_on_subscription: sub.into(), - }); - }, - } - } - - WorkspaceSubscriptionInfoPB { - plan, - plan_subscription, - add_ons, - } - } -} - -#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default)] -pub enum WorkspacePlanPB { - #[default] - FreePlan = 0, - ProPlan = 1, - TeamPlan = 2, -} - -impl From<WorkspacePlanPB> for i64 { - fn from(val: WorkspacePlanPB) -> Self { - val as i64 - } -} - -impl From<i64> for WorkspacePlanPB { - fn from(value: i64) -> Self { - match value { - 0 => WorkspacePlanPB::FreePlan, - 1 => WorkspacePlanPB::ProPlan, - 2 => WorkspacePlanPB::TeamPlan, - _ => WorkspacePlanPB::FreePlan, - } - } -} - -#[derive(Debug, ProtoBuf, Default, Clone, Serialize, Deserialize)] -pub struct WorkspaceAddOnPB { - #[pb(index = 1)] - type_: WorkspaceAddOnPBType, - #[pb(index = 2)] - add_on_subscription: WorkspaceSubscriptionV2PB, -} - -#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] -pub enum WorkspaceAddOnPBType { - #[default] - AddOnAiLocal = 0, - AddOnAiMax = 1, -} - -#[derive(Debug, ProtoBuf, Default, Clone, Serialize, Deserialize)] -pub struct WorkspaceSubscriptionV2PB { +pub struct WorkspaceSubscriptionPB { #[pb(index = 1)] pub workspace_id: String, @@ -614,117 +301,45 @@ pub struct WorkspaceSubscriptionV2PB { pub subscription_plan: SubscriptionPlanPB, #[pb(index = 3)] - pub status: WorkspaceSubscriptionStatusPB, + pub recurring_interval: RecurringIntervalPB, #[pb(index = 4)] - pub end_date: i64, // Unix timestamp of when this subscription cycle ends + pub is_active: bool, #[pb(index = 5)] - pub interval: RecurringIntervalPB, + pub has_canceled: bool, + + #[pb(index = 6)] + pub canceled_at: i64, // value is valid only if has_canceled is true } -impl WorkspaceSubscriptionV2PB { - pub fn default_with_workspace_id(workspace_id: String) -> Self { +impl From<WorkspaceSubscription> for WorkspaceSubscriptionPB { + fn from(s: WorkspaceSubscription) -> Self { Self { - workspace_id, - subscription_plan: SubscriptionPlanPB::Free, - status: WorkspaceSubscriptionStatusPB::Active, - end_date: 0, - interval: RecurringIntervalPB::Month, + workspace_id: s.workspace_id, + subscription_plan: s.subscription_plan.into(), + recurring_interval: s.recurring_interval.into(), + is_active: s.is_active, + has_canceled: s.canceled_at.is_some(), + canceled_at: s.canceled_at.unwrap_or_default(), } } } -impl From<WorkspaceSubscriptionStatus> for WorkspaceSubscriptionV2PB { - fn from(sub: WorkspaceSubscriptionStatus) -> Self { - Self { - workspace_id: sub.workspace_id, - subscription_plan: sub.workspace_plan.clone().into(), - status: if sub.cancel_at.is_some() { - WorkspaceSubscriptionStatusPB::Canceled - } else { - WorkspaceSubscriptionStatusPB::Active - }, - interval: sub.recurring_interval.into(), - end_date: sub.current_period_end, - } - } -} - -#[derive(ProtoBuf_Enum, Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] -pub enum WorkspaceSubscriptionStatusPB { - #[default] - Active = 0, - Canceled = 1, -} - -impl From<WorkspaceSubscriptionStatusPB> for i64 { - fn from(val: WorkspaceSubscriptionStatusPB) -> Self { - val as i64 - } -} - -impl From<i64> for WorkspaceSubscriptionStatusPB { - fn from(value: i64) -> Self { - match value { - 0 => WorkspaceSubscriptionStatusPB::Active, - _ => WorkspaceSubscriptionStatusPB::Canceled, - } - } -} - -#[derive(ProtoBuf, Default, Clone, Validate)] -pub struct UpdateWorkspaceSubscriptionPaymentPeriodPB { +#[derive(Debug, ProtoBuf, Default, Clone)] +pub struct WorkspaceUsagePB { #[pb(index = 1)] - #[validate(custom(function = "required_not_empty_str"))] - pub workspace_id: String, - + pub member_count: u64, #[pb(index = 2)] - pub plan: SubscriptionPlanPB, - + pub member_count_limit: u64, #[pb(index = 3)] - pub recurring_interval: RecurringIntervalPB, -} - -#[derive(ProtoBuf, Default, Clone)] -pub struct RepeatedSubscriptionPlanDetailPB { - #[pb(index = 1)] - pub items: Vec<SubscriptionPlanDetailPB>, -} - -#[derive(ProtoBuf, Default, Clone)] -pub struct SubscriptionPlanDetailPB { - #[pb(index = 1)] - pub currency: CurrencyPB, - #[pb(index = 2)] - pub price_cents: i64, - #[pb(index = 3)] - pub recurring_interval: RecurringIntervalPB, + pub total_blob_bytes: u64, #[pb(index = 4)] - pub plan: SubscriptionPlanPB, + pub total_blob_bytes_limit: u64, } -impl From<SubscriptionPlanDetail> for SubscriptionPlanDetailPB { - fn from(value: SubscriptionPlanDetail) -> Self { - Self { - currency: value.currency.into(), - price_cents: value.price_cents, - recurring_interval: value.recurring_interval.into(), - plan: value.plan.into(), - } - } -} - -#[derive(ProtoBuf_Enum, Clone, Default)] -pub enum CurrencyPB { - #[default] - USD = 0, -} - -impl From<Currency> for CurrencyPB { - fn from(value: Currency) -> Self { - match value { - Currency::USD => CurrencyPB::USD, - } - } +#[derive(Debug, ProtoBuf, Default, Clone)] +pub struct BillingPortalPB { + #[pb(index = 1)] + pub url: String, } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index cbcf6f4477..b2c36d8c6b 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -1,3 +1,14 @@ +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_sqlite::kv::StorePreferences; +use flowy_user_pub::cloud::UserCloudConfig; +use flowy_user_pub::entities::*; +use lib_dispatch::prelude::*; +use lib_infra::box_any::BoxAny; +use serde_json::Value; +use std::sync::Weak; +use std::{convert::TryInto, sync::Arc}; +use tracing::{event, trace}; + use crate::entities::*; use crate::notification::{send_notification, UserNotification}; use crate::services::cloud_config::{ @@ -5,18 +16,6 @@ use crate::services::cloud_config::{ }; use crate::services::data_import::prepare_import; use crate::user_manager::UserManager; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::entities::*; -use flowy_user_pub::sql::UserWorkspaceChangeset; -use lib_dispatch::prelude::*; -use lib_infra::box_any::BoxAny; -use serde_json::Value; -use std::str::FromStr; -use std::sync::Weak; -use std::{convert::TryInto, sync::Arc}; -use tracing::{event, trace}; -use uuid::Uuid; fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc<UserManager>> { let manager = manager @@ -26,30 +25,32 @@ fn upgrade_manager(manager: AFPluginState<Weak<UserManager>>) -> FlowyResult<Arc } fn upgrade_store_preferences( - store: AFPluginState<Weak<KVStorePreferences>>, -) -> FlowyResult<Arc<KVStorePreferences>> { + store: AFPluginState<Weak<StorePreferences>>, +) -> FlowyResult<Arc<StorePreferences>> { let store = store .upgrade() .ok_or(FlowyError::internal().with_context("The store preferences is already drop"))?; Ok(store) } -#[tracing::instrument(level = "debug", name = "sign_in", skip(data, manager), fields( - email = % data.email -), err)] +#[tracing::instrument(level = "debug", name = "sign_in", skip(data, manager), fields(email = %data.email), err)] pub async fn sign_in_with_email_password_handler( data: AFPluginData<SignInPayloadPB>, manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<GotrueTokenResponsePB, FlowyError> { +) -> DataResult<UserProfilePB, FlowyError> { let manager = upgrade_manager(manager)?; let params: SignInParams = data.into_inner().try_into()?; + let auth_type = params.auth_type.clone(); - match manager - .sign_in_with_password(¶ms.email, ¶ms.password) - .await - { - Ok(token) => data_result_ok(token.into()), - Err(err) => Err(err), + let old_authenticator = manager.cloud_services.get_user_authenticator(); + match manager.sign_in(params, auth_type).await { + Ok(profile) => data_result_ok(UserProfilePB::from(profile)), + Err(err) => { + manager + .cloud_services + .set_user_authenticator(&old_authenticator); + return Err(err); + }, } } @@ -58,8 +59,8 @@ pub async fn sign_in_with_email_password_handler( name = "sign_up", skip(data, manager), fields( - email = % data.email, - name = % data.name, + email = %data.email, + name = %data.name, ), err )] @@ -69,11 +70,17 @@ pub async fn sign_up( ) -> DataResult<UserProfilePB, FlowyError> { let manager = upgrade_manager(manager)?; let params: SignUpParams = data.into_inner().try_into()?; - let auth_type = params.auth_type; + let authenticator = params.auth_type.clone(); - match manager.sign_up(auth_type, BoxAny::new(params)).await { + let old_authenticator = manager.cloud_services.get_user_authenticator(); + match manager.sign_up(authenticator, BoxAny::new(params)).await { Ok(profile) => data_result_ok(UserProfilePB::from(profile)), - Err(err) => Err(err), + Err(err) => { + manager + .cloud_services + .set_user_authenticator(&old_authenticator); + return Err(err); + }, } } @@ -91,28 +98,22 @@ pub async fn get_user_profile_handler( manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<UserProfilePB, FlowyError> { let manager = upgrade_manager(manager)?; - let session = manager.get_session()?; - - let mut user_profile = manager - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; + let uid = manager.get_session()?.user_id; + let mut user_profile = manager.get_user_profile_from_disk(uid).await?; let weak_manager = Arc::downgrade(&manager); let cloned_user_profile = user_profile.clone(); - let workspace_id = session.user_workspace.id.clone(); // Refresh the user profile in the background - tokio::spawn(async move { + af_spawn(async move { if let Some(manager) = weak_manager.upgrade() { - let _ = manager - .refresh_user_profile(&cloned_user_profile, &workspace_id) - .await; + let _ = manager.refresh_user_profile(&cloned_user_profile).await; } }); // When the user is logged in with a local account, the email field is a placeholder and should // not be exposed to the client. So we set the email field to an empty string. - if user_profile.auth_type == AuthType::Local { + if user_profile.authenticator == Authenticator::Local { user_profile.email = "".to_string(); } @@ -135,15 +136,6 @@ pub async fn sign_out_handler(manager: AFPluginState<Weak<UserManager>>) -> Resu Ok(()) } -#[tracing::instrument(level = "debug", skip(manager))] -pub async fn delete_account_handler( - manager: AFPluginState<Weak<UserManager>>, -) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - manager.delete_account().await?; - Ok(()) -} - #[tracing::instrument(level = "debug", skip(data, manager))] pub async fn update_user_profile_handler( data: AFPluginData<UpdateUserProfilePayloadPB>, @@ -159,7 +151,7 @@ const APPEARANCE_SETTING_CACHE_KEY: &str = "appearance_settings"; #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_appearance_setting( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, data: AFPluginData<AppearanceSettingsPB>, ) -> Result<(), FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; @@ -167,13 +159,13 @@ pub async fn set_appearance_setting( if setting.theme.is_empty() { setting.theme = APPEARANCE_DEFAULT_THEME.to_string(); } - store_preferences.set_object(APPEARANCE_SETTING_CACHE_KEY, &setting)?; + store_preferences.set_object(APPEARANCE_SETTING_CACHE_KEY, setting)?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] pub async fn get_appearance_setting( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, ) -> DataResult<AppearanceSettingsPB, FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; match store_preferences.get_str(APPEARANCE_SETTING_CACHE_KEY) { @@ -195,7 +187,7 @@ const DATE_TIME_SETTINGS_CACHE_KEY: &str = "date_time_settings"; #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_date_time_settings( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, data: AFPluginData<DateTimeSettingsPB>, ) -> Result<(), FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; @@ -204,13 +196,13 @@ pub async fn set_date_time_settings( setting.timezone_id = "".to_string(); } - store_preferences.set_object(DATE_TIME_SETTINGS_CACHE_KEY, &setting)?; + store_preferences.set_object(DATE_TIME_SETTINGS_CACHE_KEY, setting)?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] pub async fn get_date_time_settings( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, ) -> DataResult<DateTimeSettingsPB, FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; match store_preferences.get_str(DATE_TIME_SETTINGS_CACHE_KEY) { @@ -235,18 +227,18 @@ const NOTIFICATION_SETTINGS_CACHE_KEY: &str = "notification_settings"; #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_notification_settings( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, data: AFPluginData<NotificationSettingsPB>, ) -> Result<(), FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; let setting = data.into_inner(); - store_preferences.set_object(NOTIFICATION_SETTINGS_CACHE_KEY, &setting)?; + store_preferences.set_object(NOTIFICATION_SETTINGS_CACHE_KEY, setting)?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] pub async fn get_notification_settings( - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, ) -> DataResult<NotificationSettingsPB, FlowyError> { let store_preferences = upgrade_store_preferences(store_preferences)?; match store_preferences.get_str(NOTIFICATION_SETTINGS_CACHE_KEY) { @@ -271,16 +263,12 @@ pub async fn import_appflowy_data_folder_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let (tx, rx) = tokio::sync::oneshot::channel(); - tokio::spawn(async move { + af_spawn(async move { let result = async { let manager = upgrade_manager(manager)?; - let imported_folder = prepare_import( - &data.path, - data.parent_view_id, - &manager.authenticate_user.user_config.app_version, - ) - .map_err(|err| FlowyError::new(ErrorCode::AppFlowyDataFolderImportError, err.to_string()))? - .with_container_name(data.import_container_name); + let imported_folder = prepare_import(&data.path) + .map_err(|err| FlowyError::new(ErrorCode::AppFlowyDataFolderImportError, err.to_string()))? + .with_container_name(data.import_container_name); manager.perform_import(imported_folder).await?; Ok::<(), FlowyError>(()) @@ -314,19 +302,6 @@ pub async fn sign_in_with_magic_link_handler( Ok(()) } -#[tracing::instrument(level = "debug", skip(data, manager), err)] -pub async fn sign_in_with_passcode_handler( - data: AFPluginData<PasscodeSignInPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<GotrueTokenResponsePB, FlowyError> { - let manager = upgrade_manager(manager)?; - let params = data.into_inner(); - let response = manager - .sign_in_with_passcode(¶ms.email, ¶ms.passcode) - .await?; - data_result_ok(response.into()) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn oauth_sign_in_handler( data: AFPluginData<OauthSignInPB>, @@ -334,7 +309,7 @@ pub async fn oauth_sign_in_handler( ) -> DataResult<UserProfilePB, FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: AuthType = params.authenticator.into(); + let authenticator: Authenticator = params.authenticator.into(); let user_profile = manager .sign_up(authenticator, BoxAny::new(params.map)) .await?; @@ -348,7 +323,7 @@ pub async fn gen_sign_in_url_handler( ) -> DataResult<SignInUrlPB, FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); - let authenticator: AuthType = params.authenticator.into(); + let authenticator: Authenticator = params.authenticator.into(); let sign_in_url = manager .generate_sign_in_url_with_email(&authenticator, ¶ms.email) .await?; @@ -369,11 +344,71 @@ pub async fn sign_in_with_provider_handler( }) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn set_encrypt_secret_handler( + manager: AFPluginState<Weak<UserManager>>, + data: AFPluginData<UserSecretPB>, + store_preferences: AFPluginState<Weak<StorePreferences>>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let store_preferences = upgrade_store_preferences(store_preferences)?; + let data = data.into_inner(); + match data.encryption_type { + EncryptionTypePB::NoEncryption => { + tracing::error!("Encryption type is NoEncryption, but set encrypt secret"); + }, + EncryptionTypePB::Symmetric => { + manager.check_encryption_sign_with_secret( + data.user_id, + &data.encryption_sign, + &data.encryption_secret, + )?; + + let config = UserCloudConfig::new(data.encryption_secret).with_enable_encrypt(true); + manager + .set_encrypt_secret( + data.user_id, + config.encrypt_secret.clone(), + EncryptionType::SelfEncryption(data.encryption_sign), + ) + .await?; + save_cloud_config(data.user_id, &store_preferences, config)?; + }, + } + + manager.resume_sign_up().await?; + Ok(()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn check_encrypt_secret_handler( + manager: AFPluginState<Weak<UserManager>>, +) -> DataResult<UserEncryptionConfigurationPB, FlowyError> { + let manager = upgrade_manager(manager)?; + let uid = manager.get_session()?.user_id; + let profile = manager.get_user_profile_from_disk(uid).await?; + + let is_need_secret = match profile.encryption_type { + EncryptionType::NoEncryption => false, + EncryptionType::SelfEncryption(sign) => { + if sign.is_empty() { + false + } else { + manager.check_encryption_sign(uid, &sign).is_err() + } + }, + }; + + data_result_ok(UserEncryptionConfigurationPB { + require_secret: is_need_secret, + }) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn set_cloud_config_handler( manager: AFPluginState<Weak<UserManager>>, data: AFPluginData<UpdateCloudConfigPB>, - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let session = manager.get_session()?; @@ -384,18 +419,40 @@ pub async fn set_cloud_config_handler( if let Some(enable_sync) = update.enable_sync { manager - .cloud_service + .cloud_services .set_enable_sync(session.user_id, enable_sync); config.enable_sync = enable_sync; } - save_cloud_config(session.user_id, &store_preferences, &config)?; + if let Some(enable_encrypt) = update.enable_encrypt { + debug_assert!(enable_encrypt, "Disable encryption is not supported"); + + if enable_encrypt { + tracing::info!("Enable encryption for user: {}", session.user_id); + config = config.with_enable_encrypt(enable_encrypt); + let encrypt_secret = config.encrypt_secret.clone(); + + // The encryption secret is generated when the user first enables encryption and will be + // used to validate the encryption secret is correct when the user logs in. + let encryption_sign = manager.generate_encryption_sign(session.user_id, &encrypt_secret)?; + let encryption_type = EncryptionType::SelfEncryption(encryption_sign); + manager + .set_encrypt_secret(session.user_id, encrypt_secret, encryption_type.clone()) + .await?; + + let params = + UpdateUserProfileParams::new(session.user_id).with_encryption_type(encryption_type); + manager.update_user_profile(params).await?; + } + } + + save_cloud_config(session.user_id, &store_preferences, config.clone())?; let payload = CloudSettingPB { enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_service.service_url(), + server_url: manager.cloud_services.service_url(), }; send_notification( @@ -411,7 +468,7 @@ pub async fn set_cloud_config_handler( #[tracing::instrument(level = "info", skip_all, err)] pub async fn get_cloud_config_handler( manager: AFPluginState<Weak<UserManager>>, - store_preferences: AFPluginState<Weak<KVStorePreferences>>, + store_preferences: AFPluginState<Weak<StorePreferences>>, ) -> DataResult<CloudSettingPB, FlowyError> { let manager = upgrade_manager(manager)?; let session = manager.get_session()?; @@ -422,7 +479,7 @@ pub async fn get_cloud_config_handler( enable_sync: config.enable_sync, enable_encrypt: config.enable_encrypt, encrypt_secret: config.encrypt_secret, - server_url: manager.cloud_service.service_url(), + server_url: manager.cloud_services.service_url(), }) } @@ -431,44 +488,22 @@ pub async fn get_all_workspace_handler( manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<RepeatedUserWorkspacePB, FlowyError> { let manager = upgrade_manager(manager)?; - let session = manager.get_session()?; - let profile = manager - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; - let user_workspaces = manager - .get_all_user_workspaces(profile.uid, profile.auth_type) - .await?; - - data_result_ok(RepeatedUserWorkspacePB::from(user_workspaces)) + let uid = manager.get_session()?.user_id; + let user_workspaces = manager.get_all_user_workspaces(uid).await?; + data_result_ok(user_workspaces.into()) } #[tracing::instrument(level = "info", skip(data, manager), err)] pub async fn open_workspace_handler( - data: AFPluginData<OpenUserWorkspacePB>, + data: AFPluginData<UserWorkspaceIdPB>, manager: AFPluginState<Weak<UserManager>>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - manager - .open_workspace(&workspace_id, AuthType::from(params.workspace_auth_type)) - .await?; + manager.open_workspace(¶ms.workspace_id).await?; Ok(()) } -#[tracing::instrument(level = "info", skip(data, manager), err)] -pub async fn get_user_workspace_handler( - data: AFPluginData<UserWorkspaceIdPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<UserWorkspacePB, FlowyError> { - let manager = upgrade_manager(manager)?; - let params = data.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let uid = manager.user_id()?; - let user_workspace = manager.get_user_workspace_from_db(uid, &workspace_id)?; - data_result_ok(UserWorkspacePB::from(user_workspace)) -} - #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn update_network_state_handler( data: AFPluginData<NetworkStatePB>, @@ -476,12 +511,12 @@ pub async fn update_network_state_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let reachable = data.into_inner().ty.is_reachable(); - manager.cloud_service.set_network_reachable(reachable); + manager.cloud_services.set_network_reachable(reachable); manager .user_status_callback .read() .await - .on_network_status_changed(reachable); + .did_update_network(reachable); Ok(()) } @@ -546,6 +581,24 @@ pub async fn get_all_reminder_event_handler( data_result_ok(reminders.into()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn reset_workspace_handler( + data: AFPluginData<ResetWorkspacePB>, + manager: AFPluginState<Weak<UserManager>>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let reset_pb = data.into_inner(); + if reset_pb.workspace_id.is_empty() { + return Err(FlowyError::new( + ErrorCode::WorkspaceInitializeError, + "The workspace id is empty", + )); + } + let _session = manager.get_session()?; + manager.reset_workspace(reset_pb).await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn remove_reminder_event_handler( data: AFPluginData<ReminderIdentifierPB>, @@ -570,6 +623,19 @@ pub async fn update_reminder_event_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn add_workspace_member_handler( + data: AFPluginData<AddWorkspaceMemberPB>, + manager: AFPluginState<Weak<UserManager>>, +) -> Result<(), FlowyError> { + let data = data.try_into_inner()?; + let manager = upgrade_manager(manager)?; + manager + .add_workspace_member(data.email, data.workspace_id) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn delete_workspace_member_handler( data: AFPluginData<RemoveWorkspaceMemberPB>, @@ -577,9 +643,8 @@ pub async fn delete_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .remove_workspace_member(data.email, workspace_id) + .remove_workspace_member(data.email, data.workspace_id) .await?; Ok(()) } @@ -591,9 +656,8 @@ pub async fn get_workspace_members_handler( ) -> DataResult<RepeatedWorkspaceMemberPB, FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; let members = manager - .get_workspace_members(workspace_id) + .get_workspace_members(data.workspace_id) .await? .into_iter() .map(WorkspaceMemberPB::from) @@ -608,9 +672,8 @@ pub async fn update_workspace_member_handler( ) -> Result<(), FlowyError> { let data = data.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&data.workspace_id)?; manager - .update_workspace_member(data.email, workspace_id, data.role.into()) + .update_workspace_member(data.email, data.workspace_id, data.role.into()) .await?; Ok(()) } @@ -621,10 +684,9 @@ pub async fn create_workspace_handler( manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<UserWorkspacePB, FlowyError> { let data = data.try_into_inner()?; - let auth_type = AuthType::from(data.auth_type); let manager = upgrade_manager(manager)?; - let new_workspace = manager.create_workspace(&data.name, auth_type).await?; - data_result_ok(UserWorkspacePB::from(new_workspace)) + let new_workspace = manager.add_workspace(&data.name).await?; + data_result_ok(new_workspace.into()) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -634,7 +696,6 @@ pub async fn delete_workspace_handler( ) -> Result<(), FlowyError> { let workspace_id = delete_workspace_param.try_into_inner()?.workspace_id; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(&workspace_id)?; manager.delete_workspace(&workspace_id).await?; Ok(()) } @@ -646,15 +707,9 @@ pub async fn rename_workspace_handler( ) -> Result<(), FlowyError> { let params = rename_workspace_param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let changeset = UserWorkspaceChangeset { - id: params.workspace_id, - name: Some(params.new_name), - icon: None, - role: None, - member_count: None, - }; - manager.patch_workspace(&workspace_id, changeset).await?; + manager + .patch_workspace(¶ms.workspace_id, Some(¶ms.new_name), None) + .await?; Ok(()) } @@ -665,15 +720,9 @@ pub async fn change_workspace_icon_handler( ) -> Result<(), FlowyError> { let params = change_workspace_icon_param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let changeset = UserWorkspaceChangeset { - id: workspace_id.to_string(), - name: None, - icon: Some(params.new_icon), - role: None, - member_count: None, - }; - manager.patch_workspace(&workspace_id, changeset).await?; + manager + .patch_workspace(¶ms.workspace_id, None, Some(¶ms.new_icon)) + .await?; Ok(()) } @@ -684,9 +733,8 @@ pub async fn invite_workspace_member_handler( ) -> Result<(), FlowyError> { let param = param.try_into_inner()?; let manager = upgrade_manager(manager)?; - let workspace_id = Uuid::from_str(¶m.workspace_id)?; manager - .invite_member_to_workspace(workspace_id, param.invitee_email, param.role.into()) + .invite_member_to_workspace(param.workspace_id, param.invitee_email, param.role.into()) .await?; Ok(()) } @@ -722,7 +770,6 @@ pub async fn leave_workspace_handler( manager: AFPluginState<Weak<UserManager>>, ) -> Result<(), FlowyError> { let workspace_id = param.into_inner().workspace_id; - let workspace_id = Uuid::from_str(&workspace_id)?; let manager = upgrade_manager(manager)?; manager.leave_workspace(&workspace_id).await?; Ok(()) @@ -740,28 +787,27 @@ pub async fn subscribe_workspace_handler( } #[tracing::instrument(level = "debug", skip_all, err)] -pub async fn get_workspace_subscription_info_handler( - params: AFPluginData<UserWorkspaceIdPB>, +pub async fn get_workspace_subscriptions_handler( manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<WorkspaceSubscriptionInfoPB, FlowyError> { - let params = params.try_into_inner()?; +) -> DataResult<RepeatedWorkspaceSubscriptionPB, FlowyError> { let manager = upgrade_manager(manager)?; let subs = manager - .get_workspace_subscription_info(params.workspace_id) - .await?; - data_result_ok(subs) + .get_workspace_subscriptions() + .await? + .into_iter() + .map(WorkspaceSubscriptionPB::from) + .collect::<Vec<_>>(); + data_result_ok(RepeatedWorkspaceSubscriptionPB { items: subs }) } #[tracing::instrument(level = "debug", skip_all, err)] pub async fn cancel_workspace_subscription_handler( - param: AFPluginData<CancelWorkspaceSubscriptionPB>, + param: AFPluginData<UserWorkspaceIdPB>, manager: AFPluginState<Weak<UserManager>>, ) -> Result<(), FlowyError> { - let params = param.into_inner(); + let workspace_id = param.into_inner().workspace_id; let manager = upgrade_manager(manager)?; - manager - .cancel_workspace_subscription(params.workspace_id, params.plan.into(), Some(params.reason)) - .await?; + manager.cancel_workspace_subscription(workspace_id).await?; Ok(()) } @@ -770,10 +816,15 @@ pub async fn get_workspace_usage_handler( param: AFPluginData<UserWorkspaceIdPB>, manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<WorkspaceUsagePB, FlowyError> { - let workspace_id = Uuid::from_str(¶m.into_inner().workspace_id)?; + let workspace_id = param.into_inner().workspace_id; let manager = upgrade_manager(manager)?; - let workspace_usage = manager.get_workspace_usage(&workspace_id).await?; - data_result_ok(WorkspaceUsagePB::from(workspace_usage)) + let workspace_usage = manager.get_workspace_usage(workspace_id).await?; + data_result_ok(WorkspaceUsagePB { + member_count: workspace_usage.member_count as u64, + member_count_limit: workspace_usage.member_count_limit as u64, + total_blob_bytes: workspace_usage.total_blob_bytes as u64, + total_blob_bytes_limit: workspace_usage.total_blob_bytes_limit as u64, + }) } #[tracing::instrument(level = "debug", skip_all, err)] @@ -785,80 +836,12 @@ pub async fn get_billing_portal_handler( data_result_ok(BillingPortalPB { url }) } -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn update_workspace_subscription_payment_period_handler( - params: AFPluginData<UpdateWorkspaceSubscriptionPaymentPeriodPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> FlowyResult<()> { - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let params = params.try_into_inner()?; - let manager = upgrade_manager(manager)?; - manager - .update_workspace_subscription_payment_period( - &workspace_id, - params.plan.into(), - params.recurring_interval.into(), - ) - .await -} - -#[tracing::instrument(level = "debug", skip_all, err)] -pub async fn get_subscription_plan_details_handler( - manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<RepeatedSubscriptionPlanDetailPB, FlowyError> { - let manager = upgrade_manager(manager)?; - let plans = manager - .get_subscription_plan_details() - .await? - .into_iter() - .map(SubscriptionPlanDetailPB::from) - .collect::<Vec<_>>(); - data_result_ok(RepeatedSubscriptionPlanDetailPB { items: plans }) -} - #[tracing::instrument(level = "debug", skip_all, err)] pub async fn get_workspace_member_info( param: AFPluginData<WorkspaceMemberIdPB>, manager: AFPluginState<Weak<UserManager>>, ) -> DataResult<WorkspaceMemberPB, FlowyError> { let manager = upgrade_manager(manager)?; - let workspace_id = manager.get_session()?.user_workspace.workspace_id()?; - let member = manager - .get_workspace_member_info(param.uid, &workspace_id) - .await?; + let member = manager.get_workspace_member_info(param.uid).await?; data_result_ok(member.into()) } - -#[tracing::instrument(level = "info", skip_all, err)] -pub async fn update_workspace_setting_handler( - params: AFPluginData<UpdateUserWorkspaceSettingPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> Result<(), FlowyError> { - let params = params.try_into_inner()?; - let manager = upgrade_manager(manager)?; - manager.update_workspace_setting(params).await?; - Ok(()) -} - -#[tracing::instrument(level = "info", skip_all, err)] -pub async fn get_workspace_setting_handler( - params: AFPluginData<UserWorkspaceIdPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> DataResult<WorkspaceSettingsPB, FlowyError> { - let params = params.try_into_inner()?; - let workspace_id = Uuid::from_str(¶ms.workspace_id)?; - let manager = upgrade_manager(manager)?; - let pb = manager.get_workspace_settings(&workspace_id).await?; - data_result_ok(pb) -} - -#[tracing::instrument(level = "info", skip_all, err)] -pub async fn notify_did_switch_plan_handler( - params: AFPluginData<SuccessWorkspaceSubscriptionPB>, - manager: AFPluginState<Weak<UserManager>>, -) -> Result<(), FlowyError> { - let success = params.into_inner(); - let manager = upgrade_manager(manager)?; - manager.notify_did_switch_plan(success).await?; - Ok(()) -} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index ba242e6d46..42be2a256b 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,13 +1,13 @@ -use client_api::entity::billing_dto::SubscriptionPlan; +use std::sync::Weak; + +use strum_macros::Display; + use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use flowy_error::FlowyResult; use flowy_user_pub::cloud::UserCloudConfig; use flowy_user_pub::entities::*; use lib_dispatch::prelude::*; -use lib_infra::async_trait::async_trait; -use std::sync::Weak; -use strum_macros::Display; -use uuid::Uuid; +use lib_infra::future::{to_fut, Fut}; use crate::event_handler::*; use crate::user_manager::UserManager; @@ -28,18 +28,18 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin { .event(UserEvent::InitUser, init_user_handler) .event(UserEvent::GetUserProfile, get_user_profile_handler) .event(UserEvent::SignOut, sign_out_handler) - .event(UserEvent::DeleteAccount, delete_account_handler) .event(UserEvent::UpdateUserProfile, update_user_profile_handler) .event(UserEvent::SetAppearanceSetting, set_appearance_setting) .event(UserEvent::GetAppearanceSetting, get_appearance_setting) .event(UserEvent::GetUserSetting, get_user_setting) .event(UserEvent::SetCloudConfig, set_cloud_config_handler) .event(UserEvent::GetCloudConfig, get_cloud_config_handler) + .event(UserEvent::SetEncryptionSecret, set_encrypt_secret_handler) + .event(UserEvent::CheckEncryptionSign, check_encrypt_secret_handler) .event(UserEvent::OauthSignIn, oauth_sign_in_handler) .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) - .event(UserEvent::GetUserWorkspace, get_user_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::OpenAnonUser, open_anon_user_handler) .event(UserEvent::GetAnonUser, get_anon_user_handler) @@ -48,11 +48,15 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin { .event(UserEvent::GetAllReminders, get_all_reminder_event_handler) .event(UserEvent::RemoveReminder, remove_reminder_event_handler) .event(UserEvent::UpdateReminder, update_reminder_event_handler) + .event(UserEvent::ResetWorkspace, reset_workspace_handler) .event(UserEvent::SetDateTimeSettings, set_date_time_settings) .event(UserEvent::GetDateTimeSettings, get_date_time_settings) .event(UserEvent::SetNotificationSettings, set_notification_settings) .event(UserEvent::GetNotificationSettings, get_notification_settings) .event(UserEvent::ImportAppFlowyDataFolder, import_appflowy_data_folder_handler) + // Workspace member + .event(UserEvent::AddWorkspaceMember, add_workspace_member_handler) // deprecated, use invite + // instead .event(UserEvent::GetMemberInfo, get_workspace_member_info) .event(UserEvent::RemoveWorkspaceMember, delete_workspace_member_handler) .event(UserEvent::GetWorkspaceMembers, get_workspace_members_handler) @@ -69,28 +73,22 @@ pub fn init(user_manager: Weak<UserManager>) -> AFPlugin { .event(UserEvent::AcceptWorkspaceInvitation, accept_workspace_invitations_handler) // Billing .event(UserEvent::SubscribeWorkspace, subscribe_workspace_handler) - .event(UserEvent::GetWorkspaceSubscriptionInfo, get_workspace_subscription_info_handler) + .event(UserEvent::GetWorkspaceSubscriptions, get_workspace_subscriptions_handler) .event(UserEvent::CancelWorkspaceSubscription, cancel_workspace_subscription_handler) .event(UserEvent::GetWorkspaceUsage, get_workspace_usage_handler) .event(UserEvent::GetBillingPortal, get_billing_portal_handler) - .event(UserEvent::UpdateWorkspaceSubscriptionPaymentPeriod, update_workspace_subscription_payment_period_handler) - .event(UserEvent::GetSubscriptionPlanDetails, get_subscription_plan_details_handler) - // Workspace Setting - .event(UserEvent::UpdateWorkspaceSetting, update_workspace_setting_handler) - .event(UserEvent::GetWorkspaceSetting, get_workspace_setting_handler) - .event(UserEvent::NotifyDidSwitchPlan, notify_did_switch_plan_handler) - .event(UserEvent::PasscodeSignIn, sign_in_with_passcode_handler) + } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum UserEvent { - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Logging into an account using a register email and password - #[event(input = "SignInPayloadPB", output = "GotrueTokenResponsePB")] + #[event(input = "SignInPayloadPB", output = "UserProfilePB")] SignInWithEmailPassword = 0, - /// Only use when the [AuthType] is Local or SelfHosted + /// Only use when the [Authenticator] is Local or SelfHosted /// Creating a new account #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 1, @@ -128,7 +126,7 @@ pub enum UserEvent { OauthSignIn = 10, /// Get the OAuth callback url - /// Only use when the [AuthType] is AFCloud + /// Only use when the [Authenticator] is AFCloud #[event(input = "SignInUrlPayloadPB", output = "SignInUrlPB")] GenerateSignInURL = 11, @@ -141,16 +139,19 @@ pub enum UserEvent { #[event(output = "CloudSettingPB")] GetCloudConfig = 14, + #[event(input = "UserSecretPB")] + SetEncryptionSecret = 15, + + #[event(output = "UserEncryptionConfigurationPB")] + CheckEncryptionSign = 16, + /// Return the all the workspaces of the user #[event(output = "RepeatedUserWorkspacePB")] GetAllWorkspace = 17, - #[event(input = "OpenUserWorkspacePB")] + #[event(input = "UserWorkspaceIdPB")] OpenWorkspace = 21, - #[event(input = "UserWorkspaceIdPB", output = "UserWorkspacePB")] - GetUserWorkspace = 22, - #[event(input = "NetworkStatePB")] UpdateNetworkState = 24, @@ -161,7 +162,7 @@ pub enum UserEvent { OpenAnonUser = 26, /// Push a realtime event to the user. Currently, the realtime event - /// is only used when the auth type is: [AuthType::Supabase]. + /// is only used when the auth type is: [Authenticator::Supabase]. /// #[event(input = "RealtimePayloadPB")] PushRealtimeEvent = 27, @@ -178,6 +179,9 @@ pub enum UserEvent { #[event(input = "ReminderPB")] UpdateReminder = 31, + #[event(input = "ResetWorkspacePB")] + ResetWorkspace = 32, + /// Change the Date/Time formats globally #[event(input = "DateTimeSettingsPB")] SetDateTimeSettings = 33, @@ -192,7 +196,6 @@ pub enum UserEvent { #[event(output = "NotificationSettingsPB")] GetNotificationSettings = 36, - // Deprecated #[event(input = "AddWorkspaceMemberPB")] AddWorkspaceMember = 37, @@ -238,7 +241,10 @@ pub enum UserEvent { #[event(input = "SubscribeWorkspacePB", output = "PaymentLinkPB")] SubscribeWorkspace = 51, - #[event(input = "CancelWorkspaceSubscriptionPB")] + #[event(output = "RepeatedWorkspaceSubscriptionPB")] + GetWorkspaceSubscriptions = 52, + + #[event(input = "UserWorkspaceIdPB")] CancelWorkspaceSubscription = 53, #[event(input = "UserWorkspaceIdPB", output = "WorkspaceUsagePB")] @@ -249,96 +255,81 @@ pub enum UserEvent { #[event(input = "WorkspaceMemberIdPB", output = "WorkspaceMemberPB")] GetMemberInfo = 56, - - #[event(input = "UpdateUserWorkspaceSettingPB")] - UpdateWorkspaceSetting = 57, - - #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSettingsPB")] - GetWorkspaceSetting = 58, - - #[event(input = "UserWorkspaceIdPB", output = "WorkspaceSubscriptionInfoPB")] - GetWorkspaceSubscriptionInfo = 59, - - #[event(input = "UpdateWorkspaceSubscriptionPaymentPeriodPB")] - UpdateWorkspaceSubscriptionPaymentPeriod = 61, - - #[event(output = "RepeatedSubscriptionPlanDetailPB")] - GetSubscriptionPlanDetails = 62, - - #[event(input = "SuccessWorkspaceSubscriptionPB")] - NotifyDidSwitchPlan = 63, - - #[event()] - DeleteAccount = 64, - - #[event(input = "PasscodeSignInPB", output = "GotrueTokenResponsePB")] - PasscodeSignIn = 65, } -#[async_trait] pub trait UserStatusCallback: Send + Sync + 'static { - /// When the [AuthType] changed, this method will be called. Currently, the auth type + /// When the [Authenticator] changed, this method will be called. Currently, the auth type /// will be changed when the user sign in or sign up. - fn on_auth_type_changed(&self, _authenticator: AuthType) {} - /// Fires on app launch, but only if the user is already signed in. - async fn on_launch_if_authenticated( + fn authenticator_did_changed(&self, _authenticator: Authenticator) {} + /// This will be called after the application launches if the user is already signed in. + /// If the user is not signed in, this method will not be called + fn did_init( + &self, + user_id: i64, + user_authenticator: &Authenticator, + cloud_config: &Option<UserCloudConfig>, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut<FlowyResult<()>>; + /// Will be called after the user signed in. + fn did_sign_in( + &self, + user_id: i64, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut<FlowyResult<()>>; + /// Will be called after the user signed up. + fn did_sign_up( + &self, + is_new_user: bool, + user_profile: &UserProfile, + user_workspace: &UserWorkspace, + device_id: &str, + ) -> Fut<FlowyResult<()>>; + + fn did_expired(&self, token: &str, user_id: i64) -> Fut<FlowyResult<()>>; + fn open_workspace(&self, user_id: i64, user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>>; + fn did_update_network(&self, _reachable: bool) {} +} + +/// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function +pub(crate) struct DefaultUserStatusCallback; +impl UserStatusCallback for DefaultUserStatusCallback { + fn did_init( &self, _user_id: i64, + _authenticator: &Authenticator, _cloud_config: &Option<UserCloudConfig>, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, - ) -> FlowyResult<()> { - Ok(()) + ) -> Fut<FlowyResult<()>> { + to_fut(async { Ok(()) }) } - async fn did_launch(&self) -> FlowyResult<()> { - Ok(()) - } - - /// Fires right after the user successfully signs in. - async fn on_sign_in( + fn did_sign_in( &self, _user_id: i64, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, - ) -> FlowyResult<()> { - Ok(()) + ) -> Fut<FlowyResult<()>> { + to_fut(async { Ok(()) }) } - /// Fires right after the user successfully signs up. - async fn on_sign_up( + fn did_sign_up( &self, _is_new_user: bool, _user_profile: &UserProfile, _user_workspace: &UserWorkspace, _device_id: &str, - _auth_type: &AuthType, - ) -> FlowyResult<()> { - Ok(()) + ) -> Fut<FlowyResult<()>> { + to_fut(async { Ok(()) }) } - /// Fires when an authentication token has expired. - async fn on_token_expired(&self, _token: &str, _user_id: i64) -> FlowyResult<()> { - Ok(()) + fn did_expired(&self, _token: &str, _user_id: i64) -> Fut<FlowyResult<()>> { + to_fut(async { Ok(()) }) } - /// Fires when a workspace is opened by the user. - async fn on_workspace_opened( - &self, - _user_id: i64, - _workspace_id: &Uuid, - _user_workspace: &UserWorkspace, - _auth_type: &AuthType, - ) -> FlowyResult<()> { - Ok(()) + fn open_workspace(&self, _user_id: i64, _user_workspace: &UserWorkspace) -> Fut<FlowyResult<()>> { + to_fut(async { Ok(()) }) } - fn on_network_status_changed(&self, _reachable: bool) {} - fn on_subscription_plans_updated(&self, _plans: Vec<SubscriptionPlan>) {} - fn on_storage_permission_updated(&self, _can_write: bool) {} } - -/// Acts as a placeholder [UserStatusCallback] for the user session, but does not perform any function -pub(crate) struct DefaultUserStatusCallback; -impl UserStatusCallback for DefaultUserStatusCallback {} diff --git a/frontend/rust-lib/flowy-user/src/lib.rs b/frontend/rust-lib/flowy-user/src/lib.rs index 9a456b01e5..08fd0a7ff8 100644 --- a/frontend/rust-lib/flowy-user/src/lib.rs +++ b/frontend/rust-lib/flowy-user/src/lib.rs @@ -1,6 +1,7 @@ #[macro_use] extern crate flowy_sqlite; +mod anon_user; pub mod entities; mod event_handler; pub mod event_map; diff --git a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs deleted file mode 100644 index 1b1c3f890f..0000000000 --- a/frontend/rust-lib/flowy-user/src/migrations/anon_user_workspace.rs +++ /dev/null @@ -1,50 +0,0 @@ -use diesel::SqliteConnection; -use semver::Version; -use std::sync::Arc; -use tracing::instrument; - -use collab_integrate::CollabKVDB; -use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; - -use crate::migrations::migration::UserDataMigration; -use flowy_user_pub::session::Session; -use flowy_user_pub::sql::upsert_user_workspace; - -pub struct AnonUserWorkspaceTableMigration; - -impl UserDataMigration for AnonUserWorkspaceTableMigration { - fn name(&self) -> &str { - "anon_user_workspace_table_migration" - } - - fn run_when( - &self, - first_installed_version: &Option<Version>, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => true, - Some(version) => version <= &Version::new(0, 8, 10), - } - } - - #[instrument(name = "AnonUserWorkspaceTableMigration", skip_all, err)] - fn run( - &self, - session: &Session, - _collab_db: &Arc<CollabKVDB>, - user_auth_type: &AuthType, - db: &mut SqliteConnection, - ) -> FlowyResult<()> { - // For historical reason, anon user doesn't have a workspace in user_workspace_table. - // So we need to create a new entry for the anon user in the user_workspace_table. - if matches!(user_auth_type, AuthType::Local) { - let mut user_workspace = session.user_workspace.clone(); - user_workspace.workspace_type = AuthType::Local; - upsert_user_workspace(session.user_id, *user_auth_type, user_workspace, db)?; - } - - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs b/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs deleted file mode 100644 index 735d8f1f49..0000000000 --- a/frontend/rust-lib/flowy-user/src/migrations/doc_key_with_workspace.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::sync::Arc; - -use collab_plugins::local_storage::kv::doc::migrate_old_keys; -use collab_plugins::local_storage::kv::KVTransactionDB; -use diesel::SqliteConnection; -use semver::Version; -use tracing::{instrument, trace}; - -use collab_integrate::CollabKVDB; -use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; - -use crate::migrations::migration::UserDataMigration; -use flowy_user_pub::session::Session; - -pub struct CollabDocKeyWithWorkspaceIdMigration; - -impl UserDataMigration for CollabDocKeyWithWorkspaceIdMigration { - fn name(&self) -> &str { - "collab_doc_key_with_workspace_id" - } - - fn run_when( - &self, - first_installed_version: &Option<Version>, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => { - // The user's initial installed version is None if they were using an AppFlowy version - // lower than 0.7.3 and then upgraded to the latest version. - true - }, - Some(version) => version < &Version::new(0, 7, 3), - } - } - - #[instrument(name = "CollabDocKeyWithWorkspaceIdMigration", skip_all, err)] - fn run( - &self, - session: &Session, - collab_db: &Arc<CollabKVDB>, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, - ) -> FlowyResult<()> { - trace!( - "migrate key with workspace id:{}", - session.user_workspace.id - ); - collab_db.with_write_txn(|txn| { - migrate_old_keys(txn, &session.user_workspace.id)?; - Ok(()) - })?; - Ok(()) - } -} diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index 996386cb5e..cf59bac68c 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -1,18 +1,18 @@ use std::sync::Arc; +use collab::core::collab::MutexCollab; use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::Collab; use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::{Folder, View}; use collab_plugins::local_storage::kv::KVTransactionDB; -use diesel::SqliteConnection; use semver::Version; use tracing::{event, instrument}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use flowy_error::{FlowyError, FlowyResult}; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -26,15 +26,8 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { "historical_empty_document" } - fn run_when( - &self, - first_installed_version: &Option<Version>, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => true, - Some(version) => version < &Version::new(0, 4, 0), - } + fn applies_to_version(&self, _version: &Version) -> bool { + true } #[instrument(name = "HistoricalEmptyDocumentMigration", skip_all, err)] @@ -42,43 +35,30 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { &self, session: &Session, collab_db: &Arc<CollabKVDB>, - user_auth_type: &AuthType, - _db: &mut SqliteConnection, + authenticator: &Authenticator, ) -> FlowyResult<()> { // - The `empty document` struct has already undergone refactoring prior to the launch of the AppFlowy cloud version. // - Consequently, if a user is utilizing the AppFlowy cloud version, there is no need to perform any migration for the `empty document` struct. // - This migration step is only necessary for users who are transitioning from a local version of AppFlowy to the cloud version. - if !matches!(user_auth_type, AuthType::Local) { + if !matches!(authenticator, Authenticator::Local) { return Ok(()); } collab_db.with_write_txn(|write_txn| { let origin = CollabOrigin::Client(CollabClient::new(session.user_id, "phantom")); - let folder_collab = match load_collab( - session.user_id, - write_txn, - &session.user_workspace.id, - &session.user_workspace.id, - ) { + let folder_collab = match load_collab(session.user_id, write_txn, &session.user_workspace.id) + { Ok(fc) => fc, Err(_) => return Ok(()), }; let folder = Folder::open(session.user_id, folder_collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; - if let Some(workspace_id) = folder.get_workspace_id() { - let migration_views = folder.get_views_belong_to(&workspace_id); + if let Ok(workspace_id) = folder.try_get_workspace_id() { + let migration_views = folder.views.get_views_belong_to(&workspace_id); // For historical reasons, the first level documents are empty. So migrate them by inserting // the default document data. for view in migration_views { - if migrate_empty_document( - write_txn, - &origin, - &view, - session.user_id, - &session.user_workspace.id, - ) - .is_err() - { + if migrate_empty_document(write_txn, &origin, &view, session.user_id).is_err() { event!( tracing::Level::ERROR, "Failed to migrate document {}", @@ -100,24 +80,25 @@ fn migrate_empty_document<'a, W>( origin: &CollabOrigin, view: &View, user_id: i64, - workspace_id: &str, ) -> Result<(), FlowyError> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { // If the document is not exist, we don't need to migrate it. - if load_collab(user_id, write_txn, workspace_id, &view.id).is_err() { - let collab = Collab::new_with_origin(origin.clone(), &view.id, vec![], false); - let document = Document::create_with_data(collab, default_document_data(&view.id))?; - let encode = document.encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; - write_txn.flush_doc( - user_id, - workspace_id, + if load_collab(user_id, write_txn, &view.id).is_err() { + let collab = Arc::new(MutexCollab::new(Collab::new_with_origin( + origin.clone(), &view.id, - encode.state_vector.to_vec(), - encode.doc_state.to_vec(), - )?; + vec![], + false, + ))); + let document = Document::create_with_data(collab, default_document_data(&view.id))?; + let encode = document + .get_collab() + .lock() + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; + write_txn.flush_doc_with(user_id, &view.id, &encode.doc_state, &encode.state_vector)?; event!( tracing::Level::INFO, "Did migrate empty document {}", diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 1cf8d6a943..26be72707a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -1,26 +1,21 @@ use std::sync::Arc; use chrono::NaiveDateTime; -use collab_integrate::CollabKVDB; use diesel::{RunQueryDsl, SqliteConnection}; +use semver::Version; + +use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; -use flowy_sqlite::kv::KVStorePreferences; use flowy_sqlite::schema::user_data_migration_records; use flowy_sqlite::ConnectionPool; -use flowy_user_pub::entities::AuthType; -use flowy_user_pub::session::Session; -use semver::Version; -use tracing::info; +use flowy_user_pub::entities::Authenticator; -/// Store the version that user first time install AppFlowy. For old user, this value will be set -/// to the version when upgrade to the newest version. -pub const FIRST_TIME_INSTALL_VERSION: &str = "first_install_version"; +use flowy_user_pub::session::Session; pub struct UserLocalDataMigration { session: Session, collab_db: Arc<CollabKVDB>, sqlite_pool: Arc<ConnectionPool>, - kv: Arc<KVStorePreferences>, } impl UserLocalDataMigration { @@ -28,13 +23,11 @@ impl UserLocalDataMigration { session: Session, collab_db: Arc<CollabKVDB>, sqlite_pool: Arc<ConnectionPool>, - kv: Arc<KVStorePreferences>, ) -> Self { Self { session, collab_db, sqlite_pool, - kv, } } @@ -54,33 +47,32 @@ impl UserLocalDataMigration { pub fn run( self, migrations: Vec<Box<dyn UserDataMigration>>, - user_auth_type: &AuthType, - app_version: &Version, + authenticator: &Authenticator, + app_version: Option<Version>, ) -> FlowyResult<Vec<String>> { let mut applied_migrations = vec![]; let mut conn = self.sqlite_pool.get()?; let record = get_all_records(&mut conn)?; - let install_version = self.kv.get_object::<Version>(FIRST_TIME_INSTALL_VERSION); - - info!("[Migration] Install app version: {:?}", install_version); let mut duplicated_names = vec![]; for migration in migrations { if !record .iter() .any(|record| record.migration_name == migration.name()) { - if !migration.run_when(&install_version, app_version) { - continue; + if let Some(app_version) = app_version.as_ref() { + if !migration.applies_to_version(app_version) { + continue; + } } let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { - migration.run(&self.session, &self.collab_db, user_auth_type, &mut conn)?; + migration.run(&self.session, &self.collab_db, authenticator)?; applied_migrations.push(migration.name().to_string()); save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); } else { - tracing::error!("[Migration] Duplicated migration name: {}", migration_name); + tracing::error!("Duplicated migration name: {}", migration_name); } } } @@ -91,15 +83,14 @@ impl UserLocalDataMigration { pub trait UserDataMigration { /// Migration with the same name will be skipped fn name(&self) -> &str; - // The user's initial installed version is None if they were using an AppFlowy version lower than 0.7.3 - // Because we store the first time installed version after version 0.7.3. - fn run_when(&self, first_installed_version: &Option<Version>, current_version: &Version) -> bool; + /// Returns bool value whether the migration should be applied to the current app version + /// true if the migration should be applied, false otherwise + fn applies_to_version(&self, app_version: &Version) -> bool; fn run( &self, user: &Session, collab_db: &Arc<CollabKVDB>, - user_auth_type: &AuthType, - db: &mut SqliteConnection, + authenticator: &Authenticator, ) -> FlowyResult<()>; } @@ -121,7 +112,7 @@ fn get_all_records(conn: &mut SqliteConnection) -> FlowyResult<Vec<UserDataMigra ) } -#[derive(Clone, Debug, Default, Queryable, Identifiable)] +#[derive(Clone, Default, Queryable, Identifiable)] #[diesel(table_name = user_data_migration_records)] pub struct UserDataMigrationRecord { pub id: i32, diff --git a/frontend/rust-lib/flowy-user/src/migrations/mod.rs b/frontend/rust-lib/flowy-user/src/migrations/mod.rs index 3d87dc595f..d5f83d47c9 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/mod.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/mod.rs @@ -1,8 +1,5 @@ use flowy_user_pub::session::Session; -use std::sync::Arc; -pub mod anon_user_workspace; -pub mod doc_key_with_workspace; pub mod document_empty_content; pub mod migration; pub mod session_migration; @@ -12,5 +9,5 @@ pub mod workspace_trash_v1; #[derive(Clone, Debug)] pub struct AnonUser { - pub session: Arc<Session>, + pub session: Session, } diff --git a/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs b/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs index df477f4a33..77376e1c6c 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/session_migration.rs @@ -1,4 +1,4 @@ -use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::kv::StorePreferences; use flowy_user_pub::session::Session; use serde_json::{json, Value}; use std::sync::Arc; @@ -8,9 +8,9 @@ const MIGRATION_USER_NO_USER_UUID: &str = "migration_user_no_user_uuid"; pub fn migrate_session_with_user_uuid( session_cache_key: &str, - store_preferences: &Arc<KVStorePreferences>, + store_preferences: &Arc<StorePreferences>, ) -> Option<Session> { - if !store_preferences.get_bool_or_default(MIGRATION_USER_NO_USER_UUID) + if !store_preferences.get_bool(MIGRATION_USER_NO_USER_UUID) && store_preferences .set_bool(MIGRATION_USER_NO_USER_UUID, true) .is_ok() diff --git a/frontend/rust-lib/flowy-user/src/migrations/util.rs b/frontend/rust-lib/flowy-user/src/migrations/util.rs index 9508228c6f..f0c4c3f7f7 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/util.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/util.rs @@ -1,3 +1,6 @@ +use std::sync::Arc; + +use collab::core::collab::MutexCollab; use collab::preclude::Collab; use collab_integrate::{CollabKVAction, PersistenceError}; @@ -6,14 +9,13 @@ use flowy_error::FlowyResult; pub(crate) fn load_collab<'a, R>( uid: i64, collab_r_txn: &R, - workspace_id: &str, object_id: &str, -) -> FlowyResult<Collab> +) -> FlowyResult<Arc<MutexCollab>> where R: CollabKVAction<'a>, PersistenceError: From<R::Error>, { - let mut collab = Collab::new(uid, object_id, "phantom", vec![], false); - collab_r_txn.load_doc_with_txn(uid, workspace_id, object_id, &mut collab.transact_mut())?; - Ok(collab) + let collab = Collab::new(uid, object_id, "phantom", vec![], false); + collab.with_origin_transact_mut(|txn| collab_r_txn.load_doc_with_txn(uid, &object_id, txn))?; + Ok(Arc::new(MutexCollab::new(collab))) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index d3cea0e976..b6d5e3e8ff 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; -use diesel::SqliteConnection; use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -16,7 +15,7 @@ use flowy_user_pub::session::Session; /// 1. Migrate the workspace: { favorite: [view_id] } to { favorite: { uid: [view_id] } } /// 2. Migrate { workspaces: [workspace object] } to { views: { workspace object } }. Make each folder -/// only have one workspace. +/// only have one workspace. pub struct FavoriteV1AndWorkspaceArrayMigration; impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { @@ -24,15 +23,8 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { "workspace_favorite_v1_and_workspace_array_migration" } - fn run_when( - &self, - first_installed_version: &Option<Version>, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => true, - Some(version) => version < &Version::new(0, 4, 0), - } + fn applies_to_version(&self, _app_version: &Version) -> bool { + true } #[instrument(name = "FavoriteV1AndWorkspaceArrayMigration", skip_all, err)] @@ -40,21 +32,13 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { &self, session: &Session, collab_db: &Arc<CollabKVDB>, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, + _authenticator: &Authenticator, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { - if let Ok(collab) = load_collab( - session.user_id, - write_txn, - &session.user_workspace.id, - &session.user_workspace.id, - ) { - let mut folder = Folder::open(session.user_id, collab, None) + if let Ok(collab) = load_collab(session.user_id, write_txn, &session.user_workspace.id) { + let folder = Folder::open(session.user_id, collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; - folder - .body - .migrate_workspace_to_view(&mut folder.collab.transact_mut()); + folder.migrate_workspace_to_view(); let favorite_view_ids = folder .get_favorite_v1() @@ -67,14 +51,13 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { } let encode = folder - .encode_collab() + .encode_collab_v1() .map_err(|err| PersistenceError::Internal(err.into()))?; - write_txn.flush_doc( + write_txn.flush_doc_with( session.user_id, &session.user_workspace.id, - &session.user_workspace.id, - encode.state_vector.to_vec(), - encode.doc_state.to_vec(), + &encode.doc_state, + &encode.state_vector, )?; } Ok(()) diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index ee9156199e..e15f2597b4 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -2,13 +2,12 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; -use diesel::SqliteConnection; use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; use flowy_error::FlowyResult; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::migration::UserDataMigration; use crate::migrations::util::load_collab; @@ -22,15 +21,8 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { "workspace_trash_map_to_section_migration" } - fn run_when( - &self, - first_installed_version: &Option<Version>, - _current_version: &Version, - ) -> bool { - match first_installed_version { - None => true, - Some(version) => version < &Version::new(0, 4, 0), - } + fn applies_to_version(&self, _app_version: &Version) -> bool { + true } #[instrument(name = "WorkspaceTrashMapToSectionMigration", skip_all, err)] @@ -38,17 +30,11 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { &self, session: &Session, collab_db: &Arc<CollabKVDB>, - _user_auth_type: &AuthType, - _db: &mut SqliteConnection, + _authenticator: &Authenticator, ) -> FlowyResult<()> { collab_db.with_write_txn(|write_txn| { - if let Ok(collab) = load_collab( - session.user_id, - write_txn, - &session.user_workspace.id, - &session.user_workspace.id, - ) { - let mut folder = Folder::open(session.user_id, collab, None) + if let Ok(collab) = load_collab(session.user_id, write_txn, &session.user_workspace.id) { + let folder = Folder::open(session.user_id, collab, None) .map_err(|err| PersistenceError::Internal(err.into()))?; let trash_ids = folder .get_trash_v1() @@ -61,14 +47,13 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { } let encode = folder - .encode_collab() + .encode_collab_v1() .map_err(|err| PersistenceError::Internal(err.into()))?; - write_txn.flush_doc( + write_txn.flush_doc_with( session.user_id, &session.user_workspace.id, - &session.user_workspace.id, - encode.state_vector.to_vec(), - encode.doc_state.to_vec(), + &encode.doc_state, + &encode.state_vector, )?; } Ok(()) diff --git a/frontend/rust-lib/flowy-user/src/notification.rs b/frontend/rust-lib/flowy-user/src/notification.rs index dd93593468..a77e0fc8aa 100644 --- a/frontend/rust-lib/flowy-user/src/notification.rs +++ b/frontend/rust-lib/flowy-user/src/notification.rs @@ -14,7 +14,6 @@ pub(crate) enum UserNotification { DidUpdateUserWorkspaces = 3, DidUpdateCloudConfig = 4, DidUpdateUserWorkspace = 5, - DidUpdateWorkspaceSetting = 6, } impl std::convert::From<UserNotification> for i32 { diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 418f0638d3..c9d779a232 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -1,44 +1,55 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; use crate::services::db::UserDB; use crate::services::entities::{UserConfig, UserPaths}; +use crate::services::sqlite_sql::user_sql::vacuum_database; use collab_integrate::CollabKVDB; -use crate::user_manager::manager_history_user::ANON_USER; -use arc_swap::ArcSwapOption; -use collab_plugins::local_storage::kv::doc::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::kv::StorePreferences; use flowy_sqlite::DBConnection; -use flowy_user_pub::entities::{AuthType, UserWorkspace}; +use flowy_user_pub::entities::UserWorkspace; use flowy_user_pub::session::Session; use std::path::PathBuf; -use std::str::FromStr; use std::sync::{Arc, Weak}; -use tracing::info; -use uuid::Uuid; +use tracing::{error, info}; + +const SQLITE_VACUUM_042: &str = "sqlite_vacuum_042_version"; pub struct AuthenticateUser { pub user_config: UserConfig, pub(crate) database: Arc<UserDB>, pub(crate) user_paths: UserPaths, - store_preferences: Arc<KVStorePreferences>, - session: ArcSwapOption<Session>, + store_preferences: Arc<StorePreferences>, + session: Arc<parking_lot::RwLock<Option<Session>>>, } impl AuthenticateUser { - pub fn new(user_config: UserConfig, store_preferences: Arc<KVStorePreferences>) -> Self { + pub fn new(user_config: UserConfig, store_preferences: Arc<StorePreferences>) -> Self { let user_paths = UserPaths::new(user_config.storage_path.clone()); let database = Arc::new(UserDB::new(user_paths.clone())); - let session = - migrate_session_with_user_uuid(&user_config.session_cache_key, &store_preferences) - .map(Arc::new); + let session = Arc::new(parking_lot::RwLock::new(None)); + *session.write() = + migrate_session_with_user_uuid(&user_config.session_cache_key, &store_preferences); Self { user_config, database, user_paths, store_preferences, - session: ArcSwapOption::from(session), + session, + } + } + + pub fn vacuum_database_if_need(&self) { + if !self.store_preferences.get_bool(SQLITE_VACUUM_042) { + if let Ok(session) = self.get_session() { + let _ = self.store_preferences.set_bool(SQLITE_VACUUM_042, true); + if let Ok(conn) = self.database.get_connection(session.user_id) { + info!("vacuum database 042"); + if let Err(err) = vacuum_database(conn) { + error!("vacuum database error: {:?}", err); + } + } + } } } @@ -47,28 +58,18 @@ impl AuthenticateUser { Ok(session.user_id) } - pub async fn is_local_mode(&self) -> FlowyResult<bool> { - let session = self.get_session()?; - Ok(matches!( - session.user_workspace.workspace_type, - AuthType::Local - )) - } - pub fn device_id(&self) -> FlowyResult<String> { Ok(self.user_config.device_id.to_string()) } - pub fn workspace_id(&self) -> FlowyResult<Uuid> { + pub fn workspace_id(&self) -> FlowyResult<String> { let session = self.get_session()?; - let workspace_uuid = Uuid::from_str(&session.user_workspace.id)?; - Ok(workspace_uuid) + Ok(session.user_workspace.id) } - pub fn workspace_database_object_id(&self) -> FlowyResult<Uuid> { + pub fn workspace_database_object_id(&self) -> FlowyResult<String> { let session = self.get_session()?; - let id = Uuid::from_str(&session.user_workspace.workspace_database_id)?; - Ok(id) + Ok(session.user_workspace.database_indexer_id.clone()) } pub fn get_collab_db(&self, uid: i64) -> FlowyResult<Weak<CollabKVDB>> { @@ -82,18 +83,9 @@ impl AuthenticateUser { self.database.get_connection(uid) } - pub fn get_index_path(&self) -> FlowyResult<PathBuf> { - let uid = self.user_id()?; - Ok(PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes")) - } - - pub fn get_user_data_dir(&self) -> FlowyResult<PathBuf> { - let uid = self.user_id()?; - Ok(PathBuf::from(self.user_paths.user_data_dir(uid))) - } - - pub fn get_application_root_dir(&self) -> &str { - self.user_paths.root() + pub fn get_index_path(&self) -> PathBuf { + let uid = self.user_id().unwrap_or(0); + PathBuf::from(self.user_paths.user_data_dir(uid)).join("indexes") } pub fn close_db(&self) -> FlowyResult<()> { @@ -103,45 +95,36 @@ impl AuthenticateUser { Ok(()) } - pub fn is_collab_on_disk(&self, uid: i64, object_id: &str) -> FlowyResult<bool> { - let session = self.get_session()?; - let collab_db = self.database.get_collab_db(uid)?; - let read_txn = collab_db.read_txn(); - Ok(read_txn.is_exist(uid, session.user_workspace.id.as_str(), object_id)) - } - - pub fn set_session(&self, session: Option<Arc<Session>>) -> Result<(), FlowyError> { - match session { + pub fn set_session(&self, session: Option<Session>) -> Result<(), FlowyError> { + match &session { None => { - let previous = self.session.swap(session); - info!("remove session: {:?}", previous); + let removed_session = self.session.write().take(); + info!("remove session: {:?}", removed_session); self .store_preferences .remove(self.user_config.session_cache_key.as_ref()); + Ok(()) }, Some(session) => { - self.session.swap(Some(session.clone())); info!("Set current session: {:?}", session); + self.session.write().replace(session.clone()); self .store_preferences - .set_object(&self.user_config.session_cache_key, &session) + .set_object(&self.user_config.session_cache_key, session.clone()) .map_err(internal_error)?; + Ok(()) }, } - Ok(()) } pub fn set_user_workspace(&self, user_workspace: UserWorkspace) -> FlowyResult<()> { - let session = self.get_session()?; - self.set_session(Some(Arc::new(Session { - user_id: session.user_id, - user_uuid: session.user_uuid, - user_workspace, - }))) + let mut session = self.get_session()?; + session.user_workspace = user_workspace; + self.set_session(Some(session)) } - pub fn get_session(&self) -> FlowyResult<Arc<Session>> { - if let Some(session) = self.session.load_full() { + pub fn get_session(&self) -> FlowyResult<Session> { + if let Some(session) = (self.session.read()).clone() { return Ok(session); } @@ -151,18 +134,10 @@ impl AuthenticateUser { { None => Err(FlowyError::new( ErrorCode::RecordNotFound, - "Can't find user session. Please login again", + "User is not logged in", )), - Some(mut session) => { - // Set the workspace type to local if the user is anon. - if let Some(anon_session) = self.store_preferences.get_object::<Session>(ANON_USER) { - if session.user_id == anon_session.user_id { - session.user_workspace.workspace_type = AuthType::Local; - } - } - - let session = Arc::new(session); - self.session.store(Some(session.clone())); + Some(session) => { + self.session.write().replace(session.clone()); Ok(session) }, } diff --git a/frontend/rust-lib/flowy-user/src/services/billing_check.rs b/frontend/rust-lib/flowy-user/src/services/billing_check.rs deleted file mode 100644 index ea5bc65b75..0000000000 --- a/frontend/rust-lib/flowy-user/src/services/billing_check.rs +++ /dev/null @@ -1,89 +0,0 @@ -use crate::services::authenticate_user::AuthenticateUser; -use client_api::entity::billing_dto::SubscriptionPlan; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_user_pub::cloud::UserCloudServiceProvider; -use std::sync::Weak; -use std::time::Duration; -use uuid::Uuid; - -/// `PeriodicallyCheckBillingState` is designed to periodically verify the subscription -/// plan of a given workspace. It utilizes a cloud service provider to fetch the current -/// subscription plans and compares them with an expected plan. -/// -/// If the expected plan is found, the check stops. Otherwise, it continues to check -/// at specified intervals until the expected plan is found or the maximum number of -/// attempts is reached. -pub struct PeriodicallyCheckBillingState { - workspace_id: Uuid, - cloud_service: Weak<dyn UserCloudServiceProvider>, - expected_plan: Option<SubscriptionPlan>, - user: Weak<AuthenticateUser>, -} - -impl PeriodicallyCheckBillingState { - pub fn new( - workspace_id: Uuid, - expected_plan: Option<SubscriptionPlan>, - cloud_service: Weak<dyn UserCloudServiceProvider>, - user: Weak<AuthenticateUser>, - ) -> Self { - Self { - workspace_id, - cloud_service, - expected_plan, - user, - } - } - - pub async fn start(&self) -> FlowyResult<Vec<SubscriptionPlan>> { - let cloud_service = self - .cloud_service - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Cloud service is not available"))?; - - let mut attempts = 0; - let max_attempts = 5; - let delay_duration = Duration::from_secs(4); - while attempts < max_attempts { - let plans = cloud_service - .get_user_service()? - .get_workspace_plan(self.workspace_id) - .await?; - - // If the expected plan is not set, return the plans immediately. Otherwise, - // check if the expected plan is found in the list of plans. - if let Some(expected_plan) = &self.expected_plan { - if plans.contains(expected_plan) { - return Ok(plans); - } - attempts += 1; - } else { - attempts += 2; - } - - tokio::time::sleep(delay_duration).await; - if let Some(user) = self.user.upgrade() { - if let Ok(current_workspace_id) = user.workspace_id() { - if current_workspace_id != self.workspace_id { - return Err( - FlowyError::internal() - .with_context("Workspace ID has changed while checking the billing state"), - ); - } - } else { - break; - } - } - - // After last retry, return plans even if the expected plan is not found - if attempts >= max_attempts { - return Ok(plans); - } - } - - Err( - FlowyError::response_timeout() - .with_context("Exceeded maximum number of checks without finding the expected plan"), - ) - } -} diff --git a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs index 30f5725f9c..6772ac7ab5 100644 --- a/frontend/rust-lib/flowy-user/src/services/cloud_config.rs +++ b/frontend/rust-lib/flowy-user/src/services/cloud_config.rs @@ -1,23 +1,23 @@ use std::sync::Arc; +use flowy_encrypt::generate_encryption_secret; use flowy_error::FlowyResult; -use flowy_sqlite::kv::KVStorePreferences; +use flowy_sqlite::kv::StorePreferences; use flowy_user_pub::cloud::UserCloudConfig; -use lib_infra::encryption::generate_encryption_secret; const CLOUD_CONFIG_KEY: &str = "af_user_cloud_config"; -fn generate_cloud_config(uid: i64, store_preference: &Arc<KVStorePreferences>) -> UserCloudConfig { +fn generate_cloud_config(uid: i64, store_preference: &Arc<StorePreferences>) -> UserCloudConfig { let config = UserCloudConfig::new(generate_encryption_secret()); let key = cache_key_for_cloud_config(uid); - store_preference.set_object(&key, &config).unwrap(); + store_preference.set_object(&key, config.clone()).unwrap(); config } pub fn save_cloud_config( uid: i64, - store_preference: &Arc<KVStorePreferences>, - config: &UserCloudConfig, + store_preference: &Arc<StorePreferences>, + config: UserCloudConfig, ) -> FlowyResult<()> { tracing::info!("save user:{} cloud config: {}", uid, config); let key = cache_key_for_cloud_config(uid); @@ -31,7 +31,7 @@ fn cache_key_for_cloud_config(uid: i64) -> String { pub fn get_cloud_config( uid: i64, - store_preference: &Arc<KVStorePreferences>, + store_preference: &Arc<StorePreferences>, ) -> Option<UserCloudConfig> { let key = cache_key_for_cloud_config(uid); store_preference.get_object::<UserCloudConfig>(&key) @@ -39,7 +39,7 @@ pub fn get_cloud_config( pub fn get_or_create_cloud_config( uid: i64, - store_preferences: &Arc<KVStorePreferences>, + store_preferences: &Arc<StorePreferences>, ) -> UserCloudConfig { let key = cache_key_for_cloud_config(uid); store_preferences @@ -47,7 +47,7 @@ pub fn get_or_create_cloud_config( .unwrap_or_else(|| generate_cloud_config(uid, store_preferences)) } -pub fn get_encrypt_secret(uid: i64, store_preference: &Arc<KVStorePreferences>) -> Option<String> { +pub fn get_encrypt_secret(uid: i64, store_preference: &Arc<StorePreferences>) -> Option<String> { let key = cache_key_for_cloud_config(uid); store_preference .get_object::<UserCloudConfig>(&key) diff --git a/frontend/rust-lib/flowy-user/src/services/collab_interact.rs b/frontend/rust-lib/flowy-user/src/services/collab_interact.rs index 90e507517c..d01d3cffde 100644 --- a/frontend/rust-lib/flowy-user/src/services/collab_interact.rs +++ b/frontend/rust-lib/flowy-user/src/services/collab_interact.rs @@ -1,21 +1,25 @@ use anyhow::Error; use collab_entity::reminder::Reminder; -use lib_infra::async_trait::async_trait; -#[async_trait] -pub trait UserReminder: Send + Sync + 'static { - async fn add_reminder(&self, _reminder: Reminder) -> Result<(), Error> { - Ok(()) - } - async fn remove_reminder(&self, _reminder_id: &str) -> Result<(), Error> { - Ok(()) - } - async fn update_reminder(&self, _reminder: Reminder) -> Result<(), Error> { - Ok(()) - } +use lib_infra::future::FutureResult; + +pub trait CollabInteract: Send + Sync + 'static { + fn add_reminder(&self, reminder: Reminder) -> FutureResult<(), Error>; + fn remove_reminder(&self, reminder_id: &str) -> FutureResult<(), Error>; + fn update_reminder(&self, reminder: Reminder) -> FutureResult<(), Error>; } pub struct DefaultCollabInteract; +impl CollabInteract for DefaultCollabInteract { + fn add_reminder(&self, _reminder: Reminder) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } -#[async_trait] -impl UserReminder for DefaultCollabInteract {} + fn remove_reminder(&self, _reminder_id: &str) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } + + fn update_reminder(&self, _reminder: Reminder) -> FutureResult<(), Error> { + FutureResult::new(async { Ok(()) }) + } +} diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 90113b8062..f60f07deac 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -1,55 +1,46 @@ use crate::migrations::session_migration::migrate_session_with_user_uuid; -use crate::services::data_import::importer::load_collab_by_object_ids; +use crate::services::data_import::importer::load_collab_by_oid; use crate::services::db::UserDBPath; use crate::services::entities::UserPaths; -use crate::user_manager::run_data_migration; +use crate::services::sqlite_sql::user_sql::select_user_profile; +use crate::user_manager::run_collab_data_migration; use anyhow::anyhow; -use collab::core::collab::DataSource; +use collab::core::collab::{DataSource, MutexCollab}; use collab::core::origin::CollabOrigin; - +use collab::core::transaction::DocTransactionExtension; use collab::preclude::updates::decoder::Decode; -use collab::preclude::updates::encoder::Encode; -use collab::preclude::{Any, Collab, Doc, ReadTxn, StateVector, Transact, Update}; +use collab::preclude::{Collab, Doc, Transact, Update}; use collab_database::database::{ is_database_collab, mut_database_views_with_collab, reset_inline_view_id, }; use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; -use collab_database::workspace_database::WorkspaceDatabase; +use collab_database::workspace_database::DatabaseMetaList; use collab_document::document_data::default_document_collab_data; use collab_entity::CollabType; -use collab_folder::hierarchy_builder::{NestedChildViewBuilder, NestedViews, ParentChildViews}; use collab_folder::{Folder, UserId, View, ViewIdentifier, ViewLayout}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_folder_pub::cloud::gen_view_id; -use flowy_folder_pub::entities::{ - ImportFrom, ImportedAppFlowyData, ImportedCollabData, ImportedFolderData, -}; -use flowy_sqlite::kv::KVStorePreferences; -use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; -use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; -use flowy_user_pub::session::Session; -use rayon::prelude::*; -use std::collections::{HashMap, HashSet}; -use collab_document::blocks::TextDelta; -use collab_document::document::Document; -use flowy_user_pub::sql::{select_user_auth_type, select_user_profile}; -use semver::Version; -use serde_json::json; +use flowy_error::FlowyError; +use flowy_folder_pub::cloud::gen_view_id; +use flowy_folder_pub::entities::{AppFlowyData, ImportData}; +use flowy_folder_pub::folder_builder::{ParentChildViews, ViewBuilder}; +use flowy_sqlite::kv::StorePreferences; +use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; +use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; +use flowy_user_pub::session::Session; +use parking_lot::{Mutex, RwLock}; +use std::collections::{HashMap, HashSet}; use std::ops::{Deref, DerefMut}; use std::path::Path; use std::sync::{Arc, Weak}; -use tracing::{error, event, info, instrument, warn}; -use uuid::Uuid; +use tracing::{debug, error, event, info, instrument, warn}; pub(crate) struct ImportedFolder { pub imported_session: Session, pub imported_collab_db: Arc<CollabKVDB>, pub container_name: Option<String>, - pub parent_view_id: Option<String>, pub source: ImportedSource, } @@ -66,20 +57,12 @@ impl ImportedFolder { } } -pub(crate) fn prepare_import( - path: &str, - parent_view_id: Option<String>, - app_version: &Version, -) -> anyhow::Result<ImportedFolder> { - info!( - "[AppflowyData]:importing data from path: {}, parent_view_id:{:?}", - path, parent_view_id - ); +pub(crate) fn prepare_import(path: &str) -> anyhow::Result<ImportedFolder> { if !Path::new(path).exists() { return Err(anyhow!("The path: {} is not exist", path)); } let user_paths = UserPaths::new(path.to_string()); - let other_store_preferences = Arc::new(KVStorePreferences::new(path)?); + let other_store_preferences = Arc::new(StorePreferences::new(path)?); migrate_session_with_user_uuid("appflowy_session_cache", &other_store_preferences); let imported_session = other_store_preferences .get_object::<Session>("appflowy_session_cache") @@ -89,42 +72,30 @@ pub(crate) fn prepare_import( ))?; let collab_db_path = user_paths.collab_db_path(imported_session.user_id); - info!("[AppflowyData]: collab db path: {:?}", collab_db_path); - let sqlite_db_path = user_paths.sqlite_db_path(imported_session.user_id); - info!("[AppflowyData]: sqlite db path: {:?}", sqlite_db_path); - let imported_sqlite_db = flowy_sqlite::init(sqlite_db_path) - .map_err(|err| anyhow!("[AppflowyData]: open import collab db failed: {:?}", err))?; - + .map_err(|err| anyhow!("open import collab db failed: {:?}", err))?; let imported_collab_db = Arc::new( CollabKVDB::open(collab_db_path) - .map_err(|err| anyhow!("[AppflowyData]: open import collab db failed: {:?}", err))?, + .map_err(|err| anyhow!("open import collab db failed: {:?}", err))?, ); - - let mut conn = imported_sqlite_db.get_connection()?; - let imported_user_auth_type = select_user_profile( + let imported_user = select_user_profile( imported_session.user_id, - &imported_session.user_workspace.id, - &mut conn, - ) - .map(|v| v.auth_type) - .or_else(|_| select_user_auth_type(imported_session.user_id, &mut conn))?; + imported_sqlite_db.get_connection()?, + )?; - run_data_migration( + run_collab_data_migration( &imported_session, - &imported_user_auth_type, + &imported_user, imported_collab_db.clone(), imported_sqlite_db.get_pool(), - other_store_preferences.clone(), - app_version, + None, ); Ok(ImportedFolder { imported_session, imported_collab_db, container_name: None, - parent_view_id, source: ImportedSource::ExternalFolder, }) } @@ -147,27 +118,20 @@ fn migrate_user_awareness( /// - log (log files with unique identifiers) /// - 2761499 (other relevant files or directories, identified by unique numbers) -#[instrument(level = "debug", skip_all, err)] pub(crate) fn generate_import_data( current_session: &Session, - user_collab_db: &Arc<CollabKVDB>, + workspace_id: &str, + collab_db: &Arc<CollabKVDB>, imported_folder: ImportedFolder, -) -> anyhow::Result<ImportedAppFlowyData> { - info!( - "[AppflowyData]:importing workspace: {}:{}", - imported_folder.imported_session.user_workspace.name, - imported_folder.imported_session.user_workspace.id, - ); - let workspace_id = current_session.user_workspace.id.clone(); - let imported_workspace_id = imported_folder.imported_session.user_workspace.id.clone(); +) -> anyhow::Result<ImportData> { let imported_session = imported_folder.imported_session.clone(); let imported_collab_db = imported_folder.imported_collab_db.clone(); let imported_container_view_name = imported_folder.container_name.clone(); let mut database_view_ids_by_database_id: HashMap<String, Vec<String>> = HashMap::new(); - let mut row_object_ids = HashSet::new(); - let mut document_object_ids = HashSet::new(); - let mut database_object_ids = HashSet::new(); + let row_object_ids = Mutex::new(HashSet::new()); + let document_object_ids = Mutex::new(HashSet::new()); + let database_object_ids = Mutex::new(HashSet::new()); // All the imported views will be attached to the container view. If the container view name is not provided, // the container view will be the workspace, which mean the root of the workspace. @@ -179,46 +143,51 @@ pub(crate) fn generate_import_data( ImportedSource::AnonUser => workspace_id.to_string(), }; - let (views, orphan_views) = user_collab_db.with_write_txn(|current_collab_db_write_txn| { - let imported_uid = imported_folder.imported_session.user_id; - let imported_collab_db_read_txn = imported_collab_db.read_txn(); + let views = collab_db.with_write_txn(|collab_write_txn| { + let imported_collab_read_txn = imported_collab_db.read_txn(); // use the old_to_new_id_map to keep track of the other collab object id and the new collab object id - let mut old_to_new_id_map = OldToNewIdMap::new(); + let old_to_new_id_map = Arc::new(Mutex::new(OldToNewIdMap::new())); // 1. Get all the imported collab object ids - let mut all_imported_object_ids = imported_collab_db_read_txn - .get_all_object_ids(imported_uid, imported_workspace_id.as_str()) + let mut all_imported_object_ids = imported_collab_read_txn + .get_all_docs() .map(|iter| iter.collect::<Vec<String>>()) .unwrap_or_default(); - info!( - "[AppflowyData]: {} has {} collab objects", - imported_uid, - all_imported_object_ids.len() - ); - // when doing import, we don't want to import these objects: // 1. user workspace - // 2. workspace database views - // 3. user awareness + // 2. database view tracker + // 3. the user awareness // So we remove these object ids from the list let user_workspace_id = &imported_session.user_workspace.id; - let workspace_database_id = &imported_session.user_workspace.workspace_database_id; + let database_indexer_id = &imported_session.user_workspace.database_indexer_id; let user_awareness_id = user_awareness_object_id(&imported_session.user_uuid, user_workspace_id).to_string(); all_imported_object_ids.retain(|id| { - id != user_workspace_id && id != workspace_database_id && id != &user_awareness_id + id != user_workspace_id && id != database_indexer_id && id != &user_awareness_id }); - // 2. mapping the workspace database ids - if let Err(err) = mapping_workspace_database_ids( - &mut old_to_new_id_map, - &imported_session, - &imported_collab_db_read_txn, - &mut database_view_ids_by_database_id, - &mut database_object_ids, - ) { - error!("[AppflowyData]:import workspace database fail: {}", err); + match imported_folder.source { + ImportedSource::ExternalFolder => { + // 2. mapping the database indexer ids + mapping_database_indexer_ids( + &mut old_to_new_id_map.lock(), + &imported_session, + &imported_collab_read_txn, + &mut database_view_ids_by_database_id, + &database_object_ids, + )?; + }, + ImportedSource::AnonUser => { + // 2. migrate the database with views object + migrate_database_with_views_object( + &mut old_to_new_id_map.lock(), + &imported_session, + &imported_collab_read_txn, + current_session, + collab_write_txn, + )?; + }, } // remove the database view ids from the object ids. Because there are no physical collab object @@ -231,239 +200,100 @@ pub(crate) fn generate_import_data( all_imported_object_ids.retain(|id| !database_view_ids.contains(id)); // 3. load imported collab objects data. - let (mut imported_collab_by_oid, invalid_object_ids) = load_collab_by_object_ids( + let imported_collab_by_oid = load_collab_by_oid( imported_session.user_id, - &imported_workspace_id, - &imported_collab_db_read_txn, + &imported_collab_read_txn, &all_imported_object_ids, ); - // remove the invalid object ids from the object ids - info!( - "[AppflowyData]: invalid object ids: {:?}", - invalid_object_ids - ); - all_imported_object_ids.retain(|id| !invalid_object_ids.contains(id)); - // the object ids now only contains the document collab object ids - all_imported_object_ids.iter().for_each(|object_id| { - old_to_new_id_map.exchange_new_id(object_id); - }); - // import the database - let MigrateDatabase { - database_view_ids, - database_row_ids, - } = migrate_databases( - &mut old_to_new_id_map, + migrate_databases( + &old_to_new_id_map, current_session, - current_collab_db_write_txn, + collab_write_txn, &mut all_imported_object_ids, - &mut imported_collab_by_oid, - &mut row_object_ids, - )?; - - migrate_database_rows( - &mut old_to_new_id_map, - current_session, - current_collab_db_write_txn, - &mut imported_collab_by_oid, - database_row_ids, - )?; - - // Update the parent view IDs of all top-level views to match the new container view ID, making - // them child views of the container. This ensures that the hierarchy within the imported - // structure is correctly maintained. - let MigrateViews { - child_views, - orphan_views, - mut invalid_orphan_views, - not_exist_parent_view_ids, - } = migrate_folder_views( - &import_container_view_id, - &mut old_to_new_id_map, - &imported_session, - &imported_collab_db_read_txn, &imported_collab_by_oid, - &database_view_ids, + &row_object_ids, )?; - // Create collaboration documents for views that don't have existing collaboration objects. - // Currently, when importing AppFlowy data, the view which is a space doesn't have a corresponding - // collab data in disk. - for view_id in not_exist_parent_view_ids { - if let Err(err) = create_empty_document_for_view( - current_session.user_id, - ¤t_session.user_workspace.id, - &view_id, - current_collab_db_write_txn, - ) { - error!( - "[AppflowyData]: create empty document for view failed: {}", - err + // the object ids now only contains the document collab object ids + for object_id in &all_imported_object_ids { + if let Some(imported_collab) = imported_collab_by_oid.get(object_id) { + let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + document_object_ids.lock().insert(new_object_id.clone()); + debug!("import from: {}, to: {}", object_id, new_object_id,); + write_collab_object( + imported_collab, + current_session.user_id, + &new_object_id, + collab_write_txn, ); } } - let gen_document_collabs = all_imported_object_ids - .par_iter() - .filter_map(|object_id| { - let f = || { - let imported_collab = imported_collab_by_oid.get(object_id)?; - let new_object_id = old_to_new_id_map.get_exchanged_id(object_id)?; - gen_sv_and_doc_state( - current_session.user_id, - new_object_id, - imported_collab, - CollabType::Document, - &old_to_new_id_map, - ) - }; - f() - }) - .collect::<Vec<_>>(); + // Update the parent view IDs of all top-level views to match the new container view ID, making + // them child views of the container. This ensures that the hierarchy within the imported + // structure is correctly maintained. + let (mut child_views, orphan_views) = mapping_folder_views( + &import_container_view_id, + &mut old_to_new_id_map.lock(), + &imported_session, + &imported_collab_read_txn, + )?; - for document_collab in gen_document_collabs { - document_object_ids.insert(document_collab.object_id.clone()); - write_gen_collab(&workspace_id, document_collab, current_collab_db_write_txn); - } - - let (mut views, orphan_views) = match imported_folder.source { + match imported_folder.source { ImportedSource::ExternalFolder => match imported_container_view_name { - None => Ok::<(Vec<ParentChildViews>, Vec<ParentChildViews>), anyhow::Error>(( - child_views, - orphan_views, - )), + None => { + child_views.extend(orphan_views); + Ok(child_views) + }, Some(container_name) => { // create a new view with given name and then attach views to it - let child_views = vec![create_new_container_view( + attach_to_new_view( current_session, - &mut document_object_ids, + &document_object_ids, &import_container_view_id, - current_collab_db_write_txn, + collab_write_txn, child_views, + orphan_views, container_name, - )?]; - Ok((child_views, orphan_views)) + ) }, }, - ImportedSource::AnonUser => Ok((child_views, orphan_views)), - }?; - - if !invalid_orphan_views.is_empty() { - let other_view_id = gen_view_id().to_string(); - invalid_orphan_views - .iter_mut() - .for_each(|parent_child_views| { - parent_child_views - .view - .parent_view_id - .clone_from(&other_view_id); - }); - let mut other_view = create_new_container_view( - current_session, - &mut document_object_ids, - &other_view_id, - current_collab_db_write_txn, - invalid_orphan_views, - "Others".to_string(), - )?; - - // if the views is empty, the other view is the top level view - // otherwise, the other view is the child view of the first view - if views.is_empty() { - views.push(other_view); - } else { - let first_view = views.first_mut().unwrap(); - other_view - .view - .parent_view_id - .clone_from(&first_view.view.id); - first_view.children.push(other_view); - } + ImportedSource::AnonUser => { + child_views.extend(orphan_views); + Ok(child_views) + }, } - - Ok((views, orphan_views)) })?; - let source = match imported_folder.source { - ImportedSource::ExternalFolder => ImportFrom::AppFlowyDataFolder, - ImportedSource::AnonUser => ImportFrom::AnonUser, - }; - - Ok(ImportedAppFlowyData { - source, - parent_view_id: imported_folder.parent_view_id, - folder_data: ImportedFolderData { - views, - orphan_views, - database_view_ids_by_database_id, - }, - collab_data: ImportedCollabData { - row_object_ids: row_object_ids.into_iter().collect(), - database_object_ids: database_object_ids.into_iter().collect(), - document_object_ids: document_object_ids.into_iter().collect(), - }, + Ok(ImportData::AppFlowyDataFolder { + items: vec![ + AppFlowyData::Folder { + views, + database_view_ids_by_database_id, + }, + AppFlowyData::CollabObject { + row_object_ids: row_object_ids.into_inner().into_iter().collect(), + database_object_ids: database_object_ids.into_inner().into_iter().collect(), + document_object_ids: document_object_ids.into_inner().into_iter().collect(), + }, + ], }) } - -fn create_empty_document_for_view<'a, W>( - uid: i64, - workspace_id: &str, - view_id: &str, - collab_write_txn: &'a W, -) -> FlowyResult<()> -where - W: CollabKVAction<'a>, - PersistenceError: From<W::Error>, -{ - let import_container_doc_state = default_document_collab_data(view_id) - .map_err(|err| PersistenceError::InvalidData(err.to_string()))? - .doc_state - .to_vec(); - - let collab = Collab::new_with_source( - CollabOrigin::Empty, - view_id, - DataSource::DocStateV1(import_container_doc_state), - vec![], - false, - ) - .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; - - write_collab_object( - &collab, - uid, - workspace_id, - view_id, - collab_write_txn, - CollabType::Document, - ); - Ok(()) -} - -#[instrument(level = "debug", skip_all, err)] -fn create_new_container_view<'a, W>( +fn attach_to_new_view<'a, W>( current_session: &Session, - document_object_ids: &mut HashSet<String>, + document_object_ids: &Mutex<HashSet<String>>, import_container_view_id: &str, collab_write_txn: &'a W, - mut child_views: Vec<ParentChildViews>, + child_views: Vec<ParentChildViews>, + orphan_views: Vec<ParentChildViews>, container_name: String, -) -> Result<ParentChildViews, PersistenceError> +) -> Result<Vec<ParentChildViews>, PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { - child_views.iter_mut().for_each(|parent_child_views| { - if parent_child_views.view.parent_view_id != import_container_view_id { - warn!( - "[AppflowyData]: The parent view id of the child views is not the import container view id: {}", - import_container_view_id - ); - parent_child_views.view.parent_view_id = import_container_view_id.to_string(); - } - }); - let name = if container_name.is_empty() { format!( "import_{}", @@ -478,69 +308,58 @@ where .map_err(|err| PersistenceError::InvalidData(err.to_string()))? .doc_state .to_vec(); - - let collab = Collab::new_with_source( - CollabOrigin::Empty, - import_container_view_id, - DataSource::DocStateV1(import_container_doc_state), - vec![], - false, - )?; - write_collab_object( - &collab, + import_collab_object_with_doc_state( + import_container_doc_state, current_session.user_id, - current_session.user_workspace.id.as_str(), import_container_view_id, collab_write_txn, - CollabType::Document, - ); + )?; - document_object_ids.insert(import_container_view_id.to_string()); - - let import_container_views = NestedChildViewBuilder::new( + document_object_ids + .lock() + .insert(import_container_view_id.to_string()); + let mut import_container_views = vec![ViewBuilder::new( current_session.user_id, current_session.user_workspace.id.clone(), ) .with_view_id(import_container_view_id) .with_layout(ViewLayout::Document) - .with_name(&name) - .with_children(child_views) - .build(); + .with_name(name) + .with_child_views(child_views) + .build()]; + import_container_views.extend(orphan_views); Ok(import_container_views) } -#[instrument(level = "debug", skip_all, err)] -fn mapping_workspace_database_ids<'a, W>( +fn mapping_database_indexer_ids<'a, W>( old_to_new_id_map: &mut OldToNewIdMap, imported_session: &Session, - imported_collab_db_read_txn: &W, + imported_collab_read_txn: &W, database_view_ids_by_database_id: &mut HashMap<String, Vec<String>>, - database_object_ids: &mut HashSet<String>, + database_object_ids: &Mutex<HashSet<String>>, ) -> Result<(), PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { - let mut workspace_database_collab = Collab::new( + let imported_database_indexer = Collab::new( imported_session.user_id, - &imported_session.user_workspace.workspace_database_id, + &imported_session.user_workspace.database_indexer_id, "import_device", vec![], false, ); - imported_collab_db_read_txn.load_doc_with_txn( - imported_session.user_id, - &imported_session.user_workspace.id, - &imported_session.user_workspace.workspace_database_id, - &mut workspace_database_collab.transact_mut(), - )?; + imported_database_indexer.with_origin_transact_mut(|txn| { + imported_collab_read_txn.load_doc_with_txn( + imported_session.user_id, + &imported_session.user_workspace.database_indexer_id, + txn, + ) + })?; - let workspace_database = init_workspace_database( - &imported_session.user_workspace.workspace_database_id, - workspace_database_collab, - ); - for database_meta_list in workspace_database.get_all_database_meta() { + let array = DatabaseMetaList::from_collab(&imported_database_indexer); + for database_meta_list in array.get_all_database_meta() { database_view_ids_by_database_id.insert( old_to_new_id_map.exchange_new_id(&database_meta_list.database_id), database_meta_list @@ -550,7 +369,7 @@ where .collect(), ); } - database_object_ids.extend( + database_object_ids.lock().extend( database_view_ids_by_database_id .keys() .cloned() @@ -559,90 +378,115 @@ where Ok(()) } -fn init_workspace_database(object_id: &str, collab: Collab) -> WorkspaceDatabase { - match WorkspaceDatabase::open(collab) { - Ok(body) => body, - Err(err) => { - error!( - "[AppflowyData]:init workspace database body failed: {:?}, create a new one", - err - ); - let collab = Collab::new_with_origin(CollabOrigin::Empty, object_id, vec![], false); - WorkspaceDatabase::create(collab) - }, - } -} - -struct MigrateDatabase { - database_view_ids: HashSet<String>, - database_row_ids: HashMap<String, HashSet<String>>, -} - -#[instrument(level = "debug", skip_all, err)] -fn migrate_databases<'a, W>( +fn migrate_database_with_views_object<'a, 'b, W, R>( old_to_new_id_map: &mut OldToNewIdMap, + old_user_session: &Session, + old_collab_r_txn: &R, + new_user_session: &Session, + new_collab_w_txn: &W, +) -> Result<(), PersistenceError> +where + 'a: 'b, + W: CollabKVAction<'a>, + R: CollabKVAction<'b>, + PersistenceError: From<W::Error>, + PersistenceError: From<R::Error>, +{ + let database_with_views_collab = Collab::new( + old_user_session.user_id, + &old_user_session.user_workspace.database_indexer_id, + "migrate_device", + vec![], + false, + ); + database_with_views_collab.with_origin_transact_mut(|txn| { + old_collab_r_txn.load_doc_with_txn( + old_user_session.user_id, + &old_user_session.user_workspace.database_indexer_id, + txn, + ) + })?; + + let new_uid = new_user_session.user_id; + let new_object_id = &new_user_session.user_workspace.database_indexer_id; + + let array = DatabaseMetaList::from_collab(&database_with_views_collab); + for database_meta in array.get_all_database_meta() { + array.update_database(&database_meta.database_id, |update| { + let new_linked_views = update + .linked_views + .iter() + .map(|view_id| old_to_new_id_map.exchange_new_id(view_id)) + .collect(); + update.database_id = old_to_new_id_map.exchange_new_id(&update.database_id); + update.linked_views = new_linked_views; + }) + } + + let txn = database_with_views_collab.transact(); + if let Err(err) = new_collab_w_txn.create_new_doc(new_uid, new_object_id, &txn) { + error!("🔴migrate database storage failed: {:?}", err); + } + drop(txn); + Ok(()) +} + +fn migrate_databases<'a, W>( + old_to_new_id_map: &Arc<Mutex<OldToNewIdMap>>, session: &Session, collab_write_txn: &'a W, - all_imported_object_ids: &mut Vec<String>, - imported_collab_by_oid: &mut HashMap<String, Collab>, - row_object_ids: &mut HashSet<String>, -) -> Result<MigrateDatabase, PersistenceError> + imported_object_ids: &mut Vec<String>, + imported_collab_by_oid: &HashMap<String, Collab>, + row_object_ids: &Mutex<HashSet<String>>, +) -> Result<(), PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { // Migrate databases + let row_document_object_ids = Mutex::new(HashSet::new()); let mut database_object_ids = vec![]; - let mut imported_database_row_ids: HashMap<String, HashSet<String>> = HashMap::new(); - let mut imported_database_view_ids = HashSet::new(); - let mut database_row_document_ids_pair = HashSet::new(); - let all_imported_keys = imported_collab_by_oid - .keys() - .cloned() - .collect::<Vec<String>>(); - for object_id in all_imported_object_ids.iter() { - if let Some(database_collab) = imported_collab_by_oid.get_mut(object_id) { + let imported_database_row_object_ids: RwLock<HashMap<String, HashSet<String>>> = + RwLock::new(HashMap::new()); + + for object_id in &mut *imported_object_ids { + if let Some(database_collab) = imported_collab_by_oid.get(object_id) { if !is_database_collab(database_collab) { continue; } database_object_ids.push(object_id.clone()); - if reset_inline_view_id(database_collab, |old_inline_view_id| { - old_to_new_id_map.exchange_new_id(&old_inline_view_id) - }) - .is_err() - { - error!( - "[AppflowyData]: reset inline view id failed for database: {}", - object_id - ); - continue; - } + reset_inline_view_id(database_collab, |old_inline_view_id| { + old_to_new_id_map + .lock() + .exchange_new_id(&old_inline_view_id) + }); mut_database_views_with_collab(database_collab, |database_view| { - let new_view_id = old_to_new_id_map.exchange_new_id(&database_view.id); + let new_view_id = old_to_new_id_map.lock().exchange_new_id(&database_view.id); let old_database_id = database_view.database_id.clone(); - let new_database_id = old_to_new_id_map.exchange_new_id(&database_view.database_id); + let new_database_id = old_to_new_id_map + .lock() + .exchange_new_id(&database_view.database_id); - imported_database_view_ids.insert(new_view_id.clone()); database_view.id = new_view_id; database_view.database_id = new_database_id; - database_view - .row_orders - .retain(|row_order| all_imported_keys.contains(&row_order.id)); database_view.row_orders.iter_mut().for_each(|row_order| { let old_row_id = String::from(row_order.id.clone()); let old_row_document_id = database_row_document_id_from_row_id(&old_row_id); - let new_row_id = old_to_new_id_map.exchange_new_id(&old_row_id); + let new_row_id = old_to_new_id_map.lock().exchange_new_id(&old_row_id); // The row document might not exist in the database row. But by querying the old_row_document_id, // we can know the document of the row is exist or not. let new_row_document_id = database_row_document_id_from_row_id(&new_row_id); - database_row_document_ids_pair - .insert((old_row_document_id.clone(), new_row_document_id.clone())); - old_to_new_id_map.insert(old_row_document_id.clone(), new_row_document_id); + + old_to_new_id_map + .lock() + .insert(old_row_document_id.clone(), new_row_document_id); + row_order.id = RowId::from(new_row_id); - imported_database_row_ids + imported_database_row_object_ids + .write() .entry(old_database_id.clone()) .or_default() .insert(old_row_id); @@ -654,85 +498,54 @@ where .iter() .map(|order| order.id.clone().into_inner()) .collect::<Vec<String>>(); - row_object_ids.extend(new_row_ids); + row_object_ids.lock().extend(new_row_ids); }); - let new_database_object_id = old_to_new_id_map.exchange_new_id(object_id); + let new_object_id = old_to_new_id_map.lock().exchange_new_id(object_id); + debug!( + "migrate database from: {}, to: {}", + object_id, new_object_id, + ); write_collab_object( database_collab, session.user_id, - session.user_workspace.id.as_str(), - &new_database_object_id, + &new_object_id, collab_write_txn, - CollabType::Database, ); } } - - // write database rows into disk - let gen_database_row_document_collabs = database_row_document_ids_pair - .par_iter() - .filter_map(|(old_object_id, new_object_id)| { - let imported_collab = imported_collab_by_oid.get(old_object_id)?; - gen_sv_and_doc_state( - session.user_id, - new_object_id, - imported_collab, - CollabType::Document, - old_to_new_id_map, - ) - }) - .collect::<Vec<_>>(); - for gen_collab in gen_database_row_document_collabs { - write_gen_collab(&session.user_workspace.id, gen_collab, collab_write_txn); - } + let imported_database_row_object_ids = imported_database_row_object_ids.read(); // remove the database object ids from the object ids - all_imported_object_ids.retain(|id| !database_object_ids.contains(id)); + imported_object_ids.retain(|id| !database_object_ids.contains(id)); // remove database row object ids from the imported object ids - all_imported_object_ids.retain(|id| { - !imported_database_row_ids + imported_object_ids.retain(|id| { + !imported_database_row_object_ids .values() .flatten() .any(|row_id| row_id == id) }); - let database_row_document_ids = database_row_document_ids_pair - .iter() - .map(|(old_object_id, _)| old_object_id.clone()) - .collect::<HashSet<String>>(); - all_imported_object_ids.retain(|id| !database_row_document_ids.contains(id)); + for (database_id, imported_row_ids) in &*imported_database_row_object_ids { + for imported_row_id in imported_row_ids { + if let Some(imported_collab) = imported_collab_by_oid.get(imported_row_id) { + let new_database_id = old_to_new_id_map.lock().exchange_new_id(database_id); + let new_row_id = old_to_new_id_map.lock().exchange_new_id(imported_row_id); + info!( + "import database row from: {}, to: {}", + imported_row_id, new_row_id, + ); - Ok(MigrateDatabase { - database_view_ids: imported_database_view_ids, - database_row_ids: imported_database_row_ids, - }) -} - -#[instrument(level = "debug", skip_all, err)] -fn migrate_database_rows<'a, W>( - old_to_new_id_map: &mut OldToNewIdMap, - session: &Session, - collab_write_txn: &'a W, - imported_collab_by_oid: &mut HashMap<String, Collab>, - imported_database_row_object_ids: HashMap<String, HashSet<String>>, -) -> Result<(), PersistenceError> -where - W: CollabKVAction<'a>, - PersistenceError: From<W::Error>, -{ - let mut row_document_ids = HashSet::new(); - for (database_id, imported_row_ids) in imported_database_row_object_ids { - imported_row_ids.iter().for_each(|imported_row_id| { - if let Some(imported_collab) = imported_collab_by_oid.get_mut(imported_row_id) { - let new_database_id = old_to_new_id_map.exchange_new_id(&database_id); - let new_row_id = old_to_new_id_map.exchange_new_id(imported_row_id); mut_row_with_collab(imported_collab, |row_update| { - row_update - .set_row_id(RowId::from(new_row_id.clone())) - .set_database_id(new_database_id.clone()); + row_update.set_row_id(RowId::from(new_row_id.clone()), new_database_id.clone()); }); + write_collab_object( + imported_collab, + session.user_id, + &new_row_id, + collab_write_txn, + ); } // imported_collab_by_oid contains all the collab object ids, including the row document collab object ids. @@ -742,85 +555,52 @@ where .get(&imported_row_document_id) .is_some() { - let new_row_document_id = old_to_new_id_map.exchange_new_id(&imported_row_document_id); - row_document_ids.insert(new_row_document_id); + let new_row_document_id = old_to_new_id_map + .lock() + .exchange_new_id(&imported_row_document_id); + row_document_object_ids.lock().insert(new_row_document_id); } - }); - - let gen_database_row_collabs = imported_row_ids - .par_iter() - .filter_map(|imported_row_id| { - let imported_collab = imported_collab_by_oid.get(imported_row_id)?; - match old_to_new_id_map.get_exchanged_id(imported_row_id) { - None => { - error!( - "[AppflowyData]: Can't find the new id for the imported row:{}", - imported_row_id - ); - None - }, - Some(new_row_id) => gen_sv_and_doc_state( - session.user_id, - new_row_id, - imported_collab, - CollabType::DatabaseRow, - old_to_new_id_map, - ), - } - }) - .collect::<Vec<_>>(); - - for gen_collab in gen_database_row_collabs { - write_gen_collab(&session.user_workspace.id, gen_collab, collab_write_txn); } } + debug!( + "import row document ids: {:?}", + row_document_object_ids + .lock() + .iter() + .collect::<Vec<&String>>() + ); + Ok(()) } -fn write_collab_object<'a, W>( - collab: &Collab, - new_uid: i64, - new_workspace_id: &str, - new_object_id: &str, - w_txn: &'a W, - collab_type: CollabType, -) where +fn write_collab_object<'a, W>(collab: &Collab, new_uid: i64, new_object_id: &str, w_txn: &'a W) +where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { - if let Ok(encode_collab) = - collab.encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - { + if let Ok(encode_collab) = collab.encode_collab_v1(|_| Ok::<(), PersistenceError>(())) { if let Ok(update) = Update::decode_v1(&encode_collab.doc_state) { let doc = Doc::new(); { let mut txn = doc.transact_mut(); - if let Err(e) = txn.apply_update(update) { - error!( - "Collab {} failed to apply update: {}", - collab.object_id(), - e - ); - return; - } + txn.apply_update(update); drop(txn); } - let txn = doc.transact(); - let state_vector = txn.state_vector(); - let doc_state = txn.encode_state_as_update_v1(&StateVector::default()); + let encoded_collab = doc.get_encoded_collab_v1(); + info!( + "import collab:{} with len: {}", + new_object_id, + encoded_collab.doc_state.len() + ); if let Err(err) = w_txn.flush_doc( new_uid, - new_workspace_id, - new_object_id, - state_vector.encode_v1(), - doc_state, + &new_object_id, + encoded_collab.state_vector.to_vec(), + encoded_collab.doc_state.to_vec(), ) { - error!( - "[AppflowyData]:import collab:{} failed: {:?}", - new_object_id, err - ); + error!("import collab:{} failed: {:?}", new_object_id, err); } } } else { @@ -828,181 +608,65 @@ fn write_collab_object<'a, W>( } } -struct GenCollab { - uid: i64, - sv: Vec<u8>, +fn import_collab_object_with_doc_state<'a, W>( doc_state: Vec<u8>, - object_id: String, -} - -fn write_gen_collab<'a, W>(workspace_id: &str, collab: GenCollab, w_txn: &'a W) + new_uid: i64, + new_object_id: &str, + w_txn: &'a W, +) -> Result<(), anyhow::Error> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { - if let Err(err) = w_txn.flush_doc( - collab.uid, - workspace_id, - collab.object_id.as_str(), - collab.sv, - collab.doc_state, - ) { - error!( - "[AppflowyData]:import collab:{} failed: {:?}", - collab.object_id, err - ); - } + let collab = Collab::new_with_source( + CollabOrigin::Empty, + new_object_id, + DataSource::DocStateV1(doc_state), + vec![], + false, + )?; + write_collab_object(&collab, new_uid, new_object_id, w_txn); + Ok(()) } -fn gen_sv_and_doc_state( - uid: i64, - object_id: &str, - collab: &Collab, - collab_type: CollabType, - ids_map: &OldToNewIdMap, -) -> Option<GenCollab> { - let encoded_collab = collab - .encode_collab_v1(|collab| collab_type.validate_require_data(collab)) - .ok()?; - - let (state_vector, doc_state) = match collab_type { - CollabType::Document => { - let collab = Collab::new_with_source( - CollabOrigin::Empty, - object_id, - encoded_collab.into(), - vec![], - false, - ) - .ok()?; - let mut document = Document::open(collab).ok()?; - if let Err(err) = replace_document_ref_ids(&mut document, ids_map) { - error!("[AppFlowyData]: replace document ref ids failed: {}", err); - } - let encode_collab = document.encode_collab().ok()?; - ( - encode_collab.state_vector.to_vec(), - encode_collab.doc_state.to_vec(), - ) - }, - _ => { - let update = Update::decode_v1(&encoded_collab.doc_state).ok()?; - let doc = Doc::new(); - let mut txn = doc.transact_mut(); - if let Err(e) = txn.apply_update(update) { - error!( - "Collab {} failed to apply update: {}", - collab.object_id(), - e - ); - return None; - } - drop(txn); - let txn = doc.transact(); - let state_vector = txn.state_vector(); - let doc_state = txn.encode_state_as_update_v1(&StateVector::default()); - (state_vector.encode_v1(), doc_state) - }, - }; - - Some(GenCollab { - uid, - sv: state_vector, - doc_state, - object_id: object_id.to_string(), - }) -} - -struct MigrateViews { - child_views: Vec<ParentChildViews>, - orphan_views: Vec<ParentChildViews>, - invalid_orphan_views: Vec<ParentChildViews>, - #[allow(dead_code)] - not_exist_parent_view_ids: Vec<String>, -} - -#[instrument(level = "debug", skip_all, err)] -fn migrate_folder_views<'a, W>( +fn mapping_folder_views<'a, W>( root_view_id: &str, old_to_new_id_map: &mut OldToNewIdMap, imported_session: &Session, - imported_collab_db_read_txn: &W, - imported_collab_by_oid: &HashMap<String, Collab>, - database_view_ids: &HashSet<String>, -) -> Result<MigrateViews, PersistenceError> + imported_collab_read_txn: &W, +) -> Result<(Vec<ParentChildViews>, Vec<ParentChildViews>), PersistenceError> where W: CollabKVAction<'a>, PersistenceError: From<W::Error>, { - let mut imported_folder_collab = Collab::new( + let imported_folder_collab = Collab::new( imported_session.user_id, &imported_session.user_workspace.id, "migrate_device", vec![], false, ); - - imported_collab_db_read_txn - .load_doc_with_txn( + imported_folder_collab.with_origin_transact_mut(|txn| { + imported_collab_read_txn.load_doc_with_txn( imported_session.user_id, &imported_session.user_workspace.id, - &imported_session.user_workspace.id, - &mut imported_folder_collab.transact_mut(), + txn, ) - .map_err(|err| { - PersistenceError::Internal(anyhow!( - "[AppflowyData]: Can't load the user:{} folder:{}. {}", - imported_session.user_id, - imported_session.user_workspace.id, - err - )) - })?; + })?; let other_user_id = UserId::from(imported_session.user_id); - let imported_folder = - Folder::open(other_user_id, imported_folder_collab, None).map_err(|err| { - PersistenceError::Internal(anyhow!("[AppflowyData]:Can't open folder:{}", err)) - })?; + let imported_folder = Folder::open( + other_user_id, + Arc::new(MutexCollab::new(imported_folder_collab)), + None, + ) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))?; - let mut imported_folder_data = imported_folder + let imported_folder_data = imported_folder .get_folder_data(&imported_session.user_workspace.id) .ok_or(PersistenceError::Internal(anyhow!( - "[AppflowyData]: Can't read the folder data" + "Can't read the folder data" )))?; - // Only import views whose collab data is available - imported_folder_data.views.iter_mut().for_each(|view| { - view.children.retain(|view_identifier| { - let new_view_id = old_to_new_id_map.exchange_new_id(&view_identifier.id); - if database_view_ids.contains(&new_view_id) { - true - } else { - imported_collab_by_oid.contains_key(&view_identifier.id) - } - }); - }); - let mut not_exist_parent_view_ids = vec![]; - imported_folder_data.views.retain(|view| { - let new_view_id = old_to_new_id_map.exchange_new_id(&view.id); - // If the view is a database view, it should be in the database view ids - if view.layout.is_database() && !database_view_ids.contains(&new_view_id) { - error!( - "[AppflowyData]: The database view:{} is not in the database view ids", - view.id - ); - } - - if database_view_ids.contains(&new_view_id) { - true - } else if view.space_info().is_some() { - if !imported_collab_by_oid.contains_key(&view.id) { - not_exist_parent_view_ids.push(new_view_id); - } - true - } else { - imported_collab_by_oid.contains_key(&view.id) - } - }); - // replace the old parent view id of the workspace old_to_new_id_map.0.insert( imported_session.user_workspace.id.clone(), @@ -1017,7 +681,7 @@ where .collect::<Vec<String>>(); // 1. Replace the views id with new view id - let mut views_not_in_trash = imported_folder_data + let mut first_level_views = imported_folder_data .workspace .child_views .items @@ -1025,7 +689,7 @@ where .filter(|view| !trash_ids.contains(&view.id)) .collect::<Vec<ViewIdentifier>>(); - views_not_in_trash.iter_mut().for_each(|view_identifier| { + first_level_views.iter_mut().for_each(|view_identifier| { view_identifier.id = old_to_new_id_map.exchange_new_id(&view_identifier.id); }); @@ -1050,74 +714,27 @@ where .collect::<HashMap<String, View>>(); // 5. create the parent views. Each parent view contains the children views. - let parent_views = views_not_in_trash + let parent_views = first_level_views .into_iter() .flat_map( |view_identifier| match all_views_map.remove(&view_identifier.id) { - None => { - warn!( - "[AppflowyData]: Can't find the view:{} in the all views map", - view_identifier.id - ); - None - }, + None => None, Some(view) => parent_view_from_view(view, &mut all_views_map), }, ) .collect::<Vec<ParentChildViews>>(); // 6. after the parent views are created, the all_views_map only contains the orphan views - info!( - "[AppflowyData]: create orphan views: {:?}", - all_views_map.keys() - ); - let parent_views = NestedViews { - views: parent_views, - }; - + debug!("create orphan views: {:?}", all_views_map.keys()); let mut orphan_views = vec![]; - let mut invalid_orphan_views = vec![]; for orphan_view in all_views_map.into_values() { - // when the view's parent view id is the same as its view id, it will be considered as the orphan view - // currently, only the document in database row follows this rule - if orphan_view.id == orphan_view.parent_view_id { - orphan_views.push(ParentChildViews { - view: orphan_view, - children: vec![], - }); - continue; - } - - if parent_views - .find_view(&orphan_view.parent_view_id) - .is_none() - { - invalid_orphan_views.push(ParentChildViews { - view: orphan_view, - children: vec![], - }); - } else { - orphan_views.push(ParentChildViews { - view: orphan_view, - children: vec![], - }); - } + orphan_views.push(ParentChildViews { + parent_view: orphan_view, + child_views: vec![], + }); } - info!( - "[AppflowyData]: parent views: {}, orphan views: {}, invalid orphan views: {}, views without collab data: {}", - parent_views.len(), - orphan_views.len(), - invalid_orphan_views.len(), - not_exist_parent_view_ids.len() - ); - - Ok(MigrateViews { - child_views: parent_views.views, - orphan_views, - invalid_orphan_views, - not_exist_parent_view_ids, - }) + Ok((parent_views, orphan_views)) } fn parent_view_from_view( @@ -1136,8 +753,8 @@ fn parent_view_from_view( .collect::<Vec<ParentChildViews>>(); Some(ParentChildViews { - view: parent_view, - children: child_views, + parent_view, + child_views, }) } @@ -1155,10 +772,6 @@ impl OldToNewIdMap { .or_insert(gen_view_id().to_string()); (*view_id).clone() } - - fn get_exchanged_id(&self, old_id: &str) -> Option<&String> { - self.0.get(old_id) - } } impl Deref for OldToNewIdMap { @@ -1179,9 +792,9 @@ impl DerefMut for OldToNewIdMap { pub async fn upload_collab_objects_data( uid: i64, user_collab_db: Weak<CollabKVDB>, - workspace_id: &Uuid, - user_authenticator: &AuthType, - collab_data: ImportedCollabData, + workspace_id: &str, + user_authenticator: &Authenticator, + appflowy_data: AppFlowyData, user_cloud_service: Arc<dyn UserCloudService>, ) -> Result<(), FlowyError> { // Only support uploading the collab data when the current server is AppFlowy Cloud server @@ -1189,92 +802,72 @@ pub async fn upload_collab_objects_data( return Ok(()); } - let ImportedCollabData { - row_object_ids, - document_object_ids, - database_object_ids, - } = collab_data; - { - let cloned_workspace_id = workspace_id.to_string(); - let object_by_collab_type = tokio::task::spawn_blocking(move || { - let user_collab_db = user_collab_db.upgrade().ok_or_else(|| { - FlowyError::internal().with_context( - "The collab db has been dropped, indicating that the user has switched to a new account", - ) - })?; + match appflowy_data { + AppFlowyData::Folder { .. } => {}, + AppFlowyData::CollabObject { + row_object_ids, + document_object_ids, + database_object_ids, + } => { + let object_by_collab_type = tokio::task::spawn_blocking(move || { + let user_collab_db = user_collab_db.upgrade().ok_or_else(|| { + FlowyError::internal().with_context("The collab db has been dropped, indicating that the user has switched to a new account") + })?; - let collab_read = user_collab_db.read_txn(); - let mut object_by_collab_type = HashMap::new(); + let collab_read = user_collab_db.read_txn(); + let mut object_by_collab_type = HashMap::new(); - event!( - tracing::Level::DEBUG, - "[AppflowyData]:upload database collab data" - ); - object_by_collab_type.insert( - CollabType::Database, - load_and_process_collab_data( - uid, - &cloned_workspace_id, - &collab_read, - &database_object_ids, - ), - ); + event!(tracing::Level::DEBUG, "upload database collab data"); + object_by_collab_type.insert( + CollabType::Database, + load_and_process_collab_data(uid, &collab_read, &database_object_ids), + ); - event!( - tracing::Level::DEBUG, - "[AppflowyData]:upload document collab data" - ); - object_by_collab_type.insert( - CollabType::Document, - load_and_process_collab_data( - uid, - &cloned_workspace_id, - &collab_read, - &document_object_ids, - ), - ); + event!(tracing::Level::DEBUG, "upload document collab data"); + object_by_collab_type.insert( + CollabType::Document, + load_and_process_collab_data(uid, &collab_read, &document_object_ids), + ); - event!( - tracing::Level::DEBUG, - "[AppflowyData]:upload database row collab data" - ); - object_by_collab_type.insert( - CollabType::DatabaseRow, - load_and_process_collab_data(uid, &cloned_workspace_id, &collab_read, &row_object_ids), - ); - Ok::<_, FlowyError>(object_by_collab_type) - }) - .await??; + event!(tracing::Level::DEBUG, "upload database row collab data"); + object_by_collab_type.insert( + CollabType::DatabaseRow, + load_and_process_collab_data(uid, &collab_read, &row_object_ids), + ); + Ok::<_, FlowyError>(object_by_collab_type) + }) + .await??; - let mut size_counter = 0; - let mut objects: Vec<UserCollabParams> = vec![]; - for (collab_type, encoded_collab_by_oid) in object_by_collab_type { - for (oid, encoded_collab) in encoded_collab_by_oid { - let obj_size = encoded_collab.len(); - // Add the current object to the batch. - objects.push(UserCollabParams { - object_id: oid, - encoded_collab, - collab_type, - }); - size_counter += obj_size; + let mut size_counter = 0; + let mut objects: Vec<UserCollabParams> = vec![]; + for (collab_type, encoded_collab_by_oid) in object_by_collab_type { + for (oid, encoded_collab) in encoded_collab_by_oid { + let obj_size = encoded_collab.len(); + // Add the current object to the batch. + objects.push(UserCollabParams { + object_id: oid, + encoded_collab, + collab_type: collab_type.clone(), + }); + size_counter += obj_size; + } } - } - // Spawn a new task to upload the collab objects data in the background. If the - // upload fails, we will retry the upload later. - // tokio::spawn(async move { - if !objects.is_empty() { - batch_create( - uid, - workspace_id, - &user_cloud_service, - &size_counter, - objects, - ) - .await; - } - // }); + // Spawn a new task to upload the collab objects data in the background. If the + // upload fails, we will retry the upload later. + // af_spawn(async move { + if !objects.is_empty() { + batch_create( + uid, + workspace_id, + &user_cloud_service, + &size_counter, + objects, + ) + .await; + } + // }); + }, } Ok(()) @@ -1282,25 +875,30 @@ pub async fn upload_collab_objects_data( async fn batch_create( uid: i64, - workspace_id: &Uuid, + workspace_id: &str, user_cloud_service: &Arc<dyn UserCloudService>, size_counter: &usize, objects: Vec<UserCollabParams>, ) { + let ids = objects + .iter() + .map(|o| o.object_id.clone()) + .collect::<Vec<_>>() + .join(", "); match user_cloud_service .batch_create_collab_object(workspace_id, objects) .await { Ok(_) => { info!( - "[AppflowyData]:Batch creating collab objects success, origin payload size: {}", + "Batch creating collab objects success, origin payload size: {}", size_counter ); }, Err(err) => { error!( - "[AppflowyData]:Batch creating collab objects fail, origin payload size: {}, workspace_id:{}, uid: {}, error: {:?}", - size_counter, workspace_id, uid,err + "Batch creating collab objects fail:{}, origin payload size: {}, workspace_id:{}, uid: {}, error: {:?}", + ids, size_counter, workspace_id, uid,err ); }, } @@ -1309,7 +907,6 @@ async fn batch_create( #[instrument(level = "debug", skip_all)] fn load_and_process_collab_data<'a, R>( uid: i64, - workspace_id: &str, collab_read: &R, object_ids: &[String], ) -> HashMap<String, Vec<u8>> @@ -1317,8 +914,7 @@ where R: CollabKVAction<'a>, PersistenceError: From<R::Error>, { - load_collab_by_object_ids(uid, workspace_id, collab_read, object_ids) - .0 + load_collab_by_oid(uid, collab_read, object_ids) .into_iter() .filter_map(|(oid, collab)| { collab @@ -1330,70 +926,3 @@ where }) .collect() } - -fn replace_document_ref_ids( - document: &mut Document, - ids_map: &OldToNewIdMap, -) -> Result<(), anyhow::Error> { - if let Some(page_id) = document.get_page_id() { - // Get all block children and process them - let block_ids = document.get_block_children_ids(&page_id); - - for block_id in block_ids.iter() { - // Process block deltas - - let block_delta = document.get_block_delta(block_id).map(|t| t.1); - if let Some(mut block_deltas) = block_delta { - let mut is_change = false; - for d in block_deltas.iter_mut() { - if let TextDelta::Inserted(_, Some(attrs)) = d { - if let Some(Any::Map(mention)) = attrs.get_mut("mention") { - if let Some(page_id) = mention.get("page_id").map(|v| v.to_string()) { - if let Some(new_page_id) = ids_map.get_exchanged_id(&page_id) { - let mention = Arc::make_mut(mention); - mention.insert("page_id".to_string(), Any::from(new_page_id.clone())); - is_change = true; - } - } - } - } - } - - if is_change { - let _ = document.set_block_delta(block_id, block_deltas); - } - } - - // Process block data - if let Some((_block_type, mut data)) = document.get_block_data(block_id) { - println!("block data: {:?}", data); - let mut updated = false; - if let Some(view_id) = data.get("view_id").and_then(|v| v.as_str()) { - if let Some(new_view_id) = ids_map.get_exchanged_id(view_id) { - data.insert("view_id".to_string(), json!(new_view_id)); - updated = true; - } - } - - if let Some(parent_id) = data.get("parent_id").and_then(|v| v.as_str()) { - if let Some(new_parent_id) = ids_map.get_exchanged_id(parent_id) { - data.insert("parent_id".to_string(), json!(new_parent_id)); - updated = true; - } - } - - // Apply updates only if any changes were made - if updated { - println!("update block data: {:?}", data); - document.update_block(block_id, data).map_err(|err| { - anyhow::Error::msg(format!( - "[AppFlowyData]: update document block data: {}", - err - )) - })?; - } - } - } - } - Ok(()) -} diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs index 3f01934ab7..b1dee99774 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs @@ -1,47 +1,30 @@ use collab::preclude::Collab; use collab_integrate::{CollabKVAction, PersistenceError}; use std::collections::HashMap; +use tracing::instrument; -/// This function loads collab objects by their object_ids. -pub fn load_collab_by_object_ids<'a, R>( +#[instrument(level = "debug", skip_all)] +pub fn load_collab_by_oid<'a, R>( uid: i64, - workspace_id: &str, collab_read_txn: &R, object_ids: &[String], -) -> (HashMap<String, Collab>, Vec<String>) +) -> HashMap<String, Collab> where R: CollabKVAction<'a>, PersistenceError: From<R::Error>, { - let mut invalid_object_ids = vec![]; let mut collab_by_oid = HashMap::new(); for object_id in object_ids { - match load_collab_by_object_id(uid, collab_read_txn, workspace_id, object_id) { - Ok(collab) => { + let collab = Collab::new(uid, object_id, "phantom", vec![], false); + match collab + .with_origin_transact_mut(|txn| collab_read_txn.load_doc_with_txn(uid, &object_id, txn)) + { + Ok(_) => { collab_by_oid.insert(object_id.clone(), collab); }, - Err(err) => { - invalid_object_ids.push(object_id.clone()); - tracing::error!("🔴load collab: {} failed: {:?} ", object_id, err) - }, + Err(err) => tracing::error!("🔴import collab:{} failed: {:?} ", object_id, err), } } - (collab_by_oid, invalid_object_ids) -} - -/// This function loads single collab object by its object_id. -pub fn load_collab_by_object_id<'a, R>( - uid: i64, - collab_read_txn: &R, - workspace_id: &str, - object_id: &str, -) -> Result<Collab, PersistenceError> -where - R: CollabKVAction<'a>, - PersistenceError: From<R::Error>, -{ - let mut collab = Collab::new(uid, object_id, "phantom", vec![], false); - collab_read_txn.load_doc_with_txn(uid, workspace_id, object_id, &mut collab.transact_mut())?; - Ok(collab) + collab_by_oid } diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs b/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs index 9fec671ade..2e5ddf9603 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/mod.rs @@ -2,5 +2,4 @@ mod appflowy_data_import; pub use appflowy_data_import::*; pub(crate) mod importer; -pub use importer::load_collab_by_object_id; -pub use importer::load_collab_by_object_ids; +pub use importer::load_collab_by_oid; diff --git a/frontend/rust-lib/flowy-user/src/services/db.rs b/frontend/rust-lib/flowy-user/src/services/db.rs index 15126558d7..3305fca41a 100644 --- a/frontend/rust-lib/flowy-user/src/services/db.rs +++ b/frontend/rust-lib/flowy-user/src/services/db.rs @@ -1,19 +1,26 @@ use std::path::{Path, PathBuf}; -use std::{fs, io, sync::Arc}; +use std::{collections::HashMap, fs, io, sync::Arc, time::Duration}; use chrono::{Days, Local}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; use collab_plugins::local_storage::kv::KVTransactionDB; -use dashmap::mapref::entry::Entry; -use dashmap::DashMap; use flowy_error::FlowyError; +use flowy_sqlite::schema::user_workspace_table; use flowy_sqlite::ConnectionPool; -use flowy_sqlite::{DBConnection, Database}; -use flowy_user_pub::entities::UserProfile; -use flowy_user_pub::sql::select_user_profile; +use flowy_sqlite::{ + query_dsl::*, + schema::{user_table, user_table::dsl}, + DBConnection, Database, ExpressionMethods, +}; +use flowy_user_pub::entities::{UserProfile, UserWorkspace}; +use lib_dispatch::prelude::af_spawn; use lib_infra::file_util::{unzip_and_replace, zip_folder}; +use parking_lot::RwLock; use tracing::{error, event, info, instrument}; +use crate::services::sqlite_sql::user_sql::UserTable; +use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; + pub trait UserDBPath: Send + Sync + 'static { fn sqlite_db_path(&self, uid: i64) -> PathBuf; fn collab_db_path(&self, uid: i64) -> PathBuf; @@ -22,8 +29,8 @@ pub trait UserDBPath: Send + Sync + 'static { pub struct UserDB { paths: Box<dyn UserDBPath>, - sqlite_map: DashMap<i64, Database>, - collab_db_map: DashMap<i64, Arc<CollabKVDB>>, + sqlite_map: RwLock<HashMap<i64, Database>>, + collab_db_map: RwLock<HashMap<i64, Arc<CollabKVDB>>>, } impl UserDB { @@ -36,8 +43,18 @@ impl UserDB { } /// Performs a conditional backup or restoration of the collaboration database (CollabDB) for a specific user. + /// + /// This function takes a user ID and conducts the following operations: + /// + /// **Backup or Restoration**: + /// - If the CollabDB exists, it tries to open the database: + /// - **Successful Open**: If the database opens successfully, it attempts to back it up. + /// - **Failed Open**: If the database cannot be opened, it indicates a potential issue, and the function + /// attempts to restore the database from the latest backup. + /// - If the CollabDB does not exist, it immediately attempts to restore from the latest backup. + /// #[instrument(level = "debug", skip_all)] - pub fn backup(&self, uid: i64, workspace_id: &str) { + pub fn backup_or_restore(&self, uid: i64, workspace_id: &str) { // Obtain the path for the collaboration database. let collab_db_path = self.paths.collab_db_path(uid); @@ -45,18 +62,23 @@ impl UserDB { if let Ok(history_folder) = self.paths.collab_db_history(uid, true) { // Initialize the backup utility for the collaboration database. let zip_backup = CollabDBZipBackup::new(collab_db_path.clone(), history_folder); + if collab_db_path.exists() { // Validate the existing collaboration database. let result = self.open_collab_db(collab_db_path, uid); let is_ok = validate_collab_db(result, uid, workspace_id); + if is_ok { // If database is valid, update the shared map and initiate backup. // Asynchronous backup operation. - tokio::spawn(async move { + af_spawn(async move { if let Err(err) = tokio::task::spawn_blocking(move || zip_backup.backup()).await { error!("Backup of collab db failed: {:?}", err); } }); + } else if let Err(err) = zip_backup.restore_latest_backup() { + // If validation fails, attempt to restore from the latest backup. + error!("Restoring collab db failed: {:?}", err); } } } @@ -73,16 +95,35 @@ impl UserDB { vec![] } + #[instrument(level = "debug", skip_all)] + pub fn restore_if_need(&self, uid: i64, workspace_id: &str) { + if let Ok(history_folder) = self.paths.collab_db_history(uid, false) { + let collab_db_path = self.paths.collab_db_path(uid); + let result = self.open_collab_db(&collab_db_path, uid); + let is_ok = validate_collab_db(result, uid, workspace_id); + if !is_ok { + let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder); + if let Err(err) = zip_backup.restore_latest_backup() { + error!("restore collab db failed, {:?}", err); + } + } + } + } + /// Close the database connection for the user. pub(crate) fn close(&self, user_id: i64) -> Result<(), FlowyError> { - if self.sqlite_map.remove(&user_id).is_some() { - tracing::trace!("close sqlite db for user {}", user_id); + if let Some(mut sqlite_dbs) = self.sqlite_map.try_write_for(Duration::from_millis(300)) { + if sqlite_dbs.remove(&user_id).is_some() { + tracing::trace!("close sqlite db for user {}", user_id); + } } - if let Some((_, db)) = self.collab_db_map.remove(&user_id) { - tracing::trace!("close collab db for user {}", user_id); - let _ = db.flush(); - drop(db); + if let Some(mut collab_dbs) = self.collab_db_map.try_write_for(Duration::from_millis(300)) { + if let Some(db) = collab_dbs.remove(&user_id) { + tracing::trace!("close collab db for user {}", user_id); + let _ = db.flush(); + drop(db); + } } Ok(()) } @@ -107,29 +148,44 @@ impl UserDB { db_path: impl AsRef<Path>, user_id: i64, ) -> Result<Arc<ConnectionPool>, FlowyError> { - match self.sqlite_map.entry(user_id) { - Entry::Occupied(e) => Ok(e.get().get_pool()), - Entry::Vacant(e) => { - tracing::debug!("open sqlite db {} at path: {:?}", user_id, db_path.as_ref()); - let db = flowy_sqlite::init(&db_path).map_err(|e| { - FlowyError::internal().with_context(format!("open user db failed, {:?}", e)) - })?; - let pool = db.get_pool(); - e.insert(db); - Ok(pool) - }, + if let Some(database) = self.sqlite_map.read().get(&user_id) { + return Ok(database.get_pool()); } + + let mut write_guard = self.sqlite_map.write(); + tracing::debug!("open sqlite db {} at path: {:?}", user_id, db_path.as_ref()); + let db = flowy_sqlite::init(&db_path) + .map_err(|e| FlowyError::internal().with_context(format!("open user db failed, {:?}", e)))?; + let pool = db.get_pool(); + write_guard.insert(user_id.to_owned(), db); + drop(write_guard); + Ok(pool) } pub fn get_user_profile( &self, pool: &Arc<ConnectionPool>, uid: i64, - workspace_id: &str, ) -> Result<UserProfile, FlowyError> { + let uid = uid.to_string(); let mut conn = pool.get()?; - let profile = select_user_profile(uid, workspace_id, &mut conn)?; - Ok(profile) + let user = dsl::user_table + .filter(user_table::id.eq(&uid)) + .first::<UserTable>(&mut *conn)?; + + Ok(user.into()) + } + + pub fn get_user_workspace( + &self, + pool: &Arc<ConnectionPool>, + uid: i64, + ) -> Result<Option<UserWorkspace>, FlowyError> { + let mut conn = pool.get()?; + let row = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(uid)) + .first::<UserWorkspaceTable>(&mut *conn)?; + Ok(Some(UserWorkspace::from(row))) } /// Open a collab db for the user. If the db is already opened, return the opened db. @@ -139,27 +195,28 @@ impl UserDB { collab_db_path: impl AsRef<Path>, uid: i64, ) -> Result<Arc<CollabKVDB>, PersistenceError> { - match self.collab_db_map.entry(uid) { - Entry::Occupied(e) => Ok(e.get().clone()), - Entry::Vacant(e) => { - info!( - "open collab db for user {} at path: {:?}", - uid, - collab_db_path.as_ref() - ); - let db = match CollabKVDB::open(&collab_db_path) { - Ok(db) => Ok(db), - Err(err) => { - error!("open collab db error, {:?}", err); - Err(err) - }, - }?; - - let db = Arc::new(db); - e.insert(db.clone()); - Ok(db) - }, + if let Some(collab_db) = self.collab_db_map.read().get(&uid) { + return Ok(collab_db.clone()); } + + let mut write_guard = self.collab_db_map.write(); + info!( + "open collab db for user {} at path: {:?}", + uid, + collab_db_path.as_ref() + ); + let db = match CollabKVDB::open(&collab_db_path) { + Ok(db) => Ok(db), + Err(err) => { + error!("open collab db error, {:?}", err); + Err(err) + }, + }?; + + let db = Arc::new(db); + write_guard.insert(uid.to_owned(), db.clone()); + drop(write_guard); + Ok(db) } } @@ -224,9 +281,8 @@ impl CollabDBZipBackup { Ok(backups) } - #[deprecated(note = "This function is deprecated", since = "0.7.1")] - #[allow(dead_code)] - fn restore_latest_backup(&self) -> io::Result<()> { + #[instrument(skip_all, err)] + pub fn restore_latest_backup(&self) -> io::Result<()> { let mut latest_zip: Option<(String, PathBuf)> = None; // When the history folder does not exist, there is no backup to restore if !self.history_folder.exists() { @@ -337,7 +393,7 @@ pub(crate) fn validate_collab_db( match result { Ok(db) => { let read_txn = db.read_txn(); - read_txn.is_exist(uid, workspace_id, workspace_id) + read_txn.is_exist(uid, workspace_id) }, Err(err) => { error!("open collab db error, {:?}", err); diff --git a/frontend/rust-lib/flowy-user/src/services/entities.rs b/frontend/rust-lib/flowy-user/src/services/entities.rs index 4e034b3bdb..831ef10751 100644 --- a/frontend/rust-lib/flowy-user/src/services/entities.rs +++ b/frontend/rust-lib/flowy-user/src/services/entities.rs @@ -63,11 +63,6 @@ impl UserPaths { pub(crate) fn user_data_dir(&self, uid: i64) -> String { format!("{}/{}", self.root, uid) } - - /// The root directory of the application - pub(crate) fn root(&self) -> &str { - &self.root - } } impl UserDBPath for UserPaths { diff --git a/frontend/rust-lib/flowy-user/src/services/mod.rs b/frontend/rust-lib/flowy-user/src/services/mod.rs index ab4b3bea37..f7fc8ae7b6 100644 --- a/frontend/rust-lib/flowy-user/src/services/mod.rs +++ b/frontend/rust-lib/flowy-user/src/services/mod.rs @@ -1,7 +1,7 @@ pub mod authenticate_user; -pub(crate) mod billing_check; pub mod cloud_config; pub mod collab_interact; pub mod data_import; pub mod db; pub mod entities; +pub mod sqlite_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs new file mode 100644 index 0000000000..70351ab105 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/member_sql.rs @@ -0,0 +1,52 @@ +use diesel::{insert_into, RunQueryDsl}; +use flowy_error::FlowyResult; + +use flowy_sqlite::schema::workspace_members_table; + +use flowy_sqlite::schema::workspace_members_table::dsl; +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; + +#[derive(Queryable, Insertable, AsChangeset, Debug)] +#[diesel(table_name = workspace_members_table)] +#[diesel(primary_key(email, workspace_id))] +pub struct WorkspaceMemberTable { + pub email: String, + pub role: i32, + pub name: String, + pub avatar_url: Option<String>, + pub uid: i64, + pub workspace_id: String, + pub updated_at: chrono::NaiveDateTime, +} + +pub fn upsert_workspace_member<T: Into<WorkspaceMemberTable>>( + mut conn: DBConnection, + member: T, +) -> FlowyResult<()> { + let member = member.into(); + + insert_into(workspace_members_table::table) + .values(&member) + .on_conflict(( + workspace_members_table::email, + workspace_members_table::workspace_id, + )) + .do_update() + .set(&member) + .execute(&mut conn)?; + + Ok(()) +} + +pub fn select_workspace_member( + mut conn: DBConnection, + workspace_id: &str, + uid: i64, +) -> FlowyResult<WorkspaceMemberTable> { + let member = dsl::workspace_members_table + .filter(workspace_members_table::workspace_id.eq(workspace_id)) + .filter(workspace_members_table::uid.eq(uid)) + .first::<WorkspaceMemberTable>(&mut conn)?; + + Ok(member) +} diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs new file mode 100644 index 0000000000..93e642f72e --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod member_sql; +pub(crate) mod user_sql; +pub(crate) mod workspace_sql; diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs new file mode 100644 index 0000000000..71d3fd50b1 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/user_sql.rs @@ -0,0 +1,154 @@ +use diesel::{sql_query, RunQueryDsl}; +use flowy_error::{internal_error, FlowyError}; +use std::str::FromStr; + +use flowy_user_pub::cloud::UserUpdate; +use flowy_user_pub::entities::*; + +use flowy_sqlite::schema::user_table; + +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +/// The order of the fields in the struct must be the same as the order of the fields in the table. +/// Check out the [schema.rs] for table schema. +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_table)] +pub struct UserTable { + pub(crate) id: String, + pub(crate) name: String, + pub(crate) workspace: String, + pub(crate) icon_url: String, + pub(crate) openai_key: String, + pub(crate) token: String, + pub(crate) email: String, + pub(crate) auth_type: i32, + pub(crate) encryption_type: String, + pub(crate) stability_ai_key: String, + pub(crate) updated_at: i64, +} + +impl UserTable { + pub fn set_workspace(mut self, workspace: String) -> Self { + self.workspace = workspace; + self + } +} + +impl From<(UserProfile, Authenticator)> for UserTable { + fn from(value: (UserProfile, Authenticator)) -> Self { + let (user_profile, auth_type) = value; + let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); + UserTable { + id: user_profile.uid.to_string(), + name: user_profile.name, + workspace: user_profile.workspace_id, + icon_url: user_profile.icon_url, + openai_key: user_profile.openai_key, + token: user_profile.token, + email: user_profile.email, + auth_type: auth_type as i32, + encryption_type, + stability_ai_key: user_profile.stability_ai_key, + updated_at: user_profile.updated_at, + } + } +} + +impl From<UserTable> for UserProfile { + fn from(table: UserTable) -> Self { + UserProfile { + uid: table.id.parse::<i64>().unwrap_or(0), + email: table.email, + name: table.name, + token: table.token, + icon_url: table.icon_url, + openai_key: table.openai_key, + workspace_id: table.workspace, + authenticator: Authenticator::from(table.auth_type), + encryption_type: EncryptionType::from_str(&table.encryption_type).unwrap_or_default(), + stability_ai_key: table.stability_ai_key, + updated_at: table.updated_at, + } + } +} + +#[derive(AsChangeset, Identifiable, Default, Debug)] +#[diesel(table_name = user_table)] +pub struct UserTableChangeset { + pub id: String, + pub workspace: Option<String>, // deprecated + pub name: Option<String>, + pub email: Option<String>, + pub icon_url: Option<String>, + pub openai_key: Option<String>, + pub encryption_type: Option<String>, + pub token: Option<String>, + pub stability_ai_key: Option<String>, +} + +impl UserTableChangeset { + pub fn new(params: UpdateUserProfileParams) -> Self { + let encryption_type = params.encryption_sign.map(|sign| { + let ty = EncryptionType::from_sign(&sign); + serde_json::to_string(&ty).unwrap_or_default() + }); + UserTableChangeset { + id: params.uid.to_string(), + workspace: None, + name: params.name, + email: params.email, + icon_url: params.icon_url, + openai_key: params.openai_key, + encryption_type, + token: params.token, + stability_ai_key: params.stability_ai_key, + } + } + + pub fn from_user_profile(user_profile: UserProfile) -> Self { + let encryption_type = serde_json::to_string(&user_profile.encryption_type).unwrap_or_default(); + UserTableChangeset { + id: user_profile.uid.to_string(), + workspace: None, + name: Some(user_profile.name), + email: Some(user_profile.email), + icon_url: Some(user_profile.icon_url), + openai_key: Some(user_profile.openai_key), + encryption_type: Some(encryption_type), + token: Some(user_profile.token), + stability_ai_key: Some(user_profile.stability_ai_key), + } + } +} + +impl From<UserUpdate> for UserTableChangeset { + fn from(value: UserUpdate) -> Self { + UserTableChangeset { + id: value.uid.to_string(), + name: value.name, + email: value.email, + ..Default::default() + } + } +} + +pub fn select_user_profile(uid: i64, mut conn: DBConnection) -> Result<UserProfile, FlowyError> { + let user: UserProfile = user_table::dsl::user_table + .filter(user_table::id.eq(&uid.to_string())) + .first::<UserTable>(&mut *conn) + .map_err(|err| { + FlowyError::record_not_found().with_context(format!( + "Can't find the user profile for user id: {}, error: {:?}", + uid, err + )) + })? + .into(); + + Ok(user) +} + +pub(crate) fn vacuum_database(mut conn: DBConnection) -> Result<(), FlowyError> { + sql_query("VACUUM") + .execute(&mut *conn) + .map_err(internal_error)?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs new file mode 100644 index 0000000000..6f438d82e1 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -0,0 +1,112 @@ +use chrono::{TimeZone, Utc}; +use diesel::{RunQueryDsl, SqliteConnection}; +use flowy_error::FlowyError; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; +use flowy_user_pub::entities::UserWorkspace; +use std::convert::TryFrom; + +#[derive(Clone, Default, Queryable, Identifiable, Insertable)] +#[diesel(table_name = user_workspace_table)] +pub struct UserWorkspaceTable { + pub id: String, + pub name: String, + pub uid: i64, + pub created_at: i64, + pub database_storage_id: String, + pub icon: String, +} + +pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option<UserWorkspace> { + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(workspace_id)) + .first::<UserWorkspaceTable>(&mut *conn) + .ok() + .map(UserWorkspace::from) +} + +pub fn get_all_user_workspace_op( + user_id: i64, + mut conn: DBConnection, +) -> Result<Vec<UserWorkspace>, FlowyError> { + let rows = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(user_id)) + .load::<UserWorkspaceTable>(&mut *conn)?; + Ok(rows.into_iter().map(UserWorkspace::from).collect()) +} + +/// Remove all existing workspaces for given user and insert the new ones. +/// +#[allow(dead_code)] +pub fn save_user_workspaces_op( + uid: i64, + mut conn: DBConnection, + user_workspaces: &[UserWorkspace], +) -> Result<(), FlowyError> { + conn.immediate_transaction(|conn| { + delete_existing_workspaces(uid, conn)?; + insert_new_workspaces_op(uid, user_workspaces, conn)?; + Ok(()) + }) +} + +#[allow(dead_code)] +fn delete_existing_workspaces(uid: i64, conn: &mut SqliteConnection) -> Result<(), FlowyError> { + diesel::delete( + user_workspace_table::dsl::user_workspace_table.filter(user_workspace_table::uid.eq(uid)), + ) + .execute(conn)?; + Ok(()) +} + +pub fn insert_new_workspaces_op( + uid: i64, + user_workspaces: &[UserWorkspace], + conn: &mut SqliteConnection, +) -> Result<(), FlowyError> { + for user_workspace in user_workspaces { + let new_record = UserWorkspaceTable::try_from((uid, user_workspace))?; + diesel::insert_into(user_workspace_table::table) + .values(new_record) + .execute(conn)?; + } + Ok(()) +} + +impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { + type Error = FlowyError; + + fn try_from(value: (i64, &UserWorkspace)) -> Result<Self, Self::Error> { + if value.1.id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The id is empty")); + } + if value.1.database_indexer_id.is_empty() { + return Err(FlowyError::invalid_data().with_context("The database storage id is empty")); + } + + Ok(Self { + id: value.1.id.clone(), + name: value.1.name.clone(), + uid: value.0, + created_at: value.1.created_at.timestamp(), + database_storage_id: value.1.database_indexer_id.clone(), + icon: value.1.icon.clone(), + }) + } +} + +impl From<UserWorkspaceTable> for UserWorkspace { + fn from(value: UserWorkspaceTable) -> Self { + Self { + id: value.id, + name: value.name, + created_at: Utc + .timestamp_opt(value.created_at, 0) + .single() + .unwrap_or_default(), + database_indexer_id: value.database_storage_id, + icon: value.icon, + } + } +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index b95ac3baaf..9a8a95181c 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -1,65 +1,69 @@ -use client_api::entity::GotrueTokenResponse; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use collab_integrate::CollabKVDB; -use flowy_error::FlowyResult; +use collab_user::core::MutexUserAwareness; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; -use arc_swap::ArcSwapOption; -use collab::lock::RwLock; -use collab_user::core::UserAwareness; -use dashmap::DashMap; -use flowy_sqlite::kv::KVStorePreferences; +use flowy_server_pub::AuthenticatorType; +use flowy_sqlite::kv::StorePreferences; use flowy_sqlite::schema::user_table; use flowy_sqlite::ConnectionPool; use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudServiceProvider, UserUpdate}; use flowy_user_pub::entities::*; use flowy_user_pub::workspace_service::UserWorkspaceService; -use lib_infra::box_any::BoxAny; use semver::Version; use serde_json::Value; use std::string::ToString; -use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::{Arc, Weak}; +use tokio::sync::{Mutex, RwLock}; use tokio_stream::StreamExt; -use tracing::{debug, error, event, info, instrument, warn}; -use uuid::Uuid; +use tracing::{debug, error, event, info, instrument, trace, warn}; +use lib_dispatch::prelude::af_spawn; +use lib_infra::box_any::BoxAny; + +use crate::anon_user::{migration_anon_user_on_sign_up, sync_supabase_user_data_to_cloud}; use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserStatusCallback}; use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration; use crate::migrations::migration::{ - save_migration_record, UserDataMigration, UserLocalDataMigration, FIRST_TIME_INSTALL_VERSION, + save_migration_record, UserDataMigration, UserLocalDataMigration, }; use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMigration; use crate::migrations::workspace_trash_v1::WorkspaceTrashMapToSectionMigration; use crate::migrations::AnonUser; use crate::services::authenticate_user::AuthenticateUser; use crate::services::cloud_config::get_cloud_config; -use crate::services::collab_interact::{DefaultCollabInteract, UserReminder}; +use crate::services::collab_interact::{CollabInteract, DefaultCollabInteract}; -use crate::migrations::anon_user_workspace::AnonUserWorkspaceTableMigration; -use crate::migrations::doc_key_with_workspace::CollabDocKeyWithWorkspaceIdMigration; +use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; +use crate::user_manager::manager_user_encryption::validate_encryption_sign; +use crate::user_manager::manager_user_workspace::save_all_user_workspaces; +use crate::user_manager::user_login_state::UserAuthProcess; use crate::{errors::FlowyError, notification::*}; use flowy_user_pub::session::Session; -use flowy_user_pub::sql::*; + +use super::manager_user_workspace::save_user_workspace; pub struct UserManager { - pub(crate) cloud_service: Arc<dyn UserCloudServiceProvider>, - pub(crate) store_preferences: Arc<KVStorePreferences>, - pub(crate) user_awareness: Arc<ArcSwapOption<RwLock<UserAwareness>>>, + pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>, + pub(crate) store_preferences: Arc<StorePreferences>, + pub(crate) user_awareness: Arc<Mutex<Option<MutexUserAwareness>>>, pub(crate) user_status_callback: RwLock<Arc<dyn UserStatusCallback>>, pub(crate) collab_builder: Weak<AppFlowyCollabBuilder>, - pub(crate) collab_interact: RwLock<Arc<dyn UserReminder>>, + pub(crate) collab_interact: RwLock<Arc<dyn CollabInteract>>, pub(crate) user_workspace_service: Arc<dyn UserWorkspaceService>, + auth_process: Mutex<Option<UserAuthProcess>>, pub(crate) authenticate_user: Arc<AuthenticateUser>, refresh_user_profile_since: AtomicI64, - pub(crate) is_loading_awareness: Arc<DashMap<Uuid, bool>>, + pub(crate) is_loading_awareness: Arc<AtomicBool>, } impl UserManager { pub fn new( cloud_services: Arc<dyn UserCloudServiceProvider>, - store_preferences: Arc<KVStorePreferences>, + store_preferences: Arc<StorePreferences>, collab_builder: Weak<AppFlowyCollabBuilder>, authenticate_user: Arc<AuthenticateUser>, user_workspace_service: Arc<dyn UserWorkspaceService>, @@ -69,22 +73,23 @@ impl UserManager { let refresh_user_profile_since = AtomicI64::new(0); let user_manager = Arc::new(Self { - cloud_service: cloud_services, + cloud_services, store_preferences, - user_awareness: Default::default(), + user_awareness: Arc::new(Default::default()), user_status_callback, collab_builder, collab_interact: RwLock::new(Arc::new(DefaultCollabInteract)), + auth_process: Default::default(), authenticate_user, refresh_user_profile_since, user_workspace_service, - is_loading_awareness: Arc::new(Default::default()), + is_loading_awareness: Arc::new(AtomicBool::new(false)), }); let weak_user_manager = Arc::downgrade(&user_manager); - if let Ok(user_service) = user_manager.cloud_service.get_user_service() { + if let Ok(user_service) = user_manager.cloud_services.get_user_service() { if let Some(mut rx) = user_service.subscribe_user_update() { - tokio::spawn(async move { + af_spawn(async move { while let Some(update) = rx.recv().await { if let Some(user_manager) = weak_user_manager.upgrade() { if let Err(err) = user_manager.handler_user_update(update).await { @@ -105,7 +110,7 @@ impl UserManager { } } - pub fn get_store_preferences(&self) -> Weak<KVStorePreferences> { + pub fn get_store_preferences(&self) -> Weak<StorePreferences> { Arc::downgrade(&self.store_preferences) } @@ -117,7 +122,7 @@ impl UserManager { /// the function will set up the collaboration configuration and initialize the user's awareness. Upon successful /// completion, a user status callback is invoked to signify that the initialization process is complete. #[instrument(level = "debug", skip_all, err)] - pub async fn init_with_callback<C: UserStatusCallback + 'static, I: UserReminder>( + pub async fn init_with_callback<C: UserStatusCallback + 'static, I: CollabInteract>( &self, user_status_callback: C, collab_interact: I, @@ -127,19 +132,30 @@ impl UserManager { *self.collab_interact.write().await = Arc::new(collab_interact); if let Ok(session) = self.get_session() { - let user = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; - let auth_type = user.workspace_auth_type; - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; + let user = self.get_user_profile_from_disk(session.user_id).await?; + + // Get the current authenticator from the environment variable + let current_authenticator = current_authenticator(); + + // If the current authenticator is different from the authenticator in the session and it's + // not a local authenticator, we need to sign out the user. + if user.authenticator != Authenticator::Local && user.authenticator != current_authenticator { + event!( + tracing::Level::INFO, + "Authenticator changed from {:?} to {:?}", + user.authenticator, + current_authenticator + ); + self.sign_out().await?; + return Ok(()); + } event!( tracing::Level::INFO, - "init user session: {}:{}, auth type: {:?}", + "init user session: {}:{}, authenticator: {:?}", user.uid, user.email, - auth_type, + user.authenticator, ); self.prepare_user(&session).await; @@ -148,22 +164,21 @@ impl UserManager { // Set the token if the current cloud service using token to authenticate // Currently, only the AppFlowy cloud using token to init the client api. // TODO(nathan): using trait to separate the init process for different cloud service - if user.auth_type.is_appflowy_cloud() { - if let Err(err) = self.cloud_service.set_token(&user.token) { + if user.authenticator.is_appflowy_cloud() { + if let Err(err) = self.cloud_services.set_token(&user.token) { error!("Set token failed: {}", err); } // Subscribe the token state - let weak_cloud_services = Arc::downgrade(&self.cloud_service); + let weak_cloud_services = Arc::downgrade(&self.cloud_services); let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); let cloned_session = session.clone(); - if let Some(mut token_state_rx) = self.cloud_service.subscribe_token_state() { + if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; let local_token = user.token.clone(); - let workspace_id = session.user_workspace.id.clone(); - tokio::spawn(async move { + af_spawn(async move { while let Some(token_state) = token_state_rx.next().await { debug!("Token state changed: {:?}", token_state); match token_state { @@ -172,7 +187,7 @@ impl UserManager { if new_token != local_token { if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { // Save the new token - if let Err(err) = save_user_token(user_uid, &workspace_id, conn, new_token) { + if let Err(err) = save_user_token(user_uid, conn, new_token) { error!("Save user token failed: {}", err); } } @@ -226,7 +241,7 @@ impl UserManager { } } - // Do the user data migration if needed. + // Do the user data migration if needed event!(tracing::Level::INFO, "Prepare user data migration"); match ( self @@ -236,59 +251,35 @@ impl UserManager { self.authenticate_user.database.get_pool(session.user_id), ) { (Ok(collab_db), Ok(sqlite_pool)) => { - run_data_migration( + run_collab_data_migration( &session, - &user.auth_type, + &user, collab_db, sqlite_pool, - self.store_preferences.clone(), - &self.authenticate_user.user_config.app_version, + Some(self.authenticate_user.user_config.app_version.clone()), ); }, _ => error!("Failed to get collab db or sqlite pool"), } - - // migrations should run before set the first time installed version - self.set_first_time_installed_version(); + self.authenticate_user.vacuum_database_if_need(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); - // Init the user awareness. here we ignore the error - let _ = self.initial_user_awareness(&session, &auth_type).await; + // Init the user awareness + self.initialize_user_awareness(&session).await; user_status_callback - .on_launch_if_authenticated( + .did_init( user.uid, + &user.authenticator, &cloud_config, &session.user_workspace, &self.authenticate_user.user_config.device_id, - &auth_type, ) .await?; - } else { - self.set_first_time_installed_version(); } Ok(()) } - fn set_first_time_installed_version(&self) { - if self - .store_preferences - .get_str(FIRST_TIME_INSTALL_VERSION) - .is_none() - { - info!( - "Set install version: {:?}", - self.authenticate_user.user_config.app_version - ); - if let Err(err) = self.store_preferences.set_object( - FIRST_TIME_INSTALL_VERSION, - &self.authenticate_user.user_config.app_version, - ) { - error!("Set install version error: {:?}", err); - } - } - } - - pub fn get_session(&self) -> FlowyResult<Arc<Session>> { + pub fn get_session(&self) -> FlowyResult<Session> { self.authenticate_user.get_session() } @@ -325,12 +316,12 @@ impl UserManager { pub async fn sign_in( &self, params: SignInParams, - auth_type: AuthType, + authenticator: Authenticator, ) -> Result<UserProfile, FlowyError> { - self.cloud_service.set_server_auth_type(&auth_type, None)?; + self.cloud_services.set_user_authenticator(&authenticator); let response: AuthResponse = self - .cloud_service + .cloud_services .get_user_service()? .sign_in(BoxAny::new(params)) .await?; @@ -338,21 +329,20 @@ impl UserManager { self.prepare_user(&session).await; let latest_workspace = response.latest_workspace.clone(); - let user_profile = UserProfile::from((&response, &auth_type)); - self.save_auth_data(&response, auth_type, &session).await?; + let user_profile = UserProfile::from((&response, &authenticator)); + self + .save_auth_data(&response, &authenticator, &session) + .await?; - let _ = self - .initial_user_awareness(&session, &user_profile.workspace_auth_type) - .await; + let _ = self.initialize_user_awareness(&session).await; self .user_status_callback .read() .await - .on_sign_in( + .did_sign_in( user_profile.uid, &latest_workspace, &self.authenticate_user.user_config.device_id, - &auth_type, ) .await?; send_auth_state_notification(AuthStateChangedPB { @@ -372,46 +362,76 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_up( &self, - auth_type: AuthType, + authenticator: Authenticator, params: BoxAny, ) -> Result<UserProfile, FlowyError> { - self.cloud_service.set_server_auth_type(&auth_type, None)?; - // sign out the current user if there is one - let migration_user = self.get_migration_user(&auth_type).await; - let auth_service = self.cloud_service.get_user_service()?; + let migration_user = self.get_migration_user(&authenticator).await; + + self.cloud_services.set_user_authenticator(&authenticator); + let auth_service = self.cloud_services.get_user_service()?; let response: AuthResponse = auth_service.sign_up(params).await?; - let new_user_profile = UserProfile::from((&response, &auth_type)); - self - .continue_sign_up(&new_user_profile, migration_user, response, &auth_type) - .await?; + let new_user_profile = UserProfile::from((&response, &authenticator)); + if new_user_profile.encryption_type.require_encrypt_secret() { + self.auth_process.lock().await.replace(UserAuthProcess { + user_profile: new_user_profile.clone(), + migration_user, + response, + authenticator, + }); + } else { + self + .continue_sign_up(&new_user_profile, migration_user, response, &authenticator) + .await?; + } Ok(new_user_profile) } + #[tracing::instrument(level = "info", skip(self))] + pub async fn resume_sign_up(&self) -> Result<(), FlowyError> { + let UserAuthProcess { + user_profile, + migration_user, + response, + authenticator, + } = self + .auth_process + .lock() + .await + .clone() + .ok_or(FlowyError::new( + ErrorCode::Internal, + "No resumable sign up data", + ))?; + self + .continue_sign_up(&user_profile, migration_user, response, &authenticator) + .await?; + Ok(()) + } + #[tracing::instrument(level = "info", skip_all, err)] async fn continue_sign_up( &self, new_user_profile: &UserProfile, migration_user: Option<AnonUser>, response: AuthResponse, - auth_type: &AuthType, + authenticator: &Authenticator, ) -> FlowyResult<()> { let new_session = Session::from(&response); self.prepare_user(&new_session).await; self - .save_auth_data(&response, *auth_type, &new_session) + .save_auth_data(&response, authenticator, &new_session) .await?; - let _ = self.initial_user_awareness(&new_session, auth_type).await; + let _ = self.try_initial_user_awareness(&new_session).await; self .user_status_callback .read() .await - .on_sign_up( + .did_sign_up( response.is_new_user, new_user_profile, &new_session.user_workspace, &self.authenticate_user.user_config.device_id, - auth_type, ) .await?; @@ -435,7 +455,7 @@ impl UserManager { new_user_profile.uid ); self - .migrate_anon_user_data_to_cloud(&old_user, &new_session, auth_type) + .migrate_anon_user_data_to_cloud(&old_user, &new_session, authenticator) .await?; self.remove_anon_user(); let _ = self @@ -456,7 +476,7 @@ impl UserManager { pub async fn sign_out(&self) -> Result<(), FlowyError> { if let Ok(session) = self.get_session() { sign_out( - &self.cloud_service, + &self.cloud_services, &session, &self.authenticate_user, self.db_connection(session.user_id)?, @@ -466,16 +486,6 @@ impl UserManager { Ok(()) } - #[tracing::instrument(level = "info", skip(self))] - pub async fn delete_account(&self) -> Result<(), FlowyError> { - self - .cloud_service - .get_user_service()? - .delete_account() - .await?; - Ok(()) - } - /// Updates the user's profile with the given parameters. /// /// This function modifies the user's profile based on the provided update parameters. After updating, it @@ -491,16 +501,14 @@ impl UserManager { let session = self.get_session()?; upsert_user_profile_change( session.user_id, - &session.user_workspace.id, self.db_connection(session.user_id)?, changeset, )?; - self - .cloud_service - .get_user_service()? - .update_user(params) - .await?; + let profile = self.get_user_profile_from_disk(session.user_id).await?; + self + .update_user(session.user_id, profile.token, params) + .await?; Ok(()) } @@ -521,27 +529,18 @@ impl UserManager { self .authenticate_user .database - .backup(session.user_id, &session.user_workspace.id); + .backup_or_restore(session.user_id, &session.user_workspace.id); } /// Fetches the user profile for the given user ID. - pub async fn get_user_profile_from_disk( - &self, - uid: i64, - workspace_id: &str, - ) -> Result<UserProfile, FlowyError> { - let mut conn = self.db_connection(uid)?; - select_user_profile(uid, workspace_id, &mut conn) + pub async fn get_user_profile_from_disk(&self, uid: i64) -> Result<UserProfile, FlowyError> { + select_user_profile(uid, self.db_connection(uid)?) } #[tracing::instrument(level = "info", skip_all, err)] - pub async fn refresh_user_profile( - &self, - old_user_profile: &UserProfile, - workspace_id: &str, - ) -> FlowyResult<()> { + pub async fn refresh_user_profile(&self, old_user_profile: &UserProfile) -> FlowyResult<()> { // If the user is a local user, no need to refresh the user profile - if old_user_profile.workspace_auth_type.is_local() { + if old_user_profile.authenticator.is_local() { return Ok(()); } @@ -554,20 +553,20 @@ impl UserManager { let uid = old_user_profile.uid; let result: Result<UserProfile, FlowyError> = self - .cloud_service + .cloud_services .get_user_service()? - .get_user_profile(uid, workspace_id) + .get_user_profile(UserCredentials::from_uid(uid)) .await; match result { Ok(new_user_profile) => { // If the user profile is updated, save the new user profile if new_user_profile.updated_at > old_user_profile.updated_at { + validate_encryption_sign(old_user_profile, &new_user_profile.encryption_type.sign()); // Save the new user profile let changeset = UserTableChangeset::from_user_profile(new_user_profile); let _ = upsert_user_profile_change( uid, - workspace_id, self.authenticate_user.database.get_connection(uid)?, changeset, ); @@ -601,16 +600,6 @@ impl UserManager { self.authenticate_user.user_paths.user_data_dir(uid) } - pub fn token_from_auth_type(&self, auth_type: &AuthType) -> FlowyResult<Option<String>> { - match auth_type { - AuthType::Local => Ok(None), - AuthType::AppFlowyCloud => { - let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - Ok(select_user_token(uid, &mut conn).ok()) - }, - } - } pub fn user_setting(&self) -> Result<UserSettingPB, FlowyError> { let session = self.get_session()?; let user_setting = UserSettingPB { @@ -624,94 +613,86 @@ impl UserManager { } pub fn workspace_id(&self) -> Result<String, FlowyError> { - let session = self.get_session()?; - Ok(session.user_workspace.id.clone()) + Ok(self.get_session()?.user_workspace.id) } pub fn token(&self) -> Result<Option<String>, FlowyError> { Ok(None) } + async fn update_user( + &self, + uid: i64, + token: String, + params: UpdateUserProfileParams, + ) -> Result<(), FlowyError> { + let server = self.cloud_services.get_user_service()?; + af_spawn(async move { + let credentials = UserCredentials::new(Some(token), Some(uid), None); + server.update_user(credentials, params).await + }) + .await + .map_err(internal_error)??; + Ok(()) + } + async fn save_user(&self, uid: i64, user: UserTable) -> Result<(), FlowyError> { - let conn = self.db_connection(uid)?; - upsert_user(user, conn)?; + let mut conn = self.db_connection(uid)?; + conn.immediate_transaction(|conn| { + // delete old user if exists + diesel::delete(user_table::dsl::user_table.filter(user_table::dsl::id.eq(&user.id))) + .execute(conn)?; + + let _ = diesel::insert_into(user_table::table) + .values(user) + .execute(conn)?; + Ok::<(), FlowyError>(()) + })?; + Ok(()) } pub async fn receive_realtime_event(&self, json: Value) { - if let Ok(user_service) = self.cloud_service.get_user_service() { + if let Ok(user_service) = self.cloud_services.get_user_service() { user_service.receive_realtime_event(json) } } - #[instrument(level = "info", skip_all)] pub(crate) async fn generate_sign_in_url_with_email( &self, - authenticator: &AuthType, + authenticator: &Authenticator, email: &str, ) -> Result<String, FlowyError> { - self - .cloud_service - .set_server_auth_type(authenticator, None)?; + self.cloud_services.set_user_authenticator(authenticator); - let auth_service = self.cloud_service.get_user_service()?; + let auth_service = self.cloud_services.get_user_service()?; let url = auth_service.generate_sign_in_url_with_email(email).await?; Ok(url) } - #[instrument(level = "info", skip_all)] - pub(crate) async fn sign_in_with_password( - &self, - email: &str, - password: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { - self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; - let response = auth_service.sign_in_with_password(email, password).await?; - Ok(response) - } - - #[instrument(level = "info", skip_all)] pub(crate) async fn sign_in_with_magic_link( &self, email: &str, redirect_to: &str, ) -> Result<(), FlowyError> { self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; auth_service .sign_in_with_magic_link(email, redirect_to) .await?; Ok(()) } - #[instrument(level = "info", skip_all)] - pub(crate) async fn sign_in_with_passcode( - &self, - email: &str, - passcode: &str, - ) -> Result<GotrueTokenResponse, FlowyError> { - self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; - let response = auth_service.sign_in_with_passcode(email, passcode).await?; - Ok(response) - } - - #[instrument(level = "info", skip_all)] pub(crate) async fn generate_oauth_url( &self, oauth_provider: &str, ) -> Result<String, FlowyError> { self - .cloud_service - .set_server_auth_type(&AuthType::AppFlowyCloud, None)?; - let auth_service = self.cloud_service.get_user_service()?; + .cloud_services + .set_user_authenticator(&Authenticator::AppFlowyCloud); + let auth_service = self.cloud_services.get_user_service()?; let url = auth_service .generate_oauth_url_with_provider(oauth_provider) .await?; @@ -722,29 +703,25 @@ impl UserManager { async fn save_auth_data( &self, response: &impl UserAuthResponse, - auth_type: AuthType, + authenticator: &Authenticator, session: &Session, ) -> Result<(), FlowyError> { - let user_profile = UserProfile::from((response, &auth_type)); + let user_profile = UserProfile::from((response, authenticator)); let uid = user_profile.uid; - - if auth_type.is_local() { + if authenticator.is_local() { event!(tracing::Level::DEBUG, "Save new anon user: {:?}", uid); - self.set_anon_user(session); + self.set_anon_user(session.clone()); } - let mut conn = self.db_connection(uid)?; - sync_user_workspaces_with_diff(uid, auth_type, response.user_workspaces(), &mut conn)?; + save_all_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; info!( "Save new user profile to disk, authenticator: {:?}", - auth_type + authenticator ); + self.authenticate_user.set_session(Some(session.clone()))?; self - .authenticate_user - .set_session(Some(session.clone().into()))?; - self - .save_user(uid, (user_profile, auth_type).into()) + .save_user(uid, (user_profile, authenticator.clone()).into()) .await?; Ok(()) } @@ -753,10 +730,14 @@ impl UserManager { let session = self.get_session()?; if session.user_id == user_update.uid { debug!("Receive user update: {:?}", user_update); + let user_profile = self.get_user_profile_from_disk(user_update.uid).await?; + if !validate_encryption_sign(&user_profile, &user_update.encryption_sign) { + return Ok(()); + } + // Save the user profile change upsert_user_profile_change( user_update.uid, - &session.user_workspace.id, self.db_connection(user_update.uid)?, UserTableChangeset::from(user_update), )?; @@ -768,38 +749,63 @@ impl UserManager { async fn migrate_anon_user_data_to_cloud( &self, old_user: &AnonUser, - _new_user_session: &Session, - auth_type: &AuthType, + new_user_session: &Session, + authenticator: &Authenticator, ) -> Result<(), FlowyError> { let old_collab_db = self .authenticate_user .database .get_collab_db(old_user.session.user_id)?; + let new_collab_db = self + .authenticate_user + .database + .get_collab_db(new_user_session.user_id)?; - if auth_type == &AuthType::AppFlowyCloud { - self - .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) - .await?; + match authenticator { + Authenticator::Supabase => { + migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user_session, &new_collab_db)?; + if let Err(err) = sync_supabase_user_data_to_cloud( + self.cloud_services.get_user_service()?, + &self.authenticate_user.user_config.device_id, + new_user_session, + &new_collab_db, + ) + .await + { + error!("Sync user data to cloud failed: {:?}", err); + } + }, + Authenticator::AppFlowyCloud => { + self + .migration_anon_user_on_appflowy_cloud_sign_up(old_user, &old_collab_db) + .await?; + }, + _ => {}, } // Save the old user workspace setting. - let mut conn = self - .authenticate_user - .database - .get_connection(old_user.session.user_id)?; - upsert_user_workspace( + save_user_workspace( old_user.session.user_id, - *auth_type, - old_user.session.user_workspace.clone(), - &mut conn, + self + .authenticate_user + .database + .get_connection(old_user.session.user_id)?, + &old_user.session.user_workspace.clone(), )?; Ok(()) } } -pub fn upsert_user_profile_change( +fn current_authenticator() -> Authenticator { + match AuthenticatorType::from_env() { + AuthenticatorType::Local => Authenticator::Local, + AuthenticatorType::Supabase => Authenticator::Supabase, + AuthenticatorType::AppFlowyCloud => Authenticator::AppFlowyCloud, + } +} + +fn upsert_user_profile_change( uid: i64, - workspace_id: &str, mut conn: DBConnection, changeset: UserTableChangeset, ) -> FlowyResult<()> { @@ -808,8 +814,11 @@ pub fn upsert_user_profile_change( "Update user profile with changeset: {:?}", changeset ); - update_user_profile(&mut conn, changeset)?; - let user = select_user_profile(uid, workspace_id, &mut conn)?; + diesel_update_table!(user_table, changeset, &mut *conn); + let user: UserProfile = user_table::dsl::user_table + .filter(user_table::id.eq(&uid.to_string())) + .first::<UserTable>(&mut *conn)? + .into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserProfile) .payload(UserProfilePB::from(user)) .send(); @@ -817,15 +826,10 @@ pub fn upsert_user_profile_change( } #[instrument(level = "info", skip_all, err)] -fn save_user_token( - uid: i64, - workspace_id: &str, - conn: DBConnection, - token: String, -) -> FlowyResult<()> { +fn save_user_token(uid: i64, conn: DBConnection, token: String) -> FlowyResult<()> { let params = UpdateUserProfileParams::new(uid).with_token(token); let changeset = UserTableChangeset::new(params); - upsert_user_profile_change(uid, workspace_id, conn, changeset) + upsert_user_profile_change(uid, conn, changeset) } #[instrument(level = "info", skip_all, err)] @@ -843,8 +847,6 @@ fn collab_migration_list() -> Vec<Box<dyn UserDataMigration>> { Box::new(HistoricalEmptyDocumentMigration), Box::new(FavoriteV1AndWorkspaceArrayMigration), Box::new(WorkspaceTrashMapToSectionMigration), - Box::new(CollabDocKeyWithWorkspaceIdMigration), - Box::new(AnonUserWorkspaceTableMigration), ] } @@ -857,33 +859,29 @@ fn mark_all_migrations_as_applied(sqlite_pool: &Arc<ConnectionPool>) { } } -pub(crate) fn run_data_migration( +pub(crate) fn run_collab_data_migration( session: &Session, - user_auth_type: &AuthType, + user: &UserProfile, collab_db: Arc<CollabKVDB>, sqlite_pool: Arc<ConnectionPool>, - kv: Arc<KVStorePreferences>, - app_version: &Version, + version: Option<Version>, ) { + trace!("Run collab data migration: {:?}", version); let migrations = collab_migration_list(); - match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool, kv).run( + match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool).run( migrations, - user_auth_type, - app_version, + &user.authenticator, + version, ) { Ok(applied_migrations) => { if !applied_migrations.is_empty() { - info!( - "[Migration]: did apply migrations: {:?}", - applied_migrations - ); + info!("Did apply migrations: {:?}", applied_migrations); } }, - Err(e) => error!("[AppflowyData]:User data migration failed: {:?}", e), + Err(e) => error!("User data migration failed: {:?}", e), } } -#[instrument(level = "info", skip_all, err)] pub async fn sign_out( cloud_services: &Arc<dyn UserCloudServiceProvider>, session: &Session, diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs index 188cc3c5ac..251a77bd98 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_history_user.rs @@ -1,18 +1,20 @@ -use std::sync::Arc; use tracing::instrument; use crate::entities::UserProfilePB; use crate::user_manager::UserManager; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::AuthType; +use flowy_user_pub::entities::Authenticator; use crate::migrations::AnonUser; use flowy_user_pub::session::Session; -pub const ANON_USER: &str = "anon_user"; +const ANON_USER: &str = "anon_user"; impl UserManager { #[instrument(skip_all)] - pub async fn get_migration_user(&self, current_authenticator: &AuthType) -> Option<AnonUser> { + pub async fn get_migration_user( + &self, + current_authenticator: &Authenticator, + ) -> Option<AnonUser> { // No need to migrate if the user is already local if current_authenticator.is_local() { return None; @@ -20,18 +22,18 @@ impl UserManager { let session = self.get_session().ok()?; let user_profile = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) + .get_user_profile_from_disk(session.user_id) .await .ok()?; - if user_profile.auth_type.is_local() { + if user_profile.authenticator.is_local() { Some(AnonUser { session }) } else { None } } - pub fn set_anon_user(&self, session: &Session) { + pub fn set_anon_user(&self, session: Session) { let _ = self.store_preferences.set_object(ANON_USER, session); } @@ -48,23 +50,11 @@ impl UserManager { "Anon user not found", ))?; let profile = self - .get_user_profile_from_disk(anon_session.user_id, &anon_session.user_workspace.id) + .get_user_profile_from_disk(anon_session.user_id) .await?; Ok(UserProfilePB::from(profile)) } - pub fn get_anon_user_id(&self) -> FlowyResult<i64> { - let anon_session = self - .store_preferences - .get_object::<Session>(ANON_USER) - .ok_or(FlowyError::new( - ErrorCode::RecordNotFound, - "Anon user not found", - ))?; - - Ok(anon_session.user_id) - } - /// Opens a historical user's session based on their user ID, device ID, and authentication type. /// /// This function facilitates the re-opening of a user's session from historical tracking. @@ -73,7 +63,7 @@ impl UserManager { pub async fn open_anon_user(&self) -> FlowyResult<()> { let anon_session = self .store_preferences - .get_object::<Arc<Session>>(ANON_USER) + .get_object::<Session>(ANON_USER) .ok_or(FlowyError::new( ErrorCode::RecordNotFound, "Anon user not found", diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index 47826054bf..826d665b39 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -1,20 +1,17 @@ +use std::sync::atomic::Ordering; use std::sync::{Arc, Weak}; use anyhow::Context; -use collab::core::collab::DataSource; -use collab::lock::RwLock; +use collab::core::collab::{DataSource, MutexCollab}; use collab_entity::reminder::Reminder; use collab_entity::CollabType; -use collab_integrate::collab_builder::{ - AppFlowyCollabBuilder, CollabBuilderConfig, CollabPersistenceImpl, -}; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; +use collab_user::core::{MutexUserAwareness, UserAwareness}; +use tracing::{debug, error, info, instrument, trace}; + use collab_integrate::CollabKVDB; -use collab_user::core::{UserAwareness, UserAwarenessNotifier}; -use dashmap::try_result::TryResult; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::{user_awareness_object_id, AuthType}; -use tracing::{error, info, instrument, trace}; -use uuid::Uuid; +use flowy_user_pub::entities::user_awareness_object_id; use crate::entities::ReminderPB; use crate::user_manager::UserManager; @@ -37,10 +34,10 @@ impl UserManager { pub async fn add_reminder(&self, reminder_pb: ReminderPB) -> FlowyResult<()> { let reminder = Reminder::from(reminder_pb); self - .mut_awareness(|user_awareness| { + .with_awareness((), |user_awareness| { user_awareness.add_reminder(reminder.clone()); }) - .await?; + .await; self .collab_interact .read() @@ -54,10 +51,10 @@ impl UserManager { /// pub async fn remove_reminder(&self, reminder_id: &str) -> FlowyResult<()> { self - .mut_awareness(|user_awareness| { + .with_awareness((), |user_awareness| { user_awareness.remove_reminder(reminder_id); }) - .await?; + .await; self .collab_interact .read() @@ -72,20 +69,12 @@ impl UserManager { pub async fn update_reminder(&self, reminder_pb: ReminderPB) -> FlowyResult<()> { let reminder = Reminder::from(reminder_pb); self - .mut_awareness(|user_awareness| { - user_awareness.update_reminder(&reminder.id, |update| { - update - .set_object_id(&reminder.object_id) - .set_title(&reminder.title) - .set_message(&reminder.message) - .set_is_ack(reminder.is_ack) - .set_is_read(reminder.is_read) - .set_scheduled_at(reminder.scheduled_at) - .set_type(reminder.ty) - .set_meta(reminder.meta.clone().into_inner()); + .with_awareness((), |user_awareness| { + user_awareness.update_reminder(&reminder.id, |new_reminder| { + new_reminder.clone_from(&reminder) }); }) - .await?; + .await; self .collab_interact .read() @@ -106,210 +95,117 @@ impl UserManager { /// - Returns a vector of `Reminder` objects containing all reminders for the user. /// pub async fn get_all_reminders(&self) -> Vec<Reminder> { - let reminders = self - .mut_awareness(|user_awareness| user_awareness.get_all_reminders()) - .await; - reminders.unwrap_or_default() + self + .with_awareness(vec![], |user_awareness| user_awareness.get_all_reminders()) + .await } - /// Init UserAwareness for user - /// 1. check if user awareness exists on disk. If yes init awareness from disk - /// 2. If not, init awareness from server. + pub async fn initialize_user_awareness(&self, session: &Session) { + match self.try_initial_user_awareness(session).await { + Ok(_) => {}, + Err(e) => error!("Failed to initialize user awareness: {:?}", e), + } + } + + /// Initializes the user's awareness based on the specified data source. + /// + /// This asynchronous function attempts to initialize the user's awareness from either a local or remote data source. + /// Depending on the chosen source, it will either construct the user awareness from an empty dataset or fetch it + /// from a remote service. Once obtained, the user's awareness is stored in a shared mutex-protected structure. + /// + /// # Parameters + /// - `session`: The current user's session data. + /// - `source`: The source from which the user's awareness data should be obtained, either local or remote. + /// + /// # Returns + /// - Returns `Ok(())` if the user's awareness is successfully initialized. + /// - May return errors of type `FlowyError` if any issues arise during the initialization. #[instrument(level = "info", skip(self, session), err)] - pub(crate) async fn initial_user_awareness( - &self, - session: &Session, - auth_type: &AuthType, - ) -> FlowyResult<()> { - let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); + pub(crate) async fn try_initial_user_awareness(&self, session: &Session) -> FlowyResult<()> { + if self.is_loading_awareness.load(Ordering::SeqCst) { + return Ok(()); + } + self.is_loading_awareness.store(true, Ordering::SeqCst); - // Try to acquire mutable access to `is_loading_awareness`. - // Thread-safety is ensured by DashMap - let should_init = match self.is_loading_awareness.try_get_mut(&object_id) { - TryResult::Present(mut is_loading) => { - if *is_loading { - false - } else { - *is_loading = true; - true - } - }, - TryResult::Absent => true, - TryResult::Locked => { - return Err(FlowyError::new( - ErrorCode::Internal, - format!( - "Failed to lock is_loading_awareness for object: {}", - object_id - ), - )); - }, - }; - - if should_init { - if let Some(old_user_awareness) = self.user_awareness.swap(None) { - info!("Closing previous user awareness"); - old_user_awareness.read().await.close(); // Ensure that old awareness is closed - } - - let is_exist_on_disk = self - .authenticate_user - .is_collab_on_disk(session.user_id, &object_id.to_string())?; - if auth_type.is_local() || is_exist_on_disk { - trace!( - "Initializing new user awareness from disk:{}, {:?}", - object_id, - auth_type - ); - let collab_db = self.get_collab_db(session.user_id)?; - let workspace_id = session.user_workspace.workspace_id()?; - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); - let awareness = Self::collab_for_user_awareness( - &self.collab_builder.clone(), - &workspace_id, - session.user_id, - &object_id, - collab_db, - doc_state, - None, - ) - .await?; - info!("User awareness initialized successfully"); - self.user_awareness.store(Some(awareness)); - if let Some(mut is_loading) = self.is_loading_awareness.get_mut(&object_id) { - *is_loading = false; - } - } else { - info!( - "Initializing new user awareness from server:{}, {:?}", - object_id, auth_type - ); - self.load_awareness_from_server(session, object_id, *auth_type)?; - } - } else { - return Err(FlowyError::new( - ErrorCode::Internal, - format!( - "User awareness is already being loaded for object: {}", - object_id - ), - )); + if let Some(old_user_awareness) = self.user_awareness.lock().await.take() { + debug!("Closing old user awareness"); + old_user_awareness.lock().close(); + drop(old_user_awareness); } - Ok(()) - } - - /// Initialize UserAwareness from server. - /// It will spawn a task in the background in order to no block the caller. This functions is - /// designed to be thread safe. - fn load_awareness_from_server( - &self, - session: &Session, - object_id: Uuid, - authenticator: AuthType, - ) -> FlowyResult<()> { - // Clone necessary data - let session = session.clone(); + let object_id = + user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); + trace!("Initializing user awareness {}", object_id); let collab_db = self.get_collab_db(session.user_id)?; + let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_user_awareness = Arc::downgrade(&self.user_awareness); let weak_builder = self.collab_builder.clone(); - let user_awareness = Arc::downgrade(&self.user_awareness); - let cloud_services = self.cloud_service.clone(); - let authenticate_user = self.authenticate_user.clone(); - let is_loading_awareness = self.is_loading_awareness.clone(); - - // Spawn an async task to fetch or create user awareness + let cloned_is_loading = self.is_loading_awareness.clone(); + let session = session.clone(); + let workspace_id = session.user_workspace.id.clone(); tokio::spawn(async move { - let set_is_loading_false = || { - if let Some(mut is_loading) = is_loading_awareness.get_mut(&object_id) { - *is_loading = false; - } - }; + if cloned_is_loading.load(Ordering::SeqCst) { + return Ok(()); + } - let workspace_id = session.user_workspace.workspace_id()?; - let create_awareness = if authenticator.is_local() { - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); - Self::collab_for_user_awareness( - &weak_builder, - &workspace_id, - session.user_id, - &object_id, - collab_db, - doc_state, - None, - ) - .await - } else { + if let (Some(cloud_services), Some(user_awareness)) = + (weak_cloud_services.upgrade(), weak_user_awareness.upgrade()) + { let result = cloud_services .get_user_service()? - .get_user_awareness_doc_state(session.user_id, &workspace_id, &object_id) + .get_user_awareness_doc_state(session.user_id, &session.user_workspace.id, &object_id) .await; - match result { + let mut lock_awareness = user_awareness + .try_lock() + .map_err(|err| FlowyError::internal().with_context(err))?; + if lock_awareness.is_some() { + return Ok(()); + } + + let awareness = match result { Ok(data) => { - trace!("Fetched user awareness collab from remote: {}", data.len()); - Self::collab_for_user_awareness( - &weak_builder, + trace!("Get user awareness collab from remote: {}", data.len()); + let collab = Self::collab_for_user_awareness( &workspace_id, + &weak_builder, session.user_id, &object_id, collab_db, DataSource::DocStateV1(data), - None, ) - .await + .await?; + MutexUserAwareness::new(UserAwareness::create(collab, None)) }, Err(err) => { if err.is_record_not_found() { info!("User awareness not found, creating new"); - let doc_state = - CollabPersistenceImpl::new(collab_db.clone(), session.user_id, workspace_id) - .into_data_source(); - Self::collab_for_user_awareness( - &weak_builder, + let collab = Self::collab_for_user_awareness( &workspace_id, + &weak_builder, session.user_id, &object_id, collab_db, - doc_state, - None, + DataSource::Disk, ) - .await + .await?; + MutexUserAwareness::new(UserAwareness::create(collab, None)) } else { - Err(err) + error!("Failed to fetch user awareness: {:?}", err); + return Err(err); } }, - } - }; + }; - match create_awareness { - Ok(new_user_awareness) => { - // Validate session before storing the awareness - if let Ok(current_session) = authenticate_user.get_session() { - if current_session.user_workspace.id == session.user_workspace.id { - if let Some(user_awareness) = user_awareness.upgrade() { - info!("User awareness initialized successfully"); - user_awareness.store(Some(new_user_awareness)); - } else { - error!("Failed to upgrade user awareness"); - } - } else { - info!("User awareness is outdated, ignoring"); - } - } - set_is_loading_false(); - Ok(()) - }, - Err(err) => { - error!("Error while creating user awareness: {:?}", err); - set_is_loading_false(); - Err(err) - }, + trace!("User awareness initialized"); + lock_awareness.replace(awareness); } + Ok(()) }); + + // mark the user awareness as not loading + self.is_loading_awareness.store(false, Ordering::SeqCst); + Ok(()) } @@ -319,27 +215,26 @@ impl UserManager { /// using a collaboration builder. This instance is specifically geared towards handling /// user awareness. async fn collab_for_user_awareness( + workspace_id: &str, collab_builder: &Weak<AppFlowyCollabBuilder>, - workspace_id: &Uuid, uid: i64, - object_id: &Uuid, + object_id: &str, collab_db: Weak<CollabKVDB>, doc_state: DataSource, - notifier: Option<UserAwarenessNotifier>, - ) -> Result<Arc<RwLock<UserAwareness>>, FlowyError> { + ) -> Result<Arc<MutexCollab>, FlowyError> { let collab_builder = collab_builder.upgrade().ok_or(FlowyError::new( ErrorCode::Internal, "Unexpected error: collab builder is not available", ))?; - let collab_object = - collab_builder.collab_object(workspace_id, uid, object_id, CollabType::UserAwareness)?; let collab = collab_builder - .create_user_awareness( - collab_object, + .build( + workspace_id, + uid, + object_id, + CollabType::UserAwareness, doc_state, collab_db, CollabBuilderConfig::default().sync_enable(true), - notifier, ) .await .context("Build collab for user awareness failed")?; @@ -357,40 +252,19 @@ impl UserManager { /// # Parameters /// - `default_value`: A default value to return if the user awareness is `None` and cannot be initialized. /// - `f`: The asynchronous closure to execute with the user awareness. - async fn mut_awareness<F, Output>(&self, f: F) -> FlowyResult<Output> + async fn with_awareness<F, Output>(&self, default_value: Output, f: F) -> Output where - F: FnOnce(&mut UserAwareness) -> Output, + F: FnOnce(&UserAwareness) -> Output, { - match self.user_awareness.load_full() { + let user_awareness = self.user_awareness.lock().await; + match &*user_awareness { None => { - info!("User awareness is not loaded when trying to access it"); - - let session = self.get_session()?; - let object_id = user_awareness_object_id(&session.user_uuid, &session.user_workspace.id); - let is_loading = self - .is_loading_awareness - .get(&object_id) - .map(|r| *r.value()) - .unwrap_or(false); - - if !is_loading { - let user_profile = self - .get_user_profile_from_disk(session.user_id, &session.user_workspace.id) - .await?; - self - .initial_user_awareness(&session, &user_profile.workspace_auth_type) - .await?; + if let Ok(session) = self.get_session() { + self.initialize_user_awareness(&session).await; } - - Err(FlowyError::new( - ErrorCode::InProgress, - "User awareness is loading", - )) - }, - Some(lock) => { - let mut user_awareness = lock.write().await; - Ok(f(&mut user_awareness)) + default_value }, + Some(user_awareness) => f(&user_awareness.lock()), } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs index 1462d1f019..4288260899 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_encryption.rs @@ -1,9 +1,32 @@ -use crate::services::cloud_config::get_encrypt_secret; +use crate::entities::{AuthStateChangedPB, AuthStatePB}; use crate::user_manager::UserManager; +use flowy_encrypt::{decrypt_text, encrypt_text}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use lib_infra::encryption::{decrypt_text, encrypt_text}; +use flowy_user_pub::entities::{ + EncryptionType, UpdateUserProfileParams, UserCredentials, UserProfile, +}; + +use crate::notification::send_auth_state_notification; +use crate::services::cloud_config::get_encrypt_secret; impl UserManager { + pub async fn set_encrypt_secret( + &self, + uid: i64, + secret: String, + encryption_type: EncryptionType, + ) -> FlowyResult<()> { + let params = UpdateUserProfileParams::new(uid).with_encryption_type(encryption_type); + self + .cloud_services + .get_user_service()? + .update_user(UserCredentials::from_uid(uid), params.clone()) + .await?; + self.cloud_services.set_encrypt_secret(secret); + + Ok(()) + } + pub fn generate_encryption_sign(&self, uid: i64, encrypt_secret: &str) -> FlowyResult<String> { let encrypt_sign = encrypt_text(uid.to_string(), encrypt_secret)?; Ok(encrypt_sign) @@ -41,3 +64,16 @@ impl UserManager { } } } + +pub(crate) fn validate_encryption_sign(user_profile: &UserProfile, encryption_sign: &str) -> bool { + // If the local user profile's encryption sign is not equal to the user update's encryption sign, + // which means the user enable encryption in another device, we should logout the current user. + let is_valid = user_profile.encryption_type.sign() == encryption_sign; + if !is_valid { + send_auth_state_notification(AuthStateChangedPB { + state: AuthStatePB::InvalidAuth, + message: "Encryption configuration was changed".to_string(), + }); + } + is_valid +} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index b78d635133..cee00fecd4 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -1,34 +1,37 @@ use chrono::{Duration, NaiveDateTime, Utc}; -use client_api::entity::billing_dto::{RecurringInterval, SubscriptionPlanDetail}; -use client_api::entity::billing_dto::{SubscriptionPlan, WorkspaceUsageAndLimit}; - -use std::str::FromStr; +use std::convert::TryFrom; use std::sync::Arc; +use collab_entity::{CollabObject, CollabType}; +use collab_integrate::CollabKVDB; +use tracing::{error, info, instrument, trace, warn}; + +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::entities::{AppFlowyData, ImportData}; +use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; +use flowy_user_pub::entities::{ + Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, + WorkspaceSubscription, WorkspaceUsage, +}; +use lib_dispatch::prelude::af_spawn; + use crate::entities::{ - RepeatedUserWorkspacePB, SubscribeWorkspacePB, SuccessWorkspaceSubscriptionPB, - UpdateUserWorkspaceSettingPB, UserWorkspacePB, WorkspaceSettingsPB, WorkspaceSubscriptionInfoPB, + RepeatedUserWorkspacePB, ResetWorkspacePB, SubscribeWorkspacePB, UserWorkspacePB, }; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; -use crate::services::billing_check::PeriodicallyCheckBillingState; use crate::services::data_import::{ generate_import_data, upload_collab_objects_data, ImportedFolder, ImportedSource, }; - -use crate::user_manager::UserManager; -use collab_integrate::CollabKVDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::entities::{ImportFrom, ImportedCollabData, ImportedFolderData}; -use flowy_sqlite::ConnectionPool; -use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; -use flowy_user_pub::entities::{ - AuthType, Role, UserWorkspace, WorkspaceInvitation, WorkspaceInvitationStatus, WorkspaceMember, +use crate::services::sqlite_sql::member_sql::{ + select_workspace_member, upsert_workspace_member, WorkspaceMemberTable, }; +use crate::services::sqlite_sql::workspace_sql::{ + get_all_user_workspace_op, get_user_workspace_op, insert_new_workspaces_op, UserWorkspaceTable, +}; +use crate::user_manager::UserManager; use flowy_user_pub::session::Session; -use flowy_user_pub::sql::*; -use tracing::{error, info, instrument, trace}; -use uuid::Uuid; impl UserManager { /// Import appflowy data from the given path. @@ -44,92 +47,99 @@ impl UserManager { let cloned_current_session = current_session.clone(); let import_data = tokio::task::spawn_blocking(move || { - generate_import_data(&cloned_current_session, &user_collab_db, imported_folder) - .map_err(|err| FlowyError::new(ErrorCode::AppFlowyDataFolderImportError, err.to_string())) + generate_import_data( + &cloned_current_session, + &cloned_current_session.user_workspace.id, + &user_collab_db, + imported_folder, + ) + .map_err(|err| FlowyError::new(ErrorCode::AppFlowyDataFolderImportError, err.to_string())) }) .await??; - info!( - "[AppflowyData]: upload {} document, {} database, {}, rows", - import_data.collab_data.document_object_ids.len(), - import_data.collab_data.database_object_ids.len(), - import_data.collab_data.row_object_ids.len() - ); - self - .upload_collab_data(¤t_session, import_data.collab_data) - .await?; - - self - .upload_folder_data( - ¤t_session, - &import_data.source, - import_data.parent_view_id, - import_data.folder_data, - ) - .await?; - + match import_data { + ImportData::AppFlowyDataFolder { items } => { + for item in items { + self + .upload_appflowy_data_item(¤t_session, item) + .await?; + } + }, + } Ok(()) } - async fn upload_folder_data( - &self, - _current_session: &Session, - source: &ImportFrom, - parent_view_id: Option<String>, - folder_data: ImportedFolderData, - ) -> Result<(), FlowyError> { - let ImportedFolderData { - views, - orphan_views, - database_view_ids_by_database_id, - } = folder_data; - self - .user_workspace_service - .import_database_views(database_view_ids_by_database_id) - .await?; - self - .user_workspace_service - .import_views(source, views, orphan_views, parent_view_id) - .await?; - - Ok(()) - } - - async fn upload_collab_data( + async fn upload_appflowy_data_item( &self, current_session: &Session, - collab_data: ImportedCollabData, + item: AppFlowyData, ) -> Result<(), FlowyError> { - let user = self - .get_user_profile_from_disk(current_session.user_id, ¤t_session.user_workspace.id) - .await?; - let user_collab_db = self - .get_collab_db(current_session.user_id)? - .upgrade() - .ok_or_else(|| FlowyError::internal().with_context("Collab db not found"))?; + match item { + AppFlowyData::Folder { + views, + database_view_ids_by_database_id, + } => { + // Since `async_trait` does not implement `Sync`, and the handler requires `Sync`, we use a + // channel to synchronize the operation. This approach allows asynchronous trait methods to be compatible + // with synchronous handler requirements." + let (tx, rx) = tokio::sync::oneshot::channel(); + let cloned_workspace_service = self.user_workspace_service.clone(); + af_spawn(async move { + let result = async { + cloned_workspace_service + .did_import_database_views(database_view_ids_by_database_id) + .await?; + cloned_workspace_service.did_import_views(views).await?; + Ok::<(), FlowyError>(()) + } + .await; + let _ = tx.send(result); + }) + .await?; + rx.await??; + }, + AppFlowyData::CollabObject { + row_object_ids, + document_object_ids, + database_object_ids, + } => { + let user = self + .get_user_profile_from_disk(current_session.user_id) + .await?; + let user_collab_db = self + .get_collab_db(current_session.user_id)? + .upgrade() + .ok_or_else(|| FlowyError::internal().with_context("Collab db not found"))?; - let user_id = current_session.user_id; - let weak_user_collab_db = Arc::downgrade(&user_collab_db); - let weak_user_cloud_service = self.cloud_service.get_user_service()?; - match upload_collab_objects_data( - user_id, - weak_user_collab_db, - ¤t_session.user_workspace.workspace_id()?, - &user.workspace_auth_type, - collab_data, - weak_user_cloud_service, - ) - .await - { - Ok(_) => info!( - "Successfully uploaded collab objects data for user:{}", - user_id - ), - Err(err) => { - error!( - "Failed to upload collab objects data: {:?} for user:{}", - err, user_id - ); + let user_id = current_session.user_id; + let weak_user_collab_db = Arc::downgrade(&user_collab_db); + let weak_user_cloud_service = self.cloud_services.get_user_service()?; + match upload_collab_objects_data( + user_id, + weak_user_collab_db, + &user.workspace_id, + &user.authenticator, + AppFlowyData::CollabObject { + row_object_ids, + document_object_ids, + database_object_ids, + }, + weak_user_cloud_service, + ) + .await + { + Ok(_) => info!( + "Successfully uploaded collab objects data for user:{}", + user_id + ), + Err(err) => { + error!( + "Failed to upload collab objects data: {:?} for user:{}", + err, user_id + ); + // TODO(nathan): retry uploading the collab objects data. + }, + } }, } Ok(()) @@ -141,10 +151,9 @@ impl UserManager { old_collab_db: &Arc<CollabKVDB>, ) -> FlowyResult<()> { let import_context = ImportedFolder { - imported_session: old_user.session.as_ref().clone(), + imported_session: old_user.session.clone(), imported_collab_db: old_collab_db.clone(), container_name: None, - parent_view_id: None, source: ImportedSource::AnonUser, }; self.perform_import(import_context).await?; @@ -152,121 +161,94 @@ impl UserManager { } #[instrument(skip(self), err)] - pub async fn open_workspace(&self, workspace_id: &Uuid, auth_type: AuthType) -> FlowyResult<()> { - info!("open workspace: {}, auth type:{}", workspace_id, auth_type); - let workspace_id_str = workspace_id.to_string(); - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; - - let uid = self.user_id()?; - let profile = self - .get_user_profile_from_disk(uid, &workspace_id_str) + pub async fn open_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + info!("open workspace: {}", workspace_id); + let user_workspace = self + .cloud_services + .get_user_service()? + .open_workspace(workspace_id) .await?; - if let Err(err) = self.cloud_service.set_token(&profile.token) { - error!("Set token failed: {}", err); - } - - let mut conn = self.db_connection(self.user_id()?)?; - let user_workspace = match select_user_workspace(&workspace_id_str, &mut conn) { - Err(err) => { - if err.is_record_not_found() { - sync_workspace( - workspace_id, - self.cloud_service.get_user_service()?, - uid, - auth_type, - self.db_pool(uid)?, - ) - .await? - } else { - return Err(err); - } - }, - Ok(row) => { - let user_workspace = UserWorkspace::from(row); - let workspace_id = *workspace_id; - let user_service = self.cloud_service.get_user_service()?; - let pool = self.db_pool(uid)?; - tokio::spawn(async move { - let _ = sync_workspace(&workspace_id, user_service, uid, auth_type, pool).await; - }); - user_workspace - }, - }; self .authenticate_user .set_user_workspace(user_workspace.clone())?; - let uid = self.user_id()?; - if let Err(err) = self - .user_status_callback - .read() - .await - .on_workspace_opened(uid, workspace_id, &user_workspace, &auth_type) - .await - { - error!("Open workspace failed: {:?}", err); - } - - if let Err(err) = self - .initial_user_awareness(self.get_session()?.as_ref(), &auth_type) - .await - { + if let Err(err) = self.try_initial_user_awareness(&self.get_session()?).await { error!( "Failed to initialize user awareness when opening workspace: {:?}", err ); } + let uid = self.user_id()?; + if let Err(err) = self + .user_status_callback + .read() + .await + .open_workspace(uid, &user_workspace) + .await + { + error!("Open workspace failed: {:?}", err); + } + Ok(()) } #[instrument(level = "info", skip(self), err)] - pub async fn create_workspace( - &self, - workspace_name: &str, - auth_type: AuthType, - ) -> FlowyResult<UserWorkspace> { - let token = self.token_from_auth_type(&auth_type)?; - self.cloud_service.set_server_auth_type(&auth_type, token)?; - + pub async fn add_workspace(&self, workspace_name: &str) -> FlowyResult<UserWorkspace> { let new_workspace = self - .cloud_service + .cloud_services .get_user_service()? .create_workspace(workspace_name) .await?; info!( - "create workspace: {}, name:{}, auth_type: {}", - new_workspace.id, new_workspace.name, auth_type + "new workspace: {}, name:{}", + new_workspace.id, new_workspace.name ); // save the workspace to sqlite db let uid = self.user_id()?; let mut conn = self.db_connection(uid)?; - upsert_user_workspace(uid, auth_type, new_workspace.clone(), &mut conn)?; + insert_new_workspaces_op(uid, &[new_workspace.clone()], &mut conn)?; Ok(new_workspace) } pub async fn patch_workspace( &self, - workspace_id: &Uuid, - changeset: UserWorkspaceChangeset, + workspace_id: &str, + new_workspace_name: Option<&str>, + new_workspace_icon: Option<&str>, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? - .patch_workspace(workspace_id, changeset.name.clone(), changeset.icon.clone()) + .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) .await?; // save the icon and name to sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - update_user_workspace(conn, changeset)?; + let mut user_workspace = match self.get_user_workspace(uid, workspace_id) { + Some(user_workspace) => user_workspace, + None => { + return Err(FlowyError::record_not_found().with_context(format!( + "Expected to find user workspace with id: {}, but not found", + workspace_id + ))); + }, + }; - let row = self.get_user_workspace_from_db(uid, workspace_id)?; - let payload = UserWorkspacePB::from(row); + if let Some(new_workspace_name) = new_workspace_name { + user_workspace.name = new_workspace_name.to_string(); + } + if let Some(new_workspace_icon) = new_workspace_icon { + user_workspace.icon = new_workspace_icon.to_string(); + } + + let _ = save_user_workspace(uid, conn, &user_workspace); + + let payload: UserWorkspacePB = user_workspace.clone().into(); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspace) .payload(payload) .send(); @@ -275,10 +257,10 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn leave_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub async fn leave_workspace(&self, workspace_id: &str) -> FlowyResult<()> { info!("leave workspace: {}", workspace_id); self - .cloud_service + .cloud_services .get_user_service()? .leave_workspace(workspace_id) .await?; @@ -286,42 +268,40 @@ impl UserManager { // delete workspace from local sqlite db let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspace(conn, workspace_id.to_string().as_str())?; + delete_user_workspaces(conn, workspace_id)?; self .user_workspace_service - .did_delete_workspace(workspace_id) - .await + .did_delete_workspace(workspace_id.to_string()) } #[instrument(level = "info", skip(self), err)] - pub async fn delete_workspace(&self, workspace_id: &Uuid) -> FlowyResult<()> { + pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { info!("delete workspace: {}", workspace_id); self - .cloud_service + .cloud_services .get_user_service()? .delete_workspace(workspace_id) .await?; let uid = self.user_id()?; let conn = self.db_connection(uid)?; - delete_user_workspace(conn, workspace_id.to_string().as_str())?; + delete_user_workspaces(conn, workspace_id)?; self .user_workspace_service - .did_delete_workspace(workspace_id) - .await?; + .did_delete_workspace(workspace_id.to_string())?; Ok(()) } pub async fn invite_member_to_workspace( &self, - workspace_id: Uuid, + workspace_id: String, invitee_email: String, role: Role, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .invite_workspace_member(invitee_email, workspace_id, role) .await?; @@ -331,7 +311,7 @@ impl UserManager { pub async fn list_pending_workspace_invitations(&self) -> FlowyResult<Vec<WorkspaceInvitation>> { let status = Some(WorkspaceInvitationStatus::Pending); let invitations = self - .cloud_service + .cloud_services .get_user_service()? .list_workspace_invitations(status) .await?; @@ -340,20 +320,34 @@ impl UserManager { pub async fn accept_workspace_invitation(&self, invite_id: String) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .accept_workspace_invitations(invite_id) .await?; Ok(()) } + // deprecated, use invite instead + pub async fn add_workspace_member( + &self, + user_email: String, + workspace_id: String, + ) -> FlowyResult<()> { + self + .cloud_services + .get_user_service()? + .add_workspace_member(user_email, workspace_id) + .await?; + Ok(()) + } + pub async fn remove_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .remove_workspace_member(user_email, workspace_id) .await?; @@ -362,10 +356,10 @@ impl UserManager { pub async fn get_workspace_members( &self, - workspace_id: Uuid, + workspace_id: String, ) -> FlowyResult<Vec<WorkspaceMember>> { let members = self - .cloud_service + .cloud_services .get_user_service()? .get_workspace_members(workspace_id) .await?; @@ -374,13 +368,13 @@ impl UserManager { pub async fn get_workspace_member( &self, - workspace_id: Uuid, + workspace_id: String, uid: i64, ) -> FlowyResult<WorkspaceMember> { let member = self - .cloud_service + .cloud_services .get_user_service()? - .get_workspace_member(&workspace_id, uid) + .get_workspace_member(workspace_id, uid) .await?; Ok(member) } @@ -388,84 +382,60 @@ impl UserManager { pub async fn update_workspace_member( &self, user_email: String, - workspace_id: Uuid, + workspace_id: String, role: Role, ) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? .update_workspace_member(user_email, workspace_id, role) .await?; Ok(()) } - pub fn get_user_workspace_from_db( - &self, - uid: i64, - workspace_id: &Uuid, - ) -> FlowyResult<UserWorkspaceTable> { - let mut conn = self.db_connection(uid)?; - select_user_workspace(workspace_id.to_string().as_str(), &mut conn) + pub fn get_user_workspace(&self, uid: i64, workspace_id: &str) -> Option<UserWorkspace> { + let conn = self.db_connection(uid).ok()?; + get_user_workspace_op(workspace_id, conn) } - pub async fn get_all_user_workspaces( - &self, - uid: i64, - auth_type: AuthType, - ) -> FlowyResult<Vec<UserWorkspace>> { - // 1) Load & return the local copy immediately - let mut conn = self.db_connection(uid)?; - let local_workspaces = select_all_user_workspace(uid, &mut conn)?; + pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult<Vec<UserWorkspace>> { + let conn = self.db_connection(uid)?; + let workspaces = get_all_user_workspace_op(uid, conn)?; - // 2) If both cloud service and pool are available, fire off a background sync - if let (Ok(service), Ok(pool)) = (self.cloud_service.get_user_service(), self.db_pool(uid)) { - // capture only what we need - let auth_copy = auth_type; - - tokio::spawn(async move { - // fetch remote list - let new_ws = match service.get_all_workspace(uid).await { - Ok(ws) => ws, - Err(e) => { - trace!("failed to fetch remote workspaces for {}: {:?}", uid, e); - return; - }, - }; - - // get a pooled DB connection - let mut conn = match pool.get() { - Ok(c) => c, - Err(e) => { - trace!("failed to get DB connection for {}: {:?}", uid, e); - return; - }, - }; - - // sync + diff - match sync_user_workspaces_with_diff(uid, auth_copy, &new_ws, &mut conn) { - Ok(changes) if !changes.is_empty() => { - info!( - "synced {} workspaces for user {} and auth type {:?}. changes: {:?}", - changes.len(), - uid, - auth_copy, - changes - ); - // only send notification if there were real changes - if let Ok(updated_list) = select_all_user_workspace(uid, &mut conn) { - let repeated_pb = RepeatedUserWorkspacePB::from(updated_list); + if let Ok(service) = self.cloud_services.get_user_service() { + if let Ok(pool) = self.db_pool(uid) { + af_spawn(async move { + if let Ok(new_user_workspaces) = service.get_all_workspace(uid).await { + if let Ok(conn) = pool.get() { + let _ = save_all_user_workspaces(uid, conn, &new_user_workspaces); + let repeated_workspace_pbs = RepeatedUserWorkspacePB::from(new_user_workspaces); send_notification(&uid.to_string(), UserNotification::DidUpdateUserWorkspaces) - .payload(repeated_pb) + .payload(repeated_workspace_pbs) .send(); } - }, - Ok(_) => trace!("no workspaces updated for {}", uid), - Err(e) => trace!("sync error for {}: {:?}", uid, e), - } - }); + } + }); + } } + Ok(workspaces) + } - Ok(local_workspaces) + /// Reset the remote workspace using local workspace data. This is useful when a user wishes to + /// open a workspace on a new device that hasn't fully synchronized with the server. + pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> { + let collab_object = CollabObject::new( + reset.uid, + reset.workspace_id.clone(), + CollabType::Folder, + reset.workspace_id.clone(), + self.authenticate_user.user_config.device_id.clone(), + ); + self + .cloud_services + .get_user_service()? + .reset_workspace(collab_object) + .await?; + Ok(()) } #[instrument(level = "info", skip(self), err)] @@ -473,12 +443,11 @@ impl UserManager { &self, workspace_subscription: SubscribeWorkspacePB, ) -> FlowyResult<String> { - let workspace_id = Uuid::from_str(&workspace_subscription.workspace_id)?; let payment_link = self - .cloud_service + .cloud_services .get_user_service()? .subscribe_workspace( - workspace_id, + workspace_subscription.workspace_id, workspace_subscription.recurring_interval.into(), workspace_subscription.workspace_subscription_plan.into(), workspace_subscription.success_url, @@ -489,180 +458,54 @@ impl UserManager { } #[instrument(level = "info", skip(self), err)] - pub async fn get_workspace_subscription_info( - &self, - workspace_id: String, - ) -> FlowyResult<WorkspaceSubscriptionInfoPB> { - let workspace_id = Uuid::from_str(&workspace_id)?; - let subscriptions = self - .cloud_service + pub async fn get_workspace_subscriptions(&self) -> FlowyResult<Vec<WorkspaceSubscription>> { + let res = self + .cloud_services .get_user_service()? - .get_workspace_subscription_one(&workspace_id) + .get_workspace_subscriptions() .await?; - - Ok(WorkspaceSubscriptionInfoPB::from(subscriptions)) + Ok(res) } #[instrument(level = "info", skip(self), err)] - pub async fn cancel_workspace_subscription( - &self, - workspace_id: String, - plan: SubscriptionPlan, - reason: Option<String>, - ) -> FlowyResult<()> { + pub async fn cancel_workspace_subscription(&self, workspace_id: String) -> FlowyResult<()> { self - .cloud_service + .cloud_services .get_user_service()? - .cancel_workspace_subscription(workspace_id, plan, reason) + .cancel_workspace_subscription(workspace_id) .await?; Ok(()) } #[instrument(level = "info", skip(self), err)] - pub async fn update_workspace_subscription_payment_period( - &self, - workspace_id: &Uuid, - plan: SubscriptionPlan, - recurring_interval: RecurringInterval, - ) -> FlowyResult<()> { - self - .cloud_service - .get_user_service()? - .update_workspace_subscription_payment_period(workspace_id, plan, recurring_interval) - .await?; - Ok(()) - } - - #[instrument(level = "info", skip(self), err)] - pub async fn get_subscription_plan_details(&self) -> FlowyResult<Vec<SubscriptionPlanDetail>> { - let plan_details = self - .cloud_service - .get_user_service()? - .get_subscription_plan_details() - .await?; - Ok(plan_details) - } - - #[instrument(level = "info", skip(self), err)] - pub async fn get_workspace_usage( - &self, - workspace_id: &Uuid, - ) -> FlowyResult<WorkspaceUsageAndLimit> { + pub async fn get_workspace_usage(&self, workspace_id: String) -> FlowyResult<WorkspaceUsage> { let workspace_usage = self - .cloud_service + .cloud_services .get_user_service()? .get_workspace_usage(workspace_id) .await?; - - // Check if the current workspace storage is not unlimited. If it is not unlimited, - // verify whether the storage bytes exceed the storage limit. - // If the storage is unlimited, allow writing. Otherwise, allow writing only if - // the storage bytes are less than the storage limit. - let can_write = if workspace_usage.storage_bytes_unlimited { - true - } else { - workspace_usage.storage_bytes < workspace_usage.storage_bytes_limit - }; - self - .user_status_callback - .read() - .await - .on_storage_permission_updated(can_write); - Ok(workspace_usage) } #[instrument(level = "info", skip(self), err)] pub async fn get_billing_portal_url(&self) -> FlowyResult<String> { let url = self - .cloud_service + .cloud_services .get_user_service()? .get_billing_portal_url() .await?; Ok(url) } - pub async fn update_workspace_setting( - &self, - updated_settings: UpdateUserWorkspaceSettingPB, - ) -> FlowyResult<()> { - let workspace_id = Uuid::from_str(&updated_settings.workspace_id)?; - let cloud_service = self.cloud_service.get_user_service()?; - let settings = cloud_service - .update_workspace_setting(&workspace_id, updated_settings.clone().into()) - .await?; - - let changeset = WorkspaceSettingsChangeset { - id: workspace_id.to_string(), - disable_search_indexing: updated_settings.disable_search_indexing, - ai_model: updated_settings.ai_model.clone(), - }; - - let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - update_workspace_setting(&mut conn, changeset)?; - - let pb = WorkspaceSettingsPB::from(&settings); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(pb) - .send(); - Ok(()) - } - - pub async fn get_workspace_settings( - &self, - workspace_id: &Uuid, - ) -> FlowyResult<WorkspaceSettingsPB> { - let uid = self.user_id()?; - let mut conn = self.db_connection(uid)?; - match select_workspace_setting(&mut conn, &workspace_id.to_string()) { - Ok(workspace_settings) => { - trace!("workspace settings found in local db"); - let pb = WorkspaceSettingsPB::from(workspace_settings); - let old_pb = pb.clone(); - let workspace_id = *workspace_id; - - // Spawn a task to sync remote settings using the helper - let pool = self.db_pool(uid)?; - let cloud_service = self.cloud_service.clone(); - tokio::spawn(async move { - let _ = sync_workspace_settings(cloud_service, workspace_id, old_pb, uid, pool).await; - }); - Ok(pb) - }, - Err(err) => { - if err.is_record_not_found() { - trace!("No workspace settings found, fetch from remote"); - let service = self.cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(workspace_id).await?; - let pb = WorkspaceSettingsPB::from(&settings); - let mut conn = self.db_connection(uid)?; - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(workspace_id, &settings), - )?; - Ok(pb) - } else { - Err(err) - } - }, - } - } - - pub async fn get_workspace_member_info( - &self, - uid: i64, - workspace_id: &Uuid, - ) -> FlowyResult<WorkspaceMember> { + #[instrument(level = "debug", skip(self), err)] + pub async fn get_workspace_member_info(&self, uid: i64) -> FlowyResult<WorkspaceMember> { + let workspace_id = self.get_session()?.user_workspace.id.clone(); let db = self.authenticate_user.get_sqlite_connection(uid)?; // Can opt in using memory cache - if let Ok(member_record) = select_workspace_member(db, &workspace_id.to_string(), uid) { + if let Ok(member_record) = select_workspace_member(db, &workspace_id, uid) { if is_older_than_n_minutes(member_record.updated_at, 10) { self - .get_workspace_member_info_from_remote(workspace_id, uid) + .get_workspace_member_info_from_remote(&workspace_id, uid) .await?; } @@ -675,7 +518,7 @@ impl UserManager { } let member = self - .get_workspace_member_info_from_remote(workspace_id, uid) + .get_workspace_member_info_from_remote(&workspace_id, uid) .await?; Ok(member) @@ -683,19 +526,19 @@ impl UserManager { async fn get_workspace_member_info_from_remote( &self, - workspace_id: &Uuid, + workspace_id: &str, uid: i64, ) -> FlowyResult<WorkspaceMember> { trace!("get workspace member info from remote: {}", workspace_id); let member = self - .cloud_service + .cloud_services .get_user_service()? - .get_workspace_member(workspace_id, uid) + .get_workspace_member_info(workspace_id, uid) .await?; let record = WorkspaceMemberTable { email: member.email.clone(), - role: member.role.into(), + role: member.role.clone().into(), name: member.name.clone(), avatar_url: member.avatar_url.clone(), uid, @@ -703,34 +546,118 @@ impl UserManager { updated_at: Utc::now().naive_utc(), }; - let mut db = self.authenticate_user.get_sqlite_connection(uid)?; - upsert_workspace_member(&mut db, record)?; + let db = self.authenticate_user.get_sqlite_connection(uid)?; + upsert_workspace_member(db, record)?; Ok(member) } +} - pub async fn notify_did_switch_plan( - &self, - success: SuccessWorkspaceSubscriptionPB, - ) -> FlowyResult<()> { - // periodically check the billing state - let workspace_id = Uuid::from_str(&success.workspace_id)?; - let plans = PeriodicallyCheckBillingState::new( - workspace_id, - success.plan.map(SubscriptionPlan::from), - Arc::downgrade(&self.cloud_service), - Arc::downgrade(&self.authenticate_user), +/// This method is used to save one user workspace to the SQLite database +/// +/// If the workspace is already persisted in the database, it will be overridden. +/// +/// Consider using [save_all_user_workspaces] if you need to override all workspaces of the user. +/// +pub fn save_user_workspace( + uid: i64, + mut conn: DBConnection, + user_workspace: &UserWorkspace, +) -> FlowyResult<()> { + conn.immediate_transaction(|conn| { + let user_workspace = UserWorkspaceTable::try_from((uid, user_workspace))?; + let affected_rows = diesel::update( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(&user_workspace.id)), ) - .start() - .await?; + .set(( + user_workspace_table::name.eq(&user_workspace.name), + user_workspace_table::created_at.eq(&user_workspace.created_at), + user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), + user_workspace_table::icon.eq(&user_workspace.icon), + )) + .execute(conn)?; - trace!("Current plans: {:?}", plans); - self - .user_status_callback - .read() - .await - .on_subscription_plans_updated(plans); - Ok(()) + if affected_rows == 0 { + diesel::insert_into(user_workspace_table::table) + .values(user_workspace) + .execute(conn)?; + } + + Ok::<(), FlowyError>(()) + }) +} + +/// This method is used to save the user workspaces (plural) to the SQLite database +/// +/// The workspaces provided in [user_workspaces] will override the existing workspaces in the database. +/// +/// Consider using [save_user_workspace] if you only need to save a single workspace. +/// +pub fn save_all_user_workspaces( + uid: i64, + mut conn: DBConnection, + user_workspaces: &[UserWorkspace], +) -> FlowyResult<()> { + let user_workspaces = user_workspaces + .iter() + .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace))) + .collect::<Result<Vec<_>, _>>()?; + + conn.immediate_transaction(|conn| { + let existing_ids = user_workspace_table::dsl::user_workspace_table + .select(user_workspace_table::id) + .load::<String>(conn)?; + let new_ids: Vec<String> = user_workspaces.iter().map(|w| w.id.clone()).collect(); + let ids_to_delete: Vec<String> = existing_ids + .into_iter() + .filter(|id| !new_ids.contains(id)) + .collect(); + + // insert or update the user workspaces + for user_workspace in &user_workspaces { + let affected_rows = diesel::update( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(&user_workspace.id)), + ) + .set(( + user_workspace_table::name.eq(&user_workspace.name), + user_workspace_table::created_at.eq(&user_workspace.created_at), + user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), + user_workspace_table::icon.eq(&user_workspace.icon), + )) + .execute(conn)?; + + if affected_rows == 0 { + diesel::insert_into(user_workspace_table::table) + .values(user_workspace) + .execute(conn)?; + } + } + + // delete the user workspaces that are not in the new list + if !ids_to_delete.is_empty() { + diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq_any(ids_to_delete)), + ) + .execute(conn)?; + } + + Ok::<(), FlowyError>(()) + }) +} + +pub fn delete_user_workspaces(mut conn: DBConnection, workspace_id: &str) -> FlowyResult<()> { + let n = conn.immediate_transaction(|conn| { + let rows_affected: usize = + diesel::delete(user_workspace_table::table.filter(user_workspace_table::id.eq(workspace_id))) + .execute(conn)?; + Ok::<usize, FlowyError>(rows_affected) + })?; + if n != 1 { + warn!("expected to delete 1 row, but deleted {} rows", n); } + Ok(()) } fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { @@ -740,45 +667,3 @@ fn is_older_than_n_minutes(updated_at: NaiveDateTime, minutes: i64) -> bool { None => false, } } - -async fn sync_workspace_settings( - cloud_service: Arc<dyn UserCloudServiceProvider>, - workspace_id: Uuid, - old_pb: WorkspaceSettingsPB, - uid: i64, - pool: Arc<ConnectionPool>, -) -> FlowyResult<()> { - let service = cloud_service.get_user_service()?; - let settings = service.get_workspace_setting(&workspace_id).await?; - let new_pb = WorkspaceSettingsPB::from(&settings); - if new_pb != old_pb { - trace!("workspace settings updated"); - send_notification( - &uid.to_string(), - UserNotification::DidUpdateWorkspaceSetting, - ) - .payload(new_pb) - .send(); - if let Ok(mut conn) = pool.get() { - upsert_workspace_setting( - &mut conn, - WorkspaceSettingsTable::from_workspace_settings(&workspace_id, &settings), - )?; - } - } - Ok(()) -} - -async fn sync_workspace( - workspace_id: &Uuid, - user_service: Arc<dyn UserCloudService>, - uid: i64, - auth_type: AuthType, - pool: Arc<ConnectionPool>, -) -> FlowyResult<UserWorkspace> { - let user_workspace = user_service.open_workspace(workspace_id).await?; - if let Ok(mut conn) = pool.get() { - upsert_user_workspace(uid, auth_type, user_workspace.clone(), &mut conn)?; - } - Ok(user_workspace) -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs index 23c050c1f2..3ce66227c5 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/mod.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/mod.rs @@ -3,5 +3,6 @@ pub(crate) mod manager_history_user; pub(crate) mod manager_user_awareness; pub(crate) mod manager_user_encryption; pub(crate) mod manager_user_workspace; +mod user_login_state; pub use manager::*; diff --git a/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs new file mode 100644 index 0000000000..906002ad10 --- /dev/null +++ b/frontend/rust-lib/flowy-user/src/user_manager/user_login_state.rs @@ -0,0 +1,11 @@ +use crate::migrations::AnonUser; +use flowy_user_pub::entities::{AuthResponse, Authenticator, UserProfile}; + +/// recording the intermediate state of the sign-in/sign-up process +#[derive(Clone)] +pub struct UserAuthProcess { + pub user_profile: UserProfile, + pub response: AuthResponse, + pub authenticator: Authenticator, + pub migration_user: Option<AnonUser>, +} diff --git a/frontend/rust-lib/lib-dispatch/Cargo.toml b/frontend/rust-lib/lib-dispatch/Cargo.toml index 1630d3bc47..f81eb2d084 100644 --- a/frontend/rust-lib/lib-dispatch/Cargo.toml +++ b/frontend/rust-lib/lib-dispatch/Cargo.toml @@ -21,8 +21,9 @@ derivative = "2.2.0" serde_json = { workspace = true, optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_repr = { workspace = true, optional = true } -validator = { workspace = true, features = ["derive"] } +validator = "0.16.1" tracing.workspace = true +parking_lot = "0.12" bincode = { version = "1.3", optional = true } protobuf = { workspace = true, optional = true } @@ -41,7 +42,9 @@ tokio = { workspace = true, features = ["rt"] } futures-util = "0.3.26" [features] -default = ["local_set", "use_protobuf"] +default = ["use_protobuf"] use_serde = ["bincode", "serde_json", "serde", "serde_repr"] use_protobuf = ["protobuf"] local_set = [] + + diff --git a/frontend/rust-lib/lib-dispatch/src/data.rs b/frontend/rust-lib/lib-dispatch/src/data.rs index e3989adfce..5d73bf87df 100644 --- a/frontend/rust-lib/lib-dispatch/src/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/data.rs @@ -2,7 +2,7 @@ use std::fmt::{Debug, Formatter}; use std::ops; use bytes::Bytes; -use validator::{Validate, ValidationErrors}; +use validator::ValidationErrors; use crate::{ byte_trait::*, @@ -28,7 +28,7 @@ impl<T> AFPluginData<T> { impl<T> AFPluginData<T> where - T: Validate, + T: validator::Validate, { pub fn try_into_inner(self) -> Result<T, ValidationErrors> { self.0.validate()?; diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index 0e8e84fa6b..eb55bfc4fa 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -1,10 +1,10 @@ +use std::any::Any; +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::{future::Future, sync::Arc}; + use derivative::*; use pin_project::pin_project; -use std::any::Any; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; -use std::task::{Context, Poll}; use tracing::event; use crate::module::AFPluginStateMap; @@ -16,53 +16,70 @@ use crate::{ service::{AFPluginServiceFactory, Service}, }; -#[cfg(feature = "local_set")] -pub trait AFConcurrent: Send {} +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] +pub trait AFConcurrent {} -#[cfg(feature = "local_set")] -impl<T> AFConcurrent for T where T: Send + ?Sized {} +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] +impl<T> AFConcurrent for T where T: ?Sized {} -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub trait AFConcurrent: Send + Sync {} -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] impl<T> AFConcurrent for T where T: Send + Sync {} -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type AFBoxFuture<'a, T> = futures_core::future::LocalBoxFuture<'a, T>; -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type AFBoxFuture<'a, T> = futures_core::future::BoxFuture<'a, T>; pub type AFStateMap = std::sync::Arc<AFPluginStateMap>; -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub(crate) fn downcast_owned<T: 'static>(boxed: AFBox) -> Option<T> { boxed.downcast().ok().map(|boxed| *boxed) } -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub(crate) fn downcast_owned<T: 'static + Send + Sync>(boxed: AFBox) -> Option<T> { boxed.downcast().ok().map(|boxed| *boxed) } -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] +pub(crate) type AFBox = Box<dyn Any>; + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub(crate) type AFBox = Box<dyn Any + Send + Sync>; -#[cfg(not(feature = "local_set"))] -pub(crate) type AFBox = Box<dyn Any + Send + Sync>; - -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type BoxFutureCallback = Box<dyn FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + 'static>; -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type BoxFutureCallback = Box<dyn FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + Send + Sync + 'static>; +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] +pub fn af_spawn<T>(future: T) -> tokio::task::JoinHandle<T::Output> +where + T: Future + 'static, + T::Output: 'static, +{ + tokio::task::spawn_local(future) +} + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] +pub fn af_spawn<T>(future: T) -> tokio::task::JoinHandle<T::Output> +where + T: Future + Send + 'static, + T::Output: Send + 'static, +{ + tokio::spawn(future) +} + pub struct AFPluginDispatcher { plugins: AFPluginMap, - #[allow(dead_code)] runtime: Arc<AFPluginRuntime>, } @@ -75,62 +92,13 @@ impl AFPluginDispatcher { } } - #[cfg(feature = "local_set")] pub async fn async_send<Req>(dispatch: &AFPluginDispatcher, request: Req) -> AFPluginEventResponse where - Req: Into<AFPluginRequest> + 'static, + Req: Into<AFPluginRequest>, { AFPluginDispatcher::async_send_with_callback(dispatch, request, |_| Box::pin(async {})).await } - #[cfg(feature = "local_set")] - pub async fn async_send_with_callback<Req, Callback>( - dispatch: &AFPluginDispatcher, - request: Req, - callback: Callback, - ) -> AFPluginEventResponse - where - Req: Into<AFPluginRequest> + 'static, - Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, - { - Self::boxed_async_send_with_callback(dispatch, request, callback).await - } - #[cfg(feature = "local_set")] - pub async fn boxed_async_send_with_callback<Req, Callback>( - dispatch: &AFPluginDispatcher, - request: Req, - callback: Callback, - ) -> AFPluginEventResponse - where - Req: Into<AFPluginRequest> + 'static, - Callback: FnOnce(AFPluginEventResponse) -> AFBoxFuture<'static, ()> + AFConcurrent + 'static, - { - let request: AFPluginRequest = request.into(); - let plugins = dispatch.plugins.clone(); - let service = Box::new(DispatchService { plugins }); - tracing::trace!("[dispatch]: Async event: {:?}", &request.event); - let service_ctx = DispatchContext { - request, - callback: Some(Box::new(callback)), - }; - - let result = tokio::task::spawn_local(async move { - service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() - }) - }) - .await; - - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - } - - #[cfg(not(feature = "local_set"))] pub async fn async_send_with_callback<Req, Callback>( dispatch: &AFPluginDispatcher, request: Req, @@ -149,25 +117,42 @@ impl AFPluginDispatcher { callback: Some(Box::new(callback)), }; - dispatch - .runtime - .spawn(async move { - service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); - InternalError::Other(format!("{:?}", e)).as_response() - }) - }) - .await - .unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() + // Spawns a future onto the runtime. + // + // This spawns the given future onto the runtime's executor, usually a + // thread pool. The thread pool is then responsible for polling the future + // until it completes. + // + // The provided future will start running in the background immediately + // when `spawn` is called, even if you don't await the returned + // `JoinHandle`. + let handle = dispatch.runtime.spawn(async move { + service.call(service_ctx).await.unwrap_or_else(|e| { + tracing::error!("Dispatch runtime error: {:?}", e); + InternalError::Other(format!("{:?}", e)).as_response() }) + }); + + let result = dispatch.runtime.run_until(handle).await; + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) } - #[cfg(not(feature = "local_set"))] - pub async fn boxed_async_send_with_callback<Req, Callback>( + pub fn box_async_send<Req>( + dispatch: &AFPluginDispatcher, + request: Req, + ) -> DispatchFuture<AFPluginEventResponse> + where + Req: Into<AFPluginRequest> + 'static, + { + AFPluginDispatcher::boxed_async_send_with_callback(dispatch, request, |_| Box::pin(async {})) + } + + pub fn boxed_async_send_with_callback<Req, Callback>( dispatch: &AFPluginDispatcher, request: Req, callback: Callback, @@ -192,21 +177,39 @@ impl AFPluginDispatcher { }) }); - let runtime = dispatch.runtime.clone(); - DispatchFuture { - fut: Box::pin(async move { - let result = runtime.spawn(handle).await.unwrap(); - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - }), + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + { + let result = dispatch.runtime.block_on(handle); + DispatchFuture { + fut: Box::pin(async move { + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] + { + let runtime = dispatch.runtime.clone(); + DispatchFuture { + fut: Box::pin(async move { + let result = runtime.run_until(handle).await; + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + }), + } } } - #[cfg(feature = "local_set")] + #[cfg(not(target_arch = "wasm32"))] pub fn sync_send( dispatch: Arc<AFPluginDispatcher>, request: AFPluginRequest, @@ -217,6 +220,43 @@ impl AFPluginDispatcher { |_| Box::pin(async {}), )) } + + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + #[track_caller] + pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output> + where + F: Future + 'static, + { + self.runtime.spawn(future) + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] + #[track_caller] + pub fn spawn<F>(&self, future: F) -> tokio::task::JoinHandle<F::Output> + where + F: Future + Send + 'static, + <F as Future>::Output: Send + 'static, + { + self.runtime.spawn(future) + } + + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + pub async fn run_until<F>(&self, future: F) -> F::Output + where + F: Future + 'static, + { + let handle = self.runtime.spawn(future); + self.runtime.run_until(handle).await.unwrap() + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] + pub async fn run_until<'a, F>(&self, future: F) -> F::Output + where + F: Future + Send + 'a, + <F as Future>::Output: Send + 'a, + { + self.runtime.run_until(future).await + } } #[derive(Derivative)] diff --git a/frontend/rust-lib/lib-dispatch/src/module/container.rs b/frontend/rust-lib/lib-dispatch/src/module/container.rs index 4082590345..d6fdf24d67 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/container.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/container.rs @@ -13,7 +13,7 @@ impl AFPluginStateMap { pub fn insert<T>(&mut self, val: T) -> Option<T> where - T: 'static + Send + Sync, + T: 'static + AFConcurrent, { self .0 diff --git a/frontend/rust-lib/lib-dispatch/src/module/data.rs b/frontend/rust-lib/lib-dispatch/src/module/data.rs index 3cf8f23d51..520c3e2494 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/data.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/data.rs @@ -53,7 +53,7 @@ where impl<T> FromAFPluginRequest for AFPluginState<T> where - T: ?Sized + Send + Sync + 'static, + T: ?Sized + AFConcurrent + 'static, { type Error = DispatchError; type Future = Ready<Result<Self, DispatchError>>; diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index d0a146da7a..a5b2df234a 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -1,3 +1,18 @@ +use std::sync::Arc; +use std::{ + collections::HashMap, + fmt, + fmt::{Debug, Display}, + future::Future, + hash::Hash, + pin::Pin, + task::{Context, Poll}, +}; + +use futures_core::ready; +use nanoid::nanoid; +use pin_project::pin_project; + use crate::dispatcher::AFConcurrent; use crate::prelude::{AFBoxFuture, AFStateMap}; use crate::service::AFPluginHandler; @@ -10,39 +25,21 @@ use crate::{ Service, ServiceRequest, ServiceResponse, }, }; -use futures_core::ready; -use nanoid::nanoid; -use pin_project::pin_project; -use std::sync::Arc; -use std::{ - collections::HashMap, - fmt, - fmt::{Debug, Display}, - future::Future, - hash::Hash, - pin::Pin, - task::{Context, Poll}, -}; pub type AFPluginMap = Arc<HashMap<AFPluginEvent, Arc<AFPlugin>>>; pub(crate) fn plugin_map_or_crash(plugins: Vec<AFPlugin>) -> AFPluginMap { let mut plugin_map: HashMap<AFPluginEvent, Arc<AFPlugin>> = HashMap::new(); plugins.into_iter().for_each(|m| { let events = m.events(); - #[allow(clippy::arc_with_non_send_sync)] let plugins = Arc::new(m); events.into_iter().for_each(|e| { if plugin_map.contains_key(&e) { let plugin_name = plugin_map.get(&e).map(|p| &p.name); - panic!( - "⚠️⚠️⚠️Error: {:?} is already defined in {:?}", - &e, plugin_name, - ); + panic!("⚠️⚠️⚠️Error: {:?} is already defined in {:?}", &e, plugin_name,); } plugin_map.insert(e, plugins.clone()); }); }); - #[allow(clippy::arc_with_non_send_sync)] Arc::new(plugin_map) } @@ -80,7 +77,6 @@ impl std::default::Default for AFPlugin { Self { name: "".to_owned(), states: Default::default(), - #[allow(clippy::arc_with_non_send_sync)] event_service_factory: Arc::new(HashMap::new()), } } @@ -92,11 +88,11 @@ impl AFPlugin { } pub fn name(mut self, s: &str) -> Self { - s.clone_into(&mut self.name); + self.name = s.to_owned(); self } - pub fn state<D: Send + Sync + 'static>(mut self, data: D) -> Self { + pub fn state<D: AFConcurrent + 'static>(mut self, data: D) -> Self { Arc::get_mut(&mut self.states) .unwrap() .insert(crate::module::AFPluginState::new(data)); diff --git a/frontend/rust-lib/lib-dispatch/src/request/request.rs b/frontend/rust-lib/lib-dispatch/src/request/request.rs index 68aab764d4..c62950f65d 100644 --- a/frontend/rust-lib/lib-dispatch/src/request/request.rs +++ b/frontend/rust-lib/lib-dispatch/src/request/request.rs @@ -8,7 +8,7 @@ use std::{ use derivative::*; use futures_core::ready; -use crate::prelude::AFStateMap; +use crate::prelude::{AFConcurrent, AFStateMap}; use crate::{ errors::{DispatchError, InternalError}, module::AFPluginEvent, @@ -39,7 +39,7 @@ impl AFPluginEventRequest { pub fn get_state<T>(&self) -> Option<T> where - T: Send + Sync + 'static + Clone, + T: AFConcurrent + 'static + Clone, { if let Some(data) = self.states.get::<T>() { return Some(data.clone()); diff --git a/frontend/rust-lib/lib-dispatch/src/runtime.rs b/frontend/rust-lib/lib-dispatch/src/runtime.rs index e2f5cd56c3..fd3658517c 100644 --- a/frontend/rust-lib/lib-dispatch/src/runtime.rs +++ b/frontend/rust-lib/lib-dispatch/src/runtime.rs @@ -7,15 +7,17 @@ use tokio::runtime::Runtime; use tokio::task::JoinHandle; pub struct AFPluginRuntime { - pub(crate) inner: Runtime, + inner: Runtime, + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + local: tokio::task::LocalSet, } impl Display for AFPluginRuntime { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if cfg!(any(target_arch = "wasm32", feature = "local_set")) { - write!(f, "Runtime(local_set)") + write!(f, "Runtime(current_thread)") } else { - write!(f, "Runtime") + write!(f, "Runtime(multi_thread)") } } } @@ -23,9 +25,23 @@ impl Display for AFPluginRuntime { impl AFPluginRuntime { pub fn new() -> io::Result<Self> { let inner = default_tokio_runtime()?; - Ok(Self { inner }) + Ok(Self { + inner, + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + local: tokio::task::LocalSet::new(), + }) } + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + #[track_caller] + pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output> + where + F: Future + 'static, + { + self.local.spawn_local(future) + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] #[track_caller] pub fn spawn<F>(&self, future: F) -> JoinHandle<F::Output> where @@ -35,6 +51,32 @@ impl AFPluginRuntime { self.inner.spawn(future) } + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + pub async fn run_until<F>(&self, future: F) -> F::Output + where + F: Future, + { + self.local.run_until(future).await + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] + pub async fn run_until<F>(&self, future: F) -> F::Output + where + F: Future, + { + future.await + } + + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + #[track_caller] + pub fn block_on<F>(&self, f: F) -> F::Output + where + F: Future, + { + self.local.block_on(&self.inner, f) + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] #[track_caller] pub fn block_on<F>(&self, f: F) -> F::Output where @@ -44,16 +86,14 @@ impl AFPluginRuntime { } } -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub fn default_tokio_runtime() -> io::Result<Runtime> { - runtime::Builder::new_multi_thread() - .enable_io() - .enable_time() + runtime::Builder::new_current_thread() .thread_name("dispatch-rt-st") .build() } -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub fn default_tokio_runtime() -> io::Result<Runtime> { runtime::Builder::new_multi_thread() .thread_name("dispatch-rt-mt") diff --git a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs index 811b995082..7ff7a7c116 100644 --- a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs +++ b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs @@ -16,7 +16,7 @@ where BoxServiceFactory(Box::new(FactoryWrapper(factory))) } -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] type Inner<Cfg, Req, Res, Err> = Box< dyn AFPluginServiceFactory< Req, @@ -27,7 +27,7 @@ type Inner<Cfg, Req, Res, Err> = Box< Future = AFBoxFuture<'static, Result<BoxService<Req, Res, Err>, Err>>, >, >; -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] type Inner<Cfg, Req, Res, Err> = Box< dyn AFPluginServiceFactory< Req, @@ -58,12 +58,12 @@ where } } -#[cfg(feature = "local_set")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type BoxService<Req, Res, Err> = Box< dyn Service<Req, Response = Res, Error = Err, Future = AFBoxFuture<'static, Result<Res, Err>>>, >; -#[cfg(not(feature = "local_set"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type BoxService<Req, Res, Err> = Box< dyn Service<Req, Response = Res, Error = Err, Future = AFBoxFuture<'static, Result<Res, Err>>> + Sync diff --git a/frontend/rust-lib/lib-dispatch/tests/api/module.rs b/frontend/rust-lib/lib-dispatch/tests/api/module.rs index 214eda32fa..2c4539bd7e 100644 --- a/frontend/rust-lib/lib-dispatch/tests/api/module.rs +++ b/frontend/rust-lib/lib-dispatch/tests/api/module.rs @@ -1,7 +1,7 @@ +use std::sync::Arc; + use lib_dispatch::prelude::*; use lib_dispatch::runtime::AFPluginRuntime; -use std::sync::Arc; -use tokio::task::LocalSet; pub async fn hello() -> String { "say hello".to_string() @@ -11,24 +11,17 @@ pub async fn hello() -> String { async fn test() { let event = "1"; let runtime = Arc::new(AFPluginRuntime::new().unwrap()); - #[allow(clippy::arc_with_non_send_sync)] let dispatch = Arc::new(AFPluginDispatcher::new( runtime, vec![AFPlugin::new().event(event, hello)], )); let request = AFPluginRequest::new(event); - let local_set = LocalSet::new(); - local_set - .run_until(AFPluginDispatcher::async_send_with_callback( - dispatch.as_ref(), - request, - |resp| { - Box::pin(async move { - dbg!(&resp); - }) - }, - )) - .await; + let _ = AFPluginDispatcher::async_send_with_callback(dispatch.as_ref(), request, |resp| { + Box::pin(async move { + dbg!(&resp); + }) + }) + .await; std::mem::forget(dispatch); } diff --git a/frontend/rust-lib/lib-infra/Cargo.toml b/frontend/rust-lib/lib-infra/Cargo.toml index a07a3413f4..6b902537ee 100644 --- a/frontend/rust-lib/lib-infra/Cargo.toml +++ b/frontend/rust-lib/lib-infra/Cargo.toml @@ -18,31 +18,20 @@ md5 = "0.7.0" anyhow.workspace = true walkdir = "2.4.0" tempfile = "3.8.1" -validator = { workspace = true, features = ["derive"] } +validator = { version = "0.16.1", features = ["derive"] } tracing.workspace = true atomic_refcell = "0.1" allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true } -futures = "0.3.31" -cfg-if = "1.0.0" -futures-util = "0.3.30" - - -aes-gcm = { version = "0.10.2", optional = true } -rand = { version = "0.8.5", optional = true } -pbkdf2 = { version = "0.12.2", optional = true } -hmac = { version = "0.12.1", optional = true } -sha2 = { version = "0.10.7", optional = true } -base64 = { version = "0.22.1" } +futures = "0.3.30" [dev-dependencies] rand = "0.8.5" -futures = "0.3.31" +futures = "0.3.30" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -zip = { version = "2.2.0", features = ["deflate"] } +zip = { version = "0.6.6", features = ["deflate"] } brotli = { version = "3.4.0", optional = true } [features] compression = ["brotli"] isolate_flutter = ["allo-isolate"] -encryption = ["aes-gcm", "rand", "pbkdf2", "hmac", "sha2"] \ No newline at end of file diff --git a/frontend/rust-lib/lib-infra/src/encryption/mod.rs b/frontend/rust-lib/lib-infra/src/encryption/mod.rs deleted file mode 100644 index 6caabdc5cc..0000000000 --- a/frontend/rust-lib/lib-infra/src/encryption/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -use aes_gcm::aead::generic_array::GenericArray; -use aes_gcm::aead::Aead; -use aes_gcm::{Aes256Gcm, KeyInit}; -use anyhow::Result; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; -use pbkdf2::hmac::Hmac; -use pbkdf2::pbkdf2; -use rand::distributions::Alphanumeric; -use rand::Rng; -use sha2::Sha256; - -/// The length of the salt in bytes. -const SALT_LENGTH: usize = 16; - -/// The length of the derived encryption key in bytes. -const KEY_LENGTH: usize = 32; - -/// The number of iterations for the PBKDF2 key derivation. -const ITERATIONS: u32 = 1000; - -/// The length of the nonce for AES-GCM encryption. -const NONCE_LENGTH: usize = 12; - -/// Delimiter used to concatenate the passphrase and salt. -const CONCATENATED_DELIMITER: &str = "$"; - -/// Generate a new encryption secret consisting of a passphrase and a salt. -pub fn generate_encryption_secret() -> String { - let passphrase = generate_random_passphrase(); - let salt = generate_random_salt(); - combine_passphrase_and_salt(&passphrase, &salt) -} - -/// Encrypt a byte slice using AES-GCM. -/// -/// # Arguments -/// * `data`: The data to encrypt. -/// * `combined_passphrase_salt`: The concatenated passphrase and salt. -pub fn encrypt_data<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> { - let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?; - let key = derive_key(passphrase, &salt)?; - let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); - let nonce: [u8; NONCE_LENGTH] = rand::thread_rng().gen(); - let ciphertext = cipher - .encrypt(GenericArray::from_slice(&nonce), data.as_ref()) - .unwrap(); - - let result = nonce - .iter() - .copied() - .chain(ciphertext.iter().copied()) - .collect(); - Ok(result) -} - -/// Decrypt a byte slice using AES-GCM. -/// -/// # Arguments -/// * `data`: The data to decrypt. -/// * `combined_passphrase_salt`: The concatenated passphrase and salt. -pub fn decrypt_data<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<Vec<u8>> { - if data.as_ref().len() <= NONCE_LENGTH { - return Err(anyhow::anyhow!("Ciphertext too short to include nonce.")); - } - let (passphrase, salt) = split_passphrase_and_salt(combined_passphrase_salt)?; - let key = derive_key(passphrase, &salt)?; - let cipher = Aes256Gcm::new(GenericArray::from_slice(&key)); - let (nonce, cipher_data) = data.as_ref().split_at(NONCE_LENGTH); - cipher - .decrypt(GenericArray::from_slice(nonce), cipher_data) - .map_err(|e| anyhow::anyhow!("Decryption error: {:?}", e)) -} - -/// Encrypt a string using AES-GCM and return the result as a base64 encoded string. -/// -/// # Arguments -/// * `data`: The string data to encrypt. -/// * `combined_passphrase_salt`: The concatenated passphrase and salt. -pub fn encrypt_text<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> { - let encrypted = encrypt_data(data.as_ref(), combined_passphrase_salt)?; - Ok(STANDARD.encode(encrypted)) -} - -/// Decrypt a base64 encoded string using AES-GCM. -/// -/// # Arguments -/// * `data`: The base64 encoded string to decrypt. -/// * `combined_passphrase_salt`: The concatenated passphrase and salt. -pub fn decrypt_text<T: AsRef<[u8]>>(data: T, combined_passphrase_salt: &str) -> Result<String> { - let encrypted = STANDARD.decode(data)?; - let decrypted = decrypt_data(encrypted, combined_passphrase_salt)?; - Ok(String::from_utf8(decrypted)?) -} - -/// Generates a random passphrase consisting of alphanumeric characters. -/// -/// This function creates a passphrase with both uppercase and lowercase letters -/// as well as numbers. The passphrase is 30 characters in length. -/// -/// # Returns -/// -/// A `String` representing the generated passphrase. -/// -/// # Security Considerations -/// -/// The passphrase is derived from the `Alphanumeric` character set which includes 62 possible -/// characters (26 lowercase letters, 26 uppercase letters, 10 numbers). This results in a total -/// of `62^30` possible combinations, making it strong against brute force attacks. -/// -fn generate_random_passphrase() -> String { - rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(30) // e.g., 30 characters - .map(char::from) - .collect() -} - -fn generate_random_salt() -> [u8; SALT_LENGTH] { - let mut rng = rand::thread_rng(); - let salt: [u8; SALT_LENGTH] = rng.gen(); - salt -} - -fn combine_passphrase_and_salt(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> String { - let salt_base64 = STANDARD.encode(salt); - format!("{}{}{}", passphrase, CONCATENATED_DELIMITER, salt_base64) -} - -fn split_passphrase_and_salt(combined: &str) -> Result<(&str, [u8; SALT_LENGTH]), anyhow::Error> { - let parts: Vec<&str> = combined.split(CONCATENATED_DELIMITER).collect(); - if parts.len() != 2 { - return Err(anyhow::anyhow!("Invalid combined format")); - } - let passphrase = parts[0]; - let salt = STANDARD.decode(parts[1])?; - if salt.len() != SALT_LENGTH { - return Err(anyhow::anyhow!("Incorrect salt length")); - } - let mut salt_array = [0u8; SALT_LENGTH]; - salt_array.copy_from_slice(&salt); - Ok((passphrase, salt_array)) -} - -fn derive_key(passphrase: &str, salt: &[u8; SALT_LENGTH]) -> Result<[u8; KEY_LENGTH]> { - let mut key = [0u8; KEY_LENGTH]; - pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), salt, ITERATIONS, &mut key)?; - Ok(key) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn encrypt_decrypt_test() { - let secret = generate_encryption_secret(); - let data = b"hello world"; - let encrypted = encrypt_data(data, &secret).unwrap(); - let decrypted = decrypt_data(encrypted, &secret).unwrap(); - assert_eq!(data, decrypted.as_slice()); - - let s = "123".to_string(); - let encrypted = encrypt_text(&s, &secret).unwrap(); - let decrypted_str = decrypt_text(encrypted, &secret).unwrap(); - assert_eq!(s, decrypted_str); - } - - #[test] - fn decrypt_with_invalid_secret_test() { - let secret = generate_encryption_secret(); - let data = b"hello world"; - let encrypted = encrypt_data(data, &secret).unwrap(); - let decrypted = decrypt_data(encrypted, "invalid secret"); - assert!(decrypted.is_err()) - } -} diff --git a/frontend/rust-lib/lib-infra/src/file_util.rs b/frontend/rust-lib/lib-infra/src/file_util.rs index 6de14b2304..2186c71eaa 100644 --- a/frontend/rust-lib/lib-infra/src/file_util.rs +++ b/frontend/rust-lib/lib-infra/src/file_util.rs @@ -4,11 +4,12 @@ use std::fs::File; use std::path::{Path, PathBuf}; use std::time::SystemTime; use std::{fs, io}; + use tempfile::tempdir; use walkdir::WalkDir; use zip::write::FileOptions; +use zip::ZipArchive; use zip::ZipWriter; -use zip::{CompressionMethod, ZipArchive}; pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) { @@ -68,67 +69,48 @@ where } pub fn zip_folder(src_path: impl AsRef<Path>, dest_path: &Path) -> io::Result<()> { - let src_path = src_path.as_ref(); - - // Check if source path exists - if !src_path.exists() { - return Err(io::Error::new( - io::ErrorKind::NotFound, - "Source path not found", - )); + if !src_path.as_ref().exists() { + return Err(io::ErrorKind::NotFound.into()); } - // Check if source and destination paths are the same - if src_path == dest_path { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Source and destination paths cannot be the same", - )); + if src_path.as_ref() == dest_path { + return Err(io::ErrorKind::InvalidInput.into()); } - // Create a file at the destination path to write the ZIP file let file = File::create(dest_path)?; let mut zip = ZipWriter::new(file); + let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated); - // Traverse through the source directory recursively - for entry in WalkDir::new(src_path).into_iter().filter_map(|e| e.ok()) { + for entry in WalkDir::new(&src_path) { + let entry = entry?; let path = entry.path(); - let name = path - .strip_prefix(src_path) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "Invalid path"))?; + let name = match path.strip_prefix(&src_path) { + Ok(n) => n, + Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Invalid path")), + }; - // If the entry is a file, add it to the ZIP archive if path.is_file() { - let file_name = name - .to_str() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?; - zip.start_file::<_, ()>( - file_name, - FileOptions::default().compression_method(CompressionMethod::Deflated), + zip.start_file( + name + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?, + options, )?; - - // Instead of loading the entire file into memory, use `copy` for efficient writing let mut f = File::open(path)?; io::copy(&mut f, &mut zip)?; - - // If the entry is a directory, add it to the ZIP archive - } else if path.is_dir() { - let dir_name = name - .to_str() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?; - if !dir_name.is_empty() { - zip.add_directory::<_, ()>( - dir_name, - FileOptions::default().compression_method(CompressionMethod::Deflated), - )?; - } + } else if !name.as_os_str().is_empty() { + zip.add_directory( + name + .to_str() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?, + options, + )?; } } - - // Finish the ZIP archive zip.finish()?; Ok(()) } + pub fn unzip_and_replace( zip_path: impl AsRef<Path>, target_folder: &Path, diff --git a/frontend/rust-lib/lib-infra/src/isolate_stream.rs b/frontend/rust-lib/lib-infra/src/isolate_stream.rs index cebc2b7d10..19f692dda4 100644 --- a/frontend/rust-lib/lib-infra/src/isolate_stream.rs +++ b/frontend/rust-lib/lib-infra/src/isolate_stream.rs @@ -1,13 +1,10 @@ use allo_isolate::{IntoDart, Isolate}; -use anyhow::anyhow; use futures::Sink; -pub use futures_util::sink::SinkExt; use pin_project::pin_project; use std::pin::Pin; use std::task::{Context, Poll}; #[pin_project] -#[derive(Clone, Debug)] pub struct IsolateSink { isolate: Isolate, } @@ -22,7 +19,7 @@ impl<T> Sink<T> for IsolateSink where T: IntoDart, { - type Error = anyhow::Error; + type Error = (); fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { Poll::Ready(Ok(())) @@ -33,7 +30,7 @@ where if this.isolate.post(item) { Ok(()) } else { - Err(anyhow!("failed to post message")) + Err(()) } } diff --git a/frontend/rust-lib/lib-infra/src/lib.rs b/frontend/rust-lib/lib-infra/src/lib.rs index dc2ed5263c..18539e49aa 100644 --- a/frontend/rust-lib/lib-infra/src/lib.rs +++ b/frontend/rust-lib/lib-infra/src/lib.rs @@ -19,12 +19,9 @@ if_wasm! { } } -#[cfg(feature = "encryption")] -pub mod encryption; #[cfg(feature = "isolate_flutter")] pub mod isolate_stream; pub mod priority_task; pub mod ref_map; -pub mod stream_util; pub mod util; pub mod validator_fn; diff --git a/frontend/rust-lib/lib-infra/src/native/future.rs b/frontend/rust-lib/lib-infra/src/native/future.rs index 0f1c174c55..4d918d7e7c 100644 --- a/frontend/rust-lib/lib-infra/src/native/future.rs +++ b/frontend/rust-lib/lib-infra/src/native/future.rs @@ -2,6 +2,7 @@ use futures_core::future::BoxFuture; use futures_core::ready; use pin_project::pin_project; use std::{ + fmt::Debug, future::Future, pin::Pin, task::{Context, Poll}, @@ -32,4 +33,33 @@ where } } +#[pin_project] +pub struct FutureResult<T, E> { + #[pin] + pub fut: Pin<Box<dyn Future<Output = Result<T, E>> + Sync + Send>>, +} + +impl<T, E> FutureResult<T, E> { + pub fn new<F>(f: F) -> Self + where + F: Future<Output = Result<T, E>> + Send + Sync + 'static, + { + Self { fut: Box::pin(f) } + } +} + +impl<T, E> Future for FutureResult<T, E> +where + T: Send + Sync, + E: Debug, +{ + type Output = Result<T, E>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { + let this = self.as_mut().project(); + let result = ready!(this.fut.poll(cx)); + Poll::Ready(result) + } +} + pub type BoxResultFuture<'a, T, E> = BoxFuture<'a, Result<T, E>>; diff --git a/frontend/rust-lib/lib-infra/src/priority_task/scheduler.rs b/frontend/rust-lib/lib-infra/src/priority_task/scheduler.rs index 96798b742a..1e1bb33c2d 100644 --- a/frontend/rust-lib/lib-infra/src/priority_task/scheduler.rs +++ b/frontend/rust-lib/lib-infra/src/priority_task/scheduler.rs @@ -2,14 +2,13 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use crate::future::BoxResultFuture; use crate::priority_task::queue::TaskQueue; use crate::priority_task::store::TaskStore; use crate::priority_task::{Task, TaskContent, TaskId, TaskState}; use anyhow::Error; -use async_trait::async_trait; use tokio::sync::{watch, RwLock}; use tokio::time::interval; -use tracing::trace; pub struct TaskDispatcher { queue: TaskQueue, @@ -106,11 +105,6 @@ impl TaskDispatcher { return; } - trace!( - "Add task: handler:{}, task:{:?}", - task.handler_id, - task.content - ); self.queue.push(&task); self.store.insert_task(task); self.notify(); @@ -166,7 +160,6 @@ impl TaskRunner { } } -#[async_trait] pub trait TaskHandler: Send + Sync + 'static { fn handler_id(&self) -> &str; @@ -174,10 +167,9 @@ pub trait TaskHandler: Send + Sync + 'static { "" } - async fn run(&self, content: TaskContent) -> Result<(), Error>; + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error>; } -#[async_trait] impl<T> TaskHandler for Box<T> where T: TaskHandler, @@ -190,12 +182,11 @@ where (**self).handler_name() } - async fn run(&self, content: TaskContent) -> Result<(), Error> { - (**self).run(content).await + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error> { + (**self).run(content) } } -#[async_trait] impl<T> TaskHandler for Arc<T> where T: TaskHandler, @@ -208,7 +199,7 @@ where (**self).handler_name() } - async fn run(&self, content: TaskContent) -> Result<(), Error> { - (**self).run(content).await + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error> { + (**self).run(content) } } diff --git a/frontend/rust-lib/lib-infra/src/priority_task/store.rs b/frontend/rust-lib/lib-infra/src/priority_task/store.rs index ed3401fd9b..bf5d8f0829 100644 --- a/frontend/rust-lib/lib-infra/src/priority_task/store.rs +++ b/frontend/rust-lib/lib-infra/src/priority_task/store.rs @@ -13,7 +13,7 @@ impl TaskStore { pub fn new() -> Self { Self { tasks: HashMap::new(), - task_id_counter: AtomicU32::new(1), + task_id_counter: AtomicU32::new(0), } } @@ -45,6 +45,7 @@ impl TaskStore { } pub(crate) fn next_task_id(&self) -> TaskId { - self.task_id_counter.fetch_add(1, SeqCst) + let old = self.task_id_counter.fetch_add(1, SeqCst); + old + 1 } } diff --git a/frontend/rust-lib/lib-infra/src/stream_util.rs b/frontend/rust-lib/lib-infra/src/stream_util.rs deleted file mode 100644 index 41c747d26a..0000000000 --- a/frontend/rust-lib/lib-infra/src/stream_util.rs +++ /dev/null @@ -1,21 +0,0 @@ -use futures_core::Stream; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tokio::sync::mpsc; -use tokio::sync::mpsc::{Receiver, Sender}; - -struct BoundedStream<T> { - recv: Receiver<T>, -} -impl<T> Stream for BoundedStream<T> { - type Item = T; - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<T>> { - Pin::into_inner(self).recv.poll_recv(cx) - } -} - -pub fn mpsc_channel_stream<T: Unpin>(size: usize) -> (Sender<T>, impl Stream<Item = T>) { - let (tx, rx) = mpsc::channel(size); - let stream = BoundedStream { recv: rx }; - (tx, stream) -} diff --git a/frontend/rust-lib/lib-infra/src/util.rs b/frontend/rust-lib/lib-infra/src/util.rs index 18ff068a73..823095a26f 100644 --- a/frontend/rust-lib/lib-infra/src/util.rs +++ b/frontend/rust-lib/lib-infra/src/util.rs @@ -1,6 +1,3 @@ -#[allow(unused_imports)] -use tracing::info; - #[macro_export] macro_rules! if_native { ($($item:item)*) => {$( @@ -70,8 +67,9 @@ pub fn md5<T: AsRef<[u8]>>(data: T) -> String { let md5 = format!("{:x}", md5::compute(data)); md5 } + #[derive(Debug, Clone, PartialEq, Eq)] -pub enum OperatingSystem { +pub enum Platform { Unknown, Windows, Linux, @@ -80,69 +78,33 @@ pub enum OperatingSystem { Android, } -impl OperatingSystem { +impl Platform { pub fn is_not_ios(&self) -> bool { - !matches!(self, OperatingSystem::IOS) - } - - pub fn is_desktop(&self) -> bool { - matches!( - self, - OperatingSystem::Windows | OperatingSystem::Linux | OperatingSystem::MacOS - ) - } - - pub fn is_not_desktop(&self) -> bool { - !self.is_desktop() + !matches!(self, Platform::IOS) } } -impl From<String> for OperatingSystem { +impl From<String> for Platform { fn from(s: String) -> Self { - OperatingSystem::from(s.as_str()) + Platform::from(s.as_str()) } } -impl From<&String> for OperatingSystem { +impl From<&String> for Platform { fn from(s: &String) -> Self { - OperatingSystem::from(s.as_str()) + Platform::from(s.as_str()) } } -impl From<&str> for OperatingSystem { +impl From<&str> for Platform { fn from(s: &str) -> Self { match s { - "windows" => OperatingSystem::Windows, - "linux" => OperatingSystem::Linux, - "macos" => OperatingSystem::MacOS, - "ios" => OperatingSystem::IOS, - "android" => OperatingSystem::Android, - _ => OperatingSystem::Unknown, + "windows" => Platform::Windows, + "linux" => Platform::Linux, + "macos" => Platform::MacOS, + "ios" => Platform::IOS, + "android" => Platform::Android, + _ => Platform::Unknown, } } } - -pub fn get_operating_system() -> OperatingSystem { - cfg_if::cfg_if! { - if #[cfg(target_os = "android")] { - OperatingSystem::Android - } else if #[cfg(target_os = "ios")] { - OperatingSystem::IOS - } else if #[cfg(target_os = "macos")] { - OperatingSystem::MacOS - } else if #[cfg(target_os = "windows")] { - OperatingSystem::Windows - } else if #[cfg(target_os = "linux")] { - OperatingSystem::Linux - } else { - OperatingSystem::Unknown - } - } -} - -#[macro_export] -macro_rules! sync_trace { - ($($arg:tt)*) => { - tracing::info!(target: "sync_trace_log", $($arg)*); - }; -} diff --git a/frontend/rust-lib/lib-infra/tests/task_test/script.rs b/frontend/rust-lib/lib-infra/tests/task_test/script.rs index 28e64d85bf..c419d981f2 100644 --- a/frontend/rust-lib/lib-infra/tests/task_test/script.rs +++ b/frontend/rust-lib/lib-infra/tests/task_test/script.rs @@ -2,7 +2,7 @@ use anyhow::Error; use futures::stream::FuturesUnordered; use futures::StreamExt; use lib_infra::async_trait::async_trait; - +use lib_infra::future::BoxResultFuture; use lib_infra::priority_task::{ Task, TaskContent, TaskDispatcher, TaskHandler, TaskId, TaskResult, TaskRunner, TaskState, }; @@ -132,25 +132,23 @@ impl RefCountValue for MockTextTaskHandler { async fn did_remove(&self) {} } -#[async_trait] impl TaskHandler for MockTextTaskHandler { fn handler_id(&self) -> &str { "1" } - async fn run(&self, content: TaskContent) -> Result<(), Error> { - let millisecond = { - let mut rng = rand::thread_rng(); - rng.gen_range(1..50) - }; - - match content { - TaskContent::Text(_s) => { - tokio::time::sleep(Duration::from_millis(millisecond)).await; - Ok(()) - }, - TaskContent::Blob(_) => panic!("Only support text"), - } + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error> { + let mut rng = rand::thread_rng(); + let millisecond = rng.gen_range(1..50); + Box::pin(async move { + match content { + TaskContent::Text(_s) => { + tokio::time::sleep(Duration::from_millis(millisecond)).await; + }, + TaskContent::Blob(_) => panic!("Only support text"), + } + Ok(()) + }) } } @@ -172,40 +170,42 @@ impl RefCountValue for MockBlobTaskHandler { async fn did_remove(&self) {} } -#[async_trait] impl TaskHandler for MockBlobTaskHandler { fn handler_id(&self) -> &str { "2" } - async fn run(&self, content: TaskContent) -> Result<(), Error> { - match content { - TaskContent::Text(_) => panic!("Only support blob"), - TaskContent::Blob(bytes) => { - let _msg = String::from_utf8(bytes).unwrap(); - tokio::time::sleep(Duration::from_millis(20)).await; - }, - } - Ok(()) + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error> { + Box::pin(async move { + match content { + TaskContent::Text(_) => panic!("Only support blob"), + TaskContent::Blob(bytes) => { + let _msg = String::from_utf8(bytes).unwrap(); + tokio::time::sleep(Duration::from_millis(20)).await; + }, + } + Ok(()) + }) } } pub struct MockTimeoutTaskHandler(); -#[async_trait] impl TaskHandler for MockTimeoutTaskHandler { fn handler_id(&self) -> &str { "3" } - async fn run(&self, content: TaskContent) -> Result<(), Error> { - match content { - TaskContent::Text(_) => panic!("Only support blob"), - TaskContent::Blob(_bytes) => { - tokio::time::sleep(Duration::from_millis(2000)).await; - }, - } - Ok(()) + fn run(&self, content: TaskContent) -> BoxResultFuture<(), Error> { + Box::pin(async move { + match content { + TaskContent::Text(_) => panic!("Only support blob"), + TaskContent::Blob(_bytes) => { + tokio::time::sleep(Duration::from_millis(2000)).await; + }, + } + Ok(()) + }) } } diff --git a/frontend/rust-lib/lib-log/Cargo.toml b/frontend/rust-lib/lib-log/Cargo.toml index 4316192f71..1b8ebcb2bc 100644 --- a/frontend/rust-lib/lib-log/Cargo.toml +++ b/frontend/rust-lib/lib-log/Cargo.toml @@ -6,8 +6,8 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tracing-subscriber = { version = "0.3.19", features = ["registry", "env-filter", "ansi", "json"] } -tracing-bunyan-formatter = "0.3.10" +tracing-subscriber = { version = "0.3.17", features = ["registry", "env-filter", "ansi", "json"] } +tracing-bunyan-formatter = "0.3.9" tracing-appender = "0.2.3" tracing-core = "0.1" tracing.workspace = true diff --git a/frontend/rust-lib/lib-log/src/layer.rs b/frontend/rust-lib/lib-log/src/layer.rs index 945db52a8b..28bd54b01f 100644 --- a/frontend/rust-lib/lib-log/src/layer.rs +++ b/frontend/rust-lib/lib-log/src/layer.rs @@ -7,7 +7,6 @@ use tracing::{Event, Id, Subscriber}; use tracing_bunyan_formatter::JsonStorage; use tracing_core::metadata::Level; use tracing_core::span::Attributes; -use tracing_core::Metadata; use tracing_subscriber::{fmt::MakeWriter, layer::Context, registry::SpanRef, Layer}; const LEVEL: &str = "level"; @@ -23,8 +22,6 @@ const IGNORE_FIELDS: [&str; 2] = [LOG_MODULE_PATH, LOG_TARGET_PATH]; pub struct FlowyFormattingLayer<'a, W: MakeWriter<'static> + 'static> { make_writer: W, with_target: bool, - #[allow(clippy::type_complexity)] - target_filter: Option<Box<dyn Fn(&str) -> bool + Send + Sync>>, phantom: std::marker::PhantomData<&'a ()>, } @@ -37,26 +34,10 @@ where Self { make_writer, with_target: true, - target_filter: None, phantom: std::marker::PhantomData, } } - pub fn with_target_filter<F>(mut self, filter: F) -> Self - where - F: Fn(&str) -> bool + Send + Sync + 'static, - { - self.target_filter = Some(Box::new(filter)); - self - } - - fn should_log(&self, metadata: &Metadata<'_>) -> bool { - self - .target_filter - .as_ref() - .map_or(true, |f| f(metadata.target())) - } - fn serialize_fields( &self, map_serializer: &mut impl SerializeMap<Error = serde_json::Error>, @@ -64,6 +45,7 @@ where _level: &Level, ) -> Result<(), std::io::Error> { map_serializer.serialize_entry(MESSAGE, &message)?; + // map_serializer.serialize_entry(LEVEL, &format!("{}", level))?; map_serializer.serialize_entry(TIME, &Local::now().format("%m-%d %H:%M:%S").to_string())?; Ok(()) } @@ -178,10 +160,6 @@ where W: for<'writer> MakeWriter<'writer> + 'static, { fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) { - if !self.should_log(event.metadata()) { - return; - } - // Events do not necessarily happen in the context of a span, hence // lookup_current returns an `Option<SpanRef<_>>` instead of a // `SpanRef<_>`. @@ -199,9 +177,14 @@ where let message = format_event_message(¤t_span, event, &event_visitor, &ctx); self.serialize_fields(&mut map_serializer, &message, event.metadata().level())?; + // Additional metadata useful for debugging + // They should be nested under `src` (see https://github.com/trentm/node-bunyan#src ) + // but `tracing` does not support nested values yet + if self.with_target { map_serializer.serialize_entry("target", event.metadata().target())?; } + // map_serializer.serialize_entry("line", &event.metadata().line())?; // map_serializer.serialize_entry("file", &event.metadata().file())?; @@ -241,9 +224,6 @@ where fn on_new_span(&self, _attrs: &Attributes, id: &Id, ctx: Context<'_, S>) { let span = ctx.span(id).expect("Span not found, this is a bug"); - if !self.should_log(span.metadata()) { - return; - } if let Ok(serialized) = self.serialize_span(&span, Type::EnterSpan, &ctx) { let _ = self.emit(serialized); } @@ -251,9 +231,6 @@ where fn on_close(&self, id: Id, ctx: Context<'_, S>) { let span = ctx.span(&id).expect("Span not found, this is a bug"); - if !self.should_log(span.metadata()) { - return; - } if let Ok(serialized) = self.serialize_span(&span, Type::ExitSpan, &ctx) { let _ = self.emit(serialized); } diff --git a/frontend/rust-lib/lib-log/src/lib.rs b/frontend/rust-lib/lib-log/src/lib.rs index 07145b27ff..a328be254c 100644 --- a/frontend/rust-lib/lib-log/src/lib.rs +++ b/frontend/rust-lib/lib-log/src/lib.rs @@ -2,11 +2,9 @@ use std::io; use std::io::Write; use std::sync::{Arc, RwLock}; -use crate::layer::FlowyFormattingLayer; -use crate::stream_log::{StreamLog, StreamLogSender}; use chrono::Local; use lazy_static::lazy_static; -use lib_infra::util::OperatingSystem; +use lib_infra::util::Platform; use tracing::subscriber::set_global_default; use tracing_appender::rolling::Rotation; use tracing_appender::{non_blocking::WorkerGuard, rolling::RollingFileAppender}; @@ -15,77 +13,58 @@ use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::fmt::MakeWriter; use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; +use crate::layer::FlowyFormattingLayer; +use crate::stream_log::{StreamLog, StreamLogSender}; + mod layer; pub mod stream_log; lazy_static! { - static ref APP_LOG_GUARD: RwLock<Option<WorkerGuard>> = RwLock::new(None); - static ref COLLAB_SYNC_LOG_GUARD: RwLock<Option<WorkerGuard>> = RwLock::new(None); + static ref LOG_GUARD: RwLock<Option<WorkerGuard>> = RwLock::new(None); } pub struct Builder { #[allow(dead_code)] name: String, env_filter: String, - app_log_appender: RollingFileAppender, - sync_log_appender: RollingFileAppender, + file_appender: RollingFileAppender, #[allow(dead_code)] - platform: OperatingSystem, + platform: Platform, stream_log_sender: Option<Arc<dyn StreamLogSender>>, } -const SYNC_TARGET: &str = "sync_trace_log"; impl Builder { pub fn new( name: &str, directory: &str, - platform: &OperatingSystem, + platform: &Platform, stream_log_sender: Option<Arc<dyn StreamLogSender>>, ) -> Self { - let app_log_appender = RollingFileAppender::builder() + let file_appender = RollingFileAppender::builder() .rotation(Rotation::DAILY) .filename_prefix(name) .max_log_files(6) .build(directory) .unwrap_or(tracing_appender::rolling::daily(directory, name)); - let sync_log_name = "log.sync"; - let sync_log_appender = RollingFileAppender::builder() - .rotation(Rotation::HOURLY) - .filename_prefix(sync_log_name) - .max_log_files(24) - .build(directory) - .unwrap_or(tracing_appender::rolling::hourly(directory, sync_log_name)); - Builder { name: name.to_owned(), env_filter: "info".to_owned(), - app_log_appender, - sync_log_appender, + file_appender, platform: platform.clone(), stream_log_sender, } } pub fn env_filter(mut self, env_filter: &str) -> Self { - env_filter.clone_into(&mut self.env_filter); + self.env_filter = env_filter.to_owned(); self } pub fn build(self) -> Result<(), String> { let env_filter = EnvFilter::new(self.env_filter); - let (appflowy_log_non_blocking, app_log_guard) = - tracing_appender::non_blocking(self.app_log_appender); - *APP_LOG_GUARD.write().unwrap() = Some(app_log_guard); - let app_file_layer = FlowyFormattingLayer::new(appflowy_log_non_blocking) - .with_target_filter(|target| target != SYNC_TARGET); - - let (sync_log_non_blocking, sync_log_guard) = - tracing_appender::non_blocking(self.sync_log_appender); - *COLLAB_SYNC_LOG_GUARD.write().unwrap() = Some(sync_log_guard); - - let collab_sync_file_layer = FlowyFormattingLayer::new(sync_log_non_blocking) - .with_target_filter(|target| target == SYNC_TARGET); + let (non_blocking, guard) = tracing_appender::non_blocking(self.file_appender); + let file_layer = FlowyFormattingLayer::new(non_blocking); if let Some(stream_log_sender) = &self.stream_log_sender { let subscriber = tracing_subscriber::fmt() @@ -100,8 +79,7 @@ impl Builder { .with_env_filter(env_filter) .finish() .with(JsonStorageLayer) - .with(app_file_layer) - .with(collab_sync_file_layer); + .with(file_layer); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; } else { let subscriber = tracing_subscriber::fmt() @@ -114,11 +92,11 @@ impl Builder { .finish() .with(FlowyFormattingLayer::new(DebugStdoutWriter)) .with(JsonStorageLayer) - .with(app_file_layer) - .with(collab_sync_file_layer); + .with(file_layer); set_global_default(subscriber).map_err(|e| format!("{:?}", e))?; }; + *LOG_GUARD.write().unwrap() = Some(guard); Ok(()) } } diff --git a/frontend/rust-lib/rust-toolchain.toml b/frontend/rust-lib/rust-toolchain.toml index 1de01fa45c..6f14058b2e 100644 --- a/frontend/rust-lib/rust-toolchain.toml +++ b/frontend/rust-lib/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.81.0" +channel = "1.77.2" diff --git a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh index bff26c5b4b..30538def96 100755 --- a/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh +++ b/frontend/scripts/code_generation/flowy_icons/generate_flowy_icons.sh @@ -1,40 +1,6 @@ -#!/usr/bin/env bash +#!/bin/bash -# check the cost time -start_time=$(date +%s) - -# read the arguments to skip the pub get and package get -skip_pub_get=false -skip_pub_packages_get=false -verbose=false -include_packages=false - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --skip-pub-get) - skip_pub_get=true - shift - ;; - --skip-pub-packages-get) - skip_pub_packages_get=true - shift - ;; - --verbose) - verbose=true - shift - ;; - --exclude-packages) - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -echo "📷 Start generating image/svg files" +echo "Generating flowy icon files" # Store the current working directory original_dir=$(pwd) @@ -48,34 +14,13 @@ rm -rf assets/flowy_icons/ mkdir -p assets/flowy_icons/ rsync -r ../resources/flowy_icons/ assets/flowy_icons/ -if [ "$skip_pub_get" = false ]; then - if [ "$verbose" = true ]; then - flutter pub get - else - flutter pub get >/dev/null 2>&1 - fi -fi +flutter pub get +flutter packages pub get -if [ "$include_packages" = true ]; then - if [ "$verbose" = true ]; then - flutter packages pub get - else - flutter packages pub get >/dev/null 2>&1 - fi -fi +echo "Generating FlowySvg classes" +dart run flowy_svg -if [ "$verbose" = true ]; then - dart run flowy_svg -else - dart run flowy_svg >/dev/null 2>&1 -fi +echo "Done generating icon files." # Return to the original directory cd "$original_dir" - -echo "📷 Done generating image/svg files." - -# echo the cost time -end_time=$(date +%s) -cost_time=$((end_time - start_time)) -echo "📷 Image/svg files generation cost $cost_time seconds." diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.cmd b/frontend/scripts/code_generation/freezed/generate_freezed.cmd index 798c3bc1dc..f543e08fd5 100644 --- a/frontend/scripts/code_generation/freezed/generate_freezed.cmd +++ b/frontend/scripts/code_generation/freezed/generate_freezed.cmd @@ -26,8 +26,6 @@ for /D %%d in (*) do ( if exist "pubspec.yaml" ( echo Generating freezed files in %%d... echo Please wait while we clean the project and fetch the dependencies. - call flutter packages pub get - call flutter pub get call dart run build_runner clean && call dart run build_runner build -d echo Done running build command in %%d ) else ( diff --git a/frontend/scripts/code_generation/freezed/generate_freezed.sh b/frontend/scripts/code_generation/freezed/generate_freezed.sh index 216a01b232..24c90650d2 100755 --- a/frontend/scripts/code_generation/freezed/generate_freezed.sh +++ b/frontend/scripts/code_generation/freezed/generate_freezed.sh @@ -1,44 +1,4 @@ -#!/usr/bin/env bash - -# check the cost time -start_time=$(date +%s) - -# read the arguments to skip the pub get and package get -skip_pub_get=false -skip_pub_packages_get=false -verbose=false -exclude_packages=false -show_loading=false - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --skip-pub-get) - skip_pub_get=true - shift - ;; - --skip-pub-packages-get) - skip_pub_packages_get=true - shift - ;; - --verbose) - verbose=true - shift - ;; - --exclude-packages) - exclude_packages=true - shift - ;; - --show-loading) - show_loading=true - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done +#!/bin/bash # Store the current working directory original_dir=$(pwd) @@ -48,93 +8,34 @@ cd "$(dirname "$0")" # Navigate to the project root cd ../../../appflowy_flutter -if [ "$exclude_packages" = false ]; then - # Navigate to the packages directory - cd packages - for d in */; do - # Navigate into the subdirectory - cd "$d" - - # Check if the pubspec.yaml file exists and contains the freezed dependency - if [ -f "pubspec.yaml" ] && grep -q "build_runner" pubspec.yaml; then - echo "🧊 Start generating freezed files ($d)." - if [ "$skip_pub_packages_get" = false ]; then - if [ "$verbose" = true ]; then - flutter packages pub get - else - flutter packages pub get >/dev/null 2>&1 - fi - fi - if [ "$verbose" = true ]; then - dart run build_runner build --delete-conflicting-outputs - else - dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 - fi - echo "🧊 Done generating freezed files ($d)." - fi - - # Navigate back to the packages directory - cd .. - done - - cd .. -fi - -# Function to display animated loading text -display_loading() { - local pid=$1 - local delay=0.5 - local spinstr='|/-\' - while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do - local temp=${spinstr#?} - printf " [%c] Generating freezed files..." "$spinstr" - local spinstr=$temp${spinstr%"$temp"} - sleep $delay - printf "\r" - done - printf " \r" -} - # Navigate to the appflowy_flutter directory and generate files -echo "🧊 Start generating freezed files (AppFlowy)." +echo "Generating files for appflowy_flutter" -if [ "$skip_pub_packages_get" = false ]; then - if [ "$verbose" = true ]; then - flutter packages pub get - else +flutter packages pub get >/dev/null 2>&1 + +dart run build_runner build -d +echo "Done generating files for appflowy_flutter" + +echo "Generating files for packages" +cd packages +for d in */; do + # Navigate into the subdirectory + cd "$d" + + # Check if the subdirectory contains a pubspec.yaml file + if [ -f "pubspec.yaml" ]; then + echo "Generating freezed files in $d..." + echo "Please wait while we clean the project and fetch the dependencies." flutter packages pub get >/dev/null 2>&1 + dart run build_runner build -d + echo "Done running build command in $d" + else + echo "No pubspec.yaml found in $d, it can\'t be a Dart project. Skipping." fi -fi -# Start the build_runner in the background -if [ "$verbose" = true ]; then - dart run build_runner build --delete-conflicting-outputs & -else - dart run build_runner build --delete-conflicting-outputs >/dev/null 2>&1 & -fi - -# Get the PID of the background process -build_pid=$! - -if [ "$show_loading" = true ]; then - # Start the loading animation - display_loading $build_pid & - - # Get the PID of the loading animation - loading_pid=$! -fi - -# Wait for the build_runner to finish -wait $build_pid - -# Clear the line -printf "\r%*s\r" $(($(tput cols))) "" + # Navigate back to the packages directory + cd .. +done +# Return to the original directory cd "$original_dir" - -echo "🧊 Done generating freezed files." - -# echo the cost time -end_time=$(date +%s) -cost_time=$((end_time - start_time)) -echo "🧊 Freezed files generation cost $cost_time seconds." diff --git a/frontend/scripts/code_generation/generate.sh b/frontend/scripts/code_generation/generate.sh index fd2edab785..f71ceba2df 100755 --- a/frontend/scripts/code_generation/generate.sh +++ b/frontend/scripts/code_generation/generate.sh @@ -1,39 +1,4 @@ -#!/usr/bin/env bash - -args=("$@") - -# check the cost time -start_time=$(date +%s) - -# read the arguments to skip the pub get and package get -skip_pub_get=false -skip_pub_packages_get=false -verbose=false - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --skip-pub-get) - skip_pub_get=true - shift - ;; - --skip-pub-packages-get) - skip_pub_packages_get=true - shift - ;; - --verbose) - verbose=true - shift - ;; - --exclude-packages) - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done +#!/bin/bash # Store the current working directory original_dir=$(pwd) @@ -42,34 +7,30 @@ original_dir=$(pwd) cd "$(dirname "$0")" # Call the script in the 'language_files' folder +echo "Generating files using easy_localization" cd language_files # Allow execution permissions on CI chmod +x ./generate_language_files.sh -# Pass the arguments to the script -./generate_language_files.sh "${args[@]}" - -# Return to the main script directory -cd .. - -# Call the script in the 'flowy_icons' folder -cd flowy_icons -# Allow execution permissions on CI -chmod +x ./generate_flowy_icons.sh -./generate_flowy_icons.sh "${args[@]}" +./generate_language_files.sh "$@" # Return to the main script directory cd .. # Call the script in the 'freezed' folder +echo "Generating files using build_runner" cd freezed # Allow execution permissions on CI chmod +x ./generate_freezed.sh -./generate_freezed.sh "${args[@]}" --show-loading --verbose +./generate_freezed.sh "$@" + +# Return to the main script directory +cd .. + +echo "Generating svg files using flowy_svg" +cd flowy_icons +# Allow execution permissions on CI +chmod +x ./generate_flowy_icons.sh +./generate_flowy_icons.sh "$@" # Return to the original directory cd "$original_dir" - -# echo the cost time -end_time=$(date +%s) -cost_time=$((end_time - start_time)) -echo "✅ Code generation cost $cost_time seconds." diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.cmd b/frontend/scripts/code_generation/language_files/generate_language_files.cmd index 9536e4359c..7b14503810 100644 --- a/frontend/scripts/code_generation/language_files/generate_language_files.cmd +++ b/frontend/scripts/code_generation/language_files/generate_language_files.cmd @@ -16,8 +16,6 @@ echo Copying resources/translations to appflowy_flutter/assets/translations xcopy /E /Y /I ..\resources\translations assets\translations REM call flutter packages pub get -call flutter pub get -call flutter packages pub get echo Specifying source directory for AppFlowy Localizations. call dart run easy_localization:generate -S assets/translations/ diff --git a/frontend/scripts/code_generation/language_files/generate_language_files.sh b/frontend/scripts/code_generation/language_files/generate_language_files.sh index 5e51b5cdba..8aa403d1f2 100755 --- a/frontend/scripts/code_generation/language_files/generate_language_files.sh +++ b/frontend/scripts/code_generation/language_files/generate_language_files.sh @@ -1,41 +1,6 @@ -#!/usr/bin/env bash +#!/bin/bash -set -e - -# check the cost time -start_time=$(date +%s) - -# read the arguments to skip the pub get and package get -skip_pub_get=false -skip_pub_packages_get=false -verbose=false - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case "$1" in - --skip-pub-get) - skip_pub_get=true - shift - ;; - --skip-pub-packages-get) - skip_pub_packages_get=true - shift - ;; - --verbose) - verbose=true - shift - ;; - --exclude-packages) - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac -done - -echo "🌍 Start generating language files." +echo "Generating language files" # Store the current working directory original_dir=$(pwd) @@ -54,35 +19,16 @@ cp -f ../resources/translations/*.json assets/translations/ # the ci alwayas return a 'null check operator used on a null value' error. # so we force to exec the below command to avoid the error. # https://github.com/dart-lang/pub/issues/3314 -if [ "$skip_pub_get" = false ]; then - if [ "$verbose" = true ]; then - flutter pub get - else - flutter pub get >/dev/null 2>&1 - fi -fi -if [ "$skip_pub_packages_get" = false ]; then - if [ "$verbose" = true ]; then - flutter packages pub get - else - flutter packages pub get >/dev/null 2>&1 - fi -fi +flutter pub get +flutter packages pub get -if [ "$verbose" = true ]; then - dart run easy_localization:generate -S assets/translations/ - dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json -else - dart run easy_localization:generate -S assets/translations/ >/dev/null 2>&1 - dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json >/dev/null 2>&1 -fi +echo "Specifying source directory for AppFlowy Localizations." +dart run easy_localization:generate -S assets/translations/ -echo "🌍 Done generating language files." +echo "Generating language files for AppFlowy." +dart run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations/ -s en.json + +echo "Done generating language files." # Return to the original directory cd "$original_dir" - -# echo the cost time -end_time=$(date +%s) -cost_time=$((end_time - start_time)) -echo "🌍 Language files generation cost $cost_time seconds." diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 9e422f80ca..8ebe5fc8ef 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -17,31 +17,40 @@ ENV PATH="/home/$user/.pub-cache/bin:/home/$user/flutter/bin:/home/$user/flutter USER $user WORKDIR /home/$user -# Install Rust and dependencies using pacman -RUN sudo pacman -S --needed --noconfirm curl base-devel openssl clang cmake ninja pkg-config xdg-user-dirs +# Install yay +RUN sudo pacman -S --needed --noconfirm curl tar +RUN curl -sSfL \ + --output yay.tar.gz \ + https://github.com/Jguer/yay/releases/download/v12.3.3/yay_12.3.3_x86_64.tar.gz && \ + tar -xf yay.tar.gz && \ + sudo mv yay_12.3.3_x86_64/yay /bin && \ + rm -rf yay_12.3.3_x86_64 && \ + yay --version + +# Install Rust +RUN yay -S --noconfirm curl base-devel openssl clang cmake ninja pkg-config xdg-user-dirs RUN xdg-user-dirs-update RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y RUN source ~/.cargo/env && \ - rustup toolchain install 1.81 && \ - rustup default 1.81 + rustup toolchain install 1.75 && \ + rustup default 1.75 # Install Flutter RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ - --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.27.4-stable.tar.xz && \ + --output flutter.tar.xz \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.22.0-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop RUN flutter doctor RUN dart pub global activate protoc_plugin 21.1.2 -# Install build dependencies for AppFlowy using pacman -RUN sudo pacman -S --needed --noconfirm jemalloc git libkeybinder3 sqlite clang rsync libnotify rocksdb zstd mpv +# Install build dependencies for AppFlowy +RUN yay -S --noconfirm jemalloc4 cargo-make cargo-binstall +RUN sudo pacman -S --noconfirm git libkeybinder3 sqlite clang rsync libnotify rocksdb zstd mpv RUN sudo ln -s /usr/bin/sha1sum /usr/bin/shasum -RUN source ~/.cargo/env && cargo install cargo-make --version 0.37.18 --locked -RUN source ~/.cargo/env && cargo install cargo-binstall --version 1.10.17 --locked -RUN source ~/.cargo/env && cargo binstall duckscript_cli --locked -y +RUN source ~/.cargo/env && cargo binstall duckscript_cli -y # Build AppFlowy COPY . /appflowy @@ -64,7 +73,7 @@ FROM archlinux/archlinux RUN pacman -Syyu --noconfirm # Install runtime dependencies -RUN pacman -S --noconfirm xdg-user-dirs gtk3 libkeybinder3 libnotify rocksdb && \ +RUN pacman -S --noconfirm xdg-user-dirs gtk3 libkeybinder3 && \ pacman -Scc --noconfirm # Set up appflowy user diff --git a/frontend/scripts/flatpack-buildfiles/launcher.sh b/frontend/scripts/flatpack-buildfiles/launcher.sh index 24b4fdbea4..c7e7b9ee4a 100644 --- a/frontend/scripts/flatpack-buildfiles/launcher.sh +++ b/frontend/scripts/flatpack-buildfiles/launcher.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash gdbus call --session --dest io.appflowy.AppFlowy \ --object-path /io/appflowy/AppFlowy/Object \ --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 1c31696f39..653eb8f1b3 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash YELLOW="\e[93m" GREEN="\e[32m" @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.27.4 -if [ "$FLUTTER_VERSION" = "3.27.4" ]; then - echo "Flutter version is already 3.27.4" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.27.4 of Flutter - git checkout 3.27.4 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.27.4" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop @@ -85,15 +85,11 @@ cd frontend || exit 1 # Install cargo make printMessage "Installing cargo-make." -cargo install --force --locked cargo-make +cargo install --force cargo-make # Install duckscript printMessage "Installing duckscript." -cargo install --force --locked duckscript_cli - -# Install cargo-lipo -printMessage "Installing cargo-lipo." -cargo install --force --locked cargo-lipo +cargo install --force duckscript_cli # Check prerequisites printMessage "Checking prerequisites." diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index 57db01a73d..b02b31d62c 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash YELLOW="\e[93m" GREEN="\e[32m" @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.27.4 -if [ "$FLUTTER_VERSION" = "3.27.4" ]; then - echo "Flutter version is already 3.27.4" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.27.4 of Flutter - git checkout 3.27.4 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.27.4" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop @@ -111,7 +111,7 @@ cargo install --force cargo-make # Install duckscript printMessage "Installing duckscript." -cargo install --force --locked duckscript_cli +cargo install --force duckscript_cli # Check prerequisites printMessage "Checking prerequisites." diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 463071e2af..8613b904c6 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash YELLOW="\e[93m" GREEN="\e[32m" @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.27.4 -if [ "$FLUTTER_VERSION" = "3.27.4" ]; then - echo "Flutter version is already 3.27.4" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.27.4 of Flutter - git checkout 3.27.4 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.27.4" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop @@ -86,8 +86,8 @@ cargo install --force cargo-make # Install duckscript printMessage "Installing duckscript." -cargo install --force --locked duckscript_cli +cargo install --force duckscript_cli # Check prerequisites printMessage "Checking prerequisites." -cargo make appflowy-flutter-deps-tools +cargo make appflowy-flutter-deps-tools \ No newline at end of file diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index 3cceec5bb0..1d68a677ae 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash YELLOW="\e[93m" GREEN="\e[32m" @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.27.4 -if [ "$FLUTTER_VERSION" = "3.27.4" ]; then - echo "Flutter version is already 3.27.4" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.27.4 of Flutter - git checkout 3.27.4 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.27.4" + echo "Switched to Flutter version 3.22.0" fi # Add pub cache and cargo to PATH @@ -100,7 +100,7 @@ $USERPROFILE/.cargo/bin/cargo install --force cargo-make # Install duckscript printMessage "Installing duckscript." -$USERPROFILE/.cargo/bin/cargo install --force --locked duckscript_cli +$USERPROFILE/.cargo/bin/cargo install --force duckscript_cli # Enable vcpkg integration # Note: Requires admin diff --git a/frontend/scripts/linux_distribution/appimage/build_appimage.sh b/frontend/scripts/linux_distribution/appimage/build_appimage.sh index 73deb45edd..a7e4d1b11b 100644 --- a/frontend/scripts/linux_distribution/appimage/build_appimage.sh +++ b/frontend/scripts/linux_distribution/appimage/build_appimage.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash VERSION=$1 diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst index bf2f79fa97..56186649d4 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postinst +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postinst @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash if [ -e /usr/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else diff --git a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm index 59a680e767..f815d1bb5c 100755 --- a/frontend/scripts/linux_distribution/deb/DEBIAN/postrm +++ b/frontend/scripts/linux_distribution/deb/DEBIAN/postrm @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash if [ -e /usr/bin/AppFlowy ]; then rm /usr/bin/AppFlowy rm /usr/bin/AppFlowyLauncher.sh diff --git a/frontend/scripts/linux_distribution/deb/build_deb.sh b/frontend/scripts/linux_distribution/deb/build_deb.sh index 42fbf7346d..35fe9dbbaf 100644 --- a/frontend/scripts/linux_distribution/deb/build_deb.sh +++ b/frontend/scripts/linux_distribution/deb/build_deb.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash LINUX_RELEASE_PRODUCTION=$1 VERSION=$2 diff --git a/frontend/scripts/linux_distribution/packaging/launcher.sh b/frontend/scripts/linux_distribution/packaging/launcher.sh index 24b4fdbea4..c7e7b9ee4a 100644 --- a/frontend/scripts/linux_distribution/packaging/launcher.sh +++ b/frontend/scripts/linux_distribution/packaging/launcher.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash gdbus call --session --dest io.appflowy.AppFlowy \ --object-path /io/appflowy/AppFlowy/Object \ --method io.appflowy.AppFlowy.Open "['$1']" {} diff --git a/frontend/scripts/linux_installer/postinst b/frontend/scripts/linux_installer/postinst index 83e1a1043e..4f495f86a2 100644 --- a/frontend/scripts/linux_installer/postinst +++ b/frontend/scripts/linux_installer/postinst @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash if [ -e /usr/local/bin/AppFlowy ]; then echo "Symlink already exists, skipping." else diff --git a/frontend/scripts/linux_installer/postrm b/frontend/scripts/linux_installer/postrm index 7927bc56e5..53304b1b48 100644 --- a/frontend/scripts/linux_installer/postrm +++ b/frontend/scripts/linux_installer/postrm @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash if [ -e /usr/local/bin/appflowy ]; then rm /usr/local/bin/appflowy -fi +fi \ No newline at end of file diff --git a/frontend/scripts/makefile/desktop.toml b/frontend/scripts/makefile/desktop.toml index f972aa5cd4..f1f0aa0219 100644 --- a/frontend/scripts/makefile/desktop.toml +++ b/frontend/scripts/makefile/desktop.toml @@ -1,3 +1,4 @@ + [tasks.env_check] dependencies = ["echo_env", "install_flutter_protobuf"] condition = { env_set = [ @@ -99,7 +100,7 @@ dependencies = ["set-app-version"] script = [ """ cd rust-lib/ - cargo build --profile ${CARGO_PROFILE} --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" + cargo build --profile ${CARGO_PROFILE} --${BUILD_FLAG} --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" cd ../ """, ] @@ -110,7 +111,7 @@ dependencies = ["set-app-version"] script = [ """ cd rust-lib/ - cargo build --profile ${CARGO_PROFILE} --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" + cargo build --profile ${CARGO_PROFILE} --${BUILD_FLAG} --package=dart-ffi --target ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" cd ../ """, ] diff --git a/frontend/scripts/makefile/env.toml b/frontend/scripts/makefile/env.toml index 96a632f8ab..30de7e138b 100644 --- a/frontend/scripts/makefile/env.toml +++ b/frontend/scripts/makefile/env.toml @@ -1,11 +1,17 @@ [tasks.appflowy-flutter-deps-tools] run_task = { name = ["install_flutter_prerequests"] } +[tasks.appflowy-tauri-deps-tools] +run_task = { name = ["install_tauri_prerequests"] } + [tasks.appflowy-flutter-dev-tools] -run_task = { name = ["appflowy-flutter-deps-tools", "install_diesel"] } +run_task = { name = ["appflowy-flutter-deps-tools","install_diesel"] } + +[tasks.appflowy-tauri-dev-tools] +run_task = { name = ["appflowy-tauri-deps-tools","install_diesel"] } [tasks.install_windows_deps.windows] -dependencies = ["check_duckscript_installation", "check_vcpkg", "install_vcpkg_sqlite", "install_rust_vcpkg_cli"] +dependencies=["check_duckscript_installation", "check_vcpkg", "install_vcpkg_sqlite", "install_rust_vcpkg_cli"] [tasks.check_visual_studio_installation.windows] script = """ @@ -31,7 +37,7 @@ script = """ @echo off @duck -h > nul if %errorlevel% GTR 0 ( - echo Please install duckscript at first: cargo install --force --locked duckscript_cli + echo Please install duckscript at first: cargo install --force duckscript_cli exit -1 ) """ @@ -94,11 +100,14 @@ script = """ rustup target add x86_64-unknown-linux-gnu """ +[tasks.install_tauri_prerequests] +dependencies=["install_targets", "install_web_protobuf"] + [tasks.install_flutter_prerequests] -dependencies = ["install_targets", "install_flutter_protobuf"] +dependencies=["install_targets", "install_flutter_protobuf"] [tasks.install_flutter_prerequests.windows] -dependencies = ["install_targets", "install_windows_deps"] +dependencies=["install_targets", "install_windows_deps"] [tasks.install_tools] script = """ @@ -139,7 +148,7 @@ script_runner = "@duckscript" [tasks.enable_git_hook] -dependencies = ["download_gitlint"] +dependencies=["download_gitlint"] script = """ git config core.hooksPath .githooks """ diff --git a/frontend/scripts/makefile/flutter.toml b/frontend/scripts/makefile/flutter.toml index e62acac3f6..4203ce678d 100644 --- a/frontend/scripts/makefile/flutter.toml +++ b/frontend/scripts/makefile/flutter.toml @@ -301,7 +301,7 @@ script = [""" [tasks.code_generation] script_runner = "@shell" script = [""" - ./scripts/code_generation/generate.sh + sh scripts/code_generation/generate.sh """] [tasks.code_generation.windows] diff --git a/frontend/scripts/makefile/mobile.toml b/frontend/scripts/makefile/mobile.toml index 56329a2936..8e89e4c2ed 100644 --- a/frontend/scripts/makefile/mobile.toml +++ b/frontend/scripts/makefile/mobile.toml @@ -26,6 +26,7 @@ private = true script = [ """ cd rust-lib/ + rustup show if [ "${BUILD_FLAG}" = "debug" ]; then echo "🚀 🚀 🚀 Building iOS SDK for debug" cargo lipo --targets ${RUST_COMPILE_TARGET} --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi @@ -67,10 +68,10 @@ script = [ cd rust-lib/ if [ "${BUILD_FLAG}" = "debug" ]; then echo "🚀 🚀 🚀 Building Android SDK for debug" - cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi + cargo ndk -t arm64-v8a -t armeabi-v7a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi else echo "🚀 🚀 🚀 Building Android SDK for release" - cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi --release + cargo ndk -t arm64-v8a -t armeabi-v7a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi --release fi cd ../ """, @@ -98,6 +99,9 @@ script = [ dart_ffi_dir= set ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/appflowy_flutter/packages/appflowy_backend/${TARGET_OS} lib = set lib${LIB_NAME}.${LIB_EXT} + ls -a ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG} + + echo "💻 💻 💻 Copying ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} to ${dart_ffi_dir}/${lib}" rm -f ${dart_ffi_dir}/${lib} cp ${CARGO_MAKE_WORKSPACE_WORKING_DIRECTORY}/rust-lib/target/${RUST_COMPILE_TARGET}/${BUILD_FLAG}/${lib} \ diff --git a/frontend/scripts/makefile/tauri.toml b/frontend/scripts/makefile/tauri.toml index 549c7e250e..4fe6b19fd7 100644 --- a/frontend/scripts/makefile/tauri.toml +++ b/frontend/scripts/makefile/tauri.toml @@ -1,3 +1,10 @@ +[tasks.tauri_build] +description = "Build the Tauri backend" +script = [""" + cd appflowy_tauri/src-tauri + cargo build + """] +script_runner = "@shell" [tasks.tauri_dev] env = { RUST_LOG = "debug" } diff --git a/frontend/scripts/makefile/web.toml b/frontend/scripts/makefile/web.toml index 52c09ea6bb..40162c9e6c 100644 --- a/frontend/scripts/makefile/web.toml +++ b/frontend/scripts/makefile/web.toml @@ -1,33 +1,43 @@ + [tasks.wasm_build] script_runner = "bash" script = [ - """ - #!/usr/bin/env bash - BASE_DIR=$(pwd) - crates=("lib-dispatch" "lib-infra" "flowy-notification" "flowy-date" "flowy-error" "collab-integrate" "flowy-document") + """ + #!/bin/bash + BASE_DIR=$(pwd) + crates=("lib-dispatch" "flowy-encrypt" "lib-infra" "flowy-notification" "flowy-date" "flowy-error" "collab-integrate" "flowy-document") - # Iterate over each crate and build it - for crate in "${crates[@]}"; do - echo "🔥🔥🔥 Building $crate with wasm-pack..." - cd "$BASE_DIR/rust-lib/$crate" || { echo "Failed to enter directory $crate"; exit 1; } + # Iterate over each crate and build it + for crate in "${crates[@]}"; do + echo "🔥🔥🔥 Building $crate with wasm-pack..." + cd "$BASE_DIR/rust-lib/$crate" || { echo "Failed to enter directory $crate"; exit 1; } - wasm-pack build || { echo "Build failed for $crate"; exit 1; } - done - """ + wasm-pack build || { echo "Build failed for $crate"; exit 1; } + done + """ ] [tasks.web_clean] description = "Remove all the building artifacts" run_task = { name = [ - "rust_lib_clean", - "rm_macro_build_cache", - "rm_rust_generated_files", - "rm_web_generated_protobuf_files", - "rm_web_generated_event_files", - "rm_pkg", + "rust_lib_clean", + "rm_macro_build_cache", + "rm_rust_generated_files", + "rm_web_generated_protobuf_files", + "rm_web_generated_event_files", + "rm_pkg", ] } + +[tasks.rm_pkg] +private = true +script = [""" +cd ${WEB_LIB_PATH} +rimraf dist pkg +"""] +script_runner = "@duckscript" + [tasks.rm_web_generated_protobuf_files] private = true script = [""" @@ -56,4 +66,4 @@ script = [""" end end """] -script_runner = "@duckscript" +script_runner = "@duckscript" \ No newline at end of file diff --git a/frontend/scripts/tool/update_client_api_rev.sh b/frontend/scripts/tool/update_client_api_rev.sh index 9146e6e2d8..1af8987922 100755 --- a/frontend/scripts/tool/update_client_api_rev.sh +++ b/frontend/scripts/tool/update_client_api_rev.sh @@ -8,7 +8,7 @@ fi NEW_REV="$1" echo "New revision: $NEW_REV" -directories=("rust-lib") +directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs" "appflowy_web_app/src-tauri") for dir in "${directories[@]}"; do echo "Updating $dir" diff --git a/frontend/scripts/tool/update_collab_rev.sh b/frontend/scripts/tool/update_collab_rev.sh index aa228a0eef..26076df248 100755 --- a/frontend/scripts/tool/update_collab_rev.sh +++ b/frontend/scripts/tool/update_collab_rev.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/bash # Ensure a new revision ID is provided if [ "$#" -ne 1 ]; then @@ -8,7 +8,7 @@ fi NEW_REV="$1" echo "New revision: $NEW_REV" -directories=("rust-lib") +directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs" "appflowy_web_app/src-tauri") for dir in "${directories[@]}"; do echo "Updating $dir" @@ -28,7 +28,7 @@ for dir in "${directories[@]}"; do # Update all the specified crates at once echo "Updating crates: $crates_to_update" - cargo update $crates_to_update 2> /dev/null + cargo update $crates_to_update popd > /dev/null done diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 697d293e31..094e5caf14 100755 --- a/frontend/scripts/tool/update_collab_source.sh +++ b/frontend/scripts/tool/update_collab_source.sh @@ -1,10 +1,13 @@ -#!/usr/bin/env bash +#!/bin/bash # Paths to your Cargo.toml files REPO_PATH="./AppFlowy-Collab" CARGO_TOML_1="./rust-lib/Cargo.toml" REPO_RELATIVE_PATH_1="../AppFlowy-Collab" +CARGO_TOML_2="./appflowy_tauri/src-tauri/Cargo.toml" +REPO_RELATIVE_PATH_2="../../AppFlowy-Collab" + # Function to switch dependencies in a given Cargo.toml switch_deps() { local cargo_toml="$1" @@ -12,7 +15,7 @@ switch_deps() { if grep -q 'git = "https://github.com/AppFlowy-IO/AppFlowy-Collab"' "$cargo_toml"; then cp "$cargo_toml" "$cargo_toml.bak" # Switch to local paths - for crate in collab collab-folder collab-document collab-database collab-plugins collab-user collab-entity collab-sync-protocol collab-persistence collab-importer; do + for crate in collab collab-folder collab-document collab-database collab-plugins collab-user collab-entity collab-sync-protocol collab-persistence; do sed -i '' \ -e "s#${crate} = { .*git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\".* }#${crate} = { path = \"$repo_path/$crate\" }#g" \ "$cargo_toml" @@ -35,3 +38,4 @@ fi # Switch dependencies in both Cargo.toml files switch_deps "$CARGO_TOML_1" "$REPO_RELATIVE_PATH_1" +switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" \ No newline at end of file diff --git a/frontend/scripts/tool/update_local_ai_rev.sh b/frontend/scripts/tool/update_local_ai_rev.sh deleted file mode 100755 index af24e0ba9f..0000000000 --- a/frontend/scripts/tool/update_local_ai_rev.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Ensure a new revision ID is provided -if [ "$#" -ne 1 ]; then - echo "Usage: $0 <new_revision_id>" - exit 1 -fi - -NEW_REV="$1" -echo "New revision: $NEW_REV" -directories=("rust-lib") - -for dir in "${directories[@]}"; do - echo "Updating $dir" - pushd "$dir" > /dev/null - - # Define the crates to update - crates=("af-local-ai" "af-plugin" "af-mcp") - - for crate in "${crates[@]}"; do - sed -i.bak "/^${crate}[[:alnum:]-]*[[:space:]]*=/s/rev = \"[a-fA-F0-9]\{6,40\}\"/rev = \"$NEW_REV\"/g" Cargo.toml - done - - # Construct the crates_to_update variable - crates_to_update="" - for crate in "${crates[@]}"; do - crates_to_update="$crates_to_update -p $crate" - done - - # Update all the specified crates at once - if [ -n "$crates_to_update" ]; then - echo "Updating crates: $crates_to_update" - cargo update $crates_to_update 2> /dev/null - fi - - popd > /dev/null -done diff --git a/frontend/scripts/white_label/code_white_label.sh b/frontend/scripts/white_label/code_white_label.sh deleted file mode 100644 index 1123a394ee..0000000000 --- a/frontend/scripts/white_label/code_white_label.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --company-name Set the custom company name" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --company-name \"MyCompany Ltd.\"" -} - -CUSTOM_COMPANY_NAME="" -CODE_FILE="appflowy_flutter/lib/workspace/application/notification/notification_service.dart" - -while [[ $# -gt 0 ]]; do - case $1 in - --company-name) - CUSTOM_COMPANY_NAME="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$CUSTOM_COMPANY_NAME" ]; then - echo "Error: Company name is required" - show_usage - exit 1 -fi - -if [ ! -f "$CODE_FILE" ]; then - echo "Error: Code file not found at $CODE_FILE" - exit 1 -fi - -echo "Replacing '_localNotifierAppName' value with '$CUSTOM_COMPANY_NAME' in code file..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -echo "Processing code file..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # First, escape any special characters in the company name - ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') - # Replace the _localNotifierAppName value with the custom company name - sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$ESCAPED_COMPANY_NAME'/" "$CODE_FILE" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $CODE_FILE with sed" - exit 1 - fi -else - # For Unix-like systems - sed $SED_INPLACE "s/const _localNotifierAppName = 'AppFlowy'/const _localNotifierAppName = '$CUSTOM_COMPANY_NAME'/" "$CODE_FILE" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $CODE_FILE with sed" - exit 1 - fi -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/font_white_label.sh b/frontend/scripts/white_label/font_white_label.sh deleted file mode 100644 index 412ee6b062..0000000000 --- a/frontend/scripts/white_label/font_white_label.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" - echo " --font-family Set the name of the font family" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --font-path \"/path/to/fonts\" --font-family \"CustomFont\"" -} - -FONT_PATH="" -FONT_FAMILY="" -TARGET_FONT_DIR="appflowy_flutter/assets/fonts/" -PUBSPEC_FILE="appflowy_flutter/pubspec.yaml" -BASE_APPEARANCE_FILE="appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart" - -while [[ $# -gt 0 ]]; do - case $1 in - --font-path) - FONT_PATH="$2" - shift 2 - ;; - --font-family) - FONT_FAMILY="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -# Validate required arguments -if [ -z "$FONT_PATH" ]; then - echo "Error: Font path is required" - show_usage - exit 1 -fi - -if [ -z "$FONT_FAMILY" ]; then - echo "Error: Font family name is required" - show_usage - exit 1 -fi - -# Check if source directory exists -if [ ! -d "$FONT_PATH" ]; then - echo "Error: Font directory not found at $FONT_PATH" - exit 1 -fi - -# Create target directory if it doesn't exist -mkdir -p "$TARGET_FONT_DIR" - -# Clean existing fonts in target directory -echo "Cleaning existing fonts in $TARGET_FONT_DIR..." -rm -rf "$TARGET_FONT_DIR"/* - -# Copy font files to target directory -echo "Copying font files from $FONT_PATH to $TARGET_FONT_DIR..." -found_fonts=false -for ext in ttf otf; do - if ls "$FONT_PATH"/*."$ext" >/dev/null 2>&1; then - cp "$FONT_PATH"/*."$ext" "$TARGET_FONT_DIR"/ 2>/dev/null && found_fonts=true - fi -done - -if [ "$found_fonts" = false ]; then - echo "Error: No font files (.ttf or .otf) found in source directory" - exit 1 -fi - -# Generate font configuration for pubspec.yaml -echo "Generating font configuration..." - -# Create temporary file for font configuration -TEMP_FILE=$(mktemp) - -{ - echo " # BEGIN: WHITE_LABEL_FONT" - echo " - family: $FONT_FAMILY" - echo " fonts:" - - # Generate entries for each font file - for font_file in "$TARGET_FONT_DIR"/*; do - filename=$(basename "$font_file") - echo " - asset: assets/fonts/$filename" - - # Try to detect font weight from filename - if [[ $filename =~ (Thin|ExtraLight|Light|Regular|Medium|SemiBold|Bold|ExtraBold|Black) ]]; then - case ${BASH_REMATCH[1]} in - "Thin") echo " weight: 100";; - "ExtraLight") echo " weight: 200";; - "Light") echo " weight: 300";; - "Regular") echo " weight: 400";; - "Medium") echo " weight: 500";; - "SemiBold") echo " weight: 600";; - "Bold") echo " weight: 700";; - "ExtraBold") echo " weight: 800";; - "Black") echo " weight: 900";; - esac - fi - - # Try to detect italic style from filename - if [[ $filename =~ Italic ]]; then - echo " style: italic" - fi - done - echo " # END: WHITE_LABEL_FONT" -} > "$TEMP_FILE" - -# Update pubspec.yaml -echo "Updating pubspec.yaml..." -if [ -f "$PUBSPEC_FILE" ]; then - # Create a backup of the original file - cp "$PUBSPEC_FILE" "${PUBSPEC_FILE}.bak" - - if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Windows-specific handling - # First, remove existing white label font configuration - awk '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/{ next } /# White-label font configuration will be added here/{ print; system("cat '"$TEMP_FILE"'"); next } 1' "$PUBSPEC_FILE" > "${PUBSPEC_FILE}.tmp" - - if [ $? -eq 0 ]; then - mv "${PUBSPEC_FILE}.tmp" "$PUBSPEC_FILE" - rm -f "${PUBSPEC_FILE}.bak" - else - echo "Error: Failed to update pubspec.yaml" - mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" - rm -f "${PUBSPEC_FILE}.tmp" - rm -f "$TEMP_FILE" - exit 1 - fi - else - # Unix-like systems handling - if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" - else - SED_INPLACE="-i ''" - fi - - # Remove existing white label font configuration - sed $SED_INPLACE '/# BEGIN: WHITE_LABEL_FONT/,/# END: WHITE_LABEL_FONT/d' "$PUBSPEC_FILE" - - # Add new font configuration - sed $SED_INPLACE "/# White-label font configuration will be added here/r $TEMP_FILE" "$PUBSPEC_FILE" - - if [ $? -ne 0 ]; then - echo "Error: Failed to update pubspec.yaml" - mv "${PUBSPEC_FILE}.bak" "$PUBSPEC_FILE" - rm -f "$TEMP_FILE" - exit 1 - fi - rm -f "${PUBSPEC_FILE}.bak" - fi -else - echo "Error: pubspec.yaml not found at $PUBSPEC_FILE" - rm -f "$TEMP_FILE" - exit 1 -fi - -# Update base_appearance.dart -echo "Updating base_appearance.dart..." -if [ -f "$BASE_APPEARANCE_FILE" ]; then - # Create a backup of the original file - cp "$BASE_APPEARANCE_FILE" "${BASE_APPEARANCE_FILE}.bak" - - if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Windows-specific handling - sed -i "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" - else - # Unix-like systems handling - sed -i '' "s/const defaultFontFamily = '.*'/const defaultFontFamily = '$FONT_FAMILY'/" "$BASE_APPEARANCE_FILE" - fi - - if [ $? -ne 0 ]; then - echo "Error: Failed to update base_appearance.dart" - mv "${BASE_APPEARANCE_FILE}.bak" "$BASE_APPEARANCE_FILE" - exit 1 - fi - rm -f "${BASE_APPEARANCE_FILE}.bak" -else - echo "Error: base_appearance.dart not found at $BASE_APPEARANCE_FILE" - exit 1 -fi - -# Cleanup -rm -f "$TEMP_FILE" - -echo "Font white labeling completed successfully!" diff --git a/frontend/scripts/white_label/i18n_white_label.sh b/frontend/scripts/white_label/i18n_white_label.sh deleted file mode 100644 index 60152d1630..0000000000 --- a/frontend/scripts/white_label/i18n_white_label.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --company-name Set the custom company name" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --company-name \"MyCompany Ltd.\"" -} - -CUSTOM_COMPANY_NAME="" -I18N_DIR="resources/translations" - -while [[ $# -gt 0 ]]; do - case $1 in - --company-name) - CUSTOM_COMPANY_NAME="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$CUSTOM_COMPANY_NAME" ]; then - echo "Error: Company name is required" - show_usage - exit 1 -fi - -if [ ! -d "$I18N_DIR" ]; then - echo "Error: Translation directory not found at $I18N_DIR" - exit 1 -fi - -echo "Replacing 'AppFlowy' with '$CUSTOM_COMPANY_NAME' in translation files..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -echo "Processing translation files..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - # Check if directory exists and has JSON files - if [ ! -d "$I18N_DIR" ] || [ -z "$(ls -A "$I18N_DIR"/*.json 2>/dev/null)" ]; then - echo "Error: No JSON files found in $I18N_DIR directory" - exit 1 - fi - - # Process each JSON file in the directory - for file in "$I18N_DIR"/*.json; do - echo "Updating $(basename "$file")" - # Use jq to replace AppFlowy with custom company name in values only - if command -v jq >/dev/null 2>&1; then - # Create a temporary file for the transformation - jq --arg company "$CUSTOM_COMPANY_NAME" 'walk(if type == "string" then gsub("AppFlowy"; $company) else . end)' "$file" > "${file}.tmp" - # Check if transformation was successful - if [ $? -eq 0 ]; then - mv "${file}.tmp" "$file" - else - echo "Error: Failed to process $file with jq" - rm -f "${file}.tmp" - exit 1 - fi - else - # Fallback to sed if jq is not available - # First, escape any special characters in the company name - ESCAPED_COMPANY_NAME=$(echo "$CUSTOM_COMPANY_NAME" | sed 's/[\/&]/\\&/g') - # Replace AppFlowy with the custom company name in JSON values - sed $SED_INPLACE 's/\(".*"\): *"\(.*\)AppFlowy\(.*\)"/\1: "\2'"$ESCAPED_COMPANY_NAME"'\3"/g' "$file" - if [ $? -ne 0 ]; then - echo "Error: Failed to process $file with sed" - exit 1 - fi - fi - done -else - for file in $(find "$I18N_DIR" -name "*.json" -type f); do - echo "Updating $(basename "$file")" - # Use jq to only replace values, not keys - if command -v jq >/dev/null 2>&1; then - jq 'walk(if type == "string" then gsub("AppFlowy"; "'"$CUSTOM_COMPANY_NAME"'") else . end)' "$file" > "$file.tmp" && mv "$file.tmp" "$file" - else - # Fallback to sed with a more specific pattern that targets values but not keys - sed $SED_INPLACE 's/: *"[^"]*AppFlowy[^"]*"/: "&"/g; s/: *"&"/: "'"$CUSTOM_COMPANY_NAME"'"/g' "$file" - # Fix any double colons that might have been introduced - sed $SED_INPLACE 's/: *: */: /g' "$file" - fi - done -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/icon_white_label.sh b/frontend/scripts/white_label/icon_white_label.sh deleted file mode 100644 index ca70bc1661..0000000000 --- a/frontend/scripts/white_label/icon_white_label.sh +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --icon-path Set the path to the folder containing application icons (.svg files)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --icon-path \"/path/to/icons_folder\"" -} - -NEW_ICON_PATH="" -ICON_DIR="resources/flowy_icons" -ICON_NAME_NEED_REPLACE=("app_logo.svg" "ai_chat_logo.svg" "app_logo_with_text_light.svg" "app_logo_with_text_dark.svg") - -while [[ $# -gt 0 ]]; do - case $1 in - --icon-path) - NEW_ICON_PATH="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$NEW_ICON_PATH" ]; then - echo "Error: Icon path is required" - show_usage - exit 1 -fi - -if [ ! -d "$NEW_ICON_PATH" ]; then - echo "Error: New icon directory not found at $NEW_ICON_PATH" - exit 1 -fi - -if [ ! -d "$ICON_DIR" ]; then - echo "Error: Icon directory not found at $ICON_DIR" - exit 1 -fi - -echo "Replacing icons..." - -echo "Processing icon files..." -if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]]; then - for subdir in "${ICON_DIR}"/*/; do - if [ -d "$subdir" ]; then - echo "Checking subdirectory: $(basename "$subdir")" - for file in "${subdir}"*.svg; do - if [ -f "$file" ] && [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - new_icon="${NEW_ICON_PATH}/$(basename "$file")" - if [ -f "$new_icon" ]; then - echo "Updating: $(basename "$subdir")/$(basename "$file")" - cp "$new_icon" "$file" - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") in $(basename "$subdir") with new icon" - else - echo "Error: Failed to replace $(basename "$file") in $(basename "$subdir")" - exit 1 - fi - else - echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" - fi - fi - done - fi - done -else - for file in $(find "$ICON_DIR" -name "*.svg" -type f); do - if [[ " ${ICON_NAME_NEED_REPLACE[@]} " =~ " $(basename "$file") " ]]; then - new_icon="${NEW_ICON_PATH}/$(basename "$file")" - if [ -f "$new_icon" ]; then - echo "Updating: $(basename "$file")" - cp "$new_icon" "$file" - if [ $? -eq 0 ]; then - echo "Successfully replaced $(basename "$file") with new icon" - else - echo "Error: Failed to replace $(basename "$file")" - exit 1 - fi - else - echo "Warning: New icon file $(basename "$file") not found in $NEW_ICON_PATH" - fi - fi - done -fi - -echo "Replacement complete!" diff --git a/frontend/scripts/white_label/resources/my_company_logo.ico b/frontend/scripts/white_label/resources/my_company_logo.ico deleted file mode 100644 index c922a6b36d..0000000000 Binary files a/frontend/scripts/white_label/resources/my_company_logo.ico and /dev/null differ diff --git a/frontend/scripts/white_label/resources/my_company_logo.png b/frontend/scripts/white_label/resources/my_company_logo.png deleted file mode 100644 index 8f50872743..0000000000 Binary files a/frontend/scripts/white_label/resources/my_company_logo.png and /dev/null differ diff --git a/frontend/scripts/white_label/resources/my_company_logo.svg b/frontend/scripts/white_label/resources/my_company_logo.svg deleted file mode 100644 index c06bf17cb4..0000000000 --- a/frontend/scripts/white_label/resources/my_company_logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><g fill="none" fill-rule="nonzero"><path d="M24 0v24H0V0zM12.593 23.258l-.011.002-.071.035-.02.004-.014-.004-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01-.017.428.005.02.01.013.104.074.015.004.012-.004.104-.074.012-.016.004-.017-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113-.013.002-.185.093-.01.01-.003.011.018.43.005.012.008.007.201.093c.012.004.023 0 .029-.008l.004-.014-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014-.034.614c0 .012.007.02.017.024l.015-.002.201-.093.01-.008.004-.011.017-.43-.003-.012-.01-.01z"/><path fill="#09244B" d="M17.42 3a2 2 0 0 1 1.649.868l.087.14L22.49 9.84a2 2 0 0 1-.208 2.283l-.114.123-9.283 9.283a1.25 1.25 0 0 1-1.666.091l-.102-.09-9.283-9.284a2 2 0 0 1-.4-2.257l.078-.15 3.333-5.832a2 2 0 0 1 1.572-1.001L6.58 3zm0 2H6.58l-3.333 5.833L12 19.586l8.753-8.753zM7.293 9.293a1 1 0 0 1 1.32-.083l.094.083L12 12.586l3.293-3.293a1 1 0 0 1 1.497 1.32l-.083.094-3.823 3.823a1.25 1.25 0 0 1-1.666.091l-.102-.09-3.823-3.824a1 1 0 0 1 0-1.414"/></g></svg> \ No newline at end of file diff --git a/frontend/scripts/white_label/white_label.sh b/frontend/scripts/white_label/white_label.sh deleted file mode 100644 index 8ecd187210..0000000000 --- a/frontend/scripts/white_label/white_label.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/bin/bash - -# Default values -APP_NAME="AppFlowy" -APP_IDENTIFIER="com.appflowy.appflowy" -COMPANY_NAME="AppFlowy Inc." -COPYRIGHT="Copyright © 2025 AppFlowy Inc." -ICON_PATH="" -WINDOWS_ICON_PATH="" -FONT_PATH="" -FONT_FAMILY="" -PLATFORMS=("windows" "linux" "macos" "ios" "android") - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --app-name Set the application name" - echo " --app-identifier Set the application identifier" - echo " --company-name Set the company name" - echo " --copyright Set the copyright information" - echo " --icon-path Set the path to the application icon (.svg)" - echo " --windows-icon-path Set the path to the windows application icon (.ico)" - echo " --font-path Set the path to the folder containing font files (.ttf or .otf files)" - echo " --font-family Set the name of the font family" - echo " --platforms Comma-separated list of platforms to white label (windows,linux,macos,ios,android)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" - echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" - echo " --platforms \"windows,linux,macos\" \\" - echo " --windows-icon-path \"./assets/icons/mycompany.ico\" \\" - echo " --icon-path \"./assets/icons/\" \\" - echo " --font-path \"./assets/fonts/\" --font-family \"CustomFont\"" -} - -while [[ $# -gt 0 ]]; do - case $1 in - --app-name) - APP_NAME="$2" - shift 2 - ;; - --app-identifier) - APP_IDENTIFIER="$2" - shift 2 - ;; - --company-name) - COMPANY_NAME="$2" - shift 2 - ;; - --copyright) - COPYRIGHT="$2" - shift 2 - ;; - --icon-path) - ICON_PATH="$2" - shift 2 - ;; - --windows-icon-path) - WINDOWS_ICON_PATH="$2" - shift 2 - ;; - --font-path) - FONT_PATH="$2" - shift 2 - ;; - --font-family) - FONT_FAMILY="$2" - shift 2 - ;; - --platforms) - IFS=',' read -ra PLATFORMS <<< "$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$APP_NAME" ] || [ -z "$APP_IDENTIFIER" ] || [ -z "$COMPANY_NAME" ] || [ -z "$COPYRIGHT" ] || [ -z "$ICON_PATH" ]; then - echo "Error: All parameters are required" - show_usage - exit 1 -fi - -if [ ! -d "$ICON_PATH" ]; then - echo "Error: Icon directory not found at $ICON_PATH" - exit 1 -fi - -if [ ! -f "$WINDOWS_ICON_PATH" ]; then - echo "Error: Windows icon file not found at $WINDOWS_ICON_PATH" - exit 1 -fi - -run_platform_script() { - local platform=$1 - local script_path="scripts/white_label/${platform}_white_label.sh" - - if [ ! -f "$script_path" ]; then - echo -e "\033[31mWarning: White label script not found for platform: $platform\033[0m" - return - fi - - echo -e "\033[32mRunning white label script for $platform...\033[0m" - bash "$script_path" \ - --app-name "$APP_NAME" \ - --app-identifier "$APP_IDENTIFIER" \ - --company-name "$COMPANY_NAME" \ - --copyright "$COPYRIGHT" \ - --icon-path "$WINDOWS_ICON_PATH" -} - -echo -e "\033[32mRunning i18n white label script...\033[0m" -bash "scripts/white_label/i18n_white_label.sh" --company-name "$COMPANY_NAME" - -echo -e "\033[32mRunning icon white label script...\033[0m" -bash "scripts/white_label/icon_white_label.sh" --icon-path "$ICON_PATH" - -echo -e "\033[32mRunning code white label script...\033[0m" -bash "scripts/white_label/code_white_label.sh" --company-name "$COMPANY_NAME" - -# Run font white label script if font parameters are provided -if [ ! -z "$FONT_PATH" ] && [ ! -z "$FONT_FAMILY" ]; then - echo -e "\033[32mRunning font white label script...\033[0m" - bash "scripts/white_label/font_white_label.sh" \ - --font-path "$FONT_PATH" \ - --font-family "$FONT_FAMILY" -fi - -for platform in "${PLATFORMS[@]}"; do - run_platform_script "$platform" -done - -echo -e "\033[32mWhite labeling process completed successfully!\033[0m" diff --git a/frontend/scripts/white_label/windows_white_label.sh b/frontend/scripts/white_label/windows_white_label.sh deleted file mode 100644 index 58801424ff..0000000000 --- a/frontend/scripts/white_label/windows_white_label.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -APP_NAME="AppFlowy" -APP_IDENTIFIER="com.appflowy.appflowy" -COMPANY_NAME="AppFlowy Inc." -COPYRIGHT="Copyright © 2025 AppFlowy Inc." -ICON_PATH="" - -show_usage() { - echo "Usage: $0 [options]" - echo "Options:" - echo " --app-name Set the application name" - echo " --app-identifier Set the application identifier" - echo " --company-name Set the company name" - echo " --copyright Set the copyright information" - echo " --icon-path Set the path to the application icon (.ico file)" - echo " --help Show this help message" - echo "" - echo "Example:" - echo " $0 --app-name \"MyCompany\" --app-identifier \"com.mycompany.mycompany\" \\" - echo " --company-name \"MyCompany Ltd.\" --copyright \"Copyright © 2025 MyCompany Ltd.\" \\" - echo " --icon-path \"./assets/icons/company.ico\"" -} - -while [[ $# -gt 0 ]]; do - case $1 in - --app-name) - APP_NAME="$2" - shift 2 - ;; - --app-identifier) - APP_IDENTIFIER="$2" - shift 2 - ;; - --company-name) - COMPANY_NAME="$2" - shift 2 - ;; - --copyright) - COPYRIGHT="$2" - shift 2 - ;; - --icon-path) - ICON_PATH="$2" - shift 2 - ;; - --output-dir) - OUTPUT_DIR="$2" - shift 2 - ;; - --help) - show_usage - exit 0 - ;; - *) - echo "Unknown option: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$APP_NAME" ]; then - echo -e "\033[31mError: Application name is required\033[0m" - exit 1 -fi - -if [ -z "$APP_IDENTIFIER" ]; then - echo -e "\033[31mError: Application identifier is required\033[0m" - exit 1 -fi - -if [ -z "$COMPANY_NAME" ]; then - echo -e "\033[31mError: Company name is required\033[0m" - exit 1 -fi - -if [ -z "$COPYRIGHT" ]; then - echo -e "\033[31mError: Copyright information is required\033[0m" - exit 1 -fi - -if [ -z "$ICON_PATH" ]; then - echo -e "\033[31mError: Icon path is required\033[0m" - exit 1 -fi - -echo "Starting Windows application customization..." - -if sed --version >/dev/null 2>&1; then - SED_INPLACE="-i" -else - SED_INPLACE="-i ''" -fi - -update_runner_files() { - runner_dir="appflowy_flutter/windows/runner" - - if [ -f "$runner_dir/Runner.rc" ]; then - sed $SED_INPLACE "s/VALUE \"CompanyName\", .*$/VALUE \"CompanyName\", \"$COMPANY_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"FileDescription\", .*$/VALUE \"FileDescription\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"InternalName\", .*$/VALUE \"InternalName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"OriginalFilename\", .*$/VALUE \"OriginalFilename\", \"$APP_NAME.exe\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"LegalCopyright\", .*$/VALUE \"LegalCopyright\", \"$COPYRIGHT\"/" "$runner_dir/Runner.rc" - sed $SED_INPLACE "s/VALUE \"ProductName\", .*$/VALUE \"ProductName\", \"$APP_NAME\"/" "$runner_dir/Runner.rc" - echo -e "Runner.rc updated successfully" - else - echo -e "\033[31mRunner.rc file not found\033[0m" - fi -} - -update_icon() { - if [ ! -z "$ICON_PATH" ] && [ -f "$ICON_PATH" ]; then - app_icon_path="appflowy_flutter/windows/runner/resources/app_icon.ico" - cp "$ICON_PATH" "$app_icon_path" - echo -e "Application icon updated successfully" - else - echo -e "\033[31mApplication icon file not found\033[0m" - fi -} - -update_cmake_lists() { - cmake_file="appflowy_flutter/windows/CMakeLists.txt" - if [ -f "$cmake_file" ]; then - sed $SED_INPLACE "s/set(BINARY_NAME .*)$/set(BINARY_NAME \"$APP_NAME\")/" "$cmake_file" - echo -e "CMake configuration updated successfully" - else - echo -e "\033[31mCMake configuration file not found\033[0m" - fi -} - -update_main_cpp() { - main_cpp_file="appflowy_flutter/windows/runner/main.cpp" - if [ -f "$main_cpp_file" ]; then - sed $SED_INPLACE "s/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"AppFlowyMutex\");/HANDLE hMutexInstance = CreateMutex(NULL, TRUE, L\"${APP_NAME}Mutex\");/" "$main_cpp_file" - sed $SED_INPLACE "s/HWND handle = FindWindowA(NULL, \"AppFlowy\");/HWND handle = FindWindowA(NULL, \"$APP_NAME\");/" "$main_cpp_file" - sed $SED_INPLACE "s/if (window.SendAppLinkToInstance(L\"AppFlowy\")) {/if (window.SendAppLinkToInstance(L\"$APP_NAME\")) {/" "$main_cpp_file" - sed $SED_INPLACE "s/if (!window.Create(L\"AppFlowy\", origin, size)) {/if (!window.Create(L\"$APP_NAME\", origin, size)) {/" "$main_cpp_file" - echo -e "main.cpp updated successfully" - else - echo -e "\033[31mMain.cpp file not found\033[0m" - fi -} - -echo "Applying customizations..." -update_runner_files -update_icon -update_cmake_lists -update_main_cpp - -echo "Windows application customization completed successfully!" diff --git a/frontend/scripts/windows_installer/inno_setup_config.iss b/frontend/scripts/windows_installer/inno_setup_config.iss index ab16c55ffb..b584f8df53 100644 --- a/frontend/scripts/windows_installer/inno_setup_config.iss +++ b/frontend/scripts/windows_installer/inno_setup_config.iss @@ -14,7 +14,7 @@ VersionInfoVersion={#AppVersion} UsePreviousAppDir=no [Files] -Source: "AppFlowy\AppFlowy.exe"; DestDir: "{app}"; DestName: "AppFlowy.exe"; Flags: ignoreversion +Source: "AppFlowy\AppFlowy.exe";DestDir: "{app}";DestName: "AppFlowy.exe" Source: "AppFlowy\*";DestDir: "{app}" Source: "AppFlowy\data\*";DestDir: "{app}\data\"; Flags: recursesubdirs diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 1341b40643..2d09236b50 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -13,8 +13,6 @@ "fa", "fr-CA", "fr-FR", - "ga-IE", - "he", "hu-HU", "id-ID", "it-IT", @@ -25,7 +23,6 @@ "pt-PT", "ru-RU", "sv-SE", - "th-TH", "tr-TR", "uk-UA", "vi", @@ -46,4 +43,4 @@ "@:" ] } -} \ No newline at end of file +}